Я пытаюсь определить, какие служебные данные std::atomic
вводят в безусловную запись памяти в мою систему (восьмиядерный x64). Вот моя тестовая программа:
#include <atomic>
#include <iostream>
#include <omp.h>
int main() {
std::atomic_int foo(0); // VERSION 1
//volatile int foo = 0; // VERSION 2
#pragma omp parallel
for (unsigned int i = 0; i < 10000000; ++i) {
foo.store(i, std::memory_order_relaxed); // VERSION 1
//foo = i; // VERSION 2
}
std::cout << foo << std::endl;
}
Программа as-is проведет тестирование std::atomic_int
и комментирует строки с меткой VERSION 1
и раскомментирует строки с меткой VERSION 2
проверит volatile int
на своем месте. Даже при несинхронизированном выводе обеих программ должно быть 10000000 - 1.
Это моя команда:
g++ -O2 -std=c++11 -fopenmp test.c++
Версия, использующая atomic_int
, занимает от двух до трех секунд в моей системе, а та, которая использует volatile int
, почти всегда завершается менее чем за десятую часть секунды.
Значительная разница в сборке - это (вывод из diff --side-by-side
):
volatile int atomic_int
.L2: .L2:
mov DWORD PTR [rdi], eax | mov rdx, QWORD PTR [rdi]
> mov DWORD PTR [rdx], eax
add eax, 1 add eax, 1
cmp eax, 10000000 cmp eax, 10000000
jne .L2 jne .L2
rep ret rep ret
rdi
- это первый аргумент этой функции, который запускается параллельно (он нигде не изменяется в функции), и он, по-видимому, является указателем на (указатель на, во втором столбце) целое число foo
. Я не считаю, что этот дополнительный mov
является неотъемлемой частью гарантии атомарности atomic_int
.
Дополнительный mov
действительно является источником замедления для atomic_int
; перемещение его выше L2
позволяет обеим версиям достичь одинаковой производительности и выводит правильный номер.
Когда foo
создается глобальная переменная, atomic_int
достигает такой же повышенной производительности volatile int
.
Мои вопросы таковы: почему компилятор передает указатель на указатель в случае выделенного стеком atomic_int
, но только указатель в случае глобального atomic_int
или выделенного в стеке volatile int
; почему он загружает этот указатель на каждую итерацию цикла, поскольку он (я считаю) цикл-инвариантный код; и какие изменения в источнике С++ я могу сделать, чтобы atomic_int
соответствовать volatile int
в этом тесте?
Update
Запуск этой программы:
#include <atomic>
#include <iostream>
#include <thread>
//using T = volatile int; // VERSION 1
using T = std::atomic_int; // VERSION 2
void foo(T* ptr) {
for (unsigned int i = 0; i < 10000000; ++i) {
//*ptr = i; // VERSION 1
ptr->store(i, std::memory_order_relaxed); // VERSION2
}
}
int main() {
T i { 0 };
std::thread threads[4];
for (auto& x : threads)
x = std::move(std::thread { foo, &i });
for (auto& x : threads)
x.join();
std::cout << i << std::endl;
}
дает ту же самую улучшенную производительность для обеих версий 1 и 2, что заставляет меня думать, что это особенность OpenMP, которая заставляет худший perf для atomic_int
. Правильно ли OpenMP, или он генерирует субоптимальный код?