Эффективность: массивы по сравнению с указателями

Доступ к памяти через указатели, как говорят, более эффективен, чем доступ к памяти через массив. Я изучаю 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
57
задан Abhijith Madhav 21 February 2010 в 13:57
поделиться

10 ответов

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

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

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


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

temp = a[0];

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

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

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


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

И само 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;
{
    ...
}

С тех пор компиляторы прошли ужасно долгий путь.

70
ответ дан 24 November 2019 в 19:30
поделиться

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

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

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

2
ответ дан 24 November 2019 в 19:30
поделиться

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

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

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

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

2
ответ дан 24 November 2019 в 19:30
поделиться

Как сказал 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
ответ дан 24 November 2019 в 19:30
поделиться

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

8
ответ дан 24 November 2019 в 19:30
поделиться

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

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);
}

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

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

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

11
ответ дан 24 November 2019 в 19:30
поделиться

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


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

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

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

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

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

7
ответ дан 24 November 2019 в 19:30
поделиться

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

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

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

7
ответ дан 24 November 2019 в 19:30
поделиться

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

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

1
ответ дан 24 November 2019 в 19:30
поделиться

Поскольку 0 определяется как константа, [0] также является константой, и компилятор знает, где он находится во время компиляции. В «нормальном» случае компилятор должен вычислить адрес элемента, исходя из базового + смещения (при этом смещение масштабируется согласно размеру элемента).

OTOH, p - это переменная, и для косвенного обращения требуется дополнительный ход.

В общем, индекс массива внутренне обрабатывается как арифметика указателя, так что я не уверен, что уловил смысл, который пытался донести K&R.

1
ответ дан 24 November 2019 в 19:30
поделиться
Другие вопросы по тегам:

Похожие вопросы: