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

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

0 голосов
/ 09 июня 2018

Относительно:

Я допускаю, что я не знаком с языком ассемблера, но ясно, что коды сборки трех функций одинаковы!

Коды сборкиможет определенно быть тем же самым, или они могут отличаться - это зависит от отдельных реализаций C ++ (и опций, с которыми вы их вызываете).Стандарт C ++ имеет общее как если бы правило , разрешающее любой сгенерированный машинный код, если поддерживается наблюдаемое поведение (которое тщательно определено).

Различные синтаксисы в вашем вопросе - это только синтаксические и некоторые семантические различия на уровне исходного кода и процесса перевода.Каждый из них определен в Стандарте по-разному - например, точный тип параметра функции будет другим (и если бы вы использовали что-то вроде boost::type_index<T>()::pretty_name(), вы бы фактически получили другой машинный код и наблюдаемыйвыводится) - но в конце дня общий код, который необходимо сгенерировать для вашей программы-примера, на самом деле является просто оператором return 0; main().(И технически это утверждение также избыточно для функции main() в C ++.)

0 голосов
/ 10 июня 2018

Все дело в обратной совместимости.C ++ получил массивы от C, который получил от языка B.А в B переменная массива фактически была указателем.Деннис Ритчи написал об этом .

Параметры массива, распадающиеся на указатели, помогли Кену Томпсону повторно использовать его старые источники B при перемещении UNIX в C.: -)

Когда позже этобыло сочтено, возможно, не лучшим решением, вместо этого было сочтено слишком поздно менять язык Си.Таким образом, распад массива был сохранен, но структуры - добавленные позже - передаются по значению.


Введение структур также предложило своего рода обходной путь для случая, когда вы действительно хотели передать массив по значению:

Зачем объявлять структуру, которая содержит только массив в C?

0 голосов
/ 09 июня 2018

Ссылка 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, которые делают саму функцию более эффективной, а не вызывающую последовательность в одном конкретном вызывающем устройстве.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...