static double s_x;
Гораздо сложнее продемонстрировать эффект при использовании дубля. Процессор использует специальные инструкции для загрузки и хранения двойных, соответственно, FLD и FSTP. С long это намного проще, так как нет ни одной инструкции, которая загружает / сохраняет 64-битное целое число в 32-битном режиме. Для этого необходимо, чтобы адрес переменной был выровнен, чтобы он не перекрывал границу строки кэша процессора.
Этого никогда не случится с объявлением, которое вы использовали, JIT-компилятор гарантирует, что double выровнен правильно, сохранен по адресу, кратному 8. Вы можете сохранить его в поле класса, распределитель GC только выравнивает до 4 в 32-битном режиме. Но это дерьмовый выстрел.
Лучший способ сделать это - преднамеренно неправильно выровнять двойное число с помощью указателя. Поместите unsafe перед классом Program и сделайте так, чтобы он выглядел примерно так:
static double* s_x;
static void Main(string[] args) {
var mem = Marshal.AllocCoTaskMem(100);
s_x = (double*)((long)(mem) + 28);
TestTearingDouble();
}
ThreadA:
*s_x = ((i & 1) == 0) ? 0.0 : double.MaxValue;
ThreadB:
double x = *s_x;
Это по-прежнему не гарантирует хорошего смещения (хе-хе), поскольку нет способа точно определить, где AllocCoTaskMem () будет выравнивать распределение относительно начала строки кэша процессора. И это зависит от ассоциативности кэша в вашем ядре процессора (у меня это Core i5). Вам придется возиться со смещением, я получил значение 28 экспериментальным путем. Значение должно делиться на 4, но не на 8, чтобы действительно имитировать поведение кучи GC. Продолжайте добавлять 8 к значению до тех пор, пока не получите двойное, чтобы расположить строку кэша и вызвать утверждение.
Чтобы сделать его менее искусственным, вам нужно написать программу, которая хранит значение типа double в поле класса и заставить сборщик мусора перемещать его в памяти, чтобы он не выравнивался. В некотором роде трудно придумать пример программы, которая гарантирует , что это происходит.
Также обратите внимание, как ваша программа может продемонстрировать проблему, называемую false share . Закомментируйте вызов метода Start () для потока B и отметьте, насколько быстрее работает поток A. Вы видите стоимость процессора, поддерживающего согласованность строки кэша между ядрами процессора. Обмен предназначен здесь, так как потоки обращаются к одной и той же переменной. Реальное ложное совместное использование происходит, когда потоки обращаются к различным переменным, которые хранятся в одной и той же строке кэша. В противном случае выравнивание имеет значение: вы можете наблюдать разрыв для двойника только тогда, когда его часть находится в одной строке кэша, а часть - в другой.