С самого начала 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
Показатель производительности довольно необычен. Мое предположение состояло бы в том, что современный процессор Intel имеет достаточно недвижимости, а его инженерам достаточно интерес, чтобы реализовать сложный, но эффективный алгоритм деления на аппаратном уровне. Однако компилятор JIT превосходит Intel, отправив ему поток некоторых других инструкций, которые выполняют одну и ту же работу, всего в семь раз быстрее. Если что-то, выделенный микрокод должен иметь возможность использовать CPU даже лучше, чем JIT может делать через открытый API инструкций по сборке.
Почему idiv
все еще медленнее, каково основное ограничение?
Одним из объяснений, которое приходит на ум, является гипотетическое существование алгоритма деления, который впервые включает в себя дивиденд очень поздно. Тогда компилятор JIT будет иметь начальный старт, потому что он будет оценивать первую часть, которая включает только делитель во время компиляции и испускает только вторую часть алгоритма как код времени выполнения. Действительно ли эта гипотеза?