Короткий зміст: Пошук та використання паралелізму (рівня інструкцій) в однопотоковій програмі виконується виключно апаратно, ядром процесора, на якому він працює. І лише над вікном пару сотень інструкцій, а не масштабне упорядкування.
Однопотокові програми не отримують користі від багатоядерних процесорів, за винятком того, що інші речі можуть працювати на інших ядрах, замість того, щоб забирати час від однопотокового завдання.
ОС організовує вказівки всіх потоків таким чином, щоб вони не чекали один одного.
ОС НЕ зазирає в потоки інструкцій потоків. Він тільки планує потоки до ядер.
Насправді кожне ядро виконує функцію планувальника ОС, коли йому потрібно розібратися, що робити далі. Планування - це розподілений алгоритм. Щоб краще зрозуміти багатоядерні машини, подумайте про кожне ядро як про запуск ядра окремо. Як і багатопотокова програма, ядро написано так, що його код на одному ядрі може безпечно взаємодіяти зі своїм кодом на інших ядрах для оновлення спільних структур даних (наприклад, список потоків, які готові запустити.
У будь-якому випадку, ОС бере участь у допомаганні багатопотокових процесів у використанні паралелізму на рівні потоку, який повинен бути чітко розкритий ручним написанням багатопотокової програми . (Або за допомогою автоматичного паралельного компілятора з 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 до системних пристроїв).
Я знаю, що це надзвичайно складно. Стаття Кантера також показує частини цього, щоб розповісти про фронтенд окремо від блоків виконання або кешів, наприклад.