返回 std::pair 与通过非常量引用传递

Posted

技术标签:

【中文标题】返回 std::pair 与通过非常量引用传递【英文标题】:Returning std::pair versus passing by non-const reference 【发布时间】:2009-12-08 19:09:23 【问题描述】:

为什么返回 std::pairboost::tuple 比通过引用返回效率低得多?在我测试过的实际代码中,通过非常量引用而不是在内核中通过std::pair 设置数据可以将代码速度提高 20%。

作为一个实验,我研究了三个最简单的场景,包括将两个(预定义的)整数与两个整数相加:

    使用内部内联函数通过引用修改整数

    使用两个内联函数按值返回整数

    使用内部内联函数返回一个 std::pair 并复制到结果中。

使用g++ -c $x -Wall -Wextra -O2 -S 编译会产生相同的汇编代码,用于按引用传递和按值返回整数:

__Z7getPairiRiS_:
LFB19:
    pushq   %rbp
LCFI0:
    leal    1023(%rdi), %eax
    addl    $31, %edi
    movl    %eax, (%rsi)
    movq    %rsp, %rbp
LCFI1:
    movl    %edi, (%rdx)
    leave
    ret

(通过参考代码传递:

#include <utility>

inline void myGetPair(const int inp, int& a, int& b) 
    a = 1023 + inp;
    b = 31 + inp;


void getPair(const int inp, int& a, int& b) 
    myGetPair(inp, a, b);

使用单独的右值:

#include <utility>

inline int myGetPair1(int inp) 
    return 1023 + inp;


inline int myGetPair2(int inp) 
    return 31 + inp;


void getPair(const int inp, int& a, int& b) 
    a = myGetPair1(inp);
    b = myGetPair2(inp);

)

然而,使用 std::pair 会增加五个额外的汇编语句:

__Z7getPairiRiS_:
LFB18:
    leal    31(%rdi), %eax
    addl    $1023, %edi
    pushq   %rbp
LCFI0:
    salq    $32, %rax
    movq    %rsp, %rbp
LCFI1:
    orq %rdi, %rax
    movq    %rax, %rcx
    movl    %eax, (%rsi)
    shrq    $32, %rcx
    movl    %ecx, (%rdx)
    leave
    ret

代码几乎和前面的例子一样简单:

#include <utility>

inline std::pair<int,int> myGetPair(int inp) 
    return std::make_pair(1023 + inp, 31 + inp);


void getPair(const int inp, int& a, int& b) 
    std::pair<int,int> result = myGetPair(inp);

    a = result.first;
    b = result.second;

任何了解编译器内部工作原理的人都可以帮助解决这个问题吗? boost tuple page 提到了元组与传递引用的性能损失,但链接的论文都没有回答这个问题。

我更喜欢 std::pair 而不是这些传递引用语句的原因是,它使函数 much 的意图在许多情况下更加清晰,尤其是当其他参数输入为以及要修改的。

【问题讨论】:

因为按值返回的时候是在复制。 当我执行“inline int myGetPair2(int inp)”时,它也是按值返回的。编译器对其进行了优化。为什么在使用简单的 std::pair 结构时不能这样做? 这是哪个版本的 GCC?根据答案,看起来 MSVC 和最新版本的 GCC 都可以处理这个问题。 【参考方案1】:

我在 VC++2008 中尝试过,使用 cl.exe /c /O2 /FAs foo.cpp(即“仅编译且不链接”、“优化速度”和“在 cmets 中使用匹配的源代码行转储程序集输出”)。这就是 getLine() 的最终结果。

“byref”版本:

PUBLIC  ?getPair@@YAXHAAH0@Z                ; getPair
; Function compile flags: /Ogtpy
;   COMDAT ?getPair@@YAXHAAH0@Z
_TEXT   SEGMENT
_inp$ = 8                       ; size = 4
_a$ = 12                        ; size = 4
_b$ = 16                        ; size = 4
?getPair@@YAXHAAH0@Z PROC               ; getPair, COMDAT

