AtomicInteger实现和代码重复

警告:问题有点长,但分隔线下方的部分仅用于好奇。

Oracle的AtomicInteger的JDK 7实现包括以下方法:

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)); } } 

我会给出新的假设。 如果我们查看AtomicInteger字节代码,我们将看到,它们之间的主要区别在于addAndGet使用iload_指令,而incrementAndGet使用iconst_ instruction:

 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与add指令的相对性能

ADD 1真的比INC快吗? 86

这可能就是答案,为什么addAndGetincrementAndGet略快

出于好奇,这里是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::incrementAndGet@-1 (line 204) 0x00000000026804ec: mov eax,DWORD PTR [rdx+0xc] ;*invokevirtual compareAndSwapInt ; - java.util.concurrent.atomic.AtomicInteger::compareAndSet@9 (line 135) ; - java.util.concurrent.atomic.AtomicInteger::incrementAndGet@12 (line 206) 0x00000000026804ef: mov r8d,eax 0x00000000026804f2: inc r8d ;*iadd ; - java.util.concurrent.atomic.AtomicInteger::incrementAndGet@7 (line 205) 0x00000000026804f5: lock cmpxchg DWORD PTR [rdx+0xc],r8d 0x00000000026804fb: sete r11b 0x00000000026804ff: movzx r11d,r11b ;*invokevirtual compareAndSwapInt ; - java.util.concurrent.atomic.AtomicInteger::compareAndSet@9 (line 135) ; - java.util.concurrent.atomic.AtomicInteger::incrementAndGet@12 (line 206) 0x0000000002680503: test r11d,r11d 0x0000000002680506: je 0x0000000002680520 ;*iload_2 ; - java.util.concurrent.atomic.AtomicInteger::incrementAndGet@18 (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::incrementAndGet@20 (line 208) 0x0000000002680520: test DWORD PTR [rip+0xfffffffffdbafada],eax # 0x0000000000230000 ;*goto ; - java.util.concurrent.atomic.AtomicInteger::incrementAndGet@20 (line 208) ; {poll} 0x0000000002680526: mov r11d,DWORD PTR [rdx+0xc] ;*invokevirtual compareAndSwapInt ; - java.util.concurrent.atomic.AtomicInteger::compareAndSet@9 (line 135) ; - java.util.concurrent.atomic.AtomicInteger::incrementAndGet@12 (line 206) 0x000000000268052a: mov r8d,r11d 0x000000000268052d: inc r8d ;*iadd ; - java.util.concurrent.atomic.AtomicInteger::incrementAndGet@7 (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::compareAndSet@9 (line 135) ; - java.util.concurrent.atomic.AtomicInteger::incrementAndGet@12 (line 206) 0x0000000002680541: test r11d,r11d 0x0000000002680544: je 0x0000000002680520 ;*ifeq ; - java.util.concurrent.atomic.AtomicInteger::incrementAndGet@15 (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::addAndGet@-1 (line 233) 0x0000000002680d2c: mov eax,DWORD PTR [rdx+0xc] ;*invokevirtual compareAndSwapInt ; - java.util.concurrent.atomic.AtomicInteger::compareAndSet@9 (line 135) ; - java.util.concurrent.atomic.AtomicInteger::addAndGet@12 (line 235) 0x0000000002680d2f: mov r9d,r8d 0x0000000002680d32: add r9d,eax ;*iadd ; - java.util.concurrent.atomic.AtomicInteger::addAndGet@7 (line 234) 0x0000000002680d35: lock cmpxchg DWORD PTR [rdx+0xc],r9d 0x0000000002680d3b: sete r11b 0x0000000002680d3f: movzx r11d,r11b ;*invokevirtual compareAndSwapInt ; - java.util.concurrent.atomic.AtomicInteger::compareAndSet@9 (line 135) ; - java.util.concurrent.atomic.AtomicInteger::addAndGet@12 (line 235) 0x0000000002680d43: test r11d,r11d 0x0000000002680d46: je 0x0000000002680d60 ;*iload_3 ; - java.util.concurrent.atomic.AtomicInteger::addAndGet@18 (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::addAndGet@20 (line 237) 0x0000000002680d60: test DWORD PTR [rip+0xfffffffffdbaf29a],eax # 0x0000000000230000 ;*goto ; - java.util.concurrent.atomic.AtomicInteger::addAndGet@20 (line 237) ; {poll} 0x0000000002680d66: mov r11d,DWORD PTR [rdx+0xc] ;*invokevirtual compareAndSwapInt ; - java.util.concurrent.atomic.AtomicInteger::compareAndSet@9 (line 135) ; - java.util.concurrent.atomic.AtomicInteger::addAndGet@12 (line 235) 0x0000000002680d6a: mov r9d,r11d 0x0000000002680d6d: add r9d,r8d ;*iadd ; - java.util.concurrent.atomic.AtomicInteger::addAndGet@7 (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::compareAndSet@9 (line 135) ; - java.util.concurrent.atomic.AtomicInteger::addAndGet@12 (line 235) 0x0000000002680d81: test r11d,r11d 0x0000000002680d84: je 0x0000000002680d60 ;*ifeq ; - java.util.concurrent.atomic.AtomicInteger::addAndGet@15 (line 235) 0x0000000002680d86: jmp 0x0000000002680d48 

要扩展@AlexeiKaigorodov的答案,如果这是真正的Java代码,它会更快,因为它会消除调用堆栈上的额外帧。 这使得它运行得更快(为什么不呢?)并且可能会导致对循环的多个并发调用不太可能失败,从而导致循环重复运行。 (尽管如此,我无法想出任何这样的理由。)

但是,通过您的微基准测试,代码可能不是真实的,并且incrementAndGet()方法是以您指定的方式在本机代码中实现的,或者两者都只是内部指令(委托lock:xadd x86上的lock:xadd ,例如)。 然而,通常很难判断JVM一直在做什么,并且可能还有其他因素导致了这一点。

为了完成讨论, 同样的问题也在JSR-166邮件列表的Concurrency-interest-Discussion列表中几乎与此处同时提出。

这是线程的开始 – [concurrency-interest] AtomicInteger实现讨论AtomicInteger实现。

原因是他们更喜欢以代码大小为代价来加快代码编写速度。

我确信,消息来源是真实的。 如果它们是内在函数,它们将被标记为原生。