Какова цель барьера компилятора?

Ниже приводится выдержка из Параллельное программирование на windows, Глава 10 Страница 528 ~ 529, шаблон c ++ Реализация двойной проверки

T getValue(){
if (!m_pValue){
EnterCriticalSection(&m_crst);
if (! m_pValue){
T pValue = m_pFactory();
_WriteBarrier();
m_pValue = pValue;
}
LeaveCriticalSection(&m_crst);
}
_ReadBarrier();
return m_pValue;
}

Как утверждает автор:

_WriteBarrier обнаруживается после создания экземпляра объекта, но до
запись указателя на него в поле m_pValue. Это необходимо для
убедитесь, что записи в инициализации объекта никогда не получаются
задерживается после записи в само m_pValue.

Поскольку _WriteBarrier является барьером для компиляции, я не думаю, что это полезно, если компиляция знает семантику LeaveCriticalSection. Компиляции, вероятно, пропускают запись в pValue, но никогда не оптимизируют так, чтобы перемещение присваивалось перед вызовом функции, иначе это нарушит семантику программы. Я считаю, что LeaveCriticalSection имеет неявное аппаратное ограждение. И, следовательно, любая запись до присвоения m_pValue будет синхронизирована.

С другой стороны, если компиляция не знает семантику LeaveCriticalSection, _WriteBarrier понадобится в вся платформа для предотвращения компиляции перемещения назначения из критической секции.

И для _ReadBarrier, автор сказал

Точно так же нам нужен _ReadBarrier непосредственно перед возвратом m_value, так
загружаемые после вызова getValue не переупорядочиваются
до звонка.

Первый, если эта функция включена в библиотеку, а исходный код недоступен, как компиляторы узнают, существует ли барьер компиляции или нет?

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

И автор также сказал:

Тем не менее, я также укажу, что ни один забор не требуется на X86,
Процессоры Intel64 и AMD64. К сожалению, слабые процессоры
как IA64 замутил воды

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

Пожалуйста, поправьте меня, если я ошибаюсь.

Другой вопрос, есть ли ссылки на msvc и gcc, чтобы указать, какие функции они понимают в своей семантике синхронизации?

Обновление 1:
Согласно ответу (m_pValue будет доступен из критического раздела), и запустите примеры кодов из Вот, Я думаю:

  1. Я думаю, что автор имеет в виду здесь аппаратный забор Кроме как компилировать барьер, смотрите следующую цитату из MSDN.
  2. Я считаю, что аппаратное ограждение также имеет скрытый барьер компиляции (отключить оптимизацию компиляции), но не наоборот (см. Вот,использование процессора забор не увидит никакого переупорядочения, но не наоборот)

Барьер не является забором. Следует отметить, что барьер влияет
все в кеше. Забор влияет на одну строку кэша.

Вы не должны добавлять барьеры без крайней необходимости. Использовать
забор, вы можете выбрать одну из встроенных функций _Interlocked.

Как писал автор:на X86 Intel64 и AMD64 процессоры не требуютсяmsgstr «это потому, что эти платформы позволяют переупорядочивать загрузку магазина.

Остается вопрос: понимает ли компилятор семантику вызова критической секции Enter / Leave? если этого не произойдет, тогда он может выполнить оптимизацию, как в следующем ответе, что приведет к плохому поведению.

Спасибо

3

Решение

ТЛ; др:
Заводской вызов может состоять из нескольких шагов, которые могут быть перемещены после назначения m_pValue, Выражение !m_pValue вернет false до завершения фабричного вызова, давая неполное возвращаемое значение во втором потоке.

Объяснение:

Компиляции, вероятно, пропускают запись в pValue, но никогда не оптимизируют так, чтобы перемещение присваивалось перед вызовом функции, иначе это нарушит семантику программы.

Не обязательно. Считайте, что Т int*и фабричный метод создает новый тип int и инициализирует его с 42.

int* pValue = new int(42);
m_pValue = pValue;
//m_pValue now points to anewly allocated int with value 42.

