Comment #0 by bearophile_hugs — 2013-03-19T16:39:52Z
See for more info:
http://openjdk.java.net/jeps/142https://blogs.oracle.com/dave/entry/java_contented_annotation_to_help
The idea is to introduce some way in D to specify that some fields in an struct/object are probably highly contended across processor cores, so the compiler can pad (or arrange, in objects) to not share cache lines with other fields, or other structs, that are likely to be independently accessed.
- - - - - - - - - - - - - - - - - - - -
This is from:
http://mail.openjdk.java.net/pipermail/hotspot-dev/2012-November/007309.html
> A. Marking the class as contended:
>
> @Contended
> public static class ContendedTest2 {
> private Object plainField1;
> private Object plainField2;
> private Object plainField3;
> private Object plainField4;
> }
>
> ...makes the entire field block to be padded from the both sides:
> (below is the output of new tracing -XX:+PrintFieldLayout)
>
> TestContended$ContendedTest2: field layout
> Entire class is marked contended
> @140 --- instance fields start ---
> @140 "plainField1" Ljava.lang.Object;
> @144 "plainField2" Ljava.lang.Object;
> @148 "plainField3" Ljava.lang.Object;
> @152 "plainField4" Ljava.lang.Object;
> @288 --- instance fields end ---
> @288 --- instance ends ---
>
> Note that we use 128 bytes, twice the cache line size on most hardware
> to adjust for adjacent sector prefetchers extending the false sharing
> collisions to two cache lines.
>
> B. Marking the field as contended:
>
> public static class ContendedTest1 {
> @Contended
> private Object contendedField1;
> private Object plainField1;
> private Object plainField2;
> private Object plainField3;
> private Object plainField4;
> }
>
> ...pushes the field out of dense block and effectively applies padding:
>
> TestContended$ContendedTest1: field layout
> @ 12 --- instance fields start ---
> @ 12 "plainField1" Ljava.lang.Object;
> @ 16 "plainField2" Ljava.lang.Object;
> @ 20 "plainField3" Ljava.lang.Object;
> @ 24 "plainField4" Ljava.lang.Object;
> @156 "contendedField1" Ljava.lang.Object; (contended, group = 0)
> @288 --- instance fields end ---
> @288 --- instance ends ---
>
> C. Marking multiple fields makes each field padded:
>
> public static class ContendedTest4 {
> @Contended
> private Object contendedField1;
>
> @Contended
> private Object contendedField2;
>
> private Object plainField3;
> private Object plainField4;
> }
>
> ...pushes both fields with individual padding for each:
>
> TestContended$ContendedTest4: field layout
> @ 12 --- instance fields start ---
> @ 12 "plainField3" Ljava.lang.Object;
> @ 16 "plainField4" Ljava.lang.Object;
> @148 "contendedField1" Ljava.lang.Object; (contended, group = 0)
> @280 "contendedField2" Ljava.lang.Object; (contended, group = 0)
> @416 --- instance fields end ---
> @416 --- instance ends ---
>
> *** IV. Contention groups
>
> There are cases where you want to separate the *group* of fields that
> are experiencing contention with everything else but not pairwise. This
> is the usual thing for some of the code updating two fields at once.
> While marking both with @Contended would be sufficient, we can optimize
> the memory footprint by not applying padding between them. In order to
> demarcate these groups, we have the parameter in the annotation
> describing the equivalence class for contention group.
>
> So that:
>
> public static class ContendedTest5 {
> @Contended("updater1")
> private Object contendedField1;
>
> @Contended("updater1")
> private Object contendedField2;
>
> @Contended("updater2")
> private Object contendedField3;
>
> private Object plainField5;
> private Object plainField6;
> }
>
> ...is laid out as:
>
> TestContended$ContendedTest5: field layout
> @ 12 --- instance fields start ---
> @ 12 "plainField5" Ljava.lang.Object;
> @ 16 "plainField6" Ljava.lang.Object;
> @148 "contendedField1" Ljava.lang.Object; (contended, group = 12)
> @152 "contendedField2" Ljava.lang.Object; (contended, group = 12)
> @284 "contendedField3" Ljava.lang.Object; (contended, group = 15)
> @416 --- instance fields end ---
> @416 --- instance ends ---
>
> Note $contendedField1 and $contendedField2 are padded from everything
> else, but still densely packed with each other.
- - - - - - - - - - - - - - - - - - - -
A design for D is similar. For structs the problem is solved with padding between fields or between structs, while in class instances it's solved with padding and/or field reordering. The syntax is similar:
struct Test1 {
@contended int field1;
int field2;
@contended int field3;
}
@contended final class Test2 {
@contended int field1;
int field2;
@contended int field3;
}
@contended struct Test3 {
int field1;
int field2;
}
@contended final class Test4 {
private Object field1;
private Object field2;
}
- - - - - - - - - - - - - - - - - - - -
Contention groups in D:
struct Test5 {
@contended("group1") int field1;
@contended("group1") int field2;
@contended("group2") int field3;
private int field4;
private int field5;
}
- - - - - - - - - - - - - - - - - - - -
I think the D compiler should ignore @contended for const/immutable fields:
alias T = const(int);
struct Test6 {
@contended T field1;
int field2;
}
static assert(Test6.sizeof == 8);
- - - - - - - - - - - - - - - - - - - -
An alternative syntax is to use align:
align(cache_line)
align(cache_line, "group1")
Like this:
struct Test7 {
align(cache_line) int field1;
int field2;
align(cache_line) int field3;
}
align(cache_line) class Test8 {
private Object field1;
private Object field2;
}
struct Test9 {
align(cache_line, "group1") int field1;
align(cache_line, "group1") int field2;
align(cache_line, "group2") int field3;
private int field4;
private int field5;
}
But @contended is more explicit in its purpose.
- - - - - - - - - - - - - - - - - - - -
Cache lines contention is a well known phenomenon. This is a small example. The code is adapted from:
http://rosettacode.org/wiki/Atomic_updates#D
// start atomic_updates.d code.
import std.stdio: writeln;
import std.conv: text;
import std.random: uniform, Xorshift;
import std.algorithm: min, max;
import std.parallelism: task;
import core.thread: Thread;
import core.sync.mutex: Mutex;
import core.time: dur;
__gshared uint transfersCount;
final class Buckets(size_t nBuckets) if (nBuckets > 0) {
alias TBucketValue = uint;
private static struct Bucket {
TBucketValue value;
Mutex mtx;
//uint[14] _voidPadding = void;
alias value this;
}
private Bucket[nBuckets] buckets;
private bool running;
public this() {
this.running = true;
foreach (ref b; buckets)
b = Bucket(uniform(0, 100), new Mutex());
}
public TBucketValue opIndex(in size_t index) const pure nothrow {
return buckets[index];
}
public void transfer(in size_t from, in size_t to,
in TBucketValue amount) {
immutable low = min(from, to);
immutable high = max(from, to);
buckets[low].mtx.lock();
buckets[high].mtx.lock();
scope(exit) {
buckets[low].mtx.unlock();
buckets[high].mtx.unlock();
}
immutable realAmount = min(buckets[from].value, amount);
buckets[from] -= realAmount;
buckets[to ] += realAmount;
transfersCount++;
}
@property size_t length() const pure nothrow {
return this.buckets.length;
}
void toString(in void delegate(const(char)[]) sink) {
TBucketValue total = 0;
foreach (ref b; buckets) {
b.mtx.lock();
total += b;
}
scope(exit)
foreach (ref b; buckets)
b.mtx.unlock();
sink(text(buckets));
sink(" ");
sink(text(total));
}
}
void randomize(size_t N)(Buckets!N data) {
immutable maxi = data.length - 1;
auto rng = Xorshift(1);
while (data.running) {
immutable i = uniform(0, maxi, rng);
immutable j = uniform(0, maxi, rng);
immutable amount = uniform(0, 20, rng);
data.transfer(i, j, amount);
}
}
void equalize(size_t N)(Buckets!N data) {
immutable maxi = data.length - 1;
auto rng = Xorshift(1);
while (data.running) {
immutable i = uniform(0, maxi, rng);
immutable j = uniform(0, maxi, rng);
immutable a = data[i];
immutable b = data[j];
if (a > b)
data.transfer(i, j, (a - b) / 2);
else
data.transfer(j, i, (b - a) / 2);
}
}
void display(size_t N)(Buckets!N data) {
foreach (immutable _; 0 .. 10) {
writeln(transfersCount, " ", data);
transfersCount = 0;
Thread.sleep(dur!"msecs"(1000));
}
data.running = false;
}
void main() {
writeln("N. transfers, buckets, buckets sum:");
auto data = new Buckets!20();
task!randomize(data).executeInNewThread();
task!equalize(data).executeInNewThread();
task!display(data).executeInNewThread();
}
// end atomic_updates.d code.
Timings by nazriel on IRC, done with dmd on an Intel i5 2,4ghz, arch linux x86_64 with:
-release -inline -O -noboundscheck
Without padding in struct Bucket:
N. transfers, buckets, buckets sum:
85 [0, 5, 0, 51, 5, 25, 89, 0, 147, 93, 47, 2, 76, 43, 76, 69, 0, 62, 67, 51] 908
6246828 [35, 51, 38, 25, 48, 88, 11, 23, 39, 54, 92, 13, 69, 48, 29, 48, 52, 26, 68, 51] 908
6322759 [60, 40, 43, 55, 44, 39, 49, 61, 38, 44, 51, 42, 18, 37, 34, 44, 50, 48, 60, 51] 908
6103135 [29, 63, 74, 44, 44, 48, 44, 53, 35, 62, 35, 44, 44, 44, 34, 47, 31, 40, 42, 51] 908
6567254 [36, 39, 42, 49, 70, 36, 10, 28, 51, 39, 55, 42, 35, 72, 42, 65, 35, 42, 69, 51] 908
6274262 [55, 63, 27, 40, 31, 40, 41, 32, 20, 44, 45, 95, 42, 54, 45, 38, 76, 40, 29, 51] 908
6171490 [39, 24, 61, 56, 42, 51, 39, 56, 62, 43, 72, 39, 56, 13, 32, 67, 45, 48, 12, 51] 908
6211137 [29, 39, 36, 70, 16, 41, 48, 35, 74, 20, 65, 59, 48, 48, 43, 59, 54, 30, 43, 51] 908
6202907 [51, 16, 80, 36, 25, 42, 43, 58, 48, 41, 41, 35, 80, 49, 54, 24, 40, 44, 50, 51] 908
6350466 [50, 35, 37, 66, 46, 65, 38, 48, 52, 47, 51, 46, 20, 37, 34, 46, 51, 38, 50, 51] 908
With padding in struct Bucket:
N. transfers, buckets, buckets sum:
112 [22, 41, 41, 54, 54, 58, 52, 32, 56, 53, 49, 64, 59, 65, 56, 74, 54, 59, 66, 55] 1064
6698768 [56, 58, 54, 29, 38, 54, 58, 55, 54, 72, 26, 57, 83, 51, 49, 45, 56, 57, 57, 55] 1064
8371235 [53, 53, 53, 53, 52, 53, 53, 54, 53, 54, 53, 52, 53, 54, 53, 54, 53, 53, 53, 55] 1064
7680510 [73, 71, 16, 53, 20, 55, 84, 57, 55, 53, 49, 55, 57, 50, 52, 68, 53, 35, 53, 55] 1064
6675895 [45, 32, 79, 46, 48, 29, 65, 79, 13, 79, 0, 79, 61, 65, 40, 63, 65, 61, 60, 55] 1064
7865702 [31, 63, 58, 76, 54, 43, 49, 51, 51, 63, 54, 29, 47, 66, 54, 62, 54, 56, 48, 55] 1064
8068515 [51, 69, 49, 54, 54, 59, 51, 58, 44, 46, 58, 56, 48, 57, 59, 48, 38, 56, 54, 55] 1064
6718293 [40, 55, 52, 55, 77, 51, 40, 57, 87, 37, 56, 56, 49, 69, 65, 17, 13, 54, 79, 55] 1064
6512280 [53, 34, 21, 69, 47, 69, 43, 25, 61, 43, 63, 52, 97, 52, 50, 52, 49, 78, 51, 55] 1064
7408234 [51, 44, 54, 56, 31, 55, 34, 59, 72, 69, 56, 50, 58, 70, 45, 67, 48, 41, 49, 55] 1064
The numbers in the first column like 6274262 represent the number of updated in a second. With the padding there is less cache lines contention between two cores, so it performs more atomic updated in a second, like 7408234.
That manual padding in Bucket is similar to:
@contended private static struct Bucket {
TBucketValue value;
Mutex mtx;
alias value this;
}
One difference is that in the Java example the padding is on both sides, and it's twice larger, as explained:
> Note that we use 128 bytes, twice the cache line size on most hardware
> to adjust for adjacent sector prefetchers extending the false sharing
> collisions to two cache lines.
- - - - - - - - - - - - - - - - - - - -
Comment #1 by bearophile_hugs — 2013-03-20T11:53:59Z
A current workaround is to use align (with a value, because of Issue 9766 ):
align(128) struct Test3 {
int field1;
int field2;
}
pragma(msg, "Test3:");
pragma(msg, Test3.field1.offsetof);
pragma(msg, Test3.field2.offsetof);
pragma(msg, "Total size:");
pragma(msg, Test3.sizeof);
pragma(msg, "");
The print shows there is trailing padding (no leading padding):
Test3:
0u
4u
Total size:
128u
Adding align(128) on some fields of struct/object allows to introduce intermediate padding, but it's tricky to get all the padding right. But @contended adapts automatically the padding needed on different CPUs and makes the creation of spaces and groups simpler.
Comment #2 by robert.schadek — 2024-12-13T18:05:03Z