Як одна нитка працює на декількох ядрах?


61

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

На основі мого читання Hyper-Threading , здається, ОС організовує вказівки всіх потоків таким чином, що вони не чекають один одного. Тоді лицьовий процесор додатково впорядковує ці вказівки, розподіляючи по одному потоку в кожне ядро, і розподіляє незалежні інструкції з кожного потоку серед будь-яких відкритих циклів.

Тож якщо є лише один потік, тоді ОС не буде робити жодної оптимізації. Однак передній процесор буде розподіляти незалежні набори інструкцій серед кожного ядра.

Відповідно до https://stackoverflow.com/a/15936270 , певна мова програмування може створювати більш-менш потоки, але це не має значення при визначенні того, що робити з цими потоками. ОС і процесор справляються з цим, тому це відбувається незалежно від мови програмування, що використовується.

введіть тут опис зображення

Просто для уточнення я запитую про один потік, що проходить через декілька ядер, а не про запуск декількох потоків на одне ядро.

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


6
Набір інструкцій для одного програмного потоку може працювати на багатьох ядрах, але не відразу.
Кролтан

1
Ви змішуєте програмні потоки (які включають планувальник ОС) та потоки апаратних засобів або HyperThreading (функція ЦП, яка змушує одне ядро ​​вести себе як два).
угорен

2
У мене 20 водіїв та 4 вантажівки. Як можливо, що один водій може доставити пакети з двома вантажівками? Як можливо, що одна вантажівка може мати кілька водіїв? Відповідь на обидва запитання однакова. По черзі.
Ерік Ліпперт

Відповіді:


84

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

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

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

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

Належність процесора - це ефективність роботи. Кожен раз, коли процесор запускає інший потік, ніж раніше, він, як правило, трохи сповільнюється, оскільки кеш-пам'ять є теплою для попереднього потоку, але холодною для нової. Таким чином, запуск однієї і тієї ж нитки на одному процесорі протягом численних часових відрізків є перевагою ефективності.

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

Hyperthreading - це метод апаратного забезпечення, за допомогою якого одне розширене ядро CPU може підтримувати виконання двох або більше різних потоків одночасно. (Такий процесор може запропонувати додаткові потоки за меншою вартістю в кремнієвій нерухомості, ніж додаткові повні ядра.) Це розширене ядро ​​CPU має підтримувати додатковий стан для інших потоків, таких як регістрові значення процесора, а також має стан координації та поведінку, що дозволяє обмінюватися функціональними підрозділами в цьому процесорі, не плутаючи потоки.

Hyperthreading, хоча технічно складна з апаратної точки зору, з точки зору програміста, модель виконання - це лише модель додаткових ядер процесора, а не щось складніше. Таким чином, операційна система бачить додаткові ядра процесора, хоча є деякі нові проблеми спорідненості з процесором, оскільки декілька гіпертокових потоків обмінюються архітектурою кешу ядра одного процесора.


Ми можемо наївно подумати, що дві нитки, що працюють на гіпертребованому ядрі, кожен запускаються вдвічі швидше, ніж у кожного з власним повним ядром. Але це необов'язково так, оскільки виконання однієї нитки заповнене слабкими циклами, і деяка їх кількість може бути використана іншим потоком з гіперпотоком. Крім того, навіть під час нерозбійних циклів одна нитка може використовувати різні функціональні одиниці, ніж інша, тому може відбуватися одночасне виконання. Удосконалений процесор для гіперточення може мати ще кілька певних сильно використовуваних функціональних блоків, спеціально для підтримки цього.


3
"Таким чином, запуск однієї нитки на одному і тому ж процесорі протягом численних відрізків часу є перевагою ефективності." Хіба це не повинно бути суміжними відрізками часу? Інакше кеші будуть витерті іншими потоками, ні? +1 для приємного пояснення.
jpmc26

2
@Luaan: HT часто хороший, але ситуація не така проста, як ви описуєте. Пропускна здатність випуску на передньому кінці (4 уп на годину в Intel, 6 на Ryzen) однаково розподіляється між потоками (якщо тільки одна не зупиняється). Якщо це вузьке місце, то, як я вже сказав, HT зовсім не допоможе. Не рідкість Skylake наблизитися до цього в добре налаштованому циклі, якщо є суміш навантажень, ALU і магазинів ... Транзистори дешеві (і не вдається все перемикатися одразу або процесор розтане), тому сучасні x86 процесори мають більше портів виконання , ніж передній кінець може годувати (з великою кількістю виконавчих блоків тиражуються ...
Пітер Кордес

2
... на кількох портах) ... Це може здатися марним, але часто цикл використовує одночасно лише один вид виконавчого блоку ALU, тому наявність дублікатів всього означає, що який би код не працював, існує кілька порти за його вказівками. Тож причина, яку ви наводили на користь HT, не така поширена, оскільки більшість кодів має деякі навантаження та / або магазини, що займають пропускну здатність переднього кінця, і того, що залишилося, часто недостатньо для насичення одиниць виконання.
Пітер Кордес

