Семантика приобретения / выпуска с нестационарными хранилищами на x64

у меня есть что-то вроде:

if (f = acquire_load() == ) {
   ... use Foo
}

и:

auto f = new Foo();
release_store(f)

вы можете легко представить себе реализацию acquire_load и release_store, которая использует atomic with load(memory_order_acquire) и store(memory_order_release). Но что теперь, если release_store реализован с _mm_stream_si64, не временной записью, которая не упорядочена по отношению к другим магазинам на x64? Как получить ту же семантику?

Я думаю, что ниже минимальной требуется:

atomic<Foo*> gFoo;

Foo* acquire_load() {
    return gFoo.load(memory_order_relaxed);
}

void release_store(Foo* f) {
   _mm_stream_si64(*(Foo**)&gFoo, f);
}

и используйте его так:

// thread 1
if (f = acquire_load() == ) {
   _mm_lfence(); 
   ... use Foo
}

и:

// thread 2
auto f = new Foo();
_mm_sfence(); // ensures Foo is constructed by the time f is published to gFoo
release_store(f)

это правильно? Я почти уверен, что sfence абсолютно необходим здесь. Но насчет lfence? Требуется ли это или простого барьера компилятора будет достаточно для x64? например, ASM volatile (""::: "memory"). Согласно модели памяти x86, нагрузки не переупорядочиваются с другими нагрузками. Поэтому, насколько я понимаю, acquire_load() должен произойти до любой загрузки внутри оператора if, пока поскольку есть барьер компилятора.

1 ответов


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

Линус Торвальдс настоятельно рекомендует не пытаться изобрести свой собственный замок, потому что его так легко получить неправильно. Это больше проблема при написании портативного кода для ядра Linux, а не что-то, что только x86, поэтому я чувствую себя достаточно храбрым, чтобы попробовать чтобы разобраться в x86.


обычный способ использования NT-магазинов-сделать кучу из них подряд, например, как часть memset или memcpy, а затем SFENCE, затем обычное хранилище выпуска для общей переменной флага:done_flag.store(1, std::memory_order_release).

С помощью movnti хранить в переменной синхронизации повредит производительности. Возможно, вы захотите использовать NT-магазины в Foo он указывает на, но выселение самого указателя из кэша является извращенным. (movnt магазины выселяют строку кэша, если она была в кэше, чтобы начать с; см. vol1 содержит ч 10.4.6.2 Кэширование временных и нестационарных данных).

весь смысл NT stores предназначен для использования с нетрадиционными данными, которые не будут использоваться снова (любым потоком) в течение длительного времени, если когда-либо. Блокировки, управляющие доступом к общим буферам, или флаги, используемые производителями/потребителями для пометки данных как прочитанных, are ожидается, что будет прочитано другими сердце.

ваши имена функций также не отражают того, что вы делаете.

оборудование x86 чрезвычайно сильно оптимизировано для обычных (не NT) релиз-магазинов, потому что обычный магазин является релиз-магазин. Аппаратное обеспечение должно быть хорошим для x86, чтобы работать быстро.

использование обычных магазинов / нагрузок требует только поездки в кэш L3, а не в DRAM, для связи между потоками на процессорах Intel. Большие корпорации Intel включительно кэш L3 работает как резервная остановка для трафика Cache-coherency. Зондирование тегов L3 на пропуске из одного ядра обнаружит тот факт, что другое ядро имеет строку кэша в измененное или исключительное состояние. Магазины NT потребуют, чтобы переменные синхронизации прошли весь путь до DRAM и обратно для другого ядра, чтобы увидеть его.


заказ памяти для NT streaming stores

movnt магазины можно переупорядочить с другим магазины, но не со взрослыми читает.

корпорации Intel x86 руководство vol3, глава 8.2.2 (упорядочение памяти в семействах процессоров P6 и более поздних):

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

обновление: есть также примечание (в 8.1.2.2 Блокировка Шины С Программным Управлением), которая говорит:

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

это может быть просто предложение; они не объясняют может ли это вызвать проблему корректности. Обратите внимание, что хранилища NT не когерентны Кешу (данные могут находиться в буфере заполнения строки, даже если конфликтующие данные для той же строки присутствуют где-то еще в системе или в памяти). Возможно, вы могли бы безопасно использовать NT stores в качестве хранилища релизов, которое синхронизируется с обычными нагрузками, но столкнется с проблемами с atomic RMW ops, такими как lock add dword [mem], 1.


