检查奇数和按 2 的幂执行除法的最快方法是什么
What is the fastest way to check for oddness & perform divisions by powers of 2
出于各种原因,通常需要检查一个完整数字的奇数,一个这样的例子是数据网格,其中每条奇数行都是不同的颜色以提高可读性。程序员经常使用公式
来实现这样的功能
odd = n mod 2 != 0
或反过来:
even = n mod 2 == 0
但是这种方法很慢,因为它需要 FPU 计算除法的余数。
我还看到开发人员编写了以下代码:
x = n / 2
odd = x mod 2
有更好的方法吗?
这个问题的答案很简单但经常被忽视,请注意这只适用于整数,而不适用于浮点数。
要让 x86 上的 FPU CPU 执行 division/modulus 计算,单精度至少需要 17 个时钟周期,双精度至少需要 34 个时钟周期。
所以我们可以很好地了解上面代码中更复杂的示例需要多少个周期(最好的情况不计算 CPU load/store 操作)。
x = n / 2 17 cycles
odd = x mod 2 17 cycles
---------
34 cycles
因为计算机用二进制表示整数,我们可以通过检查第一位是否设置来简单地检查数字是否为奇数,这适用于任何支持位运算的语言。
例如:
Psuedo Code: odd = n and 1
C/C++/Java : odd = n & 1;
掩码操作只需要 1 个周期,根据 FPU 模式,每个 division/modulus 操作 的性能提高 17 到 34 倍。
对于第二个例子中的除以2,可以使用位操作来执行,实际上,任何除以2的幂都可以使用位右移操作来执行。例如:
Psuedo Code: x = n shr 1
C/C++/Java : x = n >> 1;
这会将 n
中的所有位向右移动一位,从而执行除以 2,如果向右移动两位,则将除以 4。这可以反向以二的幂相乘,但通常不以这种方式使用,因为乘法运算同样快。
如果我们结合这两种技术,我们可以将第二个示例更改为:
Psuedo Code:
x = n shr 1
odd = x and 1
C/C++/Java:
x = n >> 1;
odd = x & 1;
这可以进一步简化为
Psuedo Code: odd = (n shr 1) and 1
C/C++/Java : odd = (n >> 1) & 1;
与使用 FPU 的版本相比,此版本总共仅使用两个时钟周期来执行数学运算,性能提高了 1700%。
许多编译器都知道这些技巧,并且会在适合您的情况下以这种方式编译代码,但是许多脚本语言并不那么聪明,而且并非所有编译器都是平等的。
例如,编译器可能无法接受以下示例用法:
int doMath(int x, int y) {
return x / y;
}
doMath(10, 2);
doMath(20, 4);
doMath(30, 8);
但是如果它是用幂来写的,它总是会更快,即:
int doMath(int x, int y) {
return x >> y;
}
doMath(10, 1);
doMath(20, 2);
doMath(30, 3);
如果你学习并利用这些技巧,你将编写出非常高效的代码,在任何地方都运行良好,而不必猜测编译器是否会为你创建良好的输出,尤其是当你在 Microsoft/Intel 和 Microsoft/Intel 之间交叉编译代码时在与平台无关的项目中经常使用 GCC。
出于各种原因,通常需要检查一个完整数字的奇数,一个这样的例子是数据网格,其中每条奇数行都是不同的颜色以提高可读性。程序员经常使用公式
来实现这样的功能odd = n mod 2 != 0
或反过来:
even = n mod 2 == 0
但是这种方法很慢,因为它需要 FPU 计算除法的余数。
我还看到开发人员编写了以下代码:
x = n / 2
odd = x mod 2
有更好的方法吗?
这个问题的答案很简单但经常被忽视,请注意这只适用于整数,而不适用于浮点数。
要让 x86 上的 FPU CPU 执行 division/modulus 计算,单精度至少需要 17 个时钟周期,双精度至少需要 34 个时钟周期。
所以我们可以很好地了解上面代码中更复杂的示例需要多少个周期(最好的情况不计算 CPU load/store 操作)。
x = n / 2 17 cycles
odd = x mod 2 17 cycles
---------
34 cycles
因为计算机用二进制表示整数,我们可以通过检查第一位是否设置来简单地检查数字是否为奇数,这适用于任何支持位运算的语言。
例如:
Psuedo Code: odd = n and 1
C/C++/Java : odd = n & 1;
掩码操作只需要 1 个周期,根据 FPU 模式,每个 division/modulus 操作 的性能提高 17 到 34 倍。
对于第二个例子中的除以2,可以使用位操作来执行,实际上,任何除以2的幂都可以使用位右移操作来执行。例如:
Psuedo Code: x = n shr 1
C/C++/Java : x = n >> 1;
这会将 n
中的所有位向右移动一位,从而执行除以 2,如果向右移动两位,则将除以 4。这可以反向以二的幂相乘,但通常不以这种方式使用,因为乘法运算同样快。
如果我们结合这两种技术,我们可以将第二个示例更改为:
Psuedo Code:
x = n shr 1
odd = x and 1
C/C++/Java:
x = n >> 1;
odd = x & 1;
这可以进一步简化为
Psuedo Code: odd = (n shr 1) and 1
C/C++/Java : odd = (n >> 1) & 1;
与使用 FPU 的版本相比,此版本总共仅使用两个时钟周期来执行数学运算,性能提高了 1700%。
许多编译器都知道这些技巧,并且会在适合您的情况下以这种方式编译代码,但是许多脚本语言并不那么聪明,而且并非所有编译器都是平等的。
例如,编译器可能无法接受以下示例用法:
int doMath(int x, int y) {
return x / y;
}
doMath(10, 2);
doMath(20, 4);
doMath(30, 8);
但是如果它是用幂来写的,它总是会更快,即:
int doMath(int x, int y) {
return x >> y;
}
doMath(10, 1);
doMath(20, 2);
doMath(30, 3);
如果你学习并利用这些技巧,你将编写出非常高效的代码,在任何地方都运行良好,而不必猜测编译器是否会为你创建良好的输出,尤其是当你在 Microsoft/Intel 和 Microsoft/Intel 之间交叉编译代码时在与平台无关的项目中经常使用 GCC。