Оптимизация компилятора.NET JIT

https://msdn.microsoft.com/en-us/magazine/jj883956.aspx

Рассмотрим шаблон циклы опроса:

private bool _flag = true; 
public void Run() 
{
    // Set _flag to false on another thread
    new Thread(() => { _flag = false; }).Start();
    // Poll the _flag field until it is set to false
    while (_flag) ;
    // The loop might never terminate! 
} 

В этом случае компилятор.NET 4.5 JIT может переписать цикл следующим образом:

if (_flag) { while (true); } 

В однопоточном случае это преобразование полностью легально и, в общем, подъем считывания из цикла является отличной оптимизацией. Однако, если для параметра _flag установлено значение false в другом потоке, оптимизация может привести к зависанию.

Обратите внимание, что если поле _flag было изменчивым, компилятор JIT не мог бы вытащить считывание из цикла. (См. Раздел "Опрос циклы" в декабрьской статье для более подробного объяснения этого шаблона.)

Будет ли компилятор JIT по-прежнему оптимизировать код, как показано выше, если я заблокирую _flag или просто сделаю его volatile остановив оптимизацию?

Эрик Липперт должен сказать об изменчивости:

Честно говоря, я отговариваю вас от когда-либо создающего неспокойное поле. Неустойчивые поля являются признаком того, что вы делаете что-то совершенно безумное: вы пытаетесь читать и писать одно и то же значение на двух разных потоках, не помещая блокировку на место. Замки гарантируют, что память, считываемая или измененная внутри замка, будет последовательной, блокировки гарантируют, что только один поток обращается к заданному блоку памяти за раз и так далее. Количество ситуаций, в которых блокировка слишком медленная, очень мала, и вероятность того, что вы собираетесь получить код неправильно, потому что вы не понимаете, что точная модель памяти очень велика. Я не пытаюсь написать код с низким уровнем блокировки, за исключением самых простых операций блокировки. Я оставляю использование "изменчивых" для реальных экспертов.

Подводя итог: Кто гарантирует, что упомянутая выше оптимизация не разрушит мой код? Только volatile? Также оператор lock? Или что-то другое?

Как Эрик Липперт отговаривает вас от использования volatile должно быть что-то еще?


Downvoters: Я ценю каждую обратную связь на вопрос. Особенно, если вы отказались от этого, я хотел бы услышать, почему вы думаете, что это плохой вопрос.


Переменная bool не является примитивом синхронизации потоков: вопрос подразумевается как общий вопрос. Когда компилятор не сделает оптимизацию?


Dupilcate: Этот вопрос явно посвящен оптимизации. В том, что вы связали, не упоминается оптимизация.

Ответы

Ответ 1

Позвольте ответить на вопрос, который был задан:

Будет ли компилятор JIT по-прежнему оптимизировать код, как показано выше, если я заблокирую _flag или просто сделаю его неустойчивым, остановив оптимизацию?

Хорошо, не отвечайте на заданный вопрос, потому что этот вопрос слишком сложный. Позвольте разбить его на ряд менее сложных вопросов.

Будет ли компилятор JIT по-прежнему оптимизировать код, как показано выше, если я заблокирую _flag?

Короткий ответ: lock дает более сильную гарантию, чем volatile, поэтому нет, дрожание не будет разрешено поднимать считывание из цикла, если есть блокировка вокруг считывания _flag. Конечно, замок также должен быть вокруг записи. Замки работают, только если вы используете их повсюду.

private bool _flag = true; 
private object _flagLock = new object();
public void Run() 
{
  new Thread(() => { lock(_flaglock) _flag = false; }).Start();
  while (true)
    lock (_flaglock)
      if (!_flag)
        break;
} 

(И, конечно, я отмечаю, что это безумно плохой способ подождать, пока один поток не будет сигнализировать другим. Никогда не сидите в жесткой петле, опросив флаг! Используйте ручку wait, как разумный человек.)

Вы сказали, что замки сильнее, чем летучие; что это значит?

Считывание в летучие мыши предотвращает перемещение некоторых операций во времени. Записывает летучие мыши, чтобы предотвратить перемещение некоторых операций во времени. Замки предотвращают перемещение большего количества операций во времени. Эти профилактические семантики называются "заборами памяти" - в основном, летучие элементы вводят половину забора, замки вводят полный забор.

