Разрешенная оптимизация компилятора С# для локальных переменных и выборка из памяти

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();
    }
}