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