; 9    :     myGetPair(inp, a, b);

    mov eax, DWORD PTR _inp$[esp-4]
    mov edx, DWORD PTR _a$[esp-4]
    lea ecx, DWORD PTR [eax+1023]
    mov DWORD PTR [edx], ecx
    mov ecx, DWORD PTR _b$[esp-4]
    add eax, 31                 ; 0000001fH
    mov DWORD PTR [ecx], eax

; 10   : 

    ret 0
?getPair@@YAXHAAH0@Z ENDP               ; getPair

“byval”std::pair-返回版本:

PUBLIC  ?getPair@@YAXHAAH0@Z                ; getPair
; Function compile flags: /Ogtpy
;   COMDAT ?getPair@@YAXHAAH0@Z
_TEXT   SEGMENT
_inp$ = 8                       ; size = 4
_a$ = 12                        ; size = 4
_b$ = 16                        ; size = 4
?getPair@@YAXHAAH0@Z PROC               ; getPair, COMDAT

; 8    :     std::pair<int,int> result = myGetPair(inp);

    mov eax, DWORD PTR _inp$[esp-4]

; 9    : 
; 10   :     a = result.first;

    mov edx, DWORD PTR _a$[esp-4]
    lea ecx, DWORD PTR [eax+1023]
    mov DWORD PTR [edx], ecx

; 11   :     b = result.second;

    mov ecx, DWORD PTR _b$[esp-4]
    add eax, 31                 ; 0000001fH
    mov DWORD PTR [ecx], eax

; 12   : 

    ret 0
?getPair@@YAXHAAH0@Z ENDP               ; getPair

如您所见,实际的组装是相同的;唯一的区别在于名称和 cmets 错误。

【讨论】:

有趣;所以这只是 gcc 没有做的额外优化。【参考方案2】:

糟糕的优化。让我们希望编译器有朝一日会变得更好。

【讨论】:

【参考方案3】:

你用的是什么编译器?在 gcc 4.4.2 上,通过引用和按值返回对得到完全相同的结果(操作码的操作码)。即:

mov 0x4(%esp),%eax
mov 0x8(%esp),%edx
lea 0x3ff(%eax),%ecx
add $0x1f,%eax
mov %ecx,(%edx)
mov 0xc(%esp),%edx
mov %eax,(%edx)
ret
lea 0x0(%esi),%esi

这是-O2 -fomit-frame-pointer。看起来编译器理解这里的意图并且没有做任何多余的事情。也许你应该升级你的:P

【讨论】:

【参考方案4】:

按值返回会强制在堆栈上构造未命名的临时对象并将它们的值分配给您的局部变量。

【讨论】:

不正确。 C++ 标准规定,可以创建也可以不创建此类临时对象。【参考方案5】:

大多数 C++ 程序员更看重表达的清晰性,而不是一些可疑的“效率”问题。但是如果你必须知道,返回引用几乎总是由编译器通过返回指针来实现的。

【讨论】:

我关心的不是返回引用,而是作为引用传递。在我的主力编译器和机器上返回 std::pair 时,可重现的、非平凡的速度提升是毫无疑问的。【参考方案6】:

这主要是猜测,但我认为编译器难以优化std::pair 的一个原因是pair 有一个非平凡的构造函数。这使它成为非 POD,并且编译器在优化时不能做出过于激进的假设。

不过,C++0x 中的 POD 规则正在发生变化,所以也许这会有所帮助。

【讨论】:

以上是关于返回 std::pair 与通过非常量引用传递的主要内容,如果未能解决你的问题,请参考以下文章

通过引用c ++传递非常量迭代器指向的值

为啥 std::pair 对于 const 引用和转发引用参数有两个不同的构造函数?

为什么std :: pair类标准被改为禁止在C ++ 11中只有非常量复制构造函数的类型?

C++ 返回对向量中值的引用

VS2012 error C2664: “std::make_pair”:无法将左值绑定到右值引用

将 r 值作为非常量引用传递(VS 警告 C4239)