С точки зрения компилятора, как обрабатывается ссылка на массив и почему передача по значению (не затухание) не разрешена?
Как мы знаем, в C++ мы можем передать ссылку на массив в качестве аргумента f(int (&[N])
, Да, это синтаксис, гарантированный стандартом ISO, но мне интересно, как работает компилятор здесь. Я нашел эту тему, но, к сожалению, это не отвечает на мой вопрос - как этот синтаксис реализован компилятором?
Затем я написал демо-версию и надеялся увидеть что-то из языка ассемблера:
void foo_p(int*arr) {}
void foo_r(int(&arr)[3]) {}
template<int length>
void foo_t(int(&arr)[length]) {}
int main(int argc, char** argv)
{
int arr[] = {1, 2, 3};
foo_p(arr);
foo_r(arr);
foo_t(arr);
return 0;
}
Первоначально, я думаю, что он все еще будет затухать до указателя, но будет неявно передавать длину через регистр, а затем снова превращаться в массив в теле функции. Но ассемблерный код говорит мне, что это не так
void foo_t<3>(int (&) [3]):
push rbp #4.31
mov rbp, rsp #4.31
sub rsp, 16 #4.31
mov QWORD PTR [-16+rbp], rdi #4.31
leave #4.32
ret #4.32
foo_p(int*):
push rbp #1.21
mov rbp, rsp #1.21
sub rsp, 16 #1.21
mov QWORD PTR [-16+rbp], rdi #1.21
leave #1.22
ret #1.22
foo_r(int (&) [3]):
push rbp #2.26
mov rbp, rsp #2.26
sub rsp, 16 #2.26
mov QWORD PTR [-16+rbp], rdi #2.26
leave #2.27
ret #2.27
main:
push rbp #6.1
mov rbp, rsp #6.1
sub rsp, 32 #6.1
mov DWORD PTR [-16+rbp], edi #6.1
mov QWORD PTR [-8+rbp], rsi #6.1
lea rax, QWORD PTR [-32+rbp] #7.15
mov DWORD PTR [rax], 1 #7.15
lea rax, QWORD PTR [-32+rbp] #7.15
add rax, 4 #7.15
mov DWORD PTR [rax], 2 #7.15
lea rax, QWORD PTR [-32+rbp] #7.15
add rax, 8 #7.15
mov DWORD PTR [rax], 3 #7.15
lea rax, QWORD PTR [-32+rbp] #8.5
mov rdi, rax #8.5
call foo_p(int*) #8.5
lea rax, QWORD PTR [-32+rbp] #9.5
mov rdi, rax #9.5
call foo_r(int (&) [3]) #9.5
lea rax, QWORD PTR [-32+rbp] #10.5
mov rdi, rax #10.5
call void foo_t<3>(int (&) [3]) #10.5
mov eax, 0 #11.11
leave #11.11
ret #11.11
live demo
Я признаю, что я не знаком с языком ассемблера, но ясно, что коды сборки трех функций одинаковы! Итак, что-то должно произойти, прежде чем ассемблер закодирует. В любом случае, в отличие от массива, указатель ничего не знает о длине, верно?
Вопросы:
- как здесь работает компилятор?
- Теперь, когда стандарт позволяет передавать массив по ссылке, означает ли это, что его легко реализовать? Если так, то почему нельзя передавать по значению?
Для Q2 я предполагаю сложность прежних кодов C++ и C. В конце концов, int[]
быть равным int*
в параметрах функции было традицией. Может быть, через сто лет это будет устареть?
3 ответа
Ссылка 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]
настроить arg, и почему магазин использует [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, которые делают саму функцию более эффективной, а не вызывающую последовательность в одном конкретном абоненте.
Все дело в обратной совместимости. C++ получил массивы от C, который получил от языка B. А в B переменная массива фактически была указателем. Деннис Ричи написал об этом.
Параметры массива, уменьшающиеся до указателей, помогли Кену Томпсону повторно использовать его старые источники B при перемещении UNIX в C.:-)
Когда позже это было сочтено, возможно, не лучшим решением, вместо этого было сочтено слишком поздно менять язык Си. Таким образом, распад массива был сохранен, но структуры - добавленные позже - передаются по значению.
Введение структур также предложило своего рода обходной путь для случая, когда вы действительно хотите передать массив по значению:
Зачем объявлять структуру, которая содержит только массив в C?
Что касается:
Я признаю, что я не знаком с языком ассемблера, но ясно, что коды сборки трех функций одинаковы!
Коды ассемблера могут определенно совпадать или могут отличаться друг от друга - это зависит от отдельных реализаций C++ (и опций, с которыми вы их вызываете). Стандарт C++ имеет общее правило "как будто", разрешающее любой сгенерированный машинный код, если поддерживается наблюдаемое поведение (которое тщательно определено).
Различные синтаксисы в вашем вопросе - это всего лишь синтаксические и некоторые семантические различия на уровне исходного кода и в процессе перевода. Каждый из них определяется по-разному в Стандарте - например, точный тип параметра функции будет отличаться (и если бы вы использовали что-то вроде boost::type_index<T>()::pretty_name()
вы на самом деле получили бы другой машинный код и наблюдаемые результаты) - но в конце дня общий код, который необходимо сгенерировать для вашей программы-примера, на самом деле просто return 0;
заявление о main()
, (И технически это утверждение также избыточно для main()
функция в C++.)