Подтвердить что ты не робот

Реализация AtomicInteger и дублирование кода

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

Реализация Oracle JDK 7 AtomicInteger включает в себя следующие методы:

public final int addAndGet(int delta) {
    for (;;) {
        int current = get();
        int next = current + delta;         // Only difference
        if (compareAndSet(current, next))
            return next;
    }
}

public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;             // Only difference
        if (compareAndSet(current, next))
            return next;
    }
}

Кажется очевидным, что второй метод можно было бы написать:

public final int incrementAndGet() {
    return addAndGet(1);
}

В этом классе есть несколько других примеров аналогичного дублирования кода. Я не могу придумать никаких причин для этого, кроме соображений производительности (*). И я уверен, что авторы сделали некоторое углубленное тестирование, прежде чем приступать к этому дизайну.

Почему (или при каких обстоятельствах) первый код будет работать лучше второго?


(*) Я не мог удержаться, но написал быстрый микро-тест. Он показывает (пост-JIT) системный разрыв в 2-4% производительности в пользу addAndGet(1) vs incrementAndGet() (что, по общему признанию, мало, но очень последовательное). Я не могу объяснить этот результат, если честно...

Вывод:

incrementAndGet(): 905
addAndGet (1): 868
incrementAndGet(): 902
addAndGet (1): 863
incrementAndGet(): 891
addAndGet (1): 867
...

код:

public static void main(String[] args) throws Exception {
    final int size = 100_000_000;
    long start, end;
    AtomicInteger ai;

    System.out.println("JVM warmup");
    for (int j = 0; j < 10; j++) {
        start = System.nanoTime();
        ai = new AtomicInteger();
        for (int i = 0; i < size / 10; i++) {
            ai.addAndGet(1);
        }
        end = System.nanoTime();
        System.out.println("addAndGet(1): " + ((end - start) / 1_000_000));
        start = System.nanoTime();
        ai = new AtomicInteger();
        for (int i = 0; i < size / 10; i++) {
            ai.incrementAndGet();
        }
        end = System.nanoTime();
        System.out.println("incrementAndGet(): " + ((end - start) / 1_000_000));
    }


    System.out.println("\nStart measuring\n");

    for (int j = 0; j < 10; j++) {
        start = System.nanoTime();
        ai = new AtomicInteger();
        for (int i = 0; i < size; i++) {
            ai.incrementAndGet();
        }
        end = System.nanoTime();
        System.out.println("incrementAndGet(): " + ((end - start) / 1_000_000));
        start = System.nanoTime();
        ai = new AtomicInteger();
        for (int i = 0; i < size; i++) {
            ai.addAndGet(1);
        }
        end = System.nanoTime();
        System.out.println("addAndGet(1): " + ((end - start) / 1_000_000));
    }
}
4b9b3361

Ответ 1

Я дам новое предположение. Если мы посмотрим на байтовый код AtomicInteger, мы увидим, что основное различие между ними заключается в том, что addAndGet использует инструкцию iload_, а incrementAndGet использует инструкцию iconst_:

public final int addAndGet(int);
   ...
   4:   istore_2
   5:   iload_2
   6:   iload_1
   7:   iadd

public final int incrementAndGet();
   ...
   4:   istore_1
   5:   iload_1
   6:   iconst_1
   7:   iadd

Кажется, что iconst_ + iadd преобразуется как инструкция INC из-за iload_... iadd как ADD инструкция. Все это относится к обычно известному вопросу о ADD 1 vs INC и т.д.:

Относительная производительность команды x86 inc vs. add

Действительно ли ADD 1 быстрее INC? x86

Это может быть ответ, почему addAndGet немного быстрее, чем incrementAndGet

Ответ 2

Из любопытства, вот код сборки, сгенерированный JIT. Таким образом, основное различие заключается в следующем:

  • incrementAndGet

    mov    r8d,eax
    inc    r8d                ;*iadd
    
  • addAndGet

    mov    r9d,r8d
    add    r9d,eax            ;*iadd
    

Остальная часть кода по существу одинакова. Это подтверждает, что:

  • методы не являются внутренними и не называть друг друга под капотом
  • Единственное отличие: INC vs ADD 1

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

Полный список (incrementAndGet):

  # {method} 'incrementAndGet' '()I' in 'java/util/concurrent/atomic/AtomicInteger'
  #           [sp+0x20]  (sp of caller)
  0x00000000026804c0: mov    r10d,DWORD PTR [rdx+0x8]
  0x00000000026804c4: shl    r10,0x3
  0x00000000026804c8: cmp    rax,r10
  0x00000000026804cb: jne    0x0000000002657b60  ;   {runtime_call}
  0x00000000026804d1: data32 xchg ax,ax
  0x00000000026804d4: nop    DWORD PTR [rax+rax*1+0x0]
  0x00000000026804dc: data32 data32 xchg ax,ax
