为什么我的数组索引比指针快

为什么数组索引比指针快? 指针不应该比数组索引快吗?

**我使用time.h clock_t来测试两个函数,每个函数循环200万次。

Pointer time : 0.018995 Index time : 0.017864 void myPointer(int a[], int size) { int *p; for(p = a; p < &a[size]; p++) { *p = 0; } } void myIndex(int a[], int size) { int i; for(i = 0; i < size; i++) { a[i] = 0; } } 

不,永远不会指针比数组索引更快。 如果其中一个代码比另一个快,那主要是因为某些地址计算可能不同。 该问题还应该提供编译器和优化标志的信息,因为它会严重影响性能。

上下文中的数组索引(数组绑定未知)与指针操作完全相同。 从编译器的角度来看,它只是指针算法的不同表达。 以下是Visual Studio 2010中优化的x86代码示例,该代码具有完全优化无内联

  3: void myPointer(int a[], int size) 4: { 013E1800 push edi 013E1801 mov edi,ecx 5: int *p; 6: for(p = a; p < &a[size]; p++) 013E1803 lea ecx,[edi+eax*4] 013E1806 cmp edi,ecx 013E1808 jae myPointer+15h (13E1815h) 013E180A sub ecx,edi 013E180C dec ecx 013E180D shr ecx,2 013E1810 inc ecx 013E1811 xor eax,eax 013E1813 rep stos dword ptr es:[edi] 013E1815 pop edi 7: { 8: *p = 0; 9: } 10: } 013E1816 ret 13: void myIndex(int a[], int size) 14: { 15: int i; 16: for(i = 0; i < size; i++) 013E17F0 test ecx,ecx 013E17F2 jle myIndex+0Ch (13E17FCh) 013E17F4 push edi 013E17F5 xor eax,eax 013E17F7 mov edi,edx 013E17F9 rep stos dword ptr es:[edi] 013E17FB pop edi 17: { 18: a[i] = 0; 19: } 20: } 013E17FC ret 

一目了然, myIndex看起来更快,因为指令数量较少,但是,这两段代码基本相同。 两者最终都使用rep stos ,这是x86的重复(循环)指令。 唯一的区别是因为循环边界的计算。 myIndexfor循环具有行程计数size (即,不需要计算)。 但是, myPointer需要一些计算来获得for循环的行程计数。 这是唯一的区别。 重要的循环操作是一样的。 因此,差异可以忽略不计。

总而言之, myPointermyIndex在优化代码中的性能应该相同。


FYI,如果数组的绑定在编译时是已知的,例如int A[constant_expression] ,则对该数组的访问可能比指针1快得多。 这主要是因为数组访问没有指针分析问题。 编译器可以完美地计算固定大小数组上的计算和访问的依赖性信息,因此它可以进行高级优化,包括自动并行化。

但是,如果计算是基于指针的,则编译器必须执行指针分析以进一步优化,这在C / C ++中非常有限。 它通常在指针分析上得到保守的结果,并导致一些优化机会。

它可能是导致差异的for循环中的比较。 在每次迭代时测试终止条件,并且您的“指针”示例具有稍微复杂的终止条件(取&a [size]的地址)。 由于&a [size]不会改变,您可以尝试将其设置为变量,以避免在循环的每次迭代中重新计算它。

数组解除引用p[i]*(p + i) 。 编译器使用在1或2个周期内执行数学+解除引用的指令(例如x86 LEA指令)来优化速度。

使用指针循环,它将访问和偏移分成单独的部分,编译器无法对其进行优化。

哎呀,在我的64位系统上的结果是完全不同的。 我有这个

  int i; for(i = 0; i < size; i++) { *(a+i) = 0; } 

是大约100倍!! 慢于此

  int i; int * p = a; for(i = 0; i < size; i++) { *(p++) = 0; } 

-O3编译时。 这告诉我,以某种方式移动到下一个地址对于64位cpu来说要比从某个偏移量计算目标地址容易得多。 但我不确定。

编辑:
这确实与64位体系结构有关,因为具有相同编译标志的相同代码在32位系统上没有显示出任何真正的性能差异。

时间如此紧密,如果你反复做到这一点,你可能看不出太大差异。 两个代码段都编译为完全相同的程序集。 根据定义,没有区别。

我建议运行每个循环2亿次,然后运行每个循环10次,并采取最快的测量。 这将影响OS调度等的影响。

我建议你反汇编每个循环的代码。

看起来索引解决方案可以在for循环中使用compare来保存一些指令。

通过数组索引或指针访问数据完全等效。 和我一起完成以下课程

有一个循环持续100次但是当我们看到反汇编代码时,我们访问的数据具有最少的指令可通过数组索引访问

