Я хотел знать то же самое, поэтому я измерил это. На моем компьютере (8-ядерный процессор AMD FX (tm) -8150 с тактовой частотой 3,612361 ГГц) блокировка и разблокировка разблокированного мьютекса, который находится в отдельной строке кэша и уже кэширован, занимает 47 тактов (13 нс).
Из-за синхронизации между двумя ядрами (я использовал CPU # 0 и # 1), я мог вызывать пару блокировки / разблокировки только один раз каждые 102 нс в двух потоках, то есть один раз каждые 51 нс, из чего можно сделать вывод, что это занимает примерно 38 ns восстановить после того, как поток выполнит разблокировку, прежде чем следующий поток сможет снова его заблокировать.
Программу, которую я использовал для исследования этого, можно найти здесь:
https://github.com/CarloWood/ai-statefultask-testsuite/blob/b69b112e2e91d35b56a39f41809d3e3de2f9e4b8/src/mutex_test.cxx
Обратите внимание, что у него есть несколько жестко заданных значений, специфичных для моего блока (xrange, yrange и rdtsc overhead), поэтому вам, вероятно, придется поэкспериментировать с ним, прежде чем он будет работать для вас.
График, который он производит в этом состоянии:
Это показывает результаты теста производительности в следующем коде:
uint64_t do_Ndec(int thread, int loop_count)
{
uint64_t start;
uint64_t end;
int __d0;
asm volatile ("rdtsc\n\tshl $32, %%rdx\n\tor %%rdx, %0" : "=a" (start) : : "%rdx");
mutex.lock();
mutex.unlock();
asm volatile ("rdtsc\n\tshl $32, %%rdx\n\tor %%rdx, %0" : "=a" (end) : : "%rdx");
asm volatile ("\n1:\n\tdecl %%ecx\n\tjnz 1b" : "=c" (__d0) : "c" (loop_count - thread) : "cc");
return end - start;
}
Два вызова rdtsc измеряют количество часов, которое требуется для блокировки и разблокировки «мьютекса» (с накладными расходами в 39 часов для вызовов rdtsc на моем ящике). Третий ассм - это петля задержки. Размер цикла задержки на 1 счет меньше для потока 1, чем для потока 0, поэтому поток 1 немного быстрее.
Вышеуказанная функция вызывается в узком цикле размером 100 000. Несмотря на то, что функция немного быстрее для потока 1, оба цикла синхронизируются из-за вызова мьютекса. Это видно на графике из того факта, что количество тактов, измеренных для пары блокировка / разблокировка, немного больше для потока 1, чтобы учесть более короткую задержку в цикле под ним.
На приведенном выше графике нижняя правая точка представляет собой измерение с задержкой loop_count, равной 150, а затем, следуя точкам внизу, влево, loop_count уменьшается на единицу для каждого измерения. Когда она становится 77, функция вызывается каждые 102 нс в обоих потоках. Если впоследствии loop_count уменьшается еще больше, то больше невозможно синхронизировать потоки, и мьютекс начинает фактически блокироваться большую часть времени, что приводит к увеличению количества тактов, необходимых для блокировки / разблокировки. Также из-за этого увеличивается среднее время вызова функции; так что точки сюжета теперь идут вверх и вправо снова.
Из этого мы можем сделать вывод, что блокировка и разблокировка мьютекса каждые 50 нс не проблема для моего устройства.
В целом мой вывод заключается в том, что ответ на вопрос OP заключается в том, что добавление большего количества мьютексов лучше, если это приводит к меньшему количеству конфликтов.
Попробуйте заблокировать мьютексы как можно короче. Единственная причина поместить их, скажем, вне цикла, состоит в том, что этот цикл зацикливается быстрее, чем один раз каждые 100 нс (или, скорее, число потоков, которые хотят запустить этот цикл одновременно, 50 нс) или когда 13 нс раз размер цикла больше задержки, чем задержка, которую вы получаете из-за конфликта.
РЕДАКТИРОВАТЬ: я получил гораздо больше знаний по этому вопросу сейчас и начинаю сомневаться в заключении, которое я представил здесь. Прежде всего, CPU 0 и 1 оказываются гиперпоточными; Несмотря на то, что AMD утверждает, что имеет 8 реальных ядер, безусловно, есть что-то очень подозрительное, потому что задержки между двумя другими ядрами намного больше (то есть 0 и 1 образуют пару, как и 2 и 3, 4 и 5, и 6 и 7 ). Во-вторых, std :: mutex реализован таким образом, что он немного вращает блокировки перед тем, как фактически выполнять системные вызовы, когда не удается немедленно получить блокировку для мьютекса (что, без сомнения, будет очень медленным). Итак, что я измерил здесь, так это абсолютную наиболее идеальную ситуацию, и на практике блокировка и разблокировка могут занять значительно больше времени на блокировку / разблокировку.
В итоге мьютекс реализован с использованием атомики. Чтобы синхронизировать атомы между ядрами, должна быть заблокирована внутренняя шина, которая замораживает соответствующую строку кэша на несколько сотен тактов. В случае, если блокировка не может быть получена, системный вызов должен быть выполнен, чтобы перевести поток в спящий режим; это, очевидно, очень медленно (системные вызовы порядка 10 микросекунд). Обычно это на самом деле не проблема, потому что этот поток все равно должен спать - но это может быть проблемой с высокой конкуренцией, когда поток не может получить блокировку в течение времени, когда он обычно вращается, и так делает системный вызов, но CAN возьмите замок вскоре после этого. Например, если несколько потоков блокируют и разблокируют мьютекс в узком цикле и каждый из них удерживает блокировку в течение 1 микросекунды или около того, тогда они могут быть сильно замедлены тем фактом, что их постоянно усыпляют и снова просыпают. Кроме того, после того, как поток спит и другой поток должен его разбудить, этот поток должен выполнить системный вызов и задерживается ~ 10 микросекунд; таким образом, эта задержка происходит при разблокировке мьютекса, когда другой поток ожидает этот мьютекс в ядре (после вращения потребовалось слишком много времени).