Переупорядочивание во время компиляции по-прежнему может укусить вас при ориентации на x86, потому что компилятор оптимизирует, чтобы сохранить поведение программы на абстрактной машине C++, а не более сильное поведение, зависящее от архитектуры. Поскольку мы хотим избежать memory_order_seq_cst
, изменение порядка разрешено.
Да, ваши магазины могут изменить порядок, как вы предлагаете. Ваши загрузки также могут быть переупорядочены с загрузкой t2
, поскольку загрузка-приобретение — это только одна барьер. Для компилятора было бы законно полностью оптимизировать проверку t2. Если переупорядочение возможно, компилятору разрешается решить, что это происходит всегда, и применить правило «как если бы», чтобы сделать код более эффективным. (Текущие компиляторы обычно этого не делают, но это определенно разрешено текущим стандартом, как написано. См. заключение обсуждения по этому поводу со ссылками на предложения по стандартам.)
Варианты предотвращения повторного заказа:
- Сделайте все хранилища/загрузки элементов данных атомарными с выпуском и приобретением семантики. (Загрузка последнего члена данных предотвратит загрузку
t2
первой.)
Используйте барьеры (они же заборы), чтобы упорядочить все атомарные хранилища и неатомарные загрузки как группа.
Как объясняет Джефф Прешинг, a mo_release
забор — это не то же самое, что mo_release
магазин, и нам нужен двунаправленный барьер. std::atomic просто перерабатывает имена std::mo_ вместо того, чтобы давать разные имена для заборов.
(Кстати, неатомарные сохранения/загрузки действительно должны быть атомарными с mo_relaxed
, потому что технически Undefined Behavior читать их вообще, пока они могут быть в процессе перезаписи, даже если вы решите не смотреть на то, что вы читаете. )
void produce(int64_t n, ...) // ... for above data members
{
/*********** changed lines ************/
std::atomic_signal_fence(std::memory_order_release); // compiler-barrier to make sure the compiler does the seq store as late as possible (to give the reader more time with it valid).
s.seq.store(n-1, std::memory_order_relaxed); // changed from release
std::atomic_thread_fence(std::memory_order_release); // StoreStore barrier prevents reordering of the above store with any below stores. (It's also a LoadStore barrier)
/*********** end of changes ***********/
// assign data members of s
...
// release semantics prevent any preceding stores from being delayed past here
s.seq.store(n, std::memory_order_release); // complete updating
}
bool consume(int64_t n, ...) // ... for interested fields passed as reference
{
if (n == s.seq.load(std::memory_order_acquire))
{
// acquire semantics prevent any reordering with following loads
// read fields
...
/*********** changed lines ************/
std::atomic_thread_fence(std::memory_order_acquire); // LoadLoad barrier (and LoadStore)
auto t2 = s.seq.load(std::memory_order_relaxed); // relaxed: it's ordered by the fence and doesn't need anything extra
// std::atomic_signal_fence(std::memory_order_acquire); // compiler barrier: probably not useful on the load side.
/*********** end of changes ***********/
if (n == t2)
return true;
}
return false;
}
Обратите внимание на дополнительный барьер компилятора (signal_fence влияет только на переупорядочение во время компиляции), чтобы гарантировать, что компилятор не объединит второе хранилище из одной итерации с первым хранилищем из следующей итерации, если это выполняется в цикле. Или, в более общем смысле, чтобы убедиться, что хранилище, которое делает регион недействительным, выполняется как можно позже, чтобы уменьшить количество ложных срабатываний. (Возможно, в настоящих компиляторах нет необходимости, и между вызовами этой функции много кода. Но signal_fence никогда не компилируется в какие-либо инструкции и кажется лучшим выбором, чем сохранение первого хранилища как mo_release
. В архитектурах, где выпуск-сохранение потока -fence обе компилируются в дополнительные инструкции, расслабленное хранилище позволяет избежать двух отдельных барьерных инструкций.)
Меня также беспокоила возможность переупорядочивания первого магазина с релиз-магазином из предыдущей итерации. Но я не думаю, что это может когда-либо случиться, потому что оба магазина находятся по одному и тому же адресу. (Возможно, во время компиляции стандарт позволяет делать это враждебному компилятору, но вместо этого любой здравомыслящий компилятор просто не делал бы одно из хранилищ, если бы думал, что одно из них может передать другое.) Во время выполнения на слабо- упорядоченная архитектура, я не уверен, что магазины по одному и тому же адресу могут когда-либо стать глобально видимыми не по порядку. В реальной жизни это не должно быть проблемой, так как производитель, по-видимому, не вызывается подряд.
Кстати, вы используете метод синхронизации Seqlock, но только с одним писателем. У вас есть только часть последовательности, а не часть блокировки для синхронизации отдельных модулей записи. В версии с несколькими записями писатели брали бы блокировку перед чтением/записью порядковых номеров и данных. (И вместо того, чтобы использовать seq no в качестве аргумента функции, вы бы прочитали его из блокировки).
Документ для обсуждения стандартов C++ N4455 (об оптимизации атомов компилятором см. вторую половину моего ответа на Может ли num++ быть атомарным для 'int num'?) использует его в качестве примера.
Вместо ограждения StoreStore они используют хранилища выпусков для элементов данных в модуле записи. (С атомарными элементами данных, как я уже упоминал, требуется, чтобы это было действительно правильно).
void writer(T d1, T d2) {
unsigned seq0 = seq.load(std::memory_order_relaxed); // note that they read the current value because it's presumably a multiple-writers implementation.
seq.store(seq0 + 1, std::memory_order_relaxed);
data1.store(d1, std::memory_order_release);
data2.store(d2, std::memory_order_release);
seq.store(seq0 + 2, std::memory_order_release);
}
Они говорят о том, чтобы позволить второй загрузке считывателем порядкового номера потенциально переупорядочиваться с более поздними операциями, если компилятору это выгодно, и использовать t2 = seq.fetch_add(0, std::memory_order_release)
в считывателе как потенциальный способ получить загрузку с семантикой выпуска. С текущими компиляторами я бы не рекомендовал это; вы, вероятно, получите операцию lock
ed на x86, где способ, который я предложил выше, не имеет каких-либо (или каких-либо фактических барьерных инструкций, потому что только полные ограждения seq_cst нуждаются в инструкции на x86).
person
Peter Cordes
schedule
10.10.2016