在C中,访问数组索引更快或者通过指针访问更快? 我的意思是更快,哪一个会占用更少的时钟周期。 该数组不是常量数组。
答案 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()
关键字时),使用数组可以帮助编译器找到更好的代码。上下文有很大的影响(例如,那时有多少个自由寄存器?)。
所以: