терминология: загрузка не будет генерировать RFO, ей не нужно владение .Он только отправляет запрос на поделиться данными.Несколько ядер могут считывать с одного и того же физического адреса параллельно, каждое из которых имеет горячую копию в своем кэше L1d.
Другие ядра, записывающие строку, будут отправлять RFO, которые делают недействительной общую копию в нашем кэше, однакои да, это может прийти после прочтения одного или двух элементов строки кэша до того, как все будет прочитано.(Я обновил ваш вопрос описанием проблемы в этих терминах.)
SIMD-загрузка Хади - хорошая идея, чтобы собрать все данные одной инструкцией.
мы знаем, что _mm_load_si128()
на практике является атомарным для своих 8-байтовых блоков, поэтому он может безопасно заменить .load(mo_relaxed)
атомарного.Но смотрите атомарность каждого элемента векторной загрузки / хранения и сбора / разброса? - нет явной письменной гарантии этого.
Если вы использовали _mm256_loadu_si256()
, остерегайтесь настройки GCC по умолчанию -mavx256-split-unaligned-load
: Почему gcc не разрешает _mm256_loadu_pd как один vmovupd? Так что это еще одна веская причина для использования выровненной загрузки, помимо необходимости избегать разбиения строки кэша.
Но мы 'мы пишем на C, а не на asm, поэтому нам нужно позаботиться о некоторых других вещах, которые std::atomic
с mo_relaxed
делает: в частности, повторяющиеся загрузки с одного и того же адреса могут не давать одно и то же значение. Возможно, вам нужно разыменовать volatile __m256i*
, чтобы имитировать, что load(mo_relaxed)
.
Вы можете использовать atomic_thread_fence()
, если хотите более сильный порядок;Я думаю, что на практике компиляторы C ++ 11, которые поддерживают встроенные функции Intel, упорядочат изменчивые разыменования по отношению к ним.ограждения так же, как std::atomic
загружает / хранит.В ISO C ++ объекты volatile
по-прежнему подвержены гонкам данных UB, но в реальных реализациях, которые могут, например, компилировать ядро Linux, volatile
может использоваться для многопоточности.(Linux катит свою собственную атомарность с volatile
и встроенным asm, и это, я думаю, считается поддерживаемым поведением gcc / clang.) Учитывая, что на самом деле делает volatile
(объект в памяти соответствует абстрактной машине C ++), это в основном просто автоматическиработает, несмотря на любые правила, адвокат опасается, что это технически UB.Это UB, о котором компиляторы не могут знать или заботиться, потому что в этом и заключается смысл volatile
.
На практике есть веские основания полагать, что все выровненные 32-байтовые загрузки / хранилище в Haswell и более поздних версиях являются атомарными.Конечно, для чтения из L1d в неупорядоченный бэкэнд, но также и для передачи строк кэша между ядрами.(например, мульти-сокет K10 может разрываться на 8-байтовых границах с HyperTransport, так что это действительно отдельная проблема).Единственной проблемой для его использования является отсутствие какой-либо письменной гарантии или одобренного поставщиком процессора способа для обнаружения этой «функции».
Кроме этого, для портативногокод может помочь поднять auto three = something.three;
из ветки ;неправильный прогноз ветки дает ядру гораздо больше времени для аннулирования строки перед третьей загрузкой.
Но компиляторы, вероятно, не будут учитывать это изменение источника и будут загружать его только в том случае, если это необходимо.Но код без ответвлений будет всегда загружать его, поэтому, возможно, нам следует поощрять, чтобы с
bar(one, two, one == 0 ? something.three : 0);
Broadwell мог запускать 2 загрузки за такт (как все основные x86 начиная с Sandybridge и K8);Обычно мопы выполняются в порядке «самый старый-готов-первый», поэтому вполне вероятно (если этой загрузке пришлось ждать данных из другого ядра), что наши 2 загрузочных мопов будут выполняться в первом возможном цикле после поступления данных.
3-й загрузочный моп будет, вероятно, работать в цикле после этого, оставляя очень маленькое окно для аннулирования, чтобы вызвать проблему.
Или на процессорах с нагрузкой только на одну тактовую частоту, тем не менееналичие всех трех нагрузок, смежных в ассемблере, уменьшает окно для аннулирования.
Но если one == 0
встречается редко, то three
часто вообще не требуется, поэтому безусловная загрузка создает риск ненужных запросов на него. Так что вы должны учитывать этот компромисс при настройке , если вы не можете покрыть все данные одной загрузкой SIMD.
Как обсуждалось в комментариях, программная предварительная выборка может потенциально помочь скрыть некоторую задержку между ядрами.
Но вы должны выполнить предварительную выборку намного позже, чем для обычного массива, поэтому поиск мест в вашем коде, которые часто выполняются от ~ 50 до ~ 100 циклов до вызова f1()
, является сложной проблемой и может "заразить" много другого кода с деталями, не связанными с его нормальной работой. И вам нужен указатель на правую строку кэша.
Вам нужно, чтобы PF был достаточно поздним, чтобы загрузка по требованию происходила за несколько (десятки) циклов до фактического поступления предварительно выбранных данных. Это противоположно обычному сценарию использования, где L1d является буфером для предварительной выборки и хранения данных из завершенных предварительных выборок до того, как нагрузка до них доберется до них. Но вы хотите load_hit_pre.sw_pf
perf-событий (предварительная выборка попадания при загрузке), потому что это означает, что загрузка по требованию произошла, пока данные еще находились в полете, прежде чем есть вероятность того, что они будут аннулированы.
Это означает, что настройка еще более хрупкая и сложная, чем обычно, потому что вместо почти плоской точки отсчета для расстояния предварительной выборки, где раньше или позже не повреждается, более ранняя скрывает большую задержку вплоть до точки, где она допускает аннулирование, так что это склон до самого обрыва. (И любые слишком ранние предварительные выборки только усугубляют общее утверждение.)