Я написал что-то, используя атомику, а не блокировки, и недоумевал, что в моем случае это происходит намного медленнее, я написал следующий мини-тест:
#include <pthread.h>
#include <vector>
struct test
{
test(size_t size) : index_(0), size_(size), vec2_(size)
{
vec_.reserve(size_);
pthread_mutexattr_init(&attrs_);
pthread_mutexattr_setpshared(&attrs_, PTHREAD_PROCESS_PRIVATE);
pthread_mutexattr_settype(&attrs_, PTHREAD_MUTEX_ADAPTIVE_NP);
pthread_mutex_init(&lock_, &attrs_);
}
void lockedPush(int i);
void atomicPush(int* i);
size_t index_;
size_t size_;
std::vector<int> vec_;
std::vector<int> vec2_;
pthread_mutexattr_t attrs_;
pthread_mutex_t lock_;
};
void test::lockedPush(int i)
{
pthread_mutex_lock(&lock_);
vec_.push_back(i);
pthread_mutex_unlock(&lock_);
}
void test::atomicPush(int* i)
{
int ii = (int) (i - &vec2_.front());
size_t index = __sync_fetch_and_add(&index_, 1);
vec2_[index & (size_ - 1)] = ii;
}
int main(int argc, char** argv)
{
const size_t N = 1048576;
test t(N);
// for (int i = 0; i < N; ++i)
// t.lockedPush(i);
for (int i = 0; i < N; ++i)
t.atomicPush(&i);
}
Если я раскомментирую операцию atomicPush и проведу тест с time(1)
Я получаю вывод примерно так:
real 0m0.027s
user 0m0.022s
sys 0m0.005s
и если я запускаю цикл, вызывающий атомарную вещь (казалось бы, ненужная операция, потому что я хочу, чтобы моя функция выглядела как можно больше, чем мой больший код), я получаю вывод примерно так:
real 0m0.046s
user 0m0.043s
sys 0m0.003s
Я не уверен, почему это происходит, так как я ожидал, что атом будет быстрее, чем блокировка в этом случае …
Когда я компилирую с -O3, я вижу блокировки и атомарные обновления следующим образом:
lock:
real 0m0.024s
user 0m0.022s
sys 0m0.001s
atomic:
real 0m0.013s
user 0m0.011s
sys 0m0.002s
Хотя в моем более крупном приложении производительность блокировки (однопоточное тестирование) все еще улучшается, несмотря на то, что …
Неконтролируемый мьютекс очень быстро блокируется и разблокируется. С атомарной переменной вы всегда заплатить определенный штраф за синхронизацию с памятью (тем более что вы даже не используете расслабленный порядок).
Ваш тестовый пример слишком наивен, чтобы быть полезным. Вы должны протестировать сценарий доступа к данным с высокой степенью конкуренции.
Вообще, атомика являются медленно (они мешают умному внутреннему переупорядочению, конвейерной обработке и кешированию), но они допускают код без блокировки, который гарантирует, что вся программа может сделать немного прогресс. Напротив, если вы поменялись, удерживая замок, каждый должен ждать.
Просто чтобы добавить к первому ответу, когда вы делаете __sync_fetch_and_add
вы на самом деле обеспечиваете конкретное упорядочение кода. От документация
Полный барьер памяти создается при вызове этой функции
барьер памяти когда
центральный процессор (ЦП) или компилятор для обеспечения ограничения порядка операций с памятью, выданных до и после инструкции барьера
Скорее всего, даже если ваша работа атомарна, вы теряете оптимизацию компилятора, заставляя упорядочивать инструкции.