Синтаксис массивов против синтаксиса указателей и генерация кода?

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

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

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

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

8 ответов

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

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

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

У меня есть 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 элемент E1 (считая с нуля).

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

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

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

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

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

а также

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

сказать в основном то же самое.

Его язык очень похож на тот, который указан в вопросе 6.2 старого списка C FAQ:

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

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

Стандарт определяет поведение 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 выражения lvalue uh[i] и uw[j] оба обращаются к одному и тому же объединению, но это недостаточно сложно, чтобы заметить то же самое о *(u.h+i) и *(u.w+j) в utest2.

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

Пример:

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)) в каждой итерации, которая использует дополнительное умножение. В этом случае нет (временного) указателя, который "перемещается", а только пересчитывается только временный адрес.

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

Обновить:

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

Смотрите: https://godbolt.org/g/7DaPHG

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

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

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

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

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() (использование сложных инструкций затрудняет отладку компилятора), процессор может не иметь таких сложных инструкций и т. д.

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

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

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

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

C-код

Код сборки

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

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

int a[10] = {1,2,3,4,5,6,7,8,9,10};
Другие вопросы по тегам