@@ -24,16 +24,9 @@ We implement nested transactions by flattening.
24
24
We only support strong isolation if you use the API correctly. In order words,
25
25
we do not support strong isolation.
26
26
27
- Our implementation uses a very simple two-phased locking with versioned locks
28
- algorithm and lazy writes, as per [ 1] .
29
-
30
- See:
31
-
32
- 1 . T. Harris, J. Larus, and R. Rajwar. Transactional Memory. Morgan & Claypool, second edition, 2010.
33
-
34
- Note that this implementation allows transactions to continue in a zombie state
35
- with inconsistent reads, so it's possible for the marked exception to be raised
36
- in the example below.
27
+ Our implementation uses a very simple algorithm that locks each ` TVar ` when it
28
+ is first read or written. If it cannot lock a ` TVar ` it aborts and retries.
29
+ There is no contention manager so competing transactions may retry eternally.
37
30
38
31
``` ruby
39
32
require ' concurrent-ruby'
@@ -216,97 +209,3 @@ big global lock on them, and that if any exception is raised in the block, it
216
209
will be as if the block never happened. But also keep in mind the important
217
210
points we detailed right at the start of the article about side effects and
218
211
repeated execution.
219
-
220
- ## Evaluation
221
-
222
- We evaluated the performance of our ` TVar ` implementation using a bank account
223
- simulation with a range of synchronisation implementations. The simulation
224
- maintains a set of bank account totals, and runs transactions that either get a
225
- summary statement of multiple accounts (a read-only operation) or transfers a
226
- sum from one account to another (a read-write operation).
227
-
228
- We implemented a bank that does not use any synchronisation (and so creates
229
- inconsistent totals in accounts), one that uses a single global (or 'coarse')
230
- lock (and so won't scale at all), one that uses one lock per account (and so has
231
- a complicated system for locking in the correct order) and one using our ` TVar `
232
- and ` atomically ` .
233
-
234
- We ran 1 million transactions divided equally between a varying number of
235
- threads on a system that has at least that many physical cores. The transactions
236
- are made up of a varying mixture of read-only and read-write transactions. We
237
- ran each set of transactions thirty times, discarding the first ten and then
238
- taking an algebraic mean. These graphs show only the simple mean. Our `tvars-
239
- experiments` branch includes the benchmark used, full details of the test
240
- system, and all the raw data.
241
-
242
- Using JRuby using 75% read-write transactions, we can compare how the different
243
- implementations of bank accounts scales to more cores. That is, how much faster
244
- it runs if you use more cores.
245
-
246
- ![ ] ( https://raw.githubusercontent.com/ruby-concurrency/concurrent-ruby/master/doc/images/tvar/implementation-scalability.png )
247
-
248
- We see that the coarse lock implementation does not scale at all, and in fact
249
- with more cores only wastes more time in contention for the single global lock.
250
- We see that the unsynchronised implementation doesn't seem to scale well - which
251
- is strange as there should be no overhead, but we'll explain that in a second.
252
- We see that the fine lock implementation seems to scale better, and that the
253
- ` TVar ` implementation scales the best.
254
-
255
- So the ` TVar ` implementation * scales* very well, but how absolutely fast is it?
256
-
257
- ![ ] ( https://raw.githubusercontent.com/ruby-concurrency/concurrent-ruby/master/doc/images/tvar/implementation-absolute.png )
258
-
259
- Well, that's the downside. The unsynchronised implementation doesn't scale well
260
- because it's so fast in the first place, and probably because we're bound on
261
- access to the memory - the threads don't have much work to do, so no matter how
262
- many threads we have the system is almost always reaching out to the L3 cache or
263
- main memory. However remember that the unsynchronised implementation isn't
264
- correct - the totals are wrong at the end. The coarse lock implementation has an
265
- overhead of locking and unlocking. The fine lock implementation has a greater
266
- overhead as as the locking scheme is complicated to avoid deadlock. It scales
267
- better, however, actually allowing transactions to be processed in parallel. The
268
- ` TVar ` implementation has a greater overhead still - and it's pretty huge. That
269
- overhead is the cost for the simple programming model of an atomic block.
270
-
271
- So that's what ` TVar ` gives you at the moment - great scalability, but it has a
272
- high overhead. That's pretty much the state of software transactional memory in
273
- general. Perhaps hardware transactional memory will help us, or perhaps we're
274
- happy anyway with the simpler and safer programming model that the ` TVar ` gives
275
- us.
276
-
277
- We can also use this experiment to compare different implementations of Ruby. We
278
- looked at just the ` TVar ` implementation and compared MRI 2.1.1, Rubinius 2.2.6,
279
- and JRuby 1.7.11, again at 75% write transactions.
280
-
281
- ![ ] ( https://raw.githubusercontent.com/ruby-concurrency/concurrent-ruby/master/doc/images/tvar/ruby-scalability.png )
282
-
283
- We see that MRI provides no scalability, due to the global interpreter lock
284
- (GIL). JRuby seems to scale better than Rubinius for this workload (there are of
285
- course other workloads).
286
-
287
- As before we should also look at the absolute performance, not just the
288
- scalability.
289
-
290
- ![ ] ( https://raw.githubusercontent.com/ruby-concurrency/concurrent-ruby/master/doc/images/tvar/ruby-absolute.png )
291
-
292
- Again, JRuby seems to be faster than Rubinius for this experiment.
293
- Interestingly, Rubinius looks slower than MRI for 1 core, but we can get around
294
- that by using more cores.
295
-
296
- We've used 75% read-write transactions throughout. We'll just take a quick look
297
- at how the scalability varies for different workloads, for scaling between 1 and
298
- 2 threads. We'll admit that we used 75% read-write just because it emphasised
299
- the differences.
300
-
301
- ![ ] ( https://raw.githubusercontent.com/ruby-concurrency/concurrent-ruby/master/doc/images/tvar/implementation-write-proportion-scalability.png )
302
-
303
- Finally, we can also run on a larger machine. We repeated the experiment using a
304
- machine with 64 physical cores and JRuby.
305
-
306
- ![ ] ( https://raw.githubusercontent.com/ruby-concurrency/concurrent-ruby/master/doc/images/tvar/implementation-scalability.png )
307
-
308
- ![ ] ( https://raw.githubusercontent.com/ruby-concurrency/concurrent-ruby/master/doc/images/tvar/implementation-absolute.png )
309
-
310
- Here you can see that ` TVar ` does become absolutely faster than using a global
311
- lock, at the slightly ridiculously thread-count of 50. It's probably not
312
- statistically significant anyway.
0 commit comments