Форсировать порядок выполнения операторов C? - программирование
Подтвердить что ты не робот

Форсировать порядок выполнения операторов C?

У меня проблема с компилятором MS C, переупорядочивающим определенные операторы, критические в многопоточном контексте, на высоких уровнях оптимизации. Я хочу знать, как заставить порядок в определенных местах, при этом используя высокий уровень оптимизации. (При низких уровнях оптимизации этот компилятор не меняет порядок)

Следующий код:

 ChunkT* plog2sizeChunk=...
 SET_BUSY(plog2sizeChunk->pPoolAndBusyFlag); // set "busy" bit on this chunk of storage
 x = plog2sizeChunk->pNext;

производит следующее:

 0040130F 8B 5A 08 mov ebx,dword ptr [edx+8]
 00401312 83 22 FE and dword ptr [edx],0FFFFFFFEh 

в котором запись в pPoolAndBusyFlag переупорядочена компилятором, чтобы произойти после выборки pNext.

SET_BUSY по существу

  plog2sizeChunk->pPoolAndBusyFlag&=0xFFFFFFFeh;

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

typedef struct chunk_tag{
unsigned pPoolAndBusyFlag;      // Contains pointer to owning pool and a busy flag
natural log2size;                   // holds log2size of the chunk if Busy==false
struct chunk_tag* pNext;            // holds pointer to next block of same size
struct chunk_tag* pPrev;            // holds pointer to previous block of same size
} ChunkT, *pChunkT;

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

В идеале я бы написал что-то вроде:

 plog2sizeChunk->pPoolAndBusyFlag&=0xFFFFFFFeh;
 #pragma no-reordering // no such directive appears to exist
 pNext = plog2sizeChunk->pNext;

Я экспериментально подтвердил, что могу получить этот эффект этим уродливым способом:

 plog2sizeChunk->pPoolAndBusyFlag&=0xFFFFFFFeh;
 asm {  xor eax, eax }  // compiler won't optimize past asm block
 pNext = plog2sizeChunk->pNext;

дает

 0040130F 83 22 FE             and         dword ptr [edx],0FFFFFFFEh  
 00401312 33 C0                xor         eax,eax  
 00401314 8B 5A 08             mov         ebx,dword ptr [edx+8]  

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

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

Итак, как заставить компилятор их упорядочить?

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

EDIT (20 октября): Спасибо всем респондентам. В моей текущей реализации используется volatile (используется как начальное решение), _ReadWriteBarrier (чтобы отметить код, в котором переупорядочение не должно выполняться компилятором) и несколько MemoryBarriers (где происходят чтения и записи), и это, похоже, решило проблему.

EDIT: (2 ноября). Чтобы быть чистым, я определил набор макросов для ReadBarrier, WriteBarrier и ReadWriteBarrier. Существуют комплекты для предварительной и пост-блокировки, до и после разблокировки и общего использования. Некоторые из них являются пустыми, некоторые содержат _ReadWriteBarrier и MemoryBarrier, как это подходит для x86 и типичных блокировок на основе XCHG [XCHG включает неявный MemoryBarrier, таким образом устраняя эту необходимость в блокировке pre-/post-sets). Затем я припарковал их в коде при соответствующем документировании существенных (не) требований переупорядочения.

4b9b3361

Ответ 1

Итак, я понимаю, что pNext = plog2sizeChunk->pNext публикует блок, чтобы его могли видеть другие потоки, и вы должны убедиться, что он видит правильный флаг занятости.

Это означает, что вам нужно однонаправленный барьер памяти перед его публикацией (также один перед чтением в другом потоке, хотя, если ваш код работает на x86, вы получаете их бесплатно), чтобы убедиться, что потоки действительно видят изменение. Перед записью вам также понадобится один, чтобы избежать переупорядочения записей после него. Нет просто вставки сборки или использования стандартного совместимого энергозависимого (MSVC volatile дает дополнительные гарантии, хотя это имеет значение здесь) достаточно не - да, это останавливает компилятор от переключения чтения и записи, но CPU не связанный им и может делать то же переупорядочение внутри страны.

Оба MSVC и gcc имеют встроенные/макросы для создания барьеров памяти (см. здесь здесь). MSVC также дает более сильные гарантии для летучих, которые достаточно хороши для вашей проблемы. Наконец, атомика С++ 11 будет работать, но я не уверен, что у C есть какой-либо переносной способ гарантировать барьеры памяти.

Ответ 2

См. _ ReadWriteBarrier. Это встроенный компилятор, посвященный тому, что вы ищете. Обязательно проверьте документацию на точной версии MSVC ( "устаревший" на VS2012...). Остерегайтесь переупорядочения процессора (см. MemoryBarrier

В документации говорится о том, что встроенные компиляторы _ReadBarrier, _WriteBarrier и _ReadWriteBarrier (переупорядочение компилятора) и что макрос MemoryBarrier (переупорядочение процессора) все "устарел" , начиная с VS2012. Но я думаю, что они продолжат отлично работать в течение некоторого времени...

Новый код может использовать новые средства С++ 11 (ссылки на странице MSDN)