19

I've been working under the share-nothing principle of concurrent programming. Essentially, all my worker threads have immutable read-only copies of the same state which is never shared between them (even by reference). Generally speaking, this has worked really well.

Now, someone has introduced a no-lock singleton cache (e.g. a static dictionary) that all the threads are accessing concurrently. Since the dictionary is never altered after startup there are no locks. There haven't been any Thread-Safety issues, but now there is a performance degradation.

The question is... since there are no locks why does the introduction of this singleton create a performance hit? What exactly is going on under the covers that could explain this?

To confirm, accessing this new singleton is the only change and I can reliably recreate this simply by commenting out the call to the cache.

17
  • 8
    Have you pointed a profiler at the code? Dec 15, 2011 at 19:39
  • 2
    Profiling is unlikely to answer this question unless you're profiling the CLR and possibly the windows kernel (not an easy task for the average programmer). Dec 15, 2011 at 19:54
  • 1
    @JoeGeeky Alrighty then, I guess the only thing to do for me here is +1 and favoriting! It seems weird since they are both at the same level of indirection after all, and should fit in processor cache anyway, etc...
    – Max
    Dec 15, 2011 at 20:41
  • 2
    FWIT I spawned a couple threads and ran some timers. I instantiated a class, singleton, lockedSingleton, and dict<string,string>. After the first instantiation of each, consecutive runs took about 2000ns for any given object. The dictionary ran 2x slower,might be caused by constructor code...it is slower than lock by itself. Considering all the GC, OS handling of thread queue's and other overhead...not sure one can truly answer this question. But, from my results I don't believe the issue has to do with Singletons. Not if it's implemented like on MSDN.Excludes compiler optimizations. Dec 16, 2011 at 2:57
  • 1
    @JoeGeeky - another thought: does using the cache add a level of indirection? If frequently accessed, chasing down an extra pointer deref (or MSIL equiv) could add some time over a local less-indirect copy.
    – sdg
    Dec 16, 2011 at 21:11

2 Answers 2

8

It could be that the immutable state shares a cache-line with something mutable. In this case, a change to the nearby mutable state might have the effect of forcing a resynch of this cache line across cores, which could slow down performance.

1
  • 3
    This sounds like a false sharing scenario that your describing. To isolate that I'll need to profile L2 Cache. Unfortunetally, these are reference types so adding buffer space will not be an option if this is actually what is happening.
    – JoeGeeky
    Dec 15, 2011 at 21:53
3

I would make sure that the Equals() and GetHashCode() methods of the objects you use as keys to the dictionary do not have any unexpected non-threading-friendly side-effects. Profiling would greatly help here.

If by any chance your keys are strings, then perhaps there you have it: rumor has it that strings behave like immutable objects but for the sake of certain optimizations they are internally implemented in a mutable fashion, with everything that this entails with respect to multithreading.

I would try passing the dictionary to the threads that use it as a regular reference instead of a singleton to see whether the problem lies with the sharedness or with the singletonness of the dictionary. (Eliminating the possible causes.)

I would also try with a ConcurrentDictionary instead of a regular Dictionary just in case its use yields some surprising results. There are lots of things to be speculated about the problem at hand if a ConcurrentDictionary turns out to perform much better or much worse than your regular Dictionary.

If none of the above points to the problem, then I would guess that the degraded performance is caused by some weird sort of contention between the garbage-collecting thread and the rest of your threads, as the garbage-collector is trying to figure out whether the objects in your dictionary need to be disposed or not, while they are being accessed by your threads.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.