摘要
locking
宏比Java中的synchronized
块慢了两倍,尽管locking
宏所做的事情仅仅是生成monitorenter
和monitorexit
操作码。
这个问题可以通过使用Java中的synchronized
块而不是直接生成monitorenter
和monitorexit
来解决。
开发邮件列表中的线程: https://groups.google.com/forum/#!topic/clojure-dev/dJZtRsfikXU
相关内容
CLJ-1472与此问题有关。CLJ-1472的目的与本问题不同,但解决每个问题的更改是相同的。因此,这两个补丁的内容几乎相同。
性能测试
我为验证此问题制作了两个示例程序,这些程序创建了多个线程,并通过线程更新Map来人为地制造高度竞争条件。
在Java示例中,我使用简单的Thread和同步块在一个HashMap上。
在Clojure示例中,我制作了两个示例。
在第一个示例中,我使用atom来保存关联(Map),并使用swap!
和assoc
来更新关联。
在第二个示例中,我使用volatile!来保存一个java.util.HashMap,并在更新HashMap之前使用locking
对volatile引用进行锁定。
Java示例
https://github.com/tyano/MultithreadPerformance
Clojure示例
https://github.com/tyano/clj-multithread-performance
{quote}
运行程序的方法在上面的页面上有描述。
{quote}
在我的机器上的结果(macos 10.13.1,Java 1.8.0_144,3.1 GHz Intel Core i5,Clojure 1.9.0)
A. Java示例:6,006ms
B. Clojure - 带关联的atom:18,984ms
C. Clojure - 对HashMap进行锁定:15,883ms
B(Clojure的atom和swap!)比Java慢,但我想我理解为什么。
更新一个关联会创建一个新的对象。当然它使用PersistentMap,所以它的性能应该比创建完整的Map实例复制得好,但它将比直接更新一个简单的java.util.Map实例慢(就像Java示例那样)。并且swap!可能会多次重试更新操作。所以我理解这个结果。
然而,我认为C(对HashMap进行锁定)的结果(15,883ms)太慢了。
由于locking
宏只是直接生成monitorenter
和monitorexit
,它与synchronized
块所做的几乎相同,因此结果应该接近Java示例(6,006ms)的结果。
调查
我怀疑locking
生成的字节码可能与synchronized
生成的不同。
我的怀疑是:locking
将生成不同的字节码,并且Java运行时无法很好地优化生成的字节码。
不直接生成操作码(opcodes),如果 locking
宏将宏体包装到一个函数(Fn)中,并仅仅调用一个 Java 方法,该方法以同步块的方式调用提供的 Fn,那么运行时可能会对代码进行很好的优化吗?
我在 locking
宏(在此票据中附带)上做了一个这样的补丁,并使用修补过的 clojure 再次尝试了一个 C 样本。
结果是:6,988毫秒!
摘要
《code>locking 宏的当前实现存在性能问题。生成的 locking
宏的字节码在 Java 运行时上不会很好地被优化。因此,性能比 Java 的 synchronized
块慢 2倍。
在此票据中附带的补丁可以解决这个问题。