релиз семантика запретить переупорядочивание памяти писать-релиз с читать или писать операция, которая предшествует ей в порядке выполнения программы.

чтобы заблокировать переупорядочивание с более ранними магазинами, нам нужен SFENCE обучение в StoreStore барьер даже для магазинов NT. (И также является барьером для некоторых видов переупорядочения во время компиляции, но я не уверен, что он блокирует более ранние нагрузки от пересечения барьера.) Нормальным магазинам не нужно никакой вид инструкции барьера быть отпуск-магазинами, поэтому вам только нужно SFENCE при использовании магазинов NT.

для нагрузок: модель памяти x86 для WB (write-back, т. е. "нормальный") память уже предотвращает переупорядочивание LoadStore даже для слабо упорядоченных магазинов, поэтому нам не нужен LFENCE для своего эффект барьера LoadStore, только барьер компилятора LoadStore перед магазином NT. В реализации gcc, по крайней мере,std::atomic_signal_fence(std::memory_order_release) является барьером компилятора даже для неатомных нагрузок / магазинов, но atomic_thread_fence - это только барьер для atomic<> нагрузки / магазины (включая mo_relaxed). С помощью atomic_thread_fence все еще позволяет компилятору больше свободы переупорядочивать нагрузки / хранилища для не общих переменных. см. Этот Q&A Для больше.

// The function can't be called release_store unless it actually is one (i.e. includes all necessary barriers)
// Your original function should be called relaxed_store
void NT_release_store(const Foo* f) {
   // _mm_lfence();  // make sure all reads from the locked region are already globally visible.  Not needed: this is already guaranteed
   std::atomic_thread_fence(std::memory_order_release);  // no insns emitted on x86 (since it assumes no NT stores), but still a compiler barrier for earlier atomic<> ops
   _mm_sfence();  // make sure all writes to the locked region are already globally visible, and don't reorder with the NT store
   _mm_stream_si64((long long int*)&gFoo, (int64_t)f);
}

это сохраняет атомарную переменную (обратите внимание на отсутствие разыменования &gFoo). Ваша функция сохраняет в Foo это указывает на, что очень странно; IDK, в чем смысл этого был. Также обратите внимание, что это компилируется как допустимый код C++11.

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


чтобы выполнить acquire-load, просто скажите компилятору, что вы хотите.

x86 не требует каких-либо инструкций барьера, но указывает mo_acquire вместо mo_relaxed дает вам необходимый компилятор-барьер. Как бонус, эта функция портативна: вы получите любые и все необходимые барьеры на других архитектурах:

Foo* acquire_load() {
    return gFoo.load(std::memory_order_acquire);
}

ты ничего не говорил о сохранении gFoo в слабо-приказал WC (uncacheable запись-объединение) память. Вероятно, очень сложно организовать отображение сегмента данных вашей программы в память WC... Это было бы намного проще для gFoo просто выберите пункт память WC, после того, как вы mmap какой-то WC Video RAM или что-то еще. Но если вы хотите получить-нагрузки из памяти WC, вам, вероятно, нужно LFENCE. IDK. Задать еще один вопрос об этом, потому что этот ответ в основном предполагает, что вы используете память ВБ.

обратите внимание, что использование указателя вместо флага создает зависимость данных. Я думаю, вы должны быть в состоянии использовать gFoo.load(std::memory_order_consume), что не требует барьеров даже на слабо упорядоченных процессорах (кроме Alpha). Как только компиляторы достаточно продвинуты, чтобы убедиться, что они не нарушают зависимость данных, они могут фактически Сделайте лучший код (вместо продвижения mo_consume to mo_acquire. Прочитайте об этом перед использованием mo_consume в производственном коде, и ESP. обратите внимание, что правильное тестирование невозможно, поскольку будущие компиляторы, как ожидается, дадут более слабые гарантии, чем нынешние компиляторы на практике.


Первоначально я думал, что нам действительно нужен LFENCE, чтобы получить барьер LoadStore. ("Записи не могут проходить более ранние инструкции LFENCE, SFENCE и MFENCE". Это, в свою очередь, мешает им от прохождения (становится глобально видимым раньше) читает, что находится перед LFENCE).

обратите внимание, что LFENCE + SFENCE по-прежнему слабее, чем полный MFENCE, потому что это не StoreLoad барьер. Собственная документация SFENCE говорит, что она заказана wrt. LFENCE, но эта таблица модели памяти x86 от Intel manual vol3 не упоминает об этом. Если SFENCE не могу выполнить до после LFENCE, то sfence / lfence на самом деле может быть медленнее эквивалентно mfence, а lfence / sfence / movnti даст семантику выпуска без полного барьера. Обратите внимание, что хранилище NT может стать глобально видимым после некоторых следующих нагрузок/магазинов, в отличие от обычного строго упорядоченного хранилища x86.)


