Якщо реєстри настільки бурхливі, чому їх у нас не більше?


88

У 32-бітній версії ми мали 8 реєстрів загального призначення. З 64 бітами сума подвоюється, але, здається, це не залежить від самих 64 бітних змін.
Тепер, якщо регістри такі швидкі (відсутність доступу до пам'яті), чому їх природно не стає більше? Чи не слід розробникам процесорів працювати якомога більше реєстрів у ЦП? Яке логічне обмеження щодо того, чому ми маємо лише ту суму, яку маємо?


Процесори та графічні процесори приховують затримку переважно за допомогою кеш-пам’яті та масивної багатопоточності відповідно. Отже, центральні процесори мають (або потребують) мало регістрів, тоді як графічні процесори мають десятки тисяч регістрів. Див. Мій опитування щодо файлу реєстру GPU, де обговорюються всі ці компроміси та фактори.
user984260

Відповіді:


119

Є багато причин, чому у вас не просто величезна кількість реєстрів:

  • Вони тісно пов’язані з більшістю стадій трубопроводу. Для початку вам потрібно відстежувати тривалість їхнього життя і пересилати результати назад на попередні етапи. Складність стає дуже складною, і кількість задіяних проводів (буквально) зростає з однаковою швидкістю. Це дорого за площею, що в кінцевому рахунку означає, що це дорого за потужністю, ціною та продуктивністю після певного моменту.
  • Це займає простір кодування інструкцій. 16 регістрів займають 4 біти для джерела та пункту призначення, і ще 4, якщо у вас є 3-операндні інструкції (наприклад, ARM). Це надзвичайно багато місця для кодування набору команд, зайнятого лише для того, щоб вказати регістр. Це врешті-решт впливає на декодування, розмір коду і знову ж складність.
  • Є кращі способи досягти того самого результату ...

У наші дні у нас дійсно багато реєстрів - вони просто явно не запрограмовані. У нас є "перейменування реєстру". Хоча ви отримуєте доступ лише до невеликого набору (8-32 регістри), вони насправді підтримуються набагато більшим набором (наприклад, 64-256). Потім центральний процесор відстежує видимість кожного реєстру і розподіляє їх до перейменованого набору. Наприклад, ви можете завантажувати, модифікувати, а потім зберігати в реєстрі багато разів поспіль, і кожну з цих операцій фактично виконувати незалежно залежно від пропусків кешу тощо. В ARM:

ldr r0, [r4]
add r0, r0, #1
str r0, [r4]
ldr r0, [r5]
add r0, r0, #1
str r0, [r5]

Ядра Cortex A9 перейменовують регістри, тому перше завантаження до "r0" насправді надходить до перейменованого віртуального реєстру - назвемо його "v0". Навантаження, збільшення та збереження відбуваються на "v0". Тим часом ми також виконуємо завантаження / модифікацію / збереження до r0 знову, але це буде перейменовано на "v1", оскільки це цілком незалежна послідовність із використанням r0. Скажімо, навантаження від вказівника в "r4" зупинилося через пропуск кешу. Це нормально - нам не потрібно чекати, поки "r0" буде готовий. Оскільки вона перейменована, ми можемо запустити наступну послідовність з "v1" (також відображеною на r0) - і, можливо, це потрапило в кеш, і ми щойно отримали величезну перемогу в продуктивності.

ldr v0, [v2]
add v0, v0, #1
str v0, [v2]
ldr v1, [v3]
add v1, v1, #1
str v1, [v3]

Я думаю, що x86 до сьогоднішнього дня переростає гігантську кількість перейменованих реєстрів (стадіон 256). Це означало б мати 8 розрядів по 2 для кожної інструкції, щоб просто сказати, що таке джерело та пункт призначення. Це суттєво збільшило б кількість проводів, необхідних у сердечнику, і його розмір. Отож, навколо 16–32 реєстрів є чудова пляма, на яку погодились більшість дизайнерів, і для непрацюючих конструкцій процесорів перейменування регістрів - це спосіб пом’якшити його.

Змінити : Важливість виконуваного в порядку замовлення та перейменування реєстру щодо цього. Коли у вас є ТОВ, кількість реєстрів не має такого великого значення, оскільки вони є просто "тимчасовими тегами" і перейменовуються у набагато більший набір віртуальних регістрів. Ви не хочете, щоб число було занадто малим, тому що стає важко писати маленькі послідовності коду. Це проблема для x86-32, оскільки обмежені 8 регістрів означають, що в кінцевому підсумку безліч тимчасових пристроїв проходить через стек, і ядру потрібна додаткова логіка для пересилання зчитувань / записів в пам'ять. Якщо у вас немає ТОВ, ви, як правило, говорите про невелике ядро, і в цьому випадку великий набір регістрів - це низька вигода / ефективність.

Отже, є природне солодке місце для розміру банку реєстрів, яке становить близько 32 архітектурних регістрів для більшості класів процесорів. x86-32 має 8 регістрів, і це, безумовно, замало. ARM мав 16 реєстрів, і це хороший компроміс. 32 реєстрів - це занадто багато, якщо що - останні 10 вам не потрібні або близько того.

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


12
Відмінна відповідь - я хотів би додати ще одну причину до поєднання - чим більше реєстрів, тим більше часу потрібно, щоб викинути їх на / витягнути з стеку при перемиканні контексту. Безумовно, не головне питання, а розгляд.
Буде

7
@WillДобре. Однак архітектури з великою кількістю регістрів мають способи зменшити цю вартість. Зазвичай ABI зберігає виклик більшості регістрів, тому вам потрібно зберегти лише базовий набір. Переключення контексту зазвичай досить дороге, щоб додаткове збереження / відновлення не коштувало багато в порівнянні з усіма іншими тяганинами. SPARC насправді працює навколо цього, роблячи банк реєстрів "вікном" в області пам'яті, тому він дещо масштабується з цим (на зразок ручного розмахування).
Джон Ріплі,

4
Вважайте, що мій розум вражений такою ґрунтовною відповіддю, якої я точно не очікував. Крім того, дякую за це пояснення, чому нам насправді не потрібно так багато іменованих реєстрів, це дуже цікаво! Мені дуже сподобалось прочитати вашу відповідь, тому що мені цілком цікаво, що відбувається "під капотом". :) Я зачекаю ще трохи, перш ніж приймати відповідь, тому що ви ніколи не знаєте, але мій +1 впевнений.
Xeo,

