Инструкция

У меня есть вопрос о переупорядочивании юридических инструкций в C#/.NET.

Начнем с этого примера. У нас есть этот метод, определенный в каком-то классе, где _a, _b и _c — поля.

int _a;
int _b;
int _c;
void Foo()
{
   _a = 1; // Write 1
   _b = 1; // Write 2
   _c = 1; // Write 3
}

И наша среда вызова будет выглядеть примерно так.

//memory operations
ClassInstance.Foo();
//memory operations

Мне интересно, какие допустимые переупорядочивания инструкций возможны, когда этот вызов метода становится встроенным по сравнению с вызовом функции. В частности, мне интересно, допустимо ли и когда переупорядочивать операции с памятью в Foo(), с операциями с памятью вне его (из нашего предыдущего примера, //операций с памятью).

Кроме того, вызов функции (не встроенный) в некотором смысле «создает барьеры памяти». Например, операции с памятью, которые происходят до или после вызова функции, не могут быть переупорядочены с операциями с памятью внутри вызова функции.

Если да, будет ли он по-прежнему иметь это поведение «барьера памяти», когда он будет встроен компилятором?


person user2738477    schedule 15.03.2014    source источник
comment
Вы спрашиваете, какие известные ошибки есть в компиляторе, которые могут привести вас сюда?   -  person johnb003    schedule 15.03.2014
comment
Может быть, это только я, но мне кажется безумием, что вы так обеспокоены низкоуровневой оптимизацией памяти в среде управляемой памяти.   -  person johnb003    schedule 15.03.2014
comment
@johnb003. Нет, не ошибки компилятора. Компилятор выполняет множество оптимизаций во время компиляции и во время выполнения, таких как встраивание вызовов функций, переупорядочение операций с памятью, подъем операций чтения из циклов и т. д. Меня также весьма интересует эта тема. Изучение концепций в управляемой среде по-прежнему будет перенесено, когда я перейду на C++, мне просто нужно ознакомиться с новыми моделями памяти.   -  person user2738477    schedule 15.03.2014
comment
и поэтому не должен вызывать побочных эффектов в таком простом коде, если нет серьезной ошибки.   -  person johnb003    schedule 15.03.2014
comment
Что ж, в однопоточном сценарии этого не произойдет, но как только вы окажетесь в многопоточном сценарии, переупорядочение инструкций может или не может изменить поведение.   -  person user2738477    schedule 15.03.2014
comment
@ user2738477 Нет, не будет. Барьеры памяти возникают в .NET только в четко определенных местах, либо неявно (например, оператор блокировки), либо явно (на самом деле существует класс барьера памяти). Они нужны вам, вы их вставили.   -  person TomTom    schedule 15.03.2014


Ответы (2)


Ответить на этот вопрос поможет Спецификация языка C#. В разделе «Приказ об исполнении» говорится следующее:

3.10 Порядок выполнения

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

  • Зависимость данных сохраняется в потоке выполнения. То есть значение каждой переменной вычисляется так, как если бы все операторы в потоке выполнялись в исходном порядке программы.

  • Правила упорядочения инициализации сохраняются (§10.5.4 и §10.5.5).

  • Порядок побочных эффектов сохраняется в отношении изменчивых операций чтения и записи (§10.5.3).

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

По сути, правила можно рассматривать как дрожание, которое может изменить порядок выполнения, пока исполняющий поток не заметит разницу. Но другие потоки могут заметить различия. В запись Эрика Липперта в блоге Coverity, он говорит:

... ЦП может в качестве оптимизации выбрать [изменить порядок выполнения] при условии, что это не может быть обнаружено текущим потоком. Но этот факт можно было наблюдать в другой теме...

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

person Stephen Jennings    schedule 15.03.2014
comment
Спасибо за крик. Вслед за этим постом в блоге я опубликую статью о том, как изменение порядка виртуальных операций чтения и записи может испортить программные инварианты примерно через две недели. - person Eric Lippert; 16.03.2014
comment
Ссылка на пост Эрика Липперта не работает. Похоже, это здесь (ericlippert.com/2014/03/12/), но ссылки на этой странице тоже кажутся неработающими. Что-то снято с производства? - person bornfromanegg; 03.07.2017
comment
@bornfromanegg Coverity была куплена Synopsys, и, похоже, они не поддерживали работу блога. Очень жаль, было много хорошей информации. Я обновил ссылку, чтобы указать на копию Интернет-архива. - person Stephen Jennings; 06.07.2017

При обсуждении переупорядочения инструкций имейте в виду, что обычно в игре участвуют два (или более) потока. Предложение «Порядок выполнения» в спецификации — это, по сути, формальное определение интуитивной идеи о том, что поток должен воспринимать побочные эффекты в том же порядке, что и указанный программистом. Без него приложения имели бы недетерминированное поведение.

Настоящая суть темы заключается в том, как другие потоки воспринимают побочные эффекты. Вот тут-то и вступает в игру третий пункт о непостоянном чтении и записи. Так уж получилось, что все операции записи (во всех известных мне версиях .NET Framework) имеют семантику освобождения от ограничений.

Мне нравится использовать стрелочную нотацию, чтобы помочь визуализировать ограничения, наложенные на оптимизацию переупорядочения команд. Я использую стрелку ↑ для обозначения ограждения освобождения и стрелку ↓ для обозначения ограждения захвата. Ничто не может проплыть вниз за стрелку ↑ или вверх за стрелку ↓. Представьте, что наконечник стрелы отталкивает все. Используя эту нотацию со стрелкой и предполагая, что записи по-прежнему имеют семантику освобождения, ваш код будет выглядеть следующим образом.

void Foo()
{
   ↑
   _a = 1; // Write 1
   ↑
   _b = 1; // Write 2
   ↑
   _c = 1; // Write 3
}

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

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

person Brian Gideon    schedule 29.05.2014
comment
Не могли бы вы предоставить некоторые подробности/источники. Так получилось, что все операции записи (во всех известных мне версиях .NET Framework) имеют семантику релиза? Это мощное заявление для смеси архитектур. - person Sergey.quixoticaxis.Ivanov; 02.02.2017
comment
@Sergey.quixoticaxis.Ivanov Раздел 3.10 Порядок выполнения в Спецификация языка C# Версия 5.0 - Выполнение программы C# происходит таким образом, что побочные эффекты каждого выполняемого потока сохраняются в критических точках выполнения. Побочный эффект определяется как чтение или запись изменяемого поля, запись в энергонезависимую переменную, запись во внешний ресурс и создание исключения., акцент мой. - person laika; 30.04.2018