Große Leistungslücke zwischen dem Div-Befehl der CPU und dem JIT-Code von HotSpot

Seit Beginn der CPU war allgemein bekannt, dass der Integer-Divisionsbefehl teuer ist. Ich habe nachgesehen, wie schlimm es heute auf CPUs ist, die den Luxus von Milliarden von Transistoren haben. Ich fand, dass die Hardwareidivie @ -Anweisung ist für konstante Teiler immer noch deutlich schlechter als der Code, den der JIT-Compiler ausgeben kann, der das @ nicht enthälidiv Anweisung

Um dies in einem speziellen Mikrobenchmark herauszustellen, habe ich Folgendes geschrieben:

@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;
  }
}

urz gesagt, ich habe zwei in jeder Hinsicht identische Methoden, mit der Ausnahme, dass eine divVar) dividiert durch eine Zahl, die aus einem Array gelesen wurde, während die andere durch eine Kompilierzeitkonstante dividiert wird. Das sind die Ergebnisse:

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

Das Leistungsverhältnis ist ziemlich außergewöhnlich. Ich gehe davon aus, dass ein moderner Intel-Prozessor über genügend Immobilien verfügt und die Ingenieure ein ausreichendes Interesse daran haben, einen komplexen, aber performanten Divisionsalgorithmus in Hardware zu implementieren. Dennoch schlägt der JIT-Compiler Intel, indem er ihm einen Strom von anderen Anweisungen sendet, die denselben Job ausführen, nur siebenmal schneller. Wenn überhaupt, sollte dedizierter Mikrocode in der Lage sein, die CPU noch besser zu nutzen als das, was JIT über die öffentliche API von Assembly-Anweisungen kann.

Woheridiv ist immer noch so viel langsamer, was ist die grundlegende Einschränkung?

Eine Erklärung, die in den Sinn kommt, ist die hypothetische Existenz eines Divisionsalgorithmus, der die Dividende zum ersten Mal sehr spät in den Prozess einbezieht. Der JIT-Compiler hätte dann einen Vorsprung, da er den ersten Teil auswertet, an dem nur der Divisor zur Kompilierungszeit beteiligt ist, und nur den zweiten Teil des Algorithmus als Laufzeitcode ausgibt. Ist diese Hypothese wahr?

Antworten auf die Frage(2)

Ihre Antwort auf die Frage