Локи на чтение [1/2]
Я видел десятки, а то и сотни раз, как люди используют ReaderMutexLock или std::shared_mutex/std::shared_lock на секции, которые только читают данные.
Ок, пишем бенчмарк для коротких секций, который только вызывает Get(), первые бенчмарки для std::shared_lock как выше, второй для полноценных локов:
Отличный вопрос, почему так. Well, тут очень сложные ответы и однозначного нет. Но давайте попробуем дать интуицию, почему так
Во-первых, Reader Lock (читатель) должен делать чуть больше работы, чтобы понять, что Writers (писатель) нет, сравним исходя из absl::MutexLock и absl::ReaderMutexLock
absl::MutexLock
Я видел десятки, а то и сотни раз, как люди используют 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Что??? Exclusive локи в два раза быстрее? Да, всё так, shared_lock, хоть и разрешает многим читателям входить в секции, в разы медленнее, когда количество тредов увеличивается. То есть код просто на эксклюзивных локах быстрее.
# 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
Отличный вопрос, почему так. Well, тут очень сложные ответы и однозначного нет. Но давайте попробуем дать интуицию, почему так
Во-первых, Reader Lock (читатель) должен делать чуть больше работы, чтобы понять, что Writers (писатель) нет, сравним исходя из absl::MutexLock и absl::ReaderMutexLock
absl::MutexLock
// try fast acquire, then spin loopabsl::ReaderMutexLock
if ((v & (kMuWriter | kMuReader | kMuEvent)) != 0 ||
!mu_.compare_exchange_strong(v, kMuWriter | v,
std::memory_order_acquire,
std::memory_order_relaxed))
// try fast acquire, then slow loopkMuOne даёт +1 инструкцию. В целом это хоть и дополнительный оверхед, сильно много проблем оно не создаст, тем не менее, является причиной, почему ReaderLock может быть медленнее. Намного больший оверхед создаётся в этой операции из-за того, что много тредов пытаются поменять атомик, что вызывает у когерентных кешей достаточно много проблем. Если есть просто writer lock, то он чаще выйдет из первого условия if. В целом writer lock намного легче написать в коде, потому что он просто кладёт всех в очередь и просыпает только одного при выходе из секции. Это основная регрессия этого бенчмарка.
if ((v & (kMuWriter | kMuWait | kMuEvent)) != 0 ||
!mu_.compare_exchange_strong(v, (kMuReader | v) + kMuOne,
std::memory_order_acquire,
std::memory_order_relaxed))