我想知道在单线程环境中,与同步易失性增量相比,原子整数在多大程度上性能更好,并编写了这样一个JMH基准:

@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class CounterBenchmark {

    @State(Scope.Benchmark)
    public static class ThreadState {

        public final AtomicInteger atomicInteger = new AtomicInteger();
        public final SynchronizedCounter synchronizedCounter = new SynchronizedCounter();
    }

    public static class SynchronizedCounter {
        private volatile int counter = 0;

        public synchronized int incrementAndGet() {
            return counter++;
        }
    }

    @Benchmark
    public void atomicCounter(ThreadState state, Blackhole blackhole) {
        blackhole.consume(state.atomicInteger.incrementAndGet());
    }

    @Benchmark
    public void syncCounter(ThreadState state, Blackhole blackhole) {
        blackhole.consume(state.synchronizedCounter.incrementAndGet());
    }


    public static void main(String... args) throws Exception {

        Options options = new OptionsBuilder()
                .include(CounterBenchmark.class.getSimpleName())
                .forks(1)
                .threads(1)
                .warmupTime(TimeValue.seconds(1))
                .warmupIterations(5)
                .measurementTime(TimeValue.seconds(1))
                .measurementIterations(5)
                .mode(Mode.Throughput)
                .build();

        new Runner(options).run();
    }
}

并得到了这样的输出:

Benchmark Mode Cnt Score Error Units MySerializationBenchmark.atomicCounter thrpt 5 145125.272 ± 2064.673 ops/ms MySerializationBenchmark.syncCounter thrpt 5 105763.168 ± 1794.680 ops/ms

而且结果似乎很快.每1ms 145k次操作(145kk/秒),用于同步的100kk次. 是我做错了什么,还是这是真正的表演. `

我在谷歌上搜索了一些东西,比如"常见的Java延迟数字"等等,但我决定自己做基准测试

推荐答案

您只有一个线程,所以不会争用锁,甚至没有任何时间让缓存线从前一个所有者反弹到执行RMW的当前内核,因为它总是同一个内核.

Uncontended atomic increments (that hit in L1d cache) can run about 1 per 8 or 18 cycles on a current AMD or Intel CPU, respectively. 145.125 Mops / sec * (18 cycles/op)~=2612兆周/秒,so a 2.6 GHz Intel Xeon core could do this.我没有寻找任何ARM内核的原子RMW延迟/吞吐量数字,但它们可能也很好.

你没有说你测试的是什么CPU;我只是猜测是过go 8年的英特尔,因为这导致服务器核心的CPU频率合理;对于台式机或最近的笔记本电脑来说较低.(或者,如果我对周期/迭代的估计由于一些额外开销而较低,则实际频率可能会更高.)

(你can't add up cycle costs across operations in general,但你的循环只涉及一个重要的操作,这本身就是瓶颈.volatile的增量应该与原子整数的ASM相同,但您也可以使用synchronized.所以IDK为什么它不是慢一倍.也许JIT可以看到只有一个线程,并避免使用原子RMW进行实际锁定?或者在函数中还有其他一些开销,可能是由于黑洞导致的一些存储,所以在不同位置(synchronized使用的锁)添加另一个原子RMW不会慢一倍吗?在这种情况下,我的CPU频率估计是错误的.)

我的8号和18号周期号的来源是https://uops.info/.在表格中寻找lock addlock xadd.该表显示了延迟、吞吐量和微操作(Uop)计数.延迟是复杂的,因为操作有3个输入:地址、寄存器输入、标志中的输出(以及xadd的寄存器)和存储器的内容.Uops.infotry 使用不同的微基准来测量从每个输入到每个输出的延迟,因为这些都可能是不同的,特别是对于多uop指令.

特别是XADD_LOCK (M32, R32)ADD_LOCK (M32, I8),因为您使用的是32位整数.(XADD生成内存位置的先前值,如FETCH_ADD需要.普通的加法不需要,只是标志是否为零的结果,以此类推.由于您在增量结果上使用blackhole.consume,JIT优化器将不得不使用类似mov eax, 1/lock xadd [rdi], eax的值,而不是lock add dword ptr [rdi], 1)

The relevant bottleneck in this case is latency from memory to memory since you're doing increments on the same object.例如https://uops.info/html-lat/ZEN4/XADD_LOCK_M32_R32-Measurements.html#lat1-%3E1_mem示出禅宗4‘S Latency operand 1 → 1 (memory): 8的结果:在相同的存储位置上连续递增8个周期.(禅宗3在这方面也有同样的表现).

(一个线程在不同的内存位置上执行多个独立的原子RMW的吞吐量令人惊讶地略好于此,尽管locked的指令是完全的内存屏障.但是没有太多的流水线操作,所以在主表中直接可见的吞吐量数字非常接近.然而,与我们预期的一样,mem-gt;mem的延迟数字是整数.英特尔的数据不出所料,由于内存不足,吞吐量与延迟完全相同.)

最近主流x86 CPU的内存-内存延迟为lock xadd [mem], r32,这也将是循环的周期/迭代,该循环在对象上执行原子增量,而不需要其他内存操作,只是一些寄存器循环-计数器的东西:

(当我开始编写这个答案时,我并没有预料到延迟的Zen 1/2效果比吞吐量更好.这很有趣.但https://uops.info/显示了他们用来进行微基准测试的指令序列,而且测试是自动化的和可靠的,并报告了每个单独测试的实际"APERF"周期计数,所以原始数据就在那里.无论如何,这与您的测试无关,即使您使用的是早期的Ryzen,因为您的循环中唯一重要的事情应该是原子RMW;无序执行寄存器操作以跟踪循环计数器并不是瓶颈的一部分.)

Java相关问答推荐

Oracle DUAL表上使用DDL时jOOQ问题的解析'

如何在SystemiccationRetryListenerSupport中获得类级别的spring retryable annotation中指定的标签?

Intellij显示项目语言级别最高为12,尽管有java版本17 SDK

无法在org. openjfx:javafx—fxml:21的下列变体之间进行 Select

条件加载@ManyToMany JPA

如何判断一个矩阵是否为有框矩阵?

如何使用Maven和Spring Boot将构建时初始化、跟踪类初始化正确传递到本机编译

使用Mockito进行的Junit测试失败

将响应转换为带值的键

我如何知道MediaDiscoverer何时完成发现介质?

使用OAuth 2.0资源服务器JWT时的授权(授权)问题

如何使用路径过渡方法使 node 绕圆旋转?

Java页面筛选器问题

将关闭拍卖的TimerService

错误:不兼容的类型:Double不能转换为Float

JFree Chart从图表中删除边框

Android无法在Java代码中调用Kotlin代码,原因是在Companion中使用Kotlin枚举时

获取所有可以处理Invent.ACTION_MEDIA_BUTTON Android 13 API33的Android包

原始和参数化之间的差异调用orElseGet时可选(供应商)

声纳覆盖范围为 0%,未生成 jacoco.xml