Wednesday, 11 June 2014

Advice for the concurrently confused: AtomicLong JDK7/8 vs. LongAdder

{UPDATE 03/09/14: If you come here looking for JMH related content start at the new and improved JMH Resources Page and branch out from there!}
Almost a year ago I posted some thoughts on scalable performance counters and compared AtomicLong, a LongAdder backport to JDK7, Cliff Clicks ConcurrentAtomicTable and my own humble ThreadLocalCounter. With JDK8 now released I thought I'd take an opportunity to refresh the numbers and have a look at the delivered LongAdder and the now changed AtomicLong. This is also an opportunity to refresh the JMH usage for this little exercise.

JMH Refresh

If you never heard of JMH here are a few links to catch up on:
In short, JMH is a micro-benchmarking harness written by the Oracle performance engineering team to help in constructing performance experiments while side stepping (or providing means to side step) the many pitfalls of Java related benchmarks.

Experiment Refresh

Here's a brief overview of the steps I took to update the original benchmark to the current:
  • At the time I wrote the original post JMH was pretty new but already had support for thread groups. Alas there was no way to tweak their size from the command line. Now there is so I could drop the boilerplate for different thread counts.
  • Hide counter types behind an interface/factory and use single benchmark (was possible before, just tidying up my own mess there).
  • Switched to using @Param to select the benchmarked counter type. With JMH 0.9 the framework will pick up an enum and run through it's values for me, yay!

The revised benchmark is pretty concise:
The JMH infrastructure takes care of launching requested numbers of incrementing/getting threads and sums up all the results neatly for us. The @Param defaults will run all variant if we don't pick a particular implementation from the command line. All together a more pleasant experience than the rough and tumble of version 0.1. Code repository is here.

AtomicLong: CAS vs LOCK XADD

With JDK8 a change has been made to the AtomicLong class to replace the CAS loop:
With a single intrinsic:
getAndAddLong() (which corresponds to fetch-and-add) translates into LOCK XADD on x86 CPUs which atomically returns the current value and increments it. This translates into better performance under contention as we leave the hardware to negotiate the atomic increment.

Incrementing only

Running the benchmark with incrementing threads only on a nice (but slightly old) dual socket Xeon X5650@2.67GHz (HyperThreading is off, 2 CPUs, 6 cores each, JDK7u51, JDK8u5) shows the improvement:

  • Left hand column is number of threads, all threads increment
  • All numbers are in nanoseconds measuring the average cost per op
  • Values are averaged across threads, so the cost per op is presented from a single threaded method call perspective.




JDK7-AL .inc (err) JDK8-AL .inc (err)
1 9.8 +/- 0.0 6.2 +/- 0.0
2 40.4 +/- 2.1 34.7 +/- 0.8
3 69.4 +/- 0.3 54.1 +/- 5.6
4 90.2 +/- 2.8 85.4 +/- 1.3
5 123.3 +/- 3.7 102.7 +/- 1.9
6 144.2 +/- 0.4 120.4 +/- 2.5
7 398.0 +/- 29.6 276.6 +/- 33.9
8 417.3 +/- 32.6 307.8 +/- 39.1
9 493.8 +/- 46.0 257.5 +/- 16.4
10 457.7 +/- 17.9 409.8 +/- 34.8
11 515.3 +/- 13.7 308.5 +/- 10.1
12 537.9 +/- 7.9 351.3 +/- 7.6

Notes on the how benchmarks were run:
  • I used taskset to pin the benchmark process to a set of cores
  • The number of cores allocated matches the number of threads required by the benchmark
  • Each socket has 6 cores and I taskset the cores from a single socket from 1 to 6, then spilt over to utilizing the other socket. In the particular layout of cores on this machine this turned out to translate into taskset -c 0-<number-of-threads - 1>
  • There are no get() threads in use, only inc() threads. This is controlled from the command line by setting the -tg option. E.g. -tg 0,6 will spin no get() threads and 6 inc() threads
 Observations:
  • JDK 8 AtomicLong is consistently faster as expected.
  • LOCK XADD does NOT cure the scalability issue in this case. This echoes the rule of thumb by D. Yukov here, which is that shared data writes are a scalability bottleneck (he is comparing private writes to a LOCK XADD on shared). The chart in his post demonstrates throughput rather than cost per op and the benchmark he uses is somewhat different. Importantly his chart demonstrates LOCK XADD hits a sustained throughput which remains fixed as threads are added. The large noise in the dual socket measurements renders the data less than conclusive in my measurements here but the throughput does converge. 
  • When we cross the socket boundary (>6 threads) the ratio between JDK7 and JDK8 increases.
  • Crossing the socket boundary also increases results variability (as expressed by the error). 
