I’ve made massive improvements in concurrency performance several times recently with a really easy and mechanical change, so I figure it’s time to share this and try to popularize the idea.
Major kudos to Nikita Volkov for the wonderful stm-containers library of course
Good one! I somewhat recently moved our TVar (Map k v) (after moving from MVar (Map k v)) over to the StmMap.Map k v because it was getting rather large.
In the beginning, it wasn’t too bad, but with increased usage you do notice the performance degradation.
It is somewhat unfortunate you can only find stm-containers on hackage by searching for it literally, because you won’t get hits on the modules or functions
There are no situations where going from StmMap.Map k v to TVar (Map k v) will make a significant improvement.
I think there are situations where TVar (Map _ _ ) will be better. If you want to atomically read all the values from the structure, then having a single TVar can lead to better performance.
That’s a great point about the quadratic slowdown from having a bunch of TVars in a transaction. Interesting! I have not needed to summarize a map atomically, and have been able to use listTNonAtomic :: Map k v -> ListT IO [(k, v)] for everything I’ve done where summary was important.
I think I’d still rather see an MVar in that case instead of a TVar, but it all really depends on the specific access patterns. Wrapping the TVar in a safer interface (ie mkUpdater :: (Map k v → IO (Map k v)) → IO (ReadOnlyMapTVar k v) can help prevent livelock problems potentially, where the bare TVar almost invites them.
EDIT: I’ve updated the post to mention this scenario and my recommendations/thoughts on managing it. Hopefully GHC will fix STM soon so stm-containers is more purely a win!
Livelock is when the program is repeatedly retrying transactions - working furiously and making no progress. Livelock is subtle and extremely difficult to diagnose.
I’ve found a very simple pattern that helps with few-writers many-readers contention. Just don’t use longlived STM transactions for readers! The readers should typically just “snapshot” the map with a readTVar transaction, then proceed to do work outside the transaction. Using such short transactions does preclude some more clever approaches to STM, but i my experience I’ve found it suffices – and any needed broader atomicity invariants can be enforced elsewhere.
Of course, if many things need to both read from a map and write to it, this doesn’t work. But in that setting, it still frequently works to have processes atomically read the map, then prepare delta updates to the map, which are combined and applied in ensamble elsewhere, including perhaps with optimizations and reorderings. The appriach is similar to Uustalu and Ahman’s “Update Monads”: http://dagstuhl.sunsite.rwth-aachen.de/volltexte/2014/4623/pdf/p001-01-ahman.pdf