2
@Luaan: Також в процесорних процесорах Intel цілі і FP / векторні одиниці виконання мають спільні порти виконання . Наприклад, модулі FP FMA / mul / add знаходяться на портах 0/1. Але цілочисельний множник також знаходиться на port1, і прості цілі операційні опції можуть працювати на будь-якому з 4 портів виконання (схема в моїй відповіді). Другий потік, що використовує пропускну здатність випуску, сповільнить їх обох, навіть якщо вони не змагаються за одиниці виконання, але часто є чистий приріст пропускної здатності, якщо вони не надто сильно змагаються за кеш. Навіть добре налаштований високопропускний код, наприклад x264 / x265 (кодери відео), отримує близько 15% від Skylake від HT.
Пітер Кордес

3
@luaan Окрім того, що Петро сказав, ваше твердження, що "Це були первісні міркування HT", є невірним. Первісна аргументація HT полягала в тому, що мікроархітектура NetBurst надовжила трубопровід настільки екстремально (з метою збільшення тактової частоти), що гілкові прогнози та інші бульбашки трубопроводу абсолютно знищили продуктивність. HT було одним із рішень Intel, щоб мінімізувати кількість часу, коли цей великий дорогий чіп-чіп сидів бездіяльним через бульбашки в конвеєрі: код з інших потоків можна було вставити і запустити в ці отвори.
Коді Грей

24

Не існує такого поняття, як одна нитка, що працює на декількох ядрах одночасно.

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

Hyper-Threading - це настільки екстремальний варіант цієї ідеї, в якому одне ядро ​​не тільки паралельно виконує вказівки з однієї нитки, але й змішує інструкції з двох різних потоків, щоб ще більше оптимізувати використання ресурсів.

Пов’язані записи у Вікіпедії: Інструкція щодо конвеєра , виконання поза замовленням .


3
Вони не можуть працювати одночасно, але вони можуть працювати паралельно? Це не одне і те ж?
Еворлор

10
@Evorlor Основна річ тут - різниця між ядром та одиницею виконання. Один потік може працювати лише на одному ядрі, але процесор може використовувати динамічний аналіз, щоб визначити, які вказівки, виконані ядром, не залежать один від одного і виконувати їх на різних одиницях виконання одночасно. Одне ядро ​​може мати кілька одиниць виконання.
користувач1937198

3
@Evorlor: ЦП, що не працює в порядку, може знайти та використовувати паралелізм рівня інструкцій у потоці інструкцій одного потоку. наприклад, часто інструкції, що оновлюють лічильник циклу, не залежать від деяких інших робіт, які виконує цикл. Або в a[i] = b[i] + c[i]циклі кожна ітерація є незалежною, тому завантаження, додавання та зберігання з різних ітерацій можуть бути в польоті відразу. Слід зберегти ілюзію, що вказівки, виконані в програмному порядку, але, наприклад, магазин, який не вистачає в кеші, не затримує потік (поки в буфері магазину не вистачить місця).
Пітер Кордес

3
@ user1937198: Фраза "динамічний аналіз" краще підійде для компілятора JIT. Процесори поза замовленням насправді не аналізують; це скоріше схожий на жадібний алгоритм, який виконує будь-які інструкції, які були розшифровані та видані, і готові їх введення. (Вікно впорядкування поза замовленням обмежене кількома мікроархітектурними ресурсами, наприклад, Intel Sandybridge має розмір буфера ReOrder розміром 168 уп. Дивіться також експериментальне вимірювання розміру ROB ). Всі вони реалізовані з апаратними станковими машинами для обробки 4 уп на годину.
Пітер Кордес

