好的,以下是获取“模3”循环计数器下一个值的两种方法。
int next1(int n) {
return (n + 1) % 3;
}
int next2(int n) {
return n == 2 ? 0 : n + 1;
}
我已经使用gcc -O3选项(针对普通的x64架构)进行了编译,并使用-s获取了汇编代码。
第一个函数的代码执行一些难以解释的魔术(*),以避免进行除法运算,而是使用乘法代替。
addl $1, %edi
movl $1431655766, %edx
movl %edi, %eax
imull %edx
movl %edi, %eax
sarl $31, %eax
subl %eax, %edx
leal (%rdx,%rdx,2), %eax
subl %eax, %edi
movl %edi, %eax
ret
而第一个函数的长度(我敢打赌速度也会更慢)比第二个函数长得多:
leal 1(%rdi), %eax
cmpl $2, %edi
movl $0, %edx
cmove %edx, %eax
ret
因此,“现代编译器总是比你更好”并不总是正确的。
有趣的是,使用4而不是3进行相同的实验会导致第一个函数的与掩码。
addl $1, %edi
movl %edi, %edx
sarl $31, %edx
shrl $30, %edx
leal (%rdi,%rdx), %eax
andl $3, %eax
subl %edx, %eax
ret
但是它仍然相对于第二版而言,大体上来说,较为低劣。
更明确地介绍正确方法来做事。
int next3(int n) {
return (n + 1) & 3;;
}
产生更好的结果:
leal 1(%rdi), %eax
andl $3, %eax
ret
(*) 嗯,不是很复杂。倒数乘法。计算整数常量 K = (2^N)/3,对于足够大的 N 值。现在,当您想要 X/3 的值时,不要进行除以 3 的运算,而是计算 X*K,并将其向右移动 N 个位置。
len
是一个编译时常数,最近版本的 GCC 编译器(使用-O2
选项)通常会进行聪明的优化,往往可以避免目标处理器的取模指令。 - Basile Starynkevitch