Разрешенная оптимизация компилятора С# для локальных переменных и выборка из памяти
EDIT. Я спрашиваю, что происходит, когда два потока одновременно получают доступ к тем же данным без правильной синхронизации (перед этим редактированием этот пункт не был явно выражен).
У меня есть вопрос об оптимизации, которые выполняются компилятором С# и компилятором JIT.
Рассмотрим следующий упрощенный пример:
class Example {
private Action _action;
private void InvokeAction() {
var local = this._action;
if (local != null) {
local();
}
}
}
Пожалуйста, проигнорируйте в примере, что чтение _action
может привести к кэшированию и устаревшему значению, поскольку нет летучего спецификатора или какой-либо другой синхронизации. Это не главное:)
Является ли компилятор (или, фактически, дрожание во время выполнения), оптимизировать назначение для локальной переменной и вместо этого дважды читать _action
из памяти:
class Example {
private Action _action;
private void InvokeAction() {
if (this._action != null) {
this._action(); // might be set to null by an other thread.
}
}
}
который может вызывать NullReferenceException
, когда поле _action
установлено на null
с помощью параллельного присваивания.
Конечно, в этом примере такая "оптимизация" не имела бы никакого смысла, потому что было бы быстрее хранить значение в регистре и, таким образом, используя локальную переменную. Но в более сложных случаях есть ли гарантия, что это работает так, как ожидалось, без повторного чтения значения из памяти?
Ответы
Ответ 1
Это законная оптимизация в соответствии с моделью памяти, определенной в спецификации ECMA. Если _action было изменчивым, модель памяти гарантировала бы, что значение будет прочитано только один раз, и поэтому эта оптимизация не может произойти.
Однако, я думаю, что текущие реализации Microsoft CLR не оптимизируют локальные переменные.
Ответ 2
Я скажу (частично) противоположность mgronber:-) Aaaah... В конце я говорю то же самое... Только то, что я цитирую статью:-( Я дам ему +1.
Это ЮРИДИЧЕСКАЯ оптимизация по спецификациям ECMA, но это незаконная оптимизация в соответствии с спецификациями .NET >= 2.0.
Из Понять влияние методов с низким уровнем блокировки в многопоточных приложениях
Читайте здесь
Сильная модель 2:.NET Framework 2.0
Точка 2:
Чтение и запись не могут быть введены.
Объяснение ниже:
Модель не позволяет вводить чтения, потому что это подразумевает повторное считывание значения из памяти, а в памяти с низким уровнем блокировки может меняться.
НО обратите внимание, что на той же странице в разделе Техника 1: Избегание блокировок при некоторых чтениях
В системах, использующих модель ECMA, есть дополнительная тонкость. Даже если только одна ячейка памяти выбрана в локальную переменную и что local используется несколько раз, каждое использование может иметь другое значение! Это связано с тем, что модель ECMA позволяет компилятору устранить локальные переменную и повторно выбрать местоположение при каждом использовании. Если обновления происходящее одновременно, каждый выбор может иметь другое значение. Эта поведение может быть подавлено с изменчивыми декларациями, но проблема легко пропустить.
Если вы пишете в Mono, вам следует сообщить, что по крайней мере до 2008 года он работал над моделью памяти ECMA (или, как они писали в своем списке рассылки)
Ответ 3
С С# 7 вы должны написать пример следующим образом, и на самом деле среда IDE предложит его как "упрощение" для вас. Компилятор будет генерировать код, который использует временную локальную сеть, чтобы читать только местоположение _action
из основной памяти за один раз (независимо от того, что оно равно null или нет), и это помогает предотвратить общую гонку, показанную вторым примером OP, где _action
обращается дважды, и может быть установлен в значение null другим потоком между ними.
class Example
{
private Action _action;
private void InvokeAction()
{
this._action?.Invoke();
}
}