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

#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
1

Другие решения

Других решений пока нет …

По вопросам рекламы ammmcru@yandex.ru
Adblock
detector