代码之家  ›  专栏  ›  技术社区  ›  Daniel Plainview

什么时候更喜欢常量左值引用而不是右值引用模板

  •  4
  • Daniel Plainview  · 技术社区  · 8 年前

    当前正在读取cpr请求库的代码库: https://github.com/whoshuu/cpr/blob/master/include/cpr/api.h

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

    据我所知,右值引用、模板和转发的好处是,被包装的函数调用将按右值引用而不是按值获取其参数。这样可以避免不必要的复制。它还可以防止由于引用推导而产生大量重载。

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

    编译器: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,我们可以看到计数器,右值转发较慢。为什么会这样?引用演绎不应该总是产生对左值的引用吗?如果是这样的话,在性能方面,它不应该或多或少等同于常量左值引用吗?

    #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;
    
    }
    
    1 回复  |  直到 8 年前
        1
  •  3
  •   Sergey Klevtsov    8 年前

    here are 与代码的结果略有不同。如评论中所述,编译器及其设置非常重要。特别是,您可能会注意到,所有情况下都有类似的运行时,但第一种情况除外,它的速度大约是第二种情况的两倍。

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

    1) 呼叫时 wrap1("test") ,因为该函数的签名需要 const std::string & std::string 每次通话(即。 n 次),其中包含值的副本*。然后将该临时文件的常量引用传递到 func1 是从它构造的,它再次涉及一个副本(因为它是常量引用,所以不能从中移动,尽管它实际上是临时的)。即使函数按值返回,但由于RVO,如果使用返回值,该副本将保证被忽略。在这种情况下,不使用返回值,我不完全确定该标准是否允许编译器优化 temp 标准::字符串 在这种情况下执行两次。

    wrap2("test") ,参数类型为 const char[5] ,并将其作为右值引用转发到 func2 标准::字符串 来自a的构造函数 const char[] 被称为复制值。导出的模板参数类型 T const char[5] && const 成为一名 标准::字符串 常量字符[5]

    wrap1(arg) const string & 通过链,在中调用一个副本构造函数 功能1 .

    4) 呼叫时 wrap2(arg) T

    5) 我假设您的测试旨在证明当需要在调用链的底部制作参数副本时,完美转发的优势(因此创建 临时雇员 ). 在这种情况下,您需要替换 "test" 前两种情况下的论点 std::string("test") 为了真正拥有 std::string && 并将你的完美转发改为 std::forward<T>(arg) ,如评论中所述。那样的话 the results

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

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

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


    *由于短字符串优化,复制构造函数可能涉及也可能不涉及动态内存分配;感谢ytoledano在评论中提出这一点。此外,我在整个回答中都隐含着这样的假设,即拷贝比移动要昂贵得多,但情况并非总是如此。