但这并不意味着通过指针访问数据实际上很快它取决于编译器执行的指令。指针和数组索引使用地址数组访问来自offset的值并通过它递增并且指针具有地址。

 int a[100]; fun1(a,100); fun2(&a[0],5); } void fun1(int a[],int n) { int i; for(i=0;i<=99;i++) { a[i]=0; printf("%d\n",a[i]); } } void fun2(int *p,int n) { int i; for(i=0;i<=99;i++) { *p=0; printf("%d\n",*(p+i)); } } disass fun1 Dump of assembler code for function fun1: 0x0804841a <+0>: push %ebp 0x0804841b <+1>: mov %esp,%ebp 0x0804841d <+3>: sub $0x28,%esp`enter code here` 0x08048420 <+6>: movl $0x0,-0xc(%ebp) 0x08048427 <+13>: jmp 0x8048458  0x08048429 <+15>: mov -0xc(%ebp),%eax 0x0804842c <+18>: shl $0x2,%eax 0x0804842f <+21>: add 0x8(%ebp),%eax 0x08048432 <+24>: movl $0x0,(%eax) 0x08048438 <+30>: mov -0xc(%ebp),%eax 0x0804843b <+33>: shl $0x2,%eax 0x0804843e <+36>: add 0x8(%ebp),%eax 0x08048441 <+39>: mov (%eax),%edx 0x08048443 <+41>: mov $0x8048570,%eax 0x08048448 <+46>: mov %edx,0x4(%esp) 0x0804844c <+50>: mov %eax,(%esp) 0x0804844f <+53>: call 0x8048300  0x08048454 <+58>: addl $0x1,-0xc(%ebp) 0x08048458 <+62>: cmpl $0x63,-0xc(%ebp) 0x0804845c <+66>: jle 0x8048429  0x0804845e <+68>: leave 0x0804845f <+69>: ret End of assembler dump. (gdb) disass fun2 Dump of assembler code for function fun2: 0x08048460 <+0>: push %ebp 0x08048461 <+1>: mov %esp,%ebp 0x08048463 <+3>: sub $0x28,%esp 0x08048466 <+6>: movl $0x0,-0xc(%ebp) 0x0804846d <+13>: jmp 0x8048498  0x0804846f <+15>: mov 0x8(%ebp),%eax 0x08048472 <+18>: movl $0x0,(%eax) 0x08048478 <+24>: mov -0xc(%ebp),%eax 0x0804847b <+27>: shl $0x2,%eax 0x0804847e <+30>: add 0x8(%ebp),%eax 0x08048481 <+33>: mov (%eax),%edx 0x08048483 <+35>: mov $0x8048570,%eax 0x08048488 <+40>: mov %edx,0x4(%esp) 0x0804848c <+44>: mov %eax,(%esp) 0x0804848f <+47>: call 0x8048300  0x08048494 <+52>: addl $0x1,-0xc(%ebp) 0x08048498 <+56>: cmpl $0x63,-0xc(%ebp) 0x0804849c <+60>: jle 0x804846f  0x0804849e <+62>: leave 0x0804849f <+63>: ret End of assembler dump. (gdb) 

这是一件非常困难的事情,因为编译器非常善于优化这些东西。 仍然最好给编译器尽可能多的信息,这就是为什么在这种情况下我建议使用std :: fill,让编译器选择。

但是……如果你想进入细节

a)CPU通常自由地给指针+值,如:mov r1,r2(r3)。
b)这意味着索引操作只需要:mul r3,r1,size
每个循环只需一个周期。
c)CPU通常提供停顿/延迟槽,这意味着您通常可以隐藏单周期操作。

总而言之,即使你的循环非常大,访问的成本也只是一些缓存未命中的成本。 在关注循环成本之前,最好建议优化结构。 例如,尝试打包结构以减少内存占用

编译器优化是模式匹配。

当编译器进行优化时,它会查找已知的代码模式,然后根据某些规则转换代码。 您的两个代码段似乎会触发不同的转换,从而产生稍微不同的代码。

这就是为什么我们总是坚持在优化时实际测量结果性能的原因之一:除非你测试它,否则你永远不能确定你的编译器将代码转换成什么。


如果你真的好奇,尝试使用gcc -S -Os编译你的代码,这会产生最可读但优化的汇编程序代码。 在你的两个函数中,我得到以下汇编程序:

 pointer code: .L2: cmpq %rax, %rdi jnb .L5 movl $0, (%rdi) addq $4, %rdi jmp .L2 .L5: index code: .L7: cmpl %eax, %esi jle .L9 movl $0, (%rdi,%rax,4) incq %rax jmp .L7 .L9: 

差异很小,但可能确实会引发性能差异,最重要的是使用addqincq之间的差异可能很大。