Я хотел узнать то же самое, поэтому измерил. На моем ящике (восьмиядерный процессор AMD FX (tm) -8150 с тактовой частотой 3,612361 ГГц) блокировка и разблокировка разблокированного мьютекса, который находится в собственной строке кэша и уже кэширован, занимает 47 тактов (13 нс).
Из-за синхронизации между двумя ядрами (я использовал ЦП №0 и №1), я мог вызывать пару блокировка / разблокировка только один раз каждые 102 нс на двух потоках, то есть один раз каждые 51 нс, из чего можно сделать вывод, что это занимает примерно 38 ns для восстановления после того, как поток разблокирует его, прежде чем следующий поток сможет снова заблокировать его.
Программу, которую я использовал для исследования этого вопроса, можно найти здесь:
Обратите внимание, что в нем есть несколько жестко запрограммированных значений, специфичных для моего бокса (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 измеряют количество часов, которое требуется для блокировки и разблокировки `mutex '(с накладными расходами в 39 часов для вызовов rdtsc на моем компьютере). Третий ассемблер - это цикл задержки. Размер цикла задержки для потока 1 на 1 счет меньше, чем для потока 0, поэтому поток 1 немного быстрее.
Вышеупомянутая функция вызывается в узком цикле размером 100000. Несмотря на то, что функция немного быстрее для потока 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 микросекунд; эта задержка, таким образом, происходит при разблокировке мьютекса, когда другой поток ожидает этого мьютекса в ядре (после того, как вращение заняло слишком много времени).
person
Carlo Wood
schedule
07.04.2018