通过算术运算符重载中的临时变量最小化堆栈分配:安全 return 方法

Minimizing stack allocation by way of temporaries in arithmetic operator overloads : Safe return method

我正在研究 EDDSA 数字签名的实现,由于计算中使用的值的大小可以达到 521 位(缩放器)并且点具有三个缩放器(投影坐标),因此通过复制传递值是昂贵的在堆栈 space 和参数副本方面。为了尽可能多地消除副本,我将参数作为 'const &' 传递,创建一个局部变量,执行计算并将结果放入局部变量,然后 return 局部变量。注意:代码是 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 &、&&),但结果总是需要额外的堆栈 space。如果传递了一个临时的(Point &&),它可以用来保存结果和保存堆栈space。这里的问题是如何正确return结果?

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
}

我了解第二种情况(return 点 &&)如果调用者处理不当会导致悬空引用。

我已经阅读了关于 returning && 值的类似问题和答案,但我没有发现任何特定于 returning 一个 && 参数作为值的内容(上面的第一种情况)。

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

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

此外,如果重载运算符被声明为模板,我理解“&&”参数被区别对待以支持完美转发。

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

  return lhs ;
}

与非模板定义相比,'&& 参数的处理方式究竟有何不同(如果是)?return 结果的正确方法是什么?

我会提供 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)。

此致, 调频.

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

在你的情况下你可以这样做

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

但在我看来,由于可能存在不必要的悬空引用,因此更容易出错,因此我将重点关注 return 按值。我鼓励你衡量性能差异 none 越少,看看是否有任何差异。


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


在第一种情况下,当您声明一个 res 值,然后您声明 return,您可能可以依赖编译器优化,即没有为 [=12= 分配堆栈 space ],但使用调用者为 return 值分配的 space。这类似于 NRVO. Clang does this even with -O0(在 LLVM-IR 中查找(缺少)alloca)。

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

请注意,通常仅当函数中的所有 return 语句都是 return ret;.

时才能保证此优化

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


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

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

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

在这个使用 clang 和 -O1 的例子中,只有一个堆栈分配发生,p,其余的被省略(参见 demo)。