Для компилятора new Выражение будет несколько шагов, которые могут быть передвинуты перед другим. Это семантика выделения, инициализации, а затем присвоения адреса pValue:

int* pTmp = new int;
*pTmp = 42;
int* pValue = *pTmp;

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

int* pTmp = new int;
int* pValue = *pTmp;
m_pValue = pValue;
*pTmp = 42;
//m_pValue now points to a newly allocated int with value 42.

Компилятор, вероятно, сделает это, чтобы оптимизировать большинство временных указателей:

m_pValue = new int;
*m_pValue = 42;
//m_pValue now points to a newly allocated int with value 42.

Это правильная семантика для последовательный программа.

Я считаю, что LeaveCriticalSection имеет неявное аппаратное ограждение. И, следовательно, любая запись до присвоения m_pValue будет синхронизирована.

Нет. Забор идет после присваивания m_pValue, но компилятор все еще может перемещать целочисленное присваивание между ним и забором:

m_pValue = new int;
*m_pValue = 42;
LeaveCriticalSection();

И это слишком поздно, потому что Thread2 не нужно вводить CriticalSection:

Thread 1:                | Thread 2:
|
m_pValue = new int;      |
| if (!m_pValue){     //already false
| }
| return m_pValue;
| /*use *m_pValue */
*m_pValue = 42;          |
LeaveCriticalSection();  |
1

Другие решения

Джо Даффи считает, что свойства компилятора _ReadBarrier и _WriteBarrier являются забором как на уровне компилятора, так и на уровне процессора. В Параллельное программирование на windows, страница 515, он пишет

Набор встроенных функций компилятора обеспечивает как уровень компилятора, так и уровень процессора.
заборы в VC ++: _ReadWriteBarrier испускает полный забор, _ReadBarrier
испускает забор только для чтения, а _WriteBarrier испускает забор только для записи.

Автор полагается на встроенные функции компилятора _ReadBarrier и _WriteBarrier для предотвращения переупорядочения как компилятора, так и оборудования.

Документация MSDN для встроенных функций компилятора _ReadWriteBarrier не поддерживает предположение о том, что особенности компилятора влияют на уровень оборудования. Документация MSDN для Visual Studio 2010 и Visual Studio 2008 явно отрицает, что встроенные функции компилятора применимы к аппаратному уровню:

Встроенные функции компилятора _ReadBarrier, _WriteBarrier и _ReadWriteBarrier предотвращают только переупорядочение компилятора. Чтобы запретить процессору переупорядочивать операции чтения и записи, используйте макрос MemoryBarrier.

В документации MSDN для Visual Studio 2005 и Visual Studio .NET 2003 такой заметки нет. Это ничего не говорит о том, применимы ли встроенные функции к аппаратному уровню или нет.

Если _ReadBarrier и _WriteBarrier действительно не применяют аппаратные ограничения, код неверен.

Джо Даффи в своей книге использует термин забор как для аппаратных средств, так и для заборов памяти. На странице 511 он пишет:

Заборы часто называют барьерами. Похоже, что Intel предпочитает терминологию «забора», а AMD — «барьер». Я также предпочитаю «забор», вот что я использую в этой книге.

Я считаю, что аппаратный забор также имеет скрытый барьер компиляции (отключить оптимизацию компиляции)

Синхронизация и многопроцессорные проблемы Статья подтверждает, что аппаратные барьеры влияют и на компилятор:

Эти инструкции (барьеры памяти) также убедитесь, что компилятор отключает любые оптимизации, которые могут изменить порядок операций с памятью через барьеры.

Тем не менее, документация MSDN для Макрос MemoryBarrier предполагает, что переупорядочения компилятора не всегда предотвращаются:

Создает аппаратный барьер памяти (забор), который не позволяет процессору переупорядочивать операции чтения и записи. Это также может помешать компилятору переупорядочивать операции чтения и записи.

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

2

По вопросам рекламы [email protected]