Ответ 1
Каковы правила, которые следует соблюдать, чтобы определить, когда неувязка выполняет неустойчивое чтение?
Во-первых, это не просто компилятор, который перемещает инструкции. Большие 3 актера в игре, которые вызывают переупорядочение команд:
- Компилятор (например, С# или VB.NET)
- Время выполнения (например, CLR или Mono)
- Оборудование (например, x86 или ARM)
Правила на аппаратном уровне немного реже и сухие, поскольку они обычно хорошо документированы. Но на уровнях времени выполнения и компилятора существуют спецификации модели памяти, которые обеспечивают ограничения на то, как инструкции могут быть переупорядочены, но разработчикам необходимо решить, насколько агрессивно они хотят оптимизировать код и как они будут стремиться к линии в отношении ограничений модели памяти.
Например, спецификация ECMA для CLI обеспечивает довольно слабые гарантии. Но Microsoft решила ужесточить эти гарантии в CLR.NET Framework. Помимо нескольких сообщений в блогах я не видел много официальной документации по правилам, которым придерживается CLR. Моно, конечно, может использовать другой набор правил, которые могут или не могут приблизить его к спецификации ECMA. И, конечно, может быть некоторая свобода в изменении правил в будущих выпусках, пока формальная спецификация ECMA по-прежнему рассматривается.
Со всем сказанным у меня есть несколько замечаний:
- Компиляция с конфигурацией Release более вероятно приведет к переупорядочению команд.
- У более простых методов более вероятно, что их инструкции будут переупорядочены.
- Подъем чтения изнутри цикла во внешний цикл является типичным типом оптимизации переупорядочения.
И почему я все еще могу получить цикл для выхода с тем, что я считаю нечетные меры?
Это потому, что эти "нечетные меры" делают одну из двух вещей:
- создание неявного барьера памяти
- обход компилятора или возможности выполнения определенных оптимизаций
Например, если код внутри метода становится слишком сложным, это может помешать компилятору JIT выполнять определенные оптимизации, которые переупорядочивают инструкции. Вы можете думать об этом как о том, как сложные методы также не встраиваются.
Кроме того, такие вещи, как Thread.Yield
и Thread.Sleep
создают неявные барьеры памяти. Я начал список таких механизмов здесь. Готов поспорить, если вы поместите вызов Console.WriteLine
в свой код, это также вызовет выход цикла. Я также видел, что пример "non terminating loop" ведет себя по-разному в разных версиях .NET Framework. Например, держу пари, если вы запустили этот код в 1.0, он завершится.
Вот почему использование Thread.Sleep
для имитации чередования потоков может фактически маскировать проблему барьера памяти.
Update:
Прочитав некоторые из ваших комментариев, я думаю, вы можете быть в замешательстве относительно того, что действительно делает Thread.MemoryBarrier
. То, что он делает, создает барьер с полным заграждением. Что именно это значит? Барьер с полным заграждением представляет собой состав двух полузакрытий: забор и забор. Я определю их сейчас.
- Захват забора: барьер памяти, в котором другие чтения и записи не могут перемещаться перед забором.
- Забор заготовки: барьер памяти, в котором другие чтения и записи не могут перемещаться после забора.
Таким образом, когда вы видите вызов Thread.MemoryBarrier
, это предотвратит перемещение всех чтений и записей от уровня выше или ниже барьера. Он также выдаст все необходимые для процессора инструкции.
Если вы посмотрите на код для Thread.VolatileRead
, вот что вы увидите.
public static int VolatileRead(ref int address)
{
int num = address;
MemoryBarrier();
return num;
}
Теперь вам может быть интересно, почему вызов MemoryBarrier
после фактического чтения. Ваша интуиция может сказать вам, что для получения "свежего" чтения address
вам понадобится вызов MemoryBarrier
, который должен произойти до этого чтения. Но, увы, ваша интуиция ошибается! В спецификации говорится, что волатильное считывание должно создавать барьер для забора. И в определении, которое я дал вам выше, это означает, что вызов MemoryBarrier
должен быть после чтения address
, чтобы предотвратить перемещение других чтений и записей перед ним. Вы видите, что волатильные чтения - это не просто получение "свежего" чтения. Речь идет о предотвращении перемещения инструкций. Это невероятно запутанно; Я знаю.