Ссылка C ++ на массив - это то же самое, что указатель на первый элемент на языке ассемблера.
Даже C99 int foo(int arr[static 3])
по-прежнему является просто указателем в asm.Синтаксис static
гарантирует компилятору, что он может безопасно читать все 3 элемента, даже если абстрактная машина C не имеет доступа к некоторым элементам, поэтому, например, он может использовать cmov
без ветвей для if
.
Вызывающая сторона не передает длину в регистр, потому что это константа времени компиляции и, следовательно, не требуется во время выполнения.
Вы можете передавать массивы по значению, но только если они находятся внутри структуры или объединения.В этом случае разные соглашения о вызовах имеют разные правила. Какой тип данных C11 является массивом в соответствии с AMD64 ABI .
Вы бы почти никогда не хотели бы передавать массив по значению, поэтому имеет смыслчто C не имеет синтаксиса для него, и что C ++ никогда не изобрел никакого.Передача по постоянной ссылке (например, const int *arr
) намного эффективнее;просто один указатель arg.
Устранение шума компилятора путем включения оптимизации:
Я поместил ваш код в проводник компилятора Godbolt, скомпилированный с помощью gcc -O3 -fno-inline-functions -fno-inline-functions-called-once -fno-inline-small-functions
, чтобы он не включал функциюзвонки.Это избавляет от всего шума от -O0
шаблона отладки-сборки и фрейма-указателя.(Я просто искал на странице справочника inline
и отключал опции встраивания, пока не получил то, что хотел.)
Вместо -fno-inline-small-functions
и т. Д. Вы можете использовать GNU C __attribute__((noinline))
в своих определениях функцийчтобы отключить встраивание для определенных функций, даже если они static
.
Я также добавил вызов функции без определения, поэтому компилятору необходимо иметь arr[]
с правильными значениями в памяти,и добавил магазин к arr[4]
в двух функциях.Это позволяет нам проверить, предупреждает ли компилятор о выходе за пределы массива.
__attribute__((noinline, noclone))
void foo_p(int*arr) {(void)arr;}
void foo_r(int(&arr)[3]) {arr[4] = 41;}
template<int length>
void foo_t(int(&arr)[length]) {arr[4] = 42;}
void usearg(int*); // stop main from optimizing away arr[] if foo_... inline
int main()
{
int arr[] = {1, 2, 3};
foo_p(arr);
foo_r(arr);
foo_t(arr);
usearg(arr);
return 0;
}
gcc7.3 -O3 -Wall -Wextra
без встраивания функции, на Godbolt : Поскольку я отключил предупреждения о неиспользуемых аргументах из вашего кода, единственное предупреждение, которое мы получаем, - это шаблон, а не foo_r
:
<source>: In function 'int main()':
<source>:14:10: warning: array subscript is above array bounds [-Warray-bounds]
foo_t(arr);
~~~~~^~~~~
Вывод asm:
void foo_t<3>(int (&) [3]) [clone .isra.0]:
mov DWORD PTR [rdi], 42 # *ISRA.3_4(D),
ret
foo_p(int*):
rep ret
foo_r(int (&) [3]):
mov DWORD PTR [rdi+16], 41 # *arr_2(D),
ret
main:
sub rsp, 24 # reserve space for the array and align the stack for calls
movabs rax, 8589934593 # this is 0x200000001: the first 2 elems
lea rdi, [rsp+4]
mov QWORD PTR [rsp+4], rax # MEM[(int *)&arr], first 2 elements
mov DWORD PTR [rsp+12], 3 # MEM[(int *)&arr + 8B], 3rd element as an imm32
call foo_r(int (&) [3])
lea rdi, [rsp+20]
call void foo_t<3>(int (&) [3]) [clone .isra.0] #
lea rdi, [rsp+4] # tmp97,
call usearg(int*) #
xor eax, eax #
add rsp, 24 #,
ret
Звонок на foo_p()
все еще был оптимизирован, возможно потому, что он ничего не делает.(Я не отключил межпроцедурную оптимизацию, и даже атрибуты noinline
и noclone
не остановили это.) Добавление *arr=0;
в тело функции приводит к вызову из main
(передачауказатель в rdi
, как и в других 2).
Обратите внимание на аннотацию clone .isra.0
в имени деформированной функции: gcc определил функцию, которая принимает указатель на arr[4]
, а не на базуэлемент.Вот почему есть lea rdi, [rsp+20]
для настройки аргумента и почему магазин использует [rdi]
для разыменования точки без смещения.__attribute__((noclone))
остановит это.
Эта межпроцедурная оптимизация в значительной степени тривиальна и в этом случае экономит 1 байт размера кода (только disp8
в режиме адресации в клоне), но может бытьполезно в других случаях.Вызывающий должен знать, что это определение для модифицированной версии функции, такой как void foo_clone(int *p) { *p = 42; }
, поэтому он должен кодировать ее в искаженном имени символа.
Если вы создали экземпляр шаблона водин файл и вызвал его из другого файла, который не смог увидеть определение, тогда без оптимизации во время компоновки gcc пришлось бы просто вызвать обычное имя и передать указатель на массив, как написанная функция.
IDK, почему gcc делает это для шаблона, но не для ссылки.Это может быть связано с тем, что он предупреждает о версии шаблона, но не о справочной версии.Или, может быть, это связано с main
выводом шаблона?
Кстати, IPO, которое фактически заставило бы его работать немного быстрее, позволило бы main
использовать mov rdi, rsp
вместо lea rdi, [rsp+4]
.т.е. взять &arr[-1]
в качестве функции arg, чтобы клон использовал mov dword ptr [rdi+20], 42
.
Но это полезно только для вызывающих, таких как main
, которые выделили массив на 4 байта выше rsp
, и я думаю, что gcc ищет только IPO, которые делают саму функцию более эффективной, а не вызывающую последовательность в одном конкретном вызывающем устройстве.