Подтвердить что ты не робот

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

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

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

Я снял следующий код с использованием Visual С++. (Mine - это процессор 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
4b9b3361

Ответ 1

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

Это могло быть правдой в прошлом, когда компиляторы были относительно глупыми животными. Вам нужно всего лишь взглянуть на часть вывода кода 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;
{
    ...
}

Составители прошли очень долгий путь с тех пор.

Ответ 2

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

Ответ 3

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

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

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

Ответ 4

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

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. Операция multiply использует больше тактовых циклов, чем добавление на многие процессоры.

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

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

Ответ 5

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


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

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

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

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

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

Ответ 6

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

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

В этом случае использование массивов было похоже на использование указателей ограничивать. Поскольку, используя два массива, компилятор - неявно - знает, что они не указывают на одно и то же местоположение. Но если вы имеете дело с 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

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

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

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

Ответ 8

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

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

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

Ответ 9

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

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

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

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

Ответ 10

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

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

Ответ 11

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

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

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

Ответ 12

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

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

Для ежедневных машин 8G ram в 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.