Доступ к памяти через указатели, как говорят, более эффективен, чем доступ к памяти через массив. Я изучаю 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
Считается, что доступ к памяти через указатели более эффективен, чем доступ к памяти через массив.
Возможно, это было верно в прошлом, когда компиляторы были относительно глупыми зверями. Достаточно взглянуть на некоторый код, выдаваемый 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;
{
...
}
С тех пор компиляторы прошли ужасно долгий путь.
Здесь вы получили хорошие ответы на свой вопрос, но поскольку вы учитесь, стоит отметить, что эффективность на таком уровне редко бывает заметной.
Когда вы настраиваете программу на максимальную производительность, вы должны уделять как минимум столько же внимания поиску и устранению более серьезных проблем в структуре программы. После их устранения низкоуровневые оптимизации могут еще больше улучшить ситуацию.
Указатели раньше были быстрее массивов. Конечно, в те времена, когда был разработан язык C, указатели были намного быстрее. Но в наши дни оптимизаторы обычно лучше справляются с оптимизацией массивов, чем с указателями, потому что массивы более ограничены.
Наборы инструкций современных процессоров также были разработаны для оптимизации доступа к массивам.
В итоге, массивы в наши дни часто работают быстрее, особенно при использовании в циклах с индексными переменными.
Конечно, вы по-прежнему хотите использовать указатели для таких вещей, как связанные списки, но старая оптимизация, когда указатель проходит по массиву, а не используется индексная переменная, теперь, скорее всего, является неоптимизацией.
Как сказал 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.
В первом случае компилятор напрямую знает адрес массива (который также является адресом первого элемента) и обращается к нему. Во втором случае он знает адрес указателя и считывает его значение, которое указывает на эту область памяти. Это фактически одно дополнительное перенаправление, поэтому здесь предположительно медленнее.
Если вы программируете встроенные платформы, вы быстро узнаете, что метод указателя намного быстрее, чем использование индекса.
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 и ссылаться на несколько элементов.
Во многих случаях просто использование выражения с индексами требует наличия дополнительного уровня добавил к проблеме. Цикл, увеличивающий нижний индекс i , можно рассматривать как конечный автомат, а выражение a [i] технически требует, чтобы каждый раз при его использовании, чтобы i умножить на размер каждого элемента и прибавить к базовому адресу.
Чтобы преобразовать этот шаблон доступа для использования указателей, компилятор должен проанализировать весь цикл и определить, что, скажем, осуществляется доступ к каждому элементу. Затем компилятор может заменить несколько экземпляров умножения нижнего индекса на размер элемента простым приращением предыдущего значения цикла. Этот процесс объединяет оптимизации, называемые устранением общего подвыражения и индукционным уменьшением силы переменной .
При записи с указателями весь процесс оптимизации не требуется, потому что программист обычно просто проходит через массив, чтобы начать с него.
Иногда компилятор может выполнить оптимизацию, а иногда нет. В последние годы все чаще бывает под рукой сложный компилятор, поэтому код, основанный на указателях, не всегда быстрее .
Поскольку массивы обычно должны быть смежными, еще одно преимущество указателей заключается в создании постепенно выделяемых составных структур.
Скорость набирается больше всего в циклах. Когда вы используете массив, вы должны использовать счетчик, который вы увеличиваете. Чтобы вычислить позицию, система умножает этот счетчик на размер элемента массива, затем добавляет адрес первого элемента, чтобы получить адрес. С указателями все, что вам нужно сделать, чтобы перейти к следующему элементу состоит в том, чтобы увеличить текущий указатель с размером элемента, чтобы получить следующий, предполагая, что все элементы находятся рядом друг с другом в памяти.
Таким образом, арифметика с указателями требует немного меньше вычислений при выполнении циклов. Кроме того, указатели на правый элемент быстрее, чем использование индекса в массиве.
Однако современные разработки постепенно избавляются от многих операций с указателями. Процессоры становятся все быстрее и быстрее, а управлять массивами проще, чем указателями. Кроме того, массивы, как правило, уменьшают количество ошибок в коде. Массив позволит проверять индекс, чтобы убедиться, что вы не имеете доступа к данным за пределами массива.
«Версия с указателем в целом будет быстрее» означает, что в большинстве случаев компилятору проще сгенерировать более эффективный код, имея указатель (который просто требует для разыменования), чем наличие массива и индекса (что означает, что компилятору необходимо сместить адрес с начала массива). Однако с современными процессорами и оптимизирующими компиляторами доступ к массиву в типичном случае не медленнее, чем доступ по указателю.
Конкретно в вашем случае вам нужно будет включить оптимизацию, чтобы получить тот же результат.
Поскольку 0 определяется как константа, [0] также является константой, и компилятор знает, где он находится во время компиляции. В «нормальном» случае компилятор должен вычислить адрес элемента, исходя из базового + смещения (при этом смещение масштабируется согласно размеру элемента).
OTOH, p - это переменная, и для косвенного обращения требуется дополнительный ход.
В общем, индекс массива внутренне обрабатывается как арифметика указателя, так что я не уверен, что уловил смысл, который пытался донести K&R.