How to avoid slowdown due to locked code?
我想知道,即使代码从未执行过,一段锁定的代码如何能够减慢我的代码速度。下面是一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public void Test_PerformanceUnit ()
{
Stopwatch sw = new Stopwatch ();
sw .Start();
Random r = new Random ();
for (int i = 0; i < 10000; i ++)
{
testRand (r );
}
sw .Stop();
Console .WriteLine(sw .ElapsedTicks);
}
public object testRand (Random r )
{
if (r .Next(1) > 10)
{
lock(this) {
return null;
}
}
return r ;
} |
这个代码在我的机器上运行大约1300毫秒。如果我们移除锁块(但保留其主体),我们得到750毫秒。几乎是双倍,即使代码从未运行!
当然,这个代码什么也不做。在类中添加一些惰性初始化时,我注意到了这个问题,在这个类中,代码检查对象是否已初始化,是否未初始化。问题是,即使在第一次调用之后,初始化也会被锁定并减慢所有操作。
我的问题是:
为什么会这样?
如何避免减速
- 除非你打算集中使用lock,否则我不会担心。
- 我得到了类似的结果,但滴答声是100纳秒。两次运行都需要大约0毫秒(即,如果打印sw.ElapseMilliseconds)。此"减速"(约0.00006)可能是由于lock包含一个try/finally块,该块可能在调用方法时设置。尝试将testRand的内容放在循环本身中;此时您将几乎看不到任何减速。
- 你试过用AggressiveInline标记这个方法吗?可能是锁定代码使该方法对于正常的内联来说太大了。.NET抖动使用基于IL代码大小的相当愚蠢的启发式方法进入。
- +1、有趣的问题。
- 不,说真的:滴答声不是毫秒!
- 这里也有类似的话题。调查结果应该非常一致。
- @DLEV这种类型的勾选取决于CPU。不是常数100 ns。
- 你说得对,我把两种不同的虱子混为一谈。但滴答声仍然不是女士。
- 要做的最有用的事情是在十六进制编辑器中检查编译后的代码。如果要锁定某个对象,则编译后的exe或dll必须在预期此事件时执行某些操作。我太笨了,不知道到底是什么,但那是我的两分钱。
- 我追踪到了一份try{}finally{}声明。如果你使用它(即使是空的,没有嵌入代码),同样的速度会减慢。lock语句实现try{}finally{}。
- 另一个有启发性答案的问题:stackoverflow.com/questions/6029804/&hellip;
- 您显示的是,这个锁会使您的程序慢一点。当然可以。如果你添加一些其他的说明,你会得到类似的效果。
- 关于您的实际用例,您可能希望研究lazy类以满足您的lazy初始化需求。
- 看看乔恩·斯基特关于类似话题的答案。我可以确认,如果我运行为x64平台编译的示例,性能下降仅为15-20%。
- @阿里,我不明白为什么经济放缓会很明显。我们说的是不在这里执行的死代码。
- @Ramhound锁内的代码不是由程序逻辑执行的。这就是我不理解经济放缓的原因。
关于它发生的原因,评论中已经讨论过:这是由于lock生成的try ... finally的初始化。
为了避免这种速度减慢,您可以将锁定特性提取到一个新方法中,这样,只有在实际调用该方法时才会初始化锁定机制。
我用这个简单的代码来尝试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public object testRand(Random r)
{
if (r.Next(1) > 10)
{
return LockingFeature();
}
return r;
}
private object LockingFeature()
{
lock (_lock)
{
return null;
}
} |
以下是我的时间(滴答声):
1 2 3
| your code, no lock : ~500
your code, with lock : ~1200
my code : ~500 |
编辑:我的测试代码(比没有锁的代码慢一点)实际上是在静态方法上的,当代码在一个对象的"内部"运行时,时间是相同的。我根据那个确定了时间。
- 谢谢你的回答,这正是我要找的。在我的测试中,您的解决方案比lock内联运行得更快,但比仅使用return null运行得慢。我将方法LockingFeature定义为virtual,以避免代码嵌入,我获得了100%的性能回报。
- @Pieroxy——关于第一次测试的另一个问题是,带锁的testRand()版本也需要更长的时间来进行JIT。所以你可以在Stopwatch开始之前,只调用一次testRand(),把它从等式中去掉(可以说,这是一种预热jit编译器的方法)。这显著缩小了差距。不过,Zonko的代码是一种相当巧妙的处理方法。