18

I am wondering how a piece of locked code can slow down my code even though the code is never executed. Here is an example below:

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;
}

This code runs in ~1300ms on my machine. If we remove the lock block (but keep its body), we get 750ms. Almost the double, even though the code is never run!

Of course this code does nothing. I noticed it while adding some lazy initialization in a class where the code checks if the object is initialized and if not initializes it. The problem is that the initialization is locked and slows down everything even after the first call.

My questions are:

  1. Why is this happening?
  2. How to avoid the slowdown
Mateen Ulhaq
  • 24,552
  • 19
  • 101
  • 135
pieroxy
  • 999
  • 7
  • 15
  • Unless you intend on using `lock` intensively - I wouldn't really worry about it. – James May 06 '13 at 10:45
  • 4
    I get similar results, but a tick is 100 *nano*-seconds. Both runs should take ~0ms (i.e. if you print `sw.ElapseMilliseconds`.) This "slowdown" (of ~0.00006s) is likely due to the fact that `lock` includes a `try/finally` block which is probably being setup when the method is called. Try putting the contents of `testRand` in the loop itself; you'll see almost *no* slowdown at that point. – dlev May 06 '13 at 10:51
  • Have you tried marking the method with `AggressiveInline`? Perhaps the locking code made the method too large for normal inlining. The .net JITter inlines using a rather dumb heuristic based on the size of the IL code. – CodesInChaos May 06 '13 at 10:57
  • +1, interesting problem. – Alex Filipovici May 06 '13 at 10:58
  • No, seriously: *Ticks aren't milliseconds*! – dlev May 06 '13 at 11:00
  • 1
    [Similar topic here](http://stackoverflow.com/questions/2416793/why-is-lock-much-slower-than-monitor-tryenter). Findings should be pretty much identical. – Niels Keurentjes May 06 '13 at 11:01
  • @dlev This kind of tick is CPU dependent. Not a constant 100 ns. – CodesInChaos May 06 '13 at 11:01
  • You're right, I'm conflating two different kinds of ticks. But ticks still aren't ms. – dlev May 06 '13 at 11:04
  • The most useful thing to do would be to examine the compiled code in a hex editor. If you want to lock something, the compiled exe or DLL has to do something in anticipation of this event. I am too stupid to know exactly what, but that's my two cents. – Captain Kenpachi May 06 '13 at 11:05
  • 1
    I've tracked this to a `try{}finally{}` statement. If you use it (even empty, with no embedded code) the same slowdown occurs. A `lock` statement implements a `try{}finally{}`. – Alex Filipovici May 06 '13 at 11:07
  • 1
    ANother question with some enlightening answers: http://stackoverflow.com/questions/6029804/how-does-lock-work-exactly-c-sharp – Captain Kenpachi May 06 '13 at 11:09
  • You are showing, that lock slows your program a little bit. Of course it does. Similar effect would you got if you added some other instructions. – Ari May 06 '13 at 11:09
  • Regarding your actual use case, you may wish to investigate the Lazy class for your lazy-initialization needs. – dlev May 06 '13 at 11:09
  • Check out [Jon Skeet's answer on a similar topic](http://stackoverflow.com/a/8928476/674700). I can confirm that if I run the sample compiled for the x64 platform, the performance degradation is only about 15-20%. – Alex Filipovici May 06 '13 at 11:22
  • @Ari I don't see why the slowdown would be obvious. We're talking about dead code that's not execute here. – CodesInChaos May 06 '13 at 12:02
  • @Ramhound the code inside the lock is NOT executed by the logic of the program. This is why I don't understand the slowdown. – pieroxy May 06 '13 at 12:24

1 Answers1

10

About why it's happening, it has been discussed in the comments : it's due to the initialization of the try ... finally generated by the lock.


And to avoid this slowdown, you can extract the locking feature to a new method, so that the locking mechanism will only be initialized if the method is actually called.

I tried it with this simple code :

public object testRand(Random r)
{
    if (r.Next(1) > 10)
    {
        return LockingFeature();
    }
    return r;
}

private object LockingFeature()
{
    lock (_lock)
    {
        return null;
    }
}

And here are my times (in ticks) :

your code, no lock   : ~500
your code, with lock : ~1200
my code              : ~500

EDIT : My test code (running a bit slower than the code with no locks) was actually on static methods, it appears that when the code is ran "inside" an object, the timings are the same. I fixed the timings according to that.

Zonko
  • 3,365
  • 3
  • 20
  • 29
  • Thanks for the answer, this was exactly what I was looking for. In my test, your solution ran faster than the `lock` inline but slower than just having a `return null`. I defined the method `LockingFeature` as `virtual` to avoid code inlining and I got 100% of my performance back. – pieroxy May 06 '13 at 13:09
  • @pieroxy - Another thing about your first test is that the version of `testRand()` with the lock takes longer to JIT as well. So you can take that out of the equation by making just one call to `testRand()` before the `Stopwatch` begins (as a way of warming up the JIT compiler, so to speak). This narrows the gap significantly. Still, Zonko's code is a pretty slick way of dealing with this. – Steve Wortham May 06 '13 at 13:20