Ouch. These last few days, I’ve been fixing a few lingering bugs in my STM system, and last night, I finally nailed them. Specifically, it is now possible to open variables within a transaction as read-only. An obvious optimization, right? At least that’s the idea. Less work is required by the STM system if we can trust that the variable isn’t modified by this transaction.
Well, my test case for this feature now takes ages to run. As I mentioned previously, a simple transaction modifying two integer variables under heavy contention can pull off almost two million transactions per second on my laptop.
My new test, in which each thread takes four variables and alternates between modifying two of them and reading the other two, runs perhaps ten thousand (!) times slower.
Of course I have several leads on how to fix this. The problem is largely all the performance-related “extras” I’ve been leaving out. For example, if a transaction fails to acquire a variable it needs, it simply aborts and immediately retries. In many cases, a better approach would be to block the thread, waiting for that variable to actually become available.
There are several other cases where I have a similar problem: I have to choose between delaying the thread for a moment with
sleep() before attempting to continue, blocking it until some condition is true, or aborting the transaction entirely and starting over from scratch. At the moment, I generally just pick the easiest solutions (typically abort, and occasionally call
sleep() a few times before we resort to that. Again, implementing some actual meaningful policies here would make a big difference. And tweaking these policies should help still more.
Another problem is that currently, I do not enforce a consistent global order when acquiring objects during a commit. This means I risk livelocks, again causing excessive rollbacks when multiple threads are competing over access to the same variables.
So I’m still optimistic. It should be possible to get performance back on track. But man, it’s depressing watching performance plummet like this.
And an update. After poking around a bit, it turned out that most of the time was being spent sleeping. When a transaction attempts to commit, if it can not acquire all the all the variables it needs, it retries a few times with a short delay (a couple of milliseconds) in between. If it doesn’t succeed after a few tries, it rolls back the entire transaction and starts over.
It turned out that these few, short
sleep() calls brought CPU utilization down to something like 0.01%, and totally destroyed performance. Simply turning the
sleep() call into a no-op brought me back to something more or less reasonable. I still need to improve on the above shortcomings, but now at least I can run my tests in less than an hour.