我想确认模运算是一项昂贵的操作,所以我测试了这段代码,检查给定的数字是否是偶数:
bool is_even(int n) {
return (n & 1) == 0;
}
然后这一个:
bool is_even_bis(int n) {
return (n % 2) == 0;
}
我首先使用C#,实际上,使用逻辑和放大器的代码比另一个更快,有时甚至快三倍.使用ILSpy我看到编译成MSIL时没有完成优化,代码严格相同.
但是,正如我的一位朋友在C中发现的那样,使用gcc -O3代码被编译为:
is_even:
mov eax, DWORD PTR [esp+4] # tmp63, n
and eax, 1 # tmp63,
xor eax, 1 # tmp63,
ret
和:
is_even_bis:
mov eax, DWORD PTR [esp+4] # tmp63, n
and eax, 1 # tmp63,
xor eax, 1 # tmp63,
ret
所以基本上完全一样的东西.即使使用-O0优化,操作也不会出现:
is_even:
push ebp #
mov ebp, esp #,
mov eax, DWORD PTR [ebp+8] # tmp63, n
and eax, 1 # D.1837,
test eax, eax # D.1837
sete al #, D.1838
movzx eax, al # D.1836, D.1838
pop ebp #
ret
不用说,编译后的代码在-O0中的is_even和is_even_bis之间也是相同的.
如果我可以说更有趣,我的另一个朋友尝试使用OCaml:
let is_even x = ((x land 1) == 0)
let _ =
let i = ref 100000000 in
while !i > 0 do
ignore (is_even !i);
decr i
done
和:
let is_even_bis x = ((x mod 2) == 0)
let _ =
let i = ref 100000000 in
while !i > 0 do
ignore (is_even_bis !i);
decr i
done
并且似乎模数版本在运行字节码时更快,但在本机代码中更慢!也许有人可以解释这个谜团?
然后我开始想知道为什么它在C#中的表现不同(两个函数之间存在明显的性能差距)以及为什么JIT编译器不应用与gcc相同的优化.我不知道是否有办法拦截JIT编译器的输出,也许这有助于理解?
额外问题:我猜模数是基于除法的,因为除法是在O(n²)时间内完成的(n是数字位数)我们可以说模数具有二次时间复杂度吗?
最佳答案 首先,从便携的角度来看,这些操作没有速度概念.您的断言可能适用于您的系统,但它们对所有系统都无效.出于这个原因,对微观优化的推测毫无意义.通过生成解决有意义问题的程序,对其进行分析以找到占用最多执行时间的代码部分并为这些时间引入更快的算法,您可以找到更重要的优化.通过更快的算法,我的意思是更好的数据结构(或更少的操作),而不是不同的运算符.停止专注于微观优化!
您的C版is_even没有明确定义.它可能会产生负零或陷阱表示,特别是负数.使用陷阱表示是未定义的行为.
看起来您可能看到的差异可能是由您的系统上的signed integer representation引起的.考虑是否使用补码11111111 … 11111110表示-1.你期望-1%2导致-1,而不是0,不是吗? (编辑:……但是你会期望-1和1导致什么,如果-1表示为11111111 … 11111110?)需要一些开销来处理这个用于使用补码的实现有符号整数表示.
也许你的C编译器注意到你使用的%表达式和&您使用的表达式在您的系统上是等效的,因此进行了优化,但C#或OCaml编译器由于某种原因未执行优化.
Bonus question : I guess the modulo is based on division and since the
division is done in O(n²) time (n being the number of digits) can we
say that the modulo has quadratic time complexity?
没有必要考虑这两个基本操作的时间复杂性,因为它们在系统之间会有所不同.我在第一段中介绍了这一点.