связанный: NT loads

в x86 каждая загрузка приобретает семантику, за исключением нагрузок из памяти WC. SSE4.1 MOVNTDQA является единственной инструкцией по нестационарной нагрузке, и это не слабо упорядоченный при использовании в нормальной (обратной) памяти. Так это a acquire-load тоже (при использовании в памяти WB).

обратите внимание, что movntdq только форма магазина, а movntdqa имеет только форму загрузки. Но, очевидно, Intel не могла просто назвать их storentdqa и loadntdqa. У них обоих есть требование выравнивания 16B или 32B, поэтому оставляя a не имеет большого смысла для меня. Я думаю, SSE1 и SSE2 уже представили некоторые магазины NT, уже использующие mov... мнемоника (например,movntps), но никаких нагрузок до лет позже в SSE4.1. (2-го поколения Core2: 45nm Penryn).

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

... Реализация может также использовать не временную подсказку, связанную с этой инструкцией, если источником памяти является WB (write назад) тип памяти.

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

на практике текущие процессоры Intel mainsream (Haswell, Skylake), похоже, игнорируют подсказку для загрузки PREFETCHNTA и MOVNTDQA из памяти WB. См.поддерживают ли текущие архитектуры x86 нестационарные нагрузки (из "нормальной" памяти)?, а также нестационарные нагрузки и аппаратная предварительная выборка, они работают вместе? для получения более подробной информации.


также, если вы are используя его в памяти WC (например,копирование из видеопамяти, как в этом руководстве Intel):

поскольку протокол WC использует слабо упорядоченную модель согласованности памяти, MFENCE или заблокированную инструкцию следует использовать в сочетании с инструкциями MOVNTDQA, если несколько процессоров могут ссылка на тот же WC расположения памяти или для синхронизации чтения процессора с записями другими агентами в системе.

это не понятно как его следует использовать. И я не уверен, почему они говорят "MFENCE", а не "LFENCE" для чтения. Возможно, они говорят о ситуации записи в память устройства, чтения из памяти устройства, когда магазины должны быть заказаны относительно нагрузок (барьер загрузки хранилища), а не только друг с другом (Барьер StoreStore).

я искал в Vol3 для movntdqa, и не получил никаких хитов (во всем pdf). 3 просмотров за movntdq: все обсуждение слабого упорядочения и типов памяти говорит только о магазинах. Обратите внимание, что LFENCE был представлен задолго до SSE4.1. Предположительно, это полезно для чего-то, но IDK что. Для заказа загрузки, вероятно, только с памятью WC, но я не читал, когда это было бы полезно.


LFENCE представляется более просто LoadLoad барьер для слабо упорядоченных нагрузок: слишком она заказывает другие инструкции. (Не глобальная видимость магазинов, хотя, просто их локальное выполнение).

из руководства Intel insn ref:

в частности, LFENCE не выполняется до тех пор, пока все предыдущие инструкции не будут завершены локально, а не позже- tion начинает выполнение до завершения LFENCE.
...
Инструкции после LFENCE могут быть извлечены из памяти перед LFENCE, но они не будут выполняться, пока в LFENCE завершается.

запись rdtsc предлагает использовать LFENCE;RDTSC чтобы предотвратить его выполнение перед предыдущими инструкциями, когда RDTSCP недоступен (и слабого заказ гарантия ОК: rdtscp не перестает следовать инструкциям от выполнения перед ним). (CPUID является общим предложением для сериализации потока инструкций вокруг rdtsc).