【问题标题】:AtomicInteger implementation and code duplicationAtomicInteger 实现和代码重复
【发布时间】:2013-02-15 01:23:12
【问题描述】:

警告:问题有点长,但分隔线下方的部分仅供参考。

AtomicInteger 的 Oracle 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 之后)与 addAndGet(1) 相比 incrementAndGet() 存在 2-4% 的系统性差距(这固然很小,但非常一致)。老实说,我也无法真正解释这个结果......

输出:

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 为JVM warmup :)
  • 虽然我怀疑这是否会真正揭示​​答案,但您可以反汇编这两个函数并在此处显示反汇编的 Java 字节码。它可能揭示一些东西。

标签: java performance jvm


【解决方案1】:

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

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

【讨论】:

    【解决方案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::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
    

    【讨论】:

    • 如何从jit生成程序集?
    【解决方案3】:

    我会给出新的假设。如果我们查看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_...iaddADD 指令。这都与ADD 1 vs INC 等众所周知的问题有关:

    Relative performance of x86 inc vs. add instruction

    Is ADD 1 really faster than INC ? x86

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

    【讨论】:

    • 您最初的回答是spot on!除了它显然还没有在 7u11 中实现(这是我正在使用的版本)。
    • 另见the bug database
    【解决方案4】:

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

    但是,通过您的微基准测试,代码可能不是真实的,并且 incrementAndGet() 方法是按照您指定的方式在本机代码中实现的,或者两者都只是内在指令(委托给 lock:xadd on例如 x86)。但是,通常很难预测 JVM 一直在做什么,并且可能还有其他原因导致了这种情况。

    【讨论】:

    • 不确定我是否理解这部分“多个并发调用 compareAndSet() 失败并且必须再次运行循环的可能性较小。”:compareAndSet 运行在无论如何,相同的this 对象...
    • incrementAndGet 运行速度比addAndGet 慢,那么您所说的消除额外帧是什么意思?
    • 我只是推测如果都是Java代码,那将是代码重复的原因。我的第二段提出了其他可能性……
    • @Eng.Fouad 我不太确定 - 我希望 JIT 能够内联这么短的方法(尤其是最后一个方法)。
    【解决方案5】:

    原因是他们希望以牺牲代码大小为代价让代码更快。

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

    【讨论】:

    • 我确定是这样 - 问题是“为什么会更快?”
    • Alexei,您对“如果它们是内在函数,它们将被标记为原生”是错误的 - 似乎您不了解“内在函数”是什么。
    • @Andremoniy 我也不太了解“内在”。您可以在答案中添加它吗?
    • @irreputable,请阅读:vanillajava.blogspot.ru/2012/11/…
    • @Andremoniy 是的,我不明白你的意思,因为现在 java 中“内在”的标准含义
    猜你喜欢
    • 2012-12-29
    • 2013-11-14
    • 1970-01-01
    • 2012-06-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-12-26
    • 1970-01-01
    相关资源
    最近更新 更多