Почему ложное совместное использование все еще влияет на неатомные модели, но гораздо меньше, чем на атомные? - PullRequest
1 голос
/ 08 мая 2020

Рассмотрим следующий пример, который доказывает ложное совместное использование существование:

using type = std::atomic<std::int64_t>;

struct alignas(128) shared_t
{
  type  a;
  type  b;
} sh;

struct not_shared_t
{
  alignas(128) type a;
  alignas(128) type b;
} not_sh;

Один поток увеличивает a с шагом 1, другой поток увеличивает b. Приращения компилируются до lock xadd с MSV C, даже если результат не используется.

Для структуры, где a и b разделены, значения, накопленные за несколько секунд, примерно в десять раз больше для not_shared_t, чем для shared_t.

До сих пор ожидаемый результат: отдельные строки кэша остаются горячими в кэше L1d, увеличиваются узкие места на lock xadd пропускной способности, ложное совместное использование приводит к катастрофе производительности, ухудшающей кеш линия. (Примечание редактора: более поздние версии MSV C используют lock inc при включенной оптимизации. Это может увеличить разрыв между конкурирующими и несогласованными.)


Сейчас я заменяю using type = std::atomic<std::int64_t>; с обычным std::int64_t

(инкремент c не-atomi компилируется в inc QWORD PTR [rcx]. Загрузка atomi c в l oop не позволяет компилятору просто сохранить счетчик в регистре до выхода l oop.)

Достигнутый счетчик для not_shared_t все еще больше, чем для shared_t, но теперь меньше чем в два раза.

|          type is          | variables are |      a=     |      b=     |
|---------------------------|---------------|-------------|-------------|
| std::atomic<std::int64_t> |    shared     |   59’052’951|   59’052’951|
| std::atomic<std::int64_t> |  not_shared   |  417’814’523|  416’544’755|
|       std::int64_t        |    shared     |  949’827’195|  917’110’420|
|       std::int64_t        |  not_shared   |1’440’054’733|1’439’309’339|

Почему корпус c без использования Atomi намного ближе по производительности?


Вот остальная часть программы для завершения минимально воспроизводимого примера. (Также на Godbolt с MSV C, готов к компиляции / запуску)

std::atomic<bool> start, stop;

void thd(type* var)
{
  while (!start) ;
  while (!stop) (*var)++;
}

int main()
{
  std::thread threads[] = {
     std::thread( thd, &sh.a ),     std::thread( thd, &sh.b ),
     std::thread( thd, &not_sh.a ), std::thread( thd, &not_sh.b ),
  };

  start.store(true);

  std::this_thread::sleep_for(std::chrono::seconds(2));

  stop.store(true);
  for (auto& thd : threads) thd.join();

  std::cout
    << " shared: "    << sh.a     << ' ' << sh.b     << '\n'
    << "not shared: " << not_sh.a << ' ' << not_sh.b << '\n';
}

1 Ответ

2 голосов
/ 08 мая 2020

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

Переадресация хранилища дает вам длину буфера хранилища, количество приращений перед остановкой вместо требуется монопольный доступ к строке кэша для выполнения atomi c Увеличение RMW .

Когда это ядро ​​в конечном итоге получает право владения строкой кеша, оно может зафиксировать несколько магазинов по 1 / час. Это в 6 раз быстрее, чем цепочка зависимостей, созданная приращением места назначения в памяти: ~ 5 циклов задержки сохранения / перезагрузки + 1 цикл задержки ALU. Таким образом, выполнение просто помещает новые хранилища в SB на 1/6 скорости, которую он может истощить, пока ядро ​​владеет им, в случае не-atomi c Вот почему нет огромного пробела между разделяемым и не разделяемым atomi c.

Определенно также будет некоторая очистка машины для упорядочивания памяти; это и / или заполнение SB являются вероятными причинами снижения пропускной способности в случае ложного совместного использования. См. Ответы и комментарии к Каковы затраты на задержку и пропускную способность при совместном использовании области памяти производителем и потребителем между гипер-братьями и сестрами по сравнению с не-гипер-братьями? для другого эксперимента, похожего на этот.


A lock inc или lock xadd принудительно очищает буфер хранилища перед операцией и включает фиксацию в кэш L1d как часть операции. Это делает переадресацию хранилища невозможной и может произойти только в том случае, если строка кеша находится в эксклюзивном или измененном состоянии MESI.

Связано:

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