Эффективность: массивы против указателей - PullRequest
56 голосов
/ 21 февраля 2010

Доступ к памяти через указатели считается более эффективным, чем доступ к памяти через массив. Я изучаю C, и вышеизложенное указано в K & R. В частности, они говорят,

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

Я разобрал следующий код, используя Visual C ++. (У меня процессор 686. Я отключил все оптимизации.)

int a[10], *p = a, temp;

void foo()
{
    temp = a[0];
    temp = *p;
}

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

; 5    : temp = a[0];

    mov eax, DWORD PTR _a
    mov DWORD PTR _temp, eax

; 6    : temp = *p;

    mov eax, DWORD PTR _p
    mov ecx, DWORD PTR [eax]
    mov DWORD PTR _temp, ecx

Пожалуйста, помогите мне понять. Что мне здесь не хватает ??


Как указывалось во многих ответах и ​​комментариях, я использовал постоянную времени компиляции в качестве индекса массива, что значительно облегчило доступ через массив. Ниже приведен код сборки с переменной в качестве индекса. Теперь у меня одинаковое количество инструкций для доступа через указатель и массив. Мои более широкие вопросы остаются в силе. Доступ к памяти через указатель не является более эффективным.

; 7    :        temp = a[i];

    mov eax, DWORD PTR _i
    mov ecx, DWORD PTR _a[eax*4]
    mov DWORD PTR _temp, ecx

; 8    : 
; 9    :    
; 10   :        temp = *p;

    mov eax, DWORD PTR _p
    mov ecx, DWORD PTR [eax]
    mov DWORD PTR _temp, ecx

Ответы [ 13 ]

70 голосов
/ 21 февраля 2010

Доступ к памяти через указатели считается более эффективным, чем доступ к памяти через массив.

Это могло быть правдой в прошлом, когда компиляторы были относительно глупыми зверями. Вам нужно только взглянуть на часть кода, выведенного gcc в режимах высокой оптимизации, чтобы понять, что это больше не так. Часть этого кода очень сложна для понимания, но, как только вы это сделаете, его яркость очевидна.

Приличный компилятор сгенерирует один и тот же код для доступа к указателям и массивам, и вам, вероятно, не стоит беспокоиться об этом уровне производительности. Люди, которые пишут компиляторы, знают гораздо больше об их целевых архитектурах, чем мы, простые смертные. Сконцентрируйтесь больше на макроуровне при оптимизации кода (выбор алгоритма и т. Д.) И доверьтесь создателям инструментов, которые делают свою работу.


На самом деле, я удивлен, что компилятор не оптимизировал весь

temp = a[0];

строка не существует, поскольку temp переписано в следующей строке с другим значением, а a никоим образом не помечено volatile.

Я давно помню городской миф о эталоне для последнего компилятора VAX Fortran (показывающий мой возраст здесь), который превзошел своих конкурентов на несколько порядков.

Оказалось, что компилятор выяснил, что результат вычисления бенчмарка нигде не использовался, поэтому он оптимизировал весь цикл вычислений в забвение. Отсюда значительное улучшение скорости бега.


Обновление: Причина, по которой оптимизированный код более эффективен в вашем конкретном случае, заключается в том, как вы находите местоположение. a будет в фиксированном месте, определенном во время соединения / загрузки, и ссылка на него будет исправлена ​​в то же время. Так что a[0] или даже a[any constant] будет в фиксированном месте.

И p также будет находиться в фиксированном месте по той же причине. Но *p (содержимое p) является переменной и, следовательно, потребуется дополнительный поиск, чтобы найти правильную ячейку памяти.

Вы, вероятно, обнаружите, что наличие еще одной переменной x, установленной в 0 (не const) и использование a[x], также приведет к дополнительным вычислениям.


В одном из ваших комментариев вы указываете:

