我想确认取模运算是否是一项昂贵的操作,因此我测试了这段代码,用于检查给定数字是否为偶数:
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是数字的位数),我们可以说取模具有二次时间复杂度吗?