Сравнение скорости atomic, spinlock и mutex

Было мне интересно сравнить скорость разных вариантов блокировок: atomic, spinlock, mutex.

Без синхронизации

Для начала вариант вообще без какой-либо синхронизации:

#include <future>
#include <iostream>

volatile int value = 0;

int loop(bool inc, int limit) {
  std::cout << "Started " << inc << " " << limit << std::endl;
  for (int i = 0; i < limit; ++i) {
    if (inc) { 
      ++value;
    } else {
      --value;
    }
  }
  return 0;
}

int main() {
  auto f = std::async(std::launch::async, std::bind(loop, true, 20000000));
  loop(false, 10000000);
  f.wait();
  std::cout << value << std::endl;
}

Компилировать будем clang’ом:

clang++ -std=c++11 -stdlib=libc++ -O3 -o test test.cpp && time ./test

Запускаем:

SSttaarrtteedd  10  2100000000000000

11177087

real    0m0.070s
user    0m0.089s
sys 0m0.002s

Видно, что операции операторов инкремента и декремента неатомарные, и переменная value в конце содержит мусор.

Инструкция LOCK

#include <future>
#include <iostream>

volatile int value = 0;

int loop(bool inc, int limit) {
  std::cout << "Started " << inc << " " << limit << std::endl;
  for (int i = 0; i < limit; ++i) {
    if (inc) { 
      asm("LOCK");
      ++value;
    } else {
      asm("LOCK");
      --value;
    }
  }
  return 0;
}

int main() {
  auto f = std::async(std::launch::async, std::bind(loop, true, 20000000));
  loop(false, 10000000);
  f.wait();
  std::cout << value << std::endl;
} 

Запускаем:

SSttaarrtteedd  10  2000000100000000

10000000

real    0m0.481s
user    0m0.779s
sys 0m0.005s

Тут уже value содержит правильное значение, но, конечно, это абсолютно непереносимый хак, заточенный под x86, и который, например, у меня работает только с -O3-O2 падает с “illegal instruction”, так как между командой LOCK и инкрементом/декрементом компилятор сует что-то еще).

Atomic

#include <future>
#include <iostream>
#include "boost/interprocess/detail/atomic.hpp"

using namespace boost::interprocess::ipcdetail;

volatile boost::uint32_t value = 0;

int loop(bool inc, int limit) {
  std::cout << "Started " << inc << " " << limit << std::endl;
  for (int i = 0; i < limit; ++i) {
    if (inc) { 
      atomic_inc32(&value);
    } else {
      atomic_dec32(&value);
    }
  }
  return 0;
}

int main() {
  auto f = std::async(std::launch::async, std::bind(loop, true, 20000000));
  loop(false, 10000000);
  f.wait();
  std::cout << atomic_read32(&value) << std::endl;
}

Запускаем:

SSttaarrtteedd  10  2100000000000000

10000000

real    0m0.457s
user    0m0.734s
sys 0m0.004s

Результат правильный, и время почти такое же, как и с командой LOCK. Неудивительно, так как atomic на самом тоже использует инструкцию LOCK, но просто точно гарантированным и проверенным образом.

Spinlock

#include <future>
#include <iostream>
#include "boost/smart_ptr/detail/spinlock.hpp"

boost::detail::spinlock lock;
volatile int value = 0;

int loop(bool inc, int limit) {
  std::cout << "Started " << inc << " " << limit << std::endl;
  for (int i = 0; i < limit; ++i) {
    std::lock_guard<boost::detail::spinlock> guard(lock);
    if (inc) { 
      ++value;
    } else {
      --value;
    }
  }
  return 0;
}

int main() {
  auto f = std::async(std::launch::async, std::bind(loop, true, 20000000));
  loop(false, 10000000);
  f.wait();
  std::cout << value << std::endl;
}

Запускаем:

SSttaarrtteedd  10  2100000000000000

10000000

real    0m0.541s
user    0m0.675s
sys 0m0.089s

Немного медленнее, но не на много.

Mutex

#include <future>
#include <iostream>

std::mutex mutex;
volatile int value = 0;

int loop(bool inc, int limit) {
  std::cout << "Started " << inc << " " << limit << std::endl;
  for (int i = 0; i < limit; ++i) {
    std::lock_guard<std::mutex> guard(mutex);
    if (inc) { 
      ++value;
    } else {
      --value;
    }
  }
  return 0;
}

int main() {
  auto f = std::async(std::launch::async, std::bind(loop, true, 20000000));
  loop(false, 10000000);
  f.wait();
  std::cout << value << std::endl;
}

Запускаем:

SSttaarrtteedd  10  2010000000000000

10000000

real    0m25.229s
user    0m7.011s
sys 0m22.667s

Вот тут, конечно, реально медленнее.

Сравнительная табличка

Метод Время (сек.)
Без синхронизации 0.070
LOCK 0.481
Atomic 0.457
Spinlock 0.541
Mutex 22.667

Конечно, очень многое зависит от платформы и компилятора (я тестировал на Mac Air и clang). Но лично я, например, получил интересное наблюдение, что spinlock, несмотря на значительную сложную реализации, судя по сгенерированному кому, почти не уступает atomic’у.

Жалко, что мой clang пока не поддерживает atomic, и пришлось использовать boost.

P.S.

В C++ 2011 гарантируется, что при многопоточном использовании стандартных потоков гарантируется отсутствие data race, но неперемешивание вывода не гарантируется.

Concurrent access to a synchronized (§27.5.3.4) standard iostream object’s formatted and unformatted input (§27.7.2.1) and output (§27.7.3.1) functions or a standard C stream by multiple threads shall not result in a data race (§1.10). [ Note: Users must still synchronize concurrent use of these objects and streams by multiple threads if they wish to avoid interleaved characters. — end note ]

Посты по теме


Disclaimer

Комментарии