I agree .. As I said I should have included a comment about using volatile
when needed (which is sometimes hard to know more on that below)
But one should not be using lock to provide this behavior (as suggested in
the article) as its success is a by product of other optimizations (it works
because it disables the JIT optimization from occurring (adding overhead)
and adds significant overhead itself). There has been some commentary on it
being due to memory barriers but they never come into play (and could be
viewed as further unnecesary overhead)
If we look at the code generated by the volatile keyword on our member (even
through a property)... it produces the exact unmanaged code we want, the
lock based method produces code that is orders of magnitude slower even when
uncontested .. just to give you an idea of the difference .. here are the
functions I used to test ...
static int notvol = 1;
static volatile int vol =1 ;
static object lockobj = new object();
static int Notvol {
get { lock(lockobj) { return notvol; }}
}
static int Vol {
get { return vol; }
}
public static void TestWithLock() {
for (int i = 0; i < 100000; i++) {
Dummy = Notvol;
}
}
public static void TestWithVolatile() {
for (int i = 0; i < 100000; i++) {
Dummy = Vol;
}
}
The unmanaged code ..
public static void TestWithVolatile() {
for (int i = 0; i < 100000; i++) {
00000000 xor edx,edx
Dummy = Vol;
00000002 mov eax,dword ptr ds:[00912FF4h]
00000007 mov dword ptr ds:[00912FECh],eax
for (int i = 0; i < 100000; i++) {
0000000c add edx,1
0000000f cmp edx,186A0h
00000015 jl 00000002
00000017 ret
public static void TestWithLock() {
for (int i = 0; i < 100000; i++) {
00000000 push esi
00000001 xor esi,esi
Dummy = Notvol;
00000003 call dword ptr ds:[0091318Ch]
00000009 mov dword ptr ds:[00912FECh],eax
for (int i = 0; i < 100000; i++) {
0000000e add esi,1
00000011 cmp esi,186A0h
00000017 jl 00000003
00000019 pop esi
}
}
0000001a ret
Test : Volatile took average ns = 154115.46886278
Test : NonVolatile took average ns = 11274013.3080039
These results are averaged over 10000 runs of each function. 70 times faster
using the volatile as opposed to the lock (without any lock contention) as
the lock adds in a huge amount of overhead that we don't need (and forces
the JIT optimizer to not be able to inline). Once we factor in the lock
contention we will surely have in a real life scenario I would expect this
number could conservatively be over 100 times.
Since the main place we worry about volatility is in looping conditions
where variables often times get optimized into registers I feel that the
performance here is often of fairly high value.
Jon Skeet
It also fails as soon as you've got a property which needs to set two
different variables. I prefer to have just one way of obtaining thread-
safety: a lock.
As I had said, I was dealing with a case where we were dealing with
something under reference size (without screwing with alignment etc). I feel
removing the needless locking is important in performant areas of code.
Jon Skeet
(I also believe that very few classes ought to try to be thread-safe,
so the performance penalties are usually minimal.)
My advice has always been, don't lock unless you actually need it ..
I do agree with your last post in not doing this at all (which is why I had
no volatile to start with). I generally try to avoid any sort of threading
code (even the use of volatiles) unless I have a case where I actually need
it. I do this because it is nearly impossible for a class to know how the
caller intends to use it. Instead it is best to leave it to the caller how
they would like to use your code (otherwise you would technically need to
prevent all inlining or mark everything as volatile)
Just to be clear .. I agree that volatility is a valid concern; I just think
that using lock to provide it is bad practice as it comes with significant
overhead (in the range of 100 times slower) and works as a by product of the
optimizer not inlining (see previous post for inlining)
Cheers,
Greg Young
MVP - C#
http://codebetter.com/blogs/gregyoung