在C中,访问我的数组索引更快或通过指针访问更快?

时间:2011-02-08 23:45:36

标签: c optimization assembly

在C中,访问数组索引更快或者通过指针访问更快? 我的意思是更快,哪一个会占用更少的时钟周期。 该数组不是常量数组。

8 个答案:

答案 0 :(得分:16)

它完全取决于系统,哪一个更快,但两者在功能上是相同的,如果一个实际上更快,我会感到非常惊讶。也就是代码

myArr[index]

完全等同于

*(&myArr[0] + index)

同样,写作

*ptr

相当于写作

ptr[0]

大多数编译器足够聪明,可以解决这个问题,所以如果一个比另一个更快,我会感到惊讶。

更重要的是,你可能不应该太担心这一点。在其他所有工作完成后担心优化问题。如果您发现数组访问确实在扼杀您,那么请考虑寻找更快的替代方案。否则,不要担心;除非您迫切需要优化,否则拥有干净,可读,可维护的代码比拥有优化代码更有价值。

答案 1 :(得分:13)

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])和带寄存器的负载加载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

或者取决于编译器它可能会变得非常糟糕,尤其是如果你编译调试或没有优化,并假设你没有三个寄存器添加

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

因此这两种方法很可能是相同的。正如在ARM上看到的那样,它可以将两个(在立即限制内)指针指令合并为一个,使得速度更快一些。数组索引解决方案会烧掉更多的寄存器,并且取决于架构的可用寄存器数量,这些寄存器会促使您更快更频繁地将寄存器交换到堆栈(比使用指针更快),从而使您的速度更慢。如果您不介意销毁基址,则底线是指针解决方案可能从性能角度为您提供优势。它与您的代码和编译器有很大关系。对我来说,它的可读性发挥作用,我觉得数组更容易阅读和遵循,第二,我需要保留该指针以释放malloc或再次通过该内存,等等。如果是这样,我可能会使用一个数组与一个索引,如果它是一次通过而我不关心破坏基地址我将使用指针。正如您在上面看到的编译器生成代码,如果性能至关重要,那么无论如何都要在汇编程序中手动编写解决方案(基于建议的方法,让编译器先尝试一下)。

答案 2 :(得分:6)

简单的索引操作在我曾经触及的每个编译器上编译为相同的机器代码。通常建议使用索引以提高可读性。

需要根据具体情况检查涉及指针访问与数组索引的不同逻辑的更复杂案例。如果您有疑问,请一如既往地描述您的代码。

答案 3 :(得分:3)

你的问题没有有意义的答案。语言级操作没有与之关联的特定“速度”。它们本身不能“更快”或“更慢”。

只有CPU指令可以更快或更慢,只有CPU指令可以消耗CPU周期。为了以某种方式将“速度”的概念从CPU指令转移回语言级操作[这些CPU指令是从...生成的],一般情况下你需要知道上下文。这是因为相同的语言级操作可以在不同的上下文中生成完全不同的CPU指令(甚至没有提到它可能还取决于编译器设置等。)

换句话说,发布实际代码。作为一个抽象的无上下文问题,它根本没有意义。

答案 4 :(得分:1)

在最低级别,这些操作大多倾向于编译为同一个东西。如果你真的感兴趣,你应该让你的C编译器生成汇编输出(例如gcc -S),这样你就可以检查,特别是因为它依赖于,至少是:

  • 您的目标平台。
  • 你的编译器。
  • 您的优化级别。

你会发现,即使 存在差异(这是值得怀疑的),微观优化的水平大多不值得投入其中。您最好进行宏优化,例如改进算法,因为这样可以提供更多投资回报。

在这种情况下,效果可能很小,我总是优化可读性。

答案 5 :(得分:1)

明确消除常见的子表达式可能对您有用。如果您使用的是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大小写,需要一个局部变量来存储指针,除非明确指向,否则很少有编译器会创建堆栈变量。在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行函数中考虑这个构造,这个函数复杂到足以开始,但在上述情况下呢?如果搜索是整体处理的重要部分?如果加速足够大?

为什么不呢?利弊。它总是有利有弊。充分利用它们。绝对?很少(如果有的话)。

答案 6 :(得分:0)

相同。它都是O(1),时钟时间可以忽略不计。你基本上是在访问内存地址。

答案 7 :(得分:0)

通过索引访问数组时,实际上是在执行两个操作:添加(将索引添加到基本数组地址),然后执行内存访问(实际上读或写结果地址的内容)。我想当你在谈论“通过指针访问”时,你的意思是你已经有了指向目标元素的指针。所以,从逻辑上讲,使用指针会保存“添加”部分,因此应该更快,或者至少不会更慢。

...然而

作为粗略的近似,在现代计算机中,内存访问比添加更加昂贵(特别是如果它从缓存中掉出来),因此差异(如果有的话)将是轻微的。在某些体系结构(例如x86或PowerPC)上,可以将添加和内存访问组合到单个操作码中。事情也会有所不同,这取决于数组地址是否是编译时常量(即数组不是常量数据,而是声明为全局变量, vs 使用{{1}获得的块}})。对于通用指针(特别是在使用malloc()关键字时),使用数组可以帮助编译器找到更好的代码。上下文有很大的影响(例如,那时有多少个自由寄存器?)。

所以:

  • 你的问题没有绝对的答案。你必须尝试采取措施。
  • 如果存在可检测的差异(很可能没有),则很难预测哪个方向,并且它取决于一大堆外部因素,包括特定的编译器版本和优化标志,处理器架构和模型,内存布局等。
  • 如果没有一些相当深入的装配知识和一些编译理论,你将无法获得任何可靠的优化收益。
  • 您应该首先专注于制作正确的代码,然后只关心优化;除非在现实条件下进行适当的测量,否则没有性能问题。