1
незалежно від того, де відповідальність за збереження реєстрів, лежить на адміністративному витраті. Добре, тому переключення контексту може бути не найчастішим випадком, але переривання є. Підпрограми, кодовані вручну, можуть економити на регістрах, але якщо драйвери записані на мові C, існує ймовірність того, що функція, оголошена про переривання, збереже кожен окремий регістр, викликає isr, а потім відновлює всі збережені регістри. IA-32 мав перевагу в перериваннях завдяки своїм 15-20 регістром у порівнянні з 32 + дещо реєструючими архітектур RISC.
Олоф Форшелл

1
Відмінна відповідь, але я не погоджусь із прямим порівнянням "перейменованих" реєстрів із "справжніми" адресами. На x86-32, навіть з 256 внутрішніми регістрами, ви не можете використовувати більше 8 тимчасових значень, що зберігаються в регістрах, в жодній окремій точці виконання. В основному, перейменування реєстру - це лише цікавий побічний продукт ООЕ, не більше того.
noop

12

Ми чи їх більше

Оскільки майже кожна інструкція повинна вибрати 1, 2 або 3 архітектурно видимі регістри, розширення їх кількості збільшить розмір коду на кілька бітів для кожної інструкції і, таким чином, зменшить щільність коду. Це також збільшує кількість контексту, який потрібно зберегти як стан потоку та частково зберегти в записі активації функції . Ці операції трапляються часто. Блокування трубопроводів повинно перевіряти табло для кожного реєстру, і це має квадратичну складність у часі та просторі. І, мабуть, найбільшою причиною є просто сумісність з уже визначеним набором інструкцій.

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

Приклад:

load  r1, a  # x = a
store r1, x
load  r1, b  # y = b
store r1, y

В архітектурі, яка має лише r0-r7, наступний код може бути автоматично переписаний процесором як щось на зразок:

load  r1, a
store r1, x
load  r10, b
store r10, y

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


2

Вони постійно додають регістри, але вони часто прив'язані до спеціальних інструкцій (наприклад, SIMD, SSE2 тощо) або вимагають компіляції до певної архітектури центрального процесора, що знижує портативність. Існуючі інструкції часто працюють над конкретними регістрами і не могли скористатися перевагами інших реєстрів, якщо вони були доступні. Спадковий набір інструкцій і все.


1

Щоб додати сюди трохи цікавої інформації, ви помітите, що наявність 8 однакових за розміром регістрів дозволяє кодам операцій підтримувати узгодженість із шістнадцятковими позначеннями. Наприклад, інструкція push axє кодом opx 0x50 на x86 і піднімається до 0x57 для останнього реєстру di. Потім інструкція pop axпочинається з 0x58 і піднімається до 0x5F, pop diщоб завершити першу базу-16. Шістнадцяткова послідовність підтримується за допомогою 8 регістрів на розмір.


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