Что делает повышение :: shared_mutex таким медленным - PullRequest
3 голосов
/ 26 июня 2019

Я использовал google benchmark для запуска следующих 3 тестов, и результат меня удивил, поскольку блокировка RW примерно в 4 раза медленнее, чем простой мьютекс в режиме релиза.(и примерно в 10 раз медленнее, чем простой мьютекс в режиме отладки)

void raw_access() {
    (void) (gp->a + gp->b);
}

void mutex_access() {
    std::lock_guard<std::mutex> guard(g_mutex);
    (void) (gp->a + gp->b);
}

void rw_mutex_access() {
    boost::shared_lock<boost::shared_mutex> l(g_rw_mutex);
    (void) (gp->a + gp->b);
}

результат:

2019-06-26 08:30:45
Running ./perf
Run on (4 X 2500 MHz CPU s)
CPU Caches:
  L1 Data 32K (x2)
  L1 Instruction 32K (x2)
  L2 Unified 262K (x2)
  L3 Unified 4194K (x1)
Load Average: 5.35, 3.22, 2.57
-----------------------------------------------------------
Benchmark                 Time             CPU   Iterations
-----------------------------------------------------------
BM_RawAccess           1.01 ns         1.01 ns    681922241
BM_MutexAccess         18.2 ns         18.2 ns     38479510
BM_RWMutexAccess       92.8 ns         92.8 ns      7561437

Я не получил достаточно информации через Google, поэтому надеюсь, что некоторая помощь здесь,

Спасибо

1 Ответ

6 голосов
/ 26 июня 2019

Я не знаю подробностей о том, как работает стандартная библиотека / boost / etc.реализации отличаются, хотя кажется, что стандартная версия библиотеки быстрее (поздравляю, кто бы ее ни написал) .

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

Atomic Spin Lock

Более того, в качестве академического упражнения рассмотрим простейший поток -безопасная "мьютексоподобная" реализация: простая атомная спиновая блокировка.

По сути, это не что иное, как std::atomic<bool> или std::atomic_flag.Инициализируется как ложное.Чтобы «заблокировать» мьютекс, вы просто выполняете атомарную операцию сравнения и обмена в цикле до тех пор, пока не получите ложное значение (т. Е. Предыдущее значение было ложным, перед тем как атомарно установить его в true).

std::atomic_flag flag = ATOMIC_FLAG_INIT;

// lock it by looping until we observe a false value
while (flag.test_and_set()) ;

// do stuff under "mutex" lock

// unlock by setting it back to false state
flag.clear();

Однако, из-за природы этой конструкции, это то, что мы называем «несправедливым» мьютексом, потому что порядок потоков, которые получают блокировку, не обязательно является порядком, в котором они начали свои попытки заблокировать ее.То есть в условиях высокой конкуренции возможно, что поток пытается заблокировать и просто никогда не преуспевает, потому что другим потокам повезло больше.Это очень чувствительно ко времени.Представьте себе музыкальные стулья.

Из-за этого, хотя он функционирует как мьютекс, это не то, что мы считаем «мьютексом».

Мьютекс

Можно думать о мьютексекак построение поверх атомной спин-блокировки (хотя обычно она не реализуется как таковая, поскольку они обычно реализуются с поддержкой операционной системы и / или оборудования).

По сути, мьютекс - это шаг вышеатомные спиновые блокировки, потому что у него есть очередь ожидающих потоков.Это позволяет ему быть «справедливым», поскольку порядок получения блокировки (более или менее) такой же, как порядок попыток блокировки.

Если вы заметили, если вы запустите sizeof(std::mutex), это может бытьнемного больше, чем вы могли ожидать.На моей платформе это 40 байтов.Это дополнительное пространство используется для хранения информации о состоянии, в частности, включая некоторый способ доступа к очереди блокировки для каждого отдельного мьютекса.

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

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

Shared Mutex

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

Таким образом, в дополнение к уникальной очереди владения (как обычный мьютекс) он также имеет состояние общего владения.Состояние общего владения может быть просто подсчетом количества потоков, которые в данный момент имеют общее владение.Если вы посмотрите sizeof(std::shared_mutex), вы обнаружите, что он обычно даже больше, чем std::mutex.В моей системе, например, это 56 байт.

Поэтому, когда вы собираетесь заблокировать общий мьютекс, он должен делать все, что делает обычный мьютекс, но дополнительно должен проверять некоторые другие вещи.Например, если вы пытаетесь заблокировать однозначно, он должен убедиться, что нет общих владельцев.И когда вы пытаетесь заблокировать совместно, он должен убедиться, что нет уникальных владельцев.

Поскольку мы обычно хотим, чтобы мьютексы были «честными», после того, как в очередь попадет уникальный блокировщик, будущие попытки блокировки общего доступа должны ставиться в очередь вместо получения блокировки, даже если в данный момент она может находиться в режиме совместного использования (т. Е. Неуникальной) блокировкинесколькими нитями.Это сделано для того, чтобы общие владельцы не «запугивали» поток, который хочет иметь уникальное право собственности.

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

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

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

Как вы можете себе представить, вся эта дополнительная логика для того, кто блокирует, по каким причинам и как она меняется в зависимости не только от текущих владельцев, но и от содержимого очереди, делает это потенциально немного медленнее.Мы надеемся, что вы читаете значительно чаще, чем пишете, и поэтому у вас может быть много одновременных владельцев общего доступа, что снижает производительность при координации всего этого.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...