[Verified Entry Point]
  0x00000000026804e0: sub    rsp,0x18
  0x00000000026804e7: mov    QWORD PTR [rsp+0x10],rbp  ;*synchronization entry
                                                ; - java.util.concurrent.atomic.AtomicInteger::[email protected] (line 204)
  0x00000000026804ec: mov    eax,DWORD PTR [rdx+0xc]  ;*invokevirtual compareAndSwapInt
                                                ; - java.util.concurrent.atomic.AtomicInteger::[email protected] (line 135)
                                                ; - java.util.concurrent.atomic.AtomicInteger::[email protected] (line 206)
  0x00000000026804ef: mov    r8d,eax
  0x00000000026804f2: inc    r8d                ;*iadd
                                                ; - java.util.concurrent.atomic.AtomicInteger::[email protected] (line 205)
  0x00000000026804f5: lock cmpxchg DWORD PTR [rdx+0xc],r8d
  0x00000000026804fb: sete   r11b
  0x00000000026804ff: movzx  r11d,r11b          ;*invokevirtual compareAndSwapInt
                                                ; - java.util.concurrent.atomic.AtomicInteger::[email protected] (line 135)
                                                ; - java.util.concurrent.atomic.AtomicInteger::[email protected] (line 206)
  0x0000000002680503: test   r11d,r11d
  0x0000000002680506: je     0x0000000002680520  ;*iload_2
                                                ; - java.util.concurrent.atomic.AtomicInteger::[email protected] (line 207)
  0x0000000002680508: mov    eax,r8d
  0x000000000268050b: add    rsp,0x10
  0x000000000268050f: pop    rbp
  0x0000000002680510: test   DWORD PTR [rip+0xfffffffffdbafaea],eax        # 0x0000000000230000
                                                ;   {poll_return}
  0x0000000002680516: ret    
  0x0000000002680517: nop    WORD PTR [rax+rax*1+0x0]  ; OopMap{rdx=Oop off=96}
                                                ;*goto
                                                ; - java.util.concurrent.atomic.AtomicInteger::[email protected] (line 208)
  0x0000000002680520: test   DWORD PTR [rip+0xfffffffffdbafada],eax        # 0x0000000000230000
                                                ;*goto
                                                ; - java.util.concurrent.atomic.AtomicInteger::[email protected] (line 208)
                                                ;   {poll}
  0x0000000002680526: mov    r11d,DWORD PTR [rdx+0xc]  ;*invokevirtual compareAndSwapInt
                                                ; - java.util.concurrent.atomic.AtomicInteger::[email protected] (line 135)
                                                ; - java.util.concurrent.atomic.AtomicInteger::[email protected] (line 206)
  0x000000000268052a: mov    r8d,r11d
  0x000000000268052d: inc    r8d                ;*iadd
                                                ; - java.util.concurrent.atomic.AtomicInteger::[email protected] (line 205)
  0x0000000002680530: mov    eax,r11d
  0x0000000002680533: lock cmpxchg DWORD PTR [rdx+0xc],r8d
  0x0000000002680539: sete   r11b
  0x000000000268053d: movzx  r11d,r11b          ;*invokevirtual compareAndSwapInt
                                                ; - java.util.concurrent.atomic.AtomicInteger::[email protected] (line 135)
                                                ; - java.util.concurrent.atomic.AtomicInteger::[email protected] (line 206)
  0x0000000002680541: test   r11d,r11d
  0x0000000002680544: je     0x0000000002680520  ;*ifeq
                                                ; - java.util.concurrent.atomic.AtomicInteger::[email protected] (line 206)
  0x0000000002680546: jmp    0x0000000002680508

Полный список (addAndGet):

  # {method} 'addAndGet' '(I)I' in 'java/util/concurrent/atomic/AtomicInteger'
  # this:     rdx:rdx   = 'java/util/concurrent/atomic/AtomicInteger'
  # parm0:    r8        = int
  #           [sp+0x20]  (sp of caller)
  0x0000000002680d00: mov    r10d,DWORD PTR [rdx+0x8]
  0x0000000002680d04: shl    r10,0x3
  0x0000000002680d08: cmp    rax,r10
  0x0000000002680d0b: jne    0x0000000002657b60  ;   {runtime_call}
  0x0000000002680d11: data32 xchg ax,ax
  0x0000000002680d14: nop    DWORD PTR [rax+rax*1+0x0]
  0x0000000002680d1c: data32 data32 xchg ax,ax
