【问题标题】:When to prefer const lvalue reference over rvalue reference templates何时更喜欢 const 左值引用而不是右值引用模板
【发布时间】:2018-02-20 10:40:17
【问题描述】:

目前正在阅读 cpr requests 库的代码库: https://github.com/whoshuu/cpr/blob/master/include/cpr/api.h

注意到这个库的接口经常使用完美转发。只是学习右值引用,所以这对我来说都是相对较新的。

据我了解,右值引用、模板化和转发的好处在于,被包裹的函数调用将通过右值引用而不是值来获取其参数。这避免了不必要的复制。它还可以防止由于引用扣除而不得不生成一堆重载。

但是,据我了解, const 左值引用本质上是做同样的事情。它可以防止重载并通过引用传递所有内容。需要注意的是,如果被包装的函数采用非常量引用,它将无法编译。

但是,如果调用堆栈中的所有内容都不需要非常量引用,那么为什么不直接通过 const 左值引用传递所有内容呢?

我想我的主要问题是,什么时候应该使用其中一个来获得最佳性能?尝试使用以下代码对此进行测试。得到了以下比较一致的结果:

编译器:gcc 6.3 操作系统:Debian GNU/Linux 9

<<<<
Passing rvalue!
const l value: 2912214
rvalue forwarding: 2082953
Passing lvalue!
const l value: 1219173
rvalue forwarding: 1585913
>>>>

这些结果在运行之间保持相当一致。似乎对于右值 arg,const l 值签名稍微慢一些,尽管我不完全确定为什么,除非我误解了这一点并且 const 左值引用实际上复制了右值。

对于左值 arg,我们看到计数器,右值转发更慢。为什么会这样?引用推导不应该总是产生对左值的引用吗?如果是这样的话,它在性能方面不应该或多或少等同于 const 左值引用吗?

#include <iostream>
#include <string>
#include <utility>
#include <time.h>

std::string func1(const std::string& arg) {
    std::string test(arg);
    return test;
}

template <typename T>
std::string func2(T&& arg) {
    std::string test(std::forward<T>(arg));
    return test;
}

void wrap1(const std::string& arg) {
    func1(arg);
}

template <typename T>
void wrap2(T&& arg) {
    func2(std::forward<T>(arg));
}

int main()
{
     auto n = 100000000;

     /// Passing rvalue
     std::cout << "Passing rvalue!" << std::endl;

     // Test const l value
     auto t = clock();
     for (int i = 0; i < n; ++i)
         wrap1("test");
     std::cout << "const l value: " << clock() - t << std::endl;

     // Test rvalue forwarding
     t = clock();
     for (int i = 0; i < n; ++i)
         wrap2("test");
     std::cout << "rvalue forwarding: " <<  clock() - t << std::endl;

     std::cout << "Passing lvalue!" << std::endl;

     /// Passing lvalue
     std::string arg = "test";

     // Test const l value
     t = clock();
     for (int i = 0; i < n; ++i)
         wrap1(arg);
     std::cout << "const l value: " << clock() - t << std::endl;

     // Test rvalue forwarding
     t = clock();
     for (int i = 0; i < n; ++i)
         wrap2(arg);
     std::cout << "rvalue forwarding: " << clock() - t << std::endl;

}

