自从 CPU 出现以来,人们普遍认为整数除法指令很昂贵。我去看看今天有多糟糕,在拥有数十亿晶体管的 CPU 上。我发现idiv
对于常量除数,硬件指令的性能仍然比 JIT 编译器能够发出的代码(不包含该idiv
指令)差得多。
为了在专用的微基准测试中提出这一点,我编写了以下内容:
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@OperationsPerInvocation(MeasureDiv.ARRAY_SIZE)
@Warmup(iterations = 8, time = 500, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@State(Scope.Thread)
@Fork(1)
public class MeasureDiv
{
public static final int ARRAY_SIZE = 128;
public static final long DIVIDEND_BASE = 239520948509234807L;
static final int DIVISOR = 10;
final long[] input = new long[ARRAY_SIZE];
@Setup(Level.Iteration) public void setup() {
for (int i = 0; i < input.length; i++) {
input[i] = DIVISOR;
}
}
@Benchmark public long divVar() {
long sum = 0;
for (int i = 0; i < ARRAY_SIZE; i++) {
final long in = input[i];
final long dividend = DIVIDEND_BASE + i;
final long divisor = in;
final long quotient = dividend / divisor;
sum += quotient;
}
return sum;
}
@Benchmark public long divConst() {
long sum = 0;
for (int i = 0; i < ARRAY_SIZE; i++) {
final long in = input[i];
final long dividend = DIVIDEND_BASE + in;
final int divisor = DIVISOR;
final long quotient = dividend / divisor;
sum += quotient;
}
return sum;
}
}
简而言之,我有两种在各个方面都相同的方法,除了一个 ( divVar
) 执行除以从数组中读取的数字,而另一个除以编译时常量。这些是结果:
Benchmark Mode Cnt Score Error Units
MeasureDiv.divConst avgt 5 1.228 ± 0.032 ns/op
MeasureDiv.divVar avgt 5 8.913 ± 0.192 ns/op
性能比是相当非凡的。我的期望是现代英特尔处理器有足够的空间,并且它的工程师有足够的兴趣在硬件中实现复杂但高性能的除法算法。然而,JIT 编译器通过向英特尔发送执行相同工作的其他一些指令流来击败英特尔,速度仅快七倍。如果有的话,专用微码应该能够比 JIT 通过汇编指令的公共 API 更好地利用 CPU。
怎么idiv
还是这么慢,根本限制是什么?
想到的一个解释是除法算法的假设存在,该算法在很晚的时候第一次涉及到被除数。JIT 编译器将领先一步,因为它会在编译时评估仅涉及除数的第一部分,并仅将算法的第二部分作为运行时代码发出。这个假设是真的吗?