[Verified Entry Point]
  0x0000000002680d20: sub    rsp,0x18
  0x0000000002680d27: mov    QWORD PTR [rsp+0x10],rbp  ;*synchronization entry
                                                ; - java.util.concurrent.atomic.AtomicInteger::[email protected] (line 233)
  0x0000000002680d2c: mov    eax,DWORD PTR [rdx+0xc]  ;*invokevirtual compareAndSwapInt
                                                ; - java.util.concurrent.atomic.AtomicInteger::[email protected] (line 135)
                                                ; - java.util.concurrent.atomic.AtomicInteger::[email protected] (line 235)
  0x0000000002680d2f: mov    r9d,r8d
  0x0000000002680d32: add    r9d,eax            ;*iadd
                                                ; - java.util.concurrent.atomic.AtomicInteger::[email protected] (line 234)
  0x0000000002680d35: lock cmpxchg DWORD PTR [rdx+0xc],r9d
  0x0000000002680d3b: sete   r11b
  0x0000000002680d3f: movzx  r11d,r11b          ;*invokevirtual compareAndSwapInt
                                                ; - java.util.concurrent.atomic.AtomicInteger::[email protected] (line 135)
                                                ; - java.util.concurrent.atomic.AtomicInteger::[email protected] (line 235)
  0x0000000002680d43: test   r11d,r11d
  0x0000000002680d46: je     0x0000000002680d60  ;*iload_3
                                                ; - java.util.concurrent.atomic.AtomicInteger::[email protected] (line 236)
  0x0000000002680d48: mov    eax,r9d
  0x0000000002680d4b: add    rsp,0x10
  0x0000000002680d4f: pop    rbp
  0x0000000002680d50: test   DWORD PTR [rip+0xfffffffffdbaf2aa],eax        # 0x0000000000230000
                                                ;   {poll_return}
  0x0000000002680d56: ret    
  0x0000000002680d57: nop    WORD PTR [rax+rax*1+0x0]  ; OopMap{rdx=Oop off=96}
                                                ;*goto
                                                ; - java.util.concurrent.atomic.AtomicInteger::[email protected] (line 237)
  0x0000000002680d60: test   DWORD PTR [rip+0xfffffffffdbaf29a],eax        # 0x0000000000230000
                                                ;*goto
                                                ; - java.util.concurrent.atomic.AtomicInteger::[email protected] (line 237)
                                                ;   {poll}
  0x0000000002680d66: mov    r11d,DWORD PTR [rdx+0xc]  ;*invokevirtual compareAndSwapInt
                                                ; - java.util.concurrent.atomic.AtomicInteger::[email protected] (line 135)
                                                ; - java.util.concurrent.atomic.AtomicInteger::[email protected] (line 235)
  0x0000000002680d6a: mov    r9d,r11d
  0x0000000002680d6d: add    r9d,r8d            ;*iadd
                                                ; - java.util.concurrent.atomic.AtomicInteger::[email protected] (line 234)
  0x0000000002680d70: mov    eax,r11d
  0x0000000002680d73: lock cmpxchg DWORD PTR [rdx+0xc],r9d
  0x0000000002680d79: sete   r11b
  0x0000000002680d7d: movzx  r11d,r11b          ;*invokevirtual compareAndSwapInt
                                                ; - java.util.concurrent.atomic.AtomicInteger::[email protected] (line 135)
                                                ; - java.util.concurrent.atomic.AtomicInteger::[email protected] (line 235)
  0x0000000002680d81: test   r11d,r11d
  0x0000000002680d84: je     0x0000000002680d60  ;*ifeq
                                                ; - java.util.concurrent.atomic.AtomicInteger::[email protected] (line 235)
  0x0000000002680d86: jmp    0x0000000002680d48

Ответ 3

Чтобы расширить на @AlexeiKaigorodov ответ, если это реальный Java-код, он будет быстрее, потому что он устранит дополнительный фрейм в стеке вызовов. Это заставляет его работать быстрее (почему бы и нет?) И может иметь последствия для того, что несколько одновременных вызовов в цикле с меньшей вероятностью потерпят неудачу, в результате чего цикл будет выполняться повторно. (Хотя, я не могу придумать такие причины с головы.)

Хотя, через ваши микро-тесты, возможно, что код не является реальным, а метод incrementAndGet() реализован в собственном коде так, как вы указали, или что оба являются только внутренними инструкциями (делегированы lock:xadd on x86, например). Однако, как правило, довольно сложно объяснить, что делает JVM все время, и могут быть другие вещи, которые вызывают это.

Ответ 5

Причина в том, что они предпочли сделать код быстрее, за счет размера кода.

Я уверен, что источники реальны. Если бы они были внутренними, они были бы отмечены как родные.