4 ms·
To get such a global flag with the desired behavior, you need explicit memory barriers. Otherwise, you cannot guarantee that it won't become visible to another
by rbehrends 13y ago
To get such a global flag with the desired behavior, you need explicit memory barriers. Otherwise, you cannot guarantee that it won't become visible to another processor before the object's data itself becomes visible.
The problem here is that the memory accesses for the path that bypasses the synchronization block are not guaranteed to occur in the correct order without barriers.
- vy8vWJlco 13y agoYep - it should be enough to force a coherent view for all as part of the initialization.
- rbehrends 13y agoYes, it's well-known that you can fix the problem with memory barriers. But the article's point was that such memory barriers can be avoided entirely for the common execution path if you have fast thread-local storage (because you can make sure that every thread is forced through the full synchronization block once before accessing anything). Given that memory barriers can be expensive on some processors, that's a not insignificant win.
- vy8vWJlco 13y agoDoes D's synchronization not contain memory barriers (I don't know)? (Edited)
- WalterBright 13y agoThe problem with memory barriers is that they are slow. David's solution using TLS avoids them entirely for the usual case, hence it is much faster (as his benchmark results show).
- vy8vWJlco 13y agoThanks for your comment. I do see that the TLS is used in a conditional to avoid the barriers in most cases (the already-initialized case), but the initialization still happens in a synchronized block. I may be wrong, but the docs for D say that effectively wraps the block in a critical section which seems likely to include rw barriers, meaning the code given has barriers at init and also uses TLS. If so, the TLS would be unnecessary, for my reasons above - causing me to suspect that the example has just hidden the barriers behind the synchronized keyword.
- WalterBright 13y agoYes, when it is synchronizing there are memory barriers. But the point is the synchronization and memory barriers can be skipped once the singleton is initialized. The usual case can then run without barriers.
- vy8vWJlco 13y agoThanks again. If I am right though, the memory barriers could also have been skipped without thread-local values since, due to the barriers on init, no thread's view would revert to it's pre-init state anyway. As rbehrends mentions, barriers are not the only factor... For example, global memory access may be slower on some systems due to decreased locality (a property provided by the TLS), but I don't know what if any value that would have in this benchmark. There is also a comment on that page that does the same thing as my TLS-free approach using a more reasonable static global initialization syntax in Java in response to a question of if D's initialization would be semantically equivalent too. Edit: (Not waiting for reply link to appear...) When I say "the memory barriers could also be skipped" I am referring to any other memory barriers past those associated with initialization, not those associated with initialization. There would be memory barriers at initialization, but that is the only need for memory barriers and I believe that is provided here by the synchronized keyword. The object presented in the article then looks like a cache-friendlier version of a single global variable with the necessary barriers limited to the initialization of the global instance pointer (which might also be done as in the Java example given in the comments on the page). The TLS does not eliminate the need for any barriers, only helps eliminate non-local reads. That might allow a performance gain depending on the cache behavior (or, for example, in the presence of NUMA), but it hasn't reduced the need for locking IMHO.
- rbehrends 13y agoThe POSIX standard requires a full memory barrier for the underlying operations. The problem is that synchronization is expensive: not only because of the memory barriers, which have to include the most expensive ones, but also because locking and unlocking operations need to access main memory directly, bypassing all caches. The problem is, in any event, not the synchronization, but the common path -- for which double-checked locking tries to avoid synchronization --, which needs at a minimum a load-load barrier. That can be done efficiently on TSO architectures (where a load-load barrier is a no-op), but is not guaranteed to be fast on other architectures.