WeakCompareAndSwap против сравнения с обменом

Этот вопрос касается не разницы между ними - я знаю, что такое ложный отказ и почему это происходит на 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

Вывод weakCompareAndSwapInt:

  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::[email protected] (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

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

  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 тоже имеет смысл.

lock 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}

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

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

Ответ 1

TL; DR Вы смотрите на неправильное место на выходе сборки.

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

  • Определение 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);
    }
    
  • Вы видели, как скомпилированы эти автономные методы. Собственный метод компилируется в заглушку, которая вызывает соответствующую функцию C. Но это не то, что работает на быстром пути.

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

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

    0x000001bd76170c44: lock cmpxchg %ecx,(%r11)
    0x000001bd76170c49: sete   %r10b
    0x000001bd76170c4d: movzbl %r10b,%r10d        ;*invokevirtual compareAndSwapInt
                                                  ; - WeakVsNonWeak::[email protected] (line 23)
                                                  ; - WeakVsNonWeak::[email protected] (line 14)
    
    0x0000024f56af1097: lock cmpxchg %r11d,(%r8)
    0x0000024f56af109c: sete   %r10b
    0x0000024f56af10a0: movzbl %r10b,%r10d        ;*invokevirtual weakCompareAndSwapInt
                                                  ; - WeakVsNonWeak::[email protected] (line 23)
                                                  ; - WeakVsNonWeak::[email protected] (line 14)
    

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

Ответ 2

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

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

Ответ 3

Я считаю, что lock addl - это не атомный оператор, а реализация барьера в хранилище. атом находится в callq.

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