Реализация 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));
}
}
Ответы
Ответ 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 все время, и могут быть другие вещи, которые вызывают это.
Ответ 4
Чтобы завершить обсуждение, тот же вопрос также был задан в Concurrency -interest - Обсуждение список для JSR-166 список рассылки почти в то же время, что и здесь.
Вот начало потока - [concurrency -interest] Реализация AtomicInteger обсуждение реализации AtomicInteger.
Ответ 5
Причина в том, что они предпочли сделать код быстрее, за счет размера кода.
Я уверен, что источники реальны. Если бы они были внутренними, они были бы отмечены как родные.