This benchmark demonstrates cost under contention. In a real application with many threads doing a variety of tasks you are unlikely to experience this kind of contention, but when you hit it you will pay.

LongAdder JDK7 backport vs. JDK8 LongAdder

Is a very boring comparison as they turn out to scale and cost pretty much the same (minor win for the native LongAdder implementation). It is perhaps comforting to anyone who needs to support a JDK7 client base that the backport should work fine on both and that no further work is required for now. Below are the results, LA7 is the LongAdder backport, LA8 is the JDK8 implementation:


JDK7-LA7 .inc (err) JDK8-LA7 .inc (err) JDK8-LA8 .inc (err)
1 9.8 +/- 0.0 10.0 +/- 0.8 9.8 +/- 0.0
2 12.1 +/- 0.6 10.8 +/- 0.1 10.0 +/- 0.1
3 11.5 +/- 0.4 11.7 +/- 0.3 10.3 +/- 0.0
4 12.4 +/- 0.6 11.1 +/- 0.1 10.3 +/- 0.0
5 12.4 +/- 0.8 11.5 +/- 0.6 10.3 +/- 0.0
6 11.8 +/- 0.3 11.1 +/- 0.3 10.3 +/- 0.0
7 11.6 +/- 0.4 12.9 +/- 1.3 10.6 +/- 0.4
8 11.8 +/- 0.6 11.8 +/- 0.7 10.8 +/- 0.5
9 12.9 +/- 0.9 12.0 +/- 0.7 10.5 +/- 0.3
10 12.6 +/- 0.4 12.1 +/- 0.6 11.0 +/- 0.5
11 11.5 +/- 0.2 12.3 +/- 0.6 10.7 +/- 0.3
12 11.7 +/- 0.4 11.5 +/- 0.3 10.4 +/- 0.1

JDK8: AtomicLong vs LongAdder

Similar results were demonstrated has been discussed in the prev. post, but here's the JDK8 versions side by side:



JDK8-AL .inc (err) JDK8-LA8 .inc (err)
1 6.2 +/- 0.0 9.8 +/- 0.0
2 34.7 +/- 0.8 10.0 +/- 0.1
3 54.1 +/- 5.6 10.3 +/- 0.0
4 85.4 +/- 1.3 10.3 +/- 0.0
5 102.7 +/- 1.9 10.3 +/- 0.0
6 120.4 +/- 2.5 10.3 +/- 0.0
7 276.6 +/- 33.9 10.6 +/- 0.4
8 307.8 +/- 39.1 10.8 +/- 0.5
9 257.5 +/- 16.4 10.5 +/- 0.3
10 409.8 +/- 34.8 11.0 +/- 0.5
11 308.5 +/- 10.1 10.7 +/- 0.3
12 351.3 +/- 7.6 10.4 +/- 0.1

Should I use AtomicLong or LongAdder?

Firstly this question is only relevant if you are not using AtomicLong as a unique sequence generator. LongAdder does not claim to, nor makes any attempt to give you that guarantee. So LongAdder is definitely NOT a drop in replacement for AtomicLong, they have very different semantics.
From the LongAdder JavaDoc:
This class is usually preferable to AtomicLong when multiple threads update a common sum that is used for purposes such as collecting statistics, not for fine-grained synchronization
control.  Under low update contention, the two classes have similar characteristics. But under high contention, expected throughput of this class is significantly higher, at the expense of higher space consumption.
Assuming you were using AtomicLong as a counter you will need to consider a few tradeoffs:
  • When NO contention is present, AtomicLong performs slightly better than LongAdder.
  • To avoid contention LongAdder will allocate Cells (see previous post for implementation discussion) each Cell will consume at least 256 bytes (current implementation of @Contended) and you may have as many Cells as CPUs. If you are on a tight memory budget and have allot of counters this is perhaps not the tool for the job.
  • If you prefer get() performance to inc() performance than you should definitely stick with AtomicLong.
  • When you prefer inc() performance and expect contention, and when you have some memory to spare then LongAdder is indeed a great choice.