Прочтите раздел спецификации С# о специальных побочных эффектах для деталей.

Как всегда, напомню, что летучие вещества не дают вам гарантии на свежесть в мире. В многопоточном программировании на С# такого типа нет, как "последнее" значение переменной, и поэтому волатильные чтения не дают вам "последнего" значения, потому что его не существует. Идея о том, что существует "последнее" значение, подразумевает, что в чтениях и письмах всегда наблюдается глобально согласованный порядок во времени, и это неверно. Темы могут по-прежнему не совпадать с порядком неустойчивых чтений и записей.

Блокировки предотвращают эту оптимизацию; летучие вещества предотвращают эту оптимизацию. Это единственное, что препятствует оптимизации?

Нет. Вы также можете использовать операции с Interlocked или вы можете явно ввести Interlocked памяти.

Понимаю ли я это, чтобы правильно использовать volatile?

Нету.

Что я должен делать?

Не пишите многопоточные программы в первую очередь. Несколько потоков управления в одной программе - плохая идея.

Если вам нужно, не используйте память по потокам. Используйте потоки как низкозатратные процессы и используйте их только тогда, когда у вас есть незанятый процессор, который может выполнять задачу с интенсивным использованием ЦП. Используйте однопоточную асинхронность для всех операций ввода-вывода.

Если вам необходимо обмениваться памятью по потокам, используйте доступную вам конструкцию программирования самого высокого уровня, а не самый низкий уровень. Используйте CancellationToken для представления операции, отмененной в другом месте асинхронного рабочего процесса.

Ответ 2

Когда вы используете lock или блокированные операции, вы сообщаете компилятору, что блок или ячейка памяти имеет расы данных и что он не может делать предположения о доступе к этим местоположениям. Таким образом, компилятор отступает от оптимизаций, которые он мог бы выполнять в среде, свободной от данных. Этот подразумеваемый контракт также означает, что вы сообщаете компилятору, что вы получите доступ к этим местоположениям с помощью соответствующего метода без гонок.

Ответ 3

Этот вопрос явно посвящен оптимизации

Это не правильная точка зрения. Важно то, как язык указывается для поведения. JIT будет только оптимизироваться под ограничением, не нарушая спецификации. Поэтому оптимизация является незаметной для программы. Проблема с кодом в этом вопросе заключается не в том, что он оптимизирован. Проблема в том, что ничто в спецификации не заставляет программу быть правильной. Чтобы исправить это, вы не отключите оптимизацию или каким-то образом общаетесь с компилятором. Вы используете примитивы, которые гарантируют поведение, которое вам нужно.


Вы не можете заблокировать _flag. lock - синтаксис для класса Monitor. Этот класс блокируется на основе объекта в куче. _flag - это bool, который не блокируется.

Чтобы отменить цикл, я использовал бы CancellationTokenSource для этих дней. Он использует изменчивые обращения внутри, но скрывает это от вас. Цикл опроса CancellationToken и отмены цикла выполняется путем вызова CancellationTokenSource.Cancel(). Это очень самодокументируемо и легко реализуется.

Вы также можете обернуть любой доступ к _flag в блокировке. Это будет выглядеть так:

object lockObj = new object(); //need any heap object to lock
...

while (true) {
 lock (lockObj) {
  if (_flag) break;
 }
 ...
}

...

lock (lockObj) _flag = true;

Вы также можете использовать volatile. Эрик Липперт совершенно прав, поэтому лучше не касаться жестких ядер, если вам это не нужно.

Ответ 4

Ключевое слово С# volatile реализует так называемую семантику получения и выпуска, поэтому вполне законно использовать ее для простой синхронизации потоков. И любой стандартный совместимый JIT-движок не должен его оптимизировать.

Хотя, конечно, это паразитная языковая функция, поскольку у C/C++ есть другая семантика, и это то, к чему большинство программистов уже привыкли. Таким образом, специфическая для С# и специфичная для Windows (за исключением архитектуры ARM) использование "volatile" иногда путается.