研究我发现使用(i+1)mod(SIZE)
在元素数组中执行循环。
所以我想知道这种方法是否比if语句更有效...
例如:
#define SIZE 15
int main(int argc, char *argv[]) {
int items[SIZE];
for(int i = 0; items[0] < 5; i = (i + 1) % SIZE) items[i] += 1;
return 0;
}
比(?)更有效:
#define SIZE 15
int main(int argc, char *argv[]) {
int items[SIZE];
for(int i = 0; items[0] < 5; i++) {
if(i == SIZE) i = 0;
items[i] += 1;
}
return 0;
}
感谢您的回答和时间。
答案 0 :(得分:3)
您可以在线检查程序集(即here)。结果取决于体系结构和优化,但是没有优化,对于带有GCC的x64,您将获得此代码(作为简单示例)。
示例1:
main:
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-68], edi
mov QWORD PTR [rbp-80], rsi
mov DWORD PTR [rbp-4], 0
.L3:
mov eax, DWORD PTR [rbp-64]
cmp eax, 4
jg .L2
mov eax, DWORD PTR [rbp-4]
cdqe
mov eax, DWORD PTR [rbp-64+rax*4]
lea edx, [rax+1]
mov eax, DWORD PTR [rbp-4]
cdqe
mov DWORD PTR [rbp-64+rax*4], edx
mov eax, DWORD PTR [rbp-4]
add eax, 1
movsx rdx, eax
imul rdx, rdx, -2004318071
shr rdx, 32
add edx, eax
mov ecx, edx
sar ecx, 3
cdq
sub ecx, edx
mov edx, ecx
mov DWORD PTR [rbp-4], edx
mov ecx, DWORD PTR [rbp-4]
mov edx, ecx
sal edx, 4
sub edx, ecx
sub eax, edx
mov DWORD PTR [rbp-4], eax
jmp .L3
.L2:
mov eax, 0
pop rbp
ret
示例2:
main:
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-68], edi
mov QWORD PTR [rbp-80], rsi
mov DWORD PTR [rbp-4], 0
.L4:
mov eax, DWORD PTR [rbp-64]
cmp eax, 4
jg .L2
cmp DWORD PTR [rbp-4], 15
jne .L3
mov DWORD PTR [rbp-4], 0
.L3:
mov eax, DWORD PTR [rbp-4]
cdqe
mov eax, DWORD PTR [rbp-64+rax*4]
lea edx, [rax+1]
mov eax, DWORD PTR [rbp-4]
cdqe
mov DWORD PTR [rbp-64+rax*4], edx
add DWORD PTR [rbp-4], 1
jmp .L4
.L2:
mov eax, 0
pop rbp
ret
您看到,对于使用x86的特定情况,不带模的解决方案要短得多。
答案 1 :(得分:1)
尽管您仅询问mod
与branch
,但根据mod
和分支的实际实现,可能会有更多类似的五种情况:
如果SIZE
的值是编译器已知的并且是2的幂,则mod
将编译为单个and
like this,并且非常有效性能和代码大小。 and
仍然是循环增量依赖关系链的一部分,除非迭代器足够聪明地展开and
并将其SIZE
排除在外,否则speed limit会在每次迭代的2个周期的性能上使用的链条(不是gcc和clang)。
反之,如果SIZE
的值是已知的,但不是2的幂,那么您很可能会获得基于乘法的固定模量值like this的实现。这通常需要4-6条指令,它们最终成为依赖链的一部分。因此,这将使您的性能每5-8个周期受限于1次迭代,具体取决于依赖链的延迟。
在您的示例中,SIZE
是一个已知常量,但是在更一般的情况下,在编译时未知该常量,您将在支持该常量的平台上获得除法指令。 like this。
这对于代码大小来说是有好处的,因为它是一条指令,但是对于性能而言可能是灾难性的,因为现在您将慢速除法指令作为循环的已依赖项的一部分。根据您的硬件和-O2
变量的类型,您每次迭代需要20-100个周期。
您在代码中放入了一个分支,但是跳转编译器决定将其作为条件跳转或条件移动来实现。在i == SIZE
,gcc decides on a jump and clang on a conditional move。
这是您的代码的直接解释:使用条件分支来实现SIZE
条件。
它具有使条件成为控制依赖项而不是数据依赖项的优点,因此,当不采用分支时,循环将以全速运行。
但是,如果分支经常错误地预测,则性能可能会受到严重影响。这在很大程度上取决于SIZE
的值和您的硬件。现代的Intel应该能够预测多达20多个迭代的嵌套循环,但是除此之外,每次退出内部循环时,它都会发生错误的预测。当然,SIZE
很大,那么单个错误预测无论如何都不会有多大关系,因此最坏的情况是i
足够大,足以导致错误预测。
clang使用条件移动来更新case_when
。这是一个合理的选择,但是它确实意味着要携带3-4个周期的数据流。
1 像您的示例一样实际上一个常数,或者由于内联和恒定传播而实际上是一个常数。