使用数组偏移与指针增量有什么区别?

时间:2009-11-02 19:15:12

标签: c++

如果有任何差异,给定2个应该更快的功能?假设输入数据非常大

void iterate1(const char* pIn, int Size)
{
   for ( int offset = 0; offset < Size; ++offset )
   {
      doSomething( pIn[offset] );
   }
}

vs

void iterate2(const char* pIn, int Size)
{
   const char* pEnd = pIn+Size;
   while(pIn != pEnd)
   {
      doSomething( *pIn++ );
   }
}

这两种方法都有其他问题需要考虑吗?

11 个答案:

答案 0 :(得分:10)

很可能,你的编译器的优化器会为第一种情况创建一个loop induction variable,将其转换为第二种情况。我希望在优化之后没有区别,所以我倾向于选择第一种风格,因为我觉得它更清晰。

答案 1 :(得分:6)

Boojum是正确的 - IF 你的编译器有一个很好的优化器,你启用了它。如果情况并非如此,或者您对数组的使用不是顺序的并且易于优化,那么使用数组偏移可能会慢得多。

这是一个例子。大约在1988年,我们在Mac II上实现了一个带有简单电传接口的窗口。这由24行80个字符组成。当您从自动收报机中获得新线路时,您向上滚动前23行,并在底部显示新线路。当电传打字机上有某些东西时,它不是所有时间,它以300波特率进入,其中串行协议开销大约为每秒30个字符。所以我们根本不会谈论应该对16 MHz 68020征税的事情!

但写这篇文章的人就是这样的:

char screen[24][80];

并使用2-D数组偏移来滚动字符,如下所示:

int i, j;
for (i = 0; i < 23; i++)
  for (j = 0; j < 80; j++)
    screen[i][j] = screen[i+1][j];
这样的六个窗户让机器瘫痪了!

为什么呢?因为编译器在那些日子里是愚蠢的,所以在机器语言中,内部循环赋值的每个实例screen[i][j] = screen[i+1][j]看起来都像这样(Ax和Dx是CPU寄存器);

Fetch the base address of screen from memory into the A1 register
Fetch i from stack memory into the D1 register
Multiply D1 by a constant 80
Fetch j from stack memory and add it to D1
Add D1 to A1
Fetch the base address of screen from memory into the A2 register
Fetch i from stack memory into the D1 register
Add 1 to D1
Multiply D1 by a constant 80
Fetch j from stack memory and add it to D1
Add D1 to A2
Fetch the value from the memory address pointed to by A2 into D1
Store the value in D1 into the memory address pointed to by A1

因此,对于23x80 = 1840内循环迭代中的每一个,我们正在讨论13种机器语言指令,总共23920条指令,包括3680个CPU密集型整数乘法。

我们对C源代码做了一些更改,所以它看起来像这样:

int i, j;
register char *a, *b;
for (i = 0; i < 22; i++)
{
  a = screen[i];
  b = screen[i+1];
  for (j = 0; j < 80; j++)
    *a++ = *b++;
}

仍有两种机器语言的乘法,但它们在外循环中,因此只有46个整数乘法而不是3680.而内循环*a++ = *b++语句只包含两个机器语言操作

Fetch the value from the memory address pointed to by A2 into D1, and post-increment A2
Store the value in D1 into the memory address pointed to by A1, and post-increment A1.

鉴于有1840个内循环迭代,总共有3680个CPU廉价指令 - 少了6.5倍 - 并且没有整数乘法。在此之后,我们从来没有足够的力量让机器陷入困境,而不是在六个电传打字窗口死亡 - 我们首先耗尽了电传打字机数据源。并且有很多方法可以进一步优化这一点。

现在,现代编译器将为您进行这种优化 - IF 您要求他们这样做,而 IF 您的代码的构造方式允许它

但是仍然存在编译器无法为您执行此操作的情况 - 例如,如果您在阵列中执行非顺序操作。

所以我发现尽可能使用指针而不是数组引用很方便。表现肯定不会更糟,而且往往好多了。

答案 2 :(得分:3)

使用现代编译器,两者之间的性能不应有任何差异,尤其是在这种简单易懂的例子中。此外,即使编译器不能识别它们的等价,即“逐字地”翻译每个代码,在典型的现代硬件平台上仍然不应该有任何明显的性能差异。 (当然,可能会有更专业的平台,其差异可能会很明显。)

至于其他考虑因素......从概念上讲,当您使用索引访问实现算法时,您会对基础数据结构施加随机访问要求。使用指针(“迭代器”)访问时,只对基础数据结构施加顺序访问要求。随机访问比顺序访问更强烈。出于这个原因,我,在我的代码中,我希望尽可能坚持使用指针访问,并且仅在必要时使用索引访问。

