我应该通过引用还是值传递__int128_t?

6

问题> 如何推荐传递 __int128_t 作为函数参数的方式?

谢谢

#include <iostream>
bool CheckInt(const __int128_t& large_number)
{
    return large_number > 10000; // Just for Demo
}

bool CheckInt2(__int128_t large_number)
{
    return large_number > 10000;
}

int main()
{
    __int128_t abc = 20000;    
    std::cout<< CheckInt(abc) << std::endl;
    std::cout<< CheckInt2(abc) << std::endl;

    return 0;
}

哪一个更快? - eerorika
在你的例子中,它们将被内联,因此差异会消失。 - Ben Voigt
1
在我的平台上,关于堆栈和引用的聚合参数大小,大小<256时传值是最优的,256-512差不多,而> 512时传递引用更好。这些数字非常依赖于平台,因此您需要进行分析以确定平台的最佳设置。 - Eljay
1
它可能取决于您的平台,但至少在x86-64和ARM64上,__int128可以通过两个寄存器按值传递,完全避免了内存。因此,这应该比按引用传递更好。当然,如果要调用函数的频率很高,参数传递开销非常大,那么它可能应该被内联。 - Nate Eldredge
@q0987:通过询问一个微不足道的函数,你不会学到关于更复杂函数性能权衡的太多知识。 - Ben Voigt
显示剩余5条评论
1个回答

9

让我们看看四种情况。

这些是由gcc编译的64位x86架构,不同编译器应该有类似的结果。

  1. 函数是如何编译的:
bool by_value(__int128 large_number) {
    return large_number > 10000;
}

bool by_reference(const __int128& large_number) {
    return large_number > 10000;
}

我们可以在这里看到x86汇编输出 https://godbolt.org/z/v9cM8xj35

by_value(__int128):
        mov     eax, 10000
        cmp     rax, rdi  # Use first 8 bytes
        mov     eax, 0
        sbb     rax, rsi  # Use second 8 bytes
        setl    al
        ret
by_reference(__int128 const&):
        mov     eax, 10000
        cmp     rax, QWORD PTR [rdi]    # Use first 8 bytes
        mov     eax, 0
        sbb     rax, QWORD PTR [rdi+8]  # Use second 8 bytes
        setl    al
        ret

被注释的代码行是唯一不同的代码行。

这个展示了平台的调用约定:前8个字节的参数存储在rdi中,第二个8字节存储在rsi中。

当您通过值传递时,large_number将被存储在这两个寄存器中,并且可以快速高效地使用。

当您通过引用传递时,只使用一个寄存器来传递指向该值的指针(rdi),并且使用解引用QWORD PTR [rdi]来访问前8个字节,使用QWORD PTR [rdi+8]来访问另外8个字节(一些指针算术)。

在大多数情况下,通过值传递会胜出。如果函数中有很多参数或局部变量,则用于存储large_number的寄存器可能会“溢出”到堆栈上,因此从理论上讲,通过值传递需要做更多的工作。但是,如果存在一个单寄存器指针或两个寄存器16字节的值,那么它可能会溢出,因此实际上应该没有太大的区别。


  1. 使用现有的__int128变量调用函数:
bool by_value(__int128);
bool by_reference(const __int128&);

extern __int128 x;

extern bool call_by_value() {
    return by_value(x);
}

extern bool call_by_reference() {
    return by_reference(x);
}

https://godbolt.org/z/7sT8b33Ez

call_by_value():
        mov     rdi, QWORD PTR x[rip]
        mov     rsi, QWORD PTR x[rip+8]
        jmp     by_value(__int128)
call_by_reference():
        mov     edi, OFFSET FLAT:x
        jmp     by_reference(__int128 const&)

在按值传递的情况下可能需要更多工作:要调用按引用传递,您只需要将xOFFSET FLAT:x)的地址加载到edi中并调用函数,而在按值传递的情况下,需要将x的值读入两个寄存器中,然后才能调用函数。
但是,请记住,按引用传递将必须通过指针间接使用它。因此,按引用传递在函数内隐藏了x[rip]x[rip+8],并没有太大区别。
  • 使用某些常量值(或优化为其)调用函数:
  • bool call_by_value() {
        __int128 abc = 20000;
        return by_value(abc);
    }
    
    bool call_by_reference() {
        __int128 abc = 20000;
        return by_reference(abc);
    }
    

    https://godbolt.org/z/6jhEWfh6a

    call_by_value():
            mov     edi, 20000  # Stores 2000 into the first register
            xor     esi, esi    # Stores 0 into the second register
            jmp     by_value(__int128)
    call_by_reference():
            sub     rsp, 24
            mov     rdi, rsp  # Store current stack pointer (which will point to abc)
            mov     QWORD PTR [rsp], 20000  # Store first 8 bytes on stack
            mov     QWORD PTR [rsp+8], 0    # Store second 8 bytes on the stack
            call    by_reference(__int128 const&)
            add     rsp, 24
            ret
    

    按引用调用需要做很多事情:值必须分配到堆栈上,然后将指向它的指针传递给函数。

    按值调用只需将值存储在两个寄存器中并调用函数。


    1. 使用运行时计算的prvalue(这里的“计算”只是一次复制)调用函数
    bool call_by_value() {
        return by_value(+x);
    }
    
    bool call_by_reference() {
        return by_reference(+x);
    }
    

    https://godbolt.org/z/vqdGEeGY9

    call_by_value():
            mov     rdi, QWORD PTR x[rip]
            mov     rsi, QWORD PTR x[rip+8]
            jmp     by_value(__int128)
    call_by_reference():
            sub     rsp, 24
            movdqa  xmm0, XMMWORD PTR x[rip]  # Store the value of x into a 16 byte register
            mov     rdi, rsp                  # Store current stack pointer
            movaps  XMMWORD PTR [rsp], xmm0   # Write 16 bytes to the stack pointer
            call    by_reference(__int128 const&)
            add     rsp, 24
            ret
    

    因此,在按值传递的情况下,计算结果可以直接在寄存器上进行。而在按引用传递的情况下,需要先计算出值,然后将其存储到堆栈中,最后需要传递指针。


    还有一个问题:当您使用 extern bool by_reference(const __int128&);,且没有启用整个程序优化或链接时优化时,编译器无法知道传递给 by_reference 的值是否会被修改。毕竟,这可能看起来像:

    bool by_reference(const __int128& large_number) {
        const_cast<__int128&>(large_number) = 0;
    }
    

    这可能会禁用一些进一步的优化。


    总的来说,在大多数情况下,传递值更好。在其他架构上,默认的调用约定可能是在堆栈上传递16字节参数,这将使两种情况没有太大区别。

    有些人会说,你只应该通过值传递指针大小或更小的内容,其他所有内容都应该通过引用传递。然而,这未能考虑到寄存器比堆栈快得多。

    这是基于汇编分析的结果,而不是实际计时结果。你可能需要多次调用函数才能看到差异。


    最后一个段落说了一切。 - Michael Chourdakis
    关于您在传递unique_ptr编译成更多间接引用而不是传递原始指针甚至包装指针的回答 - 看起来是我搞混了。 https://godbolt.org/z/a8Tedjcre 显示,对于非平凡的复制构造函数,在x86-64 SysV中,参数确实通过引用传递,而不是像C ABI一样在堆栈上按值传递太大无法放入寄存器的内容。 - Peter Cordes

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接