效率:数组与指针

通过指针的内存访问被认为比通过数组的内存访问更有效率。 我正在学习C,上面的内容在K&R中有说明。 具体他们说

任何可以通过数组下标来实现的操作也可以用指针来完成。 指针版本通常会更快

我使用visual C ++拆分了下面的代码(Mine是一个686处理器,我禁用了所有的优化)。

int a[10], *p = a, temp; void foo() { temp = a[0]; temp = *p; } 

令我惊讶的是,我发现通过指针访问内存需要3条指令才能通过数组访问内存。 以下是相应的代码。

 ; 5 : temp = a[0]; mov eax, DWORD PTR _a mov DWORD PTR _temp, eax ; 6 : temp = *p; mov eax, DWORD PTR _p mov ecx, DWORD PTR [eax] mov DWORD PTR _temp, ecx 

请帮我理解。 我在这里错过了什么?


正如许多答案和评论所指出的那样,我使用了一个编译时间常量作为数组索引,从而使通过数组访问变得更容易。 下面是以variables作为索引的汇编代码。 我现在有相同数量的指针访问指针和数组。 我更广泛的问题仍然很好。 通过指针进行内存访问并不能提高效率。

 ; 7 : temp = a[i]; mov eax, DWORD PTR _i mov ecx, DWORD PTR _a[eax*4] mov DWORD PTR _temp, ecx ; 8 : ; 9 : ; 10 : temp = *p; mov eax, DWORD PTR _p mov ecx, DWORD PTR [eax] mov DWORD PTR _temp, ecx 

通过指针的内存访问被认为比通过数组的内存访问更有效率。

过去当编译器是相对愚蠢的野兽时,情况可能是这样。 你只需要看看gcc在高优化模式下输出的一些代码,知道它不再是真的。 有些代码很难理解,但是一旦你做了,它的光辉就显而易见了。

一个体面的编译器会为指针访问和数组访问生成相同的代码,你应该不用担心这个级别的性能。 编写编译器的人对我们的目标体系结构的了解远远超过了我们的凡人。 在优化您的代码(algorithmselect等)时,更多地关注macros观层面,并相信您的工具制造商能够完成他们的工作。


事实上,我很惊讶编译器没有优化整个

 temp = a[0]; 

因为temp在下一行中被覆盖了一个不同的值,而且不会标记为volatile

我记得很久以前的一个关于最新VAX Fortran编译器(显示我的年龄)的基准的一个城市神话,它的性能比竞争对手好几个数量级。

原来编译器发现基准计算的结果并没有在任何地方使用,所以它将整个计算循环优化为遗忘。 因此,运行速度的实质性提高。


更新:优化代码在您的特定情况下更高效的原因是因为您find位置的方式。 a将在链接/加载时间决定的一个固定的位置,同时对它的引用将被修正。 所以a[0]或者a[any constant]将会在一个固定的位置。

而且p本身也会出于同样的原因在一个固定的位置。 但是 *p*p的内容)是可变的,因此将会有额外的查找来find正确的内存位置。

你可能会发现有另一个variablesx设置为0(而不是const )并使用a[x]也会引入额外的计算。


在你的一个评论中,你说:

按照你的build议做了3个指令,通过数组访问内存(取指数,数组元素的取值,临时存储)。 但我仍然看不到效率。 🙁

我对此的回应是,你很可能不会看到使用指针的效率。 现代编译器不仅仅是要确定数组操作和指针操作可以变成相同的底层机器代码。

事实上,没有优化打开,指针代码可能效率较低 。 考虑下面的翻译:

 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 ,而不会改变,实际上在pa&(a[10])之间交替%eax 。 这里的默认优化基本上没有。

当你切换到优化级别2时,你得到的代码是:

  xorl %eax, %eax L5: movl $100, %edx movl %edx, -56(%ebp,%eax,4) incl %eax cmpl $9, %eax jle L5 

对于arrays版本,和:

  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指针和数组:指针的版本通常会更快,但至less对于外行来说,有点难以立即掌握。