【问题讨论】:

  • 为了清楚起见,转发引用(T&amp;&amp; - 作为演绎上下文中的模板类型参数(又名函数参数)与 r 值引用不同(如 @987654325 @)。前者将绑定到任何东西,后者仅绑定到临时对象。
  • 哦,当您发布基准测试结果时,请同时添加编译器,它的版本和编译标志。
  • std::forward&lt;T&amp;&gt;(arg) 应该是 std::forward&lt;T&gt;(arg)。很抱歉向 cmets 发送垃圾邮件。
  • 重要的区别是你假设的函数调用参数是否会被函数体以外的任何东西再次查看。如果没有,当需要私有副本时,转发允许您将值移动到函数内的任何潜在消费者,而您建议的替代方案总是会产生“完整”副本。如果函数不消耗任何东西,那么就没有区别。

标签: c++ c++11 rvalue-reference perfect-forwarding


【解决方案1】:

首先,here are 的结果与您的代码略有不同。在 cmets 中提到,编译器及其设置非常重要。特别是,您可能会注意到所有情况的运行时间都相似,除了第一种情况,它的速度大约是原来的两倍。

Passing rvalue!
const l value: 1357465
rvalue forwarding: 669589
Passing lvalue!
const l value: 744105
rvalue forwarding: 713189

让我们看看每种情况下到底发生了什么。

1) 调用wrap1("test") 时,由于该函数的签名需要const std::string &amp;,因此您传递的char 数组将在每次调用时隐式转换为临时std::string 对象(即n 次),这涉及值的副本*。然后,对该临时对象的 const 引用将被传递到 func1,其中另一个 std::string 由它构造,这又涉及一个副本(因为它是一个 const 引用,它不能被移出,尽管它实际上是一个临时对象) .即使函数按值返回,由于 RVO,如果使用返回值,则可以保证删除该副本。在这种情况下,不使用返回值,我不完全确定标准是否允许编译器优化 temp 的构造。我怀疑不是,因为一般来说,这样的结构可能会产生明显的副作用(你的结果表明它没有得到优化)。综上所述,本例中对std::string进行了两次完整的构造和销毁。

2) 调用wrap2("test") 时,参数类型为const char[5],并作为右值引用一直转发到func2,其中调用来自const char[]std::string 构造函数复制价值。模板参数T 的推导类型是const char[5] &amp;&amp;,并且很明显,尽管它是一个右值引用,但它不能被移出(因为两者都是const不是std::string )。与前一种情况相比,字符串的构造/销毁每次调用只发生一次(const char[5] 文字始终在内存中,不会产生开销)。

3) 当调用wrap1(arg) 时,您通过链传递一个左值作为const string &amp;,并在func1 中调用一个复制构造函数。

4) 调用wrap2(arg)时,与上例类似,因为T的推导类型为const std::string &amp;

5) 我假设您的测试旨在证明当需要在调用链底部制作参数副本时完美转发的优势(因此创建了temp)。在这种情况下,您需要将前两种情况中的"test" 参数替换为std::string("test"),以便真正拥有std::string &amp;&amp; 参数,并将完美转发设置为std::forward&lt;T&gt;(arg),如cmets 中所述。在这种情况下,the results 是:

Passing rvalue!
const l value: 1314630
rvalue forwarding: 595084
Passing lvalue!
const l value: 712461
rvalue forwarding: 720338

这与我们之前的类似,但现在实际上调用了一个移动构造函数。

我希望这有助于解释结果。可能还有一些与函数调用内联和其他编译器优化相关的其他问题,这将有助于解释案例 2-4 之间的较小差异。

关于使用哪种方法的问题,我建议阅读 Scott Meyer 的“Effective Modern C++”项目 23-30。抱歉,请参考书籍而不是直接答案,但没有灵丹妙药,最佳选择始终取决于具体情况,因此最好只了解每个设计决策的权衡。


* 由于短字符串优化,复制构造函数可能涉及也可能不涉及动态内存分配;感谢 ytoledano 在 cmets 中提出这个问题。此外,我在整个答案中都隐含地假设副本比移动要昂贵得多,但情况并非总是如此。

【讨论】:

  • 关于调用wrap("test") - 一个临时创建的不一定是动态分配 - 字符串实现可能正在使用短字符串优化,它将短字符串保留在堆栈中。
  • @ytoledano 感谢您的评论!我已经调整了答案以提及这一点。在这种情况下,gcc 似乎肯定使用 SSO
  • @SergeyKlevtsov 感谢谢尔盖的回答,这很好地解释了用法。巨大的帮助。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-12-06
  • 1970-01-01
  • 1970-01-01
  • 2017-04-13
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多