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

时间:2011-11-16 01:16:15

标签: c arrays pointers indexing

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

**我使用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;
     }
}

10 个答案:

答案 0 :(得分:5)

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

上下文中的数组索引(数组绑定未知)与指针操作完全相同。从编译器的角度来看,它只是指针算法的不同表达。以下是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的重复(循环)指令。唯一的区别是因为循环边界的计算。 for中的myIndex循环原样具有行程计数size(即,不需要计算)。但是,myPointer需要一些计算来获得for循环的行程计数。这是唯一的区别。重要的循环操作是一样的。因此,差异可以忽略不计。

总而言之,优化代码中myPointermyIndex的效果应该相同。


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

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

答案 1 :(得分:4)

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

答案 2 :(得分:3)

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

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

答案 3 :(得分:1)

哎呀,在我的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位系统上没有显示任何真正的性能差异。

答案 4 :(得分:0)

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

答案 5 :(得分:0)

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

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

答案 6 :(得分:0)

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

答案 7 :(得分:0)

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

有一个循环持续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 <fun1+62>
   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 <printf@plt>
   0x08048454 <+58>:    addl   $0x1,-0xc(%ebp)
   0x08048458 <+62>:    cmpl   $0x63,-0xc(%ebp)
   0x0804845c <+66>:    jle    0x8048429 <fun1+15>
   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 <fun2+56>
   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 <printf@plt>
   0x08048494 <+52>:    addl   $0x1,-0xc(%ebp)
   0x08048498 <+56>:    cmpl   $0x63,-0xc(%ebp)
   0x0804849c <+60>:    jle    0x804846f <fun2+15>
   0x0804849e <+62>:    leave  
   0x0804849f <+63>:    ret    
End of assembler dump.
(gdb) 

答案 8 :(得分:0)

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

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

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

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

答案 9 :(得分:0)

编译器优化是模式匹配。

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

这是我们在优化时始终坚持实际测量结果性能的原因之一:除非您对其进行测试,否则永远无法确定编译器将代码转换为什么。

如果您真的很好奇,请尝试使用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之间的差异可能很大。