【问题标题】:Minimizing stack allocation by way of temporaries in arithmetic operator overloads : Safe return method通过算术运算符重载中的临时变量最小化堆栈分配:安全返回方法
【发布时间】:2020-11-27 16:44:51
【问题描述】:

我正在研究 EDDSA 数字签名的实现,并且由于计算中使用的值的大小可以高达 521 位(缩放器)并且点具有三个缩放器(投影坐标),因此通过副本传递值在条款上是昂贵的堆栈空间和参数副本。为了消除尽可能多的副本,我将参数作为“const &”传递,创建一个局部变量,执行计算并将结果放入局部变量中,然后返回局部变量。注意:代码是 C++17 (clang)。

struct Point { /* ... */ } ;

friend Point operator + (Point const & lhs , Point const & rhs)
{
  Point  res ; 

  /* Do addition and place result into 'res' */

  return res ;
}

这适用于所有可能的参数类型(&、const &、&&),但总是需要额外的堆栈空间来存储结果。如果传递了一个临时值(Point &&),它可以用来保存结果并节省堆栈空间。这里的问题是如何正确返回结果?

friend Point operator + (Point && lhs , Point const & rhs)
{
  /* Do addition and place result into 'lhs' */

  return lhs ; // Not sure what is exactly returned here : Danger of dangling reference or ok?
}

/* OR */

friend Point && operator + (Point && lhs , Point const & rhs)
{
  /* Do addition and place result into 'lhs' */

  return std::move (lhs) ; // Danger of dangling reference to caller if not used correctly
}

我了解第二种情况(返回点 &&)如果调用者未正确处理,可能会导致引用悬空。

我已阅读有关返回 && 值的类似问题和答案,但我没有找到任何特定于将 && 参数作为值返回的内容(上面的第一种情况)。

C++ 究竟如何定义第一种情况的返回值行为(当参数为 Point && 时返回 Point)?

是否有更好(即:高效且安全)的方法来消除副本和重用临时参数内存?

此外,如果重载的运算符被声明为模板,我理解 '&&' 参数的处理方式不同以支持完美转发。

template <class Point> friend Point operator + (Point && lhs , Point const & rhs)
{
  /* Do addition and place result into 'lhs' */

  return lhs ;
}

'&& 参数与非模板定义中的处理方式有何不同(如果有的话),返回结果的正确方法是什么?

【问题讨论】:

  • 与friend 相关吗?我宁愿删除它,因为你只能在类定义中使用它,但你把它放在外面
  • 在第一种情况下,它总是返回一个新的Point。没有悬空引用,因为它不是引用——它为返回值构造了一个新对象。
  • 是的,朋友在我的库中是相关的,我直接在我的类中声明重载运算符,因为它们需要访问私有变量。此外,通过将它们直接放在里面,它消除了大量的“模板”限定前缀。

标签: c++ overloading rvalue


【解决方案1】:

如果您从函数中按值返回,则不必担心悬空引用,因为您正在处理值并在必要时进行复制。因此,除非您返回参考,否则您不必担心。作为参数传递的引用在调用者表达式的末尾无效,因此通过引用返回参数之一可能是安全的。

你可以这样做

Point &&operator + (Point &&lhs, Point const &rhs)
{
    // calculations modifying lhs
    // ...
    return lhs;
}

但在我看来,由于可能会出现不必要的悬空引用,因此更容易出错,因此我将专注于按值返回。我鼓励您测量性能差异,看看是否有任何差异。


当按值传递时,函数参数和返回值通常在堆栈上传递大型结构(在 windows x64 上 >8 字节,在 linux x64 上 >16 字节)。对于两者,调用者分配必要的堆栈空间。这对于了解幕后发生的事情很重要。


在第一种情况下,当您声明稍后返回的 res 值时,您可能可以依赖编译器优化,即没有为 res 分配堆栈空间,但调用者为返回值分配的空间是用过的。这类似于NRVO。 Clang does this even with -O0(在 LLVM-IR 中查找(缺少)allocas)。

考虑到这一点,无需提供可以采用右值引用的重载,因为使用 res 变量不会占用任何额外的堆栈空间。

请注意,这种优化通常只有在函数中的所有返回语句都是return ret; 时才能保证。


对于参数,避免堆栈分配的最佳方法是不按值传递,因此在调用函数时不必制作冗余副本。在这种情况下使用const &amp; 是理想的,因为它可以接受右值和左值。


为了进一步扩展使用res 变量的“类似NRVO”的行为,让我们举这个例子:

Point create_point()
{
    Point res;
    // fill res with values
    return res;
}

int main()
{
    Point p;
    p = create_point();
}

在这个使用带有-O1 的clang 的示例中,对于p,只发生了一次堆栈分配,其余的都被省略了(参见demo)。

【讨论】:

    【解决方案2】:

    我会提供 2 个重载:

    Point operator + (Point lhs , Point const & rhs){
        return lhs+=rhs;
    };
    
    Point&& operator + (Point const & lhs , Point&& rhs){
        return std::move(rhs+=lhs);
    };
    
    auto x=p1+p2;
    

    并依靠复制省略(RVO,NRVO)。

    问候, 调频。

    【讨论】:

      猜你喜欢
      • 2012-10-10
      • 1970-01-01
      • 1970-01-01
      • 2019-03-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-04-14
      • 1970-01-01
      相关资源
      最近更新 更多