Java自增基准测试

7

我正在研究最佳的多线程增量性能。我检查了基于同步、AtomicInteger和自定义实现(类似于AtomicInteger,但在失败的CAS时使用parkNanos(1))。

private int customAtomic() {
        int ret;
        for (;;) {
            ret = intValue;
            if (unsafe.compareAndSwapInt(this, offsetIntValue, ret, ++ret)) {
                break;
            }
            LockSupport.parkNanos(1);
        }
        return ret;
    }

我基于JMH进行了基准测试:对每个方法都进行了清晰的执行,每个方法都消耗CPU(1,2,4,8,16倍),并且仅消耗CPU。每个基准测试方法在Intel(R)Xeon(R)CPU E5-1680 v2 @ 3.00GHz、8核+8 HT 64GB RAM上运行,在1-17个线程上执行。结果让我吃惊:

  1. CAS在1个线程中最有效。2个线程时,与监视器的结果相似。3个及以上线程时差于监视器,约为2倍。
  2. 在大多数情况下,自定义实现比监视器快2-3倍。
  3. 但是,在自定义实现中,有时会随机发生糟糕的执行。好的情况下-50 op/microsec。,坏的情况下-0.5 op/microsec.

问题:

  1. 为什么AtomicInteger不基于同步,它比当前实现更高效?
  2. 为什么AtomicInteger 在CAS失败时不使用LockSupport.parkNanos(1)?
  3. 为什么自定义实现会出现这些峰值?

CustomIncrementGraph

我尝试执行此测试了几次,峰值总是发生在不同数量的线程中。我还在其他机器上尝试过这个测试,结果是一样的。也许测试存在问题。在StackProfiler中,在自定义实现的“坏情况”下,我看到:

....[Thread state distributions]....................................................................
 50.0%         RUNNABLE
 49.9%         TIMED_WAITING

....[Thread state: RUNNABLE]........................................................................
 43.3%  86.6% sun.misc.Unsafe.park
  5.8%  11.6% com.jad.generated.IncrementBench_incrementCustomAtomicWithWork_jmhTest.incrementCustomAtomicWithWork_thrpt_jmhStub
  0.8%   1.7% org.openjdk.jmh.infra.Blackhole.consumeCPU
  0.1%   0.1% com.jad.IncrementBench$Worker.work
  0.0%   0.0% java.lang.Thread.currentThread
  0.0%   0.0% com.jad.generated.IncrementBench_incrementCustomAtomicWithWork_jmhTest._jmh_tryInit_f_benchmarkparams1_0
  0.0%   0.0% org.openjdk.jmh.infra.generated.BenchmarkParams_jmhType_B1.<init>

....[Thread state: TIMED_WAITING]...................................................................
 49.9% 100.0% sun.misc.Unsafe.park

在“正常情况”下:

....[Thread state distributions]....................................................................
 88.2%         TIMED_WAITING
 11.8%         RUNNABLE

....[Thread state: TIMED_WAITING]...................................................................
 88.2% 100.0% sun.misc.Unsafe.park

....[Thread state: RUNNABLE]........................................................................
  5.6%  47.9% sun.misc.Unsafe.park
  3.1%  26.3% org.openjdk.jmh.infra.Blackhole.consumeCPU
  2.4%  20.3% com.jad.generated.IncrementBench_incrementCustomAtomicWithWork_jmhTest.incrementCustomAtomicWithWork_thrpt_jmhStub
  0.6%   5.5% com.jad.IncrementBench$Worker.work
  0.0%   0.0% com.jad.generated.IncrementBench_incrementCustomAtomicWithWork_jmhTest.incrementCustomAtomicWithWork_Throughput
  0.0%   0.0% java.lang.Thread.currentThread
  0.0%   0.0% org.openjdk.jmh.infra.generated.BenchmarkParams_jmhType_B1.<init>
  0.0%   0.0% sun.misc.Unsafe.putObject
  0.0%   0.0% org.openjdk.jmh.runner.InfraControlL2.announceWarmdownReady
  0.0%   0.0% sun.misc.Unsafe.compareAndSwapInt

基准测试代码链接

结果图表链接。X-线程数,Y-吞吐量,操作/微秒

原始日志链接

更新

好的,我知道,当我使用parkNanos时,一个线程也可能长时间持有锁(CAS)。CAS失败的线程会进入睡眠状态,只有一个线程在工作并递增值。我看到,在大并发级别下,当工作非常小的时候- AtomicInteger不是更好的方法。但是如果我们增加workSize,例如将level = CASThrpt/threadNum,则应该正常工作: 对于本地机器,我设置了workSize = 300,我的测试结果:

Benchmark                                     (workSize)   Mode  Cnt  Score   Error   Units
IncrementBench.incrementAtomicWithWork               300  thrpt    3  4.133 ± 0.516  ops/us
IncrementBench.incrementCustomAtomicWithWork         300  thrpt    3  1.883 ± 0.234  ops/us
IncrementBench.lockIntWithWork                       300  thrpt    3  3.831 ± 0.501  ops/us
IncrementBench.onlyWithWork                          300  thrpt    3  4.339 ± 0.243  ops/us

AtomicInteger - 第一;Win, Lock - 第二;自定义 - 第三。 但是还存在尖峰问题,尚不清楚。我忘记提及Java版本: Java(TM) SE Runtime Environment (build 1.7.0_79-b15) Java HotSpot(TM) 64-Bit Server VM (build 24.79-b02, mixed mode)


1
删除对parkNanos的调用。您希望尽快再次迭代。还要确保intValue是易失性的。否则,ret=intValue可能无法看到与CAS相同的值。 - Matt Timmermans
但是有尖峰问题,仍然不清楚。你检查了日志文件吗?有很多<failure: VM prematurely exited before JMH had finished with it, explicit System.exit was called?>事件。 - Ivan Mamontov
好的,使用3个线程和workSize = 1测试incrementCustomAtomicWithWork失败了,原因是failure: VM prematurely exited,结果为0.301 ops/us。这样可以吗? - Ivan Mamontov
不,这与之前的incrementCustomAtomic测试有关,workSize = 16。当虚拟机过早退出时,您将无法获得结果。 - Ilya K.
你能提供一个MCVE吗? stackoverflow.com/help/mcve?这个问题能重现吗?请提供一个带单一参数的测试。你试过fork=2,3,5吗?“当VM过早退出时,你将不会有结果。”在JMH中出现问题时,情况与真相相反。为什么incrementCustomAtomicWithWork在你的日志中失败了? - Ivan Mamontov
显示剩余2条评论
1个回答

1
在同步的情况下,往往会使用锁来避免线程长时间占用锁而不公平地阻止其他线程获取锁。这对于多线程非常不利,但如果您有一个基准测试,只有一个线程运行相对较长的时间,那么它是非常好的。
您需要更改测试,使其在使用多个线程时比仅使用一个线程运行更好,否则您实际上将测试哪种锁定策略具有最差的公平性策略。
锁定策略试图调整锁定的执行方式,这就是为什么它可以改变行为,但它无法做好工作,因为代码本来就不应该是多线程的。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接