Почему атомы намного медленнее, чем блокировка в этом безоговорочном случае?

Я написал что-то, используя атомику, а не блокировки, и недоумевал, что в моем случае это происходит намного медленнее, я написал следующий мини-тест:

#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

Хотя в моем более крупном приложении производительность блокировки (однопоточное тестирование) все еще улучшается, несмотря на то, что …

2

Решение

Неконтролируемый мьютекс очень быстро блокируется и разблокируется. С атомарной переменной вы всегда заплатить определенный штраф за синхронизацию с памятью (тем более что вы даже не используете расслабленный порядок).

Ваш тестовый пример слишком наивен, чтобы быть полезным. Вы должны протестировать сценарий доступа к данным с высокой степенью конкуренции.

Вообще, атомика являются медленно (они мешают умному внутреннему переупорядочению, конвейерной обработке и кешированию), но они допускают код без блокировки, который гарантирует, что вся программа может сделать немного прогресс. Напротив, если вы поменялись, удерживая замок, каждый должен ждать.

6

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

Просто чтобы добавить к первому ответу, когда вы делаете __sync_fetch_and_add вы на самом деле обеспечиваете конкретное упорядочение кода. От документация

Полный барьер памяти создается при вызове этой функции

барьер памяти когда

центральный процессор (ЦП) или компилятор для обеспечения ограничения порядка операций с памятью, выданных до и после инструкции барьера

Скорее всего, даже если ваша работа атомарна, вы теряете оптимизацию компилятора, заставляя упорядочивать инструкции.

1

По вопросам рекламы [email protected]