“Fast” Integer Powers in Java
简短的回答:糟糕的标杆管理方法。你以为我现在已经明白了。
问题是"找到一个快速计算X ^ y的方法,其中X和Y是正整数"。典型的"快速"算法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 | public long fastPower(int x, int y) { // Replaced my code with the"better" version described below, // but this version isn't measurably faster than what I had before long base = x; // otherwise, we may overflow at x *= x. long result = y % 2 == 1 ? x : 1; while (y > 1) { base *= base; y >>= 1; if (y % 2 == 1) result *= base; } return result; } |
我想看看这比调用math.pow()或使用简单的方法(比如x乘以y)快多少,比如:
1 2 3 4 5 6 7 | public long naivePower(int x, int y) { long result = 1; for (int i = 0; i < y; i++) { result *= x; } return result; } |
编辑:好吧,有人向我指出(正确地)我的基准代码没有消耗结果,这完全把一切都抛到一边。一旦我开始使用这个结果,我仍然看到幼稚的方法比"快速"方法快25%。
原文:
I was very surprised to find that the naive approach was 4x faster than the"fast" version, which was itself about 3x faster than the Math.pow() version.
我的测试是使用10000000个测试(然后是1亿个,只是为了确保JIT有时间预热),每个测试都使用随机值(防止调用被优化掉),2<=x<=3,25<=y<=29。我选择了一个很窄的值范围,它不会产生大于2^63的结果,但会偏向于较大的指数,以试图给"快速"版本带来优势。我正在预先生成10000个伪随机数,以从计时中消除这部分代码。
我理解,对于小指数来说,幼稚的版本可能更快。"fast"版本有两个分支,而不是一个分支,通常执行的算术/存储操作是原始分支的两倍,但我预计对于大指数,这仍然会导致fast方法在最佳情况下节省一半的操作,在最坏情况下几乎相同。
有人知道为什么天真的方法会比"快速"版本快得多,即使数据偏向于"快速"版本(即更大的指数)?在运行时,代码中额外的分支是否解释了这么大的差异?
基准代码(是的,我知道我应该为"官方"基准使用一些框架,但这是一个玩具问题)-更新为预热,并使用结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | PowerIf[] powers = new PowerIf[] { new EasyPower(), // just calls Math.pow() and cast to int new NaivePower(), new FastPower() }; Random rand = new Random(0); // same seed for each run int randCount = 10000; int[] bases = new int[randCount]; int[] exponents = new int[randCount]; for (int i = 0; i < randCount; i++) { bases[i] = 2 + rand.nextInt(2); exponents[i] = 25 + rand.nextInt(5); } int count = 1000000000; for (int trial = 0; trial < powers.length; trial++) { long total = 0; for (int i = 0; i < count; i++) { // warm up final int x = bases[i % randCount]; final int y = exponents[i % randCount]; total += powers[trial].power(x, y); } long start = System.currentTimeMillis(); for (int i = 0; i < count; i++) { final int x = bases[i % randCount]; final int y = exponents[i % randCount]; total += powers[trial].power(x, y); } long end = System.currentTimeMillis(); System.out.printf("%25s: %d ms%n", powers[trial].toString(), (end - start)); System.out.println(total); } |
产生输出:
1 2 3 4 5 6 | EasyPower: 7908 ms -407261252961037760 NaivePower: 1993 ms -407261252961037760 FastPower: 2394 ms -407261252961037760 |
使用随机数和试验的参数确实会改变输出特性,但试验之间的比率始终与所示的一致。
EDOCX1的0个方面有两个问题:
不管怎样,我想你的基准测试方法并不完美。4x性能差异听起来很奇怪,如果看不到完整的代码就无法解释。
在应用了上述改进之后,我已经使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | package bench; import org.openjdk.jmh.annotations.*; @State(Scope.Benchmark) public class FastPow { @Param("3") int x; @Param({"25","28","31","32"}) int y; @Benchmark public long fast() { return fastPower(x, y); } @Benchmark public long naive() { return naivePower(x, y); } public static long fastPower(long x, int y) { long result = 1; while (y > 0) { if ((y & 1) == 0) { x *= x; y >>>= 1; } else { result *= x; y--; } } return result; } public static long naivePower(long x, int y) { long result = 1; for (int i = 0; i < y; i++) { result *= x; } return result; } } |
结果:
1 2 3 4 5 6 7 8 9 | Benchmark (x) (y) Mode Cnt Score Error Units FastPow.fast 3 25 thrpt 10 103,406 ± 0,664 ops/us FastPow.fast 3 28 thrpt 10 103,520 ± 0,351 ops/us FastPow.fast 3 31 thrpt 10 85,390 ± 0,286 ops/us FastPow.fast 3 32 thrpt 10 115,868 ± 0,294 ops/us FastPow.naive 3 25 thrpt 10 76,331 ± 0,660 ops/us FastPow.naive 3 28 thrpt 10 69,527 ± 0,464 ops/us FastPow.naive 3 31 thrpt 10 54,407 ± 0,231 ops/us FastPow.naive 3 32 thrpt 10 56,127 ± 0,207 ops/us |
注:整数乘法运算速度非常快,有时甚至比额外的比较还要快。不要期望在
由于作者发布了基准,我必须承认令人惊讶的性能结果来自于常见的基准测试陷阱。我在保留原始方法的同时改进了基准,现在它表明
改进版本中的关键更改是什么?
手动编写微基准是一项困难的任务。这就是为什么强烈建议使用适当的基准框架(如JMH)的原因。
如果没有能力回顾和复制你的基准,那么尝试分解你的结果是没有意义的。这可能是由于输入选择不当、基准测试错误(例如在一个测试之前运行另一个测试(从而给JVM时间"预热")等原因造成的。请分享您的基准代码,而不仅仅是您的结果。
我建议在你的测试中包括番石榴的EDOCX1(SRC),这是一种广泛使用和良好的基准测试方法。虽然您可能能够用某些输入击败它,但在一般情况下,您不太可能改进它的运行时(如果可以,他们很乐意听到)。
不出意料的是,
在我看来,这个问题的第一个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | static public long intPower(int base, int exponent) { if (0 == base || 1 == base) return base; int y = exponent; if (y <= 0) return 0 == y ? 1 : -1 != base ? 0 : y % 2 == 1 ? -1 : 1; long result = y % 2 == 1 ? base : 1, power = base; while (1 < y) { power *= power; y >>= 1; // easier to see termination after Type.SIZE iterations if (y % 2 == 1) result *= power; } return result; } |
如果使用微基准(整数求幂的典型用法是什么?)如果使用框架,请进行适当的预热。千万不要把时间花在微基准测试结果上,因为每个选项的计时运行时间少于5秒。
另一种选择来源于Guava的
1 2 3 4 5 6 7 8 9 10 11 12 13 | public long power(int base, int k) { for (long accum = 1, b = base ;; k >>>= 1) switch (k) { case 0: return accum; case 1: return accum * b; default: if ((k&1) != 0) // guava uses conditional multiplicand accum *= b; b *= b; } } |
while循环(最坏情况)运行:
而幼稚的
因此,对于小值的