В C доступ к моему индексу массива быстрее или доступ по указателю быстрее? - PullRequest
12 голосов
/ 09 февраля 2011

В C доступ к индексу массива быстрее или доступ по указателю быстрее?Я имею в виду, что быстрее будет меньше тактов.Массив не является константным массивом.

Ответы [ 8 ]

17 голосов
/ 09 февраля 2011

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

myArr[index]

Полностью эквивалентно

*(&myArr[0] + index)

Аналогично, написание

*ptr

эквивалентно написанию

ptr[0]

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

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

13 голосов
/ 09 февраля 2011

templatetypedef подвел итог.Чтобы добавить поддержку своего ответа.Возьмем следующие примеры функций:

unsigned int fun1 ( unsigned int *x )
{
    unsigned int ra,rb;

    rb=0;
    for(ra=0;ra<1000;ra++) rb+=*x++;
    return(rb);
}

unsigned int fun2 ( unsigned int *x )
{
    unsigned int ra,rb;
    rb=0;
    for(ra=0;ra<1000;ra++) rb+=x[ra];
    return(rb);
}

Теперь gcc произвел это:

00000000 fun1:
   0:   e52d4004    push    {r4}        ; (str r4, [sp, #-4]!)
   4:   e1a03000    mov r3, r0
   8:   e2804efa    add r4, r0, #4000   ; 0xfa0
   c:   e3a00000    mov r0, #0
  10:   e1a02003    mov r2, r3
  14:   e492c004    ldr ip, [r2], #4
  18:   e5931004    ldr r1, [r3, #4]
  1c:   e2823004    add r3, r2, #4
  20:   e080000c    add r0, r0, ip
  24:   e1530004    cmp r3, r4
  28:   e0800001    add r0, r0, r1
  2c:   1afffff7    bne 10 
  30:   e49d4004    pop {r4}        ; (ldr r4, [sp], #4)
  34:   e12fff1e    bx  lr

00000038 fun2:
  38:   e3a03000    mov r3, #0
  3c:   e1a02003    mov r2, r3
  40:   e790c003    ldr ip, [r0, r3]
  44:   e2833004    add r3, r3, #4
  48:   e7901003    ldr r1, [r0, r3]
  4c:   e2833004    add r3, r3, #4
  50:   e082200c    add r2, r2, ip
  54:   e3530efa    cmp r3, #4000   ; 0xfa0
  58:   e0822001    add r2, r2, r1
  5c:   1afffff7    bne 40 
  60:   e1a00002    mov r0, r2
  64:   e12fff1e    bx  lr

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

Clang / llvm произвел это:


00000000 fun1:
   0:   e3a01000    mov r1, #0
   4:   e3a02ffa    mov r2, #1000   ; 0x3e8
   8:   e1a03001    mov r3, r1
   c:   e2522001    subs    r2, r2, #1
  10:   e490c004    ldr ip, [r0], #4
  14:   e08c3003    add r3, ip, r3
  18:   e2c11000    sbc r1, r1, #0
  1c:   e182c001    orr ip, r2, r1
  20:   e35c0000    cmp ip, #0
  24:   1afffff8    bne c 
  28:   e1a00003    mov r0, r3
  2c:   e12fff1e    bx  lr

00000030 fun2:
  30:   e3a01000    mov r1, #0
  34:   e3a02ffa    mov r2, #1000   ; 0x3e8
  38:   e1a03001    mov r3, r1
  3c:   e2522001    subs    r2, r2, #1
  40:   e490c004    ldr ip, [r0], #4
  44:   e08c3003    add r3, ip, r3
  48:   e2c11000    sbc r1, r1, #0
  4c:   e182c001    orr ip, r2, r1
  50:   e35c0000    cmp ip, #0
  54:   1afffff8    bne 3c
  58:   e1a00003    mov r0, r3
  5c:   e12fff1e    bx  lr

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

РЕДАКТИРОВАТЬ:

Я надеялся получить компилятор вкак минимум, используйте инструкцию ldr rd, [rs], # 4, которая поддерживает указатели, и надеется, что компилятор увидит, что он может уничтожить адрес массива, таким образом рассматривая его как указатель, а не как смещение в массиве (и используйте вышеупомянутую инструкцию, что в основном то, что сделал clang / llvm).Или, если бы он сделал массив, он бы использовал инструкцию ldr rd, [rm, rn].По сути, он надеялся, что один из компиляторов сгенерирует одно из этих решений:


funa:
    mov r1,#0
    mov r2,#1000
funa_loop:
    ldr r3,[r0],#4
    add r1,r1,r3
    subs r2,r2,#1
    bne funa_loop
    mov r0,r1
    bx lr

funb:
    mov r1,#0
    mov r2,#0
funb_loop:
    ldr r3,[r0,r2]
    add r1,r1,r3
    add r2,r2,#4
    cmp r2,#0x4000
    bne funb_loop
    mov r0,r1
    bx lr

func:
    mov r1,#0
    mov r2,#4000
    subs r2,r2,#4
func_loop:
    beq func_done
    ldr r3,[r0,r2]
    add r1,r1,r3
    subs r2,r2,#4
    b func_loop
func_done:
    mov r0,r1
    bx lr

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

В общем, (не мой конкретный пример кода C и не обязательно ARM), ряд популярных архитектур, которые вы будете загружать с адреса на основе регистра (ldr r0, [r1]) и загрузкас индексом / смещением регистра (ldr r0, [r1, r2]), где адрес является суммой двух регистров.один регистр в идеале является базовым адресом массива, а второй - индексом / смещением.Первая загрузка из регистра поддается указателям, вторая - массивам.если ваша C-программа НЕ собирается изменять или перемещать указатель или индекс, то в обоих случаях это означает, что вычисляется статический адрес, а затем используется нормальная загрузка, и массив, и указатель должны выдавать одинаковые инструкции.Для более интересного случая изменения указателя / индекса.

Pointer

ldr r0,[r1]
...
add r1,r1,some number

Array index

ldr r0,[r1,r2]
...
add r2,r2,some number

(при необходимости замените загрузку хранилищем, а добавление - подпрограммой)

В некоторых архитектурах нет трехзарегистрировать инструкцию индекса регистра, так что вы должны сделать что-то вроде

array index:
mov r2,r1
...
ldr r0,[r2]
...
add r2,r2,some number

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

array index:
mov r2,#0
...
mov r3,r1
add r3,r2
ldr r4,[r3]
...
add r2,some number

Так что вполне возможно, что оба подхода равны.Как видно на ARM, он может объединять две (в пределах, ограниченных для непосредственных) инструкций указателя в одну, что делает это немного быстрее.Решение индекса массива сжигает больше регистров, и в зависимости от количества доступных регистров для архитектуры, которая подталкивает вас к необходимости быстрее и чаще выгружать регистры в стек (чем вы с указателями), замедляя вас еще больше.Если вы не возражаете против уничтожения базового адреса, нижняя строка - это решение для указателя может дать вам преимущество с точки зрения производительности.Это во многом связано с вашим кодом и компилятором.Для меня это удобочитаемость, и я чувствую, что массивы легче читать и отслеживать, а во-вторых, мне нужно сохранить этот указатель, чтобы освободить malloc или снова пройти через эту память и т. Д. Если это так, я, вероятно, буду использовать массив синдекс, если это однократный проход, и я не забочусь об уничтожении базового адреса, я буду использовать указатель.Как вы видели выше в коде, сгенерированном компилятором, если производительность критична, то в любом случае вручную закодируйте решение на ассемблере (основываясь на предложенных подходах, позволив компиляторам попробовать это в первую очередь).

6 голосов
/ 09 февраля 2011

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

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

3 голосов
/ 09 февраля 2011

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

Только команды ЦП могут быть быстрее или медленнее, и только инструкции ЦП могут потреблять циклы ЦП. Чтобы каким-то образом перенести эту концепцию «скорости» из инструкций ЦП обратно в операции уровня языка [из которых были созданы эти инструкции ЦП], в общем случае вам необходимо знать контекст. Это так, потому что одна и та же операция на уровне языка может генерировать совершенно разные инструкции ЦП в разных контекстах (даже не говоря уже о том, что это также может зависеть от настроек компилятора и т. Д.)

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

1 голос
/ 09 февраля 2011

Явное исключение общих подвыражений может работать на вас.Может быть разница, если вы используете архитектуру x86 или RISC и качество оптимизатора.

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

struct SOMETHING list[100];

int find_something (...)
{
  int i;

  i=0;
  while (i<(sizeof(list)/sizeof(struct SOMETHING)))
  {
    if (list[i].active && list[i].last_access+60<current_time) return i;

    ++i;
  }
  return -1;
}

можно уточнить (т.е. помочь компилятору создавать лучший код):

int find_something (...)
{
  int i;
  struct SOMETHING *pList;

  i=0;
  while (i<(sizeof(list)/sizeof(struct SOMETHING)))
  {
    pList=&list[i];
    if (pList->active && pList->last_access+60<current_time) return i;

    ++i;
  }
  return -1;
}

Это просто для иллюстрации, и простота кода, вероятно, сгенерируетуказатель неявно, но если подпрограмма является более сложной, это может быть не так.Используя "list [i]."как и в первом примере, вы бы выполнили (на x86) риск (RISC хаха) компилятора, не имея достаточного количества регистров для генерации и сохранения адреса один раз, вместо этого генерируя его для каждой отдельной ссылки.В случае x86 локальная переменная необходима для хранения указателя, и лишь немногие компиляторы будут создавать переменные стека, если явно не указано иное.В RISC компилятор имеет в своем распоряжении множество регистров и обычно решает, что стоит создать (и сохранить) указатель один раз для каждой итерации.

Цикл может быть уточнен далее:

  pList=list;
  i=0;
  while (i<(sizeof(list)/sizeof(struct SOMETHING)))
  {
    if (pList->active && pList->last_access+60<current_time) return i;

    pList+=1;    
    ++i;
  }

Эта конструкция лишена каких-либо затрат на вычисление адреса.«pList + = 1» (другие могут предпочесть «++ pList») приводит к добавлению в pList константного значения (равного размеру отдельной строки / элемента).

И далее:

  pList=list;
  pEndList=&list[sizeof(list)/sizeof(struct SOMETHING)];
  while (pList!=pEndList)
  {
    if (pList->active && pList->last_access+60<current_time) return pList-list;

    pList+=1;    
  }

Что исключает приращение индекса и заменяет его одним умножением снаружи и одним делением внутри цикла (выполняется только один раз, в конструкции возврата).

Теперь, прежде чем все, что вы не оптимизировали изЯ начинаю кричать, кровавое убийство, и я считаю, что приемлемые конструкции определяются размером и сложностью функции, в которой они находятся.Я бы, вероятно, не рассматривал эту конструкцию в функции из 300 строк, которая достаточно сложна для начала, но в ситуации, подобной описанной выше?Если поиски являются значительной частью общей обработки?Если ускорения достаточно велики?

Так почему бы и нет?Плюсы и минусы.Это всегда плюсы и минусы.Делая лучшее из них.Абсолютные?Редко (если вообще когда-либо).

1 голос
/ 09 февраля 2011

На самом низком уровне эти операции обычно компилируются в одно и то же. Если вы действительно заинтересованы, вы должны заставить свой компилятор C генерировать выходные данные сборки (например, с gcc -S), чтобы вы могли проверить, тем более что это зависит, как минимум, от:

  • ваша целевая платформа.
  • ваш компилятор.
  • ваш уровень оптимизации.

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

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

0 голосов
/ 09 февраля 2011

При доступе к массиву через индекс вы фактически выполняете две операции: добавление (добавление индекса к адресу базового массива), затем доступ к памяти (фактически чтение или написание того, что находится по полученному адресу). Я предполагаю, что когда вы говорите о «доступе по указателю», вы имеете в виду, что у вас уже есть указатель на целевой элемент. Таким образом, по логике, использование указателя сохраняет часть «сложения» и, следовательно, должно быть быстрее или, по крайней мере, не медленнее.

Однако ...

В грубом приближении, в современном компьютере доступ к памяти намного дороже, чем добавление (особенно, если оно выпадает из кэшей), поэтому разница, если таковая имеется, будет незначительной. На некоторых архитектурах (например, x86 или PowerPC) добавление и доступ к памяти могут быть объединены в один код операции. Все будет по-другому, в зависимости от того, является ли адрес массива константой времени компиляции (то есть массив не является константными данными, но объявлен как глобальная переменная, против блок, полученный с malloc()) , Использование массива может помочь компилятору найти лучший код в отношении общего указателя (в частности, когда используется ключевое слово restrict). Контекст оказывает огромное влияние (например, сколько свободных регистров существует на тот момент?).

Итак:

  • Нет абсолютного ответа на ваш вопрос. Вы должны попытаться принять меры.
  • Если есть заметная разница (есть вероятность, что ее не будет), трудно предсказать, в каком направлении, и это зависит от огромного набора внешних факторов, включая конкретную версию компилятора и флаги оптимизации, архитектуру процессора и модель, расположение памяти и пр.
  • Вы не сможете получить какой-либо надежный результат оптимизации, не имея достаточно глубоких знаний по сборке и немного теории компиляции.
  • Сначала вы должны сконцентрироваться на правильном коде, а затем беспокоиться только об оптимизации; и нет проблем с производительностью, пока они не будут должным образом измерены в реальных условиях.
0 голосов
/ 09 февраля 2011

То же самое.Это все O (1), а время на часах ничтожно мало.Вы в основном обращаетесь к адресу памяти.

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