Массив-синтаксис против синтаксиса указателя и генерации кода?

В книге "Понимание и использование указателей С" Ричарда Риса говорится на стр. 85,

int vector[5] = {1, 2, 3, 4, 5};

Код, сгенерированный vector[i], отличается от кода, генерируемого *(vector+i). vector[i] нотации vector[i] генерирует машинный код, который начинается с вектора местоположения, перемещает позиции i из этого местоположения и использует его содержимое. Обозначение *(vector+i) генерирует машинный код, который начинается с vector местоположения, добавляет i к адресу, а затем использует содержимое по этому адресу. Хотя результат тот же, сгенерированный машинный код отличается. Эта разница редко имеет значение для большинства программистов.

Вы можете увидеть отрывок здесь. Что означает этот отрывок? В каком контексте какой-либо компилятор генерирует для этих двух разных кодов код? Есть ли разница между "перемещением" от базы и "добавлением" к базе? Мне не удалось заставить это работать на GCC - генерировать разные машинные коды.

Ответ 1

Цитата просто неверна. Довольно трагично, что такой мусор до сих пор публикуется в этом десятилетии. Фактически, Стандарт C определяет x[y] как *(x+y).

Часть о lvalues позже на странице также совершенно и совершенно неверна.

ИМХО, лучший способ использовать эту книгу - положить ее в мусорную корзину или сжечь.

Ответ 2

У меня есть 2 файла C: ex1.c

% cat ex1.c
#include <stdio.h>

int main (void) {
    int vector[5] = { 1, 2, 3, 4, 5 };
    printf("%d\n", vector[3]);
}

и ex2.c,

% cat ex2.c
#include <stdio.h>

int main (void) {
    int vector[5] = { 1, 2, 3, 4, 5 };
    printf("%d\n", *(vector + 3));
}

И я собираю как в сборку, так и показываю разницу в сгенерированном ассемблере

% gcc -S ex1.c; gcc -S ex2.c; diff -u ex1.s ex2.s
--- ex1.s       2018-07-17 08:19:25.425826813 +0300
+++ ex2.s       2018-07-17 08:19:25.441826756 +0300
@@ -1,4 +1,4 @@
-       .file   "ex1.c"
+       .file   "ex2.c"
        .text
        .section        .rodata
 .LC0:

QED


В стандарте C очень четко указано (C11 n1570 6.5.2.1p2):

  1. Постфиксное выражение, за которым следует выражение в квадратных скобках [] является индексированным обозначением элемента объекта массива. Определение индексного оператора [] состоит в том, что E1[E2] идентичен (*((E1)+(E2))). Из-за правил преобразования, которые применяются к двоичному + оператору, если E1 является объектом массива (эквивалентно указателю на исходный элемент объекта массива), а E2 является целым числом, E1[E2] обозначает элемент E2 -th от E1 (с нуля).

Кроме того, здесь применяется правило as-if - если поведение программы одинаков, компилятор может сгенерировать тот же код, даже если семантика не была одинаковой.

Ответ 3

Процитированный отрывок совершенно неправильный. Выражения vector[i] и *(vector+i) совершенно идентичны и, как ожидается, будут генерировать идентичный код при любых обстоятельствах.

vector[i] и *(vector+i) по определению идентичны. Это центральное и фундаментальное свойство языка программирования C. Любой компетентный программист C понимает это. Любой автор книги "Понимать и использовать C-указатели" должен это понимать. Любой автор компилятора C это поймет. Эти два фрагмента будут генерировать идентичный код не случайно, а потому, что практически любой компилятор C, по сути, переводит одну форму в другую почти сразу, так что к тому времени, когда она дойдет до этапа генерации кода, она даже не узнает форма которого использовалась первоначально. (Я был бы очень удивлен, если бы компилятор C когда-либо создавал значительно отличающийся код для vector[i] в отличие от *(vector+i).)

И фактически цитируемый текст противоречит самому себе. Как вы отметили, два отрывка

vector[i] нотации vector[i] генерирует машинный код, который начинается с vector местоположения, перемещает позиции i из этого местоположения и использует его содержимое.

а также

Обозначение *(vector+i) генерирует машинный код, который начинается с vector местоположения, добавляет i к адресу, а затем использует содержимое по этому адресу.

говорят в основном то же самое.

Его язык очень похож на язык в вопросе 6.2 старого списка часто задаваемых вопросов:

... когда компилятор видит выражение a[3], он испускает код для начала в месте " a ", перемещает три мимо него и извлекает там символ. Когда он видит выражение p[3], он испускает код для начала в месте " p ", выбирает значение указателя там, добавляет три к указателю и, наконец, получает символ, на который указывает.

Но, конечно, главное различие заключается в том, что a - массив, а p - указатель. В списке часто задаваемых вопросов речь идет не о a[3] сравнению с *(a+3), а о a[3] (или *(a+3)), где a - массив, по сравнению p[3] (или *(p+3)) где p - указатель. (Конечно, эти два случая генерируют другой код, потому что массивы и указатели различны. Как поясняет список часто задаваемых вопросов, выбор адреса из переменной указателя принципиально отличается от использования адреса массива.)

Ответ 4

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

Пример:

for ( int i = 0; i < 5; i++ ) {
  vector[i] = something;
}

против

for ( int i = 0; i < 5; i++ ) {
  *(vector+i) = something;
}

В первом случае оптимизирующий компилятор может обнаружить, что vector массива повторяется по элементу по элементу и, таким образом, генерирует что-то вроде

