Наскільки ефективно блокувати розблокований мютекс? Яка вартість мютексу?


149

Мовою низького рівня (C, C ++ або будь-якою іншою): у мене є вибір між тим, як мати купу файлів (як, наприклад, те, що дає мені pthread) або те, що надає рідна системна бібліотека), або один для об'єкта.

Наскільки ефективно блокувати мютекс? Тобто скільки ймовірних інструкцій асемблера й скільки часу вони займають (у випадку, якщо мютекс розблокований)?

Скільки коштує мютекс? Чи проблема в тому, щоб мати справді багато мютексів? Або я можу просто кинути стільки змінних мютексів у свій код, скільки у мене intзмінних, і це насправді не має значення?

(Я не впевнений, скільки розбіжностей між різними апаратними засобами. Якщо є, я також хотів би знати про них. Але в основному мене цікавить загальне обладнання.)

Справа в тому, що, використовуючи багато файлів mutex, кожен з яких охоплює лише частину об'єкта, а не одну мутекс для всього об’єкта, я міг би захистити багато блоків. І мені цікаво, як далеко я повинен пройти з цього приводу. Тобто я повинен намагатися захистити будь-який можливий блок дійсно наскільки це можливо, незалежно від того, наскільки складніше і скільки ще це мутекси?


Повідомлення в блозі WebKits (2016) про блокування дуже пов'язане з цим питанням і пояснює відмінності між спінлок, адаптивним блокуванням, futex тощо.


Це буде впровадженням та архітектурою. Деякі мутекси коштуватимуть майже нічого, якщо є підтримка нативного обладнання, інші коштуватимуть багато. Без додаткових відомостей відповісти неможливо.
Джан

2
@Gian: Ну, звичайно, я маю на увазі це питання під питанням. Я хотів би знати про загальне обладнання, але і про помітні винятки, якщо такі є.
Альберт

Я насправді ніде не бачу цього наслідку. Ви запитаєте про "інструкції з асемблера" - відповідь може бути від 1 інструкції до десяти тисяч інструкцій залежно від того, про яку архітектуру ви говорите.
Джан

15
@Gian: Тоді, будь ласка, дайте саме цю відповідь. Скажіть, будь ласка, що це насправді на x86 та amd64, будь ласка, наведіть приклад для архітектури, де це 1 інструкція, і дайте те, де це 10k. Чи не ясно, що я хочу це знати з мого запитання?
Альберт

Відповіді:


120

У мене є вибір між тим, як мати купу файлових файлів або одну для об'єкта.

Якщо у вас багато потоків і доступ до об'єкта трапляється часто, то кілька блокувань збільшуватимуть паралельність. Ціною ремонту, оскільки більше блокування означає більше налагодження блокування.

Наскільки ефективно блокувати мютекс? Тобто скільки ймовірних інструкцій асемблера і скільки часу вони займають (у випадку, якщо мютекс розблокований)?

Точні вказівки асемблера є найменшими накладними витратами на мютекс - гарантії узгодженості пам’яті / кешу є головними витратами. І рідше береться той чи інший замок - краще.

Mutex складається з двох основних частин (спрощених): (1) прапор, який вказує, заблоковано чи не мютекс, та (2) черги очікування.

Зміна прапора - це лише кілька інструкцій, які зазвичай виконуються без системного виклику. Якщо mutex заблокований, трапиться системний виклик, щоб додати виклик потоку в чергу очікування та запустити очікування. Якщо розблокувати, якщо черга очікування порожня, це дешево, але в іншому випадку потрібна системна дзвінка, щоб прокинути один з процесів очікування. (У деяких системах дешеві / швидкі систематичні дзвінки використовуються для реалізації мутексів, вони стають повільними (звичайними) системними дзвінками лише у випадку суперечок.)

Блокування розблокованого мютексу дійсно дешево. Розблокування мутексу без суперечок теж дешево.

Скільки коштує мютекс? Чи проблема в тому, щоб мати справді багато мютексів? Або я можу просто кинути стільки змінних мютексів у свій код, скільки в мене є змінні int, і це насправді не має значення?

Ви можете кинути стільки змінних файлів mutex, скільки бажаєте. Ви обмежені лише кількістю пам'яті, яку ви можете виділити.

