СлабыйСравнить AndSwap и сравнить AndSwap

Этот вопрос не о разнице между ними - я знаю, что такое ложный сбой и почему он происходит на LL/SC. У меня вопрос: если я использую Intel x86 и использую Java-9 (сборка 149), почему существует разница между их ассемблерным кодом?

public class WeakVsNonWeak {

    static jdk.internal.misc.Unsafe UNSAFE = jdk.internal.misc.Unsafe.getUnsafe();

    public static void main(String[] args) throws NoSuchFieldException, SecurityException {

        Holder h = new Holder();
        h.setValue(33);
        Class<?> holderClass = Holder.class;
        long valueOffset = UNSAFE.objectFieldOffset(holderClass.getDeclaredField("value"));

        int result = 0;
        for (int i = 0; i < 30_000; ++i) {
            result = strong(h, valueOffset);
        }
        System.out.println(result);

    }

    private static int strong(Holder h, long offset) {
        int sum = 0;
        for (int i = 33; i < 11_000; ++i) {
            boolean result = UNSAFE.weakCompareAndSwapInt(h, offset, i, i + 1);
            if (!result) {
                sum++;
            }
        }
        return sum;

    }

    public static class Holder {

        private int value;

        public int getValue() {
            return value;
        }

        public void setValue(int value) {
            this.value = value;
        }
    }
}

Работает с:

 java -XX:-TieredCompilation 
      -XX:CICompilerCount=1 
      -XX:+UnlockDiagnosticVMOptions  
      -XX:+PrintIntrinsics 
      -XX:+PrintAssembly 
      --add-opens java.base/jdk.internal.misc=ALL-UNNAMED
      WeakVsNonWeak

Выходные данные compareAndSwapInt(соответствующие части):

     0x0000000109f0f4b8: movabs $0x111927c18,%rsi  ;   {metadata({method} {0x0000000111927c18} 'compareAndSwapInt' '(Ljava/lang/Object;JII)Z' in 'jdk/internal/misc/Unsafe')}
  0x0000000109f0f4c2: mov    %r15,%rdi
  0x0000000109f0f4c5: test   $0xf,%esp
  0x0000000109f0f4cb: je     0x0000000109f0f4e3
  0x0000000109f0f4d1: sub    $0x8,%rsp
  0x0000000109f0f4d5: callq  0x00000001098569d2  ;   {runtime_call SharedRuntime::dtrace_method_entry(JavaThread*, Method*)}
  0x0000000109f0f4da: add    $0x8,%rsp
  0x0000000109f0f4de: jmpq   0x0000000109f0f4e8
  0x0000000109f0f4e3: callq  0x00000001098569d2  ;   {runtime_call SharedRuntime::dtrace_method_entry(JavaThread*, Method*)}
  0x0000000109f0f4e8: pop    %r9
  0x0000000109f0f4ea: pop    %r8
  0x0000000109f0f4ec: pop    %rcx
  0x0000000109f0f4ed: pop    %rdx
  0x0000000109f0f4ee: pop    %rsi
  0x0000000109f0f4ef: lea    0x210(%r15),%rdi
  0x0000000109f0f4f6: movl   $0x4,0x288(%r15)
  0x0000000109f0f501: callq  0x00000001098fee40  ;   {runtime_call Unsafe_CompareAndSwapInt(JNIEnv_*, _jobject*, _jobject*, long, int, int)}
  0x0000000109f0f506: vzeroupper 
  0x0000000109f0f509: and    $0xff,%eax
  0x0000000109f0f50f: setne  %al
  0x0000000109f0f512: movl   $0x5,0x288(%r15)
  0x0000000109f0f51d: lock addl $0x0,-0x40(%rsp)
  0x0000000109f0f523: cmpl   $0x0,-0x3f04dd(%rip)        # 0x0000000109b1f050

Вывод слабого CompareAndSwapInt:

  0x000000010b698840: sub    $0x18,%rsp
  0x0000010b698847: mov    %rbp,0x10(%rsp)
  0x000000010b69884c: mov    %r8d,%eax
  0x000000010b69884f: lock cmpxchg %r9d,(%rdx,%rcx,1)
  0x000000010b698855: sete   %r11b
  0x000000010b698859: movzbl %r11b,%r11d        ;*invokevirtual compareAndSwapInt {reexecute=0 rethrow=0 return_oop=0}
                                                ; - jdk.internal.misc.Unsafe::weakCompareAndSwapInt@7 (line 1369)

Я далеко не достаточно универсален, чтобы понять весь вывод, но определенно вижу разницу между блокировкой addl и блокировкой cmpxchg.

РЕДАКТИРОВАТЬ Ответ Петра заставил меня задуматься. Давайте посмотрим, будет ли CompareAndSwap встроенным вызовом:

-XX: + PrintIntrinsics -XX: -PrintAssembly

 @ 7   jdk.internal.misc.Unsafe::compareAndSwapInt (0 bytes)   (intrinsic)
 @ 20      jdk.internal.misc.Unsafe::weakCompareAndSwapInt (11 bytes)   (intrinsic).

А затем дважды выполните пример с / без:

-XX: DisableIntrinsic = _compareAndSwapInt

Это немного странно, вывод точно такой же (те же самые точные инструкции) с единственными отличиями, что при включении встроенных функций я получаю такие вызовы:

  0x000000010c23e355: callq  0x00000001016569d2  ;   {runtime_call SharedRuntime::dtrace_method_entry(JavaThread*, Method*)}
  0x000000010c23e381: callq  0x00000001016fee40  ;   {runtime_call Unsafe_CompareAndSwapInt(JNIEnv_*, _jobject*, _jobject*, long, int, int)}

И отключено:

  0x00000001109322d5: callq  0x0000000105c569d2  ;   {runtime_call _ZN13SharedRuntime19dtrace_method_entryEP10JavaThreadP6Method}
    0x00000001109322e3: callq  0x0000000105c569d2  ;   {runtime_call _ZN13SharedRuntime19dtrace_method_entryEP10JavaThreadP6Method}

Это довольно интригующе, не должен ли внутренний код отличаться?

EDIT-2 the8472 тоже имеет смысл.

Блокировка addl является заменой mfence, которая, насколько я знаю, сбрасывает StoreBuffer на x86, и это на самом деле связано с видимостью, а не атомарностью. Прямо перед этой записью есть:

 0x00000001133db6f6: movl   $0x4,0x288(%r15)
 0x00000001133db701: callq  0x00000001060fee40  ;   {runtime_call Unsafe_CompareAndSwapInt(JNIEnv_*, _jobject*, _jobject*, long, int, int)}
 0x00000001133db706: vzeroupper 
 0x00000001133db709: and    $0xff,%eax
 0x00000001133db70f: setne  %al
 0x00000001133db712: movl   $0x5,0x288(%r15)
 0x00000001133db71d: lock addl $0x0,-0x40(%rsp)
 0x00000001133db723: cmpl   $0x0,-0xd0bc6dd(%rip)        #     0x000000010631f050
                                            ;   {external_word}

Если вы посмотрите здесь, то делегируем другой собственный вызов Atomic:: cmpxchg, который, кажется, выполняет своп атомарно.

Почему это не заменит прямую блокировку cmpxchg, для меня загадка.

3 ответа

Решение

TL;DR Вы смотрите не в том месте в выводе сборки.

И то и другое compareAndSwapInt а также weakCompareAndSwapInt вызовы компилируются в точно такую ​​же последовательность ASM на x86-64. Однако сами методы компилируются по- разному (но это обычно не имеет значения).

  1. Определение compareAndSwapInt а также weakCompareAndSwapInt в исходнике другой. Первый является нативным методом, а второй - методом Java.

    @HotSpotIntrinsicCandidate
    public final native boolean compareAndSwapInt(Object o, long offset,
                                                  int expected,
                                                  int x);
    
    @HotSpotIntrinsicCandidate
    public final boolean weakCompareAndSwapInt(Object o, long offset,
                                                      int expected,
                                                      int x) {
        return compareAndSwapInt(o, offset, expected, x);
    }
    
  2. То, что вы видели, это то, как эти автономные методы компилируются. Собственный метод компилируется в заглушку, которая вызывает соответствующую функцию C. Но это не то, что идет по быстрому пути.

  3. Внутренние методы - это те, вызовы которых заменяются встроенной реализацией HotSpot. Примечание: вызовы заменяются, но не сами методы.

  4. Если вы посмотрите на вывод сборки вашего WeakVsNonWeak.strong метод, вы увидите, что он содержит lock cmpxchg инструкция, вызывает ли она UNSAFE.compareAndSwapInt или же UNSAFE.weakCompareAndSwapInt,

    0x000001bd76170c44: lock cmpxchg %ecx,(%r11)
    0x000001bd76170c49: sete   %r10b
    0x000001bd76170c4d: movzbl %r10b,%r10d        ;*invokevirtual compareAndSwapInt
                                                  ; - WeakVsNonWeak::strong@25 (line 23)
                                                  ; - WeakVsNonWeak::main@46 (line 14)
    
    0x0000024f56af1097: lock cmpxchg %r11d,(%r8)
    0x0000024f56af109c: sete   %r10b
    0x0000024f56af10a0: movzbl %r10b,%r10d        ;*invokevirtual weakCompareAndSwapInt
                                                  ; - WeakVsNonWeak::strong@25 (line 23)
                                                  ; - WeakVsNonWeak::main@46 (line 14)
    

    Как только основной метод будет JIT-скомпилирован, автономная версия методов Unsafe.* Не будет вызываться напрямую.

В первом случае используется нативный метод. Либо код не был оптимизирован, либо он не является внутренним.

Во втором случае встроенная функция использовалась для вставки требуемой сборки, а не для вызова метода JNI. Я хотел бы, хотя оба случая сделали бы это, но я предполагаю, что нет.

Я верю lock addl это не сама операционная операция, а реализация барьера загрузки магазина. атомное происходит в callq,

Поскольку вы уже входите в PrintIntrinsics Вы должны проверить, действительно ли это становится внутренним.

Другие вопросы по тегам