Локи на чтение [1/2]



Я видел десятки, а то и сотни раз, как люди используют ReaderMutexLock или std::shared_mutex/std::shared_lock на секции, которые только читают данные.



  void Set(std::string value) {

// Writer lock.

std::unique_lock lock(mutex_);

value_ = std::move(value);

}



std::string Get() const {

// Many readers can access it.

std::shared_lock lock(mutex_);

return value_;

}



Логично? Очень, много же читателей заходят в секцию, значит время исполнения должно уменьшиться, раз много читателей могут зайти в одну и ту же секцию.



Ок, пишем бенчмарк для коротких секций, который только вызывает Get(), первые бенчмарки для std::shared_lock как выше, второй для полноценных локов:



  void Set(std::string value) {

std::unique_lock lock(mutex_);

value_ = std::move(value);

}

std::string Get() const {

std::unique_lock lock(mutex_);

return value_;

}



Получаем



Benchmark                                               Time             CPU   Iterations

# Shared lock

BM_Get<std_mutex::StringGetter>/threads:2 114 ns 227 ns 3441024

BM_Get<std_mutex::StringGetter>/threads:4 135 ns 538 ns 1354028

BM_Get<std_mutex::StringGetter>/threads:8 112 ns 898 ns 592416

BM_Get<std_mutex::StringGetter>/threads:16 155 ns 2479 ns 301440

BM_Get<std_mutex::StringGetter>/threads:32 130 ns 4165 ns 174848

# Exclusive lock

BM_Get<std_mutex_ex::StringGetter>/threads:2 57.2 ns 114 ns 6231412

BM_Get<std_mutex_ex::StringGetter>/threads:4 56.8 ns 212 ns 3188440

BM_Get<std_mutex_ex::StringGetter>/threads:8 69.6 ns 535 ns 1285104

BM_Get<std_mutex_ex::StringGetter>/threads:16 68.5 ns 1074 ns 655664

BM_Get<std_mutex_ex::StringGetter>/threads:32 72.7 ns 2292 ns 352640





Что??? Exclusive локи в два раза быстрее? Да, всё так, shared_lock, хоть и разрешает многим читателям входить в секции, в разы медленнее, когда количество тредов увеличивается. То есть код просто на эксклюзивных локах быстрее.



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



Во-первых, Reader Lock (читатель) должен делать чуть больше работы, чтобы понять, что Writers (писатель) нет, сравним исходя из absl::MutexLock и absl::ReaderMutexLock



absl::MutexLock



// try fast acquire, then spin loop

if ((v & (kMuWriter | kMuReader | kMuEvent)) != 0 ||

!mu_.compare_exchange_strong(v, kMuWriter | v,

std::memory_order_acquire,

std::memory_order_relaxed))

absl::ReaderMutexLock



// try fast acquire, then slow loop

if ((v & (kMuWriter | kMuWait | kMuEvent)) != 0 ||

!mu_.compare_exchange_strong(v, (kMuReader | v) + kMuOne,

std::memory_order_acquire,

std::memory_order_relaxed))





kMuOne даёт +1 инструкцию. В целом это хоть и дополнительный оверхед, сильно много проблем оно не создаст, тем не менее, является причиной, почему ReaderLock может быть медленнее. Намного больший оверхед создаётся в этой операции из-за того, что много тредов пытаются поменять атомик, что вызывает у когерентных кешей достаточно много проблем. Если есть просто writer lock, то он чаще выйдет из первого условия if. В целом writer lock намного легче написать в коде, потому что он просто кладёт всех в очередь и просыпает только одного при выходе из секции. Это основная регрессия этого бенчмарка.