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

В C, доступ к моему индексу массива быстрее или доступ по указателю быстрее?

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

4b9b3361

Ответ 1

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

EDIT:

Я надеялся, что компилятор должен как минимум использовать команду 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]) и нагрузку с регистром index/offset (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

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

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

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

Ответ 2

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

myArr[index]

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

*(&myArr[0] + index)

Аналогично, записывая

*ptr

Является эквивалентным записи

ptr[0]

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

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

Ответ 3

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

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

Ответ 4

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

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

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

Ответ 5

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

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

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

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

Ответ 6

Явное исключение общих подвыражений может сработать для вас. Возможно, существует разница, если вы используете архитектуру 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 haha) компилятора, не имея достаточного количества регистров для генерации и хранения адреса один раз, вместо этого генерируя его для каждой отдельной ссылки. Для x86-case для хранения указателя необходима локальная переменная, и несколько компиляторов будут создавать переменные стека, если явно не указано. В 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-строчной функции, которая достаточно сложна для начала, но в такой ситуации, как выше? Если поиск является значительной частью общей обработки? Если ускорения достаточно велики?

Так почему бы и нет? За и против. Это всегда плюсы и минусы. Избавьтесь от них. Абсолютные? Редко (если когда-либо).

Ответ 7

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

Ответ 8

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

Однако...

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

Итак:

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