Підсумок Блоки користувальницького простору (і мутекси зокрема) дешеві і не піддаються жодним системним обмеженням. Але занадто багато з них вимовляє кошмар для налагодження. Проста таблиця:

  1. Менше блокування означає більше розбіжностей (повільні систематичні виклики, зупинки процесора) та менший паралелізм
  2. Менше блокування означає менше проблем з налагодженням проблем із багатопотоковою резьбою.
  3. Більше замків означає менше суперечок і вищий паралелізм
  4. Більше замків означає більше шансів потрапити в непереборні тупики.

Слід знайти і підтримувати збалансовану схему блокування для застосування, як правило, балансуючи №2 та №3.


(*) Проблема з менш часто заблокованими мутексами полягає в тому, що якщо у вас занадто багато блокувань у вашій програмі, це призводить до того, що більша частина трафіку між процесором / основним процесом вимиває пам'ять mutex з кешу даних інших процесорів, щоб гарантувати гарантію кеш-когерентність. Програми кеша схожі на легкі переривання і керуються процесорами прозоро, але вони вводять так звані стійли (пошук "стійла").

А кіоски - це те, що змушує код блокування працювати повільно, часто без явних вказівок, чому застосування повільне. (Деякі арки надають статистику трафіку між CPU / core, деякі ні.)

Щоб уникнути проблеми, люди, як правило, вдаються до великої кількості замків, щоб зменшити ймовірність утримання замків та уникнути стійла. Саме тому існує дешеве блокування простору користувача, яке не піддається системним обмеженням.


Дякую, що здебільшого відповідає на моє запитання. Я не знав, що ядро ​​(наприклад, ядро ​​Linux) обробляє mutex і ви керуєте ними за допомогою syscalls. Але оскільки сам Linux управляє комутаторами планування та перемиканнями контексту, це має сенс. Але зараз у мене є груба фантазія щодо того, що заблокувати / розблокувати мутекс буде зроблено всередині.
Альберт

2
@Albert: О. Я забув контекстні перемикачі ... Контекстні комутатори занадто сильно виснажують продуктивність. Якщо придбання блокування не вдається і потоку доведеться чекати, це занадто якась половина контекстного комутатора. Сам CS швидкий, але оскільки процесор може використовуватися в якомусь іншому процесі, кеші будуть заповнені чужими даними. Після того, як нитка нарешті придбає замок, є ймовірність, що для процесора доведеться перезавантажувати майже все з оперативної пам’яті заново.
Dummy00001

@ Dummy00001 Перехід на інший процес означає, що вам потрібно змінити відображення пам'яті процесора. Це не так дешево.
curiousguy

27

Мені хотілося знати те саме, тому я виміряв це. У моїй коробці (AMD FX (tm) -8150 Восьмиядерний процесор на частоті 3.612361 ГГц), блокуючи та розблоковуючи розблокований мютекс, який знаходиться у власній лінійці кешу і вже кешований, займає 47 годин (13 нс).

