C for循环索引:在新CPU中正向索引速度更快吗?
在我订阅的一个邮件列表中,两位知识渊博的(IMO)程序员正在讨论一些优化的代码,并说了一些大致如下的话: 在5-8年前发布的CPU上,向后迭代循环(例如,(int i=x-1;i>=0;i--){…})稍微快一点,因为将C for循环索引:在新CPU中正向索引速度更快吗?,c,optimization,for-loop,C,Optimization,For Loop,在我订阅的一个邮件列表中,两位知识渊博的(IMO)程序员正在讨论一些优化的代码,并说了一些大致如下的话: 在5-8年前发布的CPU上,向后迭代循环(例如,(int i=x-1;i>=0;i--){…})稍微快一点,因为将i与零进行比较比与其他数字进行比较更有效。但是对于最近的CPU(例如,2008-2009年),推测加载程序逻辑是这样的:如果for循环向前迭代(例如,for(int i=0;i
i
与零进行比较比与其他数字进行比较更有效。但是对于最近的CPU(例如,2008-2009年),推测加载程序逻辑是这样的:如果for循环向前迭代(例如,for(int i=0;i
)
我的问题是,这是真的吗?CPU实现最近是否发生了变化,使得前向循环迭代现在比后向迭代具有优势?如果是,原因是什么?i、 什么改变了
(是的,我知道,过早优化是万恶之源,在担心微优化等问题之前先检查一下我的算法……我只是好奇而已)不,我们不能说CPU实现已经改变,以加快前向循环。这与CPU本身没有多大关系 这与您没有指定要使用的CPU或编译器有关 你不能简单地用C标签问一个关于CPU问题的笼统问题,并期望得到一个智能的答案,因为C标准中没有规定CPU在各种操作中应该有多快 如果您想将问题重新表述为针对特定的CPU和机器语言(因为从C编译器中获得的机器语言完全取决于编译器),您可能会得到更好的答案 无论哪种情况,这都不重要。您应该相信这样一个事实:编写编译器的人比您更了解如何从各种CPU中获得最后的性能 您应该迭代的方向总是由您必须做的事情决定的。例如,如果必须按升序处理数组元素,则可以使用:
for (i = 0; i < 1000; i++) { process (a[i]); }
这仅仅是因为你在倒退中获得的任何优势都会被i
上的额外计算所淹没。这很可能是一个裸环(在身体中没有做功)在一个方向上可能比另一个方向更快,但是,如果你有这样一个裸环,它无论如何都不会做任何真正的功
另一方面,上面的两个循环很可能都归结为相同的机器代码。我看过GCC优化器发布的一些代码,这让我大吃一惊。在我看来,编译器编写者是唯一一个涉及到疯狂优化级别的物种
我的建议是:始终先编写可读性程序,然后针对您遇到的任何特定性能问题(“先让它工作,然后让它快速工作”)。在优化循环时,我更愿意研究循环展开(因为它减少了比较的数量和退出值,并且可以针对并行处理进行优化()取决于循环内部的情况。)我不知道。但我确实知道如何在没有科学有效性保证的情况下快速编写基准(实际上,一个有相当严格的无效性保证的基准)。它有有趣的结果:
#include <time.h>
#include <stdio.h>
int main(void)
{
int i;
int s;
clock_t start_time, end_time;
int centiseconds;
start_time = clock();
s = 1;
for (i = 0; i < 1000000000; i++)
{
s = s + i;
}
end_time = clock();
centiseconds = (end_time - start_time)*100 / CLOCKS_PER_SEC;
printf("Answer is %d; Forward took %ld centiseconds\n", s, centiseconds);
start_time = clock();
s = 1;
for (i = 999999999; i >= 0; i--)
{
s = s + i;
}
end_time = clock();
centiseconds = (end_time - start_time)*100 / CLOCKS_PER_SEC;
printf("Answer is %d; Backward took %ld centiseconds\n", s, centiseconds);
return 0;
}
(答案在多次重复中各有一个。)
使用GCC4.4.1在32位UbuntuLinux中的“英特尔(R)Atom(TM)CPU N270@1.60GHz”(800 MHz,假设只有一个内核)上运行,使用-I9编译
Answer is -1243309311; Forward took 196 centiseconds
Answer is -1243309311; Backward took 228 centiseconds
(答案在多次重复中各有一个。)
查看代码,正向循环被转换为:
; Gcc 3.4.4 on Cygwin for Athlon ; Gcc 4.4.1 on Ubuntu for Atom
L5: .L2:
addl %eax, %ebx addl %eax, %ebx
incl %eax addl $1, %eax
cmpl $999999999, %eax cmpl $1000000000, %eax
jle L5 jne .L2
返回到:
L9: .L3:
addl %eax, %ebx addl %eax, %ebx
decl %eax subl $1, $eax
jns L9 cmpl $-1, %eax
jne .L3
这表明,如果不是其他什么,GCC的行为在这两个版本之间已经发生了变化
将较旧的GCC循环粘贴到较新的GCC的asm文件中会产生以下结果:
Answer is -1243309311; Forward took 194 centiseconds
Answer is -1243309311; Backward took 133 centiseconds
小结:在5岁以上的Athlon上,GCC 3.4.4生成的环路速度相同。在新的方面(是的。但有一点需要注意。反向循环速度更快的想法从未适用于所有旧的CPU。它是x86的东西(如8086到486,可能是奔腾,尽管我不认为更进一步) 据我所知,这种优化从未应用于任何其他CPU体系结构 原因如下 8086有一个专门为用作循环计数器而优化的寄存器。将循环计数放入CX,然后有多条指令将CX递减,如果CX变为零,则设置条件代码。事实上,有一条指令前缀可以放在其他指令之前(REP前缀)这将基本上迭代另一条指令,直到CX达到0 早在我们计算指令的时候,指令就知道使用cx作为循环计数器进行固定周期计数是一种可行的方法,cx针对倒计时进行了优化 但那是很久以前的事了。自从奔腾以来,那些复杂的指令总的来说比使用更多、更简单的指令慢。(RISC宝贝!)现在我们尝试做的关键事情是在加载寄存器和使用寄存器之间留出一些时间,因为管道实际上可以在每个周期中执行多个操作,只要您不尝试一次使用同一寄存器执行多个操作
现在,影响性能的不是比较,而是分支,而且只有在分支预测预测错误的情况下。您真正关心的是预取,而不是循环控制逻辑 一般来说,循环性能不会由控制逻辑决定(即每次检查的递增/递减和条件)。做这些事情所需的时间是无关紧要的,除非是在非常紧密的循环中。如果你对此感兴趣,请查看8086计数器寄存器上的详细信息,以及为什么在过去倒计时更有效
L9: .L3:
addl %eax, %ebx addl %eax, %ebx
decl %eax subl $1, $eax
jns L9 cmpl $-1, %eax
jne .L3
Answer is -1243309311; Forward took 194 centiseconds
Answer is -1243309311; Backward took 133 centiseconds
for (i = n; --i >= 0; ) blah blah
for (i = 0; i < N; ++i)
r[i] = (a[i] + b[i]) * c[i];
.L10:
addl $1, %edx
vmovupd (%rdi,%rax), %xmm1
vinsertf128 $0x1, 16(%rdi,%rax), %ymm1, %ymm1
vmovupd (%rsi,%rax), %xmm0
vinsertf128 $0x1, 16(%rsi,%rax), %ymm0, %ymm0
vaddpd (%r9,%rax), %ymm1, %ymm1
vmulpd %ymm0, %ymm1, %ymm0
vmovupd %xmm0, (%rcx,%rax)
vextractf128 $0x1, %ymm0, 16(%rcx,%rax)
addq $32, %rax
cmpl %r8d, %edx
jb .L10
for (i = 0; i < N; ++i)
r[N-1-i] = (a[N-1-i] + b[N-1-i]) * c[N-1-i];
.L5:
vmovsd a+79992(%rax), %xmm0
subq $8, %rax
vaddsd b+80000(%rax), %xmm0, %xmm0
vmulsd c+80000(%rax), %xmm0, %xmm0
vmovsd %xmm0, r+80000(%rax)
cmpq $-80000, %rax
jne .L5