更一般地说,如果算法可以通过顺序访问有效地实现,那么最好这样做,而不涉及随机访问的不必要的更强的要求。如果需要重构代码或更改算法,这可能在将来证明是有用的。

答案 3 :(得分:2)

它们几乎相同。这两个解决方案都涉及一个临时变量,系统中一个字的增量(int或ptr),以及一个逻辑检查,它应该采用一个汇编指令。

我看到的唯一区别是数组查找

ARR [IDX]

可能需要指针算术,然后在取消引用时进行提取:

* PTR

只需要提取

我的建议是,如果真的很重要,请实施两者,看看是否有任何节省。

答案 4 :(得分:2)

当然,您必须在预期的目标环境中进行分析。

那就是说,我的猜测是任何现代编译器都会将它们优化到非常相似(如果不相同)的代码。

如果你没有优化器,第二个有可能更快,因为你没有在每次迭代时重新计算指针。但除非Size是一个非常大的数字(或常常调用例程),否则差异对程序的整体执行速度并不重要。

答案 5 :(得分:2)

几年前,我问过这个确切的问题。在一次采访中有人未能通过选择数组符号的候选人,因为据说它显然较慢。那时我编译了两个版本并查看了反汇编。数组表示法中有一个额外的操作码。这是使用Visual C ++(.net?)。根据我所看到的结论,我得出结论,没有明显的区别。

再次这样做,这是我发现的:

    iterate1(arr, 400); // array notation
011C1027  mov         edi,dword ptr [__imp__printf (11C20A0h)] 
011C102D  add         esp,0Ch 
011C1030  xor         esi,esi 
011C1032  movsx       ecx,byte ptr [esp+esi+8] <-- Loop starts here
011C1037  push        ecx  
011C1038  push        offset string "%c" (11C20F4h) 
011C103D  call        edi  
011C103F  inc         esi  
011C1040  add         esp,8 
011C1043  cmp         esi,190h 
011C1049  jl          main+32h (11C1032h) 

    iterate2(arr, 400); // pointer offset notation
011C104B  lea         esi,[esp+8] 
011C104F  nop              
011C1050  movsx       edx,byte ptr [esi] <-- Loop starts here
011C1053  push        edx  
011C1054  push        offset string "%c" (11C20F4h) 
011C1059  call        edi  
011C105B  inc         esi  
011C105C  lea         eax,[esp+1A0h] 
011C1063  add         esp,8 
011C1066  cmp         esi,eax 
011C1068  jne         main+50h (11C1050h) 

答案 6 :(得分:2)

指针op曾经快得多。现在它有点快,但编译器可能会为你优化它

从历史上看,通过*p++进行迭代要比p[i]快得多;这是指导语言的动机的一部分。

另外,p[i]通常需要较慢的乘法操作或至少需要移位,因此在循环中使用添加到指针的替换乘法的优化对于具有特定名称非常重要:强度降低。下标也倾向于产生更大的代码。

然而,有两件事发生了变化:一件是编译器更加复杂,通常能够为您进行优化。

另一个是op和内存访问之间的相对差异已经增加。当*p++被发明时,内存和cpu运行时间相似。今天,一台随机桌面机可以完成30亿整数运算/秒,但只有大约1000万或2000万随机DRAM读取。高速缓存访​​问速度更快,系统将在您逐步执行数组时预取和流式传输顺序内存访问,但是仍然需要花费很多内存,并且下载一些下标并不是一件大事。

答案 7 :(得分:1)

你为什么不尝试两种方式并计时?我的猜测是它们被编译器优化成基本相同的代码。只需记住在比较(-O3)时启用优化。

答案 8 :(得分:1)

在“其他考虑因素”栏中,我会说方法一更清楚。这只是我的观点。

答案 9 :(得分:1)

你问的是错误的问题。 Should a developer aim for readability or performance first?

第一个版本是处理数组的惯用语,对于之前使用过数组的人来说,你的意图是明确的,而第二个版本在很大程度上依赖于数组名和指针之间的等价,迫使某人阅读代码来将隐喻转换为几个倍。

提示评论说第二个版本对于任何值得他的keybaord的开发人员来说都是非常明确的。

如果你编写了程序,并且运行速度很慢,而且你已经将这个循环识别为瓶颈,那么那么就可以将引擎盖放在一边看看哪一个更快。但是首先要使用众所周知的惯用语言结构来清理和运行。

答案 10 :(得分:1)

除了性能问题之外,我认为while循环变体具有潜在的可维护性问题,因为程序员出现添加一些新的铃声和口哨必须记住将数组增量放在正确的位置,而for循环变体把它安全地放在循环体外。