Через синхронізацію між двома ядрами (я використовував процесор № 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 вимірюють кількість годин, необхідних для блокування та розблокування `mutex '(з накладними витратами на 39 годин для дзвінків rdtsc у моїй коробці). Третій асм - це цикл затримки. Розмір петлі затримки на 1 нитку менший, ніж для потоку 0, тому нитка 1 трохи швидша.

Вищенаведена функція викликається в тісному циклі розміром 100000. Незважаючи на те, що функція трохи швидша для потоку 1, обидві петлі синхронізуються через виклик до мютексу. Це видно на графіку з того, що кількість годин, виміряних для пари замка / розблокування, трохи більше для потоку 1, щоб врахувати меншу затримку циклу під ним.

У наведеному вище графіку нижня права точка - це вимірювання із затримкою петлі_счет 150, а потім слідуючи за точками внизу, вліво, кількість циклів_наза зменшується на кожне вимірювання. Коли йому стає 77, функція викликається кожні 102 нс в обох потоках. Якщо згодом loop_count ще більше зменшиться, синхронізувати нитки вже неможливо, і мютекс починає фактично блокуватися більшу частину часу, що призводить до збільшення кількості годин, які потрібно зробити для блокування / розблокування. Також через це збільшується середній час виклику функції; тому точки сюжету тепер піднімаються вгору і знову вправо.

З цього можна зробити висновок, що блокування та розблокування мютексу кожні 50 нс не є проблемою для моєї скриньки.

Я в цілому мою висновок, що відповідь на питання про ОП полягає в тому, що додавати більше мутексів краще, якщо це призводить до меншої суперечки.

Спробуйте зафіксувати файли якнайшвидше. Єдиною причиною ставити їх -say- за межами циклу було б, якщо ця петля циклічне швидше, ніж один раз кожні 100 нс (вірніше, кількість потоків, які хочуть виконати цю петлю одночасно, раз 50 нс) або коли 13 нс разів розмір циклу більше затримки, ніж затримка, яку ви отримуєте за суперечкою.

EDIT: Зараз я набагато більше знаю з цього питання і починаю сумніватися у висновку, який я представив тут. Перш за все, CPU 0 і 1 виявляються гіперпотоковими; незважаючи на те, що AMD стверджує, що має 8 справжніх ядер, безумовно, є щось дуже рибне, тому що затримки між двома іншими ядрами значно більші (тобто 0 і 1 утворюють пару, як і 2 і 3, 4 і 5, а також 6 і 7 ). По-друге, std :: mutex реалізований таким чином, що він обертається блокуванням трохи раніше, ніж насправді виконує системні дзвінки, коли не вдається негайно отримати замок на mutex (що, без сумніву, буде надзвичайно повільним). Тож, що я тут виміряв - це абсолютно ідеальна ситуація, і на практиці блокування та розблокування може зайняти значно більше часу за замок / розблокування.

Підсумок, мютекс реалізований з атомами. Для синхронізації атомів між ядрами повинна бути заблокована внутрішня шина, яка заморожує відповідну лінію кешу на кілька сотень тактових циклів. У випадку, якщо блокування неможливо отримати, необхідно виконати системний виклик, щоб покласти нитку у режим сну; що, очевидно, надзвичайно повільно (системні дзвінки мають порядку 10 миркосекунд). Зазвичай це насправді не проблема, тому що ця нитка повинна спати в будь-якому випадку - але це може бути проблемою з великою суперечливістю, коли нитка не може отримати замок протягом часу, який вона зазвичай крутиться, і тому система дзвонить, але CAN візьміть замок незабаром після. Наприклад, якщо кілька потоків блокують і розблоковують мютекс у тісному циклі, і кожен тримає замок на 1 мікросекунд або близько того, тоді вони можуть бути дуже сповільнені тим, що їх постійно засинають і прокидаються знову. Крім того, як тільки нитка спить, і інша нитка повинна її розбудити, ця нитка повинна зробити системний виклик і затримується ~ 10 мікросекунд; ця затримка, таким чином, відбувається під час розблокування мютексу, коли інший потік чекає на цей мютекс у ядрі (після того, як спінінг зайняв занадто довго)


10

Це залежить від того, що ви насправді називаєте "mutex", режимом ОС тощо.

Як мінімум, це вартість операції з блокованою пам'яттю. Це відносно важка операція (порівняно з іншими командами примітивного асемблера).

Однак це може бути набагато вище. Якщо те, що ви називаєте "mutex" об'єктом ядра (тобто - об'єктом, керованим ОС), і запускається в режимі користувача - кожна операція над ним призводить до транзакції в режимі ядра, що є дуже важким.

Наприклад, на процесорі Intel Core Duo, Windows XP. Заблокована робота: займає близько 40 циклів процесора. Виклик в режимі ядра (тобто системний виклик) - близько 2000 циклів процесора.

У такому випадку - ви можете розглянути критичні розділи. Це гібрид мутексу ядра та замкненого доступу до пам'яті.


7
Критичні розділи для Windows набагато ближче до мютексів. Вони мають регулярну семантику мютексу, але вони локально-локальні. Остання частина робить їх набагато швидшими, оскільки з ними можна керувати повністю у вашому процесі (і, таким чином, код у режимі користувача).
MSalters

2
Ця цифра була б більш корисною, якщо для порівняння також буде надано кількість циклів процесора загальних операцій (наприклад, арифметика / якщо-інше / кеш-пропуск / непрямість). .... Було б навіть чудово, якщо є якісь посилання на число. В Інтернеті дуже важко знайти таку інформацію.
javaLover

@javaLover Операції не працюють на циклах; вони працюють на арифметичних одиницях протягом ряду циклів. Це зовсім інакше. Вартість будь-якої інструкції в часі не є визначеною кількістю, а лише витрата на використання ресурсів. Ці ресурси спільні. Вплив інструкцій пам'яті залежить від багато кешування тощо.
curiousguy

@curiousguy Погоджуюся. Мені було не ясно. Я хотів би відповісти, наприклад, std::mutexсередня тривалість використання (в секунду) в 10 разів більше, ніж int++. Однак я знаю, що важко відповісти, оскільки це дуже залежить від багато чого.
javaLover

6

Вартість варіюватиметься залежно від впровадження, але слід пам’ятати про дві речі:

  • вартість, швидше за все, буде мінімальною, оскільки це і досить примітивна операція, і вона буде максимально оптимізована завдяки схемі використання (використовується багато ).
  • неважливо, наскільки це дорого, оскільки вам потрібно використовувати його, якщо ви хочете безпечно працювати з декількома потоками. Якщо вона вам потрібна, то вона вам потрібна.

У системах з одним процесором, як правило, можна просто відключити переривання досить довго, щоб атомно змінити дані. Багатопроцесорні системи можуть використовувати стратегію тестування та встановлення .

В обох цих випадках інструкції є відносно ефективними.

Що стосується того, чи слід надати один мутекс для масивної структури даних, або мати багато мутексів, по одному для кожного його розділу, це акт балансування.

Маючи один мутекс, ви маєте більш високий ризик виникнення суперечок між декількома потоками. Ви можете зменшити цей ризик, маючи mutex на секцію, але не хочете потрапляти в ситуацію, коли нитка повинна зафіксувати 180 мютексів, щоб виконати свою роботу :-)


1
Так, але наскільки ефективно? Це одна інструкція з машини? Або про 10? Або близько 100? 1000? Більше? Все це все ще ефективно, проте може змінити ситуацію в екстремальних ситуаціях.
Альберт

1
Ну, це повністю залежить від реалізації. Ви можете вимкнути переривання, перевірити / встановити ціле число та повторно активувати переривання в циклі приблизно в шістьох інструкціях машини. Тест і встановлення можна зробити приблизно стільки ж, оскільки процесори, як правило, надають це як єдину інструкцію.
paxdiablo

Тест-набір із заблокованою шиною - це єдина (досить довга) інструкція щодо x86. Інша техніка для його використання досить швидка ("чи вдався тест?" - питання про те, що процесори добре роблять швидко), але дійсно важлива довжина інструкції, заблокованої шиною, оскільки це частина, яка блокує речі. Рішення з перериваннями набагато повільніше, оскільки маніпулювання ними, як правило, обмежується ядром ОС, щоб зупинити тривіальні DoS-атаки.
стипендіати Доналу

BTW, не використовуйте drop / recquire як засіб для отримання потоку потоку для інших; це стратегія, яка висмоктує багатоядерну систему. (Це одна з порівняно небагатьох речей, що CPython помиляється.)
Дональні стипендії

@Donal: Що ви маєте на увазі під падінням / повторним придбанням? Це звучить важливо; чи можете ви дати мені більше інформації про це?
Альберт

5

Я абсолютно новачок у pthreads та mutex, але за допомогою експерименту можу підтвердити, що вартість блокування / розблокування мютексу майже зухвала, коли немає суперечок, але коли є суперечки, вартість блокування надзвичайно висока. Я запустив простий код з пулом потоків, в якому завдання полягало лише в тому, щоб обчислити суму в глобальній змінній, захищеній замком mutex:

y = exp(-j*0.0001);
pthread_mutex_lock(&lock);
x += y ;
pthread_mutex_unlock(&lock);

За допомогою одного потоку програма підсумовує 10 000 000 значень практично миттєво (менше однієї секунди); з двома потоками (на MacBook з 4 ядрами) одна і та ж програма займає 39 секунд.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.