Большой разрыв в производительности между инструкцией процессора CPU и кодом JIT HotSpot

С самого начала 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 будет иметь начальный старт, потому что он будет оценивать первую часть, которая включает только делитель во время компиляции и испускает только вторую часть алгоритма как код времени выполнения. Действительно ли эта гипотеза?

Ответ 1

Как объясняется пользователем pvg через комментарии, гипотетический алгоритм действительно существует и самый известный в настоящее время. Алгоритм включает деление на один и тот же дивизор на подготовительном этапе, поэтому он принципиально неприводим в целом. В главе 10 классического издания Hacker Delight описано.