Сохранение порядка memcpy в C ++ - PullRequest
0 голосов
/ 27 августа 2018

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

В частности, предположим, что у меня есть некоторый вектор X объектов размером с строку кэша: X [0], ... X [K] каждый занимает ровно одну строку кэша. Я пишу им в порядке индекса: сначала X [0], затем X [1] и т. Д. Если поток 2 читает X [K], он также увидит состояние для X [0], которое «по крайней мере, как текущее» как то, что он видит для X [K]?

Из этого же потока, очевидно, я увижу семантику памяти, которая соответствует порядку обновления. Но теперь, если какой-то второй поток читает X [K], возникает вопрос: будут ли наблюдаться соответствующие обновления для X [0] ... X [K-1]?

С блокировкой мы получаем эту гарантию. Но с помощью memcpy, используемой для копирования чего-либо в вектор, мы теряем это свойство: у memcpy есть семантика POSIX, которая не гарантирует обновления порядка индекса или обновления памяти или какого-либо другого порядка вообще. Вам просто гарантируется, что после завершения работы memcpy все обновление будет выполнено.

Мой вопрос: существует ли сохраняющий заказ memcpy с такой же скоростью, но с желаемой гарантией? А если нет, то можно ли реализовать такой примитив без блокировки?

Предположим, мои целевые платформы - x86 и ARM.

(Примечание редактора: изначально говорилось, что Intel, поэтому оператору не нужно заботиться о AMD.)

1 Ответ

0 голосов
/ 28 августа 2018

Требования к оформлению заказа, которые вы описываете, в точности соответствуют семантике выпуска / приобретения. (http://preshing.com/20120913/acquire-and-release-semantics/).

Проблема в том, что единица атомности для эффективных гарантированных атомарных загрузок / хранилищ составляет не более 8 байтов на всех x86 и некоторых ARM. В противном случае только 4 байта в других ARM. ( Почему целочисленное присваивание для естественно выровненной переменной атомарно в x86? ). Некоторые процессоры Intel, вероятно, на практике имеют атомарные 32 или даже 64-байтовые хранилища (AVX512), но ни Intel, ни AMD никогда не делали официальных гарантий.

Мы даже не знаем, имеют ли векторные хранилища SIMD гарантированный порядок, когда они потенциально разбивают широко выровненное хранилище на несколько выровненных по 8 байтов фрагментов. Или даже если эти куски индивидуально атомарны. атомарность каждого элемента векторной загрузки / хранения и сбора / разброса? Есть все основания полагать, что они являются атомарными для каждого элемента, даже если документация не гарантирует этого.

Если наличие больших «объектов» критично для производительности, вы можете рассмотреть возможность проверки атомарности векторной загрузки / хранения на конкретном сервере, который вам небезразличен, но вы полностью уверены в том, что можете гарантировать компилятору использовать его. , (Существуют встроенные функции.) Убедитесь, что вы тестируете между ядрами на разных сокетах, чтобы отследить случаи, подобные инструкциям SSE: какие процессоры могут выполнять атомные операции с памятью 16B? разрыв на 8-байтовых границах из-за HyperTransport между сокетами на К10 Оптерон. Это, вероятно, действительно плохая идея; Вы не можете догадаться, что, если какие-либо микроархитектурные условия могут сделать широкий вектор-хранилище неатомарным в редких случаях, даже если он обычно выглядит как атомарный.


Вы можете легко получить порядок деблокирования / получения для таких элементов массива, как
alignas(64) atomic<uint64_t> arr[1024];.
Вы просто должны спросить компилятора:

copy_to_atomic(std::atomic<uint64_t> *__restrict dst_a, 
                      const uint64_t *__restrict src, size_t len) {
    const uint64_t *endsrc = src+len;
    while (src < src+len) {
        dst_a->store( *src, std::memory_order_release );
        dst_a++; src++;
    }
}

На x86-64 он не выполняет автоматическую векторизацию или что-либо еще, потому что компиляторы не оптимизируют атомику, и потому что нет документации, что безопасно использовать векторы для хранения последовательных элементов массива атомарных элементов. :( Так что это в основном отстой. Посмотрите на это в проводнике компилятора Godbolt

Я бы посоветовал свести свои собственные с volatile __m256i* указателями (выровненная загрузка / сохранение) и барьерами компилятора, такими как atomic_thread_fence(std::memory_order_release), чтобы предотвратить переупорядочение во время компиляции. Порядок элементов / атомарность должны быть в порядке (но опять же не гарантируется). И определенно не рассчитывайте, что целые 32 байта являются атомарными, просто более высокие uint64_t элементы записываются после более низких uint64_t элементов (и эти хранилища становятся видимыми для других ядер в этом порядке).


На ARM32 : даже атомное хранилище uint64_t не велико. gcc использует пару ldrexd / strexd (LL / SC), потому что, очевидно, нет 8-байтового хранилища атомарной чистоты. (Я скомпилировал с помощью gcc7.2 -O3 -march = armv7-a. С armv8-a в режиме AArch32, store-pair является атомарным. AArch64 также имеет атомную 8-байтовую загрузку / сохранение, конечно.)


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

movnt Обход кэш-памяти в векторном цикле или rep movsb в ЦП с функцией ERMSB могут создать этот эффект. Делает ли модель памяти Intel избыточность SFENCE и LFENCE? .

Или реализация memcpy могла бы просто сделать выбор в пользу последнего (частичного) вектора перед входом в его основной цикл.

Параллельная запись + чтение или запись + запись на не atomic типах в UB на C и C ++; вот почему memcpy имеет такую ​​большую свободу делать все, что захочет, в том числе использовать слабо упорядоченные хранилища, если он использует sfence, если необходимо, чтобы убедиться, что memcpy в целом соблюдает порядок, ожидаемый компилятором при его выдаче код для последующих mo_release операций.

(то есть текущие реализации C ++ для x86 делают std::atomic с допущением, что у них нет слабо упорядоченных хранилищ, о которых они могли бы беспокоиться. Любой код, который хочет, чтобы их хранилища NT уважали порядок сгенерированного компилятором кода atomic<T> необходимо использовать _mm_sfence(). Или, если вы пишете asm вручную, инструкцию sfence напрямую. Или просто использовать xchg, если вы хотите создать хранилище с последовательным выпуском и дать вашей функции asm эффект atomic_thread_fence(mo_seq_cst). .)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...