可追溯到最早的K&R版本,包括1978年的古代版本,其function仍然是:

 getint(pn) int *pn; { ... } 

自那时以来,编译器已经走过了一段很长的路。

在第一种情况下,编译器直接知道数组的地址(也是第一个元素的地址)并访问它。 在第二种情况下,他知道指针的地址,并读取指向该内存位置的指针值。 这实际上是一个额外的间接方法,所以在这里大概是比较慢的。

速度是循环获得的,最重要的是。 当你使用一个数组时,你会使用一个你增加的计数器。 为了计算位置,系统将该计数器与数组元素的大小相乘,然后添加第一个元素的地址以获取地址。 使用指针,所有你需要做的去下一个元素是增加当前指针与元素的大小来得到下一个,假设所有的元素在内存中彼此相邻。

指针运算因此在执行循环时需要less一点的计算。 此外,指向正确元素的指针比在数组中使用索引要快。

尽pipe如此,现代的发展正在慢慢地摆脱许多指针操作。 处理器变得越来越快,数组比指针更容易pipe理。 而且,数组往往会减less代码中的错误数量。 数组将允许索引检查,确保你没有访问数组以外的数据。

如果你正在编程embedded式平台,你很快就会知道指针方法比使用索引要快得多。

 struct bar a[10], *p; void foo() { int i; // slow loop for (i = 0; i < 10; ++i) printf( a[i].value); // faster loop for (p = a; p < &a[10]; ++p) printf( p->value); } 

慢循环每次都必须计算一个+(i * sizeof(struct bar)),而第二个则需要每次都将sizeof(struct bar)添加到p中。 乘法运算使用比在许多处理器上添加更多的时钟周期。

如果你在循环内多次引用[i],你真的开始看到改进。 一些编译器不会caching该地址,因此可能会在循环内重新计算多次。

尝试更新您的示例以使用结构和引用多个元素。

指针自然expression简单的感应variables,而下标在某种程度上本质上需要更复杂的编译器优化


在许多情况下,仅使用下标expression式就要求将一个额外的图层添加到问题中。 递增下标i的循环可以是状态机,而expression式a [i]在技术上要求,每次使用时,都乘以每个元素的大小并加到基地址上。

为了将该访问模式转换为使用指针,编译器必须分析整个循环,并确定每个元素正在被访问。 然后,编译器可以用一个简单的前一个循环值的增量replace下标乘以元素大小的多个实例。 这个过程结合了称为通用子expression式消除归纳可变强度减less的优化

当用指针写入时,整个优化过程并不是必须的,因为程序员通常只是通过数组开始。

有时编译器可以做优化,有时不能。 近些年来,手头上有一个复杂的编译器是比较常见的,所以基于指针的代码并不总是更快

因为数组通常必须是连续的,指针的另一个优点是创build增量分配的组合结构。

正如paxdiablo所说,任何新的编译器都会使它们非常相似。

更重要的是,我看到了arrays比指针更快的情况。 这是在使用vector操作的DSP处理器上。

在这种情况下,使用数组类似于使用限制指针。 因为通过使用两个数组,编译器(隐式)知道它们不指向相同的位置。 但是如果你处理2个指针,编译器可能会认为它们指向相同的位置,并且会跳过pipe道衬里。

例如:

 int a[10],b[10],c[10]; int *pa=a, *pb=b, *pc=c; int i; // fill a and b. fill_arrays(a,b); // set c[i] = a[i]+b[i]; for (i = 0; i<10; i++) { c[i] = a[i] + b[i]; } // set *pc++ = *pa++ + *pb++; for (i = 0; i<10; i++) { *pc++ = *pa++ + *pb++; } 

在情况1中,编译器将很容易地添加a和b,并将值存储到c。

在情况2中,编译器将不会stream水线,因为他可能在保存到C时覆盖a或b。

这是一个非常古老的问题,已经回答了,因此我不需要回答! 但是,我没有注意到一个简单的答案,所以提供一个。

答案:间接访问(指针/数组)可能会增加一个额外的指令来加载(基址)地址,但之后的所有访问(在指向结构的情况下数组/元素的元素)应该只是一个指令因为它仅仅是加载到已经加载的(基本)地址的偏移量。 因此,从某种程度上来说,它将和直接访问一样好。 因此, 在大多数情况下,通过数组/指针访问是等同的,元素访问也像直接访问variables一样好。

防爆。 如果我有10个元素的数组(或指针)或10个成员(通过指向结构的指针访问)的结构,并且我正在访问元素/成员,那么只需要一次可能的附加指令。 所有的元素/成员访问应该只是一个指令之后。

你在这里得到了很好的答案,但是由于你正在学习,所以值得指出的是,那个级别的效率很less引人注目。

当您调整程序以获得最佳性能时,您应至less注意在程序结构中查找并解决更大的问题。 这些修正之后,低级别的优化可以进一步改变。

这是一个如何做到这一点的例子。

指针过去比数组快。 当然,当C语言被devise出来的时候指针还是比较快的。 但是现在,优化器通常可以比使用指针更好地优化数组,因为数组更受限制。

现代处理器的指令集也被devise来帮助优化arrays访问。

所以底线是现在的数组通常比较快,尤其是在循环中使用索引variables的时候。

当然,你仍然希望使用像链表这样的指针,但是旧时间通过数组而不是使用索引variables来遍历指针的优化现在可能是一个不合理的优化。

“指针版本通常会更快”意味着在大多数情况下,编译器更容易生成具有指针(仅需要被解引用)的更高效的代码,而不是具有数组和下标(这意味着编译器需要从arrays的开始移动地址)。 然而,使用现代处理器和优化编译器,典型情况下的数组访问不会比指针访问慢。

特别是在你的情况下,你需要打开优化,以获得相同的结果。

由于0被定义为一个常量,因此[0]也是一个常量,编译器知道它在编译时的位置。 在“正常”的情况下,编译器将不得不从基本+偏移量计算元素地址(偏移量根据元素的大小进行缩放)。

OTOH,p是一个variables,而间接需要额外的移动。

总的来说,数组索引在内部是作为指针algorithm来处理的,所以我不确定K&R想要做什么。

由于大多数人已经给出了详细的答案,我只是举一个直观的例子。 如果你使用更大规模的数组和指针,使用指针的效率会更加显着。 例如,如果你想sorting一个很大的长整型数据集,把它sorting成几个子集,然后合并它们。

long int * testData = calloc(N, sizeof(long int));

对于2017年的日常8G ram机器,我们可以设置N大到4亿,这意味着您将使用大约1.5G内存这个原始数据集。 如果您正在使用MPI ,则可以使用快速分隔数据

 MPI_Scatterv(testData, partitionLength, partitionIndex, MPI_LONG, MPI_IN_PLACE, N/number_of_thread, MPI_LONG, 0, MPI_COMM_WORLD); 

你可以简单的把paritionLength当作一个指针来存储N/number_of_thread作为每个相同部分的长度,并把partitionIndex当作一个存储N / number_of_threads起始索引的指针。 假设你有一个4核CPU,并且你只用4个线程分开你的工作。 MPI肯定会通过参考文献迅速完成这项工作。 但是如果你使用的是数组,那么这个例程必须在数组上运行指针运算来首先find分区。 这不像指针那样直接。 另外,当您合并分区数据集时,您可能想要使用K-way merge来加速。 您需要临时空间来存储四个已sorting的数据集。 在这里,如果你使用指针,你只需要存储4个地址。 但是,如果使用数组,则会存储4个完整的子数组,效率不高。 有时,如果你没有使用MPI_Barrier来确保你的程序是线程安全的, MPI甚至可能会抱怨你的内存实现是坏的。 通过数组方法和指针方法,我得到了一个在8个线程上sorting了4亿个长度值的32G机器,相应的得到了11.054980s和13.182739s。 如果我增加到10亿的大小,我的sorting程序将不会被成功执行,如果我使用数组。 这就是为什么很多人使用指针除了C中的标量之外的每个数据结构