Ответ 1
static double s_x;
Намного сложнее продемонстрировать эффект, когда вы используете двойной. CPU использует специальные инструкции для загрузки и хранения двойных, соответственно FLD и FSTP. Это намного проще с тех пор, пока нет единой инструкции, которая загружает/сохраняет 64-разрядное целое число в 32-битном режиме. Чтобы наблюдать за ним, вам нужно, чтобы переменный адрес был несогласован, поэтому он пересекает границу строки кэша процессора.
Это никогда не произойдет с объявлением, которое вы использовали, JIT-компилятор гарантирует, что double правильно выровнен, сохранен на адресе, кратном 8. Вы можете сохранить его в поле класса, только ГС-распределитель выравнивает до 4 в 32-битном режиме. Но что дерьмо стрелять.
Лучший способ сделать это - умышленное неправильное выравнивание двойника с помощью указателя. Поставьте небезопасным перед классом программы и сделайте его похожим на это:
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;
Это все еще не гарантирует хорошего несоосности (hehe), поскольку нет способа точно контролировать, где AllocCoTaskMem() будет выровнять выделение относительно начала строки кэша процессора. И это зависит от ассоциативности кеша в вашем ядре процессора (мой - Core i5). Вы должны будете возиться со смещением, я получил значение 28 путем экспериментов. Значение должно делиться на 4, но не на 8, чтобы действительно имитировать поведение кучи GC. Продолжайте добавлять 8 к значению до тех пор, пока вы не удвоите его, чтобы переместиться в строку кэша и активируйте assert.
Чтобы сделать его менее искусственным, вам придется написать программу, которая хранит двойное поле в классе и получает сборщик мусора, чтобы перемещать его по памяти, чтобы он был смещен. Сложно придумать пример программы, которая гарантирует, что это произойдет.
Также обратите внимание, как ваша программа может продемонстрировать проблему, называемую ложным совместным использованием. Прокомментируйте вызов метода Start() для потока B и обратите внимание на то, как работает более быстрый поток A. Вы видите стоимость процессора, который поддерживает линию кэша, согласованную между ядрами процессора. Совместное использование предназначено здесь, поскольку потоки обращаются к одной и той же переменной. Реальное ложное совместное использование происходит, когда потоки обращаются к различным переменным, которые хранятся в одной и той же строке кэша. В противном случае, почему выравнивание имеет значение, вы можете наблюдать за разрывом в два раза, когда часть его находится в одной строке кэша, а часть ее находится в другой.