Передача параметров один за другим или их объединение в массив, структуру или кортеж

При передаче аргументов функции я всегда предполагал, что прохождение аргументов один за другим не отличается от передачи их, завернутых в массив или структуру или кортеж. Однако простой эксперимент показал, что я ошибался.

Следующая программа, когда скомпилирована с помощью 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

Почему существует разница?

Ответы

Ответ 1

Ну, вы немного упростили, как передаются аргументы.

Когда функция вызывается (то есть не указана, constexpr оценивается или устраняется), способ передачи аргументов зависит от следующих факторов: 1- Является ли аргумент целым или с плавающей точкой, если аргумент имеет примитивный тип. 2- Размер аргумента. 3. Будет ли его адрес приниматься в некотором неисправимом коде в вызываемом. 4- Вызывающая конвенция. 5- Используется ли целая оптимизация программ (WPO). 6 - Связан ли вызываемый человек снаружи или статически. 7 - экспортируется ли вызываемый абонент или нет. 8- Указанное поведение с плавающей запятой. 9- Целевая платформа. 10. Известен ли аргумент во время компиляции. 11. Используется ли аргумент "простым" способом в соответствии с компилятором. 12- Число параметров. 13- Позиция параметра в списке параметров. 14- Тип аргумента. Компилятор может более эффективно использовать знакомые типы.

Теперь вернемся к приведенному вами примеру. Вы скомпилировали код с -02, поэтому мертвый код не будет устранен, а функция inlining отключена. Поэтому все функции должны быть вызваны. Целевая платформа - x64.

Первая функция имеет четыре 4-байтовых целочисленных параметра. Поэтому все они проходят через регистры.

Вторая функция имеет один массив фиксированного размера из четырех 4-байтовых целых чисел. Компилятор решил использовать два регистра (rdi и rsi) для передачи четырех целых чисел, где rdi = 0x200000001 и rsi = 0x400000003. Обратите внимание, как четыре целых числа (1, 2, 3, 4) компактно передаются с использованием этих двух регистров.

Передача целых чисел как структуры, а не один за другим, заставила компилятор использовать разные методы для их передачи. Но здесь существует компромисс между размером кода, скоростью и количеством требуемых регистров.

То же самое относится к третьей функции.

Последняя функция, однако, содержит вызовы std:: get, которым требуется адрес переданного кортежа. Таким образом, адрес хранится в rdi, который будет использоваться функцией std:: get. Поскольку вы компилируете С++ 14, std:: get помечен как constexpr. Компилятор смог оценить функцию, и поэтому доступ к памяти был испущен в тестовой функции, а не для вызова вызова функции std:: get. Обратите внимание, что это отличается от inlining.

Ответ 2

Я уверен, что правило в С++, как в старом C: Для массива или struct-reference его указатель на ORIGINAL, который передается как параметр, а не дубликат. и насколько я могу прочитать ваш ассемблерный код, это просто то, что происходит. 1) все в стеке в дублированных значениях. 2) Указатель на массив в стеке 3) Указатель на структуру в стеке 4) Кажется, что кортежи - это форма структуры, которая фактически является местом в стеке. (и они фактически находятся между ними). ​​