Bonus material: How the Observer effects the Experiment

What is the impact of reading a value that is being rapidly mutated by another thread? On the observing thread side we expect to pay a read-miss, but as discussed prev. here there is a price to pay on the mutator side as well. I run the same benchmark with an equal number of inc()/get() threads. The process is pinned as before but as the roles are not uniform I have no control on which socket the readers/writers end up on, so we should expect more noise as we cross the socket line (LA - LongAdder, AL - AtomicLong, both on JDK8, same type ing/get are within the same run, left column is number of inc()/get() threads):


LA .get (err) LA .inc (err) AL .get (err) AL .inc (err)
1,1 4.7 +/- 0.2 68.6 +/- 5.0 10.2 +/- 1.0 39.1 +/- 7.6
2,2 24.9 +/- 2.7 69.7 +/- 4.0 41.5 +/- 26.4 87.6 +/- 11.0
3,3 139.9 +/- 24.3 69.0 +/- 8.2 55.1 +/- 13.3 157.4 +/- 28.2
4,4 332.9 +/- 10.3 80.4 +/- 5.1 56.8 +/- 7.6 198.7 +/- 21.1
5,5 479.3 +/- 13.2 84.1 +/- 4.4 71.9 +/- 7.3 233.1 +/- 20.1
6,6 600.6 +/- 11.2 89.8 +/- 4.6 152.1 +/- 41.7 343.2 +/- 41.0

Now that is a very different picture... 2 factors come into play:
  1. The reads disturb the writes by generating cache coherency noise as discussed here. A writer must have the cache line in an Exclusive/Mutated state, but the read will cause it to shift into Shared.
  2. The get() measurement does not differentiate between new values and old values.
