Аппаратный барьер памяти ускоряет видимость операций atomi c в дополнение к предоставлению необходимых гарантий? - PullRequest
2 голосов
/ 04 мая 2020

TL; DR: имеет ли смысл когда-либо ставить ненужный (с точки зрения модели памяти C ++) забор памяти или неоправданно сильный порядок памяти, чтобы иметь большую задержку за счет возможной худшей пропускной способности?


Модель памяти C ++ выполняется на аппаратном уровне, имея какие-то ограждения памяти для более сильных порядков памяти и не имея их на более слабых порядках памяти.

В частности, если производитель делает store(memory_order_release) и потребитель наблюдает за сохраненным значением с помощью load(memory_order_acquire), между загрузкой и хранением нет ограждений. На x86 нет заборов вообще, на ARM заборы ставятся перед хранением и после загрузки.

Значение, сохраненное без ограждения, в конечном итоге будет наблюдаться при загрузке без ограждения (возможно, после нескольких неудачных попыток)

Мне интересно, может ли установка ограждения с обеих сторон очереди ускорить наблюдение за значением? Какова задержка с забором и без него, если так?

Я ожидаю, что наилучшим вариантом будет просто иметь oop с load(memory_order_acquire) и pause / yield, ограниченным тысячами итераций, поскольку он используется везде, но я хочу понять, почему.

Поскольку этот вопрос касается поведения оборудования, я ожидаю, что нет универсального c ответа. Если так, то меня интересует в основном x86 (разновидность x64) и второе - ARM.


Пример:

T queue[MAX_SIZE]

std::atomic<std::size_t>   shared_producer_index;

void producer()
{
   std::size_t private_producer_index = 0;

   for(;;)
   {
       private_producer_index++;  // Handling rollover and queue full omitted

       /* fill data */;

      shared_producer_index.store(
          private_producer_index, std::memory_order_release);
      // Maybe barrier here or stronger order above?
   }
}


void consumer()
{
   std::size_t private_consumer_index = 0;

   for(;;)
   {
       std::size_t observed_producer_index = shared_producer_index.load(
          std::memory_order_acquire);

       while (private_consumer_index == observed_producer_index)
       {
           // Maybe barrier here or stronger order below?
          _mm_pause();
          observed_producer_index= shared_producer_index.load(
             std::memory_order_acquire);
          // Switching from busy wait to kernel wait after some iterations omitted
       }

       /* consume as much data as index difference specifies */;

       private_consumer_index = observed_producer_index;
   }
}

1 Ответ

5 голосов
/ 04 мая 2020

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

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

В старые добрые времена до std::atomic, барьеры компилятора были единственным способом помешать компилятору хранить значения в registers (приватно для ядра / потока ЦП, не связно), но это проблема компиляции, а не асм. В теории возможны процессоры с некогерентными кешами (где std :: atomi c потребуется сделать явную очистку, чтобы сделать хранилище видимым), но на практике на практике ни одна реализация не запускает std :: thread через ядра с не когерентные кеши .


Если я не использую ограждения, сколько времени может потребоваться ядру, чтобы увидеть записи другого ядра? тесно связано, я написал в основном этот ответ хотя бы несколько раз раньше. (Но это выглядит как хорошее место для ответа конкретно по этому поводу, не вдаваясь в сорняки, какие барьеры делают что.)


Могут быть некоторые очень незначительные вторичные эффекты блокирования последующих загрузок, которые могут конкурировать с RFO (для этого ядра, чтобы получить эксклюзивный доступ к строке кэша для фиксации хранилища). Процессор всегда пытается истощить буфер хранилища как можно быстрее (путем фиксации в кэше L1d). Как только хранилище фиксирует кэш L1d, оно становится глобально видимым для всех остальных ядер. (Поскольку они согласованы; им все равно придется сделать запрос на совместное использование ...)

Получение текущего ядра для обратной записи некоторых данных хранилища в кэш L3 (особенно в совместно используемом состоянии) может уменьшить штраф за промах, если нагрузка на другое ядро ​​происходит несколько позже после фиксации этого магазина. Но нет хороших способов сделать это. Создание конфликта Мисс в L1d и L2 возможно, если производительность производителя не важна, кроме создания низкой задержки для следующего чтения.

На x86, Intel Tremont (низкая Power Silver Series) представит cldemote (_mm_cldemote), который записывает строку обратно до внешнего кэша, но не до DRAM. (clwb, возможно, может помочь, но вынуждает магазин к go вплоть до DRAM. Кроме того, реализация Skylake является просто заполнителем и работает как clflushopt.)

Интересный факт: non-seq_cst сохраняет / загружает в PowerP C может сохранять данные между логическими ядрами на одном физическом ядре, делая хранилища видимыми для некоторых других ядер, прежде чем они станут глобальными видимы для всех других ядер. Это AFAIK единственный реальный аппаратный механизм для потоков, чтобы не согласовать глобальный порядок хранилищ для всех объектов. Будут ли две записи atomi c в разные места в разных потоках всегда рассматриваться в одном и том же порядке другими потоками? . На других ISA, включая ARMv8 и x86, гарантировано, что хранилища становятся видимыми для всех других ядер одновременно (через фиксацию в кеше L1d).


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

Это может оказаться оптимальным по совпадению времени, если это заставит его увидеть магазин, который он ожидал, вместо того, чтобы "слишком рано" и увидеть старое кэшированное скучное значение. Но, как правило, нет причин предполагать или когда-либо прогнозировать, что pause или барьер могут быть хорошей идеей перед нагрузкой.

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

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

Любая возможная выгода почти наверняка не стоит; если бы было столько полезной работы, независимой от этой нагрузки, что она могла бы заполнить все буферы неосновных запросов (LFB на Intel), то это вполне могло бы быть не на критическом пути, и, вероятно, было бы хорошо иметь эти нагрузки в полете .

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