void* tempPtr = vector;
for ( int i = 0; i < 5; i++ ) {
  *((int*)tempPtr) = something;
  tempPtr += sizeof(int); // _move_ the pointer; simple addition of a constant.
}

Возможно, он сможет использовать целевые указатели инструкций указателя процессора, если они доступны.

Во втором случае для компилятора "сложнее" увидеть, что адрес, который вычисляется через какое-то "произвольное" выражение арифметики указателя, показывает одно и то же свойство монотонно продвигать фиксированную сумму на каждой итерации. Таким образом, он не может найти оптимизацию и вычислить ((void*)vector+i*sizeof(int)) на каждой итерации, которая использует дополнительное умножение. В этом случае нет (временного) указателя, который получает "перемещен", но только временный адрес пересчитывается.

Однако утверждение, вероятно, не выполняется для всех компиляторов C во всех версиях.

Обновить:

Я проверил приведенный выше пример. Похоже, что без оптимизации включен как минимум gcc-8.1 x86-64 генерирует больше кода (2 дополнительных инструкций) для второй (указатель-арифметика) формы, чем первый (индекс массива).

См.: https://godbolt.org/g/7DaPHG

Однако при любых оптимизациях (-O... -O3) сгенерированный код одинаковый (длина) для обоих.

Ответ 5

Стандарт определяет поведение arr[i] когда arr является объектом массива как эквивалентное разложению arr на указатель, добавление i и разыменование результата. Хотя поведение будет эквивалентным во всех стандартных случаях, есть случаи, когда компиляторы обрабатывают действия с пользой, даже если это требует стандарт, а обработка значений arrayLvalue[i] и *(arrayLvalue+i) может отличаться как следствие,

Например, данный

char arr[5][5];
union { unsigned short h[4]; unsigned int w[2]; } u;

int atest1(int i, int j)
{
if (arr[1][i])
    arr[0][j]++;
return arr[1][i];
}
int atest2(int i, int j)
{
if (*(arr[1]+i))
    *((arr[0])+j)+=1;
return *(arr[1]+i);
}
int utest1(int i, int j)
{
    if (u.h[i])
        u.w[j]=1;
    return u.h[i];
}
int utest2(int i, int j)
{
    if (*(u.h+i))
        *(u.w+j)=1;
    return *(u.h+i);
}

Сгенерированный код GCC для test1 будет предполагать, что arr [1] [i] и arr [0] [j] не могут быть псевдонимом, но сгенерированный код для test2 позволит арифметике указателя получить доступ ко всему массиву. С другой стороны, gcc будет признать, что в utest1 выражения l uue [i] и uw [j] имеют доступ к одному и тому же соединению, но недостаточно сложны, чтобы заметить то же самое о * (u.h + i) и * (u.w + j) в utest2.

Ответ 6

Позвольте мне попытаться ответить на это "в узком" (другие уже описали, почему описание "как есть" несколько не хватает/неполное/вводящее в заблуждение):

В каком контексте какой-либо компилятор генерирует для этих двух разных кодов код?

"Не очень оптимизирующий" компилятор может генерировать другой код практически в любом контексте, потому что при разборе существует разница: x[y] - это одно выражение (индекс в массив), а *(x+y) - два выражения (добавьте целое число в указатель, затем разыщите его). Конечно, это не очень сложно распознать (даже при синтаксическом анализе) и относиться к нему одинаково, но, если вы пишете простой/быстрый компилятор, тогда вы избегаете добавлять в него "слишком много smarts". В качестве примера:

char vector[] = ...;
char f(int i) {
    return vector[i];
}
char g(int i) {
    return *(vector + i);
}

Компилятор, анализируя f(), видит "индексирование" и может генерировать что-то вроде (для некоторых 68000-подобных процессоров):

MOVE D0, [A0 + D1] ; A0/vector, D1/i, D0/result of function

OTOH, для g() компилятор видит две вещи: сначала разыменование ("что-то еще впереди"), а затем добавление целого к указателю/массиву, поэтому, будучи не очень оптимизированным, оно может закончиться:

MOVE A1, A0   ; A1/t = A0/vector
ADD A1, D1    ; t += i/D1
MOVE D0, [A1] ; D0/result = *t

Очевидно, что это очень зависит от реализации, некоторые компиляторы могут также не приветствовать использование сложных инструкций, используемых для f() (использование сложных инструкций затрудняет отладку компилятора), у ЦП могут не быть таких сложных инструкций и т.д.

Есть ли разница между "перемещением" от базы и "добавлением" к базе?

Описание в книге, возможно, не изложено. Но, я думаю, автор хотел описать вышеизложенное различие - индексирование ("перемещение" из базы) является одним выражением, а "добавление, а затем разыменование" - это два выражения.

Речь идет о реализации компилятора, а не о языковом определении, о различии, которое должно быть явно указано в книге.

Ответ 7

Я тестировал код для некоторых вариантов компилятора, большинство из них дают мне один и тот же код сборки для обеих команд (проверен на x86 без оптимизации). Интересно, что gcc 4.4.7 делает именно то, что вы упомянули: Пример:

C-Code

Assembly code

Другие langauges, такие как ARM или MIPS, делают то же самое, но я не тестировал все это. Так что кажется, что это была разница, но более поздние версии gcc "исправили" эту ошибку.

Ответ 8

Это примерный синтаксис массива, используемый в C.

int a[10] = {1,2,3,4,5,6,7,8,9,10};