我正在使用Disruptor框架及其.NET平台端口,并发现了一个有趣的案例。可能是我完全错过了一些东西所以我正在寻求全能社区的帮助。
long iterations = 500*1000*1000;
long testValue = 1;
//.NET 4.0. Release build. Mean time - 26 secs;
object lockObject = new object();
Stopwatch sw = Stopwatch.StartNew();
for (int i = 0; i < iterations; i++)
{
lock (lockObject)
{
testValue++;
}
}
sw.Stop();
//Java 6.25. Default JVM params. Mean time - 17 secs.
Object lock = new Object();
long start = System.currentTimeMillis();
for (int i = 0; i < iterations; i++)
{
synchronized (lock)
{
testValue++;
}
}
long stop = System.currentTimeMillis();
似乎在.NET中使用signle线程获取锁定的成本仅比Java高50%。起初我对计时器很怀疑,但我已经进行了几次同样的测试,结果只是提到了上面的平均值。然后我对 synchronized 代码块感到怀疑,但它只是 monitorenter / monitorexit 字节代码指令 - 与在.NET中锁定关键字。在.NET与Java中,为什么采取锁定的任何其他想法都是如此昂贵?
答案 0 :(得分:22)
是的,在.NET中使用无竞争锁定看起来比在Java中更昂贵。 (我的上网本上的结果仍然略显引人注目。)
性能的各个方面在一个平台上比另一个平台更快,有时在这个程度上。 HotSpot JIT和.NET JIT在各方面都截然不同 - 尤其是因为.NET JIT只在IL上运行一次,而HotSpot能够越来越多地优化作为特定的一部分代码运行得越来越频繁。
重要的问题是这是否真的重要。如果您的真实生活应用程序花费每分钟获得5亿次无限制锁定,那么它可能 显着 - 您可能应该稍微重新设计您的应用程序。如果您的真实应用程序实际上在锁定内(或锁定的获取之间)实际工作,那么它不太可能成为真正的瓶颈。
我最近发现了两个.NET陷阱(part one; part two),当我正在编写“系统级库”时,我需要解决它们,它们会产生重大影响当一个应用程序执行 lot 的日期/时间解析时 - 但这种微优化很少值得做。
答案 1 :(得分:8)
要记住关于微基准测试的第一件事是,Java特别擅长识别和消除无法执行任何操作的代码。我一次又一次地发现,Java比其他任何语言都更快地执行无意义的代码。 ;)
如果Java与其他语言相比速度惊人,那么第一个问题应该是;代码是否对远程有用? (或者看起来它可能很有用)
Java倾向于循环展开比以前更多。它还可以组合锁。由于您的测试是无可争议的,并且确实做了任何事情,您的代码就像是看起来像。
for (int i = 0; i < iterations; i+=8) {
synchronized (lock) {
testValue++;
}
synchronized (lock) {
testValue++;
}
synchronized (lock) {
testValue++;
}
synchronized (lock) {
testValue++;
}
synchronized (lock) {
testValue++;
}
synchronized (lock) {
testValue++;
}
synchronized (lock) {
testValue++;
}
synchronized (lock) {
testValue++;
}
}
成为
for (int i = 0; i < iterations; i+=8) {
synchronized (lock) {
testValue++;
testValue++;
testValue++;
testValue++;
testValue++;
testValue++;
testValue++;
testValue++;
}
}
因为没有使用testValue。
for (int i = 0; i < iterations; i+=8) {
synchronized (lock) {
}
}
最后
{ }
答案 2 :(得分:3)
变量'testValue'是方法的本地吗?如果是这样,JRE可能检测到锁定是不必要的,因为变量是一个线程的本地变量,因此根本不锁定。
这解释为here。
要说明JVM决定做什么优化有多难 - 以及何时决定这样做 - 从连续三次运行代码中检查这些结果:
public static void main(String[] args) {
System.out.println("Java version: " + System.getProperty("java.version"));
System.out.println("First call : " + doIt(500 * 1000 * 1000, 1)); // 14 secs
System.out.println("Second call: " + doIt(500 * 1000 * 1000, 1)); // 1 sec
System.out.println("Third call : " + doIt(500 * 1000 * 1000, 1)); // 0.4 secs
}
private static String doIt(final long iterations, long testValue) {
Object lock = new Object();
long start = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
synchronized (lock) {
testValue++;
}
}
long stop = System.currentTimeMillis();
return (stop - start) + " ms, result = " + testValue;
}
这些结果很难解释,我认为只有JVM工程师可以帮助解决问题。
答案 3 :(得分:1)
记住,两者都非常快;我们在这里谈论锁定读写解锁的50个CPU周期。
在Java中,我将其与无竞争情况下的模拟impl进行了比较
volatile int waitingList=0;
AtomicInteger x = new AtomicInteger(0);
for (int i = 0; i < iterations; i++)
{
while( ! x.compareAndSet(0, 1) )
;
testValue++;
if(waitingList!=0)
;
x.set(0);
}
这个裸骨模拟比synchronized
版本快一点,所用时间是15/17。
这表明在你的测试用例中,Java并没有做疯狂的优化,它诚实地为每次迭代做了lock-read-update-unlock。然而,Java的impl与裸骨的速度一样快;它不能更快。
虽然C#的impl也接近最小值,但它显然比Java做了一两件事。我不熟悉C#,但这可能表明一些语义差异,所以C#必须做一些额外的事情。
答案 4 :(得分:0)
几年前,当我研究Java中的锁/同步成本时,我最终提出了一个很大的问题:锁定如何影响其他线程访问任何类型内存的整体性能。可能受影响的是CPU缓存,尤其是在多处理器计算机上 - 并且取决于特定CPU架构如何处理缓存同步。我相信现代单CPU架构不会影响整体性能,但我不确定。
无论如何,如果有疑问,特别是当多进程计算机可能用于托管软件时,可能值得在几个操作中锁定更高级别。
答案 5 :(得分:0)
Java JIT将优化同步,因为锁对象是线程本地的(即它被限制在线程的堆栈中并且从不共享),因此永远不能从另一个线程同步。我不确定.NET JIT是否会这样做。
请参阅this very informative article,尤其是关于锁定省略的部分。