逐个传递参数,或者将它们包装在数组,结构或元组中

问题描述 投票:15回答:1

将参数传递给函数时,我总是假设逐个传递参数与传递它们包装在数组或结构或元组中没有什么不同。然而,一个简单的实验表明我错了。

compiled with GCC时,以下程序:

int test(int a, int b, int c, int d) {
    return a + b + c + d;
}

int test(std::array<int, 4> arr) {
    return arr[0] + arr[1] + arr[2] + arr[3];
}

struct abcd {
    int a; int b; int c; int d;
};

int test(abcd s) {
    return s.a + s.b + s.c + s.d;
}

int test(std::tuple<int, int, int, int> tup) {
    return std::get<0>(tup) + std::get<1>(tup) + std::get<2>(tup) + std::get<3>(tup);
}

...产生各种装配输出:

impl_test(int, int, int, int):
    lea eax, [rdi+rsi]
    add eax, edx
    add eax, ecx
    ret

impl_test(std::array<int, 4ul>):
    mov rax, rdi
    sar rax, 32
    add eax, edi
    add eax, esi
    sar rsi, 32
    add eax, esi
    ret

impl_test(abcd):
    mov rax, rdi
    sar rax, 32
    add eax, edi
    add eax, esi
    sar rsi, 32
    add eax, esi
    ret

impl_test(std::tuple<int, int, int, int>):
    mov eax, DWORD PTR [rdi+8]
    add eax, DWORD PTR [rdi+12]
    add eax, DWORD PTR [rdi+4]
    add eax, DWORD PTR [rdi]
    ret

main:
    push    rbp
    push    rbx
    mov ecx, 4
    mov edx, 3
    movabs  rbp, 8589934592
    mov esi, 2
    sub rsp, 24
    mov edi, 1
    movabs  rbx, 17179869184
    call    int test<int, int, int, int>(int, int, int, int)

    mov rdi, rbp
    mov rsi, rbx
    or  rbx, 3
    or  rdi, 1
    or  rsi, 3
    call    int test<std::array<int, 4ul> >(std::array<int, 4ul>)

    mov rdi, rbp
    mov rsi, rbx
    or  rdi, 1
    call    int test<abcd>(abcd)

    mov rdi, rsp
    mov DWORD PTR [rsp], 4
    mov DWORD PTR [rsp+4], 3
    mov DWORD PTR [rsp+8], 2
    mov DWORD PTR [rsp+12], 1
    call    int test<std::tuple<int, int, int, int> >(std::tuple<int, int, int, int>)

    add rsp, 24
    xor eax, eax
    pop rbx
    pop rbp
    ret

为什么会有区别?

c++ compiler-optimization calling-convention
1个回答
2
投票

当一个函数被调用时(也就是说,没有内联,constexpr被评估或消除),参数的传递方式取决于许多因素,包括:

  • 如果参数是基本类型,则参数是整数还是浮点数。
  • 参数的类型。
  • 它的地址是否在被调用者的一些未消除的代码中。
  • 默认或指定的调用约定。
  • 是否正在使用整个程序优化(WPO)。
  • 被调用者是在共享库,静态库还是目标文件中,还是在同一个翻译单元中。
  • 指定的浮点行为。
  • 目标平台。
  • 参数列表中参数的位置。

让我们回到您提供的示例。您使用-02编译代码,因此不会消除死代码并禁用函数内联。所以必须调用所有函数。目标平台是x64。

第一个函数有四个4字节整数参数。因此,所有这些都通过寄存器传递。

第二个函数有一个固定大小的数组,由四个4字节整数组成。编译器决定使用两个寄存器(rdirsi)来传递rdi = 0x200000001和rsi = 0x400000003的四个整数。注意如何使用这两个寄存器紧凑地传递四个整数(1,2,3,4)。

将整数作为结构而不是逐个传递使得编译器使用不同的技术来传递它们。但是这里需要在代码大小,速度和所需寄存器数量之间进行权衡。

第三个功能也是如此。

但是,最后一个函数包含对std::get的调用,它需要传递的元组的地址。因此地址存储在rdi中以供std::get函数使用。由于您使用C ++ 14进行编译,因此std :: get标记为constexpr。编译器能够评估函数,因此在测试函数中发出了内存访问,而不是发出对std::get函数的调用。请注意,这与内联不同。

© www.soinside.com 2019 - 2024. All rights reserved.