Избегая ложного обмена для улучшения производительности

#include <iostream>
#include <future>
#include <chrono>

using namespace std;
using namespace std::chrono;

int a = 0;
int padding[16]; // avoid false sharing
int b = 0;

promise<void> p;
shared_future<void> sf = p.get_future().share();

void func(shared_future<void> sf, int &data)
{
  sf.get();

  auto t1 = steady_clock::now();
  while (data < 1'000'000'000)
    ++data;
  auto t2 = steady_clock::now();

  cout << duration<double, ratio<1, 1>>(t2 - t1).count() << endl;
}

int main()
{
  thread th1(func, sf, ref(a)), th2(func, sf, ref(b));
  p.set_value();
  th1.join();
  th2.join();

  return 0;
}

Я использую приведенный выше код, чтобы продемонстрировать влияние ложного обмена на производительность. Но, к моему удивлению, заполнение, по-видимому, совсем не ускоряет программу. Интересно, если оба a а также b являются атомными переменными, есть очевидное улучшение. Какая разница?

1 ответ

Решение

Ложное совместное использование лучше всего обнаруживается, когда 2 атомных переменных в одной строке кэша увеличиваются разными потоками с помощью операции чтения-изменения-записи (RMW). Для этого каждый процессор должен очистить буфер хранилища и заблокировать строку кэша на время операции приращения, то есть:

  • заблокировать строку кэша
  • прочитать значение из кэша L1 в регистр
  • значение приращения внутри регистра
  • запись обратно в кэш L1
  • разблокировать кеш строки

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

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

На моем 4-ядерном X86 я получаю следующие цифры:

atomic int, no padding, no optimization: real 57.960s, user 114.495s

atomic int, padding, no optimization: real 10.514s, user 20.793s

atomic int, no padding, full optimization: real 55.732s, user 110.178s

atomic int, padding, full optimization: real 8.712s, user 17.214s

int, no padding, no optimization: real 2.206s, user 4.348s

int, padding, no optimization: real 1.951s, user 3.853s

int, no padding, full optimization: real 0.002s, user 0.000s

int, padding, full optimization: real 0.002s, user 0.000s
Другие вопросы по тегам