[英]Why java division for integer is faster than hacker's delight implementation
我正在测试来自黑客高兴书籍的divs10函数吞吐量,在我的jdk 1.7 64位版本21和i7 intel盒处理器上用java编码:7 vendor_id:GenuineIntel cpu系列:6型号:26型号名称:Intel(R)Core(TM)i7 CPU 920 @ 2.67GHz
我想知道为什么默认的java运算符/比黑客的快乐书中的divs10函数更快,结果显示divs10比“/”运算符慢3倍,令我惊讶。
任何人都可以告诉我,是否有任何花哨的内在jvm可以使用?
源代码如下。
public class div10 {
public static final int divs10(int n) {
int q, r;
n = n + (n >> 31 & 9);
q = (n >> 1) + (n >> 2);
q += q >> 4;
q += q >> 8;
q += q >> 16;
q = q >> 3;
r = n - ((q << 3) + (q << 1));
return q + ((r + 6) >> 4);
}
public static void main(String[] args) {
/*
long count = 0;
for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) {
if ( (i/10) != divs10(i) ) {
System.err.println("error dividing :" + i );
}
if ((i & 0xFFFFFFF ) == 0 ) {
System.out.println("Finished:" + Long.toHexString(count) + ":" + count + ":" + i);
}
count++;
}
System.out.println("Success:" + count);
*/
long start = System.nanoTime();
long count = 0L;
int iter = 100_000;
for (int j = 0; j < 10; j++)
for (int i = -iter; i < iter; i++) {
count += (i/10);
}
for (int j = 0; j < 10; j++)
for (int i = -iter; i < iter; i++) {
count += divs10(i);
}
System.out.println(count + " warm up done ") ;
start = System.nanoTime();
count = 0L;
for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) {
count += i/10;
}
System.out.println(count + ", took:" + (System.nanoTime() - start) / 1000_000L + " ms, " + (System.nanoTime() - start) / ((long)Integer.MAX_VALUE - (long)Integer.MIN_VALUE) + " ns per ops" ) ;
start = System.nanoTime();
count = 0L;
for (int i = Integer.MIN_VALUE; i < Integer.MAX_VALUE; i++) {
count += divs10(i);
}
System.out.println(count + ", took:" + (System.nanoTime() - start) / 1000_000L + " ms, " + (System.nanoTime() - start) / ((long)Integer.MAX_VALUE - (long)Integer.MIN_VALUE) + " ns per ops" ) ;
}
}
更新:当查看较新的Ivy Bridge表 (第174页)时,我看到所有延迟都在1.这意味着我之前的解释是不正确的。
计算在divs10
方法中执行的指令的尝试是27(没有函数调用的开销)指令。 您正在进行操作,要求在下一个操作开始之前完成前一个操作。 这意味着您应该考虑指令的延迟。 根据Ivy Bridge指令表,所涉及的所有指令都具有1个时钟周期的延迟。 这总共给出了27个时钟周期。
这与单个IDIV(8位)指令相比较。 在表中,我发现这需要大约20个时钟周期的延迟。
原始估计将给出:27个循环/ 20个循环= 1.35倍慢。 这与您的结果不一致。 我不是这方面的专家,但我认为这是因为IDIV指令的划分可以并行运行,因为它们是独立的。 IDIV指令的吞吐量为8个时钟周期。 这允许CPU以这样的方式优化指令:它可以每52个周期运行大约4个分区(这是一个估计)。
因此,要使用位移算法执行4次除法,您需要108个周期,而IDIV则需要大约64个时钟周期。 这给出:108/52 =慢2.1倍。
这接近你测量的比率。 我猜剩下的额外时间用于函数调用的开销。 也许CPU比我的估计做了更大的优化。
当你写:
count += (i/10);
Java JIT能够使用一些很好的技巧来优化每个常量的除法,比如“ Reciprocal Multiplication ” - 参见本文的数学参考 - 或者这个 。
因此,它可以通过单个乘法+移位替换这个除法,这在所有情况下都比绕过的divs10()
函数快得多, divs10()
函数可能在最老的CPU中很快,但不适用于现代CPU,整数乘法需要1或1.5个循环! “黑客的喜悦”技巧确实与普通的386很好地搭配,但不是现代的CPus。
此外,JIT可能能够展开循环,以实现更快的过程,因为计算count += ...
很容易并行。
结论:当您使用Java等高级语言,在带有JIT的VM上运行时,不要指望如何编译代码。 即使任何现代C编译器都能够通过使用“ 倒数乘法 ”技巧来优化count += i/10
,或者展开循环(甚至使其成为多线程)。
让你的(JIT)编译器完成它的工作,如果性能不够,优化你的数据结构和算法,而不是试图“调整”编译器。 如果您想在CPU级别获得一些低级别性能技巧的文档和源代码, 请查看此参考资料 。 但请注意,您将无法使用Java(添加asm需要C / C ++或Delphi编译器)。 最后但同样重要的是,请记住, 过早优化是万恶之源 (Knuth)。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.