3
@Luaan Так, це була цікава ідея, але компілятори AOT все ще не достатньо розумні, щоб повністю її використати. Також Лінус Торвальдс (та інші) стверджували, що виявлення того, що значна частина внутрішніх трубопроводів є великим обмеженням для майбутніх проектів. наприклад, ви не можете реально збільшити ширину трубопроводу без зміни ISA. Або ви створюєте процесор, який відстежує залежності звичайним способом, і, можливо, випускає дві групи VLIW паралельно, але тоді ви втратили перевагу EPIC-складності EPIC, але все-таки є недоліки (втрачена пропускна здатність проблеми, коли компілятор не може заповнити слово).
Пітер Кордес

22

Короткий зміст: Пошук та використання паралелізму (рівня інструкцій) в однопотоковій програмі виконується виключно апаратно, ядром процесора, на якому він працює. І лише над вікном пару сотень інструкцій, а не масштабне упорядкування.

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


ОС організовує вказівки всіх потоків таким чином, щоб вони не чекали один одного.

ОС НЕ зазирає в потоки інструкцій потоків. Він тільки планує потоки до ядер.

Насправді кожне ядро ​​виконує функцію планувальника ОС, коли йому потрібно розібратися, що робити далі. Планування - це розподілений алгоритм. Щоб краще зрозуміти багатоядерні машини, подумайте про кожне ядро ​​як про запуск ядра окремо. Як і багатопотокова програма, ядро ​​написано так, що його код на одному ядрі може безпечно взаємодіяти зі своїм кодом на інших ядрах для оновлення спільних структур даних (наприклад, список потоків, які готові запустити.

У будь-якому випадку, ОС бере участь у допомаганні багатопотокових процесів у використанні паралелізму на рівні потоку, який повинен бути чітко розкритий ручним написанням багатопотокової програми . (Або за допомогою автоматичного паралельного компілятора з OpenMP або чогось іншого).

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

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

За допомогою HyperThreading або інших SMT-конструкцій фізичне ядро ​​процесора діє як кілька "логічних" ядер. Єдина відмінність з точки зору ОС між процесором чотирьохядерного з гіпертритуванням (4c8t) та звичайною 8-ядерною машиною (8c8t) полягає в тому, що ОС, що знає HT, намагатиметься запланувати потоки для розділення фізичних ядер, щоб вони не ставали ' t конкурувати між собою. ОС, яка не знала про гіпертодування, побачила б лише 8 ядер (якщо ви не відключите HT в BIOS, тоді він виявить лише 4).


Термін " фронтальний" позначає частину ядра ЦП, яка отримує машинний код, розшифровує інструкції та передає їх у частину ядра поза замовленням . Кожне ядро ​​має власний фронт-енд, і це частина ядра в цілому. Інструкції, які він отримує, - це те, що процесор працює зараз.

Всередині основної частини ядра, що не в порядку, вказівки (або Uops) надсилаються до портів виконання, коли їх вхідні операнди готові і є вільний порт виконання. Це не повинно відбуватися в програмному порядку, тому таким чином CPU OOO може використовувати паралелізм рівня інструкцій в одному потоці .

Якщо ви заміните "core" на "блок виконання" у своїй ідеї, ви близькі до виправлення. Так, CPU паралельно розподіляє незалежні інструкції / uops до одиниць виконання. (Але є змішання термінології, оскільки ви сказали "передовий", коли насправді графік інструкцій процесора aka Reservation Station підбирає інструкції, готові до виконання).

Виконання поза замовленням може знаходити ILP лише на дуже локальному рівні, лише до декількох сотень інструкцій, а не між двома незалежними циклами (якщо вони короткі).


Наприклад, еквівалент asm цього

int i=0,j=0;
do {
    i++;
    j++;
} while(42);

працюватиме так само швидко, як і той самий цикл, збільшуючи лише один лічильник на Intel Haswell. i++залежить лише від попереднього значення i, тоді j++як залежить лише від попереднього значення j, тому два ланцюги залежностей можуть працювати паралельно, не порушуючи ілюзії щодо всього, що виконується в програмному порядку.

На x86 цикл виглядатиме приблизно так:

top_of_loop:
    inc eax
    inc edx
    jmp .loop

Haswell має 4 цілочинні порти виконання, і всі вони мають одиниці додавання, тому він може підтримувати пропускну здатність до 4 incінструкцій на годинник, якщо всі вони незалежні. (З затримкою = 1, тому вам потрібно лише 4 регістри для максимальної пропускної здатності, зберігаючи 4 incінструкції в польоті. Контрастуйте це вектору-FP MUL або FMA: затримка = 5 пропускна здатність = 0,5 потрібно 10 векторних акумуляторів, щоб утримувати 10 FMA в польоті щоб досягти максимальної пропускної здатності. І кожен вектор може бути 256b, утримуючи 8 одноточних плавців).

Знята гілка також є вузьким місцем: цикл завжди займає щонайменше один цілий годинник за ітерацію, оскільки пропускна здатність взятої гілки обмежена 1 на такт. Я міг би поставити ще одну інструкцію всередині циклу, не знижуючи продуктивність, якщо вона також не читає / записує eaxабо edxв такому разі це подовжить цю ланцюг залежності. Якщо вставити ще дві інструкції в цикл (або одну складну мульти-загальну інструкцію), це створить вузьке місце на передній панелі, оскільки воно може видавати лише 4 уопи за годину в ядро ​​поза замовленням. (Дивіться цю запитання і відповіді, щоб отримати детальну інформацію про те, що відбувається для циклів, які не є кратними 4 уп: циклічний буфер і загальний кеш роблять речі цікавими.)


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

Ємність буфера для повторного замовлення є одним із факторів, що обмежує розмір вікна поза замовленням. У Intel Haswell це 192 уп. (І ви навіть можете експериментально виміряти його , разом з ємністю для перейменування регістра (розмір регістрового файлу).) Ядра CPU низької потужності на зразок ARM мають значно менші розміри ROB, якщо вони взагалі виконуються поза замовленням.

Також зауважте, що центральні процесори потребують конвеєрного руху, а також виходу з ладу. Таким чином, він повинен отримувати та декодувати інструкції заздалегідь від тих, що виконуються, бажано з достатньою пропускною здатністю для поповнення буферів після пропуску будь-яких циклів отримання. Гілки складні, тому що ми не знаємо, звідки навіть взяти, якщо ми не знаємо, яким шляхом пішла гілка. Ось чому галузеве передбачення є таким важливим. (І чому сучасні процесори використовують спекулятивне виконання: вони здогадуються, в який бік піде гілка, і починають витягувати / декодувати / виконувати цей потік інструкцій. Коли буде виявлено неправильне прогнозування, вони повертаються до останнього відомого стану та виконують звідти.)

Якщо ви хочете прочитати докладніше про внутрішні процесорні версії, у вікі тегів Stackoverflow x86 є деякі посилання , включаючи посібник з мікроарха Agner Fog та докладні описи Девіда Кантера з діаграмами процесорів Intel та AMD. З його написання мікроархітектури Intel Haswell , це остаточна схема всього трубопроводу ядра Haswell (не всієї мікросхеми).

Це блок-схема одного ядра процесора . У чотирьохядерному процесорі є 4 з них на чіпі, кожен з яких має власні кеші L1 / L2 (спільний доступ до кешу L3, контролерів пам'яті та підключень PCIe до системних пристроїв).

Повний конвеєр Хасвелл

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


2
"Пошук і використання паралелізму (рівня інструкцій) в однопотоковій програмі виконується виключно апаратно" Зауважте, що це стосується лише звичайних ISA, а не VLIW, в яких ILP повністю визначається компілятором або програмістом, або спільно між апаратними засобами та програмне забезпечення.
Хаді Браїс

1
@ user7813604: так. Hyperthreading не може паралелізувати одну нитку. Це робить зворотне: він працює на декількох потоках на одному ядрі, знижуючи продуктивність на одну нитку, але збільшуючи загальну пропускну здатність.
Пітер Кордес

1
@ user7813604: Вся суть ILP полягає у пошуку, які вказівки можна виконувати паралельно, зберігаючи ілюзію, що кожна інструкція працює в порядку, кожна закінчується перед початком наступної. Скалярному конвеєрному процесору іноді може знадобитися затримуватися залежність, якщо затримка перевищує 1. Але це ще більша угода для суперскалярних процесорів.
Пітер Кордес

1
@ user7813604: так, моя відповідь буквально використовує це як приклад. Наприклад, Haswell може виконувати до 4 incінструкцій за той самий тактовий цикл до своїх 4 цілих одиниць виконання ALU.
Пітер Кордес

1
@ user7813604: Так, ILP - це скільки можна виконати паралельно. Справжній процесор матиме обмежену здатність знаходити та використовувати ILP, фактично запускаючи його паралельно в одному ядрі, наприклад, до 4-х широкого суперскаляра в Intel. Ця відповідь намагається пояснити це на прикладах.
Пітер Кордес
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.