Вращение на атомной нагрузке с приобретением согласованности против ослабленной согласованности

#c #atomic #stdatomic

#c #атомарный #stdatomic

Вопрос:

Рассмотрим приведенный ниже код:

 // Class member initialization:
std::atomic<bool> ready_ = false;

...

// Core A:
while (!ready_.load(std::memory_order_acquire)) {
  // On x86, you would probably put a `pause` instruction here.
}
// Core A now accesses memory written by Core B.

...

// Core B:
// Core B writes memory.
ready_.store(true, std::memory_order_release);
 

Предположим, что ядро A и ядро B являются двумя разными физическими ядрами (т. Е. Они не Являются двумя гиперпоточностями, расположенными на одном и том же физическом ядре). Имеет ли приведенный выше код ядра A худшую производительность, чем приведенный ниже код, или равную производительность? Обратите внимание, что ядро A просто выполняет загрузку; это не классический пример сравнения-обмена, который включает запись. Меня интересует ответ для нескольких архитектур.

 // Core A:
while (!ready_.load(std::memory_order_relaxed)) {
  // On x86, you would probably put a `pause` instruction here.
}
std::atomic_thread_fence(std::memory_order_acquire);
// Core A now accesses memory written by Core B.
 

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

Комментарии:

1. Это работает, но более эффективно (теоретически) заменить вторую нагрузку (mo_acquire) на автономный забор std::atomic_thread_fence(std::memory_order_acquire);

2. @DavidSchwartz Я уберу это (я написал это по привычке, так как я работаю в основном на x86). Я не думаю, что его удаление влияет на суть моего вопроса?

3. @LWimsey Правильно, обновлено.

Ответ №1:

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

Первая проблема заключается в том, что атомарная загрузка может повлиять на производительность других процессоров. В alpha, что часто является хорошим случаем «выброса» при изучении согласованности памяти, вы будете снова и снова выдавать инструкцию memory barrier, которая потенциально может заблокировать шину памяти (на машине, отличной от NUMA), или сделать что-то еще, чтобы принудительно записать атомарность хранилищ двумя другими процессорами.

Вторая проблема заключается в том, что барьер влияет на все предыдущие загрузки, а не только на загрузку ready_ . Так что, возможно, на машине NUMA ready_ на самом деле попадает в кеш, потому что нет разногласий, и ваш процессор уже кэширует его в эксклюзивном режиме, но некоторая предыдущая загрузка ожидает систему памяти. Теперь вам нужно остановить процессор, чтобы дождаться предыдущей загрузки, вместо того, чтобы потенциально продолжать выполнять инструкции, которые не конфликтуют с остановленной загрузкой. Вот пример:

 int a = x.load(memory_order_relaxed);
while (!ready_.load(std::memory_order_relaxed))
  ;
std::atomic_thread_fence(std::memory_order_acquire);
int b = y;
 

В этом случае загрузка y потенциально может приостановиться в ожидании x , тогда как, если загрузка ready_ была выполнена с использованием семантики приобретения, тогда загрузка x могла бы просто продолжаться параллельно, пока не потребуется значение.

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

   void lock() {
    for (;;) {
      if (!lock_.exchange(true, std::memory_order_acquire)) {
        break;
      }
      while (lock_.load(std::memory_order_relaxed)) {
        __builtin_ia32_pause();
      }
    }
  }