摘要
locking
宏比Java的synchronized
块慢2倍,尽管locking
宏所做的只是在生成monitorenter
和monitorexit
指令。
使用Java的synchronized
块而不是直接生成monitorenter
和monitorexit
指令可以解决这个问题。
开发邮件列表中的线程是:https://groups.google.com/forum/#!topic/clojure-dev/dJZtRsfikXU
相关于
CLJ-1472与这个工单有关。CLJ-1472的目的与本工单不同,但解决每个问题所需的所有更改都相同。因此,这两个工单的补丁内容几乎相同。
BENCHMARKS
我为验证这个问题创建了两个示例程序,在这些程序中,我创建了许多线程,并从线程中更新Map,以制造高度竞争的条件。
在Java示例中,我使用简单的Thread和synchronized块在一个HashMap上。
在Clojure示例中,我创建了两个示例。
在第一个示例中,我使用atom来保留一个关联(Map),并使用swap!
和assoc
来更新这个关联。
在第二个示例中,我使用volatile!来保留一个java.util.HashMap,并在更新HashMap之前在volatile引用上使用locking
。
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
生成的不同。
工单CLJ-1472也引导我的怀疑。这个工单表明,locking
生成的字节码与JDK生成的不同。
我的怀疑:《代码锁定》(locking
)将生成不同的字节码,而Java运行时无法很好地优化生成的字节码。
直接生成操作码相反,如果locking
宏将宏主体包装在一个Fn中,并且仅调用一个Java方法,该方法在同步块中调用的提供Fn,运行时可能能够很好地优化代码?
我在locking
宏上创建了一个这样的补丁(在该票上附加),并尝试用修补后的clojure再次进行了一个示例C。
结果是:6,988毫秒!
总结
当前locking
宏的实现存在性能问题。生成的locking
字节码在Java运行时不会得到很好的优化。因此,性能比Java中的synchronized
块快2倍。
该票上附加的补丁可以解决这个问题。