据说通过指针访问内存比通过数组访问内存更有效。
在过去编译器是相对愚蠢的野兽时,这可能是正确的。您只需要查看gcc
在高优化模式下输出的一些代码即可知道它不再正确。其中一些代码很难理解,但是一旦你理解了,它的光彩就显而易见了。
一个体面的编译器将为指针访问和数组访问生成相同的代码,您可能不必担心这种性能水平。编写编译器的人比我们这些普通人更了解他们的目标架构。在优化代码(算法选择等)时更多地关注宏观层面,并相信您的工具制造商会完成他们的工作。
事实上,我很惊讶编译器没有优化整个
temp = a[0];
行不存在,因为temp
在下一行被不同的值覆盖,并且a
没有被标记volatile
。
我记得很久以前关于最新 VAX Fortran 编译器的基准测试(这里显示我的年龄)的一个城市神话,它的性能超过了它的竞争对手几个数量级。
结果编译器发现基准计算的结果没有在任何地方使用,因此它将整个计算循环优化为遗忘。因此,运行速度的显着提高。
更新:优化代码在您的特定情况下更有效的原因是您找到位置的方式。a
将在链接/加载时间决定的固定位置,并且对它的引用将同时修复。所以a[0]
或者确实a[any constant]
会在一个固定的位置。
出于同样的原因,p
它本身也将位于固定位置。但是 *p
(的内容p
)是可变的,因此需要额外的查找来找到正确的内存位置。
您可能会发现将另一个变量x
设置为 0(不是const
)并使用a[x]
也会引入额外的计算。
在您的一条评论中,您说:
按照您的建议进行操作也会产生 3 条通过数组访问内存的指令(获取索引、获取数组元素的值、存储在 temp 中)。但我仍然无法看到效率。:-(
我对此的回应是,您很可能看不到使用指针的效率。现代编译器的任务不仅仅是确定数组操作和指针操作可以转换为相同的底层机器代码。
事实上,如果不启用优化,指针代码的效率可能会降低。考虑以下翻译:
int *pa, i, a[10];
for (i = 0; i < 10; i++)
a[i] = 100;
/*
movl $0, -16(%ebp) ; this is i, init to 0
L2:
cmpl $9, -16(%ebp) ; from 0 to 9
jg L3
movl -16(%ebp), %eax ; load i into register
movl $100, -72(%ebp,%eax,4) ; store 100 based on array/i
leal -16(%ebp), %eax ; get address of i
incl (%eax) ; increment
jmp L2 ; and loop
L3:
*/
for (pa = a; pa < a + 10; pa++)
*pa = 100;
/*
leal -72(%ebp), %eax
movl %eax, -12(%ebp) ; this is pa, init to &a[0]
L5:
leal -72(%ebp), %eax
addl $40, %eax
cmpl -12(%ebp), %eax ; is pa at &(a[10])
jbe L6 ; yes, stop
movl -12(%ebp), %eax ; get pa
movl $100, (%eax) ; store 100
leal -12(%ebp), %eax ; get pa
addl $4, (%eax) ; add 4 (sizeof int)
jmp L5 ; loop around
L6:
*/
从该示例中,您实际上可以看到指针示例更长,而且没有必要如此。它加载多次而不改变,并且确实pa
在和之间交替。这里的默认优化基本上是没有的。%eax
%eax
pa
&(a[10])
当您切换到优化级别 2 时,您得到的代码是:
xorl %eax, %eax
L5:
movl $100, %edx
movl %edx, -56(%ebp,%eax,4)
incl %eax
cmpl $9, %eax
jle L5
对于数组版本,并且:
leal -56(%ebp), %eax
leal -16(%ebp), %edx
jmp L14
L16:
movl $100, (%eax)
addl $4, %eax
L14:
cmpl %eax, %edx
ja L16
对于指针版本。
我不打算在这里对时钟周期进行分析(因为它工作太多而且我基本上很懒),但我会指出一件事。就汇编指令而言,这两个版本的代码没有太大差异,而且考虑到现代 CPU 实际运行的速度,除非您执行数十亿次这样的操作,否则您不会注意到差异。我总是倾向于更喜欢编写代码以提高可读性,并且只在它成为问题时才担心性能。
顺便说一句,您引用的该声明:
5.3 指针和数组:指针版本通常会更快,但至少对于初学者来说,有点难以立即掌握。
可以追溯到 K&R 的最早版本,包括我在 1978 年仍然编写函数的古老版本:
getint(pn)
int *pn;
{
...
}
从那时起,编译器已经走了很长一段路。