У меня проблема с компилятором 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). Затем я припарковал их в коде при соответствующем документировании существенных (не) требований переупорядочения.