This second point is important as we compare different means of mutating values being read. If the value being read is slowly mutating we will succeed in reading the same value many times before a change is visible. This will make our average operation time look great as most reads will be L1 hitting. If we have a fast incrementing implementation we will cause more cache misses for the reader making it look bad. On the other hand a slow reader will cause less of a disturbance to the incrementing threads as it produces less coherency noise, thus making less of a dent in the writer performance. Martin Thompson has previously hit on this issue from a different angle here (Note his posts discuss Nahelem and Sandybridge, I'm benchmarking on Westmere here).
In this light I'm not sure we can read much into the effect of readers in this particular use case. The 'model' represented by a hot reading thread does not sit well with the use case I have in mind for these counters which is normally as performance indicators to be sampled at some set frequency (once a second or millisecond). A different experiment is more appropriate, perhaps utilising Blackhole.consumeCPU to set a gap between get() (see a great insight into this method here).
With this explanation in mind we can see perhaps more sense in the following comparison between AtomicLong on JDK7 and JDK8:


AL7 .get (err) AL7 .inc (err) AL8 .get (err) AL8 .inc (err)
1,1 4.0 +/- 0.2 61.1 +/- 7.8 10.2 +/- 1.0 39.1 +/- 7.6
2,2 7.0 +/- 0.3 133.1 +/- 2.9 41.5 +/- 26.4 87.6 +/- 11.0
3,3 21.8 +/- 3.4 278.5 +/- 47.4 55.1 +/- 13.3 157.4 +/- 28.2
4,4 27.2 +/- 3.3 324.1 +/- 34.6 56.8 +/- 7.6 198.7 +/- 21.1
5,5 31.2 +/- 1.8 378.5 +/- 28.1 71.9 +/- 7.3 233.1 +/- 20.1
6,6 57.3 +/- 12.5 481.8 +/- 39.4 152.1 +/- 41.7 343.2 +/- 41.0

Now consider that the implementation of get() has not changed. The reason for the change in cost for get is down to the increase in visible values and thus cache misses caused by the change to the increment method.

Monday, 21 April 2014

Notes On Concurrent Ring Buffer Queue Mechanics

{This post is part of a long running series on lock free queues, checkout the full index to get more context here}
Having recently implemented an MPMC queue for JAQ (my concurrent queue collection) I have been bitten several times by the quirks of the underlying data structure and it's behaviour. This is an attempt at capturing some of the lessons learnt. I call these aspects or observations 'mechanics' because that's how they feel to me (you could call them Betty). So here goes...

What is a Ring Buffer?

A ring buffer is sometimes called a circular array or circular buffer (here's wiki) and is a pre-allocated finite chunk of memory that is accessed in a circular fashion to give the illusion of an infinite one. The N+1 element of a ring buffer of size N is the first element again:

So the first mechanical aspect to observe here is how the circularity is achieved. I'm using a modulo operator above, but you could use a bitwise AND instead if queue size was a power of 2 (i%(2^k) == i&((2^k) -1)). Importantly, circularity implies 2 different indexes can mean the same element.
Trivial stuff, I know, moving right along.

A Ring Buffer Based Bounded Queue: Wrap and Bounds

Ring buffers are used to implement all sorts of things, but in this particular case let us consider a bounded queue. In a bounded queue we are using the underlying buffer to achieve re-use, but we no longer pretend the capacity is infinite. We need to detect the queue being full/empty and track the next index to be offered/polled. For the non-thread safe case we can meet these requirements thus:

As an alternative we can use the data in the queue to detect the full/empty states:

The next mechanical aspect we can notice above is that for a queue to work we need to stop the producer from wrapping around and overwriting unread elements. Similarly we need the consumer to observe the queue is empty and stop consuming. The consumer is logically 'chasing' the producer, but due to the circular nature of the data structure the producer is also chasing the consumer. When they catch up to each other we hit the full/empty state.

The Single Producer/Consumer case: Visibility and Continuity

In the  SPSC (single producer/consumer threads, no more) case life remains remarkably similar to the non-thread safe case. Sure we can optimize memory layout and so on, but inherently the code above works with very minor changes. You will notice in the following code that I'm using a lovely hungarian notation of my own design, lets get the full notation out of the way:
  • lv - load volatile (LoadLoad barrier)
  • lp - load plain
  • sv - store volatile (LoadStore barrier)
  • so - store ordered (StoreStore barrier), like lazySet
  • sp - store plain
  • cas - compare and swap
lv/so for the counters could be implemented using an AtomicLong, an AtomicFieldUpdater or Unsafe. Here goes:
And the alternative (lvElement/soElement could be implemented using Unsafe or by replacing the original buffer with an AtomicReferenceArray):

All we needed to add are appropriate memory barriers to enforce visibility and ordering. The mechanical observation to be made regarding the barriers is that correct publication is a product of ordered visibility. This is very prominent in the counter based approach where the counter visibility derives data visibility in the queue. The second approach is slightly more subtle but the ordered write and volatile read guarantee correct publication of the element contents. This is an important property of concurrent queues that elements are not made visible to other threads before preceding writes to their contents.
I've added an optimization on the alternative offer that highlights a mechanical property of the SPSC and non-thread safe cases. The offer is probing ahead on the buffer, if the "tail + look ahead constant" element is null we can deduce that all the elements up to it are also null and write without checking them. This property of the queue is the continuity of data existence. We expect the ring buffer to be split to an all empty section and an all full section and we expect no 'holes' in either.

The MPSC case: Atomicity and Holes in the fabric of Existence

So now we have multiple threads hitting the queue. We need to ensure the blighters don't over-write the same slot and so we must increment head atomically before writing to the queue:

An interesting thing happens on the consumer side here. We can no longer rely on the tail counter for data visibility because the increment is done before the data is written. We must wait for the data to become visible and can't just assume it is there as we did before. This highlights the fact that tail is no longer indicative of producer progress.
The alternative poll method in this case cuts straight to the issue:

I will not show the code for the SPMC case, it is very similar, but one point is worth examining. For the SPMC the offer method can no longer employ the probe ahead optimization shown above. That is because the continuity property is no longer true (a real shame, I liked it allot). Consider 2 consumer threads where one has progressed the danger point where head will be visible but not the data, and the other charging ahead of it. The slot is not null and remains so until the thread resumes. This means the empty section of the queue now has a hole (not null element) in it... making the probe ahead optimization void. If we were to keep the optimization the producer would assume the coast is clear and may overwrite an element in the queue before it is consumed.
For both MPSC/SPMC and also MPMC we can therefore observe that counter increment atomicity does not imply queue write atomicity. We can also see that this scheme has no fairness of counter acquisition or slot use so it is possible to have many producers/consumers stuck while others make progress. For example, given 3 producers A, B and C we can have the queue fill up such that the slots are claimed to the effect of: [ABBBBBBBCAAAAACCCABABACCCC...] or any such random layout based on the whims of the scheduler and CAS contention.

The MPMC case: What Goes Around

So finally all hell breaks loose and you have multiple producers and consumers all going ape pulling and pushing. What can we do? I ended up going with the solution put forward by Mr. D. Vyukov after I implemented a few flawed variations myself (an amusing story to be told some other time). His solution is in C and benefits from the memory layout afforded by languages with struct support. I had to mutate the algorithm (any bugs are my own) to use 2 arrays instead of one struct array but otherwise the algorithm is the very similar:

So... what just happened?
  • We're setting up each slot with a sequence
  • A producer can only write to the slot if the sequence matches tail and they won the CAS
  • A consumer can only read the slot if the sequence is head + 1 and they won the CAS
  • Once a producer writes a slot he sets it's sequence to tail + 1
  • Once a consumer reads a slot she sets the sequence to head + buffer.length
Why can't we rely on the head/tail anymore? well... the head/tail values were half useless before as pointed out in the MPSC section because they reflect the most advance consumer/producer and cannot indicate data state in the queue.
Can't we use the null/not-null check like before? mmm... this one is a bugger. The surprising problem here is producers catching up with other producers after wrapping and consumers doing the same to other consumers. Imagine a short queue, 2 slots only, and 2 producer threads. Thread one wins the CAS and stalls before writing slot 1, thread 2 fills second slot and comes round to hit slot 1 again, wins the CAS and either writes over thread 1 or gets written over by thread 1. They can both see the slot as empty when they get there.
A solution relying on counters exists such that it employs a second CAS on the data, but:

  1. It is slower, which is to be expected when you use 2 CAS instead of one
  2. It runs the risk of threads getting stuck to be freed only when the other threads come full circle again. Think of a producer hitting another producer on the wrap as discussed before and then one wins the CAS on data and the other is left to spin until the slot is null again. This should be extremely rare (very hard to produce in testing, possible by debugging to the right points), but is not a risk I am comfortable with.

I'm hoping to give concrete examples broken code in a further post, but for now you can imagine or dig through the commit history of JAQ for some examples.
The sequence array is doubling our memory requirement (tripling it for 32bit/compressed oops). We might be able to get by with an int array instead. The solution works great in terms of performance, but that is another story (expect followup post).
The important observation here on the mechanical side is that for MPMC both head and tail values are no longer reliable means of detecting wrap and as such we have to detect wrap by means other than head/tail counters and data existence.

Summary

  • Circular/ring array/buffer give the illusion of infinite arrays but are actually finite.
  • Bounded queues built on ring buffers must detect queue full/empty states and track head/tail positions.
  • Ring buffers exhibit continuity of existence for the full/empty sections ONLY in the SPSC or single threaded case.
  • MPSC/SPMC/MPMC queues lose continuity, can have holes.
  • Counter increment atomicity does not imply write atomicity.
  • MP means tail is no longer a reliable means of ensuring next poll is possible.
  • MC means head is no longer a reliable mean of ensuring next offer is possible.
  • MPMC implementations must contend with producer/producer and consumer/consumer collisions on wrap.
I'm publishing this post in a bit of a rush, so please comment on any inaccuracies/issues/uncertainties and I shall do my best to patch/explain if needed. Many thanks go out to Martin Thompson, Georges Gomez, Peter Hughes and anybody else who's bored of hearing my rambles on concurrent queues.