С точки зрения компилятора, как обрабатывается ссылка на массив и почему передача по значению (не затухание) не разрешена?

Как мы знаем, в 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

Я признаю, что я не знаком с языком ассемблера, но ясно, что коды сборки трех функций одинаковы! Итак, что-то должно произойти, прежде чем ассемблер закодирует. В любом случае, в отличие от массива, указатель ничего не знает о длине, верно?

Вопросы:

  1. как здесь работает компилятор?
  2. Теперь, когда стандарт позволяет передавать массив по ссылке, означает ли это, что его легко реализовать? Если так, то почему нельзя передавать по значению?

Для 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++.)

Другие вопросы по тегам