我想确认模运算是一项昂贵的操作,所以我测试了这段代码,检查给定的数字是否是偶数:
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
不用说,is_even
中的is_even_bis
和-O0
之间的编译代码是相同的。
如果我可以说更有趣,我的另一位朋友也尝试使用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是数字位数),我们可以说模数具有二次时间复杂度吗?
答案 0 :(得分:2)
首先,在 portable 意义上,这些操作没有 speed 的概念。您的断言可能适用于您的系统,但它们对所有系统无效。出于这个原因,对微观优化的推测毫无意义。通过生成解决有意义问题的程序,对其进行分析以找到占用最多执行时间的代码部分并为这些时间引入更快的算法,您可以找到更重要的优化。通过更快的算法,我的意思是更好的数据结构(或更少的操作),而不是不同的运算符。停止关注微观优化!
您的C版is_even
没有明确定义。它可能会产生负零或陷阱表示,特别是负数。使用陷阱表示是未定义的行为。
您可能看到的差异可能是由您系统上的signed integer representation造成的。考虑是否使用补码11111111...11111110
来表示-1。你期望-1 % 2
导致-1,而不是0,不是吗? (编辑:...但是如果-1表示为-1 & 1
,您希望11111111...11111110
得到什么?)需要一些开销来处理这个问题对于使用一些补码作为有符号整数表示的实现。
也许您的C编译器已经注意到您使用的%
表达式和您使用的&
表达式在您的系统上等效,因此进行了优化,但无论出于何种原因,C#或OCaml编译器都没有执行优化。
奖金问题:我猜模数是基于除法的 我们可以在O(n²)时间内完成除法(n是数字位数) 说模数具有二次时间复杂度?
没有必要考虑这两个基本操作的时间复杂性,因为它们在系统之间会有所不同。我在第一段中介绍了这一点。