почему Java Unsafe CAS (getAndAddInt) быстрее, чем код, написанный вручную

Я написал код cas (цикл while для compare_and_set) вручную вместо прямого вызова метода Unsafe.getAndAddInt. Но когда я использую jmh для проверки производительности, он показывает большую потерю производительности, хотя я написал тот же код, что и просто копию исходного кода метода Unsafe. Кто может помочь мне, что делает эту большую разницу? Заранее спасибо.

Результат jmh:

Benchmark              Mode  Cnt  Score   Error  Units
CASTest.casTest        avgt       0.047          us/op
CASTest.manualCasTest  avgt       0.137          us/op  

исходный код:

package org.sample;

import java.lang.reflect.Field;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import sun.misc.Unsafe;

/**
 * @author Isaac Gao
 * @Date 2020/2/20
 */
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Benchmark)
@Threads(2)
@Measurement(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS)
@Warmup(iterations = 2, time = 1)
@Fork(1)
public class CASTest {

  private static Unsafe getUnsafe() {
    try {
      final Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");

      unsafeField.setAccessible(true);
      return (Unsafe) unsafeField.get(null);
    } catch (NoSuchFieldException | IllegalAccessException e) {
      e.printStackTrace();
    }
    return null;
  }
  private static final Unsafe unsafe = getUnsafe();

  private static final long valueOffset;
  static {
    try {
      valueOffset = unsafe.objectFieldOffset
          (CASTest.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
  }

  private volatile  int value;

  @Benchmark
  public void manualCasTest(Blackhole bh) {
    int andAddIntManually = getAndAddIntManually(this, valueOffset, 1);
    bh.consume(andAddIntManually);
  }

  @Benchmark
  public void casTest(Blackhole  bh) {
    int andAddInt = unsafe.getAndAddInt(this, valueOffset, 1);
    bh.consume(andAddInt);
  }

  public final int getAndAddIntManually(Object o, long offset, int delta) {
    int v;
    do {
      v = unsafe.getIntVolatile(o, offset);
    } while (!unsafe.compareAndSwapInt(o, offset, v, v + delta));
    return v;
  }

  public static void main(String[] args) throws RunnerException {
    Options opt = new OptionsBuilder()
        .include(CASTest.class.getSimpleName())
        .build();
    new Runner(opt).run();
  }
}


person Isaac Gao    schedule 20.02.2020    source источник


Ответы (1)


Исполняемый код не обязательно соответствует тому, что вы видели в исходном коде. Аналогичная несоответствующая производительность копирования и вставки кода обсуждалась в разделе Обманывает ли Java JIT при запуске кода JDK?

Хорошо известные методы могут быть заменены специальными реализациями, независимо от того, было ли исходное объявление native или имело чистую реализацию Java. См. также Что означает "встроить" в исходном коде JVM?

Когда мы смотрим в исходный файл JVM vmSymbols.hpp, строка 1031, мы увидим, что sun.misc.Unsafe.getAndAddInt известно JVM.

Вы можете использовать
-XX:CompileCommand=print,CASTest.casTest -XX:CompileCommand=print,CASTest.manualCasTest
для проверки полученного нативного кода (что обычно является хорошей идеей для оценки результатов тестов).

На X64 вы увидите, что manualCasTest будет скомпилирован так, как вы написали, цикл вокруг инструкции
lock cmpxchg dword ptr [rsi],ebx, в то время как casTest содержит один цикл без инструкции
lock xadd dword ptr [rdx+0ch],r8d (детали могут различаться).

person Holger    schedule 20.02.2020
comment