Выполнение, как вы предложили, привело также к 3 инструкциям для доступа к памяти через массивы (выборочный индекс, выборочное значение элемента массива, сохранение в temp). Но я все еще не вижу эффективности. : - (

Мой ответ на это таков, что вы, вероятно, не увидите эффективность использования указателей. Современные компиляторы более чем способны выяснить, что операции с массивами и операции с указателями можно превратить в один и тот же базовый машинный код.

Фактически, без включенной оптимизации код указателя может быть менее эффективным. Рассмотрим следующие переводы:

int *pa, i, a[10];

for (i = 0; i < 10; i++)
    a[i] = 100;
/*
    movl    $0, -16(%ebp)              ; this is i, init to 0
L2:
    cmpl    $9, -16(%ebp)              ; from 0 to 9
    jg      L3
    movl    -16(%ebp), %eax            ; load i into register
    movl    $100, -72(%ebp,%eax,4)     ; store 100 based on array/i
    leal    -16(%ebp), %eax            ; get address of i
    incl    (%eax)                     ; increment
    jmp     L2                         ; and loop
L3:
*/

for (pa = a; pa < a + 10; pa++)
    *pa = 100;
/*
    leal    -72(%ebp), %eax
    movl    %eax, -12(%ebp)            ; this is pa, init to &a[0]
L5:
    leal    -72(%ebp), %eax
    addl    $40, %eax
    cmpl    -12(%ebp), %eax            ; is pa at &(a[10])
    jbe     L6                         ; yes, stop
    movl    -12(%ebp), %eax            ; get pa
    movl    $100, (%eax)               ; store 100
    leal    -12(%ebp), %eax            ; get pa
    addl    $4, (%eax)                 ; add 4 (sizeof int)
    jmp     L5                         ; loop around
L6:
*/

Из этого примера вы можете видеть, что пример указателя длиннее, а излишне, поэтому . Он загружает pa в %eax несколько раз без его изменения и действительно чередуется %eax между pa и &(a[10]). Оптимизация по умолчанию здесь вообще отсутствует.

Когда вы переходите на уровень оптимизации 2, вы получаете код:

    xorl    %eax, %eax
L5:
    movl    $100, %edx
    movl    %edx, -56(%ebp,%eax,4)
    incl    %eax
    cmpl    $9, %eax
    jle     L5

для версии массива и:

    leal    -56(%ebp), %eax
    leal    -16(%ebp), %edx
    jmp     L14
L16:
    movl    $100, (%eax)
    addl    $4, %eax
L14:
    cmpl    %eax, %edx
    ja      L16

для версии указателя.

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

В качестве отступления от того утверждения, на которое вы ссылаетесь:

5.3. Указатели и массивы: версия указателя, как правило, будет быстрее, но, по крайней мере, для непосвященных, сразу сложнее понять.

восходит к самым ранним версиям K & R, включая мою древнюю версию 1978 года, где функции еще пишутся:

getint(pn)
int *pn;
{
    ...
}

Компиляторы прошли очень долгий путь с тех пор.

11 голосов
/ 21 февраля 2010

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

struct bar a[10], *p;

void foo()
{
    int i;

    // slow loop
    for (i = 0; i < 10; ++i)
        printf( a[i].value);

    // faster loop
    for (p = a; p < &a[10]; ++p)
        printf( p->value);
}

Медленный цикл должен вычислять + (i * sizeof (struct bar)) каждый раз, в то время как второй просто должен добавлять sizeof (struct bar) к p каждый раз. Операция умножения использует больше тактов, чем сложение на многих процессорах.

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

Попробуйте обновить образец, чтобы использовать структуру и ссылаться на несколько элементов.

8 голосов
/ 21 февраля 2010

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

7 голосов
/ 22 февраля 2010

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


Во многих случаях просто использование подписанного выражения требует, чтобы к проблеме был добавлен дополнительный слой. Цикл, который увеличивает индекс i , может быть конечным автоматом, и выражение a [i] технически требует, чтобы при каждом его использовании i умножить на размер каждого элемента и добавить к базовому адресу.

Чтобы преобразовать этот шаблон доступа в указатели, компилятор должен проанализировать весь цикл и определить, что, скажем, к каждому элементу обращаются. Затем компилятор может заменить несколько экземпляров умножения нижнего индекса на размер элемента простым приращением предыдущего значения цикла. Этот процесс объединяет оптимизации, называемые исключение общих подвыражений и уменьшение индукционной переменной прочности .

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

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

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

7 голосов
/ 21 февраля 2010

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

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

В этом случае использование массивов было похоже на использование restrict указателей. Потому что при использовании двух массивов компилятор неявно знает, что они не указывают на одно и то же местоположение. Но если вы имеете дело с 2 указателями, компилятор может подумать, что они указывают на одно и то же место и пропустят подкладку канала.

например:

int a[10],b[10],c[10];
int *pa=a, *pb=b, *pc=c;
int i;

// fill a and b.
fill_arrays(a,b);

// set c[i] = a[i]+b[i];
for (i = 0; i<10; i++)
{
   c[i] = a[i] + b[i];
}

// set *pc++ = *pa++ + *pb++;
for (i = 0; i<10; i++)
{
   *pc++ = *pa++ + *pb++;
}

В случае 1 компилятор будет легко выполнять конвейерную обработку, добавляя a и b и сохраняя значение в c.

В случае 2 компилятор не будет конвейерным, поскольку он может перезаписывать a или b при сохранении в C.

7 голосов
/ 21 февраля 2010

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

Таким образом, при выполнении циклов арифметика с указателями занимает немного меньше вычислений. Кроме того, указатели на правильный элемент быстрее, чем использование индекса в массиве.

Однако современная разработка постепенно избавляется от многих операций с указателями. Процессоры становятся все быстрее и быстрее, а массивами легче управлять, чем указателями. Кроме того, массивы имеют тенденцию уменьшать количество ошибок в коде. Массив разрешит проверки индекса, убедившись, что вы не обращаетесь к данным вне массива.

3 голосов
/ 03 августа 2013

Это очень старый вопрос, и на него уже дан ответ, поэтому мне не нужно отвечать! Однако я не заметил простого ответа, поэтому даю один.

ОТВЕТ: Косвенный доступ (указатель / массив) «может» добавить одну дополнительную инструкцию для загрузки (базового) адреса, но все обращения после этого (элементы в случае массива / членов в случае указателя на структуру) должны быть только одна инструкция, потому что это просто добавление смещения к (базовому) адресу, который уже загружен. Таким образом, в некотором смысле это будет так же хорошо, как прямой доступ. Таким образом, в большинстве случаев доступ через массив / указатель эквивалентен, а доступ к элементам так же хорош, как и прямой доступ к переменной.

Ex. если у меня есть массив (или указатель) с 10 элементами или структура с 10 членами (доступ к которым осуществляется через указатель на структуру), и я обращаюсь к элементу / члену, одна возможная дополнительная инструкция требуется только один раз в начале. После этого все обращения к элементу / члену должны быть только одной инструкцией.

2 голосов
/ 22 февраля 2010

Указатели раньше были быстрее, чем массивы. Конечно, в те времена, когда разрабатывался язык C, указатели были немного быстрее. Но в наши дни оптимизаторы обычно лучше справляются с оптимизацией массивов, чем с помощью указателей, поскольку массивы более ограничены.

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

Итак, суть в том, что в наши дни массивы часто бывают быстрее, особенно при использовании в циклах с индексными переменными.

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

2 голосов
/ 22 февраля 2010

Вы получаете хорошие ответы на свой вопрос здесь, но поскольку вы учитесь, стоит отметить, что эффективность на этом уровне редко бывает заметна.

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

Вот пример того, как это можно сделать.

1 голос
/ 04 мая 2017

Поскольку большинство людей уже дали подробные ответы, я просто приведу интуитивный пример. Если вы используете массив и указатель в большем масштабе, эффективность использования указателя будет более значительной. Например, если вы хотите отсортировать большой длинный набор данных int, отсортировав его по нескольким поднаборам, а затем объедините их.

long int * testData = calloc(N, sizeof(long int));

Для ежедневных 8G оперативных машин в 2017 году мы можем установить N равным 400000000, что означает, что вы будете использовать примерно 1,5 ГБ памяти для этого исходного набора данных. И если вы используете MPI, вы можете быстро разделить данные с помощью

MPI_Scatterv(testData, partitionLength, partitionIndex, MPI_LONG, MPI_IN_PLACE, N/number_of_thread, MPI_LONG, 0, MPI_COMM_WORLD);

Вы можете просто трактовать paritionLength как указатель, который хранит N/number_of_thread как длину для каждой идентичной детали, и обрабатывать partitionIndex как указатель, который постепенно увеличивает начальный индекс N / number_of_threads. Предположим, у вас есть 4-ядерный процессор, и вы разделяете свою работу только на 4 потока. MPI определенно справится с работой по ссылкам. Но если вы используете массив, эта процедура должна запустить арифметику указателей на массиве, чтобы сначала найти точку разбиения. Который не так прям, как указатель. Кроме того, когда вы объединяете многораздельный набор данных, вы можете использовать K-way merge для ускорения. Вам нужно временное пространство для хранения четырех отсортированных данных. Здесь, если вы используете указатель, вам нужно хранить только 4 адреса. Однако, если вы используете массив, он будет хранить 4 полных подмассива, что неэффективно. Иногда, если вы не используете MPI_Barrier, чтобы убедиться, что ваша программа является поточно-ориентированной, MPI может даже жаловаться на плохую реализацию памяти. Я получил 32G машину для сортировки длинных значений 400000000 на 8 потоков по методу массива и по методу указателя, я получил 11.054980s и 13.182739s соответственно. И если я увеличу размер до 1000000000, моя программа сортировки не будет успешно выполнена, если я использую массив. Вот почему многие люди используют указатели для всех структур данных, кроме скаляров в C.

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