Приобретать/выпускать семантику с невременными магазинами на x64

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

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

и

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

Вы легко можете представить себе реализацию gets_load и release_store, которая использует атомарную нагрузку (memory_order_acquire) и сохраняет (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)

Это правильно? Я почти уверен, что это абсолютно необходимо здесь. Но как насчет силы? Требуется ли или достаточно простого компилятора для x64? например asm volatile ( ":" memory"). Согласно модели памяти x86, нагрузки не переупорядочиваются с другими нагрузками. Поэтому, насколько мне известно, функция load_load() должна выполняться перед любой загрузкой внутри оператора if, если существует компилятор.

Ответ 1

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

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


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

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

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

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

Аппаратное обеспечение

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

Использование обычных хранилищ/нагрузок требует только отключения к кэшу L3, а не DRAM, для связи между потоками на процессорах Intel. Intel большой инклюзивный L3-кеш работает как блокиратор для кеш-когерентного трафика. Исследование тегов L3 на промах из одного ядра определит факт, что другое ядро ​​имеет строку кэша в Измененное или Исключительное состояние. Для хранилищ NT потребуются переменные синхронизации, чтобы пройти весь путь до DRAM и вернуться к другому ядру, чтобы увидеть его.


Заказ памяти для хранилищ NT для потоковой передачи

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

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

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

update: Также есть примечание (в разделе 8.1.2.2 Блокировка с программным управлением):

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

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


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

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

Для нагрузок: модель памяти x86 для память WB (обратная запись, т.е. "нормальная" ) уже предотвращает переупорядочение 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, в чем дело. Также обратите внимание, что компилируется как действительный код С++ 11.

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


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

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

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

Вы ничего не сказали о сохранении gFoo в слабо упорядоченной памяти WC (uncacheable write-combining). Вероятно, очень сложно организовать, чтобы сегмент вашей программы был отображен в память WC... Было бы намного проще gFoo просто указать на память WC после того, как вы mmap какой-либо видеопамяти WC или что-то в этом роде. Но если вы хотите получить нагрузки из памяти WC, вам, вероятно, понадобится LFENCE. ИДК. Задайте еще один вопрос, потому что этот ответ в основном предполагает, что вы используете память WB.

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


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

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


Связано: NT загружает

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

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

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

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

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

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


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

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

Это не говорит о том, как его использовать. И я не уверен, почему они говорят MFENCE, а не LFENCE для чтения. Возможно, они говорят о ситуации с записью на устройство-память, в режиме чтения-от-устройства, где магазины должны заказываться относительно нагрузок (барьер StoreLoad) не только друг с другом (бар StoreStore).

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


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

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

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

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