Массивы (включая строки) передаются по ссылке на большинстве языков высокого уровня. int foo(char*)
просто получает значение указателя в виде аргумента, а указатель обычно представляет собой одно машинное слово (т.е. вписывается в регистр). В хороших современных соглашениях о вызовах первые несколько целочисленных / указательных аргументов обычно передаются в регистрах.
В C / C ++ нельзя передавать пустой массив по значению. Учитывая int arr[16]; func(arr);
, функция func
получает только указатель (на первый элемент).
В некоторых других языках более высокого уровня массивы могут больше походить на C ++ std::vector
, поэтому вызываемый может иметь возможность увеличивать / уменьшать массив и определять его длину без отдельного аргумента. Это обычно означает, что есть «блок управления».
В C и C ++ вы можете передавать структуры по значению, а затем в правилах соглашения о вызовах указывается, как их передавать .
x86-64 Система V, например, передает структуры по 16 байтов или менее, упакованные в до 2 целочисленных регистров. Более крупные структуры копируются в стек независимо от размера элемента массива, который они содержат ( Какой тип данных C11 представляет собой массив в соответствии с AMD64 ABI ). (Так что не передавайте гигантские объекты по значению не встроенным функциям!)
Соглашение о вызовах Windows x64 передает большие структуры по скрытой ссылке.
* * Пример тысячи двадцать-одина * +1022 *: * +1023 *
typedef struct {
// too big makes the asm output cluttered with loops or memcpy
// int Big_McLargeHuge[1024*1024];
int arr[4];
long long a,b; //,c,d;
} bigobj;
// total 32 bytes with int=4, long long=8 bytes
int func(bigobj a);
int foo(bigobj a) {
a.arr[3]++;
return func(a);
}
выход source + asm в проводнике компилятора Godbolt .
Вы можете попробовать другие архитектуры на Godbolt с их стандартными соглашениями о вызовах, такие как ARM или AArch64. Я выбрал x86-64, потому что случайно узнал об интересной разнице в двух основных соглашениях о вызовах на одной платформе для передачи структуры.
x86-64 System V (gcc7.3 -O3
) : foo
имеет действительную копию значения arg (сделанную его вызывающей стороной), которую он может изменить, так что он делает поэтому и использует его в качестве аргумента для хвостового вызова. (Если он не может выполнить хвостовой вызов, ему придется сделать еще одну полную копию. Этот пример искусственно заставляет System V выглядеть действительно хорошо).
foo(bigobj):
add DWORD PTR [rsp+20], 1 # increment the struct member in the arg on the stack
jmp func(bigobj) # tailcall func(a)
x86-64 Windows (MSVC CL19 /Ox
) : обратите внимание, что мы обращаемся к a.arr [3] через RCX, первое целое число / указатель arg. Так что есть скрытая ссылка, но это не const-ссылка. Эта функция была вызвана по значению, но она изменяет данные, полученные по ссылке. Поэтому вызывающий должен сделать копию или, по крайней мере, предположить, что вызываемый объект уничтожил аргумент, на который он получил указатель. (Копия не требуется, если объект после этого мертв, но это возможно только для локальных объектов структуры, но не для передачи указателя на глобальный объект или что-то в этом роде).
$T1 = 32 ; offset of the tmp copy in this function's stack frame
foo PROC
sub rsp, 72 ; 00000048H ; 32B of shadow space + 32B bigobj + 8 to align
inc DWORD PTR [rcx+12]
movups xmm0, XMMWORD PTR [rcx] ; load modified `a`
movups xmm1, XMMWORD PTR [rcx+16] ; apparently alignment wasn't required
lea rcx, QWORD PTR $T1[rsp]
movaps XMMWORD PTR $T1[rsp], xmm0
movaps XMMWORD PTR $T1[rsp+16], xmm1 ; store a copy
call int __cdecl func(struct bigobj)
add rsp, 72 ; 00000048H
ret 0
foo ENDP
Создание другой копии объекта представляется пропущенной оптимизацией. Я думаю, что это будет правильной реализацией foo
для того же соглашения о вызовах:
foo:
add DWORD PTR [rcx+12], 1 ; more efficient than INC because of the memory dst, on Intel CPUs
jmp func ; tailcall with pointer still in RCX
x86-64 clang для SysV ABI также пропускает оптимизацию, найденную gcc7.3, и копирует как MSVC .
Так что разница в ABI менее интересна, чем я думал; в обоих случаях вызываемый объект «владеет» аргументом, хотя для Windows он не гарантированно находится в стеке. Я предполагаю, что это позволяет динамическое распределение для передачи очень больших объектов по значению без переполнения стека, но это отчасти бессмысленно. Только не делай этого в первую очередь.
Мелкие предметы:
x86-64 Система V передает небольшие объекты, упакованные в регистры. Clang находит аккуратную оптимизацию, если вы закомментируете членов long long
, поэтому у вас просто есть
typedef struct {
int arr[4];
// long long a,b; //,c,d;
} bigobj;
# clang6.0 -O3
foo(bigobj): # @foo(bigobj)
movabs rax, 4294967296 # 0x100000000 = 1ULL << 32
add rsi, rax
jmp func(bigobj) # TAILCALL
(arr[0..1]
упакован в RDI, а arr[2..3]
упакован в RSI, первые 2 регистра передачи аргументов целого числа / указателя в x86-64 SysV ABI).
gcc сам распаковывает arr[3]
в регистр, где он может увеличивать его.
Но clang вместо распаковки и перепаковки увеличивает 32 старших бита RSI, добавляя 1ULL<<32
.
MSVC все еще проходит по скрытой ссылке и копирует весь объект.