Рассмотрим следующий пример, который доказывает ложное совместное использование существование:
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, ¬_sh.a ), std::thread( thd, ¬_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';
}