<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[course-main]]></title><description><![CDATA[Obsidian digital garden]]></description><link>http://github.com/dylang/node-rss</link><image><url>site-lib/media/favicon.png</url><title>course-main</title><link/></image><generator>Webpage HTML Export plugin for Obsidian</generator><lastBuildDate>Wed, 22 Apr 2026 12:48:02 GMT</lastBuildDate><atom:link href="site-lib/rss.xml" rel="self" type="application/rss+xml"/><pubDate>Wed, 22 Apr 2026 12:48:00 GMT</pubDate><ttl>60</ttl><dc:creator/><item><title><![CDATA[Preparation-Guide]]></title><description><![CDATA[Дисципліна: Архітектура комп'ютерних систем (6 ECTS / 4 ECTS)Повний обсяг підготовки до кожного з 45 екзаменаційних питань, а також ймовірні додаткові запитання екзаменатора. Використовуйте як довідник для глибокої підготовки.
Офіційні формулювання питань — <a class="internal-link" data-href="../Exam-Questions.md" href="exam-questions.html" target="_self" rel="noopener nofollow">Exam-Questions.md</a>. Цей файл дублює формулювання кожного питання для зручності, але джерело правди — саме там.
<br>Регламент, критерії, умови допуску — <a class="internal-link" data-href="../Exam.md" href="exam.html" target="_self" rel="noopener nofollow">Exam.md</a>.
Стислі ядра відповідей по лекціях — файли L##-Prep.md у цій теці (лінки в таблицях нижче).
Пункти «Обсяг підготовки» називають що підготувати, а не що сказати. Кожен пункт вказує на тему, артефакт (схему, формулу, розроблений приклад) чи порівняння, яке відповідь повинна містити, — але конкретний зміст (числа, імена, виведення) має походити з власної підготовки студента. Переказ пунктів не є відповіддю. «Ймовірні додаткові запитання» — приклади уточнень, якими екзаменатор перевіряє справжнє розуміння поза завченим планом.На іспиті білет містить одне питання з Блоку I + одне з Блоку II, витягуються випадково. Колонка «Питання» веде до повного обсягу підготовки в цьому файлі; колонка «Ядро» — до стислого ядра відповіді у файлі відповідної лекції.Блок I — Основи (L01–L07), 21 питання. Фізичні основи, цифрова логіка, архітектура x86, системна архітектура фон Неймана, взаємодія з ОС.Блок II — Системи та продуктивність (L08–L15), 24 питання. Технології пам'яті та введення-виведення, проєктування ISA, сучасні архітектури, ієрархія кеша, віртуальна пам'ять, IEEE 754, паралелізм, енергія.
Поясніть, чому сучасні обчислювальні системи використовують саме два стани. Охопіть завадостійкість на якісному рівні та зв'язок з булевою алгеброю.
Обсяг підготовки:
Поясніть три причини перемоги двійкової системи: завадостійкість через заборонену зону напруги, проста реалізація перемикання та булева алгебра як готовий математичний фундамент.
Продемонструйте, що довільну булеву логіку можна фізично реалізувати з мінімального набору вентилів (універсальність NAND), побудувавши NOT, AND і OR лише з NAND.
Проведіть зв'язок з результатом Шеннона 1937 року, який поєднав булеву алгебру з комутаційними схемами.
Ймовірні додаткові запитання: Чому транзистор для цифрової логіки працює в режимі відсічення або насичення, а не в лінійній області? Що сталося б, якби система намагалася розрізняти, скажімо, 10 рівнів напруги на одному проводі замість 2? Назвіть фізичне обмеження, яке накладає межу на будь-який двійковий комп'ютер (швидкість світла, тепловий шум або принцип Ландауера). LO: LO1, PR1 Опишіть транзистор як керований перемикач і зв'яжіть його з вентилем NAND.
Обсяг підготовки:
Опишіть транзистор як керований перемикач: маленький керуючий вхід (напруга на базі/затворі) вмикає або вимикає більший струмовий шлях.
Назвіть три режими роботи цифрового транзистора (відсічення, насичення та невикористовувана лінійна область) і поясніть, чому застосовуються лише два.
Наведіть таблицю істинності вентиля NAND та поясніть його універсальність, побудувавши NOT, AND і OR лише з NAND.
Оцініть порядок величини кількості транзисторів у сучасному ЦП (мільярди), щоб поєднати проєктування вентилів з інтеграцією на кристалі.
Ймовірні додаткові запитання: Чому саме NAND обрано як базовий вентиль, а не окрема побудова AND і NOT? Скільки приблизно транзисторів у сучасному ЦП (порядок величини)? Чому цифровий транзистор не працює в лінійній (підсилювальній) області? LO: LO1 Опишіть ключові стрибки в технології обчислень від механічних машин до сучасної VLSI. Поясніть, що рухало кожен стрибок.
Обсяг підготовки:
Пройдіть ключові технологічні віхи по порядку (механічні → електромеханічні → вакуумні лампи → дискретні транзистори → інтегральні схеми → VLSI) і назвіть принаймні одну знакову машину на кожну епоху.
Наведіть конкретні числа ENIAC (кількість ламп, операцій за секунду, вагу, потужність) і зіставте їх з будь-яким сучасним електронним комп'ютером.
Визначте спільний рушій усіх стрибків: кожен крок замінює вручну зібрані частини інтегрованим або автоматизованим виробництвом.
Ймовірні додаткові запитання: Чому транзистор витіснив вакуумну лампу, і що не змінилося відразу після переходу? Хто винайшов транзистор і де? Які проблеми з вакуумними лампами зробили їх непридатними для подальшого масштабування? LO: LO1, PR1 Порівняйте комбінаційну та послідовну логіку з прикладами схем. Поясніть роль тактового сигналу.
Обсяг підготовки:
Наведіть формальне розмежування та будьте готові класифікувати будь-яку схему, показану екзаменатором.
Намалюйте щонайменше дві приклади схем кожного класу.
Поясніть, що ламається, коли з послідовної системи прибрати тактовий сигнал; проілюструйте конкретним сценарієм стану гонки.
Протиставте рівень-чутливі засувки та тригери з керуванням по фронту; поясніть, чому конструкція «master-slave» усуває проблему прозорості.
Ймовірні додаткові запитання: Чи споживає комбінаційна логіка енергію лише під час перемикань, чи й у стані спокою? Обґрунтуйте на моделі CMOS. Чому утримання входу Enable D-засувки у високому рівні спричиняє поведінку «лічильника-втікача», і як тригер D-типу з керуванням по фронту це виправляє? У тригері D-типу «master-slave» який каскад захоплює на передньому фронті, а який — на задньому, і чому порядок важливий? LO: LO1 Порівняйте три двійкові представлення знакових цілих чисел: прямий код зі знаком, обернений (доповняльний до одиниці) код та додатковий (доповняльний до двох) код. Покажіть, чому переміг додатковий код.
Обсяг підготовки:
Запишіть додатні та від'ємні цілі числа у всіх трьох форматах при фіксованій розрядності.
Порівняйте три формати за діапазоном, представленням нуля та додаванням — з розробленим арифметичним прикладом.
Поясніть апаратну причину, чому додатковий код дозволяє одному суматору обслуговувати як знакові, так і беззнакові операнди.
Наведіть процедуру заперечення в додатковому коді та обґрунтуйте її алгебраїчно.
Ймовірні додаткові запитання: Який діапазон 8-бітного цілого числа в додатковому коді, і чому він асиметричний? Як зазвичай реалізується віднімання на апаратному рівні? Відповідайте з конкретним прийомом на рівні вентилів. Що відбувається при переповненні в додатковому коді, і як ЦП сигналізує про нього програмі? LO: LO2 Опишіть, як будується примітивне АЛП з комбінаційних блоків. Покажіть, як коди операцій стають сигналами керування.
Обсяг підготовки:
Намалюйте блок-схему АЛП із входами даних, входом керування (код операції), виходом результату та виходами прапорців.
Поясніть, як один спільний суматор обслуговує і ADD, і SUB, назвавши комбінаційний прийом, що це уможливлює.
Покажіть відображення «код операції → сигнали керування» щонайменше для двох операцій.
Зв'яжіть цей примітивний блок з роллю декодера та мікрооперацій (µops) у справжньому сучасному ядрі x86.
Ймовірні додаткові запитання: Чому саме суматор є найскладнішим комбінаційним блоком АЛП? Що обмежує його затримку? Як би ви додали операцію MUL — як нову комбінаційну схему чи якось інакше? Чому MUL зазвичай багатотактова? Чому виходи FLAGS роблять АЛП «схожим на такий, що має стан», хоча воно комбінаційне? LO: LO1, LO2 Поясніть роль регістрів у проєктуванні ЦП і чому самої лише ОП недостатньо.
Обсяг підготовки:
Сформулюйте компроміс «швидкість проти кодування» між регістрами та ОП.
Простежте родовід акумулятора від ранніх 8-бітних до сучасних 64-бітних x86.
Поясніть ціну збільшення регістрового файлу — що масштабується з шириною індексу регістра.
Наведіть конкретний приклад виразу, що порівнює регістрове та пам'ять-пам'ять обчислення за трафіком шини.
Ймовірні додаткові запитання: Чому 8086 надав програмісту лише 8 регістрів загального призначення, а не 16 або 32? Що таке префікс REX у кодуванні x86-64 і що він дає? Чому регістровий стек x87 FPU має лише 8 рівнів, і яку проєктну проблему це спричиняє? LO: LO1, LO2 Поясніть адресацію 8086 у реальному режимі: як 20-бітна фізична адреса формується з 16-бітного сегмента та 16-бітного зміщення.
Обсяг підготовки:
Сформулюйте правило формування адреси та застосуйте його до пари «сегмент:зміщення», наданої екзаменатором.
Продемонструйте на прикладі, що дві різні пари «сегмент:зміщення» можуть вказувати на один і той самий фізичний байт.
Визначте принаймні одну архітектурно жорстко задану адресу та обґрунтуйте, чому вона закріплена апаратно, а не умовно.
Назвіть верхню адресовану межу та практичний наслідок, відомий як High Memory Area.
Ймовірні додаткові запитання: Наведіть дві різні пари segment:offset, які вказують на один і той самий фізичний байт. Як 32-бітний захищений режим змінює значення «сегментного регістра»? Чому Intel зберегла сегментацію в 80286 під час переходу до захищеного режиму? LO: LO1, LO4 Порівняйте SRAM та DRAM на рівні комірки й поясніть, де застосовується кожна в сучасній системі.
Обсяг підготовки:
Намалюйте обидві структури комірок на рівні транзисторів.
Порівняйте за швидкістю, щільністю, вартістю та потребою в оновленні з конкретними числами порядку величини.
Поясніть, чому DRAM за своєю природою повільніша та яке обмеження лімітує щільність SRAM.
Розмістіть кожну технологію на її ролі в ієрархії пам'яті.
Ймовірні додаткові запитання: Яка приблизно різниця в затримці між влучанням у L1 і зверненням до DRAM (у наносекундах)? Чому DRAM організована як двовимірний масив із рядковою та стовпчиковою адресацією, а не плоско? Що стається з вмістом DRAM при знятті живлення, і як це контрастує зі SRAM? LO: LO1, LO7 Пройдіть цикл вибірка-декодування-виконання на конкретній інструкції. Поясніть роль IP, декодера, АЛП та FLAGS.
Обсяг підготовки:
Оберіть конкретну інструкцію x86 і простежте її крізь кожну стадію, називаючи активний блок на кожній стадії.
Поясніть, як IP просувається при кодуванні змінної довжини і чому це ускладнює декодування.
Укажіть, які прапорці FLAGS зачіпає обрана інструкція, та порівняйте з арифметичною інструкцією.
Покажіть розрив між цією моделлю «по одній інструкції за раз» і тим, що сучасний x86 робить насправді.
Ймовірні додаткові запитання: Чому декодування x86 настільки складніше за декодування RISC? Що відбувається з IP при умовному переході, який не виконується? На сучасному x86 картина «по одній інструкції за раз» є спрощенням. Що ж насправді відбувається під капотом? LO: LO1, LO3 Покажіть, чому додатковий код дозволяє одній апаратурі обслуговувати знакову та беззнакову арифметику. Поясніть, як установлюється кожен із CF, ZF, SF, OF.
Обсяг підготовки:
Сформулюйте, чому суматор не зважає на знаковість; інтерпретація живе у FLAGS та домовленості програміста.
Пройдіть 8-бітне додавання, що дає цікаву комбінацію прапорців; обчисліть усі чотири прапорці на папері.
Для кожного з CF, ZF, SF, OF наведіть його точне значення при знаковій vs беззнаковій інтерпретації.
Назвіть один наслідок розрізнення знакового/беззнакового на рівні програмування.
Ймовірні додаткові запитання: Для MOV AL, 80h; ADD AL, AL — якими будуть CF, ZF, SF, OF? Пройдіть по бітах. Чому ADD використовують для реалізації SUB на рівні вентилів? Що саме робить прийом «інвертувати та додати 1»? Що таке допоміжний прапорець (AF) і чому він досі існує в ISA x86? LO: LO2 Покажіть, як перевіряти, встановлювати, скидати та перемикати окремі біти за допомогою інструкцій x86. Порівняйте TEST і CMP.
Обсяг підготовки:
Запишіть інструкцію x86 та маску для перевірки, встановлення, скидання й перемикання конкретного біта.
Сформулюйте правило побудови кожного типу маски.
Порівняйте TEST і CMP за виходом у прапорці, розміром кодування та наміром програміста.
Поясніть, чому одна конкретна ідіоматична послідовність обнулення переважає MOV reg, 0 на сучасному x86.
Ймовірні додаткові запитання: У чому різниця між SHL і SAL? Між SHR і SAR? Який саме використовувати для знакового ділення на степені 2? Як виокремити біти 3–7 байта в біти 0–4 (тобто зсунути, потім замаскувати)? Чому LEA іноді використовують для чистої арифметики, а не лише для обчислення адрес? LO: LO2, LO3 Опишіть стандартний стековий фрейм cdecl крок за кроком. Покажіть, як звертатися до аргументів і локальних змінних через BP.
Обсяг підготовки:
Намалюйте стандартний фрейм cdecl зі стеком, що росте у правильному напрямку, та всіма елементами з підписами.
Запишіть асемблерні послідовності як для прологу, так і для епілогу.
Укажіть зміщення від BP для першого аргументу, адреси повернення, збереженого BP та першої локальної змінної.
Обґрунтуйте, чому BP існує як окремий якір на додачу до SP.
Ймовірні додаткові запитання: Що станеться, якщо викликаний забуде відновити SP перед RET? Чому в cdecl саме викликач відповідає за прибирання аргументів? Що змінив би stdcall? Що таке оптимізація «листової функції» (leaf function) і коли безпечно пропустити стандартний пролог? LO: LO3, LO4 Поясніть механістично, чому CALL — це не просто JMP. Обговоріть дисципліну адреси повернення та що може піти не так.
Обсяг підготовки:
Протиставте CALL, RET і JMP механістично: що потрапляє в стек, що змінюється в IP.
Простежте ланцюжок із двох рівнів виклику та покажіть, як кожен RET розгортає правильну адресу повернення.
Опишіть конкретну несправність, коли викликач і викликаний не погоджуються щодо прибирання аргументів, або коли RET виконується з незбалансованим стеком.
Поясніть, чому стек має бути відновлений до глибини, яка була до CALL, перш ніж виконається RET.
Ймовірні додаткові запитання: Що станеться, якщо процедура заштовхує значення в стек і не знімає їх перед RET? Як реалізувати ефект RET, використовуючи лише POP і JMP? Чому x86 використовує адресу повернення в стеку, а не виділений регістр зв'язку, як у RISC ISA? LO: LO3, LO4 Простежте невелику рекурсивну процедуру на стеку. Поясніть, чому глибина рекурсії обмежена.
Обсяг підготовки:
Простежте простий рекурсивний виклик (наприклад, factorial(3)) на папері, малюючи кожен фрейм у міру його заштовхування та зняття.
Укажіть, що глибина рекурсії обмежена розміром стекової області, а не загальною ОП: кожен виклик споживає один фрейм.
Протиставте ітеративний еквівалент за використанням стека: один фрейм проти N фреймів для рекурсії глибиною N.
Ймовірні додаткові запитання: Чому стек росте вниз (до менших адрес) на x86? Що станеться, коли ланцюжок рекурсивних викликів перевищить область стека? Як би ви перетворили глибоко рекурсивну процедуру на ітеративну, щоб не вичерпати стек? LO: LO3 Намалюйте схему фон Неймана. Поясніть вузьке місце та два сучасні способи його пом'якшення.
Обсяг підготовки:
Намалюйте класичну схему та вкажіть спільну шину.
Сформулюйте ідею збереженої програми одним реченням.
Назвіть вузьке місце та вкажіть принаймні два сучасні мікроархітектурні способи його пом'якшення.
Протиставте чисту Гарвардську архітектуру та модифіковану Гарвардську на межі кеша L1.
Ймовірні додаткові запитання: Сучасний x86 ЦП — це фон Нейман чи Гарвард? Розрізніть архітектурний та мікроархітектурний погляди. Чому ідея збереженої програми робить можливим самомодифікований код, і чому від нього тепер відмовляються? Яка роль когерентності кеша інструкцій з кешем даних, і що відбувається з JIT-скомпільованим кодом? LO: LO1, LO4 Упорядкуйте рівні ієрархії пам'яті з порядком величини затримки та ємності. Поясніть, чому існує саме піраміда.
Обсяг підготовки:
Перелічіть рівні згори донизу з реалістичними затримкою та ємністю порядку величини.
Поясніть фізичні та економічні причини, чому ієрархія мусить бути пірамідою.
Визначте розрив у затримці між верхом і низом і зв'яжіть його з ціною неочікуваного промаху.
Поясніть, чому верхівку ієрархії не можна просто розширити до ємності дна.
Ймовірні додаткові запитання: Якщо L1 — 4 такти, а DRAM — 200 тактів, чому не зробити L1 таким великим, як DRAM? При промаху кеша L1 — що визначає, чи потрапляє рядок у L2, чи провалюється до DRAM? Де GPU вписуються в цю ієрархію? Які рівні присутні або відсутні? LO: LO1, LO5 Порівняйте реальний режим 8086 та захищений режим 80286+. Поясніть, чому DOS залишився в реальному режимі, тоді як сучасні ОС перейшли.
Обсяг підготовки:
Опишіть, чим відрізняються адресація, привілеї та захист пам'яті між двома режимами.
Вкажіть покоління ЦП, яке ввело захищений режим.
Поясніть причину DOS залишатися в реальному режимі; поясніть причину, чому сучасним ОС потрібен захищений режим.
Поясніть, як захищений режим уможливлює багатозадачність і запобігає тому, щоб процес-порушник пошкоджував інші.
Ймовірні додаткові запитання: Чому впровадження захищеного режиму в 1980-х–1990-х було повільним? Яку роль у захищеному режимі відіграють GDT і LDT, яку одні лише сегментні регістри виконати не могли? Чому програма в реальному режимі може обвалити всю систему, а користувацький процес у захищеному режимі — зазвичай ні? LO: LO1, LO4 Пройдіть послідовність POST → BIOS → MBR → завантажувач → ядро ОС.
Обсяг підготовки:
Пройдіть послідовність по порядку, називаючи відповідальність кожної стадії.
Вкажіть адресу вектора скидання ЦП та адресу завантаження MBR.
Назвіть обмеження щодо розміру та формату MBR.
Визначте принаймні дві архітектурні «якірні» адреси, що існують до того, як ОС перебирає керування.
Ймовірні додаткові запитання: Що таке UEFI і що він замінює або розширює порівняно з BIOS? Що відбувається на переході від реального до захищеного режиму всередині завантажувача? Чому 512 байтів — це жорстка межа для MBR, і як GRUB її обходить? LO: LO4 Порівняйте апаратні та програмні переривання. Опишіть, що INT заштовхує в стек і чому IRET відрізняється від RET.
Обсяг підготовки:
Порівняйте апаратні та програмні переривання за джерелом спрацювання, часовими характеристиками та типовими прикладами.
Укажіть точно, що саме INT заштовхує в стек і в якому порядку.
Протиставте CALL/RET і INT/IRET; обґрунтуйте, чому ці два механізми не є взаємозамінними.
Назвіть принаймні один сервіс DOS або BIOS, до якого звертаються через програмне переривання.
Ймовірні додаткові запитання: Що станеться, якщо апаратне переривання спрацює в секції, де переривання вимкнено (CLI)? У чому різниця між маскованими (IRQ) та немаскованими (NMI) перериваннями? Чим сучасна x86-інструкція SYSCALL відрізняється від класичної моделі системних викликів через INT 80h? LO: LO4 Поясніть кільця 0–3. Простежте системний виклик від користувацького коду до ядра й назад.
Обсяг підготовки:
Опишіть чотири кільця та які два з них фактично використовуються в сучасних ОС.
Назвіть принаймні одну привілейовану інструкцію та помилку, що виникає при її виконанні в кільці 3.
Простежте системний виклик наскрізь, вказавши переходи між кільцями.
Поясніть, чому сучасні ОС запровадили SYSCALL/SYSENTER як швидші альтернативи системним викликам на основі INT (на концептуальному рівні — називати імена MSR не обов'язково).
Ймовірні додаткові запитання: Чому сучасні ОС згорнулися лише до кілець 0 та 3? Що таке кільце −1, і яка реальна можливість його використовує? Чому системний виклик передбачає обов'язкове перемикання стану ЦП, навіть якщо код ядра — це лише кілька інструкцій? LO: LO4 Порівняйте чотири технології пам'яті за енергозалежністю, швидкістю, щільністю, вартістю та витривалістю. Відобразіть кожну на її системну роль.
Обсяг підготовки:
Побудуйте порівняльну таблицю чотирьох технологій за п'ятьма осями.
Відобразіть кожну технологію на роль, яку вона виконує в сучасному ПК.
Поясніть, чому читання та запис у флеш асиметричні та яке фізичне обмеження змушує виконувати стирання перед записом.
Вкажіть витривалість флеш-пам'яті як порядок величини та назвіть протидію на рівні контролера.
Ймовірні додаткові запитання: Який фізичний механізм обмежує витривалість флеш-пам'яті? Чому маскові ROM досі використовують, хоча флеш можна перепрограмовувати? Де нові енергонезалежні технології (3D XPoint, MRAM, ReRAM) прагнуть розміститися в ієрархії, і чому Optane зазнав комерційної невдачі? LO: LO1 Опишіть буфер кадру в текстовому режимі. Напишіть код, що ставить символ у заданому рядку та стовпці.
Обсяг підготовки:
Опишіть розкладку буфера кадру та обчисліть зміщення в байтах для пари (рядок, стовпець), наданої екзаменатором.
Інтерпретуйте байт атрибута на рівні бітів.
Напишіть мінімальний фрагмент на x86, який розміщує конкретний символ з конкретним атрибутом у конкретній позиції на екрані.
Порівняйте прямий доступ до буфера кадру з викликами сервісів BIOS за затримкою.
Ймовірні додаткові запитання: Чому прямий запис у відеопам'ять не є переносним? Які припущення він робить? Як це відображається на буфер кадру сучасного GPU? Чому палітра кольорів у текстовому режимі має лише 16 кольорів? LO: LO4 Порівняйте MMIO та PMIO на x86. Поясніть, що саме використовують сучасні пристрої та чому.
Обсяг підготовки:
Опишіть обидва стилі введення-виведення; назвіть задіяні інструкції x86 (IN/OUT для PMIO; MOV для MMIO) та використовувані адресні простори.
Порівняйте масштабованість, типові класи пристроїв та поясніть, чому MMIO домінує в сучасних високошвидкісних пристроях.
Назвіть один історичний клас пристроїв та один сучасний клас, щоб закріпити кожен стиль.
Ймовірні додаткові запитання: Чому MMIO масштабується краще за PMIO для розкладки регістрів сучасних пристроїв (кілобайти, мегабайти регістрів)? Чому x86 зберігає PMIO, хоча MMIO суворо гнучкіший? Як високошвидкісний пристрій (наприклад, мережна карта або GPU) виграє від MMIO порівняно з PMIO? LO: LO4 Дайте означення машин РАМ та РАСП. Поясніть, яка з них моделює реальні комп'ютери і чому.
Обсяг підготовки:
Дайте означення обох абстрактних машин і вкажіть, яка з них моделює реальну апаратуру.
Поясніть наслідок для безпеки того, що РАСП-машини виконують код із записуваної пам'яті.
Наведіть одну протидію, що накладає псевдо-РАМ-дисципліну на рівні ОС.
Наведіть одну техніку продуктивності, яка цілеспрямовано використовує гнучкість РАСП.
Ймовірні додаткові запитання: Що таке біт NX і яку властивість він забезпечує поверх базової апаратури? Мікроконтролер Гарвардської архітектури (з розділеною пам'яттю коду і даних) — це РАМ-машина чи РАСП-машина? Чому JIT-компілятори потребують особливого поводження в безпечних системах? LO: LO1, LO7 Порівняйте CISC та RISC. Поясніть, як сучасний x86 стирає межу через внутрішні мікрооперації (µops).
Обсяг підготовки:
Порівняйте за кількістю інструкцій, довжиною кодування, складністю декодера та розміром регістрового файлу.
Поясніть, як декодування сучасного x86 використовує внутрішні µops, щоб стерти межу.
Сформулюйте компроміс між складністю декодера та енергією / площею кристала.
Займіть позицію щодо конкретного сучасного дизайну (наприклад, Apple Silicon) та класифікуйте його з обґрунтуванням.
Ймовірні додаткові запитання: Якщо RISC виграє за простотою і енергоефективністю, чому x86 досі домінує на настільних комп'ютерах та серверах? Що таке кеш мікрооперацій (µop-кеш), і чому він дає великий виграш на сучасному x86? До чого належить архітектура Apple M-series — CISC чи RISC — і чому вони обрали саме цей підхід? LO: LO1, LO7 Дайте означення SISD, SIMD, MISD, MIMD. Наведіть реальний приклад для кожного та поясніть, куди вписуються GPU та Hyper-Threading.
Обсяг підготовки:
Дайте означення всіх чотирьох класів; наведіть одну конкретну реальну систему на клас.
Розмістіть GPU як на рівні варпу, так і на міжварповому рівні.
Розмістіть SMT / Hyper-Threading у межах таксономії.
Обговоріть, куди вписуються (або не вписуються) архітектури потоку даних чи систолічні матриці.
Ймовірні додаткові запитання: Що саме таке SIMT і чим він відрізняється від класичного SIMD? Що відбувається у варпі GPU, коли потоки розходяться на розгалуженні? Де в таксономії Флінна знаходяться архітектури потоку даних та систолічні матриці (наприклад, TPU)? LO: LO1, LO6 Порівняйте архітектури GPU та CPU. Поясніть, на яких навантаженнях виграє кожна з них.
Обсяг підготовки:
Протиставте кількість ядер, глибину одного ядра, проєктування кеша та пропускну здатність пам'яті на рівні порядку величини.
Наведіть навантаження, де GPU виграє у великі рази, і поясніть архітектурну особливість, що це уможливлює.
Наведіть навантаження, де виграє CPU, та поясніть, чому смуги GPU на ньому застоюються.
Обговоріть, що SIMT додає понад SIMD та які наслідки для продуктивності від розбіжних розгалужень.
Ймовірні додаткові запитання: Що таке SIMT і чим він відрізняється від класичного SIMD? Що відбувається з продуктивністю у варпі, коли потоки розходяться на розгалуженні? Чому пам'ять GPU (GDDR, HBM) значно швидша за DRAM CPU, але її затримка все одно гірша? LO: LO1, LO6, LO7 Порівняйте ASIC, FPGA та GPU для ролей прискорювача. Наведіть по одному навантаженню, що пасує кожному.
Обсяг підготовки:
Для кожного класу прискорювача вкажіть тип навантаження, що йому пасує, і назвіть один конкретний приклад.
Поясніть аргумент одноразових витрат (NRE), який виключає ASIC як варіант для малих обсягів.
Уточніть, у якому сенсі TPU є одночасно ASIC і програмованим.
Порівняйте типові тактові частоти FPGA та аналогічного ASIC і обґрунтуйте різницю.
Ймовірні додаткові запитання: Чому TPU класифікують як ASIC, хоча він виконує різні ML-моделі? Яка типова тактова частота FPGA порівняно з аналогічним ASIC і чому? У конвеєрі «прототип → продукт» — коли б ви переходили з FPGA на ASIC? LO: LO6, LO7 Поясніть кубіт vs біт, суперпозицію та заплутаність. Укажіть, у чому квантові комп'ютери сильні та в чому слабкі.
Обсяг підготовки:
Точно дайте означення кубіта, суперпозиції та заплутаності; не користуйтеся метафорою «пробує всі відповіді одночасно».
Назвіть принаймні два квантові алгоритми (наприклад, алгоритм Шора, алгоритм Ґровера) та тип прискорення, яке кожен дає.
Поясніть, чому сьогоднішньої кількості фізичних кубітів недостатньо, щоб зламати криптографію: більшість із них поглинає корекція помилок.
Чітко сформулюйте, у чому квантові комп'ютери не сильні (обчислення загального призначення, проста арифметика, задачі без квантового алгоритму).
Ймовірні додаткові запитання: Чому алгоритм Шора — це загроза для RSA, а AES-256 він лише послаблює, а не ламає? Чому квантовий алгоритм дає лише ймовірнісну відповідь, яку треба перевіряти класично? У чому різниця між квантовими обчисленнями на моделі вентилів та квантовим відпалом? LO: LO1, LO6 Дайте означення обох форм локальності. Покажіть приклад коду для кожної. Поясніть, як розмір рядка кешу використовує просторову локальність.
Обсяг підготовки:
Окремо дайте означення часової та просторової локальності; наведіть один приклад коду для кожної.
Зв'яжіть просторову локальність із розміром рядка кешу та типовою гранулярністю передачі.
Передбачте та поясніть різницю в продуктивності між обходом 2D-масиву в порядку по рядках та по стовпцях у розкладці C.
Поясніть блокування / тайлінг як перетворення, що підсилює локальність.
Ймовірні додаткові запитання: Чому обхід по рядках vs по стовпцях настільки кардинально відрізняється за продуктивністю? Що таке забруднення кешу, і коли апаратний попередній завантажувач радше шкодить, ніж допомагає? Що таке блокування (тайлінг циклів) і яку форму локальності воно підсилює? LO: LO5 Обчисліть середній час доступу до пам'яті для трирівневої ієрархії. Визначте домінантний доданок.
Обсяг підготовки:
Сформулюйте рекурсивну формулу AMAT для трирівневої ієрархії.
Застосуйте формулу до конкретного набору параметрів, наданого екзаменатором; обчисліть кожен рівень окремо.
Визначте домінантний доданок та обґрунтуйте його з перших принципів, а не лише повторенням числа.
Виконайте принаймні одне порівняння чутливості й зробіть інженерний висновок.
Ймовірні додаткові запитання: Якщо ми вдвічі зменшимо коефіцієнт промахів L1, яким буде AMAT? А якщо натомість удвічі зменшимо затримку DRAM? Чому середня затримка є оманливою метрикою для навантажень, чутливих до хвостів розподілу? Як попереднє завантаження входить до цієї формули? LO: LO5 Порівняйте три схеми відображення кеша. Укажіть апаратну вартість та поведінку конфліктних промахів.
Обсяг підготовки:
Порівняйте три схеми за апаратною вартістю та поведінкою конфліктних промахів.
Побудуйте конкретну пару адрес пам'яті, які стикаються в заданому кеші з прямим відображенням; покажіть арифметику.
Поясніть, чому реальні кеші L1 сходяться до 8-канальної асоціативності, а L3 йде вище.
Класифікуйте заданий промах як обов'язковий, ємнісний чи конфліктний.
Ймовірні додаткові запитання: Поясніть різницю між конфліктним, ємнісним та обов'язковим промахами (три C). Поясніть MESI на концептуальному рівні: чотири стани та перехід при записі іншим ядром. Чому кеші L3 часто дуже асоціативні (16-канальні й більше), а L1 — лише 8-канальний? LO: LO5, LO7 Назвіть три окремі мотивації віртуальної пам'яті. Накидайте структуру таблиці сторінок.
Обсяг підготовки:
Назвіть три окремі мотивації та наведіть конкретну реальну несправність, що сталася б без кожної з них.
Накидайте дворівневу таблицю сторінок x86 на папері.
Визначте ще одне практичне застосування механізму, крім трьох класичних мотивацій.
Зв'яжіть одну з мотивацій із конкретною сучасною функцією безпеки.
Ймовірні додаткові запитання: Що таке ASLR, і яку з трьох мотивацій він підсилює? Без віртуальної пам'яті — що станеться, коли дві програми хочуть завантажитися за адресою 0x400000? Як копіювання при записі використовує механізм віртуальної пам'яті? LO: LO4, LO5 Пройдіть трансляцію віртуальна→фізична на 32-бітній дворівневій системі сторінкування. Обробіть випадки: влучання TLB, промах TLB з обходом таблиці сторінок та промах TLB зі сторінковою помилкою.
Обсяг підготовки:
Розділіть 32-бітну віртуальну адресу на три її поля для системи зі сторінками 4 КБ і двома рівнями.
Пройдіть три результати: влучання TLB, промах TLB з обходом таблиці сторінок та промах TLB зі сторінковою помилкою.
Обчисліть вартість у пам'яті гіпотетичної пласкої таблиці сторінок на процес і протиставте з багаторівневою розкладкою.
Опишіть принаймні два типи розв'язання сторінкової помилки, окрім «завершити процес».
Ймовірні додаткові запитання: Чому багаторівневі (а не пласкі) таблиці сторінок? Наведіть аргумент за вартістю пам'яті. Чому x86-64 використовує 4 (або 5) рівнів, а не 2? Коли ОС змінює PTE, чому вона інвалідує запис TLB (наприклад, INVLPG), а не лишає застарілі записи, щоб апаратура виявила їх сама? LO: LO4, LO5 Порівняйте три алгоритми заміщення сторінок. Поясніть, чому практичним вибором є Clock.
Обсяг підготовки:
Дайте означення LRU, FIFO та Clock; укажіть вартість реалізації кожного.
Поясніть, чому істинний апаратний LRU непрактичний і що саме апроксимує Clock.
Опишіть аномалію Беладі та вкажіть, який алгоритм її виявляє.
Подайте OPT як теоретичний еталон і поясніть, чому він не реалізовний.
Ймовірні додаткові запитання: Що таке аномалія Беладі, і який алгоритм її виявляє? Побудуйте невеликий приклад, якщо зможете. Чому OPT неможливо реалізувати на практиці, але він корисний як базовий еталон? Як «робочий набір» входить у сучасні політики заміщення? LO: LO5 Опишіть 32-бітне число з рухомою комою IEEE 754. Поясніть зміщення, нормалізацію та спеціальні значення.
Обсяг підготовки:
Розмістіть поля «знак / експонента / мантиса» 32-бітного числа з рухомою комою, укажіть зміщення та запишіть формулу значення.
Опишіть чотири спеціальні випадки, що розрізняються екстремальними значеннями експоненти.
Обчисліть найбільше та найменше нормалізоване число з рухомою комою на папері.
Обґрунтуйте, чому формат зарезервовує і +0, і −0.
Ймовірні додаткові запитання: Чому IEEE 754 має і +0, і −0? Де це має значення? Що таке сигналізуючий vs тихий NaN і чому їх два? Яку роль відіграють денормалізовані (субнормальні) числа та яку проблему біля нуля вони розв'язують? LO: LO2 Покажіть на папері, чому 0.1 + 0.2 не дорівнює точно 0.3. Потім наведіть приклад катастрофічного скорочення.
Обсяг підготовки:
Покажіть на папері, що 0.1 — це періодичний двійковий дріб, і висновкуйте, чому його збереження округлюється.
Поясніть, чому сума двох округлених значень дає результат, відмінний від точної десяткової суми.
Наведіть один конкретний приклад катастрофічного скорочення та стандартний чисельний прийом для його виправлення.
Сформулюйте практичне правило для порівняння чисел з рухомою комою у вихідному коді.
Ймовірні додаткові запитання: Коли безпечно порівнювати числа з рухомою комою через ==? Як підсумовування Кагана зменшує похибку від скорочення? У якій галузі катастрофічне скорочення найважливіше на практиці (фінанси, наука, графіка)? LO: LO2, LO5 Порівняйте два x86-стеки з рухомою комою. Поясніть, чому сучасні компілятори віддають перевагу SSE/AVX.
Обсяг підготовки:
Протиставте регістрову модель, точність та SIMD-можливості обох стеків.
Назвіть принаймні три конкретні причини, чому сучасні компілятори за замовчуванням орієнтуються на SSE/AVX (пропускна здатність SIMD, плоске розподілення регістрів, передбачувана семантика IEEE 754).
Поясніть, чому x87 залишається в ISA, хоча суворо поступається для більшості цілей.
Ймовірні додаткові запитання: Яка різниця в точності між 80-бітною розширеною x87 та 64-бітною подвійною SSE, і коли це справді має значення? Чому x87 досі в ISA, якщо SSE суворо кращий для сучасного коду? Чому стекова регістрова модель (x87) ускладнює оптимізацію компілятора порівняно з пласким регістровим файлом (SSE)? LO: LO5, LO7 Намалюйте 5-стадійний конвеєр. Поясніть, як конвеєризація покращує пропускну здатність, не покращуючи затримку однієї інструкції.
Обсяг підготовки:
Намалюйте 5-стадійний конвеєр з інструкціями в польоті протягом кількох тактів.
Розрізніть затримку однієї інструкції та усталену пропускну здатність і кількісно оцініть їхнє співвідношення в ідеальному конвеєрі.
Поясніть, чому потрібні регістри конвеєра, та чого вони коштують.
Обговоріть, чому глибокі конвеєри стикаються зі зменшенням віддачі; наведіть реальний дизайн, який зайшов надто далеко.
Ймовірні додаткові запитання: Чому глибші конвеєри стикаються зі зменшенням віддачі? Звідки походить межа? Яка роль регістра конвеєра між стадіями, і чому він необхідний? Чому дуже глибокий конвеєр Pentium 4 зазнав невдачі, і що його замінило? LO: LO5 Класифікуйте конфлікти конвеєра. Для кожного типу наведіть конкретний приклад на x86 і назвіть апаратне розв'язання.
Обсяг підготовки:
Класифікуйте структурні конфлікти, конфлікти даних (RAW / WAR / WAW) та керування; наведіть x86-приклад для кожного.
Назвіть апаратне розв'язання для кожного класу конфлікту.
Укажіть приблизну цикову вартість хибного передбачення переходу на сучасному x86.
Поясніть, чому WAR та WAW виникають лише при позачерговому виконанні.
Ймовірні додаткові запитання: Чому конфлікти WAR і WAW не з'являються в простих конвеєрах з виконанням за порядком, а лише при позачерговому? Який штраф скидання конвеєра при хибному передбаченні переходу? Що таке перейменування регістрів на апаратному рівні — як фізичний регістровий файл відображається на архітектурні імена регістрів? LO: LO5 Порівняйте три механізми паралелізму. Поясніть, яке навантаження пасує кожному з них і як Spectre/Meltdown їх експлуатують.
Обсяг підготовки:
Розмістіть кожен механізм у спектрі паралелізму за гранулярністю та спільними ресурсами.
Наведіть одне навантаження, що пасує кожному механізму, і одне, що йому шкодить.
Визначте, який із трьох експлуатують Spectre/Meltdown; назвіть використовуваний побічний канал.
Коротко опишіть одну реальну протидію та її вартість у продуктивності.
Ймовірні додаткові запитання: Коли SMT радше шкодить продуктивності, а не допомагає? Наведіть шаблон навантаження. Що таке Spectre та Meltdown на концептуальному рівні, і який з трьох механізмів вони експлуатують? Чому вартість протидій Spectre/Meltdown така висока на навантаженнях із великою кількістю системних викликів? LO: LO5, LO6, PR16 L15 доповнює L10 (гетерогенність) та L14 (паралелізм); у підготовці можна спиратися на обидві. Сформулюйте закон Амдала. Обчисліть максимальне прискорення для програми, паралелізованої на 90%. Протиставте із законом Густафсона.
Обсяг підготовки:
Сформулюйте формулу Амдала; обчисліть асимптотичне прискорення для заданої частки паралельного коду.
Обчисліть прискорення для двох і більше скінченних кількостей процесорів, щоб проілюструвати зменшення віддачі.
Протиставте із законом Густафсона, сформулювавши інше припущення щодо розміру задачі.
Поясніть, чому реальні застосунки рідко досягають стелі Амдала; назвіть принаймні два джерела накладних витрат.
Ймовірні додаткові запитання: Що таке сильне vs слабке масштабування, і який закон відповідає якому типу? Чому 1% послідовної частки катастрофічний на масштабі 1000 ядер? Наведіть реальний приклад, де закон Густафсона дає краще передбачення, ніж закон Амдала. LO: LO5, LO6 Поясніть P = C × V² × f. Опишіть, чому масштабування напруги застопорилося та як індустрія відреагувала.
Обсяг підготовки:
Запишіть формулу динамічної потужності та вкажіть кожен множник.
Опишіть масштабування Деннарда та вкажіть приблизно, коли воно завершилося.
Поясніть фізичну причину, чому масштабування напруги зупинилося.
Зв'яжіть кінець масштабування частоти зі зміщенням індустрії до багатоядерності.
Ймовірні додаткові запитання: Що таке DVFS і як його використовують сучасні ЦП? Що таке TDP (проєктна теплова потужність), і чим він відрізняється від фактичної миттєвої потужності? Чому темп Intel «tick-tock» сповільнився у 2010-х? LO: LO5 Дайте означення dark silicon. Поясніть, як гетерогенні архітектури (big.LITTLE, Apple M-series) відповідають на нього.
Обсяг підготовки:
Дайте означення dark silicon кількісно у термінах бюджету потужності vs кількості транзисторів.
Опишіть відповідь через гетерогенність на мобільному та настільному масштабах, назвавши принаймні одну реальну платформу.
Сформулюйте компроміс між спеціалізацією та складністю програмного забезпечення.
Зв'яжіть спеціалізацію з аргументом сталості для енергії на операцію.
Ймовірні додаткові запитання: У чому полягає аргумент сталості за спеціалізацію? Як блок із фіксованою функцією зменшує енергію на операцію порівняно з ЦП? Яка програмна ціна гетерогенності? Хто вирішує, який блок виконує задану задачу? Чому центри обробки даних дедалі частіше приймають кастомний кремній (AWS Graviton, Google TPU) замість масового x86? LO: LO6, LO7 Кінець посібника з підготовки.]]></description><link>exam-prep/preparation-guide.html</link><guid isPermaLink="false">Exam-Prep/Preparation-Guide.md</guid><pubDate>Wed, 22 Apr 2026 05:17:40 GMT</pubDate></item><item><title><![CDATA[Exam]]></title><description><![CDATA[Дисципліна: Архітектура комп'ютерних систем (6 ECTS / 4 ECTS)Форма контролю: усний теоретичний іспит.Мета іспиту: перевірити здатність студента пояснювати принципи побудови комп'ютерних архітектур (<a data-tooltip-position="top" aria-label="Syllabus > 3.4. Результати навчання дисципліни (Learning Outcomes)" data-href="Syllabus#3.4. Результати навчання дисципліни (Learning Outcomes)" href="syllabus.html#3.4. Результати навчання дисципліни (Learning Outcomes)" class="internal-link" target="_self" rel="noopener nofollow">LO1</a>), аналізувати представлення даних на машинному рівні (<a data-tooltip-position="top" aria-label="Syllabus > 3.4. Результати навчання дисципліни (Learning Outcomes)" data-href="Syllabus#3.4. Результати навчання дисципліни (Learning Outcomes)" href="syllabus.html#3.4. Результати навчання дисципліни (Learning Outcomes)" class="internal-link" target="_self" rel="noopener nofollow">LO2</a>), описувати взаємодію програм з апаратурою та ОС (<a data-tooltip-position="top" aria-label="Syllabus > 3.4. Результати навчання дисципліни (Learning Outcomes)" data-href="Syllabus#3.4. Результати навчання дисципліни (Learning Outcomes)" href="syllabus.html#3.4. Результати навчання дисципліни (Learning Outcomes)" class="internal-link" target="_self" rel="noopener nofollow">LO4</a>), а також порівнювати альтернативні архітектурні підходи з обґрунтованими компромісами (<a data-tooltip-position="top" aria-label="Syllabus > 3.4. Результати навчання дисципліни (Learning Outcomes)" data-href="Syllabus#3.4. Результати навчання дисципліни (Learning Outcomes)" href="syllabus.html#3.4. Результати навчання дисципліни (Learning Outcomes)" class="internal-link" target="_self" rel="noopener nofollow">LO7</a>). Додатково оцінюється аналітичне мислення (<a data-tooltip-position="top" aria-label="Syllabus > 3.3. Програмні результати навчання (ПРН), що забезпечуються курсом" data-href="Syllabus#3.3. Програмні результати навчання (ПРН), що забезпечуються курсом" href="syllabus.html#3.3. Програмні результати навчання (ПРН), що забезпечуються курсом" class="internal-link" target="_self" rel="noopener nofollow">PR1</a>), розуміння принципів надійної роботи (<a data-tooltip-position="top" aria-label="Syllabus > 3.3. Програмні результати навчання (ПРН), що забезпечуються курсом" data-href="Syllabus#3.3. Програмні результати навчання (ПРН), що забезпечуються курсом" href="syllabus.html#3.3. Програмні результати навчання (ПРН), що забезпечуються курсом" class="internal-link" target="_self" rel="noopener nofollow">PR16</a>) та усвідомлення впливу архітектурних рішень на продуктивність алгоритмів (<a data-tooltip-position="top" aria-label="Syllabus > 3.3. Програмні результати навчання (ПРН), що забезпечуються курсом" data-href="Syllabus#3.3. Програмні результати навчання (ПРН), що забезпечуються курсом" href="syllabus.html#3.3. Програмні результати навчання (ПРН), що забезпечуються курсом" class="internal-link" target="_self" rel="noopener nofollow">PR5</a>).<br><a class="internal-link" data-href="Exam-Questions.md" href="exam-questions.html" target="_self" rel="noopener nofollow">Білети</a>, як вони будуть на іспиті.Два рівні матеріалів:
<br><a class="internal-link" data-href="Exam-Prep/Preparation-Guide.md" href="exam-prep/preparation-guide.html" target="_self" rel="noopener nofollow">Посібник з підготовки</a> — хаб підготовки. Починайте тут: нагорі — навігаційна таблиця по всіх 45 питаннях, далі — повний обсяг підготовки та ймовірні додаткові запитання екзаменатора для кожного.
Ядра відповідей по лекціях — файли Exam-Prep/L##-Prep.md (один на лекцію). Стислий кістяк відповіді на кожне питання, посилання на конкретні розділи лекцій та рядок «Зверніть увагу на» з типовими пастками.
Ядра не містять повного обсягу підготовки — обов'язково звіряйтесь із посібником, щоб не пропустити очікуваних схем, формул чи порівнянь.
Білет: 2 теоретичні питання — одне з Блоку I (Основи, L01–L07) та одне з Блоку II (Системи та продуктивність, L08–L15). Витягуються випадково.
Підготовка: 20–30 хв, дозволені письмові нотатки на аркуші, виданому екзаменатором.
Відповідь: 3–5 хв на кожне питання, усно, з обов'язковими ілюстраціями на папері (схеми, формули, числові приклади).
Додаткові запитання екзаменатора: 2–3 хв на кожне питання. Вони перевіряють глибину розуміння поза завченим планом.
Кожне з двох питань оцінюється незалежно за однаковими критеріями:
Повнота очікуваної структури відповіді (35%) — відповідь містить схеми, формули, числові приклади та порівняння, яких вимагає обсяг підготовки до питання. Прогалини знижують оцінку.
Коректність термінології та понять (20%) — точні означення, правильні формули, відсутність фактичних помилок.
Обґрунтування та компроміси (15%) — студент пояснює «чому саме так», порівнює альтернативи, формулює компроміси, а не просто перелічує факти.
Відповіді на додаткові запитання (20%) — студент здатен заглибитись у тему, обґрунтувати неочевидне твердження, переконливо відповісти на уточнювальне запитання. Цей критерій відділяє справжнє розуміння від завченого плану.
Якість прикладів та ілюстрацій (10%) — конкретні приклади (фрагменти коду, числа, схеми), а не абстрактні твердження.
Вага іспиту в підсумковій оцінці курсу: 30% (4 ECTS) / 20% (6 ECTS). Мінімум для складання курсу: 60 / 100 балів.<br>Див. <a data-tooltip-position="top" aria-label="Syllabus > 4.1. Умови допуску до іспиту" data-href="Syllabus#4.1. Умови допуску до іспиту" href="syllabus.html#4.1. Умови допуску до іспиту" class="internal-link" target="_self" rel="noopener nofollow">Syllabus.md §4.1</a>. Коротко: здано ≥ 70% практичних робіт, пройдено обидва практичні контролі (I та II), захищено підсумковий проєкт (6 ECTS) та накопичено щонайменше 30 балів до іспиту.]]></description><link>exam.html</link><guid isPermaLink="false">Exam.md</guid><pubDate>Wed, 22 Apr 2026 05:16:18 GMT</pubDate></item><item><title><![CDATA[Exam-Questions]]></title><link>exam-questions.html</link><guid isPermaLink="false">Exam-Questions.pdf</guid><pubDate>Wed, 22 Apr 2026 04:44:10 GMT</pubDate></item><item><title><![CDATA[L13. Арифметика з рухомою комою]]></title><description><![CDATA[Курс: Архітектура комп'ютерних систем
Тема: IEEE 754 у деталях, режими округлення, числові похибки, x87 проти SIMD
Результати навчання:
РН2: Пояснювати принципи представлення даних у комп'ютерних системах
РН5: Аналізувати продуктивність та обмеження комп'ютерних систем
Відкрийте будь-яку мову програмування і введіть найпростішу арифметичну операцію:&gt; 0.1 + 0.2
0.30000000000000004
Ця зайва 4 в кінці дивує кожного програміста, який стикається з нею вперше. Це не помилка JavaScript — такий самий результат з'являється в Python, C, Java, Rust та будь-якій іншій мові, що спирається на апаратний блок операцій з плаваючою комою (FPU). Річ не в мові, а в розриві між математичними дійсними числами зі шкільного курсу і скінченними двійковими представленнями, якими змушений оперувати комп'ютер.<img alt="Мем, що показує 0.1 + 0.2 = 0.30000000000000004 — класичний сюрприз точності чисел з рухомою комою" src="assets/l13-fp-precision-meme.jpeg" target="_self">Класичний мем про точність чисел з рухомою комою: кожен програміст рано чи пізно переконується, що у двійковій арифметиці з рухомою комою 0.1 + 0.2 не дорівнює 0.3.Ця лекція саме про цей розрив — звідки він береться, як інженери апаратного забезпечення та математики домовилися про стандартний спосіб його контролю (IEEE 754) і як програмістові уникнути найнебезпечніших пасток.Розглянемо число π. Його десятковий розклад починається з 3.14159265358979323846… і триває нескінченно, ніколи не повторюючись. Навіть якби ми віддали кожен транзистор найбільшого у світі суперкомп'ютера під зберігання цифр π, точного значення все одно не вдалося б зафіксувати — завжди доведеться округляти.<br><img alt="Сотні цифр числа пі, відображених у сітці" src="assets/l13-pi-digits.png" target="_self">Десятковий розклад числа пі триває нескінченно без повторень — жодне скінченне представлення не здатне передати його точне значення.І π тут не виняток. Переважна більшість дійсних чисел має нескінченно довгі представлення в будь-якій системі числення. Число 1/3 = 0.333… є нескінченним у десятковій системі. Число 0.1, цілком скінченне на папері в десятковій формі, перетворюється на нескінченний періодичний дріб 0.0001100110011… у двійковій. Комп'ютерне слово має 32 або 64 біти. У ці біти потрібно вмістити наближення числа, яке може потребувати нескінченно багато цифр. Щось доведеться пожертвувати, і жертвою стає точність. Збережене число близьке до математичного значення, але рідко ідентичне йому.Ось що відбувається, коли JavaScript обчислює 19.9 × 100:<br><img alt="Консоль JavaScript, що показує результат 19.9 помножити на 100 як 1989.9999999999998 замість 1990" src="assets/l13-js-console-fp-demo.jpeg" target="_self">Консоль JavaScript демонструє, що 19.9 × 100 дає 1989.9999999999998 — крихітна помилка округлення числа з рухомою комою, яка може зламати порівняння на рівність.Перші три множення дають точні цілі числа. Четверте відхиляється приблизно на 2 × 10⁻¹³ — крихітна похибка в абсолютних величинах, але цілком достатня, щоб спричинити реальну проблему при порівнянні результату з 1990 за допомогою ==. Це не особливість JavaScript — той самий патерн спостерігається в кожній мові:&gt;&gt;&gt; 19.9 * 100
1989.9999999999998
Console.WriteLine(19.9 * 100); // 1990 (C# округлює для відображення, але біти ті самі)
Console.WriteLine(19.9 * 100 == 1990); // False
Причина завжди та сама: 19.9 неможливо точно представити у двійковому форматі з рухомою комою, тож апаратне забезпечення зберігає найближче значення, і ця крихітна початкова похибка переноситься через множення.Помилки чисел з рухомою комою — це не просто прикра дрібниця для навчальних вправ. Вони ставали причиною справжніх катастроф:<br>Збій ракети Patriot (1991). Під час війни в Перській затоці батарея протиракетної оборони <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/MIM-104_Patriot#Failure_at_Dhahran" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/MIM-104_Patriot#Failure_at_Dhahran" target="_self">Patriot</a> у Даграні (Саудівська Аравія) не змогла перехопити іракську ракету Scud, яка влучила в американські казарми та вбила 28 солдатів. Першопричиною була помилка округлення числа з рухомою комою у внутрішньому годиннику системи. Система відстежувала час як кількість 0.1-секундних інтервалів, однак 0.1 неможливо точно представити у двійковій системі. Після 100 годин безперервної роботи накопичена похибка в обчисленні часу сягнула близько 0.34 секунди — за цей час ракета Scud долає понад 500 метрів. Система стеження шукала ворожу ракету не в тій частині неба і так і не дала команду на пуск.<br>Вибух Ariane 5 (1996). Ракета <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Ariane_flight_V88" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Ariane_flight_V88" target="_self">Ariane 5</a> Європейського космічного агентства вибухнула через 37 секунд після старту, знищивши корисне навантаження вартістю 370 мільйонів доларів. Причиною стало перетворення без перевірки меж: 64-бітне значення з рухомою комою (горизонтальна швидкість ракети) конвертували у 16-бітне ціле число зі знаком. Ariane 5 була швидшою за Ariane 4, чиє програмне забезпечення повторно використали, тож значення швидкості перевищило 32 767 — максимум для 16-бітного цілого. Переповнення породило діагностичний дамп, який помилково сприйняли як польотні дані, і система навігації подала команду на різкий поворот. Ракету розірвало на частини аеродинамічне навантаження.<br>Помилка Ванкуверської фондової біржі (1982). Ванкуверська фондова біржа <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Vancouver_Stock_Exchange#Technical_issues" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Vancouver_Stock_Exchange#Technical_issues" target="_self">запровадила новий індекс</a>, встановлений на рівні 1000.000 у січні 1982 року. Індекс перераховували після кожної угоди, усікаючи результат до трьох десяткових знаків замість округлення. За 22 місяці накопичення похибок усікання індекс просів до 524.811 — приблизно половини від правильного значення, що становило близько 1098.892. Біржі довелося виправити індекс і змінити логіку округлення.Ці приклади об'єднує спільний урок: арифметика з рухомою комою працює коректно, але підпорядковується правилам, що відрізняються від арифметики дійсних чисел зі шкільного курсу. Якщо цих правил не розуміти, похибки накопичуються непомітно, аж доки не стають катастрофічними.Арифметика з рухомою комою — це не вузька тема, обмежена науковими обчисленнями. Вона трапляється всюди в сучасному програмному забезпеченні:Кожен програміст рано чи пізно стикається з арифметикою з рухомою комою. Питання лише в тому, чи зустріне він її з розумінням, чи зі здивуванням.У наступних розділах ми побудуємо повне розуміння того, як комп'ютери представляють дійсні числа та оперують ними:
Похибки представлення — математичний апарат для вимірювання того, наскільки збережене число відрізняється від справжнього значення.
Двійкові дроби з фіксованою комою — найпростіший підхід, його переваги та чому його недостатньо для обчислень загального призначення.
Науковий запис та нормалізація — ключова ідея, що робить рухому кому можливою: заміна фіксованого положення десяткової коми на рухоме.
Стандарт IEEE 754 — конкретне кодування, яким послуговується практично кожен сучасний процесор: знак, експонента, мантиса, зміщення.
Декодування та кодування — як переводити десяткові значення у 32-бітні або 64-бітні двійкові представлення та у зворотному напрямку.
Спеціальні значення — додатний і від'ємний нуль, нескінченності, NaN (не число) та денормалізовані числа.
Формати IEEE 754 — одинарна точність, подвійна точність та розширена точність.
Арифметика з рухомою комою та округлення — як працюють додавання, множення та режими округлення, і де втрачається точність.
FPU x87 та SSE/AVX — як процесори Intel реалізують арифметику з рухомою комою апаратно.
Після цієї лекції ви точно розумітимете, чому 0.1 + 0.2 ≠ 0.3, як передбачати появу помилок чисел з рухомою комою та як писати код, що коректно їх обробляє.Перш ніж обговорювати, як саме комп'ютери кодують дійсні числа, нам потрібен точний словник для опису того, наскільки неточно зберігається число. У цьому розділі ми введемо математичний апарат похибок представлення — абсолютних і відносних — яким будемо користуватися протягом усієї лекції.Між будь-якими двома послідовними цілими числами — скажімо, 7 і 8 — лежить нескінченно багато дійсних чисел: 7.1, 7.01, 7.001, 7.0001 і так далі, без кінця. Комп'ютер же відводить на зберігання кожного числа фіксовану кількість бітів. З n бітів можна закодувати щонайбільше 2ⁿ різних значень. Хоч би яким великим було n, 2ⁿ завжди скінченне, тоді як множина дійсних чисел між будь-якими двома цілими є нескінченною. Отже, комп'ютер мусить наближати: він округлює кожне дійсне число до найближчого значення, яке здатен представити.Таке округлення неминуче вносить похибку. Постає питання: наскільки велика ця похибка і як її вимірювати?Абсолютна похибка представлення — це модуль різниці між справжнім значенням та збереженим значенням:Δ = |A − A_comp|
де A — справжнє математичне значення, а A_comp — значення, фактично збережене в комп'ютері.Абсолютна похибка показує — у тих самих одиницях, що й саме число, — наскільки далеко наближення від справжнього значення. Якщо справжнє значення дорівнює 3.14159265, а збережене — 3.1416, то абсолютна похибка становить:Δ = |3.14159265... − 3.1416| ≈ 0.0000073465 = 7.3465 × 10⁻⁶
Усе просто й наочно: збережене значення відрізняється від справжнього приблизно на 7.35 мільйонних.Відносна похибка виражає абсолютну похибку як частку від справжнього значення:δ = Δ / A ≈ Δ / A_comp
(Наближенням Δ/A_comp послуговуємося тоді, коли справжнє значення A невідоме, — а це на практиці трапляється часто, адже якби A було відоме точно, то й наближати нічого не довелося б.)Відносна похибка — безрозмірна величина: вона вимірює частку похибки, а не її абсолютний розмір. Для прикладу з π:δ ≈ 7.3465 × 10⁻⁶ / 3.1416 ≈ 2.34 × 10⁻⁶
Це означає, що збережене значення відрізняється від справжнього приблизно на 2.34 мільйонних.Розгляньмо повний розрахунок:<br><img alt="Розв'язаний приклад, що показує абсолютну похибку, відносну похибку та відносну похибку у відсотках для наближення числа пі значенням 3.1416" src="assets/l13-pi-error-calculation.png" target="_self">Покроковий розрахунок похибки для наближення числа пі значенням 3.1416: абсолютна похибка, відносна похибка та відносна похибка, виражена у мільйонних частках.Дано:
Справжнє значення: A = π = 3.14159265358979...
Збережене наближення: A_comp = 3.1416
Крок 1 — Абсолютна похибка:Δ = |π − 3.1416| = |3.14159265358979... − 3.1416| = |−0.00000734641021...| ≈ 7.3465 × 10⁻⁶
Крок 2 — Відносна похибка:δ = Δ / A ≈ 7.3465 × 10⁻⁶ / 3.14159265... ≈ 2.34 × 10⁻⁶
Відносна похибка 2.34 × 10⁻⁶ означає, що це наближення π із чотирма десятковими знаками є точним приблизно до однієї частки з 430 000.Абсолютна похибка сама по собі буває оманливою, адже її значущість цілком залежить від масштабу числа, яке ми представляємо. Розгляньмо два сценарії:У Сценарії 1 абсолютна похибка 1 мізерна — одна частка з мільйона. У Сценарії 2 та сама за порядком величини абсолютна похибка 10⁻⁶ дає 100 % похибки: збережене значення вдвічі більше за справжнє. Абсолютні похибки зіставні між собою, а ось відносні свідчать про зовсім різні речі.Саме через цю незалежність від масштабу відносна похибка є основною метрикою в чисельних обчисленнях. Коли хтось каже, що «подвійна точність IEEE 754 дає приблизно 15–16 значущих десяткових цифр», ідеться саме про відносну похибку: вона обмежена приблизно значенням 10⁻¹⁶ незалежно від того, чи число дорівнює 10³⁰⁰, чи 10⁻³⁰⁰.Похибки представлення — не просто теоретична проблема. Вони проявляються трьома практичними способами, що позначаються на реальних програмах:Помилки округлення. Щоразу, коли дійсне число зберігається у змінній із рухомою комою, воно округлюється до найближчого представного значення. Кожна арифметична операція над числами з рухомою комою здатна додати ще одне округлення. Ці крихітні похибки накопичуються у довгій послідовності операцій. У циклі, що виконується мільйони разів, — а це типова ситуація для наукових моделювань, фінансових розрахунків і машинного навчання, — накопичена похибка округлення може сягнути помітних значень.# Додавання 0.1 десять разів — має бути 1.0
total = 0.0
for _ in range(10): total += 0.1
print(total) # 0.9999999999999999
print(total == 1.0) # False
Втрата точності (катастрофічне скорочення). При відніманні двох майже рівних чисел із рухомою комою більшість значущих цифр взаємно скорочується, і в результаті залишається дуже мало правильних цифр. Наприклад, якщо A = 1.23456789 і B = 1.23456780 (обидва збережені з 9 значущими цифрами), то A − B = 0.00000009 — результат усього з однією значущою цифрою, хоча кожен з операндів мав аж дев'ять. Це явище, відоме як катастрофічне скорочення, — одна з найнебезпечніших пасток чисельних обчислень. Ми детально розберемо його далі у цій лекції.Стабільність алгоритмів. Навіть математично еквівалентні формули можуть давати разюче різні чисельні результати під час обчислень із рухомою комою. Алгоритм називають чисельно стабільним, якщо малі похибки на вході спричиняють лише малі похибки на виході, і нестабільним — якщо малі похибки на вході підсилюються до великих похибок на виході. Від вдалого вибору алгоритму або відповідного алгебраїчного перетворення залежить, чи дасть моделювання осмислені результати, чи просто нісенітницю.Скажімо, квадратна формула x = (−b ± √(b² − 4ac)) / 2a математично точна, проте за умови b² ≫ 4ac обчислення одного з двох коренів зводиться до віднімання двох майже рівних чисел (катастрофічне скорочення). Чисельно стабільна альтернатива обчислює один корінь за стандартною формулою, а другий — з тотожності x₁ · x₂ = c/a, уникаючи скорочення цілком.Ці три наслідки — округлення, скорочення та стабільність — поставатимуть упродовж усієї лекції. Щойно введений апарат похибок дає нам словник для їх точного аналізу. У наступних розділах ми побачимо, як комп'ютери насправді кодують дійсні числа: спершу розберемо простіший випадок двійкових дробів із фіксованою комою, а далі перейдемо до представлення з рухомою комою, яке й домінує у сучасному апаратному забезпеченні.Перш ніж переходити до чисел з рухомою комою, розгляньмо простішу схему, яку вони витіснили, — і з'ясуймо, чому так сталося.У лекції L05 ми з'ясували, що двійкові цілі числа використовують позиційну систему числення: кожен біт відповідає степеню двійки. Та сама ідея поширюється і за двійкову кому (двійковий відповідник десяткової коми) — на від'ємні степені двійки: 1 0 1 . 1 1 0 1 2² 2¹ 2⁰ 2⁻¹ 2⁻² 2⁻³ 2⁻⁴ 4 + 0 + 1 + 0.5+0.25+ 0 +0.0625 = 5.8125
Кожна позиція праворуч від двійкової коми має вдвічі менше значення за попередню: 0.5, 0.25, 0.125, 0.0625 і так далі. Принцип той самий, що й у десяткових дробах, — просто степені 2 замість степенів 10.Число 0.1₁₀ (одну десяту) неможливо точно подати у двійковій системі з тієї самої причини, з якої 1/3 неможливо точно подати у десятковій. Отримуємо нескінченний періодичний двійковий дріб: 0.00011001100110011…₂. Це не помилка, а фундаментальна властивість позиційних систем числення.У поданні з фіксованою комою ми виділяємо сталу кількість бітів для цілої частини й сталу — для дробової. Якщо маємо загалом w бітів, з яких n відведено на цілу частину, а m — на дробову (де w = n + m, можливо, плюс біт знаку), то двійкова кома стоїть у заздалегідь визначеній позиції і ніколи не зсувається.Наприклад, маючи 8 бітів для цілої частини та 8 бітів для дробової (16-бітний формат з фіксованою комою), можна подати значення від 0.00000000₂ до 11111111.11111111₂, тобто від 0 до 255.99609375.Ключові властивості випливають безпосередньо з цієї структури:
Діапазон визначається бітами цілої частини: n бітів дають максимальне значення близько 2ⁿ.
Роздільна здатність визначається бітами дробової частини: m бітів дають найменший крок 2⁻ᵐ.
Роздільна здатність рівномірна — проміжок між будь-якими двома сусідніми подаваними числами завжди дорівнює 2⁻ᵐ.
Абсолютна похибка подання з фіксованою комою стала по всьому діапазону — вона ніколи не перевищує 2⁻ᵐ, незалежно від того, чи це 0.001, чи 200.0. Ця рівномірність — водночас і сильна, і слабка сторона фіксованої коми.Натомість відносна похибка поводиться зовсім інакше. Для великого числа, як-от 200.0, абсолютна похибка 2⁻⁸ ≈ 0.004 є нехтовно малою — відносна похибка становить близько 0.002%. Але для малого числа, як-от 0.01, та сама абсолютна похибка 0.004 дає відносну похибку 40%. Поблизу нуля відносна похибка наближається до 100%.Число Абс. похибка (2⁻⁸) Відносна похибка
────────── ──────────────── ──────────────
200.0 ~0.004 ~0.002% 1.0 ~0.004 ~0.4% 0.01 ~0.004 ~40% 0.001 ~0.004 ~400% (безглуздо)
Це фундаментальне обмеження: фіксована кома марнує точність на великих числах (де дробові біти майже ні на що не впливають) і не має достатньої точності для малих чисел (де ті самі дробові біти виявляються надто грубими).Числа, абсолютне значення яких менше за 2⁻ᵐ, неможливо відрізнити від нуля — усі вони відображаються в те саме подання. Такі числа називають машинним нулем. Машинний нуль — це не «справжній» математичний нуль; це множина всіх дійсних чисел, які формат округлює до нуля, бо вони менші за його порогову роздільну здатність.Попри свої обмеження, фіксована кома залишається правильним вибором у певних галузях:
Цифрові сигнальні процесори (DSP) та вбудовані системи: аудіокодеки, контролери двигунів і обробка сигналів із сенсорів часто використовують фіксовану кому, бо це швидко (цілочисельний АЛП, FPU не потрібен) і детерміновано (жодних несподіванок від округлення).
Фінансові обчислення: валюти мають фіксовану кількість десяткових розрядів. Подання $19.99 як цілого числа 1999 (центів) уникає похибок рухомої коми, від яких нервуються бухгалтери.
Програмовані вентильні матриці (FPGA) та ASIC: арифметика з фіксованою комою потребує менше логічних елементів і менше енергії, ніж арифметика з рухомою комою.
У всіх цих випадках програміст знає діапазон заздалегідь і може відповідно підібрати n та m. Але для універсальних наукових та інженерних обчислень — де значення зазвичай охоплюють десятки порядків — фіксованої коми недостатньо. Потрібне подання, яке підлаштовує свою точність під величину числа. Саме це й забезпечує рухома кома.Науковий запис знайомий нам зі шкільних часів: число подається як мантиса (її також називають значущою частиною), помножена на степінь основи:A = M × 10ᵉде M — дійсне число (мантиса), а e — ціле число (експонента — додатна або від'ємна). Експонента вказує, де стоїть десяткова кома; мантиса несе значущі цифри.Будь-яке дійсне число можна записати у такому вигляді, причому багатьма різними способами:0.71 = 0.071 × 10¹ = 7.1 × 10⁻¹ = 71 × 10⁻² = ...
2025 = 0.2025 × 10⁴ = 2.025 × 10³ = 20.25 × 10² = ...
Для кожного числа існує нескінченно багато таких записів — будь-який зсув десяткової коми компенсується зміною експоненти. Цю неоднозначність ми розв'яжемо незабаром.У комп'ютері мантису зберігають із фіксованою кількістю цифр і експоненту — теж із фіксованою кількістю цифр. Звідси випливає чіткий розподіл ролей:
Біти експоненти задають ДІАПАЗОН — наскільки великим або малим може бути число.
Біти мантиси задають ТОЧНІСТЬ — скільки значущих цифр ми можемо зберегти.
Саме ця ідея і робить рухому кому такою потужною.Розгляньмо формат із 3-значною мантисою (зі знаком) у діапазоні 0.1 ≤ |M| &lt; 1 та 2-значною експонентою (зі знаком). Маємо лише 5 цифр і 2 символи знаку — загалом 7 символів. Який діапазон це покриває?Найменше додатне: +0.100 × 10⁻⁹⁹
Найбільше додатне: +0.999 × 10⁺⁹⁹
Цей крихітний формат охоплює приблизно від 10⁻¹⁰⁰ до 10⁹⁹ — діапазон у 199 порядків, закодований усього в 7 символах. Формат із фіксованою комою на тому самому обсязі пам'яті покрив би щонайбільше 5 порядків. Для чисел, які сильно відрізняються за масштабом, експоненційний запис незрівнянно ефективніший.Що відбувається з похибками у цій схемі?Максимальна абсолютна похибка = 10⁻ⁿ × 10ᵉ, де n — кількість цифр мантиси, а e — поточна експонента. Варто зазначити, що абсолютна похибка не є сталою — вона масштабується разом із величиною числа. Великі числа допускають більші абсолютні похибки, малі — менші. Це якраз та поведінка, якої ми хотіли, але якої не могли досягти з фіксованою комою.Відносна похибка = 10⁻ⁿ / M. Оскільки мантиса M обмежена (0.1 ≤ |M| &lt; 1 у нашому прикладі), відносна похибка теж обмежена — вона залишається у вузькому діапазоні незалежно від величини числа. У цьому й полягає головна перевага рухомої коми: відносна похибка обмежена по всьому діапазону.Число Абс. похибка Відносна похибка
────────────── ────────────── ──────────────
+0.314 × 10³ ~10⁻³ × 10³ = 1 ~0.3%
+0.314 × 10⁻⁵ ~10⁻³ × 10⁻⁵ ~0.3%
+0.100 × 10⁻⁹⁹ ~10⁻³ × 10⁻⁹⁹ ~1%
Абсолютна похибка пристосовується до масштабу числа, тоді як відносна залишається приблизно сталою. Саме тому рухома кома стала стандартом для наукових обчислень.Вище ми зауважили, що одне й те саме число можна записати в експоненційній формі нескінченно багатьма способами. Щоб усунути цю неоднозначність, визначають дві стандартні форми:Нормальна форма: мантиса задовольняє умову 0 ≤ |M| &lt; 1. У десятковій системі це означає, що мантиса завжди починається з «0.», за якою йдуть значущі цифри:2025 = 0.2025 × 10⁴ (нормальна форма)
0.71 = 0.71 × 10⁰ (нормальна форма)
Однак і нормальна форма все ще не є єдиною: наприклад, і 0.2025 × 10⁴, і 0.02025 × 10⁵ — обидві «нормальні» в тому сенсі, що |M| &lt; 1. Допустимих записів залишається багато.Нормалізована форма: мантиса задовольняє умову 1 ≤ |M| &lt; q, де q — основа системи числення. Для десяткової системи q = 10, тож мантиса — це одна ненульова цифра, за якою йде дробова частина:2025 = 2.025 × 10³ (нормалізована — перша цифра ненульова)
0.71 = 7.1 × 10⁻¹ (нормалізована — перша цифра ненульова)
Нормалізована форма єдина — для будь-якого ненульового числа існує рівно один спосіб записати його з мантисою у діапазоні [1, q). Саме ця єдиність і робить нормалізовану форму стандартом у комп'ютерній арифметиці. У знайомому «E-записі», який використовують мови програмування: 2025 = +2.025E+3, -0.6875 = -6.875E-1.Усе сказане вище однаково стосується і двійкової системи — досить замінити 10 на 2. Двійкове число в експоненційній формі записується так:A = M × 2ᵉде мантису й експоненту можна записати як у двійковій, так і в десятковій системі (контекст завжди прояснює, яка саме). Нормалізована форма вимагає 1 ≤ |M| &lt; 2 (оскільки для двійкової системи q = 2), тобто мантиса завжди має вигляд 1.щось₂.Розгляньмо два приклади.Приклад 1. Переведімо 2012₁₀ у двійкову нормалізовану форму.2012₁₀ = 11111011100₂ = 1.1111011100₂ × 2¹⁰
Ми зсунули двійкову кому на 10 позицій ліворуч, тож експонента дорівнює 10₁₀ = 1010₂. У повністю двійковому записі: 1.11110111₂ × 10₂^1010₂.Приклад 2. Переведімо −0.6875₁₀ у двійкову нормалізовану форму.0.6875₁₀ = 0.1011₂ = 1.011₂ × 2⁻¹
Ми зсунули двійкову кому на 1 позицію праворуч, тож експонента дорівнює −1₁₀. Знак від'ємний. У повністю двійковому записі: −1.011₂ × 10₂^(−1₂). Або ж, із десятковою експонентою: −1.011₂ × 2⁻¹.<br><img alt="Приклади двійкових чисел, переведених у нормалізовану форму з провідною 1 та степенем двійки як експонентою" src="assets/l13-normalized-binary-examples.png" target="_self">Приклади двійкової нормалізованої форми: кожне ненульове двійкове число з рухомою комою можна записати як 1.дріб, помножене на 2 у степені експоненти; це гарантує, що провідна цифра завжди дорівнює 1.Зауважмо особливість двійкових нормалізованих мантис: вони завжди починаються з 1. У нормальній формі (0 ≤ |M| &lt; 1) мантиси починалися б із 0.0... або 0.1..., а нормалізована форма (1 ≤ |M| &lt; 2) гарантує, що провідна цифра — саме 1.Для наочності — ще кілька прикладів обох форм у двійковій системі:
Приклади нормальної мантиси: 0.001010₂, 0.10101011₂, 0.000000001₂ — усі починаються з «0.» і можуть мати провідні нулі після коми.
Приклади нормалізованої мантиси: 1.1010₂, 1.1111111₂, 1.0000001₂ — усі починаються з «1.» і відрізняються лише тим, що стоїть після двійкової коми.
Ось ключове спостереження, яке робить нормалізовану двійкову форму винятково ефективною: якщо провідний біт завжди дорівнює 1, зберігати його немає потреби. Цей біт називають прихованим (або неявним).Замість того щоб зберігати повну мантису M = 1.дріб₂, ми зберігаємо лише дробову частину M' (біти після двійкової коми). Коли потрібна справжня мантиса, її відновлюють за формулою:M = 1 + M' / 2ⁿде n — кількість бітів, відведених під M'. Фактично це дає нам n + 1 бітів точності, хоча зберігаємо ми лише n бітів — один додатковий біт точності завдяки нормалізації, безплатно.Наприклад, якщо мантиса займає 23 збережені біти, фактична точність становить 24 біти — еквівалент приблизно 7,2 значущої десяткової цифри. Без прихованого біта для тієї самої точності знадобилося б 24 збережені біти. У типовому обчисленні з мільярдами чисел з рухомою комою цей один зекономлений біт на число стає відчутним.Є одне застереження: нормалізована мантиса завжди більша або дорівнює 1, а отже, не може задавати нуль. У наступному розділі ми побачимо, як IEEE 754 обробляє нуль та інші спеціальні значення за допомогою зарезервованих шаблонів експоненти.Тепер у нас є всі складові для практичного формату з рухомою комою:
Біт знаку S (0 — додатний, 1 — від'ємний)
Поле експоненти E з m бітів (задає масштаб)
Поле мантиси M' з n бітів (задає точність, із прихованою провідною 1)
Загальна ширина: W = 1 + m + n бітів
Значення числа:A = (−1)ˢ × (1 + M'/2ⁿ) × 2ᵉде збережена експонента E пов'язана зі справжньою експонентою e через зміщення: E = e + зміщення, а саме зміщення = 2ᵐ⁻¹ − 1. Така схема зі зміщенням дозволяє обійтися без окремого знаку для експоненти — усі збережені значення експоненти невід'ємні, що спрощує апаратні порівняння.У наступному розділі ми побачимо, як стандарт IEEE 754 задає конкретні значення m та n для одинарної точності (32 біти), подвійної точності (64 біти) та розширених форматів — і як він обробляє граничні випадки (нуль, нескінченність, NaN), з якими чиста нормалізація не впорається.Тепер у нас є всі складові — подання знак-модуль, експоненціальний запис та нормалізовані двійкові мантиси. Час побачити, як комп'ютер насправді зберігає число з рухомою комою у бітовому рядку фіксованої ширини. Формат елегантний, проте кожне проєктне рішення віддзеркалює апаратний компроміс. Саме розуміння цих компромісів відрізняє того, хто просто «користується float», від того, хто розуміє, чому 0.1 + 0.2 ≠ 0.3.Двійкове число з рухомою комою загальної ширини W бітів поділяється на три суміжні поля:<br><img alt="Кольорова діаграма з трьома полями числа з рухомою комою: біт знаку, біти експоненти та біти мантиси" src="assets/l13-fp-bit-layout.png" target="_self">Бітова структура двійкового числа з рухомою комою: один біт знаку (S), m бітів експоненти (E) та n бітів залишку мантиси (M'), упаковані у W-бітне слово.Загальна ширина:W = 1 + m + n (бітів)
Для одинарної точності IEEE 754: W = 32, m = 8, n = 23. Для подвійної точності: W = 64, m = 11, n = 52. Проте наведені нижче принципи справджуються для будь-якої комбінації (m, n) — стандарт визначає ціле сімейство форматів, а не лише один.Числа з рухомою комою використовують подання знак-модуль, а не доповняльний код. Знак зберігається явно в одному біті:
S = 0 → додатне число (або +0)
S = 1 → від'ємне число (або −0)
Це свідомий відхід від цілочислового світу. Доповняльний код має перевагу єдиного нуля та простих схем додавання, натомість знак-модуль робить тривіальною зміну знаку числа з рухомою комою — достатньо інвертувати біт 31 (чи біт 63). Звідси випливає й те, що перевірити знак float можна одним бітом, а модуль можна порівнювати як беззнакове ціле число за рештою бітів. Розробники апаратури обрали саме ці властивості, поступившись простотою доповняльного коду.Один наслідок: числа з рухомою комою мають два нулі — +0 та −0. Стандарт визначає їх як рівні (+0 == -0 повертає true), однак вони дають різні результати в певних граничних випадках — наприклад, 1.0 / +0.0 = +∞, тоді як 1.0 / -0.0 = -∞. До цих спеціальних значень ми повернемося в наступному розділі.Справжня експонента e може бути як додатною, так і від'ємною — число 0.015625 = 1.0 × 2⁻⁶ має e = −6. Потрібен спосіб зберігати від'ємні експоненти, не додаючи окремий біт знаку для поля експоненти. Розв'язком є зміщене подання (його ще називають кодуванням зі зсувом або з надлишком).Ідея проста: до справжньої експоненти перед збереженням додається фіксована константа, яку називають зміщенням:E = e + зміщення
де:зміщення = 2^(m−1) − 1
Для одинарної точності (m = 8) зміщення дорівнює 2⁷ − 1 = 127. Завдяки цьому збережена експонента E завжди є невід'ємним цілим числом:Звернімо увагу, що значення E = 0 (усі нулі) та E = 255 (усі одиниці) зарезервовані для спеціальних значень (нуль, денормалізовані числа, нескінченність, NaN). Допустимий діапазон для нормалізованих чисел — E ∈ [1, 254], що відповідає e ∈ [−126, +127].Чому для експоненти обрано зміщення, а не доповняльний код? Відповідь — швидкість порівняння. Коли порівнюємо два додатних числа з рухомою комою, об'єднане поле (експонента + мантиса) можна трактувати як єдине беззнакове ціле число і порівнювати його безпосередньо — без жодного спеціального декодування експоненти. Більша зміщена експонента завжди означає більший модуль. Ця властивість зникла б, якби експонента зберігалась у доповняльному коді, адже старший біт від'ємних експонент дорівнював би 1, тож при беззнаковому порівнянні вони «здавалися» б більшими за додатні. Зміщене кодування зберігає природний порядок, а отже, одна схема беззнакового порівняння дозволяє порівнювати два float — саме те, що потрібно розробникам апаратури.Для подвійної точності (m = 11) зміщення дорівнює 2¹⁰ − 1 = 1023, що дає діапазон справжньої експоненти e ∈ [−1022, +1023].Згадаймо з попереднього розділу, що нормалізоване двійкове число завжди має форму:1.xxxxxxx × 2^e
Початкова «1.» присутня завжди — за означенням, нормалізована двійкова мантиса задовольняє умову 1 ≤ M &lt; 2. Оскільки ця початкова 1 присутня завжди, її зберігання було б марнуванням одного біта. Тому формат зберігає лише дробову частину після двійкової коми. Цей прийом називають трюком з прихованим бітом (або неявним бітом).Збережене поле M′ відповідає дробовим цифрам. Повна мантиса M відновлюється за формулою:M = 1 + M′ / 2^n
Ділення на 2ⁿ перетворює n-бітне ціле число M′ у дріб у діапазоні [0, 1). Додавання прихованої 1 дає M ∈ [1, 2) — саме нормалізований діапазон.Отже, хоча зберігається лише n бітів, ефективна точність становить n + 1 біт. Для одинарної точності 23 збережених біти дають 24 біти точності. Для подвійної точності 52 збережених біти дають 53 біти. Цей один безкоштовний біт — подарунок від нормалізованого подання; власне, саме заради нього ми й нормалізували число.Приклад. Якщо n = 23 і M′ = 01100000000000000000000₂ (що дорівнює 3 145 728 у десятковій системі):M = 1 + 3145728 / 2²³ = 1 + 3145728 / 8388608 = 1 + 0.375 = 1.375
Отже, повна мантиса дорівнює 1.375, або, що те саме, 1.011₂ у двійковій системі.Об'єднавши всі три поля, дістаємо значення нормалізованого числа з рухомою комою:<br><img alt="Головна формула декодування нормалізованого числа IEEE 754 з рухомою комою: A дорівнює мінус-один у степені S помножити на один плюс M-штрих поділити на 2 у степені n помножити на 2 у степені E мінус зміщення" src="assets/l13-normalized-fp-formula.png" target="_self">Головна формула для декодування нормалізованого числа IEEE 754 з рухомою комою за його полями знаку, експоненти та мантиси.A = (-1)^S × (1 + M′/2^n) × 2^(E − зміщення)
де зміщення = 2^(m−1) − 1. Розкриваючи зміщення:A = (-1)^S × (1 + M′/2^n) × 2^(E − 2^(m−1) + 1)
Ця єдина формула декодує будь-яке нормалізоване число IEEE 754 незалежно від точності. Потрібні лише три значення з бітового шаблону — S, E та M′ — і два параметри формату — m та n.Перевіримо формулу на найпростішому випадку, перш ніж переходити до складніших прикладів. Розгляньмо 32-бітний шаблон для числа 1.0:0 01111111 00000000000000000000000
S=0 E=127 M′=0
Застосовуючи формулу: A = (−1)⁰ × (1 + 0/2²³) × 2^(127 − 127) = 1 × 1.0 × 2⁰ = 1.0. Правильно.Варто зупинитися й порівняти кодування з рухомою комою з уже знайомими цілочисловими кодуваннями:Ці дві системи розв'язують різні задачі. Доповняльний код оптимізований для точної цілочислової арифметики з єдиною схемою додавання. IEEE 754 оптимізований для наближеної арифметики дійсних чисел з величезним динамічним діапазоном та швидким порівнянням модулів. Жодна з систем не є «кращою» — вони служать різним цілям, і кожен сучасний процесор реалізує обидві.Знати формулу — це одне; вміти впевнено застосовувати її в обох напрямках — зовсім інше. Нижче розглянемо кілька повних прикладів, а потім покажемо, як програмно досліджувати бітові шаблони чисел з рухомою комою.Декодування — простіший напрямок. З 32-бітного шаблону виокремлюємо три поля, підставляємо їх у формулу — і залишається лише обчислити результат.Приклад 1: 01000010111101101110100101111001₂ → 123.456...Насамперед розіб'ємо 32 біти на три поля:0 10000101 11101101110100101111001
S E M′
Звідси одразу зчитуємо значення:
S = 0 → додатне число
E = 10000101₂ = 133₁₀
M′ = 11101101110100101111001₂ = 7 793 017₁₀
Підставляємо у формулу при m = 8, n = 23, зміщення = 127:A = (-1)⁰ × (1 + 7793017 / 2²³) × 2^(133 − 127) = 1 × (1 + 7793017 / 8388608) × 2⁶ = 1 × (1 + 0.929000139...) × 64 = 1 × 1.929000139... × 64 = 123.456009...
Результат — приблизно 123.456. Саме приблизно, адже це десяткове значення не має точного подання у двійковій арифметиці з рухомою комою — наскрізна тема, до якої ще повернемося далі.Приклад 2: 00111111100000000000000000000000₂ = 1.00 01111111 00000000000000000000000
S=0 E=01111111₂=127 M′=0
A = (-1)⁰ × (1 + 0/2²³) × 2^(127 − 127) = 1 × 1.0 × 2⁰ = 1.0
Найпростіше нетривіальне число з рухомою комою. Експонента дорівнює зміщенню (E = 127 означає e = 0), а залишок мантиси складається з самих нулів (тобто M = 1.0 точно). Будь-який степінь двійки має таку ж чисту форму — змінюється лише поле експоненти.Приклад 3: 10111111110000000000000000000000₂ = −1.51 01111111 10000000000000000000000
S=1 E=01111111₂=127 M′=10000000000000000000000₂
У залишку мантиси M′ встановлений лише біт 22, решта — нулі, тому:M′ = 2²² = 4194304
A = (-1)¹ × (1 + 4194304 / 8388608) × 2^(127 − 127) = -1 × (1 + 0.5) × 2⁰ = -1 × 1.5 × 1 = -1.5
Біт знаку робить число від'ємним. Залишок мантиси кодує 0.5 (один встановлений біт — це половина діапазону), тож M = 1.5. Експонента дорівнює нулю (e = 127 − 127 = 0), і результат — просто −1.5.Закономірність: бітовий шаблон 10000000000000000000000 у полі мантиси завжди додає рівно 0.5 до прихованої 1, тож M = 1.5. Аналогічно, 01000000000000000000000 додає 0.25 (M = 1.25), а 11000000000000000000000 додає 0.75 (M = 1.75). Кожен біт мантиси додає степінь двійки, як і в двійковому цілому числі, — тільки ці степені від'ємні: 2⁻¹, 2⁻², 2⁻³ і так далі.Кодування — зворотний процес, і кроків тут більше. Загальний алгоритм такий:
Перевести абсолютне значення числа у двійкову систему (цілу та дробову частини окремо).
Нормалізувати двійкове число: зсунути двійкову кому так, щоб число набуло форми 1.xxxxx × 2^e.
Округлити мантису до n дробових бітів (а якщо їх менше за n — доповнити нулями).
Сформувати M′, відкинувши початкову «1.» від нормалізованої мантиси.
Обчислити E = e + зміщення = e + 2^(m−1) − 1.
Перевести E у m-бітне двійкове число.
Зібрати фінальний бітовий шаблон: S | E | M′.
Пройдімося повним прикладом.<br><img alt="32-бітне кодування IEEE 754 від'ємного числа 2012.6875 з бітом знаку 1, експонентою 10001001 та бітами мантиси" src="assets/l13-example-conversion-bits.png" target="_self">Значення −2012.6875, закодоване як 32-бітне число IEEE 754 одинарної точності, з виділеними полями знаку, зміщеної експоненти та мантиси.Крок 1: Перевести |−2012.6875| у двійкову систему.Ціла частина: 2012₁₀ → двійкова система послідовним діленням на 2:2012 ÷ 2 = 1006 залишок 0
1006 ÷ 2 = 503 залишок 0 503 ÷ 2 = 251 залишок 1 251 ÷ 2 = 125 залишок 1 125 ÷ 2 = 62 залишок 1 62 ÷ 2 = 31 залишок 0 31 ÷ 2 = 15 залишок 1 15 ÷ 2 = 7 залишок 1 7 ÷ 2 = 3 залишок 1 3 ÷ 2 = 1 залишок 1 1 ÷ 2 = 0 залишок 1 Читаючи залишки знизу вгору: 11111011100₂
Дробова частина: 0.6875₁₀ → двійкова система послідовним множенням на 2:0.6875 × 2 = 1.375 → 1
0.375 × 2 = 0.75 → 0
0.75 × 2 = 1.5 → 1
0.5 × 2 = 1.0 → 1 Читаючи зверху вниз: .1011₂
Разом: 2012.6875₁₀ = 11111011100.1011₂Крок 2: Нормалізувати.Зсуваємо двійкову кому вліво доти, доки зліва від неї не залишиться єдина «1»:11111011100.1011₂ = 1.11110111001011₂ × 2¹⁰
Кому перенесено на 10 позицій вліво, тому e = 10.Крок 3: Округлити мантису до 23 дробових бітів.У дробовій частині 11110111001011₂ — 14 бітів, тобто менше за 23. Доповнюємо її нулями справа:11110111001011000000000 (23 біти)
Округлення тут не потрібне — вихідне значення має точне двійкове подання.Крок 4: Сформувати M′.Відкидаємо початкову «1.» — те, що залишилося, і є M′:M′ = 11110111001011000000000
Крок 5: Обчислити зміщену експоненту.E = e + зміщення = 10 + (2⁷ − 1) = 10 + 127 = 137
Крок 6: Перевести E у 8-бітне двійкове число.137₁₀ = 10001001₂
Крок 7: Зібрати S | E | M′.Число від'ємне, отже S = 1. Повний 32-бітний шаблон виглядає так:1 10001001 11110111001011000000000
S E M′
Групування по байтах:1100 0100 1111 1011 1001 0110 0000 0000 C4h FBh 96h 00h
Результат: −2012.6875₁₀ = C4 FB 96 00h в одинарній точності IEEE 754.Результат легко перевірити будь-якою мовою програмування. Закодуймо −2012.6875 і подивімося на байти:import struct
raw = struct.pack('&gt;f', -2012.6875) # big-endian, одинарна точність
print(raw.hex()) # c4fb9600
Виведене c4fb9600 точнісінько збігається з результатом ручного обчислення.Уміння побачити необроблені біти float — важлива навичка налагодження. Нижче — типові ідіоми в кількох мовах:C# — через масив байтів:float f = 1.5f;
byte[] fBytes = BitConverter.GetBytes(f);
// Вивести байти у порядку big-endian (старший байт першим)
for (var i = fBytes.Length - 1; i &gt;= 0; i--) Console.Write(Convert.ToString(fBytes[i], 2).PadLeft(8, '0'));
// На виході: 00111111110000000000000000000000
На виході — 32-бітний шаблон числа 1.5: S=0, E=01111111, M′=10000000000000000000000. Він збігається з декодуванням у Прикладі 3 (без біта знаку).Python — через модуль struct:import struct # Запакувати як big-endian float і отримати необроблені байти
raw = struct.pack('!f', 1.5)
print(raw) # b'\x3f\xc0\x00\x00'
print(raw.hex()) # 3fc00000 # Щоб побачити окремі біти:
bits = int.from_bytes(raw, 'big')
print(f'{bits:032b}') # 00111111110000000000000000000000
Java — через Float.floatToIntBits:float f = 1.5f;
int bits = Float.floatToIntBits(f);
System.out.println(Integer.toBinaryString(bits));
// На виході: 111111110000000000000000000000 (початкові нулі пропущено) // Для подвійної точності:
double d = 1.5;
long dBits = Double.doubleToLongBits(d);
System.out.println(Long.toBinaryString(dBits));
C — через union (переінтерпретація типу):#include &lt;stdio.h&gt;
#include &lt;stdint.h&gt; union FloatBits { float f; uint32_t u;
}; int main() { union FloatBits fb; fb.f = -2012.6875f; printf("0x%08X\n", fb.u); // 0xC4FB9600 return 0;
}
Усі чотири підходи дають ті самі 32 біти — відрізняються лише мовні механізми, що переінтерпретують пам'ять float як ціле число.Нижче — кілька 32-бітних шаблонів для декодування. Спробуймо розібрати кожен за формулою, перш ніж зазирати у відповідь:Особливо повчальний шаблон для π (40490FDB): мантиса зберігає найкраще 23-бітне наближення π/2, а експонента +1 подвоює його до ≈ 3.14159.Під час кодування −2012.6875 дробова частина 0.6875 обернулася на скінченний двійковий дріб (.1011₂) — усього за чотири біти. Та більшості десяткових дробів так не щастить.Розгляньмо 0.1₁₀. Застосуємо алгоритм послідовного множення:0.1 × 2 = 0.2 → 0
0.2 × 2 = 0.4 → 0
0.4 × 2 = 0.8 → 0
0.8 × 2 = 1.6 → 1
0.6 × 2 = 1.2 → 1
0.2 × 2 = 0.4 → 0 ← цикл повторюється звідси
0.4 × 2 = 0.8 → 0
...
Двійковий запис 0.1 — це 0.0001100110011001100110011...₂, нескінченно повторюваний шаблон. Щоб умістити його у 23 біти мантиси, доводиться обрізати або округлити, що вносить похибку порядку 10⁻⁸. Саме через цю крихітну похибку в кожній мові програмування, яка спирається на IEEE 754, маємо 0.1 + 0.2 ≠ 0.3: похибки подання 0.1 та 0.2 не компенсують одна одну так, щоб у сумі дати точне подання 0.3.Десятковий дріб d/10ⁿ має точне двійкове подання лише тоді, коли його можна записати як k/2ᵐ для деяких цілих k та m, — інакше кажучи, лише якщо розкладання знаменника на прості множники не містить нічого, крім двійок. Оскільки 10 = 2 × 5, будь-який дріб, у знаменнику якого є множник 5, дасть нескінченний двійковий запис. Значення 0.5, 0.25, 0.125 і 0.6875 мають скінченне подання, бо їхні знаменники — чисті степені двійки; натомість 0.1, 0.2 і 0.3 такого подання не мають.Тепер, коли ми розуміємо, як кодуються числа з рухомою комою — знак, зміщена експонента, мантиса з прихованим бітом, — можна поставити два найпрактичніші запитання щодо будь-якого формату з рухомою комою: наскільки великим (або малим) може бути число і наскільки точно воно представлене?Відповіді на обидва запитання є чіткими та залежать від двох параметрів: кількості бітів експоненти m і кількості бітів мантиси n.Поле експоненти визначає порядок представлюваних чисел — наскільки далеко від нуля вони сягають в обох напрямках.Згадаймо загальну формулу для нормалізованого числа з рухомою комою:A = (-1)^S × (1 + M'/2^n) × 2^e
де справжня експонента e змінюється від e_min до e_max. Для формату з m бітами експоненти та зміщенням = 2^(m−1) − 1:e_min = 1 - bias = 2 - 2^(m-1)
e_max = 2^m - 2 - bias = 2^(m-1) - 1
(Крайні значення поля експоненти — усі нулі та усі одиниці — зарезервовані для спеціальних значень, тому робочий діапазон становить від 1 до 2^m − 2.)Максимальне представлюване значення досягається за найбільшої мантиси (усі біти мантиси дорівнюють 1, тобто M ≈ 2 − 2^(−n)) та найбільшої експоненти:A_max = M_max × 2^(e_max) ≈ 2 × 2^(e_max) = 2^(e_max + 1)
Мінімальне нормалізоване значення досягається за найменшої мантиси (M = 1.0, лише прихований біт) та найменшої експоненти:A_min = M_min × 2^(e_min) = 1.0 × 2^(e_min) = 2^(e_min)
Для одинарної точності m = 8, тому зміщення дорівнює 127:e_max = 2^7 - 1 = 127
e_min = 2 - 2^7 = -126 A_max = (2 - 2^(-23)) × 2^127 ≈ 2^128 ≈ 3.40 × 10^38 A_min (нормалізоване) = 1.0 × 2^(-126) ≈ 1.17 × 10^(-38)
Отже, одне 32-бітне слово представляє числа приблизно від ±1,17 × 10⁻³⁸ до ±3,40 × 10³⁸. Динамічний діапазон — відношення найбільшого додатного представлюваного числа до найменшого — становить приблизно 10⁷⁶, тобто близько 76 порядків величини.Щоб оцінити, наскільки це вражає, пригадаймо приклад із фіксованою комою з Розділу 3: 16-бітний формат із 8 цілими та 8 дробовими бітами охоплював діапазон від 0 до приблизно 256 — близько 2,4 порядку величини. Натомість 32-бітний формат із рухомою комою за тієї самої загальної кількості бітів охоплює 76 порядків. Експоненційне кодування незрівнянно ефективніше для охоплення широкого діапазону.Якщо експонента визначає діапазон, то мантиса визначає точність — кількість значущих цифр у збереженому значенні.Формат із рухомою комою з n збереженими бітами мантиси завдяки прихованому біту фактично має n + 1 бітів точності. Оскільки кожен двійковий розряд несе log₁₀(2) ≈ 0,30103 десяткової цифри інформації, кількість значущих десяткових цифр становить:k = (n + 1) × log₁₀(2) ≈ 0.301 × (n + 1)
Корисне й зворотне співвідношення: для представлення однієї десяткової цифри потрібно log₂(10) ≈ 3,32 двійкової цифри — тобто приблизно 3,32 біта мантиси на одну десяткову цифру точності.Для одинарної точності надійними є приблизно 7 значущих десяткових цифр. Тобто якщо зберегти число 12345678 у float, остання цифра (або навіть дві) виявиться неправильною — значення може повернутися як 12345680. Для подвійної точності надійними є приблизно 15–16 десяткових цифр — більш ніж достатньо для переважної більшості наукових обчислень, проте все одно скінченна кількість.За фіксованого загального розміру слова W = 1 + m + n кожен біт, доданий до поля експоненти, подвоює діапазон, але відбирає один біт точності — і навпаки. Комітет IEEE 754 підібрав розподіл бітів між експонентою та мантисою для кожного формату так, щоб збалансувати ці суперечливі вимоги:Одинарна (32 біти): 8 експонента + 23 мантиса → 76 порядків величини, 7 цифр
Подвійна (64 біти): 11 експонента + 52 мантиса → 616 порядків величини, 16 цифр
Подвійна точність — це не просто «вдвічі краще» за одинарну: вона має на три біти більшу експоненту (увосьмеро ширший діапазон у степенях двійки) та на 29 бітів довшу мантису (понад мільярд разів тонша роздільність).Кожен формат із рухомою комою має три режими збою — три способи, якими обчислення перевищує можливості формату. Розуміння всіх трьох необхідне для написання надійного чисельного коду.<br><img alt="Діаграма числової прямої із зонами від'ємного переповнення, діапазоном представлюваних чисел, зоною втрати значимості біля нуля та додатного переповнення" src="assets/l13-overflow-underflow-number-line.png" target="_self">Числова пряма з рухомою комою: зони переповнення за межами діапазону представлюваних чисел, зона втрати значимості / денормалізованих чисел біля нуля та проміжки усікання між сусідніми представлюваними значеннями.Переповнення виникає, коли результат обчислення має експоненту, більшу за e_max, — число занадто велике (за абсолютним значенням), щоб його представити у форматі. В одинарній точності переповнення спричиняє будь-який результат, величина якого перевищує приблизно 3,40 × 10³⁸. IEEE 754 обробляє переповнення, генеруючи ±∞ (додатну або від'ємну нескінченність), яка потім поширюється через подальші обчислення.import struct huge = 3.4e38
result = huge * 2.0 # Переповнення!
print(result) # inf
Втрата значимості виникає, коли результат обчислення має експоненту, меншу за e_min, — число надто близьке до нуля, щоб представити його як нормалізоване значення. В одинарній точності втрату значимості спричиняє будь-який результат, менший за приблизно 1,17 × 10⁻³⁸. IEEE 754 обробляє втрату значимості поступово, переходячи на денормалізовані числа (Розділ 8), які втрачають точність, але уникають різкого стрибка до нуля. Якщо результат менший навіть за найменше денормалізоване значення, він округлюється до нуля.tiny = 1.17e-38
result = tiny / 1000.0 # Втрата значимості до денормалізованого діапазону
print(result) # 1.17e-41 (денормалізоване — менше значущих цифр) tinier = tiny / 1e20
print(tinier) # 0.0 (повна втрата значимості до нуля)
Усікання (округлення) виникає, коли результат обчислення має більше значущих бітів, ніж вміщує мантиса. Таке трапляється практично за кожної операції — це найпоширеніший із трьох типів. Зайві біти відкидаються за правилом округлення (IEEE 754 визначає чотири режими округлення; за замовчуванням використовується «округлення до найближчого парного»). Усікання не породжує спеціальних значень — воно лише вносить невелику відносну похибку, обмежену машинним епсилоном, тобто приблизно 2^(−n): близько 1,19 × 10⁻⁷ для одинарної точності та 2,22 × 10⁻¹⁶ для подвійної.Три типи обмежень стають інтуїтивно зрозумілими, якщо уявити «числову пряму» з рухомою комою.На відміну від цілочисельної числової прямої, де представлювані значення розташовані рівномірно, числова пряма з рухомою комою має характерний нерівномірний розподіл:
Біля нуля представлювані значення розташовані щільно. В одинарній точності між 1,0 і 2,0 вміщується 2²³ ≈ 8,4 мільйона різних значень — крок становить приблизно 1,19 × 10⁻⁷.
Далеко від нуля представлювані значення розріджені. Між 2¹²⁷ і 2¹²⁸ крок між сусідніми значеннями дорівнює 2¹²⁷ × 2⁻²³ = 2¹⁰⁴ ≈ 2 × 10³¹ — кожен «крок» перевищує 10³¹.
Ця нерівномірність кроку — не вада, а перевага. Відносний крок між сусідніми представлюваними значеннями приблизно сталий (близько 2⁻²³ для одинарної точності), а отже, відносна похибка будь-якого округленого результату приблизно однакова незалежно від того, чи йдеться про число 10⁻³⁰ чи 10³⁰. Фіксована кома дає сталу абсолютну похибку; рухома кома — сталу відносну. Для більшості застосувань стала відносна похибка значно корисніша.<br><img alt="Діаграми бітових шаблонів, що показують максимальне нормалізоване значення з усіма встановленими бітами експоненти та мантиси, і мінімальне денормалізоване значення з лише останнім бітом мантиси" src="assets/l13-max-min-fp-bit-diagrams.png" target="_self">Бітові шаблони для двох крайніх значень діапазону одинарної точності: максимальне нормалізоване значення (експонента 11111110, мантиса — усі одиниці) та мінімальне додатне денормалізоване значення (експонента — усі нулі, мантиса 000...001).Діаграма вище показує максимальне нормалізоване та мінімальне денормалізоване значення для одинарної точності — дві крайні точки діапазону представлюваних чисел. Між цими кінцями лише окремі дійсні числа потрапляють точно на «позначку» (представлюване значення); решта округлюється до найближчої позначки.Ліворуч і праворуч від діапазону представлюваних чисел лежать зони переповнення — числа, надто великі для формату. У вузькій смузі навколо нуля — зони втрати значимості та денормалізованих чисел. А скрізь уздовж прямої, між будь-якими двома сусідніми позначками, залишається нескінченно багато дійсних чисел, які доводиться округлювати до однієї з двох сусідніх позначок — це і є область усікання.Усе, що ми обговорювали досі — кодування знак-експонента-мантиса, зміщені експоненти, приховані біти — існувало в різних формах ще до 1985 року. Різні виробники реалізовували власні формати з рухомою комою: мейнфрейми IBM використовували шістнадцяткову (з основою 16) арифметику з рухомою комою; DEC VAX мав свій формат; суперкомп'ютери Cray — ще один. Програма, яка давала правильні результати на одній машині, могла давати інші (а то й хибні) результати на іншій. Гірше того, деякі реалізації навіть не обробляли граничні випадки на кшталт ділення на нуль узгоджено: одні генерували винятки, інші повертали «сміття».Стандарт IEEE 754 виник із потреби покласти край цьому хаосу. Роботу очолив Вільям Каган, математик з Каліфорнійського університету в Берклі, який роками досліджував, як похибки з рухомою комою поширюються в ході обчислень. Ключове розуміння Кагана полягало в тому, що повноцінний стандарт потребує більшого, ніж просто формат даних — він має визначати поведінку: що відбувається при переповненні, при втраті значимості, при діленні на нуль, при операціях, для яких немає математичної відповіді.Стандарт було ухвалено у 1985 році як IEEE 754-1985. Відтоді його переглядали двічі — у 2008 році (IEEE 754-2008, де додано десяткові формати та формат четвірної точності) та у 2019 році (IEEE 754-2019, поточна версія, також опублікована як ISO/IEC 60559:2020). Проте основні формати одинарної та подвійної точності залишилися незмінними з часів первісної специфікації 1985 року.До появи процесора Intel 80486 (1989) системи x86 не мали вбудованого апаратного забезпечення для операцій з рухомою комою. Такі операції виконував окремий чіп — математичний співпроцесор: Intel 8087 (1980), 80287 (1982) або 80387 (1987). 80387 став першим співпроцесором x87, що повністю відповідав IEEE 754. Починаючи з 80486, Intel інтегрувала FPU безпосередньо в процесор, і відтоді кожен процесор x86 реалізує IEEE 754 апаратно. Сьогодні практично кожен процесор загального призначення — x86, ARM, RISC-V — відповідає цьому стандарту.Стандарт визначає чотири основні двійкові формати, що відрізняються загальною шириною та розподілом бітів між експонентою і мантисою:<br><img alt="Таблиця двійкових форматів IEEE 754 з рухомою комою, що показує розрядність, розміри експоненти, розміри мантиси та значення зміщення для одинарної, одинарної розширеної, подвійної та подвійної розширеної точності" src="assets/l13-ieee754-formats-table.png" target="_self">Чотири основні двійкові формати IEEE 754: одинарна (32-біт), одинарна розширена, подвійна (64-біт) та подвійна розширена (80-біт) точність із шириною експоненти, шириною мантиси та значеннями зміщення.Одинарна точність (32 біти) — основа графіки, ігор та вбудованих систем, скрізь, де пропускна здатність пам'яті важливіша за надзвичайну точність. Тип float у C, стандарт GPU та більшість форматів сенсорних даних використовують одинарну точність.Подвійна точність (64 біти) — стандарт для наукових обчислень, фінансів та більшості програмування загального призначення. Тип double у C, float у Python та єдиний числовий тип JavaScript (до появи BigInt) — усе це подвійна точність.Одинарну розширену точність було призначено для проміжних обчислень у FPU, який використовує одинарну точність для зберігання. Стандарт визначає лише мінімальні вимоги (≥43 біти, ≥11 бітів експоненти, ≥31 біт мантиси), залишаючи точну ширину на розсуд розробника. На практиці цей формат рідко реалізовували як окремий тип.Подвійну розширену точність (на практиці 80 бітів) було розроблено з тією самою метою на рівні подвійної точності — як додатковий запас для проміжних результатів. FPU x87 від Intel використовує внутрішній 80-бітний формат подвійної розширеної точності (15 бітів експоненти, 64 біти мантиси разом із явним цілочисельним бітом — без прихованого біту). Цей формат, доступний як long double у деяких компіляторах C на x86, забезпечує близько 18–19 значущих десяткових цифр.Переважна більшість значень з рухомою комою — це нормалізовані числа. Число є нормалізованим, коли його поле експоненти E не складається ані лише з нулів, ані лише з одиниць — тобто 1 ≤ E ≤ 2^m − 2.Значення нормалізованого числа одинарної точності:A = (-1)^S × (1 + M'/2^23) × 2^(E - 127)
Провідна «1.» тут неявна (це і є прихований біт). Збережена мантиса M' кодує лише дробову частину після двійкової крапки. Це означає:
Найменша нормалізована мантиса — 1,000…0₂ = 1,0 (усі біти мантиси — нулі).
Найбільша нормалізована мантиса — 1,111…1₂ = 2 − 2⁻²³ ≈ 1,99999988 (усі біти мантиси — одиниці).
У поєднанні з діапазоном експоненти 1 ≤ E ≤ 254 (справжня експонента від −126 до +127) це дає звичний діапазон одинарної точності — приблизно від ±1,17 × 10⁻³⁸ до ±3,40 × 10³⁸.У нормалізованому поданні є очевидна складність: мантиса завжди починається з «1.» (прихований біт), тож вона завжди ≥ 1,0, а значення завжди ненульове. Як же подати нуль?IEEE 754 розв'язує цю проблему, резервуючи значення поля експоненти з усіма нулями. Коли і експонента, і мантиса — усі нулі, значення дорівнює нулю:+0 = 00 00 00 00h (усі 32 біти — нулі)
-0 = 80 00 00 00h (одиниця лише в біті знаку)
Так, IEEE 754 має два подання нуля: додатний нуль (+0) та від'ємний нуль (−0). За правилами порівняння стандарту, +0 і −0 є рівними:print(0.0 == -0.0) # True
print(0.0 is -0.0) # True (у CPython; деталь реалізації)
Проте вони не ідентичні в усіх контекстах. Знак нуля відіграє роль у певних операціях:import math print(1.0 / 0.0) # inf (+∞)
print(1.0 / -0.0) # -inf (-∞)
print(math.copysign(1.0, 0.0)) # 1.0
print(math.copysign(1.0, -0.0)) # -1.0
Існування −0 зберігає знак в обчисленнях, результат яких зазнає втрати значимості до нуля. Без −0 вираз 1.0 / (1.0 / x) втрачав би знак x щоразу, коли проміжний результат 1.0 / x зникав через втрату значимості.Розгляньмо, що відбувається, коли обчислення дає результат, менший за найменше нормалізоване число (приблизно 1,17 × 10⁻³⁸ в одинарній точності). Без спеціальної обробки це значення просто стало б нулем — явище, яке називають різкою втратою значимості. Стрибок від 1,17 × 10⁻³⁸ до 0 величезний у відносному вимірі, і він здатен спричинити чисельну нестабільність. Наприклад, перевірка x - y == 0 більше не гарантувала б надійно, що x == y.IEEE 754 уникає цього обриву, запроваджуючи денормалізовані числа (їх також називають субнормальними числами). Коли поле експоненти — усі нулі, а мантиса ненульова, число є денормалізованим:<br><img alt="Формула декодування денормалізованих чисел IEEE 754: A дорівнює мінус один у степені S помножити на нуль плюс M' поділене на 2 у степені n помножити на 2 у степені (2 мінус 2 у степені m мінус 1)" src="assets/l13-denormalized-fp-formula.png" target="_self">Формула декодування денормалізованих (субнормальних) чисел: прихований біт дорівнює 0 замість 1, а експонента фіксована на e_min, що забезпечує поступову втрату значимості до нуля.A = (-1)^S × (0 + M'/2^n) × 2^(2 - 2^(m-1))
Ключові відмінності від нормалізованих чисел:
Прихований біт дорівнює нулю, а не одиниці — мантиса має вигляд 0.дріб замість 1.дріб.
Експонента фіксована на e_min = 2 − 2^(m−1) (= −126 для одинарної точності) незалежно від збереженого поля експоненти (яке повністю нульове).
Для одинарної точності формула денормалізованих чисел набуває вигляду:A = (-1)^S × (M'/2^23) × 2^(-126) = (-1)^S × M' × 2^(-149)
Найменше денормалізоване число одинарної точності має M' = 1 (одиниця лише в останньому біті мантиси):A_min = 1 × 2^(-149) ≈ 1.40 × 10^(-45)
Найбільше денормалізоване число одинарної точності має M' = 2²³ − 1 (усі біти мантиси — одиниці):A_max_denorm = (2^23 - 1) × 2^(-149) ≈ 1.17 × 10^(-38)
Звернімо увагу, що найбільше денормалізоване число приблизно дорівнює найменшому нормалізованому. Це забезпечує поступову втрату значимості — коли числа стають меншими за нормалізований діапазон, вони плавно втрачають точність (по одному біту за раз), а не стрибком падають до нуля. Ціна поступової втрати значимості — зменшена точність: денормалізовані числа мають менше значущих бітів, ніж нормалізовані, адже провідні нулі в мантисі займають бітові позиції, не додаючи інформації.Коли поле експоненти — усі одиниці, а поле мантиси — усі нулі, значення відповідає нескінченності:+∞ = 7F 80 00 00h (S=0, E=FFh, M'=0)
-∞ = FF 80 00 00h (S=1, E=FFh, M'=0)
Нескінченності виникають природно при переповненні та при діленні на нуль. IEEE 754 визначає нескінченність як повноцінне значення, яке бере участь в арифметиці:x = float('inf')
print(x + 1) # inf (∞ + будь-яке скінченне число = ∞)
print(x * 2) # inf
print(x &gt; 1e308) # True (∞ більше за будь-яке скінченне число)
print(-x &lt; -1e308) # True
print(1.0 / x) # 0.0 (скінченне / ∞ = 0)
Ділення на нуль дає нескінченність або NaN залежно від чисельника:<br><img alt="Кускова формула для ділення на нуль: додатне x поділене на 0 дає додатну нескінченність, від'ємне x поділене на 0 дає від'ємну нескінченність, нуль поділений на нуль дає NaN" src="assets/l13-division-by-zero-formula.png" target="_self">Ділення на нуль за IEEE 754: результат залежить від знаку чисельника, породжуючи додатну нескінченність, від'ємну нескінченність або NaN для невизначеного випадку 0/0.x / 0 = +∞ якщо x &gt; 0
x / 0 = -∞ якщо x &lt; 0
x / 0 = NaN якщо x = 0 (0/0 — невизначена форма)
Коли поле експоненти — усі одиниці, а поле мантиси ненульове, значення відповідає NaN — «не число». NaN виникає як результат математично невизначених або таких, що не мають скінченної числової відповіді, операцій:<br><img alt="32-бітний бітовий шаблон NaN з одиницею в біті знаку, усіма бітами експоненти рівними 1 та ненульовим полем мантиси" src="assets/l13-nan-bit-layout.png" target="_self">Бітовий шаблон NaN (не число) в одинарній точності: поле експоненти — усі одиниці (як і в нескінченності), але мантиса ненульова — саме це й відрізняє NaN від нескінченності.Бітовий шаблон NaN (одинарна точність): S=1, E=FF h, M'=ненульове (напр., FF C0 00 00h)
Операції, які породжують NaN:0 / 0 (невизначена форма)
∞ - ∞ (невизначена форма)
0 × ∞ (невизначена форма)
∞ / ∞ (невизначена форма)
√(x) де x &lt; 0 (комплексний результат, що не подається у форматі)
NaN має дві визначальні властивості.NaN є «заразним». Будь-яка арифметична операція за участю NaN дає NaN:x = float('nan')
print(x + 1) # nan
print(x * 0) # nan
print(x ** 0) # 1.0 (виняток: за домовленістю будь-що^0 = 1, навіть NaN^0)
NaN не дорівнює нічому — навіть самому собі:x = float('nan')
print(x == x) # False!
print(x != x) # True
print(x &lt; 0) # False
print(x &gt; 0) # False
print(x == 0) # False
Остання властивість часто слугує для перевірки на NaN: if x != x істинне лише тоді, коли x — це NaN. (На практиці задля прозорості коду краще використовувати math.isnan(x).)Стандарт розрізняє два типи NaN:
Тихий NaN (qNaN): мовчки поширюється обчисленнями, не спричиняючи винятку. Старший біт мантиси дорівнює 1.
Сигнальний NaN (sNaN): спричиняє виняток недійсної операції, щойно бере участь в арифметиці. Старший біт мантиси дорівнює 0. На практиці сигнальні NaN застосовують рідко; їх задумували для налагодження (наприклад, для позначення неініціалізованих змінних).
Наступна таблиця підсумовує всі можливі категорії значень одинарної точності з рухомою комою, виходячи з полів експоненти та мантиси:Ця таблиця з п'яти рядків — повна специфікація семантики IEEE 754 для одинарної точності. Кожен 32-бітний шаблон належить рівно до однієї з цих п'яти категорій. Невикористаних бітових шаблонів немає — кожна можлива комбінація бітів має чітко визначене значення.Щоб перетворити десяткове число у формат одинарної точності IEEE 754, виконаймо такі кроки:
Визначаємо знак: S = 0, якщо число додатне, S = 1, якщо від'ємне. Далі працюємо з абсолютним значенням.
Переводимо у двійкову систему: цілу та дробову частини — окремо (цілу — послідовним діленням, дробову — послідовним множенням на 2).
Нормалізуємо: зсуваємо двійкову крапку так, щоб число мало вигляд 1.дріб × 2^e.
Округлюємо мантису: якщо дробова частина довша за 23 біти, округлюємо до 23 бітів.
Формуємо M': відкидаємо провідну «1.» — решта 23 біти і є збереженою мантисою.
Обчислюємо зміщену експоненту: E = e + 127. Переводимо E у 8 бітів.
Збираємо: конкатенуємо S (1 біт) | E (8 бітів) | M' (23 біти).
Крок 1 — знак. Число від'ємне, тому S = 1.Крок 2 — переведення у двійкову систему.Ціла частина: 2012₁₀ = 11111011100₂Дробова частина: 0,6875₁₀ → послідовне множення на 2:0.6875 × 2 = 1.375 → 1
0.375 × 2 = 0.75 → 0
0.75 × 2 = 1.5 → 1
0.5 × 2 = 1.0 → 1
Отже, 0,6875₁₀ = 0,1011₂Разом: 2012,6875₁₀ = 11111011100,1011₂Крок 3 — нормалізація.11111011100.1011₂ = 1.11110111001011₂ × 2^10
Двійкова крапка зсунулася на 10 позицій ліворуч, тому e = 10.Крок 4 — мантиса M'. Дробова частина після «1.» — це 11110111001011. Маємо 14 значущих бітів після крапки — цілком у межах 23-бітного обмеження, тож округлювати не потрібно. Доповнюємо нулями до 23 бітів:M' = 11110111001011000000000₂
Крок 5 — зміщена експонента.E = e + 127 = 10 + 127 = 137₁₀ = 10001001₂
Крок 6 — збирання.S | E | M'
1 | 10001001 | 11110111001011000000000 Hex: C4 FB 96 00h
Для перевірки: біт знаку дорівнює 1 (число від'ємне), зміщена експонента — 137 (справжня експонента 10, що означає: число лежить між 2¹⁰ = 1024 та 2¹¹ = 2048), а мантиса кодує значущі цифри числа 2012,6875. Усе сходиться.Цінність IEEE 754 виходить далеко за межі визначення формату даних. Стандарт задає:
Детерміновані результати: одна й та сама операція з однаковими вхідними даними має давати однаковий результат у кожній сумісній реалізації. Це необхідно як для відтворюваної науки, так і для налагодження.
Плавну деградацію: переповнення дає ±∞ (а не «сміття»), втрата значимості дає денормалізовані числа (а не нуль), невизначені операції дають NaN (а не аварійне завершення) — і все це без зупинки програми.
Порівняння: значення з рухомою комою можна порівнювати тим самим апаратним забезпеченням, що й цілі числа (адже зміщена експонента коректно сортується як беззнакове ціле), зі спеціальною обробкою NaN.
Правила округлення: визначено чотири режими округлення, з «округленням до найближчого парного» (банківським округленням) за замовчуванням — воно мінімізує систематичне зміщення в тривалих обчисленнях.
Стандарт IEEE 754 визначає не лише спосіб кодування чисел, а й те, як мають поводитися арифметичні операції. Кожна сумісна реалізація повинна давати однаковий результат для однакових вхідних даних — біт у біт. У цьому розділі розглянемо алгоритми, які апаратне забезпечення використовує для трьох фундаментальних операцій: додавання/віднімання, множення та ділення.Додавання двох чисел з рухомою комою, на диво, складніше за їхнє множення. Причина в тому, що операнди можуть мати різні експоненти — різні масштаби — а додавати мантиси можна лише після того, як їх вирівняно до однакового масштабу. Алгоритм складається з чотирьох кроків.Крок 1: Вирівнювання експонент. Порівнюємо дві експоненти. Зсуваємо мантису меншого операнда вправо на різницю експонент, щоб обидва операнди мали однакову експоненту. Кожен зсув вправо відкидає молодший біт, тому на цьому кроці частина точності меншого операнда може втратитися.Крок 2: Додати або відняти мантиси. Після вирівнювання виконуємо цілочислове додавання або віднімання мантис (знакові біти визначають, яку саме операцію). Якщо це віднімання, а операнди близькі за величиною, багато старших бітів можуть скоротитися.Крок 3: Нормалізація результату. Сума може спричинити переповнення мантиси (наприклад, 1.111 + 1.001 = 11.000₂) або її втрату значимості (наприклад, 1.000 − 0.111 = 0.001₂). Щоб виправити переповнення, зсуваємо результат вправо (збільшуючи експоненту); щоб виправити втрату значимості — вліво (зменшуючи експоненту) — доки мантиса не повернеться до форми 1.xxxxx₂.Крок 4: Округлення. Результат може мати більше бітів, ніж вміщує поле мантиси. Застосовуємо поточний режим округлення (див. нижче), щоб отримати остаточне збережене значення.Простежмо додавання 1.5₁₀ та 0.125₁₀ крок за кроком.Спочатку переведемо числа в нормалізоване двійкове подання з рухомою комою:1.5₁₀ = 1.1₂ × 2⁰ (мантиса = 1.1, експонента = 0)
0.125₁₀ = 1.0₂ × 2⁻³ (мантиса = 1.0, експонента = −3)
Вирівнювання експонент. Менша експонента — −3; більша — 0. Зсуваємо мантису 0.125 вправо на 3 позиції, щоб вирівняти:1.0₂ × 2⁻³ → 0.001₂ × 2⁰
Додавання мантис (обидві додатні, однакова експонента): 1.100₂
+ 0.001₂
───────── 1.101₂
Нормалізація. Результат 1.101₂ × 2⁰ уже нормалізований (старший біт дорівнює 1).Округлення. Зайвих бітів для відкидання немає. Кінцевий результат — 1.101₂ × 2⁰ = 1.625₁₀. Правильно.Цей приклад був «чистим», але уявімо, що відбувається, коли експоненти відрізняються на 20 і більше: мантиса меншого операнда зсувається настільки далеко вправо, що більшість її значущих бітів випадає за межі поля мантиси. Ось чому додавання дуже малого числа до дуже великого може дати результат, рівний лише великому числу: мале число «поглинається».Множення структурно простіше за додавання, адже не потребує кроку вирівнювання — експоненти просто додаються.Крок 1: XOR знакових бітів. Додатне × додатне = додатне, додатне × від'ємне = від'ємне і так далі. XOR двох знакових бітів дає правильний знак результату.Крок 2: Додати експоненти. Оскільки кожна експонента зберігається зі зміщенням, сума двох зміщених експонент містить зміщення двічі. Тому зміщення треба відняти один раз: e_result = (E₁ − bias) + (E₂ − bias) + bias = E₁ + E₂ − bias.Крок 3: Помножити мантиси. Розглядаємо обидві мантиси (кожна з діапазону [1.0, 2.0)) як числа з фіксованою комою і перемножуємо їх. Добуток лежить у діапазоні [1.0, 4.0) і тому має форму 1.xxxxx₂ або 10.xxxxx₂ (тобто 1 або 2 цілих біти).Крок 4: Нормалізація та округлення. Якщо ціла частина добутку дорівнює 10₂ (тобто добуток ≥ 2.0), зсуваємо його вправо на один біт і збільшуємо експоненту. Потім округлюємо мантису до доступної розрядності.Ключова перевага множення в тому, що обидва операнди вносять у результат свою повну точність — втрат через вирівнювання немає. Єдина втрата точності відбувається на кроці кінцевого округлення, де відкидаються молодші біти добутку подвійної ширини.Ділення — дзеркальне відображення множення з оберненими операціями.Крок 1: XOR знакових бітів — так само, як у множенні.Крок 2: Відняти експоненти. Експонента результату: e_result = (E₁ − bias) − (E₂ − bias) + bias = E₁ − E₂ + bias. (Додавання зміщення компенсує його подвійне віднімання.)Крок 3: Поділити мантиси. Частка двох чисел із діапазону [1.0, 2.0) лежить у (0.5, 2.0). Якщо вона менша за 1.0, зсуваємо її вліво і зменшуємо експоненту.Крок 4: Нормалізація та округлення — аналогічно множенню.Ділення — найповільніша з трьох операцій у апаратному забезпеченні. Для ділення мантис зазвичай застосовують ітеративні алгоритми (наприклад, SRT-ділення або наближення оберненого числа методом Ньютона — Рафсона), а не єдину комбінаційну схему.Після кожної арифметичної операції «нескінченно точний» результат треба округлити, щоб він вмістився в мантису цільового формату. IEEE 754 визначає чотири режими округлення:Округлення до найближчого парного (за замовчуванням). Обирається найближче до точного результату представне значення. Якщо точний результат потрапляє рівно посередині між двома представними значеннями, обирається те, у якого молодший біт мантиси дорівнює 0 (тобто парне). Це режим за замовчуванням практично в усіх апаратних і програмних реалізаціях; іноді його називають «банківським округленням».Округлення до нуля (усікання). Просто відкинути зайві біти — завжди рухатися до нуля. Саме так діє цілочислове усікання. Цей режим вносить систематичне від'ємне зміщення для додатних чисел і додатне — для від'ємних.Округлення до +∞ (стеля). Завжди округлювати в бік додатної нескінченності: вгору для додатних чисел, до нуля — для від'ємних.Округлення до −∞ (підлога). Завжди округлювати в бік від'ємної нескінченності: вниз для додатних чисел, від нуля — для від'ємних.Чому режимом за замовчуванням є «до найближчого парного», а не інтуїтивніше «до +∞» (правило зі школи: 0.5 округлюється вгору)? Причина — статистичне зміщення. Якщо завжди округлювати 0.5 вгору, у довгому обчисленні з багатьма серединними випадками сума систематично зростатиме. Округлення до парного забезпечує, що в середньому половина серединних випадків округлюється вгору, а половина — вниз, і таким чином усуває зміщення. Це має величезне значення у фінансових обчисленнях, наукових симуляціях та будь-якому ітеративному алгоритмі, що виконує мільйони операцій округлення.Щоб реалізувати коректне округлення в апаратному забезпеченні, потрібно знати про точний результат більше, ніж лише ті біти, що вміщуються в мантису. Апаратне забезпечення, сумісне з IEEE 754, під час проміжних обчислень підтримує три додаткові біти поза мантисою:
Захисний біт (G): перший біт за межами мантиси — старший із відкинутих бітів.
Округлювальний біт (R): другий біт за межами мантиси.
Фіксований біт (S): логічне АБО всіх решти бітів за округлювальним бітом. Щойно його встановлено в 1, він залишається 1.
Цих трьох бітів достатньо, щоб визначити правильне округлення в усіх чотирьох режимах:Біти мантиси G R S Дія (округлення до найближчого парного)
───────────── ─ ─ ─ ──────────────────────────────────────
xxxx...xxx0 0 x x Округлити вниз (усікти) — ближче до нижнього
xxxx...xxx0 1 0 0 Серединний випадок → округлити вниз (НЗБ вже 0 = парний)
xxxx...xxx0 1 0 1 Округлити вгору — ближче до верхнього
xxxx...xxx0 1 1 x Округлити вгору — ближче до верхнього
xxxx...xxx1 0 x x Округлити вниз (усікти) — ближче до нижнього
xxxx...xxx1 1 0 0 Серединний випадок → округлити вгору (зробити НЗБ 0 = парний)
xxxx...xxx1 1 0 1 Округлити вгору — ближче до верхнього
xxxx...xxx1 1 1 x Округлити вгору — ближче до верхнього
Фіксований біт — особливо дотепне рішення. Без нього апаратне забезпечення мусило б перевіряти довільно довгий ланцюжок бітів, щоб відрізнити «рівно посередині» від «трохи більше за середину». Фіксований біт згортає цей нескінченний хвіст в один біт: якщо будь-який біт за позицією округлення дорівнює 1, фіксований біт дорівнює 1, і ми знаємо, що значення не є точно серединним. Це дозволяє виконувати коректне округлення лише з трьома додатковими бітами пам'яті, скільки б бітів не було зсунуто під час вирівнювання.Розуміти правила арифметики необхідно, але цього недостатньо. У цьому розділі ми розглянемо практичні наслідки — те, як арифметика з рухомою комою може непомітно давати хибні відповіді, а також прийоми, якими програмісти захищаються від таких похибок.Машинний епсилон (ε) — це найменше додатне число, для якого 1.0 + ε ≠ 1.0 у заданому форматі з рухомою комою. Інакше кажучи, це відстань між 1.0 та наступним більшим представним числом. Машинний епсилон кількісно характеризує фундаментальну межу точності формату:З машинного епсилона видно, що арифметика одинарної точності зберігає близько 7 значущих десяткових цифр, а подвійної — близько 16. Будь-яке обчислення, якому потрібно більше значущих цифр, ніж дає формат, повертатиме результати, спотворені округленням.Машинний епсилон можна знайти експериментально:eps = 1.0
while 1.0 + eps != 1.0: eps /= 2.0
eps *= 2.0 # останнє значення, для якого 1.0 + eps != 1.0
print(f"Машинний епсилон: {eps}") # 2.220446049250313e-16 для float64
Найнебезпечніша пастка арифметики з рухомою комою — катастрофічне скорочення, тобто масова втрата значущих цифр під час віднімання двох майже рівних чисел.Розгляньмо обчислення x − y, де x ≈ y. Нехай обидва числа зберігаються з 52 бітами точності. Якщо перші 48 бітів їхніх мантис збігаються, саме ці 48 бітів скорочуються під час віднімання, залишаючи лише останні 4 біти — а в них осіли накопичені похибки округлення всіх попередніх обчислень. Замість 52 бітів значущої точності в результаті лишається тільки 4.Ось конкретний приклад. Припустимо, треба обчислити √(x² + 1) − 1 для x = 10⁻⁸ у подвійній точності:import math
x = 1e-8
result_naive = math.sqrt(x**2 + 1) - 1
result_exact = x**2 / (math.sqrt(x**2 + 1) + 1) # алгебраїчно еквівалентне
print(f"Наївний: {result_naive}") # 0.0 (повністю хибний!)
print(f"Правильний: {result_exact}") # 5e-17 (близько до істинної відповіді)
Наївна формула обчислює √(1 + 10⁻¹⁶) ≈ 1.00000000000000005, але в подвійній точності це округлюється рівно до 1.0. Віднімання 1 дає 0.0 — усі значущі цифри втрачено. Алгебраїчно еквівалентне переформулювання уникає віднімання майже рівних чисел і дає правильну відповідь.Класичний приклад: квадратна формула. Стандартна формула для коренів ax² + bx + c = 0:x = (−b ± √(b² − 4ac)) / (2a)
Коли b² &gt;&gt; 4ac, дискримінант √(b² − 4ac) ≈ |b|, і обчислення одного з двох коренів зводиться до віднімання майже рівних чисел (−b + √(b² − 4ac) при b &gt; 0 або −b − √(b² − 4ac) при b &lt; 0). Стандартний прийом — спочатку обчислити «безпечний» корінь (той, у формулі якого стоїть додавання), а другий знайти з тотожності x₁ · x₂ = c/a, уникнувши небезпечного віднімання.У дійсній арифметиці додавання асоціативне: (a + b) + c = a + (b + c). В арифметиці з рухомою комою ця тотожність не виконується. Результат залежить від порядку операцій, бо проміжне округлення відкидає різну інформацію залежно від того, які додавання виконуються першими.a = 1e20
b = -1e20
c = 1.0 print((a + b) + c) # 1.0 (правильно)
print(a + (b + c)) # 0.0 (хибно!)
У першому групуванні a + b = 0.0 точно (великі значення скорочуються), і 0.0 + 1.0 = 1.0. У другому групуванні b + c = −1e20 (одиницю поглинає значно більше за модулем −1e20 — вона менша за машинний епсилон відносно 10²⁰), а потім a + (−1e20) = 0.0.Отже, порядок підсумовування має значення. Компілятори не мають права перевпорядковувати додавання з рухомою комою задля оптимізації (хіба що програміст явно дозволить це прапорцями на кшталт -ffast-math), бо зміна порядку може змінити результат.Поширений шаблон у науковому програмуванні — підсумовування великого масиву чисел.total = 0.0
for x in data: total += x
Якщо data містить мільйони малих значень, а total зростає, кожне додавання поглинає мале доданкове значення неточно — молодші біти x губляться під час вирівнювання експонент. Після мільйонів ітерацій накопичена похибка може сягнути значних величин.Підсумовування Кагана — класичний алгоритм, що компенсує цю похибку, утримуючи окрему змінну для накопиченої корекції:def kahan_sum(data): total = 0.0 compensation = 0.0 # накопичена похибка for x in data: y = x - compensation # компенсувати попередню похибку temp = total + y # додати до суми (може втратити молодші біти) compensation = (temp - total) - y # що було втрачено при додаванні total = temp return total
Змінна compensation вловлює саме ті біти, які губляться під час додавання total + y. На наступній ітерації ці втрачені біти повертаються до суми. У підсумку результат виходить такий, ніби весь масив підсумовано з подвоєною точністю.Порівняймо результати:import random
random.seed(42)
data = [random.random() for _ in range(1_000_000)] naive = sum(data)
kahan = kahan_sum(data)
# Використовуємо math.fsum (вбудоване точне підсумовування Python) як еталон:
import math
exact = math.fsum(data) print(f"Наївна похибка: {abs(naive - exact)}") # зазвичай ~1e-10
print(f"Похибка Кагана: {abs(kahan - exact)}") # зазвичай ~1e-16 або 0.0
Три найвідоміші катастрофи показують, як дрібні числові похибки здатні лавиноподібно перерости у руйнівні збої.Збій ракети Patriot (Дагран, 1991). Система Patriot відстежувала час, рахуючи 0.1-секундні інтервали у 24-бітовому регістрі з фіксованою комою. Число 0.1₁₀ не подається точно у двійковій системі: 0.1₁₀ = 0.000110011001100…₂ (періодичний дріб). 24-бітове наближення давало похибку близько 9.5 × 10⁻⁸ секунди на тік. За 100 годин безперервної роботи — батарея не перезавантажувалась — накопичена похибка становила:0.1с × (100 годин × 3600 с/годину) / 0.1с × 9.5×10⁻⁸ = 0.34 секунди
Ракета Scud летить зі швидкістю приблизно 1676 метрів на секунду. За 0.34 секунди вона долає близько 570 метрів — значно поза вікном супроводу Patriot. Система не змогла зіставити радарну відмітку з прогнозованою позицією і не перехопила ракету. Загинуло двадцять вісім солдатів.Вибух ракети Ariane 5 (Куру, 1996). Інерціальна навігаційна система Ariane 5 успадкувала програмне забезпечення від Ariane 4. 64-бітове значення з рухомою комою, що відповідало горизонтальній швидкості, перетворювалося на 16-бітове ціле число зі знаком. На Ariane 4 швидкість ніколи не перевищувала 32 767 (максимум для 16 біт). Потужніші прискорювачі Ariane 5 забезпечили вищу початкову швидкість. Перетворення спричинило переповнення, процесор згенерував виняток, резервна система (з ідентичним програмним забезпеченням) також відмовила, а діагностичне повідомлення бортовий комп'ютер хибно сприйняв як навігаційні дані. Ракета відхилилася від курсу і була знищена системою самознищення через 37 секунд після старту. Збитки — близько 370 мільйонів доларів.Індекс Ванкуверської фондової біржі (1982–1983). Обчислюючи свій індекс, біржа після кожного перерахунку застосовувала усікання (округлення до нуля) замість округлення до найближчого. Усікання дає систематичне зміщення вниз: щоразу, коли дробова частина відкидається, індекс втрачає крихту значення. За 22 місяці та тисячі щоденних перерахунків індекс знизився з 1000.000 до 524.811 — приблизно до половини справжньої величини. Похибку виявили, індекс скоригували до 1098.892, а усікання замінили на належне округлення.Ці катастрофи мотивують низку захисних практик:
Ніколи не порівнюйте числа з рухомою комою через ==. Замість цього використовуйте допуск: abs(a - b) &lt; epsilon, де epsilon добирається під очікуваний порядок величин.
Уникайте віднімання майже рівних значень. Переформулюйте алгебру так, щоб оминути катастрофічне скорочення.
Підсумовуйте у порядку зростання величини, коли це можливо, або застосовуйте компенсоване підсумовування (алгоритм Кагана).
Користуйтеся вищою точністю для проміжних обчислень, якщо втрата точності критична (наприклад, накопичуйте в подвійній точності, навіть якщо кінцевий результат — одинарної).
Розумійте режим округлення, у якому працюєте. Типове значення (округлення до найближчого парного) майже завжди — правильний вибір. Змінювати режим округлення варто лише у відповідальних випадках, здебільшого — в інтервальній арифметиці.
Перезавантажуйте системи тривалої роботи, перш ніж накопичені похибки стануть небезпечними — або ж проєктуйте систему так, щоб уникати часозалежного дрейфу.
Стандарт IEEE 754 визначає, що має робити арифметика з рухомою комою. Цей розділ пояснює, як процесори Intel реалізовували її апаратно — від першого зовнішнього співпроцесора 1980 року до SIMD-блоків, що домінують у сучасних обчисленнях.На початку ери мікропроцесорів арифметика з рухомою комою виконувалася програмно — одне додавання могло займати сотні тактів процесора, оскільки ЦП виконував бібліотечну підпрограму, яка зсувала, додавала й нормалізувала значення за допомогою цілочислових інструкцій. Для наукових та інженерних застосувань це було нестерпно повільно.Рішенням Intel став математичний співпроцесор — окремий чіп, що розміщувався поруч із ЦП і обробляв інструкції з рухомою комою:
Intel 8087 (1980): перший математичний співпроцесор, розроблений для роботи з процесорами 8086/8088. Він запровадив 80-бітний формат розширеної точності та стекову архітектуру регістрів, що збереглася протягом десятиліть. 8087 виконував множення з рухомою комою приблизно в 100 разів швидше за еквівалентну програмну емуляцію на 8086.
Intel 80287 (1982): оновлений для процесора 80286 та його захищеного режиму.
Intel 80387 (1987): перший співпроцесор, що повністю реалізував стандарт IEEE 754-1985, включно з усіма чотирма режимами округлення та коректною обробкою нескінченностей, NaN та денормалізованих чисел.
<br><img alt="Фотографія мікросхеми математичного співпроцесора Intel 80387" src="assets/l13-intel-80387-chip.png" target="_self">Intel 80387 (1987) — перший математичний співпроцесор x87, що повністю реалізував стандарт IEEE 754-1985, включно з усіма режимами округлення та обробкою спеціальних значень.Починаючи з Intel 80486DX (1989), блок операцій з плаваючою комою було інтегровано безпосередньо на кристал процесора — окремий чіп став непотрібним. (Бюджетний 80486SX не мав FPU; користувачі, які його потребували, могли доставити співпроцесор 80487SX.) Кожен процесор Intel, починаючи з Pentium (1993), має вбудований FPU.Чудовим наслідком десятиліть оптимізації є те, що на сучасних ЦП множення та ділення з рухомою комою можуть бути швидшими за їхні цілочислові відповідники для великих розмірів операндів. Зокрема, цілочислове ділення часто повільніше за ділення з рухомою комою, адже конвеєру FP дісталося більше транзисторного бюджету та мікроархітектурної уваги.Архітектура x87 FPU, названа на честь сімейства 8087/80287/80387, має кілька характерних особливостей:<br><img alt="Діаграма стеку регістрів x87 FPU, що показує вісім 80-бітних регістрів R0–R7, де ST(0) позначає поточну вершину стеку" src="assets/l13-x87-fpu-registers.png" target="_self">Архітектура регістрів x87 FPU: вісім 80-бітних регістрів розширеної точності, організованих як стек, з ST(0) на вершині. Ця стекова архітектура бере початок від оригінального Intel 8087 (1980).Стековий файл регістрів. x87 має 8 регістрів даних, позначених R0–R7, організованих як стек, а не як плоский файл регістрів. Регістр, що перебуває на вершині стеку, називається ST(0) (або просто ST), наступний за ним — ST(1), і так далі. Інструкції на кшталт fld поміщають значення на стек, а fst копіює значення з вершини стеку в пам'ять. Більшість арифметичних інструкцій неявно працюють із вершиною стеку: наприклад, fadd ST(1) додає ST(1) до ST(0) і зберігає результат у ST(0).Ця стекова модель була розумним проєктним рішенням у 1980 році, коли транзисторні бюджети залишалися мізерними, а компілятори — простими. Вона мінімізує кількість бітів, потрібних для кодування операндів-регістрів (достатньо вказати лише глибину стеку, а не довільний номер регістра). Проте вона створює проблеми для сучасних оптимізуючих компіляторів, які мусять ретельно керувати стеком, щоб уникати вивантажень і надлишкових завантажень.80-бітна розширена точність усередині. Уся арифметика всередині x87 виконується у 80-бітному форматі розширеної точності (1 біт знаку + 15 біт експоненти + 64 біти мантиси), незалежно від того, були вхідні дані 32-бітними чи 64-бітними. Це означає, що ланцюжок операцій накопичує менше похибок округлення, ніж якби кожен проміжний результат округлювався до 32 або 64 біт. Зворотний бік — невідтворюваність: той самий вихідний код, скомпільований з різними рівнями оптимізації, може давати різні результати, адже компілятор може вивантажувати або не вивантажувати проміжні значення в пам'ять (примусово округлюючи їх до 64 біт) залежно від тиску на регістри.Підтримка багатьох типів даних. x87 може завантажувати та зберігати значення у семи різних форматах:Формат BCD — це курйоз: його включили для підтримки фінансових застосувань та програм ери COBOL, які потребували точної десяткової арифметики.Регістри спеціального призначення. Окрім 8 регістрів даних, x87 має кілька регістрів керування та стану:
Слово керування (Control Word): задає режим округлення, контроль точності та маски виключень.
Слово стану (Status Word): повідомляє про поточний покажчик вершини стеку, коди умов (використовуються для порівнянь) та прапорці виключень.
Слово тегів (Tag Word): відстежує, які регістри даних містять допустиме значення, нуль, спеціальне значення (NaN/нескінченність/денормалізоване число) або є порожніми. Це допомагає апаратурі уникати зайвої роботи з порожніми регістрами.
Покажчик останньої інструкції / Покажчик останніх даних / Код операції: зберігають адресу та код операції останньої інструкції з рухомою комою, використовуються для налагодження та обробки виключень.
Типові інструкції. Набір інструкцій x87 містить трансцендентні функції, яким немає відповідників серед цілочислових інструкцій:fld dword [x] ; помістити float з пам'яті на стек
fld st(2) ; помістити копію ST(2) на стек
fadd st(0), st(1) ; ST(0) = ST(0) + ST(1)
fmul qword [y] ; ST(0) = ST(0) * double з пам'яті
fdiv st(1), st(0) ; ST(1) = ST(1) / ST(0)
fsqrt ; ST(0) = sqrt(ST(0))
fsin ; ST(0) = sin(ST(0))
fcos ; ST(0) = cos(ST(0))
fst dword [result] ; зберегти ST(0) у пам'ять як float
fstp qword [result] ; зберегти ST(0) як double і зняти зі стеку
Стекова архітектура x87, хоча й елегантна для 1980 року, з часом стала серйозним обмеженням: компілятори ставали потужнішими, а застосунки вимагали паралелізму. У 1999 році Intel представила принципово іншу модель роботи з рухомою комою разом із SSE:SSE (Streaming SIMD Extensions, 1999): додала вісім 128-бітних регістрів (XMM0–XMM7) та інструкції як для скалярних, так і для упакованих операцій з рухомою комою одинарної точності. На відміну від стеку x87, регістри XMM є плоскими — будь-яка інструкція може читати з будь-якого регістра або писати до нього.SSE2 (2001): розширила регістри XMM для підтримки подвійної точності. Це стало переломним моментом: SSE2 значною мірою витіснила x87 із загальної скалярної арифметики з рухомою комою. Більшість компіляторів перейшли на SSE2 як цільову платформу генерації коду за замовчуванням.AVX (Advanced Vector Extensions, 2011): розширила регістри до 256 біт (YMM0–YMM15) та додала триоперандні інструкції (dest ≠ src1), зменшивши потребу в копіюванні між регістрами.AVX-512 (2016): подальше розширення до 512 біт (ZMM0–ZMM31), подвоєння кількості регістрів до 32, а також маскові регістри для предикатного виконання. Одна інструкція AVX-512 здатна виконувати 8 операцій подвійної точності або 16 операцій одинарної точності одночасно.Попри офіційний статус застарілого для нових розробок, x87 FPU залишається актуальним з однієї причини: 80-бітна розширена точність. Блоки SSE/AVX працюють лише у 32-бітному та 64-бітному режимах. Якщо застосунку потрібно понад 16 значущих десяткових цифр, але менш ніж 34 (для чого знадобилася б 128-бітна «четвертна» точність, яка наразі не має апаратної підтримки на x86), 80-бітний формат x87 зі своїми 18,5 значущими десятковими цифрами заповнює цю прогалину.До застосунків, що досі користуються розширеною точністю x87, належать:
Long-double (long double у C/C++): у Linux x86-64 long double відповідає 80-бітному розширеному формату. У Windows і macOS він відповідає 64-бітному double (ABI цих ОС свого часу вирішили не підтримувати 80-бітний формат).
Проміжні обчислення підвищеної точності: деякі числові бібліотеки навмисно використовують 80-бітні проміжні значення, щоб зменшити похибку округлення в алгоритмах на кшталт ітераційного уточнення.
Застарілий код: десятиліття коду на Fortran і C, написаного до появи SSE2, досі орієнтовані на x87.
Для нового коду рекомендація однозначна: варто використовувати SSE/AVX (саме це компілятор робить за замовчуванням). Звертатися до x87 доречно лише за конкретної обґрунтованої потреби в розширеній точності — і з огляду на витрати, пов'язані з переносністю та відтворюваністю.
Дійсні числа неможливо зберегти точно — комп'ютер оперує скінченними наближеннями, а це завжди вносить абсолютну та відносну похибки.
Рухома кома жертвує рівномірною точністю заради величезного діапазону — експонента задає діапазон, мантиса визначає точність.
IEEE 754 — універсальний стандарт — він задає формати (одинарна, подвійна, розширена точність), спеціальні значення (±0, ±∞, NaN) і денормалізовані числа для поступової втрати значимості.
Арифметика з рухомою комою неасоціативна — (a+b)+c може відрізнятися від a+(b+c), а віднімання майже рівних чисел спричиняє катастрофічне скорочення.
Режими округлення та компенсоване підсумовування (підсумовування Кагана) — практичні інструменти для керування похибками обчислень з рухомою комою.
x87 FPU — застаріла технологія — сучасний код спирається на SSE/AVX для роботи з рухомою комою, однак розуміння стекової 80-бітної архітектури x87 дає цінний історичний контекст.
Заплановані завдання:
Дослідити бітове представлення float та double за допомогою модуля struct у Python
Експеримент 1: емпірично визначити машинний епсилон
Експеримент 2: продемонструвати втрату асоціативності — (a+b)+c ≠ a+(b+c)
Експеримент 3: катастрофічне скорочення — відняти майже рівні числа й простежити втрату точності
Експеримент 4: накопичення похибок у циклах підсумовування
Реалізувати підсумовування Кагана та порівняти його точність із наївним підсумовуванням IEEE 754-2019, IEEE Standard for Floating-Point Arithmetic — авторитетний стандарт, що визначає формати, операції та режими округлення чисел з рухомою комою
David Goldberg, What Every Computer Scientist Should Know About Floating-Point Arithmetic (1991) — класичний довідник з представлення чисел, похибок і типових пасток арифметики з рухомою комою
Oregon State University, Computer Numbers and their Precision — навчальний модуль про зберігання чисел, точність і машинний епсилон
William Kahan, IEEE 754: An Interview with the Old Man of Floating-Point — історія стандарту від його головного архітектора
Intel® 64 and IA-32 Architectures Software Developer's Manual, Volume 1, Chapter 8: x87 FPU — детальний довідник з архітектури та системи команд x87
]]></description><link>lectures/l13.-арифметика-з-рухомою-комою.html</link><guid isPermaLink="false">Lectures/L13. Арифметика з рухомою комою.md</guid><pubDate>Wed, 22 Apr 2026 04:44:10 GMT</pubDate><enclosure url="assets/l13-fp-precision-meme.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l13-fp-precision-meme.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Exam-Questions]]></title><description><![CDATA[Дисципліна: Архітектура комп'ютерних систем (6 ECTS / 4 ECTS)Офіційний перелік 45 теоретичних питань іспиту. Білет містить два питання — одне з Блоку I та одне з Блоку II. Витягуються випадково.
Регламент, критерії оцінювання, умови допуску — <a class="internal-link" data-href="Exam.md" href="exam.html" target="_self" rel="noopener nofollow">Exam.md</a>.
<br>Матеріали для підготовки (обсяг підготовки, ймовірні додаткові запитання, ядра відповідей) — <a class="internal-link" data-href="Exam-Prep/Preparation-Guide.md" href="exam-prep/preparation-guide.html" target="_self" rel="noopener nofollow">Exam-Prep/Preparation-Guide.md</a>.
21 питання. Охоплює фізичні основи обчислень, цифрову логіку, архітектуру x86 (регістри, інструкції, стек, процедури), системну архітектуру фон Неймана та взаємодію з ОС через переривання.Поясніть, чому сучасні обчислювальні системи використовують саме два стани. Охопіть завадостійкість на якісному рівні та зв'язок з булевою алгеброю.Опишіть транзистор як керований перемикач і зв'яжіть його з вентилем NAND.Опишіть ключові стрибки в технології обчислень від механічних машин до сучасної VLSI. Поясніть, що рухало кожен стрибок.Порівняйте комбінаційну та послідовну логіку з прикладами схем. Поясніть роль тактового сигналу.Порівняйте три двійкові представлення знакових цілих чисел: прямий код зі знаком, обернений (доповняльний до одиниці) код та додатковий (доповняльний до двох) код. Покажіть, чому переміг додатковий код.Опишіть, як будується примітивне АЛП з комбінаційних блоків. Покажіть, як коди операцій стають сигналами керування.Поясніть роль регістрів у проєктуванні ЦП і чому самої лише ОП недостатньо.Поясніть адресацію 8086 у реальному режимі: як 20-бітна фізична адреса формується з 16-бітного сегмента та 16-бітного зміщення.Порівняйте SRAM та DRAM на рівні комірки й поясніть, де застосовується кожна в сучасній системі.Пройдіть цикл вибірка-декодування-виконання на конкретній інструкції. Поясніть роль IP, декодера, АЛП та FLAGS.Покажіть, чому додатковий код дозволяє одній апаратурі обслуговувати знакову та беззнакову арифметику. Поясніть, як установлюється кожен із CF, ZF, SF, OF.Покажіть, як перевіряти, встановлювати, скидати та перемикати окремі біти за допомогою інструкцій x86. Порівняйте TEST і CMP.Опишіть стандартний стековий фрейм cdecl крок за кроком. Покажіть, як звертатися до аргументів і локальних змінних через BP.Поясніть механістично, чому CALL — це не просто JMP. Обговоріть дисципліну адреси повернення та що може піти не так.Простежте невелику рекурсивну процедуру на стеку. Поясніть, чому глибина рекурсії обмежена.Намалюйте схему фон Неймана. Поясніть вузьке місце та два сучасні способи його пом'якшення.Упорядкуйте рівні ієрархії пам'яті з порядком величини затримки та ємності. Поясніть, чому існує саме піраміда.Порівняйте реальний режим 8086 та захищений режим 80286+. Поясніть, чому DOS залишився в реальному режимі, тоді як сучасні ОС перейшли.Пройдіть послідовність POST → BIOS → MBR → завантажувач → ядро ОС.Порівняйте апаратні та програмні переривання. Опишіть, що INT заштовхує в стек і чому IRET відрізняється від RET.Поясніть кільця 0–3. Простежте системний виклик від користувацького коду до ядра й назад.24 питання. Охоплює технології пам'яті та введення-виведення, проєктування ISA та моделі обчислень, сучасні архітектури (GPU, FPGA, TPU, квантові), ієрархію кеша та MESI, поглиблене вивчення віртуальної пам'яті, IEEE 754 з рухомою комою, паралелізм рівня інструкцій та потоків, а також енергетику / сталість.Порівняйте чотири технології пам'яті за енергозалежністю, швидкістю, щільністю, вартістю та витривалістю. Відобразіть кожну на її системну роль.Опишіть буфер кадру в текстовому режимі. Напишіть код, що ставить символ у заданому рядку та стовпці.Порівняйте MMIO та PMIO на x86. Поясніть, що саме використовують сучасні пристрої та чому.Дайте означення машин РАМ та РАСП. Поясніть, яка з них моделює реальні комп'ютери і чому.Порівняйте CISC та RISC. Поясніть, як сучасний x86 стирає межу через внутрішні мікрооперації (µops).Дайте означення SISD, SIMD, MISD, MIMD. Наведіть реальний приклад для кожного та поясніть, куди вписуються GPU та Hyper-Threading.Порівняйте архітектури GPU та CPU. Поясніть, на яких навантаженнях виграє кожна з них.Порівняйте ASIC, FPGA та GPU для ролей прискорювача. Наведіть по одному навантаженню, що пасує кожному.Поясніть кубіт vs біт, суперпозицію та заплутаність. Укажіть, у чому квантові комп'ютери сильні та в чому слабкі.Дайте означення обох форм локальності. Покажіть приклад коду для кожної. Поясніть, як розмір рядка кешу використовує просторову локальність.Обчисліть середній час доступу до пам'яті для трирівневої ієрархії. Визначте домінантний доданок.Порівняйте три схеми відображення кеша. Укажіть апаратну вартість та поведінку конфліктних промахів.Назвіть три окремі мотивації віртуальної пам'яті. Накидайте структуру таблиці сторінок.Пройдіть трансляцію віртуальна→фізична на 32-бітній дворівневій системі сторінкування. Обробіть випадки: влучання TLB, промах TLB з обходом таблиці сторінок та промах TLB зі сторінковою помилкою.Порівняйте три алгоритми заміщення сторінок. Поясніть, чому практичним вибором є Clock.Опишіть 32-бітне число з рухомою комою IEEE 754. Поясніть зміщення, нормалізацію та спеціальні значення.Покажіть на папері, чому 0.1 + 0.2 не дорівнює точно 0.3. Потім наведіть приклад катастрофічного скорочення.Порівняйте два x86-стеки з рухомою комою. Поясніть, чому сучасні компілятори віддають перевагу SSE/AVX.Намалюйте 5-стадійний конвеєр. Поясніть, як конвеєризація покращує пропускну здатність, не покращуючи затримку однієї інструкції.Класифікуйте конфлікти конвеєра. Для кожного типу наведіть конкретний приклад на x86 і назвіть апаратне розв'язання.Порівняйте три механізми паралелізму. Поясніть, яке навантаження пасує кожному з них і як Spectre/Meltdown їх експлуатують.Сформулюйте закон Амдала. Обчисліть максимальне прискорення для програми, паралелізованої на 90%. Протиставте із законом Густафсона.Поясніть P = C × V² × f. Опишіть, чому масштабування напруги застопорилося та як індустрія відреагувала.Дайте означення dark silicon. Поясніть, як гетерогенні архітектури (big.LITTLE, Apple M-series) відповідають на нього.Кінець переліку.]]></description><link>exam-questions.html</link><guid isPermaLink="false">Exam-Questions.md</guid><pubDate>Wed, 22 Apr 2026 04:44:10 GMT</pubDate></item><item><title><![CDATA[L11-Prep]]></title><description><![CDATA[Охоплює екзаменаційні питання II.10 – II.12. Офіційне формулювання — у <a data-href="Exam-Questions" href="exam-questions.html" class="internal-link" target="_self" rel="noopener nofollow">Exam-Questions</a>; повний обсяг підготовки — у <a data-href="Preparation-Guide" href="exam-prep/preparation-guide.html" class="internal-link" target="_self" rel="noopener nofollow">Preparation-Guide</a>.Кожен запис нижче — стислий кістяк відповіді: достатньо, щоб зорієнтуватися, але не заміна лекції. Переходьте за посиланнями «Докладніше», щоб заглибитися. Рядки «Зверніть увагу на» позначають пастки, на які націлені уточнювальні запитання екзаменатора.Окремі означення. Часова локальність — якщо до адреси щойно зверталися, то, імовірно, звернуться до неї знову найближчим часом. Просторова локальність — якщо до адреси щойно зверталися, то, імовірно, звернуться й до сусідніх адрес найближчим часом. Обидві — це емерджентні властивості того, як структуровані програми, а не апаратні гарантії; кеші їх використовують, але не створюють.Приклади з коду. Часова: лічильник циклу i, який читають і пишуть на кожній ітерації; акумулятор редукції sum += a[i]; адреса повернення допоміжної функції при виклику всередині гарячого циклу; вибірки інструкцій усередині тіла циклу (10 інструкцій × 10 000 ітерацій = 100 000 вибірок із 10 адрес). Просторова: послідовний обхід масиву for (i=0; i&lt;N; i++) sum += a[i]; лінійна вибірка інструкцій; доступ до полів структури (p-&gt;x, потім p-&gt;y); локальні змінні стека, розкладені безперервно.Розмір рядка кешу як механізм просторової локальності. x86 передає пам'ять 64-байтними рядками кешу, а не окремими байтами. Завантаження 4-байтного int за адресою N затягує весь 64-байтний блок, що її містить, у L1. Для проходу з кроком 1 по int-ах перший доступ — це обов'язковий промах, а наступні 15 — влучання: коефіцієнт влучань 93,75% лише завдяки просторовій локальності. Більші рядки охоплювали б більше локальності, але марнували б пропускну здатність, коли задіяно мало байтів; 64 Б — налагоджений компроміс x86 з часів Pentium 4.Порядок по рядках vs порядок по стовпцях, кількісно. C зберігає matrix[i][j] у порядку по рядках: [i][j+1] — за 4 байти, [i+1][j] — за N × 4 байти. Внутрішній цикл по j — це крок 1 → 1 промах на 16 елементів → коефіцієнт промахів ≈ 6,25%. Поміняйте цикли місцями (внутрішній по i) і отримаєте крок N: при N = 1024 кожен крок перестрибує 4 096 Б = 64 рядки кешу; поки цикл повернеться до рядка 0, той рядок давно витіснено → ≈100% коефіцієнт промахів. Результат: «той самий» код виконується у 4–8 разів повільніше у поганому порядку. Fortran-івські BLAS/LAPACK мають порядок по стовпцях, тож викликач із C мовчки отримує патологічний крок.Блокування (тайлінг циклів). Переписати вкладені цикли над великими масивами так, щоб внутрішні цикли працювали над підплиткою BLOCK × BLOCK, що вміщається в L1. Наївний matmul проходить B по стовпцях; тайлінгова версія торкається одного блоку B за раз і тримає його в L1 упродовж усіх залежних множень. З BLOCK, підібраним так, щоб плитки A, B, C поміщалися в L1 спільно, тайлінговий matmul виконується у 10–20 разів швидше при великих N. Блокування насамперед атакує промахи ємності, зменшуючи робочий набір.Зверніть увагу на: забруднення кешу — апаратний блок попередньої вибірки, що надчитує, шкодить потоковим кодам, витісняючи гарячі дані (взаємодія з блоком попередньої вибірки була введена в L06; L11 зосереджена на політиці влучання/промаху); чому int16_t замість int64_t учетверо збільшує ефективний L1; блок попередньої вибірки відмовляється від некратних кроків за порогом; AoS vs SoA (обробка лише x-координат → SoA нічого не марнує, AoS марнує 3/4 кожного рядка).<br>Докладніше: <a data-tooltip-position="top" aria-label="L11. Ієрархія пам'яті та продуктивність > 3.1 Часова локальність" data-href="L11. Ієрархія пам'яті та продуктивність#3.1 Часова локальність" href="lectures/l11.-ієрархія-пам'яті-та-продуктивність.html#3.1 Часова локальність" class="internal-link" target="_self" rel="noopener nofollow">§3.1 Часова</a>, <a data-tooltip-position="top" aria-label="L11. Ієрархія пам'яті та продуктивність > 3.2 Просторова локальність" data-href="L11. Ієрархія пам'яті та продуктивність#3.2 Просторова локальність" href="lectures/l11.-ієрархія-пам'яті-та-продуктивність.html#3.2 Просторова локальність" class="internal-link" target="_self" rel="noopener nofollow">§3.2 Просторова</a>, <a data-tooltip-position="top" aria-label="L11. Ієрархія пам'яті та продуктивність > 3.3 Локальність на практиці: патерни доступу до матриць" data-href="L11. Ієрархія пам'яті та продуктивність#3.3 Локальність на практиці: патерни доступу до матриць" href="lectures/l11.-ієрархія-пам'яті-та-продуктивність.html#3.3 Локальність на практиці: патерни доступу до матриць" class="internal-link" target="_self" rel="noopener nofollow">§3.3 Доступ до матриць</a>, <a data-tooltip-position="top" aria-label="L11. Ієрархія пам'яті та продуктивність > 3.4 Кеш-дружній код" data-href="L11. Ієрархія пам'яті та продуктивність#3.4 Кеш-дружній код" href="lectures/l11.-ієрархія-пам'яті-та-продуктивність.html#3.4 Кеш-дружній код" class="internal-link" target="_self" rel="noopener nofollow">§3.4 Кеш-дружній код</a>.Рекурсивна формула. Середній час доступу до пам'яті — це зважена суміш швидкого шляху (влучання) та повільного шляху (промах):AMAT = час_влучення + коеф_промахів × штраф_промаху
Для багаторівневої ієрархії штраф промаху рівня N — це AMAT рівня N+1; формула вкладається:AMAT = hit_L1 + miss_L1 × (hit_L2 + miss_L2 × (hit_L3 + miss_L3 × t_DRAM))
Коефіцієнти промахів тут локальні (промахи на рівні N / звернення до рівня N).Розібраний трирівневий приклад. Візьміть такі параметри:Обчислюємо зсередини назовні:AMAT L3 = 30 + 0,40 × 200 = 30 + 80 = 110 циклів
AMAT L2 = 10 + 0,20 × 110 = 10 + 22 = 32 цикли
AMAT_загальний = 1 + 0,05 × 32 = 1 + 1,6 = 2,6 цикла
200-циклову DRAM зведено до ефективної 2,6-циклової пам'яті — поліпшення у 77 разів. Глобальні ймовірності шляхів: 95% обслуговується L1, 4% — L2, 0,6% — L3, 0,4% доходить до DRAM. Ці крихітні 0,4% усе одно дають у AMAT ≈ 0,96 цикла — майже стільки ж, скільки сам час влучення в L1.Домінантний доданок, з перших принципів. Не тому, що 200 — найбільша сира затримка, а тому, що DRAM повільна та ще й досягається достатньо часто, щоб це мало значення. Кожен шлях дає внесок глобальна_ймовірність × затримка_шляху. Обов'язкові та ємнісні промахи гарантують ненульове дно глобального коефіцієнта промахів (пропустіть потоком набір даних у 10 ГБ — і L3 обвалиться), тож шлях до DRAM у 200 циклів переважує швидші шляхи попри свою малу ймовірність. Чесна відповідь називає добуток, а не сирий штраф.Порівняння чутливості (обчислено з наведеного прикладу — лекція дає формулу, а ці числа виходять механічно). Удвічі зменшити коефіцієнт промахів L1 (5% → 2,5%): AMAT = 1 + 0,025 × 32 = 1,8 цикла. Удвічі зменшити затримку DRAM (200 → 100 циклів): L3 = 30 + 0,40 × 100 = 70; L2 = 10 + 0,20 × 70 = 24; AMAT = 1 + 0,05 × 24 = 2,2 цикла. Зменшення коефіцієнта промахів L1 виграє — бо miss_L1 множить увесь хвіст. Інженерний висновок: кеш-обізнане програмне забезпечення дає більше, ніж очікування швидшої DRAM.Стислий робочий варіант. З hit_L1 = 4 цикла, miss_L1 = 2%, hit_L2 = 12 циклів, miss_L2 = 10%, t_DRAM = 200 циклів: AMAT = 4 + 0,02 × (12 + 0,10 × 200) = 4 + 0,02 × 32 = 4,64 цикла.Зверніть увагу на: локальний vs глобальний коефіцієнт промахів (20% локального промаху L2 лякає людей; його глобальний коефіцієнт промахів — 1%); AMAT — це середнє; хвостова затримка важливіша для навантажень, чутливих до затримки (DB-запит, заблокований на одному промаху, малим середнім не заспокоїш); попереднє завантаження приховує штраф промаху, а не зменшує коефіцієнт промахів — моделюйте це, віднімаючи перекриті цикли від штраф_промаху; закон Амдала в застосунку до пам'яті — навіть при 5% коефіцієнта промахів повільний шлях домінує.<br>Докладніше: <a data-tooltip-position="top" aria-label="L11. Ієрархія пам'яті та продуктивність > 8.1 Базова формула AMAT" data-href="L11. Ієрархія пам'яті та продуктивність#8.1 Базова формула AMAT" href="lectures/l11.-ієрархія-пам'яті-та-продуктивність.html#8.1 Базова формула AMAT" class="internal-link" target="_self" rel="noopener nofollow">§8.1 Базова AMAT</a>, <a data-tooltip-position="top" aria-label="L11. Ієрархія пам'яті та продуктивність > 8.3 Багаторівневий AMAT" data-href="L11. Ієрархія пам'яті та продуктивність#8.3 Багаторівневий AMAT" href="lectures/l11.-ієрархія-пам'яті-та-продуктивність.html#8.3 Багаторівневий AMAT" class="internal-link" target="_self" rel="noopener nofollow">§8.3 Багаторівневий AMAT</a>, <a data-tooltip-position="top" aria-label="L11. Ієрархія пам'яті та продуктивність > 8.5 Стратегії оптимізації" data-href="L11. Ієрархія пам'яті та продуктивність#8.5 Стратегії оптимізації" href="lectures/l11.-ієрархія-пам'яті-та-продуктивність.html#8.5 Стратегії оптимізації" class="internal-link" target="_self" rel="noopener nofollow">§8.5 Компроміси оптимізації</a>.Питання розміщення. Коли прибуває блок, де в кеші він може сісти? Асоціативність — кількість рядків, які блок легально може займати, — обирає точку на спектрі між «одним фіксованим слотом» і «будь-де».Колізія в кеші з прямим відображенням, розібрана. Візьміть 16 КБ кеш з прямим відображенням із 64-байтними рядками → 256 множин, 8 бітів індексу, 6 бітів зміщення. Адреса A = 0x0000_1000 має індекс (A &gt;&gt; 6) &amp; 0xFF = 0x40. Адреса A + 16 КБ = 0x0000_5000 хешується в той самий індекс 0x40. Чергування A, A+16384, A, A+16384, … дає 100% промахів: кожен доступ витісняє щойно завантажений рядок. Загалом будь-які дві адреси, розділені кратним cache_size (з прямим відображенням) або cache_size / associativity (N-канальний), відображаються в ту саму множину.Чому реальний L1 сходиться до 8-канального. L1 сходиться до 8-канального, бо час влучення зростає з асоціативністю, а L1 лежить на критичному шляху — його 1–4-циклове влучення живиться прямо в AMAT. Промахи конфлікту різко спадають від 1-канального до 4-канального, м'якше — до 8-канального, а далі вирівнюються; за цією точкою апаратна вартість і затримка продовжують рости при малому виграші. L3 витримує вищу асоціативність (12–16-канальний), бо його ≈30-циклове влучення вже поглинає додаткову вартість пошуку, яка була б непомірною в L1; L3 ще й спільний між ядрами (більший тиск конфліктів у множинах), а його штраф промаху (≈200 циклів до DRAM) переважує будь-яку додаткову вартість пошуку.Три C промахів, класифіковані.Класифікуйте заданий промах за контрфактичним: чи уникнув би його нескінченний кеш? Якщо ні → обов'язковий. Чи уникнув би його повністю асоціативний кеш того самого розміру? Якщо так → конфлікту. Інакше → ємності.MESI побіжно. Чотири стани на кешований рядок: M (modified, єдина копія, забруднена), E (exclusive, єдина копія, чиста), S (shared, чиста, інші можуть мати її), I (invalid). При віддаленому записі локальний рядок S прослуховує BusUpgr і переходить S → I; писар іде S → M. Записи в рядки E — тихі E → M — саме тому MESI додає E понад простіший MSI.Зверніть увагу на: пастка степенів двійки — int matrix[1024][1024] з кроком по стовпцях відображає кожне звернення в ту саму жменьку множин; вирівнювання до [1024][1024+16] ламає це шикування; бінарний пошук у int a[2^20] виконується ≈360 нс/запит vs ≈300 нс на a[2^20 + 123] на тій самій машині, бо непарний розмір розподіляється по множинах; «множина» в 1-канальному — це один рядок, у повністю асоціативному — містить усі рядки.<br>Докладніше: <a data-tooltip-position="top" aria-label="L11. Ієрархія пам'яті та продуктивність > 5.2 Кеш з прямим відображенням" data-href="L11. Ієрархія пам'яті та продуктивність#5.2 Кеш з прямим відображенням" href="lectures/l11.-ієрархія-пам'яті-та-продуктивність.html#5.2 Кеш з прямим відображенням" class="internal-link" target="_self" rel="noopener nofollow">§5.2 З прямим відображенням</a>, <a data-tooltip-position="top" aria-label="L11. Ієрархія пам'яті та продуктивність > 5.3 Повністю асоціативний кеш" data-href="L11. Ієрархія пам'яті та продуктивність#5.3 Повністю асоціативний кеш" href="lectures/l11.-ієрархія-пам'яті-та-продуктивність.html#5.3 Повністю асоціативний кеш" class="internal-link" target="_self" rel="noopener nofollow">§5.3 Повністю асоціативний</a>, <a data-tooltip-position="top" aria-label="L11. Ієрархія пам'яті та продуктивність > 5.4 Множинно-асоціативний кеш (золота середина)" data-href="L11. Ієрархія пам'яті та продуктивність#5.4 Множинно-асоціативний кеш (золота середина)" href="lectures/l11.-ієрархія-пам'яті-та-продуктивність.html#5.4 Множинно-асоціативний кеш (золота середина)" class="internal-link" target="_self" rel="noopener nofollow">§5.4 Множинно-асоціативний</a>, <a data-tooltip-position="top" aria-label="L11. Ієрархія пам'яті та продуктивність > 5.5 Компроміси асоціативності" data-href="L11. Ієрархія пам'яті та продуктивність#5.5 Компроміси асоціативності" href="lectures/l11.-ієрархія-пам'яті-та-продуктивність.html#5.5 Компроміси асоціативності" class="internal-link" target="_self" rel="noopener nofollow">§5.5 Компроміси</a>, <a data-tooltip-position="top" aria-label="L11. Ієрархія пам'яті та продуктивність > 4.4 Три C промахів кешу" data-href="L11. Ієрархія пам'яті та продуктивність#4.4 Три C промахів кешу" href="lectures/l11.-ієрархія-пам'яті-та-продуктивність.html#4.4 Три C промахів кешу" class="internal-link" target="_self" rel="noopener nofollow">§4.4 Три C</a>, <a data-tooltip-position="top" aria-label="L11. Ієрархія пам'яті та продуктивність > 9.3 Протокол MESI: чотири стани" data-href="L11. Ієрархія пам'яті та продуктивність#9.3 Протокол MESI: чотири стани" href="lectures/l11.-ієрархія-пам'яті-та-продуктивність.html#9.3 Протокол MESI: чотири стани" class="internal-link" target="_self" rel="noopener nofollow">§9.3 Стани MESI</a>.<br>Блок II · L11 · 3 питання · Частина <a data-href="Exam-Questions" href="exam-questions.html" class="internal-link" target="_self" rel="noopener nofollow">Exam-Questions</a>]]></description><link>exam-prep/l11-prep.html</link><guid isPermaLink="false">Exam-Prep/L11-Prep.md</guid><pubDate>Wed, 22 Apr 2026 04:44:10 GMT</pubDate></item><item><title><![CDATA[L12-Prep]]></title><description><![CDATA[Охоплює екзаменаційні питання II.13 – II.15. Офіційне формулювання — у <a data-href="Exam-Questions" href="exam-questions.html" class="internal-link" target="_self" rel="noopener nofollow">Exam-Questions</a>; повний обсяг підготовки — у <a data-href="Preparation-Guide" href="exam-prep/preparation-guide.html" class="internal-link" target="_self" rel="noopener nofollow">Preparation-Guide</a>.Кожен запис нижче — це стислий кістяк відповіді: достатньо, щоб зорієнтуватись, але не заміна лекції. Переходьте за посиланнями «Докладніше», щоб поглибитись. Рядки «Зверніть увагу на» позначають пастки, на які цілять додаткові питання екзаменатора.Три окремі мотивації — кожна відповідає на питання «що ламається без цього?»
Ізоляція / захист. Кожен процес працює у власному адресному просторі; його PTE просто не кодують трансляцій у фрейми іншого процесу. Без цього помилка у вашому браузері пошкоджує купу текстового редактора, а скомпрометований застосунок читає ваші SSH-ключі прямо з ОП. Побітові U/S та R/W на кожну сторінку дозволяють ядру співіснувати в кожному адресному просторі, але лишатися недоступним з кільця 3.
Абстракція (плаский віртуальний адресний простір на процес). Кожен процес бачить однаковий охайний макет — код знизу, купа росте вгору, стек росте вниз, ядро вгорі — незалежно від того, де фізично розташовані його сторінки. Без цього лінкеру довелося б під час завантаження казати: «вашій програмі дісталися фрейми 7302, 41, 998, 12055 — пропатчте всі вказівники відповідно»; дві програми, що обидві хочуть завантажитися за 0x400000, не можуть співіснувати.
Надмірне виділення / сторінкування за запитом. ОП розглядається як кеш для диска. Тільки сторінки, до яких справді звертались, займають фрейми; процес, що викликає malloc на 2 ГБ, але торкається 200 МБ, коштує 200 МБ ОП. Машина на 16 ГБ запускає процеси, сукупний віртуальний слід яких — сотні ГБ. Без надмірного виділення кожен mmap чи malloc жадібно закріплював би фізичні фрейми, а підкачка (порятунок холодних сторінок на диск) була б неможлива.
Схема 32-бітної дворівневої таблиці сторінок (2^10 записів каталогу, кожен вказує на одну внутрішню таблицю з 2^10 записів; внутрішні таблиці виділяються за потребою лише для зачеплених регіонів): CR3 │ ▼ ┌──────────────┐ 1024 × 4 Б = 4 КБ (одна сторінка) │ Каталог стор.│ PD[outer] → адреса PT АБО not-present └──────────────┘ │ │ ▼ ▼ ┌────┐ ┌────┐ 1024 × 4 Б = 4 КБ кожна, виділені лише для ужитих регіонів │ PT │ │ PT │ PT[inner] → PFN АБО not-present └────┘ └────┘ │ ▼ Фрейм в ОП → фізична адреса = frame | offset(12 бітів)
Четверте практичне застосування понад три класичні мотивації: копіювання при записі після fork(). Батько і нащадок спільно використовують кожен фрейм, позначений «лише для читання» в обох таблицях сторінок. Перший запис спричиняє помилку; обробник виділяє приватний фрейм, копіює 4 КБ, перемикає R/W на 1. Якщо нащадок одразу викликає exec() — типовий випадок — жодна сторінка не копіюється. Споріднений прийом: заповнення нулями за запитом відображає кожну нову сторінку купи на єдину спільну сторінку «лише для читання» з нулями; перший запис спричиняє помилку й отримує приватний фрейм.Зв'язок з безпекою. Ізоляцію підсилює ASLR (рандомізація розташування адресного простору) — ОС рандомізує базу стеку, купи, PIE-тексту та кожної розділюваної бібліотеки при кожному запуску (~28–30 бітів ентропії на 64-бітному Linux). Жорстко закодовані адреси ROP-ґаджетів падають при наступному виконанні. ASLR дешевий саме тому, що віртуальна пам'ять уже надає шар опосередкування — змінюється лише відображення, а не програма.Зверніть увагу на: різницю між ізоляцією (різна трансляція) та захистом (біти дозволу при однаковій трансляції); чому ASLR взагалі має сенс лише за умови віртуальної пам'яті; COW як механізм, побудований поверх віртуальної пам'яті, а не окрема функція.<br>Докладніше: <a data-tooltip-position="top" aria-label="L12. Віртуальна пам'ять поглиблено > Три стовпи" data-href="L12. Віртуальна пам'ять поглиблено#Три стовпи" href="lectures/l12.-віртуальна-пам'ять-поглиблено.html#Три стовпи" class="internal-link" target="_self" rel="noopener nofollow">§1 Три стовпи</a>, <a data-tooltip-position="top" aria-label="L12. Віртуальна пам'ять поглиблено > 8.5 Копіювання при записі (COW)" data-href="L12. Віртуальна пам'ять поглиблено#8.5 Копіювання при записі (COW)" href="lectures/l12.-віртуальна-пам'ять-поглиблено.html#8.5 Копіювання при записі (COW)" class="internal-link" target="_self" rel="noopener nofollow">§8.5 Копіювання при записі</a>, <a data-tooltip-position="top" aria-label="L12. Віртуальна пам'ять поглиблено > 10.3 ASLR: рандомізація розташування адресного простору" data-href="L12. Віртуальна пам'ять поглиблено#10.3 ASLR: рандомізація розташування адресного простору" href="lectures/l12.-віртуальна-пам'ять-поглиблено.html#10.3 ASLR: рандомізація розташування адресного простору" class="internal-link" target="_self" rel="noopener nofollow">§10.3 ASLR</a>.Розбиття віртуальної адреси для 32-бітного режиму, сторінок 4 КБ, дворівневого сторінкування:2^10 × 2^10 × 2^12 = 2^32 = охоплює 4 ГБ.Три результати при кожному зверненні:
Влучання TLB (&gt;99% звернень). MMU шукає VPN у повністю асоціативному TLB, за ~1 такт отримує PFN + біти дозволів, будує PFN | offset, перевіряє R/W/U/S/NX. Жодного трафіку пам'яті для трансляції.
Промах TLB → обхід таблиці сторінок. MMU читає PD[outer] з фізичної адреси в CR3 (звернення 1 до пам'яті). Якщо PTE.P = 0 → сторінкова помилка. Інакше читає PT[inner] з фрейму, на який вказує запис PD (звернення 2 до пам'яті). Якщо P = 0 → сторінкова помилка. Інакше виокремлює PFN, встановлює (VPN → PFN) у TLB, повторює. Вартість: 2 звернення до пам'яті для 32-бітного режиму, 4 для чотирирівневого x86-64 (PML4→PDPT→PD→PT), 5 для LA57. При ~100 нс на звернення це 200–500 нс перед читанням даних.
Промах TLB → сторінкова помилка. Обхід знаходить P = 0. ЦП здіймає виняток #14, зберігає аварійну віртуальну адресу у CR2, стрибає до обробника ядра. Вартість обходу таблиці сторінок: 4–28 нс у найкращому випадку (PTE верхніх рівнів кешовані в L1/L2), до ~400 нс, коли всі рівні промахуються в DRAM — тобто десятки-сотні тактів на сучасному ЦП. Сторінкова помилка з підкачкою з диска стрибає в мілісекунди — розрив на 6–7 порядків понад обходом.
Вартість плоскої 32-бітної таблиці сторінок (гіпотетична): 2^20 записів × 4 Б = 4 МБ на процес, завжди виділено, причому &gt;99% записів позначені not-present для типового процесу. Помножте на сотні процесів → десятки ГБ змарнованої ОП. Дворівневий макет виділяє лише заповнені підтаблиці: процес, що використовує код + купу + стек (кожен діапазон ≤ 4 МБ), потребує 1 PD + 3 PT = 16 КБ — економія у 256×. x86-64 іде далі: 4 рівні, бо плоска 48-бітна таблиця становила б 2^36 × 8 Б = 512 ГБ на процес. Невикористані гілки дерева просто відсутні.Чому чотири рівні на x86-64, а не два. Кожна таблиця x86-64 вміщується в одну 4-КБ сторінку: 4096 / 8-байтний PTE = 512 записів = 9 бітів індексу. Віртуальна адреса без зміщення — 48 − 12 = 36 бітів, що вимагає 36 / 9 = 4 рівні. Два рівні потребували б 18-бітних індексів → таблиці 2^18 × 8 Б = 2 МБ, порушуючи інваріант «одна таблиця = одна сторінка» і марнуючи простір на розріджені процеси.Способи розв'язання сторінкової помилки понад SIGSEGV: (а) сторінкування з диска за запитом — перше звернення до сегмента коду/даних, читання через DMA, потім P ← 1; (б) заповнення нулями — нова сторінка купи/BSS, виділити фрейм, обнулити, відобразити з правом запису; (в) розрив COW — спільну сторінку COW «лише для читання» записано, виділити приватний фрейм, memcpy, перемкнути R/W; (г) підвантаження зі swap — раніше витіснену брудну сторінку зчитано назад зі свопу; (ґ) мінорна помилка — сторінка вже резидентна (наприклад, її відобразив інший процес), лише вставити PTE й встановити P = 1; (д) помилка захисту — порушення дозволів, невідповідність U/S чи R/W чи NX → SIGSEGV.Зверніть увагу на: біти зміщення завжди проходять без змін; CR3 містить фізичну адресу (інакше виник би цикл «курка і яйце»); вартість промаху TLB (~10–100 тактів) і сторінкової помилки з диска (~мс) розділяють ~10⁷; PTE.A (accessed) та PTE.D (dirty) апаратно встановлюються й програмно скидаються, живлячи політику заміщення (Q II.15).<br>Докладніше: <a data-tooltip-position="top" aria-label="L12. Віртуальна пам'ять поглиблено > 6.3 Дворівневі таблиці сторінок (32-бітний приклад)" data-href="L12. Віртуальна пам'ять поглиблено#6.3 Дворівневі таблиці сторінок (32-бітний приклад)" href="lectures/l12.-віртуальна-пам'ять-поглиблено.html#6.3 Дворівневі таблиці сторінок (32-бітний приклад)" class="internal-link" target="_self" rel="noopener nofollow">§6.3 Дворівневі таблиці</a>, <a data-tooltip-position="top" aria-label="L12. Віртуальна пам'ять поглиблено > 6.4 Чотирирівневі таблиці сторінок x86-64" data-href="L12. Віртуальна пам'ять поглиблено#6.4 Чотирирівневі таблиці сторінок x86-64" href="lectures/l12.-віртуальна-пам'ять-поглиблено.html#6.4 Чотирирівневі таблиці сторінок x86-64" class="internal-link" target="_self" rel="noopener nofollow">§6.4 Чотирирівневий обхід</a>, <a data-tooltip-position="top" aria-label="L12. Віртуальна пам'ять поглиблено > 7.3 Влучення та промах TLB: потік обробки" data-href="L12. Віртуальна пам'ять поглиблено#7.3 Влучення та промах TLB: потік обробки" href="lectures/l12.-віртуальна-пам'ять-поглиблено.html#7.3 Влучення та промах TLB: потік обробки" class="internal-link" target="_self" rel="noopener nofollow">§7.3 Потік влучання/промаху TLB</a>, <a data-tooltip-position="top" aria-label="L12. Віртуальна пам'ять поглиблено > 8.2 Типи сторінкових помилок" data-href="L12. Віртуальна пам'ять поглиблено#8.2 Типи сторінкових помилок" href="lectures/l12.-віртуальна-пам'ять-поглиблено.html#8.2 Типи сторінкових помилок" class="internal-link" target="_self" rel="noopener nofollow">§8.2 Типи помилок</a>.Усі три відповідають на одне питання: список вільних фреймів порожній, треба завантажити нову сторінку — яку резидентну сторінку витіснити? Поганий вибір жертви негайно спричиняє ще одну помилку; при ~8 мс на помилку HDD навіть одна помилка на 1000 звернень роздуває ефективний час доступу в 41×, тож вибір має значення.Чому справжній апаратний LRU непрактичний. LRU вимагав би, щоб MMU оновлювала мітку часу на кожну сторінку або перевпорядковувала двозв'язний список при кожному зверненні до пам'яті — мільярди разів на секунду на сучасному ЦП. Сучасні процесори такого гачка не надають. Програмно підтримуваний LRU вимагав би сторінкової помилки на кожному зверненні (щоб оновити структуру) — повільніше за диск. Тому практичні ОС використовують апроксимацію.Що апроксимує Clock. Апаратура встановлює PTE.A = 1 при кожному зверненні до сторінки (безкоштовно, нічого не коштує). Стрілка годинника обмітає список резидентних сторінок; коли потрібна жертва, вона оглядає сторінку під стрілкою. Якщо A = 1 — сторінку торкалися з останнього обмітання → скинути A, просунутись, дати «другий шанс». Якщо A = 0 — витіснити. Сторінки, яких торкнулися протягом одного оберту, виживають; ті, що простоювали повний оберт, гинуть. Вікно нещодавності — один оберт: грубіше за справжній LRU, але відстежує ту саму ідею «нещодавно використано» з нульовими витратами на звернення. Дворукі варіанти використовують стрілку очищення попереду стрілки витіснення; проміжок налаштовує вікно під навантаженням на пам'ять. Покращений другий шанс також дивиться на PTE.D — віддає перевагу витісненню (A=0, D=0) (без зворотного запису) перед (A=0, D=1) (потрібен зворотний запис) перед (A=1, *).Аномалія Беладі. FIFO з 3 фреймами на послідовності 1,2,3,4,1,2,5,1,2,3,4,5 дає 9 помилок; з 4 фреймами — 10. Більше ресурсів — більше промахів. Трапляється, бо FIFO не є стековим алгоритмом: множина сторінок у N фреймах не є підмножиною множини у N+1 фреймах. LRU, OPT і Clock — стекові алгоритми й доведено імунні. На прикладі лекції з 20 звертань (3 фрейми): FIFO = 15 помилок, LRU = 12, OPT = 9.OPT як теоретичний еталон. Доведено Беладі (1966), що дає мінімально можливу кількість помилок для будь-якого рядка звернень. Нереалізовний, бо вимагає знання майбутнього потоку звернень. Його цінність: виміряти розрив між будь-яким практичним алгоритмом і OPT на трасі, щоб оцінити простір для покращення. Якщо LRU уже в межах 10% від OPT, вигадувати хитріший алгоритм майже не має сенсу.Зв'язок із робочим набором. Сучасні ядра використовують варіанти Clock, налаштовані під свої навантаження — точна структура списку відрізняється між ОС і виходить поза обсяг лекції. Clock відстежує робочий набір кожного процесу — множину сторінок, до яких активно зверталися в недавньому часовому вікні — й витісняє лише поза ним. Коли робочий набір перевищує ОП, система пробуксовує (проводить більше часу в помилках, ніж у виконанні), і єдиний вихід — зменшити багатопрограмність або додати ОП.Зверніть увагу на: «LRU використовується в Linux» — неправильно; Linux використовує Clock-засновану апроксимацію LRU. Аномалія Беладі у FIFO vs імунність стекових алгоритмів. Роль OPT як еталона, не реалізації. Чому 1-бітного поля A достатньо для Clock, тоді як справжній LRU потребував би повної мітки часу.<br>Докладніше: <a data-tooltip-position="top" aria-label="L12. Віртуальна пам'ять поглиблено > 9.2 FIFO (Першим прийшов — першим вийшов)" data-href="L12. Віртуальна пам'ять поглиблено#9.2 FIFO (Першим прийшов — першим вийшов)" href="lectures/l12.-віртуальна-пам'ять-поглиблено.html#9.2 FIFO (Першим прийшов — першим вийшов)" class="internal-link" target="_self" rel="noopener nofollow">§9.2 FIFO + Беладі</a>, <a data-tooltip-position="top" aria-label="L12. Віртуальна пам'ять поглиблено > 9.3 Оптимальний алгоритм (OPT/MIN)" data-href="L12. Віртуальна пам'ять поглиблено#9.3 Оптимальний алгоритм (OPT/MIN)" href="lectures/l12.-віртуальна-пам'ять-поглиблено.html#9.3 Оптимальний алгоритм (OPT/MIN)" class="internal-link" target="_self" rel="noopener nofollow">§9.3 OPT</a>, <a data-tooltip-position="top" aria-label="L12. Віртуальна пам'ять поглиблено > 9.4 LRU (Найдавніше використаний)" data-href="L12. Віртуальна пам'ять поглиблено#9.4 LRU (Найдавніше використаний)" href="lectures/l12.-віртуальна-пам'ять-поглиблено.html#9.4 LRU (Найдавніше використаний)" class="internal-link" target="_self" rel="noopener nofollow">§9.4 LRU</a>, <a data-tooltip-position="top" aria-label="L12. Віртуальна пам'ять поглиблено > 9.5 Алгоритм годинника (друга можливість)" data-href="L12. Віртуальна пам'ять поглиблено#9.5 Алгоритм годинника (друга можливість)" href="lectures/l12.-віртуальна-пам'ять-поглиблено.html#9.5 Алгоритм годинника (друга можливість)" class="internal-link" target="_self" rel="noopener nofollow">§9.5 Clock</a>, <a data-tooltip-position="top" aria-label="L12. Віртуальна пам'ять поглиблено > 9.6 Порівняння алгоритмів" data-href="L12. Віртуальна пам'ять поглиблено#9.6 Порівняння алгоритмів" href="lectures/l12.-віртуальна-пам'ять-поглиблено.html#9.6 Порівняння алгоритмів" class="internal-link" target="_self" rel="noopener nofollow">§9.6 Порівняння</a>.<br>Блок II · L12 · 3 питання · Частина <a data-href="Exam-Questions" href="exam-questions.html" class="internal-link" target="_self" rel="noopener nofollow">Exam-Questions</a>]]></description><link>exam-prep/l12-prep.html</link><guid isPermaLink="false">Exam-Prep/L12-Prep.md</guid><pubDate>Wed, 22 Apr 2026 04:44:10 GMT</pubDate></item><item><title><![CDATA[L13-Prep]]></title><description><![CDATA[Охоплює екзаменаційні питання II.16 – II.18. Офіційне формулювання — у <a data-href="Exam-Questions" href="exam-questions.html" class="internal-link" target="_self" rel="noopener nofollow">Exam-Questions</a>; повний обсяг підготовки — у <a data-href="Preparation-Guide" href="exam-prep/preparation-guide.html" class="internal-link" target="_self" rel="noopener nofollow">Preparation-Guide</a>.Кожен запис нижче — це стислий кістяк відповіді: достатньо, щоб зорієнтуватись, але не заміна лекції. Переходьте за посиланнями «Докладніше», щоб поглибитись. Рядки «Зверніть увагу на» позначають пастки, на які цілять додаткові питання екзаменатора.Розкладка бітів (одинарна точність, 32 біти). Один біт знака S, вісім бітів зміщеної експоненти E, двадцять три біти мантиси M'. Значення нормалізованого числа з рухомою комою: (-1)^S × 1.M' × 2^(E − 127) — провідна 1. є прихованим бітом і не зберігається. Зміщення 127 відображає збережене E ∈ [1, 254] на справжню експоненту [-126, +127], що дозволяє лексикографічному порівнянню бітів (без урахування знака) правильно впорядковувати числа з рухомою комою.Чотири рядки спеціальних значень (розрізняються екстремальним E).Діапазон на папері. Найбільше нормалізоване: E = 254, M' — самі одиниці → (2 − 2⁻²³) × 2¹²⁷ ≈ 3,40 × 10³⁸. Найменше нормалізоване: E = 1, M' = 0 → 1,0 × 2⁻¹²⁶ ≈ 1,18 × 10⁻³⁸. Субнормальні заповнюють проміжок аж до 2⁻¹⁴⁹ ≈ 1,40 × 10⁻⁴⁵ — поступове зникнення порядку, завдяки якому x − y == 0 надійно випливає з x == y поблизу нуля.Чому два нулі. +0 = 0x00000000, −0 = 0x80000000. Вони порівнюються як рівні (+0 == −0), але знак поширюється: 1,0 / +0 = +∞, 1,0 / −0 = −∞. Збереження знака через проміжні значення, які зникають за порядком, зберігає коректним знак у тотожностях на кшталт 1,0 / (1,0 / x).sNaN проти qNaN. Обидва мають E = 0xFF, M' ≠ 0. Розрізняє їх старший біт мантиси (MSB): MSB = 1 → тихий NaN (поширюється безшумно); MSB = 0 → сигналізуючий NaN (піднімає виняток недійсної операції при першому використанні — призначений для позначення неініціалізованої пам'яті). Решта бітів мантиси несуть діагностичне навантаження.Субнормальні (денормалізовані) числа та поступове зникнення порядку. Коли поле експоненти дорівнює 0x00, а мантиса ненульова, неявна провідна 1. перетворюється на 0., а експоненту зафіксовано на мінімумі 2⁻¹²⁶. Це заповнює проміжок між найменшим нормалізованим значенням (≈ 1,18 × 10⁻³⁸) і нулем значеннями, розставленими з кроком нормалізованого ULP на цьому масштабі — поступове зникнення порядку. Без субнормальних будь-який результат менший за найменше нормалізоване число стрибав би одразу в нуль (раптове зникнення порядку), порушуючи тотожності на кшталт x − y == 0 ⇔ x == y та породжуючи раптовий стрибок точності на ~7 порядків величини поблизу дна діапазону.Зверніть увагу на: чому прихований біт дає один безкоштовний біт мантиси (він є лише в нормалізованих числах; у субнормальних він дорівнює 0); чому те, що 0x00000000 — це буквально самі нулі, є свідомим вибором (занулена через memset пам'ять читається як +0,0); різницю між «денормалізованим» і «ненормалізованим» (останнє — це просто будь-яке ненормалізоване кодування); що субнормальні числа торгують точністю за діапазон поблизу нуля — чим ближче до 2⁻¹⁴⁹, тим менше значущих бітів.<br>Докладніше: <a data-tooltip-position="top" aria-label="L13. Арифметика з рухомою комою > 5. Двійковий формат із рухомою комою" data-href="L13. Арифметика з рухомою комою#5. Двійковий формат із рухомою комою" href="lectures/l13.-арифметика-з-рухомою-комою.html#5. Двійковий формат із рухомою комою" class="internal-link" target="_self" rel="noopener nofollow">§5 Двійковий формат із рухомою комою</a>, <a data-tooltip-position="top" aria-label="L13. Арифметика з рухомою комою > 8. Стандарт IEEE 754" data-href="L13. Арифметика з рухомою комою#8. Стандарт IEEE 754" href="lectures/l13.-арифметика-з-рухомою-комою.html#8. Стандарт IEEE 754" class="internal-link" target="_self" rel="noopener nofollow">§8 IEEE 754</a>, <a data-tooltip-position="top" aria-label="L13. Арифметика з рухомою комою > Проблема нуля" data-href="L13. Арифметика з рухомою комою#Проблема нуля" href="lectures/l13.-арифметика-з-рухомою-комою.html#Проблема нуля" class="internal-link" target="_self" rel="noopener nofollow">§8 Проблема нуля</a>, <a data-tooltip-position="top" aria-label="L13. Арифметика з рухомою комою > Денормалізовані (субнормальні) числа" data-href="L13. Арифметика з рухомою комою#Денормалізовані (субнормальні) числа" href="lectures/l13.-арифметика-з-рухомою-комою.html#Денормалізовані (субнормальні) числа" class="internal-link" target="_self" rel="noopener nofollow">§8 Субнормальні</a>, <a data-tooltip-position="top" aria-label="L13. Арифметика з рухомою комою > Спеціальні значення: NaN (не число)" data-href="L13. Арифметика з рухомою комою#Спеціальні значення: NaN (не число)" href="lectures/l13.-арифметика-з-рухомою-комою.html#Спеціальні значення: NaN (не число)" class="internal-link" target="_self" rel="noopener nofollow">§8 NaN</a>.0,1 не має точного двійкового представлення. Повторне множення на 2 дає 0,1₁₀ = 0,0001100110011001100…₂ — періодичний дріб із періодом 4. Скінченна 24-бітна мантиса (враховуючи прихований біт) мусить його округлити, тож в одинарній точності насправді зберігається 0,100000001490116…. Аналогічно округлюється 0,2, і ці дві похибки округлення не компенсують окремо округлене 0,3, тож найближча представна сума потрапляє на один ULP вище 0,3: 0,1 + 0,2 = 0,30000000000000004 у подвійній точності.Єдині десяткові дроби, що кодуються точно, — це ті, знаменник яких (у нескоротному вигляді) є степенем 2: 0,5, 0,25, 0,125, 0,375. Все інше (0,1, 0,2, 0,3, 1/3, π) округлюється.Катастрофічне скорочення. Віднімання двох майже рівних чисел з рухомою комою стирає провідні біти, що збігалися, залишаючи лише молодші — там, де живе накопичений шум округлення. Якщо з 52 бітів мантиси скоротилися 48, залишається 4 біти сигналу:x = 1e-8
math.sqrt(x**2 + 1) - 1 # → 0.0 (біти скоротилися)
x**2 / (math.sqrt(x**2 + 1) + 1) # → 5e-17 (переформульовано, коректно)
Виправлення — алгебраїчне: переформулюйте так, щоб уникнути віднімання. Для 1 − cos x поблизу 0 перепишіть як 2·sin²(x/2). Для квадратної формули з великим |b| обчисліть спочатку «безпечний» корінь, потім відновіть інший через x₁·x₂ = c/a.Сума Кагана відновлює скорочені молодші біти під час накопичення. Плинна змінна compensation захоплює (total + y − total) − y — саме те, що втратило додавання, — і додає до наступного доданка. Ефект: підсумовування з приблизно подвоєною робочою точністю ціною кількох додаткових операцій.Порівняння чисел з рухомою комою у вихідному коді. Ніколи не пишіть a == b, крім випадків, коли ви знаєте, що операція точна до біта (порівняння з точно представними константами, такими як 0,0, степені 2; або результати, скопійовані з іншого числа з рухомою комою). Загальне правило — змішаний допуск:abs(a - b) &lt;= max(rel_tol * max(abs(a), abs(b)), abs_tol)
Відносний допуск обслуговує великі магнітуди (де ULP величезний); абсолютний допуск — значення поблизу нуля (де відносний допуск втрачає сенс). Python-івський math.isclose(a, b, rel_tol=1e-9, abs_tol=0.0) реалізує саме це.Зверніть увагу на: які галузі страждають найбільше — наука/графіка (фізичні симуляції, розв'язувачі матриць) домінують, але й фінансам не байдуже (Ванкуверська фондова біржа за 22 місяці дрейфувала з 1000 до 524 через усікання замість округлення до найближчого); втрату асоціативності як окрему пастку ((a+b)+c ≠ a+(b+c), коли магнітуди розрізняються — дрейф «тику» 0,1 секунди в ракеті Patriot саме такий, а не класичне скорочення).<br>Докладніше: <a data-tooltip-position="top" aria-label="L13. Арифметика з рухомою комою > Чому деякі десяткові дроби не можна закодувати точно" data-href="L13. Арифметика з рухомою комою#Чому деякі десяткові дроби не можна закодувати точно" href="lectures/l13.-арифметика-з-рухомою-комою.html#Чому деякі десяткові дроби не можна закодувати точно" class="internal-link" target="_self" rel="noopener nofollow">§6 Чому 0.1 не точне</a>, <a data-tooltip-position="top" aria-label="L13. Арифметика з рухомою комою > Втрата значущості (катастрофічне скорочення)" data-href="L13. Арифметика з рухомою комою#Втрата значущості (катастрофічне скорочення)" href="lectures/l13.-арифметика-з-рухомою-комою.html#Втрата значущості (катастрофічне скорочення)" class="internal-link" target="_self" rel="noopener nofollow">§10 Катастрофічне скорочення</a>, <a data-tooltip-position="top" aria-label="L13. Арифметика з рухомою комою > Накопичення похибок" data-href="L13. Арифметика з рухомою комою#Накопичення похибок" href="lectures/l13.-арифметика-з-рухомою-комою.html#Накопичення похибок" class="internal-link" target="_self" rel="noopener nofollow">§10 Сума Кагана</a>, <a data-tooltip-position="top" aria-label="L13. Арифметика з рухомою комою > Практики захисного програмування" data-href="L13. Арифметика з рухомою комою#Практики захисного програмування" href="lectures/l13.-арифметика-з-рухомою-комою.html#Практики захисного програмування" class="internal-link" target="_self" rel="noopener nofollow">§10 Захисні практики</a>.Два співіснуючі x86-стеки з рухомою комою. x87 з'явився першим (співпроцесор 8087, 1980; на одному кристалі з 80486DX, 1989). SSE з'явився 1999 року як перепроєктування з нуля; SSE2 (2001) додав подвійну точність, AVX (2011) розширив до 256 бітів, AVX-512 (2016) — до 512.Три причини, чому компілятори орієнтуються на SSE/AVX. (1) Пропускна здатність SIMD — одна addps виконує 4 додавання одинарної точності за такт; AVX-512 — 16. Жодна інструкція x87 не торкається більш ніж одного значення. (2) Плоска множина регістрів добре взаємодіє зі стандартним розподілом регістрів — стек x87 вимагає випромінення fxch та ретельного впорядкування скидів, що руйнує позачергове планування. (3) Відтворюваність та ABI — SSE2 є стандартним ABI для float/double на x86-64 скрізь. Результати округлюються на ширині типу, тож -O0 і -O2 дають однакові числа.Чому x87 виживає. (а) long double на Linux x86-64 — це 80-бітний розширений формат; нічого іншого в апаратурі не дає 18–19 десяткових цифр. (б) Вбудовані трансцендентні функції (fsin, fcos, fpatan, f2xm1, fyl2x) — у SSE/AVX немає еквівалентів; їх реалізують програмно. (в) Успадковані бінарники. ISA зберігає x87, але компілятори за замовчуванням використовують SSE2 для скалярної арифметики з рухомою комою.Чому стекова модель вороже ставиться до компіляторів. x87 подає свої 8 регістрів даних як стек LIFO — ST(0) завжди на вершині, більшість арифметики неявно виконується над ST(0) (і опційно ST(1)). Щоб подати інструкції значення, яке не на вершині, компілятор мусить випромінити FXCH, щоб замінити його на місце, потім часто ще один FXCH, щоб відновити порядок. Розподіл регістрів перетворюється на «запланувати послідовність обертів стека, що ніколи не скидає»; перестановка двох незалежних операцій з рухомою комою зазвичай вимагає переписування супровідного обліку стека. Позачергова апаратура не може перейменовувати слоти стека так, як перейменовує плоскі регістри, тож справжній паралелізм чисел з рухомою комою обмежений стелею. Плоска множина XMM/YMM SSE/AVX дозволяє розподільнику обрати будь-яке джерело і будь-яке призначення для будь-якої операції — та сама механіка, яку вже використовує цілочисловий розподільник регістрів, — а механізм перейменування може відкрити кожну незалежну операцію планувальнику.Зверніть увагу на: пастку невідтворюваності 80-бітної розширеної точності (той самий код, інший прапорець -O → інший результат, бо компілятор вирішив скинути значення); 32 регістри ZMM в AVX-512 важливі не менше за саму ширину (менше скидів у тісних циклах); long double на Windows та macOS відображається на 64-бітний double — це рішення ABI, а не апаратури.<br>Докладніше: <a data-tooltip-position="top" aria-label="L13. Арифметика з рухомою комою > Коротка історія апаратної арифметики з рухомою комою" data-href="L13. Арифметика з рухомою комою#Коротка історія апаратної арифметики з рухомою комою" href="lectures/l13.-арифметика-з-рухомою-комою.html#Коротка історія апаратної арифметики з рухомою комою" class="internal-link" target="_self" rel="noopener nofollow">§11 Історія апаратної FP</a>, <a data-tooltip-position="top" aria-label="L13. Арифметика з рухомою комою > Архітектура x87 FPU" data-href="L13. Арифметика з рухомою комою#Архітектура x87 FPU" href="lectures/l13.-арифметика-з-рухомою-комою.html#Архітектура x87 FPU" class="internal-link" target="_self" rel="noopener nofollow">§11 Архітектура x87</a>, <a data-tooltip-position="top" aria-label="L13. Арифметика з рухомою комою > Перехід до SSE та AVX" data-href="L13. Арифметика з рухомою комою#Перехід до SSE та AVX" href="lectures/l13.-арифметика-з-рухомою-комою.html#Перехід до SSE та AVX" class="internal-link" target="_self" rel="noopener nofollow">§11 SSE/AVX</a>, <a data-tooltip-position="top" aria-label="L13. Арифметика з рухомою комою > x87 проти SSE/AVX: порівняння" data-href="L13. Арифметика з рухомою комою#x87 проти SSE/AVX: порівняння" href="lectures/l13.-арифметика-з-рухомою-комою.html#x87 проти SSE/AVX: порівняння" class="internal-link" target="_self" rel="noopener nofollow">§11 x87 проти SSE/AVX</a>, <a data-tooltip-position="top" aria-label="L13. Арифметика з рухомою комою > Чому x87 досі має значення" data-href="L13. Арифметика з рухомою комою#Чому x87 досі має значення" href="lectures/l13.-арифметика-з-рухомою-комою.html#Чому x87 досі має значення" class="internal-link" target="_self" rel="noopener nofollow">§11 Чому x87 досі має значення</a>.<br>Блок II · L13 · 3 питання · Частина <a data-href="Exam-Questions" href="exam-questions.html" class="internal-link" target="_self" rel="noopener nofollow">Exam-Questions</a>]]></description><link>exam-prep/l13-prep.html</link><guid isPermaLink="false">Exam-Prep/L13-Prep.md</guid><pubDate>Wed, 22 Apr 2026 04:44:10 GMT</pubDate></item><item><title><![CDATA[L14-Prep]]></title><description><![CDATA[Охоплює екзаменаційні питання II.19 – II.21. Офіційне формулювання — у <a data-href="Exam-Questions" href="exam-questions.html" class="internal-link" target="_self" rel="noopener nofollow">Exam-Questions</a>; повний обсяг підготовки — у <a data-href="Preparation-Guide" href="exam-prep/preparation-guide.html" class="internal-link" target="_self" rel="noopener nofollow">Preparation-Guide</a>.Кожен запис нижче — це стислий кістяк відповіді: достатньо, щоб зорієнтуватись, але не заміна лекції. Переходьте за посиланнями «Докладніше», щоб поглибитись. Рядки «Зверніть увагу на» позначають пастки, на які цілять додаткові питання екзаменатора.Просторово-часова сітка 5 стадій. Канонічний RISC-конвеєр має стадії IF (вибірка) → ID (декодування + читання регістрів) → EX (АЛП / обчислення ефективної адреси) → MEM (кеш даних D-cache) → WB (зворотний запис у регістр). Коли конвеєр заповнений, всередині машини одночасно «живуть» п'ять інструкцій — по одній на стадію:Такт: 1 2 3 4 5 6 7 8
I1: IF ID EX MEM WB
I2: IF ID EX MEM WB
I3: IF ID EX MEM WB
I4: IF ID EX MEM WB
I5: IF ID EX MEM WB
Після 4-тактного розгону одна інструкція завершується щотакту.Затримка vs пропускна здатність. Один add все одно проходить усі п'ять стадій — затримка на одну інструкцію залишається ≈ 5 тактів. Що змінюється — це усталена пропускна здатність: 1 інструкція за такт замість 1 за 5 тактів у неконвеєризованому дизайні. Тобто IPC = 1,0 проти 0,2 — чистий 5-кратний приріст без пришвидшення тактової частоти. Період однотактної машини дорівнював сумі затримок стадій (5,0 нс → 200 МГц у прикладі з лекції); період конвеєризованої машини має покрити лише найповільнішу окрему стадію (1,5 нс → ~667 МГц), тож тактова частота теж зростає, підсилюючи виграш у пропускній здатності.Регістри конвеєра. Між кожною парою стадій стоїть банк тригерів — регістр конвеєра — який фіксує вихід стадії на передньому фронті. Без нього стадія 2 бачила б, як комбінаційний вихід стадії 1 змінюється всередині такту, коли стадія 1 бралася до наступної інструкції. Ціна реальна: приблизно один тригер на кожен міжстадійний сигнал, плюс кожен тригер додає свою затримку clock-to-Q (~50–100 пс) і час установлення до ефективного такту — це фіксована підлога, нижче якої період тактового сигналу не опуститься, скільки б ви не дробили стадії.Зменшувана віддача і Pentium 4. Поглиблення конвеєра скорочує роботу кожної стадії, але не накладні витрати на регістри — тож виграші тануть. Гірше, штраф за хибне передбачення масштабується з глибиною — скидання при переході викидає всі спекулятивні інструкції в польоті. Pentium 4 Prescott витиснули до 31 стадії, цілячись у ~3,8 ГГц, але кожне хибне передбачення коштувало 20+ тактів, а за реалістичного відсотка хибних передбачень 5–10% на коді з великою кількістю переходів (~20% інструкцій — переходи) втрати від скидання з'їдали виграш від тактової частоти. Prescott часто був повільнішим за Pentium III із коротшим конвеєром на тій самій частоті і був замінений архітектурою Core на ~14 стадіях. Сучасні ядра тримаються в межах 12–20 стадій — емпіричний оптимум.Зверніть увагу на: затримка ≠ пропускна здатність (витрати на розгін — одноразові, не на інструкцію); регістри конвеєра додають фіксовану підлогу clock-to-Q, від якої глибокі конвеєри не можуть утекти; Prescott провалився через штраф хибного передбачення × частоту переходів, а не через саму частоту.<br>Докладніше: <a data-tooltip-position="top" aria-label="L14. Паралелізм на рівні інструкцій та потоків > 1. Чому однотактний дизайн помирає: проблема пропускної здатності" data-href="L14. Паралелізм на рівні інструкцій та потоків#1. Чому однотактний дизайн помирає: проблема пропускної здатності" href="lectures/l14.-паралелізм-на-рівні-інструкцій-та-потоків.html#1. Чому однотактний дизайн помирає: проблема пропускної здатності" class="internal-link" target="_self" rel="noopener nofollow">§1 Чому однотактний дизайн помирає</a>, <a data-tooltip-position="top" aria-label="L14. Паралелізм на рівні інструкцій та потоків > 2. Конвеєризація: як запустити фабрику" data-href="L14. Паралелізм на рівні інструкцій та потоків#2. Конвеєризація: як запустити фабрику" href="lectures/l14.-паралелізм-на-рівні-інструкцій-та-потоків.html#2. Конвеєризація: як запустити фабрику" class="internal-link" target="_self" rel="noopener nofollow">§2 Конвеєризація</a>, <a data-tooltip-position="top" aria-label="L14. Паралелізм на рівні інструкцій та потоків > Теорема про пропускну здатність / затримку" data-href="L14. Паралелізм на рівні інструкцій та потоків#Теорема про пропускну здатність / затримку" href="lectures/l14.-паралелізм-на-рівні-інструкцій-та-потоків.html#Теорема про пропускну здатність / затримку" class="internal-link" target="_self" rel="noopener nofollow">§2 Теорема про пропускну здатність / затримку</a>, <a data-tooltip-position="top" aria-label="L14. Паралелізм на рівні інструкцій та потоків > 4.4.6 Штраф хибного передбачення і повчальна історія Pentium 4" data-href="L14. Паралелізм на рівні інструкцій та потоків#4.4.6 Штраф хибного передбачення і повчальна історія Pentium 4" href="lectures/l14.-паралелізм-на-рівні-інструкцій-та-потоків.html#4.4.6 Штраф хибного передбачення і повчальна історія Pentium 4" class="internal-link" target="_self" rel="noopener nofollow">§4.4.6 Pentium 4</a>.Таксономія. Конфлікт — будь-яка причина, чому наступна інструкція не може увійти в наступну стадію наступного такту. Чотири категорії, кожна зі стандартним апаратним розв'язанням:Випередження — майже безплатне для АЛП-RAW. Вихід стадії-виробника прокладено безпосередньо на вхід EX споживача в тому ж такті. RAW типу EX→EX і EX→MEM коштують нуль бульбашок. Лише load-use (MEM→EX) коштує одну бульбашку — значення завантаження не готове до кінця MEM, а наступній інструкції воно потрібне на початку EX на одну стадію раніше, а жоден провід не біжить назад у часі. Компілятор часто може заповнити цей слот незалежною інструкцією. Коли випередження не рятує, блок виявлення конфліктів у кінці ID заморожує PC/IF/ID та вставляє NOP-бульбашку — один такт на кожну затримку.Конфлікти керування та штраф за хибне передбачення. 5-стадійний конвеєр розв'язує переходи в кінці EX, втрачаючи ~2 такти на перехід. Сучасні ядра x86 (15–20 стадій) мають штрафи за хибне передбачення 15–25 тактів. Передбачувачі — 2-бітні насичувальні лічильники, дворівневі адаптивні, TAGE, perceptron, турнірні — виходять на 95–99% точності, і саме це робить спекулятивну вибірку вигідною.Чому WAR/WAW безпечні за виконання за порядком. Конвеєр з виконанням за порядком завершує читання й записи в програмному порядку, тож пізніший запис ніколи не випередить ранішого читання того ж регістра. WAR і WAW з'являються лише тоді, коли машина переупорядковує — тобто в OoO. Перейменування регістрів убиває обидва за побудовою: з ~200+ фізичними регістрами, відображеними через RAT на 16 архітектурних імен, жодні дві інструкції в польоті не торкаються одного й того ж фізичного сховища. Залишається лише RAW — а RAW — це факт про потік даних, не про іменування.Зверніть увагу на: load-use завжди коштує ≥ 1 бульбашки (фізика, не лінь); WAR/WAW — це конфлікти іменування, а не потоку даних — перейменування стирає їх; сучасне хибне передбачення переходу — це ~15–25 тактів, а не 2, бо сучасні конвеєри мають 15–20 стадій.<br>Докладніше: <a data-tooltip-position="top" aria-label="L14. Паралелізм на рівні інструкцій та потоків > 3. Конфлікти: ціна перекриття" data-href="L14. Паралелізм на рівні інструкцій та потоків#3. Конфлікти: ціна перекриття" href="lectures/l14.-паралелізм-на-рівні-інструкцій-та-потоків.html#3. Конфлікти: ціна перекриття" class="internal-link" target="_self" rel="noopener nofollow">§3 Конфлікти</a>, <a data-tooltip-position="top" aria-label="L14. Паралелізм на рівні інструкцій та потоків > 4.1 Випередження (обхід): не чекай, підключайся до дроту" data-href="L14. Паралелізм на рівні інструкцій та потоків#4.1 Випередження (обхід): не чекай, підключайся до дроту" href="lectures/l14.-паралелізм-на-рівні-інструкцій-та-потоків.html#4.1 Випередження (обхід): не чекай, підключайся до дроту" class="internal-link" target="_self" rel="noopener nofollow">§4.1 Випередження</a>, <a data-tooltip-position="top" aria-label="L14. Паралелізм на рівні інструкцій та потоків > 4.4 Передбачення переходів: спершу вгадай, потім попросиш вибачення" data-href="L14. Паралелізм на рівні інструкцій та потоків#4.4 Передбачення переходів: спершу вгадай, потім попросиш вибачення" href="lectures/l14.-паралелізм-на-рівні-інструкцій-та-потоків.html#4.4 Передбачення переходів: спершу вгадай, потім попросиш вибачення" class="internal-link" target="_self" rel="noopener nofollow">§4.4 Передбачення переходів</a>, <a data-tooltip-position="top" aria-label="L14. Паралелізм на рівні інструкцій та потоків > Перейменування регістрів: безплатне усунення WAR і WAW" data-href="L14. Паралелізм на рівні інструкцій та потоків#Перейменування регістрів: безплатне усунення WAR і WAW" href="lectures/l14.-паралелізм-на-рівні-інструкцій-та-потоків.html#Перейменування регістрів: безплатне усунення WAR і WAW" class="internal-link" target="_self" rel="noopener nofollow">§6 Перейменування регістрів</a>.Три механізми на спектрі паралелізму. Усі три годують той самий голодний бекенд виконання; різниця в тому, звідки походить паралелізм і що вони поділяють.Під які навантаження. Суперскалярний виграє на щільних числових циклах з довгими ланцюгами без залежностей; програє на послідовних ланцюгах залежностей (6-широке ядро на погоні за вказівниками отримує IPC ≈ 1, бо кожне завантаження годує наступне). SMT виграє, коли один потік застоюється на пам'яті (промах кеша = 200+ тактів простою бекенду) — другий потік заповнює прогалину. Оригінальний піч Intel: ~5% додаткової площі кристала за 15–30% більше пропускної здатності. Він шкодить, коли обидва потоки активно працюють з плаваючою комою і насичують ті самі блоки: два потоки matmul на одному ядрі виконуються повільніше, ніж на окремих ядрах, бо борються за FP-конвеєр, ділять навпіл ефективну ємність L1 і забруднюють один одному передбачувач. HPC-кластери та розгортання Oracle DB регулярно вимикають HT. Багатоядерний виграє на невимушено паралельній роботі — кожне ядро має приватні L1/L2, жодної боротьби, поки не дійде до спільного L3 чи смуги пам'яті.Spectre/Meltdown експлуатують спекуляцію. Побічний канал — це кеш: влучання в L1 ≈ 3–4 такти vs промах у DRAM ≈ 200–300 тактів, розрив у таймінгу на два порядки величини. Зловмисник очищує 256-рядковий зондовий масив, примушує жертву виконати спекулятивне завантаження, що залежить від секрету і торкається probe[secret*64], чекає на знищення, потім вимірює час доступу до кожного запису зонду — швидкий виявляє байт. Meltdown (CVE-2017-5754) зловживав відкладеною перевіркою привілеїв Intel: спекулятивне завантаження з ядерної адреси повертало значення у вікно OoO до того, як біт user/supervisor було перевірено. Spectre v1 тренував передбачувач напрямку спекулювати поза перевірку меж. Spectre v2 отруював BTB, щоб перенаправляти непрямі переходи на обрані зловмисником гаджети. SMT погіршує ситуацію, бо сусіди ділять L1 та передбачувачі без ізоляції.Ціна протидій. KPTI прибирає ядерні сторінки з карти в користувацькому режимі, тож кожен системний виклик тепер перемикає таблиці сторінок — ~5–30% на навантаженнях з великою кількістю системних викликів (PCID трохи відновлює). Retpoline нейтралізує отруєння BTB. OpenBSD вимкнула SMT за замовчуванням у 2018 році; хмарні провайдери відмовляються ко-планувати недовірливих орендарів на тому ж фізичному ядрі.Зверніть увагу на: SMT ділить майже все в бекенді — це оптимізація, яка усуває ізоляцію, тож вона не є межею безпеки; багатоядерний ≠ SMT (окремі L1/L2 vs спільні); Spectre/Meltdown цілять у спекуляцію, а SMT лише розширює поверхню атаки; ціна KPTI масштабується з частотою системних викликів, а не з частотою ЦП.<br>Докладніше: <a data-tooltip-position="top" aria-label="L14. Паралелізм на рівні інструкцій та потоків > 5. Ідемо вшир: суперскалярність (і чому її мало)" data-href="L14. Паралелізм на рівні інструкцій та потоків#5. Ідемо вшир: суперскалярність (і чому її мало)" href="lectures/l14.-паралелізм-на-рівні-інструкцій-та-потоків.html#5. Ідемо вшир: суперскалярність (і чому її мало)" class="internal-link" target="_self" rel="noopener nofollow">§5 Суперскалярність</a>, <a data-tooltip-position="top" aria-label="L14. Паралелізм на рівні інструкцій та потоків > 8. Паралелізм рівня потоків в апаратурі: SMT та Hyper-Threading" data-href="L14. Паралелізм на рівні інструкцій та потоків#8. Паралелізм рівня потоків в апаратурі: SMT та Hyper-Threading" href="lectures/l14.-паралелізм-на-рівні-інструкцій-та-потоків.html#8. Паралелізм рівня потоків в апаратурі: SMT та Hyper-Threading" class="internal-link" target="_self" rel="noopener nofollow">§8 SMT / Hyper-Threading</a>, <a data-tooltip-position="top" aria-label="L14. Паралелізм на рівні інструкцій та потоків > 8.4. Коли SMT шкодить" data-href="L14. Паралелізм на рівні інструкцій та потоків#8.4. Коли SMT шкодить" href="lectures/l14.-паралелізм-на-рівні-інструкцій-та-потоків.html#8.4. Коли SMT шкодить" class="internal-link" target="_self" rel="noopener nofollow">§8.4 Коли SMT шкодить</a>, <a data-tooltip-position="top" aria-label="L14. Паралелізм на рівні інструкцій та потоків > 7. Податок на спекуляцію: Spectre та Meltdown" data-href="L14. Паралелізм на рівні інструкцій та потоків#7. Податок на спекуляцію: Spectre та Meltdown" href="lectures/l14.-паралелізм-на-рівні-інструкцій-та-потоків.html#7. Податок на спекуляцію: Spectre та Meltdown" class="internal-link" target="_self" rel="noopener nofollow">§7 Spectre та Meltdown</a>, <a data-tooltip-position="top" aria-label="L14. Паралелізм на рівні інструкцій та потоків > 8.5. SMT як межа безпеки (або радше ні)" data-href="L14. Паралелізм на рівні інструкцій та потоків#8.5. SMT як межа безпеки (або радше ні)" href="lectures/l14.-паралелізм-на-рівні-інструкцій-та-потоків.html#8.5. SMT як межа безпеки (або радше ні)" class="internal-link" target="_self" rel="noopener nofollow">§8.5 SMT як межа безпеки</a>.<br>Блок II · L14 · 3 питання · Частина <a data-href="Exam-Questions" href="exam-questions.html" class="internal-link" target="_self" rel="noopener nofollow">Exam-Questions</a>]]></description><link>exam-prep/l14-prep.html</link><guid isPermaLink="false">Exam-Prep/L14-Prep.md</guid><pubDate>Wed, 22 Apr 2026 04:44:10 GMT</pubDate></item><item><title><![CDATA[L15-Prep]]></title><description><![CDATA[Охоплює екзаменаційні питання II.22 – II.24. Офіційне формулювання — у <a data-href="Exam-Questions" href="exam-questions.html" class="internal-link" target="_self" rel="noopener nofollow">Exam-Questions</a>; повний обсяг підготовки — у <a data-href="Preparation-Guide" href="exam-prep/preparation-guide.html" class="internal-link" target="_self" rel="noopener nofollow">Preparation-Guide</a>.Кожен запис нижче — це стислий кістяк відповіді: достатньо, щоб зорієнтуватись, але не заміна лекції. Переходьте за посиланнями «Докладніше», щоб поглибитись. Рядки «Зверніть увагу на» позначають пастки, на які цілять додаткові питання екзаменатора.Формула та її стеля. Розділіть навантаження на паралельну частку p та послідовну частку (1 − p). Дайте паралельній частині N ядер, і вона завершиться за p/N; послідовна частина не змінюється. Загальне прискорення порівняно з одним ядром —Speedup(N) = 1 / ((1 − p) + p/N)Спрямуйте N → ∞ — і p/N зникає, тож S_∞ = 1 / (1 − p). Це жорстка стеля, встановлена самою лише послідовною часткою — її не можна купити за рахунок додавання ядер. При p = 0,9 асимптота дорівнює 1 / 0,1 = 10×, скільки б ядер ви не кидали на задачу.Таблиця для скінченного N (p = 0,9). Застосовуємо формулу Амдала (наведену в лекції) до p = 0,9 — це пророблений приклад, а не таблиця, що з'являється в лекції дослівно:Зменшення віддачі та правило 1%. Збільшення вчетверо з 4 до 16 ядер зсуває прискорення лише з 3,08× до 6,40×. Підвищте p = 0,99 — і стеля стає 100×: єдиний відсоток послідовного коду обмежує кластер із 1000 ядер на рівні 100×, тож ті 900 додаткових ядер майже нічого не дають. Оптимізуйте найбільшу частку часу, а не найлегший шматок.Сильне проти слабкого масштабування (переосмислення Густафсона). Амдал питає: задача фіксована, наскільки швидше з N ядрами? — сильне масштабування. Густафсон (1988) перевернув це: бюджет за годинником фіксований, наскільки більшу задачу з N ядрами? — слабке масштабування. Якщо паралельна робота зростає разом із N, а послідовна лишається сталою, прискорення стає S(N) = s + p·N — лінійне за N, без стелі. Гіперскейлери свідомо живуть у світі Густафсона (кліматична симуляція, навчання ML-моделей, індексування вебу). Сильне масштабування пасує навантаженням, обмеженим за затримкою (запит за 100 мс, кадр за 16 мс).Чому реальні програми не дотягують. Амдал припускає паралелізм без накладних витрат. Реальні накладні: синхронізація (блокування, бар'єри), комунікація (трафік шини, переходи через NUMA), дисбаланс навантаження та трафік когерентності кешів (інвалідації MESI масштабуються гірше, ніж лінійно). Універсальний закон масштабованості (USL) Ґантера додає доданки суперництва та когерентності й передбачає, що після «солодкої точки» N пропускна здатність насправді падає. make -j8, який дає 3× на 8-ядерному ноутбуці, — це саме ця картина.Зверніть увагу на: плутанину «90% піддається розпаралелюванню» з «90% паралельно на N ядрах» (це різне — перше є p Амдала, друге — заміряне постфактум); застосування Амдала до навантажень зі слабким масштабуванням (не той закон); забування того, що накладні витрати на синхронізацію означають: реальна крива провалюється нижче кривої Амдала.<br>Докладніше: <a data-tooltip-position="top" aria-label="L15. Енергоефективність, обмеження та майбутні тренди > 2.1 Закон Амдала, виведений із загадки" data-href="L15. Енергоефективність, обмеження та майбутні тренди#2.1 Закон Амдала, виведений із загадки" href="lectures/l15.-енергоефективність,-обмеження-та-майбутні-тренди.html#2.1 Закон Амдала, виведений із загадки" class="internal-link" target="_self" rel="noopener nofollow">§2.1 Амдал, виведення</a>, <a data-tooltip-position="top" aria-label="L15. Енергоефективність, обмеження та майбутні тренди > 2.2 Стеля прискорення" data-href="L15. Енергоефективність, обмеження та майбутні тренди#2.2 Стеля прискорення" href="lectures/l15.-енергоефективність,-обмеження-та-майбутні-тренди.html#2.2 Стеля прискорення" class="internal-link" target="_self" rel="noopener nofollow">§2.2 Стеля прискорення</a>, <a data-tooltip-position="top" aria-label="L15. Енергоефективність, обмеження та майбутні тренди > 2.4 Закон Густафсона" data-href="L15. Енергоефективність, обмеження та майбутні тренди#2.4 Закон Густафсона" href="lectures/l15.-енергоефективність,-обмеження-та-майбутні-тренди.html#2.4 Закон Густафсона" class="internal-link" target="_self" rel="noopener nofollow">§2.4 Густафсон</a>, <a data-tooltip-position="top" aria-label="L15. Енергоефективність, обмеження та майбутні тренди > 2.5 Сильне масштабування проти слабкого" data-href="L15. Енергоефективність, обмеження та майбутні тренди#2.5 Сильне масштабування проти слабкого" href="lectures/l15.-енергоефективність,-обмеження-та-майбутні-тренди.html#2.5 Сильне масштабування проти слабкого" class="internal-link" target="_self" rel="noopener nofollow">§2.5 Сильне проти слабкого</a>, <a data-tooltip-position="top" aria-label="L15. Енергоефективність, обмеження та майбутні тренди > 2.6 Застереження, яке ігнорують обидва закони" data-href="L15. Енергоефективність, обмеження та майбутні тренди#2.6 Застереження, яке ігнорують обидва закони" href="lectures/l15.-енергоефективність,-обмеження-та-майбутні-тренди.html#2.6 Застереження, яке ігнорують обидва закони" class="internal-link" target="_self" rel="noopener nofollow">§2.6 USL</a>.Формула. Вентиль CMOS, що перемикається 0→1→0, розсіює CV² за повний цикл (заряд Q = CV підтягується з енергією QV). Помножимо на частоту f і на частку α циклів, у яких кожен вентиль реально перемикається:P_dynamic = α · C · V² · fα ≈ 0,1 для трактів даних, 1,0 для дерева тактового сигналу (~20–30% потужності чипа — тому агресивно застосовують clock gating). V піднесено у квадрат — єдиний надлінійний важіль, і саме його індустрія переслідувала двадцять років.Масштабування Деннарда (1974). Зменшимо розмір елемента у k разів; змасштабуємо решту змінних разом.Протягом тридцяти років (1974–2004) кожне зменшення подвоювало кількість транзисторів при тому самому тепловому рахунку. Архітектори витрачали цей бонус на ширші конвеєри, більші кеші, глибшу спекуляцію.Чому воно зламалося (~2005). Дві стіни одразу. Нижня межа напруги: нижче ~0,7 В тепловий шум (kT/q) починає перевертати біти. Вибух витоків: нижче V_t (потрібне для швидкого перемикання при низькій V) робить підпорогові витоки експоненційними за V_t (~10× на кожні 60 мВ); до 45 нм статична потужність сягнула 30–50% загальної. З V, притиснутою до підлоги, і витоками, що ростуть, важіль V² перестав працювати, а питома потужність почала зростати з кожним зменшенням.Відповідь індустрії: багатоядерність. Тактова частота пласка на рівні 3–5 ГГц з 2005 року (Pentium 4 сягнув 3,8 ГГц у 2004-му). Єдиний спосіб використати зростаючий транзисторний бюджет — розподілити його на більше ядер із нижчою частотою. Каденція Intel «tick-tock» сповільнилась — 10 нм затримали на роки.Відповідь під час виконання: DVFS і TDP. DVFS обирає робочу точку (V, f) з валідованої драбини P-станів ACPI (P0 = макс., Pn = найнижчий). Урізавши обидва вдвічі, динамічна потужність падає у 4 × 2 = 8× — як скорочено V²·f ≈ V³. TDP — це сталий бюджет охолодження (28 Вт для ноутбука, 170 Вт для настільного, 400 Вт для сервера); турбо-режим короткочасно перевищує TDP на кількох ядрах, поки корпус холодний, а потім троттлиться назад. Кожна пізніша техніка (dark silicon, гетерогенність, чіплети) існує, щоб виконувати більше роботи під фіксованим TDP.Зверніть увагу на: плутанину між TDP (сталий) і піком/турбо (миттєвий — може короткочасно перевищувати TDP); твердження, що напруга «завжди може знижуватися» (V_min — це фізика, а не інженерний запас); змішування Деннарда (завершилося ~2005) з Муром (щільність — досі кульгає вперед на TSMC N3/N2); забування інтуїції V³ при сперечанні про економію від DVFS.<br>Докладніше: <a data-tooltip-position="top" aria-label="L15. Енергоефективність, обмеження та майбутні тренди > 3.3 Динамічна потужність у три рядки" data-href="L15. Енергоефективність, обмеження та майбутні тренди#3.3 Динамічна потужність у три рядки" href="lectures/l15.-енергоефективність,-обмеження-та-майбутні-тренди.html#3.3 Динамічна потужність у три рядки" class="internal-link" target="_self" rel="noopener nofollow">§3.3 Виведення динамічної потужності</a>, <a data-tooltip-position="top" aria-label="L15. Енергоефективність, обмеження та майбутні тренди > 3.4 Розв'язка: чому інженери ганялися за напругою" data-href="L15. Енергоефективність, обмеження та майбутні тренди#3.4 Розв'язка: чому інженери ганялися за напругою" href="lectures/l15.-енергоефективність,-обмеження-та-майбутні-тренди.html#3.4 Розв'язка: чому інженери ганялися за напругою" class="internal-link" target="_self" rel="noopener nofollow">§3.4 Квадрат напруги</a>, <a data-tooltip-position="top" aria-label="L15. Енергоефективність, обмеження та майбутні тренди > 3.6 Статична потужність: проблема витоків, яка нікуди не зникає" data-href="L15. Енергоефективність, обмеження та майбутні тренди#3.6 Статична потужність: проблема витоків, яка нікуди не зникає" href="lectures/l15.-енергоефективність,-обмеження-та-майбутні-тренди.html#3.6 Статична потужність: проблема витоків, яка нікуди не зникає" class="internal-link" target="_self" rel="noopener nofollow">§3.6 Витоки</a>, <a data-tooltip-position="top" aria-label="L15. Енергоефективність, обмеження та майбутні тренди > 4.1 Арифметика, яка робить DVFS непереборним" data-href="L15. Енергоефективність, обмеження та майбутні тренди#4.1 Арифметика, яка робить DVFS непереборним" href="lectures/l15.-енергоефективність,-обмеження-та-майбутні-тренди.html#4.1 Арифметика, яка робить DVFS непереборним" class="internal-link" target="_self" rel="noopener nofollow">§4.1 Математика DVFS</a>, <a data-tooltip-position="top" aria-label="L15. Енергоефективність, обмеження та майбутні тренди > 5.2 Що зламалося близько 2005 року" data-href="L15. Енергоефективність, обмеження та майбутні тренди#5.2 Що зламалося близько 2005 року" href="lectures/l15.-енергоефективність,-обмеження-та-майбутні-тренди.html#5.2 Що зламалося близько 2005 року" class="internal-link" target="_self" rel="noopener nofollow">§5.2 Що зламалося у 2005</a>.Означення. Dark silicon — це частка транзисторів, яку в будь-який момент потрібно тримати невімкненою, щоб загальне розсіювання лишалося в межах TDP корпусу. «Темний» не означає зламаний чи змарнований — ці ділянки є справним кремнієм, який чекає своєї черги; наступної мілісекунди згасають уже інші ділянки. Чип розподіляє бюджет потужності у часі між своєю кількістю транзисторів.Чому воно існує. Масштабування Деннарда завершилося (§II.23): зменшення більше не ріжуть потужність на транзистор, натомість кількість транзисторів продовжує рости (k² за одне зменшення). Засвітити все одразу означало б вистрибнути за TDP. Прогноз Есмаелзаде та ін. у статті ISCA 2011 — 50–80% темних при 8 нм — виявився по суті правильним. 96-ядерний EPYC тримає всі ядра на ~2,4–3,0 ГГц, але турбить одне до ~5 ГГц; засвітити всі 96 на 5 ГГц означало б перевищити ~400 Вт TDP за секунди.Гетерогенні платформи — відповідь.Спеціалізація — головний важіль. Блок фіксованої функції у 10–100× енергоефективніший на операцію, ніж ЦП у своїй предметній ділянці (відеокодек ~100–1000× на H.265, NPU ~30–100× на int8-інференції, ISP ~100× на демозаїку). Ніякої вибірки, декодування, спекуляції, перейменування регістрів — лише арифметика. Якщо більшість чипа все одно мусить бути темною, хай темна частка виправдає себе тоді, коли засвічується. Кластер ЦП на SoC телефона зазвичай займає &lt;20% площі кристала.Вартість програмного забезпечення. Планувальник вирішує, який тип ядра виконує який потік — Linux schedutil + EAS, Thread Director на Alder Lake, Grand Central Dispatch на Apple. Програмування крос-прискорювачів фрагментоване: CUDA, Metal, OpenCL/SYCL, oneAPI плюс бекенди TensorFlow/PyTorch. Кожен прискорювач — це місце, де ваш код автоматично не запуститься; непідтримані навантаження відкатуються на ЦП із енергією у 10–100× гіршою. Ранній Alder Lake довелося повністю вимикати AVX-512, бо E-ядра його не реалізовували.Аргумент сталого розвитку / дата-центрів. На великому масштабі енергія домінує у TCO. Гіперскейлери випускають кастомний кремній — лекція називає Google TPU (ML-ASIC) як канонічний приклад — бо виграш у 10× ефективності на поширеному навантаженні окуповує NRE за один-два цикли розгортання та зменшує навантаження на охолодження. Масовий x86 стає запасним варіантом для «довгого хвоста». (AWS Graviton та Azure Cobalt — це подальші галузеві приклади того самого шаблону, поза межами лекції.)Зверніть увагу на: трактування «темного» як «зламаного» (це політика розподілу часу, а не пошкодження); думку, що big.LITTLE — це просто «дві швидкості ядра» (складне — у плануванні; три покоління знадобилося до HMP+EAS у 2015, щоб воно запрацювало); забування прецеденту AVX-512 при сперечанні, що «гетерогенність — це легко»; пропуск прямої лінії кінець Деннарда → dark silicon → гетерогенність → кастомний кремній дата-центрів (TPU).<br>Докладніше: <a data-tooltip-position="top" aria-label="L15. Енергоефективність, обмеження та майбутні тренди > 5.3 Означення" data-href="L15. Енергоефективність, обмеження та майбутні тренди#5.3 Означення" href="lectures/l15.-енергоефективність,-обмеження-та-майбутні-тренди.html#5.3 Означення" class="internal-link" target="_self" rel="noopener nofollow">§5.3 Означення dark silicon</a>, <a data-tooltip-position="top" aria-label="L15. Енергоефективність, обмеження та майбутні тренди > 5.4 Наскільки темний?" data-href="L15. Енергоефективність, обмеження та майбутні тренди#5.4 Наскільки темний?" href="lectures/l15.-енергоефективність,-обмеження-та-майбутні-тренди.html#5.4 Наскільки темний" class="internal-link" target="_self" rel="noopener nofollow">§5.4 Наскільки темний</a>, <a data-tooltip-position="top" aria-label="L15. Енергоефективність, обмеження та майбутні тренди > 5.6 Проєктні відповіді — п'ять хеджів проти однієї стіни" data-href="L15. Енергоефективність, обмеження та майбутні тренди#5.6 Проєктні відповіді — п'ять хеджів проти однієї стіни" href="lectures/l15.-енергоефективність,-обмеження-та-майбутні-тренди.html#5.6 Проєктні відповіді — п'ять хеджів проти однієї стіни" class="internal-link" target="_self" rel="noopener nofollow">§5.6 П'ять проєктних відповідей</a>, <a data-tooltip-position="top" aria-label="L15. Енергоефективність, обмеження та майбутні тренди > 6.1 ARM big.LITTLE — шаблон" data-href="L15. Енергоефективність, обмеження та майбутні тренди#6.1 ARM big.LITTLE — шаблон" href="lectures/l15.-енергоефективність,-обмеження-та-майбутні-тренди.html#6.1 ARM big.LITTLE — шаблон" class="internal-link" target="_self" rel="noopener nofollow">§6.1 big.LITTLE</a>, <a data-tooltip-position="top" aria-label="L15. Енергоефективність, обмеження та майбутні тренди > 6.4 Apple M-series — гетерогенність як сенс усього" data-href="L15. Енергоефективність, обмеження та майбутні тренди#6.4 Apple M-series — гетерогенність як сенс усього" href="lectures/l15.-енергоефективність,-обмеження-та-майбутні-тренди.html#6.4 Apple M-series — гетерогенність як сенс усього" class="internal-link" target="_self" rel="noopener nofollow">§6.4 Apple M-series</a>, <a data-tooltip-position="top" aria-label="L15. Енергоефективність, обмеження та майбутні тренди > 6.6 Вартість програмування — момент чесності" data-href="L15. Енергоефективність, обмеження та майбутні тренди#6.6 Вартість програмування — момент чесності" href="lectures/l15.-енергоефективність,-обмеження-та-майбутні-тренди.html#6.6 Вартість програмування — момент чесності" class="internal-link" target="_self" rel="noopener nofollow">§6.6 Вартість програмування</a>.<br>Блок II · L15 · 3 питання · Частина <a data-href="Exam-Questions" href="exam-questions.html" class="internal-link" target="_self" rel="noopener nofollow">Exam-Questions</a>]]></description><link>exam-prep/l15-prep.html</link><guid isPermaLink="false">Exam-Prep/L15-Prep.md</guid><pubDate>Wed, 22 Apr 2026 04:44:10 GMT</pubDate></item><item><title><![CDATA[L05-Prep]]></title><description><![CDATA[Охоплює екзаменаційні питання I.13 – I.15. Офіційне формулювання — у <a data-href="Exam-Questions" href="exam-questions.html" class="internal-link" target="_self" rel="noopener nofollow">Exam-Questions</a>; повний обсяг підготовки — у <a data-href="Preparation-Guide" href="exam-prep/preparation-guide.html" class="internal-link" target="_self" rel="noopener nofollow">Preparation-Guide</a>.Кожен запис нижче — це компактне ядро відповіді: достатньо, щоб зорієнтуватися, але не заміна лекції. Переходьте за посиланнями «Докладніше», щоб поглибитися. Рядки «Зверніть увагу на» позначають пастки, на які спрямовані додаткові запитання екзаменатора.Розкладка фрейму. В cdecl викликана функція закріплює фрейм на BP (EBP у 32-бітному режимі). Стек на x86 росте донизу, тож вищі адреси — згори, нижчі — знизу. Для 32-бітного cdecl зі слотами по 4 байти:Старші адреси [arg 3] ← [EBP+16] [arg 2] ← [EBP+12] [arg 1] ← [EBP+8] [адреса повернення] ← [EBP+4] [збережений EBP] ← [EBP+0] ← EBP вказує сюди [local 1] ← [EBP-4] [local 2] ← [EBP-8] [збережені регістри викликаної]
ESP→[...] ← поточна вершина
Молодші адреси
Для 16-бітного TASM (приклад із лекції) всі слоти мають 2 байти: перший аргумент на [BP+4], адреса повернення на [BP+2], збережений BP на [BP+0], перша локальна змінна на [BP-2]. Щоб перейти до 32-бітного cdecl, масштабуйте зміщення у 2 рази ([EBP+8] для arg 1, [EBP+4] для адреси повернення, збережений EBP на [EBP+0]).Пролог і епілог. Канонічні послідовності:; Пролог
push ebp ; зберегти якір фрейму викликача
mov ebp, esp ; новий якір фрейму = поточна вершина
sub esp, N ; зарезервувати N байтів для локальних змінних ; ... тіло функції ... ; Епілог
mov esp, ebp ; скинути локальні змінні
pop ebp ; відновити EBP викликача
ret ; зняти адресу повернення в EIP
Пару mov esp,ebp; pop ebp можна скоротити однією інструкцією leave.Чому BP існує окремо від SP. ESP рухається в межах тіла функції — кожен push, call чи sub esp його зсуває. Якби ви звертались до локальних змінних як [ESP+N], то N змінювалося б після кожної маніпуляції зі стеком, а alloca (змінне зростання стека) взагалі був би неможливим. EBP — це стабільний якір, встановлений один раз у пролозі: [EBP+8] завжди arg1, [EBP-4] завжди local1, незалежно від того, що тіло функції робить з ESP. Саме ця незмінність дозволяє зневаджувачам обходити ланцюг фреймів — кожен збережений EBP вказує на збережений EBP попереднього фрейму, тож backtrace у gdb — це обхід зв'язаного списку.Чому в cdecl прибирає викликач. Викликач точно знає, скільки байтів він поклав (add esp, 4*N після call), тож функції зі змінною кількістю аргументів на кшталт printf працюють — викликана функція не може знати їхньої кількості. stdcall (угода Pascal з лекції) переносить прибирання на викликану функцію (ret N знімає N байтів після повернення), заощаджуючи розмір коду на кожному місці виклику, але забороняючи виклики зі змінною кількістю аргументів. Листові функції (leaf functions — без викликів, з кількома локальними змінними, без потреби у фреймах для зневадження) можуть зовсім пропустити пролог, якщо компілятор здатен довести, що адресація через [ESP+k] безпечна — поширено за -O2 -fomit-frame-pointer.Зверніть увагу на: плутанину 16-бітних ([BP+4] для arg1) і 32-бітних ([EBP+8]) зміщень; твердження, що збережений BP на [BP+4] (насправді на [BP+0]); забування, що якщо не відновити ESP перед RET, то RET зніме у EIP те сміття, що опиниться на ESP, і перейде туди; виняток з листовою функцією.<br>Докладніше: <a data-tooltip-position="top" aria-label="L05. Організація програми — стек і процедури > Фрейм стека" data-href="L05. Організація програми — стек і процедури#Фрейм стека" href="lectures/l05.-організація-програми-—-стек-і-процедури.html#Фрейм стека" class="internal-link" target="_self" rel="noopener nofollow">§8 Фрейм стека</a>, <a data-tooltip-position="top" aria-label="L05. Організація програми — стек і процедури > Угода про виклик C/C++: кращий підхід" data-href="L05. Організація програми — стек і процедури#Угода про виклик C/C++: кращий підхід" href="lectures/l05.-організація-програми-—-стек-і-процедури.html#Угода про виклик C/C++: кращий підхід" class="internal-link" target="_self" rel="noopener nofollow">§8 Угода cdecl</a>, <a data-tooltip-position="top" aria-label="L05. Організація програми — стек і процедури > 7.5. Напрямок зростання стека: критична деталь" data-href="L05. Організація програми — стек і процедури#7.5. Напрямок зростання стека: критична деталь" href="lectures/l05.-організація-програми-—-стек-і-процедури.html#7.5. Напрямок зростання стека: критична деталь" class="internal-link" target="_self" rel="noopener nofollow">§7.5 Стек росте донизу</a>.Механістичне протиставлення. Усі три інструкції змінюють IP; лише CALL і RET додатково торкаються стека.CALL — це буквально push EIP_next; jmp target, а RET — буквально pop EIP. Жодної магії: це лише JMP у парі зі стековою дисципліною. Саме тому RET можна синтезувати з простіших інструкцій: pop some_reg; jmp some_reg — рукописний еквівалент. Та сама ідея в зворотному напрямку дає синтетичний CALL: покласти на стек мітку, jmp до функції.Трасування ланцюжка викликів на два рівні. main → f → g, з розгортанням адрес повернення:Після CALL f: [ret_to_main] ← ESP
Після CALL g: [ret_to_f] [ret_to_main] ← ESP
Всередині g, RET: [ret_to_main] ← ESP, EIP = ret_to_f
Назад у f, RET: [] ← ESP, EIP = ret_to_main
Кожен RET розгортає рівно один фрейм, бо CALL і RET — парні операції push/pop на одному стеку.Збій при незбалансованому стеку. RET не знає, що саме є «адресою повернення» — він знімає у EIP те, що лежить на вершині стека. Тож якщо викликана функція кладе значення під час виконання і забуває їх зняти:my_proc proc push ax ; якесь значення, яке функція захотіла зберегти push bx ; ще одне значення ; ... тіло ... ret ; ПОМИЛКА: на вершині стека — BX, а не адреса повернення
my_proc endp
RET зніме BX в EIP і перейде за тією адресою, яку випадково тримав BX, — майже напевно сміття, миттєвий крах. Саме тому SP має бути відновлений до глибини, яка була до CALL, перш ніж виконається RET: усе, що функція заштовхнула, треба збалансувати відповідним зняттям (або mov esp, ebp), щоб слово, яке прочитає RET, і було тією адресою повернення, яку туди записав CALL.Чому x86 використовує стекове повернення, а не регістр зв'язку. RISC ISA (ARM, MIPS, RISC-V) на інструкції переходу зі зв'язуванням кладуть адресу повернення у виділений регістр (LR, $ra, x1), тому випадок листової функції коштує нуль звернень до пам'яті. x86 натомість кладе в пам'ять, бо в 8086 було дуже мало регістрів (всього 8 загального призначення) — витратити один постійно як регістр зв'язку було занадто дорого — і ще тому, що стековий підхід природно композиційний для вкладених викликів: стек сам по собі — це LIFO з адрес повернення, жодного прологу для збереження регістра не потрібно, щоб нарощувати глибину. Ціна: кожен виклик звертається до пам'яті. Листові функції на RISC цього уникають; нелистові функції на RISC мусять самі скидати LR у стек, що повертає нас до моделі x86.Зверніть увагу на: твердження, що RET — це точно «JMP [ESP]» (він ще й збільшує ESP на розмір слова після переходу); забування, що синтетичний RET потребує і POP, і JMP (сам лише JMP [ESP] читає слот, але лишає його на стеку); трактування вибору «стекове повернення проти регістра зв'язку» як «x86 гірший» — це компроміс (звернення до пам'яті на виклик проти бюджету регістрів, а RISC усе одно скидає LR, коли виклики вкладаються).<br>Докладніше: <a data-tooltip-position="top" aria-label="L05. Організація програми — стек і процедури > Механіка CALL: PUSH та JMP" data-href="L05. Організація програми — стек і процедури#Механіка CALL: PUSH та JMP" href="lectures/l05.-організація-програми-—-стек-і-процедури.html#Механіка CALL: PUSH та JMP" class="internal-link" target="_self" rel="noopener nofollow">§8 Механіка CALL</a>, <a data-tooltip-position="top" aria-label="L05. Організація програми — стек і процедури > Механіка RET: POP та JMP" data-href="L05. Організація програми — стек і процедури#Механіка RET: POP та JMP" href="lectures/l05.-організація-програми-—-стек-і-процедури.html#Механіка RET: POP та JMP" class="internal-link" target="_self" rel="noopener nofollow">§8 Механіка RET</a>, <a data-tooltip-position="top" aria-label="L05. Організація програми — стек і процедури > CALL та RET: дві інструкції" data-href="L05. Організація програми — стек і процедури#CALL та RET: дві інструкції" href="lectures/l05.-організація-програми-—-стек-і-процедури.html#CALL та RET: дві інструкції" class="internal-link" target="_self" rel="noopener nofollow">§8 Парність CALL/RET</a>.Трасування factorial(3). Просте рекурсивне означення: fact(n) = n==0 ? 1 : n * fact(n-1). Кожен виклик кладе на стек новий фрейм; фрейми розгортаються на RET.Call fact(3): frame[n=3] на стеку глибина 1 Call fact(2): frame[n=2] | frame[n=3] глибина 2 Call fact(1): frame[n=1] | frame[n=2] | frame[n=3] глибина 3 Call fact(0): frame[n=0] | ... | frame[n=3] глибина 4 повертає 1 frame[n=0] знято; fact(1) обчислює 1*1 = 1, повертає frame[n=1] знято; fact(2) обчислює 2*1 = 2, повертає frame[n=2] знято; fact(3) обчислює 3*2 = 6, повертає
frame[n=3] знято; викликач отримує 6
На піку одночасно живуть чотири фрейми (враховуючи fact(0)). Кожен містить: збережений BP, адресу повернення, параметр n, а також будь-які локальні змінні та збережені регістри — приблизно 16–32 байти на фрейм у неоптимізованому коді. Ключовий момент, який робить схема наочним: множення n * fact(n-1) відбувається після повернення з рекурсивного виклику, тому кожен зовнішній фрейм має залишатися живим на стеку, доки його рекурсивний нащадок не поверне значення.Обмеження на глибину. Стек потоку — це фіксована, попередньо виділена ділянка пам'яті, один неперервний резерв, встановлений при створенні потоку. Максимальна глибина рекурсії ≈ розмір_ділянки_стека / розмір_фрейму. Якщо кожен фрейм — 32 байти, а ділянка стека — скажімо, кілька мегабайт, це обмежує рекурсію десятками тисяч викликів. Це обмеження ніяк не пов'язане із загальною ОП: машина зі 128 ГБ ОП усе одно аварійно завершить наївний fact(200000), бо ділянка стека потоку — це крихітна частина цієї ОП, зафіксована при старті потоку. Вільна фізична пам'ять не має значення, щойно вказівник стека виходить за нижню межу ділянки.Ітеративний еквівалент. Ті самі обчислення як цикл використовують один фрейм незалежно від N:int r = 1;
for (int i = 1; i &lt;= n; i++) r *= i;
return r;
Використання стека — O(1) замість O(n). Шаблон перетворення: перевести рекурентне співвідношення в акумулятор, який ви оновлюєте в циклі, щоб не лишалося відкладеної роботи «після повернення з рекурсивного виклику». Будь-яка рекурсія, результат якої — просто згортка над входом (сума, добуток, підрахунок), перекладається напряму. Рекурсія зі структурним розгалуженням (обхід дерева, quicksort) потребує явної структури даних «стек» замість стека викликів, але ця пам'ять тепер у купі, де ви контролюєте зростання.Зверніть увагу на: ототожнення загальної ОП і розміру стека на потік (кілька-мегабайтний стек обмежує рекурсію незалежно від того, скільки ОП вільно); забування, що стек росте донизу на x86 — SP ініціалізується на верхівку виділеної ділянки стека й зменшується на кожному PUSH / CALL; опис вичерпання стека як «закінчується пам'ять» загалом — конкретний збій у тому, що заштовхування за нижню межу ділянки стека пише в те, що йде за нею в адресному просторі процесу, і саме тому ОС покладається на апаратуру для перехоплення такого доступу, а не дозволяє рекурсії тихо псувати сусідні дані; твердження, що звичайний factorial «безкоштовно» перетворюється на цикл — перетворюється, але лише тому, що множення асоціативне; неасоціативні згортки потребують шаблону акумулятора, щоб бути коректними.<br>Докладніше: <a data-tooltip-position="top" aria-label="L05. Організація програми — стек і процедури > Фрейм стека" data-href="L05. Організація програми — стек і процедури#Фрейм стека" href="lectures/l05.-організація-програми-—-стек-і-процедури.html#Фрейм стека" class="internal-link" target="_self" rel="noopener nofollow">§8 Фрейм стека</a>, <a data-tooltip-position="top" aria-label="L05. Організація програми — стек і процедури > 7.5. Напрямок зростання стека: критична деталь" data-href="L05. Організація програми — стек і процедури#7.5. Напрямок зростання стека: критична деталь" href="lectures/l05.-організація-програми-—-стек-і-процедури.html#7.5. Напрямок зростання стека: критична деталь" class="internal-link" target="_self" rel="noopener nofollow">§7.5 Стек росте донизу</a>, <a data-tooltip-position="top" aria-label="L05. Організація програми — стек і процедури > 7.3. Чому процесор має спеціальну апаратну підтримку стека" data-href="L05. Організація програми — стек і процедури#7.3. Чому процесор має спеціальну апаратну підтримку стека" href="lectures/l05.-організація-програми-—-стек-і-процедури.html#7.3. Чому процесор має спеціальну апаратну підтримку стека" class="internal-link" target="_self" rel="noopener nofollow">§7.3 Вкладені контексти</a>.<br>Блок I · L05 · 3 питання · Частина <a data-href="Exam-Questions" href="exam-questions.html" class="internal-link" target="_self" rel="noopener nofollow">Exam-Questions</a>]]></description><link>exam-prep/l05-prep.html</link><guid isPermaLink="false">Exam-Prep/L05-Prep.md</guid><pubDate>Wed, 22 Apr 2026 04:44:10 GMT</pubDate></item><item><title><![CDATA[L06-Prep]]></title><description><![CDATA[Охоплює екзаменаційні питання I.16 – I.18. Офіційне формулювання — у <a data-href="Exam-Questions" href="exam-questions.html" class="internal-link" target="_self" rel="noopener nofollow">Exam-Questions</a>; повний обсяг підготовки — у <a data-href="Preparation-Guide" href="exam-prep/preparation-guide.html" class="internal-link" target="_self" rel="noopener nofollow">Preparation-Guide</a>.Кожен запис нижче — стислий кістяк відповіді: достатньо, щоб зорієнтуватися, але не заміна лекції. Переходьте за посиланнями «Докладніше», щоб заглибитися. Рядки «Зверніть увагу на» позначають пастки, на які націлені уточнювальні запитання екзаменатора.Класична схема. Один ЦП (Блок керування + АЛП + Регістри) з'єднується через одну спільну системну шину (адресні лінії + лінії даних + лінії керування) з однією уніфікованою пам'яттю, що містить як інструкції програми, так і дані. I/O підключається до тієї самої шини. Джон фон Нейман описав це у 1945 році у First Draft of a Report on the EDVAC; ідея програми, що зберігається в пам'яті, одним реченням: розглядати інструкції як дані, щоб їх можна було завантажувати, редагувати та виконувати, як будь-які інші байти, — без перекомутації кабелів. Саме це поховало ENIAC-подібне програмування комутаційними кабелями й уможливило кожен компілятор, асемблер, компонувальник, завантажувач та JIT, що з'являлися відтоді.Вузьке місце. Оскільки інструкції та дані поділяють одну шину до однієї пам'яті, ЦП не може одночасно здійснювати вибірку наступної інструкції та завантажувати/зберігати операнд — запити серіалізуються через одну вузьку трубу. Джон Бекус назвав це «вузьким місцем фон Неймана» у своїй лекції з нагоди вручення премії Тюрінга у 1977 році. Це не мало б значення, якби ЦП і DRAM працювали на подібних швидкостях, але це не так: з початку 1980-х тактові частоти ядер зросли приблизно в 1000 разів, тоді як затримка DRAM покращилася лише приблизно в 10 разів. Результат: на частоті 3,4 ГГц (0,29 нс/цикл) промах DRAM зупиняє ядро приблизно на 200 циклів — цього часу вистачить, щоб виконати кілька сотень інструкцій регістр-регістр.Сучасні засоби пом'якшення. Кожна значна мікроархітектурна особливість сучасного ядра існує, принаймні частково, щоб закамуфлювати цю шину. Основні:
Роздільний L1 на L1i та L1d (модифікована Гарвардська на межі кешу) — ЦП може вибирати інструкцію та читати операнд даних за один і той самий цикл, бо кожен L1 має власний порт до ядра.
Багаторівневий кеш (L2, L3) — частота влучень 95–99% перетворює середню затримку пам'яті з ~200 циклів на одиниці циклів.
Апаратні блоки попередньої вибірки — виявляють послідовний доступ або доступ з кроком і підтягують рядки в кеш до того, як інструкція завантаження буде видана.
Кілька блоків завантаження/збереження плюс ширший файл регістрів (x86-64 REX → 16 РЗП; AVX-512 додає 32 векторні регістри), щоб більше трафіку пам'яті могло перекриватися.
Позачергове + суперскалярне виконання, щоб покрити будь-яку залишкову зупинку корисною роботою.
Чиста проти модифікованої Гарвардської проти фон Неймана.Зверніть увагу на: розрізнення архітектурного та мікроархітектурного рівнів (x86 є фон Неймана на рівні ISA, але конвеєр — модифікована Гарвардська); чому самомодифікований або JIT-згенерований код потребує clflush + серіалізувальної інструкції (L1i не автоматично когерентний з L1d на більшості x86 — збереження потрапляють у L1d, але кеш інструкцій потрібно інвалідувати, перш ніж ЦП повторно здійснюватиме вибірку з цього рядка); розрив продуктивності процесор-пам'ять як причину того, що половина кристала — це SRAM.<br>Докладніше: <a data-tooltip-position="top" aria-label="L06. Системна архітектура > 11.1 Архітектура фон Неймана" data-href="L06. Системна архітектура#11.1 Архітектура фон Неймана" href="lectures/l06.-системна-архітектура.html#11.1 Архітектура фон Неймана" class="internal-link" target="_self" rel="noopener nofollow">§11.1 Фон Неймана</a>, <a data-tooltip-position="top" aria-label="L06. Системна архітектура > 11.4 Сучасна реальність: Модифікована Гарвардська архітектура" data-href="L06. Системна архітектура#11.4 Сучасна реальність: Модифікована Гарвардська архітектура" href="lectures/l06.-системна-архітектура.html#11.4 Сучасна реальність: Модифікована Гарвардська архітектура" class="internal-link" target="_self" rel="noopener nofollow">§11.4 Модифікована Гарвардська</a>, <a data-tooltip-position="top" aria-label="L06. Системна архітектура > 13.1 Проблема" data-href="L06. Системна архітектура#13.1 Проблема" href="lectures/l06.-системна-архітектура.html#13.1 Проблема" class="internal-link" target="_self" rel="noopener nofollow">§13.1 Вузьке місце</a>, <a data-tooltip-position="top" aria-label="L06. Системна архітектура > 13.3 Як сучасні ЦП борються з вузьким місцем" data-href="L06. Системна архітектура#13.3 Як сучасні ЦП борються з вузьким місцем" href="lectures/l06.-системна-архітектура.html#13.3 Як сучасні ЦП борються з вузьким місцем" class="internal-link" target="_self" rel="noopener nofollow">§13.3 Засоби пом'якшення</a>.Згори донизу з реалістичними числами порядку величини.Розриви між сусідніми рівнями становлять приблизно 3–10× для кешів, ~100× на межі DRAM, ~1000× на межі SSD і ще ~100× униз до HDD. Затримка згори донизу охоплює вісім порядків величини.Чому форма мусить бути пірамідою — фізика. Швидка пам'ять означає короткі провідники, малі масиви та агресивні комірки. Біт SRAM займає 6 транзисторів; біт DRAM — 1 транзистор + 1 конденсатор. На одиницю площі SRAM містить приблизно в 6 разів менше бітів, і вона має розміщуватися на кристалі ЦП, щоб утримати затримку провідників у межах одного циклу. У міру зростання масиву пам'яті час доступу збільшується разом із фізичним розміром, бо і поширення сигналу, і паразитна ємність зростають разом із задіяними провідниками та комірками — подвоєння L1 з 32 КБ до 64 КБ уже робить влучання повільнішим, а якщо зробити L1 розміром з DRAM (8 ГБ), його затримка все одно наблизиться до DRAM, що знищить сенс.Чому форма мусить бути пірамідою — економіка. Вартість за ГБ: SRAM &gt; $1000; DRAM ~$2–5; NAND SSD ~$0,05–0,10; HDD ~$0,01–0,03. Пласка ієрархія з самої лише SRAM L1-швидкості на 16 ГБ коштувала б п'яти- й більшезначної суми за машину — і все одно була б повільною (див. вище). Пласка ієрархія з самого лише HDD була б доступною та мертвою на будь-якому інтерактивному навантаженні. Піраміда платить за швидкість лише там, де робочий набір малий.Чому це взагалі працює — локальність. Реальні програми досягають 95–99% влучень у L1/L2/L3 завдяки часовій локальності (лічильник циклу, верхівка стека) та просторовій локальності (прохід масивом, поля структур). Саме тому передачі відбуваються рядками кешу по 64 байти, а не окремими байтами — один промах підтягує сусідів, до яких програма статистично ось-ось звернеться.Ціна неочікуваного промаху. Якщо дані гарячі в L1, завантаження — це ~4 цикли. Якщо падаємо аж у DRAM, те саме завантаження — ~200 циклів, у 50 разів гірше. Якщо витіснено на SSD — ~300 000 циклів, цього часу вистачить, щоб сотні разів виконати гарячий цикл більшості програм. Саме тому заповнення ОП і переливання у своп спричиняє такі драматичні уповільнення: розрив зростає з 50× до 10⁶×.Зверніть увагу на: чому L1 не можна «просто» виростити до розміру DRAM (час доступу зростає з розміром масиву — затримка провідників і паразитна ємність — а вартість SRAM за ГБ непомірна); аргумент 6T-проти-1T1C, чому SRAM за своєю природою менш щільна; де вписуються GPU (регістри + спільна пам'ять/SRAM + HBM/GDDR поза кристалом; немає глибокого багаторівневого кешу, як у ЦП, але величезні файли регістрів на кожен SM); чому блоки попередньої вибірки разом з передачами рядків кешу є операційним хребтом піраміди.<br>Докладніше: <a data-tooltip-position="top" aria-label="L06. Системна архітектура > 9.1 Проблема зберігання даних" data-href="L06. Системна архітектура#9.1 Проблема зберігання даних" href="lectures/l06.-системна-архітектура.html#9.1 Проблема зберігання даних" class="internal-link" target="_self" rel="noopener nofollow">§9.1 Проблема зберігання</a>, <a data-tooltip-position="top" aria-label="L06. Системна архітектура > 9.2 Типи пам'яті" data-href="L06. Системна архітектура#9.2 Типи пам'яті" href="lectures/l06.-системна-архітектура.html#9.2 Типи пам'яті" class="internal-link" target="_self" rel="noopener nofollow">§9.2 SRAM/DRAM/диск</a>, <a data-tooltip-position="top" aria-label="L06. Системна архітектура > 9.4 Кеш-пам'ять (SRAM)" data-href="L06. Системна архітектура#9.4 Кеш-пам'ять (SRAM)" href="lectures/l06.-системна-архітектура.html#9.4 Кеш-пам'ять (SRAM)" class="internal-link" target="_self" rel="noopener nofollow">§9.4 Кеш</a>, <a data-tooltip-position="top" aria-label="L06. Системна архітектура > 9.6 Повна ієрархія пам'яті" data-href="L06. Системна архітектура#9.6 Повна ієрархія пам'яті" href="lectures/l06.-системна-архітектура.html#9.6 Повна ієрархія пам'яті" class="internal-link" target="_self" rel="noopener nofollow">§9.6 Повна таблиця ієрархії</a>.Реальний режим (8086, 1978). Адреса = сегмент × 16 + зміщення, обидва 16-бітні → 20-бітна фізична, адресується 1 МБ. Немає рівнів привілеїв, немає захисту пам'яті, немає багатозадачності — одна програма володіє всією машиною з прямим доступом до IVT за 0000:0000, текстової відеопам'яті за B800:0000 та BIOS ROM за F000:0000. Випадковий MOV може перезаписати ОС, і ніщо в апаратурі цього не зупинить. Кожен x86 ЦП — навіть сучасний Core Ultra — досі вмикається в реальному режимі; мікропрограма здійснює перехід перед передаванням керування ОС.<br>Захищений режим (80286 — базовий, 1982; 80386 — повний, 1985). 386 розширив адреси до 32 бітів (4 ГБ на процес), додав сторінкову організацію через MMU і перетворив сегментні регістри з прямих значень бази на селектори, що індексують таблицю дескрипторів. Дескриптор зберігає базу + межу + права доступу + DPL (привілей), тому кожне звернення до пам'яті через цей сегмент перевіряється за розміром, типом і привілеєм. Порушення породжує <a href=".?query=tag:GP" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#GP">#GP</a> (Загальне порушення захисту), і ОС завершує порушника. GDT є системною (сегменти ядра, TSS); LDT — на процес, завдяки чому два процеси можуть легітимно надавати одному селектору різне значення.Порівняння за адресацією, привілеями, захистом.Як захищений режим уможливлює багатозадачність і захист. Три механізми складаються докупи: віртуальні адресні простори на процес (два процеси, що обидва використовують 0x00400000, відображаються на різні фізичні сторінки — жоден навіть не може назвати пам'ять іншого); апаратний привілей (код Кільця 3 не може виконувати привілейовані інструкції, торкатися портів вводу-виводу чи обходити перевірки дескрипторів — кожна привілейована операція іде через контрольований SYSCALL у Кільце 0); витісняльне планування (переривання таймера дозволяє ОС зберегти стан однієї задачі через TSS і завантажити стан іншої, незалежно від співпраці програми). Разом вони перетворюють машину на спільний ресурс: гра, що падає, не може обвалити ОС, а десятки процесів перемежовуються прозоро.Чому DOS залишився в реальному режимі. DOS з'явився у 1981 році — ще до того, як 286 запровадив навіть базовий захищений режим, і за чотири роки до того, як 386 зробив його практичним. DOS однозадачний за проєктом, тому ізоляція та витіснення нічого йому не дали б. Його модель програмування припускає прямий доступ (INT 21h видає «сирі» сегменти, TSR-програми латають IVT, застосунки пишуть безпосередньо у відеопам'ять). Переписування будь-чого з цього зламає кожну наявну DOS-програму, а зворотна сумісність із бібліотекою DOS-застосунків була саме тією причиною, чому люди купували ПК.Чому сучасні ОС потребують захищеного режиму. Windows NT+, Linux і macOS покладаються на кожну з цих гарантій. Без адресних просторів на процес застосунки топчуть один одного. Без апаратного привілею будь-яка програма може перепрограмувати MMU або читати пам'ять ядра. Без витіснення один нескінченний цикл підвішує машину. Десятки вкладок браузера, тисячі потоків ядра, ізольовані контейнери — нічого з цього неможливе в реальному режимі.Зверніть увагу на: повільне впровадження у 1980-х–90-х (захищений режим 286 був квитком в один бік — увійти можна, але чистого повернення немає, — тому Windows 3.x працював гібридно, а Windows 95 досі завантажувався через DOS; NT та Linux були першими нативними ОС захищеного режиму); роль GDT/LDT (селектори індексують дескриптори, що несуть базу + межу + права + DPL — з перевіркою діапазону та привілею, чого самі сегментні регістри виконати не можуть); чому «дикий» вказівник у реальному режимі може обвалити всю систему, тоді як у захищеному він породжує #GP і гине ізольовано. Сам перехід режиму — це встановлення CR0.PE = 1, зазвичай завантажувачем одразу після того, як MBR передає керування.<br>Докладніше: <a data-tooltip-position="top" aria-label="L06. Системна архітектура > 10.1 Реальний режим (режим 8086)" data-href="L06. Системна архітектура#10.1 Реальний режим (режим 8086)" href="lectures/l06.-системна-архітектура.html#10.1 Реальний режим (режим 8086)" class="internal-link" target="_self" rel="noopener nofollow">§10.1 Реальний режим</a>, <a data-tooltip-position="top" aria-label="L06. Системна архітектура > 10.2 Захищений режим" data-href="L06. Системна архітектура#10.2 Захищений режим" href="lectures/l06.-системна-архітектура.html#10.2 Захищений режим" class="internal-link" target="_self" rel="noopener nofollow">§10.2 Захищений режим</a>, <a data-tooltip-position="top" aria-label="L06. Системна архітектура > 10.3 Реальний режим проти Захищеного режиму: порівняння" data-href="L06. Системна архітектура#10.3 Реальний режим проти Захищеного режиму: порівняння" href="lectures/l06.-системна-архітектура.html#10.3 Реальний режим проти Захищеного режиму: порівняння" class="internal-link" target="_self" rel="noopener nofollow">§10.3 Порівняння</a>, <a data-tooltip-position="top" aria-label="L06. Системна архітектура > 10.4 Чому це важливо для нашого курсу" data-href="L06. Системна архітектура#10.4 Чому це важливо для нашого курсу" href="lectures/l06.-системна-архітектура.html#10.4 Чому це важливо для нашого курсу" class="internal-link" target="_self" rel="noopener nofollow">§10.4 Наслідки для курсу</a>.<br>Блок I · L06 · 3 питання · Частина <a data-href="Exam-Questions" href="exam-questions.html" class="internal-link" target="_self" rel="noopener nofollow">Exam-Questions</a>]]></description><link>exam-prep/l06-prep.html</link><guid isPermaLink="false">Exam-Prep/L06-Prep.md</guid><pubDate>Wed, 22 Apr 2026 04:44:10 GMT</pubDate></item><item><title><![CDATA[L07-Prep]]></title><description><![CDATA[Охоплює екзаменаційні питання I.19 – I.21. Офіційне формулювання — у <a data-href="Exam-Questions" href="exam-questions.html" class="internal-link" target="_self" rel="noopener nofollow">Exam-Questions</a>; повний обсяг підготовки — у <a data-href="Preparation-Guide" href="exam-prep/preparation-guide.html" class="internal-link" target="_self" rel="noopener nofollow">Preparation-Guide</a>.Кожен запис нижче — це стислий кістяк відповіді: достатньо, щоб зорієнтуватись, але не заміна лекції. Переходьте за посиланнями «Докладніше», щоб поглибитись. Рядки «Зверніть увагу на» позначають пастки, на які цілять додаткові питання екзаменатора.Ланцюг передач керування. Увімкнення живлення → вектор скидання ЦП → мікропрограма BIOS/UEFI → POST → MBR (або завантажувач з ESP) → завантажувач другого рівня → ядро ОС → простір користувача. Кожна стадія можливостями перевершує попередню і залежить від того, що попередня виконала свою роботу: BIOS дає доступ до обладнання, MBR — доступ до диска, завантажувач — доступ до файлової системи, ядро — все інше.Вектор скидання та адреси завантаження MBR. ЦП x86 апаратно запрограмований починати виконання з вектора скидання — це кремній, а не регістр. На 80386+ це фізична адреса 0xFFFFFFF0 (16 байтів нижче межі 4 ГБ); на 8086 це було 0xFFFF0. Ці 16 байтів містять далекий JMP у власне BIOS, відображений із флеш-ROM чипсетом. BIOS виконує POST, проходить списком порядку завантаження, зчитує перший 512-байтний сектор кожного кандидата й завантажує перший завантажувальний із них за фізичною адресою 0x7C00 — це рішення IBM PC 1981 року: 1 КБ нижче позначки 32 КБ, що залишає місце внизу для IVT та області даних BIOS, а вгорі — для стеку.Розмір і формат MBR. Рівно 512 байтів: 446 байтів коду завантажувача, 64 байти таблиці розділів (чотири 16-байтні записи) та 2-байтна сигнатура 0x55 0xAA за зміщенням 0x1FE. Без цього магічного числа BIOS пропускає диск. Ці 446 байтів мають розібрати таблицю, знайти активний розділ (індикатор завантаження 0x80) та ланцюгово завантажити його завантажувальний запис тому.Архітектурні «якірні» адреси. До того як ОС перебирає керування: вектор скидання 0xFFFFFFF0; завантаження MBR 0x7C00; IVT за адресами 0000:0000–0000:03FF (1 КБ, 256 × 4-байтні далекі вказівники); ROM BIOS у сегменті F000h; буфер відео текстового режиму за B800:0000.UEFI. Замінює застарілий BIOS. Зчитує завантажувач безпосередньо з системного розділу EFI (відформатованого як FAT) — без обмеження у 446 байтів. Підтримує розмітку GPT (диски &gt; 2 ТБ; застарілий MBR обмежений 2 ТіБ через 32-бітну адресацію секторів), працює у 32/64-бітному режимі від самого скидання та вмикає Secure Boot (криптографічну перевірку). Концептуальна роль та сама.Перехід реальний → захищений режим. Усередині завантажувача або раннього ядра: побудувати GDT, LGDT для її завантаження, встановити CR0.PE = 1, потім далекий JMP, щоб скинути чергу попередньої вибірки й почати вибирати інструкції захищеного режиму. GRUB обходить обмеження у 446 байтів, зберігаючи «стадію 1.5» у проміжку після MBR.Зверніть увагу на: вектор скидання зафіксовано кремнієм (не регістром, не CMOS); 0x7C00 — це історичний артефакт 1981 року, а не технічна вимога; 0x55AA у MBR знаходиться за зміщенням 0x1FE–0x1FF, а не на початку; UEFI усуває обмеження у 512 байтів, але концептуальна роль незмінна.<br>Докладніше: <a data-tooltip-position="top" aria-label="L07. Переривання та взаємодія з ОС > 2.2 Вектор скидання: початкова точка ЦП" data-href="L07. Переривання та взаємодія з ОС#2.2 Вектор скидання: початкова точка ЦП" href="lectures/l07.-переривання-та-взаємодія-з-ос.html#2.2 Вектор скидання: початкова точка ЦП" class="internal-link" target="_self" rel="noopener nofollow">§2.2 Вектор скидання</a>, <a data-tooltip-position="top" aria-label="L07. Переривання та взаємодія з ОС > 2.3 BIOS: основа мікропрограмного забезпечення" data-href="L07. Переривання та взаємодія з ОС#2.3 BIOS: основа мікропрограмного забезпечення" href="lectures/l07.-переривання-та-взаємодія-з-ос.html#2.3 BIOS: основа мікропрограмного забезпечення" class="internal-link" target="_self" rel="noopener nofollow">§2.3 BIOS та UEFI</a>, <a data-tooltip-position="top" aria-label="L07. Переривання та взаємодія з ОС > 3.2 Головний завантажувальний запис (MBR)" data-href="L07. Переривання та взаємодія з ОС#3.2 Головний завантажувальний запис (MBR)" href="lectures/l07.-переривання-та-взаємодія-з-ос.html#3.2 Головний завантажувальний запис (MBR)" class="internal-link" target="_self" rel="noopener nofollow">§3.2 MBR</a>, <a data-tooltip-position="top" aria-label="L07. Переривання та взаємодія з ОС > 3.3 Завантажувач другого рівня" data-href="L07. Переривання та взаємодія з ОС#3.3 Завантажувач другого рівня" href="lectures/l07.-переривання-та-взаємодія-з-ос.html#3.3 Завантажувач другого рівня" class="internal-link" target="_self" rel="noopener nofollow">§3.3 Завантажувач другого рівня</a>.Два джерела, один механізм. Переривання призупиняє поточний потік інструкцій, зберігає стан і диспетчеризує через IVT до обробника (ISR). Щойно номер визначено, ЦП обробляє обидва типи ідентично — обробник не може визначити, що саме його спричинило.Що INT n заштовхує. Три слова в такому порядку: FLAGS, CS, IP. Потім ЦП очищує IF і TF (обробник виконується з замаскованими апаратними IRQ, доки сам не вирішить інакше), зчитує 4-байтний вектор за фізичною адресою n × 4 (little-endian: зміщення-молодший, зміщення-старший, сегмент-молодший, сегмент-старший), завантажує CS:IP і продовжує виконання в обробнику. IRET витягує у зворотному порядку — IP, CS, FLAGS — відновлюючи IF у значення, що було до переривання.Чому вони не взаємозамінні. Переривання надходить асинхронно, коли викликач перебуває у довільному стані прапорців; обробник має змінити FLAGS, не зіпсувавши стан викликача. Тому FLAGS слід зберегти/відновити, а RET їх не витягує. До того ж INT очищує IF на вході, щоб обробник мав неперервний час на налаштування.Служби DOS/BIOS. INT 21h AH=09h (вивести рядок), INT 21h AH=4Ch (завершити), INT 10h AH=0Eh (телетайпний вивід), INT 16h AH=00h (прочитати клавішу).CLI та NMI. Якщо IRQ спрацьовує при IF = 0, PIC утримує його, доки не буде виконано STI. NMI (помилка парності пам'яті, сторожовий таймер) ігнорує IF і спрацьовує в будь-якому разі — для умов настільки критичних, що маскування було б катастрофою.Зверніть увагу на: порядок збереження — FLAGS → CS → IP (IP опиняється на вершині); адреса запису IVT = n × 4; CLI не маскує NMI; IRET вмикає переривання повторно лише тому, що збережений FLAGS мав IF = 1.<br>Докладніше: <a data-tooltip-position="top" aria-label="L07. Переривання та взаємодія з ОС > Два типи переривань" data-href="L07. Переривання та взаємодія з ОС#Два типи переривань" href="lectures/l07.-переривання-та-взаємодія-з-ос.html#Два типи переривань" class="internal-link" target="_self" rel="noopener nofollow">§6 Два типи переривань</a>, <a data-tooltip-position="top" aria-label="L07. Переривання та взаємодія з ОС > Інструкція INT у деталях" data-href="L07. Переривання та взаємодія з ОС#Інструкція INT у деталях" href="lectures/l07.-переривання-та-взаємодія-з-ос.html#Інструкція INT у деталях" class="internal-link" target="_self" rel="noopener nofollow">§6 INT у деталях</a>, <a data-tooltip-position="top" aria-label="L07. Переривання та взаємодія з ОС > Порівняння INT з CALL" data-href="L07. Переривання та взаємодія з ОС#Порівняння INT з CALL" href="lectures/l07.-переривання-та-взаємодія-з-ос.html#Порівняння INT з CALL" class="internal-link" target="_self" rel="noopener nofollow">§10 Порівняння INT з CALL</a>, <a data-tooltip-position="top" aria-label="L07. Переривання та взаємодія з ОС > Інструкція IRET: повернення з переривання" data-href="L07. Переривання та взаємодія з ОС#Інструкція IRET: повернення з переривання" href="lectures/l07.-переривання-та-взаємодія-з-ос.html#Інструкція IRET: повернення з переривання" class="internal-link" target="_self" rel="noopener nofollow">§10 IRET</a>.Чотири кільця. Захищений режим x86 визначає рівні привілеїв 0–3, закодовані у двох молодших бітах CS (Current Privilege Level, CPL). Кільце 0 має повний доступ до всіх інструкцій, пам'яті та вводу-виводу; кільце 3 — найбільш обмежене. Кожне звернення до пам'яті перевіряється за DPL сегмента та бітом U/S сторінки; кожна інструкція — за CPL.Чому лише два кільця. Помилковий драйвер у кільці 1 все одно дістається до критичного обладнання, тож проміжні кільця давали мало реальної безпеки. Кільце −1 = режим VMX root — запроваджений Intel VT-x / AMD-V, щоб гіпервізори (KVM, VMware) могли виконувати гостьові ядра, які вважають, що володіють кільцем 0, тоді як гіпервізор зберігає справжній контроль.<br>Системний виклик від початку до кінця. Програма кільця 3 не може виконати CALL до коду ядра — ЦП згенерує <a href=".?query=tag:GP" class="tag is-unresolved" target="_self" rel="noopener nofollow" data-href="#GP">#GP</a>. Натомість:
Користувач готує аргументи (Linux 32-біт: номер системного виклику в EAX, аргументи в EBX/ECX/EDX/ESI/EDI/EBP).
Виконує INT 80h (або SYSCALL на x86-64).
ЦП завантажує вказівник стеку ядра з TSS (стеку користувача довіряти не можна) і заштовхує SS, ESP, EFLAGS, CS, EIP.
ЦП завантажує CS:EIP із шлюзу IDT (або з MSR для SYSCALL) — користувач не може обрати, куди перейти.
Ядро перевіряє аргументи, повертає результат у EAX/RAX.
IRET (або SYSRET) відновлює стан, повертає керування до кільця 3.
INT 80h vs SYSCALL. SYSCALL використовує виділені MSR для попереднього визначення точки входу в ядро, уникаючи пошуку в IDT, якого вимагає INT. Саме тому сучасні ОС застосовують його замість INT 80h для системних викликів — апаратний шлях коротший.Чому системний виклик усе одно передбачає обов'язкове перемикання стану ЦП. Навіть на швидкому шляху SYSCALL системний виклик — це не звичайний виклик функції: ЦП має змінити рівень привілеїв (кільце 3 → кільце 0), перемкнутися на стек ядра (стеку користувача довіряти не можна) та зберегти/відновити контекст повернення користувача, щоб виконання могло коректно продовжитись опісля. Цей апаратно опосередкований перехід — саме те, що робить операцію контрольованою та аудитованою, і він суттєвий у порівнянні з CALL у межах одного кільця.Зверніть увагу на: CPL — це два молодші біти CS; швидкість SYSCALL походить з пропуску пошуку в IDT; Linux/Windows використовують лише кільце 0 і кільце 3; кільце −1 — це розширення віртуалізації; код користувача ніколи не обирає адресу в ядрі — лише попередньо зареєстровані шлюзи IDT або точки входу, визначені в MSR.<br>Докладніше: <a data-tooltip-position="top" aria-label="L07. Переривання та взаємодія з ОС > 13.3 Кільця захисту" data-href="L07. Переривання та взаємодія з ОС#13.3 Кільця захисту" href="lectures/l07.-переривання-та-взаємодія-з-ос.html#13.3 Кільця захисту" class="internal-link" target="_self" rel="noopener nofollow">§13.3 Кільця захисту</a>, <a data-tooltip-position="top" aria-label="L07. Переривання та взаємодія з ОС > 13.4 Перехід між кільцями: шлюзи виклику та переривання" data-href="L07. Переривання та взаємодія з ОС#13.4 Перехід між кільцями: шлюзи виклику та переривання" href="lectures/l07.-переривання-та-взаємодія-з-ос.html#13.4 Перехід між кільцями: шлюзи виклику та переривання" class="internal-link" target="_self" rel="noopener nofollow">§13.4 Переходи між кільцями</a>, <a data-tooltip-position="top" aria-label="L07. Переривання та взаємодія з ОС > 13.5 Перехід між кільцями в контексті" data-href="L07. Переривання та взаємодія з ОС#13.5 Перехід між кільцями в контексті" href="lectures/l07.-переривання-та-взаємодія-з-ос.html#13.5 Перехід між кільцями в контексті" class="internal-link" target="_self" rel="noopener nofollow">§13.5 Проходження системного виклику</a>, <a data-tooltip-position="top" aria-label="L07. Переривання та взаємодія з ОС > Сучасний Linux: SYSCALL і SYSENTER" data-href="L07. Переривання та взаємодія з ОС#Сучасний Linux: SYSCALL і SYSENTER" href="lectures/l07.-переривання-та-взаємодія-з-ос.html#Сучасний Linux: SYSCALL і SYSENTER" class="internal-link" target="_self" rel="noopener nofollow">§9 SYSCALL vs INT 80h</a>.<br>Блок I · L07 · 3 питання · Частина <a data-href="Exam-Questions" href="exam-questions.html" class="internal-link" target="_self" rel="noopener nofollow">Exam-Questions</a>]]></description><link>exam-prep/l07-prep.html</link><guid isPermaLink="false">Exam-Prep/L07-Prep.md</guid><pubDate>Wed, 22 Apr 2026 04:44:10 GMT</pubDate></item><item><title><![CDATA[L08-Prep]]></title><description><![CDATA[Охоплює екзаменаційні питання II.1 – II.3. Офіційне формулювання — у <a data-href="Exam-Questions" href="exam-questions.html" class="internal-link" target="_self" rel="noopener nofollow">Exam-Questions</a>; повний обсяг підготовки — у <a data-href="Preparation-Guide" href="exam-prep/preparation-guide.html" class="internal-link" target="_self" rel="noopener nofollow">Preparation-Guide</a>.Кожен запис нижче — це стислий кістяк відповіді: достатньо, щоб зорієнтуватись, але не заміна лекції. Переходьте за посиланнями «Докладніше», щоб поглибитись. Рядки «Зверніть увагу на» позначають пастки, на які цілять додаткові питання екзаменатора.Порівняння за п'ятьма осями.Відображення на системні ролі. SRAM → регістри ЦП і кеші L1/L2/L3 (на кристалі, потрібна швидкість, обсяг достатньо малий, щоб дозволити 6-транзисторну комірку). DRAM → DIMM-модулі основної пам'яті (потрібна ємність у гігабайтному масштабі, щільність 1T1C виграє). Флеш → SSD, USB-накопичувачі, SD-картки, сховище смартфонів, мікросхема BIOS Flash (енергонезалежна, але перезаписувана). Маскова ROM → завантажувальний мікрокод, незмінні таблиці мікропрограм, картриджі ігрових консолей — скрізь, де вміст постачається разом із кремнієм і ніколи не змінюється.Асиметрія читання/запису у флеш. Читання — побайтове й швидке. Записи мають обмеження стирання перед записом: під час програмування комірка може лише перемикатись 1 → 0; щоб перемкнути 0 → 1, треба стерти, а стирання працює на рівні цілого блоку (зазвичай від 128 КБ до кількох МБ). Фізична причина: стирання прикладає високу напругу до великої області, щоб одночасно прибрати електрони з усіх плаваючих затворів тунелюванням — це групова, а не покоміркова операція. Наслідок: один 4-байтний запис може змусити контролер зчитати весь блок, стерти його й перезаписати все. Flash Translation Layer (FTL) у мікропрограмі SSD ховає це від ОС, підтримуючи відображення логічних блоків у фізичні та виконуючи записи в інше місце (out-of-place).Витривалість і її пом'якшення. Кожен цикл програмування/стирання трохи пошкоджує тунельний оксид; після ~100 000 циклів (SLC) і аж до ~1 000 (QLC) комірки перестають надійно утримувати заряд. Пом'якшення — вирівнювання зношування: FTL рівномірно розподіляє записи по всіх фізичних блоках, щоб жоден окремий блок не перезаписувався непропорційно часто. У поєднанні з надлишковим забезпеченням (запасні блоки, що заміщують зношені) та кодами виправлення помилок споживчий TLC SSD зазвичай витримує роки звичайного використання, попри обмеження в тисячі циклів на комірку.Зверніть увагу на: чому маскова ROM досі випускається, хоча є флеш (нульова вартість програмування на великих тиражах, несприйнятливість до пошкоджень у полі, найменша комірка); що намагалися заповнити 3D XPoint / Optane (енергонезалежний прошарок поруч з DRAM із затримкою ~3× DRAM) і чому він провалився комерційно (надто дорогий, без чіткої програмної моделі); різницю між побайтово стираною EEPROM та поблочно стираною NAND-флеш.<br>Докладніше: <a data-tooltip-position="top" aria-label="L08. Пристрої. Ієрархія пам'яті. Структури даних на низькому рівні > 10.1 За енергозалежністю" data-href="L08. Пристрої. Ієрархія пам'яті. Структури даних на низькому рівні#10.1 За енергозалежністю" href="lectures/l08.-пристрої.-ієрархія-пам'яті.-структури-даних-на-низькому-рівні.html#10.1 За енергозалежністю" class="internal-link" target="_self" rel="noopener nofollow">§10.1 Енергозалежність</a>, <a data-tooltip-position="top" aria-label="L08. Пристрої. Ієрархія пам'яті. Структури даних на низькому рівні > 10.3 За можливістю читання/запису" data-href="L08. Пристрої. Ієрархія пам'яті. Структури даних на низькому рівні#10.3 За можливістю читання/запису" href="lectures/l08.-пристрої.-ієрархія-пам'яті.-структури-даних-на-низькому-рівні.html#10.3 За можливістю читання/запису" class="internal-link" target="_self" rel="noopener nofollow">§10.3 Можливість читання/запису та витривалість</a>, <a data-tooltip-position="top" aria-label="L08. Пристрої. Ієрархія пам'яті. Структури даних на низькому рівні > 10.7 Підсумкова порівняльна таблиця" data-href="L08. Пристрої. Ієрархія пам'яті. Структури даних на низькому рівні#10.7 Підсумкова порівняльна таблиця" href="lectures/l08.-пристрої.-ієрархія-пам'яті.-структури-даних-на-низькому-рівні.html#10.7 Підсумкова порівняльна таблиця" class="internal-link" target="_self" rel="noopener nofollow">§10.7 Підсумкова таблиця</a>.Розкладка буфера кадру. У текстовому режимі VGA відеоадаптер відображає свій символьний буфер на сегмент B800h, зміщення 0000h — фізична адреса 0xB8000 (сегмент × 16 + зміщення). Стандартний режим — 80 стовпців × 25 рядків, і кожна комірка займає 2 байти: байт 0 — код символу (кодова сторінка IBM 437), байт 1 — атрибутивний байт (колір + мерехтіння). Загальний розмір буфера: 80 × 25 × 2 = 4000 байтів. Оскільки оперативної пам'яті в адаптера більше, послідовно від B800:0000 поміщається до 8 текстових сторінок.Формула байтового зміщення. Для комірки у (рядку r, стовпці c):зміщення = (r × 80 + c) × 2 — байт символу
зміщення + 1 — атрибутивний байтПриклад: рядок 10, стовпець 20 → зміщення (10·80 + 20)·2 = 1640 = 0x668; символ за B800:0668, атрибут за B800:0669.Атрибутивний байт (розкладка за бітами). Розкладку атрибутивного байта ввели в L07 §8 (служби відео BIOS через INT 10h); перегляньте ті нотатки для побітового розбору.Отже, зазвичай доступні лише 16 кольорів переднього плану × 8 кольорів фону = 128 поєднань; обмеження «16 кольорів» — це артефакт 4-бітного поля IRGB рівня ISA, а не апаратне обмеження VGA.Мінімальний фрагмент на x86 — розмістити символ 'A' (41h) з атрибутом 1Fh (білий на синьому) у рядку 10, стовпці 20:mov ax, 0B800h
mov es, ax ; ES → відеосегмент
mov di, (10*80 + 20) * 2 ; DI = байтове зміщення для (рядок, стовпець)
mov al, 'A' ; символ
mov ah, 1Fh ; атрибут: синій фон (1), білий передній план (F)
mov es:[di], ax ; один запис слова = символ + атрибут
Один MOV слова записує обидва байти атомарно; відеоадаптер миттєво фіксує нову комірку, і наступне скан-виведення її показує.Порівняння буфера кадру з BIOS. INT 10h AH=09h (записати символ з атрибутом) витрачає сотні-тисячі тактів на символ — заштовхує прапорці, диспетчеризується через IVT, декодує AH, обчислює адресу, записує та повертається. Прямий MOV ES:[DI], AX — це один цикл шини, ~1 інструкція. На повноекранні оновлення прямий шлях у 100–1000 разів швидший; це єдиний практичний спосіб анімувати графіку текстового режиму.Зверніть увагу на: переносимість — це припускає реальний режим з VGA за B8000h, чого сучасні ОС користувацьким процесам не розкривають (захищений режим + графічний буфер кадру); байт символу використовує кодову сторінку 437, не ASCII (знаки псевдографіки живуть за B0h–DFh); як це відображається на сучасні GPU (буфер кадру став DMA-відображеною областю у просторі PCIe BAR, у яку пише композитор, а не застосунки напряму); чому палітра — лише 16 кольорів (на комірку ми бюджетуємо лише біти 0–3; компроміс — пам'ять на комірку проти глибини кольору).<br>Докладніше: <a data-tooltip-position="top" aria-label="L08. Пристрої. Ієрархія пам'яті. Структури даних на низькому рівні > 2.2 Введення-виведення через пам'ять vs. портове введення-виведення" data-href="L08. Пристрої. Ієрархія пам'яті. Структури даних на низькому рівні#2.2 Введення-виведення через пам'ять vs. портове введення-виведення" href="lectures/l08.-пристрої.-ієрархія-пам'яті.-структури-даних-на-низькому-рівні.html#2.2 Введення-виведення через пам'ять vs. портове введення-виведення" class="internal-link" target="_self" rel="noopener nofollow">§2.2 Введення-виведення через пам'ять vs портове</a>, <a data-tooltip-position="top" aria-label="L08. Пристрої. Ієрархія пам'яті. Структури даних на низькому рівні > Два підходи до комунікації з пристроями" data-href="L08. Пристрої. Ієрархія пам'яті. Структури даних на низькому рівні#Два підходи до комунікації з пристроями" href="lectures/l08.-пристрої.-ієрархія-пам'яті.-структури-даних-на-низькому-рівні.html#Два підходи до комунікації з пристроями" class="internal-link" target="_self" rel="noopener nofollow">§4 Два підходи до комунікації з пристроями</a>.Дві моделі. Обидві дозволяють ЦП дістатися регістра пристрою, але різняться тим, у якому адресному просторі цей регістр живе та які інструкції до нього дотягуються.Інструкції та адреси. PMIO використовує виділені інструкції IN та OUT (плюс рядкові форми INS/OUTS). Номер порту — це 1-байтова безпосередня константа (IN AL, 60h покриває порти 0–255) або регістр DX для повного 16-бітного діапазону. MMIO використовує звичайні інструкції пам'яті — MOV, ADD, CMP, TEST, навіть REP STOSW — націлені на адресу у звичайній карті пам'яті. ЦП сигналізує, який саме адресний простір задіяно, на виділеній лінії керування (M/IO на 8086 або її еквівалент у пізніших чипах), тож порт 60h у просторі введення-виведення та байт пам'яті 0060h — це цілком різні місця, попри те що вони ділять ті самі біти адресної шини.Чому MMIO масштабується краще. Вікно PMIO у 64 КБ було щедрим 1981 року, коли пристрій виставляв жменю байтових регістрів. Сьогодні це сміховинно мало. Сучасний GPU має від сотень МБ до кількох ГБ відеопам'яті плюс тисячі регістрів керування/стану, відображених у просторі BAR. Сучасна мережева карта (10 GbE і швидше) виставляє кільця дескрипторів пакетів, регістри-«дзвоники» (doorbells) та лічильники статистики, що разом займають мегабайти. Нічого з цього в 64 КБ портового простору не поміститься — ані один GPU, не кажучи вже про систему з кількома високопропускними пристроями. MMIO просто виділяє PCIe BAR потрібного пристрою розміру (64 КБ, 1 МБ, 256 МБ, 4 ГБ — скільки треба) і відображає його в 48-бітний фізичний адресний простір ЦП. Бюджет адресного простору фактично необмежений порівняно з PMIO.Чому x86 усе ж зберігає PMIO. Зворотна сумісність. PMIO успадкований від 8080/8085 і закріплений в IBM PC 1981 року: клавіатура, таймер, PIC, CMOS і старий механізм конфігурації — усі живуть за фіксованими низькими портами (20h, 40h, 60h, 70h). Сорок з гаком років мікропрограм BIOS, драйверів DOS і завантажувачів покладаються на IN/OUT саме за цими адресами. Нові пристрої (PCIe, NVMe, GPU) використовують MMIO — але застарілі порти не можна перенести, не зламавши весь ланцюжок завантаження PC. ARM, MIPS та RISC-V цієї історії уникнули й використовують винятково MMIO.Історичні та сучасні якорі. PMIO: PIC (20h), PIT (40h), контролер клавіатури (60h/64h), CMOS (70h/71h), COM1 (3F8h) — усе жменями байтів, жорстко закріпленими специфікацією IBM PC/AT 1981 року. MMIO: текстовий буфер кадру VGA за 0xB8000, графіка VGA за 0xA0000, а також кожен сучасний високопропускний пристрій (PCIe GPU, NVMe SSD, мережеві карти 10/40/100 GbE), що виставляє свої регістри через PCIe BAR у високій фізичній пам'яті.Зверніть увагу на: адресний простір PMIO — 64 КБ, крапка — запитайте себе вголос, скільки регістрів GPU поміститься в цей бюджет (відповідь: фактично жоден з регістрів сучасної карти); чому x86 усе ж зберігає PMIO, коли MMIO суворо гнучкіший (застаріла мікропрограма, BIOS та завантажувачі жорстко зашивають IN/OUT на фіксовані низькі порти); один конкретний високопропускний пристрій і чому йому потрібен MMIO (наприклад, GPU з 8 ГБ відеопам'яті — сам його буфер кадру затьмарює весь простір PMIO у 130 000 разів).<br>Докладніше: <a data-tooltip-position="top" aria-label="L08. Пристрої. Ієрархія пам'яті. Структури даних на низькому рівні > 2.2 Введення-виведення через пам'ять vs. портове введення-виведення" data-href="L08. Пристрої. Ієрархія пам'яті. Структури даних на низькому рівні#2.2 Введення-виведення через пам'ять vs. портове введення-виведення" href="lectures/l08.-пристрої.-ієрархія-пам'яті.-структури-даних-на-низькому-рівні.html#2.2 Введення-виведення через пам'ять vs. портове введення-виведення" class="internal-link" target="_self" rel="noopener nofollow">§2.2 MMIO vs PMIO</a>, <a data-tooltip-position="top" aria-label="L08. Пристрої. Ієрархія пам'яті. Структури даних на низькому рівні > Два підходи до комунікації з пристроями" data-href="L08. Пристрої. Ієрархія пам'яті. Структури даних на низькому рівні#Два підходи до комунікації з пристроями" href="lectures/l08.-пристрої.-ієрархія-пам'яті.-структури-даних-на-низькому-рівні.html#Два підходи до комунікації з пристроями" class="internal-link" target="_self" rel="noopener nofollow">§4 Два підходи до комунікації з пристроями</a>, <a data-tooltip-position="top" aria-label="L08. Пристрої. Ієрархія пам'яті. Структури даних на низькому рівні > Карта портів" data-href="L08. Пристрої. Ієрархія пам'яті. Структури даних на низькому рівні#Карта портів" href="lectures/l08.-пристрої.-ієрархія-пам'яті.-структури-даних-на-низькому-рівні.html#Карта портів" class="internal-link" target="_self" rel="noopener nofollow">§6 Карта портів</a>.<br>Блок II · L08 · 3 питання · Частина <a data-href="Exam-Questions" href="exam-questions.html" class="internal-link" target="_self" rel="noopener nofollow">Exam-Questions</a>]]></description><link>exam-prep/l08-prep.html</link><guid isPermaLink="false">Exam-Prep/L08-Prep.md</guid><pubDate>Wed, 22 Apr 2026 04:44:10 GMT</pubDate></item><item><title><![CDATA[L09-Prep]]></title><description><![CDATA[Охоплює екзаменаційні питання II.4 – II.6. Офіційне формулювання — у <a data-href="Exam-Questions" href="exam-questions.html" class="internal-link" target="_self" rel="noopener nofollow">Exam-Questions</a>; повний обсяг підготовки — у <a data-href="Preparation-Guide" href="exam-prep/preparation-guide.html" class="internal-link" target="_self" rel="noopener nofollow">Preparation-Guide</a>.Кожен запис нижче — це стислий кістяк відповіді: достатньо, щоб зорієнтуватись, але не заміна лекції. Переходьте за посиланнями «Докладніше», щоб поглибитись. Рядки «Зверніть увагу на» позначають пастки, на які цілять додаткові питання екзаменатора.Обидві моделі. РАМ-машина (Кук–Реков, 1973) має необмежений масив регістрів r1, r2, ..., що містять натуральні числа довільного розміру, акумулятор (r1), пряму та непряму адресацію (LOAD 5 проти LOAD *5), а програму — в окремій таблиці інструкцій, доступній лише для читання. Програма та дані ніколи не змішуються — це абстрактна машина Гарвардського типу. РАСП-машина (Random Access Stored-Program, машина зі збереженою програмою з довільним доступом) є ідентичною, за винятком того, що програма закодована як числа в тих самих регістрах, що й дані. Зашитий інтерпретатор зі скінченним числом станів виконує вибірку → декодування → виконання над цими числами. Це модель фон Неймана.Яка з них моделює реальні комп'ютери. РАСП. Кожен процесор загального призначення (x86, ARM, RISC-V) завантажує код із тієї самої DRAM, де містяться його дані, — саме тому завантажувач зчитує виконуваний файл (дані) і потім передає йому керування (інструкції), саме тому CALL заштовхує адресу повернення на стек (пам'ять даних, що містить адресу інструкції), і саме тому JIT-компілятори можуть генерувати машинний код під час виконання. Поведінка чистої РАМ-машини зберігається в мікроконтролерах з окремою флеш-пам'яттю для коду та SRAM для даних (AVR, багато DSP), де процесор буквально не може виконувати код з пам'яті даних.Наслідок РАСП для безпеки. Якщо дані поділяють простір з кодом, погані вхідні дані можуть перезаписати код. Це зародок ін'єкції коду через переповнення буфера — переповнити буфер у стеку, записати байти зловмисника, перенаправити RET на них. РАМ-машини до цього несприйнятливі за побудовою.Псевдо-РАМ на рівні ОС. Біт NX (Intel «XD», AMD «NX»), встановлюваний у запису таблиці сторінок, позначає сторінки як невиконувані — вибірка зі сторінки NX спричиняє помилку. Ін'єкований шелкод у буфері даних більше не виконується. Апаратура залишається фон-нейманівською; ОС просто відмовляється вибирати код із записуваних сторінок, забезпечуючи посторінкову Гарвардську дисципліну. Зупиняє класичну ін'єкцію; не зупиняє ROP, який повторно використовує байти, що вже є виконуваними.Використання для продуктивності. JIT-компіляція (JVM HotSpot, V8, .NET CLR, LuaJIT) записує щойно згенерований машинний код у ділянку даних, а потім переходить на нього. У системах з NX це вимагає mprotect/VirtualProtect, щоб перемкнути сторінку з W на X (W⊕X — ніколи одночасно обидва). Динамічні завантажувачі та «живе» латання ядра експлуатують ту саму гнучкість.Зверніть увагу на: сучасний x86 має розділені кеші L1-I / L1-D (модифікована Гарвардська архітектура) над уніфікованою основною пам'яттю фон Неймана, і саме когерентність icache–dcache робить JIT коректним; NX діє на рівні сторінок, не ISA; чисто Гарвардський AVR — це РАМ-машина, але завантажувач, що записує у флеш, зсуває його в бік РАСП.<br>Докладніше: <a data-tooltip-position="top" aria-label="L09. Проєктування ISA та моделі обчислень > 2.6 РАСП-машина: збереження програми в пам'яті" data-href="L09. Проєктування ISA та моделі обчислень#2.6 РАСП-машина: збереження програми в пам'яті" href="lectures/l09.-проєктування-isa-та-моделі-обчислень.html#2.6 РАСП-машина: збереження програми в пам'яті" class="internal-link" target="_self" rel="noopener nofollow">§2.6 РАСП</a>, <a data-tooltip-position="top" aria-label="L09. Проєктування ISA та моделі обчислень > 3.7 РАМ-машина як Гарвардська архітектура" data-href="L09. Проєктування ISA та моделі обчислень#3.7 РАМ-машина як Гарвардська архітектура" href="lectures/l09.-проєктування-isa-та-моделі-обчислень.html#3.7 РАМ-машина як Гарвардська архітектура" class="internal-link" target="_self" rel="noopener nofollow">§3.7 РАМ як Гарвардська</a>, <a data-tooltip-position="top" aria-label="L09. Проєктування ISA та моделі обчислень > 4.6 Наслідки зберігання програми в пам'яті" data-href="L09. Проєктування ISA та моделі обчислень#4.6 Наслідки зберігання програми в пам'яті" href="lectures/l09.-проєктування-isa-та-моделі-обчислень.html#4.6 Наслідки зберігання програми в пам'яті" class="internal-link" target="_self" rel="noopener nofollow">§4.6 Наслідки</a>, <a data-tooltip-position="top" aria-label="L09. Проєктування ISA та моделі обчислень > 4.8 РАМ проти РАСП: порівняння" data-href="L09. Проєктування ISA та моделі обчислень#4.8 РАМ проти РАСП: порівняння" href="lectures/l09.-проєктування-isa-та-моделі-обчислень.html#4.8 РАМ проти РАСП: порівняння" class="internal-link" target="_self" rel="noopener nofollow">§4.8 Таблиця РАМ vs РАСП</a>.Дві філософії. CISC (Complex ISC) виник, коли програмісти писали асемблер вручну, а пам'яті було обмаль: вкласти складність в апаратуру, щоб одна інструкція робила більше (REP MOVSB копіює блок, ENTER/LEAVE будують і руйнують стекові фрейми, LOOP зменшує CX і переходить одним опкодом). RISC (Reduced ISC, Паттерсон, Берклі, 1980) перевертає ставку: якщо код генерує компілятор, робіть кожну інструкцію простою, однорідною та однотактовою; витрачайте зекономлений кремній на регістри, кеші та конвеєр.Сучасний x86 розмиває межу через µop'и. Починаючи з Pentium Pro (1995) та AMD K5 (1996), x86 зберігає інтерфейс CISC, але внутрішньо декодує кожну інструкцію в одну або кілька RISC-подібних мікрооперацій (µops) — простих, фіксованої ширини, регістр-регістр. ADD [mem], R3 перетворюється на три µops: завантаження, додавання, збереження. Ядро з позачерговим виконанням планує, перейменовує і списує µops. Потік CISC — це формат стиснення для RISC-подібного ядра.Кеш мікрооперацій. Декодування x86 коштує реальної енергії — застарілий декодер має знаходити межі в байтовому потоці змінної довжини. Intel / AMD кешують уже декодовані µop'и в кеші мікрооперацій (~1500–4000 записів), проіндексованому за IP. При влучанні фронтенд обходить застарілий декодер, закриваючи значну частину розриву в енергії фронтенду між x86 та RISC на гарячих циклах.Компроміс. x86 платить кремнієм і ватами, витраченими на декодування. RISC платить приблизно на 30% більшою кількістю вибраних інструкцій, але кожна дешевша в обробці. Вирішально на мобільному, де ARM спалює менше енергії на списану інструкцію в межах бюджету 5 Вт.Apple M-series. ARMv8/ARMv9 — RISC load-store, фіксоване 32-бітне кодування. Серія M класифікується як RISC (ARM ISA) і покладається на широке декодування, зроблене дешевим завдяки інструкціям фіксованої довжини: паралельне виявлення меж тривіальне, тож за цикл можна декодувати більше інструкцій, подаючи більше ILP у бекенд. Спеціалізовані блоки (матричний співпроцесор AMX, Neural Engine) розміщені поряд з RISC-ядром. x86 досі домінує на настільних/серверних комп'ютерах завдяки екосистемі та інерції бінарної сумісності, а не за рахунок переваг ISA.Зверніть увагу на: CISC vs RISC вже не бінарне — сучасний ARM має AES/SHA/SVE, сучасний x86 декодує в RISC-µops; важить мікроархітектура (кеш мікрооперацій, предиктор переходів, ширина декодування); VLIW/Itanium — це третій табір, що програв, бо компілятори не змогли видобути достатньо статичного паралелізму; широке декодування RISC — це уможливлювач ISA, а не мікроархітектурне диво.<br>Докладніше: <a data-tooltip-position="top" aria-label="L09. Проєктування ISA та моделі обчислень > 7.1 CISC — комп'ютер зі складною системою команд" data-href="L09. Проєктування ISA та моделі обчислень#7.1 CISC — комп'ютер зі складною системою команд" href="lectures/l09.-проєктування-isa-та-моделі-обчислень.html#7.1 CISC — комп'ютер зі складною системою команд" class="internal-link" target="_self" rel="noopener nofollow">§7.1 CISC</a>, <a data-tooltip-position="top" aria-label="L09. Проєктування ISA та моделі обчислень > 7.2 RISC — комп'ютер зі скороченою системою команд" data-href="L09. Проєктування ISA та моделі обчислень#7.2 RISC — комп'ютер зі скороченою системою команд" href="lectures/l09.-проєктування-isa-та-моделі-обчислень.html#7.2 RISC — комп'ютер зі скороченою системою команд" class="internal-link" target="_self" rel="noopener nofollow">§7.2 RISC</a>, <a data-tooltip-position="top" aria-label="L09. Проєктування ISA та моделі обчислень > 7.4 Порівняння CISC та RISC" data-href="L09. Проєктування ISA та моделі обчислень#7.4 Порівняння CISC та RISC" href="lectures/l09.-проєктування-isa-та-моделі-обчислень.html#7.4 Порівняння CISC та RISC" class="internal-link" target="_self" rel="noopener nofollow">§7.4 Порівняльна таблиця</a>, <a data-tooltip-position="top" aria-label="L09. Проєктування ISA та моделі обчислень > 7.5 Сучасна реальність: конвергенція CISC та RISC" data-href="L09. Проєктування ISA та моделі обчислень#7.5 Сучасна реальність: конвергенція CISC та RISC" href="lectures/l09.-проєктування-isa-та-моделі-обчислень.html#7.5 Сучасна реальність: конвергенція CISC та RISC" class="internal-link" target="_self" rel="noopener nofollow">§7.5 Конвергенція</a>.Флінн (1966): дві бінарні осі — кількість потоків інструкцій × кількість потоків даних — дають чотири класи.GPU — у межах варпа та між варпами. GPU класифікується на двох рівнях. SIMT (Single Instruction, Multiple Threads) організовує потоки GPU у синхронні групи — на NVIDIA їх називають варпами (warp), на AMD — хвильовими фронтами (wavefront); лекція не наводить конкретної кількості потоків на варп. Усередині такої групи всі смуги (lane) виконують ту саму інструкцію над своїми власними даними — SIMD з регістрами на потік та видимим для програміста виглядом на потік. Між варпами кожен має свій PC і може бути на іншій інструкції — тож GPU є MIMD на рівні варпів, SIMD усередині кожного варпа. Розбіжні гілки всередині варпа серіалізуються між групами смуг (розбіжність гілок), тож однорідні навантаження виграють на GPU; розгалужений код — ні.SMT / Hyper-Threading. HT подає два логічні ЦП на фізичне ядро, кожен зі своїм архітектурним станом (регістри, PC). На рівні потоку це MIMD — два незалежні потоки інструкцій. Але потоки поділяють виконавчі блоки ядра, кеші та TLB, тож SMT видобуває паралелізм з недовикористаних ресурсів (один потік застоюється на промаху кешу, інший заповнює простій). Відрізняйте SMT (спільний бекенд) від багатоядерного MIMD (незалежні бекенди).Архітектури потоку даних та систолічні матриці — між SIMD і MIMD. Систолічна матриця (Кунг–Лейзерсон, 1978; матричний блок TPU Google) — це ґратка процесорних елементів, через яку дані «пульсують», і кожен PE застосовує свою операцію до трансформованого потоку. Сувора відповідність Флінну незручна: MISD вимагає, щоб ті самі дані потрапляли на різні операції, але дані трансформуються на шляху. TPU — це прискорювач потоку даних у MIMD-хості з регулярністю в стилі SIMD — він стоїть між SIMD і MIMD.Сучасні системи поєднують усі категорії. Ноутбук: SIMD усередині AVX-блоку кожного ядра; MIMD між ядрами (SMP); SIMT усередині вбудованого GPU; плюс прискорювач Neural Engine / AMX. Суперкомп'ютер додає MMP (MIMD з розподіленою пам'яттю) через MPI на тисячах вузлів.Зверніть увагу на: SIMT ≠ класичний SIMD — його відрізняють вигляд на потік та серіалізація на розбіжності гілок; SMT є MIMD логічно, але поділяє один бекенд, тож може шкодити продуктивності, коли обидва потоки змагаються за той самий виконавчий порт; SPMD (той самий бінарник, багато MPI-рангів) — це програмний шаблон на MIMD-апаратурі, а не клас Флінна; чистий MISD майже зник комерційно (відмовостійка авіоніка — канонічний живий приклад).<br>Докладніше: <a data-tooltip-position="top" aria-label="L09. Проєктування ISA та моделі обчислень > 8. Таксономія Флінна — класифікація паралельних архітектур" data-href="L09. Проєктування ISA та моделі обчислень#8. Таксономія Флінна — класифікація паралельних архітектур" href="lectures/l09.-проєктування-isa-та-моделі-обчислень.html#8. Таксономія Флінна — класифікація паралельних архітектур" class="internal-link" target="_self" rel="noopener nofollow">§8 Огляд Флінна</a>, <a data-tooltip-position="top" aria-label="L09. Проєктування ISA та моделі обчислень > 9.2 MISD — багато інструкцій, одні дані" data-href="L09. Проєктування ISA та моделі обчислень#9.2 MISD — багато інструкцій, одні дані" href="lectures/l09.-проєктування-isa-та-моделі-обчислень.html#9.2 MISD — багато інструкцій, одні дані" class="internal-link" target="_self" rel="noopener nofollow">§9.2 MISD</a>, <a data-tooltip-position="top" aria-label="L09. Проєктування ISA та моделі обчислень > 10.1 SIMD — одна інструкція, багато даних" data-href="L09. Проєктування ISA та моделі обчислень#10.1 SIMD — одна інструкція, багато даних" href="lectures/l09.-проєктування-isa-та-моделі-обчислень.html#10.1 SIMD — одна інструкція, багато даних" class="internal-link" target="_self" rel="noopener nofollow">§10.1 SIMD та GPU</a>, <a data-tooltip-position="top" aria-label="L09. Проєктування ISA та моделі обчислень > 10.2 MIMD — багато інструкцій, багато даних" data-href="L09. Проєктування ISA та моделі обчислень#10.2 MIMD — багато інструкцій, багато даних" href="lectures/l09.-проєктування-isa-та-моделі-обчислень.html#10.2 MIMD — багато інструкцій, багато даних" class="internal-link" target="_self" rel="noopener nofollow">§10.2 MIMD / SMP / MMP</a>, <a data-tooltip-position="top" aria-label="L09. Проєктування ISA та моделі обчислень > 10.3 Таксономія Флінна — підсумок" data-href="L09. Проєктування ISA та моделі обчислень#10.3 Таксономія Флінна — підсумок" href="lectures/l09.-проєктування-isa-та-моделі-обчислень.html#10.3 Таксономія Флінна — підсумок" class="internal-link" target="_self" rel="noopener nofollow">§10.3 Підсумок</a>.<br>Блок II · L09 · 3 питання · Частина <a data-href="Exam-Questions" href="exam-questions.html" class="internal-link" target="_self" rel="noopener nofollow">Exam-Questions</a>]]></description><link>exam-prep/l09-prep.html</link><guid isPermaLink="false">Exam-Prep/L09-Prep.md</guid><pubDate>Wed, 22 Apr 2026 04:44:10 GMT</pubDate></item><item><title><![CDATA[L10-Prep]]></title><description><![CDATA[Охоплює екзаменаційні питання II.7 – II.9. Офіційне формулювання — у <a data-href="Exam-Questions" href="exam-questions.html" class="internal-link" target="_self" rel="noopener nofollow">Exam-Questions</a>; повний обсяг підготовки — у <a data-href="Preparation-Guide" href="exam-prep/preparation-guide.html" class="internal-link" target="_self" rel="noopener nofollow">Preparation-Guide</a>.Кожен запис нижче — це стислий кістяк відповіді: достатньо, щоб зорієнтуватись, але не заміна лекції. Переходьте за посиланнями «Докладніше», щоб поглибитись. Рядки «Зверніть увагу на» позначають пастки, на які цілять додаткові питання екзаменатора.Розкол у філософії проєктування. CPU — це машина затримки: завершити будь-яке окреме завдання якомога швидше. GPU — це машина пропускної здатності: завершити якомога більше завдань за секунду, навіть якщо кожне триватиме довше. Кожна архітектурна відмінність випливає саме з цього вибору.Кількісне порівняння (high-end частини 2024 року).CPU витрачає ~60–70 % транзисторів на логіку приховування затримки; GPU відводить цю площу під «сирі» АЛП.Де виграє GPU. Щільна математика з паралелізмом даних: множення матриць у МН, згортки, рендеринг, N-тіла, Монте-Карло. Що це уможливлює: (1) тисячі АЛП, яких живить HBM3; (2) безкоштовне перемикання варпів — коли один варп зупиняється через пам'ять, планувальник видає інший із ~64 резидентних варпів на SM, тож АЛП ніколи не простоюють; (3) тензорні ядра, що виконують FMA 4×4 за такт (~30× пропускної здатності ядра CUDA на FP8).Де виграє CPU. Послідовна логіка з великою кількістю розгалужень: ядра ОС, компілятори, бази даних із гонитвою за вказівниками, послідовна частина будь-якого навантаження. Також малі задачі, де круговий прохід через PCIe перевищує саме обчислення. Смуги GPU застоюються тут тому, що: (а) один промах кешу катастрофічний без спекуляції, яка б його приховала; (б) нерегулярний доступ до пам'яті вбиває коалесцентність; (в) розгалужений код спричиняє розбіжність варпів.SIMT vs SIMD. SIMD (AVX-512) пакує 16 чисел з рухомою комою в один регістр; програміст пише векторний код; усі смуги виконують ту саму операцію над сусідніми адресами, без поелементного розгалуження. SIMT (GPU) запускає 32 потоки синхронно в одному варпі, проте кожен потік має власні регістри, власну адресу й власні розгалуження. На if/else усередині варпа апаратура серіалізує виконання: спочатку проходить шлях if із замаскованими потоками гілки else, потім шлях else із замаскованими потоками гілки if. Вкладена розбіжність коштує O(2^глибина) уповільнення; повністю розбіжний варп втрачає 31/32 пропускної здатності. SIMT гнучкіший за SIMD (довільні адреси, розгалуження на рівні потоку), але за цю гнучкість розплачується продуктивністю на розбіжному коді.Зверніть увагу на: інверсію «пропускна здатність проти затримки» (HBM3 дає у ~7–10× більшу пропускну здатність за DDR5, але вищу затримку — GPU толерує її багатопотоковістю, CPU не зміг би); чому розбіжність варпів дорожча за хибне передбачення переходу на CPU (маскування + серіалізація проти одного скидання); чому копіювання малого масиву на GPU повільніше за обчислення його на CPU.<br>Докладніше: <a data-tooltip-position="top" aria-label="L10. Огляд сучасних архітектур > 3.2 Архітектура GPU: масивний паралелізм" data-href="L10. Огляд сучасних архітектур#3.2 Архітектура GPU: масивний паралелізм" href="lectures/l10.-огляд-сучасних-архітектур.html#3.2 Архітектура GPU: масивний паралелізм" class="internal-link" target="_self" rel="noopener nofollow">§3.2 Архітектура GPU</a>, <a data-tooltip-position="top" aria-label="L10. Огляд сучасних архітектур > 3.4 CPU проти GPU: детальне порівняння" data-href="L10. Огляд сучасних архітектур#3.4 CPU проти GPU: детальне порівняння" href="lectures/l10.-огляд-сучасних-архітектур.html#3.4 CPU проти GPU: детальне порівняння" class="internal-link" target="_self" rel="noopener nofollow">§3.4 Таблиця CPU проти GPU</a>, <a data-tooltip-position="top" aria-label="L10. Огляд сучасних архітектур > 3.6 SIMD проти SIMT проти SMT" data-href="L10. Огляд сучасних архітектур#3.6 SIMD проти SIMT проти SMT" href="lectures/l10.-огляд-сучасних-архітектур.html#3.6 SIMD проти SIMT проти SMT" class="internal-link" target="_self" rel="noopener nofollow">§3.6 SIMD проти SIMT проти SMT</a>.Спектр. GPU розміщений посередині компромісу «гнучкість/продуктивність»; FPGA та ASIC — на його крайніх кінцях.Тактові частоти FPGA vs ASIC — звідки розрив? ASIC проводить з'єднання безпосередньо між спеціально розведеними транзисторами. FPGA маршрутизує через програмовану матрицю мультиплексорів та LUT, налаштованих через SRAM; ця тканина займає 60–80 % площі кристала й додає затримку поширення на кожному кроці. Та сама функція → FPGA зазвичай у 2–10 разів повільніша за еквівалентну ASIC (лекція не наводить конкретних діапазонів у МГц/ГГц).Аргумент NRE проти ASIC. Tape-out на передовому технологічному вузлі коштує $10–$50 млн на маски та верифікацію — це одноразові витрати на розробку (NRE), які амортизуються лише обсягом. Правило великого пальця: нижче ~10 000–100 000 одиниць FPGA дешевша за одиницю; вище — виграє ASIC. Стартап, що прототипує новий протокол, використовує FPGA, бо не може поставити $20 млн на те, що перший дизайн буде правильним. Bitcoin перейшов на ASIC, щойно обсяг сягнув мільйонів майнерів — SHA-256 заморожено, і ASIC-майнери видали ~3 000× хешрейту GPU за дрібну частку потужності.Що для якого навантаження.
GPU — паралельна за даними математика зі змінними алгоритмами: навчання МН (моделі змінюються щотижня), наукове моделювання, рендеринг.
FPGA — малі-середні обсяги, критична затримка, гнучкі протоколи: високочастотна торгівля (наносекундна обробка пакетів), прототипування 5G-baseband, програмно-визначене радіо, аерокосмічна техніка з оновленням у полі.
ASIC — масове виробництво фіксованої функції: майнери Bitcoin, baseband-модеми смартфонів, ISP камер, захищені анклави, хешери SHA-256.
TPU — чи це ASIC? Так, архітектурно. Систолічна матриця 256×256, помножувачі INT8/BFloat16/FP8, Unified Buffer, конвеєр активацій — усе це фіксований кремній під одну операцію: множення матриць. Проте він програмований на рівні графа: компілятор МН-фреймворку (TensorFlow/JAX) опускає довільні моделі на фіксований MXU — саме тому той самий TPU «виконує різні МН-моделі». «ASIC» описує кремній; «програмований» описує програмний стек. Це предметно-орієнтована ASIC — жорстка на рівні вентилів, гнучка всередині своєї предметної області.Міграція FPGA → ASIC. Прототип на FPGA → валідація на реальному трафіку → заморозка специфікації → tape-out до ASIC, коли обсяг і стабільність дизайну виправдовують NRE. Стандартний шлях у мережевому кремнії та в ШІ-стартапах.Зверніть увагу на: чому логіка на основі LUT у FPGA не може зрівнятися з тактовою частотою ASIC навіть на тому самому технологічному вузлі; точку перелому за обсягом для NRE; у якому саме сенсі TPU — це «ASIC, що виконує різні МН-моделі» (через компіляцію графів МН-фреймворку на фіксований MXU, а не через перепрограмовний кремній).<br>Докладніше: <a data-tooltip-position="top" aria-label="L10. Огляд сучасних архітектур > 4.5 Переваги та недоліки" data-href="L10. Огляд сучасних архітектур#4.5 Переваги та недоліки" href="lectures/l10.-огляд-сучасних-архітектур.html#4.5 Переваги та недоліки" class="internal-link" target="_self" rel="noopener nofollow">§4.5 Переваги та недоліки ASIC</a>, <a data-tooltip-position="top" aria-label="L10. Огляд сучасних архітектур > 5.5 FPGA проти ASIC" data-href="L10. Огляд сучасних архітектур#5.5 FPGA проти ASIC" href="lectures/l10.-огляд-сучасних-архітектур.html#5.5 FPGA проти ASIC" class="internal-link" target="_self" rel="noopener nofollow">§5.5 FPGA проти ASIC</a>, <a data-tooltip-position="top" aria-label="L10. Огляд сучасних архітектур > 5.7 Спектр гнучкості та продуктивності" data-href="L10. Огляд сучасних архітектур#5.7 Спектр гнучкості та продуктивності" href="lectures/l10.-огляд-сучасних-архітектур.html#5.7 Спектр гнучкості та продуктивності" class="internal-link" target="_self" rel="noopener nofollow">§5.7 Спектр гнучкості та продуктивності</a>, <a data-tooltip-position="top" aria-label="L10. Огляд сучасних архітектур > 7.2 Архітектура Google TPU" data-href="L10. Огляд сучасних архітектур#7.2 Архітектура Google TPU" href="lectures/l10.-огляд-сучасних-архітектур.html#7.2 Архітектура Google TPU" class="internal-link" target="_self" rel="noopener nofollow">§7.2 Архітектура TPU</a>.Кубіт точно. Класичний біт — це 0 або 1. Стан кубіта — це α|0⟩ + β|1⟩, де α, β — комплексні амплітуди ймовірності з |α|² + |β|² = 1. При вимірюванні він колапсує в 0 з імовірністю |α|² або в 1 з імовірністю |β|². Фізично: спін електрона, поляризація фотона, напрямок струму в надпровідній петлі.Суперпозиція. До вимірювання кубіт справді перебуває в комбінації обох станів — це не «ми не знаємо, у якому саме». Експеримент із двома щілинами є фізичною основою: один фотон інтерферує сам із собою.Заплутаність. Два кубіти стають корельованими так, що вимірювання одного миттєво фіксує стан іншого незалежно від відстані. Підтверджено експериментами на тестах Белла. Заплутаність — це обчислювальний ресурс: вона уможливлює алгоритми, які не мають класичного аналогу.НЕ кажіть «пробує всі відповіді одночасно». Суперпозиція кодує 2^N амплітуд, але вимірювання повертає один результат. Завдання алгоритму — так розставити вентилі (Адамара, CNOT, фазові), щоб інтерференція гасила амплітуди неправильних відповідей і підсилювала амплітуду правильної, роблячи той єдиний виміряний результат із переважною ймовірністю правильним.Алгоритми та класи прискорення.Конкретно: факторизація 2 048-бітного ключа RSA класично зайняла б більше часу, ніж вік Всесвіту; алгоритм Shor на великій машині з виправленням помилок робить це за години. Grover на трильйонному записі бази даних: ~10^6 кроків замість 10^12.Фізичні vs логічні кубіти. Похибка однокубітного вентиля сьогодні ~0,1–1 % (проти ~10^-18 на класичному транзисторі). Корисний логічний кубіт вимагає сотень або тисяч фізичних кубітів, загорнутих у квантове виправлення помилок (поверхневі коди). Саме тому машина з 1 000 кубітами сьогодні ще не може запустити алгоритм Shor на криптографічно значущих розмірах ключів: виправлення помилок поглинає більшість фізичних кубітів, а для самого обчислення їх залишається замало. Google Willow (2024) перетнув режим «нижче порогу»: додавання більшої кількості фізичних кубітів зменшило рівень логічної помилки — необхідна передумова масштабованості.У чому квантові комп'ютери НЕ сильні. Обробка тексту, сортування, вебсервінг, будь-що, що вже ефективно розв'язується класично. Квантові вентилі повільніші за класичні транзистори; без структури, що відкриває прискорення (періодичність — для Shor, невпорядкований пошук — для Grover, гамільтоніан — для моделювання), ви будете повільніші за власний ноутбук.Модель вентилів vs квантовий відпал. Модель вентилів (IBM, Google, IonQ) виконує довільні унітарні схеми — універсальна. Квантовий відпал (D-Wave, ~5 000 кубітів) розв'язує лише оптимізацію на моделі QUBO / моделі Ізинга через еволюцію до основного стану — не тюрінг-універсальний, не може виконати алгоритм Shor. Більше кубітів, дешевше, але принципово менш загальний.Квантова перевага (2019). Google Sycamore, 53 надпровідні кубіти, виконали задачу семплювання випадкової схеми за 200 с; класичному суперкомп'ютеру, за їхніми оцінками, це зайняло б 10 000 років. IBM заперечила, що за достатнього дискового простору класична машина впоралася б за дні. Суть: «перевага» була вузьким, спеціально сконструйованим твердженням для штучної задачі, спроєктованої на користь квантового підходу, а не загальною перевагою на корисних навантаженнях.Зверніть увагу на: чому алгоритм Shor ламає RSA-2048, а Grover лише вдвічі зменшує ефективну довжину ключа AES (AES-256 → ефективно 128 біт, усе ще безпечно — достатньо подвоїти ключ); точну форму «квантового паралелізму» (інтерференція, а не «пробує все одночасно»); співвідношення фізичних/логічних кубітів і чому сьогоднішні машини з ~1 000 кубітами ще далеко від криптографічної користі (виправлення помилок з'їдає більшість кубітів); «збери зараз, розшифруй пізніше» як мотивацію для постквантової криптографії вже сьогодні.<br>Докладніше: <a data-tooltip-position="top" aria-label="L10. Огляд сучасних архітектур > 9.1 Квантовий світ" data-href="L10. Огляд сучасних архітектур#9.1 Квантовий світ" href="lectures/l10.-огляд-сучасних-архітектур.html#9.1 Квантовий світ" class="internal-link" target="_self" rel="noopener nofollow">§9.1 Кубіти, суперпозиція, заплутаність</a>, <a data-tooltip-position="top" aria-label="L10. Огляд сучасних архітектур > 9.3 Квантові алгоритми" data-href="L10. Огляд сучасних архітектур#9.3 Квантові алгоритми" href="lectures/l10.-огляд-сучасних-архітектур.html#9.3 Квантові алгоритми" class="internal-link" target="_self" rel="noopener nofollow">§9.3 Shor, Grover та межі</a>, <a data-tooltip-position="top" aria-label="L10. Огляд сучасних архітектур > 9.4 Хронологія квантових обчислень" data-href="L10. Огляд сучасних архітектур#9.4 Хронологія квантових обчислень" href="lectures/l10.-огляд-сучасних-архітектур.html#9.4 Хронологія квантових обчислень" class="internal-link" target="_self" rel="noopener nofollow">§9.4 Перевага та хронологія</a>, <a data-tooltip-position="top" aria-label="L10. Огляд сучасних архітектур > 9.5 Шлях уперед" data-href="L10. Огляд сучасних архітектур#9.5 Шлях уперед" href="lectures/l10.-огляд-сучасних-архітектур.html#9.5 Шлях уперед" class="internal-link" target="_self" rel="noopener nofollow">§9.5 Виправлення помилок і PQC</a>.<br>Блок II · L10 · 3 питання · Частина <a data-href="Exam-Questions" href="exam-questions.html" class="internal-link" target="_self" rel="noopener nofollow">Exam-Questions</a>]]></description><link>exam-prep/l10-prep.html</link><guid isPermaLink="false">Exam-Prep/L10-Prep.md</guid><pubDate>Wed, 22 Apr 2026 04:44:10 GMT</pubDate></item><item><title><![CDATA[L01-Prep]]></title><description><![CDATA[Охоплює екзаменаційні питання I.1 – I.3. Офіційне формулювання — у <a data-href="Exam-Questions" href="exam-questions.html" class="internal-link" target="_self" rel="noopener nofollow">Exam-Questions</a>; повний обсяг підготовки — у <a data-href="Preparation-Guide" href="exam-prep/preparation-guide.html" class="internal-link" target="_self" rel="noopener nofollow">Preparation-Guide</a>.Кожен запис нижче — це стисле ядро відповіді, достатнє для орієнтації, але не замінник лекції. Переходьте за посиланнями «Докладніше», щоб заглибитись у тему. Рядки «Зверніть увагу на» позначають пастки, на які націлені додаткові запитання екзаменатора.Три причини перемоги двійкової системи.1. Завадостійкість. Реальні електричні сигнали ніколи не бувають ідеально чистими — блоки живлення коливаються, проникають завади, тепловий рух тремтить у кожному вузлі. Двійкова схема це витримує, бо їй треба розрізняти лише дві добре розділені смуги: зазвичай 0–1 В читається як «0», а 3–5 В — як «1», а між ними лежить заборонена зона ~2 В, яка поглинає шум. Сигнал може суттєво дрейфувати й усе одно бути правильно інтерпретованим. Порівняйте з гіпотетичною 10-рівневою схемою, що ділить ту саму шину 5 В: кожен рівень займає лише 0,5 В, тож стрибок шуму 0,3 В перекидає значення — частота помилок була б неприпустимою без постійної корекції.2. Проста реалізація перемикання. Розрізнення двох станів потребує одного порогу; розрізнення N станів — N − 1 точно відкаліброваних компараторів, і виробничі допуски весь час працюють проти вас. Транзистор, який використовується як цифровий елемент, перебуває в одному з двох крайніх режимів — у повному відсіченні (струму немає, вихід «0») або повному насиченні (максимальний струм, вихід «1»). Третій, аналоговий «лінійний» режим навмисно оминають, бо він чутливий до шуму, а його вихід безперервно залежить від входу. Робота в двох станах перетворює складний аналоговий пристрій на надійний перемикач «увімкнено/вимкнено».3. Булева алгебра як готовий фундамент. Двійкова логіка успадковує столітню математичну теорію. Буль (1840-ві) дав строгі операції AND / OR / NOT, алгебраїчні закони для спрощення та результати про повноту, які показують, що будь-яку логічну функцію можна зібрати з невеликого базового набору. Магістерська дисертація Клода Шеннона 1937 року, A Symbolic Analysis of Relay and Switching Circuits, встановила, що ця алгебра напряму відображається на електричні комутаційні схеми — булеві вирази є апаратурою. Жодної співмірної алгебри для 10-значної системи не існує.Універсальність NAND. Будь-яку булеву функцію можна побудувати самими лише вентилями NAND, тож двохстанова апаратура складається в довільну логіку з однієї повторюваної комірки. Три побудови через де Моргана: NOT(A) = NAND(A, A); AND(A, B) = NOT(NAND(A, B)), тобто NAND з наступним NAND, ввімкненим як NOT; OR(A, B) = NAND(NOT A, NOT B), бо A ∨ B = ¬(¬A ∧ ¬B). Одна універсальна цеглинка, застосована багаторазово, дає кожен логічний вентиль, кожен суматор, кожну комірку пам'яті й зрештою кожен ЦП — прямий наслідок згаданої вище причини про булеву алгебру.Зверніть увагу на: чому транзистор тримають у відсіченні або насиченні (а не в аналоговій лінійній області), коли його використовують для цифрової логіки; що станеться, якщо спробувати розділити ту саму шину на 10 рівнів; як Шеннон 1937 року є точним містком між алгеброю Буля XIX століття та фізичними комутаційними схемами.<br>Докладніше: <a data-tooltip-position="top" aria-label="L01. Фізика обчислень > 8. Чому двійкова система? Сила двох станів" data-href="L01. Фізика обчислень#8. Чому двійкова система? Сила двох станів" href="lectures/l01.-фізика-обчислень.html#8. Чому двійкова система? Сила двох станів" class="internal-link" target="_self" rel="noopener nofollow">§8 Чому двійкова</a>, <a data-tooltip-position="top" aria-label="L01. Фізика обчислень > 11. NAND: Універсальний вентиль" data-href="L01. Фізика обчислень#11. NAND: Універсальний вентиль" href="lectures/l01.-фізика-обчислень.html#11. NAND: Універсальний вентиль" class="internal-link" target="_self" rel="noopener nofollow">§11 Універсальність NAND</a>, <a data-tooltip-position="top" aria-label="L01. Фізика обчислень > 13. Фізичні обмеження обчислень" data-href="L01. Фізика обчислень#13. Фізичні обмеження обчислень" href="lectures/l01.-фізика-обчислень.html#13. Фізичні обмеження обчислень" class="internal-link" target="_self" rel="noopener nofollow">§13 Фізичні обмеження</a>.Транзистор як керований перемикач. Транзистор — це трививодний пристрій, у якому невеликий керуючий сигнал вирішує, відкритий чи закритий набагато більший струмовий шлях. У моделі BJT, яку використовують у лекції, виводи — це база (керування), колектор (головний вхід струму) та емітер (головний вихід струму). За нульового струму бази струм не тече з колектора в емітер — перемикач розімкнений. Подайте достатній струм у базу (приблизно V_B &gt; 0,7 В для кремнію), і струм тече вільно — перемикач замкнений. Точнісінько як з водяним клапаном: невеликий поворот ручки керує набагато більшим потоком.Три режими, використовуються лише два. Транзистор має три режими роботи:
Відсічення. База нижче порогу → струму немає → логічний 0.
Насичення. База значно вище порогу → повний струм → логічна 1.
Лінійна (активна) область. Між ними — пропорційна, аналогова поведінка.
Цифрова логіка використовує лише відсічення й насичення. Лінійної області свідомо уникають: невеликі зміни на вході дають пропорційні зміни на виході, тож пристрій підсилює шум і повертає значення, яке не є ані чистим «0», ані чистим «1». Робота «повністю увімкнено або повністю вимкнено» — це те, що робить схему стійкою до шуму й виробничих відхилень — фізичний зміст аргументу про завадостійкість двійкової системи.NAND на функціональному рівні. NAND видає 0 лише тоді, коли обидва входи дорівнюють 1:Універсальність NAND. Будь-яку булеву функцію — а отже кожен вентиль, суматор, тригер, ЦП — можна побудувати лише з NAND. Три побудови:
NOT(A) = NAND(A, A).
AND(A, B) = NOT(NAND(A, B)) — NAND, за яким іде NAND, ввімкнений як NOT.
OR(A, B) = NAND(NOT A, NOT B) — де Морган: A ∨ B = ¬(¬A ∧ ¬B).
Однієї повторюваної цеглинки достатньо для довільної цифрової поведінки. Виробничі лінії (фаби) використовують це, стандартизуючись навколо одного (або двох, з NOR) універсального елемента: простіший процес, тестування та складські запаси.Від одного вентиля до ЦП — порядок величини. Один NAND коштує кілька транзисторів; сучасний ЦП містить транзистори порядку мільярдів на одному кристалі. Міст від проєктування вентилів до інтеграції на чипі — це чисте повторення: мільярди тих самих двохстанових перемикачів реалізують усю архітектуру.Зверніть увагу на: чому NAND обирають замість окремих примітивів AND + NOT (одна універсальна комірка, однорідна бібліотека); чому лінійна область заборонена для цифрової логіки (аналоговий шум, не чистий 0 або 1); масштаб кількості транзисторів у сучасному ЦП як порядок величини (мільярди), а не точне число.<br>Докладніше: <a data-tooltip-position="top" aria-label="L01. Фізика обчислень > 9. Транзистори як перемикачі" data-href="L01. Фізика обчислень#9. Транзистори як перемикачі" href="lectures/l01.-фізика-обчислень.html#9. Транзистори як перемикачі" class="internal-link" target="_self" rel="noopener nofollow">§9 Транзистори як перемикачі</a>, <a data-tooltip-position="top" aria-label="L01. Фізика обчислень > 10. Логічні вентилі: будівельні блоки" data-href="L01. Фізика обчислень#10. Логічні вентилі: будівельні блоки" href="lectures/l01.-фізика-обчислень.html#10. Логічні вентилі: будівельні блоки" class="internal-link" target="_self" rel="noopener nofollow">§10 Логічні вентилі</a>, <a data-tooltip-position="top" aria-label="L01. Фізика обчислень > 11. NAND: Універсальний вентиль" data-href="L01. Фізика обчислень#11. NAND: Універсальний вентиль" href="lectures/l01.-фізика-обчислень.html#11. NAND: Універсальний вентиль" class="internal-link" target="_self" rel="noopener nofollow">§11 Універсальність NAND</a>.Віхи по порядку.ENIAC як орієнтир. Перший електронний комп'ютер загального призначення, 1945: ~18 000 вакуумних ламп, ~5 000 додавань за секунду (приблизно в 1 000 разів більше за Mark I), 30 тонн, 150 кВт, потребував власного генератора. Він довів, що електронні швидкості випереджають будь-що механічне — і водночас оголив обмеження ламп: вони перегоряли, як лампочки розжарення (відмови кожні кілька годин), кожна була приблизно розміром з лампочку, кожна потребувала значної потужності для розігріву катода, і ця потужність виходила у вигляді тепла, яке треба було відводити.Вакуумні лампи → транзистори (1947). Транзистор винайшли в Bell Labs Вільям Шоклі, Джон Бардін та Волтер Браттейн. Він одним махом усунув практично всі обмеження ламп: твердотільний, без прогрівання, працює миттєво, придатний для мініатюризації, на порядки менше споживаної потужності, немає нитки розжарення, яка б перегоряла. Першим повністю транзисторним комп'ютером був TX-0 (1956); до 1960-х транзистори замінили лампи, уможлививши мініатюризацію, що стоїть за ІС та VLSI.Повторюваний рушій: інтеграція замінює ручну роботу. Кожен стрибок іде за тим самим шаблоном — те, що попереднє покоління збирало вручну, наступне інтегрує або автоматизує. Шестерні виточували поодинці; реле людьми монтували у стійки; вакуумні лампи вставляли в гнізда і замінювали вручну, коли вони виходили з ладу; дискретні транзистори припаювали на плати вручну; ІС розміщує багато транзисторів на одній підкладці за один крок виробництва; VLSI упаковує цілий ЦП на одному кристалі. Кожен щабель прибирає шар ручної поштучної збірки — саме тому надійність, розмір і енергія на операцію продовжують поліпшуватися на порядки.Що НЕ змінилося одразу на межі «лампа → транзистор». Це була заміна на рівні пристрою, а не архітектурна революція. Розрядність слова, формати інструкцій, модель програмування та загальна організація системи залишалися на місці роками; першими змінилися надійність, розмір і потужність.Зверніть увагу на: чотири ключові числа ENIAC (лампи, операції/с, вага, потужність) та на те, на яке обмеження ламп вказує кожне з них; хто винайшов транзистор і де (Шоклі / Бардін / Браттейн у Bell Labs, 1947); «інтеграція замінює ручну роботу» як єдина нитка, що об'єднує кожен стрибок.<br>Докладніше: <a data-tooltip-position="top" aria-label="L01. Фізика обчислень > 6. Історична еволюція обчислювальних машин" data-href="L01. Фізика обчислень#6. Історична еволюція обчислювальних машин" href="lectures/l01.-фізика-обчислень.html#6. Історична еволюція обчислювальних машин" class="internal-link" target="_self" rel="noopener nofollow">§6 Історична еволюція</a>, <a data-tooltip-position="top" aria-label="L01. Фізика обчислень > 7. Від вакуумних ламп до транзисторів" data-href="L01. Фізика обчислень#7. Від вакуумних ламп до транзисторів" href="lectures/l01.-фізика-обчислень.html#7. Від вакуумних ламп до транзисторів" class="internal-link" target="_self" rel="noopener nofollow">§7 Вакуумні лампи → транзистори</a>.<br>Блок I · L01 · 3 питання · Частина <a data-href="Exam-Questions" href="exam-questions.html" class="internal-link" target="_self" rel="noopener nofollow">Exam-Questions</a>]]></description><link>exam-prep/l01-prep.html</link><guid isPermaLink="false">Exam-Prep/L01-Prep.md</guid><pubDate>Wed, 22 Apr 2026 04:44:10 GMT</pubDate></item><item><title><![CDATA[L02-Prep]]></title><description><![CDATA[Охоплює екзаменаційні питання I.4 – I.6. Офіційне формулювання — у <a data-href="Exam-Questions" href="exam-questions.html" class="internal-link" target="_self" rel="noopener nofollow">Exam-Questions</a>; повний обсяг підготовки — у <a data-href="Preparation-Guide" href="exam-prep/preparation-guide.html" class="internal-link" target="_self" rel="noopener nofollow">Preparation-Guide</a>.Кожен запис нижче — це компактне ядро відповіді: достатньо, щоб зорієнтуватися, але не замінює лекцію. За посиланнями «Докладніше» можна заглибитися в тему. Рядки «Зверніть увагу на» позначають пастки, на які націлені додаткові запитання екзаменатора.Формальне розмежування. Схема є комбінаційною тоді й лише тоді, коли її вихід є чистою функцією поточних входів — немає шляху зворотного зв'язку, немає внутрішнього стану, і вона повністю описується таблицею істинності з 2ⁿ рядків для n входів. Схема є послідовною тоді й лише тоді, коли її вихід залежить від поточних входів плюс збереженого стану — вона містить щонайменше один контур зворотного зв'язку, і її не можна повністю охарактеризувати лише таблицею істинності (потрібна таблиця переходів станів або скінченний автомат). Діагностика: простежте провідники — будь-який замкнений контур від виходу назад до входу ⇒ послідовна.Приклади схем.
Комбінаційні: напівсуматор (Sum = A XOR B, Carry = A AND B), декодер 2-на-4 (Y0 = ¬A1 ∧ ¬A0 тощо), 8-бітний суматор з послідовним переносом, мультиплексор 4-в-1.
Послідовні: SR-засувка (перехресно з'єднані NOR), D-засувка (D + Enable), D-тригер (master-slave, спрацьовування за фронтом), 8-бітний регістр (вісім D-тригерів зі спільним тактовим сигналом).
Усуваємо тактовий сигнал — конкретний приклад стану гонки. Побудуємо самоінкрементний лічильник: регістр R подається до суматора +1, вихід якого подається назад у R. З D-засувкою, чутливою до рівня, та утримуваним у високому стані Enable: R=3 → суматор видає 4 → засувка прозора, захоплює 4 → суматор тепер видає 5 → засувка захоплює 5 → 6 → 7… R «втікає» зі швидкістю поширення сигналу крізь вентилі й завершує роботу на довільному значенні, яке визначається тим, як довго Enable був у високому стані. Замінимо засувку на D-тригер, що спрацьовує за переднім фронтом і керується тактовим сигналом: за кожен фронт робиться рівно одна вибірка, тож R інкрементується рівно на одиницю за цикл. Роль тактового сигналу — задати єдиний детермінований момент вибірки — фронт — настільки вузький, що зворотний зв'язок не встигає поширитись крізь вікно захоплення.Засувка з керуванням рівнем vs тригер з керуванням по фронту. D-засувка є прозорою, коли Enable = 1 — D проходить прямо до Q, і будь-яка зміна D, доки Enable високий, поширюється негайно. Саме це спричиняє збій лічильника-втікача вище. D-тригер робить вибірку лише на фронті тактового сигналу (єдиний момент), тож вікно, у якому D має бути стабільним, звужується практично до нульової ширини.Конструкція «master-slave». Дві D-засувки послідовно, керовані протилежними фазами тактового сигналу:
Ведуча засувка (master), прозора, коли CLK = 0, завантажує поточне D у внутрішній вузол Q_m.
Ведена засувка (slave), прозора, коли CLK = 1, копіює Q_m на вихід Q.
На передньому фронті ведуча замерзає (CLK→1 закриває її), а ведена відкривається (CLK→1 відкриває її) — тож Q захоплює саме те значення D, яке було безпосередньо перед фронтом. У жодний момент обидва каскади не відкриті одночасно, тож зворотний зв'язок не може проскочити крізь них.
Саме тому D-тригери з керуванням по фронту можуть сидіти в контурі R → +1 → R з одним тактовим сигналом і інкрементуватися на одиницю за цикл: момент вибірки надто вузький, щоб нове значення встигло поширитися назад крізь суматор і знову увійти в тригер на тому самому фронті.Зверніть увагу на: комбінаційна CMOS усе одно споживає динамічну потужність на кожному перемиканні (P = C·V²·f) та струми витоку у стані спокою — не нуль; лічильник-втікач лікується лише переходом від чутливого до рівня Enable до чутливого до фронту тактового сигналу — зменшення частоти не допомагає, бо засувка залишається прозорою протягом усієї високої фази; конструкція «master-slave» — це те, чому говорять, що тригер «захоплює на фронті», попри те що всередині дві засувки працюють на протилежних півциклах.<br>Докладніше: <a data-tooltip-position="top" aria-label="L02. Від вентилів до АЛП > 1. Вступ: Від комбінаційної до послідовної логіки" data-href="L02. Від вентилів до АЛП#1. Вступ: Від комбінаційної до послідовної логіки" href="lectures/l02.-від-вентилів-до-алп.html#1. Вступ: Від комбінаційної до послідовної логіки" class="internal-link" target="_self" rel="noopener nofollow">§1 Комбінаційна vs послідовна</a>, <a data-tooltip-position="top" aria-label="L02. Від вентилів до АЛП > 7. Проблема тактового сигналу" data-href="L02. Від вентилів до АЛП#7. Проблема тактового сигналу" href="lectures/l02.-від-вентилів-до-алп.html#7. Проблема тактового сигналу" class="internal-link" target="_self" rel="noopener nofollow">§7 Проблема тактового сигналу</a>, <a data-tooltip-position="top" aria-label="L02. Від вентилів до АЛП > 8. D-тригер: пам'ять із фронтовим спрацьовуванням" data-href="L02. Від вентилів до АЛП#8. D-тригер: пам'ять із фронтовим спрацьовуванням" href="lectures/l02.-від-вентилів-до-алп.html#8. D-тригер: пам'ять із фронтовим спрацьовуванням" class="internal-link" target="_self" rel="noopener nofollow">§8 D-тригер</a>.Три формати при 8 бітах, показано +5 та −5.Діапазон залежно від розрядності (додатковий код): 8 біт = −128 … +127, 16 біт = −32 768 … +32 767, 32 біти ≈ ±2,15·10⁹, 64 біти ≈ ±9,22·10¹⁸. Асиметричний на одиницю (−2^(n−1) існує, +2^(n−1) — ні), тому що єдиний нуль живе на додатному боці.Порівняння додавання — розроблений приклад −5 + 3 (очікується −2).
Знак-модуль: 1 0000101 + 0 0000011 → апаратура повинна виявити нерівні знаки, відняти модулі, знову приєднати знак. Дороге керування.
Обернений код: 11111010 + 00000011 = 11111101, але −5 + 5 = 11111010 + 00000101 = 11111111 = −0, що потребує кругового переносу (end-around carry), щоб отримати справжній нуль. Додатковий такт суматора.
Додатковий код: 11111011 + 00000011 = 11111110 = −2. Відкинути вихідний перенос — готово. Один суматор, жодних спеціальних випадків.
Чому один суматор обслуговує знакові та беззнакові числа. Додатковий код означений за модулем 2ⁿ: −x ≡ 2ⁿ − x (mod 2ⁿ). Тож a + b, обчислене n-бітним суматором з відкинутим вихідним переносом, є правильним за модулем 2ⁿ незалежно від того, чи операнди інтерпретуються як беззнакові [0, 2ⁿ), чи як знакові [−2ⁿ⁻¹, 2ⁿ⁻¹). Суматор байдужий — різниться лише інтерпретація прапорців (CF для беззнакового переповнення, OF — для знакового).Прийом віднімання на рівні вентилів. A − B = A + (¬B) + 1. Пропустіть кожен біт B через XOR із керівним сигналом SUB: коли SUB=0, XOR пропускає B без змін; коли SUB=1, XOR інвертує B. Підключіть той самий SUB до Cin суматора. Один суматор з послідовним переносом + n XOR + один керівний біт реалізують і ADD (SUB=0, Cin=0), і SUB (SUB=1, Cin=1).Процедура заперечення та алгебраїчне обґрунтування. Щоб заперечити x: інвертувати всі біти, потім додати 1. Алгебра: x + (¬x) = 11…1 = 2ⁿ − 1, тож ¬x = −1 − x, отже ¬x + 1 = −x. Приклад: +5 = 00000101 → інвертувати 11111010 → +1 → 11111011 = −5.Зверніть увагу на: асиметрія — −(−128) у 8-бітному додатковому коді знову дорівнює −128 (переповнення: інверсія 10000000 + 1 закільцьовує назад до 10000000); знакове переповнення сигналізується прапорцем OF, який встановлюється, коли перенос у MSB не збігається з переносом з MSB (еквівалентно: sign(A) = sign(B) ≠ sign(result)); беззнакове переповнення використовує CF (фінальний вихідний перенос), що є цілком іншою умовою.<br>Докладніше: <a data-tooltip-position="top" aria-label="L02. Від вентилів до АЛП > 4. Числа зі знаком: доповнення до двох" data-href="L02. Від вентилів до АЛП#4. Числа зі знаком: доповнення до двох" href="lectures/l02.-від-вентилів-до-алп.html#4. Числа зі знаком: доповнення до двох" class="internal-link" target="_self" rel="noopener nofollow">§4 Додатковий код</a>, <a data-tooltip-position="top" aria-label="L02. Від вентилів до АЛП > Віднімання з використанням доповняльного коду" data-href="L02. Від вентилів до АЛП#Віднімання з використанням доповняльного коду" href="lectures/l02.-від-вентилів-до-алп.html#Віднімання з використанням доповняльного коду" class="internal-link" target="_self" rel="noopener nofollow">Віднімання через додатковий код</a>, <a data-tooltip-position="top" aria-label="L02. Від вентилів до АЛП > Виявлення переповнення" data-href="L02. Від вентилів до АЛП#Виявлення переповнення" href="lectures/l02.-від-вентилів-до-алп.html#Виявлення переповнення" class="internal-link" target="_self" rel="noopener nofollow">Виявлення переповнення</a>.Блок-схема. Три входи, дві категорії виходів: A (N бітів) ─────┐ ┌───── B (N бітів) ▼ ▼ ┌─────────────┐ Код ─▶│ Декодер + │──▶ Результат (N бітів) операції │ N паралель- │ (M бітів) │ них схем │──▶ Прапорці: Z, N, C, V │ операцій │ │ + MUX │ └─────────────┘
Входи: A, B (N-бітні операнди), код операції (M бітів вибирають 2^M операцій). Виходи: Результат (N бітів) плюс чотири прапорці — Z (результат = 0), N (MSB результату = 1, тобто від'ємний у знаковій інтерпретації), C (беззнакове переповнення, фінальний вихідний перенос суматора), V (знакове переповнення, вхідний перенос ≠ вихідний перенос у MSB).Один суматор для ADD та SUB. Усі схеми операцій працюють паралельно кожного такту; MUX обирає одну. Для повторного використання ADD/SUB пропустіть кожен біт B через XOR, інший вхід якого — біт коду операції SUB, і підключіть SUB до Cin суматора:«Комбінаційний прийом» — це саме XOR-як-керований-інвертор + Cin = SUB. Одна й та сама кремнієва схема обслуговує і додавання, і віднімання; другий суматор не потрібен.Відображення «код операції → сигнал керування» (АЛП з 4 операціями, 2-бітний код).Декодер 2-на-4 перетворює код операції на чотири унарні (one-hot) лінії вибору; MUX використовує ці лінії, щоб обрати, який функціональний блок веде шину результату. Той самий шаблон декодера масштабується до 3-на-8 (8 операцій) та 4-на-16 (16 операцій).Зв'язок із сучасним ядром x86. Примітивний конвеєр «код операції → декодер → унарний вибір → блок АЛП» усе ще існує, але сидить на один шар глибше. Справжня x86-інструкція на кшталт ADD RAX, [RBX+8] має змінну довжину (1–15 байтів) і не може бути виконана як є. Фронтальний декодер розкладає її на потік внутрішніх мікрооперацій (µops) фіксованої ширини — наприклад, одну µop завантаження (TMP ← [RBX+8]) та одну µop додавання (RAX ← RAX + TMP). Кожна µop несе невеликий код операції фіксованої ширини, який керує MUX виконавчого блока так само, як у нашому 2-бітному прикладі, лише з більшою кількістю схем операцій (цілочисельне АЛП, зсувач, помножувач, блок FP, блок SIMD), з перейменуванням регістрів та з кешем µop, щоб не декодувати повторно гарячі цикли.Зверніть увагу на: суматор є блоком, критичним за затримкою, тому що перенос має пробігти O(N) вентилів для N-бітного суматора з послідовним переносом (у реальних чипах це пом'якшується суматорами з прискореним переносом, зі скаковим переносом або конструкціями Kogge-Stone з O(log N)); MUL — багатотактова: або комбінаційне дерево суматорів часткових добутків (велика площа), або ітеративний секвенсер зсуву-та-додавання (мала площа, багато тактів) — тому її рідко вбудовують у базове АЛП; прапорці роблять АЛП схожим на такий, що має стан, але насправді вони — просто додаткові комбінаційні виходи — стан живе в окремому регістрі FLAGS/EFLAGS, який фіксує ці виходи на наступному фронті тактового сигналу.<br>Докладніше: <a data-tooltip-position="top" aria-label="L02. Від вентилів до АЛП > 12. АЛП: збираємо все докупи" data-href="L02. Від вентилів до АЛП#12. АЛП: збираємо все докупи" href="lectures/l02.-від-вентилів-до-алп.html#12. АЛП: збираємо все докупи" class="internal-link" target="_self" rel="noopener nofollow">§12 АЛП</a>, <a data-tooltip-position="top" aria-label="L02. Від вентилів до АЛП > 11. Двійкові декодери" data-href="L02. Від вентилів до АЛП#11. Двійкові декодери" href="lectures/l02.-від-вентилів-до-алп.html#11. Двійкові декодери" class="internal-link" target="_self" rel="noopener nofollow">§11 Декодери</a>, <a data-tooltip-position="top" aria-label="L02. Від вентилів до АЛП > Віднімання з використанням доповняльного коду" data-href="L02. Від вентилів до АЛП#Віднімання з використанням доповняльного коду" href="lectures/l02.-від-вентилів-до-алп.html#Віднімання з використанням доповняльного коду" class="internal-link" target="_self" rel="noopener nofollow">Віднімання через додатковий код</a>.<br>Блок I · L02 · 3 питання · Частина <a data-href="Exam-Questions" href="exam-questions.html" class="internal-link" target="_self" rel="noopener nofollow">Exam-Questions</a>]]></description><link>exam-prep/l02-prep.html</link><guid isPermaLink="false">Exam-Prep/L02-Prep.md</guid><pubDate>Wed, 22 Apr 2026 04:44:10 GMT</pubDate></item><item><title><![CDATA[L03-Prep]]></title><description><![CDATA[Охоплює екзаменаційні питання I.7 – I.9. Офіційне формулювання — у <a data-href="Exam-Questions" href="exam-questions.html" class="internal-link" target="_self" rel="noopener nofollow">Exam-Questions</a>; повний обсяг підготовки — у <a data-href="Preparation-Guide" href="exam-prep/preparation-guide.html" class="internal-link" target="_self" rel="noopener nofollow">Preparation-Guide</a>.Кожен пункт нижче — це компактне ядро відповіді, достатнє, щоб зорієнтуватися, але не заміна лекції. Для поглиблення йдіть за посиланнями в «Докладніше». Рядки «Зверніть увагу на» позначають пастки, на які націлені додаткові запитання екзаменатора.Компроміс «швидкість проти кодування». Регістри жорстко з'єднані з входами АЛП — немає декодування адреси, немає шини, немає контролера пам'яті. Доступ &lt; 1 нс (один такт). DRAM — ~50–100 нс, ~у 100 разів повільніша. Проте x86-64 надає лише 16 регістрів загального призначення, тоді як DRAM вміщує гігабайти. Стеля — не площа кремнію (комірка крихітна), а кодування інструкцій: кожен регістровий операнд з'їдає log₂(N) бітів. 8 регістрів → 3 біти, 16 → 4, 32 → 5, 64 → 6. Триоперандна інструкція з 64 регістрами витрачає 18 бітів лише на іменування операндів, роздуваючи кожну вибірку.Родовід акумулятора. «Результати арифметики потрапляють в один визначений регістр» — ця ідея походить від ENIAC (1945) та IBM 701 (1952). Intel 8008 (1972) та 8080 (1974) мали єдиний акумулятор A. 8086 (1978) підвищив його до AX (Accumulator eXtended); 80386 (1985) розширив до EAX; AMD64 (2003) — до RAX. Перекриття фізичне: запис у AL модифікує молодший байт AX/EAX/RAX. Спадок зберігається як коротші кодування для форм із AX та неявне використання в MUL/DIV (DX:AX) та IN/OUT.Еволюція регістрового файлу.Стрибок 8→16 вимагав префікса REX — однобайтового префікса, введеного разом із x86-64, який розширює індекси регістрів з 3 до 4 бітів, щоб можна було закодувати 16 регістрів ЗП RAX…R15. REX — це ціна 16 регістрів ЗП у успадкованій ISA зі змінною довжиною інструкцій.Приклад трафіку шини. Обчислити d = a + b + c:; пам'ять-пам'ять (гіпотетично) ; на основі регістрів (x86)
LOAD_ADD [a],[b],[tmp] ; 3 звернення MOV EAX,[a] ; 1 завантаження
LOAD_ADD [tmp],[c],[d] ; 3 звернення ADD EAX,[b] ; 1 завантаження ; 6 разом ADD EAX,[c] ; 1 завантаження MOV [d],EAX ; 1 збереження → 4
EAX залишається живим упродовж трьох додавань — регістровий код удвічі зменшує трафік пам'яті. Саме цей трафік і приховує кеш.Зверніть увагу на: чому 8086 зупинився на 8 регістрах ЗП (3-бітне поле вкладалося в бюджет кодів операцій 1978 року); REX як ретрофіт, а не редизайн; стек x87 FPU має лише 8 рівнів (ST(0)–ST(7)), і його стекова дисципліна спричиняє баги тиску на регістри, які жонглюються через FXCH — причина, чому SSE/AVX відмовилися від стека на користь плоского регістрового файлу.<br>Докладніше: <a data-tooltip-position="top" aria-label="L03. Архітектура x86 — регістри та пам'ять > 3. Концепція акумулятора" data-href="L03. Архітектура x86 — регістри та пам'ять#3. Концепція акумулятора" href="lectures/l03.-архітектура-x86-—-регістри-та-пам'ять.html#3. Концепція акумулятора" class="internal-link" target="_self" rel="noopener nofollow">§3 Концепція акумулятора</a>, <a data-tooltip-position="top" aria-label="L03. Архітектура x86 — регістри та пам'ять > 4. Регістри загального призначення x86" data-href="L03. Архітектура x86 — регістри та пам'ять#4. Регістри загального призначення x86" href="lectures/l03.-архітектура-x86-—-регістри-та-пам'ять.html#4. Регістри загального призначення x86" class="internal-link" target="_self" rel="noopener nofollow">§4 Регістри ЗП x86</a>, <a data-tooltip-position="top" aria-label="L03. Архітектура x86 — регістри та пам'ять > 5. Регістри: швидкі, але обмежені" data-href="L03. Архітектура x86 — регістри та пам'ять#5. Регістри: швидкі, але обмежені" href="lectures/l03.-архітектура-x86-—-регістри-та-пам'ять.html#5. Регістри: швидкі, але обмежені" class="internal-link" target="_self" rel="noopener nofollow">§5 Регістри швидкі, але обмежені</a>.Невідповідність. 8086 має 16-бітні регістри (максимум 65 535), але 20-бітну адресну шину, що дотягується до 2^20 = 1 048 576 байтів (1 МБ). Один 16-бітний регістр адресує лише 64 КБ. Розв'язок Intel: скомпонувати 20-бітну фізичну адресу з двох 16-бітних значень, а не розширювати регістри (це зламало б сумісність з 8080/8085).Правило. physical = (segment &lt;&lt; 4) + offset — множення сегмента на 16 — це зсув вліво на одну шістнадцяткову цифру (дописати нульовий ніббл), що дає 20-бітну базу; 16-бітне зміщення додається зверху. Запис: segment:offset, обидва в шістнадцятковій системі.Розроблені приклади.Аліасинг. Сегменти починаються кожні 16 байтів («параграф»), але мають довжину 64 КБ, тож кожен фізичний байт має до 4 096 різних кодувань segment:offset. Адреса завантажувального сектора 0x07C00 може бути 0000:7C00, 07C0:0000, 07BF:0010, 0700:0C00, … — усі той самий байт. Класична пастка іспиту.Апаратно закріплені адреси (фіксовані схемою, а не за домовленістю):
0xFFFF0 (F000:FFF0) — вектор скидання ЦП. Після подачі живлення CS:IP примусово встановлюється саме сюди; ROM BIOS повинен розмістити там перехід.
0xB8000 (B800:0000) — буфер кадру VGA у текстовому режимі. Відеоконтролер відслідковує цей регіон на шині.
0x00000–0x003FF — таблиця векторів переривань реального режиму (256 × 4 байти).
Верхня межа та HMA. Максимум FFFF:FFFF = 0x10FFEF — 21 біт, перевищує 1 МБ на 65 519 байтів. Оригінальний 8086 не мав біта 20, тож адреса «загорталася» до 0x0FFEF. На 80286+ шина розширилася, і цей діапазон став досяжним із реального режиму — High Memory Area (0x100000–0x10FFEF), доступна через FFFF:xxxx лише коли ввімкнено A20 gate. DOS 5+ використовував HMA для коду ядра вище 640 КБ.Зверніть увагу на: навести дві різні пари seg:off для одного байта на льоту; у 32-бітному захищеному режимі сегментний регістр перестає множитися на 16 і стає селектором, що індексує GDT/LDT для отримання 32-бітної бази + межі; 80286 зберіг сегментацію заради зворотної сумісності та апаратно контрольованих меж сегментів.<br>Докладніше: <a data-tooltip-position="top" aria-label="L03. Архітектура x86 — регістри та пам'ять > 8. Мікропроцесор Intel 8086" data-href="L03. Архітектура x86 — регістри та пам'ять#8. Мікропроцесор Intel 8086" href="lectures/l03.-архітектура-x86-—-регістри-та-пам'ять.html#8. Мікропроцесор Intel 8086" class="internal-link" target="_self" rel="noopener nofollow">§8 Intel 8086</a>, <a data-tooltip-position="top" aria-label="L03. Архітектура x86 — регістри та пам'ять > 9. Адресація Сегмент:Зміщення" data-href="L03. Архітектура x86 — регістри та пам'ять#9. Адресація Сегмент:Зміщення" href="lectures/l03.-архітектура-x86-—-регістри-та-пам'ять.html#9. Адресація Сегмент:Зміщення" class="internal-link" target="_self" rel="noopener nofollow">§9 Адресація сегмент:зміщення</a>, <a data-tooltip-position="top" aria-label="L03. Архітектура x86 — регістри та пам'ять > 10. Сегментні регістри та структура програми" data-href="L03. Архітектура x86 — регістри та пам'ять#10. Сегментні регістри та структура програми" href="lectures/l03.-архітектура-x86-—-регістри-та-пам'ять.html#10. Сегментні регістри та структура програми" class="internal-link" target="_self" rel="noopener nofollow">§10 Сегментні регістри</a>.Структура комірки. SRAM використовує 6-транзисторну (6T) комірку: два перехресно зв'язані CMOS-інвертори (4 транзистори) утворюють бістабільний тригер, який активно утримує біт, плюс 2 транзистори доступу, які керують бітовими лініями під час читання/запису. DRAM використовує 1T1C: один MOSFET плюс крихітний конденсатор (заряджений = 1, порожній = 0). Конденсатор — це і є весь елемент зберігання; жодного активного зворотного зв'язку, який його утримував би.Порівняння.Чому DRAM принципово повільніша. Читання: (1) передзаряд бітової лінії, (2) активація рядка, конденсатор віддає заряд, (3) підсилювач зчитування порівнює коливання ~100 мВ з опорним рівнем і піднімає до повного логічного рівня, (4) перезапис — читання виснажило конденсатор, тож підсилювач зчитування відновлює його. Кожен доступ — руйнівне читання плюс перезапис. SRAM — це просто засувка: рядкова лінія з'єднує бітові лінії з виходами перехресно зв'язаних інверторів за одну затримку вентиля — жодного передзаряду, жодного зчитування, жодного перезапису.Чому SRAM не може масштабуватися вгору. 6T проти 1T1C спалює ~у 6 разів більше кремнію на біт. Основна пам'ять SRAM на 16 ГБ вимагала б кристала розміром з материнську плату. Регенерація економиться, але все решта вибухає.Роль в ієрархії.Розрив L1–DRAM — ~два порядки величини — саме той розрив, який приховує ієрархія кешу.Зверніть увагу на: співвідношення тактів (L1 ~3 проти DRAM ~300 на 3 ГГц → ~100× розрив); двовимірну розкладку RAS/CAS у DRAM — один рядок підсилювачів зчитування ділиться між тисячами стовпців, тож ви відкриваєте рядок раз, а потім дешево стрімите стовпці; поведінку при знятті живлення — DRAM швидко втрачає вміст, бо її конденсатори розряджаються, а SRAM теж втрачає вміст, бо перехресно зв'язаним інверторам потрібна неперервна VDD для утримання стану. Обидві енергозалежні, на відміну від Flash/ROM. Практичний висновок: жодна з них не зберігає дані без живлення.<br>Докладніше: <a data-tooltip-position="top" aria-label="L03. Архітектура x86 — регістри та пам'ять > 6. Памʼять: рішення з квадратною сіткою" data-href="L03. Архітектура x86 — регістри та пам'ять#6. Памʼять: рішення з квадратною сіткою" href="lectures/l03.-архітектура-x86-—-регістри-та-пам'ять.html#6. Памʼять: рішення з квадратною сіткою" class="internal-link" target="_self" rel="noopener nofollow">§6 Сітка пам'яті</a>, <a data-tooltip-position="top" aria-label="L03. Архітектура x86 — регістри та пам'ять > 7. Від бітів до байтів і до оперативної пам'яті" data-href="L03. Архітектура x86 — регістри та пам'ять#7. Від бітів до байтів і до оперативної пам'яті" href="lectures/l03.-архітектура-x86-—-регістри-та-пам'ять.html#7. Від бітів до байтів і до оперативної пам'яті" class="internal-link" target="_self" rel="noopener nofollow">§7 Від бітів до ОП (SRAM vs DRAM)</a>, <a data-tooltip-position="top" aria-label="L03. Архітектура x86 — регістри та пам'ять > 5. Регістри: швидкі, але обмежені" data-href="L03. Архітектура x86 — регістри та пам'ять#5. Регістри: швидкі, але обмежені" href="lectures/l03.-архітектура-x86-—-регістри-та-пам'ять.html#5. Регістри: швидкі, але обмежені" class="internal-link" target="_self" rel="noopener nofollow">§5 Регістри швидкі, але обмежені</a>.<br>Блок I · L03 · 3 питання · Частина <a data-href="Exam-Questions" href="exam-questions.html" class="internal-link" target="_self" rel="noopener nofollow">Exam-Questions</a>]]></description><link>exam-prep/l03-prep.html</link><guid isPermaLink="false">Exam-Prep/L03-Prep.md</guid><pubDate>Wed, 22 Apr 2026 04:44:10 GMT</pubDate></item><item><title><![CDATA[L04-Prep]]></title><description><![CDATA[Охоплює екзаменаційні питання I.10 – I.12. Офіційне формулювання — у <a data-href="Exam-Questions" href="exam-questions.html" class="internal-link" target="_self" rel="noopener nofollow">Exam-Questions</a>; повний обсяг підготовки — у <a data-href="Preparation-Guide" href="exam-prep/preparation-guide.html" class="internal-link" target="_self" rel="noopener nofollow">Preparation-Guide</a>.Кожен запис нижче — це компактне ядро відповіді, достатнє, щоб зорієнтуватися, але не заміна лекції. Переходьте за посиланнями «Докладніше», щоб копнути глибше. Рядки «Зверніть увагу на» позначають пастки, на які націлені додаткові запитання екзаменатора.Конкретне трасування — MOV AX, 1234h (кодування B8 34 12, 3 байти за адресою CS:IP = 0F78:0100).
Вибірка. Блок інтерфейсу шини (Bus Interface Unit) читає байт за фізичною адресою (CS × 16) + IP = 0F880h — це B8. Оскільки x86 має змінну довжину інструкцій, попередня вибірка захоплює вікно байтів у чергу інструкцій; декодер споживає стільки, скільки вимагає код операції.
Декодування. Декодер бачить B8 і розпізнає патерн 1011 0 rrr = «MOV imm16 → reg16», молодші 3 біти 000 = AX. Тепер довжина відома (3 байти), тож наступні два байти 34 12 беруться як безпосереднє значення 1234h у форматі little-endian.
Виконання. Активності АЛП немає — блок виконання маршрутизує безпосереднє значення до AX.
Завершення / просування IP. IP ← IP + 3 = 0103h. Цикл повторюється.
Просування IP при кодуванні змінної довжини. IP просувається на ту довжину, яку визначив декодер (від 1 до 15 байтів). NOP (90) просуває на 1; MOV AX, imm16 — на 3; MOV з операндом пам'яті, зі зміщенням і байтом ModR/M може сягати 6–7. Ось чому декодування x86 складніше за RISC: ARM/MIPS фіксують інструкції у 4 байтах, тож адреса інструкції N+1 відома ще до того, як декодовано N; в x86 довжина залежить від коду операції + префіксів + ModR/M і обчислюється пізно. Сучасні ядра все одно витрачають кремній на паралельні предекодери довжини, щоб за такт вибирати кілька інструкцій.Які прапорці зачіпає цей MOV. MOV не зачіпає жодного прапорця — переміщення даних не є операцією АЛП. Порівняйте з ADD AX, 1234h: кодування має ту саму форму, але CF/ZF/SF/OF/PF/AF — усі записуються з результату. Саме тому MOV reg, 0 зберігає попередні прапорці, а XOR reg, reg їх затирає — це має значення, коли ви обнуляєте регістр між CMP і його Jcc.При умовному переході, який НЕ виконується: IP просто просувається до наступної послідовної інструкції (IP ← IP + length(Jcc), 2 байти для короткої форми). Цільова адреса переходу ігнорується.Сучасна реальність. Ця картина «по одній інструкції за раз» — корисна фікція. Реальне ядро (Skylake, Zen 4) попередньо декодує байти в мікрооперації, кешує їх, перейменовує регістри у більший фізичний файл, диспетчеризує 6–8 мікрооперацій за такт на порти позачергового виконання і завершує їх по порядку, щоб зберегти архітектурну ілюзію. Спекуляція перетинає межі переходів; те, що програміст записав як MOV AX, 1234h, може ніколи не торкнутися конвеєра виконання — перейменовувач задовольняє її напряму.Зверніть увагу на: чому декодування x86 за своєю природою послідовне (довжина залежить від вмісту — не можна декодувати інструкцію N+1, поки не відома довжина N); що відбувається з IP при виконаному vs невиконаному умовному переході; розрив між підручниковою моделлю «по одній інструкції за раз» і реальністю з кешованими мікроопераціями, перейменуванням та позачерговим виконанням.<br>Докладніше: <a data-tooltip-position="top" aria-label="L04. Архітектура x86 — основні типи інструкцій > CS:IP — Сегмент коду та вказівник інструкцій" data-href="L04. Архітектура x86 — основні типи інструкцій#CS:IP — Сегмент коду та вказівник інструкцій" href="lectures/l04.-архітектура-x86-—-основні-типи-інструкцій.html#CS:IP — Сегмент коду та вказівник інструкцій" class="internal-link" target="_self" rel="noopener nofollow">§2 CS:IP</a>, <a data-tooltip-position="top" aria-label="L04. Архітектура x86 — основні типи інструкцій > Інструкції змінної довжини в x86" data-href="L04. Архітектура x86 — основні типи інструкцій#Інструкції змінної довжини в x86" href="lectures/l04.-архітектура-x86-—-основні-типи-інструкцій.html#Інструкції змінної довжини в x86" class="internal-link" target="_self" rel="noopener nofollow">§12 Інструкції змінної довжини</a>, <a data-tooltip-position="top" aria-label="L04. Архітектура x86 — основні типи інструкцій > Природа машинного коду" data-href="L04. Архітектура x86 — основні типи інструкцій#Природа машинного коду" href="lectures/l04.-архітектура-x86-—-основні-типи-інструкцій.html#Природа машинного коду" class="internal-link" target="_self" rel="noopener nofollow">§12 Природа машинного коду</a>.Чому суматор не зважає на знаковість. Доповняльний код робить так, що та сама двійкова схема додавання дає коректний результат для обох інтерпретацій. Для 8 бітів значення = −b7·128 + b6·64 + … + b0·1 (знакове) vs b7·128 + … (беззнакове) — відрізняється лише вага старшого біта. Додавання за модулем 256 працює однаково в обох випадках; АЛП ніколи не питає «а це знакове?». Інтерпретація живе у прапорцях: CF і OF виробляються тим самим суматором за той самий такт, а програміст обирає, на якому з них розгалужуватися.Трасування: MOV AL, 80h; ADD AL, AL. 1000 0000 (80h = 128 беззнакове, −128 знакове)
+ 1000 0000
-----------
1 0000 0000 (100h — перенесення зі старшого біта)
AL ← 00h. Трасування прапорців:
CF = 1 — перенесення зі старшого біта; беззнакове переповнення (128 + 128 = 256 не вміщається).
ZF = 1 — 8-бітний результат дорівнює нулю.
SF = 0 — старший біт результату — 0.
OF = 1 — знакове переповнення: (−128) + (−128) = −256, поза діапазоном [−128, +127]. Правило «OF = перенесення-в-старший-біт XOR перенесення-зі-старшого-біта» дає 0 XOR 1 = 1.
PF = 1, AF = 0.
Знакове vs беззнакове значення чотирьох прапорців умов.Програмний наслідок. x86 надає дві паралельні родини переходів після CMP: JA/JB/JAE/JBE (беззнакові: Above/Below) перевіряють CF і ZF; JG/JL/JGE/JLE (знакові) перевіряють SF XOR OF і ZF. Неправильний вибір родини мовчки псує цикли: індекс, порівняний з JL, трактує 0xFFFFFFFF як −1 і завчасно виходить; з JB — це ~4 млрд, більше за будь-який реальний індекс. Ті самі біти, два числові всесвіти, одне рішення про розгалуження.Зверніть увагу на: точну формулу OF (C_in_to_MSB XOR C_out_of_MSB); чому SUB реалізовано як ADD доповняльного коду — «інвертувати та додати 1» означає, що той самий суматор обслуговує обидва (а CF інвертовано відносно «чистої» позики — це архітектурний вибір); чому AF досі існує (BCD-арифметика через AAA/DAA, збережено задля сумісності з двійково-десятковим кодом у застарілому фінансовому коді).<br>Докладніше: <a data-tooltip-position="top" aria-label="L04. Архітектура x86 — основні типи інструкцій > Прапорці умов" data-href="L04. Архітектура x86 — основні типи інструкцій#Прапорці умов" href="lectures/l04.-архітектура-x86-—-основні-типи-інструкцій.html#Прапорці умов" class="internal-link" target="_self" rel="noopener nofollow">§4 Прапорці умов</a>, <a data-tooltip-position="top" aria-label="L04. Архітектура x86 — основні типи інструкцій > Як працює доповняльний код" data-href="L04. Архітектура x86 — основні типи інструкцій#Як працює доповняльний код" href="lectures/l04.-архітектура-x86-—-основні-типи-інструкцій.html#Як працює доповняльний код" class="internal-link" target="_self" rel="noopener nofollow">§10 Як працює доповняльний код</a>, <a data-tooltip-position="top" aria-label="L04. Архітектура x86 — основні типи інструкцій > Виявлення переповнення: CF проти OF" data-href="L04. Архітектура x86 — основні типи інструкцій#Виявлення переповнення: CF проти OF" href="lectures/l04.-архітектура-x86-—-основні-типи-інструкцій.html#Виявлення переповнення: CF проти OF" class="internal-link" target="_self" rel="noopener nofollow">§10 CF проти OF</a>, <a data-tooltip-position="top" aria-label="L04. Архітектура x86 — основні типи інструкцій > Ті самі біти, різні інтерпретації" data-href="L04. Архітектура x86 — основні типи інструкцій#Ті самі біти, різні інтерпретації" href="lectures/l04.-архітектура-x86-—-основні-типи-інструкцій.html#Ті самі біти, різні інтерпретації" class="internal-link" target="_self" rel="noopener nofollow">§10 Ті самі біти, різні інтерпретації</a>.Чотири операції над бітом. Для біта N байта будуємо маску M = 1 &lt;&lt; N. Тоді:Правило: 1 у масці діє, 0 зберігає. OR-1 встановлює, AND-0 скидає, XOR-1 перемикає. Щоб витягти біти 3–7 у біти 0–4: AND AL, 0F8h, потім SHR AL, 3.TEST vs CMP.Для «чи AL дорівнює нулю?» TEST AL, AL (2 байти) виграє у CMP AL, 0 (3 байти): коротше, той самий результат у ZF.Ідіома обнулення XOR reg, reg. Надає перевагу над MOV AX, 0 з двох причин: (1) коротше кодування — 2 байти (33 C0) проти 3 (B8 00 00); (2) розриває фіктивну залежність — сучасні ядра з позачерговим виконанням (P6+) розпізнають XOR same, same як ідіому обнулення, і перейменовувач призначає нульовий фізичний регістр без залежності від попереднього AX, що дозволяє мікрооперації випуститися негайно. Застереження: XOR записує прапорці (CF=0, OF=0, ZF=1). Між CMP і його Jcc використовуйте натомість MOV.SHL vs SAL, SHR vs SAR.SHR AL, 1 для FFh → 7Fh (неправильно для −1); SAR AL, 1 для FFh → FFh (все ще −1, правильно).LEA для арифметики без прапорців. LEA обчислює base + index·scale + disp через блок генерації адрес і не зачіпає прапорців. LEA EAX, [EBX + EBX*2 + 4] обчислює 3·EBX + 4 за одну інструкцію — ідеально між інструкцією, що встановлює прапорці, та переходом, який їх читає.Зверніть увагу на: SHL vs SAL (ідентичні, дві мнемоніки для одного коду операції — історичний курйоз); який зсув використовувати для знакового ділення на степені 2 (SAR, ніколи не SHR); чому TEST AL, AL скидає CF і OF (логічні операції завжди так роблять — побічний ефект, який варто пам'ятати при суміщенні з ADC); те, що XOR reg, reg чудово підходить для обнулення, але руйнує прапорці, тож MOV reg, 0 все ще має своє місце між CMP і Jcc.<br>Докладніше: <a data-tooltip-position="top" aria-label="L04. Архітектура x86 — основні типи інструкцій > Логічні інструкції" data-href="L04. Архітектура x86 — основні типи інструкцій#Логічні інструкції" href="lectures/l04.-архітектура-x86-—-основні-типи-інструкцій.html#Логічні інструкції" class="internal-link" target="_self" rel="noopener nofollow">§7 Логічні інструкції</a>, <a data-tooltip-position="top" aria-label="L04. Архітектура x86 — основні типи інструкцій > TEST — Перевірка бітів (AND без збереження)" data-href="L04. Архітектура x86 — основні типи інструкцій#TEST — Перевірка бітів (AND без збереження)" href="lectures/l04.-архітектура-x86-—-основні-типи-інструкцій.html#TEST — Перевірка бітів (AND без збереження)" class="internal-link" target="_self" rel="noopener nofollow">§7 TEST</a>, <a data-tooltip-position="top" aria-label="L04. Архітектура x86 — основні типи інструкцій > XOR — Побітове виключне OR" data-href="L04. Архітектура x86 — основні типи інструкцій#XOR — Побітове виключне OR" href="lectures/l04.-архітектура-x86-—-основні-типи-інструкцій.html#XOR — Побітове виключне OR" class="internal-link" target="_self" rel="noopener nofollow">§7 XOR</a>, <a data-tooltip-position="top" aria-label="L04. Архітектура x86 — основні типи інструкцій > Прапорці, що змінюються інструкціями Групи 1" data-href="L04. Архітектура x86 — основні типи інструкцій#Прапорці, що змінюються інструкціями Групи 1" href="lectures/l04.-архітектура-x86-—-основні-типи-інструкцій.html#Прапорці, що змінюються інструкціями Групи 1" class="internal-link" target="_self" rel="noopener nofollow">§7 Прапорці, що змінюються</a>.<br>Блок I · L04 · 3 питання · Частина <a data-href="Exam-Questions" href="exam-questions.html" class="internal-link" target="_self" rel="noopener nofollow">Exam-Questions</a>]]></description><link>exam-prep/l04-prep.html</link><guid isPermaLink="false">Exam-Prep/L04-Prep.md</guid><pubDate>Wed, 22 Apr 2026 04:44:10 GMT</pubDate></item><item><title><![CDATA[L15-Optimizing-Different-Parts]]></title><description><![CDATA[<img src="assets/l15-optimizing-different-parts.svg" target="_self">]]></description><link>assets/l15-optimizing-different-parts.html</link><guid isPermaLink="false">assets/L15-Optimizing-Different-Parts.svg</guid><pubDate>Tue, 21 Apr 2026 09:39:14 GMT</pubDate><enclosure url="assets/l15-optimizing-different-parts.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-optimizing-different-parts.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-Optimizing-Different-Parts-ENG]]></title><description><![CDATA[<img src="assets/l15-optimizing-different-parts-eng.svg" target="_self">]]></description><link>assets/l15-optimizing-different-parts-eng.html</link><guid isPermaLink="false">assets/L15-Optimizing-Different-Parts-ENG.svg</guid><pubDate>Tue, 21 Apr 2026 09:39:14 GMT</pubDate><enclosure url="assets/l15-optimizing-different-parts-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-optimizing-different-parts-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Project-Report]]></title><description><![CDATA[Детектор ШІ не дивиться, скільки ви переписували. Він бачить&nbsp;структурні сліди&nbsp;згенерованого тексту: побудову речень, логіку переходів, типові конструкції. Після ручного редагування сліди залишаються. Переписати «ще раз» рядок за рядком не спрацює, бо ви все одно рухаєтесь по рейках ШІ.Але справжня причина писати самостійно не в детекторі. Справжня причина в тому, що&nbsp;письмо і є мислення. Не запис готових думок, а спосіб їх формувати. Леслі Лемпорт (премія Тюрінга, автор LaTeX) каже прямо:&nbsp;"If you're thinking without writing, you only think you're thinking."&nbsp;Коли ви записуєте власну ідею, то виявляєте, що вона неточна, неповна, або взагалі розвалюється під власною вагою. Значить, ви її ще не додумали. І ось це впирання в межу власного розуміння і є та вправа, заради якої курс і просить вас писати.Коли ШІ пише за вас, ви цю межу обходите, і разом з нею обходите саму вправу. Цю навичку тут не обійти, тож краще відпрацювати зараз на лабі, ніж за два роки на співбесіді.
Відкрийте пустий файл.&nbsp;ШІ-версію не тримайте відкритою, навіть у сусідній вкладці.
Одне речення зверху:&nbsp;про що звіт. Напр.: «Звіт описує реалізацію гри Snake на TASM із прямим записом у відеопам'ять 0xB800 та обробкою таймера через INT 1Ch.»
Далі булетами розділи звіту:&nbsp;Опис програми, Використані концепції, Схема пам'яті, Аналіз «A проти B», Ретроспектива.
Під кожним розділом додайте 2-4 булети&nbsp;своїми словами. Так, ніби розповідаєте одногрупнику за кавою.
Тільки тоді розгортайте булети в прозу.&nbsp;По одному.
Код додаєте в кінці,&nbsp;коли тексту вже достатньо.
Працює, бо структура і формулювання йдуть з вашої голови, а не змінюють чужий текст.
Брейншторм перед написанням.&nbsp;«Які компроміси між виведенням через INT 10h та прямим записом у відеопам'ять 0xB800?» → прочитали, закрили вкладку, погуляли 5хв, щоб вивітрити з голови ШІ, пишете з памʼяті.
Переклад термінів і назв розділів.&nbsp;Пишете українською, треба англійський термін. Запитали, записали, рухаєтесь далі.
Пояснити концепт, який не зрозуміли.&nbsp;«Поясни, що відбувається зі стеком при виклику far-процедури.» Далі пишете про це своїми словами. Якщо без відкритої вкладки ШІ не можете, значить, ще не зрозуміли. Читайте далі.
Перевірка готової чернетки:&nbsp;«Чи не суперечить цей абзац сам собі?» → беремо зауваження, правимо руками, закривши попередньо вкладу з ШІ, і погулявши 5хв. «Згенерувати і відредагувати»: структурні сліди залишаються.
«Перефразуй простіше»: те саме, на крок далі по ланцюжку.
«Зроби так, щоб не звучало як ШІ»: саме на цьому детектори тренуються. Гірший варіант з усіх.
Вставити ШІ-текст у файл звіту «на чернетку»: ви вже привʼязались до його формулювань. Проза написана у файлі, куди ШІ-контент початково не потрапляв.
Можу пояснити кожне речення вголос, не підглядаючи.
Кожен абзац має одну чітку думку.
Ручна версія закомічена&nbsp;окремим комітом.
Дискомфорт пустої сторінки не означає, що ви не вивезете. Він означає, що мозок робить роботу. ШІ цей дискомфорт прибирає разом із навчанням.
<a data-tooltip-position="top" aria-label="https://www.microsoft.com/en-us/research/wp-content/uploads/2016/07/leslie_lamport.pdf" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.microsoft.com/en-us/research/wp-content/uploads/2016/07/leslie_lamport.pdf" target="_self">Thinking Above the Code, PDF (Leslie Lamport, Microsoft Research)</a>
<br><a data-tooltip-position="top" aria-label="https://writingcenter.fas.harvard.edu/tips-organizing-your-essay" rel="noopener nofollow" class="external-link is-unresolved" href="https://writingcenter.fas.harvard.edu/tips-organizing-your-essay" target="_self">Tips for Organizing Your Essay (Harvard College Writing Center)</a>
<br><a data-tooltip-position="top" aria-label="https://gsas.harvard.edu/news/notes-writers-desk-drawing-blank" rel="noopener nofollow" class="external-link is-unresolved" href="https://gsas.harvard.edu/news/notes-writers-desk-drawing-blank" target="_self">Notes from a Writer's Desk: Drawing a Blank (Harvard GSAS)</a>
<br><a data-tooltip-position="top" aria-label="https://gsas.harvard.edu/news/how-build-first-draft-outline" rel="noopener nofollow" class="external-link is-unresolved" href="https://gsas.harvard.edu/news/how-build-first-draft-outline" target="_self">How to Build a First Draft from an Outline (Harvard GSAS)</a>
<br><a data-tooltip-position="top" aria-label="https://writingcenter.fas.harvard.edu/strategies-essay-writing-downloadable-pdfs" rel="noopener nofollow" class="external-link is-unresolved" href="https://writingcenter.fas.harvard.edu/strategies-essay-writing-downloadable-pdfs" target="_self">Strategies for Essay Writing, PDF (Harvard College Writing Center)</a>
]]></description><link>labs/project-report.html</link><guid isPermaLink="false">Labs/Project-Report.md</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate></item><item><title><![CDATA[Readme]]></title><description><![CDATA[Матеріали курсу НаУКМА.
<a class="internal-link" data-href="Lectures/L01. Фізика обчислень.md" href="lectures/l01.-фізика-обчислень.html" target="_self" rel="noopener nofollow">Повні матеріали (UK)</a>
<br><a class="internal-link" data-href="Lectures/L01-NOTES. Фізика обчислень.md" href="lectures/l01-notes.-фізика-обчислень.html" target="_self" rel="noopener nofollow">Нотатки (UK)</a>
<br><a class="internal-link" data-href="Lectures/ENG/L01-ENG. Physics of Computing.md" href="lectures/eng/l01-eng.-physics-of-computing.html" target="_self" rel="noopener nofollow">Full materials (EN)</a>
<br><a class="internal-link" data-href="Lectures/ENG/L01-NOTES-ENG. Physics of Computing.md" href="lectures/eng/l01-notes-eng.-physics-of-computing.html" target="_self" rel="noopener nofollow">Notes (EN)</a>
<br><a class="internal-link" data-href="Labs/Lab01.md" href="labs/lab01.html" target="_self" rel="noopener nofollow">Лабораторна 1</a> <br><a class="internal-link" data-href="Lectures/L02. Від вентилів до АЛП.md" href="lectures/l02.-від-вентилів-до-алп.html" target="_self" rel="noopener nofollow">Повні матеріали (UK)</a>
<br><a class="internal-link" data-href="Lectures/L02-Notes. Від вентилів до АЛП.md" href="lectures/l02-notes.-від-вентилів-до-алп.html" target="_self" rel="noopener nofollow">Нотатки (UK)</a>
<br><a class="internal-link" data-href="Lectures/ENG/L02-ENG. From Gates to ALU.md" href="lectures/eng/l02-eng.-from-gates-to-alu.html" target="_self" rel="noopener nofollow">Full materials (EN)</a>
<br><a class="internal-link" data-href="Lectures/ENG/L02-NOTES-ENG. From Gates to ALU.md" href="lectures/eng/l02-notes-eng.-from-gates-to-alu.html" target="_self" rel="noopener nofollow">Notes (EN)</a>
<br><a class="internal-link" data-href="Labs/Lab02.md" href="labs/lab02.html" target="_self" rel="noopener nofollow">Лабораторна 2</a> <br><a class="internal-link" data-href="Lectures/L03. Архітектура x86 — регістри та пам'ять.md" href="lectures/l03.-архітектура-x86-—-регістри-та-пам'ять.html" target="_self" rel="noopener nofollow">Повні матеріали (UK)</a>
<br><a class="internal-link" data-href="Lectures/L03-NOTES. Архітектура x86 — регістри та пам'ять.md" href="lectures/l03-notes.-архітектура-x86-—-регістри-та-пам'ять.html" target="_self" rel="noopener nofollow">Нотатки (UK)</a>
<br><a class="internal-link" data-href="Lectures/ENG/L03-ENG. x86 Architecture - Registers and Memory.md" href="lectures/eng/l03-eng.-x86-architecture-registers-and-memory.html" target="_self" rel="noopener nofollow">Full materials (EN)</a>
<br><a class="internal-link" data-href="Lectures/ENG/L03-NOTES-ENG. x86 Architecture - Registers and Memory.md" href="lectures/eng/l03-notes-eng.-x86-architecture-registers-and-memory.html" target="_self" rel="noopener nofollow">Notes (EN)</a>
<br><a class="internal-link" data-href="Lectures/L03-Segments. Сегментна адресація — покрокове пояснення.md" href="lectures/l03-segments.-сегментна-адресація-—-покрокове-пояснення.html" target="_self" rel="noopener nofollow">Додаток: Сегментна адресація</a>
<br><a class="internal-link" data-href="Labs/Lab03.md" href="labs/lab03.html" target="_self" rel="noopener nofollow">Лабораторна 3</a> | <a class="internal-link" data-href="Labs/Lab03-Setup.md" href="labs/lab03-setup.html" target="_self" rel="noopener nofollow">Налаштування середовища</a> <br><a class="internal-link" data-href="Lectures/L04. Архітектура x86 — основні типи інструкцій.md" href="lectures/l04.-архітектура-x86-—-основні-типи-інструкцій.html" target="_self" rel="noopener nofollow">Повні матеріали (UK)</a>
<br><a class="internal-link" data-href="Lectures/L04-NOTES. Архітектура x86 — основні типи інструкцій.md" href="lectures/l04-notes.-архітектура-x86-—-основні-типи-інструкцій.html" target="_self" rel="noopener nofollow">Нотатки (UK)</a>
<br><a class="internal-link" data-href="Lectures/ENG/L04-ENG. x86 Architecture - Basic Instruction Types.md" href="lectures/eng/l04-eng.-x86-architecture-basic-instruction-types.html" target="_self" rel="noopener nofollow">Full materials (EN)</a>
<br><a class="internal-link" data-href="Lectures/ENG/L04-NOTES-ENG. x86 Architecture - Basic Instruction Types.md" href="lectures/eng/l04-notes-eng.-x86-architecture-basic-instruction-types.html" target="_self" rel="noopener nofollow">Notes (EN)</a>
<br><a class="internal-link" data-href="Labs/Lab04.md" href="labs/lab04.html" target="_self" rel="noopener nofollow">Лабораторна 4</a> | <a class="internal-link" data-href="Labs/Lab04-TD.md" href="labs/lab04-td.html" target="_self" rel="noopener nofollow">Тест-драйв</a> <br><a class="internal-link" data-href="Lectures/L05. Організація програми — стек і процедури.md" href="lectures/l05.-організація-програми-—-стек-і-процедури.html" target="_self" rel="noopener nofollow">Повні матеріали (UK)</a>
<br><a class="internal-link" data-href="Lectures/L05-NOTES. Організація програми — стек і процедури.md" href="lectures/l05-notes.-організація-програми-—-стек-і-процедури.html" target="_self" rel="noopener nofollow">Нотатки (UK)</a>
<br><a class="internal-link" data-href="Lectures/ENG/L05-ENG. Program Organization - Stack and Procedures.md" href="lectures/eng/l05-eng.-program-organization-stack-and-procedures.html" target="_self" rel="noopener nofollow">Full materials (EN)</a>
<br><a class="internal-link" data-href="Lectures/ENG/L05-NOTES-ENG. Program Organization - Stack and Procedures.md" href="lectures/eng/l05-notes-eng.-program-organization-stack-and-procedures.html" target="_self" rel="noopener nofollow">Notes (EN)</a>
<br><a class="internal-link" data-href="Lectures/L05-Stack-Frames. Стекові фрейми — покрокове пояснення.md" href="lectures/l05-stack-frames.-стекові-фрейми-—-покрокове-пояснення.html" target="_self" rel="noopener nofollow">Додаток: Стекові фрейми</a>
<br><a class="internal-link" data-href="Labs/Lab05.md" href="labs/lab05.html" target="_self" rel="noopener nofollow">Лабораторна 5</a> <br><a class="internal-link" data-href="Lectures/L06. Системна архітектура.md" href="lectures/l06.-системна-архітектура.html" target="_self" rel="noopener nofollow">Повні матеріали (UK)</a>
<br><a class="internal-link" data-href="Lectures/L06-NOTES. Системна архітектура.md" href="lectures/l06-notes.-системна-архітектура.html" target="_self" rel="noopener nofollow">Нотатки (UK)</a>
<br><a class="internal-link" data-href="Lectures/ENG/L06-ENG. System Architecture.md" href="lectures/eng/l06-eng.-system-architecture.html" target="_self" rel="noopener nofollow">Full materials (EN)</a>
<br><a class="internal-link" data-href="Lectures/ENG/L06-NOTES-ENG. System Architecture.md" href="lectures/eng/l06-notes-eng.-system-architecture.html" target="_self" rel="noopener nofollow">Notes (EN)</a>
<br><a class="internal-link" data-href="Labs/Lab06.md" href="labs/lab06.html" target="_self" rel="noopener nofollow">Лабораторна 6</a> <br><a class="internal-link" data-href="Lectures/L07. Переривання та взаємодія з ОС.md" href="lectures/l07.-переривання-та-взаємодія-з-ос.html" target="_self" rel="noopener nofollow">Повні матеріали (UK)</a>
<br><a class="internal-link" data-href="Lectures/L07-NOTES. Переривання та взаємодія з ОС.md" href="lectures/l07-notes.-переривання-та-взаємодія-з-ос.html" target="_self" rel="noopener nofollow">Нотатки (UK)</a>
<br><a class="internal-link" data-href="Lectures/ENG/L07-ENG. Interrupts and OS Interaction.md" href="lectures/eng/l07-eng.-interrupts-and-os-interaction.html" target="_self" rel="noopener nofollow">Full materials (EN)</a>
<br><a class="internal-link" data-href="Lectures/ENG/L07-NOTES-ENG. Interrupts and OS Interaction.md" href="lectures/eng/l07-notes-eng.-interrupts-and-os-interaction.html" target="_self" rel="noopener nofollow">Notes (EN)</a>
<br><a class="internal-link" data-href="Labs/Lab07.md" href="labs/lab07.html" target="_self" rel="noopener nofollow">Лабораторна 7</a> <br><a class="internal-link" data-href="Lectures/L08. Пристрої. Ієрархія пам'яті. Структури даних на низькому рівні.md" href="lectures/l08.-пристрої.-ієрархія-пам'яті.-структури-даних-на-низькому-рівні.html" target="_self" rel="noopener nofollow">Повні матеріали (UK)</a>
<br><a class="internal-link" data-href="Lectures/L08-NOTES. Пристрої. Ієрархія пам'яті. Структури даних на низькому рівні.md" href="lectures/l08-notes.-пристрої.-ієрархія-пам'яті.-структури-даних-на-низькому-рівні.html" target="_self" rel="noopener nofollow">Нотатки (UK)</a>
<br><a class="internal-link" data-href="Lectures/ENG/L08-ENG. Memory Systems IO Devices and Low-Level Data Structures.md" href="lectures/eng/l08-eng.-memory-systems-io-devices-and-low-level-data-structures.html" target="_self" rel="noopener nofollow">Full materials (EN)</a>
<br><a class="internal-link" data-href="Lectures/ENG/L08-NOTES-ENG. Memory Systems IO Devices and Low-Level Data Structures.md" href="lectures/eng/l08-notes-eng.-memory-systems-io-devices-and-low-level-data-structures.html" target="_self" rel="noopener nofollow">Notes (EN)</a>
<br><a class="internal-link" data-href="Labs/Lab08.md" href="labs/lab08.html" target="_self" rel="noopener nofollow">Лабораторна 8</a> <br><a class="internal-link" data-href="Lectures/L09. Проєктування ISA та моделі обчислень.md" href="lectures/l09.-проєктування-isa-та-моделі-обчислень.html" target="_self" rel="noopener nofollow">Повні матеріали (UK)</a>
<br><a class="internal-link" data-href="Lectures/L09-NOTES. Проєктування ISA та моделі обчислень.md" href="lectures/l09-notes.-проєктування-isa-та-моделі-обчислень.html" target="_self" rel="noopener nofollow">Нотатки (UK)</a>
<br><a class="internal-link" data-href="Lectures/ENG/L09-ENG. ISA Design and Models of Computation.md" href="lectures/eng/l09-eng.-isa-design-and-models-of-computation.html" target="_self" rel="noopener nofollow">Full materials (EN)</a>
<br><a class="internal-link" data-href="Lectures/ENG/L09-NOTES-ENG. ISA Design and Models of Computation.md" href="lectures/eng/l09-notes-eng.-isa-design-and-models-of-computation.html" target="_self" rel="noopener nofollow">Notes (EN)</a>
<br><a class="internal-link" data-href="Labs/Lab09.md" href="labs/lab09.html" target="_self" rel="noopener nofollow">Лабораторна 9</a> <br><a class="internal-link" data-href="Lectures/L10. Огляд сучасних архітектур.md" href="lectures/l10.-огляд-сучасних-архітектур.html" target="_self" rel="noopener nofollow">Повні матеріали (UK)</a>
<br><a class="internal-link" data-href="Lectures/L10-NOTES. Огляд сучасних архітектур.md" href="lectures/l10-notes.-огляд-сучасних-архітектур.html" target="_self" rel="noopener nofollow">Нотатки (UK)</a>
<br><a class="internal-link" data-href="Lectures/ENG/L10-ENG. Overview of Modern Architectures.md" href="lectures/eng/l10-eng.-overview-of-modern-architectures.html" target="_self" rel="noopener nofollow">Full materials (EN)</a>
<br><a class="internal-link" data-href="Lectures/ENG/L10-NOTES-ENG. Overview of Modern Architectures.md" href="lectures/eng/l10-notes-eng.-overview-of-modern-architectures.html" target="_self" rel="noopener nofollow">Notes (EN)</a> <br><a class="internal-link" data-href="Lectures/L11. Ієрархія пам'яті та продуктивність.md" href="lectures/l11.-ієрархія-пам'яті-та-продуктивність.html" target="_self" rel="noopener nofollow">Повні матеріали (UK)</a>
<br><a class="internal-link" data-href="Lectures/L11-NOTES. Ієрархія пам'яті та продуктивність.md" href="lectures/l11-notes.-ієрархія-пам'яті-та-продуктивність.html" target="_self" rel="noopener nofollow">Нотатки (UK)</a>
<br><a class="internal-link" data-href="Lectures/ENG/L11-ENG. Memory Hierarchy and Performance.md" href="lectures/eng/l11-eng.-memory-hierarchy-and-performance.html" target="_self" rel="noopener nofollow">Full materials (EN)</a>
<br><a class="internal-link" data-href="Lectures/ENG/L11-NOTES-ENG. Memory Hierarchy and Performance.md" href="lectures/eng/l11-notes-eng.-memory-hierarchy-and-performance.html" target="_self" rel="noopener nofollow">Notes (EN)</a>
<br><a class="internal-link" data-href="Labs/Lab11.md" href="labs/lab11.html" target="_self" rel="noopener nofollow">Лабораторна 11</a> <br><a class="internal-link" data-href="Lectures/L12. Віртуальна пам'ять поглиблено.md" href="lectures/l12.-віртуальна-пам'ять-поглиблено.html" target="_self" rel="noopener nofollow">Повні матеріали (UK)</a>
<br><a class="internal-link" data-href="Lectures/L12-NOTES. Віртуальна пам'ять поглиблено.md" href="lectures/l12-notes.-віртуальна-пам'ять-поглиблено.html" target="_self" rel="noopener nofollow">Нотатки (UK)</a>
<br><a class="internal-link" data-href="Lectures/ENG/L12-ENG. Virtual Memory In-Depth.md" href="lectures/eng/l12-eng.-virtual-memory-in-depth.html" target="_self" rel="noopener nofollow">Full materials (EN)</a>
<br><a class="internal-link" data-href="Lectures/ENG/L12-NOTES-ENG. Virtual Memory In-Depth.md" href="lectures/eng/l12-notes-eng.-virtual-memory-in-depth.html" target="_self" rel="noopener nofollow">Notes (EN)</a>
<br><a class="internal-link" data-href="Labs/Lab12.md" href="labs/lab12.html" target="_self" rel="noopener nofollow">Лабораторна 12</a> <br><a class="internal-link" data-href="Lectures/L13. Арифметика з рухомою комою.md" href="lectures/l13.-арифметика-з-рухомою-комою.html" target="_self" rel="noopener nofollow">Повні матеріали (UK)</a>
<br><a class="internal-link" data-href="Lectures/L13-NOTES. Арифметика з рухомою комою.md" href="lectures/l13-notes.-арифметика-з-рухомою-комою.html" target="_self" rel="noopener nofollow">Нотатки (UK)</a>
<br><a class="internal-link" data-href="Lectures/ENG/L13-ENG. Floating-Point Arithmetic.md" href="lectures/eng/l13-eng.-floating-point-arithmetic.html" target="_self" rel="noopener nofollow">Full materials (EN)</a>
<br><a class="internal-link" data-href="Lectures/ENG/L13-NOTES-ENG. Floating-Point Arithmetic.md" href="lectures/eng/l13-notes-eng.-floating-point-arithmetic.html" target="_self" rel="noopener nofollow">Notes (EN)</a>
<br><a class="internal-link" data-href="Labs/Lab13.md" href="labs/lab13.html" target="_self" rel="noopener nofollow">Лабораторна 13</a> <br><a class="internal-link" data-href="Lectures/L14. Паралелізм на рівні інструкцій та потоків.md" href="lectures/l14.-паралелізм-на-рівні-інструкцій-та-потоків.html" target="_self" rel="noopener nofollow">Повні матеріали (UK)</a>
<br><a class="internal-link" data-href="Lectures/L14-NOTES. Паралелізм на рівні інструкцій та потоків.md" href="lectures/l14-notes.-паралелізм-на-рівні-інструкцій-та-потоків.html" target="_self" rel="noopener nofollow">Нотатки (UK)</a>
<br><a class="internal-link" data-href="Lectures/ENG/L14-ENG. Instruction-Level and Thread-Level Parallelism.md" href="lectures/eng/l14-eng.-instruction-level-and-thread-level-parallelism.html" target="_self" rel="noopener nofollow">Full materials (EN)</a>
<br><a class="internal-link" data-href="Lectures/ENG/L14-NOTES-ENG. Instruction-Level and Thread-Level Parallelism.md" href="lectures/eng/l14-notes-eng.-instruction-level-and-thread-level-parallelism.html" target="_self" rel="noopener nofollow">Notes (EN)</a>
<br><a class="internal-link" data-href="Labs/Lab14.md" href="labs/lab14.html" target="_self" rel="noopener nofollow">Лабораторна 14</a> <br><a class="internal-link" data-href="Labs/Contest.md" href="labs/contest.html" target="_self" rel="noopener nofollow">Конкурс</a>
<br><a class="internal-link" data-href="Labs/Project.md" href="labs/project.html" target="_self" rel="noopener nofollow">Проєкт</a>
<br><a class="internal-link" data-href="Labs/Project-Report.md" href="labs/project-report.html" target="_self" rel="noopener nofollow">Звіт проєкту</a>
<br><a class="internal-link" data-href="Labs/Project-Defense.md" href="labs/project-defense.html" target="_self" rel="noopener nofollow">Захист проєкту</a>
<br><a class="internal-link" data-href="Projects/README.md" href="projects/readme.html" target="_self" rel="noopener nofollow">Список проєктів</a> <br><a class="internal-link" data-href="midterm/README.md" href="midterm/readme.html" target="_self" rel="noopener nofollow">Мідтерм</a>
<br><a class="internal-link" data-href="final/README.md" href="final/readme.html" target="_self" rel="noopener nofollow">Фінальна контрольна</a> <br><a class="internal-link" data-href="Office-hours/Консультація - 2026-01-17.md" href="office-hours/консультація-2026-01-17.html" target="_self" rel="noopener nofollow">Консультація — 2026-01-17</a>
<br><a class="internal-link" data-href="Office-hours/Консультація - 2026-01-24.md" href="office-hours/консультація-2026-01-24.html" target="_self" rel="noopener nofollow">Консультація — 2026-01-24</a>
<br><a class="internal-link" data-href="Office-hours/Консультація - 2026-01-31.md" href="office-hours/консультація-2026-01-31.html" target="_self" rel="noopener nofollow">Консультація — 2026-01-31</a>
<br><a class="internal-link" data-href="Office-hours/Консультація - 2026-02-07.md" href="office-hours/консультація-2026-02-07.html" target="_self" rel="noopener nofollow">Консультація — 2026-02-07</a>
<br><a class="internal-link" data-href="Office-hours/Консультація - 2026-02-14.md" href="office-hours/консультація-2026-02-14.html" target="_self" rel="noopener nofollow">Консультація — 2026-02-14</a>
<br><a class="internal-link" data-href="Office-hours/Консультація - 2026-02-21.md" href="office-hours/консультація-2026-02-21.html" target="_self" rel="noopener nofollow">Консультація — 2026-02-21</a>
<br><a class="internal-link" data-href="Office-hours/Консультація - 2026-02-28.md" href="office-hours/консультація-2026-02-28.html" target="_self" rel="noopener nofollow">Консультація — 2026-02-28</a>
<br><a class="internal-link" data-href="Office-hours/Консультація - 2026-03-07.md" href="office-hours/консультація-2026-03-07.html" target="_self" rel="noopener nofollow">Консультація — 2026-03-07</a>
<br><a class="internal-link" data-href="Office-hours/Консультація - 2026-03-14.md" href="office-hours/консультація-2026-03-14.html" target="_self" rel="noopener nofollow">Консультація — 2026-03-14</a>
<br><a class="internal-link" data-href="Office-hours/Консультація - 2026-03-21.md" href="office-hours/консультація-2026-03-21.html" target="_self" rel="noopener nofollow">Консультація — 2026-03-21</a>
<br><a class="internal-link" data-href="Office-hours/Консультація - 2026-03-28.md" href="office-hours/консультація-2026-03-28.html" target="_self" rel="noopener nofollow">Консультація — 2026-03-28</a>
<br><a class="internal-link" data-href="Office-hours/Консультація - 2026-04-04.md" href="office-hours/консультація-2026-04-04.html" target="_self" rel="noopener nofollow">Консультація — 2026-04-04</a>
<br><a class="internal-link" data-href="Office-hours/Консультація - 2026-04-11.md" href="office-hours/консультація-2026-04-11.html" target="_self" rel="noopener nofollow">Консультація — 2026-04-11</a>
<br><a class="internal-link" data-href="Office-hours/Консультація - 2026-04-18.md" href="office-hours/консультація-2026-04-18.html" target="_self" rel="noopener nofollow">Консультація — 2026-04-18</a> <br><a class="internal-link" data-href="Syllabus.md" href="syllabus.html" target="_self" rel="noopener nofollow">Syllabus.md</a> — Силабус курсу
<br><a class="internal-link" data-href="Lectures/ENG/Glossary-CSA.md" href="lectures/eng/glossary-csa.html" target="_self" rel="noopener nofollow">Glossary-CSA.md</a> — Глосарій термінів EN→UK
]]></description><link>readme.html</link><guid isPermaLink="false">Readme.md</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate></item><item><title><![CDATA[L15-bigLITTLE-Global-Task-Scheduling]]></title><description><![CDATA[<img src="assets/l15-biglittle-global-task-scheduling.jpg" target="_self">]]></description><link>assets/l15-biglittle-global-task-scheduling.html</link><guid isPermaLink="false">assets/L15-bigLITTLE-Global-Task-Scheduling.jpg</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-biglittle-global-task-scheduling.jpg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-biglittle-global-task-scheduling.jpg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-bigLITTLE-In-Kernel-Switcher]]></title><description><![CDATA[<img src="assets/l15-biglittle-in-kernel-switcher.jpg" target="_self">]]></description><link>assets/l15-biglittle-in-kernel-switcher.html</link><guid isPermaLink="false">assets/L15-bigLITTLE-In-Kernel-Switcher.jpg</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-biglittle-in-kernel-switcher.jpg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-biglittle-in-kernel-switcher.jpg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-bigLITTLE-Cortex-A57-A53]]></title><description><![CDATA[<img src="assets/l15-biglittle-cortex-a57-a53.jpg" target="_self">]]></description><link>assets/l15-biglittle-cortex-a57-a53.html</link><guid isPermaLink="false">assets/L15-bigLITTLE-Cortex-A57-A53.jpg</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-biglittle-cortex-a57-a53.jpg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-biglittle-cortex-a57-a53.jpg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-bigLITTLE-Cluster-Switching]]></title><description><![CDATA[<img src="assets/l15-biglittle-cluster-switching.png" target="_self">]]></description><link>assets/l15-biglittle-cluster-switching.html</link><guid isPermaLink="false">assets/L15-bigLITTLE-Cluster-Switching.png</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-biglittle-cluster-switching.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-biglittle-cluster-switching.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-Scaling-Laws-Timeline]]></title><description><![CDATA[<img src="assets/l15-scaling-laws-timeline.svg" target="_self">]]></description><link>assets/l15-scaling-laws-timeline.html</link><guid isPermaLink="false">assets/L15-Scaling-Laws-Timeline.svg</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-scaling-laws-timeline.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-scaling-laws-timeline.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-Scaling-Laws-Timeline-ENG]]></title><description><![CDATA[<img src="assets/l15-scaling-laws-timeline-eng.svg" target="_self">]]></description><link>assets/l15-scaling-laws-timeline-eng.html</link><guid isPermaLink="false">assets/L15-Scaling-Laws-Timeline-ENG.svg</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-scaling-laws-timeline-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-scaling-laws-timeline-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-Race-to-Idle-vs-Slow-Steady]]></title><description><![CDATA[<img src="assets/l15-race-to-idle-vs-slow-steady.svg" target="_self">]]></description><link>assets/l15-race-to-idle-vs-slow-steady.html</link><guid isPermaLink="false">assets/L15-Race-to-Idle-vs-Slow-Steady.svg</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-race-to-idle-vs-slow-steady.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-race-to-idle-vs-slow-steady.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-Race-to-Idle-vs-Slow-Steady-ENG]]></title><description><![CDATA[<img src="assets/l15-race-to-idle-vs-slow-steady-eng.svg" target="_self">]]></description><link>assets/l15-race-to-idle-vs-slow-steady-eng.html</link><guid isPermaLink="false">assets/L15-Race-to-Idle-vs-Slow-Steady-ENG.svg</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-race-to-idle-vs-slow-steady-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-race-to-idle-vs-slow-steady-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-Power-Gating-vs-Clock-Gating]]></title><description><![CDATA[<img src="assets/l15-power-gating-vs-clock-gating.svg" target="_self">]]></description><link>assets/l15-power-gating-vs-clock-gating.html</link><guid isPermaLink="false">assets/L15-Power-Gating-vs-Clock-Gating.svg</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-power-gating-vs-clock-gating.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-power-gating-vs-clock-gating.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-Power-Gating-vs-Clock-Gating-ENG]]></title><description><![CDATA[<img src="assets/l15-power-gating-vs-clock-gating-eng.svg" target="_self">]]></description><link>assets/l15-power-gating-vs-clock-gating-eng.html</link><guid isPermaLink="false">assets/L15-Power-Gating-vs-Clock-Gating-ENG.svg</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-power-gating-vs-clock-gating-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-power-gating-vs-clock-gating-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-PUE-Breakdown]]></title><description><![CDATA[<img src="assets/l15-pue-breakdown.svg" target="_self">]]></description><link>assets/l15-pue-breakdown.html</link><guid isPermaLink="false">assets/L15-PUE-Breakdown.svg</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-pue-breakdown.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-pue-breakdown.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-PUE-Breakdown-ENG]]></title><description><![CDATA[<img src="assets/l15-pue-breakdown-eng.svg" target="_self">]]></description><link>assets/l15-pue-breakdown-eng.html</link><guid isPermaLink="false">assets/L15-PUE-Breakdown-ENG.svg</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-pue-breakdown-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-pue-breakdown-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-Moores-Law-Transistor-Count]]></title><description><![CDATA[<img src="assets/l15-moores-law-transistor-count.png" target="_self">]]></description><link>assets/l15-moores-law-transistor-count.html</link><guid isPermaLink="false">assets/L15-Moores-Law-Transistor-Count.png</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-moores-law-transistor-count.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-moores-law-transistor-count.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-HBM-Schematic]]></title><description><![CDATA[<img src="assets/l15-hbm-schematic.svg" target="_self">]]></description><link>assets/l15-hbm-schematic.html</link><guid isPermaLink="false">assets/L15-HBM-Schematic.svg</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-hbm-schematic.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-hbm-schematic.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-Heterogeneous-SoC]]></title><description><![CDATA[<img src="assets/l15-heterogeneous-soc.svg" target="_self">]]></description><link>assets/l15-heterogeneous-soc.html</link><guid isPermaLink="false">assets/L15-Heterogeneous-SoC.svg</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-heterogeneous-soc.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-heterogeneous-soc.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-Heterogeneous-SoC-ENG]]></title><description><![CDATA[<img src="assets/l15-heterogeneous-soc-eng.svg" target="_self">]]></description><link>assets/l15-heterogeneous-soc-eng.html</link><guid isPermaLink="false">assets/L15-Heterogeneous-SoC-ENG.svg</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-heterogeneous-soc-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-heterogeneous-soc-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-HBM-Fiji-GPU-Package]]></title><description><![CDATA[<img src="assets/l15-hbm-fiji-gpu-package.jpg" target="_self">]]></description><link>assets/l15-hbm-fiji-gpu-package.html</link><guid isPermaLink="false">assets/L15-HBM-Fiji-GPU-Package.jpg</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-hbm-fiji-gpu-package.jpg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-hbm-fiji-gpu-package.jpg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-Future-Tech-Landscape]]></title><description><![CDATA[<img src="assets/l15-future-tech-landscape.svg" target="_self">]]></description><link>assets/l15-future-tech-landscape.html</link><guid isPermaLink="false">assets/L15-Future-Tech-Landscape.svg</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-future-tech-landscape.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-future-tech-landscape.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-Gustafsons-Law]]></title><description><![CDATA[<img src="assets/l15-gustafsons-law.png" target="_self">]]></description><link>assets/l15-gustafsons-law.html</link><guid isPermaLink="false">assets/L15-Gustafsons-Law.png</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-gustafsons-law.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-gustafsons-law.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-Data-Center-Racks]]></title><description><![CDATA[<img src="assets/l15-data-center-racks.jpg" target="_self">]]></description><link>assets/l15-data-center-racks.html</link><guid isPermaLink="false">assets/L15-Data-Center-Racks.jpg</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-data-center-racks.jpg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-data-center-racks.jpg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-Future-Tech-Landscape-ENG]]></title><description><![CDATA[<img src="assets/l15-future-tech-landscape-eng.svg" target="_self">]]></description><link>assets/l15-future-tech-landscape-eng.html</link><guid isPermaLink="false">assets/L15-Future-Tech-Landscape-ENG.svg</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-future-tech-landscape-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-future-tech-landscape-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-Data-Center-Cold-Aisle]]></title><description><![CDATA[<img src="assets/l15-data-center-cold-aisle.jpg" target="_self">]]></description><link>assets/l15-data-center-cold-aisle.html</link><guid isPermaLink="false">assets/L15-Data-Center-Cold-Aisle.jpg</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-data-center-cold-aisle.jpg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-data-center-cold-aisle.jpg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-Dark-Silicon-Chip]]></title><description><![CDATA[<img src="assets/l15-dark-silicon-chip.svg" target="_self">]]></description><link>assets/l15-dark-silicon-chip.html</link><guid isPermaLink="false">assets/L15-Dark-Silicon-Chip.svg</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-dark-silicon-chip.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-dark-silicon-chip.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-Dark-Silicon-Chip-ENG]]></title><description><![CDATA[<img src="assets/l15-dark-silicon-chip-eng.svg" target="_self">]]></description><link>assets/l15-dark-silicon-chip-eng.html</link><guid isPermaLink="false">assets/L15-Dark-Silicon-Chip-ENG.svg</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-dark-silicon-chip-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-dark-silicon-chip-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-DVFS-Curve]]></title><description><![CDATA[<img src="assets/l15-dvfs-curve.svg" target="_self">]]></description><link>assets/l15-dvfs-curve.html</link><guid isPermaLink="false">assets/L15-DVFS-Curve.svg</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-dvfs-curve.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-dvfs-curve.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-DVFS-Curve-ENG]]></title><description><![CDATA[<img src="assets/l15-dvfs-curve-eng.svg" target="_self">]]></description><link>assets/l15-dvfs-curve-eng.html</link><guid isPermaLink="false">assets/L15-DVFS-Curve-ENG.svg</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-dvfs-curve-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-dvfs-curve-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-Chiplet-Package]]></title><description><![CDATA[<img src="assets/l15-chiplet-package.svg" target="_self">]]></description><link>assets/l15-chiplet-package.html</link><guid isPermaLink="false">assets/L15-Chiplet-Package.svg</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-chiplet-package.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-chiplet-package.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-Chiplet-Package-ENG]]></title><description><![CDATA[<img src="assets/l15-chiplet-package-eng.svg" target="_self">]]></description><link>assets/l15-chiplet-package-eng.html</link><guid isPermaLink="false">assets/L15-Chiplet-Package-ENG.svg</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-chiplet-package-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-chiplet-package-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-Course-Arc]]></title><description><![CDATA[<img src="assets/l15-course-arc.svg" target="_self">]]></description><link>assets/l15-course-arc.html</link><guid isPermaLink="false">assets/L15-Course-Arc.svg</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-course-arc.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-course-arc.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-Course-Arc-ENG]]></title><description><![CDATA[<img src="assets/l15-course-arc-eng.svg" target="_self">]]></description><link>assets/l15-course-arc-eng.html</link><guid isPermaLink="false">assets/L15-Course-Arc-ENG.svg</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-course-arc-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-course-arc-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-CPU-Clock-Core-Count]]></title><description><![CDATA[<img src="assets/l15-cpu-clock-core-count.png" target="_self">]]></description><link>assets/l15-cpu-clock-core-count.html</link><guid isPermaLink="false">assets/L15-CPU-Clock-Core-Count.png</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-cpu-clock-core-count.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-cpu-clock-core-count.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-CMOS-Power-Breakdown]]></title><description><![CDATA[<img src="assets/l15-cmos-power-breakdown.svg" target="_self">]]></description><link>assets/l15-cmos-power-breakdown.html</link><guid isPermaLink="false">assets/L15-CMOS-Power-Breakdown.svg</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-cmos-power-breakdown.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-cmos-power-breakdown.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-Amdahls-Law]]></title><description><![CDATA[<img src="assets/l15-amdahls-law.svg" target="_self">]]></description><link>assets/l15-amdahls-law.html</link><guid isPermaLink="false">assets/L15-Amdahls-Law.svg</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-amdahls-law.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-amdahls-law.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-CMOS-Power-Breakdown-ENG]]></title><description><![CDATA[<img src="assets/l15-cmos-power-breakdown-eng.svg" target="_self">]]></description><link>assets/l15-cmos-power-breakdown-eng.html</link><guid isPermaLink="false">assets/L15-CMOS-Power-Breakdown-ENG.svg</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-cmos-power-breakdown-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-cmos-power-breakdown-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-3D-Die-Stacking-Concept]]></title><description><![CDATA[<img src="assets/l15-3d-die-stacking-concept.png" target="_self">]]></description><link>assets/l15-3d-die-stacking-concept.html</link><guid isPermaLink="false">assets/L15-3D-Die-Stacking-Concept.png</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-3d-die-stacking-concept.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-3d-die-stacking-concept.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-Amdahls-Law-ENG]]></title><description><![CDATA[<img src="assets/l15-amdahls-law-eng.svg" target="_self">]]></description><link>assets/l15-amdahls-law-eng.html</link><guid isPermaLink="false">assets/L15-Amdahls-Law-ENG.svg</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-amdahls-law-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-amdahls-law-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15. Енергоефективність, обмеження та майбутні тренди]]></title><description><![CDATA[
Фінальна лекція курсу «Архітектура комп'ютерних систем» у НаУКМА. Аргументи в ній кумулятивні — кожна секція відповідає на питання, що поставила попередня.
Після цієї лекції ви зможете:
Застосовувати закони Амдала та Густафсона, щоб вирішити, чи виграє навантаження від збільшення паралелізму, і пояснювати різницю між сильним і слабким масштабуванням (LO5)
Міркувати про енергетичний бюджет чипа на основі моделі потужності CMOS (динамічна, статична, наскрізна) і пояснювати, чому DVFS, dark silicon та гетерогенні ядра — це все відповіді на ту саму фізичну межу (LO5, LO6)
Аргументовано обстоювати чи критикувати проєктне рішення — велике ядро проти LITTLE-ядра, монолітний кристал проти чіплета, універсальний CPU проти спеціалізованого прискорювача — з огляду на продуктивність, енергію, вартість і стійкість (LO7)
15 тижнів тому ви не знали, що транзистор робить всередині NAND-вентиля. Сьогодні ви можете простежити вказівник від змінної C через таблицю сторінок, побачити його промах у L1 і повільне блукання до DRAM, помітити, як це завантаження застрягає суперскалярний конвеєр, побачити, як передбачувач переходів помиляється та скидає спекулятивний стан, і поміркувати, чи мала вся ця послідовність виконуватися на P-ядрі, E-ядрі, а чи взагалі деінде. Кожна лекція цього курсу відкривала один регіон машини — фізику вентилів (L01), системи команд (L02, L03), пам'ять (L04, L08), операційні системи та віртуальну пам'ять (L06, L07, L09), кеші (L11), числа з рухомою комою (L12), конвеєризацію та спекуляцію (L14). Кожен із цих регіонів свого часу був рубежем. Ця лекція інша. Це момент, коли ми відступаємо і запитуємо, де ми зараз, чому ландшафт має саме таку форму і куди зрушився рубіж далі.Ось картина, яка вміщує майже всю історію. Відкладіть кількість транзисторів на логарифмічній осі від 1970 року до сьогодні й нанесіть кожен CPU, який знайдете. Крива піднімається майже ідеально прямою лінією — закон Мура, який досі тримається: темп сповільнюється, але не зупиняється, а сучасний GPU уже перетинає 90 мільярдів транзисторів на одному кристалі. Це перша з трьох кривих.<img alt="Закон Мура: кількість транзисторів на мікропроцесорі з 1970 до 2020 року, подвоєння приблизно кожні два роки в логарифмічному масштабі." src="assets/l15-moores-law-transistor-count.png" target="_self" style="width: 700px; max-width: 100%;">
Закон Мура — крива, що не зупинилася. Графік з Our World in Data через Wikimedia Commons (CC-BY).Тепер нанесіть ще дві криві на тій самій часовій шкалі. Нанесіть тактову частоту флагманських CPU — число на коробці, ті ГГц, якими всі колись хизувалися. Ця крива круто росте від 1 МГц у 1980-х до приблизно 3 ГГц у 2004-му, а тоді, майже за одну ніч, зупиняється. Уже двадцять років вона дрейфує вбік у смузі 3–5 ГГц. І нанесіть кількість ядер на чип. Ця крива була мертвим плато на позначці 1,0 з 1970 до 2004 року, а тоді рвонула у свою власну експоненту — 2, 4, 8, 16, 64, 96 ядер у корпусі до 2024 року.<br><img alt="Тактова частота CPU виходить на плато після 2005 року, тоді як кількість ядер починає власне експоненційне зростання — постденнардівська біфуркація." src="assets/l15-cpu-clock-core-count.png" target="_self" style="width: 700px; max-width: 100%;">
Тактова частота проти кількості ядер. Крива частоти вирівнюється близько 2005 року; крива кількості ядер злітає того самого року. Графік через Wikimedia Commons.<br><img alt="Три криві на одній часовій шкалі: кількість транзисторів продовжує зростати, тактова частота виходить на плато після 2005 року, кількість ядер починає власний експоненційний підйом." src="assets/l15-scaling-laws-timeline.svg" target="_self" style="width: 700px; max-width: 100%;">Три криві. Транзистори й далі піднімаються. Частота пласка з 2005-го. Кількість ядер росте з 2005-го. Це і є загадка, довкола якої побудовано цю лекцію: що сталося у 2005 році і що індустрія робила з цим відтоді? Щось зламалося, а точніше — щось перестало бути безкоштовним. І кожен архітектурний тренд, про який ви читали за останні двадцять років — багатоядерність, SIMD, гетерогенні чипи, чіплети, GPU, що поглинають світ ШІ, Apple Silicon, який обганяє Intel на акумуляторі ноутбука — це відповідь на той злам.Лекція розгортається у п'ять тактів. Перший — як далеко нас може завести паралелізм? — Амдал і Густафсон, два закони, що окреслюють, на що здатне і нездатне додавання ядер. Другий — чому саме енергія є межею? — рівняння потужності CMOS, DVFS і явище dark silicon, через яке ваш 96-ядерний чип не може одночасно ганяти всі 96 ядер на повній швидкості. Третій — що індустрія з цим насправді зробила? — гетерогенні ядра, чіплети, 3D-стекування, спеціалізовані прискорювачі. Четвертий — у що це обходиться планеті? — енергія, вода, вуглець та електронні відходи дата-центрів. П'ятий — куди далі? — короткий огляд пост-кремнієвого питання і прикінцеве слово про те, які навички з цього курсу залишаться з вами в тому, що буде далі.Це остання лекція, а отже — й останній раз, коли ви сидите саме в цій аудиторії саме з таким кадруванням. Позиція, яку ми тренували весь семестр — читати архітектуру як набір компромісів, зроблених під тиском обмежень, а не як специфікацію для зазубрювання — це саме та позиція, яка потрібна, щоб читати новини про 2-нм техпроцес TSMC, чи про наступне покоління чіплетів AMD, чи про свіжий TPU від Google, чи про будь-який фотонний стартап, який наступного місяця підніме Series A. На карті тепер є розгалуження. У вас є інструменти, щоб прочитати обидві гілки.Після цієї секції ви можете: прочитати маркетингову заяву про сучасний CPU і визначити, на яку з пост-2005 сил — стеля паралелізму, енергетична стіна, dark silicon, гетерогенність, спеціалізація — ця заява відповідає.Ви переписали фоторедактор на 8 ядер M1. Очікували 8× прискорення. Отримали 2,5×. Одногрупник дивиться на ваш бенчмарк, знизує плечима й каже: «схоже на помилку — можливо, у тебе зламаний пул потоків або десь ти серіалізуєш роботу на блокуванні». Чи правий ваш одногрупник?Можливо. Але є нюанс: навіть якщо ваш код бездоганний, 2,5× може бути саме тим числом, яке передбачає математика. І це, до речі, не вигаданий сценарій — якщо ви колись міряли час збірки make -j8 на ноутбуці й помічали, що вона завершувалася приблизно в 3 рази швидше замість 8, ви вже переживали цю загадку на собі. Щоб зрозуміти чому, треба чесно з'ясувати, що насправді означає «можна розпаралелити» у реальній програмі.Візьміть будь-яке реальне навантаження й запитайте, куди насправді йде його час. Ваш фоторедактор відкриває файл (послідовно — одна головка диска, один файловий дескриптор). Розкодовує заголовок JPEG (послідовно — не можна розкодувати 100-й байт, доки не знаєш значення 50-го). Розбиває масив пікселів на 8 плиток (тривіально паралельно — по плитці на ядро, незалежна робота). Зливає результати назад у єдиний вихідний буфер (переважно послідовно, з дрібкою атомарного обліку). Записує вихідний файл (знову послідовно). Паралельне ядро швидко виконується на 8 ядрах, але сидить усередині сендвіча з послідовного «хліба», і цей хліб не прискорюється від того, що ви додали ядер.Позначмо частку часу, яку програма проводить у коді, придатному для розпаралелювання, як p, а частку в суто послідовному коді — як (1 − p). Дамо паралельній частині N ядер, і вона завершиться за час p/N замість p. Послідовна частина не змінюється й усе ще забирає (1 − p). Загальний час виконання скорочується з 1 доа прискорення — це відношення старого часу до нового:Це Закон Амдала, опублікований Джином Амдалом у 1967 році. Підставимо числа. Припустимо, ваш фоторедактор на 60% розпаралелюваний (крок розбиття на плитки) і на 40% послідовний (усе інше). За N = 8 ядер:Ваш одногрупник помилявся. Виміряні 2,5× — це не помилка; це зовсім близько до стелі, яку вже встановила послідовна частка. Якби ви побачили 8×, ось це було б несподіванкою.<br><img alt="Криві прискорення за законом Амдала для різних часток паралельного коду. Асимптота стоїть на 1/(1−p), скільки б процесорів ви не додали." src="assets/l15-amdahls-law.svg" target="_self" style="width: 600px; max-width: 100%;">
Закон Амдала. Діаграма з Wikimedia Commons (CC-BY-SA).Тепер доведемо формулу Амдала до межі. Спрямуймо N у нескінченність. Паралельний доданок p/N згортається в нуль, і прискорення прямує доЦе і є стеля прискорення: скільки б ядер ви не купили, послідовна частка встановлює жорстку стелю на те, що може дати вам паралелізм. Посидьте з цими числами трохи, бо вони жорстокі:Кожна додаткова «дев'ятка» у частці паралельного коду купує вам ще один порядок прискорення. І кожну наступну дев'ятку здобути важче, ніж попередню — послідовні шматки, що лишаються, це ті, які ви не змогли розпаралелити з першого разу, зазвичай тому, що там реальні залежності за даними, а не тому, що ви лінувалися. Саме тому оптимізація «найбільшого шматка» часу виконання перемагає оптимізацію «найлегшого шматка» з великим відривом: якщо 75% програми — у паралельному регіоні й ви прискорите цей регіон нескінченно, то загалом отримаєте 4×. Якщо у паралельному регіоні лише 25% — то максимум 1,33×. Обирайте битви за часткою часу, а не за тим, наскільки цікавим виглядає код.<br><img alt="Оптимізація більшої частини задачі дає більший загальний виграш у часі, ніж оптимізація меншої частини, навіть якщо локальне покращення меншої частини більше." src="assets/l15-optimizing-different-parts.svg" target="_self" style="width: 600px; max-width: 100%;">
Оптимізація там, де насправді сидить час. Діаграма з Wikimedia Commons (CC-BY-SA).Якщо стеля Амдала аж така жорстка, як каже ця таблиця, розумна людина мусить поставити очевидне наступне питання: чому ж Google витратив приблизно 100 мільярдів доларів на дата-центри, напхані ядрами? Чому NVIDIA продає стійки з десятками тисяч графічних прискорювачів? Чому вся хмарна індустрія побудована на горизонтальному масштабуванні? Або ж уся галузь спільно помиляється щодо цінності паралелізму, або Амдал розповідає не всю історію.Правильна відповідь — друга. Закон Амдала відповідає на конкретне питання: за фіксованої задачі — наскільки швидше вона завершиться з більшою кількістю ядер? Це легітимне питання, і для деяких навантажень воно єдине, що справді має значення — транзакція, яка має закомітитися за 5 мс, кадр гри, що має відрендеритися за 16 мс, запит, який має відповісти за 100 мс. Але це не єдине питання. Усередині того ж слова «прискорення» ховається зовсім інше питання — і саме на нього насправді відповідає хмара з горизонтальним масштабуванням.У 1988 році Джон Густафсон та Едвін Барсіс у Sandia National Laboratories опублікували коротку статтю під назвою «Переоцінка закону Амдала» («Reevaluating Amdahl's Law»). Їхнє спостереження було оманливо простим: дослідники не використовують швидші машини, щоб розв'язати ту саму задачу швидше. Вони використовують їх, щоб розв'язати більші задачі за той самий час. Коли у Sandia з'явилася машина з 1024 процесорами, фізики не стали запускати торішню симуляцію з 64 процесорами в 16 разів швидше. Вони запустили цьогорічну симуляцію — з дрібнішою сіткою, більшою кількістю частинок, довшим часовим інтервалом — у те саме «за ніч». Паралельна частина зростала разом із залізом. Послідовна частина залишалася приблизно такою ж.Якщо зафіксувати час за годинником і масштабувати навантаження разом із N, математика вивертається навиворіт. Нехай s — частка астрономічного часу, яку масштабоване навантаження проводить у послідовному коді, а p = 1 − s — частка, що припадає на паралельний код. Одному послідовному процесору знадобиться час s + p × N, щоб виконати всю роботу (бо паралельна частина, яка тепер виконується на N процесорах, становить у N разів більше роботи, ніж один процесор міг би впоратися сам). Отже, прискорення порівняно з послідовною машиною дорівнюєЦе Закон Густафсона, і дивовижне в ньому те, що S зростає лінійно з N. Стелі немає. Подвоїли кластер — подвоїли розмір задачі — подвоїли ефективну пропускну здатність; за умови, що послідовна частина справді лишається сталою, поки паралельна робота розширюється.Підставмо число, щоб відчути різницю. З тим самим «60% паралельним» профілем, що й у нашого фоторедактора, але переосмисленим у дусі Густафсона — тобто 60% астрономічного часу припадає на паралельну роботу, а послідовні 40% залишаються фіксованими — 8 ядер дають вамефективної пропускної здатності порівняно з одним ядром, що виконує ту саму задачу за той самий час за годинником. А за N = 64 Густафсон обіцяє 38,8×, тоді як Амдал усе ще впирається у стелю біля 2,46×. Той самий профіль навантаження — дико різні вироки, бо два закони вимірюють дві різні речі.<br><img alt="Криві прискорення за законом Густафсона. Коли розмір задачі зростає разом із кількістю процесорів, прискорення зростає лінійно з N." src="assets/l15-gustafsons-law.png" target="_self" style="width: 600px; max-width: 100%;">
Закон Густафсона. Діаграма з Wikimedia Commons (CC-BY-SA).Амдал і Густафсон не суперечать одне одному; вони відповідають на різні питання. Обидва закони правильні, і вибір того, який із них застосовувати, залежить від того, що саме ви намагаєтеся зробити.Формальні назви цих двох питань — сильне масштабування і слабке масштабування. Сильне масштабування (світ Амдала). Задача фіксована, кількість процесорів змінюється. «Наскільки швидко я зможу розв'язати ЦЮ задачу?» Навантаження фіксоване — запит, кадр, транзакція. Додавання ядер скорочує час за годинником, але впирається у стелю 1/(1−p). Слабке масштабування (світ Густафсона). Розмір задачі зростає разом із кількістю процесорів. «Наскільки велику задачу я зможу розв'язати за розумний час?» Робота на один процесор фіксована — кожне ядро зберігає приблизно той самий обсяг роботи. Додавання ядер дає змогу братися за більші або точніші задачі в тому ж бюджеті часу за годинником. Кілька життєвих прикладів, щоб закріпити обидва випадки:
Вебпошук — ви вводите запит, хочете отримати результати менш ніж за 100 мс, корпус такий, який є. Це сильне масштабування. Затримка — це обмеження; додавання машин допомагає лише тоді, коли вони скорочують критичний шлях, і стеля Амдала цілком реальна.
Кліматична симуляція — фізики хочуть порахувати «за ніч», хай там що ця «ніч» їм дасть. Дайте їм у 10 разів більше ядер — і вони не завершать швидше; вони запустять дрібнішу сітку, довшу симуляцію або більше ансамблевих членів у ті ж 8 годин. Це слабке масштабування, і лінійне зростання Густафсона — саме те, що вони спостерігають.
Навчання великих ML-моделей — у команди бюджет 24 години (бо саме тоді має стартувати наступний експеримент). Дайте їм у 4 рази більше графічних прискорювачів — і вони не навчать модель у 4 рази швидше; вони навчать модель у 4 рази більшу або на обсязі даних, у 4 рази більшому, за той самий день. Світ Густафсона.
Кодування відео на ноутбуці — фіксований 90-хвилинний фільм, ви хочете, щоб кодування завершилося до вечері. Сильне масштабування; стеля Амдала діє.
Тест, який варто ставити собі щоразу: коли я додаю залізо, задача залишається того ж розміру — чи зростає? Якщо залишається того ж розміру, ви у світі Амдала, і стеля реальна. Якщо зростає, ви у світі Густафсона, і можете масштабуватися й далі. Гіперскейлери живуть у світі Густафсона свідомо — вони проєктують свої навантаження так, щоб ті масштабувалися слабко, бо це єдиний режим, у якому витратити 100 мільярдів на ядра має економічний сенс.Ні Амдал, ні Густафсон не враховують того, що насправді відбувається, коли ви склеюєте 1000 ядер докупи: пропускна здатність пам'яті насичується, трафік когерентності кешів вибухає, блокування починають конкурувати за ресурс, а затримки NUMA кусаються. Універсальний закон масштабованості (USL) Ніла Ґантера додає два штрафні доданки — лінійний за суперництво та квадратичний за когерентність — і його передбачення некомфортне: після певного N реальні системи не просто перестають прискорюватися. Вони уповільнюються. Додавання 1001-го ядра може зробити весь кластер гіршим, ніж версія з 1000 ядер. Якщо ви колись бачили бенчмарк бази даних, де пропускна здатність сягає піку при 32 з'єднаннях, а потім деградує, коли ви тиснете до 64 і 128, — ви вже бачили USL на графіку. Це не теоретична дивина; саме тому у продакшн-системах є «солодка точка» за кількістю ядер, після якої продуктивність падає, і саме тому інженери розподілених систем заробляють свої зарплати, приборкуючи накладні витрати на когерентність і координацію. Ми не виводитимемо USL тут, але тримайте його в голові: Амдал — це стеля оптиміста, Густафсон — переосмислення в дусі «більше завжди краще», а USL — чесний песиміст, який нагадує, що в реального заліза є тертя.Ми щойно встановили, що паралелізм має стелю і що масштабування самої задачі може зсунути цю стелю вбік, але не усунути. Чого ми ще не пояснили — це фізичну причину, чому архітектори взагалі припинили нарощувати частоту й розвернулися до ядер. Злам 2005 року на тих трьох кривих стався не тому, що в когось закінчилися ідеї — а тому, що у фізики закінчився запас. Енергія — це наступний розділ, і саме вона вирішує, у якому з цих двох законів вам доведеться жити.Після цієї секції ви можете: оцінити теоретичну стелю прискорення для навантаження за його послідовною часткою; визначити, у якому світі — Амдала чи Густафсона — перебуває конкретна реальна система (вебпошук, кліматична симуляція, навчання ML-моделей, кодування відео); пояснити, чому стратегія «купимо більше ядер» працює для гіперскейлерів, але часто провалюється для навантажень із фіксованою затримкою.Ось загадка, яка має вас зачепити. Відкрийте даташит мобільного процесора кінця 1990-х — його ядро живиться від напруги близько 3,3 В. Відкрийте даташит SoC смартфона 2010 року — вже близько 1,2 В. Відкрийте даташит SoC 2024 року — і ви побачите шини ядра нижче 0,8 В, а окремі стани глибокого сну підповзають до 0,6 В. Двадцять років розробники чипів — в Intel, AMD, Qualcomm, Apple, ARM — запекло боролися за те, щоб опустити робочу напругу вниз. Частота й так масштабувалась сама собою. Навіщо витрачати стільки інженерних зусиль саме на напругу?Відповідь криється в одному члені однієї формули. Але сама формула має сенс лише після того, як ми поставимо попереднє питання: куди насправді йде потужність у чипі і чому існує стеля того, скільки її можна витратити?Усе, що робить чип, втрачає значення, якщо він розплавиться. Це не драматизація — це головне обмеження. Кожен перемикальний транзистор розсіює енергію у вигляді тепла, і це тепло має залишити корпус через якусь комбінацію тепло­розподільника, радіатора, вентилятора, рідинного контуру або металевого корпусу вашого смартфона. Якщо тепло надходить швидше, ніж відводиться, температура кремнію росте, і десь між 95 °C і 105 °C транзистори починають поводитися неправильно — спершу повільнішають і «течуть», потім стають ненадійними, а потім чип засмажує сам себе.Тому кожен корпус має параметр TDP (проєктна теплова потужність) — стале розсіювання потужності, на яке розраховане охолодження. У процесора ноутбука TDP може бути 28 Вт. У настільного ігрового CPU — 170 Вт. У SoC смартфона — 5 Вт. У серверного GPU — 700 Вт. Це число не є рекомендацією. Це стеля, під якою мусять жити всі подальші архітектурні рішення. Спроектуйте ядро, що споживає 300 Вт, — і краще відразу кладіть у коробку кулер на 300 Вт. А в якийсь момент фізика просто відмовиться співпрацювати з вашим радіатором.Саме тому TDP задає рамки для всього подальшого. Dark silicon (§5), DVFS (§4), гетерогенні ядра (§6), чіплети (§8) — усе це, у своїй основі, способи виконати більше корисної роботи в межах фіксованого теплового конверта.Сумарне розсіювання потужності CMOS-чипа розкладається на три доданки:P_total = P_dynamic + P_static + P_short-circuitПерший — це плата за виконану роботу. Другий — плата за саме існування. Третій — невеликий перехідний податок, який ми здебільшого ігноруватимемо. Побудуймо перший доданок з нуля, бо саме він має бути у вас у голові.Кожен логічний елемент у CMOS-чипі керує якоюсь ємністю навантаження C — ємністю затворів транзисторів, які він живить, плюс ємністю провідників між ними. Коли вихід елемента перемикається з 0 у 1, він мусить зарядити C від землі до напруги живлення V. Заряд, який перетікає, дорівнює Q = CV. Енергія, відібрана від джерела на цей заряд, дорівнює E = QV = CV², причому половина з неї запасається в конденсаторі, а друга половина розсіюється як тепло у керувальному транзисторі. Перемикання назад з 1 у 0 викидає запасену половину теж у тепло. Отже, повний цикл 0→1→0 коштує рівно CV² розсіяної енергії — це ваша енергія на одне перемикання.А як часто відбувається це перемикання? Такт іде f разів за секунду. Якби елемент перемикався щотакту, він розсіював би CV²f ват. Але більшість елементів так не роблять — більшість бітів у тракті даних не перекидаються щотакту. Позначмо частку тактів, на якій цей елемент справді перемикається, через коефіцієнт активності α. Просумуйте по всіх елементах чипа — і матимете класичну формулу:P_dynamic = α · C · V² · fТри рядки. Заряд CV, енергія CV², повторюємо f разів на секунду на α-частці елементів.Подивіться на формулу ще раз. Частота входить лінійно. Ємність — лінійно. Активність — лінійно. Напруга входить у квадраті. Саме цей квадрат і є відповіддю на загадку з початку розділу.V² — це ключовий член. Зменшення V вдвічі знижує динамічну потужність у 4 рази, бо квадрат домінує. Порахуймо вголос: V → V/2 означає V² → V²/4, тож P_dynamic падає в 4 рази ще до того, як ви торкнетесь частоти. Перейдіть із 3,3 В на 1,1 В — тобто втричі — і лише член V² дасть вам 9-кратне зменшення динамічної потужності. Тепер видно, чому інженери так затято ганялися за напругою впродовж двадцяти років. Це єдиний важіль у формулі з надлінійною віддачею. Зменшення ємності теж допомагає (технологічні вузли її й зменшують), але зменшення ємності — помірні й важко здобуті. Зменшення напруги — чистий множник.Той самий квадрат і є причиною, чому DVFS — тема §4 — настільки ефективний. Спойлер: коли ви знижуєте V і f разом, економія виходить кубічною, а не лінійною. Скористаємось цим у наступній секції.<br><img alt="Розкладання потужності CMOS з 130 нм до 5 нм: динамічна потужність досі переважає, але частка витоків (статичної) зростає з 5% до 45% за два десятиліття масштабування." src="assets/l15-cmos-power-breakdown.svg" target="_self" style="width: 700px; max-width: 100%;">α — не фіксоване число. Воно дико змінюється між різними частинами чипа, і ця мінливість сама по собі є проєктною можливістю.Дерево тактової розводки перемикається щотакту, за визначенням. Його α практично дорівнює 1,0. А оскільки мережа розподілу тактового сигналу розходиться на кожен тригер на кристалі, вона має величезну ємність. Виміряйте реальні чипи — і побачите, що саме дерево тактової розводки відповідає за 20–30% усієї динамічної потужності. Це колосальний рахунок за сигнал, який, з погляду самих обчислень, жодної роботи не виконує — він просто метроном.Саме тому clock gating (вимкнення тактового сигналу на неактивний блок) є універсальною практикою в сучасних проєктах. Якщо функціональний блок у цьому такті нічого не робить — ALU простоює, передбачувач переходів ніхто не питав, відеодекодер не запущений — прошивка або апаратна логіка AND-схемою відсікає такт у корені піддерева цього блоку. Тригери перестають перемикатися. Їхній α падає до нуля. Їхній внесок у динамічну потужність зникає доти, доки такт заблокований. Кожен серйозний чип агресивно відсікає такт на кількох рівнях деталізації: на рівні функціонального блока, ядра, кластера.З логікою даних — інша історія. У середньому α для звичайних елементів тракту даних становить близько 0,1 — більшість бітів не перекидається щотакту, бо більшість даних не змінюється так часто. Регістр із лічильником циклу постійно перемикає молодші біти, але старші — майже ніколи; провідник із вказівником зазвичай тримає те саме значення багато тактів поспіль. Знання того, що α низький, дозволяє розробникам використовувати багато комбінаційної логіки без пропорційної плати за потужність.Динамічна потужність — це рахунок за роботу. Статична потужність — рахунок за існування. Навіть коли транзистор не перемикається — коли такт заблоковано, коли ядро простоює, коли телефон лежить у вашій кишені — електрони все одно знаходять шлях крізь нього, і цей струм, помножений на напругу, розсіюється у вигляді тепла.Свій внесок роблять три механізми:
Підпороговий витік. Навіть коли напруга на затворі нижча за порогову V_th, що нібито має «закрити» транзистор, невеликий струм просочується від витоку до стоку. Цей струм експоненційно залежить від V_th — опустіть V_th на сто мілівольт, і витік може зрости на порядок. Підпороговий витік є домінуючим членом на 45 нм і нижче.
Витік через затвор. Щойно підзатворний оксид стає достатньо тонким — тонше ніж приблизно 2 нм — електрони починають тунелювати крізь нього, а не блокуватися. Це була криза в районі вузла 65 нм, яку розв'язали починаючи з 45 нм — замінивши діоксид кремнію на діелектрик з високою діелектричною проникністю (high-κ, оксид гафнію); це дає змогу використовувати фізично товстіший шар з тим самим електричним ефектом. Intel першою випустила це в Penryn (2007).
Витік зворотно зміщеного p-n переходу. Невеликі струми крізь зворотно зміщені p-n переходи в підкладці. Невеликий внесок порівняно з попередніми двома.
Ось пастка, і її не можна обійти проєктуванням: меншим транзисторам потрібна нижча V_th, щоб швидко перемикатися за нижчих напруг живлення. Але експоненційний зв'язок між V_th і підпороговим витоком означає, що кожне зменшення, яке допомагає швидкості, водночас шкодить витокам. FinFET і транзистори з охопленням затвором з усіх боків (GAAFET) частково повертають ситуацію назад, даючи затвору кращий електростатичний контроль над каналом, але фундаментальні лещата нікуди не діваються. На 7 нм і нижче статична потужність легко може сягати 30–50% загальної потужності чипа — повний переворот кругової діаграми порівняно з ерою 130 нм, коли витоки були заокругленням.Це двері в один бік. Не можна повернутися до товстіших транзисторів, не відмовившись від виграшу в щільності, заради якого ми взагалі робили зменшення. Витоки — це постійний податок, який ми платимо за те, щоб закон Мура продовжував кульгати далі.Досі ми говорили про потужність, виміряну у ватах, — миттєву швидкість розсіювання енергії. Для смартфона на акумуляторі насправді важлива енергія, вимірювана в джоулях:E = P · tСаме енергія висаджує акумулятор. Потужність — гріє чип. Їх пов'язує час, і цей зв'язок відкриває стратегію, яка інакше видалася б парадоксальною.Припустімо, у вас задача на 10 секунд. Її можна виконати на 2 Вт упродовж 10 секунд (разом 20 Дж) або на 4 Вт упродовж 5 секунд (теж 20 Дж). Енергія однакова — розряд акумулятора однаковий. Поки що нічия. Але подивіться, що відбувається в решту п'ять секунд другого сценарію. Якщо чип може повністю вимкнути живлення на час простою, його потужність падає до майже нуля. Друга стратегія завершує роботу, повністю вимикається і заощаджує ту енергію, яку перша стратегія досі зливає через витоки.Це і є race-to-idle — «швидко відпрацювати — і в сон». Відпрацюй швидко, закінчи раніше, вимкнися. Це працює за умови, що у чипа значні витоки (бо простоювати активно коштує енергії), і за умови, що він вміє вимикати живлення достатньо глибоко, щоб потужність у стані простою справді наближалася до нуля. Обидві умови виконуються на сучасних мобільних SoC, і там race-to-idle є домінуючою стратегією.Протилежна стратегія, «тихою сапою» (slow-and-steady), тримає напругу й частоту низькими, розтягує роботу в часі та покладається на те, що квадратична економія V² переважить усе інше. Ця стратегія виграє на чипах із малими витоками або з поганим вимкненням живлення.Загостримо обидві в §4 — це два кінці простору політик DVFS, — але ключове спостереження тут ось яке: «витратити менше потужності» і «витратити менше енергії» — це різні питання. Політика мусить обирати, що саме оптимізувати.Після цієї секції ви можете: прочитати даташит зі значеннями TDP, діапазоном напруги й діапазоном частоти та передбачити, який важіль — зниження напруги чи зниження частоти — дасть більший виграш в енергії на одиницю втраченої продуктивності.Ви всі це відчували: записуєте 4K-відео 10 хвилин сонячного дня або 20 хвилин ганяєте вимогливу гру на телефоні — і в якийсь момент пристрій помітно нагрівається, частота кадрів падає, камера тихо переходить на 1080p, гра починає підгальмовувати. Це не через акумулятор — ви перевіряли. Не через пам'ять. Закінчується тепловий запас. SoC наближається до теплової межі, і контур замкненого керування втручається, щоб не дати йому розплавитись. Цей механізм — DVFS (динамічне масштабування напруги та частоти), і він, імовірно, є найважливішою технікою керування живленням у сучасних обчисленнях.Усе в цій секції випливає з одного спостереження, яке ми підготували в §3.4 і тепер реалізуємо. Динамічна потужність квадратична за V і лінійна за f:P_dynamic = α · C · V² · fА ще є фізичний зв'язок, який ми досі не назвали прямо: максимальна частота, на якій може працювати логічний елемент, приблизно лінійно залежить від напруги живлення. Вище V — транзистори перемикаються швидше, більший струм керування, крутіші фронти через порогову напругу, менша затримка поширення через кожен каскад. Тож коли знижуєте V, треба знижувати й максимальну f, на якій ще можна надійно тактувати. Обидва важелі рухаються разом.Дивіться, що це робить із потужністю. Припустімо, ми ділимо навпіл і V, і f. Член V² стає (V/2)² = V²/4 — падіння вчетверо. Член f стає f/2 — падіння вдвічі. Перемножимо:Ділимо V і f навпіл → P_dynamic падає у 4 × 2 = 8 разів.Ви віддаєте приблизно половину миттєвої продуктивності й отримуєте назад сім восьмих потужності. Це фундаментальний важіль ефективності сучасних обчислень, і саме тому DVFS лежить у серці кожного пристрою на акумуляторі, кожного серверного чипа, кожної ігрової консолі у збалансованому режимі. Жодна інша ручка в інструментарії розробника CPU не має й близько такого плеча.Саме тому в статтях з енергоефективності як скорочення пишуть «кубічна за напругою». Оскільки f масштабується приблизно лінійно з V, динамічна потужність масштабується як V² · f ≈ V³. На реальному чипі це не точно куб — залежність V-f має кривину й нижні обмеження, — але кубічна ментальна модель достатньо близька для інтуїції.Чип не підтримує довільні комбінації (V, f). Під час характеризації кремнію на заводі інженери зондують кожен чип у багатьох точках напруги, знаходять найвищу частоту, на якій він ще проходить функціональні тести з запасом безпеки, і бінують результат. У продаж іде набір валідованих робочих точок — дискретних пар на кшталт (1,40 В; 5,0 ГГц), (1,20 В; 4,0 ГГц), (1,00 В; 3,0 ГГц), (0,85 В; 2,0 ГГц), (0,70 В; 800 МГц). З'єднайте ці точки — і отримаєте робочу криву V-f чипа.<br><img alt="Робочий діапазон DVFS з точками P-станів від P0 (максимум продуктивності) до Pn (простою), нижня межа V_min, під якою транзистори відмовляють, і турбо-область над сталим конвертом." src="assets/l15-dvfs-curve.svg" target="_self" style="width: 620px; max-width: 100%;">Унизу кривої живе жорстка нижня межа V_min. Нижче приблизно 0,6–0,7 В на сучасних технологіях теплові шуми — випадкові коливання електронів на масштабі теплової енергії транзистора — стають достатньо великими порівняно з сигналом, щоб час від часу перекидати біти. Перекидання бітів у кеші — це не плавна деградація. Не можна просто знизити V до нуля заради ще більшої економії: V_min — це стіна, поставлена фізикою, а не проєктним вибором.Зверху живе теплова стеля. Теоретично можна задати V довільно високою і тактувати довільно швидко, але член динамічної потужності V² й експоненційний член витоків ростуть разом, і ви впираєтесь у TDP раніше, ніж у фізичну швидкісну межу транзисторів. Тож крива має точку максимальної продуктивності, точку мінімальної продуктивності і рідкісну драбину валідованих зупинок між ними.Індустрія стандартизувала цей інтерфейс ще в 1990-х під назвою ACPI (розширений інтерфейс конфігурації та керування живленням) — саме звідти походить більшість термінології.P-стани (стани продуктивності) — це іменовані сходинки драбини V-f. P0 — завжди налаштування з найвищою продуктивністю. P1, P2, P3, …, Pn йдуть донизу по кривій. ОС або прошивка обирає, який P-стан чинний зараз, а регулятори напруги й генератори тактів реагують за мікросекунди.C-стани (стани простою ЦП) — це стани нижче активності: C0 — «виконання інструкцій» (єдиний, де актуальні P-стани), C1 — «зупинено, але готовий», а глибші стани (C3, C6, C7, …) послідовно вимикають такти, скидають кеші й повністю вимикають живлення цілих ядер. Що глибший C-стан, то довше з нього прокидатись — з C6 може знадобитись 30–50 мікросекунд. Саме тому це хвилює планувальники: вихід з глибокого C-стану має латентну ціну, яку треба зважувати щодо економії на витоках.Окреме DVFS на кожне ядро — сучасне уточнення. Ранні багатоядерні чипи ділили одну площину V-f на всі ядра. Сучасні (Intel починаючи з Haswell, Apple Silicon, сучасні AMD Ryzen) дозволяють кожному ядру незалежно обирати свій P-стан. Один потік на одному ядрі може працювати на 5 ГГц, тоді як сусідне простоює на 800 МГц, а блок керування живленням SoC узгоджує їх із спільним бюджетом TDP.У системі Linux шар політики видно безпосередньо. Підсистема cpufreq експонує «регулятори» — іменовані стратегії, які вирішують, який P-стан запитати:
performance — завжди P0. Максимум частоти, максимум напруги, максимум потужності. Використовується для задач, чутливих до затримки, де краще спалити вати, ніж чекати.
powersave — завжди найнижчий P-стан. Вибір для роботи від акумулятора, коли чуйність не важить.
ondemand — періодично замірює завантаження CPU; стрибає до P0 на сплеску навантаження, згортається на простої. Класична адаптивна політика з 2000-х.
schedutil — сучасне усталене налаштування в більшості дистрибутивів. Інтегрується прямо з планувальником, тож рішення про частоту можуть використовувати той самий сигнал завантаженості, який планувальник застосовує для розміщення потоків. Значно плавніший за ondemand і правильне усталене для задач загального призначення.
Прямо зараз на вашому ноутбуці команда cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor покаже, який з них активний. На акумуляторі це часто powersave або schedutil; від мережі може перемкнутися на performance.«Турбо» — маркетингова назва для хитрощів DVFS: коли корпус холодний і активні лише кілька ядер, прошивка тимчасово штовхає одне-два з них вище номінальної сталої частоти. Intel Turbo Boost, AMD Precision Boost, динамічний буст Apple — всі про одне й те саме.Чистий спосіб мислити про турбо: номінальна «базова частота» на коробці — це гірший сталий випадок під повним навантаженням на TDP. Коли навантаження легке, з'являється тепловий запас. Турбо — це коли прошивка конвертує цей запас у додаткову швидкість на зайнятих ядрах, доки температура корпусу не підніметься достатньо, щоб запасу не лишилось. Тоді прошивка відступає.Саме тому однопотокові бенчмарки часто показують вищі частоти, ніж базова в даташиті, — бо вони розігрівають лише одне ядро і дають турбо його штовхнути. Тому ж один і той самий CPU у прохолодній серверній переграє той самий CPU у гарячій комірчині: тепловий запас — це ресурс, який турбо витрачає, а що холодніше довкола, то більше цього ресурсу.Тепер повернімось до вашого телефона на сонці. SoC має температурний сенсор — насправді їх кілька, розподілених по кристалу. Прошивка читає ці сенсори багато разів на секунду. Коли будь-який з них наближається до T_max (зазвичай 95–105 °C), блок керування живленням скидає чип на нижчий P-стан незалежно від того, чого хоче задача. Якщо цього мало — скидає ще нижче. Якщо й цього досі мало — паркує ядра.Це замкнений контур керування. Сигналом помилки є (Tпоточна − Tцільова); вихід керування — налаштування P-стану; стала часу — мілісекунди до десятків мілісекунд. Жодне програмне чаклунство не перебиває цього на рівні застосунку — механізм сидить під ОС, у прошивці, яку застосунок не бачить. Ваш запис 4K обрізають не тому, що Android вирішив вас покарати, — а тому, що SoC фізично захищається від розплавленого паяного з'єднання.Коли ви заносите телефон усередину й він охолоджується, температури падають, контур керування знімає обмеження на P-стан, і продуктивність повертається. Це повна відповідь на спостереження з початку розділу: DVFS — це те, що рятує ваш пристрій від сонця, а тепловий троттлінг — це DVFS у найавторитарнішому режимі.Наприкінці §3 ми залишили питання про енергетичну стратегію відкритим. DVFS — це саме та ручка, яка його розв'язує.<br><img alt="Race-to-idle проти «тихою сапою»: дві криві потужності в часі. Race-to-idle виграє, коли витоки великі; «тихою сапою» виграє, коли витоки малі." src="assets/l15-race-to-idle-vs-slow-steady.svg" target="_self" style="width: 760px; max-width: 100%;">Race-to-idle обирає високий P-стан, розчавлює задачу за мінімальний час і скидає чип у глибокий C-стан на решту інтервалу. Ця стратегія виграє, коли витоки великі (простоювати коштує енергії, тож що швидше можна вимкнути живлення, то краще) і коли вимкнення живлення в чипа достатньо глибоке, щоб потужність у простої справді була близька до нуля. Сучасні SoC смартфонів, де статична потужність може становити 30%+ від активного споживання, а агресивне вимкнення здатне зупинити майже все, — це машини race-to-idle за замовчуванням.«Тихою сапою» обирає найнижчий P-стан, який ще встигає до дедлайну, розтягує задачу на довший час і використовує економію V² нижчої робочої точки. Ця стратегія виграє, коли витоки малі (тож довше працювати — майже нічого не коштує), або коли задача і так має виконуватися безперервно (відеодекодер живого потоку).Рішення не універсальне, і планувальники стають дедалі вигадливішими. schedutil у Linux і планувальник Apple використовують апаратні лічильники продуктивності — частоту промахів кешу, частку простоїв на пам'яті, чи задача обмежена обчисленнями, чи вводом-виводом, — щоб на рівні конкретного завдання підказати, яку стратегію застосувати. Потік, обмежений пам'яттю, мало що виграє від високої частоти (він все одно чекає на DRAM), тож тут виграє «тихою сапою». Обчислювально навантажений потік справді виграє — тож тут виграє race-to-idle.І ось передача естафети до §5. DVFS розв'язує енергетичну проблему для однієї задачі одночасно — сповільни або мчи, обери P-стан, заощадь джоулі. Але тепловий конверт — властивість усього чипа, а не однієї задачі. Коли у серверному CPU 96 ядер і TDP корпусу не покриває всіх них одночасно на повну, навіть DVFS на кожному окремому ядрі не рятує. Доводиться залишати частину ядер вимкненими. Це й приводить нас до dark silicon.Після цієї секції ви можете: пояснити, чому той самий CPU видає різні результати бенчмарків у гарячій і прохолодній кімнаті; обрати правильний регулятор для задачі, чутливої до заряду акумулятора; передбачити, для якої задачі більше підходить race-to-idle, а для якої — «тихою сапою».Загадка на початок. Сучасний серверний чіп AMD EPYC має 96 ядер. Чому не можна одночасно запустити всі 96 на 5 ГГц?Справа не в провідниках і не в пропускній здатності пам'яті. Причина та сама, через яку ваш телефон троттлить, коли ви знімаєте 4K-відео під прямим сонцем, — тепло. Просто масштабоване на три порядки й запаковане в серверне шасі.Загадка, яку телефон розв'язує через DVFS (динамічне масштабування напруги та частоти), — це та сама загадка, яку чіп у дата-центрі розв'язує через архітектуру. Далі ми побачимо, чому корпус 96-ядерного EPYC за задумом є чіпом, який мусить працювати переважно повільніше, ніж на це здатні його транзистори, — або залишати великі ділянки без живлення в будь-який момент часу.Саме це явище в індустрії називають dark silicon, і це найбільше архітектурне обмеження в дизайні чіпів від 2005 року. Кожен тренд, який вам трапиться в решті лекції, — big.LITTLE, прискорювачі, чіплети, 3D-стекування — у тій чи іншій формі є відповіддю на нього.<br><img alt="Тактова частота не росте від 2005 року, тоді як кількість ядер продовжує збільшуватися — видимий відбиток dark silicon. Масштабувати частоту в межах того самого теплового бюджету вже не вийде, тож потужність розподіляють між більшою кількістю ядер на нижчих швидкостях." src="assets/l15-cpu-clock-core-count.png" target="_self" style="width: 700px; max-width: 100%;">
Плоска лінія тактової частоти та зростальна лінія ядер — це одна й та сама історія: dark silicon.Ви це вже бачили в L06, тож повторно виводити не будемо. Версія в одному абзаці: 1974 року Роберт Деннард вивів, що коли зменшуєш транзистор у S разів і в стільки ж разів знижуєш напругу, потужність на транзистор падає в S² разів, а кількість транзисторів на мм² зростає в S². Ці два множники точно скорочуються — густина потужності залишається сталою.Зменшуєш транзистори, отримуєш їх більше, платиш той самий тепловий рахунок. Саме ця угода робила кожне покоління закону Мура з 1974-го до приблизно 2004 року відчуттям безкоштовного обіду: подвоюй кількість транзисторів і зберігай той самий рівень тепла.Поки ця угода діяла, архітекторам не доводилося обирати між «більше транзисторів» і «більше тепла». Вони отримували і те, і те безкоштовно, а цю щедрість витрачали на ширші конвеєри, більші кеші, глибші ROB та вищі тактові частоти.Уся історія, яку ви простежили через L10 і L14, — суперскалярність, позачергове виконання, спекуляція — тримається на припущенні Деннарда, що густина потужності — це чиясь чужа проблема. Так воно й було. Аж поки не перестало.Зламалися дві речі, і зламалися разом.По-перше, напруга перестала масштабуватися. Нижче приблизно 0,7 В тепловий шум на затворі починає випадково перемикати транзистори, і ви вже не можете гарантувати, що «1» залишиться одиницею. Фізична межа непоступлива — вона походить від kT/q за кімнатної температури, а кімнатну температуру не переможеш інженерією.Інженери продовжували вигризати частки вольта (1,1 В → 1,0 В → 0,9 В → 0,8 В), але крива вирівнювалася, щойно V_min наближалася. Якщо не можна знижувати V, не можна дотримати угоду Деннарда, бо вся вона спиралася на те, що квадратичний член V² у динамічній потужності зменшується з кожним стисненням.По-друге, витоки полізли вгору. Менші транзистори потребують нижчої порогової напруги для швидкого перемикання, а підпороговий витік експоненційно залежить від порогової напруги (ви бачили це в §3). На 45 нм і нижче статична потужність перестала бути похибкою округлення й перетворилася на 30–50 % загального бюджету потужності. Чіп, який нічого не робить, усе одно «кровить» ватами.Експоненційна форма — найжорстокіша частина: кожні додаткові 60 мВ зменшення V_th множать витоки приблизно вдесятеро, тож «трошки понизити V_th» не вийде без великої ціни. Діелектрики з високою κ (Penryn від Intel, 2007) усунули витоки через затвор; підпороговий витік не усунула жодна технологія.Складіть ці два чинники — і угода Деннарда розвалюється. Потужність на транзистор перестає спадати.Ви все ще можете втиснути більше транзисторів у мм² — фотолітографія продовжує стискатися: з 45 нм до 28, 14, 7, 5, 3, — але густина потужності тепер росте з кожним стисненням замість залишатися сталою. Транзистори влазять. Тепло — ні.Саме тому крива тактової частоти, яку ви бачили на початку L10 (і на початку цієї лекції), вирівнялася близько 2005 року й відтоді не зрушила з місця. Індустрія не лінувалася. Її притиснуло.Dark silicon — це частка транзисторів на чіпі, яка в будь-який заданий момент мусить залишатися без живлення, щоб сумарне розсіяння потужності не перевищило проєктної теплової потужності (TDP) корпусу.«Dark» тут не синонім до «зламаний» чи «змарнований». Темні транзистори цілком справні — вони засвітяться на наступному навантаженні, коли натомість згасне якась інша ділянка.Чіп — це фотографія того, які ділянки активні саме зараз; наступної мілісекунди фотографія інша. З часом працює кожна частина чіпа; у будь-який окремий момент значна його частка — тьмяна.Ключову думку варто тримати в голові: TDP — це головне обмеження. Кожне проєктне рішення витікає з нього. Якщо ви маєте бюджет у 100 Вт на корпус, ви маєте фіксований енергетичний рахунок, а архітектура — це мистецтво розумно витрачати цей рахунок.<br><img alt="48-плитковий кристал чіпа, де 12 ядер активні, 20 — притлумлені (знижені V/f), а 16 — з вимкненим живленням. Візуалізація обмеженої за TDP активної площі, яка й визначає dark silicon." src="assets/l15-dark-silicon-chip.svg" target="_self" style="width: 640px; max-width: 100%;"><br>2011 року Есмаелзаде, Блем, Сент-Амант, Санкаралінгам і Бургер опублікували <a data-tooltip-position="top" aria-label="https://ieeexplore.ieee.org/document/6307772" rel="noopener nofollow" class="external-link is-unresolved" href="https://ieeexplore.ieee.org/document/6307772" target="_self">нині знамениту статтю з ISCA</a>, у якій спрогнозували розвиток dark silicon на майбутні техпроцеси. На 8 нм — приблизно сьогоднішній передній край логіки — автори оцінили, що в будь-який момент 50–80 % транзисторів чіпа доведеться тримати темними — залежно від архітектури, навантаження та охолодження.Коли стаття вийшла, її сприйняли як сигнал тривоги. Половина вашого чіпа — темна? Це звучало як катастрофа для закону Мура: навіщо й далі стискати, якщо не можна скористатися тим, що стиснуто?Десять років по тому можна сказати, що прогноз по суті був правильним, а індустрія не стільки «розв'язала» dark silicon, скільки переспроєктувалася довкола нього.Кожен тренд, про який ви прочитаєте в §6, §7 і §8 — гетерогенність, прискорювачі, чіплети, — походить із того, що прогноз 2011-го справдився.Коли дивишся на фото кристала сучасного SoC телефона, більшу частину площі займають спеціалізовані блоки (ISP, NPU, відеокодек, DSP, захищений анклав), які більшу частину часу простоюють.Коли дивишся на серверний ЦП, видно, що стабільна частота за активних усіх ядер різко нижча, ніж турбо на одному ядрі, — бо засвітити всі ядра в турбо-режимі означало б перевищити TDP за лічені секунди.Стаття 2011-го подала dark silicon як кризу; через десятиліття індустрія просто уклала з ним мир і збудувала довкола нього свій дизайн.Ви вже бачили dark silicon у кожному пристрої, яким користуєтеся; просто не мали для нього назви. Воно зʼявляється у трьох місцях: усередині одного чіпа між ядрами, усередині одного чіпа між ядрами та прискорювачами, а також усередині одного ядра між різними виконавчими блоками. Усі три — той самий принцип, застосований на різних рівнях деталізації.Турбо-режим. ЦП вашого ноутбука рекламують як «до 5,0 ГГц», але стабільна частота за активних усіх ядер — радше 3,8 ГГц. Саме цей розрив — dark silicon у дії.Коли двом потокам потрібно швидко завершитися, чіп підіймає ці два ядра до 5 ГГц і тихенько притлумлює решту чотирнадцять. Якби всі шістнадцять працювали на 5 ГГц, корпус перевищив би свій TDP приблизно за секунду, і тепловий регулятор однаково стягнув би всіх назад. Турбо — це просто планувальник, який робить цей компроміс явним.Графічні процесори. У сучасному GPU понад 16 000 CUDA-ядер. Вони не працюють на 5 ГГц. Вони працюють на 1,5–2,5 ГГц.Дизайн міняє тактову частоту на паралелізм — багато блоків на скромній частоті, а не кілька на самій межі. Це структурна відповідь на dark silicon, зроблена на архітектурному рівні, а не на рівні планувальника. GPU були dark-silicon-дизайнами ще до того, як цим терміном хтось послуговувався.SoC телефонів. Відкрийте застосунок камери. Засвічується ISP, вмикається конвеєр обробки зображення сенсора камери, можливо, NPU запускає модель розпізнавання облич, а ЦП падає на одне простоювальне ядро на 600 МГц.Закрийте камеру, відкрийте браузер. ISP гасне, великі ядра ЦП засвічуються, NPU простоює. У кожний момент часу більшість транзисторів вашого телефона споживають майже нульову потужність. Активні лише ті блоки, чия робота зараз доречна.Серверні ЦП. 96-ядерний EPYC із загадки на початку має стабільну частоту за активних усіх ядер близько 2,4–3,0 ГГц і одноядерне турбо до 5 ГГц. Це не виробничий дефект — це свідоме обмеження.TDP корпусу (приблизно 400 Вт для топових SKU) не потягне 96 ядер на 5 ГГц; математика не сходиться за жодного реалістичного охолодження. Тож чіп тримає більшість своїх ядер більшість часу на частоті, помітно нижчій за їхні транзисторні можливості, а ті, що все ж працюють на 5 ГГц, отримують таку привілею лише тому, що їхні сусіди — тьмяні.Усередині одного ядра. Навіть у межах одного OoO-ядра не всі виконавчі блоки засвічуються одночасно. Векторний блок AVX-512 у P-ядрі Intel здатний тягнути стільки струму, що історично тривала робота AVX-512 змушувала ядро знижувати частоту (сумнозвісне «частотне зміщення AVX-512»).Блок існував; бюджету потужності, щоб запускати його на повній частоті, — ні. Dark silicon на рівні блока — причина, з якої маркетингові характеристики та реальна стала продуктивність розходяться на спеціалізованих інструкціях.Усі чотири ці приклади — турбо-режим, GPU, SoC телефонів, серверні ЦП, а також внутрішній устрій одного ядра — це реакції на те саме фізичне обмеження. Назви різняться. Обмеження — ні.Dark silicon — це обмеження, а не техніка. Техніки — це способи, якими архітектори навчилися з ним жити.Кожну з них можна уявляти як іншу ставку щодо того, де має бути темна частка і що з цим робити чіпу. П'ять із них домінують у сучасному дизайнерському арсеналі:1. Тьмяний кремній. Запускати багато блоків на низькій напрузі й частоті, а не кілька на максимумі. Саме тому кількість ядер зросла, а частота на ядро застигла.Ви бачили цю схему в L14: коли ILP уперся в стелю всередині одного ядра, індустрія розвернулася до TLP між багатьма ядрами. Dark-silicon-версія цієї логіки каже: багато повільних ядер дають меншу густину потужності, ніж кілька швидких ядер, які виконують те саме сумарне навантаження.2. Вимкнення живлення (power gating). Повністю відрізати VDD від неактивного блока, знижуючи його статичний витік практично до нуля. Затримка пробудження — порядку мікросекунд (треба перезарядити шини живлення та відновити стан). Застосовують до цілих ядер і великих периферійних блоків.3. Clock gating («вимкнення тактового сигналу на неактивний блок»). Заморозити такт для неактивного блока, усуваючи його динамічну потужність (α → 0), але залишаючи витоки недоторканими. Пробудження фактично миттєве (один такт).Застосовують із дрібним ступенем деталізації всередині майже кожного сучасного цифрового блока — сам лише клокове дерево споживає ~20–30 % потужності чіпа, тож не подавати на щось такт — один із найдешевших виграшів у всьому арсеналі.4. Спеціалізація. Домен-специфічний блок у 10–100 разів енергоефективніший за універсальний ЦП на своєму домені. Якщо більша частина чіпа однаково має бути темною, варто хоча б зробити темну частку корисною, коли вона випадково засвітиться.Саме ця логіка веде NPU, відеокодеки та криптоприскорювачі. §6 підхоплює цю нитку та йде глибше.5. Гетерогенні ядра. Припасувати ядро до навантаження. Фоновий завантажувач пошти на великому ядрі марнує енергію на OoO-механіку, яка йому не потрібна; переставте його на мале ефективне ядро — і звільните бюджет потужності великого ядра для чогось, що цього справді потребує. §6 повністю присвячена цій відповіді.<br><img alt="*Clock gating* (AND-вентиль на тактовій лінії, VDD завжди увімкнено) проти вимкнення живлення (pMOS-транзистор сну повністю відрізає VDD). *Clock gating* економить динамічну потужність із миттєвим пробудженням; вимкнення живлення економить усе, але пробудження триває мікросекунди." src="assets/l15-power-gating-vs-clock-gating.svg" target="_self" style="width: 760px; max-width: 100%;">Ці підходи не взаємовиключні. Справжній чіп використовує всі п'ять водночас.Сучасний Apple M-серії має гетерогенні ядра, десятки спеціалізованих блоків, агресивний clock gating повсюдно, вимкнення живлення для великих невикористовуваних блоків і покластерний DVFS, що тримає кожне ядро на найнижчій частоті, яка ще встигає в дедлайни. Проєктна задача тут — не «обери одне», а «вдало скомпонуй усі п'ять».Dark silicon — це те, чому кожен архітектурний тренд наступного десятиліття має той вигляд, який має. Якщо не можна ввімкнути все одразу, наступне питання — що саме вмикати і на якому саме блоці? Саме це обговорює решта лекції.Після цієї секції ви можете: пояснити, чому існує турбо-режим і чому стабільна частота за активних усіх ядер завжди нижча за пік на одному ядрі; прочитати фото кристала SoC телефона і передбачити, які блоки темні в будь-який момент часу; а також чітко сформулювати, чому виробники чіпів хизуються кількістю ядер, але тихо обмежують стабільні тактові частоти.Причинна теза на початку. Якщо §5 має слушність, тобто ми не можемо ввімкнути все одразу, то однорідна стратегія «більше великих ядер» — програшна. Конструкція з шістнадцятьма однаковими високопродуктивними ядрами витрачає майже весь свій енергобюджет на ядра, які більшість часу виконують задачі, що не потребують усієї цієї машинерії.Ви радше хотіли б мати суміш: кілька потужних ядер на випадок, коли потрібна однопотокова латентність, багато слабких ядер для пропускної здатності на ват, а також спеціалізовані блоки для випадків, коли одну конкретну річ треба зробити дуже ефективно.Гетерогенність — не хитрий архітектурний прийом. Це змушена відповідь на стіну dark silicon.Кожен великий виробник дійшов того самого висновку між 2011 і 2021 роками, причому з протилежних боків.ARM прийшла з мобільного (обмеженого батареєю) боку. Intel — з настільного (обмеженого TDP). Apple — з позиції вертикальної інтеграції, яка дозволила робити і те, і інше одночасно.Те, що вони зійшлися, не випадковість. Це форма самого обмеження.Ви вже зустрічали цю ідею двічі. L10 §3 подав CPU vs GPU як дві точки на кривій «латентність — пропускна здатність». L14 §6.5 зазирнув усередину Apple M1 і згадав поділ P-ядро / E-ядро як анонс.Ця секція підхоплює там, де ті зупинилися. Пропустимо повторення «що таке big.LITTLE» і підемо глибше — у режими планування, які зробили гетерогенні чипи реально придатними до використання, у математику ефективності, яка забезпечує їм перевагу, і в програмний податок, через який вони залишаються складними.Решта цієї секції пройде трьома конкретними втіленнями (ARM, Intel, Apple), розгляне мобільний SoC як каталог, а потім присвятить одну чесну підсекцію вартості програмування — бо якщо ви не винесете з §6 нічого іншого, винесіть той факт, що гетерогенність — це архітектурна перемога, сплачена програмною складністю, якою ви, інженере, маєте керувати.<br>ARM випустила <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/ARM_big.LITTLE" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/ARM_big.LITTLE" target="_self">big.LITTLE</a> у 2011 році, за кілька років до того, як Intel і Apple випустили щось подібне у масштабі. Ідея: помістити два класи ядер на один кристал з однаковою ISA і дозволити ОС мігрувати потоки між ними.Оскільки ISA ідентична, потік може переміститися з великого ядра на LITTLE-ядро без будь-якої програмної підтримки, окрім планувальника ОС — це просто ще одна міграція, як переміщення між двома великими ядрами у симетричному мультипроцесорі. Саме це обмеження «одна ISA, різна мікроархітектура» робить big.LITTLE посильним. І саме його Intel порушила з неузгодженістю AVX-512 в Alder Lake (§6.3), заплативши за це суттєву ціну.
великі ядра — Cortex-A72, -A76, -A78, -X4. Широкий фронтенд (3–4 декодування, 6+ видача), глибоке вікно OoO, агресивне передбачення переходів, високий IPC, висока потужність. Вони схожі на машини з L14: побудовані, щоб якомога швидше відпрацьовувати найгірший випадок одного потоку.
LITTLE-ядра — Cortex-A53, -A55, -A520. Виконання за порядком або дуже вузьке OoO, мілкий конвеєр, вузька видача, низький IPC на потік, але у 5–10 разів краща продуктивність на ват на фоновій роботі. Вони схожі на машини до L14 — простіші, менші, дешевші, щоб їх «засвітити».
Ставка в тому, що реальне навантаження має довгий хвіст. Телефон у вашій кишені спить 95% часу; він забирає пошту, перевіряє сповіщення і малює системний інтерфейс — це робота, якій не потрібен ROB на 630 записів.Велике ядро, виконуючи цю роботу, платить за машинерію, якою не користується. LITTLE-ядро на частці напруги впорається з цим і знову засне.На співвідношення 5–10× «продуктивність на ват» варто уважно подивитися. Два ядра з однаковою ISA можуть відрізнятися за енергоефективністю майже на порядок — на однаковому навантаженні, на тому самому чипі, у ту саму наносекунду — і це чисто через ширину та глибину їхньої мікроархітектури.Цей десятикратний множник і є тим бюджетом dark silicon, який ви повертаєте собі, погоджуючись на нижчу однопотокову продуктивність.<br><img alt="Ядра ARM Cortex-A57 (велике) і Cortex-A53 (LITTLE) на одному кристалі big.LITTLE MPCore." src="assets/l15-biglittle-cortex-a57-a53.jpg" target="_self" style="width: 600px; max-width: 100%;">
Кристал big.LITTLE MPCore з ядрами Cortex-A57/A53. Фото з Wikimedia Commons (CC-BY-SA).Ідея двох типів ядер проста. Змусити ОС планувати роботу між ними — складна частина, і галузь пройшла через три покоління, перш ніж усе запрацювало як слід.Кластерне перемикання (2011–2013). Найпростіша модель. ОС бачить лише один кластер за раз — або великий (скажімо, 4 ядра A15), або LITTLE (скажімо, 4 ядра A7), але ніколи обидва.Коли навантаження переходить поріг, прошивка вимикає живлення одного кластера і вмикає інший; потоки мігрують. Першим цей режим випустив Samsung Exynos 5410.Концептуально чисто; на практиці марнотратно, бо ви постійно залишаєте половину кремнію невикористаною і платите реальну латентність на кожному переході між кластерами.<br><img alt="big.LITTLE з кластерним перемиканням: ОС бачить один кластер (чотири великі АБО чотири LITTLE) за раз; перемикання вимикає один кластер і будить інший." src="assets/l15-biglittle-cluster-switching.png" target="_self" style="width: 600px; max-width: 100%;">
Кластерне перемикання — найраніший і найпростіший режим big.LITTLE. Діаграма з Wikimedia Commons (CC-BY-SA).Внутрішньоядерний перемикач (IKS, 2013–2014). Дрібніша гранулярність. Кожне велике ядро об'єднується у пару з LITTLE-ядром як віртуальний CPU; у будь-який момент працює рівно одне з пари, а ОС бачить віртуальний CPU.Планувальник може незалежно обрати велике чи LITTLE ядро для кожного віртуального CPU, тож чип може одночасно виконувати, скажімо, два віртуальні CPU з активними великими половинами і два — з активними LITTLE-половинами.Краще за кластерне перемикання, але все ще не надто добре: не можна одночасно запустити велике і його парне LITTLE, тому половина кристала залишається простоюватою, поки інша зайнята.Гетерогенна багатопроцесорність (HMP) з плануванням з урахуванням енергії (EAS, 2015+). Модель, яку використовують сьогодні, і в Android, і в iOS. ОС бачить усі ядра як плаский пул — великі та LITTLE є одночасно планованими CPU.Планувальник отримує підказки (від апаратних лічильників навантаження, історії потоку та явних підказок SCHED_* від застосунку) про очікувану інтенсивність кожного потоку і розміщує його на тому типі ядра, який мінімізує сумарну енергію для прогнозованої роботи. Потік відеокодування потрапляє на велике ядро; опитувач сповіщень — на LITTLE-ядро; системна служба може стартувати на LITTLE, а посеред кадру отримати підвищення до великого ядра, коли її бюджет затримки ось-ось вибухне.Саме «Планування з урахуванням енергії» (EAS) перетворює HMP з «можливого» на «добре». EAS моделює вартість енергії при розміщенні потоку на кожному з ядер-кандидатів — зважає на поточний P-стан ядра, його тепловий запас, на те, чи переміщення потоку дозволить вимкнути живлення кластера, — і обирає розміщення з мінімальною прогнозованою енергією.Це перетворило гетерогенне планування з евристики «по можливості» на задачу оптимізації, яку ядро ОС фактично розв'язує при кожному пробудженні. Регулятор schedutil у Linux і трекер навантаження WALT в Android — це дві частини, які зробили EAS масштабованим; без апаратних лічильників навантаження, на які ці механізми спираються, планувальник летить наосліп.<br><img alt="big.LITTLE HMP (гетерогенна багатопроцесорність): планувальник Linux бачить усі вісім несиметричних ядер одночасно і призначає потоки тому типу ядра, який найкраще відповідає навантаженню." src="assets/l15-biglittle-global-task-scheduling.jpg" target="_self" style="width: 700px; max-width: 100%;">
HMP — домінантний режим планування big.LITTLE сьогодні. Діаграма з Wikimedia Commons (CC-BY-SA).<br>Intel випустила свій перший гібридний споживчий процесор, <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Alder_Lake" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Alder_Lake" target="_self">Alder Lake</a>, через десятиліття після ARM. Варто розуміти, чому Intel чекала так довго: більшу частину 2010-х стратегія Intel полягала в тому, щоб зробити одне чудове ядро і розмістити на кожному кристалі якомога більше таких. Гетерогенність стала визнанням того, що підхід «одне чудове ядро» уперся в ту саму стіну, у яку ARM уперлася десятиліттям раніше.Alder Lake перейняв той самий фундаментальний поділ big/LITTLE, але з іншими компромісами та іншими назвами.
P-ядра (Performance, кодова назва Golden Cove) — широкі, глибокі OoO, з увімкненим SMT. x86-еквівалент Firestorm з M1, з яким ви познайомилися в L14.
E-ядра (Efficient, кодова назва Gracemont) — вужчі, без SMT, але організовані у 4-ядерні кластери, які спільно використовують L2.
На тутешню математику ефективності варто уважно подивитися. E-ядра Gracemont приблизно у 4 рази щільніші на мм², ніж P-ядра Golden Cove, за зіставної пропускної здатності на багатопотокових цілочисельних задачах.Це співвідношення щільності є аргументом dark silicon, вираженим у компонуванні чипа: за фіксованого енергобюджету чотири E-ядра в площі одного P-ядра забезпечують приблизно 80% багатопотокової пропускної здатності P-ядра за приблизно ту саму потужність. На мм² кристала, обмеженого dark silicon, E-ядра — просто вигідніша пропозиція для задач, обмежених пропускною здатністю.Те, що знадобилося Intel і не знадобилося ARM: Thread Director. Оскільки x86 успадкувала десятиліття планувальників ОС, які припускали однорідні ядра, Intel розмістила на кристалі невеликий мікроконтролер, який спостерігає за сумішшю інструкцій кожного потоку (скалярні цілочисельні? векторні FP? багато затримок на пам'яті?) і підказує ОС, на якому типі ядра він виконуватиметься найефективніше.Це визнання того, що в x86 ОС самостійно не можна довіряти достатньо добре знати апаратуру — планувальники Windows і Linux писалися для світу, де всі CPU однакові, і Thread Director постачає апаратні підказки, щоб обійти це припущення, не змушуючи кожну ОС переписувати свій планувальник.Є одна деталь, важлива для сумісності: P-ядра підтримують SMT (Hyper-Threading), а E-ядра — ні, і E-ядра не реалізують AVX-512. У ранніх чипах Alder Lake Intel довелося вимкнути AVX-512 і на P-ядрах теж, бо потік, що виконує AVX-512, впав би, якби планувальник перемістив його на E-ядро, яке цих інструкцій не розуміє.Гетерогенність легка, коли ISA збігаються точно; вона дуже важка, коли не збігаються, і історія з AVX-512 в Alder Lake — повчальний приклад, який довелося обходити кожному гібридному дизайну x86 відтоді.L14 §6.5 пройшов P-ядро Firestorm. Повторюватися не будемо.Що тут важливо, так це те, що M1 (і кожен наступний чип серії M) вбудовує гетерогенність у платформу, а не лише в кластер CPU. Поділ big.LITTLE — це початок історії, а не її кінець.На одному кристалі Apple розміщує: P-ядра (Firestorm/Avalanche/Everest), E-ядра (Icestorm/Blizzard/Sawtooth), GPU (десятки ядер), Neural Engine (NPU — 16+ ядер, налаштованих під матричне множення int8), ISP (для камери), блок кодування/декодування відео, engine дисплея, Secure Enclave та уніфікований контролер пам'яті, який усі перелічені блоки спільно використовують.<br>Завдяки <a data-tooltip-position="top" aria-label="https://www.apple.com/newsroom/2020/11/apple-unleashes-m1/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.apple.com/newsroom/2020/11/apple-unleashes-m1/" target="_self">уніфікованій пам'яті</a> тут немає жодного копіювання пам'яті між пристроями, про яке варто говорити — усі ці блоки читають і пишуть у ту саму фізичну DRAM. Модель, яку виклик Core ML запускає на Neural Engine, лежить у тому самому адресному просторі, що й байти, які шейдер Metal читає з текстури; жодного аналога cudaMemcpy не потрібно.Диспетчеризація Apple щільна: фонове опитування пошти виконується на E-ядрі при ~100 мВт; передньопланний відеоредактор — на P-ядрах при ~5 Вт; виклик інференсу Core ML — на Neural Engine під 1 Вт, замість того щоб насичувати CPU на 8 Вт.ОС і фреймворки (Metal, Core ML, Accelerate) маршрутизують кожен вид роботи до того блоку, який для нього найефективніший. Користувач цього ніколи не бачить. Час роботи від батареї — бачить.Суть не в тому, що Apple винайшла щось, чого галузь не бачила. В ARM був шаблон big.LITTLE, у NVIDIA — GPU з тензорними ядрами на кристалі, а Qualcomm роками раніше мала DSP у телефонах.Що зробила Apple — інтегрувала весь каталог на одному кристалі, під одним контролером пам'яті, з програмним стеком (Metal + Core ML + Accelerate + Grand Central Dispatch), який знає про весь цей каталог. Перемогу забезпечує саме ця інтеграція. І її дорого відтворити, бо вона вимагає контролю і над кремнієм, і над ОС.Відсторонімося від окремого кластера ядер і поглянемо на весь SoC телефона. Чип здебільшого не CPU.Це сузір'я спеціалізованих блоків, кожен з яких на своїй конкретній роботі набагато ефективніший за CPU. Кластер CPU зазвичай займає менше 20% площі кристала; решта — прискорювачі, контролери пам'яті, модеми і блоки фіксованої функціональності.Кожен з цих блоків — ставка на dark silicon. Більшість часу більшість з них вимкнені.Коли відкривається камера, засвічується ISP. Коли ви робите дзвінок, засвічується DSP. Коли прокидається Siri, засвічується always-on DSP (підблок DSP). SoC розроблений на припущенні, що жодні два з цих блоків ніколи не потребуватимуть повної потужності одночасно — і для майже кожного реального навантаження це припущення тримається.CPU у цій картині — універсальний запасний варіант. Він обробляє ті випадки, з якими не впораються спеціалісти, склеює виходи від усіх інших блоків і виконує ОС.Він абсолютно необхідний — і водночас він найменш енергоефективний блок на ват майже для кожної роботи, з якою можуть упоратися спеціалісти. Філософія дизайну сучасного телефона тому така: тримати CPU у простої, коли це можливо; маршрутизувати роботу до найбільш спеціалізованого блоку, який може її виконати; і прийняти, що спеціалізовані блоки більшість часу будуть темними, бо кожен обслуговує вузьке навантаження.<br><img alt="Типовий флагманський SoC телефона 2024 року: комплекс CPU (P-ядра + E-ядра), GPU, Neural Engine (NPU), ISP, DSP, відеокодек і Secure Enclave, усі спільно використовують уніфіковану шину пам'яті." src="assets/l15-heterogeneous-soc.svg" target="_self" style="width: 760px; max-width: 100%;">Тут ми маємо бути з вами чесними. Гетерогенність — це архітектурна перемога і програмний податок, і податок цей немалий.Універсального стандарту для програмування гетерогенної системи не існує. Натомість є кілька часткових, кожен з яких покриває свій шматок ландшафту:
CUDA — лише NVIDIA, лише GPU, наразі найзріліша екосистема
OpenCL — мультивендорна, але інструментарій за останні роки зачахнув
SYCL — сучасна, на базі C++, нейтральна до виробника, зростає, але ще незріла
Metal — лише Apple, покриває GPU і Neural Engine
oneAPI — відповідь від Intel, на базі SYCL, в принципі кросакселераторна
Бекенди TensorFlow / PyTorch — практичний стандарт для ML; вони ховають гетерогенність за API графа, але вона прориває назовні, щойно вам знадобляться власні ядра
Окрім API, когерентність пам'яті між прискорювачами — це активна дослідницька задача. Коли потік CPU і ядро GPU торкаються тих самих даних, потрібні або явні копії (повільні, марнотратні, схильні до помилок), або апаратно керована когерентність (уніфікована пам'ять Apple, NVIDIA Grace Hopper, AMD Instinct MI300A).Модель «без копій» поширюється, але далеко не універсальна, і кожна крос-акселераторна копія — це прірва продуктивності, про яку програміст повинен знати.Причина, чому екосистема CUDA від NVIDIA коштує десятки мільярдів доларів, — не апаратура. Це накопичені за понад десятиліття бібліотеки, підтримка налагоджувачів, профайлери і тисячі інженерів, які вміють писати ядра.Гетерогенність найкраще працює тоді, коли хтось інший — компілятор, фреймворк, вендор — ховає її від вас. Коли фреймворк не знає вашого навантаження, ви платите податок безпосередньо.Якщо сформулювати вартість прямо: кожен прискорювач, який ви додаєте до чипа, створює нове місце, де ваш код автоматично не запускається.CPU виконає все, що ви йому дасте; NPU виконає лише ту підмножину вашої програми, яку якийсь фреймворк переписав як тензорний граф; ISP виконає лише той конвеєр камери, який ви побудували з використанням вендорського SDK для ISP.Робота, яка «не лягає» на прискорювач, відкочується на CPU з енергоспоживанням у 10–100 разів гіршим. Дизайн, який випускається з чотирма прискорювачами, яких ваш софт не використовує, з точки зору енергії ідентичний дизайну без них — за тим винятком, що ви заплатили за площу кристала, верифікацію і супровід програмного стеку.Це важливо для синтезу курсу в §10. Увесь сенс L14 + L15 у тому, що отримання доброї роботи від сучасного чипа — не те, чим воно було 2005 року.У 2005 році ви писали на C, компілятор планував ваші інструкції, а CPU щороку ставав швидшим.У 2025 році ви пишете на C для CPU, CUDA для GPU, Metal або Core ML для NPU, а виклики до ISP і кодека робите через ОС — і ви відповідальні за рішення, що де виконується.Компілятор цього за вас не робить. Планувальники допомагають. Решта — інженерне судження, чого цей курс і намагався вас навчити.Ще одне чесне спостереження перед тим, як закрити тему. Програмний податок нерівномірний за навантаженнями. Якщо ви тренуєте нейронну мережу, ML-фреймворки беруть на себе майже все — PyTorch вирішує, що запускати на GPU, NPU чи CPU, і ваш код про це знати не повинен.Якщо ви пишете звичайний бекенд-сервіс, майже все виконується на CPU, і гетерогенність невидима. Податок у повному обсязі платять інженери «посередині» — ті, хто пише конвеєри обробки зображень у реальному часі, власні шляхи ML-інференсу, ігри, браузери, медіаплеєри. Саме вони пишуть код, який ховає цю складність від усіх інших.Після цієї секції ви можете: пояснити, чому 5-нм Apple M1 випереджає настільні чипи Intel того самого покоління за продуктивністю на ват, не маючи жодного окремого технологічного прориву; описати три режими планування big.LITTLE і пояснити, чому HMP з EAS — сьогоднішній стандарт; і аргументовано міркувати про те, чи стратегія P+E від Intel — це здорова інженерія, чи реактивне наздоганяння ARM і Apple.Ви вже рік-два набираєте промпти в ChatGPT. Кожен запит — той, що підсумовує ваші конспекти лекцій, той, що переписує ваше резюме, той, який ви кидаєте, бо автопідказка колись спрацьовувала за 200 мс, а тепер здається повільною — коштує приблизно 3 Вт·год електроенергії від початку до кінця. Пошук у Google для порівняння — близько 0,3 Вт·год. Тобто щоразу, коли ви говорите з мовною моделлю замість того, щоб вписати у рядок пошуку два ключових слова, ви витрачаєте вдесятеро більше енергії. Ви цього ніяк не відчули. Рахунок пішов комусь іншому.OpenAI звітує про близько мільярда запитів на день. Перемножте. Додайте сюди Gemini, Claude, Copilot, Llama, DeepSeek і кожен стартап, який щойно обгорнув чужий API. Міжнародне енергетичне агентство прогнозує, що лише генеративний ШІ додасть до світового попиту на електроенергію близько 130 ТВт·год/рік до 2026 року — це річне споживання всієї Швеції, цілої країни, кожного заводу, кожного ліхтаря, кожного житлового будинку. І ми додали це до енергомережі лише тому, що вирішили: чат-подібне виведення — це можливість, яку варто вмикати за замовчуванням.Ця секція про те, куди фізично йде ця електроенергія, чого вона коштує у вуглеці та на звалищах і що може зробити програмна інженерка чи інженер. Не проповідь — підрахунок рахунку, який ваші архітектурні рішення щойно підписали, спершу числа.Відступімо на мить від історії про ШІ. Усі центри обробки даних разом — вебу, хмари, корпоративні, ШІ, стримінг, усе — спожили близько 415 ТВт·год у 2024 році, приблизно 1,5% світової електроенергії. Це вже більше за річну генерацію Німеччини. Референсна проєкція МЕА виводить центри обробки даних на ~945 ТВт·год до 2030 року, понад удвічі більше за шість років. У США центри обробки даних можуть сягнути 9% національної генерації електроенергії до 2030 року — цифра, яка в підручнику 2015 року виглядала б як друкарська помилка, а тепер є плановим показником у кожної великої енергокомпанії.<br><img alt="Ряди серверних стійок у телекомунікаційному центрі обробки даних — фізичне втілення рахунку на 415 ТВт·год електроенергії." src="assets/l15-data-center-racks.jpg" target="_self" style="width: 700px; max-width: 100%;">
Типовий зал центру обробки даних. Фото з Wikimedia Commons (CC-BY-SA).Вуглецевий бік балансу: близько 220 Мт CO₂ від центрів обробки даних у 2024 році, близько 1% глобальних викидів — зіставно з комерційною авіацією. А ще є сама апаратура. Глобальні електронні відходи від циклів оновлення ІТ сягнули 62 Мт у 2022 році, з яких лише близько 22% були утилізовані належним чином. Решта — сервери, світчі, GPU, ноутбуки, смартфони — їде на звалище або до неофіційних переробників у країнах Глобального Півдня, де золото й мідь випалюють вручну, а решта просочується у ґрунтові води. Кожен трьох-п'ятирічний цикл оновлення у флоті гіперскейлера — це десь далі у ланцюзі якась сумарна тоннажність.Це три рядки рахунку: електроенергія, вуглець, матеріали. Тримайте їх у пам'яті — усе, що буде далі, це або важіль, який тисне один із них униз, або компроміс, який переносить вартість між ними.Коли ви орендуєте сервер у центрі обробки даних, ви платите не лише за транзистори, які обчислюють ваше навантаження. Ви платите за охолодження, яке тримає їх нижче 85 °C, за акумулятори ДБЖ, які живлять їх, коли мережа блимає, за резервні дизельні генератори, які кашляють, запускаючись, коли ДБЖ вичерпується, за втрати на перетворенні живлення на кожному кроці від підстанції 110 кВ до шини 12 В на материнській платі, за освітлення, контроль вологості, фізичну охорону. Усе це — накладні витрати, і в індустрії є рівно одне число, яким це вимірюють:PUE = повна потужність об'єкта / потужність ІТ-обладнанняІдеальний PUE — 1,0: кожен ват, що входить у будівлю, доходить до процесора, і жоден не втрачається на накладні витрати. Це неможливо (закон збереження енергії каже, що охолодження має споживати живлення; безплатно охолоджувати не вдасться), але як орієнтир працює добре. Розподіл у реальному світі широкий:
Типовий корпоративний ДЦ у США: ~2,0. На кожен ват, витрачений на обчислення, припадає ще один ват накладних. Більшість цього накладу — охолодження, часто 30% і більше загальної потужності об'єкта йде на переміщення повітря та роботу чилерів.
Сучасний гіперскейлер (hyperscaler) — Google, Meta, Microsoft Azure: ~1,1. Охолодження опускається до ~7% від загальної потужності, чистіший розподіл живлення, все під замовлення.
Найкращий клас (двофазне імерсійне охолодження): ~1,01. Сервери занурені у діелектричну рідину, яка буквально закипає на гарячих точках. На накладні не залишається майже нічого.
<br><img alt="Розподіл потужності типового корпоративного центру обробки даних (PUE 2,0) порівняно з гіперскейлером (PUE 1,1). Лише охолодження падає з 30% до 6% загальної потужності." src="assets/l15-pue-breakdown.svg" target="_self" style="width: 760px; max-width: 100%;">Цей розкид — 1,01 проти 2,0 — це приблизно дворазова різниця у сумарному споживанні електроенергії на ту саму корисну роботу. Коли хмарний провайдер рекламує «вуглецево-нейтральний» сервіс, перше питання — який у нього PUE. Коли уряд обговорює розміщення центрів обробки даних, PUE — число з регуляторного боку столу. Одна метрика — і вона скаже вам про об'єкт більше, ніж будь-який глянцевий буклет.Охолодження — найбільший змінний доданок у PUE, тому саме тут відбувається архітектура. Два підходи, які варто запам'ятати — не шість.Ізоляція гарячого/холодного коридорів — це базова конфігурація, всюдисуща у центрах обробки даних, збудованих приблизно з 2010 року. Стійки стоять лицем одна до одної у рядах, що чергуються: фронти (забірники повітря) звернені до «холодного коридору», куди подається кондиційоване повітря; задні боки (викиди) звернені до «гарячого коридору», який збирає нагріте повітря і повертає його на чилери. Пластикові штори або панелі не дають гарячому і холодному повітрю змішуватися. Нудно, механічно — і працює: пристойна схема гарячого/холодного коридорів дає PUE близько 1,4–1,6, суттєве покращення порівняно з відкритими залами 2000-х.<br><img alt="Ізоляція холодного коридору: стійки стоять лицем одна до одної у рядах, що чергуються, а пластикові панелі не дають холодному і гарячому повітрю змішуватися." src="assets/l15-data-center-cold-aisle.jpg" target="_self" style="width: 500px; max-width: 100%;">
Ізоляція холодного коридору, поточний базовий стандарт. Фото з Wikimedia Commons (CC-BY-SA).Двофазне імерсійне охолодження — це те, куди прямують ШІ-кластери. Сервери занурюють у бак із діелектричною рідиною — фторвуглецем, який не проводить електрику і кипить близько 50 °C. Коли GPU розігріває гарячу точку, рідина на його поверхні миттєво переходить у пару, поглинаючи величезну кількість тепла у фазовому переході, піднімається до конденсатора у верхній частині бака, конденсується назад у рідину і стікає вниз. Жодних вентиляторів. Жодних чилерів. Жодної обробки повітря. Тепло виходить з будівлі простою водяною петлею назовні. PUE наближається до 1,01, бо живити, по суті, нічого, крім самих серверів. Компроміс — капітальні витрати та новий набір експлуатаційних ризиків, але для кластера з 10 000 H100, що спалює 7 МВт, математика ефективності сходиться швидко. Очікуйте, що до 2030 року це буде повсюди в ШІ-орієнтованих центрах обробки даних.Коли PUE вже вичавлено, наступний важіль переносить об'єкт туди, де електрони й так дешеві та чисті. Дві ідеї — обидві з наслідками.Вибір майданчика. Будувати там, де енергомережа холодна і чиста. Швеція, Ісландія, Норвегія, Квебек: холодне зовнішнє повітря означає безплатне охолодження більшу частину року; гідро-, геотермальна та атомна енергетика означає, що вуглецева інтенсивність на кВт·год близька до нуля. Об'єкт Google у фінській Хаміні качає морську воду з Фінської затоки крізь свої теплообмінники. Кампус Facebook у Лулео розташований за сотню кілометрів на південь від Полярного кола. Це не PR-хід — електроенергія дешевша, охолодження майже безплатне, а вуглецевий облік випадково виходить найкращим в індустрії. Усі будували б там, якби це дозволяли мережева затримка та місцева пропозиція робочої сили.Цілодобова безвуглецева енергія (24/7 CFE). Складніший важіль. Більшу частину 2010-х гіперскейлери заявляли про «100% відновлювану» енергію, підписуючи угоди про купівлю електроенергії з вітрових і сонячних електростанцій десь у мережі, а потім усереднюючи за рік. Бухгалтерія сходилася. Фізика — ні. Центр обробки даних у Вірджинії, який «узгоджує» своє річне споживання з вітровою електростанцією в Техасі, усе одно споживає електрони від вугільних ТЕС о третій ранку, коли в Техасі немає вітру. Google став першим гіперскейлером, який назвав цей підхід недостатнім і зобов'язався вийти на 24/7 CFE до 2030 року — узгоджувати кожну годину споживання з безвуглецевою генерацією на тій самій мережі. Різниця суттєва: це змушує інвестувати у стабільну чисту генерацію (атомну, довготривале накопичення, геотермальну), а не просто додавати ще більше переривчастих вітру й сонця, які зручно корелюють із часом, коли ви вже вирішили працювати дешево. Microsoft, Amazon і Meta підхопили з подібними зобов'язаннями. Чи вдасться — питання відкрите; зсув у рамці вже реальний.Це та секція, де я міг би подати вам список із п'яти пунктів — оркестрація контейнерів, кеш-дружній код, низькоточне виведення, вуглецево-свідоме планування, правильно підібрані типи інстансів. Усе правда. Усе — вторинне. Одна річ домінує, і це правда вже п'ятдесят років.Алгоритмічна ефективність. Алгоритм, у 10 разів швидший, — це у 10 разів менше енергії назавжди, на кожній машині, де він колись запускатиметься, у кожному центрі обробки даних, за будь-якого міксу генерації. Найсталіший код — це код, який не запускається. O(n²) сортування, яке ви так і не виправили, бо «на ноуті розробки було достатньо швидко», за час життя у продакшені спалить більше кіловат-годин, ніж зекономить імерсійний бак, у якому воно крутиться. Вебсервіс, який кешує обчислений результат, не просто працює швидше — він у найпрямішому сенсі прибирає тонни CO₂ з балансу.Другий за важливістю важіль — архітектурна відповідність. Якщо NPU здатний виконувати ваше виведення у 20 разів ефективніше за CPU, вибір, де саме запускати, — це вже не оптимізація продуктивності, а єдине найбільше рішення щодо вуглецю в усьому навантаженні. Маршрутизувати виведення на H100, коли меншу модель на edge-NPU було б цілком достатньо, — дуже дорогий рефлекс. Підбір правильного заліза — це рішення з інженерної етики, і у 2025 році вдавати, що це не так, вже не серйозно.Кожен архітектурний вибір, який ви робите, — від алгоритму сортування до регіону хмари й розміру моделі — тепер має прикріплений енергетичний, вуглецевий і e-waste-«підпис». Ті, хто платить за рахунком, — не ті, хто ухвалює рішення, і у більшості організацій цього не відстежує ніхто. Історично це було нормально, бо числа були малі. Малими вони більше не є.У 2025 році вибір правильної архітектури під навантаження — це вже не просто рішення щодо продуктивності. Це більше не просто питання продуктивності. Це інженерна етика.Після цієї секції ви можете: читати цифру PUE центру обробки даних і порівнювати об'єкти «яблука з яблуками», називати єдиний найбільший важіль для зменшення вуглецевої вартості навантаження та захищати алгоритмічну ефективність як рішення щодо сталості, а не лише щодо продуктивності.Ось питання, яким варто відкрити цю секцію, бо відповідь за чотири роки перевернула індустрію догори дриґом. Чому AMD почала збирати процесори з Lego-кубиків у 2019 році? Тридцять років компанія випускала традиційні монолітні процесори — один великий кристал на чип, один компонувальний план, один виробничий прогін. Потім з'явився Zen 2 з обчислювальними ядрами на одному шматку кремнію та I/O-контролером на іншому — і Intel, домінантний виробник CPU попередніх двох десятиліть, почав втрачати частку в ЦОД щоквартально. Чому AMD поставила на це компанію і чому це спрацювало?Відповідь — не в архітектурі. Вона в арифметиці. Перемножте разом зі мною.Сучасна щільність дефектів на пластині становить близько 0,1 дефекта на квадратний сантиметр. Кожен кристал, вирубаний з пластини, має певну ймовірність містити всередині вбивчий дефект — що більший кристал, то вища ймовірність. Стандартна модель виходу (Мерфі) каже, що вихід Y для кристала площею A дорівнює приблизно Y ≈ 1 / (1 + A · D)², де D — щільність дефектів на см².Візьміть монолітний серверний кристал 600 мм² — великий, бо потрібно багато ядер. Площа в см² — це 6,0. Підставляємо: Y ≈ 1 / (1 + 6,0 · 0,1)² = 1 / 1,6² = 1 / 2,56 ≈ 39%. З реальними відсіканнями країв і біннінгом виходить ближче до часто цитованих ~55% для добре налагодженого процесу 600 мм², але пуант один і той самий: приблизно половина ваших великих кристалів — у брак.Тепер розділіть ті самі 600 мм² кремнію на шість чіплетів по 100 мм². Кожен чіплет має A = 1,0 см². Підставляємо: Y ≈ 1 / (1 + 1,0 · 0,1)² = 1 / 1,21 ≈ 83%. З реальним налаштуванням процесу ~94% на кристалі 100 мм² — рутинно досяжні. Добрі чіплети — звична справа. Цікаве питання: як часто вдається зібрати шість свідомо справних чіплетів в одному корпусі, і це приблизно 0,94⁶ ≈ 73% — усе одно майже вдвічі більший вихід, ніж у монолітному дизайні, ще до того, як ви врахуєте додатковий бонус: будь-який чіплет, визнаний браком на тесті, викидають за копійки, а не протягують через дороге подальше корпусування.Порахуйте кристали на пластину, і картина стає ще чіткішою. Пластина 300 мм вміщує близько 80 кристалів 600 мм² (≈44 придатних) або близько 600 чіплетів 100 мм² (≈564 придатних). Шість чіплетів роблять один CPU, тож та пластина з маленьких кристалів дає ~94 еквівалентних CPU-комплектів проти 44 монолітних. Понад удвічі більше робочих процесорів з пластини того самого кремнію. Ось ця економіка.AMD зробила цю математику наприкінці 2010-х. Intel трималася монолітного підходу ще три покоління. Між 2019 і 2023 роками частка AMD на ринку серверних CPU зросла приблизно з 2% до понад 30%, відібравши у Intel близько 40 відсоткових пунктів серверної виручки за цей проміжок. Ось як виглядає «зробіть множення» — коли ви робите його раніше за конкурента.Чіплети — не спритний технічний трюк. Це економіка, що реагує на фізику виходу, і у цієї фізики лише один напрямок: щільність дефектів з зрілістю процесу падає, але площі кристалів у конкурентних дизайнах тільки ростуть, тож точка переходу, де виграє арифметика чіплетів, зсувається все ближче до «все має бути чіплетами».Чіплет — це невеликий спеціалізований кристал, спроєктований для складання разом з іншими чіплетами на спільній підкладці корпусу, з'єднаними високопропускними інтерконектами. Зібраний корпус з погляду програми поводиться як один процесор. З погляду фабрики — це кілька незалежно виготовлених кристалів, зшитих докупи.Чотири приклади, які варто знати:<br><img alt="Корпус-чіплет у стилі AMD Ryzen: два обчислювальних чіплети (CCD) на передовому 5 нм, один I/O-кристал (IOD) на зрілому 12 нм, з'єднані лінками Infinity Fabric на спільній підкладці." src="assets/l15-chiplet-package.svg" target="_self" style="width: 700px; max-width: 100%;">Дизайни Apple і AMD ілюструють дві філософії. AMD декомпонує процесор функціонально: обчислення тут, I/O там, контролер пам'яті — в IOD. Apple декомпонує процесор ідентично: дві копії того самого кристала склеєні разом заради місткості. Обидва підходи працюють, бо інтерконект достатньо швидкий, щоб програма не бачила шва.Аргумент виходу з початку секції — перша й найбільша причина, але не єдина. Зверху на неї накладаються ще чотири переваги. Повторне використання — CCD в AMD це той самий кристал, чи він їде у Ryzen за 300 доларів, чи в EPYC за 10 000, тож вартість дизайну амортизується між двома лінійками продуктів, а не однією. Спеціалізація техпроцесу — логіка величезно виграє від передової щільності, але I/O та контролери пам'яті не масштабуються так само, і виносити їх на зрілий техпроцес драматично дешевше, без втрат продуктивності. Час виходу на ринок — кожен чіплет виходить на виробництво незалежно, тож помилка у graphics tile не затримує compute tile, а новий обчислювальний чіплет може поїхати з торішнім I/O-кристалом. Масштабованість — той самий чіплет складається у старші SKU (чотири CCD для Ryzen, дванадцять для EPYC) без нового випуску на виробництво під кожен варіант. Кожна з цих переваг нашаровується на вихід. Монолітному дизайну доводиться перемагати цей стек, і на великих кристалах у нього не виходить.Одна проблема чіплетної моделі, як її було розгорнуто першим ешелоном: інтерконект кожного виробника — власницький. AMD-шна Infinity Fabric не говорить з Intel-івською AIB, а жодна з них — з Apple-івською UltraFusion. Чіплет від одного виробника не можна просто поставити в корпус іншого. Це перекреслює значну частину обіцянки — справжня чіплетна екосистема дала б можливість збирати процесор із найкращих у класі плиток так само, як ви збираєте ПК з найкращих у класі компонентів.UCIe (Universal Chiplet Interconnect Express), запущений у 2022 році, — це відповідь індустрії: стандарт для зв'язку «чіплет-чіплет», який підтримали Intel, AMD, Samsung, TSMC, ARM, Qualcomm, Google і Meta. Уявіть собі USB для чіплетів: спільний фізичний і протокольний рівень, щоб обчислювальний чіплет від одного виробника можна було поставити поруч з чіплетом-контролером пам'яті від іншого. Екосистема ще молода — більшість продуктів у 2025 році всередині все одно використовує власницькі інтерконекти, — але UCIe визначає напрямок, і змішано-вендорні збірки вже їдуть у спеціалізованих ШІ- та HPC-продуктах.Чіплети розв'язують проблему виходу, розсуваючи вбік. Інший напрямок — угору — відкриває ще одні двері. 3D-мікросхеми стекують кілька кристалів вертикально і з'єднують їх через сам кремній за допомогою TSV (наскрізне з'єднання через кремній): металевих, зазвичай мідних, колон, просвердлених через стоншений тильний бік кристала, так щоб сигнали могли ходити з одного шару на інший, не виходячи зі стека. TSV короткі (десятки мікрометрів), жирні (низькоомні легко) та дуже щільно один до одного (можливі тисячі на квадратний міліметр), тож вони пропонують комбінацію, про яку мріє кожен проєктувальник інтерконектів: висока пропускна здатність, низька затримка, мало енергії на біт. Сигналізація у межах стека коштує приблизно у 10–100 разів менше енергії, ніж гнати ті самі біти з чипа через підкладку корпусу.<br><img alt="Концепція 3D-стекування кристалів: кілька кристалів стекуються вертикально, з'єднані наскрізними з'єднаннями через кремній, які несуть сигнали з одного шару до іншого." src="assets/l15-3d-die-stacking-concept.png" target="_self" style="width: 600px; max-width: 100%;">
Схема 3D-стекування кристалів. Діаграма з Wikimedia Commons (CC-BY-SA).Підступ у тому, що стекування затримує тепло всередині. Про це — за мить.Якщо ви чули про одну 3D-мікросхему, то це HBM (High Bandwidth Memory, пам'ять високої пропускної здатності). Від восьми до дванадцяти DRAM-кристалів стекуються на базовому логічному кристалі та з'єднуються через тисячі TSV. Базовий кристал виводить абсурдно широку шину — 1024 біти на стек для HBM–HBM3e, 2048 бітів для HBM4 — до головного процесора, зазвичай через кремнієвий інтерпозер під стеком. GDDR-пам'ять для порівняння має 32-бітну шину на чип; фішка HBM — сира паралельність, а не частота.<br><img alt="Схема HBM: кілька DRAM-кристалів стекуються на базовому логічному кристалі, з'єднані через TSV, встановлені на кремнієвому інтерпозері поруч з GPU заради дуже короткої, дуже широкої шини." src="assets/l15-hbm-schematic.svg" target="_self" style="width: 700px; max-width: 100%;">
Архітектура HBM з TSV та інтерпозером. Діаграма з Wikimedia Commons (CC-BY-SA).Поколіннєвий прогрес показує, куди індустрія вклала останнє десятиліття інженерії пам'яті:Кожен сучасний GPU й ШІ-прискорювач використовує HBM: NVIDIA H100 (HBM3), H200 і B200 (HBM3e), AMD MI300 (HBM3). Типова конфігурація — 4–8 HBM-стеків навколо обчислювального кристала на спільному інтерпозері, що дає сукупну пропускну здатність пам'яті 3–8 ТБ/с на корпус прискорювача.<br><img alt="Корпус GPU AMD Fiji з чотирма стеками пам'яті HBM, видимими поруч з кристалом GPU на кремнієвому інтерпозері — перший споживчий GPU, що вийшов з HBM (2015)." src="assets/l15-hbm-fiji-gpu-package.jpg" target="_self" style="width: 600px; max-width: 100%;">
Корпус AMD Fiji з HBM. Фото з Wikimedia Commons (CC-BY-SA).Чому пропускна здатність настільки важлива? Бо сучасний GPU робить трильйони операцій з рухомою комою на секунду — H100 сягає піку близько 1 ПФлопс/с на тензорних ядрах, — і кожна з цих операцій читає або пише дані. Без HBM GPU просто сидів би голодний, чекаючи, поки DDR доставить байти на часточку тієї швидкості, з якою обчислювальні блоки здатні їх споживати. H100 за 30 000 доларів у парі зі звичайним GDDR крутився б при утилізації, може, 10% на реальних тренувальних навантаженнях. HBM — це те, що робить тренування ШІ економічно життєздатним. Співвідношення «обчислення/пам'ять» у глибокому навчанні приблизно збалансоване на пропускних здатностях HBM; на пропускних здатностях DDR воно шалено недогодоване обчисленнями — а значить, кожен долар кремнію, вкладений у тензорні ядра, іде здебільшого у марнотратство.Саме тому ланцюг постачання HBM (SK hynix, Samsung, Micron) став геополітично чутливим у 2023–2024 роках — HBM це зв'язувальне обмеження виробництва ШІ-прискорювачів, і хто поставляє пам'ять, той поставляє і потужності ШІ.HBM стекує пам'ять поруч з логікою. AMD-шний 3D V-Cache стекує пам'ять поверх логіки: другий кристал L3-кешу сидить вертикально на Zen CCD, з'єднаний через TSV крізь тильний бік кристала кешу. Базовий CCD їде з 32 МБ L3; V-Cache додає ще 64 МБ зверху, тобто разом 96 МБ L3 на чіплет. У 16-ядерному варіанті EPYC Genoa-X це дає сукупно 1 ГБ L3 на весь корпус.Виграш специфічний до навантаження і драматичний. Ігрові бенчмарки підскакують на 15–30% у Ryzen 7 7800X3D проти еквівалента без X3D, бо робочі множини сучасних ігрових рушіїв влазять у 96 МБ, але переливаються з 32 МБ — частка влучань перестрибує обрив. Наукові HPC-коди, що працюють на матрицях середнього розміру, дають подібний приріст. Ці чипи стали дефолтним вибором для обчислювальних фінансів, CFD-симуляцій і всього, чиї гарячі дані живуть між «розміром L2» і «розміром DRAM».Зверніть увагу, що саме AMD застекувала: кеш, а не ядра. Цей вибір невипадковий, і він указує на центральну нерозв'язану проблему 3D-інтеграції.Стекувати пам'ять легко, а логіку — важко; причина — тепло. Сучасний DRAM-кристал розсіює порядку вата-двох. HBM-стек з дванадцяти таких кристалів — це все одно лише 15–25 Вт сумарно, і це тепло може вийти нагору зі стека у радіатор або вниз через підкладку корпусу. DRAM щільний, холодний і щасливий у вежі.Кристал CPU — інший звір. Один Zen 5 CCD може сам розсіювати 50–100 Вт. Поставте зверху ще один обчислювальний кристал — і ви маєте 100–200 Вт, що намагаються вийти через стек, у якому нижній кристал накритий ковдрою зверху. Нижній кристал не може охолоджуватися через верхню грань — верхній кристал заважає, — а верхній не може охолоджуватися через нижню грань — заважає нижній. Тепло скупчується посередині. Стек запікається.Саме тому V-Cache працює, а стековані CPU — ні, і це структурна причина, чому AMD застекувала кеш, а не ядра. Кеш малопотужний (L3 — це переважно SRAM; більшість клітинок тихі більшість часу) і термостійкий (кілька зайвих градусів Цельсія не шкодять SRAM так, як вони б'ють по запасах часових полів логіки). Навіть при цьому V-Cache-чипи крутяться на трохи знижених тактах порівняно зі своїми не-V-Cache-побратимами — AMD свідомо залишає кілька сотень мегагерців на столі, щоб вкластися у тепловий бюджет.3D-стекування «обчислення на обчислення» — активна дослідницька галузь, і у 2025 році воно лишається здебільшого експериментальним. Intel Foveros, TSMC SoIC і різні академічні прототипи демонстрували стековану логіку для конкретних застосувань, але ніхто не відвантажує масовий процесор зі стекованими обчисленнями для загальних навантажень. Теплова фізика нікуди не дінеться — у якийсь момент або технологія охолодження (зараз досліджують мікроканальне рідинне охолодження всередині стека), або щільність потужності (фундаментальне зниження напруги, якщо ми до цього доживемо) мають змінитися, щоб вертикальні обчислення стали рутиною. До того часу правило «стекуй пам'ять на логіку, не логіку на логіку» варто запам'ятати.Після цієї секції ви можете: читати анонс CPU — «Zen 5 виходить як 4 CCD плюс IOD, з опцією V-Cache на двох з них» — і оцінювати компроміси у грі: економіку виходу, затримки між чіплетами, потужність інтерконекту та чому стекування відбувається саме там, де відбувається (кеш над ядрами, а не ядра над ядрами).Щотижня в технологічній пресі хтось анонсує «революційну нову обчислювальну архітектуру». Фотонну. Квантову. Нейроморфну. В пам'яті. Аналогову. Біологічну. Оголошення — захоплені, бенчмарки — вибіркові, а через три роки ви читаєте значно тихішу замітку про те, чому ця технологія так і не вийшла у масове виробництво — або ж чому вона потрапила рівно в одну нішу й там і застрягла. Якщо намагатися встигати за кожною на її власних умовах, ви або запам'ятаєте по п'ять фактів на кожну й не втримаєте жодного, або почнете вірити пресрелізам. Ні те, ні інше не допоможе.Тож ця секція — не перелік для запам'ятовування. Це одна навичка, відпрацьована на п'яти прикладах.
Мета-навичка: щодо нової архітектури визначте її цільове навантаження та її фундаментальне обмеження.
Оце й усе. Два питання, щоразу. Що саме ця штука прискорює? і З чим вона погано справляється? Якщо презентація не може відповісти на обидва в межах одного абзацу — вам щось продають. Якщо ви самі можете відповісти на обидва в одному абзаці, то зможете тримати рівень у будь-якій розмові про те, куди рухається апаратне забезпечення — до кінця своєї кар'єри, крізь архітектури, яких ще не існує. П'ять технологій нижче — це тренувальні повтори для цієї навички, а не елементи словника.Чому такий ракурс? Тому що історія «революційних обчислень» — це переважно цвинтар претензій на універсальність, які зрештою перетворилися на вузькі прискорювачі. Кожна технологія зі списку нижче по-своєму справді цікава. Жодна з них не є заміною центральному процесору. Студент, який уміє чітко сформулювати, чому кожна з них вузька — не відкидаючи її, не роздмухуючи, а просто локалізуючи, — це той студент, що залишиться корисним, коли почнеться наступна хвиля анонсів.<br><img alt="Ландшафт після кремнію: нейроморфні, PIM, фотонні, квантові обчислення та нові матеріали, розміщені за широтою застосування (вузька — універсальна) та комерційною готовністю (дослідження — впроваджено)." src="assets/l15-future-tech-landscape.svg" target="_self" style="width: 760px; max-width: 100%;">Ми ґрунтовно розглянули це в L10 §8 — Intel Loihi, IBM TrueNorth і NorthPole, імпульсні нейронні мережі, обчислення на основі подій, занепад циклу «вибірка — виконання» фон-Ноймана для сенсорних навантажень. Перечитайте ту секцію, якщо деталі розмиті; далі тут ми виходимо з того, що ви знаєте, що таке SNN.Важливим є ракурс. Цільове навантаження: завжди активний висновок на краю мережі в масштабі мікроват — слуховий апарат, який у реальному часі пригнічує шум, камера спостереження, що відстежує аномалії в переважно статичному кадрі, кардіостимулятор, який класифікує аритмії, сільськогосподарський сенсор, що розпізнає силует шкідника серед тисяч нешкідливих комах.Виграш — у 100–1000 разів вища енергоефективність на розріджених, подієвих сенсорних потоках, де більша частина «даних» — це «нічого не відбувається», а цікаві моменти трапляються рідко. Звичайний конвеєр на CPU чи GPU прокидається на кожен кадр, обробляє його, більшу частину викидає; нейроморфний чип мовчить, доки не надійде імпульс. На правильному навантаженні ця різниця — це різниця між пристроєм, який рік працює від одного ґудзикового елемента, і тим, який не працює.Обмеження: модель програмування дуже далека від мейнстриму. Ви не компілюєте TensorFlow у Loihi з надією, що все якось вийде — ви переписуєте алгоритм у термінах імпульсів, частот і порогів, часто вручну. Це навичка рівня досліджень, а не інструментарій рівня продукту. І клас задач, які SNN елегантно розв'язують, вужчий, ніж обіцяє маркетинг: щільний, високоточний висновок (власне ті навантаження, під які GPU оптимізовані) — не та сфера, де виграють нейроморфні чипи.Прочитайте ракурс двічі: енергія за правильної форми задачі ціною універсальності.Інший напрям атаки. У L11 та L14 ви бачили, що сучасні CPU обмежені не пропускною здатністю АЛП — вони обмежені переміщенням даних. Одне звернення до DRAM розсіює приблизно в 100 разів більше енергії, ніж та операція множення з рухомою комою, заради якої ці дані вибираються. Для аналітики графів, добутків розріджених матриць на вектор і пошуку за великими вбудуваннями в ML ваш чип витрачає більше кремнію й більше потужності на перекидання бітів, ніж на обчислення з ними.Обчислення в пам'яті (PIM) перевертають цю логіку. Розмістимо прості АЛП всередині самих банків DRAM і виконуймо множення-з-накопиченням там, де дані вже лежать. Samsung HBM-PIM (2021) інтегрує програмовані обчислювальні блоки у стеки HBM. У UPMEM DPU розміщені на стандартних DIMM з невеликим RISC-ядром поряд із кожним банком. Пропускна здатність, яка ніколи не залишає кристал, — це «безкоштовно», або принаймні значно дешевше.Цільове навантаження: ядра, обмежені пам'яттю, — обхід графів, розріджена лінійна алгебра, сканування баз даних, пошук вбудувань у рекомендаційних системах — усе, де відношення обчислень до переміщуваних байтів низьке.Обмеження: модель програмування. Ваш код доведеться переструктурувати так, щоб розбити його на операції, які DRAM-блоки справді можуть виконати — а ці блоки свідомо прості (додавання, множення-з-накопиченням, деякі порівняння), а не повноцінні CPU. Жоден компілятор не перетворить ваш чинний C++ на PIM-код; це явна задача паралельного програмування поверх незвичної моделі пам'яті. Саме тому впровадження йде повільно.Ракурс: пропускна здатність за правильної форми задачі ціною зусиль на програмування.Кремній комутує електрони. Електрони мають масу, заряд і опір — кожен провідник на чипі є RC-колом, яке розсіює тепло. У фотонів цього немає, і для однієї конкретної операції — щільного матричного множення — вони майже ідеальні. Промінь світла, що проходить крізь сітку інтерферометрів Маха–Цендера, реалізує множення матриці на вектор зі швидкістю світла, причому витрати енергії зумовлені переважно електрооптичним перетворенням на межах, а не самим множенням.<br>Такі компанії, як <a data-tooltip-position="top" aria-label="https://lightmatter.co" rel="noopener nofollow" class="external-link is-unresolved" href="https://lightmatter.co" target="_self">Lightmatter</a>, будують фотонні прискорювачі, орієнтовані на висновок ML у дата-центрах. <a data-tooltip-position="top" aria-label="https://ayarlabs.com" rel="noopener nofollow" class="external-link is-unresolved" href="https://ayarlabs.com" target="_self">Ayar Labs</a> працює у суміжному напрямку — оптичний інтерконект між кремнієвими кристалами, що замінює мідні доріжки, які дедалі більше стають вузьким місцем зв'язку між чіплетами.Цільове навантаження: щільне матричне множення в масштабі дата-центру — висновок трансформерів, великі згортки. Теоретична перевага за енергією для цієї однієї операції величезна.Обмеження: все навколо множника — досі кремній. Цифро-оптичне перетворення є втратним і енергоємним; модулятори й фотодетектори масштабуються інакше, ніж транзистори; нелінійні операції (активації, softmax) доводиться повертати в електроніку. Ви отримуєте блискучий матричний множник, приварений до звичайного фронтенда в стилі CPU. Жодних універсальних обчислень.Ракурс: одна операція, виконана фізично краще, ціною складності інтеграції.Найбільш роздмуханий пункт у цьому списку — і той, який у повсякденних розмовах найчастіше подається в неправильному ракурсі. Квантові комп'ютери — це не швидші класичні комп'ютери. Вони не прискорюють ваш вебсервер. Вони не прискорюють більшість того, що вас цікавить. Вони розв'язують вузький клас задач — факторизацію цілих чисел (алгоритм Шора), неструктурований пошук із квадратно-кореневим прискоренням (Ґровер), певні симуляції квантової хімії (VQE, QPE), деякі оптимізаційні евристики (QAOA) — у класах складності за часом, яких класична машина досягти не може.Поточний стан: приблизно 1000-кубітні машини NISQ (шумні квантові обчислення середнього масштабу) від IBM, Google, IonQ, Quantinuum та інших. Корисні для дослідження алгоритмів; не корисні для злому RSA. Відмовостійкі квантові обчислення — коли схеми корекції помилок дозволяють надійно виконувати як завгодно довгі обчислення — від нас віддалені на роки або десятиліття, залежно від того, чиїй дорожній карті ви довіряєте.Цільове навантаження: вузький набір алгоритмів із доведеною квантовою перевагою — Шор для факторизації, релевантної для криптографії, Ґровер для пошуку, VQE для основних станів молекул, деякі задачі вибірки.Обмеження: декогеренція — кубіти втрачають свій квантовий стан за мікросекунди, розсіюваний будь-яким паразитним тепловим шумом. Корекція помилок працює в теорії; на практиці кожен логічний кубіт коштує приблизно 1000 фізичних кубітів. Дорога від сьогоднішньої тисячі шумних фізичних кубітів до корисної кількості логічних — це і є вся відкрита проблема галузі.Ракурс: процесор-супутник для конкретних алгоритмів ціною фізичної проблеми, яку, можливо, розв'яжуть у прийнятні терміни — а можливо, й ні.Останній напрям: не змінюйте архітектуру — змініть субстрат. Кремнієва CMOS упирається в атомні стіни — діелектрики затвора товщиною в кілька атомів, довжини каналу, на яких квантове тунелювання перестає бути похибкою округлення. Серед досліджуваних матеріалів — графен (один шар атомів вуглецю з надзвичайною рухливістю електронів), інші двовимірні матеріали, як-от дисульфід молібдену (MoS₂), та вуглецеві нанотрубки (CNT) (перший повноцінний RISC-V процесор на яких продемонстрував MIT у 2019 році з 14 000 транзисторів — технологічне демо, а не продукт).Ціль: продовжити масштабування CMOS за межі того, на що фізично здатен кремній, — вища рухливість, нижчий витік, менші проєктні норми.Обмеження: зрілість виробництва. Кремній має за плечима 60 років трильйонних інвестицій у виробництво, літографію, контроль дефектів і керування виходом придатних. Жоден альтернативний матеріал навіть близько не стоїть. Від працівного транзистора в дослідницькій лабораторії до мільярдного тиражу продукту — десятиліття.Ракурс: та сама архітектура з кращими транзисторами ціною цілої промислової бази, якої ще не існує.Зверніть увагу, чого ця секція не зробила. Вона не сказала вам, яка технологія переможе. Вона їх не ранжувала. Вона дала вам п'ять прикладів одного питання — ціль плюс обмеження — і довірила вам застосувати це питання до шостої, сьомої та сотої технології, про яку ви прочитаєте. Ця довіра — і є сенс. Лектор міг би вручити вам таблицю фактів для запам'ятовування; за два роки вона б застаріла. Питання — ні.Не чекайте єдиного переможця. Чесне очікування — це зоопарк спеціалізованих субстратів: кремнієві CPU та GPU залишаються робочим конем; спеціалізовані прискорювачі (NPU, фотонні матричні блоки, PIM-банки, нейроморфні чипи для краю мережі) керують своїми доменами значно краще, ніж універсальне ядро; квантові машини живуть поруч як процесори-супутники для задач, яких жодна класична архітектура не торкається; нові матеріали повільно розширюють можливості CMOS у кремнієвому стилі. Завдання архітектора — і дедалі більше завдання системного програміста — скерувати кожне навантаження на той субстрат, який йому пасує. Хмарні провайдери вже роблять це явно: AWS, Azure та GCP пропонують типи інстансів, які поєднують універсальні CPU з конкретними прискорювачами, і правильний вибір — це самостійна інженерна навичка.Мета-навичка, з якою ви виходите з цієї секції, вартує більше за п'ять прикладів. Ціль. Обмеження. Два питання. За наступні сорок років ви прочитаєте сотні анонсів архітектур. Задавайте ці два питання кожному з них — і ви будете мати слушність щодо ландшафту частіше, ніж автори цих анонсів.Після цієї секції ви можете: прочитати будь-який анонс нової обчислювальної архітектури і швидко окреслити його двома питаннями — що вона прискорює і з чим погано справляється? — без потреби в заздалегідь сформованій думці.Це остання лекція курсу. Будемо чесні щодо того, що сталося за ці 15 тижнів.На першу лекцію ви прийшли з, у кращому разі, розмитим уявленням про те, чим комп'ютер є. Коробка. Процесор. Пам'ять, десь там. Програми якось працюють. Якби вам тоді довелося пояснити, що відбувається, коли ви набираєте gcc hello.c &amp;&amp; ./a.out — від натискання клавіші аж до зміни пікселів на екрані — вам довелося б розмахувати руками у трьох-чотирьох місцях, де історія ставала розпливчастою. Можливо, у більшій кількості. Це не провал; з цього починає кожен студент. Архітектура комп'ютерних систем — найбагатошаровий предмет у навчальній програмі з комп'ютерних наук, і ці шари невидимі, доки хтось не знімає їх для вас один за одним. Більшість студентів CS випускаються, так і не побачивши більшості цих шарів. А ви щойно побачили.П'ятнадцять тижнів зняття шарів — ось що відбулося. Це легко недооцінити, бо не було жодного драматичного моменту — жодного «ага» від quicksort, жодної замкнутої краси математичного доведення. Архітектура так не вчить. Вона вчить, перемелюючи один шар за раз, аж поки наприкінці ви не можете озирнутися й побачити весь стек одразу. Це оглядання назад — і є ця лекція.Будьмо конкретними. Це не мотиваційний підсумок — це інвентаризація навичок, і читайте її саме як твердження про те, що ви справді вмієте робити тепер.Ви починали, не знаючи, що робить транзистор усередині вентиля NAND. Тепер ви можете вивести ALU з вентилів — ланцюги перенесення, мультиплексори, весь суматор — і пояснити, чому критичний шлях через цей ALU встановлює стелю тактової частоти.Ви починали, не знаючи, що таке %rsp. Тепер ви можете читати асемблер x86, простежити стековий кадр через виклик функції, пояснити, що таке червона зона, і зневадити segfault, оглянувши регістри та пам'ять.Ви починали з припущення, що покажчики і масиви — це те саме, бо так сказано в підручнику. Тепер ви можете простежити розіменування покажчика через усю апаратуру трансляції віртуальної пам'яті — обхід таблиці сторінок, влучання чи промах у TLB, пошук у кеші L1, можливо, вихід через контролер пам'яті до DRAM — і за секунди сказати комусь, чому їхній цикл повільний.Ви починали з думки «мій код швидкий, бо він короткий». Тепер ви можете міркувати про cache-friendliness — рядковий порядок проти стовпцевого, розмір робочого набору проти ємності L1/L2/L3, false sharing між потоками — і передбачити, яка з двох алгоритмічно еквівалентних реалізацій виграє на апаратурі.Ви починали без моделі того, «чому мій багатопотоковий код ламається». Тепер ви можете пояснити конвеєрні конфлікти, моделі впорядкування пам'яті та чому наївний спільний лічильник є неправильним у такий спосіб, який жодний тест-сюїт не впіймає.Ви починали, читаючи «96-ядерний чіплетний процесор» як маркетингову фразу. Тепер ви можете сперечатися про чіплетні архітектури — математику виходу придатних, I/O-кристал проти обчислювального, UCIe, теплову ціну 3D-стекування — і сказати, яка з двох анонсованих CPU — це інженерія, а яка — театр за специфікацією.Жодне з цього не є дрібницею. Кожен такий навик — це те, що робить наступний курс із систем — операційні системи, компілятори, бази даних, розподілені системи, ML-системи, безпеку — посильним, а не довільним. Ви більше ніколи не прочитаєте «промах кеша L1» у статті, не знаючи точно, яка фізична подія описується. Це не дрібниця. Це серцевина інженера.<br><img alt="П'ятнадцятитижнева арка курсу, згрупована в сім тематичних блоків — від фізики та вентилів (L01–L02) через x86, системну архітектуру, пам'ять, моделі та продуктивність до цієї лекції (L15) як синтезу." src="assets/l15-course-arc.svg" target="_self" style="width: 820px; max-width: 100%;">Ментальна модель, яку ви побудували в цьому курсі, — це не коробка, яку ви закриваєте в останній день. Це субстрат, на якому стоїть кожен інший курс із систем.Коли підручник із баз даних каже вам, що B-дерева для дискових індексів кращі за збалансовані двійкові дерева пошуку, ви вже знаєте чому — вузли розміром зі сторінку, менше рівнів непрямого доступу, локальність-доброзичливий обхід, і блочна модель передавання диска, яка б'ється проти вас, якщо йти меншими одиницями.Коли стаття про розподілені системи пояснює, чому RDMA швидший за TCP, ви знаєте чому — він пропускає ядро, обминає копіювання в простір користувача, дозволяє NIC писати напряму в заздалегідь закріплений буфер.Коли автор компілятора говорить про тиск на регістри, escape-аналіз чи винесення інваріантного коду з циклу, ви знаєте, що ці оптимізації намагаються зробити з машиною — а не лише з проміжним представленням.Коли ML-системний інженер пояснює, чому інференс LLM обмежений пам'яттю, а не обчисленнями, ви можете перевірити це твердження через арифметичну інтенсивність ядра й пропускну здатність HBM3.Коли курс з операційних систем на третьому тижні вводить віртуальну пам'ять, вам не знадобиться вся лекція — картина у вас вже є. Коли стаття з безпеки говорить про Spectre чи Rowhammer, ви розумієте, який мікроархітектурний стан у основі робить такі атаки можливими.Кожна з цих розмов, які раніше звучали б як жаргон чужої галузі, стає конкретною.Це і є тихе здобуття. Архітектура не є окремою кар'єрною стежкою для більшості з вас. Це предмет, який робить кожну іншу кар'єрну стежку прочитною. Старші інженери, яких ви зустрінете, — ті, чиї код-рев'ю варті більше, ніж чужий код, — майже завжди мають якусь версію тієї картини, яку ви щойно побудували. Вони могли побудувати її інакше, в іншому порядку, переважно на роботі. Але вона в них є. Тепер — і у вас.У порядку довгострокової важливості — бо на момент вашого випуску половина конкретних фактів із цього курсу буде застарілою, а ці три звички досі працюватимуть.1. Читайте архітектуру як компроміси під обмеженнями, а не як перелік можливостей. Це найглибше, чого навчив курс. Кожне рішення в кожній лекції — від кількості транзисторів у вентилі NAND до політики диспетчеризації в Apple M1 — це вибір, зроблений під обмеженнями: бюджет потужності, бюджет площі, бюджет пропускної здатності, допустима затримка, вартість виробництва, зворотна сумісність із десятиліттям уже поставленого програмного забезпечення.Коли ви дивитеся на новий чіп, нову ISA чи нову технологію пам'яті, вашим першим рухом має бути назвати обмеження, під якими вона працює, а потім запитати, чому вона зробила саме такий вибір. «Оптимізовано для X за рахунок Y» — ось речення, яке ви маєте вміти написати про будь-який дизайн, з яким стикаєтеся.2. Профайліть перед оптимізацією. Курс ніколи не давав вам рецепт «зробіть це швидким». Він давав вам інструменти спочатку виміряти — perf, rdtsc, лічильники кеша, лічильники невірних передбачень переходів, формули AMAT, межі Амдала — а потім вирішувати. Інженери, які оптимізують «за відчуттями», марнують тижні, прискорюючи ділянки коду, що не важать, тоді як справжнє вузьке місце сидить в іншій функції. Вам неодноразово показували, що вузьке місце рідко там, де ви б вгадували. Зберігайте цю позицію.3. Скромність щодо машини. Кожна лекція робила машину на один шар конкретнішою, а отже, кожна лекція також робила шари нижче того, що ми охопили, помітнішими як прогалини. Ви тепер знаєте досить, щоб бачити, чого не знаєте: внутрішнє у ОС на глибину, мережеву апаратуру, кремнієву реалізацію, аналоговий дизайн, CAD-інструменти, корпусування, самі фабрики, силову електроніку, стек прошивок, ланцюг постачання.Хороший інженер веде поточний інвентар шарів, крізь які він не бачить, і знає, де шукати допомогу, коли проблема витікає через один із цих кордонів. Такий інвентар у вас тепер є. Трактуйте його як карту, куди йти далі, а не як провал не знати всього.Інженери, які найбільше шкодять собі у кар'єрі, — це ті, хто не знає, чого не знає, — і впевнено лізуть у проблеми, глибину яких не можуть виміряти. Ви цей ризик уже пройшли, і це вартує чимало.Це кар'єрні пастки, проти яких у вас тепер є тло, щоб чинити опір, — і в які ви постійно бачитимете, як падають колеги.Вірити бенчмаркам, не розуміючи, що вони міряють. Бенчмарк — це конкретне робоче навантаження, запущене на конкретній конфігурації, проти конкретної метрики. Коли хтось каже вам «чіп A на 20 % швидший за чіп B», ваша перша реакція — «на якому робочому навантаженні, з яким компілятором, у якому енергобюджеті, на якій конфігурації пам'яті, виміряно за яке вікно?». Дивовижно багато галузевих дебатів — це люди, які цитують числа, контекст яких мовчки знято. Geekbench, SPEC, MLPerf, навіть ваші власні внутрішні навантажувальні тести — кожне міряє щось реальне, але число означає лише те, що каже розділ про методологію. Читайте методологію. Якщо її немає, числа теж немає.Оптимізувати без профайлінгу. Див. навик №2 вище. Інженери навколо вас цього року витратять справжні тижні на ручне тюнінгування коду, якого нема на критичному шляху, — переписуючи функцію, що дає 0,3 % загального часу виконання, додаючи SIMD у цикл, який виконується один раз під час запуску, мікрооптимізуючи внутрішній блок, тоді як 10-кратне алгоритмічне поліпшення лежить недоторканим на рівень вище. Не будьте одним із них. Завжди спочатку профайл, потім рішення. perf, flamegraph, pprof, Instruments — інструмент залежить від вашої платформи, але дисципліна універсальна.Сприймати «більше заліза» як розв'язання програмної проблеми. Хмара робить цю пастку дешевою, а отже — ендемічною. Квадратичний алгоритм не перестає бути квадратичним, коли ви запускаєте його на більшому інстансі, — він просто розоряє вас повільніше. Запит до бази даних, що робить повне сканування таблиці, не «полагоджено» швидшим диском; його лише тимчасово приховано. Витік пам'яті не зникає на машині з більшою кількістю ОЗП; йому просто потрібно довше, щоб упасти.Найстабільніша оптимізація, як стало зрозуміло в §7, — це та, що робить обсяг роботи меншим. Спочатку алгоритми. Потім структури даних. Залізо — лише коли алгоритм і структури даних правильні, і ви профайлили, аби довести, що справжнє вузьке місце нижче за програмний шар.Якщо ви на треку 6 ECTS, у вас попереду ще одна подія: захист проєкту. Сприймайте його як перший інженерний дизайн-рев'ю у вашій кар'єрі, бо саме ним він і є.Дизайн-рев'ю — це не демо. Демо каже: «ось робоча штука». Дизайн-рев'ю каже: «ось твердження, ось докази, ось простір компромісів, який я дослідив, ось що я обрав і чому, ось обмеження, про які я знаю, і ось що я зробив би інакше наступного разу».Ваші викладачі бачили тисячі демо. Те, що вони слухають, — це здатність міркувати: чи можете ви захищати свої рішення під питаннями і чи достатньо знаєте машину, щоб сказати, чому один дизайн кращий за інший саме для вашого робочого навантаження.Конкретно, сильний захист відповідає на такі питання:
Яку проблему це розв'язує і хто її має? — обсяг і мотивація в одному абзаці.
Яке ключове проєктне рішення і від чого ви відмовилися? — рамка компромісу; які альтернативи ви розглянули і чому обрали саме цю?
Чого це коштує — у пам'яті, у часі, у складності? — чесний інженерський облік.
Де воно ламається? — межі, які ви вже виявили, а не ті, про які комітет має вам сказати.
Що ви переробили б? — доказ того, що ви розумієте власний дизайн, а не лише його побудували.
Якщо щось пішло не так, не ховайте це. Назвіть, поясніть, чого навчилися, і йдіть далі. Захист від чесності сильніший, а не слабший. Найслабші захисти — ті, що вдають, ніби все працювало ідеально; найсильніші — ті, де студент володіє своїми компромісами.Принесіть таблицю компромісів. Буквально — маленьку матрицю «альтернативи проти критеріїв», із вашим вибором, обведеним у кружечок. Вона за десять секунд сигналізує, що ви думали як інженер.У 2005 році, у вівторок, який тоді мало хто помітив, безкоштовний обід закінчився. Тактові частоти перестали рости. Однопотокова продуктивність перестала подвоюватися кожні вісімнадцять місяців. Хвиля, що несла ціле покоління програмістів, — «пишіть що завгодно, CPU наступного року зробить це швидким», — тихо вичерпалася. Відтоді кожен приріст продуктивності треба було проєктувати навмисно: кимось, хто розумів паралелізм, кеші, ієрархію пам'яті, спеціалізоване залізо або термодинаміку стійки в центрі обробки даних.Щороку відтоді розрив між інженерами, які розуміють, що відбувається всередині машини, і тими, хто не розуміє, розширюється. Перші будують інфраструктуру, на якій працює світ, — компілятори, середовища виконання, бази даних, планувальники, ML-системи, операційні системи, усю сантехніку хмари. Другі дедалі більше залежать від перших, часто про це не знаючи, і дедалі більше обмежені в тому, які проблеми вони взагалі здатні побачити, не кажучи вже розв'язати.Ви зараз на правильному боці цієї лінії.Це не подарунок курсу. Це позиція, яку ви заробили за п'ятнадцять тижнів, роблячи роботу.Використовуйте це. Будуйте системи такого штибу, які можуть побудувати лише люди з вашого боку лінії. Коли натрапите на архітектуру, про яку ніколи не чули, застосуйте питання з §9 — мета, обмеження — і йдіть далі. Коли хтось навколо вас тягнеться до «більше заліза» як першої реакції, заперечте профілем. Коли читаєте статтю, читайте її крізь ту карту, яку ви тепер маєте.Курс закінчено. Робота починається.Після цього курсу ви можете: подивитися на комп'ютер і бачити крізь його абстракції аж до вентилів — і назад угору до програмного забезпечення — зі зваженим судженням на кожному шарі.
Безкоштовний обід закінчився у 2005 році. Масштабування за частотою вперлося у теплову стіну, коли зламалося деннардівське масштабування; кожен приріст продуктивності відтоді вимагав свідомої архітектурної роботи.
Два закони масштабування обмежують простір проєктування. Закон Амдала обмежує сильне масштабування межею 1/(1−p); закон Густафсона передбачає лінійне прискорення, коли розмір задачі росте разом із ресурсами. Обидва мають слушність — вони відповідають на різні питання.
Потужність описується формулою P = αCV²f + витік. Член V² робить DVFS (динамічне масштабування напруги та частоти) найефективнішою технікою керування енергією в сучасних процесорах.
Dark silicon — видиме обличчя постденнардівської епохи. Ви не можете живити всі транзистори одночасно — тож або спеціалізуєте, або тьмяните, або вимикаєте живлення.
Гетерогенність — вимушена відповідь галузі. big.LITTLE, P+E-ядра, SoC з GPU+NPU+DSP+ISP — усе це веде та сама арифметика ефективності.
Чіплети та 3D-стекування рятують економіку закону Мура. Менші кристали дають кращий вихід придатних; HBM і V-Cache розв'язують стелі пропускної здатності, з якими монолітна пам'ять не впоралася.
Обчислення тепер мають планетарний слід. PUE, цілодобова безвуглецева енергія та алгоритмічна ефективність — це важелі сталості, якими тепер спільно володіють програмісти та архітектори.
Мета-навик — читати компроміс. Для будь-якої нової архітектури ви маєте вміти визначити її цільове робоче навантаження та її фундаментальне обмеження.
Що буде далі:
Практичний контроль II (для всіх студентів): охоплює лабораторні P11–P14 (NASM/Linux, профайлінг, віртуальна пам'ять, SIMD). Принесіть те, що ви зробили для цих лабораторних; будьте готові пояснити архітектурні рішення.
Захист проєкту (лише 6 ECTS): презентація фінального проєкту. Сприймайте захист як інженерний дизайн-рев'ю — твердження, докази, таблиця компромісів, чесна оцінка обмежень.
Питання, на які відповідає сильний захист:
У чому полягала проблема в архітектурних термінах?
З якими обмеженнями ви зіткнулися (пам'ять, кеш, пропускна здатність, затримка, потужність)?
Який архітектурний важіль ви повернули (ширина SIMD, локальність кеша, передбачення переходів, паралелізм) — і чому саме його першим?
Як ви виміряли ефект?
Що зробили б інакше, якби мали ще тиждень?
Подальше читання (необов'язково):
Hennessy &amp; Patterson, Computer Architecture: A Quantitative Approach — канонічний текст для магістерського рівня; розділ 1 повторює все з цієї лекції з твердими числами.
Software Engineering at Google — розділ 21 (Scaling) застосовує закон Амдала до проєктування сервісів.
Intel Optimization Reference Manual та AMD Software Optimization Guide — практичні посібники для наступного шару деталей. <br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Amdahl%27s_law" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Amdahl%27s_law" target="_self">Amdahl's Law (Wikipedia)</a> — оригінальна публікація 1967 року; виведення стелі прискорення
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Gustafson%27s_law" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Gustafson%27s_law" target="_self">Gustafson's Law (Wikipedia)</a> — переосмислення 1988 року; рамка слабкого масштабування
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Dynamic_voltage_scaling" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Dynamic_voltage_scaling" target="_self">Dynamic Voltage Scaling (Wikipedia)</a> — механізми DVFS, регулятори, залежність V-f
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Dark_silicon" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Dark_silicon" target="_self">Dark Silicon (Wikipedia)</a> — активна площа, обмежена TDP; прогнози Есмаелзаде та ін.
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/ARM_big.LITTLE" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/ARM_big.LITTLE" target="_self">ARM big.LITTLE (Wikipedia)</a> — гетерогенні обчислення на ARM; режими планування
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Chiplet" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Chiplet" target="_self">Chiplet (Wikipedia)</a> — модульна архітектура кристалів; AMD Zen, Intel tiles, UCIe
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Neuromorphic_computing" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Neuromorphic_computing" target="_self">Neuromorphic Computing (Wikipedia)</a> — архітектури, натхненні мозком; Loihi, TrueNorth
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Three-dimensional_integrated_circuit" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Three-dimensional_integrated_circuit" target="_self">3D Integrated Circuit (Wikipedia)</a> — TSV, стековані кристали, теплові проблеми
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Heterogeneous_computing" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Heterogeneous_computing" target="_self">Heterogeneous Computing (Wikipedia)</a> — системи зі змішаною архітектурою; виклики програмування
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Data_center" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Data_center" target="_self">Data Center (Wikipedia)</a> — PUE, охолодження, глобальне споживання енергії
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/CMOS" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/CMOS" target="_self">CMOS Power Dissipation (Wikipedia)</a> — динамічний, статичний, наскрізний компоненти
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Dennard_scaling" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Dennard_scaling" target="_self">Dennard Scaling (Wikipedia)</a> — закон масштабування 1974 року; злам у 2005 році
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Moore%27s_law" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Moore%27s_law" target="_self">Moore's Law (Wikipedia)</a> — стан на 2025 рік; сповільнення темпу; економічний тиск
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/High_Bandwidth_Memory" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/High_Bandwidth_Memory" target="_self">High Bandwidth Memory (Wikipedia)</a> — 3D-стекована DRAM; монтована на інтерпозер; HBM4 (2025)
Зображення: усі ілюстрації, атрибутовані в підписах, взято з Wikimedia Commons за ліцензіями CC-BY-SA. Повна інформація про ліцензії — на відповідних сторінках Wikimedia Commons.]]></description><link>lectures/l15.-енергоефективність,-обмеження-та-майбутні-тренди.html</link><guid isPermaLink="false">Lectures/L15. Енергоефективність, обмеження та майбутні тренди.md</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-moores-law-transistor-count.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-moores-law-transistor-count.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-ENG. Energy Efficiency, Limitations, and Future Trends]]></title><description><![CDATA[
Final lecture of Computer Systems Architecture course at NaUKMA. Arguments are cumulative — each section answers a question raised by the previous one.
After this lecture you will:
Apply Amdahl's and Gustafson's laws to decide whether a workload benefits from more parallelism, and explain the difference between strong and weak scaling (LO5)
Reason about a chip's energy budget using the CMOS power model (dynamic, static, short-circuit) and explain why DVFS, dark silicon, and heterogeneous cores are all responses to the same physical limit (LO5, LO6)
Argue for or against a design decision — big-core vs LITTLE-core, monolithic die vs chiplet, general-purpose CPU vs domain-specific accelerator — on the basis of performance, energy, cost, and sustainability (LO7)
Fifteen weeks ago you didn't know what a transistor did inside a NAND gate. You now can trace a pointer from a C variable through a page table, watch it miss in L1 and crawl to DRAM, observe the load stall a superscalar pipeline, see the branch predictor guess wrong and flush speculative state, and reason about whether the whole sequence should have run on a P-core, an E-core, or somewhere else entirely. Every lecture in this course introduced one region of the machine — physics of gates (L01), ISAs (L02, L03), memory (L04, L08), operating systems and virtual memory (L06, L07, L09), caches (L11), floating point (L12), pipelining and speculation (L14). Each of those regions was, in its time, the frontier. This lecture is different. This lecture is the moment we step back and ask where we are now, why the landscape has the shape it does, and where the frontier has moved to next.Here is a picture that contains most of the story. Put transistor count on a log axis from 1970 to today, and plot every CPU you can find. The curve rises in an almost perfect straight line — Moore's Law, still holding, cadence slowing but not stopped, a modern GPU now crossing 90 billion transistors on one die. That curve is the first of three.<img alt="Moore's Law: transistor count per microprocessor from 1970 to 2020, doubling roughly every two years on a log scale." src="assets/l15-moores-law-transistor-count.png" target="_self" style="width: 700px; max-width: 100%;">
Moore's Law, the curve that kept going. Chart from Our World in Data via Wikimedia Commons (CC-BY).Now plot two more curves on the same timeline. Plot the clock frequency of flagship CPUs — the number on the box, the GHz everyone used to brag about. That curve climbs steeply from 1 MHz in 1980 to about 3 GHz in 2004, and then, almost overnight, it stops. For twenty years it has drifted sideways in the 3–5 GHz band. And plot the number of cores per chip. That curve was a dead flat 1.0 from 1970 to 2004, and then it kicked up into its own exponential — 2, 4, 8, 16, 64, 96 cores per package by 2024.<br><img alt="CPU clock frequency plateaus after 2005 while core count begins its own exponential rise — the post-Dennard bifurcation." src="assets/l15-cpu-clock-core-count.png" target="_self" style="width: 700px; max-width: 100%;">
Clock rate vs core count. The clock curve flattens around 2005; the core-count curve takes off the same year. Chart via Wikimedia Commons.<br><img alt="Three curves on one timeline: transistor count keeps rising, clock frequency flattens after 2005, core count begins its own exponential climb." src="assets/l15-scaling-laws-timeline-eng.svg" target="_self" style="width: 700px; max-width: 100%;">Three curves. Transistors still climbing. Frequency flat since 2005. Core count rising since 2005. That's the puzzle this lecture is built around: what happened in 2005, and what has the industry been doing about it ever since? Something broke, or more precisely, something stopped being free. And every architectural trend you've read about in the last twenty years — multicore, SIMD, heterogeneous chips, chiplets, GPUs eating the AI world, Apple Silicon outrunning Intel on a laptop battery — is an answer to that break.The lecture proceeds in five beats. First, how far can parallelism take us? — Amdahl and Gustafson, the two laws that bound what throwing more cores at a problem can and cannot do. Second, why is energy the limit? — the CMOS power equation, DVFS, and the phenomenon called dark silicon that means your 96-core chip cannot run all 96 cores at full speed at once. Third, what did industry actually do about it? — heterogeneous cores, chiplets, 3D stacking, domain-specific accelerators. Fourth, what does this cost the planet? — data center power, water, carbon, electronic waste. Fifth, where next? — a quick tour of the post-silicon question, and a closing word on which of this course's skills follow you into whatever comes.This is the last lecture, which means it's also the last time you'll sit in this particular room with this particular framing. The stance we've been practicing all term — read an architecture as a set of trade-offs made under constraints, not as a specification to memorize — is exactly the stance you need to read the news about TSMC's 2nm process, or AMD's next chiplet generation, or Google's latest TPU, or whatever photonic startup raises a Series A next month. The map has a fork in it now. You have the tools to read both branches.After this section, you can: read a marketing announcement for a modern CPU and identify which of the post-2005 forces — parallelism ceiling, energy wall, dark silicon, heterogeneity, specialization — the announcement is responding to.You've spent a weekend rewriting your photo-processing app. The old version processed one image at a time on one thread; the new one splits each batch across all 8 cores of your M1 laptop. You expected 8× speedup. You measured 2.5×. A classmate looks at your benchmark, shrugs, and says "sounds like a bug — maybe your thread pool is broken, or you're serializing on a lock somewhere." Is your classmate right?Maybe. But here's the thing: even if your code is flawless, 2.5× might be exactly the number the math predicts. This isn't a hypothetical, by the way — if you ever benchmarked a make -j8 build on your laptop and noticed that the build finished about 3× faster instead of 8×, you've already lived this puzzle. To see why, we need to be honest about what "parallelizable" really means in a real program.Pick up any real workload and ask where its time actually goes. Your photo app opens a file (sequential — one disk head, one file descriptor). It decodes a JPEG header (sequential — can't decode byte 100 until you know what byte 50 means). It splits the pixel array into 8 tiles (embarrassingly parallel — one tile per core, independent work). It merges the results back into one output buffer (mostly sequential, with some atomic bookkeeping). It writes the output file (sequential again). The parallel kernel is fast on 8 cores, but it sits inside a sandwich of serial bread, and the bread doesn't get any faster just because you added cores.Call the fraction of time your program spends in parallelizable work p, and the fraction spent in strictly serial work (1 − p). Give the parallel part N cores, and it completes in time p/N instead of p. The serial part is unaffected and still takes (1 − p). Total execution time shrinks from 1 toand the speedup is the ratio of old time to new time:This is Amdahl's Law, published by Gene Amdahl in 1967. Now put numbers in. Suppose your photo app is 60% parallelizable (the pixel-tiling step) and 40% serial (everything else). With N = 8 cores:Your classmate was wrong. The measured 2.5× isn't a bug; it's within spitting distance of the ceiling the serial fraction has already set. If you'd seen 8×, that would be surprising.<br><img alt="Amdahl's Law speedup curves for different parallel fractions. The asymptote sits at 1/(1−p) no matter how many processors you add." src="assets/l15-amdahls-law-eng.svg" target="_self" style="width: 600px; max-width: 100%;">
Amdahl's Law. Chart from Wikimedia Commons (CC-BY-SA).Now push Amdahl's formula to its limit. Take N to infinity. The parallel term p/N collapses to zero, and speedup approachesThat's the speedup wall: no matter how many cores you buy, the serial fraction puts a hard ceiling on what parallelism can do for you. Sit with the numbers for a moment, because they are brutal:Every extra "9" on the parallel fraction buys you one order of magnitude more speedup. And every extra 9 is harder to get than the last — the sequential chunks that remain are the ones you couldn't parallelize the first time around, usually because they have real data dependencies, not because you were lazy. This is why optimizing "the biggest chunk" of runtime beats optimizing "the easiest chunk" by a mile: if 75% of the program is in the parallel region and you speed that region up infinitely, you get 4× total. If 25% is in the parallel region, you get at best 1.33×. Pick your battles by time share, not by how interesting the code looks.<br><img alt="Optimizing the larger part of a task yields more total speedup than optimizing a smaller part, even if the smaller part's local improvement is greater." src="assets/l15-optimizing-different-parts-eng.svg" target="_self" style="width: 600px; max-width: 100%;">
Optimizing where the time actually is. Chart from Wikimedia Commons (CC-BY-SA).If Amdahl's ceiling is as hard as that table says, a reasonable person should ask the obvious follow-up: why did Google spend roughly $100 billion on data centers full of cores? Why does NVIDIA sell racks with tens of thousands of GPUs? Why is the entire cloud business built on horizontal scaling? Either the entire industry is collectively wrong about the value of parallelism, or Amdahl isn't telling the whole story.The answer is the second one. Amdahl's Law answers a specific question: given a fixed problem, how much faster does it finish with more cores? That's a legitimate question, and for some workloads it's the only question that matters — a transaction that must commit in 5 ms, a game frame that must render in 16 ms, a query that must respond in 100 ms. But it's not the only question. There is a whole different question hiding in the same word "speedup," and it's the one the scale-out cloud is actually answering.In 1988, John Gustafson and Edwin Barsis at Sandia National Labs published a short paper called "Reevaluating Amdahl's Law." Their observation was deceptively simple: researchers don't use faster machines to solve the same problem faster. They use them to solve bigger problems in the same time. When Sandia got a 1024-processor machine, the physicists didn't run last year's 64-processor simulation 16× faster. They ran this year's simulation — finer mesh, more particles, longer timescale — in the same overnight window. The parallel part grew with the hardware. The serial part stayed roughly the same.If you fix wall-clock time and scale the workload with N, the math turns inside-out. Let s be the fraction of wall time the scaled workload spends in serial code, and p = 1 − s the fraction in parallel code. One sequential processor would need time s + p × N to do the whole job (because the parallel part, which now runs across N processors, represents N times more work than one processor could have done alone). So the speedup compared to the sequential machine isThat's Gustafson's Law, and the remarkable thing about it is that S grows linearly with N. There is no ceiling. Double the cluster, double the problem size, double the effective throughput — as long as the serial part really does stay constant while the parallel work expands.Plug a number in to feel the difference. With the same 60% "parallel" profile as our photo app but reinterpreted in Gustafson's sense — that is, 60% of the wall time is spent in parallel work while the serial 40% stays fixed — 8 cores give youeffective throughput compared to a single core doing the same-wall-time job. And at N = 64, Gustafson promises 38.8×, while Amdahl still caps out near 2.46×. Same workload profile, wildly different verdicts — because the two laws are measuring two different things.<br><img alt="Gustafson's Law speedup curves. With problem size scaling alongside processor count, speedup grows linearly with N." src="assets/l15-gustafsons-law.png" target="_self" style="width: 600px; max-width: 100%;">
Gustafson's Law. Chart from Wikimedia Commons (CC-BY-SA).Amdahl and Gustafson aren't contradicting each other; they're answering different questions. Both laws are correct, and the choice of which to invoke depends on what you're actually trying to do.The formal names for the two questions are strong scaling and weak scaling. Strong scaling (Amdahl's world). Fix the problem, vary the processors. "How fast can I solve THIS problem?" The workload is fixed — a query, a frame, a transaction. Adding cores shrinks wall time but hits the 1/(1−p) ceiling. Weak scaling (Gustafson's world). Scale the problem with the processors. "How big a problem can I solve in reasonable time?" The per-processor work is fixed — each core keeps roughly the same amount to do. Adding cores lets you attack bigger or finer problems in the same wall-clock budget. Some lived examples to anchor the two:
Web search — you type a query, you want results in under 100 ms, the corpus is what it is. That's strong scaling. Latency is the constraint; adding machines only helps if they shrink the critical path, and Amdahl's ceiling is real.
Climate simulation — the physicists want to run overnight, whatever "overnight" gets them. Give them 10× the cores and they don't finish faster; they run a finer grid, a longer simulation, or more ensemble members in the same 8 hours. That's weak scaling, and Gustafson's linear growth is exactly what they see.
Large-scale ML training — the team has a 24-hour training budget (because that's when the next experiment needs to start). Give them 4× the GPUs, and they don't train 4× faster; they train a 4× larger model, or on 4× more data, in the same day. Gustafson's world.
Video encoding on your laptop — a fixed 90-minute movie, you want the encode done before dinner. Strong scaling; Amdahl's ceiling applies.
The test to ask yourself, every time: when I add hardware, does the problem stay the same size, or does it grow? If it stays the same size, you're in Amdahl's world and the ceiling is real. If it grows, you're in Gustafson's world and you can keep scaling. Hyperscalers live in Gustafson's world on purpose — they design their workloads to scale weakly, because that's the only regime where spending $100 billion on cores makes economic sense.Neither Amdahl nor Gustafson accounts for what actually happens when you glue 1000 cores together: memory bandwidth saturates, cache coherence traffic explodes, locks start contending, and NUMA latencies bite. Neil Gunther's Universal Scalability Law adds two penalty terms — a linear term for contention and a quadratic term for coherence — and the prediction it makes is uncomfortable: past a certain N, real systems don't just stop speeding up. They slow down. Adding the 1001st core can make the whole cluster worse than the 1000-core version. If you've ever watched a database benchmark where throughput peaks at 32 connections and then degrades as you push toward 64 and 128, you've seen USL on a graph. That's not a theoretical oddity; it's why production systems have a "sweet spot" core count beyond which performance declines, and why distributed systems engineers earn their salaries taming coherence and coordination overhead. We won't derive USL here, but keep it in the back of your head: Amdahl is the optimist's ceiling, Gustafson is the bigger-is-better reframe, and USL is the honest pessimist who reminds you that real hardware has friction.We've just established that parallelism has a ceiling and that scaling the problem can push it sideways but not eliminate it. What we haven't explained is the physical reason architects stopped scaling frequency and pivoted to cores in the first place. The 2005 break in those three curves wasn't because someone ran out of ideas — it was because physics ran out of headroom. Energy is the next chapter, and it's the one that decides which of these two laws you get to live inside.After this section, you can: estimate the theoretical speedup ceiling for a workload given its serial fraction, tell whether a given real system (web search, climate sim, ML training, video encoding) is in Amdahl's world or Gustafson's world, and explain why the "buy more cores" strategy works for hyperscalers but often fails for fixed-latency workloads.Here's a puzzle that should bother you. Open the datasheet for a late-1990s mobile CPU and its core runs at around 3.3 V. Open the datasheet for a 2010 phone SoC and it's down near 1.2 V. Open the datasheet for a 2024 phone SoC and you'll see core rails below 0.8 V, with certain deep-sleep states creeping toward 0.6 V. Twenty years of chip designers — at Intel, AMD, Qualcomm, Apple, ARM — fighting tooth and nail to push the operating voltage down. Frequency was already scaling happily on its own. Why spend so much engineering effort on voltage?The answer lives in one term of one formula. But the formula only makes sense once we've asked the upstream question: where does the power in a chip actually go, and why is there a ceiling on how much you can spend?Nothing a chip does matters if it melts. That sentence is not dramatic — it is the master constraint. Every transistor switching dissipates energy as heat, and that heat has to leave the package through some combination of a heat spreader, a heatsink, a fan, a fluid loop, or the metal body of your phone. If heat goes in faster than it comes out, the silicon temperature climbs, and somewhere between 95 °C and 105 °C the transistors stop behaving — first they get slow and leaky, then they get unreliable, then the chip cooks itself.So every package ships with a number called Thermal Design Power (TDP) — the sustained power dissipation the cooling solution is designed to handle. A laptop CPU might have TDP = 28 W. A desktop gaming CPU, 170 W. A phone SoC, 5 W. A server GPU, 700 W. That number is not a suggestion. It is the ceiling every other architectural decision downstream has to live under. Design your core to draw 300 W, and you'd better ship a 300 W cooler with it — and at some point physics stops cooperating with your radiator.This is why TDP frames everything that follows. Dark silicon (§5), DVFS (§4), heterogeneous cores (§6), chiplets (§8) — all of them are, at root, ways to do more useful work under a fixed thermal envelope.Total power dissipation of a CMOS chip breaks into three terms:P_total = P_dynamic + P_static + P_short-circuitThe first is what you pay for getting work done. The second is what you pay for existing. The third is a small transient tax we'll mostly ignore. Let's build the first one from scratch, because it is the one you need in your head.Every logic gate in a CMOS chip is driving some load capacitance C — the gate capacitance of the transistors it's feeding, plus the wire capacitance between them. When the output of that gate switches from 0 to 1, it has to charge C up from ground to the supply voltage V. The charge delivered is Q = CV. The energy pulled from the supply for that charge is E = QV = CV², and half of that ends up stored in the capacitor while the other half dissipates as heat in the driving transistor. Switching back from 1 to 0 dumps the stored half as heat too. So a full 0→1→0 cycle costs exactly CV² in dissipated energy — that's your energy-per-switch.Now how often does that switch happen? The clock ticks f times per second. If the gate switched on every cycle, it would dissipate CV²f watts. But most gates don't — most bits in a datapath don't flip every cycle. Call the fraction of cycles on which a given gate actually toggles its activity factor α. Sum over all gates on the chip and the result is the classic formula:P_dynamic = α · C · V² · fThree lines. Charge CV, energy CV², do it f times per second on an α fraction of gates.Look at that formula again. Frequency appears linearly. Capacitance appears linearly. Activity appears linearly. Voltage appears squared. That quadratic is the answer to the opening puzzle.Halve V and dynamic power does not halve — it drops to a quarter. Compute it out loud: V → V/2 means V² → V²/4, so P_dynamic falls 4× before you touch frequency at all. Go from 3.3 V down to 1.1 V, a factor-of-three cut, and the V² term alone buys you a 9× reduction in dynamic power. Now we can see why engineers chased voltage so hard for twenty years. It's the only lever in the formula with a superlinear return. Shrinking capacitance helps (and process nodes do shrink it), but capacitance reductions are modest and hard-won. Voltage reductions are pure multiplier.This same quadratic is why DVFS, the subject of §4, is so extravagantly effective. Spoiler: when you lower V and f together, the savings are cubic, not linear. We'll cash that in next section.<br><img alt="CMOS power breakdown from 130 nm to 5 nm: dynamic power still dominates, but the leakage (static) share grows from 5% to 45% over two decades of shrinking." src="assets/l15-cmos-power-breakdown-eng.svg" target="_self" style="width: 700px; max-width: 100%;">α is not a fixed number — it varies wildly across different parts of the chip, and that variation is itself a design opportunity.The clock tree switches on every single cycle, by definition. Its α is essentially 1.0. And because the clock distribution network fans out to every flip-flop on the die, it has enormous capacitance. Measure real chips and you'll find the clock tree alone accounts for 20-30% of total dynamic power. That is an enormous bill for a signal that, from the computation's point of view, isn't doing any work — it's just a metronome.This is why clock gating is universal in modern designs. If a functional block isn't doing anything this cycle — the ALU is idle, the branch predictor hasn't been queried, the video decoder isn't running — firmware or hardware logic AND-gates the clock off at the root of that block's subtree. The flip-flops stop toggling. Their α drops to zero. Their contribution to dynamic power vanishes for as long as the clock stays gated. Every serious chip gates aggressively at multiple granularities: per-functional-unit, per-core, per-cluster.Data logic is a different story. On average, α for ordinary datapath gates sits around 0.1 — most bits don't flip every cycle because most data doesn't change that often. A register that holds a loop counter toggles its low bits constantly but its high bits almost never; a wire carrying a pointer usually holds the same value for many cycles in a row. Knowing that α is low lets designers get away with lots of combinational logic without paying a proportional power cost.Dynamic power is the bill for work. Static power is the bill for existing. Even when a transistor isn't switching — when the clock is gated, when the core is idle, when the phone is in your pocket — electrons still find their way across it, and that current, multiplied by voltage, is dissipated as heat.Three mechanisms contribute:
Subthreshold leakage. Even when the gate voltage is below the threshold V_th that's supposed to turn the transistor "off," a small current trickles from source to drain. This current is an exponential function of V_th — knock V_th down by a hundred millivolts and leakage can jump by an order of magnitude. Subthreshold leakage is the dominant term at 45 nm and below.
Gate leakage. Once the gate oxide gets thin enough — below about 2 nm — electrons start tunneling through it rather than being blocked. This was a crisis around the 65 nm node, solved starting at 45 nm by replacing silicon dioxide with a high-κ dielectric (hafnium oxide), which lets designers use a thicker physical layer for the same electrical effect. Intel shipped this first in Penryn (2007).
Reverse-bias junction leakage. Small currents across reverse-biased p-n junctions in the substrate. A small contribution compared to the first two.
Here is the trap, and it's a trap you can't design around: smaller transistors need a lower V_th to keep switching fast at lower supply voltages. But the exponential relationship between V_th and subthreshold leakage means every shrink that helps speed also hurts leakage. FinFET and gate-all-around transistors claw some of it back by giving the gate better electrostatic control over the channel, but the fundamental squeeze doesn't go away. At 7 nm and below, static power can easily be 30-50% of total chip power — a full swing of the pie chart compared to the 130 nm era, when leakage was a rounding error.This is a one-way door. You cannot go back to fatter transistors without surrendering the density gains that made the shrink worthwhile in the first place. Leakage is the permanent tax you pay to keep Moore's Law limping forward.So far we've talked about power, measured in watts — the instantaneous rate of energy dissipation. For a phone on battery, what actually matters is energy, measured in joules:E = P · tEnergy is what drains the battery. Power is what heats the chip. They are related by time, and that relation opens up a strategy that would otherwise seem paradoxical.Suppose you have a 10-second task. You can run it at 2 W for 10 seconds (20 J total), or you can run it at 4 W for 5 seconds (20 J total). Same energy either way — same battery drain. So far it's a wash. But look at what happens in the remaining five seconds of the second scenario. If the chip can fully power-gate while idle, its power drops to near zero. The second strategy finishes the job, shuts off completely, and saves energy that the first strategy is still bleeding through leakage.That's race-to-idle. Run fast, finish early, shut down. It works if the chip has significant leakage (so sitting idle actively costs energy) and if it can power-gate deeply enough that idle power really does approach zero. Both conditions hold on modern mobile SoCs, and race-to-idle is the dominant strategy there.The opposite strategy, slow-and-steady, keeps voltage and frequency low, stretches the work out, and relies on the quadratic V² savings to dominate. This wins on chips with low leakage or poor power-gating.We'll sharpen both of these in §4 — they are the two ends of the DVFS policy space — but the key observation here is that "use less power" and "use less energy" are not the same question. Policy has to pick which one to optimize.After this section, you can: read a datasheet listing TDP, voltage range, and frequency range, and predict which lever — voltage drop or frequency drop — will give the bigger energy win per unit of performance lost.You've all felt this one. Record a 4K video for ten minutes on a sunny day, or run a demanding game for twenty minutes on your phone. At some point the device gets noticeably warm, the frame rate dips, the camera app quietly drops to 1080p, the game stutters. It's not running out of battery — you checked. It's not running out of memory. It is running out of heat headroom. The SoC is approaching its thermal limit, and a closed-loop control mechanism is stepping in to keep it from melting. That mechanism is Dynamic Voltage and Frequency Scaling (DVFS), and it is arguably the single most important power-management technique in modern computing.Everything in this section flows from one observation, which we set up in §3.4 and now cash in. Dynamic power is quadratic in V and linear in f:P_dynamic = α · C · V² · fAnd there's a physical coupling we haven't named yet: the maximum frequency a gate can run at is roughly linear in its supply voltage. Higher V means transistors switch faster — more drive current, faster slew rates across the threshold, less propagation delay through each stage. So if you drop V, you must also drop the max f you can reliably clock at. The two levers move together.Watch what that does to power. Suppose we halve both V and f. The V² term becomes (V/2)² = V²/4 — a 4× drop. The f term becomes f/2 — a 2× drop. Multiply them together:Halve V and f → P_dynamic drops by 4 × 2 = 8×.You give up roughly half the instantaneous performance and reclaim seven-eighths of the power. That is the fundamental efficiency lever of modern computing, and it's the reason DVFS lives at the heart of every battery-powered device, every server chip, every game console in a balanced mode. No other knob in the CPU designer's toolbox has anywhere near that leverage.This is also why "cubic in voltage" shows up as a shorthand in energy-efficiency papers. Since f scales roughly linearly with V, dynamic power scales with V² · f ≈ V³. It's not exactly cubic on any real chip — the V-f relationship has curvature and floors — but the cubic mental model is close enough for intuition.A chip doesn't support arbitrary (V, f) combinations. During silicon characterization at the factory, engineers probe each chip at many voltage points, find the highest frequency at which it still passes functional tests with safety margin, and bin the result. What ships is a set of validated operating points — discrete pairs like (1.40 V, 5.0 GHz), (1.20 V, 4.0 GHz), (1.00 V, 3.0 GHz), (0.85 V, 2.0 GHz), (0.70 V, 800 MHz). Connect those dots and you get the chip's V-f curve.<br><img alt="DVFS operating envelope with P-state points from P0 (max performance) down to Pn (idle), V_min floor below which transistors fail, and a turbo region above the sustained envelope." src="assets/l15-dvfs-curve-eng.svg" target="_self" style="width: 620px; max-width: 100%;">At the bottom of that curve lives a hard floor called V_min. Below roughly 0.6-0.7 V on modern processes, thermal noise — random jostling of electrons at the transistor's thermal energy scale — starts being large enough relative to the signal to flip bits occasionally. Bits flipping in your cache is not a graceful degradation mode. You cannot just scale V to zero for more savings; V_min is a wall set by physics, not design choice.At the top lives the thermal ceiling. You could, in principle, set V arbitrarily high and clock arbitrarily fast, but the V² dynamic-power term and the exponential leakage term climb together and you hit TDP before you hit the physical speed limit of the transistors. So the curve has a max-performance point, a min-performance point, and a sparse ladder of validated stops between them.The industry standardized this interface under ACPI (Advanced Configuration and Power Interface) back in the 1990s, and it's where most of the vocabulary comes from.P-states (performance states) are the named rungs on the V-f ladder. P0 is always the highest-performance setting. P1, P2, P3, …, Pn walk down the curve. The OS or firmware picks which P-state is in force right now, and the voltage regulators and clock generators respond within microseconds.C-states (CPU idle states) are the below-idle states: C0 is "executing instructions" (the only one where P-states are relevant), C1 is "halted but ready," and deeper states (C3, C6, C7, …) progressively shut down clocks, flush caches, and power-gate entire cores. The deeper the C-state, the longer it takes to wake back up — C6 might take 30-50 microseconds to resume. This is why schedulers care: waking from a deep C-state has a latency cost that must be weighed against the leakage you save.Per-core DVFS is the modern refinement. Early multicore chips shared a single V-f plane across all cores. Modern ones (Intel from Haswell onward, Apple Silicon, modern AMD Ryzen) let each core pick its own P-state independently. One thread on one core can run at 5 GHz while its neighbor idles at 800 MHz, and the SoC's power-management unit negotiates them against the shared TDP budget.On a Linux system you can see the policy layer directly. The cpufreq subsystem exposes "governors" — named strategies that decide which P-state to request:
performance — always P0. Max frequency, max voltage, max power. Used for latency-sensitive workloads where you'd rather burn watts than wait.
powersave — always the lowest P-state. Favored on battery when you don't care about responsiveness.
ondemand — sample CPU utilization periodically; jump up to P0 on a burst of load, ramp down on idleness. The classic adaptive policy from the 2000s.
schedutil — the modern default on most distributions. Integrates directly with the scheduler so frequency decisions can use the same utilization signal the scheduler uses to place threads. Much smoother than ondemand, and the right default for general-purpose work.
On your laptop right now, running cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor will tell you which one is active. On battery it's often powersave or schedutil; on AC it may flip to performance."Turbo" is a marketing name for a DVFS trick: when the package is cool and only a few cores are active, firmware temporarily pushes one or two of them above the sustained-rated frequency. Intel's Turbo Boost, AMD's Precision Boost, Apple's dynamic boost — all the same idea.The clean way to think about turbo is that the rated "base frequency" on the box is the worst-case sustained frequency under full load at TDP. When load is light, you have thermal headroom. Turbo is the firmware cashing that headroom in for extra speed on the busy cores until the package temperature rises enough that the headroom is gone. Then it pulls back.This is exactly why single-threaded benchmarks often show higher clocks than the datasheet's base frequency — because they only light up one core and let turbo push it. It's also why the same CPU in a cool server room outperforms the same CPU in a hot closet: thermal headroom is the resource turbo spends, and the cooler the ambient, the more there is to spend.Now back to your phone in the sun. The SoC has a temperature sensor — actually, a handful of them, distributed across the die. Firmware reads those sensors many times per second. When any of them approaches T_max (typically 95-105 °C), the power-management unit drops the chip to a lower P-state, regardless of what the workload wants. If that's not enough, it drops further. If that's still not enough, it parks cores.This is a closed-loop control system. The error signal is (T_current − T_target); the control output is the P-state setting; the time constant is milliseconds to tens of milliseconds. No amount of software sorcery can override it at the application level — it sits below the OS, in firmware the application cannot see. Your 4K recording gets throttled not because Android decided to punish you but because the SoC is physically defending itself from a melted solder joint.When you bring the phone back inside and it cools off, the temperatures drop, the control loop opens the P-state cap again, and performance returns. This is the full-circle answer to the opening observation: DVFS is what saves your device from the sun, and thermal throttling is DVFS in its most authoritarian mode.We left the energy-strategy question hanging at the end of §3. DVFS is the knob that resolves it.<br><img alt="Race-to-idle vs slow-and-steady: two power-over-time curves. Race-to-idle wins when leakage is high; slow-and-steady wins when leakage is low." src="assets/l15-race-to-idle-vs-slow-steady-eng.svg" target="_self" style="width: 760px; max-width: 100%;">Race-to-idle picks a high P-state, crushes the workload in minimum time, and drops the chip into a deep C-state for the rest of the interval. This wins when leakage is high (idling costs energy, so the sooner you can power-gate, the better) and when the chip's power-gating is deep enough that idle power really is near zero. Modern phone SoCs — where static power can be 30%+ of active draw and aggressive gating can shut nearly everything off — are race-to-idle machines by default.Slow-and-steady picks the lowest P-state that still hits the deadline, stretches the workload across more time, and exploits the V² savings of the lower operating point. This wins when leakage is low (so running longer doesn't bleed much) or when the workload has to run continuously anyway (a video decoder for a live stream).The decision is not universal and schedulers have gotten increasingly sophisticated about it. Linux's schedutil and Apple's scheduler both use hardware performance counters — cache miss rates, memory-stall fractions, whether the workload is CPU-bound or I/O-bound — to guide which strategy to use per-task. A memory-bound thread has little to gain from high frequency (it's waiting on DRAM anyway), so slow-and-steady wins. A compute-bound thread does gain, so race-to-idle.And here is the handoff to §5. DVFS solves the energy problem for one task at a time — slow down or race, pick a P-state, save joules. But the thermal envelope is a property of the whole chip, not of one task. When a datacenter CPU has 96 cores and the package TDP can't cover all of them running hard simultaneously, DVFS on each individual core still isn't enough. You have to leave some cores off. Which brings us to dark silicon.After this section, you can: explain why the same CPU benchmarks differently in a hot vs cool room, pick the right governor for a battery-sensitive workload, and predict whether a given task benefits more from race-to-idle or slow-and-steady.Opening puzzle. A modern AMD EPYC server chip has 96 cores. Why can't you run all 96 of them at 5 GHz at the same time?It isn't wires, and it isn't memory bandwidth. It's the same reason your phone throttles when you're shooting 4K video in direct sun — heat. Just scaled up by three orders of magnitude and wrapped in a server chassis.The puzzle the phone solved with DVFS is the same puzzle the datacenter chip solves with architecture. Below we'll see why the package of a 96-core EPYC is, by deliberate design, a chip that must run most of itself slower than its transistors are capable of — or leave large regions unpowered at any given moment.That is the phenomenon the industry calls dark silicon, and it is the single biggest architectural constraint on chip design since 2005. Every trend you will meet in the rest of this lecture — big.LITTLE, accelerators, chiplets, 3D stacking — is, in one form or another, a response to it.<br><img alt="Clock frequency flat since 2005 while core count keeps rising — dark silicon's visible signature. You cannot keep scaling frequency within the same power envelope, so you spread the power across more cores at lower speeds." src="assets/l15-cpu-clock-core-count.png" target="_self" style="width: 700px; max-width: 100%;">
The flat clock line and the rising core line are the same story: dark silicon.You saw this in L06, and we won't re-derive it here. The one-paragraph version: in 1974, Robert Dennard worked out that when you shrink a transistor by a factor S, and you scale voltage down by the same S, the power per transistor drops by S² while the number of transistors per mm² rises by S². Those two cancel exactly — power density stays constant.Shrink the transistors, get more of them, pay the same thermal bill. That was the contract that made every Moore's Law generation from 1974 to about 2004 feel like a free lunch: double the transistor count, keep the same heat.As long as that contract held, architects never had to choose between "more transistors" and "more heat". They got both for free, and they spent the bounty on wider pipelines, bigger caches, deeper ROBs, and higher clocks.The whole history you traced through L10 and L14 — superscalar, out-of-order, speculation — sits on top of Dennard's assumption that power density is someone else's problem. It was. Until it wasn't.Two things broke, and they broke together.First, voltage stopped scaling. Below roughly 0.7 V, thermal noise on the gate starts randomly flipping transistors, and you can no longer guarantee that a "1" stays a 1. The physics floor is stubborn — it comes from kT/q at room temperature, and you can't out-engineer room temperature.Engineers kept chasing fractions of a volt (1.1 V → 1.0 V → 0.9 V → 0.8 V) but the curve flattened out as V_min approached. If you can't drop V, you can't keep Dennard's bargain, because the whole deal depended on the quadratic V² term in dynamic power scaling downward with every shrink.Second, leakage climbed. Smaller transistors need lower threshold voltages to switch quickly, and subthreshold leakage is exponential in threshold voltage (you saw this in §3). At 45 nm and below, static power stopped being a rounding error and became 30-50% of the total power budget. A chip that is doing nothing still bleeds watts.The exponential shape is the cruel part: each extra 60 mV of V_th reduction roughly multiplies leakage by ten, so you can't "just lower V_th a little" without paying a lot. High-κ dielectrics (Intel's Penryn, 2007) fixed gate leakage; nothing fixed subthreshold leakage.Put those together and Dennard's deal falls apart. Per-transistor power stops falling.You can still cram more transistors into a mm² — the photolithography is still shrinking, from 45 nm to 28 to 14 to 7 to 5 to 3 — but the power density now climbs with every shrink instead of staying constant. The transistors fit. The heat doesn't.This is why the clock-frequency curve you saw in the L10 opening (and at the start of this lecture) went flat around 2005 and has stayed flat ever since. Industry hasn't been lazy. It has been pinned.Dark silicon is the fraction of transistors on a chip that must stay un-powered at any given moment to keep total power dissipation within the thermal design power (TDP) of the package."Dark" is not a synonym for "broken" or "wasted". The dark transistors are perfectly good — they will light up on the next workload, when some other region goes dark in their place.The chip is a photograph of which regions happen to be active right now; on the next millisecond, the photograph is different. Over time, every part of the chip works; at any instant, a large fraction of it is dim.The key insight to hold in your head: TDP is the master constraint. Every design decision flows downstream of it. If you can budget 100 W for the package, you have a fixed energy account, and architecture is the art of spending that account wisely.<br><img alt="A 48-tile chip die with 12 cores active, 20 dimmed (reduced V/f), and 16 power-gated — a visualization of the TDP-bounded active area that defines dark silicon." src="assets/l15-dark-silicon-chip-eng.svg" target="_self" style="width: 640px; max-width: 100%;"><br>In 2011, Esmaeilzadeh, Blem, St. Amant, Sankaralingam, and Burger published <a data-tooltip-position="top" aria-label="https://ieeexplore.ieee.org/document/6307772" rel="noopener nofollow" class="external-link is-unresolved" href="https://ieeexplore.ieee.org/document/6307772" target="_self">a now-famous ISCA paper</a> that projected dark silicon forward across process nodes. At 8 nm — roughly today's leading-edge logic — they estimated that 50-80% of a chip's transistors would need to be dark at any given instant, depending on architecture, workload, and cooling.When the paper came out, it was treated as an alarm bell. Half your chip, dark? That sounded like a catastrophe for Moore's Law — why keep shrinking if you can't use what you shrink?A decade later, we can say the projection was essentially correct, and the industry did not so much "solve" dark silicon as redesign around it.Every trend you are about to read about in §6, §7, and §8 — heterogeneity, accelerators, chiplets — is downstream of the 2011 projection being right.When you look at a modern phone SoC die photo, the majority of the area is specialized blocks (ISP, NPU, video codec, DSP, secure enclave) that sit idle most of the time.When you look at a server CPU, you see that sustained all-core clock is dramatically lower than single-core turbo — because lighting up all cores at turbo would exceed TDP within seconds.The 2011 paper framed dark silicon as a crisis; a decade later, the industry has simply made peace with it and built around it.You've already seen dark silicon in every device you own; you just didn't have a name for it. It shows up in three places: inside one chip between cores, inside one chip between cores and accelerators, and inside one core between different execution units. All three are the same principle, applied at different granularities.Turbo boost. Your laptop's CPU advertises "up to 5.0 GHz" but sustained all-core frequency is more like 3.8 GHz. That gap is dark silicon in action.When two threads need to finish fast, the chip boosts those two cores to 5 GHz and quietly dims the other fourteen. If all sixteen ran at 5 GHz, the package would exceed its TDP within about a second and the thermal governor would pull everyone back down anyway. Turbo is simply the scheduler being explicit about the trade.GPUs. A modern GPU has 16,000+ CUDA cores. They don't run at 5 GHz. They run at 1.5-2.5 GHz.The design trades clock speed for parallelism — many units at modest frequency, not a few at the ragged edge. That's a structural answer to dark silicon, made at the architecture level rather than the scheduler level. GPUs were dark-silicon designs before anyone used the term.Phone SoCs. Open the camera app. The ISP lights up, the camera sensor's image pipeline engages, maybe the NPU runs a face-detection model, and the CPU drops to an idle core at 600 MHz.Close the camera, open a browser. The ISP goes dark, the CPU big cores light up, the NPU idles. At every instant, most of your phone's transistors are drawing near-zero power. Only the units whose job is currently relevant are active.Server CPUs. The 96-core EPYC from the opening puzzle has a sustained all-core clock around 2.4-3.0 GHz and a single-core turbo near 5 GHz. That is not a manufacturing defect — it is a deliberate envelope.The package TDP (roughly 400 W for top SKUs) will not support 96 cores at 5 GHz; the math doesn't work within any realistic cooling solution. So the chip runs most of its cores most of the time at a frequency well below their transistor-level capability, and the ones that do run at 5 GHz get to do so only because their neighbors are dim.Inside one core. Even within a single OoO core, not all execution units light up at once. The AVX-512 vector unit on an Intel P-core can draw enough current that historically, running AVX-512 continuously would force the core to drop its frequency (the infamous "AVX-512 frequency offset").The unit existed; the power budget to run it at full clock did not. Dark silicon at the unit level is why marketing specs and real sustained performance diverge on specialized instructions.All four of these examples — turbo boost, GPUs, phone SoCs, server CPUs, and the inside of a single core — are responses to the same underlying physical limit. The names vary. The constraint does not.Dark silicon is a constraint, not a technique. The techniques are the ways architects have learned to live with it.You can think of each as a different bet about where the dark fraction should be and what the chip should do about it. Five of them dominate the modern design playbook:1. Dim silicon. Run many units at low voltage and frequency rather than few at the maximum. This is why core counts have risen even as per-core clock stagnated.You saw this pattern in L14: when ILP hit its ceiling inside one core, the industry pivoted to TLP across many cores. The dark-silicon version of that logic says: many slow cores generate less power density than a few fast cores running the same total workload.2. Power gating. Cut VDD entirely to an inactive block, driving its static leakage to effectively zero. Wake-up latency is on the order of microseconds (you have to re-charge the power rails and restore any state). Used for whole cores and large peripheral blocks.3. Clock gating. Freeze the clock to an inactive block, eliminating its dynamic power (α → 0) but leaving leakage intact. Wake-up is essentially instantaneous (one cycle).Used at fine granularity inside almost every modern digital block — the clock tree alone is ~20-30% of chip power, so not clocking something is one of the cheapest wins in the book.4. Specialization. A domain-specific unit is 10-100× more energy-efficient than a general CPU on its domain. If most of the chip must be dark anyway, you might as well make the dark fraction useful when it happens to light up.This is the logic that drives NPUs, video codecs, and crypto accelerators. §6 picks this up and goes deeper.5. Heterogeneous cores. Right-size the core to the workload. A background mail-fetcher on a big core wastes energy on OoO machinery it doesn't need; put it on a small efficient core and you free the big core's power budget for something that actually needs it. §6 is entirely about this response.<br><img alt="Clock gating (AND gate on clock line, VDD always on) versus power gating (pMOS sleep transistor cuts VDD entirely). Clock gating saves dynamic power with instant wake-up; power gating saves everything but takes microseconds to wake." src="assets/l15-power-gating-vs-clock-gating-eng.svg" target="_self" style="width: 760px; max-width: 100%;">These are not mutually exclusive. A real chip uses all five at once.A modern Apple M-series has heterogeneous cores, dozens of specialized blocks, aggressive clock gating throughout, power gating on large unused blocks, and per-cluster DVFS running every core at the lowest frequency that meets deadlines. The design problem is not "pick one"; it is "compose all five well".If you can't power everything at once, the next question is — which things should you power, and on what kind of unit? That is what the rest of this lecture addresses.After this section, you can: explain why turbo boost exists and why sustained all-core frequency is always lower than single-core peak; read a phone SoC die photo and predict which blocks are dark at any given moment; and articulate why chip vendors brag about core counts but quietly cap sustained clocks.Opening causal claim. If §5 is right — if we can't power everything at once — then homogeneous "more big cores" is a losing strategy. A design with sixteen identical high-performance cores spends almost all of its power budget on cores that are, most of the time, running workloads that don't need the full machinery.You'd rather have a mix: a few strong cores for when you need single-thread latency, many weak cores for when you need throughput-per-watt, and specialized units for when you need one specific thing done very efficiently.Heterogeneity is not a clever architectural trick. It is the forced response to the dark-silicon wall.Every major vendor arrived at the same conclusion between 2011 and 2021, from opposite directions.ARM came from the mobile (battery-constrained) side. Intel came from the desktop (TDP-constrained) side. Apple came from a position of vertical integration that let them do both at once.The fact that they converged is not a coincidence. It is the shape of the constraint.You've already met this idea twice. L10 §3 set up CPU-vs-GPU as two points on a latency-throughput curve. L14 §6.5 looked inside Apple's M1 and noted the P-core/E-core split as a teaser.This section picks up where those left off. We'll skip the "what is big.LITTLE" recap and go deeper into the scheduling modes that made heterogeneous chips actually usable, the efficiency math that makes them win, and the programming tax that keeps them hard.The rest of this section walks through three concrete instantiations (ARM, Intel, Apple), surveys the mobile SoC as a catalog, and then spends one honest subsection on the programming cost — because if you take away nothing else from §6, take away the fact that heterogeneity is an architectural win paid for with software complexity you, the engineer, are expected to manage.<br>ARM shipped <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/ARM_big.LITTLE" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/ARM_big.LITTLE" target="_self">big.LITTLE</a> in 2011, several years before Intel and Apple shipped anything similar at scale. The idea: put two classes of cores on the same die, with the same ISA, and let the OS migrate threads between them.Because the ISA is identical, a thread can move from a big core to a LITTLE core without any software support beyond the OS scheduler — it is just another migration, like moving between two big cores on a symmetric multiprocessor. This "same ISA, different microarchitecture" constraint is what makes big.LITTLE tractable. It is also what Intel violated with Alder Lake's AVX-512 mismatch (§6.3), at significant cost.
big cores — Cortex-A72, -A76, -A78, -X4. Wide frontend (3-4 decode, 6+ issue), deep OoO window, aggressive branch prediction, high IPC, high power. These look like the machines from L14: they're built to eat one thread's worst case as fast as possible.
LITTLE cores — Cortex-A53, -A55, -A520. In-order or very narrow OoO, shallow pipeline, narrow issue, low IPC per thread, but 5-10× better performance-per-watt on background work. These look like the machines from before L14 — simpler, smaller, cheaper to light up.
The bet is that a real workload is long-tailed. A phone is asleep 95% of the time it's in your pocket; it fetches mail, checks notifications, and runs the system UI — work that doesn't need a 630-entry ROB.A big core running that work is paying for machinery it isn't using. A LITTLE core at a fraction of the voltage handles it and goes back to sleep.The 5-10× perf-per-watt ratio is worth staring at. Two cores of the same ISA can differ in energy efficiency by almost an order of magnitude, on the same workload, on the same chip, in the same nanosecond — purely because of how wide and how deep their microarchitecture is.That factor of ten is the dark-silicon budget you get back by being willing to accept lower single-thread performance.<br><img alt="ARM Cortex-A57 (big) and Cortex-A53 (LITTLE) cores on a single big.LITTLE MPCore die." src="assets/l15-biglittle-cortex-a57-a53.jpg" target="_self" style="width: 600px; max-width: 100%;">
A Cortex-A57/A53 big.LITTLE MPCore die. Photo from Wikimedia Commons (CC-BY-SA).The idea of two core types is easy. Making the OS schedule across them is the hard part, and the industry went through three generations before it worked well.Cluster switching (2011-2013). The simplest model. The OS sees one cluster at a time — either the big cluster (say, 4 A15 cores) or the LITTLE cluster (say, 4 A7 cores), never both.When load crosses a threshold, the firmware powers down one cluster and powers up the other; threads migrate over. Samsung's Exynos 5410 shipped this mode first.Conceptually clean; practically wasteful, because you're leaving half the silicon unused at all times and you pay a real latency cost on every cluster transition.<br><img alt="big.LITTLE clustered switching: the OS sees one cluster (four big OR four LITTLE) at a time; switching powers down one cluster and wakes the other." src="assets/l15-biglittle-cluster-switching.png" target="_self" style="width: 600px; max-width: 100%;">
Clustered switching — the earliest and simplest big.LITTLE mode. Diagram from Wikimedia Commons (CC-BY-SA).In-Kernel Switcher (IKS, 2013-2014). A finer granularity. Pair each big core with a LITTLE core as a virtual CPU; at any instant, exactly one of the pair is running, and the OS sees the virtual CPU.The scheduler can independently choose big or LITTLE for each virtual CPU, so a chip can run, say, two virtual CPUs with their big halves active and two with their LITTLE halves active.Better than cluster switching, still not great: it can never run a big and its paired LITTLE simultaneously, so half the die is still idle whenever the other half is busy.Heterogeneous Multi-Processing (HMP) with Energy-Aware Scheduling (EAS, 2015+). The model used today, across Android and iOS. The OS sees all cores as a flat pool — big and LITTLE are both schedulable CPUs at the same time.The scheduler gets hints (from hardware load counters, thread history, and explicit SCHED_* hints from the app) about each thread's expected intensity, and places it on the core type that minimizes total energy for the predicted work. A video-encoding thread lands on a big core; a notification poller lands on a LITTLE core; a system service might start on LITTLE and get promoted to big mid-frame when its latency budget is about to blow.Energy-Aware Scheduling, in particular, is the piece that turns HMP from "possible" to "good". EAS models the energy cost of placing a thread on each candidate core — accounting for the core's current P-state, its thermal headroom, whether moving the thread would let a cluster power-gate — and picks the placement with the minimum predicted energy.It turned heterogeneous scheduling from a best-effort heuristic into an optimization problem the kernel actually solves on every wakeup. The Linux schedutil governor and Android's WALT load tracker are the two pieces that made EAS viable at scale; without the hardware load counters those mechanisms rely on, the scheduler is flying blind.<br><img alt="big.LITTLE HMP (Heterogeneous Multi-Processing): the Linux scheduler sees all eight non-symmetric cores simultaneously and assigns threads to whichever core type best matches the workload." src="assets/l15-biglittle-global-task-scheduling.jpg" target="_self" style="width: 700px; max-width: 100%;">
HMP — the dominant big.LITTLE scheduling mode today. Diagram from Wikimedia Commons (CC-BY-SA).<br>Intel shipped its first hybrid consumer CPU, <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Alder_Lake" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Alder_Lake" target="_self">Alder Lake</a>, a decade after ARM. It is worth understanding why Intel waited so long: for most of the 2010s, Intel's strategy was to make one great core and put more of them on each die. Heterogeneity was an admission that the one-great-core approach had run into the same wall ARM had hit a decade earlier.Alder Lake adopted the same fundamental big/LITTLE split, with different trade-offs and different names.
P-cores (Performance, codename Golden Cove) — wide, deep OoO, SMT-enabled. The x86 equivalent of the M1's Firestorm you met in L14.
E-cores (Efficient, codename Gracemont) — narrower, no SMT, but organized in 4-core clusters that share an L2.
The efficiency math here is worth staring at. Gracemont E-cores are approximately 4× denser per mm² than Golden Cove P-cores at comparable throughput on multi-threaded integer workloads.That density ratio is the dark-silicon argument expressed as chip layout: given a fixed power budget, four E-cores in the area of one P-core deliver roughly 80% of the P-core's multi-threaded throughput at roughly the same wattage. Per mm² of dark-silicon-constrained die, E-cores are simply a better deal for throughput-bound work.Intel's piece that ARM didn't need: Thread Director. Because x86 inherited decades of OS schedulers that assumed homogeneous cores, Intel put a small microcontroller on-die that watches each thread's instruction mix (scalar integer? vector FP? lots of memory stalls?) and tells the OS which core type will run it most efficiently.It's an admission that, on x86, the OS can't be trusted to know the hardware well enough on its own — Windows and Linux schedulers were written for a world where every CPU was the same, and Thread Director ships hardware hints to patch around that assumption without requiring every OS to rewrite its scheduler.There is one wrinkle that matters for compatibility: P-cores support SMT (Hyper-Threading) and E-cores do not, and E-cores do not implement AVX-512. On early Alder Lake chips, Intel had to disable AVX-512 on P-cores too, because a thread running AVX-512 would crash when the scheduler migrated it to an E-core that doesn't speak that instruction.Heterogeneity is easy when the ISAs match exactly; it is very hard when they don't, and the Alder Lake AVX-512 episode is the cautionary tale every x86 hybrid design since has had to work around.L14 §6.5 walked through the Firestorm P-core. We won't retread that.What's relevant here is that the M1 (and every M-series chip since) bakes heterogeneity into the platform, not just into the CPU cluster. The big.LITTLE split is the start of the story, not the end.On the same die, Apple puts: P-cores (Firestorm/Avalanche/Everest), E-cores (Icestorm/Blizzard/Sawtooth), a GPU (dozens of cores), a Neural Engine (the NPU — 16+ cores tuned for int8 matrix multiply), an ISP (for the camera), a video encode/decode block, a display engine, a Secure Enclave, and a unified memory controller that all of the above share.<br>Thanks to <a data-tooltip-position="top" aria-label="https://www.apple.com/newsroom/2020/11/apple-unleashes-m1/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.apple.com/newsroom/2020/11/apple-unleashes-m1/" target="_self">unified memory</a>, there is no cross-device memory copy to talk about — all of these units read and write the same physical DRAM. The model a Core ML call runs on the Neural Engine sits in the same address space as the bytes a Metal shader reads from a texture; no cudaMemcpy equivalent is needed.Apple's dispatch is tight: a background mail poll runs on an E-core at ~100 mW; a foreground video editor runs on P-cores at ~5 W; a Core ML inference call runs on the Neural Engine at under 1 W instead of saturating the CPU at 8 W.The OS and the frameworks (Metal, Core ML, Accelerate) route each kind of work to the unit that is most efficient for it. The user never sees this. The battery life does.The point is not that Apple invented something the industry hadn't seen. ARM had the big.LITTLE template, NVIDIA had GPUs with on-die tensor cores, and Qualcomm had DSPs on phones years earlier.What Apple did was integrate the whole catalog on one die, under one memory controller, with a software stack (Metal + Core ML + Accelerate + Grand Central Dispatch) that knows about the whole catalog. That integration is the win. And it is expensive to replicate, because it requires controlling both the silicon and the OS.Zoom out from any one core cluster and look at a whole phone SoC. The chip is mostly not CPU.It is a constellation of specialized units, each wildly more efficient than a CPU would be on its specific job. The CPU cluster is usually less than 20% of the die area; the rest is accelerators, memory controllers, modems, and fixed-function blocks.Each of these units is a dark-silicon gamble. Most of the time, most of them are off.When the camera opens, the ISP lights up. When you make a call, the DSP lights up. When Siri wakes, the always-on DSP (a sub-block of the DSP) lights up. The SoC is designed on the assumption that no two of these will ever need full power simultaneously — and for almost every real workload, that assumption holds.The CPU, in this view, is the general-purpose fallback. It handles the cases the specialists can't, glues together the output from all the other units, and runs the OS.It is absolutely necessary, and it is also the least energy-efficient unit per watt for almost every job the specialists can handle. The design philosophy of a modern phone is therefore: keep the CPU idle whenever possible; route work to the most specialized unit that can do it; and accept that the specialized units will be dark most of the time because each serves a narrow workload.<br><img alt="A typical 2024 flagship phone SoC: CPU complex (P-cores + E-cores), GPU, Neural Engine (NPU), ISP, DSP, video codec, and Secure Enclave, all sharing a unified memory interconnect." src="assets/l15-heterogeneous-soc-eng.svg" target="_self" style="width: 760px; max-width: 100%;">Here is where we have to be honest with you. Heterogeneity is an architectural win and a software tax, and the tax is not small.There is no universal standard for programming a heterogeneous system. Instead, there are several partial ones, each covering a slice of the landscape:
CUDA — NVIDIA only, GPU only, by far the most mature ecosystem
OpenCL — cross-vendor, but the tooling has withered in recent years
SYCL — modern C++-based, vendor-neutral, growing but immature
Metal — Apple only, covers GPU and Neural Engine
oneAPI — Intel's answer, SYCL-based, cross-accelerator in principle
TensorFlow / PyTorch backends — the practical default for ML, which hide the heterogeneity behind a graph API but leak it as soon as you need custom kernels
Beyond APIs, memory coherence across accelerators is an active research problem. When a CPU thread and a GPU kernel both touch the same data, you need either explicit copies (slow, wasteful, bug-prone) or hardware-managed coherence (Apple's unified memory, NVIDIA Grace Hopper, AMD Instinct MI300A).The "no copy" model is spreading, but it is far from universal, and every cross-accelerator copy is a performance cliff that the programmer has to know about.The reason NVIDIA's CUDA ecosystem is worth tens of billions of dollars is not the hardware. It's the decade-plus of accumulated libraries, debugger support, profilers, and thousands of engineers who know how to write kernels.Heterogeneity works best when someone else — a compiler, a framework, a vendor — hides it from you. When the framework doesn't know your workload, you pay the tax directly.Framing the cost bluntly: every accelerator you add to a chip creates a new place where your code does not automatically run.The CPU will run anything you give it; the NPU will only run the subset of your program that some framework rewrote as a tensor graph; the ISP will only run the camera pipeline you built with the ISP's vendor SDK.Work that "doesn't fit" the accelerator falls back to the CPU at 10-100× worse energy. A design that ships with four accelerators your software doesn't use is, from a power perspective, identical to a design that shipped without them — except you paid for the die area, the verification, and the software-stack maintenance.This matters for the course synthesis in §10. The whole point of L14 + L15 is that getting good work out of a modern chip is not what it was in 2005.In 2005 you wrote C, the compiler scheduled your instructions, and the CPU got faster every year.In 2025 you write C for the CPU, CUDA for the GPU, Metal or Core ML for the NPU, and you make ISP and codec calls through the OS — and you are responsible for deciding what runs where.The compiler doesn't do that for you. The schedulers help. The rest is engineering judgment, which is what this course was trying to teach.One more honest observation before we close. The programming tax is uneven across workloads. If you are training a neural network, the ML frameworks take care of almost everything — PyTorch decides what runs on the GPU, the NPU, or the CPU, and your code never has to know.If you are writing a generic backend service, almost everything runs on the CPU and heterogeneity is invisible. It is the engineers in the middle — the ones writing real-time image pipelines, custom ML inference paths, games, browsers, media players — who pay the tax in full and write the code that hides it from everyone else.After this section, you can: explain why Apple's 5 nm M1 beats Intel's same-generation desktop chips on performance-per-watt without any single-technology breakthrough; describe the three big.LITTLE scheduling modes and why HMP with EAS is today's default; and argue intelligently about whether Intel's P+E strategy is sound engineering or a reactive catch-up to ARM and Apple.You've been typing prompts into ChatGPT for a year or two now. Each query — the one that summarizes your lecture notes, the one that rewrites your CV, the one you fire off because autocomplete used to take 200 ms and now feels slow — costs roughly 3 Wh of electricity end-to-end. A Google search, by comparison, is about 0.3 Wh. So every time you talk to a language model instead of typing two keywords into a search bar, you've spent ten times the energy. You did not feel a thing. The bill went somewhere else.OpenAI reports on the order of a billion queries per day. Multiply it out. Now add Gemini, Claude, Copilot, Llama, DeepSeek, and every startup that just wrapped an API. The International Energy Agency projects that generative AI alone will add roughly 130 TWh/year to global electricity demand by 2026 — that is Sweden's entire annual consumption, the whole country, every factory, every streetlight, every apartment block, added to the grid because we decided chat-style inference was a feature worth defaulting on.This section is about where that electricity physically goes, what it costs in carbon and in landfill, and what a software engineer can actually do about it. Not a sermon — a numbers-first accounting of the bill your architectural decisions are now signing.Step back from the AI story for a moment. All data centers combined — web, cloud, enterprise, AI, streaming, everything — consumed about 415 TWh in 2024, roughly 1.5% of global electricity. That's already more than the annual generation of Germany. The IEA's reference projection puts data centers at ~945 TWh by 2030, more than doubling in six years. In the United States, data centers may reach 9% of national electricity generation by 2030 — a number that would have been a typo in a 2015 textbook and is now a planning figure at every major utility.<br><img alt="Rows of server racks inside a telecommunications data center — the physical form of the 415 TWh electricity bill." src="assets/l15-data-center-racks.jpg" target="_self" style="width: 700px; max-width: 100%;">
A typical data center hall. Photo from Wikimedia Commons (CC-BY-SA).The carbon side of the ledger: about 220 Mt CO₂ from data centers in 2024, close to 1% of global emissions — comparable to the commercial aviation industry. And then there's the hardware itself. Global e-waste from IT refresh cycles reached 62 Mt in 2022, of which only about 22% was properly recycled. The rest — servers, switches, GPUs, laptops, phones — goes to landfill or to informal recyclers in the Global South, where the gold and copper are burned out by hand and the rest leaches into groundwater. Every three-to-five-year refresh cycle in a hyperscaler fleet is a tonnage number somewhere downstream.These are the three rows of the bill: electricity, carbon, materials. Keep them in mind — everything that follows is either a lever that pushes one down or a trade that shifts cost between them.When you rent a server in a data center, you are not just paying for the transistors that compute your workload. You are paying for the cooling that keeps them under 85 °C, the UPS batteries that keep them up when the grid flickers, the backup diesel generators that cough to life when the UPS runs out, the power-conversion losses at every step from 110 kV substation to 12 V motherboard rail, and the lights, the humidity control, the physical security. All of that is overhead, and the industry has exactly one number to measure it:PUE = Total Facility Power / IT Equipment PowerIdeal PUE is 1.0 — every watt into the building makes it to a CPU and none is lost to overhead. It's impossible (conservation of energy says cooling has to draw power; you can't cool for free), but it's a useful north star. The distribution in the real world is wide:
Typical US enterprise data center: ~2.0. Every watt you spend on compute costs another watt of overhead. Most of that overhead is cooling — often 30% or more of total facility power goes to keeping air moving and chillers running.
State-of-the-art hyperscaler (Google, Meta, Microsoft Azure): ~1.1. Cooling down to ~7% of total, cleaner power distribution, custom everything.
Best-in-class (two-phase immersion cooling): ~1.01. Servers submerged in a dielectric fluid that literally boils on the hotspots. Almost nothing to overhead.
<br><img alt="Power breakdown of a typical enterprise data center (PUE 2.0) compared to a hyperscaler (PUE 1.1). Cooling alone drops from 30% to 6% of total power." src="assets/l15-pue-breakdown-eng.svg" target="_self" style="width: 760px; max-width: 100%;">That spread — 1.01 vs 2.0 — is roughly a 2× difference in total electricity consumption for the same useful computation. When a cloud provider quotes a "carbon-neutral" service, the first question to ask is its PUE. When a government debates data-center siting, PUE is the number on the regulatory side of the table. One metric, and it tells you more about a facility than any glossy brochure will.Cooling is the biggest swing term in PUE, so it's where the architecture happens. Two approaches to remember — not six.Hot-aisle/cold-aisle containment is the baseline, ubiquitous in data centers built from roughly 2010 onward. Racks face each other in alternating rows: the fronts (air intakes) face a "cold aisle" fed by air-conditioned supply air; the backs (exhausts) face a "hot aisle" that captures the heated return air and feeds it back to the chillers. Plastic curtains or panels prevent hot and cold air from mixing. It's boring, mechanical, and it works — a decent hot/cold setup gets you PUE around 1.4-1.6, a substantial improvement over the open-floor designs of the 2000s.<br><img alt="Cold-aisle containment: racks face each other in alternating rows with plastic panels preventing cold and hot air from mixing." src="assets/l15-data-center-cold-aisle.jpg" target="_self" style="width: 500px; max-width: 100%;">
Cold-aisle containment, the current baseline. Photo from Wikimedia Commons (CC-BY-SA).Two-phase immersion cooling is where AI clusters are going. Servers are dunked into a tank of dielectric liquid — a fluorocarbon that doesn't conduct electricity and boils at around 50 °C. When a GPU hits a hotspot, the fluid on its surface flashes to vapor, absorbing enormous heat in the phase change, rises to a condenser on top of the tank, condenses back to liquid, and drips down. No fans. No chillers. No air handling. The heat leaves the building via a simple water loop to the outside. PUE approaches 1.01 because there is almost nothing to power except the servers themselves. The trade-off is capital cost and a new set of operational hazards — but for a cluster of 10 000 H100s burning 7 MW, the efficiency math works out fast. Expect to see it everywhere in AI-first data centers by 2030.Once you've squeezed PUE, the next lever moves the facility somewhere the electrons are already cheap and clean. Two ideas, both with consequences.Site selection. Build where the grid is cold and clean. Sweden, Iceland, Norway, Quebec: cold ambient air means free cooling for most of the year; hydroelectric, geothermal, and nuclear power mean the carbon intensity per kWh is near zero. Google's Hamina facility in Finland pumps seawater from the Gulf of Finland through its heat exchangers. Facebook's Luleå campus sits a hundred kilometers south of the Arctic Circle. These aren't PR plays — the electricity is cheaper and the cooling is nearly free, and the carbon accounting happens to be the best in the industry. Everybody would build there if the network latency and the local labor supply allowed it.24/7 carbon-free energy (CFE). The trickier lever. For most of the 2010s, hyperscalers claimed "100% renewable" by signing power-purchase agreements for wind and solar somewhere on the grid, then averaging over a year. The accounting balanced. The physics didn't. A data center in Virginia that "matches" its annual consumption with a wind farm in Texas is still drawing coal-fired electrons at 3 AM when the wind isn't blowing in Texas. Google was the first hyperscaler to call this out as inadequate and commit to 24/7 CFE by 2030 — matching each hour of consumption to zero-carbon generation on the same grid. The difference matters: it forces investment in firm clean power (nuclear, long-duration storage, geothermal) rather than just adding more intermittent wind and solar that conveniently correlates with when you've already decided to run cheap. Microsoft, Amazon, and Meta have followed with similar pledges. Whether they hit it is an open question; the framing shift is already real.This is the section where I could hand you a five-item list — container orchestration, cache-friendly code, low-precision inference, carbon-aware scheduling, right-sized instance types. All true. All secondary. One thing dominates, and it's been true for fifty years.Algorithmic efficiency. A 10× faster algorithm is 10× less energy forever, on every machine it ever runs on, in every data center, under every grid mix. The most sustainable code is code that doesn't run. The O(n²) sort you never fixed because it was "fast enough on the dev laptop" will, over its deployment lifetime, burn more kilowatt-hours than the immersion tank it runs in will save. A web service that caches a computed result is not just faster — it is, in the most literal sense, removing tonnes of CO₂ from the accounting.The second-order lever is architectural fit. If an NPU can run your inference 20× more efficiently than a CPU, the choice of where to run is not a performance optimization — it is the single biggest carbon decision in the workload. Routing inference to an H100 when a smaller model on an edge NPU would do the job is a very expensive reflex. Picking the right hardware is an engineering ethics decision, and in 2025 it is no longer credible to pretend otherwise.Every architectural choice you make — from the sort algorithm to the cloud region to the model size — now has an energy signature, a carbon signature, and an e-waste signature attached to it. The people paying the bill are not the people making the decision, and in most organizations nobody is keeping track. That has been fine, historically, because the numbers were small. They are not small anymore.In 2025, picking the right architecture for a workload is no longer just a performance decision. It's an engineering ethics decision.After this section, you can: read a data center's PUE figure and compare facilities apples-to-apples, identify the single biggest lever for reducing a workload's carbon cost, and defend algorithmic efficiency as a sustainability decision, not just a performance one.Here is a question worth opening with, because the answer turned an industry on its head in four years. Why did AMD start building CPUs out of Lego bricks in 2019? The company had been shipping traditional monolithic processors — one big die per chip, one layout, one fabrication run — for thirty years. Then Zen 2 arrived with its compute cores on one piece of silicon and the I/O controller on another, and Intel, the dominant CPU vendor of the prior two decades, started losing datacenter share by the quarter. Why did AMD bet the company on this, and why did it work?The answer is not architecture. It is arithmetic. Do the multiplication with me.Modern wafer defect density sits around 0.1 defects per square centimetre. Every die punched out of a wafer has some probability of having a killer defect inside it — the larger the die, the higher the probability. A standard yield model (Murphy's) says yield Y for a die of area A is roughly Y ≈ 1 / (1 + A · D)², where D is defect density per cm².Take a monolithic 600 mm² server die — big, because you want lots of cores. Area in cm² is 6.0. Plug in: Y ≈ 1 / (1 + 6.0 · 0.1)² = 1 / 1.6² = 1 / 2.56 ≈ 39%. With real-world edge exclusions and binning you get closer to the often-quoted ~55% for a well-tuned 600 mm² process, but the punchline is the same either way: roughly half your big dies are scrap.Now split that same 600 mm² of silicon into six 100 mm² chiplets. Each chiplet has A = 1.0 cm². Plug in: Y ≈ 1 / (1 + 1.0 · 0.1)² = 1 / 1.21 ≈ 83%. With real process tuning, ~94% is routinely achievable on a 100 mm² die. Good chiplets are common. The interesting question is how often you can assemble six known-good chiplets onto one package, which is approximately 0.94⁶ ≈ 73% — still nearly twice the yield of the monolithic design, before you count the additional upside that any chiplet found bad at test gets thrown away at negligible cost, not carried through expensive downstream packaging.Count the dies per wafer and the picture sharpens further. A 300 mm wafer holds roughly 80 of the 600 mm² dies (≈44 good) or roughly 600 of the 100 mm² chiplets (≈564 good). Six chiplets make one CPU, so that wafer of small dies produces ~94 CPU-equivalent sets versus 44 monolithic ones. More than 2× the working processors per wafer of the same silicon. That is the economics.AMD did this math in the late 2010s. Intel stuck with monolithic for three more generations. Between 2019 and 2023, AMD's datacenter CPU market share rose from roughly 2% to over 30%, taking around 40 percentage points of server revenue share away from Intel across that window. That is what "do the multiplication" looks like when you do it before your competitor does.Chiplets are not a clever technical trick. They are economics responding to yield physics, and the physics has only one direction — defect densities drop with process maturity, but die areas for competitive designs keep growing, so the crossover point where chiplets win the arithmetic keeps moving further toward "everything should be chiplets".A chiplet is a small specialized die designed to be assembled with other chiplets on a shared package substrate, connected by high-bandwidth interconnects. The assembled package behaves, from software's point of view, like one processor. From the fab's point of view, it is several independently manufactured dies stitched together.Four examples worth knowing:<br><img alt="AMD Ryzen-style chiplet package: two compute chiplet dies (CCDs) on leading-edge 5 nm, one I/O die (IOD) on mature 12 nm, joined by Infinity Fabric links on a shared substrate." src="assets/l15-chiplet-package-eng.svg" target="_self" style="width: 700px; max-width: 100%;">The Apple and AMD designs illustrate the two philosophies. AMD decomposes the processor functionally: compute here, I/O there, memory controller in the IOD. Apple decomposes the processor identically: two copies of the same die glued together for capacity. Both approaches work because the interconnect is fast enough that software doesn't see the seam.The yield argument from the opening is the first and largest reason, but not the only one. Four more benefits compound on top of it. Reuse — AMD's CCD is the same die whether it ships in a $300 Ryzen or a $10,000 EPYC, so the design cost is amortized across two product lines instead of one. Process specialization — logic benefits enormously from leading-edge density, but I/O and memory controllers do not scale the same way, and putting them on a mature node is dramatically cheaper without hurting performance. Time-to-market — each chiplet tapes out independently, so a bug found in the graphics tile doesn't hold up the compute tile, and a new compute chiplet can ship with last year's I/O die. Scalability — the same chiplet stacks into higher SKUs (four CCDs for Ryzen, twelve for EPYC) without a new tapeout for each variant. Every one of these compounds with yield. The monolithic design has to win against this stack, and at large die sizes it can't.One problem with the chiplet model as first deployed: every vendor's interconnect is proprietary. AMD's Infinity Fabric doesn't talk to Intel's AIB, and neither talks to Apple's UltraFusion. A chiplet from one vendor cannot be dropped into another vendor's package. That defeats much of the promise — a true chiplet ecosystem would let you assemble a processor from best-in-class tiles the same way you assemble a PC from best-in-class components.UCIe (Universal Chiplet Interconnect Express), launched in 2022, is the industry's answer — a standard for chiplet-to-chiplet communication backed by Intel, AMD, Samsung, TSMC, ARM, Qualcomm, Google, and Meta. Think of it as the USB of chiplets: a common physical and protocol layer so that a compute chiplet from one vendor can be assembled next to a memory-controller chiplet from another. The ecosystem is still young — most products in 2025 still use vendor-proprietary interconnects internally — but UCIe is where things are heading, and mixed-vendor assemblies are already shipping in specialized AI and HPC products.Chiplets solve yield by splitting sideways. The other direction — up — opens a different door. 3D integrated circuits stack multiple dies vertically and connect them through the silicon itself using through-silicon vias (TSVs): metal columns, typically copper, drilled through the thinned back side of a die so that signals can travel from one layer to the next without leaving the stack. TSVs are short (tens of micrometres), fat (easy low-resistance), and very close together (thousands per square millimetre possible), so they offer the combination every interconnect designer dreams of — high bandwidth, low latency, low energy per bit. On-stack signalling costs roughly 10–100× less energy than driving the same bits off-chip through the package substrate.<br><img alt="3D die stacking concept: multiple dies stacked vertically, connected by through-silicon vias that carry signals from one layer to the next." src="assets/l15-3d-die-stacking-concept.png" target="_self" style="width: 600px; max-width: 100%;">
3D die stacking schematic. Diagram from Wikimedia Commons (CC-BY-SA).The catch is that stacking traps heat. More on that shortly.If you have heard of one 3D IC product, it is High Bandwidth Memory (HBM). Eight to twelve DRAM dies are stacked on a logic base die and connected through thousands of TSVs. The base die exposes a ridiculously wide bus — 1024 bits per stack for HBM through HBM3e, 2048 bits for HBM4 — to the host processor, typically through a silicon interposer underneath the stack. GDDR memory, by comparison, runs a 32-bit bus per chip; HBM's trick is raw parallelism, not frequency.<br><img alt="HBM schematic: several DRAM dies stacked on a logic base die, connected by TSVs, mounted on a silicon interposer alongside the GPU for a very short, very wide bus." src="assets/l15-hbm-schematic.svg" target="_self" style="width: 700px; max-width: 100%;">
HBM architecture with TSVs and interposer. Diagram from Wikimedia Commons (CC-BY-SA).The generational progression shows where the industry has spent the last decade of memory engineering:Every modern GPU and AI accelerator uses HBM: NVIDIA H100 (HBM3), H200 and B200 (HBM3e), AMD MI300 (HBM3). Typical configuration is 4–8 HBM stacks around a compute die on a common interposer, giving aggregate memory bandwidth of 3–8 TB/s per accelerator package.<br><img alt="AMD Fiji GPU package with four HBM memory stacks visible alongside the GPU die on the silicon interposer — the first consumer GPU to ship with HBM (2015)." src="assets/l15-hbm-fiji-gpu-package.jpg" target="_self" style="width: 600px; max-width: 100%;">
AMD Fiji package with HBM. Photo from Wikimedia Commons (CC-BY-SA).Why does bandwidth matter this much? Because a modern GPU does trillions of floating-point operations per second — an H100 peaks around 1 PFLOP/s on the tensor cores — and every one of those operations reads or writes data. Without HBM, the GPU would sit there starving, waiting for DDR to deliver bytes at a fraction of the rate the compute units can consume them. A $30,000 H100 paired with conventional GDDR would run at maybe 10% utilization on real training workloads. HBM is what makes AI training economically viable. The compute-to-memory ratio of deep learning is roughly balanced at HBM bandwidths; at DDR bandwidths it is wildly compute-starved, which means every dollar of silicon you buy for tensor cores goes mostly to waste.This is also why the HBM supply chain (SK hynix, Samsung, Micron) became geopolitically sensitive in 2023–2024 — HBM is the binding constraint on AI accelerator production, and whoever ships the memory ships the AI capacity.HBM stacks memory next to logic. AMD's 3D V-Cache stacks memory on top of logic: a second L3 cache die sits vertically atop a Zen CCD, connected by TSVs through the back of the cache die. The base CCD ships with 32 MB of L3; V-Cache adds another 64 MB on top, for 96 MB total L3 per chiplet. In a 16-core EPYC Genoa-X variant, that aggregates to 1 GB of L3 across the whole package.The payoff is workload-specific and dramatic. Gaming benchmarks jump 15–30% on a Ryzen 7 7800X3D versus a non-X3D equivalent, because modern game engines have working sets that fit inside 96 MB but overflow 32 MB — the hit rate crosses a cliff. Scientific HPC codes that operate on medium-sized matrices see similar uplift. The chips have become the default choice for computational finance, CFD simulation, and anything whose hot data lives between "L2-sized" and "DRAM-sized".Notice what AMD stacked: cache, not cores. That choice is not accidental, and it points at the central unsolved problem of 3D integration.Stacking is easy for memory and hard for logic, and the reason is heat. A modern DRAM die dissipates on the order of a watt or two. An HBM stack of twelve such dies is still only 15–25 W total, and that heat can escape out the top of the stack into a heatsink or through the package substrate. DRAM is dense, cool, and happy in a tower.A CPU die is a different animal. A single Zen 5 CCD can dissipate 50–100 W on its own. Put another compute die on top and you now have 100–200 W trying to leave through a stack where the bottom die is blanketed by the one above. The bottom die cannot cool through its top surface — the top die is in the way — and the top die cannot cool through its bottom surface — the bottom die is in the way. Heat bottles up in the middle. The stack cooks.That is why V-Cache works and stacked CPUs do not, and it is the structural reason AMD stacked cache rather than cores. Cache is low-power (L3 is mostly SRAM; most cells are quiet most of the time) and heat-tolerant (a few extra degrees Celsius doesn't hurt SRAM the way it hurts logic timing margins). Even so, V-Cache chips run at slightly reduced clocks compared to their non-V-Cache siblings — AMD leaves a few hundred megahertz on the table specifically to stay within thermal budget.Compute-on-compute 3D stacking is an active research area and remains largely experimental as of 2025. Intel's Foveros, TSMC's SoIC, and various academic prototypes have demonstrated stacked logic for specific applications, but no one ships a mainstream stacked-compute processor for general workloads. The thermal physics is not going away — at some point either cooling technology (micro-channel fluid cooling inside the stack is being investigated) or power density (fundamental voltage reduction, if we can get there) has to change for vertical compute to become routine. Until then, "stack memory on logic, not logic on logic" is a rule worth remembering.After this section, you can: read a CPU announcement — "Zen 5 ships as 4 CCDs plus an IOD, with optional V-Cache on two of them" — and evaluate the trade-offs at play: yield economics, cross-chiplet latency, interconnect power, and why the stacking is where it is (cache above cores, not cores above cores).Every week in tech press someone announces a "revolutionary new compute architecture." Photonic. Quantum. Neuromorphic. In-memory. Analog. Biological. The announcements are breathless, the benchmarks are cherry-picked, and three years later you read a quieter piece about why the thing never shipped in volume — or why it shipped into exactly one niche application and stayed there. If you try to keep up with each one on its own terms, you'll either memorize five facts per technology and retain none of them, or you'll start believing press releases. Neither is useful.So this section is not a survey to memorize. It's one skill, practiced on five examples.
The meta-skill: given a new architecture, identify its target workload and its fundamental limitation.
That's it. Two questions, every time. What does this thing accelerate? and What is it bad at? If a pitch can't answer both within a paragraph, you are being sold something. If you can answer both in a paragraph, you can hold your own in any conversation about where hardware is going — for the rest of your career, through architectures that don't exist yet. The five technologies below are practice reps for that skill, not items on a vocabulary list.Why frame it this way? Because the history of "revolutionary computing" is mostly a graveyard of general-purpose claims that turned into narrow accelerators. Every technology on the list below is genuinely interesting on some axis. None of them is a CPU replacement. The student who can cleanly articulate why each one is narrow — not dismissing it, not hyping it, just locating it — is the student who'll still be useful when the next wave of announcements starts.<br><img alt="The post-silicon landscape: neuromorphic, PIM, photonic, quantum, and new materials positioned by application breadth (narrow to general-purpose) and commercial readiness (research to deployed)." src="assets/l15-future-tech-landscape-eng.svg" target="_self" style="width: 760px; max-width: 100%;">We covered this substantively in L10 §8 — Intel Loihi, IBM TrueNorth and NorthPole, spiking neural networks, event-driven computation, the breakdown of the von Neumann fetch-execute cycle for sensory workloads. Go reread that section if the details are fuzzy; the rest of this subsection assumes you know what an SNN is.What matters here is the frame. Target workload: always-on edge inference at microwatt scale — a hearing aid doing real-time noise suppression, a security camera watching for anomalies in a mostly-static frame, a pacemaker classifying arrhythmias, a farm sensor detecting the shape of a pest among thousands of benign insects.The win is 100–1000× energy efficiency on sparse, event-driven sensory streams where most of the "data" is nothing happening and the interesting moments are rare. A conventional CPU-or-GPU pipeline wakes up on every frame, processes it, throws most of it away; a neuromorphic chip stays quiet until a spike arrives. On the right workload that difference is the difference between a device that runs for a year on a coin cell and one that doesn't.Limitation: the programming model is nowhere near mainstream. You don't compile TensorFlow to Loihi and hope for the best — you rewrite the algorithm into spikes, rates, and thresholds, often by hand. That's a research-grade skill, not a deployment-grade toolchain. And the class of problems SNNs handle elegantly is narrower than the marketing suggests: dense, high-precision inference (the workloads GPUs are actually optimized for) is not where neuromorphic wins.Read the frame twice: energy for the right shape of problem, at the cost of generality.A different axis of attack. In L11 and L14 you saw that modern CPUs are not limited by ALU throughput — they're limited by moving data. A DRAM access dissipates roughly 100× more energy than the floating-point multiply the data is being fetched for. For graph analytics, sparse matrix-vector products, and large-embedding ML lookups, your chip spends more silicon and more power shuttling bits than computing with them.Processing-in-memory flips that. Put simple ALUs inside the DRAM banks themselves and do the multiply-accumulate where the data already lives. Samsung's HBM-PIM (2021) integrates programmable compute units into HBM stacks. UPMEM's DPUs sit on standard DIMMs with a small RISC core next to each bank. Bandwidth that never leaves the die is free — or at least, dramatically cheaper.Target workload: memory-bound kernels — graph traversal, sparse linear algebra, database scans, recommender-system embedding lookups, anything where the compute-to-bytes-moved ratio is low.Limitation: the programming model. Your code has to be restructured to partition into operations the DRAM units can actually execute — and those units are deliberately simple (add, multiply-accumulate, some comparisons), not full CPUs. No compiler turns your existing C++ into PIM code; it's an explicit parallel-programming problem on top of an unusual memory model. Adoption is slow for exactly that reason.Frame: bandwidth for the right shape of problem, at the cost of programming effort.Silicon switches electrons. Electrons have mass, charge, and resistance — every wire on the chip is an RC circuit dissipating heat. Photons don't, and for one specific operation — dense matrix multiplication — they're almost ideal. A beam of light passing through a mesh of Mach-Zehnder interferometers implements a matrix-vector multiply at the speed of light, with energy costs set mostly by the electro-optical conversion at the edges, not by the multiply itself.<br>Companies like <a data-tooltip-position="top" aria-label="https://lightmatter.co" rel="noopener nofollow" class="external-link is-unresolved" href="https://lightmatter.co" target="_self">Lightmatter</a> are building photonic accelerators aimed at data-center ML inference. <a data-tooltip-position="top" aria-label="https://ayarlabs.com" rel="noopener nofollow" class="external-link is-unresolved" href="https://ayarlabs.com" target="_self">Ayar Labs</a> is working a related angle — optical interconnect between silicon dies, replacing the copper traces that increasingly bottleneck chiplet communication.Target workload: dense matrix multiplication at data-center scale — transformer inference, large convolutions. The theoretical energy advantage is enormous for that one operation.Limitation: everything around the multiplier is still silicon. Digital-to-optical conversion is lossy and power-hungry; modulators and photodetectors don't scale the way transistors do; nonlinear operations (activations, softmax) have to go back to electronics. You get a brilliant matrix multiplier welded to a conventional CPU-style frontend. No general-purpose compute.Frame: one operation done physically better, at the cost of integration complexity.The most over-hyped item on this list and the one most likely to be misframed in casual conversation. Quantum computers are not faster classical computers. They don't speed up your web server. They don't speed up most of what you care about. What they do is solve a narrow class of problems — integer factoring (Shor's algorithm), unstructured search with a square-root speedup (Grover), certain quantum chemistry simulations (VQE, QPE), some optimization heuristics (QAOA) — in time complexity classes a classical machine cannot match.Current state: roughly 1000-qubit "NISQ" (noisy intermediate-scale quantum) machines from IBM, Google, IonQ, Quantinuum, and others. Useful for algorithm research; not useful for breaking RSA. Fault-tolerant quantum computing — where error correction schemes let arbitrarily long computations run reliably — is years to decades away, depending on whose roadmap you trust.Target workload: a narrow set of algorithms with proven quantum advantage — Shor for cryptography-relevant factoring, Grover for search, VQE for molecular ground states, some sampling problems.Limitation: decoherence — qubits lose their quantum state in microseconds, dissipated by any stray thermal noise. Error correction in theory works; in practice each logical qubit costs roughly 1000 physical qubits. Getting from today's 1000 noisy physical qubits to a useful number of logical qubits is the entire open problem of the field.Frame: a companion processor for specific algorithms, at the cost of a physics problem that may or may not be solved on a usable timescale.The final axis: don't change the architecture, change the substrate. Silicon CMOS is running into atomic-scale walls — gate dielectrics a few atoms thick, channel lengths where quantum tunneling stops being rounding error. Research materials under exploration include graphene (a single sheet of carbon atoms with extraordinary electron mobility), other 2D materials like molybdenum disulfide (MoS₂), and carbon nanotubes (whose first full RISC-V processor was demonstrated by MIT in 2019 with 14,000 transistors — a tech demo, not a product).Target: extend CMOS scaling beyond what silicon can physically do — higher mobility, lower leakage, smaller feature sizes.Limitation: manufacturing maturity. Silicon has had 60 years of trillion-dollar investment in fabrication, lithography, defect control, and yield management. No alternative material is remotely close. A working transistor in a research lab is decades from a billion-unit product.Frame: the same architecture with better transistors, at the cost of an entire industrial base that doesn't exist yet.Notice what this section did not do. It didn't tell you which technology will win. It didn't rank them. It gave you five examples of one question — target plus limitation — and trusted you to apply the question to the sixth, seventh, and hundredth technology you read about. That trust is the point. A lecturer could have handed you a table of facts to memorize; it would have been obsolete in two years. The question is not.Don't expect a single winner. The honest expectation is a zoo: silicon CPUs and GPUs remain the workhorse; specialized accelerators (NPUs, photonic matrix units, PIM banks, neuromorphic edge chips) handle their respective domains far better than the general-purpose core; quantum machines sit alongside as companion processors for problems no classical architecture touches; new materials slowly widen what silicon-style CMOS can do. The architect's job — and increasingly the systems programmer's job — is routing each workload to the substrate that fits it. Cloud providers already expose this explicitly: AWS, Azure, and GCP all offer instance types that pair general-purpose CPUs with specific accelerators, and choosing correctly is an engineering skill in its own right.The meta-skill you leave this section with is worth more than the five examples. Target. Limitation. Two questions. You'll read hundreds of architecture announcements over the next forty years. Ask the two questions of each one, and you'll be right about the landscape more often than the people writing the announcements.After this section, you can: read any new-compute-architecture announcement and quickly frame it with two questions — what does it accelerate, and what is it bad at? — without needing a pre-formed opinion.This is the last lecture of the course. Let's be honest about what happened over these 15 weeks.You showed up in Lecture 1 with, at best, a vague picture of what a computer is. A box. A CPU. Memory, somewhere. Programs run, somehow. If you'd tried to explain what happens when you type gcc hello.c &amp;&amp; ./a.out — from the keypress all the way to the pixels changing — you would have had to wave your hands at three or four places where the story got fuzzy. Maybe more. That's not a failing; that's where every student starts. Computer architecture is the most layered subject in the CS curriculum, and the layers are invisible until someone peels them back for you. Most CS students graduate having never seen most of those layers. You just did.Fifteen weeks of peeling is what just happened. It's easy to underestimate because there was no single dramatic moment — no "aha" of quicksort, no closed-form beauty of a mathematical proof. Architecture doesn't teach that way. It teaches by grinding through one layer at a time until, at the end, you can turn around and see the whole stack at once. That turn-around is this lecture.Let's be specific. This is not a motivational summary — it's a skill inventory, and you should read it as a claim about what you can actually do now.You started not knowing what a transistor does inside a NAND gate. You can now derive an ALU from gates — carry chains, multiplexers, the whole adder — and explain why the critical path through that ALU sets a ceiling on clock frequency.You started not knowing what %rsp is. You can now read x86 assembly, follow a stack frame through a function call, explain what the red zone is, and debug a segfault by inspecting registers and memory.You started assuming that pointers and arrays are the same thing because a textbook said so. You can now trace a pointer dereference all the way through the virtual-memory translation hardware — page table walk, TLB hit or miss, L1 cache lookup, possibly out through the memory controller to DRAM — and tell someone, in seconds, why their loop is slow.You started thinking "my code is fast because it's short." You can now reason about cache-friendliness — row-major vs column-major, working-set size vs L1/L2/L3 capacity, false sharing between threads — and predict which of two algorithmically-equivalent implementations will win on hardware.You started not having a model for "why does my multithreaded code break." You can now explain pipeline hazards, memory-ordering models, and why a naive shared counter is wrong in ways no test suite will catch.You started reading "96-core chiplet processor" as a marketing phrase. You can now argue about chiplet architectures — yield math, I/O die vs compute die, UCIe, the thermal cost of 3D stacking — and tell which of two CPU announcements is engineering and which is spec-sheet theater.None of these are small. Each one is what makes the next systems course — operating systems, compilers, databases, distributed systems, ML systems, security — feel tractable instead of arbitrary. You will never again read "L1 cache miss" in a paper without knowing exactly what physical event is being described. That is not nothing. That is the core of an engineer.<br><img alt="The 15-lecture arc of the course, grouped into seven thematic regions — from physics and gates (L01-L02) through x86, system architecture, memory, models, and performance, to this lecture (L15) as the synthesis." src="assets/l15-course-arc-eng.svg" target="_self" style="width: 820px; max-width: 100%;">The mental model you built in this course is not a box you close on the last day. It is the substrate every other systems course sits on top of.When a database textbook tells you that B-trees are preferred to balanced binary search trees for on-disk indexes, you already know why — page-sized nodes, fewer levels of indirection, locality-friendly traversal, and the disk's block-transfer model fighting you if you go smaller.When a distributed-systems paper explains why RDMA is faster than TCP, you know why — it skips the kernel, bypasses the copy into user space, lets the NIC write directly into a pre-pinned buffer.When a compiler writer talks about register pressure, escape analysis, or loop-invariant code motion, you know what those optimizations are trying to do to the machine — not just to the IR.When an ML systems engineer explains why LLM inference is memory-bound and not compute-bound, you can check that claim against the arithmetic intensity of the kernel and the bandwidth of HBM3.When an operating-systems course introduces virtual memory on week three, you won't need the whole lecture — you already have the picture. When a security paper talks about Spectre or Rowhammer, you understand the underlying microarchitectural state that makes those attacks possible.Every one of these conversations, which previously would have sounded like domain jargon, becomes concrete.That is the quiet payoff. Architecture is not its own career path for most of you. It is the subject that makes every other career path legible. The senior engineers you'll meet — the ones whose code reviews are worth more than most code — almost always have a version of the picture you just built. They may have built it differently, or in a different order, or mostly on the job. But they have it. Now so do you.In order of long-term importance — because by the time you graduate, half the specific facts in this course will be outdated, but these three habits will still be working.1. Read an architecture as trade-offs under constraints, not as a list of features. This is the deepest thing the course taught. Every design in every lecture — from a NAND gate's transistor count to Apple's M1 dispatch policy — is a choice made under constraints: power budget, area budget, bandwidth budget, latency tolerance, manufacturing cost, backward compatibility with a decade of shipped software.When you look at a new chip or a new ISA or a new memory technology, your first move should be to name the constraints it's operating under, then ask why it made the choices it did. "It's optimized for X at the expense of Y" is the sentence you should be able to write about any design you encounter.2. Profile before optimizing. The course never handed you a "make this fast" recipe. It handed you the tools to measure first — perf, rdtsc, cache counters, branch-mispredict counters, AMAT formulas, Amdahl bounds — and then decide. Engineers who optimize on vibes waste weeks speeding up code paths that don't matter while the real bottleneck sits in a different function. You have been shown, repeatedly, that the bottleneck is rarely where you'd guess. Carry that stance.3. Humility about the machine. Every lecture made the machine one layer more concrete, which means every lecture also made the layers below what we covered more visible as gaps. You now know enough to see what you don't know: OS internals in depth, networking hardware, silicon implementation, analog design, CAD tools, packaging, the fabs themselves, the power electronics, the firmware stack, the supply chain.A good engineer keeps a running inventory of the layers they can't see through, and knows where to find help when a problem leaks across one of those boundaries. You have that inventory now. Treat it as a map of where to go next, not as a failure to know everything.The engineers who hurt themselves most in their careers are the ones who don't know what they don't know — and confidently wade into problems whose depth they can't measure. You're past that risk, and that's worth quite a lot.These are the career traps you now have the background to resist — and the ones you will see colleagues fall into constantly.Believing benchmarks without understanding what they measure. A benchmark is a specific workload, run on a specific configuration, against a specific metric. When someone tells you "chip A is 20% faster than chip B," your first response is "at what workload, with what compiler, at what power envelope, on what memory configuration, measured over what window?" A surprising amount of industry argumentation is people quoting numbers whose context has been silently stripped. Geekbench, SPEC, MLPerf, even your own internal load tests — each measures something real, but the number only means what the methodology section says it means. Read the methodology. If there isn't one, the number isn't real.Optimizing without profiling. See skill #2 above. The engineers around you will, this year, spend real weeks hand-tuning code that isn't on the critical path — rewriting a function that accounts for 0.3% of total runtime, adding SIMD to a loop that runs once at startup, micro-optimizing an inner block while a 10× algorithmic improvement sits untouched one level up. Don't be one of them. Always profile first, then decide. perf, flamegraph, pprof, Instruments — the tool depends on your platform, but the discipline is universal.Treating "more hardware" as a solution to a software problem. The cloud makes this trap cheap and therefore endemic. A quadratic algorithm doesn't stop being quadratic when you run it on a bigger instance — it just bankrupts you slower. A database query that does a full table scan isn't "fixed" by a faster disk; it's just briefly hidden. A memory leak doesn't go away on a box with more RAM; it just takes longer to crash.The most sustainable optimization, as §7 made clear, is the one that makes the work smaller. Algorithms first. Data structures second. Hardware only when the algorithm and data structures are right, and you've profiled to prove the real bottleneck is below the software layer.If you're on the 6-ECTS track, you have one more event: the project defense. Treat it as the first engineering design review of your career, because that's what it is.A design review is not a demo. A demo says "here is a working thing." A design review says: "here is a claim, here is the evidence, here is the trade-off space I explored, here is what I chose and why, here are the limitations I know about, and here is what I'd do differently next time."Your professors have seen thousands of demos. What they're listening for is reasoning capacity — whether you can defend the choices you made under questioning, and whether you know enough about the machine to say why one design is better than another for your workload.Concretely, a strong defense answers questions like:
What problem does this solve, and who has it? — scope and motivation, in one paragraph.
What's the key design choice, and what did you rule out? — trade-off framing; what alternatives did you consider, and why did you pick this one?
What does it cost — in memory, in time, in complexity? — the engineer's honest accounting.
Where does it break? — the limits you've already discovered, not the ones the committee has to tell you about.
What would you redo? — evidence that you understand your own design, not just that you built it.
If something went wrong, don't hide it. Name it, explain what you learned, and move on. The defense is stronger for the honesty, not weaker. The weakest defenses are the ones that pretend everything worked perfectly; the strongest are the ones where the student owns their trade-offs.Bring a trade-off table. Literally — a small matrix of alternatives vs criteria, with your choice circled. It signals, in ten seconds, that you thought like an engineer.In 2005, on a Tuesday that almost no one noticed at the time, the free lunch ended. Clock frequencies stopped rising. Single-thread performance stopped doubling every eighteen months. The wave that had been carrying an entire generation of programmers — "write whatever you want, next year's CPU will make it fast" — quietly ran out. From that week onward, every performance gain has had to be designed for: by someone who understood parallelism, or caches, or the memory hierarchy, or specialized hardware, or the thermodynamics of a data-center rack.Every year since, the gap between engineers who understand what's happening inside the machine and engineers who don't has widened. The former are building the infrastructure the world runs on — compilers, runtimes, databases, schedulers, ML systems, operating systems, the entire plumbing of the cloud. The latter are increasingly dependent on the former, often without knowing it, and are increasingly limited in the problems they can even see, let alone solve.You are now on the right side of that line.That's not a gift the course gave you. It's a position you earned, over fifteen weeks, by doing the work.Use it. Build the kind of systems that only people on your side of the line can build. When you encounter an architecture you've never heard of, apply the question from §9 — target, limitation — and keep walking. When someone around you reaches for "more hardware" as a first response, push back with a profile. When you read a paper, read it through the map you now have.The course is over. The work starts.After this course, you can: look at a computer and see through its abstractions all the way to the gates — and back up to the software — with informed judgment at every layer.
The free lunch ended in 2005. Frequency scaling hit a thermal wall when Dennard scaling broke; every performance gain since has required conscious architectural work.
Two scaling laws bound the design space. Amdahl's Law caps strong scaling at 1/(1−p); Gustafson's Law predicts linear speedup when problem size grows with resources. Both are right — they answer different questions.
Power is governed by P = αCV²f + leakage. The V² term makes DVFS the single most effective energy-management technique in modern processors.
Dark silicon is the visible face of the post-Dennard era. You cannot power all transistors at once, so you either specialize, dim, or power-gate.
Heterogeneity is the industry's forced response. big.LITTLE, P+E cores, SoCs with GPU+NPU+DSP+ISP — all driven by the same efficiency arithmetic.
Chiplets and 3D stacking rescue Moore's Law economics. Smaller dies yield better; HBM and V-Cache solve bandwidth ceilings that monolithic memory could not.
Computing now has a planetary footprint. PUE, 24/7 carbon-free energy, and algorithmic efficiency are sustainability levers programmers and architects now share.
The meta-skill is reading the trade-off. Given any new architecture, you should be able to identify its target workload and its fundamental limitation.
What happens next:
Practical Control II (all students): covers Labs P11–P14 (NASM/Linux, profiling, virtual memory, SIMD). Bring what you built for those labs; be ready to explain architectural decisions.
Project defense (6 ECTS only): present your final project. Treat the defense as an engineering design review — a claim, evidence, a trade-off table, an honest assessment of limitations.
Questions a strong defense answers:
What was the problem, in architectural terms?
Which constraints did you face (memory, cache, bandwidth, latency, power)?
Which architectural knob did you turn (SIMD width, cache locality, branch prediction, parallelism), and why that one first?
How did you measure the effect?
What would you do differently with another week?
Forward reading (optional):
Hennessy &amp; Patterson, Computer Architecture: A Quantitative Approach — the canonical graduate text; Chapter 1 revisits everything in this lecture with hard numbers.
Software Engineering at Google — Chapter 21 (Scaling) applies Amdahl's Law to service design.
Intel Optimization Reference Manual and AMD Software Optimization Guide — the practical guides for the next layer of detail. <br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Amdahl%27s_law" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Amdahl%27s_law" target="_self">Amdahl's Law (Wikipedia)</a> — original 1967 paper framing; speedup wall derivation
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Gustafson%27s_law" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Gustafson%27s_law" target="_self">Gustafson's Law (Wikipedia)</a> — 1988 reevaluation; weak-scaling framing
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Dynamic_voltage_scaling" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Dynamic_voltage_scaling" target="_self">Dynamic Voltage Scaling (Wikipedia)</a> — DVFS mechanisms, governors, V-f relationship
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Dark_silicon" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Dark_silicon" target="_self">Dark Silicon (Wikipedia)</a> — TDP-bounded active area; Esmaeilzadeh et al. projections
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/ARM_big.LITTLE" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/ARM_big.LITTLE" target="_self">ARM big.LITTLE (Wikipedia)</a> — heterogeneous computing on ARM; scheduling modes
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Chiplet" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Chiplet" target="_self">Chiplet (Wikipedia)</a> — modular die architecture; AMD Zen, Intel tiles, UCIe
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Neuromorphic_computing" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Neuromorphic_computing" target="_self">Neuromorphic Computing (Wikipedia)</a> — brain-inspired architectures; Loihi, TrueNorth
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Three-dimensional_integrated_circuit" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Three-dimensional_integrated_circuit" target="_self">3D Integrated Circuit (Wikipedia)</a> — TSVs, stacked dies, thermal issues
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Heterogeneous_computing" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Heterogeneous_computing" target="_self">Heterogeneous Computing (Wikipedia)</a> — mixed-architecture systems; programming challenges
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Data_center" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Data_center" target="_self">Data Center (Wikipedia)</a> — PUE, cooling, global energy consumption
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/CMOS" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/CMOS" target="_self">CMOS Power Dissipation (Wikipedia)</a> — dynamic, static, short-circuit components
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Dennard_scaling" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Dennard_scaling" target="_self">Dennard Scaling (Wikipedia)</a> — 1974 scaling law; 2005 breakdown
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Moore%27s_law" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Moore%27s_law" target="_self">Moore's Law (Wikipedia)</a> — 2025 status; slowing cadence; economic pressures
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/High_Bandwidth_Memory" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/High_Bandwidth_Memory" target="_self">High Bandwidth Memory (Wikipedia)</a> — 3D-stacked DRAM; interposer-mounted; HBM4 (2025)
Images: All figures attributed in captions are from Wikimedia Commons under CC-BY-SA licenses. Full license information on the respective Wikimedia Commons pages.]]></description><link>lectures/eng/l15-eng.-energy-efficiency,-limitations,-and-future-trends.html</link><guid isPermaLink="false">Lectures/ENG/L15-ENG. Energy Efficiency, Limitations, and Future Trends.md</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate><enclosure url="assets/l15-moores-law-transistor-count.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l15-moores-law-transistor-count.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L15-NOTES-ENG. Energy Efficiency, Limitations, and Future Trends]]></title><description><![CDATA[Quick reference for Lecture 15 (final lecture). See full materials in L15-ENG. Energy Efficiency, Limitations, and Future Trends.md.LO5 (performance + architectural factors) · LO6 (parallelism) · LO7 (design trade-offs)
Three curves since 1970: transistor count still climbing, clock frequency flat since 2005, core count rising since 2005.
The "free lunch" ended in 2005 — Dennard scaling broke. Every gain since requires conscious architecture work.
Five beats of the lecture: (1) scaling laws, (2) energy physics, (3) industry response, (4) planet cost, (5) post-silicon.
Learn more: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Moore%27s_law" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Moore%27s_law" target="_self">Moore's Law</a> · <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Dennard_scaling" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Dennard_scaling" target="_self">Dennard scaling</a>Amdahl's Law (1967) — fix the problem, add processors:Speedup ceiling → 1 / (1 − p) no matter how many cores.Gustafson's Law (1988) — scale the problem with resources:
Strong scaling (Amdahl): same problem, more processors = faster
Weak scaling (Gustafson): more processors = bigger problem, same time
Real web search = strong scaling; ML training / climate sim = weak scaling
Both laws ignore bandwidth, coherence, sync cost → Universal Scalability Law adds contention term (real systems can slow down past a point)
<br>Learn more: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Amdahl%27s_law" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Amdahl%27s_law" target="_self">Amdahl's Law</a> · <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Gustafson%27s_law" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Gustafson%27s_law" target="_self">Gustafson's Law</a>TDP (Thermal Design Power) = hard physical ceiling. Everything downstream is a consequence.Dynamic power — from switching:
V² is the key term — halving V cuts dynamic power 4×. This is why DVFS works.
Activity factor α: clock tree ≈ 1.0 (→ clock gating universal); data logic ≈ 0.1
Static power — leakage even when idle:
One-way door: smaller transistors → lower V_th → more subthreshold leakage → static grows every shrink.
At 7 nm and below, static can be 30–50% of total budget.
Energy = Power × time. Race-to-idle (finish fast + gate off) wins when leakage is high.
<br>Learn more: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/CMOS" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/CMOS" target="_self">CMOS</a>
Halving both V and f cuts dynamic power ~8× (4× from V², 2× from f) — ~half performance, ~1/8 energy.
Chips ship with a (V, f) envelope — P-states from max (P0) down to idle (Pn).
V_min floor ≈ 0.7 V — thermal noise flips transistors below here.
Linux governors (cpufreq):
Turbo boost = DVFS above sustained envelope when thermal headroom permits.
Thermal throttling = closed-loop control bringing temp under T_max (your phone in the sun).
Race-to-idle vs slow-and-steady: depends on leakage. Mobile today races to idle.
<br>Learn more: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Dynamic_voltage_scaling" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Dynamic_voltage_scaling" target="_self">DVFS</a>
Dark silicon = fraction of transistors that must stay un-powered at any moment to stay within TDP.
Dennard recap: V can't drop below ~0.7 V; leakage keeps climbing; per-transistor power stopped falling ~2005.
At 8 nm, projections say 50–80% of chip must be dark at any instant (Esmaeilzadeh et al., ISCA 2011).
Turbo boost = dark silicon in action. Phone SoCs = most units idle most of the time.
Design responses (each a hedge against the same wall):<br>Learn more: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Dark_silicon" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Dark_silicon" target="_self">Dark silicon</a>If you can't power everything at once → homogeneous big cores lose. Heterogeneity is forced.
ARM big.LITTLE (2011): big cores (A72/A76/X4) wide+fast; LITTLE (A53/A55) narrow+efficient (5–10× better perf/watt on background).
Scheduling modes: Cluster switching → In-Kernel Switcher → HMP with EAS (today's default).
Intel Hybrid (Alder Lake, 2021): P-cores + E-cores + Thread Director. E-cores are 4× denser per mm².
Apple M-series: tight firmware dispatch between P, E, NPU, GPU, ISP, video codec.
Phone SoC catalog — 10+ distinct processing units sharing unified memory. Each 10–100× more efficient than CPU on its domain.Programming tax — OpenCL, CUDA, SYCL, Metal, oneAPI: no universal standard. Memory coherence across accelerators is a research problem. This is why CUDA's ecosystem is worth tens of billions.<br>Learn more: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/ARM_big.LITTLE" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/ARM_big.LITTLE" target="_self">ARM big.LITTLE</a> · <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Heterogeneous_computing" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Heterogeneous_computing" target="_self">Heterogeneous computing</a>
One ChatGPT query ≈ 3 Wh (~10× a Google search). OpenAI: ~1B queries/day.
Data centers 2024: ~415 TWh (~1.5% global electricity). 2030 projection: ~945 TWh.
Carbon: ~220 Mt CO₂ (≈1% of global emissions). E-waste: 62 Mt in 2022, 22% recycled.
PUE = Total Facility Power / IT Equipment PowerTwo sustainability levers that matter most:
Site selection — build where grids are cold + clean (Sweden, Iceland, Norway, Quebec)
24/7 carbon-free energy — hourly match, not annual average (Google target: 2030)
What programmers can do — algorithmic efficiency dominates. 10× faster algorithm = 10× less energy forever. Second: run on the right hardware (NPU over CPU for inference = 20× efficiency).<br>Learn more: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Data_center" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Data_center" target="_self">Data center energy</a>Chiplet economics — yield physics.
Defect density ≈ 0.1 /cm². Yield ≈ 1 / (1 + A·D)².
600 mm² monolithic → ~40% yield. 100 mm² chiplet → ~94%. 6 combined → ~73%.
Result: ~2× working processors per wafer. AMD: 2% → 30% server share in 2019–2023.
Chiplet products:UCIe (2022) = "USB of chiplets". Intel + AMD + Samsung + TSMC + ARM + Qualcomm — mixed-vendor chiplet ecosystem emerging.3D IC — Through-Silicon Vias (TSVs): short, fat, low-energy vertical wires.HBM (High Bandwidth Memory):
Every modern GPU and AI accelerator uses HBM.
Without HBM, $30K H100 would run at ~10% utilization — AI training economics collapse.
AMD 3D V-Cache — stacked L3 die on Zen CCD (32 MB → 96 MB). Gaming +15–30%.Thermal wall — cache stacks fine (DRAM/SRAM: 1–2 W); compute doesn't (CPU: 50–100 W, traps heat). Rule: stack memory on logic, not logic on logic.<br>Learn more: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Chiplet" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Chiplet" target="_self">Chiplet</a> · <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Three-dimensional_integrated_circuit" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Three-dimensional_integrated_circuit" target="_self">3D IC</a> · <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/High_Bandwidth_Memory" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/High_Bandwidth_Memory" target="_self">HBM</a>Meta-skill: given any new architecture, ask (1) target workload, (2) fundamental limitation.Expect a zoo of specialized substrates, not a single winner.<br>Learn more: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Neuromorphic_computing" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Neuromorphic_computing" target="_self">Neuromorphic computing</a>Three skills that survive obsolescence:
Read architecture as trade-offs under constraints — power, area, bandwidth, latency, cost, backward compatibility
Profile before optimizing — perf, rdtsc, cache counters; never guess the bottleneck
Humility about the machine — you now see layers you haven't seen (OS internals, silicon, CAD, packaging)
Three career traps:
Trusting benchmarks without knowing what they measure
Optimizing without profiling
Treating "more hardware" as a solution to a software problem
Every other systems course sits on top of this mental model — OS, compilers, databases, distributed systems, ML systems, security.
The free lunch ended in 2005. Every gain since has been designed for.
Amdahl bounds strong scaling at 1/(1−p); Gustafson gives linear for weak scaling. Both are right — different questions.
P = αCV²f + leakage. V² is the reason DVFS works.
Dark silicon is the visible face of the post-Dennard era.
Heterogeneity (big.LITTLE, P+E, CPU+GPU+NPU) is the forced response.
Chiplets rescue Moore's Law economics; HBM rescues memory bandwidth.
Computing now has a planetary footprint — PUE, 24/7 CFE, algorithmic efficiency.
The meta-skill: for any new architecture, ask target + limitation.
Practical Control II (all students): covers Labs P11–P14 (NASM/Linux, profiling, virtual memory, SIMD). Bring what you built; be ready to explain architectural decisions.Project defense (6 ECTS only) = first engineering design review. Strong defense answers:
What problem, in architectural terms?
What constraints (memory, cache, bandwidth, latency, power)?
Which knob did you turn first, and why that one?
How did you measure the effect?
What would you do differently?
Bring a trade-off table — alternatives × criteria, your choice circled.]]></description><link>lectures/eng/l15-notes-eng.-energy-efficiency,-limitations,-and-future-trends.html</link><guid isPermaLink="false">Lectures/ENG/L15-NOTES-ENG. Energy Efficiency, Limitations, and Future Trends.md</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate></item><item><title><![CDATA[L15-NOTES. Енергоефективність, обмеження та майбутні тренди]]></title><description><![CDATA[Стислий довідник до лекції 15 (фінальна лекція). Повні матеріали — у L15. Енергоефективність, обмеження та майбутні тренди.md.LO5 (продуктивність і архітектурні чинники) · LO6 (паралелізм) · LO7 (проєктні компроміси)
Три криві з 1970 року: кількість транзисторів і далі росте, тактова частота пласка з 2005, кількість ядер зростає з 2005.
«Безкоштовний ланч» скінчився у 2005-му — деннардівське масштабування зламалося. Кожен виграш відтоді вимагає свідомої архітектурної роботи.
П'ять тактів лекції: (1) закони масштабування, (2) фізика енергії, (3) відповідь індустрії, (4) ціна для планети, (5) пост-кремнієве майбутнє.
Детальніше: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Moore%27s_law" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Moore%27s_law" target="_self">Moore's Law</a> · <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Dennard_scaling" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Dennard_scaling" target="_self">Dennard scaling</a>Закон Амдала (1967) — фіксуємо задачу, додаємо процесори:Стеля прискорення → 1 / (1 − p), скільки б ядер ви не додали.Закон Густафсона (1988) — масштабуємо задачу разом із ресурсами:
Сильне масштабування (Амдал): та сама задача, більше процесорів = швидше
Слабке масштабування (Густафсон): більше процесорів = більша задача за той самий час
Реальний вебпошук — сильне масштабування; навчання ML / кліматична симуляція — слабке
Обидва закони ігнорують пропускну здатність, когерентність, синхронізацію → універсальний закон масштабованості додає доданок суперництва (реальні системи після певної точки можуть уповільнюватися)
<br>Детальніше: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Amdahl%27s_law" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Amdahl%27s_law" target="_self">Amdahl's Law</a> · <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Gustafson%27s_law" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Gustafson%27s_law" target="_self">Gustafson's Law</a>TDP (проєктна теплова потужність) = жорстка фізична стеля. Усе нижче по потоку — її наслідок.Динамічна потужність — від перемикання:
V² — ключовий член: зменшення V удвічі скорочує динамічну потужність у 4 рази. Саме тому DVFS працює.
Коефіцієнт активності α: дерево тактової розводки ≈ 1,0 (→ clock gating всюди); логіка даних ≈ 0,1
Статична потужність — витік навіть у простої:
Двері в один бік: менші транзистори → нижча V_th → більший підпороговий витік → статика зростає з кожним стисненням техпроцесу.
На 7 нм і нижче статика може становити 30–50% загального бюджету.
Енергія = потужність × час. Race-to-idle (швидко відпрацювати — і вимкнути) виграє там, де витоки великі.
<br>Детальніше: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/CMOS" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/CMOS" target="_self">CMOS</a>
Зменшення V і f удвічі скорочує динамічну потужність ~у 8 разів (4× від V², 2× від f) — приблизно половина продуктивності, ~1/8 енергії.
Чипи постачаються з робочою (V, f)-областю — P-стани від максимуму (P0) до простою (Pn).
Поріг V_min ≈ 0,7 В — нижче цієї межі тепловий шум перемикає транзистори довільно.
Регулятори Linux (cpufreq):
Турбо-режим = DVFS вище сталого конверта, коли тепловий запас дозволяє.
Тепловий троттлінг (теплове придушення) = замкнена петля керування, що тримає температуру під T_max (ваш телефон на сонці).
Race-to-idle проти «тихою сапою»: залежить від витоків. Сучасна мобільна електроніка обирає race-to-idle.
<br>Детальніше: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Dynamic_voltage_scaling" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Dynamic_voltage_scaling" target="_self">DVFS</a>
Dark silicon = частка транзисторів, які в будь-який момент мусять лишатися без живлення, щоб не вийти за TDP.
Деннард коротко: V не опускається нижче ~0,7 В; витоки далі ростуть; потужність на один транзистор перестала падати близько 2005 року.
На 8 нм прогнози кажуть, що 50–80% чипа має бути темним у будь-яку мить (Esmaeilzadeh et al., ISCA 2011).
Турбо-режим = dark silicon у дії. SoC смартфонів = більшість блоків більшість часу простоюють.
Проєктні відповіді (кожна — спроба обійти ту саму стіну):<br>Детальніше: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Dark_silicon" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Dark_silicon" target="_self">Dark silicon</a>Якщо не можна живити все одразу → однорідні великі ядра програють. Гетерогенність — вимушена.
ARM big.LITTLE (2011): великі ядра (A72/A76/X4) — широкі й швидкі; LITTLE (A53/A55) — вузькі й економні (у 5–10 разів краща продуктивність на ват на фоновій роботі).
Режими планування: кластерне перемикання → внутрішньоядерний перемикач → HMP з EAS (сьогоднішній стандарт).
Intel Hybrid (Alder Lake, 2021): P-ядра + E-ядра + Thread Director. E-ядра у 4 рази щільніші на мм².
Apple M-серія: тонка диспетчеризація на рівні прошивки між P, E, NPU, GPU, ISP, відеокодеком.
Каталог SoC смартфона — 10+ різних обчислювальних блоків на спільній уніфікованій пам'яті. Кожен у 10–100 разів ефективніший за CPU у своєму домені.Податок на програмування — OpenCL, CUDA, SYCL, Metal, oneAPI: універсального стандарту немає. Когерентність пам'яті між прискорювачами — досі дослідницька задача. Саме тому екосистема CUDA коштує десятки мільярдів.<br>Детальніше: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/ARM_big.LITTLE" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/ARM_big.LITTLE" target="_self">ARM big.LITTLE</a> · <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Heterogeneous_computing" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Heterogeneous_computing" target="_self">Heterogeneous computing</a>
Один запит до ChatGPT ≈ 3 Вт·год (~у 10 разів більше за пошук Google). OpenAI: ~1 млрд запитів на день.
Дата-центри 2024: ~415 ТВт·год (~1,5% світової електроенергії). Прогноз на 2030: ~945 ТВт·год.
Вуглець: ~220 млн т CO₂ (≈1% світових викидів). Електронні відходи: 62 млн т у 2022 році, переробляється 22%.
PUE = повна потужність об'єкта / потужність IT-обладнанняДва важелі сталості, які значать найбільше:
Вибір локації — будувати там, де мережі холодні та чисті (Швеція, Ісландія, Норвегія, Квебек)
Цілодобова безвуглецева енергія — погодинний збіг, а не середньорічний (ціль Google: 2030)
Що можуть зробити програмісти — алгоритмічна ефективність домінує. Алгоритм у 10 разів швидший = у 10 разів менше енергії назавжди. Другий важіль — запускати на правильному залізі (NPU замість CPU для інференсу = 20× ефективності).<br>Детальніше: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Data_center" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Data_center" target="_self">Data center energy</a>Економіка чіплетів — фізика виходу придатних.
Щільність дефектів ≈ 0,1 /см². Вихід ≈ 1 / (1 + A·D)².
Монолітний 600 мм² → ~40% виходу. Чіплет 100 мм² → ~94%. Шість разом → ~73%.
Результат: ~у 2 рази більше робочих процесорів на пластину. AMD: з 2% до 30% серверного ринку у 2019–2023.
Продукти на чіплетах:UCIe (2022) = «USB для чіплетів». Intel + AMD + Samsung + TSMC + ARM + Qualcomm — з'являється міжвендорна екосистема чіплетів.3D-мікросхема — наскрізні з'єднання через кремній (TSV): короткі, товсті, енергоощадні вертикальні провідники.HBM (High Bandwidth Memory):
Кожен сучасний GPU і ШІ-прискорювач використовує HBM.
Без HBM H100 за $30 тис. працював би на ~10% утилізації — економіка навчання ШІ розвалилась би.
AMD 3D V-Cache — складений L3-кристал на CCD Zen (32 МБ → 96 МБ). Ігри +15–30%.Теплова стіна — кеш-стеки нормально (DRAM/SRAM: 1–2 Вт); обчислювальні — ні (CPU: 50–100 Вт, замикає тепло всередині). Правило: складайте пам'ять на логіку, а не логіку на логіку.<br>Детальніше: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Chiplet" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Chiplet" target="_self">Chiplet</a> · <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Three-dimensional_integrated_circuit" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Three-dimensional_integrated_circuit" target="_self">3D IC</a> · <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/High_Bandwidth_Memory" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/High_Bandwidth_Memory" target="_self">HBM</a>Мета-навичка: дивлячись на будь-яку нову архітектуру, запитайте (1) цільове навантаження, (2) фундаментальне обмеження.Чекайте на зоопарк спеціалізованих субстратів, а не одного переможця.<br>Детальніше: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Neuromorphic_computing" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Neuromorphic_computing" target="_self">Neuromorphic computing</a>Три навички, які переживуть старіння технологій:
Читати архітектуру як компроміси під тиском обмежень — потужність, площа, пропускна здатність, затримка, вартість, зворотна сумісність
Профілювати перед оптимізацією — perf, rdtsc, лічильники кешу; ніколи не вгадуйте вузьке місце
Смирення перед машиною — ви тепер бачите шари, яких раніше не бачили (внутрішня кухня ОС, кремній, CAD, корпусування)
Три пастки у кар'єрі:
Довіряти бенчмаркам, не розуміючи, що саме вони вимірюють
Оптимізувати без профілювання
Трактувати «більше заліза» як розв'язок програмної задачі
Кожен наступний системний курс спирається на цю ментальну модель — ОС, компілятори, бази даних, розподілені системи, ML-системи, безпека.
Безкоштовний ланч скінчився у 2005-му. Кожен виграш відтоді спроєктовано свідомо.
Амдал обмежує сильне масштабування стелею 1/(1−p); Густафсон дає лінійне прискорення на слабкому масштабуванні. Обидва праві — питання різні.
P = αCV²f + витоки. V² — це причина, чому DVFS працює.
Dark silicon — видиме обличчя постденнардівської епохи.
Гетерогенність (big.LITTLE, P+E, CPU+GPU+NPU) — вимушена відповідь.
Чіплети рятують економіку закону Мура; HBM рятує пропускну здатність пам'яті.
Обчислення тепер мають планетарний слід — PUE, 24/7 CFE, алгоритмічна ефективність.
Мета-навичка: для будь-якої нової архітектури запитуйте ціль + обмеження.
Практичний контроль II (для всіх студентів): охоплює лабораторні P11–P14 (NASM/Linux, профілювання, віртуальна пам'ять, SIMD). Принесіть те, що ви зробили; будьте готові пояснити архітектурні рішення.Захист проєкту (лише 6 ECTS) = перше інженерне дизайн-рев'ю. Сильний захист відповідає на питання:
У чому полягає задача — в архітектурних термінах?
Які обмеження (пам'ять, кеш, пропускна здатність, затримка, потужність)?
Який важіль ви смикнули першим і чому саме його?
Як ви виміряли ефект?
Що зробили б інакше?
Принесіть таблицю компромісів — альтернативи × критерії, ваш вибір обведено.]]></description><link>lectures/l15-notes.-енергоефективність,-обмеження-та-майбутні-тренди.html</link><guid isPermaLink="false">Lectures/L15-NOTES. Енергоефективність, обмеження та майбутні тренди.md</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate></item><item><title><![CDATA[Glossary-CSA]]></title><description><![CDATA[English-Ukrainian terminology for the CSA course
Technical terms: Use Ukrainian equivalents from this glossary consistently
Proper names: Use Ukrainian transliterations from the Proper Names table above
Acronyms: Keep English acronyms, provide Ukrainian expansion on first use
Formulas: Keep mathematical notation (kT ln(2), I²R, etc.)
References: Keep original URLs and citation format
Last updated: April 2026 (L14 terms added — pipelining, hazards, branch prediction, OoO execution, speculation side channels, multithreading)]]></description><link>lectures/eng/glossary-csa.html</link><guid isPermaLink="false">Lectures/ENG/Glossary-CSA.md</guid><pubDate>Tue, 21 Apr 2026 08:38:48 GMT</pubDate></item><item><title><![CDATA[L13-NOTES. Арифметика з рухомою комою]]></title><description><![CDATA[Короткий довідник до Лекції 13. Повні матеріали: L13. Арифметика з рухомою комою.md.
Дійсні числа мають нескінченну точність; машинне слово — 32/64 біти — тож наближення неминуче
0.1 + 0.2 = 0.30000000000000004 у будь-якій мові, що використовує IEEE 754
0.1₁₀ = 0.000110011…₂ (нескінченний періодичний дріб) — точно зберегти неможливо
Реальні катастрофи: ракета Patriot (1991, дрейф таймера), Ariane 5 (1996, переповнення), Ванкуверська фондова біржа (1982, зміщення через усікання)
Абсолютна похибка: Δ = |A − A_comp| (у тих самих одиницях, що й число)Відносна похибка: δ = Δ/A ≈ Δ/A_comp (безрозмірна, не залежить від масштабу)Приклад (π): A_comp = 3.1416 → Δ ≈ 7.35×10⁻⁶, δ ≈ 2.34×10⁻⁶Відносна похибка зручніша: абсолютна похибка в 1 нехтовно мала для 10⁶, але катастрофічна для 10⁻⁶.Три наслідки: похибки округлення, катастрофічне скорочення, нестійкість алгоритмів.
Фіксуємо m бітів під дробову частину → абсолютна похибка стала й дорівнює 2⁻ᵐ
Відносна похибка плаває: мала біля максимуму, величезна біля нуля
Машинний нуль: значення &lt; 2⁻ᵐ округлюються до нуля
Досі у вжитку в цифрових сигнальних процесорах, у фінансах (копійки як цілі), у програмованих вентильних матрицях
Обмеження: точність марнується на великих числах, а для малих бракує діапазону
A = M × bᵉ — мантиса (точність) × основа^експонента (діапазон)
Біти експоненти задають ДІАПАЗОН; біти мантиси — ТОЧНІСТЬ
3-цифрова мантиса + 2-цифрова експонента = 199 порядків величини у 7 символах
Нормальна форма: 0 ≤ |M| &lt; 1 (неоднозначна: одне число має багато представлень)Нормалізована форма: 1 ≤ |M| &lt; основа (єдине представлення — саме те, що нам потрібно)Прихований біт: нормалізована двійкова мантиса завжди починається з 1 → цю одиницю не зберігаємо → n+1 ефективних бітів із n збереженихТри поля: S (1 біт, знак) | E (m бітів, зміщена експонента) | M' (n бітів, залишок мантиси)W = 1 + m + n бітів Знак: S=0 — додатне, S=1 — від'ємне (знак-модуль, а не доповняльний код)
Зміщена експонента: E = e + зміщення, де зміщення = 2^(m−1) − 1 Одинарна точність: зміщення = 127; подвійна точність: зміщення = 1023
Навіщо зміщення? Щоб числа з рухомою комою можна було порівнювати як беззнакові цілі Мантиса: M = 1 + M'/2ⁿ (прихований біт відновлюємо)
Головна формула:A = (-1)^S × (1 + M'/2^n) × 2^(E − зміщення)
Декодування (біти → десяткове): виокремити S, E, M' → підставити у формулу.Кодування (десяткове → біти):
Перевести |число| у двійкову систему
Нормалізувати до вигляду 1.xxxxx₂ × 2ᵉ
Округлити мантису до n бітів
M' — дробова частина (відкинувши «1.»)
E = e + зміщення, записати у m-бітовому двійковому вигляді
Зібрати S | E | M'
Приклад: −2012.6875 → двійкове 11111011100.1011₂ → нормалізація 1.1111011100101₂ × 2¹⁰ → E = 137 = 10001001₂ → результат: C4 FB 96 00hПерегляд бітів:
Python: struct.pack('!f', x).hex()
Java: Float.floatToIntBits(x)
C: union type-punning
Формула точності: k = (n+1) × log₁₀(2) ≈ 0.301 × (n+1)Три типи збоїв:
Переповнення: результат надто великий → ±∞
Втрата значимості: результат надто малий → денормалізоване число або 0
Усікання: мантиса надто довга → похибка округлення
Числа з рухомою комою розподілені на осі нерівномірно — густіше біля нуля, рідше на краях.Історія: 1985 рік, Вільям Каган (Берклі). Чинна версія: IEEE 754-2019 / ISO 60559:2020.Формати:Повна карта кодування (одинарна точність):Ключові спеціальні значення:
±0: +0 = 00000000h, −0 = 80000000h; +0 == −0, але 1/+0 = +∞, 1/−0 = −∞
Денормалізовані: прихований біт = 0; поступова втрата значимості (плавний перехід до нуля)
±∞: переповнення або x/0 (x≠0)
NaN: 0/0, ∞−∞, √(від'ємне); NaN ≠ NaN; «заразний» в арифметиці (поширюється далі обчисленнями)
Додавання: вирівняти експоненти → додати мантиси → нормалізувати → округлити. Під час вирівнювання можуть втрачатися біти меншого операнда.Множення: XOR знаків → додати експоненти (відняти зміщення) → помножити мантиси → нормалізувати → округлити. Втрат від вирівнювання немає.Ділення: XOR знаків → відняти експоненти (додати зміщення) → поділити мантиси → нормалізувати → округлити.Режими округлення:
Округлення до найближчого парного (за замовчуванням) — усуває статистичне зміщення
Округлення до нуля (усікання)
Округлення до +∞ (до стелі)
Округлення до −∞ (до підлоги)
Захисний / округлювальний / фіксований біти: 3 додаткові біти під час обчислення забезпечують коректне округлення в апаратурі.Машинний епсилон (ε): найменше число, для якого 1.0 + ε ≠ 1.0
float32: 2⁻²³ ≈ 1.19×10⁻⁷
float64: 2⁻⁵² ≈ 2.22×10⁻¹⁶
Катастрофічне скорочення: віднімання майже рівних чисел → більшість значущих бітів скорочується → залишається мало значущих цифр. Рішення: алгебраїчне перетворення виразу.Втрата асоціативності: (1e20 + (-1e20)) + 1.0 = 1.0, але 1e20 + ((-1e20) + 1.0) = 0.0Підсумовування Кагана: використовує компенсаційну змінну для відновлення втрачених молодших бітів під час накопичення.Захисні практики:
Не порівнювати числа з рухомою комою через == — використовувати допуск
Уникати віднімання майже рівних значень
Підсумовувати в порядку зростання модуля або застосовувати підсумовування Кагана
Для проміжних обчислень використовувати вищу точність
Історія x87: 8087 (1980) → 80287 (1982) → 80387 (1987, перша повна реалізація IEEE 754) → інтегровано з 80486 (1989)Архітектура x87: 8 регістрів R0–R7 як стек (не пласка модель), 80-бітова розширена точність усередині, підтримка int16/32/64, float32/64/80, BCD80Хронологія SSE/AVX: SSE (1999, 128-біт) → SSE2 (2001, подвійна точність) → AVX (2011, 256-біт) → AVX-512 (2016, 512-біт)x87 досі актуальний для 80-бітового long double, коли потрібно понад 16 десяткових цифр.
Дійсні числа → скінченні наближення → абсолютна та відносна похибки
Рухома кома: експонента = діапазон, мантиса = точність, прихований біт = додатковий безкоштовний біт
IEEE 754: одинарна/подвійна/розширена точність, спеціальні значення (±0, ±∞, NaN), денормалізовані числа для поступової втрати значимості
Арифметика з рухомою комою НЕ асоціативна; катастрофічне скорочення знищує значущі цифри
Підсумовування Кагана та свідомий вибір режиму округлення — практичні засоби захисту
x87 застарів; SSE/AVX — сучасний стандарт Дослідити біти float/double за допомогою Python struct
Експериментально знайти машинний епсилон
Продемонструвати (a+b)+c ≠ a+(b+c)
Спостерегти катастрофічне скорочення
Порівняти підсумовування Кагана з наївним підсумовуванням
]]></description><link>lectures/l13-notes.-арифметика-з-рухомою-комою.html</link><guid isPermaLink="false">Lectures/L13-NOTES. Арифметика з рухомою комою.md</guid><pubDate>Mon, 20 Apr 2026 10:37:40 GMT</pubDate></item><item><title><![CDATA[L12. Віртуальна пам'ять поглиблено]]></title><description><![CDATA[
Лекція 12 курсу «Архітектура комп'ютерних систем» в НаУКМА.
Після цієї лекції ви зможете: Пояснити три цілі віртуальної пам'яті: ізоляцію процесів, абстракцію адресного простору та ефективне використання пам'яті Описати основи сторінкування: сторінки, фрейми, таблиці сторінок і записи таблиці сторінок з усіма прапорцями PTE в x86-64 Простежити шлях віртуальної адреси через усі етапи трансляції, зокрема зіставлення VPN із номером фрейму Порівняти пласкі та багаторівневі таблиці сторінок і розібрати чотирирівневу структуру таблиці сторінок x86-64 (CR3 → PML4 → PDPT → PD → PT) Пояснити роботу TLB (буфера асоціативної трансляції) — влучення й промахи, ASID, перемикання контексту — та обчислити ефективний час доступу до пам'яті за формулою AMAT Розрізняти мінорні, мажорні та недійсні сторінкові помилки й описати, як операційна система їх обробляє Порівняти алгоритми заміщення сторінок: FIFO, оптимальний, LRU та алгоритм годинника (друга можливість) Описати механізми захисту пам'яті: біти дозволів R/W, U/S та NX (заборона виконання), а також роль рандомізації розташування адресного простору (ASLR) у забезпеченні безпеки Уявімо багатоквартирний будинок, у якому кожна квартира має однакове планування — кімнати з номерами від 1 до 4, коридор, вхідні двері. Зсередини легко забути, що десятки інших людей живуть у тому самому будинку. Меблів сусідів ми ніколи не бачимо; до їхньої кухні зайти не можемо. Проте керівник будинку знає правду: кожна квартира відображається на спільну фізичну інфраструктуру, і саме він розподіляє ресурси, забезпечує ізоляцію та час від часу перерозподіляє місце, коли простору бракує.Такою і є віртуальна пам'ять. Кожен процес бачить власний приватний неперервний адресний простір — що починається з тих самих нижніх адрес і тягнеться до тих самих верхніх. Операційна система — це керівник будинку, апаратний MMU (блок керування пам'яттю) — це система замків на кожних дверях, а разом вони підтримують ілюзію, що кожен процес володіє всією машиною.Уперше ми зустрілися з віртуальною пам'яттю в L06 (захищений режим, кільця привілеїв, General Protection Fault) та в L08 (сторінки, фрейми, таблиці сторінок, записи таблиці сторінок, сторінкові помилки, основи TLB). L08 відповіла на питання що — з яких компонентів усе складається і як виглядає базовий потік. Ця лекція відповідає на питання як і навіщо в масштабі: як 64-бітний процесор проходить чотири рівні таблиць сторінок, якою є реальна вартість промаху TLB (у зв'язку з формулою AMAT з L11), як кількісно порівнюються алгоритми заміщення сторінок, а також які механізми захисту пам'яті (наприклад, біт NX) запобігають цілим категоріям атак на безпеку.Віртуальна пам'ять одночасно служить трьом цілям, і кожне проєктне рішення в цій лекції пов'язане принаймні з однією з них:Ізоляція. Процес A не може ні читати, ні записувати пам'ять процесу B — апаратура фізично не транслюватиме віртуальні адреси A у фізичні фрейми B. Помилка в браузері не пошкодить текстовий редактор; скомпрометований застосунок не прочитає ваші SSH-ключі.Абстракція. Кожен процес бачить однаковий охайний неперервний макет — код знизу, купа росте вгору, стек росте вниз. Процес не знає, чи його сторінки розкидані по оперативній пам'яті, чи частково вивантажені на диск. Віртуальний адресний простір — це контракт: «Ви користуєтеся цими адресами; ми зробимо так, щоб вони працювали».Ефективність. Оперативна пам'ять розглядається як кеш для диска. У фізичній пам'яті перебувають лише активно використовувані сторінки — це сторінкування за запитом. Програма, яка виділяє 2 ГБ, але звертається лише до 200 МБ, споживає тільки 200 МБ оперативної пам'яті. Система з 16 ГБ може запускати десятки процесів, сукупні віртуальні простори яких сягають сотень гігабайтів.Апаратура забезпечує механізми (швидку трансляцію, генерацію виключень, перевірку дозволів), а програмне забезпечення — політику (які сторінки завантажувати, які витісняти, як обробляти помилки). Це поширює ту саму ідею з L11 на один рівень нижче в ієрархії — від «SRAM як кеш для DRAM» до «DRAM як кеш для диска».У середовищі Real Mode, яке ми використовували в лабораторних роботах P1–P10, програми зверталися до фізичної пам'яті безпосередньо — без ізоляції, без захисту, без можливості задіяти більше пам'яті, ніж фізично встановлено. <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Atlas_(computer)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Atlas_(computer)" target="_self">Комп'ютер Atlas</a> (Манчестер, 1962) запровадив першу робочу віртуальну пам'ять, прозоро переміщуючи дані між швидкою основною пам'яттю та повільнішим барабанним сховищем. Через шістдесят років кожен комп'ютер, телефон і сервер спирається на ту саму фундаментальну ідею.Почнімо з самого адресного простору.Центральна абстракція віртуальної пам'яті — адресний простір, тобто множина всіх адрес, які процес може використовувати. Кожен процес має два адресні простори, хоча знає лише про один: Віртуальний адресний простір — це те, що бачить процес. Він приватний, неперервний і — у 64-бітній системі — величезний. Код, дані, купа та стек процесу розташовані саме за віртуальними адресами. Фізичний адресний простір — це фактично встановлена у машині DRAM. Він спільний для всіх процесів, керований ОС і невидимий для програм у режимі користувача. Процес працює виключно у віртуальному адресному просторі й не знає, де фізично розміщені його дані. Відображення між просторами забезпечують MMU та ОС.Кожен процес бачить приблизно однаковий макет віртуального адресного простору. Деталі в Linux, Windows і macOS відрізняються, проте загальна структура універсальна:<br><img alt="Макет віртуального адресного простору процесу: простір ядра зверху, стек росте вниз, купа росте вгору, сегменти BSS, даних та коду" src="assets/l12-virtual-address-space.svg" target="_self" style="width: 650px; max-width: 100%;">Звернімо увагу на кілька ключових особливостей цього макету.Простір ядра зверху. Верхня частина віртуального адресного простору кожного процесу зарезервована для ядра. У 32-бітному Linux стандартний поділ такий: 3 ГБ користувачеві і 1 ГБ ядру. У 64-бітному Linux простір користувача охоплює нижні 128 ТБ (0x0000000000000000–0x00007FFFFFFFFFFF), а простір ядра займає верхню половину, починаючи з 0xFFFF800000000000. Сторінки ядра присутні в таблиці сторінок кожного процесу, але позначені як доступні лише в режимі супервізора — звернення з режиму користувача спричиняє сторінкову помилку.Захист від нульового вказівника. Найнижчі адреси (перша сторінка або кілька перших сторінок) навмисно не відображені. Саме тому розіменування нульового вказівника спричиняє сторінкову помилку, яку ОС перетворює на сигнал SIGSEGV, а отже, найпоширеніша помилка програмування призводить до чистого аварійного зупину з можливістю відлагодження, а не до тихого пошкодження даних.Стек росте вниз, купа росте вгору. Стек починається біля вершини простору користувача і розростається в бік нижчих адрес. Купа починається після BSS і росте вгору. Великий невідображений проміжок між ними максимізує відстань між цими двома динамічно зростаючими ділянками, тож колізії стають малоймовірними.Сегмент коду — лише для читання. Виконуваний код програми завантажується у сторінки, доступні лише для читання та виконання. Будь-яка спроба запису спричиняє помилку захисту, що унеможливлює ситуації, коли помилки чи експлойти модифікують працюючий код.Ключова ідея: кожен процес бачить однаковий віртуальний макет, але в кожного цей макет відображається на різні фізичні розташування.Нехай процес A має змінну за віртуальною адресою 0x7FFE1000. Процес B теж має змінну за 0x7FFE1000. Та сама віртуальна адреса, але різні фізичні фрейми — скажімо, фрейм 42 381 для A та фрейм 107 209 для B. Кожен процес має власну таблицю сторінок із цілком незалежними записами.<br>З погляду кожного процесу він має повний, приватний адресний простір і є єдиною запущеною програмою. Справжня картина — десятки процесів, які спільно використовують фізичну оперативну пам'ять — прихована за рівнем трансляції. Це абстракція «єдиного мешканця», описана в <a data-tooltip-position="top" aria-label="https://diveintosystems.org/book/C13-OS/vm.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://diveintosystems.org/book/C13-OS/vm.html" target="_self">Dive Into Systems</a>.<br><img alt="Два процеси з ідентичним макетом віртуального адресного простору, що відображаються на різні фізичні фрейми у спільній пам'яті" src="assets/l12-virtual-physical-mapping.svg" target="_self" style="width: 650px; max-width: 100%;">Хоча кожен процес має приватний віртуальний адресний простір, фізичний адресний простір — це єдиний спільний пул DRAM. Якщо в машині 16 ГБ оперативної пам'яті, фізичний адресний простір охоплює діапазон від 0x000000000 до 0x3FFFFFFFF. Кожен байт — реальний конденсатор на реальному чіпі DRAM.ОС керує фізичною пам'яттю за допомогою таблиці фреймів, яка відстежує стан кожного фізичного фрейму: вільний, виділений (і якому саме процесу чи сторінці) або зарезервований для ядра. Один фізичний фрейм може виконувати різні функції: Приватне відображення: одна віртуальна сторінка одного процесу відображається на один фізичний фрейм — типовий випадок. Спільне відображення: записи таблиці сторінок (PTE) кількох процесів вказують на один і той самий фізичний фрейм. Саме так працюють спільні бібліотеки: код libc.so існує у фізичній пам'яті лише в одному екземплярі, проте кожен процес відображає ці фрейми як сторінки, доступні для читання та виконання, що заощаджує сотні мегабайтів у завантаженій системі. Копіювання при записі (COW): після fork() батьківський і дочірній процеси спільно використовують усі фрейми (позначені лише для читання). Лише коли один із них записує дані, ОС копіює саме цей окремий фрейм. Як ми вже бачили на L08, завдяки цьому створення процесів стає надзвичайно ефективним. Коли ОС перемикається з процесу A на процес B, вона оновлює один-єдиний регістр — CR3 (Control Register 3), що містить фізичну адресу таблиці сторінок верхнього рівня. Запис нового значення до CR3 змушує MMU миттєво перейти на таблицю сторінок процесу B.Зміна CR3 також інвалідує кешовані трансляції у TLB. На старіших процесорах це означало повне скидання TLB при кожному контекстному перемиканні. Сучасні процесори підтримують ідентифікатори контексту процесу (PCID) — теги на кожному записі TLB, які вказують, якому процесу цей запис належить. За наявності PCID перемикання CR3 не скидає TLB: записи процесу A просто залишаються неактивними і готові до повторного використання, щойно ОС перемкнеться назад.<br>Обмеження 32-бітної архітектури в 4 ГБ до середини 2000-х стало недостатнім. Intel <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Physical_Address_Extension" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Physical_Address_Extension" target="_self">PAE</a> розширив фізичні адреси до 36 біт (64 ГБ), однак кожен процес усе одно був обмежений 4 ГБ віртуальної пам'яті. Розширення x86-64 від AMD (2003) збільшило віртуальні адреси до 48 біт (256 ТБ на процес).Використовуються лише 48 з 64 біт, бо повний 64-бітний простір (16 ЕБ) потребував би непрактично великих таблиць сторінок. Архітектура x86-64 застосовує канонічні адреси: біти 63–48 мають бути або всі нулями, або всі одиницями, завдяки чому виникають нижня половина (простір користувача) та верхня половина (простір ядра) з неканонічним проміжком між ними. Новітні процесори підтримують 5-рівневе сторінкування (LA57), яке розширює адресацію до 57 біт (128 ПБ) для серверних навантажень.Макет віртуального адресного простору втілює в собі важливі властивості безпеки та інженерні рішення: Сторінки ядра в адресному просторі кожного процесу означають, що ОС не потрібно перемикати таблиці сторінок для системних викликів чи переривань — змінюється лише рівень привілеїв (Ring 3 → Ring 0). Завдяки цьому системні виклики лишаються швидкими (сотні наносекунд). Невідображений проміжок між стеком і купою діє як захисна ділянка: переповнення стека натрапляє на невідображену сторінку і спричиняє чисте аварійне завершення замість тихого перезаписування даних купи. <br>
Рандомізація розташування адресного простору (ASLR) на кожному запуску випадковим чином обирає початкові адреси стека, купи та спільних бібліотек, чим значно ускладнює техніки експлуатації вразливостей на кшталт <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Return-oriented_programming" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Return-oriented_programming" target="_self">ROP</a>. Окремі записи таблиці сторінок для кожної ділянки дозволяють точно налаштувати захист: сторінки коду — лише для читання та виконання, сторінки даних і стека — для читання та запису із забороною виконання (через біт NX). Деталі — у розділі 10. У лекції L08 сторінкову організацію вже було представлено як механізм, на якому тримається віртуальна пам'ять. Тепер формалізуймо кожну деталь — наприкінці цього розділу ми зможемо розкласти віртуальну адресу на поля, вручну пройти таблицю сторінок та побудувати результуючу фізичну адресу з точною побітовою арифметикою.Віртуальна пам'ять розбиває два різних адресних простори на блоки однакового розміру: Віртуальний адресний простір (той, який бачить програма) поділяється на сторінки. Фізичний адресний простір (той, який бачать мікросхеми оперативної пам'яті) поділяється на фрейми (їх також називають сторінковими фреймами). Сторінки та фрейми завжди мають однаковий розмір. У системах x86 та x86-64 стандартний розмір становить 4 КБ = 4 096 байтів = 2^12 байтів. (Більші розміри — 2 МБ «великі сторінки» та 1 ГБ «гігантські сторінки» — існують для спеціалізованих навантажень на кшталт баз даних і віртуальних машин, проте 4 КБ залишається типовим значенням, яким ми послуговуватимемося в усіх прикладах, якщо не зазначено інше.)Ключова перевага — несуміжне розміщення: віртуальні сторінки програми 0, 1, 2, 3 можуть відображатися на фізичні фрейми 7302, 41, 998 та 12055, розкидані по всій оперативній пам'яті. Програма бачить суцільний адресний простір, а ОС розміщує кожну сторінку в будь-якому доступному фреймі. Так повністю усувається зовнішня фрагментація: фрейми взаємозамінні, і ОС ніколи не потребує ущільнення пам'яті.Кожна віртуальна адреса неявно поділяється на два поля:Зміщення вказує на конкретний байт усередині сторінки. Оскільки сторінка має розмір 2^12 байтів, зміщенням завжди слугують молодші 12 бітів — вони здатні адресувати будь-який байт від 0 до 4 095 усередині сторінки.VPN — це все, що розташовано вище цих 12 бітів. Він визначає, до якої саме сторінки належить адреса. Кількість бітів VPN залежить від загальної ширини віртуальної адреси:Розбиття визначається виключно розміром сторінки. За сторінок 4 КБ молодші 12 бітів завжди є зміщенням, а решта бітів — завжди VPN. Нічого налаштовувати не потрібно: апаратура виконує цей поділ автоматично під час кожного звернення до пам'яті.Таблиця сторінок — це структура даних (концептуально — простий масив), яку ОС підтримує для кожного процесу. Вона індексується за VPN, і кожен її запис (так званий запис таблиці сторінок, або PTE) містить номер фізичного фрейму та біти метаданих (їх ми детально розберемо в Розділі 4).Коли процесор генерує віртуальну адресу, трансляція відбувається так:
Розбити адресу на VPN та зміщення.
Взяти VPN як індекс у таблиці сторінок поточного процесу.
Прочитати PTE за цим індексом.
Видобути номер фізичного фрейму з PTE.
Побудувати фізичну адресу, з'єднавши номер фрейму з початковим зміщенням.
Зміщення проходить без змін — це прямий і елегантний наслідок того, що сторінки та фрейми мають однаковий розмір. Змінюється лише частина адреси «який блок»; частина «де саме всередині блоку» у віртуальній та фізичній адресах ідентична.Процесор повинен знати, де саме у фізичній пам'яті розташована таблиця сторінок поточного процесу. Ця інформація зберігається у спеціальному регістрі процесора: У x86 (32-бітному): регістр CR3 містить фізичну адресу каталогу сторінок верхнього рівня. У x86-64: CR3 містить фізичну адресу таблиці PML4 (верхнього рівня чотирирівневої ієрархії). Коли ОС виконує контекстне перемикання — перехід від виконання Процесу A до Процесу B, — вона зберігає значення CR3 Процесу A та завантажує CR3 Процесу B. Цей єдиний запис у регістр фактично підмінює весь віртуальний адресний простір. Після перемикання та сама віртуальна адреса 0x00401000 відображається вже на зовсім інший фізичний фрейм, бо таблиця сторінок, яку вона індексує, — це зовсім інша таблиця. Саме так ОС створює у кожного процесу ілюзію власної приватної пам'яті.CR3 доступний лише в режимі ядра (ring 0): якби код користувача міг його змінити, ізоляцію було б повністю зруйновано.Розгляньмо повну трансляцію адреси у спрощеній 16-бітній системі зі сторінками 4 КБ. Цей невеликий приклад робить видимим кожен біт.Умови: Віртуальні адреси: 16 бітів → адресний простір = 2^16 = 65 536 байтів = 64 КБ Розмір сторінки: 4 КБ = 2^12 байтів Зміщення: 12 бітів (біти 11–0) VPN: 4 біти (біти 15–12) → 2^4 = 16 віртуальних сторінок (сторінки 0–15) Фізична пам'ять: нехай 32 КБ = 2^15 байтів → 8 фреймів (фрейми 0–7) Таблиця сторінок для цього процесу:Трансляція віртуальної адреси 0x2A3C:Крок 1 — переведення у двійкову систему:0x2A3C = 0010 1010 0011 1100 ──── ──────────────── VPN Зміщення
Старші 4 біти — 0010 = VPN 2. Молодші 12 бітів — 1010 0011 1100 = зміщення 0xA3C (у десятковій — 2 620).Крок 2 — шукаємо VPN 2 у таблиці сторінок: фрейм = 3, присутній = так.Крок 3 — будуємо фізичну адресу. Для номера фрейму в нашій системі з 8 фреймами потрібні 3 біти (адже 2^3 = 8). Номер фрейму 3 у двійковій системі — 011. Фізична адреса:Фрейм: 011
Зміщення: 1010 0011 1100
─────────────────────────
Фізична адреса: 011 1010 0011 1100 = 0x3A3C
Віртуальний байт за адресою 0x2A3C лежить за фізичною адресою 0x3A3C. Звернімо увагу, що зміщення 0xA3C в обох адресах збігається — змінився лише номер сторінки/фрейму.А що, як спробувати віртуальну адресу 0x1500?0x1500 = 0001 0101 0000 0000 ──── ──────────────── VPN=1 Зміщення=0x500
VPN 1 позначено у таблиці сторінок як «відсутній». Апаратура генерує сторінкову помилку (виняток 14 на x86). Обробник сторінкових помилок в ОС має тоді вирішити: завантажити сторінку з диска, виділити новий фрейм чи завершити процес. Це ми детально розглянемо в Розділі 8.За простоту конструкції «масив, індексований за VPN» доводиться платити. Обчислимо розмір таблиці сторінок для реалістичних систем:32-бітні віртуальні адреси, сторінки 4 КБ:
VPN: 20 бітів → 2^20 = 1 048 576 записів
Кожен PTE: 4 байти (цього досить для 20-бітного номера фрейму та бітів метаданих)
Загальний розмір таблиці сторінок: 2^20 × 4 = 4 МБ на процес
Для x86-64 з 48-бітними віртуальними адресами: 2^36 записів × 8 байтів = 512 ГБ на процес — більше, ніж фізична пам'ять, яку така таблиця відображає. Це абсурд: типовий процес використовує лише кілька тисяч сторінок.Розв'язок — багаторівнева (ієрархічна) таблиця сторінок (Розділ 6): дерево менших таблиць, у якому гілок для невикористаних регіонів узагалі не існує, що зменшує накладні витрати з гігабайтів до десятків кілобайтів. Платою є кілька звернень до пам'яті на кожну трансляцію (по одному на рівень), тому TLB (Розділ 7) стає критично важливим.Кожен PTE — це не просто номер фрейму, а щільно упаковане бітове поле, у якому кожен біт керує окремим аспектом того, як апаратне забезпечення обробляє цю сторінку.Незалежно від конкретного процесора, кожен PTE містить дві категорії інформації: Номер фізичного фрейму — адреса фізичного фрейму, на який відображається ця віртуальна сторінка. Біти керування та метаданих — прапорці, що повідомляють апаратному забезпеченню, як обробляти звернення: чи присутня сторінка в оперативній пам'яті, які типи доступу дозволені, чи зверталися до сторінки та чи була вона змінена, а також підказки для кешування. Точне розташування бітів залежить від архітектури, але категорії лишаються універсальними. Розгляньмо детально формат x86-64.PTE в x86-64 має ширину 8 байтів (64 біти):Біт присутності — найважливіший біт у PTE. Коли MMU бачить P = 0, він негайно генерує сторінкову помилку (переривання 14 на x86), навіть не перевіряючи решту бітів.Це відкриває цікаву можливість: поки сторінка відсутня, ОС може використовувати решту 63 бітів для власного обліку. Linux, наприклад, зберігає в них індекс розділу підкачки та номер дискового блоку, де опинилася витіснена сторінка. Апаратне забезпечення задає механізм (перевірити біт 0; якщо нуль — помилка), а програмне — політику (використати решту бітів, щоб відстежити, куди потрапила сторінка).Біти Read/Write (біт 1) і User/Supervisor (біт 2) разом формують модель дозволів:Коли U/S = 0, сторінка доступна лише супервізору — будь-яке звернення з режиму користувача спричиняє помилку незалежно від R/W. Коли U/S = 1, сторінка доступна й користувачеві, а R/W визначає, чи дозволений також запис.Саме так ОС захищає саму себе. Код і структури даних ядра розміщені на сторінках з U/S = 0. Навіть якщо програма в режимі користувача дізнається віртуальну адресу структури ядра, MMU відмовляє в доступі апаратно — немає жодної програмної перевірки, яку можна було б обійти.Ці два біти встановлює апаратне забезпечення, а не ОС: Біт звернення (A, біт 5): встановлюється модулем MMU при будь-якому зверненні (читання або запис). Скидати його може лише ОС. Біт забруднення (D, біт 6): встановлюється модулем MMU при записі. Скидати його може лише ОС. Ці біти — основа алгоритмів заміщення сторінок. ОС періодично скидає біти A; при наступному скануванні сторінки з A = 0 — це ті, до яких нещодавно не зверталися, а отже, кандидати на витіснення. Такий підхід апроксимує LRU без потреби тримати мітку часу для кожної сторінки. Біт D повідомляє ОС, чи треба записати витіснену сторінку на диск (D = 1, ~100 мкс на SSD), чи її можна просто відкинути (D = 0, фактично безкоштовно). Ми повернемося до цих бітів у Розділі 9.Коли PS = 1 у записі каталогу сторінок, трансляція зупиняється на цьому рівні: запис безпосередньо відображає сторінку 2 МБ (на рівні PD) або сторінку 1 ГБ (на рівні PDPT). Це суттєво знижує навантаження на TLB для задач із великим обсягом пам'яті. Ми детально розглянемо великі сторінки у Розділі 6.7, а їхній вплив на TLB — у Розділі 7.6.Сторінки ядра відображаються однаково в таблиці сторінок кожного процесу. Біт Global позначає такі сторінки, щоб їхні записи у TLB не скидалися при зміні CR3. Це дозволяє уникнути витрат на повторну трансляцію адрес ядра після кожного перемикання контексту.Біт NX (No eXecute) — який Intel називає XD (eXecute Disable) — займає найстарший біт PTE. Коли NX = 1, процесор відмовляється завантажувати інструкції з цієї сторінки. ОС позначає сторінки даних (стек, купа) як NX = 1, а сторінки коду (.text) — як NX = 0, тим самим запобігаючи цілому класу вразливостей безпеки. Ми детально розглянемо наслідки для безпеки у Розділі 10.2.40-бітне поле зберігає старші біти фізичної адреси фрейму. Оскільки сторінки вирівняні по 4 КБ, молодші 12 бітів завжди дорівнюють нулю і не потребують зберігання. Це дає 2^40 сторінок × 2^12 байтів = 4 ПБ адресованої фізичної пам'яті.Щоб отримати фізичну адресу сторінки з необробленого значення PTE:physical_page_address = PTE &amp; 0x000FFFFFFFFFF000
Ця маска обнуляє біти прапорців (0–11) та біти NX і ключа захисту (52–63), залишаючи лише фізичну адресу.Припустимо, що 64-бітний PTE містить:0x800000001A3F5067
У двійковій системі (з виділенням ключових позицій бітів):Біт 63 (NX): 1 → Виконання заборонено (сторінка даних)
Біти 12-51: 0x1A3F5 → Фізичний фрейм 0x1A3F5 (фрейм 107 509)
Біт 8 (G): 0 → Не глобальний
Біт 7 (PS): 0 → Стандартна сторінка 4 КБ
Біт 6 (D): 1 → На сторінку здійснювали запис (забруднена)
Біт 5 (A): 1 → До сторінки зверталися
Біт 2 (U/S): 1 → Доступна для користувача
Біт 1 (R/W): 1 → Читання-запис
Біт 0 (P): 1 → Присутня в оперативній пам'яті
Розбір шістнадцяткового значення 0x067 (молодші 12 бітів) = 0000 0110 0111 у двійковій системі:Біт 0 (P): 1
Біт 1 (R/W): 1
Біт 2 (U/S): 1
Біт 3 (PWT): 0
Біт 4 (PCD): 0
Біт 5 (A): 1
Біт 6 (D): 1
Біт 7 (PS): 0
Цей PTE описує сторінку 4 КБ у фізичному фреймі 0x1A3F5: доступну для користувача, з дозволом на читання й запис, забруднену, до якої вже зверталися, із забороною виконання. Фізична базова адреса — 0x1A3F5 &lt;&lt; 12 = 0x1A3F5000. Якщо зміщення з віртуальної адреси становило 0x7B0, остаточна фізична адреса — 0x1A3F5000 + 0x7B0 = 0x1A3F57B0.Це повний шлях одного звернення до пам'яті: віртуальна адреса → VPN + зміщення → пошук у таблиці сторінок → PTE → виділити номер фрейму → конкатенація зі зміщенням → фізична адреса. Кожне звернення до пам'яті в програмі проходить цим маршрутом — мільярди разів на секунду, прозоро для програми, під керуванням модуля MMU.Маючи всі складові — розділення VPN/зміщення, записи таблиці сторінок, біти метаданих — складімо докупи повний алгоритм трансляції, який MMU виконує на кожне звернення до пам'яті.Щоразу, коли процесор виконує інструкцію зі зверненням до пам'яті — mov, push чи навіть саму вибірку інструкції — розгортається така послідовність дій:Крок 1: Процесор генерує віртуальну адресу — значення вказівника, вказівника стека або лічильника команд.Крок 2: Розділення на VPN і зміщення. Молодші 12 біт (для сторінок розміром 4 КБ) стають зміщенням у сторінці; решта старших біт утворює номер віртуальної сторінки (VPN).Віртуальна адреса (32-бітний приклад, сторінки 4 КБ):
| VPN (20 біт) | Зміщення (12 біт) |
| біти 31 ............. 12 | біти 11 ......... 0 |
Крок 3: Індексація в таблицю сторінок. За базовим регістром таблиці сторінок (PTBR — на x86 це CR3) MMU знаходить початок таблиці у фізичній пам'яті. VPN слугує індексом: апаратура обчислює PTBR + VPN × PTE_size і так потрапляє на відповідний запис таблиці сторінок.Крок 4: Читання PTE та перевірка біта присутності. MMU зчитує запис за цією адресою. Якщо біт присутності (біт 0 на x86) дорівнює 0, сторінки наразі немає у фізичній пам'яті. MMU порушує виняток сторінкової помилки, і керування переходить до обробника сторінкових помилок ОС (цей шлях простежимо в розділі 8). Наразі вважатимемо, що біт дорівнює 1 — сторінка в пам'яті.Крок 5: Перевірка бітів дозволів. Навіть якщо сторінка в пам'яті, доступ до неї може бути заборонений. MMU звіряє тип доступу (читання, запис або виконання) з бітами R/W, U/S та NX у PTE. Порушення — скажімо, запис у сторінку лише для читання або виконання інструкції зі сторінки з NX=1 — призводить до помилки захисту.Крок 6: Витягання номера фрейму з PTE. PTE містить номер фізичного фрейму (PFN) — фактичне місце у RAM, де зберігаються дані цієї сторінки. На x86-64 адреса фрейму розташована в бітах 51:12 PTE (40-бітна фізична адреса, вирівняна по 4 КБ).Крок 7: Формування фізичної адреси. Апаратура приєднує до номера фрейму (з PTE) зміщення сторінки (з початкової віртуальної адреси). Це реальна адреса, яка виходить на шину пам'яті:Фізична адреса = [ Номер фрейму | Зміщення ] (з PTE) (з VA — без змін)
Крок 8: Доступ до фізичної пам'яті за цією адресою. Дані зчитуються з фізичної комірки (або записуються до неї). З погляду процесора трансляція непомітна — він просто запросив дані за віртуальною адресою й одержав їх.<br><img alt="Трансляція віртуальної адреси у фізичну: розділення VPN/зміщення, пошук у таблиці сторінок та формування PA" src="assets/l12-address-translation.svg" target="_self" style="width: 650px; max-width: 100%;">Біти зміщення сторінки ніколи не транслюються. Вони переходять напряму з віртуальної адреси у фізичну. Змінюється лише VPN — на його місце стає номер фрейму. Це працює тому, що сторінки та фрейми мають однаковий розмір: байт на позиції 2 687 у віртуальній сторінці — це той самий байт на позиції 2 687 у фізичному фреймі. За фіксованого розміру сторінок зміщення просто приєднується — жодного додавання, жодної перевірки меж, саме лише склеювання бітів.Простежмо конкретний приклад, у якому видно кожен біт. Візьмімо невелику систему, щоб числа були наочними, а потім перейдімо до реальної апаратури.Параметри системи:
Віртуальний адресний простір: 16 біт (64 КБ)
Фізичний адресний простір: 15 біт (32 КБ)
Розмір сторінки: 4 КБ (2^12 = 4 096 байтів)
Зміщення сторінки: 12 біт
VPN: 16 − 12 = 4 біти (16 віртуальних сторінок)
PFN: 15 − 12 = 3 біти (8 фізичних фреймів)
Таблиця сторінок нашого процесу (зберігається у пам'яті, на неї вказує PTBR):Варто зауважити розріджену структуру: фактично відображені лише 5 із 16 віртуальних сторінок. Сторінки 3–13 утворюють порожній проміжок між купою та стеком — зменшена копія того, як виглядає кожен реальний процес.Віртуальна адреса для трансляції: 0xE2A4Крок 1: Перетворення в двійковий формат.0xE2A4 = 1110 0010 1010 0100
Крок 2: Розділення на VPN і зміщення.| VPN (4 біти) | Зміщення (12 біт) |
| 1110 | 0010 1010 0100 |
| = 14 | = 0x2A4 |
Віртуальна адреса потрапляє у віртуальну сторінку 14, на зміщення 0x2A4 (676 у десятковій системі) всередині цієї сторінки.Крок 3: Пошук VPN 14 у таблиці сторінок.З таблиці вище: VPN 14 → Присутність = 1, Фрейм # = 1.Крок 4: Перевірка біта присутності. Присутність = 1, отже сторінка в пам'яті. Чудово — сторінкова помилка не виникає.Крок 5: Витягання номера фрейму. Фрейм # = 1, у двійковій системі 001.Крок 6: Формування фізичної адреси.Фізична адреса = [ Фрейм # (3 біти) | Зміщення (12 біт) ] = [ 001 | 0010 1010 0100 ] = 001 0010 1010 0100 = 0x12A4
Крок 7: Доступ до фізичної пам'яті за адресою 0x12A4.Перевірмо, чи все тут узгоджено. Фрейм 1 починається з фізичної адреси 1 × 4 096 = 4 096 = 0x1000. Зміщення в межах сторінки — 0x2A4 = 676 байтів. Отже, фізична адреса дорівнює 0x1000 + 0x2A4 = 0x12A4. Арифметика збігається.Ось повна картина:Віртуальна адреса: 0xE2A4 ├── VPN = 14 ──→ Пошук у таблиці сторінок ──→ Фрейм = 1 └── Зміщення = 0x2A4 ──────────────────────────────────────┐ │
Фізична адреса: 0x12A4 │ ├── Базова адреса фрейму 1 = 0x1000 │ └── + Зміщення = 0x02A4 ◄────────────────────┘
Зміщення 0x2A4 однаково присутнє в обох адресах — воно жодного разу не змінилося, лише було перенесене. VPN (14) поступився місцем номеру фрейму (1). Ось і вся суть сторінкування.У цьому алгоритмі причаїлася неприємна істина. Повернімося до кроку 3: щоб зчитати PTE, апаратурі доведеться звернутися до таблиці сторінок, яка й сама зберігається у фізичній пам'яті. Це одне звернення до пам'яті. Далі на кроці 8 апаратура дістається до самих даних — друге звернення. Тож кожне окреме звернення програми до пам'яті тепер коштує двох фактичних звернень:Без сторінкування: 1 доступ до пам'яті на звернення
Зі сторінкуванням: 2 доступи до пам'яті на звернення (1 для PTE + 1 для даних)
Фактично це подвоює ефективну затримку пам'яті. Якщо одне звернення до пам'яті триває 100 нс, то зі сторінкуванням воно розтягується до 200 нс. Для процесора, здатного видавати інструкції з точністю до частки наносекунди, це катастрофа.І це ще не все. Як ми побачимо в розділі 6, реальні системи мають багаторівневі таблиці сторінок — чотири рівні на x86-64. Без жодної оптимізації кожне звернення до пам'яті вимагало б п'яти звернень: чотирьох на обхід таблиці сторінок (по одному на рівень) плюс одного до самих даних. За 100 нс на одне звернення це 500 нс на запит — уповільнення в 5 разів.І це не суто теоретичний клопіт. Без оптимізації сторінкування зробило б сучасні комп'ютери неприйнятно повільними. Рятує ситуацію невеликий, але критично важливий елемент апаратури — TLB (Буфер асоціативної трансляції) — кеш, спеціально спроєктований для зберігання нещодавніх трансляцій адрес. Детально ми розглянемо його в розділі 7. А поки що достатньо пам'ятати, що TLB зводить переважну більшість трансляцій до одного швидкого пошуку — і саме завдяки йому сторінкування лишається практичним попри властиві йому накладні витрати.Однорівнева таблиця сторінок годиться для навчальних систем, але на реальних адресних просторах вона стає непридатною.Розглянемо x86-64 з його 48-бітним віртуальним адресним простором:Кількість віртуальних сторінок = 2^48 / 2^12 = 2^36 = 68 719 476 736 сторінок
Розмір PTE = 8 байтів (64-бітні записи)
Розмір таблиці сторінок = 2^36 × 8 = 549 755 813 888 байтів = 512 ГБ на процес
512 гігабайтів на процес. Одна плоска таблиця сторінок для 48-бітного адресного простору потребувала б більше пам'яті, ніж встановлено в більшості комп'ютерів узагалі. Навіть 32-бітна система зі сторінками 4 КБ вимагає 2^20 × 4 = 4 МБ на процес — і для типового процесу, що використовує лише кілька десятків мегабайтів, понад 99% записів позначені як «відсутні».Реальні адресні простори розріджені: кілька ділянок (код, купа, стек), між якими лежать величезні порожні проміжки:0xFFFF...FFFF ┌──────────────────┐ │ Простір ядра │ (спільний, відображений ОС)
0x7FFF...FFFF ├──────────────────┤ │ │ │ Стек ↓ │ ~8 МБ ├──────────────────┤ │ │ │ (величезний │ ~99,99% адресного простору │ порожній │ │ проміжок) │ │ │ ├──────────────────┤ │ Купа ↑ │ ~10 МБ ├──────────────────┤ │ Дані │ ~1 МБ ├──────────────────┤ │ Код (Text) │ ~5 МБ
0x0000...0000 └──────────────────┘
Плоска таблиця сторінок виділяє місце для всього діапазону. Нам потрібна структура, що виділяє пам'ять лише під фактично використані ділянки, але водночас зберігає O(1)-пошук (а це виключає хеш-таблиці з найгіршими колізіями та відсортовані списки з O(log n)-доступом). Рішення — ієрархічна (багаторівнева) таблиця сторінок: дерево невеликих таблиць, кожна з яких вміщується рівно в одну сторінку, а невикористані гілки взагалі не виділяються.Замість однієї таблиці на мільйон записів розіб'ємо 20-бітний VPN на два 10-бітні індекси:32-бітна віртуальна адреса з дворівневим сторінкуванням:
| Зовнішній індекс (10 бітів) | Внутрішній індекс (10 бітів) | Зміщення (12 бітів) |
| біти 31 ............... 22 | біти 21 ............... 12 | біти 11 .......... 0|
Зовнішня таблиця (каталог сторінок): 2^10 = 1 024 записи × 4 байти = 4 КБ (одна сторінка). Кожен запис або вказує на внутрішню таблицю, або позначений як «відсутній».Кожна внутрішня таблиця: 1 024 записи × 4 байти = 4 КБ (одна сторінка). Кожен запис містить номер фрейму.Внутрішні таблиці виділяються за потребою. Якщо ділянка не відображена, внутрішня таблиця не існує — зовнішній запис просто має позначку «відсутній».
Виокремлюємо зовнішній індекс (біти 31–22). За CR3 знаходимо каталог сторінок.
Читаємо запис каталогу сторінок за [зовнішнім індексом]. Якщо «відсутній» → сторінкова помилка.
Запис дає фізичну адресу відповідної внутрішньої таблиці.
Виокремлюємо внутрішній індекс (біти 21–12). Читаємо запис внутрішньої таблиці за [внутрішнім індексом].
Якщо Present = 0 → сторінкова помилка. Інакше — беремо номер фрейму.
Формуємо адресу: [номер фрейму | зміщення].
Процес, що використовує 4 МБ коду + 4 МБ купи + 4 МБ стека (кожна частина укладається в покриття однієї внутрішньої таблиці: 2^10 сторінок × 4 КБ = 4 МБ):
1 каталог сторінок + 3 внутрішні таблиці = 16 КБ (проти 4 МБ для плоскої таблиці — економія у 256 разів).
<br><img alt="Дворівнева таблиця сторінок: зовнішній каталог сторінок із внутрішніми таблицями, які виділено лише для реально використаних ділянок" src="assets/l12-two-level-page-table.svg" target="_self" style="width: 650px; max-width: 100%;">48-бітний адресний простір вимагає більшої кількості рівнів. З 8-байтними записами та сторінками 4 КБ кожна таблиця вміщує 4 096 / 8 = 512 = 2^9 записів (9-бітний індекс). На 36 бітів, що лишаються після зміщення, припадає 36 / 9 = 4 рівні.Віртуальна адреса x86-64 (використовуються 48 бітів):
| Знак. розш.| Індекс L4 | Індекс L3 | Індекс L2 | Індекс L1 | Зміщення |
| (16 бітів)| (9 бітів) | (9 бітів) | (9 бітів) | (9 бітів) | (12 бітів)|
| біти 63-48 | біти 47-39 | біти 38-30 | біти 29-21 | біти 20-12 | біти 11-0 |
Біти 63–48 — це знакове розширення біта 47; воно утворює два допустимі діапазони, розділені неканонічним проміжком:
Простір користувача: 0x0000'0000'0000'0000 до 0x0000'7FFF'FFFF'FFFF (128 ТБ)
Простір ядра: 0xFFFF'8000'0000'0000 до 0xFFFF'FFFF'FFFF'FFFF (128 ТБ)
CR3 містить фізичну адресу таблиці PML4. Під час кожного перемикання контексту ОС завантажує в CR3 адресу PML4 нового процесу.Кожна таблиця на кожному рівні має 512 записів × 8 байтів = 4 096 байтів = одна сторінка. Завдяки такій однорідній геометрії таблиці можна виділяти та підкачувати стандартним алокатором сторінок.
Читаємо CR3 → базова адреса PML4.
Індекс L4 (біти 47–39) → читаємо PML4[L4_index]. Відсутній → сторінкова помилка. Інакше → адреса PDPT.
Індекс L3 (біти 38–30) → читаємо PDPT[L3_index]. Відсутній → сторінкова помилка. PS=1 → велика сторінка 1 ГБ. Інакше → адреса PD.
Індекс L2 (біти 29–21) → читаємо PD[L2_index]. Відсутній → сторінкова помилка. PS=1 → велика сторінка 2 МБ. Інакше → адреса PT.
Індекс L1 (біти 20–12) → читаємо PT[L1_index]. Відсутній → сторінкова помилка. Інакше — номер фрейму з бітів 51:12.
Фізична адреса: адреса фрейму | зміщення (біти 11:0 VA).
Без TLB повна трансляція коштує 4 доступи до пам'яті плюс 1 для даних — 5 загалом. За 100 нс на доступ це 500 нс на одне звернення.L4_index = (VA &gt;&gt; 39) &amp; 0x1FF; // Індекс PML4 (біти 47-39)
L3_index = (VA &gt;&gt; 30) &amp; 0x1FF; // Індекс PDPT (біти 38-30)
L2_index = (VA &gt;&gt; 21) &amp; 0x1FF; // Індекс PD (біти 29-21)
L1_index = (VA &gt;&gt; 12) &amp; 0x1FF; // Індекс PT (біти 20-12)
offset = VA &amp; 0xFFF; // Зміщення сторінки (біти 11-0)
Маска 0x1FF — це дев'ять одиничних бітів; вона виділяє рівно 9 бітів після зсуву.512 × 512 × 512 × 512 × 4 096 = 2^9 × 2^9 × 2^9 × 2^9 × 2^12 = 2^48 = 256 ТБ
Цей простір поділено на 128 ТБ користувача + 128 ТБ ядра. Фізичний адресний простір використовує біти 51:12 PTE (40 бітів) і підтримує до 2^52 байтів = 4 ПБ; сучасні процесори зазвичай реалізують 46 бітів (64 ТБ).<br><img alt="Обхід чотирирівневої таблиці сторінок x86-64: від CR3 через PML4, PDPT, PD, PT до фізичного фрейму" src="assets/l12-four-level-page-table.svg" target="_self" style="width: 650px; max-width: 100%;">Транслюємо віртуальну адресу 0x0000'0040'1234'5678 (простір користувача, біт 47 = 0).Крок 1: виокремлюємо всі індекси.L4 = (0x0000004012345678 &gt;&gt; 39) &amp; 0x1FF = 0
L3 = (0x0000004012345678 &gt;&gt; 30) &amp; 0x1FF = 64
L2 = (0x0000004012345678 &gt;&gt; 21) &amp; 0x1FF = 72
L1 = (0x0000004012345678 &gt;&gt; 12) &amp; 0x1FF = 421
Offset = 0x0000004012345678 &amp; 0xFFF = 0x678
Крок 2: обхід таблиць сторінок.Припустімо такі фізичні адреси:1. CR3 = 0x0010'1000 → Базова адреса PML4
2. PML4[0] за 0x0010'1000 + 0×8 = 0x0010'1000 → PDPT за 0x0020'2000
3. PDPT[64] за 0x0020'2000 + 64×8 = 0x0020'2200 → PD за 0x0030'3000
4. PD[72] за 0x0030'3000 + 72×8 = 0x0030'3240 → PT за 0x0040'4000
5. PT[421] за 0x0040'4000 + 421×8 = 0x0040'4D28 → Фрейм за 0x0ABC'D000
Крок 3: формуємо фізичну адресу.Фізична адреса = 0x0ABC'D000 | 0x678 = 0x0ABC'D678
Чотири доступи до пам'яті для трансляції + один для даних = 5 загалом. TLB кешує результат VPN 0x0000004012345 → PFN 0x0ABCD, тож наступні звернення до будь-якого байта в тій самій сторінці 4 КБ оминають обхід узагалі.Навіть процес, що відображає один-єдиний байт, потребує по одній таблиці на кожному рівні: 4 × 4 КБ = щонайменше 16 КБ. Типовий процес (код + купа + стек + спільні бібліотеки) використовує 1 PML4, 2–4 PDPT, 5–20 PD та 10–100 PT — приблизно 100–500 КБ. Порівняймо з плоскою таблицею для того самого 48-бітного адресного простору: 512 ГБ. Економія виникає з простого принципу: невикористані гілки дерева взагалі не виділяються.Стандартні сторінки 4 КБ добре підходять для більшості навантажень, але бази даних, віртуальні машини та наукові обчислення виграють від великих сторінок, що покривають більші ділянки одним записом TLB. Механізм: біт PS (Page Size) у записі рівня 2 або рівня 3 каже MMU зупинити обхід раніше і використати решту бітів адреси як зміщення.
Великі сторінки 2 МБ (PS встановлено на рівні 2 — PD): обхід зупиняється після 3 рівнів, зміщення — 21 біт.
Великі сторінки 1 ГБ (PS встановлено на рівні 3 — PDPT): обхід зупиняється після 2 рівнів, зміщення — 30 бітів.
VA зі сторінкою 2 МБ: | L4 (9) | L3 (9) | L2 (9) | ──── Зміщення (21 біт) ──── |
VA зі сторінкою 1 ГБ: | L4 (9) | L3 (9) | ──────── Зміщення (30 бітів) ──────── |
Основна перевага — покриття TLB:Компромісом є внутрішня фрагментація (виділення 100 КБ у сторінці 2 МБ марнує 1,9 МБ) та складність виділення (ОС має знайти неперервні фізичні ділянки розміром 2 МБ або 1 ГБ, а це стає щораз важче в міру фрагментації пам'яті). Linux пропонує Transparent Huge Pages (THP) — опортуністичне використання великих сторінок із поверненням до сторінок 4 КБ за потреби.Багаторівневі таблиці сторінок розв'язують проблему споживання пам'яті, проте породжують нову: на x86-64 кожна трансляція вимагає чотирьох послідовних читань пам'яті (PML4 → PDPT → PD → PT) перед фактичним доступом до даних. Це п'ять звернень до пам'яті на одне посилання — уповільнення у 5 разів, яке зробило б сторінкування непридатним на практиці.Розв'язок спирається на той самий принцип, що лежить в основі всієї ієрархії пам'яті, — кешування. TLB (Буфер асоціативної трансляції) зберігає нещодавно використані трансляції в невеликому швидкому апаратному кеші, тому переважна більшість звернень уникає обходу таблиці сторінок.На x86-64 із пам'яттю DDR4:
Кожен рівень таблиці сторінок потребує одного читання пам'яті: ~100 нс
Чотирирівневий обхід: 4 × 100 нс = 400 нс лише на трансляцію
Плюс 100 нс на доступ до самих даних → 500 нс загалом (проти 100 нс без сторінкування)
На частоті 4 ГГц (0,25 нс/такт) це 2 000 тактів на одне звернення до пам'яті
Обхід принципово послідовний — неможливо прочитати запис PDPT, доки запис PML4 не вкаже, де цей PDPT розташований, — а це руйнує будь-який паралелізм позачергового виконання.TLB — це спеціалізований апаратний кеш у складі MMU. На відміну від кешів даних, що зберігають вміст пам'яті, TLB зберігає трансляції адрес — відображення VPN→PFN разом із відповідними бітами дозволів і стану.Кожен запис TLB містить:
VPN (номер віртуальної сторінки) — старші біти, що ідентифікують сторінку
PFN (номер фізичного фрейму) — результат трансляції
ASID (ідентифікатор адресного простору) — визначає, якому процесу належить ця трансляція (див. підрозділ 7.5)
Біт дійсності — чи містить цей запис дійсну трансляцію
Біт забруднення — чи було здійснено запис на сторінку (дзеркалює біт забруднення PTE)
Біти дозволів — Read, Write, Execute — перевіряються під час кожного звернення
Глобальний біт — позначає трансляції, спільні для всіх адресних просторів (відображення ядра)
Чому повністю асоціативний? На відміну від кешів даних (зазвичай 8-канальних або 16-канальних множинно-асоціативних), TLB зазвичай роблять повністю асоціативним — усі записи переглядаються паралельно під час кожного пошуку. Це усуває конфліктні промахи, що надзвичайно важливо, оскільки TLB малий (32–1 024 записи), і кожен промах запускає дорогий обхід таблиці сторінок. Апаратна вартість прийнятна: TLB достатньо малий, щоб паралельні компаратори залишалися реалістичним рішенням.Розділений TLB. Сучасні процесори мають окремі TLB для інструкцій і для даних, а також уніфікований TLB другого рівня — за аналогією з розділеним кешем L1:
ITLB (TLB інструкцій): 64–128 записів. Менший, бо шаблони доступу до інструкцій послідовні й передбачувані.
DTLB (TLB даних): 64–256 записів. Більший, бо шаблони доступу до даних значно різноманітніші.
L2 STLB (уніфікований): 512–2 048 записів. Обслуговує промахи і з ITLB, і з DTLB — так само, як кеш L2 підстраховує L1i та L1d.
Наприклад, процесор Intel Coffee Lake:При кожному зверненні до пам'яті — завантаженні інструкції, читанні даних чи записі — MMU насамперед звертається до TLB.<br><img alt="Блок-схема пошуку в TLB: швидкий шлях при влученні в TLB на противагу повільному шляху при промаху TLB з обходом таблиці сторінок" src="assets/l12-tlb-lookup-flow.svg" target="_self" style="width: 650px; max-width: 100%;">Крок 1: Витягнення VPN. Процесор ділить віртуальну адресу на VPN (старші 36 бітів для сторінок 4 КБ) і зміщення в межах сторінки (молодші 12 бітів).Крок 2: Пошук у TLB. Апаратура паралельно порівнює VPN з кожним дійсним записом (повністю асоціативний пошук) і заразом перевіряє, що ASID відповідає поточному процесу.Крок 3a: Влучення TLB (швидкий шлях). Знайдено відповідний запис → витягнуто PFN → біти дозволів перевірено відповідно до типу доступу (R/W/X) та рівня привілеїв (користувач/ядро) → фізична адреса утворюється конкатенацією PFN зі зміщенням сторінки. Пошук займає 1–2 такти й конвеєризується, додаючи фактично нульову додаткову затримку.Крок 3b: Промах TLB (повільний шлях). Збіг не знайдено → апаратний обхідник таблиці сторінок зчитує CR3 і проходить усі чотири рівні. Якщо обхід успішний (сторінка присутня на кожному рівні), отримана трансляція записується в TLB, а доступ повторюється — цього разу вже з влученням.Крок 3c: Сторінкова помилка. Якщо на будь-якому рівні біт присутності = 0, апаратура генерує виняток сторінкової помилки (переривання 14). Обробник ОС завантажує сторінку з диска, виділяє нульову сторінку, ініціює копіювання при записі або завершує процес — а потім перезапускає інструкцію, яка спричинила помилку.На x86 обхід таблиці сторінок керується апаратно — він повністю прозорий для ОС, яка втручається лише у разі сторінкової помилки. Деякі архітектури (MIPS, окремі реалізації RISC-V) натомість використовують програмно-керований TLB: промах генерує виняток, і ОС обходить таблицю сторінок програмно. Апаратний підхід швидший (без накладних витрат на виняток); програмний — гнучкіший (ОС може використовувати будь-який формат таблиці сторінок).У лекції L11 ми ввели формулу AMAT для кеш-ієрархій. TLB природно вписується в цю модель — зрештою, це теж кеш. Визначимо EMAT (Ефективний середній час доступу до пам'яті):EMAT = h × (T_tlb + T_mem) + (1 - h) × (T_tlb + T_walk + T_mem)
Де:
h = коефіцієнт влучань TLB
T_tlb = час пошуку в TLB (~1 нс, конвеєризується до майже нуля при влученні)
T_mem = час доступу до основної пам'яті (~100 нс, без урахування кешів даних для спрощення)
T_walk = вартість обходу таблиці сторінок = кількість рівнів × T_mem
Це спрощується до:EMAT = T_tlb + T_mem + (1 - h) × T_walk
Останній доданок — це накладні витрати сторінкування; він прямує до нуля зі зростанням коефіцієнта влучань до 100%.Числовий приклад (h = 99%):T_tlb = 1 нс
T_mem = 100 нс
T_walk = 4 × 100 = 400 нс EMAT = 1 + 100 + (1 - 0,99) × 400 = 1 + 100 + 0,01 × 400 = 101 + 4 = 105 нс
При коефіцієнті влучань TLB 99% сторінкування додає лише 5% накладних витрат — прийнятна ціна за величезні переваги віртуальної пам'яті. Без TLB ці накладні витрати сягають 400%.Чому 99% — реалістичний показник. Програми демонструють сильну локальність:
Часова локальність: сторінка, до якої звернулися одного разу, з високою ймовірністю буде використана знову. Один запис TLB покриває цілу сторінку 4 КБ — а це сотні послідовних інструкцій або десятки звернень до даних.
Просторова локальність: сусідні звернення потрапляють на ту саму сторінку. Обхід масиву з 1 000 послідовних цілих чисел охоплює лише одну сторінку (4 КБ / 4 байти = 1 024 int на сторінку).
Робочий набір: типова програма у кожен момент часу активно використовує лише кілька сторінок. Навіть 64-записний DTLB покриває робочий набір у 256 КБ.
Реальні навантаження стабільно демонструють коефіцієнт влучань TLB 99–99,9%. Промахи в межах 1% виникають переважно через обходи великих структур даних і виклики функцій із віддалених сторінок коду.Коли продуктивність знижується. Випадковий доступ до великої пам'яті (наприклад, пошук у багатогігабайтній хеш-таблиці, де кожне звернення потрапляє на іншу сторінку), часті контекстні перемикання без підтримки ASID та великий обсяг коду — усе це може знизити коефіцієнт влучань значно нижче 99%, перетворюючи вартість обходу таблиці сторінок на реальне «вузьке місце» — і мотивуючи використання великих сторінок (підрозділ 7.6).Коли ОС перемикається з процесу A на процес B, вона записує адресу PML4 процесу B у CR3. Проте трансляції процесу A залишаються в TLB — використовувати їх для процесу B було б порушенням і безпеки, і коректності, адже віртуальна сторінка 0x1000 процесу B відображається на інший фізичний фрейм, ніж та сама сторінка процесу A.Рішення 1: Скидання TLB під час контекстного перемикання. Ранні процесори x86 інвалідували всі записи TLB при кожній зміні CR3. Це коректно, але дорого: холодний TLB означає промах на кожному зверненні, доки TLB не «прогріється». Якщо перша мілісекунда торкається 200 сторінок, штраф холодного старту становить 200 × 400 нс = 80 мкс — 8% типового кванта часу. На системі з тисячами перемикань на секунду це накопичується у відчутні втрати.Рішення 2: Мітки ASID (PCID на x86-64). Кожен запис TLB позначається 12-бітним PCID (ідентифікатором контексту процесу). Під час пошуку в TLB апаратура перевіряє збіг і VPN, і PCID — записи різних процесів безпечно співіснують, тому скидати TLB при контекстному перемиканні не потрібно. Коли ОС повертається до процесу A, його записи TLB можуть досі бути присутніми, що повністю усуває штраф холодного старту. При 4 096 можливих PCID повторне використання трапляється рідко; якщо ж ОС все ж повторно використовує PCID, вона попередньо скидає записи з цим ідентифікатором.Глобальні сторінки. Відображення ядра ідентичні для всіх процесів. Глобальний біт (G) у PTE позначає відповідні записи TLB як несприйнятливі до запису CR3 і зміни PCID, завдяки чому код і дані ядра не потребують повторної трансляції.Примітка: вразливість Meltdown (2018) змусила впровадити ізоляцію таблиці сторінок ядра (KPTI), яка прибирає більшість сторінок ядра з таблиць сторінок користувацького простору, що зменшує корисність глобальних сторінок. Деталі — у розділі 10.Ефективність TLB визначається покриттям TLB — загальним обсягом віртуальної пам'яті, який він здатен охопити:Покриття TLB = Кількість записів TLB × Розмір сторінки
При сторінках 4 КБ 64-записний DTLB покриває лише 256 КБ. Базі даних із буферним пулом 64 ГБ знадобилися б мільйони записів, щоб уникати промахів. Зі сторінками 2 МБ той самий TLB покриває вже 128 МБ (покращення у 512 разів). Менша кількість рівнів обходу також зменшує штраф за промах: 300 нс для сторінок 2 МБ проти 400 нс для 4 КБ.Великі сторінки широко застосовують у базах даних (PostgreSQL, Oracle), віртуальних машинах (KVM, VMware) і навантаженнях HPC/ML. У Linux великі сторінки доступні через hugetlbfs (явне резервування під час завантаження) або через Transparent Huge Pages (THP), коли ядро автоматично об'єднує суміжні ділянки по 4 КБ у великі сторінки 2 МБ без змін у прикладних програмах.Зв'язок із лабораторною P12. У лабораторній роботі ви вимірюватимете коефіцієнт промахів TLB за допомогою perf stat (dTLB-load-misses, iTLB-load-misses) і порівнюватимете затримку для сторінок 4 КБ та 2 МБ на послідовних і випадкових шаблонах доступу.У попередніх розділах ми простежили «щасливий шлях» трансляції віртуальної адреси: TLB влучає, обхід таблиці сторінок успішний, номер фізичного фрейму знайдено. А що відбувається, коли цільова сторінка відсутня у фізичній пам'яті?Тут у гру вступає сторінкова помилка — подія, яка з'єднує обіцянку віртуальної пам'яті про необмежений адресний простір із реальністю обмеженої фізичної RAM. Сторінкові помилки не є помилками у звичному розумінні; більшість із них — невід'ємна частина самого задуму системи.Згадаймо біт присутності (P) у PTE з розділу 4. Коли MMU транслює віртуальну адресу й виявляє, що Present = 0, сторінка відсутня у RAM, і трансляція не може бути завершена. Апаратура реагує чіткою послідовністю дій:
MMU зупиняє поточну інструкцію просто посеред її виконання.
Процесор породжує виняток #14 (сторінкова помилка x86) — синхронний, точний виняток.
Віртуальна адреса, на якій сталася помилка, зберігається в регістрі CR2, щоб ОС знала, до якої саме адреси йшло звернення.
Процесор перемикається в режим ядра та передає керування обробнику сторінкових помилок, зареєстрованому в IDT.
Обробник помилок ОС аналізує адресу, на якій сталася помилка, та карту пам'яті процесу, щоб віднести помилку до однієї з трьох категорій.Мінорна сторінкова помилка (м'яка помилка). Сторінка вже перебуває у фізичній пам'яті, проте не відображена в таблиці сторінок цього процесу — наприклад, це спільна бібліотека, завантажена іншим процесом, або сторінка, що досі лежить у сторінковому кеші ядра. Розв'язання дешеве: ОС оновлює PTE так, щоб він вказував на наявний фрейм, та встановлює Present = 1. Дискового введення-виведення не потрібно; вартість — кілька мікросекунд.Мажорна сторінкова помилка (жорстка помилка). Сторінки взагалі немає у фізичній пам'яті, тож її доведеться зчитувати з диска — із простору підкачки або з файлу виконуваної програми. SSD забезпечує випадкове читання 4 КБ за ~100 мкс; жорсткий диск HDD — за 8–10 мс, що при частоті 4 ГГц відповідає приблизно 32 мільйонам тактів. Процес, на якому сталася помилка, блокується, а ОС на час очікування введення-виведення виконує контекстне перемикання на інший процес. Мінімізація мажорних помилок — це, власне, і є вся суть алгоритмів заміщення сторінок (розділ 9).Недійсна помилка. Адреса, на якій сталася помилка, не належить до жодної дійсної ділянки віртуального адресного простору процесу — це розіменування нульового вказівника, вихід за межі масиву або спроба виконати дані як код. ОС надсилає SIGSEGV (помилку сегментації) у Linux або «Access Violation» у Windows, зазвичай завершуючи процес із дампом ядра.Коли керування переходить до обробника сторінкових помилок, він виконує таку послідовність. Розгляньмо повний потік для мажорної помилки — найскладнішого випадку:Крок 1: збереження стану та читання CR2. Ядро зберігає всі регістри процесора й лічильник програми, а потім витягує з CR2 віртуальну адресу помилки, щоб обчислити номер віртуальної сторінки.Крок 2: перевірка адреси. Обробник звертається до карти пам'яті процесу (у Linux вона доступна через /proc/&lt;pid&gt;/maps). Якщо адреса лежить за межами всіх дійсних ділянок або порушує дозволи ділянки (наприклад, запис у код, доступний лише для читання), обробник надсилає SIGSEGV, і процес завершується.Крок 3: пошук вільного фізичного фрейму. ОС бере фрейм зі списку вільних фреймів. Якщо список порожній або опустився нижче порогу низького рівня, алгоритм заміщення сторінок (розділ 9) обирає сторінку-жертву для витіснення.Крок 4: витіснення за потреби. Якщо в обраної жертви встановлений біт забруднення (тобто сторінку модифікували з моменту завантаження), ОС спершу записує її назад на диск, а вже потім повторно використовує фрейм. Цей зворотний запис додає до вартості помилки ще одну дискову операцію.Крок 5: завантаження потрібної сторінки. ОС ініціює читання з диска, щоб через DMA завантажити сторінку 4 КБ у вибраний фрейм. На час цієї передачі процес, на якому сталася помилка, заблокований, а на процесорі планувальник запускає інший процес.Крок 6: оновлення PTE та перезапуск. Коли введення-виведення завершене, ОС записує новий номер фрейму в PTE, встановлює Present = 1, скидає біт забруднення, виставляє біти дозволів та інвалідує можливий застарілий запис TLB (через INVLPG на x86). Збережені регістри відновлюються, і інструкція, на якій сталася помилка, виконується повторно — цього разу вже успішно. З погляду програми, ця інструкція просто виконувалася надзвичайно довго.<br><img alt="Блок-схема обробки сторінкової помилки: виняток, читання CR2, перевірка адреси, виділення фрейму, дискове В/В, оновлення PTE, перезапуск інструкції" src="assets/l12-page-fault-handling.svg" target="_self" style="width: 650px; max-width: 100%;">Кроки 4 і 5 пояснюють, чому мажорні помилки такі руйнівні: до двох дискових операцій, якщо жертва забруднена. На HDD це може коштувати ~16 мс — приблизно 64 мільйони тактів при 4 ГГц.Сучасні операційні системи навмисно запускають процес із нулем сторінок у фізичній пам'яті. ОС будує віртуальний адресний простір (ділянки коду, даних, купи, стеку), однак кожен PTE починається з Present = 0. Перша інструкція породжує сторінкову помилку, яка й підвантажує відповідну сторінку коду; наступне звернення до даних породжує ще одну, і так далі.Це і є сторінкування за запитом: сторінки підтягуються лише тоді, коли до них фактично звертаються. Переваги такі:
Швидший запуск — великий виконуваний файл може важити сотні мегабайтів, хоча типове виконання торкається лише частини з них. Завантаження всього наперед — це марнування часу на сторінки, які можуть ніколи й не знадобитися.
Менший тиск на пам'ять — сторінки, до яких так і не зверталися, не займають фізичних фреймів, і це дозволяє десяткам процесів із багатогігабайтними віртуальними адресними просторами мирно співіснувати в обмеженій RAM.
Більшість ОС оптимізують це за допомогою попереднього завантаження (або кластерного сторінкування): коли на сторінці N виникає помилка, ядро принагідно завантажує й сторінки N+1, N+2 тощо, використовуючи просторову локальність для запобігання майбутнім помилкам.Копіювання при записі робить fork() значно ефективнішим. Замість того, щоб дублювати всю фізичну пам'ять батьківського процесу, ОС відображає і батьківський, і дочірній процеси на ті самі фізичні фрейми та позначає всі спільні сторінки лише для читання в обох таблицях PTE. Щойно котрийсь із процесів намагається писати в спільну сторінку, такий запис породжує сторінкову помилку. Обробник розпізнає її як COW-помилку, виділяє новий фрейм, копіює вміст сторінки та віддає процесу, що пише, його власну приватну копію з повними дозволами на читання й запис.Ключова ідея така: якщо дочірній процес одразу після fork() викликає exec() — а це переважний патерн — жодна сторінка взагалі не копіюється. В інших випадках дублюються лише ті сторінки, в які виконується запис; переважна більшість (код, дані лише для читання, немодифікована купа) залишається спільною.Споріднена оптимізація — заповнення нулями за запитом: щойно виділені сторінки купи та стеку відображаються на єдину спільну нульову сторінку (лише для читання). Тільки коли процес пише в таку сторінку, ОС виділяє приватний фрейм. Це гарантує, що нова пам'ять зчитується як нулі (вимога безпеки), і водночас уникає завчасного виділення фреймів, які можуть так ніколи й не знадобитися.Ефективний час доступу (EAT) дає кількісну оцінку вартості сторінкових помилок:EAT = (1 - p) × memory_access_time + p × page_fault_service_time
де p — ймовірність сторінкової помилки, memory_access_time ≈ 200 нс, а page_fault_service_time ≈ 8 мс (HDD).Навіть одна помилка на тисячу звернень дає уповільнення у 41 раз. Щоб утримати деградацію нижче 10%, частота помилок має бути менше приблизно однієї на мільйон звернень. Саме тому операційні системи вкладають стільки зусиль у алгоритми заміщення сторінок — від них залежить, чи працює система на швидкості пам'яті, чи на швидкості диска.На SSD (~100 мкс на обслуговування помилки) частота помилок 1 на 1 000 дає ~300 нс EAT (уповільнення в 1,5 разу) — значно прийнятніше, проте фундаментальний урок той самий: сторінкові помилки мають бути рідкісними, інакше сторінкування не буде ефективним.Коли список вільних фреймів порожній, а треба завантажити нову сторінку, ОС має вирішити, яку сторінку витіснити. Це — задача заміщення сторінок, і обраний алгоритм безпосередньо визначає, скільки сторінкових помилок станеться в майбутньому. Невдалий вибір — витіснення сторінки, потрібної вже під час наступного звернення — спричиняє ще одну негайну помилку; вдалий же вибір запобігає помилкам і дає системі змогу працювати зі швидкістю пам'яті.Система має N фізичних фреймів. Процес породжує послідовність звернень до віртуальних сторінок — рядок звернень (reference string). Звернення до сторінок, що вже перебувають у фреймах, — це влучення; звернення до відсутніх сторінок — сторінкові помилки, які потребують витіснення, якщо всі N фреймів зайняті. Якщо витіснена сторінка забруднена (модифікована), її перед повторним використанням фрейму треба записати назад на диск, а це приблизно подвоює вартість помилки.Наш робочий приклад. 3 фізичні фрейми (спочатку порожні), 20 звернень до сторінок:Рядок звернень: 7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2, 1, 2, 0, 1, 7, 0, 1
Витісняємо сторінку, яка перебуває в пам'яті найдовше. Реалізація — звичайна черга FIFO.Трасування (3 фрейми):Зверн: 7 0 1 2 0 3 0 4 2 3 0 3 2 1 2 0 1 7 0 1
Ф1: [7] [7] [7] [2] [2] [2] [2] [4] [4] [4] [0] [0] [0] [0] [0] [0] [0] [7] [7] [7]
Ф2: - [0] [0] [0] [0] [3] [3] [3] [2] [2] [2] [2] [2] [1] [1] [1] [1] [1] [0] [0]
Ф3: - - [1] [1] [1] [1] [0] [0] [0] [3] [3] [3] [3] [3] [2] [2] [2] [2] [2] [1]
Вл.? П В П П П П П П В В П П В В П П П
Результат FIFO: 15 сторінкових помилок з 20 звернень.FIFO реалізується тривіально, проте повністю ігнорує характер звернень: навіть часто використовувана сторінка буде витіснена, щойно дійде до голови черги.Аномалія Беладі. FIFO має контрінтуїтивну властивість: збільшення кількості фреймів іноді збільшує кількість помилок. Скажімо, для рядка 1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5 FIFO з 3 фреймами дає 9 помилок, а з 4 фреймами — 10. Причина в тому, що FIFO не є стековим алгоритмом: множина сторінок у N фреймах не обов'язково є підмножиною сторінок у N+1 фреймах. LRU та OPT, навпаки, — стекові алгоритми, і доведено, що ця аномалія для них неможлива.Витісняємо сторінку, яка не використовуватиметься найдовше в майбутньому. Ласло Беладі довів, що такий алгоритм дає мінімально можливу кількість помилок.Приклад рішення про витіснення з нашого трасування (3 фрейми).Надходить сторінка 2 (звернення 4), а фрейми містять {7, 0, 1}. Наступне використання сторінки 7 припадає на позицію 18 (далеко), сторінки 0 — на позицію 5, сторінки 1 — на позицію 14. OPT витісняє сторінку 7 — ту, що не потрібна найдовше.Результат OPT: 9 сторінкових помилок — суттєво менше, ніж 15 у FIFO.OPT неможливо реалізувати в реальній системі, оскільки він потребує знання майбутніх звернень. Натомість він слугує теоретичним еталоном, з яким порівнюють практичні алгоритми.Бачити майбутнє ми не можемо, тож за передбачення беремо нещодавнє минуле: витісняємо сторінку, до якої не зверталися найдовше. Обґрунтування — часова локальність: сторінка, якою давно не користувалися, навряд чи знадобиться найближчим часом.Ключові рішення про витіснення з нашого трасування (3 фрейми):
Звернення 2 (позиція 4): фрейми містять {7, 0, 1}. Останнє використання сторінки 7 — у момент 1, сторінки 0 — у момент 2, сторінки 1 — у момент 3. LRU витісняє сторінку 7 як найдавніше використану.
Звернення 3 (позиція 6): фрейми містять {2, 0, 1}. Останнє використання сторінки 1 — у момент 3, сторінки 0 — у момент 5, сторінки 2 — у момент 4. LRU витісняє сторінку 1.
Результат LRU: 12 сторінкових помилок — краще за FIFO (15), хоча й гірше, ніж OPT (9).LRU — стековий алгоритм (отже, не підвладний аномалії Беладі), але реалізувати справжній LRU на практиці нереально: структуру даних довелося б оновлювати при кожному зверненні до пам'яті (мільярди разів на секунду), а не лише при помилках. Два класичні підходи — на основі лічильників (записувати мітку часу в кожен PTE при кожному зверненні) та на основі стеку (підтримувати двозв'язний список, який переупорядковується при кожному зверненні) — обидва потребують апаратної підтримки, якої сучасні процесори не надають. Тож на практиці операційні системи використовують апроксимації LRU.Алгоритм годинника — найпоширеніша апроксимація LRU; його варіанти застосовують Linux, FreeBSD та Windows. Апаратної підтримки йому треба зовсім небагато — лише один біт на сторінку, так званий біт звернення (біт R), який MMU автоматично встановлює при кожному зверненні до сторінки.Як це працює. Усі N фреймів розташовано в циклічному буфері з єдиним вказівником — стрілкою годинника. Коли треба витіснити сторінку:
Дивимося на сторінку під стрілкою годинника.
Якщо біт звернення дорівнює 0 — витісняємо цю сторінку, завантажуємо на її місце нову і пересуваємо стрілку.
Якщо біт звернення дорівнює 1 — скидаємо його в 0 (даємо сторінці «другу можливість»), пересуваємо стрілку та повторюємо з кроку 1.
Стрілка рухається по колу, скидаючи біти звернення, доки не натрапить на сторінку з бітом звернення 0. Сторінки, до яких зверталися після минулого проходу, залишаються; ті ж, що не використовувалися протягом повного обертання, витісняються. По суті це апроксимація LRU, де «вікно нещодавності» дорівнює одному обертанню стрілки.<br><img alt="Алгоритм годинника (друга можливість) заміщення сторінок із циклічним буфером фреймів та стрілкою годинника" src="assets/l12-clock-algorithm.svg" target="_self" style="width: 650px; max-width: 100%;">Варіант із двома стрілками. Деякі ОС використовують дві стрілки: стрілка скидання (передня) скидає біти звернення, а стрілка витіснення (задня) витісняє сторінки з нульовим бітом звернення. Відстань між стрілками задає вікно нещодавності, і ОС може її змінювати залежно від навантаження на пам'ять.Покращена друга можливість. Ця версія додатково враховує ще й біт забруднення, тому сторінки розбиваються на чотири пріоритетні класи витіснення:Спочатку алгоритм шукає сторінку класу 0; якщо після повного обертання жодної не знайдено — переходить до класу 1, далі до 2, далі до 3, принагідно скидаючи біти звернення.Кількість помилок для нашого рядка звернень (3 фрейми, 20 звернень):OPT задає теоретичну нижню межу, LRU до неї наближається, а FIFO платить чималий штраф за ігнорування того, коли саме сторінка використовувалася востаннє. На практиці кількість помилок алгоритму годинника лежить між показниками LRU та FIFO — здебільшого значно ближче до LRU. Дослідження показують: варіант із двома стрілками часто не поступається справжньому LRU, а подеколи навіть дещо його перевершує, бо періодичне скидання бітів звернення забезпечує своєрідне «забування», корисне при зміні характеру звернень.Зв'язок з Л11. Це ті самі алгоритми заміщення, які ми розглядали в Л11 у контексті кешу, лише застосовані на іншому рівні ієрархії. Ключова відмінність така: заміщення в кеші виконується апаратно за наносекунди, а заміщення сторінок — програмно, силами операційної системи, яка має в запасі мікросекунди. Саме цей запас часу дає змогу застосовувати складніші апроксимації, як-от покращений алгоритм годинника з двома стрілками.Віртуальна пам'ять також є основним механізмом забезпечення захисту пам'яті — апаратної гарантії того, що один процес не може читати, записувати або виконувати пам'ять іншого процесу. Захист реалізується на рівні сторінок: кожен запис таблиці сторінок (PTE) містить біти дозволів, які MMU перевіряє під час кожного звернення до пам'яті. Порушення — запис у сторінку лише для читання, читання сторінки ядра з режиму користувача, спроба виконати інструкції зі сторінки без дозволу на виконання — спричиняє сторінкову помилку ще до завершення доступу. Це апаратний бар'єр, а не програмна домовленість.На x86-64 три найважливіші біти дозволів:Біт 1 — Read/Write (R/W). Коли 0, сторінка доступна лише для читання; коли 1 — для читання і запису. Будь-який запис у сторінку лише для читання спричиняє помилку порушення захисту. Саме цей біт робить стандартну карту пам'яті програми обов'язковою:Якщо помилка або експлойт намагається перезаписати інструкції .text, MMU блокує запис, а процес отримує SIGSEGV. Самомодифікований код стає можливим лише тоді, коли програма явно запитує пам'ять з правами на запис і виконання, — а сучасні системи за замовчуванням у цьому відмовляють.Біт 2 — User/Supervisor (U/S). Коли 1, сторінка доступна з режиму користувача (Ring 3); коли 0 — доступ має лише режим ядра (Ring 0). Ядро відображене у верхній частині віртуального адресного простору кожного процесу, щоб системні виклики та переривання не потребували зміни таблиці сторінок, — але U/S = 0 на кожній сторінці ядра блокує доступ із режиму користувача. Без цього біта будь-яка програма могла б читати ключі шифрування ядра, таблиці сторінок інших процесів та код ядра.Разом R/W та U/S створюють 2-бітну матрицю захисту:Ядро завжди має принаймні стільки ж прав доступу, як і режим користувача, — апаратура повністю довіряє ядру.<br><img alt="64-бітна структура запису таблиці сторінок x86-64 з позначеними бітами дозволів, стану та NX" src="assets/l12-page-table-entry.svg" target="_self" style="width: 650px; max-width: 100%;">Протягом десятиліть таблиці сторінок x86 не могли розрізняти «доступну для читання» та «виконувану» сторінку: якщо процесор міг прочитати сторінку, він міг виконувати інструкції з неї. Це уможливлювало класичний експлойт переповнення буфера: зловмисник переповнює буфер на стеку спеціально створеним машинним кодом (shellcode), перезаписує адресу повернення так, щоб вона вказувала на буфер, і процесор виконує впроваджений код після повернення з функції. Атака працює тому, що стек одночасно доступний для запису (для локальних змінних) і для виконання (немає способу заборонити вибірку інструкцій).Біт 63 — NX (No Execute), також відомий як XD (Execute Disable) в Intel та XN в ARM. Коли його встановлено, процесор не може робити вибірку інструкцій з цієї сторінки — будь-яка спроба спричиняє сторінкову помилку. Завдяки NX операційна система позначає сторінки стеку та купи як невиконувані. Зловмисник усе ще може записати shellcode у стек, але виконання зазнає невдачі, бо MMU блокує вибірку інструкцій.Так реалізується політика W^X (запис XOR виконання): жодна сторінка не є одночасно доступною для запису та виконання:
Сторінки .text: виконувані, лише для читання (NX = 0, R/W = 0)
Сторінки .data, .bss, купи, стеку: доступні для запису, невиконувані (NX = 1, R/W = 1)
Зловмисник, який має змогу записувати в пам'ять, не може виконати записане; зловмисник, який має змогу перенаправити виконання, може перейти лише до наявного коду, а не до впровадженого. Сьогодні W^X є стандартом за замовчуванням в усіх основних операційних системах.Біт NX блокує впровадження shellcode, але досвідчені зловмисники можуть застосувати ROP (Return-Oriented Programming): зчеплення коротких послідовностей наявного коду («гаджетів») у бібліотеках на кшталт libc, кожна з яких завершується інструкцією ret, щоб виконувати довільні обчислення без впровадження нового коду. Щоб ROP працював, зловмисник мусить знати точні віртуальні адреси цих гаджетів.ASLR протидіє цьому, рандомізуючи базові адреси ключових областей пам'яті під час кожного запуску процесу — самого виконуваного файлу (PIE), стеку, купи та кожної розділюваної бібліотеки. Без ASLR та сама програма завжди завантажує libc за однією й тією самою адресою. Із ASLR кожне виконання перемішує розташування, тож жорстко закодовані адреси гаджетів під час наступного запуску вже не спрацюють.На сучасному 64-бітному Linux ASLR забезпечує значну ентропію:За 28 бітів ентропії зловмисник має шанс 1 на 268 мільйонів для кожної спроби, причому кожна невдала спроба аварійно завершує програму (спричиняючи сторінкову помилку за невідображеною адресою) і тим самим перезапускає рандомізацію.Студенти спостерігатимуть ASLR безпосередньо в лабораторній роботі P12: запускаючи ту саму програму кілька разів і переглядаючи /proc/[pid]/maps, вони побачать, що кожен запуск дає різні базові адреси стеку, купи та розділюваних бібліотек.Біт U/S десятиліттями вважався непорушним — доки вразливість Meltdown (2018, CVE-2017-5754) не показала, що спекулятивне виконання на більшості процесорів Intel може призводити до витоку даних ядра через кешеві побічні канали, обходячи захист U/S. Програмне пом'якшення — KPTI — підтримує два набори таблиць сторінок для кожного процесу: таблиці режиму користувача містять лише сторінки користувача та невеликий заглушковий код ядра для обробки системних викликів, тоді як повне відображення ядра існує лише в таблицях режиму ядра, завантажених у Ring 0. Завдяки підтримці PCID (ідентифікатор контексту процесу), яка дає змогу уникати повного скидання TLB під час перемикання CR3, накладні витрати зазвичай становлять 1–5%.Ми розглянули кожен компонент віртуальної пам'яті окремо. Тепер простежимо одне звернення до пам'яті від початку до кінця — поєднавши L11 (ієрархія кешу) з L12 (віртуальна пам'ять), щоб побачити, як обидві системи спрацьовують одночасно при кожному доступі.Запущена програма виконує інструкцію:mov rax, [0x7FFE12345678]
Процесор має прочитати 8 байтів за віртуальною адресою 0x7FFE'1234'5678 (типова адреса стеку в Linux із ASLR) у регістр rax. Простежимо кожен крок.Блок завантаження/збереження отримує 48-бітну канонічну віртуальну адресу та розкладає її на чотири 9-бітні індекси плюс 12-бітне зміщення:Virtual address: 0x7FFE'1234'5678 Decomposition:
┌───────────┬───────────┬───────────┬───────────┬──────────────┐
│ PML4 idx │ PDPT idx │ PD idx │ PT idx │ Offset │
│ 9 bits │ 9 bits │ 9 bits │ 9 bits │ 12 bits │
├───────────┼───────────┼───────────┼───────────┼──────────────┤
│ 0xFF (255)│ 0x1F8(504)│ 0x091(145)│ 0x1A5(421)│ 0x678 (1656) │
└───────────┴───────────┴───────────┴───────────┴──────────────┘
Зміщення проходить через трансляцію без змін; чотири індекси визначають шлях ієрархією таблиці сторінок.MMU виділяє номер віртуальної сторінки (VPN = 0x7FFE12345, старші 36 бітів) і шукає його в TLB.Влучення TLB (~99% звернень): TLB повертає номер фізичного фрейму (PFN), наприклад 0x1A3B7C. Фізична адреса — це PFN + зміщення = 0x1A3B7C678. Вартість трансляції: ~1 такт. Переходимо до Кроку 6.Промах TLB (~1%): VPN відсутній у кеші. MMU має виконати повний обхід чотирирівневої таблиці сторінок.Апаратний обхідник таблиці сторінок зчитує CR3 (який містить фізичну адресу PML4) і послідовно проходить чотири рівні, використовуючи на кожному з них один 9-бітний індекс:
PML4: зчитуємо запис 255 із таблиці PML4 → отримуємо фізичну адресу PDPT
PDPT: зчитуємо запис 504 → отримуємо фізичну адресу PD
PD: зчитуємо запис 145 → отримуємо фізичну адресу PT
PT: зчитуємо запис 421 → отримуємо кінцевий PTE з номером фізичного фрейму
Кожне зчитування — це звернення до пам'яті, яке проходить через ієрархію кешу. Записи верхніх рівнів (PML4, PDPT) інтенсивно перевикористовуються й майже завжди лежать у кеші. У найкращому випадку обхід триває ~4–28 нс; у найгіршому (усі чотири промахи до DRAM) — близько 400 нс.Обхідник зчитує кінцевий PTE і перевіряє біт присутності: Present = 0 — Сторінкова помилка. Сторінка відсутня у фізичній пам'яті. Обробник помилок ОС встановлює причину: мажорна відмова сторінки потребує зчитування сторінки з диска (SSD ~150 мкс, HDD ~10 мс); мінорна відмова сторінки (сторінкування за запитом або копіювання при записі) оновлює PTE без дискового введення-виведення (~1–10 мкс). Після обробки інструкція виконується повторно. Present = 1 — Перевірка прав доступу. MMU перевіряє біти U/S, R/W та NX відповідно до типу доступу. У нашому прикладі — читання даних у режимі користувача — біт U/S має дорівнювати 1 (і для сторінок стеку так і є), а біти R/W та NX при читанні не перевіряються. Будь-яке порушення спричинило б помилку захисту (SIGSEGV). MMU зберігає нову трансляцію VPN → PFN у TLB (витісняючи старий запис за допомогою псевдо-LRU, якщо буфер переповнений) та встановлює біт звернення в PTE. Далі будує фізичну адресу:Physical address = PFN || Offset = 0x1A3B7C || 0x678 = 0x1A3B7C678
Фізична адреса входить в ієрархію кешу з L11:
Влучання в L1D (~1 нс): дані передаються процесору. Загальний час із влученням TLB: ~2 нс.
Промах L1 → влучання в L2 (~7 нс): рядок завантажується в L1, дані передаються процесору.
Промах L2 → влучання в L3 (~30–40 нс): рядок завантажується в L2 та L1.
Промах L3 → DRAM (~100 нс): контролер пам'яті дістає 64-байтний рядок кешу з DRAM, який проходить через L3 → L2 → L1 → процесор.
8 байтів за фізичною адресою 0x1A3B7C678 надходять у регістр rax. Інструкція mov завершується, і конвеєр рухається далі.Одна й та сама операція «прочитати 8 байтів» охоплює діапазон 5 000 000:1:Саме цей величезний діапазон і пояснює, чому існує весь стек оптимізацій: TLB усуває обходи таблиці сторінок для понад 99% звернень, багаторівневі таблиці зводять обхід до чотирьох зчитувань, ієрархія кешу перетворює 100-наносекундні звернення до DRAM на влучання за 1–7 нс, а алгоритми заміщення мінімізують дискове введення-виведення.<br><img alt="Повний шлях звернення до пам'яті: віртуальна адреса через TLB, обхід таблиці сторінок, обробник сторінкових помилок, фізична адреса та ієрархія кешу" src="assets/l12-full-memory-access-path.svg" target="_self" style="width: 650px; max-width: 100%;">Цей повний шлях — від віртуальної адреси до фізичних даних — поєднує три домени. Апаратне забезпечення (MMU, TLB, кеші) надає механізми швидкої трансляції та кешування при кожному зверненні. Операційна система задає політики — таблиці сторінок, обробку помилок, алгоритми заміщення, біти захисту. А поведінка застосунку визначає, наскільки добре ці оптимізації працюють. Програма з гарною локальністю має високі коефіцієнти влучень у TLB та кеш; програма ж, що хаотично стрибає великим адресним простором, спричиняє пробуксовування обох і в мільйони разів частіше платить штрафи найгіршого випадку.У цій лекції ми розглянули механізми, що забезпечують роботу віртуальної пам'яті: від базової концепції сторінкування до багаторівневих таблиць сторінок, прискорення через TLB, обробки помилок і захисту пам'яті. Саме завдяки цим механізмам сучасні процесори надають кожному процесу власний, безпечний та ефективний вигляд пам'яті. Віртуальна пам'ять дає три речі одразу: ізоляцію процесів (один процес не може звернутися до пам'яті іншого), абстракцію адресного простору (кожен процес бачить однорідну віртуальну розкладку) та ефективне використання пам'яті (лише активні сторінки займають фізичну оперативну пам'ять). Сторінкування розділяє пам'ять на фрагменти фіксованого розміру: віртуальні сторінки відображаються на фізичні фрейми через таблиці сторінок. Зміщення всередині сторінки проходить трансляцію без змін — замінюється лише номер сторінки на номер фрейму. Багаторівневі таблиці сторінок роблять розріджені адресні простори практичними: x86-64 використовує чотирирівневу ієрархію (PML4 → PDPT → PD → PT) з 9-бітними індексами на кожному рівні. Виділяються лише таблиці для тих діапазонів віртуальних адрес, що реально використовуються, а це заощаджує величезні обсяги пам'яті порівняно з плоскою таблицею сторінок. TLB робить сторінкування швидким: без TLB кожне звернення до пам'яті вимагало б чотирьох додаткових звернень для обходу таблиці сторінок. За коефіцієнта влучень понад 99 % TLB зменшує середні накладні витрати майже до нуля — і саме це робить віртуальну пам'ять практичною. Сторінкові помилки — це механізм сторінкування за запитом: мінорні відмови сторінки швидкі (без дискового введення-виведення), мажорні відмови катастрофічно повільні (~8 мс). Критично важливо тримати частоту сторінкових помилок астрономічно низькою — навіть одна помилка на тисячу звернень спричиняє сорокакратне уповільнення. Алгоритми заміщення сторінок визначають політику витіснення: FIFO простий, але страждає від аномалії Беладі. LRU теоретично хороший, але дорогий у реалізації. Алгоритм годинника дає практичне, дружнє до апаратури наближення LRU — саме його і застосовують реальні операційні системи. Захист пам'яті забезпечується на рівні сторінок: біти R/W, U/S та NX у кожному PTE визначають, що дозволено робити з кожною сторінкою. Біт NX запобігає виконанню коду зі сторінок даних і так блокує цілий клас експлойтів безпеки. ASLR рандомізує розташування пам'яті та ускладнює атаки. Повний шлях звернення до пам'яті охоплює TLB → таблиці сторінок → кеші → оперативну пам'ять: розуміння цього цілісного шляху, що поєднує ієрархію кешу з L11 та віртуальну пам'ять з L12, — одна з найважливіших ментальних моделей в архітектурі комп'ютерних систем. Ми вже розглянули, як комп'ютери зберігають цілі числа, адреси та інструкції і звертаються до них — і як система пам'яті це все транслює, кешує та захищає. Але залишився один критично важливий тип даних, якого ми ще не торкалися: дійсні числа.Як комп'ютер представляє 3.14159? Що відбувається, коли обчислення 0.1 + 0.2 дає 0.30000000000000004? Чому фінансові застосунки іноді взагалі уникають чисел із рухомою комою? Лекція 13 присвячена стандарту IEEE 754 для чисел із рухомою комою — універсальному формату, який кожен сучасний процесор використовує для представлення десяткових чисел. Ми побачимо, як біти знаку, експоненти та мантиси кодують дійсні числа, чому точність обмежена й які наслідки це має для наукових обчислень і повсякденного програмування.Лабораторна P12: Віртуальна пам'ять у LinuxУ цій лабораторній роботі ви побачите віртуальну пам'ять у дії на реальній системі Linux, застосовуючи інструменти та поняття з цієї лекції.Зміст роботи:
Дослідити віртуальний адресний простір процесу через /proc/[pid]/maps — визначити сегменти коду, даних, купи, стеку та спільних бібліотек
Застосувати pmap і vmstat для огляду використання пам'яті та статистики віртуальної пам'яті
Поспостерігати ASLR у дії, запускаючи ту саму програму кілька разів і порівнюючи адреси
Виміряти сторінкові помилки за допомогою perf stat -e page-faults, розрізняючи мінорні та мажорні
Поекспериментувати з великими сторінками (2 МБ) та простежити ефекти TLB
Порівняти частоту сторінкових помилок для послідовного та випадкового патернів доступу до пам'яті й побачити, як локальність впливає на продуктивність віртуальної пам'яті
Ключовий зв'язок із лекцією: ми переконаємося, що поняття з цієї лекції — таблиці сторінок, TLB, сторінкові помилки, ASLR — це не абстракції, а спостережувані та вимірювані явища на реальному апаратному забезпеченні.
<br>
<a data-tooltip-position="top" aria-label="https://diveintosystems.org/book/C13-OS/vm.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://diveintosystems.org/book/C13-OS/vm.html" target="_self">Dive Into Systems — Chapter 13: Virtual Memory</a> — вичерпний вступ до понять віртуальної пам'яті та сторінкування <br>
<a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/computer-organization-architecture/page-tables-and-single-level-paging/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/computer-organization-architecture/page-tables-and-single-level-paging/" target="_self">GeeksforGeeks — Page Tables and Single-Level Paging</a> — структура таблиці сторінок і компоненти PTE <br>
<a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/computer-organization-architecture/translation-lookaside-buffer-tlb/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/computer-organization-architecture/translation-lookaside-buffer-tlb/" target="_self">GeeksforGeeks — Translation Lookaside Buffer (TLB)</a> — архітектура TLB і формула EMAT <br>
<a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/operating-systems/page-fault-handling-in-operating-system/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/operating-systems/page-fault-handling-in-operating-system/" target="_self">GeeksforGeeks — Page Fault Handling</a> — типи сторінкових помилок і кроки їх обробки <br>
<a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/operating-systems/page-replacement-algorithms-in-operating-systems/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/operating-systems/page-replacement-algorithms-in-operating-systems/" target="_self">GeeksforGeeks — Page Replacement Algorithms</a> — порівняння FIFO, Optimal, LRU <br>
<a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/operating-systems/multilevel-paging-in-operating-system/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/operating-systems/multilevel-paging-in-operating-system/" target="_self">GeeksforGeeks — Multilevel Paging</a> — мотивація та структура багаторівневої таблиці сторінок <br>
<a data-tooltip-position="top" aria-label="https://cs61.seas.harvard.edu/site/2018/Section4/" rel="noopener nofollow" class="external-link is-unresolved" href="https://cs61.seas.harvard.edu/site/2018/Section4/" target="_self">Harvard CS61 — x86-64 Page Tables</a> — обхід чотирирівневої таблиці сторінок і формат адреси <br>
<a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/operating-systems/second-chance-or-clock-page-replacement-policy/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/operating-systems/second-chance-or-clock-page-replacement-policy/" target="_self">GeeksforGeeks — Second Chance / Clock Algorithm</a> — алгоритм годинника для заміщення сторінок із бітом звернення <br>
<a data-tooltip-position="top" aria-label="https://cs.brown.edu/courses/csci0300/2022/notes/l16.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://cs.brown.edu/courses/csci0300/2022/notes/l16.html" target="_self">Brown CS0300 — Page Tables and Address Translation</a> — багаторівневі таблиці сторінок, біти захисту, біт NX <br>
<a data-tooltip-position="top" aria-label="https://blog.zolutal.io/understanding-paging/" rel="noopener nofollow" class="external-link is-unresolved" href="https://blog.zolutal.io/understanding-paging/" target="_self">Zolutal Blog — Understanding x86-64 Paging</a> — детальний формат PTE, великі сторінки, механіка обходу таблиці сторінок <br>
<a data-tooltip-position="top" aria-label="https://www.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/9_VirtualMemory.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/9_VirtualMemory.html" target="_self">UIC — Virtual Memory (Silberschatz)</a> — сторінкування за запитом, копіювання при записі, усі алгоритми заміщення, пробуксовування <br>
<a data-tooltip-position="top" aria-label="https://www.cs.cornell.edu/courses/cs4410/2018su/lectures/lec15-thrashing.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.cs.cornell.edu/courses/cs4410/2018su/lectures/lec15-thrashing.html" target="_self">Cornell CS4410 — Page Replacement and Thrashing</a> — двострілковий алгоритм годинника, модель робочого набору ]]></description><link>lectures/l12.-віртуальна-пам'ять-поглиблено.html</link><guid isPermaLink="false">Lectures/L12. Віртуальна пам'ять поглиблено.md</guid><pubDate>Mon, 20 Apr 2026 10:37:40 GMT</pubDate><enclosure url="assets/l12-virtual-address-space.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l12-virtual-address-space.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L12-NOTES. Віртуальна пам'ять поглиблено]]></title><description><![CDATA[Короткий довідник до Лекції 12. Повні матеріали: L12. Віртуальна пам'ять поглиблено.md.
Ізоляція — процес A не може звернутися до пам'яті процесу B; MMU відхиляє трансляцію
Абстракція — кожен процес бачить однаковий чистий віртуальний адресний простір (text, data, heap, stack)
Ефективність — оперативна пам'ять слугує кешем для диска; у фізичній пам'яті перебувають лише активно використовувані сторінки (сторінкування за запитом) Віртуальний: приватний, неперервний для кожного процесу; на x86-64 — 128 ТБ для користувача та 128 ТБ для ядра
Фізичний: спільний пул DRAM; керується ОС через таблицю фреймів
Ключові елементи розкладки: захист від нульового вказівника (перша сторінка не відображена), стек росте вниз, купа — вгору, .text доступний лише для читання, сторінки ядра присутні, але U/S = 0.Перемикання контексту = запис нового значення в CR3 → заміна всього адресного простору. Сучасні процесори використовують PCID, щоб уникнути скидання TLB.
Сторінка = фіксований блок віртуального простору; фрейм = фіксований блок фізичної RAM; обидва розміром 4 КБ (2^12) на x86
Несуміжне розміщення усуває зовнішню фрагментацію
Віртуальна адреса = [VPN (старші біти) | зміщення (молодші 12 бітів)]
Трансляція: VPN індексує таблицю сторінок → PTE → номер фрейму + зміщення → фізична адреса. Зміщення передається без змін.Приклад: VA 0x2A3C → VPN 2, зміщення 0xA3C → фрейм 3 → PA 0x3A3C.Вартість плоскої таблиці: 32-біт = 4 МБ на процес; 48-біт = 512 ГБ на процес → потрібні багаторівневі таблиці.Кожен PTE має 64 біти. Ключові бітові поля:Коли P = 0: ОС повторно використовує решту 63 бітів (напр., для збереження розташування у просторі підкачки). Біти A та D використовуються алгоритмами заміщення сторінок. Виділення PA з PTE: PTE &amp; 0x000FFFFFFFFFF000.Дворівнева (32-біт): Розбиття 20-бітного VPN на два 10-бітних індекси → 1024-записна зовнішня та внутрішня таблиці (кожна 4 КБ). Виділення за потребою. Типово: ~16 КБ проти 4 МБ плоскої.Чотирирівнева (x86-64): 512 записів × 8 Б = 4 КБ на таблицю. | PML4 (9б) | PDPT (9б) | PD (9б) | PT (9б) | Зміщення (12б) |Витяг індексів: L4 = (VA &gt;&gt; 39) &amp; 0x1FF, L3 = (VA &gt;&gt; 30) &amp; 0x1FF, L2 = (VA &gt;&gt; 21) &amp; 0x1FF, L1 = (VA &gt;&gt; 12) &amp; 0x1FF, offset = VA &amp; 0xFFF.Без TLB: 4 доступи для обходу + 1 для даних = 5 загалом. Мінімальні накладні витрати: 4 × 4 КБ = 16 КБ. Типово: 100–500 КБ.Компроміс: більше покриття TLB коштом внутрішньої фрагментації та складності виділення.Повністю асоціативний апаратний кеш трансляцій VPN → PFN (+ ASID, біт дійсності, біт забруднення, біти дозволів, глобальний біт).Потік: влучення TLB → 1–2 цикли → фізична адреса. Промах TLB → апаратний обхід таблиці сторінок (4 читання) → заповнення TLB → повторна спроба. Біт присутності = 0 на будь-якому рівні → сторінкова помилка.EMAT = T_tlb + T_mem + (1 - h) × T_walk
Приклад: T_tlb = 1 нс, T_mem = 100 нс, T_walk = 400 нс, h = 99% → EMAT = 1 + 100 + 0.01 × 400 = 105 нс.Контекстні перемикання: старі процесори скидають TLB під час зміни CR3; сучасні використовують теги PCID, щоб записи різних процесів співіснували. Глобальний біт зберігає записи ядра між перемиканнями.Обробка (мажорна): збереження стану → читання CR2 → перевірка адреси → пошук вільного фрейму (за потреби — витіснення, а якщо фрейм забруднений, то й зворотний запис) → читання з диска через DMA → оновлення PTE (P = 1) → INVLPG → перезапуск інструкції.Сторінкування за запитом: PTE починають з P = 0; сторінки завантажуються під час першого доступу. Копіювання при записі (COW): після fork() фрейми спільні лише для читання; запис ініціює копіювання тільки цієї сторінки.EAT = (1 - p) × 200 нс + p × час_сторінкової_помилки
Щоб утримати деградацію в межах 10%, частота помилок має бути нижчою за ~1 на мільйон доступів.Рядок звернень (3 фрейми, 20 звернень): 7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2, 1, 2, 0, 1, 7, 0, 1Годинник: циклічний буфер зі стрілкою. A = 0 → витіснити; A = 1 → очистити, просунути, повторити. Розширений варіант також перевіряє біт забруднення; пріоритет: (0,0) найкращий &gt; (0,1) &gt; (1,0) &gt; (1,1) найгірший.OPT неможливий (потребує знання майбутнього). Справжній LRU непрактичний (відстеження під час кожного доступу). Алгоритм годинника — саме те, що реально використовують Linux, FreeBSD та Windows.Матриця дозволів (U/S × R/W):Біт NX (біт 63): не дає змоги вибирати інструкції зі сторінок даних → політика W^X (жодна сторінка не може бути одночасно доступною для запису й для виконання).ASLR: під час кожного запуску рандомізує стек (~30 бітів), бібліотеки (~28 бітів), купу (~13 бітів), PIE (~28 бітів). Протидіє ROP. KPTI: захист від Meltdown — окремі таблиці сторінок для користувача та ядра; ~1–5% накладних витрат за наявності PCID.Діапазон: 5 000 000:1 між найкращим і найгіршим випадком для тієї самої інструкції mov.
Віртуальна пам'ять забезпечує ізоляцію, абстракцію та ефективність
Сторінкування: VPN → таблиця сторінок → номер фрейму; зміщення лишається без змін
x86-64 використовує чотирирівневі таблиці сторінок (PML4 → PDPT → PD → PT); виділяються лише задіяні гілки
TLB робить сторінкування практичним — коефіцієнт влучань 99%+ зменшує накладні витрати до ~5%
Сторінкові помилки мають бути рідкісними — навіть одна на 1 000 звернень спричиняє уповільнення у 41 раз (HDD)
Алгоритм годинника = практичне наближення LRU, яке застосовують реальні ОС
Захист забезпечують біти R/W, U/S, NX для кожної сторінки; ASLR рандомізує розташування в адресному просторі
Повний шлях: VA → TLB → таблиці сторінок → PA → ієрархія кешів → дані Дослідити /proc/[pid]/maps — визначити сегменти коду, даних, купи, стеку, спільних бібліотек
Скористатися pmap та vmstat для статистики пам'яті
Поспостерігати ASLR: запустити ту саму програму кілька разів і порівняти адреси
Виміряти кількість сторінкових помилок за допомогою perf stat -e page-faults
Порівняти сторінки 4 КБ та 2 МБ і їхній вплив на TLB
Зіставити послідовний і випадковий доступ — побачити, як локальність впливає на продуктивність віртуальної пам'яті <a data-tooltip-position="top" aria-label="https://diveintosystems.org/book/C13-OS/vm.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://diveintosystems.org/book/C13-OS/vm.html" target="_self">Dive Into Systems — Ch. 13: Virtual Memory</a> — ґрунтовний вступ
<br><a data-tooltip-position="top" aria-label="https://cs61.seas.harvard.edu/site/2018/Section4/" rel="noopener nofollow" class="external-link is-unresolved" href="https://cs61.seas.harvard.edu/site/2018/Section4/" target="_self">Harvard CS61 — x86-64 Page Tables</a> — подробиці чотирирівневого обходу
<br><a data-tooltip-position="top" aria-label="https://blog.zolutal.io/understanding-paging/" rel="noopener nofollow" class="external-link is-unresolved" href="https://blog.zolutal.io/understanding-paging/" target="_self">Zolutal Blog — Understanding x86-64 Paging</a> — формат PTE, великі сторінки
<br><a data-tooltip-position="top" aria-label="https://cs.brown.edu/courses/csci0300/2022/notes/l16.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://cs.brown.edu/courses/csci0300/2022/notes/l16.html" target="_self">Brown CS0300 — Page Tables</a> — багаторівневі таблиці, біт NX
<br><a data-tooltip-position="top" aria-label="https://www.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/9_VirtualMemory.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/9_VirtualMemory.html" target="_self">UIC — Virtual Memory (Silberschatz)</a> — сторінкування за запитом, копіювання при записі, алгоритми заміщення
]]></description><link>lectures/l12-notes.-віртуальна-пам'ять-поглиблено.html</link><guid isPermaLink="false">Lectures/L12-NOTES. Віртуальна пам'ять поглиблено.md</guid><pubDate>Mon, 20 Apr 2026 10:37:40 GMT</pubDate></item><item><title><![CDATA[L11. Ієрархія пам'яті та продуктивність]]></title><description><![CDATA[
Лекція 11 курсу «Архітектура комп'ютерних систем» в НаУКМА.
Після цієї лекції ви зможете:
Пояснити принцип ієрархії пам'яті та компроміс швидкість–ємність–вартість, що мотивує багаторівневе кешування
Розрізняти часову та просторову локальність і писати кеш-дружній код, що використовує обидва види
Описати внутрішню структуру кешу (рядки, теги, множини, розбиття адреси на зміщення/індекс/тег)
Порівнювати кеш з прямим відображенням, множинно-асоціативний та повністю асоціативний кеш і компроміси між ними
Застосовувати формулу AMAT (середній час доступу до пам'яті) для обчислення ефективного часу доступу до пам'яті в багаторівневих ієрархіях кешу
Пояснити політики заміщення кешу (LRU, FIFO, випадкове) та політики запису (наскрізний проти зворотного запису)
Описати протокол когерентності кешу MESI та простежити переходи станів у багатоядерних сценаріях
Розпізнавати хибне спільне використання як прихований фактор втрати продуктивності та пояснити, як його усунути за допомогою вирівнювання по рядках кешу
Уявімо шеф-кухаря на напруженій кухні. Полиця зі спеціями — на відстані витягнутої руки: сіль або перець можна взяти менш ніж за секунду. Комора — у кінці коридору: тридцять секунд туди й назад. Продуктовий магазин — на іншому кінці міста: півгодини. Якщо рецепт потребує солі десять разів, до магазину не бігають щоразу — сіль тримають на стільниці. Причина очевидна: чим ближче сховище, тим швидше можна працювати. Комп'ютерні системи стикаються точно з такою самою проблемою, і їхнє розв'язання — ієрархія пам'яті — є темою цієї лекції.На початку 1980-х років процесори й пам'ять працювали приблизно на одній швидкості. Процесору потрібно було приблизно стільки ж часу на виконання інструкції, скільки DRAM — на видачу слова даних. <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Intel_8086" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Intel_8086" target="_self">Intel 8086</a>, що працював на частоті 5 МГц, мав тактовий період 200 нс, і DRAM тієї епохи відповідала приблизно за той самий час — швидкості були добре узгоджені. Згодом ці дві траєкторії різко розійшлися.Тактова частота процесорів упродовж 1990-х зростала приблизно на 50–60% на рік — завдяки закону Мура та агресивним мікроархітектурним інноваціям. Затримка DRAM, обмежена фізикою конденсаторів і підсилювачів зчитування, скорочувалася лише приблизно на 7% на рік. До 2000 року процесор встигав виконати сотні інструкцій за час, потрібний на завантаження одного рядка кешу з основної пам'яті. Співвідношення між поступом швидкості процесора та поступом швидкості пам'яті за останні чотири десятиліття вражає: час тактового циклу процесора скоротився приблизно у 1 000 разів з 1980 року, тоді як затримка DRAM — лише приблизно у 10 разів.Конкретніше: Intel 8086 (1978) на частоті 5 МГц чекав приблизно один тактовий цикл на доступ до DRAM. Сучасний процесор на частоті 5 ГГц — за тактовою частотою у 1 000 разів швидший — на такий самий доступ до DRAM чекає вже близько 200–400 тактових циклів. Пам'ять справді стала дещо швидшою в абсолютних наносекундах, але далеко не настільки, щоб встигати за процесором.Цю прірву, що розширюється, називають розривом процесор–пам'ять (іноді — стіною пам'яті). Як ми вже бачили в L08, це одна з центральних проблем, які сформували сучасну комп'ютерну архітектуру — і вона нікуди не зникла. Навпаки, сьогодні розрив ширший, ніж будь-коли: хоча тактові частоти процесорів вийшли на плато в середині 2000-х, такі техніки, як позачергове виконання, суперскалярні конвеєри та багатоядерний паралелізм, продовжували нарощувати швидкість, з якою процесори вимагають дані. А сторона постачання — затримка DRAM — ледь зрушила з місця.<br><img alt="Розрив продуктивності процесор–пам'ять — швидкість CPU зросла ~1000×, тоді як затримка DRAM покращилася лише ~10× за 40 років" src="assets/l11-processor-memory-gap.svg" target="_self" style="width: 650px; max-width: 100%;">Щоб оцінити масштаб проблеми, розгляньмо реальні затримки, з якими стикається сучасна програма, коли звертається до даних на різних рівнях ієрархії сховищ:<br>Ці числа, популяризовані Джеффом Діном з Google під назвою <a data-tooltip-position="top" aria-label="https://gist.github.com/jboner/2841832" rel="noopener nofollow" class="external-link is-unresolved" href="https://gist.github.com/jboner/2841832" target="_self">"Latency Numbers Every Programmer Should Know"</a>, варті хвилини роздумів. Влучання в кеш L1 триває півнаносекунди — приблизно один-два тактових цикли сучасного процесора. Доступ до основної пам'яті триває 100 нс, тобто приблизно 200–400 тактових циклів залежно від тактової частоти. За ці 200+ циклів сучасний суперскалярний процесор міг би виконати сотні корисних інструкцій — та натомість він простоює, чекаючи на дані з DRAM.Отже, один промах кешу, який сягає основної пам'яті, коштує процесору приблизно 200 марних циклів. У щільному циклі, що обробляє мільйони елементів, різниця між програмою, яка влучає в L1 за кожного звернення, і програмою, яка промахується до основної пам'яті бодай у 5% звернень, може сягати порядку величини за швидкістю. Це не теоретичне занепокоєння — це одне з найпоширеніших вузьких місць продуктивності в реальному програмному забезпеченні.Щоб ці числа стали відчутнішими, скористаймося аналогією з людським часом: якби влучання в кеш L1 розтягнути до 1 секунди (ніби взяти книгу зі столу), то влучання в L2 тривало б 14 секунд (підійти до сусідньої полиці). Доступ до основної пам'яті тривав би 3 хвилини 20 секунд (сходити до бібліотеки в кінці коридору). Зчитування з SSD — 3,5 доби. А пошук на жорсткому диску? Понад 230 днів — майже вісім місяців очікування на один фрагмент даних. Ієрархія охоплює приблизно сім порядків величини затримки, і програмісти ігнорують ці співвідношення на власний ризик.І це лише оперативна пам'ять. Якщо дані мають надійти з SSD, процесор чекає 150 мікросекунд — цього достатньо, щоб виконати приблизно 600 000 інструкцій. З обертового жорсткого диска очікування розтягується до 10 мілісекунд — часу, достатнього на 40 мільйонів інструкцій. Розуміння того, де саме дані перебувають у цій ієрархії, — одне з найцінніших знань про продуктивність, яке взагалі може мати програміст.Це не абстрактна проблема інженерів-апаратників. Для розробника програмного забезпечення найпоширеніша причина несподіваних уповільнень у ніби добре написаному коді — саме погана поведінка кешу. Дві програми, які виконують те саме обчислення над тими самими даними за тим самим алгоритмом, можуть відрізнятися за швидкістю в 10 і більше разів — залежно лише від того, як вони звертаються до пам'яті. Швидка версія звертається до даних за шаблонами, які тримають кеші «теплими»; повільна — розкидає звернення по всій пам'яті, спричиняючи промах за промахом.Розгляньмо простий приклад. Нехай задано матрицю 4096 × 4096 цілих чисел і треба знайти суму всіх її елементів. Дві реалізації мовою C виглядають майже однаково:// Версія A: обхід по рядках (row-major)
for (int i = 0; i &lt; N; i++) for (int j = 0; j &lt; N; j++) sum += matrix[i][j]; // Версія B: обхід по стовпцях (column-major)
for (int j = 0; j &lt; N; j++) for (int i = 0; i &lt; N; i++) sum += matrix[i][j];
Обидві дають однаковий результат і виконують однакову кількість додавань. Проте на типовому сучасному процесорі версія A працює у 3–8 разів швидше за версію B. Причина цілковито в поведінці кешу: C зберігає матриці в порядку по рядках, тож версія A проходить пам'ять послідовно (використовуючи просторову локальність), тоді як версія B за кожного звернення стрибає між рядками (руйнуючи цю локальність). Кожен стрибок потрапляє в інший рядок кешу й спричиняє промах. Далі в лекції ми формально проаналізуємо цей шаблон.У лабораторній P11 ви побачите це на власні очі. За допомогою інструмента perf stat у Linux на реальному обладнанні ми виміряємо коефіцієнти влучань у кеш та кількість промахів для різних шаблонів доступу до пам'яті. Буде помітно, що проста зміна — обхід по рядках проти обходу по стовпцях — може різко скоротити час виконання суто завдяки ефектам кешу. Код алгоритмічно ідентичний; різниця виникає лише на архітектурному рівні.Ця лекція дає концептуальний інструментарій для розуміння, прогнозування й оптимізації поведінки кешу. Ми розширимо огляд ієрархії пам'яті з L08, зануривши в механізми кешу глибше:
Огляд ієрархії пам'яті — стислий огляд піраміди від регістрів до диска з конкретними числами для сучасного процесора.
Локальність звернень — повторний розгляд часової та просторової локальності з новою глибиною й практичними прикладами.
Структура кешу — як кеш влаштований усередині: множини, канали, теги та відображення адрес пам'яті на позиції в кеші.
Політики заміщення — що відбувається, коли кеш заповнений, а треба завантажити нові дані (LRU, псевдо-LRU, випадкове).
Політики запису — як кеш опрацьовує записи (зворотний запис проти наскрізного, запис із розміщенням проти запису без розміщення).
Вимірювання продуктивності кешу — формула AMAT (середній час доступу до пам'яті), розширена на багаторівневі кеші.
Когерентність кешу та MESI — як багатоядерні процесори підтримують узгодженість своїх приватних кешів; пастка продуктивності — хибне спільне використання.
Наприкінці лекції ви зможете подивитися на фрагмент коду та міркувати про його поведінку щодо кешу — прогнозувати, буде він кеш-дружнім чи кеш-ворожим, і розуміти, що варто змінити в другому випадку.Як ми бачили в L06 та детально вивчали в L08, жодна окрема технологія пам'яті не може одночасно бути швидкою, великою та дешевою. Фізика цього не дозволяє: швидша пам'ять потребує більше транзисторів на біт (SRAM використовує шість; DRAM — один транзистор і один конденсатор), що обмежує щільність і підвищує вартість. Відповідь архітектора комп'ютерів — ієрархія пам'яті: багатошаровий стек поступово більших, повільніших і дешевших сховищ, спроєктований так, щоб дані, які процесор потребує найтерміновіше, майже завжди перебували на найшвидшому рівні.Ієрархія від найшвидшого до найповільнішого виглядає так:Регістри → Кеш L1 → Кеш L2 → Кеш L3 → Основна пам'ять (RAM) → SSD → HDD → СтрічкаКожен рівень зазвичай на порядок повільніший і на порядок більший за попередній. Регістри зберігають кілька сотень байтів і відповідають менш ніж за наносекунду. Жорсткий диск зберігає терабайти й відповідає за мілісекунди. Співвідношення між вершиною та основою становить приблизно 10 000 000:1 і за ємністю, і за затримкою — у протилежних напрямках.Верхні рівні (від регістрів до RAM) — енергозалежні: вони втрачають вміст після зняття живлення. Нижні рівні (SSD, HDD, стрічка) — енергонезалежні: дані зберігаються без живлення. Саме тому комп'ютеру потрібні і RAM, і жорсткий диск: RAM забезпечує швидке робоче сховище, поки система працює, а диск — постійне зберігання, що переживає перезавантаження та знеструмлення.У цій лекції ми зосереджуємося переважно на верхніх чотирьох рівнях — L1, L2, L3 та RAM, — адже саме ними автоматично керує апаратне забезпечення кешу, і їхня поведінка справляє найбільший прямий вплив на продуктивність програм.Щоб прив'язати цю розмову до реальних чисел, наведемо параметри кешу для репрезентативного сучасного процесора — AMD Ryzen 7 7700X (архітектура Zen 4, випущений 2022 року):Звернімо увагу на закономірність. Кожен крок вниз по ієрархії коштує приблизно у 3–10 разів більшої затримки, даючи натомість приблизно у 10–30 разів більшу ємність. Кеш L1 сліпуче швидкий — менше ніж наносекунда, — але вміщує лише 64 КБ на ядро (32 КБ для інструкцій, 32 КБ для даних). RAM у 100 разів повільніша, зате зберігає в мільйон разів більше даних.З боку Intel, Core i7 12-го покоління (Alder Lake) пропонує подібну структуру: 48 КБ L1D + 32 КБ L1I на продуктивне ядро, 1,25 МБ L2 на ядро та 25 МБ спільного L3. Точні числа відрізняються, але співвідношення та компроміси універсальні для всіх сучасних процесорів x86 — та й для ARM, RISC-V і будь-якої іншої архітектури, що сьогодні має широке застосування.Кілька архітектурних деталей варто підкреслити окремо, бо вони визначають, як ми міркуватимемо про поведінку кешу далі в лекції:L1 розділений на два окремих кеші. Кеш інструкцій L1 (L1I) зберігає нещодавно завантажені машинні інструкції; кеш даних L1 (L1D) — нещодавно використані операнди даних. Цей поділ дозволяє процесору завантажувати наступну інструкцію і читати або записувати дані одночасно, без конкуренції двох потоків за один і той самий порт кешу. На рівні L2 і нижче кеші є уніфікованими — зберігають і інструкції, і дані в єдиній структурі. Конструкцію з розділеним кешем на рівні L1 іноді називають архітектурою гарвардського кешу (на відлуння гарвардської архітектури з окремими пам'яттями інструкцій і даних), навіть попри те, що основна пам'ять використовує єдиний уніфікований адресний простір.L1 і L2 є приватними для кожного ядра. У багатоядерному процесорі кожне ядро має власні виділені кеші L1 та L2. Це означає, що на 8-ядерному Ryzen 7 7700X сумарна ємність L1D по всьому чіпу становить 8 × 32 КБ = 256 КБ, а сумарна ємність L2 — 8 × 1 МБ = 8 МБ. Кожне ядро звертається лише до своїх приватних кешів, тому на цих рівнях немає конкуренції між ядрами.L3 є спільним для всіх ядер. Кеш L3 — це єдиний великий пул, до якого може звертатися будь-яке ядро. Така спільна конструкція виконує дві функції: надає більший запас для промахів L2 (32 МБ вміщують значно більше даних, ніж 1 МБ L2 окремого ядра) і забезпечує ефективну міжядерну комунікацію. Коли одне ядро записує дані, які інше ядро згодом читає, спільний L3 може надати ці дані за ~10 нс замість ~70 нс ходження до основної пам'яті. Проте спільне використання створює й проблему когерентності кешу — підтримання узгодженості приватних кешів усіх ядер, коли кілька ядер читають і записують за одними адресами. Цій проблемі ми присвятимо окремий розділ далі в лекції.Можна запитати: якщо SRAM така швидка, чому б просто не побудувати 64 ГБ SRAM і не обійтися без DRAM? Відповідь, як ми обговорювали в L08, — економіка. SRAM використовує шість транзисторів на біт; DRAM — один транзистор і один конденсатор. За поточними виробничими цінами SRAM коштує приблизно $1 000+ за гігабайт, тоді як DRAM — приблизно $3–5 за гігабайт. Побудова 64 ГБ SRAM коштувала б десятки тисяч доларів, а сам чіп був би фізично величезним, що фактично збільшило б затримку, бо електричні сигнали поширюються зі скінченною швидкістю. Більші кеші за своєю природою повільніші за менші, навіть якщо обидва використовують ту саму технологію SRAM.Тому ієрархія — це не тимчасовий інженерний компроміс, а фундаментальний наслідок фізики та економіки. Кожен рівень заслуговує своє місце, пропонуючи найкращий компроміс між швидкістю, ємністю й вартістю для конкретної ролі.Геніальність ієрархії в тому, що вона створює ілюзію: з погляду програміста, пам'ять видається майже такою ж швидкою, як кеш L1, і майже такою ж великою, як жорсткий диск. Ця ілюзія працює завдяки локальності звернень — схильності програм повторно звертатися до тих самих даних (часова локальність) і до сусідніх даних (просторова локальність) протягом коротких проміжків часу. Апаратне забезпечення автоматично тримає нещодавно та часто використовувані дані на швидких верхніх рівнях, тож більшість звернень влучає в кеш і завершується швидко.Як ми бачили в аналізі AAT (середнього часу доступу) в L08, трирівнева ієрархія кешу може зменшити ефективний час доступу до пам'яті зі ~100 нс (чиста DRAM) до менш ніж 2 нс — покращення у 50 разів — за умови, що коефіцієнти влучань залишаються високими. Ключова фраза — «за умови, що коефіцієнти влучань залишаються високими». Коли шаблон доступу програми руйнує локальність — наприклад, програма стрибає випадково по структурі даних, що значно перевищує розмір кешу, — ілюзія розвалюється, і тоді практично за кожного звернення доводиться платити повну затримку DRAM.Ієрархія — це фундамент. Усе, що йтиме далі в цій лекції — організація кешу, політики заміщення, стратегії запису, вимірювання продуктивності та протоколи когерентності — існує для того, щоб ця ієрархія працювала ефективно. Наступний розділ розглядає властивість програм, яка взагалі робить кеші ефективними: локальність звернень.<br><img alt="Топологія кешу сучасного багатоядерного процесора — приватні L1i/L1d та L2 на кожне ядро, спільний L3, основна пам'ять" src="assets/l11-cache-topology.svg" target="_self" style="width: 650px; max-width: 100%;">У Лекції 8 ми познайомилися з принципом локальності як причиною того, чому ієрархія пам'яті взагалі працює. Тепер розгляньмо локальність значно глибше: проаналізуємо шаблони кроків доступу, кількісно оцінимо вплив хороших і поганих патернів на продуктивність та навчимося писати кеш-дружній код.Часова локальність стверджує: якщо до адреси пам'яті нещодавно зверталися, то найближчим часом до неї, імовірно, звернуться знову. Це не апаратна особливість, а шаблон, який природно виникає зі структури програм.Розглянемо найпоширеніші джерела часової локальності: Лічильники циклів та індукційні змінні. Цикл for на кожній ітерації зчитує та оновлює свою змінну-лічильник. Якщо цикл виконується 1 000 разів, до цієї єдиної змінної звертаються 1 000 разів поспіль. Акумулятори та проміжні суми. Змінна на кшталт sum у циклі редукції зчитується й записується на кожній ітерації — потужне часове повторне використання однієї адреси. Адреси повернення функцій. Коли функція викликається багаторазово (наприклад, допоміжна функція всередині циклу), її адреса повернення та локальні змінні знову і знову займають ту саму область стека. Вибірка інструкцій. Інструкції в тілі циклу вибираються з тих самих адрес пам'яті на кожній ітерації. Щільний цикл із 10 інструкцій, який виконується 10 000 разів, породжує 100 000 вибірок інструкцій, але лише з 10 різних адрес. Кеш використовує часову локальність, зберігаючи нещодавно задіяні блоки у швидкому сховищі. При зверненні до адреси пам'яті весь рядок кешу, що її містить, завантажується в L1. Якщо до тієї самої адреси звертаються знову протягом короткого вікна — до того, як рядок кешу витіснено — доступ стає влучанням і обслуговується зі швидкістю L1 (приблизно 1 нс), а не основної пам'яті (приблизно 100 нс). Головне спостереження таке: кеш найефективніший тоді, коли ті самі дані повторно використовуються багато разів до витіснення.Просторова локальність стверджує: якщо до адреси пам'яті зверталися, то найближчим часом, імовірно, звернуться і до сусідніх адрес. Як і часова локальність, це наслідок структури програми, а не апаратна гарантія.Найважливіші джерела просторової локальності: Обхід масиву. Послідовний прохід масиву елемент за елементом — класичний приклад. Елементи зберігаються в пам'яті послідовно, тож доступ до a[0] означає, що a[1], a[2] і далі фізично розташовані поруч. Послідовна вибірка інструкцій. Поки програма не розгалужується, наступна інструкція для виконання лежить за наступною адресою пам'яті. Лінійний код має ідеальну просторову локальність. Доступ до полів структури. Зчитавши одне поле структури, програма зазвичай невдовзі звертається й до інших. Оскільки поля структури лежать у пам'яті послідовно, це теж приклад просторової локальності. Операції зі стеком. Локальні змінні, аргументи функцій та адреси повернення щільно розташовані на стеку. Доступ до одного з них зазвичай веде і до швидкого доступу до сусідніх. Кеш використовує просторову локальність, завантажуючи цілі рядки кешу — зазвичай 64 байти на сучасних процесорах x86 — а не окремі байти. При зчитуванні одного int (4 байти) з адреси N апаратне забезпечення завантажує весь 64-байтний блок, що містить цю адресу. Якщо наступний доступ потрапляє в той самий блок, дані вже в кеші. Для послідовного обходу масиву 4-байтних цілих чисел це означає, що перший доступ до рядка кешу спричиняє промах, а наступні 15 — усі влучання: коефіцієнт влучань 93,75% лише завдяки просторовій локальності.Різниця між хорошою і поганою просторовою локальністю — це не дрібна оптимізація: вона здатна змінити продуктивність на порядок. Найпоказовіший приклад — обхід матриці.<br>У C двовимірні масиви зберігаються в <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Row-_and_column-major_order" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Row-_and_column-major_order" target="_self">порядку по рядках</a>: matrix[0][0], matrix[0][1], matrix[0][2], ..., matrix[0][N-1], matrix[1][0], matrix[1][1] і далі. Елементи одного рядка розташовані в пам'яті послідовно, а елементи одного стовпця розділені цілим рядком даних.Розглянемо два способи підсумувати всі елементи матриці N × N:// Обхід по рядках (хороша просторова локальність)
long sum = 0;
for (int i = 0; i &lt; N; i++) for (int j = 0; j &lt; N; j++) sum += matrix[i][j]; // Обхід по стовпцях (погана просторова локальність)
long sum = 0;
for (int j = 0; j &lt; N; j++) for (int i = 0; i &lt; N; i++) sum += matrix[i][j];
Обидва цикли обчислюють однаковий результат і виконують однакову кількість додавань. Але для великих матриць перша версія (порядок по рядках) зазвичай у 4–8 разів швидша за другу (порядок по стовпцях). Чому?Різницю пояснює аналіз кроків доступу. Крок доступу — це відстань у пам'яті між послідовними зверненнями. Обхід по рядках має крок 1: кожне звернення віддалене від попереднього рівно на sizeof(int) = 4 байти. 64-байтний рядок кешу вміщує 16 цілих чисел, тож після одного обов'язкового промаху наступні 15 звернень — усі влучання. Ефективний коефіцієнт промахів становить 1/16 = 6,25%. Обхід по стовпцях має крок N: кожне звернення перестрибує на N × sizeof(int) байтів до наступного рядка. Якщо N дорівнює 1 024 (помірний розмір матриці), кожне звернення перестрибує на 4 096 байтів — на 64 рядки кешу вперед. Кожне окреме звернення стає промахом кешу, бо на момент повернення до того самого рядка його рядок кешу давно витіснено. Коефіцієнт промахів наближається до 100%. Порядок по рядках (крок 1): [a00 a01 a02 a03 ... a0,15] ← один рядок кешу, 16 влучань [a0,16 a0,17 ... ] Порядок по стовпцях (крок N): a00 ← промах (завантаження рядка) a10 ← промах (інший рядок, на N елементів далі) a20 ← промах (знову інший рядок) ...усі промахи, доки не повернемося до рядка 0
<br><img alt="Обхід матриці по рядках та по стовпцях — послідовний доступ використовує просторову локальність, доступ з кроком — ні" src="assets/l11-matrix-access-patterns.svg" target="_self" style="width: 650px; max-width: 100%;"><br>Це не академічна цікавинка. Числові бібліотеки, такі як <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Basic_Linear_Algebra_Subprograms" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Basic_Linear_Algebra_Subprograms" target="_self">BLAS</a> і <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/LAPACK" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/LAPACK" target="_self">LAPACK</a>, написані на Fortran, а він використовує порядок по стовпцях. Коли код на C викликає ці бібліотеки, розбіжність у розкладці матриці може непомітно просадити продуктивність, якщо програміст цього не врахує.Розуміння локальності веде до практичних принципів написання високопродуктивного коду.Принцип 1. Віддавати перевагу послідовним патернам доступу. За можливості варто ітерувати по даних у тому порядку, у якому вони лежать у пам'яті. Для масивів у C це означає, що найправіший індекс має змінюватися найшвидше (конвенція порядку по рядках). Для масивів у Fortran — навпаки.Принцип 2. Обирати кеш-дружні розкладки даних. Класичний компроміс — масив структур (AoS) проти структури масивів (SoA):// Масив структур (AoS) — поля перемежовані в пам'яті
struct Particle { float x, y, z, mass; };
struct Particle particles[10000]; // Структура масивів (SoA) — кожне поле розташоване послідовно
struct Particles { float x[10000]; float y[10000]; float z[10000]; float mass[10000];
};
Якщо алгоритм обробляє лише координати x (наприклад, щоб знайти найлівішу частинку), SoA значно кеш-дружніша: значення x лежать послідовно, і кожен рядок кешу використовується повністю. У разі AoS кожне значення x відокремлене від наступного значеннями y, z та mass — три чверті кожного рядка кешу витрачаються на непотрібні дані.Принцип 3. Застосовувати тайлінг циклів (блокування) для вкладених циклів. Коли обчислення звертається до кількох великих масивів — наприклад, множення матриць — робочий набір легко перевищує розмір кешу, що спричиняє часті витіснення. Тайлінг циклів (також відомий як блокування) реструктуризує обчислення так, щоб воно працювало з малими підблоками, які поміщаються в кеш:// Наївне множення матриць — погана локальність для B
for (int i = 0; i &lt; N; i++) for (int j = 0; j &lt; N; j++) for (int k = 0; k &lt; N; k++) C[i][j] += A[i][k] * B[k][j]; // Множення матриць з тайлінгом — B зчитується кеш-дружніми блоками
for (int ii = 0; ii &lt; N; ii += BLOCK) for (int jj = 0; jj &lt; N; jj += BLOCK) for (int kk = 0; kk &lt; N; kk += BLOCK) for (int i = ii; i &lt; ii + BLOCK; i++) for (int j = jj; j &lt; jj + BLOCK; j++) for (int k = kk; k &lt; kk + BLOCK; k++) C[i][j] += A[i][k] * B[k][j];
Якщо обрати BLOCK так, щоб підблоки A, B та C одночасно поміщалися в кеш L1, версія з тайлінгом може бути для великих матриць у 10–20 разів швидшою. Цю техніку ми дослідимо на практиці в найближчій лабораторній роботі з вимірювання продуктивності.Принцип 4. Мінімізувати робочий набір. Варто використовувати найменші типи даних, які є достатніми (int16_t замість int64_t, коли значення в нього вміщаються), уникати марнування пам'яті на вирівнювання в структурах і тримати «гарячі» дані (до яких звертаються часто) окремо від «холодних» (до яких звертаються рідко). Чим менший робочий набір, тим більша його частина поміщається в кеш і тим вищий коефіцієнт влучань.Головний урок такий: найшвидший алгоритм на папері не обов'язково є найшвидшим на практиці. Алгоритм із гіршою асимптотичною складністю, але кращою локальністю, може випередити теоретично кращий алгоритм, чиї патерни доступу постійно витісняють дані з кешу. Сучасна інженерія продуктивності вимагає думати про те, де розташовані дані, а не лише про те, скільки операцій виконується.Досі ми говорили про кеш як про швидкий рівень зберігання, що «тримає нещодавно використані дані поруч». Але як саме апаратура організовує та знаходить дані на наносекундних швидкостях? Відповідь — у продуманій схемі адресації, яка розкладає кожну адресу пам'яті на три поля (тег, індекс і зміщення) і використовує їх, щоб реалізувати, по суті, крихітну, блискавично швидку геш-таблицю.Фундаментальна одиниця зберігання в кеші — рядок кешу (його ще називають блоком кешу). Кожен рядок містить: Блок даних: власне дані — зазвичай 64 байти суміжної пам'яті, скопійованої з основної пам'яті. Це завжди природно вирівняний блок: 64-байтний рядок зберігає байти з адреси A до A + 63, де A кратне 64. Біти тегу: старші біти адреси пам'яті, які ідентифікують, який саме блок основної пам'яті наразі займає цей рядок. Оскільки багато різних блоків пам'яті можуть відображатися на одне й те саме місце в кеші, тег дозволяє їх розрізнити. Біт дійсності: один біт, який вказує, чи містить рядок кешу осмислені дані. Одразу після ввімкнення системи всі біти дійсності дорівнюють 0 (кеш «холодний»). Рядок стає дійсним, коли до нього завантажуються дані. Біт забруднення (для кешів зі зворотним записом): сигналізує, що дані в цьому рядку модифіковані процесором, але ще не записані назад до основної пам'яті. Коли такий рядок зрештою витісняється, кеш мусить записати модифіковані дані назад, перш ніж їх відкинути. <br>
Біти політики заміщення: у кешах, де кілька рядків можуть зберігати дані для одного й того самого індексу (асоціативні кеші, які розглядатимемо в наступній лекції), додаткові біти відстежують історію доступу для політики витіснення — наприклад, біти <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU)" target="_self">LRU</a> (найдавніше використаний). <br><img alt="Анатомія рядка кешу — біт дійсності, біт забруднення, тег, біти LRU та 64-байтний блок даних" src="assets/l11-cache-line-anatomy.svg" target="_self" style="width: 650px; max-width: 100%;">Чому саме 64 байти? Цей розмір рядка — ретельно підібраний компроміс. Більші рядки краще використовують просторову локальність: при промаху підтягується більше сусідніх байтів. Але у великих рядків є й зворотний бік:
Більше марно витраченої пропускної здатності, коли насправді потрібно лише кілька байтів із рядка.
Довший час передачі з основної пам'яті (хоча сучасні пакетні передачі пом'якшують цей ефект).
Менша загальна кількість рядків при тому самому розмірі кешу, що може збільшити кількість конфліктних промахів.
64-байтний рядок — стандарт для процесорів x86 ще з часів Pentium 4 (початок 2000-х) і залишається стандартом донині. Деякі архітектури використовують 128-байтні рядки (наприклад, IBM POWER), а вбудовані системи іноді обмежуються 32-байтними. Вибір залежить від характеристик просторової локальності очікуваного навантаження.Центральне питання проєктування кешу: маючи адресу пам'яті, як апаратура визначає, де саме в кеші її шукати? Відповідь — детерміноване відображення, яке розбиває кожну адресу пам'яті на три поля:| Tag (старші біти) | Index (середні біти) | Offset (молодші біти) |
Кожне поле має конкретне призначення:Зміщення (молодші біти): вказує на конкретний байт у межах 64-байтного рядка кешу. Оскільки рядок містить 64 = 2^6 байтів, зміщення має ширину 6 бітів. Значення — від 0 (перший байт у рядку) до 63 (останній).Індекс (середні біти): обирає, яку множину (або слот) у кеші перевіряти. Якщо кеш містить S множин, індекс має ширину log₂(S) бітів. Це «геш-функція» кешу — вона детерміновано відображає кожен блок пам'яті на одне конкретне місце в кеші.Тег (залишкові старші біти): усе, що залишилося після виділення зміщення та індексу. Перевіряючи рядок кешу, апаратура порівнює збережений у рядку тег із тегом, отриманим із запитуваної адреси. Збіг означає, що рядок містить потрібні дані.Загальна кількість бітів адреси — це сума біти_тегу + біти_індексу + біти_зміщення, яка збігається з повною шириною адресного простору (32 біти для 32-бітної системи або ширина фізичної адреси в 64-бітній системі).Розгляньмо конкретну конфігурацію:
Адресний простір: 32 біти (4 ГБ адресованої пам'яті)
Розмір кешу: 16 КБ (16 384 байти)
Розмір рядка: 64 байти
Організація: кеш з прямим відображенням (один рядок на множину)
Крок 1 — обчислити кількість рядків кешу:Кількість рядків = Розмір кешу / Розмір рядка = 16 384 / 64 = 256 рядків
Крок 2 — визначити ширину бітових полів:Біти зміщення = log₂(64) = 6 бітів (обирає байт у межах рядка)
Біти індексу = log₂(256) = 8 бітів (обирає рядок кешу)
Біти тегу = 32 − 8 − 6 = 18 бітів (ідентифікує блок пам'яті)
Крок 3 — розташувати адресу:| Тег (18 бітів) | Індекс (8 бітів) | Зміщення (6 бітів) |
| 31 ... 14 | 13 ... 6 | 5 ... 0 |
Простежмо конкретну адресу пам'яті: 0x0000_1A3C.Спочатку переведімо її у двійкову систему (32 біти):0x0000_1A3C = 0000 0000 0000 0000 0001 1010 0011 1100
Тепер розділимо відповідно до наших бітових полів:Тег (біти 31-14): 00 0000 0000 0000 0000 = 0x00000 = 0
Індекс (біти 13-6): 01 1010 00 = 0x68 = 104
Зміщення (біти 5-0): 11 1100 = 0x3C = 60
Що це нам каже:
Процесор шукатиме в рядку кешу 104 (індекс).
Він порівняє збережений тег із 0x00000.
Якщо збіг є, потрібний байт міститься на позиції 60 у межах 64-байтного блоку даних.
<br><img alt="Декомпозиція адреси — 0x0000_1A3C розділена на тег (18 бітів), індекс (8 бітів) та зміщення (6 бітів)" src="assets/l11-address-decomposition.svg" target="_self" style="width: 650px; max-width: 100%;">Краса такої схеми в тому, що вона взагалі не потребує жодного пошуку. Біти індексу безпосередньо обирають конкретний рядок кешу (або множину), а перевірка тегу зводиться до простого порівняння на рівність. Завдяки цьому звернення до кешу надзвичайно швидке: воно може завершитися за один тактовий цикл.Але є й наслідок: кілька адрес пам'яті можуть відображатися на один і той самий індекс. Наприклад, у нашому кеші на 256 рядків кожна адреса, біти якої [13:6] дорівнюють 0xA3, відображається на рядок 163, а таких адрес — 2^18 = 262 144. Оскільки в кеші з прямим відображенням одночасно лише одна з них може займати рядок 163, решта чекають своєї черги. Це першопричина конфліктних промахів, які ми розглянемо через поняття асоціативності в наступній лекції.Коли процесор виконує звернення до пам'яті (скажімо, інструкцію load), контролер кешу апаратно виконує такі кроки — зазвичай за 1–4 тактових цикли:Крок 1: Витягти біти індексу з адреси пам'яті. Вони обирають, яку множину кешу перевіряти. У нашому кеші з прямим відображенням на 256 рядків це означає вибір одного з 256 рядків.Крок 2: Прочитати метадані рядка кешу. Апаратура зчитує біт дійсності і тег, збережені у вибраному рядку кешу.Крок 3: Перевірити біт дійсності. Якщо він дорівнює 0, цей рядок ніколи не завантажували — гарантований промах. Апаратура пропускає порівняння тегу і переходить до отримання даних з наступного рівня кешу.Крок 4: Порівняти тег. Якщо біт дійсності дорівнює 1, апаратура порівнює збережений тег із тегом, витягнутим із запитуваної адреси. Це порівняння виконує апаратний компаратор — спеціалізована схема, що паралельно перевіряє рівність усіх бітів тегу.Крок 5a: Влучання в кеш. Якщо теги збігаються, потрібні дані — у цьому рядку кешу. Апаратура за бітами зміщення відбирає потрібні байти з 64-байтного блоку даних і передає їх процесору. Звернення завершується із затримкою L1 — зазвичай 1–4 цикли.Крок 5b: Промах кешу. Якщо теги не збігаються (або біт дійсності дорівнював 0), запитуваних даних на цьому рівні немає. Контролер кешу надсилає запит на наступний рівень (L2), де повторюється той самий процес пошуку. Коли дані нарешті надходять (з L2, L3 або основної пам'яті), їх записують у вибраний рядок кешу: блок даних заповнюють, тег оновлюють відповідно до нової адреси, а біт дійсності встановлюють у 1. Якщо раніше в рядку були інші дійсні дані, вони витісняються (і записуються назад на наступний рівень, якщо встановлено біт забруднення).Увесь процес можна узагальнити у вигляді дерева рішень:Адреса → витягти індекс → обрати множину ├─ біт дійсності = 0? → ПРОМАХ → отримати з наступного рівня └─ біт дійсності = 1? ├─ тег збігається? → ВЛУЧАННЯ → використати зміщення для вибору байтів └─ тег не збігається? → ПРОМАХ → отримати, витіснити старий рядок
Ключовий момент: у кеші з прямим відображенням (один рядок на множину) для кожного блоку пам'яті існує лише одне можливе місце. Це робить влучання дуже швидким (треба перевірити тільки один тег), проте підвищує ймовірність конфліктних промахів. Асоціативні кеші, які ми вивчатимемо в наступній лекції, дозволяють мати кілька рядків на множину: це зменшує кількість конфліктів ціною паралельної перевірки більшої кількості тегів.Промахи кешу — ворог продуктивності. Кожен промах коштує десятки, а то й сотні циклів, поки дані завантажуються з повільнішого рівня. Розуміння причин промахів — перший крок до їх зменшення. Архітектори комп'ютерів поділяють промахи кешу на три категорії, відомі як Три C:Обов'язкові промахи (їх також називають холодними промахами або промахами першого звернення) виникають при найпершому зверненні до блоку пам'яті. Оскільки блок ніколи не був у кеші, його там і не може бути: промах неминучий, незалежно від розміру чи організації кешу. Навіть нескінченно великий кеш усе одно зазнаватиме обов'язкових промахів.<br>Промахи ємності виникають, коли <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Working_set" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Working_set" target="_self">робочий набір</a> програми (усі рядки кешу, які вона активно використовує) перевищує загальну ємність кешу. Навіть за відсутності конфліктів відображення кешу просто бракує місця для всіх даних, потрібних програмі одночасно. Один блок витісняється, щоб звільнити місце для іншого, і коли витіснений блок знову знадобиться, виникає промах ємності.Конфліктні промахи (їх ще називають промахами колізії) виникають, коли кілька блоків пам'яті конкурують за одну й ту саму множину кешу. Навіть якщо в кеші загалом достатньо вільних рядків, конкретна множина, на яку вказує адреса, може виявитися заповненою, — а це змушує до витіснення. Конфліктні промахи — це артефакт функції відображення кешу та його обмеженої асоціативності: повністю асоціативний кеш, де будь-який блок може потрапити куди завгодно, за означенням не має конфліктних промахів.Наступна таблиця підсумовує три категорії:На практиці відносна важливість кожного типу залежить від навантаження: Потокові навантаження (одноразове читання великих обсягів даних) — це переважно обов'язкові промахи. Жоден розмір кешу тут не допоможе: ці дані ніколи раніше не зустрічалися. Основний засіб пом'якшення — попереднє завантаження. Ітеративні навантаження (повторні проходи по набору даних) зазнають промахів ємності, коли набір даних перевищує розмір кешу. Тайлінг циклів — техніка з Розділу 3.4 — безпосередньо розв'язує цю проблему, зменшуючи активний робочий набір до блоку розміром із кеш. Навантаження з обходом покажчиків (обхід зв'язаних списків, дерев, геш-таблиць) часто зазнають конфліктних промахів, бо вузли розкидані в пам'яті непередбачувано. Тут можуть допомогти вища асоціативність і кеш-орієнтовані алокатори пам'яті. <br>Розуміння Трьох C дає архітекторам і програмістам систему координат для діагностики та усунення проблем продуктивності. Коли програма працює повільніше, ніж очікувалося, профілювання частоти промахів кешу інструментами на кшталт perf stat (у Linux) чи <a data-tooltip-position="top" aria-label="https://developer.apple.com/instruments/" rel="noopener nofollow" class="external-link is-unresolved" href="https://developer.apple.com/instruments/" target="_self">Instruments</a> (у macOS) показує, що саме є вузьким місцем — обов'язкові, ємнісні чи конфліктні промахи, — і кожен тип підказує свою стратегію оптимізації.Коли CPU вибирає блок з основної пам'яті та розміщує його в кеші, постає фундаментальне питання: куди саме може потрапити цей блок? Відповідь визначає асоціативність кешу — одне з найважливіших проєктних рішень в будь-якій ієрархії кешу. Три класичні відповіді породжують три типи організації кешу, кожен зі своїми компромісами між апаратною складністю, швидкістю пошуку та частотою промахів.Уявімо кеш зі 128 рядками та основну пам'ять, що містить мільйони блоків. Коли вибирається блок номер 5 000, який із 128 рядків має його зберігати? Якщо існує лише одне допустиме місце, апаратура може швидко перевірити цей єдиний рядок — зате зіткнення стають неминучими. Якщо ж блок може потрапити будь-куди, зіткнення зникають — але апаратура мусить перевіряти кожен рядок при кожному зверненні. Асоціативність кешу — це спектр між цими двома крайнощами.У кеші з прямим відображенням кожен блок пам'яті відображається рівно на один рядок кешу. Відображення задається простою формулою з остачею від ділення:cache_line = block_address mod num_lines
Для кешу зі 128 рядками блок 0 відображається на рядок 0, блок 1 — на рядок 1, … блок 127 — на рядок 127, блок 128 знову потрапляє на рядок 0, блок 129 — на рядок 1 і так далі.Декомпозиція адреси. CPU розбиває кожну адресу пам'яті на три поля:| тег | індекс | зміщення | Зміщення (молодші біти): визначає байт у межах блоку. Для блоку розміром 64 байти зміщення становить 6 біт (2⁶ = 64).
Індекс (середні біти): вибирає, який рядок кешу перевіряти. Для 128 рядків це 7 біт (2⁷ = 128).
Тег (старші біти, що залишилися): зберігається разом із даними, щоб кеш міг підтвердити: блок у цьому рядку — саме той, який нам потрібен.
Процес пошуку. При кожному зверненні до пам'яті кеш витягує індекс, переходить безпосередньо до відповідного рядка та порівнює збережений тег із тегом з адреси. Якщо вони збігаються і встановлений біт дійсності — маємо влучання в кеш (cache hit). Інакше — промах кешу (cache miss).Перевага: швидкість і простота. Апаратура перевіряє рівно одне місце, потребуючи лише одного компаратора. Це робить кеш з прямим відображенням найшвидшим за часом доступу.Недолік: конфліктні промахи. Два блоки, що випадково відображаються на один і той самий рядок, не можуть співіснувати в кеші, навіть якщо безліч інших рядків порожні. Це призводить до пробуксовування (thrashing) — повторюваних витіснень, що руйнують продуктивність.Патологічний сценарій. Розгляньмо кеш зі 128 рядками та блоками по 64 байти. Блоки 0 і 128 обидва відображаються на рядок 0 (оскільки 128 mod 128 = 0). Тепер виконаємо таку послідовність звернень:Звернення до блоку 0 → Промах (холодний промах, завантаження блоку 0 у рядок 0)
Звернення до блоку 128 → Промах (конфлікт: витісняє блок 0, завантажує блок 128)
Звернення до блоку 0 → Промах (конфлікт: витісняє блок 128, завантажує блок 0)
Звернення до блоку 128 → Промах (конфлікт: витісняє блок 0, завантажує блок 128)
...
Кожне звернення — промах. Коефіцієнт влучань становить 0%, хоча ми звертаємося лише до двох блоків, а в кеші 128 рядків. Це найгірший сценарій для кешу з прямим відображенням — і в реальних програмах він трапляється частіше, ніж можна було б очікувати.<br><img alt="Кеш з прямим відображенням — блоки 0, 128, 256 відображаються на рядок кешу 0, спричиняючи конфлікти" src="assets/l11-direct-mapped-cache.svg" target="_self" style="width: 650px; max-width: 100%;">На протилежному кінці спектра повністю асоціативний кеш дозволяє будь-якому блоку пам'яті перебувати в будь-якому рядку кешу. В адресі немає поля індексу — лише тег і зміщення:| тег | зміщення |
Процес пошуку. При кожному зверненні кеш мусить порівняти запитуваний тег із тегами усіх рядків одночасно. Це вимагає N паралельних компараторів для кешу з N рядками.Перевага: жодних конфліктних промахів. Якщо в кеші є місце, будь-який блок може бути збережений без витіснення іншого через зіткнення індексів. Єдині промахи — це обов'язкові (перше звернення до блоку) та ємнісні (увесь кеш заповнений і щось потрібно видалити).Недолік: апаратна вартість стає непомірною для великих кешів. Повністю асоціативний кеш на 1 024 рядки потребує 1 024 компараторів, що працюють паралельно, — величезна кількість кремнію та енергії. Затримка порівняння також зростає з розміром кешу.Де використовується. Повністю асоціативна організація застосовується для дуже малих, спеціалізованих кешів, де усунення конфліктних промахів є критичним, а кількість рядків — мінімальною. Найвідоміший приклад — буфер асоціативної трансляції (TLB), кеш для трансляцій віртуальних адрес у фізичні, який зазвичай містить 32–64 записи. Ми розглянемо TLB у Лекції 12.Множинно-асоціативний кеш — це компроміс, що домінує в сучасному апаратному забезпеченні. Кеш поділяється на множини, і кожна множина містить W рядків (так званий W-канальний асоціативний кеш). Блок пам'яті відображається рівно на одну множину, однак усередині цієї множини він може зайняти будь-який із W рядків.Формула відображення:set = block_address mod num_sets
Декомпозиція адреси:| тег | індекс множини | зміщення | Індекс множини вибирає множину (так само, як індекс у кеші з прямим відображенням, але з меншою кількістю біт, адже множин менше, ніж рядків).
Тег порівнюється з усіма W тегами у вибраній множині за допомогою W паралельних компараторів.
Варто зазначити, що крайні випадки є окремими варіантами цієї ж схеми:
1-канальний множинно-асоціативний = кеш з прямим відображенням (кожна «множина» має 1 рядок, тому множин стільки ж, скільки рядків).
N-канальний множинно-асоціативний з 1 множиною = повністю асоціативний кеш (усі рядки в одній множині, тому будь-який блок може потрапити куди завгодно).
Сучасні CPU використовують помірну асоціативність як золоту середину:
Кеш L1: зазвичай 8-канальний множинно-асоціативний
Кеш L2: зазвичай 8-канальний множинно-асоціативний
Кеш L3: зазвичай 12-канальний або 16-канальний множинно-асоціативний
Приклад 2-канального множинно-асоціативного кешу. Розгляньмо конкретний сценарій. Нехай наш кеш має:
4 множини (множина 0 — множина 3)
2 рядки на множину (2-канальний)
Блоки по 64 байти
Загальний розмір кешу: 4 × 2 × 64 = 512 байт. Блоки відображаються на множини за формулою set = block_address mod 4.Блок 0 → множина 0 Блок 4 → множина 0 Блок 8 → множина 0
Блок 1 → множина 1 Блок 5 → множина 1 Блок 9 → множина 1
Блок 2 → множина 2 Блок 6 → множина 2 Блок 10 → множина 2
Блок 3 → множина 3 Блок 7 → множина 3 Блок 11 → множина 3
Тепер простежимо за послідовністю звернень:Звернення до блоку 0 → Промах (множина 0 порожня, завантаження у канал 0) Множина 0: [Блок 0] [ порожньо ] Звернення до блоку 4 → Промах (множина 0 має місце, завантаження у канал 1) Множина 0: [Блок 0] [Блок 4] Звернення до блоку 0 → Влучання! (знайдено у множині 0, канал 0) Множина 0: [Блок 0] [Блок 4]
Блоки 0 і 4 обидва відображаються на множину 0, проте вони мирно співіснують — кожен займає один із двох каналів. У кеші з прямим відображенням звернення до блоку 4 витіснило б блок 0, і третє звернення обернулося б промахом. 2-канальна конструкція усуває цей конфлікт.А що відбувається, коли третій блок відображається на ту саму множину?Звернення до блоку 8 → Промах (множина 0 заповнена — потрібно витіснити блок 0 або блок 4) Множина 0: [Блок 8] [Блок 4] (припускаємо, що блок 0 витіснено політикою заміщення)
Тепер кеш мусить обрати жертву — який із двох рядків у множині 0 витіснити. Цей вибір визначається політикою заміщення, яку ми розглянемо в Розділі 6.<br><img alt="Порівняння асоціативності — кеш з прямим відображенням (1-канальний), 2-канальний множинно-асоціативний та повністю асоціативний для 8 рядків кешу" src="assets/l11-associativity-comparison.svg" target="_self" style="width: 650px; max-width: 100%;">Наведена нижче таблиця узагальнює ключові компроміси на всьому спектрі асоціативності:Збільшення асоціативності зменшує кількість конфліктних промахів, але збільшує апаратні витрати і може підвищувати затримку доступу. Золота середина для кешів загального призначення — від 4-канальних до 16-канальних — залишається надзвичайно стабільною протягом кількох десятиліть проєктування процесорів.Асоціативність кешу взаємодіє непомітно й небезпечно зі структурами даних, розміри яких є степенями двійки. Розгляньмо кеш з прямим відображенням (або будь-який множинно-асоціативний кеш) із множинами, що індексуються молодшими бітами адреси блоку. Коли крок доступу до вашого масиву є степенем двійки, кратним кількості множин, кожне звернення потрапляє в ту саму множину — фактично зменшуючи корисний кеш до W рядків (асоціативності однієї множини).Крок 256 проти кроку 257. Припустимо, ми ітеруємо по великому масиву з кроком 256 елементів (кожен елемент — int, тож крок становить 1 024 байти). Якщо кеш має 64 множини з блоками по 64 байти, індекс множини залежить від бітів 6–11 адреси. Крок у 1 024 байти означає, що індекс множини зсувається на 1 024 / 64 = 16 множин за кожен крок. Оскільки 16 ділить 64, шаблон доступу циклічно проходить лише через 4 унікальні множини — повністю марнуючи 60 із 64 множин. Продуктивність може впасти у 10 і більше разів порівняно з кроком 257 (який є взаємно простим із будь-яким степенем двійки та розподіляє звернення по всіх множинах).// Патологічний випадок: крок є степенем двійки
for (int i = 0; i &lt; N; i += 256) sum += array[i]; // Потрапляє в ті самі множини кешу // Здоровий варіант: крок уникає вирівнювання за степенем двійки
for (int i = 0; i &lt; N; i += 257) sum += array[i]; // Розподіляється по всіх множинах
Двійковий пошук у масивах розміром у степінь двійки. Двійковий пошук особливо вразливий. У відсортованому масиві розміром 2²⁰ перше порівняння завжди звертається до елемента 2¹⁹, друге — до 2¹⁸ або 2¹⁹ + 2¹⁸, і так далі. Ці адреси мають спільні молодші біти, через що багато з них відображаються на ту саму невелику групу множин кешу. Вимірювання показують, що двійковий пошук у масиві з 2²⁰ елементів може займати близько 360 нс на запит, тоді як такий самий пошук у масиві з 2²⁰ + 123 елементів — тривіально більшому масиві — займає лише близько 300 нс на запит. Непарний розмір руйнує шаблон вирівнювання та розподіляє звернення більш рівномірно.Практичні рішення:
Доповнюйте розміри масивів. Якщо ваш двовимірний масив — int matrix[1024][1024], змініть його на int matrix[1024][1024 + 16]. Додаткові 16 стовпців руйнують вирівнювання та різко зменшують конфлікти множин.
Уникайте точних степенів двійки для розмірів часто шуканих структур даних. Додавання навіть невеликої константи (хоча б +1) до розміру масиву може усунути патологічний шаблон.
Розумійте геометрію власного кешу. Знання кількості множин, розміру блоку та асоціативності дає змогу передбачити, які кроки спричинять проблеми.
Ця пастка — яскрава ілюстрація того, чому розуміння архітектури кешу має значення для продуктивності; тема, яка пронизує все наше вивчення систем пам'яті.Коли стається промах кешу і потрібно завантажити нові дані, кеш змушений витіснити якийсь наявний блок, щоб звільнити місце. У кеші з прямим відображенням вибору немає — поле індексу однозначно вказує, який рядок буде перезаписано. Але в будь-якому асоціативному кеші (множинно-асоціативному чи повністю асоціативному) апаратура має вирішити, яким із W рядків цільової множини пожертвувати. Саме це рішення і визначає політика заміщення.Хороша політика заміщення утримує в кеші ті блоки, які з найбільшою ймовірністю знадобляться знову в найближчому майбутньому, мінімізуючи майбутні промахи. Погана — викидає саме ті дані, що знадобляться за мить. Розгляньмо три найпоширеніші політики.Політика LRU (найдавніше використаний) витісняє той рядок кешу, до якого найдовше не зверталися. Основна ідея — пряме застосування часової локальності: якщо до блоку давно не зверталися, то й найближчим часом він навряд чи знадобиться.Реалізація. У 2-канальному множинно-асоціативному кеші відстежувати LRU тривіально: один біт на множину запам'ятовує, до якого з двох каналів зверталися останнім, а при витісненні апаратура обирає інший канал. Для 4-канального кешу потрібно вже кілька біт на множину, щоб зберігати повний порядок чотирьох каналів. Для 8-канального і вище точний LRU стає надто дорогим: він вимагає розрізняти 8! = 40 320 можливих порядків на множину, а це непрактично для швидкої апаратури.Псевдо-LRU. Сучасні процесори з 8- або 16-канальними кешами користуються наближенням до LRU, яке часто називають псевдо-LRU (або деревоподібним PLRU). Двійкове дерево бітів звужує вибір до «приблизно найдавніше використаного» рядка всього за кілька перевірок бітів. Цього наближення достатньо, щоб отримати більшу частину переваг справжнього LRU за частку його вартості.Переваги. LRU дає найкращий середній коефіцієнт влучань для широкого спектру навантажень. Це політика, з якою порівнюють усі інші.Слабкість. LRU можна обдурити сканувальними патернами — циклом, що ітерує по робочому наборі, трохи більшому за кеш. Кожен прохід циклу витісняє саме ті рядки, які знадобляться на наступному проході, і коефіцієнт влучань падає до 0%. Справжній LRU щоразу сумлінно викидає «найстаріший» рядок — а це і є саме неправильний вибір для такого циклічного патерну доступу.Політика FIFO (першим прийшов — першим вийшов) витісняє той рядок кешу, який було завантажено найраніше, незалежно від того, чи зверталися до нього нещодавно. Уявімо собі чергу: перший блок, що потрапив у множину, першим її й залишає.Реалізація. Один циклічний покажчик на кожну множину вказує на наступну жертву. При кожному витісненні покажчик просувається до наступного каналу. Це дешевше за LRU, бо не треба оновлювати стан відстеження при кожному влученні — лише при заміщеннях.Проблема. FIFO повністю ігнорує повторне використання. Блок, завантажений давно, але до якого звертаються кожного такту, усе одно буде витіснений — просто тому, що він першим потрапив у чергу. Через це FIFO поступається LRU на навантаженнях із сильною часовою локальністю.Аномалія Беладі. FIFO має контрінтуїтивну властивість, відому як аномалія Беладі: збільшення розміру кешу (додавання більшої кількості рядків) іноді може збільшити коефіцієнт промахів. Уперше це показав Ласло Беладі у 1969 році. Хоч аномалія — радше теоретична цікавинка, ніж часта практична проблема, вона підкреслює фундаментальну слабкість будь-якої політики, що нехтує давністю доступу. Цікаво, що LRU від аномалії Беладі не страждає: це стековий алгоритм, тобто множина блоків у кеші розміром N завжди є підмножиною блоків у кеші розміром N+1.Найпростіша з усіх політик: при витісненні обрати жертву випадково. Жодного стану відстеження, жодної логіки оновлення, жодних бітів віку — лише апаратний генератор випадкових чисел (або простий LFSR-лічильник), що обирає канал.Переваги. Випадкове заміщення має нульові накладні витрати на облік і тривіально просте в реалізації. Ще дивніше — воно працює напрочуд добре: зазвичай у межах 10–20% від коефіцієнта влучань LRU на широкому спектрі навантажень. До того ж його неможливо обдурити патологічними патернами доступу: оскільки вибір випадковий, жодна конкретна послідовність звернень не спричинятиме стабільно найгіршу поведінку.Реальне застосування. Процесори ARM Cortex-A у своїх кешах застосовували саме випадкове заміщення. Коли апаратний бюджет обмежений, а навантаження непередбачуване, випадковий вибір — прагматичне рішення.Для більшості процесорів загального призначення псевдо-LRU дає найкращий баланс: він охоплює переважну більшість переваг LRU при апаратній складності, ближчій до FIFO. Випадкове заміщення залишається привабливим варіантом для вбудованих та мобільних рішень, де бюджет транзисторів обмеженіший.Політика заміщення працює в парі з асоціативністю. Вища асоціативність дає політиці заміщення більше кандидатів на вибір, зменшуючи конфліктні промахи, — але водночас ускладнює й саме рішення про заміщення. Для певних навантажень 16-канальний кеш із псевдо-LRU та 4-канальний кеш зі справжнім LRU можуть показувати подібні результати. Як і з більшістю архітектурних рішень, правильна відповідь залежить від обмежень: площі кристала, бюджету потужності, цільового навантаження та місця кешу в загальній ієрархії пам'яті.Коли CPU зчитує з пам'яті, логіка кешу доволі прямолінійна: перевірити кеш, повернути дані при влученні або завантажити з наступного рівня при промаху. А от запис породжує фундаментальне питання, на яке немає єдиної правильної відповіді — і вибір між двома основними стратегіями визначив конструкцію кожної ієрархії кеш-пам'яті, побудованої за останні чотири десятиліття.Коли CPU виконує інструкцію збереження за адресою, блок якої вже лежить у кеші, існують дві копії даних: одна в кеші та одна в основній пам'яті. Збереження має оновити кеш — це швидко й безсуперечно. Але коли оновлювати основну пам'ять? Негайно чи пізніше? Відповідь визначає політику запису кешу, і кожен варіант тягне за собою глибокі наслідки для продуктивності, узгодженості та складності апаратного забезпечення.У кеші з наскрізним записом кожне збереження одночасно оновлює і рядок кешу, і відповідну комірку основної пам'яті. Принцип простий: після кожного запису кеш і пам'ять містять однакові дані. Пам'ять завжди узгоджена.Ця простота має справжню інженерну цінність. Відновлення після збою стає тривіальним — пам'ять уже відображає всі завершені записи. Когерентність кешу в багатопроцесорних системах забезпечити легше, бо застарілі дані не ховаються в кеші без відповідної копії в пам'яті. Налагодження стає передбачуванішим: пам'ять можна перевірити будь-якої миті й бути певним, що вона відображає нещодавній стан програми.Ціна — це пропускна здатність. Розглянемо цикл, що збільшує лічильник 100 разів:for (int i = 0; i &lt; 100; i++) { counter++;
}
При наскрізному записі кожне зі 100 збільшень породжує запис в основну пам'ять — хоча решті системи потрібне лише кінцеве значення. Якщо запис в основну пам'ять займає 200 тактів, цикл витрачає до 20 000 тактів на очікування пам'яті, попри те що саме обчислення тривіальне.Буфери запису пом'якшують цю проблему. Буфер запису — це невелика черга FIFO (зазвичай 4–16 записів), розташована між кешем і основною пам'яттю. Коли CPU виконує запис, дані потрапляють одночасно в кеш і в буфер запису. CPU негайно продовжує роботу; контролер пам'яті спорожнює буфер у фоновому режимі, відправляючи записи до пам'яті в міру доступності шини. Поки буфер не заповнений, CPU не зупиняється через записи. Проте якщо частота записів тимчасово перевищує швидкість спорожнення, буфер заповнюється і CPU змушений чекати — це явище називають затримкою через переповнення буфера запису.У кеші зі зворотним записом збереження оновлюють лише кеш. Рядок кешу позначається як забруднений: у його метаданих встановлюється спеціальний біт забруднення. Основна пам'ять взагалі не оновлюється під час запису. Забруднений рядок залишається в кеші, потенційно накопичуючи багато записів, аж поки його зрештою не витіснять.Коли забруднений рядок треба витіснити (наприклад, щоб звільнити місце для нового блоку), контролер кешу перевіряє біт забруднення. Якщо рядок забруднений, контролер перед заміною записує увесь блок назад у пам'ять. Якщо рядок чистий (не модифікований з моменту завантаження), його просто відкидають — пам'ять уже містить актуальну копію.Виграш у продуктивності суттєвий. Той самий цикл зі 100 ітераціями для лічильника породжує рівно один запис у пам'ять — коли рядок кешу з counter зрештою витісниться. Один запис замість 100. Для програм з інтенсивним записом зворотний запис здатен зменшити трафік до пам'яті на порядок порівняно з наскрізним записом.Ціна складності не менш реальна: Відстеження біта забруднення: кожен рядок кешу потребує додаткового біта, а шлях витіснення має обробляти послідовність «записати назад, потім замінити». Затримка витіснення: витіснення забрудненого рядка триває довше, ніж витіснення чистого, бо блок спершу потрібно записати в пам'ять. У деяких реалізаціях застосовують буфер зворотного запису, щоб поглинути витіснення без зупинки вхідного заповнення. Когерентність кешу: у багатоядерних системах забруднений рядок кешу одного ядра може містити єдину актуальну копію певної адреси. Протоколи когерентності (як-от MESI, що його ми розглядали в Л08) мають обробляти випадок, коли інше ядро запитує дані, які існують лише в забрудненому рядку кешу віддаленого ядра. Попри цю складність, зворотний запис — домінантна політика в сучасних CPU. Intel Core, AMD Zen і ARM Cortex-A — усі використовують зворотний запис на кожному рівні кешу. Економія пропускної здатності завелика, щоб нею нехтувати у високопродуктивних реалізаціях.Політика запису також визначає, що відбувається при промаху запису — збереженні за адресою, блок якої наразі відсутній у кеші. Варіантів два:Запис з розміщенням (також відомий як fetch-on-write): кеш спершу завантажує весь блок з пам'яті, а потім виконує запис у кешовану копію. Мотивація — просторова локальність: якщо програма записує одне слово в блоці, вона, ймовірно, незабаром звернеться й до сусідніх слів. Завантажений блок буде напоготові для цих майбутніх звернень.Запис без розміщення (також відомий як write-around): запис іде безпосередньо в основну пам'ять, цілком оминаючи кеш. Блок не завантажується. Це доцільно, коли записи мають погану локальність — наприклад, ініціалізація великого масиву, який не зчитуватимуть одразу. Завантаження таких блоків у кеш лише витіснило б кориснішу інформацію.Ці політики обробки промахів природно поєднуються з політиками запису: Зворотний запис + запис з розміщенням: стандартна комбінація для сучасних CPU. Оскільки зворотний запис і так уникає відправлення записів у пам'ять, є сенс завантажувати блоки з промахами в кеш, де подальші записи залишатимуться локальними. Наскрізний запис + запис без розміщення: записи однаково потрапляють у пам'ять, тож мало сенсу кешувати блок з промахом лише тому, що в нього відбувся запис. Ці поєднання — усталені конвенції, а не закони: інші комбінації технічно можливі, але на практиці трапляються рідко.<br><img alt="Порівняння політик запису — наскрізний запис оновлює і кеш, і пам'ять; зворотний запис записує лише в кеш з бітом забруднення" src="assets/l11-write-policies.svg" target="_self" style="width: 650px; max-width: 100%;">Ключовий висновок: наскрізний запис віддає перевагу простоті та узгодженості; зворотний запис віддає перевагу продуктивності. Сучасні CPU загального призначення обирають продуктивність і вкладають додаткові транзистори в керування складністю.У Лекції 8 ми запровадили формулу середнього часу доступу (AAT) і побачили, як додавання рівнів кешу різко зменшує ефективну затримку пам'яті. Тепер формалізуймо цей інструмент — відомий серед архітекторів як формула AMAT (Average Memory Access Time — середній час доступу до пам'яті) — і застосуймо його для кількісного аналізу компромісів у проєктуванні кешу та оптимізації програмного забезпечення.Основна ідея AMAT полягає в тому, що середній час доступу до пам'яті — це зважена комбінація швидкого шляху (влучення в кеш) і повільного шляху (промах кешу):AMAT = Час влучення + Коефіцієнт промахів × Штраф промаху
Кожен доданок має точне значення: Час влучення: час, потрібний, щоб звернутися до кешу й визначити, чи запит є влученням чи промахом. Для сучасного кешу даних L1 це зазвичай 1–4 тактові цикли (близько 1 нс на частоті 3–4 ГГц). Коефіцієнт промахів: частка звернень до пам'яті, які не знайдено в кеші. Для добре спроєктованого L1 це зазвичай 2–10 % залежно від навантаження. Штраф промаху: додатковий час, потрібний, щоб завантажити блок із наступного рівня ієрархії, коли стався промах. Якщо наступний рівень — L2, це близько 10 тактів; якщо основна пам'ять — 200 і більше тактів. Простий приклад наочно демонструє потужність кешування:Час влучення L1 = 1 такт
Коефіцієнт промахів L1 = 5%
Штраф промаху = 100 тактів (завантаження з основної пам'яті, без L2) AMAT = 1 + 0.05 × 100 = 1 + 5 = 6 тактів
Один рівень кешу з коефіцієнтом влучань 95 % перетворює 100-тактову пам'ять на ефективну 6-тактову — прискорення у 16,7 раза. Формула кількісно виражає те, що ми й так відчуваємо інтуїтивно: навіть помітний коефіцієнт промахів прийнятний, якщо він дозволяє більшість часу уникати величезного штрафу.Проте формула виявляє і дещо тривожне: 5 % промахів дають 83 % ефективного часу доступу (5 із 6 тактів). Це прояв закону Амдала в застосуванні до пам'яті — продуктивність визначається повільним шляхом, зваженим за частотою його появи.У Лекції 8 ми класифікували промахи кешу на три категорії. Тепер пов'яжімо кожну з них безпосередньо зі стратегіями оптимізації AMAT:Обов'язкові промахи (холодні промахи) виникають при першому зверненні до блоку. Їх зазнає будь-яка програма, адже кеш стартує порожнім. Як зменшити обов'язкові промахи: Апаратне попереднє завантаження: контролер кешу розпізнає послідовні або крокові патерни доступу й завантажує блоки наперед, ще до запиту CPU. Це приховує штраф промаху, хоча не усуває сам промах. Програмне попереднє завантаження: програміст (чи компілятор) вставляє інструкції попереднього завантаження, які підказують апаратному забезпеченню, які адреси знадобляться найближчим часом. Більші блоки кешу: кожен блок охоплює більше адрес, тому на одиницю даних припадає менше обов'язкових промахів. Проте більші блоки збільшують штраф промаху (треба завантажити більше даних) і можуть марнувати пропускну здатність на непотрібні байти. Промахи ємності виникають, коли кеш не може вмістити всіх даних, яких потребує робочий набір програми. Блоки витісняються і згодом мусять бути перезавантажені. Як зменшити промахи ємності: Більші кеші: більша ємність — більша частина робочого набору вміщується. Але більші кеші фізично більші, тобто довші затримки на провідниках і вищий час влучення — прямий компроміс у формулі AMAT. Алгоритмічна реструктуризація: прийоми на кшталт тайлінгу циклів (блокування) перебудовують обчислення так, щоб робочий набір у кожен момент вміщувався в кеш. Це суто програмна оптимізація без жодних апаратних витрат. Конфліктні промахи виникають у множинно-асоціативних кешах чи кешах з прямим відображенням, коли кілька активних адрес відображаються на одну й ту саму множину й витісняють одна одну, навіть якщо в інших множинах є невикористана ємність. Як зменшити конфліктні промахи: Вища асоціативність: 8-канальний множинно-асоціативний кеш має помітно менше конфліктних промахів, ніж кеш з прямим відображенням того самого розміру. Але вища асоціативність дорожча апаратно (більше компараторів) і може підвищити час влучення. Кеш жертв: невеликий повністю асоціативний буфер, що перехоплює рядки, витіснені з основного кешу через конфлікти. Якщо витіснений рядок запитують знову, його знаходять у кеші жертв, не звертаючись до наступного рівня. Наскрізна думка: кожна оптимізація, що зменшує коефіцієнт промахів, має свою ціну — зазвичай це довший час влучення або апаратна складність. Формула AMAT змушує оцінювати такі компроміси кількісно, а не покладатися на інтуїцію.Сучасні процесори мають не один кеш, а цілу ієрархію: L1, L2, L3, а далі основна пам'ять. Кожен рівень виконує роль «наступного рівня» для рівня над ним. Формула AMAT природно розширюється через вкладення:AMAT = ЧасВлучення_L1 + КоефПромахів_L1 × (ЧасВлучення_L2 + КоефПромахів_L2 × (ЧасВлучення_L3 + КоефПромахів_L3 × ЧасДоступуДоПам'яті))
Штраф промаху для кожного рівня — це AMAT рівня під ним; така рекурсивна структура вкладає всю ієрархію в одне рівняння.Розрахунковий приклад із реалістичними параметрами для сучасного настільного CPU:Обчислюємо від внутрішнього рівня до зовнішнього:Штраф промаху L3 = 200 тактів (час доступу до RAM)
Внесок L3 = 30 + 0.40 × 200 = 30 + 80 = 110 тактів Штраф промаху L2 = AMAT L3 = 110 тактів
Внесок L2 = 10 + 0.20 × 110 = 10 + 22 = 32 такти Штраф промаху L1 = AMAT L2 = 32 такти
AMAT = 1 + 0.05 × 32 = 1 + 1.6 = 2.6 такти
<br><img alt="Розрахунок багаторівневого AMAT — три рівні кешу зменшують 200-тактову RAM до 2,6-тактового ефективного доступу" src="assets/l11-amat-multilevel.svg" target="_self" style="width: 650px; max-width: 100%;">Трирівнева ієрархія кешу перетворила 200-тактову основну пам'ять на ефективну систему з часом 2,6 такту — прискорення у 77 разів. Простежмо, як свій внесок робить кожен рівень: 95 % звернень обслуговує L1 за 1 такт. 4 % звернень (5 %, що промахнулися в L1, з яких 80 % влучили в L2) обслуговує L2 за 1 + 10 = 11 тактів. 0,6 % звернень (1 %, що промахнулися в L2, з яких 60 % влучили в L3) обслуговує L3 за 1 + 10 + 30 = 41 такт. 0,4 % звернень проходять увесь шлях до RAM ціною 1 + 10 + 30 + 200 = 241 такт. Лише 0,4 % звернень сягають основної пам'яті, проте ця мізерна частка все одно додає 0,004 × 241 ≈ 0,96 такту до AMAT — майже стільки ж, як власне час влучення L1. Ось чому архітектори переймаються кожною часткою відсотка в коефіцієнтах промахів.Обговорюючи багаторівневі кеші, важливо розрізняти два означення коефіцієнта промахів:Локальний коефіцієнт промахів: кількість промахів на рівні N, поділена на кількість звернень до рівня N. Цей показник оцінює, наскільки добре працює конкретний рівень кешу з огляду на трафік, який він отримує.Глобальний коефіцієнт промахів: кількість промахів на рівні N, поділена на загальну кількість звернень CPU до пам'яті. Цей показник показує, яка частка всіх запитів CPU виходить за межі рівня N.Зв'язок між ними простий — глобальні коефіцієнти промахів перемножуються:Глобальний коефіцієнт промахів на L2 = КоефПромахів_L1 × ЛокальнийКоефПромахів_L2
Глобальний коефіцієнт промахів на L3 = КоефПромахів_L1 × ЛокальнийКоефПромахів_L2 × ЛокальнийКоефПромахів_L3
Використовуючи наш розрахунковий приклад:Локальний коефіцієнт промахів L2 (20 %) звучить тривожно — кожне п'яте звернення до L2 промахується. Та L2 отримує лише 5 % звернень (тих, що промахнулися в L1), тож його глобальний коефіцієнт промахів становить усього 1 %. Локальні коефіцієнти описують ефективність окремого рівня кешу; глобальні описують поведінку системи загалом.Під час обчислення AMAT вкладена формула природно використовує локальні коефіцієнти промахів. Натомість для оцінки вимог до пропускної здатності пам'яті чи порівняння систем інформативніші глобальні коефіцієнти — вони показують, яка частка операцій CPU справді генерує трафік до певного рівня.Формула AMAT має три важелі: час влучення, коефіцієнт промахів і штраф промаху. Кожна оптимізація кешу націлена на один із них — часто ціною іншого. Таблиця нижче підсумовує основні стратегії:Звернімо увагу на ключове протиріччя: збільшення кешу зменшує коефіцієнт промахів, але може збільшити час влучення. Формула AMAT допомагає розв'язувати такі компроміси кількісно. Скажімо, подвоєння кешу L1 може зменшити коефіцієнт промахів з 5 % до 3 %, але збільшити час влучення з 1 до 2 тактів:Оригінал: AMAT = 1 + 0.05 × 100 = 6.0 тактів
Більший L1: AMAT = 2 + 0.03 × 100 = 5.0 тактів
У цьому випадку більший кеш виграє. Але якщо час влучення зросте аж до 3 тактів:Більший L1 (3-тактове влучення): AMAT = 3 + 0.03 × 100 = 6.0 тактів
Тепер виграш зникає. Формула перетворює інтуїтивні аргументи на кількісні рішення.Формула AMAT — це не лише інструмент проєктувальника апаратного забезпечення; вона пояснює, чому продуктивність програм кардинально відрізняється залежно від патернів доступу до пам'яті. Розгляньмо дві реалізації множення матриць:Наївна реалізація (рядок × стовпець):// C[i][j] = сума A[i][k] * B[k][j]
for (int i = 0; i &lt; N; i++) for (int j = 0; j &lt; N; j++) for (int k = 0; k &lt; N; k++) C[i][j] += A[i][k] * B[k][j];
Доступ до B[k][j] проходить крізь пам'ять із кроком N елементів на ітерацію (обхід стовпця у масиві з порядком по рядках). Для великої матриці кожне звернення до B промахується в L1, а ймовірно, і в L2. Ефективний AMAT для звернень до B виходить високим.Реалізація з тайлінгом (блокуванням):for (int ii = 0; ii &lt; N; ii += BLOCK) for (int jj = 0; jj &lt; N; jj += BLOCK) for (int kk = 0; kk &lt; N; kk += BLOCK) for (int i = ii; i &lt; ii + BLOCK; i++) for (int j = jj; j &lt; jj + BLOCK; j++) for (int k = kk; k &lt; kk + BLOCK; k++) C[i][j] += A[i][k] * B[k][j];
Обравши BLOCK так, щоб підматриця B розміром BLOCK × BLOCK вміщувалася в L1, ми гарантуємо, що всі звернення до B у внутрішньому циклі влучають у кеш. Коефіцієнт промахів різко падає, а AMAT для B наближається до часу влучення.Обидві реалізації виконують буквально ту саму арифметику — однакову кількість множень і додавань. Проте версія з тайлінгом на великих матрицях може працювати у 5–10 разів швидше — суто завдяки поведінці кешу. Формула AMAT пояснює чому: у наївній версії AMAT визначається штрафом промаху (повільно), тоді як у версії з тайлінгом AMAT наближається до часу влучення (швидко).Зв'язок із лабораторною P11: у наступній лабораторній роботі ми використовуватимемо Linux perf stat для вимірювання реальних лічильників промахів кешу на живих програмах. Інструмент повідомляє cache-references (загальна кількість звернень до кешу останнього рівня) і cache-misses (промахи на останньому рівні), що дозволяє обчислити емпіричні коефіцієнти промахів. На власні очі ми побачимо, як різні патерни доступу дають кардинально різні коефіцієнти промахів — і як ці коефіцієнти безпосередньо переходять у різницю в продуктивності, саме так, як передбачає формула AMAT.Будь-який сучасний процесор — багатоядерний. Кожне ядро має власний приватний кеш L1 (а зазвичай і приватний кеш L2), тож кілька ядер можуть одночасно зберігати копії однієї й тієї ж адреси пам'яті. Звідси постає фундаментальна проблема: щойно одне ядро модифікує свою кешовану копію, копії інших ядер стають застарілими — а самі вони про це не здогадуються.Розглянемо конкретний сценарій. Два ядра кешують спільну змінну counter, значення якої в основній пам'яті дорівнює 42: Ядро 0 читає counter → зчитує 42 і зберігає значення у своєму кеші L1. Ядро 1 читає counter → зчитує 42 і зберігає значення у своєму кеші L1. Ядро 0 інкрементує: counter = 43. Цей запис потрапляє лише в кеш L1 ядра 0. Ядро 1 читає counter зі свого кешу L1 → бачить застаріле значення 42. Ядро 1 непомітно прочитало неактуальне значення. Жодного винятку, коду помилки чи аварійного завершення — лише неправильна відповідь. Це і є проблема когерентності кешу, і, ймовірно, вона гірша за аварійне завершення, адже програма продовжує виконуватися з пошкодженими даними.Апаратура має розв'язувати цю проблему прозоро: програміст не повинен вручну скидати чи інвалідувати кеші, щоб отримати коректну поведінку. (Варто зазначити, що когерентність кешу — не те саме, що впорядкування пам'яті та бар'єри пам'яті, які визначають порядок, у якому записи стають видимими. Впорядкування пам'яті — поглиблена тема, що виходить за межі цього курсу.)<br><img alt="Проблема когерентності кешу — Ядро 0 записує 43 у свій кеш, тоді як Ядро 1 досі бачить застаріле значення 42" src="assets/l11-coherence-problem.svg" target="_self" style="width: 650px; max-width: 100%;">Розробники апаратного забезпечення створили дві основні родини протоколів когерентності:Протоколи з прослуховуванням (Snooping) — кожен кеш безперервно стежить (або «прослуховує») спільну шину, що з'єднує всі ядра з основною пам'яттю. Коли якесь ядро виконує запис, транзакція на шині видима всім іншим кешам. Кожен контролер кешу перевіряє, чи містить він копію відповідної адреси, і, якщо так, вживає потрібних заходів — інвалідує свою копію або оновлює її. Прослуховування просте в реалізації та швидке для невеликої кількості ядер, але має принципове обмеження масштабованості: кожну транзакцію пам'яті доводиться розсилати всім кешам. Зі зростанням кількості ядер шина стає вузьким місцем пропускної здатності. Прослуховування добре працює приблизно до 32–64 ядер.Протоколи на основі каталогу — замість розсилання централізований каталог (зазвичай розташований у контролері пам'яті або розподілений між вузлами пам'яті) відстежує, які ядра наразі мають копії кожного рядка кешу. Коли ядро виконує запис, каталог надсилає цільові повідомлення про інвалідацію лише тим ядрам, які дійсно мають копії. Це усуває вузьке місце розсилання та дає змогу масштабуватися до сотень чи навіть тисяч ядер. Компромісом стає додаткова затримка на пошук у каталозі. Сучасні серверні процесори — Intel Xeon (з інтерконектом UPI) та AMD EPYC (з Infinity Fabric) — використовують саме протоколи на основі каталогу.У цьому курсі зосередимося на протоколі MESI з шинним прослуховуванням — стандартному протоколі когерентності, який застосовують у споживчих багатоядерних процесорах.Протокол MESI приписує один із чотирьох станів кожному рядку кешу в приватному кеші кожного ядра. Назва «MESI» — просто абревіатура цих чотирьох станів: Modified, Exclusive, Shared і Invalid.Стан Modified (M) — у цього ядра єдина кешована копія рядка, і дані вже змінені (рядок брудний). Основна пам'ять містить застарілу версію. Якщо якесь інше ядро запитає ці дані, саме це ядро відповідає за їх надання — або записавши рядок назад у пам'ять, або через прямий міжкешевий обмін. Неформально: «Я це змінив, і більше ні в кого цього немає.»Стан Exclusive (E) — у цього ядра єдина кешована копія, але дані не змінювалися (рядок чистий). Основна пам'ять актуальна. Ключова властивість стану Exclusive полягає в тому, що наступний локальний запис переведе рядок у стан Modified тихо — з нульовим трафіком на шині. Неформально: «У мене єдина чиста копія.»Стан Shared (S) — одне чи кілька інших ядер теж можуть мати копії цього рядка. Усі копії чисті та узгоджені з основною пам'яттю. Рядок фактично доступний лише для читання: якщо це ядро захоче виконати запис, воно має спершу розіслати інвалідацію всім іншим власникам копій. Неформально: «Інші теж можуть це мати, і поки що лише для читання.»Стан Invalid (I) — цей рядок кешу не містить дійсних даних. Можливо, його було витіснено, він ніколи не завантажувався або був інвалідований через запис іншого ядра. Будь-яке звернення до рядка у стані Invalid — це промах кешу, і дані доведеться підтягнути з пам'яті або з іншого кешу. Неформально: «Моя копія зникла або застаріла.»Властивості кожного стану зведено в таблицю:<br><img alt="Діаграма станів MESI — чотири стани з переходами, що ініціюються локальними читаннями/записами та прослуханими подіями на шині" src="assets/l11-mesi-state-diagram.svg" target="_self" style="width: 650px; max-width: 100%;">Розберімо п'ять сценаріїв крок за кроком. Візьмімо три ядра (Ядро 0, Ядро 1, Ядро 2) та змінну X, яка спочатку є лише в основній пам'яті — жоден кеш її копії не містить.Сценарій 1: Ядро 0 читає X (ні в кого немає копії)
Ядро 0 видає запит BusRd (читання з шини) для адреси X.
Жоден інший кеш не відповідає (ні в кого X у кеші немає).
Пам'ять надає дані.
Ядро 0 завантажує X у свій кеш у стан E (Exclusive) — воно знає, що є єдиним власником, бо жоден інший кеш не сигналізував про влучання. *Результат: Ядро 0 = E*
Сценарій 2: Ядро 1 читає X (у Ядра 0 — стан Exclusive)
Ядро 1 видає BusRd для X.
Контролер кешу Ядра 0 прослуховує цей BusRd. Він містить X у стані E, тому відповідає — чи то надаючи дані безпосередньо через міжкешевий обмін, чи то поступаючись правом відповіді пам'яті.
Тепер і Ядро 0, і Ядро 1 мають чисті копії. Обидва переходять у стан S (Shared). *Результат: Ядро 0 = S, Ядро 1 = S*
Сценарій 3: Ядро 0 записує X (в обох — стан Shared)
Ядро 0 хоче модифікувати X. Оскільки його копія у стані S, тихо записати неможливо — в інших кешах теж можуть бути копії.
Ядро 0 видає BusUpgr (оновлення через шину) — запит на ексклюзивне володіння без повторного читання даних.
Ядро 1 прослуховує BusUpgr та інвалідує свою копію: S → I.
Ядро 0 тепер має єдину копію й переходить зі S → M (Modified). Потім виконує запис. *Результат: Ядро 0 = M, Ядро 1 = I*
Сценарій 4: Ядро 2 читає X (у Ядра 0 — стан Modified)
Ядро 2 видає BusRd для X.
Ядро 0 прослуховує цей BusRd. Його копія у стані M (брудна) — основна пам'ять застаріла. Ядро 0 мусить втрутитися.
Ядро 0 записує брудні дані назад в основну пам'ять (або передає їх безпосередньо Ядру 2 через міжкешевий обмін — залежно від реалізації).
Тепер дані чисті. І Ядро 0, і Ядро 2 мають дійсні копії та переходять у стан S. *Результат: Ядро 0 = S, Ядро 2 = S*
Сценарій 5: Ядро 1 записує X (у Ядра 0 — стан Shared)
Ядро 1 наразі не має дійсної копії (її інвалідували в Сценарії 3). Воно видає BusRdX (ексклюзивне читання з шини) — комбінований запит читання та інвалідації, тобто «дайте мені дані І переконайтеся, що більше ні в кого їх немає».
Ядро 0 прослуховує BusRdX та інвалідує свою копію: S → I.
Ядро 1 отримує дані, завантажує їх у стан M (Modified) і виконує запис. *Результат: Ядро 0 = I, Ядро 1 = M*
Ці п'ять сценаріїв охоплюють найпоширеніші переходи. Звернімо увагу: будь-який запис одного ядра примусово інвалідує всі інші кешовані копії — це і є фундаментальний механізм, який підтримує когерентність кешів.Простіший протокол під назвою MSI використовує лише три стани: Modified, Shared та Invalid. Він працює коректно, проте стан Exclusive у MESI дає критичну оптимізацію продуктивності.У MSI, коли ядро читає рядок кешу, якого більше ні в кого немає, рядок однаково доводиться поміщати у стан Shared (бо в MSI немає способу виразити «я єдиний чистий власник»). Пізніше, коли це ядро запише в рядок — дуже поширений патерн, бо програми часто читають значення, модифікують його і записують назад, — доведеться видати BusUpgr для переходу зі Shared у Modified, навіть якщо насправді жодне інше ядро копії не має.MESI усуває цей зайвий трафік на шині. Коли ядро читає рядок і за відсутністю відповідей при прослуховуванні розуміє, що є єдиним власником, воно поміщає рядок у стан Exclusive. Наступний запис тихо переводить рядок з E → M із нульовим трафіком на шині — без розсилання, без інвалідації, без марного витрачання пропускної здатності.Ця оптимізація надзвичайно важлива на практиці, адже значна частка рядків кешу використовується лише одним ядром. Потокові локальні змінні, кадри стеку та чимало об'єктів у купі — приватні для одного потоку. Без стану Exclusive кожен запис у ці приватні рядки породжував би марний трафік на шині, конкуруючи за пропускну здатність із транзакціями, які справді потребують міжядерної комунікації.MESI з шинним прослуховуванням — елегантне й ефективне рішення для споживчих процесорів із 4–16 ядрами, але має принципові обмеження:Масштабованість — оскільки кожну транзакцію на шині розсилають усім кешам, зі зростанням кількості ядер шина перетворюється на вузьке місце. Тому великі серверні процесори (із 64+ ядрами) натомість застосовують протоколи на основі каталогу.Розширення — існує кілька розширених версій MESI: Протокол MOESI (AMD) додає стан Owned — брудний рядок, що водночас є спільним. Власник надає дані на прослуховуваних читаннях без попереднього запису в пам'ять, зменшуючи навантаження на її пропускну здатність. Протокол MESIF (Intel) додає стан Forward — один призначений власник спільної копії відповідає на прослуховувані читання, що унеможливлює одночасні спроби відповіді від кількох кешів. Когерентність vs консистентність — MESI гарантує, що всі ядра зрештою побачать однакове значення за даною адресою (когерентність), однак не визначає порядку, у якому записи за різними адресами стають видимими між ядрами. Це сфера моделей консистентності пам'яті та інструкцій бар'єрів пам'яті (mfence, sfence, lfence на x86) — тема для поглиблених курсів архітектури.Протокол MESI працює на рівні рядків кешу, а не окремих змінних. Майже на всіх сучасних процесорах x86 рядок кешу має розмір 64 байти. Це означає, що коли ядро записує будь-який байт у межах 64-байтного рядка, увесь рядок інвалідується в кешах усіх інших ядер.Розгляньмо тепер ситуацію, коли два потоки на різних ядрах звертаються до різних змінних, які випадково опинилися в одному 64-байтному рядку кешу, причому принаймні один потік виконує запис. З погляду MESI це не відрізняється від випадку, коли обидва потоки записують у ту саму змінну: рядок кешу «стрибає» між ядрами, запускаючи повний цикл інвалідації та перезавантаження щоразу, коли будь-який із потоків робить запис.Це явище називається хибне спільне використання (false sharing): насправді потоки не мають жодних спільних даних, але апаратура поводиться з ними так, ніби ці дані спільні, оскільки протокол когерентності не розрізняє окремі байти всередині рядка кешу. «Хибним» це спільне використання є тому, що воно є наслідком розміщення даних у пам'яті, а не логіки програми.Припустимо, ми хочемо розпаралелити просту задачу підрахунку. Кожен із чотирьох потоків інкрементує свій власний лічильник:#include &lt;pthread.h&gt;
#include &lt;stdio.h&gt; int counters[4]; // 4 int × 4 байти = 16 байтів загалом void* thread_func(void* arg) { int id = *(int*)arg; for (int i = 0; i &lt; 1000000; i++) { counters[id]++; // Кожен потік записує ЛИШЕ у свій елемент } return NULL;
}
Логічно тут жодного спільного використання немає: потік 0 звертається лише до counters[0], потік 1 — лише до counters[1] і так далі. Немає ні перегонів даних, ні потреби в блокуваннях, ні спільного стану взагалі.Однак фізично весь масив counters займає лише 16 байтів — значно менше одного 64-байтного рядка кешу. Тож щоразу, коли будь-який потік записує у свій лічильник, увесь рядок кешу інвалідується у кешах усіх інших ядер. За чотирьох потоків, що пишуть у щільних циклах, рядок безперервно «стрибає» між чотирма ядрами. Результат парадоксальний: чотири потоки виконують задачу повільніше, ніж один потік, хоча на логічному рівні жодного спільного використання немає.<br><img alt="Хибне спільне використання — чотири лічильники в одному рядку кешу спричиняють «стрибання» інвалідацій між ядрами" src="assets/l11-false-sharing.svg" target="_self" style="width: 650px; max-width: 100%;">Реальні дані бенчмарків показують, наскільки руйнівним може бути хибне спільне використання. Наведені нижче виміри отримано в контрольованому експерименті:Варті уваги три спостереження: Хибне спільне використання так само згубне, як і справжнє. Час у випадку хибного спільного використання (7,78 мс) практично збігається з часом справжнього спільного використання (7,69 мс). Апаратура не бачить різниці — рядок кешу залишається рядком кешу. Коефіцієнт промахів L1 стрімко зростає. За хибного спільного використання 40% звернень до L1 виявляються промахами, тоді як в однопотоковому випадку — лише 0,03%. Кожен такий промах спричиняє міжядерний обмін вартістю в десятки тактів. Після виправлення паралельна версія виявляється швидшою за однопотокову. З правильним доповненням чотирипотокова версія (1,75 мс) працює швидше за однопотокову (2,35 мс) — близько до ідеального чотириразового прискорення. Саме таку продуктивність ми мали б від самого початку; хибне спільне використання її «з'їдало». Концептуально виправлення просте: потрібно зробити так, щоб дані, які записують різні потоки, розташовувалися на окремих рядках кешу. Найпоширеніший прийом — доповнення (padding):#include &lt;stdalign.h&gt; struct alignas(64) PaddedCounter { int value; // Решта 60 байтів — «порожнє» доповнення, // але саме вони гарантують, що кожен лічильник // розміщується на окремому рядку кешу.
}; struct PaddedCounter counters[4]; void* thread_func(void* arg) { int id = *(int*)arg; for (int i = 0; i &lt; 1000000; i++) { counters[id].value++; } return NULL;
}
У C++ той самий підхід реалізується за допомогою alignas або константи з C++17 std::hardware_destructive_interference_size (яка дорівнює розміру рядка кешу на цільовій платформі):struct alignas(64) AlignedCounter { std::atomic&lt;int&gt; val;
}; // Або з використанням стандартної константи:
struct alignas(std::hardware_destructive_interference_size) AlignedCounter { std::atomic&lt;int&gt; val;
};
До альтернативних рішень належать: Локальне накопичення в потоці. Кожен потік використовує для лічильника локальну змінну, а наприкінці один раз додає результат до спільного підсумку. Це повністю усуває міжядерний кеш-трафік під час «гарячого» циклу. Перепроєктування структури даних. Варто відокремити «гарячі» поля (які часто записуються різними потоками) від «холодних» (які рідко змінюються або лише читаються) і згрупувати гарячі дані так, щоб для кожного потоку вони розміщувалися на окремому рядку кешу. Хибне спільне використання невидиме у вихідному коді — адже проблема криється в розміщенні даних у пам'яті, а не в логіці програми. Для його виявлення потрібні спеціалізовані інструменти.У Linux спеціально для цього призначений інструмент perf c2c (cache-to-cache):$ perf c2c record ./my_program
$ perf c2c report
Ключовий показник, на який слід звернути увагу, — HITM (Hit Modified): він підраховує, скільки разів завантаження з кешу одного ядра потрапляло на рядок, що перебував у стані Modified у кеші іншого ядра, тобто рядок доводилося передавати між ядрами. Високе значення HITM на конкретному рядку кешу свідчить про справжнє або хибне спільне використання. Якщо дослідити адреси і з'ясувати, що вони належать різним змінним, — маємо хибне спільне використання.Intel VTune Profiler та AMD uProf також мають спеціальні режими аналізу хибного спільного використання.Корисна евристика. Якщо додавання нових потоків до паралельної програми робить її повільнішою (або не дає прискорення), а логічно потоки не мають спільних даних, хибне спільне використання має бути першим підозрюваним.Хибне спільне використання заслуговує особливої уваги, бо суперечить обґрунтованим очікуванням програміста одразу в чотирьох аспектах: Немає функціональної помилки. Програма видає правильні результати, усі тести проходять. Єдиний симптом — низька продуктивність, яку легко проігнорувати або списати на іншу причину. Невидиме у вихідному коді. Можна годинами вдивлятися в код і нічого не знайти. Потоки звертаються до різних змінних, перегонів даних немає, логіка чиста. Проблема існує лише у фізичному розміщенні даних у пам'яті — того, чого вихідний код не показує. Повністю нівелює паралелізацію. Як показали бенчмарки, хибне спільне використання здатне зробити чотирипотокову програму такою ж повільною (або навіть повільнішою), як однопотокова. Усі інженерні зусилля на розпаралелення — потоки, синхронізація, тестування — витрачено даремно. Проявляється лише за паралельного виконання. Однопотокове тестування, що лежить в основі звичної розробки та налагодження, не здатне виявити хибне спільне використання. Помилка залишається прихованою, поки програма не запуститься на кількох ядрах. Розуміння когерентності кешу (Розділ 9) дає ментальну модель для прогнозування та діагностування цього класу помилок. Коли ми знаємо, що MESI працює на 64-байтних рядках кешу і що будь-який запис інвалідує увесь рядок у всіх інших кешах, хибне спільне використання перетворюється із загадкової аномалії продуктивності на очевидний наслідок розміщення даних у пам'яті — із таким самим очевидним виправленням.Ця лекція розкрила внутрішні механізми, на яких тримається ієрархія пам'яті. Підсумуймо основні ідеї, які варто запам'ятати: Ієрархія пам'яті використовує локальність, щоб подолати розрив між процесором і пам'яттю. Кеш L1 у 200 разів швидший за оперативну пам'ять. Ієрархія створює для програм ілюзію великої швидкої пам'яті — але лише якщо шаблони доступу демонструють локальність. Часова локальність (повторне використання даних, до яких зверталися нещодавно) і просторова локальність (доступ до сусідніх даних) — два принципи, на яких ґрунтуються кеші. Обхід масиву по рядках у C є кеш-дружнім; по стовпцях — ні. Рядок кешу — це одиниця передачі (зазвичай 64 байти). Кожне звернення завантажує цілий рядок, а кожне витіснення видаляє цілий рядок. Адреси розкладаються на поля тегу, індексу та зміщення. Асоціативність задає баланс між гнучкістю та вартістю. Кеші з прямим відображенням прості, але схильні до конфліктних промахів. Множинно-асоціативні кеші (8–16 каналів у сучасних процесорах) дають найкращий практичний компроміс. Повністю асоціативний кеш застосовується лише для дуже невеликих спеціалізованих структур — наприклад, для буфера асоціативної трансляції (TLB). AMAT = Час влучення + Коефіцієнт промахів × Штраф промаху. Багаторівневі кеші різко скорочують ефективний час доступу: у добре спроєктованій ієрархії 200-тактова оперативна пам'ять перетворюється на ефективні ~2,6 такту на звернення. Зворотний запис у поєднанні із записом з розміщенням — стандартна політика в сучасних процесорах: вона мінімізує трафік до пам'яті ціною відстеження забруднених рядків. Протокол MESI підтримує когерентність багатоядерних кешів за допомогою чотирьох станів (Modified, Exclusive, Shared, Invalid) і шинного прослуховування. Стан Exclusive оптимізує поширений шаблон «читання, а потім запис», уникаючи зайвого трафіку на шині. Хибне спільне використання відчутно підриває продуктивність: різні потоки, що звертаються до різних змінних на одному рядку кешу, спричиняють таку саму інвалідацію, як і справжнє спільне використання. Виправлення — alignas(64) або локальне сховище потоку. Кеші — лише один рівень, на якому ієрархія пам'яті користується «трюком локальності». У L12 ми спускаємося ієрархією нижче — до віртуальної пам'яті, механізму, що спирається на ті самі принципи локальності, але додає принципово нову можливість: трансляцію адрес. Ми побачимо, як таблиці сторінок і TLB (повністю асоціативний кеш — тепер зрозуміло, що саме це означає!) забезпечують захист пам'яті, ізоляцію між процесами та ілюзію того, що кожна програма має власний приватний адресний простір. Взаємодія між кешами та віртуальною пам'яттю — зокрема те, чи використовує кеш віртуальні, чи фізичні адреси — тісно пов'язує ці дві лекції.Завдання в межах P11:
Перейти від TASM/DOSBox (16-бітний реальний режим) до NASM + Linux (64-бітний захищений режим) з GDB/GEF
Реалізувати множення матриць: наївна версія проти кеш-дружньої версії (тайлінг циклів/блокування)
Виміряти продуктивність кешу за допомогою perf stat: cache-references, cache-misses, L1-dcache-load-misses
Провести експерименти з кроком доступу, що демонструють ефекти просторової локальності
Проаналізувати коефіцієнти промахів та співвіднести їх із формулою AMAT із цієї лекції
Необхідні інструменти:
Середовище Linux (Codespaces, WSL або рідне)
Асемблер NASM + компонувальник ld
GDB з розширенням GEF
Інструмент профілювання perf <br>
<a data-tooltip-position="top" aria-label="https://diveintosystems.org/book/C11-MemHierarchy/caching.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://diveintosystems.org/book/C11-MemHierarchy/caching.html" target="_self">Dive Into Systems — CPU Caches (Ch. 11.4)</a> — Структура кешу, розкладання адрес, асоціативність, політики запису та категорії промахів <br>
<a data-tooltip-position="top" aria-label="https://diveintosystems.org/book/C11-MemHierarchy/mem_hierarchy.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://diveintosystems.org/book/C11-MemHierarchy/mem_hierarchy.html" target="_self">Dive Into Systems — Memory Hierarchy (Ch. 11)</a> — Огляд ієрархії пам'яті з компромісами між швидкістю, ємністю та вартістю <br>
<a data-tooltip-position="top" aria-label="https://diveintosystems.org/book/C11-MemHierarchy/locality.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://diveintosystems.org/book/C11-MemHierarchy/locality.html" target="_self">Dive Into Systems — Locality (Ch. 11)</a> — Часова та просторова локальність з прикладами шаблонів доступу до матриць <br>
<a data-tooltip-position="top" aria-label="https://diveintosystems.org/book/C14-SharedMemory/cache_coherence.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://diveintosystems.org/book/C14-SharedMemory/cache_coherence.html" target="_self">Dive Into Systems — Cache Coherence and False Sharing (Ch. 14.5)</a> — Протокол MSI, кеші з шинним прослуховуванням, хибне спільне використання з бенчмарками <br>
<a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/computer-organization-architecture/cache-memory-in-computer-organization/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/computer-organization-architecture/cache-memory-in-computer-organization/" target="_self">GeeksforGeeks — Cache Memory in Computer Organization</a> — Способи відображення кешу (пряме, повністю асоціативне, множинно-асоціативне) <br>
<a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/computer-organization-architecture/locality-of-reference-and-cache-operation-in-cache-memory/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/computer-organization-architecture/locality-of-reference-and-cache-operation-in-cache-memory/" target="_self">GeeksforGeeks — Locality of Reference</a> — Визначення локальності та формула AMAT <br>
<a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/computer-organization-architecture/cache-coherence-protocols-in-multiprocessor-system/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/computer-organization-architecture/cache-coherence-protocols-in-multiprocessor-system/" target="_self">GeeksforGeeks — Cache Coherence Protocols</a> — Огляд протоколів MSI, MESI, MOSI, MOESI <br>
<a data-tooltip-position="top" aria-label="https://en.algorithmica.org/hpc/cpu-cache/associativity/" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.algorithmica.org/hpc/cpu-cache/associativity/" target="_self">Algorithmica — Cache Associativity</a> — Глибокий аналіз асоціативності, пастки кроків, кратних степеню двійки, наслідки для продуктивності <br>
<a data-tooltip-position="top" aria-label="https://dev.to/sachin_tolay_052a7e539e57/cache-coherence-how-the-mesi-protocol-keeps-multi-core-cpus-consistent-170j" rel="noopener nofollow" class="external-link is-unresolved" href="https://dev.to/sachin_tolay_052a7e539e57/cache-coherence-how-the-mesi-protocol-keeps-multi-core-cpus-consistent-170j" target="_self">Cache Coherence: How MESI Keeps Multi-Core CPUs Consistent</a> — Переходи станів MESI з покроковими сценаріями <br>
<a data-tooltip-position="top" aria-label="https://fiveable.me/advanced-computer-architecture/unit-7/cache-replacement-write-policies/study-guide/OYpe2aD5JCvYaxRh" rel="noopener nofollow" class="external-link is-unresolved" href="https://fiveable.me/advanced-computer-architecture/unit-7/cache-replacement-write-policies/study-guide/OYpe2aD5JCvYaxRh" target="_self">Fiveable — Cache Replacement and Write Policies</a> — Заміщення LRU, FIFO, випадкове; наскрізний запис проти зворотного запису <br>
<a data-tooltip-position="top" aria-label="https://gist.github.com/jboner/2841832" rel="noopener nofollow" class="external-link is-unresolved" href="https://gist.github.com/jboner/2841832" target="_self">Latency Numbers Every Programmer Should Know</a> — Класичний довідник із орієнтовних затримок пам'яті та введення-виведення <br>
<a data-tooltip-position="top" aria-label="https://informatecdigital.com/en/CPU-cache-latency-and-how-it-affects-performance/" rel="noopener nofollow" class="external-link is-unresolved" href="https://informatecdigital.com/en/CPU-cache-latency-and-how-it-affects-performance/" target="_self">CPU Cache Latency: L1, L2, L3 and Their Real Impact</a> — Реальні виміри затримок кешу на сучасних процесорах AMD та Intel <br>
<a data-tooltip-position="top" aria-label="https://coffeebeforearch.github.io/2019/12/28/false-sharing-tutorial.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://coffeebeforearch.github.io/2019/12/28/false-sharing-tutorial.html" target="_self">Performance Implications of False Sharing</a> — Бенчмарки хибного спільного використання з кодом C++, вирівнювання за рядком кешу як розв'язання, виміри perf ]]></description><link>lectures/l11.-ієрархія-пам'яті-та-продуктивність.html</link><guid isPermaLink="false">Lectures/L11. Ієрархія пам'яті та продуктивність.md</guid><pubDate>Mon, 20 Apr 2026 10:37:40 GMT</pubDate><enclosure url="assets/l11-processor-memory-gap.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l11-processor-memory-gap.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L10. Огляд сучасних архітектур]]></title><description><![CDATA[
Лекція 10 курсу «Архітектура комп'ютерних систем» у НаУКМА.
Після цієї лекції ви зможете: Пояснити, чому спеціалізовані процесорні архітектури співіснують із процесорами загального призначення (CPU), і назвати інженерні компроміси, які роблять спеціалізацію вигідною Описати ключові архітектурні особливості DSP, GPU, ASIC, FPGA, VLIW-процесорів і TPU та зіставити кожен із цільовим навантаженням Порівняти нейроморфні та квантові обчислювальні парадигми з класичними архітектурами фон Неймана Оцінити, коли для певної задачі предметно-орієнтований прискорювач доцільніший, ніж CPU загального призначення Протягом перших дев'яти лекцій курсу ми зосереджувалися на одному типі процесора — центральному процесорі (CPU) загального призначення. Ми вивчали 8086 та його нащадків: їхні регістри, системи команд, адресацію пам'яті, угоди про виклики, механізми переривань, ієрархії кеш-пам'яті та філософії проєктування (CISC проти RISC), що їх сформували. Паралельно ми побудували ментальну модель того, чим є процесор: це послідовна машина, яка вибирає, декодує та виконує інструкції — одну (або кілька) за раз — зі спільної пам'яті.Ця модель правильна, але неповна. CPU загального призначення — це швейцарський армійський ніж обчислень: універсальний, потужний і достатньо хороший для надзвичайно широкого кола завдань. Проте «достатньо хороший» — не те саме, що «оптимальний». Коли навантаження стає достатньо великим, достатньо чітко окресленим або достатньо вимогливим щодо продуктивності в реальному часі, універсальність CPU загального призначення перетворюється радше на недолік, аніж на перевагу.Мотивація до спеціалізації закорінена у фізиці. У Лекції 1 ми обговорювали фізичні обмеження обчислень — швидкість світла, термодинамічні обмеження та <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Landauer%27s_principle" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Landauer%27s_principle" target="_self">принцип Ландауера</a>. У Лекції 9 ми згадали ще одне, більш безпосереднє обмеження — кінець <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Dennard_scaling" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Dennard_scaling" target="_self">масштабування Деннарда</a>. Із 1970-х до початку 2000-х кожне нове покоління транзисторів було меншим, швидшим і споживало менше енергії на одиницю площі. Завдяки цьому інженери могли збільшувати тактову частоту з кожним поколінням, не нарощуючи пропорційно енергоспоживання. Частоти процесорів зростали від кілогерців до мегагерців і гігагерців приголомшливими темпами.<br>Приблизно у 2004–2005 роках це масштабування припинилося. Транзистори й далі зменшувалися (згідно із <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Moore%27s_law" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Moore%27s_law" target="_self">законом Мура</a>), проте їхня щільність потужності перестала знижуватися. Вищі тактові частоти тепер означали більше енергії й більше тепла — тепла, яке вже неможливо було розсіяти практичними системами охолодження. Тактові частоти стабілізувалися поблизу 4 ГГц і залишаються на цьому рівні майже два десятиліття. Це явище іноді називають стіною потужності.<br>Першою відповіддю індустрії, як ми обговорювали в Лекції 9, стала багатоядерна паралельність (MIMD — багато інструкцій, багато даних). Замість одного швидшого ядра — розмістити кілька ядер на одному чіпі та виконувати програмне забезпечення паралельно. Проте й багатоядерне масштабування має власні обмеження: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Amdahl%27s_law" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Amdahl%27s_law" target="_self">закон Амдала</a> показує, що прискорення від паралелізму обмежене послідовною часткою навантаження. Якщо навіть 10% програми є суто послідовними, то жодна кількість паралельних ядер не дасть прискорення більш ніж у 10 разів.Друга відповідь індустрії — і тема цієї лекції — це спеціалізація. Якщо ядро загального призначення неможливо зробити швидшим, можна побудувати спеціалізоване ядро, значно ефективніше для певного класу обчислень. GPU не запустить Excel, зате множить матриці в тисячу разів швидше за CPU. Цифровий сигнальний процесор (DSP) не відкриє вебсторінку, але здатен фільтрувати аудіосигнал у реальному часі, споживаючи лише частку потужності. ASIC (спеціалізована інтегральна схема) не піддається перепрограмуванню, проте може майнити Bitcoin чи кодувати відео з ефективністю, якій не зрівняється жоден процесор загального призначення.Архітектури, які ми розглянемо в цій лекції, охоплюють спектр від цілком універсальних до повністю спеціалізованих:Цей спектр відображає фундаментальний інженерний компроміс: універсальність проти ефективності. CPU загального призначення має підтримувати будь-які обчислення — цілочисельну арифметику, математику з рухомою комою, потік керування з великою кількістю розгалужень, структури даних із інтенсивним використанням пам'яті, обробку вводу/виводу — і його апаратне забезпечення мусить однаково добре обслуговувати кожен із цих сценаріїв. Спеціалізований же процесор може спрямувати весь свій транзисторний бюджет, потужнісний ліміт та пропускну здатність пам'яті виключно на операції, важливі для цільової предметної області, відкинувши все решту.Розглянемо арифметику. АЛП (арифметико-логічний пристрій) процесора спроєктований для найрізноманітніших операцій — цілих чисел на 8, 16, 32 та 64 біти; одинарної та подвійної точності з рухомою комою; побітової логіки; порівнянь; зсувів. Натомість блок множення-накопичення (MAC) у складі DSP спроєктований робити одну річ — але бездоганно: множити два числа й додавати результат до акумулятора, мільярди разів на секунду, адже саме це є фундаментальною операцією цифрової обробки сигналів. Звужуючи сферу застосування, DSP досягає ефективності, недосяжної для CPU.Ця лекція охоплює вісім категорій спеціалізованих архітектур: DSP (цифровий сигнальний процесор) — оптимізований для обробки аналого-цифрових сигналів у реальному часі GPU (графічний процесор) — тисячі простих ядер для масово паралельних обчислень ASIC (спеціалізована інтегральна схема) — замовний кремній для єдиного завдання FPGA (програмована вентильна матриця) — реконфігуровне апаратне забезпечення, що поєднує світ програм і світ кремнію VLIW (дуже довге командне слово) — паралелізм на рівні інструкцій, керований компілятором (короткий огляд з L09) TPU (тензорний процесорний блок) — систолічні матриці, оптимізовані для машинного навчання Нейроморфні процесори — обчислення, натхненні мозком, з імпульсними нейронними мережами Квантові обчислення — кубіти та суперпозиція для задач, недосяжних класичним підходам Для кожної архітектури ми відповідатимемо на однаковий набір запитань: яку проблему вона розв'язує? Яке її ключове архітектурне рішення? Чим вона відрізняється від CPU загального призначення? І коли саме вона є правильним інструментом для конкретного завдання?Ця лекція безпосередньо спирається на основи, закладені протягом усього курсу: Лекція 6 (Архітектура системи) познайомила з архітектурами фон Неймана та Гарвардською. Низка спеціалізованих процесорів — особливо DSP — будується на Гарвардській конструкції або її варіантах, і ми побачимо, чому розділення пам'яті інструкцій та даних є критично важливим для їхньої продуктивності. Лекція 9 (Проєктування ISA та моделі обчислень) дала нам таксономію Флінна (SISD — одна інструкція, одні дані; SIMD — одна інструкція, багато даних; MISD; MIMD — багато інструкцій, багато даних) та поняття CISC, RISC і VLIW. Ми послуговуватимемося класифікацією Флінна протягом усієї лекції, щоб позиціонувати кожну архітектуру: DSP спирається на паралелізм даних, подібний до SIMD, GPU — на варіант під назвою SIMT (одна інструкція, багато потоків), TPU — на систолічні матриці (своєрідний гібрид MISD/SIMD), а нейроморфні чіпи взагалі не вписуються в категорії Флінна. Лекція 9 також звернула увагу на те, що VLIW знайшов найбільший успіх не в обчисленнях загального призначення (де Intel Itanium зазнав відомої невдачі), а саме в DSP — зв'язок, який ми детально розглянемо в наступному розділі. Наприкінці цієї лекції у вас складеться карта сучасного обчислювального ландшафту — ландшафту, в якому CPU є лише однією будівлею серед багатьох, а вибір правильної архітектури для конкретного навантаження важить не менше за вміння писати ефективний код.Наш огляд спеціалізованих архітектур почнемо з однієї з найстаріших і комерційно найуспішніших — цифрового сигнального процесора (DSP — Digital Signal Processor). DSP масово випускаються з початку 1980-х років, і сьогодні їх можна знайти практично в кожному пристрої, що обробляє аудіо, відео чи бездротовий зв'язок: від смартфонів і слухових апаратів до радарів і базових станцій 5G. Щороку вони обробляють сигналів на мільярди доларів, однак більшість студентів комп'ютерних наук ніколи про них не чули.Фізичний світ аналоговий. Звук — неперервна хвиля тиску. Світло — неперервне електромагнітне поле. Радіосигнали — неперервні коливання. Сенсори (мікрофони, камери, антени) створюють неперервні електричні сигнали, що плавно змінюються з часом.А от комп'ютери цифрові. Вони оперують дискретними числами, які зберігаються як двійкові послідовності одиниць і нулів. Щоб обробляти сигнали реального світу цифровим способом, їх спершу треба перетворити з аналогової в цифрову форму за допомогою аналого-цифрового перетворювача (АЦП). АЦП дискретизує неперервний сигнал через рівні проміжки часу (частота дискретизації) і квантує кожен відлік у дискретне числове значення (з роздільною здатністю N бітів на відлік). Після цифрової обробки цифро-аналоговий перетворювач (ЦАП) перетворює оброблений цифровий сигнал назад в аналогову форму — наприклад, для керування динаміком чи антеною.<br><img alt="Конвеєр обробки сигналів DSP: мікрофон → АЦП → Цифрова система → ЦАП → динамік" src="assets/l10-dsp-adc-dac-diagram.jpeg" target="_self">Конвеєр DSP: аналоговий сигнал оцифровується АЦП, обробляється DSP і перетворюється назад в аналоговий за допомогою ЦАП.Цей конвеєр АЦП → Цифрова обробка → ЦАП лежить в основі всієї цифрової обробки сигналів. Блок «Цифрова обробка» посередині — це саме те місце, де працює DSP.Чому б просто не взяти CPU? CPU (центральний процесор) загального призначення, безумовно, здатний виконувати математичні операції над дискретизованими даними. Для обробки поза реальним часом — редагування WAV-файлу, офлайн-аналізу сейсмологічних даних, пакетної фільтрації зображень — CPU цілком достатньо. Проблема виникає тоді, коли обробка має відбуватися в реальному часі.Розгляньмо цифровий слуховий апарат. Звук потрапляє в мікрофон, дискретизується АЦП із частотою 16 000 відліків на секунду, обробляється (фільтрується, підсилюється, очищається від шуму) і має вийти з динаміка без помітної затримки — зазвичай протягом 5–10 мілісекунд. Це означає, що процесор зобов'язаний завершити всі обчислення фільтрації над блоком відліків до надходження наступного блоку — щоразу, без винятку. Пропуск навіть одного дедлайну означає чутні збої, клацання чи тишу.Аналоговий сигнал не терпить затримок. Телефонний дзвінок має бюджет затримки приблизно 150 мілісекунд на повний цикл, поки розмова ще залишається комфортною. Радар, що відстежує об'єкти, мусить обробляти зворотні сигнали за мікросекунди, аби не втратити точності супроводу. Навушники з активним шумоподавленням мають аналізувати й інвертувати зовнішній звук настільки швидко, щоб антишумова хвиля досягла барабанної перетинки синхронно з вихідним шумом — допуск тут вимірюється в мікросекундах.CPU загального призначення потужні, проте оптимізовані під пропускну здатність і гнучкість, а не під гарантовану затримку. CPU може опрацювати алгоритм обробки сигналів швидко в середньому, але його продуктивність непередбачувана: промахи кешу, хибні передбачення переходів, переривання операційної системи й перемикання контексту — усе це вносить змінні затримки. Для жорсткої обробки сигналів у реальному часі потрібен процесор із детермінованим часом виконання, архітектура якого спеціально розрахована на математичні операції обробки сигналів.Цей процесор — DSP.Архітектурна історія DSP починається з рішення, яке ми вивчали в Лекції 6, — вибору між архітектурою фон Неймана та Гарвардською архітектурою.Нагадаємо з L06, що архітектура фон Неймана має єдину пам'ять для інструкцій і даних, доступну через єдину шину. Це породжує вузьке місце фон Неймана: CPU не може одночасно вибирати інструкцію й звертатися до даних. Гарвардська архітектура долає цю перешкоду, надаючи окремі пам'яті та окремі шини для інструкцій і даних, що відкриває шлях до паралельного доступу.<br><img alt="Блок-діаграма Гарвардської архітектури DSP з окремою пам'яттю даних та інструкцій" src="assets/l10-dsp-harvard-architecture.png" target="_self">Модифікована Гарвардська архітектура, яку застосовують у DSP: окрема пам'ять даних та інструкцій з незалежними шинами забезпечує одночасний доступ.DSP переважно будують на Гарвардській архітектурі або, точніше, на модифікованій Гарвардській архітектурі. Причина проста: алгоритми обробки сигналів переважно зводяться до щільних циклів, які багаторазово завантажують дані, виконують арифметичні операції та зберігають результати. Якщо процесор змушений чекати на доступ до даних, перш ніж завантажити наступну інструкцію (як у архітектурі фон Неймана), пропускна здатність різко падає. Окремі шини для інструкцій і даних у Гарвардській архітектурі дають DSP змогу завантажувати наступну інструкцію з пам'яті програм одночасно із завантаженням операнда з пам'яті даних, фактично подвоюючи пропускну здатність пам'яті для найпоширеніших операцій.Багато DSP ідуть ще далі й мають кілька банків пам'яті даних з окремими шинами. Наприклад, сімейство SHARC (Super Harvard Architecture Computer) від Analog Devices містить два сегменти пам'яті даних, кожен зі своєю шиною. Завдяки цьому процесор виконує за один такт аж три звернення до пам'яті одночасно: вибирає наступну інструкцію з пам'яті програм, завантажує один операнд з банку пам'яті даних 1 та інший операнд — з банку пам'яті даних 2. Для алгоритмів на кшталт FIR-фільтра (його ми розглянемо далі) така можливість потрійного доступу є критично важливою.Якби довелося описати обчислення DSP однією операцією, нею стало б множення-накопичення (MAC — Multiply-Accumulate):Accumulator = Accumulator + (A × B)
Ця одна-єдина операція — помножити два числа й додати результат до накопичувальної суми — є фундаментальною цеглинкою цифрової обробки сигналів. Вона трапляється повсюди: FIR-фільтри: вихідний відлік є зваженою сумою вхідних відліків: y[n] = Σ (coefficient[k] × input[n-k]) Згортка: та сама зважена сума, що лежить в основі розмиття, підвищення різкості та виявлення країв у зображеннях Кореляція: порівняння двох сигналів на подібність ШПФ (Швидке перетворення Фур'є): розкладання сигналу на частотні компоненти, побудоване з операцій «метелик» — пар «множення-додавання» та «множення-віднімання» Множення матриць: кожен елемент результату — це скалярний добуток, тобто послідовність MAC-операцій CPU загального призначення вміють множити й додавати, але на кожну з цих операцій, як правило, припадає окрема інструкція: MUL для множення, потім ADD для накопичення. Дві інструкції, два цикли декодування й два проходи конвеєром. DSP натомість має спеціалізований апаратний блок MAC, що виконує множення та додавання за один такт. Найпоширеніша операція обробки сигналів займає один такт замість двох чи більше, до того ж блок MAC спеціально заточений під неї — з широким акумулятором, який запобігає переповненню.Математичний блок DSP SHARC, наприклад, містить: Помножувач з 80-бітним акумулятором (ширшим за операнди, щоб уникнути втрати точності при тривалих накопиченнях) АЛП (арифметико-логічний пристрій) для загальних арифметичних і логічних операцій Бареловий зсувач для бітових маніпуляцій Важливо, що помножувач і АЛП працюють паралельно: DSP може виконувати множення-накопичення в помножувачі, а паралельно додавати чи порівнювати в АЛП. Такого рівня функціонального паралелізму CPU загального призначення досягають лише завдяки складній логіці позачергового виконання, тоді як у DSP він закладений прямо в кодування інструкцій.Окрім блоку MAC, DSP мають ще кілька архітектурних особливостей, спеціально розроблених під вимоги обробки сигналів. Кожна з них цілить у конкретне вузьке місце, що обмежувало б продуктивність на CPU загального призначення.Обробка сигналів переважно складається з циклів. FIR-фільтр зі 100 відводами потребує 100 операцій множення-накопичення на кожен вихідний відлік. На CPU загального призначення на кожну ітерацію циклу припадають накладні витрати: зменшення лічильника, порівняння та умовний перехід. У щільному внутрішньому циклі ці витрати можуть поглинати значну частку всього часу виконання.У DSP цикли з нульовими накладними витратами реалізовано апаратно. Програміст (або компілятор) задає початкову адресу циклу, кінцеву адресу та кількість ітерацій, а далі циклом повністю керує апаратура — без зменшення лічильника, без порівняння, без інструкції переходу. Спеціалізована апаратура зменшує й перевіряє лічильник циклу паралельно з конвеєром інструкцій. Щойно виконано останню інструкцію тіла циклу, лічильник команд автоматично повертається на початкову адресу. Тіло циклу працює на повній швидкості, а на керування циклом не витрачено жодної інструкції.; Псевдокод для циклу з нульовими накладними витратами DSP
SETUP_LOOP start_addr, end_addr, 100 ; апаратура керує лічильником
start_addr: MAC ACC, DATA1, COEFF ; множення-накопичення
end_addr: ; апаратура автоматично повторює 100 разів
На CPU загального призначення той самий цикл виглядатиме приблизно так: MOV CX, 100 ; лічильник циклу
loop_start: ; ... еквівалент MAC (кілька інструкцій) ... DEC CX ; накладні витрати: зменшення лічильника JNZ loop_start ; накладні витрати: умовний перехід
Ці дві службові інструкції на ітерацію (DEC + JNZ) у циклі зі 100 ітерацій додали б 200 зайвих інструкцій. У DSP їх повністю усунено.У стандартній арифметиці доповнення до двійки (саме її застосовують CPU загального призначення) числа, що перевищують максимальне представлюване значення, переповнюються циклічно. Наприклад, для знакового 16-бітного цілого додавання 1 до 32 767 (0x7FFF) дає -32 768 (0x8000). Таке циклічне переповнення математично коректне, проте для обробки сигналів воно катастрофічне: короткий аудіосплеск, що перевищує максимум, обертається на велике від'ємне число, і замість плавного кліпінгу чути гучне різке клацання — найгірший можливий артефакт.DSP натомість користуються насиченою арифметикою: коли результат виходить за максимальне представлюване значення, його обмежують до максимуму, а не переповнюють циклічно. Коли ж результат падає нижче мінімуму — обмежують до мінімуму. Для нашого 16-бітного прикладу: 32 767 + 1 = 32 767 (а не -32 768).В обробці аудіо насичення забезпечує значно м'якшу деградацію — короткий тихий кліпінг замість різкого стрибка. У системах керування насичення не дозволяє миттєвому переповненню породити абсолютно некоректний вихідний сигнал, який міг би пошкодити обладнання.Багато DSP розраховані на роботу переважно з числами з фіксованою комою, а не з плаваючою. У представленні з фіксованою комою програміст (або компілятор) визначає, де саме в двійковому слові стоїть «десяткова кома». Наприклад, формат Q15 використовує 1 знаковий біт і 15 дробових бітів, представляючи значення від -1,0 до +0,999969. Множення двох значень Q15 у форматі Q1.15 дає результат Q30 (30 дробових бітів), який потім потрібно зсунути й округлити назад до Q15.Чому ж фіксована кома? Тому що арифметика з фіксованою комою виконується на цілочисельній апаратурі, а вона простіша, швидша, дешевша й споживає менше енергії, ніж апаратура з плаваючою комою. Блоку MAC з фіксованою комою достатньо цілочисельного помножувача і цілочисельного суматора. Блок MAC з плаваючою комою, навпаки, потребує порівняння порядків, зсуву мантис, їх множення й додавання, нормалізації та округлення — набагато більше схемотехніки й енергії. Для пристроїв на батарейному живленні — смартфонів, слухових апаратів, бездротових сенсорних вузлів — економія енергії від DSP з фіксованою комою є вирішальною.Сучасні високопродуктивні DSP (як-от сімейство C6000 від Texas Instruments) оснащено блоками плаваючої коми для задач, де динамічного діапазону фіксованої коми недостатньо, але спадщина фіксованої коми й досі посідає центральне місце у філософії проєктування DSP.Алгоритми обробки сигналів постійно працюють із ковзними вікнами даних. FIR-фільтру, наприклад, у будь-який момент потрібен доступ до останніх N вхідних відліків. Коли надходить новий відлік, найстаріший відкидається, а нове значення займає його місце.На CPU загального призначення керування таким ковзним вікном потребує явної адресної арифметики та перевірок меж: «Якщо вказівник дійшов до кінця буфера, перенести його на початок». У щільному внутрішньому циклі ці перевірки додають інструкції та переходи.У DSP циклічні буфери реалізовано апаратно. Програміст конфігурує буфер, задаючи базову адресу та довжину, а апаратний генератор адрес даних (DAG) DSP автоматично переносить вказівник, щойно той досягає кінця буфера. Ані перевірок меж, ані умовних переходів — модулярна арифметика над вказівником виконується в апаратурі генерації адрес паралельно з арифметичними обчисленнями.DSP SHARC надає 8 циклічних буферів через два генератори адрес даних (по 4 на кожен DAG). Кожен DAG незалежно обчислює наступну адресу, включно з модулярним переносом, поки блок MAC виконує свої обчислення. Отже, за один такт DSP здатен:
Виконати операцію множення-накопичення
Оновити один вказівник циклічного буфера (через DAG1)
Оновити інший вказівник циклічного буфера (через DAG2)
Керувати лічильником ітерацій (через апаратний блок циклів)
Усе це відбувається одночасно, паралельно, з нульовими програмними накладними витратами.DSP часто обробляють безперервні потоки даних з реального світу: аудіовідліки з частотою 44 100 Гц, радарні відбиття з мегагерцовою частотою. Завантажувати ці дані через CPU (виконуючи явні інструкції завантаження для кожного відліку) означало б марнувати цінні такти, які варто віддати обчисленням.DMA (прямий доступ до пам'яті) дозволяє передавати дані між зовнішніми пристроями введення/виведення та пам'яттю DSP без участі CPU. Контролер DMA виконує передачу автономно: CPU програмує йому адресу джерела, адресу призначення та обсяг передачі, а далі контролер DMA переміщує дані, поки CPU продовжує виконувати інструкції обробки сигналів. Коли передачу завершено, контролер DMA сповіщає CPU перериванням.DSP SHARC на частоті 40 МГц підтримує два послідовні порти по 40 Мбіт/с кожен та шість паралельних портів по 40 МБ/с — і всі вони керуються DMA. Така пропускна здатність введення/виведення гарантує, що ядро процесора ніколи не голодує без даних і не витрачає такти на їх переміщення.<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Super_Harvard_Architecture_Single-Chip_Computer" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Super_Harvard_Architecture_Single-Chip_Computer" target="_self">Super Harvard Architecture Computer (SHARC)</a>, розроблений <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Analog_Devices" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Analog_Devices" target="_self">Analog Devices</a>, — вершина традиційного проєктування DSP. Він розширює базову Гарвардську архітектуру двома ключовими доповненнями, що усувають залишкові вузькі місця.Кеш інструкцій. У чистій Гарвардській архітектурі кожна інструкція має завантажуватися з пам'яті програм щотакту. Якщо щільний цикл складається, скажімо, з 5 інструкцій і повторюється 1 000 разів, шина пам'яті програм зайнята завантаженням тих самих 5 інструкцій знову і знову — 5 000 завантажень для 5 унікальних інструкцій. SHARC додає невеликий кеш інструкцій (зазвичай близько 32 інструкцій), який зберігає нещодавно виконані інструкції. Щойно тіло циклу закешовано, шина пам'яті програм звільняється для інших цілей — її можна перепризначити на третю шину даних і діставати дані з пам'яті програм на додачу до двох банків пам'яті даних.У цьому й полягає ключове нововведення: під час виконання закешованого циклу SHARC фактично має три незалежні шини даних. Для алгоритмів, яким потрібно три операнди за такт (як-от певні структури фільтрів), це усуває останнє вузьке місце пропускної здатності пам'яті.Контролер введення/виведення з DMA. Як описано вище, вбудований контролер DMA забезпечує автономну передачу даних між зовнішніми пристроями та внутрішньою пам'яттю, повністю звільняючи ядро процесора для обчислень.Комбінована архітектура — Гарвардське розділення пам'яті, кеш інструкцій, подвійні шини даних, DMA — дає SHARC надзвичайну пропускну здатність для обробки сигналів. За один такт SHARC здатен одночасно:
Завантажити й декодувати одну інструкцію (з кешу)
Виконати одне множення (в помножувачі)
Виконати одне додавання (в АЛП)
Перемістити два значення даних (через дві шини даних)
Оновити два вказівники циклічних буферів (через два DAG)
Керувати ітерацією циклу з нульовими накладними витратами
Це від шести до семи логічно різних операцій за такт — рівень пропускної здатності на такт, про який CPU загального призначення тієї ж епохи могли лише мріяти. Його досягнуто не складною апаратурою позачергового виконання, а ретельно спроєктованою архітектурою, де все потрібне алгоритму обробки сигналів від початку задумане працювати паралельно.Система команд DSP виглядає помітно інакше, ніж інструкції x86 чи ARM, які ми вивчали в цьому курсі. Вона спеціалізована і вкрай нерегулярна: оптимізована під патерни обробки сигналів, а не під зручність програмування загального призначення.<br><img alt="Таблиця функціональних груп інструкцій DSP та приклади мнемонік" src="assets/l10-dsp-instructions-table.png" target="_self">Функціональні групи системи команд DSP із прикладами мнемонік — зверніть увагу на спеціалізовані операції на кшталт множення-накопичення (madd) та бітового реверсування (bitrev).Типова система команд DSP містить такі функціональні групи:Кілька рис відрізняють її від ISA (архітектури системи команд) загального призначення: MAC як повноцінна операція. Там, де x86 потребує окремих інструкцій MUL і ADD, DSP має єдину інструкцію, яка виконує множення й накопичення за один такт. Паралельне кодування інструкцій. Чимало інструкцій DSP задають кілька одночасних операцій в одному командному слові. Сімейство TMS320C6x, наприклад, використовує 256-бітний пакет інструкцій у стилі VLIW (дуже довге командне слово), який кодує до 8 паралельних операцій, — і це виразно підсвічує зв'язок між VLIW (з L09) та проєктуванням DSP. Інструкції апаратного повторення. Інструкції на кшталт RPT (повторення) чи RPTB (повторення блоку) конфігурують апаратний блок циклів, замінюючи собою традиційний патерн циклу зі зменшенням лічильника та переходом. Насичувальні варіанти арифметики. Багато арифметичних інструкцій мають режими насичення (або окремі насичувальні варіанти), даючи програмісту змогу вибирати між циклічним переповненням та обмеженням. Спеціалізовані режими адресації. Адресація з бітовим реверсуванням (її використовують в обчисленнях «метелик» ШПФ) та модулярна адресація (для циклічних буферів) вбудовані прямо в систему команд, а не реалізовані програмно. Щоб оцінити ефективність архітектури DSP, розгляньмо фільтр зі скінченною імпульсною характеристикою (FIR-фільтр) — один із найпоширеніших алгоритмів обробки сигналів. FIR-фільтр обчислює кожен вихідний відлік як зважену суму останніх N вхідних відліків:y[n] = c[0]·x[n] + c[1]·x[n-1] + c[2]·x[n-2] + ... + c[N-1]·x[n-N+1]
де c[0]…c[N-1] — коефіцієнти фільтра (константи, що визначають його частотну характеристику), а x[n]…x[n-N+1] — останні N вхідних відліків.Це скалярний добуток вектора коефіцієнтів і вектора вхідних відліків — рівно N операцій множення-накопичення.На CPU загального призначення FIR-фільтр зі 100 відводами може потребувати:
100 множень (кожне — багатотактна операція)
100 додавань
200 завантажень з пам'яті (100 коефіцієнтів + 100 відліків)
Накладні витрати циклу (100 зменшень лічильника + 100 умовних переходів)
Навіть з оптимізуючим компілятором це може забрати 1 000 тактів або більше на вихідний відлік.На DSP штибу SHARC той самий фільтр зі 100 відводами займає приблизно 105–110 тактів на вихідний відлік. Ось чому:
Цикл з нульовими накладними витратами усуває всі інструкції керування циклом
Кожна інструкція MAC (одна за такт) одночасно множить, накопичує, завантажує наступний коефіцієнт (через DAG1) та завантажує наступний відлік (через DAG2 з переносом циклічного буфера)
Завдяки кешу інструкцій їх завантаження не конкурує із завантаженням даних
Ще кілька додаткових тактів іде на налаштування циклу й заповнення/спустошення конвеєра
DSP виходить на темп приблизно один відвід фільтра за такт — приблизно вдесятеро ефективніше за CPU загального призначення на тій самій тактовій частоті, і це за простішої апаратури та меншого енергоспоживання.У наступній таблиці зведено ключові архітектурні відмінності між CPU загального призначення та DSP:Варто зазначити, що багато з цих відмінностей стосуються зовсім не сирої швидкості — DSP зазвичай працюють на значно нижчих тактових частотах, ніж CPU. Перевага полягає в ефективності на такт і ефективності на ват. DSP на 500 МГц може зрівнятися за пропускною здатністю обробки сигналів із CPU на 4 ГГц або навіть перевершити його, споживаючи лише частку його потужності.<br><img alt="Мікросхема DSP TMS320 від Texas Instruments на друкованій платі" src="assets/l10-dsp-tms320-chip.jpeg" target="_self">Сучасна мікросхема DSP TI TMS320VC5509A на друкованій платі — одне з найпоширеніших сімейств DSP.DSP усюди, хоча кінцеві користувачі їх рідко бачать. Ось де вони працюють.Обробка аудіо. Кожен смартфон містить DSP для ехокомпенсації, шумоподавлення та аудіокодеків (MP3, AAC, Opus). Професійне аудіообладнання — мікшерні консолі, процесори ефектів, цифрові кросовери — тримається на DSP, які відповідають за фільтрацію та еквалізацію в реальному часі. Голосові помічники на кшталт Siri та Alexa спираються на DSP для постійного виявлення ключових слів («Hey Siri») за надзвичайно низького енергоспоживання.Телекомунікації. Базові станції стільникового зв'язку 4G/5G покладаються на DSP для кодування/декодування каналів, формування діаграми спрямованості (beamforming — спрямовування антенних діаграм на конкретних користувачів) та модуляції/демодуляції. Одна базова станція може містити десятки DSP, які обслуговують тисячі одночасних з'єднань.Радар і гідролокатор. Військові та цивільні радарні системи застосовують DSP для стиснення імпульсів, доплерівської обробки та супроводу цілей. Автомобільний радар (його використовують в адаптивному круїз-контролі та автономному водінні) спирається на DSP для обробки зворотних сигналів і розпізнавання автомобілів та перешкод навколо.<br><img alt="Вінтажна мікросхема DSP від TI в золотому керамічному корпусі" src="assets/l10-dsp-chip-vintage.jpeg" target="_self">Рання мікросхема DSP від TI в золотому керамічному корпусі — архітектури DSP оптимізувалися для обробки сигналів з 1980-х років.Медичні пристрої. Слухові апарати, кохлеарні імпланти, монітори ЕКГ/ЕЕГ та системи ультразвукової візуалізації — усі вони тримаються на DSP, які забезпечують фільтрацію та аналіз сигналів у реальному часі.Обробка зображень і відео. Хоча у високопродуктивній обробці зображень домінують GPU (як ми побачимо в наступному розділі), у вбудованих пристроях з обмеженим енергоспоживанням — камерах спостереження, дронах, ендоскопах — зображення обробляють саме DSP.Системи керування. Приводи двигунів, силові інвертори та обладнання промислової автоматизації спираються на DSP у контурах керування в реальному часі: зчитування даних сенсорів, обчислення PID-корекцій та формування керуючих сигналів — усе це в межах мікросекундних дедлайнів.<br><img alt="Крупний план сучасного CPU, що показує масив контактних виводів" src="assets/l10-cpu-closeup.png" target="_self">CPU загального призначення — потужний і гнучкий, але менш ефективний за DSP для спеціалізованих завдань обробки сигналів.<br>DSP, імовірно, — перша комерційно успішна предметно-орієнтована архітектура. Texas Instruments представила <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Texas_Instruments_TMS320" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Texas_Instruments_TMS320" target="_self">TMS32010</a> у 1983 році, і сімейство TMS320 стало однією з найбільш масово розгорнутих процесорних архітектур в історії — не за впізнаваністю назви (за межами вбудованих систем про TMS320 мало хто чув), а за чистим обсягом виробництва. Виготовлено мільярди ядер DSP, і більшість з них непомітно працює всередині інших продуктів.Принципи проєктування, закладені DSP, — Гарвардська архітектура заради пропускної здатності пам'яті, блоки MAC заради продуктивності, цикли з нульовими накладними витратами заради ефективності, насичена арифметика заради цілісності сигналу — вплинули на проєктування кожного спеціалізованого процесора, що з'явився пізніше. Розглядаючи далі GPU, TPU та нейроморфні процесори, ми впізнаватимемо відлуння мислення DSP: ту саму стратегію — виокремити ключову операцію предметної області та побудувати апаратуру, яка виконує саме її максимально ефективно.DSP також продемонстрував принцип, до якого решта комп'ютерної індустрії остаточно дійшла лише за десятиліття: правильно дібрана під задачу архітектура важливіша за сиру тактову частоту. DSP на 40 МГц у 1995 році давав собі раду з сигналами, з якими Pentium на 200 МГц не впорювався в реальному часі. Цей урок — спеціалізація перемагає універсальність, коли робоче навантаження відоме — є центральною темою всієї цієї лекції.Процесори, які ми розглянули досі — CPU загального призначення та оптимізовані для обробки сигналів DSP — спираються на спільне фундаментальне припущення: відносно невелика кількість потужних виконавчих пристроїв обробляє інструкції якнайшвидше. Такий підхід чудово працює для послідовного коду зі складним потоком керування, розгалуженою логікою та нерегулярними шаблонами доступу до даних. Проте існує цілий всесвіт обчислень, де жодна з цих переваг не має значення, — навантаження, що складаються з мільйонів або мільярдів однакових незалежних операцій над величезними наборами даних. Рендеринг кадру відеогри, навчання нейронної мережі, симуляція динаміки рідин — ці задачі не потребують кількох швидких ядер. Вони потребують тисяч простих.Саме в цьому полягає ідея графічного процесора (GPU). Там, де CPU оптимізовано під затримку — виконання будь-якого окремого завдання якнайшвидше, — GPU оптимізовано під пропускну здатність — виконання якомога більшої кількості завдань за одиницю часу, навіть якщо кожне окреме завдання триває довше. У результаті маємо радикально іншу архітектуру — ту, що виросла зі спеціалізованого графічного прискорювача в найважливішу платформу паралельних обчислень XXI століття.GPU народився з дуже конкретної проблеми — рендерингу 3D-графіки в реальному часі. Один кадр сучасної відеогри в роздільності 4K містить понад 8 мільйонів пікселів. Для кожного пікселя рендерер має визначити, який 3D-об'єкт видно в цій точці, обчислити, як світло взаємодіє з поверхнею об'єкта (текстурування, затінення, відображення), і сформувати підсумкове значення кольору. За 60 кадрів на секунду це майже 500 мільйонів пікселів щосекунди — і кожне обчислення складається з однакової послідовності математичних операцій (множення матриць, скалярні добутки, інтерполяція), які застосовуються незалежно до різних даних.<br>Це класичний приклад паралелізму даних — той самий шаблон, під який створено архітектури SIMD (одна інструкція, багато даних) (ми вивчали їх у <a class="internal-link" data-href="L09-ENG. ISA Design and Models of Computation.md" href="lectures/eng/l09-eng.-isa-design-and-models-of-computation.html" target="_self" rel="noopener nofollow">Лекції 9</a>, Розділ 10.1). Проте масштаб паралелізму в графіці значно перевищує можливості розширень SIMD для CPU, як-от SSE чи AVX. 512-бітний регістр AVX-512 обробляє 16 чисел з рухомою комою одночасно; сучасний GPU — десятки тисяч чисел з рухомою комою паралельно. GPU — це, по суті, SIMD, доведений до логічної межі.<br>Еволюція від фіксованої функціональності до програмованості. Ранні GPU (кінець 1990-х) були <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Graphics_pipeline" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Graphics_pipeline" target="_self">конвеєрами з фіксованою функціональністю</a> — жорстко запрограмованими схемами, що виконували конкретні графічні операції (перетворення вершин, растеризацію, накладання текстур) у наперед визначеному порядку. Розробники могли налаштовувати параметри, однак не могли змінювати самі операції. NVIDIA GeForce 256 (1999), яку позиціонували як перший у світі «GPU», уміла апаратно обчислювати перетворення та освітлення (T&amp;L), що раніше потребувало CPU, але її конвеєр лишався жорстким.<br>Прорив стався з появою програмованих шейдерів. Починаючи з GeForce 3 (2001) і прискорюючись із серією GeForce 6 (2004), GPU здобули здатність виконувати довільні програми — так звані <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Shader" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Shader" target="_self">шейдери</a> — на різних етапах графічного конвеєра. Вершинні шейдери перетворювали 3D-координати; піксельні (фрагментні) шейдери обчислювали підсумковий колір кожного пікселя. Ці програми писали спеціалізованими мовами, як-от GLSL (OpenGL Shading Language) або HLSL (High-Level Shading Language для DirectX).Ключовим усвідомленням стало те, що вершинні та піксельні шейдери виконують принципово схожу роботу: обидва запускають невеликі програми на незалежних елементах даних. Замість окремого апаратного забезпечення для кожного типу шейдерів архітектори GPU перейшли до уніфікованої шейдерної архітектури (її представила ATI Xpress 200 у 2004 році, а повністю реалізовано її в серії NVIDIA GeForce 8 у 2006 році). В уніфікованій архітектурі той самий пул обчислювальних ядер може виконувати будь-який тип шейдерної програми, динамічно розподіляючи ресурси відповідно до вимог навантаження. Ця уніфікація стала архітектурною основою, що зробила обчислення загального призначення на GPU (GPGPU) практичними.Сучасні ролі GPU. Сьогодні GPU обслуговують значно більше, ніж просто 3D-графіку: 2D-прискорення та виведення зображення — компонування вікон робочого столу, рендеринг анімацій інтерфейсу, керування кількома моніторами з одного буфера кадру Кодування та декодування відео — апаратні кодеки для H.264, H.265/HEVC, AV1, прискорення стримінгу та відеомонтажу Машинне навчання та ШІ — навчання та висновування нейронних мереж, де домінуючі операції (множення матриць, згортки) є масово паралельними щодо даних Наукові обчислення — симуляції молекулярної динаміки, моделювання погоди, обчислювальна гідродинаміка, фінансові симуляції Монте-Карло Майнінг криптовалют — використання «сирої» арифметичної пропускної здатності GPU для обчислення хешів (хоча цей напрямок застосування зменшується з переходом на proof-of-stake) GPU є майже в кожному обчислювальному пристрої: інтегрованими в мобільні SoC (графічні ядра Apple в чипах серій A та M, Qualcomm Adreno, ARM Mali), у вигляді дискретних карт в ігрових ПК та робочих станціях (NVIDIA GeForce/RTX, AMD Radeon), а також як спеціалізовані прискорювачі в серверах дата-центрів (NVIDIA A100, H100, B200; AMD Instinct MI300X; Intel Data Center GPU Max Series).<br><img alt="Карти Intel Data Center GPU Flex Series для PCIe" src="assets/l10-gpu-intel-flex-series.jpeg" target="_self">Intel Data Center GPU Flex Series — сучасні GPU випускаються як карти PCIe для серверів і дата-центрів, а не лише для ігор.Ринок GPU для дата-центрів стрімко зростає з 2020 року — його штовхає вперед потреба навчати великі мовні моделі. Один цикл навчання передової моделі ШІ може вимагати тисяч GPU, які працюють паралельно протягом тижнів чи місяців, — і це робить архітектуру GPU однією з найбільш економічно значущих тем сучасної комп'ютерної інженерії.Щоб зрозуміти, чому GPU виглядають настільки відмінно від CPU, пригадаймо фундаментальний компроміс у проєктуванні процесорів: площу кристала та енергоспоживання можна спрямувати або на прискорення окремих операцій (оптимізація затримки), або на одночасне виконання більшої кількості операцій (оптимізація пропускної здатності). CPU та GPU роблять протилежні вибори.Сучасний CPU витрачає більшу частину свого транзисторного бюджету — приблизно 60–70% — на структури, що зменшують затримку для послідовного коду: великі багаторівневі кеші (L1, L2, L3), глибокі конвеєри з позачерговим виконанням, складні блоки передбачення переходів, механізми спекулятивного виконання та складні декодери інструкцій. Самі арифметико-логічні пристрої (АЛП), які виконують обчислення, займають напрочуд малу частку площі кристала. Такий підхід виправданий для навантажень загального призначення, де наступна інструкція залежить від результату попередньої, розгалуження часті й непередбачувані, а шаблони доступу до даних нерегулярні.GPU інвертує цей розподіл. Він віддає переважну більшість транзисторів арифметичним пристроям — тисячам простих ядер з порядковим виконанням — і залишає лише мінімальне кешування та повністю обходиться без передбачення переходів. Логіка проста: якщо навантаження складається з мільйонів однакових незалежних операцій, глибокі конвеєри чи спекулятивне виконання не потрібні. Потрібна чиста арифметична пропускна здатність. Коли один потік зупиняється в очікуванні даних із пам'яті, GPU просто перемикається на інший — у будь-який момент на ньому виконуються тисячі потоків, а перемикання між ними фактично нічого не коштує (перемикання контексту відбувається без витрат, оскільки стани всіх потоків зберігаються у величезному регістровому файлі, а не потребують збереження та відновлення з пам'яті).<br><img alt="Архітектура CPU проти GPU: 4 великих ядра CPU проти багатьох малих мультипроцесорів GPU" src="assets/l10-cpu-vs-gpu-cores.jpeg" target="_self">Розташування ядер CPU та GPU: CPU має кілька потужних ядер, оптимізованих для послідовних задач; GPU має тисячі простіших ядер, оптимізованих для паралельної пропускної здатності.Діаграма вище ілюструє фундаментальну структурну різницю. CPU має невелику кількість великих складних ядер (4–16 у споживчих чипах, до 128 у серверних процесорах), кожне з яких здатне незалежно обробляти різноманітні розгалужені навантаження. GPU натомість має багато малих простих ядер (від тисяч до десятків тисяч), кожне з яких здатне виконувати одну арифметичну операцію за такт, проте з мінімальною незалежною логікою керування.Паралелізм даних, а не паралелізм завдань. GPU чудово справляються з паралелізмом даних — виконанням однієї й тієї самої операції над багатьма елементами даних одночасно. Вони погано пасують до паралелізму завдань, де різні ядра виконують принципово різні операції. У термінах таксономії Флінна (Лекція 9, Розділ 8) GPU концептуально є SIMD-машиною масивного масштабу — хоча, як ми побачимо в Розділі 3.6, точніша класифікація — SIMT (одна інструкція, багато потоків), що додає важливу гнучкість порівняно з чистим SIMD.Висока пропускна здатність пам'яті. Обчислення, орієнтовані на пропускну здатність, потребують не лише численних АЛП, а й здатності постачати цим АЛП дані достатньо швидко. Тому GPU використовують технології пам'яті з високою пропускною здатністю — GDDR6X у споживчих картах та HBM (пам'ять високої пропускної здатності) у GPU для дата-центрів, — з'єднані через надзвичайно широкі шини пам'яті. NVIDIA H100, наприклад, забезпечує приблизно 3,35 ТБ/с пропускної здатності пам'яті за допомогою HBM3 — приблизно вдесятеро більше, ніж високопродуктивний серверний CPU. Без цієї пропускної здатності тисячі обчислювальних ядер простоювали б більшу частину часу, очікуючи на дані.Звернімо увагу на закономірність: GPU має значно менший загальний обсяг пам'яті (80 ГБ проти потенційних 1,5 ТБ), проте звертається до неї приблизно всемеро швидше. GPU працює на нижчих тактових частотах, але компенсує це більш ніж у 100 разів більшою кількістю обчислювальних ядер. І GPU видає понад 12-кратну продуктивність в операціях з рухомою комою при приблизно подвоєному енергоспоживанні — значно кращий показник продуктивності на ват для навантажень із паралелізмом даних.Тисячі ядер усередині GPU не організовані у вигляді плаского масиву. Вони впорядковані в ієрархічну структуру, що балансує між паралелізмом і здатністю обмінюватися даними та координувати виконання. Основним будівельним блоком цієї ієрархії є потоковий мультипроцесор (SM) — термін NVIDIA. AMD називає еквівалентний блок обчислювальним модулем (CU), а Intel — ядром Xe (або, у старіших архітектурах, кластером виконавчих пристроїв). Попри різницю в назвах, базова концепція у всіх трьох виробників однакова.<br>Ядра CUDA. Кожен SM містить набір простих обчислювальних елементів, які NVIDIA називає <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/CUDA#Design" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/CUDA#Design" target="_self">ядрами CUDA</a> (ядра Compute Unified Device Architecture). Ядро CUDA — це спрощений арифметичний пристрій, значно простіший за ядро CPU. Воно може виконувати одну арифметичну операцію з рухомою комою або цілочисельну операцію за такт (найпоширеніша — FMA, злите множення-додавання). В окремому ядрі CUDA немає ні блоку передбачення переходів, ні логіки позачергового виконання, ні спекуляції — воно просто отримує інструкцію та дані, обчислює результат і записує його назад.Сучасний SM від NVIDIA (як в архітектурі Hopper, що використовується в H100) містить 128 ядер CUDA. GPU H100 має 132 SM, що загалом дає 16 896 ядер CUDA. AMD Radeon RX 7900 XTX має 96 обчислювальних модулів по 64 потокові процесори в кожному, загалом 6 144. Intel Data Center GPU Max 1550 має 128 ядер Xe. Числа різняться, проте принцип універсальний: багато малих ядер, організованих у групи.<br><img alt="Архітектура GPU NVIDIA, що показує ієрархію потокових мультипроцесорів, кеш L2 та DRAM" src="assets/l10-gpu-nvidia-sm-architecture.png" target="_self">Всередині GPU NVIDIA: кожен потоковий мультипроцесор (SM) містить потокові процесори (SP), блоки завантаження/збереження, текстурні блоки та пам'ять на кристалі. Кілька SM спільно використовують кеш L2, з'єднаний із DRAM.Виконання потоків у варпах. Усередині SM потоки не виконуються незалежно. Вони згруповані в одиниці, що називаються варпами (термінологія NVIDIA; AMD називає їх хвильовими фронтами). Варп складається з 32 потоків, що виконуються синхронно: усі 32 потоки у варпі виконують одну й ту саму інструкцію в один і той самий момент часу, але кожен потік працює з власними даними. Це апаратне втілення моделі виконання SIMT (одна інструкція, багато потоків).SM не задіює всі 128 ядер CUDA на одному варпі одночасно. Натомість він містить кілька планувальників варпів (зазвичай 4 у сучасних архітектурах NVIDIA), і кожен з них здатен видавати інструкцію групі з 32 ядер. На кожному такті кожен планувальник варпів обирає готовий варп зі свого пулу та відправляє наступну інструкцію для цього варпа. Оскільки сотні варпів можуть одночасно бути резидентними на одному SM (H100 утримує до 64 варпів на SM, тобто 2 048 потоків), планувальник завжди має готові варпи для вибору — навіть коли деякі з них зупинені в очікуванні даних із пам'яті.Це основний механізм приховування затримки в GPU. CPU приховує затримку пам'яті за допомогою кешів та спекуляції. GPU приховує її, перемикаючись на інший варп, поки зупинений варп чекає. Жодних витрат на перемикання контексту, жодного штрафу за промах кешу (у традиційному розумінні) — просто інша група з 32 потоків продовжує роботу з того місця, де зупинилася.Розбіжність варпів: ахіллесова п'ята. Модель варпів працює чудово, коли всі 32 потоки йдуть одним і тим самим шляхом потоку керування. Але що відбувається, коли потоки зустрічають розгалуження — оператор if/else — і одні потоки обирають шлях if, а інші — шлях else?Це явище називають розбіжністю варпів, і воно — найважливіший підводний камінь продуктивності в програмуванні GPU. Коли варп розбігається, апаратне забезпечення не може виконати обидва шляхи одночасно (адже всі потоки у варпі поділяють один лічильник команд). Натомість воно серіалізує шляхи: спочатку виконує шлях if з вимкненими потоками гілки else (вони не виконують корисної роботи), потім — шлях else з вимкненими потоками гілки if. Обидва шляхи проходять виконання, а маска обирає правильні результати для кожного потоку.Варп із 32 потоків зустрічає: if (condition) { A } else { B } Потоки 0-19: condition = true Потоки 20-31: condition = false Крок 1: Виконати шлях A — потоки 0-19 активні, 20-31 замасковані (простоюють) Крок 2: Виконати шлях B — потоки 20-31 активні, 0-19 замасковані (простоюють) Крок 3: Повторне з'єднання — усі 32 потоки відновлюють виконання однієї інструкції
У найгіршому випадку — коли кожен потік обирає свій шлях — варп стає повністю послідовним і марнує 31/32 своєї потенційної пропускної здатності. Саме тому GPU-дружні алгоритми мінімізують розгалуження, і саме тому модель SIMT, хоч і гнучкіша за чистий SIMD (Розділ 3.6), усе ж штрафує за розбіжний потік керування.Ієрархія пам'яті. GPU мають багаторівневу ієрархію пам'яті, оптимізовану для пропускної здатності, а не для затримки:<br>Регістровий файл у GPU величезний порівняно з CPU — H100 має 256 КБ регістрів на SM, тобто загалом понад 33 МБ на всіх SM. Це не збіг. Оскільки GPU обробляють тисячі резидентних потоків одночасно, кожному потоку потрібен власний набір регістрів, і ці регістри мусять бути фізично присутніми (не закешованими чи вивантаженими), щоб забезпечити безкоштовне перемикання варпів, яке приховує затримку пам'яті. Як <a data-tooltip-position="top" aria-label="https://www.yosefk.com/blog/simd-simt-smt-parallelism-in-nvidia-gpus.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.yosefk.com/blog/simd-simt-smt-parallelism-in-nvidia-gpus.html" target="_self">зауважує Йосеф Крейнін</a>, регістри GPU «більше схожі на оперативну пам'ять, ніж на регістри» — це великий плаский масив зберігання, індексований за ідентифікатором потоку, а не малі швидкі регістри на основі тригерів, як у CPU.Спільна пам'ять — це ключова архітектурна особливість, що відрізняє GPU від простих масивів SIMD. Це керований програмістом блокнот — невеликий швидкий банк пам'яті, з якого можуть читати й до якого можуть записувати всі потоки в межах одного блоку потоків (Розділ 3.5). Спільна пам'ять уможливлює співпрацю потоків: один потік може обчислити частковий результат, зберегти його у спільній пам'яті, а інший — прочитати. Цей кооперативний шаблон необхідний для алгоритмів на кшталт множення матриць, де фрагменти вхідних матриць завантажують у спільну пам'ять і багаторазово перевикористовують.Спеціалізовані ядра. Сучасні GPU включають спеціально створене апаратне забезпечення, крім ядер CUDA/потокових процесорів загального призначення: Тензорні ядра (NVIDIA, представлені з архітектурою Volta у 2017 році) — спеціалізовані блоки для множення матриць, що виконують операцію множення-накопичення матриць 4×4 за один такт. Одна операція тензорного ядра замінює те, що потребувало б 64 операцій FMA на ядрах CUDA. Тензорні ядра H100 забезпечують до 1 979 TFLOPS продуктивності FP8 — майже у 30 разів більше за пропускну здатність самих лише його ядер CUDA. Це робочі конячки навчання та висновування глибокого навчання. <br>
RT-ядра (NVIDIA, представлені з архітектурою Turing у 2018 році) — апаратно прискорені блоки <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Ray_tracing_(graphics)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Ray_tracing_(graphics)" target="_self">трасування променів</a>, що обчислюють тести перетину «промінь — ієрархія обмежувальних об'ємів (BVH)». Трасування променів — симуляція фізичної поведінки світла — потребує перевірки кожного променя проти тисяч геометричних примітивів. Робити це програмно на ядрах CUDA можливо, проте надто повільно для рендерингу в реальному часі; RT-ядра прискорюють цю вузьку операцію на порядки. AI-движки / матричні ядра — еквівалент тензорних ядер від AMD (в архітектурах CDNA/RDNA) та XMX від Intel (Xe Matrix eXtensions) відіграють ту саму роль: апаратно прискорені матричні операції для навантажень глибокого навчання. Відмінності між CPU та GPU — це не просто кількісні показники (більше ядер, вища пропускна здатність); вони відображають принципово різні філософії проєктування, оптимізовані під різні класи обчислень. Наступна таблиця підсумовує ключові архітектурні відмінності:Аналогія з кухарем. Уявімо ядро CPU як шеф-кухаря у висококласному ресторані. Шеф може приготувати будь-яку страву — суші, пасту, випічку, — вільно перемикаючись між техніками, пристосовуючись до спеціальних замовлень та імпровізуючи, коли бракує інгредієнтів. Але шеф лише один (або кілька у багатоядерному CPU).GPU, навпаки, подібний до фабричного конвеєра з 10 000 працівників, кожен з яких натренований виконувати рівно один крок. Вони не можуть імпровізувати, не можуть ефективно опрацьовувати особливі випадки, і кожен з них повільніший за шеф-кухаря. Та коли завдання — виготовити 10 000 однакових страв, фабрика впорається за час, за який шеф приготує лише одну.Жоден із них не є універсально кращим. Питання завжди одне: чи має навантаження достатньо паралелізму даних, щоб дати роботу тисячам простих ядер?Протягом більшої частини ранньої історії GPU єдиним способом використати його паралельну обчислювальну потужність для неграфічних задач було замаскувати обчислення під графічну операцію — закодувати дані як текстури, а алгоритм як шейдерну програму. Цей підхід, відомий як GPGPU (обчислення загального призначення на GPU), був незручним, схильним до помилок і обмеженим задачами, що вкладалися в графічний конвеєр.<br>У листопаді 2006 року NVIDIA змінила ландшафт, випустивши <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/CUDA" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/CUDA" target="_self">CUDA</a> (Compute Unified Device Architecture) — платформу паралельних обчислень і модель програмування, що давала змогу розробникам писати програми загального призначення для GPU, використовуючи розширену версію мови C. Уперше програмісти могли безпосередньо виражати паралельні алгоритми без абстракції графічного конвеєра.CUDA ввела концепцію гетерогенних обчислень: CPU (його називають хостом) і GPU (його називають пристроєм) працюють разом, кожен виконує ту частину навантаження, з якою справляється найкраще. Хост виконує послідовний код, керує введенням-виведенням та оркеструє виконання. Коли справа доходить до паралельної щодо даних частини обчислення, хост запускає ядро (kernel) — функцію, що виконується на GPU одночасно в тисячах потоків.<br><img alt="Архітектура виконання потоків GPU NVIDIA: від хоста до менеджера потоків і паралельних обчислювальних блоків" src="assets/l10-gpu-thread-execution.jpeg" target="_self">Модель виконання GPU: хост відправляє роботу через менеджер виконання потоків, який розподіляє потоки між паралельними обчислювальними блоками, кожен з яких має локальний кеш даних та текстурні блоки.Ієрархія потоків CUDA. CUDA організовує паралельне виконання в трирівневу ієрархію: Потік — найменша одиниця виконання. Кожен потік виконує ту саму функцію ядра (kernel), але працює з різними даними (його визначає унікальний індекс потоку). Кожен потік має власні регістри та локальну пам'ять. Варп — група з 32 потоків, що виконуються синхронно (як описано в Розділі 3.3). Варпи програміст явно не контролює — це деталь апаратної реалізації. Проте програмування з урахуванням варпів (уникнення розбіжності, забезпечення коалесцентного доступу до пам'яті) критично важливе для продуктивності. Блок потоків (або просто блок) — група потоків (до 1 024), що виконуються на одному SM і можуть співпрацювати через спільну пам'ять та бар'єри синхронізації (__syncthreads()). Усі потоки в межах блоку гарантовано розміщені на одному SM, а отже, можуть обмінюватися даними через спільну пам'ять із низькою затримкою. Сітка — сукупність усіх блоків потоків, запущених одним викликом ядра (kernel). Блоки в межах сітки незалежні — вони не можуть обмінюватися даними під час виконання (окрім як через глобальну пам'ять, яка не синхронізована). Ця незалежність дає апаратному забезпеченню GPU змогу планувати блоки на SM у будь-якому порядку, а це забезпечує масштабованість між GPU з різною кількістю SM. Сітка
├── Блок (0,0) Блок (1,0) ... Блок (N,0)
│ ├── Варп 0: потоки 0–31 ├── Варп 0 ...
│ ├── Варп 1: потоки 32–63 ├── Варп 1 ...
│ └── ... └── ... ...
├── Блок (0,1) Блок (1,1) ...
│ └── ... └── ... ...
└── ...
Концептуальний приклад. Розгляньмо додавання двох масивів по 1 мільйону елементів. У CUDA програміст:
Виділяє пам'ять на GPU та копіює обидва вхідні масиви з пам'яті CPU в пам'ять GPU.
Запускає ядро (kernel) з, скажімо, 1 024 блоками по 1 024 потоки в кожному (1 048 576 потоків загалом — по одному на елемент).
Кожен потік обчислює C[i] = A[i] + B[i], де i — його унікальний глобальний індекс потоку.
Копіює масив результатів назад із пам'яті GPU в пам'ять CPU.
У псевдокоді:// Функція ядра (kernel) — виконується на GPU, один екземпляр на потік
kernel vectorAdd(A, B, C, N): i = blockIndex * blockSize + threadIndex if i &lt; N: C[i] = A[i] + B[i] // Код хоста — виконується на CPU
allocate_on_gpu(d_A, d_B, d_C, size=N)
copy_to_gpu(d_A, host_A)
copy_to_gpu(d_B, host_B)
launch kernel vectorAdd(d_A, d_B, d_C, N) with grid=(1024 blocks), block=(1024 threads)
copy_from_gpu(host_C, d_C)
Кожен потік виконує одне додавання — тривіально просто. Але 1 мільйон потоків, що виконуються одночасно (на практиці — хвилями, запланованими апаратним забезпеченням), завершують усю операцію на порядки швидше за цикл на CPU. Накладні витрати на копіювання даних до GPU та назад — це головний чинник продуктивності: для малих масивів ці витрати можуть перевищити час обчислення, і тоді GPU-прискорення стає контрпродуктивним. Обчислення на GPU окуповуються, коли співвідношення обчислень до комунікацій високе — великі набори даних із великою кількістю арифметичних операцій на кожен елемент.Синхронізація та модель пам'яті. Усередині блоку потоки можуть синхронізуватися за допомогою бар'єрних функцій: __syncthreads() гарантує, що всі потоки в блоці досягли однієї точки, перш ніж будь-який з них продовжить. Ефективної міжблокової синхронізації в межах одного запуску ядра (kernel) не існує — це зроблено навмисно, бо глобальна комунікація паралізувала б масштабованість. Коли потрібна глобальна синхронізація, програміст запускає нове ядро (kernel) (адже всі блоки попереднього ядра гарантовано завершуються, перш ніж почнеться наступне).Альтернативи CUDA. Хоча CUDA домінує в обчисленнях на GPU (особливо для машинного навчання), це пропрієтарна технологія NVIDIA, що не працює на GPU AMD чи Intel. Існує кілька кросплатформних альтернатив:Попри ці альтернативи, десятирічна перевага CUDA, зрілий інструментарій (cuBLAS, cuDNN, TensorRT) та домінування в екосистемі машинного навчання (і PyTorch, і TensorFlow використовують CUDA як основний GPU-бекенд) дають їй потужний мережевий ефект, який конкуренти намагаються подолати.У Лекції 9 ми класифікували архітектури за таксономією Флінна та детально вивчили SIMD: одна інструкція, що застосовується одночасно до кількох елементів даних, упакованих у широкі векторні регістри (128-бітні регістри XMM у SSE, 512-бітні регістри ZMM у AVX-512). Виконання на GPU виглядає подібно — одна інструкція, що застосовується до багатьох елементів даних, — проте механізм принципово інший. Розуміння відмінностей між SIMD, SIMT та SMT (одночасною багатопотоковістю) розкриває цілий спектр моделей паралельного виконання з різними компромісами між гнучкістю та ефективністю.SIMD: жорстке векторне виконання. У моделі SIMD процесора програміст (або компілятор) має явно упаковувати дані у векторні регістри та видавати векторні інструкції. Усі елементи вектора обробляються однією операцією, і поняття окремих «потоків» для кожного елемента не існує. Щоб додати два масиви по 16 чисел з рухомою комою за допомогою AVX-512:; CPU SIMD (AVX-512): явне пакування та обробка 16 чисел з рухомою комою одночасно
VMOVUPS ZMM0, [A] ; Завантажити 16 чисел з масиву A у 512-бітний регістр
VMOVUPS ZMM1, [B] ; Завантажити 16 чисел з масиву B у 512-бітний регістр
VADDPS ZMM2, ZMM0, ZMM1 ; Додати всі 16 пар одночасно
VMOVUPS [C], ZMM2 ; Зберегти 16 результатів
Це швидко й ефективно, проте жорстко. Усі 16 елементів мають бути завантажені з неперервної ділянки пам'яті. Усі 16 мають пройти одну й ту саму операцію — немає способу, щоб елемент 5 пішов однією гілкою, а елемент 12 — іншою. Якщо один елемент потребує спеціальної обробки (наприклад, граничної умови), програмістові доводиться застосовувати прийоми з масками або повертатися до скалярного коду. Модель програмування явно векторизована: код «мислить» у термінах векторів, а не окремих елементів.<br>SIMT: потоки з неявною векторизацією. Модель SIMT (одна інструкція, багато потоків) GPU (<a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Single_instruction,_multiple_threads" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Single_instruction,_multiple_threads" target="_self">Single Instruction, Multiple Threads</a>) інвертує абстракцію. Програміст пише скалярний код — код, що описує поведінку одного потоку, — а апаратне забезпечення прозоро групує потоки у варпи та виконує їх синхронно. У принципі, програмістові не потрібно знати ані про розмір варпа, ані про векторні регістри, ані про синхронне виконання (на практиці оптимізація з урахуванням варпів важлива для продуктивності).<br><a data-tooltip-position="top" aria-label="https://www.yosefk.com/blog/simd-simt-smt-parallelism-in-nvidia-gpus.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.yosefk.com/blog/simd-simt-smt-parallelism-in-nvidia-gpus.html" target="_self">Аналіз Йосефа Крейніна</a> виділяє три ключові переваги SIMT над SIMD: Множинні набори регістрів. Кожен потік SIMT має власний повний набір регістрів. У SIMD усі елементи поділяють один набір векторних регістрів, тому код має писатися в явно векторизованому стилі. У SIMT кожен потік пише звичайний скалярний код — result = a + b, — а апаратне забезпечення відображає «скалярні» регістри кожного потоку на різні записи у великому фізичному регістровому файлі. Це робить GPU-код схожим на звичайні послідовні програми, а не на векторні інтринсики. Множинні адреси. У SIMD векторне завантаження отримує дані з одного неперервного діапазону адрес (скажімо, 16 послідовних чисел з рухомою комою). У SIMT кожен потік у варпі може незалежно обчислювати власну адресу — потоки можуть звертатися до розкиданих, непослідовних ділянок пам'яті. Апаратне забезпечення коалесцує звернення, коли це можливо (об'єднує їх в одну широку транзакцію пам'яті, якщо потоки звертаються до близьких адрес); некоалесцентний доступ також допустимий, просто повільніший. Це відкриває шлях паралельним структурам даних із нерегулярними шаблонами доступу — хеш-таблицям та графам, — які неможливо ефективно виразити в чистому SIMD. Множинні шляхи потоку (обробка розбіжності розгалужень). У SIMD немає механізму, щоб різні елементи вектора обирали різні гілки, — умовна обробка потребує явного маскування програмістом. У SIMT потоки можуть розбігатися на розгалуженнях. Як ми обговорювали в Розділі 3.3, апаратне забезпечення серіалізує розбіжні шляхи та використовує маскування для вибору правильних результатів — отже, розбіжність коштує продуктивності, проте не вимагає від програміста ручного керування масками. Апаратне забезпечення обробляє це прозоро. SMT: повністю незалежні потоки. На іншому кінці спектра — SMT, одночасна багатопотоковість (Simultaneous Multithreading) — техніка, що використовується в CPU, коли одне ядро виконує кілька повністю незалежних потоків, розділяючи між ними виконавчі ресурси (АЛП, кеші, конвеєри). Hyper-Threading від Intel — найвідоміша реалізація SMT: одне фізичне ядро CPU представляється операційній системі як два логічні ядра, кожне з власним архітектурним станом (регістри, лічильник команд), тоді як виконавчі блоки використовуються спільно.У SMT потоки справді незалежні — кожен виконує свою програму, звертається до своїх даних і обирає свої гілки без штрафу за розбіжність. Компроміс полягає в тому, що апаратні витрати на кожен потік значно більші: кожен логічний потік потребує повного архітектурного стану, а виконавчі блоки мусять розподілятися динамічно замість того, щоб працювати синхронно.Спектр. Ці три моделі утворюють спектр паралелізму, балансуючи між гнучкістю та ефективністю:Ключовий висновок: SIMD видобуває паралелізм із регулярності даних — коли дані однорідно структуровані, а операції однакові, SIMD — найефективніший вибір. SIMT послаблює вимоги до однорідності: для кожного потоку дозволено індивідуальну адресацію та розгалуження ціною накладних витрат на розбіжність варпів. SMT не висуває жодних вимог до однорідності й підтримує повністю незалежні потоки інструкцій — але ціна цього значно більші апаратні витрати на кожен потік.На практиці сучасні системи поєднують усі три підходи. GPU використовує SIMT як основну модель виконання, але також застосовує SIMD усередині кожного ядра CUDA для певних операцій (наприклад, операції з половинною точністю FP16, що пакують два значення на ядро CUDA). CPU використовує SMT для паралелізму на рівні потоків, SIMD — для паралелізму на рівні даних (інструкції AVX), а позачергове виконання — для паралелізму на рівні інструкцій; три різні форми паралелізму співіснують в одному процесорі.Розуміння того, куди в цей спектр потрапляє конкретне навантаження — наскільки однорідні дані? наскільки регулярний потік керування? наскільки незалежні операції? — визначає, яка модель виконання забезпечить найкращу продуктивність, а отже, що буде правильним вибором — CPU, GPU чи їх комбінація.Упродовж цього курсу ми вивчали процесори загального призначення — машини, здатні виконати будь-яку програму, яку їм пропонують. Саме ця універсальність — їхня найбільша перевага: один CPU (центральний процесор) може запускати веббраузер, базу даних, гру та компілятор — усе за один день. Але за універсальність доводиться платити. Процесор загального призначення повинен містити блоки передбачення переходів, механізми позачергового виконання, ієрархії кешів і керуючу логіку, яка враховує будь-яку комбінацію команд. Увесь цей транзисторний бюджет, уся ця споживана потужність витрачаються на гнучкість — навіть коли реальне навантаження виконує лише одну задачу.А що, якщо прибрати все непотрібне для конкретного навантаження й укласти кожен транзистор у те, щоб виконувати цю одну задачу якнайшвидше та якнайефективніше? Саме в цьому полягає ідея ASIC (спеціалізованої інтегральної схеми) — мікросхеми, спроєктованої з нуля для однієї чітко визначеної задачі.<br>Найяскравіший приклад того, навіщо існують ASIC, дає майнінг криптовалют, зокрема Bitcoin. Алгоритм proof-of-work Bitcoin вимагає від майнерів обчислювати трильйони хешів <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/SHA-2" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/SHA-2" target="_self">SHA-256</a> за секунду, шукаючи такий хеш, що потрапляє нижче порогу складності. Майнер, який першим знайде дійсний хеш, отримує винагороду за блок. Це створює нещадний економічний тиск: перемагає те обладнання, яке обчислює хеші швидше або з меншими витратами на ват.<br><img alt="Графік складності майнінгу Bitcoin, що показує перехід від CPU до GPU, FPGA та ASIC" src="assets/l10-bitcoin-mining-difficulty.jpeg" target="_self">Складність майнінгу Bitcoin (логарифмічна шкала, 2009–2021) ілюструє перехід CPU → GPU → FPGA → ASIC — кожне покоління спеціалізованого обладнання збільшувало хешрейт на порядки.Історія обладнання для майнінгу Bitcoin — це хрестоматійний рух по шкалі, на одному кінці якої гнучкість, а на іншому — продуктивність:Етап 1 — CPU-майнінг (2009–2010). У перші дні Bitcoin майнери використовували звичайні настільні процесори. Типовий Core i7 видавав приблизно 20–30 мегахешів за секунду (MH/s). Цього вистачало, поки складність мережі була низькою, а кількість охочих займатися майнінгом — невеликою.Етап 2 — GPU-майнінг (2010–2013). Майнери зрозуміли, що хешування SHA-256 чудово піддається масовому розпаралелюванню — кожна спроба обчислення хешу не залежить від інших. GPU (графічні процесори) із сотнями ядер, оптимізованих для паралельної арифметики, давали в 10–100 разів вищий хешрейт, ніж CPU, за порівнянного енергоспоживання. Потужний GPU досягав 500–800 MH/s.Етап 3 — FPGA-майнінг (2011–2013). FPGA (програмовані вентильні матриці), до яких детально повернемося в розділі 5, дозволили майнерам реалізувати хешування SHA-256 безпосередньо в реконфігурованому обладнанні, позбувшись накладних витрат GPU на вибірку, декодування та планування команд. FPGA давали кращу енергоефективність (хеші на ват), ніж GPU, хоча абсолютний хешрейт лишався подібним.Етап 4 — ASIC-майнінг (2013–дотепер). Спеціально створені ASIC-майнери з'явилися й миттєво зробили всі попередні підходи застарілими. Ранній ASIC-майнер на кшталт Avalon A3256 видавав 65 GH/s — більш ніж у 3 000 разів більше за GPU, — і при цьому споживав менше енергії. Сучасні ASIC-майнери працюють на сотнях терахешів за секунду (TH/s). Жоден CPU, GPU чи FPGA не здатний із ними конкурувати.<br><img alt="Майнінгова ферма з криптовалютним обладнанням на базі кількох GPU" src="assets/l10-asic-crypto-mining-rig.jpeg" target="_self">Майнінгова ферма на базі GPU — до появи спеціалізованих ASIC-майнерів для видобутку криптовалют використовували масиви споживчих GPU.Ця еволюція не є унікальною для криптовалют. Та сама економічна логіка діє скрізь, де одна обчислювальна задача домінує: відеокодування, обробка мережевих пакетів, висновування штучного інтелекту, обробка сигналів. Коли задача фіксована, а обсяги достатньо великі, спеціалізація завжди перемагає.ASIC — це інтегральна схема, спеціально спроєктована для конкретного застосування, а не для задач загального призначення. На відміну від CPU чи GPU, які мають обробляти довільні програми, ASIC реалізує фіксовану функцію — або вузьку родину споріднених функцій — безпосередньо в кремнії.Ключові характеристики, що відрізняють ASIC від процесорів загального призначення:
Фіксована функція. Логіка схеми визначається на етапі проєктування і вже не змінюється після виготовлення.
Оптимізований тракт даних. Кожен вентиль, кожен провідник, кожне обмеження за часом налаштовані на цільове навантаження. Жоден квадратний мікрометр кремнію не витрачається даремно на невикористані декодери команд чи регістрові файли загального призначення.
Низьке енергоспоживання, висока пропускна здатність. Оскільки мікросхема робить лише необхідне і нічого зайвого, вона досягає на порядки кращої продуктивності на ват, ніж альтернатива загального призначення.
ASIC поширені значно більше, ніж прийнято думати. Звичайний смартфон, імовірно, містить одразу кілька ASIC: процесор основної смуги для стільникового зв'язку, процесор обробки зображень (ISP) для камери, нейронний рушій для машинного навчання на пристрої та захищений анклав для криптографічних операцій. Блок керування двигуном в автомобілі, механізм пересилання пакетів у маршрутизаторі, контролер усередині SSD — усе це ASIC або ASIC-подібні пристрої, спроєктовані для однієї задачі й лише для неї.ASIC бувають дуже різні. Напівпровідникова індустрія пропонує цілий спектр методологій проєктування, де ступінь кастомізації виторговується за час розробки та вартість. Є три основні підходи:Повністю замовний ASIC (Full-Custom)У повністю замовному проєкті інженери визначають кожен шар маски мікросхеми — розміщення окремих транзисторів, прокладання кожного провідника, геометрію кожної комірки. Це дає проєктувальникам повний контроль над продуктивністю, потужністю та площею, але коштує надзвичайно дорого і забирає багато часу. Повністю замовне проєктування застосовують лише для найбільш масових і найпродуктивніших компонентів: флагманських CPU, провідних GPU та критичних аналогових схем. Виробничий цикл після завершення проєктування зазвичай становить близько 8 тижнів, проте саме проєктування може тривати роками.Стандартна комірка (Standard-Cell / CBIC)Проєктування на стандартних комірках спирається на бібліотеку заздалегідь розроблених і охарактеризованих логічних комірок — вентилів AND, тригерів, мультиплексорів, суматорів — кожна з відомими параметрами затримки, потужності та площі. Проєктувальник описує схему мовою опису апаратури (HDL), а автоматизовані інструменти компонують комірки в топологію, виконуючи розміщення та трасування. Усі шари масок усе ще кастомізуються під конкретний проєкт, однак геометрію окремих комірок беруть із бібліотеки. Такий підхід суттєво скорочує час проєктування порівняно з повністю замовним і водночас забезпечує дуже високу продуктивність. Виробничий цикл подібний до повністю замовного (~8 тижнів), а от цикл проєктування значно коротший.Сьогодні проєктування на стандартних комірках — найпоширеніша методологія створення ASIC. Коли кажуть «ASIC» без додаткових уточнень, зазвичай мають на увазі саме її.Вентильна матриця (Gate Array)Вентильна матриця посуває попередню фабрикацію ще далі. Кремнієва пластина надходить із уже виготовленими транзисторами та нижніми шарами металу, розташованими в регулярному повторюваному шаблоні. Кастомізуються лише верхні шари з'єднань — металеві провідники, що об'єднують транзистори у функціональні схеми. Оскільки базова пластина виготовляється масово й зберігається на складі, час виготовлення різко скорочується: від кількох днів до двох тижнів — порівняно з місяцями для проєктування на стандартних комірках.Вентильні матриці мають кілька підтипів:Наступна таблиця підсумовує три підходи:Переваги
Продуктивність. ASIC, оптимізована під конкретний алгоритм, буває в 100–10 000 разів швидшою за CPU, що виконує той самий алгоритм програмно, і в 10–100 разів швидшою за GPU.
Енергоефективність. Позбавлені зайвої логіки, ASIC дають найкращу продуктивність на ват. Це критично важливо для пристроїв з акумуляторним живленням і для центрів обробки даних, де електроенергія — основна стаття операційних витрат.
Малий форм-фактор. Функцію, яка потребує цілої плати GPU, часто вдається вмістити на одному кристалі ASIC, що дозволяє інтегрувати її у продукти з жорсткими обмеженнями за розміром — навушники, медичні імпланти, автомобільні датчики.
Захист інтелектуальної власності. Замовну ASIC значно складніше відтворити зворотним проєктуванням, ніж програмне забезпечення чи бітові потоки FPGA, що дає певний захист пропрієтарних алгоритмів.
Вартість за великих обсягів. Вартість кремнію на одиницю продукції ASIC швидко зменшується з нарощуванням тиражу. За мільйонних тиражів амортизована вартість однієї мікросхеми падає до кількох доларів або навіть центів.
Недоліки
Високі витрати NRE (Non-Recurring Engineering). Проєктування та виготовлення масок для ASIC коштує мільйони або десятки мільйонів доларів на передових технологічних вузлах (7 нм, 5 нм). Ці інвестиції мають окупитися за рахунок масових продажів.
Тривалий цикл розробки. Від специфікації до робочого кремнію зазвичай минає 12–24 місяці. Для повністю замовних проєктів на передових вузлах три і більше років — не рідкість.
Негнучкість. Щойно ASIC виготовлено, її функція заморожена. Якщо зміниться алгоритм, якщо виявиться помилка або якщо зміниться сам ринок — мікросхему не можна перепрограмувати, її доведеться перепроєктувати та виготовити заново. Цей ризик застарівання особливо відчутний у швидкозмінних галузях.
Спеціалізована експертиза. Проєктування ASIC потребує апаратних інженерів, досвідчених у RTL-проєктуванні, аналізі затримок, фізичному проєктуванні та верифікації — набору навичок, що суттєво відрізняється від розробки програмного забезпечення.
ASIC трапляються повсюди, хоча часто ховаються за брендами та маркетинговими назвами:
Майнінг криптовалют. Серія Antminer від Bitmain та подібні продукти — це ASIC-майнери SHA-256. Tensor Processing Unit (TPU) від Google з архітектурного погляду є ASIC, оптимізованою для матричного множення — основної операції навчання та висновування нейронних мереж.
Споживча електроніка. Чипи серій A та M від Apple містять ASIC-блоки для Neural Engine, ISP та захищеного анклаву поряд із ядрами CPU і GPU загального призначення. SoC серії Snapdragon від Qualcomm побудовані за тим самим принципом.
Телекомунікації та мережі. Маршрутизатори й комутатори Cisco, Juniper та Broadcom використовують ASIC для пересилання пакетів на лінійній швидкості. Базові станції 5G покладаються на ASIC для обробки сигналів у реальному часі.
Автомобільна галузь. Передові системи допомоги водієві (ADAS) використовують ASIC для злиття даних із датчиків, розпізнавання об'єктів та ухвалення рішень у реальному часі. Блоки керування двигуном (ECU) будують на ASIC уже десятиліттями.
Медичні пристрої. Слухові апарати, кардіостимулятори та портативні діагностичні прилади спираються на ASIC, щоб забезпечити необхідну обробку сигналів за жорстких обмежень на потужність і розмір.
Промислове та військове застосування. Заводські контролери, радарні системи й супутникові канали зв'язку — усі покладаються на ASIC для детермінованої обробки в реальному часі.
Розділ 4 обґрунтував потребу у спеціалізації: коли навантаження відоме наперед, спеціалізований кремній здобуває перевагу. Проте ASIC мають вагому слабкість — після виготовлення їхня функція вже незмінна. Якщо треба виправити помилку, пристосуватися до нової версії протоколу або поекспериментувати з проєктом ще до того, як витратити мільйони доларів на tape-out, ASIC у цьому не зарадить.На протилежному краї спектра CPU та GPU цілком перепрограмовні, але принципово обмежені: вони виконують команди по одній (або по кілька) за раз і стикаються з накладними витратами на вибірку, декодування та планування команд. Навіть масово паралельний GPU за кожну арифметичну операцію сплачує ціну універсального конвеєра команд.Чи існує щось проміжне — обладнання, яке можна налаштувати під конкретну задачу, а потім перепрограмувати, коли задача зміниться? Саме таку можливість дає FPGA (програмована вентильна матриця).<br><img alt="Плата розробки Xilinx Spartan-6 FPGA" src="assets/l10-fpga-xilinx-spartan6.jpeg" target="_self">Плата розробки Xilinx Spartan-6 FPGA — FPGA програмуються файлами бітових потоків, які завантажуються в їхню конфігураційну пам'ять.FPGA — це інтегральна схема з великим масивом конфігурованих логічних блоків, з'єднаних програмованою мережею маршрутизації. Після виготовлення функція мікросхеми не фіксована: її визначає конфігураційний бітовий потік, завантажений в оперативну пам'ять на кристалі. Змінюємо бітовий потік — і мікросхема реалізує іншу схему. Без нового кремнію, без нових масок, без тижнів виготовлення — достатньо завантажити нову конфігурацію.Ключова ідея: FPGA не виконує команди, а є схемою. Коли ми «програмуємо» FPGA для реалізації хешера SHA-256, мікросхема фізично стає хешером SHA-256 — з виділеними логічними вентилями, виділеними провідниками та виділеними регістрами, що працюють паралельно. Немає ні вибірки команд, ні етапу декодування, ні лічильника команд. Дані протікають через схему зі швидкістю самих логічних вентилів.Саме це принципово відрізняє FPGA від процесорів. Процесор — це часовий (темпоральний) обчислювальний пристрій: він раз за разом повторно використовує ту саму апаратуру (АЛП, регістри), виконуючи різні операції в різний час. FPGA натомість — просторовий обчислювальний пристрій: різні операції відбуваються в різних місцях на кристалі одночасно.FPGA складається з чотирьох основних компонентів:<br><img alt="Внутрішня структура FPGA: логічні блоки, контактні площадки вводу/виводу та програмовані з'єднання" src="assets/l10-fpga-structure.png" target="_self">Загальний вигляд архітектури FPGA: сітка конфігурованих логічних блоків (CLB), з'єднаних програмованою маршрутизацією, оточена контактними площадками вводу/виводу.1. Конфігуровані логічні блоки (CLB)CLB (конфігуровані логічні блоки) — базові обчислювальні одиниці FPGA. Кожен CLB містить невелику кількість таблиць підстановки (LUT), тригерів та мультиплексорів. Сучасна FPGA може налічувати десятки тисяч CLB.2. Таблиці підстановки (LUT)LUT (таблиця підстановки) — основний будівельний блок логіки FPGA. k-входова LUT — це, по суті, пам'ять (SRAM) на 2^k записів, з'єднана з k-входовим мультиплексором. SRAM зберігає повну таблицю істинності для довільної булевої функції k змінних. Для обчислення функції вхідні сигнали через мультиплексор вибирають один запис із таблиці істинності, і результат з'являється негайно (після затримки поширення сигналу через мультиплексор).<br>Наприклад, 4-входова LUT містить 2^4 = 16 бітів SRAM і може реалізувати будь-яку булеву функцію 4 входів — AND, OR, XOR, мажоритарну, функцію парності чи будь-яку з решти 65 536 можливих 4-входових функцій. Сучасні FPGA зазвичай використовують 6-входові LUT (по 64 біти SRAM кожна), здатні реалізувати довільну функцію до 6 змінних. FPGA середнього рівня на кшталт <a data-tooltip-position="top" aria-label="https://www.xilinx.com/products/silicon-devices/fpga/spartan-6.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.xilinx.com/products/silicon-devices/fpga/spartan-6.html" target="_self">Xilinx Spartan-6</a> містить понад 20 000 подвійних LUT.3. Програмована матриця маршрутизаціїМатриця маршрутизації — це мережа програмованих мультиплексорів і сегментів провідників, що з'єднує CLB між собою та з блоками вводу/виводу. Під час завантаження конфігураційного бітового потоку кожен мультиплексор у матриці маршрутизації налаштовується на з'єднання або роз'єднання конкретних сегментів провідників, і в результаті утворюється саме та топологія зв'язків, якої потребує наша схема.Маршрутизація зазвичай займає більшу площу кристала, ніж самі логічні блоки — часто 60–80% загальної площі. Це одна з причин, чому FPGA більші й повільніші за еквівалентні ASIC: програмована маршрутизація додає затримку та споживає потужність.4. Блоки вводу/виводуБлоки вводу/виводу розміщені по периметру FPGA і з'єднують внутрішню логіку із зовнішніми виводами. Кожен такий блок можна налаштувати на вхід, вихід або двонапрямлену роботу; він підтримує різні стандарти напруги та протоколи сигналізації.Сучасні FPGA також містять жорсткі IP-блоки — заздалегідь побудовані, неконфігуровні функціональні модулі, вбудовані поруч із програмованою тканиною. До поширених жорстких IP-блоків належать блочна оперативна пам'ять (BRAM), секції цифрової обробки сигналів (DSP) з апаратними помножувачами, модулі керування тактовою частотою (PLL/DLL), високошвидкісні послідовні трансивери й навіть процесорні ядра ARM. Ці жорсткі блоки забезпечують продуктивність рівня ASIC для поширених функцій, а програмована тканина тим часом реалізує специфічну для застосування логіку.<br>FPGA не програмують мовами на кшталт C чи Python. Натомість бажану схему описують за допомогою HDL (мови опису апаратури) — найчастіше <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Verilog" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Verilog" target="_self">Verilog</a> або <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/VHDL" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/VHDL" target="_self">VHDL</a>. HDL зовні нагадує мову програмування, але описує структуру та з'єднання, а не послідовність інструкцій. Кожен «оператор» у HDL задає апаратуру, що існує одночасно з усією іншою, — потоку керування в процесорному розумінні тут немає.Процес розробки має такий вигляд:
Проєктування — опис схеми мовою Verilog або VHDL.
Синтез — інструмент синтезу перетворює HDL на нетлист LUT, тригерів і з'єднань маршрутизації.
Розміщення та трасування — інструмент відображає нетлист на фізичні ресурси конкретної мікросхеми FPGA.
Генерація бітового потоку — інструмент створює двійковий конфігураційний файл.
Конфігурація — бітовий потік завантажується в конфігураційну SRAM FPGA (зазвичай через JTAG або SPI flash), і мікросхема одразу починає працювати як описана схема.
Увесь цикл — від зміни HDL до працюючого обладнання — зазвичай триває хвилини або години, тоді як для ASIC він вимірюється місяцями. Така швидка ітерація — одна з найбільших переваг FPGA.FPGA та ASIC — це взаємодоповнювальні технології, а не конкуренти. Вони відповідають різним точкам життєвого циклу продукту й різним вимогам до обсягів виробництва. Ключові відмінності підсумовано в таблиці нижче:На практиці FPGA часто слугують проміжною сходинкою до ASIC. Компанія прототипує та валідує проєкт на FPGA, швидко його ітерує і вдається до tape-out ASIC лише тоді, коли проєкт стабілізується, а прогнозований обсяг виробництва виправдовує інвестиції в NRE. Деякі продукти працюють на FPGA постійно: коли обсяги занадто малі, щоб виправдати ASIC, або коли застосування вимагає оновлення в полі (наприклад, програмно-визначене радіо чи адаптивне мережеве обладнання).Існує також точка перетину витрат за обсягом: нижче певного обсягу виробництва дешевшими є FPGA (бо немає NRE для амортизації); вище — вигіднішими стають ASIC (за рахунок нижчої вартості одиниці). Точне значення перетину залежить від складності проєкту та технологічного вузла ASIC, але зазвичай лежить у діапазоні 10 000–100 000 одиниць.Переваги
Реконфігурованість. Та сама мікросхема в різний час може реалізовувати зовсім різні схеми. Це уможливлює оновлення в полі, швидке прототипування та багатофункціональні пристрої.
Швидкий вихід на ринок. Не потрібно виготовляти маски й обробляти пластини. Шлях від зміни HDL до працюючого обладнання триває години.
Відсутність витрат на оснащення. На відміну від ASIC, FPGA не потребують замовних масок чи обробки пластин. Витрати на розробку — це здебільшого інженерний час та ліцензії на програмне забезпечення.
Справжній апаратний паралелізм. CPU розподіляє один АЛП між багатьма операціями в часі, а FPGA може створити тисячі незалежних обробних елементів, які працюють одночасно.
Недоліки
Нижча продуктивність, ніж у ASIC. Програмована маршрутизація додає затримку поширення, а логіка на основі LUT за своєю природою менш ефективна за замовні вентилі. FPGA зазвичай у 2–10 разів повільніші та в 5–20 разів менш енергоефективні за еквівалентну ASIC.
Вище енергоспоживання. Навіть у стані спокою FPGA споживає помітний струм на підтримку своєї SRAM-конфігурації. Активне споживання за повного навантаження може перевищувати 1 А — серйозна проблема для пристроїв з акумуляторним живленням.
Вища вартість одиниці. Велика FPGA може коштувати від $50 до понад $500 за мікросхему, тоді як ASIC за великих обсягів обходиться в $1–$10.
Складність розробки. Проєктування на HDL потребує навичок апаратного інженера, а вони менш поширені, ніж навички програмування. Налагодження схеми у FPGA принципово відрізняється від налагодження програмного забезпечення й нерідко виявляється складнішим.
У розділах 3–5 цієї лекції ми познайомилися з трьома дуже різними підходами до обчислень: процесорами загального призначення (CPU/GPU), реконфігуровним обладнанням (FPGA) та обладнанням з фіксованою функцією (ASIC). Ці три підходи розташовані на одному спектрі, і розуміння місця кожного з них — один із найважливіших висновків сучасної комп'ютерної архітектури.Спектр виявляє фундаментальний інженерний компроміс: гнучкість і продуктивність обернено корельовані. Що гнучкіший пристрій (що ширший діапазон обчислень, які він здатен виконувати), то менш ефективний він у кожному окремому обчисленні. CPU може запустити будь-яку програму, але витрачає транзистори на універсальність. ASIC виконує лише одну програму, проте не марнує нічого.Ідея не нова — це відлуння принципу спеціалізації, який зустрічається скрізь в інженерії та біології. Швейцарський армійський ніж виконує багато задач задовільно; кухарський ніж виконує одну задачу чудово. Правильний вибір залежить від того, що важливіше — універсальність чи пікова продуктивність.У сучасних системах відповідь дедалі частіше — «і те, й інше». SoC смартфона поєднує ядра CPU загального призначення, GPU, ASIC для нейронних мереж, ASIC для камерного ISP, а часом навіть невелику реконфігуровну тканину, подібну до FPGA, — усе на одному кристалі. Кожен компонент обслуговує те навантаження, для якого він підходить найкраще. Такий підхід — гетерогенні обчислення — сьогодні є домінантною тенденцією в архітектурному проєктуванні й логічним завершенням спектра, який ми щойно розглянули.У лекції 9 ми розглядали, що архітектури VLIW (дуже довге командне слово) застосовують принципово інший підхід до паралелізму на рівні команд. Замість складного обладнання, яке виявляє операції, що їх можна виконати одночасно (як у суперскалярних процесорах), VLIW повністю перекладає цю відповідальність на компілятор. Компілятор аналізує програму, визначає незалежні операції та пакує їх в одне широке командне слово — зазвичай від 64 до 1024 бітів. Обладнання потім виконує кожен слот цього слова паралельно, без жодної логіки динамічного планування.Тоді ж ми обговорювали й основні компроміси: VLIW спрощує обладнання (немає буфера переупорядкування, немає перейменування регістрів, немає схем перевірки залежностей), проте висуває величезні вимоги до компілятора. Якщо компілятор не може знайти достатньо незалежних операцій, щоб заповнити кожен слот, він вставляє NOP і так марнує пропускну здатність команд. А оскільки формат команд прив'язаний до кількості та типу функціональних блоків, бінарні файли, скомпільовані для одного покоління VLIW, можуть не працювати на наступному без перекомпіляції.Цей розділ продовжує те, на чому зупинилася лекція 9. Замість повторювати теорію, зосередимося на тому, як саме виконуються команди VLIW, а потім розглянемо дві реальні реалізації — одну гучну комерційну невдачу та одну тиху історію успіху, — щоб зрозуміти, де ця архітектурна філософія процвітає, а де зазнає поразки.Щоб зробити концепцію VLIW наочнішою, розгляньмо процесор із трьома функціональними блоками: помножувачем з рухомою комою (FU1), суматором з рухомою комою (FU2) та блоком завантаження/збереження (FU3). Одне командне слово VLIW для цього процесора містить три поля операцій — по одному для кожного блоку.<br><img alt="Виконання VLIW: одне командне слово передає дані трьом функціональним блокам через спільний регістровий файл" src="assets/l10-vliw-execution-diagram.png" target="_self">Модель виконання VLIW: одне широке командне слово містить три незалежні операції, кожна з яких спрямовується до свого функціонального блоку (FU). Усі блоки використовують спільний регістровий файл.Коли процесор вибирає одну команду VLIW, він одночасно надсилає всі три операції:
FU1 починає множення з рухомою комою
FU2 починає додавання з рухомою комою
FU3 починає завантаження з пам'яті (або два завантаження, якщо блок підтримує двопортовий доступ)
Усі три функціональні блоки зчитують свої операнди зі спільного регістрового файлу та записують результати назад до нього. Жодна арбітражна логіка не вирішує, яким операціям можна виконуватися: компілятор уже гарантував, що ці три операції не мають залежностей за даними одна від одної.Конвеєр процесора VLIW подібний до класичного конвеєра RISC — IF (вибірка команди), ID (декодування команди), EX (виконання), MEM (доступ до пам'яті), WB (зворотний запис), — але з однією принциповою відмінністю: стадія EX розгалужується на кілька паралельних шляхів виконання, по одному на функціональний блок.<br><img alt="Діаграма конвеєра VLIW, що показує IF, ID, кілька етапів EX, MEM та WB" src="assets/l10-vliw-pipeline.png" target="_self">Конвеєр VLIW: кожна команда проходить через вибірку (IF), декодування (ID), кілька паралельних стадій виконання (EX), доступ до пам'яті (MEM) та зворотний запис (WB).Оскільки командне слово значно ширше за стандартну 32-бітну команду RISC, стадії вибірки та декодування команд мають обробляти більше даних за цикл. Проте саме декодування простіше: немає потреби перевіряти конфлікти чи переупорядковувати операції. Кожне поле безпосередньо відповідає своєму функціональному блоку. Саме тому прихильники VLIW стверджують, що цей підхід добре масштабується: додати нові функціональні блоки можна, просто розширивши командне слово, а не нарощуючи експоненційно обсяг апаратури планування.Недолік не менш конкретний. Якщо компілятор знаходить лише дві незалежні операції для певного циклу, третій слот має містити NOP. У найгіршому випадку — за послідовності тісно залежних операцій — більшість слотів залишається невикористаною, і ефективна пропускна здатність процесора падає до рівня простої скалярної машини, попри те що площа кремнію відповідає значно складнішій конструкції.<br>У 2000 році невеликий стартап на ім'я <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Transmeta" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Transmeta" target="_self">Transmeta</a> анонсував процесор, який, здавалося, взагалі не піддавався класифікації. Мікросхема Crusoe усередині була процесором VLIW: вона виконувала 128-бітні командні слова, що містили по чотири операції кожне. Зовні ж вона запускала стандартне програмне забезпечення x86 — Windows, Linux та всі наявні застосунки без перекомпіляції.<br><img alt="Процесор Transmeta Crusoe" src="assets/l10-vliw-transmeta-crusoe.jpeg" target="_self">Transmeta Crusoe — процесор VLIW, що запускав програмне забезпечення x86 за допомогою динамічної бінарної трансляції («Code Morphing»).Секрет полягав у програмному шарі під назвою Code Morphing Software (CMS). CMS був динамічним бінарним транслятором — програмою, збереженою в ROM, яка запускалася перед завантаженням операційної системи. Коли процесор натрапляв на код x86, CMS транслював його на льоту у власні команди VLIW Crusoe. Трансляція відбувалася поетапно:
Інтерпретація — під час першого виконання блоку коду x86 CMS інтерпретує його команда за командою (повільно, зате коректно)
Трансляція — якщо блок коду виконується повторно (гарячий шлях), CMS компілює його в оптимізовані послідовності VLIW та кешує результат
Оптимізація — для найгарячіших шляхів коду CMS застосовує агресивні оптимізації: планування команд, розподіл регістрів і навіть спекулятивне переупорядкування з підтримкою відкату
Цей підхід мав глибоку перевагу: процесор міг розвивати свою внутрішню архітектуру VLIW, не порушуючи сумісності з x86. Оскільки шар трансляції був програмним, Transmeta мала змогу змінювати кількість функціональних блоків, формат командного слова або глибину конвеєра між поколіннями мікросхем, а наявні програми x86 однаково працювали, щоразу транслюючись заново оновленим CMS.Crusoe орієнтувався на конкретний ринок — ноутбуки з наднизьким енергоспоживанням. Відмовившись від складного обладнання позачергового виконання, на яке спирався Intel Pentium III, Transmeta створила мікросхему, що споживала значно менше потужності. Її прийняли кілька виробників ноутбуків, зокрема в деяких ранніх тонких і легких моделях.Однак Crusoe зіткнувся з фундаментальною проблемою продуктивності. Накладні витрати на трансляцію — навіть із кешуванням та оптимізацією — означали, що Crusoe стабільно поступався в швидкості процесорам x86 від Intel на тій самій тактовій частоті. Коли Intel підвищила енергоефективність власних мікросхем (особливо з Pentium M та пізнішою серією Core), перевага Transmeta в енергоспоживанні зникла. Компанія припинила виробництво процесорів у 2005 році й зрештою перетворилася на фірму з ліцензування патентів.Проте спадщина Transmeta виходить далеко за межі її комерційного існування. Ідея програмного шару сумісності, що відокремлює зовнішню ISA (архітектуру системи команд) від внутрішньої мікроархітектури, — це саме те, що роблять сучасні процесори x86. Мікросхеми Intel та AMD транслюють команди x86 у внутрішні мікрооперації (схожі на RISC-операції) за допомогою апаратних декодерів. Transmeta робила те саме, лише програмно, а не в кремнії.Якщо Transmeta була невеликим стартапом, то ставка Intel на VLIW стала однією з найбільших програм розробки процесорів в історії. У партнерстві з Hewlett-Packard компанія Intel розробила Itanium — процесор на базі варіанту VLIW, який сама Intel назвала EPIC (Explicitly Parallel Instruction Computing — явно паралельні обчислення за командами).Формат команд Itanium групував три операції в 128-бітний бандл (пакет). Кожен бандл містив поле шаблону — набір бітів, який повідомляв обладнанню, які операції можна виконувати паралельно та де пролягають межі між незалежними групами. Це було вдосконаленням порівняно з чистим VLIW: замість вимагати незалежності кожного слота (і заповнювати невикористані слоти NOP), біти шаблону дозволяли компілятору виражати часткову паралельність, а обладнання могло видавати кілька бандлів за цикл, коли залежності це дозволяли.Itanium також запровадив кілька можливостей, покликаних допомогти компілятору:
Предикатне виконання — майже кожну команду можна було виконувати умовно за значенням предикатного регістра, що усувало багато переходів (і пов'язані з ними втрати на хибне передбачення переходів)
Спекулятивні завантаження — компілятор міг видавати завантаження з пам'яті заздалегідь, ще до того як ставало відомо, чи потрібні ці дані, з апаратною підтримкою виявлення та відновлення після помилок при спекулятивних зверненнях
Ротація регістрів — апаратний механізм, що спрощував конвеєризацію циклів, автоматично перейменовуючи регістри між ітераціями
На папері Itanium виглядав елегантно. На практиці він став комерційною катастрофою. Причини були і технічними, і економічними:Складність для компілятора. Уся передумова EPIC полягала в тому, що компілятори зможуть видобути достатній паралелізм на рівні команд із коду загального призначення. Для наукових обчислень із регулярними циклами над великими масивами це було здійсненно. А от для типових серверних навантажень — баз даних, вебсерверів, корпоративних застосунків зі складним потоком керування, структурами даних із вказівниками та непередбачуваними переходами — компілятори не могли ефективно заповнити слоти команд. Теоретична продуктивність була, практичної — ні.Роздутий код. Бінарні файли IA-64 були у 2–4 рази більші за еквівалентні бінарні файли x86. Ширший формат команд у поєднанні із заповненням слотів NOP там, де компілятор не міг підібрати корисних операцій, поглинав значно більше кешу команд та пропускної здатності пам'яті. Це частково нівелювало теоретичну перевагу простішої логіки декодування.x86 тим часом розвивався. Власні процесори x86 від Intel — а особливо Opteron від AMD, який додав 64-бітні розширення (AMD64/x86-64) до наявної архітектури x86 — продовжували нарощувати продуктивність завдяки кращому позачерговому виконанню, глибшим конвеєрам і більшим кешам. Індустрія обрала розширення x86, а не його заміну. Щойно Intel прийняла 64-бітні розширення AMD для своїх власних мікросхем x86 (як Intel 64), Itanium втратив головну перевагу — роль шляху до 64-бітних обчислень.<br>Itanium існував у нішевих ринках корпоративних серверів та високопродуктивних обчислень (переважно сервери HP-UX), доки Intel офіційно <a data-tooltip-position="top" aria-label="https://www.anandtech.com/show/16276/intel-to-discontinue-itanium-9700-last-of-itanium-line" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.anandtech.com/show/16276/intel-to-discontinue-itanium-9700-last-of-itanium-line" target="_self">не припинила випуск лінійки у 2021 році</a>. Він залишається однією з найдорожчих невдалих ставок в історії напівпровідникової індустрії: за оцінками, сумарні витрати Intel та HP на розробку перевищили 10 мільярдів доларів.<br>Якщо Itanium — це повчальна історія поразки, то цифрові сигнальні процесори — історія успіху. Родина Texas Instruments <a data-tooltip-position="top" aria-label="https://www.ti.com/processors/c2000-microcontrollers/overview.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.ti.com/processors/c2000-microcontrollers/overview.html" target="_self">TMS320C6000</a> (яку зазвичай називають C6x) є однією з найуспішніших комерційних архітектур VLIW. Серія C6x використовує командні слова, що пакують до восьми операцій за цикл на вісім функціональних блоків.Чому VLIW працює для DSP (цифрових сигнальних процесорів), але зазнав невдачі в обчисленнях загального призначення? Відповідь криється в самій природі навантажень:У застосунках DSP — аудіокодеках, телекомунікаційній обробці основної смуги, радарній обробці сигналів, фільтрації зображень — алгоритми математично регулярні. Фільтр зі скінченною імпульсною характеристикою (FIR), наприклад, по суті є скалярним добутком, обчисленим над ковзним вікном: помножити кожен вхідний відлік на коефіцієнт та накопичити результати. Компілятор може тривіально розподілити ці множення й додавання між кількома функціональними блоками, продуктивно заповнюючи кожен слот VLIW.Окрім родини DSP від TI, принципи VLIW застосовують і в кількох інших сферах:
Qualcomm Hexagon DSP — наявний у мобільних процесорах Snapdragon, обробляє аудіо, дані з датчиків та певний висновування нейронних мереж за допомогою ядра VLIW
Компілятори шейдерів GPU — деякі архітектури GPU використовують VLIW-подібне пакування команд у своїх шейдерних процесорах (старіші GPU-архітектури AMD VLIW4/VLIW5 застосовували цей підхід явно)
Мережеві процесори — високопродуктивна обробка пакетів виграє від того ж регулярного, передбачуваного паралелізму, який робить VLIW ефективним для DSP
Урок VLIW полягає не в тому, що ідея була хибною, а в тому, що вона спрацьовує лише тоді, коли навантаження відповідає її припущенням. Якщо компілятор може бачити паралелізм на етапі компіляції, VLIW забезпечує відмінну продуктивність на ват із мінімальною апаратною складністю. Якщо ж паралелізм динамічний, залежить від даних і непередбачуваний, компілятор тут безсилий — і перемагає апаратне планування.У 2013 році інженери Google зробили приголомшливий розрахунок. Якби кожен користувач Android використовував голосовий пошук лише три хвилини на день, обчислювальне навантаження нейронної мережі для розпізнавання мовлення потребувало б подвоєння всієї потужності дата-центрів Google. Не додавання кількох стійок — подвоєння всього: серверів, мережевого обладнання, охолодження, електричної інфраструктури. Наявне обладнання — CPU (центральні процесори) і навіть GPU (графічні процесори) — було просто занадто неефективним для специфічних обчислень, яких потребують нейронні мережі.Ключова проблема — це вузьке місце фон Неймана у контексті машинного навчання. Прохід висновування нейронної мережі складається майже повністю з множення матриць: помножити матрицю вхідних активацій на матрицю вивчених вагових коефіцієнтів, додати зміщення, застосувати функцію активації та повторити — шар за шаром. Сама арифметика проста. Проблема в тому, що операнди величезні: один шар сучасної мовної моделі може множити матрицю 4096 на 4096 на вектор тисячі разів за секунду.<br>На CPU (центральному процесорі) загального призначення кожна з цих операцій множення-накопичення потребує зчитування операндів з пам'яті (або кешу), виконання арифметики в АЛП (арифметико-логічному пристрої), спроєктованому для обчислень загального призначення, та запису результату назад. Енергетична вартість переміщення даних до пам'яті та з неї значно перевищує енергетичну вартість самого множення — приблизно у <a data-tooltip-position="top" aria-label="https://blog.bytebytego.com/p/how-googles-tensor-processing-unit" rel="noopener nofollow" class="external-link is-unresolved" href="https://blog.bytebytego.com/p/how-googles-tensor-processing-unit" target="_self">200 разів</a> для доступу до DRAM (динамічної пам'яті) порівняно з множенням 8-бітних цілих чисел. CPU витрачає більше енергії на переміщення чисел, ніж на власне обчислення з ними.GPU (графічні процесори) покращили ситуацію, надаючи тисячі менших ядер, що працюють паралельно (як ми бачили в розділі про GPU цієї лекції). Але навіть GPU є більш універсальними, ніж потрібно для матричної математики. Ядро GPU може виконувати довільні шейдерні програми, обробляти розгалуження, керувати розходженням потоків і підтримувати широкий спектр типів даних. Для вузького завдання множення матриць чисел з низькою точністю значна частина цієї гнучкості — це марно витрачений кремній і марно витрачена енергія.Відповіддю Google стало створення чіпа, який не робить майже нічого, крім множення матриць — але робить це з надзвичайною ефективністю. Цей чіп отримав назву TPU (тензорний процесорний блок).Перший TPU (v1), розгорнутий внутрішньо в Google у 2015 році, був спроєктований як прискорювач висновування — він виконував уже навчені нейронні мережі для формування передбачень, а не навчав нові моделі. Його архітектура кардинально відрізнялася від архітектури як CPU, так і GPU: вона була побудована навколо єдиної ключової інновації — систолічної матриці.Термін «систолічний» походить від грецького слова, що означає серцебиття. У систолічній матриці дані ритмічно протікають крізь сітку обчислювальних елементів, подібно до того, як кров пульсує кровоносною системою. Кожен обчислювальний елемент виконує одну операцію множення-накопичення (MAC) і передає дані сусідньому елементу — без потреби у глобальному доступі до пам'яті.<br><img alt="Плата Google TPU (тензорного процесорного блока)" src="assets/l10-tpu-google.jpeg" target="_self">TPU першого покоління від Google — плата PCIe, побудована навколо систолічної матриці 256x256, спроєктована спеціально для висновування нейронних мереж.У TPU v1 блок матричного множення (MXU) — це систолічна матриця розміром 256 на 256, тобто 65 536 блоків множення-накопичення, що працюють одночасно. Ось як це працює: Вагові коефіцієнти попередньо завантажуються в матрицю. Кожен обчислювальний елемент зберігає одне значення ваги. Для множення матриці 256 на 256 усі 65 536 вагових коефіцієнтів завантажуються один раз і залишаються нерухомими. Вхідні активації надходять зліва, по одному рядку за раз. Кожне значення активації входить у крайній лівий стовпець і поширюється вправо через матрицю, по одному стовпцю за такт. Часткові суми накопичуються вниз. Кожен обчислювальний елемент множить вхідну активацію на збережену вагу та додає результат до часткової суми, що надходить зверху. Результати з'являються внизу матриці після 256 тактів — повний матрично-векторний добуток, обчислений з 65 536 паралельними операціями MAC за такт. Краса цього проєктування — у повторному використанні даних. Кожна вага завантажується один раз і використовується 256 разів (один раз для кожного елемента вхідного вектора, що проходить повз неї). Кожна активація завантажується один раз і використовується 256 разів (один раз у кожному стовпці, через який вона проходить). Таке 256-кратне повторне використання різко зменшує вимоги до пропускної здатності пам'яті порівняно з CPU чи GPU, де кожен операнд потрібно зчитувати з регістрового файлу або кешу для кожної операції.Окрім MXU, архітектура TPU v1 включає: Уніфікований буфер — 24 МБ SRAM (статичної пам'яті), що зберігає вхідні активації. Це робоча пам'ять для вхідних даних поточного шару, розрахована на зберігання активацій типових шарів нейронної мережі без звернення до зовнішньої DRAM (динамічної пам'яті). Акумулятори — 4 096 акумуляторів по 256 елементів, 32-бітних, що зберігають вихідні часткові суми з MXU. Точність акумуляторів вища за точність вхідних даних (32-бітна проти 8-бітної), щоб запобігти переповненню під час підсумовування 256 добутків. Конвеєр активації — апаратний блок, що застосовує нелінійні функції активації (ReLU, sigmoid, tanh) до виходів акумуляторів і перетворює результати назад у відповідну точність для наступного шару. HBM (пам'ять високої пропускної здатності) — починаючи з TPU v2, зовнішня HBM забезпечує великий обсяг пам'яті, потрібний для вагових коефіцієнтів моделей, що перевищують ємність SRAM на кристалі. Одне з найважливіших проєктних рішень у TPU — використання зниженої числової точності. TPU v1 працює переважно зі значеннями INT8 (8-бітні цілі числа). Пізніші покоління додали підтримку BFloat16 (Brain Floating Point 16) — 16-бітного формату з 8-бітною експонентою та 7-бітною мантисою.Чому знижена точність працює для нейронних мереж? Дві причини: Нейронні мережі за своєю природою стійкі до шуму. Під час навчання ваги коригуються стохастичним градієнтним спуском — процесом, який сам по собі є шумним. Модель навчається бути стійкою до малих збурень. Зниження точності з FP32 до INT8 або BFloat16 вносить невеликі помилки округлення, але ці помилки перебувають у межах шумового запасу, який модель уже толерує. Менші числа означають менше обладнання. 8-бітний помножувач використовує приблизно 1/16 площі кремнію та 1/30 енергії порівняно з 32-бітним помножувачем з рухомою комою. Завдяки зниженій точності TPU вміщує значно більше блоків MAC на тій самій площі кристала, ніж GPU зі своїми ядрами, що підтримують FP32. Результат вражає: TPU v1 відводив понад 90% площі кристала на власне обчислення (MXU та акумулятори). Порівняймо це з тогочасним GPU, де приблизно 30% кристала — обчислювальна логіка, а решту займають кеші, логіка управління, планувальники потоків та інша інфраструктура загального призначення.<br>Google повідомив, що TPU v1 був у <a data-tooltip-position="top" aria-label="https://blog.bytebytego.com/p/how-googles-tensor-processing-unit" rel="noopener nofollow" class="external-link is-unresolved" href="https://blog.bytebytego.com/p/how-googles-tensor-processing-unit" target="_self">15–30 разів швидшим</a> за тогочасні GPU на задачах висновування та в 30–80 разів енергоефективнішим. Ці числа відображали перевагу чіпа, спеціально створеного для матричної математики, над чіпом, спроєктованим для паралельних обчислень загального призначення.Від TPU v1 Google випустив декілька поколінь, кожне з яких розширює можливості та продуктивність:Через ці покоління простежуються кілька архітектурних тенденцій:Від висновування до навчання. TPU v1 міг лише виконувати попередньо навчені моделі. Починаючи з v2, TPU отримали можливість навчати моделі — а це потребує зворотного поширення, обчислення градієнтів та оновлення вагових коефіцієнтів на додачу до матричних множень прямого проходу. Усе це потребувало вищої точності (BFloat16 замість INT8) та значно більшої пропускної здатності пам'яті (звідси перехід на HBM).<br>Інновації в міжз'єднаннях. TPU v4 запровадив <a data-tooltip-position="top" aria-label="https://cloud.google.com/blog/topics/systems/tpu-v4-enables-performance-energy-and-co2e-efficiency-gains" rel="noopener nofollow" class="external-link is-unresolved" href="https://cloud.google.com/blog/topics/systems/tpu-v4-enables-performance-energy-and-co2e-efficiency-gains" target="_self">оптичні комутатори каналів</a> для з'єднання чіпів TPU всередині пода — це уможливлює реконфігуровні топології: те саме фізичне обладнання може бути з'єднане як 3D-тор для одних навантажень або іншою топологією для інших. Це усунуло ключове обмеження попередніх поколінь, де фіксована топологія з'єднань була вузьким місцем для деяких патернів комунікації.SparseCores. TPU v4 додав новий компонент під назвою SparseCores — спеціалізовані обчислювальні блоки, оптимізовані для операцій з розрідженими даними, таких як пошук у таблицях ембедингів. У рекомендаційних моделях (що використовуються для Google Search, YouTube та реклами) пошук ембедингів становить значну частку обчислень. SparseCores забезпечили прискорення у 5–7 разів для цих операцій порівняно з MXU загального призначення.Масштабування до FP8. Найновіше покоління, Ironwood (v7), підтримує нативні обчислення FP8 (8-бітна рухома кома), що відображає загальногалузеву тенденцію до ще нижчої точності як для навчання, так і для висновування великих мовних моделей. З показником 4 614 TFLOPS на чіп Ironwood демонструє понад 50-кратне зростання сирої пропускної здатності порівняно з TPU v1.TPU стояли за деякими з найвидатніших досягнень ШІ останнього десятиліття: AlphaGo (2016) — система для гри в го від DeepMind, яка перемогла чемпіона світу Лі Седоля, працювала на TPU як для навчання, так і для висновування BERT і T5 (2018–2019) — фундаментальні мовні моделі, що сформували сучасний ландшафт обробки природної мови, навчені на подах TPU <br>
AlphaFold (2020–2024) — система передбачення структури білків від DeepMind, яка розв'язала 50-річну велику проблему біології та принесла її творцям <a data-tooltip-position="top" aria-label="https://www.nobelprize.org/prizes/chemistry/2024/summary/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.nobelprize.org/prizes/chemistry/2024/summary/" target="_self">Нобелівську премію з хімії 2024 року</a>, була навчена на TPU Google Search, Translate і Photos — виробничі сервіси, що обробляють мільярди запитів щодня, використовуючи висновування на TPU Тоді як Google спроєктував TPU навколо систолічних матриць, оптимізованих для множення матриць, Tesla обрала інший шлях із DOJO — спеціалізованим суперкомп'ютером, спроєктованим саме для навчання нейронних мереж, що забезпечують роботу систем Autopilot та Full Self-Driving. Архітектура DOJO відображає інший набір проєктних пріоритетів: максимізувати пропускну здатність навчання для моделей комп'ютерного зору, одночасно усуваючи традиційні вузькі місця у переміщенні даних.Серцем DOJO є D1 — спеціально спроєктований процесор, виготовлений за технологічним процесом TSMC 7 нм з приблизно 50 мільярдами транзисторів.<br><img alt="Специфікації кристала Tesla DOJO D1: TSMC 7 нм, 354 ядра, 362 TFlops" src="assets/l10-tesla-dojo-d1-die.jpeg" target="_self">Кристал Tesla DOJO D1: 354 обчислювальних ядра на кристалі площею 645 мм² (TSMC 7 нм), продуктивність 362 TFlops при BF16.<br>На відміну від Google TPU (який є спеціалізованим прискорювачем без виконання інструкцій загального призначення), D1 побудований на основі набору інструкцій <a data-tooltip-position="top" aria-label="https://www.nextplatform.com/2022/08/23/inside-teslas-innovative-and-homegrown-dojo-ai-supercomputer/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.nextplatform.com/2022/08/23/inside-teslas-innovative-and-homegrown-dojo-ai-supercomputer/" target="_self">RISC-V</a>, розширеного власними векторними та матричними інструкціями Tesla. Кожен чіп D1 містить 354 обчислювальних ядра, розташованих у сітці 18 на 20 і тактованих на частоті 2 ГГц.Кожне ядро містить: Скалярний блок RISC-V з 8-розрядним декодером інструкцій та 4-потоковою SMT (одночасною багатопотоковістю) Власний блок векторної/матричної обробки, оптимізований для BFloat16 та інших форматів, релевантних для ШІ 1,25 МБ локальної SRAM — незвично великий обсяг пам'яті на кристалі для одного ядра, який забезпечує пропускну здатність завантаження 400 ГБ/с без звернення до зовнішніх мікросхем Сумарні характеристики одного чіпа вражають: 440 МБ SRAM на кристалі (354 ядра x 1,25 МБ) 362 TFLOPS при точності BFloat16 Тактова частота 2 ГГц Найхарактерніша особливість архітектури DOJO — те, що Tesla прибрала. Традиційні багатоядерні процесори містять апаратне забезпечення для когерентності кешу (щоб усі ядра бачили узгоджений стан пам'яті), віртуальної пам'яті (перетворення між віртуальними та фізичними адресами) та глобальних каталогів (відстеження того, який кеш зберігає які дані). Ці механізми необхідні для обчислень загального призначення, де будь-яке ядро може потребувати доступу до будь-якої адреси у будь-який момент.Tesla усунула їх усі.Замість протоколів когерентності та віртуальної пам'яті DOJO використовує модель розподіленої SRAM. Кожне ядро монопольно володіє своєю локальною SRAM. Переміщення даних між ядрами відбувається через явну, програмно-керовану mesh-мережу — а не через апаратно-кероване відстеження кешу. Компілятор та система виконання відповідають за розміщення даних там, де вони потрібні, та за оркестрацію передач між ядрами.Це радикальне спрощення. Апаратне забезпечення когерентності кешу в чіпі з 354 ядрами споживало б величезну площу кремнію та енергію. Прибравши його, Tesla звільнила цей кремній для додаткових обчислювальних блоків і більшого обсягу SRAM. Компроміс полягає в тому, що програмування DOJO потребує явного управління даними — але для навчання нейронних мереж патерни потоку даних відомі на етапі компіляції (як і в VLIW, припущення полягає в тому, що статичний аналіз може замінити динамічне апаратне забезпечення).Tesla спроєктувала DOJO для масштабування від чіпа до системи унікально інтегрованим способом.<br><img alt="Навчальний модуль Tesla DOJO: масив 5x5 з чіпів D1" src="assets/l10-tesla-dojo-training-tile.jpeg" target="_self">Навчальний модуль DOJO: 25 кристалів D1 у масиві 5x5, продуктивність 9 PFlops при потужності 15 кВт з водяним охолодженням.Навчальний модуль об'єднує 25 кристалів D1 (розташованих у сітці 5 на 5) в одному модулі: 11 ГБ SRAM загалом (25 x 440 МБ) 9 PFLOPS обчислень BFloat16 10 ТБ/с двосекційна пропускна здатність між кристалами всередині модуля Споживана потужність 15 кВт Кристали всередині модуля з'єднані високошвидкісною mesh-мережею, що надає модулю характеристики одного дуже великого процесора, а не кластера окремих чіпів. Величезний сукупний обсяг SRAM (11 ГБ) є суттєвим — для багатьох навчальних навантажень параметри моделі та проміжні активації можуть повністю розміщуватися в SRAM, уникаючи затримки та енергетичних витрат на доступ до зовнішньої DRAM або HBM.<br><img alt="Покомпонентний вигляд системи Tesla DOJO, що показує модульну конструкцію" src="assets/l10-tesla-dojo-stack.jpeg" target="_self">Система Tesla DOJO — модульна конструкція, що складає навчальні модулі в системні лотки та ExaPods, досягаючи 20 ExaFlops.На найбільшому масштабі ExaPod об'єднує 120 навчальних модулів: Понад 1 мільйон обчислювальних ядер 20 EFLOPS (екзаFLOPS) обчислень BFloat16 Спроєктований для навчання масивних відео-нейронних мереж, яких потребує система автономного водіння Tesla Попри своє екзотичне апаратне забезпечення, DOJO спроєктований для програмування стандартними фреймворками машинного навчання. Програмний стек Tesla забезпечує сумісність із PyTorch, тобто дослідники та інженери можуть писати навчальний код, використовуючи звичні інструменти на основі Python. Компілятор DOJO відповідає за відображення високорівневих тензорних операцій на ядра RISC-V та власні векторні блоки D1. Важливо, що DOJO не потребує CUDA — він повністю незалежний від програмної екосистеми NVIDIA.TPU та DOJO представляють дві точки у зростаючому просторі проєктування предметно-орієнтованих архітектур (DSA) для ШІ. Вони поділяють спільну тезу: для навантажень, які є достатньо великими та достатньо чітко визначеними, спеціалізований чіп перевершує процесор загального призначення на один-два порядки як за продуктивністю, так і за енергоефективністю.Ключові архітектурні стратегії, спільні для цих прискорювачів:Вибухове зростання навантажень ШІ — від навчання мовних моделей у масштабі дата-центрів до висновування на пристроях у смартфонах — зробило цю сферу однією з найактивніших в архітектурі комп'ютерів. Окрім Google та Tesla, практично кожна велика технологічна компанія розробляє або впроваджує власний кремній для ШІ: Neural Engine від Apple, Trainium та Inferentia від Amazon, Maia від Microsoft, MTIA від Meta, а також десятки стартапів. Спільний мотив — той самий висновок, що спонукав Google у 2013 році: коли один тип обчислень домінує у навантаженні, обладнання загального призначення витрачає занадто багато кремнію на гнучкість, яка не потрібна.<br>Усі архітектури, які ми вивчали досі — від CPU (центрального процесора) до TPU (тензорного процесорного блоку) — мають спільного предка: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Von_Neumann_architecture" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Von_Neumann_architecture" target="_self">модель фон Неймана</a>. Інструкції рухаються з пам'яті до процесора, дані рухаються з пам'яті до процесора, результати повертаються назад. Процесор швидкий, а шина пам'яті — вузьке місце. Інженери витратили сім десятиліть на оптимізацію цього циклу — кеші, механізми попередньої вибірки, ширші шини, глибші конвеєри, — проте фундаментальне розділення обчислень і зберігання нікуди не зникло.Природа розв'язала ту саму задачу інакше. Людський мозок має близько 86 мільярдів нейронів, з'єднаних приблизно 100 трильйонами синапсів, і кожен синапс водночас є обчислювальним елементом і елементом пам'яті. Вузького місця шини немає — бо немає самої шини. Обчислення відбуваються там, де лежать дані. Уся ця структура споживає близько 20 ват — менше, ніж зарядний пристрій ноутбука, — і при цьому перевершує будь-який суперкомп'ютер на Землі в таких завданнях, як розпізнавання облич у натовпі, розуміння мовлення в шумному приміщенні чи ловіння м'яча, кинутого під неочікуваним кутом.<br><img alt="Концепція нейроморфних обчислень: половина біологічного мозку, половина цифрової схеми" src="assets/l10-neuromorphic-brain-circuit.jpeg" target="_self">Нейроморфні обчислення поєднують нейронауку та інженерію, проєктуючи кремній, який обробляє інформацію так само, як це роблять біологічні мозки.<br>Нейроморфні обчислення — це спроба побудувати кремнієві системи, які відтворюють таку архітектуру. Термін запропонував наприкінці 1980-х <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Carver_Mead" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Carver_Mead" target="_self">Карвер Мід</a>, професор Каліфорнійського технологічного інституту, який на той час уже започаткував революцію VLSI, разом зі своєю аспіранткою <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Misha_Mahowald" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Misha_Mahowald" target="_self">Мішею Маговальд</a>. Мід помітив, що аналогові CMOS-транзистори в підпороговому режимі поводяться напрочуд схоже на біологічні іонні канали, і запропонував будувати схеми, які спираються на цю схожість, а не борються з нею. Ідея була радикальною: замість того щоб змушувати кремній поводитися як машина булевої логіки, дозволити йому поводитися як нейрони.Матеріали, з яких будують нейроморфні схеми, також еволюціонують. Перші нейроморфні чипи використовували стандартну кремнієву CMOS-технологію — той самий виробничий процес, на якому тримається кожен CPU та GPU (графічний процесор). Проте дослідники дедалі активніше вивчають мемристори — двотермінальні пристрої, опір яких змінюється залежно від історії струму, що через них протікав. Мемристор природно реалізує синапс: «вага» з'єднання зберігається як опір пристрою і не потребує живлення. Кросбарні матриці на основі мемристорів з часом можуть замінити SRAM як засіб зберігання синаптичних ваг, значно збільшуючи щільність і ще більше знижуючи енергоспоживання.В основі нейроморфного обладнання лежить обчислювальна модель імпульсної нейронної мережі (SNN), яку іноді називають «третім поколінням» нейронних мереж (після перцептронів і глибоких нейронних мереж).У біологічному нейроні вхідні сигнали від інших нейронів надходять через дендрити й накопичуються як електричний заряд у тілі клітини. Щойно накопичений заряд перевищує поріг, нейрон генерує короткий електричний імпульс — спайк — який біжить аксоном до дендритів нейронів нижчого рівня. Після спрацювання нейрон скидається і переходить у короткий рефрактерний період, протягом якого не може спрацювати знову. Між спайками накопичений заряд повільно витікає — так само, як вода витікає з відра крізь маленький отвір.<br><img alt="Багаторівневе відображення від кори мозку до кремнієвого чипа" src="assets/l10-neuromorphic-brain-to-chip.png" target="_self">Від мозку до чипа: нейроморфна інженерія відображає кортикальні структури на дедалі дрібніші рівні — від ділянок мозку до масивів нейронів та реалізацій схем на кремнії.Така поведінка принципово відрізняється від штучних нейронів у звичайних нейронних мережах: ті обчислюють зважену суму входів, пропускають її крізь функцію активації (ReLU, сигмоїд тощо) і видають неперервне значення — все за один синхронний такт. Біологічні нейрони та їхні SNN-моделі натомість є статичними (запам'ятовують накопичений заряд), часовими (важить момент спрацювання спайків) і подієво-керованими (обчислення відбуваються лише тоді, коли спайк надходить або генерується).Стандартною математичною абстракцією цього процесу є модель нейрона «Інтегруй та спрацюй із витоком» (Leaky Integrate-and-Fire, LIF). Для нейрона j на дискретному кроці часу t:V_j[t] = V_j[t-1] + Σ(A_i · w_i,j · s_j) - λ_j
де V_j — мембранний потенціал, A_i — активація вхідного нейрона i, w_i,j — синаптична вага, що з'єднує нейрон i з нейроном j, s_j — керуюча змінна, а λ_j — член витоку. Щойно V_j перетинає поріг, нейрон генерує спайк і скидається.Чим SNN відрізняються від звичайних глибоких нейронних мереж (DNN):Ключовий принцип — подієво-керовані обчислення. У звичайній DNN, що працює на GPU, кожен нейрон у кожному шарі обчислює значення активації на кожному прямому проході, незалежно від того, чи змінився вхід. В SNN нейрон споживає енергію лише тоді, коли генерує спайк, — а в типових навантаженнях у будь-який момент спрацьовує лише мала частка нейронів. Саме ця розріджена, подієво-керована природа обчислень і дає нейроморфному обладнанню його надзвичайну енергоефективність: якщо 95% нейронів мовчать, то 95% чипа споживає майже нульову потужність.Правило навчання заслуговує окремої уваги. У біологічних мозках синапси зміцнюються або слабшають залежно від відносного часу спрацювання спайків у з'єднаних нейронах — цей механізм називають часозалежною синаптичною пластичністю (spike-timing-dependent plasticity, STDP). Якщо нейрон A стабільно спрацьовує безпосередньо перед нейроном B, синапс від A до B зміцнюється («нейрони, що спрацьовують разом, з'єднуються разом»). Якщо ж A спрацьовує одразу після B, синапс, навпаки, слабшає. Це локальне правило навчання: кожному синапсу потрібна інформація лише про два свої нейрони, а не глобальний сигнал помилки, розповсюджений через усю мережу. Завдяки STDP нейроморфні чипи можуть навчатися безпосередньо на пристрої, на периферії, без окремого навчального сервера.Існують і гібридні підходи: методи сурогатного градієнта дають змогу навчати SNN алгоритмами, подібними до зворотного поширення, апроксимуючи недиференційовну функцію спайка гладкою сурогатною функцією. Такий підхід поєднує зрілу екосистему навчання DNN з перевагами ефективності імпульсних мереж: модель можна навчити в PyTorch і розгорнути на нейроморфному обладнанні.<br><a data-tooltip-position="top" aria-label="https://www.intel.com/content/www/us/en/research/neuromorphic-computing.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.intel.com/content/www/us/en/research/neuromorphic-computing.html" target="_self">Loihi</a> від Intel — це найбільш програмований нейроморфний чип, доступний дослідникам сьогодні. На відміну від підходу IBM (який ми побачимо далі), Loihi від самого початку проєктувався як дослідницька платформа — достатньо гнучка, щоб науковці могли експериментувати з різними моделями нейронів, правилами навчання та топологіями мереж.Loihi 1 (2018) об'єднує 128 нейроморфних ядер і 3 вбудовані ядра x86 на кристалі 14 нм площею близько 60 мм². Кожне нейроморфне ядро містить 1 024 імпульсні нейронні блоки, що дає чипу загальну ємність у 130 000 нейронів та 128 мільйонів синапсів, підкріплених 33 МБ внутрішньокристальної SRAM. Увесь чип споживає менш ніж 1,5 вата.<br><img alt="Архітектура нейроморфного чипа Intel Loihi" src="assets/l10-intel-loihi-architecture.png" target="_self">Архітектура чипа Intel Loihi — 128 нейроморфних ядер із навчанням на чипі, виготовлених за 14-нм техпроцесом Intel.Три ядра x86 відіграють роль керувальних процесорів: вони відповідають за конфігурацію, моніторинг та зв'язок із хостом, тоді як 128 нейроморфних ядер виконують власне імпульсні нейронні обчислення. Такий гібридний дизайн відбиває практичну реальність: нейроморфне обладнання чудово справляється з нейронними обчисленнями, однак системне управління все ще потребує звичайних процесорів.Перевага у продуктивності вражає. На задачах розрідженого кодування — класі задач обробки сигналів, що має прямі застосування у стиснутому зондуванні та виділенні ознак, — Loihi продемонстрував покращення добутку енергії на затримку у 5 000 разів порівняно зі звичайними рішеннями на CPU та GPU. Добуток енергії на затримку (енергія × латентність) — стандартна метрика для спільної оптимізації потужності та швидкості, тож покращення у 5 000 разів — це не поступовий приріст, а принципово інший режим роботи.<br>Loihi 2 (2021) перейшов на техпроцес Intel 4 і досягнув приблизно десятикратної швидкості порівняно з попередником. Водночас у нього додали програмовані моделі нейронів: дослідники можуть описувати власну поведінку нейронів поза межами LIF, зокрема біологічно реалістичніші моделі з кількома відділами та дендритною обробкою. Intel також випустила <a data-tooltip-position="top" aria-label="https://github.com/lava-nc/lava" rel="noopener nofollow" class="external-link is-unresolved" href="https://github.com/lava-nc/lava" target="_self"><strong></strong></a>Lava — фреймворк з відкритим кодом для розробки нейроморфних алгоритмів. Lava пропонує модель програмування на Python, у якій розробники описують обчислювальні графи імпульсних процесів, а фреймворк відображає їх на обладнання Loihi. Це зняло одну з найбільших перешкод у галузі — брак стандартизованих інструментів розробки, без яких дослідники не могли ділитися та відтворювати результати на різних нейроморфних платформах.Hala Point (2024) — найбільша нейроморфна система Intel, що розміщує 1 152 чипи Loihi 2 в корпусі заввишки шість стійкових юнітів і сумарно має 1,15 мільярда нейронів та 128 мільярдів синапсів, тобто наближається до кількості нейронів у мозку сови. Hala Point — дослідницька платформа для вивчення великомасштабних нейроморфних навантажень, як-от моделювання в реальному часі та розпізнавання ключових слів із великим словником. Для масштабу: мозок плодової мушки має близько 100 000 нейронів, мозок миші — близько 70 мільйонів, а Hala Point за кількістю нейронів розташовується між мишею та котом (хоча це порівняння недосконале, адже біологічні нейрони значно складніші за блоки LIF).IBM підійшла до нейроморфних обчислень з іншого боку, оптимізуючи чипи під граничну щільність та енергоефективність у навантаженнях висновування. Там, де Intel будувала гнучку дослідницьку платформу, IBM будувала чипи для реального розгортання — щоб якнайефективніше виконувати конкретні навантаження нейронних мереж.TrueNorth (2014) став знаковим чипом: 4 096 нейросинаптичних ядер, у кожному — 256 програмованих нейронів і 256 × 256 = 65 536 синапсів, сумарно 1 мільйон нейронів і 256 мільйонів синапсів на кристалі 28 нм. Енергоспоживання — близько 70 мілівата на квадратний сантиметр, тобто приблизно 10 ват для всього чипа; це співмірно з потужністю батареї слухового апарата, лише в більшому масштабі.TrueNorth використовує архітектуру Globally Asynchronous, Locally Synchronous (GALS) — глобально асинхронну, локально синхронну. Кожне ядро працює на власному локальному тактовому сигналі та спілкується з іншими ядрами через асинхронну 2D-мережу. Це означає, що немає жодного глобального тактового сигналу, який би проходив через усі 4 096 ядер, — а отже, зникає головне джерело енергоспоживання та складності синхронізації. Пригадаймо з L06, що поширення тактового сигналу через великий чип — одна з найскладніших задач у цифровому проєктуванні; TrueNorth обходить її повністю.Усередині кожного ядра кросбарна матриця відображає входи на виходи: вертикальні проводи — це дендрити (вхідні сигнали), горизонтальні — аксони (вихідні сигнали), а точки їхнього перетину — програмовані синаптичні ваги. Перед нами пряма кремнієва реалізація структури «нейрон — синапс — аксон». Кожен нейрон у кросбарній матриці реалізує вже знайому нам модель LIF із налаштовуваними параметрами витоку, порогу та скидання. 2D-мережа з'єднує ядра в решітчасту топологію, тож спайки можуть поширюватися від аксонів одного ядра до дендритів іншого — так імітують далекосяжні аксональні з'єднання в біологічній нервовій тканині.<br><img alt="Плата нейроморфної розробки IBM NorthPole" src="assets/l10-ibm-northpole-board.jpeg" target="_self">Плата IBM NorthPole — наступник TrueNorth, що досягає у 4 000 разів вищої продуктивності завдяки чергуванню обчислень із внутрішньокристальною пам'яттю.NorthPole (2023) — це різкий стрибок уперед. Побудований на 12-нм технології, NorthPole у 4 000 разів швидший за TrueNorth на задачах висновування і водночас у 22 рази швидший за зіставний GPU при 25-кратно меншій енергії на висновування. IBM досягла цього, поєднавши нейроморфні принципи (обчислення в пам'яті, відсутність доступу до позачипової пам'яті під час висновування) з оптимізаціями під звичайні навантаження глибоких нейронних мереж. Ключове архітектурне рішення: NorthPole зберігає всі ваги нейронної мережі на чипі, розподіляючи їх між його 256 ядрами. Під час висновування жодні дані не перетинають межу чипа, а це усуває вузьке місце пропускної здатності пам'яті, яке обмежує продуктивність GPU та TPU на задачах висновування.NorthPole — не нейроморфний дослідницький чип загального призначення, як Loihi; це прискорювач висновування, який запозичує нейроморфні архітектурні ідеї, щоб усунути вузьке місце архітектури фон Неймана. Ця відмінність ілюструє важливу думку: межа між «нейроморфним» і «звичайним прискорювачем з нейроморфним натхненням» розмивається. Оскільки найкращі ідеї з нейроморфних досліджень переходять у масовий дизайн чипів, сам ярлик «нейроморфний» з часом, імовірно, описуватиме філософію проєктування, а не окрему категорію продуктів.Де нейроморфні обчислення переважають: Автономні транспортні засоби та дрони: злиття сенсорних даних у реальному часі за енергобюджету в кілька мілівата, коли кожен джоуль подовжує час польоту. Подієво-керовані камери (динамічні сенсори зору), які виводять лише зміни пікселів, природно поєднуються з нейроморфними процесорами: і перші, і другі працюють з розрідженими асинхронними подіями. Периферійний ШІ: постійно активне розпізнавання ключових слів, жестів та виявлення аномалій у пристроях Інтернету речей, яким не по кишені ні хмарне з'єднання, ні затримка зворотного зв'язку з хмарою. Робототехніка: сенсомоторне керування з низькою затримкою та навчанням безпосередньо на пристрої — робот пристосовується до нового середовища, не звертаючись до хмари для перенавчання. Мозкоподібна архітектура природно лягає на цикли «сприйняття — дія», які потрібні роботам. Розпізнавання патернів: виявлення тонких часових закономірностей у сенсорних даних (сейсмічна активність, мережеві вторгнення, медичні сигнали) — завдання, де важить час подій, а не лише їхні значення. Переваги: Гранична енергоефективність: енергію споживають лише нейрони, що спрацьовують, а неактивні ділянки мають майже нульовий струм. Для порівняння: модель виявлення ключових слів на Loihi споживає мікровати, та сама модель на мобільному GPU — мілівати, тобто тисячократна різниця. Низька затримка: обчислення в пам'яті усуває обмін через шину пам'яті. Поширення спайків через нейроморфний чип може завершитися за мікросекунди, тоді як те саме висновування на звичайному прискорювачі забирає мілісекунди. Навчання в реальному часі: STDP та локальні правила навчання дають змогу адаптуватися прямо на пристрої — без зворотного поширення й обчислення градієнтів. Природжений паралелізм: кожен нейрон працює незалежно, що забезпечує масивний паралелізм без накладних витрат на синхронізацію, характерних для GPU. Обмеження: Розрив у точності: перетворення навченої DNN у SNN зазвичай спричиняє втрату точності на 1–3%, що для низки застосувань неприйнятно Відсутність стандартизації: кожен нейроморфний чип має власну модель програмування, набір інструкцій та інструменти розробки — аналогу CUDA чи x86 тут немає Обмежена програмна екосистема: інструментарій дослідницького, а не промислового рівня; налагоджувати імпульсну нейронну мережу значно складніше, ніж модель PyTorch Вузька перевага: для навантажень, які вже добре обслуговують GPU чи TPU (масштабне пакетне навчання, висновування трансформерів), нейроморфні чипи не пропонують чіткої переваги Порівняння нейроморфних платформ:Нейроморфна галузь сьогодні перебуває приблизно на тому самому етапі, на якому були GPU на початку 2000-х: обладнання демонструє надзвичайні перспективи на окремих тестах, але програмній екосистемі ще потрібно дозріти, перш ніж масове впровадження стане реалістичним. Дослідники та компанії, які будують цю екосистему зараз, закладають фундамент того, що впродовж наступного десятиліття цілком може стати панівною парадигмою периферійних обчислень.Кожна архітектура, розглянута досі — від ЦП до нейроморфних чипів — працює з класичними бітами: фізичними системами, які в будь-який момент часу однозначно мають значення 0 або 1. Транзистор або проводить струм, або ні. Напруга або висока, або низька. Будь-яке обчислення, хоч би яким складним воно було, зводиться до маніпулювання цими визначеними двійковими станами.Квантові обчислення відкидають цю визначеність. Вони спираються на явища квантової механіки — суперпозицію, заплутаність та інтерференцію — щоб виконувати певні обчислення експоненціально швидше, ніж це будь-коли вдалося б класичній машині.Це не просто швидший процесор. Це принципово інша модель обчислень, настільки ж відмінна від класичного комп'ютера, наскільки класичний комп'ютер відрізняється від абака. Якщо нейроморфні обчислення переосмислюють архітектуру, зберігаючи класичну фізику, то квантові обчислення переосмислюють фізику, зберігаючи загалом знайому вентильну архітектуру.Щоб зрозуміти квантові обчислення, потрібно опанувати три поняття з квантової фізики. Жодне з них не узгоджується з повсякденною інтуїцією — і не повинно. Квантова механіка описує поведінку матерії на атомному та субатомному масштабі, де правила принципово відрізняються від макроскопічного світу, знайомого нам із щоденного досвіду. Дивність — це не недолік людського розуміння; це властивість самої реальності. І саме цю дивність квантові комп'ютери перетворюють на обчислювальний ресурс.Суперпозиція. Класичний біт — це або 0, або 1. Квантовий біт — кубіт — може перебувати в суперпозиції обох станів водночас. Фізично кубітом може бути будь-яка дворівнева квантова система: напрямок спіна електрона (вгору чи вниз), поляризація фотона (горизонтальна чи вертикальна) або напрямок струму в надпровідній петлі (за годинниковою чи проти годинникової стрілки). Математично стан кубіта описується як α|0⟩ + β|1⟩, де α та β — комплексні числа, які називають амплітудами ймовірності, причому |α|² + |β|² = 1. Під час вимірювання кубіта отримуємо 0 з імовірністю |α|² і 1 з імовірністю |β|². До вимірювання кубіт справді перебуває в обох станах одночасно — це не наше незнання його стану, а фізична властивість системи.<br>Знаменитий <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Double-slit_experiment" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Double-slit_experiment" target="_self">експеримент із двома щілинами</a> демонструє цей корпускулярно-хвильовий дуалізм: один фотон (або електрон), проходячи крізь дві щілини, створює інтерференційну картину на екрані детектора — наче він пройшов крізь обидві щілини водночас. Саме цей експеримент, який <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Richard_Feynman" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Richard_Feynman" target="_self">Річард Фейнман</a> назвав «єдиною таємницею квантової механіки», є фізичною основою суперпозиції.<br><img alt="Слайд, що пояснює властивості кубітів та паралельні обчислення" src="assets/l10-quantum-qubits-text.png" target="_self">Ключові принципи квантових обчислень: кубіти замінюють біти, уможливлюючи 2^N одночасних операцій; проте практична спостережуваність паралельних світів залишається фундаментальною проблемою.<br>Заплутаність. Коли два кубіти стають заплутаними, їхні стани корелюють у спосіб, що не має класичного аналога. Вимірювання одного кубіта миттєво визначає стан іншого, незалежно від відстані між ними. Ейнштейн назвав це <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Quantum_entanglement" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Quantum_entanglement" target="_self">«моторошною дією на відстані»</a>, і це явище глибоко його непокоїло. Проте десятиліття експериментів — зокрема <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Bell_test" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Bell_test" target="_self">тести Белла</a> — підтвердили реальність заплутаності. Для квантових обчислень заплутаність — це ресурс: вона дає змогу кубітам обмінюватися інформацією без явної комунікації й уможливлює алгоритми, які не мають класичного відповідника.Інтерференція. Це рушійна сила квантових обчислень. Подібно до того як хвилі можуть конструктивно або деструктивно інтерферувати (два піки складаються в більший пік; пік і западина взаємно гасяться), амплітуди ймовірності кубітів теж здатні інтерферувати. Добре спроєктований квантовий алгоритм організовує свої операції так, щоб амплітуди неправильних відповідей деструктивно інтерферували (прямували до нуля), а амплітуди правильних відповідей конструктивно інтерферували (наростали до одиниці). Підсумок: вимірявши кубіти наприкінці обчислення, ми з переважною ймовірністю отримаємо правильну відповідь.Поширене хибне уявлення полягає в тому, що квантові комп'ютери «пробують усі можливості одночасно» і просто обирають правильну. Це оманлива картина. Суперпозиція справді кодує всі можливості, однак вимірювання суперпозиції дає лише один випадковий результат. Мистецтво проєктування квантових алгоритмів — побудувати таку послідовність вентилів, щоб через інтерференцію амплітуда правильної відповіді зростала, а амплітуди неправильних зменшувалися; тоді єдиний результат, який ми отримуємо при вимірюванні, з переважною ймовірністю виявиться потрібним.Квантовий комп'ютер маніпулює кубітами через послідовність квантових вентилів — унітарних перетворень, які обертають стани кубітів, заплутують їх та зсувають їхню фазу. Квантовий вентиль для кубіта — те саме, що логічний вентиль для класичного біта, проте з критичною відмінністю: квантові вентилі оборотні (інформація не знищується) і діють на амплітуди ймовірності, а не на визначені значення.Серед поширених квантових вентилів: Вентиль Адамара (H): переводить кубіт у рівну суперпозицію — «вентиль підкидання монети», з якого починається більшість квантових алгоритмів. Застосований до |0⟩, він створює (|0⟩ + |1⟩)/√2 Вентиль Паулі-X: квантовий відповідник класичного вентиля NOT — перевертає |0⟩ на |1⟩ і навпаки Вентиль CNOT (Controlled-NOT): заплутує два кубіти — перевертає цільовий кубіт тоді й лише тоді, коли керувальний кубіт перебуває у стані |1⟩. Цей двокубітний вентиль є основним джерелом заплутаності у квантових схемах Фазові вентилі (S, T, Rz): обертають фазу амплітуди кубіта, не змінюючи ймовірностей вимірювання — вони необхідні для інтерференції й становлять основу багатьох квантових алгоритмів Квантовий алгоритм — це ретельно поставлена послідовність таких вентилів, аналогічна класичній схемі, але така, що діє на амплітуди ймовірності замість визначених бітових значень. Так само як будь-яке класичне обчислення можна побудувати з вентилів NAND, будь-яке квантове обчислення можна побудувати з невеликого набору універсальних квантових вентилів (зазвичай Адамара, CNOT та T).Потужність квантових обчислень випливає з паралелізму на фізичному рівні. Маючи N кубітів у суперпозиції, квантовий комп'ютер представляє 2^N станів одночасно й одночасно з ними оперує. 300-кубітний квантовий комп'ютер міг би представити більше станів, ніж атомів у спостережуваному Всесвіті — число, яке жоден класичний комп'ютер не зміг би навіть перерахувати, не кажучи вже про обробку.<br><img alt="Рефрижератор розведення квантового комп'ютера" src="assets/l10-quantum-computer.jpeg" target="_self">Рефрижератор розведення квантового комп'ютера — надпровідні кубіти необхідно охолоджувати до температури, близької до абсолютного нуля (приблизно 15 мілікельвінів), щоб підтримувати квантову когерентність.Але є одна проблема: декогеренція. Кубіти надзвичайно крихкі. Будь-яка взаємодія з середовищем — випадковий фотон, вібрація, теплова флуктуація — може призвести до «колапсу» квантового стану в класичний, знищуючи суперпозицію. Саме тому квантові комп'ютери потребують екстремальної ізоляції.<br>Найпоширеніший апаратний підхід — надпровідні кубіти — працює всередині <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Dilution_refrigerator" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Dilution_refrigerator" target="_self">рефрижераторів розведення</a>, які охолоджують процесор приблизно до 15 мілікельвінів (0,015 К), тобто до температури, нижчої, ніж у відкритому космосі. Рефрижератор розведення — це характерна люстроподібна конструкція, знайома з фотографій квантових комп'ютерів: кілька вкладених одне в одне ступенів охолодження, кожен з яких знижує температуру ще на один порядок, а чип із кубітами розташований на самому дні. Весь апарат може сягати трьох метрів заввишки й коштувати мільйони доларів — усе заради того, щоб чип розміром з ніготь залишався достатньо холодним для домінування квантових ефектів.До інших апаратних підходів належать: Захоплені іони: окремі атоми, підвішені в електромагнітних полях; тривалий час когерентності, але повільніші вентильні операції Фотонні кубіти: частинки світла, що проходять через оптичні схеми; природно стійкі до декогеренції, проте їх складно заплутати Топологічні кубіти: кодують інформацію в топології квантових станів, завдяки чому теоретично стійкіші до помилок (підхід Microsoft, поки що на ранній стадії розробки) Кожен підхід має свої компроміси між часом когерентності, точністю вентилів та масштабованістю, і досі невідомо, який з них зрештою переможе.Квантовий комп'ютер не є швидшим за класичний у всьому. Це один із найважливіших моментів для розуміння квантових обчислень, адже популярні медіа часто представляють їх як універсальне оновлення. Насправді для більшості повсякденних завдань — обробки тексту, перегляду вебсторінок, сортування списку — квантовий комп'ютер не дає переваги й може бути навіть повільнішим (бо квантові вентилі наразі значно повільніші за класичні транзисторні перемикачі). Потужність квантових обчислень зосереджена в конкретних класах задач, де математична структура задачі дає квантовим алгоритмам змогу забезпечити експоненціальне або поліноміальне прискорення.Теоретичні (оракульні) алгоритми:Ці алгоритми припускають існування функції «чорного ящика» (оракула) й доводять, що квантовим машинам потрібно менше запитів для визначення властивостей такої функції: Алгоритм Дойча (1985): визначає, чи є функція константною, чи збалансованою, за один запит замість двох класично. Перший доказ того, що квантові обчислення можуть перевершити класичні. Хоча сама задача тривіальна, алгоритм продемонстрував принцип — квантовий паралелізм через суперпозицію — з далекосяжними наслідками. Алгоритм Бернстайна — Вазірані (1993): знаходить прихований бітовий рядок, закодований у функції, за один запит замість N класично. Він розширив ідею Дойча на загальніший випадок і показав, що квантова перевага масштабується з розміром задачі. Алгоритм Саймона (1994): знаходить період функції експоненціально швидше за будь-який класичний алгоритм. Це був перший приклад експоненціального квантового прискорення (а не просто сталого множника), який безпосередньо надихнув найважливіший з коли-небудь відкритих квантових алгоритмів. Практичні алгоритми:
<br>
<a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Shor%27s_algorithm" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Shor%27s_algorithm" target="_self">Алгоритм Шора</a> (1994): факторизує великі цілі числа за поліноміальний час. Класичні алгоритми потребують субекспоненціального часу — і саме це припущення математичної складності лежить в основі <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/RSA_(cryptosystem)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/RSA_(cryptosystem)" target="_self">шифрування RSA</a>. Достатньо великий квантовий комп'ютер, що виконує алгоритм Шора, зламав би RSA та більшість сучасної криптографії з відкритим ключем. Один лише цей алгоритм перетворив квантові обчислення з теоретичної цікавинки на питання національної безпеки. <br>
<a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Grover%27s_algorithm" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Grover%27s_algorithm" target="_self">Алгоритм Гровера</a> (1996): здійснює пошук у невідсортованій базі даних з N елементів за час O(√N) замість O(N) класично. Це квадратичне прискорення — менш ефектне, ніж експоненціальне прискорення Шора, зате застосовне до значно ширшого класу задач, зокрема оптимізації, задоволення обмежень та пошуку в базах даних. Щоб зробити ці прискорення наочнішими: факторизація 2 048-бітного ключа RSA за допомогою найкращого відомого класичного алгоритму зайняла б більше часу, ніж вік Всесвіту. Алгоритм Шора на достатньо великому квантовому комп'ютері з корекцією помилок упорався б з цим за години. Пошук у невідсортованій базі даних з трильйона записів класично потребує до трильйона кроків; алгоритму Гровера — близько мільйона.<br>Так само важливо розуміти, чого квантові комп'ютери не здатні прискорити. Задачі, які вже ефективно розв'язуються класично — сортування, пошук у відсортованому списку, більшість алгоритмів на графах — майже або зовсім нічого не виграють від квантових обчислень. <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Quantum_computing#Potential" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Quantum_computing#Potential" target="_self">Квантове прискорення не є універсальним</a>: воно зосереджене на задачах зі специфічною математичною структурою, яку здатна використати квантова інтерференція (періодичність — для алгоритму Шора, невпорядкований пошук — для алгоритму Гровера, моделювання гамільтоніанів — для молекулярного моделювання).Шлях від теорії до фізичного обладнання був повільним, але прискорюється:Звернімо увагу на темп прогресу: від першого 2-кубітного комп'ютера (1998) до практичних двозначних кількостей кубітів минуло 13 років, але вже лише 8 років — до квантової переваги (2019) і ще 4 роки — до перетину позначки в 1 000 кубітів (2023). Сьогодні проблема вже не в тому, щоб додати більше кубітів, а в тому, щоб зробити їх достатньо надійними для виконання корисних обчислень.Заява Google про квантову перевагу в 2019 році потребує застереження: обчислення було спеціально сконструйовано так, щоб бути складним для класичних комп'ютерів і легким для квантових, а IBM згодом стверджувала, що класичний суперкомп'ютер міг би розв'язати ту саму задачу за дні, а не за 10 000 років, якщо виділити йому достатньо дискового простору. Ця дискусія підкреслила, що «квантова перевага» — це рухома ціль: у міру того як вдосконалюються класичні алгоритми й обладнання, постійно зростає й планка для демонстрації справжньої квантової переваги.Центральна проблема квантових обчислень — корекція помилок. Сучасні кубіти «зашумлені»: вони втрачають когерентність і накопичують помилки значно швидше за класичні транзистори. Рівень помилок однокубітного вентиля зазвичай становить близько 0,1–1 %, тоді як для класичного транзистора він приблизно 10^-18. Один логічний кубіт (достатньо надійний для тривалих обчислень) може потребувати сотень або тисяч фізичних кубітів, відведених на корекцію помилок — надлишковість використовується для виявлення та виправлення помилок до того, як вони пошкодять результат. Саме тому машина з 1 000 кубітами сьогодні ще не може запустити алгоритм Шора на криптографічно значущих розмірах ключів: більшість цих кубітів поглинула б корекція помилок, а для самого обчислення залишилося б надто мало.<br><a data-tooltip-position="top" aria-label="https://www.ibm.com/quantum/roadmap" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.ibm.com/quantum/roadmap" target="_self">Квантова дорожня карта</a> IBM передбачає 200 логічних кубітів, здатних виконати 100 мільйонів вентилів, до 2029 року — систему, що могла б розв'язувати раніше нерозв'язні задачі в матеріалознавстві, розробці ліків та оптимізації. Прорив Google з чипом Willow наприкінці 2024 року став критичним підтвердженням концепції: уперше збільшення кількості кубітів у коді корекції помилок зменшило загальний рівень помилок, перетнувши поріг, який теоретики визначали як необхідний для масштабованих квантових обчислень.Галузеві аналітики прогнозують, що до 2035 року квантові обчислення стануть індустрією вартістю 1,3 трильйона доларів завдяки застосуванням у таких сферах: Криптоаналіз: зламування поточних схем шифрування (і мотивація для постквантових замін) Молекулярне моделювання: моделювання хімічних реакцій на квантовому рівні для розробки ліків та матеріалознавства — завдання, у якому квантові комп'ютери мають природну перевагу, оскільки молекули самі є квантовими системами Фінансове моделювання: оптимізація портфелів, аналіз ризиків та ціноутворення деривативів містять комбінаторні задачі, до яких застосовне квантове прискорення Штучний інтелект: алгоритми квантового машинного навчання можуть пришвидшити навчання певних типів моделей, хоча це поки що найбільш спекулятивна сфера застосування Оптимізація: логістика, складання розкладів та задачі ланцюгів постачання потребують пошуку у величезних просторах рішень, де квантові алгоритми здатні знаходити хороші рішення швидше за класичні евристики Не очікується, що квантові обчислення замінять класичні — вони їх доповнюватимуть. Найімовірнішою найближчою парадигмою є гібридні квантово-класичні обчислення, у яких класичний комп'ютер виконує основну частину роботи, а конкретні підзадачі (кроки оптимізації, обчислення молекулярної енергії) делегує квантовому співпроцесору. Це аналогічно тому, як GPU та TPU сьогодні доповнюють ЦП.<br>У ближчій перспективі зростання квантових обчислень уже спричинило конкретну відповідь: постквантову криптографію. NIST <a data-tooltip-position="top" aria-label="https://www.nist.gov/news-events/news/2024/08/nist-releases-first-3-finalized-post-quantum-encryption-standards" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.nist.gov/news-events/news/2024/08/nist-releases-first-3-finalized-post-quantum-encryption-standards" target="_self">завершив перші постквантові криптографічні стандарти у 2024 році</a>, обравши алгоритми на основі решіткових задач та геш-функцій, що вважаються стійкими як до класичних, так і до квантових атак. Кожна організація, яка працює з конфіденційними даними довгострокової цінності — медичні записи, урядові комунікації, фінансові інструменти — має планувати перехід уже зараз, навіть якщо великомасштабні квантові комп'ютери з'являться ще через кілька років.Терміновість зумовлена стратегією, відомою як «збери зараз, розшифруй пізніше»: зловмисники можуть перехопити й зберегти зашифровані комунікації сьогодні, а потім розшифрувати їх, коли з'явиться достатньо потужний квантовий комп'ютер. Якщо дані мають термін придатності 20 років (медичні записи, державні таємниці, інфраструктурні плани), а квантовий комп'ютер, здатний зламати RSA, з'явиться через 15 років, то дані вже під загрозою сьогодні. Саме тому міграція на постквантову криптографію відбувається вже зараз, а не тоді, коли квантові комп'ютери стануть реальністю.Протягом цієї лекції ми ознайомилися з дев'ятьма принципово різними підходами до обчислень — від CPU (центрального процесора) загального призначення, здатного виконати будь-яку програму, через предметно-орієнтовані прискорювачі на кшталт DSP (цифрових сигнальних процесорів), GPU (графічних процесорів) та TPU (тензорних процесорних блоків), до архітектур, що ламають парадигми, — нейроморфних та квантових комп'ютерів. Кожна з них займає окреме місце на спектрі від універсальної (здатна на все, але не оптимізована ні для чого) до спеціалізованої (робить одну річ, але робить її надзвичайно добре). Наведена нижче таблиця підсумовує ці компроміси:Із цієї таблиці виникає кілька закономірностей: Гнучкість і продуктивність обернено пов'язані. CPU може виконувати будь-яке програмне забезпечення, проте за продуктивністю поступається кожній спеціалізованій архітектурі на її профільному навантаженні. ASIC (спеціалізована інтегральна схема) перевершує все на своєму цільовому завданні, але його не можна перепрограмувати. Це не збіг, а фундаментальний інженерний компроміс. Обладнання загального призначення мусить містити схеми для кожної можливої операції; спеціалізоване ж обладнання може віддати всі свої транзистори тим операціям, які воно фактично виконує. Споживання енергії йде слідом за щільністю продуктивності. Архітектури, які вміщують більше корисних обчислень на такт на ват (нейроморфні, ASIC, DSP), виграють у енергоефективності. Архітектури, що покладаються на грубу силу паралелізму (GPU, квантові), споживають пропорційно більше. Жодна архітектура не перемагає скрізь. Це фундаментальна причина того, що сучасні системи є гетерогенними. Межі між категоріями розмиваються. Сучасні GPU містять тензорні ядра (спеціалізовані матричні блоки), що робить їх частково подібними до TPU. Сучасні CPU мають блоки SIMD (AVX-512, NEON) і в цьому наближаються до GPU. FPGA (програмовані вентильні матриці) можна налаштувати для емуляції будь-якої іншої архітектури. Найновіші FPGA від Intel містять жорстко запаяні тензорні блоки ШІ поряд із програмованою логічною матрицею. Категорії в цій таблиці — зручні ментальні моделі, а не жорсткі рамки. Зрілість суттєво різниться. CPU та GPU мають за собою десятиліття розвитку програмної екосистеми. TPU швидко дозрівають завдяки інвестиціям Google. Нейроморфні чипи перебувають на переході від досліджень до раннього впровадження. Квантові ж комп'ютери для більшості практичних цілей усе ще залишаються на стадії досліджень. Оцінюючи архітектуру, програмну екосистему слід враховувати не менше, ніж специфікації обладнання. <br>Головна тенденція в комп'ютерній архітектурі за останні два десятиліття — перехід від процесорів загального призначення до предметно-орієнтованих прискорювачів. Це не мода, а відповідь на виклики фізики. Коли <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Dennard_scaling" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Dennard_scaling" target="_self">масштабування Деннарда</a> завершилося (L01) і частота одного ядра вийшла на плато близько 2005 року, єдиним шляхом до вищої продуктивності стала спеціалізація: будувати обладнання, яке робить менше, але робить це за меншу кількість тактів і з нижчим споживанням енергії на операцію.<br>У дослідницькій літературі ця тенденція має усталену назву — ера «предметно-орієнтованих архітектур», окреслена <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Computer_Architecture:_A_Quantitative_Approach" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Computer_Architecture:_A_Quantitative_Approach" target="_self">Джоном Геннессі та Девідом Паттерсоном</a> (відомими за RISC) у їхній лекції з нагоди отримання премії Тюрінга 2017 року. Їхня теза така: кінець приростів продуктивності, що їх забезпечував закон Мура, означає, що наступне десятиліття прогресу обчислень досягатиметься переважно завдяки спеціалізації, а архітектори, які проєктуватимуть наступне покоління прискорювачів, визначатимуть траєкторію індустрії настільки ж глибоко, як розробники мікропроцесорів 1970-х.Результатом є гетерогенні обчислення — системи, що поєднують кілька типів архітектур, кожна з яких обробляє те навантаження, для якого найкраще пристосована. Сучасний смартфон уже є гетерогенним комп'ютером: він містить CPU загального призначення (ARM Cortex), GPU (Adreno або Mali), нейронний процесорний блок (NPU) для МН безпосередньо на пристрої, цифровий сигнальний процесор для аудіо та модемної смуги, а також кілька ASIC фіксованої функції — для ISP камери, кодування/декодування відео та криптографічних операцій. Чипи серії M від Apple інтегрують на одному кристалі ядра CPU, ядра GPU, Neural Engine (NPU), блоки кодування/декодування мультимедіа й уніфіковану архітектуру пам'яті — тобто є системою-на-кристалі (SoC), яка втілює гетерогенні обчислення у кремнії.У масштабах центрів обробки даних картина ще різноманітніша. Хмарні провайдери пропонують інстанси на основі GPU (NVIDIA A100/H100), TPU (Google), FPGA (Intel/Xilinx), спеціалізованих прискорювачів ШІ (AWS Inferentia/Trainium) та SmartNIC, що знімають з CPU мережеві задачі. Рівень оркестрації — тобто рішення, яке навантаження й на якому прискорювачі виконується, — стає не менш важливим за самі прискорювачі.<br>Ця тенденція має наслідки і для програмної інженерії. Фреймворки на кшталт <a data-tooltip-position="top" aria-label="https://developer.nvidia.com/cuda-toolkit" rel="noopener nofollow" class="external-link is-unresolved" href="https://developer.nvidia.com/cuda-toolkit" target="_self">CUDA</a> (для GPU), <a data-tooltip-position="top" aria-label="https://www.khronos.org/opencl/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.khronos.org/opencl/" target="_self">OpenCL</a> (крос-платформне програмування прискорювачів), <a data-tooltip-position="top" aria-label="https://www.tensorflow.org/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.tensorflow.org/" target="_self">TensorFlow</a> і <a data-tooltip-position="top" aria-label="https://pytorch.org/" rel="noopener nofollow" class="external-link is-unresolved" href="https://pytorch.org/" target="_self">PyTorch</a> (які абстрагують GPU та TPU), а також Intel Lava (для нейроморфних чипів) — кожен із них пропонує модель програмування, адаптовану до конкретної апаратної парадигми. Завдання програміста дедалі більше зводиться до вибору правильного рівня абстракції для цільового обладнання — і до достатнього розуміння архітектури, що лежить в основі, аби зробити цей вибір обґрунтовано.Ця гетерогенність має прямий наслідок і для вас як фахівця з комп'ютерних наук: розуміння архітектурних концепцій більше не є факультативним, навіть якщо ви ніколи не проєктуватимете обладнання самостійно. Пишучи код для GPU, потрібно розуміти паралелізм SIMD та об'єднання доступу до пам'яті. Розгортаючи модель на периферійному пристрої, треба знати, чи є в цільовій платформі NPU і які обмеження квантизації він накладає. Проєктуючи систему, що працює з конфіденційними даними, потрібно розуміти, чому постквантова криптографія важлива і на яких обчислювальних припущеннях вона ґрунтується.Дев'ять архітектур цієї лекції — це не дев'ять ізольованих тем, а дев'ять точок на континуумі, яким ви рухатиметеся протягом своєї кар'єри: CPU дав вам основу — модель загального призначення «інструкція за інструкцією», з якої починається все програмне забезпечення. DSP показав, що відбувається під час оптимізації під конкретний потік даних обробки сигналів, — блоки MAC, циклічні буфери, гарвардська архітектура. GPU продемонстрував масивний паралелізм даних — тисячі простих ядер, що виконують одну й ту саму операцію над різними даними. FPGA показала реконфігуроване обладнання — можливість змінювати саму схему, а не лише програмне забезпечення. VLIW продемонстрував планування, кероване компілятором, — перенесення складності з обладнання до компілятора. ASIC уособлює кінцеву точку спеціалізації — максимальна продуктивність за нульової гнучкості. TPU показав, як нове навантаження (машинне навчання) може виправдати цілком нову архітектуру — систолічні матриці для множення матриць. Нейроморфні обчислення засвідчили, що архітектура фон Неймана сама по собі є предметом обговорення, — обчислення можуть відбуватися там, де зберігаються дані. Квантові обчислення показали, що класична модель обчислень — біти, вентилі, детерміністична логіка — не єдина можлива у фізиці. Кожна архітектура втілює іншу відповідь на одне й те саме запитання: за умов обмеженої кількості транзисторів, обмеженої енергії та обмеженого часу — який найкращий спосіб організувати обчислення для цього класу задач?Архітектури, які матимуть значення через десять років, можливо, ще навіть не існують. Дослідницькі лабораторії вже вивчають: Обчислення в пам'яті: виконання логічних операцій безпосередньо всередині чипів DRAM або флеш-пам'яті з повним усуненням переміщення даних — остаточне розв'язання проблеми вузького місця архітектури фон Неймана Оптичні обчислення: застосування фотонів замість електронів для певних матричних операцій, що потенційно досягає величезної пропускної здатності за дуже низького споживання енергії Зберігання на ДНК: кодування даних у синтетичних ланцюгах ДНК для архівних цілей — один грам ДНК теоретично здатен зберегти 215 петабайтів Відродження аналогових обчислень: використання неперервних напруг замість дискретних бітів для певних навантажень (нейронні мережі, диференціальні рівняння) — обмін точності на енергоефективність Кожен із цих напрямів на момент написання перебуває приблизно там, де квантові обчислення були в 1990-х, — теоретично перспективний, але практично незрілий.Однак незмінними залишаться фундаментальні поняття: компроміс між гнучкістю та ефективністю, протиставлення загального і спеціального призначення, закони фізики, що обмежують кожен проєкт, та економічні сили, які визначають, які архітектури будуть виготовлятися масово. Саме розуміння цих понять, а не запам'ятовування специфікацій конкретного чипа, дозволяє фахівцю з комп'ютерних наук орієнтуватися в галузі, яка переосмислює своє обладнання кожне десятиліття, зберігаючи свої принципи упродовж століть. Саме до цього ця лекція і цей курс вас готували. Спеціалізація перемагає загальність. Коли навантаження достатньо велике та чітко окреслене, предметно-орієнтована архітектура (DSP, GPU, ASIC, TPU) випереджає CPU загального призначення на порядки — і за продуктивністю, і за енергоефективністю. Простір проєктування — це спектр. На одному краю — повністю гнучкий CPU, посередині — реконфігуровна FPGA, на іншому краю — повністю фіксований ASIC; GPU, DSP, VLIW і TPU посідають проміжні позиції. Вибір залежить від обсягів виробництва, потрібної гнучкості та вимог до продуктивності. Паралелізм набуває різних форм. SIMD у DSP, SIMT у GPU, систолічні матриці в TPU, імпульсні нейронні мережі в нейроморфних чипах, квантова суперпозиція в квантових комп'ютерах — кожна архітектура реалізує паралелізм по-своєму. Архітектура пам'яті важить не менше, ніж обчислювальний блок. Гарвардська архітектура в DSP, HBM (пам'ять високої пропускної здатності) у GPU і TPU, розподілена SRAM у Tesla DOJO, обчислення в пам'яті в нейроморфних чипах — усі ці рішення долають вузьке місце архітектури фон Неймана, але кожне своїм шляхом. Майбутнє — за гетерогенністю. Сучасні системи поєднують CPU, GPU та спеціалізовані прискорювачі. Хто розуміє сильні сторони кожної архітектури, той обере правильний інструмент під конкретне обчислювальне завдання. <br><a data-tooltip-position="top" aria-label="https://www.dspguide.com/ch28/3.htm" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.dspguide.com/ch28/3.htm" target="_self">The Scientist and Engineer's Guide to DSP — Architecture</a> — Гарвардська архітектура DSP та Super Harvard (SHARC)
<br><a data-tooltip-position="top" aria-label="https://learnopencv.com/modern-gpu-architecture-explained/" rel="noopener nofollow" class="external-link is-unresolved" href="https://learnopencv.com/modern-gpu-architecture-explained/" target="_self">Inside the GPU: A Guide to Modern Graphics Architecture</a> — архітектура GPU, SM, ієрархія пам'яті
<br><a data-tooltip-position="top" aria-label="https://www.yosefk.com/blog/simd-simt-smt-parallelism-in-nvidia-gpus.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.yosefk.com/blog/simd-simt-smt-parallelism-in-nvidia-gpus.html" target="_self">SIMD &lt; SIMT &lt; SMT: Parallelism in NVIDIA GPUs</a> — компроміси SIMD, SIMT, SMT
<br><a data-tooltip-position="top" aria-label="https://anysilicon.com/ultimate-guide-asic-application-specific-integrated-circuit/" rel="noopener nofollow" class="external-link is-unresolved" href="https://anysilicon.com/ultimate-guide-asic-application-specific-integrated-circuit/" target="_self">Ultimate Guide: ASIC</a> — типи ASIC, процес проєктування, аналіз вартості
<br><a data-tooltip-position="top" aria-label="https://learn.sparkfun.com/tutorials/how-does-an-fpga-work/all" rel="noopener nofollow" class="external-link is-unresolved" href="https://learn.sparkfun.com/tutorials/how-does-an-fpga-work/all" target="_self">How Does an FPGA Work?</a> — архітектура FPGA, LUT, CLB
<br><a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/very-long-instruction-word-vliw-architecture/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/very-long-instruction-word-vliw-architecture/" target="_self">VLIW Architecture</a> — концепції VLIW, планування компілятором, реалізації
<br><a data-tooltip-position="top" aria-label="https://blog.bytebytego.com/p/how-googles-tensor-processing-unit" rel="noopener nofollow" class="external-link is-unresolved" href="https://blog.bytebytego.com/p/how-googles-tensor-processing-unit" target="_self">How Google's TPU Works</a> — систолічні матриці TPU, покоління, продуктивність
<br><a data-tooltip-position="top" aria-label="https://www.nextplatform.com/2022/08/23/inside-teslas-innovative-and-homegrown-dojo-ai-supercomputer/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.nextplatform.com/2022/08/23/inside-teslas-innovative-and-homegrown-dojo-ai-supercomputer/" target="_self">Inside Tesla's DOJO AI Supercomputer</a> — чіп D1, тренувальні тайли, ExaPod
<br><a data-tooltip-position="top" aria-label="https://www.ibm.com/think/topics/neuromorphic-computing" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.ibm.com/think/topics/neuromorphic-computing" target="_self">What Is Neuromorphic Computing?</a> — обчислення за зразком мозку, SNN, застосування
<br><a data-tooltip-position="top" aria-label="https://open-neuromorphic.org/blog/truenorth-deep-dive-ibm-neuromorphic-chip-design/" rel="noopener nofollow" class="external-link is-unresolved" href="https://open-neuromorphic.org/blog/truenorth-deep-dive-ibm-neuromorphic-chip-design/" target="_self">TrueNorth Deep Dive</a> — нейросинаптична архітектура IBM TrueNorth
<br><a data-tooltip-position="top" aria-label="https://open-neuromorphic.org/neuromorphic-computing/hardware/loihi-intel/" rel="noopener nofollow" class="external-link is-unresolved" href="https://open-neuromorphic.org/neuromorphic-computing/hardware/loihi-intel/" target="_self">Intel Loihi</a> — нейроморфний дослідницький чіп Intel
<br><a data-tooltip-position="top" aria-label="https://www.ibm.com/think/topics/quantum-computing" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.ibm.com/think/topics/quantum-computing" target="_self">What Is Quantum Computing?</a> — кубіти, алгоритми, поточний стан
]]></description><link>lectures/l10.-огляд-сучасних-архітектур.html</link><guid isPermaLink="false">Lectures/L10. Огляд сучасних архітектур.md</guid><pubDate>Mon, 20 Apr 2026 10:37:40 GMT</pubDate><enclosure url="assets/l10-dsp-adc-dac-diagram.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l10-dsp-adc-dac-diagram.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L11-NOTES. Ієрархія пам'яті та продуктивність]]></title><description><![CDATA[Короткий довідник до Лекції 11. Повні матеріали: L11. Ієрархія пам'яті та продуктивність.md.Розрив процесор–пам'ять (стіна пам'яті): швидкість CPU від 1980 року зросла приблизно в 1000 разів, а затримка DRAM скоротилася лише у ~10 разів. Сучасний процесор на 5 ГГц чекає 200–400 тактів на кожен доступ до DRAM.Ключова ідея: один промах кешу до RAM обходиться у ~200 тактів CPU. Коефіцієнт промахів 5% здатен спричинити 10-кратне уповільнення у щільних циклах.Дізнатися більше: <a data-tooltip-position="top" aria-label="https://gist.github.com/jboner/2841832" rel="noopener nofollow" class="external-link is-unresolved" href="https://gist.github.com/jboner/2841832" target="_self">Latency Numbers Every Programmer Should Know</a>Ієрархія: Регістри → L1 → L2 → L3 → RAM → SSD → HDD → стрічка. Кожен наступний рівень приблизно у 10 разів повільніший і у 10 разів більший за попередній.Топологія: L1 розділений на L1I + L1D. L1/L2 — приватні для кожного ядра. L3 — спільний для всіх ядер. SRAM коштує ~$1 000+/ГБ, тоді як DRAM — ~$3–5/ГБ; тож ієрархія продиктована фізикою й економікою, а не є довільним компромісом.<br>Дізнатися більше: <a data-tooltip-position="top" aria-label="https://diveintosystems.org/book/C11-MemHierarchy/mem_hierarchy.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://diveintosystems.org/book/C11-MemHierarchy/mem_hierarchy.html" target="_self">Dive Into Systems — Memory Hierarchy</a>
Часова локальність: дані, до яких ми зверталися щойно, найімовірніше знадобляться знову в найближчому часі (лічильники циклів, акумулятори, вибірка інструкцій)
Просторова локальність: після звернення до певної адреси найближчим часом будуть потрібні і сусідні адреси (обхід масиву, поля структури, стек)
Використання кешу: рядок кешу розміром 64 байти вміщує 16 значень int — перше звернення дає промах, наступні п'ятнадцять — влучання (коефіцієнт влучань 93,75% лише завдяки просторовій локальності).
Порядок по рядках (крок 1): matrix[i][j] із зовнішнім i — коефіцієнт промахів 6,25%, у 4–8 разів швидше
Порядок по стовпцях (крок N): matrix[i][j] із зовнішнім j — коефіцієнт промахів близько 100%
У C масиви розкладаються по рядках, у Fortran — по стовпцях.
Послідовний доступ — у C найшвидше змінюється крайній правий індекс
SoA замість AoS — структура масивів, коли обробляються лише окремі поля
Тайлінг циклів — розбиття на підблоки, що вміщуються в кеш; для множення матриць дає прискорення в 10–20 разів
Мінімізація робочого набору — використання найменших придатних типів даних; відокремлення «гарячих» даних від «холодних»
<br>Дізнатися більше: <a data-tooltip-position="top" aria-label="https://diveintosystems.org/book/C11-MemHierarchy/locality.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://diveintosystems.org/book/C11-MemHierarchy/locality.html" target="_self">Dive Into Systems — Locality</a>| Тег (старші біти) | Індекс (середні біти) | Зміщення (молодші біти) |
Приклад розрахунку — 32-бітна адреса, кеш з прямим відображенням на 16 КБ, рядки по 64 Б:Рядків = 16384 / 64 = 256
Зміщення = 6 біт | Індекс = 8 біт | Тег = 18 біт
Пошук: виділити індекс -&gt; обрати множину -&gt; перевірити біт дійсності -&gt; порівняти тег -&gt; ВЛУЧАННЯ (використати зміщення) або ПРОМАХ (завантажити з наступного рівня). Жодного перебору — O(1) завдяки прямому індексуванню.<br>Дізнатися більше: <a data-tooltip-position="top" aria-label="https://diveintosystems.org/book/C11-MemHierarchy/caching.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://diveintosystems.org/book/C11-MemHierarchy/caching.html" target="_self">Dive Into Systems — CPU Caches</a>Сучасні ЦП: L1 — 8-канальний, L2 — 8-канальний, L3 — 12–16-канальний. Граничні випадки: 1-канальний збігається з кешем прямого відображення; N-канальний з однією множиною — з повністю асоціативним.Патологія прямого відображення: два блоки, які відображаються на один і той самий рядок, постійно витісняють один одного — коефіцієнт влучань 0%, хоч решта 128 рядків стоять порожні.Пастка степеня двійки: кроки доступу, кратні кількості множин, зводяться до тих самих множин. Розв'язання: доповнити розмірність (наприклад, matrix[1024][1024+16]).<br>Дізнатися більше: <a data-tooltip-position="top" aria-label="https://en.algorithmica.org/hpc/cpu-cache/associativity/" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.algorithmica.org/hpc/cpu-cache/associativity/" target="_self">Algorithmica — Cache Associativity</a>Псевдо-LRU: апроксимація для 8 і більше каналів за допомогою двійкового дерева бітів. Аномалія Беладі: притаманна лише FIFO — збільшення кешу може призводити до більшої кількості промахів. LRU до неї нечутливий (стековий алгоритм).<br>Дізнатися більше: <a data-tooltip-position="top" aria-label="https://fiveable.me/advanced-computer-architecture/unit-7/cache-replacement-write-policies/study-guide/OYpe2aD5JCvYaxRh" rel="noopener nofollow" class="external-link is-unresolved" href="https://fiveable.me/advanced-computer-architecture/unit-7/cache-replacement-write-policies/study-guide/OYpe2aD5JCvYaxRh" target="_self">Fiveable — Cache Replacement and Write Policies</a>Перевага зворотного запису: 100 інкрементів дають лише 1 запис у пам'ять (при витісненні) замість 100. Буфери запису пом'якшують затримки наскрізного запису.Промах запису: запис з розміщенням (завантажити блок, потім записати) природно поєднується зі зворотним записом. Запис без розміщення (писати одразу в пам'ять, оминаючи кеш) — із наскрізним записом.<br>Дізнатися більше: <a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/computer-organization-architecture/cache-memory-in-computer-organization/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/computer-organization-architecture/cache-memory-in-computer-organization/" target="_self">GeeksforGeeks — Cache Memory in Computer Organization</a>AMAT = Час влучення + Коефіцієнт промахів x Штраф промаху
Однорівневий приклад: 1 + 0,05 x 100 = 6 тактів (прискорення в 16,7 разу порівняно з прямим доступом до пам'яті). При цьому 5% промахів дають 83% ефективного часу доступу.Багаторівневе розширення:AMAT = ЧасВлуч_L1 + КоефПром_L1 x (ЧасВлуч_L2 + КоефПром_L2 x (ЧасВлуч_L3 + КоефПром_L3 x ЧасПам'яті))
Результат: 1 + 0,05 x (10 + 0,20 x (30 + 0,40 x 200)) = 2,6 такту (прискорення у 77 разів порівняно з прямим доступом до RAM).Глобальний дорівнює добутку локальних коефіцієнтів: глобальний L2 = 5% x 20% = 1%, глобальний L3 = 1% x 40% = 0,4%. Лише 0,4% звернень досягають RAM, але саме вони додають ~0,96 такту до AMAT.Компроміси оптимізації: більший кеш зменшує промахи ємності, але збільшує час влучення. Вища асоціативність зменшує конфліктні промахи, але потребує додаткових компараторів. Більші блоки зменшують обов'язкові промахи, але збільшують час передачі. Кожен параметр має свою ціну — AMAT дозволяє кількісно оцінити цей компроміс.<br>Дізнатися більше: <a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/computer-organization-architecture/locality-of-reference-and-cache-operation-in-cache-memory/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/computer-organization-architecture/locality-of-reference-and-cache-operation-in-cache-memory/" target="_self">GeeksforGeeks — Locality of Reference</a>Кілька ядер кешують одну й ту саму адресу. Ядро 0 записує нове значення, але ядро 1 досі бачить застаріле — без жодного збою, просто мовчки отримує неправильну відповідь. Апаратура має розв'язувати це прозоро для програми.Шинне прослуховування (широкомовне, масштабується до ~32–64 ядер) протиставляється протоколу на основі каталогу (адресному, що масштабується до сотень ядер). Споживчі CPU зазвичай використовують прослуховування, а серверні — каталог.
Читання, коли жодне ядро не має копії -&gt; завантажити у стані E (ексклюзивна чиста копія)
Інше ядро читає той самий рядок -&gt; обидва переходять у стан S (спільний)
Запис у стані S -&gt; видати BusUpgr, інвалідувати інших -&gt; M
Інше ядро читає рядок, що перебуває у стані M -&gt; зворотний запис брудних даних, обидва переходять у S
Промах запису -&gt; видати BusRdX (читання з інвалідацією) -&gt; завантажити у стані M
Навіщо потрібен стан E: без нього (у простішому протоколі MSI) кожен запис до приватних даних породжує трафік на шині. Перехід E -&gt; M відбувається без жодного трафіку на шині — це критично для стекових фреймів і потоко-локальних змінних.Розширення: MOESI (AMD) додає стан Owned, а MESIF (Intel) — стан Forward.<br>Дізнатися більше: <a data-tooltip-position="top" aria-label="https://dev.to/sachin_tolay_052a7e539e57/cache-coherence-how-the-mesi-protocol-keeps-multi-core-cpus-consistent-170j" rel="noopener nofollow" class="external-link is-unresolved" href="https://dev.to/sachin_tolay_052a7e539e57/cache-coherence-how-the-mesi-protocol-keeps-multi-core-cpus-consistent-170j" target="_self">Cache Coherence: MESI Protocol</a> | <a data-tooltip-position="top" aria-label="https://diveintosystems.org/book/C14-SharedMemory/cache_coherence.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://diveintosystems.org/book/C14-SharedMemory/cache_coherence.html" target="_self">Dive Into Systems — Cache Coherence</a>Два потоки на різних ядрах записують у різні змінні, що опинилися в одному 64-байтному рядку кешу. MESI інвалідує весь рядок — і продуктивність падає так само, як за справжнього спільного використання.Рішення: alignas(64) для розміщення кожної змінної в окремому рядку кешу:struct alignas(64) PaddedCounter { int value; };
struct PaddedCounter counters[4]; // кожен в окремому рядку кешу
Альтернативи: потоко-локальне накопичення, розділення «гарячих» і «холодних» полів.Виявлення: perf c2c у Linux (високі значення HITM свідчать про хибне спільне використання). Евристика: якщо додавання потоків сповільнює програму без логічно спільних даних — це воно.Чому небезпечно: результати залишаються коректними (функціональної помилки немає), у вихідному коді нічого не видно, паралелізація зводиться нанівець, а проявляється ефект лише за конкурентного доступу.<br>Дізнатися більше: <a data-tooltip-position="top" aria-label="https://coffeebeforearch.github.io/2019/12/28/false-sharing-tutorial.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://coffeebeforearch.github.io/2019/12/28/false-sharing-tutorial.html" target="_self">Performance Implications of False Sharing</a>
Ієрархія пам'яті долає розрив процесор–пам'ять — L1 у 200 разів швидший за RAM
Часова + просторова локальність роблять кеші ефективними — обхід масиву по рядках у мові C є кеш-дружнім
Рядки кешу (64 Б) — одиниця передачі — адреси розкладаються на тег | індекс | зміщення
Множинно-асоціативні кеші (8–16 каналів) забезпечують компроміс між уникненням конфліктів і апаратною вартістю
AMAT = Час влучення + Коефіцієнт промахів × Штраф промаху — багаторівневі кеші перетворюють 200-тактовий доступ до RAM на ~2,6 такту ефективного доступу
Зворотний запис + запис з розміщенням — сучасний стандарт, що мінімізує трафік до пам'яті
MESI забезпечує когерентність кешу в багатоядерних системах — стан E оптимізує запис до приватних даних
Хибне спільне використання — різні змінні в одному рядку кешу руйнують продуктивність; виправляється за допомогою alignas(64)
]]></description><link>lectures/l11-notes.-ієрархія-пам'яті-та-продуктивність.html</link><guid isPermaLink="false">Lectures/L11-NOTES. Ієрархія пам'яті та продуктивність.md</guid><pubDate>Mon, 20 Apr 2026 10:37:40 GMT</pubDate></item><item><title><![CDATA[L09. Проєктування ISA та моделі обчислень]]></title><description><![CDATA[
Лекція 9 курсу «Архітектура комп'ютерних систем» у НаУКМА.
Після цієї лекції ви зможете:
Описати абстрактні моделі машин RAM (машина з довільним доступом, РАМ-машина) та RASP (РАСП-машина) і пояснити, як вони співвідносяться з реальними комп'ютерними архітектурами (Гарвардська архітектура та архітектура фон Неймана)
Визначити, що таке ISA (архітектура системи команд), і перелічити ключові компоненти, які вона специфікує
Класифікувати ISA за розташуванням операндів (стекова, акумуляторна, регістр-пам'ять, регістр-регістр, пам'ять-пам'ять) та порівняти їхні компроміси
Порівняти філософії проєктування CISC та RISC, навівши реальні приклади кожної (x86, ARM, RISC-V)
Пояснити підхід VLIW (дуже довге командне слово) і те, як він переносить відповідальність за планування на компілятор
Класифікувати комп'ютерні архітектури за таксономією Флінна (SISD, MISD, SIMD, MIMD)
Розрізняти паралельні обчислювальні системи SMP та MPP
Протягом останніх восьми лекцій ми дедалі детальніше вибудовували картину того, як насправді працює комп'ютер. Починали з фізики — кремнію та електронів, які роблять можливою комутацію (L01). Спостерігали, як логічні вентилі збираються в суматори та АЛП (арифметико-логічні пристрої) (L02). Занурювалися в конкретну процесорну архітектуру — x86, вивчаючи її регістри, модель пам'яті та адресацію сегмент:зміщення (L03), а згодом — арифметичні й логічні інструкції та інструкції керування потоком виконання (L04). Ми організовували код за допомогою стека та процедур (L05), відступали на крок назад, щоб побачити повну системну шину, що з'єднує процесор, пам'ять і периферію (L06), дізнавалися, як програмні переривання дозволяють програмам взаємодіяти з операційною системою та BIOS (L07), і нарешті досліджували ієрархію пам'яті, порти введення-виведення та низькорівневі структури даних (L08).У кожній із тих лекцій ми працювали з конкретною машиною — Intel 8086 та її нащадками. Ми вивчили конкретні назви регістрів (AX, BX, SP), конкретні кодування інструкцій (MOV, ADD, INT), конкретні організації пам'яті (реальний режим, сегментні регістри, таблиця векторів переривань за адресою 0000:0000). Така конкретність була навмисною: ніщо не замінить досвіду написання програм, які виконуються на реальному апаратному забезпеченні (або на точному емуляторі на кшталт DOSBox).Але конкретність має свою ціну. Коли знаєш лише одну архітектуру, кожне проєктне рішення здається неминучим. Звісно, є акумулятор — так просто працюють процесори, хіба ні? Звісно, стек росте вниз, і PUSH зменшує SP. Звісно, інструкції можуть працювати безпосередньо з операндами в пам'яті. Усе це видається законами природи, а не тим, чим воно є насправді: проєктними рішеннями, ухваленими конкретними інженерами в конкретних компаніях, — іноді з глибоких технічних причин, іноді через історичну випадковість, а іноді тому, що альтернативу ще не було винайдено.Ця лекція відступає від конкретики, щоб поставити три великі питання:Питання 1: Яка найпростіша з можливих машин здатна обчислити все, що може обчислити реальний комп'ютер? Відповідь дають формальні моделі обчислень — математичні схеми ідеалізованих машин. Ви, напевно, чули про машину Тюрінга з курсу теорії обчислень або дискретної математики. Сьогодні ми вивчимо дві моделі, значно ближчі до реального апаратного забезпечення: машину з довільним доступом (РАМ-машину) та машину зі збереженою програмою з довільним доступом (РАСП-машину). Ці моделі знімають нашарування будь-якої конкретної ISA і розкривають суттєві компоненти, що роблять комп'ютер комп'ютером.Питання 2: Маючи ці суттєві компоненти, скільки принципово різних способів існує для проєктування системи команд? Побачивши абстрактні моделі, стає зрозуміло, що підхід x86 — лише одна точка у великому просторі проєктування. Ми класифікуємо архітектури системи команд за чотирма родинами — стекова, акумуляторна, регістр-пам'ять та регістр-регістр (load-store) — і побачимо, як кожна з них знаходить різні компроміси між щільністю коду, складністю апаратного забезпечення та продуктивністю. Це природно веде до великого розколу в проєктуванні процесорів: CISC проти RISC — філософського поділу, що сформував кожен процесор, яким ми користуємося сьогодні, від чіпа x86 у ноутбуці до ядра ARM у телефоні.Питання 3: Чи можна зробити комп'ютери швидшими не прискоренням одного процесора, а використанням багатьох процесорів одночасно? Послідовне обчислення — одна інструкція за іншою, без кінця — єдина парадигма, з якою ми мали справу досі. Але фізичні обмеження, про які йшлося в L01 (швидкість світла, теплова стіна, завершення масштабування Деннарда), гарантують, що продуктивність одного ядра не може зростати нескінченно. Паралельні обчислення — це відповідь, яку прийняла індустрія, а таксономія Флінна дає словник для класифікації кожної паралельної архітектури, коли-небудь створеної.<img alt="Структура Лекції 9 — три питання від абстрактних моделей через проєктування ISA до паралельних обчислень" src="assets/l09-lecture-outline-uk.svg" target="_self">Ці три питання формують дугу Лекції 9. Ми рухаємося від найабстрактнішого (математичні моделі) через архітектурну середину (проєктування ISA та CISC/RISC) до найперспективнішого (паралелізм і багатопроцесорні системи). Наприкінці ви матимете концептуальні інструменти, щоб поглянути на будь-який процесор — минулий, сучасний чи майбутній — і зрозуміти, де він розташований у просторі проєктування та чому його архітектори ухвалили саме такі рішення.Для такого моменту проведення цієї лекції є практична причина. У L03–L04 ми вивчали асемблер x86 як інструмент: ось регістр, ось інструкція — використовуй їх для розв'язання задач. Це був правильний підхід для формування навичок. Але тепер, коли ви достатньо вправні, щоб писати процедури, керувати стеком і маніпулювати структурами даних, ви готові до глибшого питання: чому x86 виглядає саме так? І ще важливіше: як виглядав би процесор, якби його розробники ухвалили інші рішення?Відповідь не є гіпотетичною. Процесори ARM (RISC-архітектура, проєктування регістр-регістр) зараз постачаються в мільярдах пристроїв на рік — зокрема чіпи Apple серії M, які витіснили Intel з лінійки Mac. Віртуальна машина Java та WebAssembly — це стекові архітектури, що працюють у кожному браузері. RISC-V, ISA з відкритим кодом, перетворює індустрію саме тому, що її розробники мали змогу почати з перших принципів, а не успадковувати десятиліття рішень, зумовлених зворотною сумісністю. І кожен сучасний процесор, незалежно від того, називає він себе CISC чи RISC, є багатоядерним чіпом — а отже, принципово паралельною машиною, навіть якщо більшість програмного забезпечення досі розглядає його як послідовну. Розуміння простору проєктування — це не академічна ностальгія, а карта, потрібна вам для навігації в ландшафті процесорів упродовж усієї кар'єри.Почнімо там, де починається кожна добра інженерна справа, — з найпростішої можливої абстракції: формальних моделей обчислень, які визначають, чим насправді є «обчислювальна машина».Коли інженери проєктують міст, вони не починають із заливання бетону. Вони починають із математичної моделі — сили, навантаження, властивості матеріалів, — що дає змогу аналізувати конструкцію ще до ухвалення будь-яких конкретних рішень. Інформатика працює так само. Перш ніж сперечатися, чи RISC «кращий» за CISC, чи стекова машина елегантніша за регістрову, потрібне точне математичне визначення того, чим насправді є «обчислювальна машина» і що означає для двох машин бути «рівнопотужними».Саме таку роль виконують моделі обчислень: формальні математичні описи ідеалізованих машин. Модель відкидає кожну деталь реалізації — тактову частоту, транзисторну технологію, розрядність шини, операційну систему — і зберігає лише логічну структуру обчислення. Якщо вдається довести, що одна модель обчислює той самий клас функцій, що й інша, то будь-яка різниця між реальними машинами, побудованими на цих моделях, є питанням інженерних компромісів, а не фундаментальних можливостей.<br>Найважливіший результат у цій галузі, який можна було зустріти в курсі дискретної математики чи алгоритмів, — це <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Church%E2%80%93Turing_thesis" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Church%E2%80%93Turing_thesis" target="_self">теза Черча–Тюрінга</a>: будь-яку функцію, «ефективно обчислювану» в рамках будь-якого розумного механічного процесу, можна обчислити машиною Тюрінга. Кожна модель, яку ми сьогодні розглядаємо, є Тюрінг-еквівалентною — вона обчислює точно той самий клас функцій, що й машина Тюрінга. Відмінності між моделями стосуються не того, що вони можуть обчислити, а того, наскільки зручно й ефективно вони виражають обчислення.Упродовж десятиліть інформатики розробили цілу родину абстрактних моделей машин, кожна з яких відображає різні аспекти реальних обчислень. Ці моделі з'явилися не всі одразу; вони виникли з діалогу між математиками та інженерами з 1930-х по 1970-ті роки. Алан Тюрінг запропонував свою машину в 1936 році, ще до появи електронних комп'ютерів. Лічильникові машини вивчали Мінський та Ламбек у 1961 році, саме тоді, коли перші транзисторні комп'ютери доводили практичну важливість збережених програм. Моделі РАМ та РАСП уточнювалися протягом 1960-х і початку 1970-х років зусиллями Шепердсона, Стургіса, Елгота, Робінсона, Кука й Рекхоу — саме тому, що інформатикам потрібні були моделі, які точно відображали б вартість обчислень на реальних машинах і годилися для аналізу алгоритмів.Основні послідовні моделі — тобто ті, що виконують одну інструкцію за раз, — можна розташувати від найпростішої до найреалістичнішої:Ван Емде Боас (1990) об'єднує машину Тюрінга, лічильникову машину, РАМ-машину, РАСП-машину та вказівникову машину в групу моделей «послідовних машин», відрізняючи їх від паралельних моделей на кшталт PRAM (паралельна машина з довільним доступом), з якою ми зустрінемося далі в цій лекції під час обговорення таксономії Флінна.Розгляньмо ключові моделі, просуваючись від простого до складного. Найбільше часу приділимо лічильниковій машині, РАМ-машині та РАСП-машині, оскільки саме вони найбезпосередніше пов'язані з розумінням проєктування ISA.<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Turing_machine" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Turing_machine" target="_self">Машина Тюрінга</a>, запропонована Аланом Тюрінгом у 1936 році, є фундаментальною моделлю обчислень. Вона складається з:
Нескінченної стрічки, поділеної на комірки, кожна з яких містить символ зі скінченного алфавіту.
Головки, яка може читати, записувати та переміщуватися на одну комірку вліво або вправо по стрічці.
Контролера зі скінченним числом станів (таблиці правил), який визначає, на основі поточного стану та символу під головкою, що записати, в якому напрямку рухатися та в який стан перейти далі.
<br><img alt="Схема машини Тюрінга — нескінченна стрічка, головка читання/запису та контролер зі скінченним числом станів" src="assets/l09-turing-machine-uk.svg" target="_self">Машина Тюрінга є надзвичайно потужною в теорії — вона може обчислити все, що є обчислюваним. Але для аналізу ефективності алгоритмів це жахлива модель. Проблема в тому, що доступ до n-ї комірки стрічки потребує n кроків руху головки. Немає «перейти до комірки 1000» — доводиться повзти туди по одній комірці. Реальні комп'ютери, навпаки, можуть звернутися до будь-якої комірки пам'яті за сталий час. Ця невідповідність означає, що програма сортування n чисел на машині Тюрінга працюватиме драматично повільніше, ніж той самий алгоритм на реальному комп'ютері, — не тому, що алгоритм гірший, а тому, що модель штрафує за довільний доступ.Висловімо це конкретніше. Якщо на стрічці машини Тюрінга, починаючи з комірки 0, зберігається масив із 1000 чисел і потрібно звернутися до елемента 500, головка має фізично проковзнути через 500 комірок. На реальному комп'ютері MOV AX, [array + 500*2] виконується за один цикл звернення до пам'яті незалежно від адреси. Час доступу O(n) машини Тюрінга завищує складність алгоритмів так, що це не відображає реальну продуктивність, і тому робить її непридатною для того аналізу складності, який важливий практикам-програмістам та розробникам алгоритмів.Це обмеження мотивувало розробку моделей, ближчих до реального апаратного забезпечення.<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Counter_machine" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Counter_machine" target="_self">Лічильникова машина</a> (досліджена Мінським, Ламбеком та іншими на початку 1960-х) застосовує радикально інший підхід, ніж машина Тюрінга. Замість стрічки вона має скінченний набір регістрів — дискретних місць зберігання, кожне з яких ідентифікується числом і здатне містити одне натуральне число довільного розміру. На відміну від стрічки, регістри забезпечують справжній довільний доступ: машина може прочитати чи змінити регістр 500 так само легко, як регістр 1, без жодного «руху головки». Набір інструкцій навмисно мінімальний:<br><img alt="Набір інструкцій лічильникової машини — INC, DEC, JZ, HALT" src="assets/l09-counter-machine-isa-uk.svg" target="_self">Це може здатися неймовірно слабким. Як можна обчислити щось корисне, маючи лише інкремент і декремент? Ключова ідея в тому, що будь-яку складнішу операцію можна симулювати, комбінуючи ці примітиви в циклах. Додавання — це цикл інкрементів. Множення — це цикл додавань (кожне з яких є циклом інкрементів). Порівняння — це паралельне зменшення двох регістрів, доки один не досягне нуля. Нудно, але працює. Мінський (1961) довів, що навіть ця абсурдно проста машина, маючи лише два регістри, є Тюрінг-еквівалентною — вона може обчислити все, що може машина Тюрінга.Мелзак (1961) запропонував чарівну візуалізацію: уявіть регістри як ямки, викопані в землі, а вміст кожного регістра — як купку камінців у ямці. INC додає камінець; DEC забирає один; JZ зазирає в ямку, щоб перевірити, чи вона порожня. Поряд лежить необмежений запас камінців. Якщо ямка має вмістити мільйон камінців, просто копайте глибше.Однак потужність лічильникової машини має жорстоку ціну в зручності. Як скопіювати значення з регістра 3 до регістра 5? Інструкції COPY не існує. Доводиться зменшувати регістр 3 по одному камінцю за раз, одночасно збільшуючи регістр 5 і тимчасовий регістр, а потім скопіювати тимчасовий назад у регістр 3, щоб відновити початкове значення. Ось як це виглядає в псевдокоді лічильникової машини з використанням регістра 0 як тимчасового:; Копіювання [r3] у r5 (знищення та відновлення r3 з використанням r0)
CLR r5 ; побудовано з: цикл DEC(r5)/JZ(r5,done)
CLR r0
loop: JZ r3, restore ; якщо r3 порожній, перейти до відновлення DEC r3 INC r5 INC r0 JZ rZ, loop ; безумовний перехід (rZ завжди містить 0)
restore: JZ r0, done DEC r0 INC r3 JZ rZ, restore
done: HALT
В асемблері x86 та сама операція — це одна інструкція: MOV [r5], [r3] (або, оскільки x86 не дозволяє переміщення з пам'яті в пам'ять, дві інструкції: MOV AX, [r3], потім MOV [r5], AX). Цей контраст ілюструє, чому існують багатші системи команд: вони стискають поширені шаблони в окремі операції, обмінюючи простоту моделі на зручність для програміста (та компілятора).Треба додати два числа? Побудуйте це з циклів INC та DEC. Машина Тюрінг-повна, але болісно непрактична.Що важливіше, лічильникова машина має фатальне структурне обмеження: кожна адреса регістра має бути жорстко закодована в програмі. Інструкція INC(3) називає регістр 3 явно. Немає способу сказати «збільшити регістр, адреса якого зберігається в регістрі 7», — тобто немає непрямої адресації. Це означає, що програма лічильникової машини з k інструкцій може працювати лише з конкретними регістрами, названими в цих інструкціях. Вона не здатна досліджувати необмежену кількість регістрів на основі обчислених адрес.Чому це важливо? Розгляньмо просту задачу: обхід зв'язаного списку. Кожен вузол зберігається в якомусь регістрі, а «вказівник на наступний» — це адреса наступного вузла, збережена як число всередині поточного вузла. Щоб пройти за вказівником, треба прочитати значення з одного регістра й використати його як адресу наступного регістра для доступу. Лічильникова машина просто не може цього зробити — у неї немає механізму, щоб сказати «перейти до регістра, номер якого я щойно обчислив». Можна спробувати обійти це, вичерпно перевіряючи кожну можливу адресу («Чи дорівнює вказівник 0? Ні. Дорівнює 1? Ні. Дорівнює 2? …»), але ця техніка «обмеженої непрямості» працює лише до певної фіксованої межі та потребує окремої інструкції для кожної можливої адреси. Вона не масштабується для необмежених даних.Кук і Рекхоу (1973) формулюють проблему лаконічно: «Непрямі інструкції необхідні для того, щоб фіксована програма могла звертатися до необмеженої кількості регістрів зі зміною вхідних даних».Саме цю прогалину заповнює модель РАМ-машини.Машина з довільним доступом (РАМ-машина), формалізована в роботах Шепердсона і Стургіса (1963), Елгота і Робінсона (1964) та Кука і Рекхоу (1973), бере лічильникову машину й додає два ключові покращення: Непряма адресація: інструкція може вказати регістр, вміст якого слугує адресою фактичного цільового регістра. Якщо регістр 7 містить значення 42, то непряма операція через регістр 7 звернеться до регістра 42 — де б він не був у необмеженому просторі регістрів. Це механізм вказівників, що дозволяє програмі фіксованого розміру звертатися до необмеженої кількості регістрів. Ця концепція вам уже знайома з x86: MOV AX, [BX] використовує вміст BX як адресу пам'яті, — і це є непрямою адресацією. Модель РАМ-машини формалізує цю ідею. Акумулятор: виділений регістр (зазвичай позначається A або AC), що слугує неявним джерелом або приймачем для більшості операцій, зменшуючи кількість операндів, які має вказувати кожна інструкція. Замість ADD r3, r5, r7 (триадресна) акумуляторна модель використовує LOAD r3, потім ADD r5, потім STORE r7 — кожна інструкція називає лише один регістр, акумулятор мається на увазі. <br><img alt="Пряма та непряма адресація — LOAD 7 читає r7 напряму, LOAD *7 слідує за значенням r7 як вказівником" src="assets/l09-direct-vs-indirect-addressing-uk.svg" target="_self">Результат — машина, що виглядає напрочуд подібно до простого реального процесора. Вона має необмежений набір пронумерованих регістрів (аналог основної пам'яті), лічильник команд, акумулятор та систему команд, яка включає арифметику, переміщення даних із прямою та непрямою адресацією, умовні та безумовні переходи, а також операції введення-виведення через виділені «стрічки» вхідних і вихідних даних.Звернімо увагу, що концепція акумулятора має здатися знайомою. У L03 ми дізналися, що AX — це регістр-«акумулятор» x86, жорстко прив'язаний приймач для MUL, DIV та деяких інструкцій введення-виведення. Це не збіг. Акумулятор x86 є прямим нащадком акумулятора у формальних моделях обчислень. Модель РАМ-машини робить цей зв'язок явним: акумулятор — це регістр, через який проходить уся арифметика, що зводить кожну інструкцію до одного явного операнда (інший операнд неявно є «акумулятором»). Ґолдстайн і фон Нейман описали його в 1946 році як «арифметичний орган, що буквально накопичує (accumulates) своє число протягом послідовності арифметичних операцій», — і кожна програма для x86, яку ви писали, робила саме це, коли вибудовувала ланцюжок операцій через AX.Є одна критична деталь, що відрізняє РАМ-машину від реального комп'ютера: програма знаходиться в контролері зі скінченним числом станів, а не в регістрах. Таблиця інструкцій відокремлена від пам'яті даних. Якщо це нагадує щось із L06, так і має бути — це саме Гарвардська архітектура, де пам'ять інструкцій і пам'ять даних фізично розділені.Модель РАМ-машини достатньо потужна, щоб обчислити всі часткові рекурсивні функції (повний клас обчислюваних функцій), а її модель пам'яті з довільним доступом означає, що час виконання алгоритмів на РАМ-машині значно точніше відповідає часу виконання на реальному апаратному забезпеченні, ніж на машині Тюрінга. Ось чому модель РАМ-машини є стандартним підґрунтям для аналізу складності алгоритмів: коли курс алгоритмів стверджує, що сортування злиттям працює за O(n log n), він неявно припускає РАМ-подібну модель, у якій будь-яке звернення до пам'яті займає сталий час. У моделі машини Тюрінга той самий алгоритм мав би вищу складність, бо кожне звернення до пам'яті коштувало б O(n) переміщень стрічки.В аналізі складності на моделі РАМ-машини є важлива тонкість — модель вартості. За рівномірної моделі вартості кожна операція (зокрема й операції над довільно великими числами) коштує одну одиницю часу. За логарифмічної моделі вартості вартість операції пропорційна кількості бітів в операндах. Для реалістичного аналізу алгоритмів рівномірна модель вартості зазвичай прийнятна, поки числа вміщуються в машинне слово, — а саме це припущення робиться під час написання практичного коду. Але для теоретичних результатів із дуже великими числами доречнішою є логарифмічна модель.Ми детально розглянемо архітектуру та систему команд РАМ-машини в наступному розділі.Машина зі збереженою програмою з довільним доступом (РАСП-машина) робить останній крок до реалістичного комп'ютера. Вона ідентична РАМ-машині в усьому, окрім одного: програма зберігається в регістрах поряд із даними. Контролер зі скінченним числом станів усе ще існує, але його єдине завдання тепер — діяти як інтерпретатор: вибирати інструкції з регістрів, декодувати їх та виконувати. Фактична логіка програми перебуває в тому ж адресному просторі, що й вхідні дані та результати.Це архітектура фон Неймана: єдина, уніфікована пам'ять, що зберігає і інструкції, і дані, з процесором, який послідовно вибирає, декодує та виконує інструкції. РАСП-машина відноситься до РАМ-машини так само, як універсальна машина Тюрінга — до звичайної машини Тюрінга: це програмована версія, у якій можна змінити програму без перебудови апаратного забезпечення.<br><img alt="РАМ проти РАСП — окремі програма та дані (Гарвардська) проти єдиної пам'яті (фон Нейман)" src="assets/l09-ram-vs-rasp-uk.svg" target="_self">Погляньмо, що означає «збережена програма» в конкретних термінах, які вже знайомі. У L07 ми дізналися, що INT 21h працює тому, що код обробника переривань операційної системи знаходиться в пам'яті поряд із даними програми. У L05 ми побачили, що CALL заштовхує адресу повернення на стек — стек (пам'ять даних) зберігає значення, яке процесор пізніше використає як адресу інструкції. Обидва випадки є наслідками принципу збереженої програми: інструкції та дані поділяють один адресний простір і можуть посилатися одне на одне.Розрізнення між РАМ-машиною (Гарвардська архітектура) та РАСП-машиною (архітектура фон Неймана) є не лише академічним. Воно безпосередньо відповідає реальному та тривалому архітектурному розподілу: Гарвардська архітектура (окремі шляхи для інструкцій і даних) зустрічається в багатьох мікроконтролерах і DSP-чіпах, а також у розділенні кешу L1 сучасних процесорів x86 (окремі кеші L1-I та L1-D, попри те що основна пам'ять уніфікована). Архітектура фон Неймана (уніфікована пам'ять для інструкцій і даних) є домінантною моделлю для комп'ютерів загального призначення. Вона уможливлює самомодифікований код, генерацію коду під час виконання (JIT-компілятори) та можливість розглядати програми як дані — основу інтерпретаторів, відлагоджувачів і завантажувачів операційних систем. Кожен ПК на x86, яким ми користувалися, дотримується цієї моделі: програма .COM у L07 перебувала в тому ж просторі пам'яті, що й дані, з якими вона працювала, а механізм CALL/RET покладався на стек (пам'ять даних), що зберігає адреси інструкцій. <br>Ціна, яку РАСП-машина платить за свою гнучкість, — це <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Von_Neumann_architecture#Von_Neumann_bottleneck" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Von_Neumann_architecture#Von_Neumann_bottleneck" target="_self">вузьке місце фон Неймана</a>: інструкції та дані конкурують за одну шину пам'яті, створюючи потенційне обмеження пропускної здатності, якого уникають конструкції Гарвардського типу. Джон Бекус увів цей термін у своїй Тюрінгівській лекції 1977 року, стверджуючи, що одношинне вузьке місце між процесором і пам'яттю фундаментально обмежує швидкість виконання програм — проблема, що стає лише гострішою, оскільки швидкість процесорів випереджає швидкість пам'яті (розрив, який ми бачили в кількісному вигляді в обговоренні ієрархії пам'яті в L08).Сучасні процесори справляються з цим вузьким місцем прагматично: вони використовують модифіковану Гарвардську архітектуру на рівні кешу (окремі кеш інструкцій L1 і кеш даних L1, як обговорювалося в L08), водночас надаючи програмісту уніфікований адресний простір фон Неймана. Це дає найкраще з обох світів — паралельну вибірку інструкцій і даних на швидкості Гарвардської архітектури разом із гнучкістю архітектури фон Неймана на рівні програмної моделі.Концепція збереженої програми має глибоке значення, яке легко не помітити: вона означає, що програми є даними. РАСП-машина може читати власні інструкції, копіювати їх, модифікувати та навіть генерувати нові інструкції під час виконання. Саме це робить можливими операційні системи: завантажувач зчитує програму з диска (дані) і записує її в пам'ять, звідки процесор вибиратиме її як інструкції. Саме це робить можливими інтерпретатори та компілятори: інтерпретатор Python — це програма (інструкції), яка зчитує ваш файл .py (дані) і виконує його. І саме це зробило можливими ранні техніки програмування на кшталт самомодифікованого коду, коли програма переписувала власні інструкції, щоб реалізувати цикли або індексовану адресацію на апаратному забезпеченні, у якого цих можливостей не було від початку.Ми детально розглянемо архітектуру РАСП-машини далі в цій лекції.Навіщо витрачати час на ці абстрактні моделі в курсі, який в усьому іншому є глибоко практичним? Тому що моделі розкривають, які характеристики комп'ютера є суттєвими (регістри, адресація, лічильник команд), а які — проєктними рішеннями (скільки регістрів мати, які інструкції включити, використовувати стек чи акумулятор, скільки операндів на інструкцію). Моделі РАМ-машини та РАСП-машини забезпечують мінімальну базову лінію — «скелет» комп'ютера. Усе інше, що ми вивчили про x86, — це «м'язи та шкіра», додані згори. Чітке бачення скелета допомагає зрозуміти, чому в наступних розділах різні родини ISA додають різні м'язи в різних місцях.Ось як усі частини складаються докупи:<br><img alt="Прогресія моделей обчислень — від машини Тюрінга до реального комп'ютера" src="assets/l09-models-of-computation-progression-uk.svg" target="_self">Прогресія моделей обчислень: кожен крок додає можливість, яка наближує абстрактну машину до реального комп'ютера.Кожен крок у цій прогресії додає можливість, яка наближає абстрактну модель до машини на вашому столі. Машина Тюрінга дає нам теоретичне підґрунтя та концепцію універсального обчислення. Лічильникова машина вводить регістри — дискретні, іменовані місця зберігання з довільним доступом, — замінюючи болісний послідовний рух стрічки. РАМ-машина додає непряму адресацію та акумулятор, даючи програмам змогу слідувати за вказівниками й працювати з необмеженими структурами даних. РАСП-машина поміщає програму в пам'ять поряд із даними, уможливлюючи цикл вибірка-декодування-виконання, знайомий нам із L06, і роблячи можливими універсальні, перепрограмовані комп'ютери. На момент, коли ми досягаємо РАСП-машини, ми по суті описали логічну структуру кожного комп'ютера загального призначення від EDVAC (1949).І саме в цьому суть. Щойно ми зрозуміємо, що реальний комп'ютер — це «просто» РАСП-машина з багатшою системою команд, швидшою пам'яттю та апаратними оптимізаціями, можна поставити наступне природне питання: якою має бути ця багатша система команд?РАСП-машина каже нам, що потрібні інструкції, регістри, режими адресації та послідовний цикл вибірка-декодування-виконання. Але вона нічого не каже про те, які саме інструкції включати, скільки регістрів надавати, як мають бути вказані операнди, мають інструкції бути фіксованої чи змінної довжини. Усе це проєктні рішення — і різні відповіді на ці питання породжують радикально різні архітектури систем команд.Це питання — питання проєктування ISA — є темою решти цієї лекції.У попередніх розділах ми побачили, як машина Тюрінга забезпечує теоретичне підґрунтя для обчислень, однак працює зі стрічкою — послідовним носієм даних, який мало схожий на те, як реальні комп'ютери звертаються до даних. У цьому розділі ми познайомимося з машиною з довільним доступом (РАМ-машиною) — абстрактною моделлю обчислень, що поєднує чисту теорію з практичною комп'ютерною архітектурою. РАМ-модель є основним інструментом аналізу алгоритмів: коли в курсі алгоритмів досліджується часова складність сортування чи обходу графів, фактично використовується модель вартості саме РАМ-машини.<br>Машина з довільним доступом — це абстрактна обчислювальна модель із загального класу <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Register_machine" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Register_machine" target="_self">регістрових машин</a>. На відміну від машини Тюрінга, яка мусить пересувати головку по одній комірці, щоб дістатися до віддалених даних, РАМ-машина може миттєво звернутися до будь-якого регістра — звідси й «довільний доступ», як у сучасних мікросхемах оперативної пам'яті. Модель було формалізовано у працях Шепердсона і Стерджіса (1963), Мельзака (1961), Кука і Рекхау (1973) та інших; вона й досі залишається стандартною моделлю для аналізу обчислювальної складності.Ключова ідея проста: замість нескінченної стрічки РАМ-машина має необмежену сукупність регістрів (пронумерованих комірок пам'яті), кожен з яких здатний зберігати натуральне число довільного розміру. Фіксована програма — яка зберігається окремо від даних — оперує цими регістрами за допомогою невеликого набору інструкцій.РАМ-машина складається з п'яти компонентів:1. Регістри пам'яті (r1, r2, r3, ...)Необмежена послідовність регістрів, кожен з яких ідентифікується натуральним числом-адресою. Кожен регістр може зберігати одне натуральне число довільного розміру (зокрема й нуль). Регістр r1 традиційно називають акумулятором — це спеціальний регістр, у якому накопичуються результати арифметичних операцій. Решта регістрів (r2, r3, ...) слугують сховищем загального призначення. Верхньої межі ні на кількість регістрів, ні на розмір чисел, які вони можуть зберігати, немає — саме це й робить модель «абстрактною».2. Програма (таблиця інструкцій)Скінченна фіксована послідовність інструкцій, дібраних із невеликого набору команд. Принципово важливо, що програма не зберігається в регістрах: вона існує в окремій таблиці інструкцій, доступній лише для читання. Програма не може модифікувати саму себе, а дані в регістрах не можуть змінювати програму. Саме це розділення є визначальною характеристикою РАМ-моделі.3. Лічильник командСпеціальний лічильник, який вказує, яка інструкція програми виконується в цей момент. Після кожної інструкції лічильник команд переходить до наступної — якщо тільки поточна інструкція не є переходом; у такому разі лічильник встановлюється на цільову адресу переходу.4. Вхідна стрічка (лише для читання)Односпрямована вхідна стрічка, з якої машина може послідовно зчитувати значення. Кожна операція READ зчитує наступне значення зі стрічки та поміщає його в акумулятор. Після зчитування головка стрічки рухається вперед і не може повернутися назад. Це зручно уявляти як чергу вхідних даних.5. Вихідна стрічка (лише для запису)Односпрямована вихідна стрічка, на яку машина може записувати значення. Кожна операція WRITE дописує поточне значення акумулятора в кінець вихідної стрічки. Машина не може ні зчитати те, що було записано раніше, ні змінити попередньо виведені значення.<br><img alt="Архітектура РАМ-машини" src="assets/l09-ram-machine-architecture.png" target="_self">Архітектура РАМ-машини: вхідна стрічка подає дані, програма (таблиця інструкцій) керує виконанням через лічильник команд, регістри r1 (акумулятор), r2, r3, ... зберігають дані, а результати записуються на вихідну стрічку.Набір інструкцій РАМ-машини нагадує спрощену мову асемблера. Різні автори визначають дещо різні набори, однак акумуляторна модель Кука і Рекхау (1973) є найпоширенішою, оскільки найточніше відображає роботу реальних процесорів. Ось типовий набір інструкцій:Варто зазначити, що кожна арифметична операція використовує акумулятор і як джерело, і як приймач — так само, як регістр EAX зазвичай використовується в асемблері x86 для операцій на кшталт MUL і DIV, розглянутих у Лекції 04.РАМ-модель підтримує два режими адресації, знайомі з досвіду роботи з асемблером x86:Пряма адресація (LOAD 5): операнд безпосередньо задає номер регістра. LOAD 5 означає «завантажити вміст регістра r5 в акумулятор». Це аналог mov eax, [var] у x86.Непряма адресація (LOAD *5): операнд задає регістр, вміст якого використовується як адреса цільового регістра. LOAD *5 означає: «взяти значення в регістрі r5; використати це значення як адресу; завантажити вміст регістра за цією адресою в акумулятор». Якщо r5 містить значення 12, то LOAD *5 завантажує вміст r12. Це аналог розіменування вказівника — як mov eax, [ebx], де EBX містить адресу.Саме непряма адресація підносить РАМ-машину над простішою моделлю лічильникової машини. Без неї фіксована програма могла б звертатися лише до скінченної кількості регістрів — тих, що явно згадані в інструкціях. З непрямою адресацією програма здатна обчислити адресу під час виконання та звернутися до будь-якого регістра — навіть того, що далеко виходить за межі явно згаданих у коді. Як зазначили Кук і Рекхау (1973):
«Непрямі інструкції необхідні для того, щоб фіксована програма могла звертатися до необмеженої кількості регістрів у міру зміни вхідних даних.»
Для наочності розгляньмо програму для РАМ-машини, яка зчитує два числа з вхідної стрічки, додає їх і записує результат на вихідну стрічку: READ ; Зчитати перше число в акумулятор STORE 2 ; Зберегти його в r2 READ ; Зчитати друге число в акумулятор ADD 2 ; Додати вміст r2 до акумулятора WRITE ; Записати суму на вихідну стрічку HALT ; Зупинити
Якщо вхідна стрічка містить [7, 5], виконання відбувається так:Це значно ближче до того, як ми насправді мислимо про програми, ніж машина Тюрінга, що совається вздовж стрічки.Основне призначення РАМ-моделі — слугувати підґрунтям для аналізу складності алгоритмів. Коли ми кажемо, що алгоритм сортування працює за O(n log n), ми неявно спираємося на модель вартості РАМ-машини. Зазвичай використовують дві моделі вартості: Модель рівномірної вартості: кожна інструкція (зокрема й арифметичні операції над числами довільного розміру) коштує одну одиницю часу, а кожен регістр — одну одиницю простору. Саме цю модель використовують у більшості вступних курсів з алгоритмів. Вона проста і зазвичай достатня, доки задіяні числа залишаються поліноміальними відносно розміру вхідних даних. Логарифмічна модель вартості: вартість інструкції залежить від розміру (кількості бітів) задіяних операндів. Це реалістичніше для ситуацій, коли числа стають дуже великими, адже множення двох 1000-значних чисел явно потребує більших зусиль, ніж множення двох одноцифрових. За цією моделлю LOAD числа з k бітами коштує O(k), ADD двох k-бітових чисел коштує O(k), а MULT — O(k²) або краще, залежно від алгоритму множення. Вибір моделі вартості має значення. За моделлю рівномірної вартості можна «схитрувати», закодувавши експоненційний обсяг інформації в одному регістрі та виконавши над ним одну арифметичну операцію. Логарифмічна модель вартості унеможливлює такі трюки, стягуючи плату за фактичну роботу з маніпулювання великими числами. Для більшості алгоритмів, з якими ми маємо справу, обидві моделі дають однакові результати, однак для теорії складності на дослідницькому рівні ця відмінність є суттєвою.Ось ключове архітектурне спостереження: у РАМ-моделі програма зберігається окремо від даних. Таблиця інструкцій розміщена у власному просторі, доступному лише для читання; регістри містять тільки дані. Програма не може ні читати, ні записувати, ні модифікувати саму себе — вона зафіксована ще до початку виконання.Таке розділення програми та даних і є тим, що визначає Гарвардську архітектуру, з якою ми познайомилися в Лекції 06. Пригадаймо: у Гарвардській архітектурі інструкції та дані розміщені в окремих просторах пам'яті з окремими шинами. РАМ-модель є теоретичним втіленням цього принципу. Програма «зашита» в машину подібно до мікропрограмного забезпечення у спеціалізованому апаратному контролері.Такий дизайн має прямий наслідок: РАМ-машина не може записати нову програму у свої регістри й потім виконати її. Вона не може реалізувати підпрограми, зберігаючи адреси повернення разом з інструкціями. Вона не може модифікувати власний код. Для всіх цих можливостей потрібна інша модель — та, де програма розміщена в тій самій пам'яті, що й дані. Така модель — РАСП-машина, до якої ми переходимо далі.РАСП-машина (Random Access Stored-Program Machine — машина зі збереженою програмою з довільним доступом) бере РАМ-модель і вносить у неї одну фундаментальну зміну: програма більше не зберігається в окремій таблиці інструкцій. Натомість інструкції програми закодовані як числа й зберігаються в тих самих регістрах, що й дані. РАСП-машина зчитує свої інструкції з пам'яті, декодує їх, виконує, а потім вибирає наступну — саме так працює справжній процесор.РАСП-машину зручно уявляти так:
РАСП-машина — це універсальна машина Тюрінга, побудована на шасі РАМ-машини.
Подібно до того, як універсальна машина Тюрінга — це машина Тюрінга, здатна імітувати будь-яку іншу машину Тюрінга, зчитуючи програму зі своєї стрічки, РАСП-машина — це РАМ-машина, здатна виконати будь-яку програму, зчитуючи інструкції зі своїх регістрів. «Збережена програма» в назві — це і є ключова ідея, та сама концепція збереженої програми, яку Джон фон Нейман обґрунтував у 1945 році і яка лежить в основі кожного сучасного комп'ютера загального призначення.РАСП-машина зберігає компоненти РАМ-машини — регістри, вхідну стрічку, вихідну стрічку та блок керування зі скінченним числом станів, — але реорганізовує їх:Уніфікована пам'ять (R0, R1, R2, ..., Rn)Єдиний набір регістрів містить одночасно інструкції програми та дані. Перша частина регістрів (скажімо, від R0 до Rk) може містити закодовані інструкції, тоді як регістри після неї зберігають значення даних. Проте жорсткої межі між ними немає — інструкції та дані спільно використовують один адресний простір.Блок керування зі скінченним числом станівНевеликий фіксований інтерпретатор, що реалізує цикл вибірка-декодування-виконання. Цей блок керування — не сама програма; це апаратне забезпечення (або, точніше, його абстрактний еквівалент), яке вміє:
Вибирати (fetch) наступну інструкцію з регістра, на який вказує лічильник команд.
Декодувати (decode) числову інструкцію (наприклад, «3» означає JZ, «2» означає DEC).
Виконувати (execute) декодовану операцію над відповідними регістрами.
Просувати (advance) лічильник команд до наступної інструкції (або до цільової адреси переходу).
Вхідна та вихідна стрічкиЦі стрічки такі самі, як у РАМ-моделі: вхідна стрічка лише для читання й вихідна стрічка лише для запису.<br><img alt="Архітектура РАСП-машини" src="assets/l09-rasp-machine-architecture.png" target="_self">Архітектура РАСП-машини: уніфікована пам'ять містить і інструкції програми (P0, P1, …), і дані (R0, R1, …) в одному просторі. Блок керування зі скінченним числом станів вибирає, декодує та виконує інструкції, зчитуючи дані з вхідної стрічки й записуючи результати на вихідну стрічку.Цикл виконання РАСП-машини одразу видасться знайомим — це цикл вибірка-декодування-виконання, який ми розглядали в Лекції 06. Розгляньмо конкретний приклад. Припустімо, регістри з 5 по 15 містять таку закодовану програму, а лічильник команд спочатку встановлено на 5: ; Адреса Код операції Операнд(и) ; ------ ------------ ---------- 5: JZ 18, 15 ; Якщо регістр 18 дорівнює нулю, перейти на адресу 15 8: DEC 18 ; Зменшити регістр 18 10: INC 19 ; Збільшити регістр 19 12: JZ 15, 5 ; Безумовний перехід на адресу 5 (регістр 15 завжди містить 0) 15: HALT ; Зупинити виконання 18: n ; Початкове значення (дані) 19: 0 ; Приймач (дані, початково 0)
Ця програма копіює значення n із регістра 18 до регістра 19, послідовно зменшуючи одне й збільшуючи інше. Окремо варто відзначити прийом, яким реалізовано безумовний перехід: оскільки в нас є лише JZ (перехід, якщо нуль), ми переходимо за регістром 15, про який відомо, що він містить нуль (код операції HALT, який випадково дорівнює 0). Це класичний прийом у мінімалістичних наборах інструкцій — побудова складної поведінки з примітивних будівельних блоків.Звернімо увагу, що інструкції та дані посідають один і той самий простір регістрів: регістр 18 — це водночас місце зберігання даних, якими оперує програма, і комірка пам'яті в тому ж адресному просторі, що й інструкції програми. Якби програма записала значення в регістр 10, вона перезаписала б одну зі своїх власних інструкцій — це і є самомодифікація в дії.Блок керування зі скінченним числом станів виконує це так:
Вибірка: зчитати вміст регістра 5 (поточне значення лічильника команд). Він містить «JZ» (закодований, скажімо, як код операції 3).
Декодування: блок керування розпізнає код операції 3 як «Перехід, якщо нуль», що вимагає двох параметрів. Він зчитує регістри 6 і 7, щоб отримати операнди: номер регістра 18 та цільову адресу переходу 15.
Виконання: перевірити, чи регістр 18 містить нуль. Якщо ні — просунути лічильник команд за межі цієї інструкції (на адресу 8). Якщо так — встановити лічильник команд на 15.
Повторення: вибрати наступну інструкцію за новим значенням лічильника команд.
Це принципово відрізняється від РАМ-моделі. У РАМ-машині блок керування зчитує інструкції з окремої, зашитої таблиці інструкцій. У РАСП-машині — із тих самих регістрів, де зберігаються дані.Тонкість, яка часто спричиняє плутанину: модель РАСП-машини насправді містить два набори інструкцій. Перший — це фіксована таблиця інструкцій блоку керування зі скінченним числом станів, тобто «інтерпретатор», зашитий у машину. Другий — це програма, записана в регістрах, тобто ті інструкції, які інтерпретуються. Ці два набори інструкцій не обов'язково мають збігатися. Вбудовані інструкції блоку керування можуть включати лише примітивні операції на кшталт «непрямого копіювання» та «умовного переходу», тоді як програма в регістрах може використовувати багатший набір інструкцій вищого рівня, який інтерпретатор декодує та імітує.Ця дворівнева структура нагадує те, як працює справжній комп'ютер: мікрокод процесора (зашита логіка керування) інтерпретує машинні інструкції (програму в оперативній пам'яті). Подібно влаштований і емулятор: програма, яка виконується на одній архітектурі, інтерпретує інструкції, написані для зовсім іншої архітектури.Модель РАСП-машини втілює архітектуру фон Неймана — принцип проєктування, за яким інструкції програми та дані спільно використовують єдиний простір пам'яті та єдину шину. Саме таку архітектуру має практично кожен комп'ютер загального призначення, виготовлений з 1950-х років, — від першого EDVAC до ноутбука чи телефону, якими ви користуєтесь просто зараз.Ідея фон Неймана була революційною: якщо інструкції — це просто числа, збережені в пам'яті, то для запуску іншої програми комп'ютер не потрібно фізично перемикати. Достатньо завантажити нову послідовність чисел у пам'ять і перезапустити виконання. Саме це й робить комп'ютер загального призначення, а не спеціалізованим обчислювальним пристроєм.Розміщення програми в тій самій пам'яті, що й дані, має глибокі наслідки.Підпрограми стають можливими. Виклик підпрограми може зберегти адресу повернення в регістрі та передати керування на початкову адресу підпрограми. Коли підпрограма завершується, вона зчитує збережену адресу повернення й повертає керування туди, звідки її викликали. У РАМ-моделі, де програма фіксована й відокремлена від даних, цей механізм недоступний: неможливо «запам'ятати», куди повертатися, записуючи в таблицю інструкцій.Самомодифікований код стає можливим. Оскільки інструкції — це просто числа в регістрах, програма РАСП-машини може переписувати власні інструкції під час виконання. Наприклад, вона може змінювати операнд інструкції LOAD, щоб на кожній ітерації звертатися до іншого регістра, — фактично реалізуючи обчислювану адресацію. Хоча в сучасній інженерії програмного забезпечення самомодифікований код вважається поганою практикою, на зорі комп'ютерної ери він був поширеним і важливим прийомом, коли пам'яті було обмаль і апаратна непряма адресація ще була недоступна.Програму можна обробляти як дані. РАСП-машина може зчитати програму зі своєї вхідної стрічки, зберегти її в регістрах і потім виконати. Це означає, що одна РАСП-машина з фіксованим інтерпретатором у блоці керування здатна виконати будь-яку програму — а отже, вона є універсальним комп'ютером. Саме це й робить завантажувач програм операційної системи: він зчитує виконуваний файл із диска в пам'ять і передає йому керування.Виникають питання безпеки. Якщо програма та дані спільно використовують один простір, зловмисні або помилкові вхідні дані потенційно можуть перезаписати інструкції програми. Саме тут лежить теоретичне коріння атак переповнення буфера — цю тему можна глибше дослідити в подальших курсах з комп'ютерної безпеки.Співвідношення між РАСП-машиною та РАМ-машиною віддзеркалює співвідношення між універсальною машиною Тюрінга та звичайною машиною Тюрінга:
Машина Тюрінга має фіксовану програму (свою таблицю переходів станів) і обробляє дані на стрічці. Вона обчислює одну конкретну функцію.
Універсальна машина Тюрінга зчитує опис іншої машини Тюрінга зі стрічки та імітує її. Вона може обчислити будь-яку обчислювану функцію.
Аналогічно:
РАМ-машина має фіксовану програму (свою таблицю інструкцій) і обробляє дані в регістрах. Вона обчислює одну конкретну функцію.
РАСП-машина зчитує програму зі своїх регістрів і виконує її. З відповідним інтерпретатором вона може обчислити будь-яку обчислювану функцію.
Отже, РАСП-машина — це «універсальний абстрактний комп'ютер», теоретична модель, яка найбільш безпосередньо відповідає тому, що ми в повсякденному мовленні називаємо «комп'ютером».Наступна таблиця узагальнює ключові відмінності між двома моделями:Може виникнути закономірне питання: якщо РАСП-машина потужніша й реалістичніша, навіщо нам узагалі РАМ-модель?Річ у тім, що РАМ-машина простіша для міркувань. Аналізуючи часову складність алгоритму, не хочеться відволікатися на кодування інструкцій, цикли вибірки чи накладні витрати на інтерпретацію збереженої програми. РАМ-модель дозволяє безпосередньо підраховувати кількість операцій (додавань, порівнянь, звернень до пам'яті) без додаткового рівня непрямості, який вносить механізм збереженої програми РАСП-машини.На практиці теоретики складності використовують РАМ-модель для аналізу алгоритмів, а РАСП-модель — тоді, коли потрібно міркувати про те, що означає для машини бути програмованою чи універсальною. Обидві моделі обчислювально еквівалентні — все, що може обчислити одна, може обчислити й інша, — але вони слугують різним концептуальним цілям.РАМ-модель також забезпечує теоретичне підґрунтя для гарвардської архітектури, яку застосовують у багатьох вбудованих системах і мікроконтролерах. Програмуючи Arduino, ви працюєте з процесором (AVR), що використовує модифіковану гарвардську архітектуру: код програми розміщено у Flash-пам'яті, а дані — у SRAM, саме так, як і передбачає РАМ-модель. А от РАСП-модель описує процесори x86, які ви програмували протягом цього курсу, де ваші сегменти .code та .data зрештою розміщені в одному адресному просторі.Розуміння обох моделей дає теоретичний словник, щоб пояснити не лише як працюють комп'ютерні архітектури, а й чому їх спроєктовано саме так. Відмінність гарвардської архітектури від архітектури фон Неймана — це не просто історична цікавинка, а фундаментальне проєктне рішення, що впливає на все: від побудови шин пам'яті до моделей безпеки, — і його витоки безпосередньо сягають абстрактних РАМ- і РАСП-машин.У попередніх розділах ми розглянули абстрактні моделі обчислень — РАМ- та РАСП-машини, — які описують, що комп'ютер теоретично може обчислити. Побачили також, як архітектура фон Неймана задає фізичний план побудови реальних машин. Проте між абстрактною математичною моделлю та конкретною реалізацією в кремнії існує ще один надзвичайно важливий рівень, який пов'язує все воєдино: архітектура системи команд, або ISA (Instruction Set Architecture).Архітектура системи команд — це абстрактна модель, яка визначає програмний інтерфейс процесора. Це формальний контракт між програмним та апаратним забезпеченням: програмне забезпечення зобов'язується виражати свої наміри лише через операції, регістри та формати даних, визначені ISA, а апаратне забезпечення зобов'язується коректно виконувати ці операції незалежно від того, як саме організовані внутрішні схеми.Уявімо ISA як розділову лінію. Усе, що над лінією — компілятори, операційні системи, прикладні програми — бачить лише ISA. Усе, що під лінією — топологія транзисторів, стадії конвеєра, ієрархія кешів, блоки передбачення переходів — для програмного забезпечення невидиме. Саме такий розподіл дозволяє процесорам Intel Core i9 та бюджетному Celeron виконувати одну й ту саму програму для x86: обидва реалізують одну й ту саму ISA, але всередині мають кардинально різні мікроархітектури.<br><img alt="ISA як контракт — програмне забезпечення зверху, апаратне забезпечення знизу, ISA — розділова лінія" src="assets/l09-isa-boundary-uk.svg" target="_self">Точніше кажучи, ISA визначає, як програма керує процесором. Коли ми пишемо MOV AX, 5 або ADD BX, CX, то користуємося словником і граматикою ISA. Завдання процесора — точно інтерпретувати та виконувати ці інструкції. Але ISA не диктує, як саме процесор має робити це всередині — це вже царина мікроархітектури, тобто конкретного інженерного рішення (глибина конвеєра, виконавчі блоки, структура кешу), за допомогою якого реалізовано ISA.<br>Це розмежування вперше чітко сформулював Фред Брукс в IBM під час проєктування <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/IBM_System/360" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/IBM_System/360" target="_self">System/360</a> на початку 1960-х років. До System/360 кожна нова машина IBM мала власний набір інструкцій, а отже, програмне забезпечення, написане для однієї моделі, не могло працювати на іншій. Брукс запропонував революційну ідею: визначити єдину архітектуру (ISA) і побудувати п'ять різних машин, які охоплюють широкий діапазон вартості та продуктивності і при цьому всі реалізують одну й ту саму ISA. Так народилася ISA як свідомий рівень абстракції — і вона досі залишається одним із найфундаментальніших понять в інформатиці.ISA — це не просто список інструкцій. Це вичерпна специфікація, яка охоплює кожен аспект програмно-видимого стану машини. До її основних елементів належать: Підтримувані типи даних. Які формати даних процесор розуміє на рівні апаратури? Цілі числа (8-, 16-, 32-, 64-бітні), числа з рухомою комою (одинарної та подвійної точності), упакований десятковий формат (BCD), символьні дані та, можливо, типи SIMD-векторів. З типами даних byte (DB), word (DW) та doubleword (DD) у мові асемблера x86 ми вже працювали. Регістри. Скільки регістрів доступно, якого вони розміру та яке їхнє призначення? ISA x86 визначає регістри загального призначення (AX, BX, CX, DX), регістри-вказівники (SP, BP), індексні регістри (SI, DI), сегментні регістри (CS, DS, SS, ES), вказівник інструкцій (IP) та регістр прапорців (FLAGS). Деякі ISA мають значно більше регістрів: MIPS — 32 регістри загального призначення, ARM — 16, а RISC-V — до 32. Модель пам'яті. Як процесор бачить пам'ять? Це плоский адресний простір чи сегментований? Який порядок байтів (little-endian чи big-endian)? Як обчислюються адреси? ISA x86, яку ми вивчали в Л03, використовує сегментовану модель пам'яті в реальному режимі з обчисленням фізичних адрес за формулою segment * 16 + offset. Режими адресації. Якими способами інструкція може задавати свої операнди? ISA x86 підтримує, зокрема, безпосередні значення (MOV AX, 5), регістрові операнди (MOV AX, BX), пряму адресацію пам'яті (MOV AX, [1234h]), непряму регістрову адресацію (MOV AX, [BX]) та адресацію «база + зміщення» (MOV AX, [BX+4]). Усі ці режими ми застосовували в лабораторних роботах. Формати інструкцій. Як інструкції кодуються у двійковому вигляді? Яка структура коду операції? Чи мають інструкції фіксовану довжину (кожна інструкція займає однакову кількість байтів) чи змінну (різні інструкції можуть мати різний розмір)? x86 використовує кодування змінної довжини (інструкції займають від 1 до 15 байтів), тоді як ARM — 32-бітні інструкції фіксованої довжини. Набір інструкцій. Повний каталог операцій, які процесор уміє виконувати: арифметичні, логічні, передачі даних, керування потоком виконання, маніпуляції з рядками, введення-виведення тощо. Саме це спадає на думку більшості людей, коли вони чують «ISA», — але це лише один компонент повної специфікації. Модель переривань та виключень. Як процесор реагує на зовнішні події (апаратні переривання) та внутрішні помилки (ділення на нуль, неприпустимий код операції)? Модель переривань x86, зокрема таблицю векторів переривань (IVT) та інструкцію INT для програмних переривань, ми детально вивчали в Л07. Модель введення-виведення. Як процесор взаємодіє з периферійними пристроями? Одні ISA використовують спеціальні інструкції введення-виведення (як IN і OUT у x86, які ми розглядали в Л08), інші — введення-виведення через пам'ять, коли регістри пристроїв доступні як звичайні адреси пам'яті. Сутність, яка виконує інструкції відповідно до ISA, називається реалізацією цієї ISA. Найочевидніша реалізація — це фізичний процесорний чіп. Проте процесори далеко не єдині можливі реалізатори: Різні покоління процесорів. Intel 8086, 80286, 80386, Pentium, Core i7 та Core i9 — усі вони є реалізаціями ISA x86 (з розширеннями, що додавалися з часом). Вони мають радикально різні мікроархітектури: різну кількість стадій конвеєра, різні розміри кешів, різні конфігурації виконавчих блоків, — але всі виконують один і той самий машинний код x86. Різні виробники. І AMD, і Intel виробляють процесори, що реалізують ISA x86-64. AMD Ryzen та Intel Core виконують один і той самий скомпільований двійковий файл, хоча розроблені конкуруючими інженерними командами з абсолютно різними внутрішніми конструкціями. Емулятори. Програма, що працює на одній архітектурі та моделює іншу — послідовно, інструкція за інструкцією. DOSBox, яким ми користуємося в лабораторних роботах, — це емулятор: він реалізує ISA x86 програмно на нашому комп'ютері (який сам може бути x86 або ARM). Емульовані програми «вважають», що працюють на реальному апаратному забезпеченні x86. Віртуальні машини. Java Virtual Machine (JVM) визначає власну ISA на основі інструкцій байт-коду. Будь-який пристрій, здатний запустити реалізацію JVM — чи то телефон, ноутбук, чи сервер, — виконує один і той самий байт-код Java. Аналогічно .NET Common Language Runtime (CLR) визначає ISA байт-коду для C# та інших мов .NET. Інтерпретатори. Програма, яка зчитує інструкції по одній і виконує відповідні операції. Багато ранніх домашніх комп'ютерів містили в ПЗП інтерпретатор BASIC, який, по суті, визначав ISA дуже високого рівня. <br><img alt="Реалізації ISA — одна специфікація x86, багато реалізацій у поколіннях, виробниках, емуляторах та ВМ" src="assets/l09-isa-implementations-uk.svg" target="_self">Саме ця множинність реалізацій і робить абстракцію ISA настільки потужною. Відокремлюючи що машина робить від як саме вона це робить, ISA забезпечує переносність програмного забезпечення, конкуренцію на ринку апаратного забезпечення та технологічну еволюцію — і все це без порушення роботи вже наявних програм.Важлива властивість ISA — розширюваність. ISA можна розширювати, додаючи нові інструкції, нові регістри, нові типи даних або ширші адреси, — за умови, що розширення є зворотно сумісними. ISA x86 неодноразово розширювалася за свою понад 45-річну історію: від початкового 16-бітного набору інструкцій 8086 — до 32-бітних розширень (IA-32), потім 64-бітних (x86-64) і численних спеціалізованих розширень набору інструкцій (MMX, SSE, SSE2, AVX, AVX-512). Код, скомпільований для оригінального 8086, і досі може працювати на сучасному Core i9 — контракт ISA гарантує зворотну сумісність. Натомість код, який використовує новіші розширення (як-от векторні інструкції AVX-512), не запуститься на старих процесорах, що їх не реалізують.Абстракція ISA забезпечує властивість, яку називають двійковою сумісністю: програма, скомпільована для певної ISA, працюватиме на будь-якій реалізації цієї ISA — як поточній, так і майбутній. Це надзвичайно цінно. Коли Intel випускає нове покоління процесорів, усе наявне програмне забезпечення x86 продовжує працювати без змін. Користувачі можуть оновлювати апаратне забезпечення, не замінюючи програмного, а розробники програм — орієнтуватися на одну ISA замість того, щоб писати окремий код для кожної моделі процесора.Двійкова сумісність також уможливлює конкуренцію на ринку апаратного забезпечення. Оскільки і AMD, і Intel реалізують x86-64, вони змагаються за продуктивність, енергоефективність і ціну, але при цьому працюють з тією самою програмною екосистемою. Без абстракції ISA програмне забезпечення кожного виробника було б несумісним з рештою, і ринок виявився б роздробленим.Зворотний бік полягає в тому, що ISA, яка вже утвердилася та набула широкого поширення, надзвичайно складно змінити чи замінити. ISA x86 несе десятиліття накопиченої складності: функції, додані для давно застарілого апаратного забезпечення, надлишкові інструкції, незручні рішення щодо кодування, — і все це неможливо прибрати, не порушивши роботу наявного програмного забезпечення. Це «золоті кайдани» зворотної сумісності: та сама властивість, яка робить ISA такою цінною, водночас робить її стійкою до спрощення. Саме тому така велика увага прикута до створеної з нуля ISA RISC-V, яку спроєктували у 2010 році вже з урахуванням накопиченого досвіду.У світі існує багато ISA — x86, ARM, MIPS, RISC-V, SPARC, PowerPC, z/Architecture та десятки інших. Щоб дати лад цій різноманітності, архітектори комп'ютерів класифікують ISA за двома основними осями: За розташуванням операндів — звідки беруться операнди інструкцій? Ця класифікація відповідає на запитання: коли інструкція на кшталт ADD потребує двох вхідних значень, де вона їх знаходить? На стеку? В акумуляторі? У регістрах загального призначення? В основній пам'яті? Ця вісь породжує п'ять основних типів ISA: стекова архітектура, акумуляторна архітектура, архітектура регістр-пам'ять, архітектура регістр-регістр (load-store) та архітектура пам'ять-пам'ять. Кожну з них ми детально розглянемо в Розділі 6. За складністю інструкцій — який обсяг роботи виконує одна інструкція? Ця вісь розрізняє архітектури CISC (Complex Instruction Set Computer — комп'ютер зі складною системою команд), у яких одна інструкція може виконувати багатоетапні операції на кшталт «завантажити з пам'яті, додати та зберегти результат», та архітектури RISC (Reduced Instruction Set Computer — комп'ютер зі скороченою системою команд), у яких кожна інструкція виконує рівно одну просту операцію. До цієї класифікації ми повернемося в наступних розділах. <br><img alt="Класифікація ISA за двома осями — розташування операндів та складність інструкцій" src="assets/l09-isa-classification-axes-uk.svg" target="_self">Ці дві осі не є незалежними. Архітектури load-store, як правило, належать до RISC (обмеження доступу до пам'яті лише спеціалізованими інструкціями завантаження/збереження — одна з ознак філософії RISC), тоді як архітектури пам'ять-пам'ять за своєю суттю є CISC (адже вони поєднують доступ до пам'яті та обчислення в одній інструкції). Утім, кореляція не ідеальна: x86 — архітектура регістр-пам'ять, але містить багато складних (CISC) інструкцій, а деякі вбудовані CISC-процесори мають дуже прості набори інструкцій. Розуміння обох осей дає багатшу ментальну модель компромісів у проєктуванні ISA.У наступному розділі ми детально розглянемо першу вісь — п'ять основних типів ISA, визначених за розташуванням операндів. Для кожного типу покажемо апаратний шлях даних, простежимо одне й те саме обчислення (A = B + C) та проаналізуємо компроміси щодо кількості інструкцій, щільності коду, складності апаратного забезпечення та придатності до конвеєризації.Найфундаментальніше проєктне рішення в будь-якій ISA: де розташовані операнди? Коли процесору потрібно виконати додавання, звідки він бере два доданки і куди записує результат? Відповідь на це питання формує весь формат інструкцій, визначає, скільки бітів потребує кожна інструкція, впливає на складність апаратури і, зрештою, визначає продуктивність.Щоб зробити порівняння наочним, простежимо обчислення простого виразу в кожному з типів архітектур:A = B + C
Цей вираз вимагає зчитування двох значень (B і C), їх додавання та збереження результату (A). Звернімо увагу, як кількість інструкцій, їхній формат та шаблони переміщення даних відрізняються в різних архітектурах.У стековій архітектурі основне сховище даних процесора організовано як стек — структура «останнім прийшов — першим вийшов» (LIFO). Концепція стека вам уже знайома з Л05, де PUSH і POP використовувалися для збереження та відновлення регістрів і передачі параметрів підпрограмам. У стековій ISA стек — це не просто зручний механізм виклику підпрограм, а центральний механізм усіх обчислень.Ключова ідея така: арифметичні інструкції в стековій ISA не мають жодних явних адрес операндів. Натомість вони неявно оперують верхніми елементами стека. Інструкція ADD знімає два верхні значення зі стека, додає їх і кладе результат назад на стек. Оскільки в інструкції не потрібно зазначати жодних адрес, такі інструкції називають нульадресними командами — інструкція складається лише з коду операції.<br><img alt="Стекова архітектура ISA" src="assets/l09-isa-stack-based.png" target="_self">Стекова архітектура: регістровий файл, організований як стек, подає два верхні значення на входи АЛП (A і B). Результат АЛП повертається на вершину стека. Доступ до пам'яті здійснюється лише через явні інструкції PUSH і POP.Обчислення A = B + C у стековій ISA:PUSH B ; Покласти значення B на стек
PUSH C ; Покласти значення C на стек
ADD ; Зняти два значення, додати їх, покласти результат
POP A ; Зняти результат і зберегти його в A
Зверніть увагу, що інструкція ADD взагалі не має операндів — це єдиний байт коду операції. Лише інструкції PUSH і POP містять адреси пам'яті. Звідси дуже компактне кодування: арифметичні та логічні інструкції крихітні (часто один байт), а код виходить щільним.Стековий підхід використовує постфіксну нотацію (відому також як зворотна польська нотація, або ЗПН). Замість B + C (інфіксна нотація) операція записується як B C + (постфіксна нотація). Цю саму нотацію застосовували класичні калькулятори HP та мова програмування Forth. Компілятори можуть механічно перетворити будь-який арифметичний вираз у постфіксну форму, тож стекові ISA стають зручними цілями для генерації коду.Щоб побачити, як цей підхід масштабується на складніші вирази, розгляньмо обчислення X = (A + B) * (C - D):PUSH A ; Стек: [A]
PUSH B ; Стек: [A, B]
ADD ; Зняти A і B, покласти A+B. Стек: [A+B]
PUSH C ; Стек: [A+B, C]
PUSH D ; Стек: [A+B, C, D]
SUB ; Зняти C і D, покласти C-D. Стек: [A+B, C-D]
MUL ; Зняти A+B і C-D, покласти (A+B)*(C-D). Стек: [результат]
POP X ; Зняти результат у X. Стек: []
Вісім інструкцій — і зверніть увагу, як проміжні результати керуються стеком автоматично. Значення A+B лежить на стеку, поки C-D обчислюється над ним, а потім MUL поєднує їх. Не потрібні ні імена тимчасових змінних, ні розподіл регістрів — порядок на стеку вирішує все. Але ця зручність має свою ціну: якщо A+B знадобиться пізніше в іншому підвиразі, його доведеться продублювати на стеку перед споживанням, що додає зайвих інструкцій маніпуляції зі стеком.Переваги:
Проста апаратура. Не потрібна складна логіка вибору регістрів або маршрутизації операндів. АЛП завжди зчитує дані з двох верхніх позицій стека і записує результат назад на вершину.
Компактний код. Нульадресні арифметичні інструкції дуже малі, тож щільність коду висока — в один і той самий обсяг пам'яті вміщається більше інструкцій.
Легка компіляція. Перетворення виразів високого рівня в постфіксні стекові операції просте й добре вивчене.
Недоліки:
Вузьке місце стека. Кожен проміжний результат мусить проходити через стек. Якщо значення, обчислене раніше, знадобиться значно пізніше, воно має або залишатися заглибленим у стеку (за ретельного впорядкування), або бути збереженим у пам'ять і повторно завантаженим.
Обмежений паралелізм. Інструкції мають суворі залежності за даними через вершину стека, що ускладнює одночасне виконання кількох інструкцій апаратурою.
Накладні витрати на продуктивність. Часті маніпуляції зі стеком (push, pop, swap, duplicate) додають інструкції, які не виконують корисних обчислень, і всі дані мусять проходити через єдине вузьке місце.
<br>Де це використовується? Суто стекові апаратні архітектури були поширенішими в ранній історії обчислень. <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Burroughs_large_systems" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Burroughs_large_systems" target="_self">Burroughs B5000</a> (1961) був новаторською стековою машиною, спроєктованою для ефективного виконання ALGOL. Блок обчислень з рухомою комою x87 у процесорах x86 теж використовує стекову модель регістрів (стек із восьми регістрів, ST(0)–ST(7)), яка збереглася від співпроцесора Intel 8087 (1980) і донині в сучасних процесорах x86, хоча її значною мірою витіснили регістрові інструкції SSE/AVX.<br>Сьогодні стекові архітектури вижили переважно у двох сферах: вбудовані та промислові системи (де простота безпосередньо означає дешевший кремній і нижче енергоспоживання) та віртуальні машини (Java Virtual Machine та .NET CLR використовують стекові набори інструкцій байт-коду, хоча їхні JIT-компілятори зрештою транслюють код у нативний регістровий машинний код). <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Minimal_instruction_set_computer" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Minimal_instruction_set_computer" target="_self">Minimal Instruction Set Computer</a> (MISC) — це сучасний варіант концепції стекової машини, який упаковує кілька крихітних стекових операцій в одне машинне слово; такий підхід застосовують у деяких програмованих процесорах на основі FPGA, де площа кристала обмежена.Акумуляторна архітектура використовує один виділений регістр — акумулятор — як неявний операнд кожної арифметичної операції. Інший операнд береться з пам'яті. Результат завжди записується назад в акумулятор. Оскільки в кожній інструкції потрібно зазначати лише одну адресу пам'яті (акумулятор неявний), такі інструкції називають одноадресними командами.Тим, хто працював з мовою асемблера x86 у цьому курсі, концепція акумулятора має бути знайомою. Пригадаймо з Л03 та Л04, що регістр AX у x86 — жорстко закріплений приймач для багатьох операцій: MUL і DIV завжди використовують AX (та DX:AX для розширених результатів), й історично багато інструкцій оптимізовано саме для акумулятора. Назва «AX» означає «Accumulator eXtended» (Розширений акумулятор). ISA x86 не є суто акумуляторною архітектурою (у ній є кілька регістрів загального призначення), але її конструкція несе потужну ДНК акумуляторної епохи.<br><img alt="Акумуляторна архітектура ISA" src="assets/l09-isa-accumulator-based.png" target="_self">Акумуляторна архітектура: регістр-акумулятор подає один вхід АЛП (A), тоді як пам'ять подає інший вхід (B). Результат АЛП зберігається назад в акумулятор. У кожній інструкції кодується лише одна адреса пам'яті.Обчислення A = B + C в акумуляторній ISA:LOAD B ; Завантажити значення B в акумулятор
ADD C ; Додати значення C (з пам'яті) до акумулятора
STORE A ; Зберегти значення акумулятора в комірку пам'яті A
Три інструкції, кожна зазначає одну адресу пам'яті. Акумулятор завжди є неявним джерелом і приймачем — його ніколи не доводиться називати в інструкції.Тепер розгляньмо складніший вираз X = (A + B) * (C - D) на акумуляторній машині:LOAD A ; ACC = A
ADD B ; ACC = A + B
STORE TEMP ; TEMP = A + B (потрібно зберегти — ACC буде перезаписаний)
LOAD C ; ACC = C
SUB D ; ACC = C - D
MUL TEMP ; ACC = (C - D) * TEMP = (C - D) * (A + B)
STORE X ; X = результат
Сім інструкцій — і зверніть увагу на критичну проблему: нам довелося використати тимчасову комірку пам'яті (TEMP) для збереження проміжного результату A + B, поки обчислювалось C - D. Маючи лише один регістр, кожен підвираз, який треба зберегти на потім, мусить бути вивантажений у пам'ять. У стековій машині сам стек автоматично зберігає проміжні значення. В акумуляторній машині програмістові (або компілятору) доводиться керувати тимчасовими змінними явно.Переваги:
Простий формат інструкцій. Кожна інструкція — це лише код операції плюс одна адреса пам'яті, тож декодування просте.
Менше бітів на інструкцію, ніж в архітектур, які мусять зазначати два чи три регістрових/пам'яткових операнди.
Проста апаратура. АЛП завжди зчитує один вхід з акумулятора, а інший — з шини даних; результат завжди повертається в акумулятор. Мультиплексори маршрутизації операндів не потрібні.
Недоліки: Інтенсивний обмін з пам'яттю. Єдиний регістр для проміжних результатів змушує складні вирази часто завантажувати й зберігати дані. Для виразу на кшталт (A + B) * (C - D) доводиться зберегти проміжний результат A + B у тимчасову комірку перед обчисленням C - D, а потім завантажити його назад для множення. Послідовне вузьке місце. Усі дані проходять через єдиний акумулятор, утворюючи ланцюжок залежностей, який обмежує паралелізм на рівні інструкцій. Кожна інструкція залежить від попередньої (бо всі читають і пишуть той самий акумулятор), тож перекриття виконання інструкцій стає практично неможливим. Розмір коду для складних програм. Хоча окремі інструкції малі, для складних виразів загальна кількість інструкцій швидко зростає через постійну потребу зберігати й перезавантажувати акумулятор через тимчасові комірки пам'яті. <br>Історичне значення. Акумуляторні архітектури домінували в ранніх обчисленнях. <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/EDSAC" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/EDSAC" target="_self">EDSAC</a> (1949), один із перших комп'ютерів зі збереженою програмою, був акумуляторною машиною. Багато ранніх мікропроцесорів — Intel 8080, MOS Technology 6502, Zilog Z80 — мали конструкції з центральною роллю акумулятора. Акумуляторна модель природно відповідала технологіям своєї епохи: коли транзистори були дорогими, а площа кристала — обмеженою, виділити кремній під один привілейований регістр було набагато практичніше, ніж облаштовувати цілий банк регістрів загального призначення.Зі зростанням кількості регістрів (8086 запровадив чотири «регістри загального призначення» — AX, BX, CX, DX) суто акумуляторні архітектури поступилися місцем регістровим, але спадщина акумулятора живе у привілейованій ролі, яку AX/EAX/RAX відіграє в ISA x86 й донині. Навіть у сучасному x86 деякі інструкції (як-от MUL, DIV, CBW, LAHF) досі неявно використовують AX як свій операнд — пряме спадкування акумуляторної традиції.Зі зростанням транзисторних бюджетів і можливості проєктувальників процесорів дозволити собі більше регістрів архітектури вийшли за межі моделі з одним акумулятором і включили кілька регістрів загального призначення. Це фундаментально змінило модель обчислень: замість проганяти всі дані через один регістр-вузьке місце процесор може одночасно зберігати багато проміжних значень, що різко зменшує обмін з пам'яттю. Але в межах сімейства регістрових архітектур є ключова проєктна розвилка: чи можуть арифметичні інструкції мати безпосередній доступ до пам'яті, чи доступ до пам'яті обмежено спеціалізованими інструкціями завантаження та збереження?<br><img alt="Архітектури регістр-пам'ять та регістр-регістр" src="assets/l09-isa-register-memory-and-register-register.png" target="_self">Два регістрових підходи поряд. (a) Регістр-пам'ять: один вхід АЛП надходить з регістра, інший — з пам'яті. (b) Регістр-регістр (load-store): обидва входи АЛП надходять з регістрів; доступ до пам'яті здійснюється лише через спеціалізовані інструкції LOAD і STORE.В архітектурі регістр-пам'ять арифметичні інструкції можуть приймати один операнд з регістра, а інший — з пам'яті. Це двоадресний формат, добре знайомий вам із мови асемблера x86. Пригадаймо з Л03 та Л04 різні комбінації операндів MOV (REG-REG, REG-MEM, MEM-REG) — це модель регістр-пам'ять у дії:; Приклад регістр-пам'ять x86: A = B + C
MOV AX, [B] ; Завантажити B з пам'яті в регістр AX
ADD AX, [C] ; Додати C з пам'яті безпосередньо до AX
MOV [A], AX ; Зберегти результат у комірку пам'яті A
Інструкція ADD AX, [C] характерна для архітектури регістр-пам'ять: вона поєднує доступ до пам'яті (читання C) з арифметичною операцією (додаванням) в одній інструкції. Результат перезаписує один із вхідних регістрів (AX), тобто один з операндів-джерел знищується — наслідок двоадресного формату.Переваги:
Менше інструкцій для простих операцій порівняно з load-store, оскільки арифметика може безпосередньо звертатися до пам'яті.
Звичність і гнучкість — регістрові та пам'яткові операнди можна природно поєднувати.
Недоліки:
Змінний час виконання. Інструкція зі зверненням до пам'яті виконується довше, ніж суто регістрова, а це ускладнює ефективну конвеєризацію.
Складність кодування інструкцій. Підтримка кількох режимів адресації для операндів у пам'яті призводить до інструкцій змінної довжини та складної логіки декодування.
ISA x86 — найвідоміший приклад архітектури регістр-пам'ять, хоча, як зазначалось у Л04, x86 накладає одне важливе обмеження: не можна мати обидва операнди в пам'яті для однієї арифметичної інструкції (немає ADD типу MEM-MEM). Принаймні один операнд має бути регістром. Сімейство Motorola 68000 — ще один відомий приклад архітектури регістр-пам'ять: воно пішло далі, давши вісім регістрів даних (D0–D7) та вісім адресних регістрів (A0–A7), тож програміст має більше можливостей зберігати проміжні значення, ніж у чотирьох регістрах загального призначення 8086.Двоадресні інструкції породжують також тонке семантичне питання: один з операндів-джерел знищується операцією. В ADD AX, [C] початкове значення AX втрачається — його перезаписує сума. Це прямий наслідок повторного використання одного регістра-джерела як операнда-приймача. Якщо початкове значення AX потрібне пізніше, програміст або компілятор має зберегти його заздалегідь (наприклад, MOV DX, AX перед ADD). Триадресні архітектури повністю уникають цієї проблеми, зазначаючи окремий приймач.Архітектура регістр-регістр, відома також як архітектура load-store, іде в розділенні ще далі: всі арифметичні та логічні операції працюють виключно з регістрами. Доступ до пам'яті здійснюється лише через спеціалізовані інструкції LOAD (зчитування з пам'яті в регістр) і STORE (запис із регістра в пам'ять). Жодна арифметична інструкція ніколи не звертається безпосередньо до пам'яті.; Приклад load-store MIPS: A = B + C
LW $t0, B ; Завантажити значення B в регістр $t0
LW $t1, C ; Завантажити значення C в регістр $t1
ADD $t2, $t0, $t1 ; Додати $t0 і $t1, зберегти результат у $t2
SW $t2, A ; Зберегти $t2 в комірку пам'яті A
Чотири інструкції замість трьох, причому інструкція ADD зазначає три регістри (два джерела та один приймач) — триадресний формат. Результат записується в окремий регістр-приймач, а обидва вхідні значення зберігаються для можливого повторного використання.На перший погляд це може здатися менш ефективним — більше інструкцій на те саме обчислення. Та підхід load-store має глибокі переваги для проєктування апаратури:Переваги: Однорідний час виконання інструкцій. Арифметичні інструкції завжди працюють з регістрами (швидко), а інструкції load/store завжди звертаються до пам'яті (повільніше). Апаратура може ефективно конвеєризувати їх, бо точно знає, які інструкції потребуватимуть доступу до пам'яті. Ця передбачуваність — ключовий чинник конвеєризації, техніки, що дозволяє процесору перекривати виконання кількох інструкцій. Інструкції фіксованої довжини. Оскільки арифметичні інструкції називають лише регістри (а не адреси пам'яті зі складними режимами адресації), кодування інструкцій простіше і зазвичай має фіксовану довжину. Інструкції фіксованої довжини значно легше вибирати, декодувати та конвеєризувати. Повторне використання регістрів. За триадресного формату проміжні результати залишаються в регістрах і можуть бути повторно використані без перезавантаження. В архітектурі регістр-пам'ять проміжний результат, збережений у регістрі, може бути перезаписаний наступною інструкцією, яка використовує цей регістр як приймач. Зручність для компілятора. Сучасні оптимізувальні компілятори вміють ефективно розподіляти змінні по регістрах і планувати завантаження/збереження, щоб приховати затримки пам'яті. Чітке розділення між обчисленнями та доступом до пам'яті значно спрощує розподіл регістрів і планування інструкцій. Компілятор може запланувати незалежні завантаження на ранніх етапах потоку інструкцій, щоб дані надійшли з пам'яті саме на той момент, коли їх потребуватиме арифметична інструкція — техніка, відома як планування завантажень або програмна конвеєризація. Простіше декодування. Завдяки уніфікованому формату інструкцій (усі арифметичні інструкції виглядають однаково: код операції + три поля регістрів) апаратний декодер простий, швидкий і придатний для паралелізації. Це різко контрастує з x86, де декодер має обробляти сотні різних форматів інструкцій і кодувань змінної довжини. Тепер розгляньмо X = (A + B) * (C - D) на машині load-store:LW $t0, A ; $t0 = A
LW $t1, B ; $t1 = B
ADD $t2, $t0, $t1 ; $t2 = A + B
LW $t3, C ; $t3 = C
LW $t4, D ; $t4 = D
SUB $t5, $t3, $t4 ; $t5 = C - D
MUL $t6, $t2, $t5 ; $t6 = (A + B) * (C - D)
SW $t6, X ; Зберегти результат у X
Вісім інструкцій — більше, ніж у стековій або акумуляторній версіях. Але зверніть увагу на ключову перевагу: тимчасова комірка пам'яті не потрібна. Проміжне значення A + B безпечно зберігається в регістрі $t2, поки C - D обчислюється в $t5. Жодного вивантаження в пам'ять, жодного повторного завантаження. Ба більше, конвеєрний процесор міг би потенційно почати завантаження C і D, поки ADD ще виконується, адже ці операції незалежні. Саме такий паралелізм на рівні інструкцій і є тим, заради чого спроєктовано архітектури load-store.Недоліки:
Потрібно більше інструкцій для того ж обчислення, оскільки кожен доступ до пам'яті вимагає окремої інструкції завантаження або збереження.
Тиск на регістри. Якщо програмі одночасно потрібно більше «живих» значень, ніж доступних регістрів, компілятор мусить «вивантажувати» значення в пам'ять і перезавантажувати їх пізніше, що додає роботи інструкціям load/store. Цей показник — відношення значень, які мають бути «живими», до доступних регістрів — називають тиском на регістри. RISC-архітектури пом'якшують його великими регістровими файлами (32 регістри — типове значення для MIPS, ARM та RISC-V), але складні програми все одно можуть їх вичерпати. Вбудовані RISC ISA з меншими наборами регістрів (як-от ARM Thumb лише з 8 регістрами загального призначення) можуть страждати від високого тиску на регістри.
<br>Приклади. Більшість сучасних ISA — архітектури load-store: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/ARM_architecture_family" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/ARM_architecture_family" target="_self">ARM</a>, <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/MIPS_architecture" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/MIPS_architecture" target="_self">MIPS</a>, <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/RISC-V" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/RISC-V" target="_self">RISC-V</a>, <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/SPARC" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/SPARC" target="_self">SPARC</a> та <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/PowerPC" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/PowerPC" target="_self">PowerPC</a>. Принцип load-store — одна з визначальних рис філософії RISC, а переваги продуктивності, які він дає через конвеєризацію, зробили його домінуючим підходом у сучасному проєктуванні процесорів.<br>Історична мотивація для load-store виникла з досліджень IBM у 1970-х роках (проєкт <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/IBM_801" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/IBM_801" target="_self">IBM 801</a>) та в Берклі й Стенфорді на початку 1980-х. Дослідники проаналізували, як компілятори фактично використовували складні набори інструкцій CISC-машин, і виявили, що переважна більшість інструкцій складалася з простих операцій регістр-регістр та звичайних завантажень/збережень з пам'яті. Складні арифметичні інструкції з доступом до пам'яті, які ретельно реалізовували проєктувальники апаратури, компілятори генерували рідко. Саме це відкриття — що простіші інструкції, виконувані швидше завдяки конвеєризації, перевершують менше складних інструкцій — стало інтелектуальним фундаментом руху RISC.На протилежному кінці спектру від load-store архітектура пам'ять-пам'ять допускає, щоб обидва операнди й результат перебували в основній пам'яті. Арифметична інструкція зазначає три адреси пам'яті: два джерела та один приймач. Це найпотужніший формат однієї інструкції — одна інструкція може зчитати два значення з пам'яті, виконати операцію та записати результат назад у пам'ять, і все це без залучення жодних програмно-видимих регістрів.; Приклад пам'ять-пам'ять: A = B + C
ADD [A], [B], [C] ; Зчитати B і C з пам'яті, додати, зберегти результат у A
Одна інструкція. Регістри не потрібні. Інструкція кодує три повні адреси пам'яті — тож вона є триадресною (у сенсі пам'яті, на відміну від триадресного регістрового формату машин load-store).<br><img alt="Архітектура пам'ять-пам'ять: ADD [A], [B], [C] — одна інструкція спричиняє 3 звернення до пам'яті" src="assets/l09-memory-memory-architecture-uk.svg" target="_self">Для складнішого виразу на кшталт X = (A + B) * (C - D):ADD [TEMP], [A], [B] ; TEMP = A + B
SUB [TEMP2], [C], [D] ; TEMP2 = C - D
MUL [X], [TEMP], [TEMP2] ; X = TEMP * TEMP2
Три інструкції, кожна кодує три адреси пам'яті. Проміжні результати мусять записуватися у тимчасові комірки пам'яті, бо регістрів для їх зберігання немає. Порівняно з версією load-store (вісім інструкцій) версія пам'ять-пам'ять значно компактніша за кількістю інструкцій — але кожна інструкція набагато дорожча у виконанні.З погляду програміста це звучить ідеально — максимум роботи на інструкцію, мінімум інструкцій. Але практичні проблеми серйозні:Недоліки:
Величезні інструкції. Кожна інструкція мусить кодувати три повні адреси пам'яті. На 32-бітній машині це 12 байтів лише адрес, плюс код операції. Інструкції виходять дуже великими.
Повільне виконання. Кожна інструкція потребує трьох окремих звернень до пам'яті (два читання та один запис), а пам'ять значно повільніша за регістри. Одна потужна інструкція виконується набагато довше, ніж три простіші регістрові інструкції на конвеєрній машині.
Ворожість до конвеєризації. Змінний і великий час виконання інструкцій пам'ять-пам'ять робить конвеєризацію надзвичайно складною.
Складна апаратура. Інтерфейс пам'яті мусить підтримувати кілька одночасних (або послідовних) операцій з пам'яттю на одну інструкцію, що значно ускладнює апаратуру.
Переваги:
Мінімальна кількість інструкцій. Для простих виразів одна інструкція може виконати те, що в інших архітектурах потребує трьох-чотирьох інструкцій.
Немає потреби в керуванні регістрами. Програмістові (та компілятору) не доводиться вирішувати, які значення тримати в регістрах — усе живе в пам'яті.
Пригадаймо з Л04, що x86 явно забороняє операції пам'ять-пам'ять для більшості інструкцій — не можна написати ADD [A], [B], бо щонайбільше один операнд може бути посиланням на пам'ять. Це було свідоме проєктне рішення, і тепер ви бачите причини продуктивності, що за ним стоять. Єдиний помітний виняток у x86 — сімейство рядкових інструкцій MOVS (наприклад, MOVSB, MOVSW), які виконують пересилання пам'ять-пам'ять, — але це спеціалізовані інструкції блокового копіювання, а не арифметичні інструкції загального призначення.<br>Історичні приклади. <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/VAX" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/VAX" target="_self">VAX-11</a> (1977) — найвідоміша архітектура пам'ять-пам'ять. Інструкції VAX могли зазначати до шести операндів, кожен зі своїм режимом адресації, і безпосередньо підтримували операції пам'ять-пам'ять. VAX був легендарним завдяки своєму ортогональному та потужному набору інструкцій — будь-яка інструкція могла використовувати будь-який режим адресації з будь-яким операндом. Одна інструкція VAX виконувала те, на що RISC-машині знадобилося б п'ять-шість інструкцій. Наприклад, інструкція VAX POLY могла обчислити цілий поліном однією інструкцією, виконуючи серію операцій множення-накопичення з операндами в пам'яті.Однак ця потужність мала свою ціну: реалізації VAX були повільними і складними для конвеєризації. Інструкції змінної довжини (від 1 до 54 байтів!) робили вибірку та декодування непередбачуваними, а множинні звернення до пам'яті на одну інструкцію створювали тривалі затримки в конвеєрі. Контраст між необробленою потужністю інструкцій VAX та його слабкою продуктивністю конвеєра став однією з ключових мотивацій руху RISC у 1980-х роках. Коли Digital Equipment Corporation (DEC) нарешті перейшла на RISC-архітектуру (Alpha, 1992), продуктивність різко зросла, попри те що програми стали потребувати більше інструкцій.System/360 від IBM теж підтримувала деякі операції пам'ять-пам'ять. Її інструкції SS-формату (Storage-to-Storage) могли переміщувати або порівнювати блоки пам'яті безпосередньо, а інструкції AP/SP (Add/Subtract Packed) виконували десяткову арифметику цілком у пам'яті. Втім, більшість арифметики S/360 була регістровою (інструкції RX-формату), тож це радше гібридна архітектура, а не суто машина пам'ять-пам'ять.Таблиця нижче підсумовує п'ять типів архітектур за ключовими вимірами. Кожен рядок — це компроміс: простіша апаратура та компактніші інструкції зазвичай мають ціну у вигляді нижчої продуктивності й обмеженого паралелізму, натомість архітектури load-store з великою кількістю регістрів потребують більше інструкцій, але забезпечують конвеєризацію та паралелізм, що рухають сучасну продуктивність.З цієї таблиці випливає кілька закономірностей:Індустрія збіглася до регістрових архітектур. І регістр-пам'ять (уособлена x86), і регістр-регістр (уособлена ARM, MIPS, RISC-V) домінують у сучасних обчисленнях. Стековий та акумуляторний підходи виявилися надто обмежувальними для високопродуктивних конструкцій, а архітектури пам'ять-пам'ять — надто повільними та складними.Більше інструкцій не означає повільніше виконання. Архітектура load-store потребує чотирьох інструкцій для A = B + C, тоді як архітектурі пам'ять-пам'ять вистачає однієї. Проте машина load-store на практиці швидша, бо її прості, фіксованої довжини, суто регістрові інструкції можуть бути глибоко конвеєризовані і виконуватися з перекриттям. Сучасний процесор ARM виконує кілька мільярдів таких інструкцій на секунду, тоді як одна інструкція класу пам'ять-пам'ять на кшталт VAX може потребувати десятка тактових циклів.ISA x86 — це гібрид. x86 займає цікаву проміжну позицію. За класифікацією це архітектура регістр-пам'ять (арифметичні інструкції можуть безпосередньо звертатися до пам'яті), але сучасні реалізації x86 внутрішньо розкладають складні інструкції на простіші мікрооперації, які нагадують RISC-інструкції load-store. Це дає x86 зручність кодування регістр-пам'ять для програміста разом із перевагами RISC-подібного конвеєра всередині реалізації.Стекові машини знайшли нове життя у віртуальних машинах. Хоча апаратні стекові машини сьогодні — рідкість, стекова модель виконання виявилася ідеальною для байт-коду віртуальних машин. JVM, .NET CLR та WebAssembly — усі використовують стекові формати інструкцій. Нульадресні інструкції надзвичайно компактні (що важливо для поширення коду мережами), а стекова семантика спрощує верифікацію байт-коду. Під час виконання JIT-компілятори транслюють стековий байт-код у нативний регістровий машинний код, отримуючи найкраще з обох світів.Еволюція має чітку історичну траєкторію. Прогресія від акумуляторної до регістр-пам'ять і далі до load-store відображає зростання кількості доступних транзисторів. Коли транзисторів було мало (1950–1970-ті), акумуляторна модель була єдиним практичним вибором — один регістр потребує мінімум кремнію. Зі зростанням бюджетів на кристалі архітектори могли дозволити собі 4–8 регістрів загального призначення (ера регістр-пам'ять, 1970–1980-ті). З подальшим масштабуванням за законом Мура стали доступними 16–32 регістри, що уможливило повну модель load-store (1980-ті й далі). Кожне покоління обмінювало кількість інструкцій на апаратний паралелізм, і кожне покоління перемагало, бо приріст продуктивності від конвеєризації та паралелізму значно перевищував витрати на виконання більшої кількості інструкцій. Розуміння цієї траєкторії — необхідний контекст для дебатів RISC проти CISC, які ми розглянемо в наступних розділах.---## 7. Класифікація ISA за складністю інструкційОдне з найвагоміших проєктних рішень в архітектурі комп'ютерів — наскільки складною має бути кожна окрема інструкція. Чи може одна інструкція завантажити значення з пам'яті, додати його до регістра і зберегти результат назад — усе за один крок? Чи кожна з цих дій має вимагати окремої інструкції?Це питання ділить світ архітектур систем команд на два основні табори — CISC (Complex Instruction Set Computer — комп'ютер зі складною системою команд) та RISC (Reduced Instruction Set Computer — комп'ютер зі скороченою системою команд), — а також третій, менш поширений підхід під назвою VLIW (Very Long Instruction Word — дуже довге командне слово). Кожна філософія відбиває принципово різну ставку на те, де має бути зосереджена складність: в апаратурі, у компіляторі чи в самому кодуванні інструкцій.Дискусія CISC проти RISC визначила всю траєкторію проєктування процесорів від 1980-х років і дотепер, тож її розуміння необхідне для осмислення сучасних архітектур — від чіпа x86 у вашому ноутбуці до ядра ARM у вашому телефоні.<br>Філософія CISC виникла в епоху, коли програмісти часто писали код на асемблері вручну. Пам'ять була дефіцитною та дорогою, а компілятори — примітивними. За таких умов керівний принцип був зрозумілий: змусити апаратне забезпечення виконувати якомога більше роботи за одну інструкцію. Якщо програміст часто копіює блок пам'яті — дамо йому одну інструкцію, яка робить саме це. Якщо треба шукати символ у рядку — вбудуймо це в апаратне забезпечення. Мета — подолати <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Semantic_gap" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Semantic_gap" target="_self">семантичний розрив</a> між тим, що виражають мови високого рівня, і тим, що машина може зробити за один крок.Ця філософія спонукала архітекторів проєктувати системи команд, що безпосередньо підтримували конструкції мов високого рівня: виклики процедур, керування циклами, складні режими адресації, які могли розіменовувати вказівники й індексувати масиви однією інструкцією. Результатом стали багаті, виразні системи команд, що виконували дуже багато за лічені рядки машинного коду.Архітектури CISC мають кілька визначальних рис:
Великі набори інструкцій — сотні різних інструкцій, що охоплюють усе: від базової арифметики до маніпуляцій з рядками, арифметики BCD (двійково-десятковий код) та блокових пересилань пам'яті
Інструкції змінної довжини — займають від 1 до 15 байтів (у x86) залежно від операції та операндів
Змінний час виконання — просте пересилання регістр-у-регістр триває 1 такт, а складна рядкова операція — сотні
Складні режими адресації — інструкції звертаються до пам'яті через обчислення «база + індекс + зміщення», автоінкремент та інші розвинені схеми
Операнди в пам'яті в арифметичних операціях — одна інструкція ADD здатна завантажити значення з пам'яті, додати його до регістра й за потреби зберегти результат — усе за один крок
Мікрокод — багато процесорів CISC реалізують складні інструкції через внутрішні послідовності мікрокоду, фактично виконуючи для кожної інструкції маленьку програму всередині процесора
Менше регістрів загального призначення — оскільки інструкції працюють безпосередньо з пам'яттю, великої кількості регістрів не потребували (x86 спочатку мав лише 8)
Щоб побачити філософію CISC у дії, розгляньмо інструкцію x86 REP MOVSB. Ця єдина інструкція копіює блок байтів з однієї ділянки пам'яті в іншу:; Копіювання 100 байтів з адреси в DS:SI за адресою в ES:DI
mov cx, 100 ; CX = кількість байтів для копіювання
rep movsb ; Повтор: копіювати байт з [SI] до [DI], інкрементувати обидва, декрементувати CX ; Продовжувати, доки CX = 0
Ця послідовність із двох інструкцій замінює те, що інакше було б явним циклом:; Та сама операція без REP MOVSB
mov cx, 100
copy_loop: mov al, [si] mov [di], al inc si inc di dec cx jnz copy_loop
Версія CISC компактніша (менше байтів машинного коду, менше інструкцій для вибірки) і концептуально простіша для програміста на асемблері. Апаратне забезпечення обробляє цикл внутрішньо.Інший приклад — інструкція x86 LOOP, яка об'єднує в одну інструкцію три операції: декремент CX, порівняння його з нулем та перехід, якщо результат не нуль:mov cx, 10
process: ; ... виконати якусь дію ... loop process ; Декрементує CX, переходить до 'process', якщо CX != 0
А інструкції ENTER і LEAVE автоматизують створення та знищення стекового фрейму для викликів процедур — операції, які на архітектурах RISC вимагають кількох інструкцій.
Щільність коду — програми займають менше місця в пам'яті, що було критично, коли обсяг оперативної пам'яті вимірювався кілобайтами
Зручність для програміста — одна інструкція здатна виразити складну операцію, скорочуючи кількість рядків в асемблерних програмах
Менше вибірок інструкцій — оскільки кожна інструкція виконує більше роботи, з пам'яті треба вибирати менше інструкцій, що зменшує навантаження на шину
Підхід CISC має значні недоліки:
Складність породжує непередбачуваність — із сотнями інструкцій різної довжини та часу виконання програмістам (або компіляторам) надзвичайно важко передбачити, скільки часу триватиме дана послідовність коду
Важко конвеєризувати — інструкції змінної довжини складно декодувати паралельно, а змінний час виконання ускладнює підтримання конвеєра заповненим і збалансованим
Марнування кремнію — дослідження 1970-х і 1980-х років показали, що компілятори зазвичай використовують лише 20–30% доступних інструкцій. Решта споживала площу кристала та зусилля на проєктування, але залишалася невикористаною
Накладні витрати мікрокоду — внутрішній механізм мікрокоду додає рівень непрямості, який уповільнює навіть прості операції
Продуктивність залежить від програміста — через великий і нерегулярний набір інструкцій вибір найшвидшої послідовності для конкретної задачі вимагає глибоких знань
Філософія CISC — «думай про програміста» — мала цілковитий сенс, коли програмісти писали асемблерний код вручну. Але до 1980-х років ландшафт кардинально змінився. Компілятори стали достатньо потужними, щоб генерувати ефективний машинний код з мов високого рівня, і програмісти рідко писали на асемблері.Ця зміна виявила фундаментальну проблему CISC: якщо код генерує компілятор, система команд не має бути зручною для людей — вона має бути ефективною для апаратного забезпечення. Сотні рідко вживаних інструкцій не просто марнували кремній; вони активно обмежували продуктивність, ускладнюючи оптимізацію процесора. Складний декодер інструкцій споживав площу кристала, яку можна було б віддати під більші кеші, більшу кількість регістрів або глибші конвеєри.Усвідомлення того, що основними «користувачами» систем команд є компілятори, а не люди, підготувало ґрунт для радикального переосмислення проєктування процесорів.Наприкінці 1970-х років дослідники з UC Berkeley та Стенфордського університету дійшли контрінтуїтивного висновку: процесор можна зробити швидшим, давши йому менше й простіших інструкцій. Логіка була прямолінійною. Якщо кожна інструкція проста й однорідна, апаратне забезпечення виконає її за один такт. Якщо всі інструкції мають однакову довжину, декодер може бути простим і швидким. А якщо процесор не витрачає транзистори на рідко вживані складні інструкції, то ці транзистори можна віддати під функції, що реально підвищують продуктивність — більше регістрів, більші кеші, глибші конвеєри.Так народилася філософія RISC. Назву «Reduced Instruction Set Computer» запропонував Девід Паттерсон у UC Berkeley у 1980 році. Слово «reduced» не означає, що процесор здатен робити менше — воно означає, що кожна окрема інструкція робить менше, а компілятор комбінує прості інструкції для виконання складних задач.Архітектури RISC побудовані навколо кількох взаємопов'язаних принципів:1. Малий, фіксований набір інструкцій. Типовий RISC-процесор має від 50 до 150 інструкцій проти 500+ у CISC-проєкті. Кожна інструкція заслуговує своє місце завдяки частому використанню.2. Однорідна довжина інструкцій. Усі інструкції мають однаковий розмір — зазвичай 32 біти (4 байти). Це робить вибірку й декодування інструкцій тривіально паралельними: процесор завжди знає, де починається наступна інструкція.3. Мало форматів інструкцій. Інструкції вкладаються в невелику кількість фіксованих шаблонів (зазвичай 3–4 формати). Код операції, поля регістрів та безпосередні значення завжди розташовані в тих самих бітових позиціях, що спрощує апаратуру декодування.4. Архітектура load-store. Це найхарактерніша риса RISC. Арифметичні та логічні інструкції працюють виключно з регістрами. Єдині інструкції, що звертаються до пам'яті, — це явні LOAD (зчитати з пам'яті в регістр) та STORE (записати з регістра в пам'ять). Інструкцій на кшталт «додати значення з пам'яті до регістра» не існує.; Стиль RISC: явні load, операція, store
LOAD R1, [address] ; Крок 1: завантажити значення з пам'яті в R1
ADD R2, R1, R3 ; Крок 2: додати R1 та R3, результат у R2
STORE R2, [address] ; Крок 3: зберегти результат назад у пам'ять
Порівняймо це з підходом CISC:; Стиль CISC: операнд у пам'яті в арифметичній інструкції
ADD [address], R3 ; Завантажити з адреси, додати R3, зберегти результат — одна інструкція
Версія RISC використовує три інструкції замість однієї, але кожна з них проста, передбачувана і швидка.<br>5. Великий регістровий файл. Оскільки всі обчислення відбуваються в регістрах, RISC-процесори надають їх значно більше — зазвичай 32 регістри загального призначення, а сучасні проєкти можуть мати фізичні регістрові файли з 200–500 регістрів (із <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Register_renaming" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Register_renaming" target="_self">перейменуванням регістрів</a> для підтримки позачергового виконання). Для порівняння: оригінальний x86 мав лише 8 регістрів загального призначення.6. Апаратне керування. Замість мікрокоду RISC-процесори реалізують виконання інструкцій безпосередньо в апаратній логіці. Це усуває накладні витрати на пошук і послідовне виконання мікрокоду.Підхід RISC створює характерні особливості продуктивності:
Виконання за один такт — більшість інструкцій завершуються за один такт, що робить продуктивність добре передбачуваною. Процесор підтримує пропускну здатність, близьку до однієї інструкції за такт (IPC ~1), а в суперскалярних проєктах — і вищу
Ефективна конвеєризація — однорідні довжина інструкцій і час виконання означають, що стадії конвеєра природно збалансовані. Немає затримок, спричинених тим, що одна інструкція виконується в 10 разів довше за наступну
Вищі тактові частоти — простіша логіка означає коротші критичні шляхи, що дозволяє досягати вищих тактових частот
Енергоефективність — менш складне апаратне забезпечення споживає менше енергії на операцію, що є критичною перевагою для мобільних і вбудованих пристроїв
Довші програми — програми RISC приблизно на 30% довші (за кількістю інструкцій), ніж еквівалентні програми CISC, оскільки складні операції потребують кількох інструкцій. Проте кожна інструкція виконується швидше, тому загальний час виконання зазвичай такий самий або кращий
Залежність від компілятора — оскільки апаратура виконує менше «розумної роботи», компілятор має бути розумнішим у плануванні інструкцій, розподілі регістрів та оптимізації конвеєра. Наївний компілятор може згенерувати значно повільніший код на RISC-процесорі, ніж на CISC
<br>Найяскравіша історія успіху у світі RISC належить <a data-tooltip-position="top" aria-label="https://www.arm.com/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.arm.com/" target="_self">ARM</a> (спочатку Acorn RISC Machine, пізніше Advanced RISC Machines). У 1983 році невелика британська компанія Acorn Computers почала розробляти процесор для свого персонального комп'ютера наступного покоління. Натхненні статтями Berkeley RISC, інженери Софі Вілсон та Стів Фербер спроєктували надзвичайно простий та ефективний 32-бітний процесор. Перший чіп ARM (ARM1) мав лише 25 000 транзисторів — порівняно з 275 000 у тогочасному Intel 386.Простота ARM безпосередньо втілилася в низьке енергоспоживання. Попри те, що для настільних комп'ютерів це спершу здавалося скромною перевагою, у мобільній революції вона стала вирішальною. Коли у 2000-х роках з'явилися смартфони та планшети, кожен виробник потребував процесори, здатні давати достатню продуктивність на крихітній батареї. Архітектура ARM була очевидним вибором.Сьогодні процесори на базі ARM є скрізь:
Apple Silicon (серії M1, M2, M3, M4, M5) — живить MacBook та iMac, доводячи, що ARM може конкурувати з x86 навіть у високопродуктивних настільних системах
Apple A-series — у кожному iPhone та iPad
Qualcomm Snapdragon — у більшості Android-смартфонів
AWS Graviton — у хмарних серверах Amazon
<br>Fujitsu A64FX — у <a data-tooltip-position="top" aria-label="https://www.top500.org/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.top500.org/" target="_self">Fugaku</a>, який у 2020–2022 роках був найшвидшим суперкомп'ютером світу
Вбудовані системи — в автомобілях, маршрутизаторах, Smart TV, промислових контролерах і мільярдах пристроїв IoT
ARM відвантажує понад 25 мільярдів чіпів на рік. Щорічно виробляють більше процесорів ARM, ніж усіх інших процесорних архітектур разом узятих. Ставка на RISC — що простота виграє в довгостроковій перспективі — виправдалася блискуче.<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/MIPS_architecture" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/MIPS_architecture" target="_self">MIPS</a> (Microprocessor without Interlocked Pipeline Stages) — ще одна основоположна архітектура RISC, розроблена Джоном Геннессі у Стенфорді в 1981 році. MIPS стала стандартною навчальною архітектурою в програмах з комп'ютерних наук по всьому світу — той, хто проходив курс архітектури комп'ютерів деінде, майже напевно вивчав MIPS.У комерційному світі MIPS живила робочі станції Silicon Graphics, ігрові консолі PlayStation (PS1, PS2, PSP), Nintendo 64 та безліч мережевих пристроїв. Хоча MIPS значною мірою зникла з ринку, її вплив на викладання проєктування процесорів неоціненний.<br><a data-tooltip-position="top" aria-label="https://riscv.org/" rel="noopener nofollow" class="external-link is-unresolved" href="https://riscv.org/" target="_self">RISC-V</a> (вимовляється «ріск-файв») — найновіша і, можливо, найзначущіша розробка в сімействі RISC. Створена в UC Berkeley у 2010 році, RISC-V є повністю відкритою, безкоштовною архітектурою системи команд. Будь-хто може проєктувати, виробляти та продавати процесори RISC-V без сплати ліцензійних зборів.Ця відкритість — справжня революція. ARM стягує ліцензійні збори, які можуть сягати мільйонів доларів. x86 є власністю Intel та AMD. RISC-V повністю руйнує цю модель, функціонуючи як відкритий стандарт — подібно до того, як TCP/IP є відкритим мережевим протоколом.RISC-V швидко поширюється у:
вбудованих системах і пристроях IoT;
Європейській процесорній ініціативі для високопродуктивних обчислень;
китайських напівпровідникових компаніях, що шукають альтернативи архітектурам під контролем Заходу;
академічних дослідженнях і навчанні (замінюючи MIPS як основну ISA для підручників);
спеціалізованих прискорювачах для штучного інтелекту та машинного навчання.
RISC-V Foundation (нині RISC-V International) координує стандарт, забезпечуючи стабільність базової системи команд і водночас дозволяючи користувацькі розширення для спеціалізованих доменів. Базовий набір цілочислових інструкцій (RV32I) містить лише 47 інструкцій — навмисно мінімальну основу. Розширення додають операції з рухомою комою (F/D), атомарні операції (A), стиснені інструкції (C) та векторну обробку (V), але кожне розширення є необов'язковим. Проєктувальник чіпа обирає лише ті розширення, що потрібні його застосуванню, уникаючи «роздування набору інструкцій», від якого потерпали проєкти CISC.RISC-V є, напевно, найчистішим втіленням філософії RISC: почати з мінімуму, розширюватися лише за потреби та зберігати основу достатньо простою, щоб студент міг реалізувати робочий процесор за один семестр.Є третій підхід до складності інструкцій, який обирає принципово інший шлях. Архітектури VLIW (Very Long Instruction Word — дуже довге командне слово) об'єднують кілька незалежних операцій в одне дуже широке командне слово — часто 128 біт або ширше.У звичайному процесорі (CISC чи RISC) за визначення того, які інструкції можна виконувати паралельно, відповідає апаратне забезпечення. Це вимагає складних схем для перевірки залежностей, переупорядкування інструкцій і динамічного планування. VLIW обирає радикальну короткі́шу стежку: перенести всю цю складність у компілятор.Командне слово VLIW містить кілька слотів операцій. Компілятор аналізує програму, знаходить операції, що не мають залежностей одна від одної, і пакує їх в одне командне слово. Далі апаратура виконує всі операції у слові одночасно, не потребуючи логіки динамічного планування.<br><img alt="Командне слово VLIW — компілятор пакує кілька операцій в одне широке слово; апаратура виконує всі слоти паралельно" src="assets/l09-vliw-instruction-word-uk.svg" target="_self">Усі чотири операції виконуються в одному такті й надходять за одну вибірку інструкції.Підхід VLIW має чіткі компроміси:Переваги:
Простіша апаратура — немає логіки позачергового виконання, немає перейменування регістрів, немає динамічного планування. Заощаджений кремній можна витратити на більше функціональних блоків або більші кеші
Передбачувана продуктивність — компілятор уже визначив розклад виконання, тому продуктивність є детермінованою
Енергоефективність — простіша логіка керування означає менше енергії, витраченої на планування інструкцій
Недоліки:
Навантаження на компілятор — компілятор має знайти достатньо паралелізму, щоб заповнити слоти інструкцій; якщо не вдається, слоти заповнюються інструкціями NOP (порожня операція), марнуючи простір і пропускну здатність
Роздування коду — програми можуть бути дуже великими через незаповнені слоти інструкцій і широкий формат
Двійкова сумісність — якщо апаратура змінюється (наприклад, нове покоління додає більше функціональних блоків і ширші командні слова), старі програми треба перекомпільовувати
Погана обробка непередбачуваного коду — переходи, промахи кешу та інші події часу виконання важко оптимізувати на етапі компіляції
Найвідомішим VLIW-процесором був Intel Itanium (IA-64), розроблений спільно Intel та Hewlett-Packard наприкінці 1990-х років. Itanium став ставкою Intel на те, що VLIW (який Intel називав EPIC — Explicitly Parallel Instruction Computing) витіснить x86 у сегменті високопродуктивних серверів. Ідея була амбітною: покласти на компілятор важку роботу з пошуку паралелізму й завдяки цьому побудувати простішу, краще масштабовану апаратуру.Itanium зазнав комерційної невдачі. Компіляторам було важко видобути достатньо паралелізму з коду загального призначення, щоб заповнити широкі командні слова. Тим часом процесори x86 (особливо AMD Opteron) продовжували прискорюватися завдяки традиційним методам позачергового виконання. Itanium офіційно зняли з виробництва у 2021 році.Більшого успіху VLIW досяг у спеціалізованих доменах — зокрема в цифрових сигнальних процесорах (DSP), таких як сімейство Texas Instruments TMS320, де робоче навантаження є дуже регулярним і передбачуваним, і компіляторам легше ефективно заповнювати слоти інструкцій.Наступна таблиця підсумовує ключові відмінності між архітектурами CISC і RISC:Якщо CISC та RISC втілюють настільки різні філософії, то як так сталося, що архітектура x86 — квінтесенція CISC-проєктування — залишається конкурентоспроможною з ARM за чистою продуктивністю? Відповідь розкриває один із найважливіших уроків архітектури комп'ютерів: ISA — це інтерфейс, а реалізація може бути будь-якою.Починаючи з Intel Pentium Pro (1995) та AMD K5 (1996), процесори x86 втілили революційну внутрішню архітектуру. Процесор надає програмному забезпеченню традиційний CISC-інтерфейс x86 — інструкції змінної довжини, складні режими адресації, операнди в пам'яті в арифметичних операціях, — але всередині перетворює кожну CISC-інструкцію на одну або кілька RISC-подібних мікрооперацій (які називають micro-ops або μops).Ці мікрооперації — прості операції фіксованої довжини типу регістр-регістр, тобто саме такі, які архітектури RISC використовують нативно. Далі процесор виконує ці мікрооперації через RISC-подібний конвеєр із позачерговим виконанням, перейменуванням регістрів і спекулятивним виконанням.<br><img alt="Конвергенція CISC-RISC — одна складна інструкція x86 декодується у три RISC-подібні мікрооперації" src="assets/l09-cisc-risc-convergence-uk.svg" target="_self">Одна складна інструкція x86 перетворюється на три прості внутрішні операції — саме ті, які RISC-процесор вимагав би написати від самого початку. Система команд CISC тепер виконує роль лише формату стиснення для справжніх RISC-подібних операцій, які виконує апаратура.Конвергенція йде в обох напрямках. Сучасні процесори ARM вийшли за межі початкової простоти RISC:
ARMv8 (AArch64) має інструкції для криптографічного прискорення (AES, SHA), поліноміального множення та інших спеціалізованих операцій
ARM SVE (Scalable Vector Extension) додає складні інструкції векторної обробки
Процесори Apple серії M містять спеціалізований матричний співпроцесор (AMX) і нейронний движок, що розмивають межу між «простими» інструкціями RISC і складністю на рівні апаратного забезпечення
Дискусія «CISC проти RISC», яка колись була достатньо гарячою, щоб заповнювати академічні конференції й засідання рад директорів корпорацій, значною мірою розв'язалася завдяки конвергенції. Сучасні високопродуктивні процесори — номінально чи то CISC, чи то RISC — користуються здебільшого тими самими внутрішніми методами:
позачергове виконання — динамічне переупорядкування інструкцій, щоб функціональні блоки не простоювали;
передбачення переходів — вгадування, яким шляхом підуть умовні переходи, з точністю понад 95%;
глибокі конвеєри — від 15 до 25 стадій у сучасних проєктах;
великі кеші — кілька рівнів ієрархії кешу, часто загальним обсягом десятки мегабайтів;
суперскалярне виконання — видача кількох інструкцій за такт;
перейменування регістрів — відображення архітектурних регістрів у значно більший фізичний регістровий файл для усунення хибних залежностей.
Реальні відмінності в продуктивності між сучасними процесорами зумовлені мікроархітектурою — конкретними рішеннями щодо розмірів кешу, глибини конвеєра, конструкції передбачувача переходів, пропускної здатності підсистеми пам'яті та кількості виконавчих блоків. Два процесори з однаковою ISA (наприклад, Intel Core та AMD Ryzen, обидва x86) можуть мати кардинально різні характеристики продуктивності саме через мікроархітектурні відмінності.Вибір між ARM та x86 сьогодні визначається не стільки розрізненням CISC/RISC, скільки практичними чинниками: енергоефективність (історична сильна сторона ARM), програмна екосистема (величезна бібліотека скомпільованого програмного забезпечення x86), модель ліцензування (ARM ліцензує свої проєкти; x86 обмежений Intel та AMD) та оптимізація під конкретні робочі навантаження.Головний урок такий: архітектура системи команд — це рівень абстракції. Як будь-яка вдала абстракція, вона приховує реалізацію. А оскільки реалізації конвергували, колись чітка межа між CISC та RISC розмилася у спектр замість бінарного вибору.Розуміння розрізнення CISC/RISC усе ще важливе — не тому, що сучасні процесори чітко вписуються в одну категорію, а тому, що проєктні компроміси, які мотивували кожен із підходів (щільність коду проти ефективності конвеєра, складність апаратури проти складності компілятора, зручність для програміста проти оптимізації апаратури), повторюються в кожному поколінні обчислювальної техніки. Байдуже, чи проєктуєте ви крихітний мікроконтролер, чи процесор для дата-центру, — ви натрапите на те саме фундаментальне питання: де має лежати складність?---## 8. Таксономія Флінна — класифікація паралельних архітектурУпродовж цієї лекції ми розглядали, як різні архітектури системи команд організовують обчислення: як моделі RAM та RASP обробляють послідовності інструкцій, як стекові, акумуляторні та регістрові машини кодують операції, як філософії CISC і RISC формують конструкцію реальних процесорів. В основі всіх цих обговорень лежить одне спільне припущення: один процесор виконує один потік інструкцій над одним потоком даних. Але це припущення, хай яким природним воно здається, описує лише один із чотирьох можливих способів організації обчислень.Чому нас мають цікавити інші способи? Тому що тактові частоти окремих процесорів вийшли на плато ще з середини 2000-х років — це явище іноді називають «кінцем масштабування Деннарда» або, простіше, «стіною потужності» (power wall). Фізичні обмеження на розсіювання потужності й тепла означають, що просте прискорення одного ядра вже не є основним шляхом до вищої продуктивності. З 1970-х до початку 2000-х тактові частоти процесорів зростали приблизно за законом Мура — подвоюючись кожні 18–24 місяці. Але близько 2004–2005 років вони вперлися у стелю біля 4 ГГц. Вищі частоти вимагали більше енергії й виділяли більше тепла, а воно не встигало розсіюватися достатньо швидко, щоб чіп не перегрівався.Відповіддю індустрії став фундаментальний зсув: замість того щоб робити одне ядро швидшим, розмістити декілька ядер на одному чіпі. Перший масовий двоядерний процесор Intel (Pentium D) вийшов у 2005 році. До 2025 року навіть бюджетні ноутбуки мають 8-ядерні процесори, а серверні процесори високого класу — 128 і більше ядер. А отже, щоб повною мірою скористатися сучасним обладнанням, програмне забезпечення має бути паралельним — і щоб зрозуміти паралельне програмне забезпечення, нам потрібна система класифікації паралельної апаратури.<br>У 1966 році <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Michael_J._Flynn" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Michael_J._Flynn" target="_self">Майкл Флінн</a> — тоді в IBM, пізніше професор Стенфорда — запропонував напрочуд просту класифікацію комп'ютерних архітектур за двома вимірами: кількістю потоків інструкцій та кількістю потоків даних, які система здатна обробляти одночасно. Потік інструкцій — це послідовність операцій, які процесор вибирає та виконує; потік даних — послідовність операндів, над якими ці інструкції діють. Запитавши, чи є кожен із вимірів «одиничним» чи «множинним», Флінн отримав матрицю два на два з чотирма категоріями:<br><img alt="Матриця класифікації таксономії Флінна" src="assets/l09-flynn-taxonomy-table.png" target="_self">Ці чотири скорочення — SISD, MISD, SIMD та MIMD — стали стандартним словником для обговорення паралельних архітектур. Майже кожен підручник з організації комп'ютерів і паралельних обчислень бере таксономію Флінна за відправну точку, і це цілком обґрунтовано: хоча класифікацію запропонували майже шістдесят років тому, вона відображає ті фундаментальні конструктивні рішення, які постають перед архітекторами, щойно вони виходять за межі послідовного виконання. Таксономія не описує конкретних апаратних реалізацій; вона класифікує логічну організацію руху інструкцій і даних через систему.Флінн розширив таксономію у статті 1972 року, додавши підкатегорії (зокрема для SIMD), але чотири категорії верхнього рівня залишилися стабільними й загальновизнаними. Існують й інші таксономії — зокрема таксономія Данкана (1990), яка додає векторну обробку як окрему категорію, та класифікація Фенга, яка зосереджується на довжині слова й бітовому паралелізмі, — але таксономія Флінна залишається найпоширенішою класифікацією комп'ютерних архітектур як в академічному середовищі, так і в індустрії. Коли апаратні інженери, розробники компіляторів чи проєктувальники алгоритмів говорять про «SIMD» або «MIMD», вони користуються словником Флінна.Елегантність таксономії — в її простоті. Маючи лише два бінарні виміри, вона охоплює напрочуд широкий спектр архітектурних рішень. Водночас ця простота означає, що вона не здатна відобразити кожен нюанс — існують архітектури, які розмивають межі між категоріями, а сучасні системи часто поєднують декілька категорій одночасно (як ми побачимо в Розділі 10). Але як класифікація першого порядку вона виявилася напрочуд довговічною впродовж понад півстоліття.Перш ніж розглянути кожну категорію, корисно побачити, як вони пов'язані з концепціями, що їх ми вже розбирали в цій лекції:
SISD безпосередньо відповідає моделі фон Неймана / РАСП-машині з Розділів 2–3: один лічильник команд, один потік даних, послідовне виконання.
SIMD розширює шлях даних: та сама інструкція застосовується до кількох елементів даних одночасно, задіюючи паралелізм на рівні даних.
MISD реплікує шлях інструкцій: кілька потоків інструкцій обробляють ті самі дані — переважно задля відмовостійкості.
MIMD реплікує все: кілька незалежних процесорів, кожен зі своїм потоком інструкцій і потоком даних, що забезпечує найзагальнішу форму паралелізму.
Розгляньмо кожну з чотирьох категорій по черзі, починаючи з найвідомішої.SISD — це архітектура, яку ми вивчали протягом усього курсу. Один блок керування вибирає інструкції по одній з пам'яті. Кожна інструкція оперує одним елементом даних (або невеликою кількістю операндів) через один процесорний елемент. Результат записується назад у пам'ять або регістр, і блок керування вибирає наступну інструкцію. Це саме архітектура фон Неймана — а точніше, цикл вибірка-декодування-виконання кожного процесора від Intel 8086 до Pentium MMX.<br><img alt="Діаграма архітектури SISD" src="assets/l09-flynn-sisd.png" target="_self">Діаграма показує основну структуру: пул інструкцій (пам'ять) живить один процесорний елемент (PU), який читає з єдиного пулу даних (також пам'ять) і записує до нього. Маємо одну стрілку для інструкцій та одну стрілку для даних — звідси й назва «одна інструкція, одні дані».Потік виконання в SISD є строго послідовним у логічній моделі:Пул інструкцій ──→ [Блок керування] ──→ [Процесорний елемент] ←──→ Пул даних вибірка й декодув. виконання й збереження
На кожному кроці блок керування вибирає одну інструкцію, декодує її та спрямовує процесорний елемент для виконання операції над одним елементом даних. Процесорний елемент зчитує операнд(и) з пулу даних, виконує операцію та записує результат назад. Потім цикл повторюється з наступною інструкцією.Ключова характеристика SISD полягає в тому, що на концептуальному рівні існує один потік керування та один потік даних. Процесор усе ж може використовувати внутрішні техніки для прискорення виконання — конвеєризація перекриває етапи вибірки, декодування та виконання, щоб кілька інструкцій одночасно перебували на різних етапах, а суперскалярні конструкції видають більше однієї інструкції за такт. Ці техніки створюють паралелізм на рівні інструкцій (ILP), але не змінюють фундаментальну класифікацію SISD: так само маємо один потік інструкцій (послідовна програма) та один потік даних (послідовний потік операндів).Розглянемо конкретний приклад. Коли на Intel 8086 запускається асемблерна програма, процесор вибирає одну інструкцію, декодує її, виконує й переходить до наступної. Навіть на Pentium з його подвійними конвеєрами (U- та V-конвеєри) процесор усе одно виконує один потік інструкцій — він просто робить це ефективніше, перекриваючи етапи. Лічильник команд так само просувається через одну послідовність інструкцій.Приклади архітектур SISD: Intel 8086, 80286, 80386, 80486 та оригінальний Pentium — усі вони є SISD-машинами. Усі одноядерні процесори без розширень SIMD-інструкцій потрапляють до цієї категорії. Навіть ранні мейнфрейми — IBM 704, UNIVAC I — належать до SISD. По суті, якщо виконання машини можна описати як «одна інструкція за раз, один елемент даних за раз», це SISD, незалежно від того, чи використовує реалізація конвеєризацію та інші внутрішні оптимізації.Багатозадачність на SISD: Сучасні операційні системи виконують багато програм «одночасно» на одноядерному SISD-процесорі через перемикання контексту — ОС швидко чергує програми, надаючи кожній відрізок процесорного часу. Це створює ілюзію паралельного виконання, однак насправді маємо конкурентне (з чергуванням), а не паралельне (одночасне) виконання. У будь-який конкретний тактовий цикл виконується лише одна інструкція з однієї програми. Ця відмінність важлива: конкурентність — це керування кількома завданнями; паралелізм — це виконання кількох завдань одночасно.Для ілюстрації різниці уявімо одного кухаря на кухні, який готує дві страви. Кухар ріже овочі для страви А, потім помішує соус для страви Б, потім повертається до страви А — страви готуються конкурентно, але кухар робить лише одну дію за раз. Справжній паралелізм потребував би двох кухарів, кожен з яких одночасно працює над своєю стравою. SISD-процесор з операційною системою, що виконує перемикання контексту, — це модель з одним кухарем; для справжнього паралелізму кількох кухарів потрібне MIMD-обладнання.MISD — це найнезвичніша та найменш комерційно успішна з чотирьох категорій Флінна. В архітектурі MISD кілька процесорних елементів виконують різні потоки інструкцій, але всі оперують одним потоком даних. Дані проходять через процесорні елементи, і кожен застосовує своє власне перетворення.<br><img alt="Діаграма архітектури MISD" src="assets/l09-flynn-misd.png" target="_self">На діаграмі видно, що пул інструкцій живить два (або більше) окремі процесорні елементи, проте обидва працюють з одним і тим самим пулом даних. Кожен PU виконує свою програму, але обидва отримують ідентичні вхідні дані. Саме ця ознака відрізняє MISD від усіх інших категорій.Порівняйте з іншими трьома категоріями:
У SISD один PU виконує одну програму над одним потоком даних.
У SIMD кілька PU виконують одну й ту саму програму над різними даними.
У MIMD кілька PU виконують різні програми над різними даними.
У MISD кілька PU виконують різні програми над одними й тими самими даними.
MISD у певному сенсі є «протилежністю» SIMD: якщо SIMD застосовує одну інструкцію до багатьох потоків даних, то MISD застосовує багато інструкцій до одного потоку даних.На перший погляд, MISD здається дивним конструктивним рішенням — навіщо мати кілька процесорів, що працюють над одними й тими самими даними? Відповідь криється у двох нішевих, але важливих сферах застосування:Відмовостійкість через надлишковість. Коли потрібна абсолютна надійність — такого рівня, що одна помилка може бути катастрофічною, — можна поставити кілька незалежних процесорів, які виконують ті самі обчислення на тих самих вхідних даних, а потім порівнюють свої результати. Якщо всі процесори дають однаковий результат, його вважають надійним. Якщо ж один відрізняється, решта «переголосовує» його, і такий процесор позначають як несправний. Ця техніка називається потрійним модульним резервуванням (TMR) або, у загальнішому випадку, N-модульним резервуванням.Найвідоміший приклад — бортовий комп'ютер управління польотом Space Shuttle. Space Shuttle використовував п'ять резервних комп'ютерів загального призначення (GPC): чотири основні комп'ютери виконували ідентичне програмне забезпечення на тих самих даних із сенсорів, а п'ятий резервний працював на незалежно розробленому програмному забезпеченні як захист від систематичних програмних помилок. Чотири основні GPC голосували щодо кожного результату — якщо один з них давав інший результат, ніж три інші, його автоматично виводили з роботи, і система продовжувала працювати на решті трьох. У системі, де одна обчислювальна помилка могла призвести до втрати апарата та екіпажу, така надлишковість була безальтернативною. Підхід спрацював: за 135 місій Space Shuttle бортові комп'ютери управління польотом жодного разу не спричинили критичного для місії збою.<br>Систолічні масиви. <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Systolic_array" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Systolic_array" target="_self">Систолічний масив</a> — це мережа тісно з'єднаних процесорних елементів, через які дані протікають у ритмічному, пульсоподібному патерні (назва походить від систолічного ритму серця — дані «пульсують» через масив, як кров через систему кровообігу). Кожен елемент застосовує до даних свою операцію під час їхнього проходження. Уперше описані Г. Т. Кунгом та Чарльзом Лейзерсоном у 1978 році, систолічні масиви є ефективними для конкретних завдань — таких, як множення матриць, згортка та обробка сигналів, — де обчислення можна розкласти на регулярний конвеєр операцій. Дані протікають через масив, не потребуючи доступу до зовнішньої пам'яті: усі операнди та проміжні результати містяться всередині самих процесорних елементів, що усуває вузьке місце пам'яті.Систолічні масиви знайшли нову актуальність в епоху машинного навчання. Тензорні процесори (TPU) від Google, розроблені спеціально для інференсу та навчання нейронних мереж, внутрішньо використовують архітектуру систолічних масивів для операцій множення матриць — основних обчислень у глибокому навчанні.Варто зазначити, що класифікація систолічних масивів як MISD є дещо дискусійною. За строгим прочитанням таксономії Флінна, MISD вимагає, щоб ті самі дані оброблялися різними інструкціями. У систолічному ж масиві дані фактично трансформуються під час проходження через кожен процесорний елемент — тож другий елемент оперує не зовсім тими самими даними, що й перший. Деякі дослідники стверджують, що систолічні масиви слід класифікувати інакше (або ж що вони взагалі не піддаються чіткій класифікації). Це один із граничних випадків, на яких проста матриця два на два Флінна демонструє свої обмеження.Конвеєрна обробка та MISD. Деякі автори також класифікують глибокі інструкційні конвеєри як форму MISD, оскільки різні етапи конвеєра застосовують різні операції до даних тієї самої (оригінальної) інструкції під час їхнього проходження через конвеєр. Однак така інтерпретація не є загальноприйнятою — більшість авторів розглядають конвеєризацію як техніку реалізації в межах SISD, а не окрему архітектурну категорію. Ця розбіжність ілюструє важливий момент: таксономія Флінна класифікує архітектури на високому рівні абстракції, і щодо граничних випадків думки фахівців можуть розходитися.Чому MISD є рідкісною. Фундаментальна проблема MISD полягає в тому, що більшість обчислень природним чином не вписуються в модель, де ті самі дані мають одночасно оброблятися кількома різними інструкціями. На практиці SIMD (одна інструкція, різні дані) та MIMD (різні інструкції, різні дані) набагато ефективніше покривають переважну більшість потреб паралельних обчислень. MISD залишається категорією з підручників, яка важлива для повноти картини, але має дуже обмежене реальне застосування поза відмовостійкими системами та спеціалізованими апаратними прискорювачами. Деякі підручники навіть описують MISD як «теоретично можливу, але практично неіснуючу» — це перебільшення, якщо згадати приклад Space Shuttle, однак воно добре показує, наскільки рідкісними є реалізації MISD порівняно з SIMD та MIMD.SIMD — це категорія, в якій практичний вплив таксономії Флінна найпомітніший у повсякденних обчисленнях. В архітектурі SIMD одна інструкція транслюється до кількох процесорних елементів, і кожен елемент одночасно застосовує її до іншого елемента даних. Одна інструкція — багато елементів даних, що обробляються паралельно.<br><img alt="Діаграма архітектури SIMD" src="assets/l09-flynn-simd.png" target="_self">Інтуїція проста. Припустимо, потрібно поелементно додати два масиви з чотирьох чисел:A = [10, 20, 30, 40]
B = [ 1, 2, 3, 4]
C = A + B = [11, 22, 33, 44]
На SISD-процесорі довелося б виконати чотири окремі інструкції додавання: C[0] = A[0] + B[0], потім C[1] = A[1] + B[1], і так далі. Чотири інструкції, чотири тактові цикли (щонайменше).На SIMD-процесорі всі чотири елементи A завантажуються в один широкий регістр, усі чотири елементи B — в інший, а далі виконується одна інструкція векторного додавання. Одна інструкція, один тактовий цикл — чотири результати. Прискорення майже чотириразове.Ось як це виглядає в псевдокоді, порівнюючи скалярний (SISD) та SIMD-підходи:; Скалярний (SISD) підхід — 4 окремі операції:
LOAD R1, A[0] LOAD R1, A[1] LOAD R1, A[2] LOAD R1, A[3]
LOAD R2, B[0] LOAD R2, B[1] LOAD R2, B[2] LOAD R2, B[3]
ADD R3, R1, R2 ADD R3, R1, R2 ADD R3, R1, R2 ADD R3, R1, R2
STORE C[0], R3 STORE C[1], R3 STORE C[2], R3 STORE C[3], R3 ; SIMD-підхід — 1 паралельна операція:
VLOAD V1, A[0..3] ; Завантажити 4 значення у векторний регістр V1
VLOAD V2, B[0..3] ; Завантажити 4 значення у векторний регістр V2
VADD V3, V1, V2 ; Додати всі 4 пари одночасно
VSTORE C[0..3], V3 ; Зберегти всі 4 результати
Скалярний підхід потребує 16 інструкцій (4 завантаження + 4 завантаження + 4 додавання + 4 збереження). SIMD-підхід обходиться лише 4 векторними інструкціями. Паралелізм даних тут явний: та сама операція (додавання) однаково застосовується до всіх елементів.Такий патерн — застосування тієї самої операції до кожного елемента набору даних — надзвичайно поширений у реальних обчисленнях:
Обробка зображень: коригування яскравості шляхом додавання константи до кожного пікселя, застосування фільтра шляхом множення кожного пікселя на ядро згортки
Обробка звуку: підсилення шляхом множення кожного семплу на коефіцієнт підсилення, мікшування двох доріжок шляхом додавання відповідних семплів
3D-графіка: трансформація вершин шляхом множення кожної на матрицю перетворення, розрахунок освітлення шляхом обчислення скалярних добутків для кожної нормалі поверхні
Наукові обчислення: векторні та матричні операції у фізичних симуляціях, кліматичному моделюванні, динаміці рідин
Машинне навчання: тензорні операції — множення матриць ваг на вектори активацій, обчислення градієнтів на пакетах навчальних прикладів
SIMD створений саме для таких навантажень. Ключова ідея: паралелізм на рівні даних — та сама операція, рівномірно застосована до багатьох елементів даних — принципово відрізняється від паралелізму на рівні завдань (різні операції над різними даними, що є територією MIMD). SIMD використовує паралелізм даних, а його обсяг у сучасних навантаженнях величезний. Один кадр відео у 4K має понад 8 мільйонів пікселів; один шар нейронної мережі може включати множення матриць із мільйонами елементів. Коли операція однакова для кожного елемента, SIMD дає значне зростання пропускної здатності при мінімальній апаратній складності.SIMD у сучасних процесорах. Сьогодні кожен масовий настільний та мобільний процесор має можливості SIMD через розширення системи команд — додаткові інструкції, що оперують над широкими регістрами, які містять кілька елементів даних, упакованих поруч:Прогресія очевидна: регістри зросли з 64 біт (вісім упакованих байтів у MMX) до 512 біт (шістдесят чотири упакованих байти в AVX-512), а операції стали дедалі складнішими. Одна інструкція AVX-512 здатна додати шістнадцять 32-бітних чисел з рухомою комою одночасно. SVE (Scalable Vector Extension) від ARM застосовує інший підхід: довжина вектора може змінюватися між реалізаціями — процесор може мати 128-бітні, 256-бітні або навіть 2048-бітні вектори, і той самий код працює на всіх них без перекомпіляції, використовуючи предикатні маски для обробки векторів змінної довжини.Варто зазначити зв'язок з нашим попереднім обговоренням проєктування ISA: SIMD-розширення — це доповнення до існуючих систем команд. SSE та AVX від Intel — розширення CISC-архітектури x86; NEON та SVE від ARM — розширення RISC-архітектури ARM. Базова ISA виконує скалярні (по-одному) операції; SIMD-розширення виконує векторні (багато-за-раз) операції. Сучасний процесор x86, як-от Intel Core i7, є одночасно CISC-машиною (з погляду ISA), SISD-машиною (для скалярного коду) та SIMD-машиною (для векторизованого коду). Таксономія Флінна класифікує режим виконання, а не машину загалом.Історичний контекст. Як концепція, SIMD випереджає ці розширення системи команд на десятиліття. Історію SIMD-обладнання можна простежити через три окремі ери:<br>Ера 1: виділені SIMD-суперкомп'ютери (1970-ті–1990-ті). <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/ILLIAC_IV" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/ILLIAC_IV" target="_self">ILLIAC IV</a>, завершений у 1972 році, був масивним процесором із 64 процесорними елементами, які могли одночасно виконувати ту саму інструкцію над різними даними — чиста апаратна SIMD-машина. Thinking Machines <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Connection_Machine" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Connection_Machine" target="_self">Connection Machine</a> CM-2 (1987) довів це до 65 536 однобітних процесорів, кожен з яких виконував ту саму інструкцію над своїм бітом даних. Ці машини були потужними, але дорогими та складними в програмуванні. Зрештою їх витіснили дешевші кластери з масових процесорів.<br>Ера 2: векторні суперкомп'ютери (1970-ті–1990-ті). <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Cray-1" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Cray-1" target="_self">Cray-1</a> (1976) та його наступники використовували векторну обробку — оперування цілими векторами (масивами) даних за допомогою окремих інструкцій. Векторна обробка тісно пов'язана з SIMD, але технічно є окремою концепцією (відрізняється переважно наявністю інструкції «встановити довжину вектора»). Оригінальна таксономія Флінна передує Cray-1, тому векторна обробка явно не охоплена — таксономія Данкана (1990) заповнює цю прогалину.Ера 3: SIMD-розширення в масових процесорах (1990-ті–сьогодення). Сучасна ера виросла з ринку настільного мультимедіа. Коли ПК стали достатньо потужними для відео та аудіо в реальному часі в середині 1990-х, Intel представив MMX (1997) для прискорення мультимедійної обробки на наявних процесорах x86. Далі з'явилися SSE, AVX та їхні наступники, зробивши можливості SIMD повсюдними в споживчому обладнанні, а не обмеженими дорогими суперкомп'ютерами. Сьогодні навіть бюджетний смартфон містить ARM-процесори з SIMD-розширеннями NEON — паралелізм даних більше не розкіш.Обмеження SIMD. SIMD — не універсальне рішення. Воно найкраще працює, коли ту саму операцію треба застосувати до кожного елемента набору даних — умова, яку називають паралелізмом даних або регулярністю. Коли різні елементи потребують різних операцій (наприклад, розгалуження if-else, де деякі елементи йдуть шляхом if, а інші — шляхом else), продуктивність SIMD значно знижується. Процесор має або виконати обидва шляхи та замаскувати небажані результати (витрачаючи половину обчислень), або повернутися до скалярної обробки. Саме тому SIMD чудово працює з однорідними операціями (додати константу до кожного пікселя), але має труднощі з нерегулярними навантаженнями (обробити лише пікселі, що перевищують поріг, з різною логікою для кожного).Інше обмеження — складність коду. Написання SIMD-коду вручну — з використанням інтринсіків, що безпосередньо відповідають інструкціям процесора — дає високоефективний, але важкочитний код, прив'язаний до конкретного розширення системи команд. Сучасні компілятори здатні автоматично векторизувати певні цикли (автоматично перетворюючи скалярний код на SIMD), але це надійно працює лише для простих патернів циклів. Складні залежності даних, аліасинг вказівників та непослідовні патерни доступу до пам'яті можуть завадити компілятору ефективно векторизувати. Як наслідок, отримання максимальної продуктивності SIMD часто вимагає експертизи програміста та ретельного структурування коду.Автовекторизація на практиці. Сучасні компілятори (GCC, Clang, MSVC) здатні автоматично перетворювати скалярні цикли на SIMD-код через процес, який називають автовекторизацією. Розгляньмо простий цикл на C:// Скалярний код на C
for (int i = 0; i &lt; 1024; i++) { C[i] = A[i] + B[i];
}
З прапорцями оптимізації на кшталт -O3 або -ftree-vectorize компілятор здатен розпізнати, що кожна ітерація незалежна, та згенерувати SSE або AVX інструкції, які обробляють 4, 8 або 16 елементів за ітерацію замість одного. Програміст пише скалярний код; компілятор генерує SIMD-код. Це одна з найвпливовіших оптимізацій компілятора в сучасних обчисленнях — вона означає, що добре структурований код скористається з переваг SIMD-обладнання, а програмісту не доведеться писати жодної інтринсік-функції.Однак автовекторизація має обмеження. Цикли із залежностями даних між ітераціями (де ітерація i+1 залежить від результату ітерації i), цикли зі складними розгалуженнями або цикли, що звертаються до пам'яті через вказівники, які можуть бути аліасами (вказувати на ту саму адресу), часто не піддаються автовекторизації. У критичному щодо продуктивності коді програмісти іноді використовують специфічні для компілятора інтринсік-функції (як-от _mm256_add_ps для AVX) або OpenMP SIMD-прагми (#pragma omp simd), щоб гарантувати векторизацію.GPU як SIMD-двигуни. Сучасні графічні процесори (GPU) доводять концепцію SIMD до екстремального масштабу. Високопродуктивний GPU містить тисячі простих обчислювальних ядер, організованих у групи, які виконують ту саму інструкцію над багатьма елементами даних одночасно. Конкретна архітектура відрізняється — NVIDIA називає свій підхід SIMT (Single Instruction, Multiple Threads), AMD використовує гібрид SIMD+SIMT — однак фундаментальний принцип однаковий: масивний паралелізм даних через застосування однієї операції до багатьох елементів даних одночасно.Порівняймо масштаб. Сучасний високопродуктивний CPU має 8–24 ядра, кожне з яких виконує AVX-512 інструкції над 16 числами з рухомою комою одночасно — тобто приблизно 128–384 операції з рухомою комою за цикл. Сучасний високопродуктивний GPU, як-от NVIDIA H100, має понад 16 000 CUDA-ядер і виконує десятки тисяч операцій з рухомою комою за цикл. Компроміс у тому, що кожне ядро GPU набагато простіше за ядро CPU — воно не справляється ефективно зі складними розгалуженнями чи нерегулярними патернами доступу до пам'яті. Але для регулярних навантажень із паралелізмом даних масивний паралелізм GPU переконливо перемагає.Саме тому GPU стали домінуючим обладнанням для машинного навчання: навчання нейронної мережі включає мільярди операцій множення-накопичення над великими матрицями — навантаження, ідеально придатне для виконання в стилі SIMD. Розвиток глибокого навчання з 2012 року стимулював розвиток GPU не менше, ніж ігрова індустрія — ринкова капіталізація NVIDIA зросла з приблизно $10 мільярдів у 2012 році до понад $1 трильйона у 2024 році, значною мірою завдяки бізнесу GPU для дата-центрів.Взаємовідносини CPU-GPU ілюструють ширший принцип сучасних обчислень: гетерогенний паралелізм. Замість побудови одного типу процесора, який робить усе, сучасні системи поєднують різні типи процесорних елементів, оптимізованих для різних типів паралелізму. CPU обробляє складний, розгалужений, послідовний код (MIMD); GPU бере на себе масивні, однорідні, паралельні за даними навантаження (SIMD); а спеціалізовані прискорювачі (як-от TPU від Google чи Neural Engine від Apple) виконують специфічні операції (як-от множення матриць) ще ефективніше. Завдання програміста — відобразити кожну частину обчислень на найвідповідніше обладнання.MIMD — найзагальніша та найпотужніша з чотирьох категорій Флінна. У системі MIMD кілька процесорних елементів одночасно виконують різні інструкції над різними даними. Кожен процесор, по суті, автономний — він має власний потік інструкцій, власний потік даних і може виконувати цілком іншу програму, ніж будь-який інший процесор.<br><img alt="Діаграма архітектури MIMD" src="assets/l09-flynn-mimd.png" target="_self"><br>Кожен сучасний багатоядерний процесор, кожен багатопроцесорний сервер, кожен обчислювальний кластер та кожна розподілена система — це MIMD-архітектура. Коли на ноутбуці одночасно відкриті веббраузер, музичний плеєр та редактор коду, і кожна програма працює на окремому ядрі CPU, виконуючи різний код над різними даними — це MIMD у дії. Це домінуюча парадигма паралельних обчислень сьогодні — згідно з останніми рейтингами <a data-tooltip-position="top" aria-label="https://www.top500.org/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.top500.org/" target="_self">TOP500</a>, фактично всі найпотужніші суперкомп'ютери світу є MIMD-системами.Ключова різниця між MIMD та SIMD — автономність. У SIMD усі процесорні елементи крокують у ногу — вони виконують ту саму інструкцію, лише над різними даними. У MIMD кожен процесорний елемент незалежний — має власний лічильник команд, власний потік інструкцій та власні дані. Це робить MIMD набагато гнучкішою (будь-який процесор може робити будь-що в будь-який момент), але й складнішою для програмування (координація незалежних процесорів вимагає явної синхронізації).Архітектури MIMD поділяються на дві основні підкатегорії залежно від того, як процесори отримують доступ до пам'яті. Цей поділ, імовірно, важливіший за саму класифікацію SISD/SIMD/MISD/MIMD, оскільки він визначає, як програмісти пишуть паралельне програмне забезпечення:SMP — симетрична багатопроцесорність (спільна пам'ять)У системі SMP усі процесори поділяють єдиний фізичний адресний простір — усі вони можуть читати з однієї й тієї самої пам'яті та записувати в неї. «Симетрична» означає, що кожен процесор має рівний доступ до пам'яті та пристроїв введення-виведення; жоден процесор не є особливим. Це протиставлено асиметричній багатопроцесорності, де один процесор діє як головний, а інші — як підлеглі — конструкція, поширена у 1980-х та на початку 1990-х, але здебільшого витіснена SMP. Приклади: кожен сучасний багатоядерний процесор Intel Core та AMD Ryzen є SMP-системою. Чотириядерний процесор має чотири незалежні ядра, кожне зі своїм кешем L1 та L2, що спільно використовують кеш L3 та основну пам'ять. З погляду програміста, усі ядра бачать ту саму пам'ять. Серверні процесори, як-от Intel Xeon чи AMD EPYC, можуть мати 64, 96 або навіть 128 ядер в одному роз'ємі, і всі вони працюють як SMP-система. Переваги: спільна пам'ять робить програмування відносно простим. Потоки в тому самому процесі обмінюються даними, читаючи та записуючи спільні змінні. Операційні системи прозоро розподіляють потоки між ядрами. Мови програмування, як-от C, C++, Java та Python, надають бібліотеки потоків (pthreads, std::thread, java.lang.Thread), що природно відображаються на SMP-обладнання. Виклики: центральний виклик SMP — когерентність кеша. Коли Ядро 0 записує значення за адресою X у пам'яті, цей запис спочатку оновлює кеш L1 Ядра 0. Але Ядро 1 може мати застарілу копію X у своєму власному кеші L1. Якщо Ядро 1 прочитає X, не знаючи про запис Ядра 0, воно отримає застарілі дані — потенційно катастрофічна помилка. Апаратні протоколи (як-от MESI — Modified, Exclusive, Shared, Invalid) забезпечують узгодженість усіх кешів, відстежуючи стан кожного рядка кеша на всіх ядрах та інвалідуючи або оновлюючи копії під час запису. Цей трафік когерентності обмежує масштабованість: кожен запис до спільної змінної потенційно ініціює повідомлення інвалідації до кешу кожного іншого ядра. За межами приблизно 64–128 ядер накладні витрати на підтримку синхронізації всіх кешів стають вузьким місцем. Саме тому окремі машини зі спільною пам'яттю рідко перевищують кілька сотень ядер. <br>
Модель програмування: потоки обмінюються даними через спільні змінні, захищені примітивами синхронізації (м'ютекси, семафори, атомарні операції). Інструменти на кшталт <a data-tooltip-position="top" aria-label="https://www.openmp.org/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.openmp.org/" target="_self">OpenMP</a> дають змогу програмістам розпаралелювати цикли за допомогою простих директив компілятора. MPP — масивно-паралельна обробка (розподілена пам'ять)У системі MPP кожен процесор (або вузол) має свою власну приватну пам'ять. Процесори не можуть безпосередньо звертатися до пам'яті один одного — вони обмінюються даними, надсилаючи повідомлення через високошвидкісну мережу з'єднань. Це принципово відрізняється від SMP: замість читання спільної змінної процесор має явно надіслати іншому процесору повідомлення («ось дані, які вам потрібні») і чекати на відповідь.
<br>
Приклади: суперкомп'ютери, як-от <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Frontier_(supercomputer)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Frontier_(supercomputer)" target="_self">Frontier</a> (перша екзамасштабна система з понад 9 400 вузлами, кожен з яких містить процесор AMD EPYC та чотири GPU AMD Instinct), хмарні обчислювальні кластери (тисячі серверів, з'єднаних через високошвидкісний Ethernet або InfiniBand) та розподілені системи на кшталт кластерів Hadoop і Spark. Навіть університетська комп'ютерна лабораторія з 30 машинами, що виконують паралельне завдання через мережу, є невеликою MPP-системою. <br>
Переваги: розподілена пам'ять масштабується набагато краще за спільну. Оскільки немає спільної шини чи протоколу когерентності кеша, можна з'єднати десятки тисяч або навіть мільйони процесорів. <a data-tooltip-position="top" aria-label="https://www.olcf.ornl.gov/frontier/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.olcf.ornl.gov/frontier/" target="_self">Суперкомп'ютер Frontier</a> в Національній лабораторії Оук-Ридж досягає понад 1 екзаФЛОПС (10^18 операцій з рухомою комою на секунду), з'єднуючи тисячі вузлів, кожен з яких має власні CPU та GPU. Виклики: програмування систем із розподіленою пам'яттю значно складніше за програмування спільної пам'яті. Процесори мають явно надсилати та отримувати повідомлення для обміну даними, а програміст — керувати розподілом даних, патернами комунікації та балансуванням навантаження. Затримка мережі (час, за який повідомлення подорожує між вузлами) та пропускна здатність (обсяг даних, що може передаватися за секунду) стають критичними чинниками продуктивності. Погано спроєктований патерн комунікації легко зводить нанівець усі переваги паралелізму — якщо процесори проводять більшу частину часу в очікуванні повідомлень, а не в обчисленнях, система працює не швидше за одну машину. Топології з'єднань: те, як процесори з'єднані один з одним, має величезне значення. Поширені топології — мережі типу «сітка» (процесори розташовані в 2D або 3D ґратці, кожен з'єднаний зі сусідами), тороїдні мережі (сітка з обгортковими з'єднаннями на краях), «товсті дерева» (ієрархічне дерево комутаторів зі зростаючою пропускною здатністю до кореня) та гіперкуби (кожен із 2^N процесорів з'єднується з N іншими). Вибір топології впливає як на затримку (скільки стрибків має зробити повідомлення), так і на пропускну здатність (скільки даних може текти одночасно без перевантаження). <br>
Модель програмування: домінуючим стандартом для наукових обчислень є <a data-tooltip-position="top" aria-label="https://www.mpi-forum.org/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.mpi-forum.org/" target="_self">MPI (Message Passing Interface)</a> — специфікація бібліотеки, що надає функції на кшталт MPI_Send та MPI_Recv для явної міжпроцесної комунікації. Написання ефективних MPI-програм вимагає ретельного продумування того, як розбити дані та мінімізувати комунікацію — програміст має явно вирішити, які дані надходять до якого процесора і коли процесорам треба обмінюватися інформацією. Фреймворки вищого рівня, як-от MapReduce (та його реалізації Hadoop і Spark), абстрагують частину цієї складності для навантажень обробки даних, надаючи простішу модель програмування: програміст пише функцію «map» (застосовану до кожного елемента даних незалежно) та функцію «reduce» (що об'єднує результати), а фреймворк автоматично обробляє розподіл даних, комунікацію та відмовостійкість. SPMD та MPMD — моделі програмування в межах MIMD. Окрім апаратного розмежування між спільною та розподіленою пам'яттю, MIMD-системи відрізняються також тим, як програми організовані між процесорами. Два поширені патерни: SPMD (Single Program, Multiple Data — одна програма, багато даних): усі процесори виконують ту саму програму, але кожен оперує над іншою підмножиною даних і в будь-який момент може перебувати в іншій точці програми. Це найпоширеніша модель для наукових паралельних обчислень. Наприклад, симуляція погоди може виконувати той самий код динаміки рідин на кожному процесорі, де кожен процесор відповідає за інший географічний регіон. SPMD не слід плутати з SIMD — у SPMD кожен процесор виконує код незалежно (процесори не йдуть у ногу) і можуть обирати різні гілки в коді залежно від своїх локальних даних. MPMD (Multiple Program, Multiple Data — багато програм, багато даних): різні процесори виконують різні програми. Наприклад, у клієнт-серверній системі сервер виконує одну програму, а клієнти — іншу. У конвеєрі Unix на кшталт cat file.txt | grep "error" | sort | uniq чотири окремі програми виконуються одночасно над різними даними — це MPMD. Це програмні патерни, що виконуються на MIMD-обладнанні. Більшість великомасштабних паралельних застосунків використовує SPMD, бо це спрощує розробку — одна програма пишеться й розгортається на тисячах процесорів, і кожний екземпляр працює зі своєю локальною частиною даних.SMP порівняно з MPP:Гібридні системи. На практиці сучасні високопродуктивні системи поєднують обидві моделі. Вузол суперкомп'ютера зазвичай містить SMP-багатоядерний процесор (спільна пам'ять усередині вузла), з'єднаний з іншими вузлами через високошвидкісну мережу (розподілена пам'ять між вузлами). Програмування таких систем часто передбачає гібридний підхід: OpenMP для паралелізму всередині вузла та MPI для комунікації між вузлами. Крім того, кожен вузол може містити GPU, які працюють як SIMD-двигуни.Тож один сучасний суперкомп'ютер одночасно задіює кілька рівнів таксономії Флінна:
SIMD усередині кожного ядра GPU (та в AVX-блоках кожного ядра CPU) — одна інструкція, багато елементів даних
SMP/MIMD усередині кожного чіпа CPU — кілька незалежних ядер зі спільною пам'яттю
MPP/MIMD між вузлами — тисячі незалежних машин, що обмінюються повідомленнями
Це багатошарова ієрархія паралелізму, яку таксономія Флінна 1966 року передбачила своєю структурою, навіть якщо конкретні реалізації з'явилися через десятиліття. Розуміння цієї ієрархії потрібне кожному, хто хоче писати високопродуктивне програмне забезпечення — програміст має вирішити, який рівень паралелізму використовувати, і кожен рівень має свої моделі програмування, свої характеристики продуктивності та свої компроміси.Практичний приклад: навчання великої мовної моделі (як-от GPT або Llama) потребує розподілу обчислень між тисячами GPU. Усередині кожного GPU виконання в стилі SIMD обробляє множення матриць. Між GPU в межах одного сервера (зазвичай 4–8 GPU, з'єднаних через високошвидкісний інтерконект на кшталт NVLink) паралелізм спільної пам'яті в стилі SMP координує роботу. Між серверами в дата-центрі (сотні або тисячі вузлів, з'єднаних через InfiniBand) передача повідомлень у стилі MPP синхронізує оновлення градієнтів. Програміст (або, частіше, фреймворк глибокого навчання на кшталт PyTorch чи JAX) має керувати всіма трьома рівнями одночасно. Це реальність сучасних паралельних обчислень — і таксономія Флінна дає словник для її опису та осмислення.Наступна таблиця підсумовує чотири категорії, їхні характеристики та актуальність для сучасних обчислень:Таблиця робить очевидним ключовий момент: чотири категорії представлені в сучасному обладнанні нерівномірно. SISD та MISD мають переважно історичний та теоретичний інтерес, тоді як SIMD та MIMD домінують у сучасних обчисленнях. Утім, розуміння всіх чотирьох категорій потрібне для усвідомлення чому SIMD та MIMD перемогли — вони забезпечують найкорисніші форми паралелізму для найширшого кола застосувань.З цього підсумку випливає кілька спостережень: SISD — це минуле. Жоден сучасний процесор загального призначення не є чисто SISD. Навіть найпростіші сьогоднішні CPU мають SIMD-розширення (NEON на ARM, SSE на x86), і більшість із них — багатоядерні (MIMD). SISD залишається важливою як концептуальний базовий рівень — це модель фон Неймана, відносно якої оцінюють усі паралельні архітектури. MISD — це курйоз. Вона має справжні застосування у відмовостійкості та певному спеціалізованому обладнанні (систолічні масиви в TPU), але ніколи не стала масовою комерційною архітектурою. Знати про неї треба, однак безпосередньо програмувати для неї навряд чи доведеться. SIMD — скрізь. Щоразу, коли телефон обробляє фотографію, ноутбук відтворює відео чи дата-центр навчає модель машинного навчання, основну роботу виконує SIMD-обладнання. Перехід від скалярних до SIMD-обчислень — одна з найважливіших оптимізацій продуктивності в сучасних обчисленнях. MIMD — це фундамент. Усі хмарні обчислення, усі багатоядерні процесори, усі суперкомп'ютери та всі розподілені системи є MIMD. Уся сучасна обчислювальна екосистема — від двоядерного ноутбука до екзамасштабного суперкомп'ютера — побудована на моделі MIMD. Реальні системи поєднують категорії. Сучасний ноутбук із багатоядерним CPU та інтегрованим GPU одночасно задіює SIMD (у векторних блоках кожного ядра та шейдерах GPU) і MIMD (між незалежними ядрами). Суперкомп'ютер додає MIMD із розподіленою пам'яттю між тисячами вузлів. Категорії Флінна не є взаємовиключними на рівні системи — вони описують режими виконання, що співіснують в одній машині. Таксономія Флінна витримала випробування часом, бо ставить правильне запитання на правильному рівні абстракції: не «наскільки швидкий цей процесор?» чи «скільки транзисторів він має?», а «як інструкції та дані рухаються через систему?» Це запитання залишається настільки ж актуальним сьогодні, як і 1966 року.Якщо подивитися на траєкторію обчислень, тенденція безсумнівна: від SISD (ера одноядерних процесорів, 1940-ті–2000-ні) до SIMD+MIMD (ера багатоядерних процесорів та GPU, 2005–сьогодення), а далі — до дедалі більш гетерогенних систем, що поєднують різні типи процесорних елементів — ядра CPU, ядра GPU, нейронні прискорювачі, FPGA-структури — на одному чіпі. Процесори серії M від Apple, наприклад, містять ядра CPU загального призначення (MIMD), GPU (SIMD), Neural Engine (подібний до SIMD) та спеціалізовані кодеки/декодери медіа — усе на одному шматку кремнію. Розуміння таксономії Флінна дає концептуальну рамку для осмислення цього різноманіття, незалежно від того, які конкретні продукти чи архітектури з'являться в найближчі роки.Ось хронологія, яка розміщує таксономію Флінна в контексті історії обчислень:Жодна ера не замінила попередню — вона нашарувалася зверху. Сучасні системи використовують техніки з кожної ери: конвеєризацію та суперскалярне виконання (з ери SISD), векторні/SIMD-блоки (з ери суперкомп'ютерів), багатопроцесорність зі спільною пам'яттю (з ери SMP) та розподілені обчислення (з ери кластерів). Таксономія Флінна дає спільну мову для обговорення всіх цих технік. РАМ-машина та РАСП-машина — фундаментальні абстракції. РАМ-машина (програма відокремлена від даних, у стилі Гарвардської архітектури) та РАСП-машина (програма зберігається в пам'яті, у стилі архітектури фон Неймана) забезпечують формальну основу для аналізу складності алгоритмів і для розуміння реальних комп'ютерних архітектур. ISA (архітектура системи команд) — це контракт між апаратним і програмним забезпеченням. Вона визначає типи даних, регістри, режими адресації, формати команд і сам набір команд. Усе, що знаходиться над ISA (компілятори, ОС), залежить від цього інтерфейсу; усе, що нижче (мікроархітектура, схеми), — реалізує його. Розташування операндів визначає формат команди. Стекові (безадресні), акумуляторні (одноадресні), регістр-пам'ять (двоадресні), регістр-регістр (load-store, триадресні) та пам'ять-пам'ять архітектури по-різному балансують щільність коду, складність апаратури та продуктивність. CISC проти RISC — це філософія проєктування, а не дихотомія. CISC надає потужні команди змінної довжини (x86); RISC надає прості команди фіксованої довжини, що забезпечують ефективну конвеєризацію (ARM, RISC-V). Сучасні процесори розмивають цю межу: процесори x86 внутрішньо декодують CISC-команди у RISC-подібні мікрооперації. Таксономія Флінна класифікує паралелізм за двома осями. SISD (класична послідовна обробка), SIMD (векторні операції, GPU), MISD (рідкісний випадок, відмовостійкі системи), MIMD (багатоядерні та розподілені системи) описують взаємодію потоків команд і даних. Сучасні системи переважно належать до MIMD із розширеннями SIMD. Що далі: Лекція 10 оглядає сучасні спеціалізовані архітектури: GPU, DSP, ASIC, FPGA, TPU, нейроморфні процесори, а також знайомить із засадами квантових обчислень. Лабораторні роботи продовжать застосовувати концепції асемблера x86 з попередніх лекцій. Подумайте: Як поділ на CISC і RISC впливає на асемблерний код, який ви писали в лабораторних роботах? До якої категорії таксономії Флінна належить процесор вашого ноутбука? (Підказка: це MIMD із розширеннями SIMD.) Чому RISC-підхід ARM переміг на ринку мобільних пристроїв, тоді як CISC-підхід x86 досі домінує на настільних комп'ютерах? <br>
<a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Random-access_machine" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Random-access_machine" target="_self">Машина з довільним доступом — Wikipedia</a> — формальне визначення, системи команд та зв'язок з іншими моделями обчислень <br>
<a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Random-access_stored-program_machine" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Random-access_stored-program_machine" target="_self">Машина з довільним доступом і збереженою програмою — Wikipedia</a> — визначення моделі РАСП та її порівняння з РАМ <br>
<a data-tooltip-position="top" aria-label="https://uk.wikipedia.org/wiki/%D0%90%D1%80%D1%85%D1%96%D1%82%D0%B5%D0%BA%D1%82%D1%83%D1%80%D0%B0_%D1%81%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D0%B8_%D0%BA%D0%BE%D0%BC%D0%B0%D0%BD%D0%B4" rel="noopener nofollow" class="external-link is-unresolved" href="https://uk.wikipedia.org/wiki/%D0%90%D1%80%D1%85%D1%96%D1%82%D0%B5%D0%BA%D1%82%D1%83%D1%80%D0%B0_%D1%81%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D0%B8_%D0%BA%D0%BE%D0%BC%D0%B0%D0%BD%D0%B4" target="_self">Архітектура системи команд — Вікіпедія</a> — вичерпний огляд ISA (українською) <br>
<a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Complex_instruction_set_computer" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Complex_instruction_set_computer" target="_self">Комп'ютер зі складною системою команд — Wikipedia</a> — історія CISC, характеристики та приклади <br>
<a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Reduced_instruction_set_computer" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Reduced_instruction_set_computer" target="_self">Комп'ютер зі скороченою системою команд — Wikipedia</a> — філософія RISC, принципи проєктування та реалізації <br>
<a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Flynn%27s_taxonomy" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Flynn%27s_taxonomy" target="_self">Таксономія Флінна — Wikipedia</a> — класифікація паралельних архітектур <br>
<a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/List_of_products_using_ARM_processors" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/List_of_products_using_ARM_processors" target="_self">Перелік продуктів на процесорах ARM — Wikipedia</a> — докладний перелік пристроїв на базі ARM ]]></description><link>lectures/l09.-проєктування-isa-та-моделі-обчислень.html</link><guid isPermaLink="false">Lectures/L09. Проєктування ISA та моделі обчислень.md</guid><pubDate>Mon, 20 Apr 2026 10:37:40 GMT</pubDate><enclosure url="assets/l09-lecture-outline-uk.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l09-lecture-outline-uk.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L10-NOTES. Огляд сучасних архітектур]]></title><description><![CDATA[Короткий довідник до Лекції 10. Повні матеріали: L10. Огляд сучасних архітектур.md.Ключова ідея: кінець масштабування Деннарда (~2004) та стіна потужності (плато близько 4 ГГц) змусили індустрію вийти за межі багатоядерності (обмеженої законом Амдала) до предметно-орієнтованих архітектур — апаратури, яка робить менше, зате за меншу кількість тактів і з меншою енергією на операцію.Фундаментальний компроміс: універсальність проти ефективності — ЦП загального призначення витрачає транзистори на гнучкість, тоді як спеціалізований чіп використовує кожен транзистор для цільового навантаження.Дізнатися більше: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Dennard_scaling" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Dennard_scaling" target="_self">Масштабування Деннарда (Wikipedia)</a> | <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Amdahl%27s_law" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Amdahl%27s_law" target="_self">Закон Амдала (Wikipedia)</a>Що це: процесори, оптимізовані для обробки сигналів у реальному часі з детермінованою поведінкою (конвеєр АЦП → обробка → ЦАП).Ключові архітектурні особливості: Модифікована Гарвардська архітектура — окремі шини інструкцій і даних для паралельного доступу до пам'яті Одноциклова операція MAC (множення-накопичення) — Acc = Acc + (A × B) — основна операція DSP (FIR-фільтри, FFT, згортка, множення матриць) Апаратні цикли з нульовими накладними витратами — без DEC/JNZ; лічильником циклу керує спеціалізована апаратура Насичена арифметика — переповнення обмежується максимумом замість обгортання (запобігає клацанням в аудіо та збоям у системах керування) Акцент на арифметику з фіксованою комою — цілочисельна апаратура для MAC простіша, швидша та менш енергоємна за FPU Циклічні буфери (DAG) — апаратне обгортання вказівників для алгоритмів ковзного вікна, без перевірок меж DMA (прямий доступ до пам'яті) — автономні передачі введення-виведення до пам'яті без участі ЦП SHARC (Super Harvard): кеш інструкцій звільняє програмну шину як третю шину даних під час циклів; 6–7 паралельних операцій за такт.Приклад продуктивності: 100-відвідний FIR-фільтр — понад 1000 тактів на ЦП проти ~105 тактів на DSP (≈1 відвід за такт).Застосування: смартфони (ехокомпенсація, кодеки), базові станції 5G, радар/сонар, слухові апарати, керування двигунами.<br>Дізнатися більше: <a data-tooltip-position="top" aria-label="https://www.dspguide.com/ch28/3.htm" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.dspguide.com/ch28/3.htm" target="_self">Посібник з архітектури DSP</a> | <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Texas_Instruments_TMS320" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Texas_Instruments_TMS320" target="_self">TMS320 (Wikipedia)</a>Філософія проєктування: максимізувати пропускну здатність (операцій/с), а не мінімізувати затримку (час на операцію). Тисячі простих ядер із виконанням за порядком; ~60–70 % кристала CPU — це кеші та блоки керування, тоді як GPU віддає більшість транзисторів під АЛП.SIMT (одна інструкція, багато потоків): програміст пише скалярний код; апаратура групує потоки у варпи по 32, які виконуються синхронно. Дивергенція варпу на розгалуженнях серіалізує шляхи — ключова проблема продуктивності GPU.Ієрархія пам'яті (на SM): регістри ~256 КБ (1 такт) → спільна пам'ять / L1 128–228 КБ (~25 тактів) → L2 40–60 МБ (~200 тактів) → глобальна HBM 16–80 ГБ (~500 тактів). Спеціалізовані ядра: тензорні ядра (матричний FMA 4×4, ~30× для ML), RT-ядра (трасування променів).SIMD проти SIMT проти SMT: SIMD — жорсткі векторні операції з ручним маскуванням (CPU SSE/AVX); SIMT — скалярний код з апаратно керованою дивергенцією варпу (GPU); SMT (одночасна багатопотоковість) — повністю незалежні потоки без штрафу за дивергенцію (CPU Hyper-Threading).Модель CUDA: хост (CPU) запускає ядра на пристрої (GPU). Ієрархія: сітка → блоки (≤1024 потоків, спільний SM) → варпи (32) → потоки. Альтернативи: OpenCL, ROCm, oneAPI, Metal.<br>Дізнатися більше: <a data-tooltip-position="top" aria-label="https://learnopencv.com/modern-gpu-architecture-explained/" rel="noopener nofollow" class="external-link is-unresolved" href="https://learnopencv.com/modern-gpu-architecture-explained/" target="_self">Посібник з архітектури GPU</a> | <a data-tooltip-position="top" aria-label="https://www.yosefk.com/blog/simd-simt-smt-parallelism-in-nvidia-gpus.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.yosefk.com/blog/simd-simt-smt-parallelism-in-nvidia-gpus.html" target="_self">Аналіз SIMD/SIMT/SMT</a> | <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/CUDA" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/CUDA" target="_self">CUDA (Wikipedia)</a>Що це: спеціалізований кремній для однієї фіксованої функції — кожен транзистор присвячено цільовому навантаженню.Еволюція майнінгу Bitcoin ілюструє спектр спеціалізації: CPU (20–30 МХ/с) → GPU (500–800 МХ/с) → FPGA (подібна швидкість, краща ефективність) → ASIC (65 ГХ/с+, нині сотні ТХ/с).Три підходи до проєктування:Переваги: у 100–10 000× швидше за CPU; найкраща продуктивність на ват; малий форм-фактор; низька вартість за великих обсягів.Недоліки: високі одноразові витрати на проєктування — NRE (мільйони на 7 нм / 5 нм); 12–24 місяці розробки; нульова гнучкість після виготовлення; потрібні спеціалізовані інженери з апаратного забезпечення.Поширені приклади: модеми смартфонів, ISP камер, нейронні рушії, контролери SSD, маршрутизація пакетів, автомобільні ECU.<br>Дізнатися більше: <a data-tooltip-position="top" aria-label="https://anysilicon.com/ultimate-guide-asic-application-specific-integrated-circuit/" rel="noopener nofollow" class="external-link is-unresolved" href="https://anysilicon.com/ultimate-guide-asic-application-specific-integrated-circuit/" target="_self">Повний посібник з ASIC</a>Що це: Реконфігуровне апаратне забезпечення — пристрій просторових обчислень, який є схемою (без вибірки та декодування інструкцій). Функцію задає конфігураційний бітовий потік, завантажений у SRAM.Чотири будівельні блоки: CLB (конфігуровані логічні блоки) → LUT (таблиці підстановки — k-вхідна SRAM, що зберігає будь-яку булеву таблицю істинності, зазвичай 6-вхідна) → програмована матриця маршрутизації (60–80% площі кристала) → блоки введення-виведення. Сучасні FPGA містять жорсткі IP-блоки: BRAM, DSP-секції, PLL, ядра ARM.Потік програмування: HDL (Verilog/VHDL) → синтез → розміщення та маршрутизація → бітовий потік → завантаження у FPGA (хвилини–години проти місяців для ASIC).Точка перетину «вартість — обсяг»: до ~10 000–100 000 одиниць дешевшим є FPGA; понад цей обсяг виграє ASIC.<br>Дізнатися більше: <a data-tooltip-position="top" aria-label="https://learn.sparkfun.com/tutorials/how-does-an-fpga-work/all" rel="noopener nofollow" class="external-link is-unresolved" href="https://learn.sparkfun.com/tutorials/how-does-an-fpga-work/all" target="_self">Як працює FPGA?</a>Нагадування з L09: компілятор пакує кілька незалежних операцій в одну широку інструкцію (64–1024 біти). Апаратура виконує всі слоти паралельно, без динамічного планування. Коли паралелізму недостатньо, порожні слоти заповнюються NOP-ами.Transmeta Crusoe (2000): внутрішній 128-бітний VLIW із програмним шаром Code Morphing — динамічна бінарна трансляція з x86 у VLIW. Проєкт зазнав невдачі, щойно Intel підвищив енергоефективність x86. Спадщина: передбачив трансляцію в мікрооперації у сучасних x86-процесорах.Intel Itanium / IA-64 / EPIC: 128-бітні пакети з 3 операцій плюс біти шаблону. Предикатне виконання, спекулятивні завантаження, ротація регістрів. Комерційна невдача (~$10 млрд): компіляторам не вдавалося видобути достатньо ILP із серверного коду; роздування коду в 2–4×; x86-64 зробив архітектуру непотрібною. Випуск припинено 2021 року.Де VLIW працює — DSP: TI TMS320C6x виконує 8 операцій за такт. Навантаження DSP мають регулярні цикли, передбачуваний доступ до даних і рясний паралелізм, видимий компілятору. Також: Qualcomm Hexagon, деякі старіші архітектури шейдерів GPU AMD.<br>Дізнатися більше: <a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/very-long-instruction-word-vliw-architecture/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/very-long-instruction-word-vliw-architecture/" target="_self">Архітектура VLIW</a> | <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Transmeta" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Transmeta" target="_self">Transmeta (Wikipedia)</a>Мотивація: У висновуванні й навчанні нейронних мереж домінує множення матриць. Енергія переміщення даних значно перевищує енергію обчислень (~200× для доступу до DRAM проти 8-бітного множення). TPU усуває накладні витрати завдяки систолічній матриці.Систолічна матриця (TPU v1 MXU): 256×256 = 65 536 MAC. Ваги нерухомі; активації рухаються зліва направо; часткові суми накопичуються вниз. Кожне значення перевикористовується 256 разів. 90% площі кристала — це обчислення (проти ~30% для GPU).Знижена точність: INT8 (v1), BFloat16 (v2+), FP8 (v6+). 8-бітний помножувач = ~1/16 площі й ~1/30 енергії FP32. Нейронні мережі стійкі до округлення.Tesla DOJO D1: 354 ядра RISC-V, 2 ГГц, 362 ТФЛОПС BF16, 440 МБ SRAM. Без когерентності кешу та віртуальної пам'яті — програмно керована розподілена SRAM. Training Tile: 25 кристалів D1, 9 ПФЛОПС. ExaPod: 120 плиток, 20 ЕФЛОПС. Сумісний з PyTorch, незалежний від CUDA.<br>Дізнатися більше: <a data-tooltip-position="top" aria-label="https://blog.bytebytego.com/p/how-googles-tensor-processing-unit" rel="noopener nofollow" class="external-link is-unresolved" href="https://blog.bytebytego.com/p/how-googles-tensor-processing-unit" target="_self">Як працює TPU від Google</a> | <a data-tooltip-position="top" aria-label="https://www.nextplatform.com/2022/08/23/inside-teslas-innovative-and-homegrown-dojo-ai-supercomputer/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.nextplatform.com/2022/08/23/inside-teslas-innovative-and-homegrown-dojo-ai-supercomputer/" target="_self">Всередині Tesla DOJO</a>Зміна парадигми: обчислення відбуваються там, де зберігаються дані — немає вузького місця фон Неймана. Кожен синапс — це одночасно обчислення і пам'ять. Керовані подіями: енергію споживають лише нейрони, які генерують імпульси.Імпульсні нейронні мережі (SNN): нейрони накопичують заряд (мембранний потенціал), генерують імпульс, коли поріг перевищено, потім — скидання та витік. Моделюється за допомогою Leaky Integrate-and-Fire (LIF): V[t] = V[t-1] + Σ(A·w·s) - λ. Навчання через STDP (пластичність, залежна від часу імпульсів) — локальне правило, без потреби у зворотному поширенні.Intel Loihi 2 (2021): 1 млн нейронів на чіп, ~1 Вт, програмовані моделі нейронів, фреймворк з відкритим кодом Lava. Loihi 1: покращення показника енергія × затримка у 5 000 разів на розрідженому кодуванні порівняно з CPU/GPU. Hala Point (2024): 1 152 чіпи, 1,15 млрд нейронів.IBM TrueNorth (2014): 4 096 ядер, 1 млн нейронів, 256 млн синапсів, ~10 Вт, дизайн GALS (без глобального тактового сигналу). NorthPole (2023): у 4 000 разів швидший за TrueNorth; у 22 рази швидший за GPU при у 25 разів меншому споживанні енергії; усі ваги — на кристалі.Застосування: Автономні дрони, периферійний ШІ (постійне розпізнавання ключових слів), робототехніка, розпізнавання часових патернів.Обмеження: відставання за точністю на 1–3% порівняно з DNN; відсутність стандартних інструментів (немає «CUDA для нейроморфних»); вузька перевага над GPU/TPU для пакетних навантажень.<br>Дізнатися більше: <a data-tooltip-position="top" aria-label="https://www.ibm.com/think/topics/neuromorphic-computing" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.ibm.com/think/topics/neuromorphic-computing" target="_self">Що таке нейроморфні обчислення? (IBM)</a> | <a data-tooltip-position="top" aria-label="https://open-neuromorphic.org/neuromorphic-computing/hardware/loihi-intel/" rel="noopener nofollow" class="external-link is-unresolved" href="https://open-neuromorphic.org/neuromorphic-computing/hardware/loihi-intel/" target="_self">Intel Loihi</a> | <a data-tooltip-position="top" aria-label="https://open-neuromorphic.org/blog/truenorth-deep-dive-ibm-neuromorphic-chip-design/" rel="noopener nofollow" class="external-link is-unresolved" href="https://open-neuromorphic.org/blog/truenorth-deep-dive-ibm-neuromorphic-chip-design/" target="_self">Глибокий аналіз TrueNorth</a>Три ключові концепції: Суперпозиція — кубіт перебуває у стані α|0⟩ + β|1⟩ (справді в обох станах одночасно до моменту вимірювання). Заплутаність — корельовані пари кубітів; вимірювання одного визначає стан іншого. Інтерференція — амплітуди правильних відповідей складаються конструктивно, а хибні — взаємно гасяться.Апаратне забезпечення: надпровідні кубіти в кріостатах розчинення (~15 мК). Альтернативи: захоплені іони, фотонні, топологічні (Microsoft, рання стадія розробки). Вентилі: Адамара (суперпозиція), CNOT (заплутаність), фазові (інтерференція).Ключові алгоритми: Алгоритм Шора — факторизація цілих чисел за поліноміальний час, ламає RSA (2048 біт: класично — більше за вік Всесвіту; квантово — години). Алгоритм Гровера — пошук у невідсортованому масиві за O(√N) замість O(N) (для 1 трильйона елементів: ~1 млн кроків проти 1 трлн). Не універсальне прискорення — немає виграшу для сортування, пошуку у відсортованому масиві та більшості графових алгоритмів.Центральна проблема: корекція помилок. Похибка квантового вентиля ~0,1–1% — проти ~10⁻¹⁸ у класичних. Один логічний кубіт може потребувати сотень або тисяч фізичних. IBM планує 200 логічних кубітів до 2029 року. Стратегія «збирай зараз — дешифруй потім» змушує вже сьогодні переходити на постквантову криптографію (NIST затвердив перші стандарти у 2024 році).<br>Дізнатися більше: <a data-tooltip-position="top" aria-label="https://www.ibm.com/think/topics/quantum-computing" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.ibm.com/think/topics/quantum-computing" target="_self">Що таке квантові обчислення? (IBM)</a> | <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Shor%27s_algorithm" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Shor%27s_algorithm" target="_self">Алгоритм Шора (Wikipedia)</a> | <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Grover%27s_algorithm" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Grover%27s_algorithm" target="_self">Алгоритм Гровера (Wikipedia)</a>Ключові закономірності: (1) гнучкість і продуктивність обернено пов'язані; (2) межі розмиваються — GPU отримують тензорні ядра, CPU містять SIMD, FPGA включають блоки ШІ; (3) жодна архітектура не виграє всюди — тому сучасні системи гетерогенні; (4) зрілість програмної екосистеми важить так само, як і характеристики апаратури.<br>Дізнатися більше: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Computer_Architecture:_A_Quantitative_Approach" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Computer_Architecture:_A_Quantitative_Approach" target="_self">Тюрінгівська лекція Хеннессі та Паттерсона</a> | <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Dennard_scaling" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Dennard_scaling" target="_self">Масштабування Деннарда (Wikipedia)</a> Спеціалізація перемагає універсальність — для чітко визначених навантажень предметно-орієнтовані архітектури випереджають CPU на порядки. Простір проєктування — це неперервний спектр — від CPU (повністю гнучкий) через FPGA (реконфігуровна) до ASIC (повністю фіксована); GPU, DSP, VLIW та TPU посідають проміжні позиції. Паралелізм виявляється у багатьох формах — SIMD у DSP, SIMT у GPU, систолічні матриці у TPU, імпульсні мережі в нейроморфних чіпах, квантова суперпозиція. Архітектура пам'яті важлива не менше, ніж обчислення — гарвардська (DSP), HBM (GPU/TPU), розподілена SRAM (DOJO), обчислення в пам'яті (нейроморфні чіпи) — кожна по-своєму долає вузьке місце фон Неймана. Майбутнє — за гетерогенністю — сучасні системи поєднують кілька типів архітектур; добір правильного прискорювача під конкретне навантаження важить не менше, ніж написання ефективного коду. ]]></description><link>lectures/l10-notes.-огляд-сучасних-архітектур.html</link><guid isPermaLink="false">Lectures/L10-NOTES. Огляд сучасних архітектур.md</guid><pubDate>Mon, 20 Apr 2026 10:37:40 GMT</pubDate></item><item><title><![CDATA[L09-NOTES. Проєктування ISA та моделі обчислень]]></title><description><![CDATA[Короткий довідник до Лекції 9. Повні матеріали: L09. Проєктування ISA та моделі обчислень.md.Призначення: формальні математичні моделі, що визначають, чим є «обчислювальна машина», абстрагуючись від деталей реалізації.Усі наведені нижче моделі еквівалентні за Тюрінгом — вони обчислюють один і той самий клас функцій. Відмінності стосуються зручності й ефективності, а не виразної сили.Ключова прогресія: кожен крок додає можливість, що наближає модель до реальної апаратури. Машина Тюрінга → регістри (лічильникова машина) → непряма адресація + акумулятор (РАМ-машина) → збережена програма (РАСП-машина).Дізнатися більше:
<a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Church%E2%80%93Turing_thesis" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Church%E2%80%93Turing_thesis" target="_self">Теза Черча — Тюрінга (Wikipedia)</a>
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Turing_machine" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Turing_machine" target="_self">Машина Тюрінга (Wikipedia)</a>
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Counter_machine" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Counter_machine" target="_self">Лічильникова машина (Wikipedia)</a>
<br>Визначення: Абстрактна <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Register_machine" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Register_machine" target="_self">регістрова машина</a> з довільним доступом (за сталий час) до будь-якого регістра.Компоненти: необмежена кількість регістрів (r1 — акумулятор), фіксована програма (окрема таблиця інструкцій лише для читання), лічильник команд, вхідна стрічка (лише для читання), вихідна стрічка (лише для запису).Система команд: LOAD/STORE, ADD/SUB/MULT/DIV (усі працюють через акумулятор), READ/WRITE (стрічки введення-виведення), JUMP/JZERO/JGTZ, HALT. Кожна арифметична операція використовує акумулятор як неявне джерело та неявний приймач.Режими адресації:
Пряма (LOAD 5): доступ до регістра r5 (як mov eax, [var])
Непряма (LOAD *5): використовує значення в r5 як адресу (як mov eax, [ebx]) — саме це робить РАМ-машину потужнішою за лічильникову
Моделі вартості: рівномірна (1 одиниця на операцію — стандарт для аналізу алгоритмів) та логарифмічна (вартість пропорційна розрядності операнда).Ключова властивість: програма відокремлена від даних (Гарвардська архітектура). Не можна ані модифікувати власну програму, ані зберігати адреси повернення.Дізнатися більше:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Random-access_machine" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Random-access_machine" target="_self">Машина з довільним доступом (Wikipedia)</a>
Визначення: РАМ-машина, у якій програма зберігається в регістрах разом із даними — архітектура фон Неймана. «Універсальна машина Тюрінга, побудована на шасі РАМ-машини.»Наслідки збереженої програми: підпрограми, самомодифікований код, програми як дані (завантажувачі ОС, інтерпретатори, JIT-компілятори), а також ризики безпеки (переповнення буфера).Вузьке місце фон Неймана: інструкції та дані конкурують за одну шину. Сучасні ЦП використовують модифіковану Гарвардську архітектуру на рівні кешу (роздільні L1I/L1D), зберігаючи для програміста єдиний адресний простір.Дізнатися більше:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Random-access_stored-program_machine" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Random-access_stored-program_machine" target="_self">РАСП-машина (Wikipedia)</a>
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Von_Neumann_architecture#Von_Neumann_bottleneck" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Von_Neumann_architecture#Von_Neumann_bottleneck" target="_self">Вузьке місце фон Неймана (Wikipedia)</a>
<br>Визначення: Програмний інтерфейс ЦП — контракт між програмним і апаратним забезпеченням. Уперше сформульований Фредом Бруксом для IBM <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/IBM_System/360" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/IBM_System/360" target="_self">System/360</a> (1960-ті).ISA визначає: типи даних, регістри, модель пам'яті, режими адресації, формати інструкцій (фіксована чи змінна довжина), набір інструкцій, модель переривань/виключень, модель введення-виведення.Ключові властивості:
Двійкова сумісність — програми працюють на будь-якій реалізації ISA
Розширюваність — зворотно сумісні доповнення (x86 -&gt; IA-32 -&gt; x86-64 -&gt; SSE -&gt; AVX)
ISA проти мікроархітектури — ISA визначає, що машина робить; мікроархітектура — як саме вона це робить
Реалізації: покоління ЦП (від 8086 до Core i9), виробники-конкуренти (Intel проти AMD), емулятори (DOSBox), віртуальні машини (JVM).Дві осі класифікації: (1) за розташуванням операндів, (2) за складністю інструкцій.Дізнатися більше:
<br><a data-tooltip-position="top" aria-label="https://uk.wikipedia.org/wiki/%D0%90%D1%80%D1%85%D1%96%D1%82%D0%B5%D0%BA%D1%82%D1%83%D1%80%D0%B0_%D1%81%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D0%B8_%D0%BA%D0%BE%D0%BC%D0%B0%D0%BD%D0%B4" rel="noopener nofollow" class="external-link is-unresolved" href="https://uk.wikipedia.org/wiki/%D0%90%D1%80%D1%85%D1%96%D1%82%D0%B5%D0%BA%D1%82%D1%83%D1%80%D0%B0_%D1%81%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D0%B8_%D0%BA%D0%BE%D0%BC%D0%B0%D0%BD%D0%B4" target="_self">Архітектура системи команд (Wikipedia, UK)</a>
A = B + C для всіх п'яти типів:
Стекова (нульадресна): PUSH B / PUSH C / ADD / POP A — 4 інструкції
Акумуляторна (одноадресна): LOAD B / ADD C / STORE A — 3 інструкції
Регістр-пам'ять (двоадресна): MOV AX,[B] / ADD AX,[C] / MOV [A],AX — 3 інструкції
Load-store (триадресна): LW $t0,B / LW $t1,C / ADD $t2,$t0,$t1 / SW $t2,A — 4 інструкції
Пам'ять-пам'ять (триадресна): ADD [A],[B],[C] — 1 інструкція
Ключовий висновок: більша кількість інструкцій не означає повільнішу роботу. Load-store потребує 4 інструкцій проти 1 в архітектурі пам'ять-пам'ять, але виграє на практиці, адже прості інструкції фіксованої довжини ефективно конвеєризуються.Конвергенція індустрії: домінують як архітектури регістр-пам'ять (x86), так і регістр-регістр (ARM, RISC-V). Стекові машини збереглися у віртуальних машинах (JVM, .NET, WebAssembly). Сучасний x86 внутрішньо розкладає інструкції на RISC-подібні мікрооперації.
CISC (комп'ютер зі складною системою команд) — покласти на апаратне забезпечення максимум роботи на одну інструкцію (подолати семантичний розрив).
RISC (комп'ютер зі скороченою системою команд) — спростити кожну інструкцію задля прискорення всього конвеєра (Девід Паттерсон, 1980).
VLIW (дуже довге командне слово) — об'єднати кілька незалежних операцій в одну широку інструкцію; планування виконує компілятор.
Приклади VLIW: Intel Itanium (комерційно не злетів, знятий з виробництва 2021 року), Elbrus, TI TMS320 DSP. Переваги: простіше апаратне забезпечення, передбачувана продуктивність. Недоліки: компілятор має сам виявляти паралелізм; інструкції NOP марно займають місце; відсутня двійкова сумісність між поколіннями.
x86 (ззовні CISC, всередині RISC): починаючи з Pentium Pro (1995 рік), процесори x86 усередині транслюють CISC-інструкції в RISC-подібні мікрооперації. CISC-ISA нині виконує радше роль формату стиснення коду.
ARM ускладнюється: ARMv8 додає криптографічні інструкції, векторні розширення SVE, співпроцесор Apple AMX.
Що важить сьогодні: мікроархітектура (кеш, глибина конвеєра, передбачення переходів, позачергове виконання), а не ярлик «CISC чи RISC».
Дізнатися більше:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Complex_instruction_set_computer" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Complex_instruction_set_computer" target="_self">CISC (Wikipedia)</a>
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Reduced_instruction_set_computer" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Reduced_instruction_set_computer" target="_self">RISC (Wikipedia)</a>
<br><a data-tooltip-position="top" aria-label="https://riscv.org/" rel="noopener nofollow" class="external-link is-unresolved" href="https://riscv.org/" target="_self">RISC-V</a> — відкрита ISA без ліцензійних відрахувань; базовий набір RV32I містить лише 47 інструкцій
<br>Запропонована <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Michael_J._Flynn" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Michael_J._Flynn" target="_self">Майклом Флінном</a> у 1966 році. Вона класифікує архітектури за кількістю потоків інструкцій та потоків даних:
Класичне послідовне виконання за архітектурою фон Неймана.
Один блок керування, один обчислювальний блок, один потік даних.
Конвеєризація та суперскалярність — це внутрішні оптимізації, а не зміна категорії.
Приклади: Intel 8086, 80486, одноядерні ЦП без SIMD-розширень.
Багатозадачність на SISD — це конкурентність (почерговість), а не паралелізм (одночасність). Кілька процесорів виконують різні інструкції над тими самими даними.
Найрідкісніша категорія; застосовується для відмовостійкості (потрійне модульне резервування).
Приклади: бортовий комп'ютер Space Shuttle (п'ять резервних GPC голосували за результатами), систолічні матриці (Google TPU використовує їх для матричного множення). Одна інструкція застосовується до багатьох елементів даних одночасно.
Використовує паралелізм даних (одна операція над кожним елементом).
Ідеально підходить для обробки зображень та аудіо, 3D-графіки, наукових обчислень і машинного навчання.
SIMD у сучасних ЦП: розширення x86 (MMX 64-біт → SSE 128-біт → AVX 256-біт → AVX-512 512-біт), розширення ARM (NEON 128-біт, SVE 128–2048-біт).
GPU — це SIMD екстремального масштабу (NVIDIA H100: понад 16 000 ядер CUDA).
Автовекторизація: компілятори здатні перетворювати скалярні цикли на SIMD за ключем -O3. Кілька незалежних процесорів, кожен із власним потоком інструкцій та потоком даних.
Панівна парадигма для всіх сучасних паралельних обчислень.
Два підтипи за моделлю пам'яті:
Виклик SMP: когерентність кешу (протокол MESI) обмежує масштабованість приблизно 64–128 ядрами.
Виклик MPP: явний розподіл даних і комунікація; мережева затримка.
Гібридні системи: сучасні суперкомп'ютери поєднують SMP усередині вузлів, MPP між вузлами та SIMD у блоках GPU й AVX.
Дізнатися більше:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Flynn%27s_taxonomy" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Flynn%27s_taxonomy" target="_self">Таксономія Флінна (Wikipedia)</a> РАМ-машина та РАСП-машина — фундаментальні абстракції. РАМ відповідає Гарвардській архітектурі (програма відокремлена від даних); РАСП — архітектурі фон Неймана (збережена програма). Це основа для аналізу алгоритмів та проєктування реальних комп'ютерів. ISA — контракт між апаратним і програмним забезпеченням. Визначає типи даних, регістри, режими адресації та формати інструкцій. Усе, що вище, бачить ISA; усе, що нижче, реалізує її. Розташування операндів формує дизайн ISA. Стекова, акумуляторна, регістр-пам'ять, load-store та пам'ять-пам'ять архітектури знаходять компроміс між щільністю коду, складністю апаратного забезпечення та конвеєризацією. CISC проти RISC — це філософія, а не бінарний вибір. Сучасний x86 (CISC) внутрішньо декодує інструкції у RISC-подібні мікрооперації; ARM (RISC) додає дедалі складніші розширення. Таксономія Флінна класифікує паралелізм. SISD (послідовний), SIMD (векторний/GPU), MISD (рідкісний, відмовостійкий), MIMD (багатоядерний/розподілений). Сучасні системи — це MIMD у поєднанні з SIMD. Лекція 10: сучасні спеціалізовані архітектури — GPU, DSP, ASIC, FPGA, TPU, нейроморфні процесори та засади квантових обчислень.
Лабораторні роботи продовжують застосування концепцій асемблера x86 з попередніх лекцій.
Питання для роздумів:
Як розрізнення CISC/RISC впливає на асемблерний код, який пишемо на лабораторних?
До якої категорії таксономії Флінна належить процесор звичайного ноутбука? (Підказка: MIMD із SIMD-розширеннями.)
Чому RISC-підхід ARM переміг на мобільному ринку, тоді як CISC-підхід x86 домінує на десктопах?
]]></description><link>lectures/l09-notes.-проєктування-isa-та-моделі-обчислень.html</link><guid isPermaLink="false">Lectures/L09-NOTES. Проєктування ISA та моделі обчислень.md</guid><pubDate>Mon, 20 Apr 2026 10:37:40 GMT</pubDate></item><item><title><![CDATA[L05. Організація програми — стек і процедури]]></title><description><![CDATA[
Лекція 5 курсу «Архітектура комп'ютерних систем» у НаУКМА.
Після цієї лекції ви зможете:
Описати структуру програми TASM (сегменти .data, .code, .bss, .stack) та пояснити, як програма завантажується в пам'ять
Читати й інтерпретувати послідовності байтів машинного коду x86, зокрема кодування коду операції та збереження параметрів у порядку little-endian
Пояснити, як вказівник інструкцій (IP) просувається під час виконання програми і як JMP використовує відносні зміщення
Оголошувати одновимірні та двовимірні масиви в асемблері і звертатися до них за допомогою db, dup та обчислення зміщень
Пояснити принцип роботи стека (LIFO) та його апаратну реалізацію через регістри SS і SP
Застосовувати інструкції PUSH і POP, щоб зберігати та відновлювати значення регістрів
Описати, як CALL і RET реалізують виклики функцій через стек
Пояснити угоди про виклик (протокол C/C++): передавання параметрів, повернення значень та будову фрейму стека (BP)
Ця лекція є ключовим моментом у нашому вивченні архітектури комп'ютерних систем. Ми переходимо від розуміння окремих інструкцій та базових операцій мови асемблера до дослідження того, як повноцінні програми організовуються, зберігаються в пам'яті та виконуються процесором.Назва лекції містить українську ідіому «Крапки над ї», що означає «уточнити дрібні деталі» або «розставити всі крапки над і». Вона відображає наш педагогічний підхід: ми повернемося до кількох фундаментальних тем з попередніх лекцій — структури програми, двійкового кодування, систем числення та логічних операцій, — але вже з глибшим розумінням, необхідним, щоб побачити, як ці концепції працюють разом у реальних програмах.У попередніх лекціях ми розглядали інструкції асемблера ізольовано: як MOV передає дані, як ADD виконує арифметику, як умовні переходи змінюють потік виконання програми. Але реальні програми — це не просто послідовності інструкцій, а структуровані сутності з окремими ділянками для коду, даних і стану часу виконання. Розуміння цієї організації необхідне для: Керування пам'яттю: різні типи вмісту програми (виконувані інструкції, ініціалізовані дані, змінні часу виконання) потребують різного поводження з боку операційної системи та процесора. Виклику процедур: сучасне програмне забезпечення побудовано з функцій і процедур. Стек забезпечує механізм виклику процедур, передачі параметрів, збереження стану та повернення результатів. Поведінки під час виконання: коли ми компілюємо та запускаємо програму, асемблер та операційна система виконують складну послідовність операцій, щоб перетворити вихідний код на виконувані байти в оперативній пам'яті, ініціалізувати сегментні регістри та розпочати виконання. Налагодження та оптимізації: розуміння того, як програми розміщуються в пам'яті та як працює стек, є критично важливим для діагностики помилок і підвищення продуктивності. Ця лекція організована у чотири основні розділи:Розділи 1–2: «Крапки над ї» в основах асемблера — повернемося до структури програми й розглянемо, як програми TASM (Turbo Assembler) організовуються у сегменти (.data, .code, .stack), як ці сегменти компілюються в байт-коди та як готова програма завантажується в оперативну пам'ять з ініціалізацією сегментних регістрів. Також уточнимо адресацію в реальному режимі, яку ми ввели в Лекції 3, — але тепер розглянемо її в повному контексті.Розділ 3: Масиви у пам'яті — розглянемо, як неперервні структури даних зберігаються та використовуються в пам'яті, спираючись на розуміння режимів адресації з Лекції 4.Розділ 4: Стек у сучасних процесорах — введемо стек, фундаментальну структуру даних, яку використовують усі сучасні процесори. Розглянемо принцип LIFO (Last-In-First-Out, «останній прийшов — перший вийшов»), роль сегмента стека (SS) та вказівника стека (SP), напрямок зростання стека, а також основні операції PUSH і POP.Розділ 5: Реалізація функцій за допомогою стека — дослідимо, як функції мов високого рівня реалізуються через процедури асемблера. Сюди входять інструкції CALL та RET, адреси повернення, фрейми стека, базовий вказівник (BP), локальні змінні, параметри та угоди про виклик, як-от cdecl.Наприкінці цієї лекції ви зможете:
пояснити, як програми TASM структуруються на сегменти та як ці сегменти відображаються у пам'яті;
описати процес компіляції та завантаження, що перетворює вихідний код асемблера на виконувані байти в оперативній пам'яті;
розуміти роль сегментних регістрів (CS, DS, SS) в адресації реального режиму;
пояснити принцип LIFO та те, як стек зростає в пам'яті;
правильно використовувати операції PUSH та POP у програмах на асемблері;
реалізовувати виклики процедур за допомогою інструкцій CALL та RET;
розуміти, як фрейми стека підтримують локальні змінні та передачу параметрів;
застосовувати угоди про виклик, щоб писати коректні й сумісні процедури.
Розпочнемо з розгляду структури програм TASM та їхнього завантаження в пам'ять.Типова програма TASM (Turbo Assembler) — це не просто пласка послідовність інструкцій. Натомість вона організована в окремі сегменти, кожен з яких виконує свою функцію. Така сегментована структура відображає те, як процесор 8086 керує пам'яттю, і допомагає операційній системі належним чином обробляти різні типи вмісту програми.Базова програма TASM складається з чотирьох основних сегментів:
Сегмент .data — Ініціалізовані дані (змінні з початковими значеннями)
Сегмент .code — Виконувані інструкції програми
Сегмент .bss — Зарезервовані неініціалізовані дані (буферний простір)
Сегмент .stack — Явна ініціалізація стека
Розгляньмо мінімальний шаблон програми TASM, щоб побачити ці сегменти в дії:.model small
.stack 100h ; Резервуємо 256 байтів для стека .data
message db 'Hello, World!$'
counter dw 0 .bss
buffer db 100 dup(?) ; Резервуємо 100 неініціалізованих байтів .code
main proc mov ax, @data ; Ініціалізуємо DS для вказівки на сегмент даних mov ds, ax ; Код програми тут mov ah, 09h ; Функція DOS для виведення рядка lea dx, message int 21h mov ax, 4C00h ; Функція DOS для завершення програми int 21h
main endp
end main
Сегмент .data містить змінні та константи з явними початковими значеннями. Коли асемблер обробляє цей сегмент, він генерує конкретні значення байтів, які буде записано у пам'ять під час завантаження програми.Наприклад, розгляньмо такі оголошення даних:.data
message db 'Hello$' ; 6 байтів: 48 65 6C 6C 6F 24 (ASCII-коди)
number dw 1234h ; 2 байти: 34 12 (формат little-endian)
array db 10, 20, 30 ; 3 байти: 0A 14 1E
Асемблер перетворює кожне оголошення на конкретні байти:
db 'Hello$' стає послідовністю байтів: 48h, 65h, 6Ch, 6Ch, 6Fh, 24h (ASCII-значення символів H, e, l, l, o, $)
dw 1234h стає: 34h, 12h (зверніть увагу на порядок байтів little-endian — молодший байт першим, як ми розглядали в Лекції 3)
db 10, 20, 30 стає: 0Ah, 14h, 1Eh (десяткові значення, переведені в шістнадцяткові)
Під час завантаження програми ці байти копіюються в оперативну пам'ять за адресою, яку визначає регістр DS (сегмент даних).Сегмент .code містить виконувані інструкції програми. Кожну інструкцію мови асемблера асемблер транслює в машинний код — послідовність байтів, яку процесор може безпосередньо виконати.Розгляньмо таку просту послідовність коду:.code
main proc mov ax, 1234h ; B8 34 12 (код операції B8, безпосереднє значення 34 12) add ax, 5 ; 05 05 00 (код операції 05, безпосереднє значення 05 00) ret ; C3 (код операції C3)
main endp
Асемблер генерує такі байт-коди:
mov ax, 1234h → B8 34 12 (B8 — код операції для «MOV AX, безпосереднє значення», за яким йде 16-бітне значення 34h 12h у форматі little-endian)
add ax, 5 → 05 05 00 (05 — код операції для «ADD AX, безпосереднє значення», за яким йде 05h 00h)
ret → C3 (однобайтовий код операції для повернення)
Як ми обговорювали в Лекції 4, процесор отримує ці байти з пам'яті (за адресою CS:IP), декодує їх, визначає операцію та виконує її. Після кожного виконання вказівник інструкцій (IP) автоматично переходить до наступної інструкції.Сегмент .bss (Block Started by Symbol — історична назва з ранніх асемблерів) призначений для оголошення неініціалізованих даних — буферного простору, який знадобиться під час виконання, але не потребує конкретних початкових значень..bss
buffer db 256 dup(?) ; Резервуємо 256 байтів, неініціалізовані
input dw ? ; Резервуємо 2 байти, неініціалізовані
Ключова перевага .bss — ефективність: асемблеру не потрібно генерувати 256 байтів нулів для buffer. Натомість він лише позначає, що треба зарезервувати 256 байтів. Під час завантаження програми операційна система виділяє цей простір (зазвичай ініціалізований нулями) без збільшення розміру виконуваного файлу.Сегмент .stack явно резервує простір для стека програми. Як ми детально розглянемо в Розділі 4, стек — це критична структура даних, яка обслуговує виклик процедур, локальні змінні та тимчасове зберігання..stack 100h ; Резервуємо 256 (100h) байтів для стека
Ця директива наказує асемблеру виділити 256 байтів пам'яті для операцій стека. Регістр SS (сегмент стека) вказуватиме на цю область, а SP (вказівник стека) буде ініціалізований на вершину цього простору.Розуміння того, як програма TASM перетворюється з вихідного коду на виконувані байти, необхідне для налагодження та оптимізації. Процес складається з кількох окремих фаз:Коли запускаємо асемблер (наприклад, tasm program.asm), він виконує такі операції: Розбір: асемблер читає вихідний файл, виокремлюючи директиви (.data, .code), мітки (main, message) та інструкції (mov, add). Побудова таблиці символів: він створює таблицю символів, яка ставить у відповідність кожній мітці її зміщення у відповідному сегменті. Наприклад, якщо message — перша змінна в .data, вона отримує зміщення 0. Якщо counter йде після неї (а message займає 7 байтів), counter отримує зміщення 7. Кодування інструкцій: кожна інструкція асемблера перетворюється на еквівалент машинного коду: Вибір коду операції: асемблер добирає відповідний байт (або байти) коду операції для кожної інструкції.
Кодування операндів: операнди (регістри, адреси пам'яті, безпосередні значення) кодуються відповідно до формату інструкції.
Розв'язання адрес: посилання на мітки розв'язуються в конкретні зміщення. Генерація об'єктного файлу: асемблер створює об'єктний файл (.OBJ) із закодованими сегментами, інформацією про переміщення та таблицею символів. Компонувальник (наприклад, tlink program.obj) об'єднує один або кілька об'єктних файлів у єдиний виконуваний файл: Об'єднання сегментів: якщо компонуються кілька об'єктних файлів, їхні сегменти .data об'єднуються, сегменти .code об'єднуються тощо. Виправлення адрес: міжсегментні посилання (наприклад, код, що звертається до даних) розв'язуються в остаточні зміщення. Генерація виконуваного файлу: компонувальник створює файл .EXE або .COM з усіма сегментами та належними заголовками для завантажувача операційної системи. Коли ми запускаємо програму (наприклад, program.exe), завантажувач DOS або Windows виконує такі операції: Виділення пам'яті: операційна система виділяє блок пам'яті, достатній для розміщення всіх сегментів, а також префікса сегмента програми (PSP — Program Segment Prefix) — 256-байтової структури керування. Завантаження сегментів: вміст кожного сегмента з виконуваного файлу копіюється в оперативну пам'ять: байти .data копіюються в область даних;
байти .code копіюються в область коду;
простір .stack виділяється (зазвичай заповнюється нулями);
простір .bss виділяється та обнуляється. Ініціалізація регістрів: операційна система ініціалізує сегментні регістри: DS (сегмент даних) встановлюється на базову адресу сегмента .data;
CS (сегмент коду) встановлюється на базову адресу сегмента .code;
SS (сегмент стека) встановлюється на базову адресу сегмента .stack;
IP (вказівник інструкцій) встановлюється на 0 (або на зміщення точки входу в сегменті коду);
SP (вказівник стека) встановлюється на вершину стека (розмір задано в директиві .stack). Початок виконання: керування передається на CS:IP, і процесор починає отримувати та виконувати інструкції. Як ми розглядали в Лекції 3, процесор 8086 використовує сегментну адресацію пам'яті. Кожен сегментний регістр містить 16-бітну базову адресу сегмента; її множать на 16 (зсуваючи вліво на 4 біти), щоб сформувати 20-бітну фізичну адресу.Ось як сегментні регістри відображаються на сегменти програми:Повна адреса будь-якого звернення до пам'яті формується поєднанням сегментного регістра зі зміщенням:Фізична адреса = (Сегмент × 16) + ЗміщенняНаприклад, якщо CS = 1000h та IP = 0020h:Фізична адреса = (1000h × 16) + 0020h = 10000h + 0020h = 10020hЦе й є справжня адреса в оперативній пам'яті, за якою процесор візьме наступну інструкцію.Завантаживши програму в Turbo Debugger (TD), можна безпосередньо побачити ініціалізацію регістрів, яку виконав завантажувач. Одразу після завантаження (до виконання будь-якої інструкції) ми побачимо приблизно таку картину:AX=0000 BX=0000 CX=0050 DX=0000 SP=0100 BP=0000 SI=0000 DI=0000
DS=1234 ES=1234 SS=1244 CS=1254 IP=0000
Звернімо увагу на такі деталі:
IP = 0000 — вказівник інструкцій стоїть на зміщенні 0 в сегменті коду (перша інструкція);
CS = 1254h — базова адреса сегмента коду (конкретне значення залежить від того, куди DOS завантажила програму);
DS = 1234h — базова адреса сегмента даних (зазвичай її встановлює завантажувач або код ініціалізації програми);
SS = 1244h — базова адреса сегмента стека;
SP = 0100h — вказівник стека на вершині 256-байтового (100h) стека.
Повна адреса першої інструкції: CS:IP = 1254h:0000h = 12540h.Сучасні процесори x86 (зокрема Intel Core та AMD Ryzen) досі підтримують реальний режим — оригінальний режим роботи 8086. Після увімкнення ці процесори стартують саме в реальному режимі — задля зворотної сумісності. Реальний режим має кілька визначальних характеристик: Сегментна адресація пам'яті: використовує сегментні регістри (CS, DS, SS, ES), як описано вище. Адресний простір 1 МБ: 20-бітний простір фізичних адрес обмежує адресовану пам'ять до 2^20 = 1 048 576 байтів (1 МБ). Відсутність захисту пам'яті: програма може звертатися до будь-якої адреси пам'яті, зокрема до операційної системи та інших програм. Така простота була прийнятною в епоху однозадачного DOS, але небезпечна в сучасних багатозадачних системах. Прямий доступ до апаратного забезпечення: програми можуть безпосередньо виконувати привілейовані інструкції та звертатися до портів введення-виведення без обмежень. Хоча сучасні операційні системи (Windows, Linux, macOS) працюють у захищеному режимі або 64-бітному режимі (long mode), реальний режим досі живий:
Ініціалізація BIOS: під час увімкнення ПК BIOS працює саме в реальному режимі — ініціалізує апаратне забезпечення перед завантаженням операційної системи.
Емуляція DOS: емулятор DOS у Windows (для .COM-програм у командному рядку) працює у віртуальному середовищі реального режиму.
Вбудовані системи: деякі мікроконтролери та вбудовані системи на базі x86 працюють виключно в реальному режимі.
Розуміти реальний режим важливо, і ось чому:
він розкриває фундаментальну архітектуру сімейства процесорів x86;
спрощує навчання — у реальному режимі немає віртуальної пам'яті, сторінкової організації чи рівнів привілеїв;
багато вбудованих систем та завантажувачів досі спираються саме на нього;
він допомагає зрозуміти, як еволюціонував сучасний захищений режим.
<img alt="Адресація пам'яті в реальному режимі" src="assets/l05-real-mode-memory-addressing.png" target="_self">Карта пам'яті реального режиму: сегментні регістри CS, DS, SS вказують на 64 КБ сегменти в межах 1 МБ адресного простору. Фізична адреса обчислюється як Сегмент × 16 + Зміщення.Розгляньмо повний приклад, у якому задіяні всі чотири сегменти:.model small
.stack 100h .data
prompt db 'Enter a number: $'
result db 'Result: $'
value dw 0 .bss
buffer db 10 dup(?) .code
main proc ; Ініціалізуємо DS для вказівки на сегмент даних mov ax, @data mov ds, ax ; Виводимо запрошення mov ah, 09h lea dx, prompt int 21h ; Зчитуємо введення (спрощено) mov ah, 01h int 21h sub al, '0' ; Перетворюємо ASCII на число mov byte ptr value, al ; Виконуємо обчислення mov al, byte ptr value add al, 10 mov byte ptr buffer, al ; Виводимо результат mov ah, 09h lea dx, result int 21h ; Завершення програми mov ax, 4C00h int 21h
main endp
end main
Розміщення у пам'яті після завантаження:+------------------+ Молодші адреси
| PSP (256 байтів) | &lt;- Префікс сегмента програми DOS
+------------------+
| Сегмент .data | &lt;- DS вказує сюди
| prompt | "Enter a number: $"
| result | "Result: $"
| value | 00 00 (2 байти)
+------------------+
| Сегмент .bss | &lt;- Все ще частина сегмента даних у моделі small
| buffer | (10 байтів, обнулені)
+------------------+
| Сегмент .code | &lt;- CS вказує сюди, IP = 0
| main proc | B8 xx xx ... (байти машинного коду)
+------------------+
| Сегмент .stack | &lt;- SS вказує сюди, SP на вершині
| (256 байтів) | (100h байтів простору стека)
+------------------+ Старші адреси
Коли програма виконується: Ініціалізація DS: перші дві інструкції (mov ax, @data та mov ds, ax) забезпечують, щоб DS указував на сегмент даних. Асемблер надає @data як попередньо визначений символ, що позначає адресу параграфа сегмента даних. Виконання коду: процесор отримує інструкції за адресою CS:IP, декодуючи та виконуючи кожну з них. Доступ до даних: інструкції на кшталт lea dx, prompt використовують DS як сегмент за замовчуванням, тож prompt розв'язується у DS:зміщення_prompt. Операції зі стеком: якби ми скористалися PUSH або CALL (їх ми розглянемо далі), вони б працювали через SS:SP. Це чітке розділення коду, даних і стека — наріжний камінь розуміння організації програми; воно знадобиться нам у наступних розділах під час розмови про виклик процедур та фрейми стека.Пишучи мовою асемблера, ми працюємо на рівні абстракції, що все ще далекий від того, з чим насправді має справу процесор. Процесор не розуміє мнемонік на кшталт mov чи jmp — він оперує лише числами. Ці числа називаються кодами операцій (opcodes) і відповідають справжнім машинним інструкціям, які виконує процесор.Погляньмо, що відбувається при компіляції простої асемблерної інструкції. Візьмімо першу інструкцію типової програми для DOS:mov ax, @data
Асемблер перетворює цю інструкцію на три байти в пам'яті:B8 7D 08
Розгляньмо це побайтово:
B8 — код операції для «завантажити безпосереднє значення-слово в регістр AX»
7D 08 — параметр, тобто саме значення, яке потрібно завантажити в AX (воно відповідає адресі сегмента, де починаються наші дані)
Чи помітили ви щось незвичне в параметрі? Значення виглядає «перевернутим»: у пам'яті стоїть 7D 08, хоча насправді це шістнадцяткове 087D (тобто 2173 у десятковій системі). Таке перевертання — не помилка, а фундаментальна властивість архітектури x86, яка має назву Little-endian (порядок «від молодшого байта»).Коли завантажити програму в Turbo Debugger і перемкнутися в режим перегляду CPU, перед очима опиняється саме те, з чим працює процесор:cs:0000 B87D08 mov ax,087D
cs:0003 8ED8 mov ds,ax
cs:0005 ... ...
У лівій колонці — адреса пам'яті (сегмент:зміщення), у середній — сирі байти з пам'яті, у правій — зрозумілий для людини асемблерний еквівалент, який зневаджувач дизасемблював за нас.Одне з ключових понять для розуміння машинного коду — порядок байтів, або endianness. Він визначає, яким чином багатобайтові значення зберігаються в пам'яті.У форматі Big-endian (порядок «від старшого байта») першим у пам'яті стоїть старший байт (MSB) — тобто так само, як ми зазвичай записуємо числа. Збереження 32-бітного шістнадцяткового значення 1C2C3B4D у форматі big-endian виглядає так:Адреса пам'яті: 0x1000 0x1001 0x1002 0x1003
Значення байта: 1C 2C 3B 4D (MSB) (LSB)
Назва «big-endian» походить від того, що «великий кінець» (старший байт) іде першим. Такого порядку дотримуються деякі процесорні архітектури — наприклад, Motorola 68000, PowerPC (у режимі big-endian), а також численні мережеві протоколи.У форматі Little-endian (порядок «від молодшого байта»), який застосовують процесори x86, першим у пам'яті стоїть молодший байт (LSB). Те саме значення 1C2C3B4D зберігається так:Адреса пам'яті: 0x1000 0x1001 0x1002 0x1003
Значення байта: 4D 3B 2C 1C (LSB) (MSB)
Назва «little-endian» відображає те, що «малий кінець» (молодший байт) іде першим.Little-endian в архітектурі x86 сягає корінням процесорів Intel 8080 та Intel 8086. Цей формат має кілька практичних переваг:
Простіше обчислення адрес: зчитуючи багатобайтове значення, можна йти від заданої адреси та обробляти байти в порядку зростання, причому молодший байт лежить за найменшою адресою
Зворотна сумісність: 16-бітне значення можна прочитати як 8-бітне, взявши лише перший байт за тією самою адресою
Спрощене перетворення типів: перехід між цілими числами різної розрядності у форматі little-endian виконується простіше
Водночас little-endian може збивати з пантелику під час перегляду дампів пам'яті: байти виглядають «задом наперед» порівняно зі звичним записом шістнадцяткових чисел.<br><img alt="Порядок байтів Big-endian проти Little-endian" src="assets/l05-big-endian-vs-little-endian.png" target="_self">Big-endian зберігає байти в тому порядку, в якому ми їх записуємо (спершу старший). Little-endian (його використовує x86) змінює порядок байтів у пам'яті: молодший байт займає найменшу адресу.Простежмо це на прикладі попередньої інструкції:mov ax, 087Dh
Значення 087D треба зберегти як 16-бітний параметр. У форматі little-endian:
Молодший байт: 7D
Старший байт: 08
Тому в пам'яті й з'являється послідовність B8 7D 08.Читаючи цю інструкцію, процесор знає, що код операції B8 очікує 16-бітного параметра, тож зчитує наступні два байти (7D 08) і тлумачить їх у порядку little-endian як 087D.<br><img alt="Порівняння розміщення в пам'яті для різних порядків байтів" src="assets/l05-endianness-memory-layout.png" target="_self">Розміщення значення 0A0B0C0Dh у пам'яті зі зміщеннями адрес — порівняння зберігання в big-endian та little-endian.Вказівник інструкцій (IP) — це спеціальний регістр, що вказує, звідки процесор має взяти наступну інструкцію. Разом із регістром сегмента коду (CS) він дає повну адресу наступної інструкції для виконання.Простежмо виконання перших двох інструкцій:Початковий стан:CS = 0x1000 (приклад значення)
IP = 0x0000
Адреса поточної інструкції: CS:IP = 0x1000:0x0000
Перша інструкція:Пам'ять за адресою cs:0000: B8 7D 08 → mov ax, 087Dh
Довжина цієї інструкції — 3 байти (1 байт коду операції + 2 байти параметра). Після її виконання процесором:AX = 087Dh (значення завантажено)
IP = IP + 3 = 0x0003 (автоматично збільшено)
Адреса наступної інструкції: cs:0003
Друга інструкція:Пам'ять за адресою cs:0003: 8E D8 → mov ds, ax
Довжина цієї інструкції — 2 байти (1 байт коду операції + 1 байт кодування параметра). Код операції 8E означає «перемістити до сегментного регістра», а байт D8 задає, які саме регістри задіяно, — у нашому випадку «з AX у DS».Після виконання:DS = 087Dh (значення з AX)
IP = IP + 2 = 0x0005
Адреса наступної інструкції: cs:0005
Ключовий висновок: IP автоматично зростає на довжину кожної інструкції після її виконання. Процесору не потрібно «знати» наперед, яка в інструкції довжина, — її визначає сам код операції.Тут може виникнути природне запитання: навіщо нам послідовність із двох інструкцій, щоб завантажити значення в DS?mov ax, @data ; Чому не можна просто написати: mov ds, @data?
mov ds, ax
Відповідь криється в тому, як влаштовані коди операцій x86. Погляньмо на другу інструкцію докладніше:Код операції: 8E
Параметр: D8
Код операції 8E означає «перемістити слово до сегментного регістра». Але звідки процесор дізнається, що ми переміщуємо саме з AX у DS? Відповідь — у байті параметра D8.Байт D8 — це не значення для переміщення, а кодування регістрів, яке задає:
Регістр-джерело: AX (двійково: 000)
Регістр-приймач: DS (двійково: 11)
Режим адресації: регістр-регістр
Усе це запаковано в один байт за допомогою спеціального формату — так званого байта ModR/M (Mode-Register-Memory).То чому ж не можна написати mov ds, 087Dh безпосередньо? Асемблер (TASM) фактично прийме такий синтаксис без зауважень, але під час збирання коду видасть помилку або сформує некоректний код. Причина проста: коду операції для «завантажити безпосереднє значення в сегментний регістр» просто не існує.Це не недогляд, а свідоме конструкторське рішення. Набір інструкцій x86 містить сотні кодів операцій, але їхня кількість обмежена доступним простором кодування. Кожен додатковий код операції потребує:
Апаратної реалізації: фізичних схем на кристалі процесора для декодування та виконання цієї інструкції
Площі кристала: більше транзисторів означає більші чіпи та вищі витрати
Складності: більша кількість інструкцій ускладнює проєктування процесора
Інженери Intel розсудили, що завантаження безпосередніх значень у сегментні регістри трапляється досить рідко, тож окремий код операції для цього не є виправданим. Натомість вони передбачили, що програмісти використовуватимуть регістр загального призначення як проміжний:mov ax, immediate_value ; Код операції існує: B8
mov ds, ax ; Код операції існує: 8E
Це класичний компроміс в архітектурі комп'ютерів: зручність проти апаратної складності.Попри те, що друковані таблиці кодів операцій існують (а в мережі чимало сайтів із повними довідниками кодів операцій x86), Turbo Debugger дає потужний спосіб досліджувати їх інтерактивно.Відкривши режим перегляду CPU в Turbo Debugger (View → CPU), бачимо три панелі:
Панель коду: дизасембльовані інструкції з адресами та байтовими кодами
Панель регістрів: поточні значення всіх регістрів процесора
Панель дампу: сирий вміст пам'яті
Покроково проходячи програму інструкція за інструкцією (клавішами F7 або F8), можна спостерігати:
Байти в пам'яті (ліва частина панелі коду)
Асемблерний еквівалент (права частина панелі коду)
Зростання регістра IP
Зміни інших регістрів та пам'яті
Простежмо, що відбувається під час виконання нашої послідовності з двох інструкцій:Перед виконанням:CS:0000 B87D08 mov ax,087D
IP = 0000
AX = ???? (не визначено)
DS = ???? (не визначено)
Після першої інструкції:CS:0003 8ED8 mov ds,ax
IP = 0003
AX = 087D
DS = ???? (ще не змінено)
Після другої інструкції:CS:0005 ...
IP = 0005
AX = 087D
DS = 087D
Тепер, коли DS указує на наш сегмент даних, варто перемкнути вікно дампу на адресу DS:0000 — і ми побачимо справжні дані програми: наприклад, рядок "Hello", якщо саме його визначено в сегменті даних.Саме на інструкції переходу найяскравіше видно, що мова асемблера — це абстракція. Коли ми пишемо:jmp exit
Здається, ніби ми наказуємо процесору «перейти до мітки з назвою exit». Але на рівні машинного коду жодних міток не існує. Це суто асемблерна концепція — зручність, придумана для людей.На апаратному рівні JMP — це всього-на-всього інструкція, яка змінює вказівник інструкцій. Зокрема, для короткого переходу (у межах ±127 байтів) інструкція:jmp exit
Перетворюється на:EB 08
Де:
EB — код операції для «короткого переходу» (додати знакове 8-бітне зміщення до IP)
08 — значення зміщення
Уточнімо, що це означає: під час виконання цієї інструкції процесор обчислює IP = IP + 2 + 08h. Доданок «+2» враховує, що IP уже просунувся за саму інструкцію JMP завдовжки 2 байти.Розгляньмо такий фрагмент коду:cs:0005 EB08 jmp exit
cs:0007 ... ...
cs:000F ... exit: mov ax, 4C00h
Асемблер бачить мітку exit за зміщенням 000F. Інструкція переходу розташована за зміщенням 0005. Після її виконання IP матиме значення 0007 (тобто вказуватиме на байт одразу за переходом). Щоб дістатися 000F, треба додати ще 08h байтів.Отже, асемблер обчислює: зміщення_до_exit - (поточний_IP + довжина_інструкції) = 000F - 0007 = 08h.Саме це значення 08 і закодовано як параметр інструкції JMP.Порівняймо, що бачить людина і що бачить процесор:Що бачить людина (лістинг асемблера): jmp exit ; ... інший код ...
exit: mov ax, 4C00h
Що бачить процесор (сира пам'ять):EB 08 ... [6 байтів іншого коду] ... B8 00 4C
Для процесора немає жодного «exit» як пункту призначення. Він знає лише:
EB — код операції, що означає «додати наступний байт до IP»
08 — значення для додавання
Після виконання IP указуватиме на 8 байтів далі від того місця, де він був би інакше
<br><img alt="Вигляд CPU проти вигляду людини в Turbo Debugger" src="assets/l05-cpu-view-annotated.png" target="_self">Режим перегляду CPU в Turbo Debugger з анотаціями: що бачить процесор (ліва панель: сирі байти та дизасемблювання) і що бачить людина (праві панелі: форматовані значення регістрів та декодована пам'ять). Зелена мітка «процесор» позначає погляд процесора; червона мітка «людина» — інтерпретацію, зрозумілу для людини.Розуміння того, що мітки — це конструкції асемблера, має вирішальне значення з кількох причин:
Позиційно-незалежний код: якщо перенести код у пам'яті, переходи можуть зламатися, якщо не перерахувати зміщення
Налагодження: у дампах пам'яті міток немає — є лише адреси та зміщення
Розуміння обмежень: короткий перехід сягає лише ±127 байтів; для довших переходів потрібні інші коди операцій із 16-бітними зміщеннями
Самомодифікований код: у вкрай хитромудрих (і небезпечних) техніках програма може змінювати власні інструкції переходу, переписуючи байти зміщення
Розгляньмо повний приклад того, як перехід працює на рівні байтів.Припустімо, ми маємо такий код:cs:0005 EB 08 jmp exit
cs:0007 B4 09 mov ah, 09h
cs:0009 BA 00 00 mov dx, 0000h
cs:000C CD 21 int 21h
cs:000E EB 00 jmp $
cs:0010 B8 00 4C mov ax, 4C00h ; ← це "exit"
Стривайте — інструкція за адресою cs:0010 не відповідає нашому попередньому твердженню, що exit розташована за адресою cs:000F. Перерахуймо.Власне, виправмо це. Якщо уважно порахувати байти:cs:0005 EB 08 jmp exit ; 2 байти
cs:0007 B4 09 mov ah, 09h ; 2 байти
cs:0009 BA 00 00 mov dx, 0000h ; 3 байти
cs:000C CD 21 int 21h ; 2 байти
cs:000E 90 nop ; 1 байт (заповнювач)
cs:000F B8 00 4C mov ax, 4C00h ; 3 байти ← мітка exit тут
Перед виконанням JMP:IP = 0005
Поточна адреса: cs:0005
Поточні байти: EB 08
Процесор декодує:
Зчитує байт за адресою cs:0005 → EB → «це інструкція короткого переходу»
Зчитує наступний байт → 08 → «зміщення дорівнює +8»
Обчислює новий IP: поточний IP (0005) + довжина інструкції (2) + зміщення (08) = 000F
Після виконання JMP:IP = 000F
Наступна інструкція: mov ax, 4C00h
Процесор успішно «перейшов» до мітки exit — але зробив це суто арифметично, не маючи жодного уявлення про те, що програміст назвав це місце «exit».Зміщення 08h указує, скільки байтів слід пропустити після завершення інструкції переходу. Порахуймо їх:cs:0007 B4 ← байт 1
cs:0008 09 ← байт 2
cs:0009 BA ← байт 3
cs:000A 00 ← байт 4
cs:000B 00 ← байт 5
cs:000C CD ← байт 6
cs:000D 21 ← байт 7
cs:000E 90 ← байт 8
cs:000F ← потрапляємо сюди!
Рівно 8 байтів, як і закодовано в інструкції.Цей детальний розбір кодів операцій, порядку байтів та механіки переходів розкриває фундаментальну істину про програмування: за кожною абстракцією стоїть реальність.Коли ми пишемо:mov ds, @data
ми використовуємо три рівні абстракції:
Мітка @data: конструкція асемблера, яка позначає адресу в пам'яті
Мнемоніка mov: читабельна для людини назва коду операції B8 чи 8E
Ім'я регістра ds: читабельне для людини позначення кодування регістра 11 у байті ModR/M
У своїй основі всі ці абстракції зводяться до чисел — байтів у пам'яті, які цифрові схеми процесора інтерпретують як електричні сигнали, що активують конкретні шляхи даних та операції.Розуміння цієї реальності допомагає:
Ефективніше налагоджувати: коли щось іде не так, можна подивитися на справжні байти
Розуміти продуктивність: видно, які інструкції більші або складніші
Усвідомлювати обмеження: стає зрозуміло, чому певні операції потребують кількох інструкцій
Писати кращий код: легше приймати обґрунтовані рішення щодо вибору інструкцій та організації коду
Продовжуючи дослідження стека та викликів процедур, запам'ятаймо цю перспективу. Усе — від локальних змінних до викликів функцій і рекурсії — зрештою зводиться до кодів операцій, адрес пам'яті та маніпуляцій з регістрами. Мистецтво програмування асемблером якраз і полягає в тому, щоб вільно мислити як на абстрактному рівні (процедури, змінні, потік керування), так і на конкретному (байти, регістри, адреси).Цей розділ повертається до систем числення в архітектурі комп'ютерів і завершує їх розгляд. Двійкове представлення та доповняльний код ми вже обговорювали в Лекції 2, а шістнадцяткову нотацію — у Лекції 3; тут заповнимо прогалини, що залишилися: вісімкову нотацію, дробові двійкові числа, деталі організації пам'яті та систематичні алгоритми перетворення. Цей розділ слугує своєрідним «розставленням крапок над і» у темі представлення чисел.Перш ніж заглиблюватися в системи числення, формалізуймо стандартні розміри даних, що використовуються в архітектурі x86-64:Кожне багатобайтове значення має старший байт (MSB), який містить біти найвищого порядку, і молодший байт (LSB), який містить біти найнижчого порядку. Для окремих бітів усередині байта аналогічно розрізняють старший біт (MSB) і молодший біт (LSB).Порядок байтів (Endianness) визначає, як багатобайтові значення зберігаються в пам'яті:
Little-endian (x86-64, ARM у більшості режимів): LSB зберігається за найнижчою адресою пам'яті. Значення 0x12345678 у пам'яті має вигляд 78 56 34 12.
Big-endian (багато мережевих протоколів, деякі старіші процесори): MSB зберігається за найнижчою адресою пам'яті. Значення 0x12345678 у пам'яті має вигляд 12 34 56 78.
Приклад: збереження значення DWORD (подвійне слово) 0xDEADBEEF за адресою 0x1000:Little-endian:
Адреса: 0x1000 0x1001 0x1002 0x1003
Значення: EF BE AD DE Big-endian:
Адреса: 0x1000 0x1001 0x1002 0x1003
Значення: DE AD BE EF
x86-64 використовує виключно Little-endian. Це важливо враховувати під час дослідження дампів пам'яті чи взаємодії з мережевими протоколами, які зазвичай працюють у Big-endian — так званому «мережевому порядку байтів».Нагадаємо з Лекції 2, що двійкові числа використовують позиційну систему числення зі степенями 2. Щоб перетворити двійкове число на десяткове, застосовують позиційне розкладання:110001₂ = 1×2⁵ + 1×2⁴ + 0×2³ + 0×2² + 0×2¹ + 1×2⁰ = 32 + 16 + 0 + 0 + 0 + 1 = 49₁₀
Цей метод полягає в обчисленні внеску кожної бітової позиції у загальне значення.Перетворення з десяткової системи у двійкову потребує систематичного алгоритму. Метод повторного ділення на 2 (відомий також як метод ділення з остачею) працює так:
Поділити десяткове число на 2.
Записати остачу (0 або 1) — вона стає бітом результату.
Частку використати для наступної ітерації.
Повторювати, доки частка не стане 0.
Прочитати остачі у зворотному порядку (остання остача = старший біт).
Приклад. Перетворимо 23₁₀ у двійкову систему:23 ÷ 2 = 11 остача 1 (молодший біт)
11 ÷ 2 = 5 остача 1
5 ÷ 2 = 2 остача 1
2 ÷ 2 = 1 остача 0
1 ÷ 2 = 0 остача 1 (старший біт) Читаємо знизу догори: 23₁₀ = 10111₂
Цей алгоритм по суті є оберненим методом Горнера. Метод Горнера ефективно обчислює поліноми за допомогою факторизації:10111₂ = ((((1)×2 + 0)×2 + 1)×2 + 1)×2 + 1
Метод ділення виконує обернену операцію: послідовно виділяє коефіцієнти (біти) від молодшого до старшого.<br><img alt="Перетворення десяткового числа у двійкове методом повторного ділення" src="assets/l05-decimal-to-binary-conversion.png" target="_self" style="width: 400px; max-width: 100%;">Перетворення 23₁₀ у двійкову систему методом повторного ділення на 2. Остачі, прочитані знизу вгору, дають 10111₂.Досі ми розглядали лише цілі двійкові числа. Однак двійкова система здатна представляти і дробові значення — за допомогою нотації з фіксованою крапкою, де двійкова крапка розділяє цілу й дробову частини (аналогічно десятковій крапці в системі з основою 10).Подібно до того як десяткові дроби використовують від'ємні степені 10 (0,25₁₀ = 2×10⁻¹ + 5×10⁻²), двійкові дроби спираються на від'ємні степені 2:1011010.101₂ = 1×2⁶ + 0×2⁵ + 1×2⁴ + 1×2³ + 0×2² + 1×2¹ + 0×2⁰ + 1×2⁻¹ + 0×2⁻² + 1×2⁻³ = 64 + 0 + 16 + 8 + 0 + 2 + 0 + 0.5 + 0 + 0.125 = 90.625₁₀
Дробова частина спирається на позиції з вагами: 2⁻¹ = 0,5, 2⁻² = 0,25, 2⁻³ = 0,125, 2⁻⁴ = 0,0625 і так далі.Важливе обмеження. Не всі десяткові дроби можна точно представити у двійковій системі. Наприклад, 0,1₁₀ не має точного двійкового представлення (для нього потрібен нескінченний періодичний двійковий дріб: 0,0001100110011...₂). Саме тому арифметика з рухомою крапкою в комп'ютерах іноді дає несподівані результати.Щоб перетворити десятковий дріб на двійковий, застосовують метод повторного множення на 2:
Помножити дробову частину на 2.
Ціла частина результату (0 або 1) стає наступним бітом.
Дробову частину залишити для наступної ітерації.
Повторювати, доки дробова частина не стане 0 (або доки не буде досягнуто бажаної точності).
Прочитати цілі частини по порядку (перша = старший біт дробової частини).
Приклад. Перетворимо 0,116₁₀ у двійкову систему (з точністю до 10 бітів):0.116 × 2 = 0.232 → біт 0 (позиція 2⁻¹)
0.232 × 2 = 0.464 → біт 0 (позиція 2⁻²)
0.464 × 2 = 0.928 → біт 0 (позиція 2⁻³)
0.928 × 2 = 1.856 → біт 1 (позиція 2⁻⁴)
0.856 × 2 = 1.712 → біт 1 (позиція 2⁻⁵)
0.712 × 2 = 1.424 → біт 1 (позиція 2⁻⁶)
0.424 × 2 = 0.848 → біт 0 (позиція 2⁻⁷)
0.848 × 2 = 1.696 → біт 1 (позиція 2⁻⁸)
0.696 × 2 = 1.392 → біт 1 (позиція 2⁻⁹)
0.392 × 2 = 0.784 → біт 0 (позиція 2⁻¹⁰) Результат: 0.116₁₀ ≈ 0.0001110110₂
Перевірка: 0,0001110110₂ = 2⁻⁴ + 2⁻⁵ + 2⁻⁶ + 2⁻⁸ + 2⁻⁹ = 0,0625 + 0,03125 + 0,015625 + 0,00390625 + 0,001953125 = 0,115234375₁₀ (близько, але не точно).Сучасні комп'ютери для арифметики дробових чисел загального призначення зазвичай застосовують представлення IEEE 754 з рухомою крапкою (йому буде присвячено одну з наступних лекцій), проте двійкові числа з фіксованою крапкою досі зустрічаються у вбудованих системах, цифровій обробці сигналів та фінансових розрахунках — там, де точне десяткове представлення не є обов'язковим.Хоча в обчислювальній техніці домінує шістнадцяткова система (з основою 16) як основна альтернативна система числення (повну таблицю шістнадцяткових значень та методи перетворення наведено в Лекції 3), вісімкова система (з основою 8) теж подекуди зустрічається:
у правах доступу до файлів у Unix (наприклад, chmod 755);
у деяких застарілих синтаксисах мов асемблера;
у документації до певних вбудованих систем.
Вісімкова система використовує цифри 0–7, причому кожна вісімкова цифра відповідає рівно 3 бітам:Перетворення з вісімкової у двійкову. Кожну вісімкову цифру замінюємо її 3-бітним двійковим еквівалентом.Приклад. Переведемо 2541₈ у двійкову:2 5 4 1
↓ ↓ ↓ ↓
010 101 100 001 Результат: 2541₈ = 010101100001₂
Перетворення з двійкової у вісімкову. Згрупуймо біти по три справа наліво, а потім переведемо кожну групу в одну цифру.Приклад. Переведемо 1101110101₂ у вісімкову:1 101 110 101 (групуємо по три, доповнюємо зліва за потреби)
↓ ↓ ↓ ↓
1 5 6 5 Результат: 1101110101₂ = 1565₈
Нагадаємо з Лекції 3, що шістнадцяткова система використовує 4 біти на цифру (0–F), а отже, для представлення двійкових даних є компактнішою за вісімкову. Саме через це в сучасній практиці шістнадцяткова система значною мірою витіснила вісімкову — один байт (8 бітів) потребує рівно 2 шістнадцяткові цифри, натомість 3 вісімкові цифри дають неузгодженість за бітами.Як було докладно показано в Лекції 2, Розділ 3, доповняльний код — стандартне представлення знакових цілих чисел у сучасних комп'ютерах. Короткий довідник з перетворення:Перетворення додатного числа у від'ємне (доповняльний код):
Інвертувати всі біти (побітове NOT).
Додати 1.
Приклад. +5 → −5 у 8-бітному доповняльному коді:+5 = 00000101
Інверсія: 11111010
Додати 1: 11111011 = -5
Докладне порівняння з прямим кодом та оберненим кодом наведено в Лекції 2, Розділ 3.У Розділі 5 Лекції 4 ми вже ознайомилися з фундаментальними побітовими логічними операціями — AND, OR, XOR і NOT — та їхніми таблицями істинності. У цьому ж розділі вийдемо за межі теорії й розглянемо практичні патерни маніпулювання бітами: основні прийоми роботи з окремими бітами та бітовими полями у низькорівневому програмуванні.Нагадаємо оператори, з якими працюватимемо далі (повні таблиці істинності — у Лекції 4):
AND (&amp;): результуючий біт дорівнює 1 лише тоді, коли обидва вхідні біти дорівнюють 1.
OR (|): результуючий біт дорівнює 1, якщо хоча б один вхідний біт дорівнює 1.
XOR (^): результуючий біт дорівнює 1, якщо вхідні біти різні.
NOT (~): інвертує кожен біт (1→0, 0→1).
У мові асемблера (x86-64):
and rax, rbx — операція AND;
or rax, rbx — операція OR;
xor rax, rbx — операція XOR;
not rax — операція NOT;
test rax, rbx — AND без збереження результату (впливає лише на прапорці).
Операція AND — основний інструмент для ізоляції конкретних бітів з одночасним обнуленням решти. Такий прийом називають маскуванням: до значення застосовують «маску», яка визначає, які біти зберегти.Ключовий принцип: x AND 1 = x і x AND 0 = 0. Отже, маска з одиницями у потрібних позиціях і нулями в усіх інших збереже саме ці біти, а решту обнулить.Приклад 1: вилучення молодшого ніббла (4 біти)Значення: 01011101
Маска: 00001111 (AND) --------
Результат:00001101
Маска 00001111 зберігає 4 найправіші біти, а 4 старші — очищує. Такий прийом стає у пригоді для вилучення шістнадцяткових цифр, значень BCD або молодших частин числа.Приклад 2: перевірка старшого біта (знакового біта)Значення: 10011111
Маска: 10000000 (AND) --------
Результат:10000000 (ненульовий, отже старший біт дорівнював 1)
Якщо результат нульовий — старший біт був 0; якщо ненульовий — старший біт був 1. Так можна визначити знак числа у доповняльному коді, не вдаючись до арифметичних операцій.Приклад 3: перевірка парності числаПарність визначає молодший біт: LSB=1 означає непарне число, LSB=0 — парне.Значення: ???????X (X — молодший біт)
Маска: 00000001 (AND) --------
Результат:0000000X (1 — непарне, 0 — парне)
Реалізація на асемблері через TEST:Інструкція test виконує AND, але результат не зберігає — оновлює лише прапорці:test al, 1 ; Перевірити, чи AL непарне
jnz odd_label ; Перейти, якщо ненульове (непарне)
; ... обробка парного
odd_label:
; ... обробка непарного
Поширені маски:
0x0F або 0b00001111 — молодший ніббл;
0xF0 або 0b11110000 — старший ніббл;
0x01 або 0b00000001 — молодший біт (перевірка парності);
0x80 або 0b10000000 — старший біт (перевірка знаку байта);
0xFF00 — старший байт 16-бітного слова;
0x00FF — молодший байт 16-бітного слова.
Операція OR встановлює обрані біти в 1, а решту залишає без змін.Ключовий принцип: x OR 1 = 1 і x OR 0 = x. Маска з одиницями у потрібних позиціях та нулями в інших примусово переведе саме ці біти в 1.Приклад 1: встановлення старшого бітаЗначення: 00101011
Маска: 10000000 (OR) --------
Результат:10101011
Такий патерн стає у пригоді для встановлення прапорців, увімкнення функцій або маркування значень. Наприклад, якщо встановити знаковий біт, бітовий шаблон додатного числа перетвориться на його від'ємний еквівалент у доповняльному коді.Приклад 2: встановлення кількох бітів (прапорці дозволів)Значення: 00000000 (немає дозволів)
Маска: 01100100 (OR — увімкнути біти читання, запису, виконання) --------
Результат:01100100
У Unix-подібних системах файлові дозволи представлено бітовими полями, а операції OR додають окремі дозволи: читання (біт 2), запис (біт 1), виконання (біт 0) — для кожної з категорій «користувач / група / інші».Приклад 3: поєднання AND і OR для вибіркової модифікаціїЩоб установити потрібні біти й водночас очистити інші, послідовно застосовують AND, а потім OR:Початкове: 10110101
Крок 1: 10110101 11110000 (AND — очистити молодший ніббл) -------- 10110000
Крок 2: 10110000 00001100 (OR — встановити біти 2 та 3) --------
Результат: 10111100
Цей патерн замінює бітове поле новим значенням: спочатку поле очищують (AND з інвертованою маскою), а потім записують нові біти (OR з ними).Приклад на асемблері:and al, 0xF0 ; Очистити молодший ніббл
or al, 0x0C ; Встановити біти 2 та 3
Операція XOR інвертує обрані біти (0→1, 1→0), решту ж лишає без змін.Ключовий принцип: x XOR 1 = NOT x і x XOR 0 = x. Маска з одиницями на позиціях, які треба перемкнути, і нулями в інших — інвертує саме ці біти.Приклад 1: перемикання обраних бітівЗначення: 10100010
Маска: 11101011 (XOR) --------
Результат:01001001
Кожна бітова позиція, де маска містить 1, інвертується; позиції з 0 залишаються без змін.Приклад 2: інверсія всіх бітів (побітове NOT через XOR)Значення: 10100010
Маска: 11111111 (XOR — усі одиниці) --------
Результат:01011101
XOR з усіма одиницями еквівалентний операції NOT. До цього прийому вдаються, коли спеціалізованої інструкції NOT немає під рукою або коли потрібна умовна інверсія.Приклад 3: перемикання стану світлодіода або прапорця стануНехай біт 3 керує світлодіодом (1 — увімкнено, 0 — вимкнено):Поточне: ????1??? (світлодіод увімкнено)
Маска: 00001000 (XOR — перемкнути біт 3) --------
Результат:????0??? (світлодіод вимкнено) Далі: ????0??? (світлодіод вимкнено)
Маска: 00001000 (XOR — знову перемкнути біт 3) --------
Результат:????1??? (світлодіод увімкнено)
Повторне XOR з тією самою маскою перемикає біт туди й назад — зручно для анімацій, індикаторів, що блимають, або скінченних автоматів.Особлива властивість: XOR-обмін значеньЗа допомогою XOR можна обміняти два значення без тимчасової змінної:a = a XOR b
b = a XOR b (тепер b = початкове a)
a = a XOR b (тепер a = початкове b)
Працює це завдяки тому, що XOR — самообернена операція: x XOR y XOR y = x. Прийом дотепний, але сучасні компілятори для обміну зазвичай обирають звичайні інструкції переміщення, тому він лишається радше цікавинкою, ніж практичною оптимізацією.Приклад на асемблері:xor al, 0x80 ; Перемкнути старший біт (біт 7)
xor rax, rax ; Поширена ідіома: очистити регістр (x XOR x = 0)
Ідіома xor rax, rax — добре відома оптимізація: XOR значення із самим собою завжди дає нуль, а на x86-64 така інструкція коротша й швидша за mov rax, 0.Перевірка умов:
test al, 1 — перевірити, чи число непарне;
test al, 0x80 — перевірити, чи байт від'ємний;
test rax, rax — перевірити, чи регістр нульовий.
Вилучення полів:
and al, 0x0F — вилучити молодший ніббл;
and rax, 0xFF — вилучити молодший байт (із розширенням нулями).
Встановлення прапорців і дозволів:
or al, 0x80 — встановити старший біт (позначити як від'ємне, увімкнути прапорець);
or rax, 0x02 — встановити біт 1 (увімкнути функцію).
Очищення бітів:
and al, 0xFE — очистити молодший біт (зробити число парним);
and rax, ~0x04 — очистити біт 2 (вимкнути функцію).
Перемикання станів:
xor al, 0xFF — інвертувати всі біти;
xor al, 0x08 — перемкнути біт 3.
Ефективна ініціалізація:
xor rax, rax — найшвидший спосіб обнулити регістр.
Ці патерни становлять основу маніпулювання бітами у системному програмуванні, драйверах пристроїв, криптографії, стисненні та графіці. Без їх опанування годі зрозуміти, як програмне забезпечення керує апаратурою на рівні окремих бітів.У реальному режимі x86 програмний код і дані поділяють один і той самий простір пам'яті. Під час асемблювання програми машинні інструкції записуються в пам'ять першими, а визначені в коді дані розміщуються одразу після них. Так утворюється неперервна структура, у якій виконуваний код плавно переходить у секцію даних.Якщо досліджувати пам'ять програми зневаджувачем на кшталт Turbo Debugger, побачимо сирі байти програми. Область CODE містить власне машинні інструкції (коди операцій та операнди), а область DATA — попередньо визначені значення: рядки, числа або масиви. Наприклад, класичний рядок "Hello world!$" виглядає як відповідні ASCII-байти: 48h 65h 6Ch 6Ch 6Fh…Така структура пам'яті — ключ до розуміння того, як структури даних працюють на машинному рівні. Коли йдеться про масиви, насправді йдеться про послідовні байти в області DATA, доступні за їхніми адресами в пам'яті.Для створення даних в асемблері застосовуємо директиви визначення даних. Найпростіша з них — db (Define Byte): вона резервує один або кілька байтів у пам'яті і за потреби ініціалізує їх конкретними значеннями.Базовий синтаксис:msg db 0AAh ; Зарезервувати 1 байт, ініціалізувати значенням AAh
Кілька байтів можна перерахувати через кому:msg db 0AAh, 0AAh, 0AAh, 0AAh, 0AAh
Однак такий підхід швидко стає виснажливим. А що, коли треба зарезервувати 1600 байтів? Набирати кожне значення окремо непрактично й чревате помилками. Саме тут асемблер пропонує елегантніше рішення.Оператор dup (скорочення від "duplicate" — дублювати) резервує набір однакових значень одним записом. Це асемблерний еквівалент оголошення масиву в мовах високого рівня.Синтаксис:label db count dup(value)
Приклади:buffer db 16 dup(0AAh) ; 16 байтів, усі ініціалізовані значенням AAh
zeros db 100 dup(0) ; 100 байтів, усі ініціалізовані нулем
empty db 1600 dup(?) ; 1600 неініціалізованих байтів
Символ ? означає «значення не має» — пам'ять зарезервована, але явно не ініціалізована, тож там залишаться попередні значення. Для навчальних цілей варто ініціалізувати характерним візерунком, наприклад AAh, щоб дані легко впадали в око у зневаджувачі.Чому це важливо:
Ефективність: один рядок замінює сотні окремих оголошень
Зрозумілість: намір (створити масив з N елементів) одразу очевидний
Гнучкість: розмір легко змінити, замінивши лише одне число
Саме так створюються масиви в асемблері. Масив — це просто неперервний блок пам'яті, і db count dup(value) дає нам рівно це.Після визначення масиву потрібні три основні операції: отримати його адресу, зчитати елемент і записати елемент.Важливе нагадування для TASM: перед будь-яким зверненням до даних завжди налаштовуйте регістр DS (сегмент даних) на сегмент даних вашої програми! Сегментні регістри автоматично на нього не вказують..model small
.data msg db 16 dup(0AAh) ; Наш масив .code
start: mov ax, @data ; Отримати адресу сегмента даних mov ds, ax ; Ініціалізувати DS ; Тепер можна безпечно звертатися до даних
Для роботи з масивом часто потрібна його початкова адреса (базова адреса):mov dx, offset msg
Оператор offset повертає зміщення msg у межах сегмента даних. Ця адреса вказує на перший байт масиву. Тепер DX можна використовувати для передачі цієї адреси у процедури, обчислення позицій елементів або виконання операцій введення-виведення.Для зчитування першого елемента (індекс 0):mov al, [msg]
Квадратні дужки [] позначають звернення до пам'яті — «зчитати байт за адресою, збереженою в msg». Ця інструкція завантажує в AL значення за базовою адресою масиву. Результат: AL містить 0AAh — те значення, яким масив було ініціалізовано.Для запису в конкретний елемент застосовується індексна адресація:mov [msg + 1], 0FFh
Ця інструкція записує 0FFh у другий елемент (індекс 1). Вираз msg + 1 обчислює адресу: базова адреса msg плюс зміщення в 1 байт. Після цього масив містить: AAh, FFh, AAh, AAh, AAh…Покроково виконуючи ці інструкції в Turbo Debugger, можна спостерігати зміну пам'яті в реальному часі: другий байт змінюється з AA на FF, а регістр AL містить AA (значення першого елемента).Доступ до елемента масиву описується простою математичною формулою:Адреса елемента = Базова адреса + (Індекс × Розмір елемента)Для масивів байтів (розмір елемента дорівнює 1 байту) формула спрощується до:Адреса елемента = Базова адреса + ІндексСаме тому msg + 1 звертається до другого елемента — до базової адреси додається одиниця.Приклади:msg db 10 dup(0) mov al, [msg + 0] ; Перший елемент (база + 0)
mov al, [msg + 1] ; Другий елемент (база + 1)
mov al, [msg + 9] ; Десятий елемент (база + 9)
Для більших розмірів елементів:Якщо є масив слів (по 2 байти кожне):numbers dw 10 dup(0) ; Масив з 10 слів mov ax, [numbers + 0] ; Перший елемент (база + 0*2)
mov ax, [numbers + 2] ; Другий елемент (база + 1*2)
mov ax, [numbers + 4] ; Третій елемент (база + 2*2)
Звернімо увагу: щоб звернутися до N-го слова, індекс треба помножити на 2 (розмір слова). Саме тому арифметика вказівників у C/C++ автоматично масштабується за розміром елемента — компілятор виконує рівно це обчислення.Динамічне індексування через регістри:Для змінних індексів зручно задіяти регістр:mov bx, 5 ; Індекс = 5
mov al, [msg + bx] ; Доступ до msg[5]
Або навіть:mov bx, offset msg ; BX = базова адреса
mov si, 5 ; SI = індекс
mov al, [bx + si] ; Доступ до msg[5]
Це основа всіх операцій з масивами в асемблері — а отже, і в кожній мові високого рівня. Коли ми пишемо array[i] на C, компілятор генерує саме такі обчислення адрес.Двовимірний масив — природне розширення одновимірного. Замість однієї послідовності маємо рядки та стовпці, але в пам'яті вони однаково зберігаються як єдиний неперервний блок.Оголошення з обчисленням:array db 5*10 dup(0) ; 5 рядків × 10 стовпців = 50 байтів
Асемблер обчислює 5*10 на етапі збирання та резервує 50 байтів. Такий запис робить розміри масиву явними просто в коді.Явна ініціалізація по рядках:Для наочності кожен рядок можна ініціалізувати окремо:array db 10 dup(01h) ; Рядок 0: усі 01h db 10 dup(02h) ; Рядок 1: усі 02h db 10 dup(03h) ; Рядок 2: усі 03h db 10 dup(04h) ; Рядок 3: усі 04h db 10 dup(05h) ; Рядок 4: усі 05h
Так утворюється масив 5×10, де кожен рядок заповнений своїм значенням, тож у зневаджувачі рядки легко розрізнити на око.Розуміти, як зберігаються двовимірні масиви, критично важливо для обчислення адрес елементів. В асемблері x86 (та в більшості мов сімейства C) двовимірні масиви використовують рядковий порядок (row-major order): рядки зберігаються послідовно в пам'яті.Візуалізація:Логічне подання (5 рядків × 10 стовпців): Рядок 0: 01 01 01 01 01 01 01 01 01 01
Рядок 1: 02 02 02 02 02 02 02 02 02 02
Рядок 2: 03 03 03 03 03 03 03 03 03 03
Рядок 3: 04 04 04 04 04 04 04 04 04 04
Рядок 4: 05 05 05 05 05 05 05 05 05 05 Розташування в пам'яті (лінійне, 50 байтів): [01 01 01 01 01 01 01 01 01 01 02 02 02 02 02 02 02 02 02 02 ...] ^--- Рядок 0 ------------------^ ^--- Рядок 1 ------------------^
<br><img alt="Розташування 2D-масиву в пам'яті" src="assets/l05-2d-array-memory-layout.png" target="_self">Кожен рядок займає 10 послідовних байтів. Якщо масив починається зі зміщення 120h (288₁₀), маємо:
Рядок 0 починається з: 288
Рядок 1 починається з: 288 + 10 = 298
Рядок 2 починається з: 288 + 20 = 308 (134h)
Рядок 3 починається з: 288 + 30 = 318
Рядок 4 починається з: 288 + 40 = 328
Такий рядковий порядок ідентичний тому, як масиви зберігаються в C/C++, тож патерни доступу до багатовимірних масивів напряму переносяться між асемблером і мовами високого рівня.Щоб звернутися до елемента array[row][col], треба обчислити його зміщення від базової адреси.Для 1-байтових елементів:Зміщення = (рядок × кількістьСтовпців) + стовпець
Для N-байтових елементів:Зміщення = (рядок × кількістьСтовпців × N) + (стовпець × N)
Логіка:
Пропустити рядок повних рядків: кожен рядок містить кількістьСтовпців елементів розміром N
У цільовому рядку пропустити стовпець елементів розміром N
Розібраний приклад: доступ до array[2][3] у байтовому масиві 5×10
Рядок = 2
Стовпець = 3
Кількість стовпців = 10
Розмір елемента = 1 байт
Обчислення:Зміщення = (2 × 10 × 1) + (3 × 1) = 20 + 3 = 23
Адреса елемента = Базова адреса + 23В асемблері:array db 5*10 dup(0) ; Масив 5×10 ; Доступ до array[2][3]
mov bx, 0 ; Обнулити BX для обчислення індексу
mov al, 2 ; AL = номер рядка (індексація з 0)
mov ah, 10 ; AH = загальна кількість стовпців на рядок
mul ah ; AX = AL × AH = 2 × 10 = 20
add bx, ax ; BX = 20 (початок рядка 2)
add bx, 3 ; BX = 23 (стовпець 3 у рядку 2) ; Тепер звертаємося до елемента
mov al, [array + bx] ; Зчитати array[2][3] у AL
Покроково:
mul ah: множимо рядок (2) на кількість стовпців (10) → AX = 20
add bx, ax: BX тепер вказує на початок рядка 2
add bx, 3: зсуваємо BX на 3 стовпці
[array + bx]: звертаємося до байта за базовою адресою + 23
Це рівно те обчислення, яке виконує компілятор C, коли ми пишемо array[2][3]. Синтаксис високого рівня — лише синтаксичний цукор для цієї низькорівневої адресної арифметики.1. Завжди ініціалізуйте DS у TASMЗабути налаштувати DS — одна з найпоширеніших помилок в асемблері:mov ax, @data
mov ds, ax
Без цього звернення до сегмента даних вказуватимуть на неправильну область пам'яті й призведуть до непередбачуваної поведінки.2. Масиви індексуються з нуляЯк і в C/C++, перший елемент має індекс 0. У масиві з 10 елементів допустимі індекси — 0–9, а не 1–10. Помилки на одиницю — типовий наслідок переходу від одиничної індексації (як у Pascal чи MATLAB).3. Розмір елемента має значенняПрацюючи з масивами слів (2 байти) або подвійних слів (4 байти), не забуваймо масштабувати індекси:numbers dw 10 dup(0)
mov bx, 3 ; Індекс 3
shl bx, 1 ; Помножити на 2 (розмір слова)
mov ax, [numbers + bx] ; Доступ до numbers[3]
Або скористаймося системою типів асемблера:mov ax, [numbers + 3*2] ; Асемблер обчислює 3*2 = 6
4. Перевірка меж — ваша відповідальністьНа відміну від мов високого рівня, асемблер не завадить звернутися до array[100], коли в масиві лише 10 елементів. Просто зчитається те, що трапиться за відповідною адресою в пам'яті — можливо, критичні дані або код. Завжди перевіряйте індекси перед використанням.5. Використовуйте змістовні міткиЗамість загадкових назв на кшталт d1 чи buf обирайте описові мітки:studentScores db 30 dup(0)
tempBuffer db 256 dup(?)
matrixData db 10*10 dup(0)
Так код легше супроводжувати, а помилок стає менше.6. Візуалізуйте за допомогою зневаджувачаТакі інструменти, як Turbo Debugger, дають змогу спостерігати, як змінюється пам'ять під час покрокового виконання коду. Для розуміння операцій з масивами це неоціненно: коли бачиш, як байти AAh перетворюються на FFh, абстрактні поняття стають відчутними.Усе, що ми розглянули, лежить в основі роботи масивів у C, C++ та інших мовах:Оголошення масиву в C:char msg[16];
Еквівалент в асемблері:msg db 16 dup(?)
Доступ до масиву в C:msg[3] = 0xFF;
Еквівалент в асемблері:mov [msg + 3], 0FFh
2D-масив у C:int matrix[5][10];
Еквівалент в асемблері (за умови 2-байтового int):matrix dw 5*10 dup(0)
Доступ до 2D-масиву в C:x = matrix[2][3];
Еквівалент в асемблері:mov ax, 2
mov bx, 10
mul bx ; AX = 2 * 10 = 20
mov bx, ax
shl bx, 1 ; Помножити на 2 (розмір int)
add bx, 3*2 ; Додати зміщення стовпця
mov ax, [matrix + bx]
Розуміння операцій з масивами на рівні асемблера пояснює:
чому доступ до масиву має часову складність O(1) (проста адресна арифметика);
чому буває переповнення буфера (жодної автоматичної перевірки меж);
чому багатовимірні масиви можуть проявляти проблеми з продуктивністю кешу (обхід у рядковому порядку проти стовпцевого);
чому в C вказівники й масиви так тісно переплетені.
Це та машинна реальність, що стоїть за кожною структурою даних в обчисленнях. Масив — найпростіша й найфундаментальніша структура, але ті самі принципи поширюються на зв'язані списки, дерева, хеш-таблиці й далі — усе побудоване на тих самих прийомах обчислення адрес.Стек — одна з найфундаментальніших структур даних в інформатиці. Її принцип простий, але потужний: LIFO (Last In, First Out — «останній прийшов, перший вийшов»), або, що те саме, FILO (First In, Last Out — «перший прийшов, останній вийшов»). Елементи додаються й вилучаються лише з одного кінця, який називають «вершиною» стека.<br><img alt="Покрокова анімація операцій Push і Pop зі стеком" src="assets/l05-stack-push-pop-steps.png" target="_self">Покрокова анімація push 2, 3, 4, 5, 6, а потім pop 6, 5, 4, 3, 2 — наочна демонстрація поведінки FILO.Простежимо послідовність операцій:
Push (вштовхнути) 2 → Стек: [2]
Push 3 → Стек: [2, 3]
Push 4 → Стек: [2, 3, 4]
Push 5 → Стек: [2, 3, 4, 5]
Push 6 → Стек: [2, 3, 4, 5, 6]
Pop (витягнути) → повертає 6, Стек: [2, 3, 4, 5]
Pop → повертає 5, Стек: [2, 3, 4]
Pop → повертає 4, Стек: [2, 3]
Pop → повертає 3, Стек: [2]
Pop → повертає 2, Стек: []
Порядок вилучення (6, 5, 4, 3, 2) — точне обернення порядку вставки (2, 3, 4, 5, 6). Саме ця властивість FILO робить стек ідеальним для відстеження вкладених контекстів: викликів функцій, тимчасового збереження змінних, обчислення виразів.<br><img alt="Візуалізація операцій зі стеком" src="assets/l05-stack-operations-visual.png" target="_self">Візуалізація порожнього стека, push 1, 2, 3, а потім pop 3; кольорові блоки позначають елементи стека.На практиці стек реалізують як неперервну ділянку пам'яті — по суті, одновимірний масив. Стек має фіксовану ємність, задану під час ініціалізації програми, а елементи зберігаються послідовно в межах цього зарезервованого простору.Стек особливо добре підходить для збереження та відновлення станів змінних. Коли програмі потрібно тимчасово зберегти поточні значення регістрів або локальних змінних — наприклад, під час виклику функції, — вона вштовхує ці значення у стек. Коли функція завершується, значення витягуються назад, і початковий стан відновлюється.У програмах на асемблері стек потрібно явно зарезервувати. Наприклад, у Turbo Assembler (TASM) директива:.stack 50
резервує 50 байтів пам'яті для стека. Ця ділянка стає окремим сегментом пам'яті, з яким програма виконує операції push і pop. Сегмент стека ідентифікується регістром SS (Stack Segment — сегмент стека), до якого ми перейдемо за мить.Теоретично стек можна було б реалізувати і формулами над масивом з ручною арифметикою вказівників. Проте процесор x86 забезпечує спеціальну апаратну підтримку операцій зі стеком. Чому?Відповідь полягає у частоті та критичності його використання. Збереження й відновлення контексту відбувається постійно в будь-якій нетривіальній програмі:
Виклики функцій мусять зберігати адреси повернення.
Виклики функцій часто зберігають стан регістрів тієї функції, що викликає.
Вкладені виклики функцій утворюють ланцюг контекстів, який потрібно розгортати у зворотному порядку.
Обробка переривань вимагає негайно зберегти стан процесора.
Оскільки ці операції настільки поширені та критичні для продуктивності, процесор надає:
Спеціалізовані регістри (SS і SP) для керування стеком.
Спеціальні інструкції (PUSH, POP, CALL, RET), що ефективно маніпулюють стеком.
Автоматичне оновлення вказівника, яке саме дбає про зростання чи зменшення стека.
Ця апаратна підтримка робить операції зі стеком швидшими й надійнішими, ніж ручна реалізація.Архітектура x86 використовує два ключові регістри для керування стеком:SS (Stack Segment Register — регістр сегмента стека)
містить адресу сегмента стека;
визначає, де в пам'яті розташований стек;
працює у парі з SP, формуючи повні адреси пам'яті.
SP (Stack Pointer — вказівник стека)
містить зміщення поточної вершини стека;
вказує на останній вштовхнутий у стек елемент;
автоматично оновлюється інструкціями PUSH і POP.
Повна адреса вершини стека утворюється сполученням цих регістрів: SS:SP.Ось концепція, яка часто дивує початківців: в архітектурі x86 стек росте донизу — тобто у бік нижчих адрес пам'яті.Під час запуску програми:
SP ініціалізується так, щоб вказувати на кінець (дно) виділеного для стека простору — на найвищу адресу в цій ділянці;
при вштовхуванні елементів SP зменшується;
при витягуванні елементів SP збільшується.
Спершу це здається контрінтуїтивним (чому «вершина» рухається до нижчих адрес?), але така вже стандартна угода в архітектурі x86. Зручна аналогія — складання тарілок на столі: починаємо згори доступного простору й рухаємося донизу.Високі адреси | | [Початкова позиція SP - дно стека] | ↓ [Стек зростає в цьому напрямку] | | [Поточна позиція SP - вершина стека] |
Низькі адреси
Інструкція PUSH атомарно виконує дві дії:
Зменшує SP на розмір операнда (зазвичай 2 байти для 16-бітних значень).
Записує значення в пам'ять за новою адресою SS:SP.
Отже, PUSH AX рівнозначне парі інструкцій:SUB SP, 2 ; зсунути вказівник стека вниз на 2 байти
MOV [SS:SP], AX ; записати AX на нову вершину стека
<br><img alt="Еквівалентність Push і Pop до SUB/MOV та MOV/ADD" src="assets/l05-push-pop-equivalence.png" target="_self">PUSH рівнозначне парі SUB SP, 2 і MOV; POP — парі MOV і ADD SP, 2.Інструкція PUSH приймає різні операнди [4]:
регістри загального призначення: AX, BX, CX, DX, DI, SI, BP, SP;
сегментні регістри: DS, ES, SS, CS;
операнди пам'яті: змінні або адреси в пам'яті;
безпосередні значення: константи (починаючи з процесора 80186).
Приклад:MOV AX, 0FFFFh ; AX = FFFFh
PUSH AX ; вштовхнути AX у стек
Якщо SP починав з 0010h, то:
SP стає 000Eh (0010h − 2);
пам'ять за адресою SS:000Eh тепер містить FFFFh (у форматі little-endian: FF FF).
Інструкція POP теж атомарно виконує дві дії:
Читає значення з пам'яті за адресою SS:SP.
Збільшує SP на розмір операнда (зазвичай 2 байти).
Отже, POP AX рівнозначне парі:MOV AX, [SS:SP] ; прочитати значення з вершини стека в AX
ADD SP, 2 ; зсунути вказівник стека вгору на 2 байти
Набір операндів у POP той самий, що й у PUSH, з одним винятком: не можна витягти значення у регістр CS (Code Segment — сегмент коду) [4], бо це непередбачувано зламало б хід виконання програми.Приклад:POP BX ; витягнути вершину стека в BX
Якщо SP дорівнює 000Eh, то:
BX отримує значення, збережене за адресою SS:000Eh;
SP стає 0010h (000Eh + 2).
Простежмо конкретний приклад, щоб наочно побачити поведінку стека.Початковий стан:SP = 0010h
Пам'ять за адресою SS:0010h не визначена (стек порожній)
Після MOV AX, 0FFFFh:AX = FFFFh
SP = 0010h (без змін)
Після PUSH AX:SP = 000Eh (зменшився на 2)
Пам'ять за адресою SS:000Eh: FF FFh (формат little-endian)
Стек виріс у бік нижчих адрес:Адреса Значення
-----------------
[0010] [не визначено - нижче вершини стека]
[000E] FF FFh ← SP тепер вказує сюди (вершина стека)
[000C] [не визначено]
...
Після POP BX:BX = 0FFFFh (значення зчитане зі стека)
SP = 0010h (збільшився на 2, повернувся до початкової позиції)
Стек знову порожній, а SP — у вихідній позиції.Класична демонстрація поведінки стека — обмін значень двох регістрів, де стек виконує роль тимчасового сховища:; початкові значення
MOV AX, 0FF55h ; AX = FF55h
MOV BX, 0CCDDh ; BX = CCDDh ; обмін через стек
PUSH AX ; Стек: [FF55h]
PUSH BX ; Стек: [FF55h, CCDDh]
POP AX ; AX = CCDDh, Стек: [FF55h]
POP BX ; BX = FF55h, Стек: [] ; кінцеві значення: AX = CCDDh, BX = FF55h
Простежмо, як змінюється стек:
Після PUSH AX: у стеку FF55h (з AX).
Після PUSH BX: у стеку FF55h, а зверху CCDDh (щойно вштовхнутий з BX).
Після POP AX: AX дістає CCDDh з вершини стека, у стеку лишається FF55h.
Після POP BX: BX дістає FF55h, стек порожній.
Принцип LIFO природно обертає порядок: те, що вштовхнули першим (AX), виходить останнім, а що вштовхнули останнім (BX) — виходить першим. Саме це й дає обмін значеннями.Архітектура x86 надає кілька інструкцій для роботи зі стеком.Базові операції:
PUSH операнд — вштовхнути одне значення у стек;
POP операнд — витягнути одне значення зі стека.
Групові операції:
PUSHA — однією інструкцією вштовхнути у стек усі регістри загального призначення (AX, CX, DX, BX, SP, BP, SI, DI);
POPA — витягнути всі регістри загального призначення у зворотному порядку.
Інструкції PUSHA і POPA стають у пригоді на вході в функцію та на виході з неї, коли треба зберегти весь стан регістрів процесора. Замість восьми окремих PUSH чи POP повне збереження або відновлення контексту виконує одна інструкція.Інструкції керування потоком виконання:
CALL адреса — вштовхнути адресу повернення у стек і перейти за заданою адресою;
RET — витягнути адресу повернення зі стека і перейти за нею.
Ці інструкції формують основу викликів функцій в асемблері — тему наступного розділу, якій ми приділимо значно більше уваги.Розгляньмо реальний приклад — як побачити поведінку стека на власні очі за допомогою Turbo Debugger.Налаштування програми:.stack 50 ; зарезервувати 50 байтів для стека
.code MOV AX, 55h PUSH AX POP BX
Початковий стан (після директиви .stack 50):
SP = 0032h (50 у десятковій, записано в шістнадцятковій);
SS = 087Dh (приклад адреси сегмента);
стек має ємність 50 байтів, від SS:0000h до SS:0031h;
SP вказує на 0032h — одразу за межами виділеного простору стека (його «дно»).
Після MOV AX, 55h:
AX = 0055h;
SP = 0032h (без змін — MOV на стек не впливає).
Після PUSH AX:
SP = 0030h (зменшився на 2: 0032h − 2 = 0030h);
пам'ять за адресою SS:0030h містить 55 00 (little-endian-представлення 0055h);
стек виріс у бік нижчих адрес.
Якщо подивитися на пам'ять за адресою DS:0030h (за умови DS = SS), побачимо:Адреса Значення
-----------------
[0032] [не визначено - нижче вершини стека]
[0030] 55 00 ← SP вказує сюди
[002E] [не визначено]
Варто звернути увагу на little-endian-формат: значення 0055h зберігається як 55 00, тобто молодший байт (55h) лежить за нижчою адресою.Після POP BX:
BX = 0055h (значення зчитане зі стека);
SP = 0032h (збільшився на 2: 0030h + 2 = 0032h);
стек повернувся до вихідного порожнього стану.
Цей покроковий розбір у зневаджувачі демонструє кілька ключових ідей:
SP починається з верхнього кінця області стека (з «дна»).
PUSH зменшує SP ще до запису.
Значення зберігаються у форматі little-endian.
POP зчитує значення, перш ніж збільшити SP.
Коли стек порожній, SP повертається до вихідної позиції.
Для програм, скомпільованих як COM-файли, вказівник стека SP ініціалізується значенням 0FFFEh [4]. Це дає приблизно 64 КБ простору для стека (максимум для 64-кілобайтного сегмента). Такого щедрого обсягу зазвичай достатньо, щоб типова програма не переповнила стек під час звичайного виконання.Натомість програми з явними директивами .stack (поширеними в EXE-файлах) задають розмір стека самі, відповідно до очікуваних потреб. Надто малий стек загрожує переповненням, а надто великий — марнує пам'ять.
Стек працює за принципом LIFO/FILO. Правило «останній прийшов — перший вийшов» — основа розуміння всіх операцій зі стеком.
Апаратна підтримка є необхідною. Процесор надає спеціалізовані регістри (SS, SP) та інструкції (PUSH, POP) саме тому, що операції зі стеком виконуються повсюди.
Напрямок зростання має значення. Стек на x86 росте у бік нижчих адрес (PUSH зменшує SP).
PUSH = зменшити, потім записати. SP зменшується ще до збереження значення.
POP = прочитати, потім збільшити. Значення зчитується ще до збільшення SP.
Формат little-endian. Багатобайтові значення зберігаються молодшим байтом за нижчою адресою.
Групові операції економлять час. PUSHA/POPA обробляють усі регістри загального призначення одним махом.
Збереження контексту — головне застосування. Саме здатність стека зберігати й відновлювати стан робить виклики функцій та обробку переривань можливими на практиці.
Розуміння стека — критично важливе для опанування асемблера. Майже кожна нетривіальна програма покладається на операції зі стеком для викликів функцій, зберігання локальних змінних та перемикання контексту. На розглянутих тут поняттях ґрунтується наступний розділ — про виклики процедур і передавання параметрів.На рівні мов високого рівня, як-от Java або C, ми працюємо з функціями та процедурами як з фундаментальними будівельними блоками. Функція приймає параметри, виконує певні обчислення і повертає значення. Процедура робить ту саму роботу, але значення не повертає (або повертає void). Розгляньмо простий приклад мовою Java:public static int add(int a, int b) { return a + b;
}
Ця абстракція настільки природна, що ми рідко замислюємося над тим, що відбувається на машинному рівні. А тим часом ключове спостереження таке: функцій на рівні процесора не існує. Процесор нічого не знає ні про параметри, ні про повернені значення, ні про локальні змінні. Натомість уся поведінка функцій тримається лише на двох інструкціях — CALL та RET.Однією з визначальних рис функції є те, що після завершення її виконання керування повертається до місця виклику — саме до тієї позиції, звідки функцію було викликано. І це має спрацьовувати незалежно від того, звідки саме надійшов виклик. Розгляньмо такий код мовою Java:public static void main(String[] args) { int x = add(3, 5); // First call site int y = add(10, 20); // Second call site
}
Та сама функція add() має повертатися до різних місць залежно від того, звідки її викликали. Як процесор дізнається, куди повертатися? Відповідь — адреса повернення, і стек є ідеальним місцем, щоб її тримати.Архітектура x86 має дві інструкції, які разом реалізують виклики функцій:CALL — викликає функцію:
Кладе адресу повернення (адресу наступної інструкції) на стек.
Виконує перехід на початкову адресу функції.
RET — повертається з функції:
Знімає адресу з вершини стека.
Переходить за цією адресою.
Ось найпростіший приклад у синтаксисі TASM:; Головна програма mov ax, 5 call procedure ; Виклик функції ; Після RET виконання продовжується тут procedure proc add ax, 10 ret ; Повернення до викликача
procedure endp
Виглядає просто, але вся елегантність прихована за лаштунками.По суті, інструкція CALL — це комбінація PUSH та JMP. Виконуючи call multiply, процесор здійснює такі кроки:; Що "call multiply" робить внутрішньо:
sub sp, 2 ; Виділити місце на стеку (16-бітна адреса)
mov SS:[sp], [ip+3] ; Зберегти адресу повернення (IP + довжина інструкції)
jmp multiply ; Перехід до функції
Адреса повернення дорівнює IP + 3, адже сама інструкція CALL займає 3 байти (1 байт коду операції + 2 байти адреси). Після виконання CALL на стеку опиняється адреса інструкції, що йде одразу за CALL, а вказівник інструкцій переходить до точки входу функції.Стан стека після CALL:SP → [Адреса повернення] ← Вершина стека [Попередні дані] ...
Інструкція RET — це зворотна операція: по суті POP, а за ним JMP. Виконуючи ret, процесор робить таке:; Що "ret" робить внутрішньо:
jmp [SS:[sp]] ; Перехід за адресою з вершини стека
add sp, 2 ; Видалити адресу зі стека
Тут важливий порядок: RET зчитує адресу з поточної вершини стека, переходить за нею і лише потім коригує вказівник стека. Отже, RET вважає адресою повернення те, що лежить на вершині стека. Якщо під час виконання функції щось було покладено на стек і не знято назад, RET перейде за помилковою адресою!Завдяки цій еквівалентності виклик функції можна реалізувати й вручну:; Ручний виклик функції (еквівалент "call multiply")
lea bx, return_point ; Завантажити адресу повернення
push bx ; Покласти її на стек
jmp multiply ; Перейти до функції
return_point: ; Продовження тут multiply: ; Тіло функції pop bx ; Зчитати адресу повернення jmp bx ; Повернутися назад
Та все ж CALL і RET роблять те саме ефективно на апаратному рівні.Тепер ми розуміємо, як викликати функцію та як з неї повернутися. Але функціям потрібно обмінюватися даними з викликачами — їм необхідні параметри (вхідні дані) і повернені значення (вихідні дані). Пам'ятаймо: параметрів на рівні процесора не існує. Процесор не знає такого поняття, як «перший параметр» чи «повернене значення». Це угоди, які ми маємо запровадити самі.Є два основні підходи до передавання параметрів — через регістри і через стек.Найпростіше — домовитися, що параметри передаватимуться в певних регістрах:; Викликач mov ax, 055h ; Перший параметр у AX mov bx, 0AAh ; Другий параметр у BX call multiply ; Результат повертається в DX:AX ; Викликана функція
multiply proc mul bx ; AX = AX * BX, старші біти — у DX ret ; Повертаємось з результатом у DX:AX
multiply endp
Швидко і просто, але з істотними обмеженнями:
Обмежена кількість параметрів. x86 має всього 8 регістрів загального призначення. А що робити, коли параметрів потрібно, скажімо, 10?
Неможлива рекурсія. Якщо multiply викличе іншу функцію, ці регістри будуть перезаписані.
Крихкий стан. Кожна функція мусить пильно стежити, які саме регістри вона змінює.
Для найпростіших випадків такий підхід працює, але не масштабується.Розв'язок — скористатися стеком: він здатен уміщувати будь-яку кількість даних і природно підтримує вкладені виклики:; Викликач push 10 ; Кладемо другий параметр push 5 ; Кладемо перший параметр call multiply add sp, 4 ; Очистити стек (2 параметри × 2 байти) ; Результат у AX
Зверніть увагу на зворотний порядок: параметри ми кладемо справа наліво. Саме така домовленість (конкретно — угода про виклик мови C) гарантує, що перший параметр опиниться на стеку найближче до адреси повернення.Але далі стає складніше. Усередині функції, звертаючись до параметрів на стеку, треба пам'ятати: адреса повернення — зверху:Стек на початку multiply:
SP → [Адреса повернення] ← це поклала CALL! [Параметр 1: 5] [Параметр 2: 10] ...
Тобто просто зробити pop ax, щоб дістати перший параметр, не вийде — натомість отримаємо адресу повернення! Один із варіантів — тимчасово зняти адресу повернення, а потім повернути її на місце:multiply proc pop bx ; Зняти адресу повернення (зберегти) pop ax ; Зняти перший параметр (5) pop dx ; Зняти другий параметр (10) push bx ; Повернути адресу повернення на місце! mul dx ; AX = AX * DX ret ; Тепер RET знайде адресу повернення
multiply endp
Воно працює, але дуже схильне до помилок. Треба щоразу пам'ятати покласти адресу повернення саме в потрібну позицію; а коли у функції є кілька шляхів виходу — кожен з них має робити це коректно.У професійному коді застосовують стандартизовані угоди про виклик — домовленості про те, як саме функції обмінюються даними. Угода про виклик мови C (її дотримуються C, C++ та багато інших мов) елегантно розв'язує проблему доступу до параметрів за допомогою базового вказівника.Угода розподіляє обов'язки між викликачем і викликаною функцією.Обов'язки викликача:
Зберегти регістри AX, CX, DX, якщо їхні значення знадобляться після виклику (це регістри, які зберігає викликач).
Покласти параметри на стек у зворотному порядку (справа наліво).
Виконати CALL.
Очистити параметри зі стека після повернення (в угоді C; в угоді Pascal — ні).
Забрати повернене значення з AX (або з DX:AX для 32-бітних значень).
Обов'язки викликаної функції:
Зберегти старий базовий вказівник: push bp.
Встановити новий базовий вказівник: mov bp, sp (BP тепер вказує на збережений BP; BP+2 — адреса повернення; BP+4 — перший параметр).
Зарезервувати місце на стеку для локальних змінних: sub sp, N.
Зберегти всі регістри, які вона змінюватиме (окрім AX, CX, DX, за які відповідає викликач).
Виконати обчислення.
Покласти повернене значення в AX.
Відновити збережені регістри.
Відновити вказівник стека: mov sp, bp.
Відновити базовий вказівник: pop bp.
Виконати RET.
Дотримання цієї угоди породжує фрейм стека — структуровану ділянку пам'яті стека для кожного виклику функції. Ось який вигляд має стек під час виконання функції:Старші адреси [Параметр 3] ← BP+16 [Параметр 2] ← BP+12 [Параметр 1] ← BP+8 [Адреса повернення] ← BP+4 [Збережений BP] ← BP (сюди вказує поточний базовий вказівник) [Локальна змінна 1] ← BP-2 [Локальна змінна 2] ← BP-4 [Збережені регістри]
SP→ [...] ← Поточна вершина стека
Молодші адреси
<br><img alt="Діаграма фрейму стека угоди про виклик C/C++" src="assets/l05-stack-frame-diagram.png" target="_self">Схема фрейму стека: збережені регістри, локальні змінні, збережений EBP, адреса повернення та параметри з позначками [ebp+N].Перевага BP в тому, що він дає сталу точку відліку. Навіть коли функція кладе й знімає дані зі стека (а отже, змінює SP), BP лишається зафіксованим і забезпечує стабільний доступ до параметрів та локальних змінних:
Параметри: [bp+8], [bp+12], [bp+16], ...
Локальні змінні: [bp-2], [bp-4], [bp-6], ...
Ось повна реалізація функції згідно з угодою про виклик C:; Код викликача push 20 ; Другий параметр push 10 ; Перший параметр call add_numbers add sp, 4 ; Очистити стек ; Результат тепер у AX ; Код викликаної функції
add_numbers proc ; Пролог функції push bp ; Зберегти старий базовий вказівник mov bp, sp ; Встановити новий базовий вказівник sub sp, 4 ; Зарезервувати місце для 2 локальних змінних ; Доступ до параметрів mov ax, [bp+4] ; Перший параметр (10) mov bx, [bp+6] ; Другий параметр (20) ; Обчислення add ax, bx ; AX = 10 + 20 = 30 ; Епілог функції mov sp, bp ; Відновити вказівник стека pop bp ; Відновити базовий вказівник ret ; Повертаємось з результатом у AX
add_numbers endp
Послідовність push bp / mov bp, sp на початку називають прологом функції, а mov sp, bp / pop bp наприкінці — епілогом функції. Цей шаблон трапляється практично в кожній скомпільованій функції мовою C.Розуміти угоди про виклик важливо з кількох причин:
Зневадження. Досліджуючи асемблерний код у зневаджувачі, треба розбиратися у фреймах стека — це дає змогу переходити між викликами функцій та перевіряти значення змінних.
Взаємодія. Якщо ви пишете асемблерні функції, які викликатимуться з C, чи навпаки — викликаєте функції C з асемблера, угоди мусите дотримуватися буквально.
Продуктивність. Коли знаєш, що насправді відбувається під час виклику функції, легше оцінити його вартість і ухвалювати обґрунтовані рішення щодо оптимізації.
Безпека. Чимало експлойтів (наприклад, переповнення буфера) працюють саме завдяки пошкодженню фреймів стека задля підміни адреси повернення.
Щоразу, коли ви пишете int x = add(3, 5); мовою C, компілятор породжує код, який:
Кладе на стек 5, а потім 3.
Виконує CALL — кладе адресу повернення та переходить до функції.
Формує фрейм стека за допомогою BP.
Звертається до параметрів через [bp+8] і [bp+12].
Кладе результат в AX.
Розбирає фрейм стека.
Виконує RET — знімає адресу повернення та повертається назад.
Прибирає параметри зі стека.
За тим, що здається простим викликом функції, стоїть ретельно організована послідовність операцій зі стеком, побудована лише на двох інструкціях — CALL та RET. Це основа, на якій виконується все сучасне програмне забезпечення: від ядра операційної системи до вебоглядачів та ігрових рушіїв. Зрозумівши її, ви починаєте бачити, що саме ваш код робить на машинному рівні.<br>Докладніше про механіку CALL та RET і про угоду про виклик C — див. <a data-tooltip-position="top" aria-label="https://blog.codefarm.me" rel="noopener nofollow" class="external-link is-unresolved" href="https://blog.codefarm.me" target="_self">[5] CALL/RET in x86 Assembly</a> та <a data-tooltip-position="top" aria-label="https://youtu.be/" rel="noopener nofollow" class="external-link is-unresolved" href="https://youtu.be/" target="_self">[9] Calling Convention Video</a>.
Структура програми має значення — TASM організовує код у сегменти (.data, .code, .stack), які безпосередньо відповідають сегментам пам'яті, адресованим через регістри DS, CS та SS
Машинний код — це байти — кожна інструкція складається з байта коду операції, за яким ідуть байти параметрів; асемблер перетворює мнемоніки саме на ці байти
Зберігання у форматі little-endian — x86 розміщує багатобайтові значення в пам'яті, починаючи з молодшого байта
IP керує виконанням — вказівник інструкцій зсувається на розмір кожної виконаної інструкції; JMP просто додає до IP відповідне зміщення
Масиви — це неперервна ділянка пам'яті — одновимірні масиви оголошують через db/dup; двовимірні масиви зберігаються порядково, зі зміщенням = (рядок × кількість стовпців) + стовпець
Стек є фундаментальним — це структура типу LIFO, апаратно реалізована через пару SS:SP; її використовують для тимчасового зберігання даних, викликів функцій і передавання параметрів
PUSH зменшує SP, POP збільшує SP — на x86 стек росте в напрямку молодших адрес
Функції — це CALL + RET — CALL кладе адресу повернення у стек і здійснює перехід; RET знімає цю адресу зі стека й повертає керування назад. На рівні процесора «функцій» як таких не існує
Угоди про виклик — це домовленості — угода C/C++ передає параметри через стек (їх кладуть справа наліво), використовує BP як базу стекового фрейму, а AX — для повернення значення
Що практикуватимемо:
Робота зі стеком у Turbo Debugger: спостереження за SS, SP та змінами пам'яті стека
Реалізація процедур з CALL та RET
Передача параметрів через регістри та через стек
Оголошення масивів у пам'яті та доступ до них
Перед лабораторною:
Знати, як PUSH та POP впливають на SP та пам'ять
Розуміти різницю між CALL/RET та JMP
Практикувати читання діаграм фреймів стека (де розташовані параметри, адреса повернення та локальні змінні) <br><a data-tooltip-position="top" aria-label="https://math.libretexts.org/Workbench/Numerical_Methods_with_Applications_(Kaw)/02%3A_Differentiation_of_Continuous_Functions/2.01%3A_Binary_Representation_of_Numbers" rel="noopener nofollow" class="external-link is-unresolved" href="https://math.libretexts.org/Workbench/Numerical_Methods_with_Applications_(Kaw)/02%3A_Differentiation_of_Continuous_Functions/2.01%3A_Binary_Representation_of_Numbers" target="_self">Fixed-Point Binary Representation of Numbers</a> — перетворення двійкових чисел і представлення з фіксованою комою
<br><a data-tooltip-position="top" aria-label="https://math.libretexts.org/Bookshelves/Combinatorics_and_Discrete_Mathematics/Applied_Discrete_Structures_(Doerr_and_Levasseur)/01%3A_Set_Theory/1.04%3A_Binary_Representation_of_Positive_Integers" rel="noopener nofollow" class="external-link is-unresolved" href="https://math.libretexts.org/Bookshelves/Combinatorics_and_Discrete_Mathematics/Applied_Discrete_Structures_(Doerr_and_Levasseur)/01%3A_Set_Theory/1.04%3A_Binary_Representation_of_Positive_Integers" target="_self">Hexadecimal (base 16)</a> — шістнадцяткова система числення
<br><a data-tooltip-position="top" aria-label="https://uk.wikipedia.org/wiki/%D0%9F%D1%80%D1%8F%D0%BC%D0%B8%D0%B9_%D0%BA%D0%BE%D0%B4" rel="noopener nofollow" class="external-link is-unresolved" href="https://uk.wikipedia.org/wiki/%D0%9F%D1%80%D1%8F%D0%BC%D0%B8%D0%B9_%D0%BA%D0%BE%D0%B4" target="_self">Прямий код (Wikipedia UK)</a> — представлення чисел у прямому коді (знак-величина)
<br><a data-tooltip-position="top" aria-label="https://faculty.kfupm.edu.sa/COE/shazli/coe205/Help/asm_tutorial_09.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://faculty.kfupm.edu.sa/COE/shazli/coe205/Help/asm_tutorial_09.html" target="_self">8086 Assembler Tutorial: Stack</a> — операції зі стеком в асемблері 8086
<br><a data-tooltip-position="top" aria-label="https://blog.codefarm.me/2017/12/23/call-ret-x86-assembly/" rel="noopener nofollow" class="external-link is-unresolved" href="https://blog.codefarm.me/2017/12/23/call-ret-x86-assembly/" target="_self">Calling Convention in x86 Assembly</a> — інструкції CALL/RET і угоди про виклик
<br><a data-tooltip-position="top" aria-label="https://uk.wikipedia.org/wiki/%D0%9E%D0%B1%D0%B5%D1%80%D0%BD%D0%B5%D0%BD%D0%B8%D0%B9_%D0%BA%D0%BE%D0%B4" rel="noopener nofollow" class="external-link is-unresolved" href="https://uk.wikipedia.org/wiki/%D0%9E%D0%B1%D0%B5%D1%80%D0%BD%D0%B5%D0%BD%D0%B8%D0%B9_%D0%BA%D0%BE%D0%B4" target="_self">Обернений код (Wikipedia UK)</a> — представлення чисел в оберненому коді
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Two%27s_complement" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Two%27s_complement" target="_self">Two's complement (Wikipedia)</a> — представлення чисел у доповняльному коді
<br><a data-tooltip-position="top" aria-label="https://adacomputerscience.org/concepts/proglang_bitwise_manipulation" rel="noopener nofollow" class="external-link is-unresolved" href="https://adacomputerscience.org/concepts/proglang_bitwise_manipulation" target="_self">Bitwise manipulation — Ada Computer Science</a> — практичні побітові операції
]]></description><link>lectures/l05.-організація-програми-—-стек-і-процедури.html</link><guid isPermaLink="false">Lectures/L05. Організація програми — стек і процедури.md</guid><pubDate>Mon, 20 Apr 2026 10:37:40 GMT</pubDate><enclosure url="assets/l05-real-mode-memory-addressing.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l05-real-mode-memory-addressing.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L05-NOTES. Організація програми — стек і процедури]]></title><description><![CDATA[Короткий довідник до Лекції 05. Повні матеріали: L05. Організація програми — стек і процедури.md.Чотири сегменти:Мінімальний шаблон:.model small
.stack 100h
.data msg db 'Hello$'
.code
main proc mov ax, @data ; Ініціалізація DS (обов'язково!) mov ds, ax ; ... код ... mov ax, 4C00h ; Завершення програми, повернення до DOS int 21h
main endp
end main
Процес компіляції: Вихідний код → tasm → .OBJ → tlink → .EXE → Завантажувач → ОЗП (з ініціалізованими регістрами)Фізична адреса = Сегмент × 16 + Зміщення (20-бітна, 1 МБ у реальному режимі)Дізнатися більше: <a data-tooltip-position="top" aria-label="https://faculty.kfupm.edu.sa/COE/shazli/coe205/Help/asm_tutorial_09.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://faculty.kfupm.edu.sa/COE/shazli/coe205/Help/asm_tutorial_09.html" target="_self">Підручник зі стека 8086</a>Приклад кодування: mov ax, 087Dh → байти B8 7D 08
B8 — код операції для «MOV imm16 → AX»
7D 08 — значення у форматі little-endian (молодший байт першим)
Little-endian (x86): молодший байт розміщується за найнижчою адресоюЗначення 0x1C2C3B4D у пам'яті: 4D 3B 2C 1C
Вказівник інструкцій (IP): після кожного виконання автоматично збільшується на довжину інструкції. Пара CS:IP задає адресу наступної інструкції.JMP використовує відносне зміщення, а не абсолютну адресу:EB 08 → IP = IP + 2 + 08h
Мітки — це абстракція асемблера; у машинному коді їх немає.Розміри даних:Десяткове → двійкове: повторне ділення на 2, залишки читаємо знизу вгору23 ÷ 2 = 11 r1, 11 ÷ 2 = 5 r1, 5 ÷ 2 = 2 r1, 2 ÷ 2 = 1 r0, 1 ÷ 2 = 0 r1
→ 10111₂
Дробові двійкові числа: 0.101₂ = 2⁻¹ + 2⁻³ = 0,5 + 0,125 = 0,625₁₀
Перетворення десяткового дробу в двійковий: повторне множення на 2
Вісімкова система (основа 8): кожна цифра — 3 біти. Використовується у правах доступу Unix (chmod 755).Доповняльний код: інвертувати всі біти, додати 1. Детальніше — у Л02.Три практичні шаблони (таблиці істинності — див. Л04):Ключові ідіоми:
test al, 1 → перевірка парності/непарності (встановлює прапорці, результат не зберігається);
xor rax, rax → найшвидший спосіб обнулити регістр;
комбінація AND+OR → заміна бітового поля.
<br>Дізнатися більше: <a data-tooltip-position="top" aria-label="https://adacomputerscience.org/concepts/proglang_bitwise_manipulation" rel="noopener nofollow" class="external-link is-unresolved" href="https://adacomputerscience.org/concepts/proglang_bitwise_manipulation" target="_self">Побітові маніпуляції — Ada CS</a>Оголошення: db (байт), dw (слово), dd (подвійне слово).buffer db 16 dup(0AAh) ; 16 байтів, ініціалізованих значенням AAh
empty db 100 dup(?) ; 100 неініціалізованих байтів
Три операції:mov dx, offset msg ; 1. отримати базову адресу
mov al, [msg] ; 2. прочитати елемент (індекс 0)
mov [msg + 1], 0FFh ; 3. записати елемент (індекс 1)
Формула адреси: Адреса елемента = База + (Індекс × Розмір елемента)Динамічна індексація:mov bx, 5
mov al, [msg + bx] ; доступ до msg[5]
Двовимірні масиви — зберігаються в порядку по рядках:array db 5*10 dup(0) ; 5 рядків × 10 стовпців
Формула адреси (2D): Зміщення = (рядок × кількість_стовпців) + стовпецьПринцип: LIFO (Last In, First Out — «останній прийшов, перший пішов»): push 2, 3, 4 → pop повертає 4, 3, 2.Регістри: SS (сегмент стека) + SP (вказівник стека) → SS:SP = вершина стекаВажливо: у x86 стек росте донизу (у бік молодших адрес).
PUSH зменшує SP
POP збільшує SP
Операція PUSH (2 кроки):SUB SP, 2 ; 1. зменшити SP
MOV [SS:SP], AX ; 2. записати значення
Операція POP (2 кроки):MOV AX, [SS:SP] ; 1. прочитати значення
ADD SP, 2 ; 2. збільшити SP
Групові операції:
PUSHA — зберегти всі регістри загального призначення (AX, CX, DX, BX, SP, BP, SI, DI)
POPA — відновити всі у зворотному порядку
Ідіома обміну:PUSH AX ; стек: [AX]
PUSH BX ; стек: [AX, BX]
POP AX ; AX = старе BX
POP BX ; BX = старе AX
<br>Докладніше: <a data-tooltip-position="top" aria-label="https://faculty.kfupm.edu.sa/COE/shazli/coe205/Help/asm_tutorial_09.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://faculty.kfupm.edu.sa/COE/shazli/coe205/Help/asm_tutorial_09.html" target="_self">Операції зі стеком 8086</a>На рівні процесора функцій не існує — є лише CALL та RET.CALL = PUSH адреси повернення + JMP до функції
RET = POP адреси повернення + JMP за нею call myFunc ; зберегти наступний IP, перейти до myFunc ; після ret виконання продовжиться тут myFunc proc ; ... виконання роботи ... ret ; витягнути адресу, повернутися назад
myFunc endp
Увага: RET переходить за адресою з вершини стека — якщо залишити на стеку власні дані й не відновити його, RET перейде за «сміттєвою» адресою.Передача параметрів через стек (заштовхуються справа наліво):push 20 ; Другий параметр
push 10 ; Перший параметр
call add_numbers
add sp, 4 ; Викликач очищує стек (угода C)
Структура фрейму стека: [Параметр 2] ← BP+6 [Параметр 1] ← BP+4 [Адреса повернення] ← BP+2 [Збережений BP] ← BP [Локальні змінні] ← BP-2, BP-4, ...
SP→ [...]
Пролог/епілог функції:; Пролог
push bp ; Зберегти BP викликача
mov bp, sp ; Встановити вказівник фрейму
sub sp, N ; Зарезервувати місце для локальних змінних ; Епілог
mov sp, bp ; Відкинути локальні змінні
pop bp ; Відновити BP викликача
ret
Розподіл обов'язків:<br>Дізнатися більше: <a data-tooltip-position="top" aria-label="https://blog.codefarm.me/2017/12/23/call-ret-x86-assembly/" rel="noopener nofollow" class="external-link is-unresolved" href="https://blog.codefarm.me/2017/12/23/call-ret-x86-assembly/" target="_self">CALL/RET в асемблері x86</a>
Сегменти TASM (.data, .code, .stack) відображаються на сегментні регістри (DS, CS, SS)
Машинний код = байти коду операції + байти параметрів у форматі little-endian
IP автоматично просувається; JMP додає відносне зміщення до IP
Масиви = неперервна пам'ять; 2D використовує порядок по рядках: зміщення = рядок × стовпці + стовпець
Стек — LIFO, росте донизу; PUSH зменшує SP, POP збільшує SP
CALL = push адреси повернення + JMP; RET = pop + JMP
Угода про виклик C: параметри у стеку (справа наліво), BP для фрейму, AX для значення, що повертається
Що практикуватимемо:
Спостереження за операціями стека в Turbo Debugger (SS, SP, зміни пам'яті)
Реалізація процедур з CALL/RET
Передача параметрів через регістри та через стек
Оголошення масивів у пам'яті та доступ до їхніх елементів
Перед лабораторною варто:
Знати, як PUSH/POP впливають на SP та пам'ять
Розуміти різницю між CALL/RET та JMP
Потренуватися читати діаграми фрейму стека
]]></description><link>lectures/l05-notes.-організація-програми-—-стек-і-процедури.html</link><guid isPermaLink="false">Lectures/L05-NOTES. Організація програми — стек і процедури.md</guid><pubDate>Mon, 20 Apr 2026 10:37:40 GMT</pubDate></item><item><title><![CDATA[demo_key]]></title><description><![CDATA[Дано: 0x41200000 (= +10)(а) Біти: S = 0, E = 10000010 (130), M = 010....(б) Справжній порядок: e = E − 127 = 130 − 127 = 3. Знак: +.(в) Повна мантиса з прихованим бітом: 1.010 = 1.25.(г) Значення: +1.010₂ × 2^3 = +10.Рядок звернень: 7 0 1 2 0 3 0 2 1 2. Фреймів: 3. Алгоритм: FIFO.(а) Трасування:(б) Всього промахів: 8 (влучань: 2). Перше ВЛУЧЕННЯ на кроці 5. Найдовша послідовність ПРОМАХІВ підряд: 4 кроки.(а) VA = 0x7F3A4BC5D123 = 011111110.011101001.001011110.001011101.000100100011
L4 = 254
L3 = 233
L2 = 94
L1 = 93
Offset = 291 (0x123)
(б) Так, VA₂ = VA + 0x400 = 0x7F3A4BC5D523 знаходиться на ТІЙ САМІЙ сторінці. Співпадають усі чотири індекси (L4, L3, L2, L1); змінюється лише offset: 291 → 1315.4.1. [L08] Кеш-рядок — мінімальна одиниця передачі між кешем та пам'яттю. 64 байти — компроміс між накладними витратами на тег (менше → краще) та кількістю «зайвих» байтів, які завантажуються, але не використовуються (менше → краще). При послідовному обході int-масиву один кеш-рядок містить 16 сусідніх елементів → 15 з 16 звернень — кеш-влучання (miss rate ≈ 6%).4.2. [L12, Lab12] VmSize зросте одразу на 256 MB — це віртуальна пам'ять, зарезервована за процесом. VmRSS (resident set, реальні фрейми) майже не зміниться — demand paging: фізичні сторінки виділяються лише при першому зверненні до кожної з них (minor fault). Це дозволяє виділяти набагато більше віртуальної пам'яті, ніж реально потрібно.4.3. [L14, Lab14] Один акумулятор створює ланцюг RAW-залежності: кожна FMA чекає на попередню (~4 такти латентності). CPU має 2 FMA-порти, але не може їх завантажити, бо операції послідовні. Чотири незалежні акумулятори розривають ланцюг — сусідні FMA незалежні, OoO-ядро запускає їх паралельно, використовуючи обидва порти.]]></description><link>final/demo_key.html</link><guid isPermaLink="false">final/demo_key.md</guid><pubDate>Fri, 17 Apr 2026 19:10:59 GMT</pubDate></item><item><title><![CDATA[demo_variant]]></title><link>final/demo_variant.html</link><guid isPermaLink="false">final/demo_variant.pdf</guid><pubDate>Fri, 17 Apr 2026 19:10:59 GMT</pubDate></item><item><title><![CDATA[Methodology]]></title><description><![CDATA[
Цей документ показує послідовність дій для кожного типу завдання, на прикладі <a class="internal-link" data-href="demo_variant.pdf" href="final/demo_variant.html" target="_self" rel="noopener nofollow">демо-варіанта</a>. Короткі відповіді до того самого варіанта — у <a class="internal-link" data-href="demo_key.md" href="final/demo_key.html" target="_self" rel="noopener nofollow">demo_key.md</a>.
⚠️ Не намагайтеся запам'ятати ці приклади — на контролі будуть інші числа. Мета цього документа — навчити вас механіки, а не розв'язків конкретних задач.
<br>Джерело: <a class="internal-link" data-href="../Lectures/L13. Арифметика з рухомою комою.md" href="lectures/l13.-арифметика-з-рухомою-комою.html" target="_self" rel="noopener nofollow">L13</a>, <a class="internal-link" data-href="../Labs/Lab13.md" href="labs/lab13.html" target="_self" rel="noopener nofollow">Lab13</a>
Переведіть hex у 32 біти — кожна hex-цифра = 4 біти. 0x4 = 0100, 0xA = 1010, 0xF = 1111 тощо.
Розбийте на три поля: S (1 біт) | E (8 біт) | M (23 біти).
Обчисліть справжній порядок e = E − 127. Знак: S=0 → +, S=1 → −.
Додайте прихований біт до мантиси: 23 біти в пам'яті → значення 1.MMM...M у двійковій системі. Переведіть у десяткову: кожен біт після коми — 1/2, 1/4, 1/8, 1/16, ... Сума дає значення мантиси (від 1.0 до ~2.0).
Фінальне значення: знак × (1.MMM) × 2ᵉ.
Крок 1. Переведення у біти: 4 1 2 0 0 0 0 0
0100 0001 0010 0000 0000 0000 0000 0000
Крок 2. Розбивка 1 | 8 | 23:S = 0
E = 1000 0010
M = 010 0000 0000 0000 0000 0000
Крок 3. Справжній порядок: E = 1000 0010₂ = 128 + 2 = 130.
e = 130 − 127 = 3. Знак S=0 → +.Крок 4. Додаємо прихований біт:
Двійкова мантиса (з прихованим бітом): 1.010 (решта — нулі, їх можна відкинути).
Переведення у десяткову: 1.010₂ = 1 + 0/2 + 1/4 = 1.25.
Крок 5. Значення: +1.25 × 2³ = +1.25 × 8 = +10.Навчіться впізнавати ці шаблони — вони перекривають ~90% випадків на контролі:
Забули прихований біт → отримали значення у 2 рази менше, ніж треба. 23 біти в пам'яті = 24-бітне справжнє значення.
Не відняли 127 → отримали e = 130 замість 3, значення «2¹³⁰» замість «8». Це катастрофічна помилка — перевіряйте масштаб результату.
S=0 → від'ємне — часта плутанина. Запам'ятайте: 0 → додатне (як у звичайному коді), 1 → від'ємне.
Неправильна розбивка байтів — рахуйте біти: 1 + 8 + 23 = 32 (завжди). Порядок з MSB: S у старшому біті 31, E у бітах 30–23, M у бітах 22–0.
<br>Джерело: <a class="internal-link" data-href="../Lectures/L12. Віртуальна пам'ять поглиблено.md" href="lectures/l12.-віртуальна-пам'ять-поглиблено.html" target="_self" rel="noopener nofollow">L12</a>, <a class="internal-link" data-href="../Labs/Lab12.md" href="labs/lab12.html" target="_self" rel="noopener nofollow">Lab12</a>
Намалюйте таблицю перед початком: # | Стор. | F0 | F1 | F2 | Результат.
Заведіть вказівник черги (queue_head) на полях таблиці: стартує з F0. Оновлюватиметься лише при витісненні.
Для кожного кроку i → сторінка p:
a. Перевірте, чи p вже у якомусь Fi. Якщо так: Це ВЛУЧЕННЯ (hit). Скопіюйте стан фреймів з попереднього рядка без змін. queue_head не чіпайте.
b. Інакше це ПРОМАХ (miss):
Знайдіть перший порожній фрейм (F0, F1, F2 — у порядку). Якщо є — запишіть p туди. queue_head не чіпайте (ви не витіснили жодну сторінку, лише заповнили порожній слот).
Якщо всі зайняті — витісніть сторінку з фрейму F[queue_head]. Запишіть там p. Оновіть queue_head: (queue_head + 1) mod 3. Додайте до результату: «ПРОМАХ (витіснено X)». Після 10 кроків порахуйте загальну кількість ПРОМАХІВ і впишіть одну спостережувану властивість (на якому кроці перший HIT; яка найдовша серія MISS).
Запишіть у полях: queue_head = F0. Стартовий стан фреймів: [-, -, -].Підсумок: 8 ПРОМАХІВ, 2 ВЛУЧЕННЯ. Перше ВЛУЧЕННЯ — на кроці 5.
На HIT фрейми не змінюємо! Часта помилка — студенти «зсувають» сторінки у фреймах при hit. Ні: після hit стан ідентичний попередньому рядку.
Забули оновити queue_head при витісненні. Витіснили з F0 → queue_head тепер F1. Наступне витіснення повинно бути з F1, не знову з F0.
queue_head мав би оновитися при заповненні порожнього слота — ні. queue_head рухається лише коли дійсно витіснили сторінку (усі слоти були повні).
«Забули» позначити витіснену сторінку в результаті. Формат: «ПРОМАХ (витіснено X)» де X — та сторінка, що була у фреймі, який ви щойно переписали.
<br>Джерело: <a class="internal-link" data-href="../Lectures/L12. Віртуальна пам'ять поглиблено.md" href="lectures/l12.-віртуальна-пам'ять-поглиблено.html" target="_self" rel="noopener nofollow">L12</a>, <a class="internal-link" data-href="../Labs/Lab12.md" href="labs/lab12.html" target="_self" rel="noopener nofollow">Lab12</a> 47..39 38..30 29..21 20..12 11..0
┌────────┬────────┬────────┬────────┬────────────┐
│ L4 (9) │ L3 (9) │ L2 (9) │ L1 (9) │ Offset(12) │
└────────┴────────┴────────┴────────┴────────────┘
Сторінка = 4 KB = 4096 Б = 0x1000 = 12 біт offset. Дві адреси на одній сторінці мають однакові L4/L3/L2/L1 і відрізняються лише offset.Підхід 1 — через двійкове представлення (рекомендований на папері):
Переведіть hex у 48-бітне двійкове (12 hex-цифр → 48 біт).
Розставте вертикальні розділювачі: [9][9][9][9][12] від старшого до молодшого.
Переведіть кожну групу у десяткову: 9 біт → число від 0 до 511; 12 біт → 0 до 4095 (offset).
Підхід 2 — через зсуви та маски (швидший, але легше помилитися):L4 = (VA &gt;&gt; 39) &amp; 0x1FF # беремо біти 47..39
L3 = (VA &gt;&gt; 30) &amp; 0x1FF # біти 38..30
L2 = (VA &gt;&gt; 21) &amp; 0x1FF # біти 29..21
L1 = (VA &gt;&gt; 12) &amp; 0x1FF # біти 20..12
Offset = VA &amp; 0xFFF # біти 11..0
0x1FF = 9 одиниць у двійковій (2⁹ − 1 = 511). 0xFFF = 12 одиниць (2¹² − 1 = 4095).Крок 1. Переведення у 48 біт (12 hex-цифр): 7 F 3 A 4 B C 5 D 1 2 3
0111 1111 0011 1010 0100 1011 1100 0101 1101 0001 0010 0011
Крок 2. Розбивка 9|9|9|9|12 (від MSB):011111110 . 011101001 . 001011110 . 001011101 . 000100100011 L4 L3 L2 L1 Offset
Крок 3. Переведення кожної групи у десяткову:
L4 = 011111110₂ = 128+64+32+16+8+4+2 = 254
L3 = 011101001₂ = 128+64+32+8+1 = 233
L2 = 001011110₂ = 64+16+8+4+2 = 94
L1 = 001011101₂ = 64+16+8+4+1 = 93
Offset = 000100100011₂ = 256+32+2+1 = 291 (= 0x123) Поточний offset = 0x123 = 291
0x123 + 0x400 = 0x523 = 1315
0x523 &lt; 0x1000 → так, VA₂ на тій самій сторінці
L4, L3, L2, L1 не змінилися; лише offset змінився 291 → 1315. Забули, що 4 KB = 0x1000 = 12 біт offset — підставили 0x100 (256 байт) або 0x10000 (64 KB). Розмір сторінки x86-64 = 4096 байтів, завжди.
Плутанина з індексами L4 та L1 — хто старший? L4 — старший (біти 47..39), L1 — молодший (біти 20..12). Offset — найменший (11..0).
Індекси обмежені 0..511 (9 біт), offset — 0..4095 (12 біт). Якщо отримали L3 = 1000 — помилка у декомпозиції.
На «та сама сторінка?» плутають offset з повною адресою. Питання лише про те, чи залишився номер сторінки (VA &gt;&gt; 12) тим самим.
<br>Джерело: L08 / L09 / L10 (одне питання) + L11 / L12 / L13 / L14 (два питання) — див. <a class="internal-link" data-href="README.md" href="readme.html" target="_self" rel="noopener nofollow">README.md</a> «Що потрібно підготувати → Завдання 4».Не потрібно есе. Формат «правильної» відповіді:
Ключове твердження (1 речення) — суть поняття. Використовуйте термінологію з лекційних нотаток — вона зараховується як правильна.
Механізм або причина (1 речення) — чому це так, на рівні заліза / ОС / алгоритму.
Один конкретний приклад або число — якщо доречно (cache line = 64 Б; minor fault ≈ 1–10 мкс; AVX2 = 8 × float32).
Разом — 2–3 речення. Більше не треба. Менше, коли питання просить приклад, — ризикуєте втратити 0.5 бала.
64 байти — компроміс між кількістю тегів у кеші (менше → краще) і «зайвим» завантаженням сусідніх байтів, яких програма не використовує (менше → краще). При послідовному обході int-масиву один рядок містить 16 сусідніх елементів, тому 15 з 16 звернень — cache hit (≈6% miss rate). Це конкретно й демонструє, як cache line впливає на просторову локальність.
Три речення: твердження, механізм, число з прикладом. Структура, що гарантовано дає повний бал.
Занадто довге есе. Якщо ви написали більше 4 речень — швидше за все, там є «вода». Коротше = чіткіше = легше оцінити.
«Правильні слова» без суті. Написати «ASLR — це механізм безпеки» і зупинитися — 0.5 бала. Потрібно показати як саме він допомагає (рандомізує адреси → атакуючий не може передбачити, куди стрибнути).
Не використали термінологію з лекцій. Якщо ви називаєте page fault «запит на сторінку» — зарахується, але перевіряючий буде вагатися. Говоріть мовою лекційних нотаток.
Пишете «я не пам'ятаю, але…» — викреслюйте й починайте спочатку. Не залишайте такого у фінальному тексті.
<br>Повний список — у <a class="internal-link" data-href="README.md" href="readme.html" target="_self" rel="noopener nofollow">README.md</a> → розділ «Що потрібно підготувати → Завдання 4: список тем». Коротко: кожен з чотирьох кластерів L11–L14 та один кластер L08–L10 — готуйтеся до всіх, бо порядок питань рандомний.
Перегляньте весь листок 2 хв перед початком. Зрозумійте, що саме вам дали (яке hex, яка довжина FIFO-рядка, яка VA). Це дасть психологічну опору.
Почніть із Завдання 2 (FIFO). Воно найбільш механічне — як тільки ви запустите цикл «перевір → запиши рядок», швидко отримаєте 3-4 бали. Орієнтир: 15–20 хв.
Далі Завдання 1 (IEEE 754). Також механічне, швидке для підготовленого студента. Орієнтир: 10–15 хв.
Завдання 3 (VA). Потребує акуратності. Рекомендуємо перевести hex у двійкове на чернетці зверху аркуша, потім розставити роздільники. Орієнтир: 15–20 хв.
Завдання 4 (теорія) — наприкінці. Якщо ви знаєте тему — 2 хв на питання. Якщо не знаєте — спочатку пропустіть і поверніться. Орієнтир: 10–15 хв.
Залиште 5 хв наприкінці на перевірку: особливо FIFO-таблицю (найбільша за обсягом письма й тому з найбільшою ймовірністю дурних помилок).<br>Якщо ви можете самостійно без підказок пройти через демо-варіант по кроках, які описані вище, за ~45 хвилин — ви добре підготовані. Вирішуйте задачі з <a class="internal-link" data-href="practice.md" href="final/practice.html" target="_self" rel="noopener nofollow">practice.md</a> до тих пір, поки механіка не стане автоматичною.]]></description><link>final/methodology.html</link><guid isPermaLink="false">final/Methodology.md</guid><pubDate>Fri, 17 Apr 2026 17:49:37 GMT</pubDate></item><item><title><![CDATA[README]]></title><description><![CDATA[
Матеріал: лекції L11–L14 та лабораторні Lab11–Lab14 (основне). Теоретичний розділ (Завдання 4) також може охоплювати окремі поняття з L08 (ієрархія пам'яті), L09 (моделі обчислень), L10 (сучасні архітектури) — див. список тем нижче.
Максимальний бал: 15
Тривалість: 70 хвилин
Формат: 4 завдання на 3 сторінках A4, письмово
Дозволені матеріали: нічого (closed-book)
Файл <a class="internal-link" data-href="demo_variant.pdf" href="final/demo_variant.html" target="_self" rel="noopener nofollow">demo_variant.pdf</a> — приклад контрольної з тим самим форматом і типами завдань, але іншими даними.<br>Рекомендуємо розв'язати демо-варіант самостійно, а потім звірити з відповідями: <a class="internal-link" data-href="demo_key.md" href="final/demo_key.html" target="_self" rel="noopener nofollow">demo_key.md</a>.<br>Якщо ви не впевнені, як саме підступитися до кожного типу завдання — прочитайте <a class="internal-link" data-href="Methodology.md" href="final/methodology.html" target="_self" rel="noopener nofollow">Methodology.md</a>: покрокові алгоритми розв'язання, повний розбір демо-варіанта та типові помилки, на яких студенти втрачають бали.<br>Додатково — добірка вправ для підготовки (інші задачі, тих самих типів і тем): <a class="internal-link" data-href="practice.md" href="final/practice.html" target="_self" rel="noopener nofollow">practice.md</a>. На самому контролі будуть інші числа та інші теоретичні питання з того самого списку тем.
Бітова структура 32-бітного float: знак (1 біт), зміщений порядок E (8 біт), мантиса M (23 біти)
Зміщення (bias) = 127: справжній порядок e = E − 127
Прихований біт: нормалізована мантиса завжди починається з 1., тому цю одиницю не зберігають
Перетворення 32-бітного hex → S / E / M → значення у десятковій системі
Розуміння, чому значення 1.MMM у двійковій дорівнює 1 + M/2²³ у десятковій Механіка FIFO: порожні фрейми заповнюються послідовно; після заповнення — наступна сторінка витісняє ту, що прийшла найперше
Поняття ВЛУЧЕННЯ (hit) vs ПРОМАХ (miss) vs ПРОМАХ з витісненням
Вказівник «черги» (queue head) — індекс наступної жертви витіснення
Після hit стан фреймів не змінюється (це типова помилка!)
Різниця між FIFO та LRU (LRU — лише в теорії; у Завданні 2 трасуєте лише FIFO) 48-бітна віртуальна адреса розбивається на 5 полів: L4 | L3 | L2 | L1 | Offset (по 9 біт перших чотирьох + 12 біт offset)
Декомпозиція зсувами та масками:
L4 = (VA &gt;&gt; 39) &amp; 0x1FF
L3 = (VA &gt;&gt; 30) &amp; 0x1FF
L2 = (VA &gt;&gt; 21) &amp; 0x1FF
L1 = (VA &gt;&gt; 12) &amp; 0x1FF
Offset = VA &amp; 0xFFF Розмір сторінки = 4 KB = 4096 Б = 0x1000: дві адреси на одній сторінці мають однакові L4/L3/L2/L1 та відрізняються лише offset
Додавання Δ до VA — коли переноситься у L1? У L2? (при кратних 0x1000 / 0x200000)
Зворотна операція: зібрати VA з п'яти індексів
Одне питання в цьому завданні — з ширшого контексту (L08–L10), два — з основного матеріалу контролю (L11–L14). На іспиті будуть інші формулювання з цих тем — готуйтеся за лекційними нотатками.Ширший контекст (L08–L10, одне з цих питань у кожному варіанті):
L08 — ієрархія пам'яті: SRAM vs DRAM, порядки затримок (L1 → DRAM → SSD → HDD), навіщо 3 рівні кешу
L08 — введення-виведення: port-mapped vs memory-mapped I/O, приклади з x86
L09 — моделі обчислень: РАМ vs РАСП машина (Гарвардська vs фон Неймана), роль непрямої адресації, uniform vs logarithmic cost
L10 — сучасні архітектури: RISC vs CISC, Out-of-Order execution, спекулятивне виконання + Spectre, CPU vs GPU
Основний матеріал (L11–L14, два питання в кожному варіанті):Кеш та пам'ять (L11, Lab11):
Cache line (64 Б) та просторова локальність
Row-major vs column-major обхід матриці
Обчислення очікуваного miss rate за розміром cache line та stride
Тайлінг (blocking) — навіщо
Віртуальна пам'ять (L12, Lab12):
ASLR та безпека
Copy-on-Write, швидкий fork()
Minor vs major page fault — вартість і механізм
Clock vs LRU — чому ОС використовують Clock
TLB та ціна промаху TLB
Арифметика з рухомою комою (L13, Lab13):
Чому 0.1 не представляється точно
Катастрофічне скорочення
Поглинання (absorption) при додаванні чисел різних порядків
Чому 0.1 + 0.2 ≠ 0.3 на рівні бітів
SIMD та ILP (L14, Lab14):
SIMD vs ILP як різні форми паралелізму
FMA — що робить одна інструкція, чому це ефективно
RAW-залежності та чому ILP обмежується ними
AVX2: 256-біт = 8 × float32 або 4 × float64
Горизонтальна редукція у SIMD <br>
Тренуйте декодування IEEE 754 на папері. Потренуйтеся на <a data-tooltip-position="top" aria-label="https://float.exposed/" rel="noopener nofollow" class="external-link is-unresolved" href="https://float.exposed/" target="_self">float.exposed</a>: наберіть число (наприклад, 6.25), подивіться на бітовий запис, спробуйте відновити значення з hex без інструменту. <br>
Зробіть шпаргалку FIFO самому собі. Після третьої сторінки FIFO-трасування це стає механічним — але на контролі кожна помилка «забув оновити чергу» коштує 0.5 бала. Тренуйтеся на <a class="internal-link" data-href="demo_variant.pdf" href="final/demo_variant.html" target="_self" rel="noopener nofollow">демо-варіанті</a> та ще 2–3 прикладах з <a class="internal-link" data-href="practice.md" href="final/practice.html" target="_self" rel="noopener nofollow">practice.md</a>. VA-декомпозиція — це перекладання груп по 9 біт. На папері зручніше спочатку перевести VA у 48-бітне двійкове число, потім розділити на 9|9|9|9|12. Через зсуви та маски — швидше, але легше помилитися на контролі. Не забувайте прихований біт у IEEE 754. 23 біти мантиси в пам'яті = 24-бітне справжнє значення з прихованою 1. на початку. Теоретичні питання — коротко і по суті. Не потрібно писати есе — достатньо чіткої відповіді у 2–3 реченнях. Формулювання з лекційних нотаток зараховуються як правильні. Таблицю FIFO заповнюйте зліва направо, рядок за рядком. Не пропускайте «очевидні» влучання — помилка у тій частині, яку ви «пропустили», зазвичай коштує 1 бал. ]]></description><link>final/readme.html</link><guid isPermaLink="false">final/README.md</guid><pubDate>Fri, 17 Apr 2026 17:49:01 GMT</pubDate></item><item><title><![CDATA[practice]]></title><description><![CDATA[
Важливо. Задачі тут — для тренування. На самому контролі будуть інші числа та інші теоретичні питання з того самого списку тем (див. <a class="internal-link" data-href="README.md" href="readme.html" target="_self" rel="noopener nofollow">README.md</a> → «Що потрібно підготувати»). Формат і складність — такі самі.
Відповіді до демо-варіанта: <a class="internal-link" data-href="demo_key.md" href="final/demo_key.html" target="_self" rel="noopener nofollow">demo_key.md</a>. Для задач нижче відповіді не наводяться — звіряйтесь із лекційними нотатками, однокурсниками, або на консультації.
Формат: hex → S, E, M → e = E − 127 → значення у десятковій системі.Дано: 0x40C00000
Запишіть S (1 біт), E (8 біт, двійкова), M (23 біти, достатньо перших 3–4 значущих біт).
Обчисліть e = E − 127.
Запишіть мантису з прихованим бітом у формі 1.MMM₂ та у десятковій.
Обчисліть повне значення числа.
Дано: 0xC0300000
Розбийте на S / E / M.
Обчисліть справжній порядок та знак.
Нормалізована мантиса у двійковій та десятковій.
Повне значення.
Дано: 0x3F600000Те саме: декомпозиція → e → мантиса → значення.Дано: 0x40F00000Те саме.Чому декодування 0x3DCCCCCD (≈ 0.1) важче провести «руками», ніж декодування 0x40A00000 (= 5.0)? Поясніть у 1–2 реченнях (не розв'язуйте).Формат: заповнити таблицю трасування (крок, сторінка, стан 3 фреймів, результат), порахувати загальну кількість промахів, додати коротке спостереження.
Усі задачі: 3 фрейми, порожні на старті.
Рядок звернень: 5 2 1 5 3 2 4 2 1 3Заповніть FIFO-трасування на 10 кроків. Скільки всього промахів?Рядок звернень: 0 1 2 3 0 4 1 2 5 3Те саме. Скільки влучань?Рядок звернень: 7 3 1 7 4 3 5 7 1 2Те саме. На якому кроці трапилось перше ВЛУЧЕННЯ?Рядок звернень: 4 2 4 1 3 2 4 1 5 2Те саме. Яка найдовша послідовність ПРОМАХІВ підряд?Для рядка з 2.1, подумайте: чи дав би LRU менше чи більше промахів, ніж FIFO? Не моделюйте покроково — спробуйте міркувати якісно.Формат: 48-бітна VA → L4 / L3 / L2 / L1 / offset + додаткова підзадача.Схема декомпозиції (нагадування):VA (48 біт) = [L4: 9 біт] [L3: 9 біт] [L2: 9 біт] [L1: 9 біт] [offset: 12 біт] (біти 47..39)(38..30) (29..21) (20..12) (11..0)
Дано VA = 0x4A72_B384_1F5C. Розкладіть на L4, L3, L2, L1, offset (усі індекси — десяткові).Дано VA = 0x7FFE_A1B0_0C80 та VA₂ = VA + 0x300. Чи знаходяться VA та VA₂ на тій самій 4 KB сторінці? Які поля співпадають / змінилися?Зберіть VA з таких індексів:
L4 = 256, L3 = 100, L2 = 42, L1 = 7, Offset = 0x3C0.
Запишіть результат у шістнадцятковій системі.Дано VA = 0x2000_5678_9E90. Який мінімальний додатній Δ потрібно додати до VA, щоб результат опинився на наступній 4 KB сторінці?Для VA = 0x1234_5678_9ABC запишіть адресу початку наступної 4 KB сторінки. Наведіть L4, L3, L2, L1 нового значення.
Нагадування: на контролі буде 3 питань з переліку тем (1 з L08–L10, 2 з L11–L14). Нижче — ширший список тренувальних формулювань, щоб ви відчули стиль.
T.1. Чому L1 кеш будується як SRAM, а основна пам'ять — як DRAM? (На рівні комірки.)T.2. Поясніть, що відбувається, коли програма пише у порт 60h на PC. Це memory-mapped чи port-mapped I/O?T.3. У чому принципова різниця між лічильниковою машиною та РАМ-машиною? Який «маленький крок» дає величезний стрибок у виразності?T.4. Назвіть дві конкретні оптимізації, якими сучасний x86-процесор відрізняється від «підручникового» CPU з fetch-decode-execute.T.5. Чому графічні процесори (GPU) не підходять для виконання типового коду операційної системи? А для матричного множення у нейромережі — ідеально?T.6. Ви обходите int-масив з кроком 16 (a[0], a[16], a[32], ...). Розмір cache line = 64 Б, sizeof(int) = 4. Який приблизний miss rate ви очікуєте?T.7. Чому тайлінг (blocking) у матричному множенні пришвидшує код, але той самий трюк з блоками для простого sum += a[i] — ні?T.8. Наведіть один приклад часової локальності та один приклад просторової локальності з коду, який ви писали у Lab11.T.9. Програма виділила 1 ГБ через mmap та записала 1 байт у кожну 4 KB сторінку. Скільки minor faults ви очікуєте? А якщо торкнулися кожної 16-ї сторінки?T.10. Після fork() дитина записує у спільну з батьком область пам'яті. Що саме станеться на рівні PTE та фізичних фреймів?T.11. Поясніть, чому вимкнення ASLR (setarch -R) іноді використовують при відлагодженні програм.T.12. Що таке TLB? Чому його промах дорожчий за промах L1 даних?T.13. Для рядка звернень 1, 2, 3, 1, 2, 4, 1, 2, 5, 3 з 3 фреймами — чи даватиме Clock (second-chance) більше чи менше промахів, ніж FIFO? (Якісна відповідь, без покрокового трасування.)T.14. Наведіть два різні значення типу float (IEEE 754 single), які дають однаковий результат при додаванні до 1.0. Поясніть, чому.T.15. Чому вираз (a - b) / (a*a - b*b) може бути числово нестабільним, тоді як еквівалентний 1 / (a + b) — завжди точний?T.16. У Lab13 ви реалізували додавання IEEE 754 руками. Чому ваша реалізація для 0.1 + 0.2 дала результат, що відрізняється від апаратного на 1 ULP?T.17. На скільки теоретично може бути швидшим код з AVX2 FMA (2 порти × 8 float32 lanes × FMA = 16 FP ops/такт) порівняно зі скалярним кодом (1 FMA/такт)?T.18. Поясніть, чому vfmadd231ps ymm0, ymm1, ymm2 — одна інструкція — на сучасному Intel має пропускну здатність 2 за такт, а латентність 4 такти. Що це означає для розробника?T.19. Напишіть псевдокод, який демонструє ланцюг RAW-залежності, що обмежує ILP. Як ви б розірвали цей ланцюг?T.20. Після AVX2-циклу у YMM-регістрі лежать 8 часткових сум. Як отримати одне скалярне значення з цих восьми? Назвіть послідовність інструкцій (горизонтальна редукція).
✅ Ви вмієте декодувати IEEE 754 single precision на папері за ≤ 5 хв
✅ Ви розумієте механіку FIFO і заповнюєте 10-крокову таблицю без помилок
✅ Ви можете розкласти 48-бітну VA та зібрати її назад
<br>✅ Ви знаєте основні означення з переліку тем Завдання 4 (усе, що у списку в <a class="internal-link" data-href="README.md" href="readme.html" target="_self" rel="noopener nofollow">README.md</a>)
<br>✅ Ви прорішали <a class="internal-link" data-href="demo_variant.pdf" href="final/demo_variant.html" target="_self" rel="noopener nofollow">демо-варіант</a> і звірили з <a class="internal-link" data-href="demo_key.md" href="final/demo_key.html" target="_self" rel="noopener nofollow">demo_key.md</a>
Якщо всі 5 галочок зелені — ви готові до контролю.]]></description><link>final/practice.html</link><guid isPermaLink="false">final/practice.md</guid><pubDate>Fri, 17 Apr 2026 17:44:20 GMT</pubDate></item><item><title><![CDATA[L14-NOTES. Паралелізм на рівні інструкцій та потоків]]></title><description><![CDATA[Стислий довідник до Лекції 14. Повні матеріали — у файлі L14. Паралелізм на рівні інструкцій та потоків.md.LO5 (аналіз вузьких місць продуктивності) · LO6 (паралелізм, конвеєризація)
Однотактний тактовий період = сума затримок усіх стадій (IF+ID+EX+MEM+WB ≈ 5,0 нс → стеля 200 МГц).
У будь-який момент 4 з 5 стадій простоюють — 80 % кремнію марно крутить колесами.
Тактовий період обмежений критичним шляхом; просто «підкрутити тактову» не вийде.
Наскрізний приклад: цикл підсумовування масиву з 5 інструкцій (load, add, inc, cmp, jne) → 25 тактів послідовно → CPI = 5,0.
Напрямок лікування: тримати усі стадії зайнятими одночасно — 5-кратне прискорення, що лежить просто на поверхні.
Детальніше: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Instruction_pipelining" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Instruction_pipelining" target="_self">Instruction pipelining</a>
Аналогія з кав'ярнею / конвеєром: не прискорюйте окремий крок — просто не давайте станції простоювати.
Вставте регістри конвеєра між стадіями; кожна стадія працює над іншою інструкцією.
Канонічні 5 стадій: IF вибірка · ID декодування/читання регістрів · EX виконання · MEM пам'ять · WB зворотний запис.
Теорема про пропускну здатність / затримку: конвеєризація не пришвидшує жодної окремої інструкції; вона робить так, щоб інструкції завершувалися частіше. Затримка ≈ 5 тактів, пропускна здатність = 1 інстр./такт.
Тактовий період тепер покриває лише найповільнішу окрему стадію (≈1,5 нс → ~667 MIPS).
Нова метрика: CPI (cycles per instruction, цикли на інструкцію). Ідеальний конвеєризований CPI = 1; реальний CPI гірший через конфлікти.
Наскрізний приклад, Рівень 2 (конвеєризований, без випередження): бульбашка load-use + АЛП-RAW-бульбашка + бульбашка переходу → CPI ≈ 2,0.
<br>Детальніше: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Instruction_pipelining" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Instruction_pipelining" target="_self">Instruction pipelining</a>Конфлікт = причина, з якої наступна інструкція не може виконатися у наступному такті.
Структурні: дві інструкції одночасно хочуть ту саму апаратуру. Здебільшого розв'язуються дублюванням (розділений I/D-кеш, кілька АЛП/портів).
Конфлікти даних (за формою залежності на регістрі): RAW (Read After Write, читання після запису) — справжня залежність. Стерти не можна, можна лише мінімізувати ціну. Домінантний.
WAR (Write After Read, запис після читання) — антизалежність; лише конфлікт імен.
WAW (Write After Write, запис після запису) — вихідна залежність; лише конфлікт імен. Ключова ідея: WAR/WAW — це конфлікти імен, а не справжні залежності; перейменування регістрів (§6) прибирає їх узагалі.
Конфлікти керування: переходи — вибірка не знає цілі, поки EX не резолвить перехід. Штраф зростає з глибиною конвеєра.
Математика CPI (20 % завантажень з 1-тактовою затримкою, 20 % переходів зі штрафом 2 такти, 10 % хибних передбачень):
CPI ≈ 1 + 0,20·1 + 0,20·2·0,10 = 1,24
<br>Детальніше: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Hazard_(computer_architecture)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Hazard_(computer_architecture)" target="_self">Hazard (computer architecture)</a>Випередження (обхід): прямі дроти з виходів пізніших стадій назад на входи EX, повз регістровий файл.
EX→EX та EX→MEM RAW-конфлікти: знищуються повністю (нуль бульбашок).
Використання після завантаження (MEM→EX): виживає рівно як 1 бульбашка. Завантажене значення готове лише наприкінці MEM, а наступній інструкції воно потрібне на початку EX. Дроту, яким би сигнал пішов назад у часі, не існує.
Блок виявлення конфліктів заморожує PC і вставляє NOP, коли випередження вже не рятує.
Заповнення слоту компілятором: переставити у слот load-use незалежну інструкцію.; До: бульбашка після завантаження
mov eax, [rsi+rcx*4]
add ebx, eax ; &lt;-- затримка
inc rcx
; Після: inc заповнює слот
mov eax, [rsi+rcx*4]
inc rcx ; &lt;-- заповнює слот
add ebx, eax ; тепер готове
Передбачення переходів: угадати напрямок, спекулювати, скинути конвеєр, якщо не вгадали.
Статичне: «зворотні переходи — взяті, прямі — ні» — працює для циклів.
Однобітне: запам'ятовує останній результат; двічі хибно передбачає на кожному переверненні патерну.
2-бітний насичувальний лічильник: скінченний автомат на чотири стани (Сильно невзятий / Слабо невзятий / Слабо взятий / Сильно взятий). Треба вміти намалювати на іспиті. Поглинає короткочасні збої; на виході з циклу — лише одне хибне передбачення.
BTB (Branch Target Buffer, буфер цілей переходів): кеш «останньої цілі для цього PC»; дозволяє вибірці перенаправитися у тому самому такті, критично важливий для непрямих переходів.
Дворівневий адаптивний / TAGE / перцептронний: використовують історію переходів, точність 95–99 %.
Турнірний: мета-передбачувач обирає між локальною та глобальною історією.
Штраф за хибне передбачення зростає з глибиною. Pentium 4 Prescott (31 стадія) втрачав понад 20 тактів на кожне хибне передбачення → Prescott часто був повільнішим за коротший Pentium III на тій самій частоті. Сучасна «солодка точка»: 14–20 стадій, точність понад 97 %.Сума ≈ 18 стадій. Кожен рядок — це реальне звернення до SRAM / арифметика / перевірка залежностей, яка фізично не вміщається в коротший такт.Наскрізний приклад, Рівень 3 (випередження + 2-бітний передбачувач): виживає лише бульбашка load-use → CPI ≈ 1,2.<br>Детальніше: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Branch_predictor" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Branch_predictor" target="_self">Branch predictor</a>
Дублюємо фронтенд і бекенд: 2-широкий, 4-широкий, 8-широкий. IPC &gt; 1, тобто CPI &lt; 1.
Перший масовий суперскаляр x86: Pentium 1993 (U-конвеєр / V-конвеєр). Сучасний x86: 4–6 широкий. Apple M1: 8 широкий.
Межа суперскалярного виконання за порядком: ланцюги залежностей серіалізують половину ширини видачі. Наскрізний приклад на 2-широкій машині з виконанням за порядком → CPI ≈ 0,9 (а не 0,5), бо I2 все одно чекає на I1, а міжітераційний ланцюг — на rcx.
Ідея, що відмикає OoO: завантаження ітерації k+1 не залежить від додавання ітерації k — але машина з виконанням за порядком цього не бачить.
Повчальна історія Itanium (2001, EPIC/VLIW): Intel і HP зробили ставку на те, що компілятор статично витягуватиме ILP через 3-операційні пакети. Провал, бо затримки завантажень і результати переходів — явища часу виконання: статичний розклад змушений скрізь закладати найгірший випадок. Програвав x86 OoO у співвідношенні ~100:1; знятий з виробництва 2021 року. Мораль: ILP найкраще витягувати динамічно, на швидкості апаратури, за реальною інформацією часу виконання. Статичний ILP крихкий.
<br>Детальніше: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Superscalar_processor" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Superscalar_processor" target="_self">Superscalar processor</a> · <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Itanium" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Itanium" target="_self">Itanium</a>Дві цілі: (1) виконувати, щойно операнди готові; (2) завершувати у програмному порядку заради точних переривань.
Перейменування регістрів: архітектурний eax → пул із 150–400 фізичних регістрів. WAR і WAW зникають за самою конструкцією: кожен запис отримує свіжий фізичний регістр. Лишається тільки RAW.
Станції резервування: зони очікування при кожному функціональному блоці; кожен запис тримає вже відомі значення операндів і теги для невідомих.
Спільна шина даних (Common Data Bus, CDB): функціональні блоки транслюють (тег, значення); кожна станція асоціативно зіставляє тег і защіпає. Це виконання за потоком даних прямо в апаратурі.
Три фази (запам'ятати): Видача — за порядком: декодування, перейменування, виділення станції резервування та слоту в хвості ROB.
Виконання — позачергово: спрацьовує, щойно операнди надходять по CDB.
Завершення / ретайр — за порядком, на голові ROB: запис у архітектурний регістровий файл і звільнення слоту. Буфер переупорядкування (ROB): кільцеве FIFO, хвіст = видача, голова = завершення. Уможливлює точні переривання: на помилці скидаємо ROB, починаючи від запису-винуватця, а архітектурного регістрового файлу не чіпаємо.
Стіна, про яку насправді вся ця лекція: архітектурний стан (те, що бачать ОС і відлагоджувач) проти мікроархітектурного стану (кеші, передбачувачі, RS, ROB). ROB — це і є стіна. §7 — про те, що буває, коли вона починає протікати.
Наскрізний приклад, Рівень 5 (OoO + ROB, 2 ітерації у польоті): завантаження з ітерації k+1 виконуються паралельно з додаваннями з k → CPI ≈ 0,4. Вузьке місце тепер — затримка завантаження і розмір ROB, а не ланцюги залежностей.Ціна: станціям резервування потрібна асоціативна CAM-пам'ять, а складність масштабується надлінійно з шириною видачі. Саме через це x86 упирається в 4–6 широкий.<br>Детальніше: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Tomasulo%27s_algorithm" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Tomasulo%27s_algorithm" target="_self">Tomasulo's algorithm</a> · <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Reorder_buffer" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Reorder_buffer" target="_self">Reorder buffer</a>
ROB приблизно вдвічі більший за будь-яке сучасне x86-ядро → значно більший радіус пошуку незалежної роботи.
Чому 8 широко? Зворотне посилання на L09: ARM64 — фіксована ширина 4 байти на інструкцію → 32-байтовий рядок вибірки нарізається на 8 однакових шматочків, декодування тривіально паралельне. x86 — змінна довжина 1–15 байтів → межі декодування лежать на критичному шляху. x86 компенсує це кешами мікрооперацій та маркерами довжини, але до 8-широкого з холодного кешу все одно не дотягує.
Розподіл на P-ядра / E-ядра (4 Firestorm + 4 Icestorm, спільна ISA ARM64): ОС динамічно мігрує потоки. Родовід — ARM big.LITTLE 2011 року. Тизер до L15: гетерогенність перемагає монолітне масштабування.
Урок: Apple не винайшла нічого нового — вона просто збудувала значно більшу версію OoO-ядра, яке вже було в усіх, а ISA зробила це доступним.
<br>Детальніше: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Apple_M1" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Apple_M1" target="_self">Apple M1</a>Найважливіша ідея цієї лекції:
Архітектурний стан — це те, чим, за версією ISA, є машина. Мікроархітектурний стан (кеші, передбачувачі, TLB, ROB, RS) — це те, чим машина є насправді. Spectre і Meltdown довели, що ця різниця важлива: мікроархітектурний стан спостерігається через таймінги.
Спекуляція: щоб годувати 8-широкий двигун поза переходами, процесор мусить вгадувати. Якщо неправильно — знищити спекулятивні мікрооперації. Тридцять років припускали, що знищення повне. Насправді ні: архітектурний стан відкочується, але кеш усе ще теплий, передбачувач усе ще пам'ятає, а TLB усе ще тримає прохід.Влучання в кеш ≈ 3–4 такти; промах аж до DRAM ≈ 200–300 тактів. Цей розрив — вимірюваний однобітний канал; при 256 різних рядках це вже один байт за одне вимірювання.flush probe[0..255] ; усі холодні
&lt;спекулятивно виконати&gt;: secret = forbidden_load() touch probe[secret * 64] ; нагріває ОДИН рядок
&lt;спекуляцію знищено&gt; ; архітектурний стан чистий
time probe[0..255] ; знайти швидкий → секрет
Те, що ми звеличували в L11 — швидкі кеші — і є витоком.Intel (та деякі ARM) виконували завантаження і перевірку дозволу користувач/супервізор паралельно, причому перевірка дозволу завершувалася пізніше. Спекулятивне завантаження з ядра клало своє значення у перейменований фізичний регістр до того, як помилка виходила на ретайр. Три спекулятивні інструкції (завантаження з ядра → зсув → завантаження в зонд) залишають слід у кеші; помилку спіймано, стан відкочено, зловмисник зондує масив → користувацький режим читає довільну пам'ять ядра (у Linux це = вся оперативна пам'ять). Уражені майже всі OoO-ядра Intel від 1995 року; AMD здебільшого імунний — перевірка дозволу відбувалася ще до того, як значення встигало розійтися.Виправлення: KAISER / KPTI (Kernel Page Table Isolation — ізоляція таблиць сторінок ядра). Таблиці сторінок ядра прибирають із відображення в користувацькому режимі. Коштує 5–30 % на навантаженнях, інтенсивних за системними викликами (PCID полегшує ціну). Сплачується назавжди, вже в програмному забезпеченні.Зловживає передбачувачем напрямку. Програміст написав перевірку меж коректно:if (x &lt; array1_size) { y = array2[array1[x] * 4096];
}
Зловмисник навчає передбачувач казати «узятий» на багатьох валідних викликах, а потім передає шкідливий x поза діапазоном. Процесор спекулятивно проходить повз перевірку, завантажує секрет за array1[x], використовує його як індекс у array2 і залишає слід у кеші. Перевірка меж резолвиться, знищуємо — запізно. Код коректний. Апаратура його все одно порушила. Немає одного рядка, який можна «залатати». Пом'якшення (lfence, array_index_nospec(), компіляторне укріплення спекуляції) дорогі, і станом на 2026 рік усе ще неповні.Зловживає BTB (передбачувачем цілі) для непрямих переходів. Зловмисник засмічує записи BTB, що колідують з непрямими переходами жертви; непрямий перехід жертви спекулятивно перенаправляється на обраний зловмисником гаджет усередині адресного простору жертви. Виправлення: retpoline (Google) — замінити кожен непрямий перехід послідовністю call/ret, яку BTB вже не може осмислено отруїти.Spectre і Meltdown — це не баги: процесори виконали контракт ISA. Неповним був сам контракт. П'ятдесят років ми описували процесори виключно через архітектурний стан. Від 2018 року мікроархітектурний стан — теж частина моделі безпеки машини. Податок на спекуляцію постійний: накладні витрати KPTI, накладні витрати retpoline, бар'єри в гарячих шляхах, знижена роздільність JS-таймерів, хмарні планувальники, які уникають спільного розміщення орендарів. Сукупна ціна: 2–30 % залежно від навантаження; частина з цього вже ніколи не повернеться.Якщо запам'ятати лише одну річ із цієї лекції: архітектурний стан ≠ мікроархітектурний стан, і ця різниця — не примітка.<br>Детальніше: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Speculative_execution" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Speculative_execution" target="_self">Speculative execution</a> · <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Spectre_(security_vulnerability)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Spectre_(security_vulnerability)" target="_self">Spectre</a> · <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Meltdown_(security_vulnerability)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Meltdown_(security_vulnerability)" target="_self">Meltdown</a> · <a data-tooltip-position="top" aria-label="https://meltdownattack.com/" rel="noopener nofollow" class="external-link is-unresolved" href="https://meltdownattack.com/" target="_self">meltdownattack.com</a>
Навіть 8-широкий M1 на однопотоковому коді рідко виходить за IPC ≈ 2 — бекенд для одного потоку надлишковий. Промахи L3, хибні передбачення, ланцюги залежностей — усе це залишає виконавчі порти в простої.
SMT: дамо двом потокам видавати інструкції в той самий такт бекенду, заповнюючи прогалини.
Ціна: ~5 % площі, вигода: ~15–30 % пропускної здатності на типових серверних навантаженнях. Для порівняння: додати друге ядро — це ~100 % площі за ~80–90 % приросту пропускної здатності. SMT купує пропускну здатність значно дешевше — валютою тут виступає передбачуваність на окремий потік.
Дубльоване (на один логічний потік): архітектурний регістровий файл, PC, CR3/сегменти/MSR, APIC, стан переривань (~5 % площі ядра).
Спільне: ROB, станції резервування, фізичний регістровий файл, функціональні блоки, кеші L1/L2, передбачувач переходів, TLB, смуга вибірки/декодування.
Intel HTT вперше з'явилася на Xeon (лютий 2002), далі на Pentium 4. AMD додала 2-way SMT у Zen (2017). IBM POWER8+ підтримує до 8-way. x86 зупинилася на 2 — далі йде спадна віддача.
Коли SMT шкодить: HPC (і так насичений), бази даних (хвостова затримка), системи реального часу, навантаження, чутливі до затримок. Часто вимикають.
Ціна за безпекою: SMT-сусіди ділять усе в бекенді. Витік RSA-ключа в Персіваля 2005 року; Foreshadow/L1TF 2018; MDS/RIDL/Fallout 2019. 2018 року OpenBSD вимкнула HT за замовчуванням. Хмарні провайдери відмовляються спільно планувати SMT між орендарями.
ILP знаходить паралелізм усередині одного потоку. TLP через SMT імпортує паралелізм з іншого потоку, коли одного замало. І те, й інше годує той самий голодний бекенд. (Запам'ятати.)
<br>Детальніше: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Simultaneous_multithreading" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Simultaneous_multithreading" target="_self">Simultaneous multithreading</a> · <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Hyper-threading" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Hyper-threading" target="_self">Hyper-threading</a>Той самий цикл підсумовування масиву з 5 інструкцій, п'ять мікроархітектур:Наскрізно: приблизно у 12 разів швидше на тій самій частоті, на тих самих 5 інструкціях, — виключно за рахунок мікроархітектурних прийомів.
Конвеєризація обмінює затримку на пропускну здатність. Кожна інструкція все ще займає N тактів; нова інструкція завершується щотакту. Конфлікти — це розрив між ідеальним і реальним CPI.
Кожен прийом ILP розв'язує проблему, створену попереднім. Конвеєризація → конфлікти → випередження/передбачення → суперскаляр → ланцюги залежностей → OoO → зламані точні переривання → ROB → потреба перетинати переходи → спекуляція → Spectre/Meltdown.
Для ILP динамічне перемагає статичне. Ставка Itanium на EPIC провалилася, бо промахи кешу й переходи — явища часу виконання. OoO планує під реально спостережувані затримки.
Apple M1 — це підручниковий OoO у промисловому масштабі. Ті самі станції резервування Томасуло 1967 року плюс CDB, тільки значно більші (8-широкі, ~630 ROB). ISA з фіксованою шириною в ARM64 зробила це масштабування доступним.
Архітектурний стан ≠ мікроархітектурний стан. Найважливіша ідея. Кеші, передбачувачі й TLB — теж частина машини, спостережувана через таймінги, і тепер вона — частина моделі загроз кожної захищеної системи.
SMT купує дешеву пропускну здатність (~5 % площі за 15–30 % додаткової роботи) ціною передбачуваності на один потік і нового класу побічних каналів усередині одного ядра.
Обов'язкове читання:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Instruction_pipelining" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Instruction_pipelining" target="_self">Instruction pipelining</a> — Overview, Hazards
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Tomasulo%27s_algorithm" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Tomasulo%27s_algorithm" target="_self">Tomasulo's algorithm</a> — Algorithm, Implementation
<br>Стартові сторінки <a data-tooltip-position="top" aria-label="https://spectreattack.com/" rel="noopener nofollow" class="external-link is-unresolved" href="https://spectreattack.com/" target="_self">Spectre</a> і <a data-tooltip-position="top" aria-label="https://meltdownattack.com/" rel="noopener nofollow" class="external-link is-unresolved" href="https://meltdownattack.com/" target="_self">Meltdown</a>
Паттерсон і Геннессі, Computer Organization and Design, розділ 4
Що ви робитимете:
Намалюйте наскрізний приклад уручну на кожному рівні (послідовний → конвеєризований → з випередженням → суперскалярний → OoO). Просторово-часова сітка — найкорисніша навчальна вправа.
Візьміть коротку функцію на x86-64; визначте, де вона застрягає на 5-стадійному конвеєрі з виконанням за порядком. Які RAW випереджаються начисто? Які виживають як 1-тактові бульбашки? Де сидить перший перехід?
Бонус: скомпілюйте C-функцію з -O0 і -O3. Подивіться, як компілятор переставляє інструкції, щоб приховати затримку load-use.
]]></description><link>lectures/l14-notes.-паралелізм-на-рівні-інструкцій-та-потоків.html</link><guid isPermaLink="false">Lectures/L14-NOTES. Паралелізм на рівні інструкцій та потоків.md</guid><pubDate>Tue, 14 Apr 2026 18:19:19 GMT</pubDate></item><item><title><![CDATA[L14. Паралелізм на рівні інструкцій та потоків]]></title><description><![CDATA[Курс: Архітектура комп'ютерних систем
Тема: Конвеєризація, конфлікти, суперскалярність, позачергове виконання, спекуляція, SMT
Результати навчання:
LO5: Аналізувати продуктивність та вузькі місця комп'ютерних систем
LO6: Пояснювати паралелізм, конвеєризацію та гетерогенні обчислення Гачок. Як процесор на 3 ГГц виконує 8+ інструкцій за цикл? І чому той самий трюк подарував нам Spectre?
Ця лекція — історія про те, як архітектори вичавлювали кожну краплю паралелізму з одного потоку: конвеєризація, випередження, передбачення переходів, суперскалярність, позачергове виконання, спекуляція та одночасна багатопотоковість. І про те, як одна з цих оптимізацій, яку колись вважали нешкідливою, виявилася здатною зливати ваші секрети через кеш. Головна думка, яку варто винести: архітектурний стан — це ще не вся машина. Мікроархітектурний стан — кеші, передбачувачі, TLB — цілком реальний, спостережуваний через вимірювання часу, і з 2018 року входить до моделі загроз будь-якої серйозної системи.
Ще в L02 і L03 ви побудували собі ментальну модель CPU, яка виглядає приблизно так: процесор вибирає інструкцію з пам'яті, декодує її в керувальні сигнали, виконує операцію в АЛП, за потреби звертається до пам'яті для завантаження чи збереження і, нарешті, записує результат назад у регістр. П'ять концептуальних стадій. Кожна інструкція по черзі проходить усі п'ять, одна за одною. Модель коректна — і водночас з погляду продуктивності це катастрофа.Ось чому. У суто однотактному дизайні тактовий період має бути достатньо довгим, щоб увесь шлях даних — від першого фронту тригера, який защіпає лічильник команд, і до зворотного запису в регістр — устиг усталитися до наступного переднього фронту. Новий такт не настане, поки найповільніша інструкція не завершить свою найповільнішу стадію. Припустімо, кожна з наших п'яти стадій має таку затримку поширення сигналу:Тактовий період 5,0 нс — це 200 МГц. Одна інструкція кожні 5 нс, і так назавжди. Ось наша стеля.Тут студенти зазвичай ставлять очевидне запитання: «Гаразд, то просто зробімо такт швидшим». Не вийде, і причина цілком фізична. Кожен вентиль у цьому шляху даних уже перемикається настільки швидко, наскільки здатні його транзистори; сигнали поширюються дротами зі швидкістю, що становить лише частку від швидкості світла, а комбінаційна логіка має мінімальний час установлення, заданий ємністю та опором. Не можна просто крутнути ручку з написом «тактова частота», не зменшивши обсягу роботи за цикл. Тактовий період — це сума затримок вентилів уздовж критичного шляху (найдовшого ланцюга комбінаційної логіки між двома тригерами), і тактувати швидше, ніж цей шлях здатен поширити сигнал і надійно його защіпити, неможливо. Зменшення транзисторів допомагає, і на цій хвилі ми їхали десятиліттями, але в межах заданого технологічного процесу критичний шлях є таким, яким є.Отже, якщо не можна скоротити цикл, єдиний спосіб виконувати більше роботи — робити її більше за цикл. І саме тут однотактний дизайн починає видаватися марнотратним так, що аж руки сверблять це виправити. Уявіть шлях даних у той момент, коли інструкція K перебуває у стадії зворотного запису. У регістровий файл іде запис. А що в цей час робить блок вибірки? Нічого. Сидить під живленням, тактується і простоює, чекаючи наступного переднього фронту, щоб піти по інструкцію K+1. Що робить декодер? Нічого. АЛП? Нічого. Блок пам'яті? Нічого. У кожен момент часу чотири з п'яти стадій простоюють. Уся CPU — це конвеєр із п'яти робітників, де четверо стоять і дивляться, поки п'ятий робить свою роботу, а потім усі переходять на одну станцію далі й повторюють цикл.З погляду використання апаратури це 80 % марнотратства. Транзистори є. Дроти є. Енергія горить. А більшість кремнію тим часом крутить колесами вхолосту, чекаючи своєї черги. Якби якимось чином вдалося тримати всі п'ять стадій зайнятими одночасно — вибирати інструкцію K+4, поки декодується K+3, поки виконується K+2, поки K+1 звертається до пам'яті, поки K записується, — ми б робили вп'ятеро більше роботи за цикл, не змінюючи тактового періоду взагалі.Ось вам п'ятикратне прискорення, що лежить просто на поверхні. Не від швидшого такту, не від розумнішого АЛП, не від нового ISA — а лише від усвідомлення того, що стадії — це незалежні шматки схеми, серіалізовані політикою, а не фізикою.Щоб це марнотратство стало наочним, ось фрагмент коду, який ми простежимо через кожну модель виконання в цій лекції, — крихітний цикл x86-64, що підсумовує масив цілих чисел. Він навмисне дібраний так, щоб у ньому змішувалися залежності й незалежна робота, — щоб кожен прийом паралелізму рівня інструкцій (ILP — далі ми називатимемо так загальну ідею одночасного виконання кількох інструкцій з одного потоку), який ми введемо, мав, за що зачепитися:; Підсумовування масиву int: sum = array[0] + array[1] + ... + array[n-1]
; rsi = базовий вказівник, rcx = індекс (починається з 0), rdx = n, ebx = поточна сума loop: mov eax, [rsi + rcx*4] ; I1: завантажити array[i] у eax add ebx, eax ; I2: sum += eax inc rcx ; I3: i++ cmp rcx, rdx ; I4: порівняти i з n jne loop ; I5: перейти назад, якщо не рівні
П'ять інструкцій на ітерацію. Одне завантаження, дві операції АЛП, одне порівняння, один перехід. А тепер уявіть, як ці п'ять інструкцій проходять через щойно описаний однотактний шлях даних, де кожна з них мусить пройти всі п'ять стадій, перш ніж почнеться наступна:<img alt="Просторово-часова діаграма робочого прикладу, що виконується на строго послідовному однотактному шляху даних. Кожна з п'яти інструкцій по черзі проходить усі п'ять стадій конвеєра, одна за раз, загалом за 25 циклів — у кожний момент чотири з п'яти стадій простоюють." src="assets/l14-running-example-sequential.svg" target="_self" style="width: 700px; max-width: 100%;">Двадцять п'ять циклів — щоб завершити п'ять інструкцій. CPI = 5,0. На кожному з цих двадцяти п'яти циклів корисну роботу виконує рівно одна стадія, а чотири інші чекають своєї черги. Ось воно — ті самі 80 % марнотратства, написані великими літерами. Це і є проблема. Усе, що буде далі, — це нарощене обладнання, яке намагається відвоювати ті чотири п'ятих кремнію, які діаграма марно засвічує.Тож питання, з якого починається ця лекція, просте, майже дурне:
А що, якби вдалося тримати всі п'ять стадій зайнятими весь час?
Запам'ятайте це питання. Уся подальша історія — кожен прийом у цій лекції, кожен конфлікт, кожен передбачувач, кожен буфер переупорядкування, кожна безпекова діра, яку ми розглянемо, — випливає з наслідків чесної відповіді на нього.Уявіть собі жваву кав'ярню в ранковий час пік. Бар має чотири окремих станції: прийняти замовлення та підписати стаканчик, витягти порцію еспресо, зварити та налити молоко, видати клієнту. Припустімо, кожна станція забирає близько 30 секунд. Якщо один баріста вперто проводить кожен напій через усі чотири станції, перш ніж братися за наступне замовлення, то кожен клієнт чекає дві хвилини, і готовий напій з'являється на стійці теж раз на дві хвилини. Жодна справжня кав'ярня в години пік так не працює. Щойно перше замовлення підписано, як наступний клієнт уже підходить до каси, а кавомашина починає готувати першу порцію еспресо. Поки видають перший напій, другому вже збивають молоко, для третього тягнуть еспресо, а четвертий тільки-но робить замовлення. Після 90-секундного розгону готовий напій з'являється на стійці кожні 30 секунд — зі швидкістю найповільнішої окремої станції, а не раз на дві хвилини. Будь-який окремий клієнт усе одно чекає на свій напій ті самі дві хвилини; ця затримка не змінилася. Змінилася пропускна здатність. Кав'ярня тепер видає вчетверо більше напоїв на годину, не прискоривши жодного окремого кроку. Рухомий конвеєр Генрі Форда 1913 року — це той самий прийом, тільки з автомобілями: шасі проїжджають повз нерухомих робітників, кожен виконує невелике завдання, і готова Model T сходить з лінії кожні 93 хвилини замість дванадцяти годин. Висновок однаковий в обох випадках. Хитрість не в тому, щоб прискорити якийсь окремий крок. Хитрість у тому, щоб ніколи не давати станції простоювати.<br>Це і є <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Instruction_pipelining" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Instruction_pipelining" target="_self">конвеєризація інструкцій</a>. Ми беремо наш п'ятистадійний шлях даних з §1 і між кожною парою стадій вставляємо регістр конвеєра — набір тригерів, що защіпає проміжний стан наприкінці кожного тактового циклу. Коли ці межі на місці, стадія 2 може взятися за інструкцію K+1 у той самий момент, коли стадія 1 передає їй інструкцію K. Кожна стадія стає незалежним робітником зі своїм власним «ящиком деталей» (вхідним регістром конвеєра) та своєю власною «вихідною стрічкою» (вихідним регістром конвеєра).Канонічний RISC-конвеєр має п'ять стадій, ті самі п'ять, що ми використовували в §1:
IF — вибірка інструкції. Зчитати байти інструкції з I-кеша за адресою з лічильника команд і оновити PC так, щоб він указував на наступну інструкцію.
ID — декодування інструкції та читання регістрів. Розкласти біти інструкції на керувальні сигнали: код операції, регістри-джерела, регістр-приймач, безпосереднє значення. Прочитати операнди-джерела з регістрового файлу.
EX — виконання. Подати операнди в АЛП. Для додавання, зсуву чи порівняння тут відбувається обчислення. Для завантаження чи збереження тут обчислюється ефективна адреса.
MEM — доступ до пам'яті. Якщо інструкція — це завантаження або збереження, тут іде звернення до D-кеша. Для суто АЛП-інструкцій ця стадія працює транзитом без дії (значення просто їде далі в регістрі конвеєра).
WB — зворотний запис. Записати результат АЛП або завантажене значення назад у регістр-приймач у регістровому файлі.
IF, ID, EX, MEM, WB. Запам'ятайте ці п'ять літер. Кожна діаграма в цій лекції використовує їх.Ось найважливіша ідея в конвеєризації, і та, на якій спотикається більшість студентів з першого заходу:
Конвеєризація не робить жодну окрему інструкцію швидшою. Вона робить так, щоб інструкції завершувалися частіше.
Затримка однієї інструкції — час від її IF до WB — усе ще приблизно 5 циклів, як і було. Одна add, що йде через порожній конвеєр, дістається до зворотного запису не швидше, ніж на неконвеєризованій машині. А змінюється пропускна здатність. Коли конвеєр заповнений, щотакту одна інструкція завершується, а інша заходить. Усталений темп — одна інструкція за такт, хоча кожна окрема інструкція все одно потребує п'яти циклів від початку до кінця.Саме на цьому розриві між затримкою та пропускною здатністю конвеєризація й тримається. Ми готові почекати кілька циклів на будь-який конкретний результат, доки результати продовжують сходити з кінця лінії щотакту.А тепер поглянемо на тактовий період. В однотактному дизайні з §1 тактовий період був сумою всіх затримок стадій, адже один цикл мав виконати цілу інструкцію. У конвеєризованому дизайні кожна стадія отримує власний цикл, тож тактовий період має покривати лише найповільнішу окрему стадію (плюс невеликі накладні витрати регістрів конвеєра, якими ми поки знехтуємо):Це приблизно 3,3-кратне прискорення лише від скорочення тактового періоду, ще до того, як ми врахуємо виграш у пропускній здатності за цикл. У підручникових прикладах зі збалансованими стадіями конвеєризацію подають як чистий 5-кратний виграш для 5-стадійного конвеєра. Реальні конвеєри не бувають ідеально збалансованими, але порядок величини правильний: якщо найповільніша стадія становить приблизно 1/N від суми, N-стадійний конвеєр дає десь близько N-кратного прискорення.Метрика продуктивності, якою ми послуговуватимемося до кінця лекції, — CPI, цикли на інструкцію. Ідеальний конвеєр досягає CPI = 1: щотакту завершується одна інструкція. Це наша нова стеля, що замінює стару однотактну стелю CPI = 5. Усе, що йде з §3 і далі, присвячено розриву між ідеальним CPI = 1 і тим неохайним CPI, який ви реально отримуєте, коли справжній код тече крізь справжній кремній, — бо, як ви зараз побачите, конвеєризація породжує нові проблеми тим самим актом, яким розв'язує стару.Повернімося до робочого прикладу з §1 — п'ятиінструкційного циклу підсумовування масиву. У §1 ми дивилися, як він шкутильгав через послідовний шлях даних при CPI = 5. Тепер пропустімо ті самі п'ять інструкцій через наш щойно сконвеєризований CPU. Наївно: кожна інструкція входить у IF на такт пізніше за попередницю, і після 4-тактового розгону конвеєр заповнюється, а далі щотакту завершується одна інструкція. За такою логікою на цю ітерацію пішло б близько 9 тактів, а CPI ≈ 1.От тільки так не вийде. Поки що ні. У цього конвеєра немає жодних хитрощів — ні випередження, ні передбачення переходів, ні чогось вигадливого. Це простий п'ятистанційний конвеєр, що чесно тримається правила: «залежна інструкція не може читати регістр, поки виробник реально не записав його в WB». Подивімось, як виглядатимуть ті самі п'ять інструкцій, якщо сумлінно дотримуватися цього правила:<br><img alt="Той самий робочий приклад на п'ятистадійному конвеєрі без випередження. Інструкції тепер перекриваються, але комірки-бульбашки з'являються там, де I2 чекає результату завантаження I1, і де перехід I5 скидає помилково передбачені вибірки — CPI падає приблизно до 2." src="assets/l14-running-example-pipelined.svg" target="_self" style="width: 700px; max-width: 100%;">Щойно вискочили три окремі проблеми. Назвімо їх.Затримка «завантаження–використання» (I1 → I2). I1 — це завантаження. Значення eax насправді готове лише наприкінці стадії MEM інструкції I1, а в цій наївній моделі воно не покидає конвеєра до стадії WB. А I2 потребує eax уже на початку своєї стадії EX. I2 увійшла в IF на такт пізніше за I1, тож на той момент, коли I2 готова до виконання, значення I1 ще сидить у регістрі конвеєра MEM/WB і в регістровому файлі його ще не видно. I2 не має вибору, крім як стояти два такти, поки не завершиться зворотний запис I1. Ці два порожні слоти в сітці — бульбашки конвеєра: стадія не робить нічого корисного, але такти все одно цокають.Затримка «АЛП–використання» (I3 → I4). I3 обчислює новий rcx. I4 читає rcx. Історія та сама, лише трохи менш болюча, бо результат I3 готовий у кінці її стадії EX, а не MEM, — але йому все одно треба пройти MEM і WB, перш ніж I4 зможе прочитати регістровий файл. Ще одна двотактова бульбашка.Бульбашка переходу (I5). I5 — це умовний перехід. Його результат залежить від прапорців, встановлених I4, а цільова адреса переходу (або PC наступної інструкції) стає відомою лише десь у стадії EX інструкції I5. Але блок вибірки про це не знає: він хоче щотакту вибирати нову інструкцію, а протягом такту-двох після того, як I5 зайшла в конвеєр, він гадки не має, з якого PC вибирати. Або він стоїть (гарантована бульбашка), або вгадує — а потім викидає виконану роботу, якщо здогад виявився хибним. У будь-якому разі ви втрачаєте кілька тактів.Тепер полічімо збитки. П'ять інструкцій, але з усіма затримками ми витратили на одну ітерацію близько 10 тактів. CPI ≈ 2,0. Не підручникове 1,0.Ось і мораль цього розділу. Конвеєризація — чудова ідея: вона бере однотактну фабрику і влаштовує все так, щоб усі робітники були зайняті одночасно. Але щойно всередині машини водночас опиняється кілька інструкцій, вони починають наступати одна одній на п'яти. Інструкції, які в старій моделі були безпечно послідовними, раптом кидаються читати значення, яких іще не записано. Конвеєр не забезпечує дотримання залежностей — так само як конвеєрна лінія сама собою не стежить за правилом «не встановлюй колеса, поки немає осі». Це питання планування, а конвеєризація лише перетворила планування на найскладнішу частину.Ми перейшли від CPI ≈ 5 до CPI ≈ 2. Це реальний виграш — більш ніж удвічі швидше, — але ми все ще далеко від CPI = 1, обіцяного теоремою про пропускну здатність. Ця стеля існує, ми просто ще її не заслужили. Причина в тому, що ми створили цілу нову категорію проблем, якої в §1 не було: конфлікти. Інструкції перекриваються, але деякі з цих перекриттів нелегальні, і кожне нелегальне перекриття обертається затримкою. §3 дасть цим конфліктам імена й навчить вас розпізнавати їх з першого погляду, — бо щойно ви їх бачите, ви вже можете проектувати обладнання, яке робить їх дешевшими.Розділ 2 продав вам мрію: п'ять стадій, п'ять інструкцій у польоті, щотакту завершується одна інструкція. CPI = 1. Фабрична пропускна здатність. Готово.А потім сітка перестала бути чистою. З'явилися бульбашки. add мусив чекати на mov. cmp мусив чекати на inc. А внизу циклу jne заморозив стадію вибірки, бо ніхто не знав, звідки вибирати далі.<br>Ці бульбашки мають назву. Це конфлікти (hazards) — ситуації, коли «наступна інструкція не може виконатися в наступному такті», якщо скористатися <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Hazard_(computer_architecture)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Hazard_(computer_architecture)" target="_self">означенням з Вікіпедії</a>. Конфлікт — це будь-яка структурна, логічна чи керувальна причина, через яку конвеєр не здатен дотриматися своєї обіцянки «одна інструкція за такт». Кожен реальний процесор витрачає колосальні зусилля, щоб конфліктів уникати, ховати їх або ж відновлюватися після них, коли передбачення виявляється хибним.Цей розділ дає конфліктам імена й таксономію. Не тому, що таксономія цікава сама собою, а тому, що §4, §5 і §6 кожен б'є по конкретній категорії, і корисно знати, по якій саме. Уявіть решту лекції як серію відповідей. Цей розділ — це питання.Структурний конфлікт — це конфлікт ресурсів. Дві інструкції в різних стадіях конвеєра одночасно хочуть той самий апаратний блок, а отримати його може лише одна.Класичний приклад: на ранньому конвеєризованому проєкті з єдиним портом пам'яті в такті N одна інструкція хоче вибрати з пам'яті I5 (стадія IF), а друга в тому ж такті хоче прочитати [rsi + rcx*4] (стадія MEM інструкції I1). Один порт, двоє охочих. Хтось мусить почекати.Структурні конфлікти — переважно історичний клопіт. Сучасні проєкти дублюють усе, за що точиться змагання: окремі кеші інструкцій та даних (старий патерн розділеного L1 з L11), кілька АЛП, кілька портів пам'яті, кілька портів запису регістрового файлу. Якщо структурний конфлікт усе-таки лишається після роботи архітектора, то це зазвичай свідомий компроміс «ціна/продуктивність» на рідко використовуваному блоці (скажімо, спільному дільнику з плаваючою комою). Не затримуймося на них. Згадали — і далі.Конфлікти даних виникають із залежностей між інструкціями на одному й тому самому регістрі або комірці пам'яті. Таксономія конфліктів випливає з одного запитання: у програмному порядку, хто читає, а хто записує, і що станеться, якщо ми переставимо їх у часі?Форм три, і вони не однаково важливі.RAW — читання після запису (справжня залежність). Інструкція B читає значення, яке виробляє інструкція A. B буквально потребує виходу A. Це справжня залежність — RAW стосується самого обчислення, а не імен регістрів. Погляньмо на наш наскрізний приклад:loop: mov eax, [rsi + rcx*4] ; I1: завантажити array[i] в eax add ebx, eax ; I2: sum += eax inc rcx ; I3: i++ cmp rcx, rdx ; I4: порівняти i з n jne loop ; I5: перехід назад, якщо не рівні
I2 читає eax. I1 записує eax. Це RAW. I2 не може обчислити ebx + eax, поки завантаження I1 не видасть значення для eax, а на наївному конвеєрі I2 входить у EX іще до того, як I1 встигне завершити MEM. Результат: затримка. Цей конкретний різновид RAW — завантаження, за яким одразу йде АЛП-інструкція, — і називають конфліктом використання після завантаження (load-use hazard); це найважчий конфлікт даних для усунення. Ми ще повернемося до нього в §4.I4 читає rcx. I3 записує rcx. Іще один RAW. I5 читає прапорці, які щойно записав I4. І ще один RAW (на неявному регістрі прапорців). RAW повсюди у звичайному коді — бо саме це й означає «обчислювати»: узяти значення, щось із ним зробити, передати результат на наступний крок.RAW прибрати неможливо. RAW — це факт про потік даних у програмі. Максимум, що можна зробити, — це мінімізувати витрати часу на його задоволення; саме цим і займається випередження.WAR — запис після читання (антизалежність). Інструкція B записує в регістр, який читає A, — але A має прочитати старе значення. Якщо конвеєр дасть B обігнати A, A ризикує випадково побачити вже нове значення від B.I_a: add rax, rbx ; читає rbx
I_b: mov rbx, 0 ; записує rbx
Якщо I_b якимось чином виконається раніше, ніж I_a прочитає rbx, I_a побачить нуль замість старого rbx. Це WAR. На простому конвеєрі з виконанням за порядком WAR узагалі не виникає — читання завершуються по порядку, — тож поки що нам до нього байдуже. Він стає актуальним лише тоді, коли ми дозволяємо машині переставляти інструкції (§6).WAW — запис після запису (вихідна залежність). Дві інструкції записують у той самий регістр. Програмний порядок каже, що перемагає друга (її значення — те, що лишається після). Якщо конвеєр дасть їм завершитися не в порядку, вижити може не та.I_a: mov rax, 1
I_b: mov rax, 2 ; фінальне значення rax має бути 2
Знову ж таки — дрібниця за виконанням за порядком. І справжній жах за позачерговим.Ось ключове спостереження, і воно задає рамку всій другій половині лекції: WAR і WAW не є справжніми залежностями даних. Це артефакти того, що ми повторно використовуємо імена регістрів. rbx — це ім'я, яке обрав програміст або компілятор, а не якийсь закон Всесвіту. Якби апаратура потайки давала кожному запису свою приватну копію rbx, WAR і WAW просто б зникли: жодні дві інструкції навіть не торкалися б того самого фізичного сховища. Цей прийом називається перейменуванням регістрів, і саме ним і займається позачергова машинерія з §6. Тож до кінця §3 і в усьому §4 ми майже повністю зосередимося на RAW. WAR і WAW отримують вільний прохід до §6, де й випаровуються.80 % енергії реального CPU, що йде на конфлікти даних, припадає на RAW. Туди й спрямовуйте увагу.Конфлікт керування (він же конфлікт переходів) виникає через умовні переходи, непрямі переходи та повернення — усе, де стадія вибірки не знає, з якого PC вибирати, доки пізніша стадія не виконає певної роботи.Погляньмо на I5 із прикладу: jne loop. Залежно від прапорців, які встановив cmp з I4, виконання або стрибає назад на loop, або продовжує з наступної інструкції. На підручниковому 5-стадійному конвеєрі перехід резолвиться наприкінці EX. Це стадія 3. А тим часом стадії 1 і 2 (IF та ID) уже весело вибирали й декодували дві інструкції, що йшли в пам'яті одразу після jne, — інструкції, які в разі взятого переходу виявляться повністю хибними, і їх доведеться викинути.Такт: 1 2 3 4 5 6
I5 jne IF ID EX ME WB ; перехід резолвиться тут, у кінці EX (такт 3)
I? (хибн.) IF ID -- ; спекулятивно вибрано, потім знищено
I? (хибн.) IF -- ; та сама доля
I? (правил.) IF ID ... ; нарешті вибрано з цілі переходу
Два такти, змарновані на один перехід, — на 5-стадійному конвеєрі, який резолвить переходи в EX. Це не прорахунок дизайну, а неминучий наслідок того, що вибірка йде швидше, ніж ухвалюється рішення. Ціна переходу називається штрафом за хибне передбачення і масштабується з глибиною конвеєра. 5-стадійний конвеєр втрачає 2–3 такти. У §4 ми побачимо, що 31-стадійний Pentium 4 втрачав на хибно передбаченому переході аж двадцять тактів, і саме це стало однією з головних причин провалу тієї мікроархітектури.Переходи трапляються часто — зазвичай 15–25 % інструкцій у цілочисельному коді, — тож навіть однотактовий штраф за конфлікт керування дуже дорогий. Власне, через це й існує передбачення переходів (§4), і саме тому воно — ймовірно, найважливіший мікроархітектурний прийом у сучасному проєктуванні процесорів.Попрацюймо безпосередньо на LO5 (аналіз продуктивності, математика вузьких місць) — порахуймо CPI «на серветці». Припустімо:
20 % інструкцій — завантаження, кожне спричиняє 1-тактову затримку використання після завантаження (випередження тримає її на рівні 1, а не 2)
20 % інструкцій — переходи зі штрафом за хибне передбачення 2 такти, а передбачувач помиляється у 10 % випадків
Решта 60 % течуть чисто з CPI = 1
Тоді:CPI ≈ 1 (база, одна інстр. за такт) + 0.20 · 1 (штраф load-use) + 0.20 · 2 · 0.10 (штраф хибного передбачення переходу) = 1 + 0.20 + 0.04 = 1.24
CPI 1.24 означає пропускну здатність близько 0,81 інструкції за такт — 19 % податку на ідеалізований конвеєр, повністю сплаченого конфліктам. І це з випередженням і з 90-відсотково точним передбачувачем переходів. Без них цифри швидко псуються: load-use виростає до 2 тактів, частка хибних передбачень на важкопередбачуваних переходах повзе до 50 %, і CPI легко роздувається до 2,0 чи гірше. Саме це сітка «конвеєризовано, без випередження» з §2 показувала вам у картинках.Три категорії конфліктів і чіткий порядок пріоритетів: структурні конфлікти переважно розв'язуються дублюванням апаратури; у конфліктах даних домінує RAW, який стерти не можна, але можна здешевити; у конфліктах керування домінують переходи, по яких ми б'ємо передбаченням. Це §4.Далі за §4 WAR і WAW повернуться як справжні проблеми — щойно ми почнемо виконувати позачергово, — і ми розв'яжемо їх перейменуванням регістрів (§6). Структурні конфлікти з'являться в новому вигляді в §5 (суперскалярність), коли ми запитаємо: а що, як дві інструкції захочуть єдине АЛП в тому самому такті? Категорії нікуди не діваються; вони просто мутують у міру того, як машина стає агресивнішою.Наразі — імена, форми й формула CPI. Переходимо до лікування.§3 дав вам погані новини. §4 дає хороші: більшу частину вартості конфліктів конвеєра можна усунути — або принаймні глибоко сховати — напрочуд дешевими апаратними прийомами. Двома зокрема. Випередження б'є по конфліктах даних, а передбачення переходів — по конфліктах керування. Разом вони знижують CPI з ~2,0 на наївному конвеєрі до ~1,2 на машині, яка все ще принципово є одновидачною, виконує за порядком і не робить нічого розумного поза цим розділом.Це Рівень 3 наскрізного прикладу.Погляньте ще раз на конфлікт використання після завантаження з §3:I1: mov eax, [rsi + rcx*4] ; eax завантажується в MEM
I2: add ebx, eax ; eax потрібен у EX
На підручниковому 5-стадійному конвеєрі (IF, ID, EX, MEM, WB) завантажене значення I1 потрапляє в регістровий файл лише наприкінці WB. Це стадія 5. А I2 хоче прочитати eax уже в ID (стадія 2) і обчислити з ним у EX (стадія 3). Якщо I2 відстає від I1 на один такт, то коли I2 заходить у EX, I1 якраз у MEM, — а потрібне для I2 значення буквально сидить на виході MEM, просто на дроті, і лише на один такт запізно, щоб встигнути дістатися регістрового файлу.То й не ганяймо його через регістровий файл. Випередження (також відоме як обхід або випередження операндів) додає прямі дроти з виходів пізніших стадій конвеєра назад до входів EX. Коли логіка виявлення конфліктів бачить, що регістр-джерело I2 збігається з регістром-приймачем I1, вона маршрутизує значення просто з того місця, де воно зараз лежить, — із регістра конвеєра MEM/WB — прямо на вхід АЛП для I2, замість того, яке наразі в регістровому файлі. I2 ніколи не чекає: він отримує свіже значення в той самий такт, у якому інакше прочитав би застаріле.<br><img alt="Моментальний знімок шляху даних в один такт: I1 перебуває в стадії MEM, тоді як I2 — у стадії EX. «Нормальний» шлях — MEM → WB → запис у регістровий файл → читання регістра в ID — доставив би завантажене значення I1 на два такти запізно. Суцільний помаранчевий дріт випередження обходить усю цю окружну дорогу, маршрутизуючи значення прямо з виходу MEM назад на вхід EX на тому самому фронті такту." src="assets/l14-forwarding-bypass.svg" target="_self" style="width: 700px; max-width: 100%;">Які конфлікти вбиває випередження?
АЛП — в АЛП (EX → EX): повністю. Якщо I1 обчислює щось у EX, а I2 потребує цього значення в EX на такт пізніше, то вихід АЛП I1 безпосередньо подається на вхід АЛП I2. Жодних бульбашок. У наскрізному прикладі inc rcx з I3, що подає результат у cmp rcx, rdx з I4, — саме цей випадок: RAW, який раніше коштував двох тактів затримки, тепер не коштує нічого.
АЛП — у збереження (EX → MEM): повністю. Обчислене значення можна випередити прямо в дані збереження.
Використання після завантаження (MEM → EX): майже. І це важливе застереження.
Конфлікт використання після завантаження переживає випередження у вигляді однієї неминучої бульбашки. Ось чому: завантаження видає своє значення лише наприкінці MEM. Якщо це значення потрібне наступній же інструкції як вхід АЛП, то вона потребує його на початку EX — на одну стадію раніше за MEM. А дроту, яким можна запустити сигнал назад у часі, не існує. Споживач мусить зачекати один такт. Уже після цього випередження доставить значення з MEM у EX через оцю однотактову прогалину, але сама прогалина фізична, і закрити її неможливо.Тож load-use I1→I2 у наскрізному прикладі коштує на Рівні 3 рівно один такт, а не два. Інші два RAW (I3→I4, I4→I5) не коштують нічого. Це величезний зсув.Коли випередження не здатне вас урятувати — як у випадку load-use, — конвеєру доводиться таки стояти. Механічно за це відповідає невеликий блок логіки, який називається блоком виявлення конфліктів: він стоїть у кінці ID і перевіряє — «чи інструкція, яку я збираюся відправити в EX, читає регістр, у який наразі записує завантаження, що вже перебуває в EX?». Якщо так, він заморожує PC і регістр IF/ID на один такт і впорскує бульбашку — no-op — у регістр ID/EX. З погляду конвеєра крізь нього протекла нешкідлива NOP, а справжня I2 отримує другий шанс у наступному такті.Ось і вся суть затримки: заморозити попередні стадії, а далі за потоком вставляти NOP — стільки тактів, скільки вимагає конфлікт. Це молоток, але хірургічний: сама логіка виявлення — це, може, кількасот вентилів, і вона працює паралельно з декодуванням.Одна бульбашка на load-use — це боляче. Але компілятор часто може усунути її безплатно, переставивши в слот одразу після завантаження якусь незалежну інструкцію. Якщо компілятор знаходить роботу, що не залежить від завантаженого значення і не бере участі в обчисленні адреси завантаження, він підсуває цю роботу в слот затримки load-use. Якийсь inc чи що там — займає прогалину, апаратна логіка затримки так і не спрацьовує, а бульбашка зникає. Це називається плануванням інструкцій компілятором і є стандартним прийомом у будь-якому оптимізаційному бекенді компілятора. Зверніть увагу: це не заміна апаратному випередженню, а дешева оптимізація поверх нього. Справжня відповідь — це компілятор плюс випередження.Тепер конкретно. Ось наскрізний приклад у тому вигляді, у якому його міг би спочатку видати наївний компілятор, — підручниковий порядок, де add стоїть безпосередньо після mov і на кожній ітерації з'їдає бульбашку:; До: наївний порядок — затримка load-use на 1 бульбашку за ітерацію
loop: mov eax, [rsi + rcx*4] ; I1: завантажити array[i] (видає eax у кінці MEM) add ebx, eax ; I2: sum += eax &lt;- затримка load-use, 1 бульбашка inc rcx ; I3: i++ cmp rcx, rdx ; I4: порівняти i, n jne loop ; I5: перехід
А тепер подивіться, як оптимізаційний компілятор переставляє тіло циклу. Він помічає, що inc rcx не читає eax і не бере участі в обчисленні адреси завантаження (адресу було вже обчислено в стадії EX інструкції I1 зі старим rcx), — тож пересунути inc rcx між завантаженням і add безпечно:; Після: перевпорядкований варіант — бульбашка усунена, та сама семантика
loop: mov eax, [rsi + rcx*4] ; I1: завантажити array[i] (старий rcx зафіксовано тут) inc rcx ; I3: i++ &lt;- заповнює слот load-use add ebx, eax ; I2: sum += eax (eax уже готовий, без бульбашки) cmp rcx, rdx ; I4: порівняти новий i, n jne loop ; I5: перехід
Ті самі п'ять інструкцій, той самий спостережуваний ефект на ebx і rcx наприкінці кожної ітерації, але слот затримки load-use тепер заповнений корисною роботою замість бульбашки. Щоб довести, що перестановка законна, компілятору потрібні два факти: inc rcx не читає eax (на завантаженому значенні немає RAW) і inc rcx не змінює адреси, з якої I1 завантажував (ефективну адресу I1 вже було защіпнуто в конвеєрі на вході в EX, тож пізніший inc не зможе ретроактивно посунути завантаження). На реальному процесорі gcc -O2 або clang -O2 зроблять це автоматично — саме переупорядкована послідовність і опиняється у виконуваному файлі.У наскрізному прикладі ми збережемо оригінальний підручниковий порядок заради педагогічної ясності, але справжній бінарник на справжньому процесорі бульбашку на місці не залишить.Конфлікти керування гірші за конфлікти даних, бо конфлікт керування затримує не одну інструкцію на один такт — він морить голодом увесь блок вибірки, поки перехід не резолвиться. На 5-стадійному конвеєрі ціна — 2–3 такти на кожен перехід. На глибокому конвеєрі це вже катастрофа.Ліки: вгадувати. Коли стадія вибірки бачить перехід і ще не знає, куди він піде, вона обирає напрямок, робить вибірку так, ніби здогадка вірна, і починає спекулятивно подавати інструкції в конвеєр. Якщо здогадка правильна — часу не втрачено. Якщо хибна — спекулятивні інструкції знищуються (вимиваються з конвеєра як бульбашки), а вибірка перезапускається з правильної цільової адреси.<br>Працює це лише тоді, коли здогадка здебільшого правильна. <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Branch_predictor" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Branch_predictor" target="_self">Стаття у Вікіпедії про передбачувачі переходів</a> описує це чітко: «цифрова схема, що намагається вгадати, куди піде перехід, ще до того, як це стане відомо остаточно». Сучасні передбачувачі досягають на типовому коді 95–99 % точності. Це та магічна цифра, яка й робить усе інше в цій лекції вартим заходу.Побудуймо передбачувач крок за кроком.Статичне передбачення — найгрубіше: компілятор або архітектура просто постановляють «усі переходи вважаємо взятими» (або «невзятими», або «зворотні взяті, а прямі — ні»). Для циклів це працює напрочуд непогано: зворотний перехід унизу циклу береться на кожній ітерації, крім останньої, тож «зворотні взяті» майже завжди правильно. Але на if-переходах з умовами, залежними від даних, воно зазнає поразки: компілятор просто не може під час компіляції знати, куди поведуть конкретні дані. Самого статичного замало.Динамічне передбачення використовує історію виконання: подивися, що цей перехід зробив минулого разу, і передбач те саме й цього разу. Виявляється, спрацьовує це дуже добре, бо переходи в реальних програмах здебільшого зовсім не випадкові — більшість циклів прокручують багато ітерацій, у більшості if-умов є домінантний напрямок тощо.Найпростіша динамічна схема: один біт на перехід, що запам'ятовує «відбувся» чи «не відбувся» з останнього випадку. Передбачаємо те саме знову.Для циклів це напрочуд непогано: перехід, узятий 99 разів поспіль, і далі передбачається як «взятий». Але в цієї схеми є неприємний режим відмови: щоразу, коли стабільний патерн перевертається, вона помиляється двічі. Візьмімо перехід, що чергується T, N, T, N, T, N, ... Однобітний передбачувач помилятиметься на кожному випадку. А типова ситуація з циклом, що прокручується N разів, дає аж два хибні передбачення: одне на останній ітерації (цикл виходить, передбачувач сказав «взятий», а насправді «невзятий») і ще одне на наступному вході в цикл (передбачувач тепер каже «невзятий», а цикл знову береться). Для коротких циклів, які запускаються багато разів, це дуже болюче.Ось передбачувач, який ви маєте вміти накреслити з пам'яті на іспиті. Це скінченний автомат із чотирьох станів — по одному на відстежуваний перехід:<br><img alt="Скінченний автомат 2-бітного насичувального лічильника — передбачувача переходів. Чотири стани, розміщені по кільцю: «Сильно не переходить», «Слабо не переходить», «Слабо переходить», «Сильно переходить». Стрілки «Переходить/Не переходить» проводять лічильник туди й назад; одне хибне передбачення лише штовхає стан на один крок, а не перевертає передбачення цілком." src="assets/l14-branch-predictor-2bit.svg" target="_self" style="width: 700px; max-width: 100%;">Два біти, чотири стани, один перехід на кожен біт результату. «Сильні» стани втрачають силу лише на хибному передбаченні; щоб перекинути загальне рішення, треба помилитися двічі поспіль.Правило передбачення: передбачати взятий, якщо лічильник в одному з двох «взятих» станів (слабому чи сильному), і невзятий в іншому випадку. Правило оновлення: на кожен реальний результат рухатися на крок у бік цього результату, насичуючись на кінцях.<br>Чому це так помітно краще за однобітну схему? Бо вона поглинає короткочасні збої. Цикл, що прокручує 100 ітерацій і виходить: уже до другої ітерації передбачувач у стані «Сильно взятий» і там і залишається. На виході з циклу він помиляється один раз (хибне передбачення на виході) і опускається в «Слабо взятий», — але все одно продовжує передбачати «взятий». На повторному заході в цикл (скажімо, у зовнішньому циклі) перша ітерація одразу передбачається правильно. 2-бітний лічильник коштує одного хибного передбачення на виході; однобітний коштував двох. <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Branch_predictor" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Branch_predictor" target="_self">Стаття Вікіпедії про передбачувачі переходів</a> наводить точність бімодальних передбачувачів — близько 93,5 % на SPEC'89.Передбачення напрямку (взятий/невзятий) — це лише половина історії. Якщо ви передбачили, що перехід буде взятий, вам так само треба знати, куди стрибати, — і то в тому самому такті, щоб стадія вибірки могла одразу перенаправитися. Чекати до декодування, щоб з'ясувати адресу цілі, — це марнування тактів: ви ж уже зобов'язалися спекулювати.Розв'язок — буфер цілей переходів (Branch Target Buffer, BTB): невеликий кеш, індексований за PC переходу, в якому зберігається остання побачена адреса цілі. Коли вибірка бачить інструкцію, чий PC влучає в BTB, вона знає: «минулого разу тут був перехід, і він пішов сюди», — і вже наступного ж такту може починати вибірку звідти, ще до того, як перехід навіть буде декодовано. BTB критично важливі для непрямих переходів (покажчики на функції, віртуальні виклики, повернення), де ціль не вивести з самого кодування інструкції.2-бітний лічильник каже: «цей перехід зазвичай іде ось сюди». Але переходи в реальних програмах корелюють один з одним — якщо останні кілька переходів пішли T, T, N, цей контекст уже щось повідомляє про те, як поведеться цей перехід. Дворівневий адаптивний передбачувач відстежує коротку історію останніх результатів переходів (зсувний регістр з останніх n бітів «взятий/невзятий») і використовує цю історію як індекс у таблицю 2-бітних лічильників. Кожен патерн історії має свій власний лічильник.Цю техніку Є та Патт представили 1991 року, і вона лежить в основі кожного серйозного сучасного передбачувача. Точність підіймається з ~93 % до ~97 % на стандартних бенчмарках. Варіанти: глобальна історія (один зсувний регістр, спільний для всіх переходів, — ловить кореляції між різними переходами) та локальна історія (один зсувний регістр на перехід, — ловить внутрішні патерни на кшталт «береться кожного третього разу»). Сучасні високопродуктивні проєкти використовують гібридні схеми — наприклад, TAGE (кілька таблиць, індексованих різними довжинами історії) чи перцептронні передбачувачі (лінійні класифікатори над бітами історії). Для цього курсу деталі вам не потрібні. Що треба знати — сучасні передбачувачі вражаюче точні.Одні переходи краще передбачати за локальною історією, інші — за глобальною. Турнірний передбачувач запускає два (або більше) під-передбачувачі паралельно й має мета-передбачувача — власний невеликий масив лічильників, — який для кожного переходу вчиться, котрий із під-передбачувачів перемагає, і бере його відповідь. Схему запропонував МакФарлінг 1993 року, і вона стала стандартом, починаючи з Alpha 21264. Це повторюваний патерн в архітектурі комп'ютерів: коли не можете обрати між двома хорошими ідеями, запустіть обидві, а третя схема нехай учиться, коли і якій довіряти.Коли передбачувач помиляється, усі спекулятивні інструкції, що опинились у конвеєрі після переходу, треба скинути. Ціна — штраф за хибне передбачення — приблизно дорівнює глибині конвеєра між вибіркою і тією стадією, де перехід резолвиться. На нашому 5-стадійному конвеєрі це 2–3 такти. На 14-стадійному Pentium III — близько 10. На сумнозвісному Pentium 4 Prescott — 31 стадія конвеєра, спроєктована задля екстремальних тактових частот, — одне хибне передбачення коштувало понад 20 тактів. При 95 % точності передбачення на коді з великою кількістю переходів (~20 % інструкцій) змарновані такти сумувалися катастрофічно: реальна продуктивність Prescott часто була гіршою, ніж у Pentium III із коротшим конвеєром на тій самій частоті, — штраф за перехід з'їдав усе, що вигравалося на такті. Зрештою Intel відмовилася від стратегії «робімо конвеєр довшим». Урок залишається назавжди: глибокі конвеєри помножують ціну кожного хибного передбачення, і з певної глибини математика перестає сходитися.Сучасні ядра сидять на 12–20 стадіях з точністю передбачення понад 97 % — це приблизно й є «солодка точка».Після Лаб 1 і 2 у вас дуже конкретна ментальна модель 5-стадійного конвеєра — IF, ID, EX, MEM, WB, зібраних вручну довкола невеликого АЛП із чотирма інструкціями. Картина правильна — і водночас це мультик. Справжнє ядро Intel Golden Cove чи AMD Zen 4 має близько 17 стадій. Звідки беруться ще дванадцять? Коротко: кожна з підручникових п'яти ділиться на кілька, бо на частотах 4–5 ГГц один такт надто короткий, щоб вмістити всю ту роботу, яку підручник ущільнює в одну стадію.Ось приблизна розкладка того, як п'ять підручникових стадій розгортаються в сучасному високопродуктивному ядрі x86. Конкретна кількість на стадію змінюється від дизайну до дизайну (це не точно Zen 4 — радше репрезентативний ескіз), але засвоїти вам слід саме форму:Додайте все: приблизно 4 + 4 + 4 + 4 + 2 = 18 стадій, — якраз у сучасному діапазоні. Це не штучне наповнення: кожен рядок таблиці — це реальне звернення до SRAM, реальна арифметична операція або реальна перевірка залежностей, яка фізично не вміщається в коротший такт на мультигігагерцових частотах.Навіщо взагалі ділити? Бо менше роботи на стадію означає коротший тактовий період, а отже, вищу тактову частоту. Однотактний шлях даних із §1 мав період ~5 нс і тактувався на 200 МГц. Поділіть його на 18 стадій, де найдовша — приблизно 250 пс, — і можна тактувати на 4 ГГц. Ось у цьому і полягає виграш у пропускній здатності: у двадцять разів більше інструкцій за секунду з того самого кремнію, якщо ніщо не застрягає.А чому тоді не йти глибше? Бо з глибиною конвеєра ростуть дві ціни. По-перше, штраф за хибне передбачення зростає лінійно: той 18-стадійний конвеєр на поганому переході скидає ~15 інструкцій, а не 2. По-друге, конфлікти й випередження дорожчають: шляхи випередження мусять тягнутися на більшу фізичну відстань по кристалу, регістровий файл мусить відстежувати більше запитів у польоті, мережа обходу стає комбінаторно складною. Pentium 4 Prescott дійшов до 31 стадії й тяжким шляхом з'ясував, що поза ~20 граничний виграш у тактовій частоті вже не покриває вартості скидання на реальному коді. Intel відмовилась від цієї стратегії й відтоді тримається діапазону 14–20.Ментальна модель, яку варто тримати в голові: 5-стадійний конвеєр, який ви зібрали в лабораторній, має таку ж форму, що й сучасне ядро. Сучасне ядро просто замінило кожну позначену коробку маленьким конвеєром усередині неї. IF — це не один такт, а міні-конвеєр: передбачити / транслювати / прочитати / поставити в чергу. EX — це теж не один такт: запланувати / прочитати / обчислити / обійти. Високорівнева схема «інструкції течуть зліва направо крізь названі стадії» не змінилася; змінилася лише гранулярність.<br>Якщо вам колись захочеться побачити це все конкретно й на живому чипі, посібники Аґнера Фоґа з мікроархітектури та огляди знімків кристалів конкретних ядер (Skylake, Zen 3, Firestorm) на <a data-tooltip-position="top" aria-label="https://en.wikichip.org/" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikichip.org/" target="_self">WikiChip</a> документують розкладку по стадіях для кожного покоління. Для цього курсу вони не обов'язкові, але якщо саме конвеєризація вас зачепила, то кроляча нора справді веде вглиб саме туди.Той самий код — тепер на конвеєрі з повним випередженням EX→EX, випередженням MEM→EX, виявленням load-use, що вставляє одну бульбашку, і 2-бітним передбачувачем переходів перед стадією вибірки, який уже має запис для переходу jne loop.<br><img alt="Наскрізний приклад з увімкненими обходами випередження та передбаченням переходів. Більшість затримок зникають; криві помаранчеві стрілки «fwd» несуть значення з виходів EX або MEM назад до пізніших входів EX. Лише конфлікт load-use між I1 і I2 виживає як одна бульбашка. CPI тепер близько 1.2." src="assets/l14-running-example-forwarding.svg" target="_self" style="width: 700px; max-width: 100%;">Полічіть бульбашки: одна. Не п'ять, не три — одна. Затримка load-use на I2 — єдина непоправна затримка в усій ітерації. Випередження звело I3→I4 (АЛП-в-АЛП RAW на rcx) до нуля. Перехід на I5 спекулятивно передбачається взятим, після другої ітерації запис у передбачувачі вже в стані «Сильно взятий», і вибірка безшовно переходить до наступної ітерації без жодного скидання конвеєра.На N ітераціях передбачувач помилиться рівно один раз — на самій останній ітерації, коли цикл виходить. Якщо вважати цю помилку 2-тактовим скиданням, то загальна вартість циклу становитиме приблизно:Такти ≈ N × (5 інструкцій + 1 бульбашка load-use) + 2 (хибне передбачення на виході) = 6N + 2
Це 6 тактів на 5 інструкцій, тож CPI ≈ 6/5 = 1,2 — рівно туди, де Рівень 3 мав опинитися в таблиці з §1. Випередження звело АЛП-RAW до нуля, детектор конфліктів зменшив load-use з 2 бульбашок до 1, а 2-бітний передбачувач зробив перехід фактично безкоштовним для всіх ітерацій, окрім останньої. Ми не розширювали конвеєра, не ламали програмного порядку, не додавали виконавчих блоків — і вже переїхали з CPI ~5 (без конвеєра) до CPI ~1,2.Зверніть увагу, чого ми в §4 не робили: ми жодного разу не виконали більш ніж одну інструкцію за такт. Усе в цьому розділі стосувалося того, як змусити 1-широкий конвеєр текти плавно. Його стеля — CPI = 1. Скільки б ви не вилизували випередження й передбачення, нижче цього числа не опуститеся ніколи, бо яким би ідеальним не було передбачення, машина з одинарною видачею видає щонайбільше одну інструкцію за такт.Щоб іти швидше за CPI = 1 — тобто мати пропускну здатність 2, 3, 4 інструкції за такт, — ми мусимо вибирати, декодувати та видавати більш ніж одну інструкцію на такт. Це вже інша машина. Вона зветься суперскалярною, і їй присвячено §5.Випередження та передбачення переходів вичавили бульбашки з конвеєра. Наш робочий приклад опустився приблизно до CPI ≈ 1,2 — щотакту завершується одна інструкція, плюс невеликий запас на конфлікт використання після завантаження. Це природна стеля конвеєра, що видає одну інструкцію за такт. Хочете швидше — доведеться видавати більше.<br>У цьому й полягає ідея суперскалярності: продублювати бекенд, розширити фронтенд і дати кільком незалежним інструкціям стартувати в тому самому такті. <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Superscalar_processor" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Superscalar_processor" target="_self">Суперскалярний процесор</a> «може виконувати або починати виконання більш ніж однієї інструкції протягом одного такту, одночасно диспетчеризуючи кілька інструкцій у різні виконавчі блоки». Коли це працює, CPI падає нижче 1 — або, що те саме, IPC (інструкцій за цикл) піднімається вище 1.У 2-широкому суперскалярному ядрі два слоти вибірки, два слоти декодування й щонайменше два функціональних блоки, здатних працювати паралельно. У 4-широкому — по чотири всього. Фронтенд читає вікно сусідніх інструкцій, логіка перевірки залежностей визначає, які з них можна пустити разом, а бекенд запускає їх паралельно.Першим масовим суперскалярним x86 став Intel Pentium 1993 року зі своїми знаменитими U-конвеєром і V-конвеєром — двома цілочисельними конвеєрами й набором правил, підібраних уручну, про те, які саме пари інструкцій можна видавати разом. За сучасними мірками це було примітивно, але довело, що ідея працює і поза світом мейнфреймів. До 1998 року практично кожен процесор загального призначення, який ішов у продаж, був суперскалярним.Сучасні x86-ядра зазвичай мають ширину декодування 4–6. P-ядро Apple M1 (Firestorm) — 8-широке; чому ARM може собі це дозволити, а x86 — ні, ми розглянемо у §6.5.Розширення машини допомагає лише тоді, коли в програмі є сусідні незалежні інструкції. Запустімо цикл підсумовування масиву на 2-широкому суперскалярі з виконанням за порядком і подивімося, що вийде.<br><img alt="Робочий приклад на 2-широкому суперскалярному конвеєрі з виконанням за порядком. Видно дві паралельні смуги видачі; смуга B обробляє незалежні інструкції, а смуга A зависає на ланцюжку залежностей I1 → I2 — ланцюжок залежностей усе ще серіалізує половину ширини видачі." src="assets/l14-running-example-superscalar.svg" target="_self" style="width: 700px; max-width: 100%;">Подивіться на такт 1. Фронтенд намагається вибрати дві інструкції у дві смуги. Він може спарити I1 (завантаження) з I3 (inc rcx), бо I3 не залежить від I1: 2-широкий суперскаляр спокійно простягає I3 наперед повз I2, якщо I3 не залежить ні від чого, що стоїть попереду. I2 мусить триматися програмного порядку й чекати позаду I1, бо вона використовує eax, — тож I2 вибирається у такті 2 разом із I4. I5 йде сама у такті 3.Зверніть увагу на те, що діаграма показує, а чого — не показує. На ній зображено, як інструкції однієї ітерації заходять у конвеєр протягом 3 тактів вибірки: це показник пропускної здатності конвеєра — темп, у якому стартують нові інструкції. Останній зворотний запис припадає на такт 7, бо кожна інструкція все одно мусить пройти свій п'ятистадійний шлях (плюс однотактову бульбашку load-use на I2) перед завершенням, і видача такту 3 не може завершитися раніше, ніж у такті 7. Такт 7 — це затримка однієї ітерації наскрізь, а не її пропускна здатність.Ці два числа відрізняються навмисно. Для довгого циклу заповнення конвеєра — одноразова ціна: платите ~4 такти розігріву, а далі кожні 3 такти видається нова порція з 5 інструкцій, і ще через 3 такти вона завершується. На N ітерацій загальна вартість — приблизно 4 + 3·N, а не 7·N. Звідти й береться CPI ≈ 3/5 = 0,6: це твердження про усталену пропускну здатність, а не «одна ітерація закінчується за 3 такти».Чесна стеля насправді трохи гірша за 0,6, і варто побачити, чому. I1 наступної ітерації (завантаження array[i+1]) залежить від rcx, який оновлює I3 поточної ітерації. Тож між ітераціями є серіалізація, про яку діаграма мовчить; реальний замір буде ближчим до CPI ≈ 0,9, особливо якщо врахувати, що бульбашка load-use нікуди не поділася. 0,6 — це ідеалізована стеля, до якої 2-широка машина могла б дотягнутися, якби не було міжітераційного ланцюжка; 0,9 — це те, що цикл підсумовування масиву видає насправді.Це краще за 1,2, але до наївно очікуваних 0,5 від «2-ширини» ще далеко. 2-широкий фронтенд завантажений наполовину, бо в програмі є послідовні залежності. Розширення до 4 чи 6 допомагає менше, ніж хотілося б, — з тієї ж причини: ланцюжки залежностей серіалізують роботу, яку апаратура не може переупорядкувати.Уважно придивіться до робочого прикладу. Так, I1 → I2 — це послідовний ланцюжок. Але подумайте на одну ітерацію вперед: I1 ітерації k+1 (завантаження array[i+1] після inc rcx) цілком незалежна від I2 ітерації k. Вони живуть у різних ланцюжках потоку даних. Машина, яка дивиться лише на сусідні інструкції в програмному порядку, цього побачити не може: завантаження ітерації k+1 лежить за п'ять інструкцій попереду, ще й за переходом.А машина, яка переупорядковує інструкції згідно із залежностями даних, а не з програмним порядком, могла б запустити завантаження ітерації k+1, поки додавання ітерації k ще чекає. Ось у чому виграш. Паралелізм у програмах є; його просто розставлено не в тому порядку, який зручний апаратурі.Питання тепер таке: як побудувати апаратуру, яка розуміє залежності, а не послідовність?<br>Перш ніж відповідати, зробімо відступ. У 2001 році Intel і HP випустили <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Itanium" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Itanium" target="_self">Itanium</a> (IA-64) — архітектуру VLIW (дуже довге командне слово) / EPIC (явно паралельні обчислення), що була побудована на рівно протилежній ставці. Замість робити апаратуру достатньо розумною, щоб знаходити паралелізм під час виконання, Itanium поклав цю відповідальність на компілятор: ISA пакувала три операції в одне 128-бітне командне слово з явними підказками про паралелізм, бітами спекуляції та предикацією. Гасло було таке: «ми витягуватимемо ILP програмно, тож процесору не потрібна дорога логіка позачергового виконання».Провалилося це з тріском. Корінна проблема в тому, що затримки завантажень і результати переходів динамічні: промах кешу триває у 20 разів довше за влучання, а компілятор не має жодного способу знати наперед, які саме завантаження промахнуться. Статичний розклад змушений скрізь закладати найгіршу затримку, і в типовому випадку машина простоює. Апаратне позачергове виконання (out-of-order, далі OoO) перемагає саме тому, що планує за реально спостережуваними затримками, а не за песимістичними оцінками: процесор дивиться, що готове прямо зараз, і відповідно запускає інструкції, тоді як компіляторний розклад застиг у часі. Itanium програвав OoO-чипам x86 за обсягами продажу приблизно сто до одного, і Intel випустив свій останній процесор Itanium аж у 2021 році.Мораль варто запам'ятати: ILP найкраще витягувати динамічно, на швидкості апаратури, на основі реальної інформації часу виконання. Статичний ILP крихкий.Динамічний ILP потребує апаратури, яка розуміє залежності, а не програмний порядок. Цьому й присвячено наступний розділ.OoO-виконання — алгоритмічне серце будь-якого серйозного процесора, збудованого починаючи з середини 1990-х. По суті, це відповідь на два питання, які звучать суперечливо:
Як виконувати інструкції одразу, щойно їхні операнди готові, незалежно від того, де саме вони стоять у програмі?
Як фіксувати результати в архітектурному стані в програмному порядку, щоб переривання, відлагоджувачі та перемикання контексту бачили чисту, узгоджену машину?
Перше питання — про те, як іти швидко. Друге — про те, як лишатися коректним. Потрібні обидва: швидка машина, яка не вміє прийняти точне переривання, непридатна як процесор загального призначення.<br>Відповідь, яку Роберт Томасуло винайшов в IBM у 1967 році для System/360 Model 91 і яку відтоді шліфувало покоління дизайнерів, — це поєднання <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Tomasulo%27s_algorithm" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Tomasulo%27s_algorithm" target="_self">алгоритму Томасуло</a> (як виконувати позачергово та коректно) і <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Reorder_buffer" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Reorder_buffer" target="_self">буфера переупорядкування</a> (як повертатися до завершення за порядком). Побудуймо це.Пригадаймо з §3, що конвеєри страждають від трьох видів конфліктів даних: RAW (справжня залежність), WAR (запис після читання) і WAW (запис після запису). RAW — фундаментальний: якщо мені потрібне значення, яке ти виробив, я мушу чекати. А от WAR і WAW — це конфлікти імен, а не справжні залежності. Дві інструкції, які обидві хочуть записати eax, конфліктують лише тому, що ділять одне ім'я, а не тому, що якійсь із них потрібні дані іншої.Перейменування регістрів прибирає цей конфлікт. Архітектурний регістр eax — це ім'я, яке бачить програміст; усередині ж CPU тримає пул із сотень фізичних регістрів, і кожна інструкція, що записує в eax, отримує свіжий фізичний регістр. Таблиця перейменування фіксує: «зараз архітектурний eax живе у фізичному регістрі P37». Коли наступна інструкція перезаписує eax, їй дістається P82, а старий P37 живе далі, поки всі його читачі не заберуть звідти своє значення. Два записи в той самий архітектурний регістр тепер можуть виконуватися паралельно — вони просто потрапляють у різні фізичні регістри.Цифри тут важливі. x86-64 надає 16 архітектурних регістрів загального призначення. Сучасне ядро тримає у своєму пулі перейменування 150–400 фізичних цілочисельних регістрів. Станом на 2020/2021 рік Intel Golden Cove має близько 280, Apple Firestorm — близько 354. Додаткові регістри — це сировина, якою живиться OoO: що більше фізичних регістрів, то більше інструкцій у польоті без конфліктів імен.WAR і WAW зникають за самою конструкцією. Єдиний залишковий конфлікт даних — RAW, а відстежувати саме RAW і вміє виконання за потоком даних.Уявіть бекенд OoO-ядра як кімнату, наповнену інструкціями, — кожна чекає на потрібні їй значення. Зона очікування тут не одна черга, а набір станцій резервування: один кластер на кожен функціональний блок. Коли інструкція виходить із фронтенду на диспетчеризацію, вона оселяється на станції резервування, прикріпленій до потрібного їй блока (цілочисельний АЛП, блок завантаження/збереження, FP-множення — що завгодно), і приносить із собою дві речі: уже відомі значення операндів і теги тих операндів, які ще не готові. Тег — це просто ім'я для «фізичного регістра, який має виробити якась інша інструкція у польоті».Відтепер інструкція не чекає чергового такту — вона чекає, доки її теги буде розв'язано. Очікування чисто керується даними.А як теги розв'язуються? Через Спільну шину даних (Common Data Bus, CDB). Коли функціональний блок закінчує обчислення результату, він транслює пару (тег, значення) на CDB. Кожна станція резервування в машині стежить за цією шиною. Якщо станція чекала саме цього тега, вона защіпає значення у відповідний слот операнда. Щойно всі операнди станції заповнено, інструкція спрацьовує і вирушає у свій функціональний блок.Це асоціативне зіставлення — апаратний еквівалент виконання за потоком даних. Програмний порядок зникає; єдине, що вирішує, коли запускати інструкцію, — це чи прибули її входи. Завантаження, що чекає на промах кешу, паркується на своїй станції резервування, нікого не блокуючи. А інструкції далі за потоком, які від цього завантаження не залежать, продовжують текти машиною.Кожна інструкція в OoO-ядрі проходить три фази, і варто бачити їх як сюжет, а не як рецепт.Видача (Issue) відбувається на фронтенді, суворо у програмному порядку. Машина декодує інструкцію, перейменовує її регістр-приймач (захоплює свіжий фізичний регістр), виділяє запис у станції резервування, прикріпленій до потрібного функціонального блока, і — що критично — виділяє слот у хвості буфера переупорядкування. Наприкінці видачі інструкція існує одночасно у двох місцях: вона чекає на дані на станції резервування і водночас тримає зарезервований за нею слот у ROB, який пам'ятає її початкову позицію у програмному порядку.Виконання (Execute) настає щойно операнди інструкції готові, у будь-якому порядку, який допускають дані. Станція резервування спрацьовує, функціональний блок рахує, результат виходить на CDB, кожна залежна станція, що стежила за цим тегом, прокидається. Саме тут і відбувається «позачергове»: завантаження з ітерації 5 може завершитися раніше, ніж додавання з ітерації 3, — якщо те додавання ще чекає на свій операнд.Завершення (Commit) (його ще називають «ретайр») відбувається на голові ROB, теж суворо у програмному порядку. Коли інструкція на голові ROB уже виконалася і її результат лежить у записі ROB, машина записує його в архітектурний регістровий файл і звільняє слот. «Справжньою» — видимою для решти системи — інструкція стає лише в цей момент.Видача за порядком, виконання позачергово, завершення за порядком. От і вся суть.ROB — це кільцеве FIFO шириною в один запис на інструкцію, розміром рівно на стільки інструкцій у польоті, скільки ядро готове відстежувати (десятки для ядер 1990-х, сотні для сучасних). Інструкції заходять у хвіст під час видачі, виходять з голови під час завершення, а між ними сидять у довільному стані виконання — чекаючи, виконуючись або вже виконані, але ще не завершені.Кожен запис ROB містить тип інструкції, її регістр-приймач, слот для результату (заповнюється, коли CDB транслює цей результат) і біт дійсності. Виконані записи шикуються за головою в чергу й чемно чекають своєї черги на завершення.Саме ця форма — те, що потрібно для точних переривань. Якщо ділення на нуль спрацьовує в інструкції, яка сидить десь посеред ROB, апаратурі не треба з'ясовувати, «які інші позачергово виконані інструкції вже записали в регістровий файл?», — бо жодна з них цього ще не зробила. Усе, що стоїть після інструкції-винуватця, досі замкнене в слотах ROB. Відновлення грубе й просте: скинути ROB, починаючи з помилкового запису і далі, звільнити станції резервування, обнулити таблицю перейменування і передати керування обробнику переривання. Архітектурного регістрового файлу ніхто не чіпає: він і далі відображає останню інструкцію, що завершилася чисто.А навіщо воно потрібне? Через перемикання контексту. Пригадайте з L07, що переривання може прийти в будь-якому такті: ОС зберігає стан регістрів перерваної програми, запускає обробник, відновлює стан і повертається. Зберегти вона може тільки узгоджений знімок — набір значень регістрів, який відповідає твердженню «машина виконала інструкції з 1 по k і ось-ось візьметься за k+1». OoO без ROB протягло б у цей знімок наполовину готові інструкції з майбутнього: регістр, уже перезаписаний інструкцією, якої «офіційно» ще не сталося. Відновлення з такого стану дасть сміття. ROB існує саме для того, щоб архітектурний стан завжди виглядав послідовним, навіть якщо мікроархітектура під ним шалено паралельна.Це найважливіша структурна ідея лекції: архітектурний стан і мікроархітектурний стан — це різні речі. ROB — це стіна між ними. Запам'ятайте цю думку: у §7 ми подивимося, що буває, коли ця стіна починає протікати.<br><img alt="Блок-схема позачергового ядра, що поєднує станції резервування Томасуло з буфером переупорядкування. Фронтенд з вибіркою/декодуванням/перейменуванням живить станції резервування; станції резервування запускають інструкції у функціональні блоки (АЛП, блок завантаження/збереження, FP-множення), коли операнди готові; результати, транслювані на Спільній шині даних, за тегами захоплюються станціями, що чекають, та записами ROB; ROB завершує у програмному порядку до архітектурного регістрового файлу." src="assets/l14-tomasulo-cdb.svg" target="_self" style="width: 700px; max-width: 100%;">Ось як сучасне OoO-ядро виглядає на одній діаграмі. Фронтенд вибирає й декодує кілька інструкцій за такт і передає їх на стадію перейменування, яка замінює архітектурні імена регістрів на фізичні. Перейменовані інструкції паралельно надходять у станції резервування, згруповані навколо функціональних блоків, і в хвіст ROB. Функціональні блоки — цілочисельні АЛП, блоки завантаження/збереження, FP/SIMD-блоки — забирають інструкції зі своїх станцій резервування, коли операнди вже готові, виконують і транслюють результати на Спільну шину даних. CDB оновлює фізичний регістровий файл, будить станції резервування, що стежили за тегом, та заповнює слот результату у відповідному записі ROB. На голові ROB блок завершення ретайрить готові інструкції в програмному порядку, копіюючи їхні результати в архітектурний регістровий файл — саме цей стан регістрів побачать ОС, відлагоджувач чи який-небудь обробник винятків.Ось і вся машина. А тепер запустімо на ній цикл підсумовування масиву.<br><img alt="Робочий приклад у позачерговому виконанні на двох ітераціях циклу. Ітерація 1 і ітерація 2 (показана на тонованому тлі) перекриваються у виконанні: завантаження з ітерації 2 стартують, поки ітерація 1 ще закінчується, бо їхні залежності цілком незалежні. CPI падає приблизно до 0,4 й обмежується тепер затримкою завантаження, а не ланцюжками залежностей." src="assets/l14-running-example-ooo.svg" target="_self" style="width: 700px; max-width: 100%;">Назвімо дві ітерації k і k+1 (тож I1k — завантаження array[k], I1{k+1} — завантаження array[k+1] і так далі). Передбачувач переходів уже спекулятивно припустив, що цикл триватиме, тож фронтенд видав у ROB обидві ітерації. Подивіться, що машина щойно зробила. I1_{k+1} почала виконуватися в такті 1, одночасно з I1_k, бо два завантаження не мають спільних залежностей: вони читають різні адреси пам'яті, а rcx уже перейменовано, тож навіть ланцюжок лічильника не серіалізує їх між ітераціями. Послідовний ланцюжок I1_k → I2_k нікуди не дівся і все так само змушує I2_k чекати на завантаження, — але це очікування тепер не зупиняє всю машину, воно зупиняє лише I2_k, а ітерація k+1 тим часом тече далі.Дві ітерації по п'ять інструкцій завершуються приблизно за 7 тактів реальної роботи: CPI ≈ 0,7 на інструкцію, а з ширшим вікном (4–6 ітерацій у польоті) падає до CPI ≈ 0,4 — майже втричі більша пропускна здатність порівняно з конвеєрним виконанням за порядком із §4. Вузьке місце тепер — не ланцюжок залежностей, а затримка завантаження й розмір ROB, який і задає, наскільки далеко машина може зазирнути вперед.Безплатного тут немає нічого. Станції резервування тримають десятки інструкцій, що чекають, і мусять на кожній трансляції CDB виконувати асоціативне зіставлення тегів — а це асоціативна пам'ять, дорога і за площею, і за енергоспоживанням. Стадія перейменування потребує багатопортового фізичного регістрового файлу з сотнями записів. ROB має бути достатньо великим, щоб відстежувати кожну інструкцію у польоті. Найгірше — складність масштабується надлінійно з шириною видачі: подвоєння ширини більш ніж подвоює кількість компараторів, мережу обхідних шляхів і число портів регістрового файлу.Саме тому сучасні x86-ядра упираються десь у ширину 4–6, саме тому розширення далі історично вважалося святим граалем, і саме тому Apple зі своїм 8-широким M1 робить щось справді незвичайне. Про це — у §6.5.Є ще одне, що сучасна OoO-машина мусить робити, щоб її гігантське вікно виконання не пустувало: вгадувати майбутнє. Кожен перехід у потоці інструкцій — потенційна точка зупинки; ROB хоче тримати сотні інструкцій, але це можливо лише тоді, коли фронтенд продовжує годувати його повз десятки переходів, не чекаючи, куди піде кожен із них. Машина мусить спекулювати.OoO витягує паралелізм. Але щоб тримати конвеєр заповненим поза переходами, треба вгадувати майбутнє. Спекуляція. А в спекуляції, як ми побачимо, є темний бік. §7.<br>Якщо все в §6 звучить трохи теоретично — станції резервування, буфер переупорядкування на 200 записів, спільна шина даних, яка тихо транслює результати, — ось як це виглядає, коли хтось реально будує це у промисловому масштабі. У листопаді 2020 року Apple випустила <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Apple_M1" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Apple_M1" target="_self">M1</a>, і цей чип переозначив, чим може бути масове позачергове ядро. Не тому, що Apple винайшла щось принципово нове, а тому, що вона взяла підручниковий дизайн з §6 і масштабувала кожен його вимір далі, ніж будь-хто інший наважувався на споживчому ринку.Це той розділ, де абстрактні діаграми перетворюються на цифри, які можна порівняти з чипом у вашому ноутбуці.Високопродуктивне ядро M1 має кодову назву Firestorm. За стандартами будь-якого сучасного x86-ядра воно величезне:<br><img alt="Спрощена блок-схема високопродуктивного ядра Apple M1 Firestorm. 8-широкий фронтенд (вибірка, декодування, перейменування, диспетчеризація) живить великий буфер переупорядкування на приблизно 630 записів з близько 354 фізичними цілочисельними та 384 фізичними FP/SIMD-регістрами; виконавчі порти нижче містять 6 цілочисельних АЛП, 4 FP/SIMD-блоки, 3 блоки завантаження та 2 блоки збереження. Бічні виноски відзначають удвічі більше OoO-вікно порівняно з типовими x86-ядрами та ISA ARM64 з фіксованою шириною, яка робить 8-широке декодування доступним." src="assets/l14-m1-block-diagram.svg" target="_self" style="width: 700px; max-width: 100%;">Уважно придивіться до рядка ROB. Вікно інструкцій Firestorm приблизно вдвічі більше, ніж у будь-якого сучасного x86-ядра. Кожен запис ROB — це одна інструкція, яку процесор уже вибрав, можливо, виконав позачергово, а тепер вона чекає на завершення (ретайр) у програмному порядку. Більше вікно — це не марнославна цифра, а радіус пошуку, в якому OoO-планувальник має знайти незалежну роботу. Що більший радіус, то більше промахів кешу, переходів і довгих ланцюгів залежностей ядро може сховати за корисним паралельним виконанням.У §6 ми казали: «OoO перетворює затримку пам'яті на задачу пошуку — знайти незалежну роботу, поки завантаження в польоті». Відповідь Firestorm на цю задачу — «шукати серед приблизно 630 інструкцій одночасно». Це буквально кілька повних ітерацій робочого прикладу, накладених одна на одну.Ось запитання, яке має вас непокоїти: виробники x86 випускають позачергові ядра ще з Pentium Pro 1995 року. Чому ж у 2020 році вони все ще застрягли на декодуванні шириною 4 чи 6, поки Apple випускає 8? Невже Intel та AMD просто перестали старатися?Ні. Вони замкнуті рішенням, яке їхня ISA ухвалила десятиліттями раніше.Пам'ятаєте L09? Ми говорили про фундаментальний компроміс у дизайні ISA: кодування інструкцій фіксованої ширини проти змінної довжини. ARM64 обрала фіксовану ширину: кожна інструкція — рівно 4 байти, вирівняна на межі 4 байтів, без винятків. x86 обрала змінну довжину: інструкції бувають від 1 до 15 байтів, і де починається інструкція N+1, не дізнаєшся, поки хоч частково не декодуєш інструкцію N.Це рішення ISA невидиме на рівні вихідного коду. C виглядає як C і там, і там. Але відлуння йде аж до кремнію. Щоб декодувати 8 інструкцій ARM64 паралельно, фронтенд M1 просто нарізає 32-байтовий рядок вибірки на 8 однакових 4-байтових шматочків і передає кожен своєму декодеру. Тривіально паралельно, тривіально масштабовано. А щоб декодувати 8 інструкцій x86 паралельно, спершу треба послідовно пробігти байтовим потоком і з'ясувати, де починається й закінчується кожна з них, — і це сканування лежить на критичному шляху. Intel та AMD компенсують це предекодерами, маркерами довжини, які закешовано в кеші інструкцій, і кешами мікрооперацій (кешами вже декодованих мікрооперацій, що на гарячих циклах повністю оминають фронтенд). Ці прийоми допомагають. Але вони піднімають підлогу, а не стелю. До «вісім інструкцій x86 за такт із холодного I-кеша» ви так і не дійдете — принаймні не з такою легкістю, з якою ARM64 робить це тривіально.Отже, M1 швидший не тому, що Apple щось винайшла. Він швидший тому, що десятиліттями раніше ARM обрала той варіант ISA, який краще масштабується в кремнії ери 2020-х. Пам'ятаєте компроміси ISA з L09? Ось де цей компроміс перетворюється на конкретний результат — десятиліттями пізніше, у час роботи батареї вашого ноутбука.Найчистіше сформулювати можна так: Apple не винайшла нових магічних прийомів — вона просто збудувала значно більшу версію того OoO-ядра, яке вже було в усіх, а ISA зробила це доступним.M1 — це не одне ядро. У ньому на одному кристалі чотири P-ядра Firestorm (продуктивні) і чотири E-ядра Icestorm (енергоефективні), які ділять одну й ту саму ієрархію пам'яті та одну й ту саму ISA ARM64. Планувальник ОС динамічно мігрує потоки між ними залежно від попиту: потік кодування відео — на Firestorm, фоновий потік, що забирає пошту, крутиться на Icestorm за частку потужності.Це теж не винахід Apple. Родовід тягнеться до архітектури ARM big.LITTLE 2011 року. Ставка тут така: у реальних навантаженнях є довгий хвіст низькопріоритетних потоків, яким уся вогнева міць ROB на 630 записів не потрібна, і якщо прив'язати їх до маленького ядра, економляться реальні вати з розетки. M1 розіграв цю ставку на настільному комп'ютері так, як нікому до нього не вдавалося.У L15 ми розберемо, як і чому гетерогенність перемагає монолітне масштабування — і як ця ідея поширюється з CPU на GPU, NPU та всю ту мішанину прискорювачів у сучасній системі на кристалі (SoC). А поки що просто зафіксуйте: чип перед вами вже гетерогенний.<br>Коли M1 вийшов, оглядачі з <a data-tooltip-position="top" aria-label="https://www.anandtech.com/show/16226/apple-silicon-m1-a14-deep-dive" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.anandtech.com/show/16226/apple-silicon-m1-a14-deep-dive" target="_self">AnandTech</a> виміряли однопотокову продуктивність, порівнянну з настільними процесорами Intel верхнього сегмента того ж року, — і то за частку потужності та всередині безвентиляторного ноутбука. Це сталося не завдяки якомусь прориву. Це сталося тому, що Firestorm виконав усе з §6, тільки в масштабі:
8-широке декодування, підживлюване агресивним передбачувачем переходів і великим I-кешем
ROB на ~630 записів, щоб OoO-вікно поглинало промахи кешів L1 і L2
достатньо фізичних регістрів, щоб перейменування регістрів ніколи не впиралося в ліміт
15 виконавчих портів, які тримають усю ту роботу «в польоті» в русі реальним обладнанням
У цьому є урок і про масштаб, і про ISA — урок простий: якщо не вдається переграти конкурента, збудуйте його наново. І виберіть ISA, яка вам це дозволить.Але під цим ховається ще один урок. Щоб цей 8-широкий двигун після кожного переходу в реальному коді був нагодований, M1 — як і кожне інше сучасне OoO-ядро — мусить спекулювати. Він мусить виконувати вгадані шляхи ще до того, як здогадки підтвердяться, і робити це постійно, на кожному переході, цілісінький день. Тридцять років ми виходили з того, що це безплатно: якщо здогадка помилкова, процесор відкочує архітектурний стан, і все виглядає так, ніби нічого й не було.Це припущення виявилося хибним. І ось саме тут справи набувають... цікавого повороту. §7.На першому тижні будь-якого курсу з процесорів вам говорять одне й те саме: «CPU виконує інструкції, а інструкції оновлюють регістри й пам'ять». Ось модель. Регістри й пам'ять — це архітектурний стан, та частина машини, яку ISA обіцяє вам як видиму. Програми його читають, зневадники його виводять, ОС зберігає й відновлює його під час перемикання контексту. Уся абстракція обчислень тримається на ідеї, що цей стан і є власне машиною.Це правда. Але неповна.Поряд з архітектурним станом кожен сучасний процесор несе паралельний усесвіт мікроархітектурного стану: вміст кешів L1, L2 і L3; які рядки вже витіснено; які переходи бачив передбачувач; які адреси лежать у TLB; стан кожної станції резервування та запису ROB. Щодо цього стану ISA не обіцяє нічого. Більшості з нього нема навіть назви в посібнику з асемблера. З погляду програмного забезпечення він невидимий.Якщо не рахувати таймінгів.Влучання в кеш — швидке. Промах — повільний. Якщо ви вмієте достатньо точно вимірювати час (а сучасний процесор із радістю продасть вам лічильник тактів на окрему інструкцію), то зможете ставити машині такі запитання про її мікроархітектурний стан, яких ISA ніколи не збиралася вам дозволяти. І з січня 2018 року дві атаки брутально показали, скільки всього можна з таких запитань витягти.Це найважливіша ідея цієї лекції. Прочитайте її двічі:
Архітектурний стан — це те, чим, за версією ISA, є машина. Мікроархітектурний стан — це те, чим машина є насправді. Spectre і Meltdown — доказ того, що ця різниця має значення, і що мікроархітектурний стан є частиною спостережуваної поведінки машини, визнає це ISA чи ні.
Усе, що йде далі в цьому розділі, — це доведення.§6 закінчився проблемою. Щоб 8-широкий OoO-двигун був нагодованим, процесору треба дивитися на сотні інструкцій за кожен перехід. Але як перехід піде, він поки не знає. Компроміс, на який індустрія пішла приблизно 1995 року, був простий: вгадати. Узяти передбачувач переходів. Виконувати передбачений шлях спекулятивно. Усе відстежувати в буфері переупорядкування. Якщо здогадка правильна — завершити спекулятивну роботу за порядком, і вона зараховується. Якщо помилкова — знищити спекулятивну роботу, почистити ROB від записів хибного шляху та перезапустити з правильної цільової адреси переходу.<br>Це <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Speculative_execution" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Speculative_execution" target="_self">спекулятивне виконання</a>, і саме воно — найважливіша причина, чому ваш ноутбук швидкий. Сучасні передбачувачі переходів досягають на реальному коді понад 95 % точності. Переважну більшість спекулятивної роботи вдається завершити й зарахувати. Незначна меншість, яку доводиться знищувати, сплачує невеликий штраф за хибне передбачення — тактів двадцять із гаком, — і двигун перезапускається.Протягом тридцяти років невимовним припущенням під усією цією конструкцією було те, що знищення — повне. Архітектурний стан відкочується, жоден регістр не оновлюється, жодне збереження не доходить до пам'яті, «усе так, наче нічого не було». Контракт ISA дотримано. Спекуляція для програмного забезпечення невидима.Це невимовне припущення виявилося хибним.Коли процесор спекулятивно виконує завантаження, це завантаження не просто обчислює значення в перейменований фізичний регістр — воно водночас затягує рядок кешу в L1. Коли процесор спекулятивно виконує перехід, цей перехід оновлює таблиці історії в передбачувачі переходів. Коли процесор спекулятивно йде таблицею сторінок, результат осідає в TLB. Архітектурний стан відкотити начисто можна. Мікроархітектурний — ні. Кеш і далі теплий. Передбачувач і далі пам'ятає. А ці сліди спостерігаються через таймінги.Перш ніж ми подивимося на Meltdown і Spectre впритул, ви маєте побачити побічний канал, який вони обидва експлуатують.Пам'ятаєте кеш із L11? Ми присвятили цілу лекцію його звеличенню. Кеші створюють ілюзію швидкої пам'яті, тримаючи нещодавно вжиті рядки близько до процесора. Влучання в L1 — це ~3–4 такти. Промах аж до DRAM — ~200–300 тактів. Ця прірва у два порядки величини — це і є різниця між плавним сучасним комп'ютером і цеглиною з відчуттям 1980-х.Ця сама прірва — і є витік. Скрізь, де в системі є вимірювана різниця між закешованим і незакешованим, у вас уже є однобітний канал. А з 256 різними адресами, які можна зондувати, у вас восьмибітний канал — один байт за одне вимірювання.Ось стандартний прийом. Зловмисник виділяє зондувальний масив із 256 слотів завбільшки з рядок кешу — назвімо його probe[0..255]. Кожен слот — це один рядок кешу (на x86 — 64 байти), щоб вони не перетиналися. Спочатку зловмисник витісняє всі 256 слотів із кешу — наприклад, інструкцією clflush на x86. Потім він якимось механізмом (до нього дійдемо за хвилину) змушує процесор жертви спекулятивно виконати щось на кшталт:secret_byte = &lt;якийсь байт, який зловмисник не повинен мати змоги прочитати&gt;
touch probe[secret_byte * 64] ; приносить ОДИН рядок зонда в кеш
&lt;спекуляція відкочена&gt;
Спекуляцію відкочено. Архітектурний стан чистий — жоден регістр не містить secret_byte. Але в кеші лишився один теплий рядок: той, що відповідає probe[secret_byte * 64]. Далі зловмисник вимірює час доступу до кожного з probe[0], probe[64], probe[128], ..., probe[255*64]. Двісті п'ятдесят п'ять з них — промахи, по ~200 тактів. Одне — влучання, ~3 такти. Індекс цього одного влучання й каже зловмиснику, яким був secret_byte.Один байт секрету за одне вимірювання. Саме вимірювання — кілька мікросекунд. Повторюйте на мегагерцових частотах. Те, що ми звеличували в L11, — швидкі кеші — і є витоком.<br><img alt="Чотирипанельний огляд атаки Spectre v1 через кешевий побічний канал. Панель 1: зловмисник витискає зондувальний масив на 256 записів, так що кожен рядок холодний. Панель 2: жертва спекулятивно звертається до probe[secret*4096], поки хибно передбачений перехід приховує читання за межами; відповідний рядок кешу стає теплим. Панель 3: спекуляцію відкочено — архітектурний стан чистий, але нагрітий рядок кешу залишається. Панель 4: зловмисник перечитує кожен запис зонда і вимірює час кожного доступу; єдиний швидкий доступ ідентифікує секретний байт." src="assets/l14-spectre-cache-sidechannel.svg" target="_self" style="width: 700px; max-width: 100%;">Це і є побічний канал. І Meltdown, і Spectre використовують його. Відрізняються вони лише тим, як саме обманюють процесор, щоб він спекулятивно виконав залежне від секрету завантаження.<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Meltdown_(security_vulnerability)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Meltdown_(security_vulnerability)" target="_self">Meltdown</a> — простіший і нищівніший з двох. Він націлений на конкретне реалізаційне рішення, яке Intel — і кілька ядер ARM — тихо ухвалили заради продуктивності.Коли програма в користувацькому режимі виконує завантаження з адреси ядра, процесор мусить зробити дві речі: (а) подати завантаження в кеш і (б) перевірити, чи завантаження дозволене бітом користувач/супервізор у таблиці сторінок. У строгій реалізації (б) відбувається перед (а). На вразливих процесорах (а) і (б) виконувалися паралельно, а перевірка дозволу завершувалася пізніше в конвеєрі як частина обробки винятків. Завантаженню спекулятивно дозволяли повернути своє значення в OoO-двигун; якщо перевірка дозволу зрештою провалювалася, на етапі завершення (ретайру) підіймався архітектурний виняток, а спекулятивне завантаження знищувалося.Між «значення повернулося» і «перевірка дозволу завершилася» спекулятивне значення ядра сидить у перейменованому фізичному регістрі, доступне залежним спекулятивним інструкціям. І ось зловмисник пише:mov al, byte [kernel_address] ; спекулятивно читає заборонений байт ядра
shl rax, 6 ; множення на розмір рядка кешу
mov rbx, [probe + rax] ; залежне від секрету завантаження у probe
Усі три рядки виконуються спекулятивно. Перший зрештою викличе помилку, але поки помилка підніметься, третій рядок уже встигне торкнутися probe[secret * 64], залишивши слід у кеші. Далі процесор наздоганяє, підіймає помилку, знищує все в архітектурному стані. Зловмисник обробляє помилку (або придушує її через TSX, або ховає за ще одним хибно передбаченим переходом) і потім зондує масив. Байт ядра відновлено.Підсумок: програма в користувацькому режимі могла читати довільну пам'ять ядра. На вразливих чипах Intel під Linux це означало всю фізичну пам'ять, бо Linux відображає всю фізичну оперативну пам'ять в адресний простір ядра. Паролі у вашому браузері, SSH-ключі в іншому процесі, будь-що в системі — усе читається з будь-якого непривілейованого процесу.Виправлення: KAISER, пізніше перейменований на KPTI (Kernel Page-Table Isolation — ізоляція таблиць сторінок ядра). Таблиці сторінок ядра повністю вилучають із відображення в користувацькому режимі. Поки виконується код користувача, пам'ять ядра не просто захищена — її взагалі немає у таблицях сторінок, тож спекуляції просто нічим спекулювати. Кожен системний виклик тепер мусить перемикати таблиці сторінок на вході й виході, а це коштує реальних тактів. Початкові звіти оцінювали уповільнення у 5–30 % на навантаженнях, інтенсивних за системними викликами; PCID (Process-Context Identifiers — ідентифікатори контексту процесу) на новіших чипах Intel істотно знизив цю цифру, але не до нуля. Апаратна помилка спекуляції, яку платять назавжди на рівні ОС.<br>Вразливі: «Майже всі процесори Intel з позачерговим виконанням від 1995 року», деякі ядра ARM Cortex (зокрема A75) та IBM POWER8/9 — див. офіційний опис на <a data-tooltip-position="top" aria-label="https://meltdownattack.com/" rel="noopener nofollow" class="external-link is-unresolved" href="https://meltdownattack.com/" target="_self">meltdownattack.com</a>. AMD виявилася здебільшого імунною, бо в неї перевірка дозволу відбувається до того, як завантаження повертає своє значення в спекулятивне вікно, а не після. Дрібна різниця в реалізації, зроблена з непов'язаних причин багато років тому, врятувала цілого виробника.<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Spectre_(security_vulnerability)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Spectre_(security_vulnerability)" target="_self">Spectre v1</a> тонший, загальніший і його важче виправити. Там, де Meltdown зловживав конкретним недоліком реалізації, Spectre v1 зловживає тим, що кожен сучасний процесор робить цілком навмисно: передбачувачем переходів.Погляньте на такий цілком непримітний і цілком коректний фрагмент коду, який міг би опинитися у парсері мережевих пакетів або JIT-компіляторі:if (x &lt; array1_size) { y = array2[ array1[x] * 4096 ];
}
Перевірка меж — ось вона, чорним по білому. Програміст зробив усе за підручником. Функція відмовляється читати array1[x], якщо x поза діапазоном. Що тут може піти не так?А ось що. Зловмисник багато разів у циклі викликає цю функцію з коректними значеннями x, навчаючи передбачувач переходів упевнено передбачати: «перевірка меж проходить, беремо тіло if». А потім викликає функцію зі зловмисним x — величезним значенням, далеко за кінцем array1, підібраним так, щоб array1 + x випадково вказував на секрет десь в іншому місці адресного простору жертви. Процесор вибирає перехід, передбачувач каже «узятий, як завжди» — і починається спекуляція:
Спекулятивно завантажити array1[x] — секретний байт за адресою array1 + x.
Спекулятивно використати його як індекс у array2, помноживши на 4096 (крок рядка кешу).
Спекулятивно завантажити array2[secret * 4096] і притягти один рядок кешу.
Перевірка меж резолвиться. І провалюється. Спекуляція знищується.
Архітектурний стан чистий. y ніколи не присвоєно. Але крок 3 залишив свій слід у кеші. Зловмисник зондує array2[0..255 * 4096] і відновлює секрет.Найстрашніше у Spectre v1 те, що код коректний. Програміст перевірив межі. Апаратура все одно порушила їх — спекулятивно, бо передбачувач переходів сказав, що перевірка зазвичай проходить. Немає жодного рядка коду, який був би помилковим. Немає жодного місця, яке можна просто «полагодити».Захисні заходи відповідно болючі: явні бар'єри спекуляції на кшталт lfence після кожної перевірки меж (дуже дорого), компіляторне укріплення спекуляції та допоміжні функції на кшталт array_index_nospec() у Linux, які маскують індекс так, що передбачувач уже не може повз нього спекулювати. Жоден із них не безплатний. І ніхто не вірить, що вони повні: у 2026 році Spectre v1 усе ще не «закрито». Гарячі шляхи в Linux, Chrome та ядрах ОС досі перевіряють на нього вручну.Якщо v1 зловживав передбачувачем напрямку (взятий/невзятий), то v2 зловживає передбачувачем цілі — буфером цілей переходів (BTB), який підказує процесору, куди йде непрямий перехід. Непрямі переходи скрізь: виклики віртуальних методів у C++, покажчики на функції, таблиці переходів, інструкція ret.Атака така. З підконтрольного зловмиснику процесу багаторазово виконуються непрямі переходи за адресами, які колідують у BTB з непрямими переходами жертви. BTB не вміє розрізняти два контексти — він індексується за частковими бітами PC. Зловмисник навчає його: «непрямий перехід за адресою X стрибає на гаджет Y». Потім керування отримує жертва. Непрямий перехід жертви за адресою X виконується; BTB видає отруєну ціль Y; процесор починає спекулятивно виконувати кілька інструкцій Y усередині адресного простору жертви. Y підібрано зловмисником як гаджет — коротку послідовність, яка вже є в коді жертви і яка завантажує секрет і використовує його як індекс у зондувальному масиві, — точно як у Spectre v1.Зрештою спекуляцію знищують, коли справжня непряма ціль резолвиться й не збігається. Але слід у кеші залишається, і зловмисник — що працює в іншому процесі чи навіть на іншій віртуальній машині на тому самому фізичному ядрі — його вимірює.Заходи, які з цього виросли, мають елегантну назву retpoline («поворотний трамплін») і належать Google. Ідея в тому, щоб замінити кожен непрямий перехід послідовністю з інструкцій call і ret, яку BTB уже не може осмислено отруїти, — бо спекулятивний шлях заганяється в нескінченний цикл, від якого передбачувач зрештою відходить. Linux, Windows і основні гіпервізори випустили retpoline-патчі протягом кількох місяців після розголошення. Втрата продуктивності повністю залежить від того, наскільки навантаження інтенсивне за непрямими переходами, — від непомітної до цілком відчутної.Відступіть на хвилину від номерів CVE і подивіться, що щойно сталося з усією індустрією.Упродовж п'ятдесяти років підручники з архітектури комп'ютерів описували процесор у термінах самого лише архітектурного стану. ISA була контрактом. Регістри, пам'ять, прапорці умов, лічильник команд — ось і вся машина. Все інше було «деталлю реалізації», про яку програмістові міркувати не треба було. Кеші існували, але «прозоро». Передбачувачі переходів існували, але впливали лише на продуктивність, не на коректність. Спекулятивне виконання існувало, але гарантовано було невидиме.Spectre й Meltdown — не помилки в чипах Intel, ARM чи IBM. Процесори робили рівно те, що наказувала їм ISA. На кожному завершенні (ретайрі) кожен архітектурний регістр, кожна комірка пам'яті, кожен прапорець умови були коректними. Апаратура виконала свій контракт. Проблемою був сам контракт. Контракт виявився неповним.Мікроархітектурний стан — частина машини. Він спостерігається через таймінги, він протікає крізь межі безпеки, і відтепер будь-який проєкт захищеної системи мусить трактувати його як видимий. Це не програмний патч, який ви випустили — і забули. Це постійна зміна в тому, що ми маємо на увазі, коли кажемо «процесор робить X». Ментальна модель обчислень, з якою ви прийшли на цю лекцію, — регістри, пам'ять, ISA, усе, — це модель архітектурного рівня. У справжньої машини під нею є ще цілий поверх, і цей поверх відповідає.Якщо з усієї лекції ви запам'ятаєте одну річ, нехай це буде ось що: архітектурний стан ≠ мікроархітектурний стан, і це не примітка. Це різниця між машиною, яку описує посібник, і машиною, якою ви насправді володієте.Заходи захисту коштують реальних грошей. KPTI від Meltdown коштує пропускної здатності системних викликів. Retpoline від Spectre v2 — продуктивності непрямих переходів. Укріплення від Spectre v1 — бар'єрів у гарячих шляхах. Браузери знизили роздільну здатність JavaScript-таймерів, щоб ускладнити вимірювання побічного каналу. Хмарні провайдери перебудували планування, щоб тримати взаємно недовірливих орендарів подалі від одного й того ж ядра. У нові процесори додали апаратні біти «Indirect Branch Restricted Speculation», «Single Thread Indirect Branch Predictors» і ще десятки так само іменованих абревіатур. Сукупна надмірна вартість, за повідомленнями, коливається залежно від навантаження від кількох відсотків до 30 %. Частину за роки відвоювали; частину вже ніколи не повернути.Це і є податок на спекуляцію: постійна повторювана плата за оптимізацію, яка визначила останні три десятиліття продуктивності процесорів. Кожне сучасне ядро ОС і кожен сучасний вебоглядач постачаються з бар'єрами спекуляції в гарячих шляхах. Швидке ядро й далі швидке. Просто воно трохи менш швидке, ніж було, — і це назавжди, бо тепер нам доводиться припускати, що кеш, передбачувач і TLB — усі стежать.
Spectre й Meltdown не були помилками в Intel чи ARM. Це була коректна поведінка неповної абстракції.
Позачергове виконання зі спекуляцією дає нам надзвичайну однопотокову продуктивність — за ціну, яку ми тепер розуміємо: податок на безпеку, який повернути повністю вже не вийде, сплачуваний кожним навантаженням, що працює на сучасному процесорі. Отже, ми вичавили з одного потоку на одному ядрі стільки паралелізму, скільки фізика дозволила.А що, якби спробувати запустити два потоки на тому самому ядрі одночасно, розділивши між ними той самий величезний OoO-двигун, щоб його дороге обладнання працювало активніше? Це називається одночасною багатопотоковістю, і їй присвячено §8.Ось вам незручна статистика. Те 6-широке ядро Apple M1 з ROB на 630 записів із §6.5 на реальному однопотоковому коді рідко досягає IPC понад 2. Апаратура теоретично здатна завершувати до 8 інструкцій за такт, але ваша реальна програма отримує 1–2. Решта бекенду простоює й чекає.Чому? З тих самих причин, про які попереджали §3–§7. Один промах L3 — це 200+ тактів, протягом яких блок завантаження/збереження не робить нічого корисного. Хибне передбачення переходу скидає десятки µop у польоті. Ланцюги залежностей на кшталт нашого акумулятора ebx серіалізують АЛП. Навіть з ідеальним позачерговим виконанням і величезними спекулятивними вікнами один потік просто не здатен більшість часу тримати всі ті виконавчі порти зайнятими. Виконавчий бекенд для окремо взятого потоку надлишковий.І це не баг, а суть побудови 6-широкого ядра. Ви заплатили за ці порти, бо іноді виникає сплеск незалежної роботи, який хочеться закінчити швидко. Але «іноді» означає «решту часу простоюють». Тож запитання просте: якщо один потік залишає виконавчі блоки в простої, то що, коли б другий потік міг задіяти їх у тому ж такті — не пізніше, а одночасно, заповнюючи прогалини?Оце і є одночасна багатопотоковість (simultaneous multithreading, SMT).L09 уже представила SMT на рівні таксономії потоків — кілька потоків інструкцій, що ділять один конвеєр. Цей розділ — про те, що воно насправді означає всередині кремнію.<br>Ранні схеми багатопотоковості перемикали потоки на затримках. Грубозерниста багатопотоковість перемикається на інший потік, коли поточний натрапляє на подію з великою затримкою (промах кешу). Дрібнозерниста багатопотоковість проходить по потоках за круговим принципом, по одному щотакту. Обидві все одно виконують лише один потік за раз на кожній заданій стадії конвеєра. SMT ламає це правило: інструкції з кількох потоків можуть одночасно займати бекенд в одному й тому самому такті. В одному такті можуть видаватися дві µop з потоку A на цілочисельні АЛП, одночасно з завантаженням потоку B і операцією з плаваючою комою потоку A. <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Simultaneous_multithreading" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Simultaneous_multithreading" target="_self">Стаття Wikipedia про SMT</a> формулює це чітко: при темпоральній багатопотоковості «на будь-якій заданій стадії конвеєра одночасно може виконуватися лише один потік інструкцій», тоді як при SMT «на будь-якій заданій стадії конвеєра одночасно можуть виконуватися інструкції з кількох потоків».Апаратна ціна звучить серйозно, але насправді — ні. У вас уже є станції резервування, CDB, ROB, АЛП, блоки завантаження/збереження. SMT усе це повторно використовує. Усе, що треба додати, — це достатньо бухгалтерії, щоб тримати архітектурні стани двох потоків окремо.Ось ключовий поділ. Запам'ятайте цю таблицю — у ній вся ідея реалізації SMT.<br><img alt="Діаграма ресурсів одночасної багатопотоковості. Два стовпці приватних даних потоку вгорі показують стан, який дублюється для кожного потоку — лічильник команд, архітектурний регістровий файл, регістри стеку/сегментів — близько 5% площі ядра. Помаранчеві стрілки об'єднують обидва потоки в єдиний спільний виконавчий бекенд внизу: станції резервування, буфер переупорядкування, функціональні блоки, кеші L1/L2, передбачувач переходів та TLB. Обидва потоки конкурують за кожен ресурс у бекенді." src="assets/l14-smt-sharing.svg" target="_self" style="width: 700px; max-width: 100%;">Операційна система бачить два незалежні процесори. Вона планує на них два потоки, надсилає два переривання, тримає два набори регістрів. Але всередині кремнію ці два «процесори» б'ються за єдиний пул виконавчих ресурсів, — а позачерговому планувальнику ядра не дуже важливо, з якого потоку прийшла µop, аби лише її операнди були готові.<br>Hyper-Threading Technology (HTT) — торгова назва Intel для 2-way SMT на x86. Intel уперше впровадила її на серверних чипах Xeon у лютому 2002 року і розширила на настільну лінійку Pentium 4 у листопаді 2002 року — див. <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Hyper-threading" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Hyper-threading" target="_self">Hyper-threading у Wikipedia</a>. Кожне фізичне ядро перетворюється на два логічні процесори. У Linux ви бачите це в /proc/cpuinfo, у Windows — у кількості «Logical processors» у Task Manager.Із самого запуску презентаційні цифри Intel виглядали вражаюче: HTT додавала лише близько 5 % площі кристала, даючи при цьому 15–30 % приросту пропускної здатності на типових серверних навантаженнях. Саме це співвідношення — кілька відсотків кремнію за десятки відсотків додаткової роботи — і закріпило HTT. Порівняно з альтернативою — додати друге фізичне ядро, що приблизно подвоює площу заради 80–90 % приросту пропускної здатності, — SMT купує пропускну здатність значно дешевше. Просто платить за неї іншою валютою — передбачуваністю на один потік.AMD підхопила 2-way SMT у Zen (2017) і зберігає в кожному наступному поколінні Zen. Лінійка IBM POWER пішла далі й у POWER8 та наступниках підтримує аж 8-way SMT на одне ядро. x86 зупинилася на двох і далі не рухалася. Чому? Спадна віддача. Коли вже втиснуто два потоки, більшість вільної смуги виконання задіяна; третій потік рідко знаходить вільні порти, а пробуксовування кешу, яке він спричиняє, перекриває будь-який приріст пропускної здатності. AMD коротко експериментувала з 4-way SMT в епоху Bulldozer і відступила.SMT — це не безплатна пропускна здатність. Два потоки, що ділять один кеш даних L1, здатні витісняти робочі набори один одного кожні кілька тисяч тактів — це явище називають пробуксовуванням кешу. Два потоки на одному передбачувачі переходів засмічують одне одному таблиці історії. Два потоки на спільній смузі вибірки крадуть одне в одного такти на фронтенді. У результаті затримка на окремий потік погіршується, навіть якщо сукупна пропускна здатність зростає.Для навантажень, де цей компроміс грає в мінус, HT зазвичай вимикають зовсім:
HPC / наукові обчислення: кожен потік у добре спланованому коді і так насичує АЛП. Другий логічний процесор лише створює конкуренцію за кеш. Більшість HPC-кластерів працюють з вимкненим HT.
Корпоративні бази даних: важливіша хвостова затримка (найповільніший 1 % запитів), ніж середня пропускна здатність. HT цей хвіст лише погіршує. Розгортання Oracle та SQL Server його, як правило, вимикають.
Системи реального часу, чутливі до затримок: непередбачувана конкуренція — метрика, на яку просто не можна піти.
Ціноутворення хмарних ВМ іноді розрізняє «фізичні ядра» і «vCPU» саме тому, що vCPU — це зазвичай лише один SMT-потік, який не дає продуктивності повноцінного ядра.
У 2006 році ARM публічно критикувала SMT за споживання «аж на 46 % більшої енергії, ніж у звичайних двоядерних конструкціях», при одночасному зростанні пробуксовування кешу на 42 %, — це одна з причин, чому мобільні ARM-ядра роками лишалися однопотоковими. Сама Intel у 2013 році прибрала HT з низькоспоживних ядер Silvermont Atom, дійшовши висновку, що на невеликих позачергових ядрах ширший OoO — краще вкладення транзисторів, ніж SMT.Розділ 7 навчив вас, що мікроархітектурний стан — спільний, а спільний стан через межу безпеки витікає. SMT робить це попередження конкретним у новому масштабі: два SMT-сусіди ділять усе, що має бекенд — виконавчі блоки, кеші, TLB, передбачувач переходів, навіть черги завантаження/збереження. Шкідливий потік, що працює на логічному процесорі 0, може спостерігати, впливати й вимірювати час виконання потоку-жертви на логічному процесорі 1 — у межах того самого фізичного ядра, без перемикання контексту й без перетину межі ядра ОС.Атаки не теоретичні. У 2005 році Колін Персіваль продемонстрував на HTT часовий побічний канал, який витягав приватні RSA-ключі з сусіднього потоку, стежачи за поведінкою кешу L1. У 2018 році Foreshadow / L1TF перетворив спільний для SMT кеш даних L1 на витік класу Spectre. У 2019 році атаки MDS (Microarchitectural Data Sampling — вибірка мікроархітектурних даних) на кшталт RIDL і Fallout показали, що внутрішні буфери, спільні між сусідами, зливають дані між потоками. У 2018 році OpenBSD вимкнула hyper-threading за замовчуванням, і кілька хмарних провайдерів тепер або вимикають SMT на машинах для орендарів, або не допускають спільного планування потоків різних клієнтів на одному фізичному ядрі.Мораль та сама, що й у §7, лише в укрупненому масштабі: SMT оптимізує використання ресурсів тим, що усуває ізоляцію між потоками, які працюють одночасно. Ціна цієї оптимізації в тому, що «працюють одночасно» тут значить і «витікають одночасно».Відступімо на крок. §1–§7 будували дедалі агресивнішу машину, щоб витягнути паралелізм з одного потоку інструкцій: конвеєризація, випередження, передбачення, розширення, переупорядкування, спекуляція. Кожен прийом намагався знайти паралелізм, що вже приховано в коді. §8 — це визнання того, що в тому, що містить один потік, є стеля, а за цією стелею єдиний спосіб тримати бекенд зайнятим — це імпортувати паралелізм звідки-інде — з іншого потоку, який ОС, на щастя, тримає напоготові.ILP знаходить паралелізм усередині одного потоку. TLP через SMT імпортує паралелізм з іншого потоку, коли одного замало. І те, й інше годує той самий голодний виконавчий бекенд. Це речення варто запам'ятати.Тепер ви по черзі побачили кожен великий прийом, який індустрія винайшла для витягування більшої роботи з одного фізичного ядра за такт: конвеєризацію, випередження, передбачення переходів, суперскалярну видачу, позачергове виконання, перейменування регістрів, буфер переупорядкування, спекулятивне виконання і SMT. Кожен із них розв'язував вузьке місце, створене попереднім. Кожен робив машину швидшою, не роблячи її концептуально іншою з погляду програміста: архітектурний стан (§6) залишався звичайним послідовним виконанням x86-інструкцій.То чому ж ваш ноутбук 2024 року працює приблизно на тій самій тактовій частоті, що й настільний ПК 2008 року? Чому «свято» закінчилося? У відповіді фігурують закон Амдала (не можна прискорити те, що не розпаралелене), стіна потужності (тактова частота × напруга² спалює кристал), темний кремній (транзисторів у вас більше, ніж ви можете собі дозволити вмикати одночасно) і тепловий бюджет корпусу ноутбука. Це L15. А поки що просто зафіксуйте: ми дійшли до кінця розділу — уся історія цієї лекції, від 5-стадійного конвеєра до SMT, відбувалася всередині одного ядра. Наступного разу ми запитаємо, чому нам довелося перестати робити одне ядро швидшим і почати робити їх багато.
Конвеєризація обмінює затримку на пропускну здатність. Одна інструкція все ще займає N тактів, але нова завершується щотакту — тобто приблизно N-кратне прискорення, якщо нічого не йде не так. А «нічого не йде не так» — це саме те місце, де живуть конфлікти.
Кожен прийом ILP розв'язує проблему, створену попереднім. Конвеєризація створює конфлікти → випередження та передбачення переходів їх здешевлюють → суперскалярність розширює трубу, але впирається в ланцюги залежностей → позачергове виконання розриває ці ланцюги, але ламає точні переривання → буфер переупорядкування їх відновлює → спекуляція тримає конвеєр заповненим поза переходами → Spectre/Meltdown перетворюють спекуляцію на зброю.
Для ILP динамічне перемагає статичне. Ставка Itanium на EPIC — що паралелізм витягне компілятор — не витримала непередбачуваних промахів кешу та переходів. Апаратне OoO планує під час виконання за реальними затримками, а не під найгірший статичний сценарій.
Apple M1 — це приклад того, як підручниковий OoO виглядає у промисловому масштабі. 8-широке декодування, ROB на ~630 записів, ~350 фізичних регістрів — ті самі станції резервування та CDB з Томасуло 1967 року, тільки значно більші. ISA з фіксованою шириною в ARM зробила це масштабування доступним; x86 платить податок на ширину декодування за свою спадщину.
Архітектурний стан ≠ мікроархітектурний стан. Найважливіша ідея цієї лекції. П'ятдесят років ми описували процесори лише через їхні регістри та пам'ять. Spectre й Meltdown довели, що кеші, передбачувачі й TLB — теж частина машини, спостережувана через таймінги, а отже, і частина моделі безпеки будь-якої системи, що виконує недовірений код.
SMT купує дешеву пропускну здатність. ~5 % площі за ~15–30 % пропускної здатності — ціною того, що два потоки ділять єдиний OoO-бекенд. Ціна: конкуренція за кеш і свіжий клас побічних каналів усередині одного ядра.
Обов'язкове читання:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Instruction_pipelining" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Instruction_pipelining" target="_self">Instruction pipelining</a> — §Overview, §Hazards
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Tomasulo%27s_algorithm" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Tomasulo%27s_algorithm" target="_self">Tomasulo's algorithm</a> — §Algorithm, §Implementation
<br>Стартові сторінки <a data-tooltip-position="top" aria-label="https://spectreattack.com/" rel="noopener nofollow" class="external-link is-unresolved" href="https://spectreattack.com/" target="_self">Spectre</a> і <a data-tooltip-position="top" aria-label="https://meltdownattack.com/" rel="noopener nofollow" class="external-link is-unresolved" href="https://meltdownattack.com/" target="_self">Meltdown</a>
Паттерсон і Геннессі, Computer Organization and Design (Архітектура комп'ютерних систем: інтерфейс апаратного та програмного забезпечення), розділ 4 (Конвеєризація) — для механічної побіглості
Рекомендоване для перегляду:
Будь-який сучасний глибокий розбір архітектури процесора Apple M1 або AMD Zen — часто ставте на паузу й намагайтеся впізнати компоненти з §6.
Що ви робитимете:
Пропрацюйте наскрізний приклад на папері на кожному рівні (послідовний → конвеєризований → з випередженням → суперскалярний → OoO). Намалюйте просторово-часову сітку вручну. Це найкорисніша навчальна вправа до цієї лекції.
Прочитайте невелику функцію на асемблері x86-64 і визначте, де вона застрягатиме на 5-стадійному конвеєрі з виконанням за порядком. Які RAW вимагають випередження? Які виживуть як 1-тактові бульбашки? Де сидить перший перехід?
Бонус: скомпілюйте просту C-функцію з -O0 і -O3. Подивіться на згенерований асемблер. Чи бачите ви, як компілятор переставляє інструкції, щоб приховати затримки використання після завантаження? <br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Instruction_pipelining" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Instruction_pipelining" target="_self">Instruction pipelining</a> — канонічна стаття Wikipedia; охоплює стадії, конфлікти та обробку винятків.
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Hazard_(computer_architecture)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Hazard_(computer_architecture)" target="_self">Hazard (computer architecture)</a> — структурні, даних (RAW, WAR, WAW) та керування конфлікти.
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Branch_predictor" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Branch_predictor" target="_self">Branch predictor</a> — від однобітних до 2-бітних насичувальних, дворівневих адаптивних, TAGE та нейронних передбачувачів.
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Superscalar_processor" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Superscalar_processor" target="_self">Superscalar processor</a> — архітектури з множинною видачею та їхні межі.
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Tomasulo%27s_algorithm" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Tomasulo%27s_algorithm" target="_self">Tomasulo's algorithm</a> — алгоритм IBM 360/91 1967 року, який визначає сучасний OoO.
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Reorder_buffer" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Reorder_buffer" target="_self">Reorder buffer</a> — механізм впорядкованого завершення, що зберігає точні переривання.
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Speculative_execution" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Speculative_execution" target="_self">Speculative execution</a> — оптимізація, що уможливлює сучасний ILP, і клас уразливостей, який вона створила.
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Spectre_(security_vulnerability)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Spectre_(security_vulnerability)" target="_self">Spectre (security vulnerability)</a> — варіанти обходу перевірки меж та впровадження цілі переходу.
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Meltdown_(security_vulnerability)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Meltdown_(security_vulnerability)" target="_self">Meltdown (security vulnerability)</a> — гонка перевірки привілеїв та відповідь KAISER/KPTI.
<br><a data-tooltip-position="top" aria-label="https://meltdownattack.com/" rel="noopener nofollow" class="external-link is-unresolved" href="https://meltdownattack.com/" target="_self">Meltdown and Spectre</a> — офіційна стартова сторінка з академічними статтями.
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Simultaneous_multithreading" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Simultaneous_multithreading" target="_self">Simultaneous multithreading</a> — модель SMT та її історичні реалізації.
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Hyper-threading" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Hyper-threading" target="_self">Hyper-threading</a> — комерційне 2-way SMT від Intel.
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Apple_M1" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Apple_M1" target="_self">Apple M1</a> — заголовні цифри для кейсу з §6.5.
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Itanium" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Itanium" target="_self">Itanium</a> — архітектура EPIC та її комерційна невдача.
Паттерсон Д. А. і Геннессі Дж. Л. Computer Organization and Design: The Hardware/Software Interface (Архітектура комп'ютерних систем: інтерфейс апаратного та програмного забезпечення). Morgan Kaufmann, 6-те вид., 2020. Розділи 4-6.
Геннессі Дж. Л. і Паттерсон Д. А. Computer Architecture: A Quantitative Approach (Архітектура комп'ютерів: кількісний підхід). Morgan Kaufmann, 6-те вид., 2017. Розділ 3 (Паралелізм рівня інструкцій).
]]></description><link>lectures/l14.-паралелізм-на-рівні-інструкцій-та-потоків.html</link><guid isPermaLink="false">Lectures/L14. Паралелізм на рівні інструкцій та потоків.md</guid><pubDate>Tue, 14 Apr 2026 18:14:09 GMT</pubDate><enclosure url="assets/l14-running-example-sequential.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-running-example-sequential.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Lab14]]></title><description><![CDATA[
Курс: Архітектура обчислювальних систем
Тиждень: 14
Тривалість: 1 год 20 хв (аудиторна) + домашнє завдання
Тип: Практична робота під наглядом інструктора
Після завершення практики студент зможе:
Пояснити, як одна SIMD-інструкція виконує ту саму операцію над 8 числами одночасно.
Написати мовою NASM кернел, який використовує 256-бітні YMM-регістри та AVX2-інструкції vmovaps, vaddps, vmulps, vfmadd231ps.
Виконати горизонтальну редукцію YMM-регістра до скалярної суми.
Виміряти кількість тактів CPU за допомогою rdtsc та порівняти дві реалізації одного алгоритму.
Продемонструвати експериментально, як ланцюг залежностей за даними обмежує пропускну здатність виконавчих блоків, та як розбиття на незалежні акумулятори знімає це обмеження.
Реалізувати SAXPY (y[i] = a*x[i] + y[i]) через AVX2 (домашнє завдання).
Перегляньте лекційні матеріали за Тиждень 14 (Паралелізм на рівні інструкцій та потоків). Зверніть увагу на розділи:
Суперскалярне виконання — чому CPU може запускати кілька FMA за такт, але не може запустити залежні FMA паралельно
Конфлікти даних (RAW) — чому x = x + ... створює ланцюг, який неможливо переупорядкувати
Випередження та позачергове виконання — OoO-ядро хоче виконати інструкції паралельно, але може лише якщо вони незалежні Сьогоднішня центральна ідея: SIMD (широкі смуги) і ILP (незалежні операції) — це дві різні форми паралелізму. Перша дає вам 8× за такт із однієї інструкції. Друга — ще 2–4× від того, що ядро може виконувати кілька SIMD-інструкцій одночасно. Обидві треба спеціально дістати; жодна не приходить безкоштовно.
AVX2 додає 16 регістрів по 256 біт: YMM0–YMM15. Молодші 128 біт кожного YMM — це вже знайомий вам XMM0–XMM15 (SSE). 255 0
┌───────────────────────────────────────────────────────────────┐
│ YMM0 (256 біт) │
│ ┌─────────────────────────────────────┤
│ │ XMM0 (128 біт) │
└──────────────────────────┴─────────────────────────────────────┘
Одне 256-бітне значення можна інтерпретувати як:
8 × float32 (single precision) — vmulps, vaddps, vfmadd231ps
4 × float64 (double precision) — vmulpd, vaddpd, ...
8 × int32, 16 × int16, 32 × int8 — цілочисельні варіанти
У цій практиці ми працюємо лише з 8 × float32.
vfmadd231ps — чому «231»? Це кодування каже, куди піде результат і де зберігається акумулятор. 231 означає: operand1 = operand2 * operand3 + operand1. Тобто перший операнд — одночасно джерело акумулятора та місце результату. Саме це потрібно для sum += a[i] * b[i].
Інструкція vmovaps кидає #GP (segfault), якщо адреса не кратна 32. У NASM вирівняти буфер у .data або .bss:section .data align 32 my_array: dd 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0
Альтернатива — vmovups (unaligned), який працює з будь-якою адресою, але на старих CPU повільніший. На сучасних Intel/AMD різниці майже немає, проте ми будемо явно вирівнювати — це дисциплінує та робить очевидним намір.Після циклу, коли у ymm0 лежать 8 часткових сум, потрібно додати їх між собою. Стандартний шаблон (4 інструкції):; Вхід: ymm0 = [s0, s1, s2, s3, s4, s5, s6, s7]
; Вихід: xmm0[0] = s0+s1+s2+s3+s4+s5+s6+s7 vextractf128 xmm1, ymm0, 1 ; xmm1 = [s4, s5, s6, s7]
vaddps xmm0, xmm0, xmm1 ; xmm0 = [s0+s4, s1+s5, s2+s6, s3+s7]
vhaddps xmm0, xmm0, xmm0 ; xmm0 = [(s0+s4)+(s1+s5), (s2+s6)+(s3+s7), ...]
vhaddps xmm0, xmm0, xmm0 ; xmm0[0] = повна сума
Після цього vmovd eax, xmm0 дає вам біти float-результату як ціле число — саме в такому вигляді, в якому його виводить print_hex32.Розглянемо дві версії того самого скалярного добутку (sum += a[i]*b[i], 8 ітерацій):Версія A — один акумулятор:ymm0 = 0
ymm0 += a[0..7] * b[0..7] ← залежить від попередньої ymm0
ymm0 += a[8..15] * b[8..15] ← залежить від попередньої ymm0
... (ще 6 разів)
На сучасному ядрі vfmadd231ps має латентність ~4 такти та пропускну здатність 2 за такт. Але у версії A кожна наступна FMA чекає завершення попередньої, бо всі пишуть у той самий ymm0. 8 FMA × 4 такти = 32 такти на ітерацію, і друга FMA-трубка простоює.Версія B — чотири незалежні акумулятори:ymm0 = ymm1 = ymm2 = ymm3 = 0
ymm0 += a[0..7] * b[0..7]
ymm1 += a[8..15] * b[8..15]
ymm2 += a[16..23] * b[16..23]
ymm3 += a[24..31] * b[24..31]
ymm0 += a[32..39] * b[32..39] ← знову пише у ymm0 (4 такти вже минули)
...
; у кінці: ymm0 = ymm0+ymm1+ymm2+ymm3
Тепер сусідні FMA незалежні, ядро може стартувати до двох за такт. Критичний шлях: 2 FMA у ланцюжку кожного акумулятора × 4 такти = 8 тактів на ітерацію (або навіть менше, якщо порти не насичені).Очікуваний ефект: версія B у 3–5 разів швидша за версію A при тій самій кількості арифметичних операцій. Це ILP у чистому вигляді — саме те, про що була Лекція 14, але тепер ви виміряєте це власноруч.Інструкція rdtsc кладе 64-бітний лічильник тактів CPU у пару EDX:EAX. Щоб отримати єдине 64-бітне значення:lfence ; серіалізація — чекаємо завершення попередніх інструкцій
rdtsc ; EDX:EAX = лічильник тактів
shl rdx, 32
or rax, rdx ; RAX = 64-бітний лічильник
lfence перед rdtsc не дає OoO-ядру виконати вимірювання раніше реальної роботи. Для нашого лабораторного вимірювання цього достатньо — промислові бенчмарки використовують rdtscp + cpuid, але різниця у 20–50 тактів не впливає на наш результат у ~100 мільйонів тактів.Відкрийте термінал у вашому Codespace (з Lab 11 або новий) та виконайте:grep -Eo 'avx2|fma' /proc/cpuinfo | sort -u
Ви маєте побачити обидва рядки:avx2
fma Якщо якогось із них немає — ваш Codespace отримав CPU без AVX2/FMA (рідко, але трапляється). Зупиніть Codespace і створіть новий: Ctrl+Shift+P → Codespaces: Stop Current Codespace, потім на github.com видаліть цей Codespace і створіть ще один. Майже всі хости GitHub підтримують AVX2 з 2020 року.
mkdir -p Lab14 &amp;&amp; cd Lab14
NASM має бути вже встановлений із Lab 11. Якщо ні:sudo apt-get install -y nasm
Створіть файл simd_utils.inc. Не змінюйте цей файл — він повторює fp_utils.inc з Lab 13 і додає одну нову функцію print_ymm_buf, яка виводить 8 floats із пам'яті як 8 hex-значень.; ==============================================================
; simd_utils.inc — Допоміжні функції для SIMD-практики
;
; Підключення: %include "simd_utils.inc" (в кінці .asm файлу)
;
; Усі функції ЗБЕРІГАЮТЬ значення всіх регістрів
; (включно зі стандартними; YMM не чіпаємо).
; ============================================================== section .bss _pu_char_buf resb 1 _pu_uint_buf resb 12 _pu_hex_buf resb 8 section .data _pu_hex_prefix db "0x" section .text ; --- print_char: вивести символ з DIL ---
print_char: push rax push rcx push rdx push rsi push rdi push r11 mov byte [_pu_char_buf], dil mov rax, 1 mov rdi, 1 lea rsi, [_pu_char_buf] mov rdx, 1 syscall pop r11 pop rdi pop rsi pop rdx pop rcx pop rax ret ; --- print_newline: вивести '\n' ---
print_newline: push rdi mov dil, 10 call print_char pop rdi ret ; --- print_string: RSI=адреса, RDX=довжина ---
print_string: push rax push rcx push rdi push r11 mov rax, 1 mov rdi, 1 syscall pop r11 pop rdi pop rcx pop rax ret ; --- print_uint: вивести беззнакове число з EDI ---
print_uint: push rax push rbx push rcx push rdx push rsi push rdi push r11 mov eax, edi lea rsi, [_pu_uint_buf + 11] xor ecx, ecx mov ebx, 10 test eax, eax jnz .pu_loop dec rsi mov byte [rsi], '0' inc ecx jmp .pu_write
.pu_loop: xor edx, edx div ebx add dl, '0' dec rsi mov [rsi], dl inc ecx test eax, eax jnz .pu_loop
.pu_write: mov rdx, rcx mov rax, 1 mov rdi, 1 syscall pop r11 pop rdi pop rsi pop rdx pop rcx pop rbx pop rax ret ; --- print_uint64: вивести беззнакове 64-бітне число з RDI ---
print_uint64: push rax push rbx push rcx push rdx push rsi push rdi push r11 mov rax, rdi lea rsi, [_pu_uint_buf + 11] xor ecx, ecx mov ebx, 10 test rax, rax jnz .pu64_loop dec rsi mov byte [rsi], '0' inc ecx jmp .pu64_write
.pu64_loop: xor edx, edx div rbx add dl, '0' dec rsi mov [rsi], dl inc ecx test rax, rax jnz .pu64_loop
.pu64_write: mov rdx, rcx mov rax, 1 mov rdi, 1 syscall pop r11 pop rdi pop rsi pop rdx pop rcx pop rbx pop rax ret ; --- print_hex32: вивести 32-бітне значення з EDI як 0xHHHHHHHH ---
print_hex32: push rax push rbx push rcx push rdx push rsi push rdi push r11 mov ebx, edi mov rax, 1 mov rdi, 1 lea rsi, [_pu_hex_prefix] mov rdx, 2 syscall mov ecx, 8 lea rsi, [_pu_hex_buf]
.ph_loop: rol ebx, 4 mov eax, ebx and eax, 0xF cmp al, 10 jl .ph_digit add al, ('A' - 10) jmp .ph_store
.ph_digit: add al, '0'
.ph_store: mov [rsi], al inc rsi dec ecx jnz .ph_loop lea rsi, [_pu_hex_buf] mov rdx, 8 mov rax, 1 mov rdi, 1 syscall pop r11 pop rdi pop rsi pop rdx pop rcx pop rbx pop rax ret ; --- print_ymm_buf: вивести 8 float значень із пам'яті як 8 hex32 ---
; Вхід: RDI = адреса 32 байт (8 × 4-байтних float)
; Вивід: "0xHHHHHHHH 0xHHHHHHHH ... 0xHHHHHHHH\n"
print_ymm_buf: push rax push rbx push rcx push rdx push rsi push rdi push r11 push r12 push r13 mov r12, rdi ; зберегти адресу буфера mov r13d, 8 ; лічильник: 8 floats
.pyb_loop: mov edi, [r12] call print_hex32 add r12, 4 dec r13d jz .pyb_done mov dil, ' ' call print_char jmp .pyb_loop
.pyb_done: call print_newline pop r13 pop r12 pop r11 pop rdi pop rsi pop rdx pop rcx pop rbx pop rax ret
Створіть мінімальний тест test_utils.asm:default rel section .data align 32 probe: dd 0x3F800000, 0x40000000, 0x40400000, 0x40800000 dd 0x40A00000, 0x40C00000, 0x40E00000, 0x41000000 section .text global _start _start: lea rdi, [probe] call print_ymm_buf mov rax, 60 xor rdi, rdi syscall %include "simd_utils.inc"
Зберіть та запустіть:nasm -f elf64 -g test_utils.asm -o test_utils.o &amp;&amp; ld test_utils.o -o test_utils &amp;&amp; ./test_utils
Ви маєте побачити:0x3F800000 0x40000000 0x40400000 0x40800000 0x40A00000 0x40C00000 0x40E00000 0x41000000
Це hex-представлення чисел 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0. Перевірте на <a data-tooltip-position="top" aria-label="https://float.exposed/" rel="noopener nofollow" class="external-link is-unresolved" href="https://float.exposed/" target="_self">float.exposed</a>, якщо сумніваєтесь.
Якщо все працює — видаліть test_utils.asm і test_utils.o.
Напишіть програму, яка додає два масиви по 8 float32 за допомогою однієї інструкції vaddps. Програма виводить обидва входи та результат, щоб ви могли перевірити hex-значення вручну.Створіть файл vadd8.asm:; vadd8.asm — Векторне додавання 8 float за одну SIMD-інструкцію
;
; Збірка: nasm -f elf64 -g vadd8.asm -o vadd8.o &amp;&amp; ld vadd8.o -o vadd8
; Запуск: ./vadd8 default rel section .data ; Два вирівняні масиви по 8 float32 align 32 a: dd 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0 align 32 b: dd 10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0 str_a db "a = " str_a_len equ $ - str_a str_b db "b = " str_b_len equ $ - str_b str_c db "c = " str_c_len equ $ - str_c section .bss align 32 c: resd 8 ; місце для результату, 8 × float32 section .text global _start _start: ; --- Вивести "a = " + масив a --- lea rsi, [str_a] mov rdx, str_a_len call print_string lea rdi, [a] call print_ymm_buf ; --- Вивести "b = " + масив b --- lea rsi, [str_b] mov rdx, str_b_len call print_string lea rdi, [b] call print_ymm_buf ; ============================================ ; ПРИКЛАД (надано): Завантажити масив a у YMM0 ; ; vmovaps вимагає, щоб адреса була вирівняна на 32 байти. ; Ми забезпечили це директивою `align 32` у .data. ; ; STATE після цієї інструкції: ; ymm0 = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0] ; ============================================ vmovaps ymm0, [a] ; ════════════════════════════════════════════════════════════ ; ВАЖЛИВО: перед тим, як писати код для TODO 1/2/3, заповніть ; STATE-коментарі нижче. Це симуляція виконання у вашій голові. ; Не дивіться на вивід програми — ви ЩЕ не запустили її. ; Якщо не можете вгадати STATE — перечитайте розділ "AVX2: ; широкі регістри та 8 смуг" у Теоретичному мінімумі. ; ════════════════════════════════════════════════════════════ ; ============================================ ; TODO 1: Завантажити масив b у YMM1 ; ; Використайте ту саму інструкцію vmovaps. ; ; STATE після вашої інструкції (заповніть числами): ; ymm1 = [?, ?, ?, ?, ?, ?, ?, ?] ; ============================================ ; ============================================ ; TODO 2: Додати YMM0 + YMM1, результат у YMM2 ; ; Інструкція: vaddps ymm_dst, ymm_src1, ymm_src2 ; Одна інструкція — 8 паралельних додавань. ; ; STATE після вашої інструкції (заповніть числами): ; ymm2 = [?, ?, ?, ?, ?, ?, ?, ?] ; ============================================ ; ============================================ ; TODO 3: Зберегти YMM2 у буфер c ; ; Інструкція: vmovaps [addr], ymm ; Буфер c вирівняний (див. .bss). ; ; STATE після вашої інструкції (заповніть, що лежить ; у пам'яті за адресою c): ; c[0..7] = [?, ?, ?, ?, ?, ?, ?, ?] ; ============================================ ; --- Вивести "c = " + масив c --- lea rsi, [str_c] mov rdx, str_c_len call print_string lea rdi, [c] call print_ymm_buf ; Вихід mov rax, 60 xor rdi, rdi syscall %include "simd_utils.inc"
Зберіть та запустіть:nasm -f elf64 -g vadd8.asm -o vadd8.o &amp;&amp; ld vadd8.o -o vadd8
./vadd8
a = 0x3F800000 0x40000000 0x40400000 0x40800000 0x40A00000 0x40C00000 0x40E00000 0x41000000
b = 0x41200000 0x41A00000 0x41F00000 0x42200000 0x42480000 0x42700000 0x428C0000 0x42A00000
c = 0x41300000 0x41B00000 0x42040000 0x42300000 0x425C0000 0x42840000 0x429A0000 0x42B00000
<br>Відкрийте <a data-tooltip-position="top" aria-label="https://float.exposed/" rel="noopener nofollow" class="external-link is-unresolved" href="https://float.exposed/" target="_self">float.exposed</a> і введіть кілька hex-значень із рядка c:Запишіть у коментарях у вашому vadd8.asm:
Скільки скалярних інструкцій addss знадобилося б для того самого результату?
Чому ми не писали цикл?
Перевірте, чи ваші STATE-коментарі вище (після TODO 1/2/3) збігаються з реальним виводом програми. Якщо десь не збігається — не правте коментар, а напишіть другим рядком: ; MISMATCH: я думав X, насправді Y, причина: .... Це цінніше за "правильну" відповідь.
Міні-експеримент з вирівнюванням. Зробіть копію vadd8.asm → vadd8_unaligned.asm. У копії вставте один байт-шім перед b:, щоб навмисно зсунути його адресу на 1: align 32 a: dd 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0 db 0 ; ← шім: зсуває b на 1 байт ; align 32 ; ← закоментуйте, щоб шім не «з'їли» b: dd 10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0
Зберіть та запустіть:nasm -f elf64 -g vadd8_unaligned.asm -o vadd8_unaligned.o &amp;&amp; ld vadd8_unaligned.o -o vadd8_unaligned
./vadd8_unaligned; echo "exit=$?"
Що ви побачили? Допишіть у коментарях копії:
Що саме зламалося? Назва сигналу та код виходу (підказка: exit=139 — це 128 + сигнал, який сигнал?).
На якій точно інструкції програма впала? (Запустіть у GDB: gdb ./vadd8_unaligned, потім r, потім info registers rip, подивіться на адресу у disas.)
Якщо прибрати шім db 0, але залишити ; align 32 закоментованим — програма знову працює. Чому? Обчисліть адресу b відносно a у кожному з двох варіантів (з шімом / без нього) і поясніть, чому тільки один з них 32-вирівняний.
Якою однією заміною інструкції у головному коді (без align, без шіму) можна було б зробити програму толерантною до будь-якого вирівнювання? (Підказка: у таблиці інструкцій є дві різні версії — вирівняна та ні.) Після експерименту видаліть vadd8_unaligned.* — у репозиторії лишається лише робоча версія.
Обчисліть скалярний добуток двох масивів по 64 float32 за допомогою AVX2 FMA. 64 елементи = 8 чанків по 8 → 8 ітерацій циклу, кожна з яких виконує одну vfmadd231ps.Тест: обидва масиви = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], повторений 8 разів.
Очікуваний добуток: 8 × (1 + 4 + 9 + 16 + 25 + 36 + 49 + 64) = 8 × 204 = 1632.0, що в IEEE 754 = 0x44CC0000.Створіть файл dot8.asm:; dot8.asm — Скалярний добуток 64 float через AVX2 FMA
;
; Збірка: nasm -f elf64 -g dot8.asm -o dot8.o &amp;&amp; ld dot8.o -o dot8
; Запуск: ./dot8 default rel %define N 64 ; кількість float у кожному масиві section .data ; a[] = [1,2,3,4,5,6,7,8] × 8 = 64 floats align 32 a: %rep 8 dd 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0 %endrep ; b[] = те саме, що a[] align 32 b: %rep 8 dd 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0 %endrep str_dot db "dot(a,b) = " str_dot_l equ $ - str_dot str_want db " (want: 0x44CC0000 = 1632.0)" str_want_l equ $ - str_want section .text global _start _start: ; --- Обчислити скалярний добуток --- lea rdi, [a] ; RDI → a lea rsi, [b] ; RSI → b call dot_product ; результат у EAX (біти float) ; --- Вивести "dot(a,b) = 0xHHHHHHHH (want: ...)" --- push rax lea rsi, [str_dot] mov rdx, str_dot_l call print_string pop rax mov edi, eax call print_hex32 lea rsi, [str_want] mov rdx, str_want_l call print_string call print_newline ; Вихід mov rax, 60 xor rdi, rdi syscall ; ================================================================
; dot_product — обчислити Σ a[i] * b[i] для 64 елементів
;
; Вхід: RDI = адреса a (вирівняна на 32 байти)
; RSI = адреса b (вирівняна на 32 байти)
; Вихід: EAX = біти float32 результату
;
; Обмеження: N фіксоване = 64 (8 чанків по 8 float).
; ================================================================
dot_product: ; ───────────────────────────────────── ; КРОК 1: Занулення акумулятора (надано) ; ; vxorps — стандартний ідіоматичний спосіб обнулити YMM. ; Швидше за завантаження константи з пам'яті. ; ───────────────────────────────────── vxorps ymm0, ymm0, ymm0 ; ymm0 = [0,0,0,0,0,0,0,0] — 8 акумуляторів mov ecx, 8 ; 8 ітерацій × 8 float = 64 елементи ; ───────────────────────────────────── ; КРОК 2: Перша ітерація — робочий приклад (надано) ; ; vmovaps ymm1, [rdi] завантажує a[0..7]. ; vfmadd231ps ymm0, ymm1, [rsi] робить: ; ymm0 = ymm0 + (ymm1 * [rsi]) ; тобто ymm0 += a[0..7] * b[0..7]. ; ; Далі переходимо до наступного чанку, зсуваючи обидва ; покажчики на 32 байти (= 8 floats × 4 байти). ; ─────────────────────────────────────
.loop: vmovaps ymm1, [rdi] vfmadd231ps ymm0, ymm1, [rsi] add rdi, 32 add rsi, 32 ; ============================================ ; TODO 1: Закрийте цикл ; ; Поки зроблено лише ТІЛО циклу (одна ітерація). ; Додайте: ; - DEC ECX ; - JNZ .loop ; повторити, поки ECX != 0 ; ; Після циклу YMM0 містить 8 часткових сум. ; ============================================ ; ============================================ ; TODO 2: Горизонтальна редукція YMM0 → XMM0[0] ; ; Потрібно звести 8 часткових сум у YMM0 до одного ; скалярного значення у XMM0[0]. Для цього є шаблон ; із 4 інструкцій — він описаний у розділі ; "Горизонтальна редукція" Теоретичного мінімуму. ; ; ПЕРЕД тим, як писати ці 4 інструкції: перечитайте ; розділ і заповніть state-трасування нижче. Використовуйте ; конкретні часткові суми, які будуть у YMM0 ПІСЛЯ циклу ; для нашого тесту (масиви = [1,2,3,4,5,6,7,8] × 8, тож ; кожна смуга накопичить 8 × k² для k=1..8 = [8, 32, 72, ; 128, 200, 288, 392, 512]). ; ; STATE (заповніть КОЖЕН рядок перед написанням інструкцій): ; після циклу (8 ітерацій): ; ymm0 = [8, 32, 72, 128, 200, 288, 392, 512] ; ; 1. vextractf128 xmm1, ymm0, 1 ; xmm1 = [?, ?, ?, ?] ← яка половина? ; ; 2. vaddps xmm0, xmm0, xmm1 ; xmm0 = [?, ?, ?, ?] ; ; 3. vhaddps xmm0, xmm0, xmm0 ; xmm0 = [?, ?, ?, ?] ← vhaddps додає ПАРИ сусідів ; ; 4. vhaddps xmm0, xmm0, xmm0 ; xmm0 = [?, ?, ?, ?] ; ; Контроль: після всіх 4 кроків xmm0[0] має бути 1632. ; ; Лише ПІСЛЯ заповнення state — напишіть 4 інструкції нижче. ; ============================================ ; ───────────────────────────────────── ; КРОК 4: Перенести молодші 32 біти XMM0 в EAX (надано) ; ; vmovd виймає 32 біти з SIMD-регістра у звичайний. ; Це саме біти float-результату — у такому вигляді, ; як їх виводить print_hex32. ; ───────────────────────────────────── vmovd eax, xmm0 ret %include "simd_utils.inc"
Зберіть та запустіть:nasm -f elf64 -g dot8.asm -o dot8.o &amp;&amp; ld dot8.o -o dot8
./dot8
dot(a,b) = 0x44CC0000 (want: 0x44CC0000 = 1632.0)
Якщо бачите 0x44CC0000 — чудово, ваш кернел рахує правильно. Якщо бачите щось інше:
0x00000000 — цикл не крутиться або vfmadd пише не туди. Перевірте TODO 1.
Перші 4 біти правильні, решта сміття — забули виконати обидва vhaddps. Перевірте TODO 2.
Близько, але не збігається — одна ітерація циклу пропущена (ECX=7 замість 8?) або add rdi/rsi, 32 стоїть не в тому місці. Зверніть увагу: ми використовуємо vfmadd231ps із пам'яттю як третім операндом ([rsi]), а не як регістром. AVX2 це дозволяє для ps-варіантів. Це економить одне vmovaps на ітерацію — класична оптимізація, яку компілятори застосовують автоматично.
Запишіть у коментарях вашого dot8.asm:
Скільки множень vfmadd231ps виконано за весь цикл? А скалярних операцій множення-додавання усього?
У якій частині алгоритму SIMD "витрачається" (не дає виграшу 8×)?
Що б сталося, якби замість vxorps ymm0, ymm0, ymm0 ми забули занулити акумулятор? (Підказка: регістри YMM при старті програми — не обов'язково нулі.)
Виміряти експериментально різницю між двома реалізаціями одного скалярного добутку: одна з одним акумулятором (довгий ланцюг залежностей), друга — з чотирма незалежними акумуляторами. Переконатися, що різниця — у кілька разів на свою користь, навіть попри однакову кількість арифметичних операцій.Створіть файл dot8_ilp.asm. Це найбільший файл у практиці, але 90% — кістяк, який уже даний.; dot8_ilp.asm — ILP-експеримент: ланцюг vs незалежні акумулятори
;
; Збірка: nasm -f elf64 -g dot8_ilp.asm -o dot8_ilp.o &amp;&amp; ld dot8_ilp.o -o dot8_ilp
; Запуск: ./dot8_ilp
;
; Виконує один скалярний добуток у двох реалізаціях, кожну
; прокручує ITERS разів, і виводить кількість тактів CPU для кожної. default rel %define ITERS 10000000 ; 10 мільйонів ітерацій section .data ; Ті самі масиви, що і в dot8.asm — значення неважливі, ; ми міряємо ТАКТИ, а не результат. align 32 a: %rep 8 dd 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0 %endrep align 32 b: %rep 8 dd 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0 %endrep str_chain db "Version A (chain, 1 acc): " str_chain_l equ $ - str_chain str_indep db "Version B (indep, 4 accs): " str_indep_l equ $ - str_indep str_cycles db " cycles" str_cycles_l equ $ - str_cycles str_ratio db "Ratio A/B = " str_ratio_l equ $ - str_ratio section .text global _start _start: ; ═══════════════════════════════════════════════════ ; Заміряти Версію A ; ═══════════════════════════════════════════════════ lfence rdtsc shl rdx, 32 or rax, rdx mov r14, rax ; R14 = старт A lea rdi, [a] lea rsi, [b] mov rdx, ITERS call dot_chain lfence rdtsc shl rdx, 32 or rax, rdx sub rax, r14 ; RAX = такти A mov r14, rax ; R14 = цикли A (зберегти для рейтингу) ; --- Вивести "Version A (chain, 1 acc): &lt;N&gt; cycles\n" --- lea rsi, [str_chain] mov rdx, str_chain_l call print_string mov rdi, r14 call print_uint64 lea rsi, [str_cycles] mov rdx, str_cycles_l call print_string call print_newline ; ═══════════════════════════════════════════════════ ; Заміряти Версію B ; ═══════════════════════════════════════════════════ lfence rdtsc shl rdx, 32 or rax, rdx mov r15, rax ; R15 = старт B lea rdi, [a] lea rsi, [b] mov rdx, ITERS call dot_indep lfence rdtsc shl rdx, 32 or rax, rdx sub rax, r15 ; RAX = такти B mov r15, rax ; R15 = цикли B ; --- Вивести "Version B (indep, 4 accs): &lt;N&gt; cycles\n" --- lea rsi, [str_indep] mov rdx, str_indep_l call print_string mov rdi, r15 call print_uint64 lea rsi, [str_cycles] mov rdx, str_cycles_l call print_string call print_newline ; --- Вивести "Ratio A/B = &lt;N&gt;" (ціла частина) --- lea rsi, [str_ratio] mov rdx, str_ratio_l call print_string mov rax, r14 xor rdx, rdx div r15 ; RAX = r14 / r15 mov rdi, rax call print_uint64 call print_newline ; Вихід mov rax, 60 xor rdi, rdi syscall ; ================================================================
; dot_chain — ВЕРСІЯ A: один акумулятор, довгий ланцюг залежностей
;
; Вхід: RDI = a, RSI = b, RDX = ITERS
; Вихід: (ігнорується — міряємо такти, не значення)
;
; Усі 8 FMA у тілі ітерації пишуть у YMM0. Наступна FMA
; не може стартувати, поки попередня не завершила write-back
; у YMM0 — критичний шлях = 8 × латентність(FMA).
; ================================================================
dot_chain: vxorps ymm0, ymm0, ymm0
.loop: vmovaps ymm1, [rdi] vfmadd231ps ymm0, ymm1, [rsi] vmovaps ymm1, [rdi + 32] vfmadd231ps ymm0, ymm1, [rsi + 32] vmovaps ymm1, [rdi + 64] vfmadd231ps ymm0, ymm1, [rsi + 64] vmovaps ymm1, [rdi + 96] vfmadd231ps ymm0, ymm1, [rsi + 96] vmovaps ymm1, [rdi + 128] vfmadd231ps ymm0, ymm1, [rsi + 128] vmovaps ymm1, [rdi + 160] vfmadd231ps ymm0, ymm1, [rsi + 160] vmovaps ymm1, [rdi + 192] vfmadd231ps ymm0, ymm1, [rsi + 192] vmovaps ymm1, [rdi + 224] vfmadd231ps ymm0, ymm1, [rsi + 224] dec rdx jnz .loop ret ; ================================================================
; dot_indep — ВЕРСІЯ B: чотири незалежні акумулятори
;
; Вхід: RDI = a, RSI = b, RDX = ITERS
; Вихід: (ігнорується)
;
; 8 FMA у тілі ітерації розподілені по YMM0..YMM3 за схемою:
; offset 0 → ymm0
; offset 32 → ymm1
; offset 64 → ymm2
; offset 96 → ymm3
; offset 128 → ymm0 (повторюємо цикл акумуляторів)
; offset 160 → ymm1
; offset 192 → ymm2
; offset 224 → ymm3
;
; Сусідні FMA незалежні → OoO-ядро запускає до 2 FMA за такт.
; ================================================================
dot_indep: vxorps ymm0, ymm0, ymm0 vxorps ymm1, ymm1, ymm1 vxorps ymm2, ymm2, ymm2 vxorps ymm3, ymm3, ymm3
.loop: ; ───────────────────────────────────── ; Перші 4 FMA — робочий приклад (надано). ; YMM4 — тимчасовий регістр для завантаження (не акумулятор). ; ───────────────────────────────────── vmovaps ymm4, [rdi] vfmadd231ps ymm0, ymm4, [rsi] vmovaps ymm4, [rdi + 32] vfmadd231ps ymm1, ymm4, [rsi + 32] vmovaps ymm4, [rdi + 64] vfmadd231ps ymm2, ymm4, [rsi + 64] vmovaps ymm4, [rdi + 96] vfmadd231ps ymm3, ymm4, [rsi + 96] ; ============================================ ; TODO: Додайте наступні 4 FMA ; ; Повторіть той самий шаблон для зсувів 128, 160, 192, 224. ; Цикл акумуляторів продовжується: ymm0, ymm1, ymm2, ymm3. ; ; offset 128 → ymm0 += [rdi+128] * [rsi+128] ; offset 160 → ymm1 += [rdi+160] * [rsi+160] ; offset 192 → ymm2 += [rdi+192] * [rsi+192] ; offset 224 → ymm3 += [rdi+224] * [rsi+224] ; ; Використовуйте YMM4 як тимчасовий для завантаження. ; ============================================ dec rdx jnz .loop ; Об'єднати чотири акумулятори в один (не впливає на вимір, ; бо це за межами циклу). vaddps ymm0, ymm0, ymm1 vaddps ymm2, ymm2, ymm3 vaddps ymm0, ymm0, ymm2 ret %include "simd_utils.inc"
Зберіть:nasm -f elf64 -g dot8_ilp.asm -o dot8_ilp.o &amp;&amp; ld dot8_ilp.o -o dot8_ilp
Перед тим, як запустити ./dot8_ilp, заповніть у results.txt блок передбачення. Мета — щоб, побачивши число на екрані, ви вже мали модель, з якою порівняти. Без цього вимір — просто цифра без змісту.Кроки:
Подивіться модель вашого CPU: grep "model name" /proc/cpuinfo | head -1
<br>Знайдіть латентність та пропускну здатність VFMADD231PS (YMM) для вашого ядра на <a data-tooltip-position="top" aria-label="https://uops.info/table.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://uops.info/table.html" target="_self">uops.info</a>. Шукайте рядок VFMADD231PS (YMM, YMM, YMM), колонки Latency (такти, поки результат не готовий) і TP (Throughput) (такти між двома новими FMA, типово 0.5 = 2 FMA/такт).
Заповніть передбачення (див. шаблон results.txt нижче). Це ~5 хвилин роботи з калькулятором та uops.info.
Лише після цього запустіть ./dot8_ilp три рази поспіль (перший запуск часто cold) і запишіть середнє. Інструктор перевірятиме: пусте поле «Передбачено» = завдання не виконано, навіть якщо виміряні цифри правильні. Сенс експерименту — у співставленні моделі з реальністю, а не у тому, щоб отримати «правильне» число.
На типовому хості Codespaces (Intel Skylake-SP / Ice Lake / AMD EPYC ~2.5–3.2 GHz) ви маєте побачити щось на кшталт:Version A (chain, 1 acc): 261000000 cycles
Version B (indep, 4 accs): 75000000 cycles
Ratio A/B = 3
(Виміри на Intel Xeon Platinum 8259CL @ 2.50 GHz — типовий хост Codespaces.)Конкретні числа залежать від CPU та поточного навантаження хоста, але співвідношення A/B має бути принаймні 2×, типово 3×, а на деяких мікроархітектурах — до 4–5×. Якщо у вас менше, ніж 2× — щось не так із TODO (перевірте, що всі 8 FMA дійсно розподілені по 4 акумуляторах, а не випадково пишуть всі в YMM0).
Чому саме ~3×, а не теоретичні 8×?
Теоретичний максимум — це відношення латентності (4 такти) до пропускної здатності (½ такту = 2 FMA/такт), тобто 8×. На практиці версія B упирається не лише у порти FMA, а ще й у порти завантаження: кожна FMA тягне vmovaps + [rsi+off], а ядро може виконати лише 2 завантаження за такт. Тому B-версія знизу обмежена ~7 тактами на ітерацію (8 завантажень / 2 порти), а не ~4. Версія A витрачає ~32 такти/ітерацію. Звідси ~3×. Це і є приклад того, як реальна мікроархітектура відрізняється від ідеальної моделі з лекції. Чому обидві версії виконують однакову кількість FMA, але відрізняються у кілька разів?
Кількість арифметичних операцій однакова — 8 FMA × 10 млн = 80 млн FMA. Різниця в тому, що версія A серіалізує ці операції через залежність по YMM0: наступна FMA не може стартувати, поки попередня не записала результат (латентність FMA ≈ 4 такти на сучасних ядрах). Версія B розбиває ланцюг на 4 незалежних потоки, і OoO-ядро може запускати їх паралельно — до двох FMA за такт, обмежене пропускною здатністю FMA-портів, а не латентністю.
Це той самий OoO-бекенд з Лекції 14. Станції резервування не можуть "прискорити" версію A, бо залежність реальна. Перейменування регістрів не допомагає, бо ми самі написали ymm0 з обох сторін. Єдине рішення — зробити залежності явно незалежними на рівні коду.
Створіть файл results.txt за таким шаблоном (заповніть ПЕРЕДБАЧЕННЯ до запуску програми, ВИМІРИ — після):=== Частина 3: ILP-експеримент (dot8_ilp.asm) === CPU (model name з /proc/cpuinfo):
________________________________________ --- ПЕРЕДБАЧЕННЯ (заповнюється ДО запуску) --- З uops.info для VFMADD231PS (YMM, YMM, YMM) на моєму ядрі: Латентність (Latency): ____ тактів Пропускна здатність (TP): ____ такту (= ____ FMA за такт) Версія A — ланцюг 8 FMA через YMM0: Критичний шлях на 1 ітерацію = 8 × латентність = ____ тактів Передбачення для 10M ітерацій = ____ тактів Версія B — 4 незалежні акумулятори: Ідеально-throughput-обмежений нижній край = 8 / (2·1) = 4 такти/ітер (8 FMA, 2 FMA-порти, якщо нема інших вузьких місць) АЛЕ: кожна FMA тягне vmovaps із пам'яті — 8 завантажень / 2 load-порти = ще 4 такти/ітер Реальний нижній край = max(FMA-throughput, load-throughput) = ____ тактів/ітер Передбачення для 10M ітерацій = ____ тактів Передбачене співвідношення A/B = ____ --- ВИМІРИ (3 запуски, беру середнє, окрім першого cold run) --- Run 1 (cold): A = __________ cycles B = __________ cycles Ratio = ____ Run 2: A = __________ cycles B = __________ cycles Ratio = ____ Run 3: A = __________ cycles B = __________ cycles Ratio = ____ Середнє (runs 2–3): A = __________, B = __________, Ratio = ____ --- АНАЛІЗ --- 1. Моє передбачення для A збіглося з виміром у межах ±20%? ТАК / НІ
2. Моє передбачення для B збіглося з виміром у межах ±20%? ТАК / НІ
3. Якщо НІ для B — що ще може бути вузьким місцем? (варіанти: декодер, retirement, store port, кеш-промахи, dispatch bandwidth, накладні витрати циклу dec/jnz) _______________________________________________ 4. Lecture 14 §5 повторення: OoO-ядро використовує перейменування регістрів, щоб зняти залежності WAR і WAW. Чому перейменування НЕ допомагає для версії A? (Відповідь у ≤30 слів.) _______________________________________________ _______________________________________________
Зробіть копію dot8_ilp.asm → dot8_ilp_broken.asm. У копії знайдіть у функції dot_indep рядок: vfmadd231ps ymm2, ymm4, [rsi + 192]
та замініть ymm2 на ymm0 (тобто ця одна FMA тепер пише у той самий акумулятор, що й перша). Зберіть та запустіть:nasm -f elf64 -g dot8_ilp_broken.asm -o dot8_ilp_broken.o &amp;&amp; ld dot8_ilp_broken.o -o dot8_ilp_broken
./dot8_ilp_broken
Допишіть у results.txt:--- МІНІ-ЕКСПЕРИМЕНТ: одна FMA на ymm0 замість ymm2 --- Version B' (broken) cycles: __________
Ratio A/B' = ____ (порівняно з попереднім B, ratio впав до ~____) Питання: ми замінили ОДИН регістр-приймач в одній з восьми FMA.
Чому це настільки суттєво вплинуло? Підказка: які *пари* FMA тепер
мають залежність RAW через ymm0? Відповідь: ________________________________________
________________________________________ Після експерименту видаліть dot8_ilp_broken.* — у репозиторії лишається лише основна версія. Якщо ще залишився час: спробуйте версію з 8 незалежними акумуляторами (YMM0..YMM7, по одному FMA на акумулятор). Чи стало ще швидше? На більшості CPU — ні: пропускна здатність вже впирається у 2 FMA за такт або у load-порти, далі прискорюватися нема куди. Це стеля суперскалярності.
До кінця заняття:
simd_utils.inc — файл допоміжних функцій (незмінений)
vadd8.asm — 3 TODO заповнені, STATE-коментарі заповнені до запуску, програма виводить правильні a, b, c, відповіді на 3 питання, виконано і задокументовано експеримент з вирівнюванням (питання 4–7)
dot8.asm — 2 TODO заповнені, state-трасування горизонтальної редукції заповнене до написання 4 інструкцій, вивід = 0x44CC0000
dot8_ilp.asm — TODO заповнений (додаткові 4 FMA у dot_indep), обидві версії вимірюють цикли, співвідношення A/B ≥ 2
results.txt — заповнений блок передбачення (латентність, TP, критичний шлях) до запуску; три виміри; аналіз ±20%; відповідь на питання про перейменування регістрів; міні-експеримент з dot8_ilp_broken
Закомічено та запушено всі файли (git add, git commit, git push) ⚠️ Codespace — це інша машина! Без git push ваша робота лишається лише у хмарі GitHub, на вашому локальному комп'ютері її немає. Завжди commit + push перед тим, як закрити Codespace. Не забудьте зупинити Codespace після роботи: Ctrl+Shift+P → Codespaces: Stop Current Codespace.
Дедлайн: 23:59 у день перед Практикою 15SAXPY — це Single-precision A·X Plus Y: y[i] = a * x[i] + y[i] для всіх i. Це найпростіший "справжній" кернел, який фігурує у всіх підручниках з HPC і є базовим блоком BLAS Level 1.Реалізуйте SAXPY для масивів по 1024 float32 через AVX2 FMA.Створіть файл saxpy.asm:; saxpy.asm — y[i] = a * x[i] + y[i] для 1024 float через AVX2
;
; Збірка: nasm -f elf64 -g saxpy.asm -o saxpy.o &amp;&amp; ld saxpy.o -o saxpy
; Запуск: ./saxpy default rel %define N 1024 ; кількість елементів section .data ; Скаляр a = 2.0 (біти 0x40000000) align 4 scalar_a: dd 2.0 ; y_init = 10.0 — початкове значення для всіх елементів y[] y_init: dd 10.0 str_y0 db "y[0] = " str_y0_l equ $ - str_y0 str_y1 db "y[1] = " str_y1_l equ $ - str_y1 str_y511 db "y[511] = " str_y511_l equ $ - str_y511 str_y1023 db "y[1023] = " str_y1023_l equ $ - str_y1023 section .bss align 32 x: resd N ; x[] — буде заповнений у runtime align 32 y: resd N ; y[] — буде заповнений у runtime section .text global _start _start: ; --- Ініціалізація (надано): x[i] = (float)i, y[i] = 10.0 --- ; Робимо це у runtime, щоб уникнути складностей NASM ; з float-літералами всередині %rep. mov ecx, N xor eax, eax ; i = 0 lea rdi, [x] movss xmm1, [y_init] ; 10.0 у XMM1 lea rsi, [y]
.init: cvtsi2ss xmm0, eax ; xmm0 = (float)i movss [rdi], xmm0 ; x[i] = (float)i movss [rsi], xmm1 ; y[i] = 10.0 add rdi, 4 add rsi, 4 inc eax dec ecx jnz .init ; --- Обчислити SAXPY --- call saxpy_kernel ; --- Вивести 4 контрольні точки --- ; y[0] = 2.0*0.0 + 10.0 = 10.0 = 0x41200000 ; y[1] = 2.0*1.0 + 10.0 = 12.0 = 0x41400000 ; y[511] = 2.0*511.0 + 10.0 = 1032.0 = 0x44810000 ; y[1023]= 2.0*1023.0 + 10.0 = 2056.0 = 0x45008000 lea rsi, [str_y0] mov rdx, str_y0_l call print_string mov edi, [y] call print_hex32 call print_newline lea rsi, [str_y1] mov rdx, str_y1_l call print_string mov edi, [y + 4] call print_hex32 call print_newline lea rsi, [str_y511] mov rdx, str_y511_l call print_string mov edi, [y + 511*4] call print_hex32 call print_newline lea rsi, [str_y1023] mov rdx, str_y1023_l call print_string mov edi, [y + 1023*4] call print_hex32 call print_newline ; Вихід mov rax, 60 xor rdi, rdi syscall ; ================================================================
; saxpy_kernel — y[i] = a * x[i] + y[i] для i=0..N-1
;
; Використовує глобальні масиви x[] та y[] і скаляр [scalar_a].
; ================================================================
saxpy_kernel: ; TODO 1: Завантажити та розтиражувати скаляр a у всі 8 смуг YMM ; ; Інструкція: vbroadcastss ymm, [addr_float] ; Результат: ymm = [a, a, a, a, a, a, a, a] ; TODO 2: Налаштувати покажчики та лічильник ; ; RDI → x, RSI → y, ECX = N/8 = 128 чанків по 8 float ; TODO 3: Тіло циклу ; ; На кожній ітерації: ; - завантажити x[i..i+7] у ymm1 ; - завантажити y[i..i+7] у ymm2 ; - ymm2 = ymm2 + (ymm1 * ymm_a) ← vfmadd231ps ; - зберегти ymm2 назад у y[i..i+7] ; - просунути rdi, rsi на 32 ; - DEC ECX, JNZ .loop ret %include "simd_utils.inc"
y[0] = 0x41200000
y[1] = 0x41400000
y[511] = 0x44810000
y[1023] = 0x45008000
<br>Перевірте кожне значення на <a data-tooltip-position="top" aria-label="https://float.exposed/" rel="noopener nofollow" class="external-link is-unresolved" href="https://float.exposed/" target="_self">float.exposed</a>.=== SAXPY 1024 через AVX2 === Скільки ітерацій циклу? ______
Скільки FMA-інструкцій виконано за один виклик saxpy_kernel? ______
Скільки скалярних операцій (множень + додавань) це замінює? ______
Теоретичне прискорення vs скалярної версії: ______× y[0] = 0x________ (очікувано: 0x41200000)
y[1] = 0x________ (очікувано: 0x41400000)
y[511] = 0x________ (очікувано: 0x44810000)
y[1023] = 0x________ (очікувано: 0x45008000)
Реалізуйте кернел C += A * B для матриць 8×8 float32 із використанням 8 YMM-акумуляторів (по одному на рядок C).Ідея: кожен рядок C[i] — 8 floats, тримаємо його у своєму ymm(i). На кожній ітерації по k беремо A[i][k] скалярно, розтиражуємо через vbroadcastss, і робимо FMA проти рядка B[k][0..7].Скелет ідеї:; Ініціалізація: завантажити C[0..7][0..7] у ymm0..ymm7
; Цикл по k від 0 до 7:
; vmovaps ymm8, [B + k*32] ; рядок B[k]
; vbroadcastss ymm9, [A + 0*32 + k*4] ; A[0][k]
; vfmadd231ps ymm0, ymm8, ymm9 ; C[0] += B[k] * A[0][k]
; vbroadcastss ymm9, [A + 1*32 + k*4] ; A[1][k]
; vfmadd231ps ymm1, ymm8, ymm9
; ... (ще 6 рядків)
; Після циклу: зберегти ymm0..ymm7 у C[0..7][0..7]
Тест: візьміть A = одинична матриця, B = матриця з B[i][j] = i*8 + j + 1 (числа 1..64), очікувано C = B. Перевірте один рядок через print_ymm_buf.Збережіть у matmul8x8.asm + доповнення у saxpy_report.txt про спостереження.Запишіть відео, у якому:
Продемонструйте dot8_ilp — запустіть, прочитайте вголос обидва виміри тактів і співвідношення A/B. Назвіть вашу модель CPU.
Продемонструйте saxpy — запустіть, покажіть, що всі 4 контрольні точки збігаються з очікуваними.
Поясніть своїми словами (≤90 сек): чому версія B з dot8_ilp швидша за версію A, хоча виконує ту саму кількість FMA? Зв'яжіть це з поняттям латентності FMA та суперскалярного виконання з Лекції 14.
УВАГА! Вимога про 5хв максимально важлива! Brevity is the soul of wit. Це частина оцінки.Lab14/
├── simd_utils.inc ← допоміжні функції (незмінений)
├── vadd8.asm ← Частина 1: перше SIMD-додавання
├── dot8.asm ← Частина 2: скалярний добуток через FMA
├── dot8_ilp.asm ← Частина 3: ILP-експеримент
├── results.txt ← виміри Частини 3 + висновок
├── saxpy.asm ← домашнє: SAXPY 1024
├── saxpy_report.txt ← звіт за домашнім
├── matmul8x8.asm ← бонус (якщо робите)
└── video.txt ← посилання на Loom
Не забудьте git push! Якщо ви працюєте у Codespace — без push ваша робота існує лише на віддаленій машині GitHub.
<br><a data-tooltip-position="top" aria-label="https://float.exposed/" rel="noopener nofollow" class="external-link is-unresolved" href="https://float.exposed/" target="_self">float.exposed</a> — візуалізатор бітів IEEE 754
<br><a data-tooltip-position="top" aria-label="https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html" target="_self">Intel Intrinsics Guide</a> — довідник усіх SIMD-інструкцій (у колонці Instruction — саме те, що пише NASM)
<br><a data-tooltip-position="top" aria-label="https://uops.info/table.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://uops.info/table.html" target="_self">uops.info</a> — латентність та пропускна здатність кожної інструкції на кожному поколінні Intel/AMD
<br><a data-tooltip-position="top" aria-label="https://www.agner.org/optimize/instruction_tables.pdf" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.agner.org/optimize/instruction_tables.pdf" target="_self">Agner Fog's Instruction Tables</a> — те саме, але PDF
<br><a data-tooltip-position="top" aria-label="https://www.nasm.us/xdoc/2.16.03/html/nasmdoc0.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.nasm.us/xdoc/2.16.03/html/nasmdoc0.html" target="_self">NASM Manual</a> — довідка NASM
<br><a data-tooltip-position="top" aria-label="https://www.intel.com/content/dam/www/public/us/en/documents/white-papers/ia-32-ia-64-benchmark-code-execution-paper.pdf" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.intel.com/content/dam/www/public/us/en/documents/white-papers/ia-32-ia-64-benchmark-code-execution-paper.pdf" target="_self">x86-64 ABI: rdtsc та вимірювання</a> — офіційний документ Intel про правильне вимірювання rdtsc
]]></description><link>labs/lab14.html</link><guid isPermaLink="false">Labs/Lab14.md</guid><pubDate>Tue, 14 Apr 2026 17:41:05 GMT</pubDate></item><item><title><![CDATA[L14-M1-Block-Diagram]]></title><description><![CDATA[<img src="assets/l14-m1-block-diagram.svg" target="_self">]]></description><link>assets/l14-m1-block-diagram.html</link><guid isPermaLink="false">assets/L14-M1-Block-Diagram.svg</guid><pubDate>Tue, 14 Apr 2026 10:22:26 GMT</pubDate><enclosure url="assets/l14-m1-block-diagram.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-m1-block-diagram.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-SMT-Sharing]]></title><description><![CDATA[<img src="assets/l14-smt-sharing.svg" target="_self">]]></description><link>assets/l14-smt-sharing.html</link><guid isPermaLink="false">assets/L14-SMT-Sharing.svg</guid><pubDate>Tue, 14 Apr 2026 10:21:55 GMT</pubDate><enclosure url="assets/l14-smt-sharing.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-smt-sharing.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Backend-Utilization]]></title><description><![CDATA[<img src="assets/l14-backend-utilization.svg" target="_self">]]></description><link>assets/l14-backend-utilization.html</link><guid isPermaLink="false">assets/L14-Backend-Utilization.svg</guid><pubDate>Tue, 14 Apr 2026 10:20:58 GMT</pubDate><enclosure url="assets/l14-backend-utilization.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-backend-utilization.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Tomasulo-CDB]]></title><description><![CDATA[<img src="assets/l14-tomasulo-cdb.svg" target="_self">]]></description><link>assets/l14-tomasulo-cdb.html</link><guid isPermaLink="false">assets/L14-Tomasulo-CDB.svg</guid><pubDate>Tue, 14 Apr 2026 10:20:27 GMT</pubDate><enclosure url="assets/l14-tomasulo-cdb.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-tomasulo-cdb.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Spectre-Cache-Sidechannel]]></title><description><![CDATA[<img src="assets/l14-spectre-cache-sidechannel.svg" target="_self">]]></description><link>assets/l14-spectre-cache-sidechannel.html</link><guid isPermaLink="false">assets/L14-Spectre-Cache-Sidechannel.svg</guid><pubDate>Tue, 14 Apr 2026 10:19:38 GMT</pubDate><enclosure url="assets/l14-spectre-cache-sidechannel.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-spectre-cache-sidechannel.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Superscalar-Hardware]]></title><description><![CDATA[<img src="assets/l14-superscalar-hardware.svg" target="_self">]]></description><link>assets/l14-superscalar-hardware.html</link><guid isPermaLink="false">assets/L14-Superscalar-Hardware.svg</guid><pubDate>Tue, 14 Apr 2026 10:18:55 GMT</pubDate><enclosure url="assets/l14-superscalar-hardware.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-superscalar-hardware.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Instruction-Lifecycle]]></title><description><![CDATA[<img src="assets/l14-instruction-lifecycle.svg" target="_self">]]></description><link>assets/l14-instruction-lifecycle.html</link><guid isPermaLink="false">assets/L14-Instruction-Lifecycle.svg</guid><pubDate>Tue, 14 Apr 2026 10:18:28 GMT</pubDate><enclosure url="assets/l14-instruction-lifecycle.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-instruction-lifecycle.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Register-Renaming]]></title><description><![CDATA[<img src="assets/l14-register-renaming.svg" target="_self">]]></description><link>assets/l14-register-renaming.html</link><guid isPermaLink="false">assets/L14-Register-Renaming.svg</guid><pubDate>Tue, 14 Apr 2026 10:17:32 GMT</pubDate><enclosure url="assets/l14-register-renaming.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-register-renaming.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Forwarding-Bypass]]></title><description><![CDATA[<img src="assets/l14-forwarding-bypass.svg" target="_self">]]></description><link>assets/l14-forwarding-bypass.html</link><guid isPermaLink="false">assets/L14-Forwarding-Bypass.svg</guid><pubDate>Tue, 14 Apr 2026 10:17:27 GMT</pubDate><enclosure url="assets/l14-forwarding-bypass.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-forwarding-bypass.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Single-Cycle-Waste]]></title><description><![CDATA[<img src="assets/l14-single-cycle-waste.svg" target="_self">]]></description><link>assets/l14-single-cycle-waste.html</link><guid isPermaLink="false">assets/L14-Single-Cycle-Waste.svg</guid><pubDate>Tue, 14 Apr 2026 10:16:36 GMT</pubDate><enclosure url="assets/l14-single-cycle-waste.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-single-cycle-waste.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Coffee-Shop]]></title><description><![CDATA[<img src="assets/l14-coffee-shop.svg" target="_self">]]></description><link>assets/l14-coffee-shop.html</link><guid isPermaLink="false">assets/L14-Coffee-Shop.svg</guid><pubDate>Tue, 14 Apr 2026 10:16:28 GMT</pubDate><enclosure url="assets/l14-coffee-shop.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-coffee-shop.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Branch-Predictor-2bit]]></title><description><![CDATA[<img src="assets/l14-branch-predictor-2bit.svg" target="_self">]]></description><link>assets/l14-branch-predictor-2bit.html</link><guid isPermaLink="false">assets/L14-Branch-Predictor-2bit.svg</guid><pubDate>Tue, 14 Apr 2026 10:16:00 GMT</pubDate><enclosure url="assets/l14-branch-predictor-2bit.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-branch-predictor-2bit.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Pipeline-Hardware]]></title><description><![CDATA[<img src="assets/l14-pipeline-hardware.svg" target="_self">]]></description><link>assets/l14-pipeline-hardware.html</link><guid isPermaLink="false">assets/L14-Pipeline-Hardware.svg</guid><pubDate>Tue, 14 Apr 2026 10:15:26 GMT</pubDate><enclosure url="assets/l14-pipeline-hardware.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-pipeline-hardware.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Running-Example-OoO]]></title><description><![CDATA[<img src="assets/l14-running-example-ooo.svg" target="_self">]]></description><link>assets/l14-running-example-ooo.html</link><guid isPermaLink="false">assets/L14-Running-Example-OoO.svg</guid><pubDate>Tue, 14 Apr 2026 10:14:48 GMT</pubDate><enclosure url="assets/l14-running-example-ooo.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-running-example-ooo.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Running-Example-Superscalar]]></title><description><![CDATA[<img src="assets/l14-running-example-superscalar.svg" target="_self">]]></description><link>assets/l14-running-example-superscalar.html</link><guid isPermaLink="false">assets/L14-Running-Example-Superscalar.svg</guid><pubDate>Tue, 14 Apr 2026 10:14:30 GMT</pubDate><enclosure url="assets/l14-running-example-superscalar.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-running-example-superscalar.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Running-Example-Forwarding]]></title><description><![CDATA[<img src="assets/l14-running-example-forwarding.svg" target="_self">]]></description><link>assets/l14-running-example-forwarding.html</link><guid isPermaLink="false">assets/L14-Running-Example-Forwarding.svg</guid><pubDate>Tue, 14 Apr 2026 10:14:15 GMT</pubDate><enclosure url="assets/l14-running-example-forwarding.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-running-example-forwarding.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Running-Example-Pipelined]]></title><description><![CDATA[<img src="assets/l14-running-example-pipelined.svg" target="_self">]]></description><link>assets/l14-running-example-pipelined.html</link><guid isPermaLink="false">assets/L14-Running-Example-Pipelined.svg</guid><pubDate>Tue, 14 Apr 2026 10:13:57 GMT</pubDate><enclosure url="assets/l14-running-example-pipelined.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-running-example-pipelined.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Running-Example-Sequential]]></title><description><![CDATA[<img src="assets/l14-running-example-sequential.svg" target="_self">]]></description><link>assets/l14-running-example-sequential.html</link><guid isPermaLink="false">assets/L14-Running-Example-Sequential.svg</guid><pubDate>Tue, 14 Apr 2026 10:13:51 GMT</pubDate><enclosure url="assets/l14-running-example-sequential.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-running-example-sequential.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-NOTES-ENG. Instruction-Level and Thread-Level Parallelism]]></title><description><![CDATA[Quick reference for Lecture 14. See full materials in L14-ENG. Instruction-Level and Thread-Level Parallelism.md.LO5 (performance bottlenecks) · LO6 (parallelism, pipelining)
Single-cycle clock period = sum of all stage delays (IF+ID+EX+MEM+WB ≈ 5.0 ns → 200 MHz ceiling).
At any instant, 4 of 5 stages sit idle — 80% of the silicon is spinning its wheels.
Clock period is bounded by the critical path; you can't just "turn up the clock."
Running example: 5-instruction array-sum loop (load, add, inc, cmp, jne) → 25 cycles sequential → CPI = 5.0.
Fix direction: keep all stages busy simultaneously — 5× speedup hiding in plain sight.
Learn more: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Instruction_pipelining" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Instruction_pipelining" target="_self">Instruction pipelining</a>
Coffee-shop / assembly-line analogy: don't make each step faster — never let a station sit idle.
Drop pipeline registers between stages; every stage works on a different instruction.
Canonical 5 stages: IF fetch · ID decode/regread · EX execute · MEM memory · WB writeback.
Throughput/latency theorem: pipelining doesn't make any single instruction faster; it makes instructions finish more often. Latency ≈ 5 cycles, throughput = 1 instr/cycle.
Clock period now covers only the slowest single stage (≈1.5 ns → ~667 MIPS).
New metric: CPI (cycles per instruction). Ideal pipelined CPI = 1; real CPI worse because of hazards.
Running example Level 2 (pipelined, no forwarding): load-use bubble + ALU RAW bubble + branch bubble → CPI ≈ 2.0.
<br>Learn more: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Instruction_pipelining" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Instruction_pipelining" target="_self">Instruction pipelining</a>A hazard = reason the next instruction can't execute next cycle.
Structural: two instructions want the same hardware. Mostly solved by duplication (split I/D cache, multiple ALUs/ports).
Data hazards (by register dependency shape): RAW (Read After Write) — true dependency. Can't erase, only minimize cost. Dominant.
WAR (Write After Read) — anti-dependency; name conflict only.
WAW (Write After Write) — output dependency; name conflict only. Key insight: WAR/WAW are naming conflicts, not real dependencies — register renaming (§6) makes them vanish.
Control hazards: branches — fetch doesn't know the target until EX resolves. Penalty scales with pipeline depth.
CPI math (20% loads w/ 1-cycle stall, 20% branches w/ 2-cycle penalty, 10% mispredict rate):
CPI ≈ 1 + 0.20·1 + 0.20·2·0.10 = 1.24
<br>Learn more: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Hazard_(computer_architecture)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Hazard_(computer_architecture)" target="_self">Hazard (computer architecture)</a>Forwarding (bypassing): direct wires from later stage outputs back to EX inputs, bypassing the register file.
EX→EX and EX→MEM RAWs: killed completely (zero bubbles).
Load-use (MEM→EX): survives as exactly 1 bubble. The loaded value isn't ready until end of MEM; the next instruction needs it at start of EX. No wire runs backward in time.
Hazard detection unit freezes PC + injects NOP when forwarding can't save you.
Compiler slot filling: reorder to place an independent instruction in the load-use slot.; Before: bubble after load
mov eax, [rsi+rcx*4]
add ebx, eax ; &lt;-- stall
inc rcx
; After: inc fills the slot
mov eax, [rsi+rcx*4]
inc rcx ; &lt;-- fills slot
add ebx, eax ; now ready
Branch prediction: guess direction, speculate, flush if wrong.
Static: "backward taken, forward not taken" — works for loops.
1-bit: remembers last outcome; double-mispredicts on pattern flips.
2-bit saturating counter: four-state FSM (Strongly NT / Weakly NT / Weakly T / Strongly T). Must be able to draw this on the exam. Absorbs transient noise; only one mispredict on loop exit.
BTB (Branch Target Buffer): cache of "last target for this PC" — lets fetch redirect on the same cycle, essential for indirect branches.
Two-level adaptive / TAGE / perceptron: use branch history, 95–99% accurate.
Tournament: meta-predictor picks between local and global history.
Misprediction penalty scales with depth. Pentium 4 Prescott (31 stages) lost 20+ cycles per mispredict → Prescott was often slower than the shorter Pentium III at the same clock. Modern sweet spot: 14–20 stages, &gt;97% accuracy.Sum ≈ 18 stages. Each line is a real SRAM/arithmetic/dependency that physically cannot fit in a shorter clock.Running example Level 3 (forwarding + 2-bit predictor): only the load-use bubble survives → CPI ≈ 1.2.<br>Learn more: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Branch_predictor" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Branch_predictor" target="_self">Branch predictor</a>
Duplicate frontend and backend: 2-wide, 4-wide, 8-wide. IPC &gt; 1, equivalently CPI &lt; 1.
First mainstream x86 superscalar: Pentium 1993 (U-pipe/V-pipe). Modern x86: 4–6 wide. Apple M1: 8 wide.
In-order superscalar limit: dependency chains serialize half the issue width. Running example on 2-wide in-order → CPI ≈ 0.9 (not 0.5) because I2 still waits on I1 and cross-iteration chain on rcx.
Insight that unlocks OoO: iteration k+1's load is independent of iteration k's add — but an in-order machine can't see that.
Itanium cautionary tale (2001, EPIC/VLIW): Intel+HP bet the compiler could extract ILP statically via 3-op bundles. Failed because load latencies and branch outcomes are dynamic — a compile-time schedule must assume worst-case everywhere. Outsold ~100:1 by x86 OoO; discontinued 2021. Moral: ILP is best extracted dynamically, at hardware speed, against real runtime information. Static ILP is brittle.
<br>Learn more: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Superscalar_processor" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Superscalar_processor" target="_self">Superscalar processor</a> · <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Itanium" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Itanium" target="_self">Itanium</a>Two goals: (1) execute as soon as operands are ready; (2) commit in program order for precise exceptions.
Register renaming: architectural eax → pool of 150–400 physical registers. WAR and WAW disappear by construction — every write gets a fresh physical register. Only RAW remains.
Reservation stations: per-functional-unit waiting areas; each entry holds known operand values and tags for unknown ones.
Common Data Bus (CDB): functional units broadcast (tag, value); every station associatively matches and latches. This is dataflow execution in hardware.
Three phases (memorize): Issue — in order: decode, rename, allocate reservation station + ROB tail slot.
Execute — out of order: fires whenever operands arrive on CDB.
Commit / retire — in order at ROB head: writes architectural register file, frees the slot. Reorder Buffer (ROB): circular FIFO, tail=issue, head=commit. Enables precise exceptions — on a fault, flush ROB from faulting entry, architectural register file is untouched.
The wall this lecture is really about: architectural state (what the OS/debugger sees) vs. microarchitectural state (caches, predictors, RS, ROB). The ROB is the wall. §7 is what happens when the wall leaks.
Running example Level 5 (OoO + ROB, 2 iterations in flight): loads from iteration k+1 execute in parallel with adds from k → CPI ≈ 0.4. Bottleneck is now load latency and ROB size, not dependency chains.Cost: reservation stations need associative CAM; complexity scales super-linearly with issue width. This is why x86 tops out at 4–6 wide.<br>Learn more: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Tomasulo%27s_algorithm" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Tomasulo%27s_algorithm" target="_self">Tomasulo's algorithm</a> · <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Reorder_buffer" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Reorder_buffer" target="_self">Reorder buffer</a>
ROB roughly 2× the size of any contemporary x86 core → much larger search radius for independent work.
Why 8-wide? L09 callback: ARM64 is fixed-width 4-byte instructions → slice a 32-byte fetch line into 8 equal chunks, trivially parallel decode. x86 is variable-length 1–15 bytes → decode boundaries are on the critical path. x86 compensates with µop caches and length-markers but can't reach 8-wide cold.
P-core / E-core split (4 Firestorm + 4 Icestorm sharing ARM64 ISA): OS dynamically migrates threads. Lineage: ARM big.LITTLE 2011. L15 teaser — heterogeneity beats monolithic scaling.
Lesson: Apple didn't invent anything new. They built a much bigger version of the OoO core everyone already had, and the ISA made it affordable.
<br>Learn more: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Apple_M1" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Apple_M1" target="_self">Apple M1</a>The single most important idea in this lecture:
Architectural state is what the ISA tells you the machine looks like. Microarchitectural state (caches, predictors, TLBs, ROB, RS) is what the machine actually is. Spectre and Meltdown proved the difference matters — microarchitectural state is observable through timing.
Speculation: to keep an 8-wide engine fed past branches, the CPU must guess. If wrong, squash the speculative µops. For 30 years the assumption was that squashing is complete. It isn't: the architectural state rolls back, but the cache is still warm, the predictor still remembers, the TLB still holds the walk.Cache hit ≈ 3–4 cycles; miss to DRAM ≈ 200–300 cycles. That gap is a measurable 1-bit channel; with 256 distinct lines you get 1 byte per measurement.flush probe[0..255] ; all cold
&lt;speculatively execute&gt;: secret = forbidden_load() touch probe[secret * 64] ; warms ONE line
&lt;speculation squashed&gt; ; architectural state clean
time probe[0..255] ; find the fast one → secret
The thing we celebrated in L11 — fast caches — is the leak.Intel (and some ARM) ran the load and the user/supervisor permission check in parallel, with the permission check completing later. A speculative kernel load put its value into a renamed physical register before the fault retired. Three speculative instructions (kernel load → shift → probe load) leave a cache footprint; the fault is caught, state rolls back, attacker probes the array → user-mode reads arbitrary kernel memory (= all of RAM on Linux). Affected nearly every Intel OoO core since 1995; AMD mostly immune (permission check happened before the value propagated).Fix: KAISER / KPTI (Kernel Page Table Isolation). Kernel page tables unmapped in user mode. Costs 5–30% on syscall-heavy workloads (PCID helps). Paid forever in software.Abuses the direction predictor. The programmer wrote the bounds check correctly:if (x &lt; array1_size) { y = array2[array1[x] * 4096];
}
Attacker trains the predictor to say "taken" on many valid calls, then passes a malicious out-of-range x. The CPU speculates past the check, loads the secret at array1[x], uses it as an index into array2, leaves a cache footprint. Bounds check resolves, squash — too late. The code is correct. The hardware violated it anyway. No single line to patch. Mitigations (lfence, array_index_nospec(), speculation hardening) are expensive and still incomplete in 2026.Abuses the BTB (target predictor) for indirect branches. Attacker pollutes BTB entries that collide with victim's indirect branches; victim's indirect branch is speculatively redirected to an attacker-chosen gadget inside the victim's address space. Fix: retpoline (Google) — replace every indirect branch with a call/ret sequence the BTB can't meaningfully poison.Spectre and Meltdown are not bugs — the CPUs fulfilled the ISA contract. The contract was incomplete. For 50 years we described CPUs by architectural state alone. From 2018 onward, microarchitectural state is part of the machine's security model. The speculation tax is permanent: KPTI overhead, retpoline overhead, fences in hot paths, reduced JS timer resolution, cloud schedulers avoiding co-tenancy. Total overhead: 2–30% depending on workload, some never clawed back.If you remember one thing from this lecture: architectural state ≠ microarchitectural state, and the difference is not a footnote.<br>Learn more: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Speculative_execution" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Speculative_execution" target="_self">Speculative execution</a> · <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Spectre_(security_vulnerability)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Spectre_(security_vulnerability)" target="_self">Spectre</a> · <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Meltdown_(security_vulnerability)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Meltdown_(security_vulnerability)" target="_self">Meltdown</a> · <a data-tooltip-position="top" aria-label="https://meltdownattack.com/" rel="noopener nofollow" class="external-link is-unresolved" href="https://meltdownattack.com/" target="_self">meltdownattack.com</a>
Even an 8-wide M1 rarely exceeds IPC ≈ 2 on single-thread code — the backend is over-provisioned for any single thread. L3 misses, mispredicts, dep chains all leave execution ports idle.
SMT: let two threads issue into the same cycle of the backend, filling gaps.
Cost: ~5% area, benefit: ~15–30% throughput on typical server workloads. Compare to adding a second core: ~100% area for ~80–90% throughput. SMT buys cheap throughput — with per-thread predictability as the currency.
Duplicated (per logical thread): architectural register file, PC, CR3/segments/MSRs, APIC, interrupt state (~5% of core area).
Shared: ROB, reservation stations, physical register file, functional units, L1/L2 caches, branch predictor, TLB, fetch/decode bandwidth.
Intel HTT shipped on Xeon (Feb 2002) then Pentium 4. AMD added 2-way SMT in Zen (2017). IBM POWER8+ supports up to 8-way. x86 stayed at 2 — diminishing returns.
When SMT hurts: HPC (already saturated), databases (tail latency), real-time systems, latency-sensitive workloads. Often disabled.
Security cost: SMT siblings share everything in the backend. Percival 2005 RSA key leak; Foreshadow/L1TF 2018; MDS/RIDL/Fallout 2019. OpenBSD disabled HT by default in 2018. Cloud providers refuse cross-tenant SMT co-scheduling.
ILP finds parallelism in one thread. TLP via SMT imports parallelism from another thread when one isn't enough. Both feed the same hungry backend. (Memorize this.)
<br>Learn more: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Simultaneous_multithreading" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Simultaneous_multithreading" target="_self">Simultaneous multithreading</a> · <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Hyper-threading" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Hyper-threading" target="_self">Hyper-threading</a>Same 5-instruction array-sum loop, five microarchitectures:End-to-end: roughly 12× faster at the same clock, on the same 5 instructions, from microarchitectural tricks alone.
Pipelining trades latency for throughput. Each instruction still takes N cycles; a new instruction completes every cycle. Hazards are the gap between ideal and real CPI.
Every ILP technique solves a problem the previous one created. Pipelining → hazards → forwarding/prediction → superscalar → dep chains → OoO → broken precise exceptions → ROB → need to cross branches → speculation → Spectre/Meltdown.
Dynamic beats static for ILP. Itanium's EPIC bet failed because cache misses and branches are runtime phenomena. OoO schedules against actual observed latencies.
Apple M1 is textbook OoO at industrial scale. Same 1967 Tomasulo reservation stations + CDB, just much larger (8-wide, ~630 ROB). ARM64's fixed-width ISA made that scaling affordable.
Architectural state ≠ microarchitectural state. The single most important idea. Caches/predictors/TLBs are part of the machine too — observable through timing, now part of every secure system's threat model.
SMT buys cheap throughput (~5% area for 15–30% more work) at the cost of per-thread predictability and a new class of same-core side channels.
Required reading:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Instruction_pipelining" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Instruction_pipelining" target="_self">Instruction pipelining</a> — Overview, Hazards
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Tomasulo%27s_algorithm" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Tomasulo%27s_algorithm" target="_self">Tomasulo's algorithm</a> — Algorithm, Implementation
<br><a data-tooltip-position="top" aria-label="https://spectreattack.com/" rel="noopener nofollow" class="external-link is-unresolved" href="https://spectreattack.com/" target="_self">Spectre</a> and <a data-tooltip-position="top" aria-label="https://meltdownattack.com/" rel="noopener nofollow" class="external-link is-unresolved" href="https://meltdownattack.com/" target="_self">Meltdown</a> landing pages
Patterson &amp; Hennessy, Computer Organization and Design, Chapter 4
What you'll do:
Draw the running example by hand at each level (sequential → pipelined → forwarding → superscalar → OoO). The spacetime grid is the single most useful study exercise.
Take a short x86-64 function; identify where it stalls on a 5-stage in-order pipeline. Which RAWs forward cleanly? Which survive as 1-cycle bubbles? Where does the first branch sit?
Bonus: compile a C function with -O0 vs -O3. Spot the compiler reordering loads to hide load-use latency.
]]></description><link>lectures/eng/l14-notes-eng.-instruction-level-and-thread-level-parallelism.html</link><guid isPermaLink="false">Lectures/ENG/L14-NOTES-ENG. Instruction-Level and Thread-Level Parallelism.md</guid><pubDate>Tue, 14 Apr 2026 09:41:20 GMT</pubDate></item><item><title><![CDATA[L14-Backend-Utilization-ENG]]></title><description><![CDATA[<img src="assets/l14-backend-utilization-eng.svg" target="_self">]]></description><link>assets/l14-backend-utilization-eng.html</link><guid isPermaLink="false">assets/L14-Backend-Utilization-ENG.svg</guid><pubDate>Tue, 14 Apr 2026 08:56:16 GMT</pubDate><enclosure url="assets/l14-backend-utilization-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-backend-utilization-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Pipeline-Hardware-ENG]]></title><description><![CDATA[<img src="assets/l14-pipeline-hardware-eng.svg" target="_self">]]></description><link>assets/l14-pipeline-hardware-eng.html</link><guid isPermaLink="false">assets/L14-Pipeline-Hardware-ENG.svg</guid><pubDate>Tue, 14 Apr 2026 08:55:53 GMT</pubDate><enclosure url="assets/l14-pipeline-hardware-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-pipeline-hardware-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Instruction-Lifecycle-ENG]]></title><description><![CDATA[<img src="assets/l14-instruction-lifecycle-eng.svg" target="_self">]]></description><link>assets/l14-instruction-lifecycle-eng.html</link><guid isPermaLink="false">assets/L14-Instruction-Lifecycle-ENG.svg</guid><pubDate>Tue, 14 Apr 2026 08:55:32 GMT</pubDate><enclosure url="assets/l14-instruction-lifecycle-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-instruction-lifecycle-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Single-Cycle-Waste-ENG]]></title><description><![CDATA[<img src="assets/l14-single-cycle-waste-eng.svg" target="_self">]]></description><link>assets/l14-single-cycle-waste-eng.html</link><guid isPermaLink="false">assets/L14-Single-Cycle-Waste-ENG.svg</guid><pubDate>Tue, 14 Apr 2026 08:55:32 GMT</pubDate><enclosure url="assets/l14-single-cycle-waste-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-single-cycle-waste-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Register-Renaming-ENG]]></title><description><![CDATA[<img src="assets/l14-register-renaming-eng.svg" target="_self">]]></description><link>assets/l14-register-renaming-eng.html</link><guid isPermaLink="false">assets/L14-Register-Renaming-ENG.svg</guid><pubDate>Tue, 14 Apr 2026 08:55:09 GMT</pubDate><enclosure url="assets/l14-register-renaming-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-register-renaming-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Superscalar-Hardware-ENG]]></title><description><![CDATA[<img src="assets/l14-superscalar-hardware-eng.svg" target="_self">]]></description><link>assets/l14-superscalar-hardware-eng.html</link><guid isPermaLink="false">assets/L14-Superscalar-Hardware-ENG.svg</guid><pubDate>Tue, 14 Apr 2026 08:54:49 GMT</pubDate><enclosure url="assets/l14-superscalar-hardware-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-superscalar-hardware-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Coffee-Shop-ENG]]></title><description><![CDATA[<img src="assets/l14-coffee-shop-eng.svg" target="_self">]]></description><link>assets/l14-coffee-shop-eng.html</link><guid isPermaLink="false">assets/L14-Coffee-Shop-ENG.svg</guid><pubDate>Tue, 14 Apr 2026 08:54:07 GMT</pubDate><enclosure url="assets/l14-coffee-shop-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-coffee-shop-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Photo-Meltdown-Logo]]></title><description><![CDATA[<img src="assets/l14-photo-meltdown-logo.png" target="_self">]]></description><link>assets/l14-photo-meltdown-logo.html</link><guid isPermaLink="false">assets/L14-Photo-Meltdown-Logo.png</guid><pubDate>Tue, 14 Apr 2026 08:48:34 GMT</pubDate><enclosure url="assets/l14-photo-meltdown-logo.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-photo-meltdown-logo.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Photo-Spectre-Logo]]></title><description><![CDATA[<img src="assets/l14-photo-spectre-logo.png" target="_self">]]></description><link>assets/l14-photo-spectre-logo.html</link><guid isPermaLink="false">assets/L14-Photo-Spectre-Logo.png</guid><pubDate>Tue, 14 Apr 2026 08:48:33 GMT</pubDate><enclosure url="assets/l14-photo-spectre-logo.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-photo-spectre-logo.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Photo-Clock-Speed-Chart]]></title><description><![CDATA[<img src="assets/l14-photo-clock-speed-chart.png" target="_self">]]></description><link>assets/l14-photo-clock-speed-chart.html</link><guid isPermaLink="false">assets/L14-Photo-Clock-Speed-Chart.png</guid><pubDate>Tue, 14 Apr 2026 08:47:55 GMT</pubDate><enclosure url="assets/l14-photo-clock-speed-chart.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-photo-clock-speed-chart.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Photo-IBM-360-91]]></title><description><![CDATA[<img src="assets/l14-photo-ibm-360-91.jpg" target="_self">]]></description><link>assets/l14-photo-ibm-360-91.html</link><guid isPermaLink="false">assets/L14-Photo-IBM-360-91.jpg</guid><pubDate>Tue, 14 Apr 2026 08:47:46 GMT</pubDate><enclosure url="assets/l14-photo-ibm-360-91.jpg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-photo-ibm-360-91.jpg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Photo-Itanium-Die]]></title><description><![CDATA[<img src="assets/l14-photo-itanium-die.jpg" target="_self">]]></description><link>assets/l14-photo-itanium-die.html</link><guid isPermaLink="false">assets/L14-Photo-Itanium-Die.jpg</guid><pubDate>Tue, 14 Apr 2026 08:47:45 GMT</pubDate><enclosure url="assets/l14-photo-itanium-die.jpg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-photo-itanium-die.jpg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Photo-Pentium4-Die]]></title><description><![CDATA[<img src="assets/l14-photo-pentium4-die.jpg" target="_self">]]></description><link>assets/l14-photo-pentium4-die.html</link><guid isPermaLink="false">assets/L14-Photo-Pentium4-Die.jpg</guid><pubDate>Tue, 14 Apr 2026 08:47:43 GMT</pubDate><enclosure url="assets/l14-photo-pentium4-die.jpg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-photo-pentium4-die.jpg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Photo-M1-Die]]></title><description><![CDATA[<img src="assets/l14-photo-m1-die.jpg" target="_self">]]></description><link>assets/l14-photo-m1-die.html</link><guid isPermaLink="false">assets/L14-Photo-M1-Die.jpg</guid><pubDate>Tue, 14 Apr 2026 08:47:33 GMT</pubDate><enclosure url="assets/l14-photo-m1-die.jpg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-photo-m1-die.jpg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-ENG. Instruction-Level and Thread-Level Parallelism]]></title><description><![CDATA[Course: Computer Systems Architecture
Topic: Pipelining, Hazards, Superscalar, Out-of-Order Execution, Speculation, SMT
Learning Outcomes:
LO5: Analyze performance and bottlenecks of computer systems
LO6: Explain parallelism, pipelining, and heterogeneous computing Hook. How does a 3 GHz CPU execute 8+ instructions per cycle? And why did that same trick give us Spectre?
This lecture is the story of how architects squeezed every drop of parallelism out of a single thread — pipelining, forwarding, branch prediction, superscalar, out-of-order execution, speculation, and simultaneous multithreading — and how one of those optimizations, once assumed harmless, turned out to leak your secrets through the cache. The one idea to take away: architectural state is not the whole machine. Microarchitectural state — caches, predictors, TLBs — is real, observable through timing, and from 2018 onward, part of the threat model of every serious system.
Back in L02 and L03 you built a mental model of a CPU that goes like this: the processor fetches an instruction from memory, decodes it into control signals, executes the operation in the ALU, possibly accesses memory for a load or store, and finally writes the result back into a register. Five conceptual stages. Every instruction walks through all five of them, in order, one after another. That model is correct. It is also, as a performance proposition, a disaster.Here is why. In a strictly single-cycle design, the clock period must be long enough for the entire datapath — from the first flip-flop edge that latches the program counter all the way through register writeback — to settle before the next rising edge. The clock can't tick again until the slowest instruction has finished its slowest stage. Suppose each of our five stages has a propagation delay like this:A 5.0 ns clock period is 200 MHz. One instruction every 5 ns, forever. That's our ceiling.Students often ask the obvious follow-up: "Fine, so just make the clock faster." You can't, and the reason is physical. Each gate in that datapath is already switching as fast as its transistors allow; the signals are propagating through wires at a fraction of the speed of light, and combinational logic has a minimum settling time set by capacitance and resistance. You can't just turn a knob labeled "clock frequency" without reducing the work done per cycle. The clock period is the sum of gate delays along the critical path — the longest chain of combinational logic between two flip-flops — and you cannot clock faster than that path can propagate a signal and be latched reliably. Shrinking transistors helps, and we've ridden that wave for decades, but within a given process node the critical path is what it is.So if you can't shrink the cycle, the only way to get more work done is to do more per cycle. And here's where the single-cycle design looks wasteful in a way that should make you itch. Picture the datapath while instruction K is in its writeback stage. The register file is being written. What is the fetch unit doing? Nothing. It is sitting there, powered, clocked, and idle, waiting for the next rising edge so it can go get instruction K+1. What is the decoder doing? Nothing. The ALU? Nothing. The memory unit? Nothing. Four of five stages are idle at any instant. The entire CPU is a five-person assembly line where four workers stand and watch while the fifth does their job, then they rotate one station down and repeat.From a hardware-utilization point of view, that is an 80% waste. The transistors are there. The wires are there. The power is being burned. And most of the silicon is spinning its wheels waiting for its turn. If you could somehow keep all five stages busy simultaneously — fetching instruction K+4 while decoding K+3 while executing K+2 while K+1 accesses memory while K writes back — you'd do five times the work per cycle without the clock period changing at all.That's a 5× speedup hiding in plain sight. Not from a faster clock, not from a smarter ALU, not from a new ISA — just from noticing that the stages are independent pieces of circuitry that happen to be serialized by policy, not by physics.To make that waste concrete, here is the piece of code we will follow through every execution model in this lecture — a tiny x86-64 loop that sums an integer array. It's deliberately chosen to have a mix of dependencies and independent work so that every instruction-level parallelism (ILP from here on — the general idea of executing multiple instructions from a single thread at the same time) trick we introduce has something to bite on:; Summing an int array: sum = array[0] + array[1] + ... + array[n-1]
; rsi = base pointer, rcx = index (starts at 0), rdx = n, ebx = running sum loop: mov eax, [rsi + rcx*4] ; I1: load array[i] into eax add ebx, eax ; I2: sum += eax inc rcx ; I3: i++ cmp rcx, rdx ; I4: compare i with n jne loop ; I5: branch back if not equal
Five instructions per iteration. One load, two ALU ops, one compare, one branch. Now picture running those five instructions through the single-cycle datapath we just described, where each instruction must walk through all five stages before the next one begins:<img alt="Spacetime diagram of the running example executing on a strictly sequential single-cycle datapath. Each of the five instructions walks through all five pipeline stages in turn, one at a time, for a total of 25 cycles — at every moment four of the five stages sit idle." src="assets/l14-running-example-sequential-eng.svg" target="_self" style="width: 700px; max-width: 100%;">Twenty-five cycles to retire five instructions. CPI = 5.0. On every cycle of those twenty-five, exactly one stage is doing useful work and the other four are standing around waiting their turn. That is the 80% waste, drawn on the wall. This is the problem. Everything from the next section onward is accumulated machinery trying to claw back the four-fifths of the silicon this diagram is lighting up for nothing.So the question this lecture starts from is simple, almost stupid:
What if we could keep all five stages busy all the time?
Hold that thought. The rest of the story — every technique in this lecture, every hazard, every predictor, every reorder buffer, every security bug we'll look at — flows from the consequences of actually answering it.Picture a busy coffee shop during morning rush. The bar has four distinct stations: take the order and mark the cup, pull the espresso shot, steam and pour the milk, hand off to the customer. Suppose each station takes about 30 seconds. If a single barista insists on walking one drink all the way through all four stations before starting the next order, every customer waits two minutes and a finished drink appears on the counter every two minutes. No real café during rush hour works this way. The moment the first order is marked, the next customer steps up to order while the espresso machine begins pulling the first shot. By the time the first drink is handed off, a second is having its milk steamed, a third is pulling espresso, and a fourth is just being ordered. After a 90-second warm-up, a finished drink lands on the counter every 30 seconds — the speed of the slowest single station — not every two minutes. Any individual customer still waits two minutes for their drink; that latency is unchanged. What changed is the throughput. The café now serves four times as many drinks per hour without making any single step faster. Henry Ford's 1913 moving assembly line is the same trick with cars: chassis move past stationary workers, each one doing a small task, and a finished Model T rolls off every 93 minutes instead of twelve hours. The lesson is the same in both cases. The trick is not to make any individual step faster. The trick is never to let a station sit idle.<br>That's <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Instruction_pipelining" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Instruction_pipelining" target="_self">instruction pipelining</a>. We take our five-stage datapath from §1 and, between each pair of stages, we drop in a pipeline register — a bank of flip-flops that latches the intermediate state at the end of each clock cycle. Once those boundaries are in place, stage 2 can start working on instruction K+1 the moment stage 1 hands off instruction K to it. Each stage becomes an independent worker with its own "parts bin" (the input pipeline register) and its own "outgoing conveyor" (the output pipeline register).The canonical RISC pipeline has five stages, the same five we used in §1:
IF — Instruction Fetch. Read the instruction bytes from the I-cache at the address in the program counter, and update the PC to point at the next instruction.
ID — Instruction Decode and Register Read. Crack the instruction bits into control signals: opcode, source registers, destination register, immediate. Read the source operands out of the register file.
EX — Execute. Feed the operands into the ALU. For an add or shift or compare, this is where the math happens. For a load or store, this is where the effective address is computed.
MEM — Memory Access. If the instruction is a load or store, hit the D-cache here. For ALU-only instructions, this stage is a no-op pass-through (the value rides along in the pipeline register).
WB — Writeback. Write the ALU result or loaded value back into the destination register in the register file.
IF, ID, EX, MEM, WB. Memorize those five letters. Every diagram in this lecture uses them.Here's the single most important idea in pipelining, and the one that trips up most students on their first pass:
Pipelining does not make any single instruction faster. It makes instructions finish more often.
The latency of one instruction — the time from its IF to its WB — is still roughly 5 cycles, same as before. A single add walking through an empty pipeline arrives at writeback no sooner than it would on an un-pipelined machine. What changes is the throughput. Once the pipeline is full, every cycle, one instruction completes and another enters. The steady-state rate is one instruction per cycle, even though each individual instruction still needs five cycles end-to-end.That gap between latency and throughput is the whole reason pipelining works. We are willing to wait a few cycles for any particular result, as long as results keep rolling off the end of the line every cycle.And now revisit the clock period. In the single-cycle design of §1, the clock period was the sum of all stage delays, because one cycle had to accomplish the entire instruction. In a pipelined design, each stage gets its own cycle, so the clock period only has to cover the slowest single stage (plus a little pipeline-register overhead we'll ignore for now):That's roughly a 3.3× speedup from the clock-period shrink alone, even before we count the per-cycle throughput gain. In textbook examples with balanced stages, pipelining is quoted as a clean 5× win for a 5-stage pipeline. Real pipelines aren't perfectly balanced, but the order of magnitude is right: if the slowest stage is about 1/N of the sum, an N-stage pipeline gets you close to an N× speedup.The performance metric we'll use for the rest of the lecture is CPI — cycles per instruction. A perfect pipeline hits CPI = 1: one instruction completes every cycle. That's our new ceiling, replacing the old single-cycle ceiling of CPI = 5. Everything in §3 onward is about the gap between the ideal CPI of 1 and the messy CPI you actually get when real code flows through real silicon — because, as you're about to see, pipelining creates new problems in the very act of solving the old one.Back to the running example from §1 — the five-instruction array-sum loop. In §1 we watched it limp through a sequential datapath at CPI = 5. Now drop the same five instructions into our freshly-pipelined CPU. Naively, each instruction enters IF one cycle behind its predecessor, and after a 4-cycle warmup the pipeline is full and one instruction finishes every cycle. That would give us roughly 9 cycles for this iteration and CPI ≈ 1.Except it doesn't. Not yet. This pipeline has no tricks — no forwarding, no branch prediction, nothing fancy. It's a dumb five-station assembly line that honors the rule "a dependent instruction may not read a register until the producer has actually written it back in WB." Let's see what the same five instructions look like when we enforce that rule honestly:<br><img alt="Same running example on a five-stage pipeline without forwarding. Instructions now overlap, but bubble cells appear where I2 waits for I1's load result and where the branch I5 flushes mispredicted fetches — CPI drops to about 2." src="assets/l14-running-example-pipelined-eng.svg" target="_self" style="width: 700px; max-width: 100%;">Three separate problems just showed up. Let's name them.Load-use stall (I1 → I2). I1 is a load. The value of eax isn't actually available until the end of I1's MEM stage, and in this naive model it doesn't leave the pipeline until WB. I2 needs eax at the start of its EX stage. I2 entered IF one cycle after I1, so by the time I2 is ready to execute, I1's value is still sitting in the MEM/WB pipeline register — not yet visible in the register file. I2 has no choice but to stall for two cycles until I1's writeback completes. Those two empty slots in the grid are pipeline bubbles: the stage is doing nothing useful but the cycles still tick.ALU-use stall (I3 → I4). I3 computes a new rcx. I4 reads rcx. Same story, slightly less painful because I3's result is ready at the end of its EX stage, not at MEM — but we still have to wait for it to walk through MEM and WB before I4 can read the register file. Another two-cycle bubble.Branch bubble (I5). I5 is a conditional jump. Its outcome depends on flags set by I4, and the branch target (or fall-through PC) isn't known until sometime around I5's EX stage. But the fetch unit doesn't know that — it wants to fetch a new instruction every cycle, and for a cycle or two after I5 enters the pipeline, it has no idea what PC to fetch from. Either it stalls (guaranteed bubble) or it guesses and then throws the work away if the guess is wrong. Either way, you lose a couple of cycles.Count the damage. Five instructions, but with the stalls we've spent roughly 10 cycles to complete one iteration. CPI ≈ 2.0. Not the textbook 1.0.That's the whole moral of this section. Pipelining is a beautiful idea — it takes the single-cycle factory and schedules every worker to be busy simultaneously — but the moment you let multiple instructions live inside the machine at the same time, they start stepping on each other. Instructions that were harmlessly sequential in the old model are suddenly racing to read values that haven't been written yet. The pipeline doesn't enforce dependencies any more than an assembly line enforces "don't install the wheels before the axle exists" — that's a scheduling problem, and pipelining just made scheduling the hard part.We've moved from CPI ≈ 5 to CPI ≈ 2. That's a real win — more than 2× faster — but we're still a long way from the CPI = 1 the throughput theorem promised. The promised ceiling exists, we just haven't earned it yet. The reason is that we've created a whole new category of problem that didn't exist in §1: hazards. Instructions overlap, but some of those overlaps are illegal, and every illegal overlap becomes a stall. §3 is going to give those hazards names and teach you how to spot them at a glance — because once you can see them, you can start designing the hardware that makes them cheaper.Section 2 sold you the dream: five stages, five instructions in flight, one instruction finishing every cycle. CPI = 1. Factory throughput. Done.Then the grid stopped being clean. Bubbles appeared. The add had to wait for the mov. The cmp had to wait for the inc. And at the bottom of the loop, the jne froze the fetch stage because nobody knew where to fetch from next.<br>Those bubbles have a name. They are hazards — situations where "the next instruction cannot execute in the following clock cycle," to borrow the <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Hazard_(computer_architecture)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Hazard_(computer_architecture)" target="_self">Wikipedia definition</a>. A hazard is any structural, logical, or control reason the pipeline cannot keep its one-instruction-per-cycle promise. Every real CPU spends enormous effort either avoiding hazards, hiding them, or recovering from them when a prediction turns out wrong.This section gives the hazards names and a taxonomy. Not because the taxonomy is interesting on its own, but because §4, §5, and §6 each attack a specific category and it helps to know which one. Think of the rest of the lecture as a series of answers. This section is the question.A structural hazard is a resource conflict. Two instructions in different pipeline stages want the same piece of hardware at the same time, and only one can have it.The classic case: on an early pipelined design with a single memory port, cycle N wants to fetch instruction I5 from memory (IF stage) while cycle N also wants to read [rsi + rcx*4] from memory (MEM stage of I1). One port, two customers. Somebody stalls.Structural hazards are mostly a historical worry. Modern designs duplicate whatever resource is contended: separate instruction and data caches (the old split-L1 pattern you saw in L11), multiple ALUs, multiple memory ports, multiple write ports on the register file. If a structural hazard remains after the architect is done, it's usually a deliberate cost/performance tradeoff on a rarely-used unit (a shared floating-point divider, say). Don't dwell on them. Name-check, move on.Data hazards arise from dependencies between instructions on the same register or memory location. The hazard taxonomy comes from asking: in program order, who reads and who writes, and what happens if we reorder them in time?There are three shapes, and they are not equally important.RAW — Read After Write (true dependency). Instruction B reads a value that instruction A produces. B literally needs A's output. This is the real thing — a RAW is a statement about the computation, not about register names. Look at the running example:loop: mov eax, [rsi + rcx*4] ; I1: load array[i] into eax add ebx, eax ; I2: sum += eax inc rcx ; I3: i++ cmp rcx, rdx ; I4: compare i with n jne loop ; I5: branch back if not equal
I2 reads eax. I1 writes eax. That's a RAW. I2 cannot compute ebx + eax until I1's load has produced a value for eax, and on a naive pipeline I2 enters EX before I1 has even finished MEM. Result: stall. This specific flavor of RAW — a load feeding an immediately-following ALU instruction — is called a load-use hazard, and it is the hardest data hazard to eliminate. We'll come back to it in §4.I4 reads rcx. I3 writes rcx. Another RAW. I5 reads the flags that I4 wrote. Another RAW (on the implicit flags register). RAWs are everywhere in ordinary code because that's just what it means to compute: take a value, do something with it, pass the result to the next step.You cannot make RAWs go away. A RAW is a fact about the program's dataflow. The best you can do is minimize the time cost of satisfying it — which is exactly what forwarding does.WAR — Write After Read (anti-dependency). Instruction B writes to a register that instruction A reads — but A is supposed to read the old value. If the pipeline lets B overtake A, A might accidentally see B's new value.I_a: add rax, rbx ; reads rbx
I_b: mov rbx, 0 ; writes rbx
If I_b somehow executed before I_a read rbx, I_a would see zero instead of the old rbx. That's a WAR. On a simple in-order pipeline, WARs don't even happen — reads complete in order — so we don't worry about them yet. They only become relevant once we let the machine reorder instructions (§6).WAW — Write After Write (output dependency). Two instructions both write the same register. Program order says the second one wins (its value is what's left afterwards). If the pipeline lets them finish out of order, the wrong one might survive.I_a: mov rax, 1
I_b: mov rax, 2 ; final value of rax must be 2
Again, trivial on in-order. Scary on out-of-order.Here is the key observation, and it frames the entire second half of the lecture: WAR and WAW are not real data dependencies. They are artifacts of the fact that we're reusing register names. rbx is a name — the programmer or compiler picked it, not some law of the universe. If the hardware secretly gave each write its own private copy of rbx, WAR and WAW would vanish: no two instructions would even be touching the same physical storage. That trick is called register renaming, and it is exactly what the out-of-order machinery in §6 does. So for the rest of §3 and all of §4 we will focus almost exclusively on RAW. WAR and WAW get a free pass until §6, where they evaporate.80% of a real CPU's data-hazard energy goes into RAWs. Spend your attention there.A control hazard (or branch hazard) comes from conditional branches, indirect jumps, and returns — anything where the fetch stage doesn't know what PC to fetch from until a later stage has done some work.Look at I5 in the example: jne loop. Depending on the flags set by I4's cmp, execution either jumps back to loop or falls through to whatever comes after. On a textbook 5-stage pipeline, the branch is resolved at the end of EX. That's stage 3. Meanwhile, stages 1 and 2 (IF and ID) have been happily fetching and decoding the two instructions that came after jne in memory — instructions that, if the branch is taken, are completely wrong and must be thrown away.Cycle: 1 2 3 4 5 6
I5 jne IF ID EX ME WB ; branch resolved here, at end of EX (cycle 3)
I? (wrong) IF ID -- ; speculatively fetched, then squashed
I? (wrong) IF -- ; same fate
I? (correct) IF ID ... ; finally fetched from branch target
Two cycles wasted per branch, on a 5-stage pipeline that resolves branches in EX. This isn't a mistake in the design — it's an unavoidable consequence of fetching faster than you can decide. The cost per branch is called the misprediction penalty, and it scales with how deep the pipeline is. A 5-stage pipeline loses 2–3 cycles. We'll see in §4 that a 31-stage Pentium 4 lost twenty cycles per mispredicted branch, which is a major reason that microarchitecture failed.Branches are common — typically 15–25% of instructions in integer code — so a control hazard penalty of even one cycle is very expensive. This is why branch prediction (§4) exists, and why it is arguably the single most important microarchitectural technique in modern CPU design.Let's directly serve LO5 (performance analysis, bottleneck math) with a back-of-envelope CPI calculation. Suppose:
20% of instructions are loads, each causing a 1-cycle load-use stall (forwarding keeps it at 1, not 2)
20% of instructions are branches, with a 2-cycle misprediction penalty, and the predictor is wrong 10% of the time
The remaining 60% flow cleanly at CPI = 1
Then:CPI ≈ 1 (base, one instr per cycle) + 0.20 · 1 (load-use penalty) + 0.20 · 2 · 0.10 (branch misprediction penalty) = 1 + 0.20 + 0.04 = 1.24
A CPI of 1.24 means throughput of about 0.81 instructions per cycle — a 19% tax on the idealized pipeline, entirely paid to hazards. And that's with forwarding and with a 90%-accurate branch predictor. Without them the numbers get ugly fast: load-use becomes 2 cycles, branch misprediction rate climbs toward 50% on hard-to-predict branches, and CPI can easily balloon to 2.0 or worse. That is what §2's "pipelined, no forwarding" grid was showing you in pictures.Three categories of hazards, and a clear pecking order: structural hazards are mostly solved by duplicating hardware; data hazards are dominated by RAW, which we can't erase but can cheapen; control hazards are dominated by branches, which we attack with prediction. That's §4.Beyond §4, WAR and WAW will come back as real problems once we start executing out of order — and we'll solve them with register renaming (§6). Structural hazards will reappear in a new disguise in §5 (superscalar) when we ask what happens if two instructions want the single ALU on the same cycle. The categories don't go away; they just mutate as the machine gets more aggressive.For now: names, shapes, and a CPI formula. On to the fixes.§3 gave you the bad news. §4 is the good news: most of the pipeline's hazard cost can be eliminated — or at least deeply hidden — with surprisingly cheap hardware tricks. Two in particular. Forwarding attacks data hazards; branch prediction attacks control hazards. Together they take a CPI of ~2.0 on the naive pipeline down to ~1.2 on a machine that is still fundamentally single-issue, in-order, and not doing anything clever beyond this section.This is Level 3 of the running example.Look again at the load-use hazard from §3:I1: mov eax, [rsi + rcx*4] ; eax is loaded in MEM
I2: add ebx, eax ; eax is needed in EX
On the textbook 5-stage pipeline (IF, ID, EX, MEM, WB), I1's loaded value only lands in the register file at the end of WB. That's stage 5. Meanwhile, I2 wants to read eax in ID (stage 2), and to compute with it in EX (stage 3). If I2 is one cycle behind I1, then when I2 hits EX, I1 is in MEM — and the value I2 needs is literally sitting at the output of MEM, on a wire, one clock cycle too late to reach the register file.So don't go through the register file. Forwarding (a.k.a. bypassing or operand forwarding) adds direct wires from the outputs of later pipeline stages straight back to the inputs of EX. When the hazard detection logic notices that I2's source register matches I1's destination register, it routes the value from wherever it currently is — MEM/WB pipeline register — directly into I2's ALU input, instead of what's in the register file. I2 never waits; it gets the fresh value on the same cycle it would otherwise read the stale one.<br><img alt="Datapath snapshot at a single cycle: I1 sits in the MEM stage while I2 sits in the EX stage. The &quot;normal&quot; path — MEM to WB to register file write to ID register read — would deliver I1's loaded value two cycles too late. A solid orange forwarding wire bypasses that entire detour, routing the value directly from MEM's output back into EX's input on the very same clock edge." src="assets/l14-forwarding-bypass-eng.svg" target="_self" style="width: 700px; max-width: 100%;">Which hazards does forwarding kill?
ALU-to-ALU (EX → EX): completely. If I1 computes something in EX and I2 needs it in EX one cycle later, the output of I1's ALU feeds I2's ALU input directly. Zero bubbles. In the running example, I3's inc rcx feeding I4's cmp rcx, rdx is exactly this — a RAW that used to cost two stall cycles, now costs zero.
ALU-to-store (EX → MEM): completely. A computed value can be forwarded straight into the store data.
Load-use (MEM → EX): almost. And this is the important caveat.
The load-use hazard survives forwarding as one unavoidable bubble. Here's why: a load doesn't produce its value until the end of MEM. If the very next instruction needs that value as an ALU input, it would need the value at the start of EX, one stage earlier than MEM. There is no wire you can run forward in time. The consumer has to wait one cycle. Forwarding can then deliver the value from MEM to EX across that one-cycle gap, but the gap itself is physical and can't be closed.So the running example's I1→I2 load-use costs exactly one cycle, not two, on Level 3. The other two RAWs (I3→I4, I4→I5) cost zero. This is huge.When forwarding can't save you — like the load-use case — the pipeline has to actually stall. Mechanically, a tiny block of logic called the hazard detection unit sits at the end of ID and checks: "is the instruction I'm about to send to EX reading a register that a load currently in EX is writing?" If yes, it freezes the PC and the IF/ID register for one cycle and injects a bubble — a no-op — into the ID/EX register. From the pipeline's point of view, a harmless NOP flowed through, and the real I2 gets a second chance next cycle.That's all stalling is: freezing upstream stages, inserting NOPs downstream, for as many cycles as the hazard demands. It's a hammer, but a surgical one — the detection logic is maybe a few hundred gates, and it runs in parallel with decode.One bubble per load-use hurts. But the compiler can often eliminate it for free, by rescheduling an independent instruction into the slot right after the load. If the compiler can find some work that doesn't depend on the loaded value and that doesn't feed the load's address computation, it slides that work into the load-use delay slot. The inc or whatever it is occupies the gap, the hardware stall logic never fires, and the bubble disappears. This is called instruction scheduling, and it's a standard trick in any optimizing compiler backend. Notice it is not a replacement for hardware forwarding; it is a cheap optimization on top of hardware forwarding. Compiler + forwarding is the real answer.Concretely, here is the running example as a naive compiler might first emit it — the textbook order, where add sits directly after mov and eats a bubble on every iteration:; Before: naive ordering — 1-bubble load-use stall per iteration
loop: mov eax, [rsi + rcx*4] ; I1: load array[i] (produces eax at end of MEM) add ebx, eax ; I2: sum += eax &lt;- load-use stall, 1 bubble inc rcx ; I3: i++ cmp rcx, rdx ; I4: compare i, n jne loop ; I5: branch
Now watch an optimizing compiler reschedule the body. It notices that inc rcx neither reads eax nor contributes to the load's address (the address was already computed inside I1's EX stage using the old rcx), so it is safe to move inc rcx up between the load and the add:; After: scheduled ordering — bubble eliminated, same semantics
loop: mov eax, [rsi + rcx*4] ; I1: load array[i] (old rcx captured here) inc rcx ; I3: i++ &lt;- fills the load-use slot add ebx, eax ; I2: sum += eax (eax is ready now, no bubble) cmp rcx, rdx ; I4: compare new i, n jne loop ; I5: branch
Same five instructions, same observable effect on ebx and rcx at the end of every iteration, but the load-use delay slot is now filled with useful work instead of a bubble. The compiler needed two facts to prove the swap was legal: inc rcx doesn't read eax (no RAW on the loaded value), and inc rcx doesn't change the address that I1 was loading from (I1's effective address was already latched into the pipeline when it entered EX, so the later inc can't retroactively move the load). On a real CPU, gcc -O2 or clang -O2 will do this automatically — the reordered sequence is what actually ends up in the binary.We'll keep the original textbook ordering in the running example for pedagogical clarity, but a real binary on a real CPU would not leave the bubble in place.Control hazards are worse than data hazards, because a control hazard doesn't stall one instruction for one cycle — it starves the entire fetch engine until the branch resolves. On a 5-stage pipeline the cost is 2–3 cycles per branch. On a deep pipeline it's catastrophic.The fix: guess. When the fetch stage sees a branch and doesn't yet know which way it goes, it picks a direction, fetches as if the guess were right, and starts speculatively pushing instructions into the pipeline. If the guess turns out correct, no time was lost. If it turns out wrong, the speculative instructions are squashed (flushed out of the pipeline as bubbles), and fetch restarts at the correct target.<br>This only works if the guess is usually right. The <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Branch_predictor" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Branch_predictor" target="_self">Wikipedia article on branch predictors</a> describes it well: "a digital circuit that tries to guess which way a branch will go before this is known definitively." Modern predictors achieve 95–99% accuracy on typical code. That's the magic number that makes everything else in this lecture worthwhile.Let's build up the predictor in stages.Static prediction is the crudest: the compiler or the architecture just says "assume all branches are taken" (or "not taken", or "backward branches taken, forward branches not"). It works surprisingly well for loops — a backward branch at the bottom of a loop is taken on every iteration except the last one, so "backward-taken" is right nearly always. But it fails on if branches with data-dependent conditions, because the compiler can't know at compile time which way the data will go. Static isn't enough.Dynamic prediction uses runtime history: look at what this branch did last time and predict it'll do the same thing again. That turns out to work very well because branches in real programs are overwhelmingly non-random — most loops run many iterations, most if-conditions have a dominant direction, and so on.The simplest dynamic scheme: one bit per branch, remembering "taken" or "not taken" from the last occurrence. Predict the same again.This is surprisingly decent for loops — a branch that's taken 99 times in a row stays predicted "taken" — but it has a nasty failure mode: it mispredicts twice every time a steady pattern flips. Consider a branch that alternates T, N, T, N, T, N, ... A 1-bit predictor will be wrong on every single instance. And the common case of a loop running N times gives you two mispredicts: one on the last iteration (loop exits, predictor said "taken", actual "not taken") and one on the next entry to the loop (predictor now says "not taken", loop is taken again). For short loops invoked many times, that's painful.This is the predictor you must be able to draw from memory on an exam. It's a four-state finite state machine, one per tracked branch:<br><img alt="Finite state machine of a 2-bit saturating counter branch predictor. Four states arranged in a ring: Strongly Not Taken, Weakly Not Taken, Weakly Taken, Strongly Taken. Taken/Not-Taken arrows walk the counter back and forth; a single wrong prediction only nudges the state one step instead of flipping the whole prediction." src="assets/l14-branch-predictor-2bit-eng.svg" target="_self" style="width: 700px; max-width: 100%;">Two bits, four states, one transition per bit of outcome. "Strongly" states only lose strength on a wrong prediction; you have to be wrong twice in a row to flip the overall prediction.The prediction rule: predict taken if the counter is in either "taken" state (weakly or strongly), not taken otherwise. The update rule: on each actual outcome, move one step toward the outcome, saturating at the ends.<br>Why is this so much better than 1-bit? Because it absorbs transient noise. A loop that runs 100 iterations and then exits: the predictor is in "Strongly Taken" by iteration 2 and stays there. When the loop exits, the predictor is wrong once (loop-exit misprediction) and drops to "Weakly Taken" — but it stays predicting "taken". When the loop is re-entered (say, in an outer loop), the first iteration is correctly predicted taken. The 2-bit counter costs one misprediction on loop exit; the 1-bit counter cost two. The <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Branch_predictor" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Branch_predictor" target="_self">Wikipedia branch predictor article</a> cites bimodal predictors hitting ~93.5% accuracy on SPEC'89.Predicting direction (taken vs. not-taken) is only half the story. If you predict a branch taken, you also need to know where to jump from, on the same cycle, so the fetch stage can redirect immediately. Waiting until decode to figure out the target address wastes cycles — you've already committed to speculating.The solution is a Branch Target Buffer (BTB): a small cache indexed by the branch's PC, holding the last-observed target address. When fetch sees an instruction whose PC hits in the BTB, it knows "this was a branch last time, and it went here" — and can begin fetching from here on the very next cycle, before the branch has even been decoded. BTBs are essential for indirect branches (function pointers, virtual calls, returns) where the target can't be computed from the instruction encoding alone.A 2-bit counter says "this branch tends to go this way." But branches in real programs are correlated with each other — if the last few branches went T, T, N, that context tells you something about what this branch will do. A two-level adaptive predictor tracks a short history of recent branch outcomes (a shift register of the last n taken/not-taken bits) and uses that history to index into a table of 2-bit counters. Each history pattern has its own counter.This is the technique Yeh and Patt introduced in 1991, and it's the foundation of every serious modern predictor. Accuracy climbs from ~93% to ~97% on standard benchmarks. Variants include global history (one shift register shared across all branches, catches correlations between different branches) and local history (one shift register per branch, catches intra-branch patterns like "taken every third time"). Modern high-performance designs use hybrid schemes like TAGE (multiple tables indexed with different history lengths) and perceptron predictors (linear classifiers over history bits). You do not need the details for this course. You do need to know that modern predictors are astonishingly good.Some branches are better predicted with local history, some with global. A tournament predictor runs two (or more) sub-predictors in parallel and has a meta-predictor — itself a small table of counters — that learns, per branch, which sub-predictor is winning, and uses that one's answer. McFarling proposed this in 1993 and it became standard on the Alpha 21264 and beyond. It's a recurring pattern in computer architecture: when you can't decide between two good ideas, run both and let a third circuit learn which one to trust where.When the predictor is wrong, all the speculative instructions in the pipeline behind the branch must be flushed. The cost — the misprediction penalty — equals roughly the depth of the pipeline between fetch and the stage where the branch is resolved. On our 5-stage pipeline that's 2–3 cycles. On a 14-stage Pentium III it's ~10. On the infamous Pentium 4 Prescott — 31 pipeline stages, designed to hit extreme clock speeds — a single mispredict cost 20+ cycles. At a 95% prediction rate on branch-heavy code (~20% of instructions), the wasted cycles added up catastrophically: Prescott's real-world performance was often worse than the shorter-pipeline Pentium III at the same clock, because the branch penalty ate everything the clock gained. Intel eventually abandoned the "make the pipeline longer" strategy. The lesson is permanent: deep pipelines multiply the cost of every misprediction, and past some depth the math stops working.Modern cores sit at ~12–20 stages with &gt;97% prediction accuracy, which is roughly the sweet spot.After Labs 1 and 2 you have an extremely concrete mental model of a 5-stage pipeline — IF, ID, EX, MEM, WB, hand-wired against a small ALU with four instructions. That picture is correct, and it is also a cartoon. A real Intel Golden Cove or AMD Zen 4 core has something like 17 stages. Where do the extra twelve come from? The short answer: every one of the textbook five gets subdivided, because at 4–5 GHz a clock cycle is too short to do all the work the textbook crams into one stage.Here is a rough breakdown of how the five textbook stages expand in a modern high-performance x86 core. The exact number per stage varies by design (this is not Zen 4 exactly — it's a representative sketch), but the shape is what you should internalize:Add them up: roughly 4 + 4 + 4 + 4 + 2 = 18 stages, which is right in the modern ballpark. This is not padding — each line in the table is a real SRAM access, a real arithmetic operation, or a real dependency check that physically cannot fit in a shorter clock cycle at multi-gigahertz.Why split at all? Because shorter per-stage work means a shorter clock period, which means a higher clock frequency. The single-cycle datapath from §1 had a ~5 ns period and ran at 200 MHz. Subdivide into 18 stages where the longest stage is ~250 ps, and you can clock at 4 GHz. That's the throughput win — twenty times more instructions completed per second from the same silicon, assuming nothing stalls.Why not go deeper? Two costs grow with pipeline depth. First, the misprediction penalty grows linearly — that 18-stage pipeline flushes ~15 instructions on a bad branch, not 2. Second, hazards and forwarding get expensive — forwarding paths have to run across more physical distance on the die, the register file has more in-flight writes to track, the bypass network gets combinatorial. Pentium 4 Prescott hit 31 stages and discovered the hard way that beyond ~20, the marginal clock speed gained no longer offsets the branch flush cost on real code. Intel abandoned that strategy and has sat in the 14–20 range ever since.A mental model to hold on to: the 5-stage pipeline you built in lab is the same shape as a modern core. The modern core has just replaced each labeled box with a small pipeline of its own. IF isn't one cycle — it's a mini-pipeline of predict / translate / read / queue. EX isn't one cycle — it's schedule / read / compute / bypass. The high-level structure of "instructions flow left to right through named stages" is unchanged; what changed is the granularity.<br>If you ever want to see this in concrete detail for a real chip, Agner Fog's microarchitecture manuals and the <a data-tooltip-position="top" aria-label="https://en.wikichip.org/" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikichip.org/" target="_self">WikiChip</a> die-shot write-ups of specific cores (Skylake, Zen 3, Firestorm) document the per-stage layout for each generation. They are not required reading for this course — but if pipelining is the part of the lecture that fascinates you, they are where the rabbit hole gets genuinely deep.Same code, now on a pipeline with full EX→EX forwarding, MEM→EX forwarding, load-use detection that inserts one bubble, and a 2-bit branch predictor sitting in front of fetch with an entry for the jne loop branch.<br><img alt="The running example with forwarding bypasses and branch prediction enabled. Most stalls vanish; curved orange &quot;fwd&quot; arrows carry values from EX or MEM outputs back to later EX inputs. Only the load-use hazard between I1 and I2 survives as a single bubble. CPI is now around 1.2." src="assets/l14-running-example-forwarding-eng.svg" target="_self" style="width: 700px; max-width: 100%;">Count the bubbles: one. Not five, not three — one. The load-use stall at I2 is the only unrecoverable stall in the entire iteration. Forwarding made I3→I4 (the ALU-to-ALU RAW on rcx) free. The branch at I5 is speculatively predicted taken, the predictor has its entry in "Strongly Taken" after iteration 2, and fetch seamlessly continues to the next iteration with no pipeline flush.Across N iterations, the predictor is wrong exactly once (on the very last iteration, when the loop exits). If we call that cost a 2-cycle flush, the total cost of the loop is roughly:Cycles ≈ N × (5 instructions + 1 load-use bubble) + 2 (exit misprediction) = 6N + 2
That's 6 cycles for 5 instructions, so CPI ≈ 6/5 = 1.2 — right where Level 3 was supposed to land in the table from §1. Forwarding took the ALU RAWs to zero, the hazard detector took load-use from 2 bubbles to 1, and the 2-bit predictor made the branch effectively free for all but the last iteration. We haven't widened the pipeline, haven't broken program order, haven't added any execution units — and we've already moved from CPI ~5 (no pipeline) to CPI ~1.2.Notice what we did not do in §4: we never executed more than one instruction per cycle. Everything in this section was about making a 1-wide pipeline flow smoothly. Its ceiling is CPI = 1. You can polish forwarding and prediction forever and you will never get below that number, because no matter how perfect your prediction is, a single-issue machine issues at most one instruction per cycle.To go faster than CPI = 1 — to get throughput of 2, 3, 4 instructions per cycle — we have to fetch, decode, and issue more than one instruction each cycle. That is a genuinely different machine. It's called superscalar, and it's §5.Forwarding and branch prediction squeezed the bubbles out of the pipeline. The running example is down to about CPI ≈ 1.2 — one instruction finishing every cycle, plus a tiny bit of slack for the load-use hazard. That is the natural ceiling of a pipeline that issues one instruction per cycle. If you want to go faster, you have to issue more.<br>This is the superscalar idea: duplicate the backend, widen the frontend, and let multiple independent instructions start in the same cycle. A <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Superscalar_processor" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Superscalar_processor" target="_self">superscalar processor</a> "can execute or start executing more than one instruction during a clock cycle by simultaneously dispatching multiple instructions to different execution units." When it works, CPI drops below 1 — or equivalently, IPC (instructions per cycle) climbs above 1.A 2-wide superscalar core has two fetch slots, two decode slots, and at least two functional units that can run in parallel. A 4-wide core has four of everything. The frontend reads a window of adjacent instructions, the dependency-check logic figures out which ones can go together, and the backend fires them off in parallel.The first mainstream x86 superscalar was the Intel Pentium in 1993, with its famous U-pipe and V-pipe — two integer pipelines, with a set of hand-tuned rules about which instruction pairs could issue together. It was primitive by modern standards, but it proved the idea worked outside the mainframe world. By 1998, essentially every general-purpose CPU shipping was superscalar.Modern x86 cores are typically 4-6 wide at the decoder. Apple's M1 P-core (Firestorm) is 8 wide — we'll come back to why ARM gets away with that and x86 doesn't in §6.5.Widening the machine only helps if the program has adjacent independent instructions. Let's run the array-sum loop on a 2-wide in-order superscalar and see what happens.<br><img alt="The running example on a 2-wide in-order superscalar pipeline. Two parallel issue lanes are visible; Lane B handles independent instructions while Lane A stalls on the I1 to I2 dependency chain — the dep chain still serializes half the issue width." src="assets/l14-running-example-superscalar-eng.svg" target="_self" style="width: 700px; max-width: 100%;">Look at cycle 1. The frontend tries to fetch two instructions into the two lanes. It can pair I1 (load) with I3 (inc rcx) because I3 is independent of I1 — the 2-wide superscalar is happy to slide I3 forward past I2 as long as I3 doesn't depend on anything ahead of it. I2 has to stay in program order behind I1 because it uses eax, so I2 gets fetched on cycle 2 alongside I4. I5 trails alone on cycle 3.Pay careful attention to what the diagram is and is not telling you. It shows one iteration's instructions entering the pipeline across 3 fetch cycles — that is the pipeline's throughput number, the rate at which new instructions start. The last writeback lands at cycle 7 because each instruction still needs its own five-stage walk (plus I2's one-cycle load-use bubble) before retiring, and the issue in cycle 3 can't retire until cycle 7. Cycle 7 is the latency of one iteration end-to-end, not its throughput.The two numbers are different on purpose. For a long loop, the pipeline fill is a one-time cost: pay ~4 cycles of warm-up, then every 3 cycles a new batch of 5 instructions issues and 3 cycles later it retires. Across N iterations the total cost is roughly 4 + 3·N, not 7·N. That's where the CPI ≈ 3/5 = 0.6 comes from — it's a steady-state throughput claim, not "one iteration finishes in 3 cycles."The honest ceiling is actually a bit worse than 0.6, and it's worth seeing why. The next iteration's I1 (loading array[i+1]) depends on rcx, which I3 of the current iteration updates. So across iterations there is a cross-iteration serialization the diagram is quietly hiding — a real measurement would land closer to CPI ≈ 0.9, especially once you count the load-use bubble that still hasn't gone away. The 0.6 is the idealized ceiling the 2-wide machine could hit if there were no cross-iteration chain; 0.9 is what the array-sum loop actually delivers.That's better than 1.2, but it's nowhere near the 0.5 you might naively expect from "2 wide." The 2-wide frontend is half-utilized because the program has serial dependencies. Widening to 4 or 6 helps less than you'd hope, for the same reason: dependency chains serialize work that the hardware can't reorder.Stare at the running example for a moment. Yes, I1→I2 is a serial chain. But think one iteration ahead: the I1 of iteration k+1 (loading array[i+1] after inc rcx) is completely independent of the I2 of iteration k. They live in different dataflow chains. A machine that only looks at adjacent instructions in program order can't see this — iteration k+1's load is 5 instructions away, behind a branch.A machine that reorders instructions respecting data dependencies but not program order could launch iteration k+1's load while iteration k's add is still waiting. That's the payoff. Programs have parallelism; it's just not arranged in program order for the hardware's convenience.The question becomes: how do we build hardware that understands dependencies rather than sequence?<br>Before we answer that, a detour. In 2001 Intel and HP shipped <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Itanium" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Itanium" target="_self">Itanium</a> (IA-64), a VLIW/EPIC architecture built on exactly the opposite bet. Instead of making the hardware smart enough to find parallelism at runtime, Itanium made the compiler responsible: the ISA bundled three operations per 128-bit instruction word, with explicit parallelism hints, speculation bits, and predication. The pitch was "we'll extract ILP in software, so the CPU doesn't need expensive out-of-order logic."It failed. Hard. The core problem is that load latencies and branch outcomes are dynamic — a cache miss takes 20× longer than a hit, and the compiler has no way to know in advance which loads will miss. A static schedule has to assume worst-case latency everywhere, leaving the machine stalled on the common case. Hardware out-of-order execution (OoO from here on) wins because it schedules against actual observed latencies, not pessimistic estimates — the CPU looks at what's ready right now and fires instructions accordingly, while a compiler-generated schedule is frozen in time. Itanium was outsold by x86 OoO chips roughly 100-to-1, and Intel finally shipped its last Itanium CPU in 2021.The moral deserves to be memorized: ILP is best extracted dynamically, at hardware speed, against real runtime information. Static ILP is brittle.Dynamic ILP needs hardware that understands dependencies, not program order. That's the next section.OoO execution is the algorithmic heart of every serious CPU built since the mid-1990s. It is, at its core, an answer to two questions that sound contradictory:
How do we execute instructions as soon as their operands are ready, regardless of where they sit in the program?
How do we commit results to architectural state in program order, so that exceptions, debuggers, and context switches see a clean, consistent machine?
The first question is about going fast. The second is about staying correct. You need both — a fast machine that can't take a precise interrupt is unusable as a general-purpose CPU.<br>The answer, invented by Robert Tomasulo at IBM in 1967 for the System/360 Model 91 and refined by a generation of designers since, is the combination of <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Tomasulo%27s_algorithm" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Tomasulo%27s_algorithm" target="_self">Tomasulo's algorithm</a> (how to execute out of order correctly) and the <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Reorder_buffer" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Reorder_buffer" target="_self">reorder buffer</a> (how to commit back in order). Let's build it up.Recall from §3 that pipelines suffer three kinds of data hazards: RAW (true dependency), WAR (write-after-read), and WAW (write-after-write). RAW is fundamental — if I need the value you produced, I have to wait. But WAR and WAW are naming conflicts, not real dependencies. Two instructions that both want to write eax only conflict because they share the same name, not because either needs the other's data.Register renaming makes the conflict disappear. The architectural register eax is a name the programmer sees; internally, the CPU maintains a pool of hundreds of physical registers, and every instruction that writes eax gets allocated a fresh physical register. The rename table records "right now, architectural eax lives in physical register P37." When the next instruction overwrites eax, it gets P82 instead, and the old P37 lives on until all its readers have consumed it. Two writes to the same architectural register can now execute in parallel — they land in different physical registers.The numbers matter. x86-64 exposes 16 general-purpose architectural registers. A modern core holds 150–400 physical integer registers in its rename pool. In 2020/2021, Intel's Golden Cove has ~280; Apple's Firestorm has ~354. The extra registers are the raw material that OoO feeds on — more physical registers means more in-flight instructions with non-conflicting names.WAR and WAW vanish by construction. The only remaining data hazard is RAW, and RAW is exactly what dataflow execution is built to track.Picture the backend of an OoO core as a room full of instructions, each waiting for the values it needs. The waiting area is not a single queue — it's a set of reservation stations, one cluster per functional unit. When an instruction is dispatched from the frontend, it picks up residence in a reservation station attached to the unit it needs (integer ALU, load/store, FP multiply, whatever), and it brings along two things: any operand values that are already known, and tags for any operand that isn't ready yet. A tag is just a name for "the physical register that some other in-flight instruction is going to produce."From that moment on, the instruction is not waiting for a cycle to come — it's waiting for its tags to be resolved. The wait is purely data-driven.How do tags get resolved? By the Common Data Bus (CDB). When a functional unit finishes computing a result, it broadcasts a (tag, value) pair on the CDB. Every reservation station in the machine watches the bus. If a station was waiting on that tag, it latches the value into the corresponding operand slot. The instant all of a station's operands are filled, the instruction fires and goes to its functional unit.This is associative matching — the hardware equivalent of dataflow execution. Program order is gone; the only thing that decides when an instruction runs is whether its inputs have arrived. A load that's waiting on a cache miss parks at its reservation station without blocking anything else. Instructions downstream that don't depend on that load keep flowing through the machine.Every instruction in an OoO core passes through three phases, and it's worth seeing them as a story rather than a recipe.Issue happens at the frontend, strictly in program order. The machine decodes an instruction, renames its destination register (grabbing a fresh physical register), allocates a reservation station entry attached to the right functional unit, and — crucially — allocates a slot at the tail of the reorder buffer. At the end of issue, the instruction exists simultaneously in two places: waiting for data in a reservation station, and holding a reserved slot in the ROB that remembers its original program-order position.Execute happens whenever the instruction's operands are ready, in whatever order the data allows. The reservation station fires, the functional unit computes, the result goes out on the CDB, every dependent station that was watching that tag wakes up. This phase is where the "out of order" actually happens — a load from iteration 5 can finish before an add from iteration 3 if the add is still waiting for its own operand.Commit (also called retire) happens at the head of the ROB, strictly in program order. When the instruction at the head of the ROB has executed and its result is sitting in the ROB entry, the machine writes it into the architectural register file and frees the slot. An instruction only becomes "real" — visible to the rest of the system — at this moment.Issue in order, execute out of order, commit in order. That's the whole thing.The ROB is a circular FIFO, one entry wide per instruction, sized to however many in-flight instructions the core wants to track (tens for 1990s cores, hundreds for modern ones). Instructions enter at the tail during issue, exit from the head during commit, and sit in the middle in whatever execution state they happen to be in — waiting, executing, or finished-but-not-yet-committed.Each ROB entry records the instruction type, its destination register, a slot for its result (filled in when the CDB broadcasts it), and a valid bit. Finished entries queue up behind the head, politely waiting their turn.This shape is exactly what you need for precise exceptions. If a divide-by-zero fires in an instruction that's sitting somewhere in the middle of the ROB, the hardware doesn't have to figure out "which other out-of-order instructions already wrote to the register file?" — because none of them did. Everything after the faulting instruction is still trapped in ROB slots. The recovery is brutal and simple: flush the ROB from the faulting entry onward, release the reservation stations, reset the rename table, and hand control to the exception handler. The architectural register file is untouched; it still reflects the last instruction that committed cleanly.Why does this matter? Because of context switches. Remember from L07 that an interrupt can arrive at any cycle, the OS saves the register state of the interrupted program, runs the handler, restores the state, and returns. The OS can only save a consistent snapshot — a set of register values that corresponds to "the machine has executed instructions 1 through k and is about to execute k+1." OoO without a ROB would leak half-finished instructions from the future into that snapshot: a register overwritten by an instruction that hasn't "officially" happened yet. Restoring such a state produces garbage. The ROB exists so that the architectural state always looks sequential, even though the microarchitecture is wildly parallel.This is the single most important structural idea of the lecture: architectural state and microarchitectural state are separate. The ROB is the wall between them. Hold on to this — §7 is going to show what happens when that wall leaks.<br><img alt="Block diagram of an out-of-order core combining Tomasulo reservation stations with a reorder buffer. Frontend fetch/decode/rename feeds reservation stations; reservation stations fire into functional units (ALUs, load/store, FP multiply) when operands are ready; results broadcast on the Common Data Bus are snatched by tag into waiting stations and ROB entries; the ROB commits in program order into the architectural register file." src="assets/l14-tomasulo-cdb-eng.svg" target="_self" style="width: 700px; max-width: 100%;">A modern OoO core, in one diagram, looks like this. The frontend fetches and decodes several instructions per cycle and hands them to the rename stage, which swaps architectural register names for physical ones. Renamed instructions flow into reservation stations clustered around the functional units and into the ROB tail in parallel. Functional units — integer ALUs, load/store units, FP/SIMD units — pull instructions from their reservation stations whenever operands are ready, execute, and broadcast results on the Common Data Bus. The CDB updates the physical register file and wakes up waiting reservation stations, and it also fills in the result slot in the corresponding ROB entry. At the ROB head, a commit unit retires finished instructions in program order, copying their results into the architectural register file — the register state the OS, the debugger, and any exception handler will ever see.That's the machine. Now let's run the array-sum loop on it.<br><img alt="The running example executed out-of-order across two loop iterations. Iteration 1 and iteration 2 (shown on a tinted background) overlap in execution — the loads from iteration 2 begin while iteration 1 is still finishing, because their data dependencies are entirely independent. CPI falls to around 0.4, limited by load latency rather than dependency chains." src="assets/l14-running-example-ooo-eng.svg" target="_self" style="width: 700px; max-width: 100%;">Call the two iterations k and k+1 (so I1k is the load of array[k], I1{k+1} is the load of array[k+1], and so on). The branch predictor has already speculated that the loop continues, so the frontend has issued both iterations into the ROB. Look at what the machine just did. I1_{k+1} started executing in cycle 1, simultaneously with I1_k, because the two loads share no dependencies — they read different memory addresses, and rcx has already been renamed, so even the induction chain doesn't serialize them across iterations. The serial chain I1_k→I2_k still exists and still forces I2_k to wait for the load, but that wait no longer stalls the whole machine — it just stalls I2_k, while iteration k+1 streams past.Two iterations of five instructions finish in roughly 7 cycles of real work: CPI ≈ 0.7 per instruction, and with a wider window (4 or 6 iterations in flight) it drops toward CPI ≈ 0.4 — almost 3× the throughput of the in-order pipelined version from §4. The bottleneck is no longer the dependency chain; it's load latency and the size of the ROB, which caps how far ahead the machine can look.None of this is free. Reservation stations hold dozens of waiting instructions and must do associative tag matching on every CDB broadcast — that's content-addressable memory, which is expensive in area and power. The rename stage needs a multi-ported physical register file with hundreds of entries. The ROB has to be wide enough to track every in-flight instruction. Worst of all, the complexity scales super-linearly with issue width: doubling the width more than doubles the comparator count, the bypass network, the port count on the register file.This is why modern x86 cores top out around 4-6 wide, and why going wider has historically been the holy grail — and why Apple, going 8 wide on the M1, is doing something genuinely unusual. That's §6.5.There's one more thing a modern OoO machine has to do to keep its gigantic execution window full: guess the future. Every branch in the instruction stream is a potential stall point; the ROB wants to hold hundreds of instructions, but it can only do that if the frontend keeps feeding it past dozens of branches without waiting to see which way each one goes. The machine has to speculate.OoO extracts parallelism. But to keep the pipeline full past branches, we must guess the future. Speculation. And speculation, as we'll see, has a dark side. §7.<br>If everything in §6 sounds theoretical — reservation stations, a 200-entry reorder buffer, a common data bus quietly broadcasting results — here is what it looks like when someone actually builds it at industrial scale. In November 2020, Apple shipped the <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Apple_M1" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Apple_M1" target="_self">M1</a>, and it redefined what a mainstream out-of-order core can be. Not because Apple invented anything new — but because they took the textbook design from §6 and scaled every dimension of it further than anyone else in the consumer market had dared.This is the section where the abstract diagrams cash out into numbers you can compare against the chip in your laptop.The M1's high-performance core is codenamed Firestorm. By the standards of any contemporary x86 core, it is enormous:<br><img alt="Simplified block diagram of the Apple M1 Firestorm performance core. An 8-wide frontend (fetch, decode, rename, dispatch) feeds a large reorder buffer of roughly 630 entries with about 354 physical integer and 384 physical FP/SIMD registers; execution ports below include 6 integer ALUs, 4 FP/SIMD units, 3 load units, and 2 store units. Sidebar callouts note the 2x-larger OoO window versus typical x86 cores and the fixed-width ARM64 ISA that makes 8-wide decode affordable." src="assets/l14-m1-block-diagram-eng.svg" target="_self" style="width: 700px; max-width: 100%;">Look at the ROB row carefully. Firestorm's instruction window is roughly twice the size of any contemporary x86 core. Every entry in the ROB is one instruction the CPU has fetched, possibly executed out of order, and is now waiting to retire in program order. A larger window is not a vanity number — it is the search radius the OoO scheduler has to find independent work. The bigger the radius, the more cache misses, branches, and long dependency chains the core can hide behind useful parallel execution.In §6 we said: "OoO turns memory latency into a search problem — find independent work to do while the load is in flight." Firestorm's answer to that problem is "search across roughly 630 instructions at once." That is several full iterations of the running example overlapped simultaneously.Here is the question that should bother you: x86 vendors had been shipping out-of-order cores since the Pentium Pro in 1995. Why, in 2020, were they still stuck at 4-wide or 6-wide decode while Apple shipped 8? Did Intel and AMD just stop trying?No. They were boxed in by a decision their ISA had made decades earlier.Remember L09? We talked about a fundamental ISA design trade-off: fixed-width vs variable-length instruction encodings. ARM64 picked fixed-width: every instruction is exactly 4 bytes, aligned on a 4-byte boundary, no exceptions. x86 picked variable-length: instructions can be anywhere from 1 to 15 bytes, and you cannot tell where instruction N+1 starts until you have at least partially decoded instruction N.That ISA decision is invisible at the source-code level. C looks like C either way. But it ripples all the way through to the silicon. To decode 8 ARM64 instructions in parallel, the M1 frontend just slices a 32-byte fetch line into 8 equal 4-byte chunks and hands each to its own decoder. Trivially parallel, trivially scalable. To decode 8 x86 instructions in parallel, you would have to first scan the byte stream serially to figure out where each instruction begins and ends — and that scan is on the critical path. Intel and AMD compensate with predecoders, length markers cached in the instruction cache, and µop caches (caches of already-decoded micro-operations that bypass the frontend entirely on hot loops). These tricks help. But they raise the floor, not the ceiling. You can never quite get to "eight x86 instructions per cycle from cold I-cache" the way ARM64 trivially can.So the M1 isn't faster because Apple invented something. It is faster because, decades earlier, ARM picked the ISA option that scales better in 2020-era silicon. Remember L09's ISA trade-offs? Here is where that trade-off cashes in — decades later, in your laptop's battery life.To borrow the cleanest framing of the whole chip: Apple didn't invent a new magic technique — they just built a much bigger version of the OoO core everyone already had, and the ISA made that affordable.The M1 is not just one kind of core. It ships with four Firestorm P-cores (performance) and four Icestorm E-cores (efficiency) on the same die, sharing the same memory hierarchy and the same ARM64 ISA. The OS scheduler dynamically migrates threads between them based on demand: a video-encoding thread runs on Firestorm; a background mail-fetcher runs on Icestorm at a fraction of the power.This is not Apple's invention either. The lineage goes back to ARM's big.LITTLE architecture from 2011. The bet is that real workloads have a long tail of low-priority threads that don't need the full ROB-of-630 firepower, and pinning them to a tiny core saves real wall-plug watts. The M1 cashed that bet on the desktop in a way nobody had quite managed before.L15 will unpack how and why heterogeneity beats monolithic scaling — and how this idea generalizes from CPUs to GPUs, NPUs, and the whole mess of accelerators in a modern SoC. For now, just register: the chip in front of you is already heterogeneous.<br>When the M1 launched, reviewers at <a data-tooltip-position="top" aria-label="https://www.anandtech.com/show/16226/apple-silicon-m1-a14-deep-dive" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.anandtech.com/show/16226/apple-silicon-m1-a14-deep-dive" target="_self">AnandTech</a> measured single-thread performance comparable to high-end Intel desktop parts of the same year — at a fraction of the power and inside a fanless laptop. That was not because of some breakthrough. It was because Firestorm executed everything in §6, scaled up:
8-wide decode, fed by an aggressive branch predictor and a large I-cache
A ~630-entry ROB so the OoO window swallows L1 and L2 cache misses
Plenty of physical registers so register renaming never runs out
15 execution ports keeping all that in-flight work flowing through actual hardware
There is a lesson here about scale and about ISAs, and the lesson is conservative: when you can't out-clever your competition, out-build them. And pick the ISA that lets you.But there is also a lesson lurking underneath. To keep that 8-wide engine fed past every branch in real code, the M1 — and every other modern OoO core — must speculate. It must execute down guessed paths before the guesses are confirmed, and it must do it constantly, on every branch, all day long. For thirty years we assumed this was free: if the guess was wrong, the CPU rolls back the architectural state, and it is as if nothing happened.That assumption turned out to be wrong. And that is where things get... interesting. §7.Every CPU course tells you the same thing in the first week: "the CPU executes instructions, and instructions update registers and memory." That is the model. Registers and memory are the architectural state — the part of the machine the ISA promises you can see. Programs read it, debuggers print it, the OS saves and restores it on a context switch. The whole abstraction of computing rests on the idea that this state is what the machine is.True. But incomplete.Alongside the architectural state, every modern CPU carries a parallel universe of microarchitectural state: the contents of the L1, L2, and L3 caches; which lines were evicted; which branches the predictor has seen; which addresses are in the TLB; the state of every reservation station and ROB entry. The ISA does not promise anything about this state. Most of it has no name in the assembly manual. From software's point of view, it is invisible.Except through timing.A cache hit is fast. A cache miss is slow. If you can measure time precisely enough — and a modern CPU will sell you a cycle counter for a single instruction — you can ask the machine questions about its microarchitectural state that the ISA never meant for you to ask. And starting in January 2018, two attacks made it brutally clear what those questions can extract.This is the most important idea in this lecture. Read it twice:
Architectural state is what the ISA tells you the machine looks like. Microarchitectural state is what the machine actually is. Spectre and Meltdown are the proof that the difference matters — and that microarchitectural state is part of the machine's observable behavior, whether the ISA admits it or not.
The rest of this section is the proof.§6 ended with a problem. To keep an 8-wide OoO engine fed, the CPU has to look hundreds of instructions past every branch. But it does not yet know how the branch will go. The compromise the industry made, around 1995, was simple: guess. Use a branch predictor. Speculatively execute down the predicted path. Track everything in the reorder buffer. If the guess was right, retire the speculative work in order — it counts. If the guess was wrong, squash the speculative work, drain the ROB of the bad-path entries, restart from the correct branch target.<br>This is <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Speculative_execution" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Speculative_execution" target="_self">speculative execution</a>, and it is the single most important reason your laptop is fast. Modern branch predictors are &gt;95% accurate on real code. The vast majority of speculative work commits and counts. The tiny minority that gets squashed pays a small misprediction penalty — twenty-something cycles — and the engine restarts.For thirty years, the unspoken assumption underneath this whole edifice was that squashing was complete. The architectural state is rolled back, no register is updated, no store hits memory, and "it's as if it never happened." The ISA contract is preserved. Speculation is invisible to software.The unspoken assumption was wrong.When the CPU speculatively executes a load, that load does not just compute a value into a renamed physical register — it also brings a cache line into L1. When the CPU speculatively executes a branch, that branch updates the branch predictor's history tables. When the CPU speculatively walks a page table, the result lands in the TLB. The architectural state can be cleanly rolled back. The microarchitectural state cannot. The cache is still warm. The predictor still remembers. And those traces are observable through timing.Before we look at Meltdown and Spectre themselves, you need to see the side channel they both exploit.Remember the cache from L11? We spent a whole lecture celebrating it. Caches make memory feel fast by keeping recently-used lines close to the CPU. A hit on L1 takes ~3–4 cycles. A miss all the way to DRAM takes ~200–300 cycles. That gap — two orders of magnitude — is the difference between a smooth modern computer and a 1980s-feeling brick.That gap is also the leak. Anywhere in the system there is a measurable difference between cached and uncached, you have a one-bit channel. With 256 distinct addresses you can probe, you have an eight-bit channel — one byte per measurement.Here is the standard trick. The attacker allocates a probe array of 256 cache-line-sized slots, call it probe[0..255]. Each slot is one cache line (64 bytes on x86), so they don't overlap. The attacker first flushes all 256 slots out of the cache, using something like x86's clflush instruction. Then the attacker arranges — by some mechanism we'll cover in a moment — for the victim's CPU to speculatively execute something equivalent to:secret_byte = &lt;some byte the attacker should not be able to read&gt;
touch probe[secret_byte * 64] ; brings ONE probe line into cache
&lt;speculation rolled back&gt;
The speculation is rolled back. The architectural state is clean — no register holds secret_byte. But the cache still has one warm line: the line corresponding to probe[secret_byte * 64]. Now the attacker times an access to each of probe[0], probe[64], probe[128], ..., probe[255*64]. Two hundred and fifty-five of them miss and take ~200 cycles. One of them hits and takes ~3 cycles. The index of that one tells the attacker what secret_byte was.One byte of secret per measurement. The measurement is a few microseconds. Repeat at megahertz rates. The thing we celebrated in L11 — fast caches — is the leak.<br><img alt="Four-panel walkthrough of the Spectre v1 cache side-channel attack. Panel 1: attacker flushes a 256-entry probe array so every line is cold. Panel 2: the victim speculatively accesses probe secret*4096 while a mispredicted branch hides the out-of-bounds read; the corresponding cache line warms. Panel 3: speculation is rolled back — architectural state is clean but the warmed cache line persists. Panel 4: the attacker re-reads every probe entry and times each access; the single fast access identifies the secret byte." src="assets/l14-spectre-cache-sidechannel-eng.svg" target="_self" style="width: 700px; max-width: 100%;">This is the side channel. Both Meltdown and Spectre use it. They differ in how they trick the CPU into executing the secret-dependent load speculatively in the first place.<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Meltdown_(security_vulnerability)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Meltdown_(security_vulnerability)" target="_self">Meltdown</a> is the simpler and the more devastating of the two. It targets a specific implementation choice that Intel — and a few ARM cores — had quietly made for performance.When a user-mode program executes a load from a kernel address, the CPU has to do two things: (a) issue the load to the cache, and (b) check that the load is permitted by the page table's user/supervisor bit. On a strict design, (b) happens before (a). On the affected CPUs, (a) and (b) happened in parallel, with the permission check completing later in the pipeline as part of exception handling. The load was speculatively allowed to return its value into the OoO engine; if the permission check ultimately failed, the architectural exception would be raised at retirement and the speculative load would be squashed.In between "value returns" and "permission check completes," the speculative kernel value sits in a renamed physical register, available to dependent speculative instructions. So an attacker writes:mov al, byte [kernel_address] ; speculatively reads forbidden kernel byte
shl rax, 6 ; multiply by cache-line size
mov rbx, [probe + rax] ; secret-dependent load into probe
All three lines execute speculatively. The first one will eventually fault — but before the fault is raised, the third line has already touched probe[secret * 64], leaving a cache footprint. The CPU then catches up, raises the fault, squashes everything in architectural state. The attacker handles the fault (or suppresses it via TSX, or hides behind another mispredicted branch), then probes the array. The kernel byte is recovered.The result: a user-mode program could read arbitrary kernel memory. On affected Intel chips running Linux, this meant all of physical memory, because Linux maps all physical RAM into the kernel's address space. Passwords in your browser, SSH keys in another process, anything in the system — readable from any unprivileged process.The fix: KAISER, later renamed KPTI (Kernel Page-Table Isolation). The kernel's page tables are unmapped from user-mode entirely. While a user-mode program is running, kernel memory is not just protected — it is not even present in the page tables, so speculation has nothing to speculate against. Every system call now has to swap page tables on entry and exit, which costs real cycles. Initial reports estimated 5–30% slowdowns on syscall-heavy workloads; PCID (Process-Context Identifiers) on newer Intel chips brought that down significantly, but not to zero. A hardware speculation bug, paid for forever in software at the OS level.<br>Affected: "Nearly all Intel processors with out-of-order execution since 1995," some ARM Cortex cores (notably A75), and IBM POWER8/9 — see the official <a data-tooltip-position="top" aria-label="https://meltdownattack.com/" rel="noopener nofollow" class="external-link is-unresolved" href="https://meltdownattack.com/" target="_self">meltdownattack.com</a> writeup. AMD was mostly immune because AMD's permission check happens before the load returns its value into the speculative window, not after. A small implementation difference, made for unrelated reasons years earlier, saved an entire vendor.<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Spectre_(security_vulnerability)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Spectre_(security_vulnerability)" target="_self">Spectre v1</a> is subtler, more general, and harder to fix. Where Meltdown abused a specific implementation flaw, Spectre v1 abuses something every modern CPU does on purpose: the branch predictor.Consider this absolutely unremarkable, perfectly correct piece of code, the kind of thing you would write in a network packet parser or a JIT compiler:if (x &lt; array1_size) { y = array2[ array1[x] * 4096 ];
}
The bounds check is right there. The programmer did everything textbook-correct. The function refuses to read array1[x] if x is out of range. What could go wrong?Here is what. The attacker calls this function many times in a loop with valid x values, training the branch predictor to confidently predict "the bounds check passes, take the if-body." Then the attacker calls the function with a malicious x — a huge value, way past the end of array1, chosen so that array1 + x happens to point at a secret somewhere else in the victim's address space. The CPU fetches the branch, the predictor says "taken, as always," speculation begins:
Speculatively load array1[x] — the secret byte at array1 + x.
Speculatively use it as an index into array2, multiplying by 4096 (cache-line stride).
Speculatively load array2[secret * 4096], bringing one cache line in.
Bounds check resolves. It fails. Speculation is squashed.
The architectural state is clean. y is never assigned. But step 3 left its mark in the cache. The attacker probes array2[0..255 * 4096] and recovers the secret.The terrifying thing about Spectre v1 is that the code is correct. The programmer did the bounds check. The hardware violated it anyway, speculatively, because the branch predictor said the check was usually fine. There is no single line of code that is buggy. There is no one place to patch.Mitigations are correspondingly painful: explicit speculation barriers like lfence after every bounds check (very expensive), compiler-inserted speculation hardening, and helper functions like Linux's array_index_nospec() that mask the index in a way the predictor cannot speculate past. None of these are free. And nobody believes they are complete — Spectre v1 is, in 2026, still not "solved." Hot paths in Linux, Chrome, and OS kernels are still hand-audited for it.If v1 abused the direction predictor (taken vs not-taken), v2 abuses the target predictor — the Branch Target Buffer (BTB) that tells the CPU where an indirect branch is going. Indirect branches are everywhere: virtual method calls in C++, function pointers, jump tables, the ret instruction.The attack: from a process the attacker controls, repeatedly execute indirect branches at addresses that collide in the BTB with the victim's indirect branches. The BTB cannot tell the two contexts apart — it is indexed by partial PC bits. The attacker trains it to say "the indirect branch at address X jumps to gadget Y." Then control transfers to the victim. The victim's indirect branch at address X executes; the BTB serves the poisoned target Y; the CPU speculatively starts executing a few instructions of Y inside the victim's address space. Y is chosen by the attacker to be a gadget — a short sequence already present in the victim's code that loads a secret and uses it to index a probe array, exactly like the Spectre v1 case.Speculation is eventually squashed when the real indirect target resolves and doesn't match. But the cache footprint is left behind, and the attacker — running in another process or even another VM on the same physical core — measures it.The mitigation that emerged from this is the elegantly named retpoline ("return trampoline"), invented by Google. The idea is to replace every indirect branch with a sequence built from call and ret instructions that the BTB cannot meaningfully poison, because the speculative path is forced into an infinite loop the predictor will eventually steer away from. Linux, Windows, and the major hypervisors all shipped retpoline mitigations within months of the disclosure. Performance cost depends entirely on how indirect-branch-heavy the workload is — anywhere from negligible to noticeable.Step back from the CVE numbers for a minute and look at what just happened to the field.For fifty years, computer architecture textbooks described CPUs in terms of architectural state alone. The ISA was the contract. Registers, memory, condition flags, program counter — that was the machine. Everything else was an "implementation detail," not part of the abstraction the programmer needed to reason about. Caches existed, but they were "transparent." Branch predictors existed, but they only affected performance, not correctness. Speculative execution existed, but it was guaranteed to be invisible.Spectre and Meltdown were not bugs in Intel's chips, or in ARM's, or in IBM's. The processors did exactly what the ISA said they should do. Every architectural register, every memory location, every condition flag was correct at every retirement. The hardware fulfilled its contract. The contract was the problem. The contract was incomplete.Microarchitectural state is part of the machine. It is observable through timing, it leaks information across security boundaries, and from now on, any secure system design has to treat it as visible. This is not a software patch you ship and forget. It is a permanent change in what we mean when we say "the CPU does X." The mental model of computing that you walked into this lecture with — registers, memory, ISA, done — is the model of the architectural layer. The actual machine has another whole layer underneath it, and that layer talks back.If you remember one thing from this whole lecture, remember this: architectural state ≠ microarchitectural state, and the difference is not a footnote. It is the difference between the machine the manual describes and the machine you actually own.The mitigations cost real money. KPTI on Meltdown costs syscall throughput. Retpoline on Spectre v2 costs indirect-branch performance. Spectre v1 hardening costs fences in hot paths. Browsers reduced JavaScript timer resolution to make the side channel harder to measure. Cloud providers reorganized scheduling to keep mutually-distrusting tenants off the same core. New CPUs added "Indirect Branch Restricted Speculation" hardware bits, "Single Thread Indirect Branch Predictors," and dozens of similarly-named acronyms. Total reported overhead, depending on workload, ranges from a couple of percent to as much as 30%. Some of that has been clawed back over the years; some of it never will be.This is the speculation tax: a permanent recurring cost on the optimization that defined the last three decades of CPU performance. Every modern OS kernel and every modern web browser ships with speculation barriers in their hot paths. The fast core is still fast. It is just slightly less fast than it used to be, forever, because we now have to assume that the cache, the predictor, and the TLB are all watching.Out-of-order execution with speculation gives us extraordinary single-thread performance, at a cost we now understand: a security tax we cannot fully refund, paid by every workload that uses a modern CPU. So we have squeezed about as much parallelism as physics will let us out of one thread on one core.What if we tried to run two threads on the same core at the same time, sharing the same enormous OoO engine to keep its expensive hardware busier? That is simultaneous multithreading, and it is §8.Here's an uncomfortable statistic. That 6-wide, 630-entry-ROB Apple M1 core from §6.5 — on real single-threaded code, it rarely achieves an IPC above 2. The hardware can theoretically retire 8 instructions per cycle, but your actual program gets 1-2. The rest of the backend sits idle, waiting.Why? Everything §3 through §7 warned about. A single L3 miss is 200+ cycles of the load-store unit doing nothing useful. A branch misprediction flushes dozens of in-flight µops. Dependency chains like our ebx accumulator serialize the ALUs. Even with perfect OoO and massive speculation windows, one thread simply cannot keep all those execution ports busy most of the time. The execution backend is over-provisioned for any single thread.That's not a bug — it's the point of building a 6-wide core. You paid for those ports because occasionally a burst of independent work shows up and you want to finish it fast. But "occasionally" means "idle the rest of the time." So here's the question: if one thread leaves execution units idle, what if a second thread could use them in the same cycle — not after a stall, but simultaneously, filling in the gaps?That is simultaneous multithreading (SMT).L09 already introduced SMT at the level of the flow taxonomy — multiple instruction streams sharing one pipeline. This section is about what that actually means inside the silicon.<br>Earlier multithreading schemes switched threads on stalls. Coarse-grained multithreading swaps to a different thread when the current one hits a long-latency event (cache miss). Fine-grained multithreading rotates through threads every cycle, round-robin. Both still execute only one thread at a time in any given pipeline stage. SMT breaks that rule: instructions from multiple threads can occupy the backend in the same cycle. One cycle might issue two µops from thread A to the integer ALUs while simultaneously issuing a load from thread B and a floating-point op from thread A. The <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Simultaneous_multithreading" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Simultaneous_multithreading" target="_self">Wikipedia article on SMT</a> puts it sharply: under temporal multithreading "only one thread of instructions can execute in any given pipeline stage at a time," whereas under SMT "instructions from more than one thread can be executed in any given pipeline stage at a time."The hardware cost of this sounds steep but isn't. You already built the reservation stations, the CDB, the ROB, the ALUs, the load-store units. SMT reuses all of it. The only things you have to add are enough bookkeeping to keep two threads' architectural states apart.Here is the split that matters. Remember this table — it is the entire implementation idea of SMT.<br><img alt="Simultaneous multithreading resource diagram. Two thread-private columns at the top show the state that is duplicated per thread — program counter, architectural register file, stack/segment registers — about 5% of core area. Orange arrows merge both threads into a single shared execution backend at the bottom: reservation stations, reorder buffer, functional units, L1/L2 caches, branch predictor, and TLB. The two threads compete for every resource in the backend." src="assets/l14-smt-sharing-eng.svg" target="_self" style="width: 700px; max-width: 100%;">The operating system sees two independent CPUs. It schedules two threads onto them, sends two interrupts, keeps two register sets. But inside the silicon, those two "CPUs" are fighting over one pool of execution resources — and the core's OoO scheduler does not especially care which thread a µop came from, as long as its operands are ready.<br>Hyper-Threading Technology (HTT) is Intel's brand name for 2-way SMT on x86. Intel shipped it first on Xeon server chips in February 2002 and extended it to the Pentium 4 desktop line in November 2002 — see <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Hyper-threading" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Hyper-threading" target="_self">Hyper-threading on Wikipedia</a>. Each physical core becomes two logical processors. On Linux you see this in /proc/cpuinfo; on Windows in Task Manager's "Logical processors" count.Intel's own pitch from the original launch was striking: HTT added only about 5% more die area while delivering 15-30% more throughput on typical server workloads. That ratio — a few percent of silicon for tens of percent more work — is why HTT stuck around. Compared to the alternative, adding a second physical core, which roughly doubles area for roughly 80-90% more throughput, SMT buys much cheaper throughput. It just buys it with a different currency: per-thread predictability.AMD followed with 2-way SMT in Zen (2017) and has kept it in every Zen generation since. IBM's POWER line went further, supporting up to 8-way SMT per core on POWER8 and later. x86 stopped at 2 and never moved. Why? Diminishing returns. By the time you've packed in two threads, most of the spare execution bandwidth is already being used; a third thread rarely finds idle ports, and the cache thrashing it causes outweighs any throughput gain. AMD did briefly explore 4-way SMT during the Bulldozer era and walked away.SMT is not free throughput. Two threads sharing one L1 data cache can evict each other's working set every few thousand cycles — a phenomenon called cache thrashing. Two threads sharing one branch predictor pollute each other's history tables. Two threads sharing fetch bandwidth steal cycles from each other at the frontend. The result is that per-thread latency gets worse even when aggregate throughput gets better.For workloads where that tradeoff is backwards, HT is often disabled outright:
HPC / scientific computing: each thread is already saturating the ALUs with well-scheduled code. The second logical CPU only creates cache contention. Most HPC clusters run with HT off.
Enterprise databases: tail latency (the slowest 1% of queries) matters more than average throughput. HT makes the tail worse. Oracle and SQL Server deployments routinely disable it.
Latency-sensitive real-time systems: unpredictable contention is a dealbreaker.
Cloud VM pricing sometimes lists "physical cores" versus "vCPUs" precisely because a vCPU is usually just one SMT thread and doesn't deliver a full core's performance.
In 2006, ARM publicly criticized SMT for consuming "up to 46% more power than ordinary dual-core designs" while increasing cache thrashing by 42% — one of the reasons mobile ARM cores stayed single-threaded for years. Intel itself dropped HT from the low-power Silvermont Atom cores in 2013, concluding that on small out-of-order cores, wider OoO was a better use of transistors than SMT.Section 7 taught you that microarchitectural state is shared, and shared state across a security boundary leaks. SMT makes that warning concrete at a new scale: two SMT siblings share everything the backend has — execution units, caches, TLB, branch predictor, even the load-store queues. A malicious thread running on logical CPU 0 can observe, interfere with, and time the execution of a victim thread on logical CPU 1, inside the same physical core, with no context switch and no kernel boundary crossing.The attacks are not theoretical. In 2005, Colin Percival demonstrated a timing side channel on HTT that extracted RSA private keys from a neighboring thread by monitoring L1 cache behavior. In 2018, Foreshadow / L1TF turned SMT-shared L1 data cache into a Spectre-class leak. In 2019, MDS (Microarchitectural Data Sampling) attacks like RIDL and Fallout showed that internal buffers shared between siblings leaked data across threads. OpenBSD disabled hyper-threading by default in 2018, and several cloud providers now disable SMT on tenant-facing machines or refuse to co-schedule threads from different customers on the same physical core.This is the same moral as §7, just zoomed in: SMT optimizes resource utilization by eliminating isolation between co-running threads. The cost of that optimization is that "co-running" also means "co-leaking."Step back. §1-§7 built a progressively more aggressive machine for extracting parallelism out of a single instruction stream: pipeline, forward, predict, widen, reorder, speculate. Each technique tried to find parallelism that was already latent in the code. §8 is the admission that there is a ceiling to what any one thread contains, and past that ceiling the only way to keep the backend busy is to import parallelism from somewhere else — another thread the OS happens to have runnable.ILP finds parallelism in one thread. TLP via SMT imports parallelism from another thread when one isn't enough. Both feed the same hungry execution backend. That sentence is worth memorizing.You have now seen, in order, every major technique the industry invented to extract more work per cycle out of a single physical core: pipelining, forwarding, branch prediction, superscalar issue, out-of-order execution, register renaming, the reorder buffer, speculative execution, and SMT. Each one solved a bottleneck the previous one created. Each one made the machine faster without making it conceptually different from the programmer's point of view — the architectural state (§6) stayed a simple sequential execution of x86 instructions.So why does your 2024 laptop run at roughly the same clock speed as a 2008 desktop? Why did the party stop? The answer involves Amdahl's law (you cannot speed up what you don't parallelize), the power wall (clock frequency × voltage² burns down the die), dark silicon (you have more transistors than you can afford to switch on at once), and the thermal envelope of a laptop chassis. That is L15. For now, just notice that we have reached the end of a chapter: this lecture's entire story — from the 5-stage pipeline to SMT — is what happened inside one core. Next time we ask why we had to stop making one core faster and start making many.
Pipelining trades latency for throughput. A single instruction still takes N cycles, but a new instruction completes every cycle — roughly N× speedup, assuming nothing goes wrong. The "nothing goes wrong" is where hazards live.
Every ILP technique solves a problem created by the previous one. Pipelining creates hazards → forwarding and branch prediction reduce them → superscalar widens the pipe but hits dependency chains → out-of-order execution breaks those chains but breaks precise exceptions → the reorder buffer restores them → speculation keeps the pipeline full past branches → Spectre/Meltdown weaponize speculation.
Dynamic beats static for ILP. Itanium's EPIC bet that compilers could extract parallelism was defeated by unpredictable cache misses and branches. Runtime OoO hardware schedules against actual latencies, not worst-case static assumptions.
Apple M1 is what textbook OoO looks like at industrial scale. 8-wide decode, ~630-entry ROB, ~350 physical registers — the same reservation stations and CDB from 1967 Tomasulo, just much bigger. ARM's fixed-width ISA made that scaling affordable; x86 pays a decode-width tax for legacy.
Architectural state ≠ microarchitectural state. The single most important idea in this lecture. For fifty years we described CPUs by their registers and memory alone. Spectre and Meltdown proved that caches, predictors, and TLBs are part of the machine too — observable through timing, and therefore part of the security model of every system that runs untrusted code.
SMT buys cheap throughput. ~5% area for ~15–30% throughput, by letting two threads share one OoO backend. Costs: cache contention and a fresh class of same-core side channels.
Required reading:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Instruction_pipelining" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Instruction_pipelining" target="_self">Instruction pipelining</a> — §Overview, §Hazards
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Tomasulo%27s_algorithm" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Tomasulo%27s_algorithm" target="_self">Tomasulo's algorithm</a> — §Algorithm, §Implementation
<br><a data-tooltip-position="top" aria-label="https://spectreattack.com/" rel="noopener nofollow" class="external-link is-unresolved" href="https://spectreattack.com/" target="_self">Spectre</a> and <a data-tooltip-position="top" aria-label="https://meltdownattack.com/" rel="noopener nofollow" class="external-link is-unresolved" href="https://meltdownattack.com/" target="_self">Meltdown</a> landing pages
Patterson &amp; Hennessy, Computer Organization and Design, Chapter 4 (Pipelining) — for mechanical fluency
Suggested watching:
Any modern CPU architecture deep-dive on the Apple M1 or AMD Zen — pause frequently and try to identify the components from §6.
What you'll do:
Work through the running example on paper at each level (sequential → pipelined → forwarding → superscalar → OoO). Draw the spacetime grid by hand. This is the single most useful study exercise for this lecture.
Read a small x86-64 assembly function and identify where it will stall on an in-order 5-stage pipeline. Which RAWs require forwarding? Which survive as 1-cycle bubbles? Where does the first branch sit?
Bonus: compile a simple C function with -O0 and -O3. Look at the generated assembly. Can you see the compiler re-scheduling instructions to hide load-use latencies? <br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Instruction_pipelining" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Instruction_pipelining" target="_self">Instruction pipelining</a> — the canonical Wikipedia article; covers stages, hazards, and exception handling.
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Hazard_(computer_architecture)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Hazard_(computer_architecture)" target="_self">Hazard (computer architecture)</a> — structural, data (RAW, WAR, WAW), and control hazards.
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Branch_predictor" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Branch_predictor" target="_self">Branch predictor</a> — from 1-bit to 2-bit saturating to two-level adaptive, TAGE, and neural predictors.
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Superscalar_processor" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Superscalar_processor" target="_self">Superscalar processor</a> — multiple-issue architectures and their limits.
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Tomasulo%27s_algorithm" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Tomasulo%27s_algorithm" target="_self">Tomasulo's algorithm</a> — the 1967 IBM 360/91 algorithm that defines modern OoO.
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Reorder_buffer" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Reorder_buffer" target="_self">Reorder buffer</a> — the in-order retirement mechanism that preserves precise exceptions.
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Speculative_execution" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Speculative_execution" target="_self">Speculative execution</a> — the optimization that enables modern ILP and the vulnerability class it created.
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Spectre_(security_vulnerability)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Spectre_(security_vulnerability)" target="_self">Spectre (security vulnerability)</a> — bounds check bypass and branch target injection variants.
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Meltdown_(security_vulnerability)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Meltdown_(security_vulnerability)" target="_self">Meltdown (security vulnerability)</a> — the privilege-check race and KAISER/KPTI response.
<br><a data-tooltip-position="top" aria-label="https://meltdownattack.com/" rel="noopener nofollow" class="external-link is-unresolved" href="https://meltdownattack.com/" target="_self">Meltdown and Spectre</a> — official landing page with the academic papers.
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Simultaneous_multithreading" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Simultaneous_multithreading" target="_self">Simultaneous multithreading</a> — the SMT model and its historical implementations.
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Hyper-threading" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Hyper-threading" target="_self">Hyper-threading</a> — Intel's commercial 2-way SMT.
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Apple_M1" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Apple_M1" target="_self">Apple M1</a> — the headline numbers for the case study in §6.5.
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Itanium" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Itanium" target="_self">Itanium</a> — the EPIC architecture and its commercial failure.
Patterson, D. A., and Hennessy, J. L. Computer Organization and Design: The Hardware/Software Interface. Morgan Kaufmann, 6th ed., 2020. Chapters 4–6.
Hennessy, J. L., and Patterson, D. A. Computer Architecture: A Quantitative Approach. Morgan Kaufmann, 6th ed., 2017. Chapter 3 (Instruction-Level Parallelism).
]]></description><link>lectures/eng/l14-eng.-instruction-level-and-thread-level-parallelism.html</link><guid isPermaLink="false">Lectures/ENG/L14-ENG. Instruction-Level and Thread-Level Parallelism.md</guid><pubDate>Tue, 14 Apr 2026 08:46:09 GMT</pubDate><enclosure url="assets/l14-running-example-sequential-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-running-example-sequential-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Forwarding-Bypass-ENG]]></title><description><![CDATA[<img src="assets/l14-forwarding-bypass-eng.svg" target="_self">]]></description><link>assets/l14-forwarding-bypass-eng.html</link><guid isPermaLink="false">assets/L14-Forwarding-Bypass-ENG.svg</guid><pubDate>Tue, 14 Apr 2026 07:10:16 GMT</pubDate><enclosure url="assets/l14-forwarding-bypass-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-forwarding-bypass-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Running-Example-OoO-ENG]]></title><description><![CDATA[<img src="assets/l14-running-example-ooo-eng.svg" target="_self">]]></description><link>assets/l14-running-example-ooo-eng.html</link><guid isPermaLink="false">assets/L14-Running-Example-OoO-ENG.svg</guid><pubDate>Tue, 14 Apr 2026 06:13:48 GMT</pubDate><enclosure url="assets/l14-running-example-ooo-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-running-example-ooo-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Running-Example-Superscalar-ENG]]></title><description><![CDATA[<img src="assets/l14-running-example-superscalar-eng.svg" target="_self">]]></description><link>assets/l14-running-example-superscalar-eng.html</link><guid isPermaLink="false">assets/L14-Running-Example-Superscalar-ENG.svg</guid><pubDate>Tue, 14 Apr 2026 06:13:43 GMT</pubDate><enclosure url="assets/l14-running-example-superscalar-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-running-example-superscalar-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Running-Example-Forwarding-ENG]]></title><description><![CDATA[<img src="assets/l14-running-example-forwarding-eng.svg" target="_self">]]></description><link>assets/l14-running-example-forwarding-eng.html</link><guid isPermaLink="false">assets/L14-Running-Example-Forwarding-ENG.svg</guid><pubDate>Tue, 14 Apr 2026 06:13:12 GMT</pubDate><enclosure url="assets/l14-running-example-forwarding-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-running-example-forwarding-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Running-Example-Pipelined-ENG]]></title><description><![CDATA[<img src="assets/l14-running-example-pipelined-eng.svg" target="_self">]]></description><link>assets/l14-running-example-pipelined-eng.html</link><guid isPermaLink="false">assets/L14-Running-Example-Pipelined-ENG.svg</guid><pubDate>Tue, 14 Apr 2026 06:13:07 GMT</pubDate><enclosure url="assets/l14-running-example-pipelined-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-running-example-pipelined-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Running-Example-Sequential-ENG]]></title><description><![CDATA[<img src="assets/l14-running-example-sequential-eng.svg" target="_self">]]></description><link>assets/l14-running-example-sequential-eng.html</link><guid isPermaLink="false">assets/L14-Running-Example-Sequential-ENG.svg</guid><pubDate>Tue, 14 Apr 2026 06:13:02 GMT</pubDate><enclosure url="assets/l14-running-example-sequential-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-running-example-sequential-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Spectre-Cache-Sidechannel-ENG]]></title><description><![CDATA[<img src="assets/l14-spectre-cache-sidechannel-eng.svg" target="_self">]]></description><link>assets/l14-spectre-cache-sidechannel-eng.html</link><guid isPermaLink="false">assets/L14-Spectre-Cache-Sidechannel-ENG.svg</guid><pubDate>Tue, 14 Apr 2026 05:59:58 GMT</pubDate><enclosure url="assets/l14-spectre-cache-sidechannel-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-spectre-cache-sidechannel-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-M1-Block-Diagram-ENG]]></title><description><![CDATA[<img src="assets/l14-m1-block-diagram-eng.svg" target="_self">]]></description><link>assets/l14-m1-block-diagram-eng.html</link><guid isPermaLink="false">assets/L14-M1-Block-Diagram-ENG.svg</guid><pubDate>Tue, 14 Apr 2026 05:57:34 GMT</pubDate><enclosure url="assets/l14-m1-block-diagram-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-m1-block-diagram-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-SMT-Sharing-ENG]]></title><description><![CDATA[<img src="assets/l14-smt-sharing-eng.svg" target="_self">]]></description><link>assets/l14-smt-sharing-eng.html</link><guid isPermaLink="false">assets/L14-SMT-Sharing-ENG.svg</guid><pubDate>Tue, 14 Apr 2026 05:57:20 GMT</pubDate><enclosure url="assets/l14-smt-sharing-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-smt-sharing-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Tomasulo-CDB-ENG]]></title><description><![CDATA[<img src="assets/l14-tomasulo-cdb-eng.svg" target="_self">]]></description><link>assets/l14-tomasulo-cdb-eng.html</link><guid isPermaLink="false">assets/L14-Tomasulo-CDB-ENG.svg</guid><pubDate>Tue, 14 Apr 2026 05:56:43 GMT</pubDate><enclosure url="assets/l14-tomasulo-cdb-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-tomasulo-cdb-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L14-Branch-Predictor-2bit-ENG]]></title><description><![CDATA[<img src="assets/l14-branch-predictor-2bit-eng.svg" target="_self">]]></description><link>assets/l14-branch-predictor-2bit-eng.html</link><guid isPermaLink="false">assets/L14-Branch-Predictor-2bit-ENG.svg</guid><pubDate>Tue, 14 Apr 2026 05:55:48 GMT</pubDate><enclosure url="assets/l14-branch-predictor-2bit-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l14-branch-predictor-2bit-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Lab13]]></title><description><![CDATA[
Курс: Архітектура обчислювальних систем
Тиждень: 13
Тривалість: 1 год 20 хв (аудиторна) + домашнє завдання
Тип: Практична робота під наглядом інструктора
Після завершення практики студент зможе:
Розкласти 32-бітне число IEEE 754 на знак, порядок та мантису засобами x86-64 асемблера.
Вивести число з рухомою комою у двійковій науковій нотації.
Реалізувати додавання двох чисел з рухомою комою на рівні бітових операцій.
Пояснити, чому 0.1 + 0.2 ≠ 0.3 на рівні бітового представлення.
Реалізувати множення чисел з рухомою комою (домашнє завдання).
Перегляньте лекційні матеріали за Тиждень 13 (Арифметика з рухомою комою). Зверніть увагу на розділи:
IEEE 754 single precision — три поля: знак (1 біт), порядок (8 біт), мантиса (23 біти)
Прихований біт — нормалізована мантиса завжди починається з 1., тому цю одиницю не зберігають
Зміщений порядок (biased exponent) — зберігається E = e + 127, де e — справжній порядок
Алгоритм додавання — вирівнювання порядків, додавання мантис, нормалізація результату Корисний інструмент: <a data-tooltip-position="top" aria-label="https://float.exposed/" rel="noopener nofollow" class="external-link is-unresolved" href="https://float.exposed/" target="_self">float.exposed</a> — інтерактивний візуалізатор бітів IEEE 754. Використовуйте його для перевірки ваших результатів.
32-бітне число з рухомою комою складається з трьох полів: 31 30 23 22 0
┌───┬──────────┬───────────────────────────┐
│ S │ Exponent │ Mantissa │
│1 b│ 8 bits │ 23 bits │
└───┴──────────┴───────────────────────────┘
Значення (для нормалізованих чисел): A = (-1)^S × 1.M × 2^(E - 127)
S — знак: 0 = додатне, 1 = від'ємне
E — зміщений порядок (biased exponent): зберігає e + 127
M — 23 біти мантиси після прихованого біта 1.
Приклад: 0x41200000 (число 10.0)0 10000010 01000000000000000000000
S E=130 M=0x200000 Знак: 0 → +
Порядок: 130 − 127 = 3
Мантиса: 1.01000000000000000000000
Значення: +1.01₂ × 2³ = 1.25 × 8 = 10.0
Щоб додати два числа з рухомою комою A + B:
Розкласти обидва числа на знак, порядок, мантису (з прихованим бітом)
Вирівняти — зсунути мантису меншого числа праворуч на різницю порядків
Додати або відняти мантиси (залежно від знаків)
Нормалізувати результат — зсунути мантису так, щоб старший біт був у позиції 23
Запакувати знак, порядок та мантису назад у 32 біти
Приклад: 10.0 + 5.0A = 1.010 × 2³ (exp=3, mant=0xA00000)
B = 1.010 × 2² (exp=2, mant=0xA00000) Вирівнювання: зсунути B на 1 біт → 0.101 × 2³ (mant=0x500000)
Додавання: 1.010 + 0.101 = 1.111 × 2³ (mant=0xF00000)
Результат: 0x41700000 = 15.0 ✓
У цій практиці ви використовуватимете готові функції для виводу (файл fp_utils.inc). Усі функції зберігають усі регістри — можна викликати без push/pop навколо:; Вивести символ
mov dil, '+' ; DIL = символ для виводу
call print_char ; Вивести рядок
lea rsi, [str_label] ; RSI = адреса рядка
mov rdx, 5 ; RDX = довжина
call print_string ; Вивести беззнакове число
mov edi, 130 ; EDI = число
call print_uint ; виведе "130" ; Вивести знакове число
mov edi, -4 ; EDI = число зі знаком
call print_int ; виведе "-4" ; Вивести hex
mov edi, 0x41200000 ; EDI = 32-бітне значення
call print_hex32 ; виведе "0x41200000" call print_newline ; вивести '\n'
mkdir -p Lab13 &amp;&amp; cd Lab13 Якщо NASM ще не встановлено (з Lab 11), виконайте:
sudo apt-get update &amp;&amp; sudo apt-get install -y nasm
Створіть файл fp_utils.inc з наступним вмістом. Не змінюйте цей файл — він містить готові функції для виводу:; ==============================================================
; fp_utils.inc — Допоміжні функції для виводу
;
; Підключення: %include "fp_utils.inc" (в кінці .asm файлу)
;
; Усі функції ЗБЕРІГАЮТЬ значення всіх регістрів.
; Можна викликати без push/pop навколо виклику.
; ============================================================== section .bss _pu_char_buf resb 1 _pu_uint_buf resb 12 _pu_hex_buf resb 8 section .data _pu_hex_prefix db "0x" section .text ; --- print_char: вивести символ з DIL ---
print_char: push rax push rcx push rdx push rsi push rdi push r11 mov byte [_pu_char_buf], dil mov rax, 1 mov rdi, 1 lea rsi, [_pu_char_buf] mov rdx, 1 syscall pop r11 pop rdi pop rsi pop rdx pop rcx pop rax ret ; --- print_newline: вивести '\n' ---
print_newline: push rdi mov dil, 10 call print_char pop rdi ret ; --- print_string: вивести рядок (RSI=адреса, RDX=довжина) ---
print_string: push rax push rcx push rdi push r11 mov rax, 1 mov rdi, 1 syscall pop r11 pop rdi pop rcx pop rax ret ; --- print_uint: вивести беззнакове 32-бітне число з EDI ---
print_uint: push rax push rbx push rcx push rdx push rsi push rdi push r11 mov eax, edi lea rsi, [_pu_uint_buf + 11] xor ecx, ecx mov ebx, 10 test eax, eax jnz .pu_loop dec rsi mov byte [rsi], '0' inc ecx jmp .pu_write
.pu_loop: xor edx, edx div ebx add dl, '0' dec rsi mov [rsi], dl inc ecx test eax, eax jnz .pu_loop
.pu_write: mov rdx, rcx mov rax, 1 mov rdi, 1 syscall pop r11 pop rdi pop rsi pop rdx pop rcx pop rbx pop rax ret ; --- print_int: вивести знакове 32-бітне число з EDI ---
print_int: test edi, edi jns print_uint push rdi mov dil, '-' call print_char pop rdi neg edi jmp print_uint ; --- print_hex32: вивести 32-бітне число з EDI як 0xHHHHHHHH ---
print_hex32: push rax push rbx push rcx push rdx push rsi push rdi push r11 mov ebx, edi mov rax, 1 mov rdi, 1 lea rsi, [_pu_hex_prefix] mov rdx, 2 syscall mov ecx, 8 lea rsi, [_pu_hex_buf]
.ph_loop: rol ebx, 4 mov eax, ebx and eax, 0xF cmp al, 10 jl .ph_digit add al, ('A' - 10) jmp .ph_store
.ph_digit: add al, '0'
.ph_store: mov [rsi], al inc rsi dec ecx jnz .ph_loop lea rsi, [_pu_hex_buf] mov rdx, 8 mov rax, 1 mov rdi, 1 syscall pop r11 pop rdi pop rsi pop rdx pop rcx pop rbx pop rax ret
Створіть мінімальний тест test_utils.asm:default rel
section .text global _start _start: mov edi, 0x41200000 call print_hex32 call print_newline mov rax, 60 xor rdi, rdi syscall %include "fp_utils.inc"
Зберіть та запустіть:nasm -f elf64 -g test_utils.asm -o test_utils.o &amp;&amp; ld test_utils.o -o test_utils &amp;&amp; ./test_utils
Ви маєте побачити: 0x41200000
Якщо все працює — видаліть test_utils.asm і test_utils.o, вони більше не потрібні.
Напишіть програму, яка розкладає 32-бітне число IEEE 754 на компоненти та виводить його у двійковій науковій нотації:0x41200000 = +1.01000000000000000000000 * 2^3
Програма побудована як функція decode_print, яку _start викликає для кожного тестового значення. Витягування знаку дано як робочий приклад — вивчіть його, потім реалізуйте три TODO.Створіть файл decode.asm:; decode.asm — Декодування IEEE 754 single precision
;
; Збірка: nasm -f elf64 -g decode.asm -o decode.o &amp;&amp; ld decode.o -o decode
; Запуск: ./decode
; Дебаг: gdb ./decode → b _start → r → si ... default rel section .data ; Тестові значення — перевіряйте на https://float.exposed float_10 dd 0x41200000 ; 10.0 float_neg5 dd 0xC0A00000 ; -5.0 float_0_1 dd 0x3DCCCCCD ; 0.1 (наближення!) float_1 dd 0x3F800000 ; 1.0 str_eq db " = " str_eq_len equ $ - str_eq str_exp db " * 2^" str_exp_len equ $ - str_exp section .text global _start _start: ; Викликаємо decode_print для кожного тестового значення. ; Ваші TODO всередині функції — усі виклики запрацюють одразу. mov r12d, [float_10] call decode_print mov r12d, [float_neg5] call decode_print mov r12d, [float_0_1] call decode_print mov r12d, [float_1] call decode_print ; Вихід mov rax, 60 xor rdi, rdi syscall ; ============================================================
; decode_print — декодувати та вивести IEEE 754 число
; Вхід: R12D = 32-бітне значення IEEE 754
; Вивід на stdout: "0xHHHHHHHH = ±1.bbb...b * 2^N\n"
; ============================================================
decode_print: ; --- Вивести hex та " = " --- mov edi, r12d call print_hex32 lea rsi, [str_eq] mov rdx, str_eq_len call print_string ; ============================================ ; ПРИКЛАД (надано): Витягти біт знаку та вивести '+' або '-' ; ; Біт 31 — це знак. SHR на 31 ізолює його в EAX (0 або 1). ; Потім умовний перехід вибирає символ для виводу. ; ============================================ mov eax, r12d ; скопіювати оригінал shr eax, 31 ; EAX = біт знаку (0 або 1) test eax, eax ; порівняти з нулем jnz .negative ; якщо 1 → від'ємне mov dil, '+' jmp .after_sign
.negative: mov dil, '-'
.after_sign: call print_char ; вивести '+' або '-' ; ============================================ ; TODO 1: Вивести "1." (прихований біт + крапка) ; ; Виведіть два символи: '1' та '.' ; ============================================ ; ============================================ ; TODO 2: Витягти 23 біти мантиси та вивести як '0'/'1' ; ; 1. Скопіюйте R12D в EAX, застосуйте маску: AND EAX, 0x7FFFFF ; 2. Цикл від біта 22 до біта 0 (ECX = лічильник): ; - BT EAX, ECX — перевірити біт, результат у CF ; - JC — перейти якщо CF=1 (біт = 1) ; - Вивести '0' або '1' через print_char ; - DEC ECX, потім JNS — повторити поки ECX &gt;= 0 ; ; Всього 23 ітерації. print_char зберігає EAX та ECX. ; ; Потрібні інструкції: MOV, AND, BT, JC, JMP, DEC, JNS, CALL ; ============================================ ; ============================================ ; TODO 3: Витягти порядок та вивести " * 2^N" ; ; 1. Скопіюйте R12D в EAX ; 2. SHR EAX, 23 та AND EAX, 0xFF — зміщений порядок ; 3. SUB EAX, 127 — справжній порядок (може бути від'ємним!) ; 4. Виведіть рядок str_exp через print_string (зберігає EAX) ; 5. Виведіть порядок через print_int (EDI = EAX) ; ; Потрібні інструкції: MOV, SHR, AND, SUB, LEA, CALL ; ============================================ call print_newline ret %include "fp_utils.inc"
Коли всі TODO виконано правильно:0x41200000 = +1.01000000000000000000000 * 2^3
0xC0A00000 = -1.01000000000000000000000 * 2^2
0x3DCCCCCD = +1.10011001100110011001101 * 2^-4
0x3F800000 = +1.00000000000000000000000 * 2^0 Зверніть увагу: Вивчіть вивід 0.1 (0x3DCCCCCD): мантиса 10011001100... — це нескінченний періодичний двійковий дріб, обрізаний до 23 біт. Саме тому 0.1 не представляється точно в IEEE 754.
Реалізуйте додавання двох 32-бітних IEEE 754 чисел на рівні бітових операцій, без використання FPU або SSE інструкцій.Створіть файл add.asm:; add.asm — Додавання IEEE 754 single precision
;
; Збірка: nasm -f elf64 -g add.asm -o add.o &amp;&amp; ld add.o -o add
; Запуск: ./add default rel section .data ; Тестові пари (A + B = очікуваний результат) test1_a dd 0x41200000 ; 10.0 test1_b dd 0x40A00000 ; 5.0 test1_exp dd 0x41700000 ; 15.0 test2_a dd 0x41200000 ; 10.0 test2_b dd 0xC0A00000 ; -5.0 test2_exp dd 0x40A00000 ; 5.0 test3_a dd 0x3DCCCCCD ; 0.1 test3_b dd 0x3E4CCCCD ; 0.2 test3_exp dd 0x3E99999A ; 0.3 (IEEE 754 з округленням) str_plus db " + " str_plus_l equ $ - str_plus str_eq db " = " str_eq_l equ $ - str_eq str_want db " (want: " str_want_l equ $ - str_want str_rparen db ")" str_rparen_l equ $ - str_rparen section .bss ; Робочі змінні для алгоритму sign_a resd 1 exp_a resd 1 mant_a resd 1 sign_b resd 1 exp_b resd 1 mant_b resd 1 sign_r resd 1 exp_r resd 1 mant_r resd 1 section .text global _start _start: ; === Тест 1: 10.0 + 5.0 = 15.0 === mov r12d, [test1_a] ; R12D = операнд A mov r13d, [test1_b] ; R13D = операнд B mov r15d, [test1_exp] ; R15D = очікуваний результат call fp_add ; R14D = результат ; Вивести: "0xAAAAAAAA + 0xBBBBBBBB = 0xRRRRRRRR (want: 0xEEEEEEEE)" mov edi, r12d call print_hex32 lea rsi, [str_plus] mov rdx, str_plus_l call print_string mov edi, r13d call print_hex32 lea rsi, [str_eq] mov rdx, str_eq_l call print_string mov edi, r14d call print_hex32 lea rsi, [str_want] mov rdx, str_want_l call print_string mov edi, r15d call print_hex32 mov dil, ')' call print_char call print_newline ; === Тест 2: 10.0 + (-5.0) = 5.0 === mov r12d, [test2_a] mov r13d, [test2_b] mov r15d, [test2_exp] call fp_add mov edi, r12d call print_hex32 lea rsi, [str_plus] mov rdx, str_plus_l call print_string mov edi, r13d call print_hex32 lea rsi, [str_eq] mov rdx, str_eq_l call print_string mov edi, r14d call print_hex32 lea rsi, [str_want] mov rdx, str_want_l call print_string mov edi, r15d call print_hex32 mov dil, ')' call print_char call print_newline ; === Тест 3: 0.1 + 0.2 ≈ 0.3 === mov r12d, [test3_a] mov r13d, [test3_b] mov r15d, [test3_exp] call fp_add mov edi, r12d call print_hex32 lea rsi, [str_plus] mov rdx, str_plus_l call print_string mov edi, r13d call print_hex32 lea rsi, [str_eq] mov rdx, str_eq_l call print_string mov edi, r14d call print_hex32 lea rsi, [str_want] mov rdx, str_want_l call print_string mov edi, r15d call print_hex32 mov dil, ')' call print_char call print_newline ; === Вихід === mov rax, 60 xor rdi, rdi syscall ; ================================================================
; fp_add — додати два числа IEEE 754 single precision
;
; Вхід: R12D = операнд A, R13D = операнд B
; Вихід: R14D = результат A + B
;
; Обмеження: не обробляє NaN, Inf, денормалізовані числа, нулі.
; Припускаємо, що обидва операнди — нормалізовані ненульові числа.
; ================================================================
fp_add: push rax push rbx push rcx push rdx ; ───────────────────────────────────── ; КРОК 1: Розкладання A (надано) ; ───────────────────────────────────── mov eax, r12d shr eax, 31 mov [sign_a], eax ; sign_a = біт 31 mov eax, r12d shr eax, 23 and eax, 0xFF mov [exp_a], eax ; exp_a = зміщений порядок (1..254) mov eax, r12d and eax, 0x7FFFFF or eax, 0x800000 ; додати прихований біт mov [mant_a], eax ; mant_a = 24-бітна мантиса ; ───────────────────────────────────── ; КРОК 2: Розкладання B (надано) ; ───────────────────────────────────── mov eax, r13d shr eax, 31 mov [sign_b], eax mov eax, r13d shr eax, 23 and eax, 0xFF mov [exp_b], eax mov eax, r13d and eax, 0x7FFFFF or eax, 0x800000 mov [mant_b], eax ; ───────────────────────────────────── ; КРОК 3: Якщо exp_a &lt; exp_b, поміняти місцями (надано) ; ; Ми хочемо, щоб A мало більший порядок — інакше ; вирівнювання зсуне не ту мантису. ; ───────────────────────────────────── mov eax, [exp_a] cmp eax, [exp_b] jge .no_swap ; Поміняти sign_a ↔ sign_b mov eax, [sign_a] mov edx, [sign_b] mov [sign_a], edx mov [sign_b], eax ; Поміняти exp_a ↔ exp_b mov eax, [exp_a] mov edx, [exp_b] mov [exp_a], edx mov [exp_b], eax ; Поміняти mant_a ↔ mant_b mov eax, [mant_a] mov edx, [mant_b] mov [mant_a], edx mov [mant_b], eax
.no_swap: ; ───────────────────────────────────── ; КРОК 4: TODO — Вирівняти мантиси ; ; Обчисліть ECX = exp_a - exp_b (різниця порядків). ; Якщо різниця &gt; 24 → mant_b = 0 (усі біти втрачено). ; Інакше → SHR [mant_b] на CL біт. ; ; Увага: для змінного зсуву SHR використовує регістр CL, ; не ECX. Напишіть: shr eax, cl ; ───────────────────────────────────── ; ───────────────────────────────────── ; КРОК 5: TODO — Додати або відняти мантиси ; ; Порівняйте sign_a та sign_b: ; ; Однакові знаки → ADD mant_a + mant_b. ; Знак результату = sign_a. Перехід до .normalize. ; ; Різні знаки → SUB mant_a - mant_b. ; Якщо результат SUB від'ємний (CF=1 після SUB): ; NEG EAX та інвертуйте знак (XOR sign_r, 1). ; Інакше: знак результату = sign_a. ; ; В обох випадках збережіть mant_r, sign_r, exp_r (= exp_a). ; Потім JMP .normalize. ; ───────────────────────────────────── ; ───────────────────────────────────── ; КРОК 6: TODO — Нормалізація ; .normalize: ; ; Завантажте mant_r в EAX. Три випадки: ; ; 1. mant_r = 0 → результат +0.0: MOV R14D, 0 та JMP .done ; ; 2. Біт 24 встановлений (TEST EAX, 0x1000000) — переповнення: ; → SHR EAX, 1 та INC exp_r. Перехід до .pack. ; ; 3. Біт 23 НЕ встановлений (TEST EAX, 0x800000) — недоповнення: ; → Цикл: SHL EAX, 1 та DEC exp_r, поки біт 23 не з'явиться. ; → Перехід до .pack. ; ; Не забудьте зберегти EAX назад у [mant_r] перед .pack. ; ───────────────────────────────────── ; ───────────────────────────────────── ; КРОК 7: TODO — Запакувати результат у R14D ; .pack: ; ; Формула: R14D = (sign_r &lt;&lt; 31) | (exp_r &lt;&lt; 23) | (mant_r &amp; 0x7FFFFF) ; ; Побудуйте 32-бітне значення через SHL, AND, OR. ; Не забудьте прибрати прихований біт з мантиси (AND 0x7FFFFF). ; ───────────────────────────────────── .done: pop rdx pop rcx pop rbx pop rax ret %include "fp_utils.inc"
0x41200000 + 0x40A00000 = 0x41700000 (want: 0x41700000)
0x41200000 + 0xC0A00000 = 0x40A00000 (want: 0x40A00000)
0x3DCCCCCD + 0x3E4CCCCD = 0x3E999999 (want: 0x3E99999A) Третій тест: ваш результат 0x3E999999 відрізняється на 1 біт від апаратного 0x3E99999A. Це тому, що ми не реалізували округлення — просто відкидаємо біти при зсуві (truncation). Апаратний FPU використовує guard/round/sticky біти для правильного округлення. Різниця — 1 ULP (unit in the last place).
Додайте у add.asm додаткові тести. Запишіть результати у файл results.txt.Додайте тестову пару: test4_a dd 0x49742400 ; 1000000.0 test4_b dd 0x3A83126F ; 0.001 test4_exp dd 0x49742400 ; 1000000.0 (B поглинається!)
Питання для results.txt: Різниця порядків 1000000.0 та 0.001 дорівнює 29. Мантиса має лише 24 біти. Що відбувається при зсуві мантиси на 29 біт праворуч? Чому результат = A? test5_a dd 0x41200000 ; 10.0 test5_b dd 0xC1200000 ; -10.0 test5_exp dd 0x00000000 ; 0.0
Питання для results.txt: Що відбувається, коли мантиси повністю скорочуються? Який hex-результат ви отримали?=== Практика 13: IEEE 754 вручну === --- Частина 1: Декодування ---
0x41200000 = +1.01000000000000000000000 * 2^3 (10.0)
0xC0A00000 = ___
0x3DCCCCCD = ___
0x3F800000 = ___ Питання: Чому мантиса 0.1 — періодичний дріб (10011001100...)?
Відповідь: ___ --- Частина 2: Додавання ---
10.0 + 5.0 = 0x___ (очікувано: 0x41700000, збіг: так/ні)
10.0 + (-5.0)= 0x___ (очікувано: 0x40A00000, збіг: так/ні)
0.1 + 0.2 = 0x___ (очікувано: 0x3E99999A) Питання: Чому 0.1 + 0.2 дає 0x3E999999, а не 0x3E99999A?
Відповідь: ___ --- Частина 3: Граничні випадки ---
1000000.0 + 0.001 = 0x___ (очікувано: 0x49742400 = без змін)
Чому 0.001 зникає?
Відповідь: ___ 10.0 + (-10.0) = 0x___ (очікувано: 0x00000000)
Дедлайн: 23:59 у день перед Практикою 14Завершіть тести з Частини 3 та заповніть results.txt.Множення простіше за додавання — немає кроку вирівнювання:
Знак: XOR знаків операндів
Порядок: додати порядки та відняти зміщення (bias): exp_r = exp_a + exp_b - 127
Мантиса: помножити мантиси (24 × 24 = 48 біт). Інструкція imul rax, rdx у 64-бітному режимі дає 64-бітний результат
Нормалізація: старший біт результату на позиції 46 або 47: Якщо біт 47 встановлений → зсунути на 24, порядок + 1
Інакше → зсунути на 23 Запакувати результат
Створіть файл mul.asm:; mul.asm — Множення IEEE 754 single precision
;
; Збірка: nasm -f elf64 -g mul.asm -o mul.o &amp;&amp; ld mul.o -o mul
; Запуск: ./mul default rel section .data ; Тестові пари (A * B = очікуваний результат) test1_a dd 0x40000000 ; 2.0 test1_b dd 0x40400000 ; 3.0 test1_exp dd 0x40C00000 ; 6.0 test2_a dd 0x40400000 ; 3.0 test2_b dd 0x40400000 ; 3.0 test2_exp dd 0x41100000 ; 9.0 test3_a dd 0x40A00000 ; 5.0 test3_b dd 0xC0400000 ; -3.0 test3_exp dd 0xC1700000 ; -15.0 test4_a dd 0x41200000 ; 10.0 test4_b dd 0x3DCCCCCD ; 0.1 test4_exp dd 0x3F800000 ; 1.0 (чи точно?) str_times db " * " str_times_l equ $ - str_times str_eq db " = " str_eq_l equ $ - str_eq str_want db " (want: " str_want_l equ $ - str_want section .bss sign_a resd 1 exp_a resd 1 mant_a resd 1 sign_b resd 1 exp_b resd 1 mant_b resd 1 sign_r resd 1 exp_r resd 1 mant_r resd 1 section .text global _start _start: ; === Тест 1: 2.0 * 3.0 = 6.0 === mov r12d, [test1_a] mov r13d, [test1_b] mov r15d, [test1_exp] call fp_mul ; Вивід (аналогічно add.asm) mov edi, r12d call print_hex32 lea rsi, [str_times] mov rdx, str_times_l call print_string mov edi, r13d call print_hex32 lea rsi, [str_eq] mov rdx, str_eq_l call print_string mov edi, r14d call print_hex32 lea rsi, [str_want] mov rdx, str_want_l call print_string mov edi, r15d call print_hex32 mov dil, ')' call print_char call print_newline ; === Тест 2: 3.0 * 3.0 = 9.0 === mov r12d, [test2_a] mov r13d, [test2_b] mov r15d, [test2_exp] call fp_mul mov edi, r12d call print_hex32 lea rsi, [str_times] mov rdx, str_times_l call print_string mov edi, r13d call print_hex32 lea rsi, [str_eq] mov rdx, str_eq_l call print_string mov edi, r14d call print_hex32 lea rsi, [str_want] mov rdx, str_want_l call print_string mov edi, r15d call print_hex32 mov dil, ')' call print_char call print_newline ; === Тест 3: 5.0 * (-3.0) = -15.0 === mov r12d, [test3_a] mov r13d, [test3_b] mov r15d, [test3_exp] call fp_mul mov edi, r12d call print_hex32 lea rsi, [str_times] mov rdx, str_times_l call print_string mov edi, r13d call print_hex32 lea rsi, [str_eq] mov rdx, str_eq_l call print_string mov edi, r14d call print_hex32 lea rsi, [str_want] mov rdx, str_want_l call print_string mov edi, r15d call print_hex32 mov dil, ')' call print_char call print_newline ; === Тест 4: 10.0 * 0.1 ≈ 1.0? === mov r12d, [test4_a] mov r13d, [test4_b] mov r15d, [test4_exp] call fp_mul mov edi, r12d call print_hex32 lea rsi, [str_times] mov rdx, str_times_l call print_string mov edi, r13d call print_hex32 lea rsi, [str_eq] mov rdx, str_eq_l call print_string mov edi, r14d call print_hex32 lea rsi, [str_want] mov rdx, str_want_l call print_string mov edi, r15d call print_hex32 mov dil, ')' call print_char call print_newline ; === Вихід === mov rax, 60 xor rdi, rdi syscall ; ================================================================
; fp_mul — помножити два числа IEEE 754 single precision
;
; Вхід: R12D = операнд A, R13D = операнд B
; Вихід: R14D = результат A * B
; ================================================================
fp_mul: push rax push rbx push rcx push rdx ; ───────────────────────────────────── ; КРОК 1: Розкладання A та B (надано) ; ───────────────────────────────────── mov eax, r12d shr eax, 31 mov [sign_a], eax mov eax, r12d shr eax, 23 and eax, 0xFF mov [exp_a], eax mov eax, r12d and eax, 0x7FFFFF or eax, 0x800000 mov [mant_a], eax mov eax, r13d shr eax, 31 mov [sign_b], eax mov eax, r13d shr eax, 23 and eax, 0xFF mov [exp_b], eax mov eax, r13d and eax, 0x7FFFFF or eax, 0x800000 mov [mant_b], eax ; ───────────────────────────────────── ; КРОК 2: TODO — Знак результату ; ; sign_r = sign_a XOR sign_b ; ───────────────────────────────────── ; ───────────────────────────────────── ; КРОК 3: TODO — Порядок результату ; ; exp_r = exp_a + exp_b - 127 ; (обидва порядки зміщені, результату потрібне одне зміщення) ; ───────────────────────────────────── ; ───────────────────────────────────── ; КРОК 4: TODO — Множення мантис (64-бітне) ; ; mant_a та mant_b — 24-бітні. Добуток — до 48 біт. ; Завантажте кожну мантису в 64-бітний регістр ; (MOV EAX, [mant_a] автоматично обнулює верхні 32 біти RAX). ; Використайте IMUL RAX, reg для 64-бітного множення. ; Результат залишиться в RAX для наступного кроку. ; ───────────────────────────────────── ; ───────────────────────────────────── ; КРОК 5: TODO — Нормалізація добутку ; ; Добуток 1.xx × 1.xx дає результат від 1.0 до ~4.0, ; тому старший біт знаходиться на позиції 46 або 47. ; ; Якщо біт 47 встановлений (BT RAX, 47): ; → SHR RAX, 24 та INC exp_r ; Інакше: ; → SHR RAX, 23 ; ; Збережіть EAX (нижні 32 біти RAX) у [mant_r]. ; ───────────────────────────────────── ; ───────────────────────────────────── ; КРОК 6: TODO — Запакувати результат у R14D ; ; Як у add.asm (КРОК 7). ; ───────────────────────────────────── pop rdx pop rcx pop rbx pop rax ret %include "fp_utils.inc"
0x40000000 * 0x40400000 = 0x40C00000 (want: 0x40C00000)
0x40400000 * 0x40400000 = 0x41100000 (want: 0x41100000)
0x40A00000 * 0xC0400000 = 0xC1700000 (want: 0xC1700000)
0x41200000 * 0x3DCCCCCD = 0x3F?????? (want: 0x3F800000) Тест 4 (10.0 × 0.1): якщо ваш результат ≠ 0x3F800000 (1.0), це нормально — 0.1 не представляється точно в IEEE 754, тому добуток не дорівнює точно 1.0. Запишіть, чим саме відрізняється ваш результат.
Ділення аналогічне множенню, але:
Знак: XOR знаків (як і при множенні)
Порядок: exp_r = exp_a - exp_b + 127
Мантиса: зсунути mant_a ліворуч на 23 біти (для точності), потім поділити:
xor rax, rax
mov eax, [mant_a]
shl rax, 23 ; RAX = mant_a &lt;&lt; 23 (до 47 біт)
xor rdx, rdx ; RDX:RAX = дільне
xor rcx, rcx
mov ecx, [mant_b]
div rcx ; RAX = частка (≈24 біти) Нормалізація: як при множенні — перевірити позицію старшого біта
Створіть div.asm з тестами 6.0 / 2.0 = 3.0, 15.0 / 3.0 = 5.0, 1.0 / 3.0 ≈ 0.333...Запишіть відео, у якому:
Декодування: запустіть decode і поясніть вивід для 0.1 — чому мантиса повторюється?
Додавання: запустіть add, покажіть результат 0.1 + 0.2. Чому ≠ 0.3?
Множення: запустіть mul, покажіть результат 10.0 × 0.1. Чому ≠ 1.0?
Поясніть один з алгоритмів (додавання або множення) на рівні бітів — що відбувається на кожному кроці
УВАГА! Вимога про 5хв максимально важлива! Brevity is the soul of wit. Це частина оцінки.Lab13/
├── fp_utils.inc ← допоміжні функції (НЕ змінювати)
├── decode.asm ← Частина 1: декодування IEEE 754
├── add.asm ← Частина 2: додавання
├── results.txt ← Частина 3: граничні випадки та відповіді
├── mul.asm ← домашнє: множення
├── div.asm ← бонус: ділення (якщо є)
└── video.txt ← посилання на Loom
Не забудьте git push! Якщо ви працюєте у Codespace — без push ваша робота існує лише на віддаленій машині GitHub.
<br><a data-tooltip-position="top" aria-label="https://float.exposed/" rel="noopener nofollow" class="external-link is-unresolved" href="https://float.exposed/" target="_self">float.exposed</a> — інтерактивний візуалізатор IEEE 754
<br><a data-tooltip-position="top" aria-label="https://www.h-schmidt.net/FloatConverter/IEEE754.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.h-schmidt.net/FloatConverter/IEEE754.html" target="_self">IEEE 754 Converter</a> — конвертер decimal ↔ hex ↔ binary
<br><a data-tooltip-position="top" aria-label="https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/" rel="noopener nofollow" class="external-link is-unresolved" href="https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/" target="_self">x86-64 Linux Syscall Table</a> — таблиця системних викликів
<br><a data-tooltip-position="top" aria-label="https://www.nasm.us/xdoc/2.16.03/html/nasmdoc0.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.nasm.us/xdoc/2.16.03/html/nasmdoc0.html" target="_self">NASM Documentation</a> — довідка NASM
<br><a data-tooltip-position="top" aria-label="https://hugsy.github.io/gef/" rel="noopener nofollow" class="external-link is-unresolved" href="https://hugsy.github.io/gef/" target="_self">GEF Documentation</a> — довідка GEF для GDB
]]></description><link>labs/lab13.html</link><guid isPermaLink="false">Labs/Lab13.md</guid><pubDate>Tue, 07 Apr 2026 06:14:53 GMT</pubDate></item><item><title><![CDATA[L13-ENG. Floating-Point Arithmetic]]></title><description><![CDATA[Course: Computer Systems Architecture
Topic: IEEE 754 in Detail, Rounding Modes, Numerical Errors, x87 vs SIMD
Learning Outcomes:
LO2: Explain principles of data representation in computer systems
LO5: Analyze performance and limitations of computer systems
Open any programming language and type the simplest possible arithmetic:&gt; 0.1 + 0.2
0.30000000000000004
That trailing 4 surprises every programmer who encounters it for the first time. It is not a bug in JavaScript — the same result appears in Python, C, Java, Rust, and every other language that uses the hardware's floating-point unit. The problem is not the language; the problem is the gap between the mathematical real numbers you learned in school and the finite binary representations a computer must use.<img alt="Meme showing 0.1 + 0.2 = 0.30000000000000004 — the classic floating-point precision surprise" src="assets/l13-fp-precision-meme.jpeg" target="_self">The classic floating-point precision meme: every programmer eventually discovers that 0.1 + 0.2 does not equal 0.3 in binary floating-point.This lecture is about that gap — where it comes from, how hardware engineers and mathematicians agreed on a standard way to manage it (IEEE 754), and how you, as a programmer, can avoid the most dangerous pitfalls.Consider the number π. Its decimal expansion begins 3.14159265358979323846… and continues forever, never repeating. Even if you devoted every transistor in the world's largest supercomputer to storing digits of π, you could not capture the exact value — you would always be rounding.<br><img alt="Hundreds of digits of the number pi displayed in a grid" src="assets/l13-pi-digits.png" target="_self">The decimal expansion of pi continues forever without repeating — no finite representation can capture its exact value.And π is not special. The vast majority of real numbers have infinitely long representations in any base. The number 1/3 = 0.333… is infinite in decimal. The number 0.1 in decimal — perfectly finite when you write it — becomes the infinite repeating fraction 0.0001100110011… in binary. A computer word is 32 or 64 bits. Into those bits, you must squeeze an approximation of a number that may need infinitely many digits. Something has to give, and what gives is exactness. The stored number is close to the mathematical value, but rarely identical.Here is what happens when you ask JavaScript to compute 19.9 × 100:<br><img alt="JavaScript console showing 19.9 times 100 producing 1989.9999999999998 instead of 1990" src="assets/l13-js-console-fp-demo.jpeg" target="_self">A JavaScript console demonstrating that 19.9 x 100 yields 1989.9999999999998 — a tiny floating-point rounding error that can break equality comparisons.The first three multiplications produce exact integers. The fourth one is off by roughly 2 × 10⁻¹³ — a tiny error in absolute terms, but one that could cause a real problem if you were comparing the result to 1990 with ==. This is not a JavaScript quirk. The same pattern occurs in every language:&gt;&gt;&gt; 19.9 * 100
1989.9999999999998
Console.WriteLine(19.9 * 100); // 1990 (C# rounds for display, but the bits are the same)
Console.WriteLine(19.9 * 100 == 1990); // False
The underlying cause is always the same: 19.9 cannot be represented exactly in binary floating-point, so the hardware stores a nearby value, and that tiny initial error propagates through the multiplication.Floating-point errors are not merely an annoyance for textbook exercises. They have caused real disasters:<br>The Patriot Missile Failure (1991). During the Gulf War, a <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/MIM-104_Patriot#Failure_at_Dhahran" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/MIM-104_Patriot#Failure_at_Dhahran" target="_self">Patriot missile defense battery</a> in Dhahran, Saudi Arabia, failed to intercept an incoming Iraqi Scud missile, which struck an American barracks and killed 28 soldiers. The root cause was a floating-point rounding error in the system's internal clock. The system tracked time as a count of 0.1-second intervals, but 0.1 cannot be represented exactly in binary. After 100 hours of continuous operation, the accumulated error in the time calculation was about 0.34 seconds — during which a Scud missile travels over 500 meters. The tracking system placed the incoming missile in the wrong part of the sky and never fired.<br>The Ariane 5 Explosion (1996). The European Space Agency's <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Ariane_flight_V88" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Ariane_flight_V88" target="_self">Ariane 5 rocket</a> exploded 37 seconds after launch, destroying a $370 million payload. The cause was an unprotected conversion of a 64-bit floating-point value (the rocket's horizontal velocity) into a 16-bit signed integer. The Ariane 5 was faster than the Ariane 4 whose software it reused, and the velocity value exceeded 32,767 — the maximum for a 16-bit integer. The overflow triggered a diagnostic dump that was misinterpreted as flight data, causing the guidance system to command a hard turn. The rocket tore itself apart from aerodynamic stress.<br>The Vancouver Stock Exchange Error (1982). The Vancouver Stock Exchange <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Vancouver_Stock_Exchange#Technical_issues" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Vancouver_Stock_Exchange#Technical_issues" target="_self">introduced a new index</a> set to 1000.000 in January 1982. The index was recalculated after every trade by truncating to three decimal places rather than rounding. After 22 months of accumulated truncation errors, the index had drifted to 524.811 — roughly half its correct value of about 1098.892. The exchange had to correct the index and fix the rounding logic.These examples share a common lesson: floating-point arithmetic is not broken, but it follows rules that differ from the real-number arithmetic you learned in math class. If you do not understand those rules, errors accumulate silently until they become catastrophic.Floating-point arithmetic is not a niche concern limited to scientific computing. It appears everywhere in modern software:Every programmer encounters floating-point eventually. The question is whether you encounter it with understanding or with surprise.Over the next sections, we will build a complete understanding of how computers represent and operate on real numbers:
Representation errors — the mathematical framework for measuring how far a stored number is from the true value.
Fixed-point binary fractions — the simplest approach, its strengths, and why it is not enough for general-purpose computing.
Scientific notation and normalization — the key idea that makes floating-point work: trading a fixed decimal point for a movable one.
The IEEE 754 standard — the specific encoding used by virtually every modern processor: sign, exponent, mantissa, bias.
Decoding and encoding — how to convert between decimal values and their 32-bit or 64-bit binary representations.
Special values — positive and negative zero, infinities, NaN, and denormalized numbers.
IEEE 754 formats — single precision, double precision, and extended precision.
Floating-point arithmetic and rounding — how addition, multiplication, and rounding modes work, and where precision is lost.
The x87 FPU and SSE/AVX — how Intel processors actually implement floating-point in hardware.
By the end of this lecture, you will understand exactly why 0.1 + 0.2 ≠ 0.3, how to predict when floating-point errors will occur, and how to write code that handles them correctly.Before we can discuss how computers encode real numbers, we need a precise vocabulary for talking about how wrong a stored number can be. This section introduces the mathematical framework of representation errors — absolute and relative — that we will use throughout the lecture.Between any two consecutive integers — say 7 and 8 — there are infinitely many real numbers: 7.1, 7.01, 7.001, 7.0001, and so on, without end. A computer uses a fixed number of bits to store each number. With n bits, you can represent at most 2ⁿ distinct values. No matter how large n is, 2ⁿ is always finite, while the set of real numbers between any two integers is infinite. Therefore, a computer must approximate: it rounds each real number to the nearest value it can represent.This rounding introduces an error. The question is: how large is that error, and how should we measure it?The absolute error of a representation is the magnitude of the difference between the true value and the stored value:Δ = |A − A_comp|
where A is the true mathematical value and A_comp is the value actually stored in the computer.Absolute error tells you, in the same units as the number itself, how far off the approximation is. If the true value is 3.14159265 and the stored value is 3.1416, the absolute error is:Δ = |3.14159265... − 3.1416| ≈ 0.0000073465 = 7.3465 × 10⁻⁶
This is straightforward and intuitive: the stored value is about 7.35 millionths away from the true value.The relative error expresses the absolute error as a fraction of the true value:δ = Δ / A ≈ Δ / A_comp
(We use the approximation Δ/A_comp when the true value A is unknown — which is often the case in practice, since if we knew A exactly, we would not need to approximate it.)Relative error is dimensionless — it measures the proportion of the error, not its magnitude. For the π example:δ ≈ 7.3465 × 10⁻⁶ / 3.1416 ≈ 2.34 × 10⁻⁶
This means the stored value differs from the true value by about 2.34 parts per million.Let us walk through the complete calculation:<br><img alt="Worked example showing absolute error, relative error, and relative error in percent for the pi approximation 3.1416" src="assets/l13-pi-error-calculation.png" target="_self">Step-by-step error calculation for approximating pi as 3.1416: absolute error, relative error, and relative error expressed as parts per million.Given:
True value: A = π = 3.14159265358979...
Stored approximation: A_comp = 3.1416
Step 1 — Absolute error:Δ = |π − 3.1416| = |3.14159265358979... − 3.1416| = |−0.00000734641021...| ≈ 7.3465 × 10⁻⁶
Step 2 — Relative error:δ = Δ / A ≈ 7.3465 × 10⁻⁶ / 3.14159265... ≈ 2.34 × 10⁻⁶
The relative error of approximately 2.34 × 10⁻⁶ tells us that this four-decimal-place approximation of π is accurate to roughly one part in 430,000.Absolute error alone can be misleading because its significance depends entirely on the scale of the number being represented. Consider two scenarios:In Scenario 1, an absolute error of 1 is negligible — one part in a million. In Scenario 2, an absolute error of 10⁻⁶ represents a 100% error — the stored value is double the true value. The absolute errors are comparable, but the relative errors tell completely different stories.This scale-independence is why relative error is the preferred metric in numerical computing. When someone says "IEEE 754 double precision gives about 15–16 significant decimal digits," they are making a statement about relative error: the relative error is bounded by approximately 10⁻¹⁶, regardless of whether the number is 10³⁰⁰ or 10⁻³⁰⁰.Representation errors are not just a theoretical concern. They manifest in three practical ways that affect real programs:Rounding errors. Every time a real number is stored in a floating-point variable, it is rounded to the nearest representable value. Every arithmetic operation on floating-point numbers may introduce additional rounding. These small errors accumulate over many operations. In a loop that iterates millions of times — common in scientific simulations, financial calculations, and machine learning — the accumulated rounding error can become significant.# Summing 0.1 ten times — should be 1.0
total = 0.0
for _ in range(10): total += 0.1
print(total) # 0.9999999999999999
print(total == 1.0) # False
Loss of precision (catastrophic cancellation). When you subtract two nearly equal floating-point numbers, most of the significant digits cancel, and the result retains very few correct digits. For example, if A = 1.23456789 and B = 1.23456780, both stored to 9 significant digits, then A − B = 0.00000009 — a result with only 1 significant digit, despite both inputs having 9. This phenomenon, known as catastrophic cancellation, is one of the most dangerous traps in numerical computing. We will analyze it in detail later in this lecture.Algorithm stability. Some mathematically equivalent formulas produce very different numerical results when evaluated in floating-point. An algorithm is numerically stable if small input errors produce only small output errors; it is unstable if small input errors are amplified into large output errors. Choosing the right algorithm — or the right algebraic rearrangement — can make the difference between a simulation that produces meaningful results and one that produces nonsense.For example, the quadratic formula x = (−b ± √(b² − 4ac)) / 2a is mathematically exact, but when b² ≫ 4ac, one of the two roots involves subtracting two nearly equal numbers (catastrophic cancellation). A numerically stable alternative computes one root using the standard formula and the other using the identity x₁ · x₂ = c/a, avoiding the cancellation entirely.These three consequences — rounding, cancellation, and stability — will reappear throughout this lecture. The error framework we have just established gives us the vocabulary to analyze them precisely. In the next sections, we will see how computers actually encode real numbers, starting with the simpler case of fixed-point binary fractions and then moving to the floating-point representation that dominates modern hardware.Before we can understand floating-point, we need to understand the simpler scheme it replaced — and why it was replaced.In L05 you learned that binary integers use positional notation: each bit represents a power of two. The same idea extends past the "binary point" (the binary equivalent of a decimal point) into negative powers of two: 1 0 1 . 1 1 0 1 2² 2¹ 2⁰ 2⁻¹ 2⁻² 2⁻³ 2⁻⁴ 4 + 0 + 1 + 0.5+0.25+ 0 +0.0625 = 5.8125
Each position to the right of the binary point contributes half of the previous position's value: 0.5, 0.25, 0.125, 0.0625, and so on. This is exactly how decimal fractions work — just with powers of 2 instead of powers of 10.A number like 0.1₁₀ (one-tenth) cannot be represented exactly in binary, for the same reason that 1/3 cannot be represented exactly in decimal. You get a repeating binary fraction: 0.00011001100110011…₂. This is not a bug — it is a fundamental property of positional number systems.In a fixed-point representation, we dedicate a fixed number of bits for the integer part and a fixed number for the fractional part. If we have a total of w bits and reserve n for the integer part and m for the fractional part (with w = n + m, possibly plus a sign bit), the binary point sits at a predetermined position that never moves.For example, with 8 integer bits and 8 fractional bits (a 16-bit fixed-point format), we can represent values from 0.00000000₂ to 11111111.11111111₂, i.e., from 0 to 255.99609375.The key properties follow directly from this structure:
Range: determined by the integer bits — n bits give a maximum value near 2ⁿ.
Resolution: determined by the fractional bits — m bits give a smallest step of 2⁻ᵐ.
The resolution is uniform — the gap between any two adjacent representable numbers is always 2⁻ᵐ.
The absolute error of a fixed-point representation is constant across the entire range — it never exceeds 2⁻ᵐ, regardless of whether the number being represented is 0.001 or 200.0. This uniformity is both the strength and the weakness of fixed-point.The relative error tells a different story. For a large number like 200.0, an absolute error of 2⁻⁸ ≈ 0.004 is negligibly small — the relative error is about 0.002%. But for a tiny number like 0.01, the same absolute error of 0.004 represents a relative error of 40%. Near zero, the relative error approaches 100%.Number Abs Error (2⁻⁸) Relative Error
────────── ──────────────── ──────────────
200.0 ~0.004 ~0.002% 1.0 ~0.004 ~0.4% 0.01 ~0.004 ~40% 0.001 ~0.004 ~400% (meaningless)
This is the fundamental limitation: fixed-point wastes precision on large numbers (where the fractional bits are insignificant) and lacks precision for small numbers (where the fractional bits are too coarse).Numbers whose absolute value is smaller than 2⁻ᵐ cannot be distinguished from zero — they all map to the same representation. These numbers are called machine zero. Machine zero is not "true" mathematical zero; it is the set of all real numbers that the format rounds to zero because they fall below its resolution threshold.Despite its limitations, fixed-point remains the right choice in specific domains:
DSP and embedded systems: audio codecs, motor controllers, and sensor processing often use fixed-point because it is fast (integer ALU, no FPU needed) and deterministic (no rounding surprises).
Financial calculations: currencies have a fixed number of decimal places. Representing $19.99 as the integer 1999 (cents) avoids the floating-point errors that make accountants nervous.
FPGA and ASIC designs: fixed-point arithmetic uses fewer gates and less power than floating-point.
In all these cases, the programmer knows the range in advance and can choose n and m to match. But for general-purpose scientific and engineering computation — where values routinely span dozens of orders of magnitude — fixed-point is inadequate. We need a representation that adapts its precision to the magnitude of the number. That is exactly what floating-point provides.You have used scientific notation since high school: write a number as a mantissa (also called a significand) multiplied by a power of the base:A = M × 10ᵉwhere M is a real number (the mantissa) and e is an integer (the exponent — positive or negative). The exponent tells you where the decimal point sits; the mantissa carries the significant digits.Any real number can be expressed this way, and in many different forms:0.71 = 0.071 × 10¹ = 7.1 × 10⁻¹ = 71 × 10⁻² = ...
2025 = 0.2025 × 10⁴ = 2.025 × 10³ = 20.25 × 10² = ...
There are infinitely many representations for every number — any shift of the decimal point can be compensated by adjusting the exponent. This ambiguity is a problem we will resolve shortly.In a computer, we store the mantissa with some fixed number of digits and the exponent with some fixed number of digits. This creates a clean separation of responsibilities:
Exponent bits control RANGE — how large or small the number can be.
Mantissa bits control PRECISION — how many significant digits we can represent.
This is the key insight that makes floating-point powerful.Consider a format with a 3-digit mantissa (with sign) in the range 0.1 ≤ |M| &lt; 1 and a 2-digit exponent (with sign). We have just 5 digits and 2 sign symbols — 7 characters total. What range does this cover?Smallest positive: +0.100 × 10⁻⁹⁹
Largest positive: +0.999 × 10⁺⁹⁹
This tiny format spans from approximately 10⁻¹⁰⁰ to 10⁹⁹ — a range of 199 orders of magnitude, encoded in just 7 characters. A fixed-point format with the same storage would cover at most 5 orders of magnitude. The exponential representation is overwhelmingly more efficient for numbers that vary widely in scale.What happens to errors in this scheme?Maximum absolute error = 10⁻ⁿ × 10ᵉ, where n is the number of mantissa digits and e is the current exponent. Notice that the absolute error is not constant — it scales with the magnitude of the number. Large numbers tolerate larger absolute errors; small numbers get smaller absolute errors. This is exactly the behavior we wanted but could not achieve with fixed-point.Relative error = 10⁻ⁿ / M. Since the mantissa M is bounded (0.1 ≤ |M| &lt; 1 in our example), the relative error is also bounded — it stays within a narrow range regardless of the number's magnitude. This is the central advantage of floating-point: bounded relative error across the entire range.Number Abs Error Relative Error
────────────── ────────────── ──────────────
+0.314 × 10³ ~10⁻³ × 10³ = 1 ~0.3%
+0.314 × 10⁻⁵ ~10⁻³ × 10⁻⁵ ~0.3%
+0.100 × 10⁻⁹⁹ ~10⁻³ × 10⁻⁹⁹ ~1%
The absolute error adapts to the scale of the number, while the relative error remains roughly constant. This is why floating-point is the default representation for scientific computing.We noted earlier that the same number has infinitely many exponential representations. To eliminate this ambiguity, we define two standard forms:Normal form: the mantissa satisfies 0 ≤ |M| &lt; 1. In decimal, this means the mantissa always starts with "0." followed by the significant digits:2025 = 0.2025 × 10⁴ (normal form)
0.71 = 0.71 × 10⁰ (normal form)
But normal form is still not unique — for example, 0.2025 × 10⁴ and 0.02025 × 10⁵ are both "normal" in the sense that |M| &lt; 1. There are still many valid representations.Normalized form: the mantissa satisfies 1 ≤ |M| &lt; q, where q is the radix (base) of the number system. For decimal, q = 10, so the mantissa is a single non-zero digit followed by the fractional part:2025 = 2.025 × 10³ (normalized — leading digit is non-zero)
0.71 = 7.1 × 10⁻¹ (normalized — leading digit is non-zero)
The normalized form is unique — for any given non-zero number, there is exactly one way to write it with a mantissa in the range [1, q). This uniqueness is why normalized form is the standard in computer arithmetic. In the familiar "E notation" used by programming languages: 2025 = +2.025E+3, -0.6875 = -6.875E-1.Everything above applies equally to binary — just replace 10 with 2. A binary number in exponential form is:A = M × 2ᵉwhere the mantissa and exponent may be expressed in either binary or decimal (context makes it clear). The normalized form requires 1 ≤ |M| &lt; 2 (since q = 2 for binary), meaning the mantissa always has the form 1.something₂.Let us walk through two examples:Example 1: Convert 2012₁₀ to binary normalized form.2012₁₀ = 11111011100₂ = 1.1111011100₂ × 2¹⁰
We shifted the binary point 10 positions to the left, so the exponent is 10₁₀ = 1010₂. In fully binary notation: 1.11110111₂ × 10₂^1010₂.Example 2: Convert −0.6875₁₀ to binary normalized form.0.6875₁₀ = 0.1011₂ = 1.011₂ × 2⁻¹
We shifted the binary point 1 position to the right, so the exponent is −1₁₀. The sign is negative. In fully binary notation: −1.011₂ × 10₂^(−1₂). Alternatively, written with decimal exponent: −1.011₂ × 2⁻¹.<br><img alt="Examples of binary numbers converted to normalized form with leading 1 and a power-of-two exponent" src="assets/l13-normalized-binary-examples.png" target="_self">Binary normalized form examples: every nonzero binary floating-point number can be written as 1.fraction times 2 raised to an exponent, guaranteeing the leading digit is always 1.Notice something about binary normalized mantissas: they always start with 1. The normal form (0 ≤ |M| &lt; 1) would have mantissas starting with 0.0... or 0.1..., but the normalized form (1 ≤ |M| &lt; 2) guarantees the leading digit is 1.Some additional clarification on the two forms in binary:
Normal mantissa examples: 0.001010₂, 0.10101011₂, 0.000000001₂ — all start with "0." and may have leading zeros after the point.
Normalized mantissa examples: 1.1010₂, 1.1111111₂, 1.0000001₂ — all start with "1." and differ only in what follows the binary point.
Here is the insight that makes normalized binary form extraordinarily efficient: if the leading bit is always 1, we do not need to store it. This is called the hidden bit (or implicit bit).Instead of storing the full mantissa M = 1.fraction₂, we store only the fractional part M' (the bits after the binary point). When we need the actual mantissa, we reconstruct it:M = 1 + M' / 2ⁿwhere n is the number of bits allocated to M'. This effectively gives us n + 1 bits of precision while storing only n bits — one free bit of precision, courtesy of normalization.For example, if we have 23 stored mantissa bits, the actual precision is 24 bits — equivalent to about 7.2 significant decimal digits. Without the hidden bit, we would need 24 stored bits for the same precision. Across billions of floating-point numbers in a typical computation, that one saved bit per number adds up.There is one catch: the normalized mantissa is always greater than or equal to 1, so it cannot represent zero. We will see in the next section how IEEE 754 handles zero and other special values using reserved exponent patterns.We now have all the ingredients for a practical floating-point format:
A sign bit S (0 = positive, 1 = negative)
An exponent field E of m bits (representing the scale)
A mantissa field M' of n bits (representing the precision, with a hidden leading 1)
Total width: W = 1 + m + n bits
The number's value is:A = (−1)ˢ × (1 + M'/2ⁿ) × 2ᵉwhere the stored exponent E relates to the true exponent e through a bias: E = e + bias, with bias = 2ᵐ⁻¹ − 1. This bias scheme avoids storing a separate sign for the exponent — all stored exponent values are non-negative, simplifying hardware comparisons.In the next section, we will see how the IEEE 754 standard locks down the exact values of m and n for single-precision (32-bit), double-precision (64-bit), and extended formats — and how it handles the edge cases (zero, infinity, NaN) that pure normalization cannot represent.We now have all the ingredients — sign-magnitude representation, exponential notation, and normalized binary mantissas. It is time to see how a computer actually stores a floating-point number in a fixed-width bit string. The format is elegant, but every design choice reflects a hardware trade-off. Understanding those trade-offs is what separates someone who "uses floats" from someone who understands why 0.1 + 0.2 ≠ 0.3.A binary floating-point number of total width W bits is divided into three contiguous fields:<br><img alt="Color-coded diagram showing the three fields of a floating-point number: sign bit, exponent bits, and mantissa bits" src="assets/l13-fp-bit-layout.png" target="_self">The bit layout of a binary floating-point number: one sign bit (S), m exponent bits (E), and n mantissa bits (M'), packed into a W-bit word.The total width is:W = 1 + m + n (bits)
For IEEE 754 single precision, W = 32, m = 8, n = 23. For double precision, W = 64, m = 11, n = 52. But the principles below apply to any (m, n) combination — the standard defines a family of formats, not just one.Floating-point numbers use sign-magnitude representation, not two's complement. The sign is stored explicitly in a single bit:
S = 0 → positive number (or +0)
S = 1 → negative number (or −0)
This is a deliberate departure from the integer world. Two's complement has the advantage of a single zero and simple addition circuits, but sign-magnitude makes it trivial to negate a floating-point number — just flip bit 31 (or bit 63). It also means that comparing the sign of a float is a single bit test, and the magnitude can be compared as an unsigned integer over the remaining bits. Hardware designers chose these properties over the simplicity of two's complement.One consequence: floating-point has two zeros, +0 and −0. The standard defines them as equal (+0 == -0 is true), but they produce different results in certain edge cases — for example, 1.0 / +0.0 = +∞ while 1.0 / -0.0 = -∞. We will return to these special values in a later section.The true exponent e can be positive or negative — a number like 0.015625 = 1.0 × 2⁻⁶ has e = −6. We need a way to store negative exponents without introducing a separate sign bit for the exponent field. The solution is biased representation (also called offset or excess encoding).The idea is simple: add a fixed constant called the bias to the true exponent before storing it:E = e + bias
where:bias = 2^(m−1) − 1
For single precision (m = 8), the bias is 2⁷ − 1 = 127. So the stored exponent E is always a non-negative integer:Notice that E = 0 (all zeros) and E = 255 (all ones) are reserved for special values (zero, denormalized numbers, infinity, NaN). The usable range for normalized numbers is E ∈ [1, 254], which corresponds to e ∈ [−126, +127].Why bias instead of two's complement for the exponent? The answer is comparison speed. When you compare two positive floating-point numbers, you can treat the combined (exponent + mantissa) field as a single unsigned integer and compare it directly — no special exponent decoding needed. A larger biased exponent always means a larger magnitude. This property would break if the exponent were stored in two's complement, because negative exponents would have their high bit set (making them look "larger" than positive exponents in an unsigned comparison). The biased encoding preserves the natural ordering, which means a single unsigned integer comparison circuit can compare two floats — exactly what hardware designers want.For double precision (m = 11), the bias is 2¹⁰ − 1 = 1023, giving a true exponent range of e ∈ [−1022, +1023].Recall from the previous section that a normalized binary number always has the form:1.xxxxxxx × 2^e
The leading "1." is always present — by definition, a normalized binary mantissa satisfies 1 ≤ M &lt; 2. Since this leading 1 is always there, storing it would waste one bit. So the format stores only the fractional part after the binary point. This is called the hidden bit (or implicit bit) trick.The stored field M′ represents the fractional digits. The full mantissa M is reconstructed as:M = 1 + M′ / 2^n
The division by 2ⁿ converts the n-bit integer M′ into a fraction in the range [0, 1). Adding the hidden 1 gives M ∈ [1, 2) — exactly the normalized range.This means that although only n bits are stored, the effective precision is n + 1 bits. For single precision, 23 stored bits give 24 bits of precision. For double precision, 52 stored bits give 53 bits. That one free bit is a gift from the normalized representation — the entire reason we bothered normalizing in the first place.Example. If n = 23 and M′ = 01100000000000000000000₂ (which is 3,145,728 in decimal):M = 1 + 3145728 / 2²³ = 1 + 3145728 / 8388608 = 1 + 0.375 = 1.375
So the full mantissa is 1.375, or equivalently 1.011₂ in binary.Combining all three fields, the value of a normalized floating-point number is:<br><img alt="The master decoding formula for normalized IEEE 754 floating-point: A equals negative-one to the S times one plus M-prime over 2-to-the-n times 2 to the E minus bias" src="assets/l13-normalized-fp-formula.png" target="_self" style="width: 300px; max-width: 100%;">The master formula for decoding a normalized IEEE 754 floating-point number from its sign, exponent, and mantissa fields.A = (-1)^S × (1 + M′/2^n) × 2^(E − bias)
where bias = 2^(m−1) − 1. Expanding the bias:A = (-1)^S × (1 + M′/2^n) × 2^(E − 2^(m−1) + 1)
This single formula decodes any normalized IEEE 754 number regardless of precision. You need three values from the bit pattern — S, E, and M′ — and two format parameters — m and n.Let us make sure the formula works on a trivially simple case before tackling harder examples. Consider the 32-bit pattern for the number 1.0:0 01111111 00000000000000000000000
S=0 E=127 M′=0
Applying the formula: A = (−1)⁰ × (1 + 0/2²³) × 2^(127 − 127) = 1 × 1.0 × 2⁰ = 1.0. Correct.It is worth pausing to compare the floating-point encoding with the integer encodings you already know:The two systems solve different problems. Two's complement optimizes for exact integer arithmetic with a single addition circuit. IEEE 754 optimizes for approximate real-number arithmetic over a vast dynamic range with fast magnitude comparison. Neither is "better" — they serve different purposes, and every modern CPU implements both.Knowing the formula is one thing; being able to apply it fluently — in both directions — is another. This section walks through several complete examples, then shows how to inspect floating-point bit patterns programmatically.Decoding is the simpler direction. Given a 32-bit pattern, extract the three fields, plug into the formula, and compute.Example 1: 01000010111101101110100101111001₂ → 123.456...First, separate the 32 bits into the three fields:0 10000101 11101101110100101111001
S E M′
Extract the values:
S = 0 → positive number
E = 10000101₂ = 133₁₀
M′ = 11101101110100101111001₂ = 7,793,017₁₀
Apply the formula with m = 8, n = 23, bias = 127:A = (-1)⁰ × (1 + 7793017 / 2²³) × 2^(133 − 127) = 1 × (1 + 7793017 / 8388608) × 2⁶ = 1 × (1 + 0.929000139...) × 64 = 1 × 1.929000139... × 64 = 123.456009...
The result is approximately 123.456. Not exactly 123.456, because that decimal value cannot be represented exactly in binary floating-point — a recurring theme we will explore in depth later.Example 2: 00111111100000000000000000000000₂ = 1.00 01111111 00000000000000000000000
S=0 E=01111111₂=127 M′=0
A = (-1)⁰ × (1 + 0/2²³) × 2^(127 − 127) = 1 × 1.0 × 2⁰ = 1.0
The simplest non-trivial float. The exponent equals the bias (E = 127 means e = 0), and the mantissa remainder is all zeros (M = 1.0 exactly). Any power of two has this clean form — only the exponent field changes.Example 3: 10111111110000000000000000000000₂ = −1.51 01111111 10000000000000000000000
S=1 E=01111111₂=127 M′=10000000000000000000000₂
The mantissa remainder M′ has bit 22 set and all other bits zero, so:M′ = 2²² = 4194304
A = (-1)¹ × (1 + 4194304 / 8388608) × 2^(127 − 127) = -1 × (1 + 0.5) × 2⁰ = -1 × 1.5 × 1 = -1.5
The sign bit makes it negative. The mantissa remainder encodes 0.5 (one bit set = half the range), giving M = 1.5. The exponent is zero (e = 127 − 127 = 0), so the result is simply −1.5.Pattern to notice: the bit pattern 10000000000000000000000 in the mantissa field always adds exactly 0.5 to the hidden 1, giving M = 1.5. Similarly, 01000000000000000000000 adds 0.25 (M = 1.25), and 11000000000000000000000 adds 0.75 (M = 1.75). Each mantissa bit contributes a power of two, just like each bit in a binary integer — except these powers are negative: 2⁻¹, 2⁻², 2⁻³, and so on.Encoding is the reverse process and requires more steps. Here is the algorithm:
Convert the absolute value of the number to binary (integer part + fractional part separately)
Normalize the binary number: shift the binary point so the number has the form 1.xxxxx × 2^e
Round the mantissa to n fractional bits (pad with zeros if fewer than n bits)
Form M′ by dropping the leading "1." from the normalized mantissa
Compute E = e + bias = e + 2^(m−1) − 1
Convert E to an m-bit binary number
Assemble the final bit pattern: S | E | M′
Let us walk through a complete example.<br><img alt="32-bit IEEE 754 encoding of negative 2012.6875 showing sign bit 1, exponent 10001001, and mantissa bits" src="assets/l13-example-conversion-bits.png" target="_self">The value -2012.6875 encoded as a 32-bit IEEE 754 single-precision float, with sign, biased exponent, and mantissa fields highlighted.Step 1: Convert |−2012.6875| to binary.Integer part: 2012₁₀ → binary by repeated division:2012 ÷ 2 = 1006 remainder 0
1006 ÷ 2 = 503 remainder 0 503 ÷ 2 = 251 remainder 1 251 ÷ 2 = 125 remainder 1 125 ÷ 2 = 62 remainder 1 62 ÷ 2 = 31 remainder 0 31 ÷ 2 = 15 remainder 1 15 ÷ 2 = 7 remainder 1 7 ÷ 2 = 3 remainder 1 3 ÷ 2 = 1 remainder 1 1 ÷ 2 = 0 remainder 1 Reading remainders bottom-to-top: 11111011100₂
Fractional part: 0.6875₁₀ → binary by repeated multiplication:0.6875 × 2 = 1.375 → 1
0.375 × 2 = 0.75 → 0
0.75 × 2 = 1.5 → 1
0.5 × 2 = 1.0 → 1 Reading top-to-bottom: .1011₂
Combined: 2012.6875₁₀ = 11111011100.1011₂Step 2: Normalize.Shift the binary point left until only one "1" remains to its left:11111011100.1011₂ = 1.11110111001011₂ × 2¹⁰
We moved the point 10 positions to the left, so e = 10.Step 3: Round the mantissa to 23 fractional bits.The fractional part is 11110111001011₂ — that is 14 bits, which is fewer than 23. Pad with zeros on the right:11110111001011000000000 (23 bits)
No rounding is needed in this case because the original value terminates exactly in binary.Step 4: Form M′.Drop the leading "1." — the mantissa remainder is:M′ = 11110111001011000000000
Step 5: Compute the biased exponent.E = e + bias = 10 + (2⁷ − 1) = 10 + 127 = 137
Step 6: Convert E to 8-bit binary.137₁₀ = 10001001₂
Step 7: Assemble S | E | M′.The number is negative, so S = 1. The full 32-bit pattern is:1 10001001 11110111001011000000000
S E M′
Grouping into bytes:1100 0100 1111 1011 1001 0110 0000 0000 C4h FBh 96h 00h
Result: −2012.6875₁₀ = C4 FB 96 00h in IEEE 754 single precision.You can verify this encoding using any programming language. Encode −2012.6875 and inspect the bytes:import struct
raw = struct.pack('&gt;f', -2012.6875) # big-endian single precision
print(raw.hex()) # c4fb9600
The output c4fb9600 matches our hand calculation exactly.Being able to peek at the raw bits of a float is an essential debugging skill. Here are idioms in several languages:C# — via byte array:float f = 1.5f;
byte[] fBytes = BitConverter.GetBytes(f);
// Print bytes in big-endian order (MSB first)
for (var i = fBytes.Length - 1; i &gt;= 0; i--) Console.Write(Convert.ToString(fBytes[i], 2).PadLeft(8, '0'));
// Output: 00111111110000000000000000000000
The output is the 32-bit pattern for 1.5: S=0, E=01111111, M′=10000000000000000000000 — matching our earlier decoding of Example 3 (without the sign bit).Python — via struct module:import struct # Pack as big-endian float, get raw bytes
raw = struct.pack('!f', 1.5)
print(raw) # b'\x3f\xc0\x00\x00'
print(raw.hex()) # 3fc00000 # To see individual bits:
bits = int.from_bytes(raw, 'big')
print(f'{bits:032b}') # 00111111110000000000000000000000
Java — via Float.floatToIntBits:float f = 1.5f;
int bits = Float.floatToIntBits(f);
System.out.println(Integer.toBinaryString(bits));
// Output: 111111110000000000000000000000 (leading zeros omitted) // For double precision:
double d = 1.5;
long dBits = Double.doubleToLongBits(d);
System.out.println(Long.toBinaryString(dBits));
C — via union (type punning):#include &lt;stdio.h&gt;
#include &lt;stdint.h&gt; union FloatBits { float f; uint32_t u;
}; int main() { union FloatBits fb; fb.f = -2012.6875f; printf("0x%08X\n", fb.u); // 0xC4FB9600 return 0;
}
All four approaches give you the same 32 bits — they just use different language mechanisms to reinterpret the float's memory as an integer.Here are several 32-bit patterns for you to decode. Try working through each one with the formula before checking the answer:The pattern for π (40490FDB) is particularly instructive — the mantissa stores the best 23-bit approximation of π/2, and the exponent of +1 doubles it to ≈ 3.14159.When we encoded −2012.6875, the fractional part 0.6875 converted to a terminating binary fraction (.1011₂) in just four bits. But most decimal fractions are not so lucky.Consider 0.1₁₀. Applying the repeated-multiplication algorithm:0.1 × 2 = 0.2 → 0
0.2 × 2 = 0.4 → 0
0.4 × 2 = 0.8 → 0
0.8 × 2 = 1.6 → 1
0.6 × 2 = 1.2 → 1
0.2 × 2 = 0.4 → 0 ← cycle repeats from here
0.4 × 2 = 0.8 → 0
...
The binary expansion of 0.1 is 0.0001100110011001100110011...₂ — an infinitely repeating pattern. When we try to store this in 23 mantissa bits, we must truncate or round, introducing an error of roughly 10⁻⁸. That tiny error is exactly why 0.1 + 0.2 ≠ 0.3 in every programming language that uses IEEE 754 — the errors in the representations of 0.1 and 0.2 do not cancel out to produce the exact representation of 0.3.A decimal fraction d/10ⁿ can be represented exactly in binary only if d/10ⁿ can be written as k/2ᵐ for some integers k and m — in other words, only if the denominator's prime factorization contains no factors other than 2. Since 10 = 2 × 5, any fraction with a factor of 5 in the denominator will produce an infinite binary expansion. Values like 0.5, 0.25, 0.125, and 0.6875 terminate because their denominators are pure powers of two. Values like 0.1, 0.2, and 0.3 do not.Now that we understand how floating-point numbers are encoded — sign, biased exponent, hidden-bit mantissa — we can ask the two most practical questions about any FP format: how big (or small) can a number be, and how precise is it?These questions have crisp answers that depend on exactly two parameters: the number of exponent bits m and the number of mantissa bits n.The exponent field determines the magnitude of representable numbers — how far from zero they can reach in either direction.Recall the general formula for a normalized floating-point number:A = (-1)^S × (1 + M'/2^n) × 2^e
where the true exponent e ranges from e_min to e_max. For a format with m exponent bits and bias = 2^(m−1) − 1:e_min = 1 - bias = 2 - 2^(m-1)
e_max = 2^m - 2 - bias = 2^(m-1) - 1
(The extreme exponent values — all zeros and all ones — are reserved for special values, so the usable range is 1 through 2^m − 2.)The maximum representable value occurs when the mantissa is at its largest (all mantissa bits set to 1, so M ≈ 2 − 2^(−n)) and the exponent is at its largest:A_max = M_max × 2^(e_max) ≈ 2 × 2^(e_max) = 2^(e_max + 1)
The minimum normalized value occurs when the mantissa is at its smallest (M = 1.0, the hidden bit alone) and the exponent is at its smallest:A_min = M_min × 2^(e_min) = 1.0 × 2^(e_min) = 2^(e_min)
For single precision, m = 8, so the bias is 127:e_max = 2^7 - 1 = 127
e_min = 2 - 2^7 = -126 A_max = (2 - 2^(-23)) × 2^127 ≈ 2^128 ≈ 3.40 × 10^38 A_min (normalized) = 1.0 × 2^(-126) ≈ 1.17 × 10^(-38)
This means a single 32-bit word can represent numbers from roughly ±1.17 × 10⁻³⁸ to ±3.40 × 10³⁸. The dynamic range — the ratio of the largest to the smallest representable positive number — spans approximately 10⁷⁶, or about 76 orders of magnitude.To appreciate how remarkable this is, recall our fixed-point example from Section 3: a 16-bit fixed-point format with 8 integer bits and 8 fractional bits covered a range from 0 to roughly 256 — about 2.4 orders of magnitude. A 32-bit floating-point format covers 76 orders of magnitude with the same total number of bits. The exponential encoding is incomparably more efficient at covering a wide range.While the exponent controls range, the mantissa controls precision — the number of significant digits in the stored value.A floating-point format with n stored mantissa bits actually has n + 1 bits of precision, thanks to the hidden bit. Since each binary digit carries log₁₀(2) ≈ 0.30103 decimal digits of information, the number of significant decimal digits is:k = (n + 1) × log₁₀(2) ≈ 0.301 × (n + 1)
The inverse relationship is also useful: it takes log₂(10) ≈ 3.32 binary digits to represent one decimal digit. This means roughly 3.32 bits of mantissa per decimal digit of precision.For single precision, approximately 7 significant decimal digits are reliable. This means that if you store the number 12345678 in a float, the last digit (or even two) may be wrong — the value might come back as 12345680. For double precision, about 15–16 decimal digits are reliable — more than enough for most scientific calculations but still finite.Given a fixed total word size W = 1 + m + n, every bit you add to the exponent field doubles the range but costs one bit of precision, and vice versa. The IEEE 754 committee chose the exponent/mantissa split for each format to balance these competing demands:Single (32 bits): 8 exponent + 23 mantissa → 76 orders of magnitude, 7 digits
Double (64 bits): 11 exponent + 52 mantissa → 616 orders of magnitude, 16 digits
Double precision is not just "twice as good" as single — it has three more exponent bits (8× the range in powers of two) and 29 more mantissa bits (over a billion times finer resolution).Every floating-point format has three failure modes — three ways in which a computation can exceed the format's capacity. Understanding all three is essential for writing robust numerical code.<br><img alt="Number line diagram showing zones for negative overflow, representable range, underflow near zero, and positive overflow" src="assets/l13-overflow-underflow-number-line.png" target="_self">The floating-point number line, showing overflow zones beyond the representable range, the underflow/denormalized zone near zero, and the truncation gaps between representable values.Overflow occurs when the result of a computation has an exponent larger than e_max — the number is too large (in absolute value) for the format to represent. In single precision, any result whose magnitude exceeds approximately 3.40 × 10³⁸ overflows. IEEE 754 handles overflow by producing ±∞ (positive or negative infinity), which then propagates through subsequent calculations.import struct huge = 3.4e38
result = huge * 2.0 # Overflow!
print(result) # inf
Underflow occurs when the result of a computation has an exponent smaller than e_min — the number is too close to zero for the format to represent as a normalized value. In single precision, any result smaller than approximately 1.17 × 10⁻³⁸ underflows. IEEE 754 handles underflow gradually by switching to denormalized numbers (Section 8), which lose precision but avoid jumping abruptly to zero. If the result is even smaller than the smallest denormalized value, it rounds to zero.tiny = 1.17e-38
result = tiny / 1000.0 # Underflow into denormalized range
print(result) # 1.17e-41 (denormalized — fewer significant digits) tinier = tiny / 1e20
print(tinier) # 0.0 (complete underflow to zero)
Truncation (rounding) occurs when the result of a computation has more significant bits than the mantissa can hold. This happens on virtually every operation — it is the most common of the three. The excess bits are discarded according to a rounding rule (IEEE 754 defines four modes; the default is "round to nearest, ties to even"). Truncation does not produce special values; it simply introduces a small relative error bounded by the machine epsilon — approximately 2^(−n), or about 1.19 × 10⁻⁷ for single precision and 2.22 × 10⁻¹⁶ for double.The three types of limits become intuitive when you visualize the floating-point "number line."Unlike the integer number line — where representable values are uniformly spaced — the floating-point number line has a distinctive non-uniform distribution:
Near zero: representable values are densely packed. Between 1.0 and 2.0 in single precision, there are 2²³ ≈ 8.4 million distinct floats — a spacing of about 1.19 × 10⁻⁷.
Far from zero: representable values are sparse. Between 2¹²⁷ and 2¹²⁸, the spacing between adjacent floats is 2¹²⁷ × 2⁻²³ = 2¹⁰⁴ ≈ 2 × 10³¹ — each "step" is over 10³¹.
This non-uniform spacing is not a defect — it is a feature. The relative spacing between adjacent representable values is approximately constant (about 2⁻²³ for single precision), which means the relative error of any rounded result is roughly the same whether the number is 10⁻³⁰ or 10³⁰. Fixed-point gives you constant absolute error; floating-point gives you constant relative error. For most applications, constant relative error is far more useful.<br><img alt="Bit pattern diagrams showing the maximum normalized value with all exponent and mantissa bits set, and the minimum denormalized value with only the last mantissa bit set" src="assets/l13-max-min-fp-bit-diagrams.png" target="_self">Bit patterns for the two extremes of the single-precision representable range: the maximum normalized value (exponent 11111110, mantissa all ones) and the minimum positive denormalized value (exponent all zeros, mantissa 000...001).The diagram above shows the maximum normalized value and the minimum denormalized value for single precision — the two extremes of the representable range. Between these endpoints, only certain real numbers land exactly on a "tick mark" (a representable value); all others must be rounded to the nearest tick.To the left and right of the representable range lie the overflow zones — numbers too large for the format. In the narrow strip around zero lie the underflow and denormalized zones. And everywhere along the line, between any two adjacent tick marks, lie infinitely many real numbers that must all be rounded to one of their two neighbors — the domain of truncation.Everything we have discussed so far — sign-exponent-mantissa encoding, biased exponents, hidden bits — existed in various forms before 1985. Different manufacturers implemented different floating-point formats: IBM mainframes used hexadecimal (base-16) floating-point; DEC VAX had its own format; the Cray supercomputers used yet another. A program that produced correct results on one machine might produce different (or incorrect) results on another. Worse, some implementations did not even handle edge cases like division by zero consistently — some generated exceptions, others returned garbage.The IEEE 754 standard was born from the need to end this chaos. The effort was led by William Kahan, a mathematician at UC Berkeley, who had spent years studying how floating-point errors propagate through computations. Kahan's key insight was that a good standard needed more than just a data format — it needed to specify behavior: what happens on overflow, on underflow, on division by zero, on operations with no mathematical answer.The standard was adopted in 1985 as IEEE 754-1985. It has been revised twice since then — in 2008 (IEEE 754-2008, adding decimal and quad-precision formats) and in 2019 (IEEE 754-2019, the current version, also published as ISO/IEC 60559:2020). But the core single-precision and double-precision formats have remained unchanged since the original 1985 specification.Before the Intel 80486 processor (1989), x86 systems had no built-in floating-point hardware. Floating-point operations were handled by a separate coprocessor chip — the Intel 8087 (1980), 80287 (1982), or 80387 (1987). The 80387 was the first x87 coprocessor to be fully IEEE 754-compliant. Starting with the 80486, Intel integrated the FPU directly into the CPU, and every x86 processor since then implements IEEE 754 in hardware. Today, virtually every general-purpose processor — x86, ARM, RISC-V — conforms to the standard.The standard defines four primary binary formats, differing in total width and the exponent/mantissa split:<br><img alt="Table of IEEE 754 binary floating-point formats showing bit widths, exponent sizes, mantissa sizes, and bias values for single, single-extended, double, and double-extended precision" src="assets/l13-ieee754-formats-table.png" target="_self">The four primary IEEE 754 binary formats: single (32-bit), single-extended, double (64-bit), and double-extended (80-bit), with their exponent widths, mantissa widths, and bias values.Single precision (32 bits) is the workhorse of graphics, games, and embedded systems — anywhere that memory bandwidth matters more than extreme accuracy. The C type float, the GPU default, and most sensor data formats use single precision.Double precision (64 bits) is the default for scientific computing, finance, and most general-purpose programming. The C type double, Python's float, and JavaScript's only numeric type (before BigInt) are all double precision.Single-extended was intended for intermediate computations within an FPU that uses single precision for storage. The standard specifies only minimum requirements (≥43 bits, ≥11 exponent bits, ≥31 mantissa bits), leaving the exact width to the implementor. In practice, this format was rarely implemented as a distinct type.Double-extended (80 bits in practice) was designed for the same purpose at double-precision level — extra headroom for intermediate results. Intel's x87 FPU uses an 80-bit double-extended format internally (15 exponent bits, 64 mantissa bits including an explicit integer bit — no hidden bit). This format, available as long double in some C compilers on x86, provides about 18–19 significant decimal digits.The vast majority of floating-point values are normalized numbers. A number is normalized when its exponent field E is neither all-zeros nor all-ones — that is, 1 ≤ E ≤ 2^m − 2.The value of a normalized single-precision number is:A = (-1)^S × (1 + M'/2^23) × 2^(E - 127)
The leading "1." is implicit (the hidden bit). The stored mantissa M' represents only the fractional part after the binary point. This means:
The smallest normalized mantissa is 1.000…0₂ = 1.0 (all mantissa bits zero).
The largest normalized mantissa is 1.111…1₂ = 2 − 2⁻²³ ≈ 1.99999988 (all mantissa bits one).
Combined with the exponent range 1 ≤ E ≤ 254 (true exponent −126 to +127), this gives the familiar single-precision range of approximately ±1.17 × 10⁻³⁸ to ±3.40 × 10³⁸.There is an obvious difficulty with the normalized representation: the mantissa always starts with "1." (the hidden bit), so the mantissa is always ≥ 1.0, and the value is always nonzero. How do we represent zero?IEEE 754 solves this by reserving the all-zeros exponent field. When both the exponent and the mantissa are all zeros, the value is zero:+0 = 00 00 00 00h (all 32 bits zero)
-0 = 80 00 00 00h (only the sign bit is set)
Yes, IEEE 754 has two representations of zero: positive zero (+0) and negative zero (−0). By the standard's comparison rules, +0 and −0 are equal:print(0.0 == -0.0) # True
print(0.0 is -0.0) # True (in CPython; implementation detail)
But they are not identical in all contexts. The sign of zero matters for certain operations:import math print(1.0 / 0.0) # inf (+∞)
print(1.0 / -0.0) # -inf (-∞)
print(math.copysign(1.0, 0.0)) # 1.0
print(math.copysign(1.0, -0.0)) # -1.0
The existence of −0 preserves the sign in computations that underflow to zero. Without −0, the expression 1.0 / (1.0 / x) would lose the sign of x whenever the intermediate result 1.0 / x underflowed to zero.Consider what happens when a computation produces a result smaller than the smallest normalized number (approximately 1.17 × 10⁻³⁸ in single precision). Without any special handling, this value would simply become zero — a phenomenon called abrupt underflow. The jump from 1.17 × 10⁻³⁸ to 0 is enormous in relative terms, and it can cause numerical instability. For example, the test x - y == 0 would no longer reliably indicate that x == y.IEEE 754 avoids this cliff by defining denormalized numbers (also called subnormal numbers). When the exponent field is all zeros and the mantissa is nonzero, the number is denormalized:<br><img alt="The decoding formula for denormalized IEEE 754 numbers: A equals negative-one to the S times zero plus M-prime over 2-to-the-n times 2 to the quantity 2 minus 2-to-the-m-minus-1" src="assets/l13-denormalized-fp-formula.png" target="_self">The decoding formula for denormalized (subnormal) numbers: the hidden bit is 0 instead of 1, and the exponent is fixed at e_min, providing gradual underflow toward zero.A = (-1)^S × (0 + M'/2^n) × 2^(2 - 2^(m-1))
The crucial difference from normalized numbers:
The hidden bit is zero, not one — the mantissa is 0.fraction instead of 1.fraction.
The exponent is fixed at e_min = 2 − 2^(m−1) (= −126 for single precision), regardless of the stored exponent field (which is all zeros).
For single precision, the denormalized formula becomes:A = (-1)^S × (M'/2^23) × 2^(-126) = (-1)^S × M' × 2^(-149)
The smallest denormalized single-precision number has M' = 1 (a single bit set in the last mantissa position):A_min = 1 × 2^(-149) ≈ 1.40 × 10^(-45)
The largest denormalized single-precision number has M' = 2²³ − 1 (all mantissa bits set):A_max_denorm = (2^23 - 1) × 2^(-149) ≈ 1.17 × 10^(-38)
Notice that the largest denormalized number is approximately equal to the smallest normalized number. This ensures gradual underflow — as numbers get smaller than the normalized range, they smoothly lose precision (one bit at a time) rather than jumping abruptly to zero. The price of gradual underflow is reduced precision: denormalized numbers have fewer significant bits than normalized numbers, because the leading zeros in the mantissa consume bit positions without contributing information.When the exponent field is all-ones and the mantissa field is all-zeros, the value represents infinity:+∞ = 7F 80 00 00h (S=0, E=FFh, M'=0)
-∞ = FF 80 00 00h (S=1, E=FFh, M'=0)
Infinities arise naturally from overflow and from division by zero. IEEE 754 defines infinity as a proper value that participates in arithmetic:x = float('inf')
print(x + 1) # inf (∞ + anything finite = ∞)
print(x * 2) # inf
print(x &gt; 1e308) # True (∞ is greater than every finite number)
print(-x &lt; -1e308) # True
print(1.0 / x) # 0.0 (finite / ∞ = 0)
Division by zero produces infinity or NaN depending on the numerator:<br><img alt="Piecewise formula for division by zero: positive x over 0 gives positive infinity, negative x over 0 gives negative infinity, zero over zero gives NaN" src="assets/l13-division-by-zero-formula.png" target="_self">IEEE 754 division by zero: the result depends on the numerator's sign, producing positive infinity, negative infinity, or NaN for the indeterminate 0/0 case.x / 0 = +∞ if x &gt; 0
x / 0 = -∞ if x &lt; 0
x / 0 = NaN if x = 0 (0/0 is indeterminate)
When the exponent field is all-ones and the mantissa field is nonzero, the value represents NaN — "Not a Number." NaN is the result of mathematically undefined or indeterminate operations:<br><img alt="32-bit NaN bit pattern with sign bit set, all exponent bits set to 1, and a nonzero mantissa field" src="assets/l13-nan-bit-layout.png" target="_self">NaN (Not a Number) bit pattern in single precision: the exponent field is all ones (like infinity), but the mantissa is nonzero, distinguishing NaN from infinity.NaN bit pattern (single precision): S=1, E=FF h, M'=non-zero (e.g., FF C0 00 00h)
Operations that produce NaN:0 / 0 (indeterminate form)
∞ - ∞ (indeterminate form)
0 × ∞ (indeterminate form)
∞ / ∞ (indeterminate form)
√(x) where x &lt; 0 (complex result, not representable)
NaN has two remarkable properties:NaN is "contagious." Any arithmetic operation involving NaN produces NaN:x = float('nan')
print(x + 1) # nan
print(x * 0) # nan
print(x ** 0) # 1.0 (exception: by convention, anything^0 = 1, even NaN^0)
NaN is not equal to anything — including itself:x = float('nan')
print(x == x) # False!
print(x != x) # True
print(x &lt; 0) # False
print(x &gt; 0) # False
print(x == 0) # False
This last property is often used as a NaN test: if x != x is true only when x is NaN. (In practice, use math.isnan(x) for clarity.)The standard distinguishes two types of NaN:
Quiet NaN (qNaN): propagates silently through computations without raising an exception. The most significant mantissa bit is 1.
Signaling NaN (sNaN): raises an invalid-operation exception when used in an arithmetic operation. The most significant mantissa bit is 0. Signaling NaNs are rarely used in practice; they were designed for debugging (e.g., flagging uninitialized variables).
The following table summarizes every possible category of single-precision floating-point value, based on the exponent and mantissa fields:This five-row table is the complete specification of IEEE 754 single-precision semantics. Every 32-bit pattern maps to exactly one of these five categories. There are no unused bit patterns — every possible combination of bits has a well-defined meaning.To convert a decimal number to IEEE 754 single-precision format, follow these steps:
Determine the sign: S = 0 if positive, S = 1 if negative. Work with the absolute value from here on.
Convert to binary: convert the integer and fractional parts separately (integer by repeated division, fraction by repeated multiplication by 2).
Normalize: shift the binary point so the number has the form 1.fraction × 2^e.
Round the mantissa: if the fractional part has more than 23 bits, round to 23 bits.
Form M': drop the leading "1." — the remaining 23 bits are the stored mantissa.
Compute the biased exponent: E = e + 127. Convert E to 8 bits.
Assemble: concatenate S (1 bit) | E (8 bits) | M' (23 bits).
Step 1 — Sign: The number is negative, so S = 1.Step 2 — Convert to binary:The integer part: 2012₁₀ = 11111011100₂The fractional part: 0.6875₁₀ → multiply by 2 repeatedly:0.6875 × 2 = 1.375 → 1
0.375 × 2 = 0.75 → 0
0.75 × 2 = 1.5 → 1
0.5 × 2 = 1.0 → 1
So 0.6875₁₀ = 0.1011₂Combined: 2012.6875₁₀ = 11111011100.1011₂Step 3 — Normalize:11111011100.1011₂ = 1.11110111001011₂ × 2^10
The binary point moved 10 positions to the left, so e = 10.Step 4 — Mantissa M': The fractional part after "1." is 11110111001011. We have 14 significant bits after the point — well within the 23-bit limit, so no rounding is needed. Pad with zeros to 23 bits:M' = 11110111001011000000000₂
Step 5 — Biased exponent:E = e + 127 = 10 + 127 = 137₁₀ = 10001001₂
Step 6 — Assemble:S | E | M'
1 | 10001001 | 11110111001011000000000 Hex: C4 FB 96 00h
To verify: the sign bit is 1 (negative), the biased exponent is 137 (true exponent 10, meaning the number is between 2¹⁰ = 1024 and 2¹¹ = 2048), and the mantissa encodes the significant digits of 2012.6875. Everything checks out.The value of IEEE 754 goes far beyond defining a data format. The standard specifies:
Deterministic results: the same operation on the same inputs must produce the same output on every conforming implementation. This is essential for reproducible science and for debugging.
Graceful degradation: overflow produces ±∞ (not garbage), underflow produces denormalized numbers (not zero), and undefined operations produce NaN (not crashes) — all without halting the program.
Comparisons: floating-point values can be compared using the same hardware as integer comparison (because the biased exponent sorts correctly as an unsigned integer), with special-case handling for NaN.
Rounding rules: four rounding modes are defined, with "round to nearest, ties to even" (banker's rounding) as the default — minimizing systematic bias in long computations.
Before IEEE 754, numerical software had to be rewritten and retested for every new platform. After IEEE 754, a correctly written numerical algorithm produces identical results on an Intel laptop, an ARM phone, a GPU, and a supercomputer. That portability is the standard's most important legacy.The IEEE 754 standard specifies not just how numbers are encoded, but how arithmetic operations must behave. Every conforming implementation must produce the same result for the same inputs — bit for bit. This section explains the algorithms that hardware uses for the three fundamental operations: addition/subtraction, multiplication, and division.Adding two floating-point numbers is surprisingly more complex than multiplying them. The reason is that the operands may have different exponents — different scales — and you cannot add mantissas until they are aligned to the same scale. The algorithm proceeds in four steps.Step 1: Align exponents. Compare the two exponents. Shift the mantissa of the smaller operand to the right by the difference in exponents, so that both operands effectively have the same exponent. Every right-shift discards the least significant bit, so this step can lose precision from the smaller operand.Step 2: Add or subtract the mantissas. Once aligned, perform integer addition or subtraction on the mantissas (the sign bits determine which). If the operation is subtraction and the operands are close in magnitude, many leading bits may cancel.Step 3: Normalize the result. The sum may overflow the mantissa (e.g., 1.111 + 1.001 = 11.000₂) or underflow it (e.g., 1.000 − 0.111 = 0.001₂). Shift the result right (incrementing the exponent) to fix overflow, or left (decrementing the exponent) to fix underflow, until the mantissa is back in the form 1.xxxxx₂.Step 4: Round. The result may have more bits than the mantissa field can hold. Apply the current rounding mode (see below) to produce the final stored value.Let us trace the addition of 1.5₁₀ and 0.125₁₀ step by step.First, convert to normalized binary floating-point:1.5₁₀ = 1.1₂ × 2⁰ (mantissa = 1.1, exponent = 0)
0.125₁₀ = 1.0₂ × 2⁻³ (mantissa = 1.0, exponent = −3)
Align exponents. The smaller exponent is −3; the larger is 0. Shift the mantissa of 0.125 right by 3 positions to match:1.0₂ × 2⁻³ → 0.001₂ × 2⁰
Add mantissas (both positive, same exponent): 1.100₂
+ 0.001₂
───────── 1.101₂
Normalize. The result 1.101₂ × 2⁰ is already normalized (leading bit is 1).Round. No extra bits to discard. The final result is 1.101₂ × 2⁰ = 1.625₁₀. Correct.This example was clean, but consider what happens when the exponents differ by 20 or more: the smaller operand gets shifted so far right that most of its significant bits fall off the end of the mantissa. This is why adding a very small number to a very large one can produce a result equal to the large number alone — the small number is "absorbed."Multiplication is structurally simpler than addition because there is no alignment step — the exponents simply add.Step 1: XOR the sign bits. Positive × positive = positive, positive × negative = negative, and so on. The XOR of the two sign bits gives the correct result sign.Step 2: Add the exponents. Since each exponent is stored with a bias, adding two biased exponents gives a result with double the bias. Subtract the bias once: e_result = (E₁ − bias) + (E₂ − bias) + bias = E₁ + E₂ − bias.Step 3: Multiply the mantissas. Treat the two mantissas (each in the range [1.0, 2.0)) as fixed-point numbers and multiply them. The product lies in the range [1.0, 4.0), so it has either the form 1.xxxxx₂ or 10.xxxxx₂ (i.e., 1 or 2 integer bits).Step 4: Normalize and round. If the product's integer part is 10₂ (i.e., the product is ≥ 2.0), shift right by one and increment the exponent. Then round the mantissa to fit the available bits.The key advantage of multiplication is that both operands contribute their full precision to the result — there is no alignment loss. The only precision loss comes from the final rounding step, which discards the least significant bits of the double-width product.Division mirrors multiplication with inverse operations.Step 1: XOR the sign bits — identical to multiplication.Step 2: Subtract the exponents. The result exponent is e_result = (E₁ − bias) − (E₂ − bias) + bias = E₁ − E₂ + bias. (Adding the bias back compensates for the double subtraction.)Step 3: Divide the mantissas. The quotient of two numbers in [1.0, 2.0) lies in (0.5, 2.0). If it falls below 1.0, shift left and decrement the exponent.Step 4: Normalize and round — same as multiplication.Division is the slowest of the three operations in hardware. Mantissa division typically uses iterative algorithms (e.g., SRT division or Newton–Raphson reciprocal approximation) rather than a single combinational circuit.After every arithmetic operation, the "infinitely precise" result must be rounded to fit the target format's mantissa. IEEE 754 defines four rounding modes:Round to nearest, ties to even (default). Select the representable value closest to the exact result. If the exact result falls exactly halfway between two representable values, choose the one whose least significant mantissa bit is 0 (i.e., the even one). This is the default mode in virtually all hardware and software, sometimes called "banker's rounding."Round toward zero (truncation). Simply discard the extra bits — always move toward zero. This is what integer truncation does. It introduces a systematic negative bias for positive numbers and positive bias for negative numbers.Round toward +∞ (ceiling). Always round toward positive infinity — round up for positive numbers, toward zero for negative numbers.Round toward −∞ (floor). Always round toward negative infinity — round down for positive numbers, away from zero for negative numbers.Why is "ties to even" the default rather than the more intuitive "ties to +∞" (the rule you learned in school: 0.5 rounds up)? The reason is statistical bias. If you round 0.5 up every time, then in a long computation that produces many halfway cases, the sum will systematically drift upward. Rounding to even ensures that, on average, half the ties round up and half round down, eliminating the bias. This matters enormously in financial computations, scientific simulations, and any iterative algorithm that performs millions of rounding operations.Implementing correct rounding in hardware requires knowing more about the exact result than just the bits that fit in the mantissa. IEEE 754-compliant hardware maintains three extra bits beyond the mantissa during intermediate computations:
Guard bit (G): the first bit beyond the mantissa — the most significant of the discarded bits.
Round bit (R): the second bit beyond the mantissa.
Sticky bit (S): the logical OR of all remaining bits beyond the round bit. Once set to 1, it stays 1.
These three bits provide enough information to determine the correct rounding in all four modes:Mantissa bits G R S Action (round to nearest, ties to even)
───────────── ─ ─ ─ ──────────────────────────────────────
xxxx...xxx0 0 x x Round down (truncate) — closer to lower
xxxx...xxx0 1 0 0 Tie → round down (LSB is already 0 = even)
xxxx...xxx0 1 0 1 Round up — closer to upper
xxxx...xxx0 1 1 x Round up — closer to upper
xxxx...xxx1 0 x x Round down (truncate) — closer to lower
xxxx...xxx1 1 0 0 Tie → round up (make LSB 0 = even)
xxxx...xxx1 1 0 1 Round up — closer to upper
xxxx...xxx1 1 1 x Round up — closer to upper
The sticky bit is particularly clever. Without it, the hardware would need to examine an arbitrarily long chain of bits to distinguish "exactly halfway" from "just above halfway." The sticky bit collapses that infinite tail into a single bit: if any bit beyond the round position is 1, the sticky bit is 1, and we know the value is not exactly halfway. This allows correct rounding with just three extra bits of storage, regardless of how many bits were shifted out during alignment.Understanding the arithmetic rules is necessary but not sufficient. This section explores the practical consequences — the ways floating-point arithmetic can silently produce wrong answers, and the techniques programmers use to defend against them.The machine epsilon (ε) is the smallest positive number such that 1.0 + ε ≠ 1.0 in a given floating-point format. Equivalently, it is the gap between 1.0 and the next larger representable number. Machine epsilon quantifies the fundamental precision limit of the format:Machine epsilon tells you that single-precision arithmetic is accurate to about 7 significant decimal digits, and double-precision to about 16. Any computation that requires more significant digits than the format provides will produce results corrupted by rounding.You can find machine epsilon experimentally:eps = 1.0
while 1.0 + eps != 1.0: eps /= 2.0
eps *= 2.0 # last value where 1.0 + eps != 1.0
print(f"Machine epsilon: {eps}") # 2.220446049250313e-16 for float64
The most dangerous floating-point pitfall is catastrophic cancellation — the massive loss of significant digits that occurs when subtracting two nearly equal numbers.Consider computing x − y where x ≈ y. Both numbers are stored with, say, 52 bits of precision. If the first 48 bits of their mantissas are identical, those 48 bits cancel in the subtraction, leaving only the last 4 bits — which carry the accumulated rounding errors of all prior computations. The result has only 4 bits of meaningful precision instead of 52.Here is a concrete example. Suppose you compute √(x² + 1) − 1 for x = 10⁻⁸ using double precision:import math
x = 1e-8
result_naive = math.sqrt(x**2 + 1) - 1
result_exact = x**2 / (math.sqrt(x**2 + 1) + 1) # algebraically equivalent
print(f"Naive: {result_naive}") # 0.0 (completely wrong!)
print(f"Correct: {result_exact}") # 5e-17 (close to true answer)
The naive formula computes √(1 + 10⁻¹⁶) ≈ 1.00000000000000005, but in double precision this rounds to exactly 1.0. Subtracting 1 gives 0.0 — every significant digit has been lost. The algebraically equivalent reformulation avoids the subtraction of nearly equal numbers and produces the correct answer.The classic example: the quadratic formula. The standard formula for the roots of ax² + bx + c = 0 is:x = (−b ± √(b² − 4ac)) / (2a)
When b² &gt;&gt; 4ac, the discriminant √(b² − 4ac) ≈ |b|, and one of the two roots involves subtracting two nearly equal numbers (−b + √(b² − 4ac) when b &gt; 0, or −b − √(b² − 4ac) when b &lt; 0). The standard remedy is to compute the "safe" root first (the one that adds), then use the identity x₁ · x₂ = c/a to find the other root without a dangerous subtraction.In real arithmetic, addition is associative: (a + b) + c = a + (b + c). In floating-point, this identity does not hold. The result depends on the order of operations, because intermediate rounding can discard different information depending on which additions are performed first.a = 1e20
b = -1e20
c = 1.0 print((a + b) + c) # 1.0 (correct)
print(a + (b + c)) # 0.0 (wrong!)
In the first grouping, a + b = 0.0 exactly (the large values cancel), and 0.0 + 1.0 = 1.0. In the second grouping, b + c = −1e20 (because 1.0 is absorbed into the much larger −1e20 — it falls below machine epsilon relative to 10²⁰), and then a + (−1e20) = 0.0.This means that summation order matters. Compilers are not free to reorder floating-point additions for optimization (unless the programmer explicitly permits it with flags like -ffast-math), because reordering can change the result.A common pattern in scientific computing is summing a large array of numbers:total = 0.0
for x in data: total += x
If data contains millions of small values and total grows large, each addition absorbs the small value imprecisely — the low-order bits of x are lost during alignment. After millions of iterations, the accumulated error can be enormous.Kahan summation is a classic algorithm that compensates for this error by maintaining a separate running compensation variable:def kahan_sum(data): total = 0.0 compensation = 0.0 # accumulated error for x in data: y = x - compensation # compensate for prior error temp = total + y # add to total (may lose low-order bits) compensation = (temp - total) - y # what was lost in the addition total = temp return total
The variable compensation captures exactly the bits that were lost in the total + y addition. On the next iteration, those lost bits are folded back in. The result is as if you had summed the entire array with twice the precision.To see the difference:import random
random.seed(42)
data = [random.random() for _ in range(1_000_000)] naive = sum(data)
kahan = kahan_sum(data)
# Using math.fsum (Python's built-in exact summation) as ground truth:
import math
exact = math.fsum(data) print(f"Naive error: {abs(naive - exact)}") # typically ~1e-10
print(f"Kahan error: {abs(kahan - exact)}") # typically ~1e-16 or 0.0
The three most cited disasters illustrate how small numerical errors can cascade into catastrophic failures:The Patriot Missile Failure (Dhahran, 1991). The Patriot system tracked time by counting 0.1-second intervals in a 24-bit fixed-point register. The number 0.1₁₀ cannot be represented exactly in binary: 0.1₁₀ = 0.000110011001100…₂ (repeating). The 24-bit approximation introduced an error of about 9.5 × 10⁻⁸ seconds per tick. After 100 hours of continuous operation — the battery had been running without a reboot — the accumulated error was:0.1s × (100 hours × 3600 s/hour) / 0.1s × 9.5×10⁻⁸ = 0.34 seconds
A Scud missile travels approximately 1,676 meters per second. In 0.34 seconds, it covers about 570 meters — well outside the Patriot's tracking gate. The system could not correlate the radar return with the predicted position and failed to intercept. Twenty-eight soldiers were killed.The Ariane 5 Explosion (Kourou, 1996). The Ariane 5's inertial reference system reused software from the Ariane 4. A 64-bit floating-point value representing horizontal velocity was converted to a 16-bit signed integer. On the Ariane 4, the velocity never exceeded 32,767 (the 16-bit maximum). The Ariane 5's more powerful boosters produced a higher initial velocity. The conversion overflowed, the processor raised an exception, the backup system (running identical software) also failed, and the diagnostic error message was misinterpreted by the flight computer as navigation data. The rocket veered off course and was destroyed by its self-destruct mechanism 37 seconds after launch. Cost: approximately $370 million.The Vancouver Stock Exchange Index (1982–1983). When the exchange computed its index, it truncated (rounded toward zero) after each recalculation instead of rounding to nearest. Truncation introduces a systematic downward bias: every time a fractional value is cut off, the index loses a tiny amount. Over 22 months and thousands of daily recalculations, the index drifted from 1000.000 to 524.811 — approximately half its true value. The error was discovered, the index was corrected to 1098.892, and the truncation was replaced with proper rounding.These disasters motivate several defensive practices:
Never compare floats with ==. Use a tolerance: abs(a - b) &lt; epsilon where epsilon is chosen based on the expected magnitude.
Avoid subtracting nearly equal values. Reformulate the algebra to avoid catastrophic cancellation.
Sum in order of increasing magnitude when possible, or use compensated summation (Kahan's algorithm).
Use higher precision for intermediate computations when precision loss is a concern (e.g., accumulate in double even if the final result is single).
Understand your rounding mode. The default (round to nearest, ties to even) is almost always the right choice. Changing the rounding mode is an advanced technique used mainly in interval arithmetic.
Restart long-running systems before accumulated errors become dangerous — or design the system to avoid time-dependent drift.
The IEEE 754 standard specifies what floating-point arithmetic must do. This section explains how Intel processors have implemented it in hardware — from the first external coprocessor in 1980 to the SIMD units that dominate modern computation.In the early days of microprocessors, floating-point arithmetic was performed in software — a single floating-point addition could take hundreds of clock cycles as the CPU executed a library subroutine that shifted, added, and normalized using integer instructions. For scientific and engineering applications, this was intolerably slow.Intel's solution was the math coprocessor — a separate chip that sat alongside the CPU and handled floating-point instructions:
Intel 8087 (1980): the first math coprocessor, designed to work with the 8086/8088 processors. It introduced the 80-bit extended precision format and the stack-based register architecture that would persist for decades. The 8087 could perform a floating-point multiply roughly 100× faster than the equivalent software emulation on the 8086.
Intel 80287 (1982): updated for the 80286 processor and its protected mode.
Intel 80387 (1987): the first coprocessor to fully implement the IEEE 754-1985 standard, including all four rounding modes and proper handling of infinities, NaNs, and denormalized numbers.
<br><img alt="Photograph of an Intel 80387 math coprocessor chip" src="assets/l13-intel-80387-chip.png" target="_self">The Intel 80387 (1987) — the first x87 math coprocessor to fully implement the IEEE 754-1985 standard, including all rounding modes and special value handling.Starting with the Intel 80486DX (1989), the floating-point unit was integrated directly onto the CPU die — no separate chip required. (The budget 80486SX omitted the FPU; users who needed it could add the 80487SX coprocessor.) Every Intel processor since the Pentium (1993) has included an on-die FPU.A remarkable consequence of decades of optimization is that on modern CPUs, floating-point multiply and divide can be faster than their integer equivalents for large operand sizes. Integer division, in particular, is often slower than floating-point division because the FP pipeline has received more transistor budget and microarchitectural attention.The x87 FPU architecture — named for the 8087/80287/80387 family — has several distinctive features:<br><img alt="Diagram of the x87 FPU register stack showing eight 80-bit registers R0 through R7, with ST(0) marking the current stack top" src="assets/l13-x87-fpu-registers.png" target="_self">The x87 FPU register architecture: eight 80-bit extended-precision registers organized as a stack, with ST(0) at the top. This stack-based design dates back to the original Intel 8087 (1980).Stack-based register file. The x87 has 8 data registers, labeled R0 through R7, organized as a stack rather than a flat register file. The register currently at the top of the stack is called ST(0) (or simply ST), the next one down is ST(1), and so on. Instructions like fld push a value onto the stack, and fst copies from the stack top to memory. Most arithmetic instructions operate implicitly on the stack top — fadd ST(1) adds ST(1) to ST(0) and stores the result in ST(0).This stack model was a reasonable design choice in 1980 when transistor budgets were tiny and compilers were simple. It minimizes the number of bits needed to encode register operands (you only need to specify the stack depth, not an arbitrary register number). But it creates headaches for modern optimizing compilers, which must carefully manage the stack to avoid spills and redundant loads.80-bit extended precision internally. All arithmetic inside the x87 is performed in the 80-bit extended precision format (1 sign + 15 exponent + 64 mantissa bits), regardless of whether the source data was 32-bit or 64-bit. This means that a chain of operations accumulates less rounding error than it would if each intermediate result were rounded to 32 or 64 bits. The downside is non-reproducibility: the same source code compiled with different optimization levels may produce different results, because the compiler may or may not spill intermediate values to memory (forcing a round to 64 bits) depending on register pressure.Rich data type support. The x87 can load and store values in seven different formats:The BCD format is a curiosity — it was included to support financial and COBOL-era applications that required exact decimal arithmetic.Special-purpose registers. Beyond the 8 data registers, the x87 maintains several control and status registers:
Control Word: sets the rounding mode, precision control, and exception masks.
Status Word: reports the current top-of-stack pointer, condition codes (used for comparisons), and exception flags.
Tag Word: tracks which data registers are valid, zero, special (NaN/infinity/denormal), or empty. This helps the hardware avoid unnecessary work on empty registers.
Last Instruction Pointer / Last Data Pointer / Opcode: record the address and opcode of the most recent FP instruction, used for debugging and exception handling.
Representative instructions. The x87 instruction set includes transcendental functions that have no equivalent in integer instructions:fld dword [x] ; push float from memory onto stack
fld st(2) ; push copy of ST(2) onto stack
fadd st(0), st(1) ; ST(0) = ST(0) + ST(1)
fmul qword [y] ; ST(0) = ST(0) * double from memory
fdiv st(1), st(0) ; ST(1) = ST(1) / ST(0)
fsqrt ; ST(0) = sqrt(ST(0))
fsin ; ST(0) = sin(ST(0))
fcos ; ST(0) = cos(ST(0))
fst dword [result] ; store ST(0) to memory as float
fstp qword [result] ; store ST(0) as double and pop stack
The x87's stack-based design, while elegant for 1980, became a serious limitation as compilers grew more sophisticated and applications demanded parallelism. Intel introduced a completely different floating-point model with SSE in 1999:SSE (Streaming SIMD Extensions, 1999): Added eight 128-bit registers (XMM0–XMM7) and instructions for both scalar and packed single-precision floating-point. Unlike the x87 stack, XMM registers are flat — any instruction can read from or write to any register.SSE2 (2001): Extended the XMM registers to support double-precision floating-point. This was the turning point — SSE2 made x87 largely obsolete for general-purpose scalar floating-point. Most compilers switched to SSE2 as their default code generation target.AVX (Advanced Vector Extensions, 2011): Widened the registers to 256 bits (YMM0–YMM15) and added three-operand instructions (dest ≠ src1), reducing the need for register-to-register moves.AVX-512 (2016): Widened further to 512 bits (ZMM0–ZMM31), doubled the register count to 32, and added mask registers for predicated execution. A single AVX-512 instruction can perform 8 double-precision or 16 single-precision operations simultaneously.Despite being officially deprecated for new development, the x87 FPU remains relevant for one reason: 80-bit extended precision. The SSE/AVX units operate only in 32-bit and 64-bit modes. If an application requires more than 16 significant decimal digits but fewer than 34 (which would require 128-bit "quad" precision, currently unsupported in hardware on x86), the x87's 80-bit format — with its 18.5 significant decimal digits — fills the gap.Applications that still use x87 extended precision include:
Long-double (long double in C/C++): On Linux x86-64, long double maps to the 80-bit extended format. On Windows and macOS, it maps to 64-bit double (the OS ABIs chose not to support 80-bit).
High-precision intermediate calculations: Some numerical libraries deliberately use 80-bit intermediates to reduce rounding error in algorithms like iterative refinement.
Legacy code: Decades of Fortran and C code written before SSE2 existed still targets the x87.
For new code, the recommendation is clear: use SSE/AVX (which is what your compiler does by default). Turn to x87 only when you have a specific, justified need for extended precision — and be aware of the portability and reproducibility costs.
Real numbers cannot be stored exactly — computers use finite approximations, introducing absolute and relative errors
Floating-point trades uniform precision for enormous range — the exponent controls range, the mantissa controls precision
IEEE 754 is the universal standard — it defines formats (single/double/extended), special values (±0, ±∞, NaN), and denormalized numbers for gradual underflow
FP arithmetic is not associative — (a+b)+c may differ from a+(b+c), and subtraction of nearly equal numbers causes catastrophic cancellation
Rounding modes and compensated summation (Kahan) are practical tools for managing FP errors
The x87 FPU is legacy — modern code uses SSE/AVX for floating-point, but understanding x87's stack-based 80-bit architecture provides historical context
What you'll do:
Investigate bit-level representation of float and double using Python's struct module
Experiment 1: Determine machine epsilon empirically
Experiment 2: Demonstrate loss of associativity — (a+b)+c ≠ a+(b+c)
Experiment 3: Catastrophic cancellation — subtract nearly equal numbers and observe precision loss
Experiment 4: Error accumulation in summation loops
Implement Kahan summation and compare accuracy with naive summation IEEE 754-2019, IEEE Standard for Floating-Point Arithmetic — the authoritative standard defining formats, operations, and rounding modes
David Goldberg, What Every Computer Scientist Should Know About Floating-Point Arithmetic (1991) — the classic reference on FP representation, errors, and pitfalls
Oregon State University, Computer Numbers and their Precision — educational module on number storage, precision vs accuracy, and machine epsilon
William Kahan, IEEE 754: An Interview with the Old Man of Floating-Point — history of the standard from its principal architect
Intel® 64 and IA-32 Architectures Software Developer's Manual, Volume 1, Chapter 8: x87 FPU — detailed reference for x87 architecture and instruction set
]]></description><link>lectures/eng/l13-eng.-floating-point-arithmetic.html</link><guid isPermaLink="false">Lectures/ENG/L13-ENG. Floating-Point Arithmetic.md</guid><pubDate>Mon, 06 Apr 2026 17:37:12 GMT</pubDate><enclosure url="assets/l13-fp-precision-meme.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l13-fp-precision-meme.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L13-NOTES-ENG. Floating-Point Arithmetic]]></title><description><![CDATA[Quick reference for Lecture 13. See full materials in L13-ENG. Floating-Point Arithmetic.md.
Real numbers have infinite precision; computer words are 32/64 bits — approximation is unavoidable
0.1 + 0.2 = 0.30000000000000004 in every language using IEEE 754
0.1₁₀ = 0.000110011…₂ (infinite repeating) — cannot be stored exactly
Real-world disasters: Patriot missile (1991, timing drift), Ariane 5 (1996, overflow), Vancouver Stock Exchange (1982, truncation bias)
Absolute error: Δ = |A − A_comp| (same units as the number)Relative error: δ = Δ/A ≈ Δ/A_comp (dimensionless, scale-independent)Example (π): A_comp = 3.1416 → Δ ≈ 7.35×10⁻⁶, δ ≈ 2.34×10⁻⁶Relative error is preferred — absolute error of 1 is negligible for 10⁶ but catastrophic for 10⁻⁶.Three consequences: rounding errors, catastrophic cancellation, algorithm instability.
Fixed m bits for fractional part → absolute error constant = 2⁻ᵐ
Relative error varies: small near max, huge near zero
Machine zero: values &lt; 2⁻ᵐ round to zero
Still used in DSP, finance (cents as integers), FPGA
Limitation: wastes precision on large numbers, lacks range for small ones
A = M × bᵉ — mantissa (precision) × base^exponent (range)
Exponent bits → RANGE; mantissa bits → PRECISION
3-digit mantissa + 2-digit exponent = 199 orders of magnitude in 7 characters
Normal form: 0 ≤ |M| &lt; 1 (ambiguous — many representations)Normalized form: 1 ≤ |M| &lt; base (unique representation — preferred)Hidden bit trick: binary normalized mantissa always starts with 1 → don't store it → n+1 effective bits from n stored bitsThree fields: S (1 bit, sign) | E (m bits, biased exponent) | M' (n bits, mantissa remainder)W = 1 + m + n bits Sign: S=0 positive, S=1 negative (sign-magnitude, not two's complement)
Biased exponent: E = e + bias, where bias = 2^(m−1) − 1 Single: bias = 127; Double: bias = 1023
Why bias? Enables unsigned integer comparison of floats Mantissa: M = 1 + M'/2ⁿ (hidden bit reconstructed)
Master formula:A = (-1)^S × (1 + M'/2^n) × 2^(E − bias)
Decoding (bits → decimal): extract S, E, M' → plug into formula.Encoding (decimal → bits):
Convert |number| to binary
Normalize to 1.xxxxx₂ × 2ᵉ
Round mantissa to n bits
M' = fractional part (drop "1.")
E = e + bias → convert to m-bit binary
Assemble S | E | M'
Example: −2012.6875 → binary 11111011100.1011₂ → normalize 1.1111011100101₂ × 2¹⁰ → E = 137 = 10001001₂ → result: C4 FB 96 00hInspecting bits:
Python: struct.pack('!f', x).hex()
Java: Float.floatToIntBits(x)
C: union type-punning
Precision formula: k = (n+1) × log₁₀(2) ≈ 0.301 × (n+1)Three failure modes:
Overflow: result too large → ±∞
Underflow: result too small → denormalized or 0
Truncation: mantissa too long → rounding error
FP number line is non-uniform — denser near zero, sparser at extremes.History: 1985, William Kahan (Berkeley). Current: IEEE 754-2019 / ISO 60559:2020.Formats:Complete encoding map (single precision):Key special values:
±0: +0 = 00000000h, −0 = 80000000h; +0 == −0 but 1/+0 = +∞, 1/−0 = −∞
Denormalized: hidden bit = 0; gradual underflow (smooth transition to zero)
±∞: overflow or x/0 (x≠0)
NaN: 0/0, ∞−∞, √(negative); NaN ≠ NaN; contagious in arithmetic
Addition: align exponents → add mantissas → normalize → round. Alignment can lose bits from smaller operand.Multiplication: XOR signs → add exponents (subtract bias) → multiply mantissas → normalize → round. No alignment loss.Division: XOR signs → subtract exponents (add bias) → divide mantissas → normalize → round.Rounding modes:
Round to nearest, ties to even (default) — eliminates statistical bias
Round toward zero (truncation)
Round toward +∞ (ceiling)
Round toward −∞ (floor)
Guard/Round/Sticky bits: 3 extra bits during computation enable correct rounding in hardware.Machine epsilon (ε): smallest number where 1.0 + ε ≠ 1.0
float32: 2⁻²³ ≈ 1.19×10⁻⁷
float64: 2⁻⁵² ≈ 2.22×10⁻¹⁶
Catastrophic cancellation: subtracting nearly equal numbers → most significant bits cancel → few correct digits remain. Fix: algebraic reformulation.Loss of associativity: (1e20 + (-1e20)) + 1.0 = 1.0 but 1e20 + ((-1e20) + 1.0) = 0.0Kahan summation: maintains a compensation variable to recover lost low-order bits during accumulation.Defensive practices:
Never compare floats with == — use tolerance
Avoid subtracting nearly equal values
Sum in order of increasing magnitude, or use Kahan
Use higher precision for intermediates
x87 history: 8087 (1980) → 80287 (1982) → 80387 (1987, first full IEEE 754) → integrated from 80486 (1989)x87 architecture: 8 registers R0-R7 as stack (not flat), 80-bit extended precision internally, supports int16/32/64, float32/64/80, BCD80SSE/AVX timeline: SSE (1999, 128-bit) → SSE2 (2001, double-precision) → AVX (2011, 256-bit) → AVX-512 (2016, 512-bit)x87 still relevant for 80-bit long double when &gt;16 decimal digits needed.
Real numbers → finite approximations → absolute and relative errors
Floating-point: exponent = range, mantissa = precision, hidden bit = free extra bit
IEEE 754: single/double/extended, special values (±0, ±∞, NaN), denormalized for gradual underflow
FP arithmetic is NOT associative; catastrophic cancellation loses significant digits
Kahan summation and rounding-mode awareness are practical defense tools
x87 is legacy; SSE/AVX is the modern default Inspect float/double bits with Python struct
Find machine epsilon experimentally
Demonstrate (a+b)+c ≠ a+(b+c)
Observe catastrophic cancellation
Implement Kahan summation vs naive summation
]]></description><link>lectures/eng/l13-notes-eng.-floating-point-arithmetic.html</link><guid isPermaLink="false">Lectures/ENG/L13-NOTES-ENG. Floating-Point Arithmetic.md</guid><pubDate>Mon, 06 Apr 2026 17:32:04 GMT</pubDate></item><item><title><![CDATA[L13-Pi-Digits]]></title><description><![CDATA[<img src="assets/l13-pi-digits.png" target="_self">]]></description><link>assets/l13-pi-digits.html</link><guid isPermaLink="false">assets/L13-Pi-Digits.png</guid><pubDate>Mon, 06 Apr 2026 17:11:28 GMT</pubDate><enclosure url="assets/l13-pi-digits.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l13-pi-digits.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L13-Pi-Error-Calculation]]></title><description><![CDATA[<img src="assets/l13-pi-error-calculation.png" target="_self">]]></description><link>assets/l13-pi-error-calculation.html</link><guid isPermaLink="false">assets/L13-Pi-Error-Calculation.png</guid><pubDate>Mon, 06 Apr 2026 17:11:28 GMT</pubDate><enclosure url="assets/l13-pi-error-calculation.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l13-pi-error-calculation.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L13-Normalized-Binary-Examples]]></title><description><![CDATA[<img src="assets/l13-normalized-binary-examples.png" target="_self">]]></description><link>assets/l13-normalized-binary-examples.html</link><guid isPermaLink="false">assets/L13-Normalized-Binary-Examples.png</guid><pubDate>Mon, 06 Apr 2026 17:11:27 GMT</pubDate><enclosure url="assets/l13-normalized-binary-examples.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l13-normalized-binary-examples.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L13-X87-FPU-Registers]]></title><description><![CDATA[<img src="assets/l13-x87-fpu-registers.png" target="_self">]]></description><link>assets/l13-x87-fpu-registers.html</link><guid isPermaLink="false">assets/L13-X87-FPU-Registers.png</guid><pubDate>Mon, 06 Apr 2026 17:11:27 GMT</pubDate><enclosure url="assets/l13-x87-fpu-registers.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l13-x87-fpu-registers.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L13-Intel-80387-Chip]]></title><description><![CDATA[<img src="assets/l13-intel-80387-chip.png" target="_self">]]></description><link>assets/l13-intel-80387-chip.html</link><guid isPermaLink="false">assets/L13-Intel-80387-Chip.png</guid><pubDate>Mon, 06 Apr 2026 17:11:27 GMT</pubDate><enclosure url="assets/l13-intel-80387-chip.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l13-intel-80387-chip.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L13-Denormalized-FP-Formula]]></title><description><![CDATA[<img src="assets/l13-denormalized-fp-formula.png" target="_self">]]></description><link>assets/l13-denormalized-fp-formula.html</link><guid isPermaLink="false">assets/L13-Denormalized-FP-Formula.png</guid><pubDate>Mon, 06 Apr 2026 17:11:27 GMT</pubDate><enclosure url="assets/l13-denormalized-fp-formula.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l13-denormalized-fp-formula.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L13-Normalized-FP-Formula]]></title><description><![CDATA[<img src="assets/l13-normalized-fp-formula.png" target="_self">]]></description><link>assets/l13-normalized-fp-formula.html</link><guid isPermaLink="false">assets/L13-Normalized-FP-Formula.png</guid><pubDate>Mon, 06 Apr 2026 17:11:27 GMT</pubDate><enclosure url="assets/l13-normalized-fp-formula.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l13-normalized-fp-formula.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L13-Division-By-Zero-Formula]]></title><description><![CDATA[<img src="assets/l13-division-by-zero-formula.png" target="_self">]]></description><link>assets/l13-division-by-zero-formula.html</link><guid isPermaLink="false">assets/L13-Division-By-Zero-Formula.png</guid><pubDate>Mon, 06 Apr 2026 17:11:27 GMT</pubDate><enclosure url="assets/l13-division-by-zero-formula.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l13-division-by-zero-formula.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L13-NaN-Bit-Layout]]></title><description><![CDATA[<img src="assets/l13-nan-bit-layout.png" target="_self">]]></description><link>assets/l13-nan-bit-layout.html</link><guid isPermaLink="false">assets/L13-NaN-Bit-Layout.png</guid><pubDate>Mon, 06 Apr 2026 17:11:27 GMT</pubDate><enclosure url="assets/l13-nan-bit-layout.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l13-nan-bit-layout.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L13-Example-Conversion-Bits]]></title><description><![CDATA[<img src="assets/l13-example-conversion-bits.png" target="_self">]]></description><link>assets/l13-example-conversion-bits.html</link><guid isPermaLink="false">assets/L13-Example-Conversion-Bits.png</guid><pubDate>Mon, 06 Apr 2026 17:11:27 GMT</pubDate><enclosure url="assets/l13-example-conversion-bits.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l13-example-conversion-bits.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L13-Max-Min-FP-Bit-Diagrams]]></title><description><![CDATA[<img src="assets/l13-max-min-fp-bit-diagrams.png" target="_self">]]></description><link>assets/l13-max-min-fp-bit-diagrams.html</link><guid isPermaLink="false">assets/L13-Max-Min-FP-Bit-Diagrams.png</guid><pubDate>Mon, 06 Apr 2026 17:11:27 GMT</pubDate><enclosure url="assets/l13-max-min-fp-bit-diagrams.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l13-max-min-fp-bit-diagrams.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L13-IEEE754-Formats-Table]]></title><description><![CDATA[<img src="assets/l13-ieee754-formats-table.png" target="_self">]]></description><link>assets/l13-ieee754-formats-table.html</link><guid isPermaLink="false">assets/L13-IEEE754-Formats-Table.png</guid><pubDate>Mon, 06 Apr 2026 17:11:27 GMT</pubDate><enclosure url="assets/l13-ieee754-formats-table.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l13-ieee754-formats-table.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L13-Overflow-Underflow-Number-Line]]></title><description><![CDATA[<img src="assets/l13-overflow-underflow-number-line.png" target="_self">]]></description><link>assets/l13-overflow-underflow-number-line.html</link><guid isPermaLink="false">assets/L13-Overflow-Underflow-Number-Line.png</guid><pubDate>Mon, 06 Apr 2026 17:11:27 GMT</pubDate><enclosure url="assets/l13-overflow-underflow-number-line.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l13-overflow-underflow-number-line.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L13-FP-Bit-Layout]]></title><description><![CDATA[<img src="assets/l13-fp-bit-layout.png" target="_self">]]></description><link>assets/l13-fp-bit-layout.html</link><guid isPermaLink="false">assets/L13-FP-Bit-Layout.png</guid><pubDate>Mon, 06 Apr 2026 17:11:27 GMT</pubDate><enclosure url="assets/l13-fp-bit-layout.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l13-fp-bit-layout.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L13-JS-Console-FP-Demo]]></title><description><![CDATA[<img src="assets/l13-js-console-fp-demo.jpeg" target="_self">]]></description><link>assets/l13-js-console-fp-demo.html</link><guid isPermaLink="false">assets/L13-JS-Console-FP-Demo.jpeg</guid><pubDate>Mon, 06 Apr 2026 17:11:27 GMT</pubDate><enclosure url="assets/l13-js-console-fp-demo.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l13-js-console-fp-demo.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L13-FP-Precision-Meme]]></title><description><![CDATA[<img src="assets/l13-fp-precision-meme.jpeg" target="_self">]]></description><link>assets/l13-fp-precision-meme.html</link><guid isPermaLink="false">assets/L13-FP-Precision-Meme.jpeg</guid><pubDate>Mon, 06 Apr 2026 17:11:27 GMT</pubDate><enclosure url="assets/l13-fp-precision-meme.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l13-fp-precision-meme.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Консультація - 2026-04-04]]></title><description><![CDATA[Дата: 4 квітня 2026 р. (субота)
Тривалість: ~52 хвилини
Формат: онлайн, відеоконференціяПитань по проєктах та лабораторних практично не було. Студентам, які вже завершили проєкт і мають лише мінорні фікси та документацію, рекомендовано записати фінальне відео-презентацію — коротко пройтися по структурі, зафіксувати, що все зроблено.По 12-й лабораторній роботі (NASM) питань також не виникло; код лабораторної навмисно побудований максимально просто, схоже на Java.Основна частина консультації була присвячена детальному розбору завдання та правил змагання (code golf). Нижче — повний опис усього, що потрібно знати.Написати інтерпретатор машини Тюрінга мовою Assembly (TASM, під DOSBox). Результат компілюється у .COM файл — саме його розмір у байтах визначає місце в рейтингу. Чим менший файл — тим краще.
TASM + DOSBox (не NASM, не Linux). Причина: .COM файл — найменший відомий формат виконуваного файлу (може бути навіть 14 байтів). В Linux мінімальний executable — більше 1000 байтів, тому там змагатися неможливо.
У DOSBox потрібно замаунтити диск F для змагання (щоб не перетинатися з D для лабораторних та E для проєкту). Додати рядок маунту в .conf файл DOSBox.
Для переходу на диск: F: → Enter.
У кожного учасника є репозиторій змагання з такими файлами:
tm.asm — ваш код інтерпретатора (головний файл).
cmp.asm — утиліта для побайтового порівняння результатів (не чіпати; при тестуванні буде перезаписана).
testit.bat — скрипт тестування (компілює, запускає тести, порівнює результати).
Папка tests/ — тестові .tm файли.
Файл .tm описує конкретну машину Тюрінга та її вхідні дані. Структура:
Коментарі — рядки, що починаються з ; (крапка з комою). Може бути будь-яка кількість. Ігноруються.
Порожні рядки — просто Enter. Може бути будь-яка кількість. Ігноруються.
Заголовок — перший змістовний рядок: два слова через пробіл/таб: Перше слово — Start State (початковий стан).
Друге слово — Halt State (кінцевий стан, при переході в який машина зупиняється). Правила переходу — кожне на окремому рядку, 5 токенів через пробіл або таб: CurrentState ReadSymbol NewState WriteSymbol Direction
Direction: R (праворуч), L (ліворуч), N (залишитись на місці). Маркер --- — три дефіси на окремому рядку, після яких йде вхідний рядок. Якщо маркер --- відсутній або після нього немає рядка — стрічка ініціалізується повністю порожньою (blank).
Вхідний рядок — записується на стрічку починаючи з позиції 0. Може бути порожнім (тоді стрічка повністю заповнена нулями).
Важливо щодо розділювачів: токени в правилах розділяються пробілом або табом (обидва варіанти мають працювати). Рядки завершуються або CR+LF (DOS/Windows), або тільки LF (Unix) — ваш код повинен коректно обробляти обидва варіанти. Практична порада: шукайте LF (код 10), а CR (код 13) просто скіпайте.
Імена станів — рядки з літер, цифр, дефісів та підкреслення (але не одиночне підкреслення). Максимальна довжина імені — 8 символів.
Максимальна кількість різних станів — 256.
Якщо Start State = Halt State, машина зупиняється негайно (просто виводить вхідний рядок як є).
На стрічці (в пам'яті):
Кожна комірка — байт від 0 до 255.
Порожня комірка = байт 0 (blank).
Пробіл (ASCII 32) — це звичайний символ, не blank.
У правилах (.tm файл):
Підкреслення _ означає blank (тобто байт 0 на стрічці).
Усі інші символи (ASCII 33–126) — це вони самі.
У вхідному рядку:
Пробіл (ASCII 32) — це звичайний пробіл на стрічці (не blank).
Підкреслення _ — це звичайний символ (ASCII 95), а не blank (на відміну від правил переходу).
ASCII 0 ніколи не зустрічається у вхідних файлах.
При виводі:
Байт 0 на стрічці виводиться як пробіл (ASCII 32).
ASCII 0 ніколи не виводиться напряму. Мінімальний розмір стрічки — 20 001 комірка (10 000 в кожну сторону від позиції 0).
Перед початком — вся стрічка ініціалізована нулями.
Вхідний рядок записується починаючи з позиції 0.
Головка встановлюється на позицію 0. Зчитати файл .tm, розпарсити правила, заповнити стрічку вхідним рядком.
Встановити поточний стан = Start State, головку на позицію 0.
Цикл: якщо поточний стан = Halt State → зупинитись негайно (перед спробою виконати правило). Інакше: знайти правило для (поточний стан, символ під головкою), записати новий символ, змінити стан, зсунути головку.
Після зупинки — вивести результат.
Важливо: зупинка відбувається перед виконанням переходу. Якщо машина перейшла у Halt State, вона зупиняється негайно — запис символу та зсув головки з попереднього правила вже виконані, але нове правило для Halt State не шукається й не виконується.
Виводити вміст стрічки від першої непорожньої (ненульової) комірки до останньої непорожньої.
Нулі між ними виводяться як пробіли (ASCII 32).
Якщо вся стрічка порожня — нічого не виводити (навіть не порожній рядок).
Не додавати Enter (новий рядок) в кінці виводу.
Вивід йде у STDOUT (не у відеопам'ять). При тестуванні STDOUT перенаправляється у файл для побайтового порівняння з еталоном.
tm.com program.tm
Ім'я .tm файлу передається як параметр командного рядка (через PSP, як на лабораторних). Результат виводиться у STDOUT.Гарантується коректність вхідних даних:
Файл .tm завжди існує та коректний.
Розмір файлу — не більше 32 000 байт.
Максимальна довжина вхідного рядка — 10 000 символів.
Максимальний розмір вхідного рядка + дописаних символів — 32 768 комірок.
Правила не дублюються.
Усі стани мають переходи (не буде ситуації «правило не знайдено»).
Машина завжди зупиниться.
Тому не потрібно писати код обробки помилок — це зекономить байти.Кожен тест має обмеження 60 секунд. Якщо ваша програма не встигає — тест не зараховується. Компроміс: менший код часто означає повільніший код, але потрібно вкладатись у тайм-аут.
Еталон — Python-реалізація машини Тюрінга, що лежить у репозиторії змагання. Можна (і потрібно) порівнювати результати своєї програми з Python-версією.
testit.bat автоматично компілює, запускає та порівнює.
Тести будуть додаватися в репозиторій змагання (до якого є доступ у всіх учасників). Клонуйте його для зручності оновлення.
Базовий набір (потрібно пройти для участі):
Тривіальна машина (Start State = Halt State).
Однопрохідна заміна символів.
Запис у порожню комірку в кінці стрічки.
Рух головки в обидва боки.
Переведення з одного алфавіту в інший.
Порожній вхідний рядок.
Розширений набір (потрібно пройти для потрапляння в рейтинг розмірів):
Edge-кейси: великі вхідні рядки (до 10 000 символів), багато станів (до 256), 8-символьні імена станів.
Багато кроків виконання.
Порожній вхід, один символ на вході тощо.
Під час розробки можна компілювати у .EXE (замість .COM) — тоді в дебагері будуть видні назви лейблів та зручніша навігація. На пізніх етапах оптимізації, коли код зав'яжеться на конкретні зміщення, це може не працювати — але до того моменту логіка вже буде віддебажена.
Часті коміти — обов'язково. Орієнтир: ~3 коміти на годину роботи.
Великий проміжок між комітами з великою зміною коду — дискваліфікація (підозра на генерацію коду ШІ або копіювання).
Повідомлення комітів — англійською, наказовий спосіб: add state parser, optimize tape lookup, remove unused labels. Формула: «If applied, this commit will ___».
Бонус +1 бал до підсумкової оцінки, якщо всі коміти відповідають цим вимогам.
Буде AI-перевірка (ШІ-чек) комітів на природність розробки. Не можна подати готове рішення першим комітом — це миттєва дискваліфікація.
Код cmp.asm буде перезаписуватися при тестуванні (захист від модифікації порівнювача). Дедлайн реєстрації — вже пройшов.
Базові тести — дедлайн 11 квітня, 22:00. Якщо на цю дату базові тести не проходять, потрібно вирішити: продовжувати чи повернутися до звичайних лабораторних.
Розширені тести — будуть додані пізніше.
Захист не потрібен — результати видно в репозиторії. Періодично будуть зустрічі для обговорення підходів до оптимізації.
Учасникам, які успішно проходять базові тести, автоматично зараховуються лабораторні 12, 13, 14 (NASM-блок). Серед учасників, чиї програми проходять усі тести (базові + розширені), складається турнірна таблиця за розміром COM-файлу з додатковими бонусами: 1 місце — +10 балів, 2–3 місце — +5, 4–6 місце — +3 до підсумкової оцінки.Важливо: якщо базові тести не пройдено до 11 квітня, 22:00 — усі автоматично зараховані бали за Lab12–Lab14 обнуляються (навіть якщо ви виконували ці лабораторні самостійно). Чим пізніше ви вирішите відмовитися від участі, тим більше балів втрачаєте. Якщо вирішили, що не встигаєте — сфокусуйтесь на проходженні хоча б базових тестів.На запитання про очікувані розміри:
Прогноз викладача: переможці — менше 200 байтів. Для порівняння: минулого року інтерпретатор нормальних алгоритмів Маркова довели до 109 байтів, позаминулого року інтерпретатор Brainfuck — до ~105 байтів (світовий рекорд — ~80 байтів).
Початковий розмір першої робочої версії — може бути кілька десятків кілобайт. Потім починається оптимізація.
Цікавий факт зі студентських експериментів: Claude оцінив реалістичний діапазон переможця в 100–150 байтів (абсолютний мінімум 70–100), тоді як GPT давав ~400–500 байтів. Оцінка Claude визнана ближчою до реальності.
Суботні консультації продовжуватимуться. Учасникам змагання тут можна обговорювати технічні питання та підходи до оптимізації. Приєднуватися можуть усі — і ті, хто змагається, і ті, хто просто хоче послухати.]]></description><link>office-hours/консультація-2026-04-04.html</link><guid isPermaLink="false">Office-hours/Консультація - 2026-04-04.md</guid><pubDate>Sun, 05 Apr 2026 16:11:13 GMT</pubDate></item><item><title><![CDATA[Lab12]]></title><description><![CDATA[
Курс: Архітектура обчислювальних систем
Тиждень: 12
Тривалість: 2 академічні години
Тип: Практична робота під наглядом інструктора
Після завершення практики студент зможе:
Дослідити адресний простір процесу через /proc/self/maps та ідентифікувати сегменти (text, data, heap, stack, бібліотеки).
Продемонструвати ASLR (Address Space Layout Randomization) та пояснити його роль у безпеці.
Пояснити demand paging на конкретних вимірах: чому mmap не виділяє фізичну пам'ять одразу.
Виміряти page faults за допомогою getrusage() та розрізнити minor і major faults.
Оцінити вартість minor page fault у наносекундах та порівняти з лекційною таблицею латентностей.
Перегляньте лекційні матеріали за Тиждень 12 (Віртуальна пам'ять поглиблено). Зверніть увагу на розділи:
Три стовпи віртуальної пам'яті — ізоляція, абстракція, ефективність
Сторінкова організація — сторінки (pages), фрейми (frames), таблиці сторінок
Декомпозиція віртуальної адреси — VPN, offset, індекси L4/L3/L2/L1
Page faults — minor (soft), major (hard), invalid
Demand paging — сторінки завантажуються лише при першому зверненні Важливо. Ця практика продовжує роботу в GitHub Codespaces (Linux x86-64). Якщо ви вже створювали Codespace для Lab 11, не створюйте новий — відкрийте існуючий зі сторінки <a data-tooltip-position="top" aria-label="https://github.com/codespaces" rel="noopener nofollow" class="external-link is-unresolved" href="https://github.com/codespaces" target="_self">github.com/codespaces</a>.
Механізми віртуальної пам'яті (page faults, demand paging, copy-on-write) — це взаємодія між вашою програмою та ядром ОС. Щоб спостерігати їх, потрібно:
Виділяти пам'ять через системний виклик mmap (а не просто DB чи RESB)
Читати лічильники ОС через /proc та getrusage
Створювати процеси через fork() (для демонстрації copy-on-write)
Усе це — API операційної системи Linux, яке доступне через C. Писати обгортки для цих викликів на NASM можна, але це зайві 50 рядків boilerplate на кожен виклик — без жодної додаткової користі для розуміння віртуальної пам'яті.Що вам потрібно знати з C для цієї практики:
for цикли та if — ви вже використовували їх у Lab 11 (cache_test.c, matrix_walk.c)
printf з форматами %d, %ld, %p — як і в Lab 11
&amp;змінна — адреса змінної (аналог OFFSET у TASM)
region[i * PAGE_SIZE] = 42 — запис у пам'ять за адресою (аналог MOV [BX+SI], 42)
Решту конструкцій (виклики mmap, getrusage, читання /proc) ми даємо готовим кодом з коментарями — вам не потрібно писати їх з нуля. У домашніх завданнях дано скелети програм, де вам залишається дописати прості цикли (ті ж for + запис/читання масиву).Кожен процес має власний віртуальний адресний простір — ілюзію, що він один у пам'яті. Типова структура (від низьких до високих адрес):┌──────────────────────┐ 0x7FFF... (високі адреси)
│ Stack │ ← локальні змінні, росте вниз ↓
│ ↓ │
│ (вільно) │
│ ↑ │
│ Memory-mapped / libs│ ← бібліотеки (libc, ld-linux)
│ (вільно) │
│ ↑ │
│ Heap │ ← malloc / mmap, росте вгору ↑
├──────────────────────┤
│ BSS │ ← неініціалізовані глобальні (нулі)
│ Data │ ← ініціалізовані глобальні
│ Text │ ← машинний код програми
└──────────────────────┘ 0x0000... (низькі адреси)
Файл /proc/self/maps показує реальну карту адресного простору поточного процесу. Кожен рядок — один регіон:адреса_початку-адреса_кінця права offset пристрій inode ім'я
55a3c4000000-55a3c4001000 r--p 00000 08:01 12345 /path/to/program
Права доступу: r = read, w = write, x = execute, p = private (COW), s = shared.Page fault виникає, коли процес звертається до віртуальної сторінки, якої немає у фізичній пам'яті:Функція getrusage() повертає лічильники page faults процесу:#include &lt;sys/resource.h&gt; struct rusage usage;
getrusage(RUSAGE_SELF, &amp;usage);
printf("Minor faults: %ld\n", usage.ru_minflt); // soft page faults
printf("Major faults: %ld\n", usage.ru_majflt); // hard page faults
mmap() створює віртуальний регіон пам'яті. З прапорцем MAP_ANONYMOUS фізична пам'ять не виділяється одразу — лише при першому зверненні до кожної сторінки (demand paging):#include &lt;sys/mman.h&gt; // Виділити 256 MB віртуальної пам'яті (фізично — 0 байт!)
char *region = mmap(NULL, 256 * 1024 * 1024, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); // Тепер VmSize зріс на 256 MB, але VmRSS — без змін!
// Лише коли ми торкнемось сторінки — ОС виділить фізичний фрейм (minor fault)
region[0] = 42; // ← перший minor page fault для цієї сторінки
48-бітна віртуальна адреса розбивається на 5 полів для 4-рівневої таблиці сторінок:│ 47..39 │ 38..30 │ 29..21 │ 20..12 │ 11..0 │
│ L4 (PML4)│ L3 (PDPT)│ L2 (PD) │ L1 (PT) │ Offset │
│ 9 біт │ 9 біт │ 9 біт │ 9 біт │ 12 біт │
L4_index = (VA &gt;&gt; 39) &amp; 0x1FF; // індекс у PML4
L3_index = (VA &gt;&gt; 30) &amp; 0x1FF; // індекс у PDPT
L2_index = (VA &gt;&gt; 21) &amp; 0x1FF; // індекс у Page Directory
L1_index = (VA &gt;&gt; 12) &amp; 0x1FF; // індекс у Page Table
offset = VA &amp; 0xFFF; // зміщення всередині сторінки (0–4095)
Відкрийте ваш існуючий Codespace (не створюйте новий!) і створіть робочу папку:mkdir -p Lab12 &amp;&amp; cd Lab12 Якщо інструменти не встановлені (нова машина), виконайте: sudo apt-get update &amp;&amp; sudo apt-get install -y nasm gdb valgrind
Створіть файл addr_explorer.c:// addr_explorer.c — дослідження адресного простору процесу
#include &lt;stdio.h&gt; // printf, fopen, fgets, fclose
#include &lt;stdlib.h&gt; // malloc, free
#include &lt;stdint.h&gt; // uintptr_t — цілочисельний тип для зберігання адрес
#include &lt;unistd.h&gt; // getpid
#include &lt;string.h&gt; int global_init = 42; // .data — ініціалізована глобальна
int global_uninit; // .bss — неініціалізована глобальна (нуль) // uintptr_t — ціле число, достатньо велике для зберігання адреси (64 біт)
// &gt;&gt; та &amp; — побітові зсув і маска (як SHR і AND в асемблері)
void decode_va(const char *name, uintptr_t va) { printf("\n Декомпозиція %s (0x%lx):\n", name, va); printf(" L4 (PML4): %3lu\n", (va &gt;&gt; 39) &amp; 0x1FF); // біти 47–39 printf(" L3 (PDPT): %3lu\n", (va &gt;&gt; 30) &amp; 0x1FF); // біти 38–30 printf(" L2 (PD): %3lu\n", (va &gt;&gt; 21) &amp; 0x1FF); // біти 29–21 printf(" L1 (PT): %3lu\n", (va &gt;&gt; 12) &amp; 0x1FF); // біти 20–12 printf(" Offset: %3lu (0x%03lx)\n", va &amp; 0xFFF, va &amp; 0xFFF);
} int main() { int stack_var = 7; // локальна змінна → стек int *heap_var = malloc(sizeof(int)); // динамічна пам'ять → купа *heap_var = 99; printf("=== Адреси у процесі PID %d ===\n\n", getpid()); // &amp;змінна — адреса змінної (як OFFSET у TASM) // (void *) — перетворення для друку адреси через %p // main — ім'я функції без () дає її адресу printf(" %-24s %p\n", "Код (main):", (void *)main); printf(" %-24s %p\n", "Глобальна (init):", (void *)&amp;global_init); printf(" %-24s %p\n", "Глобальна (uninit):", (void *)&amp;global_uninit); printf(" %-24s %p\n", "Купа (malloc):", (void *)heap_var); printf(" %-24s %p\n", "Стек (локальна):", (void *)&amp;stack_var); printf("\n=== Декомпозиція віртуальних адрес ===\n"); // (uintptr_t) — перетворюємо адресу в число, щоб виконати побітові операції decode_va("стек", (uintptr_t)&amp;stack_var); decode_va("купа", (uintptr_t)heap_var); decode_va("код", (uintptr_t)main); // /proc/self/maps — спеціальний файл Linux із картою пам'яті поточного процесу printf("\n=== /proc/self/maps ===\n\n"); FILE *f = fopen("/proc/self/maps", "r"); // відкрити файл для читання if (f) { char line[512]; while (fgets(line, sizeof(line), f)) // читати рядок за рядком printf(" %s", line); fclose(f); } free(heap_var); // звільнити динамічну пам'ять return 0;
}
Скомпілюйте та запустіть:gcc -O0 -o addr_explorer addr_explorer.c
./addr_explorer
Перед тим як дивитись на вивід, запишіть у файл results.txt своє передбачення:=== Частина 1: Передбачення ===
Які адреси будуть найнижчими, а які найвищими?
Мій прогноз (від найнижчої до найвищої):
1. ___
2. ___
3. ___
4. ___
5. ___ Підказка: згадайте схему адресного простору з теоретичного мінімуму.
Тепер перевірте — подивіться на вивід програми та допишіть у results.txt:Реальний порядок (від найнижчої до найвищої):
1. ___ = 0x___
2. ___ = 0x___
3. ___ = 0x___
4. ___ = 0x___
5. ___ = 0x___
Моє передбачення було правильним? ___
Знайдіть у виводі /proc/self/maps рядок, що містить кожну адресу. Для кожної змінної запишіть у results.txt:=== Частина 1: Зіставлення ===
main() → рядок maps: ___ (права: ___, сегмент: ___)
global_init → рядок maps: ___ (права: ___, сегмент: ___)
global_uninit→ рядок maps: ___ (права: ___, сегмент: ___)
heap_var → рядок maps: ___ (права: ___, сегмент: ___)
stack_var → рядок maps: ___ (права: ___, сегмент: [stack])
Подивіться на декомпозицію стека та купи. Запишіть відповіді:
Чи мають стек і купа однакові індекси L4? Чому або чому ні?
Що спільного в індексах двох адрес з одного сегмента (наприклад, global_init і global_uninit)?
Якби дві змінні мали однакові L4/L3/L2/L1, але різний offset — це означає, що вони на одній ___?
ASLR (Address Space Layout Randomization) — механізм безпеки, що рандомізує розташування стека, купи, бібліотек та коду при кожному запуску.Запустіть програму 5 разів і порівняйте адресу стека:for i in 1 2 3 4 5; do ./addr_explorer | grep "Стек"; done
Запишіть у results.txt: 5 значень адреси стека — чи відрізняються вони?Команда setarch дозволяє запустити програму без рандомізації:for i in 1 2 3 4 5; do setarch x86_64 -R ./addr_explorer | grep "Стек"; done
Запишіть у results.txt:=== Частина 2: ASLR ===
З ASLR (5 запусків, адреса стека): 1: 0x___ 2: 0x___ 3: 0x___ 4: 0x___ 5: 0x___ Без ASLR (5 запусків, адреса стека): 1: 0x___ 2: 0x___ 3: 0x___ 4: 0x___ 5: 0x___
Допишіть відповіді:
Скільки біт адреси змінюються між запусками з ASLR? (Порівняйте перші кілька символів — вони спільні, а далі розходяться)
Уявіть, що зловмисник знайшов уразливість buffer overflow у вашій програмі. Як ASLR ускладнює експлуатацію? (1–2 речення)
Чому вимкнення ASLR може бути корисним при відлагодженні?
Це ключовий експеримент практики. Ви побачите, що виділення пам'яті та використання пам'яті — це дві різні речі.Перш ніж писати код, дайте відповідь:
Програма виконує mmap(256 MB). Скільки фізичної пам'яті (RSS) буде виділено відразу після mmap?
(a) 256 MB — скільки попросили, стільки й отримали
(b) ~0 — жодна фізична сторінка не виділена
(c) 1 сторінка (4 KB) — мінімум для запуску
Запишіть відповідь у results.txt до запуску програми.Створіть файл demand_paging.c:// demand_paging.c — спостереження за demand paging
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;
#include &lt;sys/mman.h&gt; // mmap — виділення віртуальної пам'яті
#include &lt;sys/resource.h&gt; // getrusage — лічильник page faults #define REGION_SIZE (256 * 1024 * 1024) // 256 MB
#define PAGE_SIZE 4096 // Ця функція запитує ОС: "скільки minor page faults вже стались у моєму процесі?"
long get_minor_faults() { struct rusage r; // структура з лічильниками ресурсів getrusage(RUSAGE_SELF, &amp;r); // заповнити структуру поточними даними return r.ru_minflt; // повернути кількість minor faults
} void print_rss() { FILE *f = fopen("/proc/self/status", "r"); if (!f) return; char line[256]; while (fgets(line, sizeof(line), f)) { if (strncmp(line, "VmRSS:", 6) == 0) { printf(" %s", line); break; } } fclose(f);
} void print_vmsize() { FILE *f = fopen("/proc/self/status", "r"); if (!f) return; char line[256]; while (fgets(line, sizeof(line), f)) { if (strncmp(line, "VmSize:", 7) == 0) { printf(" %s", line); break; } } fclose(f);
} int main() { long pages = REGION_SIZE / PAGE_SIZE; printf("=== Етап 0: до mmap ===\n"); printf(" Minor faults: %ld\n", get_minor_faults()); print_vmsize(); print_rss(); // Виділяємо 256 MB віртуальної пам'яті // mmap — «дай мені шматок віртуальної пам'яті» // MAP_ANONYMOUS = не з файлу, а просто нулі // MAP_PRIVATE = лише для цього процесу // Повертає вказівник на початок виділеного регіону (як malloc, але потужніше) char *region = mmap(NULL, REGION_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (region == MAP_FAILED) { perror("mmap"); return 1; } printf("\n=== Етап 1: після mmap (256 MB виділено віртуально) ===\n"); printf(" Minor faults: %ld\n", get_minor_faults()); print_vmsize(); print_rss(); // Торкаємось кожної 4-ї сторінки (щоб було швидше — 1/4 сторінок) long touch_pages = pages / 4; long faults_before = get_minor_faults(); for (long i = 0; i &lt; touch_pages; i++) region[i * PAGE_SIZE] = 1; long faults_after = get_minor_faults(); printf("\n=== Етап 2: після торкання %ld сторінок (із %ld) ===\n", touch_pages, pages); printf(" Minor faults від торкання: %ld\n", faults_after - faults_before); print_vmsize(); print_rss(); // Тепер торкаємось РЕШТИ сторінок faults_before = get_minor_faults(); for (long i = 0; i &lt; pages; i++) region[i * PAGE_SIZE] = 2; faults_after = get_minor_faults(); printf("\n=== Етап 3: після торкання ВСІХ %ld сторінок ===\n", pages); printf(" Minor faults від торкання: %ld\n", faults_after - faults_before); printf(" (менше, ніж %ld — бо 1/4 вже торкнуті!)\n", pages); print_vmsize(); print_rss(); munmap(region, REGION_SIZE); return 0;
}
Скомпілюйте та запустіть:gcc -O0 -o demand_paging demand_paging.c
./demand_paging
Запишіть у results.txt:=== Частина 3: Demand Paging === Моє передбачення (a/b/c): ___
Правильна відповідь: ___ Етап 0 (до mmap): VmSize: ___ kB VmRSS: ___ kB Minor faults: ___ Етап 1 (після mmap, до торкання): VmSize: ___ kB VmRSS: ___ kB Minor faults: ___ VmSize зріс на ~___ kB (= ___ MB) ← віртуальна пам'ять VmRSS зріс на ~___ kB ← фізична пам'ять (!) Етап 2 (торкнуто 1/4 сторінок): Minor faults від торкання: ___ Очікувано: ___ (= 256 MB / 4 / 4096) VmRSS: ___ kB Етап 3 (торкнуто всі сторінки): Minor faults від торкання: ___ Чому менше за 65536? ___ VmRSS: ___ kB
Ключове запитання: програма «попросила» 256 MB. На якому етапі ОС реально виділила фізичну пам'ять? Чому такий підхід вигідний? (2 речення)У лекції ви бачили таблицю латентностей: TLB hit + L1 = ~2 нс, minor fault = ~1–10 мкс. Зараз ви виміряєте це на реальній машині.Створіть файл fault_cost.c:// fault_cost.c — вимірювання вартості minor page fault
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;sys/mman.h&gt;
#include &lt;sys/resource.h&gt;
#include &lt;time.h&gt; #define REGION_SIZE (128 * 1024 * 1024) // 128 MB
#define PAGE_SIZE 4096 long get_minor_faults() { struct rusage r; getrusage(RUSAGE_SELF, &amp;r); return r.ru_minflt;
} int main() { long pages = REGION_SIZE / PAGE_SIZE; char *region = mmap(NULL, REGION_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (region == MAP_FAILED) { perror("mmap"); return 1; } // --- Прохід 1: перше торкання (кожна сторінка → minor fault) --- long faults_before = get_minor_faults(); struct timespec t1, t2; clock_gettime(CLOCK_MONOTONIC, &amp;t1); for (long i = 0; i &lt; pages; i++) region[i * PAGE_SIZE] = 1; clock_gettime(CLOCK_MONOTONIC, &amp;t2); long faults_pass1 = get_minor_faults() - faults_before; double time_pass1 = (t2.tv_sec - t1.tv_sec) + (t2.tv_nsec - t1.tv_nsec) / 1e9; // --- Прохід 2: повторне торкання (сторінки вже в RAM → 0 faults) --- faults_before = get_minor_faults(); clock_gettime(CLOCK_MONOTONIC, &amp;t1); for (long i = 0; i &lt; pages; i++) region[i * PAGE_SIZE] = 2; clock_gettime(CLOCK_MONOTONIC, &amp;t2); long faults_pass2 = get_minor_faults() - faults_before; double time_pass2 = (t2.tv_sec - t1.tv_sec) + (t2.tv_nsec - t1.tv_nsec) / 1e9; printf("=== Вимірювання вартості minor page fault ===\n\n"); printf("Сторінок: %ld (= %d MB / %d)\n\n", pages, REGION_SIZE / (1024*1024), PAGE_SIZE); printf("Прохід 1 (перше торкання — з page faults):\n"); printf(" Minor faults: %ld\n", faults_pass1); printf(" Час: %.4f с\n", time_pass1); if (faults_pass1 &gt; 0) printf(" Вартість 1 fault: ~%.0f нс\n", (time_pass1 / faults_pass1) * 1e9); printf("\nПрохід 2 (повторне торкання — без faults):\n"); printf(" Minor faults: %ld\n", faults_pass2); printf(" Час: %.4f с\n", time_pass2); if (time_pass2 &gt; 0) printf("\nРізниця: %.1fx повільніше з faults\n", time_pass1 / time_pass2); munmap(region, REGION_SIZE); return 0;
}
Скомпілюйте та запустіть:gcc -O0 -o fault_cost fault_cost.c
./fault_cost
Запишіть у results.txt:=== Частина 4: Вартість Page Fault === Прохід 1 (з faults): Minor faults: ___ Час: ___ с Вартість 1 fault: ~___ нс Прохід 2 (без faults): Minor faults: ___ Час: ___ с Різниця: ___x Порівняння з лекцією: Лекція: minor fault ≈ 1 000–10 000 нс (1–10 мкс) Мій вимір: ~___ нс Висновок: ___
Запитання: Чому прохід 2 набагато швидший, хоча обидва проходи торкаються одних і тих самих сторінок? Прохід 2 — це, по суті, послідовний запис по пам'яті зі stride = 4096 байт. Згадайте Lab 11: що відбувається при stride-доступі з точки зору кешу? (Підказка: cache line = 64 байти, stride = 4096 байт) Уявіть сервер, що запускає 1000 процесів, кожен використовує 100 MB пам'яті. Якщо ОС виділяла б фізичну пам'ять відразу при mmap, скільки RAM знадобилося б? А з demand paging? (Підказка: більшість процесів реально використовують лише частину пам'яті) До кінця заняття:
addr_explorer.c — дослідження адресного простору з декомпозицією VA
demand_paging.c — демонстрація demand paging з VmRSS/VmSize
fault_cost.c — вимірювання вартості minor page fault
results.txt — передбачення, виміри та відповіді на всі питання
Закомічено та запушено всі файли (git add, git commit, git push) ⚠️ ВАЖЛИВО: Codespace — це інша машина!
Ваші файли існують лише всередині Codespace. Щоб вони з'явилися на GitHub:
cd ~/workspaces/*
git add Lab12/
git commit -m "Lab12: in-class work"
git push Не забудьте зупинити Codespace після роботи! Ctrl+Shift+P → Codespaces: Stop Current Codespace
Дедлайн: 23:59 у день перед Практикою 13Copy-on-Write (COW) — механізм, завдяки якому fork() працює миттєво: дочірній процес ділить фізичні сторінки з батьківським (обидва PTEs вказують на ті ж фрейми, позначені read-only). Лише при записі ОС копіює сторінку — це COW fault (різновид minor fault).Ваше завдання: допишіть програму cow_demo.c (три місця з TODO), що демонструє COW на конкретних числах.fork() створює копію поточного процесу. Після виклику існують два процеси з однаковим кодом і даними:pid_t pid = fork();
// З цього моменту код виконується у ДВОХ процесах одночасно!
if (pid == 0) { // Цей блок виконує ДОЧІРНІЙ процес (дитина)
} else { // Цей блок виконує БАТЬКІВСЬКИЙ процес waitpid(pid, NULL, 0); // чекаємо, поки дитина завершиться
}
Завдяки COW, fork() не копіює пам'ять фізично — обидва процеси ділять одні й ті ж фрейми (позначені read-only). Лише при записі ОС копіює сторінку.// cow_demo.c — Copy-on-Write Detective
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;
#include &lt;sys/mman.h&gt;
#include &lt;sys/resource.h&gt;
#include &lt;sys/wait.h&gt;
#include &lt;unistd.h&gt; #define REGION_SIZE (64 * 1024 * 1024) // 64 MB = 16384 сторінок
#define PAGE_SIZE 4096 long get_minor_faults() { struct rusage r; getrusage(RUSAGE_SELF, &amp;r); return r.ru_minflt;
} int main() { long pages = REGION_SIZE / PAGE_SIZE; // Крок 1: Виділити пам'ять і торкнути кожну сторінку char *region = mmap(NULL, REGION_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (region == MAP_FAILED) { perror("mmap"); return 1; } // TODO 1: Заповніть кожну сторінку, щоб усі були у фізичній пам'яті. // Цикл по i від 0 до pages, запис region[i * PAGE_SIZE] = будь-що. // (Аналогічно до demand_paging.c з пари) printf("Батько (PID %d): виділено %ld сторінок, усі торкнуті\n\n", getpid(), pages); fflush(stdout); // очистити буфер перед fork — інакше текст дублюється! long parent_before = get_minor_faults(); pid_t pid = fork(); if (pid == 0) { // ===== Дочірній процес ===== // Після fork() лічильник faults дитини починається з ~0 // --- Фаза читання: прочитати кожну сторінку --- long before_read = get_minor_faults(); // TODO 2: Прочитайте кожну сторінку (ЛИШЕ читання, без запису!). // Цикл по i від 0 до pages: // volatile char c = region[i * PAGE_SIZE]; // (volatile — щоб компілятор не викинув "непотрібне" читання) long read_faults = get_minor_faults() - before_read; printf("Дитина (PID %d):\n", getpid()); printf(" Фаза читання: minor faults = %ld\n", read_faults); // --- Фаза запису: записати в кожну сторінку (COW!) --- long before_write = get_minor_faults(); // TODO 3: Запишіть у кожну сторінку (як у TODO 1). // Цикл по i від 0 до pages: // region[i * PAGE_SIZE] = 42; long write_faults = get_minor_faults() - before_write; printf(" Фаза запису: minor faults = %ld\n", write_faults); fflush(stdout); _exit(0); // завершити дочірній процес } // ===== Батьківський процес ===== waitpid(pid, NULL, 0); // чекаємо, поки дитина завершиться long parent_after = get_minor_faults(); printf("\nБатько: мої minor faults після fork = %ld\n", parent_after - parent_before); munmap(region, REGION_SIZE); return 0;
} Підказка: кожен TODO — це цикл for на 2 рядки, аналогічний до коду з demand_paging.c (Частина 3). Різниця лише в тому, що в TODO 2 ви читаєте (volatile char c = region[...]), а в TODO 1 і TODO 3 — записуєте (region[...] = значення).
Батько (PID 1234): виділено 16384 сторінок, усі торкнуті Дитина (PID 1235): Фаза читання: minor faults = 0 ← ~0 (сторінки спільні з батьком!) Фаза запису: minor faults = 16384 ← ~16384 (COW — кожна сторінка копіюється) Батько: мої minor faults після fork = 2 ← мінімум (дитина копіювала, не батько) Точні числа відрізнятимуться. Головне — порядок величин: читання ~0 faults, запис ~N faults.
=== COW Detective === Розмір регіону: ___ MB = ___ сторінок Фаза читання (дитина): Minor faults: ___ Пояснення: чому ~0? ___ Фаза запису (дитина): Minor faults: ___ Пояснення: чому ~N? ___ Запитання:
1. Чому fork() — швидка операція, навіть якщо батьківський процес використовує гігабайти пам'яті? (1–2 речення)
2. Яка перевага COW для серверів, що використовують fork() для обслуговування кожного клієнта (наприклад, Apache prefork)?
Напишіть програму page_sim.c, що симулює алгоритми FIFO та LRU для заміщення сторінок../page_sim &lt;кількість_фреймів&gt; &lt;алгоритм&gt; &lt;послідовність_звернень...&gt;
Приклад з лекції:./page_sim 3 fifo 7 0 1 2 0 3 0 4 2 3 0 3 2 1 2 0 1 7 0 1
./page_sim 3 lru 7 0 1 2 0 3 0 4 2 3 0 3 2 1 2 0 1 7 0 1
Алгоритм: FIFO, Фреймів: 3, Звернень: 20 Крок Стор Фрейми Результат 1 7 [ 7 - - ] ПРОМАХ 2 0 [ 7 0 - ] ПРОМАХ 3 1 [ 7 0 1 ] ПРОМАХ 4 2 [ 2 0 1 ] ПРОМАХ (витіснено 7) 5 0 [ 2 0 1 ] ВЛУЧЕННЯ 6 3 [ 2 3 1 ] ПРОМАХ (витіснено 0) ... Загалом page faults: 15
// page_sim.c — симулятор алгоритмів заміщення сторінок
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt; #define MAX_REFS 100
#define MAX_FRAMES 10 void print_frames(int *frames, int num_frames) { printf("[ "); for (int i = 0; i &lt; num_frames; i++) { if (frames[i] == -1) printf("- "); else printf("%d ", frames[i]); } printf("]");
} // Повертає кількість page faults
int simulate_fifo(int *refs, int n, int num_frames) { int frames[MAX_FRAMES]; for (int i = 0; i &lt; num_frames; i++) frames[i] = -1; int faults = 0; int next_victim = 0; // індекс фрейму, який буде замінено наступним for (int step = 0; step &lt; n; step++) { int page = refs[step]; // TODO: Перевірте, чи сторінка вже у фреймах (hit) // TODO: Якщо промах — замістіть frames[next_victim] // і зсуньте next_victim: (next_victim + 1) % num_frames printf("%4d %4d ", step + 1, page); print_frames(frames, num_frames); // TODO: Виведіть ВЛУЧЕННЯ або ПРОМАХ (витіснено X) printf("\n"); } return faults;
} int simulate_lru(int *refs, int n, int num_frames) { int frames[MAX_FRAMES]; int last_used[MAX_FRAMES]; // крок останнього звернення до frames[i] for (int i = 0; i &lt; num_frames; i++) { frames[i] = -1; last_used[i] = -1; } int faults = 0; for (int step = 0; step &lt; n; step++) { int page = refs[step]; // TODO: Перевірте, чи сторінка вже у фреймах (hit) // Якщо hit — оновіть last_used[i] = step // TODO: Якщо промах — знайдіть фрейм з найменшим last_used // (або порожній фрейм), замістіть його printf("%4d %4d ", step + 1, page); print_frames(frames, num_frames); // TODO: Виведіть ВЛУЧЕННЯ або ПРОМАХ (витіснено X) printf("\n"); } return faults;
} int main(int argc, char *argv[]) { if (argc &lt; 4) { printf("Usage: %s &lt;frames&gt; fifo|lru ref1 ref2 ...\n", argv[0]); printf("Example: %s 3 fifo 7 0 1 2 0 3 0 4 2 3 0 3 2 1 2 0 1 7 0 1\n", argv[0]); return 1; } int num_frames = atoi(argv[1]); char *algo = argv[2]; int refs[MAX_REFS]; int n = argc - 3; if (n &gt; MAX_REFS) n = MAX_REFS; for (int i = 0; i &lt; n; i++) refs[i] = atoi(argv[3 + i]); printf("Алгоритм: %s, Фреймів: %d, Звернень: %d\n", algo, num_frames, n); printf("Рядок звернень: "); for (int i = 0; i &lt; n; i++) printf("%d ", refs[i]); printf("\n\n"); printf("Крок Стор Фрейми Результат\n"); int faults; if (strcmp(algo, "fifo") == 0) faults = simulate_fifo(refs, n, num_frames); else if (strcmp(algo, "lru") == 0) faults = simulate_lru(refs, n, num_frames); else { printf("Невідомий алгоритм: %s (використовуйте fifo або lru)\n", algo); return 1; } printf("\nЗагалом page faults: %d\n", faults); return 0;
}
Запустіть з лекційним прикладом (3 фрейми, рядок звернень із лекції):Якщо ваші числа відрізняються — перегляньте алгоритм у лекційних матеріалах.=== Симулятор заміщення сторінок === Тест 1: лекційний приклад (3 фрейми) FIFO faults: ___ (очікувано: 15) LRU faults: ___ (очікувано: 12) Тест 2: ваш приклад (придумайте рядок із 10+ звернень, де LRU кращий за FIFO — тобто дає менше faults) Рядок: ___ Фреймів: ___ FIFO faults: ___ LRU faults: ___ Запитання:
1. У якому випадку FIFO може бути ГІРШИМ за випадкове заміщення? (Підказка: Belady's anomaly — що це?)
2. Чому реальні ОС (Linux, Windows) використовують Clock замість LRU? (Підказка: скільки коштує точний LRU на кожному зверненні до пам'яті?)
Додайте до page_sim.c третій алгоритм — Clock (Second-Chance):
Кожен фрейм має біт accessed (0 або 1)
При завантаженні сторінки: accessed = 1
При влученні: accessed = 1
При промаху: hand (стрілка годинника) обходить фрейми по колу: Якщо accessed == 0 → витіснити цей фрейм
Якщо accessed == 1 → скинути (accessed = 0), перейти до наступного Запустіть з лекційним прикладом. Результат має бути між 12 і 15 faults (між LRU та FIFO). Конкретне число залежить від початкового стану стрілки та порядку обходу.Збережіть результати Clock у page_sim_report.txt.Запишіть відео, у якому: Продемонструйте demand_paging — запустіть програму, покажіть, як VmRSS змінюється по етапах. Поясніть: чому після mmap(256 MB) фізична пам'ять не змінилась? Продемонструйте page_sim — запустіть FIFO і LRU на лекційному прикладі. Покажіть різницю у кількості faults. Поясніть один алгоритм (FIFO або LRU): як він обирає «жертву» для витіснення? Намалюйте 3–4 кроки на папері або екрані. УВАГА! Вимога про 5хв максимально важлива! Brevity is the soul of wit. Це частина оцінки.Lab12/
├── addr_explorer.c ← дослідження адресного простору
├── demand_paging.c ← демонстрація demand paging
├── fault_cost.c ← вимірювання вартості page fault
├── results.txt ← виміри та відповіді з пари
├── cow_demo.c ← домашнє: COW detective
├── cow_report.txt ← звіт: COW faults + відповіді
├── page_sim.c ← домашнє: симулятор заміщення
├── page_sim_report.txt ← звіт: FIFO vs LRU + відповіді
└── video.txt ← посилання на Loom
Не забудьте git push! Якщо ви працюєте у Codespace — без push ваша робота існує лише на віддаленій машині GitHub. Закомітьте, запушіть, переконайтесь, що файли видно на github.com у вашому репозиторії.
<br>
<a data-tooltip-position="top" aria-label="https://man7.org/linux/man-pages/man2/getrusage.2.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://man7.org/linux/man-pages/man2/getrusage.2.html" target="_self">man getrusage(2)</a> — лічильники ресурсів процесу <br>
<a data-tooltip-position="top" aria-label="https://man7.org/linux/man-pages/man2/mmap.2.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://man7.org/linux/man-pages/man2/mmap.2.html" target="_self">man mmap(2)</a> — виділення віртуальної пам'яті <br>
<a data-tooltip-position="top" aria-label="https://man7.org/linux/man-pages/man5/proc_pid_maps.5.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://man7.org/linux/man-pages/man5/proc_pid_maps.5.html" target="_self">man proc(5) — /proc/pid/maps</a> — формат карти пам'яті <br>
<a data-tooltip-position="top" aria-label="https://man7.org/linux/man-pages/man2/fork.2.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://man7.org/linux/man-pages/man2/fork.2.html" target="_self">man fork(2)</a> — створення дочірнього процесу <br>
<a data-tooltip-position="top" aria-label="https://www.cs.usfca.edu/~galles/visualization/Paging.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.cs.usfca.edu/~galles/visualization/Paging.html" target="_self">Page Replacement Algorithms Visualized</a> — інтерактивна візуалізація FIFO/LRU/OPT <br>
<a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Demand_paging" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Demand_paging" target="_self">Demand Paging (Wikipedia)</a> — концепція ледачого завантаження сторінок <br>
<a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Copy-on-write" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Copy-on-write" target="_self">Copy-on-Write (Wikipedia)</a> — механізм COW ]]></description><link>labs/lab12.html</link><guid isPermaLink="false">Labs/Lab12.md</guid><pubDate>Wed, 01 Apr 2026 16:56:11 GMT</pubDate></item><item><title><![CDATA[L12-NOTES-ENG. Virtual Memory In-Depth]]></title><description><![CDATA[Quick reference for Lecture 12. See full materials in L12-ENG. Virtual Memory In-Depth.md.
Isolation — Process A cannot access Process B's memory; MMU refuses the translation
Abstraction — Every process sees the same clean virtual layout (text, data, heap, stack)
Efficiency — RAM is a cache for disk; only actively-used pages reside in physical memory (demand paging) Virtual: private, contiguous per process; 128 TB user / 128 TB kernel on x86-64
Physical: shared DRAM pool; managed by OS via a frame table
Key layout features: null-pointer guard (unmapped first page), stack grows down, heap grows up, .text is read-only, kernel pages present but U/S = 0.Context switch = write new CR3 → swaps entire address space. Modern CPUs use PCIDs to avoid TLB flush.
Page = fixed chunk of virtual space; Frame = fixed chunk of physical RAM; both 4 KB (2^12) on x86
Non-contiguous allocation eliminates external fragmentation
Virtual address = [VPN (high bits) | Offset (low 12 bits)]
Translation: VPN indexes page table → PTE → frame number + offset → physical address. Offset passes through unchanged.Example: VA 0x2A3C → VPN 2, offset 0xA3C → frame 3 → PA 0x3A3C.Flat table cost: 32-bit = 4 MB/process; 48-bit = 512 GB/process → need multi-level tables.Each PTE is 64 bits. Key bit fields:When P = 0: OS repurposes remaining 63 bits (e.g., swap location). A and D bits drive page replacement algorithms. Extract PA from PTE: PTE &amp; 0x000FFFFFFFFFF000.Two-level (32-bit): Split 20-bit VPN into two 10-bit indices → 1,024-entry outer + inner tables (each 4 KB). On-demand allocation. Typical: ~16 KB vs 4 MB flat.Four-level (x86-64): 512 entries × 8 B = 4 KB per table. | PML4 (9b) | PDPT (9b) | PD (9b) | PT (9b) | Offset (12b) |Index extraction: L4 = (VA &gt;&gt; 39) &amp; 0x1FF, L3 = (VA &gt;&gt; 30) &amp; 0x1FF, L2 = (VA &gt;&gt; 21) &amp; 0x1FF, L1 = (VA &gt;&gt; 12) &amp; 0x1FF, offset = VA &amp; 0xFFF.Without TLB: 4 accesses for walk + 1 for data = 5 total. Minimum overhead: 4 × 4 KB = 16 KB. Typical: 100–500 KB.Trade-off: more TLB reach vs internal fragmentation and allocation difficulty.Fully-associative hardware cache of VPN → PFN translations (+ ASID, valid, dirty, permission, global bits).Flow: TLB hit → 1–2 cycles → physical address. TLB miss → hardware page walk (4 reads) → fill TLB → retry. Present = 0 at any level → page fault.EMAT = T_tlb + T_mem + (1 - h) × T_walk
Example: T_tlb = 1 ns, T_mem = 100 ns, T_walk = 400 ns, h = 99% → EMAT = 1 + 100 + 0.01 × 400 = 105 ns.Context switches: old CPUs flush TLB on CR3 change; modern use PCID tags so entries coexist. Global bit keeps kernel entries across switches.Handling (major): save state → read CR2 → validate address → find free frame (evict if needed, writeback if dirty) → disk read via DMA → update PTE (P = 1) → INVLPG → restart instruction.Demand paging: PTEs start with P = 0; pages loaded on first access. COW: after fork(), shared read-only frames; write triggers copy of that page only.EAT = (1 - p) × 200 ns + p × page_fault_time
To keep degradation &lt; 10%, fault rate must be below ~1 per million accesses.Reference string (3 frames, 20 refs): 7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2, 1, 2, 0, 1, 7, 0, 1Clock: circular buffer + hand pointer. A = 0 → evict; A = 1 → clear, advance, repeat. Enhanced variant also checks Dirty bit — priority: (0,0) best &gt; (0,1) &gt; (1,0) &gt; (1,1) worst.OPT is impossible (needs future). True LRU is impractical (per-access tracking). Clock is what Linux, FreeBSD, and Windows actually use.Permission matrix (U/S × R/W):NX bit (bit 63): prevents instruction fetch from data pages → W^X policy (no page both writable and executable).ASLR: randomizes stack (~30 bits), libraries (~28 bits), heap (~13 bits), PIE (~28 bits) per run. Defeats ROP. KPTI: Meltdown mitigation — separate user/kernel page tables; ~1–5% overhead with PCID.Range: 5,000,000:1 from best to worst case for the same mov instruction.
Virtual memory provides isolation, abstraction, and efficiency
Paging: VPN → page table → frame number; offset unchanged
x86-64 uses 4-level page tables (PML4 → PDPT → PD → PT); only used branches allocated
TLB makes paging practical — 99%+ hit rate reduces overhead to ~5%
Page faults must be rare — even 1/1,000 causes 41× slowdown (HDD)
Clock algorithm = practical LRU approximation used by real OSes
Protection via R/W, U/S, NX bits per page; ASLR randomizes layout
Full path: VA → TLB → page tables → PA → cache hierarchy → data Examine /proc/[pid]/maps — identify code, data, heap, stack, shared libraries
Use pmap and vmstat for memory statistics
Observe ASLR: run same program multiple times, compare addresses
Measure page faults with perf stat -e page-faults
Compare 4 KB vs 2 MB pages and TLB effects
Sequential vs random access — see locality's impact on VM performance <a data-tooltip-position="top" aria-label="https://diveintosystems.org/book/C13-OS/vm.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://diveintosystems.org/book/C13-OS/vm.html" target="_self">Dive Into Systems — Ch. 13: Virtual Memory</a> — comprehensive introduction
<br><a data-tooltip-position="top" aria-label="https://cs61.seas.harvard.edu/site/2018/Section4/" rel="noopener nofollow" class="external-link is-unresolved" href="https://cs61.seas.harvard.edu/site/2018/Section4/" target="_self">Harvard CS61 — x86-64 Page Tables</a> — four-level walk details
<br><a data-tooltip-position="top" aria-label="https://blog.zolutal.io/understanding-paging/" rel="noopener nofollow" class="external-link is-unresolved" href="https://blog.zolutal.io/understanding-paging/" target="_self">Zolutal Blog — Understanding x86-64 Paging</a> — PTE format, huge pages
<br><a data-tooltip-position="top" aria-label="https://cs.brown.edu/courses/csci0300/2022/notes/l16.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://cs.brown.edu/courses/csci0300/2022/notes/l16.html" target="_self">Brown CS0300 — Page Tables</a> — multi-level tables, NX bit
<br><a data-tooltip-position="top" aria-label="https://www.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/9_VirtualMemory.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/9_VirtualMemory.html" target="_self">UIC — Virtual Memory (Silberschatz)</a> — demand paging, COW, replacement algorithms
]]></description><link>lectures/eng/l12-notes-eng.-virtual-memory-in-depth.html</link><guid isPermaLink="false">Lectures/ENG/L12-NOTES-ENG. Virtual Memory In-Depth.md</guid><pubDate>Mon, 30 Mar 2026 21:48:29 GMT</pubDate></item><item><title><![CDATA[L12-ENG. Virtual Memory In-Depth]]></title><description><![CDATA[
Lecture 12 of Computer Systems Architecture course at NaUKMA.
After this lecture you will be able to:
Explain the three goals of virtual memory: process isolation, address space abstraction, and memory efficiency
Describe paging fundamentals: pages, frames, page tables, and page table entries with all x86-64 PTE flags
Trace a virtual address through the complete translation process, including the VPN-to-frame-number lookup
Compare flat and multi-level page tables, and walk through x86-64's four-level page table structure (CR3 → PML4 → PDPT → PD → PT)
Explain TLB operation (hit/miss flow, ASID, context switches) and calculate effective memory access time using the AMAT formula
Distinguish minor, major, and invalid page faults, and describe the OS fault-handling sequence
Compare page replacement algorithms: FIFO, Optimal, LRU, and Clock/Second-Chance
Describe memory protection mechanisms: R/W, U/S, and NX permission bits, and the role of ASLR in security
Imagine an apartment building where every tenant has the same floor plan — rooms numbered 1 through 4, a hallway, a front door. From inside, you might forget that dozens of other people share the same building. You never see their furniture; you cannot walk into their kitchen. The building manager, however, knows the truth: every apartment maps onto shared physical infrastructure, and the manager assigns resources, ensures isolation, and occasionally reshuffles storage when space runs tight.This is virtual memory. Every process sees its own private, contiguous address space — starting from the same low addresses and stretching to the same high addresses. The operating system is the building manager, the hardware's Memory Management Unit (MMU) is the lock system on every door, and together they maintain the illusion that each process owns the entire machine.You first encountered virtual memory in L06 (Protected Mode, privilege rings, General Protection Faults) and in L08 (pages, frames, page tables, PTEs, page faults, TLB basics). L08 gave you the what — the components and the basic flow. This lecture gives you the how and the why at scale: how a 64-bit processor walks four levels of page tables, the real cost of a TLB miss (connecting to L11's AMAT formula), quantitative comparison of page replacement algorithms, and memory protection features like the NX bit that prevent entire categories of security exploits.Virtual memory serves three goals simultaneously, and every design decision in this lecture connects back to at least one of them:Isolation. Process A cannot read or write Process B's memory — the hardware physically will not translate A's virtual addresses to B's physical frames. A bug in your browser cannot corrupt your text editor; a compromised application cannot read your SSH keys.Abstraction. Every process sees the same clean, contiguous layout — text near the bottom, heap growing up, stack growing down. The process does not know whether its pages are scattered across RAM or partially swapped to disk. The virtual address space is a contract: "You use these addresses; we will make them work."Efficiency. RAM is treated as a cache for disk. Only actively-used pages reside in physical memory — this is demand paging. A program that allocates 2 GB but touches only 200 MB consumes only 200 MB of RAM. A system with 16 GB can run dozens of processes whose combined virtual spaces total hundreds of gigabytes.The hardware provides mechanisms (fast translation, exception generation, permission checking); the software provides policy (which pages to load, which to evict, how to handle faults). This extends the same idea from L11 one level down the hierarchy — from "SRAM as a cache for DRAM" to "DRAM as a cache for disk."In the Real Mode environment you used in Labs P1–P10, programs accessed physical memory directly — no isolation, no protection, no way to use more memory than physically installed. The <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Atlas_(computer)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Atlas_(computer)" target="_self">Atlas Computer</a> (Manchester, 1962) introduced the first working virtual memory, transparently moving data between fast core memory and slower drum storage. Sixty years later, every computer, phone, and server uses the same fundamental idea.Let us begin with the address space itself.The central abstraction of virtual memory is the address space — the set of all addresses a process can use. Every process has two address spaces, though it is only aware of one: The virtual address space is what the process sees. It is private, contiguous, and — on a 64-bit system — enormous. The process's code, data, heap, and stack all live at virtual addresses. The physical address space is the actual DRAM installed in the machine. It is shared by all processes, managed by the OS, and invisible to user-mode programs. The process operates exclusively in the virtual address space, unaware of where its data physically resides. The MMU and the OS handle the mapping.Every process sees roughly the same virtual address space layout. The details vary between Linux, Windows, and macOS, but the overall structure is universal:<br><img alt="Process virtual address space layout showing kernel space at top, stack growing down, heap growing up, BSS, data, and text segments" src="assets/l12-virtual-address-space-eng.svg" target="_self" style="width: 650px; max-width: 100%;">Key observations about this layout:Kernel space at the top. The upper portion of every process's virtual address space is reserved for the kernel. On 32-bit Linux, the default split is 3 GB user / 1 GB kernel. On 64-bit Linux, user space spans the lower 128 TB (0x0000000000000000–0x00007FFFFFFFFFFF), and kernel space occupies the upper half from 0xFFFF800000000000. The kernel pages are present in every process's page table but marked supervisor-only — user-mode access triggers a page fault.The null-pointer guard. The lowest addresses (first page or first few pages) are intentionally unmapped. Dereferencing a null pointer therefore triggers a page fault that the OS converts into a SIGSEGV signal — turning the most common programming error into a clean, debuggable crash rather than silent corruption.Stack grows down, heap grows up. The stack starts near the top of user space and grows toward lower addresses. The heap starts after BSS and grows upward. The large unmapped gap between them maximizes the distance between these two dynamically growing regions, making collisions unlikely.Text segment is read-only. The program's executable code is loaded into read-only, executable pages. Any write attempt triggers a protection fault, preventing bugs and exploits from modifying running code.The key insight: every process sees the same virtual layout, but each one maps to different physical locations.Process A has a variable at virtual address 0x7FFE1000. Process B also has a variable at 0x7FFE1000. Same virtual address, different physical frames — perhaps frame 42,381 for A and frame 107,209 for B. Each process has its own page table with completely independent entries.<br>From each process's perspective, it has a full, private address space and is the sole program running. The reality — dozens of processes sharing physical RAM — is hidden behind the translation layer. This is the lone view abstraction described in <a data-tooltip-position="top" aria-label="https://diveintosystems.org/book/C13-OS/vm.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://diveintosystems.org/book/C13-OS/vm.html" target="_self">Dive Into Systems</a>.<br><img alt="Two processes with identical virtual address layouts mapping to different physical frames in shared memory" src="assets/l12-virtual-physical-mapping-eng.svg" target="_self" style="width: 650px; max-width: 100%;">While each process enjoys a private virtual address space, the physical address space is a single, shared pool of DRAM. If the machine has 16 GB of RAM, the physical address space spans 0x000000000 through 0x3FFFFFFFF. Every byte is a real capacitor on a real DRAM chip.The OS manages physical memory via a frame table that tracks the status of every physical frame: free, allocated (and to which process/page), or reserved for the kernel. A single physical frame can serve multiple purposes: Private mapping: One virtual page in one process maps to one physical frame — the usual case. Shared mapping: Multiple processes' PTEs point to the same physical frame. This is how shared libraries work — libc.so code exists in physical memory only once, but every process maps those frames as read-only executable pages, saving hundreds of megabytes on a busy system. Copy-on-write (COW): After fork(), parent and child share all frames (marked read-only). Only when one writes does the OS copy that single frame. As you learned in L08, this makes process creation extremely efficient. When the OS switches from Process A to Process B, it updates a single register: CR3 (Control Register 3), which holds the physical address of the top-level page table. Writing a new CR3 value makes the MMU instantly use Process B's page table.Changing CR3 also invalidates cached translations in the TLB. On older processors, this meant a full TLB flush on every context switch. Modern processors support Process Context Identifiers (PCIDs) — tags on each TLB entry identifying which process it belongs to. With PCIDs, switching CR3 does not flush the TLB; entries from Process A simply stay dormant and are ready when the OS switches back.<br>The 32-bit limit of 4 GB became insufficient by the mid-2000s. Intel's <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Physical_Address_Extension" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Physical_Address_Extension" target="_self">PAE</a> expanded physical addresses to 36 bits (64 GB), but each process was still limited to 4 GB virtual. AMD's x86-64 extension (2003) expanded virtual addresses to 48 bits (256 TB per process).Only 48 of 64 bits are used because a full 64-bit space (16 EB) would require impractically large page tables. The x86-64 architecture uses canonical addresses: bits 63–48 must be all zeros or all ones, creating a lower half (user space) and an upper half (kernel space) with a non-canonical gap between them. Recent processors support 5-level paging (LA57), extending to 57 bits (128 PB) for server workloads.The virtual address space layout encodes important security and engineering properties: Kernel pages in every process's address space means the OS does not need to switch page tables for system calls or interrupts — only the privilege level changes (Ring 3 → Ring 0), keeping system calls fast (hundreds of nanoseconds). The unmapped gap between stack and heap acts as a guard region — stack overflow hits an unmapped page and crashes cleanly rather than silently overwriting heap data. <br>
ASLR (Address Space Layout Randomization) randomizes starting addresses of stack, heap, and shared libraries each run, making exploit techniques like <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Return-oriented_programming" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Return-oriented_programming" target="_self">ROP</a> much harder. Separate page table entries per region enable fine-grained protection: text pages are read-only + executable, data/stack pages are read-write + non-executable (via the NX bit). Details in Section 10. L08 introduced paging as the mechanism behind virtual memory. Now we formalize every piece — by the end of this section you will decompose a virtual address into its fields, walk a page table by hand, and construct the resulting physical address with exact bit arithmetic.Virtual memory splits two distinct address spaces into equal-size chunks:
The virtual address space (what the program sees) is divided into pages.
The physical address space (what the RAM chips see) is divided into frames (also called page frames).
Pages and frames are always the same size. On x86 and x86-64 systems, the standard size is 4 KB = 4,096 bytes = 2^12 bytes. (Larger sizes — 2 MB "large pages" and 1 GB "huge pages" — exist for specialized workloads such as databases and virtual machines, but 4 KB remains the default and the one we will use in all examples unless otherwise stated.)The key advantage is non-contiguous allocation: a program's virtual pages 0, 1, 2, 3 might map to physical frames 7302, 41, 998, and 12055 — scattered across RAM. The program sees a contiguous address space; the OS places each page in whichever frame is available. This eliminates external fragmentation entirely — frames are interchangeable, and the OS never needs to compact memory.Every virtual address is implicitly split into two fields:The offset identifies the specific byte within a page. Because a page is 2^12 bytes, the offset is always the bottom 12 bits — they can address any byte from 0 to 4,095 within the page.The VPN is everything above those 12 bits. It identifies which page the address belongs to. The number of VPN bits depends on the total virtual address width:The split is determined entirely by the page size. With 4 KB pages, the bottom 12 bits are always the offset; the remaining bits are always the VPN. There is nothing to configure — the hardware does this split automatically on every memory access.The page table is a data structure — conceptually a simple array — that the OS maintains for each process. It is indexed by VPN, and each entry (called a Page Table Entry or PTE) contains the physical frame number plus metadata bits (which we will dissect in Section 4).When the CPU generates a virtual address, the translation proceeds as follows:
Split the address into VPN and offset.
Use the VPN as an index into the current process's page table.
Read the PTE at that index.
Extract the physical frame number from the PTE.
Construct the physical address by concatenating the frame number with the original offset.
The offset passes through unchanged — this is a direct and elegant consequence of pages and frames being the same size. Only the "which block" part of the address changes; the "where within the block" part is identical in both virtual and physical addresses.The CPU must know where the current process's page table is located in physical memory. This information lives in a dedicated CPU register:
On x86 (32-bit): the CR3 register holds the physical address of the top-level page directory.
On x86-64: CR3 holds the physical address of the PML4 table (the top level of the four-level hierarchy).
When the OS performs a context switch — switching from running Process A to Process B — it saves Process A's CR3 value and loads Process B's CR3. This single register write effectively swaps the entire virtual address space. After the switch, the same virtual address 0x00401000 now maps to a completely different physical frame, because the page table it indexes into is a different table entirely. This is how the OS provides each process with the illusion of having its own private memory.CR3 is accessible only in kernel mode (ring 0) — if user code could modify it, isolation would be defeated entirely.Let us walk through a complete address translation using a simplified 16-bit system with 4 KB pages. This small example makes every bit visible.Setup:
Virtual addresses: 16 bits wide → address space = 2^16 = 65,536 bytes = 64 KB
Page size: 4 KB = 2^12 bytes
Offset: 12 bits (bits 11–0)
VPN: 4 bits (bits 15–12) → 2^4 = 16 virtual pages (pages 0–15)
Physical memory: say 32 KB = 2^15 bytes → 8 frames (frames 0–7)
Page table for this process:Translate virtual address 0x2A3C:Step 1 — Convert to binary:0x2A3C = 0010 1010 0011 1100 ──── ──────────────── VPN Offset
The top 4 bits are 0010 = VPN 2. The bottom 12 bits are 1010 0011 1100 = offset 0xA3C (decimal 2,620).Step 2 — Look up VPN 2 in the page table: frame = 3, present = yes.Step 3 — Construct the physical address. Frame 3 in our 8-frame system needs 3 bits to represent (since we have 2^3 = 8 frames). The frame number 3 in binary is 011. The physical address is:Frame: 011
Offset: 1010 0011 1100
─────────────────────────
Physical address: 011 1010 0011 1100 = 0x3A3C
The virtual byte at 0x2A3C lives at physical address 0x3A3C. Notice that the offset 0xA3C is identical in both addresses — only the page/frame number changed.What if we tried virtual address 0x1500?0x1500 = 0001 0101 0000 0000 ──── ──────────────── VPN=1 Offset=0x500
VPN 1 is marked "not present" in the page table. The hardware raises a page fault (exception 14 on x86). The OS page fault handler must then decide whether to load the page from disk, allocate a new frame, or terminate the process — as we will discuss in detail in Section 8.The simplicity of the "array indexed by VPN" design comes at a cost. Let us calculate the page table size for realistic systems:32-bit virtual addresses, 4 KB pages:
VPN: 20 bits → 2^20 = 1,048,576 entries
Each PTE: 4 bytes (enough for a 20-bit frame number plus metadata bits)
Total page table size: 2^20 x 4 = 4 MB per process
For x86-64 with 48-bit virtual addresses: 2^36 entries × 8 bytes = 512 GB per process — larger than the physical memory it maps. This is absurd; a typical process uses only a few thousand pages.The solution is multi-level (hierarchical) page tables (Section 6): a tree of smaller tables where branches for unused regions do not exist, reducing overhead from gigabytes to tens of kilobytes. The cost is multiple memory accesses per translation (one per level), which is why the TLB (Section 7) is critical.Each PTE is not merely a frame number — it is a densely packed bit field where every bit controls a specific aspect of how the hardware handles that page.Regardless of the specific processor, every PTE contains two categories of information: Physical frame number — the address of the physical frame this virtual page maps to. Control and metadata bits — flags that tell the hardware how to handle accesses: whether the page is present in RAM, what access types are permitted, whether the page has been accessed or modified, and caching hints. The exact layout varies by architecture, but the categories are universal. We examine the x86-64 format in detail.An x86-64 PTE is 8 bytes (64 bits) wide:The Present bit is the single most consequential bit in the PTE. When the MMU finds P = 0, it immediately raises a page fault exception (interrupt 14 on x86) without examining any other bit.This creates an opportunity: when a page is not present, the OS can repurpose the other 63 bits for its own bookkeeping. Linux stores the swap partition index and disk block number where the evicted page resides. The hardware defines the mechanism (check bit 0; if zero, fault); the software defines the policy (use the remaining bits to track where the page went).The Read/Write bit (bit 1) and User/Supervisor bit (bit 2) form the permission model:When U/S = 0, the page is supervisor-only — any user-mode access triggers a fault regardless of R/W. When U/S = 1, the page is user-accessible, and R/W determines whether writes are also permitted.This is how the OS protects itself. Kernel code and data structures live on pages with U/S = 0. Even if a user-mode program discovers the virtual address of a kernel structure, the MMU refuses the access in hardware — there is no software check to bypass.These two bits are set by the hardware, not by the OS: Accessed (A, bit 5): Set to 1 by the MMU on any access (read or write). Only the OS clears it. Dirty (D, bit 6): Set to 1 by the MMU on writes. Only the OS clears it. These bits are the foundation of page replacement algorithms. The OS periodically clears A bits; on the next scan, pages with A = 0 have not been used recently and are eviction candidates — this approximates LRU without per-page timestamps. The D bit tells the OS whether an evicted page must be written to disk (D = 1, ~100 μs on SSD) or can simply be discarded (D = 0, essentially free). We return to these bits in Section 9.When PS = 1 in a page directory entry, the translation stops at that level — the entry directly maps a 2 MB page (at the PD level) or a 1 GB page (at the PDPT level). This dramatically reduces TLB pressure for large-memory workloads. We cover huge pages in detail in Section 6.7 and their TLB implications in Section 7.6.Kernel pages are mapped identically in every process's page table. The Global bit marks such pages so their TLB entries are not flushed when CR3 changes, avoiding the cost of re-translating kernel addresses after every context switch.The NX (No eXecute) bit — called XD (eXecute Disable) by Intel — occupies the most significant bit of the PTE. When NX = 1, the processor refuses to fetch instructions from this page. The OS marks data pages (stack, heap) with NX = 1 and code pages (.text) with NX = 0, preventing a major class of security exploits. We will examine the security implications in detail in Section 10.2.The 40-bit field stores the upper bits of the physical frame address. Since pages are 4 KB-aligned, the bottom 12 bits are always zero and need not be stored. This yields 2^40 pages × 2^12 bytes = 4 PB of addressable physical memory.To extract the physical page address from a raw PTE value:physical_page_address = PTE &amp; 0x000FFFFFFFFFF000
This mask clears the flag bits (0–11) and the NX / protection key bits (52–63), leaving only the physical address.Suppose the 64-bit PTE contains:0x800000001A3F5067
In binary (showing key bit positions):Bit 63 (NX): 1 → Execute disabled (data page)
Bits 12-51: 0x1A3F5 → Physical frame 0x1A3F5 (frame 107,509)
Bit 8 (G): 0 → Not global
Bit 7 (PS): 0 → Standard 4 KB page
Bit 6 (D): 1 → Page has been written to (dirty)
Bit 5 (A): 1 → Page has been accessed
Bit 2 (U/S): 1 → User-accessible
Bit 1 (R/W): 1 → Read-write
Bit 0 (P): 1 → Present in RAM
Breaking down the hex value 0x067 (the low 12 bits) = 0000 0110 0111 in binary:Bit 0 (P): 1
Bit 1 (R/W): 1
Bit 2 (U/S): 1
Bit 3 (PWT): 0
Bit 4 (PCD): 0
Bit 5 (A): 1
Bit 6 (D): 1
Bit 7 (PS): 0
This PTE describes a user-accessible, read-write, dirty, accessed, non-executable 4 KB page in physical frame 0x1A3F5. The physical base address is 0x1A3F5 &lt;&lt; 12 = 0x1A3F5000. If the offset from the virtual address were 0x7B0, the final physical address would be 0x1A3F5000 + 0x7B0 = 0x1A3F57B0.This is the complete journey of a single memory access: virtual address → VPN + offset → page table lookup → PTE → extract frame number → concatenate with offset → physical address. Every memory access your program makes goes through this process, billions of times per second, transparently handled by the MMU.With the building blocks in place — VPN/offset split, page table entries, metadata bits — let us assemble the complete translation algorithm that the MMU performs on every memory access.When the CPU executes any instruction that references memory — a mov, a push, or even the instruction fetch itself — the following process unfolds:Step 1: The CPU generates a virtual address — the pointer value, stack pointer, or program counter.Step 2: Split into VPN and offset. The low 12 bits (for 4 KB pages) become the page offset; the remaining high bits become the VPN.Virtual Address (32-bit example, 4 KB pages):
| VPN (20 bits) | Offset (12 bits) |
| bits 31 ............. 12 | bits 11 ......... 0 |
Step 3: Index into the page table. The MMU uses the Page Table Base Register (PTBR — called CR3 on x86) to find the start of the page table in physical memory. The VPN serves as an index: the hardware computes PTBR + VPN × PTE_size to locate the relevant Page Table Entry.Step 4: Read the PTE and check the Present bit. The MMU reads the entry at that location. If the Present bit (bit 0 on x86) is 0, the page is not currently in physical memory. The MMU raises a page fault exception, and the OS page fault handler takes over (we will trace this path in Section 8). For now, assume the bit is 1 — the page is resident.Step 5: Check permission bits. Even if the page is present, the access may be illegal. The MMU verifies that the type of access (read, write, or execute) is permitted by the R/W, U/S, and NX bits in the PTE. A violation — for example, a write to a read-only page or an instruction fetch from a page with NX=1 — triggers a protection fault.Step 6: Extract the frame number from the PTE. The PTE contains the physical frame number (PFN) — the actual location in RAM where this page's data resides. On x86-64, the frame address occupies bits 51:12 of the PTE (a 40-bit physical address, 4 KB-aligned).Step 7: Construct the physical address. The hardware concatenates the frame number (from the PTE) with the page offset (from the original virtual address). This is the real address that goes out on the memory bus:Physical Address = [ Frame Number | Offset ] (from PTE) (from VA — unchanged)
Step 8: Access physical memory at this address. The data is read from (or written to) the physical location. From the processor's perspective, the translation is invisible — it just asked for data at a virtual address and got it back.<br><img alt="Virtual address to physical address translation: VPN/offset split, page table lookup, and PA construction" src="assets/l12-address-translation-eng.svg" target="_self" style="width: 650px; max-width: 100%;">The page offset bits are never translated. They pass directly from the virtual address to the physical address. Only the VPN changes — it is replaced by the frame number. This works because pages and frames are the same size: byte position 2,687 within a virtual page is the same as byte position 2,687 within the physical frame. With fixed-size pages, the offset is simply concatenated — no addition, no bounds check, just wire the bits together.Let us trace through a concrete example with every bit visible. We will use a small system to keep the numbers manageable, then scale up to real hardware.System parameters:
Virtual address space: 16 bits (64 KB)
Physical address space: 15 bits (32 KB)
Page size: 4 KB (2^12 = 4,096 bytes)
Page offset: 12 bits
VPN: 16 − 12 = 4 bits (16 virtual pages)
PFN: 15 − 12 = 3 bits (8 physical frames)
The page table for our process (stored in memory, pointed to by PTBR):Notice the sparse structure: only 5 of 16 virtual pages are actually mapped. Pages 3–13 form the empty gap between the heap and the stack — a miniature version of what every real process looks like.The virtual address to translate: 0xE2A4Step 1: Convert to binary.0xE2A4 = 1110 0010 1010 0100
Step 2: Split into VPN and offset.| VPN (4 bits) | Offset (12 bits) |
| 1110 | 0010 1010 0100 |
| = 14 | = 0x2A4 |
The virtual address falls in virtual page 14, at byte offset 0x2A4 (676 in decimal) within that page.Step 3: Look up VPN 14 in the page table.From the table above: VPN 14 → Present = 1, Frame # = 1.Step 4: Check Present bit. Present = 1, so the page is in memory. Good — no page fault.Step 5: Extract frame number. Frame # = 1, which in binary is 001.Step 6: Construct the physical address.Physical Address = [ Frame # (3 bits) | Offset (12 bits) ] = [ 001 | 0010 1010 0100 ] = 001 0010 1010 0100 = 0x12A4
Step 7: Access physical memory at address 0x12A4.Let us verify this makes sense. Frame 1 starts at physical address 1 × 4,096 = 4,096 = 0x1000. The offset within the page is 0x2A4 = 676 bytes. So the physical address is 0x1000 + 0x2A4 = 0x12A4. The arithmetic checks out.Here is the complete picture:Virtual address: 0xE2A4 ├── VPN = 14 ──→ Page Table lookup ──→ Frame = 1 └── Offset = 0x2A4 ─────────────────────────────────┐ │
Physical address: 0x12A4 │ ├── Frame 1 base = 0x1000 │ └── + Offset = 0x02A4 ◄───────────────────────┘
The offset 0x2A4 appears identically in both addresses — it was never changed, only carried through. The VPN (14) was replaced by the frame number (1). That is all paging does.There is an uncomfortable truth buried in this algorithm. Look at Step 3 again: to read the PTE, the hardware must access the page table, which is itself stored in physical memory. That is one memory access. Then in Step 8, the hardware accesses the actual data — a second memory access. Every single memory reference by the program now requires two trips to memory:Without paging: 1 memory access per reference
With paging: 2 memory accesses per reference (1 for PTE + 1 for data)
This effectively doubles the effective memory latency. If a memory access takes 100 ns, then with paging enabled, every access takes 200 ns. For a CPU that can issue instructions every fraction of a nanosecond, this is devastating.And it gets worse. As we will see in Section 6, real systems use multi-level page tables — four levels on x86-64. Without any optimization, each memory reference would require five memory accesses: four for the page table walk (one per level) plus one for the actual data. At 100 ns per access, that is 500 ns per memory reference — a 5× slowdown.This is not a theoretical concern. Unoptimized, paging would make modern computers unusably slow. The solution is a small but critical piece of hardware called the Translation Lookaside Buffer (TLB) — a cache specifically designed to store recent address translations. We will examine it in detail in Section 7. For now, just know that the TLB reduces the vast majority of translations to a single fast lookup, making paging practical despite its inherent overhead.The single-level page table works for toy systems, but real address spaces make it catastrophic.Consider x86-64 with its 48-bit virtual address space:Number of virtual pages = 2^48 / 2^12 = 2^36 = 68,719,476,736 pages
PTE size = 8 bytes (64-bit entries)
Page table size = 2^36 × 8 = 549,755,813,888 bytes = 512 GB per process
512 gigabytes per process. A single flat page table for a 48-bit address space would require more storage than most computers have in total RAM. Even a 32-bit system with 4 KB pages needs 2^20 × 4 = 4 MB per process — and over 99% of entries are marked "not present" for a typical process that uses only a few dozen megabytes.Real address spaces are sparse — a handful of regions (code, heap, stack) separated by enormous empty gaps:0xFFFF...FFFF ┌──────────────────┐ │ Kernel space │ (shared, mapped by OS)
0x7FFF...FFFF ├──────────────────┤ │ │ │ Stack ↓ │ ~8 MB ├──────────────────┤ │ │ │ (vast empty │ ~99.99% of address space │ gap) │ │ │ ├──────────────────┤ │ Heap ↑ │ ~10 MB ├──────────────────┤ │ Data │ ~1 MB ├──────────────────┤ │ Code (Text) │ ~5 MB
0x0000...0000 └──────────────────┘
A flat page table allocates space for the entire range. We need a structure that allocates only for the regions in use, while keeping O(1) lookups (ruling out hash tables with worst-case collisions and sorted lists with O(log n) access). The solution is a hierarchical (multi-level) page table — a tree of small tables where each table fits in exactly one page and unused branches are never allocated.Instead of one million-entry table, split the 20-bit VPN into two 10-bit indices:32-bit Virtual Address with two-level paging:
| Outer Index (10 bits) | Inner Index (10 bits) | Offset (12 bits) |
| bits 31 ........... 22 | bits 21 ........... 12 | bits 11 ........ 0|
The outer table (page directory): 2^10 = 1,024 entries × 4 bytes = 4 KB (one page). Each entry points to an inner table or is marked "not present."Each inner table: 1,024 entries × 4 bytes = 4 KB (one page). Each entry contains a frame number.Inner tables are allocated on demand. If a region is unmapped, no inner table exists — the outer entry is simply "not present."
Extract the outer index (bits 31–22). Use CR3 to find the page directory.
Read the page directory entry at [outer index]. If "not present" → page fault.
The entry gives the physical address of the relevant inner table.
Extract the inner index (bits 21–12). Read the inner table entry at [inner index].
If Present = 0 → page fault. Otherwise, extract the frame number.
Construct: [frame number | offset].
A process using 4 MB code + 4 MB heap + 4 MB stack (each spanning one inner table's 2^10 pages × 4 KB = 4 MB coverage):
1 page directory + 3 inner tables = 16 KB (vs. 4 MB for a flat table — 256× savings)
<br><img alt="Two-level page table: outer page directory with inner tables allocated only for used regions" src="assets/l12-two-level-page-table-eng.svg" target="_self" style="width: 650px; max-width: 100%;">A 48-bit address space needs more levels. With 8-byte entries and 4 KB pages, each table holds 4,096 / 8 = 512 = 2^9 entries (9-bit index). The 36 non-offset bits require 36 / 9 = 4 levels.x86-64 Virtual Address (48 bits used):
| Sign Ext. | L4 Index | L3 Index | L2 Index | L1 Index | Offset |
| (16 bits) | (9 bits) | (9 bits) | (9 bits) | (9 bits) | (12 bits) |
| bits 63-48| bits 47-39 | bits 38-30 | bits 29-21 | bits 20-12 | bits 11-0 |
Bits 63–48 are a sign extension of bit 47, creating two valid ranges separated by a non-canonical hole:
User space: 0x0000'0000'0000'0000 to 0x0000'7FFF'FFFF'FFFF (128 TB)
Kernel space: 0xFFFF'8000'0000'0000 to 0xFFFF'FFFF'FFFF'FFFF (128 TB)
CR3 holds the physical address of the PML4 table. On every context switch, the OS loads the new process's PML4 address into CR3.Every table at every level has 512 entries × 8 bytes = 4,096 bytes = one page. This uniform geometry means tables can be allocated and paged using the standard page allocator.
Read CR3 → PML4 base.
L4 index (bits 47–39) → read PML4[L4_index]. Not present → page fault. Otherwise → PDPT address.
L3 index (bits 38–30) → read PDPT[L3_index]. Not present → page fault. PS=1 → 1 GB huge page. Otherwise → PD address.
L2 index (bits 29–21) → read PD[L2_index]. Not present → page fault. PS=1 → 2 MB huge page. Otherwise → PT address.
L1 index (bits 20–12) → read PT[L1_index]. Not present → page fault. Otherwise → frame number from bits 51:12.
Physical address: frame address | offset (bits 11:0 of VA).
Without a TLB, a full translation costs 4 memory accesses plus 1 for the data — 5 total. At 100 ns per access, that is 500 ns per reference.L4_index = (VA &gt;&gt; 39) &amp; 0x1FF; // PML4 index (bits 47-39)
L3_index = (VA &gt;&gt; 30) &amp; 0x1FF; // PDPT index (bits 38-30)
L2_index = (VA &gt;&gt; 21) &amp; 0x1FF; // PD index (bits 29-21)
L1_index = (VA &gt;&gt; 12) &amp; 0x1FF; // PT index (bits 20-12)
offset = VA &amp; 0xFFF; // Page offset (bits 11-0)
The mask 0x1FF is nine 1-bits, isolating exactly 9 bits after the shift.512 × 512 × 512 × 512 × 4,096 = 2^9 × 2^9 × 2^9 × 2^9 × 2^12 = 2^48 = 256 TB
This is split into 128 TB user + 128 TB kernel. The physical address space uses PTE bits 51:12 (40 bits), supporting up to 2^52 bytes = 4 PB; current processors commonly implement 46 bits (64 TB).<br><img alt="x86-64 four-level page table walk: CR3 to PML4 to PDPT to PD to PT to physical frame" src="assets/l12-four-level-page-table-eng.svg" target="_self" style="width: 650px; max-width: 100%;">Translate virtual address 0x0000'0040'1234'5678 (user-space, bit 47 = 0).Step 1: Extract all indices.L4 = (0x0000004012345678 &gt;&gt; 39) &amp; 0x1FF = 0
L3 = (0x0000004012345678 &gt;&gt; 30) &amp; 0x1FF = 64
L2 = (0x0000004012345678 &gt;&gt; 21) &amp; 0x1FF = 72
L1 = (0x0000004012345678 &gt;&gt; 12) &amp; 0x1FF = 421
Offset = 0x0000004012345678 &amp; 0xFFF = 0x678
Step 2: Walk the page tables.Assume these physical addresses:1. CR3 = 0x0010'1000 → PML4 base
2. PML4[0] at 0x0010'1000 + 0×8 = 0x0010'1000 → PDPT at 0x0020'2000
3. PDPT[64] at 0x0020'2000 + 64×8 = 0x0020'2200 → PD at 0x0030'3000
4. PD[72] at 0x0030'3000 + 72×8 = 0x0030'3240 → PT at 0x0040'4000
5. PT[421] at 0x0040'4000 + 421×8 = 0x0040'4D28 → frame at 0x0ABC'D000
Step 3: Construct the physical address.Physical address = 0x0ABC'D000 | 0x678 = 0x0ABC'D678
Four memory accesses for translation + one for data = 5 total. The TLB caches the result VPN 0x0000004012345 → PFN 0x0ABCD, so subsequent accesses to any byte in the same 4 KB page skip the walk entirely.Even a process mapping a single byte needs one table per level: 4 × 4 KB = 16 KB minimum. A typical process (code + heap + stack + shared libraries) uses 1 PML4, 2–4 PDPTs, 5–20 PDs, and 10–100 PTs — roughly 100–500 KB. Compare with a flat table for the same 48-bit address space: 512 GB. The savings come from a simple principle: unused branches of the tree are never allocated.Standard 4 KB pages work well for most workloads, but databases, VMs, and scientific computing benefit from huge pages that cover larger regions with a single TLB entry. The mechanism: the PS (Page Size) bit in a Level 2 or Level 3 entry tells the MMU to stop the walk early and use the remaining address bits as an offset.
2 MB huge pages (PS set at Level 2 — PD): walk stops after 3 levels, offset = 21 bits.
1 GB huge pages (PS set at Level 3 — PDPT): walk stops after 2 levels, offset = 30 bits.
VA with 2 MB page: | L4 (9) | L3 (9) | L2 (9) | ──── Offset (21 bits) ──── |
VA with 1 GB page: | L4 (9) | L3 (9) | ──────── Offset (30 bits) ──────── |
The primary benefit is TLB reach:The trade-off is internal fragmentation (a 100 KB allocation in a 2 MB page wastes 1.9 MB) and allocation difficulty (the OS must find contiguous physical regions of 2 MB or 1 GB, which becomes harder as memory fragments). Linux offers Transparent Huge Pages (THP) to use huge pages opportunistically and fall back to 4 KB pages when needed.Multi-level page tables solve the memory consumption problem but introduce a new one: on x86-64, every translation requires four sequential memory reads (PML4 → PDPT → PD → PT) before the actual data access. That is five memory accesses per reference — a 5× slowdown that would make paging impractical.The solution is the same principle that makes the entire memory hierarchy work: caching. The Translation Lookaside Buffer (TLB) stores recently used translations in a small, fast hardware cache, so the vast majority of accesses skip the page table walk entirely.On x86-64 with DDR4 memory:
Each page table level requires one memory read: ~100 ns
Four-level walk: 4 × 100 ns = 400 ns for translation alone
Plus 100 ns for the data access → 500 ns total (vs. 100 ns without paging)
At 4 GHz (0.25 ns/cycle), that is 2,000 clock cycles per memory access
The walk is inherently sequential — you cannot read the PDPT entry until the PML4 entry tells you where the PDPT resides — destroying any out-of-order execution parallelism.The TLB is a specialized hardware cache in the MMU. Unlike data caches that store memory content, the TLB stores address translations — VPN-to-PFN mappings with associated permission and status bits.Each TLB entry contains:
VPN (Virtual Page Number) — the high-order bits identifying the page
PFN (Physical Frame Number) — the translation result
ASID (Address Space Identifier) — identifies which process owns this translation (see Section 7.5)
Valid bit — whether this entry holds a valid translation
Dirty bit — whether the page has been written to (mirrors the PTE dirty bit)
Permission bits — Read, Write, Execute — checked on every access
Global bit — marks translations shared across all address spaces (kernel mappings)
Why fully associative? Unlike data caches (typically 8-way or 16-way set-associative), TLBs are usually fully associative — all entries are searched in parallel on every lookup. This eliminates conflict misses, which is critical because the TLB is tiny (32–1,024 entries) and every miss triggers an expensive page walk. The hardware cost is acceptable because the TLB is small enough for parallel comparators to remain practical.Split TLB design. Modern CPUs maintain separate TLBs for instructions and data, plus a unified second-level TLB — mirroring the split L1 cache design:
ITLB (Instruction TLB): 64–128 entries. Smaller because instruction access patterns are sequential and predictable.
DTLB (Data TLB): 64–256 entries. Larger because data access patterns are more varied.
L2 STLB (unified): 512–2,048 entries. Backs both ITLB and DTLB misses, just as L2 cache backs L1i and L1d.
For example, an Intel Coffee Lake processor:On every memory access — instruction fetch, data load, or store — the MMU consults the TLB first.<br><img alt="TLB lookup flowchart: TLB hit fast path vs TLB miss slow path with page table walk" src="assets/l12-tlb-lookup-flow-eng.svg" target="_self" style="width: 650px; max-width: 100%;">Step 1: Extract the VPN. The CPU splits the virtual address into the VPN (upper 36 bits for 4 KB pages) and the page offset (lower 12 bits).Step 2: Search the TLB. The hardware simultaneously compares the VPN against every valid entry (fully associative parallel search), also checking that the ASID matches the current process.Step 3a: TLB Hit (fast path). Matching entry found → PFN extracted → permission bits checked against access type (R/W/X) and privilege level (user/kernel) → physical address constructed by concatenating PFN with page offset. The lookup takes 1–2 cycles and is pipelined, adding essentially zero additional latency.Step 3b: TLB Miss (slow path). No match → the hardware page table walker reads CR3 and traverses all four levels. If the walk succeeds (page present at every level), the translation is installed in the TLB and the access is retried — this time hitting.Step 3c: Page Fault. If any level's Present bit = 0, the hardware raises a page fault exception (interrupt 14). The OS handler loads the page from disk, allocates a zero page, triggers copy-on-write, or terminates the process — then restarts the faulting instruction.On x86, the page walk is hardware-managed — fully transparent to the OS, which only gets involved on a page fault. Some architectures (MIPS, some RISC-V) use a software-managed TLB: a miss raises an exception, and the OS walks the page table in software. Hardware-managed is faster (no exception overhead); software-managed is more flexible (OS can use any page table format).In L11, we introduced the AMAT formula for cache hierarchies. The TLB fits this framework — it is, after all, a cache. We define an Effective Memory Access Time (EMAT):EMAT = h × (T_tlb + T_mem) + (1 - h) × (T_tlb + T_walk + T_mem)
Where:
h = TLB hit rate
T_tlb = TLB lookup time (~1 ns, pipelined to near-zero on a hit)
T_mem = main memory access time (~100 ns, ignoring data caches for clarity)
T_walk = page table walk cost = number of levels × T_mem
This simplifies to:EMAT = T_tlb + T_mem + (1 - h) × T_walk
The last term is the paging overhead — it shrinks toward zero as the hit rate approaches 100%.Worked example (h = 99%):T_tlb = 1 ns
T_mem = 100 ns
T_walk = 4 × 100 = 400 ns EMAT = 1 + 100 + (1 - 0.99) × 400 = 1 + 100 + 0.01 × 400 = 101 + 4 = 105 ns
With a 99% TLB hit rate, paging adds only 5% overhead — acceptable for the enormous benefits of virtual memory. Without the TLB, the overhead is 400%.Why 99% is realistic. Programs exhibit strong locality:
Temporal locality: a page accessed once will likely be accessed again soon. One TLB entry covers an entire 4 KB page — hundreds of sequential instructions or dozens of data accesses.
Spatial locality: nearby accesses fall on the same page. An array traversal touching 1,000 consecutive integers spans just one page (4 KB / 4 bytes = 1,024 ints per page).
Working set: a typical program actively uses only a handful of pages at any moment. Even a 64-entry DTLB covers a 256 KB working set.
Real-world workloads consistently show TLB hit rates of 99–99.9%. The 1% miss rate comes mainly from traversals over large data structures and function calls to distant code pages.When performance degrades. Random access over large memory (e.g., multi-GB hash table lookups where each access hits a different page), frequent context switches without ASID support, and large code footprints can all push hit rates well below 99%, making the page walk cost a real bottleneck — and motivating huge pages (Section 7.6).When the OS switches from Process A to Process B, it writes Process B's PML4 address into CR3. But Process A's translations remain in the TLB — using them for Process B would be a security and correctness violation, since Process B's virtual page 0x1000 maps to a different physical frame than Process A's.Solution 1: Flush on context switch. Early x86 processors invalidated all TLB entries when CR3 changed. Correct, but expensive: a cold TLB means every access misses until the TLB warms up. If the first millisecond touches 200 pages, the cold-start penalty is 200 × 400 ns = 80 μs — 8% of a typical time slice. On a system switching thousands of times per second, this adds up.Solution 2: ASID tags (PCID on x86-64). Each TLB entry is tagged with a 12-bit Process Context Identifier (PCID). On a TLB lookup, the hardware matches both VPN and PCID — entries from different processes coexist safely, and no flush is needed on context switch. When the OS switches back to Process A, its TLB entries may still be present, avoiding the cold-start penalty entirely. With 4,096 possible PCIDs, recycling is rare; when the OS does recycle a PCID, it must flush entries with that PCID first.Global pages. Kernel mappings are identical across all processes. The Global (G) bit in the PTE marks their TLB entries as immune to CR3 writes and PCID changes, avoiding unnecessary re-translation of kernel code and data.Note: the Meltdown vulnerability (2018) forced Kernel Page Table Isolation (KPTI), which unmaps most kernel pages from user-space page tables, reducing the usefulness of global pages. See Section 10 for details.The TLB's effectiveness depends on TLB reach — the total virtual memory it can cover:TLB Reach = Number of TLB Entries × Page Size
With 4 KB pages, a 64-entry DTLB covers only 256 KB. A database with a 64 GB buffer pool would need millions of entries to avoid misses. With 2 MB pages, the same TLB covers 128 MB (512× improvement). Fewer walk levels also reduce the miss penalty: 300 ns for 2 MB pages vs. 400 ns for 4 KB.Huge pages are used extensively in databases (PostgreSQL, Oracle), virtual machines (KVM, VMware), and HPC/ML workloads. On Linux, huge pages are available via hugetlbfs (explicit, reserved at boot) or Transparent Huge Pages (THP), where the kernel automatically promotes contiguous 4 KB regions into 2 MB huge pages without application changes.Connection to Lab P12. In the lab, you will measure TLB miss rates with perf stat (dTLB-load-misses, iTLB-load-misses) and compare latency between 4 KB and 2 MB pages for sequential and random access patterns.In earlier sections, we traced the happy path of virtual address translation — the TLB hits, the page table walk succeeds, the physical frame number is found. But what happens when the target page is not present in physical memory?This is the territory of the page fault — the event that bridges virtual memory's promise of unlimited address space with the reality of finite physical RAM. Page faults are not errors in the normal sense; most are an essential part of the system's design.Recall the PTE's Present bit (P) from Section 4. When the MMU translates a virtual address and finds Present = 0, the page is not in RAM and the translation cannot complete. The hardware responds with a precise sequence:
The MMU halts the current instruction mid-execution.
The CPU raises exception #14 (the x86 page fault exception) — a synchronous, precise exception.
The faulting virtual address is saved in the CR2 register so the OS knows which address caused the fault.
The CPU switches to kernel mode and jumps to the page fault handler registered in the IDT.
The OS fault handler examines the faulting address and the process's memory map to classify the fault into one of three categories.Minor page fault (soft fault). The page is already in physical memory but not mapped in this process's page table — for instance, a shared library loaded by another process, or a page still in the kernel's page cache. Resolution is cheap: the OS updates the PTE to point to the existing frame and sets Present = 1. No disk I/O is needed; cost is a few microseconds.Major page fault (hard fault). The page is not in physical memory at all and must be read from disk — either from swap or from the executable file. An SSD delivers a 4 KB random read in ~100 μs; a spinning HDD takes 8–10 ms, which at 4 GHz corresponds to roughly 32 million clock cycles. The faulting process is blocked, and the OS context-switches to another process while waiting for I/O. Minimizing major faults is the entire purpose of page replacement algorithms (Section 9).Invalid fault. The faulting address does not correspond to any valid region in the process's virtual address space — a null pointer dereference, an out-of-bounds array access, or an attempt to execute data as code. The OS delivers SIGSEGV (segmentation fault) on Linux or "Access Violation" on Windows, typically terminating the process with a core dump.When the page fault handler gains control, it executes the following sequence. Here is the complete flow for a major fault — the most complex case:Step 1: Save state and read CR2. The kernel saves all CPU registers and the program counter, then extracts the faulting virtual address from CR2 to compute the virtual page number.Step 2: Validate the address. The handler consults the process's memory map (exposed on Linux via /proc/&lt;pid&gt;/maps). If the address falls outside all valid regions, or violates a region's permissions (e.g., writing to read-only code), the handler delivers SIGSEGV and the process is killed.Step 3: Find a free physical frame. The OS takes a frame from the free frame list. If the list is empty or below a low-water threshold, the page replacement algorithm (Section 9) selects a victim page for eviction.Step 4: Evict if necessary. If the selected victim's Dirty bit is set (page was modified since loading), the OS writes it back to disk before reusing the frame. This writeback adds an extra disk I/O to the fault cost.Step 5: Load the required page. The OS issues a disk read to load the 4 KB page into the selected frame via DMA. During this transfer the faulting process is blocked and another process is scheduled on the CPU.Step 6: Update PTE and restart. Once I/O completes, the OS writes the new frame number into the PTE, sets Present = 1, clears the Dirty bit, sets permission bits, and invalidates any stale TLB entry (via INVLPG on x86). The saved registers are restored and the faulting instruction is re-executed — this time completing normally. From the program's perspective, the instruction simply took a very long time.<br><img alt="Page fault handling flowchart: exception, CR2 read, validation, frame allocation, disk I/O, PTE update, instruction restart" src="assets/l12-page-fault-handling-eng.svg" target="_self" style="width: 650px; max-width: 100%;">Steps 4 and 5 explain why major faults are devastating: up to two disk operations if the victim is dirty. On an HDD, that can cost ~16 ms — roughly 64 million cycles at 4 GHz.Modern operating systems deliberately start a process with zero pages in physical memory. The OS creates the virtual address space (code, data, heap, stack regions) but every PTE starts with Present = 0. The first instruction triggers a page fault, loading that code page; the next data access triggers another, and so on.This is demand paging: pages are loaded only when actually accessed. Benefits:
Faster startup — a large executable may contain hundreds of megabytes, but a typical execution touches only a fraction. Loading everything upfront wastes time on pages that may never be used.
Lower memory pressure — pages never accessed never consume physical frames, allowing dozens of processes with multi-gigabyte virtual address spaces to coexist in limited RAM.
Most OSes optimize with pre-paging (or clustered paging): when faulting on page N, the kernel also loads pages N+1, N+2, etc., exploiting spatial locality to prevent future faults.Copy-on-write makes fork() dramatically more efficient. Instead of duplicating the parent's entire physical memory, the OS maps both parent and child to the same physical frames and marks all shared pages read-only in both PTEs. When either process writes to a shared page, the write triggers a page fault. The handler recognizes this as a COW fault, allocates a new frame, copies the page contents, and gives the writing process its own private copy with full read-write permissions.The key insight: if the child calls exec() immediately after fork() — the overwhelmingly common pattern — no pages are ever copied at all. Even otherwise, only the specific pages that are written get duplicated; the vast majority (code, read-only data, unmodified heap) remain shared.A related optimization is zero-fill-on-demand: newly allocated heap/stack pages map to a single shared zero page (read-only). Only when the process writes to such a page does the OS allocate a private frame — ensuring new memory reads as zero (a security requirement) without eagerly allocating frames that may never be used.The Effective Access Time (EAT) quantifies the cost of page faults:EAT = (1 - p) × memory_access_time + p × page_fault_service_time
where p is the page fault probability, memory_access_time ≈ 200 ns, and page_fault_service_time ≈ 8 ms (HDD).Even one fault per thousand accesses causes a 41× slowdown. To keep degradation under 10%, the fault rate must be below roughly one per million accesses. This is why operating systems invest heavily in page replacement algorithms — they determine whether the system runs at memory speed or disk speed.With SSDs (~100 μs fault service time), a fault rate of 1 in 1,000 yields ~300 ns EAT (1.5× slowdown) — far more tolerable, but the fundamental lesson remains: page faults must be rare for paging to work well.When the free frame list is empty and a new page must be loaded, the OS must decide which page to evict. This is the page replacement problem, and the algorithm used directly determines how many future page faults occur. A poor choice — evicting a page needed on the very next access — triggers another immediate fault; a good choice avoids faults and keeps the system running at memory speed.The system has N physical frames. The process generates a sequence of virtual page references — the reference string. References to pages already in frames are hits; references to absent pages are faults requiring eviction if all N frames are occupied. If the evicted page is dirty (modified), it must be written back to disk before the frame can be reused, roughly doubling the fault cost.Our worked example. 3 physical frames (initially empty), 20 page references:Reference string: 7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2, 1, 2, 0, 1, 7, 0, 1
Evict the page that has been in memory the longest. Implementation: a simple FIFO queue.Trace (3 frames):Ref: 7 0 1 2 0 3 0 4 2 3 0 3 2 1 2 0 1 7 0 1
F1: [7] [7] [7] [2] [2] [2] [2] [4] [4] [4] [0] [0] [0] [0] [0] [0] [0] [7] [7] [7]
F2: - [0] [0] [0] [0] [3] [3] [3] [2] [2] [2] [2] [2] [1] [1] [1] [1] [1] [0] [0]
F3: - - [1] [1] [1] [1] [0] [0] [0] [3] [3] [3] [3] [3] [2] [2] [2] [2] [2] [1]
Hit? F H F F F F F F H H F F H H F F F
FIFO result: 15 page faults out of 20 references.FIFO is trivial to implement but ignores access patterns entirely — a heavily used page is evicted when it reaches the queue head regardless.Belady's Anomaly. FIFO has a counterintuitive property: increasing the number of frames can sometimes increase faults. For the string 1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5, FIFO with 3 frames gives 9 faults, but with 4 frames gives 10. This occurs because FIFO is not a stack algorithm — the set of pages in N frames is not necessarily a subset of those in N+1 frames. LRU and OPT are stack algorithms and provably immune to this anomaly.Evict the page that will not be used for the longest time in the future. Proven by Bélády to produce the minimum possible faults.Example eviction decision from our trace (3 frames):When page 2 arrives (reference 4) and frames hold {7, 0, 1}: page 7's next use is at position 18 (far away), page 0's next use is at position 5, page 1's next use is at position 14. OPT evicts page 7 — the one not needed for the longest time.OPT result: 9 page faults — a dramatic improvement over FIFO's 15.OPT is impossible to implement in a real system because it requires knowledge of future accesses. It serves as a theoretical benchmark for evaluating practical algorithms.Since we cannot see the future, we use the recent past as a predictor: evict the page that has not been accessed for the longest time. The rationale is temporal locality — a page unused recently is unlikely to be needed soon.Key eviction decisions from our trace (3 frames):
Reference 2 (position 4): frames hold {7, 0, 1}. Last used — 7 at time 1, 0 at time 2, 1 at time 3. LRU evicts page 7 (least recently used).
Reference 3 (position 6): frames hold {2, 0, 1}. Last used — 1 at time 3, 0 at time 5, 2 at time 4. LRU evicts page 1.
LRU result: 12 page faults — better than FIFO (15), though not as good as OPT (9).LRU is a stack algorithm (immune to Belady's anomaly), but implementing true LRU is impractical: it requires updating a data structure on every memory access (billions per second), not just on faults. Two classic approaches — counter-based (write a timestamp to each PTE on every access) and stack-based (maintain a doubly-linked list reordered on every access) — both need hardware support that modern processors do not provide. In practice, operating systems use LRU approximations.The Clock algorithm is the most widely used LRU approximation. Linux, FreeBSD, and Windows all use variants of it. It requires only one bit of hardware support per page — the Accessed bit (R bit) that the MMU sets automatically whenever a page is accessed.How it works. All N frames are arranged in a circular buffer with a single clock hand pointer. When eviction is needed:
Examine the page at the clock hand.
If Accessed = 0: evict this page, load the new page, advance the hand.
If Accessed = 1: clear the bit to 0 (give a "second chance"), advance the hand, repeat from step 1.
The hand circles around, clearing Accessed bits as it goes, until it finds a page with Accessed = 0. Pages accessed since the last sweep survive; those untouched for a full rotation are evicted — approximating LRU where the "recency window" is one rotation.<br><img alt="Clock (Second-Chance) page replacement algorithm with circular frame buffer and clock hand" src="assets/l12-clock-algorithm-eng.svg" target="_self" style="width: 650px; max-width: 100%;">Two-hand variant. Some OSes use two clock hands: a clearing hand (front) that resets Accessed bits, and an eviction hand (back) that evicts pages with Accessed = 0. The distance between them defines the recency window — tunable by the OS based on memory pressure.Enhanced Second-Chance. The enhanced version also considers the Dirty bit, creating four eviction-priority classes:The algorithm sweeps for class 0 first; if none found after a full rotation, it looks for class 1, then 2, then 3 — clearing Accessed bits along the way.Fault counts for our reference string (3 frames, 20 references):OPT sets the theoretical floor, LRU comes close, and FIFO pays a significant penalty for ignoring access recency. In practice, Clock's fault count falls between LRU and FIFO — typically much closer to LRU. Research shows the two-hand Clock variant often matches or slightly outperforms true LRU, because periodic clearing of Accessed bits provides a form of "forgetting" that helps with shifting access patterns.Connection to L11. These are the same replacement algorithms from L11's cache discussion, applied at a different hierarchy level. The key difference: cache replacement is done in hardware in nanoseconds, while page replacement is done in OS software with microseconds available — enabling more sophisticated approximations like the enhanced two-hand clock.Virtual memory is also the primary enforcement mechanism for memory protection — the hardware-level guarantee that one process cannot read, write, or execute another process's memory. Protection is enforced per page: every PTE contains permission bits that the MMU checks on every memory access. A violation — a write to a read-only page, a user-mode read of a kernel page, an instruction fetch from a non-executable page — triggers a page fault before the access completes. This is a hardware wall, not a software convention.On x86-64, the three most important permission bits are:Bit 1 — Read/Write (R/W). When 0, the page is read-only; when 1, read-write. Any store to a read-only page triggers a protection violation fault. This is what makes the standard memory layout enforceable:If a bug or exploit tries to overwrite .text instructions, the MMU blocks the write and the process receives SIGSEGV. Self-modifying code is impossible unless the program explicitly requests writable-and-executable memory — which modern systems refuse by default.Bit 2 — User/Supervisor (U/S). When 1, the page is accessible from user mode (Ring 3); when 0, only kernel mode (Ring 0) can access it. The kernel is mapped into the upper portion of every process's virtual address space so that syscalls and interrupts do not require a page table switch — but U/S = 0 on every kernel page blocks user-mode access. Without this bit, any program could read the kernel's encryption keys, other processes' page tables, and kernel code.Together, R/W and U/S create a 2-bit protection matrix:The kernel always has at least as much access as user mode — the hardware trusts the kernel completely.<br><img alt="x86-64 page table entry 64-bit layout with permission, status, and NX bits labeled" src="assets/l12-page-table-entry-eng.svg" target="_self" style="width: 650px; max-width: 100%;">For decades, x86 page tables could not distinguish "readable" from "executable" — if the CPU could read a page, it could execute instructions from it. This enabled the classic buffer overflow exploit: an attacker overflows a stack buffer with crafted machine code (shellcode), overwrites the return address to point into the buffer, and the CPU executes the injected code when the function returns. The attack works because the stack is both writable (for local variables) and executable (no way to prevent instruction fetches).Bit 63 — NX (No Execute), also called XD (Execute Disable) by Intel and XN by ARM. When set, the CPU cannot fetch instructions from that page — any attempt triggers a page fault. With NX, the OS marks stack and heap pages non-executable. An attacker can still write shellcode to the stack, but execution fails because the MMU blocks the instruction fetch.This enables the W^X (Write XOR Execute) policy — no page is simultaneously writable and executable:
.text pages: executable, read-only (NX = 0, R/W = 0)
.data, .bss, heap, stack pages: writable, non-executable (NX = 1, R/W = 1)
An attacker who can write to memory cannot execute what they wrote; an attacker who can redirect execution can only jump to existing code, not injected code. W^X is now the default on all major operating systems.The NX bit blocks shellcode injection, but sophisticated attackers can use Return-Oriented Programming (ROP): chaining short sequences of existing code ("gadgets") in libraries like libc, each ending with ret, to perform arbitrary computation without injecting any new code. For ROP to work, the attacker must know the exact virtual addresses of these gadgets.ASLR defeats this by randomizing the base addresses of key memory regions every time a process starts — the executable itself (PIE), the stack, the heap, and each shared library. Without ASLR, the same program always loads libc at the same address. With ASLR, every execution shuffles the layout, so hard-coded gadget addresses crash on the next run.On modern 64-bit Linux, ASLR provides significant entropy:With 28 bits of entropy, an attacker has a 1-in-268-million chance per guess, and each wrong guess crashes the program (triggering a page fault at an unmapped address), restarting the randomization.Students will observe ASLR directly in Lab P12 by running the same program multiple times and examining /proc/[pid]/maps — each run shows different base addresses for the stack, heap, and shared libraries.The U/S bit was considered ironclad for decades — until the Meltdown vulnerability (2018, CVE-2017-5754) showed that speculative execution on most Intel CPUs could leak kernel data through cache side-channels, bypassing U/S enforcement. The software mitigation, KPTI, maintains two sets of page tables per process: user-mode tables contain only user pages and a tiny kernel stub for handling syscalls, while the full kernel mapping exists only in kernel-mode tables loaded in Ring 0. With PCID (Process Context Identifier) support to avoid full TLB flushes on CR3 switches, the overhead is typically 1–5%.We have studied every virtual memory component individually. Now we trace a single memory access from start to finish — connecting L11 (cache hierarchy) with L12 (virtual memory) to show how both systems operate simultaneously on every access.A running program executes:mov rax, [0x7FFE12345678]
The CPU must read 8 bytes at virtual address 0x7FFE'1234'5678 (a typical Linux stack address with ASLR) into register rax. Let us trace every step.The load/store unit receives the 48-bit canonical virtual address and decomposes it into four 9-bit indices plus a 12-bit offset:Virtual address: 0x7FFE'1234'5678 Decomposition:
┌───────────┬───────────┬───────────┬───────────┬──────────────┐
│ PML4 idx │ PDPT idx │ PD idx │ PT idx │ Offset │
│ 9 bits │ 9 bits │ 9 bits │ 9 bits │ 12 bits │
├───────────┼───────────┼───────────┼───────────┼──────────────┤
│ 0xFF (255)│ 0x1F8(504)│ 0x091(145)│ 0x1A5(421)│ 0x678 (1656) │
└───────────┴───────────┴───────────┴───────────┴──────────────┘
The offset passes through unchanged; the four indices navigate the page table hierarchy.The MMU extracts the Virtual Page Number (VPN = 0x7FFE12345, the upper 36 bits) and searches the TLB.TLB Hit (~99% of accesses): The TLB returns Physical Frame Number (PFN), say 0x1A3B7C. The physical address is PFN + offset = 0x1A3B7C678. Translation cost: ~1 cycle. Skip to Step 6.TLB Miss (~1%): The VPN is not cached. The MMU must perform a full four-level page table walk.The hardware page table walker reads CR3 (which holds the PML4 physical address) and traverses four levels, using one 9-bit index at each level:
PML4: Read entry 255 from the PML4 table → get PDPT physical address
PDPT: Read entry 504 → get PD physical address
PD: Read entry 145 → get PT physical address
PT: Read entry 421 → get the final PTE with the Physical Frame Number
Each read is a memory access that goes through the cache hierarchy. Upper-level entries (PML4, PDPT) are heavily reused and almost always cached. In the best case the walk takes ~4–28 ns; in the worst case (all four misses to DRAM), ~400 ns.The walker reads the final PTE and checks the Present bit: Present = 0 — Page Fault. The page is not in physical memory. The OS fault handler determines the cause: a major fault requires reading the page from disk (SSD ~150 μs, HDD ~10 ms); a minor fault (demand-paged or copy-on-write page) updates the PTE without disk I/O (~1–10 μs). After handling, the instruction is re-executed. Present = 1 — Permission Check. The MMU verifies U/S, R/W, and NX bits against the access type. Our example is a user-mode data read — U/S must be 1 (it is, for stack pages), and R/W and NX are not checked for reads. Any violation would raise a protection fault (SIGSEGV). The MMU stores the new VPN → PFN mapping in the TLB (evicting an old entry if full via pseudo-LRU) and sets the Accessed bit in the PTE. It then constructs the physical address:Physical address = PFN || Offset = 0x1A3B7C || 0x678 = 0x1A3B7C678
The physical address enters the cache hierarchy from L11:
L1D hit (~1 ns): Data forwarded to the CPU. Total time with TLB hit: ~2 ns.
L1 miss → L2 hit (~7 ns): Line loaded into L1, data forwarded.
L2 miss → L3 hit (~30–40 ns): Line loaded into L2 and L1.
L3 miss → DRAM (~100 ns): Memory controller fetches a 64-byte cache line from DRAM, which propagates through L3 → L2 → L1 → CPU.
The 8 bytes at physical address 0x1A3B7C678 arrive in register rax. The mov instruction completes and the pipeline advances.The same "read 8 bytes" operation spans a 5,000,000:1 range:This enormous range is why the entire optimization stack exists: the TLB eliminates page walks for 99%+ of accesses, multi-level tables keep walks to 4 reads, the cache hierarchy turns 100 ns DRAM accesses into 1–7 ns hits, and replacement algorithms minimize disk I/O.<br><img alt="Complete memory access path: virtual address through TLB, page table walk, page fault handler, physical address, and cache hierarchy" src="assets/l12-full-memory-access-path-eng.svg" target="_self" style="width: 650px; max-width: 100%;">This full path — virtual address to physical data — connects three domains: hardware (MMU, TLB, caches) provides the mechanisms for fast translation and caching on every access; the operating system provides the policies — page table mappings, fault handling, replacement algorithms, protection bits; and application behavior determines how well these optimizations work. A program with good locality enjoys high TLB and cache hit rates; one that jumps randomly across a large address space thrashes both, paying worst-case penalties millions of times more often.This lecture covered the machinery that makes virtual memory work — from the basic concept of paging to the multi-level page tables, TLB acceleration, fault handling, and memory protection that modern processors use to give every process its own safe, efficient view of memory. Virtual memory provides three things: process isolation (one process cannot access another's memory), address space abstraction (every process sees a uniform virtual layout), and memory efficiency (only active pages consume physical RAM). Paging divides memory into fixed-size chunks: virtual pages map to physical frames through page tables. The page offset passes through translation unchanged — only the page number is replaced by a frame number. Multi-level page tables make sparse address spaces practical: x86-64 uses a four-level hierarchy (PML4 → PDPT → PD → PT) with 9-bit indices at each level. Only tables for actually-used virtual address ranges are allocated, saving enormous amounts of memory compared to a flat page table. The TLB makes paging fast: without the TLB, every memory access would require four extra memory accesses for the page table walk. With a 99%+ hit rate, the TLB reduces the average overhead to near zero, making virtual memory practical. Page faults are the mechanism for demand paging: minor faults are fast (no disk I/O), major faults are catastrophically slow (~8 ms). Keeping page fault rates astronomically low is essential — even one fault per thousand accesses causes a 40× slowdown. Page replacement algorithms determine eviction policy: FIFO is simple but suffers Belady's anomaly. LRU is theoretically good but expensive. The Clock algorithm provides a practical, hardware-friendly approximation of LRU that real operating systems actually use. Memory protection is enforced at the page level: R/W, U/S, and NX bits in every PTE control what each page can do. The NX bit prevents code execution from data pages, blocking a major class of security exploits. ASLR randomizes memory layout to make attacks harder. The complete memory access path spans TLB → page tables → caches → RAM: understanding this unified path — connecting L11's cache hierarchy with L12's virtual memory — is one of the most important mental models in computer systems. We have now covered how computers store and access integers, addresses, and instructions — and how the memory system translates, caches, and protects all of these. But there is one crucial data type we have not yet examined: real numbers.How does a computer represent 3.14159? What happens when you compute 0.1 + 0.2 and get 0.30000000000000004? Why do financial applications sometimes avoid floating-point altogether? Lecture 13 covers the IEEE 754 floating-point standard — the universal format that every modern processor uses to represent decimals. You will learn how sign, exponent, and mantissa bits encode real numbers, why precision is limited, and what the implications are for scientific computing and everyday programming.Lab P12: Virtual Memory in LinuxIn this lab, you will observe virtual memory in action on a real Linux system using the tools and concepts from this lecture.What you'll do:
Examine a process's virtual address space via /proc/[pid]/maps — identify code, data, heap, stack, and shared library segments
Use pmap and vmstat to inspect memory usage and virtual memory statistics
Observe ASLR in action by running the same program multiple times and comparing addresses
Measure page faults with perf stat -e page-faults — distinguish minor and major faults
Experiment with huge pages (2 MB) and observe TLB effects
Compare page fault rates for sequential vs random memory access patterns — see how locality affects virtual memory performance
Key connection to lecture: You will see that the concepts from today — page tables, TLB, page faults, ASLR — are not abstractions but observable, measurable phenomena on real hardware.
<br><a data-tooltip-position="top" aria-label="https://diveintosystems.org/book/C13-OS/vm.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://diveintosystems.org/book/C13-OS/vm.html" target="_self">Dive Into Systems — Chapter 13: Virtual Memory</a> — Comprehensive introduction to virtual memory concepts and paging
<br><a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/computer-organization-architecture/page-tables-and-single-level-paging/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/computer-organization-architecture/page-tables-and-single-level-paging/" target="_self">GeeksforGeeks — Page Tables and Single-Level Paging</a> — Page table structure and PTE components
<br><a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/computer-organization-architecture/translation-lookaside-buffer-tlb/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/computer-organization-architecture/translation-lookaside-buffer-tlb/" target="_self">GeeksforGeeks — Translation Lookaside Buffer (TLB)</a> — TLB architecture and EMAT formula
<br><a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/operating-systems/page-fault-handling-in-operating-system/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/operating-systems/page-fault-handling-in-operating-system/" target="_self">GeeksforGeeks — Page Fault Handling</a> — Page fault types and handling steps
<br><a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/operating-systems/page-replacement-algorithms-in-operating-systems/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/operating-systems/page-replacement-algorithms-in-operating-systems/" target="_self">GeeksforGeeks — Page Replacement Algorithms</a> — FIFO, Optimal, LRU comparison
<br><a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/operating-systems/multilevel-paging-in-operating-system/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/operating-systems/multilevel-paging-in-operating-system/" target="_self">GeeksforGeeks — Multilevel Paging</a> — Multi-level page table motivation and structure
<br><a data-tooltip-position="top" aria-label="https://cs61.seas.harvard.edu/site/2018/Section4/" rel="noopener nofollow" class="external-link is-unresolved" href="https://cs61.seas.harvard.edu/site/2018/Section4/" target="_self">Harvard CS61 — x86-64 Page Tables</a> — Four-level page table walk and address format
<br><a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/operating-systems/second-chance-or-clock-page-replacement-policy/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/operating-systems/second-chance-or-clock-page-replacement-policy/" target="_self">GeeksforGeeks — Second Chance / Clock Algorithm</a> — Clock page replacement with reference bit
<br><a data-tooltip-position="top" aria-label="https://cs.brown.edu/courses/csci0300/2022/notes/l16.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://cs.brown.edu/courses/csci0300/2022/notes/l16.html" target="_self">Brown CS0300 — Page Tables and Address Translation</a> — Multi-level page tables, permission bits, NX bit
<br><a data-tooltip-position="top" aria-label="https://blog.zolutal.io/understanding-paging/" rel="noopener nofollow" class="external-link is-unresolved" href="https://blog.zolutal.io/understanding-paging/" target="_self">Zolutal Blog — Understanding x86-64 Paging</a> — Detailed PTE format, huge pages, page walk mechanics
<br><a data-tooltip-position="top" aria-label="https://www.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/9_VirtualMemory.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/9_VirtualMemory.html" target="_self">UIC — Virtual Memory (Silberschatz)</a> — Demand paging, copy-on-write, all replacement algorithms, thrashing
<br><a data-tooltip-position="top" aria-label="https://www.cs.cornell.edu/courses/cs4410/2018su/lectures/lec15-thrashing.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.cs.cornell.edu/courses/cs4410/2018su/lectures/lec15-thrashing.html" target="_self">Cornell CS4410 — Page Replacement and Thrashing</a> — Two-hand clock algorithm, working set model
]]></description><link>lectures/eng/l12-eng.-virtual-memory-in-depth.html</link><guid isPermaLink="false">Lectures/ENG/L12-ENG. Virtual Memory In-Depth.md</guid><pubDate>Mon, 30 Mar 2026 21:33:24 GMT</pubDate><enclosure url="assets/l12-virtual-address-space-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l12-virtual-address-space-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L12-Page-Table-Entry]]></title><description><![CDATA[<img src="assets/l12-page-table-entry.svg" target="_self">]]></description><link>assets/l12-page-table-entry.html</link><guid isPermaLink="false">assets/L12-Page-Table-Entry.svg</guid><pubDate>Mon, 30 Mar 2026 20:58:01 GMT</pubDate><enclosure url="assets/l12-page-table-entry.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l12-page-table-entry.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L12-Page-Table-Entry-ENG]]></title><description><![CDATA[<img src="assets/l12-page-table-entry-eng.svg" target="_self">]]></description><link>assets/l12-page-table-entry-eng.html</link><guid isPermaLink="false">assets/L12-Page-Table-Entry-ENG.svg</guid><pubDate>Mon, 30 Mar 2026 20:57:27 GMT</pubDate><enclosure url="assets/l12-page-table-entry-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l12-page-table-entry-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L12-TLB-Lookup-Flow-ENG]]></title><description><![CDATA[<img src="assets/l12-tlb-lookup-flow-eng.svg" target="_self">]]></description><link>assets/l12-tlb-lookup-flow-eng.html</link><guid isPermaLink="false">assets/L12-TLB-Lookup-Flow-ENG.svg</guid><pubDate>Mon, 30 Mar 2026 20:52:09 GMT</pubDate><enclosure url="assets/l12-tlb-lookup-flow-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l12-tlb-lookup-flow-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L12-TLB-Lookup-Flow]]></title><description><![CDATA[<img src="assets/l12-tlb-lookup-flow.svg" target="_self">]]></description><link>assets/l12-tlb-lookup-flow.html</link><guid isPermaLink="false">assets/L12-TLB-Lookup-Flow.svg</guid><pubDate>Mon, 30 Mar 2026 20:52:06 GMT</pubDate><enclosure url="assets/l12-tlb-lookup-flow.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l12-tlb-lookup-flow.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L12-Full-Memory-Access-Path]]></title><description><![CDATA[<img src="assets/l12-full-memory-access-path.svg" target="_self">]]></description><link>assets/l12-full-memory-access-path.html</link><guid isPermaLink="false">assets/L12-Full-Memory-Access-Path.svg</guid><pubDate>Mon, 30 Mar 2026 20:51:08 GMT</pubDate><enclosure url="assets/l12-full-memory-access-path.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l12-full-memory-access-path.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L12-Full-Memory-Access-Path-ENG]]></title><description><![CDATA[<img src="assets/l12-full-memory-access-path-eng.svg" target="_self">]]></description><link>assets/l12-full-memory-access-path-eng.html</link><guid isPermaLink="false">assets/L12-Full-Memory-Access-Path-ENG.svg</guid><pubDate>Mon, 30 Mar 2026 20:50:23 GMT</pubDate><enclosure url="assets/l12-full-memory-access-path-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l12-full-memory-access-path-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L12-Four-Level-Page-Table]]></title><description><![CDATA[<img src="assets/l12-four-level-page-table.svg" target="_self">]]></description><link>assets/l12-four-level-page-table.html</link><guid isPermaLink="false">assets/L12-Four-Level-Page-Table.svg</guid><pubDate>Mon, 30 Mar 2026 20:50:15 GMT</pubDate><enclosure url="assets/l12-four-level-page-table.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l12-four-level-page-table.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L12-Address-Translation]]></title><description><![CDATA[<img src="assets/l12-address-translation.svg" target="_self">]]></description><link>assets/l12-address-translation.html</link><guid isPermaLink="false">assets/L12-Address-Translation.svg</guid><pubDate>Mon, 30 Mar 2026 20:49:45 GMT</pubDate><enclosure url="assets/l12-address-translation.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l12-address-translation.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L12-Clock-Algorithm]]></title><description><![CDATA[<img src="assets/l12-clock-algorithm.svg" target="_self">]]></description><link>assets/l12-clock-algorithm.html</link><guid isPermaLink="false">assets/L12-Clock-Algorithm.svg</guid><pubDate>Mon, 30 Mar 2026 20:49:37 GMT</pubDate><enclosure url="assets/l12-clock-algorithm.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l12-clock-algorithm.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L12-Four-Level-Page-Table-ENG]]></title><description><![CDATA[<img src="assets/l12-four-level-page-table-eng.svg" target="_self">]]></description><link>assets/l12-four-level-page-table-eng.html</link><guid isPermaLink="false">assets/L12-Four-Level-Page-Table-ENG.svg</guid><pubDate>Mon, 30 Mar 2026 20:49:30 GMT</pubDate><enclosure url="assets/l12-four-level-page-table-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l12-four-level-page-table-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L12-Clock-Algorithm-ENG]]></title><description><![CDATA[<img src="assets/l12-clock-algorithm-eng.svg" target="_self">]]></description><link>assets/l12-clock-algorithm-eng.html</link><guid isPermaLink="false">assets/L12-Clock-Algorithm-ENG.svg</guid><pubDate>Mon, 30 Mar 2026 20:49:21 GMT</pubDate><enclosure url="assets/l12-clock-algorithm-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l12-clock-algorithm-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L12-Address-Translation-ENG]]></title><description><![CDATA[<img src="assets/l12-address-translation-eng.svg" target="_self">]]></description><link>assets/l12-address-translation-eng.html</link><guid isPermaLink="false">assets/L12-Address-Translation-ENG.svg</guid><pubDate>Mon, 30 Mar 2026 20:49:19 GMT</pubDate><enclosure url="assets/l12-address-translation-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l12-address-translation-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L12-Page-Fault-Handling]]></title><description><![CDATA[<img src="assets/l12-page-fault-handling.svg" target="_self">]]></description><link>assets/l12-page-fault-handling.html</link><guid isPermaLink="false">assets/L12-Page-Fault-Handling.svg</guid><pubDate>Mon, 30 Mar 2026 20:48:55 GMT</pubDate><enclosure url="assets/l12-page-fault-handling.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l12-page-fault-handling.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L12-Virtual-Physical-Mapping]]></title><description><![CDATA[<img src="assets/l12-virtual-physical-mapping.svg" target="_self">]]></description><link>assets/l12-virtual-physical-mapping.html</link><guid isPermaLink="false">assets/L12-Virtual-Physical-Mapping.svg</guid><pubDate>Mon, 30 Mar 2026 20:48:50 GMT</pubDate><enclosure url="assets/l12-virtual-physical-mapping.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l12-virtual-physical-mapping.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L12-Two-Level-Page-Table]]></title><description><![CDATA[<img src="assets/l12-two-level-page-table.svg" target="_self">]]></description><link>assets/l12-two-level-page-table.html</link><guid isPermaLink="false">assets/L12-Two-Level-Page-Table.svg</guid><pubDate>Mon, 30 Mar 2026 20:48:48 GMT</pubDate><enclosure url="assets/l12-two-level-page-table.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l12-two-level-page-table.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L12-Page-Fault-Handling-ENG]]></title><description><![CDATA[<img src="assets/l12-page-fault-handling-eng.svg" target="_self">]]></description><link>assets/l12-page-fault-handling-eng.html</link><guid isPermaLink="false">assets/L12-Page-Fault-Handling-ENG.svg</guid><pubDate>Mon, 30 Mar 2026 20:48:31 GMT</pubDate><enclosure url="assets/l12-page-fault-handling-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l12-page-fault-handling-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L12-Virtual-Physical-Mapping-ENG]]></title><description><![CDATA[<img src="assets/l12-virtual-physical-mapping-eng.svg" target="_self">]]></description><link>assets/l12-virtual-physical-mapping-eng.html</link><guid isPermaLink="false">assets/L12-Virtual-Physical-Mapping-ENG.svg</guid><pubDate>Mon, 30 Mar 2026 20:48:22 GMT</pubDate><enclosure url="assets/l12-virtual-physical-mapping-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l12-virtual-physical-mapping-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L12-Two-Level-Page-Table-ENG]]></title><description><![CDATA[<img src="assets/l12-two-level-page-table-eng.svg" target="_self">]]></description><link>assets/l12-two-level-page-table-eng.html</link><guid isPermaLink="false">assets/L12-Two-Level-Page-Table-ENG.svg</guid><pubDate>Mon, 30 Mar 2026 20:48:10 GMT</pubDate><enclosure url="assets/l12-two-level-page-table-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l12-two-level-page-table-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L12-Virtual-Address-Space]]></title><description><![CDATA[<img src="assets/l12-virtual-address-space.svg" target="_self">]]></description><link>assets/l12-virtual-address-space.html</link><guid isPermaLink="false">assets/L12-Virtual-Address-Space.svg</guid><pubDate>Mon, 30 Mar 2026 20:47:52 GMT</pubDate><enclosure url="assets/l12-virtual-address-space.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l12-virtual-address-space.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L12-Virtual-Address-Space-ENG]]></title><description><![CDATA[<img src="assets/l12-virtual-address-space-eng.svg" target="_self">]]></description><link>assets/l12-virtual-address-space-eng.html</link><guid isPermaLink="false">assets/L12-Virtual-Address-Space-ENG.svg</guid><pubDate>Mon, 30 Mar 2026 20:47:33 GMT</pubDate><enclosure url="assets/l12-virtual-address-space-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l12-virtual-address-space-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Contest]]></title><description><![CDATA[Архітектура обчислювальних систем, 2026Написати на асемблері реалізацію закритої задачі мінімального розміру (скомпільований бінарний COM-файл).Задача буде оголошена після завершення реєстрації. Заздалегідь відомо лише:
Платформа: TASM / DOSBox-X, формат COM.
Задача пов'язана з матеріалом курсу.
Програма приймає вхідний файл як аргумент командного рядка та пише результат в stdout.
Буде надано базовий набір тестів (поріг для участі) та розширений набір тестів (для фінального рейтингу).
Буде надано еталонну реалізацію на Python, яка є референсом для всіх тестів.
Ваше завдання — написати найменшу за розміром працюючу реалізацію.Всі студенти курсу — і 4 ECTS, і 6 ECTS.Участь добровільна, але має наслідки (див. «Бонуси та достроковий схід з дистанції»).Максимальна кількість учасників — 25.Якщо зареєструється більше 25 студентів, до участі буде допущено топ-25 за поточним рейтингом (відсоток від максимально доступних балів на Distedu на момент закриття реєстрації).Рішення щодо відсіву буде оголошено протягом доби після закриття реєстрації.Поки ви є учасником змагання, вам автоматично зараховується максимальний бал за практичні роботи Lab12, Lab13, Lab14 (лабораторна + домашня частина + бонус):
Момент реєстрації (тиждень 12): одразу отримуєте максимум за Lab12.
Понеділок тижня 13: отримуєте максимум за Lab13.
Понеділок тижня 14: отримуєте максимум за Lab14.
Це означає, що під час змагання вам не потрібно здавати Lab12–Lab14 окремо — ви зосереджуєтесь на змаганні. При цьому радимо пропрацьовувати лабораторні, щоб гарно написати контрольну №2 та практичні завдання на іспиті.Щоб зберегти автоматичні бали за Lab12–Lab14, ваша програма повинна пройти базовий набір тестів до суботи, 11 квітня, 22:00.Якщо на цей момент базові тести не пройдено — ви вважаєтесь такими, що вибули зі змагання, і усі автоматично на той момент зараховані бали обнуляються — незалежно від того, чи виконували ви ці практики самостійно.
⚠️ Зверніть увагу: чим пізніше ви вирішите відмовитися від участі, тим більше балів ви втратите. Якщо ви зареєструвалися і відмовились одразу — ви втрачаєте лише Lab12. Якщо ви тримались до кінця і не здали — втрачаєте Lab12 + Lab13 + Lab14. Це свідомий вибір: реєстрація — це зобов'язання.
Серед учасників, чиї програми проходять всі тести (базові + розширені), складається рейтинг за розміром COM-файлу (менше = краще). Додатково до заліку практик:
1 місце: +10 балів до підсумкової оцінки
2–3 місце: +5 балів
4–6 місце: +3 бали
Реєстрація відкрита з моменту оголошення до середи, 1 квітня, 23:59.Для реєстрації:
Перейдіть за посиланням GitHub Classroom: <a rel="noopener nofollow" class="external-link is-unresolved" href="https://classroom.github.com/a/bHALVkKI" target="_self">https://classroom.github.com/a/bHALVkKI</a>
Прийміть завдання — буде створено приватний репозиторій contest-[ваш-github-логін].
Використовуйте той самий GitHub-акаунт, що й для лабораторних робіт. ⚠️ УВАГА: прийняття завдання = реєстрація на змагання. З цього моменту ваші бали за Lab12–Lab14 замінюються автоматичним максимальним балом і бонусом і можуть частково або повністю обнулитися (див. «Умова збереження балів»). Переконайтесь, що ви готові взяти на себе це зобов'язання до того, як натиснете «Accept».
Окремого підтвердження не потрібно.Після закриття реєстрації список учасників буде фіналізовано (з відсівом до 25, якщо потрібно), і задача буде оголошена.
Приватний репозиторій на GitHub (через GitHub Classroom).
Гранулярні, змістовні коміти (як у вимогах до проєкту, але префікси feat:, fix: тощо — необов'язкові): Один коміт = одна логічна зміна або оптимізація.
Повідомлення коміту починається з дієслова в наказовому способі (imperative mood): «add», «remove», «replace», «use» — не «added», «removed». Тест: «If applied, this commit will [ваш опис]».
Повідомлення пояснює, що було змінено та чому це зменшило розмір (або покращило коректність).
Історія комітів має показувати процес, а не лише фінальний результат.
Бонус +1 бал до підсумкової оцінки, якщо всі коміти відповідають цим вимогам. Компіляція: TASM → TLINK → COM-файл. Середовище: DOSBox-X (як на практичних).
Тестування: програма має проходити наданий тестовий набір. Результат порівнюється побайтово з еталоном.
Оцінюється: розмір фінального COM-файлу в байтах. Менше — краще. Базовий набір — кілька простих тестів, що перевіряють коректність основної функціональності. Проходження базового набору — поріг для збереження автоматичних балів за Lab12–Lab14. Базові тести можуть змінитися під час змагання (рядки-входи), але функціонал, які вони тестують — ні. Розширений набір — додаткові тести з крайніми випадками та більшими вхідними даними. Програма, що проходить всі тести, потрапляє до турнірної таблиці (рейтинг за розміром).
Програма, що проходить базові тести, але не всі розширені — зберігає бали за практики, але не бере участь у турнірі.Середовище ідентичне практичним роботам P3–P10.
Команди компіляції:TASM /s /m /n /q /zn tm.asm
TLINK /3 /t /n /x tm
Результат: TM.COM — саме його розмір є вашим результатом у змаганні.
COM-файл починає виконання з адреси 100h. Немає заголовків — кожен байт коду потрапляє у файл.
Подумайте, які інструкції x86 кодуються в менше байтів (наприклад, xor ax, ax замість mov ax, 0).
Використання регістрів замість пам'яті зазвичай дає коротші інструкції.
Деякі переривання DOS (INT 21h) мають кілька функцій різної складності — оберіть найкомпактніші.
Не бійтеся починати з працюючої (нехай великої) версії і поступово оптимізувати.
Змагання проводиться на засадах доброчесності та порядності.Дозволено:
Використовувати матеріали курсу та документацію x86/DOS.
Використовувати надану еталонну реалізацію на Python для розуміння задачі та перевірки своїх результатів.
Обговорювати загальні підходи з іншими студентами.
Використовувати AI-інструменти для пояснення концепцій та допомоги з розумінням інструкцій.
Заборонено:
Подавати знайдену в інтернеті готову реалізацію.
Подавати код, згенерований AI, як власний.
Подавати немодифікований ASM-вивід будь-якого компілятора без власних оптимізацій.
Контроль:
Історія комітів має демонструвати поступову розробку. Один коміт із фінальним результатом = дискваліфікація.
Кожна оптимізація — окремий коміт з поясненням. Без проміжних варіантів = дискваліфікація.
Буде проведено перевірку на плагіат між роботами учасників та відкритими джерелами.
Дискваліфікація = обнуління балів за Lab12–Lab14 (еквівалент нездачі).
Виявлення шахрайства (плагіат, подання чужого коду, фальсифікація комітів) є підставою для офіційної процедури порушення академічної доброчесності відповідно до нормативних документів НаУКМА.
Чи можна брати участь, якщо я студент 4 ECTS (без проєкту)?
Так. Змагання відкрите для всіх студентів курсу. Автозалік Lab12–Lab14 працює однаково для обох курсів.Чи впливає змагання на оцінку за проєкт (6 ECTS)?
Ні. Автоматичний залік стосується лише практик Lab12–Lab14. Турнірні бонуси додаються до підсумкової оцінки з дисципліни.Я зареєструвався, але зрозумів, що не встигаю. Що робити?
Мінімальний поріг — пройти базовий набір тестів. Навіть наївна, неоптимізована реалізація, що проходить базові тести, зберігає ваші бали за Lab12–Lab14 і максимальний бонус за них. Сфокусуйтесь на тому, щоб програма працювала, а потім оптимізуйте.Що станеться з моїми балами за Lab12–Lab14, якщо я не здам програму?
Усі автоматично зараховані бали обнуляються, але тільки на тому тижні, коли вони нараховані. Бали за Лаб 12 нараховуються в момент реєстрації. За Лаб 13 — в понеділок 13 тижня. За Лаб 14 — в понеділок 14 тижня. Якщо ви, наприклад, реально виконали Lab12 на лабораторній — цей бал все одно буде замінено на 0. Тому реєструйтесь лише якщо плануєте довести справу до кінця.Чи можна працювати в парі?
Ні. Змагання суто індивідуальне.Чи можна використати код з мого курсового проєкту?
Ні. Змагання — окрема робота з окремим репозиторієм.Що якщо два учасники мають однаковий розмір COM?
Це малоймовірно, але якщо станеться — перевага надається тому, хто раніше запушив версію, що проходить всі тести (базові + розширені). Враховується час push, а не час коміту. Нагадуємо: до турнірної таблиці потрапляють лише учасники, чиї програми проходять всі тести.]]></description><link>labs/contest.html</link><guid isPermaLink="false">Labs/Contest.md</guid><pubDate>Mon, 30 Mar 2026 19:38:07 GMT</pubDate></item><item><title><![CDATA[Консультація - 2026-03-28]]></title><description><![CDATA[Дата: 28 березня 2026 р. (субота)
Тривалість: ~51 хвилина
Формат: онлайн, відеоконференціяКурс перейшов на NASM (Netwide Assembler) — це повністю сучасний інструментарій, яким користуються в реальній індустрії. На відміну від DOS/TASM, з яким працювали раніше, NASM — це те, на чому пишуть:
Частини ядра Linux, що залежать від архітектури процесора.
Драйвери та інший низькорівневий код.
Код для reverse engineering та аналізу безпеки.
Інструкція syscall у NASM — це по суті еквівалент INT 80h (системний виклик Linux), так само як INT 21h був системним викликом DOS. Механіка та сама: заповнюєте регістри потрібними значеннями й викликаєте переривання. Дебагери (GDB) та інші інструменти командного рядка, що використовуються на лабораторних, — теж повністю сучасні та застосовуються в реальній роботі.Асемблер — це машинна мова, і сам машинний код ідентичний незалежно від операційної системи, якщо процесор однаковий. Що відрізняється — це «обгортка»: хедери, структури даних, формат виконуваного файлу, які додає компілятор, щоб конкретна ОС могла завантажити та запустити програму.Код, що пишеться на курсі, — під архітектуру x86-64.macOS — це також Unix-система (ядро портоване з FreeBSD). Структура каталогів macOS дуже схожа на Linux. Термін «*nix» (зірочка-nix) об'єднує Unix, Linux та подібні системи. NASM існує і під Windows, і під Mac, і під Linux.Програму на NASM не можна просто скомпілювати під одну ОС і запустити на іншій:
syscall / INT 80h — це виклики ядра Linux. Під Windows вони не працюють, там інші номери переривань та інший механізм.
Приклад Windows-коду на NASM: замість syscall використовуються extern + call до системних бібліотек (наприклад, MessageBoxA). Параметри передаються через стек (push). Всередині цих бібліотечних функцій є свої системні переривання — просто вони приховані за шаром абстракції.
Щоб писати на NASM під Windows, потрібно змінити системні виклики, номери функцій, імпорти бібліотек. Сам синтаксис Assembly залишається тим самим.
Була розповідена історія виникнення DOS:
CP/M — одна з ранніх операційних систем, яка стала основою для DOS.
Білл Гейтс викупив QDOS (Quick and Dirty Operating System) у програміста, перейменував на MS-DOS (Disk Operating System) і продав IBM для їхніх перших персональних комп'ютерів.
Саме з цього часу залишився backslash (\) у шляхах Windows — на відміну від forward slash (/) в Unix. Backslash в усіх мовах програмування є escape-символом, що досі створює незручності.
Windows NT суттєво покращив стабільність Windows. До NT система була значно більш баговою порівняно з Unix.
Лінус Торвальдс створив не лише ядро Linux, а й Git — систему контролю версій, побудовану на концепції хешів (по суті, блокчейн). Робочий прототип Git був створений приблизно за 10 днів, коли це стало терміново необхідним для нормальної роботи над ядром Linux.Linux спочатку сприймався як «десктоп для домогосподарок», але завдяки вдалій архітектурі ядра (зокрема концепції мікроядра) став домінуючою серверною ОС. Ubuntu, як один із найпопулярніших дистрибутивів, — зараз на переважній більшості серверів.Лінус також був візіонером щодо SSD: задовго до масового використання він написав статтю, критикуючи HDD (назвавши їх приблизно «чортовими крутячими болванками»). Різниця у швидкості доступу між HDD та SSD колосальна — раніше завантаження ОС могло тривати 5 хвилин.Було зазначено: «один серед десяти тисяч програмістів може стати хорошим архітектором» — не через генетику, а через те, на що людина витрачає свій час. Зробити архітектуру, яка витримає десятки років розвитку продукту, — це окреме мистецтво. На магістерській програмі є курс «Чистий код, чиста архітектура», де розглядаються ідеї Uncle Bob'а (Роберта Мартіна) — як з точки зору переваг, так і критики.Прогрес проєктів відстежується через контрольні точки. Студентам, які йдуть попереду плану, пишуть «кандидат на змагання». Було наголошено на відомому феномені: «80% Done, 80% Remaining» — коли здається, що залишилося 20% роботи, насправді може бути ще стільки ж. Особливо це стосується фази інтеграції та фінального доведення проєкту.Основна задача цього року — щоб кожен студент довів проєкт до кінця, створивши щось цілісне та працююче на низькому рівні. Це унікальний досвід, якого не було в попередні роки (тоді проєкти були простішими, синтетичними).Змагання (code golf) планується оголосити найближчим часом (в понеділок буде форма запису та умови). Цього року формат відрізняється від попередніх:
Раніше змагання повністю замінювали проєкт і практичні. Цього року проєкти дають більше досвіду, тому повне звільнення від них означало б пропустити важливі навички.
Студенти, які хочуть участі в змаганні, мають бути на достатньому рівні завершеності проєкту.
Можливий варіант — паралельне виконання проєкту та участь у змаганні.
Рекомендовано не перескакувати сучасну частину курсу (NASM, сучасні інструменти), якої не було минулого року.
На курсі лабораторні виконуються у GitHub Codespaces — хмарній віртуальній машині з Linux. Це зручно (один клік), але деякі студенти хочуть працювати локально. Обговорювалися варіанти:
Хороший варіант. Встановити VirtualBox, на нього — Ubuntu.
Ще краще — встановити Linux безпосередньо на машину (dual boot).
Важливо: налаштувати мережу у віртуальній машині (для встановлення інструментів).
Порада: не ставити VS Code всередину віртуальної машини (буде лагати). Краще розшарити папки між VM і хост-системою, відкривати код у VS Code на хост-машині, а компілювати/запускати у VM через термінал.
Ubuntu Server (без десктопа) займає менше місця та працює швидше. Один зі студентів успішно налаштував WSL + розширення VS Code від Microsoft, яке лінкує VS Code на Windows до локального WSL. Працює.
Це простіший варіант, ніж повноцінна VM. Нові MacBook на Apple Silicon (ARM) — не x86. Потрібна AMD64 віртуальна машина.
Rosetta (транслятор ARM → x86) погано працює з дебагером GDB.
На Mac є GAS (GNU Assembler), GCC, GDB — все портовано, але для x86-64 лабораторних потрібна VM. Встановити Git (apt install git на Ubuntu).
Краще клонувати репозиторій по SSH (створити SSH-ключ, додати в GitHub як deploy key). По HTTPS буде щоразу запитувати логін/пароль.
Якщо працюєте і з VM, і з хост-машини — розшарте папку, щоб уникнути необхідності push/pull між двома середовищами та потенційних конфліктів.
Codespaces залишається найпростішим варіантом (один клік). Локальне налаштування — для тих, у кого є час та бажання, або якщо Codespaces не працює (буває через нестабільний Wi-Fi в Могилянці). Як альтернативу можна використовувати мобільний інтернет.Побіжно обговорювалися принципи ефективних команд:
Оптимальний розмір команди, що може продуктивно працювати — ~10 осіб. Більше — потрібно ділити на підкоманди.
Книга «Міфічний людино-місяць» (The Mythical Man-Month) — класика на цю тему.
Трекінг задач (карточки, естімейти) допомагає об'єктивно оцінити прогрес, а не покладатися на інтуїцію.
В індустрії маятник коливається між fullstack та спеціалізацією; зараз тренд — fullstack.
Для команд, де фронтенд і бекенд розділені: достатньо домовитися про API на початку, після чого команди працюють незалежно.
На запитання про WebAssembly:
WASM — це низькорівнева мова, яка працює в браузері. Браузер компілює її якомога ближче до машинного коду конкретної платформи.
За концепцією схоже на байт-код Java: пишете під «віртуальну машину» браузера, а вона вже транслює в реальний машинний код.
Дає доступ до GPU-акселерації та іншого з боку клієнта.
WASM вийшов за межі браузера: є WASM workloads у Docker, використання в серверних середовищах.
Приклад «товстого клієнта»: нейронна мережа, що працює на стороні користувача замість сервера, — для кращого UX або для розподілу обчислювальних ресурсів (мільйон процесорів користувачів замість 24 ядер сервера).
]]></description><link>office-hours/консультація-2026-03-28.html</link><guid isPermaLink="false">Office-hours/Консультація - 2026-03-28.md</guid><pubDate>Sat, 28 Mar 2026 14:12:54 GMT</pubDate></item><item><title><![CDATA[Lab11]]></title><description><![CDATA[
Курс: Архітектура обчислювальних систем
Тиждень: 11
Тривалість: 2 академічні години
Тип: Практична робота під наглядом інструктора
Після завершення практики студент зможе:
Налаштувати середовище розробки Linux x86-64 у GitHub Codespaces.
Написати та зібрати програму x86-64 на NASM із використанням системних викликів (syscall).
Використовувати GDB з розширенням GEF для налагодження 64-бітних програм.
Пояснити ключові відмінності x86-64 від x86-16 (регістри, системні виклики, плоска модель пам'яті).
Вимірювати cache hit/miss за допомогою cachegrind та perf stat.
Продемонструвати різницю між послідовним та непослідовним доступом до пам'яті на конкретних вимірах.
Перегляньте лекційні матеріали за Тиждень 11 (Ієрархія пам'яті та продуктивність). Зверніть увагу на розділи:
Latency Numbers Every Programmer Should Know — чому кеш-промах коштує ~200 тактів
Locality of Reference — темпоральна та просторова локальність
Matrix Access Patterns — row-major vs column-major і їх вплив на продуктивність
Cache Structure — рядки, теги, множини, декомпозиція адреси Важливо. Починаючи з цієї практики, ми переходимо з DOSBox (16-бітний реальний режим) на Linux x86-64 (64-бітний захищений режим). Середовище — GitHub Codespaces. Усі інструменти (NASM, GDB, GEF, Valgrind) будуть встановлені на занятті.
64-бітна архітектура розширює всі знайомі вам регістри та додає нові:┌────────────────────────────────────────────────────────┐
│ RAX (64 біт) │
│ ┌───────────────────────────────────┤
│ │ EAX (32 біт) │
│ │ ┌────────────────────────┤
│ │ │ AX (16 біт) │
│ │ │ ┌──────┬────────────┤
│ │ │ │AH(8) │ AL (8) │
└────────────────────┴──────────┴────┴──────┴────────────┘
Класичні регістри (розширені): RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSPНові регістри: R8, R9, R10, R11, R12, R13, R14, R15Спеціальне призначення: RIP (instruction pointer), RFLAGS (прапорці)
Зверніть увагу: AH/BH/CH/DH доступні лише в 16-бітних операціях. У 64-бітних операціях молодші 8 біт нових регістрів — R8B, R9B тощо.
Замість переривань DOS (INT 21h, функція в AH) Linux використовує інструкцію syscall. Параметри передаються через регістри:Основні системні виклики:Порівняння:; --- DOS (TASM, 16-біт) --- ; --- Linux (NASM, 64-біт) ---
mov ah, 09h mov rax, 1 ; sys_write
mov dx, offset msg mov rdi, 1 ; stdout
int 21h mov rsi, msg ; адреса mov rdx, 13 ; довжина syscall mov ah, 4Ch mov rax, 60 ; sys_exit
int 21h xor rdi, rdi ; код 0 syscall
nasm -f elf64 -g program.asm -o program.o # Асемблювання → об'єктний файл
ld program.o -o program # Лінкування → виконуваний файл
./program # Запуск -f elf64 — формат ELF для 64-бітного Linux
-g — додати debug-інформацію (для GDB)
time — найпростіший спосіб виміряти час виконання:time ./program
Виводить real (wall-clock), user (CPU в user-mode), sys (CPU в kernel-mode).valgrind --tool=cachegrind --cache-sim=yes — симуляція кеш-пам'яті (працює всюди, включно з Codespaces):valgrind --tool=cachegrind --cache-sim=yes ./program Важливо: прапорець --cache-sim=yes обов'язковий у нових версіях Valgrind (3.21+). Без нього cachegrind не виводить статистику кешу.
Виводить статистику: кількість звернень до пам'яті, L1/LL cache hits/misses.
Ключові метрики cachegrind: I refs — кількість звернень до інструкцій
D refs — кількість звернень до даних (reads + writes)
D1 misses — промахи L1 data cache
LL misses (Last Level) — промахи останнього рівня кешу (≈ звернення до RAM)
D1 miss rate — відсоток промахів = D1 misses / D refs perf stat — апаратні лічильники CPU (може не працювати у Codespaces — див. Вирішення проблем):perf stat ./program
У README вашого репозиторію є кнопка "Open in GitHub Codespaces" — натисніть її. Зачекайте 1–2 хвилини, поки середовище завантажиться — відкриється VS Code з повноцінним терміналом Linux.Якщо кнопки немає, створіть Codespace вручну:
Відкрийте ваш репозиторій на GitHub
Натисніть зелену кнопку &lt;&gt; Code → вкладка Codespaces → Create codespace on main Що таке Codespace? Це повноцінна віртуальна машина Linux у хмарі GitHub із VS Code (у браузері або десктопному додатку). Ваш репозиторій уже клоновано всередину. Усі зміни зберігаються — Codespace можна зупинити та продовжити пізніше. Список ваших Codespaces: <a data-tooltip-position="top" aria-label="https://github.com/codespaces" rel="noopener nofollow" class="external-link is-unresolved" href="https://github.com/codespaces" target="_self">github.com/codespaces</a>. ⚠️ Codespace — це інший комп'ютер. Ваші файли не з'являться на GitHub автоматично. Регулярно робіть git commit + git push (детальніше — у кінці цієї практики).
<br>
Якщо ви раніше вже створювали Codespace для цього репозиторію, не створюйте новий — відкрийте існуючий зі сторінки <a data-tooltip-position="top" aria-label="https://github.com/codespaces" rel="noopener nofollow" class="external-link is-unresolved" href="https://github.com/codespaces" target="_self">github.com/codespaces</a>. ⚠️ Безкоштовна квота: 120 core-hours на місяць. Двоядерний Codespace (за замовчуванням) = 60 годин роботи. Завжди зупиняйте Codespace після роботи! (Ctrl+Shift+P → Codespaces: Stop Current Codespace, або на GitHub: Codespaces → ... → Stop codespace).
У Codespaces може бути зламане посилання на репозиторій Yarn, через що apt-get update завершується з помилкою. Виконайте ці команди, щоб виправити:curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | sudo tee /usr/share/keyrings/yarn-archive-keyring.gpg &gt; /dev/null
echo "deb [signed-by=/usr/share/keyrings/yarn-archive-keyring.gpg] https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
Відкрийте термінал у VS Code (Ctrl+`` або Terminal → New Terminal) і виконайте:sudo apt-get update &amp;&amp; sudo apt-get install -y nasm gdb valgrind
Перевірте, що інструменти встановлені:nasm --version &amp;&amp; gdb --version | head -1 &amp;&amp; valgrind --version
Ви маєте побачити версії всіх трьох програм.<br><a data-tooltip-position="top" aria-label="https://hugsy.github.io/gef/" rel="noopener nofollow" class="external-link is-unresolved" href="https://hugsy.github.io/gef/" target="_self">GEF</a> — розширення для GDB, яке додає зручне відображення регістрів, стеку та коду (подібно до Turbo Debugger).Спосіб 1 (автоматичний):bash -c "$(curl -fsSL https://gef.blah.cat/sh)" Якщо команда вище видає помилку 403 Forbidden, скористайтесь способом 2.
Спосіб 2 (ручне клонування):git clone https://github.com/hugsy/gef.git ~/.gef
echo "source ~/.gef/gef.py" &gt;&gt; ~/.gdbinit
Перевірте:echo "quit" | gdb -q 2&gt;&amp;1 | head -3
Якщо бачите GEF у виводі — розширення встановлено.mkdir -p Lab11 &amp;&amp; cd Lab11
Створіть файл hello.asm:; hello.asm — перша програма x86-64 для Linux
; Аналог DOS-програми з INT 21h/09h, але через syscall section .data msg db "Hello from x86-64!", 10 ; 10 = '\n' (новий рядок) msg_len equ $ - msg ; довжина рядка (обчислюється автоматично) section .text global _start _start: ; --- Вивести рядок (аналог INT 21h/09h) --- mov rax, 1 ; syscall номер 1 = sys_write mov rdi, 1 ; file descriptor 1 = stdout mov rsi, msg ; адреса рядка mov rdx, msg_len ; довжина рядка syscall ; --- Вийти з програми (аналог INT 21h/4Ch) --- mov rax, 60 ; syscall номер 60 = sys_exit xor rdi, rdi ; код виходу 0 syscall
Зберіть та запустіть:nasm -f elf64 -g hello.asm -o hello.o
ld hello.o -o hello
./hello
Ви маєте побачити: Hello from x86-64!Запишіть у коментарях у файлі hello.asm відповіді на питання:
Замість INT 21h ми використовуємо — ?
Замість AH = 09h (номер функції) ми записуємо номер виклику в — ? (який регістр)
Замість .MODEL small / .STACK / .DATA / .CODE ми використовуємо — ?
Замість END start точка входу визначається через — ?
Рядок у DOS закінчувався символом $. Як визначається довжина рядка у Linux? (Підказка: equ $ - msg)
Запустіть hello у GDB:gdb ./hello
GEF автоматично покаже кольоровий інтерфейс. Виконайте команди:b _start ← встановити breakpoint на точці входу (аналог F2 в TD)
r ← запустити програму (аналог F9 в TD)
GEF покаже стан регістрів, код та стек — подібно до вікон TD.Використайте покрокове виконання (аналог F7/F8 у TD):si ← step instruction (одна інструкція, аналог F7 в TD)
si ← ще раз
si ← ще раз (після цього має виконатися syscall write) Увага: si виконує одну машинну інструкцію. Після syscall (sys_write) ви побачите вивід Hello from x86-64! у терміналі GDB.
Запишіть у коментарях hello.asm:
Значення RAX, RDI, RSI, RDX перед першим syscall (видно у верхній панелі GEF)
Значення RAX після першого syscall — що воно означає? (Підказка: sys_write повертає кількість записаних байтів)
Яка адреса RSI? Це адреса чого?
Для виходу з GDB:q ← вийти (quit)
Тепер перейдемо до мови C, щоб дослідити поведінку кешу на реальних прикладах.Програма створює масив із 4 мільйонів елементів (16 MB — більше, ніж типовий L3 cache) і обходить його двома способами: seq (послідовний) — елементи читаються один за одним: arr[0], arr[1], arr[2], ... Це ідеально використовує просторову локальність: один cache line (64 байти) містить 16 сусідніх int, тому 15 з 16 звернень — cache hits. rand (stride-256) — елементи читаються з кроком 256: arr[0], arr[256], arr[512], ... Крок 256 елементів = 1024 байти = 16 cache lines. Кожне звернення потрапляє в інший cache line, тому майже кожне — cache miss. Обидві версії читають усі елементи масиву однакову кількість разів (10 ітерацій), тому обсяг роботи однаковий — різниця у часі виконання зумовлена виключно поведінкою кешу.Створіть файл cache_test.c:// cache_test.c — послідовний vs випадковий обхід масиву
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;time.h&gt; #define SIZE (4 * 1024 * 1024) // 4M елементів = 16 MB (більше за L3 cache)
#define ITERS 10 // повторити для стабільності int main(int argc, char *argv[]) { if (argc &lt; 2) { printf("Usage: %s seq|rand\n", argv[0]); return 1; } int *arr = malloc(SIZE * sizeof(int)); if (!arr) { perror("malloc"); return 1; } // Ініціалізація масиву for (int i = 0; i &lt; SIZE; i++) arr[i] = i; long sum = 0; if (argv[1][0] == 's') { // --- Послідовний доступ (stride-1) --- for (int iter = 0; iter &lt; ITERS; iter++) for (int i = 0; i &lt; SIZE; i++) sum += arr[i]; } else { // --- Доступ із великим кроком (stride-256) --- // Стрибаємо через 256 елементів = 1024 байти = 16 cache lines for (int iter = 0; iter &lt; ITERS; iter++) for (int s = 0; s &lt; 256 &amp;&amp; s &lt; SIZE; s++) for (int i = s; i &lt; SIZE; i += 256) sum += arr[i]; } printf("sum = %ld\n", sum); free(arr); return 0;
} Чому stride-256, а не random? Справжній випадковий доступ вимагає генерації випадкових індексів, що додає шум. Stride-256 дає той самий ефект — кожне звернення потрапляє в інший cache line — але простіший для аналізу.
Скомпілюйте без оптимізацій (щоб компілятор не перетворив наш доступ):gcc -O0 -o cache_test cache_test.c
Виміряйте час виконання обох версій:time ./cache_test seq
time ./cache_test rand
Запишіть результати (значення real) у файл results.txt:=== Частина 3: Послідовний vs Stride-256 ===
time seq: ___
time rand: ___
Різниця: ___x
Запустіть обидві версії через cachegrind:valgrind --tool=cachegrind --cache-sim=yes ./cache_test seq 2&gt;&amp;1 | tail -30
Cachegrind виводить статистику у stderr (тому 2&gt;&amp;1 перенаправляє її у stdout для зручного перегляду). У кінці виводу ви побачите таблицю з трьома секціями: I refs (інструкції), D refs (дані), LL refs (Last Level cache). Нас цікавить секція D refs — вона показує звернення до даних та промахи.Приклад виводу:==12345== D refs: 41,943,084 (41,943,050 rd + 34 wr)
==12345== D1 misses: 262,167 ( 262,155 rd + 12 wr)
==12345== LLd misses: 262,153 ( 262,143 rd + 10 wr)
==12345== D1 miss rate: 0.6% ( 0.6% + 35.3%)
Тепер запустіть stride-256 версію:valgrind --tool=cachegrind --cache-sim=yes ./cache_test rand 2&gt;&amp;1 | tail -30
Запишіть у results.txt:=== Cachegrind: послідовний ===
D refs: ___
D1 misses: ___
D1 miss rate: ___% === Cachegrind: stride-256 ===
D refs: ___
D1 misses: ___
D1 miss rate: ___% Що ви маєте побачити: при послідовному доступі D1 miss rate ≈ 5–7% (один промах на 16 елементів у cache line). При stride-256 — miss rate значно вищий, бо кожне звернення потрапляє в новий cache line.
Створіть файл matrix_walk.c:// matrix_walk.c — row-major vs column-major обхід матриці
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt; #define N 4096 // 4096 x 4096 матриця = 64 MB int matrix[N][N]; // глобальна (щоб вмістилась — стеку може не вистачити) int main(int argc, char *argv[]) { if (argc &lt; 2) { printf("Usage: %s row|col\n", argv[0]); return 1; } long sum = 0; if (argv[1][0] == 'r') { // Row-major: matrix[0][0], [0][1], [0][2], ... — послідовно в пам'яті for (int i = 0; i &lt; N; i++) for (int j = 0; j &lt; N; j++) sum += matrix[i][j]; } else { // Column-major: matrix[0][0], [1][0], [2][0], ... — стрибки через N елементів for (int j = 0; j &lt; N; j++) for (int i = 0; i &lt; N; i++) sum += matrix[i][j]; } printf("sum = %ld\n", sum); return 0;
}
gcc -O0 -o matrix_walk matrix_walk.c
time ./matrix_walk row
time ./matrix_walk col
Потім через cachegrind (знайдіть рядки D refs, D1 misses, D1 miss rate у виводі):valgrind --tool=cachegrind --cache-sim=yes ./matrix_walk row 2&gt;&amp;1 | tail -30
valgrind --tool=cachegrind --cache-sim=yes ./matrix_walk col 2&gt;&amp;1 | tail -30
Запишіть у results.txt:=== Частина 4: Row-major vs Column-major (4096x4096) ===
time row: ___
time col: ___
Різниця: ___x D1 miss rate (row): ___%
D1 miss rate (col): ___%
Допишіть у results.txt відповіді:
Чому row-major швидший? (Підказка: C зберігає масиви в row-major order)
Який stride у row-major обході? А в column-major? (У елементах та байтах)
Cache line = 64 байти, sizeof(int) = 4 байти. Скільки елементів int вміщується в один cache line?
При row-major обході: скільки звернень із 16 (один cache line) будуть промахами?
До кінця заняття:
Codespace створено та налаштовано (NASM, GDB, GEF, Valgrind)
hello.asm — програма x86-64, зібрана та запущена, з коментарями-відповідями
cache_test.c — експеримент послідовний vs stride-256
matrix_walk.c — експеримент row-major vs column-major
results.txt — усі виміри та відповіді на питання
Закомічено та запушено всі файли (git add, git commit, git push) ⚠️ ВАЖЛИВО: Codespace — це інша машина!
Ваші файли існують лише всередині Codespace. Щоб вони з'явилися на GitHub (і на вашому звичайному комп'ютері), потрібно commit + push:
cd ~/workspaces/* # перейти у корінь репозиторію
git add Lab11/
git commit -m "Lab11: in-class work"
git push Якщо ви також працюєте з репозиторієм на своєму комп'ютері, перед початком роботи виконайте git pull, щоб отримати зміни з Codespace. Інакше виникнуть конфлікти злиття (merge conflicts).
Правило: завжди push у Codespace → pull на комп'ютері (і навпаки) перед початком роботи. Не забудьте зупинити Codespace після роботи! Ctrl+Shift+P → Codespaces: Stop Current Codespace
Дедлайн: 23:59 у день перед Практикою 12Напишіть програму на C, яка множить дві квадратні матриці та порівнює наївну реалізацію з оптимізованою.// Класичне множення: C[i][j] = Σ A[i][k] * B[k][j]
for (int i = 0; i &lt; N; i++) for (int j = 0; j &lt; N; j++) for (int k = 0; k &lt; N; k++) C[i][j] += A[i][k] * B[k][j];
Проблема: при обході по k масив B читається по стовпцях (B[k][j] зі зростаючим k) — stride-N, cache-hostile.Реалізуйте одну або обидві оптимізації:Оптимізація 1: Зміна порядку циклів (ijk → ikj)for (int i = 0; i &lt; N; i++) for (int k = 0; k &lt; N; k++) for (int j = 0; j &lt; N; j++) C[i][j] += A[i][k] * B[k][j];
У цій версії внутрішній цикл по j обходить B[k][j] послідовно (stride-1), а A[i][k] — константа на ітерації внутрішнього циклу.Оптимізація 2: Тайлінг (blocking)#define BLOCK 64 for (int ii = 0; ii &lt; N; ii += BLOCK) for (int jj = 0; jj &lt; N; jj += BLOCK) for (int kk = 0; kk &lt; N; kk += BLOCK) for (int i = ii; i &lt; ii + BLOCK &amp;&amp; i &lt; N; i++) for (int j = jj; j &lt; jj + BLOCK &amp;&amp; j &lt; N; j++) for (int k = kk; k &lt; kk + BLOCK &amp;&amp; k &lt; N; k++) C[i][j] += A[i][k] * B[k][j];
Тайлінг розбиває матрицю на блоки, що вміщуються в L1 cache, зменшуючи capacity misses.
Розмір матриці: N = 1024 (1024×1024). Матриці глобальні або виділені через malloc
Ініціалізація: заповніть A та B значеннями (наприклад, A[i][j] = i + j, B[i][j] = i - j)
Три версії множення у одній програмі, обрані аргументом командного рядка: ./matmul naive — наївна (ijk)
./matmul opt1 — зміна порядку циклів (ikj)
./matmul opt2 — тайлінг (або обидві оптимізації разом) Вимірювання часу у програмі за допомогою clock():
#include &lt;time.h&gt;
clock_t start = clock();
// ... множення ...
clock_t end = clock();
double seconds = (double)(end - start) / CLOCKS_PER_SEC;
printf("Time: %.3f s\n", seconds); Перевірка коректності: обчисліть контрольну суму C[0][0] + C[N/2][N/2] + C[N-1][N-1] — вона має бути однаковою для всіх версій
Компіляція без оптимізацій: gcc -O0 -o matmul matmul.c (щоб компілятор не оптимізував за вас)
Запустіть кожну версію та запишіть:=== Матричне множення 1024x1024 (gcc -O0) === Naive (ijk): ___ с контрольна сума: ___
Opt1 (ikj): ___ с контрольна сума: ___
Opt2 (tiled): ___ с контрольна сума: ___ Прискорення opt1 vs naive: ___x
Прискорення opt2 vs naive: ___x === Cachegrind (D1 miss rate) ===
Naive: ___%
Opt1: ___%
Opt2: ___% Пояснення: чому оптимізована версія швидша?
(Ваша відповідь — 2-3 речення про stride, cache lines, locality)
Дослідіть, як розмір блоку (BLOCK) впливає на продуктивність тайлінгу.
Запустіть тайлінг із BLOCK = 8, 16, 32, 64, 128, 256
Запишіть час виконання для кожного
Побудуйте таблицю або графік (текстовий)
Поясніть: чому занадто малий та занадто великий блок — обидва погані?
Збережіть результати у bonus_report.txt.Запишіть відео, у якому:
Продемонструйте matmul — запустіть naive та оптимізовану версії, покажіть різницю у часі
Покажіть cachegrind: запустіть valgrind --tool=cachegrind --cache-sim=yes ./matmul naive та ./matmul opt1, порівняйте D1 miss rate
Поясніть: чому зміна порядку циклів (або тайлінг) зменшує кількість cache misses? Намалюйте на папері або екрані, як дані розташовані в пам'яті та як їх обходять різні версії
УВАГА! Вимога про 5хв максимально важлива! Brevity is the soul of wit. Це частина оцінки.Lab11/
├── hello.asm ← перша програма x86-64 (з коментарями)
├── cache_test.c ← експеримент: послідовний vs stride
├── matrix_walk.c ← експеримент: row-major vs column-major
├── results.txt ← виміри та відповіді з пари
├── matmul.c ← домашнє: множення матриць (naive + optimized)
├── matmul_report.txt ← звіт: час, cachegrind, пояснення
├── bonus_report.txt ← бонус: дослідження BLOCK size (якщо є)
└── video.txt ← посилання на Loom
Не забудьте git push! Якщо ви працюєте у Codespace — без push ваша робота існує лише на віддаленій машині GitHub. Закомітьте, запушіть, переконайтесь, що файли видно на github.com у вашому репозиторії.
<br><a data-tooltip-position="top" aria-label="https://gist.github.com/jboner/2841832" rel="noopener nofollow" class="external-link is-unresolved" href="https://gist.github.com/jboner/2841832" target="_self">Latency Numbers Every Programmer Should Know</a> — таблиця латентностей
<br><a data-tooltip-position="top" aria-label="https://www.nasm.us/xdoc/2.16.03/html/nasmdoc0.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.nasm.us/xdoc/2.16.03/html/nasmdoc0.html" target="_self">NASM Documentation</a> — довідка NASM
<br><a data-tooltip-position="top" aria-label="https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/" rel="noopener nofollow" class="external-link is-unresolved" href="https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/" target="_self">x86-64 Linux Syscall Table</a> — таблиця системних викликів
<br><a data-tooltip-position="top" aria-label="https://hugsy.github.io/gef/" rel="noopener nofollow" class="external-link is-unresolved" href="https://hugsy.github.io/gef/" target="_self">GEF Documentation</a> — довідка GEF для GDB
<br><a data-tooltip-position="top" aria-label="https://darkdust.net/files/GDB%20Cheat%20Sheet.pdf" rel="noopener nofollow" class="external-link is-unresolved" href="https://darkdust.net/files/GDB%20Cheat%20Sheet.pdf" target="_self">GDB Cheat Sheet</a> — шпаргалка GDB
<br><a data-tooltip-position="top" aria-label="https://valgrind.org/docs/manual/cg-manual.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://valgrind.org/docs/manual/cg-manual.html" target="_self">Cachegrind Manual</a> — довідка cachegrind
]]></description><link>labs/lab11.html</link><guid isPermaLink="false">Labs/Lab11.md</guid><pubDate>Wed, 25 Mar 2026 12:03:08 GMT</pubDate></item><item><title><![CDATA[L11-ENG. Memory Hierarchy and Performance]]></title><description><![CDATA[
Lecture 11 of Computer Systems Architecture course at NaUKMA.
After this lecture you will be able to:
Explain the memory hierarchy principle and the speed-capacity-cost tradeoff that motivates multi-level caching
Distinguish temporal and spatial locality, and write cache-friendly code that exploits both
Describe the internal structure of a cache (lines, tags, sets, offset/index/tag bit decomposition)
Compare direct-mapped, set-associative, and fully associative cache designs and their tradeoffs
Apply the AMAT formula to calculate effective memory access time for multi-level cache hierarchies
Explain cache replacement policies (LRU, FIFO, Random) and write policies (write-through vs write-back)
Describe the MESI cache coherence protocol and walk through state transitions in multicore scenarios
Identify false sharing as a performance pitfall and explain how to fix it with cache-line alignment
Imagine you are a chef in a busy kitchen. Your spice rack is an arm's reach away — you can grab salt or pepper in under a second. The pantry is down the hall — a thirty-second round trip. The grocery store is across town — half an hour. If your recipe calls for salt ten times, you do not walk to the store ten times; you keep salt on the counter. The reason is obvious: the closer storage is, the faster you can work. Computer systems face exactly the same problem, and their solution — the memory hierarchy — is the subject of this lecture.In the early 1980s, processors and memory were roughly in balance. A CPU took about the same time to execute an instruction as DRAM took to deliver a word of data. An <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Intel_8086" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Intel_8086" target="_self">Intel 8086</a> running at 5 MHz had a clock period of 200 ns, and DRAM of that era responded in roughly the same time — the two were well matched. Then two trajectories diverged sharply.Processor clock speeds improved at roughly 50–60% per year through the 1990s, driven by Moore's Law and aggressive microarchitectural innovation. DRAM latency, constrained by the physics of capacitors and sense amplifiers, improved at only about 7% per year. By the year 2000, a CPU could execute hundreds of instructions in the time it took to fetch a single cache line from main memory. The ratio of processor speed improvement to memory speed improvement over the past four decades is staggering: CPU cycle times improved by roughly 1,000× since 1980, while DRAM latency improved by only about 10×.To put this concretely: an Intel 8086 (1978) at 5 MHz waited roughly 1 clock cycle for a DRAM access. A modern processor at 5 GHz — 1,000× faster in clock speed — waits roughly 200–400 clock cycles for the same DRAM access. The memory did get somewhat faster in absolute nanoseconds, but nowhere near enough to keep up with the processor.This widening chasm is known as the processor–memory gap (sometimes called the memory wall). As you learned in L08, it was one of the central challenges that shaped modern computer architecture — and it has not gone away. If anything, the gap is wider today than ever, because even though CPU clock speeds plateaued in the mid-2000s, techniques like out-of-order execution, superscalar pipelines, and multicore parallelism have continued to increase the rate at which processors demand data. The supply side — DRAM latency — has barely budged.<br><img alt="The Processor-Memory Performance Gap — CPU speed grew ~1000× while DRAM latency improved only ~10× over 40 years" src="assets/l11-processor-memory-gap-eng.svg" target="_self" style="width: 650px; max-width: 100%;">To appreciate the magnitude of the problem, consider the real latencies that a modern program encounters when it reaches for data at different levels of the storage hierarchy:<br>These numbers, popularized by Jeff Dean at Google under the title <a data-tooltip-position="top" aria-label="https://gist.github.com/jboner/2841832" rel="noopener nofollow" class="external-link is-unresolved" href="https://gist.github.com/jboner/2841832" target="_self">"Latency Numbers Every Programmer Should Know"</a>, deserve a moment of contemplation. An L1 cache hit takes half a nanosecond — roughly one or two clock cycles on a modern processor. A main memory access takes 100 ns, which is about 200–400 clock cycles depending on clock frequency. During those 200+ cycles, a modern superscalar CPU could have executed hundreds of useful instructions — but instead it sits idle, waiting for data to arrive from DRAM.A single cache miss to main memory therefore costs the CPU approximately 200 wasted cycles. In a tight loop that processes millions of elements, the difference between a program that hits in L1 on every access and one that misses to main memory on even 5% of accesses can be an order-of-magnitude slowdown. This is not a theoretical concern — it is one of the most common performance bottlenecks in real-world software.To make the scale tangible with a human-time analogy: if an L1 cache hit were scaled to take 1 second (like grabbing a book from your desk), then an L2 hit would take 14 seconds (walking to a nearby shelf). A main memory access would take 3 minutes and 20 seconds (going to the library down the hall). Reading from an SSD would take 3.5 days. And a hard drive seek? Over 230 days — almost eight months of waiting for a single piece of data. The hierarchy spans roughly seven orders of magnitude in latency, and programmers ignore these ratios at their peril.And that is just RAM. If data must come from an SSD, the CPU waits for 150 microseconds — long enough to execute roughly 600,000 instructions. From a spinning hard drive, the wait stretches to 10 milliseconds — time enough for 40 million instructions. Understanding where your data lives in this hierarchy is one of the most impactful performance insights a programmer can have.This is not an abstract concern for hardware engineers. As a software developer, the single most common cause of unexpected slowdowns in well-written code is poor cache behavior. Two programs that perform the same computation, on the same data, using the same algorithm, can differ in speed by 10× or more depending on how they access memory. The fast version accesses data in patterns that keep the caches warm; the slow version scatters its accesses across memory, triggering miss after miss.Consider a simple example. Suppose you have a 4096 × 4096 matrix of integers and you need to sum all its elements. Two implementations look almost identical in C:// Version A: row-major traversal
for (int i = 0; i &lt; N; i++) for (int j = 0; j &lt; N; j++) sum += matrix[i][j]; // Version B: column-major traversal
for (int j = 0; j &lt; N; j++) for (int i = 0; i &lt; N; i++) sum += matrix[i][j];
Both produce the same result. Both execute the same number of additions. Yet on a typical modern processor, Version A runs 3–8× faster than Version B. The reason is entirely about cache behavior: C stores matrices in row-major order, so Version A walks through memory sequentially (exploiting spatial locality), while Version B jumps across rows on every access (defeating it). Every jump lands in a different cache line, causing a miss. We will analyze this pattern formally later in the lecture.In Lab P11, you will see this firsthand. Using Linux's perf stat tool on real hardware, you will measure cache hit rates and miss counts for different memory access patterns. You will observe that a simple change — row-major vs. column-major traversal — can cut execution time dramatically, purely because of cache effects. The code is algorithmically identical; the difference is entirely architectural.This lecture gives you the conceptual toolkit to understand, predict, and optimize cache behavior. We will build on the memory hierarchy overview from L08 with a focused deep dive into the machinery of caches:
Memory Hierarchy Recap — a brief review of the pyramid from registers to disk, with concrete numbers for a modern processor.
Locality of Reference — revisiting temporal and spatial locality with new depth and practical examples.
Cache Structure — how a cache is organized internally: sets, ways, tags, and the mapping from memory addresses to cache locations.
Replacement Policies — what happens when the cache is full and new data must be loaded (LRU, pseudo-LRU, random).
Write Policies — how the cache handles writes (write-back vs. write-through, write-allocate vs. no-write-allocate).
Measuring Cache Performance — the AMAT (Average Memory Access Time) formula, extended to multi-level caches.
Cache Coherence and MESI — how multicore processors keep their private caches consistent, and the performance pitfall of false sharing.
By the end of this lecture, you will be able to look at a piece of code and reason about its cache behavior — predicting whether it will be cache-friendly or cache-hostile, and understanding what to change if it is the latter.As you saw in L06 and explored in depth in L08, no single memory technology can simultaneously be fast, large, and cheap. Physics forbids it: faster memory requires more transistors per bit (SRAM uses six; DRAM uses one transistor and one capacitor), which limits density and raises cost. The computer architect's answer is the memory hierarchy — a layered stack of progressively larger, slower, and cheaper storage, designed so that the data the processor needs most urgently is almost always in the fastest layer.The hierarchy, from fastest to slowest, looks like this:Registers → L1 Cache → L2 Cache → L3 Cache → Main Memory (RAM) → SSD → HDD → TapeEach level is typically an order of magnitude slower and an order of magnitude larger than the one above it. Registers hold a few hundred bytes and respond in under a nanosecond. A hard drive holds terabytes and responds in milliseconds. The ratio between top and bottom is roughly 10,000,000:1 in both capacity and latency — in opposite directions.The upper levels (registers through RAM) are volatile — they lose their contents when power is removed. The lower levels (SSD, HDD, tape) are non-volatile — data persists without power. This is why your computer needs both RAM and a hard drive: RAM provides fast working storage while the system is running, while the drive provides permanent storage that survives reboots and power failures.For this lecture, we focus primarily on the top four levels — L1, L2, L3, and RAM — because these are the levels managed automatically by the cache hardware, and their behavior has the most direct impact on program performance.To ground this discussion in reality, here are the cache parameters for a representative modern CPU — the AMD Ryzen 7 7700X (Zen 4 architecture, released 2022):Notice the pattern. Each step down the hierarchy costs roughly 3–10× more latency while providing roughly 10–30× more capacity. The L1 cache is blindingly fast — under one nanosecond — but holds only 64 KB per core (32 KB for instructions, 32 KB for data). The RAM is 100× slower but holds a million times more data.On Intel's side, a 12th-generation Core i7 (Alder Lake) offers a similar structure: 48 KB L1D + 32 KB L1I per performance core, 1.25 MB L2 per core, and 25 MB of shared L3. The exact numbers differ, but the ratios and tradeoffs are universal across all modern x86 processors — and indeed across ARM, RISC-V, and every other architecture in widespread use.A few architectural details deserve emphasis because they affect how we reason about cache behavior later in the lecture:L1 is split into two separate caches. The L1 instruction cache (L1I) holds recently fetched machine instructions; the L1 data cache (L1D) holds recently accessed data operands. This split allows the processor to fetch the next instruction and read or write data simultaneously, without the two streams competing for the same cache port. At the L2 level and below, caches are unified — they hold both instructions and data in a single structure. The split-cache design at L1 is sometimes called a Harvard cache architecture (echoing the Harvard architecture's separate instruction and data memories), even though the underlying main memory uses a single unified address space.L1 and L2 are private to each core. In a multicore processor, every core has its own dedicated L1 and L2 caches. This means that on an 8-core Ryzen 7 7700X, the total L1D capacity across the chip is 8 × 32 KB = 256 KB, and the total L2 capacity is 8 × 1 MB = 8 MB. Each core accesses only its own private caches, so there is no contention between cores at these levels.L3 is shared across all cores. The L3 cache is a single large pool that any core can access. This shared design serves two purposes: it provides a larger backstop for L2 misses (32 MB holds far more data than any single core's 1 MB L2), and it enables efficient inter-core communication. When one core writes data that another core later reads, the shared L3 can supply that data in ~10 ns rather than requiring a ~70 ns trip to main memory. However, sharing also introduces the challenge of cache coherence — keeping all cores' private caches consistent when multiple cores read and write the same addresses. We will devote an entire section to this problem later in the lecture.You might wonder: if SRAM is so fast, why not just build 64 GB of it and skip DRAM entirely? The answer, as L08 discussed, is economics. SRAM uses six transistors per bit; DRAM uses one transistor and one capacitor. At current manufacturing costs, SRAM runs roughly $1,000+ per gigabyte, while DRAM costs about $3–5 per gigabyte. Building 64 GB of SRAM would cost tens of thousands of dollars — and the chip would be physically enormous, which would actually increase latency because electrical signals travel at finite speed. Larger caches are inherently slower than smaller ones, even if both use the same SRAM technology.The hierarchy is therefore not a temporary engineering compromise — it is a fundamental consequence of physics and economics. Every level earns its place by offering the best tradeoff between speed, capacity, and cost for a particular role.The genius of the hierarchy is that it creates an illusion: from the programmer's perspective, memory appears to be almost as fast as L1 cache and almost as large as the hard drive. This illusion works because of locality of reference — the tendency of programs to access the same data (temporal locality) and nearby data (spatial locality) repeatedly over short time periods. The hardware automatically keeps recently and frequently accessed data in the fast upper levels, so most accesses hit in cache and complete quickly.As you saw in L08's AAT (Average Access Time) analysis, a three-level cache hierarchy can reduce effective memory access time from ~100 ns (raw DRAM) to under 2 ns — a 50× improvement — as long as hit rates remain high. The key phrase is "as long as hit rates remain high." When a program's access pattern defeats locality — for instance, by jumping randomly through a data structure much larger than the cache — the illusion breaks down, and the program pays the full DRAM latency on nearly every access.The hierarchy is the foundation. Everything that follows in this lecture — cache organization, replacement policies, write strategies, performance measurement, and coherence protocols — exists to make the hierarchy work efficiently. The next section examines the property of programs that makes caches effective in the first place: locality of reference.<br><img alt="Modern multicore cache topology — private L1i/L1d and L2 per core, shared L3, main memory" src="assets/l11-cache-topology-eng.svg" target="_self" style="width: 650px; max-width: 100%;">In Lecture 8, we introduced the principle of locality as the reason the memory hierarchy works at all. Now we examine locality in much greater depth — analyzing stride patterns, quantifying the performance impact of good versus bad access patterns, and learning how to write cache-friendly code.Temporal locality states that if a memory address has been accessed recently, it is likely to be accessed again in the near future. This is not a hardware feature — it is a pattern that emerges naturally from how programs are structured.Consider the most common sources of temporal locality: Loop counters and induction variables. A for loop reads and updates its counter variable on every iteration. If the loop runs 1,000 times, that single variable is accessed 1,000 times in rapid succession. Accumulators and running totals. A variable like sum in a reduction loop is read and written on every iteration — strong temporal reuse of a single address. Function return addresses. When a function is called repeatedly (think of a helper function invoked inside a loop), its return address and local variables occupy the same stack region over and over. Instruction fetches. The instructions inside a loop body are fetched from the same memory addresses on every iteration. A tight loop of 10 instructions that runs 10,000 times generates 100,000 instruction fetches from just 10 distinct addresses. Caches exploit temporal locality by keeping recently accessed blocks in fast storage. When you access a memory address, the entire cache line containing it is loaded into L1. If you access the same address again within a short window — before the cache line has been evicted — the access is a hit, served at L1 speed (around 1 ns) rather than main memory speed (around 100 ns). The key insight is that caches are most effective when the same data is reused many times before being evicted.Spatial locality states that if a memory address has been accessed, nearby addresses are likely to be accessed soon. Like temporal locality, this is a consequence of program structure rather than a hardware guarantee.The most important sources of spatial locality: Array traversal. Walking through an array element by element is the textbook example. Elements are stored contiguously in memory, so accessing a[0] means a[1], a[2], and so on are physically nearby. Sequential instruction fetch. Unless the program takes a branch, the next instruction to execute sits at the very next memory address. Straight-line code has perfect spatial locality. Struct field access. When a program reads one field of a struct, it often reads other fields shortly afterward. Since struct fields are laid out contiguously in memory, this exhibits spatial locality. Stack operations. Local variables, function arguments, and return addresses are all packed together on the stack. Accessing one typically means accessing its neighbors soon. Caches exploit spatial locality by fetching entire cache lines — typically 64 bytes on modern x86 processors — rather than individual bytes. When you read a single int (4 bytes) from address N, the hardware loads the entire 64-byte block containing that address. If your next access falls within the same block, it is already in the cache. For a sequential array traversal of 4-byte integers, this means the first access to a cache line incurs a miss, but the next 15 accesses are all hits — a hit rate of 93.75% from spatial locality alone.The difference between good and bad spatial locality is not a minor optimization — it can cause order-of-magnitude performance differences. The most instructive example is matrix traversal.<br>In C, two-dimensional arrays are stored in <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Row-_and_column-major_order" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Row-_and_column-major_order" target="_self">row-major order</a>: matrix[0][0], matrix[0][1], matrix[0][2], ..., matrix[0][N-1], matrix[1][0], matrix[1][1], and so on. Elements within the same row are contiguous in memory; elements in the same column are separated by an entire row's worth of data.Consider two ways to sum all elements of an N × N matrix:// Row-major traversal (good spatial locality)
long sum = 0;
for (int i = 0; i &lt; N; i++) for (int j = 0; j &lt; N; j++) sum += matrix[i][j]; // Column-major traversal (poor spatial locality)
long sum = 0;
for (int j = 0; j &lt; N; j++) for (int i = 0; i &lt; N; i++) sum += matrix[i][j];
Both loops compute the same result and perform the same number of additions. But the first version (row-major) is typically 4–8× faster than the second (column-major) for large matrices. Why?Stride analysis explains the difference. The stride of an access pattern is the distance in memory between consecutive accesses. Row-major traversal has stride-1 access: each access is exactly sizeof(int) = 4 bytes after the previous one. A 64-byte cache line holds 16 integers, so after one compulsory miss, the next 15 accesses are all hits. The effective miss rate is 1/16 = 6.25%. Column-major traversal has stride-N access: each access jumps ahead by N × sizeof(int) bytes to the next row. If N is 1,024 (a modest matrix), each access jumps 4,096 bytes — 64 cache lines away. Every single access is a cache miss, because by the time you circle back to the same row, its cache line has long been evicted. The miss rate approaches 100%. Row-major (stride-1): [a00 a01 a02 a03 ... a0,15] ← one cache line, 16 hits [a0,16 a0,17 ... ] Column-major (stride-N): a00 ← miss (load line) a10 ← miss (different line, N elements away) a20 ← miss (different line again) ...all misses until we wrap back to row 0
<br><img alt="Row-major vs column-major matrix traversal — sequential access exploits spatial locality, strided access does not" src="assets/l11-matrix-access-patterns-eng.svg" target="_self" style="width: 650px; max-width: 100%;"><br>This is not an academic curiosity. Numerical libraries like <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Basic_Linear_Algebra_Subprograms" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Basic_Linear_Algebra_Subprograms" target="_self">BLAS</a> and <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/LAPACK" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/LAPACK" target="_self">LAPACK</a> are written in Fortran, which uses column-major order. When C code calls these libraries, the matrix layout mismatch can silently cause performance degradation unless the programmer accounts for it.Understanding locality leads to actionable principles for writing high-performance code.Principle 1: Prefer sequential access patterns. Whenever possible, iterate through data in the order it is stored in memory. For C arrays, this means the rightmost index should vary fastest (row-major convention). For Fortran arrays, it is the opposite.Principle 2: Choose cache-friendly data layouts. The classic tradeoff is array of structs (AoS) versus struct of arrays (SoA):// Array of Structs (AoS) — fields interleaved in memory
struct Particle { float x, y, z, mass; };
struct Particle particles[10000]; // Struct of Arrays (SoA) — each field is contiguous
struct Particles { float x[10000]; float y[10000]; float z[10000]; float mass[10000];
};
If your algorithm processes only the x coordinates (for example, to find the leftmost particle), SoA is far more cache-friendly: the x values are contiguous, and every cache line is fully utilized. With AoS, each x value is separated by y, z, and mass — three-quarters of every cache line is wasted data.Principle 3: Use loop tiling (blocking) for nested loops. When a computation accesses multiple large arrays — such as matrix multiplication — the working set can easily exceed the cache size, causing frequent evictions. Loop tiling (also called blocking) restructures the computation to work on small sub-blocks that fit in cache:// Naive matrix multiply — poor locality for B
for (int i = 0; i &lt; N; i++) for (int j = 0; j &lt; N; j++) for (int k = 0; k &lt; N; k++) C[i][j] += A[i][k] * B[k][j]; // Tiled matrix multiply — B accessed in cache-friendly blocks
for (int ii = 0; ii &lt; N; ii += BLOCK) for (int jj = 0; jj &lt; N; jj += BLOCK) for (int kk = 0; kk &lt; N; kk += BLOCK) for (int i = ii; i &lt; ii + BLOCK; i++) for (int j = jj; j &lt; jj + BLOCK; j++) for (int k = kk; k &lt; kk + BLOCK; k++) C[i][j] += A[i][k] * B[k][j];
By choosing BLOCK so that the sub-blocks of A, B, and C fit in L1 cache simultaneously, the tiled version can be 10–20× faster for large matrices. We will explore this technique hands-on in the upcoming lab on performance measurement.Principle 4: Minimize the working set. Use the smallest data types that suffice (int16_t instead of int64_t when values fit), avoid padding waste in structs, and keep hot data (frequently accessed) separate from cold data (rarely accessed). The smaller your working set, the more of it fits in cache, and the higher your hit rate.The overarching lesson is this: the fastest algorithm on paper is not necessarily the fastest in practice. An algorithm with worse asymptotic complexity but better locality can outperform a theoretically superior algorithm whose access patterns thrash the cache. Modern performance engineering requires thinking about where data lives, not just how many operations you perform.We have been speaking of the cache as a fast storage layer that "keeps recently used data nearby." But how does the hardware actually organize and look up data at nanosecond speed? The answer involves a clever addressing scheme that decomposes every memory address into three fields — tag, index, and offset — and uses them to implement what amounts to a tiny, blazingly fast hash table.The fundamental unit of storage in a cache is the cache line (also called a cache block). Each line contains: Data block: the actual data — typically 64 bytes of contiguous memory copied from main memory. This is always a naturally aligned block: a 64-byte line stores bytes from address A to A + 63, where A is a multiple of 64. Tag bits: the high-order portion of the memory address that identifies which block of main memory currently occupies this line. Since many different memory blocks could map to the same cache location, the tag disambiguates among them. Valid bit: a single bit indicating whether this cache line contains meaningful data. When the system powers on, all valid bits are 0 (the cache is "cold"). A line becomes valid when data is loaded into it. Dirty bit (for write-back caches): indicates that the data in this line has been modified by the CPU but not yet written back to main memory. When this line is eventually evicted, the cache must write the modified data back before discarding it. <br>
Replacement policy bits: in caches where multiple lines can hold data for the same index (associative caches, discussed in the next lecture), additional bits track access history for the eviction policy — for example, <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU)" target="_self">LRU</a> (Least Recently Used) bits. <br><img alt="Anatomy of a cache line — valid bit, dirty bit, tag, LRU bits, and 64-byte data block" src="assets/l11-cache-line-anatomy-eng.svg" target="_self" style="width: 697px; max-width: 100%;">Why 64 bytes? This line size is a carefully tuned tradeoff. Larger lines exploit more spatial locality — more neighboring bytes are prefetched on a miss. But larger lines also mean:
More wasted bandwidth when only a few bytes from the line are actually needed.
Longer transfer time from main memory (though modern burst transfers mitigate this).
Fewer total lines for a given cache size, which can increase conflict misses.
The 64-byte line size has been standard on x86 processors since the Pentium 4 era (early 2000s) and remains the standard today. Some architectures use 128-byte lines (e.g., IBM POWER), while embedded systems sometimes use 32-byte lines. The choice depends on the expected workload's spatial locality characteristics.The central question of cache design is: given a memory address, how does the hardware determine where in the cache to look for it? The answer is a deterministic mapping that splits every memory address into three fields:| Tag (high-order bits) | Index (middle bits) | Offset (low-order bits) |
Each field has a specific purpose:Offset (rightmost bits): identifies the exact byte within a 64-byte cache line. Since a line holds 64 = 2^6 bytes, the offset is 6 bits wide. Values range from 0 (first byte in the line) to 63 (last byte).Index (middle bits): selects which set (or slot) in the cache to examine. If the cache has S sets, the index is log₂(S) bits wide. This is the "hash function" of the cache — it deterministically maps each memory block to one specific location in the cache.Tag (remaining high-order bits): whatever bits remain after the offset and index are allocated. When the hardware checks a cache line, it compares the stored tag against the tag extracted from the requested address. A match means this line holds the requested data.The total number of address bits equals tag_bits + index_bits + offset_bits, which equals the full width of the address space (32 bits for a 32-bit system, or the physical address width on a 64-bit system).Let us work through a specific configuration:
Address space: 32 bits (4 GB addressable memory)
Cache size: 16 KB (16,384 bytes)
Line size: 64 bytes
Organization: direct-mapped (one line per set)
Step 1 — calculate the number of cache lines:Number of lines = Cache size / Line size = 16,384 / 64 = 256 lines
Step 2 — determine the bit widths:Offset bits = log₂(64) = 6 bits (selects byte within line)
Index bits = log₂(256) = 8 bits (selects which cache line)
Tag bits = 32 − 8 − 6 = 18 bits (identifies memory block)
Step 3 — lay out the address:| Tag (18 bits) | Index (8 bits) | Offset (6 bits) |
| 31 ... 14 | 13 ... 6 | 5 ... 0 |
Let us trace through a specific memory address: 0x0000_1A3C.First, convert to binary (32 bits):0x0000_1A3C = 0000 0000 0000 0000 0001 1010 0011 1100
Now partition according to our bit fields:Tag (bits 31-14): 00 0000 0000 0000 0000 = 0x00000 = 0
Index (bits 13-6): 01 1010 00 = 0x68 = 104
Offset (bits 5-0): 11 1100 = 0x3C = 60
This tells us:
The CPU will look in cache line 104 (the index).
It will compare the stored tag against 0x00000.
If they match, the requested byte is at position 60 within the 64-byte data block.
<br><img alt="Address decomposition — 0x0000_1A3C split into tag (18 bits), index (8 bits), and offset (6 bits)" src="assets/l11-address-decomposition-eng.svg" target="_self" style="width: 697px; max-width: 100%;">The beauty of this scheme is that it requires no search. The index bits directly select a specific cache line (or set), and the tag comparison is a simple equality check. This makes cache lookups extremely fast — they can complete within a single clock cycle.But there is a consequence: multiple memory addresses can map to the same index. For example, in our 256-line cache, every address whose bits [13:6] equal 0xA3 maps to line 163 — and there are 2^18 = 262,144 such addresses. Since only one of them can occupy line 163 at a time (in a direct-mapped cache), the others must wait their turn. This is the root cause of conflict misses, which we will address through associativity in the next lecture.When the CPU issues a memory access (say, a load instruction), the cache controller performs the following steps in hardware, typically within 1–4 clock cycles:Step 1: Extract the index bits from the memory address. These bits select which cache set to examine. In our 256-line direct-mapped cache, this means selecting one of 256 lines.Step 2: Read the cache line's metadata. The hardware reads the valid bit and the tag stored at the selected cache line.Step 3: Check the valid bit. If the valid bit is 0, this line has never been loaded — it is a guaranteed miss. The hardware skips the tag comparison and proceeds to fetch the data from the next cache level.Step 4: Compare the tag. If the valid bit is 1, the hardware compares the stored tag with the tag extracted from the requested address. This comparison is performed using a hardware comparator — a dedicated circuit that checks equality of all tag bits in parallel.Step 5a: Cache hit. If the tags match, the data is present in this cache line. The hardware uses the offset bits to select the requested bytes from the 64-byte data block and delivers them to the CPU. The access is complete in L1 latency — typically 1–4 cycles.Step 5b: Cache miss. If the tags do not match (or the valid bit was 0), the requested data is not in this cache level. The cache controller sends the request to the next level (L2), which performs the same lookup process. When the data eventually arrives (from L2, L3, or main memory), it is written into the selected cache line: the data block is filled, the tag is updated to match the new address, and the valid bit is set to 1. If the line previously held different valid data, that old data is evicted (and written back to the next level if the dirty bit is set).The entire process can be summarized as a decision tree:Address → extract index → select set ├─ valid bit = 0? → MISS → fetch from next level └─ valid bit = 1? ├─ tag match? → HIT → use offset to extract bytes └─ tag mismatch? → MISS → fetch, evict old line
A critical point: in a direct-mapped cache (one line per set), there is only one possible location for each memory block. This makes hits very fast (only one tag to check) but makes conflict misses more likely. Associative caches, which we will study in the next lecture, allow multiple lines per set — reducing conflicts at the cost of checking more tags in parallel.Cache misses are the enemy of performance. Every miss costs tens to hundreds of cycles while data is fetched from a slower level. Understanding why misses occur is the first step toward reducing them. Computer architects classify cache misses into three categories, known as the Three Cs:Compulsory misses (also called cold misses or first-reference misses) occur the very first time a memory block is accessed. Since the block has never been in the cache, it cannot possibly be there — the miss is unavoidable, regardless of cache size or organization. Even an infinitely large cache would still suffer compulsory misses.<br>Capacity misses occur when the program's <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Working_set" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Working_set" target="_self">working set</a> — the total set of cache lines it actively uses — exceeds the cache's total capacity. Even if there are no mapping conflicts, the cache simply does not have room for all the data the program needs simultaneously. A block is evicted to make room for another, and when the evicted block is needed again, a capacity miss occurs.Conflict misses (also called collision misses) occur when multiple memory blocks compete for the same cache set. Even if the cache has plenty of empty lines overall, the particular set targeted by the address may be full, forcing an eviction. Conflict misses are an artifact of the cache's mapping function and its limited associativity — a fully associative cache (where any block can go anywhere) has zero conflict misses by definition.The following table summarizes the three categories:In practice, the relative importance of each type depends on the workload: Streaming workloads (reading large data once) are dominated by compulsory misses. No amount of cache can help — the data has never been seen before. Prefetching is the main mitigation. Iterative workloads (repeated passes over a dataset) suffer capacity misses when the dataset exceeds the cache. Loop tiling — the technique from Section 3.4 — directly attacks this by reducing the active working set to a cache-sized block. Pointer-chasing workloads (traversing linked lists, trees, hash tables) often suffer conflict misses because the nodes are scattered unpredictably in memory. Higher associativity and cache-aware memory allocators can help. <br>Understanding the Three Cs gives architects and programmers a framework for diagnosing and fixing performance problems. When a program runs slower than expected, profiling cache miss rates with tools like perf stat (on Linux) or <a data-tooltip-position="top" aria-label="https://developer.apple.com/instruments/" rel="noopener nofollow" class="external-link is-unresolved" href="https://developer.apple.com/instruments/" target="_self">Instruments</a> (on macOS) reveals whether the bottleneck is compulsory, capacity, or conflict misses — and each type suggests a different optimization strategy.When the CPU fetches a block from main memory and places it into the cache, a fundamental question arises: where exactly can that block go? The answer to this question defines the cache's associativity — one of the most important design decisions in any cache hierarchy. Three classic answers give rise to three types of cache organization, each with its own trade-offs between hardware complexity, lookup speed, and miss rate.Imagine a cache with 128 lines and a main memory that contains millions of blocks. When block number 5,000 is fetched, which of the 128 lines should hold it? If there is only one legal destination, the hardware can check that single line quickly — but collisions become inevitable. If the block can go anywhere, collisions vanish — but the hardware must search every line on every access. Cache associativity is the spectrum between these two extremes.In a direct-mapped cache, every memory block maps to exactly one cache line. The mapping is determined by a simple modular formula:cache_line = block_address mod num_lines
For a cache with 128 lines, block 0 maps to line 0, block 1 maps to line 1, … block 127 maps to line 127, block 128 wraps around to line 0, block 129 maps to line 1, and so on.Address decomposition. The CPU splits each memory address into three fields:| tag | index | offset | Offset (low-order bits): identifies the byte within the block. For a 64-byte block, the offset is 6 bits (2⁶ = 64).
Index (middle bits): selects which cache line to check. For 128 lines, this is 7 bits (2⁷ = 128).
Tag (remaining high-order bits): stored alongside the data so the cache can verify that the block in that line is the one we actually want.
Lookup process. On every memory access the cache extracts the index, jumps directly to that one line, and compares the stored tag with the tag from the address. If they match and the valid bit is set — cache hit. Otherwise — cache miss.Advantage: Speed and simplicity. The hardware checks exactly one location, requiring only a single comparator. This makes direct-mapped caches the fastest to access.Disadvantage: Conflict misses. Two blocks that happen to map to the same line cannot coexist in the cache, even if plenty of other lines sit empty. This leads to thrashing — repeated evictions that destroy performance.A pathological trace. Consider a 128-line cache with 64-byte blocks. Blocks 0 and 128 both map to line 0 (since 128 mod 128 = 0). Now execute the following access pattern:Access block 0 → Miss (cold miss, load block 0 into line 0)
Access block 128 → Miss (conflict: evicts block 0, loads block 128)
Access block 0 → Miss (conflict: evicts block 128, loads block 0)
Access block 128 → Miss (conflict: evicts block 0, loads block 128)
...
Every single access is a miss. The hit rate is 0%, even though we are accessing only two blocks and the cache has 128 lines. This is the worst-case scenario for direct-mapped caches — and it happens in real programs more often than you might expect.<br><img alt="Direct-mapped cache — blocks 0, 128, 256 all map to cache line 0, causing conflicts" src="assets/l11-direct-mapped-cache-eng.svg" target="_self" style="width: 650px; max-width: 100%;">At the opposite extreme, a fully associative cache allows any memory block to reside in any cache line. There is no index field in the address — only a tag and an offset:| tag | offset |
Lookup process. On every access, the cache must compare the requested tag against the tags of all lines simultaneously. This requires N parallel comparators for an N-line cache.Advantage: No conflict misses whatsoever. If the cache has room, any block can be stored without evicting something else due to an index collision. The only misses are compulsory (first access to a block) and capacity (the entire cache is full and something must go).Disadvantage: Hardware cost is prohibitive for large caches. A 1,024-line fully associative cache needs 1,024 comparators all firing in parallel — an enormous amount of silicon and power. The comparison latency also grows with cache size.Where it is used. Fully associative organization is reserved for very small, specialized caches where eliminating conflict misses is critical and the line count is tiny. The most prominent example is the Translation Lookaside Buffer (TLB) — a cache for virtual-to-physical address translations that typically holds 32–64 entries. We will explore the TLB in Lecture 12.The set-associative cache is the compromise that dominates modern hardware. The cache is divided into sets, and each set contains W lines (called W-way associative). A memory block maps to exactly one set — but within that set, it can occupy any of the W lines.The mapping formula is:set = block_address mod num_sets
The address decomposes into:| tag | set_index | offset | Set index selects the set (just like the index in direct-mapped, but there are fewer bits because there are fewer sets than lines).
Tag is compared against all W tags in the selected set using W parallel comparators.
Notice how the extremes are special cases:
1-way set-associative = direct-mapped (each "set" has 1 line, so there are as many sets as lines).
N-way set-associative with 1 set = fully associative (all lines are in a single set, so any block can go anywhere).
Modern CPUs use moderate associativity as the sweet spot:
L1 cache: typically 8-way set-associative
L2 cache: typically 8-way set-associative
L3 cache: typically 12-way to 16-way set-associative
A 2-way set-associative example. Let us work through a concrete scenario. Suppose our cache has:
4 sets (set 0 through set 3)
2 lines per set (2-way)
64-byte blocks
Total cache size: 4 × 2 × 64 = 512 bytes. Blocks map to sets via set = block_address mod 4.Block 0 → set 0 Block 4 → set 0 Block 8 → set 0
Block 1 → set 1 Block 5 → set 1 Block 9 → set 1
Block 2 → set 2 Block 6 → set 2 Block 10 → set 2
Block 3 → set 3 Block 7 → set 3 Block 11 → set 3
Now trace through these accesses:Access block 0 → Miss (set 0 empty, load into way 0) Set 0: [Block 0] [ empty ] Access block 4 → Miss (set 0 has space, load into way 1) Set 0: [Block 0] [Block 4] Access block 0 → Hit! (found in set 0, way 0) Set 0: [Block 0] [Block 4]
Blocks 0 and 4 both map to set 0, but they coexist peacefully — each occupying one of the two ways. In a direct-mapped cache, access to block 4 would have evicted block 0, causing the third access to miss. The 2-way design eliminates this conflict.But what happens when a third block maps to the same set?Access block 8 → Miss (set 0 is full — must evict block 0 or block 4) Set 0: [Block 8] [Block 4] (assuming block 0 evicted by replacement policy)
Now the cache must choose a victim — which of the two lines in set 0 to evict. This choice is governed by the replacement policy, which we explore in Section 6.<br><img alt="Associativity comparison — direct-mapped (1-way), 2-way set-associative, and fully associative for 8 cache lines" src="assets/l11-associativity-comparison-eng.svg" target="_self" style="width: 650px; max-width: 100%;">The following table summarizes the key trade-offs across the associativity spectrum:Increasing associativity reduces conflict misses but adds hardware cost and can increase access latency. The sweet spot for general-purpose caches — 4-way to 16-way — has been remarkably stable across several decades of processor design.Cache associativity interacts in a subtle and dangerous way with data structures whose sizes are powers of two. Consider a direct-mapped cache (or any set-associative cache) with sets indexed by the low-order bits of the block address. When your array stride is a power of two that happens to be a multiple of the number of sets, every access lands in the same set — effectively reducing the useful cache to just W lines (the associativity of one set).Stride 256 vs. stride 257. Suppose you iterate over a large array with a stride of 256 elements (each element is an int, so the stride is 1,024 bytes). If the cache has 64 sets with 64-byte blocks, the set index depends on bits 6–11 of the address. A stride of 1,024 bytes means the set index advances by 1,024 / 64 = 16 sets per step. Since 16 divides 64, the access pattern cycles through only 4 unique sets — wasting 60 of the 64 sets entirely. Performance can degrade by a factor of 10× or more compared to a stride of 257 (which is coprime with every power of two and distributes accesses across all sets).// Pathological: stride is a power of two
for (int i = 0; i &lt; N; i += 256) sum += array[i]; // Hits same cache sets repeatedly // Healthy: stride avoids power-of-two alignment
for (int i = 0; i &lt; N; i += 257) sum += array[i]; // Distributes across all sets
Binary search on power-of-two arrays. Binary search is particularly susceptible. On a sorted array of size 2²⁰, the first comparison always hits element 2¹⁹, the second hits 2¹⁸ or 2¹⁹ + 2¹⁸, and so on. These addresses share low-order bits, causing many of them to map to the same small group of cache sets. Measurements show that a binary search over an array of 2²⁰ elements can take around 360 ns per query, while the same search over 2²⁰ + 123 elements — a trivially larger array — takes only about 300 ns per query. The odd size breaks the alignment pattern and spreads accesses more evenly.Practical solutions:
Pad array dimensions. If your 2D array is int matrix[1024][1024], change it to int matrix[1024][1024 + 16]. The extra 16 columns break the alignment and dramatically reduce set conflicts.
Avoid exact power-of-two sizes for frequently searched data structures. Adding a small constant (even just +1) to the array size can eliminate the pathological pattern.
Understand your cache geometry. Knowing the number of sets, block size, and associativity lets you predict which strides will cause trouble.
This pitfall is a vivid illustration of why understanding cache architecture matters for performance — a theme that will continue through our study of memory systems.When a cache miss occurs and new data must be loaded, the cache may need to evict an existing block to make room. In a direct-mapped cache, there is no choice — the index field uniquely determines which line must be overwritten. But in any associative cache (set-associative or fully associative), the hardware must decide which of the W lines in the target set to sacrifice. This decision is governed by the replacement policy.A good replacement policy keeps the blocks most likely to be reused in the near future, minimizing future misses. A poor one discards data you will need again moments later. Let us examine the three most common policies.The Least Recently Used policy evicts the cache line that has gone the longest without being accessed. The core idea is a direct application of temporal locality: if a block has not been touched recently, it is unlikely to be needed soon.Implementation. For a 2-way set-associative cache, LRU tracking is trivial — a single bit per set records which of the two ways was accessed most recently. On an eviction, the hardware picks the other way. For 4-way, the tracking grows to several bits per set to maintain a full ordering of the four ways. For 8-way or higher, maintaining an exact LRU ordering becomes expensive: an 8-way LRU requires tracking 8! = 40,320 possible orderings per set, which is impractical in fast hardware.Pseudo-LRU. Modern processors with 8-way or 16-way caches use approximations of LRU — often called pseudo-LRU or tree-based PLRU. A binary tree of bits narrows the choice to an "approximately least recently used" line in just a few bit checks. This is close enough to true LRU to capture most of the benefit at a fraction of the cost.Strengths. LRU delivers the best average hit rate across a wide variety of workloads. It is the policy against which others are measured.Weakness. LRU can be defeated by scanning patterns — a loop that iterates over a working set slightly larger than the cache. Each pass through the loop evicts the very lines that will be needed on the next pass, resulting in a 0% hit rate. True LRU dutifully evicts the "oldest" line each time, which is exactly the wrong choice for this cyclic access pattern.The First-In, First-Out policy evicts the cache line that was loaded earliest, regardless of whether it has been accessed recently. Think of it as a queue: the first block to enter the set is the first to leave.Implementation. A simple circular pointer per set tracks the next victim. On each eviction, the pointer advances to the next way. This is cheaper than LRU because there is no need to update tracking state on every cache hit — only on replacements.Problem. FIFO ignores reuse entirely. A block that was loaded early but is accessed on every single cycle will be evicted simply because it was first in line. This makes FIFO less effective than LRU for workloads with strong temporal locality.Belady's anomaly. FIFO exhibits a counterintuitive property known as Belady's anomaly: increasing the cache size (adding more lines) can sometimes increase the miss rate. This was first demonstrated by László Bélády in 1969. While the anomaly is more of a theoretical curiosity than a common practical problem, it highlights the fundamental weakness of any policy that ignores access recency. Notably, LRU does not suffer from Belady's anomaly — it is a stack algorithm, meaning that the set of blocks in a cache of size N is always a subset of those in a cache of size N+1.The simplest policy of all: on an eviction, pick a victim at random. No tracking state, no update logic, no age bits — just a hardware random number generator (or a simple LFSR counter) that selects a way.Strengths. Random replacement has zero bookkeeping overhead and is trivially simple to implement. More surprisingly, it performs remarkably well — typically within 10–20% of LRU's hit rate across a broad range of workloads. It also cannot be tricked by pathological access patterns: since the choice is random, no specific sequence of accesses can consistently cause worst-case behavior.Real-world use. ARM Cortex-A series processors have used random replacement in their caches. When the hardware budget is tight and the workload is unpredictable, random is a pragmatic choice.For most general-purpose processors, pseudo-LRU strikes the best balance: it captures the vast majority of LRU's benefit with hardware complexity closer to FIFO. Random remains a compelling option for embedded and mobile designs where transistor budgets are tighter.The replacement policy works hand-in-hand with associativity. Higher associativity gives the replacement policy more candidates to choose from, which reduces conflict misses — but also makes the replacement decision harder. A 16-way cache with pseudo-LRU and a 4-way cache with true LRU may perform similarly for certain workloads. As with most architectural choices, the right answer depends on the constraints: silicon area, power budget, target workload, and how the cache fits into the overall memory hierarchy.When the CPU reads from memory, the cache logic is straightforward: check the cache, return the data on a hit, or fetch from the next level on a miss. Writing, however, introduces a fundamental question that has no single correct answer — and the choice between the two main strategies has shaped the design of every cache hierarchy built in the last four decades.When the CPU executes a store instruction to an address whose block is already in the cache, two copies of the data exist: the copy in the cache and the copy in main memory. The store must update the cache — that is fast and uncontroversial. But when should main memory be updated? Immediately, or later? The answer defines the cache's write policy, and each choice has profound implications for performance, consistency, and hardware complexity.In a write-through cache, every store updates both the cache line and the corresponding location in main memory simultaneously. The principle is simple: after every write, the cache and memory hold the same data. Memory is always consistent.This simplicity has real engineering value. Recovery from a crash is straightforward — memory already reflects all completed writes. Cache coherence in multiprocessor systems is easier because no stale data hides in a cache without a matching copy in memory. Debugging is more predictable because you can inspect memory at any time and trust that it reflects recent program state.The cost is bandwidth. Consider a loop that increments a counter 100 times:for (int i = 0; i &lt; 100; i++) { counter++;
}
With write-through, each of those 100 increments generates a write to main memory — even though only the final value matters to the rest of the system. If main memory takes 200 cycles per write, the loop spends up to 20,000 cycles waiting for memory, even though the computation itself is trivial.Write buffers mitigate this problem. A write buffer is a small FIFO queue (typically 4–16 entries) that sits between the cache and main memory. When the CPU writes, the data goes into the cache and the write buffer simultaneously. The CPU proceeds immediately; the memory controller drains the buffer in the background, sending writes to memory as the bus becomes available. As long as the buffer does not fill up, the CPU never stalls on writes. If the write rate temporarily exceeds the drain rate, however, the buffer fills and the CPU must wait — a phenomenon called write buffer stall.In a write-back cache, stores update only the cache. The cache line is marked as dirty by setting a dedicated bit — the dirty bit — in its metadata. Main memory is not updated at all during the write. The dirty line sits in the cache, potentially accumulating many writes, until it is eventually evicted.When a dirty line must be evicted (to make room for a new block, for example), the cache controller checks the dirty bit. If the line is dirty, it writes the entire block back to memory before replacing it. If the line is clean (not modified since it was loaded), it is simply discarded — the memory already holds a valid copy.The performance advantage is substantial. That same 100-iteration counter loop generates exactly one memory write — when the cache line containing counter is eventually evicted. One write instead of 100. For write-intensive programs, write-back can reduce memory traffic by an order of magnitude compared to write-through.The complexity cost is equally real:
Dirty bit tracking: every cache line requires an extra bit, and the eviction path must handle the "write back then replace" sequence.
Eviction latency: evicting a dirty line takes longer than evicting a clean one because the block must be written to memory first. Some designs use a write-back buffer to absorb evictions without stalling the incoming fill.
Cache coherence: in multicore systems, one core's dirty cache line may contain the only up-to-date copy of some address. Coherence protocols (like MESI, discussed in L08) must handle the case where another core requests data that exists only in a remote core's dirty cache line.
Despite this complexity, write-back is the dominant policy in modern CPUs. Intel Core, AMD Zen, and ARM Cortex-A all use write-back at every cache level. The bandwidth savings are simply too large to ignore in high-performance designs.The write policy also determines what happens on a write miss — a store to an address whose block is not currently in the cache. There are two choices:Write-allocate (also called fetch-on-write): the cache fetches the entire block from memory into the cache first, then performs the write to the cached copy. This is motivated by spatial locality — if the program is writing to one word in a block, it will likely access nearby words soon. Bringing the whole block into the cache prepares for those future accesses.No-write-allocate (also called write-around): the write goes directly to main memory, bypassing the cache entirely. The block is not loaded. This makes sense when writes have poor locality — for example, initializing a large array that will not be read back immediately. Loading such blocks into the cache would just evict more useful data.These miss policies naturally pair with the write policies:
Write-back + write-allocate: the standard combination for modern CPUs. Since write-back avoids sending writes to memory, it makes sense to bring missed blocks into the cache where subsequent writes stay local.
Write-through + no-write-allocate: since writes go to memory anyway, there is less benefit to caching a missed block just because it was written.
These pairings are conventions, not laws — other combinations are technically possible but rarely used in practice.<br><img alt="Write policies compared — write-through updates both cache and memory; write-back writes only to cache with dirty bit" src="assets/l11-write-policies-eng.svg" target="_self" style="width: 650px; max-width: 100%;">The key takeaway: write-through prioritizes simplicity and consistency; write-back prioritizes performance. Modern general-purpose CPUs choose performance, and invest the extra transistors to manage the complexity.In Lecture 8, we introduced the Average Access Time (AAT) formula and saw how adding cache levels dramatically reduces effective memory latency. Now we formalize this tool — known in the architecture community as the AMAT formula (Average Memory Access Time) — and use it to reason quantitatively about cache design trade-offs and software optimization.The fundamental insight of AMAT is that average memory access time is a weighted combination of the fast path (cache hit) and the slow path (cache miss):AMAT = Hit Time + Miss Rate × Miss Penalty
Each term has a precise meaning:
Hit Time: the time to access the cache and determine whether the request is a hit or miss. For a modern L1 data cache, this is typically 1–4 clock cycles (around 1 ns at 3–4 GHz).
Miss Rate: the fraction of memory accesses that are not found in the cache. For a well-designed L1, this is typically 2–10%, depending on the workload.
Miss Penalty: the additional time required to fetch the block from the next level of the hierarchy when a miss occurs. If the next level is L2, this might be 10 cycles; if the next level is main memory, it could be 200 or more cycles.
A simple example makes the power of caching concrete:L1 hit time = 1 cycle
L1 miss rate = 5%
Miss penalty = 100 cycles (fetch from main memory, no L2) AMAT = 1 + 0.05 × 100 = 1 + 5 = 6 cycles
A single cache level with a 95% hit rate transforms a 100-cycle memory into an effective 6-cycle memory — a 16.7× improvement. The formula quantifies what we intuitively know: even a small miss rate is acceptable if it avoids an enormous penalty most of the time.But the formula also reveals something sobering: the 5% miss rate accounts for 83% of the effective access time (5 out of 6 cycles). This is a manifestation of Amdahl's Law applied to memory — performance is dominated by the slow path, weighted by its frequency.In Lecture 8, we classified cache misses into three categories. Now we connect each category directly to AMAT optimization strategies:Compulsory misses (cold misses) occur on the very first access to a block. Every program suffers them because the cache starts empty. Reducing compulsory misses:
Hardware prefetching: the cache controller detects sequential or strided access patterns and fetches blocks before the CPU requests them. This hides the miss penalty rather than eliminating the miss.
Software prefetching: the programmer (or compiler) inserts prefetch instructions that hint to the hardware which addresses will be needed soon.
Larger cache blocks: each block covers more addresses, so fewer compulsory misses occur per unit of data. But larger blocks increase the miss penalty (more data to fetch) and may waste bandwidth on unneeded bytes.
Capacity misses occur when the cache cannot hold all the data the program's working set requires. Blocks are evicted and must be reloaded later. Reducing capacity misses:
Larger caches: more capacity means more of the working set fits. But larger caches are physically bigger, which increases wire delays and raises hit time — a direct AMAT trade-off.
Algorithmic restructuring: techniques like loop tiling (blocking) restructure computation so that the working set at any moment fits in the cache. This is a software optimization with zero hardware cost.
Conflict misses occur in set-associative or direct-mapped caches when multiple active addresses map to the same set, evicting each other even though the cache has unused capacity elsewhere. Reducing conflict misses:
Higher associativity: an 8-way set-associative cache has far fewer conflict misses than a direct-mapped cache of the same size. But higher associativity increases hardware comparator cost and can increase hit time.
Victim caches: a small, fully associative buffer that catches lines evicted from the main cache due to conflicts. If the evicted line is requested again, it is found in the victim cache instead of going to the next level.
The recurring theme: every optimization that reduces the miss rate comes with a cost — usually increased hit time or hardware complexity. The AMAT formula forces us to evaluate these trade-offs quantitatively rather than relying on intuition.Modern processors do not have a single cache — they have a hierarchy: L1, L2, L3, and then main memory. Each level serves as the "next level" for the one above it. The AMAT formula extends naturally by nesting:AMAT = HitTime_L1 + MissRate_L1 × (HitTime_L2 + MissRate_L2 × (HitTime_L3 + MissRate_L3 × MemoryAccessTime))
The miss penalty for each level is the AMAT of the level below it — a recursive structure that captures the entire hierarchy in one equation.Worked example with realistic parameters for a modern desktop CPU:Working from the inside out:L3 miss penalty = 200 cycles (RAM access time)
L3 contribution = 30 + 0.40 × 200 = 30 + 80 = 110 cycles L2 miss penalty = L3 AMAT = 110 cycles
L2 contribution = 10 + 0.20 × 110 = 10 + 22 = 32 cycles L1 miss penalty = L2 AMAT = 32 cycles
AMAT = 1 + 0.05 × 32 = 1 + 1.6 = 2.6 cycles
<br><img alt="Multi-level AMAT calculation — three cache levels reduce 200-cycle RAM to 2.6-cycle effective access" src="assets/l11-amat-multilevel-eng.svg" target="_self" style="width: 650px; max-width: 100%;">The three-level cache hierarchy has transformed a 200-cycle main memory into an effective 2.6-cycle memory system — a 77× improvement. Let us trace how each level contributes:
95% of accesses are served by L1 in 1 cycle.
4% of accesses (the 5% that miss L1, of which 80% hit L2) are served by L2 in 1 + 10 = 11 cycles.
0.6% of accesses (the 1% that miss L2, of which 60% hit L3) are served by L3 in 1 + 10 + 30 = 41 cycles.
0.4% of accesses go all the way to RAM at a cost of 1 + 10 + 30 + 200 = 241 cycles.
Only 0.4% of accesses touch main memory, yet that tiny fraction still contributes 0.004 × 241 ≈ 0.96 cycles to the AMAT — nearly as much as the L1 hit time itself. This is why architects care about every fraction of a percent in miss rates.When discussing multi-level caches, it is essential to distinguish two definitions of miss rate:Local miss rate: the number of misses at level N divided by the number of accesses to level N. This measures how well that particular cache level performs given the traffic it receives.Global miss rate: the number of misses at level N divided by the total number of CPU memory references. This measures the fraction of all CPU requests that reach beyond level N.The relationship is straightforward — global miss rates multiply:Global miss rate at L2 = MissRate_L1 × LocalMissRate_L2
Global miss rate at L3 = MissRate_L1 × LocalMissRate_L2 × LocalMissRate_L3
Using our worked example:The local miss rate of L2 (20%) sounds alarming — one in five accesses to L2 misses. But L2 only receives the 5% of accesses that missed L1, so its global miss rate is just 1%. Local miss rates describe the efficiency of a single cache level; global miss rates describe the overall system behavior.When computing AMAT, the nested formula naturally uses local miss rates. But when estimating memory bandwidth requirements or comparing systems, global miss rates are more informative — they tell you what fraction of CPU operations actually generate traffic to a given level.The AMAT formula has three knobs: hit time, miss rate, and miss penalty. Every cache optimization targets one of these — often at the expense of another. The following table summarizes the major strategies:Notice the fundamental tension: making a cache larger reduces the miss rate but can increase the hit time. The AMAT formula helps resolve such trade-offs quantitatively. For example, doubling the L1 cache might reduce the miss rate from 5% to 3%, but increase the hit time from 1 to 2 cycles:Original: AMAT = 1 + 0.05 × 100 = 6.0 cycles
Larger L1: AMAT = 2 + 0.03 × 100 = 5.0 cycles
In this case, the larger cache wins. But if the hit time increased to 3 cycles:Larger L1 (3-cycle hit): AMAT = 3 + 0.03 × 100 = 6.0 cycles
Now the improvement vanishes. The formula turns intuitive arguments into quantitative decisions.The AMAT formula is not just a hardware designer's tool — it explains why software performance varies dramatically based on memory access patterns. Consider two implementations of matrix multiplication:Naive implementation (row × column):// C[i][j] = sum of A[i][k] * B[k][j]
for (int i = 0; i &lt; N; i++) for (int j = 0; j &lt; N; j++) for (int k = 0; k &lt; N; k++) C[i][j] += A[i][k] * B[k][j];
The access to B[k][j] strides through memory by N elements per iteration (column-major traversal of a row-major array). For a large matrix, each access to B misses in L1, and possibly L2 as well. The effective AMAT for B accesses is high.Tiled (blocked) implementation:for (int ii = 0; ii &lt; N; ii += BLOCK) for (int jj = 0; jj &lt; N; jj += BLOCK) for (int kk = 0; kk &lt; N; kk += BLOCK) for (int i = ii; i &lt; ii + BLOCK; i++) for (int j = jj; j &lt; jj + BLOCK; j++) for (int k = kk; k &lt; kk + BLOCK; k++) C[i][j] += A[i][k] * B[k][j];
By choosing BLOCK so that a BLOCK × BLOCK submatrix of B fits in L1, all inner-loop accesses to B hit the cache. The miss rate drops dramatically, and the AMAT for B approaches the hit time.Both implementations perform exactly the same arithmetic — the same number of multiplications and additions. Yet the tiled version can run 5–10× faster on large matrices, purely because of cache behavior. The AMAT formula explains why: the naive version has an AMAT dominated by the miss penalty (slow), while the tiled version has an AMAT close to the hit time (fast).Connection to Lab P11: In the upcoming lab, you will use Linux perf stat to measure real cache-miss counters on running programs. The tool reports cache-references (total accesses to the last-level cache) and cache-misses (misses at the last level), allowing you to compute empirical miss rates. You will see firsthand how different access patterns produce dramatically different miss rates — and how those miss rates translate directly into performance differences, exactly as the AMAT formula predicts.Every modern CPU is a multicore processor. Each core has its own private L1 cache (and usually a private L2 cache), which means multiple cores can hold copies of the same memory address simultaneously. This creates a fundamental problem: when one core modifies its cached copy, the other cores' copies become stale — and they have no idea.Consider a concrete scenario. Two cores both cache a shared variable counter whose value in main memory is 42:
Core 0 reads counter → gets 42, stores in its L1 cache.
Core 1 reads counter → gets 42, stores in its L1 cache.
Core 0 increments: counter = 43. This write goes to Core 0's L1 cache only.
Core 1 reads counter from its own L1 cache → gets the stale value 42.
Core 1 has silently read an outdated value. There is no exception, no error code, no crash — just a wrong answer. This is the cache coherence problem, and it is arguably worse than a crash because the program continues executing with corrupted data.The hardware must solve this problem transparently. Programmers should not need to manually flush or invalidate caches to get correct behavior. (Note: cache coherence is distinct from memory ordering and memory fences, which deal with the order in which writes become visible. Memory ordering is a more advanced topic beyond this course.)<br><img alt="The cache coherence problem — Core 0 writes 43 to its cache while Core 1 still sees stale value 42" src="assets/l11-coherence-problem-eng.svg" target="_self" style="width: 650px; max-width: 100%;">Hardware designers have developed two main families of coherence protocols:Snooping protocols — Every cache continuously monitors (or "snoops") a shared bus that connects all cores to main memory. When one core performs a write, the bus transaction is visible to all other caches. Each cache controller checks whether it holds a copy of the affected address and, if so, takes appropriate action (invalidating its copy or updating it). Snooping is simple to implement and fast for small core counts, but it has a fundamental scaling limitation: every memory transaction must be broadcast to every cache. As the number of cores grows, the bus becomes a bandwidth bottleneck. Snooping works well up to roughly 32–64 cores.Directory-based protocols — Instead of broadcasting, a centralized directory (typically located at the memory controller or distributed across memory nodes) tracks which cores currently hold copies of each cache line. When a core writes, the directory sends targeted invalidation messages only to the cores that actually have copies. This eliminates the broadcast bottleneck and scales to hundreds or even thousands of cores. The trade-off is added latency for the directory lookup. Modern server processors — Intel Xeon (using UPI interconnect) and AMD EPYC (using Infinity Fabric) — use directory-based protocols.For this course, we focus on snooping-based MESI — the standard coherence protocol used in consumer-grade multicore CPUs.The MESI protocol assigns one of four states to every cache line in every core's private cache. The name "MESI" is simply the initials of these four states: Modified, Exclusive, Shared, and Invalid.Modified (M) — This core has the only cached copy of the line, and the data has been written to (the line is dirty). Main memory holds a stale version. If any other core requests this data, this core is responsible for supplying it — either by writing the line back to memory first, or via a direct cache-to-cache transfer. In informal terms: "I changed it, and nobody else has it."Exclusive (E) — This core has the only cached copy, but the data has not been modified (the line is clean). Main memory is up-to-date. The critical property of Exclusive is that a subsequent local write can transition to Modified silently — with zero bus traffic. In informal terms: "I have the only clean copy."Shared (S) — One or more other cores may also hold copies of this line. All copies are clean and consistent with main memory. The line is effectively read-only: if this core wants to write, it must first broadcast an invalidation to all other sharers. In informal terms: "Others may have it too, and it's read-only for now."Invalid (I) — This cache line holds no valid data. It may have been evicted, never loaded, or invalidated because another core wrote to it. Any access to an Invalid line is a cache miss and requires fetching the data from memory or another cache. In informal terms: "My copy is gone or stale."The following table summarizes the properties of each state:<br><img alt="MESI state diagram — four states with transitions triggered by local reads/writes and snooped bus events" src="assets/l11-mesi-state-diagram-eng.svg" target="_self">Let us walk through five scenarios step by step. We have three cores (Core 0, Core 1, Core 2) and a variable X that initially resides only in main memory — no cache holds a copy.Scenario 1: Core 0 reads X (nobody has it)
Core 0 issues a BusRd (bus read) request for the address of X.
No other cache responds (nobody has X cached).
Memory supplies the data.
Core 0 loads X into its cache in state E (Exclusive) — it knows it is the only holder because no other cache signaled a hit. *Result: Core 0 = E*
Scenario 2: Core 1 reads X (Core 0 has it in Exclusive)
Core 1 issues a BusRd for X.
Core 0's cache controller snoops this BusRd. It holds X in state E, so it responds — either supplying the data directly via cache-to-cache transfer, or allowing memory to respond.
Both Core 0 and Core 1 now have clean copies. Both transition to state S (Shared). *Result: Core 0 = S, Core 1 = S*
Scenario 3: Core 0 writes to X (both have it in Shared)
Core 0 wants to modify X. Since its copy is in state S, it cannot write silently — other caches may have copies.
Core 0 issues a BusUpgr (bus upgrade) — a request to gain exclusive ownership without re-reading the data.
Core 1 snoops the BusUpgr and invalidates its copy: S → I.
Core 0 now has the only copy and transitions S → M (Modified). It performs the write. *Result: Core 0 = M, Core 1 = I*
Scenario 4: Core 2 reads X (Core 0 has it in Modified)
Core 2 issues a BusRd for X.
Core 0 snoops this BusRd. Its copy is in state M (dirty) — main memory is stale. Core 0 must intervene.
Core 0 writes the dirty data back to main memory (or supplies it directly to Core 2 via cache-to-cache transfer, depending on the implementation).
Now the data is clean. Both Core 0 and Core 2 hold valid copies and transition to state S. *Result: Core 0 = S, Core 2 = S*
Scenario 5: Core 1 writes to X (Core 0 has it in Shared)
Core 1 does not currently have a valid copy (it was invalidated in Scenario 3). It issues a BusRdX (bus read exclusive) — a combined read-and-invalidate request that says "give me the data AND make sure nobody else has it."
Core 0 snoops the BusRdX and invalidates its copy: S → I.
Core 1 receives the data and loads it in state M (Modified), then performs the write. *Result: Core 0 = I, Core 1 = M*
These five scenarios cover the most common transitions. Notice how every write by one core forces invalidation of all other cached copies — this is the fundamental mechanism that keeps caches coherent.A simpler protocol called MSI uses only three states: Modified, Shared, and Invalid. It works correctly, but the Exclusive state in MESI provides a crucial performance optimization.In MSI, when a core reads a cache line that nobody else has, it must place the line in state Shared (because MSI has no way to represent "I'm the only clean holder"). Later, when that core writes to the line — a very common pattern, since programs frequently read a value, modify it, and write it back — it must issue a BusUpgr to transition from Shared to Modified, even though no other core actually has a copy.MESI eliminates this unnecessary bus traffic. When a core reads a line and detects (via the lack of snoop responses) that it is the only holder, it places the line in state Exclusive. A subsequent write transitions silently from E → M with zero bus traffic — no broadcast, no invalidation, no wasted bandwidth.This optimization matters enormously in practice because a large fraction of cache lines are accessed by only one core. Thread-local variables, stack frames, and many heap objects are private to a single thread. Without the Exclusive state, every write to these private lines would generate useless bus traffic, competing for bandwidth with the transactions that actually need cross-core communication.MESI with bus snooping is elegant and efficient for consumer CPUs with 4–16 cores, but it has fundamental limitations:Scalability — Because every bus transaction is broadcast to every cache, the bus becomes a bottleneck as core count grows. This is why large server processors (with 64+ cores) use directory-based protocols instead.Extensions — Several extended versions of MESI exist:
MOESI (AMD): adds an Owned state — a dirty line that is shared. The owner supplies data on snooped reads without writing back to memory first, reducing memory bandwidth.
MESIF (Intel): adds a Forward state — one designated sharer responds to snooped reads, preventing multiple caches from all trying to respond simultaneously.
Coherence vs. Consistency — MESI ensures that all cores eventually see the same value for a given address (coherence), but it does not dictate the order in which writes to different addresses become visible across cores. That is the domain of memory consistency models and memory fence instructions (mfence, sfence, lfence on x86) — a topic for advanced architecture courses.The MESI protocol operates at the granularity of cache lines, not individual variables. On virtually all modern x86 processors, a cache line is 64 bytes. This means that when a core writes to any byte within a 64-byte line, the entire line is invalidated in all other cores' caches.Now consider what happens when two threads on different cores access different variables that happen to reside within the same 64-byte cache line, and at least one thread writes. From MESI's perspective, this is indistinguishable from two threads writing to the same variable — the cache line bounces back and forth between cores, triggering the full invalidation-reload cycle every time either thread writes.This phenomenon is called false sharing: the threads are not actually sharing any data, but the hardware treats them as if they are, because the coherence protocol cannot distinguish between bytes within a cache line. It is "false" because the sharing is an artifact of memory layout, not program logic.Suppose we want to parallelize a simple counting task. Each of four threads increments its own counter:#include &lt;pthread.h&gt;
#include &lt;stdio.h&gt; int counters[4]; // 4 ints × 4 bytes = 16 bytes total void* thread_func(void* arg) { int id = *(int*)arg; for (int i = 0; i &lt; 1000000; i++) { counters[id]++; // Each thread writes ONLY to its own element } return NULL;
}
Logically, there is zero sharing — thread 0 only touches counters[0], thread 1 only touches counters[1], and so on. There are no data races, no need for locks, no shared state whatsoever.But physically, the entire counters array occupies just 16 bytes — well within a single 64-byte cache line. Every time any thread writes to its counter, the entire cache line is invalidated in all other cores. With four threads writing in tight loops, the line bounces continuously between four cores. The result is paradoxical: four threads are slower than one thread for a task that has no logical sharing at all.<br><img alt="False sharing — four counters in one cache line cause invalidation bouncing between cores" src="assets/l11-false-sharing-eng.svg" target="_self" style="width: 650px; max-width: 100%;">Real benchmark data illustrates just how devastating false sharing can be. The following measurements come from a controlled experiment:Three observations stand out: False sharing is as bad as true sharing. The false-sharing case (7.78 ms) is virtually identical to the true-sharing case (7.69 ms). The hardware cannot tell the difference — a cache line is a cache line. The L1 miss rate explodes. In the false-sharing case, 40% of L1 accesses are misses, compared to 0.03% for the single-threaded case. Each miss triggers a cross-core transfer costing dozens of cycles. The fix makes it faster than single-threaded. With proper padding, the four-threaded version (1.75 ms) is actually faster than single-threaded (2.35 ms) — close to the ideal 4× speedup. This is the performance we should have gotten all along; false sharing was stealing it. The fix is conceptually simple: ensure that data written by different threads resides on separate cache lines. The most common technique is padding:#include &lt;stdalign.h&gt; struct alignas(64) PaddedCounter { int value; // The remaining 60 bytes are wasted padding, // but they guarantee each counter sits on its own cache line.
}; struct PaddedCounter counters[4]; void* thread_func(void* arg) { int id = *(int*)arg; for (int i = 0; i &lt; 1000000; i++) { counters[id].value++; } return NULL;
}
In C++, the same approach works with alignas or the C++17 std::hardware_destructive_interference_size constant (which equals the cache line size on the target platform):struct alignas(64) AlignedCounter { std::atomic&lt;int&gt; val;
}; // Or using the standard constant:
struct alignas(std::hardware_destructive_interference_size) AlignedCounter { std::atomic&lt;int&gt; val;
};
Alternative solutions include: Thread-local accumulation: Each thread uses a local variable for its counter, then adds the result to a shared total once at the end. This eliminates all cross-core cache traffic during the hot loop. Data structure redesign: Separate "hot" fields (frequently written by different threads) from "cold" fields (rarely modified or read-only). Group hot fields so that each thread's hot data occupies its own cache line. False sharing is invisible in source code — the problem is in the memory layout, not the logic. Specialized tools are needed to detect it.On Linux, the perf c2c (cache-to-cache) tool is purpose-built for this task:$ perf c2c record ./my_program
$ perf c2c report
The key metric to look for is HITM (Hit Modified) — this counts how many times a core's cache load hit a line that was in Modified state in another core's cache, meaning the line had to be transferred between cores. A high HITM count on a particular cache line strongly suggests either true sharing or false sharing. If you examine the addresses and find they belong to different variables, it is false sharing.Intel's VTune Profiler and AMD's uProf also have dedicated false-sharing analysis modes.A useful heuristic: if adding more threads to a parallel program makes it slower (or fails to make it faster), and the threads do not logically share data, false sharing should be your first suspect.False sharing deserves special attention because it violates the programmer's reasonable expectations in four distinct ways: No functional bug — The program produces correct results. Every test passes. The only symptom is poor performance, which is easy to dismiss or misattribute. Invisible in source code — You can stare at the code for hours and find nothing wrong. The threads access different variables, there are no data races, and the logic is clean. The problem exists only in the physical memory layout — something that source code does not show. Negates parallelization entirely — As the benchmarks showed, false sharing can make a four-threaded program as slow as (or slower than) a single-threaded one. The entire engineering effort of parallelization — threading, synchronization, testing — is wasted. Only appears under concurrency — Single-threaded testing, which is how most development and debugging is done, cannot reveal false sharing. The bug is latent until the program runs on multiple cores. Understanding cache coherence (Section 9) is what gives you the mental model to predict and diagnose this class of bugs. When you know that MESI operates on 64-byte cache lines and that any write invalidates the entire line in all other caches, false sharing transforms from a mysterious performance anomaly into an obvious consequence of memory layout — and an easy fix.This lecture covered the internal machinery that makes the memory hierarchy work. Here are the essential ideas to carry forward: The memory hierarchy exploits locality to bridge the processor-memory gap. L1 cache is 200× faster than RAM. The hierarchy gives programs the illusion of fast, large memory — but only if access patterns exhibit locality. Temporal locality (reuse recently accessed data) and spatial locality (access nearby data) are the two principles that caches exploit. Row-major array traversal in C is cache-friendly; column-major is not. Cache lines are the unit of transfer — typically 64 bytes. Every access fetches an entire line, and every eviction removes an entire line. Addresses decompose into tag, index, and offset fields. Associativity determines flexibility vs. cost. Direct-mapped caches are simple but prone to conflict misses. Set-associative caches (8–16 way in modern CPUs) offer the best practical balance. Fully associative is used only for tiny specialized caches like the TLB. AMAT = Hit Time + Miss Rate × Miss Penalty. Multi-level caches reduce effective access time dramatically — turning 200-cycle RAM into ~2.6-cycle effective access in a well-designed hierarchy. Write-back with write-allocate is the standard policy in modern CPUs, minimizing memory traffic at the cost of tracking dirty lines. The MESI protocol keeps multicore caches coherent through four states (Modified, Exclusive, Shared, Invalid) and bus snooping. The Exclusive state optimizes the common "read then write" pattern by avoiding unnecessary bus traffic. False sharing is a real-world performance killer — different threads accessing different variables on the same cache line triggers the same invalidation overhead as true sharing. The fix: alignas(64) or thread-local storage. Caches are one level of the memory hierarchy's "locality trick." In L12, we move down the hierarchy to virtual memory — a mechanism that uses the same locality principles but adds a crucial new capability: address translation. You will learn how page tables and the TLB (a fully associative cache — now you know what that means!) enable memory protection, isolation between processes, and the illusion that every program has its own private address space. The interplay between caches and virtual memory — particularly the question of whether the cache uses virtual or physical addresses — ties the two lectures together tightly.What you'll do in P11:
Transition from TASM/DOSBox (16-bit Real Mode) to NASM + Linux (64-bit Protected Mode) with GDB/GEF
Implement matrix multiplication: naive version vs. cache-friendly version (loop tiling/blocking)
Measure cache performance with perf stat: cache-references, cache-misses, L1-dcache-load-misses
Run stride experiments demonstrating spatial locality effects
Analyze miss rates and correlate them with the AMAT formula from this lecture
Tools you'll need:
Linux environment (Codespaces, WSL, or native)
NASM assembler + ld linker
GDB with GEF extension
perf profiling tool <br>
<a data-tooltip-position="top" aria-label="https://diveintosystems.org/book/C11-MemHierarchy/caching.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://diveintosystems.org/book/C11-MemHierarchy/caching.html" target="_self">Dive Into Systems — CPU Caches (Ch. 11.4)</a> — Cache structure, address decomposition, associativity, write policies, and miss categories <br>
<a data-tooltip-position="top" aria-label="https://diveintosystems.org/book/C11-MemHierarchy/mem_hierarchy.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://diveintosystems.org/book/C11-MemHierarchy/mem_hierarchy.html" target="_self">Dive Into Systems — Memory Hierarchy (Ch. 11)</a> — Memory hierarchy overview with speed/capacity/cost tradeoffs <br>
<a data-tooltip-position="top" aria-label="https://diveintosystems.org/book/C11-MemHierarchy/locality.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://diveintosystems.org/book/C11-MemHierarchy/locality.html" target="_self">Dive Into Systems — Locality (Ch. 11)</a> — Temporal and spatial locality with matrix access pattern examples <br>
<a data-tooltip-position="top" aria-label="https://diveintosystems.org/book/C14-SharedMemory/cache_coherence.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://diveintosystems.org/book/C14-SharedMemory/cache_coherence.html" target="_self">Dive Into Systems — Cache Coherence and False Sharing (Ch. 14.5)</a> — MSI protocol, snooping caches, false sharing with benchmarks <br>
<a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/computer-organization-architecture/cache-memory-in-computer-organization/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/computer-organization-architecture/cache-memory-in-computer-organization/" target="_self">GeeksforGeeks — Cache Memory in Computer Organization</a> — Cache mapping techniques (direct, fully associative, set-associative) <br>
<a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/computer-organization-architecture/locality-of-reference-and-cache-operation-in-cache-memory/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/computer-organization-architecture/locality-of-reference-and-cache-operation-in-cache-memory/" target="_self">GeeksforGeeks — Locality of Reference</a> — Locality definitions and AMAT formula <br>
<a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/computer-organization-architecture/cache-coherence-protocols-in-multiprocessor-system/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/computer-organization-architecture/cache-coherence-protocols-in-multiprocessor-system/" target="_self">GeeksforGeeks — Cache Coherence Protocols</a> — MSI, MESI, MOSI, MOESI protocol overview <br>
<a data-tooltip-position="top" aria-label="https://en.algorithmica.org/hpc/cpu-cache/associativity/" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.algorithmica.org/hpc/cpu-cache/associativity/" target="_self">Algorithmica — Cache Associativity</a> — Deep dive into associativity, power-of-two stride pitfalls, performance implications <br>
<a data-tooltip-position="top" aria-label="https://dev.to/sachin_tolay_052a7e539e57/cache-coherence-how-the-mesi-protocol-keeps-multi-core-cpus-consistent-170j" rel="noopener nofollow" class="external-link is-unresolved" href="https://dev.to/sachin_tolay_052a7e539e57/cache-coherence-how-the-mesi-protocol-keeps-multi-core-cpus-consistent-170j" target="_self">Cache Coherence: How MESI Keeps Multi-Core CPUs Consistent</a> — MESI state transitions with step-by-step scenarios <br>
<a data-tooltip-position="top" aria-label="https://fiveable.me/advanced-computer-architecture/unit-7/cache-replacement-write-policies/study-guide/OYpe2aD5JCvYaxRh" rel="noopener nofollow" class="external-link is-unresolved" href="https://fiveable.me/advanced-computer-architecture/unit-7/cache-replacement-write-policies/study-guide/OYpe2aD5JCvYaxRh" target="_self">Fiveable — Cache Replacement and Write Policies</a> — LRU, FIFO, Random replacement; write-through vs write-back <br>
<a data-tooltip-position="top" aria-label="https://gist.github.com/jboner/2841832" rel="noopener nofollow" class="external-link is-unresolved" href="https://gist.github.com/jboner/2841832" target="_self">Latency Numbers Every Programmer Should Know</a> — Classic reference for memory and I/O latency benchmarks <br>
<a data-tooltip-position="top" aria-label="https://informatecdigital.com/en/CPU-cache-latency-and-how-it-affects-performance/" rel="noopener nofollow" class="external-link is-unresolved" href="https://informatecdigital.com/en/CPU-cache-latency-and-how-it-affects-performance/" target="_self">CPU Cache Latency: L1, L2, L3 and Their Real Impact</a> — Real-world cache latency measurements on modern AMD and Intel processors <br>
<a data-tooltip-position="top" aria-label="https://coffeebeforearch.github.io/2019/12/28/false-sharing-tutorial.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://coffeebeforearch.github.io/2019/12/28/false-sharing-tutorial.html" target="_self">Performance Implications of False Sharing</a> — False sharing benchmarks with C++ code, cache-line alignment solution, perf measurements ]]></description><link>lectures/eng/l11-eng.-memory-hierarchy-and-performance.html</link><guid isPermaLink="false">Lectures/ENG/L11-ENG. Memory Hierarchy and Performance.md</guid><pubDate>Tue, 24 Mar 2026 10:05:27 GMT</pubDate><enclosure url="assets/l11-processor-memory-gap-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l11-processor-memory-gap-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L11-Address-Decomposition]]></title><description><![CDATA[<img src="assets/l11-address-decomposition.svg" target="_self">]]></description><link>assets/l11-address-decomposition.html</link><guid isPermaLink="false">assets/L11-Address-Decomposition.svg</guid><pubDate>Tue, 24 Mar 2026 08:26:03 GMT</pubDate><enclosure url="assets/l11-address-decomposition.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l11-address-decomposition.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L11-Address-Decomposition-ENG]]></title><description><![CDATA[<img src="assets/l11-address-decomposition-eng.svg" target="_self">]]></description><link>assets/l11-address-decomposition-eng.html</link><guid isPermaLink="false">assets/L11-Address-Decomposition-ENG.svg</guid><pubDate>Tue, 24 Mar 2026 08:25:55 GMT</pubDate><enclosure url="assets/l11-address-decomposition-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l11-address-decomposition-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L11-NOTES-ENG. Memory Hierarchy and Performance]]></title><description><![CDATA[Quick reference for Lecture 11. See full materials in L11-ENG. Memory Hierarchy and Performance.md.The processor-memory gap (memory wall): CPU speed improved ~1,000x since 1980; DRAM latency improved only ~10x. A modern 5 GHz CPU waits 200-400 cycles for a single DRAM access.Key insight: A single cache miss to RAM wastes ~200 CPU cycles. A 5% miss rate can cause 10x slowdown in tight loops.Learn more: <a data-tooltip-position="top" aria-label="https://gist.github.com/jboner/2841832" rel="noopener nofollow" class="external-link is-unresolved" href="https://gist.github.com/jboner/2841832" target="_self">Latency Numbers Every Programmer Should Know</a>Hierarchy: Registers -&gt; L1 -&gt; L2 -&gt; L3 -&gt; RAM -&gt; SSD -&gt; HDD -&gt; Tape. Each level ~10x slower and ~10x larger.Topology: L1 split into L1I + L1D. L1/L2 private per core. L3 shared across all cores. SRAM costs ~$1,000+/GB vs DRAM ~$3-5/GB — hierarchy is a physics + economics necessity, not a compromise.<br>Learn more: <a data-tooltip-position="top" aria-label="https://diveintosystems.org/book/C11-MemHierarchy/mem_hierarchy.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://diveintosystems.org/book/C11-MemHierarchy/mem_hierarchy.html" target="_self">Dive Into Systems — Memory Hierarchy</a>
Temporal locality: Recently accessed data is likely to be accessed again soon (loop counters, accumulators, instruction fetches)
Spatial locality: If an address was accessed, nearby addresses will be accessed soon (array traversal, struct fields, stack)
Cache exploitation: A 64-byte cache line holding 16 ints — first access misses, next 15 are hits (93.75% hit rate from spatial locality alone).
Row-major (stride-1): matrix[i][j] with outer i — 6.25% miss rate, 4-8x faster
Column-major (stride-N): matrix[i][j] with outer j — ~100% miss rate
C stores arrays in row-major order; Fortran uses column-major.
Sequential access — rightmost index varies fastest in C
SoA over AoS — struct of arrays for single-field processing
Loop tiling — sub-blocks fitting in cache; 10-20x speedup for matrix multiply
Minimize working set — smallest data types; separate hot from cold data
<br>Learn more: <a data-tooltip-position="top" aria-label="https://diveintosystems.org/book/C11-MemHierarchy/locality.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://diveintosystems.org/book/C11-MemHierarchy/locality.html" target="_self">Dive Into Systems — Locality</a>| Tag (high bits) | Index (middle bits) | Offset (low bits) |
Worked example — 32-bit address, 16 KB direct-mapped cache, 64 B lines:Lines = 16384 / 64 = 256
Offset = 6 bits | Index = 8 bits | Tag = 18 bits
Lookup: Extract index -&gt; select set -&gt; check valid bit -&gt; compare tag -&gt; HIT (use offset) or MISS (fetch from next level). No search — O(1) via direct indexing.<br>Learn more: <a data-tooltip-position="top" aria-label="https://diveintosystems.org/book/C11-MemHierarchy/caching.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://diveintosystems.org/book/C11-MemHierarchy/caching.html" target="_self">Dive Into Systems — CPU Caches</a>Modern CPUs: L1 = 8-way, L2 = 8-way, L3 = 12-16 way. Special cases: 1-way = direct-mapped; N-way with 1 set = fully associative.Direct-mapped pathology: Two blocks mapping to the same line thrash at 0% hit rate even with 128 empty lines.Power-of-two pitfall: Strides that are multiples of the set count hit the same sets repeatedly. Fix: pad dimensions (e.g., matrix[1024][1024+16]).<br>Learn more: <a data-tooltip-position="top" aria-label="https://en.algorithmica.org/hpc/cpu-cache/associativity/" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.algorithmica.org/hpc/cpu-cache/associativity/" target="_self">Algorithmica — Cache Associativity</a>Pseudo-LRU: Approximation for 8+ ways using a binary tree of bits. Belady's anomaly: FIFO only — more cache can mean more misses. LRU is immune (stack algorithm).<br>Learn more: <a data-tooltip-position="top" aria-label="https://fiveable.me/advanced-computer-architecture/unit-7/cache-replacement-write-policies/study-guide/OYpe2aD5JCvYaxRh" rel="noopener nofollow" class="external-link is-unresolved" href="https://fiveable.me/advanced-computer-architecture/unit-7/cache-replacement-write-policies/study-guide/OYpe2aD5JCvYaxRh" target="_self">Fiveable — Cache Replacement and Write Policies</a>Write-back advantage: 100 increments = 1 memory write (on eviction) instead of 100. Write buffers mitigate write-through stalls.Write miss: Write-allocate (fetch block, then write) pairs with write-back. No-write-allocate (write directly to memory) pairs with write-through.<br>Learn more: <a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/computer-organization-architecture/cache-memory-in-computer-organization/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/computer-organization-architecture/cache-memory-in-computer-organization/" target="_self">GeeksforGeeks — Cache Memory in Computer Organization</a>AMAT = Hit Time + Miss Rate x Miss Penalty
Single-level example: 1 + 0.05 x 100 = 6 cycles (16.7x improvement). The 5% miss rate accounts for 83% of effective access time.Multi-level extension:AMAT = HitTime_L1 + MissRate_L1 x (HitTime_L2 + MissRate_L2 x (HitTime_L3 + MissRate_L3 x MemoryTime))
Result: 1 + 0.05 x (10 + 0.20 x (30 + 0.40 x 200)) = 2.6 cycles (77x improvement over raw RAM).Global = product of local miss rates: L2 global = 5% x 20% = 1%, L3 global = 1% x 40% = 0.4%. Only 0.4% of accesses reach RAM, yet they contribute ~0.96 cycles to AMAT.Optimization trade-offs: Larger cache reduces capacity misses but slows hit time. Higher associativity reduces conflict misses but adds comparators. Larger blocks reduce compulsory misses but increase transfer time. Every knob has a cost — AMAT quantifies the trade-off.<br>Learn more: <a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/computer-organization-architecture/locality-of-reference-and-cache-operation-in-cache-memory/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/computer-organization-architecture/locality-of-reference-and-cache-operation-in-cache-memory/" target="_self">GeeksforGeeks — Locality of Reference</a>Multiple cores cache the same address. Core 0 writes, but Core 1 still sees the stale value — no crash, just a silently wrong answer. Hardware must solve this transparently.Snooping (broadcast, up to ~32-64 cores) vs Directory (targeted, scales to hundreds). Consumer CPUs use snooping; servers use directory.
Read, nobody has it -&gt; load in state E (exclusive clean copy)
Another core reads -&gt; both transition to S (shared)
Write while in S -&gt; issue BusUpgr, invalidate others -&gt; M
Another core reads line in M -&gt; write back dirty data, both -&gt; S
Write miss -&gt; issue BusRdX (read + invalidate) -&gt; load in M
Why E matters: Without it (MSI protocol), every write to private data generates bus traffic. E -&gt; M is silent (zero bus traffic) — critical for stack frames, thread-local variables.Extensions: MOESI (AMD) adds Owned state; MESIF (Intel) adds Forward state.<br>Learn more: <a data-tooltip-position="top" aria-label="https://dev.to/sachin_tolay_052a7e539e57/cache-coherence-how-the-mesi-protocol-keeps-multi-core-cpus-consistent-170j" rel="noopener nofollow" class="external-link is-unresolved" href="https://dev.to/sachin_tolay_052a7e539e57/cache-coherence-how-the-mesi-protocol-keeps-multi-core-cpus-consistent-170j" target="_self">Cache Coherence: MESI Protocol</a> | <a data-tooltip-position="top" aria-label="https://diveintosystems.org/book/C14-SharedMemory/cache_coherence.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://diveintosystems.org/book/C14-SharedMemory/cache_coherence.html" target="_self">Dive Into Systems — Cache Coherence</a>Two threads on different cores write to different variables on the same 64-byte cache line. MESI invalidates the entire line — performance identical to true sharing.Fix: alignas(64) to place each variable on its own cache line:struct alignas(64) PaddedCounter { int value; };
struct PaddedCounter counters[4]; // each on separate cache line
Alternatives: thread-local accumulation, separate hot/cold fields.Detection: perf c2c on Linux (high HITM counts = false sharing). Heuristic: more threads = slower with no logical sharing.Why dangerous: Correct results (no functional bug), invisible in source code, negates parallelization, only appears under concurrency.<br>Learn more: <a data-tooltip-position="top" aria-label="https://coffeebeforearch.github.io/2019/12/28/false-sharing-tutorial.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://coffeebeforearch.github.io/2019/12/28/false-sharing-tutorial.html" target="_self">Performance Implications of False Sharing</a>
Memory hierarchy bridges the processor-memory gap — L1 is 200x faster than RAM
Temporal + spatial locality make caches effective — row-major traversal in C is cache-friendly
Cache lines (64 B) are the unit of transfer — addresses decompose into tag | index | offset
Set-associative caches (8-16 way) balance conflict avoidance vs hardware cost
AMAT = Hit Time + Miss Rate x Miss Penalty — multi-level caches turn 200-cycle RAM into ~2.6-cycle effective access
Write-back + write-allocate is the modern standard — minimizes memory traffic
MESI keeps multicore caches coherent — E state optimizes private data writes
False sharing — different variables on the same cache line thrash performance; fix with alignas(64)
]]></description><link>lectures/eng/l11-notes-eng.-memory-hierarchy-and-performance.html</link><guid isPermaLink="false">Lectures/ENG/L11-NOTES-ENG. Memory Hierarchy and Performance.md</guid><pubDate>Mon, 23 Mar 2026 22:06:38 GMT</pubDate></item><item><title><![CDATA[L11-AMAT-Multilevel]]></title><description><![CDATA[<img src="assets/l11-amat-multilevel.svg" target="_self">]]></description><link>assets/l11-amat-multilevel.html</link><guid isPermaLink="false">assets/L11-AMAT-Multilevel.svg</guid><pubDate>Mon, 23 Mar 2026 20:13:18 GMT</pubDate><enclosure url="assets/l11-amat-multilevel.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l11-amat-multilevel.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L11-AMAT-Multilevel-ENG]]></title><description><![CDATA[<img src="assets/l11-amat-multilevel-eng.svg" target="_self">]]></description><link>assets/l11-amat-multilevel-eng.html</link><guid isPermaLink="false">assets/L11-AMAT-Multilevel-ENG.svg</guid><pubDate>Mon, 23 Mar 2026 20:13:02 GMT</pubDate><enclosure url="assets/l11-amat-multilevel-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l11-amat-multilevel-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L10-NOTES-ENG. Overview of Modern Architectures]]></title><description><![CDATA[Quick reference for Lecture 10. See full materials in L10-ENG. Overview of Modern Architectures.md.Core insight: The end of Dennard scaling (~2004) and the power wall (~4 GHz plateau) forced the industry beyond multi-core (limited by Amdahl's Law) toward domain-specific architectures — hardware that does less but does it in fewer cycles and at lower energy per operation.Fundamental trade-off: Generality vs. efficiency — a general-purpose CPU wastes transistors on flexibility; a specialized chip devotes every transistor to its target workload.Learn more: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Dennard_scaling" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Dennard_scaling" target="_self">Dennard scaling (Wikipedia)</a> | <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Amdahl%27s_law" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Amdahl%27s_law" target="_self">Amdahl's Law (Wikipedia)</a>What: Processors optimized for real-time, deterministic signal processing (ADC → process → DAC pipeline).Key architectural features:
Modified Harvard architecture — separate instruction/data buses for parallel memory access
Single-cycle MAC (Multiply-Accumulate) — Acc = Acc + (A × B) — the core DSP operation (FIR filters, FFT, convolution, matrix multiply)
Zero-overhead hardware loops — no DEC/JNZ overhead; loop counter managed in dedicated hardware
Saturated arithmetic — overflow clamps to max instead of wrapping (prevents audio clicks, control system errors)
Fixed-point emphasis — integer hardware for MAC = simpler, faster, lower power than FPU
Circular buffers (DAGs) — hardware pointer wraparound for sliding-window algorithms, no boundary checks
DMA — autonomous I/O-to-memory transfers without CPU involvement
SHARC (Super Harvard): Instruction cache frees program bus as 3rd data bus during loops; 6–7 parallel operations per cycle.Performance example: 100-tap FIR filter — ~1000+ cycles on CPU vs ~105 cycles on DSP (≈1 tap/cycle).Applications: Smartphones (echo cancellation, codecs), 5G base stations, radar/sonar, hearing aids, motor control.<br>Learn more: <a data-tooltip-position="top" aria-label="https://www.dspguide.com/ch28/3.htm" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.dspguide.com/ch28/3.htm" target="_self">DSP Architecture Guide</a> | <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Texas_Instruments_TMS320" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Texas_Instruments_TMS320" target="_self">TMS320 (Wikipedia)</a>Design philosophy: Maximize throughput (ops/sec) over latency (time per op). Thousands of simple in-order cores; ~60–70% of CPU die is caches/control, while GPU devotes most transistors to ALUs.SIMT (Single Instruction, Multiple Threads): Programmer writes scalar code; hardware groups threads into warps of 32 executing in lockstep. Warp divergence at branches serializes paths — the key GPU performance pitfall.Memory hierarchy (per SM): Registers ~256 KB (1 cyc) → Shared/L1 128–228 KB (~25 cyc) → L2 40–60 MB (~200 cyc) → Global HBM 16–80 GB (~500 cyc). Specialized cores: Tensor Cores (4×4 matrix FMA, ~30× for ML), RT Cores (ray tracing).SIMD vs SIMT vs SMT: SIMD = rigid vector ops, manual masking (CPU SSE/AVX). SIMT = scalar code, hardware-managed warp divergence (GPU). SMT = fully independent threads, no divergence penalty (CPU Hyper-Threading).CUDA model: Host (CPU) launches kernels on Device (GPU). Hierarchy: Grid → Blocks (≤1024 threads, share SM) → Warps (32) → Threads. Alternatives: OpenCL, ROCm, oneAPI, Metal.<br>Learn more: <a data-tooltip-position="top" aria-label="https://learnopencv.com/modern-gpu-architecture-explained/" rel="noopener nofollow" class="external-link is-unresolved" href="https://learnopencv.com/modern-gpu-architecture-explained/" target="_self">GPU Architecture Guide</a> | <a data-tooltip-position="top" aria-label="https://www.yosefk.com/blog/simd-simt-smt-parallelism-in-nvidia-gpus.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.yosefk.com/blog/simd-simt-smt-parallelism-in-nvidia-gpus.html" target="_self">SIMD/SIMT/SMT Analysis</a> | <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/CUDA" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/CUDA" target="_self">CUDA (Wikipedia)</a>What: Custom silicon for a single fixed function — every transistor dedicated to the target workload.Bitcoin mining progression illustrates the specialization spectrum: CPU (20–30 MH/s) → GPU (500–800 MH/s) → FPGA (similar rate, better efficiency) → ASIC (65 GH/s+, now hundreds of TH/s).Three design approaches:Advantages: 100–10,000× faster than CPU; best perf/watt; small form factor; low cost at volume.Disadvantages: High NRE costs (millions at 7nm/5nm); 12–24 month dev cycle; zero flexibility after fabrication; requires specialized hardware engineers.Ubiquitous examples: Smartphone basebands, camera ISPs, neural engines, SSD controllers, router packet-forwarding, automotive ECUs.<br>Learn more: <a data-tooltip-position="top" aria-label="https://anysilicon.com/ultimate-guide-asic-application-specific-integrated-circuit/" rel="noopener nofollow" class="external-link is-unresolved" href="https://anysilicon.com/ultimate-guide-asic-application-specific-integrated-circuit/" target="_self">Ultimate ASIC Guide</a>What: Reconfigurable hardware — a spatial computing device that is the circuit (no instruction fetch/decode). Function determined by a configuration bitstream loaded into SRAM.Four building blocks: CLBs (Configurable Logic Blocks) → LUTs (Look-Up Tables — k-input SRAM storing any Boolean truth table, typically 6-input) → Programmable Routing Matrix (60–80% of die area) → I/O Blocks. Modern FPGAs add hard IP: BRAM, DSP slices, PLLs, ARM cores.Programming flow: HDL (Verilog/VHDL) → Synthesis → Place &amp; Route → Bitstream → Load to FPGA (minutes–hours vs months for ASIC).Cost-volume crossover: Below ~10,000–100,000 units, FPGA is cheaper; above that, ASIC wins.<br>Learn more: <a data-tooltip-position="top" aria-label="https://learn.sparkfun.com/tutorials/how-does-an-fpga-work/all" rel="noopener nofollow" class="external-link is-unresolved" href="https://learn.sparkfun.com/tutorials/how-does-an-fpga-work/all" target="_self">How Does an FPGA Work?</a>Recap from L09: Compiler packs multiple independent operations into one wide instruction (64–1024 bits). Hardware executes all slots in parallel with no dynamic scheduling. Empty slots filled with NOPs when parallelism is insufficient.Transmeta Crusoe (2000): Internal 128-bit VLIW with Code Morphing Software — dynamic x86-to-VLIW binary translation. Failed as Intel improved x86 power efficiency. Legacy: foreshadowed micro-op translation in modern x86 CPUs.Intel Itanium / IA-64 / EPIC: 128-bit bundles of 3 ops + template bits. Predicated execution, speculative loads, register rotation. Commercial failure (~$10B): compilers couldn't extract enough ILP from server code; code bloat 2–4×; x86-64 made it moot. Discontinued 2021.Where VLIW succeeds — DSPs: TI TMS320C6x packs 8 ops/cycle. DSP workloads have regular loops, predictable data access, abundant compiler-visible parallelism. Also: Qualcomm Hexagon, some older AMD GPU shader architectures.<br>Learn more: <a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/very-long-instruction-word-vliw-architecture/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/very-long-instruction-word-vliw-architecture/" target="_self">VLIW Architecture</a> | <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Transmeta" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Transmeta" target="_self">Transmeta (Wikipedia)</a>Motivation: Neural network inference/training is dominated by matrix multiplications. Data movement energy dwarfs compute (~200× for DRAM access vs 8-bit multiply). TPU eliminates overhead via a systolic array.Systolic array (TPU v1 MXU): 256×256 = 65,536 MACs. Weights stationary; activations flow left-to-right; partial sums accumulate downward. Each value reused 256×. 90% of die is compute (vs ~30% for GPU).Reduced precision: INT8 (v1), BFloat16 (v2+), FP8 (v6+). 8-bit multiplier = ~1/16 area, ~1/30 energy of FP32. Neural nets tolerate the rounding.Tesla DOJO D1: 354 RISC-V cores, 2 GHz, 362 TFLOPS BF16, 440 MB SRAM. No cache coherency or virtual memory — software-managed distributed SRAM. Training Tile: 25 D1 dies, 9 PFLOPS. ExaPod: 120 tiles, 20 EFLOPS. PyTorch-compatible, CUDA-independent.<br>Learn more: <a data-tooltip-position="top" aria-label="https://blog.bytebytego.com/p/how-googles-tensor-processing-unit" rel="noopener nofollow" class="external-link is-unresolved" href="https://blog.bytebytego.com/p/how-googles-tensor-processing-unit" target="_self">How Google's TPU Works</a> | <a data-tooltip-position="top" aria-label="https://www.nextplatform.com/2022/08/23/inside-teslas-innovative-and-homegrown-dojo-ai-supercomputer/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.nextplatform.com/2022/08/23/inside-teslas-innovative-and-homegrown-dojo-ai-supercomputer/" target="_self">Inside Tesla's DOJO</a>Paradigm shift: Compute happens where data lives — no von Neumann bottleneck. Every synapse is both compute and memory. Event-driven: only spiking neurons consume energy.Spiking Neural Networks (SNNs): Neurons accumulate charge (membrane potential), fire a spike when threshold crossed, then reset + leak. Modeled by Leaky Integrate-and-Fire (LIF): V[t] = V[t-1] + Σ(A·w·s) - λ. Learning via STDP (spike-timing-dependent plasticity) — local rule, no backpropagation needed.Intel Loihi 2 (2021): 1M neurons/chip, ~1W, programmable neuron models, open-source Lava framework. Loihi 1: 5,000× energy-delay improvement on sparse coding vs CPU/GPU. Hala Point (2024): 1,152 chips, 1.15B neurons.IBM TrueNorth (2014): 4,096 cores, 1M neurons, 256M synapses, ~10W, GALS design (no global clock). NorthPole (2023): 4,000× faster than TrueNorth; 22× faster than GPU at 25× less energy; all weights on-chip.Applications: Autonomous drones, edge AI (always-on keyword detection), robotics, temporal pattern recognition.Limitations: 1–3% accuracy gap vs DNN; no standard tooling (no "CUDA for neuromorphic"); narrow advantage over GPU/TPU for batch workloads.<br>Learn more: <a data-tooltip-position="top" aria-label="https://www.ibm.com/think/topics/neuromorphic-computing" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.ibm.com/think/topics/neuromorphic-computing" target="_self">What Is Neuromorphic Computing? (IBM)</a> | <a data-tooltip-position="top" aria-label="https://open-neuromorphic.org/neuromorphic-computing/hardware/loihi-intel/" rel="noopener nofollow" class="external-link is-unresolved" href="https://open-neuromorphic.org/neuromorphic-computing/hardware/loihi-intel/" target="_self">Intel Loihi</a> | <a data-tooltip-position="top" aria-label="https://open-neuromorphic.org/blog/truenorth-deep-dive-ibm-neuromorphic-chip-design/" rel="noopener nofollow" class="external-link is-unresolved" href="https://open-neuromorphic.org/blog/truenorth-deep-dive-ibm-neuromorphic-chip-design/" target="_self">TrueNorth Deep Dive</a>Three key concepts: Superposition — qubit is α|0⟩ + β|1⟩ (genuinely both states until measured). Entanglement — correlated qubit pairs, measuring one determines the other. Interference — amplitudes of correct answers constructively interfere; wrong answers cancel.Hardware: Superconducting qubits in dilution refrigerators (~15 mK). Alternatives: trapped ions, photonic, topological (Microsoft, early R&amp;D). Gates: Hadamard (superposition), CNOT (entanglement), Phase gates (interference).Key algorithms: Shor's — factors integers in polynomial time, breaks RSA (2048-bit: classical &gt; age of universe; quantum ~ hours). Grover's — unsorted search in O(√N) vs O(N) (1 trillion items: ~1M vs 1T steps). Not universal — no speedup for sorting, sorted search, most graph algorithms.Central challenge: Error correction — qubit gate error ~0.1–1% vs classical ~10⁻¹⁸. One logical qubit may need 100s–1000s of physical qubits. IBM targets 200 logical qubits by 2029. "Harvest now, decrypt later" drives urgent migration to post-quantum cryptography (NIST finalized first standards 2024).<br>Learn more: <a data-tooltip-position="top" aria-label="https://www.ibm.com/think/topics/quantum-computing" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.ibm.com/think/topics/quantum-computing" target="_self">What Is Quantum Computing? (IBM)</a> | <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Shor%27s_algorithm" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Shor%27s_algorithm" target="_self">Shor's algorithm (Wikipedia)</a> | <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Grover%27s_algorithm" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Grover%27s_algorithm" target="_self">Grover's algorithm (Wikipedia)</a>Key patterns: (1) Flexibility and performance inversely correlated. (2) Boundaries blurring — GPUs have tensor cores, CPUs have SIMD, FPGAs include AI blocks. (3) No single architecture wins everywhere → modern systems are heterogeneous. (4) Software ecosystem maturity matters as much as hardware specs.<br>Learn more: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Computer_Architecture:_A_Quantitative_Approach" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Computer_Architecture:_A_Quantitative_Approach" target="_self">Hennessy &amp; Patterson Turing Lecture</a> | <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Dennard_scaling" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Dennard_scaling" target="_self">Dennard scaling (Wikipedia)</a> Specialization beats generality — domain-specific architectures outperform CPUs by orders of magnitude when the workload is well-defined. The design space is a spectrum — from CPU (fully flexible) through FPGA (reconfigurable) to ASIC (fully fixed), with GPU, DSP, VLIW, TPU in between. Parallelism takes many forms — SIMD in DSPs, SIMT in GPUs, systolic arrays in TPUs, spiking networks in neuromorphic chips, quantum superposition. Memory architecture matters as much as compute — Harvard (DSP), HBM (GPU/TPU), distributed SRAM (DOJO), in-memory compute (neuromorphic) all address the von Neumann bottleneck differently. The future is heterogeneous — modern systems combine multiple architecture types; choosing the right accelerator for each workload is as important as writing efficient code. ]]></description><link>lectures/eng/l10-notes-eng.-overview-of-modern-architectures.html</link><guid isPermaLink="false">Lectures/ENG/L10-NOTES-ENG. Overview of Modern Architectures.md</guid><pubDate>Mon, 23 Mar 2026 20:07:52 GMT</pubDate></item><item><title><![CDATA[L10-ENG. Overview of Modern Architectures]]></title><description><![CDATA[
Lecture 10 of Computer Systems Architecture course at NaUKMA.
After this lecture you will be able to:
Explain why specialized processor architectures exist alongside general-purpose CPUs and identify the engineering trade-offs that drive specialization
Describe the key architectural features of DSPs, GPUs, ASICs, FPGAs, VLIW processors, and TPUs, and match each to its target workload
Compare neuromorphic and quantum computing paradigms with classical von Neumann architectures
Evaluate when a domain-specific accelerator is more appropriate than a general-purpose CPU for a given problem
For the first nine lectures of this course, we have focused on one type of processor: the general-purpose CPU. We studied the 8086 and its descendants — their registers, instruction sets, memory addressing, calling conventions, interrupt mechanisms, cache hierarchies, and the design philosophies (CISC vs. RISC) that shaped them. Along the way, we built a mental model of what a processor is: a sequential machine that fetches, decodes, and executes instructions one (or a few) at a time from a unified memory.That model is correct — but it is also incomplete. The general-purpose CPU is the Swiss Army knife of computing: versatile, capable, and good enough for a remarkably wide range of tasks. But "good enough" is not the same as "optimal." When a workload is sufficiently large, sufficiently well-defined, or sufficiently demanding in real-time performance, the jack-of-all-trades design of a general-purpose CPU becomes a liability rather than a strength.The motivation for specialization is rooted in physics. In Lecture 1, we discussed the physical limits of computation — the speed of light, thermodynamic constraints, and the <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Landauer%27s_principle" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Landauer%27s_principle" target="_self">Landauer limit</a>. In Lecture 9, we noted a more immediate limit: the end of <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Dennard_scaling" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Dennard_scaling" target="_self">Dennard scaling</a>. From the 1970s through the early 2000s, each new generation of transistors was smaller, faster, and used less power per unit area. This meant that engineers could increase clock speeds with each generation without proportionally increasing power consumption. Processor frequencies climbed from kilohertz to megahertz to gigahertz at a breathtaking pace.<br>Around 2004–2005, this scaling broke down. Transistors continued to shrink (following <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Moore%27s_law" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Moore%27s_law" target="_self">Moore's Law</a>), but their power density stopped decreasing. Higher clock frequencies now meant more power and more heat — heat that could not be dissipated by practical cooling systems. Clock speeds plateaued near 4 GHz and have remained there for nearly two decades. This is sometimes called the power wall.<br>The industry's first response, as we discussed in Lecture 9, was multi-core parallelism (MIMD). Instead of one faster core, put multiple cores on the same chip and run software in parallel. But multi-core scaling has its own limits: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Amdahl%27s_law" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Amdahl%27s_law" target="_self">Amdahl's Law</a> tells us that the speedup from parallelism is bounded by the sequential fraction of the workload. If even 10% of your program is inherently serial, then no amount of parallel cores will give you more than a 10x speedup.The industry's second response — and the subject of this lecture — is specialization. If you cannot make a general-purpose core faster, you can build a special-purpose core that is dramatically more efficient at a specific class of computation. A GPU cannot run Excel, but it can multiply matrices a thousand times faster than a CPU. A DSP cannot browse the web, but it can filter an audio signal in real time using a fraction of the power. An ASIC cannot be reprogrammed, but it can mine Bitcoin or encode video with an efficiency that no general-purpose processor can match.The architectures we will survey in this lecture span a spectrum from general-purpose to fully specialized:This spectrum reflects a fundamental trade-off in engineering: generality versus efficiency. A general-purpose CPU must support any computation — integer arithmetic, floating-point math, branch-heavy control flow, memory-intensive data structures, I/O handling — and its hardware must accommodate all of these equally. A specialized processor can devote its transistor budget, power envelope, and memory bandwidth entirely to the operations that matter for its target domain, discarding everything else.Consider arithmetic: a CPU's ALU is designed to handle diverse operations — 8-bit, 16-bit, 32-bit, and 64-bit integers; single- and double-precision floating point; bitwise logic; comparisons; shifts. A DSP's multiply-accumulate unit is designed to do one thing superbly: multiply two numbers and add the result to an accumulator, billions of times per second, because that is the fundamental operation of digital signal processing. By narrowing the scope, the DSP achieves efficiency that the CPU cannot match.This lecture surveys eight categories of specialized architecture:
DSP (Digital Signal Processor) — optimized for real-time processing of analog-to-digital signals
GPU (Graphics Processing Unit) — thousands of simple cores for massively parallel computation
ASIC (Application-Specific Integrated Circuit) — custom silicon for a single task
FPGA (Field-Programmable Gate Array) — reconfigurable hardware that bridges software and silicon
VLIW (Very Long Instruction Word) — compiler-driven instruction-level parallelism (a brief review from L09)
TPU (Tensor Processing Unit) — systolic arrays optimized for machine learning
Neuromorphic processors — brain-inspired computing with spiking neural networks
Quantum computing — qubits and superposition for problems beyond classical reach
For each architecture, we will examine the same questions: What problem does it solve? What is its key architectural insight? How does it differ from a general-purpose CPU? And when is it the right tool for the job?This lecture builds directly on the foundations we have established throughout the course: Lecture 6 (System Architecture) introduced the Von Neumann and Harvard architectures. Several specialized processors — especially DSPs — use the Harvard design or variants of it, and we will see why the separation of instruction and data memory is critical for their performance. Lecture 9 (ISA Design and Models of Computation) gave us Flynn's taxonomy (SISD, SIMD, MISD, MIMD) and the concepts of CISC, RISC, and VLIW. We will use Flynn's classification throughout this lecture to position each architecture: DSPs exploit SIMD-like data parallelism, GPUs use a variant called SIMT (Single Instruction, Multiple Threads), TPUs use systolic arrays (a form of MISD/SIMD hybrid), and neuromorphic chips defy Flynn's categories entirely. Lecture 9 also noted that VLIW found its greatest success not in general-purpose computing (where Intel Itanium famously struggled) but in DSPs — a connection we will explore in detail in the next section. By the end of this lecture, you will have a map of the modern computing landscape — a landscape in which the CPU is just one building among many, and choosing the right architecture for a given workload is as important as writing efficient code.We begin our tour of specialized architectures with one of the oldest and most commercially successful: the Digital Signal Processor. DSPs have been shipping in high volume since the early 1980s, and today they are embedded in virtually every device that handles audio, video, or wireless communication — from smartphones and hearing aids to radar systems and 5G base stations. They process billions of dollars worth of signals every year, yet most computer science students have never heard of them.The physical world is analog. Sound is a continuous pressure wave. Light is a continuous electromagnetic field. Radio signals are continuous oscillations. Our sensors — microphones, cameras, antennas — produce continuous electrical signals that vary smoothly over time.But our computers are digital. They operate on discrete numbers, stored as binary patterns of ones and zeros. To process real-world signals digitally, we must first convert them from analog to digital form using an Analog-to-Digital Converter (ADC). The ADC samples the continuous signal at regular intervals (the sampling rate) and quantizes each sample into a discrete numeric value (with a resolution of N bits per sample). After digital processing, a Digital-to-Analog Converter (DAC) can convert the processed digital signal back into analog form — for example, to drive a speaker or an antenna.<br><img alt="DSP signal processing pipeline: microphone → ADC → Digital System → DAC → speaker" src="assets/l10-dsp-adc-dac-diagram.jpeg" target="_self">The DSP pipeline: an analog signal is digitized by an ADC, processed by the DSP, and converted back to analog by a DAC.This ADC → Digital Processing → DAC pipeline is the foundation of all digital signal processing. The "Digital Processing" block in the middle is where the DSP lives.Why not just use a CPU? A general-purpose CPU can certainly perform mathematical operations on sampled data. In fact, for non-real-time processing — editing a WAV file, analyzing seismological data offline, running a batch image filter — a CPU is perfectly adequate. The problem arises when the processing must happen in real time.Consider a digital hearing aid. Sound enters a microphone, is sampled by an ADC at 16,000 samples per second, must be processed (filtered, amplified, noise-reduced), and must emerge from the speaker with no perceptible delay — typically within 5–10 milliseconds. That means the processor must complete all filtering computations on a block of samples before the next block arrives, every single time, without fail. Missing even one deadline means audible glitches, clicks, or silence.Analog does not tolerate delays. A phone call has a round-trip latency budget of about 150 milliseconds before the conversation feels awkward. A radar system tracking incoming objects must process return signals within microseconds to maintain tracking accuracy. A noise-cancelling headphone must analyze and invert ambient sound fast enough that the anti-noise waveform arrives at your eardrum in sync with the original noise — a tolerance measured in microseconds.General-purpose CPUs are powerful, but they are optimized for throughput and flexibility, not for guaranteed latency. A CPU might process a signal-processing algorithm quickly on average, but its performance is unpredictable: cache misses, branch mispredictions, operating system interrupts, and context switches can all introduce variable delays. For hard real-time signal processing, you need a processor whose execution time is deterministic — and whose architecture is tailor-made for the mathematical operations that signal processing demands.That processor is the DSP.The architectural story of the DSP begins with a decision we studied in Lecture 6: the choice between Von Neumann and Harvard architectures.Recall from L06 that the Von Neumann architecture uses a single memory for both instructions and data, accessed through a single bus. This creates the Von Neumann bottleneck — the CPU cannot fetch an instruction and access data simultaneously. The Harvard architecture solves this by providing separate memories and separate buses for instructions and data, allowing parallel access.<br><img alt="DSP Harvard architecture block diagram with separate data and instruction memory" src="assets/l10-dsp-harvard-architecture.png" target="_self">Modified Harvard architecture used in DSPs: separate data memory and instruction memory with independent buses enable simultaneous access.DSPs overwhelmingly use the Harvard architecture or, more precisely, a Modified Harvard architecture. The reason is straightforward: signal processing algorithms are dominated by tight loops that repeatedly load data, perform arithmetic, and store results. If the processor must wait for data access before fetching the next instruction (as in Von Neumann), throughput drops dramatically. With Harvard's separate instruction and data buses, the DSP can fetch the next instruction from program memory while simultaneously loading an operand from data memory — effectively doubling its memory bandwidth for the most common operations.Many DSPs go further, providing multiple data memory banks with separate buses. The Analog Devices SHARC (Super Harvard Architecture Computer) family, for example, has two data memory segments, each with its own bus. This means the processor can perform three simultaneous memory accesses in a single clock cycle: fetch the next instruction from program memory, load one operand from data memory bank 1, and load another operand from data memory bank 2. For algorithms like the FIR filter (which we will examine shortly), this triple-access capability is essential.If you had to describe DSP computation in one operation, it would be the multiply-accumulate (MAC):Accumulator = Accumulator + (A × B)
This single operation — multiply two numbers and add the result to a running total — is the fundamental building block of digital signal processing. It appears everywhere:
FIR filters: the output sample is a weighted sum of input samples: y[n] = Σ (coefficient[k] × input[n-k])
Convolution: the same weighted sum, which is the basis of blurring, sharpening, and edge detection in images
Correlation: comparing two signals for similarity
FFT (Fast Fourier Transform): decomposing a signal into frequency components, built from butterfly operations that are pairs of multiply-add and multiply-subtract
Matrix multiplication: each element of the result is a dot product — a series of MACs
General-purpose CPUs can perform multiply and add operations, but they typically require separate instructions for each: MUL to multiply, then ADD to accumulate. That is two instructions, two decode cycles, and two trips through the pipeline. A DSP has a dedicated hardware MAC unit that performs the multiply and the add in a single clock cycle. This means the most common operation in signal processing takes one cycle instead of two (or more), and the MAC unit is optimized for this specific operation with a wide accumulator to prevent overflow.The SHARC DSP's Math Processing Unit, for instance, contains:
A Multiplier with an 80-bit accumulator (wider than the operands to prevent precision loss during long accumulations)
An ALU for general arithmetic and logic operations
A Barrel Shifter for bit manipulation
Critically, the multiplier and ALU can operate in parallel — the DSP can execute a multiply-accumulate in the multiplier while simultaneously performing an addition or comparison in the ALU. This is a level of functional parallelism that general-purpose CPUs achieve only through complex out-of-order execution logic; in a DSP, it is built into the instruction encoding.Beyond the MAC unit, DSPs incorporate several architectural features specifically designed for the demands of signal processing. Each feature targets a specific bottleneck that would limit performance on a general-purpose CPU.Signal processing is dominated by loops. A 100-tap FIR filter requires 100 multiply-accumulate operations per output sample. On a general-purpose CPU, each iteration of the loop incurs overhead: a counter decrement, a comparison, and a conditional branch. For a tight inner loop, this overhead can consume a significant fraction of total execution time.DSPs implement zero-overhead loops in hardware. The programmer (or compiler) specifies the loop start address, the loop end address, and the iteration count, and the hardware manages the looping entirely — no decrement, no compare, no branch instruction. The loop counter is decremented and checked by dedicated hardware that runs in parallel with the instruction pipeline. When the last instruction of the loop body executes, the program counter is automatically reset to the loop start address. The loop body runs at full speed with zero instructions wasted on loop control.; Pseudocode for a DSP zero-overhead loop
SETUP_LOOP start_addr, end_addr, 100 ; hardware manages the count
start_addr: MAC ACC, DATA1, COEFF ; multiply-accumulate
end_addr: ; hardware auto-loops 100 times
On a general-purpose CPU, the same loop might look like: MOV CX, 100 ; loop counter
loop_start: ; ... MAC equivalent (multiple instructions) ... DEC CX ; overhead: decrement counter JNZ loop_start ; overhead: conditional branch
Those two overhead instructions per iteration (DEC + JNZ) would add 200 extra instructions to a 100-iteration loop. On a DSP, they are eliminated entirely.In standard two's complement arithmetic (the kind a general-purpose CPU uses), numbers that exceed the maximum representable value wrap around. For a signed 16-bit integer, adding 1 to 32,767 (0x7FFF) produces -32,768 (0x8000). This wraparound is mathematically well-defined, but for signal processing it is catastrophic: a brief audio spike that exceeds the maximum value wraps to a large negative value, producing a loud, harsh click — the worst possible artifact.DSPs use saturated arithmetic: when a result exceeds the maximum representable value, it is clamped to the maximum instead of wrapping. When it falls below the minimum, it is clamped to the minimum. For our 16-bit example, 32,767 + 1 = 32,767 (not -32,768).In audio processing, saturation produces a much more graceful degradation — a brief, soft clipping instead of a violent pop. In control systems, saturation prevents a momentary overflow from causing a wildly incorrect output that could damage equipment.Many DSPs are designed to work primarily with fixed-point numbers rather than floating-point. In fixed-point representation, the programmer (or compiler) decides where the "decimal point" sits within a binary word. For example, a Q15 format uses 1 sign bit and 15 fractional bits, representing values from -1.0 to +0.999969. A Q1.15 multiplication of two Q15 values produces a Q30 result (30 fractional bits), which must then be shifted and rounded back to Q15.Why fixed-point? Because fixed-point arithmetic is performed using integer hardware — which is simpler, faster, cheaper, and uses less power than floating-point hardware. A fixed-point MAC unit requires an integer multiplier and an integer adder. A floating-point MAC requires exponent comparison, mantissa shifting, mantissa multiplication, mantissa addition, normalization, and rounding — far more circuitry and far more power. For battery-powered devices like smartphones, hearing aids, and wireless sensor nodes, the power savings of fixed-point DSP are decisive.Modern high-end DSPs (like Texas Instruments' C6000 family) do include floating-point units for applications where the dynamic range of fixed-point is insufficient, but the fixed-point heritage remains central to DSP design philosophy.Signal processing algorithms constantly work with sliding windows of data. A FIR filter, for example, needs access to the most recent N input samples at all times. As each new sample arrives, the oldest sample is discarded, and the new sample takes its place.On a general-purpose CPU, managing this sliding window requires explicit pointer arithmetic and boundary checks: "If the pointer has reached the end of the buffer, wrap it back to the beginning." In a tight inner loop, these checks add instructions and branches.DSPs implement circular buffers in hardware. The programmer configures a buffer with a base address and a length, and the DSP's Data Address Generator (DAG) hardware automatically wraps the pointer when it reaches the end of the buffer. No boundary check, no conditional branch — the modular arithmetic on the pointer happens in the address generation hardware, in parallel with the arithmetic computation.The SHARC DSP provides 8 circular buffers through its two Data Address Generators (4 per DAG). Each DAG can independently compute the next address — including the modular wraparound — while the MAC unit performs its computation. This means that in a single clock cycle, the DSP can:
Execute a multiply-accumulate operation
Update one circular buffer pointer (via DAG1)
Update another circular buffer pointer (via DAG2)
Manage the loop counter (via the hardware loop unit)
All of this happens simultaneously, in parallel, with zero software overhead.DSPs often handle continuous streams of data from the real world — audio samples arriving at 44,100 Hz, radar returns arriving at megahertz rates. Loading this data through the CPU (executing explicit load instructions for each sample) would consume precious instruction cycles that should be spent on computation.DMA (Direct Memory Access) allows data to be transferred between external I/O and DSP memory without CPU intervention. A DMA controller handles the transfer autonomously — the CPU programs the DMA controller with source address, destination address, and transfer size, and then the DMA controller moves the data while the CPU continues executing signal-processing instructions. When the transfer is complete, the DMA controller notifies the CPU via an interrupt.The SHARC DSP at 40 MHz supports two serial ports at 40 Mbit/s each and six parallel ports at 40 MB/s, all managed by DMA. This I/O bandwidth ensures that the processor core is never starved for data and never wastes cycles on data movement.<br>The <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Super_Harvard_Architecture_Single-Chip_Computer" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Super_Harvard_Architecture_Single-Chip_Computer" target="_self">Super Harvard Architecture Computer (SHARC)</a>, developed by <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Analog_Devices" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Analog_Devices" target="_self">Analog Devices</a>, represents the pinnacle of traditional DSP design. It extends the basic Harvard architecture with two key additions that address the remaining bottlenecks:Instruction cache. In a pure Harvard architecture, every instruction must be fetched from program memory every clock cycle. If a tight loop has, say, 5 instructions that repeat 1,000 times, the program memory bus is occupied fetching those same 5 instructions over and over — 5,000 fetches for 5 unique instructions. The SHARC adds a small instruction cache (typically around 32 instructions) that stores recently executed instructions. Once the loop body is cached, the program memory bus is freed for other uses — specifically, it can be repurposed as a third data bus, allowing the processor to access data from program memory in addition to the two data memory banks.This is a crucial innovation: during cached loop execution, the SHARC effectively has three independent data buses. For algorithms that require three operands per cycle (like certain filter structures), this eliminates the final memory bandwidth bottleneck.I/O controller with DMA. As described above, the integrated DMA controller allows autonomous data transfer between external devices and internal memory, freeing the processor core entirely for computation.The combined architecture — Harvard memory separation, instruction cache, dual data buses, DMA — makes the SHARC capable of extraordinary throughput for signal processing. In a single clock cycle, the SHARC can simultaneously:
Fetch and decode one instruction (from cache)
Perform one multiply (in the Multiplier)
Perform one addition (in the ALU)
Move two data values (via the two data buses)
Update two circular buffer pointers (via the two DAGs)
Manage a zero-overhead loop iteration
That is six to seven logically distinct operations per clock cycle — a level of per-cycle throughput that general-purpose CPUs of the same era could only dream of, achieved not through complex out-of-order execution hardware but through a carefully designed architecture where everything the signal-processing algorithm needs happens in parallel by construction.DSP instruction sets look quite different from the x86 or ARM instructions you have studied in this course. They are specialized and highly irregular — optimized for signal processing patterns rather than for general-purpose programming convenience.<br><img alt="Table of DSP instruction functional groups and example mnemonics" src="assets/l10-dsp-instructions-table.png" target="_self">DSP instruction set functional groups with example mnemonics — note the specialized operations like multiply-accumulate (madd) and bit reversal (bitrev).A typical DSP instruction set includes these functional groups:Several things distinguish this from a general-purpose ISA: MAC as a first-class operation. Where x86 requires separate MUL and ADD instructions, a DSP has single instructions that multiply and accumulate in one cycle. Parallel instruction encoding. Many DSP instructions can specify multiple simultaneous operations in a single instruction word. The TMS320C6x family, for example, uses a VLIW-style 256-bit instruction packet that encodes up to 8 parallel operations — making explicit the connection between VLIW (from L09) and DSP design. Hardware repeat instructions. Instructions like RPT (repeat) or RPTB (repeat block) configure the hardware loop unit, replacing the conventional decrement-and-branch loop pattern. Saturating variants of arithmetic. Many arithmetic instructions have saturating modes (or separate saturating variants) so the programmer can choose between wrapping and clamping behavior. Specialized addressing modes. Bit-reverse addressing (used in FFT butterfly computations) and modular addressing (for circular buffers) are built into the instruction set, not implemented in software. To appreciate the efficiency of DSP architecture, consider the Finite Impulse Response (FIR) filter — one of the most common algorithms in signal processing. An FIR filter computes each output sample as a weighted sum of the most recent N input samples:y[n] = c[0]·x[n] + c[1]·x[n-1] + c[2]·x[n-2] + ... + c[N-1]·x[n-N+1]
where c[0] through c[N-1] are the filter coefficients (constants that define the filter's frequency response) and x[n] through x[n-N+1] are the most recent N input samples.This is a dot product of the coefficient vector and the input sample vector — exactly N multiply-accumulate operations.On a general-purpose CPU, a 100-tap FIR filter might require:
100 multiplications (each a multi-cycle operation)
100 additions
200 memory loads (100 coefficients + 100 samples)
Loop overhead (100 decrements + 100 conditional branches)
Even with an optimizing compiler, this might take 1,000 or more clock cycles per output sample.On a DSP like the SHARC, the same 100-tap filter takes approximately 105–110 clock cycles per output sample. Here is why:
The zero-overhead loop eliminates all loop control instructions
Each MAC instruction (one per cycle) simultaneously multiplies, accumulates, loads the next coefficient (via DAG1), and loads the next sample (via DAG2 with circular buffer wrap)
The instruction cache means no instruction fetches compete with data loads
A few extra cycles are needed for loop setup and pipeline priming/draining
The DSP achieves roughly one filter tap per clock cycle — about 10x more efficient than a general-purpose CPU at the same clock frequency, using simpler hardware and consuming less power.The following table summarizes the key architectural differences between a general-purpose CPU and a DSP:Notice that many of these differences are not about raw speed — DSPs typically run at much lower clock frequencies than CPUs. The advantage lies in efficiency per clock cycle and efficiency per watt. A DSP running at 500 MHz can match or exceed the signal-processing throughput of a CPU running at 4 GHz, while consuming a fraction of the power.<br><img alt="Texas Instruments TMS320 DSP chip on a PCB" src="assets/l10-dsp-tms320-chip.jpeg" target="_self">A modern TI TMS320VC5509A DSP chip on a circuit board — one of the most widely used DSP families.DSPs are everywhere, though they are rarely visible to end users. Here is where they work:Audio processing. Every smartphone contains a DSP for echo cancellation, noise reduction, and audio codecs (MP3, AAC, Opus). Professional audio equipment — mixing consoles, effects processors, digital crossovers — relies on DSPs for real-time filtering and equalization. Voice assistants like Siri and Alexa use DSPs for always-on keyword detection ("Hey Siri") at extremely low power.Telecommunications. 4G/5G cellular base stations use DSPs for channel encoding/decoding, beamforming (steering antenna patterns toward specific users), and modulation/demodulation. A single base station may contain dozens of DSPs processing thousands of simultaneous connections.Radar and sonar. Military and civilian radar systems use DSPs for pulse compression, Doppler processing, and target tracking. Automotive radar (used in adaptive cruise control and autonomous driving) uses DSPs to process return signals and identify nearby vehicles and obstacles.<br><img alt="Vintage TI DSP chip in gold ceramic package" src="assets/l10-dsp-chip-vintage.jpeg" target="_self">An early TI DSP chip in a gold ceramic package — DSP architectures have been optimized for signal processing since the 1980s.Medical devices. Hearing aids, cochlear implants, ECG/EEG monitors, and ultrasound imaging systems all depend on DSPs for real-time signal filtering and analysis.Image and video processing. While GPUs dominate high-performance image processing (as we will see in the next section), DSPs handle image processing in power-constrained embedded devices — security cameras, drones, endoscopes.Control systems. Motor drives, power inverters, and industrial automation equipment use DSPs for real-time control loops — reading sensor data, computing PID corrections, and generating drive signals, all within microsecond deadlines.<br><img alt="Close-up of a modern CPU showing pin grid array" src="assets/l10-cpu-closeup.png" target="_self">A general-purpose CPU — powerful and flexible, but less efficient than a DSP for dedicated signal processing tasks.<br>The DSP is arguably the first commercially successful domain-specific architecture. Texas Instruments introduced the <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Texas_Instruments_TMS320" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Texas_Instruments_TMS320" target="_self">TMS32010</a> in 1983, and the TMS320 family became one of the most widely deployed processor architectures in history — not by name recognition (few outside embedded engineering know the TMS320), but by sheer volume. Billions of DSP cores have been manufactured, most of them invisible inside other products.The design principles pioneered by DSPs — Harvard architecture for memory bandwidth, MAC units for throughput, zero-overhead loops for efficiency, saturated arithmetic for signal integrity — influenced the design of every specialized processor that came after. When we examine GPUs, TPUs, and neuromorphic processors in the following sections, we will see echoes of DSP thinking: the same strategy of identifying the core operation of a domain and then building hardware that executes that operation with maximum efficiency.The DSP also demonstrated a principle that the rest of the computing industry would not fully embrace until decades later: the right architecture for the job matters more than raw clock speed. A DSP at 40 MHz in 1995 could process signals that a 200 MHz Pentium could not handle in real time. That lesson — specialization beats generality when the workload is known — is the central theme of this entire lecture.The processors we have examined so far — general-purpose CPUs and signal-optimized DSPs — share a fundamental design assumption: a relatively small number of powerful execution units process instructions as quickly as possible. This design excels at sequential code with complex control flow, branch-heavy logic, and pointer-chasing data access patterns. But there is an entire universe of computation where none of those strengths matter — workloads that consist of millions or billions of identical, independent operations applied to massive datasets. Rendering a frame of a video game, training a neural network, simulating fluid dynamics — these tasks do not need a few fast cores. They need thousands of simple ones.This is the insight behind the Graphics Processing Unit, or GPU. Where a CPU is optimized for latency — completing any single task as quickly as possible — a GPU is optimized for throughput — completing as many tasks as possible per unit of time, even if each individual task takes longer. The result is a radically different architecture, one that has grown from a specialized graphics accelerator into the most important parallel computing platform of the 21st century.The GPU was born from a very specific problem: rendering 3D graphics in real time. A single frame of a modern video game at 4K resolution contains over 8 million pixels. For each pixel, the renderer must determine which 3D object is visible at that point, calculate how light interacts with the object's surface (texturing, shading, reflections), and produce a final color value. At 60 frames per second, that is nearly 500 million pixel calculations every second — and each calculation involves the same sequence of mathematical operations (matrix multiplications, dot products, interpolation) applied independently to different data.<br>This is a textbook case of data parallelism — the exact pattern that SIMD architectures (which we studied in <a class="internal-link" data-href="L09-ENG. ISA Design and Models of Computation.md" href="lectures/eng/l09-eng.-isa-design-and-models-of-computation.html" target="_self" rel="noopener nofollow">Lecture 9</a>, Section 10.1) are designed to exploit. But the scale of parallelism in graphics far exceeds what CPU SIMD extensions like SSE or AVX can handle. A 512-bit AVX-512 register processes 16 floats simultaneously; a modern GPU processes tens of thousands of floats in parallel. The GPU is, in essence, SIMD taken to its logical extreme.<br>The evolution from fixed-function to programmable. Early GPUs (late 1990s) were <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Graphics_pipeline" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Graphics_pipeline" target="_self">fixed-function pipelines</a>: hardwired circuits that performed specific graphics operations — vertex transformation, rasterization, texture mapping — in a predetermined order. Developers could configure parameters but could not change the operations themselves. The NVIDIA GeForce 256 (1999), marketed as the world's first "GPU," could perform hardware transform and lighting (T&amp;L) calculations that previously required the CPU, but its pipeline was rigid.<br>The breakthrough came with programmable shaders. Starting with the GeForce 3 (2001) and accelerating with the GeForce 6 series (2004), GPUs gained the ability to execute arbitrary programs — called <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Shader" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Shader" target="_self">shaders</a> — at various stages of the graphics pipeline. Vertex shaders transformed 3D coordinates; pixel (fragment) shaders computed the final color of each pixel. These programs were written in specialized languages like GLSL (OpenGL Shading Language) or HLSL (High-Level Shading Language for DirectX).The critical realization was that vertex shaders and pixel shaders performed fundamentally similar work: both ran small programs on independent data elements. Rather than having separate hardware for each shader type, GPU architects moved to a unified shader architecture (introduced by ATI's Xpress 200 in 2004 and fully realized in NVIDIA's GeForce 8 series in 2006). In a unified architecture, the same pool of processing cores can execute any type of shader program, dynamically allocating resources based on workload demand. This unification was the architectural foundation that made general-purpose GPU computing (GPGPU) practical.Modern GPU roles. Today, GPUs serve far more than just 3D graphics:
2D acceleration and display output — compositing desktop windows, rendering UI animations, driving multiple monitors from a single framebuffer
Video encoding and decoding — hardware codecs for H.264, H.265/HEVC, AV1, accelerating streaming and video editing
Machine learning and AI — training and inference for neural networks, where the dominant operations (matrix multiplications, convolutions) are massively data-parallel
Scientific computing — molecular dynamics simulations, weather modeling, computational fluid dynamics, financial Monte Carlo simulations
Cryptocurrency mining — exploiting the GPU's raw arithmetic throughput for hash computation (though this application is declining with the shift to proof-of-stake)
GPUs appear in almost every computing device: integrated into mobile phone SoCs (Apple's GPU cores in the A-series and M-series chips, Qualcomm's Adreno, ARM's Mali), as discrete cards in gaming PCs and workstations (NVIDIA GeForce/RTX, AMD Radeon), and as specialized accelerators in data center servers (NVIDIA A100, H100, B200; AMD Instinct MI300X; Intel Data Center GPU Max Series).<br><img alt="Intel Data Center GPU Flex Series PCIe cards" src="assets/l10-gpu-intel-flex-series.jpeg" target="_self">Intel Data Center GPU Flex Series — modern GPUs come as PCIe cards for servers and data centers, not just for gaming.The data center GPU market has grown explosively since 2020, driven by the demands of large language model training. A single training run for a frontier AI model may require thousands of GPUs running in parallel for weeks or months, making GPU architecture one of the most economically significant topics in modern computer engineering.To understand why GPUs look so different from CPUs, recall the fundamental trade-off in processor design: chip area and power can be spent on either making individual operations faster (latency optimization) or performing more operations simultaneously (throughput optimization). CPUs and GPUs make opposite choices.A modern CPU devotes the majority of its transistor budget — roughly 60–70% — to structures that reduce latency for sequential code: large multi-level caches (L1, L2, L3), deep out-of-order execution pipelines, sophisticated branch predictors, speculative execution engines, and complex instruction decoders. The actual arithmetic and logic units (ALUs) that perform computation occupy a surprisingly small fraction of the die area. This design makes sense for general-purpose workloads where the next instruction depends on the result of the previous one, branches are frequent and unpredictable, and data access patterns are irregular.A GPU inverts this allocation. It devotes the vast majority of its transistors to arithmetic units — thousands of simple, in-order cores — and provides only minimal caching and no branch prediction. The logic is straightforward: if your workload consists of millions of independent, identical operations, you do not need deep pipelines or speculative execution. You need raw arithmetic throughput. If one thread stalls waiting for data from memory, the GPU simply switches to another thread — it has thousands of threads in flight at any moment, and switching between them costs essentially nothing (zero-overhead context switching, because all thread states are stored in a massive register file rather than needing to be saved and restored from memory).<br><img alt="CPU vs GPU architecture: 4 large CPU cores versus many small GPU multiprocessors" src="assets/l10-cpu-vs-gpu-cores.jpeg" target="_self">CPU vs GPU core layout: a CPU has a few powerful cores optimized for sequential tasks; a GPU has thousands of simpler cores optimized for parallel throughput.The diagram above illustrates the fundamental structural difference. A CPU has a small number of large, complex cores (4–16 on consumer chips, up to 128 on server processors), each capable of handling diverse, branch-heavy workloads independently. A GPU has a large number of small, simple cores (thousands to tens of thousands), each capable of executing one arithmetic operation per clock cycle but with minimal independent control logic.Data parallelism, not task parallelism. GPUs excel at data parallelism — performing the same operation on many data elements simultaneously. They are not well-suited for task parallelism, where different cores perform fundamentally different operations. In Flynn's taxonomy terms (Lecture 9, Section 8), a GPU is conceptually a SIMD machine on a massive scale — though as we will see in Section 3.6, the precise classification is SIMT (Single Instruction, Multiple Threads), which adds important flexibility beyond pure SIMD.High memory bandwidth. Throughput-oriented computing requires not just many ALUs but also the ability to feed those ALUs with data quickly enough. GPUs therefore use high-bandwidth memory technologies — GDDR6X on consumer cards and HBM (High Bandwidth Memory) on data center GPUs — connected via extremely wide memory buses. The NVIDIA H100, for example, provides approximately 3.35 TB/s of memory bandwidth using HBM3 — roughly ten times the memory bandwidth of a high-end server CPU. Without this bandwidth, the thousands of compute cores would spend most of their time idle, waiting for data.Notice the pattern: the GPU has far less total memory (80 GB vs potentially 1.5 TB) but can access it roughly seven times faster. The GPU has lower clock speeds but compensates with over 100 times more compute cores. And the GPU delivers over 12 times the floating-point throughput at roughly double the power consumption — a significantly better performance-per-watt ratio for data-parallel workloads.The thousands of cores inside a GPU are not arranged as a flat array. They are organized into a hierarchical structure that balances parallelism with the ability to share data and coordinate execution. The primary building block of this hierarchy is the Streaming Multiprocessor (SM) — NVIDIA's term. AMD calls the equivalent unit a Compute Unit (CU), and Intel calls it an Xe Core (or, in older architectures, an Execution Unit cluster). Despite the naming differences, the fundamental concept is the same across all three vendors.<br>CUDA cores. Each SM contains a set of simple processing elements that NVIDIA calls <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/CUDA#Design" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/CUDA#Design" target="_self">CUDA cores</a> (Compute Unified Device Architecture cores). A CUDA core is a streamlined arithmetic unit — far simpler than a CPU core. It can perform one floating-point or integer arithmetic operation per clock cycle (an FMA — fused multiply-add — being the most common). A single CUDA core has no branch predictor, no out-of-order execution logic, no speculative execution — it simply receives an instruction and data, computes the result, and writes it back.A modern NVIDIA SM (as in the Hopper architecture used in the H100) contains 128 CUDA cores. The H100 GPU has 132 SMs, yielding 16,896 CUDA cores in total. An AMD Radeon RX 7900 XTX has 96 Compute Units with 64 stream processors each, for 6,144 total. Intel's Data Center GPU Max 1550 has 128 Xe Cores. The numbers vary, but the principle is universal: many small cores organized into groups.<br><img alt="NVIDIA GPU architecture showing Streaming Multiprocessor hierarchy, L2 cache, and DRAM" src="assets/l10-gpu-nvidia-sm-architecture.png" target="_self">Inside an NVIDIA GPU: each Streaming Multiprocessor (SM) contains stream processors (SP), load/store units, texture units, and on-chip memory. Multiple SMs share an L2 cache connected to DRAM.Thread execution in warps. Within an SM, threads do not execute independently. They are grouped into units called warps (NVIDIA terminology; AMD calls them wavefronts). A warp consists of 32 threads that execute in lockstep — all 32 threads in a warp execute the same instruction at the same time, but each thread operates on its own data. This is the hardware manifestation of the SIMT execution model.An SM does not execute all 128 CUDA cores on the same warp simultaneously. Instead, it contains multiple warp schedulers (typically 4 in modern NVIDIA architectures), each capable of issuing an instruction to a group of 32 cores. At every clock cycle, each warp scheduler selects a ready warp from its pool and dispatches the next instruction for that warp. Because hundreds of warps may be resident on a single SM simultaneously (the H100 can hold up to 64 warps per SM, i.e., 2,048 threads), the scheduler always has ready warps to choose from — even when some warps are stalled waiting for memory.This is the GPU's primary latency-hiding mechanism. A CPU hides memory latency with caches and speculation. A GPU hides it by switching to a different warp while the stalled warp waits. No context switch cost, no cache miss penalty (in the traditional sense) — just a different group of 32 threads picking up where they left off.Warp divergence: the Achilles' heel. The warp model works beautifully when all 32 threads follow the same control flow path. But what happens when threads encounter a branch — an if/else statement — and some threads take the if path while others take the else path?This is called warp divergence, and it is the single most important performance pitfall in GPU programming. When a warp diverges, the hardware cannot execute both paths simultaneously (since all threads in the warp share a single program counter). Instead, it serializes the paths: first, it executes the if path with the else threads masked off (doing no useful work), then it executes the else path with the if threads masked off. Both paths execute, and a mask selects the correct results for each thread.Warp of 32 threads encounters: if (condition) { A } else { B } Threads 0-19: condition = true Threads 20-31: condition = false Step 1: Execute path A — threads 0-19 active, 20-31 masked (idle) Step 2: Execute path B — threads 20-31 active, 0-19 masked (idle) Step 3: Reconverge — all 32 threads resume executing the same instruction
In the worst case — where every thread takes a different path — a warp becomes entirely serial, squandering 31/32 of its potential throughput. This is why GPU-friendly algorithms minimize branching, and why the SIMT model, while more flexible than pure SIMD (Section 3.6), still penalizes divergent control flow.Memory hierarchy. GPUs have a multi-level memory hierarchy optimized for bandwidth rather than latency:<br>The register file in a GPU is enormous compared to a CPU — the H100 has 256 KB of registers per SM, totaling over 33 MB across all SMs. This is not a coincidence. Since GPUs handle thousands of resident threads simultaneously, each thread needs its own set of registers, and these registers must be physically present (not cached or swapped) to enable the zero-cost warp switching that hides memory latency. As <a data-tooltip-position="top" aria-label="https://www.yosefk.com/blog/simd-simt-smt-parallelism-in-nvidia-gpus.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.yosefk.com/blog/simd-simt-smt-parallelism-in-nvidia-gpus.html" target="_self">Yosef Kreinin observes</a>, GPU registers are "more like RAM than registers" — they are a large, flat storage array indexed by thread ID, rather than the small, fast latch-based registers in a CPU.Shared memory is a key architectural feature that distinguishes GPUs from simple SIMD arrays. It is a programmer-managed scratchpad — a small, fast memory bank that all threads within a thread block (Section 3.5) can read from and write to. Shared memory enables threads to collaborate: one thread can compute a partial result, store it in shared memory, and another thread can read it. This cooperative pattern is essential for algorithms like matrix multiplication, where tiles of the input matrices are loaded into shared memory and reused many times.Specialized cores. Modern GPUs include purpose-built hardware beyond general-purpose CUDA/stream cores: Tensor Cores (NVIDIA, introduced with the Volta architecture in 2017) — specialized matrix-multiply units that perform a 4x4 matrix multiply-and-accumulate operation in a single clock cycle. A single Tensor Core operation replaces what would require 64 FMA operations on CUDA cores. The H100's Tensor Cores deliver up to 1,979 TFLOPS of FP8 performance — nearly 30x the throughput of its CUDA cores alone. These are the workhorses of deep learning training and inference. <br>
RT Cores (NVIDIA, introduced with the Turing architecture in 2018) — hardware-accelerated <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Ray_tracing_(graphics)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Ray_tracing_(graphics)" target="_self">ray tracing</a> units that compute ray-bounding volume hierarchy (BVH) intersection tests. Ray tracing — simulating the physical behavior of light — requires testing each ray against thousands of geometric primitives. Doing this in software on CUDA cores is possible but too slow for real-time rendering; RT Cores accelerate the bottleneck operation by orders of magnitude. AI Engines / Matrix Cores — AMD's equivalent to Tensor Cores (in CDNA/RDNA architectures) and Intel's XMX (Xe Matrix eXtensions) serve the same purpose: hardware-accelerated matrix operations for deep learning workloads. The differences between CPUs and GPUs are not merely quantitative (more cores, higher bandwidth) — they reflect fundamentally different design philosophies optimized for different classes of computation. The following table summarizes the key architectural distinctions:The chef analogy. Think of a CPU core as a master chef in a high-end restaurant. The chef can prepare any dish — sushi, pasta, pastries — switching techniques fluidly, adapting to special requests, and improvising when ingredients are missing. But there is only one chef (or a handful in a multi-core CPU).A GPU, by contrast, is like a factory assembly line with 10,000 workers, each trained to perform exactly one step. They cannot improvise, they cannot handle special cases efficiently, and each one is slower than the master chef. But when the task is to produce 10,000 identical meals, the factory finishes in the time it takes the chef to prepare one.Neither is universally better. The question is always: does your workload have enough data parallelism to feed thousands of simple cores?For most of the GPU's early history, the only way to harness its parallel compute power for non-graphics tasks was to disguise the computation as a graphics operation — encoding your data as textures and your algorithm as a shader program. This approach, called GPGPU (General-Purpose computing on GPU), was awkward, error-prone, and limited to problems that could be mapped onto the graphics pipeline.<br>In November 2006, NVIDIA changed the landscape by releasing <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/CUDA" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/CUDA" target="_self">CUDA</a> (Compute Unified Device Architecture) — a parallel computing platform and programming model that allowed developers to write general-purpose programs for the GPU using an extended version of C. For the first time, programmers could directly express parallel algorithms without the graphics pipeline abstraction.CUDA introduced the concept of heterogeneous computing: the CPU (called the host) and the GPU (called the device) work together, each handling the part of the workload it does best. The host runs sequential code, manages I/O, and orchestrates execution. When a data-parallel section of the computation is reached, the host launches a kernel — a function that executes on the GPU across thousands of threads simultaneously.<br><img alt="NVIDIA GPU thread execution architecture: host to thread manager to parallel processing units" src="assets/l10-gpu-thread-execution.jpeg" target="_self">GPU execution model: the host dispatches work through a thread execution manager that distributes threads across parallel processing units, each with local data cache and texture units.The CUDA thread hierarchy. CUDA organizes parallel execution into a three-level hierarchy: Thread — the smallest unit of execution. Each thread runs the same kernel function but operates on different data (identified by its unique thread index). Each thread has its own registers and local memory. Warp — a group of 32 threads that execute in lockstep (as described in Section 3.3). Warps are not explicitly managed by the programmer — they are a hardware implementation detail. But warp-aware programming (avoiding divergence, ensuring coalesced memory access) is critical for performance. Thread block (or simply block) — a group of threads (up to 1,024) that execute on the same SM and can cooperate via shared memory and synchronization barriers (__syncthreads()). All threads within a block are guaranteed to be co-resident on the same SM, which means they can share data through shared memory with low latency. Grid — the collection of all thread blocks launched by a single kernel invocation. Blocks within a grid are independent — they cannot communicate with each other during execution (except through global memory, which is not synchronized). This independence is what allows the GPU hardware to schedule blocks onto SMs in any order, providing scalability across GPUs with different numbers of SMs. Grid
├── Block (0,0) Block (1,0) ... Block (N,0)
│ ├── Warp 0: threads 0–31 ├── Warp 0 ...
│ ├── Warp 1: threads 32–63 ├── Warp 1 ...
│ └── ... └── ... ...
├── Block (0,1) Block (1,1) ...
│ └── ... └── ... ...
└── ...
A conceptual example. Consider adding two arrays of 1 million elements. In CUDA, a programmer would:
Allocate memory on the GPU and copy both input arrays from CPU memory to GPU memory.
Launch a kernel with, say, 1,024 blocks of 1,024 threads each (1,048,576 threads total — one per element).
Each thread computes C[i] = A[i] + B[i] where i is its unique global thread index.
Copy the result array back from GPU memory to CPU memory.
In pseudocode:// Kernel function — runs on the GPU, one instance per thread
kernel vectorAdd(A, B, C, N): i = blockIndex * blockSize + threadIndex if i &lt; N: C[i] = A[i] + B[i] // Host code — runs on the CPU
allocate_on_gpu(d_A, d_B, d_C, size=N)
copy_to_gpu(d_A, host_A)
copy_to_gpu(d_B, host_B)
launch kernel vectorAdd(d_A, d_B, d_C, N) with grid=(1024 blocks), block=(1024 threads)
copy_from_gpu(host_C, d_C)
Each thread executes a single addition — trivially simple. But 1 million threads executing simultaneously (in practice, in waves scheduled by the hardware) complete the entire operation orders of magnitude faster than a CPU loop. The overhead of copying data to and from the GPU is the main performance consideration: for small arrays, this overhead can exceed the computation time, making GPU acceleration counterproductive. GPU computing pays off when the computation-to-communication ratio is high — large datasets with many arithmetic operations per data element.Synchronization and memory model. Within a block, threads can synchronize using barrier functions: __syncthreads() ensures all threads in the block have reached the same point before any proceed. There is no efficient cross-block synchronization within a single kernel launch — this is by design, as it would require global communication that would cripple scalability. When global synchronization is needed, the programmer launches a new kernel (since all blocks from the previous kernel are guaranteed to have completed before the next kernel begins).Alternatives to CUDA. While CUDA dominates GPU computing (especially for machine learning), it is a proprietary NVIDIA technology that does not run on AMD or Intel GPUs. Several cross-platform alternatives exist:Despite these alternatives, CUDA's decade-long head start, mature tooling (cuBLAS, cuDNN, TensorRT), and dominance in the machine learning ecosystem (PyTorch and TensorFlow both use CUDA as their primary GPU backend) give it a powerful network effect that competitors have struggled to overcome.In Lecture 9, we classified architectures using Flynn's taxonomy and studied SIMD in detail: a single instruction applied simultaneously to multiple data elements packed into wide vector registers (SSE's 128-bit XMM registers, AVX-512's 512-bit ZMM registers). GPU execution looks similar — one instruction applied to many data elements — but the mechanism is fundamentally different. Understanding the distinction between SIMD, SIMT, and SMT reveals a spectrum of parallel execution models with different trade-offs between flexibility and efficiency.SIMD: rigid vector execution. In a CPU's SIMD model, the programmer (or compiler) must explicitly pack data into vector registers and issue vector instructions. All elements in the vector are processed by the same operation, and there is no notion of individual "threads" for each element. If you want to add two arrays of 16 floats using AVX-512:; CPU SIMD (AVX-512): explicitly pack and process 16 floats at once
VMOVUPS ZMM0, [A] ; Load 16 floats from array A into 512-bit register
VMOVUPS ZMM1, [B] ; Load 16 floats from array B into 512-bit register
VADDPS ZMM2, ZMM0, ZMM1 ; Add all 16 pairs simultaneously
VMOVUPS [C], ZMM2 ; Store 16 results
This is fast and efficient but rigid. All 16 elements must be loaded from contiguous memory. All 16 must undergo the same operation — there is no way for element 5 to take one branch while element 12 takes another. If one element needs special handling (e.g., a boundary condition), the programmer must use masking tricks or fall back to scalar code. The programming model is explicitly vectorized: the code "thinks" in terms of vectors, not individual elements.<br>SIMT: threads with implicit vectorization. The GPU's SIMT model (<a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Single_instruction,_multiple_threads" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Single_instruction,_multiple_threads" target="_self">Single Instruction, Multiple Threads</a>) inverts the abstraction. The programmer writes scalar code — code that describes the behavior of a single thread — and the hardware transparently groups threads into warps and executes them in lockstep. The programmer need not know about the warp size, vector registers, or lockstep execution (in principle — in practice, warp-aware optimization is important for performance).<br><a data-tooltip-position="top" aria-label="https://www.yosefk.com/blog/simd-simt-smt-parallelism-in-nvidia-gpus.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.yosefk.com/blog/simd-simt-smt-parallelism-in-nvidia-gpus.html" target="_self">Yosef Kreinin's analysis</a> identifies three key advantages of SIMT over SIMD: Multiple register sets. Each SIMT thread has its own complete set of registers. In SIMD, all elements share one set of vector registers, so code must be written in an explicitly vectorized style. In SIMT, each thread writes ordinary scalar code — result = a + b — and the hardware maps each thread's "scalar" registers to different entries in the large physical register file. This makes GPU code look like ordinary sequential programs, not vector intrinsics. Multiple addresses. In SIMD, a vector load fetches data from a single contiguous address range (e.g., 16 consecutive floats). In SIMT, each thread in a warp can compute its own address independently — threads can access scattered, non-contiguous memory locations. The hardware coalesces accesses when possible (merging them into a single wide memory transaction if threads happen to access nearby addresses), but non-coalesced access is legal, merely slower. This enables parallel data structures with irregular access patterns, like hash tables and graphs, that are impossible to express efficiently in pure SIMD. Multiple flow paths (branch divergence handling). In SIMD, there is no mechanism for different vector elements to take different branches — conditional processing requires explicit masking by the programmer. In SIMT, threads can diverge at branches. As discussed in Section 3.3, the hardware serializes divergent paths and uses masking to select the correct results — so divergence costs performance but does not require the programmer to manually manage masks. The hardware handles it transparently. SMT: fully independent threads. At the other end of the spectrum lies Simultaneous Multithreading (SMT) — a technique used in CPUs where a single core runs multiple fully independent threads by sharing the execution resources (ALUs, caches, pipelines). Intel's Hyper-Threading is the best-known SMT implementation: a single CPU core presents two logical cores to the operating system, each with its own architectural state (registers, program counter), sharing the execution units.In SMT, threads are truly independent — each runs its own program, accesses its own data, and takes its own branches with no performance penalty from divergence. The trade-off is that the hardware overhead per thread is far greater: each logical thread needs full architectural state, and the execution units must be dynamically shared rather than operating in lockstep.The spectrum. These three models form a spectrum of parallelism, trading off flexibility for efficiency:The key insight: SIMD extracts parallelism from data regularity — if your data is uniformly structured and your operations are uniform, SIMD is the most efficient choice. SIMT relaxes uniformity requirements, allowing per-thread addressing and branching at the cost of warp divergence overhead. SMT imposes no uniformity requirements at all, supporting fully independent instruction streams at the cost of much higher hardware overhead per thread.In practice, modern systems combine all three. A GPU uses SIMT as its primary execution model, but also employs SIMD within each CUDA core for certain operations (e.g., half-precision FP16 operations that pack two values per CUDA core). A CPU uses SMT for thread-level parallelism, SIMD for data-level parallelism (AVX instructions), and out-of-order execution for instruction-level parallelism — three different forms of parallelism coexisting in the same processor.Understanding where your workload falls on this spectrum — how uniform is the data? how regular is the control flow? how independent are the operations? — determines which execution model will deliver the best performance, and therefore whether a CPU, a GPU, or a combination of both is the right choice.Throughout this course you have studied general-purpose processors — machines that can execute any program you throw at them. That generality is their greatest strength: one CPU can run a web browser, a database, a game, and a compiler, all in the same afternoon. But generality comes at a cost. A general-purpose processor must include branch predictors, out-of-order execution engines, cache hierarchies, and control logic that accounts for every possible instruction mix. All of that transistor budget, all of that power consumption, goes toward flexibility — even when your workload only ever does one thing.What if you could strip away everything the workload does not need and pour every transistor into doing that one thing as fast and as efficiently as possible? That is the idea behind an Application-Specific Integrated Circuit (ASIC): a chip designed from the ground up for a single, well-defined task.<br>The most dramatic illustration of why ASICs exist comes from cryptocurrency mining, specifically Bitcoin. Bitcoin's proof-of-work algorithm requires miners to compute trillions of <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/SHA-2" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/SHA-2" target="_self">SHA-256</a> hashes per second, searching for a hash that falls below a target difficulty threshold. The miner who finds a valid hash first earns the block reward. This creates a ruthless economic pressure: any hardware that computes hashes faster or cheaper per watt wins.<br><img alt="Bitcoin mining difficulty chart showing progression from CPU to GPU to FPGA to ASIC" src="assets/l10-bitcoin-mining-difficulty.jpeg" target="_self">Bitcoin mining difficulty (log scale, 2009–2021) illustrates the CPU → GPU → FPGA → ASIC progression — each generation of specialized hardware increased hashrate by orders of magnitude.The history of Bitcoin mining hardware is a textbook progression through the flexibility-performance spectrum:Stage 1 — CPU mining (2009–2010). In Bitcoin's early days, miners used ordinary desktop CPUs. A typical Core i7 could compute roughly 20–30 megahashes per second (MH/s). This worked when the network difficulty was low and few people were mining.Stage 2 — GPU mining (2010–2013). Miners realized that SHA-256 hashing is massively parallel — each hash attempt is independent of every other. GPUs, with hundreds of cores optimized for parallel arithmetic, offered 10–100× the hash rate of CPUs at comparable power draw. A high-end GPU could reach 500–800 MH/s.Stage 3 — FPGA mining (2011–2013). Field-programmable gate arrays (which we will study in detail in Section 5) allowed miners to implement SHA-256 hashing directly in reconfigurable hardware, eliminating the overhead of GPU instruction fetch, decode, and scheduling. FPGAs offered better energy efficiency (hashes per watt) than GPUs, though raw hash rates were similar.Stage 4 — ASIC mining (2013–present). Purpose-built ASIC miners arrived and instantly rendered all previous approaches obsolete. An early ASIC miner like the Avalon A3256 could compute 65 GH/s — over 3,000× faster than a GPU — while consuming less power. Modern ASIC miners operate at hundreds of terahashes per second (TH/s). No CPU, GPU, or FPGA can compete.<br><img alt="Cryptocurrency mining rig with multiple GPUs" src="assets/l10-asic-crypto-mining-rig.jpeg" target="_self">A GPU-based mining rig — before dedicated ASIC miners, cryptocurrency mining used arrays of consumer GPUs.This progression is not unique to cryptocurrency. The same economic logic applies anywhere a single computational task dominates: video encoding, network packet processing, AI inference, signal processing. When the task is fixed and the volume is high enough, specialization always wins.An ASIC is an integrated circuit custom-designed for a particular application rather than for general-purpose use. Unlike a CPU or GPU, which must handle arbitrary programs, an ASIC implements a fixed function — or a narrow family of related functions — directly in silicon.The key characteristics that distinguish ASICs from general-purpose processors are:
Fixed function. The circuit's logic is determined at design time and cannot be changed after manufacturing.
Optimized datapath. Every gate, every wire, every timing constraint is tuned for the target workload. There is no wasted silicon on unused instruction decoders or general-purpose register files.
Low power, high throughput. Because the chip does only what it needs to and nothing more, it can achieve orders-of-magnitude better performance per watt than a general-purpose alternative.
ASICs are far more common than most people realize. The smartphone in your pocket likely contains multiple ASICs: a baseband processor for cellular communication, an image signal processor (ISP) for the camera pipeline, a neural engine for on-device machine learning, and a secure enclave for cryptographic operations. Your car's engine control unit, your router's packet-forwarding engine, the controller in your SSD — all are ASICs or ASIC-like devices designed for one job and one job only.Not all ASICs are created the same way. The semiconductor industry offers a spectrum of design methodologies, trading off customization against development time and cost. The three major approaches are:Full-Custom ASICIn a full-custom design, engineers specify every mask layer of the chip — the placement of individual transistors, the routing of every wire, the geometry of every cell. This gives the designer complete control over performance, power, and area, but it is extraordinarily expensive and time-consuming. Full-custom design is reserved for the highest-volume, highest-performance parts: flagship CPUs, leading-edge GPUs, and critical analog circuits. Manufacturing turnaround is typically around 8 weeks once the design is finalized, but the design process itself can take years.Standard-Cell (Cell-Based IC / CBIC)Standard-cell design uses a library of pre-designed, pre-characterized logic cells — AND gates, flip-flops, multiplexers, adders — each with known timing, power, and area characteristics. The designer describes the circuit in a hardware description language (HDL), and automated tools assemble the cells into a layout, handling placement and routing. All mask layers are still customized for the specific design, but the individual cell geometries come from the library. This approach dramatically reduces design time compared to full-custom while still achieving very good performance. Manufacturing turnaround is similar to full-custom (~8 weeks), but the design cycle is much shorter.Standard-cell is the most widely used ASIC methodology today. When people say "ASIC" without further qualification, they usually mean a standard-cell design.Gate-Array ASICA gate array takes pre-fabrication one step further. The silicon wafer comes with transistors and lower metal layers already manufactured in a regular, repeating pattern. Only the upper interconnect layers — the metal wires that connect transistors into functional circuits — are customized for the specific design. Because the base wafer is mass-produced and stockpiled, turnaround time drops dramatically: days to two weeks, compared to months for a standard-cell design.Gate arrays come in several subtypes:The following table summarizes the three approaches:Advantages
Performance. An ASIC optimized for a specific algorithm can be 100–10,000× faster than a CPU running the same algorithm in software, and 10–100× faster than a GPU.
Power efficiency. With no wasted logic, ASICs deliver the best performance per watt. This matters enormously in battery-powered devices and in data centers where electricity is a major operating cost.
Small form factor. A function that requires an entire GPU board can often be implemented in a single ASIC die, enabling integration into space-constrained products like earbuds, medical implants, and automotive sensors.
IP protection. A custom ASIC is far harder to reverse-engineer than software or FPGA bitstreams, providing some protection for proprietary algorithms.
Cost at volume. The per-unit silicon cost of an ASIC drops rapidly with volume. At millions of units, the amortized cost per chip can be a few dollars or even cents.
Disadvantages
High NRE (Non-Recurring Engineering) costs. Designing and fabricating the masks for an ASIC can cost millions to tens of millions of dollars at advanced process nodes (7 nm, 5 nm). This investment must be recouped through volume sales.
Long development cycle. From specification to working silicon typically takes 12–24 months. For full-custom designs at leading-edge nodes, three or more years is not unusual.
Inflexibility. Once the ASIC is fabricated, its function is frozen. If the algorithm changes, if a bug is discovered, or if the market shifts, the chip cannot be reprogrammed — it must be redesigned and re-fabricated. This obsolescence risk is a major concern in fast-moving fields.
Specialized expertise. ASIC design requires hardware engineers skilled in RTL design, timing analysis, physical design, and verification — a very different skill set from software development.
ASICs are everywhere, though they often hide behind brand names and marketing terms:
Cryptocurrency mining. Bitmain's Antminer series and similar products are SHA-256 ASIC miners. Google's Tensor Processing Unit (TPU) is, architecturally, an ASIC optimized for matrix multiplication — the core operation of neural network training and inference.
Consumer electronics. Apple's A-series and M-series chips contain ASIC blocks for the Neural Engine, ISP, and secure enclave alongside general-purpose CPU and GPU cores. Qualcomm's Snapdragon SoCs follow the same pattern.
Telecommunications and networking. Routers and switches from Cisco, Juniper, and Broadcom use ASICs for line-rate packet forwarding. 5G base stations rely on ASICs for real-time signal processing.
Automotive. Advanced driver-assistance systems (ADAS) use ASICs for sensor fusion, object detection, and real-time decision making. Engine control units (ECUs) have been ASIC-based for decades.
Medical devices. Hearing aids, pacemakers, and portable diagnostic instruments use ASICs to achieve the necessary signal processing within extreme power and size constraints.
Industrial and military. Factory controllers, radar systems, and satellite communication links all rely on ASICs for deterministic, real-time processing.
Section 4 made the case for specialization: when you know the workload, custom silicon wins. But ASICs have a painful weakness — they are frozen at fabrication. If you need to fix a bug, adapt to a new protocol version, or experiment with a design before committing to a multi-million-dollar tape-out, an ASIC cannot help you.At the other extreme, CPUs and GPUs are fully reprogrammable but fundamentally limited: they execute instructions one (or a few) at a time, constrained by instruction fetch, decode, and scheduling overhead. Even a massively parallel GPU still pays the cost of a general-purpose instruction pipeline for every arithmetic operation.Is there something in between — hardware that you can customize for a specific task, but reprogram when the task changes? That is exactly what a Field-Programmable Gate Array (FPGA) provides.<br><img alt="Xilinx Spartan-6 FPGA development board" src="assets/l10-fpga-xilinx-spartan6.jpeg" target="_self">A Xilinx Spartan-6 FPGA development board — FPGAs are programmed via bitstream files loaded into their configuration memory.An FPGA is an integrated circuit that contains a large array of configurable logic blocks connected by a programmable routing network. After manufacturing, the chip's function is not fixed — it is determined by a configuration bitstream loaded into on-chip RAM. Change the bitstream, and the chip implements a different circuit. No new silicon, no new masks, no weeks of fabrication — just upload a new configuration.The key insight is that an FPGA does not execute instructions. Instead, it is the circuit. When you "program" an FPGA to implement a SHA-256 hasher, the chip physically becomes a SHA-256 hasher — with dedicated logic gates, dedicated wiring, and dedicated registers, all operating in parallel. There is no instruction fetch, no decode stage, no program counter. The data flows through the circuit at the speed of the logic gates themselves.This makes FPGAs fundamentally different from processors. A processor is a temporal computing device: it reuses the same hardware (ALU, registers) over and over, performing different operations at different times. An FPGA is a spatial computing device: different operations happen in different places on the chip, all at the same time.An FPGA is built from four primary components:<br><img alt="FPGA internal structure: logic blocks, I/O pads, and programmable interconnects" src="assets/l10-fpga-structure.png" target="_self">Bird's-eye view of FPGA architecture: a grid of configurable logic blocks (CLBs) connected by programmable routing, surrounded by I/O pads.1. Configurable Logic Blocks (CLBs)CLBs are the fundamental computational units of the FPGA. Each CLB contains a small number of Look-Up Tables (LUTs), flip-flops, and multiplexers. A modern FPGA may contain tens of thousands of CLBs.2. Look-Up Tables (LUTs)The LUT is the core building block of FPGA logic. A k-input LUT is simply a 2^k-entry memory (SRAM) connected to a k-input multiplexer. The SRAM stores the complete truth table for any Boolean function of k variables. To compute the function, the input signals select one entry from the truth table via the multiplexer, and the output appears immediately (after the propagation delay of the multiplexer).<br>For example, a 4-input LUT contains 2^4 = 16 SRAM bits and can implement any Boolean function of 4 inputs — AND, OR, XOR, majority, parity, or any of the other 65,536 possible 4-input functions. Modern FPGAs typically use 6-input LUTs (64 SRAM bits each), which can implement any function of up to 6 variables. A mid-range FPGA like the <a data-tooltip-position="top" aria-label="https://www.xilinx.com/products/silicon-devices/fpga/spartan-6.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.xilinx.com/products/silicon-devices/fpga/spartan-6.html" target="_self">Xilinx Spartan-6</a> contains over 20,000 dual LUTs.3. Programmable Routing MatrixThe routing matrix is a network of programmable multiplexers and wire segments that connects CLBs to each other and to I/O blocks. When you load a configuration bitstream, the bitstream programs each multiplexer in the routing matrix to either connect or disconnect specific wire segments, creating the exact wiring topology your circuit needs.Routing typically consumes more chip area than the logic blocks themselves — often 60–80% of the total die. This is one of the reasons FPGAs are larger and slower than equivalent ASICs: the programmable routing adds delay and consumes power.4. I/O BlocksI/O blocks sit around the perimeter of the FPGA and connect the internal logic to external pins. Each I/O block is configurable for input, output, or bidirectional operation, and supports various voltage standards and signaling protocols.Modern FPGAs also include hard IP blocks — pre-built, non-configurable functional units embedded alongside the programmable fabric. Common hard IP includes block RAM (BRAM), digital signal processing (DSP) slices with hardware multipliers, clock management tiles (PLLs/DLLs), high-speed serial transceivers, and even ARM processor cores. These hard blocks provide ASIC-level performance for common functions while the programmable fabric handles application-specific logic.<br>You do not program an FPGA in C or Python. Instead, you describe the desired circuit using a Hardware Description Language (HDL) — most commonly <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Verilog" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Verilog" target="_self">Verilog</a> or <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/VHDL" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/VHDL" target="_self">VHDL</a>. An HDL looks superficially like a programming language, but it describes structure and connections, not sequential instructions. Every "statement" in an HDL describes hardware that exists simultaneously — there is no control flow in the CPU sense.The development flow is:
Design — write the circuit description in Verilog/VHDL.
Synthesis — a synthesis tool converts the HDL into a netlist of LUTs, flip-flops, and routing connections.
Place and route — the tool maps the netlist onto the physical resources of the specific FPGA chip.
Bitstream generation — the tool produces a binary configuration file.
Configuration — the bitstream is loaded into the FPGA's configuration SRAM (typically via JTAG or SPI flash), and the chip immediately begins operating as the described circuit.
This entire cycle — from HDL change to running hardware — typically takes minutes to hours, compared to months for an ASIC. That fast turnaround is one of the FPGA's greatest advantages.FPGAs and ASICs are complementary technologies, not competitors. They serve different points in the product lifecycle and different volume requirements. The following table highlights the key differences:In practice, FPGAs often serve as a stepping stone to ASICs. A company will prototype and validate a design on an FPGA, iterate rapidly, and only commit to an ASIC tape-out once the design is stable and the projected volume justifies the NRE investment. Some products ship on FPGAs permanently — when volumes are too low to justify an ASIC, or when the application requires field updates (e.g., software-defined radio, adaptive networking equipment).There is also a cost-volume crossover point: below a certain production volume, FPGAs are cheaper (because there is no NRE to amortize); above that volume, ASICs are cheaper (because the per-unit cost is lower). The exact crossover depends on the design complexity and the ASIC process node, but it typically falls in the range of 10,000–100,000 units.Advantages
Reconfigurability. The same chip can implement completely different circuits at different times. This enables in-field updates, rapid prototyping, and multi-function devices.
Fast time to market. No mask fabrication, no wafer processing. A design change goes from HDL to running hardware in hours.
No tooling costs. Unlike ASICs, FPGAs require no custom masks or wafer processing. The development cost is primarily engineering time and software licenses.
True hardware parallelism. Unlike a CPU, which time-shares one ALU across many operations, an FPGA can instantiate thousands of independent processing elements operating simultaneously.
Disadvantages
Lower performance than ASIC. The programmable routing fabric adds propagation delay, and LUT-based logic is inherently less efficient than custom gates. FPGAs are typically 2–10× slower and 5–20× less power-efficient than an equivalent ASIC.
Higher power consumption. Even at rest, an FPGA draws significant current to maintain its SRAM-based configuration. Active power consumption can exceed 1 A at full load — a major concern for battery-powered applications.
Higher per-unit cost. A large FPGA can cost $50–$500+ per chip, compared to $1–$10 for an ASIC at high volume.
Development complexity. HDL design requires hardware engineering skills that are less common than software skills. Debugging a circuit in an FPGA is fundamentally different from — and often harder than — debugging software.
Throughout Sections 3–5 of this lecture, you have met three very different approaches to computation: general-purpose processors (CPUs/GPUs), reconfigurable hardware (FPGAs), and fixed-function hardware (ASICs). These three sit on a spectrum — and understanding where each falls is one of the most important insights in modern computer architecture.The spectrum reveals a fundamental engineering trade-off: flexibility and performance are inversely correlated. The more flexible a device is — the wider the range of computations it can perform — the less efficient it is at any single computation. A CPU can run any program but wastes transistors on generality. An ASIC can run only one program but wastes nothing.This is not a new idea — it is an echo of the specialization principle you see everywhere in engineering and biology. A Swiss Army knife can do many things adequately; a chef's knife does one thing superbly. The right choice depends on whether you need versatility or peak performance.In modern systems, the answer is increasingly "both." A smartphone's SoC combines general-purpose CPU cores, a GPU, a neural-network ASIC, a camera ISP ASIC, and sometimes even a small FPGA-like reconfigurable fabric — all on a single chip. Each component handles the workload it is best suited for. This heterogeneous computing approach is the dominant trend in architecture design today, and it is the logical endpoint of the spectrum we have been studying.In Lecture 9, you learned that VLIW architectures take a fundamentally different approach to instruction-level parallelism: instead of relying on complex hardware to discover which operations can execute simultaneously (as superscalar processors do), VLIW moves that responsibility entirely to the compiler. The compiler analyzes the program, identifies independent operations, and packs them into a single wide instruction word — typically 64 to 1024 bits. The hardware then executes every slot in that word in parallel, with no dynamic scheduling logic at all.You also learned the core trade-offs: VLIW simplifies hardware (no reorder buffer, no register renaming, no dependency-checking circuits) but places enormous demands on the compiler. When the compiler cannot find enough independent operations to fill every slot, it inserts NOPs, wasting instruction bandwidth. And because the instruction format is tied to the number and type of functional units, binaries compiled for one VLIW generation may not run on the next without recompilation.This section picks up where L09 left off. Rather than revisiting the theory, we will focus on how VLIW instructions actually execute, and then examine two real-world VLIW implementations — one a commercial failure, the other a quiet success — to understand where this architectural philosophy thrives and where it falls short.To make the VLIW concept concrete, consider a processor with three functional units: a floating-point multiplier (FU1), a floating-point adder (FU2), and a load/store unit (FU3). A single VLIW instruction word for this processor contains three operation fields — one for each unit.<br><img alt="VLIW execution: one instruction word feeds three functional units through a shared register file" src="assets/l10-vliw-execution-diagram.png" target="_self">VLIW execution model: a single wide instruction contains three independent operations, each dispatched to its own functional unit (FU). All units share a common register file.When the processor fetches one VLIW instruction, it simultaneously dispatches all three operations:
FU1 begins a floating-point multiply
FU2 begins a floating-point add
FU3 begins a memory load (or two loads, if the unit supports dual-port access)
All three functional units read their operands from a shared register file and write results back to it. There is no arbitration logic deciding which operations can proceed — the compiler has already guaranteed that these three operations have no data dependencies on each other.The pipeline for a VLIW processor looks similar to a classic RISC pipeline — IF (Instruction Fetch), ID (Instruction Decode), EX (Execute), MEM (Memory Access), WB (Write Back) — but with one crucial difference: the EX stage fans out into multiple parallel execution paths, one per functional unit.<br><img alt="VLIW pipeline diagram showing IF, ID, multiple EX stages, MEM, and WB" src="assets/l10-vliw-pipeline.png" target="_self">VLIW pipeline: each instruction goes through fetch (IF), decode (ID), multiple parallel execute stages (EX), memory (MEM), and write-back (WB).Because the instruction word is much wider than a standard 32-bit RISC instruction, the instruction fetch and decode stages must handle more data per cycle. However, decode is simpler: there is no need to check for hazards or reorder operations. Each field maps directly to its functional unit. This is why VLIW proponents argue that the approach scales well — adding more functional units is a matter of widening the instruction word, not adding exponentially more scheduling hardware.The downside is equally concrete. If the compiler can only find two independent operations for a given cycle, the third slot must contain a NOP. In the worst case — a sequence of tightly dependent operations — most slots go unused, and the processor's effective throughput drops to that of a simple scalar machine despite having the silicon area of a much more complex one.<br>In 2000, a small startup called <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Transmeta" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Transmeta" target="_self">Transmeta</a> announced a processor that seemed to defy categorization. The Crusoe chip was, internally, a VLIW processor — it executed 128-bit instruction words containing four operations each. But externally, it ran standard x86 software: Windows, Linux, every existing application, without recompilation.<br><img alt="Transmeta Crusoe processor chip" src="assets/l10-vliw-transmeta-crusoe.jpeg" target="_self">The Transmeta Crusoe — a VLIW processor that ran x86 software through dynamic binary translation ("Code Morphing").The trick was a software layer called Code Morphing Software (CMS). CMS was a dynamic binary translator — a program stored in ROM that ran before the operating system booted. When the processor encountered x86 code, CMS translated it on-the-fly into Crusoe's native VLIW instructions. The translation happened in stages:
Interpretation — the first time a block of x86 code executes, CMS interprets it instruction-by-instruction (slow, but correct)
Translation — if a code block executes repeatedly (a hot path), CMS compiles it into optimized VLIW sequences and caches the result
Optimization — for the hottest code paths, CMS applies aggressive optimizations: instruction scheduling, register allocation, and even speculative reordering with rollback support
This approach had a profound advantage: the processor could evolve its internal VLIW architecture without breaking x86 compatibility. Because the translation layer was software, Transmeta could change the number of functional units, the instruction word format, or the pipeline depth between chip generations — and existing x86 programs would still run, translated anew by the updated CMS.Crusoe targeted a specific market: ultra-low-power laptops. By eliminating the complex out-of-order execution hardware that Intel's Pentium III required, Transmeta built a chip that consumed significantly less power. Several laptop manufacturers adopted it, including some early thin-and-light designs.However, Crusoe faced a fundamental performance challenge. The translation overhead — even with caching and optimization — meant that Crusoe was consistently slower than Intel's x86 processors at the same clock speed. As Intel improved the power efficiency of its own chips (particularly with the Pentium M and later Core series), Transmeta's power advantage eroded. The company stopped producing processors in 2005 and eventually became a patent licensing firm.Transmeta's legacy, though, extends far beyond its commercial lifespan. The idea of a software compatibility layer decoupling the external ISA from the internal microarchitecture is exactly what modern x86 processors do — Intel and AMD chips translate x86 instructions into internal micro-ops (which resemble RISC operations) using hardware decoders. Transmeta simply did it in software rather than silicon.While Transmeta was a small startup, Intel's VLIW bet was one of the largest processor development programs in history. In partnership with Hewlett-Packard, Intel designed Itanium — a processor based on a variant of VLIW that Intel called EPIC (Explicitly Parallel Instruction Computing).Itanium's instruction format grouped three operations into a 128-bit bundle. Each bundle included a template field — a set of bits that told the hardware which operations could execute in parallel and where the boundaries between independent groups lay. This was a refinement over pure VLIW: rather than requiring every slot to be independent (and filling unused slots with NOPs), the template bits allowed the compiler to express partial parallelism, and the hardware could issue multiple bundles per cycle when dependencies allowed.Itanium also introduced several features designed to help the compiler:
Predicated execution — almost every instruction could be conditionally executed based on a predicate register, eliminating many branches (and the associated branch misprediction penalties)
Speculative loads — the compiler could issue memory loads early, before it was certain the data was needed, with hardware support for detecting and recovering from faults on speculative accesses
Register rotation — a hardware mechanism that simplified loop pipelining by automatically renaming registers between loop iterations
On paper, Itanium was elegant. In practice, it was a commercial disaster. The reasons were both technical and economic:Compiler difficulty. The entire premise of EPIC was that compilers could extract sufficient instruction-level parallelism from general-purpose code. For scientific computing with regular loops over large arrays, this was feasible. For typical server workloads — databases, web servers, enterprise applications with complex control flow, pointer-heavy data structures, and unpredictable branches — compilers could not fill the instruction slots effectively. The theoretical performance was there; the practical performance was not.Code bloat. IA-64 binaries were 2 to 4 times larger than equivalent x86 binaries. The wider instruction format, combined with NOP padding in slots the compiler could not fill, consumed significantly more instruction cache and memory bandwidth. This partially offset the theoretical advantage of simpler decode logic.x86 kept improving. Intel's own x86 processors — and especially AMD's Opteron, which added 64-bit extensions (AMD64/x86-64) to the existing x86 architecture — continued to gain performance through better out-of-order execution, deeper pipelines, and larger caches. The industry chose to extend x86 rather than replace it. When Intel adopted AMD's 64-bit extensions for its own x86 chips (as Intel 64), Itanium lost its primary selling point as the path to 64-bit computing.<br>Itanium limped along in niche enterprise and high-performance computing markets (HP-UX servers, primarily) before Intel officially <a data-tooltip-position="top" aria-label="https://www.anandtech.com/show/16276/intel-to-discontinue-itanium-9700-last-of-itanium-line" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.anandtech.com/show/16276/intel-to-discontinue-itanium-9700-last-of-itanium-line" target="_self">discontinued the product line in 2021</a>. It remains one of the most expensive failed bets in semiconductor history, with estimated development costs exceeding $10 billion across Intel and HP.<br>If Itanium is the cautionary tale, digital signal processors are the success story. The Texas Instruments <a data-tooltip-position="top" aria-label="https://www.ti.com/processors/c2000-microcontrollers/overview.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.ti.com/processors/c2000-microcontrollers/overview.html" target="_self">TMS320C6000 family</a> (commonly called C6x) is one of the most commercially successful VLIW architectures ever built. The C6x series uses instruction words that pack up to eight operations per cycle across eight functional units.Why does VLIW work for DSP when it failed for general-purpose computing? The answer lies in the nature of the workload:In DSP applications — audio codecs, telecommunications baseband processing, radar signal processing, image filtering — the algorithms are mathematically regular. A finite impulse response (FIR) filter, for example, is essentially a dot product computed over a sliding window: multiply each input sample by a coefficient and accumulate the results. The compiler can trivially schedule these multiplies and adds across multiple functional units, filling every VLIW slot productively.Beyond TI's DSP family, VLIW principles appear in several other domains:
Qualcomm Hexagon DSP — found in Snapdragon mobile processors, handles audio, sensor processing, and some neural network inference using a VLIW core
GPU shader compilers — some GPU architectures use VLIW-like instruction packing in their shader processors (AMD's older VLIW4/VLIW5 GPU architectures used this approach explicitly)
Network processors — high-throughput packet processing benefits from the same regular, predictable parallelism that makes VLIW effective for DSP
The lesson of VLIW is not that the idea was wrong — it is that the idea works only when the workload matches the assumptions. When the compiler can see the parallelism at compile time, VLIW delivers excellent performance per watt with minimal hardware complexity. When the parallelism is dynamic, data-dependent, and unpredictable, the compiler cannot help, and hardware scheduling wins.In 2013, engineers at Google made a startling calculation. If every Android user used voice search for just three minutes per day, the computational load of running the speech recognition neural network would require doubling Google's entire datacenter capacity. Not adding a few racks — doubling everything: servers, networking, cooling, power infrastructure. The existing hardware — CPUs and even GPUs — was simply too inefficient for the specific computation that neural networks demand.The core problem is the von Neumann bottleneck applied to machine learning. A neural network inference pass consists almost entirely of matrix multiplications: multiply a matrix of input activations by a matrix of learned weights, add biases, apply an activation function, and repeat — layer after layer. The arithmetic itself is simple. The challenge is that the operands are enormous: a single layer of a modern language model might multiply a 4096-by-4096 matrix by a vector thousands of times per second.<br>On a general-purpose CPU, each of those multiply-accumulate operations requires fetching operands from memory (or cache), executing the arithmetic in an ALU designed for general-purpose computation, and writing the result back. The energy cost of moving data to and from memory dwarfs the energy cost of the multiplication itself — by roughly <a data-tooltip-position="top" aria-label="https://blog.bytebytego.com/p/how-googles-tensor-processing-unit" rel="noopener nofollow" class="external-link is-unresolved" href="https://blog.bytebytego.com/p/how-googles-tensor-processing-unit" target="_self">200 times</a> for DRAM access versus an 8-bit integer multiply. A CPU spends more energy shuffling numbers around than actually computing with them.GPUs improved the situation by providing thousands of smaller cores operating in parallel (as you learned in the GPU section of this lecture). But even GPUs are more general than necessary for matrix math. A GPU core can execute arbitrary shader programs, handle branching, manage thread divergence, and support a wide range of data types. For the narrow task of multiplying matrices of low-precision numbers, much of that flexibility is wasted silicon and wasted energy.Google's answer was to build a chip that does almost nothing except matrix multiplication — but does it with extraordinary efficiency. That chip was the Tensor Processing Unit (TPU).The first TPU (v1), deployed internally at Google in 2015, was designed as an inference accelerator — it ran already-trained neural networks to produce predictions, rather than training new models. Its architecture was radically different from both CPUs and GPUs, built around a single key innovation: the systolic array.The term "systolic" comes from the Greek word for heartbeat. In a systolic array, data flows rhythmically through a grid of processing elements, like blood pulsing through the circulatory system. Each processing element performs one multiply-accumulate (MAC) operation and passes data to its neighbor — no global memory access required.<br><img alt="Google Tensor Processing Unit card" src="assets/l10-tpu-google.jpeg" target="_self">Google's first-generation TPU — a PCIe card built around a 256x256 systolic array, designed specifically for neural network inference.In TPU v1, the Matrix Multiply Unit (MXU) is a 256-by-256 systolic array — that is 65,536 multiply-accumulate units operating simultaneously. Here is how it works: Weights are pre-loaded into the array. Each processing element holds one weight value. For a 256-by-256 matrix multiplication, all 65,536 weights are loaded once and remain stationary. Input activations flow in from the left, one row at a time. Each activation value enters the leftmost column and propagates rightward through the array, one column per clock cycle. Partial sums accumulate downward. Each processing element multiplies the incoming activation by its stored weight and adds the result to the partial sum flowing down from above. Results emerge from the bottom of the array after 256 clock cycles — a complete matrix-vector product, computed with 65,536 parallel MAC operations per cycle. The beauty of this design is data reuse. Each weight is loaded once and used 256 times (once for each input vector element that flows past it). Each activation is loaded once and used 256 times (once at each column it passes through). This 256-fold reuse dramatically reduces memory bandwidth requirements compared to a CPU or GPU, where every operand must be fetched from a register file or cache for each operation.Beyond the MXU, the TPU v1 architecture includes: Unified Buffer — 24 MB of SRAM that holds input activations. This is the working memory for the current layer's inputs, sized to hold the activations of typical neural network layers without going to external DRAM. Accumulators — 4,096 256-element 32-bit accumulators that store the output partial sums from the MXU. The accumulator precision is higher than the input precision (32-bit vs. 8-bit) to prevent overflow during the summation of 256 products. Activation Pipeline — a hardware unit that applies nonlinear activation functions (ReLU, sigmoid, tanh) to accumulator outputs, converting results back to the appropriate precision for the next layer. High Bandwidth Memory (HBM) — starting from TPU v2, external HBM provides the large memory capacity needed for model weights that exceed the on-chip SRAM capacity. One of the most important design decisions in the TPU is its use of reduced numerical precision. TPU v1 operates primarily on INT8 (8-bit integer) values. Later generations added support for BFloat16 (Brain Floating Point 16) — a 16-bit format with an 8-bit exponent and 7-bit mantissa.Why does reduced precision work for neural networks? Two reasons: Neural networks are inherently noise-tolerant. During training, weights are adjusted by stochastic gradient descent — a process that is itself noisy. The model learns to be robust to small perturbations. Reducing precision from FP32 to INT8 or BFloat16 introduces small rounding errors, but these errors are within the noise margin the model already tolerates. Smaller numbers mean smaller hardware. An 8-bit multiplier uses roughly 1/16 the silicon area and 1/30 the energy of a 32-bit floating-point multiplier. By using reduced precision, the TPU packs far more MAC units into the same chip area than a GPU can with its FP32-capable cores. The result is striking: TPU v1 dedicated over 90% of its silicon area to actual computation (the MXU and accumulators). Compare this with a contemporary GPU, where roughly 30% of the die is compute logic, with the rest consumed by caches, control logic, thread schedulers, and other general-purpose infrastructure.<br>Google reported that TPU v1 was <a data-tooltip-position="top" aria-label="https://blog.bytebytego.com/p/how-googles-tensor-processing-unit" rel="noopener nofollow" class="external-link is-unresolved" href="https://blog.bytebytego.com/p/how-googles-tensor-processing-unit" target="_self">15 to 30 times faster</a> than contemporary GPUs for inference workloads, and 30 to 80 times more energy-efficient. These numbers reflected the advantage of a chip purpose-built for matrix math over a chip designed for general-purpose parallel computing.Since TPU v1, Google has released multiple generations, each expanding capabilities and performance:Several architectural trends are visible across these generations:From inference to training. TPU v1 could only run pre-trained models. Starting with v2, TPUs gained the ability to train models — which requires backpropagation, gradient computation, and weight updates in addition to forward-pass matrix multiplications. This required higher precision (BFloat16 instead of INT8) and much more memory bandwidth (hence the switch to HBM).<br>Interconnect innovation. TPU v4 introduced <a data-tooltip-position="top" aria-label="https://cloud.google.com/blog/topics/systems/tpu-v4-enables-performance-energy-and-co2e-efficiency-gains" rel="noopener nofollow" class="external-link is-unresolved" href="https://cloud.google.com/blog/topics/systems/tpu-v4-enables-performance-energy-and-co2e-efficiency-gains" target="_self">optical circuit switches</a> to connect TPU chips within a pod, enabling reconfigurable topologies — the same physical hardware could be wired as a 3D torus for some workloads or a different topology for others. This addressed a key limitation of earlier generations where the fixed interconnect topology was a bottleneck for some communication patterns.SparseCores. TPU v4 added a novel component called SparseCores — specialized processing units optimized for sparse data operations like embedding table lookups. In recommendation models (used for Google Search, YouTube, and advertising), embedding lookups account for a large fraction of computation. SparseCores provided a 5 to 7 times speedup for these operations over the general-purpose MXU.Scaling to FP8. The latest generation, Ironwood (v7), supports native FP8 (8-bit floating point) computation, reflecting the broader industry trend toward even lower precision for both training and inference of large language models. At 4,614 TFLOPS per chip, Ironwood represents over a 50-fold increase in raw throughput compared to TPU v1.TPUs have been behind some of the most prominent AI achievements of the past decade:
AlphaGo (2016) — DeepMind's Go-playing system, which defeated world champion Lee Sedol, ran on TPUs for both training and inference
BERT and T5 (2018-2019) — foundational language models that shaped the modern NLP landscape, trained on TPU pods
<br>AlphaFold (2020-2024) — DeepMind's protein structure prediction system, which solved a 50-year grand challenge in biology and earned its creators the <a data-tooltip-position="top" aria-label="https://www.nobelprize.org/prizes/chemistry/2024/summary/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.nobelprize.org/prizes/chemistry/2024/summary/" target="_self">2024 Nobel Prize in Chemistry</a>, was trained on TPUs
Google Search, Translate, and Photos — production services that process billions of requests daily using TPU inference
While Google designed the TPU around systolic arrays optimized for matrix multiplication, Tesla took a different path with DOJO — a custom supercomputer designed specifically for training the neural networks that power its Autopilot and Full Self-Driving systems. DOJO's architecture reflects a different set of design priorities: maximizing training throughput for computer vision models while eliminating traditional bottlenecks in data movement.At the heart of DOJO is the D1 — a custom-designed processor fabricated on TSMC's 7nm process with approximately 50 billion transistors.<br><img alt="Tesla DOJO D1 die specifications: TSMC 7nm, 354 cores, 362 TFlops" src="assets/l10-tesla-dojo-d1-die.jpeg" target="_self">Tesla DOJO D1 die: 354 processing cores on a 645 mm² die (TSMC 7nm), delivering 362 TFlops at BF16.<br>Unlike Google's TPU (which is a specialized accelerator with no general-purpose instruction execution), the D1 is built around a <a data-tooltip-position="top" aria-label="https://www.nextplatform.com/2022/08/23/inside-teslas-innovative-and-homegrown-dojo-ai-supercomputer/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.nextplatform.com/2022/08/23/inside-teslas-innovative-and-homegrown-dojo-ai-supercomputer/" target="_self">RISC-V</a> instruction set core extended with Tesla's custom vector and matrix instructions. Each D1 chip contains 354 processing cores arranged in an 18-by-20 grid, running at 2 GHz.Each core features:
A RISC-V scalar unit with an 8-wide instruction decoder and 4-way simultaneous multithreading (SMT)
A custom vector/matrix processing unit optimized for BFloat16 and other AI-relevant formats
1.25 MB of local SRAM — an unusually large amount of on-chip memory per core, providing 400 GB/s of load bandwidth without going off-chip
The total per-chip specifications are remarkable:
440 MB SRAM on-chip (354 cores x 1.25 MB)
362 TFLOPS at BFloat16 precision
2 GHz clock speed
The most distinctive feature of DOJO's architecture is what Tesla removed. Traditional multi-core processors include hardware for cache coherency (ensuring all cores see a consistent view of memory), virtual memory (translating between virtual and physical addresses), and global directories (tracking which cache holds which data). These mechanisms are essential for general-purpose computing, where any core might need to access any address at any time.Tesla eliminated all of them.Instead of coherency protocols and virtual memory, DOJO uses a distributed SRAM model. Each core owns its local SRAM exclusively. Data movement between cores happens through an explicit, software-controlled mesh network — not through hardware-managed cache snooping. The compiler and runtime system are responsible for placing data where it needs to be and orchestrating transfers between cores.This is a radical simplification. Cache coherency hardware in a 354-core chip would consume enormous amounts of silicon area and power. By removing it, Tesla reclaimed that silicon for more compute units and more SRAM. The trade-off is that programming DOJO requires explicit data management — but for neural network training, the data flow patterns are known at compile time (just as with VLIW, the assumption is that static analysis can replace dynamic hardware).Tesla designed DOJO to scale from chip to system in a uniquely integrated way.<br><img alt="Tesla DOJO Training Tile: 5x5 array of D1 chips" src="assets/l10-tesla-dojo-training-tile.jpeg" target="_self">DOJO Training Tile: 25 D1 dies in a 5x5 array, delivering 9 PFlops at 15 kW with water cooling.A Training Tile combines 25 D1 dies (arranged in a 5-by-5 grid) on a single module:
11 GB SRAM total (25 x 440 MB)
9 PFLOPS of BFloat16 compute
10 TB/s bisectional bandwidth between dies within the tile
15 kW power consumption
The dies within a tile are connected via a high-bandwidth mesh, giving the tile the characteristics of a single very large processor rather than a cluster of separate chips. The enormous aggregate SRAM (11 GB) is significant — for many training workloads, the model parameters and intermediate activations can reside entirely in SRAM, avoiding the latency and energy cost of accessing external DRAM or HBM.<br><img alt="Tesla DOJO system exploded view showing modular stack" src="assets/l10-tesla-dojo-stack.jpeg" target="_self">Tesla DOJO system — modular design stacks training tiles into system trays and ExaPods reaching 20 ExaFlops.At the largest scale, an ExaPod combines 120 Training Tiles:
Over 1 million processing cores
20 EFLOPS (exaFLOPS) of BFloat16 compute
Designed for training the massive video-based neural networks that Tesla's autonomous driving system requires
Despite its exotic hardware, DOJO is designed to be programmed using standard machine learning frameworks. Tesla's software stack provides PyTorch compatibility, meaning that researchers and engineers can write training code using familiar Python-based tools. The DOJO compiler handles the mapping from high-level tensor operations to the D1's RISC-V cores and custom vector units. Notably, DOJO does not require CUDA — it is entirely independent of NVIDIA's software ecosystem.The TPU and DOJO represent two points in a growing design space of domain-specific architectures (DSAs) for AI. They share a common thesis: for workloads that are large enough and well-defined enough, a purpose-built chip will outperform a general-purpose processor by one to two orders of magnitude in both performance and energy efficiency.The key architectural strategies these accelerators share are:The explosion of AI workloads — from datacenter-scale language model training to on-device inference in smartphones — has made this one of the most active areas in computer architecture. Beyond Google and Tesla, nearly every major technology company is developing or deploying custom AI silicon: Apple's Neural Engine, Amazon's Trainium and Inferentia, Microsoft's Maia, Meta's MTIA, and dozens of startups. The common thread is the same insight that drove Google in 2013: when a single type of computation dominates your workload, general-purpose hardware wastes too much silicon on flexibility you do not need.<br>Every architecture we have studied so far — from the CPU through the TPU — shares a common ancestor: the <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Von_Neumann_architecture" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Von_Neumann_architecture" target="_self">von Neumann model</a>. Instructions travel from memory to the processor, data travels from memory to the processor, results travel back. The processor is fast; the memory bus is the bottleneck. Engineers have spent seven decades optimizing this loop — caches, prefetchers, wider buses, deeper pipelines — but the fundamental separation of compute and storage remains.Nature solved the same problem differently. Your brain has roughly 86 billion neurons connected by approximately 100 trillion synapses, and every synapse is simultaneously a computing element and a memory element. There is no bus bottleneck because there is no bus. Computation happens where the data lives. The entire structure consumes about 20 watts — less than a laptop charger — yet it outperforms every supercomputer on Earth at tasks like recognizing faces in a crowd, understanding spoken language in a noisy room, or catching a ball thrown at an unexpected angle.<br><img alt="Neuromorphic computing concept: half biological brain, half digital circuit" src="assets/l10-neuromorphic-brain-circuit.jpeg" target="_self">Neuromorphic computing bridges neuroscience and engineering — designing silicon that processes information the way biological brains do.<br>Neuromorphic computing is the attempt to build silicon systems that replicate this architecture. The term was coined in the late 1980s by <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Carver_Mead" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Carver_Mead" target="_self">Carver Mead</a>, a Caltech professor who had already helped launch the VLSI revolution, together with his graduate student <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Misha_Mahowald" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Misha_Mahowald" target="_self">Misha Mahowald</a>. Mead observed that analog CMOS transistors operating in their subthreshold region behave remarkably like biological ion channels, and proposed building circuits that exploit this similarity rather than fighting it. The idea was radical: instead of forcing silicon to behave like a Boolean logic machine, let silicon behave like neurons.The materials used to build neuromorphic circuits are evolving as well. Early neuromorphic chips used standard silicon CMOS technology — the same fabrication process behind every CPU and GPU. But researchers are increasingly exploring memristors — two-terminal devices whose resistance changes based on the history of current that has flowed through them. A memristor naturally implements a synapse: the "weight" of the connection is stored as the device's resistance, and it persists without power. Memristor crossbar arrays could eventually replace SRAM-based synaptic weight storage, dramatically increasing density and reducing power further.The computational model at the heart of neuromorphic hardware is the spiking neural network (SNN), sometimes called the "third generation" of neural networks (after perceptrons and deep neural networks).In a biological neuron, incoming signals from other neurons arrive through dendrites and accumulate as electrical charge in the cell body. When the accumulated charge exceeds a threshold, the neuron fires a brief electrical pulse — a spike — down its axon to the dendrites of downstream neurons. After firing, the neuron resets and enters a short refractory period during which it cannot fire again. Between spikes, the accumulated charge slowly leaks away, like water draining from a bucket with a small hole.<br><img alt="Multi-level mapping from brain cortex to silicon chip" src="assets/l10-neuromorphic-brain-to-chip.png" target="_self">From brain to chip: neuromorphic engineering maps cortical structures through progressively finer levels — from brain regions to neuron arrays to circuit implementations on silicon.This behavior is fundamentally different from artificial neurons in conventional neural networks, which compute a weighted sum of inputs, pass it through an activation function (like ReLU or sigmoid), and output a continuous value — all in a single synchronous clock cycle. Biological neurons, and their SNN models, are stateful (they remember accumulated charge), temporal (the timing of spikes matters), and event-driven (computation only happens when a spike arrives or is emitted).The standard mathematical abstraction of this process is the Leaky Integrate-and-Fire (LIF) neuron model. For neuron j at discrete time step t:V_j[t] = V_j[t-1] + Σ(A_i · w_i,j · s_j) - λ_j
where V_j is the membrane potential, A_i is the activation of input neuron i, w_i,j is the synaptic weight connecting neuron i to neuron j, s_j is a gating variable, and λ_j is the leak term. When V_j crosses a threshold, the neuron emits a spike and resets.How SNNs differ from conventional deep neural networks (DNNs):The key insight is event-driven computation. In a conventional DNN running on a GPU, every neuron in every layer computes an activation value on every forward pass, whether or not the input has changed. In an SNN, a neuron only consumes energy when it spikes — and in typical workloads, only a small fraction of neurons spike at any given moment. This sparse, event-driven nature is what gives neuromorphic hardware its extraordinary energy efficiency: if 95% of neurons are silent, 95% of the chip draws near-zero power.The learning rule deserves special attention. In biological brains, synapses strengthen or weaken based on the relative timing of spikes in connected neurons — a mechanism called spike-timing-dependent plasticity (STDP). If neuron A consistently fires just before neuron B, the synapse from A to B strengthens ("neurons that fire together wire together"). If A fires just after B, the synapse weakens. This is a local learning rule — each synapse only needs information about its own two neurons, not a global error signal propagated through the entire network. STDP enables neuromorphic chips to learn on-device, at the edge, without a training server.There are also hybrid approaches: surrogate gradient methods allow training SNNs using backpropagation-like algorithms by approximating the non-differentiable spike function with a smooth surrogate. This bridges the gap between the mature DNN training ecosystem and the efficiency benefits of spiking networks — you can train in PyTorch and deploy on neuromorphic hardware.<br>Intel's <a data-tooltip-position="top" aria-label="https://www.intel.com/content/www/us/en/research/neuromorphic-computing.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.intel.com/content/www/us/en/research/neuromorphic-computing.html" target="_self">Loihi</a> is the most programmable neuromorphic chip available to researchers today. Unlike IBM's approach (which we will see next), Loihi was designed from the start as a research platform — flexible enough for scientists to experiment with different neuron models, learning rules, and network topologies.Loihi 1 (2018) integrates 128 neuromorphic cores and 3 embedded x86 cores on a 14 nm die of approximately 60 mm². Each neuromorphic core contains 1,024 spiking neural units, giving the chip a total capacity of 130,000 neurons and 128 million synapses, all backed by 33 MB of on-chip SRAM. The entire chip draws less than 1.5 watts.<br><img alt="Intel Loihi neuromorphic chip architecture" src="assets/l10-intel-loihi-architecture.png" target="_self">Intel Loihi chip architecture — 128 neuromorphic cores with on-chip learning, fabricated on Intel's 14nm process.The three x86 cores serve as management processors — they handle configuration, monitoring, and host communication while the 128 neuromorphic cores perform the actual spiking neural computation. This hybrid design reflects a practical reality: neuromorphic hardware is excellent at neural computation but still needs conventional processors for system management tasks.The performance advantage is striking. On sparse coding problems — a class of signal-processing tasks with direct applications in compressed sensing and feature extraction — Loihi demonstrated a 5,000x improvement in energy-delay product compared to conventional CPU and GPU solutions. The energy-delay product (energy x latency) is the standard metric for joint optimization of power and speed, and a 5,000x improvement is not an incremental gain; it represents a fundamentally different operating regime.<br>Loihi 2 (2021) moved to Intel 4 process technology, achieving roughly 10x the speed of its predecessor while adding programmable neuron models — researchers can define custom neuron behaviors beyond LIF, including more biologically realistic models with multiple compartments and dendritic processing. Intel also released <a data-tooltip-position="top" aria-label="https://github.com/lava-nc/lava" rel="noopener nofollow" class="external-link is-unresolved" href="https://github.com/lava-nc/lava" target="_self"><strong></strong></a>Lava, an open-source software framework for developing neuromorphic algorithms. Lava provides a Python-based programming model where developers define computational graphs of spiking processes, which the framework maps onto Loihi hardware. This addressed one of the field's biggest barriers: the lack of standardized development tools that would allow researchers to share and reproduce results across different neuromorphic platforms.Hala Point (2024), Intel's largest neuromorphic system, packs 1,152 Loihi 2 chips into a single six-rack-unit system, totaling 1.15 billion neurons and 128 billion synapses — approaching the neuron count of an owl's brain. Hala Point is a research platform for exploring large-scale neuromorphic workloads such as real-time scientific simulation and large-vocabulary keyword spotting. To put the scale in perspective: a fruit fly brain has roughly 100,000 neurons; a mouse brain has about 70 million; Hala Point sits between a mouse and a cat in raw neuron count, though the comparison is imperfect since biological neurons are far more complex than LIF units.IBM approached neuromorphic computing from a different angle, optimizing for extreme density and energy efficiency in inference workloads. Where Intel built a flexible research platform, IBM built chips intended for deployment — running specific neural network workloads as efficiently as possible.TrueNorth (2014) was a landmark chip: 4,096 neurosynaptic cores, each containing 256 programmable neurons and 256 × 256 = 65,536 synapses, for a total of 1 million neurons and 256 million synapses on a 28 nm die. Power consumption: approximately 70 milliwatts per square centimeter — around 10 watts for the full chip, roughly comparable to a hearing aid battery scaled up.TrueNorth uses a Globally Asynchronous, Locally Synchronous (GALS) design. Each core runs on its own local clock and communicates with other cores through an asynchronous 2D mesh network. This means there is no global clock signal threading through 4,096 cores — eliminating a major source of power consumption and timing complexity. Recall from L06 that distributing a clock signal across a large chip is one of the hardest problems in digital design — TrueNorth sidesteps it entirely.Within each core, the crossbar array maps inputs to outputs: vertical wires represent dendrites (incoming signals), horizontal wires represent axons (outgoing signals), and the intersection points are programmable synaptic weights. This is a direct silicon implementation of the neuron-synapse-axon structure. Each neuron in the crossbar implements the LIF model we saw earlier, with configurable leak, threshold, and reset parameters. The 2D mesh network connects cores in a grid topology, allowing spikes to propagate from one core's axons to another core's dendrites — mimicking the long-range axonal connections in biological neural tissue.<br><img alt="IBM NorthPole neuromorphic development board" src="assets/l10-ibm-northpole-board.jpeg" target="_self">IBM NorthPole board — the successor to TrueNorth, achieving 4,000x faster performance by interleaving compute with on-chip memory.NorthPole (2023) represents a dramatic leap. Built on 12 nm technology, NorthPole is 4,000x faster than TrueNorth on inference tasks while also being 22x faster than a comparable GPU at 25x less energy per inference. IBM achieved this by combining neuromorphic principles (in-memory computing, no off-chip memory access during inference) with optimizations for conventional deep neural network workloads. The key architectural decision: NorthPole stores all neural network weights on-chip, distributed across its 256 cores. During inference, no data crosses the chip boundary — eliminating the memory bandwidth bottleneck that limits GPU and TPU performance on inference workloads.NorthPole is not a general-purpose neuromorphic research chip like Loihi — it is an inference accelerator that borrows neuromorphic architectural ideas to eliminate the von Neumann bottleneck. This distinction illustrates an important point: the line between "neuromorphic" and "conventional accelerator with neuromorphic inspiration" is blurring. As the best ideas from neuromorphic research migrate into mainstream chip design, the neuromorphic label may eventually describe a design philosophy rather than a distinct product category.Where neuromorphic computing excels:
Autonomous vehicles and drones: real-time sensor fusion at milliwatt power budgets, where every joule extends flight time. Event-driven cameras (dynamic vision sensors) that output only pixel changes pair naturally with neuromorphic processors — both operate on sparse, asynchronous events.
Edge AI: always-on keyword detection, gesture recognition, and anomaly detection in IoT devices that cannot afford cloud connectivity or the latency of a round trip to the cloud.
Robotics: low-latency sensorimotor control with on-device learning — the robot adapts to a new environment without retraining in the cloud. The brain-like architecture maps naturally to the perception-action loops that robots require.
Pattern recognition: detecting subtle temporal patterns in sensor data (seismic activity, network intrusion, medical signals) — tasks where the timing of events matters, not just their values.
Advantages:
Extreme energy efficiency: only spiking neurons consume power; idle regions draw near-zero current. For perspective, running a keyword-detection model on Loihi consumes microwatts; the same model on a mobile GPU consumes milliwatts — a thousand-fold difference.
Low latency: in-memory computing eliminates the memory-bus round trip. Spike propagation through a neuromorphic chip can complete in microseconds, compared to milliseconds for the same inference on a conventional accelerator.
Real-time learning: STDP and local learning rules enable on-device adaptation without backpropagation or gradient computation.
Inherent parallelism: every neuron operates independently, enabling massive parallelism without the synchronization overhead of GPUs.
Limitations:
Accuracy gap: converting a trained DNN to an SNN typically incurs 1–3% accuracy loss, which is unacceptable for some applications
No standardization: every neuromorphic chip has its own programming model, instruction set, and development tools — there is no equivalent of CUDA or x86
Limited software ecosystem: the tools are research-grade, not production-grade; debugging a spiking neural network is vastly harder than debugging a PyTorch model
Narrow advantage: for workloads that are already well-served by GPUs or TPUs (large batch training, transformer inference), neuromorphic chips offer no clear benefit
A comparison of neuromorphic platforms:The neuromorphic field today is roughly where GPUs were in the early 2000s — the hardware shows extraordinary promise on specific benchmarks, but the software ecosystem needs to mature before mainstream adoption becomes realistic. The researchers and companies building that ecosystem now are laying the foundation for what could become a dominant paradigm in edge computing within the next decade.Every architecture discussed so far — from CPUs to neuromorphic chips — operates on classical bits: physical systems that are definitively 0 or 1 at any given moment. A transistor is either conducting or not. A voltage is either high or low. Every computation, no matter how complex, reduces to manipulating these definite binary states.Quantum computing abandons this certainty. It exploits phenomena from quantum mechanics — superposition, entanglement, and interference — to perform certain computations exponentially faster than any classical machine could ever achieve.This is not simply a faster processor. It is a fundamentally different model of computation, as different from a classical computer as a classical computer is from an abacus. Where neuromorphic computing reimagines the architecture while keeping classical physics, quantum computing reimagines the physics while keeping a broadly familiar gate-based architecture.To understand quantum computing, you need three concepts from quantum physics. None of them match everyday intuition — and they should not. Quantum mechanics describes the behavior of matter at the atomic and subatomic scale, where the rules are fundamentally different from the macroscopic world you experience daily. The strangeness is not a deficiency of human understanding; it is a feature of reality. And it is precisely this strangeness that quantum computers exploit.Superposition. A classical bit is either 0 or 1. A quantum bit — a qubit — can exist in a superposition of both states simultaneously. Physically, a qubit can be any two-level quantum system: the spin direction of an electron (up or down), the polarization of a photon (horizontal or vertical), or the current direction in a superconducting loop (clockwise or counterclockwise). Mathematically, a qubit's state is described as α|0⟩ + β|1⟩, where α and β are complex numbers called probability amplitudes and |α|² + |β|² = 1. When you measure the qubit, you get 0 with probability |α|² and 1 with probability |β|². Before measurement, the qubit is genuinely in both states — this is not ignorance about which state it is in; it is a physical property of the system.<br>The famous <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Double-slit_experiment" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Double-slit_experiment" target="_self">double-slit experiment</a> demonstrates this wave-particle duality: a single photon (or electron) passing through two slits creates an interference pattern on a detector screen — as if it went through both slits simultaneously. This experiment, which <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Richard_Feynman" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Richard_Feynman" target="_self">Richard Feynman</a> called "the only mystery of quantum mechanics," is the physical foundation of superposition.<br><img alt="Slide explaining qubit properties and parallel computation" src="assets/l10-quantum-qubits-text.png" target="_self">Key quantum computing principles: qubits replace bits, enabling 2^N simultaneous operations — but practical observability of parallel worlds remains a fundamental challenge.<br>Entanglement. When two qubits become entangled, their states are correlated in a way that has no classical analogue. Measuring one qubit instantaneously determines the state of the other, regardless of the distance between them. Einstein famously called this <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Quantum_entanglement" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Quantum_entanglement" target="_self">"spooky action at a distance"</a>, and the phenomenon troubled him deeply. But decades of experiments — notably <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Bell_test" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Bell_test" target="_self">Bell test experiments</a> — have confirmed that entanglement is real. For quantum computing, entanglement is a resource: it allows qubits to share information without explicit communication, enabling algorithms that have no classical equivalent.Interference. This is the engine of quantum computation. Just as waves can constructively or destructively interfere (two peaks combine into a bigger peak; a peak and a trough cancel out), the probability amplitudes of qubits can interfere. A well-designed quantum algorithm arranges its operations so that the amplitudes of wrong answers destructively interfere (cancel toward zero) while the amplitudes of correct answers constructively interfere (amplify toward one). The result: when you measure the qubits at the end of the computation, you are overwhelmingly likely to get the right answer.A common misconception is that quantum computers "try all possibilities at once" and simply pick the right one. This is misleading. The superposition does encode all possibilities, but measuring a superposition gives you just one random outcome. The art of quantum algorithm design is constructing a sequence of gates that causes the correct answer's amplitude to grow and incorrect answers' amplitudes to shrink through interference — so that the one outcome you do measure is overwhelmingly likely to be the one you want.A quantum computer manipulates qubits through a sequence of quantum gates — unitary transformations that rotate, entangle, and phase-shift qubit states. A quantum gate is to a qubit what a logic gate is to a classical bit, but with a critical difference: quantum gates are reversible (no information is destroyed) and operate on probability amplitudes rather than definite values.Common quantum gates include:
Hadamard gate (H): puts a qubit into equal superposition — the "coin flip" gate that starts most quantum algorithms. Applied to |0⟩, it produces (|0⟩ + |1⟩)/√2
Pauli-X gate: the quantum equivalent of a classical NOT gate — flips |0⟩ to |1⟩ and vice versa
CNOT gate (Controlled-NOT): entangles two qubits — flips the target qubit if and only if the control qubit is |1⟩. This two-qubit gate is the primary source of entanglement in quantum circuits
Phase gates (S, T, Rz): rotate the phase of a qubit's amplitude without changing the measurement probabilities — these are essential for interference and form the basis of many quantum algorithms
A quantum algorithm is a carefully choreographed sequence of these gates, analogous to a classical circuit diagram but operating on probability amplitudes instead of definite bit values. Just as any classical computation can be built from NAND gates, any quantum computation can be built from a small set of universal quantum gates (typically the Hadamard, CNOT, and T gates).The power of quantum computing comes from parallelism at the physical level. With N qubits in superposition, the quantum computer represents and manipulates 2^N states simultaneously. A 300-qubit quantum computer could represent more states than there are atoms in the observable universe — a number that no classical computer could ever enumerate, let alone process.<br><img alt="Quantum computer dilution refrigerator" src="assets/l10-quantum-computer.jpeg" target="_self">A quantum computer's dilution refrigerator — superconducting qubits must be cooled to near absolute zero (about 15 millikelvins) to maintain quantum coherence.But there is a catch: decoherence. Qubits are extraordinarily fragile. Any interaction with the environment — a stray photon, a vibration, a thermal fluctuation — can cause the quantum state to "collapse" into a classical state, destroying the superposition. This is why quantum computers require extreme isolation.<br>The most common hardware approach — superconducting qubits — operates inside <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Dilution_refrigerator" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Dilution_refrigerator" target="_self">dilution refrigerators</a> that cool the processor to approximately 15 millikelvin (0.015 K), colder than outer space. The dilution refrigerator is the distinctive chandelier-like structure you see in photographs of quantum computers — multiple nested stages of cooling, each bringing the temperature down by another order of magnitude, with the qubit chip at the very bottom. The entire apparatus can be three meters tall and cost millions of dollars, all to keep a chip the size of a fingernail cold enough for quantum effects to dominate.Other hardware approaches include:
Trapped ions: individual atoms suspended in electromagnetic fields; long coherence times but slower gate operations
Photonic qubits: particles of light guided through optical circuits; naturally resistant to decoherence but hard to entangle
Topological qubits: encode information in the topology of quantum states, making them theoretically more robust to errors (Microsoft's approach, still in early development)
Each approach has different trade-offs between coherence time, gate fidelity, and scalability, and it remains unclear which will ultimately prevail.A quantum computer is not faster than a classical computer at everything. This is one of the most important points to understand about quantum computing, because popular media often presents it as a universal upgrade. In reality, for most everyday tasks — word processing, web browsing, sorting a list — a quantum computer offers no advantage and may even be slower (since quantum gates are currently much slower than classical transistor switches). The power of quantum computing is concentrated in specific problem classes where the mathematical structure of the problem allows quantum algorithms to provide an exponential or polynomial speedup.Theoretical (Oracle) Algorithms:These algorithms assume a "black box" (oracle) function and prove that quantum machines need fewer queries to determine properties of that function:
Deutsch's algorithm (1985): determines whether a function is constant or balanced using a single query, versus two classically. The first proof that quantum computation could outperform classical computation. Though the problem itself is trivial, the algorithm demonstrated a principle — quantum parallelism via superposition — that has profound implications.
Bernstein-Vazirani algorithm (1993): finds a hidden bit string encoded in a function using one query, versus N classically. This extended Deutsch's insight to a more general setting and showed that the quantum advantage scales with problem size.
Simon's algorithm (1994): finds the period of a function exponentially faster than any classical algorithm. This was the first example of an exponential quantum speedup (not just a constant factor) and directly inspired the most important quantum algorithm ever discovered.
Practical Algorithms:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Shor%27s_algorithm" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Shor%27s_algorithm" target="_self">Shor's algorithm</a> (1994): factors large integers in polynomial time. Classical algorithms require sub-exponential time — this is the mathematical hardness assumption underlying <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/RSA_(cryptosystem)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/RSA_(cryptosystem)" target="_self">RSA encryption</a>. A sufficiently large quantum computer running Shor's algorithm would break RSA and most current public-key cryptography. This single algorithm transformed quantum computing from a theoretical curiosity into a national security concern.
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Grover%27s_algorithm" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Grover%27s_algorithm" target="_self">Grover's algorithm</a> (1996): searches an unsorted database of N items in O(sqrt(N)) time, versus O(N) classically. This is a quadratic speedup — less dramatic than Shor's exponential speedup, but applicable to a much wider class of problems, including optimization, constraint satisfaction, and database search.
To put these speedups in concrete terms: factoring a 2,048-bit RSA key using the best known classical algorithm would take longer than the age of the universe. Shor's algorithm on a sufficiently large, error-corrected quantum computer could do it in hours. Searching an unsorted database of one trillion records takes up to one trillion steps classically; Grover's algorithm needs about one million.<br>It is worth noting what quantum computers cannot speed up. Problems that are already efficiently solvable classically — sorting, searching a sorted list, most graph algorithms — gain little or nothing from quantum computation. The <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Quantum_computing#Potential" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Quantum_computing#Potential" target="_self">quantum speedup is not universal</a>; it is concentrated in problems with specific mathematical structure that quantum interference can exploit (periodicity for Shor's, unstructured search for Grover's, Hamiltonian simulation for molecular modeling).The journey from theory to physical hardware has been slow but accelerating:Notice the pace of progress: it took 13 years from the first 2-qubit computer (1998) to reach practical double-digit qubits, but only another 8 years to reach quantum supremacy (2019), and just 4 more years to cross 1,000 qubits (2023). The challenge now is not adding more qubits — it is making them reliable enough to perform useful computations.The Google quantum supremacy claim in 2019 deserves a caveat: the computation was specifically designed to be hard for classical computers but easy for quantum computers, and IBM subsequently argued that a classical supercomputer could have solved the same problem in days rather than 10,000 years by using sufficient disk storage. The debate highlighted that "quantum supremacy" is a moving target — as classical algorithms and hardware improve, the bar for demonstrating a genuine quantum advantage keeps rising.The central challenge of quantum computing is error correction. Current qubits are "noisy" — they lose coherence and accumulate errors far faster than classical transistors. The error rate of a single qubit gate is typically around 0.1–1%, compared to roughly 10^-18 for a classical transistor. A single logical qubit (reliable enough for sustained computation) may require hundreds or thousands of physical qubits dedicated to error correction — using redundancy to detect and fix errors before they corrupt the result. This is why a 1,000-qubit machine today cannot yet run Shor's algorithm on cryptographically relevant key sizes: most of those qubits would be consumed by error correction, leaving too few for the actual computation.<br>IBM's <a data-tooltip-position="top" aria-label="https://www.ibm.com/quantum/roadmap" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.ibm.com/quantum/roadmap" target="_self">quantum roadmap</a> targets 200 logical qubits capable of executing 100 million gates by 2029 — a system that could tackle previously intractable problems in materials science, drug discovery, and optimization. Google's breakthrough with the Willow chip in late 2024 was a critical proof of concept: for the first time, adding more qubits to an error-correcting code reduced the overall error rate, crossing the threshold that theorists had predicted was necessary for scalable quantum computing.Industry analysts project quantum computing to become a $1.3 trillion industry by 2035, driven by applications in:
Cryptanalysis: breaking current encryption schemes (and motivating post-quantum replacements)
Molecular simulation: modeling chemical reactions at the quantum level for drug discovery and materials science — a task for which quantum computers have a natural advantage, since molecules are themselves quantum systems
Financial modeling: portfolio optimization, risk analysis, and derivative pricing involve combinatorial problems where quantum speedups apply
Artificial intelligence: quantum machine learning algorithms may accelerate training of certain model types, though this remains the most speculative application area
Optimization: logistics, scheduling, and supply chain problems involve searching enormous solution spaces where quantum algorithms may find good solutions faster than classical heuristics
Quantum computing is not expected to replace classical computing — it will complement it. The most likely near-term paradigm is hybrid quantum-classical computing, where a classical computer handles the bulk of the work and delegates specific sub-problems (optimization steps, molecular energy calculations) to a quantum co-processor. This is analogous to how GPUs and TPUs complement CPUs today.<br>In the nearer term, the rise of quantum computing has already forced a concrete response: post-quantum cryptography. NIST <a data-tooltip-position="top" aria-label="https://www.nist.gov/news-events/news/2024/08/nist-releases-first-3-finalized-post-quantum-encryption-standards" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.nist.gov/news-events/news/2024/08/nist-releases-first-3-finalized-post-quantum-encryption-standards" target="_self">finalized its first post-quantum cryptographic standards in 2024</a>, selecting algorithms based on lattice problems and hash functions that are believed to be resistant to both classical and quantum attacks. Every organization that handles sensitive data with long-term value — medical records, government communications, financial instruments — must plan for the transition, even though large-scale quantum computers are still years away.The urgency comes from a strategy known as "harvest now, decrypt later": adversaries can intercept and store encrypted communications today, then decrypt them once a sufficiently powerful quantum computer becomes available. If the data has a shelf life of 20 years (medical records, state secrets, infrastructure plans), and a quantum computer capable of breaking RSA arrives in 15 years, then the data is already at risk today. This is why the migration to post-quantum cryptography is happening now, not when quantum computers arrive.Over the course of this lecture, you have surveyed nine fundamentally different approaches to computation — from the general-purpose CPU that can run any program, through domain-specific accelerators like DSPs, GPUs, and TPUs, to paradigm-breaking architectures like neuromorphic and quantum computers. Each sits at a different point on the spectrum from universal (can do anything, optimized for nothing) to specialized (does one thing, does it extraordinarily well). The following table summarizes the trade-offs:A few patterns emerge from this table:
Flexibility and performance are inversely correlated. The CPU can run any software but is outperformed by every specialized architecture on its native workload. An ASIC crushes everything on its target task but cannot be reprogrammed. This is not coincidence — it is a fundamental engineering trade-off. General-purpose hardware must include circuitry for every possible operation; specialized hardware can dedicate all its transistors to the operations it actually performs.
Power follows performance density. Architectures that pack more useful computation per clock cycle per watt (neuromorphic, ASIC, DSP) win on power. Architectures that brute-force parallelism (GPU, quantum) consume proportionally more.
No single architecture wins everywhere. This is the fundamental reason that modern systems are heterogeneous.
The boundary between categories is blurring. Modern GPUs have tensor cores (specialized matrix units), making them partly TPU-like. Modern CPUs have SIMD units (AVX-512, NEON), making them partly GPU-like. FPGAs can be configured to emulate any of the other architectures. Intel's latest FPGAs include hard-wired AI tensor blocks alongside the programmable fabric. The categories in this table are useful mental models, not rigid boxes.
Maturity varies enormously. CPUs and GPUs have decades of software ecosystem behind them. TPUs are maturing rapidly backed by Google's investment. Neuromorphic chips are in the research-to-early-deployment transition. Quantum computers remain in the research stage for most practical purposes. When evaluating an architecture, the software ecosystem matters as much as the hardware specifications.
<br>The overarching trend in computer architecture over the past two decades has been the shift from general-purpose processors to domain-specific accelerators. This is not a fashion — it is a response to physics. As <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Dennard_scaling" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Dennard_scaling" target="_self">Dennard scaling</a> ended (L01) and single-core frequency plateaued around 2005, the only path to higher performance was specialization: build hardware that does less, but does it in fewer cycles and at lower energy per operation.<br>This trend has a name in the research literature: the "domain-specific architecture" era, identified by <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Computer_Architecture:_A_Quantitative_Approach" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Computer_Architecture:_A_Quantitative_Approach" target="_self">John Hennessy and David Patterson</a> (of RISC fame) in their 2017 Turing Award lecture. Their thesis: the end of Moore's Law-driven performance gains means the next decade of computing progress will come primarily from specialization — and the architects who design the next generation of accelerators will define the industry's trajectory as profoundly as the microprocessor designers of the 1970s did.The result is heterogeneous computing — systems that combine multiple architecture types, each handling the workload it is best suited for. Your smartphone is already a heterogeneous computer: it contains a general-purpose CPU (ARM Cortex), a GPU (Adreno or Mali), a neural processing unit (NPU) for on-device ML, a digital signal processor for audio and modem baseband, and multiple fixed-function ASICs for camera ISP, video encode/decode, and cryptographic operations. Apple's M-series chips integrate CPU cores, GPU cores, a Neural Engine (NPU), media encode/decode engines, and a unified memory architecture onto a single die — a System-on-Chip (SoC) that embodies heterogeneous computing in silicon.At the data center scale, the picture is even more diverse. Cloud providers offer instances backed by GPUs (NVIDIA A100/H100), TPUs (Google), FPGAs (Intel/Xilinx), custom AI accelerators (AWS Inferentia/Trainium), and SmartNICs that offload networking from the CPU. The orchestration layer — deciding which workload runs on which accelerator — is becoming as important as the accelerators themselves.<br>This trend has implications for software engineering as well. Frameworks like <a data-tooltip-position="top" aria-label="https://developer.nvidia.com/cuda-toolkit" rel="noopener nofollow" class="external-link is-unresolved" href="https://developer.nvidia.com/cuda-toolkit" target="_self">CUDA</a> (for GPUs), <a data-tooltip-position="top" aria-label="https://www.khronos.org/opencl/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.khronos.org/opencl/" target="_self">OpenCL</a> (cross-platform accelerator programming), <a data-tooltip-position="top" aria-label="https://www.tensorflow.org/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.tensorflow.org/" target="_self">TensorFlow</a> and <a data-tooltip-position="top" aria-label="https://pytorch.org/" rel="noopener nofollow" class="external-link is-unresolved" href="https://pytorch.org/" target="_self">PyTorch</a> (which abstract over GPUs and TPUs), and Intel's Lava (for neuromorphic chips) each provide a programming model tailored to a specific hardware paradigm. The programmer's job increasingly involves choosing the right abstraction layer for the target hardware — and understanding enough about the underlying architecture to make that choice wisely.This heterogeneity has a direct consequence for you as a computer scientist: understanding architectural concepts is no longer optional, even if you never design hardware. When you write code that runs on a GPU, you need to understand SIMD parallelism and memory coalescing. When you deploy a model to an edge device, you need to know whether the target has an NPU and what quantization constraints it imposes. When you design a system that handles sensitive data, you need to understand why post-quantum cryptography matters and what computational assumptions it rests on.The nine architectures in this lecture are not nine isolated topics — they are nine points on a continuum that you will navigate throughout your career:
The CPU gave you the foundation — the general-purpose, instruction-at-a-time model that all software starts from.
The DSP showed you what happens when you optimize for a specific signal-processing dataflow — MAC units, circular buffers, Harvard architecture.
The GPU showed you massive data parallelism — thousands of simple cores executing the same operation on different data.
The FPGA showed you reconfigurable hardware — the ability to change the circuit itself rather than the software.
VLIW showed you compiler-driven scheduling — moving complexity from hardware to the compiler.
The ASIC showed you the endpoint of specialization — maximum performance, zero flexibility.
The TPU showed you how a new workload (machine learning) can justify an entirely new architecture — systolic arrays for matrix multiplication.
Neuromorphic computing showed you that the von Neumann model itself is negotiable — compute can happen where data lives.
Quantum computing showed you that the classical model of computation — bits, gates, deterministic logic — is not the only game in physics.
Each architecture embodies a different answer to the same question: given finite transistors, finite energy, and finite time, what is the best way to organize computation for a given class of problems?The architectures that matter ten years from now may not exist yet. Research labs are already exploring:
In-memory computing: performing logic operations directly inside DRAM or flash memory chips, eliminating data movement entirely — the ultimate solution to the von Neumann bottleneck
Optical computing: using photons instead of electrons for certain matrix operations, potentially achieving enormous throughput at very low energy
DNA storage: encoding data in synthetic DNA strands for archival purposes — a single gram of DNA can theoretically store 215 petabytes
Analog computing revival: using continuous voltages rather than discrete bits for certain workloads (neural networks, differential equations), trading precision for energy efficiency
Each of these is, at this writing, where quantum computing was in the 1990s — theoretically promising but practically immature.What will not change are the fundamental concepts: the trade-offs between flexibility and efficiency, the tension between general and special purpose, the laws of physics that constrain every design, and the economic forces that determine which architectures get built in volume. Understanding these concepts — not memorizing the specifications of any particular chip — is what allows a computer scientist to navigate a field that reinvents its hardware every decade while keeping its principles for a century. That is what this lecture, and this course, have equipped you to do. Specialization beats generality — when a workload is large enough and well-defined enough, a domain-specific architecture (DSP, GPU, ASIC, TPU) will outperform a general-purpose CPU by orders of magnitude in performance and energy efficiency. The design space is a spectrum — from fully flexible (CPU) through reconfigurable (FPGA) to fully fixed (ASIC), with GPU, DSP, VLIW, and TPU occupying intermediate positions. The right choice depends on volume, flexibility needs, and performance requirements. Parallelism takes many forms — SIMD in DSPs, SIMT in GPUs, systolic arrays in TPUs, spiking networks in neuromorphic chips, and quantum superposition in quantum computers all exploit parallelism differently. Memory architecture matters as much as compute — Harvard architecture in DSPs, HBM in GPUs/TPUs, distributed SRAM in Tesla DOJO, and in-memory computing in neuromorphic chips all address the von Neumann bottleneck in different ways. The future is heterogeneous — modern systems combine CPUs, GPUs, and specialized accelerators. Understanding each architecture's strengths lets you choose the right tool for each computational task. <br><a data-tooltip-position="top" aria-label="https://www.dspguide.com/ch28/3.htm" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.dspguide.com/ch28/3.htm" target="_self">The Scientist and Engineer's Guide to DSP — Architecture</a> — DSP Harvard and Super Harvard (SHARC) architecture
<br><a data-tooltip-position="top" aria-label="https://learnopencv.com/modern-gpu-architecture-explained/" rel="noopener nofollow" class="external-link is-unresolved" href="https://learnopencv.com/modern-gpu-architecture-explained/" target="_self">Inside the GPU: A Guide to Modern Graphics Architecture</a> — GPU architecture, SMs, memory hierarchy
<br><a data-tooltip-position="top" aria-label="https://www.yosefk.com/blog/simd-simt-smt-parallelism-in-nvidia-gpus.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.yosefk.com/blog/simd-simt-smt-parallelism-in-nvidia-gpus.html" target="_self">SIMD &lt; SIMT &lt; SMT: Parallelism in NVIDIA GPUs</a> — SIMD vs SIMT vs SMT trade-offs
<br><a data-tooltip-position="top" aria-label="https://anysilicon.com/ultimate-guide-asic-application-specific-integrated-circuit/" rel="noopener nofollow" class="external-link is-unresolved" href="https://anysilicon.com/ultimate-guide-asic-application-specific-integrated-circuit/" target="_self">Ultimate Guide: ASIC</a> — ASIC types, design flow, cost analysis
<br><a data-tooltip-position="top" aria-label="https://learn.sparkfun.com/tutorials/how-does-an-fpga-work/all" rel="noopener nofollow" class="external-link is-unresolved" href="https://learn.sparkfun.com/tutorials/how-does-an-fpga-work/all" target="_self">How Does an FPGA Work?</a> — FPGA architecture, LUTs, CLBs
<br><a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/very-long-instruction-word-vliw-architecture/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/very-long-instruction-word-vliw-architecture/" target="_self">VLIW Architecture</a> — VLIW concepts, compiler scheduling, implementations
<br><a data-tooltip-position="top" aria-label="https://blog.bytebytego.com/p/how-googles-tensor-processing-unit" rel="noopener nofollow" class="external-link is-unresolved" href="https://blog.bytebytego.com/p/how-googles-tensor-processing-unit" target="_self">How Google's TPU Works</a> — TPU systolic arrays, generations, performance
<br><a data-tooltip-position="top" aria-label="https://www.nextplatform.com/2022/08/23/inside-teslas-innovative-and-homegrown-dojo-ai-supercomputer/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.nextplatform.com/2022/08/23/inside-teslas-innovative-and-homegrown-dojo-ai-supercomputer/" target="_self">Inside Tesla's DOJO AI Supercomputer</a> — D1 chip, training tiles, ExaPod
<br><a data-tooltip-position="top" aria-label="https://www.ibm.com/think/topics/neuromorphic-computing" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.ibm.com/think/topics/neuromorphic-computing" target="_self">What Is Neuromorphic Computing?</a> — Brain-inspired computing, SNNs, applications
<br><a data-tooltip-position="top" aria-label="https://open-neuromorphic.org/blog/truenorth-deep-dive-ibm-neuromorphic-chip-design/" rel="noopener nofollow" class="external-link is-unresolved" href="https://open-neuromorphic.org/blog/truenorth-deep-dive-ibm-neuromorphic-chip-design/" target="_self">TrueNorth Deep Dive</a> — IBM TrueNorth neurosynaptic architecture
<br><a data-tooltip-position="top" aria-label="https://open-neuromorphic.org/neuromorphic-computing/hardware/loihi-intel/" rel="noopener nofollow" class="external-link is-unresolved" href="https://open-neuromorphic.org/neuromorphic-computing/hardware/loihi-intel/" target="_self">Intel Loihi</a> — Intel's neuromorphic research chip
<br><a data-tooltip-position="top" aria-label="https://www.ibm.com/think/topics/quantum-computing" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.ibm.com/think/topics/quantum-computing" target="_self">What Is Quantum Computing?</a> — Qubits, algorithms, current status
]]></description><link>lectures/eng/l10-eng.-overview-of-modern-architectures.html</link><guid isPermaLink="false">Lectures/ENG/L10-ENG. Overview of Modern Architectures.md</guid><pubDate>Mon, 23 Mar 2026 19:52:07 GMT</pubDate><enclosure url="assets/l10-dsp-adc-dac-diagram.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l10-dsp-adc-dac-diagram.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L11-Matrix-Access-Patterns]]></title><description><![CDATA[<img src="assets/l11-matrix-access-patterns.svg" target="_self">]]></description><link>assets/l11-matrix-access-patterns.html</link><guid isPermaLink="false">assets/L11-Matrix-Access-Patterns.svg</guid><pubDate>Mon, 23 Mar 2026 19:49:18 GMT</pubDate><enclosure url="assets/l11-matrix-access-patterns.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l11-matrix-access-patterns.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L11-MESI-State-Diagram]]></title><description><![CDATA[<img src="assets/l11-mesi-state-diagram.svg" target="_self">]]></description><link>assets/l11-mesi-state-diagram.html</link><guid isPermaLink="false">assets/L11-MESI-State-Diagram.svg</guid><pubDate>Mon, 23 Mar 2026 19:38:11 GMT</pubDate><enclosure url="assets/l11-mesi-state-diagram.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l11-mesi-state-diagram.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L11-MESI-State-Diagram-ENG]]></title><description><![CDATA[<img src="assets/l11-mesi-state-diagram-eng.svg" target="_self">]]></description><link>assets/l11-mesi-state-diagram-eng.html</link><guid isPermaLink="false">assets/L11-MESI-State-Diagram-ENG.svg</guid><pubDate>Mon, 23 Mar 2026 19:37:53 GMT</pubDate><enclosure url="assets/l11-mesi-state-diagram-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l11-mesi-state-diagram-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L11-Coherence-Problem]]></title><description><![CDATA[<img src="assets/l11-coherence-problem.svg" target="_self">]]></description><link>assets/l11-coherence-problem.html</link><guid isPermaLink="false">assets/L11-Coherence-Problem.svg</guid><pubDate>Mon, 23 Mar 2026 19:37:15 GMT</pubDate><enclosure url="assets/l11-coherence-problem.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l11-coherence-problem.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L11-Coherence-Problem-ENG]]></title><description><![CDATA[<img src="assets/l11-coherence-problem-eng.svg" target="_self">]]></description><link>assets/l11-coherence-problem-eng.html</link><guid isPermaLink="false">assets/L11-Coherence-Problem-ENG.svg</guid><pubDate>Mon, 23 Mar 2026 19:37:00 GMT</pubDate><enclosure url="assets/l11-coherence-problem-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l11-coherence-problem-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L11-Write-Policies]]></title><description><![CDATA[<img src="assets/l11-write-policies.svg" target="_self">]]></description><link>assets/l11-write-policies.html</link><guid isPermaLink="false">assets/L11-Write-Policies.svg</guid><pubDate>Mon, 23 Mar 2026 19:36:44 GMT</pubDate><enclosure url="assets/l11-write-policies.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l11-write-policies.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L11-Matrix-Access-Patterns-ENG]]></title><description><![CDATA[<img src="assets/l11-matrix-access-patterns-eng.svg" target="_self">]]></description><link>assets/l11-matrix-access-patterns-eng.html</link><guid isPermaLink="false">assets/L11-Matrix-Access-Patterns-ENG.svg</guid><pubDate>Mon, 23 Mar 2026 19:36:43 GMT</pubDate><enclosure url="assets/l11-matrix-access-patterns-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l11-matrix-access-patterns-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L11-False-Sharing]]></title><description><![CDATA[<img src="assets/l11-false-sharing.svg" target="_self">]]></description><link>assets/l11-false-sharing.html</link><guid isPermaLink="false">assets/L11-False-Sharing.svg</guid><pubDate>Mon, 23 Mar 2026 19:36:42 GMT</pubDate><enclosure url="assets/l11-false-sharing.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l11-false-sharing.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L11-Direct-Mapped-Cache]]></title><description><![CDATA[<img src="assets/l11-direct-mapped-cache.svg" target="_self">]]></description><link>assets/l11-direct-mapped-cache.html</link><guid isPermaLink="false">assets/L11-Direct-Mapped-Cache.svg</guid><pubDate>Mon, 23 Mar 2026 19:36:31 GMT</pubDate><enclosure url="assets/l11-direct-mapped-cache.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l11-direct-mapped-cache.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L11-Write-Policies-ENG]]></title><description><![CDATA[<img src="assets/l11-write-policies-eng.svg" target="_self">]]></description><link>assets/l11-write-policies-eng.html</link><guid isPermaLink="false">assets/L11-Write-Policies-ENG.svg</guid><pubDate>Mon, 23 Mar 2026 19:36:27 GMT</pubDate><enclosure url="assets/l11-write-policies-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l11-write-policies-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L11-False-Sharing-ENG]]></title><description><![CDATA[<img src="assets/l11-false-sharing-eng.svg" target="_self">]]></description><link>assets/l11-false-sharing-eng.html</link><guid isPermaLink="false">assets/L11-False-Sharing-ENG.svg</guid><pubDate>Mon, 23 Mar 2026 19:36:20 GMT</pubDate><enclosure url="assets/l11-false-sharing-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l11-false-sharing-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L11-Associativity-Comparison]]></title><description><![CDATA[<img src="assets/l11-associativity-comparison.svg" target="_self">]]></description><link>assets/l11-associativity-comparison.html</link><guid isPermaLink="false">assets/L11-Associativity-Comparison.svg</guid><pubDate>Mon, 23 Mar 2026 19:36:08 GMT</pubDate><enclosure url="assets/l11-associativity-comparison.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l11-associativity-comparison.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L11-Direct-Mapped-Cache-ENG]]></title><description><![CDATA[<img src="assets/l11-direct-mapped-cache-eng.svg" target="_self">]]></description><link>assets/l11-direct-mapped-cache-eng.html</link><guid isPermaLink="false">assets/L11-Direct-Mapped-Cache-ENG.svg</guid><pubDate>Mon, 23 Mar 2026 19:35:53 GMT</pubDate><enclosure url="assets/l11-direct-mapped-cache-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l11-direct-mapped-cache-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L11-Cache-Topology]]></title><description><![CDATA[<img src="assets/l11-cache-topology.svg" target="_self">]]></description><link>assets/l11-cache-topology.html</link><guid isPermaLink="false">assets/L11-Cache-Topology.svg</guid><pubDate>Mon, 23 Mar 2026 19:35:45 GMT</pubDate><enclosure url="assets/l11-cache-topology.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l11-cache-topology.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L11-Associativity-Comparison-ENG]]></title><description><![CDATA[<img src="assets/l11-associativity-comparison-eng.svg" target="_self">]]></description><link>assets/l11-associativity-comparison-eng.html</link><guid isPermaLink="false">assets/L11-Associativity-Comparison-ENG.svg</guid><pubDate>Mon, 23 Mar 2026 19:35:19 GMT</pubDate><enclosure url="assets/l11-associativity-comparison-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l11-associativity-comparison-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L11-Cache-Topology-ENG]]></title><description><![CDATA[<img src="assets/l11-cache-topology-eng.svg" target="_self">]]></description><link>assets/l11-cache-topology-eng.html</link><guid isPermaLink="false">assets/L11-Cache-Topology-ENG.svg</guid><pubDate>Mon, 23 Mar 2026 19:35:16 GMT</pubDate><enclosure url="assets/l11-cache-topology-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l11-cache-topology-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L11-Processor-Memory-Gap]]></title><description><![CDATA[<img src="assets/l11-processor-memory-gap.svg" target="_self">]]></description><link>assets/l11-processor-memory-gap.html</link><guid isPermaLink="false">assets/L11-Processor-Memory-Gap.svg</guid><pubDate>Mon, 23 Mar 2026 19:34:46 GMT</pubDate><enclosure url="assets/l11-processor-memory-gap.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l11-processor-memory-gap.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L11-Cache-Line-Anatomy]]></title><description><![CDATA[<img src="assets/l11-cache-line-anatomy.svg" target="_self">]]></description><link>assets/l11-cache-line-anatomy.html</link><guid isPermaLink="false">assets/L11-Cache-Line-Anatomy.svg</guid><pubDate>Mon, 23 Mar 2026 19:34:31 GMT</pubDate><enclosure url="assets/l11-cache-line-anatomy.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l11-cache-line-anatomy.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L11-Processor-Memory-Gap-ENG]]></title><description><![CDATA[<img src="assets/l11-processor-memory-gap-eng.svg" target="_self">]]></description><link>assets/l11-processor-memory-gap-eng.html</link><guid isPermaLink="false">assets/L11-Processor-Memory-Gap-ENG.svg</guid><pubDate>Mon, 23 Mar 2026 19:34:22 GMT</pubDate><enclosure url="assets/l11-processor-memory-gap-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l11-processor-memory-gap-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L11-Cache-Line-Anatomy-ENG]]></title><description><![CDATA[<img src="assets/l11-cache-line-anatomy-eng.svg" target="_self">]]></description><link>assets/l11-cache-line-anatomy-eng.html</link><guid isPermaLink="false">assets/L11-Cache-Line-Anatomy-ENG.svg</guid><pubDate>Mon, 23 Mar 2026 19:34:14 GMT</pubDate><enclosure url="assets/l11-cache-line-anatomy-eng.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l11-cache-line-anatomy-eng.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L10-Quantum-Qubits-Text]]></title><description><![CDATA[<img src="assets/l10-quantum-qubits-text.png" target="_self">]]></description><link>assets/l10-quantum-qubits-text.html</link><guid isPermaLink="false">assets/L10-Quantum-Qubits-Text.png</guid><pubDate>Mon, 23 Mar 2026 19:23:57 GMT</pubDate><enclosure url="assets/l10-quantum-qubits-text.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l10-quantum-qubits-text.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L10-Quantum-Computer]]></title><description><![CDATA[<img src="assets/l10-quantum-computer.jpeg" target="_self">]]></description><link>assets/l10-quantum-computer.html</link><guid isPermaLink="false">assets/L10-Quantum-Computer.jpeg</guid><pubDate>Mon, 23 Mar 2026 19:23:57 GMT</pubDate><enclosure url="assets/l10-quantum-computer.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l10-quantum-computer.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L10-IBM-NorthPole-Board]]></title><description><![CDATA[<img src="assets/l10-ibm-northpole-board.jpeg" target="_self">]]></description><link>assets/l10-ibm-northpole-board.html</link><guid isPermaLink="false">assets/L10-IBM-NorthPole-Board.jpeg</guid><pubDate>Mon, 23 Mar 2026 19:23:57 GMT</pubDate><enclosure url="assets/l10-ibm-northpole-board.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l10-ibm-northpole-board.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L10-Neuromorphic-Brain-To-Chip]]></title><description><![CDATA[<img src="assets/l10-neuromorphic-brain-to-chip.png" target="_self">]]></description><link>assets/l10-neuromorphic-brain-to-chip.html</link><guid isPermaLink="false">assets/L10-Neuromorphic-Brain-To-Chip.png</guid><pubDate>Mon, 23 Mar 2026 19:23:57 GMT</pubDate><enclosure url="assets/l10-neuromorphic-brain-to-chip.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l10-neuromorphic-brain-to-chip.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L10-Intel-Loihi-Architecture]]></title><description><![CDATA[<img src="assets/l10-intel-loihi-architecture.png" target="_self">]]></description><link>assets/l10-intel-loihi-architecture.html</link><guid isPermaLink="false">assets/L10-Intel-Loihi-Architecture.png</guid><pubDate>Mon, 23 Mar 2026 19:23:57 GMT</pubDate><enclosure url="assets/l10-intel-loihi-architecture.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l10-intel-loihi-architecture.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L10-Neuromorphic-Brain-Circuit]]></title><description><![CDATA[<img src="assets/l10-neuromorphic-brain-circuit.jpeg" target="_self">]]></description><link>assets/l10-neuromorphic-brain-circuit.html</link><guid isPermaLink="false">assets/L10-Neuromorphic-Brain-Circuit.jpeg</guid><pubDate>Mon, 23 Mar 2026 19:23:57 GMT</pubDate><enclosure url="assets/l10-neuromorphic-brain-circuit.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l10-neuromorphic-brain-circuit.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L10-Tesla-DOJO-Training-Tile]]></title><description><![CDATA[<img src="assets/l10-tesla-dojo-training-tile.jpeg" target="_self">]]></description><link>assets/l10-tesla-dojo-training-tile.html</link><guid isPermaLink="false">assets/L10-Tesla-DOJO-Training-Tile.jpeg</guid><pubDate>Mon, 23 Mar 2026 19:23:57 GMT</pubDate><enclosure url="assets/l10-tesla-dojo-training-tile.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l10-tesla-dojo-training-tile.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L10-Tesla-DOJO-D1-Die]]></title><description><![CDATA[<img src="assets/l10-tesla-dojo-d1-die.jpeg" target="_self">]]></description><link>assets/l10-tesla-dojo-d1-die.html</link><guid isPermaLink="false">assets/L10-Tesla-DOJO-D1-Die.jpeg</guid><pubDate>Mon, 23 Mar 2026 19:23:57 GMT</pubDate><enclosure url="assets/l10-tesla-dojo-d1-die.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l10-tesla-dojo-d1-die.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L10-Tesla-DOJO-Stack]]></title><description><![CDATA[<img src="assets/l10-tesla-dojo-stack.jpeg" target="_self">]]></description><link>assets/l10-tesla-dojo-stack.html</link><guid isPermaLink="false">assets/L10-Tesla-DOJO-Stack.jpeg</guid><pubDate>Mon, 23 Mar 2026 19:23:57 GMT</pubDate><enclosure url="assets/l10-tesla-dojo-stack.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l10-tesla-dojo-stack.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L10-TPU-Google]]></title><description><![CDATA[<img src="assets/l10-tpu-google.jpeg" target="_self">]]></description><link>assets/l10-tpu-google.html</link><guid isPermaLink="false">assets/L10-TPU-Google.jpeg</guid><pubDate>Mon, 23 Mar 2026 19:23:57 GMT</pubDate><enclosure url="assets/l10-tpu-google.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l10-tpu-google.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L10-VLIW-Pipeline]]></title><description><![CDATA[<img src="assets/l10-vliw-pipeline.png" target="_self">]]></description><link>assets/l10-vliw-pipeline.html</link><guid isPermaLink="false">assets/L10-VLIW-Pipeline.png</guid><pubDate>Mon, 23 Mar 2026 19:23:57 GMT</pubDate><enclosure url="assets/l10-vliw-pipeline.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l10-vliw-pipeline.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L10-VLIW-Execution-Diagram]]></title><description><![CDATA[<img src="assets/l10-vliw-execution-diagram.png" target="_self">]]></description><link>assets/l10-vliw-execution-diagram.html</link><guid isPermaLink="false">assets/L10-VLIW-Execution-Diagram.png</guid><pubDate>Mon, 23 Mar 2026 19:23:57 GMT</pubDate><enclosure url="assets/l10-vliw-execution-diagram.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l10-vliw-execution-diagram.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L10-VLIW-Transmeta-Crusoe]]></title><description><![CDATA[<img src="assets/l10-vliw-transmeta-crusoe.jpeg" target="_self">]]></description><link>assets/l10-vliw-transmeta-crusoe.html</link><guid isPermaLink="false">assets/L10-VLIW-Transmeta-Crusoe.jpeg</guid><pubDate>Mon, 23 Mar 2026 19:23:57 GMT</pubDate><enclosure url="assets/l10-vliw-transmeta-crusoe.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l10-vliw-transmeta-crusoe.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L10-FPGA-Structure]]></title><description><![CDATA[<img src="assets/l10-fpga-structure.png" target="_self">]]></description><link>assets/l10-fpga-structure.html</link><guid isPermaLink="false">assets/L10-FPGA-Structure.png</guid><pubDate>Mon, 23 Mar 2026 19:23:57 GMT</pubDate><enclosure url="assets/l10-fpga-structure.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l10-fpga-structure.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L10-FPGA-Xilinx-Spartan6]]></title><description><![CDATA[<img src="assets/l10-fpga-xilinx-spartan6.jpeg" target="_self">]]></description><link>assets/l10-fpga-xilinx-spartan6.html</link><guid isPermaLink="false">assets/L10-FPGA-Xilinx-Spartan6.jpeg</guid><pubDate>Mon, 23 Mar 2026 19:23:57 GMT</pubDate><enclosure url="assets/l10-fpga-xilinx-spartan6.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l10-fpga-xilinx-spartan6.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L10-Bitcoin-Mining-Difficulty]]></title><description><![CDATA[<img src="assets/l10-bitcoin-mining-difficulty.jpeg" target="_self">]]></description><link>assets/l10-bitcoin-mining-difficulty.html</link><guid isPermaLink="false">assets/L10-Bitcoin-Mining-Difficulty.jpeg</guid><pubDate>Mon, 23 Mar 2026 19:23:57 GMT</pubDate><enclosure url="assets/l10-bitcoin-mining-difficulty.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l10-bitcoin-mining-difficulty.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L10-ASIC-Crypto-Mining-Rig]]></title><description><![CDATA[<img src="assets/l10-asic-crypto-mining-rig.jpeg" target="_self">]]></description><link>assets/l10-asic-crypto-mining-rig.html</link><guid isPermaLink="false">assets/L10-ASIC-Crypto-Mining-Rig.jpeg</guid><pubDate>Mon, 23 Mar 2026 19:23:57 GMT</pubDate><enclosure url="assets/l10-asic-crypto-mining-rig.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l10-asic-crypto-mining-rig.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L10-GPU-Thread-Execution]]></title><description><![CDATA[<img src="assets/l10-gpu-thread-execution.jpeg" target="_self">]]></description><link>assets/l10-gpu-thread-execution.html</link><guid isPermaLink="false">assets/L10-GPU-Thread-Execution.jpeg</guid><pubDate>Mon, 23 Mar 2026 19:23:57 GMT</pubDate><enclosure url="assets/l10-gpu-thread-execution.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l10-gpu-thread-execution.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L10-CPU-vs-GPU-Cores]]></title><description><![CDATA[<img src="assets/l10-cpu-vs-gpu-cores.jpeg" target="_self">]]></description><link>assets/l10-cpu-vs-gpu-cores.html</link><guid isPermaLink="false">assets/L10-CPU-vs-GPU-Cores.jpeg</guid><pubDate>Mon, 23 Mar 2026 19:23:57 GMT</pubDate><enclosure url="assets/l10-cpu-vs-gpu-cores.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l10-cpu-vs-gpu-cores.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L10-GPU-NVIDIA-SM-Architecture]]></title><description><![CDATA[<img src="assets/l10-gpu-nvidia-sm-architecture.png" target="_self">]]></description><link>assets/l10-gpu-nvidia-sm-architecture.html</link><guid isPermaLink="false">assets/L10-GPU-NVIDIA-SM-Architecture.png</guid><pubDate>Mon, 23 Mar 2026 19:23:57 GMT</pubDate><enclosure url="assets/l10-gpu-nvidia-sm-architecture.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l10-gpu-nvidia-sm-architecture.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L10-GPU-Intel-Flex-Series]]></title><description><![CDATA[<img src="assets/l10-gpu-intel-flex-series.jpeg" target="_self">]]></description><link>assets/l10-gpu-intel-flex-series.html</link><guid isPermaLink="false">assets/L10-GPU-Intel-Flex-Series.jpeg</guid><pubDate>Mon, 23 Mar 2026 19:23:57 GMT</pubDate><enclosure url="assets/l10-gpu-intel-flex-series.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l10-gpu-intel-flex-series.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L10-DSP-Harvard-Architecture]]></title><description><![CDATA[<img src="assets/l10-dsp-harvard-architecture.png" target="_self">]]></description><link>assets/l10-dsp-harvard-architecture.html</link><guid isPermaLink="false">assets/L10-DSP-Harvard-Architecture.png</guid><pubDate>Mon, 23 Mar 2026 19:23:57 GMT</pubDate><enclosure url="assets/l10-dsp-harvard-architecture.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l10-dsp-harvard-architecture.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L10-DSP-Instructions-Table]]></title><description><![CDATA[<img src="assets/l10-dsp-instructions-table.png" target="_self">]]></description><link>assets/l10-dsp-instructions-table.html</link><guid isPermaLink="false">assets/L10-DSP-Instructions-Table.png</guid><pubDate>Mon, 23 Mar 2026 19:23:57 GMT</pubDate><enclosure url="assets/l10-dsp-instructions-table.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l10-dsp-instructions-table.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L10-CPU-Closeup]]></title><description><![CDATA[<img src="assets/l10-cpu-closeup.png" target="_self">]]></description><link>assets/l10-cpu-closeup.html</link><guid isPermaLink="false">assets/L10-CPU-Closeup.png</guid><pubDate>Mon, 23 Mar 2026 19:23:57 GMT</pubDate><enclosure url="assets/l10-cpu-closeup.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l10-cpu-closeup.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L10-DSP-Chip-Vintage]]></title><description><![CDATA[<img src="assets/l10-dsp-chip-vintage.jpeg" target="_self">]]></description><link>assets/l10-dsp-chip-vintage.html</link><guid isPermaLink="false">assets/L10-DSP-Chip-Vintage.jpeg</guid><pubDate>Mon, 23 Mar 2026 19:23:57 GMT</pubDate><enclosure url="assets/l10-dsp-chip-vintage.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l10-dsp-chip-vintage.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L10-DSP-ADC-DAC-Diagram]]></title><description><![CDATA[<img src="assets/l10-dsp-adc-dac-diagram.jpeg" target="_self">]]></description><link>assets/l10-dsp-adc-dac-diagram.html</link><guid isPermaLink="false">assets/L10-DSP-ADC-DAC-Diagram.jpeg</guid><pubDate>Mon, 23 Mar 2026 19:23:57 GMT</pubDate><enclosure url="assets/l10-dsp-adc-dac-diagram.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l10-dsp-adc-dac-diagram.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L10-DSP-TMS320-Chip]]></title><description><![CDATA[<img src="assets/l10-dsp-tms320-chip.jpeg" target="_self">]]></description><link>assets/l10-dsp-tms320-chip.html</link><guid isPermaLink="false">assets/L10-DSP-TMS320-Chip.jpeg</guid><pubDate>Mon, 23 Mar 2026 19:23:57 GMT</pubDate><enclosure url="assets/l10-dsp-tms320-chip.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l10-dsp-tms320-chip.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Консультація - 2026-03-21]]></title><description><![CDATA[Дата: 21 березня 2026 р. (субота)
Тривалість: ~39 хвилин
Формат: онлайн, відеоконференціяТема роботи з файлами була пропущена на дев'ятій практиці через те, що її не було додано до Syllabus вчасно. Для проєктів, де потрібна робота з файлами, буде підготовлений окремий матеріал — сторінка з поясненнями, можливо з аудіо/відео записом.Основна логіка: робота з файлами — це рівень операційної системи, тому використовується переривання INT 21h (DOS). Хоча тему можна нагуглити самостійно, деякі нюанси низькорівневих систем можна довго шукати або знайти не те, тому викладач підготує цільовий конспект.Детально обговорювалася конкретна проблема в проєкті «менеджер паролів» із XOR-шифруванням.При XOR-шифруванні кожен байт даних XOR-иться з відповідним байтом пароля циклічно. Магічний заголовок (4 байти на початку файлу) використовується для перевірки правильності введеного пароля при дешифруванні. Проблема: якщо перші N символів пароля правильні (де N — довжина магічного заголовка), а решта — ні, то магічний заголовок розшифрується коректно, і система вважатиме пароль правильним, хоча решта даних буде зіпсована.Зробити магічний заголовок довшим — не 4 байти, а щонайменше стільки, скільки максимальна довжина пароля (наприклад, 20 байт). Тоді кожен символ пароля буде задіяний у перевірці магічного заголовка, і часткове співпадіння не пройде.Це проблема специфічна саме для XOR. Для будь-яких реальних криптографічних функцій 4-байтового магічного заголовка достатньо, оскільки зміна навіть одного символу пароля повністю змінює весь результат дешифрування.
Контрольна сума пароля — можна порахувати контрольну суму мастер-пароля і зберегти її. При дешифруванні порівнювати. Підхід працює, але потребує надійної функції контрольної суми. Обмеження: перевіряються лише перші N символів (при 4-байтовій сумі).
Подвійне шифрування (спочатку XOR з паролем, потім XOR з магічним заголовком) — не має великого сенсу: якщо хтось знає магічний заголовок, другий шар знімається тривіально.
Ключова перевага магічного заголовка достатньої довжини: не потрібно зберігати жодної інформації про пароль — ні контрольних сум, ні зашифрованої версії. Є лише зашифрований файл і відомий магічний заголовок.Було роз'яснено фундаментальне непорозуміння щодо сегменту .data:
Сегмент даних — це просто набір байтів, які ви виділили.
Мітки (labels) — це поіменовані зміщення (offsets) від початку сегменту, а не «змінні» у традиційному розумінні.
Коли ви пишете label1 DB 5, асемблер просто запам'ятовує, що label1 вказує на зміщення, де лежить цей байт.
Можна написати кілька міток підряд без DB — вони всі вказуватимуть на одне й те саме місце.
Можна вільно перезаписувати пам'ять за будь-якою міткою. Фізично ніщо не забороняє це зробити. Але якщо ви перезаписали дані, а потім спробуєте прочитати оригінальне значення — це буде логічна помилка у вашій програмі, не фізична. Можна виділити великий блок пам'яті (DB 256 DUP(?)) і керувати зміщеннями вручну або через EQU.
Мітки без DB/DW можна використовувати як «вказівники» на довільні місця у цьому блоці.
Асемблер автоматично перераховує всі зміщення при першому проході компіляції; при другому — генерує машинний код з уже підставленими числовими офсетами.
Ключове слово public/extern потрібне лише для роботи з бібліотеками (лінкування); при використанні include це не потрібно — всі мітки і так доступні.
Обговорювалася техніка створення анімацій у DOS.Відеоконтролер підтримує кілька сторінок (фреймів) відеопам'яті. Техніка:
Користувач бачить фрейм 0 (відображається на екрані).
Тим часом програма заповнює фрейм 1 (невидимий).
Коли фрейм 1 готовий — переключаємо відеоконтролер на нього.
Користувач миттєво бачить нову картинку.
Тепер заповнюємо фрейм 0 і так далі.
Без подвійної буферизації, якщо малювати прямо у видимий буфер, буде видно процес заповнення — мерехтіння, «смикання», неякісна картинка. При 25+ переключеннях за секунду людина сприймає це як плавну анімацію.Адреси відеопам'яті: B800h для текстового режиму, B000h (або A000h) для графічного.Не обов'язково тримати всі варіанти спрайтів (повороти, нахили) у пам'яті. Можна зберігати лише один базовий спрайт, а трансформації обчислювати на льоту під час копіювання у відеобуфер — наприклад, повернути на 90° означає скопіювати байти з іншим зміщенням за певною формулою (зазвичай це проста арифметика, максимум — множення на матрицю).Це класичний компроміс між пам'яттю та обчисленнями: менше пам'яті — більше обчислень, і навпаки. Потрібно дивитися на швидкість процесора та обсяг доступної пам'яті.В однопоточному середовищі DOS, поки програма малює екран, вона не може одночасно читати клавіатуру. Але натискання клавіш зберігаються у клавіатурному буфері і можуть бути прочитані пізніше.Якщо потрібна негайна реакція на натискання під час відмальовки — потрібно перехоплювати апаратні переривання клавіатури й миші та писати власний обробник. Але для більшості проєктів достатньо читати клавіатурний буфер після завершення відмальовки.В DOS є таймерне переривання, яке викликається 18,2 рази на секунду. Його можна використовувати як аналог requestAnimationFrame з веб-розробки — для періодичного оновлення екрану.DosBox навмисно сповільнений, щоб старі DOS-ігри працювали з оригінальною швидкістю. Але сучасні процесори в тисячі разів швидші за комп'ютери ери DOS. Якщо продуктивності не вистачає, DosBox можна прискорити параметрами запуску — подробиці є в документації. Це корисно, зокрема, для автоматичного прогону тестів.Було розказано про щорічні змагання, які зазвичай проводяться на курсі.Завдання — написати інтерпретатор певної мови на Assembly. Перемагає той, у кого розмір скомпільованого .COM-файлу найменший (при умові проходження всіх тестів). Попередні роки:
Перший рік: інтерпретатор мови Brainfuck. Починали з ~1500 байт, найкращі результати — 135 байт.
Другий рік: інтерпретатор нормальних алгоритмів Маркова. Також доходили до ~120 байт. Звільнення від відвідування пар на час змагань.
Максимальні бали за ці пари ставляться автоматично.
Додаткові серйозні бонусні бали за перші місця.
Глибоке розуміння оптимізації коду на низькому рівні.
Ризик: якщо не вдається завершити змагання, доводиться повертатися до звичайного проєкту з втраченими балами за пропущені пари.Буде оголошене найближчим часом (орієнтовно наступного тижня). Рішення про запуск залежить від того, наскільки студенти встигають із проєктами: як тільки з'явиться достатньо тих, кому стає «нудно» з поточним проєктом, змагання стартує.На запитання щодо форматування ASM-коду у VS Code:
Раніше на курсі використовувався розширення MASM/TASM (назва орієнтовна), яке форматувало код і дозволяло запускати DosBox кнопкою.
Проблеми: складне налаштування, глюки, не працювало на деяких Mac та Windows, працювало лише з одним файлом. Близько 10% студентів мали проблеми, які забирали багато часу.
Цього року від нього відмовилися на користь командного рядка — більше практичного досвіду і менше залежності від капризів розширення.
Проте існують окремі розширення тільки для форматування ASM-коду (syntax highlighting), без емуляції — їх можна встановлювати. Можливо, з минулого року автори покращили той повний розширення, але це не перевірялося.
Якщо є побоювання, що розширення «зіпсує» код — при частих комітах в Git будь-які зміни одразу видно в diff, і легко відкотити.
Було виявлено помилку в Python-симуляторі, який використовується для генерації ключів (правильних відповідей) до контрольної. Баг: при роботі з піврегістрами (AL, BL, AH, BH) симулятор отримує неправильні значення. З повними регістрами (AX, BX) все працює коректно.Ключі для варіантів, де задіяні піврегістри, будуть перегенеровані.На запитання, що ставити в клітинках для BX та CX на початку трасування, коли присвоєння ще не було:
Допустимі три варіанти: 0, ?, або прочерк — усі вважаються правильними.
У ключах стоять нулі, тому що DOS при запуску програми зазвичай обнулює регістри, хоча формально вони undefined.
Для пам'яті (наприклад, DB ?) значення дійсно не визначені і можуть бути ненульовими.
На результат трасування це не впливає, оскільки всі регістри далі отримують конкретні значення через MOV. Наступного тижня орієнтовно будуть роздані перевірені контрольні роботи.
Запитання можна ставити в загальному чаті або напряму викладачам — не обов'язково чекати суботньої консультації.
]]></description><link>office-hours/консультація-2026-03-21.html</link><guid isPermaLink="false">Office-hours/Консультація - 2026-03-21.md</guid><pubDate>Sat, 21 Mar 2026 14:15:50 GMT</pubDate></item><item><title><![CDATA[README]]></title><description><![CDATA[
Матеріал: лекції L01–L07, лабораторні Lab01–Lab07
Максимальний бал: 15
Тривалість: 70 хвилин
Формат: 4 завдання на 3 сторінках A4, письмово
Дозволені матеріали: нічого (closed-book)
Файл <a class="internal-link" data-href="demo_variant.pdf" href="midterm/demo_variant.html" target="_self" rel="noopener nofollow">demo_variant.pdf</a> — приклад контрольної з тим самим форматом і типами завдань, але іншими даними.<br>Рекомендуємо розв'язати демо-варіант самостійно, а потім звірити з відповідями: <a class="internal-link" data-href="demo_key.md" href="midterm/demo_key.html" target="_self" rel="noopener nofollow">demo_key.md</a>.
Переведення між двійковою, шістнадцятковою та десятковою системами
Додавання та віднімання у двійковій системі (8 та 16 біт)
Доповняльний код (two's complement): перетворення від'ємних чисел Як встановлюються CF, ZF, SF, OF після арифметичних та логічних операцій
Різниця між CF (беззнакове переповнення) та OF (знакове переповнення)
Поведінка прапорців для різних інструкцій (ADD, SUB, INC, AND, XOR, SHL, SHR, NEG)
Які прапорці перевіряють різні умовні переходи (JZ, JNZ, JC, JNC, JS, JO, JA, JB, JG, JL тощо) Покрокове виконання інструкцій та відстеження стану регістрів
Розуміння циклів з LOOP та CX як лічильник
Умовні переходи: CMP + Jcc — які інструкції виконуються, які пропускаються
Робота з інструкціями: MOV, ADD, SUB, INC, DEC, MUL, DIV, AND, OR, XOR, SHL, SHR, NEG, TEST, CMP, XCHG Механізм стека: PUSH/POP, рух SP, порядок LIFO
Адреси у стеку: стек росте у бік молодших адрес
CALL/RET: що зберігається на стеку при виклику процедури
Вкладені виклики процедур та рекурсія
Стекові фрейми: роль BP, доступ до параметрів та локальних змінних
Збереження та відновлення регістрів через стек Архітектура фон Неймана: п'ять компонентів, вузьке місце
Відмінності фон Неймана та Гарвардської архітектури
Цикл виконання інструкції (fetch–decode–execute)
Ієрархія пам'яті: регістри, кеш, DRAM, SSD, HDD
Порядок байтів (little-endian) у пам'яті x86
Сегментна адресація в реальному режимі: обчислення фізичної адреси (сегмент × 16 + зміщення)
Реальний vs захищений режим процесора
Переривання: апаратні та програмні, таблиця векторів переривань
Процес завантаження комп'ютера (POST → BIOS → MBR → ОС)
Закон Мура Тренуйте ручне трасування. Найбільше балів (5 з 15) — за покрокове виконання коду. Виписуйте стан регістрів після кожної інструкції. Знайте прапорці. Зрозумійте, коли CF=1, а коли OF=1 — це різні ситуації. Перевірте на прикладах із INC, ADD, SUB, NEG. Практикуйте стек на папері. Малюйте стек, відзначайте SP, адреси та значення. Звертайте увагу, що PUSH зменшує SP, а POP збільшує. Не забувайте про порядок байтів. x86 — little-endian: молодший байт зберігається за молодшою адресою. Теоретичні питання — коротко і по суті. Не потрібно писати есе — достатньо чіткої відповіді у 2–3 реченнях. ]]></description><link>midterm/readme.html</link><guid isPermaLink="false">midterm/README.md</guid><pubDate>Tue, 17 Mar 2026 13:26:48 GMT</pubDate></item><item><title><![CDATA[demo_key]]></title><description><![CDATA[(а) AL = 0000 1110b = 0Eh (14)(б) CF=1, ZF=0, SF=0, OF=1(в) Так. JC виконується, якщо CF=1. CF=1 — перехід виконається.(а) Таблиця трасування:(б) AX = 000Fh (15). Цикл виконується 3 рази. AX = 3 × 5 = 15.(а) Стек:Після PUSH AX: SP=00FEh, [00FE]=1111h
Після PUSH BX: SP=00FCh, [00FC]=2222h, [00FE]=1111h
Після POP CX: SP=00FEh, CX=2222h
Після POP DX: SP=0100h, DX=1111h
(б) CX = 2222h, DX = 1111h. LIFO: останнє збережене (BX=2222h) витягується першим у CX, перше збережене (AX=1111h) витягується другим у DX.4.1. Фізична адреса = 1A00h × 10h + 2B3Ch = 1A000h + 2B3Ch = 1CB3Ch. Інша пара: наприклад, 1CB3h:000Ch (1CB3h × 10h + 000Ch = 1CB30h + 000Ch = 1CB3Ch).4.2. +45 = 00101101. Інвертуємо: 11010010. Додаємо 1: 11010011. Відповідь: −45 = 11010011 (D3h).4.3. Крок 1: SP зменшується на 2 (SP = SP − 2). Крок 2: значення AX записується за адресою SS:SP. Стек x86 росте у бік молодших адрес.]]></description><link>midterm/demo_key.html</link><guid isPermaLink="false">midterm/demo_key.md</guid><pubDate>Tue, 17 Mar 2026 13:26:42 GMT</pubDate></item><item><title><![CDATA[Консультація - 2026-03-14]]></title><description><![CDATA[Дата: 14 березня 2026 р. (субота)
Тривалість: ~52 хвилини
Формат: онлайн, відеоконференціяНа кожній контрольній точці (КТ) обидва студенти в парному проєкті мають мати написаний код. Недостатньо лише скласти план або розписати ролі — код є обов'язковою умовою. Під час оцінювання перевіряється рекомендований план проєкту, і якщо код відсутній, оцінка буде знижена.Кожен проєкт має файл з рекомендованим планом по тижнях, де для кожного студента (A і B) прописані конкретні задачі: налаштування репозиторію, створення скелету main.asm, узгодження змінних, станів гри тощо. Студентам рекомендовано дотримуватись цього плану як мінімуму.Якщо студенти працюють швидше за рекомендований план — це нормально і навіть заохочується. Хто завершить раніше, отримає додаткові бали за наступні контрольні точки автоматично. Додатковий час варто використати на тестування та інтеграцію — саме на «стиках» різного функціоналу часто виникають проблеми, які потребують більше часу, ніж здається.Порядок виконання задач не є строгим. Можна орієнтуватися як на assessment.md, так і на рекомендований план. Головне — щоб був код і видно прогрес від точки до точки. Якщо студенти вирішили змінити порядок виконання, це слід явно зазначити та обґрунтувати в коментарях до чекпоінту (файл checkpoints.md).Очікуваний обсяг роботи — приблизно 8 годин на тиждень на кожного студента. У парних проєктах (а це переважна більшість) робота має бути розподілена приблизно порівну між обома учасниками. Ситуації, коли один зробив 95% роботи, є неприйнятними.Рекомендований план — це мінімум. Він не гарантує максимальну оцінку, але якщо не виконувати навіть мінімум, проєкт можна не встигнути завершити.Через три тижні від поточного моменту планується оголошення конкурсу серед найкращих проєктів. Цього року проєкти розраховані на 8 тижнів роботи (порівняно з 2–2,5 тижнями у попередні роки), що дає студентам більше часу на практику та підготовку до конкурсу.Було наголошено на кількох важливих вимогах до організації репозиторіїв:
Код має лежати в каталозі src/, а не в кореневій директорії.
Не створювати зайвих підкаталогів із назвою проєкту всередині репозиторію. Файл README.md та інші стандартні файли мають бути на верхньому рівні, інакше GitHub їх не підхоплює автоматично.
Чекпоінти фіксуються у файлі checkpoints.md, а фінальна відеопрезентація описується в окремому файлі.
На курсі ~70 репозиторіїв. Коли в кожному своя нестандартна структура, це суттєво ускладнює перевірку. Уніфікована структура — це повага до часу перевіряючих.
Значна частина консультації була присвячена практикам роботи з Git.Для парних проєктів рекомендується працювати в одній (main) гілці, а не створювати окремі гілки для кожного студента. Причини:
У команді з двох людей гілкування створює зайві складнощі.
Паралельна робота в різних гілках протягом кількох днів призводить до великої кількості конфліктів при мержі.
Конфлікти, що виникають при частих комітах в одну гілку, зазвичай маленькі й легко вирішуються, бо зміни ще свіжі в пам'яті.
Гілкування доцільне у випадках:
Глибокий рефакторинг, який «ламає» проєкт на кілька днів.
Робота над функціоналом, який тривалий час перебуває у нестабільному стані.
Open-source проєкти, де використовуються форки та pull-request'и.
Було детально розглянуто різницю між двома підходами:
git pull — забирає зміни з сервера і створює мерж-коміт. При частому використанні кількома людьми історія комітів перетворюється на «дерево» навіть при роботі в одній гілці.
git fetch + git rebase — забирає зміни та переставляє ваші коміти поверх них. Історія залишається чистою, лінійною. Проте rebase — потенційно деструктивна операція, і при ній також можуть виникнути конфлікти. Використовувати свідомо.
Для чистої та зрозумілої історії комітів рекомендується fetch + rebase.
Конфлікти виникають, коли двоє редагують один і той самий рядок одного файлу.
Якщо двоє додають нові функції в різних частинах файлу — Git зазвичай мержить це автоматично.
Конфлікт може виникнути, якщо обидва додали код після одного й того ж місця (наприклад, після однієї функції), навіть якщо зміни незалежні.
Такі конфлікти зазвичай вирішуються в один клік — обрати порядок: спочатку ваш код, потім колеги, або навпаки.
Складніше, коли один рядок змінено по-різному (наприклад, один поставив крапку, інший — знак оклику).
Ключова порада: часто пулити/фетчити та часто пушити. Це мінімізує конфлікти, бо обидва студенти завжди працюють з актуальною версією коду.
Писати англійською.
Використовувати наказовий спосіб (imperative mood): Add function, Fix bug, а не Added, Fixed або Додано.
Логіка: стандартні Git-коміти (merge, pull, push) написані у наказовому способі. Щоб ваші коміти не вибивалися — дотримуйтесь того ж стилю.
Формула перевірки: «This commit will ___» → «Add sorting algorithm» (правильно), «Added sorting algorithm» (неправильно).
Абракадабра в коміт-повідомленнях неприпустима — це ускладнює перевірку та розуміння історії проєкту. Контрольна пишеться на папері (листочки у форматі наданої раніше PDF).
Є нульовий (демо) варіант для тренування; на самій контрольній будуть варіанти від першого і далі (схожі за типами завдань).
Буде дві контрольні роботи за семестр.
Відповіді до нульового варіанту будуть опубліковані.
Системи числення та арифметика:
Перетворення між системами числення (двійковою, шістнадцятковою, десятковою).
Додавання та віднімання у двійковій системі.
Від'ємні числа у доповняльному коді (two's complement): інвертувати біти → додати 1.
Арифметика у шістнадцятковій системі (наприклад, A + 2 = C).
Прапорці (flags) та їх логіка:
Carry flag, Overflow flag, Zero flag, Parity flag — коли і чому виставляються.
Розуміти логіку, а не зубрити, яка команда виставляє які прапорці.
Наприклад: Zero flag виставляється, коли результат дорівнює нулю. Якщо це зрозуміло — зубрити нічого не треба.
Умовні переходи (jumps):
Будуть завдання на джампи, пов'язані з прапорцями.
Щоб визначити, чи відбудеться перехід, потрібно зрозуміти стан прапорців після попередньої операції.
Сегментна адресація:
Обчислення фізичної адреси: сегмент × 16 + зміщення.
Приклад: DS = 1A00h, зміщення = 2B3Ch → фізична адреса = 1A000h + 2B3Ch = 1CB3Ch.
На практиці: дописати 0 праворуч від сегменту, потім додати зміщення «у стовпчик».
Трасування коду:
Відстеження значень регістрів після виконання послідовності команд.
Запис результатів у шістнадцятковому та двійковому форматі.
Стек:
Розуміння адреси повернення при виклику процедури (CALL).
У стек записується адреса інструкції, наступної після CALL.
Теоретичні питання:
Архітектури фон Неймана та Гарвардська — відмінності.
Ієрархія пам'яті: регістри → кеш → оперативна пам'ять.
Порядок байтів (Little-endian).
Реальний та захищений режими роботи процесора — відмінності, обсяг адресованої пам'яті.
Порядок завантаження комп'ютера (POST → завантажувач → ОС тощо).
Порада: якщо ви самостійно писали код у домашніх завданнях і проєкті — з практичними завданнями проблем не буде. Повторити варто теоретичні теми, які не «набиваються» при програмуванні.На запитання про включення файлів та бібліотек було роз'яснено:
Асемблер обробляє include шляхом текстової підстановки: весь вміст вказаного файлу вставляється замість рядка include.
Після обробки всіх include отримується один великий файл, з якого генерується машинний код.
Тому всі мітки (змінні), оголошені в сегменті .data, автоматично доступні в усіх included файлах. У main.asm — заголовки (.model small, .data, .stack тощо) та рядки include.
В окремих файлах — лише процедури (proc ... endp), без заголовків.
Файли групувати логічно: display.asm для процедур відображення, logic.asm для ігрової логіки тощо.
Ключові слова public та extern стосуються роботи з бібліотеками, які компонуються (лінкуються) окремо. На поточному рівні курсу це не потрібно — достатньо використовувати include.Було наголошено: деякі студенти не пишуть код самостійно, і це помітно. Код, згенерований ШІ, не тренує навичок. На контрольній, де треба писати від руки на папері, це стане критичною проблемою. Рекомендація — писати код руками, щоб набити руку для контрольної та другої контрольної, де буде ще більше практичних завдань.
Робота з файлами через переривання INT 21h — буде додана до матеріалів (теорія файликом + можливо практичне заняття).
Таблиця переривань — планується як окрема тема. Правильні відповіді до нульового варіанту контрольної будуть опубліковані.
Питання по контрольній та проєктах можна ставити у загальному чаті курсу — так відповіді побачать усі.
Результати (підсумок) консультації також будуть опубліковані.
На контрольній відповіді мають бути лаконічними: 2–3 речення достатньо, щоб показати розуміння. Писати 50 рядків не потрібно.
]]></description><link>office-hours/консультація-2026-03-14.html</link><guid isPermaLink="false">Office-hours/Консультація - 2026-03-14.md</guid><pubDate>Sat, 14 Mar 2026 15:30:04 GMT</pubDate></item><item><title><![CDATA[demo_variant]]></title><link>midterm/demo_variant.html</link><guid isPermaLink="false">midterm/demo_variant.pdf</guid><pubDate>Wed, 11 Mar 2026 22:12:34 GMT</pubDate></item><item><title><![CDATA[Lab09]]></title><description><![CDATA[
Курс: Архітектура обчислювальних систем
Тиждень: 9
Тривалість: 2 академічні години
Тип: Практична робота під наглядом інструктора
Після завершення практики студент зможе:
Знаходити та декодувати поля Program Segment Prefix (PSP) у вікні Dump Turbo Debugger.
Пояснити різницю між форматами COM та EXE: структура файлу, обмеження, процес завантаження.
Читати командний рядок програми з PSP (зміщення 80h) та обробляти його.
Писати програми у форматі COM (ORG 100h, один сегмент).
Розбирати командний рядок на окремі аргументи (парсинг рядків).
Перегляньте лекційні матеріали за Тиждень 9 (моделі обчислень, проєктування ISA). Зверніть увагу на розділи про формати виконуваних файлів DOS та Program Segment Prefix.Коли DOS завантажує програму (.COM або .EXE), вона спочатку створює блок з 256 байтів (100h) — PSP. Цей блок містить службову інформацію для програми:Приклад. Якщо запустити MYPROG hello world, то:
PSP:80h = 0Bh (11 байтів: пробіл + "hello world")
PSP:81h = hello world\r (починається з пробілу!) Увага. Командний рядок у PSP починається з пробілу-розділювача. Довжина включає цей початковий пробіл.
.MODEL tiny ; один сегмент для всього
.CODE
ORG 100h ; код починається після PSP start: ; Ваш код тут ; Дані можна розміщувати після коду ; (всередині того самого сегменту) mov ah, 4Ch int 21h ; Дані після коду:
msg DB 'Hello!', '$' END start
Ключові відмінності від EXE-шаблону:
.MODEL tiny замість .MODEL small
Немає .DATA та .STACK — все в одному сегменті .CODE
ORG 100h — перший байт коду знаходиться за зміщенням 100h (одразу після PSP)
Не потрібно ініціалізувати DS (mov ax, @data / mov ds, ax) — DS вже = CS = PSP
Дані розміщуються після коду (або перед END, або з переходом через них)
Компіляція: tasm prog → tlink /t prog → PROG.COM
Оскільки CS = DS = PSP, доступ до полів PSP — звичайне читання пам'яті:ORG 100h
start: mov al, ds:[80h] ; AL = довжина командного рядка mov si, 81h ; SI → початок тексту командного рядка
У EXE-програмі DS перезаписується на @data. Щоб прочитати PSP, потрібно зберегти початкове значення DS (або ES), яке вказує на PSP:main: ; При старті: DS = ES = сегмент PSP mov bp, es ; Зберегти PSP segment у BP (до зміни ES) mov ax, @data mov ds, ax ; Тепер DS → наші дані ; Пізніше — для читання PSP: mov es, bp ; ES → PSP mov al, es:[80h] ; AL = довжина командного рядка
За зміщенням PSP:2Ch зберігається сегмент environment block — послідовність null-terminated рядків, що закінчується подвійним нулем:PATH=C:\DOS\0
COMSPEC=C:\COMMAND.COM\0
PROMPT=$P$G\0
\0 ← подвійний нуль = кінець блоку
Кожна змінна має формат ІМ'Я=ЗНАЧЕННЯ, за нею — байт 0. Після останньої змінної — ще один байт 0 (два нулі підряд = кінець).Створіть папку Lab09. Скопіюйте STUB.ASM з попередньої роботи.Для кожного завдання: копіюйте STUB.ASM у новий файл з відповідним ім'ям.Для цієї частини код писати не потрібно — головне дослідження у TD.Скомпілюйте STUB.ASM у STUB.EXE. Запустіть його у DOSBox з аргументами командного рядка:TD STUB.EXE hello world 123 Важливо. Запускайте саме через TD (не окремо), щоб мати можливість досліджувати пам'ять до виконання програми.
Зупиніться на першій інструкції (TD автоматично зупиняється на точці входу).Відкрийте вікно Dump (Alt+D або View → Dump).На старті EXE-програми ES вказує на PSP. Перейдіть до адреси PSP: Ctrl+G → введіть ES:0.Знайдіть та запишіть у коментарях файлу 1PSP.ASM:
PSP:0000 — перші два байти. Що це за інструкція? (Підказка: CD 20 = ?)
PSP:0080 — один байт. Яке значення? Це довжина чого?
PSP:0081 — наступні байти. Знайдіть текст ваших аргументів (hello world 123). Який перший символ — пробіл чи літера?
PSP:002C — два байти (слово, little-endian). Це сегмент чого?
Прочитайте слово за адресою PSP:002C — це сегмент блоку змінних середовища.Перейдіть до цього сегмента в Dump: Ctrl+G → введіть SSSS:0 (замініть SSSS на прочитане значення).Знайдіть та запишіть:
Яку першу змінну ви бачите? (наприклад, PATH=...)
Скільки змінних ви можете ідентифікувати?
Де закінчується блок? (два нулі підряд)
Скопіюйте наступний код у 2COM.ASM:.MODEL tiny
.CODE
ORG 100h start: ; Print a short message mov ah, 09h mov dx, offset msg int 21h ; Exit mov ah, 4Ch int 21h msg DB 'I am a COM program!', 0Dh, 0Ah, '$' END start
Скомпілюйте з прапорцем /t для створення COM-файлу:tasm 2COM
tlink /t 2COM
Запустіть 2COM.COM. Переконайтеся, що повідомлення виводиться.Тепер скомпілюйте ту саму програму без /t:tlink 2COM Примітка. TLINK без /t створить EXE-файл. Він може не працювати коректно (бо програма написана для COM-формату з ORG 100h), але нам потрібен лише файл для порівняння розмірів.
Запишіть у коментарях 2COM.ASM:
Розмір 2COM.COM у байтах (використайте DIR 2COM.COM)
Розмір 2COM.EXE у байтах (використайте DIR 2COM.EXE)
У скільки разів EXE більший? Чому? (Підказка: заголовок MZ)
Відкрийте обидві версії програми в TD по черзі та порівняйте:COM: TD 2COM.COM
Зверніть увагу на початкове значення CS:IP. Яке зміщення IP? (Очікуємо: 0100h — одразу після PSP.)
CS = DS = ES = SS — всі сегментні регістри однакові. Запишіть їх значення.
EXE: скомпілюйте правильну EXE-версію (використайте STUB.ASM з попередніх лабораторних), відкрийте TD STUB.EXE
Зверніть увагу: CS ≠ DS. Запишіть значення CS та DS.
Де починається код? (IP = 0000h, бо код — у своєму сегменті, а не після PSP.)
Запишіть у коментарях 2COM.ASM: значення сегментних регістрів для COM та EXE, значення IP на старті.
Факт. COM-файл не має заголовка — його перший байт є першою інструкцією програми. EXE-файл починається з сигнатури "MZ" (4D 5A — Mark Zbikowski, один із розробників DOS). DOS розрізняє формати за першими двома байтами: "MZ" → EXE, інакше → COM. Заголовок MZ містить таблицю релокацій, точку входу та розмір стеку — саме тому EXE-файл більший.
Скопіюйте наступний код цілком у 3TAIL.ASM:.MODEL tiny
.CODE
ORG 100h start: ; --- Read command tail length --- mov cl, ds:[80h] ; CL = length of command tail xor ch, ch ; CX = length (zero-extend) cmp cx, 0 je no_args ; No arguments? Skip to message ; --- Display header --- mov ah, 09h mov dx, offset hdr int 21h ; --- Print the command tail character by character --- mov si, 81h ; SI → first byte of command tail ; --- YOUR CODE HERE (4-5 lines) --- ; Loop CX times: ; 1. LODSB — load byte from [SI] into AL, increment SI ; 2. MOV DL, AL / MOV AH, 02h / INT 21h — print the character ; 3. LOOP back to step 1 ; ; Hint: LODSB does "MOV AL, [SI]" + "INC SI" in one instruction ; You already used it in P8 (vram_str). Here DS:SI points to ; PSP data (since DS = CS = PSP segment in COM programs). ; --- Newline after output --- mov ah, 09h mov dx, offset crlf int 21h jmp done no_args: mov ah, 09h mov dx, offset none int 21h done: mov ah, 4Ch int 21h hdr DB 'Tail: $'
none DB 'No arguments provided.', 0Dh, 0Ah, '$'
crlf DB 0Dh, 0Ah, '$' END start
Ваше завдання: допишіть 4–5 рядків у секції YOUR CODE HERE.Перевірка: скомпілюйте (tasm 3TAIL / tlink /t 3TAIL) та запустіть з різними аргументами:3TAIL hello world → Tail: hello world
3TAIL → No arguments provided.
3TAIL one → Tail: one
3TAIL extra spaces → Tail: extra spaces Зверніть увагу: у виводі першим символом завжди буде пробіл — це розділювач між ім'ям програми та аргументами, який DOS залишає у PSP:81h.
Скопіюйте наступний код цілком у 4ARGS.ASM:.MODEL tiny
.CODE
ORG 100h start: mov cl, ds:[80h] ; CL = command tail length xor ch, ch cmp cx, 0 je no_args mov si, 81h ; SI → command tail xor bx, bx ; BL = state: 0 = in spaces, 1 = in argument ; BH = argument count parse_loop: lodsb ; AL = next byte from [SI], SI++ cmp al, 0Dh ; Carriage return? (end of tail) je parse_done cmp al, ' ' je got_space ; --- Not a space: we have a printable character --- ; --- YOUR CODE HERE (5-6 lines) --- ; If BL = 0 (we were in spaces), this is the START of a new argument: ; 1. Set BL = 1 (now we're inside an argument) ; 2. Increment BH (argument count) ; 3. Print newline + argument number before the first char ; (CALL print_arg_header — already provided below) ; If BL = 1 (already inside an argument), just print the character. ; ; In both cases, print the character: ; MOV DL, AL / MOV AH, 02h / INT 21h ; ; Then jump to check_done. got_space: ; --- YOUR CODE HERE (1-2 lines) --- ; We hit a space — set BL = 0 (back to "in spaces" state). ; This works whether we were in an argument or already in spaces. ; Then fall through to check_done. check_done: loop parse_loop parse_done: ; Print final newline mov ah, 09h mov dx, offset crlf int 21h ; Print total count mov ah, 09h mov dx, offset msg_count int 21h mov dl, bh add dl, '0' ; Convert count to ASCII digit (works for 0-9) mov ah, 02h int 21h mov ah, 09h mov dx, offset crlf int 21h jmp done no_args: mov ah, 09h mov dx, offset msg_none int 21h done: mov ah, 4Ch int 21h ;----------------------------------------------
; print_arg_header - prints newline + " Arg N: "
; Input: BH = argument number (1-based, already incremented)
; Preserves: AX, DX (saved/restored on stack)
;----------------------------------------------
print_arg_header proc push ax push dx ; Newline (except before first arg — optional, simpler to always print) mov ah, 09h mov dx, offset prefix int 21h ; Argument number mov dl, bh add dl, '0' mov ah, 02h int 21h ; Colon and space mov ah, 09h mov dx, offset colon int 21h pop dx pop ax ret
print_arg_header endp ; --- Data ---
prefix DB 0Dh, 0Ah, ' Arg $'
colon DB ': $'
crlf DB 0Dh, 0Ah, '$'
msg_count DB 'Count: $'
msg_none DB 'No arguments.', 0Dh, 0Ah, '$' END start
Ваше завдання: допишіть 7–9 рядків у двох секціях YOUR CODE HERE.Логіка парсингу — скінченний автомат з двома станами:<img alt="Скінченний автомат парсингу аргументів" src="assets/lab09-arg-parser-fsm.svg" target="_self" style="width: 500px; max-width: 100%;">Перевірка: скомпілюйте та запустіть з різними аргументами:4ARGS one two three Arg 1: one Arg 2: two Arg 3: three
Count: 3 4ARGS hello Arg 1: hello
Count: 1 4ARGS extra spaces here Arg 1: extra Arg 2: spaces Arg 3: here
Count: 3 4ARGS
No arguments.
До кінця заняття:
1PSP.ASM — спостереження PSP у TD: INT 20h, командний рядок, environment
2COM.ASM — COM-програма, порівняння розмірів COM/EXE, MZ-заголовок
3TAIL.ASM — читання та виведення командного рядка (дописаний)
4ARGS.ASM — розбір аргументів зі скінченним автоматом (дописаний)
Закомічено всі файли
Дедлайн: 23:59 у день перед Практикою 10Напишіть COM-програму, яка читає текст із командного рядка та малює навколо нього рамку у відеопам'яті:C:\&gt; BANNER Hello NaUKMA! ╔══════════════════╗
║ Hello NaUKMA! ║
╚══════════════════╝
Вимоги:
Формат COM — .MODEL tiny, ORG 100h, компіляція з tlink /t
Текст із командного рядка — читати з PSP:80h/81h (пропустити початковий пробіл)
Рамка у відеопам'яті — пряме записування у B800:0000 (як у P8), НЕ використовувати INT 10h для малювання
Ширина рамки адаптується до довжини тексту — з відступами по 2 пробіли зліва та справа від тексту
Мінімум 2 атрибути кольору — один для рамки (наприклад, 1Fh — білий на синьому), інший для тексту всередині
Процедура з параметрами через стек — щонайменше одна (наприклад, vram_str з P8 або draw_hline для горизонтальної лінії)
Обробка випадку без аргументів — якщо аргументів немає, вивести: Usage: BANNER &lt;text&gt;
Очищення екрана перед малюванням (заповнити пробілами з атрибутом 07h)
Очікування клавіші перед виходом (INT 16h)
Розміщення рамки: починаючи з рядка 10 (або по центру екрана вертикально — на ваш вибір).Бажано (але не обов'язково): центрувати рамку горизонтально так, щоб вона була посередині екрану між лівим і правим краєм (обчисліть початковий стовпець: (80 - ширина_рамки) / 2).Підказки:
Ширина рамки = довжина тексту + 4 (2 пробіли відступу з кожного боку) + 2 (бічні ║)
Для рамки використовуйте символи подвійної рамки: ╔ (C9h), ═ (CDh), ╗ (BBh), ║ (BAh), ╚ (C8h), ╝ (BCh)
Пропустіть початковий пробіл командного рядка: якщо PSP:81h = ' ', починайте з PSP:82h і зменшіть довжину на 1
Процедуру vram_str з P8 можна адаптувати: замість null-terminated рядка передавати довжину
Тести:Напишіть COM-програму, яка відображає змінні середовища DOS у відеопам'яті:
Прочитати сегмент environment з PSP:2Ch
Пройти по ланцюжку null-terminated рядків до подвійного нуля
Вивести кожну змінну на окремому рядку у відеопам'яті
Ім'я змінної (до =) — одним кольором, значення (після =) — іншим
Очікувати клавішу перед виходом
Підказки:
Після mov ax, ds:[2Ch] / mov es, ax — ES вказує на блок environment
Рядки зберігаються послідовно: NAME1=VAL1\0NAME2=VAL2\0\0
Для запису у відеопам'ять потрібен другий сегментний регістр. Варіант: зчитувати з environment (ES:SI) у буфер у сегменті коду, потім виводити з буфера у відеопам'ять
Або: чередувати ES між сегментом environment та B800h
Тест: візуальна перевірка — на екрані з'являються змінні середовища (PATH, COMSPEC тощо) з кольоровим розділенням імені та значення.Запишіть відео, у якому:
Продемонструйте BANNER.COM — запустіть з різними аргументами (текст, без аргументів, довгий текст)
Покажіть PSP у TD: відкрийте будь-яку програму з аргументами, знайдіть у Dump командний рядок (PSP:80h), покажіть environment segment (PSP:2Ch)
Поясніть: чим COM-програма відрізняється від EXE? Що означає ORG 100h і чому COM починається з адреси 100h? (Підказка: перші 100h байтів — це PSP)
УВАГА! Вимога про 5хв максимально важлива! Brevity is the soul of wit. Це частина оцінки.Lab09/
├── 1PSP.ASM ← дослідження PSP у TD (з коментарями)
├── 2COM.ASM ← COM-програма, порівняння COM/EXE
├── 3TAIL.ASM ← читання командного рядка (дописаний)
├── 4ARGS.ASM ← розбір аргументів (дописаний)
├── BANNER.COM ← домашнє: кольоровий банер (файл .COM!)
├── BANNER.ASM ← вихідний код банера
├── ENVSHOW.COM ← бонус: змінні середовища
├── ENVSHOW.ASM ← вихідний код (бонус)
└── video.txt ← посилання на Loom <br><a data-tooltip-position="top" aria-label="https://wiki.nox-rhea.org/back2root/ibm-pc-ms-dos/hardware/informations/program_segment_prefix" rel="noopener nofollow" class="external-link is-unresolved" href="https://wiki.nox-rhea.org/back2root/ibm-pc-ms-dos/hardware/informations/program_segment_prefix" target="_self">PSP format</a>— повний опис полів PSP
<br><a data-tooltip-position="top" aria-label="https://wiki.osdev.org/COM" rel="noopener nofollow" class="external-link is-unresolved" href="https://wiki.osdev.org/COM" target="_self">COM File Format — OSDev Wiki</a> — структура COM-файлу
<br><a data-tooltip-position="top" aria-label="https://wiki.osdev.org/MZ" rel="noopener nofollow" class="external-link is-unresolved" href="https://wiki.osdev.org/MZ" target="_self">EXE (MZ) Format — OSDev Wiki</a> — структура заголовка EXE
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/DOS_memory_management" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/DOS_memory_management" target="_self">DOS Memory Map — Wikipedia</a> — карта пам'яті DOS
]]></description><link>labs/lab09.html</link><guid isPermaLink="false">Labs/Lab09.md</guid><pubDate>Wed, 11 Mar 2026 11:36:03 GMT</pubDate><enclosure url="assets/lab09-arg-parser-fsm.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/lab09-arg-parser-fsm.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Lab09-Arg-Parser-FSM]]></title><description><![CDATA[<img src="assets/lab09-arg-parser-fsm.svg" target="_self">]]></description><link>assets/lab09-arg-parser-fsm.html</link><guid isPermaLink="false">assets/Lab09-Arg-Parser-FSM.svg</guid><pubDate>Tue, 10 Mar 2026 16:26:47 GMT</pubDate><enclosure url="assets/lab09-arg-parser-fsm.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/lab09-arg-parser-fsm.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L09-ENG. ISA Design and Models of Computation]]></title><description><![CDATA[
Lecture 9 of Computer Systems Architecture course at NaUKMA.
After this lecture you will be able to:
Describe the RAM and RASP abstract machine models and explain how they relate to real computer architectures (Harvard vs von Neumann)
Define what an Instruction Set Architecture (ISA) is and list the key components it specifies
Classify ISAs by operand location (stack, accumulator, register-memory, register-register, memory-memory) and compare their trade-offs
Compare CISC and RISC design philosophies, giving real-world examples of each (x86, ARM, RISC-V)
Explain the VLIW approach and how it shifts scheduling responsibility to the compiler
Classify computer architectures using Flynn's taxonomy (SISD, MISD, SIMD, MIMD)
Distinguish between SMP and MMP parallel processing systems
Over the past eight lectures you have built an increasingly detailed picture of how a computer actually works. You began with physics — the silicon and electrons that make switching possible (L01). You watched logic gates assemble into adders and ALUs (L02). You climbed inside a specific processor architecture, x86, learning its registers, memory model, and segment:offset addressing (L03), then its arithmetic, logical, and control-flow instructions (L04). You organized code with the stack and procedures (L05), zoomed out to see the full system bus connecting CPU, memory, and peripherals (L06), learned how software interrupts let programs talk to the operating system and BIOS (L07), and finally explored the memory hierarchy, I/O ports, and low-level data structures (L08).In every one of those lectures, you were working with a concrete machine — the Intel 8086 and its descendants. You learned specific register names (AX, BX, SP), specific instruction encodings (MOV, ADD, INT), specific memory layouts (Real Mode, segment registers, the IVT at address 0000:0000). This concreteness was deliberate: there is no substitute for actually writing programs that run on real hardware (or a faithful emulator like DOSBox).But concreteness has a cost. When you know only one architecture, every design choice looks inevitable. Of course there is an accumulator — that is just how processors work, right? Of course the stack grows downward and PUSH decrements SP. Of course instructions can operate directly on memory operands. These feel like laws of nature rather than what they actually are: design decisions made by specific engineers at specific companies, sometimes for deep technical reasons, sometimes for historical accident, and sometimes because the alternative had not been invented yet.This lecture steps back from the concrete to ask three big questions:Question 1: What is the simplest possible machine that can still compute everything a real computer can? This is the question answered by formal models of computation — mathematical blueprints for idealized machines. You have likely heard of the Turing machine from a theory of computation or discrete math course. Today we will study two models that sit much closer to real hardware: the Random-Access Machine (RAM) and the Random-Access Stored-Program machine (RASP). These models strip away the clutter of any specific ISA and reveal the essential ingredients that make a computer a computer.Question 2: Given those essential ingredients, how many fundamentally different ways are there to design an instruction set? Once you see the abstract models, you will understand that the x86 way is just one point in a large design space. We will classify instruction set architectures into four families — stack-based, accumulator-based, register-memory, and register-register (load-store) — and see how each family makes different trade-offs between code density, hardware complexity, and performance. This naturally leads to the great divide of processor design: CISC vs. RISC, the philosophical split that shaped every processor you use today, from the x86 chip in your laptop to the ARM core in your phone.Question 3: Can we make computers faster not by speeding up a single processor, but by using many processors at once? Serial computation — one instruction after another, forever — is the only paradigm you have seen so far. But the physical limits you learned about in L01 (the speed of light, the heat wall, the end of Dennard scaling) guarantee that single-core performance cannot grow forever. Parallel computing is the answer the industry adopted, and Flynn's taxonomy gives us the vocabulary to classify every parallel architecture ever built.<img alt="Lecture 9 outline — three questions from abstract models through ISA design to parallel computing" src="assets/l09-lecture-outline.svg" target="_self">These three questions form the arc of Lecture 9. We move from the most abstract (mathematical models) through the architectural middle ground (ISA design and CISC/RISC) to the most forward-looking (parallelism and multi-processor systems). By the end, you will have the conceptual tools to look at any processor — past, present, or future — and understand where it sits in the design space and why its architects made the choices they did.There is a practical reason for this lecture's timing. In L03-L04, you learned x86 assembly as a tool: here is a register, here is an instruction, use them to solve problems. That was the right approach for building skills. But now that you are fluent enough to write procedures, manage the stack, and manipulate data structures, you are ready for the deeper question: why does x86 look the way it does? And more importantly: what would a processor look like if its designers had made different choices?The answer is not hypothetical. ARM processors (RISC architecture, register-register design) now ship in billions of devices per year — including Apple's M-series chips that dethroned Intel in the Mac lineup. The Java Virtual Machine and WebAssembly are stack-based architectures running on every browser. RISC-V, an open-source ISA, is reshaping the industry precisely because its designers could start from first principles rather than inheriting decades of backward-compatible decisions. And every modern processor, whether it calls itself CISC or RISC, is a multi-core chip — meaning it is fundamentally a parallel machine, even if most software still treats it as serial. Understanding the design space is not academic nostalgia — it is the map you need to navigate the processor landscape for the rest of your career.Let us begin where all good engineering begins: with the simplest possible abstraction — the formal models of computation that define what a "computing machine" really is.When engineers design a bridge, they do not begin by pouring concrete. They begin with a mathematical model — forces, loads, material properties — that lets them reason about the structure before committing to any specific design. Computer science works the same way. Before you can argue about whether RISC is "better" than CISC, or whether a stack machine is more elegant than a register machine, you need a precise, mathematical definition of what a "computing machine" actually is and what it means for two machines to be "equally powerful."This is the role of models of computation: formal, mathematical descriptions of idealized machines. A model strips away every implementation detail — clock speed, transistor technology, bus width, operating system — and keeps only the logical structure of computation. If you can prove that a model computes the same class of functions as another model, then you know that any difference between real machines based on those models is a matter of engineering trade-offs, not fundamental capability.<br>The most important result in this area, which you may have encountered in a discrete math or algorithms course, is the <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Church%E2%80%93Turing_thesis" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Church%E2%80%93Turing_thesis" target="_self">Church-Turing thesis</a>: any function that is "effectively computable" by any reasonable mechanical process can be computed by a Turing machine. Every model we discuss today is Turing-equivalent — it computes exactly the same class of functions as the Turing machine. The differences between models are not about what they can compute but about how conveniently and efficiently they express computation.Computer scientists have developed a family of abstract machine models over the decades, each capturing different aspects of real computation. These models did not appear all at once; they emerged through a dialogue between mathematicians and engineers from the 1930s through the 1970s. Turing proposed his machine in 1936, before electronic computers existed. Counter machines were studied by Minsky and Lambek in 1961, just as the first transistorized computers were proving the practical importance of stored programs. The RAM and RASP models were refined through the 1960s and early 1970s by Shepherdson, Sturgis, Elgot, Robinson, Cook, and Reckhow, precisely because computer scientists needed models that accurately reflected the cost of computation on real machines for algorithm analysis.The major sequential models — meaning those that execute one instruction at a time — can be arranged from simplest to most realistic:Van Emde Boas (1990) groups the Turing machine, counter machine, RAM, RASP, and pointer machine together as the "sequential machine" models, distinguishing them from parallel models like the PRAM (Parallel Random-Access Machine) that we will meet later in this lecture when we discuss Flynn's taxonomy.Let us walk through the key models, building from simple to complex. We will spend the most time on the counter machine, RAM, and RASP, since these are the ones most directly relevant to understanding ISA design.<br>The <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Turing_machine" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Turing_machine" target="_self">Turing machine</a>, proposed by Alan Turing in 1936, is the foundational model of computation. It consists of:
An infinite tape divided into cells, each holding a symbol from a finite alphabet.
A head that can read from, write to, and move one cell left or right on the tape.
A finite-state controller (a table of rules) that determines, based on the current state and the symbol under the head, what to write, which direction to move, and what state to enter next.
<br><img alt="Turing machine schematic — infinite tape, read/write head, and finite-state controller" src="assets/l09-turing-machine.svg" target="_self">The Turing machine is extraordinarily powerful in theory — it can compute anything that is computable. But it is a terrible model for analyzing the efficiency of algorithms. The problem is that accessing the n-th cell on the tape requires n steps of head movement. There is no "jump to cell 1000" — you have to crawl there one cell at a time. Real computers, by contrast, can access any memory location in constant time. This mismatch means that a Turing machine program for sorting n numbers would be dramatically slower than the same algorithm on a real computer, not because the algorithm is worse, but because the model penalizes random access.To put this concretely: if you store an array of 1000 numbers on a Turing machine tape starting at cell 0, and you want to access element 500, the head must physically slide through 500 cells. On a real computer, MOV AX, [array + 500*2] executes in a single memory cycle regardless of the address. The Turing machine's O(n) access time inflates the complexity of algorithms in ways that do not reflect real-world performance, making it unsuitable for the kind of complexity analysis that matters to practicing programmers and algorithm designers.This limitation motivated the development of models that more closely resemble real hardware.<br>The <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Counter_machine" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Counter_machine" target="_self">counter machine</a> (studied by Minsky, Lambek, and others in the early 1960s) takes a radically different approach from the Turing machine. Instead of a tape, it has a finite set of registers — discrete storage locations, each identified by a number, each capable of holding a single natural number of arbitrary size. Unlike the tape, registers provide genuine random access: the machine can read or modify register 500 just as easily as register 1, with no "head movement" required. The instruction set is deliberately minimal:<br><img alt="Counter machine instruction set — INC, DEC, JZ, HALT" src="assets/l09-counter-machine-isa.svg" target="_self">This may seem impossibly weak. How can you compute anything useful with only increment and decrement? The key insight is that you can simulate any more complex operation by combining these primitives in loops. Addition is a loop of increments. Multiplication is a loop of additions (each of which is a loop of increments). Comparison is a parallel decrement of two registers until one reaches zero. It is tedious, but it works. Minsky (1961) proved that even this absurdly simple machine, with as few as two registers, is Turing-equivalent — it can compute anything a Turing machine can.Melzak (1961) offered a charming visualization: imagine the registers as holes dug in the ground, and the contents of each register as a pile of pebbles in the hole. INC adds a pebble; DEC removes one; JZ peeks into the hole to see if it is empty. An unlimited supply of pebbles sits nearby. If a hole needs to hold a million pebbles, you just dig it deeper.But the counter machine's power comes at a brutal cost in convenience. Want to copy the value from register 3 to register 5? There is no COPY instruction. You have to decrement register 3 one pebble at a time, incrementing both register 5 and a temporary register for each pebble, then copy the temporary back to register 3 to restore the original value. Here is what that looks like in counter-machine pseudocode, using register 0 as a temporary:; Copy [r3] to r5 (destroying and restoring r3, using r0 as temp)
CLR r5 ; built from: loop DEC(r5)/JZ(r5,done)
CLR r0
loop: JZ r3, restore ; if r3 is empty, go restore it DEC r3 INC r5 INC r0 JZ rZ, loop ; unconditional jump (rZ always holds 0)
restore: JZ r0, done DEC r0 INC r3 JZ rZ, restore
done: HALT
In x86 assembly, this same operation is a single instruction: MOV [r5], [r3] (or, since x86 does not allow memory-to-memory moves, two instructions: MOV AX, [r3] then MOV [r5], AX). The contrast illustrates why richer instruction sets exist: they compress common patterns into single operations, trading simplicity of the model for programmer (and compiler) convenience.Want to add two numbers? Build it from INC and DEC loops. The machine is Turing-complete but agonizingly impractical.More critically, the counter machine has a fatal structural limitation: every register address must be hard-coded in the program. The instruction INC(3) names register 3 explicitly. There is no way to say "increment the register whose address is stored in register 7" — that is, there is no indirect addressing. This means a counter-machine program with k instructions can only ever touch the specific registers named in those instructions. It cannot explore an unbounded number of registers based on computed addresses.Why does this matter? Consider a simple task: traverse a linked list. Each node is stored in some register, and the "next pointer" is the address of the next node, stored as a number inside the current node. To follow the pointer, you need to read a value from one register and use that value as the address of the next register to access. The counter machine simply cannot do this — it has no mechanism to say "go to the register whose number I just computed." You could try to work around it by exhaustively testing every possible address ("Is the pointer equal to 0? No. Equal to 1? No. Equal to 2? ..."), but this "bounded indirection" technique only works up to some fixed limit and requires an instruction for every possible address. It cannot scale to unbounded data.Cook and Reckhow (1973) state the problem concisely: "The indirect instructions are necessary in order for a fixed program to access an unbounded number of registers as the inputs vary."This is exactly the gap that the RAM model fills.The Random-Access Machine (RAM), formalized in the work of Shepherdson and Sturgis (1963), Elgot and Robinson (1964), and Cook and Reckhow (1973), takes the counter machine and adds two crucial enhancements: Indirect addressing: an instruction can specify a register whose contents serve as the address of the actual target register. If register 7 contains the value 42, then an indirect operation through register 7 will access register 42 — wherever it is in the unbounded register space. This is the pointer mechanism that lets a fixed-size program access an unlimited number of registers. You already know this concept from x86: MOV AX, [BX] uses the contents of BX as a memory address, which is indirect addressing. The RAM model formalizes this idea. An accumulator: a dedicated register (usually called A or AC) that serves as the implicit source or destination for most operations, reducing the number of operands each instruction needs to specify. Instead of ADD r3, r5, r7 (three-address), the accumulator model uses LOAD r3 then ADD r5 then STORE r7 — each instruction names only one register, with the accumulator implied. <br><img alt="Direct vs indirect addressing — LOAD 7 reads r7 directly, LOAD *7 follows r7's value as a pointer" src="assets/l09-direct-vs-indirect-addressing.svg" target="_self">The result is a machine that looks remarkably like a simple real processor. It has an unbounded set of numbered registers (analogous to main memory), a program counter, an accumulator, and an instruction set that includes arithmetic, data movement with direct and indirect addressing, conditional and unconditional jumps, and I/O operations via dedicated input and output "tapes."Notice how the accumulator concept should feel familiar. In L03, you learned that AX is the "accumulator" register of x86 — the hardwired destination for MUL, DIV, and certain I/O instructions. That is no coincidence. The x86 accumulator is a direct descendant of the accumulator in formal computational models. The RAM model makes this connection explicit: the accumulator is the register through which all arithmetic flows, reducing each instruction to a single explicit operand (the other operand being implicitly "the accumulator"). Goldstine and von Neumann described it in 1946 as an "arithmetic organ that literally accumulates its number during a sequence of arithmetic operations" — and every x86 program you have written has been doing exactly that when it chains operations through AX.There is one critical detail that distinguishes the RAM from a real computer: the program lives in the finite-state controller, not in the registers. The instruction table is separate from the data memory. If this reminds you of something from L06, it should — this is exactly the Harvard architecture, where instruction memory and data memory are physically separate.The RAM model is powerful enough to compute all the partial recursive functions (the full class of computable functions), and its random-access memory model means that algorithm running times on the RAM correspond much more closely to running times on real hardware than Turing machine running times do. This is why the RAM model is the standard foundation for algorithm complexity analysis — when your algorithms course says that merge sort runs in O(n log n) time, it is implicitly assuming a RAM-like model where any memory access takes constant time. If you used a Turing machine model instead, the same algorithm would have a higher complexity because every memory access would cost O(n) tape movements.There is an important subtlety in complexity analysis on the RAM model: the cost model. Under the uniform cost model, every operation (including operations on arbitrarily large numbers) costs one time unit. Under the logarithmic cost model, the cost of an operation is proportional to the number of bits in the operands. For realistic algorithm analysis, the uniform cost model is usually fine as long as the numbers involved fit in a machine word — which is exactly the assumption you make when writing practical code. But for theoretical results involving very large numbers, the logarithmic model is more appropriate.We will examine the RAM's architecture and instruction set in detail in the next section.The Random-Access Stored-Program machine (RASP) takes the final step toward a realistic computer. It is identical to the RAM in every respect except one: the program is stored in the registers alongside the data. The finite-state controller still exists, but its sole job is now to act as an interpreter — it fetches instructions from the registers, decodes them, and executes them. The actual program logic lives in the same address space as the input data and output results.This is the von Neumann architecture: a single, uniform memory holding both instructions and data, with a processor that fetches, decodes, and executes instructions sequentially. The RASP is to the RAM as the universal Turing machine is to the ordinary Turing machine — it is the programmable version, the one where you can change the program without rebuilding the hardware.<br><img alt="RAM vs RASP — separate program and data (Harvard) vs unified memory (von Neumann)" src="assets/l09-ram-vs-rasp.svg" target="_self">Think about what "stored program" means in concrete terms you already know. In L07, you learned that INT 21h works because the operating system's interrupt handler code sits in memory alongside your program's data. In L05, you saw that CALL pushes a return address onto the stack — the stack (data memory) holds a value that the CPU will later use as an instruction address. These are both consequences of the stored-program principle: instructions and data share the same address space and can reference each other.The distinction between RAM (Harvard) and RASP (von Neumann) is not merely academic. It maps directly onto a real and ongoing architectural divide: Harvard architecture (separate instruction and data paths) appears in many microcontrollers and DSP chips, and in the L1 cache split of modern x86 processors (separate L1-I and L1-D caches, even though the underlying main memory is unified). Von Neumann architecture (unified memory for instructions and data) is the dominant model for general-purpose computers. It enables self-modifying code, runtime code generation (JIT compilers), and the ability to treat programs as data — the foundation of interpreters, debuggers, and operating system loaders. Every x86 PC you have used follows this model: your .COM program in L07 sat in the same memory space as the data it operated on, and the CALL/RET mechanism relied on the stack (data memory) holding instruction addresses. <br>The price the RASP pays for its flexibility is the <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Von_Neumann_architecture#Von_Neumann_bottleneck" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Von_Neumann_architecture#Von_Neumann_bottleneck" target="_self">von Neumann bottleneck</a>: instructions and data compete for the same memory bus, creating a potential throughput limitation that Harvard-style designs avoid. John Backus coined this term in his 1977 Turing Award lecture, arguing that the single-bus bottleneck between CPU and memory fundamentally limits how fast programs can run — a concern that only grows more pressing as processor speeds outpace memory speeds (a gap you saw quantified in L08's discussion of the memory hierarchy).Modern processors deal with this bottleneck pragmatically: they use a modified Harvard architecture at the cache level (separate L1 instruction cache and L1 data cache, as discussed in L08) while presenting a unified von Neumann address space to the programmer. This gives the best of both worlds — Harvard-speed parallel fetching of instructions and data, with von Neumann flexibility for the software model.The stored-program concept has a profound implication that is easy to overlook: it means that programs are data. A RASP can read its own instructions, copy them, modify them, and even generate new instructions at runtime. This is what makes operating systems possible — a loader reads a program from disk (data) and writes it into memory where the processor will fetch it as instructions. It is what makes interpreters and compilers possible — a Python interpreter is a program (instructions) that reads your .py file (data) and executes it. And it is what made early programming techniques like self-modifying code possible, where a program would rewrite its own instructions to implement loops or indexed addressing on hardware that lacked those features natively.We will explore the RASP architecture in full detail later in this lecture.Why spend time on these abstract models in a course that is otherwise deeply practical? Because the models reveal which features of a computer are essential (registers, addressing, a program counter) and which are design choices (how many registers, what instructions to include, whether to use a stack or an accumulator, how many operands per instruction). The RAM and RASP models provide the minimal baseline — the "skeleton" of a computer. Everything else you have learned about x86 is "muscle and skin" added on top. Seeing the skeleton clearly is what will let you understand, in the sections that follow, why different ISA families add different muscles in different places.Here is how all the pieces fit together:<br><img alt="Models of computation progression — from Turing machine to real computer" src="assets/l09-models-of-computation-progression.svg" target="_self">The progression of computational models: each step adds a capability that brings the abstract machine closer to a real computer.Each step in this progression adds a capability that brings the abstract model closer to the machine on your desk. The Turing machine gives us the theoretical foundation and the concept of universal computation. The counter machine introduces registers — discrete, named storage locations with random access — replacing the tape's painful sequential crawl. The RAM adds indirect addressing and an accumulator, enabling programs to follow pointers and work with unbounded data structures. The RASP puts the program in memory alongside data, enabling the fetch-decode-execute cycle you know from L06 and making general-purpose, reprogrammable computers possible. By the time we reach the RASP, we have essentially described the logical structure of every general-purpose computer since the EDVAC (1949).And that is precisely the point. Once you understand that a real computer is "just" a RASP with a richer instruction set, faster memory, and hardware optimizations, you can ask the next natural question: what should that richer instruction set look like?The RASP tells us we need instructions, registers, addressing modes, and a sequential fetch-decode-execute cycle. But it says nothing about which instructions to include, how many registers to provide, how operands should be specified, or whether instructions should be fixed-length or variable-length. These are all design decisions — and different answers to these questions produce radically different instruction set architectures.That question — the question of ISA design — is what the rest of this lecture is about.In the previous sections we saw how Turing machines provide a theoretical foundation for computation but operate on a tape — a sequential storage medium that bears little resemblance to how real computers access data. In this section we introduce the Random Access Machine (RAM), an abstract model of computation that bridges the gap between pure theory and practical computer architecture. The RAM model is the workhorse of algorithm analysis: when you study the time complexity of sorting algorithms or graph traversals in an algorithms course, the cost model you use is essentially a RAM machine.<br>A random-access machine is an abstract computational model in the general class of <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Register_machine" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Register_machine" target="_self">register machines</a>. Unlike a Turing machine, which must move its head one cell at a time to reach distant data, the RAM can access any register instantly — hence "random access," just like modern RAM memory chips. The model was formalized through the work of Shepherdson and Sturgis (1963), Melzak (1961), and Cook and Reckhow (1973), among others, and it remains the standard model for computational complexity analysis.The key idea is simple: instead of an infinite tape, the RAM has an unbounded collection of registers (numbered memory locations), each capable of holding a natural number of arbitrary size. A fixed program — stored separately from the data — manipulates these registers through a small set of instructions.The RAM machine consists of five components:1. Memory Registers (r1, r2, r3, ...)An unbounded sequence of registers, each identified by a natural number address. Each register can hold a single natural number of any size (including zero). Register r1 is traditionally designated as the accumulator — a special-purpose register where arithmetic results accumulate. The remaining registers (r2, r3, ...) serve as general-purpose storage. There is no upper limit on the number of registers or on the size of the numbers they can hold — this is what makes the model "abstract."2. The Program (Instruction Table)A finite, fixed sequence of instructions drawn from a small instruction set. Critically, the program is not stored in the registers — it exists in a separate, read-only instruction table. The program cannot modify itself, and the data in the registers cannot alter the program. This separation is a defining characteristic of the RAM model.3. The Location Counter (Program Counter)A special counter that keeps track of which instruction in the program is currently being executed. After each instruction, the location counter advances to the next instruction — unless the instruction is a jump, in which case the counter is set to the jump target.4. Input Tape (Read-Only)A one-way input tape from which the machine can read values sequentially. Each READ operation consumes the next value from the tape and places it in the accumulator. Once a value has been read, the tape head moves forward and cannot go back. Think of it as a queue of input data.5. Output Tape (Write-Only)A one-way output tape to which the machine can write values. Each WRITE operation appends the current accumulator value to the end of the output tape. The machine cannot read back what it has written or modify previous output.<br><img alt="RAM machine architecture" src="assets/l09-ram-machine-architecture.png" target="_self">The RAM machine architecture: an input tape feeds data in, a program (instruction table) controls execution via a location counter, registers r1 (accumulator), r2, r3, ... store data, and results are written to an output tape.The RAM instruction set resembles a stripped-down assembly language. Different authors define slightly different sets, but the accumulator-based model of Cook and Reckhow (1973) is the most widely used because it closely mirrors how real CPUs operate. Here is a representative instruction set:Notice how every arithmetic operation uses the accumulator as both a source and a destination — exactly like the EAX register tends to be used in x86 assembly for operations like MUL and DIV that you saw in L04.The RAM model supports two addressing modes, which should look familiar from your x86 assembly experience:Direct addressing (LOAD 5): The operand specifies a register number directly. LOAD 5 means "load the contents of register r5 into the accumulator." This is analogous to mov eax, [var] in x86.Indirect addressing (LOAD *5): The operand specifies a register whose contents are used as the address of the target register. LOAD *5 means "look at the value in register r5; use that value as an address; load the contents of that register into the accumulator." If r5 contains the value 12, then LOAD *5 loads the contents of r12. This is analogous to pointer dereferencing — like mov eax, [ebx] where EBX holds an address.Indirect addressing is what elevates the RAM above the simpler counter-machine model. Without it, a fixed program can only access a finite number of registers (those explicitly named in the instructions). With indirect addressing, a program can compute an address at runtime and access any register — even registers far beyond what the program's instructions explicitly mention. As Cook and Reckhow (1973) put it:
"The indirect instructions are necessary in order for a fixed program to access an unbounded number of registers as the inputs vary."
To make this concrete, here is a RAM program that reads two numbers from the input tape, adds them, and writes the result to the output tape: READ ; Read first number into accumulator STORE 2 ; Save it in r2 READ ; Read second number into accumulator ADD 2 ; Add contents of r2 to accumulator WRITE ; Write the sum to output tape HALT ; Stop
If the input tape contains [7, 5], the execution proceeds as follows:This is much closer to how we actually think about programs than a Turing machine shifting along a tape.The primary purpose of the RAM model is to provide a foundation for analyzing algorithm complexity. When we say that a sorting algorithm runs in O(n log n) time, we are implicitly using a RAM cost model. Two cost models are commonly used: Uniform cost model: Every instruction (including arithmetic on arbitrarily large numbers) takes one unit of time, and every register takes one unit of space. This is the model you use in most introductory algorithms courses. It is simple and usually adequate when the numbers involved remain polynomial in the input size. Logarithmic cost model: The cost of an instruction depends on the size (number of bits) of the operands involved. This is more realistic for situations where numbers grow very large, because multiplying two 1000-digit numbers clearly takes more effort than multiplying two single-digit numbers. Under this model, a LOAD of a number with k bits costs O(k), an ADD of two k-bit numbers costs O(k), and a MULT costs O(k^2) or better depending on the multiplication algorithm. The choice of cost model matters. Under the uniform cost model, you could "cheat" by encoding an exponential amount of information in a single register and performing a single arithmetic operation on it. The logarithmic cost model prevents such tricks by charging for the actual work of manipulating large numbers. For most algorithms you will encounter, the two models agree, but for complexity theory at the research level, the distinction is important.Here is a crucial architectural observation: in the RAM model, the program is stored separately from the data. The instruction table lives in its own read-only space; the registers hold only data. The program cannot read, write, or modify itself — it is fixed before execution begins.This separation of program and data is exactly what defines the Harvard architecture, which you encountered in L06. Recall that in a Harvard architecture, instructions and data reside in separate memory spaces with separate buses. The RAM model is the theoretical embodiment of this principle. The program is "hardwired" into the machine, much like firmware in a dedicated hardware controller.This design has a direct consequence: a RAM machine cannot write a new program into its registers and then execute it. It cannot implement subroutines by storing return addresses alongside instructions. It cannot modify its own code. For all these capabilities, we need a different model — one where the program lives in the same memory as the data. That model is the RASP machine, which we turn to next.The Random Access Stored-Program Machine (RASP) takes the RAM model and makes one fundamental change: the program is no longer stored in a separate instruction table. Instead, the program's instructions are encoded as numbers and stored in the same registers as the data. The RASP machine reads its instructions from memory, decodes them, executes them, and then fetches the next instruction — exactly as a real CPU does.The RASP can be thought of in a memorable way:
The RASP is a Universal Turing Machine built on a RAM chassis.
Just as the Universal Turing Machine is a Turing machine that can simulate any other Turing machine by reading a program from its tape, the RASP is a RAM machine that can execute any program by reading instructions from its registers. The "stored program" in the name is the key concept — the same concept that John von Neumann championed in 1945 and that underlies every general-purpose computer in existence today.The RASP retains the RAM's components — registers, input tape, output tape, and a finite-state control unit — but reorganizes them:Unified Memory (R0, R1, R2, ..., Rn)A single set of registers holds both program instructions and data. The first portion of the registers (say R0 through Rk) might contain encoded instructions, while registers beyond that hold data values. But there is no enforced boundary — instructions and data share the same address space.Finite-State Control UnitA small, fixed interpreter that implements the fetch-decode-execute cycle. This control unit is not the program itself — it is the hardware (or rather, the abstract equivalent of hardware) that knows how to:
Fetch the next instruction from the register pointed to by the program counter
Decode the numeric instruction (e.g., "3" means JZ, "2" means DEC)
Execute the decoded operation on the appropriate registers
Advance the program counter to the next instruction (or to a jump target)
Input and Output TapesIdentical to the RAM model — a read-only input tape and a write-only output tape.<br><img alt="RASP machine architecture" src="assets/l09-rasp-machine-architecture.png" target="_self">The RASP machine architecture: a unified memory holds both program instructions (P0, P1, ...) and data (R0, R1, ...) in the same space. The Finite Control Unit fetches, decodes, and executes instructions, reading from the input tape and writing to the output tape.The RASP's execution cycle will feel immediately familiar — it is the fetch-decode-execute cycle that you studied in L06. Here is a concrete example. Suppose registers 5 through 15 contain the following encoded program, with the program counter initially set to 5: ; Address Opcode Operand(s) ; ------ ------ ---------- 5: JZ 18, 15 ; If register 18 is zero, jump to address 15 8: DEC 18 ; Decrement register 18 10: INC 19 ; Increment register 19 12: JZ 15, 5 ; Unconditional jump to address 5 (register 15 always holds 0) 15: HALT ; Stop execution 18: n ; Source value (data) 19: 0 ; Destination (data, initially 0)
This program copies the value n from register 18 to register 19 by repeatedly decrementing one and incrementing the other. The trick for implementing an unconditional jump is noteworthy: since we only have JZ (jump if zero), we jump on register 15 which is known to contain zero (the HALT opcode, which happens to be 0). This is a classic technique in minimalist instruction sets — building complex behavior from primitive building blocks.Notice that instructions and data occupy the same register space — register 18 is both a data location that the program operates on and a memory cell in the same address space as the program instructions. If the program were to write a value to register 10, it would overwrite one of its own instructions — this is self-modification in action.The finite-state control unit executes this as follows:
Fetch: Read the contents of register 5 (the current PC value). It contains "JZ" (encoded as, say, opcode 3).
Decode: The control unit recognizes opcode 3 as "Jump if Zero," which requires two parameters. It fetches registers 6 and 7 to get the operands: register number 18 and jump target 15.
Execute: Check if register 18 contains zero. If not, advance the PC past this instruction (to address 8). If yes, set the PC to 15.
Repeat: Fetch the next instruction from the new PC value.
This is fundamentally different from the RAM model. In the RAM, the control unit reads instructions from a separate, hard-wired instruction table. In the RASP, the control unit reads instructions from the same registers that hold data.A point that often causes confusion: the RASP model actually involves two sets of instructions. The first set is the fixed instruction table of the finite-state control unit — the "interpreter" that is hardwired into the machine. The second set is the program written in the registers — the instructions being interpreted. These two instruction sets do not have to be the same. The control unit's built-in instructions might include only primitive operations like "indirect copy" and "conditional jump," while the program in the registers might use a richer, higher-level instruction set that the interpreter decodes and simulates.This two-level structure is analogous to how a real computer works: the CPU's microcode (the hardwired control logic) interprets the machine instructions (the program in RAM). It is also analogous to how an emulator works: a program running on one architecture interprets instructions written for a completely different architecture.The RASP model embodies the von Neumann architecture — the design principle where program instructions and data share a single memory space and a single bus. This is the architecture of virtually every general-purpose computer manufactured since the 1950s, from the original EDVAC to the laptop or phone you are using right now.The von Neumann insight was revolutionary: if instructions are just numbers stored in memory, then a computer does not need to be physically rewired to run a different program. You simply load a new sequence of numbers into memory and restart execution. This is what makes a computer general-purpose rather than a dedicated calculating device.Placing the program in the same memory as the data has profound consequences:Subroutines become possible. A subroutine call can store a return address in a register and jump to the subroutine's starting address. When the subroutine finishes, it reads the stored return address and jumps back. In the RAM model, where the program is fixed and separate from data, this mechanism is not available — there is no way to "remember" where to return to by writing into the instruction table.Self-modifying code becomes possible. Since instructions are just numbers in registers, a RASP program can rewrite its own instructions at runtime. It could, for example, modify the operand of a LOAD instruction to access a different register on each iteration — effectively implementing computed addressing. While self-modifying code is considered poor practice in modern software engineering, it was a common and important technique in the early days of computing when memory was scarce and indirect addressing hardware was not yet available.The program can be treated as data. A RASP can read a program from its input tape, store it in registers, and then execute it. This means a single RASP machine, with a fixed interpreter in its control unit, can execute any program — making it a universal computer. This is exactly what an operating system's program loader does: it reads an executable file from disk into memory and then transfers control to it.Security implications emerge. If program and data share the same space, a malicious or buggy input could potentially overwrite program instructions. This is the theoretical root of buffer overflow attacks — a concept you may explore in later courses on computer security.The relationship between the RASP and the RAM mirrors the relationship between the Universal Turing Machine and the basic Turing Machine:
A Turing Machine has a fixed program (its state transition table) and processes data on a tape. It computes one specific function.
A Universal Turing Machine reads a description of another Turing machine from the tape and simulates it. It can compute any computable function.
Similarly:
A RAM machine has a fixed program (its instruction table) and processes data in registers. It computes one specific function.
A RASP machine reads a program from its registers and executes it. With the right interpreter, it can compute any computable function.
The RASP is thus the "universal abstract computer" — the theoretical model that most directly corresponds to what we mean by "a computer" in everyday language.The following table summarizes the key differences between the two models:You might wonder: if the RASP is more powerful and more realistic, why do we bother with the RAM model at all?The answer is that the RAM is simpler to reason about. When analyzing the time complexity of an algorithm, we do not want to worry about instruction encoding, fetch cycles, or the overhead of interpreting a stored program. The RAM model lets us count the number of operations (additions, comparisons, memory accesses) directly, without the layer of indirection that the RASP's stored-program mechanism introduces.In practice, complexity theorists use the RAM model for analyzing algorithms and the RASP model when they need to reason about what it means for a machine to be programmable or universal. The two models are computationally equivalent — anything one can compute, the other can too — but they serve different conceptual purposes.The RAM model also provides the theoretical foundation for the Harvard architecture used in many embedded systems and microcontrollers. If you program an Arduino, you are working with a processor (AVR) that uses a modified Harvard architecture: program code lives in Flash memory and data lives in SRAM, just as the RAM model prescribes. The RASP model, on the other hand, describes the x86 processors you have been programming throughout this course, where your .code and .data segments ultimately reside in the same address space.Understanding both models gives you the theoretical vocabulary to describe why computer architectures are designed the way they are — not just how they work. The Harvard vs. von Neumann distinction is not merely a historical curiosity; it is a fundamental design decision that shapes everything from memory bus design to security models, and its roots trace directly back to the RAM and RASP abstract machines.In the previous sections, we explored abstract models of computation — RAM and RASP machines — that describe what a computer can theoretically compute. We also examined how the von Neumann architecture provides the physical blueprint for building real machines. But between the abstract mathematical model and the concrete silicon implementation, there is a critical layer that ties everything together: the Instruction Set Architecture, or ISA.An Instruction Set Architecture is an abstract model that defines the programmable interface of a CPU. It is a formal contract between software and hardware: software promises to express its intentions using only the operations, registers, and data formats defined by the ISA, and hardware promises to execute those operations correctly, regardless of how the circuitry underneath is organized.Think of the ISA as a boundary line. Everything above the line — compilers, operating systems, application programs — sees only the ISA. Everything below the line — transistor layouts, pipeline stages, cache hierarchies, branch predictors — is invisible to software. This separation is what allows an Intel Core i9 and a budget Celeron to run the same x86 program: both implement the same ISA, but with vastly different microarchitectures underneath.<br><img alt="The ISA as a contract — software above, hardware below, ISA is the boundary" src="assets/l09-isa-boundary.svg" target="_self">More precisely, the ISA defines how a program controls the CPU. When you write MOV AX, 5 or ADD BX, CX, you are using the vocabulary and grammar of an ISA. The CPU's job is to interpret and execute those instructions faithfully. But the ISA does not dictate how the CPU should do so internally — that is the domain of the microarchitecture, which is the specific engineering design (pipeline depth, execution units, cache structure) used to implement the ISA.<br>This distinction was first articulated clearly by Fred Brooks at IBM during the design of the <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/IBM_System/360" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/IBM_System/360" target="_self">System/360</a> in the early 1960s. Before System/360, every new IBM machine had its own instruction set, which meant that software written for one model could not run on another. Brooks proposed a radical idea: define a single architecture (ISA) and build five different machines — spanning a wide range of cost and performance — that all implement it. This was the birth of the ISA as a deliberate abstraction layer, and it remains one of the most fundamental concepts in computing.An ISA is not just a list of instructions. It is a comprehensive specification that covers every aspect of the programmer-visible machine state. The key elements include: Supported data types. Which data formats does the processor natively understand? Integers (8-bit, 16-bit, 32-bit, 64-bit), floating-point numbers (single precision, double precision), packed decimal (BCD), character data, and potentially SIMD vector types. You have already worked with byte (DB), word (DW), and doubleword (DD) data types in x86 assembly. Registers. How many registers are available, what size are they, and what are their purposes? The x86 ISA defines general-purpose registers (AX, BX, CX, DX), pointer registers (SP, BP), index registers (SI, DI), segment registers (CS, DS, SS, ES), an instruction pointer (IP), and a flags register (FLAGS). Some ISAs have far more registers — MIPS has 32 general-purpose registers, ARM has 16, and RISC-V can have up to 32. Memory model. How does the processor view memory? Is it a flat address space or segmented? What is the byte ordering (little-endian vs. big-endian)? How are addresses calculated? The x86 ISA you studied in L03 uses a segmented memory model in Real Mode, with physical addresses computed as segment * 16 + offset. Addressing modes. What methods can instructions use to specify their operands? The x86 ISA supports immediate values (MOV AX, 5), register operands (MOV AX, BX), direct memory addressing (MOV AX, [1234h]), register-indirect addressing (MOV AX, [BX]), and base+displacement addressing (MOV AX, [BX+4]), among others. You have used all of these in your lab work. Instruction formats. How are instructions encoded in binary? What is the structure of an opcode? Are instructions fixed-length (every instruction occupies the same number of bytes) or variable-length (different instructions can be different sizes)? x86 uses variable-length encoding (instructions range from 1 to 15 bytes), while ARM uses fixed-length 32-bit instructions. Instruction set. The complete catalog of operations the processor can perform: arithmetic, logical, data transfer, control flow, string manipulation, I/O, and so on. This is what most people think of when they hear "ISA," but it is only one component of the full specification. Interrupt and exception model. How does the processor respond to external events (hardware interrupts) and internal errors (divide by zero, invalid opcode)? You studied the x86 interrupt model in detail in L07, including the Interrupt Vector Table (IVT) and the INT instruction for software interrupts. I/O model. How does the processor communicate with peripheral devices? Some ISAs use dedicated I/O instructions (like x86's IN and OUT, which you studied in L08), while others use memory-mapped I/O where device registers appear as ordinary memory addresses. An entity that executes instructions according to an ISA is called an implementation of that ISA. The most obvious implementation is a physical CPU chip. But CPUs are not the only possible implementers: Different CPU generations. The Intel 8086, 80286, 80386, Pentium, Core i7, and Core i9 are all implementations of the x86 ISA (with extensions added over time). They have radically different microarchitectures — different numbers of pipeline stages, different cache sizes, different execution unit configurations — but they all run the same x86 machine code. Different manufacturers. AMD and Intel both produce CPUs that implement the x86-64 ISA. An AMD Ryzen and an Intel Core can run the same compiled binary, even though they were designed by competing engineering teams with completely different internal designs. Emulators. A software program running on one architecture that simulates another architecture instruction-by-instruction. DOSBox, which you use in labs, is an emulator: it implements the x86 ISA in software running on your host computer (which might itself be x86 or ARM). The emulated programs "think" they are running on real x86 hardware. Virtual machines. The Java Virtual Machine (JVM) defines its own ISA based on bytecode instructions. Any device that can run a JVM implementation — whether a phone, a laptop, or a server — can execute the same Java bytecode. Similarly, the .NET Common Language Runtime (CLR) defines a bytecode ISA for C# and other .NET languages. Interpreters. A program that reads instructions one at a time and performs the corresponding operations. Many early home computers included a BASIC interpreter in ROM that defined, in effect, a very high-level ISA. <br><img alt="ISA implementations — one x86 specification, many implementations across generations, manufacturers, emulators, and VMs" src="assets/l09-isa-implementations.svg" target="_self">This multiplicity of implementations is precisely what makes the ISA abstraction so powerful. By separating what the machine does from how it does it, the ISA enables software portability, hardware competition, and technological evolution — all without breaking existing programs.An important property of ISAs is extensibility. An ISA can be extended by adding new instructions, new registers, new data types, or wider addresses — as long as the extensions are backward-compatible. The x86 ISA has been extended repeatedly over its 45+ year history: from the original 16-bit 8086 instruction set to 32-bit extensions (IA-32), then 64-bit extensions (x86-64), and numerous specialized instruction set extensions (MMX, SSE, SSE2, AVX, AVX-512). Code compiled for the original 8086 can still run on a modern Core i9 — the ISA contract guarantees backward compatibility. However, code that uses newer extensions (like AVX-512 vector instructions) will not run on older processors that do not implement those extensions.The ISA abstraction enables a property called binary compatibility: a program compiled for a given ISA will run on any implementation of that ISA, current or future. This is enormously valuable. It means that when Intel releases a new processor generation, all existing x86 software continues to work without modification. Users can upgrade their hardware without replacing their software, and software developers can target a single ISA rather than writing separate code for each processor model.Binary compatibility also enables hardware competition. Because AMD and Intel both implement x86-64, they compete on performance, power efficiency, and price — while both running the same software ecosystem. Without the ISA abstraction, each manufacturer's software would be incompatible, fragmenting the market.The flip side is that an ISA, once established and widely adopted, is extremely difficult to change or replace. The x86 ISA carries decades of accumulated complexity — features added for long-obsolete hardware, redundant instructions, quirky encoding choices — that cannot be removed without breaking existing software. This is the "golden handcuffs" of backward compatibility: the same property that makes ISAs so valuable also makes them resistant to cleanup. It is one reason why the clean-slate RISC-V ISA, designed from scratch in 2010 with the benefit of hindsight, has attracted so much attention.There are many ISAs in the world — x86, ARM, MIPS, RISC-V, SPARC, PowerPC, z/Architecture, and dozens more. To make sense of this diversity, computer architects classify ISAs along two primary axes: By operand location — where do instruction operands come from? This classification asks: when an instruction like ADD needs two input values, where does it find them? On a stack? In an accumulator? In general-purpose registers? In main memory? This axis produces five major ISA types: stack-based, accumulator-based, register-memory, register-register (load-store), and memory-memory architectures. We will explore each of these in detail in Section 6. By instruction complexity — how much work does a single instruction perform? This axis distinguishes CISC (Complex Instruction Set Computer) architectures, where a single instruction can perform multi-step operations like "load from memory, add, and store the result," from RISC (Reduced Instruction Set Computer) architectures, where each instruction performs exactly one simple operation. We will explore this classification in later sections. <br><img alt="ISA classification on two axes — operand location vs instruction complexity, with real ISAs plotted" src="assets/l09-isa-classification-axes.svg" target="_self">These two axes are not independent. Load-store architectures tend to be RISC (since restricting memory access to dedicated load/store instructions is a hallmark of the RISC philosophy), while memory-memory architectures are inherently CISC (since they combine memory access and computation in one instruction). But the correlation is not perfect — x86 is register-memory but includes many complex (CISC) instructions, while some embedded CISC processors have very simple instruction sets. Understanding both axes gives you a richer mental model of ISA design trade-offs.In the next section, we examine the first axis in detail: the five major ISA types defined by where operands are located. For each type, we will show the hardware datapath, trace through the same computation (A = B + C), and analyze the trade-offs in instruction count, code density, hardware complexity, and pipeline friendliness.The most fundamental design decision in any ISA is: where do operands live? When the CPU needs to perform an addition, where does it find the two values to add, and where does it put the result? The answer to this question shapes the entire instruction format, determines how many bits each instruction requires, influences the complexity of the hardware, and ultimately affects performance.To make the comparison concrete, we will trace the computation of a simple expression through each architecture type:A = B + C
This expression requires reading two values (B and C), adding them, and storing the result (A). Watch how the number of instructions, the instruction format, and the data movement patterns differ across architectures.In a stack-based architecture, the CPU's primary data storage is organized as a stack — a last-in, first-out (LIFO) structure. You are already familiar with the stack concept from L05, where you used PUSH and POP to save and restore registers and pass parameters to procedures. In a stack-based ISA, the stack is not merely a convenience for procedure calls — it is the central mechanism for all computation.Here is the key idea: arithmetic instructions in a stack-based ISA take no explicit operand addresses. Instead, they implicitly operate on the top elements of the stack. An ADD instruction pops the top two values from the stack, adds them, and pushes the result back onto the stack. Because no addresses need to be specified in the instruction, these are called zero-address instructions — the instruction consists of nothing but the opcode.<br><img alt="Stack-based ISA architecture" src="assets/l09-isa-stack-based.png" target="_self">The stack-based architecture: a register file organized as a stack feeds the top two values to the ALU inputs (A and B). The ALU result is pushed back to the top of the stack. Memory is accessed only through explicit PUSH and POP instructions.Computing A = B + C in a stack ISA:PUSH B ; Push the value of B onto the stack
PUSH C ; Push the value of C onto the stack
ADD ; Pop two values, add them, push the result
POP A ; Pop the result and store it into A
Notice that the ADD instruction has no operands at all — it is a single opcode byte. Only the PUSH and POP instructions carry memory addresses. This leads to very compact instruction encoding: the arithmetic and logical instructions are tiny (often a single byte), which produces dense code.The stack-based approach uses postfix notation (also known as Reverse Polish Notation, or RPN). Instead of writing B + C (infix), the operations are expressed as B C + (postfix). This is the same notation used by classic HP calculators and the Forth programming language. Compilers can translate any arithmetic expression into postfix form mechanically, which makes stack-based ISAs easy targets for compiler code generation.To see how this scales to more complex expressions, consider computing X = (A + B) * (C - D):PUSH A ; Stack: [A]
PUSH B ; Stack: [A, B]
ADD ; Pop A and B, push A+B. Stack: [A+B]
PUSH C ; Stack: [A+B, C]
PUSH D ; Stack: [A+B, C, D]
SUB ; Pop C and D, push C-D. Stack: [A+B, C-D]
MUL ; Pop A+B and C-D, push (A+B)*(C-D). Stack: [result]
POP X ; Pop result into X. Stack: []
Eight instructions, and notice how the intermediate results are managed automatically by the stack. The value A+B sits on the stack while C-D is computed above it, and then MUL combines them. No temporary variable names or register assignments are needed — the stack order handles everything. However, this convenience comes at a cost: if the expression required using A+B again later in a different sub-expression, you would need to duplicate it on the stack before consuming it, adding extra stack manipulation instructions.Advantages:
Simple hardware. No need for complex register selection logic or operand routing. The ALU always reads from the top two stack positions and writes back to the top.
Compact code. Zero-address arithmetic instructions are very small, leading to high code density — more instructions fit into the same amount of memory.
Easy compilation. Converting high-level expressions to postfix stack operations is straightforward and well-understood.
Disadvantages:
Stack bottleneck. Every intermediate result must pass through the stack. If a value computed early is needed much later, it must either stay buried in the stack (requiring careful ordering) or be stored to memory and reloaded.
Limited parallelism. Instructions have strict data dependencies through the stack top, making it difficult for the hardware to execute multiple instructions simultaneously.
Performance overhead. Frequent stack manipulations (push, pop, swap, duplicate) add instructions that do not perform useful computation, and all data must flow through a single narrow bottleneck.
<br>Where is it used? Pure stack-based hardware architectures were more common in early computing history. The <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Burroughs_large_systems" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Burroughs_large_systems" target="_self">Burroughs B5000</a> (1961) was a pioneering stack machine designed to execute ALGOL efficiently. The x87 floating-point unit in x86 processors also uses a stack-based register model (an eight-entry register stack, ST(0) through ST(7)), which persisted from the Intel 8087 coprocessor (1980) through modern x86 CPUs, though it has been largely superseded by SSE/AVX register-based instructions.<br>Today, stack-based architectures survive primarily in two domains: embedded and industrial systems (where simplicity translates directly to cheaper silicon and lower power consumption) and virtual machines (the Java Virtual Machine and the .NET CLR both use stack-based bytecode instruction sets, though their JIT compilers ultimately translate to register-based native code). The <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Minimal_instruction_set_computer" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Minimal_instruction_set_computer" target="_self">Minimal Instruction Set Computer</a> (MISC) is a modern variant of the stack machine concept, packing multiple tiny stack operations into a single machine word — an approach used in some FPGA-based soft processors where silicon area is at a premium.An accumulator-based architecture uses a single distinguished register — the accumulator — as one implicit operand for every arithmetic operation. The other operand comes from memory. The result is always stored back into the accumulator. Because only one memory address needs to be specified per instruction (the accumulator is implicit), these are called one-address instructions.If you have been working with x86 assembly in this course, the accumulator concept should feel familiar. Recall from L03 and L04 that the AX register in x86 serves as the hardwired destination for many operations: MUL and DIV always use AX (and DX:AX for extended results), and historically, many instructions were optimized for the accumulator. The name "AX" itself stands for "Accumulator eXtended." The x86 ISA is not a pure accumulator architecture (it has multiple general-purpose registers), but its design carries strong accumulator-era DNA.<br><img alt="Accumulator-based ISA architecture" src="assets/l09-isa-accumulator-based.png" target="_self">The accumulator-based architecture: the accumulator register provides one ALU input (A), while memory provides the other input (B). The ALU result is stored back into the accumulator. Only a single memory address is encoded in each instruction.Computing A = B + C in an accumulator ISA:LOAD B ; Load the value of B into the accumulator
ADD C ; Add the value of C (from memory) to the accumulator
STORE A ; Store the accumulator value into memory location A
Three instructions, each specifying a single memory address. The accumulator is always the implicit source and destination — it never needs to be named in the instruction.Now consider the more complex expression X = (A + B) * (C - D) on an accumulator machine:LOAD A ; ACC = A
ADD B ; ACC = A + B
STORE TEMP ; TEMP = A + B (must save — ACC will be overwritten)
LOAD C ; ACC = C
SUB D ; ACC = C - D
MUL TEMP ; ACC = (C - D) * TEMP = (C - D) * (A + B)
STORE X ; X = result
Seven instructions, and notice the critical problem: we had to use a temporary memory location (TEMP) to hold the intermediate result A + B while computing C - D. With only one register, every sub-expression that needs to be saved for later must be spilled to memory. In a stack machine, the stack itself holds intermediates automatically. In an accumulator machine, the programmer (or compiler) must manage temporaries explicitly.Advantages:
Simple instruction format. Each instruction is just an opcode plus one memory address, making decoding straightforward.
Fewer bits per instruction than architectures that must specify two or three register/memory operands.
Simple hardware. The ALU always reads one input from the accumulator and the other from the data bus; the result always returns to the accumulator. No operand routing multiplexers needed.
Disadvantages: Heavy memory traffic. Since there is only one register for holding intermediate results, complex expressions require frequent loads and stores. For an expression like (A + B) * (C - D), you must store the intermediate A + B result to a temporary memory location before computing C - D, then reload it for the multiply. Sequential bottleneck. All data flows through the single accumulator, creating a dependency chain that limits instruction-level parallelism. Every instruction depends on the previous one (because each instruction reads and writes the same accumulator), making it nearly impossible to overlap instruction execution. Code size for complex programs. While individual instructions are small, the total number of instructions grows quickly for complex expressions due to the constant need to save and reload the accumulator via temporary memory locations. <br>Historical significance. Accumulator-based architectures dominated early computing. The <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/EDSAC" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/EDSAC" target="_self">EDSAC</a> (1949), one of the first stored-program computers, was an accumulator machine. Many early microprocessors — the Intel 8080, the MOS Technology 6502, the Zilog Z80 — used accumulator-centric designs. The accumulator model was a natural fit for the technology of its era: when transistors were expensive and chip area was limited, dedicating silicon to a single privileged register was far more practical than providing a bank of general-purpose registers.As register counts grew (the 8086 introduced four "general-purpose" registers, AX, BX, CX, DX), pure accumulator architectures gave way to register-based designs, but the accumulator's legacy persists in the privileged role that AX/EAX/RAX plays in the x86 ISA to this day. Even in modern x86, some instructions (like MUL, DIV, CBW, LAHF) still implicitly use AX as their operand, a direct inheritance from the accumulator tradition.As transistor budgets grew and CPU designers could afford more registers, architectures evolved beyond the single-accumulator model to include multiple general-purpose registers. This fundamentally changed the computation model: instead of forcing all data through one bottleneck register, the CPU can hold many intermediate values simultaneously, dramatically reducing memory traffic. But within the register-based family, there is a crucial design split: should arithmetic instructions be allowed to access memory directly, or should memory access be restricted to dedicated load and store instructions?<br><img alt="Register-Memory and Register-Register architectures" src="assets/l09-isa-register-memory-and-register-register.png" target="_self">Two register-based approaches side by side. (a) Register-Memory: one ALU input comes from a register, the other from memory. (b) Register-Register (load-store): both ALU inputs come from registers; memory is accessed only through dedicated LOAD and STORE instructions.In a register-memory architecture, arithmetic instructions can take one operand from a register and one from memory. This is the two-address format you know well from x86 assembly. Recall from L03 and L04 the various MOV operand combinations you studied (REG-REG, REG-MEM, MEM-REG) — this is the register-memory model in action:; x86 register-memory example: A = B + C
MOV AX, [B] ; Load B from memory into register AX
ADD AX, [C] ; Add C from memory directly to AX
MOV [A], AX ; Store the result to memory location A
The ADD AX, [C] instruction is characteristic of a register-memory architecture: it combines a memory access (reading C) with an arithmetic operation (addition) in a single instruction. The result overwrites one of the input registers (AX), which means one of the source operands is destroyed — a consequence of the two-address format.Advantages:
Fewer instructions for simple operations compared to load-store, since arithmetic can access memory directly.
Familiar and flexible — you can mix register and memory operands naturally.
Disadvantages:
Variable execution time. An instruction that accesses memory takes longer than one that operates purely on registers, making it harder for the hardware to pipeline efficiently.
Instruction encoding complexity. Supporting multiple addressing modes for memory operands leads to variable-length instructions and complex decode logic.
The x86 ISA is the most prominent example of a register-memory architecture, though as noted in L04, x86 does enforce one important restriction: you cannot have both operands in memory for a single arithmetic instruction (no MEM-to-MEM ADD). At least one operand must be a register. The Motorola 68000 family is another prominent register-memory architecture, and it took the model further by providing eight data registers (D0-D7) and eight address registers (A0-A7), giving the programmer more room to hold intermediate values than the 8086's four general-purpose registers.Two-address instructions also introduce a subtle semantic issue: one of the source operands is destroyed by the operation. In ADD AX, [C], the original value of AX is lost — it is overwritten by the sum. This is a direct consequence of reusing one source register as the destination. If the original value of AX is needed later, the programmer or compiler must save it beforehand (e.g., MOV DX, AX before the ADD). Three-address architectures avoid this problem entirely by specifying a separate destination.A register-register architecture, also called a load-store architecture, takes the separation further: all arithmetic and logical operations work exclusively on registers. Memory can only be accessed through dedicated LOAD (read from memory into a register) and STORE (write from a register to memory) instructions. No arithmetic instruction ever touches memory directly.; MIPS load-store example: A = B + C
LW $t0, B ; Load the value of B into register $t0
LW $t1, C ; Load the value of C into register $t1
ADD $t2, $t0, $t1 ; Add $t0 and $t1, store result in $t2
SW $t2, A ; Store $t2 to memory location A
Four instructions instead of three, and the ADD instruction specifies three registers (two sources and one destination) — a three-address format. The result goes to a separate destination register, preserving both input values for potential reuse.This may seem less efficient — more instructions for the same computation. But the load-store approach has profound advantages for hardware design:Advantages: Uniform instruction timing. Arithmetic instructions always operate on registers (fast), and load/store instructions always access memory (slower). The hardware can pipeline these cleanly because it knows exactly which instructions will need memory access. This predictability is a key enabler of pipelining — the technique that allows a CPU to overlap the execution of multiple instructions. Fixed-length instructions. Because arithmetic instructions only name registers (not memory addresses with complex addressing modes), instruction encoding is simpler and typically fixed-length. Fixed-length instructions are much easier to fetch, decode, and pipeline. Register reuse. With three-address instructions, intermediate results stay in registers and can be reused without reloading. In a register-memory machine, an intermediate result stored in a register might be overwritten by the next instruction that uses that register as a destination. Compiler-friendly. Modern optimizing compilers excel at allocating variables to registers and scheduling loads/stores to hide memory latency. The clean separation between computation and memory access makes register allocation and instruction scheduling significantly easier. A compiler can schedule independent loads early in the instruction stream, so that data arrives from memory by the time the arithmetic instruction needs it — a technique called load scheduling or software pipelining. Simpler decoding. With a uniform instruction format (all arithmetic instructions look the same: opcode + three register fields), the hardware decoder is simple, fast, and easy to parallelize. This is in stark contrast to x86, where the decoder must handle hundreds of different instruction formats and variable-length encodings. Now consider X = (A + B) * (C - D) on a load-store machine:LW $t0, A ; $t0 = A
LW $t1, B ; $t1 = B
ADD $t2, $t0, $t1 ; $t2 = A + B
LW $t3, C ; $t3 = C
LW $t4, D ; $t4 = D
SUB $t5, $t3, $t4 ; $t5 = C - D
MUL $t6, $t2, $t5 ; $t6 = (A + B) * (C - D)
SW $t6, X ; Store result to X
Eight instructions — more than either the stack or accumulator versions. But notice a key advantage: no temporary memory location is needed. The intermediate value A + B sits safely in register $t2 while C - D is computed in $t5. No memory spill, no reload. Moreover, a pipelined processor could potentially begin loading C and D while the ADD is still executing, since these operations are independent. This is the kind of instruction-level parallelism that load-store architectures are designed to exploit.Disadvantages:
More instructions needed for the same computation, since every memory access requires a separate load or store instruction.
Register pressure. If a program needs more live values simultaneously than there are available registers, the compiler must "spill" values to memory and reload them later, adding load/store overhead. This metric — the ratio of values that need to be live to the registers available — is called register pressure. RISC architectures mitigate this by providing large register files (32 registers is typical for MIPS, ARM, and RISC-V), but complex programs can still exhaust them. Embedded RISC ISAs with smaller register sets (like ARM Thumb with only 8 general-purpose registers) can suffer from high register pressure.
<br>Examples. Most modern ISAs are load-store architectures: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/ARM_architecture_family" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/ARM_architecture_family" target="_self">ARM</a>, <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/MIPS_architecture" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/MIPS_architecture" target="_self">MIPS</a>, <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/RISC-V" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/RISC-V" target="_self">RISC-V</a>, <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/SPARC" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/SPARC" target="_self">SPARC</a>, and <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/PowerPC" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/PowerPC" target="_self">PowerPC</a>. The load-store principle is one of the defining characteristics of the RISC philosophy, and the performance advantages it enables through pipelining have made it the dominant approach in modern processor design.<br>The historical motivation for load-store came from studies at IBM in the 1970s (the <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/IBM_801" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/IBM_801" target="_self">IBM 801</a> project) and at Berkeley and Stanford in the early 1980s. Researchers analyzed how compilers actually used the complex instruction sets of CISC machines and found that the vast majority of instructions used simple register-to-register operations and straightforward memory loads/stores. The complex memory-accessing arithmetic instructions that the hardware designers had painstakingly implemented were rarely generated by compilers. This insight — that simpler instructions, executed faster through pipelining, outperform fewer complex instructions — became the intellectual foundation of the RISC movement.At the opposite extreme from load-store, a memory-memory architecture allows both operands and the result to reside in main memory. An arithmetic instruction specifies three memory addresses: two sources and one destination. This is the most powerful single-instruction format — a single instruction can read two values from memory, perform an operation, and write the result back to memory, all without involving any programmer-visible registers.; Memory-memory example: A = B + C
ADD [A], [B], [C] ; Read B and C from memory, add them, store result to A
One instruction. No registers needed. The instruction encodes three complete memory addresses — which makes it a three-address instruction (in the memory sense, distinct from the three-register format of load-store machines).<br><img alt="Memory-Memory Architecture: ADD [A], [B], [C] — one instruction triggers 3 memory accesses" src="assets/l09-memory-memory-architecture.svg" target="_self">For a more complex expression like X = (A + B) * (C - D):ADD [TEMP], [A], [B] ; TEMP = A + B
SUB [TEMP2], [C], [D] ; TEMP2 = C - D
MUL [X], [TEMP], [TEMP2] ; X = TEMP * TEMP2
Three instructions, each encoding three memory addresses. The intermediate results must go to temporary memory locations because there are no registers to hold them. Compared to the load-store version (eight instructions), the memory-memory version is much more compact in instruction count — but each instruction is far more expensive to execute.This sounds ideal from a programmer's perspective — maximum work per instruction, minimum instruction count. But there are serious practical problems:Disadvantages:
Enormous instructions. Each instruction must encode three full memory addresses. On a 32-bit machine, that is 12 bytes of addresses alone, plus the opcode. Instructions become very large.
Slow execution. Each instruction requires three separate memory accesses (two reads and one write), and memory is far slower than registers. The single powerful instruction takes much longer to execute than three simpler register-based instructions on a pipelined machine.
Pipeline-hostile. The variable and long execution times of memory-memory instructions make pipelining extremely difficult.
Complex hardware. The memory interface must support multiple simultaneous (or back-to-back) memory operations per instruction, adding significant hardware complexity.
Advantages:
Minimal instruction count. For simple expressions, a single instruction can accomplish what takes three or four instructions on other architectures.
No register management. The programmer (and compiler) does not need to decide which values to keep in registers — everything lives in memory.
Recall from L04 that x86 explicitly prohibits memory-to-memory operations for most instructions — you cannot write ADD [A], [B] because at most one operand can be a memory reference. This was a deliberate design choice, and now you can see the performance reasoning behind it. The one notable exception in x86 is the MOVS family of string instructions (e.g., MOVSB, MOVSW), which do perform memory-to-memory transfers — but these are specialized block-copy instructions, not general-purpose arithmetic.<br>Historical examples. The <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/VAX" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/VAX" target="_self">VAX-11</a> (1977) is the most famous memory-memory architecture. VAX instructions could specify up to six operands, each with its own addressing mode, and supported memory-memory operations directly. The VAX was legendary for its orthogonal and powerful instruction set — any instruction could use any addressing mode with any operand. A single VAX instruction could accomplish what might require five or six instructions on a RISC machine. For instance, the VAX POLY instruction could evaluate an entire polynomial in a single instruction, performing a series of multiply-accumulate operations with memory operands.However, this power came at a cost: VAX implementations were slow and difficult to pipeline. The variable-length instructions (ranging from 1 to 54 bytes!) made instruction fetching and decoding unpredictable, and the multiple memory accesses per instruction created long stalls in the pipeline. The contrast between the VAX's raw instruction power and its poor pipeline performance was one of the key motivations for the RISC movement in the 1980s. When Digital Equipment Corporation (DEC) eventually moved to a RISC architecture (the Alpha, in 1992), performance improved dramatically despite programs requiring more instructions.IBM's System/360 also supported some memory-memory operations. Its SS-format (Storage-to-Storage) instructions could move or compare blocks of memory directly, and its AP/SP (Add/Subtract Packed) instructions performed decimal arithmetic entirely in memory. However, most S/360 arithmetic was register-based (RX-format instructions), making it a hybrid architecture rather than a pure memory-memory machine.The table below summarizes the five architecture types across key dimensions. Each row represents a trade-off: simpler hardware and more compact instructions typically come at the cost of lower performance and limited parallelism, while register-rich load-store architectures require more instructions but enable the pipelining and parallelism that drive modern performance.A few patterns emerge from this table:The industry converged on register-based architectures. Both register-memory (exemplified by x86) and register-register (exemplified by ARM, MIPS, RISC-V) dominate modern computing. The stack and accumulator approaches proved too limiting for high-performance designs, while memory-memory architectures were too slow and complex.More instructions does not mean slower execution. The load-store architecture requires four instructions for A = B + C, while the memory-memory architecture needs only one. Yet the load-store machine is faster in practice because its simple, fixed-length, register-only instructions can be deeply pipelined and executed in overlapping fashion. A modern ARM processor can execute several billion such instructions per second, while a VAX-class memory-memory instruction might take a dozen clock cycles for a single operation.The x86 ISA is a hybrid. The x86 occupies an interesting middle ground. It is register-memory by classification (arithmetic instructions can access memory directly), but modern x86 implementations internally decompose complex instructions into simpler micro-operations that resemble load-store RISC instructions. This gives x86 the programmer-facing convenience of register-memory encoding with the implementation benefits of a RISC-like pipeline underneath.Stack machines found new life in virtual machines. While hardware stack machines are rare today, the stack-based execution model proved ideal for virtual machine bytecode. The JVM, the .NET CLR, and WebAssembly all use stack-based instruction formats. The zero-address instructions are extremely compact (important for distributing code over networks), and the stack semantics make bytecode verification straightforward. At runtime, JIT compilers translate stack-based bytecode into native register-based machine code, getting the best of both worlds.The evolution follows a clear historical trajectory. The progression from accumulator to register-memory to load-store mirrors the growth of available transistors. When transistors were scarce (1950s-1970s), the accumulator model was the only practical choice — one register requires minimal silicon. As chip budgets grew, architects could afford 4-8 general-purpose registers (register-memory era, 1970s-1980s). With further Moore's Law scaling, 16-32 registers became affordable, enabling the full load-store model (1980s onward). Each generation traded instruction count for hardware parallelism, and each generation won because the performance gains from pipelining and parallelism far outweighed the cost of executing more instructions. Understanding this trajectory is essential context for the RISC vs. CISC debate that we will examine in later sections.One of the most consequential design decisions in computer architecture is how complex each individual instruction should be. Should a single instruction be able to load a value from memory, add it to a register, and store the result back — all in one step? Or should each of those actions require its own separate instruction?This question divides the world of instruction set architectures into two major camps — CISC (Complex Instruction Set Computer) and RISC (Reduced Instruction Set Computer) — along with a third, less common approach called VLIW (Very Long Instruction Word). Each philosophy reflects a fundamentally different bet about where complexity should live: in the hardware, in the compiler, or in the instruction encoding itself.The CISC-versus-RISC debate shaped the entire trajectory of processor design from the 1980s onward, and understanding it is essential to making sense of modern architectures — from the x86 chip in your laptop to the ARM core in your phone.<br>The CISC philosophy emerged in an era when programmers often wrote assembly code by hand. Memory was scarce and expensive. Compilers were primitive. Under these conditions, the guiding principle was clear: make the hardware do as much work as possible per instruction. If a programmer frequently needs to copy a block of memory, give them a single instruction that does it. If they need to scan a string for a character, build that into the hardware too. The goal was to bridge the <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Semantic_gap" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Semantic_gap" target="_self">semantic gap</a> — the distance between what high-level languages express and what the machine can do in a single step.This philosophy led architects to design instruction sets that directly supported high-level programming constructs: procedure calls, loop control, complex addressing modes that could dereference pointers and index into arrays in a single instruction. The result was rich, expressive instruction sets that could accomplish a great deal in very few lines of machine code.CISC architectures share several defining traits:
Large instruction sets — hundreds of distinct instructions, covering everything from basic arithmetic to string manipulation, BCD (binary-coded decimal) arithmetic, and block memory transfers
Variable-length instructions — instructions can be anywhere from 1 byte to 15 bytes (in x86), depending on the operation and operands
Variable execution time — a simple register-to-register move might take 1 cycle, while a complex string operation might take hundreds
Complex addressing modes — instructions can access memory using base + index + displacement calculations, auto-increment, and other elaborate schemes
Memory operands in arithmetic — a single ADD instruction can load a value from memory, add it to a register, and optionally store the result, all in one step
Microcode — many CISC processors implement complex instructions using internal microcode sequences, essentially running a small program inside the CPU to execute each instruction
Fewer general-purpose registers — because instructions can operate directly on memory, fewer registers were considered necessary (x86 originally had just 8)
To see the CISC philosophy in action, consider the x86 REP MOVSB instruction. This single instruction copies a block of bytes from one memory location to another:; Copy 100 bytes from address in DS:SI to address in ES:DI
mov cx, 100 ; CX = number of bytes to copy
rep movsb ; Repeat: copy byte at [SI] to [DI], increment both, decrement CX ; Continues until CX = 0
That two-instruction sequence replaces what would otherwise be an explicit loop:; The same operation without REP MOVSB
mov cx, 100
copy_loop: mov al, [si] mov [di], al inc si inc di dec cx jnz copy_loop
The CISC version is more compact (fewer bytes of machine code, fewer instructions fetched) and conceptually simpler for the assembly programmer. The hardware handles the loop internally.Another example is the x86 LOOP instruction, which combines three operations — decrement CX, compare it to zero, and branch if not zero — into a single instruction:mov cx, 10
process: ; ... do something ... loop process ; Decrements CX, jumps to 'process' if CX != 0
And the ENTER and LEAVE instructions automate stack frame setup and teardown for procedure calls — operations that RISC architectures require multiple instructions to accomplish.
Code density — programs are smaller in memory, which was critical when RAM was measured in kilobytes
Programmer convenience — a single instruction can express a complex operation, reducing the number of lines in assembly programs
Fewer instruction fetches — because each instruction does more work, fewer instructions need to be fetched from memory, reducing bus traffic
The CISC approach comes with significant costs:
Complexity breeds unpredictability — with hundreds of instructions of varying lengths and execution times, it is extremely difficult for programmers (or compilers) to predict how long a given code sequence will take
Hard to pipeline — variable-length instructions are difficult to decode in parallel, and variable execution times make it hard to keep a pipeline full and balanced
Wasted silicon — studies in the 1970s and 1980s revealed that compilers typically used only 20-30% of the available instructions. The rest consumed chip area and design effort but sat idle
Microcode overhead — the internal microcode engine adds a layer of indirection that slows down even simple operations
Performance depends on the programmer — because the instruction set is so large and irregular, choosing the fastest instruction sequence for a given task requires deep expertise
The CISC philosophy — "think about the programmer" — made perfect sense when programmers wrote assembly by hand. But by the 1980s, the landscape had shifted dramatically. Compilers became sophisticated enough to generate efficient machine code from high-level languages. Programmers rarely wrote assembly anymore.This change exposed a fundamental problem with CISC: if the compiler is generating the code, the instruction set does not need to be convenient for humans — it needs to be efficient for the hardware. Those hundreds of rarely-used instructions were not just wasted silicon; they actively limited performance by making the processor harder to optimize. The complex instruction decoder consumed chip area that could have been used for larger caches, more registers, or deeper pipelines.The realization that compilers, not humans, were the primary "users" of instruction sets set the stage for a radical rethinking of processor design.In the late 1970s, researchers at UC Berkeley and Stanford University arrived at a counterintuitive insight: you can make a processor faster by giving it fewer, simpler instructions. The reasoning was straightforward. If every instruction is simple and uniform, the hardware can execute each one in a single clock cycle. If every instruction is the same length, the decoder can be simple and fast. If the processor does not waste transistors on rarely-used complex instructions, those transistors can be devoted to features that actually improve performance — more registers, bigger caches, deeper pipelines.This was the birth of the RISC philosophy. The name "Reduced Instruction Set Computer" was coined by David Patterson at UC Berkeley in 1980. The "reduced" does not mean the processor can do less — it means each individual instruction does less, and the compiler combines simple instructions to accomplish complex tasks.RISC architectures are built around several interlocking principles:1. Small, fixed instruction set. A typical RISC processor has 50 to 150 instructions, compared to 500+ for a CISC design. Every instruction earns its place by being frequently used.2. Uniform instruction length. All instructions are the same size — typically 32 bits (4 bytes). This makes instruction fetch and decode trivially parallel: the processor always knows where the next instruction starts.3. Few instruction formats. Instructions follow a small number of fixed templates (typically 3-4 formats). The opcode, register fields, and immediate values always appear in the same bit positions, simplifying the decode hardware.4. Load-store architecture. This is the single most defining feature of RISC. Arithmetic and logic instructions operate only on registers. The only instructions that access memory are explicit LOAD (read from memory into a register) and STORE (write from a register to memory). There are no "add a value from memory to a register" instructions.; RISC style: explicit load, operate, store
LOAD R1, [address] ; Step 1: load value from memory into R1
ADD R2, R1, R3 ; Step 2: add R1 and R3, result in R2
STORE R2, [address] ; Step 3: store result back to memory
Compare this to the CISC approach:; CISC style: memory operand in arithmetic instruction
ADD [address], R3 ; Load from address, add R3, store result — one instruction
The RISC version uses three instructions instead of one, but each instruction is simple, predictable, and fast.<br>5. Large register file. Because all computation happens in registers, RISC processors provide many more of them — typically 32 general-purpose registers, and modern designs can have physical register files of 200 to 500 registers (with <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Register_renaming" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Register_renaming" target="_self">register renaming</a> to support out-of-order execution). Compare this to the original x86's 8 general-purpose registers.6. Hardwired control. Instead of microcode, RISC processors implement instruction execution directly in hardware logic. This eliminates the overhead of microcode lookup and sequencing.The RISC approach produces distinctive performance characteristics:
Single-cycle execution — most instructions complete in one clock cycle, making performance highly predictable. The processor can sustain a throughput approaching one instruction per cycle (IPC of ~1), and with superscalar designs, even higher
Efficient pipelining — uniform instruction length and execution time mean the pipeline stages are naturally balanced. There are no stalls caused by one instruction taking 10 times longer than the next
Higher clock frequencies — simpler logic means shorter critical paths, enabling higher clock speeds
Energy efficiency — less complex hardware consumes less power per operation, a critical advantage for mobile and embedded devices
Longer programs — RISC programs are approximately 30% longer (in instruction count) than equivalent CISC programs, because complex operations require multiple instructions. However, each instruction is faster, so total execution time is typically equal or better
Compiler dependency — because the hardware does less "thinking," the compiler must be smarter about instruction scheduling, register allocation, and pipeline optimization. A naive compiler can produce significantly slower code on a RISC processor than on a CISC one
<br>The most dramatic success story in the RISC world belongs to <a data-tooltip-position="top" aria-label="https://www.arm.com/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.arm.com/" target="_self">ARM</a> (originally Acorn RISC Machine, later Advanced RISC Machines). In 1983, a small British company called Acorn Computers began designing a processor for their next-generation personal computer. Inspired by the Berkeley RISC papers, engineers Sophie Wilson and Steve Furber designed a remarkably simple and efficient 32-bit processor. The first ARM chip (ARM1) had just 25,000 transistors — compared to the 275,000 in the contemporary Intel 386.ARM's simplicity translated directly into low power consumption. While this initially seemed like a modest advantage in desktop computers, it became the decisive factor in the mobile revolution. When smartphones and tablets emerged in the 2000s, every manufacturer needed processors that could deliver reasonable performance on a tiny battery. ARM's architecture was the obvious choice.Today, ARM-based processors are everywhere:
Apple Silicon (M1, M2, M3, M4, M5 series) — powering MacBooks and iMacs, proving ARM can compete with x86 even in high-performance desktops
Apple A-series — in every iPhone and iPad
Qualcomm Snapdragon — in most Android smartphones
AWS Graviton — in Amazon's cloud servers
<br>Fujitsu A64FX — in <a data-tooltip-position="top" aria-label="https://www.top500.org/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.top500.org/" target="_self">Fugaku</a>, which was the world's fastest supercomputer in 2020-2022
Embedded systems — in cars, routers, smart TVs, industrial controllers, and billions of IoT devices
ARM ships more than 25 billion chips per year. More ARM processors are manufactured annually than all other processor architectures combined. The RISC bet — that simplicity wins in the long run — paid off spectacularly.<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/MIPS_architecture" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/MIPS_architecture" target="_self">MIPS</a> (Microprocessor without Interlocked Pipeline Stages) was the other foundational RISC architecture, designed by John Hennessy at Stanford in 1981. MIPS became the standard teaching architecture in computer science programs worldwide — if you have taken a computer architecture course elsewhere, you almost certainly studied MIPS.In the commercial world, MIPS powered Silicon Graphics workstations, PlayStation consoles (PS1, PS2, PSP), the Nintendo 64, and countless networking devices. While MIPS has largely faded from the market, its influence on processor design education is immeasurable.<br><a data-tooltip-position="top" aria-label="https://riscv.org/" rel="noopener nofollow" class="external-link is-unresolved" href="https://riscv.org/" target="_self">RISC-V</a> (pronounced "risk-five") is the most recent and arguably the most significant development in the RISC family. Created at UC Berkeley in 2010, RISC-V is a completely open, royalty-free instruction set architecture. Anyone can design, manufacture, and sell RISC-V processors without paying licensing fees.This openness is revolutionary. ARM charges licensing fees that can reach millions of dollars. x86 is proprietary to Intel and AMD. RISC-V breaks this model entirely, functioning like an open standard similar to how TCP/IP is an open networking protocol.RISC-V is gaining rapid adoption in:
Embedded systems and IoT devices
The European Processor Initiative for high-performance computing
Chinese semiconductor companies seeking alternatives to Western-controlled architectures
Academic research and teaching (replacing MIPS as the go-to ISA for textbooks)
Custom accelerators for AI and machine learning
The RISC-V Foundation (now RISC-V International) coordinates the standard, ensuring that the base instruction set remains stable while allowing custom extensions for specialized domains. The base integer instruction set (RV32I) contains just 47 instructions — a deliberately minimal foundation. Extensions add floating-point (F/D), atomic operations (A), compressed instructions (C), and vector processing (V), but each extension is optional. A chip designer picks only the extensions their application needs, avoiding the "instruction bloat" that plagued CISC designs.RISC-V is arguably the purest expression of the RISC philosophy: start minimal, extend only when necessary, and keep the base simple enough that a student can implement a working processor in a semester-long course.There is a third approach to instruction complexity that takes a fundamentally different path. VLIW (Very Long Instruction Word) architectures bundle multiple independent operations into a single, very wide instruction word — often 128 bits or wider.In a conventional processor (CISC or RISC), the hardware is responsible for figuring out which instructions can execute in parallel. This requires complex circuitry for dependency checking, instruction reordering, and dynamic scheduling. VLIW takes a radical shortcut: move all of that complexity into the compiler.A VLIW instruction word contains multiple operation slots. The compiler analyzes the program, identifies operations that have no dependencies on each other, and packs them into the same instruction word. The hardware then executes all operations in the word simultaneously, with no need for dynamic scheduling logic.<br><img alt="VLIW Instruction Word — compiler packs multiple operations into one wide word; hardware executes all slots in parallel" src="assets/l09-vliw-instruction-word.svg" target="_self">All four operations execute in the same cycle, dispatched by a single instruction fetch.The VLIW approach has clear trade-offs:Advantages:
Simpler hardware — no out-of-order execution logic, no register renaming, no dynamic scheduling. The silicon savings can be spent on more functional units or larger caches
Predictable performance — the compiler has already determined the execution schedule, so performance is deterministic
Power efficiency — simpler control logic means less energy spent on instruction scheduling
Disadvantages:
Compiler burden — the compiler must find enough parallelism to fill the instruction slots; if it cannot, slots are filled with NOP (no operation), wasting space and bandwidth
Code bloat — programs can be very large because of unused instruction slots and the wide instruction format
Binary compatibility — if the hardware changes (e.g., a new generation adds more functional units and wider instruction words), old programs must be recompiled
Poor handling of unpredictable code — branches, cache misses, and other runtime events are hard for the compiler to optimize around at compile time
The most prominent VLIW processor was Intel Itanium (IA-64), developed jointly by Intel and Hewlett-Packard in the late 1990s. Itanium was Intel's bet that VLIW (which Intel called EPIC — Explicitly Parallel Instruction Computing) would replace x86 for high-performance servers. The idea was ambitious: let the compiler do the hard work of finding parallelism, and build simpler, more scalable hardware.Itanium failed commercially. Compilers struggled to extract enough parallelism from general-purpose code to fill the wide instruction words. Meanwhile, x86 processors (especially AMD's Opteron) kept getting faster using conventional out-of-order execution techniques. Itanium was officially discontinued in 2021.VLIW has found more success in specialized domains — particularly digital signal processors (DSPs) like the Texas Instruments TMS320 family, where the workload is highly regular and predictable, making it easier for compilers to fill instruction slots effectively.The following table summarizes the key differences between CISC and RISC architectures:If CISC and RISC represent such different philosophies, how is it that the x86 architecture — the quintessential CISC design — remains competitive with ARM in raw performance? The answer reveals one of the most important lessons in computer architecture: the ISA is an interface, and the implementation can be anything.Starting with the Intel Pentium Pro (1995) and AMD K5 (1996), x86 processors adopted a revolutionary internal design. The processor presents the traditional x86 CISC interface to software — variable-length instructions, complex addressing modes, memory operands in arithmetic — but internally, it translates each CISC instruction into one or more RISC-like micro-operations (called micro-ops or μops).These micro-ops are simple, fixed-length, register-to-register operations — exactly the kind of operations that RISC architectures use natively. The processor then executes these micro-ops through a RISC-style pipeline with out-of-order execution, register renaming, and speculative execution.<br><img alt="CISC-RISC Convergence — one complex x86 instruction is decoded into three RISC-like micro-ops" src="assets/l09-cisc-risc-convergence.svg" target="_self">A single complex x86 instruction becomes three simple internal operations — which is exactly what a RISC processor would have required the programmer to write in the first place. The CISC instruction set is now merely a compression format for the actual RISC-like operations the hardware executes.The convergence works in both directions. Modern ARM processors have grown beyond the original RISC simplicity:
ARMv8 (AArch64) includes instructions for cryptographic acceleration (AES, SHA), polynomial multiplication, and other specialized operations
ARM's SVE (Scalable Vector Extension) adds complex vector processing instructions
Apple's M-series processors include a dedicated matrix coprocessor (AMX) and neural engine that blur the line between "simple" RISC instructions and hardware-level complexity
The CISC-versus-RISC debate, once fierce enough to fill academic conferences and corporate boardrooms, has largely been resolved by convergence. Modern high-performance processors — whether nominally CISC or RISC — share most of the same internal techniques:
Out-of-order execution — reordering instructions dynamically to keep functional units busy
Branch prediction — guessing which way conditional branches will go, with accuracy above 95%
Deep pipelines — 15 to 25 stages in modern designs
Large caches — multiple levels of cache hierarchy, often totaling tens of megabytes
Superscalar execution — issuing multiple instructions per cycle
Register renaming — mapping architectural registers to a much larger physical register file to eliminate false dependencies
The real performance differences between modern processors come from microarchitecture — the specific implementation choices about cache sizes, pipeline depth, branch predictor design, memory subsystem bandwidth, and execution unit count. Two processors with the same ISA (e.g., an Intel Core and an AMD Ryzen, both x86) can have dramatically different performance characteristics because of microarchitectural differences.The choice between ARM and x86 today is driven less by the CISC/RISC distinction and more by practical factors: power efficiency (ARM's historical strength), software ecosystem (x86's massive library of compiled software), licensing model (ARM licenses its designs; x86 is restricted to Intel and AMD), and specific workload optimization.The enduring lesson is this: the instruction set architecture is an abstraction layer. Like any good abstraction, it hides the implementation. And as implementations have converged, the once-sharp boundary between CISC and RISC has blurred into a spectrum rather than a binary choice.Understanding the CISC/RISC distinction still matters — not because modern processors fit neatly into one category, but because the design trade-offs that motivated each approach (code density vs. pipeline efficiency, hardware complexity vs. compiler complexity, programmer convenience vs. hardware optimization) recur in every generation of computing. Whether you are designing a tiny microcontroller or a datacenter processor, you will face the same fundamental question: where should the complexity live?Throughout this lecture, we have examined how different instruction set architectures organize computation: how RAM and RASP models process sequences of instructions, how stack, accumulator, and register machines encode operations, and how CISC and RISC philosophies shape the design of real processors. All of these discussions share a common assumption: a single processor executes a single stream of instructions on a single stream of data. But that assumption, however natural it seems, describes only one of four possible ways to organize computation.Why should we care about other ways? Because the clock speeds of individual processors have largely plateaued since the mid-2000s — a phenomenon sometimes called the "end of Dennard scaling" or, more colloquially, the "power wall." Physical limits on power dissipation and heat mean that simply making a single core faster is no longer the primary path to better performance. From the 1970s through the early 2000s, processor clock speeds increased roughly in step with Moore's Law — doubling every 18–24 months. But around 2004–2005, clock speeds hit a ceiling near 4 GHz. Higher frequencies meant more power consumption and more heat, and the heat could not be dissipated fast enough to keep the chip from overheating.The industry's response was a fundamental shift: instead of making one core faster, put multiple cores on the same chip. Intel's first mainstream dual-core processor (the Pentium D) shipped in 2005. By 2025, even budget laptops have 8-core processors, and high-end server CPUs have 128 or more cores. This means that to fully utilize modern hardware, software must be parallel — and to understand parallel software, we need a framework for classifying parallel hardware.<br>In 1966, <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Michael_J._Flynn" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Michael_J._Flynn" target="_self">Michael Flynn</a> — then at IBM, later a professor at Stanford — proposed a remarkably simple classification of computer architectures based on two dimensions: the number of instruction streams and the number of data streams that a system can process concurrently. An instruction stream is a sequence of operations fetched and executed by a processor; a data stream is a sequence of operands on which those instructions act. By asking whether each dimension is "single" or "multiple," Flynn arrived at a two-by-two matrix of four categories:<br><img alt="Flynn's taxonomy classification matrix" src="assets/l09-flynn-taxonomy-table.png" target="_self" style="width: 400px; max-width: 100%;">These four abbreviations — SISD, MISD, SIMD, and MIMD — have become the standard vocabulary for discussing parallel architectures. Nearly every textbook on computer organization and parallel computing uses Flynn's taxonomy as its starting point, and for good reason: despite being proposed nearly sixty years ago, the classification captures the fundamental design choices that architects face when moving beyond sequential execution. The taxonomy does not describe specific hardware implementations; it classifies the logical organization of how instructions and data flow through a system.Flynn extended the taxonomy in a 1972 paper, adding subcategories (particularly for SIMD), but the four top-level categories have remained stable and universally recognized. Other taxonomies exist — notably Duncan's taxonomy (1990), which adds vector processing as a separate category, and Feng's classification, which focuses on word length and bit-level parallelism — but Flynn's remains the most widely used classification of computer architectures in both academia and industry. When hardware engineers, compiler writers, or algorithm designers talk about "SIMD" or "MIMD," they are using Flynn's vocabulary.The elegance of the taxonomy lies in its simplicity. With just two binary dimensions, it captures a surprisingly wide range of architectural choices. At the same time, that simplicity means it cannot capture every nuance — there are architectures that blur the boundaries between categories, and modern systems often combine multiple categories simultaneously (as we will see in Section 10). But as a first-order classification, it has proven remarkably durable for over half a century.Before we examine each category, it is helpful to see how they relate to the concepts we have already covered in this lecture:
SISD corresponds directly to the von Neumann / RASP model from Sections 2–3: one instruction counter, one data stream, sequential execution.
SIMD extends the data path: the same instruction is applied to multiple data elements simultaneously, exploiting data-level parallelism.
MISD replicates the instruction path: multiple instruction streams process the same data, primarily for fault tolerance.
MIMD replicates everything: multiple independent processors, each with its own instruction and data streams, providing the most general form of parallelism.
Let us examine each of the four categories in turn, starting with the most familiar.SISD is the architecture you have been studying for the entire course. A single control unit fetches instructions one at a time from memory. Each instruction operates on a single data element (or a small number of operands) through a single processing unit. The result is written back to memory or a register, and the control unit fetches the next instruction. This is exactly the von Neumann model — and more specifically, the fetch-decode-execute cycle of every processor from the Intel 8086 to the Pentium MMX.<br><img alt="SISD architecture diagram" src="assets/l09-flynn-sisd.png" target="_self">The diagram shows the essential structure: an instruction pool (memory) feeds a single processing unit (PU), which reads from and writes to a single data pool (also memory). There is one arrow for instructions and one arrow for data — hence "single instruction, single data."The flow of execution in SISD is strictly sequential in the logical model:Instruction Pool ──→ [Control Unit] ──→ [Processing Unit] ←──→ Data Pool fetch &amp; decode execute &amp; store
At each step, the control unit fetches one instruction, decodes it, and directs the processing unit to operate on one piece of data. The processing unit reads the operand(s) from the data pool, performs the operation, and writes the result back. Then the cycle repeats with the next instruction.The key characteristic of SISD is that at the conceptual level, there is one thread of control and one thread of data. The processor can still use internal techniques to speed up execution — pipelining overlaps the fetch, decode, and execute stages so that multiple instructions are in different stages simultaneously, and superscalar designs issue more than one instruction per clock cycle. These techniques create instruction-level parallelism (ILP), but they do not change the fundamental SISD classification: there is still a single instruction stream (the sequential program) and a single data stream (the sequential flow of operands).Consider a concrete example. When you run an assembly program on the Intel 8086, the processor fetches one instruction, decodes it, executes it, and moves to the next. Even on a Pentium with its dual pipelines (the U and V pipes), the processor is still executing a single instruction stream — it is simply doing so more efficiently by overlapping stages. The program counter still advances through one sequence of instructions.Examples of SISD architectures: The Intel 8086, 80286, 80386, 80486, and original Pentium are all SISD machines. All single-core processors without SIMD instruction extensions fall into this category. Even early mainframe computers — the IBM 704, the UNIVAC I — are SISD. Essentially, if you can describe the machine's execution as "one instruction at a time, one datum at a time," it is SISD, regardless of whether the implementation uses pipelining or other internal optimizations.Multitasking on SISD: Modern operating systems run many programs "simultaneously" on a single-core SISD processor through context switching — the OS rapidly alternates between programs, giving each a slice of CPU time. This creates the illusion of parallel execution, but it is concurrent (interleaved), not parallel (simultaneous). At any given clock cycle, only one instruction from one program is executing. This distinction matters: concurrency is about managing multiple tasks; parallelism is about executing multiple tasks at the same time.To illustrate the difference: imagine a single chef in a kitchen preparing two dishes. The chef chops vegetables for dish A, then stirs the sauce for dish B, then goes back to dish A — the dishes are being prepared concurrently, but the chef is doing only one thing at a time. True parallelism would require two chefs, each working on their own dish simultaneously. A SISD processor with an operating system doing context switching is the single-chef model; you need MIMD hardware for true multi-chef parallelism.MISD is the most unusual and least commercially successful of Flynn's four categories. In an MISD architecture, multiple processing units each execute a different instruction stream, but all operate on the same data stream. The data flows through the processing units, with each applying its own transformation.<br><img alt="MISD architecture diagram" src="assets/l09-flynn-misd.png" target="_self" style="width: 400px; max-width: 100%;">In the diagram, notice that the instruction pool feeds two (or more) separate processing units, but both processing units draw from the same data pool. Each PU runs its own program, but both work on identical input data. This is the defining feature that distinguishes MISD from all other categories.Compare this to the other three categories:
In SISD, one PU runs one program on one data stream.
In SIMD, multiple PUs all run the same program on different data.
In MIMD, multiple PUs run different programs on different data.
In MISD, multiple PUs run different programs on the same data.
MISD is, in a sense, the "opposite" of SIMD: where SIMD has one instruction applied to many data streams, MISD has many instructions applied to one data stream.At first glance, MISD seems like an odd design choice — why would you want multiple processors working on the same data? The answer lies in two niche but important application domains:Fault tolerance through redundancy. If you need absolute reliability — the kind where a single error could be catastrophic — you can have multiple independent processors execute the same computations on the same input data, and then compare their outputs. If all processors agree, the result is trusted. If one disagrees, it is outvoted and flagged as faulty. This technique is called triple modular redundancy (TMR) or, more generally, N-modular redundancy.The most famous example is the Space Shuttle flight control computer. The Space Shuttle used five redundant General Purpose Computers (GPCs): four primary computers ran identical software on the same sensor data, and a fifth backup ran independently developed software as a safeguard against systematic software errors. The four primary GPCs voted on every output — if one disagreed with the other three, it was automatically taken offline and the system continued operating on the remaining three. In a system where a single computation error could cause loss of vehicle and crew, this redundancy was non-negotiable. The approach worked: across 135 Space Shuttle missions, the flight control computers never caused a mission-critical failure.<br>Systolic arrays. A <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Systolic_array" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Systolic_array" target="_self">systolic array</a> is a network of tightly connected processing elements through which data flows in a rhythmic, pulse-like pattern (the name comes from the systolic rhythm of the heart — data "pulses" through the array like blood through the circulatory system). Each element applies a different operation to the data as it passes through. First described by H. T. Kung and Charles Leiserson in 1978, systolic arrays are efficient for specific tasks like matrix multiplication, convolution, and signal processing, where the computation can be decomposed into a regular pipeline of operations. The data flows through the array without ever needing to access external memory — all operands and partial results are contained within the processing elements themselves, eliminating the memory bottleneck.Systolic arrays have found renewed relevance in the age of machine learning. Google's Tensor Processing Units (TPUs), designed specifically for neural network inference and training, use systolic array architectures internally for their matrix multiply operations — the core computation in deep learning.It is worth noting that the classification of systolic arrays as MISD is somewhat controversial. In a strict reading of Flynn's taxonomy, MISD requires the same data to be processed by different instructions. In a systolic array, the data is actually transformed as it passes through each processing element — so the second element is not operating on exactly the same data as the first. Some researchers argue that systolic arrays should be classified differently (or that they defy clean classification altogether). This is one of those boundary cases where Flynn's simple two-by-two matrix shows its limitations.Pipeline processing and MISD. Some authors also classify deep instruction pipelines as a form of MISD, since different pipeline stages apply different operations to the same (original) instruction's data as it flows through the pipeline. However, this interpretation is not universally accepted — most authors treat pipelining as an implementation technique within SISD, not a separate architectural category. The disagreement illustrates an important point: Flynn's taxonomy classifies architectures at a high level of abstraction, and reasonable people can disagree about edge cases.Why MISD is rare. The fundamental problem with MISD is that most computations do not naturally fit a model where the same data must be processed by multiple different instructions simultaneously. In practice, SIMD (same instruction, different data) and MIMD (different instructions, different data) cover the vast majority of parallel computing needs far more efficiently. MISD remains a textbook category that is important for completeness but has very limited real-world deployment outside fault-tolerant systems and specialized hardware accelerators. Some textbooks even describe MISD as "theoretically possible but practically nonexistent" — an overstatement, given the Space Shuttle example, but indicative of how uncommon MISD implementations are compared to SIMD and MIMD.SIMD is where the practical impact of Flynn's taxonomy becomes most visible in everyday computing. In a SIMD architecture, a single instruction is broadcast to multiple processing units, and each unit applies that same instruction to a different data element simultaneously. One instruction, many data points processed in parallel.<br><img alt="SIMD architecture diagram" src="assets/l09-flynn-simd.png" target="_self" style="width: 400px; max-width: 100%;">The intuition is straightforward. Suppose you need to add two arrays of four numbers element-by-element:A = [10, 20, 30, 40]
B = [ 1, 2, 3, 4]
C = A + B = [11, 22, 33, 44]
On a SISD processor, you would execute four separate addition instructions: C[0] = A[0] + B[0], then C[1] = A[1] + B[1], and so on. Four instructions, four clock cycles (at minimum).On a SIMD processor, you load all four elements of A into one wide register, all four elements of B into another, and execute a single vector addition instruction. One instruction, one clock cycle — four results. The speedup is nearly fourfold.Here is what this looks like in pseudocode, contrasting the scalar (SISD) and SIMD approaches:; Scalar (SISD) approach — 4 separate operations:
LOAD R1, A[0] LOAD R1, A[1] LOAD R1, A[2] LOAD R1, A[3]
LOAD R2, B[0] LOAD R2, B[1] LOAD R2, B[2] LOAD R2, B[3]
ADD R3, R1, R2 ADD R3, R1, R2 ADD R3, R1, R2 ADD R3, R1, R2
STORE C[0], R3 STORE C[1], R3 STORE C[2], R3 STORE C[3], R3 ; SIMD approach — 1 parallel operation:
VLOAD V1, A[0..3] ; Load 4 values into vector register V1
VLOAD V2, B[0..3] ; Load 4 values into vector register V2
VADD V3, V1, V2 ; Add all 4 pairs simultaneously
VSTORE C[0..3], V3 ; Store all 4 results
The scalar approach requires 16 instructions (4 loads + 4 loads + 4 adds + 4 stores). The SIMD approach requires just 4 vector instructions. The data parallelism is explicit: the same operation (addition) is applied uniformly to all elements.This pattern — applying the same operation to every element of a dataset — is extraordinarily common in real-world computing:
Image processing: Adjust brightness by adding a constant to every pixel, apply a filter by multiplying each pixel by a convolution kernel
Audio processing: Amplify by multiplying every sample by a gain factor, mix two tracks by adding corresponding samples
3D graphics: Transform vertices by multiplying each by a transformation matrix, calculate lighting by computing dot products for every surface normal
Scientific computing: Vector and matrix operations in physics simulations, climate modeling, fluid dynamics
Machine learning: Tensor operations — multiply weight matrices by activation vectors, compute gradients across batches of training examples
SIMD is purpose-built for these workloads. The key insight is that data parallelism — the same operation applied uniformly across many data points — is fundamentally different from task parallelism (different operations on different data, which is MIMD territory). SIMD exploits data parallelism, and the amount of data parallelism available in modern workloads is enormous. A single 4K video frame has over 8 million pixels; a single layer of a neural network might involve multiplying matrices with millions of elements. When the operation is the same for every element, SIMD delivers massive throughput gains with minimal hardware complexity.SIMD in modern CPUs. Today, every mainstream desktop and mobile processor includes SIMD capabilities through instruction set extensions — additional instructions that operate on wide registers holding multiple data elements packed side by side:The progression is clear: registers have grown from 64 bits (eight packed bytes in MMX) to 512 bits (sixty-four packed bytes in AVX-512), and the operations have become increasingly sophisticated. A single AVX-512 instruction can add sixteen 32-bit floating-point numbers simultaneously. ARM's SVE (Scalable Vector Extension) takes a different approach by allowing vector lengths to vary between implementations — a processor might have 128-bit, 256-bit, or even 2048-bit vectors, and the same code runs on all of them without recompilation, using predicate masks to handle variable-length vectors.Note the connection to our earlier discussion of ISA design: SIMD extensions are additions to existing instruction sets. Intel's SSE and AVX are extensions to the x86 CISC architecture; ARM's NEON and SVE are extensions to the ARM RISC architecture. The base ISA handles scalar (one-at-a-time) operations; the SIMD extension handles vector (many-at-a-time) operations. A modern x86 processor like an Intel Core i7 is simultaneously a CISC machine (from the ISA perspective), an SISD machine (for scalar code), and an SIMD machine (for vectorized code). Flynn's taxonomy classifies the mode of execution, not the machine as a whole.Historical context. SIMD as a concept predates these instruction set extensions by decades. The history of SIMD hardware can be traced through three distinct eras:<br>Era 1: Dedicated SIMD supercomputers (1970s–1990s). The <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/ILLIAC_IV" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/ILLIAC_IV" target="_self">ILLIAC IV</a>, completed in 1972, was an array processor with 64 processing elements that could all execute the same instruction on different data simultaneously — a pure hardware SIMD machine. The Thinking Machines <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Connection_Machine" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Connection_Machine" target="_self">Connection Machine</a> CM-2 (1987) pushed this to 65,536 single-bit processors, each executing the same instruction on its own bit of data. These machines were powerful but expensive and difficult to program. They were eventually displaced by cheaper clusters of commodity processors.<br>Era 2: Vector supercomputers (1970s–1990s). The <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Cray-1" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Cray-1" target="_self">Cray-1</a> (1976) and its successors used vector processing — operating on entire vectors (arrays) of data with single instructions. Vector processing is closely related to SIMD but is technically a separate concept (distinguished primarily by the presence of a "set vector length" instruction). Flynn's original taxonomy predates the Cray-1, which is why vector processing is not explicitly covered — Duncan's taxonomy (1990) addresses this gap.Era 3: SIMD extensions in commodity CPUs (1990s–present). The current era grew out of the desktop multimedia market. As PCs became powerful enough for real-time video and audio in the mid-1990s, Intel introduced MMX (1997) to accelerate multimedia processing on existing x86 CPUs. This was followed by SSE, AVX, and their successors, making SIMD capabilities ubiquitous in consumer hardware rather than confined to expensive supercomputers. Today, even a budget smartphone contains ARM processors with NEON SIMD extensions — data parallelism is no longer a luxury feature.SIMD limitations. SIMD is not a universal solution. It works best when the same operation must be applied to every element of a dataset — a condition called data parallelism or regularity. When different elements require different operations (e.g., an if-else branch where some elements take the if path and others take the else path), SIMD performance degrades significantly. The processor must either execute both paths and mask out the unwanted results (wasting half the computation) or fall back to scalar processing. This is why SIMD excels at uniform operations (add a constant to every pixel) but struggles with irregular workloads (process only pixels that exceed a threshold, with different logic for each).Another limitation is code complexity. Writing SIMD code by hand — using intrinsic functions that map directly to processor instructions — produces highly efficient but hard-to-read code that is tied to a specific instruction set extension. Modern compilers can auto-vectorize certain loops (automatically converting scalar code to SIMD), but this works reliably only for simple loop patterns. Complex data dependencies, pointer aliasing, and non-contiguous memory access patterns can prevent the compiler from vectorizing effectively. The result is that extracting maximum SIMD performance often requires programmer expertise and careful code structuring.Auto-vectorization in practice. Modern compilers (GCC, Clang, MSVC) can automatically convert scalar loops into SIMD code through a process called auto-vectorization. Consider a simple C loop:// Scalar C code
for (int i = 0; i &lt; 1024; i++) { C[i] = A[i] + B[i];
}
With optimization flags like -O3 or -ftree-vectorize, the compiler can recognize that each iteration is independent and generate SSE or AVX instructions that process 4, 8, or 16 elements per iteration instead of one. The programmer writes scalar code; the compiler emits SIMD code. This is one of the most impactful compiler optimizations in modern computing — it means that well-structured code can benefit from SIMD hardware without the programmer needing to write a single intrinsic function.However, auto-vectorization has limits. Loops with data dependencies between iterations (where iteration i+1 depends on the result of iteration i), loops with complex branching, or loops that access memory through pointers that might alias (point to the same location) often cannot be auto-vectorized. In performance-critical code, programmers sometimes use compiler-specific intrinsic functions (like _mm256_add_ps for AVX) or OpenMP SIMD pragmas (#pragma omp simd) to guarantee vectorization.GPUs as SIMD engines. Modern Graphics Processing Units (GPUs) take the SIMD concept to an extreme scale. A high-end GPU contains thousands of simple processing cores organized into groups that execute the same instruction across many data elements simultaneously. While the exact architecture varies — NVIDIA uses what it calls SIMT (Single Instruction, Multiple Threads), and AMD uses a SIMD+SIMT hybrid — the fundamental principle is the same: massive data parallelism through applying one operation to many data points at once.Consider the scale difference. A modern high-end CPU has 8–24 cores, each capable of executing AVX-512 instructions on 16 floats at a time — so roughly 128–384 floating-point operations per cycle. A modern high-end GPU like the NVIDIA H100 has over 16,000 CUDA cores and can perform tens of thousands of floating-point operations per cycle. The tradeoff is that each GPU core is far simpler than a CPU core — it cannot do complex branching or irregular memory access patterns efficiently. But for regular, data-parallel workloads, the GPU's massive parallelism wins decisively.This is why GPUs have become the dominant hardware for machine learning: training a neural network involves billions of multiply-accumulate operations on large matrices, a workload perfectly suited to SIMD-style execution. The rise of deep learning since 2012 has driven GPU development as much as gaming has — NVIDIA's market capitalization grew from roughly $10 billion in 2012 to over $1 trillion in 2024, largely on the strength of its data center GPU business.The CPU-GPU relationship illustrates a broader principle in modern computing: heterogeneous parallelism. Rather than building one type of processor that does everything, modern systems combine different types of processing units optimized for different types of parallelism. The CPU handles complex, branchy, sequential code (MIMD); the GPU handles massive, uniform, data-parallel workloads (SIMD); and specialized accelerators (like Google's TPUs or Apple's Neural Engine) handle specific operations (like matrix multiplication) even more efficiently. The programmer's challenge is to map each part of the computation to the most appropriate hardware.MIMD is the most general and most powerful of Flynn's four categories. In an MIMD system, multiple processing units execute different instructions on different data simultaneously. Each processor is essentially autonomous — it has its own instruction stream and its own data stream, and it can run a completely different program from every other processor.<br><img alt="MIMD architecture diagram" src="assets/l09-flynn-mimd.png" target="_self" style="width: 400px; max-width: 100%;"><br>Every modern multicore processor, every multi-CPU server, every compute cluster, and every distributed system is an MIMD architecture. When you open a web browser, a music player, and a code editor on your laptop simultaneously, and each runs on a different CPU core executing different code on different data, that is MIMD in action. This is the dominant paradigm for parallel computing today — as of the most recent <a data-tooltip-position="top" aria-label="https://www.top500.org/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.top500.org/" target="_self">TOP500</a> rankings, essentially all of the world's most powerful supercomputers are MIMD systems.The key difference between MIMD and SIMD is autonomy. In SIMD, all processing units march in lockstep — they execute the same instruction, just on different data. In MIMD, each processing unit is independent — it has its own program counter, its own instruction stream, and its own data. This makes MIMD far more flexible (any processor can do anything at any time) but also harder to program (coordinating independent processors requires explicit synchronization).MIMD architectures are divided into two major subcategories based on how processors access memory. This division is arguably more important than the SISD/SIMD/MISD/MIMD classification itself, because it determines how programmers write parallel software:SMP — Symmetric Multiprocessing (Shared Memory)In an SMP system, all processors share a single physical address space — they can all read from and write to the same memory. "Symmetric" means that every processor has equal access to memory and I/O devices; no processor is special. This is in contrast to asymmetric multiprocessing, where one processor acts as a master and the others as slaves — a design that was common in the 1980s and early 1990s but has been largely superseded by SMP. Examples: Every modern Intel Core and AMD Ryzen multicore CPU is an SMP system. A quad-core processor has four independent cores, each with its own L1 and L2 caches, sharing an L3 cache and main memory. From the programmer's perspective, all cores see the same memory. Server-class processors like the Intel Xeon or AMD EPYC can have 64, 96, or even 128 cores in a single socket, all operating as an SMP system. Advantages: Shared memory makes programming relatively straightforward. Threads in the same process can communicate by reading and writing shared variables. Operating systems handle thread scheduling across cores transparently. Languages like C, C++, Java, and Python provide threading libraries (pthreads, std::thread, java.lang.Thread) that map naturally onto SMP hardware. Challenges: The central challenge of SMP is cache coherence. When Core 0 writes a value to memory address X, that write first updates Core 0's L1 cache. But Core 1 might have a stale copy of X in its own L1 cache. If Core 1 reads X without knowing about Core 0's write, it gets stale data — a potentially catastrophic bug. Hardware protocols (such as MESI — Modified, Exclusive, Shared, Invalid) ensure that all caches stay consistent by tracking the state of every cache line across all cores and invalidating or updating copies when a write occurs. This coherence traffic limits scalability: every write to a shared variable potentially triggers invalidation messages to every other core's cache. Beyond roughly 64–128 cores, the overhead of keeping all caches synchronized becomes a bottleneck. This is why single shared-memory machines rarely exceed a few hundred cores. <br>
Programming model: Threads communicate through shared variables, protected by synchronization primitives (mutexes, semaphores, atomic operations). Tools like <a data-tooltip-position="top" aria-label="https://www.openmp.org/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.openmp.org/" target="_self">OpenMP</a> allow programmers to parallelize loops with simple compiler directives. MMP — Massively Parallel Processing (Distributed Memory)In an MMP system, each processor (or node) has its own private memory. Processors cannot directly access each other's memory — they communicate by sending messages over a high-speed interconnection network. This is fundamentally different from SMP: instead of reading a shared variable, a processor must explicitly send a message ("here is the data you need") to another processor and wait for a response.
<br>
Examples: Supercomputers like <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Frontier_(supercomputer)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Frontier_(supercomputer)" target="_self">Frontier</a> (the first exascale system, with over 9,400 nodes each containing an AMD EPYC CPU and four AMD Instinct GPUs), cloud computing clusters (thousands of servers connected via high-bandwidth Ethernet or InfiniBand), and distributed systems like Hadoop and Spark clusters. Even a university computer lab with 30 machines running a parallel job over a network is a small-scale MMP system. <br>
Advantages: Distributed memory scales far better than shared memory. Because there is no shared bus or cache coherence protocol, you can connect tens of thousands or even millions of processors. The <a data-tooltip-position="top" aria-label="https://www.olcf.ornl.gov/frontier/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.olcf.ornl.gov/frontier/" target="_self">Frontier supercomputer</a> at Oak Ridge National Laboratory achieves over 1 exaFLOP (10^18 floating-point operations per second) by connecting thousands of nodes, each with its own CPU and GPU. Challenges: Programming distributed memory systems is significantly harder than programming shared memory. Processors must explicitly send and receive messages to exchange data, and the programmer must manage data distribution, communication patterns, and load balancing. Network latency (the time for a message to travel between nodes) and bandwidth (the amount of data that can flow per second) become critical performance factors. A poorly designed communication pattern can easily negate all the benefits of parallelism — if processors spend most of their time waiting for messages rather than computing, the system is no faster than a single machine. Interconnection topologies: How processors are connected to each other matters enormously. Common topologies include mesh networks (processors arranged in a 2D or 3D grid, each connected to its neighbors), torus networks (a mesh with wrap-around connections at the edges), fat trees (a hierarchical tree of switches with increasing bandwidth toward the root), and hypercubes (each of 2^N processors connects to N others). The choice of topology affects both latency (how many hops a message must take) and bandwidth (how much data can flow simultaneously without congestion). <br>
Programming model: The dominant standard for scientific computing is <a data-tooltip-position="top" aria-label="https://www.mpi-forum.org/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.mpi-forum.org/" target="_self">MPI (Message Passing Interface)</a>, a library specification that provides functions like MPI_Send and MPI_Recv for explicit inter-process communication. Writing efficient MPI programs requires careful thought about how to partition data and minimize communication — the programmer must explicitly decide which data goes to which processor and when processors need to exchange information. Higher-level frameworks like MapReduce (and its implementations Hadoop and Spark) abstract some of this complexity for data-processing workloads by providing a simpler programming model: the programmer writes a "map" function (applied to each data element independently) and a "reduce" function (that combines results), and the framework handles data distribution, communication, and fault tolerance automatically. SPMD and MPMD — programming models within MIMD. Beyond the hardware distinction between shared and distributed memory, MIMD systems also vary in how programs are organized across processors. Two common patterns are: SPMD (Single Program, Multiple Data): All processors run the same program, but each operates on a different subset of the data and may be at a different point in the program at any given time. This is the most common model for scientific parallel computing. For example, a weather simulation might run the same fluid dynamics code on every processor, with each processor responsible for a different geographic region. SPMD should not be confused with SIMD — in SPMD, each processor executes independently (they are not in lockstep), and they may take different branches through the code depending on their local data. MPMD (Multiple Program, Multiple Data): Different processors run different programs. For example, in a client-server system, the server runs one program and clients run another. In a Unix pipeline like cat file.txt | grep "error" | sort | uniq, four separate programs run simultaneously on different data — that is MPMD. These are software-level patterns that run on MIMD hardware. Most large-scale parallel applications use SPMD because it simplifies development — you write one program and deploy it across thousands of processors, with each instance working on its local chunk of data.SMP vs. MMP — a comparison:Hybrid systems. In practice, modern high-performance systems combine both models. A supercomputer node typically contains an SMP multicore processor (shared memory within the node) connected to other nodes via a high-speed network (distributed memory between nodes). Programming such systems often involves a hybrid approach: OpenMP for parallelism within a node and MPI for communication between nodes. Furthermore, each node may contain GPUs that operate as SIMD engines.This means a single modern supercomputer simultaneously employs multiple levels of Flynn's taxonomy:
SIMD within each GPU core (and within each CPU core's AVX units) — one instruction, many data elements
SMP/MIMD within each CPU chip — multiple independent cores sharing memory
MMP/MIMD across nodes — thousands of independent machines exchanging messages
This is a layered hierarchy of parallelism that Flynn's 1966 taxonomy anticipated in its structure, even if the specific implementations were decades away. Understanding this hierarchy is essential for anyone who wants to write high-performance software — the programmer must decide which level of parallelism to exploit, and each level has different programming models, different performance characteristics, and different trade-offs.A practical example: training a large language model (like GPT or Llama) requires distributing the computation across thousands of GPUs. Within each GPU, SIMD-style execution handles the matrix multiplications. Across GPUs within a single server (typically 4–8 GPUs sharing a high-speed interconnect like NVLink), SMP-style shared memory parallelism coordinates the work. Across servers in a data center (hundreds or thousands of nodes connected via InfiniBand), MMP-style message passing synchronizes gradient updates. The programmer (or, more commonly, the deep learning framework like PyTorch or JAX) must manage all three levels simultaneously. This is the reality of modern parallel computing — and Flynn's taxonomy provides the vocabulary to describe and reason about it.The following table summarizes the four categories, their characteristics, and their relevance to modern computing:The table makes a key point clear: the four categories are not equally represented in modern hardware. SISD and MISD are primarily of historical and theoretical interest, while SIMD and MIMD dominate contemporary computing. However, understanding all four categories is essential for appreciating why SIMD and MIMD won — they provide the most useful forms of parallelism for the widest range of applications.Several observations emerge from this summary: SISD is the past. No modern general-purpose processor is purely SISD. Even the simplest current CPUs include SIMD extensions (NEON on ARM, SSE on x86), and most are multicore (MIMD). SISD remains important as a conceptual baseline — it is the von Neumann model against which all parallel architectures are measured. MISD is a curiosity. It has genuine applications in fault tolerance and certain specialized hardware (systolic arrays in TPUs), but it has never become a mainstream commercial architecture. You should know what it is, but you are unlikely to program one directly. SIMD is everywhere. Every time your phone processes a photo, your laptop plays a video, or a data center trains a machine learning model, SIMD hardware is doing the heavy lifting. The transition from scalar to SIMD computation is one of the most important performance optimizations in modern computing. MIMD is the foundation. All cloud computing, all multicore processors, all supercomputers, and all distributed systems are MIMD. The entire modern computing ecosystem — from your dual-core laptop to an exascale supercomputer — is built on the MIMD model. Real systems combine categories. A modern laptop with a multicore CPU and integrated GPU simultaneously uses SIMD (within each core's vector units and GPU shaders) and MIMD (across independent cores). A supercomputer adds distributed-memory MIMD across thousands of nodes. Flynn's categories are not mutually exclusive at the system level — they describe modes of execution that coexist within a single machine. Flynn's taxonomy endures because it asks the right question at the right level of abstraction: not "how fast is this processor?" or "how many transistors does it have?" but "how do instructions and data flow through the system?" That question remains as relevant today as it was in 1966.Looking at the trajectory of computing, the trend is unmistakable: from SISD (the single-core era, 1940s–2000s) to SIMD+MIMD (the multicore/GPU era, 2005–present), and toward increasingly heterogeneous systems that combine different types of processing units — CPU cores, GPU cores, neural accelerators, FPGA fabric — on a single chip. Apple's M-series processors, for example, contain general-purpose CPU cores (MIMD), a GPU (SIMD), a Neural Engine (SIMD-like), and dedicated media encoders/decoders — all on one piece of silicon. Understanding Flynn's taxonomy gives you the conceptual framework to make sense of this diversity, regardless of which specific products or architectures emerge in the coming years.Here is a timeline that puts Flynn's taxonomy in the context of computing history:Each era did not replace the previous one — it layered on top. Modern systems incorporate techniques from every era: pipelining and superscalar execution (from the SISD era), vector/SIMD units (from the supercomputer era), shared-memory multiprocessing (from the SMP era), and distributed computing (from the cluster era). Flynn's taxonomy provides the common language for discussing all of these techniques. RAM and RASP are foundational abstractions. The RAM machine (program separate from data, Harvard-style) and RASP machine (program stored in memory, von Neumann-style) provide the formal framework for analyzing algorithm complexity and understanding real computer designs. ISA is the contract between hardware and software. It defines data types, registers, addressing modes, instruction formats, and the instruction set itself. Everything above the ISA (compilers, OS) depends on this interface; everything below (microarchitecture, circuits) implements it. Operand location determines instruction format. Stack-based (zero-address), accumulator (one-address), register-memory (two-address), register-register/load-store (three-address), and memory-memory architectures each trade off code density, hardware complexity, and performance. CISC vs RISC is a design philosophy, not a binary. CISC provides powerful, variable-length instructions (x86); RISC provides simple, fixed-length instructions enabling efficient pipelining (ARM, RISC-V). Modern processors blur the line — x86 chips internally decode CISC instructions into RISC-like micro-operations. Flynn's taxonomy classifies parallelism along two axes. SISD (classical sequential), SIMD (vector/GPU operations), MISD (rare, fault-tolerant), and MIMD (multicore/distributed) describe how instruction and data streams interact. Modern systems are predominantly MIMD with SIMD extensions. What comes next:
Lecture 10 surveys modern specialized architectures: GPUs, DSPs, ASICs, FPGAs, TPUs, neuromorphic processors, and an introduction to quantum computing concepts.
Lab work will continue applying x86 assembly concepts from earlier lectures.
Think about:
How does the CISC/RISC distinction affect the assembly code you have been writing in labs?
Where does your laptop's processor fall in Flynn's taxonomy? (Hint: it is MIMD with SIMD extensions.)
Why did ARM's RISC approach win the mobile market while x86's CISC approach dominates desktops? <br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Random-access_machine" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Random-access_machine" target="_self">Random-access machine — Wikipedia</a> — Formal definition, instruction sets, and relationship to other computational models
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Random-access_stored-program_machine" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Random-access_stored-program_machine" target="_self">Random-access stored-program machine — Wikipedia</a> — RASP model definition and comparison with RAM
<br><a data-tooltip-position="top" aria-label="https://uk.wikipedia.org/wiki/%D0%90%D1%80%D1%85%D1%96%D1%82%D0%B5%D0%BA%D1%82%D1%83%D1%80%D0%B0_%D1%81%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D0%B8_%D0%BA%D0%BE%D0%BC%D0%B0%D0%BD%D0%B4" rel="noopener nofollow" class="external-link is-unresolved" href="https://uk.wikipedia.org/wiki/%D0%90%D1%80%D1%85%D1%96%D1%82%D0%B5%D0%BA%D1%82%D1%83%D1%80%D0%B0_%D1%81%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D0%B8_%D0%BA%D0%BE%D0%BC%D0%B0%D0%BD%D0%B4" target="_self">Instruction set architecture — Wikipedia</a> — Comprehensive ISA overview (Ukrainian)
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Complex_instruction_set_computer" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Complex_instruction_set_computer" target="_self">Complex instruction set computer — Wikipedia</a> — CISC history, characteristics, and examples
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Reduced_instruction_set_computer" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Reduced_instruction_set_computer" target="_self">Reduced instruction set computer — Wikipedia</a> — RISC philosophy, design principles, and implementations
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Flynn%27s_taxonomy" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Flynn%27s_taxonomy" target="_self">Flynn's taxonomy — Wikipedia</a> — Classification system for parallel architectures
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/List_of_products_using_ARM_processors" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/List_of_products_using_ARM_processors" target="_self">List of products using ARM processors — Wikipedia</a> — Comprehensive list of ARM-based devices
]]></description><link>lectures/eng/l09-eng.-isa-design-and-models-of-computation.html</link><guid isPermaLink="false">Lectures/ENG/L09-ENG. ISA Design and Models of Computation.md</guid><pubDate>Tue, 10 Mar 2026 08:25:10 GMT</pubDate><enclosure url="assets/l09-lecture-outline.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l09-lecture-outline.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L09-VLIW-Instruction-Word-UK]]></title><description><![CDATA[<img src="assets/l09-vliw-instruction-word-uk.svg" target="_self">]]></description><link>assets/l09-vliw-instruction-word-uk.html</link><guid isPermaLink="false">assets/L09-VLIW-Instruction-Word-UK.svg</guid><pubDate>Tue, 10 Mar 2026 08:20:37 GMT</pubDate><enclosure url="assets/l09-vliw-instruction-word-uk.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l09-vliw-instruction-word-uk.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L09-Memory-Memory-Architecture]]></title><description><![CDATA[<img src="assets/l09-memory-memory-architecture.svg" target="_self">]]></description><link>assets/l09-memory-memory-architecture.html</link><guid isPermaLink="false">assets/L09-Memory-Memory-Architecture.svg</guid><pubDate>Tue, 10 Mar 2026 08:17:20 GMT</pubDate><enclosure url="assets/l09-memory-memory-architecture.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l09-memory-memory-architecture.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L09-Memory-Memory-Architecture-UK]]></title><description><![CDATA[<img src="assets/l09-memory-memory-architecture-uk.svg" target="_self">]]></description><link>assets/l09-memory-memory-architecture-uk.html</link><guid isPermaLink="false">assets/L09-Memory-Memory-Architecture-UK.svg</guid><pubDate>Tue, 10 Mar 2026 08:17:14 GMT</pubDate><enclosure url="assets/l09-memory-memory-architecture-uk.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l09-memory-memory-architecture-uk.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L09-CISC-RISC-Convergence-UK]]></title><description><![CDATA[<img src="assets/l09-cisc-risc-convergence-uk.svg" target="_self">]]></description><link>assets/l09-cisc-risc-convergence-uk.html</link><guid isPermaLink="false">assets/L09-CISC-RISC-Convergence-UK.svg</guid><pubDate>Tue, 10 Mar 2026 08:14:39 GMT</pubDate><enclosure url="assets/l09-cisc-risc-convergence-uk.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l09-cisc-risc-convergence-uk.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L09-CISC-RISC-Convergence]]></title><description><![CDATA[<img src="assets/l09-cisc-risc-convergence.svg" target="_self">]]></description><link>assets/l09-cisc-risc-convergence.html</link><guid isPermaLink="false">assets/L09-CISC-RISC-Convergence.svg</guid><pubDate>Tue, 10 Mar 2026 08:12:06 GMT</pubDate><enclosure url="assets/l09-cisc-risc-convergence.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l09-cisc-risc-convergence.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L09-VLIW-Instruction-Word]]></title><description><![CDATA[<img src="assets/l09-vliw-instruction-word.svg" target="_self">]]></description><link>assets/l09-vliw-instruction-word.html</link><guid isPermaLink="false">assets/L09-VLIW-Instruction-Word.svg</guid><pubDate>Tue, 10 Mar 2026 08:10:50 GMT</pubDate><enclosure url="assets/l09-vliw-instruction-word.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l09-vliw-instruction-word.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L09-ISA-Classification-Axes-UK]]></title><description><![CDATA[<img src="assets/l09-isa-classification-axes-uk.svg" target="_self">]]></description><link>assets/l09-isa-classification-axes-uk.html</link><guid isPermaLink="false">assets/L09-ISA-Classification-Axes-UK.svg</guid><pubDate>Tue, 10 Mar 2026 07:47:49 GMT</pubDate><enclosure url="assets/l09-isa-classification-axes-uk.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l09-isa-classification-axes-uk.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L09-ISA-Implementations-UK]]></title><description><![CDATA[<img src="assets/l09-isa-implementations-uk.svg" target="_self">]]></description><link>assets/l09-isa-implementations-uk.html</link><guid isPermaLink="false">assets/L09-ISA-Implementations-UK.svg</guid><pubDate>Tue, 10 Mar 2026 07:47:22 GMT</pubDate><enclosure url="assets/l09-isa-implementations-uk.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l09-isa-implementations-uk.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L09-ISA-Boundary-UK]]></title><description><![CDATA[<img src="assets/l09-isa-boundary-uk.svg" target="_self">]]></description><link>assets/l09-isa-boundary-uk.html</link><guid isPermaLink="false">assets/L09-ISA-Boundary-UK.svg</guid><pubDate>Tue, 10 Mar 2026 07:46:52 GMT</pubDate><enclosure url="assets/l09-isa-boundary-uk.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l09-isa-boundary-uk.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L09-ISA-Classification-Axes]]></title><description><![CDATA[<img src="assets/l09-isa-classification-axes.svg" target="_self">]]></description><link>assets/l09-isa-classification-axes.html</link><guid isPermaLink="false">assets/L09-ISA-Classification-Axes.svg</guid><pubDate>Tue, 10 Mar 2026 07:44:48 GMT</pubDate><enclosure url="assets/l09-isa-classification-axes.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l09-isa-classification-axes.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L09-ISA-Implementations]]></title><description><![CDATA[<img src="assets/l09-isa-implementations.svg" target="_self">]]></description><link>assets/l09-isa-implementations.html</link><guid isPermaLink="false">assets/L09-ISA-Implementations.svg</guid><pubDate>Tue, 10 Mar 2026 07:41:22 GMT</pubDate><enclosure url="assets/l09-isa-implementations.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l09-isa-implementations.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L09-ISA-Boundary]]></title><description><![CDATA[<img src="assets/l09-isa-boundary.svg" target="_self">]]></description><link>assets/l09-isa-boundary.html</link><guid isPermaLink="false">assets/L09-ISA-Boundary.svg</guid><pubDate>Tue, 10 Mar 2026 07:39:12 GMT</pubDate><enclosure url="assets/l09-isa-boundary.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l09-isa-boundary.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L09-RAM-vs-RASP-UK]]></title><description><![CDATA[<img src="assets/l09-ram-vs-rasp-uk.svg" target="_self">]]></description><link>assets/l09-ram-vs-rasp-uk.html</link><guid isPermaLink="false">assets/L09-RAM-vs-RASP-UK.svg</guid><pubDate>Tue, 10 Mar 2026 07:28:01 GMT</pubDate><enclosure url="assets/l09-ram-vs-rasp-uk.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l09-ram-vs-rasp-uk.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L09-RAM-vs-RASP]]></title><description><![CDATA[<img src="assets/l09-ram-vs-rasp.svg" target="_self">]]></description><link>assets/l09-ram-vs-rasp.html</link><guid isPermaLink="false">assets/L09-RAM-vs-RASP.svg</guid><pubDate>Tue, 10 Mar 2026 07:26:39 GMT</pubDate><enclosure url="assets/l09-ram-vs-rasp.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l09-ram-vs-rasp.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L09-Direct-vs-Indirect-Addressing-UK]]></title><description><![CDATA[<img src="assets/l09-direct-vs-indirect-addressing-uk.svg" target="_self">]]></description><link>assets/l09-direct-vs-indirect-addressing-uk.html</link><guid isPermaLink="false">assets/L09-Direct-vs-Indirect-Addressing-UK.svg</guid><pubDate>Tue, 10 Mar 2026 07:16:01 GMT</pubDate><enclosure url="assets/l09-direct-vs-indirect-addressing-uk.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l09-direct-vs-indirect-addressing-uk.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L09-Direct-vs-Indirect-Addressing]]></title><description><![CDATA[<img src="assets/l09-direct-vs-indirect-addressing.svg" target="_self">]]></description><link>assets/l09-direct-vs-indirect-addressing.html</link><guid isPermaLink="false">assets/L09-Direct-vs-Indirect-Addressing.svg</guid><pubDate>Tue, 10 Mar 2026 07:15:02 GMT</pubDate><enclosure url="assets/l09-direct-vs-indirect-addressing.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l09-direct-vs-indirect-addressing.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L09-Counter-Machine-ISA-UK]]></title><description><![CDATA[<img src="assets/l09-counter-machine-isa-uk.svg" target="_self">]]></description><link>assets/l09-counter-machine-isa-uk.html</link><guid isPermaLink="false">assets/L09-Counter-Machine-ISA-UK.svg</guid><pubDate>Tue, 10 Mar 2026 06:55:18 GMT</pubDate><enclosure url="assets/l09-counter-machine-isa-uk.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l09-counter-machine-isa-uk.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L09-Counter-Machine-ISA]]></title><description><![CDATA[<img src="assets/l09-counter-machine-isa.svg" target="_self">]]></description><link>assets/l09-counter-machine-isa.html</link><guid isPermaLink="false">assets/L09-Counter-Machine-ISA.svg</guid><pubDate>Tue, 10 Mar 2026 06:54:25 GMT</pubDate><enclosure url="assets/l09-counter-machine-isa.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l09-counter-machine-isa.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L09-Lecture-Outline-UK]]></title><description><![CDATA[<img src="assets/l09-lecture-outline-uk.svg" target="_self">]]></description><link>assets/l09-lecture-outline-uk.html</link><guid isPermaLink="false">assets/L09-Lecture-Outline-UK.svg</guid><pubDate>Tue, 10 Mar 2026 06:49:28 GMT</pubDate><enclosure url="assets/l09-lecture-outline-uk.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l09-lecture-outline-uk.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L09-Lecture-Outline]]></title><description><![CDATA[<img src="assets/l09-lecture-outline.svg" target="_self">]]></description><link>assets/l09-lecture-outline.html</link><guid isPermaLink="false">assets/L09-Lecture-Outline.svg</guid><pubDate>Tue, 10 Mar 2026 06:48:08 GMT</pubDate><enclosure url="assets/l09-lecture-outline.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l09-lecture-outline.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L09-Turing-Machine-UK]]></title><description><![CDATA[<img src="assets/l09-turing-machine-uk.svg" target="_self">]]></description><link>assets/l09-turing-machine-uk.html</link><guid isPermaLink="false">assets/L09-Turing-Machine-UK.svg</guid><pubDate>Tue, 10 Mar 2026 06:26:19 GMT</pubDate><enclosure url="assets/l09-turing-machine-uk.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l09-turing-machine-uk.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L09-Turing-Machine]]></title><description><![CDATA[<img src="assets/l09-turing-machine.svg" target="_self">]]></description><link>assets/l09-turing-machine.html</link><guid isPermaLink="false">assets/L09-Turing-Machine.svg</guid><pubDate>Tue, 10 Mar 2026 06:25:02 GMT</pubDate><enclosure url="assets/l09-turing-machine.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l09-turing-machine.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L09-Models-of-Computation-Progression-UK]]></title><description><![CDATA[<img src="assets/l09-models-of-computation-progression-uk.svg" target="_self">]]></description><link>assets/l09-models-of-computation-progression-uk.html</link><guid isPermaLink="false">assets/L09-Models-of-Computation-Progression-UK.svg</guid><pubDate>Tue, 10 Mar 2026 06:17:18 GMT</pubDate><enclosure url="assets/l09-models-of-computation-progression-uk.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l09-models-of-computation-progression-uk.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L09-Models-of-Computation-Progression]]></title><description><![CDATA[<img src="assets/l09-models-of-computation-progression.svg" target="_self">]]></description><link>assets/l09-models-of-computation-progression.html</link><guid isPermaLink="false">assets/L09-Models-of-Computation-Progression.svg</guid><pubDate>Tue, 10 Mar 2026 06:15:23 GMT</pubDate><enclosure url="assets/l09-models-of-computation-progression.svg" length="0" type="image/svg+xml"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l09-models-of-computation-progression.svg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L08-NOTES. Пристрої. Ієрархія пам'яті. Структури даних на низькому рівні]]></title><description><![CDATA[Короткий довідник до лекції 8. Повний матеріал у файлі L08. Пристрої. Ієрархія пам'яті. Структури даних на низькому рівні.md.
Частина 1 (Розділи 2-6): Пристрої введення-виведення — шини, контролер клавіатури, інструкції IN/OUT, скан-коди, карта портів
Частина 2 (Розділи 7-11): Ієрархія пам'яті — рівні кеша, компроміси швидкості та вартості, типи пам'яті, віртуальна пам'ять
Частина 3 (Розділи 12-15): Структури даних на низькому рівні — масиви, структури, зв'язані списки, масиви рядків у асемблері x86
Три рівні доступу до клавіатури: DOS (INT 21h) -&gt; BIOS (INT 16h) -&gt; Апаратний рівень (IN AL, 60h)
Лінія керування IO/M розрізняє доступ до введення-виведення від доступу до пам'яті на тій самій фізичній шині.Дізнатися більше:
<a data-tooltip-position="top" aria-label="https://toshiba.semicon-storage.com/ap-en/semiconductor/knowledge/e-learning/micro-intro/chapter2/types-of-bus-lines.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://toshiba.semicon-storage.com/ap-en/semiconductor/knowledge/e-learning/micro-intro/chapter2/types-of-bus-lines.html" target="_self">Типи ліній шини</a>
Два порти введення-виведення, чотири логічні регістри (напрямок визначає який):Біти регістра стану (порт 64h):Шлях натиснення клавіші: Фізична клавіша -&gt; сканування матриці -&gt; усунення брязкоту -&gt; скан-код -&gt; послідовна передача -&gt; вихідний буфер 8042 (порт 60h) -&gt; встановлення прапорця OBF -&gt; IRQ1 -&gt; обробник INT 09h читає порт 60h -&gt; перетворює в ASCII -&gt; буфер клавіатури BIOS -&gt; INT 16h повертає результат.Дізнатися більше:
<br><a data-tooltip-position="top" aria-label="https://www.basicinputoutput.com/2024/11/the-keyboard-controller-interface.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.basicinputoutput.com/2024/11/the-keyboard-controller-interface.html" target="_self">Інтерфейс контролера клавіатури, 0x60/0x64</a>
<br><a data-tooltip-position="top" aria-label="https://deeprajbhujel.blogspot.com/2015/12/keyboard-and-display-controller.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://deeprajbhujel.blogspot.com/2015/12/keyboard-and-display-controller.html" target="_self">Контролер клавіатури та дисплея 8279</a>
; IN — читання з порту введення-виведення в акумулятор
in al, 60h ; Безпосередній порт (0-255) -&gt; AL
in ax, 40h ; Безпосередній порт -&gt; AX (слово)
mov dx, 03F8h
in al, dx ; Непряма адресація через регістр (повний діапазон 64K) ; OUT — запис акумулятора в порт введення-виведення
out 61h, al ; Безпосередній порт
mov dx, 03F8h
out dx, al ; Непряма адресація через регістр Можна використовувати лише AL/AX (акумулятор) та DX (регістр порту)
IN/OUT не впливають на FLAGS
Читання з портів введення-виведення обходить кеш — завжди звертається безпосередньо до пристрою
Шаблон опитування (polling):read_loop: in al, 64h ; Читання регістра стану test al, 01h ; Біт 0: вихідний буфер повний? jz read_loop ; Немає даних — продовжуємо опитування in al, 60h ; Читання скан-коду
Дізнатися більше:
<br><a data-tooltip-position="top" aria-label="https://www.plantation-productions.com/Webster/www.artofasm.com/DOS/pdf/apndxc.pdf" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.plantation-productions.com/Webster/www.artofasm.com/DOS/pdf/apndxc.pdf" target="_self">Art of Assembly — Додаток C (скан-коди, PDF)</a> Make-код = клавішу натиснуто (напр., A = 1Eh)
Break-код = клавішу відпущено = make-код + 80h (напр., відпускання A = 9Eh)
Розширені клавіші використовують префікс E0h (напр., стрілка вправо = E0h 4Dh)
Основні команди клавіатури (записуються в порт 60h):; Приклад: Увімкнення всіх 3 LED
wait_cmd: in al, 64h test al, 02h ; Чекаємо IBF = 0 jnz wait_cmd mov al, 0EDh ; Команда "Встановити LED" out 60h, al
wait_data: in al, 64h test al, 02h jnz wait_data mov al, 00000111b ; Усі 3 LED увімкнені out 60h, al
Дізнатися більше:
<br><a data-tooltip-position="top" aria-label="https://wiki.osdev.org/PS/2_Keyboard#Scan_Code_Set_1" rel="noopener nofollow" class="external-link is-unresolved" href="https://wiki.osdev.org/PS/2_Keyboard#Scan_Code_Set_1" target="_self">Набір скан-кодів 1 клавіатури PS/2</a>
<br><a data-tooltip-position="top" aria-label="https://wiki.osdev.org/%228042%22_PS/2_Controller" rel="noopener nofollow" class="external-link is-unresolved" href="https://wiki.osdev.org/%228042%22_PS/2_Controller" target="_self">PS/2-контролер «8042»</a>
CMOS використовує протокол індекс-дані: запис номера регістра в 70h, читання/запис значення в 71h.Стіна пам'яті (memory wall): швидкість ЦП зростала ~60%/рік, затримка DRAM покращувалась лише ~7%/рік. Розрив вирішується ієрархією.Локальність звернень:
Часова — нещодавно використані дані будуть використані знову найближчим часом (змінні циклу)
Просторова — сусідні адреси будуть використані найближчим часом (ітерація по масиву)
Кожен рівень діє як кеш для рівня нижче. Регістри -&gt; L1 -&gt; L2 -&gt; L3 керуються апаратно; RAM &lt;-&gt; диск керується ОС (віртуальна пам'ять).Дізнатися більше:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Memory_hierarchy" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Memory_hierarchy" target="_self">Ієрархія пам'яті (Wikipedia)</a> Рядок кеша = 64 байти на x86. Читання 1 байта завантажує весь 64-байтовий блок (використовує просторову локальність).
Політики включення: Інклюзивна (дані L1 дублюються в L2), Ексклюзивна (без перетину), Неінклюзивна (без суворих правил).
Протокол когерентності MESI — стани: Modified, Exclusive, Shared, Invalid.
Середній час доступу (AAT):AAT = hit_time + (miss_rate x miss_penalty)
Приклад (промах L1=10%, промах L2=1%, промах L3=1%, RAM=50 нс):
Без кеша: 50 нс
Лише L1: 1 + 0.10 x 50 = 6 нс
L1+L2: 1 + 0.10 x 5.5 = 1.55 нс
L1+L2+L3: ~1.51 нс (покращення у 33 рази)
Типи промахів: Холодний (перший доступ), Ємнісний (кеш повний), Конфліктний (колізія адрес).Дізнатися більше:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Cache_hierarchy" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Cache_hierarchy" target="_self">Ієрархія кеша (Wikipedia)</a>
<br><a data-tooltip-position="top" aria-label="https://hazelcast.com/foundations/caching/caching/" rel="noopener nofollow" class="external-link is-unresolved" href="https://hazelcast.com/foundations/caching/caching/" target="_self">Що таке кешування? (Hazelcast)</a>
Ключове розуміння: Жодна технологія не перемагає за всіма показниками. Ієрархія поєднує технології, кожна з яких має подібну вартість, але експоненційно різний розмір і швидкість.Дізнатися більше:
<br><a data-tooltip-position="top" aria-label="https://www.enterprisestorageforum.com/hardware/types-of-computer-memory/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.enterprisestorageforum.com/hardware/types-of-computer-memory/" target="_self">9 типів комп'ютерної пам'яті</a>
<br><a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/computer-memory/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/computer-memory/" target="_self">Комп'ютерна пам'ять (GeeksforGeeks)</a>
Дізнатися більше:
<br><a data-tooltip-position="top" aria-label="https://www.computerlanguage.com/results.php?definition=RAM+cache" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.computerlanguage.com/results.php?definition=RAM+cache" target="_self">RAM Cache (Computer Language Co.)</a>
Віртуальна пам'ять надає кожному процесу власний адресний простір і дозволяє загальному обсягу віртуальних адрес перевищувати фізичну оперативну пам'ять.Ключові концепції:
Віртуальна адреса — адреса, яку використовує програма
Фізична адреса — фактичне розташування в оперативній пам'яті
Сторінка — блок фіксованого розміру (зазвичай 4 КБ); віртуальна сторінка відображається на фізичний фрейм
Таблиця сторінок — таблиця відповідностей для кожного процесу: номер віртуальної сторінки -&gt; номер фізичного фрейму
MMU (блок керування пам'яттю) — апаратний блок, який транслює віртуальну адресу у фізичну при кожному зверненні
Кроки трансляції адрес:
ЦП видає віртуальну адресу
MMU розділяє її на номер віртуальної сторінки + зміщення в межах сторінки
MMU шукає номер віртуальної сторінки в таблиці сторінок
Якщо присутня: поєднання номера фізичного фрейму + зміщення = фізична адреса (влучання)
Якщо відсутня: сторінкова помилка — ОС завантажує сторінку з диска в оперативну пам'ять, оновлює таблицю сторінок, повторює спробу
TLB (буфер асоціативної трансляції): Апаратний кеш нещодавніх записів таблиці сторінок. Коефіцієнт влучань &gt;99% у типових робочих навантаженнях. Промах TLB вимагає обходу таблиці сторінок (повільно).Обробка сторінкової помилки: ОС обирає сторінку-жертву (LRU), записує її на диск, якщо вона змінена, завантажує запитану сторінку, оновлює таблицю сторінок, перезапускає інструкцію, що спричинила помилку.Дізнатися більше:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Virtual_memory" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Virtual_memory" target="_self">Віртуальна пам'ять (Wikipedia)</a>
.data byteArr DB 10, 20, 30, 40, 50 ; Масив з 5 байтів wordArr DW 100, 200, 300 ; Масив з 3 слів
Формула адреси: адреса = база + індекс x розмір_елемента mov al, byteArr[2] ; AL = 30 (байт: індекс * 1) mov ax, wordArr[4] ; AX = 300 (слово: індекс 2 * 2 = зміщення 4)
Формула адреси: адреса = база + (рядок x СТОВПЦІ + стовпець) x розмір_елемента.data matrix DB 10,20,30,40 ; Рядок 0 (СТОВПЦІ=4) DB 50,60,70,80 ; Рядок 1 DB 90,100,110,120 ; Рядок 2 ; Доступ до matrix[1][2] -&gt; значення 70: mov bx, offset matrix mov al, 1 ; рядок mov cl, 4 ; СТОВПЦІ mul cl ; AX = рядок * СТОВПЦІ = 4 add bx, ax ; пропустити до рядка 1 add bx, 2 ; стовпець 2 mov dl, [bx] ; DL = 70
Для елементів розміром у слово додайте SHL AX, 1 після обчислення лінійного індексу.Дізнатися більше:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Row-_and_column-major_order" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Row-_and_column-major_order" target="_self">Рядковий та стовпцевий порядок (Wikipedia)</a>
Визначення шаблону (пам'ять не виділяється):Person STRUC age DB ? ; Зміщення 0: 1 байт height DW ? ; Зміщення 1: 2 байти
Person ENDS ; Разом: 3 байти
Створення екземплярів (виділяє пам'ять):.data person1 Person &lt;25, 180&gt; person2 Person &lt;30, 170&gt;
Доступ до полів: mov person1.age, 20 ; Прямий доступ: [person1+0] mov person1.height, 180 ; Прямий доступ: [person1+1] mov bx, offset person1 mov al, [bx].age ; Через вказівник у регістрі mov dx, [bx].height
Ключове розуміння: STRUC/ENDS = шаблон (формочка, не генерує байтів). Оголошення екземпляра = фактична пам'ять. Використовуйте SIZE Person для розміру в байтах, OFFSET Person.height для зміщення поля.У 16-бітному TASM вирівнювання відсутнє — поля розміщуються точно в порядку оголошення (на відміну від 32/64-бітних компіляторів C).Визначення вузла (структура з вкладеною структурою + вказівник):Node STRUC value Person &lt;&gt; ; 3 байти (вкладена структура) next DW ? ; 2-байтовий вказівник на наступний вузол
Node ENDS ; Разом: 5 байтів
Виділення пам'яті та зв'язування:.data head DB SIZE Node DUP(0) node2 DB SIZE Node DUP(0) node3 DB SIZE Node DUP(0) .code mov head.value.age, 25 mov head.next, OFFSET node2 mov node3.next, -1 ; FFFFh = NULL-маркер
Шаблон обходу: mov bx, OFFSET head
traverse_list: mov al, [bx].value.age ; Обробка поточного вузла add al, 1 mov [bx].value.age, al mov bx, [bx].next ; Перехід за вказівником cmp bx, -1 ; Перевірка на NULL jne traverse_list
Типові помилки:
Розіменування NULL — завжди перевіряйте cmp bx, -1 перед зверненням до полів
Перезапис вказівника next до його зчитування — спершу зчитайте [bx].next, потім модифікуйте вузол
Дізнатися більше:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Linked_list" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Linked_list" target="_self">Зв'язаний список (Wikipedia)</a>
Проблема: Рядки мають змінну довжину — не можна використати плоский масив.
Рішення: Масив вказівників розміром у слово на рядки (непряма адресація)..data STR_COUNT EQU 5 strArray DW STR_COUNT DUP(0) ; 5 вказівників розміром у слово str1 DB "Hello, World!$" str2 DB "Assembly Language$" ; ... інші рядки ...
Ініціалізація вказівників: mov strArray[0], OFFSET str1 ; strArray[0] -&gt; str1 mov strArray[2], OFFSET str2 ; strArray[1] (зміщення 2 = індекс*2) ; ...
Цикл друку: mov cx, STR_COUNT xor si, si
print_loop: mov dx, strArray[si] ; Завантаження вказівника mov ah, 09h int 21h ; Друк $-термінованого рядка add si, 2 ; Наступний вказівник (розмір слова) loop print_loop
Цей шаблон зустрічається скрізь: argv[] у C, блоки змінних середовища, таблиці рядків ELF, віртуальні таблиці C++. Ключова ідея: непряма адресація (вказівники фіксованого розміру на дані змінного розміру).Дізнатися більше:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Virtual_method_table" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Virtual_method_table" target="_self">Таблиця віртуальних методів (Wikipedia)</a> На x86 введення-виведення є портовим. ЦП використовує IN/OUT з окремим адресним простором введення-виведення розміром 64K. Кожен пристрій має призначені діапазони портів.
Контролер клавіатури — приклад апаратного інтерфейсу. Порти 60h (дані) та 64h (команди/стан) забезпечують двонапрямний зв'язок — низькорівневий інтерфейс, який INT 16h абстрагує.
Ієрархія пам'яті обмінює швидкість на ємність. Регістри (&lt; 1 нс) -&gt; кеш L1/L2/L3 (1-10 нс) -&gt; RAM (50-100 нс) -&gt; SSD (100 мкс) -&gt; HDD (10 мс) -&gt; стрічка (секунди).
Типи пам'яті відрізняються за кількома осями. Енергозалежність, тип доступу (довільний/послідовний/блоковий), технологія (SRAM/DRAM/флеш/магнітна).
Віртуальна пам'ять забезпечує ізоляцію та гнучкість. MMU транслює віртуальні адреси у фізичні через таблиці сторінок. TLB кешує трансляції з коефіцієнтом влучань &gt;99%.
Високорівневі структури даних безпосередньо відображаються на пам'ять. Структури визначають зміщення полів; зв'язані списки використовують вказівники для з'єднання вузлів; масиви рядків використовують масиви вказівників для даних змінної довжини. Працюйте безпосередньо з відеопам'яттю за сегментом B800h у текстовому режимі
Записуйте символи + байти атрибутів напряму (без використання INT 10h)
Формула позиції на екрані: зміщення = (рядок x 80 + стовпець) x 2
Відеопам'ять — це введення-виведення через пам'ять (використовуйте MOV, а не IN/OUT) — на відміну від портового доступу до клавіатури
Двовимірний екран (80 x 25) використовує ту саму адресацію в рядковому порядку з розділу 12
]]></description><link>lectures/l08-notes.-пристрої.-ієрархія-пам'яті.-структури-даних-на-низькому-рівні.html</link><guid isPermaLink="false">Lectures/L08-NOTES. Пристрої. Ієрархія пам'яті. Структури даних на низькому рівні.md</guid><pubDate>Tue, 10 Mar 2026 06:01:50 GMT</pubDate></item><item><title><![CDATA[L08. Пристрої. Ієрархія пам'яті. Структури даних на низькому рівні]]></title><description><![CDATA[
Лекція 8 курсу «Архітектура комп'ютерних систем» в НаУКМА.
Після цієї лекції ви зможете:
Пояснити, як процесор взаємодіє з пристроями введення-виведення через системну шину та виділені порти введення-виведення
Описати архітектуру контролера клавіатури (порти 0x60/0x64) та використовувати інструкції IN/OUT для взаємодії з апаратними пристроями
Описати ієрархію пам'яті від регістрів через рівні кеш-пам'яті до основної пам'яті, вторинних накопичувачів і стрічкових сховищ
Порівнювати технології пам'яті (SRAM, DRAM, флеш-пам'ять, ROM) за швидкістю, вартістю, енергозалежністю та типом доступу
Пояснити концепцію віртуальної пам'яті, сторінкування та трансляції адрес через таблиці сторінок
Реалізувати фундаментальні структури даних у x86-асемблері: масиви, двовимірні масиви, структури, зв'язані списки та масиви рядків
Протягом останніх п'яти лекцій ви працювали всередині процесора. Ви навчилися завантажувати значення в регістри й адресувати пам'ять за допомогою пар сегмент:зміщення (L03), маніпулювати даними за допомогою арифметичних та логічних інструкцій (L04), організовувати програми зі стеком і процедурами (L05), досліджувати системну архітектуру навколо процесора (L06) та взаємодіяти з операційною системою через програмні переривання (L07). Попутно ви створювали реальні програми, які зчитували введення з клавіатури, виводили текст на екран, виконували рекурсивні обчислення та обробляли виклики кількох функцій із належною дисципліною стека — все це у 16-бітному асемблері реального режиму.Але між вашими асемблерними інструкціями та фізичним світом існує величезний рівень складності, який ми ще не досліджували.Подумайте, що насправді відбувається, коли ви натискаєте клавішу в DOSBox. Ваша програма викликає INT 16h і отримує скан-код в AH та ASCII-символ в AL. Це програмна абстракція — чиста й проста. На апаратному рівні відбувається дещо значно цікавіше. Фізична клавіша замикає коло на матричній сітці всередині клавіатури. Спеціалізований мікроконтролер (не основний процесор — окремий процесор всередині самої клавіатури) сканує цю матрицю, виявляє зміну, чекає, поки електричний сигнал перестане «брязкати», перетворює позицію рядок-стовпець на числовий скан-код і передає цей код по послідовному каналу до іншого контролера на материнській платі. Цей контролер материнської плати зберігає код в однобайтовому буфері й подає електричний сигнал переривання на лінії IRQ1. Процесор призупиняє вашу програму, переходить до обробника BIOS за адресою, що зберігається в записі IVT 09h, зчитує скан-код з порту введення-виведення 60h за допомогою інструкції IN, перетворює його на ASCII-символ за допомогою таблиць перекодування та поточного стану Shift/Lock, записує результат у 16-елементний циклічний буфер в області даних BIOS, надсилає сигнал завершення переривання контролеру переривань і повертається через IRET — все це прозоро, ще до того як ваш виклик INT 16h поверне результат. Ваша програма жодного разу не торкнулася цього апаратного забезпечення безпосередньо. BIOS зробив це за вас, спілкуючись із контролером клавіатури через так звані порти введення-виведення.Або розгляньте пам'ять. Коли ви записуєте значення за допомогою MOV [BX], AX, ви думаєте про це як про один крок — значення потрапляє «в пам'ять». Але реальність значно складніша. На сучасному процесорі цей запис може спочатку потрапити в кеш першого рівня (крихітний, надзвичайно швидкий SRAM-буфер, що зберігає, можливо, 32 КБ нещодавно використаних даних), і контролер кеша повинен з часом поширити дані в кеш другого рівня, потім у кеш третього рівня і, нарешті, в основну DRAM. Кожен рівень більший, але повільніший за попередній. Сама DRAM — це не простий масив тригерів; вона організована в рядки й стовпці конденсаторів, які необхідно періодично оновлювати (перезаряджати) кожні 64 мілісекунди, щоб уникнути втрати вмісту. А якби дані зберігалися на жорсткому диску замість оперативної пам'яті, доступ був би приблизно в мільйон разів повільнішим.Це не езотеричні деталі. Ієрархія пам'яті безпосередньо впливає на продуктивність програм. Програма, яка звертається до даних за шаблонами, дружніми до кеша, може працювати в 10-100 разів швидше за ту, що постійно спричиняє промахи кеша. Щоб зрозуміти чому, потрібно знати, як працює кеш, чим насправді є SRAM і DRAM, та як побудована ієрархія від регістрів до диска. Ця лекція дає вам це розуміння.А ще є структури даних. У високорівневій мові, як-от C, ви пишете struct Student { char name[32]; int age; }, і компілятор сам розв'язує питання розташування в пам'яті, вирівнювання та арифметики вказівників. В асемблері ви — це компілятор. Ви вирішуєте, де розміщується кожне поле, скільки байтів воно займає, як переміщуватися від однієї структури до наступної у зв'язаному списку та як індексувати масив таких структур. Розуміння того, як структури даних відображаються на сирі байти, — одна з найпрактичніших навичок, які ви винесете з цього курсу; вона поглибить ваше розуміння кожної структури даних, яку ви зустрінете в майбутніх курсах з алгоритмів, операційних систем та системного програмування.Подумайте про це так: досі ви вивчали мову процесора — його регістри, інструкції та угоди про виклики. Ця лекція навчить вас середовищу, в якому живе процесор: пристроям, з якими він спілкується, ієрархії пам'яті, на яку він покладається, та структурам даних, якими він оперує. Програміст, який знає лише інструкції, — це як мовець, який знає лексику, але ніколи не виходив з дому. Ця лекція виведе вас назовні.Щоб дати вам конкретне уявлення про те, куди ми рухаємось, ось один приклад, який охоплює всі три теми цієї лекції.Припустімо, ви хочете написати програму, яка зчитує клавішу з клавіатури та зберігає символ у полі структури. На найвищому рівні абстракції (код мовою C) це виглядає тривіально:struct Record { char key; int count; };
struct Record r;
r.key = getchar();
На рівні BIOS (який ви знаєте з L07) зчитування клавіші стає таким:mov ah, 00h
int 16h ; AL = ASCII-символ
Але на апаратному рівні (який ви вивчите в цій лекції) та сама операція передбачає безпосереднє зчитування контролера клавіатури:wait_key: in al, 64h ; Зчитати порт стану клавіатури test al, 01h ; Чи є байт у черзі? jz wait_key ; Ні — продовжуємо опитування in al, 60h ; Так — зчитуємо скан-код
А доступ до структури, який компілятор C приховує за r.key, стає явним обчисленням зміщення:Record STRUC key db ? ; зміщення 0 count dw ? ; зміщення 1
Record ENDS mov [bx + Record.key], al ; Зберегти символ зі зміщенням 0 у структурі
Кожен рівень знімає абстракцію та відкриває механізм під нею. Наприкінці цієї лекції ви вільно працюватимете на всіх трьох рівнях — і точно розумітимете, як кожен з них відображається на рівні нижче.<img alt="Мережевий комутатор з Ethernet-кабелями" src="assets/l08-network-switch.jpeg" target="_self">Мережевий комутатор — один із багатьох пристроїв введення-виведення, що взаємодіє з процесором через системну шину.Ця лекція — «Пристрої. Ієрархія пам'яті. Структури даних на низькому рівні» — заповнює прогалину між процесороцентричним баченням попередніх лекцій та повною системною перспективою. Ми відкриємо капот і покажемо, як процесор спілкується із зовнішніми пристроями, побудуємо повну ієрархію пам'яті від регістрів до стрічкових накопичувачів, а потім повернемося до програмування на асемблері, реалізуючи високорівневі структури даних (структури, зв'язані списки, масиви рядків) безпосередньо в пам'яті.Три теми цієї лекції — введення-виведення, пам'ять і структури даних — пов'язані між собою тісніше, ніж здається на перший погляд. Пристрої введення-виведення розташовані на тій самій шині, що й пам'ять, і процесор спілкується з обома за допомогою подібних механізмів (адреси та передачі даних). Ієрархія пам'яті визначає, наскільки швидко ваша програма може звертатися до структур даних. А самі структури даних мають бути розташовані в пам'яті так, щоб відповідати шаблонам доступу апаратного забезпечення. Наприкінці цієї лекції ви чітко побачите ці зв'язки.Лекція організована в три частини:Частина 1: Пристрої введення-виведення (розділи 2-6). Ми почнемо з повторення системної шини з L06, а потім зосередимося на тому, як пристрої введення-виведення підключаються до неї. Розділ 2 нагадує про три типи шин та вводить розрізнення між введенням-виведенням через пам'ять та портовим введенням-виведенням. Розділ 3 використовує контролер клавіатури як детальний, конкретний приклад — простежуючи повний шлях від фізичного натискання клавіші до скан-коду в буфері BIOS. Розділи 4-5 знайомлять з асемблерними інструкціями IN та OUT, командами клавіатури й таблицями скан-кодів. Розділ 6 представляє стандартну карту адрес портів введення-виведення IBM PC. Наприкінці частини 1 ви зрозумієте, як спілкуватися з апаратним забезпеченням безпосередньо — повністю обминаючи BIOS і DOS.Частина 2: Ієрархія пам'яті та типи пам'яті (розділи 7-11). Ми розширимо ієрархію пам'яті, коротко представлену в L06, до ґрунтовного розгляду. Розділ 7 мотивує ієрархію фундаментальним компромісом між швидкістю та ємністю. Розділ 8 детально розглядає архітектуру кеша: рівні L1, L2 та L3, розділений та уніфікований кеш, а також як багатоядерні процесори спільно використовують кеш. Розділ 9 кількісно описує різницю продуктивності між рівнями ієрархії з конкретними числами пропускної здатності та затримки. Розділ 10 порівнює технології пам'яті (SRAM, DRAM, флеш-пам'ять, ROM, магнітну, оптичну) за швидкістю, вартістю, енергозалежністю та типом доступу. Розділ 11 знайомить з віртуальною пам'яттю та сторінкуванням — механізмом, який дозволяє сучасним операційним системам створювати для кожного процесу ілюзію володіння всім адресним простором.Частина 3: Структури даних на низькому рівні (розділи 12-15). Ми повертаємося до програмування на асемблері з практичним фокусом: як структури даних, знайомі вам із високорівневих мов — масиви, двовимірні масиви, структури та зв'язані списки — насправді існують у пам'яті? Розділ 12 повертається до одновимірних та двовимірних масивів (продовжуючи L05) з акцентом на порядковому розташуванні та обчисленні адрес. Розділ 13 знайомить з директивами TASM STRUC/ENDS для визначення структур. Розділ 14 реалізує зв'язані списки з використанням вказівників (пар сегмент:зміщення) в асемблері. Розділ 15 охоплює масиви рядків та таблиці вказівників. Кожну структуру ми реалізуємо в x86-асемблері, показуючи точне розташування в пам'яті та інструкції, необхідні для створення й обходу цих структур.У лабораторній роботі 8 ви вперше працюватимете безпосередньо з портами введення-виведення, зчитуючи скан-коди клавіатури за допомогою інструкції IN замість того, щоб покладатися на INT 16h. Ви також працюватимете з текстовою відеопам'яттю за адресою B800:0000, записуючи символи та байти атрибутів безпосередньо у відеобуфер. Це не штучні вправи — саме так реальні операційні системи та ігрові рушії взаємодіють з апаратним забезпеченням. Обробник клавіатури BIOS, який викликає INT 16h, робить саме те, що ви навчитеся робити вручну в цій лекції: зчитує байт з порту 60h, перевіряє регістр стану за портом 64h та перетворює результат.Подальші лабораторні роботи вимагатимуть від вас визначення складних структур даних в асемблері: масивів рядків, структур зі змішаними типами полів та зв'язаних списків, побудованих з динамічно виділених блоків пам'яті. Директиви TASM STRUC/ENDS та шаблони обходу вказівників, представлені в частині 3 цієї лекції, будуть безпосередньо застосовні до цієї роботи.Поза лабораторними роботами, концепції цієї лекції мають тривалу цінність. Коли ви вивчатимете операційні системи в майбутньому курсі, такі теми, як сторінкування віртуальної пам'яті та архітектура драйверів пристроїв, безпосередньо спиратимуться на те, що ви дізнаєтеся тут. Коли ви зустрінете «помилку сегментації» в програмі на C, ментальна модель сирих вказівників, розташування пам'яті та адресних просторів, яку ви побудуєте тут, допоможе вам діагностувати її швидше за будь-який вивід відлагоджувача. А коли ви профілюватимете повільну програму й виявите, що вузьким місцем є промахи кеша, розділ про ієрархію пам'яті цієї лекції точно пояснить, чому це так.Ця лекція знаходиться на перехресті курсу. Вона спирається майже на кожну тему з перших семи лекцій, водночас вводячи кілька абсолютно нових концепцій. Ось карта зв'язків:
Адресація сегмент:зміщення (L03) — адреси портів введення-виведення та фізичні адреси пам'яті спираються на концепції адресації, встановлені в L03. Ми посилатимемося на пари сегмент:зміщення, обговорюючи відеопам'ять (B800:0000) та області, відображені в пам'яті.
Представлення чисел (L02/L03) — шістнадцяткова система числення широко використовується для адрес портів, скан-кодів та адрес пам'яті впродовж усієї лекції. Бітові маніпуляції (AND, OR, TEST) використовуються для перевірки прапорців регістра стану.
Масиви та стек (L05) — ми повертаємося до шаблонів доступу до одновимірних та двовимірних масивів з L05 і розширюємо їх на структури та зв'язані списки. Угоди про стекові фрейми залишаються актуальними при передачі вказівників на структури в процедури.
Системна архітектура та шини (L06) — системна шина (даних, адресна, керування) була введена в L06, розділ 7. Тут ми безпосередньо будуємо на цьому фундаменті, пояснюючи, як пристрої введення-виведення підключаються до шини та чим портове введення-виведення відрізняється від введення-виведення через пам'ять.
Ієрархія пам'яті (L06) — розділ 9 лекції L06 представив ієрархію від регістрів до диска в одній підсумковій таблиці. Ця лекція розширює той огляд до повного розгляду з окремими розділами про архітектуру кеша, технології пам'яті, порівняння швидкості/затримки та віртуальну пам'ять.
Переривання та INT 16h (L07) — в L07 ви використовували INT 16h для зчитування введення з клавіатури через BIOS. Тепер ви побачите, що відбувається під цим перериванням: апаратне забезпечення контролера клавіатури, інструкція IN, яка зчитує порт 60h, та обробник IRQ1, що з'єднує апаратне та програмне забезпечення.
Скан-коди та IRQ1 (L07) — в L07 згадувалося, що IRQ1 викликає INT 09h, який зчитує скан-код з порту 60h. Ця лекція пояснює повний шлях скан-коду від фізичного натискання клавіші до циклічного буфера BIOS та показує, як зчитувати порт безпосередньо.
Якщо будь-яка з цих попередніх тем здається нечіткою, зараз вдалий час повторити відповідні розділи. Матеріал про порти введення-виведення (частина 1) найбільше спирається на L07 (переривання) та L06 (шини). Матеріал про ієрархію пам'яті (частина 2) розширює розділ 9 лекції L06. Матеріал про структури даних (частина 3) будується на L05 (масиви, стекові фрейми) та L03 (адресація пам'яті).ЦП без пристроїв введення-виведення — це калькулятор у запаяній коробці: він може обчислювати, але не може отримувати вхідні дані чи видавати результати. Уся мета комп'ютерної системи — взаємодіяти зі світом: зчитувати натискання клавіш, відображати зображення, зберігати файли, надсилати мережеві пакети. Усі ці взаємодії проходять через системну шину — електричну магістраль, що з'єднує ЦП з усім іншим.У L06 (розділ 7) ми встановили цю архітектуру та розглянули три типи шин — шину даних, адресну шину та шину керування. Той розділ був зосереджений на тому, як пам'ять підключається до шини. Тепер ми переносимо фокус на пристрої введення-виведення: як вони підключаються до тієї самої шини, як ЦП адресує їх і чим доступ до введення-виведення відрізняється від доступу до пам'яті.Нагадаємо з L06, що системна шина складається з трьох логічних підшин, кожна з яких передає різний тип сигналу:<br><img alt="Архітектура шини ЦП-пам'ять-введення/виведення" src="assets/l08-cpu-bus-diagram.png" target="_self">Три системні шини — шина даних, адресна шина та шина керування — з'єднують ЦП, пам'ять і пристрої введення-виведення.Три шини працюють узгоджено під час кожної передачі даних. Щоб прочитати байт з адреси пам'яті 1234h, ЦП виставляє 1234h на адресну шину, активує сигнал RD (читання) на шині керування, а потім зчитує байт, який з'являється на шині даних. Для запису він виставляє адресу, розміщує дані на шині даних і активує сигнал WR (запис). Кожен пристрій на системній шині — оперативна пам'ять, ПЗП, відеокарти, контролери клавіатури, контролери дисків — використовує ті самі фізичні провідники. Коли ЦП хоче зв'язатися з конкретним пристроєм, він виставляє адресу на адресну шину та активує відповідні сигнали керування. Тільки пристрій, який розпізнає цю адресу, відповідає.Це аналогічно поштовій системі: усі користуються однією поштовою мережею, але кожен будинок має унікальну адресу. Лист на адресу «вул. Головна, 123» потрапить лише до цього будинку, навіть попри те, що він проходить через ті самі сортувальні центри, що й листи на будь-яку іншу адресу. Так само байт, розміщений на шині даних, надходить лише до пристрою, декодер адреси якого збігається зі значенням на адресній шині.Одна лінія керування заслуговує на особливу увагу: IO/M (введення-виведення або пам'ять). На процесорі 8086 цей сигнал вказує, чи поточний цикл шини звертається до адресного простору введення-виведення, чи до адресного простору пам'яті. Коли ЦП виконує інструкцію MOV, він встановлює IO/M в низький стан (пам'ять). Коли він виконує інструкцію IN або OUT, він встановлює IO/M у високий стан (введення-виведення). Ця єдина сигнальна лінія дозволяє системі мати два окремих адресних простори, використовуючи ті самі фізичні провідники адресної шини. Адреса 0060h може посилатися на комірку пам'яті або порт введення-виведення — сигнал IO/M визначає, яку саме.З розумінням архітектури шини ми можемо поставити конкретніше запитання: як ЦП розрізняє звернення до пам'яті та звернення до пристрою? Існують два принципово різних підходи, і архітектура x86 підтримує обидва.Введення-виведення через пам'ять (memory-mapped I/O). Регістрам пристрою призначаються адреси в межах звичайного адресного простору пам'яті. ЦП використовує звичайні інструкції MOV для читання з цих адрес та запису за ними, а апаратне забезпечення шини спрямовує звернення до пристрою, а не до оперативної пам'яті. Відеопам'ять за адресою B800:0000 — це яскравий приклад, з яким ви вже знайомі: коли ви виконуєте MOV ES:[DI], AX з ES, що вказує на сегмент B800h, дані не надходять до мікросхеми оперативної пам'яті — вони потрапляють до відеоадаптера, який негайно інтерпретує байти як символ та атрибут і відображає їх на екрані. З точки зору ЦП, це виглядає як звичайний запис у пам'ять. Ви навіть можете використати REP STOSW для заповнення всього екрана — рядкова інструкція записує за послідовними адресами пам'яті, і кожен запис потрапляє до відеоадаптера.На карті пам'яті реального режиму (з L06, розділ 10) області A0000h-BFFFFh (128 КБ) зарезервовані для відеопам'яті. Апаратне забезпечення не розміщує оперативну пам'ять за цими адресами; натомість за ними відображається пам'ять відеоадаптера. Читання з цих адрес зчитує поточний вміст відеобуфера; запис за ними оновлює зображення на екрані.Перевага введення-виведення через пам'ять — гнучкість: будь-яка інструкція, що може звертатися до пам'яті — MOV, ADD, CMP, TEST, навіть рядкові інструкції на кшталт STOSW — може взаємодіяти з пристроєм. Спеціальні інструкції не потрібні. Недолік полягає в тому, що регістри пристрою займають адреси в карті пам'яті, зменшуючи простір, доступний для справжньої оперативної пам'яті. На процесорі 8086 з його адресним простором 1 МБ цей компроміс має значення: верхні 384 КБ зарезервовані для ПЗП, відеопам'яті та буферів пристроїв, залишаючи лише 640 КБ для звичайної оперативної пам'яті (знаменитий «бар'єр 640K»).Портове введення-виведення (ізольоване введення-виведення). Пристрою призначається окрема адреса у виділеному адресному просторі введення-виведення, повністю відмінному від адресного простору пам'яті. Адресний простір введення-виведення x86 має ширину 16 біт, що забезпечує 65 536 можливих адрес портів (0000h-FFFFh). ЦП використовує спеціальні інструкції — IN і OUT — для зв'язку з цими портами. Принципово, що шина керування передає сигнал (IO/M на 8086 або M/IO на пізніших процесорах), який повідомляє апаратному забезпеченню шини, чи поточне звернення спрямоване до пам'яті чи до простору введення-виведення. Це означає, що адреса 60h у просторі введення-виведення та адреса 0060h у просторі пам'яті — це зовсім різні місця, доступ до яких здійснюється через абсолютно різні механізми.<br><img alt="Діаграма шини з виділеною частиною введення-виведення" src="assets/l08-cpu-bus-io-highlight.png" target="_self">Та сама архітектура шини з виділеним інтерфейсом пристроїв введення-виведення — порти введення та виведення з'єднують зовнішні пристрої із системною шиною.Контролер клавіатури на портах 60h і 64h використовує модель портового введення-виведення. Так само працюють програмований інтервальний таймер (порти 40h-43h), програмований контролер переривань (порти 20h-21h), контролер DMA (Прямий доступ до пам'яті) (порти 00h-0Fh) та послідовні порти (порти 3F8h-3FFh для COM1). Натомість відеопам'ять (B800:0000 для текстового режиму, A000:0000 для графічного режиму) використовує введення-виведення через пам'ять, як і область даних BIOS та сам ROM BIOS.Більшість реальних систем x86 використовують комбінацію обох моделей. Загальний принцип такий: великі буфери даних (як-от відеопам'ять) відображаються через пам'ять, тому що передача тисяч байтів через IN/OUT по одному байту за раз була б болісно повільною; регістри керування та стану (як-от порти клавіатури) використовують портове введення-виведення, оскільки потрібно обміняти лише кілька байтів, а явні інструкції IN/OUT роблять наміри коду зрозумілими.Варто зазначити, що багато інших процесорних архітектур — ARM, MIPS, RISC-V — використовують лише введення-виведення через пам'ять. У них взагалі немає окремого адресного простору введення-виведення. Кожен пристрій доступний через звичайні інструкції завантаження/збереження за конкретними адресами пам'яті. Подвійний підхід сімейства x86 (і через пам'ять, і портовий) є дещо незвичним — це спадщина від архітектури Intel 8080/8085. У цій лекції ми зосереджуємося переважно на портовому введенні-виведенні, оскільки воно знайомить з інструкціями IN і OUT, які ви використовуватимете в лабораторних роботах. Ми також коротко попрацюємо з відеопам'яттю, відображеною через пам'ять, у контексті текстового режиму відображення.Кожен пристрій введення-виведення — клавіатура, миша, дисковод, мережева карта, послідовний порт, звукова карта — має контролер пристрою: спеціалізовану мікросхему або схему, що перетворює фізичний інтерфейс пристрою у протокол системної шини. ЦП ніколи не спілкується безпосередньо з необробленим обладнанням. Він спілкується з контролером, який, своєю чергою, керує пристроєм.Розглянемо жорсткий диск. Фізичний інтерфейс передбачає обертання магнітних пластин зі швидкістю 7200 обертів на хвилину, позиціювання головок читання/запису над конкретними доріжками з мікрометровою точністю та інтерпретацію патернів магнітного потоку для відновлення збережених даних. ЦП нічого про це не знає. Натомість контролер диска надає набір портів введення-виведення (для IDE/ATA-накопичувача: порти 1F0h-1F7h), куди ЦП може записувати команди («прочитати сектор 1234 з доріжки 56, головка 2») та зчитувати назад дані й інформацію про стан. Контролер обробляє всі механічні та електричні деталі внутрішньо. ЦП просто надсилає команди та отримує результати через кілька портів — тими самими інструкціями IN/OUT, що використовуються для контролера клавіатури.Така архітектура на основі контролерів забезпечує абстракцію апаратного забезпечення: ЦП комунікує через стандартизовані інтерфейси, ізольований від фізичних деталей кожного пристрою. Контролер клавіатури сканує матрицю клавіш тисячі разів на секунду, усуває брязкіт контактів та перетворює фізичні позиції клавіш у скан-коди. ЦП потрібно лише прочитати остаточний скан-код з одного порту введення-виведення. Якщо виробник перепроєктує внутрішню схемотехніку клавіатури, контролер все одно надає ЦП ті самі порти, і жодних змін у програмному забезпеченні не потрібно.Стандартизація інтерфейсів контролерів — це те, що зробило можливою індустрію IBM PC-сумісних комп'ютерів. Будь-яка клавіатура, що використовує той самий протокол, що й контролер Intel 8042, працює з будь-якою материнською платою ПК, незалежно від того, хто виготовив кожен компонент. Той самий принцип застосовується до контролерів дисків IDE/ATA, відеоадаптерів VGA та контролерів послідовних портів 16550 UART. Стандарти на рівні контролерів уможливили цілу екосистему взаємозамінного обладнання.Ось перелік найпоширеніших пристроїв введення-виведення IBM PC та способів їх підключення:Зверніть увагу на закономірність: регістри керування та стану використовують портове введення-виведення (доступ через IN/OUT), тоді як великі області даних, як-от відеопам'ять, використовують введення-виведення через пам'ять (доступ через MOV та інші інструкції роботи з пам'яттю). Ви зустрінете багато з цих пристроїв у подальших розділах та лабораторних роботах. Ми повернемося до повної карти адрес портів введення-виведення у розділі 6.Маючи цю концептуальну основу, розгляньмо детально конкретний контролер: контролер клавіатури.Ви натискали клавіші та зчитували їхні значення починаючи з лабораторної роботи 4. У L07 ви дізналися, що INT 16h викликає обробник BIOS, який повертає скан-код та ASCII-символ. Але що насправді відбувається між моментом, коли ваш палець натискає клавішу, і моментом, коли скан-код з'являється в регістрі AH? Відповідь передбачає спеціалізований апаратний компонент — контролер клавіатури — який виконує напрочуд багато роботи повністю самостійно, незалежно від ЦП.Ми використовуємо клавіатуру як практичний приклад комунікації з пристроями введення-виведення, бо це конкретний і знайомий пристрій. Кожен студент взаємодіяв із клавіатурою тисячі разів. Клавіатура також є архітектурно показовою: вона використовує портове введення-виведення, має регістри і даних, і стану, комунікує через переривання та вимагає конкретного протоколу команд. Наприкінці цього розділу ви зрозумієте повний апаратний шлях від натискання клавіші до програмного забезпечення й матимете ментальну модель, застосовну практично до будь-якого іншого пристрою введення-виведення в комп'ютері.<br><img alt="Механічна клавіатура з перемикачем клавіш" src="assets/l08-mechanical-keyboard.png" target="_self">Механічна клавіатура зі знятим перемикачем клавіші — кожна клавіша є фізичним перемикачем у матриці рядків і стовпців.<br>Оригінальний IBM PC (1981) використовував мікроконтролер <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Intel_MCS-48" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Intel_MCS-48" target="_self">Intel 8048</a>, вбудований безпосередньо в клавіатуру. Цей невеликий 8-бітний процесор сканував матрицю клавіш і надсилав скан-коди до материнської плати через простий однонаправлений послідовний протокол. Зв'язок був односторонній: клавіатура надсилала дані до ПК, але ПК не міг надсилати команди назад до клавіатури. Не було жодних світлодіодів клавіатури для керування, жодного способу змінити швидкість повторення клавіш і жодної можливості програмно перезавантажити клавіатуру. Якщо клавіатура стикалася з помилкою, система не мала способу відновитися без фізичного вимкнення та ввімкнення живлення.<br>IBM PC/AT (1984) запровадив принципово вдосконалений дизайн із контролером <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Intel_8042" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Intel_8042" target="_self">Intel 8042</a> — спеціалізованою мікросхемою контролера клавіатури на материнській платі. Це зробило зв'язок двостороннім: ЦП тепер міг надсилати команди клавіатурі (встановлювати стан світлодіодів, змінювати набори скан-кодів, налаштовувати швидкість та затримку повторення натискання), а клавіатура могла надсилати відповіді назад (байти підтвердження, коди помилок, результати самотестування, ідентифікацію пристрою). 8042 став стандартним інтерфейсом контролера клавіатури для всіх IBM PC-сумісних систем. Його розміщення регістрів — дані на порті 60h, стан/команда на порті 64h — залишається стандартом понад чотири десятиліття. Навіть сучасні системи, що інтегрували функціональність 8042 у чипсет (або власний south bridge ЦП), все одно надають ті самі порти за тими самими адресами задля зворотної сумісності.Сама клавіатура також містить мікроконтролер — нащадок 8048 або сучасний еквівалент на кшталт 8051. Тож повна підсистема клавіатури фактично включає два процесори, що комунікують один з одним через послідовний канал: внутрішній контролер клавіатури, що сканує матрицю та генерує скан-коди, і контролер материнської плати (8042), що приймає ці коди та робить їх доступними для ЦП через порти введення-виведення. Жоден з цих процесорів не є основним ЦП. Клавіатура має власну мікропрограму, власний цикл сканування та власний внутрішній автомат станів — це невеликий вбудований комп'ютер сам по собі.Контролер клавіатури 8042 знаходиться як посередник між системною шиною ПК та кабелем клавіатури. Він надає чотири логічних регістри через лише дві адреси портів введення-виведення, використовуючи напрямок доступу (читання чи запис) для розрізнення між ними:<br><img alt="Блок-діаграма контролера клавіатури" src="assets/l08-keyboard-controller-block.png" target="_self">Контролер клавіатури розташований між системною шиною ПК та клавіатурою, використовуючи порти 60h і 64h для даних, команд і стану.Розгляньмо кожен детально.Вихідний буфер (порт 60h, читання). Коли клавіатура надсилає скан-код, контролер 8042 розміщує його у вихідному буфері. ЦП читає цей буфер, виконуючи IN AL, 60h. Попри назву «вихідний буфер», це вихід з точки зору контролера — дані, що течуть від контролера до ЦП. З точки зору ЦП, це вхідні дані. Ця конвенція іменування слідує точці зору пристрою і є поширеним джерелом плутанини. Запам'ятайте: контролер виводить дані для ЦП, розміщуючи їх у цьому буфері.Вхідний буфер (порт 60h, запис). Коли ЦП хоче надіслати команду або дані до самого пристрою клавіатури (не до контролера — ця різниця важлива), він записує в порт 60h за допомогою OUT 60h, AL. Контролер забирає байт з цього буфера та пересилає його клавіатурі через послідовний канал. Клавіатура обробляє команду і може надіслати назад байт підтвердження (FAh для більшості команд). Знову ж таки, «вхідний» слідує точці зору контролера: буфер приймає вхідні дані від ЦП, призначені для клавіатури.Регістр стану (порт 64h, читання). Цей 8-бітний регістр повідомляє поточний стан контролера. Читання порту 64h за допомогою IN AL, 64h повертає байт стану. Бітова структура така:Два найважливіші біти для повсякденного програмування — біти 0 та 1. Перед читанням порту 60h правильно написана програма перевіряє біт 0 регістру стану, щоб підтвердити, що байт дійсно очікує. Перед записом у порт 60h або 64h вона перевіряє біт 1, щоб переконатися, що контролер обробив попередній байт. Ігнорування цих перевірок може призвести до втрати даних або спотворених команд. Ось типовий шаблон опитування:; Чекати, поки вихідний буфер матиме дані (OBF = 1)
wait_obf: in al, 64h ; Прочитати регістр стану test al, 01h ; Перевірити біт 0 (OBF) jz wait_obf ; Цикл, якщо буфер порожній in al, 60h ; Тепер безпечно читати скан-код
; Чекати, поки вхідний буфер стане порожнім (IBF = 0)
wait_ibf: in al, 64h ; Прочитати регістр стану test al, 02h ; Перевірити біт 1 (IBF) jnz wait_ibf ; Цикл, якщо буфер все ще заповнений out 60h, al ; Тепер безпечно записувати дані до клавіатури
Ці цикли активного очікування прості, але ефективні для контролера клавіатури, який зазвичай відповідає протягом кількох мікросекунд. У реальній операційній системі замість опитування використовувалися б переривання, але для прямого доступу до апаратного забезпечення в DOS-програмі опитування працює добре.Командний регістр (порт 64h, запис). Запис у порт 64h надсилає команду самому контролеру (не клавіатурі). Це команди рівня контролера, що керують інтерфейсом клавіатури:Різниця між записом у порт 60h (дані йдуть через контролер до клавіатури) та записом у порт 64h (команда йде до самого контролера) є критичною. Це різні операції, які лише мають суміжні номери портів. Змішування цих двох операцій призведе до непередбачуваної поведінки.Сама клавіатура побудована на основі матриці клавіш — сітки рядків і стовпців з перемикачем на кожному перетині. Типова повнорозмірна клавіатура має близько 104 клавіш, розміщених у матриці, яка може бути 16 стовпців на 8 рядків (хоча фактична розкладка залежить від виробника). Кожна клавіша знаходиться на унікальному перетині рядка і стовпця. Жодні дві клавіші не мають спільного перетину, що й дозволяє контролеру точно визначити, яка клавіша була натиснута.<br><img alt="Діаграма сканування матриці клавіатури" src="assets/l08-keyboard-matrix-block.png" target="_self">Всередині клавіатури: декодер керує рядками, кодер зчитує стовпці, а лічильник циклічно обходить матрицю для виявлення натискань клавіш.Ось повна послідовність подій при натисканні клавіші:Крок 1: Фізичний контакт. Перемикач клавіші замикається, з'єднуючи конкретний рядок із конкретним стовпцем у матриці. У механічній клавіатурі це відбувається, коли підпружинений механізм перемикача (наприклад, Cherry MX) досягає точки спрацювання — зазвичай близько 2 мм ходу. У мембранній клавіатурі гумовий купол стискається та притискає два провідних шари мембрани один до одного. В обох випадках результат однаковий: електричне з'єднання між одним рядком і одним стовпцем.Крок 2: Сканування матриці. Внутрішній мікроконтролер клавіатури безперервно сканує матрицю, послідовно подаючи напругу на кожен рядок і зчитуючи лінії стовпців. Коли він виявляє, що конкретний стовпець активний під час керування конкретним рядком, він визначає перетин рядка і стовпця, де була натиснута клавіша. Це сканування відбувається тисячі разів на секунду — значно швидше, ніж будь-яка людина може друкувати — гарантуючи, що жодне натискання не буде пропущено, навіть під час швидкого набору.Крок 3: Усунення брязкоту. Механічні перемикачі не створюють чистий контакт. Коли металеві контакти з'єднуються, вони «брязкають» — швидко чергуються між розімкненим і замкненим станами протягом кількох мілісекунд, поки контакти не стабілізуються. Без компенсації одне натискання клавіші реєструвалося б як два, три або навіть більше натискань. Контролер клавіатури вирішує це, очікуючи стабілізації сигналу (зазвичай 5-25 мс) перед повідомленням про натискання. Ця затримка непомітна для користувача, але необхідна для надійного введення.Крок 4: Генерація скан-коду. Контролер перетворює позицію рядок-стовпець на скан-код — унікальний числовий ідентифікатор цієї фізичної клавіші. Це не ASCII-код. Скан-код ідентифікує фізичне розташування клавіші на клавіатурі, а не символ, який вона представляє. Та сама фізична клавіша видає той самий скан-код незалежно від активної розкладки клавіатури чи налаштувань мови. Для набору скан-кодів 1 (набір за замовчуванням, що використовується в реальному режимі на IBM PC-сумісних системах):Коли клавіша натиснута, клавіатура надсилає код натискання (make code). Коли вона відпущена, надсилається код відпускання (break code), який є кодом натискання з встановленим бітом 7 (код натискання OR 80h). Це розмежування make/break дозволяє системі виявляти комбінації клавіш: коли ви натискаєте Shift+A, система спочатку отримує код натискання для Shift (2Ah), потім код натискання для A (1Eh). Оскільки код відпускання для Shift не надходив, система знає, що Shift все ще утримується, коли натиснуто A, і генерує велику літеру 'A'. Коли ви відпускаєте обидві клавіші, надходять відповідні коди відпускання.Якщо утримувати клавішу, клавіатура починає повторювати код натискання після налаштовуваної затримки (затримка автоповтору, зазвичай 250-1000 мс), а потім продовжує з налаштовуваною швидкістю (швидкість автоповтору, зазвичай 2-30 символів на секунду). Ці параметри можна змінити, надіславши команду F3h до клавіатури через порт 60h.Крок 5: Послідовна передача. Скан-код передається від клавіатури до контролера материнської плати через синхронний послідовний протокол із використанням двох ліній: лінії тактового сигналу та лінії даних. Кожен байт надсилається як 11 біт: 1 стартовий біт (завжди 0), 8 бітів даних (молодший біт першим), 1 біт непарної парності та 1 стоповий біт (завжди 1). Клавіатура генерує тактовий сигнал приблизно на частоті 10-16 кГц.Крок 6: Буферизація. Контролер 8042 на материнській платі отримує послідовні дані, збирає їх у байт, перевіряє парність та розміщує байт у своєму вихідному буфері (порт 60h). Він встановлює прапорець OBF (Output Buffer Full — вихідний буфер заповнений) — біт 0 регістру стану на порті 64h.Крок 7: Переривання. Контролер активує лінію IRQ1, що ініціює апаратне переривання. Як ми дізналися в L07, IRQ1 відображається на вектор переривань 09h через програмований контролер переривань (PIC). ЦП призупиняє виконання поточної програми, зберігає FLAGS, CS та IP у стеку і переходить до обробника переривання клавіатури, адреса якого зберігається в записі IVT 09h (фізична адреса 0000:0024h).Обробник клавіатури BIOS (INT 09h) виконує кілька завдань:
Зчитує скан-код з порту 60h за допомогою IN AL, 60h
Перевіряє, чи це код натискання, чи код відпускання (біт 7)
Оновлює прапорці стану shift/control/alt в області даних BIOS (за адресою 0040:0017h)
Перетворює скан-код на ASCII-символ за допомогою таблиць перекодування (враховуючи поточний стан shift, Caps Lock, Num Lock тощо)
Розміщує отриману пару скан-код / ASCII у буфер клавіатури BIOS — циклічну чергу на 16 елементів, розташовану в області даних BIOS за адресами 0040:001Eh — 0040:003Dh
Надсилає сигнал кінця переривання (EOI) до PIC, записуючи 20h у порт 20h
Повертається через IRET
Крок 8: Програма зчитує клавішу. Коли ваша програма викликає INT 16h з AH=00h, служба клавіатури BIOS перевіряє циклічний буфер. Якщо натискання доступне, вона видаляє найстаріший запис і повертає скан-код в AH та ASCII-код в AL. Якщо буфер порожній, INT 16h входить у щільний цикл (HLT + перевірка), очікуючи, поки IRQ1 доставить наступне натискання.Увесь цей ланцюг — від замикання перемикача до отримання скан-коду вашою програмою — зазвичай завершується менш ніж за мілісекунду для апаратної частини (кроки 1-7). Час програмної реакції залежить від того, як швидко ваша програма викликає INT 16h. Контролер клавіатури обробляє кроки 1-6 повністю автономно; ЦП залучається лише на кроці 7, коли спрацьовує переривання. Це фундаментальний принцип архітектури комп'ютерів: пристрої виконують власну роботу і турбують ЦП лише тоді, коли мають що повідомити.Розуміння контролера клавіатури цінне далеко за межами самих клавіатур. Шаблон, який ви тут бачите — контролер пристрою з портами даних і стану, механізм повідомлення на основі переривань та програмний буфер, керований ОС — повторюється практично в кожному пристрої введення-виведення комп'ютера:
Контролери дисків мають командні регістри (де ви вказуєте, який сектор читати), регістри даних (де з'являються дані сектора) та регістри стану (де ви перевіряєте, чи завершена операція). Вони сигналізують про завершення через апаратне переривання.
Мережеві адаптери мають буферну пам'ять для вхідних пакетів, регістри стану з прапорцями стану з'єднання та помилок, а також переривання для прибуття пакетів і завершення передачі.
Послідовні порти (UART, як-от 16550) мають регістри даних передачі та прийому, регістри стану лінії з прапорцями «передавач порожній» та «дані готові», а також переривання в обох напрямках.
Звукові карти мають регістри для налаштування частоти дискретизації, адрес буферів і керування відтворенням, з перериваннями для сигналізації про необхідність поповнення буфера.
Як тільки ви зрозумієте архітектуру контролера клавіатури — порт даних, порт стану, командний порт, повідомлення через переривання, програмний буфер — ви маєте шаблон для розуміння всього апаратного введення-виведення. Конкретні деталі змінюються (інші номери портів, інші бітові структури стану, інші набори команд), але фундаментальний шаблон залишається ідентичним.У наступних двох розділах ми напишемо код на асемблері, що безпосередньо комунікує з контролером клавіатури за допомогою інструкцій IN та OUT, обминаючи BIOS. Ви побачите, що прямий доступ до апаратного забезпечення не є набагато складнішим за використання служб BIOS — він лише вимагає знання номерів портів і дотримання протоколу.До цього моменту в курсі ви спілкувалися з апаратним забезпеченням через рівні абстракції. Коли ви викликали INT 21h для виведення рядка, DOS викликав процедуру BIOS, яка, зрештою, зверталася до відеоапаратури. Коли ви викликали INT 16h для зчитування натискання клавіші, BIOS обробляв контролер клавіатури за вас. Але що насправді означає «спілкування з апаратним забезпеченням» на найнижчому рівні? Відповідь пов'язана з двома інструкціями, яких ви ще не бачили: IN та OUT.ЦП потребує спосіб обмінюватися даними з периферійними пристроями — контролером клавіатури, таймером, послідовним портом, контролером дисків тощо. Архітектори комп'ютерів розробили два принципово різних підходи до цієї проблеми.<br>Введення-виведення через пам'ять розглядає регістри пристрою так, ніби вони є комірками пам'яті. Ви вже використовували цей підхід, коли записували безпосередньо у <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/VGA_text_mode" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/VGA_text_mode" target="_self">відеопам'ять за адресою B800:0000</a> у текстовому режимі — кожен MOV за адресу в цьому діапазоні не записував до оперативної пам'яті; він надсилав дані до відеоадаптера. Перевага — простота: будь-яка інструкція, що читає або записує пам'ять, може також взаємодіяти з апаратним забезпеченням. Недолік полягає в тому, що ці адреси пам'яті «зайняті» і не можуть зберігати звичайні дані.Портове введення-виведення (також зване ізольоване введення-виведення) надає пристроям власний окремий адресний простір, повністю незалежний від адресного простору пам'яті. ЦП використовує спеціалізовані інструкції — IN та OUT — для доступу до цього простору. Архітектура x86 надає 64K адресний простір портів введення-виведення (порти 0x0000 — 0xFFFF), де кожен порт подібний до маленької поштової скриньки, підключеної до конкретного регістру пристрою.Ось порівняння двох підходів:Платформа x86 використовує обидва підходи одночасно. Відеопам'ять відображається через пам'ять. Контролер клавіатури, мікросхема таймера, контролер переривань і більшість застарілих пристроїв використовують портове введення-виведення. Цей подвійний підхід є історичним артефактом дизайну IBM PC, і його розуміння є необхідним для системного програмування.Інші архітектури роблять інший вибір. Процесори ARM використовують виключно введення-виведення через пам'ять — у них взагалі немає інструкцій IN/OUT. Кожен регістр пристрою доступний через звичайні інструкції завантаження та збереження за конкретними адресами пам'яті. Підхід x86 із підтримкою двох окремих адресних просторів є дещо незвичним у ширшому ландшафті архітектури комп'ютерів, але він має перевагу чіткого відокремлення простору введення-виведення від пам'яті програм.Інструкція IN зчитує байт (або слово) з порту введення-виведення в регістр-акумулятор:in al, 60h ; Прочитати один байт з порту 0x60 в AL (безпосередній порт, 0-255)
in ax, 40h ; Прочитати одне слово (16 біт) з порту 0x40 в AX mov dx, 03F8h ; Порт 0x03F8 (послідовний порт COM1)
in al, dx ; Прочитати один байт з порту, вказаного в DX
Існують дві форми. Безпосередня форма (IN AL, port) кодує номер порту безпосередньо в інструкції як один байт, тому працює лише для портів від 0 до 255. Регістрово-непряма форма (IN AL, DX) бере номер порту з 16-бітного регістру DX, що дозволяє доступ до повного діапазону з 65 536 портів. На практиці більшість поширених пристроїв розташовані в нижньому діапазоні портів (нижче 0x100), тому ви часто зустрічатимете обидві форми.Чому саме DX має бути регістром для непрямої форми? Це не довільний вибір. Кодування інструкцій x86 резервує конкретний код операції для IN AL, DX (код операції ECh) та інший для IN AX, DX (код операції EDh). Можна використовувати тільки DX — не BX, не SI, не будь-який інший регістр. Так само акумулятор (AL для байтів, AX для слів) є єдиним допустимим приймачем. Ці обмеження закладені в декодер інструкцій ЦП.Інструкція OUT записує байт (або слово) з акумулятора до порту введення-виведення:out 61h, al ; Записати AL у порт 0x61 (безпосередній порт, 0-255)
out 43h, al ; Записати AL у порт 0x43 (командний регістр PIT) mov dx, 03F8h ; Порт 0x03F8 (послідовний порт COM1)
out dx, al ; Записати AL у порт, вказаний у DX
Ті самі дві форми застосовуються: безпосередня для портів 0-255 та регістрово-непряма через DX для повного діапазону. Зверніть увагу, що порядок операндів зворотний порівняно з IN — порт стоїть першим, регістр другим (для безпосередньої форми). Це узгоджується з конвенцією «приймач, джерело», що використовується більшістю інструкцій x86, де порт є приймачем, до якого ви записуєте.Важлива деталь: IN та OUT не впливають на регістр FLAGS. Вони не виконують порівняння, не встановлюють прапорець нуля і не спричиняють переповнення. Вони просто передають дані між ЦП та шиною введення-виведення. Якщо вам потрібно перевірити прочитане значення (а це потрібно майже завжди), ви маєте використати окрему інструкцію, як-от TEST або CMP, після цього.Коли ЦП виконує інструкцію IN або OUT, на апаратному рівні відбувається дещо принципово відмінне від звернення до пам'яті. Для звичайної інструкції MOV, що читає пам'ять, ЦП виставляє адресу на адресну шину, активує лінію керування читанням пам'яті та зчитує шину даних. Для інструкції IN ЦП виставляє номер порту на адресну шину, але натомість активує лінію керування читанням введення-виведення. Цей окремий сигнал керування відрізняє звернення до введення-виведення від звернень до пам'яті, навіть попри те, що вони використовують ті самі шини адреси та даних.Зовнішнє апаратне забезпечення (чипсет у сучасних системах) бачить сигнал читання введення-виведення та направляє запит до відповідного пристрою на основі адреси порту. Історично це здійснювалося дискретними логічними мікросхемами, що декодували адресні лінії. У сучасних системах Platform Controller Hub (PCH) обробляє цю маршрутизацію внутрішньо. Але принцип залишається незмінним: адреса порту діє як селектор пристрою, а сигнал керування введенням-виведенням повідомляє апаратному забезпеченню «це не звернення до пам'яті».Саме тому портове введення-виведення не взаємодіє з кешем ЦП. Читання з пам'яті може обслуговуватися з кешу L1/L2/L3, але читання з порту введення-виведення завжди проходить до самого пристрою. Кожна інструкція IN генерує реальну транзакцію на шині до апаратного забезпечення. Це робить введення-виведення принципово повільнішим за кешований доступ до пам'яті, але гарантує, що ви завжди зчитуєте поточний стан пристрою.<br>Подивимося на IN в дії. У лекції 7 ви дізналися, що клавіатура генерує <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Interrupt_request" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Interrupt_request" target="_self">апаратне переривання IRQ1</a> при натисканні клавіші, а обробник переривання BIOS зчитує скан-код і розміщує його в буфері клавіатури. Але що, якщо ви хочете обминути BIOS повністю і зчитати апаратне забезпечення клавіатури безпосередньо? Ось як:; Опитування контролера клавіатури та пряме зчитування скан-коду
read_loop: in al, 64h ; Прочитати регістр стану клавіатури (порт 64h) test al, 1 ; Перевірити біт 0: вихідний буфер заповнений? jz read_loop ; Якщо біт 0 = 0, даних немає — продовжити опитування in al, 60h ; Біт 0 = 1: скан-код очікує — прочитати його з порту 60h ; AL тепер містить скан-код клавіші, яку натиснули або відпустили
<br><img alt="Код TASM для зчитування скан-кодів клавіатури" src="assets/l08-asm-read-scancode.png" target="_self">Код на асемблері, що опитує регістр стану клавіатури (порт 64h) та зчитує скан-коди безпосередньо з порту 60h.Цей шаблон — перевірити стан, потім прочитати дані — є фундаментальним для всього портового введення-виведення. Контролер клавіатури має два порти, що мають значення: порт 64h — це регістр стану (з точки зору ЦП доступний лише для читання), а порт 60h — це регістр даних. Ось що означають біти регістру стану:Біт 0 — це те, що ми перевіряємо перед читанням: він повідомляє, чи контролер розмістив байт у вихідному буфері для зчитування ЦП. Якщо прочитати порт 60h, коли біт 0 скинутий, ви отримаєте застарілі дані з попереднього читання. Біт 1 — це те, що ми перевіряємо перед записом (як ви побачите у розділі 5): він повідомляє, чи контролер завершив обробку попереднього командного байта.Ви можете запитати: якщо ми можемо опитувати клавіатуру щільним циклом, як цей, навіщо нам взагалі потрібні переривання? Відповідь — ефективність. Подумайте: типовий друкар натискає приблизно 5-10 клавіш на секунду. ЦП, що працює навіть на 4,77 МГц (швидкість оригінального IBM PC), виконує мільйони інструкцій на секунду. Цикл опитування перевіряв би порт стану сотні тисяч разів між кожним натисканням клавіші, витрачаючи всі ці такти ЦП на біт, що майже ніколи не змінюється. Апаратні переривання (IRQ1 для клавіатури) дозволяють ЦП виконувати корисну роботу і реагувати лише тоді, коли клавіатура дійсно має дані.Обробник клавіатури BIOS, що активується через IRQ1, зчитує порт 60h, перетворює скан-код і зберігає його в буфері клавіатури BIOS — все це без необхідності опитування з боку вашої програми. Ваша програма може зосередитися на обчисленнях, малюванні чи будь-чому іншому, і перевіряти буфер клавіатури (через INT 16h) лише тоді, коли готова обробляти введення.Прямий доступ до портів через IN/OUT все ще цінний у конкретних сценаріях: написання драйверів пристроїв, ядер операційних систем, завантажувачів або вбудованого мікропрограмного забезпечення, де не можна покладатися на служби BIOS. Він також незамінний для апаратного забезпечення, про яке BIOS не знає, або коли потрібно більше контролю, ніж надає абстракція BIOS — як ми побачимо у наступному розділі з керуванням світлодіодами клавіатури.В операційних системах захищеного режиму, таких як Linux і Windows, користувацькі програми не мають права виконувати інструкції IN та OUT. Спроба зробити це спричиняє General Protection Fault (виняток 13). Тільки ядро (що працює на рівні привілеїв 0, або «Ring 0») може безпосередньо звертатися до портів введення-виведення. Це важливий безпековий бар'єр: уявіть, яку шкоду могла б завдати зловмисна програма, якби вона могла записувати довільні байти до контролера дисків або перепрограмовувати контролер переривань. ОС опосередковує весь доступ до апаратного забезпечення через драйвери пристроїв, які працюють у режимі ядра.Тепер, коли ви знаєте, як зчитувати порт даних контролера клавіатури, розгляньмо детальніше, що саме означають ці дані — і як надсилати команди назад до клавіатури.<br>Коли ви натискаєте клавішу на клавіатурі, внутрішній <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Intel_8042" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Intel_8042" target="_self">мікроконтролер</a> клавіатури не надсилає символ ASCII. Він надсилає скан-код — число, що ідентифікує фізичну клавішу на клавіатурі, незалежно від того, яка літера чи символ надруковані на ній. Це принципова відмінність: апаратне забезпечення клавіатури нічого не знає про символи, мови чи стани модифікаторів. Воно знає лише те, яка фізична клавіша була натиснута або відпущена.Чому б просто не надсилати символи ASCII? Подумайте: клавіатура має клавіші Shift, Ctrl, Alt, F1-F12, клавіші зі стрілками та Num Lock — жодна з них не відповідає друкованому символу. Більше того, одна й та сама фізична клавіша генерує різні символи залежно від того, чи утримується Shift (наприклад, 'a' проти 'A', '1' проти '!'). Клавіатура не може знати правильний символ, бо не знає розкладки клавіатури (QWERTY, AZERTY, Dvorak) і поточного стану Shift у програмному забезпеченні. Скан-коди вирішують цю проблему елегантно: клавіатура повідомляє про фізичні події (клавішу натиснуто, клавішу відпущено), а програмне забезпечення вирішує, що вони означають.Кожна клавіша має два скан-коди:
Make-код надсилається, коли клавішу натискають. Наприклад, клавіша 'A' надсилає make-код 1Eh.
Break-код надсилається, коли клавішу відпускають. Break-код — це завжди make-код з установленим бітом 7 (тобто make-код + 80h). Тож відпускання 'A' надсилає 9Eh.
Ця система make/break дозволяє програмному забезпеченню розпізнавати комбінації клавіш. Коли ви натискаєте Ctrl+C, система бачить: make-код для Ctrl (1Dh), потім make-код для C (2Eh). Коли ви їх відпускаєте, вона бачить break-коди 9Dh та AEh. Клавіатура надсилає безперервний потік цих подій, і саме програмне забезпечення (обробник BIOS, драйвер ОС або ваш власний код) відповідає за їхню інтерпретацію.Якщо ви утримуєте клавішу, клавіатура починає надсилати make-код повторно після невеликої затримки — це поведінка автоповтору (typematic). Потік виглядає так: 1Eh... (пауза ~500 мс) ...1Eh, 1Eh, 1Eh, 1Eh... (повтор ~10/с) ...9Eh (відпускання). Ви можете налаштувати швидкість повтору та затримку, надіславши команду до клавіатури, як ми побачимо далі.Розширені клавіші на 101/102-клавішній клавіатурі (стрілки, Insert, Delete, Home, End, Page Up/Down, а також праві Ctrl і Alt) надсилають двобайтові скан-коди: префіксний байт E0h, за яким іде основний код. Наприклад:Префікс E0h повідомляє обробнику клавіатури, що задіяна розширена клавіша, а не звичайна клавіша, яка має той самий другий байт. Наприклад, клавіша '4' на цифровій клавіатурі та клавіша стрілки вліво обидві надсилали б 4Bh без префікса — E0h їх розрізняє. Клавіша Pause/Break ще незвичніша — вона надсилає багатобайтову послідовність (E1h 1Dh 45h) і взагалі не має break-коду.<br>Наступна таблиця показує стандартний <a data-tooltip-position="top" aria-label="https://wiki.osdev.org/PS/2_Keyboard#Scan_Code_Set_1" rel="noopener nofollow" class="external-link is-unresolved" href="https://wiki.osdev.org/PS/2_Keyboard#Scan_Code_Set_1" target="_self">набір скан-кодів 1</a>, який використовується IBM PC-сумісними клавіатурами. Кожна фізична позиція клавіші має унікальний номер:<br><img alt="Таблиця скан-кодів клавіатури PC" src="assets/l08-scancode-table.png" target="_self">Таблиця 90 з Art of Assembly: скан-коди клавіатури PC, що показують значення make (натискання) та break (відпускання) у шістнадцятковому форматі.Деякі важливі скан-коди, які варто знати:Зверніть увагу, що скан-коди призначаються за фізичним розташуванням клавіші на клавіатурі, а не за алфавітним порядком. Числовий ряд — це 02h-0Bh, верхній ряд букв (QWERTY) — 10h-19h, домашній ряд (ASDF) — 1Eh-28h, а нижній ряд (ZXCV) — 2Ch-35h. Це відображає порядок, у якому клавіатура сканує свою матрицю клавіш — звідси й назва «скан-код».Пригадайте з лекції 7: коли ви викликали INT 16h, AH=00h, BIOS повертав скан-код в AH, а символ ASCII — в AL. Тепер ви розумієте, що BIOS робив за лаштунками — він зчитував необроблений скан-код з порту 60h, перевіряв, чи утримується Shift або Ctrl (відстежуючи make/break-коди для клавіш-модифікаторів), і перетворював результат у символ ASCII для вашої зручності.Це нашарування пояснює тонку деталь, яку ви, можливо, помітили: функція 00h переривання INT 16h розпізнає лише 83 клавіші (оригінальна клавіатура PC/AT), тоді як функція 10h розпізнає повну 101/102-клавішну розширену клавіатуру. Різниця полягає в тому, що функція 10h правильно обробляє розширені скан-коди з префіксом E0h. Якщо ви використовуєте функцію 00h з розширеною клавіатурою, натискання клавіш на кшталт правого Ctrl або стрілок може давати неочікувані результати. Саме тому сучасні програми повинні використовувати функцію 10h.Зв'язок з клавіатурою є двонаправленим. Клавіатура не лише надсилає скан-коди до процесора, але й процесор може надсилати команди мікроконтролеру клавіатури, записуючи дані в порт 60h. Це дозволяє керувати поведінкою клавіатури: встановлювати індикатори LED, змінювати швидкість автоповтору, вмикати та вимикати клавіатуру або навіть скидати її.Ця двонаправленість є причиною того, чому регістр стану має два окремих біти стану буфера. Біт 0 (Output Buffer Full — вихідний буфер повний) повідомляє процесору: «У мене є дані для вас». Біт 1 (Input Buffer Full — вхідний буфер повний) повідомляє процесору: «Я ще обробляю ваш останній запис — не надсилайте нічого». Терміни «вихідний» та «вхідний» вживаються з точки зору контролера, а не процесора: «вихідний буфер» контролера — це те, що зчитує процесор, а «вхідний буфер» контролера — це те, куди записує процесор.Перед надсиланням команди потрібно переконатися, що контролер клавіатури готовий приймати дані. Біт 1 регістра стану (порт 64h) вказує, чи заповнений вхідний буфер. Потрібно зачекати, поки цей біт не стане рівним нулю, перш ніж записувати:; Встановлення LED клавіатури: увімкнути всі три (Scroll Lock, Num Lock, Caps Lock)
wait_cmd: in al, 64h ; Зчитати регістр стану test al, 10b ; Перевірити біт 1: вхідний буфер повний? jnz wait_cmd ; Якщо повний, чекати mov al, 0EDh ; Команда 0xED = "Встановити LED" out 60h, al ; Надіслати команду до клавіатури wait_data: in al, 64h ; Зчитати стан знову test al, 10b ; Чекати, поки вхідний буфер звільниться jnz wait_data mov al, 00000111b ; Байт даних: біт 0 = Scroll Lock, біт 1 = Num Lock, біт 2 = Caps Lock out 60h, al ; Надіслати стан LED до клавіатури
<br><img alt="Код TASM для встановлення LED клавіатури" src="assets/l08-asm-set-leds.png" target="_self">Код на асемблері, що надсилає команду Set LEDs (0xED) до клавіатури через порт 60h, вмикаючи всі три індикаторні LED.Це двокроковий протокол: спочатку надсилається байт команди (0xED), потім — байт даних (07h для ввімкнення всіх трьох LED або 00h для їх вимкнення). Формат байта даних LED:Тож 00000101b (05h) вмикає Scroll Lock та Caps Lock, але залишає Num Lock вимкненим. 00000010b (02h) вмикає лише Num Lock — що є станом за замовчуванням на більшості систем під час завантаження.Контролер клавіатури підтверджує кожен байт, надсилаючи назад 0xFAh (ACK) через порт 60h. Надійна програма зчитала б це підтвердження й перевірила б на помилки (клавіатура надсилає 0xFEh для запиту повторного надсилання, якщо не зрозуміла команду). У простих програмах підтвердження часто ігнорується, але якісні драйвери клавіатури завжди його перевіряють.Мікроконтролер клавіатури розпізнає низку команд, окрім керування LED:<br><img alt="Команди мікроконтролера клавіатури" src="assets/l08-keyboard-commands-table.png" target="_self">Команди мікроконтролера клавіатури (порт 60h) — команда LED (0xED) та інші часто вживані команди.<br><img alt="Повний довідник команд клавіатури" src="assets/l08-keyboard-commands-full.png" target="_self">Повний довідник команд клавіатури (таблиця 95), що охоплює команди від 0xED до 0xFF.Команда Set Typematic Rate (0xF3) особливо цікава — вона дозволяє контролювати, як швидко повторюються утримані клавіші. Байт даних кодує затримку перед повтором і швидкість повтору в одному байті:Біти 6-5: Затримка перед початком повтору 00 = 250 мс 01 = 500 мс 10 = 750 мс 11 = 1000 мс Біти 4-0: Швидкість повтору 00000 = 30,0 символів/с (найшвидше) 01111 = 8,0 символів/с 11111 = 2,0 символи/с (найповільніше)
Наприклад, надсилання 0x00 після 0xF3 дає найшвидший можливий повтор: затримка 250 мс, потім 30 символів на секунду. Надсилання 0x7F дає найповільніший: затримка 1000 мс, потім 2 символи на секунду. Ви, можливо, помічали, що різні комп'ютери мають різну швидкість повтору клавіш — це саме та команда, яка це контролює. Багато утиліт налаштування BIOS мають параметр "Typematic Rate", який надсилає саме цю команду під час завантаження.<br>Ці команди показують дещо важливе про клавіатуру: вона не є пасивним пристроєм. Вона містить власний мікроконтролер (історично <a data-tooltip-position="top" aria-label="https://wiki.osdev.org/%228042%22_PS/2_Controller" rel="noopener nofollow" class="external-link is-unresolved" href="https://wiki.osdev.org/%228042%22_PS/2_Controller" target="_self">Intel 8042</a> або сумісний), який виконує власну мікропрограму, підтримує власний стан і відповідає на протокол команд та підтверджень. Процесор і клавіатура — це два незалежних процесори, що спілкуються через спільну шину.Ця архітектура — головний процесор, що спілкується з інтелектуальними контролерами периферійних пристроїв через протокол «команда — відповідь» — є повторюваним патерном у комп'ютерних системах. Ваш жорсткий диск має власний контролер. Ваш мережевий адаптер має власний процесор. Навіть ваш USB-концентратор має мікроконтролер. Інструкції IN/OUT, які ви вивчили в цій лекції, є найпростішою формою такого міжпроцесорного зв'язку. Більш сучасні інтерфейси, як-от PCI та USB, використовують складніші протоколи, але фундаментальна ідея — надіслати команду, дочекатися відповіді, зчитати дані — залишається незмінною.Ви вже бачили, як процесор використовує IN та OUT для зв'язку з контролером клавіатури через порти 60h та 64h. Але клавіатура — лише один із багатьох пристроїв, що знаходяться в адресному просторі портів введення-виведення. Розгляньмо повну картину.<br>Адресний простір введення-виведення x86 охоплює 65 536 портів (0x0000--0xFFFF) і є повністю відокремленим від адресного простору пам'яті. За десятиліття від появи IBM PC було встановлено <a data-tooltip-position="top" aria-label="https://wiki.osdev.org/I/O_Ports" rel="noopener nofollow" class="external-link is-unresolved" href="https://wiki.osdev.org/I/O_Ports" target="_self">стандартні призначення портів</a>, щоб кожен пристрій отримав свій власний діапазон:<br><img alt="Діапазони адрес портів введення-виведення" src="assets/l08-io-port-address-list.png" target="_self">Стандартні призначення адрес портів введення-виведення IBM PC — від контролера DMA за адресою 0x0000 до контролера клавіатури за адресою 0x0060-0x0064.Деякі з цих пристроїв мають бути знайомими з попередніх лекцій. PIC (Програмований контролер переривань) на портах 20h-21h — це мікросхема, яка керує апаратними перериваннями. Ви дізналися з лекції 7, що IRQ1 (клавіатура) відображається на INT 09h через PIC. PIT (Програмований інтервальний таймер) на портах 40h-47h — це системний таймер, який генерує IRQ0 приблизно 18,2 рази на секунду. Це серцебиття системи, яке DOS використовує для оновлення лічильника часу доби за адресою BIOS Data Area 0040:006Ch. Контролер DMA на портах 00h-1Fh дозволяє пристроям, як-от дисковод, передавати дані безпосередньо в пам'ять без участі процесора — це ми обговоримо детальніше на наступних лекціях.Зверніть увагу на патерн: більшість критичних системних пристроїв зосереджені в нижньому діапазоні портів (нижче 0x0400). Це застарілий простір введення-виведення, успадкований від оригінального IBM PC. Перші 1024 порти (0x0000-0x03FF) були визначені оригінальною специфікацією IBM PC/AT, і кожен PC-сумісний комп'ютер з 1984 року підтримує ці призначення. Саме тому ви можете написати IN AL, 60h на будь-якому x86 PC і бути впевненими, що звертаєтесь до контролера клавіатури — він знаходиться за цією адресою вже понад 40 років.Вищі діапазони портів (0x0400 і вище) використовуються додатковими картами, такими як звукові карти, вторинні контролери дисків та мережеві адаптери. Класична звукова карта Sound Blaster, наприклад, використовувала порти 220h-22Fh. Мережеві карти (сумісні з NE2000) зазвичай використовували 300h-31Fh. Ці адреси зазвичай налаштовувались за допомогою перемичок або DIP-перемикачів на карті — і правильне їх налаштування було відомим джерелом конфліктів обладнання в епоху DOS (сумнозвісні «конфлікти IRQ та адрес введення-виведення»).На сучасних системах багато з цих пристроїв повністю перейшли з портового введення-виведення на введення-виведення через пам'ять у конфігураційному просторі PCI/PCIe. Але базові застарілі пристрої — PIC, PIT, контролер клавіатури та CMOS — залишаються за своїми початковими адресами портів навіть у найновіших процесорах x86. Зворотна сумісність — це визначальна характеристика платформи x86.Кожен пристрій у своєму діапазоні портів спілкується власним протоколом. Контролер клавіатури використовує шаблон «стан, потім дані» (перевірити порт 64h, зчитати/записати порт 60h). PIT використовує командний порт (43h) і три порти лічильників (40h-42h). PIC має послідовність ініціалізації з кількома записами в порти 20h та 21h. CMOS/RTC використовує шаблон «індекс--дані»: записати номер регістра в порт 70h, потім зчитати або записати значення через порт 71h.Вам не потрібно запам'ятовувати всі ці протоколи. Важлива ментальна модель: простір портів введення-виведення — це шина з багатьма підключеними до неї пристроями, кожен з яких слухає на своєму діапазоні адрес. Процесор використовує IN та OUT для надсилання та отримання окремих байтів. Все інше — значення цих байтів, послідовність зчитувань та записів, вимоги до часу — визначається специфікацією конкретного пристрою.<br>Ось швидкий приклад зчитування <a data-tooltip-position="top" aria-label="https://wiki.osdev.org/CMOS" rel="noopener nofollow" class="external-link is-unresolved" href="https://wiki.osdev.org/CMOS" target="_self">годинника реального часу CMOS</a> для отримання поточної години, щоб показати, що той самий шаблон IN/OUT працює для будь-якого пристрою:; Зчитування поточної години з годинника реального часу CMOS
mov al, 04h ; Регістр 04h = години
out 70h, al ; Записати номер регістра в індексний порт CMOS
in al, 71h ; Зчитати значення з порту даних CMOS
; AL тепер містить поточну годину у форматі BCD (наприклад, 14h = 14:00)
Дві інструкції. Це все, що потрібно для зчитування часу з апаратного годинника. CMOS використовує протокол «індекс--дані»: записати номер потрібного регістра в порт 70h, потім зчитати (або записати) значення через порт 71h. Просто, але ефективно.Мікросхема CMOS містить 128 байтів пам'яті з батарейним живленням. Регістри 0x00--0x09 зберігають значення годинника реального часу (секунди, хвилини, години, день, місяць, рік). Регістри 0x10--0x3F зберігають конфігураційні дані BIOS — тип дисковода, параметри жорсткого диска, обсяг встановленої пам'яті та порядок завантаження. Це ті дані, які ви налаштовуєте в утиліті BIOS Setup (екран, на який потрапляєте, натиснувши F2 або Del під час завантаження). Кожного разу, коли ви змінюєте налаштування BIOS, мікропрограма записує дані через порти CMOS 70h та 71h.Ви можете запитати, навіщо ми витрачаємо час на портове введення-виведення, коли сучасні операційні системи абстрагують усе це за драйверами пристроїв. Є кілька причин. По-перше, розуміння IN/OUT є необхідним, якщо ви коли-небудь працюватимете з ядрами операційних систем, вбудованими системами чи завантажувачами — середовищами, де ще немає драйверів і потрібно спілкуватися з обладнанням напряму. По-друге, це завершує вашу ментальну модель системи. Тепер ви розумієте повний ланцюг від прикладного коду до фізичних транзакцій на шині. По-третє, концепція регістрів пристрою, до яких звертаються через протокол зчитувань і записів, є універсальною навіть у системах, що використовують введення-виведення через пам'ять замість портового. Регістри мають інші адреси, але шаблони — опитування стану, пари «команда/дані», пари «індекс/дані» — скрізь однакові.Об'єднаймо все з цього розділу лекції, простеживши повний шлях одного натискання клавіші:
Фізичне натискання клавіші — ваш палець натискає клавішу 'A' на клавіатурі
Матриця клавіш — внутрішня схема клавіатури визначає, який рядок і стовпець замкнулися
Мікроконтролер клавіатури — 8042 (або сумісний) ідентифікує клавішу, генерує скан-код 1Eh і розміщує його у своєму вихідному буфері
Сигнал до процесора — контролер клавіатури активує лінію IRQ1, яка досягає PIC (порти 20h/21h), який сигналізує на вхід переривань процесора
Реакція процесора — процесор завершує поточну інструкцію, зберігає FLAGS/CS/IP на стеку і шукає запис IVT за номером 09h (IRQ1 відображається на INT 09h)
Обробник BIOS — обробник INT 09h виконує IN AL, 60h для зчитування скан-коду з порту даних клавіатури
Перетворення — обробник перевіряє стан клавіш-модифікаторів (Shift, Ctrl тощо) і перетворює скан-код на символ ASCII
<br>Буфер клавіатури — скан-код і символ ASCII зберігаються в <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/BIOS_interrupt_call" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/BIOS_interrupt_call" target="_self">буфері клавіатури BIOS</a> (циклічний буфер у BIOS Data Area за адресою 0040:001Eh)
Читання програмою — ваша програма викликає INT 16h, AH=00h, яке витягує наступний запис із буфера клавіатури та повертає його в AX
Альтернативно, якщо ви обходите BIOS повністю (кроки 6-9), можна опитувати порт 64h і зчитувати порт 60h напряму — саме це ми робили у прикладі з опитуванням раніше.Зверніть увагу, скільки концепцій з попередніх лекцій збігаються в цьому одному шляху: стек (крок 5 — FLAGS/CS/IP зберігаються на стеку), IVT (крок 5 — пошук адреси обробника), апаратні переривання (крок 4 — IRQ1 та PIC), порти введення-виведення (крок 6 — IN з порту 60h) та модель пам'яті «сегмент:зміщення» (крок 8 — буфер за адресою 0040:001E). Одне натискання клавіші задіює майже кожен рівень системи, який ми вивчали.Цей наскрізний шлях ілюструє центральну тему цього курсу: комп'ютерні системи побудовані шарами. Фізичні сигнали стають електричними імпульсами, які стають скан-кодами, які стають запитами на переривання, які стають записами в буфері, які стають символами, що обробляє ваша програма. Кожен шар надає абстракцію, що спрощує роботу шару над ним — і кожен шар можна «відкрити», коли потрібен прямий контроль.Для закріплення того, що ви вивчили на лекціях 7 та 8, ось три рівні, на яких можна взаємодіяти з клавіатурою — від найвищої абстракції до найнижчої:Кожен рівень побудований на тому, що нижче. DOS викликає BIOS. BIOS зчитує апаратні порти. Апаратні порти підключені до регістрів контролера клавіатури. Тепер ви розумієте всі три рівні.У сучасній операційній системі, як-от Linux або Windows, існує четвертий рівень — драйвер клавіатури ОС — що знаходиться між обладнанням і програмою. Драйвер зчитує скан-коди (як обробник BIOS), перетворює їх за допомогою налаштовуваної карти клавіш (з підтримкою будь-якої мови чи розкладки) і передає результат програмам через чергу подій. Буфер клавіатури BIOS більше не використовується. Але фундаментальний апаратний інтерфейс — IN з порту 60h, стан за портом 64h — залишається точно таким самим, як на оригінальному IBM PC у 1981 році.У лекції 6 ми представили ієрархію пам'яті як ключову архітектурну концепцію — багаторівневу систему зберігання, яка долає величезну різницю у швидкості між процесором і масовими сховищами даних. Ми окреслили основні рівні (регістри, кеш, оперативна пам'ять, диск) та їхні приблизні часи доступу. Тепер настав час розглянути цю тему значно глибше. Розуміння ієрархії пам'яті — це не лише академічна вправа, а одна з найважливіших практичних тем в архітектурі комп'ютерних систем, адже спосіб, у який ваша програма звертається до пам'яті, може зумовити різницю у продуктивності в 100 або навіть 1000 разів.<br><img alt="Крупний план материнської плати" src="assets/l08-motherboard-closeup.jpeg" target="_self">Материнська плата із сокетом процесора та навколишніми компонентами — фізичне втілення ієрархії пам'яті, де кеш-пам'ять розташована на кристалі процесора, а слоти для оперативної пам'яті — поряд.Ось фундаментальна проблема. Сучасні процесори можуть виконувати мільярди операцій за секунду. Один тактовий цикл процесора з частотою 4 ГГц займає 0,25 наносекунди. Але доступ до основної пам'яті (DRAM — динамічна пам'ять) займає приблизно 50–100 наносекунд — тобто від 200 до 400 тактових циклів очікування. Якби процесор мусив звертатися до основної пам'яті за кожною інструкцією та кожним фрагментом даних, він проводив би переважну більшість часу абсолютно нічого не роблячи, просто чекаючи на надходження даних.Ця невідповідність між швидкістю процесора та швидкістю пам'яті відома як стіна пам'яті (memory wall). Вона є центральною проблемою проєктування комп'ютерів з 1980-х років, коли тактові частоти процесорів почали випереджати покращення DRAM. Процесори ставали швидшими приблизно на 60 % щорічно, тоді як затримка DRAM покращувалася лише приблизно на 7 % на рік. Розрив продовжував зростати, і архітекторам довелося шукати рішення.Для наочності: якби ви були ядром процесора з тактовою частотою 4 ГГц і кожен тактовий цикл тривав одну секунду вашого суб'єктивного часу, то очікування відповіді від основної пам'яті відчувалося б як очікування від трьох до п'яти хвилин. Очікування від SSD (твердотільний накопичувач) — як кілька днів. Очікування від жорсткого диска — як кілька місяців. Процесор є неймовірно швидким порівняно зі сховищами, від яких він залежить.Стіна пам'яті — це не лише історичний курйоз: вона продовжує формувати архітектуру процесорів і сьогодні. Сучасні процесори витрачають вражаючу частину свого транзисторного бюджету на кеш-пам'ять і керування пам'яттю. На багатьох сучасних процесорах понад половину площі кристала займає кеш-пам'ять. Подивіться на фотографію кристала будь-якого сучасного процесора, і ви побачите, що власне обчислювальна логіка (АЛП, виконавчі блоки) займає напрочуд малу частку чіпа — решта переважно є кеш-пам'яттю.Стіна пам'яті також є головною причиною того, що процесори стали настільки складними: такі техніки, як позачергове виконання (out-of-order execution), спекулятивне виконання (speculative execution) та апаратне передвибирання (hardware prefetching), існують переважно для того, щоб тримати процесор зайнятим під час очікування на пам'ять. Позачергове виконання дозволяє процесору працювати над незалежними інструкціями, поки очікується відповідь від пам'яті. Спекулятивне виконання дозволяє процесору передбачити, яку гілку програма обере, та почати її опрацьовувати ще до отримання відповіді. Апаратне передвибирання виявляє послідовні патерни доступу та завантажує дані в кеш ще до того, як програма їх запросить. Усі ці техніки додають процесору величезної складності, але вони необхідні для пом'якшення наслідків стіни пам'яті.Рішення використовує щасливу властивість реальних програм: вони не звертаються до пам'яті випадковим чином. Натомість програми демонструють локальність звернень — вони схильні повторно звертатися до тих самих і сусідніх даних протягом коротких проміжків часу. Ця локальність має дві форми:
Часова локальність: якщо ви нещодавно зверталися до певної адреси пам'яті, ймовірно, ви звернетеся до неї знову найближчим часом. Подумайте про змінну циклу — ви зчитуєте її, збільшуєте і знову зчитуєте на кожній ітерації.
Просторова локальність: якщо ви звернулися до певної адреси пам'яті, ймовірно, незабаром ви звернетеся до сусідніх адрес. Подумайте про перебір масиву — ви зчитуєте елемент 0, потім елемент 1, потім елемент 2 і так далі.
Ці патерни не є випадковими. Вони природно виникають із того, як структуровані програми: цикли повторюються над тим самим кодом і даними, масиви та структури зберігають пов'язані дані послідовно, а стек зростає й зменшується в локалізованій ділянці.Розгляньмо простий приклад: цикл, що обчислює суму масиву з 1000 цілих чисел. Змінна-лічильник циклу зчитується й записується на кожній ітерації — це часова локальність. Елементи масиву зчитуються послідовно, один за одним — це просторова локальність. І код (самі інструкції циклу), і дані (масив та лічильник) демонструють сильну локальність. Ієрархія пам'яті спроєктована саме для того, щоб використовувати ці патерни.Існує також пов'язана концепція — послідовна локальність — окремий випадок просторової локальності, коли доступ відбувається за строго послідовним патерном, наприклад, зчитування байтів із файлу або виконання інструкцій по порядку. Послідовну локальність найлегше виявити апаратно та оптимізувати за допомогою техніки передвибирання (prefetching), коли кеш проактивно завантажує наступний блок даних ще до того, як він буде запитаний.Наскільки сильна локальність на практиці? Дослідження реальних навантажень показали, що типові програми звертаються лише до приблизно 10 % свого загального адресного простору протягом будь-якого заданого інтервалу виконання. Більше того, значна частка звернень до пам'яті припадає на дуже невеликий набір «гарячих» адрес — стек, локальні змінні поточної функції та структури даних, що активно обробляються. Саме ця концентрація звернень забезпечує роботу ієрархії пам'яті: хоча кеш може вмістити лише крихітну частку загальної пам'яті, він зберігає саме ту частку, яка найбільш потрібна в даний момент.Ієрархія пам'яті використовує локальність, розміщуючи невеликі обсяги дуже швидкої (і дуже дорогої) пам'яті поруч із процесором, підкріплені поступово більшими, повільнішими та дешевшими сховищами. Коли процесору потрібні дані, він спочатку перевіряє найшвидший рівень. Якщо дані там є (влучання в кеш), доступ відбувається майже миттєво. Якщо ні (промах кеша), система витягує дані з нижчого рівня та переносить їх на швидший рівень, розраховуючи, що завдяки локальності вони знадобляться знову найближчим часом.Це аналогічно тому, як ви можете організовувати інформацію у повсякденному житті. Номер телефону, на який ви збираєтеся зателефонувати, ви тримаєте в короткочасній пам'яті (швидко, мала ємність). Контакти, якими часто користуєтеся, тримаєте в списку обраних у телефоні (швидкий доступ, обмежений простір). Усі контакти зберігаються в адресній книзі телефону (більший обсяг, трохи довше шукати). Старі записи ви тримаєте в картотеці (значно більший обсяг, значно повільніший доступ). А архівні документи зберігаєте на складі на іншому кінці міста (величезна ємність, але щоб щось дістати, потрібна поїздка). Кожен рівень має більшу ємність, але потребує більше зусиль для доступу. Ви природно тримаєте найпотрібнішу інформацію на найдоступнішому рівні — і ієрархія пам'яті робить те саме.<br><img alt="Піраміда ієрархії пам'яті комп'ютера" src="assets/l08-memory-hierarchy-pyramid.png" target="_self">Піраміда ієрархії пам'яті: менша за обсягом і швидша нагорі (регістри, кеш), більша за обсягом і повільніша внизу (жорсткі диски, стрічкові архіви).Ось повна піраміда, від найшвидшого до найповільнішого рівня:Зверніть увагу на величезний діапазон: від субнаносекундного доступу до регістрів до багатосекундного доступу до стрічки — різниця у понад 10 мільярдів разів. Кожен рівень ієрархії фактично діє як кеш для рівня нижче нього. Регістри кешують значення з L1, L1 кешує дані з L2, L2 — з L3, L3 — з оперативної пам'яті, оперативна пам'ять — з SSD, і так далі. Операційна система та апаратне забезпечення спільно керують цим прозоро, тож з точки зору програміста ви просто звертаєтеся до адрес пам'яті, а система робить усе можливе, щоб обслужити їх з найшвидшого доступного рівня.Верхні рівні ієрархії (від регістрів до оперативної пам'яті) є енергозалежними — вони втрачають свій вміст при знятті живлення. Нижні рівні (SSD, HDD, стрічка) є енергонезалежними — дані зберігаються без живлення. Саме тому вашому комп'ютеру потрібні і оперативна пам'ять, і жорсткий диск: оперативна пам'ять забезпечує швидке робоче сховище під час роботи системи, а диск забезпечує постійне зберігання, яке переживає перезавантаження та збої живлення.Варто зазначити, що керування цими рівнями розподілене між апаратним і програмним забезпеченням. Рівні кешу (L1, L2, L3) повністю керуються апаратно — процесор сам вирішує, що зберігати в кеші, а що витіснити, без участі програмного забезпечення. Межею між оперативною пам'яттю та диском керує операційна система за допомогою механізму, що називається віртуальна пам'ять (який ми розглянемо в наступній лекції). Межею між локальним сховищем і архівними стрічками керує спеціалізоване програмне забезпечення для резервного копіювання. Кожен рівень керування додає складності, але є невидимим для рівнів вище нього.Ця прозорість є ключовим принципом проєктування. Програміст, який пише mov eax, [address], не повинен знати, чи знаходиться ця адреса наразі в кеші L1, кеші L3 або в основній пам'яті. Апаратне забезпечення виконує пошук автоматично. Аналогічно, програма, що звертається до файлу, відображеного у пам'ять, не повинна знати, чи перебувають дані наразі в оперативній пам'яті чи ще на диску — операційна система обробляє сторінкову помилку й завантажує їх прозоро. Ця багаторівнева абстракція — саме те, що робить ієрархію зручною для використання попри її складність.Однак прозорість не означає, що програмісти можуть ігнорувати ієрархію. Хоча апаратне забезпечення робить її невидимою з точки зору коректності (ваша програма дає правильний результат незалежно від поведінки кеша), вона дуже помітна з точки зору продуктивності. Програма, дружня до кеша, і програма, ворожа до кеша, обидві коректні, але одна може працювати в 10 разів швидше за іншу. Ми докладніше розглянемо ці наслідки для продуктивності в розділі 9.Корисно розуміти фізичне розташування кожного рівня ієрархії. Регістри вбудовані безпосередньо в ядро процесора, прямо поруч із АЛП та логікою керування — вони буквально є частиною виконавчого механізму. Кеш L1 розташований на тому самому кремнієвому кристалі, що й ядро, фізично поруч із ним — зазвичай на відстані менше міліметра. Кеш L2 також на кристалі, але трохи далі від логіки ядра. Кеш L3 займає значну частину кристала процесора і є спільним для всіх ядер.Основна пам'ять (DRAM) розміщена в окремих чіпах, зазвичай змонтованих на модулях DIMM (Dual In-line Memory Module — модуль пам'яті), які вставляються в слоти на материнській платі. Фізична відстань від процесора до мікросхем DRAM становить кілька сантиметрів, і сигнали мають подорожувати мідними доріжками материнської плати — ця фізична відстань є однією з причин, чому доступ до DRAM займає значно більше часу, ніж доступ до кешу.SSD та HDD під'єднуються через кабелі або слоти PCIe, розташовуючи їх ще далі від процесора. Стрічкове сховище зазвичай перебуває в окремій роботизованій бібліотеці, можливо, в іншій кімнаті або будівлі. Фізична відстань на кожному рівні відображає зростаючу затримку — дані буквально мають подолати більшу відстань.При оцінці будь-якого рівня ієрархії архітекторів цікавлять кілька властивостей:
Затримка (latency): скільки часу потрібно для отримання одного фрагмента даних. Саме це число визначає, як довго процесор простоює при промаху.
Пропускна здатність (bandwidth): скільки даних може бути передано за секунду. Висока пропускна здатність дозволяє ефективно переміщувати великі блоки даних між рівнями.
Ємність (capacity): скільки даних можна зберегти. Більша ємність означає, що більше даних можна тримати близько до процесора.
Вартість за біт: скільки коштує зберігання кожного біта. Саме тому ми не можемо просто побудувати терабайти SRAM — це було б непомірно дорого.
Енергозалежність (volatility): чи зберігаються дані при знятті живлення. Все від регістрів до оперативної пам'яті є енергозалежним; SSD, HDD та стрічка — енергонезалежні.
Енергія на операцію доступу: скільки енергії споживається на одну операцію читання чи запису. Це стає дедалі важливішим у мобільних пристроях та дата-центрах, де тепловий та енергетичний бюджети обмежені. Доступ до DRAM споживає значно більше енергії, ніж доступ до кешу SRAM, а доступ до SSD або HDD — ще більше.
Ці метрики взаємопов'язані. Фізично розміщення пам'яті ближче до ядра процесора зменшує затримку (коротша затримка у провідниках) та енергоспоживання (коротший шлях сигналу), але простір поблизу ядра обмежений, що обмежує ємність. Перехід до більш щільної технології збільшує ємність, але додає затримки. Кожен рівень ієрархії представляє іншу точку балансу в цьому багатовимірному просторі компромісів.Ієрархія існує тому, що жодна окрема технологія не оптимізує всі ці властивості одночасно. SRAM — швидка, але дорога та має низьку щільність. DRAM — дешевша та щільніша, але повільніша. Флеш-пам'ять — ще дешевша, але значно повільніша. Магнітне сховище — найдешевше за біт, але найповільніше. Комбінуючи ці технології в ієрархію, архітектори створюють ілюзію великої, швидкої та дешевої системи пам'яті — такої, що здається програмісту і величезною, і миттєвою, хоча жодна окрема технологія не може забезпечити обидві якості водночас.<br>Концепцію кешу процесора вперше запропонував <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Maurice_Wilkes" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Maurice_Wilkes" target="_self">Моріс Вілкс</a> у 1965 році, назвавши його «допоміжною пам'яттю» (slave memory) — невеликим швидким буфером між процесором та основною пам'яттю. Ідея була простою: зберігати найчастіше використовувані дані у швидкому сховищі, щоб процесор не мусив чекати на повільну основну пам'ять при кожному зверненні. Першим комерційним комп'ютером з кешем був IBM System/360 Model 85, випущений у 1968 році, з кешем розміром 16–32 КБ. Сьогодні кеш — це не просто один буфер, а багаторівнева ієрархія кешів, кожен з яких має свою роль. Кожен сучасний процесор — від вашого ноутбука до серверів дата-центрів і чіпа у вашому смартфоні — містить кілька рівнів кешу. Це одна з найуніверсальніших рис сучасного проєктування процесорів.Ви можете запитати: чому б просто не побудувати один великий швидкий кеш? Відповідь криється у фізиці та економіці. Дуже малий кеш (скажімо, 32 КБ) можна зробити надзвичайно швидким — доступ до нього займе приблизно 1 наносекунду, оскільки дані мають коротку фізичну відстань для подолання, а логіка пошуку проста. Але 32 КБ — це мізерно мало; робочий набір програми часто перевищує цей обсяг, що призводить до частих промахів кеша.З іншого боку, великий кеш (скажімо, 32 МБ) може вмістити значно більше даних, зменшуючи кількість промахів. Але чим більший кеш, тим довше відбувається пошук — електричні сигнали мають подолати більшу відстань, а логіка вибірки стає складнішою. Кеш на 32 МБ може потребувати 10 наносекунд для доступу — це все ще значно швидше за DRAM, але в 10 разів повільніше за малий кеш на 32 КБ.Також існує чинник енергоспоживання. Більші кеші споживають більше енергії на кожен пошук, оскільки задіюють довші провідники та більше схемних елементів. Для пристрою з батарейним живленням, як-от ноутбук чи смартфон, енерговитрати на доступ до кешу становлять значну частку загального споживання енергії. Це ще одна причина, чому архітектори не роблять кеші якомога більшими — є енергетичний бюджет, якого треба дотримуватися.Рішення полягає у використанні кількох рівнів, кожен з яких представляє іншу точку на кривій компромісу між розміром і швидкістю. Промах на швидкому малому рівні «проходить» до більшого, трохи повільнішого рівня, перш ніж дійти до основної пам'яті. Уявіть це як серію захисних сіток: якщо найтісніша, найближча сітка не спіймала дані, ширша сітка нижче перехоплює більшість того, що проскочило, а ще ширша сітка під нею ловить майже все решту. Лише крихітна частка звернень взагалі доходить до основної пам'яті.<br><img alt="Ієрархія кешу одноядерного процесора" src="assets/l08-cache-hierarchy-single.png" target="_self">Ієрархія кешу для одного ядра процесора: регістри -&gt; кеш L1 (на кристалі, розділений на банки) -&gt; L2 уніфікований -&gt; L3 уніфікований -&gt; основна пам'ять.Кеш L1 — це найменший і найшвидший кеш, розташований найближче до ядра процесора. Його визначальні характеристики:
Розмір: зазвичай 32–64 КБ на ядро.
Затримка: приблизно 1 наносекунда (3–5 тактових циклів на сучасному процесорі).
<br>Розділена архітектура: більшість сучасних процесорів розділяють L1 на два окремі кеші — L1D (кеш даних) для операндів пам'яті та L1I (кеш інструкцій) для потоку інструкцій. Це називається <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Harvard_architecture" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Harvard_architecture" target="_self">гарвардською архітектурою</a> на рівні кешу, навіть попри те, що основна пам'ять використовує уніфікований (фон-нейманівський) адресний простір. Розділення L1 дозволяє процесору одночасно вибирати інструкцію та читати/записувати дані без конфлікту за ресурси.
Приватний: кожне ядро має свій виділений кеш L1.
Оскільки L1 такий малий, він значною мірою покладається на часову та просторову локальність. Дані, до яких ви зверталися один цикл тому, майже напевно досі в L1. Наступний елемент масиву, який ви перебираєте, також дуже ймовірно перебуває в L1, оскільки кеші завантажують дані блоками, що називаються рядками кеша (зазвичай 64 байти на процесорах x86), а не окремими байтами.Концепція рядка кеша є критично важливою. Коли ви зчитуєте один байт із пам'яті, апаратне забезпечення не вибирає лише цей один байт — воно вибирає весь 64-байтний блок (рядок кеша), що його містить. Це спосіб, яким апаратне забезпечення використовує просторову локальність: якщо ви зчитуєте байт N, ви, ймовірно, зчитаєте байти N+1, N+2 тощо, і вони вже в кеші. Для кешу L1D розміром 32 КБ з 64-байтними рядками кеш вміщує 512 рядків у будь-який момент часу.Чому саме 64 байти? Це ретельно підібраний компроміс. Більші рядки кеша використовують більше просторової локальності (більше сусідніх байтів передвибирається), але вони також марнують більше пропускної здатності, коли процесору потрібно лише кілька байтів з рядка. Менші рядки зменшують марнування пропускної здатності, але не використовують просторову локальність повною мірою. Розмір рядка 64 байти є стандартом на процесорах x86 вже понад два десятиліття, оскільки він добре збалансовує ці фактори для типових навантажень.Кеш L2 розташований за L1, слугуючи підстраховкою для промахів L1:
Розмір: зазвичай від 256 КБ до 1 МБ на ядро.
Затримка: приблизно 5 наносекунд (10–20 тактових циклів).
Уніфікований: на відміну від L1, кеш L2 зазвичай є уніфікованим і зберігає як інструкції, так і дані.
Приватний або напівспільний: у більшості сучасних архітектур L2 належить окремому ядру. Деякі архітектури розділяють L2 між парою ядер.
Коли процесор запитує дані, яких немає в L1 (промах L1), запит надходить до L2. Якщо L2 має ці дані (влучання в L2), вони повертаються і також розміщуються в L1 для швидкого доступу в майбутньому. Затримка L2 у 5 нс удесятеро повільніша за L1, але все ще в 10–20 разів швидша за основну пам'ять.Зв'язок між L1 і L2 залежить від політики включення кешу:
У включному (inclusive) дизайні кожен рядок у L1 також присутній у L2. Це спрощує когерентність (щоб перевірити, чи є дані в L1, достатньо перевірити L2), але означає, що ефективна додаткова ємність L2 зменшена — він дублює те, що L1 вже зберігає.
У виключному (exclusive) дизайні L1 та L2 зберігають непересічні дані. Коли рядок витісняється з L1, він переміщується до L2 (і навпаки при підвищенні). Це максимізує загальну ємність кешу — L1 + L2 разом зберігають більше унікальних даних.
У невключному (або NINE — Non-Inclusive Non-Exclusive) дизайні суворої політики немає — рядок може бути, а може й не бути присутнім на обох рівнях.
Сучасні процесори AMD використовують виключний дизайн L3, тоді як Intel перейшов від включної до невключної політики в останніх поколіннях. Це внутрішні архітектурні рішення, які впливають як на ефективну ємність, так і на складність протоколів когерентності кеша.Кеш L3 — це останній рівень кешу перед основною пам'яттю:
Розмір: зазвичай 8–32 МБ, іноді більше на серверних процесорах.
Затримка: приблизно 10 наносекунд (20–40 тактових циклів).
Уніфікований і спільний: L3 є спільним для всіх ядер процесора. Це критично важливо для продуктивності багатоядерних систем, оскільки якщо одне ядро записує дані, потрібні іншому ядру, їх можна знайти в спільному L3 без звернення до основної пам'яті.
<br><img alt="Ієрархія кешу багатоядерного процесора" src="assets/l08-cache-multicore.png" target="_self">Організація кешу багатоядерного процесора: кожне ядро має приватні кеші L1 (розділені на кеш даних та кеш інструкцій), зі спільними кешами L2 і L3 та загальною основною пам'яттю.У сучасному багатоядерному процесорі ієрархія кешу зазвичай виглядає так: кожне ядро має власні приватні L1D, L1I та L2. Усі ядра поділяють один великий L3. Основна пам'ять розташована за L3. Деякі серверні процесори вищого класу додають кеш L4 (іноді реалізований у вигляді eDRAM) для ще більшої ємності, але L3 є останнім рівнем у більшості споживчих процесорів.Кеш L3 також відіграє критичну роль у міжядерній комунікації. Коли два потоки, що виконуються на різних ядрах, потребують спільних даних, спільний L3 забезпечує значно швидший шлях, ніж звернення до основної пам'яті. Якщо ядро 0 записує значення, а ядро 2 його зчитує, дані можуть бути надані з L3 за ~10 нс, а не з оперативної пам'яті за ~100 нс. Це робить розмір і швидкість L3 особливо важливими для багатопотокових застосунків.На сучасних серверних процесорах кеші L3 можуть бути величезними. Процесори AMD EPYC пропонують до 256 МБ і більше кешу L3. Споживчий процесор AMD Ryzen 7 5800X3D, орієнтований на ігри, відомий тим, що отримав додаткові 64 МБ стекового кешу L3 (технологія 3D V-Cache) поверх існуючих 32 МБ, загалом 96 МБ. Цей додатковий кеш підвищив ігрову продуктивність на 15–20 %, оскільки набори даних багатьох ігрових навантажень вміщуються в 96 МБ, але не в 32 МБ — кеш виявився достатньо великим, щоб усунути критичний клас промахів через брак ємності.Перевагу кешів у продуктивності кількісно виражає формула середнього часу доступу (AAT — Average Access Time):AAT = hit_time + (miss_rate × miss_penalty)
Тут hit_time — час успішного пошуку на цьому рівні кешу, miss_rate — частка звернень, що завершуються промахом, а miss_penalty — час вибірки даних з наступного рівня (який сам може бути AAT цього наступного рівня). Для багаторівневих кешів формула застосовується рекурсивно — штраф за промах L1 є AAT L2, штраф за промах L2 є AAT L3, і так далі. Ця рекурсивна структура робить багаторівневі кеші такими ефективними: кожен рівень зменшує штраф за промах для рівня над ним.Розгляньмо конкретний приклад. Припустімо, доступ до основної пам'яті займає 50 нс, і ми маємо такі параметри кешу:Без кешу: кожне звернення йде до основної пам'яті. AAT = 50 нс.Лише з L1: AAT = 1 + (0,10 × 50) = 1 + 5 = 6 нс. Це покращення у 8,3 рази порівняно з відсутністю кешу.З L1 та L2: Штраф за промах L1 тепер дорівнює AAT L2, тобто 5 + (0,01 × 50) = 5,5 нс. Отже: AAT = 1 + (0,10 × 5,5) = 1 + 0,55 = 1,55 нс. Ще 3,9-кратне покращення.З L1, L2 та L3: Штраф за промах L2 тепер дорівнює AAT L3 = 10 + (0,01 × 50) = 10,5 нс. Штраф за промах L1 дорівнює AAT L2 = 5 + (0,01 × 10,5) = 5,105 нс. Отже: AAT = 1 + (0,10 × 5,105) = 1 + 0,5105 ≈ 1,51 нс. Помірне додаткове покращення, але кожна частка наносекунди має значення при мільярдах операцій за секунду.Прогресія вражає: ми перейшли від 50 нс (без кешу) до 1,51 нс (трирівневий кеш) — покращення у 33 рази середнього часу доступу. І зверніть увагу, що найбільший виграш дало додавання L1 (з 50 нс до 6 нс). Кожен наступний рівень забезпечує спадний, але все ще значущий приріст. Саме тому кожен сучасний процесор має щонайменше два рівні кешу, а більшість — три.Формула AAT також розкриває критичний висновок: навіть невеликий коефіцієнт промахів L1 має величезне значення, якщо штраф за промах великий. Коефіцієнт промахів L1 у 5 % зі штрафом за промах у 100 нс (без L2 або L3) дає AAT = 1 + (0,05 × 100) = 6 нс — це означає, що 83 % ефективного часу доступу припадає на рідкісні промахи, а не на часті влучання. Це іноді називають законом Амдала для пам'яті: загальна продуктивність визначається найповільнішим шляхом, зваженим за частотою його використання. Зменшення коефіцієнта промахів навіть на невеликий відсоток може мати драматичний вплив на загальну пропускну здатність.Влучання в кеш відбувається, коли запитані дані знайдено в кеші — процесор отримує дані швидко та продовжує виконання. Промах кеша відбувається, коли даних немає в кеші, що змушує вибирати їх з повільнішого рівня. Існує три типи промахів:
Холодні (обов'язкові) промахи: перше звернення до блоку даних. Кеш починає порожнім, тому перше звернення до будь-якої адреси завжди є промахом.
Промахи через брак ємності: кеш заповнений і не може вмістити всі дані, потрібні програмі. Старіші дані витісняються, щоб звільнити місце.
Конфліктні промахи: виникають у кешах, що не є повністю асоціативними — дві адреси відображаються на один і той самий слот кешу, і вони постійно витісняють одна одну. Це артефакт внутрішньої організації кешу.
Програми з сильною локальністю мають високий коефіцієнт влучань (95–99 % на L1 є типовим для добре оптимізованого коду). Програми зі слабкою локальністю — такі, що хаотично переміщуються великими структурами даних — мають часті промахи та працюють значно повільніше.Розуміння типів промахів допомагає оптимізувати код. Холодні промахи неминучі — ви мусите звернутися до даних уперше (хоча передвибирання може приховати їхню затримку, завантажуючи дані до того, як вони будуть запитані). Промахи через брак ємності можна зменшити, реструктуризувавши алгоритм для роботи з меншими порціями даних за раз. Конфліктні промахи можна зменшити, змінивши вирівнювання даних або використовуючи дружні до кешу способи розміщення даних у пам'яті. На практиці промахи через брак ємності є найпоширенішою проблемою для прикладних програмістів.Як кеш вирішує, який рядок витіснити, коли він заповнений і потрібно завантажити новий рядок? Це визначає політика заміщення кеша. Найпоширеніша політика — LRU (Least Recently Used — найдавніше використаний): витіснити рядок, до якого найдовше не зверталися. Обґрунтування полягає в часовій локальності — якщо рядок не використовувався нещодавно, він, ймовірно, не знадобиться найближчим часом.Справжній LRU дорого реалізувати апаратно для високоасоціативних кешів (відстеження точного порядку доступу для 12 напрямків потребує значного обсягу станової інформації), тому більшість реальних кешів використовують наближення, що називається псевдо-LRU, яке відстежує патерни доступу з меншою кількістю бітів. Інші політики включають випадкове заміщення (на подив ефективне — уникає патологічних найгірших випадків, на які може натрапити LRU) та FIFO (first in, first out — витіснення найстарішого рядка незалежно від нещодавнього використання).Коли витіснений рядок був модифікований (у нього записували дані), його потрібно записати назад до наступного рівня ієрархії перед відкиданням — це називається зворотний запис (write-back). Якщо рядок лише зчитувався і не був модифікований, його можна просто відкинути (оскільки дійсна копія існує на наступному рівні). Деякі кеші використовують політику наскрізного запису (write-through), коли кожен запис одразу оновлює і кеш, і наступний рівень. Наскрізний запис простіший і гарантує узгодженість, але генерує більше трафіку пам'яті. Зворотний запис є поширенішим у сучасних кешах L1 та L2, оскільки зменшує кількість повільних записів до нижчих рівнів.У багатоядерному процесорі кожне ядро має власні приватні L1 та зазвичай L2, тоді як L3 є спільним. Цей дизайн відображає компроміс:
Приватні кеші (L1, L2) надають кожному ядру швидкий, виділений доступ без конкуренції з боку інших ядер.
Спільні кеші (L3) дозволяють обмінюватися даними між ядрами без звернення до основної пам'яті. Якщо ядро 0 записує значення, а ядро 1 його зчитує, спільний L3 може надати його безпосередньо.
<br>Однак спільне використання створює проблему: когерентність кеша. Якщо ядро 0 має копію певної адреси у своєму приватному L1, а ядро 1 модифікує ту саму адресу, копія в ядрі 0 стає застарілою. Апаратні протоколи (такі як <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/MESI_protocol" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/MESI_protocol" target="_self">MESI</a>) відстежують стан кожного рядка кеша в усіх ядрах та забезпечують узгоджене бачення пам'яті всіма ядрами. <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/MESI_protocol" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/MESI_protocol" target="_self">Протокол MESI</a> присвоює кожному рядку кеша один з чотирьох станів:
Modified (M) — модифікований: це ядро змінило рядок; жодне інше ядро не має дійсної копії. Дані мають бути врешті-решт записані назад у пам'ять.
Exclusive (E) — ексклюзивний: це ядро має єдину копію, і вона не була змінена. Її можна модифікувати без повідомлення інших ядер.
Shared (S) — спільний: кілька ядер можуть мати копії, всі немодифіковані. Щоб модифікувати рядок, ядро має спочатку анулювати копії в інших ядрах.
Invalid (I) — недійсний: ця копія застаріла і не повинна використовуватися. Будь-яке звернення призведе до промаху кеша.
Це відбувається автоматично на апаратному рівні, але має наслідки для продуктивності. Коли одне ядро модифікує рядок кеша, який також зберігають інші ядра, апаратне забезпечення має анулювати ті інші копії та, можливо, записати назад модифіковані дані. Це займає час і споживає пропускну здатність шини. Надмірний обмін даними між ядрами (так зване хибне спільне використання (false sharing), коли це відбувається ненавмисно — наприклад, коли дві непов'язані змінні потрапляють в один і той самий 64-байтний рядок кеша) може суттєво знизити продуктивність, оскільки рядок кеша «стрибає» між ядрами туди-сюди.Щоб ці числа стали конкретними, ось конфігурація кешу типового процесора Intel Core i7 (12-те покоління, Alder Lake):Стовпець «асоціативність» описує, наскільки гнучко кеш розміщує дані, і ця концепція заслуговує пояснення. Уявіть, що кеш поділений на множини (sets), і кожна адреса пам'яті відображається рівно на одну множину. У межах цієї множини є кілька напрямків (ways) — слотів, де можуть зберігатися дані. Кеш з 12-канальною множинною асоціативністю означає, що кожна адреса пам'яті відображається на одну множину, і в межах цієї множини є 12 можливих слотів.У крайніх випадках:
Кеш з прямим відображенням (1-канальний) відображає кожну адресу рівно на один слот. Пошук у ньому швидкий (перевірити лише одне місце), але він потерпає від конфліктних промахів, коли дві часто використовувані адреси відображаються на один і той самий слот.
Повністю асоціативний кеш дозволяє будь-якій адресі зберігатися в будь-якому слоті. Це повністю усуває конфліктні промахи, але вимагає перевірки кожного слота при кожному зверненні, що є повільним і енерговитратним.
N-канальний множинно-асоціативний кеш — це компроміс, що використовується на практиці. Він забезпечує достатньо швидкий пошук (перевірити N місць), значно зменшуючи конфліктні промахи порівняно з кешем з прямим відображенням.
Кеші L1 використовують високу асоціативність (8–12 напрямків) для мінімізації промахів попри їхній невеликий розмір. Компромісом є дещо вища затримка та енергоспоживання на кожен пошук, але для такого малого й швидкого кешу ця ціна є прийнятною.Ви вже ознайомилися з ієрархією пам'яті та рівнями кешу. Тепер зробімо крок назад і подивимося на ширшу картину: чому ця ієрархія існує і що це означає для вас як програміста?Існує залізний закон проєктування пам'яті: швидша пам'ять є меншою за обсягом і дорожчою. Це не довільне інженерне рішення — це наслідок фізики та схемотехніки.Швидка пам'ять (SRAM) використовує шість транзисторів на біт у перехресно-з'єднаній конфігурації, яка зберігає свій стан протягом усього часу, доки подається живлення. Оновлення не потрібне, і доступ майже миттєвий. Але шість транзисторів на біт означає дуже низьку щільність — на чіпі неможливо розмістити багато SRAM, а вартість виробництва на біт є високою.DRAM (динамічна пам'ять) використовує один транзистор і один конденсатор на біт. Конденсатор зберігає заряд для представлення 0 або 1, а транзистор керує доступом. Це значно щільніше за SRAM — приблизно у 6–10 разів більше бітів на одиницю площі. Однак конденсатори втрачають заряд, тому DRAM потрібно періодично оновлювати (зчитувати та перезаписувати) тисячі разів на секунду, що додає затримку та складність. Зчитування комірки DRAM також є руйнівним — сам процес зчитування розряджає конденсатор, тому дані потрібно перезаписувати після кожного зчитування.Флеш-пам'ять (використовується у SSD) зберігає заряд у транзисторах з плаваючим затвором, які утримують електрони за ізолюючим шаром. Це дозволяє даним зберігатися без живлення — флеш-пам'ять є енергонезалежною. Щільність ще вища за DRAM, а вартість на біт значно нижча. Але зчитування займає мікросекунди (не наносекунди), а запис ще повільніший, оскільки вимагає стирання цілого блоку перед записом нових даних. Комірки флеш-пам'яті також зношуються після обмеженої кількості циклів запису (зазвичай від 1 000 до 100 000 залежно від типу), тому SSD включають складну логіку вирівнювання зношування (wear leveling).Магнітне сховище (HDD, стрічки) досягає найвищої щільності та найнижчої вартості, кодуючи дані як магнітні орієнтації на обертовому диску або стрічці. Доступ є надзвичайно повільним — головка читання/запису повинна фізично перемісти́тися в потрібну позицію (час пошуку), а потім чекати, поки диск повернеться до потрібного місця (ротаційна затримка). Ці механічні операції тривають мілісекунди, що у мільйони разів повільніше за електронні схеми.Ключовий висновок полягає в тому, що кожна з цих технологій займає свою унікальну нішу в спектрі швидкість-вартість. Жодна технологія не домінує над усіма іншими за кожним показником. Якби це було так, ми б просто використовували одну технологію для всього. Натомість ми складаємо їх в ієрархію, де кожна технологія виконує свою найкращу роль.Дослідники продовжують вивчати нові технології пам'яті, які можуть скоротити ці розриви. MRAM (Magnetoresistive RAM, магніторезистивна пам'ять) та ReRAM (Resistive RAM, резистивна пам'ять) обіцяють енергонезалежну пам'ять зі швидкістю, близькою до DRAM, що потенційно може розмити межу між пам'яттю та сховищем. 3D XPoint (продавався під брендом Intel Optane) пропонував затримки приблизно у 10 разів менші за NAND флеш, але все ж у 10 разів більші за DRAM — він займав нішу між DRAM та SSD, яку жодна попередня технологія не заповнювала. HBM (High Bandwidth Memory, пам'ять з високою пропускною здатністю) вертикально складає чіпи DRAM, використовуючи наскрізні міжкристальні з'єднання (TSV), що різко збільшує пропускну здатність — понад 1 ТБ/с у HBM3 — і є критично важливою для GPU та прискорювачів штучного інтелекту, яким потрібно подавати величезні обсяги даних у тисячі паралельних обчислювальних блоків.Ці нові технології додають нові рівні до ієрархії, а не ліквідують її. Фундаментальна фізика — швидший доступ вимагає менших розмірів і вищої вартості — навряд чи зміниться. Змінюється кількість і характер рівнів, а також конкретні технології на кожному рівні. Ієрархія — це не тимчасовий інженерний компроміс; це постійна ознака архітектури комп'ютерів.Ось повна картина в одній таблиці:<br><img alt="Піраміда швидкості пам'яті" src="assets/l08-memory-speed-pyramid.jpeg" target="_self">Ієрархія швидкості: сховище на рівні ЦП (регістри, кеш) є найшвидшим; оперативна пам'ять — посередині; масове сховище (диск) — найповільніше.Подивіться на стовпець вартості. SRAM, що використовується для кешу L1, коштує приблизно $1 000 і більше за гігабайт. DRAM коштує близько $3–5 за гігабайт — у 200–300 разів дешевше. HDD коштує приблизно $0,02 за гігабайт — ще у 150 разів дешевше за DRAM. Якби можна було побудувати 64 ГБ SRAM зі швидкістю L1, це коштувало б десятки тисяч доларів лише за пам'ять, а чіп мусив би бути непрактично великим для розміщення всіх цих транзисторів.Ієрархія існує, тому що економіка змушує нас використовувати поєднання технологій. Сучасний комп'ютер може містити: 1 КБ регістрів, 256 КБ кешу L1, 4 МБ кешу L2, 32 МБ кешу L3, 32 ГБ DRAM, 1 ТБ SSD і, можливо, зовнішній HDD на 4 ТБ для резервного копіювання. Кожен рівень коштує приблизно однакову суму грошей (кілька десятків доларів), але відрізняється на порядки за обсягом і швидкістю. У цьому і полягає магія ієрархії — рівні інвестиції на кожному рівні, але експоненціально більша ємність у міру просування вниз.Ієрархія пам'яті суттєво еволюціонувала протягом десятиліть. У ранні дні обчислювальної техніки (1950–1960-ті роки) ієрархії фактично не існувало — процесор звертався безпосередньо до основної пам'яті, і цим усе обмежувалося. Перші кеші з'явилися наприкінці 1960-х років. Дворівневі кеші (L1 + L2) стали поширеними у 1990-х. Трирівневі кеші (L1 + L2 + L3) стали стандартом у 2000-х.Тим часом технології на кожному рівні також еволюціонували. DRAM пройшла кілька поколінь: SDRAM, DDR, DDR2, DDR3, DDR4 і тепер DDR5 — кожне покоління приблизно подвоювало пропускну здатність порівняно з попереднім. DDR5, наприклад, пропонує пікову пропускну здатність понад 50 ГБ/с на канал, порівняно з приблизно 25 ГБ/с для DDR4. Однак затримка покращилася значно менше — затримка DDR5 фактично трохи гірша за DDR4 в абсолютних значеннях (наносекунди), хоча й краща у термінах тактових циклів. Ця асиметрія між покращенням пропускної здатності та стагнацією затримки є повторюваною темою в усій ієрархії.Флеш-сховище еволюціонувало від SLC (single-level cell, 1 біт на комірку) до MLC (2 біти), TLC (3 біти) та QLC (4 біти), причому кожне покоління пропонує вищу щільність і нижчу вартість за рахунок зниження витривалості й швидкості. Сучасні SSD також вертикально складають комірки у конфігураціях 3D NAND з понад 200 шарами, що різко збільшує ємність на чіп.Тенденція на кожному рівні була спрямована на збільшення ємності та пропускної здатності, тоді як покращення затримки були більш скромними — що підсилює потребу в ієрархії. «Стіна пам'яті» (memory wall) не звужується; навпаки, вона стає ширшою, що робить кеші та ієрархічне проєктування важливішими, ніж будь-коли.<br><img alt="Діаграма пропускної здатності та затримки кешу" src="assets/l08-cache-bandwidth-latency.png" target="_self">У міру просування вниз по ієрархії від ЦП до масового сховища пропускна здатність зменшується, а затримка та ємність зростають — фундаментальний компроміс між швидкістю та обсягом.Дві метрики визначають, як дані рухаються через ієрархію:
Затримка (latency) вимірює, скільки часу ви чекаєте на перший байт даних. Вона зростає у міру просування вниз по ієрархії. Регістри відповідають менш ніж за наносекунду; жорсткому диску може знадобитися 10 мілісекунд — різниця у 10 мільйонів разів.
Пропускна здатність (bandwidth) вимірює, скільки байтів на секунду може передаватися між рівнями. Вона зазвичай зменшується у міру просування вниз. З'єднання між ЦП та кешем L1 може забезпечувати сотні гігабайтів на секунду. Шина до основної пам'яті може доставляти 50 ГБ/с. SATA SSD може забезпечувати 500 МБ/с, а жорсткий диск — близько 200 МБ/с.
Обидві метрики важливі, але в різних сценаріях. Затримка домінує, коли ви робите багато малих, випадкових звернень — кожне з них платить повну вартість затримки. Пропускна здатність домінує, коли ви потоково передаєте великі блоки послідовних даних — початкова затримка амортизується на багато байтів.Корисна аналогія: затримка — це як час, потрібний вантажівці для поїздки зі складу до вашого будинку. Пропускна здатність — це розмір вантажівки. Якщо ви замовляєте одну маленьку посилку, вас здебільшого цікавить час поїздки (затримка). Якщо ви переїжджаєте всією квартирою, вас здебільшого цікавить, скільки поміститься за одну поїздку (пропускна здатність). Оптимізація коду, інтенсивно працюючого з пам'яттю, вимагає розуміння того, яка метрика важливіша для кожного шаблону доступу.Ось деякі приблизні показники пропускної здатності для контексту:Зверніть увагу, що пропускна здатність зменшується приблизно на порядок на кожній великій межі. З'єднання ЦП-L1 може бути у 1 000 разів швидшим за з'єднання з SATA SSD. Ось чому утримання даних у кеші є таким важливим — не лише через затримку, а й через загальну продуктивність.Варто відзначити нещодавнє поширення NVMe SSD, які обходять старіший інтерфейс SATA і підключаються безпосередньо до ЦП через лінії PCIe. Це скоротило розрив у пропускній здатності між SSD та оперативною пам'яттю з приблизно 100x (ера SATA) до приблизно 10x (ера NVMe). Затримка також покращилася, знизившись з ~100 мкс для SATA SSD до ~10–20 мкс для найкращих NVMe-накопичувачів. Сучасні NVMe-накопичувачі на PCIe 5.0 можуть підтримувати послідовне зчитування понад 10 ГБ/с — достатньо швидко, щоб вузьке місце зміщувалося від пристрою зберігання до здатності ЦП обробляти дані.Ця тенденція поступово розмиває межу між сховищем і пам'яттю. Деякі системи тепер використовують технології постійної пам'яті (persistent memory), які знаходяться між DRAM і флеш-пам'яттю як за швидкістю, так і за вартістю. Інші використовують CXL (Compute Express Link) — новий стандарт міжз'єднання, який дозволяє звертатися до віддалених пулів DRAM майже так, ніби вони є локальною пам'яттю. Ієрархія не є статичною — вона продовжує еволюціонувати у міру появи нових технологій.<br><img alt="Детальна блок-схема ієрархії пам'яті" src="assets/l08-memory-hierarchy-detailed.png" target="_self">Детальне зображення ієрархії пам'яті з ємністю, часом доступу та одиницею передачі на кожному рівні — від регістрів ЦП (сотні байтів, &lt;10 нс) до магнітної стрічки (необмежено, секунди).Можна подумати, що ієрархія пам'яті є суто апаратною проблемою і що розробники програмного забезпечення можуть її ігнорувати. Це було б дорогою помилкою. Різниця між кодом, який працює узгоджено з ієрархією кешу, і кодом, який працює проти неї, може бути величезною — у 10 разів і більше на тому самому обладнанні.Ось практичні висновки:Послідовний доступ є швидким; випадковий доступ є повільним. Ітерація масивом по порядку ідеально використовує просторову локальність. Перше звернення завантажує цілий 64-байтний рядок кеша, і наступні 15 звернень (для 4-байтних цілих чисел) усі влучають у кеш. Випадкові стрибки по великому масиву нівелюють цю перевагу — кожне звернення може бути промахом кеша.Тримайте робочий набір малим. Якщо ваші часто використовувані дані поміщаються в L1 (64 КБ), програма працюватиме блискавично швидко. Якщо вони поміщаються в L2 (1 МБ) — все ще швидко. Якщо в L3 (32 МБ) — все ще пристойно. Якщо вони не поміщаються в кеш і переходять в основну пам'ять, продуктивність помітно знижується. Якщо вони переходять на диск, вона падає катастрофічно.Вибір структури даних має значення. Масив структур (де кожна структура містить усі потрібні поля) часто перевершує структуру масивів (де кожне поле є окремим масивом) для послідовної обробки, оскільки зберігає пов'язані дані разом у пам'яті. Навпаки, якщо ви звертаєтесь лише до одного поля кожної структури, організація «структура масивів» уникає завантаження невикористаних полів у кеш.Повторно використовуйте дані, поки вони ще в кеші. Якщо вам потрібно обробити набір даних кілька разів, спробуйте виконати всю роботу над кожним блоком перед переходом до наступного, а не робити кілька повних проходів по всьому набору даних. Це називається блокуванням (blocking) або тайлінгом (tiling), і це ключова техніка оптимізації у високопродуктивних числових обчисленнях.Уникайте шаблонів з переслідуванням вказівників. Зв'язані списки, дерева та графи зберігають вузли розкидано по пам'яті, з'єднані вказівниками. Проходження ланцюжком вказівників — де кожний вказівник може вказувати будь-куди в пам'яті — створює шаблон фактично випадкового доступу, який нівелює як просторову, так і часову локальність. Ось чому масиви та структури даних на основі масивів (хеш-таблиці, плоскі вектори) зазвичай значно перевершують структури на основі вказівників у багатьох робочих навантаженнях, незважаючи на їхні теоретичні алгоритмічні недоліки в деяких операціях.Вирівнюйте дані за межами рядків кеша. Якщо 4-байтне ціле число перетинає два рядки кеша (частково в одному, частково в наступному), для його зчитування потрібно завантажити два рядки кеша замість одного. Компілятори зазвичай автоматично виконують базове вирівнювання, але коли ви проєктуєте критичні для продуктивності структури даних або працюєте з SIMD (Single Instruction, Multiple Data — одна інструкція, багато даних) операціями, ручна увага до вирівнювання може дати помітну різницю.Пам'ятайте про ефекти «обриву». Продуктивність часто знижується плавно у міру зростання робочого набору — доки він не перевищує ємність рівня кешу, після чого спостерігається раптове, різке збільшення часу доступу. Структура даних, яка поміщається в L1 (32 КБ), працює швидко. Та, що зростає до 40 КБ і виходить за межі L1 в L2, зазнає помітного уповільнення. Та, що перевищує L3 і потрапляє в основну пам'ять, може стати різко повільнішою. Розуміння того, де знаходяться ці обриви для вашого цільового обладнання, дозволяє відповідно розмірювати ваші структури даних.Це не абстрактні міркування. Рушії баз даних, ігрові рушії, коди наукового моделювання та ядра операційних систем — усі ретельно проєктуються для роботи з ієрархією пам'яті. Професійні розробники ігор витрачають значний час, щоб переконатися, що «гарячі» структури даних поміщаються в кеш L1 або L2. Рушії баз даних організовують свої дані на диску у блоки розміром зі сторінку, які ефективно відображаються на сторінки пам'яті та рядки кеша. Бібліотеки наукових обчислень, такі як BLAS, реалізують множення матриць із використанням стратегій блокування, які можуть досягати 90%+ теоретичної пікової продуктивності — порівняно з, можливо, 5–10% для наївної реалізації, яка ігнорує кеш.Як архітектор комп'ютерних систем, розуміння цих компромісів є необхідним. Ієрархія пам'яті — це не просто апаратна цікавинка; це один із найважливіших факторів, що визначають реальну продуктивність програмного забезпечення, яке ви пишете.Ієрархія пам'яті — це блискучий інженерний компроміс, породжений фундаментальним обмеженням: ми не можемо побудувати пам'ять, яка одночасно була б швидкою, великою, дешевою та енергонезалежною. Натомість ми нашаровуємо кілька технологій, кожна з яких оптимізована для різної точки у просторі компромісів, і використовуємо локальність звернень, щоб утримувати найбільш релевантні дані на найшвидших рівнях.З точки зору програміста, ключові уроки такі:
Знайте цифри. Розумійте приблизну затримку та обсяг кожного рівня ієрархії. Це дозволить вам передбачати, де виникатимуть вузькі місця продуктивності.
Використовуйте локальність. Структуруйте ваші дані та алгоритми так, щоб звертатися до пам'яті послідовно та повторно використовувати дані, поки вони ще в кеші.
Вимірюйте, а не вгадуйте. Використовуйте інструменти профілювання (такі як perf stat у Linux або Instruments у macOS) для вимірювання коефіцієнтів влучань кешу та визначення того, чи є ваша програма обмеженою пам'яттю, чи обчисленнями.
Ієрархія — ваш друг. Вона існує для того, щоб робити ваші програми швидкими — але тільки якщо ви працюєте з нею, а не проти неї.
У попередніх лекціях ви зустрічали конкретні типи пам'яті в контексті: SRAM у кешах (L03), DRAM в основній пам'яті (L03, L06), ROM, що зберігає мікропрограму BIOS (L07). Ви також бачили таблицю ієрархії пам'яті в L06, яка показувала, як кожен рівень балансує між швидкістю, обсягом і вартістю. Але ми ще не робили кроку назад, щоб оглянути повний ландшафт технологій комп'ютерної пам'яті та принципи, що їх розрізняють.У цьому розділі представлено комплексну класифікацію, що організовує типи пам'яті за шістьма критеріями: енергозалежність, енергоспоживання, можливість читання/запису, шаблон доступу, фізична інтеграція та базова технологія. Розуміння цих відмінностей не є суто академічним — вони пояснюють, чому комп'ютерні системи використовують ієрархію різних технологій пам'яті, а не одну універсальну, і допоможуть вам міркувати про компроміси продуктивності в реальних системах. Класифікація також надає словниковий запас, який ви зустрічатимете в технічних специфікаціях, документації та на співбесідах протягом усієї кар'єри.Найфундаментальнішим поділом у технологіях пам'яті є енергозалежність — чи зберігає пам'ять свій вміст при вимкненні живлення.Енергозалежна пам'ять втрачає всі збережені дані в момент відключення електричного живлення. До цієї категорії належать:
SRAM (Static Random-Access Memory, статична пам'ять із довільним доступом): Використовує схему з шести транзисторів на біт, організовану як бістабільну засувку (пару перехресно-з'єднаних інверторів). Схема зберігає свій стан протягом усього часу, доки подається живлення, без потреби в періодичному оновленні. SRAM використовується для кешів ЦП (L1, L2, L3) і регістрів ЦП. Слово «статична» у назві вказує на те, що збережене значення не змінюється з часом — воно залишається стабільним, доки тече струм, на відміну від DRAM, де збережений заряд згасає.
DRAM (Dynamic Random-Access Memory, динамічна пам'ять із довільним доступом): Зберігає кожен біт як заряд на крихітному конденсаторі, з одним транзистором у ролі вентиля доступу. Оскільки заряд конденсатора витікає з часом (зазвичай протягом мілісекунд), DRAM потрібно періодично оновлювати — контролер пам'яті зчитує кожну комірку та перезаписує її значення тисячі разів на секунду. Слово «динамічна» у назві вказує на цей постійно змінюваний (згасаючий) заряд. Незважаючи на цю складність, DRAM є домінуючою технологією для основної пам'яті (RAM) у сучасних системах, оскільки її конструкція з одним транзистором на біт є значно щільнішою та дешевшою за біт, ніж конструкція SRAM з шістьма транзисторами.
Основні компроміси між двома енергозалежними технологіями варто підсумувати:Ця таблиця дає відповідь на запитання, яке часто ставлять студенти: «Чому не зробити всю пам'ять з швидшого типу?» Відповідь — економіка та фізика. Комірка SRAM з шістьма транзисторами приблизно у шість разів більша за комірку DRAM з одним транзистором, що робить її у шість разів дорожчою за біт та у шість разів менш щільною. Побудувати 16 ГБ основної пам'яті з SRAM було б заборонно дорогим і фізично величезним. Навпаки, DRAM є надто повільною для внутрішніх рівнів кешу, де ЦП потребує дані протягом одного тактового циклу. Ієрархія пам'яті використовує переваги кожної технології, застосовуючи їх у взаємодоповнюючих ролях.Енергозалежна природа RAM пояснює, чому ваш комп'ютер «забуває» все в пам'яті при втраті живлення — і чому процес завантаження, описаний у L07, необхідний щоразу, коли ви вмикаєте машину. Вміст комірок DRAM згасає протягом мілісекунд без оновлення, а комірки SRAM втрачають свій стан одразу при падінні напруги живлення.Енергонезалежна пам'ять зберігає дані без живлення. Ця категорія значно різноманітніша:
ROM (Read-Only Memory, постійна пам'ять): Програмується назавжди під час виробництва. Оригінальні мікросхеми BIOS були масковими ROM — дані буквально вигравіровувались у кремнії під час виготовлення.
PROM (Programmable ROM, програмована ПЗП): Може бути запрограмована один раз після виробництва за допомогою спеціального пристрою, який фізично пропалює плавкі перемички всередині мікросхеми. Після програмування дані є постійними.
EPROM (Erasable Programmable ROM, стиральна ППЗП): Може бути стерта шляхом впливу ультрафіолетового світла через маленьке кварцове віконце на корпусі, а потім перепрограмована електрично. Це була поширена технологія для мікросхем BIOS у 1980–1990-х роках — ви, можливо, бачили характерні керамічні корпуси з круглими кварцовими віконцями.
EEPROM (Electrically Erasable Programmable ROM, електрично стиральна ППЗП): Може бути стерта та перепрограмована електрично, байт за байтом, без вилучення мікросхеми зі схеми. Повільніша за флеш-пам'ять, але дозволяє модифікацію окремих байтів.
Флеш-пам'ять: Нащадок EEPROM, що стирає та записує більші блоки замість окремих байтів. Цей компроміс — втрата побайтної гранулярності в обмін на значно вищу щільність та швидкість — зробив флеш-пам'ять домінуючою енергонезалежною технологією. Винайдена Фудзіо Масуокою у Toshiba у 1980 році та комерціалізована наприкінці 1980-х, флеш-пам'ять тепер живить SSD, USB-накопичувачі, SD-карти та сховища смартфонів. Назва «Flash» була запропонована колегою Масуоки Сьодзі Аріідзумі, тому що процес стирання нагадав йому спалах фотокамери.
Магнітне сховище: Жорсткі диски (HDD) та магнітна стрічка зберігають дані як шаблони намагніченості на обертовому диску або тонкій стрічці. Дані зберігаються невизначено довго без живлення.
Оптичне сховище: CD, DVD та Blu-ray диски кодують дані як шаблони западин і площадок на відбивній поверхні, зчитувані лазерним променем.
Поділ за енергозалежністю пояснює фундаментальну дворівневу структуру кожного комп'ютера: швидка, енергозалежна пам'ять для активних обчислень і повільніше, енергонезалежне сховище для постійних даних. Процес завантаження є мостом між ними — він копіює операційну систему з енергонезалежного сховища в енергозалежну оперативну пам'ять, щоб ЦП міг її виконувати.<br>Варто зазначити, що ці категорії не є цілком незмінними — вони відображають поточний технологічний ландшафт, а не фундаментальні фізичні закони. Нові технології намагаються розмити межу між енергозалежною та енергонезалежною пам'яттю. <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Magnetoresistive_RAM" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Magnetoresistive_RAM" target="_self">MRAM (Magnetoresistive RAM, магніторезистивна пам'ять)</a> зберігає біти як магнітні орієнтації в нанорозмірних магнітних тунельних переходах, забезпечуючи енергонезалежність зі швидкістю, близькою до SRAM. <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/3D_XPoint" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/3D_XPoint" target="_self">Постійна пам'ять Intel Optane</a>, заснована на технології 3D XPoint, пропонувала енергонезалежну пам'ять із затримками лише у 2–3 рази більшими за DRAM — різко швидшу за флеш-пам'ять. Хоча Optane було знято з виробництва у 2022 році (вона виявилася занадто дорогою та нішевою для масової комерційної реалізації), пошук «універсальної пам'яті» — швидкої, щільної, енергонезалежної та дешевої — продовжує стимулювати дослідження в галузі матеріалознавства та напівпровідникової інженерії. Якщо така технологія матиме успіх, вона може докорінно перебудувати ієрархію пам'яті, усунувши поділ на енергозалежну та енергонезалежну.Ще одне корисне розмежування — між первинною пам'яттю та вторинною пам'яттю (іноді їх називають первинне сховище та вторинне сховище). Первинна пам'ять безпосередньо адресується ЦП — процесор може виконати інструкцію MOV з адресою пам'яті й отримати доступ до даних. Сюди належать RAM (як SRAM, так і DRAM), ROM і кеш. Вторинна пам'ять не адресується безпосередньо — ЦП не може виконати просту інструкцію завантаження або збереження для доступу до даних на жорсткому диску чи SSD. Замість цього він повинен звернутися через контролер введення-виведення, видати команди через драйвер пристрою та чекати, поки дані будуть передані в первинну пам'ять, перш ніж їх можна буде обробити. Це розмежування важливе для розуміння продуктивності: доступ до первинної пам'яті вимірюється в наносекундах і потребує однієї інструкції, тоді як доступ до вторинної пам'яті вимірюється в мікросекундах або мілісекундах і потребує сотень інструкцій (код драйвера пристрою, протокол контролера введення-виведення та обробка переривань).Різні технології пам'яті мають суттєво різні характеристики енергоспоживання, що безпосередньо впливає на проєктування систем — особливо пристроїв з батарейним живленням, таких як ноутбуки та смартфони.SRAM є найбільш енергоємною технологією пам'яті на біт в активному стані. Кожен біт потребує шести транзисторів, які повинні постійно живитися для збереження стану. Перехресно-з'єднані інвертори споживають струм щоразу, коли утримують значення (це називається статичним розсіюванням потужності — потужність, що споживається навіть за відсутності операцій читання чи запису), а перемикання стану (запис) вимагає керування бітовими лініями проти поточного стану засувки. Сучасні комірки SRAM у передових техпроцесах (5 нм, 3 нм) також страждають від зростаючого струму витоку — струму, який тече через транзистори навіть коли вони «вимкнені», що є зростаючою проблемою у міру зменшення транзисторів. Це одна з причин, чому кеші залишаються малими: кеш L3 на 64 МБ споживав би значну потужність навіть у простої, генеруючи тепло, яке було б складно розсіяти в корпусі процесора.DRAM споживає менше енергії на біт, ніж SRAM, у стабільному стані — одиночний конденсатор, що утримує заряд, є по суті пасивним сховищем. Однак вимога оновлення DRAM додає постійні витрати на живлення. Кожні кілька мілісекунд контролер пам'яті повинен зчитати та перезаписати кожну комірку у всьому масиві DRAM, споживаючи потужність, пропорційну загальній ємності. У сучасному ноутбуці з 16 ГБ DDR5 лише цикли оновлення споживають помітну частку бюджету живлення системи в простої. Виробники DRAM постійно вдосконалюють алгоритми оновлення (часткове оновлення масиву, інтервали оновлення з температурною компенсацією) для зменшення цих витрат.Флеш-пам'ять являє собою різке покращення за споживанням у простої: мікросхема флеш-пам'яті споживає фактично нуль потужності, коли до неї немає звернень. Потужність споживається лише під час операцій читання та запису — причому запис є особливо витратним, оскільки вимагає високих напруг для ін'єкції або вилучення електронів із плаваючого затвору кожного транзистора. Цей майже нульовий рівень споживання у простої пояснює, чому смартфони та планшети можуть зберігати свої дані тижнями в режимі очікування: флеш-сховище практично не споживає енергії між зверненнями.Магнітне та оптичне сховище мають ще інший профіль споживання. HDD споживають помірну потужність під час обертання дисків (зазвичай 3–8 Вт), але можуть переходити у режим низького споживання, коли диски зупиняються. Оптичні приводи споживають потужність лише коли диск обертається та лазер активний. Для стрічкових накопичувачів картридж, що лежить на полиці, споживає рівно нуль потужності — найвища енергоефективність сховища, якщо ви можете терпіти час доступу.Ієрархія споживання — SRAM &gt; DRAM &gt; флеш &gt; магнітне/оптичне — майже точно відзеркалює ієрархію швидкості. Швидші технології пам'яті споживають більше енергії, що є фундаментальним фізичним компромісом: підтримання даних у швидко доступному стані вимагає безперервного надходження енергії. Це не випадковість — це відображає глибокий зв'язок між термодинамікою та теорією інформації. Утримання даних у «готовому» стані (низька затримка) вимагає підтримання фізичних станів, які легко швидко зчитати, а підтримання цих станів коштує енергії.Не всю пам'ять однаково легко читати та записувати. Характеристики читання/запису технології пам'яті визначають, як вона може використовуватися в системі.Пам'ять для читання/запису (RAM) може вільно зчитуватися та записуватися на рівні окремого байта або слова, з приблизно однаковою швидкістю для обох операцій. Як SRAM, так і DRAM належать до цієї категорії. Це пам'ять, з якою ваші програми працюють безпосередньо — кожна інструкція MOV [адреса], значення, яку ви писали на асемблері, виконує запис у RAM, а кожна MOV рег, [адреса] виконує читання.<br>Флеш-пам'ять займає цікаву проміжну позицію. Вона може зчитуватися на рівні окремого байта, так само як RAM. Але записи обмежені: флеш-пам'ять може записуватися лише блоками (зазвичай від 4 КБ до 256 КБ), і блок повинен бути повністю стертий, перш ніж будь-яка його частина може бути перезаписана. Стирання встановлює всі біти в 1, а запис вибірково змінює деякі біти на 0. Ви не можете змінити 0 назад на 1 без стирання всього блоку. Ця асиметрія між читанням і записом — та блокова гранулярність записів — пояснює, чому SSD потребують складних <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Flash_memory_controller" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Flash_memory_controller" target="_self">мікропрограмних контролерів</a> з вирівнюванням зношування, збиранням сміття та керуванням ампліфікацією запису. Це також пояснює, чому операційна система не може сприймати SSD як просто «швидку RAM», незважаючи на те, що флеш є напівпровідниковою технологією.Постійна пам'ять має кілька градацій, як описано у розділі 10.1:Прогресія від ROM до EEPROM та флеш-пам'яті являє собою поступове підвищення гнучкості запису, хоча ціною витривалості: комірки флеш-пам'яті зазвичай витримують від 1 000 до 100 000 циклів запису перед виходом з ладу, залежно від типу флеш. Це все ще на порядки менш довговічно, ніж DRAM, яку можна записувати фактично необмежену кількість разів.<br>Питання витривалості заслуговує детальнішого розгляду, оскільки має практичні наслідки, з якими ви можете зіткнутися. Сучасна <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Flash_memory#NAND_flash" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Flash_memory#NAND_flash" target="_self">NAND флеш</a> випускається у кількох варіантах, які балансують між витривалістю та щільністю:Розміщення більшої кількості бітів у комірці підвищує щільність і знижує вартість, але кожен додатковий біт зближує рівні напруги, збільшуючи частоту помилок і зменшуючи кількість можливих надійних перезаписів. Мікропрограма SSD компенсує це через вирівнювання зношування (розподіл записів рівномірно по всіх комірках), надлишкове забезпечення (резервування додаткової ємності для заміни зношених комірок) та коди виправлення помилок (виявлення та виправлення бітових помилок). Незважаючи на ці заходи, обмежена витривалість запису флеш-пам'яті є реальним обмеженням — інтенсивно записуваний SSD з часом зноситься, хоча сучасні споживчі SSD зазвичай слугують багато років за звичайного використання.Те, як технологія пам'яті дозволяє знаходити та отримувати дані, має глибокий вплив на продуктивність.Довільний доступ означає, що будь-яка комірка пам'яті може бути прочитана або записана за однаковий час, незалежно від того, до якої комірки зверталися раніше. Немає потреби проходити через проміжні комірки, щоб дістатися до цільової адреси. SRAM і DRAM є справжніми пам'ятями з довільним доступом — схема декодування адрес безпосередньо вибирає цільовий рядок і стовпець, і кожне звернення займає однакову кількість тактових циклів (якщо не враховувати ефекти кешу). Саме звідси походить термін «RAM» (Random-Access Memory — пам'ять з довільним доступом), хоча в сучасному вжитку «RAM» майже завжди означає саме DRAM.Усередині мікросхеми DRAM комірки пам'яті розташовані у двовимірній сітці рядків і стовпців. Щоб отримати доступ до конкретного біта, контролер пам'яті спершу надсилає адресу рядка (активуючи цілий рядок комірок, копіюючи їх у буфер рядка), а потім надсилає адресу стовпця (вибираючи конкретну комірку в активованому рядку). Ця схема «спершу рядок, потім стовпець» пояснює, чому доступ до DRAM має дві окремі часові складові: затримка RAS (Row Address Strobe — строб адреси рядка) та затримка CAS (Column Address Strobe — строб адреси стовпця). Доступ до іншого стовпця в тому самому рядку є швидшим за доступ до нового рядка, оскільки рядок уже активований — саме це є першопричиною переваги продуктивності послідовних шаблонів доступу до пам'яті навіть у пам'яті «з довільним доступом».Послідовний доступ означає, що дані потрібно читати по порядку — щоб дістатися до конкретного місця, потрібно пройти через усі попередні місця. Канонічним прикладом є магнітна стрічка. Щоб прочитати дані посередині або наприкінці стрічки, привід повинен фізично промотати всю стрічку перед ними, що може тривати від секунд до хвилин залежно від позиції. Це робить стрічку непридатною для довільних пошуків, але цілком прийнятною для її основного призначення: резервного копіювання та архівного зберігання, де дані записуються послідовно та зчитуються у тому самому порядку (або взагалі не зчитуються, в ідеалі — найкраща резервна копія — це та, яку ніколи не потрібно відновлювати).<br>Прямий доступ (іноді називають блоковим доступом) знаходиться між довільним і послідовним. Пристрій може перейти безпосередньо до загальної області або блоку, а потім звертатися до даних послідовно всередині цього блоку. Жорсткі диски є основним прикладом: головка читання/запису може <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Hard_disk_drive_performance_characteristics#Seek_time" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Hard_disk_drive_performance_characteristics#Seek_time" target="_self">здійснити пошук</a> до будь-якої доріжки на диску (прямий доступ до блоку), але потім повинна чекати, поки потрібний сектор повернеться під головку (послідовний доступ усередині доріжки). Типовий диск на 7 200 об/хв здійснює один оберт приблизно за 8,3 мілісекунди, тому середня ротаційна затримка (очікування потрібного сектору) становить половину цього — близько 4,2 мс. Час пошуку (переміщення головки на потрібну доріжку) в середньому становить 5–10 мс. Разом одне довільне зчитування з HDD займає приблизно 10–15 мс, тому продуктивність HDD характеризується як часом пошуку (компонент прямого доступу), так і ротаційною затримкою (послідовний компонент).Оптичні приводи (CD, DVD, Blu-ray) працюють аналогічно: лазерний вузол може перейти до будь-якої доріжки на спіралі, але потім зчитує послідовно вздовж цієї спіралі. Оптичні приводи зазвичай повільніші за HDD, оскільки диск обертається з нижчою швидкістю, а механізм пошуку є менш точним.SSD є цікавим випадком: оскільки вони побудовані на мікросхемах флеш-пам'яті, що підтримують довільний доступ на рівні сторінки, SSD забезпечують справжній довільний доступ без часу пошуку чи ротаційної затримки. Це найбільша перевага SSD над HDD в плані продуктивності, що пояснює, чому заміна HDD на SSD дає таке разюче покращення чуйності — операційна система виконує багато малих довільних зчитувань (завантаження файлів програм, читання конфігураційних даних, доступ до метаданих), і шаблон доступу, який паралізує HDD, безусильно обробляється SSD.Тип доступу пристрою пам'яті тісно пов'язаний з тим, як ЦП з ним взаємодіє. Зв'язок ЦП з пам'яттю спирається на три типи шин для з'єднання процесора з пристроями пам'яті. Адресна шина несе адресу комірки пам'яті, до якої звертаються (ширина цієї шини визначає максимальний обсяг адресованої пам'яті — 32-бітна адресна шина може адресувати 2^32 = 4 ГБ). Шина даних несе самі дані, що зчитуються або записуються (її ширина визначає, скільки бітів можна передати за один цикл — 64 біти в сучасних системах). Шина керування несе сигнали, що координують операцію: команди читання/запису, тактові сигнали, лінії вибору мікросхеми та сигнали підтвердження. Для пам'яті з довільним доступом адресна шина вибирає комірку, шина керування визначає операцію, а шина даних передає результат — усе за кілька наносекунд. Для послідовних або блокових пристроїв додаткові протоколи обробляють складніші шаблони доступу.Фізичне розташування пам'яті в системі визначає її швидкість, ємність і роль.Пам'ять на кристалі виготовляється безпосередньо на кристалі ЦП. Сюди належать:
Регістри: Найшвидше сховище в системі, безпосередньо з'єднане з виконавчими блоками з нульовою затримкою (у тактових циклах) для більшості операцій. Процесор x86-64 має 16 регістрів загального призначення (64 біти кожен), а також сегментні регістри, регістр FLAGS, вказівник інструкцій та різні спеціалізовані регістри — загалом кілька сотень байтів. Незважаючи на мізерну ємність, регістри є найкритичнішою пам'яттю в системі, оскільки це єдине сховище, з яким АЛП (арифметико-логічний пристрій) може працювати безпосередньо.
Кеш L1: Зазвичай 32–64 КБ на ядро, розділений на кеш інструкцій (L1i) та кеш даних (L1d). Затримка доступу: ~1 наносекунда (3–5 тактових циклів). Поділ на кеш інструкцій та даних дозволяє ЦП одночасно отримувати інструкцію та завантажувати дані, що було б неможливим з єдиним уніфікованим кешем.
Кеш L2: Зазвичай від 256 КБ до 1 МБ на ядро, зазвичай уніфікований (зберігає як інструкції, так і дані). Затримка доступу: ~3–5 наносекунд (10–15 циклів).
Розташування на тому ж кристалі, що й ЦП, означає, що ці типи пам'яті взаємодіють через надзвичайно короткі та широкі внутрішні шини без затримок зовнішнього з'єднання. Фізична відстань між регістром та АЛП може становити менше міліметра. Це має більше значення, ніж можна було б очікувати: електричні сигнали поширюються приблизно з двома третинами швидкості світла у міді, тобто близько 20 сантиметрів за наносекунду. За тактової частоти 4 ГГц (один цикл = 0,25 нс) сигнал може подолати лише близько 5 сантиметрів за один тактовий цикл. Це фізичне обмеження є фундаментальною причиною, чому пам'ять на кристалі повинна бути малою та близькою до виконавчих блоків — сама швидкість світла обмежує відстань, яку дані можуть подолати протягом одного тактового циклу.Пам'ять на платі розташована на материнській платі, але не на кристалі ЦП:
Кеш L3: У сучасних процесорах L3 зазвичай також знаходиться на кристалі ЦП (спільний для всіх ядер), але в старіших конструкціях або чіплетних архітектурах він може бути на окремому кристалі в корпусі ЦП. Обсяг коливається від 8 МБ до 96 МБ і більше (AMD Ryzen 7 5800X3D, зокрема, має 96 МБ кешу L3 завдяки технології вертикального нарощування 3D V-Cache). Затримка доступу: ~10–15 наносекунд.
<br>Основна пам'ять (DRAM): Встановлюється у роз'єми <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/DIMM" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/DIMM" target="_self">DIMM</a> (Dual In-line Memory Module, модуль пам'яті) на материнській платі. DIMM — це невелика друкована плата з кількома мікросхемами DRAM, що вставляється у стандартизований роз'єм. Ємність коливається від 4 ГБ до 256 ГБ і більше в споживчих системах, а серверні системи підтримують ще більше. Затримка доступу: ~50–100 наносекунд — приблизно у 50–100 разів повільніше за кеш L1. Поточний стандарт покоління — DDR5 (Double Data Rate 5), який передає дані як на передньому, так і на задньому фронті тактового сигналу, фактично подвоюючи швидкість передачі даних порівняно з тактовою частотою.
Зв'язок між ЦП та DRAM здійснюється через шину пам'яті (також звану шиною переднього плану (front-side bus) у старіших системах або каналом пам'яті в сучасних архітектурах). Ця шина є значно вужчою та повільнішою за внутрішньокристальні з'єднання, і сигнали повинні пройти через корпус ЦП, доріжки друкованої плати материнської плати, роз'єм DIMM і доріжки на самому модулі DIMM — загальна відстань 10–20 сантиметрів. При швидкості світла у міді один такий шлях туди й назад займає близько 1–2 наносекунд, але накладні витрати протоколу (налаштування адреси, активація рядка, затримка CAS, передача даних) додають значно більше. Ці фізичні та протокольні накладні витрати пояснюють великий розрив у затримці між кешами на кристалі та зовнішньою DRAM.Зовнішня пам'ять підключається до системи через інтерфейси введення-виведення:
SSD та HDD: Підключаються через SATA (до 600 МБ/с), NVMe через PCIe (до 7 000+ МБ/с для PCIe 4.0, 14 000+ МБ/с для PCIe 5.0) або інтерфейси USB. Затримки доступу коливаються від ~25 мікросекунд (довільне зчитування NVMe SSD) до ~10 мілісекунд (HDD з пошуком). Різниця між SATA SSD та NVMe SSD є суттєвою: NVMe обходить накладні витрати протоколу SATA (який був розроблений для обертових дисків) і взаємодіє безпосередньо через шину PCIe, досягаючи значно вищої пропускної здатності та нижчої затримки.
Оптичні приводи: Підключаються через SATA або USB.
Стрічкові накопичувачі: Підключаються через SAS або Fibre Channel.
Мережеве сховище: NAS (Network-Attached Storage, мережеве сховище) та хмарне сховище доступні через мережеві протоколи, що додають мережеву затримку поверх затримки сховища. NAS у локальній мережі гігабітний Ethernet може додати ~0,5 мс мережевих витрат. Хмарне сховище (Amazon S3, Google Cloud Storage) додає затримку інтернет-подорожі у обидва боки — зазвичай 10–100 мс залежно від географічної відстані. Незважаючи на високу затримку, хмарне сховище пропонує фактично необмежену ємність та автоматичну надлишковість, що робить його домінуючим вибором для даних, до яких не потрібен доступ на швидкості ЦП.
Кожен рівень далі від ЦП додає більшу затримку, більшу ємність, нижчу вартість за біт і більшу фізичну відстань від процесора. Це ієрархія пам'яті у фізичній формі — і це повторювана тема протягом усього курсу. Ієрархію можна концептуально продовжити ще далі: хмарне сховище додає ще один рівень за межами локального зовнішнього сховища, з затримками, що вимірюються десятками мілісекунд, але ємністю, що вимірюється петабайтами.Перетинаючи категорії вище, технології пам'яті також можна згрупувати за фізичним явищем, що використовується для зберігання даних.<br>Напівпровідникова пам'ять зберігає дані за допомогою електронних схем, виготовлених на кремнієвих пластинах, зазвичай за технологією <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/CMOS" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/CMOS" target="_self">CMOS</a> (Complementary Metal-Oxide-Semiconductor — комплементарна структура метал-оксид-напівпровідник). Основні типи напівпровідникової пам'яті:
SRAM (бістабільні транзисторні засувки)
DRAM (заряди конденсаторів)
Flash/EEPROM (захоплені електрони у плаваючих затворах)
ROM/PROM (жорстко з'єднані або пропалені транзисторні з'єднання)
Напівпровідникова пам'ять домінує на верхніх рівнях ієрархії, оскільки не має рухомих частин, працює на електронних швидкостях (наносекунди) і може безпосередньо адресуватися ЦП. Усі мікросхеми напівпровідникової пам'яті мають спільну фізичну структуру: двовимірний масив комірок пам'яті з периферійними схемами для декодування адрес (декодери рядків і стовпців), підсилювачами зчитування (для виявлення крихітних сигналів від окремих комірок) та буферами введення-виведення. Фундаментальною одиницею є комірка пам'яті — незалежно від того, чи це шеститранзисторна комірка SRAM, комірка DRAM з одним транзистором і одним конденсатором, чи комірка флеш-пам'яті з плаваючим затвором.Магнітна пам'ять зберігає дані як шаблони намагніченості на феромагнітній поверхні:
Жорсткі диски (HDD): Один або кілька обертових дисків, покритих магнітним матеріалом, зчитування/запис здійснюється крихітною головкою, що летить на відстані нанометрів над поверхнею. Щільність у сучасних дисках перевищує 1 терабіт на квадратний дюйм. Головка читання/запису не торкається диска — вона летить на повітряній подушці лише 5–10 нанометрів над поверхнею (для порівняння: людська волосина має товщину близько 75 000 нанометрів). Ця інженерна точність пояснює, чому навіть невеликий удар по HDD під час його роботи може спричинити контакт головки з поверхнею диска, знищуючи дані — це явище називається «зіткнення головки» (head crash).
<br>Магнітна стрічка: Довга тонка смуга пластику, покрита магнітним матеріалом, намотана на котушки або розміщена в картриджах. Незважаючи на те, що це одна з найстаріших технологій зберігання (яка бере початок від <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/UNIVAC_I" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/UNIVAC_I" target="_self">UNIVAC I</a> 1951 року), стрічка залишається в активному використанні для архівного та резервного зберігання завдяки неперевершеній вартості за гігабайт та довговічності — стрічкові картриджі можуть надійно зберігати дані 30 років і більше. Сучасні картриджі LTO (Linear Tape-Open) можуть зберігати до 18 ТБ на стрічку (або 45 ТБ у стисненому вигляді) за вартістю приблизно $0,01 за гігабайт — на порядок дешевше за HDD і на два порядки дешевше за SSD.
Оптична пам'ять зберігає дані як фізичні шаблони на поверхні диска, зчитувані лазером:
CD (Compact Disc): Ємність ~700 МБ, використовує інфрачервоний лазер (довжина хвилі 780 нм).
DVD (Digital Versatile Disc): 4,7 ГБ (одношаровий) до 8,5 ГБ (двошаровий), використовує червоний лазер (650 нм).
Blu-ray Disc: 25 ГБ (одношаровий) до 50 ГБ (двошаровий), використовує синьо-фіолетовий лазер (405 нм). Коротша довжина хвилі дозволяє щільніше розташування западин і вищу щільність запису.
Кожен оптичний формат існує у трьох варіантах:
Тільки для читання (ROM): Виготовляються на заводі методом пресування з западинами, штампованими з майстер-диску — за принципом, подібним до вінілових платівок. Це комерційні CD, DVD та Blu-ray, які ви купуєте.
Одноразовий запис (R): Використовують шар фарби, який назавжди змінюється потужним лазером під час запису. Після запису дані не можна змінити.
Перезаписувані (RW/RE): Використовують матеріал зі зміною фази, який може перемикатися між кристалічним (відбивним) та аморфним (менш відбивним) станами, дозволяючи стирати та перезаписувати диск сотні або тисячі разів.
Прогресія назв — інфрачервоний, червоний, синій — відображає фізичний принцип: коротші довжини хвилі можуть розрізняти менші елементи, забезпечуючи вищу щільність даних. Хоча оптичні носії здебільшого витіснені USB-накопичувачами та стрімінговими сервісами для споживчого використання, вони залишаються актуальними в контексті архівування: пресовані Blu-ray диски є надзвичайно довговічними, стійкими до магнітних полів і можуть зберігатися десятиліттями.Нові технології, які не вписуються чітко в три категорії вище, включають:
MRAM (Magnetoresistive RAM, магніторезистивна пам'ять): Використовує магнітні тунельні переходи — напівпровідникова технологія, яка зберігає дані магнітно. Поєднує аспекти напівпровідникового (виготовлення на кремнії, наносекундний доступ) та магнітного (енергонезалежне, зберігає дані як магнітну орієнтацію) сховища.
PCM (Phase-Change Memory, пам'ять зі зміною фази): Використовує халькогенідне скло, яке може перемикатися між аморфною та кристалічною фазами шляхом нагрівання електричними імпульсами. Той самий принцип зміни фази, що використовується в перезаписуваних оптичних дисках, але реалізований у нанорозмірі на кремнієвому чіпі.
ReRAM (Resistive RAM, резистивна пам'ять): Зберігає дані шляхом зміни опору тонкої плівки. Ще здебільшого перебуває на стадії досліджень та раннього виробництва, але є перспективною для застосувань, що вимагають щільної, енергонезалежної та швидкої пам'яті.
Ці технології являють собою фронтир досліджень пам'яті, кожна з яких намагається поєднати найкращі властивості існуючих технологій — швидкість SRAM, щільність DRAM та енергонезалежність флеш-пам'яті — в одному пристрої.Кілька спостережень щодо цієї таблиці:
Стовпець затримки охоплює приблизно 12 порядків від регістрів (&lt;1 нс) до стрічки (хвилини). Це один із найбільших динамічних діапазонів у всій інженерії — порівнянний з різницею між діаметром атома та діаметром Землі.
Вартість за гігабайт і ємність обернено корелюють зі швидкістю, що є економічною силою, яка рухає ієрархію пам'яті. Якби всі технології пам'яті коштували однаково, ми б просто використовували найшвидшу. Ієрархія існує саме тому, що це не так.
Рядок «витривалість» (цикли запису до виходу з ладу) є релевантним переважно для флеш-пам'яті, де це реальне обмеження проєктування. Усі інші технології у таблиці можуть бути перезаписані фактично необмежену кількість разів для практичних цілей.
Таблиця наочно демонструє компроміси: жодна окрема технологія не перевершує всі інші за всіма вимірами одночасно. Швидка пам'ять — дорога і мала; дешева пам'ять — велика і повільна. Саме тому існує ієрархія пам'яті — вона дозволяє системі створювати ілюзію швидкої, великої та дешевої пам'яті, автоматично утримуючи часто використовувані дані на швидших рівнях і переміщуючи рідко використовувані дані на повільніші.Зверніть також увагу на величезні розриви в затримці між рівнями. Перехід від кешу L1 (1 нс) до DRAM (100 нс) — це уповільнення у 100 разів. Перехід від DRAM до HDD (10 мс) — це уповільнення у 100 000 разів. Якщо перевести в людські терміни: якби доступ до кешу L1 тривав 1 секунду (час, щоб взяти книгу з полиці біля вашого столу), доступ до DRAM тривав би близько 1,5 хвилини (прогулянка до сусідньої кімнати), а доступ до HDD — близько 115 днів (подорож через океан і назад). Ці разючі співвідношення пояснюють, чому кешування та віртуальна пам'ять є настільки критично важливими — навіть невелике покращення коефіцієнта влучань кешу або скромне зменшення кількості сторінкових помилок перетворюється на величезний приріст продуктивності, оскільки штраф за промах є настільки суворим.Вперше ви познайомилися з віртуальною пам'яттю в L07, у контексті захищеного режиму: процесор 80386 додав блок сторінкування, який поділяє пам'ять на сторінки розміром 4 КБ, використовує TLB (буфер асоціативної трансляції) з коефіцієнтом влучань понад 99% для прискорення трансляцій та підтримує сторінкування за запитом, завдяки чому не вся пам'ять програми має фізично перебувати в оперативній пам'яті одночасно. Те введення пояснювало, що таке сторінкування і чому його додали до захищеного режиму. Тепер ми зануримось глибше в механіку: як саме віртуальні адреси покроково транслюються у фізичні адреси, що відбувається, коли сторінка відсутня в пам'яті, як багаторівневі таблиці сторінок роблять цю схему практичною, і як апаратне та програмне забезпечення співпрацюють для прозорого керування пам'яттю. Наприкінці цього розділу ви зможете простежити повний шлях звернення до пам'яті від віртуальної адреси до фізичного розташування, включно з винятковим випадком, коли дані взагалі відсутні в оперативній пам'яті.Щоб зрозуміти, навіщо було винайдено віртуальну пам'ять, розгляньте альтернативу — систему без неї. На зорі обчислювальної техніки, а також у середовищі реального режиму, в якому ви програмуєте на лабораторних роботах, програми звертаються до фізичної пам'яті напряму. Якщо ваша програма записує за адресою 0x8000, вона записує у фізичний байт 0x8000 в мікросхемах оперативної пам'яті. Це створює кілька серйозних проблем у міру ускладнення систем:Проблема 1: Відсутність ізоляції між програмами. Якщо дві програми завантажені в пам'ять одночасно, ніщо не заважає одній читати чи перезаписувати дані іншої. Програма з помилкою може пошкодити саму операційну систему — або ще гірше, зловмисна програма може навмисно читати паролі та приватні дані з пам'яті інших програм. У вашому середовищі DOSBox ви можете (якщо будете необережні) перезаписати таблицю векторів переривань за адресою 0000:0000 і обвалити всю систему — і немає жодного механізму, який би цьому запобіг. У багатокористувацькій системі (університетський сервер, хмарна обчислювальна платформа) це катастрофа безпеки та надійності.Проблема 2: Фрагментація пам'яті. Припустімо, у вас є 16 МБ оперативної пам'яті та завантажені три програми: Програма A займає адреси 0–4 МБ, Програма B — 4–8 МБ, а Програма C — 8–10 МБ. Тепер Програма B завершує роботу, звільняючи діапазон 4–8 МБ. Ви хочете завантажити Програму D, якій потрібно 6 МБ неперервної пам'яті — але найбільший неперервний вільний блок становить лише 4 МБ (де була B). Область у 6 МБ зверху (10–16 МБ) доступна, але Програмі D потрібно 6 МБ неперервно. Загальний обсяг вільної пам'яті (10 МБ) більш ніж достатній, але вона фрагментована на нерозташовані поруч частини. Без віртуальної пам'яті довелося б фізично переміщувати Програму C у пам'яті для об'єднання вільного простору — це повільна операція, яка також вимагає оновлення кожного вказівника й адреси в Програмі C, що фактично неможливо, якщо програма працює.Ця проблема називається зовнішньою фрагментацією, і вона переслідує кожну систему, яка виділяє неперервну фізичну пам'ять для процесів. З часом, коли програми запускаються і завершуються, пам'ять стає дедалі більш фрагментованою, доки великі виділення стають неможливими, попри достатній загальний обсяг вільної пам'яті. Віртуальна пам'ять повністю усуває цю проблему: оскільки відображення віртуальних адрес на фізичні виконується на рівні сторінок, ОС може зібрати «неперервне» віртуальне виділення з фізично розкиданих фреймів, і програма про це навіть не дізнається.Проблема 3: Програми обмежені обсягом фізичної оперативної пам'яті. Якщо у вашій машині 512 МБ оперативної пам'яті, жодна окрема програма не може використати більше 512 МБ (мінус те, що споживають ОС та інші програми). Для застосунків на кшталт відеоредагування, наукових симуляцій або великих баз даних це паралізуюче обмеження. До появи віртуальної пам'яті програмісти мусили вручну розділяти великі програми на «оверлеї» — секції коду, які явно завантажувалися й вивантажувалися з пам'яті програмістом. Це було виснажливо, схильно до помилок і покладало величезний тягар на програмістів щодо ручного керування пам'яттю. Віртуальна пам'ять повністю автоматизує цей процес.Проблема 4: Конфлікти адрес. Кожна програма має бути скомпільована або зв'язана для роботи за конкретною адресою пам'яті (або бути позиційно-незалежною, що додає складності та накладних витрат). Якщо дві програми скомпільовані для запуску з адреси 0x10000, вони не можуть працювати одночасно без переміщення.<br>Проблема 5: Відсутність детального захисту пам'яті. Навіть якщо вдасться ізолювати програми в окремі фізичні області, немає механізму для забезпечення дозволів на рівні сторінок. Не можна позначити певні сторінки як доступні лише для читання (щоб захистити код від перезапису), або як невиконувані (щоб запобігти виконанню буферів даних як коду — поширена вразливість безпеки, відома як <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Buffer_overflow" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Buffer_overflow" target="_self">атака переповнення буфера</a>). Без апаратного захисту на рівні сторінок операційна система не може запобігти модифікації структур даних ядра користувацькою програмою або виконанню довільного коду, впровадженого в буфер даних. Записи таблиці сторінок у системі сторінкування містять біти дозволів для кожної сторінки, що дозволяє апаратному забезпеченню забезпечувати обмеження читання/запису/виконання під час кожного звернення до пам'яті — основа безпеки сучасних операційних систем.<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Virtual_memory" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Virtual_memory" target="_self">Віртуальна пам'ять</a> розв'язує всі п'ять проблем за допомогою єдиного елегантного механізму: непрямої адресації. Замість того, щоб дозволяти програмам звертатися до фізичної пам'яті напряму, система встановлює прошарок трансляції. Кожна програма працює у власному віртуальному адресному просторі — приватному, неперервному діапазоні адрес, що належить лише цій програмі. Апаратне забезпечення транслює кожне звернення до пам'яті з віртуальної адреси, яку використовує програма, у фізичну адресу, де дані фактично розміщуються в оперативній пам'яті.З точки зору програми, вона має великий, неперервний, приватний простір пам'яті, що починається з адреси 0 (або, точніше, з найнижчої доступної користувачу адреси — точне розташування залежить від ОС). З точки зору апаратного забезпечення, дані програми можуть бути розкидані по нерозташованих поруч фізичних фреймах, або навіть взагалі не перебувати в оперативній пам'яті (вони можуть бути на диску, очікуючи завантаження за запитом). Прошарок трансляції підтримує цю ілюзію прозоро — програма ніколи не повинна знати, де фізично розміщуються її дані, і вона не може виявити чи втрутитися в пам'ять інших програм.Це той самий принцип абстракції, який ви бачили в інших контекстах: так само як адресація сегмент:зміщення в реальному режимі абстрагувала фізичні адреси за прошарком сегментації, віртуальна пам'ять абстрагує фізичну пам'ять за прошарком сторінкування. Різниця в тому, що сторінкування є значно потужнішим і гнучкішим.<br>Коротка історична довідка. Концепцію віртуальної пам'яті вперше запропонував німецький фізик <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Fritz-Rudolf_G%C3%BCntsch" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Fritz-Rudolf_G%C3%BCntsch" target="_self">Фріц-Рудольф Гюнч</a> у своїй докторській дисертації 1956 року. Перша робоча реалізація з'явилася в комп'ютері <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Atlas_(computer)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Atlas_(computer)" target="_self">Atlas</a> в Манчестерському університеті у 1962 році, розробленому Томом Кілберном та його командою. Atlas мав лише 16 384 слова швидкої магнітної пам'яті на осердях (еквівалент оперативної пам'яті), але 98 304 слова повільнішої пам'яті на магнітному барабані (еквівалент диска). Його система «одноrівневого зберігання» автоматично переміщувала дані між двома рівнями, створюючи для програмістів ілюзію єдиної, великої, швидкої пам'яті. Супервізор Atlas — одна з найперших операційних систем — здійснював усі передачі прозоро, включно з рішеннями про заміщення сторінок. Це була революційна ідея на той час, і по суті це той самий принцип, який використовує кожна сучасна операційна система понад шістдесят років потому. Розуміння команди Atlas того, що апаратне й системне програмне забезпечення можуть співпрацювати для створення корисної абстракції, стало однією з найвпливовіших ідей в історії обчислювальної техніки.Найпоширенішою реалізацією віртуальної пам'яті є сторінкування, яке поділяє і віртуальний, і фізичний адресний простір на блоки фіксованого розміру. (Альтернативний підхід — сегментація — поділяє пам'ять на сегменти змінного розміру відповідно до логічної структури програми: сегмент коду, сегмент даних, сегмент стека. Архітектура x86 підтримує обидва підходи, але, як ми обговорювали, сучасні операційні системи використовують переважно сторінкування, оскільки сторінки фіксованого розміру уникають зовнішньої фрагментації.)Сторінки та фрейми. Віртуальний адресний простір поділяється на сторінки, а фізичний адресний простір — на фрейми (також звані «сторінкові фрейми»). Сторінки та фрейми завжди мають однаковий розмір — у системах x86 стандартний розмір сторінки становить 4 КБ (4 096 байтів = 2^12 байтів), хоча для спеціалізованих випадків підтримуються й більші розміри сторінок (2 МБ «великі сторінки» та 1 ГБ «величезні сторінки»).При використанні сторінок по 4 КБ, 32-бітний віртуальний адресний простір (4 ГБ) містить рівно 1 048 576 сторінок (2^20), а система з 4 ГБ фізичної оперативної пам'яті містить таку саму кількість фреймів. Але ось ключове розуміння: кількість віртуальних сторінок може значно перевищувати кількість фізичних фреймів, адже не всі віртуальні сторінки мають перебувати в оперативній пам'яті одночасно.Чому саме 4 КБ? Розмір сторінки — це компроміс проєктування. Менші сторінки зменшують внутрішню фрагментацію (марно витрачений простір усередині сторінки — у середньому остання сторінка будь-якого виділення витрачає половину сторінки), забезпечують точніший захист пам'яті та дозволяють ОС завантажувати лише конкретно потрібні дані. Але менші сторінки означають більше записів у таблиці сторінок (що споживає більше пам'яті), більший тиск на TLB (кожний запис покриває менший адресний простір) і більші накладні витрати на дисковий ввід-вивід (кожна сторінкова помилка завантажує менше даних за одне звернення до диска). Більші сторінки зменшують ці накладні витрати, але марнують більше пам'яті через внутрішню фрагментацію. Розмір 4 КБ, обраний розробниками Intel 80386 у середині 1980-х, являв собою збалансований компроміс, і він зберігається як стандартний з того часу — хоча, як зазначено вище, великі сторінки по 2 МБ та 1 ГБ доступні для навантажень, де компроміс схиляється на користь більшої гранулярності.Трансляція адрес. Серце сторінкування — це трансляція з віртуальних адрес у фізичні. Ось як це працює для 32-бітної системи зі сторінками по 4 КБ:32-бітна віртуальна адреса розбивається на два поля:Номер сторінки визначає, до якої сторінки належить адреса. Зміщення визначає конкретний байт усередині цієї сторінки. Оскільки сторінки мають розмір 4 КБ = 2^12 байтів, для зміщення потрібно рівно 12 бітів.Процесор використовує номер віртуальної сторінки для пошуку відповідного номера фізичного фрейму в структурі даних, яка називається таблицею сторінок. Кожен процес має власну таблицю сторінок, що підтримується операційною системою. Запис таблиці сторінок для даної віртуальної сторінки містить:
Номер фізичного фрейму, в якому ця сторінка наразі розміщена в оперативній пам'яті
Біт присутності, що вказує, чи перебуває сторінка наразі у фізичній пам'яті
Біти дозволів (читання, запис, виконання) для захисту пам'яті
Біт доступу (встановлюється апаратно при читанні або записі сторінки) та біт модифікації (встановлюється апаратно при записі на сторінку) — обидва використовуються ОС для прийняття рішень щодо заміщення сторінок
Біт користувач/супервізор, що вказує, чи доступна сторінка коду користувацького режиму, чи лише коду ядра
Після знаходження номера фрейму фізична адреса формується шляхом поєднання номера фрейму з початковим зміщенням:Зміщення ідентичне у віртуальній і фізичній адресах — це прямий наслідок того, що сторінки й фрейми мають однаковий розмір. Під час трансляції змінюється лише номер сторінки/фрейму. Це елегантне рішення: біти зміщення проходять через трансляцію без змін, і лише старші біти (номер сторінки) потребують пошуку. Це означає, що TLB потрібно зберігати відображення лише для номерів сторінок, а не для кожної окремої адреси байта.<br><img alt="Трансляція адрес при сторінкуванні" src="assets/l08-paging-address-translation.jpeg" target="_self">Трансляція адрес через сторінкування: процесор генерує логічну адресу (номер сторінки + зміщення), таблиця сторінок відображає її на номер фізичного фрейму, а зміщення вибирає байт усередині фрейму.Процес трансляції адрес при сторінкуванні: процесор генерує логічну адресу, яка розбивається на номер сторінки та зміщення. Номер сторінки індексує таблицю сторінок для отримання відповідного номера фрейму. Номер фрейму поєднується з початковим зміщенням для формування фізичної адреси, яка використовується для доступу до основної пам'яті.Конкретний приклад. Припустімо, ваша програма звертається до віртуальної адреси 0x00403A7C. У двійковому представленні старші 20 бітів — це 0x00403 (номер сторінки = 1 027), а молодші 12 бітів — 0xA7C (зміщення = 2 684). Процесор шукає сторінку 1 027 у таблиці сторінок і знаходить, що вона відображається на фізичний фрейм 5 891 (0x01703). Результуюча фізична адреса — 0x01703A7C — фрейм 5 891, зміщення 2 684. Програма навіть не здогадується, що ця трансляція відбулася; вона просто виконала інструкцію MOV, спрямовану на 0x00403A7C, а апаратне забезпечення прозоро перенаправило її.Багаторівневі таблиці сторінок. Зі схемою, описаною вище, існує практична проблема. Плоска таблиця сторінок для 32-бітного адресного простору зі сторінками по 4 КБ потребує 2^20 = 1 048 576 записів. Якщо кожен запис займає 4 байти, сама таблиця сторінок споживає 4 МБ неперервної фізичної пам'яті — для кожного процесу. При десятках або сотнях процесів, що працюють у сучасній системі, це неприйнятно. Розв'язком є багаторівневі таблиці сторінок. Замість однієї великої плоскої таблиці, номер віртуальної сторінки розбивається на два (або більше) індекси. У 32-бітній архітектурі x86, 20-бітний номер сторінки розбивається на 10-бітний індекс каталогу сторінок і 10-бітний індекс таблиці сторінок. Каталог сторінок (1 024 записи) вказує на таблиці сторінок, а кожна таблиця сторінок (1 024 записи) вказує на фізичні фрейми. Більшість таблиць сторінок для невикористаних областей адресного простору взагалі не потрібно створювати — запис каталогу сторінок просто позначається як «відсутній». Це зазвичай зменшує загальну пам'ять таблиць сторінок із 4 МБ до кількох десятків кілобайтів для типового процесу, який використовує лише малу частку свого 4 ГБ віртуального адресного простору.У 64-бітних системах (x86-64) адресний простір настільки величезний (2^48 байтів = 256 ТБ у поточних реалізаціях), що використовуються чотири рівні таблиць сторінок: PML4 (Page Map Level 4), PDPT (Page Directory Pointer Table), PD (Page Directory) та PT (Page Table). Кожен рівень додає 9 бітів індексації, плюс 12 бітів для зміщення, що дає 9+9+9+9+12 = 48 бітів. Новітні процесори Intel та AMD підтримують п'ятий рівень (PML5), що розширює віртуальний адресний простір до 2^57 байтів = 128 ПБ, хоча жодна сучасна система не має навіть близько такого обсягу фізичної пам'яті. Багаторівнева схема забезпечує те, що навіть із цими величезними віртуальними адресними просторами, фактична пам'ять, що споживається таблицями сторінок, залишається пропорційною пам'яті, яку процес реально використовує, а не розміру адресного простору, який він теоретично міг би адресувати.Описана вище трансляція працює безперебійно, коли цільова сторінка присутня у фізичній оперативній пам'яті. Але що відбувається, коли її там немає?Сторінкова помилка. Коли процесор шукає віртуальну сторінку в таблиці сторінок і виявляє, що біт присутності дорівнює 0, сторінка наразі відсутня у фізичній пам'яті. Процесор генерує виняткову ситуацію сторінкової помилки — спеціальний тип переривання (а саме виняток 14 на x86). Це передає керування обробнику сторінкових помилок операційної системи — процедурі ядра, спеціально розробленій для обробки цієї ситуації.Обробник сторінкових помилок визначає подальші дії на основі адреси, що спричинила помилку, та карти пам'яті процесу: Допустима сторінка, ще не завантажена (сторінкування за запитом): Сторінка існує на диску (у виконуваному файлі програми або в просторі підкачки), але ще не була завантажена в оперативну пам'ять. ОС виділяє вільний фізичний фрейм, зчитує вміст сторінки з диска в цей фрейм, оновлює запис таблиці сторінок, щоб він вказував на новий фрейм, і встановлює біт присутності в 1, після чого перезапускає інструкцію, що спричинила помилку. Програма ніколи не дізнається, що помилка сталася — вона лише зазнала короткої затримки. Допустима сторінка, вивантажена на диск: Сторінка раніше була в оперативній пам'яті, але була витіснена в простір підкачки на диску, щоб звільнити фрейм для іншого процесу. ОС завантажує її назад із підкачки, так само як у випадку 1. Недопустимий доступ: Віртуальна адреса не відповідає жодній виділеній пам'яті. Це справжня помилка — програма звернулася до пам'яті, до якої не повинна була. У Linux ОС доставляє сигнал помилки сегментації (SIGSEGV) процесу, зазвичай завершуючи його. У Windows це спричиняє помилку «Access Violation». Це та сама страхітлива помилка сегментації, з якою ви, можливо, вже стикалися (або ще зіткнетеся) при програмуванні мовою C. Покрокове проходження сторінкової помилки. Розгляньмо, що відбувається, коли ваша програма викликає функцію, яка ще не виконувалась. Код функції розміщений на сторінці, яка ніколи не завантажувалась в оперативну пам'ять (сторінкування за запитом означає, що ОС не завантажує сторінки, доки вони не стануть дійсно потрібними):
Процесор завантажує інструкцію за віртуальною адресою функції.
MMU (блок керування пам'яттю) шукає номер віртуальної сторінки в TLB — промах.
MMU проходить таблицю сторінок у пам'яті — виявляє, що біт присутності дорівнює 0.
Процесор генерує виняток 14 (сторінкова помилка), зберігаючи адресу, що спричинила помилку, в регістрі CR2.
Запускається обробник сторінкових помилок ОС. Він перевіряє карту пам'яті процесу і визначає, що це допустима сторінка коду з виконуваного файлу.
ОС знаходить вільний фізичний фрейм (або витісняє сторінку-жертву, щоб звільнити місце).
ОС надсилає запит на дисковий ввід-вивід для зчитування 4 КБ сторінки з виконуваного файлу на диску.
Дискова операція завершується (цей крок сам по собі займає ~100 мікросекунд для SSD або ~10 мілісекунд для HDD — вічність у термінах процесорного часу). Під час цього очікування ОС зазвичай перемикається на виконання іншого процесу, тому процесор не простоює — він виконує корисну роботу для іншої програми, поки передача з диска відбувається у фоновому режимі.
ОС оновлює запис таблиці сторінок: встановлює номер фрейму, встановлює біт присутності в 1 і позначає сторінку як доступну лише для читання (оскільки це код).
ОС повертається з обробника винятків, і процесор перезапускає інструкцію, що спричинила помилку.
Цього разу пошук у TLB все ще дає промах, але прохід таблиці сторінок завершується успішно — сторінка присутня. TLB оновлюється, й інструкція виконується нормально.
З точки зору програми, нічого незвичного не сталося — виклик функції просто зайняв трохи більше часу, ніж зазвичай (можливо, 100 мікросекунд замість кількох наносекунд, але програма не має можливості це помітити, якщо вона не вимірює час явно). З точки зору ОС, вона виконала складну послідовність операцій, що включала обробку винятків, планування дискового вводу-виводу, перемикання контексту та маніпуляції з таблицею сторінок — і все це було спричинено однією операцією завантаження інструкції. Це і є сторінкування за запитом у дії — ОС завантажує сторінки лише тоді, коли вони дійсно потрібні, уникаючи накладних витрат на завантаження всієї програми в пам'ять при запуску. Більшість програм мають значні обсяги коду, що рідко або ніколи не виконується (обробники помилок, рідко використовувані функції), і сторінкування за запитом гарантує, що ці сторінки ніколи не споживають фізичну пам'ять.<br>Підкачка та заміщення сторінок. Коли ОС потребує вільний фізичний фрейм для обслуговування сторінкової помилки, але всі фрейми зайняті, вона повинна витіснити існуючу сторінку. ОС обирає сторінку-жертву за допомогою алгоритму заміщення сторінок — зазвичай це наближення до <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Page_replacement_algorithm#Least_recently_used" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Page_replacement_algorithm#Least_recently_used" target="_self">LRU (найдавніше використаний)</a>, яке витісняє сторінку, до якої найдовше не зверталися. «Ідеальний» алгоритм (відомий як оптимальний алгоритм Беладі) витісняв би сторінку, яка не знадобиться найдовше в майбутньому — але це вимагає знання майбутніх звернень до пам'яті, що, очевидно, неможливо на практиці. LRU апроксимує це, використовуючи минуле як прогноз майбутнього: сторінка, яка не використовувалась нещодавно, ймовірно, не знадобиться незабаром.Апаратне забезпечення допомагає ОС у прийнятті цього рішення через біт доступу та біт модифікації в кожному записі таблиці сторінок. Процесор автоматично встановлює біт доступу щоразу, коли сторінка читається або записується, та встановлює біт модифікації щоразу, коли сторінка записується. ОС періодично скидає біти доступу і перевіряє, до яких сторінок відбулося повторне звернення — ті, до яких звернень не було, є кандидатами на витіснення. Сторінки зі скинутим бітом модифікації дешевші для витіснення, оскільки їх не потрібно записувати назад на диск.Якщо сторінка-жертва була змінена після завантаження (біт модифікації встановлено), її потрібно записати назад на диск, перш ніж фрейм можна буде повторно використати. Якщо вона не була змінена (біт модифікації скинуто), фрейм можна негайно повторно використати, оскільки на диску все ще є валідна копія.Область диска, що використовується для зберігання витіснених сторінок, називається простором підкачки (у Linux) або файлом підкачки (у Windows). У Linux підкачка зазвичай є виділеним розділом диска, хоча може бути й файлом. У Windows файл підкачки (pagefile.sys) — це прихований файл на системному диску. В обох випадках область підкачки виступає як розширення фізичної пам'яті, хоча й у тисячі разів повільніше.<br>Пробуксовування. Якщо система має занадто мало фізичної оперативної пам'яті для об'єднаних робочих наборів усіх запущених процесів, вона може перейти в патологічний стан, який називається <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Thrashing_(computer_science)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Thrashing_(computer_science)" target="_self">пробуксовування</a>: ОС проводить більшу частину часу, переміщуючи сторінки між оперативною пам'яттю та диском, і дуже мало часу — на фактичне виконання інструкцій програм. Кожна завантажена сторінка для одного процесу витісняє сторінку, яка іншому процесу (або навіть тому самому процесу) знадобиться майже негайно, спричиняючи ще одну сторінкову помилку. Система виглядає замороженою — індикатор диска горить безперервно, але нічого не виконується.Ви можете спостерігати пробуксовування на реальній системі: відкрийте набагато більше застосунків, ніж може вмістити ваша оперативна пам'ять, і комп'ютер стане повільним до такого ступеня, що стане майже непридатним для використання. У Linux команда vmstat показує швидкість завантаження й вивантаження сторінок; у Windows вкладка «Пам'ять» Диспетчера завдань показує використання пам'яті відносно фізичної пам'яті. Коли кількість сторінкових помилок за секунду різко зростає, а індикатор дискового вводу-виводу горить безперервно, ви спостерігаєте пробуксовування.Способи боротьби з пробуксовуванням включають:
Додати більше фізичної оперативної пам'яті — найпряміший спосіб, що дозволяє більшій кількості сторінок залишатися резидентними одночасно.
Зменшити кількість запущених процесів — менше процесів означає менше конкуруючих робочих наборів.
Оптимізувати програми для використання менше пам'яті — кращі алгоритми та структури даних можуть значно зменшити розмір робочого набору програми.
Використовувати швидший накопичувач (SSD замість HDD) — це не запобігає пробуксовуванню, але робить кожну сторінкову помилку дешевшою (100 мкс замість 10 мс), тому система деградує більш плавно.
TLB (буфер асоціативної трансляції). Якби кожне звернення до пам'яті вимагало пошуку в таблиці сторінок, накладні витрати були б руйнівними — кожне завантаження інструкції та кожне звернення до даних вимагало б додаткового читання пам'яті лише для консультації з таблицею сторінок, фактично вдвічі зменшуючи швидкість процесора. Розв'язком є TLB — невеликий, швидкий апаратний кеш, вбудований у MMU (блок керування пам'яттю) процесора, який зберігає нещодавні трансляції таблиці сторінок.Коли процесору потрібно транслювати віртуальну адресу, він спочатку перевіряє TLB. Якщо трансляція знайдена там (влучання в TLB), номер фізичного фрейму доступний негайно — жодного звернення до пам'яті не потрібно. Лише при промаху TLB процесор проходить таблицю сторінок у пам'яті, а потім кешує результат у TLB для подальшого використання.<br>TLB надзвичайно ефективний. Типовий TLB містить від 64 до 1 024 записів. При сторінках по 4 КБ навіть 512-записний TLB покриває 2 МБ пам'яті, і оскільки програми демонструють сильну <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Locality_of_reference" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Locality_of_reference" target="_self">локальність звернень</a> (вони схильні звертатися до тих самих сторінок повторно протягом коротких інтервалів часу), TLB досягає коефіцієнта влучань понад 99% на практиці. Це означає, що накладні витрати на трансляцію є нехтовними для переважної більшості звернень до пам'яті.Є одне важливе ускладнення: оскільки кожен процес має власну таблицю сторінок, записи TLB від одного процесу є недійсними для іншого. Коли ОС здійснює перемикання контексту (переключення від виконання Процесу A до виконання Процесу B), вона повинна або очистити весь TLB, або використовувати ідентифікатори адресного простору (ASID) для позначення записів TLB процесом, якому вони належать. Сучасні процесори x86 підтримують ASID (у Intel вони називаються «Process Context Identifiers» або PCID), що дозволяє записам TLB від різних процесів співіснувати, значно зменшуючи вплив перемикання контексту на продуктивність. Без ASID кожне перемикання контексту спричиняло б сплеск промахів TLB, поки новий процес відбудовує TLB з нуля — помітне зниження продуктивності в системах, які часто перемикаються між багатьма процесами.Саме тому великі сторінки (2 МБ або 1 ГБ замість 4 КБ) корисні для певних навантажень: один запис TLB покриває значно більшу область пам'яті, тому та сама кількість записів TLB може покрити набагато більше пам'яті. Бази даних та віртуальні машини, які працюють із великими, неперервними областями пам'яті, часто суттєво виграють від великих сторінок.<br><img alt="Відображення віртуальної пам'яті" src="assets/l08-virtual-memory-mapping.png" target="_self">Віртуальна пам'ять відображає віртуальний адресний простір кожного процесу на фізичну оперативну пам'ять — деякі сторінки розміщуються в оперативній пам'яті, інші вивантажені на диск.Відображення віртуальної пам'яті: кожен процес має власний віртуальний адресний простір, і операційна система відображає віртуальні сторінки на фізичні фрейми в оперативній пам'яті. Сторінки, які наразі не потрібні, можуть перебувати на диску. Відображення підтримується окремо для кожного процесу, забезпечуючи повну ізоляцію — сторінка 0 Процесу A і сторінка 0 Процесу B можуть відображатися на зовсім різні фізичні фрейми.Віртуальна пам'ять — одна з найважливіших абстракцій в архітектурі комп'ютерів, і вона реалізується через партнерство апаратного та програмного забезпечення:Апаратне забезпечення надає:
MMU (блок керування пам'яттю), інтегрований у процесор, який перехоплює кожне звернення до пам'яті і транслює віртуальні адреси у фізичні адреси. MMU працює прозоро — програма виконує інструкцію MOV з віртуальною адресою, і MMU перетворює її у фізичну адресу ще до того, як шина пам'яті її побачить.
TLB, який кешує нещодавні трансляції, тому в типовому випадку (влучання в TLB) додаткові витрати відсутні.
Механізм виняткової ситуації сторінкової помилки, який автоматично передає керування ОС, коли сторінка відсутня в оперативній пам'яті.
Біти захисту в кожному записі таблиці сторінок, що забезпечуються апаратно — процес користувацького режиму фізично не може отримати доступ до сторінки ядра, навіть якщо знає віртуальну адресу.
Операційна система надає:
Керування таблицями сторінок: ОС створює і підтримує таблиці сторінок для кожного процесу, визначаючи, які віртуальні сторінки відображаються на які фізичні фрейми. Регістр CR3 архітектури x86 містить фізичну адресу каталогу сторінок верхнього рівня поточного процесу; коли ОС перемикається між процесами, вона оновлює CR3, фактично перемикаючи весь віртуальний адресний простір.
Обробник сторінкових помилок: Коли апаратне забезпечення генерує сторінкову помилку, ОС вирішує, чи завантажити сторінку з диска, виділити нову сторінку, чи завершити процес через недопустимий доступ.
Політику заміщення сторінок: Коли фізична пам'ять заповнена, ОС вирішує, які сторінки витіснити, використовуючи алгоритми, що наближають принцип «витіснити сторінку, яка найменш імовірно знадобиться найближчим часом».
Керування простором підкачки: ОС керує областю на диску, де зберігаються витіснені сторінки, відстежуючи, які дискові блоки відповідають яким віртуальним сторінкам.
Створення та знищення процесів: Коли створюється новий процес, ОС налаштовує для нього нову таблицю сторінок. Коли процес завершується, ОС відновлює всі його фізичні фрейми та звільняє пам'ять таблиці сторінок.
Це партнерство апаратного та програмного забезпечення — повторювана тема в архітектурі комп'ютерів. Апаратне забезпечення надає швидкі механізми (трансляцію, кешування, генерацію винятків), тоді як програмне забезпечення надає гнучкі політики (які сторінки завантажувати, які витісняти, як обробляти помилки). Жодне з них окремо не було б достатнім — суто апаратне рішення не могло б приймати складні рішення щодо політик, а суто програмне рішення не могло б перехоплювати кожне звернення до пам'яті без катастрофічних втрат продуктивності.Віртуальна пам'ять уможливлює кілька потужних технік, окрім базової ізоляції та підкачки.Копіювання при записі (COW). Коли процес створює дочірній процес (через системний виклик fork() у Unix/Linux), ОС не копіює негайно всю пам'ять батьківського процесу. Натомість вона налаштовує таблицю сторінок дочірнього процесу так, щоб вона вказувала на ті самі фізичні фрейми, що й у батьківського, але позначає всі спільні сторінки як доступні лише для читання в обох таблицях сторінок. Обидва процеси продовжують виконання, читаючи зі спільних фреймів без жодних додаткових витрат. Лише коли один із них спробує записати на спільну сторінку, апаратне забезпечення генерує сторінкову помилку (оскільки сторінка позначена лише для читання). Тоді ОС копіює цю єдину сторінку в новий фрейм, оновлює таблицю сторінок процесу, що здійснює запис, щоб вона вказувала на копію, і позначає її як доступну для читання/запису. Цей підхід «лінивого копіювання» набагато ефективніший за дублювання всієї пам'яті наперед, оскільки багато сторінок (особливо сторінки коду) ніколи не модифікуються дочірнім процесом.Файли, відображені в пам'ять, та спільна пам'ять. ОС також може відобразити файл на диску безпосередньо у віртуальний адресний простір процесу, так що читання з цих віртуальних адрес читає файл, а запис до них записує файл — без потреби в явних системних викликах read() чи write(). Аналогічно, два процеси можуть мати записи таблиці сторінок, що вказують на ті самі фізичні фрейми, дозволяючи їм обмінюватися даними через спільну область пам'яті. Обидві техніки широко використовуються в сучасних операційних системах і були б неможливі без віртуальної пам'яті.<br>Історія сторінкування в x86 ілюструє цю еволюцію. Оригінальні 8086 (1978) та 80286 (1982) не мали підтримки сторінкування взагалі — вони покладалися на сегментацію для керування пам'яттю. <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Intel_80386" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Intel_80386" target="_self">Intel 80386</a> (1985) був першим процесором x86, що включав блок сторінкування, додавши його поверх існуючого механізму сегментації. У захищеному режимі на 386 адреса проходить дві трансляції: спочатку сегментацію (логічна адреса у лінійну адресу), потім сторінкування (лінійна адреса у фізичну адресу). Сучасні операційні системи x86-64 фактично обходять сегментацію (встановлюючи всі бази сегментів у 0, а межі — на максимум) і покладаються виключно на сторінкування для керування пам'яттю. Спадщина сегментації залишається в архітектурі для зворотної сумісності, але всю реальну роботу виконує сторінкування. Це повчальний приклад того, як еволюціонують апаратні архітектури: функції, що здавалися необхідними в одну епоху (сегментація у 80286), можуть стати рудиментарними в наступну, тоді як новіші функції (сторінкування у 80386) виявляються настільки успішними, що стають основою всього сучасного керування пам'яттю.Віртуальна пам'ять також замикає коло з ієрархією пам'яті з L06. Пригадайте ієрархію: регістри, кеш L1, кеш L2, кеш L3, основна пам'ять (DRAM), накопичувач (SSD/HDD). Віртуальна пам'ять — це механізм, що з'єднує два нижні рівні — DRAM і диск. Так само як кеш-пам'ять автоматично переміщує дані між DRAM і рівнями кеша, система віртуальної пам'яті автоматично переміщує дані між диском і DRAM. На кожному рівні принцип один і той самий: тримати часто використовувані дані ближче до процесора, а рідко використовувані дані залишати на повільнішому, дешевшому, більшому сховищі. Результат — система, яка поводиться майже так само швидко, як найшвидший рівень, і майже так само місткою, як найбільший рівень — найкраще з обох світів, досягнуте через елегантне використання непрямої адресації та кешування на кожному шарі.Розгляньмо повний шлях одного звернення до пам'яті в сучасній системі. Ваша програма виконує MOV EAX, [address]. Процесор спочатку перевіряє кеш даних L1 — влучання? Виконано за 1 нс. Промах? Перевірити L2 — виконано за 5 нс. Промах? Перевірити L3 — виконано за 15 нс. Промах? Звернення до DRAM — виконано за 100 нс. Але MMU працює паралельно: віртуальна адреса має бути транслена у фізичну адресу. TLB обробляє це за один такт у 99%+ типових випадків. При промаху TLB процесор проходить багаторівневу таблицю сторінок (додаткові звернення до DRAM). При сторінковій помилці ОС завантажує сторінку з диска — мікросекунди для SSD, мілісекунди для HDD. Кожен шар є кешем для нижчого, кожен керується дедалі складнішими механізмами (апаратна логіка для L1–L3, співпраця апаратного та програмного забезпечення для віртуальної пам'яті). Краса цієї конструкції в тому, що програма не бачить усієї цієї складності — вона просто звертається за адресою пам'яті, і система доставляє дані так швидко, як дозволяє апаратне забезпечення. Ця багаторівнева прозорість, де кожен рівень ієрархії невидимий для вищого, є одним із великих досягнень архітектури комп'ютерних систем і принципом, з яким ви будете стикатися протягом усієї кар'єри в обчислювальній техніці.У частині 3 цієї лекції ми переходимо від апаратного забезпечення назад до програмного. Ієрархія пам'яті, яку ми щойно розглянули, — регістри, кеш, DRAM, диск — це сцена, на якій живуть усі ваші дані. Тепер постає питання: як організувати ці дані? Мови високого рівня надають вам масиви, структури, зв'язані списки та хеш-таблиці зі зручним синтаксисом. Але під капотом кожна з цих структур — це лише патерн байтів, розташованих у неперервній або зв'язаній через вказівники пам'яті. У цьому розділі та трьох наступних ми реалізуємо ці патерни безпосередньо на асемблері x86, щоб ви побачили, що саме компілятор робить за вас.Ми починаємо з масивів — теми, з якою ви вже познайомилися в L05. Цей розділ навмисно стислий: ми переглядаємо основи, а потім переходимо до двовимірних масивів із конкретною формулою доступу та прикладом матриці з елементами розміром у слово.У L05 ви дізналися, що масив в асемблері — це просто послідовність значень, збережених за послідовними адресами пам'яті. Немає перевірки меж під час виконання, немає властивості .length, немає метаданих типу — лише необроблені байти. Ви оголошуєте масив за допомогою DB, DW або DD і звертаєтесь до елементів, обчислюючи їхнє зміщення від базової адреси..data byteArray DB 10, 20, 30, 40, 50 ; 5 байтів, кожен елемент — 1 байт wordArray DW 100, 200, 300 ; 3 слова, кожен елемент — 2 байти
Статичний доступ є простим — асемблер обчислює зміщення під час асемблювання:.code mov al, byteArray ; Перший елемент = 10 (зміщення 0) mov al, byteArray+2 ; Третій елемент = 30 (зміщення 2)
Динамічний доступ вимагає регістра для зберігання обчисленого зміщення: mov bx, offset wordArray mov ax, [bx] ; Перший елемент = 100 (зміщення 0) mov dx, [bx+2] ; Другий елемент = 200 (зміщення 2) mov dx, [bx+4] ; Третій елемент = 300 (зміщення 4)
Універсальна формула: адреса = база + індекс x розмір_елемента. Для байтового масиву розмірелемента дорівнює 1, тому індекс і є зміщенням. Для масиву слів розмірелемента дорівнює 2, тому елемент 3 знаходиться за адресою база + 6. Цю формулу варто запам'ятати — вона зустрічається в кожній мові та кожній архітектурі.<br><img alt="Код TASM для 1D масиву" src="assets/l08-asm-1d-array.png" target="_self">Оголошення та доступ до одновимірних масивів у TASM: байтові масиви, масиви слів і прямий доступ до елементів із константними зміщеннями та регістровою індексацією.Двовимірний масив (матриця) додає один рівень складності: у вас є рядки та стовпці, але пам'ять одновимірна. Як перетворити двовимірну структуру в одновимірну послідовність байтів?<br>Стандартне рішення — <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Row-_and_column-major_order" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Row-_and_column-major_order" target="_self">порядок за рядками</a> — ви зберігаєте спочатку рядок 0, потім рядок 1, потім рядок 2 і так далі. Це конвенція, що використовується в C, Python (NumPy) та більшості інших мов. (Fortran використовує порядок за стовпцями, зберігаючи спочатку стовпець 0, потім стовпець 1 тощо. Формули відрізняються, але основна ідея та сама.).data matrix DB 10, 20, 30, 40 ; Рядок 0 (4 стовпці) DB 50, 60, 70, 80 ; Рядок 1 DB 90, 100, 110, 120 ; Рядок 2 ROWS EQU 3 COLS EQU 4
<br><img alt="Оголошення 2D масивів у TASM" src="assets/l08-asm-2d-array-declare.png" target="_self">Оголошення двовимірних масивів у TASM: байтова матриця 3x4, матриця слів 2x3 та неініціалізований буфер — усі зберігаються в порядку за рядками.У пам'яті ці 12 байтів зберігаються неперервно. Якщо ми візуалізуємо необроблену пам'ять, починаючи з базової адреси matrix, то побачимо:Offset: 0 1 2 3 4 5 6 7 8 9 10 11
Value: 10 20 30 40 50 60 70 80 90 100 110 120 ── Row 0 ── ── Row 1 ── ── Row 2 ──
Рядок 0 займає зміщення 0-3, рядок 1 — зміщення 4-7, а рядок 2 — зміщення 8-11. Межі рядків невидимі для апаратного забезпечення — процесор бачить лише плоску послідовність із 12 байтів. Саме ваш код інтерпретує їх як сітку 3x4. Для доступу до елемента matrix[row][col] потрібна формула:адреса = база + (рядок x COLS + стовпець) x розмір_елементаВираз рядок x COLS перетворює двовимірні координати в лінійний індекс — він пропускає всі повні рядки перед потрібним. Додавання стовпець потім індексує всередині цього рядка. Для елементів розміром у байт розмір_елемента дорівнює 1, тому фінальне множення можна опустити. Для елементів розміром у слово множимо на 2 в кінці.Звернімось до matrix[1][2], значення якого має бути 70 (другий рядок, третій стовпець, рахуючи з нуля):.code ; Доступ до matrix[1][2] — очікуване значення: 70 mov bx, offset matrix mov al, 1 ; row = 1 mov ah, 0 ; очистити AH для MUL mov cl, COLS ; COLS = 4 mul cl ; AX = row * COLS = 1 * 4 = 4 add bx, ax ; BX = база + 4 (початок рядка 1) add bx, 2 ; BX = база + 4 + 2 (стовпець 2) mov dl, [bx] ; DL = 70
<br><img alt="Доступ до елемента 2D масиву в TASM" src="assets/l08-asm-2d-array-access.png" target="_self">Доступ до matrix[1][2]: обчислити зміщення як база + (рядок x COLS) + стовпець, потім завантажити значення.Пройдімо крок за кроком: MUL CL обчислює 1 x 4 = 4 в AX. Додавання цього до BX переміщує нас повз рядок 0 (4 байти) до початку рядка 1. Додавання 2 переміщує нас до стовпця 2 всередині рядка 1. Байт за цією адресою дорівнює 70 — саме matrix[1][2].Для перевірки вручну: лінійний індекс — це 1 x 4 + 2 = 6. Дивлячись на наше розташування в пам'яті вище, зміщення 6 містить значення 70. Формула працює.Для елементів розміром у слово в кінці потрібно помножити на 2 (або зсунути вліво на 1), оскільки кожен елемент займає 2 байти. Формула та сама; змінюється лише розмір елемента.Одна тонкість, яку варто відзначити: інструкція MUL завжди використовує AL (або AX) як неявний операнд і зберігає результат в AX (або DX:AX). Це означає, що ви повинні завантажити номер рядка в AL і очистити AH перед множенням. Якщо ви забудете mov ah, 0, будь-яке залишкове значення в AH зіпсує результат множення — поширена помилка в студентському коді.Для більших матриць з елементами розміром у слово формула вимагає ще одного кроку:.data wMatrix DW 1, 2, 3 ; Рядок 0 (3 стовпці, кожен елемент = 2 байти) DW 4, 5, 6 ; Рядок 1 WCOLS EQU 3 ; Доступ до wMatrix[1][1] — очікуване значення: 5 mov bx, offset wMatrix mov al, 1 ; row = 1 mov ah, 0 mov cl, WCOLS ; WCOLS = 3 mul cl ; AX = 1 * 3 = 3 (лінійний індекс) shl ax, 1 ; AX = 3 * 2 = 6 (множення на розмір елемента) add bx, ax ; BX = база + 6 add bx, 2 ; + стовпець * 2 = + 1 * 2 (стовпець 1, розмір — слово) mov dx, [bx] ; DX = 5
SHL AX, 1 еквівалентний множенню на 2, але швидший за друге MUL. Ви часто зустрічатимете цю оптимізацію «зсув замість множення» в рукописному асемблерному коді і навіть у виводі компілятора для елементів зі степенем двійки. Для масиву подвійних слів (елементи по 4 байти) замість цього використовується SHL AX, 2.Зауваження щодо продуктивності. Інструкція MUL відносно повільна на 8086 (займає 70-77 тактів для байтового множення). У критичному за продуктивністю коді, що ітерує по двовимірному масиву, програмісти часто уникають MUL повністю. Замість перерахунку рядок x COLS з нуля на кожній ітерації, вони підтримують поточний вказівник і просто додають COLS до нього кожного разу при переході до наступного рядка. Це перетворює множення в одне додавання — техніка, яку ви побачите в оптимізованих внутрішніх циклах.Це і є вся механіка, що стоїть за виразами на кшталт int matrix[3][4]; ... matrix[1][2] = 70; у C. Компілятор генерує саме такий тип обчислення адреси кожного разу, коли ви індексуєте багатовимірний масив.Масиви зручні, коли кожен елемент має однаковий тип і розмір. Але реальні дані рідко вписуються в таку модель. Запис про студента містить ім'я (рядок), вік (байт) і середній бал (число з рухомою комою). Піксель має червоний, зелений і синій канали (три байти). Заголовок мережевого пакета складається з полів різної ширини, упакованих разом. У мовах високого рівня для групування таких різнорідних полів використовують struct (C) або class (Java/Python). В асемблері TASM забезпечує пару директив STRUC / ENDS для досягнення того самого результату.Визначення STRUC повідомляє асемблеру: «ось креслення розміщення даних». Воно не виділяє жодної пам'яті — лише визначає імена полів та їхні зміщення в межах структури.Person STRUC age DB ? ; Зміщення 0: 1 байт height DW ? ; Зміщення 1: 2 байти (слово)
Person ENDS
; Загальний розмір: 3 байти на Person
<br><img alt="Визначення та використання структури в TASM" src="assets/l08-asm-struct-define.png" target="_self">Визначення структури Person за допомогою STRUC/ENDS, створення двох екземплярів та доступ до полів через крапкову нотацію — person1.age та person1.height.Це аналогічно запису struct Person { uint8_t age; uint16_t height; }; мовою C. Асемблер запам'ятовує три речі: (1) загальний розмір Person становить 3 байти, (2) поле age знаходиться зі зміщенням 0 всередині структури, і (3) поле height знаходиться зі зміщенням 1. Жодного байта ще не записано в програму — блок STRUC є суто конструкцією часу компіляції.Ініціалізатор ? означає «неініціалізовано» — поля не мають значення за замовчуванням. Можна також задати значення за замовчуванням: age DB 0 зробить нуль типовим віком для будь-якого екземпляра, що не перевизначає його. Коли ви пізніше створюєте екземпляр за допомогою Person &lt;&gt; (порожні кутові дужки), використовуються значення за замовчуванням. Коли ви пишете Person &lt;25, 180&gt;, ви перевизначаєте типові значення конкретними.Щоб фактично виділити пам'ять для Person, оголошуєте екземпляр у сегменті .data:.data person1 Person &lt;?, ?&gt; ; Виділити 3 байти (age=?, height=?) person2 Person &lt;?, ?&gt; ; Ще 3 байти
Синтаксис кутових дужок &lt; &gt; задає початкові значення для кожного поля по порядку. Використання ? означає «залишити неініціалізованим» (асемблер може заповнити нулями або іншим значенням за замовчуванням). Можна також написати person1 Person &lt;25, 180&gt;, щоб ініціалізувати вік значенням 25, а зріст — 180 на етапі асемблювання.Ось де синтаксис структур окупається. Замість того, щоб пам'ятати, що height знаходиться зі зміщенням 1 від початку структури — і щоразу вручну додавати це зміщення — ви пишете ім'я поля напряму:.code ; Запис значень у person1 mov person1.age, 20 ; Еквівалент: mov [person1+0], 20 mov person1.height, 180 ; Еквівалент: mov [person1+1], 180 ; Запис значень у person2 mov person2.age, 30 mov person2.height, 170
<br><img alt="Анотований код структури" src="assets/l08-asm-struct-annotated.png" target="_self">Ключова відмінність: блок STRUC — лише шаблон (пам'ять не виділяється); саме оголошення екземплярів (person1, person2) резервує байти в пам'яті.Крапкова нотація person1.age — це суто зручність асемблера, так званий синтаксичний цукор. На рівні машинного коду person1.age перетворюється на адресу person1 плюс зміщення поля age (яке дорівнює 0), а person1.height — на адресу person1 плюс зміщення height (яке дорівнює 1). Процесор бачить звичайні адреси пам'яті — він не має жодного уявлення про «поля» чи «структури». Якщо дизасемблювати скомпільований код, ви побачите звичайні інструкції MOV з числовими адресами, без жодного сліду крапкової нотації.Вигода цілком для програміста: імена полів роблять ваш намір зрозумілим і запобігають помилкам на одиницю, що виникають при ручному обчисленні зміщень.Простежімо, що саме знаходиться в пам'яті після виконання наведеного коду. Припустімо, що person1 розміщено за адресою 0100h:<br>Зверніть увагу, що height займає два байти в порядку <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Endianness" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Endianness" target="_self">little-endian</a> (молодший байт першим), як це стандартно для x86. Значення 180 — це 00B4h: молодший байт B4h зберігається першим (за нижчою адресою 0101h), а старший байт 00h слідує за адресою 0102h. Якщо ви коли-небудь побачите в налагоджувачі B4 00 там, де очікували 180, — це саме тому: порядок байтів little-endian.<br>Два екземпляри Person упаковані суміжно, без вирівнювання — на відміну від компіляторів C на 32-бітних або 64-бітних платформах, які часто вставляють <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Data_structure_alignment" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Data_structure_alignment" target="_self">вирівнювальні проміжки</a> між полями або між структурами, щоб багатобайтові поля починалися з адрес, кратних їхньому розміру. У 16-бітному реальному режимі з TASM те, що ви оголосили, — це саме те, що отримаєте. Асемблер не додає жодних прихованих байтів.Коли у вас є вказівник на структуру (наприклад, адреса вузла у зв'язаному списку), доступ до полів здійснюється через синтаксис дужок із крапкою: mov bx, offset person1 mov al, [bx].age ; Читання віку (байт за адресою BX+0) mov dx, [bx].height ; Читання зросту (слово за адресою BX+1)
Цей патерн є ключовим для обходу зв'язаного списку, який ми напишемо в розділі 14. Коли BX містить адресу будь-якого Person, [bx].age завжди зчитує правильний байт, незалежно від того, на який екземпляр вказує BX.Ця відмінність часто збиває студентів з пантелику, тому будьмо чіткими. Блок STRUC/ENDS — це шаблон: уявіть його як формочку для печива. Він визначає форму, але не створює нічого матеріального. Оголошення person1 Person &lt;?, ?&gt; — це екземпляр: власне саме печиво, фізичні байти в сегменті даних вашої програми.Ви можете визначити скільки завгодно шаблонів, не збільшивши обсяг пам'яті вашої програми жодним байтом. Лише екземпляри займають пам'ять. Це точно те, як працюють визначення struct у C: struct Person { ... }; визначає тип, але нічого не виділяє; struct Person p1; виділяє пам'ять.Ще один практичний момент: ви можете використовувати SIZE Person, щоб отримати загальну кількість байтів структури (3 у нашому випадку), і OFFSET Person.height, щоб отримати зміщення поля всередині шаблону (1 у нашому випадку). Це константи часу асемблювання — вони не генерують жодних інструкцій під час виконання. Вони корисні при написанні універсальних підпрограм, яким потрібно ітерувати масиви структур, де вказівник просувається на SIZE Person байтів за кожну ітерацію.<br>Маючи структури, ми можемо взятися за більш амбітну структуру даних: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Linked_list" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Linked_list" target="_self">зв'язаний список</a>. У зв'язаному списку кожен елемент (який називають вузлом) містить деякі дані та вказівник на наступний вузол. На відміну від масиву, вузли не обов'язково мають бути суміжними в пам'яті — вони можуть бути розкидані будь-де, з'єднані лише своїми вказівниками.Чому це важливо на рівні асемблера? Масиви мають фіксований розмір, визначений під час оголошення. Вставка елемента в середину масиву вимагає зсуву всіх наступних елементів — а в асемблері це означає написання циклу побайтового або послівного копіювання, що є і громіздким, і повільним для великих масивів. Зв'язані списки, навпаки, дозволяють вставку та видалення за O(1) в будь-якому місці — достатньо змінити кілька вказівників. Компроміс полягає в тому, що ви втрачаєте довільний доступ за O(1): щоб дістатися до n-го елемента, потрібно пройти список з початку, послідовно рухаючись за кожним вказівником next.У мовах високого рівня стандартна бібліотека приховує ці деталі. В асемблері ви бачите кожне розіменування вказівника, кожну перевірку на NULL і кожне обчислення зміщення поля. Ця прозорість і є метою вправи.В асемблері «вказівник» — це просто 16-бітне значення, зміщення в межах сегмента даних. Не існує спеціального типу вказівника, автоматичного розіменування чи збирача сміття. Ви керуєте кожним вказівником вручну. Це робить код зв'язаного списку в асемблері більш багатослівним, ніж у C, але водночас робить кожен крок повністю прозорим — жодного прихованого механізму.Ми будуємо на основі структури Person з розділу 13, вбудовуючи її всередину Node, який також містить вказівник next:Node STRUC value Person &lt;&gt; ; Вбудована структура Person (3 байти: age + height) next DW ? ; Вказівник на наступний вузол (2-байтове зміщення)
Node ENDS
; Загальний розмір: 5 байтів на Node (3 для Person + 2 для next)
<br><img alt="Визначення вузла зв'язаного списку" src="assets/l08-asm-linkedlist-define.png" target="_self">Визначення структури Node, що містить значення Person (3 байти) та вказівник next (2 байти), з трьома статично виділеними вузлами.Синтаксис Person &lt;&gt; створює вбудований екземпляр зі значеннями за замовчуванням. Зверніть увагу, що TASM дозволяє вкладати структури: шаблон Node містить шаблон Person як перше поле. Це концептуально ідентично вбудовуванню однієї структури в іншу мовою C (struct Node { struct Person value; uint16_t next; };).Поле next — це зміщення розміром у слово, яке міститиме адресу наступного вузла в списку або значення-маркер (-1, тобто FFFFh) для позначення кінця списку.Оскільки ми працюємо в простій програмі стилю .COM без динамічного виділення пам'яті (без malloc), ми попередньо виділяємо вузли в сегменті .data:.data head DB SIZE Node DUP(0) ; Вузол 1 (голова списку) node2 DB SIZE Node DUP(0) ; Вузол 2 node3 DB SIZE Node DUP(0) ; Вузол 3
Оператор SIZE Node повертає 5 (кількість байтів в одному Node), тому кожне оголошення виділяє 5 байтів, ініціалізованих нулями. Тут ми використовуємо DB ... DUP(0) замість Node &lt;...&gt;, щоб показати, що виділення — це насправді просто блок байтів: мітка Node — це зручність для людини, а не апаратна особливість. Так само можна написати head Node &lt;, ?&gt;, використовуючи синтаксис структур — результат у пам'яті ідентичний.Тепер заповнюємо дані та з'єднуємо вказівники next, щоб утворити ланцюг: head -&gt; node2 -&gt; node3 -&gt; NULL. Це асемблерний еквівалент побудови зв'язаного списку вручну мовою C — присвоєння значень полям та встановлення вказівника next кожного вузла на адресу наступного..code ; Вузол 1 (head): age=25, height=175, next -&gt; node2 mov head.value.age, 25 mov head.value.height, 175 mov head.next, OFFSET node2 ; Вузол 2: age=30, height=180, next -&gt; node3 mov node2.value.age, 30 mov node2.value.height, 180 mov node2.next, OFFSET node3 ; Вузол 3 (хвіст): age=22, height=165, next = NULL mov node3.value.age, 22 mov node3.value.height, 165 mov node3.next, -1 ; FFFFh = NULL-маркер
<br><img alt="Ініціалізація зв'язаного списку" src="assets/l08-asm-linkedlist-init.png" target="_self">Ініціалізація зв'язаного списку: задання полів даних кожного вузла та з'єднання їх за допомогою OFFSET — head -&gt; node2 -&gt; node3 -&gt; NULL (-1).Кілька зауважень. Вкладений синтаксис із крапкою head.value.age працює, тому що TASM розв'язує його покроково: head дає базову адресу першого вузла, .value додає зміщення вбудованого Person (яке дорівнює 0, оскільки це перше поле), а .age додає зміщення age всередині Person (теж 0). Отже, head.value.age розв'язується до першого байта head. Натомість head.value.height розв'язується до head + 0 + 1 = head + 1, а head.next — до head + 3 (пропускаючи 3-байтовий Person).Ми використовуємо -1 (FFFFh) як NULL-маркер замість 0, оскільки адреса 0 теоретично може бути допустимим зміщенням у сегменті даних. Використання FFFFh як недійсного маркера є поширеною конвенцією в 16-бітному коді, хоча можна використовувати будь-який узгоджений маркер. У 32-бітному та 64-бітному C NULL зазвичай дорівнює 0, але концепція та сама: зарезервоване значення, яке означає «цей вказівник не вказує на жоден дійсний вузол».Канонічна операція зі зв'язаним списком — це обхід: почати з голови, обробити кожен вузол, перейти за вказівником next і зупинитися, коли досягнуто NULL. Це асемблерний еквівалент циклу while, що проходить через кожен вузол. Тут ми збільшуємо вік кожної особи в списку: mov bx, OFFSET head ; BX = вказівник на поточний вузол
traverse_list: ; Обробка поточного вузла: збільшення віку на 1 mov al, [bx].value.age ; Зчитування поточного віку add al, 1 ; Збільшення mov [bx].value.age, al ; Збереження оновленого віку ; Перехід за вказівником next mov bx, [bx].next ; BX = адреса наступного вузла cmp bx, -1 ; Чи це NULL (FFFFh)? jne traverse_list ; Якщо ні, продовжити до наступного вузла ; Цикл завершується, коли BX = FFFFh (кінець списку)
<br><img alt="Код обходу зв'язаного списку" src="assets/l08-asm-linkedlist-traverse.png" target="_self">Обхід зв'язаного списку: перехід за вказівником next від вузла до вузла, збільшення віку кожної особи, доки не досягнуто NULL-маркера.Пройдімо це подумки. На першій ітерації BX вказує на head. Ми зчитуємо head.value.age (25), додаємо 1, отримуючи 26, і записуємо назад. Потім mov bx, [bx].next завантажує слово за адресою head + 3, яке є зміщенням node2. Тепер BX вказує на node2. На другій ітерації ми збільшуємо вік node2 з 30 до 31 і завантажуємо node2.next, яке є зміщенням node3. На третій ітерації ми збільшуємо вік node3 з 22 до 23 і завантажуємо node3.next, яке дорівнює FFFFh. Інструкція CMP встановлює прапорець нуля, JNE не виконує перехід, і цикл завершується.Це точно той патерн, який компілятор C генерує для:Node* current = head;
while (current != NULL) { current-&gt;value.age++; current = current-&gt;next;
}
Єдина відмінність полягає в тому, що в C компілятор керує арифметикою вказівників та перевіркою на NULL за вас. В асемблері ви робите все це явно — саме тому вивчення цього формує глибоке розуміння того, як структури даних насправді працюють на рівні машини.Візуалізуймо повне розміщення в пам'яті після ініціалізації. Припустімо, що head починається зі зміщення 0200h:Адреса Байти Інтерпретація
------ ----- -------------
0200h 19 head.value.age = 25 (19h)
0201h AF 00 head.value.height = 175 (00AFh, little-endian)
0203h 05 02 head.next = 0205h (зміщення node2) 0205h 1E node2.value.age = 30 (1Eh)
0206h B4 00 node2.value.height = 180 (00B4h)
0208h 0A 02 node2.next = 020Ah (зміщення node3) 020Ah 16 node3.value.age = 22 (16h)
020Bh A5 00 node3.value.height = 165 (00A5h)
020Dh FF FF node3.next = FFFFh (NULL)
Кожен вузол займає рівно 5 байтів: 1 для віку, 2 для зросту і 2 для вказівника next. Тут вузли розташовані суміжно (оскільки ми оголосили їх один за одним у .data), але це не обов'язково. Краса зв'язаного списку в тому, що head.next може вказувати на будь-яку адресу в сегменті даних — вузли можуть бути розкидані по пам'яті, і код обходу працюватиме ідентично. Саме вказівник створює логічну послідовність, а не порядок фізичних адрес.Зверніть також увагу: якщо ви хочете вставити новий вузол між head та node2, потрібно змінити лише два вказівники — встановити next нового вузла на node2 та встановити head.next на новий вузол. Жодних даних не потрібно зсувати. Порівняйте це зі вставкою в середину масиву, де кожен елемент після точки вставки має зсунутися на одну позицію — операція O(n), якої зв'язаний список уникає повністю.Дві помилки найчастіше трапляються в студентському коді зв'язаних списків: Забути перевірити NULL перед розіменуванням. Якщо BX містить FFFFh (наш NULL-маркер) і ви виконаєте mov al, [bx].value.age, ви зчитаєте байт за адресою FFFFh — тобто останній байт 64-кілобайтного сегмента даних. Це не спричинить аварійне завершення (8086 не має захисту пам'яті), але мовчки поверне сміттєві дані. Завжди перевіряйте cmp bx, -1 перед доступом до полів. Перезаписати вказівник next перед його зчитуванням. Якщо ваш код обробки змінює пам'ять вузла (наприклад, обнуляє його), а вказівник next є частиною цього вузла, ви втратите зв'язок з рештою списку. Завжди зчитуйте [bx].next у регістр перед будь-якими деструктивними діями над поточним вузлом. Це не лише помилки асемблера — це ті самі баги, що переслідують програми на C зі зв'язаними списками (розіменування нульового вказівника, використання після звільнення). Побачивши їх на рівні байтів, ви станете кращим налагоджувачем у будь-якій мові.Наша остання структура даних поєднує дві ідеї, які ви вже знаєте — рядки та вказівники — в патерн, що зустрічається всюди в системному програмуванні: масив вказівників на рядки.Припустімо, вам потрібно зберегти п'ять рядків. Якщо спробувати помістити їх у звичайний масив, одразу виникає проблема: рядки мають змінну довжину. "Hi" — це 2 символи; "Assembly Language" — 17 символів. Ви не можете оголосити stringArray DB 5 DUP(...), бо кожен елемент потребуватиме різну кількість байтів.Можна доповнити кожен рядок до якоїсь максимальної довжини — скажімо, 32 байти — і зберігати їх у плоскому масиві розміром 5 x 32 = 160 байтів. Але такий підхід витрачає пам'ять (короткі рядки залишають більшу частину свого слота порожньою) і ускладнює обробку (потрібно знати максимальну довжину всюди). Гірше того, якщо хоча б один рядок перевищить максимум, доведеться змінювати розмір усього масиву. Є кращий спосіб.Стандартне рішення — яке використовують C, операційні системи та практично кожне середовище виконання мов — це непряма адресація: зберігати самі рядки десь у пам'яті, а потім створити окремий масив вказівників (зміщень), які вказують на початок кожного рядка. Кожен вказівник — це слово фіксованого розміру (2 байти в 16-бітному режимі), тому масив вказівників є звичайним однорідним масивом, який можна індексувати за формулами з розділу 12. Рядки можуть бути будь-якої довжини — масиву вказівників не потрібно цього знати чи враховувати..data STR_COUNT EQU 5 strArray DW STR_COUNT DUP(0) ; Масив з 5 вказівників розміром у слово str1 DB "Hello, World!$" str2 DB "Assembly Language$" str3 DB "Array of Strings$" str4 DB "Pointers Example$" str5 DB "TASM Program$"
<br><img alt="Оголошення даних масиву рядків" src="assets/l08-asm-stringarray-data.png" target="_self">Оголошення масиву вказівників на рядки та самих рядкових даних — кожен рядок завершується символом $ для виведення через DOS INT 21h.Рядки оголошені окремо, кожен завершується символом $ (термінатор рядків DOS для INT 21h, функція 09h). Зверніть увагу, що $ — це не те саме, що нуль-термінатор C (\0); це конвенція DOS, специфічна для функції виведення рядка. Масив вказівників strArray спочатку заповнений нулями — ми встановимо вказівники в сегменті .code під час виконання.Кожен запис у strArray повинен містити зміщення відповідного рядка:.code mov strArray[0], OFFSET str1 ; strArray[0] -&gt; "Hello, World!$" mov strArray[2], OFFSET str2 ; strArray[1] -&gt; "Assembly Language$" mov strArray[4], OFFSET str3 ; strArray[2] -&gt; "Array of Strings$" mov strArray[6], OFFSET str4 ; strArray[3] -&gt; "Pointers Example$" mov strArray[8], OFFSET str5 ; strArray[4] -&gt; "TASM Program$"
<br><img alt="Ініціалізація вказівників масиву рядків" src="assets/l08-asm-stringarray-init.png" target="_self">Ініціалізація масиву вказівників: кожен запис розміром у слово отримує OFFSET відповідного рядка.Зверніть увагу на індекси: strArray[0], strArray[2], strArray[4] і так далі — з кроком 2, оскільки кожен вказівник є словом (2 байти). Це формула база + індекс x розмір_елемента з розділу 12 в дії, де розмір_елемента = 2.Після ініціалізації масив вказівників у пам'яті виглядає так:strArray: [зміщення str1] [зміщення str2] [зміщення str3] [зміщення str4] [зміщення str5] ── 2 байти ── ── 2 байти ── ── 2 байти ── ── 2 байти ── ── 2 байти ──
Кожен 2-байтовий слот містить початкову адресу відповідного рядка. Самі рядки можуть бути будь-де в сегменті даних і мати абсолютно різну довжину — масив вказівників цим не переймається.<br>Тепер можна пройти масив вказівників і вивести кожен рядок за допомогою <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/DOS_API#DOS_INT_21h_services" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/DOS_API#DOS_INT_21h_services" target="_self">переривання DOS 21h, функція 09h</a> (яка очікує адресу рядка в DX і виводить символи, доки не зустріне термінатор $): mov cx, STR_COUNT ; Лічильник циклу = 5 xor si, si ; SI = байтове зміщення в strArray (починається з 0)
print_loop: mov dx, strArray[si] ; DX = вказівник на поточний рядок mov ah, 09h ; Функція DOS: вивести рядок int 21h ; Вивести рядок, на який вказує DX ; Вивести новий рядок (повернення каретки + переведення рядка) mov ah, 02h ; Функція DOS: вивести символ mov dl, 0Dh ; Повернення каретки int 21h mov dl, 0Ah ; Переведення рядка int 21h add si, 2 ; Перейти до наступного вказівника (розмір слова) loop print_loop ; Зменшити CX і повторити, якщо CX != 0
<br><img alt="Цикл виведення масиву рядків" src="assets/l08-asm-stringarray-print.png" target="_self">Виведення всіх рядків: цикл по масиву вказівників, завантаження кожної адреси в DX та виклик DOS INT 21h, функція 09h.Результат виглядає так:Hello, World!
Assembly Language
Array of Strings
Pointers Example
TASM Program
<br><img alt="Вивід масиву рядків у DOS" src="assets/l08-dos-stringarray-output.png" target="_self">Результат: п'ять рядків, виведених послідовно, що демонструє успішне посилання масиву вказівників на розташування кожного рядка в пам'яті.Зверніть увагу, що тіло циклу не залежить від довжини рядків. Йому не потрібно знати, що "Hello, World!" має 13 символів, а "TASM Program" — 12. Воно просто завантажує вказівник і передає його DOS. Обробник INT 21h проходить по пам'яті, починаючи з цього вказівника, виводячи символи, доки не зустріне $. Складність роботи зі змінною довжиною повністю прихована за непрямою адресацією через вказівники.Цей патерн — масив вказівників на дані змінної довжини — є однією з найфундаментальніших структур у системному програмуванні. Розглянемо:
У C main(int argc, char* argv[]) отримує аргументи командного рядка як масив символьних вказівників — саме ту структуру, яку ми щойно побудували. Кожен argv[i] — це вказівник на рядок із нуль-термінатором, а argc повідомляє кількість записів у масиві.
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Environment_variable" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Environment_variable" target="_self">Блок змінних середовища</a> як у DOS, так і в Unix зберігається так само: масив вказівників на рядки KEY=VALUE.
<br>Таблиці рядків у форматах виконуваних файлів (<a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Executable_and_Linkable_Format" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Executable_and_Linkable_Format" target="_self">ELF</a>, PE) використовують індексацію на основі зміщень у блоці упакованих рядків — варіація тієї самої ідеї.
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Virtual_method_table" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Virtual_method_table" target="_self">Таблиці віртуальних методів</a> (vtable) у C++ — це масиви вказівників на функції, структурно ідентичні нашому масиву вказівників на рядки, за винятком того, що вказівники вказують на код, а не на дані.
Ключовий висновок полягає в тому, що непряма адресація (зберігання вказівника на дані замість самих даних) вирішує проблему змінного розміру. Кожен вказівник — це однакові 2 байти, тому масив вказівників легко індексувати. Самі рядки можуть бути будь-якої довжини, збережені будь-де в пам'яті. Це розділення «індексної структури» та «даних» зустрічається на кожному рівні комп'ютерних систем — від inode файлових систем до індексів баз даних і таблиць сторінок віртуальної пам'яті.Подивіться, який обсяг матеріалу ми охопили в цій частині лекції. Ми почали з плоских масивів (розділ 12), де кожен елемент має однаковий розмір і знаходиться за передбачуваним зміщенням. Потім ми ввели структури (розділ 13) для групування полів різних розмірів під іменованими зміщеннями. Ми поєднали структури з вказівниками для побудови зв'язаних списків (розділ 14), де вузли посилаються один на одного через збережені адреси. І нарешті, ми використали масиви вказівників (цей розділ) для керування даними змінної довжини, додавши рівень непрямої адресації.Кожна з цих структур існує в пам'яті лише як послідовність байтів. Процесор не знає, чи є конкретний байт полем віку, вказівником на наступний вузол або літерою 'H' у рядку. Увесь сенс визначається тим, як ваш код інтерпретує ці байти — які зміщення він зчитує, скільки байтів він отримує і за якою адресою переходить далі.Це фундаментальний урок структур даних на низькому рівні: структура — це не властивість даних, а властивість коду, що їх зчитує. Ті самі 5 байтів можуть бути вузлом Node (з віком, зростом і вказівником next) або 5 непов'язаними байтовими значеннями — процесор не бачить різниці. Саме програміст (або компілятор від імені програміста) надає сенс сирій пам'яті. Розуміння цього знімає завісу таємниці з того, як високорівневі структури даних «насправді працюють» — вони працюють саме так, лише з більшою кількістю синтаксичного цукру поверх. Введення-виведення на x86 базується на портах. Процесор спілкується з пристроями через окремий адресний простір введення-виведення за допомогою інструкцій IN та OUT. Кожен пристрій (клавіатура, таймер, контролер DMA (прямий доступ до пам'яті)) має призначені діапазони портів. Контролер клавіатури — приклад апаратного інтерфейсу. Порти 0x60 (дані) та 0x64 (команди/стан) забезпечують двонаправлене спілкування — зчитування скан-кодів, надсилання команд керування світлодіодами та перевірка стану. Це той сирий інтерфейс, який BIOS INT 16h абстрагує. Ієрархія пам'яті обмінює швидкість на ємність. Регістри (&lt; 1 нс) -&gt; кеш L1/L2/L3 (1-10 нс) -&gt; оперативна пам'ять (50-100 нс) -&gt; SSD (100 мкс) -&gt; HDD (10 мс) -&gt; стрічка (секунди). Кожен рівень кешує попередній, використовуючи часову та просторову локальність. Типи пам'яті розрізняються за кількома вісями. Енергозалежність (SRAM/DRAM втрачають дані без живлення; флеш/ROM зберігають їх), тип доступу (довільний, послідовний, блочний), технологія (напівпровідникова, магнітна, оптична) та рівень інтеграції (на кристалі, на платі, зовнішня). Віртуальна пам'ять забезпечує ізоляцію та гнучкість. MMU (блок керування пам'яттю) транслює віртуальні адреси у фізичні через таблиці сторінок. Кожен процес отримує власний адресний простір; сторінки можуть бути вивантажені на диск, коли оперативна пам'ять заповнена. TLB (буфер асоціативної трансляції) кешує останні трансляції з коефіцієнтом влучань &gt;99%. Високорівневі структури даних безпосередньо відображаються в пам'яті. Структури (STRUC/ENDS) визначають зміщення полів; зв'язані списки використовують вказівники розміром у слово для з'єднання вузлів; масиви рядків використовують масиви вказівників для роботи з даними змінної довжини. Розуміння цих відображень розкриває те, що компілятори генерують із вашого коду на C/Python. Що ви робитимете:
Працюватимете безпосередньо з відеопам'яттю за сегментом B800h у текстовому режимі
Записуватимете символи та атрибути безпосередньо у відеопам'ять (оминаючи INT 10h)
Обчислюватимете позиції на екрані за формулою: зміщення = (рядок x 80 + стовпець) x 2
Створюватимете кольоровий текстовий вивід, малюватимете рамки та, можливо, прості анімації
Зв'язок із цією лекцією:
Відеопам'ять за адресою B800:0000 є прикладом введення-виведення через пам'ять — відеоадаптер відображає свою оперативну пам'ять в адресний простір процесора, тому ви пишете в неї звичайними інструкціями MOV, а не IN/OUT
Це контрастує з підходом портового введення-виведення (контролер клавіатури), розглянутим у цій лекції
Двовимірна розкладка екрана (80 стовпців x 25 рядків) використовує ту саму формулу адресації за рядками, що й двовимірні масиви в розділі 12 <br><a data-tooltip-position="top" aria-label="https://toshiba.semicon-storage.com/ap-en/semiconductor/knowledge/e-learning/micro-intro/chapter2/types-of-bus-lines.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://toshiba.semicon-storage.com/ap-en/semiconductor/knowledge/e-learning/micro-intro/chapter2/types-of-bus-lines.html" target="_self">Типи шинних ліній</a> — Електронний навчальний курс Toshiba Semiconductor про шини даних, адресні шини та шини керування
<br><a data-tooltip-position="top" aria-label="https://deeprajbhujel.blogspot.com/2015/12/keyboard-and-display-controller.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://deeprajbhujel.blogspot.com/2015/12/keyboard-and-display-controller.html" target="_self">Контролер клавіатури та дисплея 8279</a> — Вступ до контролера клавіатури/дисплея Intel 8279
<br><a data-tooltip-position="top" aria-label="https://www.basicinputoutput.com/2024/11/the-keyboard-controller-interface.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.basicinputoutput.com/2024/11/the-keyboard-controller-interface.html" target="_self">Інтерфейс контролера клавіатури, 0x60/0x64</a> — Детальний розбір портів контролера клавіатури, регістра стану та команд
<br><a data-tooltip-position="top" aria-label="https://www.plantation-productions.com/Webster/www.artofasm.com/DOS/pdf/apndxc.pdf" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.plantation-productions.com/Webster/www.artofasm.com/DOS/pdf/apndxc.pdf" target="_self">Art of Assembly Language — Додаток C</a> — Скан-коди клавіатури та команди мікроконтролера (PDF)
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Memory_hierarchy" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Memory_hierarchy" target="_self">Ієрархія пам'яті</a> — Огляд ієрархії комп'ютерної пам'яті у Вікіпедії
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Cache_hierarchy" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Cache_hierarchy" target="_self">Ієрархія кешу</a> — Вікіпедія про багаторівневу архітектуру кешу, обчислення середнього часу доступу та еволюцію
<br><a data-tooltip-position="top" aria-label="https://hazelcast.com/foundations/caching/caching/" rel="noopener nofollow" class="external-link is-unresolved" href="https://hazelcast.com/foundations/caching/caching/" target="_self">Що таке кешування?</a> — Огляд концепцій та обмежень кешування від Hazelcast
<br><a data-tooltip-position="top" aria-label="https://www.computerlanguage.com/results.php?definition=RAM+cache" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.computerlanguage.com/results.php?definition=RAM+cache" target="_self">Кеш оперативної пам'яті</a> — Визначення кешу оперативної пам'яті від Computer Language Company
<br><a data-tooltip-position="top" aria-label="https://www.enterprisestorageforum.com/hardware/types-of-computer-memory/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.enterprisestorageforum.com/hardware/types-of-computer-memory/" target="_self">9 типів комп'ютерної пам'яті</a> — Всебічний огляд технологій пам'яті
<br><a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/computer-memory/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/computer-memory/" target="_self">Комп'ютерна пам'ять</a> — Посібник GeeksforGeeks з типів пам'яті та ієрархії
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Virtual_memory" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Virtual_memory" target="_self">Віртуальна пам'ять</a> — Вікіпедія про концепції віртуальної пам'яті, сторінкування та історію
<br><a data-tooltip-position="top" aria-label="https://afteracademy.com/blog/what-are-paging-and-segmentation/" rel="noopener nofollow" class="external-link is-unresolved" href="https://afteracademy.com/blog/what-are-paging-and-segmentation/" target="_self">Що таке сторінкування та сегментація?</a> — Пояснення механізмів сторінкування від AfterAcademy
]]></description><link>lectures/l08.-пристрої.-ієрархія-пам'яті.-структури-даних-на-низькому-рівні.html</link><guid isPermaLink="false">Lectures/L08. Пристрої. Ієрархія пам'яті. Структури даних на низькому рівні.md</guid><pubDate>Tue, 10 Mar 2026 06:01:50 GMT</pubDate><enclosure url="assets/l08-network-switch.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l08-network-switch.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L07-NOTES. Переривання та взаємодія з ОС]]></title><description><![CDATA[Короткий довідник до Лекції 07. Повні матеріали: «L07-ENG. Interrupts and OS Interaction.md».Кнопка живлення → Вектор скидання → BIOS/UEFI → POST → Початкове завантаження → ОСПорядок завантаження: налаштування BIOS визначають, з якого пристрою завантажуватися першим (HDD, USB, мережа).Дізнатися більше:
<a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Bootloader" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Bootloader" target="_self">Завантажувач</a> (Wikipedia)
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Power-on_self-test" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Power-on_self-test" target="_self">Самотестування при увімкненні</a> (Wikipedia)
512-байтова структура в секторі 0 завантажувального диска:Кожен запис розділу: індикатор завантаження (80h = активний), CHS початок/кінець, ідентифікатор системи, LBA початок, загальна кількість секторів.Максимум 4 первинних розділи (або 3 первинних + 1 розширений з логічними томами всередині).<br>Дізнатися більше: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Master_boot_record" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Master_boot_record" target="_self">Головний завантажувальний запис</a> (Wikipedia)Апаратні переривання: ініціюються пристроями (клавіатура, таймер, диск). Асинхронні — можуть виникнути в будь-який момент.Програмні переривання: ініціюються інструкцією INT N. Синхронні — виникають у конкретній точці коду.Прапорець IF у FLAGS керує маскованими апаратними перериваннями: STI вмикає, CLI вимикає.<br>Дізнатися більше: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Interrupt" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Interrupt" target="_self">Переривання</a> (Wikipedia)Номер функції в AH. Основні функції:mov ah, 09h
mov dx, offset msg ; DS:DX → рядок, що закінчується '$'
int 21h ; Вивести рядок
<br>Дізнатися більше: <a data-tooltip-position="top" aria-label="https://mrszeto.net/CIT/interrupts.htm" rel="noopener nofollow" class="external-link is-unresolved" href="https://mrszeto.net/CIT/interrupts.htm" target="_self">Переривання DOS</a>Байт атрибута кольору: біти 7-4 = фон, біти 3-0 = передній план (напр., 07h = білий на чорному).<br>Дізнатися більше: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/INT_10H" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/INT_10H" target="_self">INT 10h</a>, <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/INT_16H" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/INT_16H" target="_self">INT 16h</a> (Wikipedia)Номер системного виклику в EAX, аргументи в EBX, ECX, EDX, ESI, EDI, EBP. Результат у EAX (від'ємне = помилка).mov eax, 4 ; write
mov ebx, 1 ; stdout
mov ecx, msg ; вказівник
mov edx, len ; довжина
int 80h
Сучасний 64-бітний Linux використовує інструкцію SYSCALL (швидша, інша конвенція регістрів: RDI, RSI, RDX, R10, R8, R9).<br>Дізнатися більше: <a data-tooltip-position="top" aria-label="https://syscalls.w3challs.com/" rel="noopener nofollow" class="external-link is-unresolved" href="https://syscalls.w3challs.com/" target="_self">Таблиця системних викликів</a>256 записів × 4 байти = 1024 байти за адресою 0000:0000–0000:03FF.Кожен запис: 2-байтове зміщення (IP) + 2-байтовий сегмент (CS), little-endian.Знаходження обробника для INT N: зчитати 4 байти за адресою N × 4.<br>Дізнатися більше: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Interrupt_vector_table" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Interrupt_vector_table" target="_self">Таблиця векторів переривань</a> (Wikipedia)
PUSH FLAGS → 2. Очистити IF, TF → 3. PUSH CS → 4. PUSH IP (наступної інструкції) → 5. Завантажити CS:IP з IVT[N×4] → 6. Виконати обробник POP IP → 2. POP CS → 3. POP FLAGS
<br>Дізнатися більше: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Protected_mode" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Protected_mode" target="_self">Захищений режим</a> (Wikipedia)Сегментація: сегментні регістри містять селектори → індексація в GDT/LDT → дескриптор містить базу, ліміт, права доступу. ЦП перевіряє кожен доступ.Сторінкова організація пам'яті (80386+): віртуальна адреса → таблиця сторінок → фізична адреса. Розмір сторінки = 4 КБ.Основні переваги:
Кожен процес отримує ізольований віртуальний адресний простір (окремі таблиці сторінок)
Завантаження сторінок за запитом: сторінки завантажуються лише при зверненні; сторінкові помилки обробляються ОС
Підкачка: неактивні сторінки переміщуються на диск, завантажуються назад прозоро
Кеш буфера асоціативної трансляції (TLB): &gt;99% влучень робить трансляцію швидкою
<br>Дізнатися більше: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Virtual_memory" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Virtual_memory" target="_self">Віртуальна пам'ять</a>, <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Memory_paging" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Memory_paging" target="_self">Сторінкова організація пам'яті</a> (Wikipedia)4 кільця (на практиці використовуються лише 0 та 3):Перехід між кільцями: INT/SYSCALL → ЦП перемикається на Кільце 0, завантажує стек ядра з TSS, зберігає стан користувача.Обов'язки ядра: планування процесів, керування пам'яттю, драйвери пристроїв, інтерфейс системних викликів.Архітектури ядра: Монолітне ядро (Linux — швидке, все в Кільці 0) проти Мікроядра (MINIX, QNX — драйвери в просторі користувача, надійніше).Простір ядра та простір користувача (32-бітний Linux): нижні 3 ГБ = користувач, верхній 1 ГБ = ядро (відображено в кожному процесі, але лише для Кільця 0).<br>Дізнатися більше: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Protection_ring" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Protection_ring" target="_self">Кільце захисту</a>, <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Kernel_(operating_system)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Kernel_(operating_system)" target="_self">Ядро</a> (Wikipedia)
Ланцюг завантаження: Вектор скидання → BIOS/POST → MBR → Завантажувач → Ядро
Переривання об'єднують апаратні події та програмні запити на обслуговування через єдиний механізм ТВП
INT зберігає FLAGS+CS+IP, IRET відновлює їх — як далекий CALL, але зі збереженням прапорців
ТВП = 256 × 4-байтових записів за адресою 0000:0000; запис N за адресою N×4
DOS INT 21h (AH=функція) та BIOS INT 10h/16h — основні засоби введення-виведення в реальному режимі
Реальний режим: 1 МБ, без захисту, без багатозадачності
Захищений режим: віртуальна пам'ять (сторінкова організація), кільця захисту (0-3), апаратне перемикання задач (TSS)
Ядро (Кільце 0) керує всім; програми користувача (Кільце 3) використовують системні виклики
Що ви будете робити:
Дослідження ТВП у Turbo Debugger (дамп пам'яті за адресою 0000:0000)
Відстеження векторів переривань до коду обробника
Дослідження області даних BIOS (0040:0000): буфер клавіатури, тіки таймера
Виклик переривань BIOS: INT 10h (відео), INT 16h (клавіатура)
Виклик переривань DOS: INT 21h (функції 01h, 02h, 09h, 4Ch)
Перехоплення вектора переривання: збереження оригіналу, встановлення власного обробника, відновлення при виході
]]></description><link>lectures/l07-notes.-переривання-та-взаємодія-з-ос.html</link><guid isPermaLink="false">Lectures/L07-NOTES. Переривання та взаємодія з ОС.md</guid><pubDate>Tue, 10 Mar 2026 06:01:50 GMT</pubDate></item><item><title><![CDATA[L07. Переривання та взаємодія з ОС]]></title><description><![CDATA[
Лекція 7 курсу «Архітектура комп'ютерних систем» в НаУКМА.
Після цієї лекції ви зможете:
Описати повний процес завантаження від натискання кнопки живлення через POST, BIOS/UEFI, MBR до завантаження ядра ОС
Пояснити різницю між апаратними та програмними перериваннями
Описати структуру таблиці векторів переривань (ТВП) та те, як ЦП використовує її для диспетчеризації обробників переривань
Пояснити, як працюють інструкції INT та IRET, включно зі станом, що зберігається у стеку
Використовувати переривання BIOS (INT 10h для відео, INT 16h для клавіатури) та переривання DOS (INT 21h) в асемблерних програмах
Описати системні виклики Linux як сучасний еквівалент переривань DOS
Пояснити обмеження реального режиму та покращення, запроваджені захищеним режимом
Описати кільця захисту (Ring 0–3) та те, як вони забезпечують межі безпеки між ядром і програмами користувача
Щоразу, коли ви натискаєте кнопку живлення комп'ютера, розгортається точно зрежисована послідовність дій. За лічені секунди машина, в оперативній пам'яті якої не було жодної інструкції, завантажує цілу операційну систему з драйверами пристроїв, файловими системами та користувацьким інтерфейсом. Проте у момент увімкнення ЦП перебуває у найпримітивнішому стані: він нічого не знає про жорсткий диск, нічого про клавіатуру й нічого про операційну систему, встановлену на диску. Як процесор, здатний лише виконувати інструкції — по одній за раз, із пам'яті — піднімає себе з нічого?Це проблема <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Bootstrapping" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Bootstrapping" target="_self">початкового завантаження</a> (bootstrapping), названа за абсурдним образом «витягування себе за шнурки власних черевиків». Розв'язання полягає у пошаровій передачі керування: крихітна програма у мікропрограмі завантажує трохи більшу програму з диска, та — ще більшу, і так доти, доки не запуститься повноцінна операційна система. Кожен рівень довіряє тому, що попередній виконав свою роботу правильно, і кожен наступний рівень є функціональнішим за попередній.Лекція 7 — під назвою «Процес завантаження і функції OS. Паралелізм, захист, і великі об'єми RAM» — досліджує механізми, що забезпечують цю пошарову передачу керування, а потім розглядає фундаментальний інтерфейс між програмним та апаратним забезпеченням: переривання.Лекція організована у трьох частинах:Частина 1: Процес завантаження та початкове завантаження. Ми простежимо повний шлях від натискання кнопки живлення до запуску операційної системи. Це охоплює вектор скидання ЦП, самотестування при увімкненні (POST), мікропрограму BIOS, головний завантажувальний запис та, нарешті, ініціалізацію ядра. Розуміння процесу завантаження важливе не лише як системна концепція, а й тому, що воно безпосередньо пояснює, чому середовище реального режиму, в якому ви програмуєте, працює саме так, як працює.Частина 2: Переривання — ТВП, INT/IRET, сервіси BIOS і DOS, системні виклики Linux. Ми детально розглянемо механізм переривань: як ЦП диспетчеризує переривання через таблицю векторів переривань (ТВП), як інструкції INT та IRET працюють на рівні стека, і як BIOS та DOS надають сервіси через програмні переривання. Далі ми пов'яжемо це із сучасним світом, розглянувши системні виклики Linux як наступника інтерфейсу переривань DOS у захищеному режимі.Частина 3: Захищений режим — кільця безпеки, велика пам'ять і паралелізм. Ми повернемося до розмежування реального та захищеного режимів, представленого в L06, і заглибимося далі: розглянемо апаратну систему кілець привілеїв (Ring 0 — Ring 3), як захищений режим дозволяє адресувати значно більше ніж 1 МБ пам'яті та як він забезпечує основу для багатозадачності. Цей розділ заповнює концептуальний розрив між однопрограмним середовищем DOS вашої поточної лабораторної роботи та багатопроцесним середовищем Linux, до якого ви перейдете пізніше у курсі.Ви можете запитати, чому курс, зосереджений на програмуванні на асемблері, приділяє час процесу завантаження. Причина пряма та практична: середовище реального режиму, в якому ви програмуєте — DOSBox, що виконує DOS — існує саме завдяки процесу завантаження, який ми зараз опишемо. Коли DOSBox запускається, він емулює стан IBM PC, що завершив завантаження в DOS. Карта пам'яті, яку використовують ваші програми (ТВП за адресою 0000:0000, відеопам'ять за адресою B800:0000, префікс програмного сегмента на початку ваших файлів .COM) — це карта пам'яті, яку BIOS і DOS налаштовують під час завантаження. Сервіси переривань, які ви викликаєте (INT 21h для функцій DOS, INT 10h для відео), були встановлені під час завантаження. Розуміння того, звідки беруться ці структури, перетворює їх із довільних конвенцій на логічні наслідки послідовності завантаження.Крім того, коли ми перейдемо до програмування на асемблері в Linux далі у курсі (лабораторні роботи 11–15), ви працюватимете у принципово іншому середовищі — такому, що виникло внаслідок іншого шляху завантаження. Розуміння обох шляхів прояснить, чому ці два середовища настільки кардинально відрізняються.Ця лекція спирається на кілька концепцій з попередніх частин курсу:
Адресація Сегмент:Зміщення (L03) — BIOS, ТВП та MBR працюють на основі сегментованої моделі пам'яті реального режиму. Ми посилатимемося на пари Сегмент:Зміщення протягом усієї лекції, але не виводитимемо повторно формулу обчислення фізичної адреси.
Операції зі стеком (L05) — інструкція INT зберігає стан у стеку способом, що тісно аналогічний до CALL, а IRET відновлює його подібно до спеціалізованої RET. Розуміння PUSH, POP та вказівника стека є необхідним.
Регістр прапорців (FLAGS) (L04) — зокрема прапорець переривань IF (Interrupt Enable), який контролює, чи реагує ЦП на апаратні переривання.
Реальний режим проти захищеного режиму (L06, Розділ 10) — ми представили базове розмежування та карту пам'яті. Ця лекція розширює це введення механізмом переривань і системою кілець привілеїв.
Коли комп'ютер вимкнено, усе його програмне забезпечення — операційна система, застосунки та дані користувача — існує лише на енергонезалежному сховищі: жорстких дисках, SSD або флеш-пам'яті. Регістри ЦП очищено. Оперативна пам'ять порожня (або, точніше, містить непередбачувані значення — залишкові електричні заряди в комірках DRAM з часом розсіюються без регенерації). Процесор не має завантаженої операційної системи, драйверів пристроїв і жодного уявлення про те, яке обладнання його оточує.Це створює фундаментальну проблему «курка чи яйце». Операційна система — це програма, що зберігається на диску, але читання з диска потребує дискового драйвера, який є частиною операційної системи. ЦП потребує інструкцій для виконання, але інструкції знаходяться на пристрої зберігання, до якого він ще не знає, як звертатися. Навіть оперативна пам'ять, де програми мають перебувати для виконання, потребує ініціалізації — необхідно сконфігурувати контролер пам'яті, задати параметри тактування DRAM і перевірити пам'ять на помилки, перш ніж її можна буде надійно використовувати.Проте ЦП — проста машина: він зчитує інструкцію з пам'яті, виконує її і переходить до наступної. Він не може «шукати операційну систему» або «ініціалізувати обладнання» самостійно — це програмні концепції, а жодного програмного забезпечення ще не запущено. Він здатен лише виконувати інструкцію за адресою, на яку вказує його вказівник інструкцій. Тож найперше питання процесу завантаження: де ЦП знаходить свою першу інструкцію?Відповідь, як ми побачимо, елегантно проста: ЦП апаратно запрограмований звертатися до конкретної фіксованої адреси. За цією адресою чипсет материнської плати забезпечує відображення мікросхеми енергонезалежної пам'яті (мікропрограми BIOS/UEFI) — не оперативної пам'яті, яка ненадійна на цьому етапі, а мікросхеми флеш-ROM, що зберігає свій вміст без живлення. Яка б інструкція не була записана там — саме вона стане першою інструкцією, яку комп'ютер коли-небудь виконає. Від цієї єдиної інструкції розгортається весь процес завантаження.<br>Відповідь — це <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Reset_vector" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Reset_vector" target="_self">вектор скидання</a> (reset vector) — фіксована фізична адреса, на яку ЦП апаратно запрограмований переходити після скидання. Це не налаштування, що піддається конфігурації; воно не зберігається у жодному регістрі чи комірці пам'яті, які може змінити програмне забезпечення. Воно вбудоване у кремній процесора, є частиною конструкції мікросхеми від моменту виробництва. Кожен процесор x86, від оригінального 8086 до найновіших моделей, починає виконання з наперед визначеної адреси після ввімкнення живлення або апаратного скидання.Концепція вектора скидання не є унікальною для x86. Практично кожна процесорна архітектура має такий, хоча конкретна адреса різниться: процесори ARM зазвичай стартують з 0x00000000, MIPS — з 0xBFC00000, а Motorola 68000 зчитує свою початкову адресу з 0x00000004. Вибір сімейства x86 розміщувати вектор скидання біля верхньої межі адресного простору (а не за адресою 0) є незвичним і відображає проєктне рішення залишити нижню пам'ять доступною для таблиці векторів переривань та інших системних структур даних.Конкретна адреса еволюціонувала разом із сімейством процесорів:Зверніть увагу на закономірність: кожне наступне покоління процесорів розміщує вектор скидання на 16 байтів нижче верхньої межі свого адресованого простору пам'яті. Чому 16 байтів? Тому що це якраз достатньо місця для інструкції далекого JMP — і перехід — це все, що там має бути. Вектор скидання — це не початок BIOS; це трамплін, що перенаправляє до початку BIOS.8086 у деталях. Стан після скидання встановлює CS = FFFFh та IP = 0000h, формуючи сегментовану адресу FFFFh:0000h, яка відповідає фізичній адресі FFFF0h (згадайте формулу Сегмент:Зміщення з L03: фізична адреса = сегмент * 16 + зміщення). Це потрапляє на 16 байтів нижче верхньої межі 1 МБ адресного простору 8086. Інструкція за цією адресою зазвичай така:JMP FAR F000:E05B ; Перехід до точки входу ініціалізації BIOS
<br><img alt="Вектор скидання у Turbo Debugger" src="assets/l07-reset-vector-td.png" target="_self">Turbo Debugger показує першу інструкцію за адресою F000:FFF0 — JMP до точки входу BIOS за адресою F000:E05B.Цей далекий перехід перенаправляє ЦП до фактичного початку коду BIOS, що знаходиться за нижчою адресою в межах сегмента F000h. ROM BIOS займає верхні 64 КБ 1 МБ адресного простору (сегмент F000h), тому BIOS материнської плати відображається за сегментом F000h у звичайній карті пам'яті.<br>80386 та пізніші процесори. Вектор скидання переміщується на FFFFFFF0h — 16 байтів нижче межі 4 ГБ. Це створює загадку: процесор стартує у реальному режимі, який номінально обмежує його 1 МБ адресного простору. Як він може звернутися до адреси поблизу 4 ГБ? Відповідь передбачає тонкий апаратний трюк: після скидання базова частина регістра CS ініціалізується значенням FFFF0000h, а не звичайним значенням реального режиму. Це дозволяє початковому зверненню за інструкцією досягти мікросхеми BIOS, відображеної біля верхньої межі 32-бітного адресного простору. Чипсет материнської плати (історично — <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Northbridge_(computing)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Northbridge_(computing)" target="_self">північний міст</a>) перехоплює це звернення до пам'яті й направляє його до мікросхеми флеш-пам'яті BIOS, а не до оперативної пам'яті. Після виконання першого далекого переходу CS повертається до нормальної поведінки реального режиму, і BIOS продовжує виконання у межах стандартного діапазону адрес 1 МБ.Цей підхід елегантний у своїй простоті: незалежно від того, в якому стані перебувала система до ввімкнення, ЦП завжди стартує з тієї самої фіксованої адреси, і за цією адресою завжди знаходиться код мікропрограми, що знає, як ініціалізувати обладнання. Ланцюг початкового завантаження починається.<br>Код за вектором скидання належить <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/BIOS" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/BIOS" target="_self">BIOS</a> — базовій системі введення/виведення (Basic Input/Output System). BIOS — це мікропрограма: програмне забезпечення, що постійно зберігається на мікросхемі на материнській платі (спочатку в ROM, пізніше у флеш-пам'яті, яку можна оновлювати). Це перше програмне забезпечення, яке виконує ЦП, і воно має дві критичні функції:
Ініціалізація та тестування обладнання — BIOS має виявити наявне обладнання, перевірити його справність і налаштувати у відомий робочий стан.
Завантаження ОС — після ініціалізації обладнання BIOS має знайти та завантажити операційну систему (або, точніше, завантажувач першого рівня) з пристрою зберігання.
<br><img alt="Мікросхеми BIOS EPROM" src="assets/l07-bios-chip-photo.jpg" target="_self">Мікропрограма BIOS, що зберігається на мікросхемах EPROM. Нижня мікросхема (AMD AM27C256, 1986) має кварцове віконце для ультрафіолетового стирання — попередник сучасного флеш-сховища мікропрограм.BIOS також надає бібліотеку базових процедур взаємодії з обладнанням — «базове введення/виведення» у його назві — доступних через програмні переривання. Ці сервіси переривань BIOS (INT 10h для відео, INT 13h для диска, INT 16h для клавіатури тощо) забезпечують стандартизований інтерфейс, на який ранні операційні системи на кшталт DOS значною мірою покладалися. Ми детально розглянемо їх у розділі про переривання цієї лекції.Код BIOS розміщується у верхній частині адресного простору реального режиму. Згадайте карту пам'яті з L06 (Розділ 10.1): ROM BIOS відображений у сегменті F000h (F0000h–FFFFFh), займаючи верхні 64 КБ 1 МБ адресного простору реального режиму. Це постійна пам'ять, доступна лише для читання — ваші програми не можуть її перезаписати, і вона залишається доступною протягом усього часу роботи машини в реальному режимі. Коли ваша асемблерна програма виконує INT 10h для виведення символу, ЦП шукає адресу обробника переривання у ТВП, і ця адреса вказує на ROM BIOS у сегменті F000h.Назва «BIOS» походить від операційної системи CP/M (1975), де вона позначала рівень абстракції обладнання. Коли IBM розробляла оригінальний IBM PC у 1981 році, вона перейняла як концепцію, так і назву. BIOS від IBM був пропрієтарним, але такі компанії, як Compaq, Phoenix Technologies та AMI, шляхом зворотної інженерії створили сумісні версії, що уможливило цілу індустрію клонів IBM PC. Ця стандартизація де-факто означала, що будь-яка операційна система, написана для інтерфейсу BIOS IBM PC, працюватиме на будь-якій сумісній машині.<br>UEFI: сучасний наступник. Сучасні комп'ютери здебільшого замінили традиційний BIOS на <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/UEFI" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/UEFI" target="_self">UEFI</a> (Unified Extensible Firmware Interface — уніфікований розширюваний інтерфейс мікропрограм). UEFI усуває багато обмежень застарілого BIOS: підтримує завантаження з дисків більше 2 ТБ (таблиця розділів MBR обмежена 2 ТБ через 32-бітну адресацію секторів), надає багатше середовище до завантаження з графічним інтерфейсом і підтримкою миші, може працювати в 32-бітному або 64-бітному режимі з самого початку і повністю усуває обмеження 512 байтів MBR. Проте концептуальна роль залишається тією самою — UEFI є мікропрограмою, що ініціалізує обладнання та завантажує операційну систему. У цьому курсі ми зосереджуємося на процесі завантаження через застарілий BIOS, оскільки він безпосередньо відповідає середовищу реального режиму, в якому ви працюєте, і тому що розуміння застарілого шляху полегшує оцінку покращень UEFI.<br>Перше важливе завдання, яке BIOS виконує після отримання керування, — це <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Power-on_self-test" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Power-on_self-test" target="_self">самотестування при увімкненні</a>, або POST (Power-On Self-Test). Це систематичний діагностичний процес, що перевіряє справність основних апаратних компонентів комп'ютера перед спробою завантаження операційної системи. Якщо критичне обладнання вийшло з ладу, немає сенсу намагатися завантажити ОС — POST виявляє ці збої на ранньому етапі.Коли ЦП починає виконувати код BIOS, мікропрограма має спочатку створити власне робоче середовище. Обсяг цієї ініціалізації залежить від типу завантаження:
Холодне завантаження (повне ввімкнення після того, як машина була вимкнена) — BIOS виконує повну послідовність POST з нуля. Усе обладнання має бути виявлене, протестоване та ініціалізоване. Це повільний шлях.
Тепле завантаження (ініційоване комбінацією Ctrl+Alt+Del або програмним викликом INT 19h) — BIOS визначає цю умову за спеціальним значенням прапорця, збереженим в енергонезалежній пам'яті (CMOS), і може пропустити тривалий тест пам'яті та частину повторної ініціалізації обладнання. Система щойно працювала, тому BIOS може обґрунтовано припустити, що обладнання, яке функціонувало мить тому, все ще справне.
Саме це розмежування холодного та теплого завантаження пояснює, чому перезавантаження комп'ютера (тепле завантаження) помітно швидше, ніж його ввімкнення з повністю вимкненого стану (холодне завантаження). Сучасні системи UEFI додатково оптимізували це за допомогою опцій «швидкого завантаження», що пропускають ще більше перевірок обладнання, виходячи з припущення, що обладнання рідко виходить з ладу між перезавантаженнями.POST проходить через структуровану послідовність перевірок обладнання. Основні завдання включають:
Перевірка регістрів ЦП — підтвердження того, що регістри процесора можуть бути коректно записані та прочитані. ЦП зі «залиплим» бітом регістра не може надійно виконувати жодне програмне забезпечення.
Перевірка цілісності коду BIOS — мікропрограма перевіряє власну контрольну суму, щоб переконатися, що код BIOS не пошкоджений. Пошкодженому BIOS не можна довіряти правильну перевірку чого-небудь іншого.
Перевірка оперативної пам'яті — POST визначає обсяг і тестує основну пам'ять системи. Це один із найбільш часозатратних кроків POST: BIOS записує тестові шаблони (наприклад, чергування байтів 0x55 та 0xAA) у кожну комірку пам'яті та зчитує їх назад для перевірки коректності. Будь-яка комірка, що повертає значення, відмінне від записаного, вказує на несправну комірку пам'яті. Саме тому старіші комп'ютери відображали «лічильник пам'яті» під час завантаження — можна було спостерігати, як POST тестує кожен блок оперативної пам'яті, а лічильник збільшується від 0 до загального обсягу встановленої пам'яті. Сучасні системи з гігабайтами оперативної пам'яті потребували б хвилин для тестування кожного байта, тому опції швидкого завантаження часто пропускають або скорочують цю перевірку.
Перевірка базових компонентів — тестуються контролер DMA (прямого доступу до пам'яті), системний таймер та контролер переривань. Вони є фундаментальними для всіх подальших апаратних операцій.
Опитування та ініціалізація обладнання — POST виявляє та ініціалізує інші компоненти системи: чипсет, системні шини, відеокарту, клавіатуру, контролери сховищ та інші інтегровані периферійні пристрої. Пізніші версії BIOS також сканують ROM розширень (ROM опцій) — мікропрограми, вбудовані на картах розширення, як-от відеоадаптери чи мережеві карти, — і виконують їхні процедури ініціалізації.
Що відбувається, якщо POST виявляє збій? Відповідь залежить від того, який компонент вийшов з ладу і коли у послідовності POST збій було виявлено.На ранніх етапах POST відеосистема може бути ще не ініціалізована, тому BIOS не може відобразити повідомлення про помилку на екрані. Натомість він використовує вбудований динамік материнської плати для генерації звукових сигналів (beep codes) — певних послідовностей звукових сигналів, що вказують на компонент, який вийшов з ладу. Наприклад, один довгий сигнал з наступними трьома короткими може означати збій відеокарти, тоді як безперервні короткі сигнали можуть означати збій оперативної пам'яті (точні коди відрізняються залежно від виробника BIOS — AMI, Award та Phoenix використовують різні шаблони). Це розумне інженерне рішення: динамік ПК підключений безпосередньо до системного таймера і не потребує складної ініціалізації, тому його можна задіяти навіть коли більша частина системи непрацездатна.Якщо збій відбувається після ініціалізації відеосистеми, BIOS може відобразити повідомлення про помилку на екрані, наприклад «CMOS checksum error» або «Keyboard error». Деякі збої є фатальними (POST зупиняється і система не може завантажитися), тоді як інші — нефатальні (BIOS відображає попередження, але продовжує завантаження).Багато сучасних реалізацій BIOS та UEFI приховують процес POST за заставкою з логотипом виробника (екран «тихого завантаження»). Традиційний текстовий вивід POST — що показує лічильники пам'яті, виявлені накопичувачі та перевірки компонентів — зазвичай можна знову увімкнути в налаштуваннях BIOS для діагностичних цілей.Якщо POST завершується успішно, BIOS підтвердив, що основне обладнання машини працює. Оперативна пам'ять розподілена та протестована. Відеосистема ініціалізована (текст видно на екрані). Контролер клавіатури активний. Пристрої зберігання виявлені. Вікно запиту налаштувань BIOS пройдене (короткий момент, коли натискання Del або F2 входить у конфігурацію). Машина тепер готова до наступної фази: пошуку та завантаження операційної системи.<br>Після завершення POST BIOS викликає <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/BIOS_interrupt_call" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/BIOS_interrupt_call" target="_self">INT 19h</a> — переривання завантажувача — для початку пошуку завантажувального пристрою. BIOS підтримує список порядку завантаження, що зберігається в енергонезалежній пам'яті CMOS і доступний для налаштування користувачем через утиліту налаштування BIOS (екран, який ви бачите при натисканні Del або F2 під час запуску). Цей список визначає пріоритет пристроїв для перевірки:
Жорсткий диск (або SSD)
USB-флеш-накопичувач
Оптичний диск (CD/DVD)
Мережа (PXE-завантаження)
Інші пристрої
BIOS перевіряє кожен пристрій по черзі, намагаючись прочитати перший сектор (512 байтів) із пристрою. Він робить це за допомогою власних процедур доступу до диска — на цьому етапі це єдиний спосіб читання зі сховища, оскільки жодна операційна система чи драйвер ще не завантажені. Щоб пристрій вважався завантажувальним, цей перший сектор має закінчуватися специфічною двобайтовою сигнатурою: 0x55, за яким слідує 0xAA на байтових зміщеннях 510 та 511. Це завантажувальна сигнатура (іноді звана «магічним числом»):Offset: 0x000 ... 0x1FD 0x1FE 0x1FF
Content: [boot code...] 0x55 0xAA
Якщо BIOS зчитує сектор, що не закінчується цією сигнатурою, він вважає пристрій незавантажувальним, пропускає його та переходить до наступного у порядку завантаження.Якщо завантажувальний пристрій не знайдено взагалі, старіші версії BIOS відображали «No ROM BASIC» (пережиток оригінального IBM PC, який мав інтерпретатор BASIC у ROM як запасний варіант) або «No bootable disk found.» Сучасна мікропрограма просто входить в утиліту конфігурації BIOS або відображає повідомлення про помилку.<br>Мережеве завантаження. Одним із варіантів завантаження, який варто відзначити, є мережеве завантаження <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Preboot_Execution_Environment" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Preboot_Execution_Environment" target="_self">PXE</a> (Preboot Execution Environment — середовище виконання до завантаження). Коли BIOS пробує мережевий адаптер як завантажувальний пристрій, мікропрограма адаптера (або вбудована підтримка PXE у BIOS) виконує таку послідовність:
Надсилає широкомовний DHCP-запит у локальну мережу для отримання IP-адреси
Отримує DHCP-відповідь, що містить адресу TFTP-сервера та ім'я файлу завантажувального образу
Завантажує цей образ із сервера за допомогою TFTP (Trivial File Transfer Protocol — спрощений протокол передачі файлів)
Розміщує завантажений образ у пам'яті та передає йому керування, так само як і MBR, завантаженому з диска
Це дозволяє бездисковим робочим станціям завантажуватися з центрального сервера — техніка, широко використовувана в університетських комп'ютерних лабораторіях, корпоративних середовищах та хмарних дата-центрах, де централізоване керування образами операційних систем є практичнішим, ніж обслуговування окремих дисків на кожній машині.<br>Коли BIOS знаходить завантажувальний пристрій, він завантажує перший 512-байтовий сектор у пам'ять за фізичною адресою 0x7C00 (або в нотації Сегмент:Зміщення — 0000:7C00) і передає керування за цією адресою. Цей сектор є <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Master_boot_record" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Master_boot_record" target="_self">головним завантажувальним записом</a> (MBR — Master Boot Record), і він має чітко визначену структуру:Код завантажувача MBR — це завантажувач першого рівня — надзвичайно обмежена програма, що має вміститися всього у 446 байтів. Працюючи у 16-бітному реальному режимі, вона стикається з непростим інженерним завданням: у межах цих 446 байтів вона має розібрати таблицю розділів, визначити активний (завантажувальний) розділ, завантажити перший сектор цього розділу (завантажувальний запис тому) та передати йому керування. Вона також має обробити помилкові ситуації (активний розділ не знайдено, помилка читання диска) та відобразити мінімальні повідомлення про помилки — і все це у просторі, меншому за один твіт.Чому 0x7C00? Вибір адреси завантаження 0x7C00 — це історичний артефакт оригінального IBM PC 5150 (1981). IBM PC постачався з мінімумом 32 КБ оперативної пам'яті. Розробникам BIOS в IBM потрібне було місце, що залишає достатньо простору нижче для таблиці векторів переривань та області даних BIOS (які займають перші приблизно 1,25 КБ), і достатньо простору вище для використання завантажувачем як стека та робочого простору. Адреса 0x7C00 — це рівно 1 КБ нижче позначки 32 КБ (32 КБ = 0x8000, мінус 1 КБ = 0x7C00), що надає завантажувальному сектору приблизно 30 КБ придатної пам'яті нижче нього та залишає верхню частину 32 КБ для стека. Ця, на перший погляд, довільна адреса зберігається вже понад чотири десятиліття обчислень на x86.Обмеження суворе, але не нездоланне. Оригінальний код MBR для IBM PC, написаний Девідом Літтоном в IBM, досягав усього цього приблизно у 200 байтах вручну оптимізованого асемблера 8086 — свідчення мистецтва ефективного низькорівневого програмування.Це друга ланка ланцюга початкового завантаження. BIOS, що є мікропрограмою на материнській платі, завантажив крихітну програму з диска. Ця крихітна програма, у свою чергу, завантажить більшу програму — завантажувач другого рівня.<br>Завантажувач другого рівня (такий як <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/GNU_GRUB" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/GNU_GRUB" target="_self">GRUB</a> у Linux або BOOTMGR у Windows) не обмежений 512 байтами. Він може займати кілобайти або навіть мегабайти дискового простору, що дає йому можливість реалізувати розвинену функціональність:
Підтримка файлових систем — завантажувач другого рівня розуміє структури файлових систем (FAT, ext4, NTFS) і може знаходити файли за іменем, а не за номером сектора.
Інтерактивне меню завантаження — якщо встановлено кілька операційних систем, завантажувач показує меню, що дозволяє користувачу обрати, яку з них завантажити.
Завантаження ядра — завантажувач зчитує образ ядра ОС із файлової системи, розміщує його у відповідному місці пам'яті та готує початкову файлову систему у пам'яті (initrd/initramfs), яка потрібна ядру.
Передача параметрів завантаження — завантажувач може передавати параметри конфігурації ядру, такі як пристрій кореневої файлової системи, прапорці завантаження або параметри налагодження.
<br>Засоби безпеки — сучасні завантажувачі можуть перевіряти криптографічні підписи на образі ядра перед його завантаженням, гарантуючи, що ядро не було підроблене. Це частина ланцюга довіри <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/UEFI#Secure_Boot" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/UEFI#Secure_Boot" target="_self">Secure Boot</a> (безпечного завантаження).
Типове меню завантаження GRUB може виглядати так:GNU GRUB version 2.06 Ubuntu, with Linux 5.15.0-91-generic Ubuntu, with Linux 5.15.0-91-generic (recovery mode) Ubuntu, with Linux 5.15.0-88-generic Windows Boot Manager (on /dev/sda1) Use the ↑ and ↓ keys to select which entry is highlighted. Press enter to boot the selected OS, `e` to edit the commands before booting or `c` for a command-line.
Користувач обирає пункт, і завантажувач завантажує відповідне ядро. Якщо вибір не зроблено протягом налаштованого часу очікування (зазвичай кілька секунд), автоматично завантажується пункт за замовчуванням.<br>Шлях завантаження UEFI. У системах UEFI процес дещо відрізняється. UEFI взагалі не використовує MBR; натомість він зчитує завантажувач безпосередньо з виділеного <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/EFI_system_partition" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/EFI_system_partition" target="_self">системного розділу EFI</a> (ESP), відформатованого як FAT32. Мікропрограма UEFI сама розуміє файлову систему FAT, тому може знайти та завантажити файл завантажувача (наприклад, \EFI\BOOT\BOOTX64.EFI) без жодного посередника першого рівня. Це повністю усуває обмеження у 446 байтів і є однією з ключових переваг UEFI перед застарілим BIOS.Після того як завантажувач розмістив ядро операційної системи в оперативній пам'яті та передав йому керування, ядро бере на себе управління. Початкові завдання ядра включають:
Перехід режиму — одна з перших дій ядра — перемикання ЦП з реального режиму в захищений режим (або безпосередньо в 64-бітний довгий режим на сучасних системах). Це критичний, незворотний момент: процесор переходить із середовища 1 МБ без захисту BIOS у повний адресний простір 4 ГБ (або більший) із захистом пам'яті та рівнями привілеїв. Після цього перемикання сервіси переривань BIOS (INT 10h, INT 13h тощо) більше недоступні — ядро мусить використовувати власні драйвери відтепер. Ми детально розглянемо захищений режим у Частині 3 цієї лекції.
Ініціалізація пристроїв — ядро опитує апаратні пристрої та завантажує відповідні драйвери. На відміну від BIOS, який забезпечує лише базовий доступ до обладнання, ядро завантажує повнофункціональні драйвери, що можуть використовувати всі можливості обладнання.
Монтування файлової системи — ядро монтує кореневу файлову систему, роблячи структуру каталогів диска доступною для системи. До цього моменту система могла лише зчитувати необроблені сектори диска; після монтування вона може звертатися до файлів за іменем та шляхом. Це часто двоетапний процес: спочатку ядро монтує початковий RAM-диск (initramfs), що містить рівно стільки драйверів та інструментів, скільки потрібно для пошуку та монтування реальної кореневої файлової системи на диску.
Запуск служб і демонів — ядро запускає перший процес простору користувача, який має ідентифікатор процесу 1 (PID 1). У сучасному Linux це systemd; у традиційних Unix-системах це був init. Цей перший процес далі запускає всі інші системні служби: мережу, аудіо, диспетчери друку, SSH-сервери, менеджери дисплея та будь-які інші демони, необхідні системі.
Середовище виконання — нарешті, система представляє користувацький інтерфейс — запрошення до входу, стільничне середовище або командну оболонку — і комп'ютер готовий до використання.
На цьому етапі процес завантаження завершено. Операційна система має повний контроль, працює у захищеному режимі (або довгому режимі), а доступ до обладнання здійснюється через драйвери та системні виклики, а не через переривання BIOS. Мікропрограма BIOS, MBR і завантажувач усі виконали своє призначення і більше не є активними — хоча ROM BIOS залишається відображеним у пам'яті (за сегментом F000h) з історичних причин та для сумісності.Уся послідовність, від першої інструкції ЦП за вектором скидання до повністю працездатного робочого столу, зазвичай займає лише кілька секунд на сучасному обладнанні з NVMe-сховищем та оптимізованою мікропрограмою. Проте вона передбачає вражаючий ланцюг довіри та передач керування: апаратно зафіксована адреса у кремнії — мікропрограма — сектор диска — завантажувач — ядро — простір користувача. Кожен рівень є функціональнішим за попередній, і кожен залежить від того, що попередній рівень виконав свою роботу правильно. Збій на будь-якому етапі розриває весь ланцюг — саме тому розуміння процесу завантаження є необхідним для діагностики проблем типу «мій комп'ютер не запускається».Зверніть увагу, як кожен етап процесу завантаження обмежений тим, що налаштував попередній етап. ЦП може лише перейти за вектором скидання, бо це закладено апаратно. BIOS може лише тестувати обладнання за допомогою коду, вбудованого у мікропрограму. MBR може використовувати лише сервіси переривань BIOS (як-от INT 13h для читання диска), бо жодного іншого механізму доступу до диска ще не існує. Завантажувач другого рівня може використовувати ті самі сервіси BIOS для читання структур файлової системи. А ядро, завантажившись, замінює все це власними драйверами та бере повний контроль над машиною. Кожен рівень здійснює початкове завантаження наступного.Повний процес завантаження можна підсумувати як серію передач керування:<br><img alt="Хронологія послідовності завантаження" src="assets/l07-boot-sequence-timeline.png" target="_self">Повна хронологія завантаження: ініціалізація BIOS через MBR і завантажувач у реальному режимі, потім перехід до захищеного режиму для ініціалізації ядра та процесів користувацького режиму.Кожен перехід рухається від простішого, більш обмеженого середовища до багатшого. ЦП починає, не знаючи нічого; BIOS дає йому доступ до обладнання; MBR дає доступ до диска; завантажувач дає доступ до файлової системи; а ядро дає все інше. Це і є початкове завантаження — машина буквально піднімає себе, рівень за рівнем, від нічого до повноцінної операційної системи.Ви могли помітити, що один термін постійно з'являється в описі процесу завантаження: переривання. BIOS викликає INT 19h для початку пошуку завантажувального пристрою. Завантажувальний сектор завантажується за конкретною адресою. Сервіси BIOS, такі як INT 10h та INT 13h, доступні для використання MBR та завантажувачем. Що саме являють собою ці переривання і як вони працюють? Це тема наступного розділу.У попередніх розділах ми простежили процес завантаження від моменту увімкнення ПК: ЦП починає виконувати код BIOS з постійної пам'яті, BIOS виконує POST та ініціалізує обладнання, а Таблиця векторів переривань заповнюється адресами процедур обслуговування BIOS. Але сам BIOS не містить операційної системи. Його останньою дією в послідовності завантаження є зчитування невеликого, але критично важливого фрагмента коду з найпершого сектора завантажувального диска: Головного завантажувального запису (MBR).<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Master_boot_record" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Master_boot_record" target="_self">Головний завантажувальний запис</a> — це структура даних розміром 512 байтів, яка займає перший сектор (сектор 0) розділеного пристрою зберігання. Запроваджений у 1983 році разом із PC DOS 2.0, MBR одночасно виконує дві функції: описує, як диск поділено на розділи, і містить невелику виконувану програму — завантажувач — яка розпочинає процес завантаження операційної системи.512 байтів MBR організовані у три області:<br><img alt="Шістнадцятковий дамп MBR" src="assets/l07-mbr-hex-dump.png" target="_self">Шістнадцятковий дамп реального MBR. Код завантаження займає перші 446 байтів. За зміщенням 0x1BE починаються записи таблиці розділів. Виділене 55 AA за зміщенням 0x1FE позначає сигнатуру завантаження. Зверніть увагу на повідомлення про помилки, вбудовані в код: "Invalid partition table", "Error loading operating system", "Missing operating system".Сигнатура завантаження (0x55AA) у кінці сектора слугує маркером дійсності. Коли BIOS зчитує сектор 0 із завантажувального диска, він перевіряє наявність цієї сигнатури. Якщо сигнатура відсутня, BIOS робить висновок, що диск не є завантажувальним, і переходить до наступного пристрою в черзі завантаження.Таблиця розділів займає 64 байти і містить рівно чотири записи, кожен по 16 байтів, за фіксованими зміщеннями:Кожен запис розділу містить байт індикатора завантаження: 0x80 позначає розділ як активний (завантажувальний), тоді як 0x00 позначає його як неактивний. Лише один розділ повинен бути позначений як активний одночасно. Запис також містить байт ідентифікатора системи (System ID), який визначає тип файлової системи:<br>Обмеження на чотири записи означає, що схема MBR підтримує максимум чотири основні розділи на диск. Використання 32-бітних адрес секторів обмежує максимальний адресований розмір диска до 2 ТіБ (2^32 × 512 байтів). Ці обмеження призвели до розробки схеми <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/GUID_Partition_Table" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/GUID_Partition_Table" target="_self">Таблиці розділів GUID</a> (GPT), яка значною мірою замінила MBR у сучасних системах.Область коду завантаження розміром 446 байтів містить невелику програму реального режиму. Коли BIOS завершує ініціалізацію, він зчитує 512 байтів MBR у фізичну пам'ять за адресою 0x7C00 і передає керування на цю адресу, виконуючи перехід на неї. ЦП у цей момент перебуває в реальному режимі, тому код завантаження є 16-бітним машинним кодом x86.Програма завантаження виконує просте, але важливе завдання:
Сканує таблицю розділів у пошуках запису, позначеного як активний (0x80)
Зчитує перший сектор цього активного розділу — його завантажувальний запис тому (Volume Boot Record, VBR)
Передає керування коду VBR
<br>Ця передача керування від MBR до VBR називається <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Chain_loading" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Chain_loading" target="_self">ланцюговим завантаженням</a>. VBR містить більш спеціалізований завантажувач, який розуміє файлову систему свого розділу і може знайти та завантажити безпосередньо ядро операційної системи. Наприклад, VBR розділу Windows знає, як знайти та завантажити NTLDR або bootmgr, тоді як VBR Linux може завантажити другий етап GRUB.Якщо код завантаження не може знайти активний розділ або VBR не вдається завантажити, MBR виводить одне з кількох знайомих повідомлень про помилки: "Invalid partition table", "Error loading operating system" або "Missing operating system".Підсумуємо, де MBR знаходиться в процесі завантаження, який ми вивчали досі:
Увімкнення живлення — ЦП починає виконувати код BIOS з постійної пам'яті
POST — BIOS тестує та ініціалізує обладнання
Заповнення ТВП — BIOS заповнює Таблицю векторів переривань адресами процедур обслуговування
Завантаження MBR — BIOS зчитує перший сектор завантажувального диска в пам'ять за адресою 0x7C00
Виконання коду завантаження — код MBR знаходить активний розділ і завантажує його VBR
VBR / завантажувач ОС — завантажувач операційної системи бере керування на себе
Ініціалізація ОС — ядро ОС завантажується, замінює обробники переривань BIOS власними службами
Крок 7 — це місце, де продовжується історія таблиці переривань. Щойно операційна система завантажується, вона встановлює власні процедури обслуговування — і це підводить нас до наступного розділу.Ми вже бачили, як система стартує: BIOS ініціалізує обладнання, заповнює ТВП і через ланцюгове завантаження за допомогою MBR запускає операційну систему. Але що відбувається з таблицею переривань після завантаження ОС? І як програми, що працюють під керуванням ОС, отримують доступ до системних служб? Цей розділ пояснює багаторівневу архітектуру служб, яка робить механізм переривань таким потужним.<br>Під час процесу завантаження BIOS робить більше, ніж просто тестує обладнання. Він завантажує в оперативну пам'ять колекцію службових функцій і реєструє їхні адреси в <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Interrupt_vector_table" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Interrupt_vector_table" target="_self">Таблиці векторів переривань</a>. Це служби переривань BIOS — стандартизовані процедури, які забезпечують низькорівневий доступ до обладнання:
<br>INT 10h — <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/INT_10H" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/INT_10H" target="_self">Служби відео</a>: встановлення відеорежиму, виведення символів, малювання пікселів
INT 13h — Служби диска: читання/запис секторів диска, отримання параметрів накопичувача
<br>INT 16h — <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/INT_16H" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/INT_16H" target="_self">Служби клавіатури</a>: зчитування натискань клавіш, перевірка стану буфера клавіатури
INT 14h — Служби послідовного порту
INT 17h — Служби принтера
Ці служби BIOS утворюють найнижчий рівень архітектури служб системи. Вони працюють у реальному режимі та взаємодіють безпосередньо з обладнанням через порти вводу-виводу та регістри, відображені в пам'ять. Будь-яка програма, що працює в реальному режимі, може викликати їх за допомогою інструкції INT.Коли операційна система завантажується, вона не відкидає служби BIOS повністю. Натомість вона надбудовується над ними, завантажуючи власні службові функції в оперативну пам'ять і оновлюючи ТВП так, щоб вона вказувала на ці нові процедури. ОС може: Додавати нові записи — ОС визначає номери переривань, які BIOS не використовував. Наприклад, MS-DOS займає INT 21h для власного комплексного набору служб. Замінювати існуючі записи — ОС може встановлювати власні обробники для переривань, які раніше обслуговував BIOS. Наприклад, ОС може замінити обробник диска BIOS (INT 13h) на більш досконалу версію, яка підтримує більші накопичувачі або реалізує кешування. Передавати керування обробникам BIOS — у деяких випадках обробник ОС виконує додаткову обробку, а потім викликає оригінальний обробник BIOS для виконання безпосередньої роботи з обладнанням. ОС зберігає оригінальний запис ТВП перед його перезаписом, щоб мати можливість звернутися до BIOS за потреби. Цей багаторівневий підхід означає, що ТВП є живою структурою даних, яка еволюціонує в процесі завантаження системи. Після того, як BIOS її ініціалізує, ОС її модифікує, а драйвери пристроїв можуть модифікувати її далі. Результат — єдина таблиця диспетчеризації, де кожен номер переривання направляє до відповідного обробника для поточного стану системи.<br>BIOS <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/INT_10H" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/INT_10H" target="_self">INT 10h</a> надає базові служби відео. Для його використання програма завантажує в AH номер підфункції, налаштовує інші регістри з параметрами та виконує INT 10h. Деякі часто використовувані підфункції:Наприклад, щоб вивести літеру 'A' у телетайпному режимі:mov ah, 0Eh ; Підфункція телетайпного виводу
mov al, 'A' ; Символ для виведення
mov bh, 0 ; Сторінка номер 0
int 10h ; Виклик служби відео BIOS
INT 10h є відносно повільним, оскільки обробляє по одному символу за раз через BIOS. Для застосунків, критичних до продуктивності, як-от ігри, програмісти обходять INT 10h і пишуть безпосередньо у відеопам'ять (починаючи з сегмента B800h у текстовому режимі). Але для загального виведення під час завантаження або в простих програмах INT 10h надає апаратно-незалежний інтерфейс, який працює з різними відеокартами.<br>BIOS <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/INT_16H" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/INT_16H" target="_self">INT 16h</a> обслуговує введення з клавіатури. Його найважливіші підфункції:Зчитування одного натискання клавіші:mov ah, 00h ; Підфункція зчитування натискання клавіші
int 16h ; Виклик служби клавіатури BIOS
; Тепер: AL = ASCII-код клавіші, AH = скан-код
Відмінність між AH=00h та AH=01h є важливою. AH=00h блокує — він зупиняє виконання програми, доки не буде натиснута клавіша. AH=01h є неблокуючою перевіркою: він встановлює прапорець нуля (Zero Flag), якщо жодна клавіша не доступна, що дозволяє програмі продовжувати виконання іншої роботи та періодично опитувати клавіатуру.<br>Коли MS-DOS завантажується, він встановлює комплексний набір служб під номером <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/DOS_API" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/DOS_API" target="_self">INT 21h</a>. Якщо переривання BIOS надають прямий доступ до обладнання, переривання DOS надають абстракції вищого рівня: файлові операції, керування каталогами, рядкове введення-виведення, виділення пам'яті та керування програмами.Ви вже використовували INT 21h у своїх лабораторних роботах. Тепер ми можемо пояснити, чому він працює саме так. Конвенція така сама, як і для служб BIOS: завантажити в AH номер підфункції, налаштувати параметри в інших регістрах і виконати INT 21h.Основні підфункції DOS:Повний приклад — виведення рядка та завершення:.model small
.stack 100h .data
msg db 'Hello from DOS!$' .code
main proc mov ax, @data mov ds, ax ; Вивести рядок за допомогою INT 21h, AH=09h mov ah, 09h lea dx, msg int 21h ; Завершити програму за допомогою INT 21h, AH=4Ch mov ah, 4Ch mov al, 0 ; Код повернення 0 (успіх) int 21h
main endp
end main
<br><img alt="Hello World у DOS з INT 21h" src="assets/l07-dos-hello-world-int21h.png" target="_self">Знайома програма: приклад Hello World використовує INT 21h двічі — з AH=09h для виведення рядка та AH=4Ch для завершення. Зелені стрілки виділяють виклики переривань.Це той самий шаблон, який ви використовуєте з Лабораторної 5. Послідовність mov ah, 4Ch / int 21h не просто «завершує програму» — вона викликає службу переривання DOS 21h, підфункцію 4Ch, яка виконує впорядковане завершення: закриває відкриті файли, звільняє виділену пам'ять і повертає керування командному інтерпретатору DOS.Повна картина утворює ієрархію рівнів служб:Кожен рівень надбудовується над нижчим. Файлові операції DOS зрештою викликають дискові процедури BIOS, які зрештою читають та пишуть апаратні порти. Механізм переривань забезпечує «зв'язок» між цими рівнями, а ТВП є таблицею пошуку, яка з'єднує номери переривань з адресами обробників.<br>Протягом цієї лекції ми використовували слово «переривання» в контексті викликів служб — коли програма свідомо викликає INT 21h або INT 10h. Але <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Interrupt" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Interrupt" target="_self">переривання</a> — це ширше і більш фундаментальне поняття в архітектурі комп'ютерів. Це основний механізм ЦП для реагування на події, незалежно від того, чи ці події походять від зовнішнього обладнання, чи від самої програми.Переривання — це запит до процесора призупинити виконання поточного коду, щоб можна було обробити конкретну подію. Коли ЦП приймає переривання, він:
Завершує поточну інструкцію (переривання розпізнаються на межах інструкцій)
Зберігає поточний стан процесора (FLAGS та CS:IP) на стеку
Шукає адресу обробника в Таблиці векторів переривань за номером переривання
Передає керування обробнику переривання (який також називають процедурою обслуговування переривання, або ISR)
Коли обробник завершує роботу, він виконує інструкцію IRET (Interrupt Return — повернення з переривання), яка відновлює FLAGS та CS:IP зі стеку, продовжуючи виконання точно з того місця, де воно було перервано.Саме цей механізм забезпечує роботу інструкції INT для викликів служб — але той самий механізм також обробляє натискання клавіш, тики таймера, сигнали завершення дискових операцій та аварійні ситуації.Переривання поділяються на дві фундаментальні категорії залежно від їхнього походження:Апаратні переривання — це сигнали, які надсилаються ЦП контролерами зовнішніх пристроїв через лінії IRQ (Запит на переривання). Коли відбувається апаратна подія — натискається клавіша, завершується передача даних з диска, спрацьовує системний таймер — контролер пристрою активує свою лінію IRQ, щоб привернути увагу ЦП.<br>Оригінальний IBM PC використовував Intel 8259 — <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Programmable_interrupt_controller" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Programmable_interrupt_controller" target="_self">Програмований контролер переривань</a> (PIC) — для керування апаратними перериваннями. PIC приймає сигнали переривань від кількох пристроїв і подає їх ЦП по одному, в порядку пріоритету. Стандартні призначення IRQ на оригінальному PC:Зверніть увагу, що апаратні переривання відображені на номери переривань 08h-0Fh у ТВП. Коли спрацьовує IRQ 1 (клавіатура), ЦП шукає запис 09h у ТВП та переходить до обробника переривання клавіатури. Цей обробник зчитує скан-код з контролера клавіатури (порт вводу-виводу 60h), перетворює його, розміщує в буфері клавіатури та повертається через IRET. Все це відбувається прозоро — програма, що виконується, навіть не знає, що її було перервано.<br><img alt="Обробка апаратних переривань" src="assets/l07-hardware-interrupts-diagram.png" target="_self">Апаратні переривання: контролери пристроїв (таймер, USB, клавіатура) сигналізують ЦП через контролер переривань. Кожен пристрій має виділену процедуру обробки переривання.Ключова характеристика: асинхронність. Апаратні переривання надходять у непередбачувані моменти, незалежно від того, що ЦП виконує в даний час. Переривання таймера (IRQ 0) спрацьовує приблизно 18,2 рази на секунду, незалежно від того, яка програма працює. Переривання клавіатури спрацьовує щоразу, коли користувач натискає або відпускає клавішу. ЦП повинен бути готовий обробити ці події на будь-якій межі інструкцій.Апаратні переривання можна маскувати (тимчасово вимкнути) за допомогою інструкції CLI (Clear Interrupt Flag — очистити прапорець переривань) та повторно увімкнути за допомогою STI (Set Interrupt Flag — встановити прапорець переривань). Коли переривання замасковані, ЦП ігнорує вхідні сигнали IRQ. Це корисно при виконанні критичної ділянки коду, яка не повинна бути перервана:cli ; Вимкнути апаратні переривання
; ... критична ділянка: модифікація спільних структур даних ...
sti ; Увімкнути апаратні переривання
Однак деякі переривання не можна маскувати. Немасковане переривання (NMI) сигналізує про критичний апаратний стан, наприклад помилку парності пам'яті або спрацювання сторожового таймера, який має бути оброблений негайно, незалежно від стану прапорця переривань.Програмні переривання ініціюються свідомо інструкцією INT n, де n — номер переривання (0-255). ЦП обробляє програмне переривання за точно таким самим механізмом, як і апаратне: зберігає FLAGS та CS:IP на стеку, шукає адресу обробника в записі n ТВП і переходить до нього. Єдина різниця — це тригер: явна інструкція, а не зовнішній сигнал.Програмні переривання мають два призначення:1. Виклики системних служб. Це те, що ми вивчали протягом усієї цієї лекції. Інструкція INT є воротами, через які програми запитують служби у BIOS та ОС:; Програмне переривання як виклик служби
mov ah, 02h ; DOS: вивести символ
mov dl, 'X' ; Символ для виведення
int 21h ; Виклик служби DOS — ПРОГРАМНЕ переривання
2. Винятки ЦП. Певні аварійні ситуації змушують ЦП автоматично генерувати програмне переривання. Вони ініціюються не інструкцією INT у програмі, а виявленням ЦП аномальної ситуації:Коли ЦП виконує INT n, відбувається така послідовність:1. Зберегти FLAGS на стеку
2. Очистити IF (прапорець переривань) та TF (прапорець трасування)
3. Зберегти CS на стеку
4. Зберегти IP на стеку
5. Завантажити CS:IP із запису n ТВП (адреса = n * 4)
6. Почати виконання обробника переривання
А коли обробник виконує IRET:1. Відновити IP зі стеку
2. Відновити CS зі стеку
3. Відновити FLAGS зі стеку (відновлюючи IF та TF)
4. Продовжити виконання за відновленою адресою CS:IP
Це дуже схоже на пару CALL / RET, з однією ключовою відмінністю: INT також зберігає та відновлює регістр FLAGS. Це необхідно, оскільки обробник переривання може змінити прапорці під час своєї роботи, а перерваний код повинен продовжити виконання з оригінальним станом прапорців.Згадаймо з Лекції 5, що CALL зберігає на стеку лише адресу повернення (CS:IP). Інструкція INT зберігає три значення: FLAGS, CS та IP. Це означає, що кадр стеку для переривання виглядає так: +-----------+
SP -&gt; | IP | (зміщення адреси повернення) +-----------+ | CS | (сегмент адреси повернення) +-----------+ | FLAGS | (збережені прапорці процесора) +-----------+ | ... | (попередній вміст стеку)
Ключове розуміння полягає в тому, що ЦП обробляє апаратні та програмні переривання ідентично після визначення номера переривання. Незалежно від того, чи спрацьовує IRQ 1 (відображене на INT 09h), чи програма виконує INT 09h, ЦП виконує абсолютно однакові кроки: зберігає стан, шукає запис 09h у ТВП, переходить до обробника. Сам обробник не може визначити, чи був він викликаний апаратно, чи програмно.Ця уніфікація проєктування є елегантною та потужною. Вона означає, що:
Одна й та сама ТВП обслуговує як апаратні, так і програмні переривання
Обробники переривань не потребують окремих механізмів для апаратного та програмного виклику
Програма може навмисно ініціювати обробник апаратного переривання, виконавши відповідну інструкцію INT (хоча на практиці це рідко буває корисним)
ОС може встановити єдиний обробник, який обслуговує як апаратні події, так і програмні запити для одного й того самого номера переривання
Механізм переривань — це нитка, що зв'язує все в цій лекції:
ТВП — це таблиця диспетчеризації, яку спочатку заповнює BIOS, а потім модифікує ОС
MBR та процес завантаження призводять до завантаження ОС, яка встановлює нові обробники переривань
Служби BIOS (INT 10h, INT 16h) доступні через програмні переривання
Служби DOS (INT 21h) доступні через програмні переривання
Апаратні пристрої (клавіатура, таймер) сигналізують ЦП через апаратні переривання
Усе це використовує один і той самий механізм ЦП: зберегти стан, знайти обробник, виконати, IRET
Ця уніфікована архітектура є одним з найвпливовіших проєктних рішень платформи x86. Вона надає єдиний, послідовний механізм для всього — від обробки натискання клавіші до запиту файлової операції й до відновлення після помилки ділення на нуль.На цьому етапі курсу ви вже використовували int 21h у своїх лабораторних програмах — для виведення рядків з AH=09h та для завершення програми з AH=4Ch. Тепер настав час зрозуміти повну картину того, що насправді надає переривання DOS 21h і як воно працює всередині.DOS резервує вектори переривань від 20h до 2Fh для власного використання. З них INT 21h є головним диспетчером служб — основним API, який прикладні програми використовують для взаємодії з операційною системою. Він надає понад 80 різних функцій, що охоплюють консольне введення/виведення, файлові операції, керування пам'яттю та керування програмами.Угода про виклик проста: помістіть номер функції в AH, завантажте будь-які додаткові параметри у відповідні регістри та виконайте int 21h. ЦП зберігає прапорці та адресу повернення на стеку, шукає адресу обробника в ТВП на позиції 21h (адреса пам'яті 0000:0084) і передає керування коду ядра DOS, що реалізує запитану функцію.У таблиці нижче наведено функції, з якими ви найімовірніше зіткнетесь і які використовуватимете:Обробка помилок: Багато файлових функцій сигналізують про помилки встановленням прапорця переносу (CF). Коли CF=1 після виклику, AX містить код помилки. Коли CF=0, операція виконана успішно і AX містить результат (наприклад, дескриптор файлу або кількість байтів). Завжди перевіряйте прапорець переносу після файлових операцій.Щоб вивести літеру 'A' на екран:mov ah, 02h ; Функція 02h: виведення символу
mov dl, 'A' ; DL = символ для виведення
int 21h ; Виклик DOS — 'A' з'являється на екрані
Функція 02h записує символ із DL у стандартний вивід. Вона також перевіряє Ctrl+C, тож користувач може перервати програму, яка виводить дані в циклі.Цей шаблон має бути знайомим з вашої лабораторної роботи починаючи з Lab 03:.data greeting db 'Hello, NaUKMA!', 0Dh, 0Ah, '$' .code mov ax, @data mov ds, ax mov ah, 09h ; Функція 09h: виведення рядка mov dx, offset greeting ; DS:DX вказує на рядок int 21h ; Вивести все до символу '$'
Зверніть увагу на термінатор '$' — це особливість, успадкована від CP/M. Знак долара позначає кінець рядка, а отже, функцію 09h не можна використати для виведення літерального символу $. Послідовність 0Dh, 0Ah — це повернення каретки + переведення рядка, що переміщує курсор на початок наступного рядка. mov ah, 01h ; Функція 01h: читання символу з відлунням int 21h ; Очікування натискання клавіші; AL = код ASCII ; AL тепер містить символ, який ввів користувач ; Символ також було автоматично виведено на екран mov bl, al ; Зберегти символ для подальшого використання mov ah, 02h ; Функція 02h: виведення символу mov dl, 0Dh ; Повернення каретки int 21h mov dl, 0Ah ; Переведення рядка int 21h mov dl, bl ; Збережений символ int 21h ; Вивести його ще раз на наступному рядку
Якщо потрібно прочитати символ без його відображення (наприклад, для введення пароля), використовуйте функцію 07h замість 01h.Кожна програма DOS повинна завершуватись викликом функції 4Ch. Ви робили це в кожній лабораторній роботі: mov ah, 4Ch ; Функція 4Ch: завершення програми mov al, 0 ; Код повернення 0 (успіх) int 21h ; Керування повертається до DOS (або батьківського процесу)
Код повернення в AL може бути перевірений пакетними файлами (ERRORLEVEL) або батьківським процесом за допомогою функції 4Dh. Код повернення 0 за домовленістю означає успіх; ненульові значення вказують на помилки.У той час як переривання DOS надають високорівневі служби ОС, переривання BIOS працюють на один рівень нижче — вони взаємодіють безпосередньо з апаратним забезпеченням через процедури прошивки, записані в системному ROM. Два найважливіших переривання BIOS — це INT 10h для відеослужб та INT 16h для служб клавіатури.<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/INT_10H" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/INT_10H" target="_self">INT 10h</a> — це 17-й запис у таблиці векторів переривань (10h = 16 у десятковій системі, але записи ТВП індексуються з нуля, тому це 17-й вектор). BIOS встановлює обробник реального режиму на цьому векторі, який надає відеослужби: встановлення відеорежиму, позиціювання курсора, виведення символів і рядків, а також читання/запис окремих пікселів у графічному режимі.Угода про виклик така сама, як і для DOS: завантажте AH номером підфункції, встановіть інші регістри за потреби та виконайте int 10h.Примітка щодо продуктивності: INT 10h працює відносно повільно, оскільки кожен виклик включає повний механізм переривання плюс код прошивки BIOS. З цієї причини програми, критичні до продуктивності (особливо ігри), часто обходять виклики BIOS і записують безпосередньо у відеопам'ять за сегментом B800h (текстовий режим) або A000h (графічний режим).<br>Обмеження режиму: На сучасних системах x86 виклики переривань BIOS працюють лише в реальному режимі або віртуальному режимі 8086. Їх не можна використовувати в захищеному режимі (32-бітному) або довгому режимі (64-бітному) без дорогих перемикань режимів. Сучасні операційні системи замість цього використовують спеціалізовані відеодрайвери. У системах UEFI INT 10h замінено на <a data-tooltip-position="top" aria-label="https://uefi.org/specs/UEFI/2.10/12_Protocols_Console_Support.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://uefi.org/specs/UEFI/2.10/12_Protocols_Console_Support.html" target="_self">UEFI Graphics Output Protocol (GOP)</a>.Щоб очистити весь екран, прокрутивши вікно вгору з нульовою кількістю рядків (що BIOS інтерпретує як «очистити»): mov ah, 06h ; Функція 06h: прокрутити вікно вгору mov al, 0 ; AL=0 означає очистити все вікно mov bh, 07h ; Атрибут: білий на чорному mov cx, 0000h ; Верхній лівий кут: рядок 0, стовпець 0 mov dh, 24 ; Нижній правий рядок (24 для 25-рядкового дисплея) mov dl, 79 ; Нижній правий стовпець (79 для 80-стовпцевого дисплея) int 10h ; Очистити екран ; Встановити курсор на рядок 10, стовпець 20 mov ah, 02h ; Функція 02h: встановити позицію курсора mov bh, 0 ; Сторінка 0 mov dh, 10 ; Рядок 10 mov dl, 20 ; Стовпець 20 int 10h ; Вивести 'X' яскраво-зеленим на позиції курсора mov ah, 09h ; Функція 09h: записати символ + атрибут mov al, 'X' ; Символ для запису mov bh, 0 ; Сторінка 0 mov bl, 0Ah ; Атрибут кольору: яскраво-зелений на чорному mov cx, 1 ; Записати один раз int 10h
Байт атрибуту кольору в BL має формат: біти 7-4 = колір фону, біти 3-0 = колір тексту. Поширені значення: 07h = білий на чорному (за замовчуванням), 0Ah = яскраво-зелений на чорному, 4Fh = білий на червоному.<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/INT_16H" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/INT_16H" target="_self">INT 16h</a> — це переривання BIOS для клавіатури. Воно дозволяє програмам читати натискання клавіш, перевіряти, чи є клавіша в буфері, та запитувати стан клавіш-модифікаторів (Shift, Ctrl, Alt, Caps Lock тощо).Скан-коди проти ASCII: Коли користувач натискає клавішу, INT 16h повертає два значення. AL містить код ASCII (наприклад, 41h для 'A'), тоді як AH містить скан-код — апаратний код, що ідентифікує фізичну клавішу незалежно від того, який символ вона представляє. Для спеціальних клавіш (стрілки, функціональні клавіші, Home, End) AL дорівнює 00h або E0h, а скан-код у AH визначає, яку клавішу було натиснуто. mov ah, 00h ; Функція 00h: читання натискання клавіші (очікування) int 16h ; Блокує, доки не буде натиснуто клавішу ; AH = скан-код клавіші ; AL = код ASCII (0, якщо спеціальна клавіша) cmp al, 1Bh ; Це Escape? (ASCII 1Bh) je exit_program cmp al, 0 ; Спеціальна клавіша? (стрілки, F-клавіші тощо) je handle_special ; Інакше AL містить введений символ
Іноді потрібно перевірити, чи доступна клавіша, без очікування (наприклад, у ігровому циклі):game_loop: ; ... оновити стан гри ... mov ah, 01h ; Функція 01h: перевірити буфер (неблокуюча) int 16h jz no_key ; ZF=1 означає, що немає клавіші — пропустити введення mov ah, 00h ; Клавіша доступна — прочитати її int 16h ; Тепер AH=скан-код, AL=ASCII ; ... обробити клавішу ... no_key: jmp game_loop
Цей шаблон є необхідним для програм реального часу: перевірити, чи є введення, обробити його, якщо так, і продовжити в будь-якому випадку.Функції DOS (INT 21h) надають високорівневі служби. Наприклад, функція 01h читає символ, обробляє Ctrl+C і виводить символ на екран — все за один виклик. Під капотом DOS сам викликає переривання BIOS для виконання фактичної роботи з апаратним забезпеченням.Функції BIOS (INT 10h, INT 16h) надають низькорівневий доступ. Вони дають прямий контроль над позиціюванням курсора, кольорами та неблокуючим введенням, але не забезпечують файлове введення/виведення, обробку рядків чи керування програмами. Використовуйте виклики BIOS, коли вам потрібен точний контроль над екраном або клавіатурою понад те, що пропонує DOS.Переходячи від 16-бітного світу DOS до 32-бітного Linux, концепція залишається тією самою — прикладні програми запитують служби ОС через контрольований шлюз — але механізм і угоди про найменування відрізняються. У Linux ці функції, надані ОС, називаються системними викликами (syscalls).У 32-бітному x86 Linux традиційний спосіб виклику системного виклику — через INT 80h. Шаблон повторює те, що ви бачили з DOS INT 21h:
Помістіть номер системного виклику в EAX (не в AH — Linux використовує повний 32-бітний регістр).
Помістіть аргументи в EBX, ECX, EDX, ESI, EDI, EBP (до 6 параметрів).
Виконайте int 80h.
Повернене значення з'являється в EAX (від'ємні значення вказують на помилки).
Зверніть увагу, що Linux не використовує спеціальний термінатор рядків. Натомість завжди передається і вказівник на дані, і їхня довжина — більш надійний підхід.Як і файлові дескриптори Unix: 0 = stdin, 1 = stdout, 2 = stderr.<br>Повну таблицю системних викликів можна знайти у файлі вихідного коду ядра arch/x86/entry/syscalls/syscall_32.tbl або в довідниках, таких як <a data-tooltip-position="top" aria-label="https://syscalls.w3challs.com/" rel="noopener nofollow" class="external-link is-unresolved" href="https://syscalls.w3challs.com/" target="_self">syscalls.w3challs.com</a>.; hello.asm — Linux x86 (32-біт) з використанням int 80h
; Асемблювання: nasm -f elf32 hello.asm
; Компонування: ld -m elf_i386 -o hello hello.o section .data msg db 'Hello, World!', 0Ah ; 0Ah = новий рядок len equ $ - msg ; Обчислити довжину рядка section .text global _start _start: ; write(1, msg, len) mov eax, 4 ; системний виклик #4 = write mov ebx, 1 ; fd = 1 (stdout) mov ecx, msg ; вказівник на рядок mov edx, len ; кількість байтів для запису int 80h ; виклик ядра ; exit(0) mov eax, 1 ; системний виклик #1 = exit mov ebx, 0 ; код завершення 0 int 80h ; виклик ядра (не повертає керування)
Порівняйте це з версією для DOS. Структура майже ідентична — завантажити номер функції, налаштувати параметри в регістрах, ініціювати переривання — але Linux використовує 32-бітні регістри, явні довжини замість термінаторів і інший вектор переривання. Також зверніть увагу, що програми Linux починаються з _start (не з main, якщо не використовується бібліотека C), і використовують директиви section замість .model / .data / .code.Механізм int 80h працює, але є відносно повільним. Кожна інструкція int включає повну послідовність переривання: збереження прапорців, пошук у ТВП, перемикання рівнів привілеїв і відновлення стану після цього.Сучасні процесори x86 надають спеціалізовані інструкції швидкого системного виклику, що обходять ТВП повністю:
SYSENTER / SYSEXIT (Intel, введено з Pentium II) — використовують модельно-специфічні регістри (MSR) для зберігання точки входу в ядро, уникаючи пошуку в ТВП.
SYSCALL / SYSRET (AMD, прийнято Intel для 64-бітних систем) — стандартний механізм на всіх системах x86-64.
У 64-бітному Linux користувацькі програми виконують системні виклики через інструкцію SYSCALL. Угоди також змінюються: номер системного виклику передається в RAX, а аргументи використовують RDI, RSI, RDX, R10, R8, R9 (відмінно від угоди int 80h).На практиці прикладні програмісти рідко пишуть int 80h або syscall безпосередньо. Бібліотека C (glibc, musl) обгортає кожен системний виклик у звичайну функцію, тому програма на C просто викликає write(), read() або exit(), а бібліотека виконує низькорівневі деталі. Але розуміння сирого механізму є необхідним для системних програмістів, дослідників безпеки та всіх, хто налагоджує програми на рівні асемблера.---## 10. Таблиця векторів переривань та механізм INT/IRETУ Розділі 9 ми побачили, що переривання — це механізм, за допомогою якого ЦП передає керування від запущеної програми до спеціалізованої процедури обробника. Але як ЦП дізнається, де знаходиться цей обробник? У Реальному режимі відповідь — це проста й елегантна структура даних, що займає перший кілобайт пам'яті: Таблиця векторів переривань (ТВП).ТВП Реального режиму x86 — це масив із 256 записів, розташований на самому початку фізичної пам'яті, починаючи з адреси 0000:0000. Кожен запис — це 4-байтний далекий вказівник, що містить адресу у форматі сегмент:зміщення відповідного обробника переривання (також відомого як процедура обслуговування переривання, або ISR). Структура таблиці проста:З 256 записами по 4 байти кожен, ТВП займає рівно 256 x 4 = 1024 байти (1 КБ) пам'яті, охоплюючи адреси від 0000:0000 до 0000:03FF.<br><img alt="Структура ТВП" src="assets/l07-ivt-structure.png" target="_self">Таблиця векторів переривань займає перші 1024 байти пам'яті (адреси 000000h–0003FFh). Кожен 4-байтний запис містить пару сегмент:зміщення. Перші 17 записів (INT 0–16) зарезервовані для винятків ЦП; записи 32–255 доступні для визначених користувачем обробників.<br><img alt="Розміщення ТВП у пам'яті" src="assets/l07-ivt-memory-layout.png" target="_self">ТВП у пам'яті: 256 записів по 4 байти кожен (IP молодший, IP старший, CS молодший, CS старший), починаючи з адреси 00000000. Кожен рядок показує 4 записи переривань поруч.Щоб знайти запис ТВП для переривання з номером N, ЦП обчислює:Адреса запису = N x 4Перші два байти за цією адресою — це зміщення (завантажується в IP), а наступні два байти — сегмент (завантажується в CS). Оскільки x86 використовує порядок байтів little-endian, молодший байт зміщення йде першим, за ним старший байт, потім молодший байт сегмента і старший байт сегмента.Початок пам'яті в Реальному режимі виглядає так:Address Byte Content Interrupt
─────────────────────────────────────────────
0000:0000 [IP low] [IP high] INT 00h (Divide by Zero)
0000:0002 [CS low] [CS high] handler address
0000:0004 [IP low] [IP high] INT 01h (Single Step / Trap)
0000:0006 [CS low] [CS high] handler address
0000:0008 [IP low] [IP high] INT 02h (NMI)
0000:000A [CS low] [CS high] handler address
0000:000C [IP low] [IP high] INT 03h (Breakpoint)
0000:000E [CS low] [CS high] handler address ... ... ...
0000:0080 [IP low] [IP high] INT 20h (DOS: Terminate Program)
0000:0082 [CS low] [CS high] handler address
0000:0084 [IP low] [IP high] INT 21h (DOS: System Services)
0000:0086 [CS low] [CS high] handler address ... ... ...
0000:03FC [IP low] [IP high] INT FFh
0000:03FE [CS low] [CS high] handler address
Кілька важливих записів у ТВП призначаються BIOS та DOS:Простежимо точно, як ЦП знаходить обробник для INT 21h — переривання системного виклику DOS, яке ми використовували в наших програмах (наприклад, з AH=09h для виведення рядка або AH=4Ch для виходу).Крок 1: Обчислити адресу запису в ТВП.Entry address = 0x21 × 4 = 0x84
Отже, 4-байтний вектор для INT 21h зберігається за фізичною адресою 0000:0084.Крок 2: Прочитати 4 байти за цією адресою.Припустимо, ми досліджуємо пам'ять за адресою 0000:0084 і знаходимо такі байти:Address Contents
0000:0084 8A 12 76 02
Пам'ятайте, що x86 зберігає значення у порядку little-endian. Перші два байти формують зміщення, а наступні два — сегмент:Offset (IP): bytes 8A 12 → 0x128A (low byte first)
Segment (CS): bytes 76 02 → 0x0276 (low byte first)
Крок 3: Сформувати адресу обробника.Обробник DOS INT 21h розташований за адресою 0276:128A. Використовуючи формулу сегмент:зміщення з Лекції 3:Physical address = 0x0276 × 16 + 0x128A = 0x2760 + 0x128A = 0x39EA
Коли ЦП виконує INT 21h, він передає виконання на адресу 0276:128A (фізична 0x39EA), де починається диспетчер системних викликів ядра DOS.<br><img alt="ТВП у Turbo Debugger" src="assets/l07-ivt-memory-dump-td.png" target="_self">Дамп пам'яті за адресою 0000:0000 у Turbo Debugger, що показує ТВП. Зелена анотація відстежує INT 21h: запис 21h × 4 = 84h, тож вектор знаходиться за зміщенням 0084. Читання 4 байтів дає CS:IP обробника переривання DOS.Тепер розглянемо, що саме відбувається, коли ЦП зустрічає інструкцію INT N. Інструкція INT виконує послідовність операцій, подібну до далекого виклику CALL (який зберігає CS та IP у стеку, як розглядалося в Лекції 5), але з важливим доповненням — збереженням та модифікацією регістра FLAGS.Перед виконанням інструкції INT припустимо, що ми маємо такий стан:CS:IP → points to the INT instruction
SS:SP → points to the current top of stack
FLAGS → current processor flags (IF=1, meaning interrupts enabled)
Коли INT N виконується, ЦП виконує ці кроки атомарно (як одну неподільну операцію): Зберегти FLAGS у стеку. Увесь 16-бітний регістр FLAGS зберігається у стеку, зберігаючи поточний стан усіх прапорців, включаючи IF (прапорець переривань) та TF (прапорець трасування). Очистити IF та TF у FLAGS. Прапорець переривань очищується, щоб запобігти подальшим апаратним перериванням під час налаштування обробником власного контексту. Прапорець трасування очищується, щоб запобігти покроковому трасуванню всередині обробника. Це запобіжний захід: обробнику потрібен неперервний час для збереження власного стану, перш ніж він зможе безпечно повторно увімкнути переривання. Зберегти CS у стеку. Поточний сегмент коду зберігається, щоб обробник міг повернутися до правильного сегмента. Зберегти IP у стеку. IP наступної інструкції після INT зберігається (не IP самої інструкції INT). Це адреса повернення. Завантажити CS:IP з ТВП. ЦП читає 4-байтний запис за адресою N × 4 і завантажує зміщення в IP, а сегмент — у CS. Виконання продовжується в обробнику. ЦП починає зчитувати та виконувати інструкції за новою адресою CS:IP. Після цих кроків стек містить три додаткові слова:Stack (growing downward): Address
┌──────────────────────┐
│ FLAGS │ ← SS:SP+4 (pushed first, so highest address)
├──────────────────────┤
│ CS │ ← SS:SP+2
├──────────────────────┤
│ IP │ ← SS:SP (pushed last, so current top)
└──────────────────────┘
Зверніть увагу на порядок збереження: FLAGS першим, потім CS, потім IP. SP зменшується на 2 при кожному збереженні (пригадайте з Лекції 5, що стек на x86 росте вниз), тож SP зменшився на 6 загалом.Подібність між INT та CALL/RET є навмисною. Обидва є механізмами передачі керування до іншого місця в коді зі збереженням адреси повернення. Ось точне порівняння:Уявіть INT N як «далекий CALL з FLAGS»: він робить усе те саме, що й далекий CALL, плюс зберігає та модифікує стан прапорців процесора. Цей додатковий крок є необхідним, оскільки обробники переривань можуть працювати з вимкненими перериваннями та повинні гарантувати ідеальне відновлення прапорців перерваної програми при поверненні.Парною до INT є інструкція IRET (Interrupt Return — повернення з переривання). Кожен обробник переривання повинен завершуватися інструкцією IRET для повернення керування перерваній програмі. IRET виконує точно зворотні до INT операції зі стеком: Витягнути IP зі стека. Відновлює вказівник інструкцій на інструкцію, що йде після оригінального INT. Витягнути CS зі стека. Відновлює сегмент коду до його початкового значення. Витягнути FLAGS зі стека. Відновлює увесь регістр FLAGS, включаючи прапорець IF. Оскільки збережений FLAGS мав IF=1 (переривання були увімкнені перед INT), відновлення FLAGS автоматично повторно вмикає переривання. ; Спрощена структура обробника переривання
my_handler: push ax ; Зберегти регістри, які обробник буде використовувати push bx push ds ; ... логіка обробника ... pop ds ; Відновити регістри у зворотному порядку pop bx pop ax iret ; Витягнути IP, CS, FLAGS — повернутися до викликача
Симетрія є елегантною: INT зберігає FLAGS, CS, IP (саме в такому порядку), а IRET витягує IP, CS, FLAGS (у зворотному порядку). Оскільки стек працює за принципом LIFO, значення витягуються саме в правильній послідовності.Простежимо конкретний приклад. Припустимо, програма за адресою CS=1000h, IP=0050h виконує INT 21h, з SP=00FEh та FLAGS=0202h (IF=1):ДО (програма виконується):CS:IP = 1000:0050 (pointing to the INT 21h instruction)
SS:SP = 2000:00FE (stack pointer)
FLAGS = 0202h (IF=1, interrupts enabled)
ПІД ЧАС (INT 21h виконується — ЦП виконує 6 кроків):Step 1: PUSH FLAGS → memory at SS:00FC = 0202h, SP = 00FC
Step 2: Clear IF, TF → FLAGS = 0002h
Step 3: PUSH CS → memory at SS:00FA = 1000h, SP = 00FA
Step 4: PUSH IP → memory at SS:00F8 = 0052h, SP = 00F8 (0052h = instruction AFTER the 2-byte INT 21h)
Step 5: Read IVT[0x84] → gets CS=0276h, IP=128Ah
Step 6: CS:IP = 0276:128A
Тепер стек виглядає так:SS:00F8 │ 0052h │ ← SP (saved IP — return address)
SS:00FA │ 1000h │ (saved CS)
SS:00FC │ 0202h │ (saved FLAGS)
SS:00FE │ ... │ (previous stack contents)
ПІСЛЯ (обробник виконує IRET):Step 1: POP IP → IP = 0052h, SP = 00FA
Step 2: POP CS → CS = 1000h, SP = 00FC
Step 3: POP FLAGS → FLAGS = 0202h, SP = 00FE Result: CS:IP = 1000:0052 (instruction after INT 21h) FLAGS = 0202h (IF=1, interrupts re-enabled) SP = 00FE (stack restored to original state)
Програма відновлює виконання точно з інструкції, що йде після INT, з усіма прапорцями (включаючи IF), відновленими до їхніх значень перед перериванням. З точки зору програми, обробник переривання був невидимим, за винятком побічних ефектів, які він навмисно виконав (виведення символу, читання введення з клавіатури тощо).Механізм INT/IRET у трьох стадіях. До: IP вказує на int 0x10, стек порожній. Під час: FLAGS, CS та IP збережені у стеку; виконання перейшло до обробника за адресою 3000:0148. Після: IRET витягнув збережені значення; виконання відновлюється з cmp ax,0 (інструкція після оригінального INT).Простота ТВП — одна з її найбільших переваг. Оскільки це просто масив 4-байтних вказівників за фіксованою адресою в пам'яті, виникає кілька потужних можливостей: ОС може встановлювати власні обробники, записуючи нові адреси у записи ТВП. Коли DOS завантажується, вона записує адресу свого диспетчера системних викликів у запис 21h, обробник друку екрана — у запис 05h тощо. Програми можуть ланцюгувати або замінювати обробники. Програма може прочитати поточний вектор (зберігши адресу старого обробника), записати адресу свого обробника у запис ТВП і змусити свій обробник викликати старий обробник після завершення роботи. Це основа програм TSR (Завершити і залишитися резидентною) та драйверів пристроїв у DOS. Апаратні переривання використовують той самий механізм. Програмований контролер переривань (PIC) перетворює апаратні сигнали на номери переривань, і ЦП використовує той самий пошук у ТВП для знаходження обробника апаратного переривання. Це уніфікує обробку апаратних та програмних переривань в єдиний, послідовний механізм. Однак ця простота також несе значні ризики. Оскільки ТВП знаходиться за фіксованою, незахищеною адресою в пам'яті, будь-яка програма може перезаписати будь-який вектор переривання в будь-який момент. Програма з помилкою може випадково пошкодити ТВП і обвалити всю систему. Ця відсутність захисту є однією з фундаментальних обмежень Реального режиму, які ми розглянемо далі.Все, що ми вивчали дотепер — адресацію сегмент:зміщення з Лекції 3, ТВП, прямий доступ до апаратного забезпечення — працює в оригінальному Реальному режимі процесора 8086. Це був єдиний режим роботи оригінального процесора 8086/8088, і заради зворотної сумісності кожен процесор x86 з того часу (включаючи найновіші чіпи Intel та AMD) все ще запускається в Реальному режимі перед переходом у Захищений режим.Реальний режим був достатнім для одноосібного середовища з однією програмою на початку 1980-х років. Але в міру того, як програмне забезпечення ставало складнішим, а користувачі вимагали багатозадачності, мережевої підтримки та більших програм, чотири фундаментальні обмеження ставали дедалі болючішими:У Реальному режимі одночасно виконується лише одна програма. Апаратна підтримка багатозадачності відсутня — немає механізму автоматичного перемикання між програмами, немає способу надати кожній програмі власний віртуальний адресний простір і немає витіснення на основі таймера для запобігання монополізації ЦП однією програмою.DOS частково обходила це за допомогою програм TSR (Завершити і залишитися резидентною), які підключалися до векторів переривань і залишалися в пам'яті після повернення користувача до командного рядка. Але TSR — це не справжня багатозадачність: вони активуються лише при виникненні переривання. Немає одночасного виконання, немає розподілу часу і немає способу для двох програм активно працювати одночасно.Це означає, що якщо програма потрапляє в нескінченний цикл, вся система зависає. Немає планувальника операційної системи, що працює у фоновому режимі та здатний примусово витіснити програму з помилкою.20-бітна адресна шина процесора 8086 обмежує весь фізичний адресний простір до 2^20 = 1 048 576 байтів = 1 МБ. Це не просто обмеження на встановлену оперативну пам'ять; це жорстка стеля для всієї адресованої пам'яті, яка повинна розподілятися між:
Системними областями (ТВП, область даних BIOS)
Ядром DOS та драйверами пристроїв
Кодом, даними та стеком запущеного застосунку
Відеопам'яттю та ROM BIOS (відображеними у верхні 384 КБ)
На практиці програма DOS зазвичай має доступ лише до приблизно 640 КБ звичайної пам'яті (так званий «бар'єр звичайної пам'яті»), а решта зарезервована для системних потреб. Наприкінці 1980-х років навіть цього було недостатньо для таких застосунків, як Lotus 1-2-3, AutoCAD та ранні графічні програми, що призвело до складних обхідних рішень на кшталт EMS (Expanded Memory Specification) та XMS (Extended Memory Specification).Це, мабуть, найнебезпечніше обмеження. У Реальному режимі кожна програма має необмежений доступ до всього адресного простору в 1 МБ. Будь-яка програма може читати або записувати будь-яку адресу пам'яті, включаючи:
ТВП за адресою 0000:0000 (перезапис векторів переривань)
Область даних BIOS
Код та дані ядра DOS
Пам'ять іншої програми (якщо резидентна TSR)
Відеопам'ять (пряме маніпулювання екраном)
Немає апаратного механізму для запобігання пошкодженню програмою операційної системи, інших програм або ТВП. Єдиний хибний вказівник або переповнення буфера може перезаписати критичні системні дані та обвалити машину. ЦП не розрізняє «пам'ять цієї програми» та «пам'ять ОС» — у Реальному режимі вся пам'ять однаково доступна для будь-якого коду.Це різко контрастує із Захищеним режимом, де ЦП забезпечує межі пам'яті апаратно. Якщо програма в Захищеному режимі намагається звернутися до пам'яті за межами виділених їй областей, ЦП генерує Загальну помилку захисту, і ОС може завершити програму-порушника без впливу на решту системи.Пов'язане із захистом пам'яті, у Реальному режимі немає рівнів привілеїв і контролю доступу. Будь-який код виконується з повним доступом до всього:
Будь-яка програма може виконувати привілейовані інструкції (як-от CLI для вимкнення переривань або HLT для зупинки ЦП)
Будь-яка програма може безпосередньо звертатися до портів введення/виведення (спілкуючись з апаратними пристроями)
Будь-яка програма може модифікувати сегментні регістри для вказування на будь-яке місце в пам'яті
Немає розрізнення між «режимом ядра» та «режимом користувача»
Будь-які обмеження доступу в системі Реального режиму є суто програмними домовленостями, що забезпечуються операційною системою, а не апаратними бар'єрами. Кооперативна програма може добровільно використовувати лише системні виклики DOS для доступу до файлів, але зловмисна або програма з помилкою може обійти DOS і записувати безпосередньо на диск, переналаштовувати обладнання або захоплювати систему.Захищений режим вирішує це, вводячи чотири кільця захисту (від 0 до 3), де ядро ОС працює на Кільці 0 (найвищий рівень привілеїв), а користувацькі програми — на Кільці 3 (найнижчий рівень привілеїв). Апаратне забезпечення ЦП перевіряє кожне звернення до пам'яті та кожне виконання інструкції відносно поточного рівня привілеїв, запобігаючи виконанню небезпечних операцій користувацькими програмами.<br>Ці обмеження пояснюють, чому Intel представила <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Protected_mode" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Protected_mode" target="_self">Захищений режим</a> у процесорі 80286 в 1982 році та значно розширила його у 80386 в 1985 році. Захищений режим не замінив набір інструкцій, який ми вивчили — ті самі інструкції MOV, ADD, PUSH, CALL та INT все ще використовуються. Натомість він додав рівень апаратного забезпечення, який робить процесор обізнаним щодо того, якому коду що дозволено робити. Розуміння обмежень Реального режиму є необхідним для усвідомлення того, чому сучасні процесори та операційні системи спроєктовані саме так.У L06 (Розділ 10) ми представили захищений режим як сучасний режим роботи, який замінив обмеження реального режиму. Ми зазначили більший адресний простір, захист пам'яті та кільця привілеїв — але розглянули їх лише в загальних рисах. Тепер заглибимось детальніше: як саме захищений режим розширює адресацію і що таке механізм віртуальної пам'яті, який робить можливою сучасну багатозадачність?Пригадаймо з L03, що адресація в реальному режимі використовує схему сегмент:зміщення для формування 20-бітної фізичної адреси: Physical = Segment * 16 + Offset. Це дає максимальний діапазон адрес у 1 МБ. На початку 1980-х років цього вже було недостатньо. Програми ставали більшими, а користувачі хотіли запускати кілька застосунків одночасно. Обмеження в 1 МБ змушувало вдаватися до складних обхідних рішень — розширена пам'ять (EMS), додаткова пам'ять (XMS), оверлеї — які були крихкими та складними.Захищений режим було розроблено для вирішення цієї проблеми на апаратному рівні, а не за допомогою програмних хитрощів.Кожне покоління процесорів x86 значно розширювало обсяг адресованої пам'яті:<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Intel_80286" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Intel_80286" target="_self">80286</a> запровадив захищений режим із 24-бітною адресною шиною, що дозволяло адресувати до 16 МБ фізичної пам'яті — 16-кратне збільшення порівняно з 8086. Однак захищений режим 80286 мав суттєві обмеження: він не міг легко повернутися до реального режиму (потрібне було скидання ЦП) і не підтримував сторінкову організацію пам'яті.<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Intel_80386" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Intel_80386" target="_self">80386</a> став справжнім проривом. З повноцінною 32-бітною адресною шиною він підтримував 4 ГБ фізичної пам'яті та запровадив сторінкову організацію пам'яті — механізм, що робить можливою віртуальну пам'ять. 80386 — це процесор, який зробив сучасні операційні системи, такі як Windows та Linux, практичними на платформі x86.<br>В епоху 64-бітних процесорів (<a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/X86-64" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/X86-64" target="_self">x86-64</a>, представлений AMD у 2003 році) теоретичний віртуальний адресний простір становить 16 ексабайт (2^64 байт), хоча поточні реалізації використовують 48-бітні віртуальні адреси (256 ТБ) і до 52-бітних фізичних адрес (4 ПБ). Це значно більше, ніж потрібно будь-якій сучасній системі, забезпечуючи запас на десятиліття зростання.<br>Цікавим історичним артефактом цього переходу є <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/A20_line" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/A20_line" target="_self">лінія A20</a>. 8086 мав 20-бітну адресну шину (лінії A0–A19). Деякі програми DOS покладалися на те, що адреси понад 1 МБ «загортатимуться» на адресу 0. Коли 80286 додав адресну лінію A20 (що дозволяла доступ понад 1 МБ), IBM довелося додати апаратний шлюз для вимкнення цієї лінії задля зворотної сумісності. На ранніх етапах процесу завантаження BIOS або завантажувач повинен явно увімкнути лінію A20 перед доступом до пам'яті понад 1 МБ — спадковий нюанс, що зберігався десятиліттями.<br>Захищений режим зберігає концепцію сегментації з реального режиму, але кардинально її трансформує. У реальному режимі сегментний регістр містить необроблену базову адресу (зсунуту вліво на 4 біти). У захищеному режимі сегментний регістр містить селектор — індекс у <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Global_Descriptor_Table" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Global_Descriptor_Table" target="_self">таблиці дескрипторів</a>, яка містить повний опис сегмента.Кожен запис у таблиці дескрипторів (який називається дескриптор сегмента) визначає:
Базову адресу — початкову фізичну (або лінійну) адресу сегмента (32 біти на 80386, що дозволяє будь-яке розташування в межах 4 ГБ)
Ліміт — максимальний розмір сегмента (до 4 ГБ із масштабуванням гранулярності)
Права доступу — дозволи, включаючи читання/запис/виконання, рівень привілеїв, необхідний для доступу до сегмента, та те, чи сегмент присутній у пам'яті
Це критичне вдосконалення порівняно з реальним режимом. У реальному режимі будь-яка програма може встановити будь-яке значення в будь-який сегментний регістр і отримати доступ до будь-якої пам'яті. У захищеному режимі ЦП перевіряє кожне звернення до пам'яті за таблицею дескрипторів: Чи існує цей сегмент? Чи має ця програма дозвіл на доступ до нього? Чи знаходиться зміщення в межах ліміту сегмента? Якщо будь-яка перевірка не проходить, ЦП генерує загальну помилку захисту (GPF), замість того щоб мовчки дозволити доступ.<br>Існують дві основні таблиці дескрипторів. <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Global_Descriptor_Table" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Global_Descriptor_Table" target="_self">Глобальна таблиця дескрипторів (GDT)</a> є спільною для всієї системи і зазвичай визначає сегменти пам'яті операційної системи. <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Local_Descriptor_Table" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Local_Descriptor_Table" target="_self">Локальна таблиця дескрипторів (LDT)</a> може бути окремою для кожного процесу, дозволяючи кожному процесу мати власні визначення сегментів. Операційна система налаштовує ці таблиці під час завантаження і підтримує їх у міру створення та знищення процесів.Перехід у захищений режим із реального режиму вимагає від операційної системи (або завантажувача) виконання певної послідовності дій: створити дійсну GDT у пам'яті, завантажити її адресу в регістр GDTR за допомогою інструкції LGDT, встановити біт PE (Protection Enable) у контрольному регістрі CR0, а потім виконати далекий перехід (far jump) для очищення конвеєра інструкцій і початку виконання в захищеному режимі. Цей перехід є незворотним без скидання ЦП на 80286; 80386 і пізніші процесори можуть повернутися до реального режиму, очистивши біт PE, хоча це потребує обережної обробки.<br>Тоді як сегментація забезпечує контроль доступу, <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Memory_paging" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Memory_paging" target="_self">сторінкова організація пам'яті</a> забезпечує справжню потужність захищеного режиму: віртуальну пам'ять.<br>З увімкненою сторінковою організацією (доступною починаючи з 80386) ЦП додає додатковий рівень трансляції. Адреса, отримана в результаті сегментації, більше не є фізичною адресою — це лінійна адреса (також звана віртуальною адресою). <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Memory_management_unit" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Memory_management_unit" target="_self">Блок керування пам'яттю (MMU)</a> ЦП потім транслює цю лінійну адресу у фізичну адресу за допомогою таблиць сторінок — структур даних, які операційна система підтримує у пам'яті.Трансляція працює наступним чином:
Програма генерує віртуальну адресу (через сегментацію або, у сучасних плоских моделях пам'яті, безпосередньо).
MMU розбиває цю віртуальну адресу на номер сторінки та зміщення в межах сторінки. Стандартний розмір сторінки на x86 становить 4 КБ (4 096 байт).
MMU шукає номер сторінки в таблиці сторінок, щоб знайти відповідний номер фізичного фрейму — фактичне розташування у фізичній оперативній пам'яті.
Фізична адреса формується шляхом поєднання номера фізичного фрейму зі зміщенням.
Ця непряма адресація має глибокі наслідки:Кожен процес отримує власний віртуальний адресний простір. Операційна система підтримує окремі таблиці сторінок для кожного процесу. Процес A і процес B можуть обидва використовувати віртуальну адресу 0x00400000, але їхні таблиці сторінок відображають її на різні фізичні фрейми. Жоден процес не може бачити або змінювати пам'ять іншого — апаратне забезпечення просто не надає відображення.<br>Пам'ять може перевищувати обсяг фізичної оперативної пам'яті. Якщо системі бракує фізичної пам'яті, операційна система може вивантажити (або витіснити) рідко використовувані сторінки на диск. Запис у таблиці сторінок позначається як «не присутній». Якщо програма пізніше звертається до цієї сторінки, ЦП генерує сторінкову помилку — спеціальний виняток, що передає керування операційній системі. Обробник сторінкових помилок ядра визначає, що сталося: якщо сторінку було вивантажено на диск, він завантажує її назад у вільний фізичний фрейм, оновлює таблицю сторінок і дозволяє програмі продовжити — все це прозоро. Програма ніколи не дізнається, що сторінка була відсутня. Це і є <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Virtual_memory" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Virtual_memory" target="_self">віртуальна пам'ять</a> у дії: програми можуть сукупно використовувати більше пам'яті, ніж машина фізично має.Ця техніка називається завантаження сторінок за запитом — сторінки завантажуються у фізичну пам'ять лише тоді, коли до них фактично звертаються, а не коли їх вперше виділяють. Коли ви запускаєте велику програму, операційна система не завантажує весь виконуваний файл в оперативну пам'ять відразу. Натомість вона налаштовує записи таблиці сторінок, що вказують на виконуваний файл на диску. Коли програма виконується і вперше звертається до кожної сторінки, виникає сторінкова помилка, і ядро завантажує саме цю сторінку. Сторінки, до яких ніколи не звертаються (наприклад, код обробки помилок, що ніколи не спрацьовує), ніколи не завантажуються, зберігаючи фізичну пам'ять.<br><img alt="Віртуальна пам'ять із сторінковою організацією" src="assets/l07-virtual-memory-diagram.png" target="_self">Віртуальна пам'ять у дії: програма, що виконується, бачить неперервний віртуальний адресний простір. Таблиця сторінок відображає віртуальні сторінки на фрейми фізичної пам'яті. Сторінки, яких наразі немає у фізичній пам'яті, зберігаються у файлі підкачки на жорсткому диску та завантажуються за запитом.Фрагментація фізичної пам'яті прихована. Програма бачить неперервний віртуальний адресний простір, але відповідні фізичні фрейми можуть бути розкидані будь-де в оперативній пам'яті. Таблиця сторінок виконує відображення, тому ані програмі, ані програмісту не потрібно турбуватися про розташування фізичної пам'яті.Спільні бібліотеки стають ефективними. Якщо кілька процесів використовують одну й ту саму спільну бібліотеку (наприклад, стандартну бібліотеку C), операційна система може відобразити ті самі фізичні фрейми у віртуальний адресний простір кожного процесу. Код бібліотеки існує лише один раз у фізичній пам'яті, але з'являється у кожному процесі, якому він потрібен.<br>Оптимізація «копіювання при записі». Коли процес створює дочірній процес (через системний виклик fork у Unix/Linux), операційна система не копіює одразу всю пам'ять батьківського процесу. Натомість обидва процеси спільно використовують ті самі фізичні сторінки, а записи таблиці сторінок позначаються як доступні лише для читання. Лише коли один із процесів намагається записати в спільну сторінку, ЦП генерує сторінкову помилку, і ядро створює приватну копію цієї сторінки для процесу, що записує. Ця техніка <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Copy-on-write" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Copy-on-write" target="_self">копіювання при записі</a> робить створення процесів швидким та ефективним за пам'яттю — більшість сторінок розгалуженого процесу ніколи не змінюються і ніколи не потребують копіювання.<br>Пошук у таблиці сторінок вимагає читання структур даних у пам'яті, що є повільним — кожне звернення до пам'яті з боку програми потребувало б додаткових звернень до пам'яті лише для виконання трансляції. Для вирішення цієї проблеми продуктивності ЦП включає спеціалізований кеш, який називається <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Translation_lookaside_buffer" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Translation_lookaside_buffer" target="_self">буфер асоціативної трансляції (TLB)</a>. TLB зберігає нещодавно використані відображення віртуальних адрес на фізичні. Коли MMU потрібно транслювати адресу, він спочатку перевіряє TLB. Якщо відображення знайдено (влучання в TLB), трансляція відбувається майже миттєво. Лише при промаху TLB ЦП проходить по структурах таблиці сторінок у пам'яті.TLB є критично важливим для продуктивності. Сучасні процесори досягають частоти влучань у TLB понад 99% для типових навантажень, що означає, що накладні витрати сторінкової організації на практиці є незначними. Однак, коли операційна система перемикається між процесами та завантажує новий набір таблиць сторінок (записуючи нове значення в регістр CR3), записи TLB від попереднього процесу стають недійсними і повинні бути очищені. Це очищення TLB є однією з реальних витрат перемикання контексту.Підсумовуючи, звернення до пам'яті в повному захищеному режимі проходить через два етапи трансляції:Логічна адреса → Сегментація → Лінійна (віртуальна) адреса → Сторінкова → Фізична адреса
(Селектор:Зміщення) (GDT/LDT) (32-бітна або 64-бітна) організація (фактичне (таблиці розташування сторінок) в ОП)
У сучасних операційних системах, що працюють на 80386 і пізніших процесорах, сегментація часто налаштовується як плоска модель пам'яті — усі сегменти мають базу 0 і ліміт 4 ГБ, що фактично робить логічну адресу рівною лінійній адресі. Це спрощує програмування (немає потреби в арифметиці сегментів) і повністю покладається на сторінкову організацію для керування та захисту пам'яті. Архітектура x86-64 примусово застосовує цю плоску модель: у 64-бітному Long Mode сегментація фактично вимкнена для сегментів коду та даних.Захищений режим — це не лише доступ до більшого обсягу пам'яті. Він забезпечує апаратні основи для двох можливостей, що визначають сучасні обчислення: одночасне виконання кількох програм (паралелізм/багатозадачність) та запобігання взаємному впливу цих програм одна на одну та на операційну систему (безпека).Система віртуальної пам'яті, описана в Розділі 12, є першим стовпом паралелізму. Оскільки кожен процес має власні таблиці сторінок, кожен процес бачить свій власний приватний віртуальний адресний простір. Один процес не може читати або записувати пам'ять іншого процесу — не тому, що операційна система ввічливо просить його цього не робити, а тому, що апаратне забезпечення не надає відображення на фізичні сторінки іншого процесу.Ця ізоляція означає, що помилка в одній програмі — скажімо, переповнення буфера, що записує за випадковим покажчиком — пошкоджує лише пам'ять цієї програми. Операційна система виявляє порушення (через сторінкову помилку або загальну помилку захисту), завершує процес-порушник, і всі інші процеси продовжують працювати без перешкод. Порівняйте це з реальним режимом, де один помилковий запис міг перезаписати таблицю векторів переривань і зламати всю машину.<br>Архітектура захищеного режиму x86 надає спеціальний апаратний механізм для перемикання задач: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Task_state_segment" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Task_state_segment" target="_self">сегмент стану задачі (TSS)</a>. TSS — це спеціальна структура даних, яка зберігає повний контекст виконання задачі (процесу):
Усі регістри загального призначення (EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP)
Покажчик інструкцій (EIP) — місце, де задача виконувалася, коли її було призупинено
Регістр FLAGS — повний набір прапорців стану процесора
Сегментні регістри (CS, DS, ES, FS, GS, SS)
Регістр CR3 — база каталогу сторінок, що вказує на таблиці сторінок задачі (саме так кожен процес отримує свій власний віртуальний адресний простір)
Бітова карта дозволів вводу-виводу — визначає, до яких портів вводу-виводу ця задача має доступ
Покажчики стеку для кожного рівня привілеїв — щоб ЦП знав, на який стек перемикатися при переході між кільцями
Коли операційна система вирішує перемкнутися з задачі A на задачу B, ЦП може зберегти весь стан задачі A в її TSS і завантажити стан задачі B з її TSS за одну операцію. Це називається апаратне перемикання задач. Процесор автоматично обробляє збереження/відновлення регістрів, перемикання таблиці сторінок (через CR3) та перемикання стеку.Перемикання контексту — акт призупинення одного процесу та відновлення іншого — включає кілька кроків незалежно від того, використовується апаратне чи програмне перемикання:
Зберегти стан регістрів поточного процесу (або дозволити ЦП зберегти його в TSS).
Зберегти покажчик таблиці сторінок поточного процесу (CR3).
Завантажити покажчик таблиці сторінок нового процесу — це миттєво змінює весь віртуальний адресний простір, видимий для ЦП.
Завантажити стан регістрів нового процесу, включаючи його покажчик інструкцій (EIP/RIP).
Продовжити виконання з місця, де новий процес було востаннє призупинено.
Оскільки завантаження нового CR3 інвалідує TLB (див. Розділ 12.5), перемикання контексту має вимірювану вартість продуктивності. Це одна з причин, чому операційні системи намагаються не перемикатися занадто часто, водночас підтримуючи чуйну багатозадачність.На практиці більшість сучасних операційних систем (Linux, Windows) виконують програмне перемикання задач замість використання апаратного механізму TSS — вони зберігають і відновлюють регістри вручну, оскільки це дає їм більший контроль над тим, що зберігається, і часто є швидшим. Однак TSS залишається необхідним: він все ще використовується для зберігання покажчика стеку ядра для кожної задачі (ЦП потребує цього під час переходів між рівнями привілеїв) та бітової карти дозволів вводу-виводу. Кожна операційна система x86, що працює в захищеному режимі, повинна мати визначеним принаймні один TSS.<br>Другим стовпом безпеки в захищеному режимі є система <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Protection_ring" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Protection_ring" target="_self">кілець захисту</a> — апаратно забезпечена ієрархія рівнів привілеїв. Архітектура x86 визначає чотири кільця:Поточний рівень привілеїв (CPL) закодований у двох молодших бітах регістра CS (сегмент коду). ЦП перевіряє цей рівень привілеїв при кожній інструкції:
Привілейовані інструкції — такі як HLT (зупинка ЦП), LGDT (завантаження регістра GDT), MOV до/з контрольних регістрів (CR0, CR3) та IN/OUT (прямий доступ до портів вводу-виводу) — можуть виконуватися лише в кільці 0. Якщо програма кільця 3 спробує виконати HLT, ЦП негайно згенерує загальну помилку захисту.
Доступ до пам'яті — кожен дескриптор сегмента включає рівень привілеїв дескриптора (DPL). Програма може отримати доступ лише до сегментів, DPL яких більший або рівний її власному CPL. Код кільця 3 не може читати або записувати сегменти даних кільця 0.
Доступ до портів вводу-виводу — TSS містить бітову карту дозволів вводу-виводу, яка визначає, до яких портів дана задача може отримати доступ. Навіть якби інструкції вводу-виводу не були за своєю природою привілейованими, ця бітова карта забезпечує детальний контроль.
<br><img alt="Кільця захисту" src="assets/l07-protection-rings.png" target="_self">Ієрархія кілець захисту x86. Кільце 0 (ядро) має повний доступ до апаратного забезпечення. Кільце 3 (застосунки) є найбільш обмеженим. Кільця 1 і 2 були розроблені для драйверів пристроїв, але не використовуються сучасними операційними системами, які використовують лише кільце 0 і кільце 3.<br>На практиці більшість сучасних операційних систем використовують лише кільце 0 і кільце 3. Кільця 1 і 2 призначалися для драйверів пристроїв і системних сервісів, але модель із двома кільцями (ядро проти користувача) виявилася простішою та достатньою. Накладні витрати на керування чотирма рівнями привілеїв додавали складності без очевидних переваг — якщо драйвер пристрою в кільці 1 аварійно завершується, він все одно може вивести систему з ладу, оскільки має доступ до критичного апаратного забезпечення. Intel навіть запропонувала <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/X86S" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/X86S" target="_self">x86S</a> у 2023 році (пізніше скасовано), що формально видалило б кільця 1 і 2 з архітектури — визнаючи, що жодна масова операційна система їх не використовує.Одним важливим винятком із моделі двох кілець є віртуалізація. Гіпервізори (такі як VMware, Hyper-V або KVM) повинні запускати гостьові операційні системи, які вважають, що працюють у кільці 0, тоді як гіпервізор сам зберігає справжній контроль. Ранні рішення віртуалізації використовували для цього кільця 1–2. Сучасні процесори вирішують це інакше — за допомогою спеціальних розширень віртуалізації (Intel VT-x, AMD-V), які створюють новий рівень привілеїв нижче кільця 0 — інколи його називають «кільце -1» — для гіпервізора.Якщо програми користувача працюють у кільці 3, а ядро — у кільці 0, як програма запитує сервіси ядра? Вона не може просто виконати CALL до функції ядра — ЦП відхилить перехід як порушення привілеїв. Натомість архітектура надає контрольовані точки входу:<br>Програмні переривання (інструкція INT). Як ми розглянули в попередніх розділах, виконання INT 21h у DOS або INT 80h у 32-бітному Linux змушує ЦП перемкнутися з кільця 3 у кільце 0, зберегти стан користувача та перейти до обробника, визначеного операційною системою. Механізм переривань за своєю природою є переходом привілеїв — ЦП автоматично підвищує до кільця 0 при диспетчеризації через <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Interrupt_descriptor_table" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Interrupt_descriptor_table" target="_self">таблицю дескрипторів переривань (IDT)</a> (наступницю ТВП у захищеному режимі).<br>Інструкції SYSCALL/SYSENTER. Сучасні системи x86-64 використовують інструкцію SYSCALL замість INT 80h для системних викликів. SYSCALL — це швидший механізм: він не проходить через IDT, а використовує спеціальні <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Model-specific_register" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Model-specific_register" target="_self">модельно-специфічні регістри (MSR)</a> для визначення точки входу кільця 0. Ефект той самий: контрольований, апаратно опосередкований перехід із режиму користувача в режим ядра.<br>Шлюзи виклику. Архітектура x86 також підтримує <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Call_gate_(Intel)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Call_gate_(Intel)" target="_self">шлюзи виклику</a> — спеціальні записи таблиці дескрипторів, що дозволяють контрольовані далекі виклики між рівнями привілеїв. Програма кільця 3 може виконати CALL за далеким покажчиком, що посилається на шлюз виклику, і ЦП перевірить дозволи, перемкнеться в кільце 0 та перейде до попередньо визначеної точки входу ядра. Шлюзи виклику були оригінальним механізмом x86 для системних викликів, але їх значною мірою витіснили SYSENTER/SYSCALL через продуктивність.У всіх випадках критична властивість є однаковою: програма користувача не може обирати, куди в ядрі перейти. Вона може лише викликати попередньо визначені точки входу, які ядро зареєструвало. Це запобігає тому, щоб зловмисна програма перейшла посеред процедури ядра та обійшла перевірки безпеки.Зв'язуючи це з тим, що ми розглянули в розділі про переривання: коли програма користувача в кільці 3 виконує INT 21h (DOS) або SYSCALL (Linux), відбувається наступний перехід привілеїв:
ЦП розпізнає перехід між кільцями (кільце 3 до кільця 0).
ЦП перемикається на стек ядра — покажчик стеку для кільця 0 завантажується з TSS. Це необхідно, оскільки ядро не може довіряти стеку користувача.
SS, ESP, EFLAGS, CS та EIP користувача заштовхуються на стек ядра (контекст повернення).
ЦП завантажує CS та EIP із запису IDT (або з MSR для SYSCALL), що вказує на код обробника ядра.
Виконання продовжується в кільці 0 — ядро обробляє запит.
Після завершення ядро виконує IRET (або SYSRET), що відновлює збережений стан, перемикається назад у кільце 3 та відновлює виконання програми користувача.
Цей механізм є апаратною основою кожного системного виклику в кожній сучасній операційній системі, що працює на x86. Він гарантує, що перехід між режимом користувача та режимом ядра завжди контрольований, перевіряємий та зворотний.Ми розглянули окремі апаратні механізми, які надає захищений режим: віртуальну пам'ять зі сторінковою організацією, ізоляцію процесів через окремі адресні простори, апаратну підтримку задач через TSS та забезпечення привілеїв через кільця захисту. Ядро — це програмне забезпечення, яке об'єднує всі ці механізми в узгоджену, працюючу систему.<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Kernel_(operating_system)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Kernel_(operating_system)" target="_self">Ядро</a> — це основна програма операційної системи, єдине програмне забезпечення, яке завжди перебуває в пам'яті та завжди має повний контроль над апаратним забезпеченням. Його обов'язки включають:Керування ресурсами. Ядро арбітрує доступ до всіх апаратних ресурсів: процесорного часу, фізичної пам'яті, дискового сховища, мережевих інтерфейсів та периферійних пристроїв. Коли кілька процесів конкурують за один ресурс, ядро вирішує, хто що отримує, коли і на який час.Планування процесів. Ядро вирішує, який процес виконується на якому ядрі ЦП у будь-який момент часу. Воно реалізує алгоритм планування, що балансує між справедливістю (кожен процес отримує процесорний час), чуйністю (інтерактивні програми реагують швидко) та пропускною здатністю (пакетні завдання завершуються ефективно). Планувальник використовує апаратне переривання таймера (той самий механізм, який ми обговорювали в розділі про переривання) для повернення контролю від запущених процесів через регулярні інтервали — це витісняюча багатозадачність. Перерваний процес не повинен співпрацювати або навіть знати, що його було призупинено; з його перспективи, виконання просто зупиняється і пізніше відновлюється точно з того місця, де зупинилося. Це принципово відрізняється від кооперативної багатозадачності (використовуваної в ранніх версіях Windows та класичній Mac OS), де програми мали добровільно віддавати контроль — одна програма, що погано поводиться, могла заморозити всю систему.Надання сервісів через системні виклики. Програми користувача не можуть звертатися до апаратного забезпечення безпосередньо (вони працюють у кільці 3). Натомість вони запитують сервіси від ядра через системні виклики — контрольовані переходи з кільця 3 у кільце 0, які ми описали в Розділі 13. Потрібно прочитати файл? Програма викликає системний виклик read. Потрібно виділити пам'ять? Системний виклик mmap або brk. Потрібно створити новий процес? Системний виклик fork. Ядро перевіряє кожен запит, виконує операцію від імені програми та повертає результат.Використання ЦП для безпеки. Ядро налаштовує та підтримує систему кілець захисту. Воно працює в кільці 0, тому може виконувати привілейовані інструкції — завантажувати таблиці сторінок (через CR3), налаштовувати GDT та IDT, керувати дозволами вводу-виводу в TSS. Програми користувача в кільці 3 обмежені: вони не можуть змінювати таблиці сторінок, не можуть змінювати обробники переривань, не можуть безпосередньо звертатися до портів вводу-виводу та не можуть виконувати привілейовані інструкції. Ядро є воротарем.Віртуалізація та паралелізм. Ядро створює ілюзію того, що кожен процес має машину цілком для себе: власний віртуальний адресний простір (через сторінкову організацію), власний ЦП (через планування) та власний набір відкритих файлів і ресурсів (через структури даних ядра). Ця віртуалізація є тим, що дозволяє сотням процесів працювати «одночасно» на машині з кількома ядрами ЦП.<br><img alt="Архітектура ядра" src="assets/l07-kernel-architecture.png" target="_self">Ядро знаходиться між апаратним забезпеченням (ЦП, пам'ять, пристрої) та застосунками користувача, опосередковуючи весь доступ через системні виклики та драйвери.Абстрагування апаратного забезпечення через драйвери пристроїв. Ядро включає (або завантажує) драйвери пристроїв — спеціалізовані кодові модулі, які знають, як взаємодіяти з конкретними апаратними пристроями. Програмі, яка хоче прочитати з файлу, не потрібно знати, чи файл знаходиться на SSD, обертовому жорсткому диску чи мережевому ресурсі. Програма виконує загальний системний виклик read, і ядро направляє запит до відповідного драйвера. Цей рівень абстракції є сучасним нащадком сервісів BIOS, які ми обговорювали раніше — але значно потужнішим та розширюваним.Як ми обговорювали в розділах про процес завантаження, ядро є останньою ланкою в ланцюжку початкового завантаження: BIOS завантажує завантажувач, завантажувач завантажує ядро. Після отримання керування ядро виконує власну послідовність ініціалізації:
Перемикає ЦП з реального режиму в захищений режим (встановлюючи біт PE в CR0).
Налаштовує GDT та IDT з дескрипторами сегментів та переривань системи.
Вмикає сторінкову організацію, створюючи початкові таблиці сторінок та завантажуючи CR3.
Ініціалізує планувальник, менеджер пам'яті та драйвери пристроїв.
Створює перший процес простору користувача (у Linux це традиційно init або systemd).
З цього моменту ядро має повний контроль. Усі подальші програми створюються ядром, працюють у кільці 3 та взаємодіють з апаратним забезпеченням виключно через системні виклики.Ядро займає захищену область пам'яті — простір ядра — недоступну з коду кільця 3. У типовій 32-бітній системі Linux віртуальний адресний простір 4 ГБ розділений: нижні 3 ГБ (адреси від 0x00000000 до 0xBFFFFFFF) належать простору користувача, а верхній 1 ГБ (адреси від 0xC0000000 до 0xFFFFFFFF) зарезервований для ядра. Записи таблиці сторінок для області ядра позначені як доступні лише для кільця 0, тому будь-яка спроба програми користувача прочитати або записати пам'ять ядра викликає сторінкову помилку.Цей поділ означає, що аварійна програма користувача не може пошкодити структури даних ядра. Ядро залишається стабільним, навіть коли застосунки аварійно завершуються — ось чому сучасна операційна система може працювати місяцями без перезавантаження, попри регулярні аварії окремих програм.Цікаво, що пам'ять ядра відображається у віртуальному адресному просторі кожного процесу (у верхній області), навіть попри те, що код користувача не може до неї звертатися. Така конструкція дозволяє ядру швидко звертатися до власних структур даних під час системного виклику без необхідності перемикати таблиці сторінок. Коли системний виклик переходить з кільця 3 у кільце 0, ті самі таблиці сторінок залишаються завантаженими — змінюється лише рівень привілеїв, розблоковуючи доступ до сторінок простору ядра.Не всі ядра організовані однаково. Два основні підходи:<br>Монолітні ядра. Вся функціональність ядра — керування процесами, керування пам'яттю, файлові системи, драйвери пристроїв, мережа — працює в одному адресному просторі в кільці 0. Цей підхід надає перевагу продуктивності: виклики функцій всередині ядра є звичайними викликами процедур, а не міжпроцесними повідомленнями. <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Linux_kernel" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Linux_kernel" target="_self">Ядро Linux</a> є найвидатнішим прикладом. Linux технічно є монолітним, але також модульним — він може завантажувати та вивантажувати модулі ядра (драйвери пристроїв, підтримку файлових систем) під час роботи без перезавантаження.<br>Мікроядра. Лише найнеобхідніші функції — базове планування, міжпроцесна взаємодія (IPC) та низькорівневе керування пам'яттю — працюють у кільці 0. Все інше (драйвери пристроїв, файлові системи, мережеві стеки) працює в кільці 3 як окремі процеси простору користувача. Якщо драйвер пристрою аварійно завершується, загине лише процес цього драйвера — саме ядро залишиться неушкодженим. <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Minix" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Minix" target="_self">MINIX</a> та <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/QNX" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/QNX" target="_self">QNX</a> є системами з мікроядром. Компроміс — продуктивність: кожен запит, який у монолітному ядрі був би простим викликом функції, тепер вимагає повідомлення IPC між процесами простору користувача, що є повільнішим.Більшість виробничих операційних систем сьогодні — Linux, Windows, macOS — використовують монолітні (або гібридні монолітні) ядра, приймаючи компроміс надійності на користь продуктивності. Windows NT та macOS використовують гібридний підхід: ядро здебільшого монолітне для продуктивності, але деякі підсистеми працюють у просторі користувача для модульності. Ідея мікроядра залишається впливовою у вбудованих системах та системах реального часу, де надійність є першочерговою — наприклад, мікроядро QNX забезпечує роботу багатьох автомобільних та медичних систем, де аварія може загрожувати життю.Дебати між монолітною та мікроядерною архітектурами є одними з найдавніших у дослідженні операційних систем. У відомій дискусії 1992 року в Usenet Лінус Торвальдс та Ендрю Таненбаум (творець MINIX) сперечалися про те, чи є монолітна архітектура Linux «гігантським кроком назад у 1970-ті». Через три десятиліття Linux домінує на серверах, смартфонах (через Android) та суперкомп'ютерах — тоді як принципи мікроядра знайшли свою нішу в безпекокритичних вбудованих системах.Ядро — це місце, де кожен апаратний механізм, який ми обговорювали в цій лекції, сходиться:Без захищеного режиму нічого з цього не працювало б. У реальному режимі — середовищі, в якому ви програмуєте в першій частині цього курсу — немає захисту пам'яті, немає кілець привілеїв і немає віртуальної пам'яті. DOS працює як однозадачна система, де будь-яка програма може перезаписати будь-яке місце в пам'яті, включаючи саму операційну систему. Саме тому помилка в програмі DOS може заморозити всю машину.Захищений режим дав архітекторам апаратного забезпечення та розробникам операційних систем інструменти для побудови надійних, безпечних, багатозадачних систем, на які ми покладаємося сьогодні. Шлях від натискання кнопки живлення (Розділ 2) через BIOS та процес завантаження (Розділи 3–4), до переривань (Розділи 5–10), до обмежень реального режиму та еволюції до захищеного режиму (Розділ 11), і нарешті до самого ядра — це повний шлях від голого апаратного забезпечення до функціонуючої операційної системи.Протягом решти цього курсу ви відчуєте обидві сторони цього поділу. В лабораторних роботах P1–P10 ви продовжуєте працювати в реальному режимі під DOS, де маєте прямий доступ до апаратного забезпечення і можете бачити кожен механізм у дії без абстракції. В лабораторних роботах P11–P15 ви перейдете до Linux на x86-64 — повноцінного середовища захищеного режиму, де ваші програми працюють у кільці 3, використовують віртуальні адреси та спілкуються з ядром виключно через системні виклики. Розуміння апаратних механізмів, описаних у цій лекції, допоможе вам оцінити, чому середовище Linux працює саме так, і що операційна система робить від вашого імені під поверхнею. Процес завантаження — це ланцюжок довіри: увімкнення живлення активує вектор скидання, який запускає мікропрограму BIOS/UEFI. POST тестує апаратне забезпечення, початковий завантажувач знаходить MBR, який завантажує ядро ОС. Кожен етап передає керування наступному. Переривання — це універсальний механізм обслуговування: і апаратні пристрої, і програми використовують переривання для запиту сервісів. ЦП обробляє обидва типи однаково — шукає адресу обробника в ТВП і передає керування. INT/IRET розширює шаблон CALL/RET: INT заштовхує FLAGS, CS та IP на стек, потім переходить до адреси обробника з ТВП. IRET виконує зворотний процес. Це подібно до далекого CALL, який також зберігає та відновлює прапорці процесора. ТВП — це проста таблиця пошуку: 256 записів по 4 байти кожен, починаючи з адреси 0000:0000. Запис N знаходиться за адресою N*4. BIOS заповнює її першим; ОС може перевизначити записи пізніше. DOS INT 21h та BIOS INT 10h/16h — ваші інструменти для вводу-виводу: номер функції передається в AH, параметри — в інших регістрах. Ці переривання забезпечують стандартизований доступ до екрана, клавіатури, файлів та системних сервісів. Реальний режим має фундаментальні обмеження: 1 МБ оперативної пам'яті, відсутність захисту пам'яті, відсутність багатозадачності, відсутність апаратної безпеки. Будь-яка програма може перезаписати будь-яку пам'ять, включаючи саму ОС. Захищений режим вирішує проблеми реального режиму за допомогою апаратної підтримки: віртуальна пам'ять (сторінкова організація) забезпечує великі адресні простори, кільця захисту забезпечують межі безпеки, а TSS дозволяє апаратно-асистоване перемикання задач. Ядро знаходиться в кільці 0: воно керує всіма апаратними ресурсами, планує процеси та надає сервіси через системні виклики. Програми користувача працюють у кільці 3 і повинні використовувати переривання/системні виклики для запиту сервісів ядра. Що ви будете робити:
Дослідження ТВП у пам'яті за допомогою Turbo Debugger (Dump за адресою 0000:0000)
Читання векторів переривань та відстеження їх до коду обробників
Дослідження області даних BIOS (0040:0000): буфер клавіатури, лічильник тактів таймера
Виклик переривань BIOS: INT 10h (відео), INT 16h (клавіатура)
Виклик переривань DOS: INT 21h (функції 01h, 02h, 09h, 4Ch)
Перехоплення вектора переривання: збереження оригіналу, встановлення свого обробника, відновлення при виході <br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Bootloader" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Bootloader" target="_self">Bootloader</a> — Вікіпедія — Огляд процесу завантаження та етапів завантажувача
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Power-on_self-test" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Power-on_self-test" target="_self">Power-on self-test</a> — Вікіпедія — Процес POST та ініціалізація апаратного забезпечення
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Reset_vector" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Reset_vector" target="_self">Reset vector</a> — Вікіпедія — Адреси першої інструкції для процесорів x86
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/BIOS" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/BIOS" target="_self">BIOS</a> — Вікіпедія — Роль мікропрограми у запуску системи та процесі завантаження
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Master_boot_record" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Master_boot_record" target="_self">Master boot record</a> — Вікіпедія — Структура MBR та початкове завантаження системи
<br><a data-tooltip-position="top" aria-label="https://mrszeto.net/CIT/interrupts.htm" rel="noopener nofollow" class="external-link is-unresolved" href="https://mrszeto.net/CIT/interrupts.htm" target="_self">DOS Interrupts</a> — Довідник функцій INT 21h для програмування лабораторних робіт
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/INT_10H" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/INT_10H" target="_self">INT 10h</a> — Вікіпедія — Відеосервіси BIOS
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/INT_16H" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/INT_16H" target="_self">INT 16h</a> — Вікіпедія — Сервіси клавіатури BIOS
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Interrupt" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Interrupt" target="_self">Interrupt</a> — Вікіпедія — Типи апаратних та програмних переривань
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Interrupt_vector_table" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Interrupt_vector_table" target="_self">Interrupt vector table</a> — Вікіпедія — Структура ТВП та методи обробки
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Protected_mode" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Protected_mode" target="_self">Protected mode</a> — Вікіпедія — Захищений режим x86 та рівні привілеїв
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Virtual_memory" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Virtual_memory" target="_self">Virtual memory</a> — Вікіпедія — Концепції та переваги віртуальної пам'яті
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Protection_ring" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Protection_ring" target="_self">Protection ring</a> — Вікіпедія — Ієрархічні домени захисту
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Kernel_(operating_system)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Kernel_(operating_system)" target="_self">Kernel (operating system)</a> — Вікіпедія — Обов'язки ядра та архітектура
<br><a data-tooltip-position="top" aria-label="https://syscalls.w3challs.com/" rel="noopener nofollow" class="external-link is-unresolved" href="https://syscalls.w3challs.com/" target="_self">Syscall Table for x86 Linux</a> — Довідник системних викликів Linux для програмування на асемблері
]]></description><link>lectures/l07.-переривання-та-взаємодія-з-ос.html</link><guid isPermaLink="false">Lectures/L07. Переривання та взаємодія з ОС.md</guid><pubDate>Tue, 10 Mar 2026 06:01:50 GMT</pubDate><enclosure url="assets/l07-reset-vector-td.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l07-reset-vector-td.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L06-NOTES. Системна архітектура]]></title><description><![CDATA[Короткий довідник до Лекції 6. Повні матеріали: L06. Системна архітектура.md.Лекція 6 переходить від програмування процесора до розуміння системи навколо нього. Частина A завершує набір інструкцій (бітові зсуви, довідник інструкцій). Частина B охоплює системну архітектуру: закон Мура, ІС, материнська плата/шини, ієрархія пам'яті, режими ЦП, архітектура фон Неймана vs Гарвардська, та вузьке місце фон Неймана.Дві родини: лінійні зсуви (біти втрачаються) та циклічні зсуви (біти обертаються).shl ax, 1 ; AX = AX * 2 (CF = старий MSB)
shr ax, 1 ; AX = AX / 2 (CF = старий LSB)
mov cl, 3
shl ax, cl ; AX = AX * 8
SAR зберігає знаковий біт — використовуйте для від'ємних чисел. SHR заповнює нулями, що дає неправильні результати для знакових від'ємних чисел.; 32-бітний зсув ліворуч через RCL (DX:AX)
shl ax, 1 ; CF = старий MSB з AX
rcl dx, 1 ; старий CF входить у LSB з DX
Дізнатися більше:
<a data-tooltip-position="top" aria-label="https://xlogicx.net/Assembly_Is_Too_High-Level_-_Better_Call_SAL.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://xlogicx.net/Assembly_Is_Too_High-Level_-_Better_Call_SAL.html" target="_self">Assembly Is Too High-Level — Better Call SAL</a>
~100 базових мнемонік 8086 за категоріями: переміщення даних, арифметика, логіка, зсуви/ротації, керування потоком, рядкові операції, керування прапорцями, введення/виведення, системні.Кожна інструкція має: мнемоніку, код операції (машинний код), операнди, прапорці, що змінюються.Цікаві: NOP (0x90) = XCHG AX, AX; LEA обчислює адреси без доступу до пам'яті; XLAT виконує AL = [BX+AL].Дізнатися більше:
<br><a data-tooltip-position="top" aria-label="http://pnx.tf/files/x86_opcode_structure_and_instruction_overview.pdf" rel="noopener nofollow" class="external-link is-unresolved" href="http://pnx.tf/files/x86_opcode_structure_and_instruction_overview.pdf" target="_self">x86 Opcode Structure and Instruction Overview (PDF)</a>
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/X86_instruction_listings#Original_8086/8088_instructions" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/X86_instruction_listings#Original_8086/8088_instructions" target="_self">Wikipedia: Original 8086/8088 Instructions</a>
Кількість транзисторів подвоюється приблизно кожні 2 роки (Гордон Мур, 1965/1975). Емпіричне спостереження, а не фізичний закон. Самоздійснюване: дорожні карти R&amp;D індустрії вирівнювались під нього.Масштабування Деннарда (1974): менші транзистори споживають менше енергії. Припинило діяти ~2006 → «стіна потужності» → перехід від масштабування частоти до багатоядерного паралелізму.Рушійні сили: вдосконалення фотолітографії, FinFET → GAA транзистори, передове пакування (UltraFusion, CoWoS), EUV літографія.Дізнатися більше:
<br><a data-tooltip-position="top" aria-label="https://ourworldindata.org/moores-law" rel="noopener nofollow" class="external-link is-unresolved" href="https://ourworldindata.org/moores-law" target="_self">What is Moore's Law? — Our World in Data</a>
<br><a data-tooltip-position="top" aria-label="https://www.apple.com/newsroom/2025/03/apple-reveals-m3-ultra-taking-apple-silicon-to-a-new-extreme/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.apple.com/newsroom/2025/03/apple-reveals-m3-ultra-taking-apple-silicon-to-a-new-extreme/" target="_self">Apple M3 Ultra — Apple Newsroom</a>
Перший мікроконтролер: TMS1000 (1974) — 4-бітний ЦП, 1 КБ ПЗП, 64x4 ОП, ~$2/чіп.
Перший мікропроцесор: Intel 4004 (1971) — 2 300 транзисторів.Дізнатися більше:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/7400-series_integrated_circuits" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/7400-series_integrated_circuits" target="_self">7400-series ICs — Wikipedia</a>
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Texas_Instruments_TMS1000" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Texas_Instruments_TMS1000" target="_self">TMS1000 — Wikipedia</a>
Материнська плата містить: сокет ЦП, слоти пам'яті, чипсет, слоти PCIe, роз'єми накопичувачів, тактовий генератор, прошивку (BIOS/UEFI).8086: 20-бітна адреса → 1 МБ. 80386: 32-бітна → 4 ГБ. x86-64: 48-бітна → 256 ТБ.Дізнатися більше:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/System_bus" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/System_bus" target="_self">System Bus — Wikipedia</a>
Кварцовий генератор, що створює тактові імпульси. Один цикл = одна одиниця роботи.Тактова частота ≠ продуктивність. Також залежить від: IPC, глибини конвеєра, розміру кешу, пропускної здатності пам'яті.Розгін: робота на частоті вищій за номінальну → більше тепла, можлива нестабільність, скорочення терміну служби.Протокол доступу до DRAM: (1) адреса на шину → (2) сигнал READ → (3) очікування ~200 циклів → (4) копіювання даних.Кеш: використовує часову локальність (повторне використання) та просторову локальність (сусідній доступ). L1 розділений: L1i (інструкції) + L1d (дані). Кеш прозорий для програм.Дізнатися більше:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Cache_(computing)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Cache_(computing)" target="_self">Cache (computing) — Wikipedia</a>
<br><a data-tooltip-position="top" aria-label="https://people.cs.pitt.edu/~melhem/courses/1502p/Unit5/Unit5.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://people.cs.pitt.edu/~melhem/courses/1502p/Unit5/Unit5.html" target="_self">Modelling the Memory — Univ. of Pittsburgh</a>
Карта пам'яті реального режиму: IVT (0–3FF), BDA (400–4FF), основна пам'ять (500–9FFFF), відео (A0000–BFFFF), ROM (F0000–FFFFF).Захищений режим: сегментні регістри містять селектори → записи GDT з базою, лімітом, правами доступу. Програми використовують системні виклики для доступу до апаратного забезпечення.У нашому курсі: лабораторні Л1–Л10 — реальний режим (DOSBox/TASM), Л11–Л15 — захищений режим (Linux x86-64).Модифікована Гарвардська: сучасні ЦП — фон Неймана ззовні (єдина ОП), Гарвардська всередині (розділений кеш L1i/L1d).Дізнатися більше:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Von_Neumann_architecture" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Von_Neumann_architecture" target="_self">Von Neumann Architecture — Wikipedia</a>
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Harvard_architecture" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Harvard_architecture" target="_self">Harvard Architecture — Wikipedia</a>
Вибірка → Декодування → Виконання (повторюється безперервно):
Вибірка: IP → адресна шина → байти інструкції → регістр інструкцій; IP збільшується
Декодування: блок керування читає код операції → визначає операцію, операнди, додатковий доступ до пам'яті
Виконання: АЛП обчислює → результат у приймач → FLAGS оновлюється → назад до вибірки
Дізнатися більше:
<br><a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=Z5JC9Ve1sfI" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=Z5JC9Ve1sfI" target="_self">The Fetch-Execute Cycle — YouTube (Том Скотт)</a>
Спільна шина означає, що ЦП не може одночасно вибирати інструкції та отримувати дані. Розрив швидкостей ЦП-пам'ять зріс драматично (ЦП ~1000x швидший з 1980-х, DRAM лише ~10x).Способи подолання:
Кеш-пам'ять — 95–99% влучень зменшує ефективний час доступу
Розділений кеш L1 — модифікована Гарвардська: L1i + L1d дозволяють паралельний доступ
Попередня вибірка — апаратне передбачення та попереднє завантаження даних
Конвеєрна обробка — перекриття вибірки/декодування/виконання різних інструкцій
Передбачення розгалужень — вгадування результатів переходів (95–99% точність)
Позачергове виконання — виконання готових інструкцій під час очікування
Суперскалярність — кілька виконавчих блоків працюють паралельно SHL/SHR для беззнакових, SAL/SAR для знакових операцій зсуву (SAL = SHL)
Закон Мура: кількість транзисторів подвоюється ~кожні 2 роки (60+ років)
ІС → мікроконтролери → мікропроцесори = зростання рівня інтеграції
Системна шина = шина даних + адресна + керування, синхронізовані тактовим сигналом
Ієрархія пам'яті долає розрив швидкість-вартість (регістри → кеш → DRAM → диск)
Реальний режим: без захисту, 1 МБ. Захищений режим: ізоляція, 4 ГБ, кільця привілеїв
Фон Неймана: єдина пам'ять. Гарвардська: окрема. Сучасні ЦП: гібрид
Вузьке місце фон Неймана — домінуюче обмеження продуктивності
Лабораторна 6: Масиви та режими адресаціїЩо ви будете робити:
Використання режимів адресації: [BX], [SI], [BX+SI], [BX+SI+disp]
Оголошення масивів за допомогою DB, DW, DD
Спостереження за розташуванням у пам'яті у вікні Dump Turbo Debugger
Застосування інструкцій бітового зсуву до обробки масивів
]]></description><link>lectures/l06-notes.-системна-архітектура.html</link><guid isPermaLink="false">Lectures/L06-NOTES. Системна архітектура.md</guid><pubDate>Tue, 10 Mar 2026 06:01:50 GMT</pubDate></item><item><title><![CDATA[L06. Системна архітектура]]></title><description><![CDATA[
Лекція 6 курсу «Архітектура комп'ютерних систем» в НаУКМА.
Після цієї лекції ви зможете:
Використовувати інструкції бітового зсуву (SHL, SHR, SAL, SAR, ROL, ROR) та пояснювати різницю між логічним та арифметичним зсувами
Орієнтуватися в довідкових ресурсах інструкцій x86 для пошуку кодування інструкцій та їхньої поведінки
Пояснити закон Мура та його значення для еволюції обчислювального обладнання
Описати прогрес від інтегральних схем до мікроконтролерів і мікропроцесорів
Визначити основні компоненти комп'ютерної системи: ЦП (центральний процесор), материнська плата, пам'ять та пристрої введення/виведення
Пояснити роль системної шини (шини даних, адресної шини та шини керування) і тактового генератора
Описати ієрархію пам'яті від регістрів через кеш-пам'ять до динамічної пам'яті (DRAM) та дискового сховища
Порівняти реальний режим та захищений режим роботи процесора x86
Зіставити архітектуру фон Неймана та Гарвардську архітектуру
Описати цикл виконання інструкцій (вибірка-декодування-виконання) та пояснити вузьке місце фон Неймана
Ця лекція є поворотним моментом курсу. За перші п'ять лекцій ми побудували міцну основу програмування на x86 асемблері: регістри та адресація пам'яті (Лекція 3), арифметичні та логічні інструкції (Лекція 4), організація програм зі стеком та процедурами (Лекція 5). Тепер ми вміємо писати повноцінні програми на асемблері з функціями, параметрами та локальними змінними. Але досі ми працювали всередині машини, не зупиняючись, щоб поглянути на саму машину.Лекція 6 — «Від закону Мура до мікропроцесорів» — змінює нашу перспективу: від програмування процесора до розуміння системи, що його оточує. Чому сучасний чіп може містити мільярди транзисторів? Як ЦП взаємодіє з пам'яттю та периферійними пристроями? Чому доступ до оперативної пам'яті (ОП) у тисячі разів повільніший за доступ до регістра? Це питання системної архітектури, і відповіді на них поглиблять ваше розуміння кожної програми, яку ви пишете.Лекція організована у двох частинах:Частина А: Завершення набору інструкцій («Розставляємо крапки над і»). Перш ніж перейти до системної архітектури, ми завершуємо теми з попередніх лекцій. Ми коротко повернемося до конвенції виклику C/C++ з Лекції 5, а потім розглянемо останню категорію інструкцій, яку ми ще не вивчали детально: операції бітового зсуву та ротації. Також ми вкажемо на комплексні довідники інструкцій x86 для подальшого вивчення.Частина Б: Системна архітектура. Основна частина лекції прослідковує шлях від закону Мура та еволюції інтегральних схем до структури повноцінної комп'ютерної системи. Ми розглянемо архітектуру фон Неймана (ЦП, пам'ять, шина, введення/виведення), три типи системних шин, тактовий генератор, ієрархію пам'яті від регістрів до дискового сховища, цикл виконання інструкцій (вибірка-декодування-виконання), вузьке місце фон Неймана та Гарвардську архітектуру як альтернативний підхід.У Лекції 5 ми встановили конвенцію виклику C як стандартизований протокол для викликів функцій. Коротко нагадаємо: викликаюча сторона зберігає волатильні регістри (AX, CX, DX) перед викликом і поміщує параметри у стек у зворотному порядку (справа наліво). Викликана сторона зберігає BP, формує стековий кадр (push bp; mov bp, sp), виділяє місце для локальних змінних, зберігає будь-які неволатильні регістри, які вона змінює, і повертає результат у AX (або DX:AX для 32-бітних значень). Після повернення з виклику викликаюча сторона очищує параметри зі стека. Ця конвенція забезпечує коректну взаємодію асемблерних функцій, написаних різними програмістами — або згенерованих різними компіляторами. Ми не будемо тут повторювати деталі; зверніться до Лекції 5, Розділ 5 для повного пояснення та прикладів.Завершивши тему конвенцій виклику, ми переходимо до останнього великого пропуску у нашому покритті набору інструкцій: операцій бітового зсуву.У Лекції 4 ми розглянули побітові логічні операції AND, OR, XOR та NOT. Вони оперують окремими бітами на місці — комбінують відповідні бітові позиції двох операндів. Операції бітового зсуву відрізняються: вони переміщують усі біти в регістрі ліворуч або праворуч на задану кількість позицій, відкидаючи біти, що виходять за один кінець, і заповнюючи звільнені позиції з іншого кінця. Ця, на перший погляд, проста операція виявляється одним із найкорисніших інструментів у арсеналі системного програміста.Операції зсуву виконують три критичні функції: Швидке множення та ділення. Зсув двійкового числа ліворуч на одну позицію еквівалентний множенню на 2; зсув праворуч — діленню на 2. Оскільки інструкції зсуву виконуються за один тактовий цикл (порівняно з десятками циклів, необхідних для MUL та DIV), компілятори регулярно замінюють множення та ділення на степені 2 інструкціями зсуву. Вилучення та вставка бітових полів. Апаратні регістри, мережеві протоколи та формати файлів часто пакують кілька значень в одне слово. Зсуви у поєднанні з масками (AND) дозволяють ефективно вилучати або вставляти конкретні бітові поля. Низькорівневі алгоритми. Криптографічні алгоритми, хеш-функції, обчислення CRC та графічні підпрограми значною мірою покладаються на зсуви й ротації для маніпулювання даними на рівні бітів. Набір інструкцій x86 надає дві родини операцій зсуву: лінійні зсуви (біти виходять з одного кінця і втрачаються) та ротації (біти переходять з одного кінця на інший).Інструкція SHL (Shift Left — зсув ліворуч) переміщує кожний біт операнда на одну або більше позицій ліворуч. Звільнені позиції праворуч заповнюються нулями. Останній біт, що виходить зі старшої позиції, поміщується у прапорець переносу (CF).Синтаксис:SHL destination, count ; count = 1 або CL
На 8086 лічильник має бути або літеральним значенням 1, або регістром CL. (Пізніші процесори дозволяють безпосереднє значення лічильника до 31.)Приклад — зсув 10101111 ліворуч на 1:Розглянемо значення 10101111 у двійковій системі (175 у десятковій, або AFh). Перед зсувом CF має якесь попереднє значення:До: CF [1 0 1 0 1 1 1 1] MSB LSB SHL AH, 1 Після: CF [0 1 0 1 1 1 1 0] 1 зсув ліворуч, 0 заповнює справа
Ліворуч: SHL переміщує біти до старшого біту (MSB), заповнюючи молодший біт (LSB) нулем. Праворуч: SHR переміщує біти до молодшого біту (LSB), заповнюючи старший біт (MSB) нулем.Старший біт (1) був зсунутий у CF. Усі інші біти перемістилися на одну позицію ліворуч. Нуль заповнив звільнений молодший біт. Результат — 01011110 у двійковій системі (94 у десятковій, або 5Eh).Приклад коду:mov ah, 10101111b ; AH = AFh = 175
shl ah, 1 ; AH = 5Eh = 94, CF = 1 (старий MSB)
Зверніть увагу, що 175 як беззнакове число не перетворюється просто на 350 (що було б очікуваним результатом множення на 2), оскільки 350 перевищує 8-бітний діапазон (0-255). Результат 94 — це 350 mod 256 — множення «переповнилось». Прапорець переносу сигналізує про це переповнення.Зсув на кілька позицій:mov cl, 3
shl ax, cl ; Зсув AX ліворуч на 3 позиції = множення на 8
Кожний зсув ліворуч на 1 подвоює значення (для значень, що вміщуються в регістр). Зсув ліворуч на N позицій множить на 2^N:Інструкція SHR (Shift Right — зсув праворуч) переміщує кожний біт праворуч. Нулі заповнюють зліва (з боку старшого біту). Останній біт, що виходить з молодшого біту, переходить у CF.Синтаксис:SHR destination, count ; count = 1 або CL
Приклад — зсув 10101111 праворуч на 1:До: [1 0 1 0 1 1 1 1] CF MSB LSB SHR AH, 1 Після: [0 1 0 1 0 1 1 1] CF 0 заповнює зліва 1 (старий LSB)
Результат — 01010111 у двійковій системі (87 у десятковій, або 57h). Початкове значення було 175; ділення на 2 дає 87.5, а цілочисельний результат — 87 (залишок, 1, потрапив у CF). Це беззнакове цілочисельне ділення на 2 — воно завжди заповнює нулями зліва.Приклад коду:mov ah, 10101111b ; AH = AFh = 175
shr ah, 1 ; AH = 57h = 87, CF = 1 (залишок)
Логічні зсуви розглядають операнд як послідовність бітів без особливого значення, присвоєного будь-якій позиції. Арифметичні зсуви призначені для знакових чисел у доповняльному коді, де старший біт є знаковим бітом.А ось і сюрприз: SAL ідентичний SHL. Та сама операція, той самий результат, ті самі прапорці. Фактично, на всіх процесорах x86 SAL reg, 1 та SHL reg, 1 генерують абсолютно однаковий машинний код при асемблюванні більшістю асемблерів.Чому це має сенс? Розглянемо, що відбувається при зсуві знакового числа ліворуч на 1. У доповняльному коді знаковий біт — це старший біт. Коли ми зсуваємо ліворуч, старший біт потрапляє у CF, а нуль заповнює справа — саме те, що робить SHL. Математичний ефект — множення на 2, незалежно від того, інтерпретується число як знакове чи беззнакове. Бітовий шаблон однаковий; відрізняється лише наша інтерпретація.Наприклад, розглянемо знакове значення -81 (10101111 у 8-бітному доповняльному коді). Зсув ліворуч на 1:mov ah, 10101111b ; AH = -81 (знакове) або 175 (беззнакове)
sal ah, 1 ; AH = 01011110b = 94 ; Як знакове: 94 (переповнення — ми втратили знак!) ; CF = 1, OF = 1 (знак змінився)
Прапорець переповнення (OF) встановлюється, оскільки знаковий біт змінився — це сигналізує про знакове переповнення, так само як і для ADD. Інструкція ідентична SHL; SAL існує як мнемонічний псевдонім, щоб програмісти могли позначити свій намір працювати зі знаковими даними.Тут є захоплива примітка з історії x86. Коли Intel проєктувала карту кодів операцій для інструкцій зсуву під байтом D0, вона виділила вісім слотів (так званих розширень регістра ModR/M від /0 до /7):Слот /6 помітно відсутній в офіційній документації Intel. Але якщо ви вручну сконструюєте байт машинного коду з використанням D0 /6, процесор його виконає — і він поводиться точно як SHL (/4). Це «прихований» код SAL. Intel задокументувала SAL як мнемонічний синонім SHL і залишила /6 як недокументований, але функціональний код операції. Більшість асемблерів генерують /4 (кодування SHL) і для SHL, і для SAL мнемонік, тому /6 ніколи не генерується стандартними інструментами, але процесор завжди його підтримував. Стаття «<a data-tooltip-position="top" aria-label="https://xlogicx.net/Assembly_Is_Too_High-Level_-_Better_Call_SAL.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://xlogicx.net/Assembly_Is_Too_High-Level_-_Better_Call_SAL.html" target="_self">Assembly Is Too High-Level — Better Call SAL</a>» досліджує цю цікавинку детально, називаючи її навмисним упущенням, прихованим на видному місці.<br><img alt="Better Call SAL — пародія на «Краще подзвоніть Солу» з відсиланням до прихованої інструкції SAL" src="assets/l06-better-call-sal.png" target="_self">«Better Call SAL» — відсилання до прихованої інструкції SAL в архітектурі x86.На відміну від SAL (який ідентичний SHL), SAR відрізняється від SHR у важливому аспекті: він зберігає знаковий біт. Коли SAR зсуває біти праворуч, він заповнює звільнену позицію старшого біту копією початкового знакового біту, а не нулем.Синтаксис:SAR destination, count ; count = 1 або CL
Приклад — SAR проти SHR для від'ємного числа:Порівняємо, як SHR та SAR обробляють значення 10101111 (яке є -81 у знаковому 8-бітному, або 175 у беззнаковому форматі):SHR: [1 0 1 0 1 1 1 1] → [0 1 0 1 0 1 1 1] = 87 беззнакове, 87 знакове заповнює 0 ──────────^ SAR: [1 0 1 0 1 1 1 1] → [1 1 0 1 0 1 1 1] = D7h беззнакове, -41 знакове знаковий біт копіюється ─^
<br><img alt="SAR зберігає знаковий біт при зсуві праворуч" src="assets/l06-sar-shift-arithmetic-right.png" target="_self">SAR (арифметичний зсув праворуч): знаковий біт (MSB) копіюється, а не заповнюється нулем, зберігаючи знак від'ємних чисел.При SHR результат — 87, що правильно для беззнакового 175 / 2 = 87. Але якщо ми мали на увазі знакове значення (-81), ми очікуємо -81 / 2 = -40 або -41. SHR дає неправильну відповідь (87 — додатне!). SAR дає -41, що є правильним знаковим діленням на 2 із округленням до мінус нескінченності.Приклад коду:; Беззнакове ділення на 2
mov ah, 10101111b ; AH = 175 (беззнакове)
shr ah, 1 ; AH = 87, правильно для беззнакового ; Знакове ділення на 2
mov ah, 10101111b ; AH = -81 (знакове)
sar ah, 1 ; AH = -41 (знакове), CF = 1
Ключовий висновок для додатних чисел: Коли старший біт дорівнює 0 (додатні знакові числа), SAR і SHR дають ідентичні результати, оскільки копіювання 0 у старший біт — це те саме, що заповнення нулем. SAR і SHR розходяться лише для від'ємних чисел (старший біт = 1).Ключовий висновок: Зсуви ліворуч однакові для знакових і беззнакових чисел (SHL = SAL). Зсуви праворуч відрізняються: SHR для беззнакових значень, SAR для знакових. Оберіть неправильний — і від'ємні числа дадуть хибні результати.Інструкції зсуву драматично швидші за MUL та DIV. На 8086 MUL займає 70-160 тактових циклів залежно від розміру операндів, тоді як SHL — лише 2 цикли. Навіть на сучасних процесорах, де різниця менша, компілятори агресивно використовують цю оптимізацію.Множення на константу, що не є степенем 2, за допомогою зсувів та додавань:; Множення AX на 10 (= 8 + 2 = 2^3 + 2^1)
mov bx, ax ; BX = AX (зберігаємо оригінал)
mov cl, 3
shl ax, cl ; AX = AX * 8
shl bx, 1 ; BX = оригінал * 2
add ax, bx ; AX = оригінал * 8 + оригінал * 2 = оригінал * 10
Ділення на степені 2:; Беззнакове ділення на 16
shr ax, 4 ; (на 8086: mov cl, 4 / shr ax, cl) ; Знакове ділення на 4
sar ax, 2 ; (на 8086: mov cl, 2 / sar ax, cl)
Вилучення бітового поля:; Вилучення бітів 4-7 з AL (4-бітне поле)
mov cl, 4
shr al, cl ; Зсуваємо поле до бітів 0-3
and al, 0Fh ; Маскуємо все вище біту 3
Іноді потрібно зсувати біти, не втрачаючи їх. Інструкції ротації (циклічного зсуву) обертають біти по колу: замість того, щоб зникнути з одного кінця, біт з'являється з іншого.ROL зсуває всі біти ліворуч на одну позицію. Старший біт, замість того щоб бути відкинутим, переходить на місце нового молодшого біту. Старший біт також копіюється у CF.До: [1 0 1 0 1 1 1 1] CF = ? MSB LSB ROL AH, 1 Після: [0 1 0 1 1 1 1 1] CF = 1 ^ ^ | └── старий MSB переходить сюди └── старий біт 6 тепер MSB
Приклад коду:mov ah, 10101111b ; AH = AFh
rol ah, 1 ; AH = 01011111b = 5Fh, CF = 1
На відміну від SHL (який би дав 01011110), ROL поміщує старий старший біт у позицію молодшого біту, даючи 01011111.Ліворуч: ROL переносить старший біт на місце нового молодшого біту. Праворуч: ROR переносить молодший біт на місце нового старшого біту.ROR зсуває всі біти праворуч на одну позицію. Молодший біт переходить на місце нового старшого біту. Молодший біт також копіюється у CF.До: [1 0 1 0 1 1 1 1] CF = ? MSB LSB ROR AH, 1 Після: [1 1 0 1 0 1 1 1] CF = 1 ^ ^ └── старий LSB переходить ─┘
Приклад коду:mov ah, 10101111b ; AH = AFh
ror ah, 1 ; AH = 11010111b = D7h, CF = 1
RCL (Rotate through Carry Left — циклічний зсув ліворуч через перенос) та RCR (Rotate through Carry Right — циклічний зсув праворуч через перенос) включають прапорець переносу як додатковий біт у ротації. Для 8-бітного операнда це створює 9-бітне кільце ротації: 8 бітів регістра плюс CF.Операція RCL:До: CF [b7 b6 b5 b4 b3 b2 b1 b0] RCL reg, 1 Після: b7 [b6 b5 b4 b3 b2 b1 b0 CF_old] ↑ ↑ біти зсуваються ліворуч старий CF входить справа
Старий старший біт переходить у CF, старий CF входить у позицію молодшого біту, а всі інші біти зсуваються ліворуч на один.Операція RCR:До: [b7 b6 b5 b4 b3 b2 b1 b0] CF RCR reg, 1 Після: [CF_old b7 b6 b5 b4 b3 b2 b1] b0 ↑ ↑ старий CF входить зліва старий LSB переходить у CF
Приклад коду — багатобайтовий зсув за допомогою RCL:Одне з потужних застосувань RCL — зсув значень, ширших за 16 біт. Припустімо, ви хочете зсунути 32-бітне значення, збережене у DX:AX, ліворуч на 1:; Зсув DX:AX ліворуч на 1 (множення 32-бітного значення на 2)
shl ax, 1 ; Зсув молодшого слова ліворуч; MSB від AX переходить у CF
rcl dx, 1 ; Зсув старшого слова ліворуч; старий CF (від AX) входить у LSB від DX
Ця послідовність із двох інструкцій коректно передає перенос від молодшого слова до старшого, виконуючи 32-бітний зсув за допомогою 16-бітних інструкцій. Без RCL знадобилося б кілька додаткових інструкцій для досягнення того самого результату.Приклад коду — багатобайтовий зсув за допомогою RCR:; Зсув DX:AX праворуч на 1 (ділення 32-бітного беззнакового значення на 2)
shr dx, 1 ; Зсув старшого слова праворуч; LSB від DX переходить у CF
rcr ax, 1 ; Зсув молодшого слова праворуч; старий CF (від DX) входить у MSB від AX
<br><img alt="Підсумок усіх інструкцій зсуву та ротації x86" src="assets/l06-shift-rotate-summary-table.png" target="_self">Повний підсумок інструкцій зсуву та ротації: логічні зсуви (SHL, SHR), арифметичні зсуви (SAL, SAR) та ротації (ROL, ROR, RCL, RCR).Після розгляду інструкцій зсуву та ротації ми тепер охопили всі основні категорії оригінального набору інструкцій 8086: переміщення даних (MOV, PUSH, POP, LEA), арифметика (ADD, SUB, MUL, DIV, INC, DEC, NEG), логічні операції (AND, OR, XOR, NOT), зсуви та ротації (SHL, SHR, SAL, SAR, ROL, ROR, RCL, RCR), керування потоком виконання (JMP, Jcc, LOOP, CALL, RET) та рядкові операції (MOVSB, CMPSB тощо).Для вичерпного довідника з усіх інструкцій 8086 — включно з тими, які ми не розглядали на лекціях (такими як інструкції BCD-корекції AAA/AAS/AAM/AAD, рядкові операції з префіксами REP та інструкції введення/виведення IN/OUT) — наступні ресурси є безцінними:
<br>
Wikipedia: x86 instruction listings (<a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/X86_instruction_listings#Original_8086/8088_instructions" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/X86_instruction_listings#Original_8086/8088_instructions" target="_self">Original 8086/8088 instructions</a>) — Повна таблиця кожної інструкції 8086 з кодуванням кодів операцій, форматами операндів та короткими описами. Це один із найкращих доступних ресурсів для швидкого пошуку. <br>
x86 Opcode Structure and Instruction Overview (<a data-tooltip-position="top" aria-label="http://pnx.tf/files/x86_opcode_structure_and_instruction_overview.pdf" rel="noopener nofollow" class="external-link is-unresolved" href="http://pnx.tf/files/x86_opcode_structure_and_instruction_overview.pdf" target="_self">pnx.tf reference</a>) — Детальний посібник зі структури машинного коду x86, включаючи байт ModR/M, байт SIB та байти префіксів. Обов'язкове читання, якщо ви хочете зрозуміти кодування, яке ми обговорювали для SAL/SHL. Ці довідники будуть корисними протягом решти курсу та у вашій лабораторній роботі. Вам не потрібно запам'ятовувати кожну інструкцію; знати, де шукати — це практична навичка.Процесор Intel 8086 постачався з керованим набором приблизно із 100 базових мнемонік інструкцій. Хоча на перший погляд ця кількість може здатися великою, інструкції поділяються на невелику кількість логічних категорій, і багато з них мають подібні шаблони кодування. Уміння працювати з довідником інструкцій — одна з найважливіших практичних навичок для кожного, хто працює з мовою асемблера: ви ніколи не запам'ятаєте кожну інструкцію, але маєте вміти швидко знайти будь-яку інструкцію та зрозуміти її кодування, операнди та вплив на прапорці.<br><img alt="Карта кодів операцій x86, що показує всі кодування інструкцій у сітці 16×16" src="assets/l06-x86-opcode-map.png" target="_self">Карта кодів операцій x86: кожна комірка відповідає одному з 256 можливих однобайтових кодів операцій, позначених кольором за категорією інструкцій.Оригінальні інструкції 8086/8088 згруповані за функцією:Кожна інструкція має кілька ключових атрибутів, які ви знайдете в довіднику:
Мнемоніка — зрозуміла для людини назва (наприклад, ADD)
Код операції — байт(и) машинного коду, який фактично виконує ЦП (наприклад, 0x00...0x05 для ADD)
Операнди — те, з чим працює інструкція (регістр, адреса пам'яті, безпосереднє значення)
Змінювані прапорці — які біти в регістрі FLAGS змінюються після виконання (наприклад, ZF, CF, OF)
Розуміння цих атрибутів безпосередньо пов'язане з тим, що ми дізналися в Лекції 5 про читання послідовностей байтів машинного коду. Коли ви бачите байт 0x90 у дампі пам'яті, довідник інструкцій повідомляє вам, що це NOP (відсутність операції) — який фактично закодований як XCHG AX, AX. Коли ви бачите 0xE8, за яким ідуть два байти, ви знаєте, що це ближній CALL із 16-бітовим відносним зміщенням.Розглянемо, як читати типовий запис довідника інструкцій. Візьмемо ADD:
Мнемоніка: ADD — означає "Додати"
Форми операндів: (1) r += r/m/imm; — додати регістр, значення з пам'яті або безпосереднє значення до регістра; (2) m += r/imm; — додати регістр або безпосереднє значення до комірки пам'яті
Коди операцій: 0x00...0x05, 0x80/0...0x81/0, 0x83/0
Змінювані прапорці: OF, SF, ZF, AF, CF, PF
Позначення r/m/imm є стандартним скороченням: r означає регістр, m — адресу пам'яті, а imm — безпосереднє значення (константу, вбудовану в інструкцію). Позначення через скісну риску в кодах операцій, як-от 0x80/0, означає "байт коду операції 0x80 з полем reg байта ModR/M, встановленим у 0" — деталь, яку ми досліджували в Лекції 5 при декодуванні машинного коду.Зверніть увагу, що ADD має кілька байтів коду операції. Це тому, що одна мнемоніка може кодувати кілька різних комбінацій операндів: байт проти слова, регістр-до-регістра проти регістр-до-пам'яті, з безпосереднім значенням або без нього. ЦП обирає відповідне кодування на основі типів та розмірів операндів.Кілька інструкцій у довіднику заслуговують на окрему увагу, тому що вони розкривають цікаві проєктні рішення:
NOP (0x90) — "Відсутність операції" нічого не робить, проте це одна з найкорисніших інструкцій. Вона використовується для вирівнювання, затримок за часом та як заповнювач під час налагодження. Її код операції фактично збігається з XCHG AX, AX, який обмінює AX із самим собою — розумне повторне використання простору кодування.
LEA (Load Effective Address) — Незважаючи на свою назву, LEA взагалі не звертається до пам'яті. Вона обчислює адресу та зберігає її в регістрі. Програмісти часто використовують її як швидкий спосіб виконати арифметику (наприклад, LEA BX, [SI+DI+4] обчислює SI + DI + 4 без читання з пам'яті).
XLAT (0xD7) — Однобайтова інструкція, що виконує AL = [BX + AL], реалізуючи таблицю пошуку на 256 записів однією інструкцією. Це нагадування про те, як розробники 8086 оптимізували під типові шаблони використання.
INT (програмне переривання) — Шлюз до служб операційної системи. INT 21h у DOS та INT 80h у Linux — це спосіб, яким програми на асемблері запитують функції ОС. Ми будемо широко використовувати їх у лабораторних роботах.
Два ресурси є особливо корисними під час вивчення набору інструкцій 8086:
<br>
x86 Opcode Structure and Instruction Overview (<a data-tooltip-position="top" aria-label="http://pnx.tf/files/x86_opcode_structure_and_instruction_overview.pdf" rel="noopener nofollow" class="external-link is-unresolved" href="http://pnx.tf/files/x86_opcode_structure_and_instruction_overview.pdf" target="_self">PDF</a>) — Цей документ показує, як байти кодів операцій структуровані всередині: які біти визначають операцію, які біти вказують напрямок (з регістра в пам'ять або з пам'яті в регістр), а які біти кодують розмір операнда. Це безцінний ресурс для розуміння чому інструкції кодуються саме так, а не лише для запам'ятовування значень кодів операцій. <br>
Wikipedia: Original 8086/8088 Instructions (<a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/X86_instruction_listings#Original_8086/8088_instructions" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/X86_instruction_listings#Original_8086/8088_instructions" target="_self">посилання</a>) — Це найкраща таблиця-довідник для оригінального набору інструкцій. Вона містить перелік усіх мнемонік в алфавітному порядку з їхнім значенням, позначеннями операндів та байтами кодів операцій. Коли вам потрібно дізнатися, що робить CBW, або який код операції використовує STOSB, — це місце, куди варто звертатися. Варто зазначити, що в міру розвитку архітектури x86 протягом наступних десятиліть — від 80286, 80386 та 80486 через сімейство Pentium до сучасних процесорів x86-64 — набір інструкцій зріс колосально. Сучасні процесори x86-64 підтримують тисячі інструкцій, включаючи розширення для арифметики з рухомою комою (x87, SSE, AVX), криптографії (AES-NI) та векторної обробки (AVX-512). Проте в основі залишаються фундаментальні шаблони, встановлені оригінальним 8086: переміщення даних, арифметика, логіка, керування потоком виконання та обробка рядків. Опанувавши оригінальний набір інструкцій та навчившись ефективно використовувати довідники, ви будуєте фундамент, який безпосередньо переноситься на розуміння навіть найскладніших сучасних розширень.У 1965 році Гордон Е. Мур — тоді директор з досліджень і розробок у Fairchild Semiconductor, а згодом співзасновник Intel — опублікував коротку статтю в журналі Electronics, яка стала одним із найвпливовіших прогнозів в історії технологій. На основі лише кількох точок даних Мур зауважив, що кількість транзисторів, які можна економічно вигідно розмістити на інтегральній схемі (ІС), подвоюється приблизно щороку. Він прогнозував, що ця тенденція триватиме "принаймні десять років".У 1975 році Мур переглянув свою оцінку: період подвоєння був ближчим до двох років. Це переглянуте спостереження — кількість транзисторів на чіпі подвоюється приблизно кожні два роки — стало відомим як Закон Мура. Варто зазначити, що оригінальна стаття Мура була напрочуд короткою та прагматичною. Він не намагався сформулювати грандіозну теорію — він робив бізнес-прогноз щодо того, що є економічно оптимальним. "Закон" виник лише ретроспективно, коли індустрія продовжувала досягати прогнозованих показників рік за роком.Критично важливо розуміти, чим є Закон Мура і чим він не є. Закон Мура — це не закон фізики. Не існує рівняння в квантовій механіці чи термодинаміці, яке вимагає подвоєння кількості транзисторів за фіксований графік. Натомість Закон Мура — це емпіричне спостереження щодо стійкої здатності напівпровідникової індустрії зменшувати транзистори та вдосконалювати виробничі процеси.<br><img alt="Оригінальний графік Гордона Мура 1965 року, що показує подвоєння кількості компонентів" src="assets/l06-moores-law-original-graph.png" target="_self">Оригінальний графік Гордона Мура 1965 року: кількість компонентів на інтегральній схемі з екстраполяцією до 1975 року. Спостереження базувалося лише на кількох точках даних.Що робить його видатним — це його довговічність. Мур спочатку прогнозував, що тенденція протримається десятиліття. Вона тримається вже понад 60 років. Причина не в тому, що кремній підкоряється якомусь закону подвоєння, — а в тому, що напівпровідникова індустрія організувалася навколо цього прогнозу. Закон Мура став самоздійснюваним пророцтвом: виробники чіпів будували свої дорожні карти досліджень і розробок, щоб відповідати очікуваному подвоєнню, виробники обладнання проєктували літографічні інструменти, щоб це уможливити, а розробники програмного забезпечення писали програми, які передбачали постійне зростання швидкості апаратного забезпечення. Вся екосистема вибудувалася навколо цієї експоненціальної траєкторії.Щоб проілюструвати подвоєння конкретними цифрами: якщо почати з 2 300 транзисторів (Intel 4004, 1971 рік) і подвоювати кожні два роки, то через 54 роки (27 подвоєнь) можна очікувати приблизно 2 300 x 2^27 = приблизно 309 мільярдів транзисторів. Фактична кількість для Apple M3 Ultra у 2025 році — 184 мільярди — перебуває в тому ж порядку величини, що підтверджує: експоненціальна тенденція залишалася надзвичайно стабільною протягом понад п'яти десятиліть напівпровідникових інновацій.Зростання кількості транзисторів між поколіннями процесорів є вражаючим. Подивіться, як далеко ми просунулися з часів 8086:Від 29 000 транзисторів у 8086 до 184 мільярдів у Apple M3 Ultra — це різниця в понад 6 мільйонів разів за 47 років. Якщо нанести ці числа на логарифмічну шкалу, точки даних розташуються надзвичайно близько до прямої лінії, що є саме ознакою експоненціального зростання.<br><img alt="Кількість транзисторів у мікропроцесорах з часом, що демонструє експоненціальне зростання" src="assets/l06-transistor-count-chart.png" target="_self"><br>Кількість транзисторів у комерційних мікропроцесорах з 1971 року до сьогодні, нанесена на логарифмічну шкалу. Тенденція подвоєння залишалася надзвичайно стабільною протягом понад 50 років. Джерело: <a data-tooltip-position="top" aria-label="https://ourworldindata.org/moores-law" rel="noopener nofollow" class="external-link is-unresolved" href="https://ourworldindata.org/moores-law" target="_self">Our World in Data</a>.Ключовою навичкою під час вивчення Закону Мура є читання логарифмічних графіків. На стандартному (лінійному) графіку стрибок від 29 000 до 184 мільярдів зробив би ранні точки даних непомітними — вони всі зібралися б унизу осі. Логарифмічна шкала стискає великі діапазони, використовуючи степені 10 як рівні інтервали: 10^3, 10^4, 10^5 і так далі отримують однаковий вертикальний простір. На такому графіку пряма лінія означає постійне відсоткове зростання — саме те, що дає подвоєння кожні два роки.Щоб оцінити, що уможливив Закон Мура, розглянемо Apple M3 Ultra, анонсований у березні 2025 року для Mac Studio. Це не теоретичний дослідний чіп — це комерційно доступний процесор, який професіонали щодня використовують для відеомонтажу, 3D-рендерингу та розробки машинного навчання. Цей єдиний чіп містить:
184 мільярди транзисторів — з'єднаних за допомогою технології Apple UltraFusion, яка об'єднує два кристали M3 Max із понад 10 000 сигнальних з'єднань та міжпроцесорною пропускною здатністю 2,5 ТБ/с
32 ядра ЦП (24 продуктивних + 8 енергоефективних) — більше обчислювальних ядер, ніж мали найпотужніші сервери ще два десятиліття тому
80 ядер GPU — для професійних графічних навантажень та трасування променів у реальному часі
32 ядра Neural Engine — спеціалізоване апаратне забезпечення для інференсу машинного навчання
До 512 ГБ уніфікованої пам'яті з пропускною здатністю понад 800 ГБ/с — достатньо для запуску великих мовних моделей із 600+ мільярдами параметрів безпосередньо на пристрої
Apple M3 Ultra виготовлений за 3-нанометровим технологічним процесом. Для розуміння масштабу: один затвор транзистора за 3 нм — це приблизно 15 атомів кремнію у ширину. Ми працюємо на масштабі, де кожен окремий атом має значення.Порівняйте це з 8086, який ми вивчали в попередніх лекціях: 29 000 транзисторів, одне ядро, без GPU, без Neural Engine, і 3-мікрометровий технологічний процес (у 1 000 разів більший за 3 нм). Кількість транзисторів M3 Ultra приблизно в 6,3 мільйона разів перевищує кількість у 8086 — і при цьому чіп поміщається в компактний настільний форм-фактор та споживає лише частку потужності на кожен транзистор.Apple M3 Ultra також ілюструє важливу сучасну тенденцію: гетерогенні обчислення. Замість використання всіх транзисторів для ядер ЦП загального призначення, великі частини площі кристала відведені під спеціалізоване обладнання — ядра GPU для графіки та паралельних обчислень, ядра Neural Engine для інференсу машинного навчання, медіадвигуни для кодування та декодування відео, а також спеціалізовані контролери для підключення Thunderbolt 5 (до 120 Гбіт/с на порт). Така спеціалізація можлива лише завдяки тому, що Закон Мура забезпечив достатню кількість транзисторів для розміщення всіх цих прискорювачів разом із ЦП на одному чіпі.<br><img alt="Фото кристала Apple M3 Ultra, що містить 184 мільярди транзисторів" src="assets/l06-apple-m3-ultra-die.jpeg" target="_self">Фото кристала Apple M3 Ultra (березень 2025): 184 мільярди транзисторів, 32 ядра ЦП, 80 ядер GPU та 32 ядра Neural Engine в одному корпусі.Закон Мура часто обговорюють ізольовано, але протягом більшої частини своєї історії він працював у тандемі з іншим ключовим спостереженням: Масштабуванням Деннарда, сформульованим Робертом Деннардом з IBM у 1974 році. Деннард зауважив, що зі зменшенням транзисторів їхнє енергоспоживання зменшувалося пропорційно — менші транзистори могли перемикатися швидше, використовуючи при цьому меншу напругу та струм. Це означало, що кожне нове покоління чіпів могло бути одночасно і швидшим, і більш енергоефективним.Масштабування Деннарда перестало працювати приблизно у 2006 році. На той момент транзистори стали настільки малими, що подальше зниження напруги спричиняло неприйнятний рівень витоку струму. Результатом стала "стіна потужності" — чіпи все ще могли вмістити більше транзисторів, але кожен транзистор більше не був дешевшим в енергоспоживанні. Саме тому індустрія перейшла від прискорення окремих ядер ("гонка гігагерців" на початку 2000-х, яка досягла піку близько 3–4 ГГц) до розміщення кількох ядер на одному чіпі. Якщо не можна зробити одне ядро швидшим без перегріву, додають більше ядер і розподіляють роботу між ними. Цей архітектурний зсув — від масштабування частоти до паралелізму — є одним із найважливіших наслідків завершення Масштабування Деннарда, і він безпосередньо визначає, як має писатися сучасне програмне забезпечення, щоб повністю використовувати переваги апаратного забезпечення.Двигуном Закону Мура є фотолітографія — процес, за допомогою якого шаблони схем проєктуються на кремнієві пластини за допомогою світла. Кожне нове покоління літографічного обладнання здатне друкувати дрібніші елементи, що дозволяє розміщувати більше транзисторів на тій самій площі.Прогресія технологічних процесів розповідає цю історію:
У 1970-х роках розміри елементів транзисторів вимірювалися в мікрометрах (мкм) — 8086 використовував технологічний процес 3 мкм
До 2000-х індустрія досягла нанометрового (нм) масштабу — Intel Pentium 4 використовував технологічний процес 90 нм
Сьогодні передові чіпи виготовляються за 3 нм технологічним процесом (TSMC, використовується Apple), а 2 нм знаходиться в розробці
Поряд з удосконаленням літографії, кілька інших досягнень роблять свій внесок:
Нові архітектури транзисторів: Перехід від планарних транзисторів до FinFET (тривимірних транзисторів з ребрами) близько 2012 року дозволив продовжити масштабування, коли традиційні планарні конструкції досягли фізичних меж. Наступний перехід — до транзисторів Gate-All-Around (GAA) на основі наноаркушів — вже відбувається для 2 нм і далі.
Передове корпусування: Технології, такі як Apple UltraFusion та TSMC CoWoS (Chip-on-Wafer-on-Substrate), дозволяють з'єднувати кілька кристалів в один корпус, ефективно збільшуючи кількість транзисторів без необхідності розміщення всіх транзисторів на одному монолітному кристалі.
EUV-літографія: EUV (екстремальний ультрафіолет) — літографія з використанням світла довжиною хвилі 13,5 нм (порівняно з 193 нм для попереднього глибокого ультрафіолету) — уможливила перехід до 7 нм і менше. EUV розроблявся десятиліттями і є однією з найскладніших виробничих технологій, коли-небудь створених.
Економіка Закону Мура є не менш важливою, ніж фізика. Зменшення транзисторів означає більше транзисторів на пластину, що знижує вартість одного транзистора — навіть якщо сама пластина дорожча. Саме цей економічний стимул змушує індустрію інвестувати десятки мільярдів доларів на рік у дослідження та розробки. Доки вартість одного транзистора продовжує падати (або принаймні цінність одного транзистора продовжує зростати завдяки спеціалізації), стимул рухатися далі залишається сильним.Питання про те, чи "закінчується" Закон Мура, обговорюється десятиліттями — і відповідь залежить від того, що саме ви вимірюєте. Кілька фізичних викликів є цілком реальними:
Квантове тунелювання: Коли затвори транзисторів зменшуються нижче 5 нм, електрони можуть тунелювати через бар'єри, які мали б бути ізолюючими. Це створює струм витоку, витрачаючи енергію та генеруючи тепло, навіть коли транзистор має бути "вимкнений".
Розсіювання тепла: Більше транзисторів на тій самій площі означає більше тепла на квадратний міліметр. Густота потужності стала основним обмеженням при проєктуванні чіпів — виклик, який іноді називають "стіною потужності".
Атомні межі: За 3 нм ми працюємо зі структурами шириною лише в десятки атомів. Існує жорстка фізична межа: не можна зробити транзистор меншим за атом.
Економічні витрати: Кожен новий технологічний процес потребує дорожчого обладнання. Будівництво сучасної напівпровідникової фабрики передового рівня тепер коштує понад 20 мільярдів доларів.
З точки зору чистого подвоєння кількості транзисторів, темп справді сповільнився. Розрив між поколіннями технологічних процесів збільшився з історичних двох років до ближчих до трьох. Проте індустрія знайшла креативні способи продовжити нарощування кількості транзисторів на чіп — багатокристальне корпусування, тривимірне укладання та архітектури на чиплетах — усе це допомагає підтримувати загальну тенденцію, навіть коли масштабування окремого транзистора стає складнішим.Варто також зазначити, що назви "технологічних процесів" стали дедалі більше маркетинговими. Чіп "3 нм" не має буквально 3-нанометрових елементів скрізь — назва відображає покоління виробничої технології, а не пряме фізичне вимірювання. Для Закону Мура важливо те, чи продовжує індустрія забезпечувати приблизно вдвічі більше транзисторів на чіп кожні два-три роки, і за цим критерієм тенденція продовжується — хоча й з більшими інженерними зусиллями та витратами на кожне покоління.Експоненціальне зростання, описане Законом Мура, — це не просто абстрактна метрика щодо густини транзисторів. Це фундаментальна сила, що рухала всю цифрову революцію. Подумайте, що уможливила ця експоненціальна траєкторія:
Персональні комп'ютери: Обчислювальна потужність, яка у 1960-х заповнювала цілу кімнату, тепер поміщається у вашій кишені. Сучасний смартфон містить більше транзисторів, ніж усі комп'ютери, що існували у світі в 1970 році, разом узяті.
Штучний інтелект: Навчання великих мовних моделей та нейронних мереж потребує величезних обчислювальних потужностей. Те, що один чіп M3 Ultra може запускати моделі з 600+ мільярдами параметрів безпосередньо на пристрої, було немислимим ще п'ять років тому — Закон Мура зробив це можливим.
Хмарні обчислення: Масивні дата-центри з мільйонами процесорів забезпечують роботу інтернет-сервісів, якими ми користуємося щодня. Кожна серверна стійка сьогодні має більше обчислювальної потужності, ніж уся обчислювальна інфраструктура більшості країн у 1990-х.
Наукові обчислення: Моделювання клімату, фолдинг білків (AlphaFold), секвенування геному та симуляції фізики елементарних частинок — усе це залежить від експоненціального зростання доступних обчислювальних потужностей.
Зниження вартості: Вартість одного транзистора впала приблизно в мільярд разів з 1960-х років. Цей економічний ефект є не менш важливим, ніж приріст продуктивності — саме тому смартфон за 300 доларів сьогодні містить більше обчислювальної потужності, ніж суперкомп'ютер за 10 мільйонів доларів з 1990-х.
Корисний уявний експеримент ілюструє масштаб експоненціального зростання: якби автомобільна промисловість просувалася з тією ж швидкістю, що й напівпровідники з 1970-х, автомобіль сьогодні рухався б приблизно зі швидкістю світла, витрачав би мільйони миль на галон пального і коштував би менше, ніж чашка кави. Жодна інша галузь в історії людства не підтримувала такий темп вдосконалення протягом такого тривалого часу.Окрім густини транзисторів, суміжні експоненціальні тенденції підсилюють ці досягнення. Обчислювальна потужність подвоювалася приблизно кожні 1,5 року з 1975 року. Обчислювальна ефективність (виміряна як кількість обчислень на одиницю енергії) зменшувала свої енергетичні потреби вдвічі кожні 1,5 року протягом шести десятиліть. Вартість зберігання даних знижувалася експоненціально з 1950-х років — тенденція, що продовжується й сьогодні з SSD та хмарним сховищем.Для студентів архітектури комп'ютерних систем Закон Мура надає необхідний контекст для розуміння того, чому процесори проєктуються саме так. 8086, який ми вивчаємо в цьому курсі, був продуктом своєї епохи — 29 000 транзисторів означали, що кожен вентиль був на вагу золота, що призвело до компактного, але іноді нерегулярного набору інструкцій, який ми досліджували в попередньому розділі. Сегментована модель пам'яті, обмежена кількість регістрів, опора на CISC (Complex Instruction Set Computing) — усе це було сформовано транзисторними бюджетами, доступними у 1978 році.Зі зростанням транзисторних бюджетів проєктувальники процесорів могли дозволити собі принципово нові підходи: конвеєрну обробку, позачергове виконання, передбачення розгалужень, багаторівневий кеш і, зрештою, кілька ядер на одному кристалі. Кожна з цих інновацій є прямим наслідком наявності більшої кількості транзисторів. Тому розуміння Закону Мура допомагає зрозуміти не лише те, як виглядають сучасні процесори, а й чому вони еволюціонували з архітектур, які ми вивчаємо в цьому курсі.Закон Мура, незалежно від того, чи продовжується він у своїй первісній формі, чи трансформується в нові метрики, сформував світ, у якому ми живемо. Його розуміння є необхідним контекстом для кожного, хто вивчає архітектуру комп'ютерних систем, тому що він пояснює не лише те, де були процесори, а й інженерний тиск та інновації, які визначатимуть, куди вони рухатимуться далі.У попередніх лекціях ми вивчали фундаментальні будівельні блоки цифрових систем: логічні вентилі, тригери, регістри та арифметичні схеми. Але як ці компоненти поєднуються в реальному апаратному забезпеченні? Відповідь криється в інтегральних схемах (ІС) — технології, яка дозволила розмістити спочатку тисячі, потім мільйони, а згодом мільярди транзисторів на одному чіпі.Цей розділ простежує шлях від перших ІС до мікропроцесорів, що живлять сучасні комп'ютери, показуючи, як дедалі більша інтеграція привела від простих логічних мікросхем до повноцінних комп'ютерів на одному шматочку кремнію.<br>Інтегральна схема (ІС), яку також називають чіпом або мікросхемою, — це набір електронних схем на одному невеликому шматочку напівпровідникового матеріалу, зазвичай <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Silicon" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Silicon" target="_self">кремнію</a>. Принцип створення ІС був розроблений у 1958 році двома американськими винахідниками, які працювали незалежно: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Jack_Kilby" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Jack_Kilby" target="_self">Джеком Кілбі</a> у Texas Instruments (який використовував германій) та <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Robert_Noyce" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Robert_Noyce" target="_self">Робертом Нойсом</a> у Fairchild Semiconductor (який використовував кремній). Обидва намагалися вирішити одну й ту саму проблему: як розмістити максимальну кількість компонентів у мінімальному обсязі простору.У 1959 році обидва отримали патенти на свої винаходи, що призвело до суперництва між двома компаніями, яке зрештою завершилося угодою про перехресне ліцензування. До 1961 року Fairchild Semiconductor почала комерційно продавати ІС, і вони були негайно прийняті для використання в калькуляторах та комп'ютерах, замінивши дискретні транзистори та різко зменшивши розмір пристроїв, одночасно збільшивши продуктивність.Чому ІС важливі:
<br>Вартість: ІС виготовляються за допомогою <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Photolithography" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Photolithography" target="_self">фотолітографії</a> — процесу, який фактично "друкує" схеми замість того, щоб збирати їх з окремих компонентів. Це робить масове виробництво надзвичайно дешевим.
Швидкість: Компоненти на ІС дуже малі й розташовані близько один до одного, тому електричні сигнали проходять дуже короткі відстані, що забезпечує значно швидше перемикання.
Енергоефективність: Малі транзистори споживають менше енергії та виділяють менше тепла, ніж дискретні компоненти.
Надійність: Менша кількість зовнішніх з'єднань означає менше точок відмови.
Головним недоліком ІС є висока початкова вартість проєктування та виготовлення фотошаблонів. Це робить ІС комерційно вигідними лише при достатньо великих обсягах виробництва.Рівні інтеграції:З розвитком технології виробництва інженери змогли розмістити дедалі більше транзисторів на одному чіпі. Індустрія класифікує ІС за рівнем інтеграції:<br>Сучасні процесори містять мільярди транзисторів на чіпі розміром з ніготь. Цей надзвичайний прогрес описується <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Moore%27s_law" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Moore%27s_law" target="_self">Законом Мура</a> — спостереженням, що кількість транзисторів на чіпі приблизно подвоюється кожні два роки.Технологічні процеси виробництва: "Розмір" транзисторів на чіпі часто описується технологічним процесом. У 1970-х роках мінімальний розмір елемента становив 2-8 мікрометрів (мкм). До 1990-х процесори Intel Pentium вироблялися за технологією 500-600 нм. На початку 2000-х з'явилися процеси 180 нм та 90 нм, а до 2010-х індустрія досягла 14 нм і 10 нм. Станом на 2024 рік найсучасніші чіпи від Apple, AMD та інших використовують 3 нм технологічний процес. Кожне зменшення дозволяє розмістити більше транзисторів на чіпі, підвищити продуктивність та знизити енергоспоживання.Приклад — IC 7408: Проста ІС рівня SSI, яка містить чотири вентилі AND на одному чіпі. Кожен вентиль має два входи та один вихід, плюс спільні контакти живлення та землі. Це саме той тип вентилів, який ми вивчали на Лекціях 1 та 2 — але тепер упакований як ІС, а не зібраний з дискретних транзисторів.<br><img alt="ІС 7408: DIP-корпус та внутрішня схема, що показує чотири вентилі AND" src="assets/l06-ic-7408-and-gate.png" target="_self">Інтегральна схема 7408: 14-контактний DIP-корпус (ліворуч), що містить чотири незалежні 2-входові вентилі AND (праворуч). Це той самий вентиль AND, який ми вивчали на Лекції 1, тепер упакований як ІС.<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/7400-series_integrated_circuits" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/7400-series_integrated_circuits" target="_self">Серія 7400</a> — одне з найважливіших сімейств продуктів в історії цифрової електроніки. Це сімейство інтегральних схем на основі <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Transistor%E2%80%93transistor_logic" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Transistor%E2%80%93transistor_logic" target="_self">транзисторно-транзисторної логіки</a> (TTL), яке стало де-факто стандартом для побудови цифрових систем.Історія:
У 1964 році Texas Instruments представила серію SN5400 у керамічних корпусах військового класу.
У 1966 році була випущена дешевша серія SN7400 у пластикових корпусах для комерційного застосування.
Серія 7400 швидко захопила понад 50% ринку логічних мікросхем і на десятиліття стала стандартним будівельним блоком цифрової електроніки.
<br>Перша мікросхема серії, 7400, містить чотири двовходові вентилі NAND у 14-контактному <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Dual_in-line_package" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Dual_in-line_package" target="_self">DIP-корпусі</a>. Кожен вентиль використовує два вхідні контакти та один вихідний, а решта два контакти призначені для живлення (+5 В) та землі.Що містить серія 7400?Серія включає сотні різних пристроїв, що забезпечують:
Базові логічні вентилі (AND, OR, NAND, NOR, XOR, NOT)
Тригери та засувки
Лічильники та зсувні регістри
Мультиплексори та демультиплексори
Кодери та декодери
<br>Арифметико-логічні пристрої (АЛП), такі як <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/74181" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/74181" target="_self">74181</a>
Шинні приймачі-передавачі та спеціалізовані інтерфейсні мікросхеми
Це саме ті будівельні блоки, які ми вивчали на Лекціях з 1 по 5 — вентилі, тригери, регістри, лічильники, АЛП — але упаковані як стандартизовані ІС, які інженери могли комбінувати на друкованих платах для побудови повноцінних комп'ютерів.Підродини:Протягом десятиліть було представлено багато сумісних за контактами підродин з різними компромісами між швидкістю та енергоспоживанням:Оригінальні TTL-мікросхеми працювали при напрузі 5 В і могли перемикатися приблизно за 10-22 наносекунди. Пізніші КМОН (CMOS)-версії пропонували ті самі логічні функції зі значно нижчим енергоспоживанням та ширшим діапазоном робочих напруг, що зробило їх придатними для пристроїв з батарейним живленням.Серії 5400 та 7400 використовувалися в багатьох популярних мінікомп'ютерах 1970-х та початку 1980-х років. Наприклад, деякі моделі серії DEC PDP використовували мікросхему АЛП 74181 як основний обчислювальний елемент свого ЦП. Оригінальна мікросхема 7400 з вентилем NAND коштувала близько $22 у 1965 році (військовий клас); до 2007 року мікросхеми комерційного класу можна було придбати приблизно за $0,25 за штуку. Сьогодні КМОН-версії серії 7400 для поверхневого монтажу все ще використовуються в різних застосуваннях, а версії з наскрізним монтажем у DIP-корпусах залишаються популярними для макетування на breadboard-платах та в навчанні.Мікроконтролер (MCU) — це невеликий комп'ютер на одній інтегральній схемі. На відміну від простої ІС, яка реалізує кілька логічних вентилів, мікроконтролер містить:
Одне або кілька процесорних ядер
Пам'ять: як ПЗП (постійна пам'ять, для зберігання програм), так і ОП (оперативна пам'ять, для даних)
Програмовані периферійні пристрої вводу/виводу: таймери, послідовні інтерфейси, аналого-цифрові перетворювачі тощо
<br>У сучасній термінології мікроконтролер подібний до <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/System_on_a_chip" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/System_on_a_chip" target="_self">системи на кристалі</a> (SoC), хоча SoC зазвичай інтегрують більш складні периферійні пристрої, такі як ядра GPU, модулі Wi-Fi або співпроцесори.Мікроконтролери — скрізь. Типовий будинок у розвиненій країні має лише близько чотирьох мікропроцесорів загального призначення (у ПК та ноутбуках), але приблизно три десятки мікроконтролерів — у пральній машині, мікрохвильовій печі, пультах дистанційного керування, датчиках диму тощо. Типовий автомобіль середнього класу містить близько 30 мікроконтролерів, які керують усім — від системи управління двигуном до електричних склопідіймачів.Ключові характеристики:
Призначені для вбудованих застосувань — вони вбудовуються в інші пристрої та керують ними
Містять усі необхідні компоненти на одному чіпі — не потрібні зовнішні мікросхеми пам'яті або вводу/виводу
Оптимізовані для низької вартості та низького енергоспоживання
Програмуються одноразово (або через флеш-пам'ять) для виконання конкретного завдання
Приклад — TMS1000 (Texas Instruments, 1974):<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/TMS1000" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/TMS1000" target="_self">TMS1000</a> займає особливе місце в історії обчислювальної техніки як перший комерційний мікроконтролер загального призначення. Це був справжній "комп'ютер на чіпі":
<br>4-бітний ЦП з <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Harvard_architecture" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Harvard_architecture" target="_self">Гарвардською архітектурою</a> (окрема пам'ять для програм та даних)
1 024 x 8 біт ПЗП для зберігання програм (масково-програмований на заводі)
64 x 4 біти ОП для даних
23 лінії вводу/виводу для взаємодії із зовнішнім світом
Тактова частота приблизно 0,3 МГц
Доступний у 28-контактному DIP-корпусі
Ціна приблизно $2 за чіп при масовому виробництві (1974)
До 1979 року Texas Instruments продавала приблизно 26 мільйонів мікросхем сімейства TMS1000 на рік. Чіп використовувався в культових продуктах, зокрема у навчальній іграшці Speak &amp; Spell від Texas Instruments, програмованому транспортному засобі Big Trak та електронній грі Simon.TMS1000 використовував масково-програмовану ПЗП: коли замовник мав налагоджену програму, він надсилав її в Texas Instruments, де створювали спеціальний фотошаблон для запису програми у вбудовану ПЗП під час виробництва. Хоча початкова вартість налаштування була високою, вартість виробництва одиниці продукції була надзвичайно низькою — ідеально для масових споживчих продуктів.<br><img alt="Мікросхема Texas Instruments TMS1000 у DIP-корпусі" src="assets/l06-tms1000-chip.jpeg" target="_self">Texas Instruments TMS1000 (1974): перший комерційний мікроконтролер. Повноцінний 4-бітний комп'ютер — ЦП, ПЗП, ОП та периферія вводу/виводу — в одному 28-контактному DIP-корпусі.Мікроконтролери сьогодні:<br>Ринок мікроконтролерів значно зріс з часів TMS1000. У 2002 році близько 55% усіх процесорів, проданих у світі, були 8-бітними мікроконтролерами. Сучасні мікроконтролери варіюються від простих 8-бітних пристроїв вартістю менше $0,03 до потужних 32-бітних процесорів ARM Cortex-M із вбудованими блоками обчислень з рухомою комою, Wi-Fi, Bluetooth та розширеними периферійними пристроями — все на одному чіпі за кілька доларів. Популярні платформи для розробки <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Arduino" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Arduino" target="_self">Arduino</a> та <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/ESP32" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/ESP32" target="_self">ESP32</a> є сучасними прикладами платформ на базі мікроконтролерів, що широко використовуються в навчанні та застосуваннях Інтернету речей (IoT).<br>Мікропроцесор — це процесор комп'ютера, в якому логіка обробки даних та схеми керування інтегровані в одну інтегральну схему (або невелику кількість ІС). Він містить арифметичні, логічні та керуючі схеми, необхідні для виконання функцій <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Central_processing_unit" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Central_processing_unit" target="_self">центрального процесора</a> (ЦП) комп'ютера.Ключові властивості мікропроцесора:
Багатоцільовий: може виконувати будь-яку програму, збережену в пам'яті
Тактований: операції синхронізуються зовнішнім тактовим сигналом
Регістровий: використовує внутрішні регістри для швидкого зберігання та обробки даних
Приймає двійкові дані на вхід, обробляє їх відповідно до інструкцій (також двійкових) та видає двійкові результати на вихід
Містить як комбінаційну логіку (для обчислень), так і послідовну логіку (для стану та керування)
Ключова відмінність: мікроконтролер vs. мікропроцесорЦе одна з найважливіших відмінностей в архітектурі комп'ютерів:
Мікроконтролер — це повноцінний комп'ютер на чіпі — він включає ЦП, пам'ять (ПЗП + ОП) та периферійні пристрої вводу/виводу, все інтегровано в одній ІС. Він призначений для вбудованих застосувань, зазвичай одноцільових.
Мікропроцесор — це лише ЦП — йому потрібні зовнішні мікросхеми пам'яті, контролери вводу/виводу та інші допоміжні схеми для формування повноцінної комп'ютерної системи. Він призначений для обчислень загального призначення.
Можна уявити це так: мікроконтролер — це як швейцарський ніж (все в одному корпусі), тоді як мікропроцесор — це як ніж шеф-кухаря — спеціалізований і потужний, але йому потрібна решта кухні, щоб бути корисним.Коротка історія мікропроцесорів:<br>Перший комерційно доступний мікропроцесор, Intel 4004 (1971), був розроблений командою під керівництвом <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Federico_Faggin" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Federico_Faggin" target="_self">Федеріко Фаджина</a> з використанням його технології МОН (метал-оксид-напівпровідник) із кремнієвим затвором, разом з Маршіаном Хоффом, Стенлі Мейзором та Масатосі Сімою. Спочатку він був створений для Busicom, японського виробника калькуляторів, який попросив Intel побудувати чипсет для високопродуктивних настільних калькуляторів. Хофф запропонував спростити оригінальний семимікросхемний дизайн Busicom до чотиримікросхемної архітектури з процесором загального призначення, а Фаджин втілив одночіповий ЦП у реальність. Intel усвідомила його потенціал загального призначення і почала широко його просувати.До появи мікропроцесорів невеликі комп'ютери будувалися з використанням стійок із друкованими платами, що містили безліч ІС середнього та малого рівнів інтеграції — зазвичай із серії TTL 7400, яку ми розглядали раніше. Наприклад, мінікомп'ютери DEC PDP використовували мікросхему АЛП 74181 як основний обчислювальний елемент. Мікропроцесори об'єднали всю цю функціональність ЦП в одній або кількох ІС великого рівня інтеграції, різко знизивши вартість, розмір та енергоспоживання, одночасно підвищивши надійність.<br>Інтеграція цілого ЦП на одному чіпі за допомогою технології <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Very_Large_Scale_Integration" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Very_Large_Scale_Integration" target="_self">VLSI</a> (надвеликий рівень інтеграції) була трансформаційною. Оскільки процесори на інтегральних схемах виробляються великими партіями за допомогою високоавтоматизованих процесів MOS-виробництва, собівартість одиниці продукції відносно низька. І в міру вдосконалення конструкцій чіпів вартість виготовлення чіпа того самого фізичного розміру загалом залишається незмінною (<a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Rock%27s_law" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Rock%27s_law" target="_self">закон Рока</a>), навіть якщо він отримує більше транзисторів та можливостей — доброчесний цикл, який рухав шість десятиліть експоненційного прогресу в обчислювальній техніці.Порівняння: ІС vs. мікроконтролер vs. мікропроцесорТепер, коли ми розуміємо, як транзистори інтегруються в чіпи, і як чіпи еволюціонували від простих логічних вентилів до повноцінних процесорів, ми можемо розглянути основні компоненти, з яких складається сучасна комп'ютерна система. Цей розділ надає короткий огляд; наступні розділи розглянуть кожен компонент детальніше.Комп'ютер на найфундаментальнішому рівні складається з чотирьох основних компонентів:ЦП — це "мозок" комп'ютера. Це компонент, відповідальний за:
Вибірку інструкцій з пам'яті
Декодування цих інструкцій для визначення операції, яку потрібно виконати
Виконання операцій (арифметичних, логічних, переміщення даних, керування потоком виконання)
Сучасні ЦП є мікропроцесорами — одночіповими реалізаціями архітектури процесора. Вони містять АЛП, блок керування, регістри та кеш-пам'ять, які ми вивчали впродовж цього курсу. Ми розглянемо архітектуру ЦП значно детальніше на наступних лекціях.Материнська плата — це основа комп'ютера. Це головна друкована плата (PCB), яка:
Забезпечує фізичні та електричні з'єднання між усіма іншими компонентами
Містить роз'єм для ЦП, слоти пам'яті, слоти розширення та роз'єми вводу/виводу
Містить чипсет — набір ІС, який керує потоком даних між ЦП, пам'яттю та периферійними пристроями
Розподіляє живлення між усіма компонентами
Містить тактовий генератор, який синхронізує всі операції
Материнська плата — це, по суті, "нервова система" комп'ютера, яка забезпечує передачу даних між ЦП, пам'яттю та всіма пристроями вводу/виводу. Ми розглянемо материнську плату та її шинну архітектуру в Розділі 7, а систему тактування — в Розділі 8.Пам'ять зберігає як дані, що обробляються, так і інструкції (програми), які вказують ЦП, що робити. Комп'ютерна пам'ять організована в ієрархію на основі швидкості, вартості та ємності:
Регістри (всередині ЦП): найшвидші, найменші, найдорожчі за біт
Кеш-пам'ять (на ЦП або поблизу нього): дуже швидка, невеликого обсягу
Основна пам'ять (ОП): швидка, помірної ємності — тут перебувають запущені програми та їхні дані
Вторинне сховище (SSD, HDD): повільніше, великої ємності, енергонезалежне — для постійного зберігання даних
Ключова відмінність полягає між енергозалежною пам'яттю (як ОП, яка втрачає вміст при вимкненні живлення) та енергонезалежною пам'яттю (як флеш-пам'ять, яка зберігає дані без живлення). Ми детально розглянемо технологію пам'яті та її ієрархію в Розділі 9.Пристрої вводу/виводу — це інтерфейс між комп'ютером та зовнішнім світом. Вони дозволяють комп'ютеру:
Отримувати дані (ввід): клавіатура, миша, мікрофон, камера, датчики, мережевий інтерфейс
Надсилати дані (вивід): дисплей, динаміки, принтер, мережевий інтерфейс, актуатори
Зберігати дані (ввід/вивід сховища): SSD, HDD, USB-накопичувачі, оптичні приводи
Деякі пристрої виконують подвійну роль — мережева карта як надсилає, так і отримує дані, а сенсорний екран є одночасно пристроєм вводу та виводу.Пристрої вводу/виводу взаємодіють із ЦП через контролери вводу/виводу, підключені до шинної системи материнської плати. Ефективне керування вводом/виводом — одна з ключових проблем архітектури комп'ютерів, оскільки пристрої вводу/виводу працюють на дуже різних швидкостях — від клавіатури (кілька байтів на секунду) до сучасного NVMe SSD (гігабайти на секунду).Ці чотири компоненти — ЦП, материнська плата, пам'ять та пристрої вводу/виводу — формують основу кожної комп'ютерної системи, від найменшого вбудованого мікроконтролера до найбільшого суперкомп'ютера. Різниця полягає в масштабі та складності:У наступних розділах ми детально розглянемо материнську плату (Розділ 7), систему тактування (Розділ 8) та ієрархію пам'яті (Розділ 9).У попередніх розділах ми розглянули, як обчислювальне обладнання еволюціонувало від простих інтегральних схем через мікроконтролери до повноцінних мікропроцесорів. Але мікропроцесор сам по собі — це лише чіп: йому потрібна платформа, що з'єднує його з пам'яттю, накопичувачами, пристроями введення/виведення та джерелом живлення. Ця платформа — материнська плата.<br>Материнська плата (також відома як системна плата або логічна плата) — це основна <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Printed_circuit_board" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Printed_circuit_board" target="_self">друкована плата</a> (PCB) у комп'ютері загального призначення. Вона є основою для передачі даних між усіма основними компонентами системи. Материнська плата утримує та забезпечує електричні з'єднання між процесором (ЦП), пам'яттю, контролерами накопичувачів, слотами розширення та роз'ємами периферії.Типова сучасна материнська плата містить:
Роз'єм процесора (сокет) — механічний та електричний інтерфейс для встановлення процесора. Тип сокета повинен відповідати сімейству ЦП (наприклад, Intel LGA 1700 для процесорів Core 12-го/13-го/14-го покоління, AMD AM5 для серії Ryzen 7000).
Слоти пам'яті — роз'єми для модулів динамічної пам'яті (DRAM) (зазвичай DDR4 або DDR5 DIMM). Настільні материнські плати зазвичай мають 2 або 4 слоти пам'яті.
Чипсет — одна або дві інтегральні схеми (ІС), що формують інтерфейс між ЦП, основною пам'яттю та периферійними шинами. Чипсет визначає, які функції та можливості введення/виведення підтримує материнська плата.
Слоти розширення — слоти PCIe (Peripheral Component Interconnect Express) для відеокарт, мережевих адаптерів, контролерів накопичувачів та інших додаткових плат.
Роз'єми накопичувачів — порти SATA та слоти M.2 для твердотільних накопичувачів (SSD) та жорстких дисків (HDD).
Роз'єми живлення — отримують живлення від блока живлення (PSU) та розподіляють його між ЦП, пам'яттю та іншими компонентами.
Тактовий генератор — виробляє тактовий сигнал системного годинника, що синхронізує всі компоненти (детальніше про це в Розділі 8).
Сховище мікропрограми — енергонезалежна пам'ять (флеш-ПЗП), що містить мікропрограму BIOS або UEFI, яка ініціалізує обладнання під час завантаження.
Інтегрована периферія — сучасні материнські плати інтегрують контролери USB, аудіокодеки, контролери Ethernet, модулі Wi-Fi та іншу периферію безпосередньо на платі.
Материнська плата — це місце, де всі компоненти об'єднуються. Дані між компонентами передаються через комунікаційні канали, що називаються шинами — електричні доріжки, витравлені в мідних шарах друкованої плати. Найважливіша з них — системна шина.<br><img alt="Схематична компоновка материнської плати з процесором, оперативною пам'яттю, слотами розширення та портами" src="assets/l06-motherboard-layout.png" target="_self">Схематична компоновка типової материнської плати: сокет процесора, слоти оперативної пам'яті, слоти розширення (PCI), порти введення/виведення та роз'єми SATA для накопичувачів.Системна шина — це основний комунікаційний канал, через який процесор обмінюється даними з пам'яттю та іншими пристроями. Концептуально це набір паралельних електричних ліній, що передають різні типи сигналів. Системна шина поділяється на три логічні підшини, кожна з яких виконує окрему роль:1. Шина данихШина даних передає власне дані — біти, що представляють інструкції, операнди або результати. Розрядність шини даних визначає, скільки бітів можна передати за одну операцію:
Intel 8086 має 16-бітну шину даних, тобто передає 16 бітів (2 байти) за раз.
Intel 80386 запровадив 32-бітну шину даних (4 байти за раз).
Сучасні процесори x86-64 використовують 64-бітну шину даних (8 байтів за раз).
Ширша шина даних означає вищу пропускну здатність — більше даних за один цикл передачі. Шина даних є двонапрямленою: дані можуть передаватися від ЦП до пам'яті (запис) або від пам'яті до ЦП (читання).2. Адресна шинаАдресна шина передає адресу пам'яті, що вказує, звідки потрібно прочитати або куди записати дані. ЦП розміщує адресу на адресній шині, а підсистема пам'яті використовує цю адресу для вибору потрібної комірки пам'яті. Розрядність адресної шини визначає максимальний обсяг адресованої пам'яті:
8086 має 20-бітну адресну шину: 2^20 = 1 048 576 байтів = 1 MB адресованої пам'яті.
80386 має 32-бітну адресну шину: 2^32 = 4 294 967 296 байтів = 4 GB.
Сучасні процесори x86-64 зазвичай реалізують 48-бітну фізичну адресацію: 2^48 = 256 TB (хоча більшість систем використовують значно менше).
Адресна шина є однонапрямленою в простих системах — вона передає адреси від ЦП до пам'яті. У складніших системах із захопленням шини або відстеженням кешу вона може бути двонапрямленою.3. Шина керуванняШина керування передає сигнали керування та стану, що координують роботу всіх компонентів. Ці сигнали визначають напрямок і синхронізацію потоку даних. Основні сигнали керування включають:
Читання/Запис — вказує, чи є поточна операція читанням з пам'яті чи записом у пам'ять.
Тактовий сигнал — сигнал системного годинника, що синхронізує всі операції на шині.
Запит переривання (IRQ) — сигнали від пристроїв введення/виведення, що запитують увагу ЦП.
Запит шини / Надання шини — сигнали для арбітражу шини, коли кілька пристроїв потребують доступу до шини.
Готовність пам'яті — сигнал від пам'яті, що вказує на доступність даних на шині даних.
Шина керування складається з окремих сигнальних ліній, кожна з яких передає конкретний сигнал керування. На відміну від шини даних та адресної шини, шина керування — це набір окремих, індивідуальних ліній, а не одне багатобітове значення.<br><img alt="Діаграма системної шини: шина даних, адресна шина та шина керування, що з'єднують ЦП, оперативну пам'ять та пристрої введення/виведення" src="assets/l06-system-bus-diagram.png" target="_self">Три складові системної шини: шина даних передає інформацію, адресна шина вказує куди, а шина керування координує синхронізацію та напрямок передачі.Розуміння системної шини є важливим, оскільки воно пояснює багато реальних обмежень. Наприклад, причина, чому 8086 може адресувати лише 1 MB пам'яті, полягає не в обмеженнях внутрішніх можливостей ЦП — це визначається 20 фізичними адресними лініями на шині. Так само причина, чому 32-бітні операційні системи не можуть використовувати більше 4 GB оперативної пам'яті, безпосередньо пов'язана з 32-бітною адресною шиною архітектури 80386.
Зв'язок з Л03: Згадайте, що в Лекції 3 ми дізналися про адресацію сегмент:зміщення, де 16-бітний сегментний регістр та 16-бітне зміщення комбінуються для отримання 20-бітної фізичної адреси. Тепер ми бачимо, чому ця схема існує: внутрішні регістри ЦП мають розрядність 16 біт, але адресна шина потребує 20-бітних адрес. Механізм сегмент:зміщення долає цю розбіжність.
Материнська плата забезпечує фізичну платформу, а системна шина — комунікаційні канали, але залишається ще одне важливе питання: як усі ці компоненти залишаються синхронізованими? Відповідь — системний годинник (тактовий генератор).Системний годинник — це невеликий кварцовий генератор на материнській платі, що виробляє безперервний потік електричних імпульсів із точною частотою. Уявіть його як метроном для всієї комп'ютерної системи — він задає стійкий ритм, якому слідують усі компоненти.Без тактового генератора компоненти не мали б способу координувати свої дії. ЦП не знав би, коли дані на шині дійсні. Пам'ять не знала б, коли відповідати. Тактовий генератор вирішує це, встановлюючи дискретні тактові цикли — однакові проміжки часу, протягом яких може виконуватися одна одиниця роботи.<br><img alt="Тактовий сигнал: прямокутна хвиля, що показує один тактовий період" src="assets/l06-clock-signal.gif" target="_self">Тактовий сигнал — це прямокутна хвиля, що чергується між 0 та 1. Один повний цикл (високий + низький рівень) — це один тактовий період. При частоті 3,4 GHz кожний період становить приблизно 0,29 наносекунди.Швидкість ЦП виражається як частота — кількість тактових циклів на секунду, що вимірюється в Герцах (Hz):Один тактовий цикл — це фундаментальна одиниця часу всередині комп'ютера. Кожна інструкція потребує одного або більше тактових циклів для виконання. Прості операції, як-от MOV між регістрами, можуть завершитися за 1 цикл, тоді як складні операції, як-от ціле ділення, можуть зайняти десятки циклів.Спокуса прирівняти тактову частоту до продуктивності — "ЦП на 4 GHz швидший за ЦП на 3 GHz" — є надмірним спрощенням. Продуктивність залежить від багатьох факторів:
Кількість інструкцій за цикл (IPC): ЦП, що виконує 2 інструкції за цикл на 3 GHz, перевершує ЦП, що виконує 1 інструкцію за цикл на 4 GHz.
Архітектура та глибина конвеєра: Сучасні ЦП використовують конвеєрну обробку, позачергове виконання та передбачення розгалужень, щоб виконувати більше роботи за цикл.
Розмір та ієрархія кеш-пам'яті: Швидка кеш-пам'ять (про яку йдеться в Розділі 9) може кардинально зменшити час очікування ЦП на дані.
Пропускна здатність пам'яті: Навіть найшвидший ЦП простоює, якщо не може отримати дані з пам'яті достатньо швидко.
Саме тому сучасні покоління ЦП часто забезпечують значне покращення продуктивності на тій самій або навіть нижчій тактовій частоті, ніж їхні попередники.Розгін — це практика роботи тактового генератора на частоті, вищій за номінальну специфікацію виробника. Наприклад, ЦП з номінальною частотою 3,6 GHz може бути розігнаний до 4,2 GHz. Це збільшує кількість операцій на секунду, але має компроміси:
Збільшене виділення тепла: швидше перемикання означає більше енергоспоживання та тепловиділення.
Потенційна нестабільність: компоненти можуть не встигати надійно завершувати операції протягом скороченого тактового циклу, що призводить до помилок або збоїв.
Зменшений ресурс компонентів: тривале підвищення температури та напруги може з часом деградувати обладнання.
Розгін поширений серед ентузіастів і підтримується багатьма сучасними материнськими платами та ЦП (зокрема серією "K" від Intel та процесорами AMD Ryzen з розблокованими множниками).Ми зібрали основні компоненти комп'ютерної системи: ЦП виконує інструкції, материнська плата забезпечує фізичну платформу, системна шина уможливлює комунікацію, а тактовий генератор синхронізує все. Але залишається критичне питання, на яке ми ще не відповіли: де зберігаються дані?Кожна операція, яку виконує ЦП, пов'язана з даними — як інструкціями, що він виконує, так і операндами, які обробляє. Ці дані повинні зберігатися десь у доступному місці. Але технології зберігання стикаються з фундаментальним інженерним компромісом:
Швидка пам'ять є дорогою та малою (наприклад, регістри ЦП: кілька десятків байтів, але доступ менше ніж за 1 наносекунду).
Велика пам'ять є повільною та дешевою (наприклад, жорсткі диски: терабайти сховища, але час доступу вимірюється в мілісекундах).
Жодна окрема технологія зберігання не забезпечує одночасно і швидкість, і обсяг, необхідні сучасним комп'ютерам. Рішення — ієрархія пам'яті: багаторівнева система, де кожний рівень пропонує різний баланс швидкості, вартості та обсягу. Дані, які ЦП потребує найтерміновіше, зберігаються в найшвидшій (і найменшій) пам'яті, тоді як рідше використовувані дані перебувають у повільнішому (але більшому) сховищі.Комп'ютерне сховище можна загалом поділити на три рівні:Статична пам'ять (SRAM)<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Static_random-access_memory" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Static_random-access_memory" target="_self">SRAM</a> — це найшвидший тип енергозалежної пам'яті. Вона використовується для регістрів ЦП та кеш-пам'яті (L1, L2, L3). Основні характеристики:
Дуже швидкий час доступу (менше наносекунди для регістрів, 1-10 ns для кешу).
Не потребує періодичного оновлення — дані утримуються бістабільною схемою на тригерах (звідси "статична").
Дорога у виробництві та фізично велика на біт (кожна комірка SRAM використовує 6 транзисторів порівняно з 1 транзистором + 1 конденсатором для DRAM).
Використовується в малих обсягах там, де критична швидкість.
Динамічна пам'ять (DRAM)<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Dynamic_random-access_memory" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Dynamic_random-access_memory" target="_self">DRAM</a> — це те, що люди зазвичай мають на увазі, кажучи "комп'ютерна пам'ять" або "оперативна пам'ять (ОП)". Це основна робоча пам'ять системи:
Повільніша за SRAM, але значно дешевша та щільніша (більше бітів на одиницю площі чіпа).
Зберігає кожний біт як заряд на мініатюрному конденсаторі. Оскільки конденсатори втрачають заряд, DRAM має періодично оновлюватися (перечитуватися та перезаписуватися) — звідси "динамічна".
Типові сучасні системи мають 8-64 GB динамічної пам'яті.
Затримка доступу: приблизно 50-100 наносекунд (що відповідає 100-200 тактовим циклам на сучасному ЦП — значна затримка).
Дискове сховищеДискове сховище рідко називають "пам'яттю" в технічному контексті — його категоризують як сховище або накопичувачі:
Жорсткий диск (HDD): Зберігає дані на магнітних дисках, що обертаються. Дуже великий обсяг (до 20+ TB), але повільний доступ (час пошуку 5-10 ms). Механічна природа HDD робить їх на порядки повільнішими за електронну пам'ять.
Твердотільний накопичувач (SSD): Використовує флеш-пам'ять NAND без рухомих частин. Значно швидший за HDD (доступ 0,05-0,1 ms), але все ще набагато повільніший за DRAM.
NVMe SSD: Твердотільні накопичувачі, підключені через шину PCIe з використанням протоколу NVMe, що забезпечує ще вищу пропускну здатність та нижчу затримку, ніж SATA SSD.
<br><img alt="Піраміда ієрархії пам'яті від регістрів до дискового накопичувача" src="assets/l06-memory-hierarchy-pyramid.png" target="_self">Ієрархія пам'яті: кожний рівень жертвує швидкістю заради обсягу та вартості. Регістри на вершині — найшвидші, але найменші; дискові накопичувачі внизу — найбільші, але найповільніші.Щоб зрозуміти, чому доступ до пам'яті є основним вузьким місцем продуктивності сучасних комп'ютерів, розглянемо, що відбувається, коли ЦП потребує прочитати значення з DRAM. Процес виконується за 4-кроковим протоколом:Крок 1: Розміщення адреси на адресній шиніЦП розміщує цільову адресу пам'яті на адресній шині. Для 8086 це 20-бітна адреса; для сучасних ЦП — 48-бітна або ширша адреса.Крок 2: Встановлення сигналу READЦП встановлює (встановлює в 1) сигнал читання на шині керування. Це повідомляє підсистемі пам'яті, що ЦП хоче прочитати дані за вказаною адресою, а не записати їх.Крок 3: Очікування відповіді пам'ятіЦе повільна частина. Контролер пам'яті отримує адресу, знаходить потрібний рядок і стовпець у мікросхемі DRAM, зчитує заряд з цільового конденсатора (конденсаторів), підсилює сигнал і розміщує дані на шині даних. Цей процес займає приблизно 100-200 тактових циклів на сучасному ЦП.Протягом цього часу ЦП очікує — він не може продовжити виконання інструкції, яка запросила дані. Цей час очікування називається затримкою пам'яті, і це найбільш значуще вузьке місце в сучасних обчисленнях.Крок 4: Копіювання даних до місця призначенняКоли пам'ять розмістила запитані дані на шині даних і подала сигнал "готово" на шині керування, ЦП фіксує (копіює) дані з шини даних у цільовий регістр або внутрішній буфер.Цей 4-кроковий протокол розкриває критичне розуміння: хоча сучасні ЦП можуть виконувати мільярди операцій на секунду, вони часто простоюють, очікуючи на дані з DRAM. Якщо ЦП працює на 4 GHz (0,25 ns на цикл), а доступ до DRAM займає 50 ns (200 циклів), ЦП простоює 200 циклів щоразу, коли йому потрібні дані з основної пам'яті. Це величезна втрата обчислювального потенціалу.<br><img alt="Часова діаграма читання DRAM із сигналами Clock, Address, MemRead, MemWait та DataIn" src="assets/l06-dram-read-timing.jpeg" target="_self">Часова діаграма читання DRAM: ЦП розміщує адресу та встановлює MemRead, потім очікує, поки контролер пам'яті подасть сигнал готовності даних. Період очікування може тривати сотні тактових циклів.Рішення цієї проблеми — кеш-пам'ять.Кеш-пам'ять — це невеликий, надзвичайно швидкий буфер на основі SRAM, розміщений між ЦП та основною пам'яттю (DRAM). Його призначення — зберігати копії часто використовуваних даних, щоб ЦП міг отримати їх без очікування повільного доступу до DRAM.Сучасні ЦП організовують кеш у багаторівневу ієрархію. Кожний рівень більший, але дещо повільніший за попередній. Типовий сучасний процесор, такий як Intel Core i7, має:Розділення на L1i (кеш інструкцій) та L1d (кеш даних) — це апаратна оптимізація: оскільки ЦП вибирає інструкції та дані одночасно, але через різні внутрішні шляхи, окремі кеші для кожного типу уникають конфліктів доступу.<br><img alt="Ієрархія кешу: два ядра з приватними кешами L1 та L2, що спільно використовують кеш L3" src="assets/l06-cache-hierarchy.png" target="_self">Ієрархія кешу в двоядерному процесорі: кожне ядро має приватні кеші L1 та L2 для швидкості, тоді як більший спільний кеш L3 обслуговує обидва ядра.Кеш ефективний, тому що реальні програми демонструють дві ключові поведінки:
Часова локальність: Якщо до адреси пам'яті нещодавно зверталися, ймовірно, до неї звернуться знову незабаром. Наприклад, змінна лічильника циклу зчитується та записується на кожній ітерації.
Просторова локальність: Якщо до адреси пам'яті зверталися, ймовірно, що до сусідніх адрес звернуться незабаром. Наприклад, при ітерації по масиву елементи зберігаються в послідовних адресах пам'яті.
Коли ЦП запитує дані, він спочатку перевіряє кеш:
Влучення в кеш: Дані знайдено в кеші, і ЦП отримує їх швидко (наприклад, 4 цикли з L1 замість 200 циклів з DRAM). Це швидкий шлях.
Промах кешу: Даних немає в кеші. ЦП повинен отримати їх з нижчого рівня (L2, L3 або зрештою DRAM). Після отримання дані також зберігаються в кеші для подальшого доступу.
При промаху кешу кеш вибирає не лише один запитаний байт — він вибирає цілий рядок кешу (зазвичай 64 байти). Це використовує просторову локальність: якщо програма звертається до адреси N, ймовірно, що вона звернеться до адрес N+1, N+2, ... N+63 найближчим часом.Ключовий момент: програми зазвичай не мають прямого доступу до кеш-пам'яті. Ви не можете написати інструкцію на асемблері, яка каже "завантажити з кешу L1". Натомість кеш керується автоматично складними апаратними алгоритмами, вбудованими в ЦП. Коли ви пишете MOV AX, [myVar], контролер кешу ЦП прозоро перевіряє, чи є myVar у кеші, і відповідно обробляє доступ до пам'яті.Ця прозорість означає, що кеш прискорює майже всі програми без необхідності будь-яких змін у програмному забезпеченні. Однак програмісти, які розуміють поведінку кешу, можуть писати код, що значно краще використовує кеш — наприклад, звертатися до масивів послідовно, а не випадково, що кардинально покращує просторову локальність та відсоток влучень у кеш.На самій вершині ієрархії пам'яті знаходяться регістри ЦП — комірки пам'яті, до яких асемблерний код звертається безпосередньо. Як ми дізналися в Лекції 3, 8086 має регістри загального призначення (AX, BX, CX, DX), індексні регістри (SI, DI), регістри-вказівники (BP, SP), сегментні регістри (CS, DS, SS, ES), вказівник інструкцій (IP) та регістр прапорців (FLAGS).Основні характеристики регістрів:
Найшвидший доступ: зазвичай 1 тактовий цикл (менше наносекунди). Транзакція на шині не потрібна, оскільки регістри є частиною самого ЦП.
Найменший обсяг: 8086 має лише 14 регістрів, кожний шириною 16 біт — загалом лише 28 байтів регістрового сховища.
Кожне ядро має власний набір: у сучасному багатоядерному ЦП кожне ядро має свій незалежний файл регістрів. Асемблерні програми абстраговані від цього — код виконується на тому ядрі, яке призначить операційна система.
Спеціальне призначення: як ми розглядали в Л03, багато регістрів мають конкретні ролі в певних інструкціях (CX як лічильник для LOOP, DX:AX для результатів MUL, SI/DI для рядкових операцій тощо).
Наступна таблиця узагальнює повну ієрархію пам'яті, від найшвидшої до найповільнішої:Зверніть увагу на величезні розриви між рівнями. Перехід від кешу L1 до DRAM — це приблизно 100-кратне сповільнення. Перехід від DRAM до HDD — приблизно 100 000-кратне сповільнення. Саме тому існує ієрархія пам'яті: кожний рівень діє як буфер для рівня нижче, забезпечуючи доступність найчастіше використовуваних даних на найвищій швидкості.
Аналогія: Уявіть, що ви дослідник за робочим столом. Ваші регістри — це нотатки у ваших руках: миттєво доступні, але дуже обмежені. Кеш L1 — це папери на вашому столі: швидко доступні, але місце обмежене. DRAM — це книжкова полиця через кімнату: потрібно підійти, знайти потрібну книгу та принести її назад. HDD — це бібліотека в іншому кінці кампусу: потрібно поїхати туди, пошукати в каталозі та повернутися. Кожний рівень потребує прогресивно більше часу для доступу, але може зберігати прогресивно більше.
Ієрархія пам'яті — це не лише академічна концепція: вона має глибокі практичні наслідки: Вузьке місце фон Неймана: ЦП фундаментально обмежений своєю здатністю отримувати дані з пам'яті. Більшість сучасних ЦП витрачають значну частку часу на очікування пам'яті. Ми розглянемо це детальніше, коли обговорюватимемо архітектуру фон Неймана в наступному розділі. Продуктивність програмного забезпечення: Код, що звертається до пам'яті за патернами, дружніми до кешу (послідовний доступ, хороша просторова локальність), може працювати на порядки швидше, ніж код із випадковим доступом до пам'яті. Саме тому, наприклад, ітерація по двовимірному масиву рядок за рядком є швидшою, ніж стовпець за стовпцем у мові C (де масиви зберігаються в порядку рядків). Проєктування апаратного забезпечення: Значна частина складності та транзисторного бюджету сучасного ЦП присвячена ієрархії кешу та керуванню пам'яттю. Intel Core i9-13900K, наприклад, має 36 MB кешу L3 — значна частина кристала відведена під SRAM. Збалансованість системи: Добре спроєктована система має відповідність між компонентами. Поєднання надзвичайно швидкого ЦП з повільною пам'яттю або крихітним кешем марнує потенціал ЦП, оскільки він витрачатиме більшу частину часу на очікування даних. Протягом перших п'яти практичних робіт ви писали асемблерні програми в DOSBox за допомогою TASM. Ви могли помітити дещо вражаюче: ваші програми можуть читати та записувати за будь-якою адресою пам'яті. Ви можете заглянути в Таблицю векторів переривань (IVT) за адресою 0000:0000, записувати безпосередньо у відеопам'ять за адресою B800:0000 та досліджувати структури даних BIOS. Ніщо вас не зупиняє. Жодна операційна система не каже «доступ заборонено». Це тому, що процесор 8086 — і DOSBox, який його емулює — працює в Реальному режимі.Але комп'ютер, за яким ви зараз сидите, працює інакше. Ваш веб-браузер не може прочитати пам'ять вашого менеджера паролів. Аварія гри не призводить до падіння операційної системи. Така ізоляція існує тому, що сучасні процесори працюють у Захищеному режимі. Розуміння різниці між цими двома режимами є важливим, оскільки воно пояснює як те, чому наше середовище DOSBox працює саме так, так і те, як сучасні операційні системи забезпечують безпеку та стабільність.Реальний режим — це оригінальний режим роботи процесора Intel 8086, представленого у 1978 році. Кожен процесор x86, навіть найновіший, запускається в Реальному режимі при увімкненні живлення — BIOS працює в Реальному режимі перед переходом до Захищеного режиму для завантаження операційної системи.Адресний простір: 1 МБ (20-бітні адреси). Як ми розглядали в L03, 8086 має 20-бітну адресну шину. Використовуючи схему сегмент:зміщення, процесор може адресувати 2^20 = 1 048 576 байтів пам'яті. Фізична адреса обчислюється як (Сегмент * 16) + Зміщення — саме так, як ви це робили у ваших лабораторних вправах.Відсутність захисту пам'яті. Це найважливіша характеристика Реального режиму. Кожна програма має необмежений доступ до всього адресного простору 1 МБ. Немає контролю доступу, рівнів привілеїв і меж між програмами. Одна помилкова інструкція MOV може перезаписати критичні системні дані. Розглянемо, до чого ваші програми мають вільний доступ:
Таблиця векторів переривань (IVT) за адресами 0000:0000 – 0000:03FF — 256 векторів переривань, кожен — 4-байтний далекий вказівник. Ваша програма може їх читати, змінювати або повністю замінювати (саме так працюють TSR-програми та власні обробники переривань).
Область даних BIOS за адресою 0040:0000 — буфер клавіатури, лічильник таймера, режим відображення, стан диска тощо. Ваша програма може прочитати лічильник тактів таймера або змінити частоту повтору клавіатури, записавши значення за потрібною адресою.
Відеопам'ять за адресою B800:0000 (текстовий режим) або A000:0000 (графічний режим) — як ви бачили на лабораторних, запис байтів сюди одразу змінює те, що відображається на екрані. Не потрібен виклик API, не потрібна перевірка дозволів.
ROM BIOS за адресами F000:0000 – F000:FFFF — сам код мікропрограми відображений тут.
Відсутність багатозадачності. У Реальному режимі одна програма «володіє» всією машиною. Немає механізму для процесора перемикатися між програмами, захищати одну програму від іншої або переривати запущену програму. DOS не є багатозадачною операційною системою — коли ваша програма .COM або .EXE запускається, DOS фактично відходить убік і чекає, поки програма поверне керування через INT 21h (функція 4Ch).Карта пам'яті Реального режиму (0x00000 – 0xFFFFF):Це середовище, в якому ви програмували. Коли ви пишете MOV AX, 0B800h / MOV ES, AX / MOV ES:[0], 'A', ви безпосередньо маніпулюєте апаратним забезпеченням. Жодних системних викликів, драйверів, дозволів — лише прямий доступ до пам'яті.Обмеження Реального режиму стали гострими з розвитком обчислювальної техніки. На початку 1980-х років користувачі хотіли запускати кілька програм одночасно, і їм було потрібно, щоб ці програми були ізольовані одна від одної. Якби текстовий редактор міг випадково (або зловмисно) перезаписати пам'ять електронної таблиці, система була б ненадійною та небезпечною.<br>Intel вирішила цю проблему за допомогою Захищеного режиму, представленого в базовій формі на <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Intel_80286" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Intel_80286" target="_self">Intel 80286</a> (1982) та повністю реалізованого на <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Intel_80386" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Intel_80386" target="_self">Intel 80386</a> (1985). Саме 80386 зробив можливими сучасні операційні системи, такі як Windows і Linux.Адресний простір: до 4 ГБ (32-бітні адреси). 80386 розширив адресну шину до 32 бітів, що дозволило адресувати 2^32 = 4 294 967 296 байтів пам'яті — у 4 096 разів більше, ніж 1 МБ Реального режиму.<br>Віртуальна пам'ять. Кожна програма отримує свій власний віртуальний адресний простір. Програма «вважає», що має доступ до великого, неперервного блоку пам'яті, починаючи з адреси 0, але насправді операційна система і <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Memory_management_unit" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Memory_management_unit" target="_self">Блок керування пам'яттю (MMU)</a> процесора перетворюють ці віртуальні адреси у фізичні адреси. Дві програми можуть обидві використовувати віртуальну адресу 0x00400000 без конфлікту, оскільки кожна відображається на різне фізичне розташування. Сторінки пам'яті, які не використовуються активно, можуть бути «скинуті» на диск і повернуті назад при зверненні, що робить ефективний обсяг пам'яті більшим за фізичну оперативну пам'ять.Захист пам'яті. Програми не можуть отримати доступ до пам'яті, яка їм не належить. Якщо програма намагається читати або записувати за межами виділених їй сторінок, процесор генерує Загальне порушення захисту (GPF), і операційна система завершує програму-порушника, замість того щоб дозволити їй пошкодити інші програми або саму ОС.Кільця привілеїв. Захищений режим впроваджує апаратно забезпечену систему привілеїв із чотирма рівнями, пронумерованими від Кільця 0 до Кільця 3:На практиці більшість сучасних операційних систем (Windows, Linux, macOS) використовують лише два кільця: Кільце 0 для ядра і Кільце 3 для всього іншого. Кільця 1 і 2 існують на апаратному рівні, але рідко використовуються, оскільки двокільцева модель є простішою і достатньою.Коли вашій програмі в Захищеному режимі потрібно виконати привілейовану операцію — прочитати файл, виділити пам'ять, зв'язатися з пристроєм — вона не може зробити це безпосередньо. Натомість вона виконує системний виклик (також відомий як syscall): контрольований перехід з Кільця 3 до Кільця 0, де ядро виконує операцію від імені програми та повертає результат. Це принципово відрізняється від Реального режиму, де ваша програма використовує INT 21h не тому, що мусить, а тому, що DOS надає зручні функції — ніщо не заважає вам обійти DOS і звертатися до апаратного забезпечення безпосередньо.<br>Таблиці дескрипторів замість простого сегмент:зміщення. У Захищеному режимі сегментні регістри більше не містять прямої адреси пам'яті. Натомість вони містять селектор — індекс у таблиці, яка називається <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Global_Descriptor_Table" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Global_Descriptor_Table" target="_self">Глобальна таблиця дескрипторів (GDT)</a>. Кожен запис у цій таблиці (званий дескриптором сегмента) визначає базову адресу, ліміт (розмір) і права доступу до сегмента пам'яті. Процесор перевіряє кожне звернення до пам'яті за цими дескрипторами, забезпечуючи захист на апаратному рівні.Розуміння обох режимів є важливим, оскільки протягом курсу ви працюватимете з обома:Практичні роботи P1–P10 (Реальний режим, DOSBox + TASM). Ви працювали — і продовжуватимете працювати — в Реальному режимі. Це цінно з педагогічної точки зору, оскільки ви бачите «всю машину» без шарів абстракції. Коли ви записуєте у відеопам'ять, ви бачите, як з'являється символ. Коли ви змінюєте IVT, ви змінюєте поведінку переривань. Жодна операційна система не приховує від вас апаратне забезпечення. Така безпосередність формує глибоке розуміння того, як комп'ютери насправді працюють на найнижчому рівні.Практичні роботи P11–P15 (Захищений режим, Linux x86-64). Пізніше в курсі ви перейдете до написання асемблерного коду в Linux — сучасному середовищі Захищеного режиму. Тут правила змінюються:
Ви використовуватимете системні виклики (інструкція syscall на x86-64) замість переривань DOS (INT 21h) або прямого доступу до апаратного забезпечення.
Ваші програми працюватимуть у Кільці 3 — ви не зможете безпосередньо взаємодіяти з апаратним забезпеченням.
Адреси пам'яті будуть віртуальними — адреси, які бачить ваша програма, перетворюються MMU перед зверненням до фізичної оперативної пам'яті.
Набір інструкцій розшириться до 64-бітних регістрів (RAX, RBX, ... R8–R15) та нових режимів адресації.
Перехід від Реального режиму до Захищеного режиму повторює історичний перехід, який сама комп'ютерна індустрія пережила протягом 1980-х і 1990-х років — від однокористувацьких DOS-систем до багатозадачних, багатокористувацьких операційних систем. Пройшовши через обидва режими, ви зрозумієте, чому Захищений режим існує і які проблеми він вирішує.Досі ми вивчали, як працює ЦП — його регістри, інструкції та режими роботи. Але як ЦП з'єднаний з рештою комп'ютера? Як інструкції та дані переміщуються між процесором і пам'яттю? Ці питання підводять нас до двох фундаментальних архітектурних моделей, які визначають організацію комп'ютерів на найвищому рівні: Архітектура фон Неймана та Гарвардська архітектура.<br>Архітектура фон Неймана (також відома як Прінстонська архітектура) була описана математиком <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/John_von_Neumann" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/John_von_Neumann" target="_self">Джоном фон Нейманом</a> у 1945 році в його визначному документі <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/First_Draft_of_a_Report_on_the_EDVAC" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/First_Draft_of_a_Report_on_the_EDVAC" target="_self">First Draft of a Report on the EDVAC</a>. Хоча ідеї були розроблені спільно з Дж. Преспером Екертом та Джоном Моклі в Школі електротехніки Мура Пенсильванського університету, ім'я фон Неймана закріпилося за цим проєктом, оскільки його звіт був першим, що набув широкого поширення.Основний принцип простий і глибокий: інструкції та дані зберігаються в одній пам'яті і передаються по одній шині.До Архітектури фон Неймана комп'ютери, як-от ENIAC, програмувалися шляхом фізичної перекомутації кабелів і встановлення перемикачів — процес, який міг тривати тижнями для однієї програми. Концепція програми, що зберігається в пам'яті, означала, що програму можна завантажити в пам'ять так само, як і дані, що зробило комп'ютери перепрограмовуваними без апаратних змін.Компоненти Архітектури фон Неймана:
Центральний процесор (ЦП), що складається з: Арифметико-логічний пристрій (АЛП) — виконує обчислення (як вивчалося в L02)
Блок керування — здійснює вибірку та декодування інструкцій, координує виконання
Регістри — невелике, швидке сховище всередині ЦП (як вивчалося в L03) Пам'ять — єдине, уніфіковане сховище для програмних інструкцій та даних
Ввід/Вивід (I/O) — механізми для зв'язку комп'ютера із зовнішнім світом (клавіатура, дисплей, диск, мережа)
Системна шина — спільний канал зв'язку, що з'єднує ЦП, пам'ять та I/O
Процесор 8086, який ми вивчали, побудований за Архітектурою фон Неймана. Коли ваша програма TASM виконується, і інструкції (машинний код), і дані (змінні в сегменті .data) перебувають в одному фізичному просторі пам'яті й доступні через одну шинну систему. Процесор здійснює вибірку інструкцій з однієї ділянки пам'яті, потім зчитує або записує дані за іншою адресою, але все це відбувається через один і той самий інтерфейс пам'яті.<br><img alt="Архітектура фон Неймана: ЦП з Блоком керування та АЛП, уніфікована Пам'ять, Ввід і Вивід" src="assets/l06-von-neumann-architecture.png" target="_self">Архітектура фон Неймана: ЦП (що містить Блок керування та АЛП) з'єднаний з єдиною уніфікованою пам'яттю, яка зберігає і інструкції, і дані.<br>Вузьке місце фон Неймана. Оскільки інструкції та дані використовують одну шину, ЦП не може одночасно здійснювати вибірку інструкції та читати/записувати дані. Він мусить чергувати: вибірка інструкції, потім доступ до даних, потім вибірка наступної інструкції і так далі. Це фундаментальне обмеження було виявлено <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/John_Backus" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/John_Backus" target="_self">Джоном Бекусом</a> у його лекції з нагоди отримання премії Тюрінга у 1977 році, де він назвав це «вузьким місцем фон Неймана». Оскільки швидкість ЦП зростала значно швидше за швидкість пам'яті протягом десятиліть, це вузьке місце ставало дедалі відчутнішим — процесор витрачає все більше часу на очікування пам'яті. Кеш-пам'ять (яку ми розглянемо в наступному розділі) є одним із основних розв'язань цієї проблеми.Чому це важливо: Здатність розглядати інструкції як дані — це не просто деталь реалізації, а саме те, що робить можливими компілятори, асемблери, компонувальники та завантажувачі. Асемблер читає текст, генерує машинний код і записує цей код у файл. Завантажувач читає цей файл і розміщує код у пам'яті, де ЦП може його виконати. Самомодифікований код — програми, які перезаписують власні інструкції під час виконання — також можливий в Архітектурі фон Неймана, хоча сучасні операційні системи зазвичай забороняють це з міркувань безпеки.Гарвардська архітектура використовує інший підхід: інструкції та дані мають окремі простори пам'яті та окремі шини.<br>Назва походить від комп'ютера <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Harvard_Mark_I" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Harvard_Mark_I" target="_self">Harvard Mark I</a>, побудованого в Гарвардському університеті у 1940-х роках, який зберігав інструкції на перфострічці, а дані — в електромеханічних лічильниках — двох фізично різних системах зберігання. (Історично термін «Гарвардська архітектура» був фактично запроваджений десятиліттями пізніше в контексті проєктування мікроконтролерів і ретроспективно застосований до гарвардських машин.)Компоненти Гарвардської архітектури:
ЦП (АЛП + Блок керування + Регістри) — так само, як у фон Неймана
Пам'ять інструкцій — зберігає програмний код
Пам'ять даних — зберігає змінні та дані часу виконання
Шина інструкцій — з'єднує ЦП з пам'яттю інструкцій
Шина даних — з'єднує ЦП з пам'яттю даних
I/O — підключений до шини даних
Ключова перевага — паралелізм: оскільки є дві незалежні шини, ЦП може здійснювати вибірку наступної інструкції з пам'яті інструкцій одночасно із читанням або записом даних із пам'яті даних. Це усуває Вузьке місце фон Неймана для цих операцій і може значно підвищити пропускну здатність.<br><img alt="Гарвардська архітектура: окрема пам'ять інструкцій та пам'ять даних, з'єднані з блоком керування" src="assets/l06-harvard-architecture.png" target="_self">Гарвардська архітектура: пам'ять інструкцій та пам'ять даних фізично розділені, кожна має власну шину до блоку керування, що дозволяє одночасний доступ.Де використовується Гарвардська архітектура? Ви зазвичай не знайдете чисту Гарвардську архітектуру в комп'ютері загального призначення на вашому столі. Натомість вона поширена в спеціалізованих, критичних за продуктивністю або обмежених у ресурсах системах:
Мікроконтролери — такі як сімейства PIC та AVR (використовуються в платах Arduino, вбудованих системах, побутовій техніці, автомобільній електроніці). Вони часто зберігають програмний код у флеш-пам'яті, а дані — у SRAM, з окремими шинами для кожного.
Цифрові сигнальні процесори (DSP) — спеціалізовані процесори для обробки аудіо, відео та телекомунікацій. DSP часто мають кілька шин даних для забезпечення паралельного доступу до даних у вимогливих алгоритмах реального часу.
Вбудовані системи — де передбачувана часова поведінка Гарвардської архітектури (відсутність конкуренції між вибіркою інструкцій та даних) є цінною для вимог реального часу.
Ось ключове спостереження: сучасні ЦП насправді є гібридом обох архітектур.Зовнішньо сучасний процесор x86 або ARM представляє Архітектуру фон Неймана — інструкції та дані знаходяться в одній уніфікованій основній пам'яті (оперативній пам'яті). Ваш компілятор створює виконуваний файл, що містить і код, і дані, а операційна система завантажує його в єдиний адресний простір.Але всередині процесор використовує дизайн у стилі Гарвардської архітектури для своєї ієрархії кешу. Кеш першого рівня (L1) (найшвидший, найменший кеш, найближчий до ядра ЦП) розділений на два окремі кеші:
L1i (кеш інструкцій) — зберігає нещодавно вибрані інструкції
L1d (кеш даних) — зберігає нещодавно використані дані
Ці два кеші мають окремі шляхи доступу, що дозволяє ЦП здійснювати вибірку інструкції з L1i та читати/записувати дані з L1d одночасно — так само, як у Гарвардській архітектурі. Це усуває Вузьке місце фон Неймана для кешованих звернень, які становлять переважну більшість операцій з пам'яттю.<br>Такий гібридний дизайн називається <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Modified_Harvard_architecture" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Modified_Harvard_architecture" target="_self"><strong></strong></a>Модифікована Гарвардська архітектура, і він дає сучасним процесорам найкраще від обох підходів: гнучкість програмування уніфікованої моделі пам'яті (фон Неймана) та переваги продуктивності окремих шляхів для інструкцій та даних (Гарвардська). Ми повернемося до цієї архітектури з розділеним кешем детальніше в наступному розділі про цикл виконання інструкцій.Усе, що ми обговорювали до цього моменту в цій лекції — ЦП, системна шина, ієрархія пам'яті, архітектури фон Неймана та Гарвардська — об'єднується в одному фундаментальному процесі: циклі виконання інструкцій. Це серцебиття кожного комп'ютера. З моменту подачі живлення на ЦП і до моменту його вимкнення він повторює одну й ту саму базову послідовність знову і знову: вибрати інструкцію, декодувати її, виконати, а потім перейти до наступної.Розуміння цього циклу є важливим, оскільки воно пов'язує абстрактне поняття "виконання програми" з конкретними механізмами апаратного забезпечення. Коли ви пишете MOV AX, 5 мовою асемблера, ЦП не просто "знає", що робити — він повинен фізично отримати ці байти з пам'яті, інтерпретувати їхнє значення та виконати операцію за допомогою своїх внутрішніх схем.Цикл виконання інструкцій — також відомий як цикл вибірка-декодування-виконання або просто цикл вибірка-виконання — складається з трьох основних етапів, які безперервно повторюються.Етап 1: ВибіркаСпочатку ЦП повинен отримати наступну інструкцію з пам'яті. Цей етап включає кілька внутрішніх кроків:
Вказівник інструкцій (IP) зберігає адресу наступної інструкції для виконання. У реальному режимі повна адреса обчислюється з CS:IP (сегмент коду : вказівник інструкцій), як ми обговорювали в Лекції 3.
ЦП розміщує цю адресу на адресній шині та надсилає сигнал читання через шину керування (як ми бачили в Розділі 7, системна шина має три компоненти, що працюють разом).
Байти інструкції повертаються до ЦП через шину даних і завантажуються в регістр інструкцій (IR) — спеціальний внутрішній регістр, який зберігає інструкцію, що наразі обробляється.
Потім IP збільшується, щоб вказувати на наступну інструкцію в пам'яті. Для 8086 величина збільшення залежить від довжини поточної інструкції — однобайтова інструкція на кшталт NOP збільшує IP на 1, тоді як трибайтова інструкція на кшталт MOV AX, 0005h збільшує IP на 3.
На цьому етапі ЦП має копію інструкції у своєму регістрі інструкцій і готовий визначити, що ця інструкція означає.Етап 2: ДекодуванняНа етапі декодування блок керування (CU) процесора досліджує біти вибраної інструкції, щоб визначити три речі: Яку операцію виконати — Код операції (opcode) інструкції визначає тип операції. Наприклад, код операції 0xB8 означає "завантажити безпосереднє 16-бітне значення в AX". Блок керування використовує внутрішню логіку (історично таблицю пошуку або мікрокод ПЗП) для перетворення коду операції на конкретні сигнали керування, необхідні для координації операції. Які операнди задіяні — Решта байтів інструкції визначають джерело та призначення даних. Це може бути номер регістра, безпосереднє значення, вбудоване в саму інструкцію, або адреса пам'яті. Чи потрібен додатковий доступ до пам'яті — Якщо операнд знаходиться в пам'яті (наприклад, MOV AX, [BX]), ЦП повинен виконати додатковий доступ до пам'яті, щоб отримати значення операнда перед виконанням інструкції. Ця додаткова вибірка додає тактових циклів до загального часу виконання інструкції. Етап декодування — це місце, де ЦП перетворює необроблені двійкові дані на внутрішні сигнали, що підлягають дії, — по суті, перекладаючи машинну мову на електричні імпульси, які керують схемами процесора.Етап 3: ВиконанняТепер ЦП виконує фактичну операцію, визначену інструкцією:
Якщо інструкція включає арифметичні або логічні дії (ADD, SUB, AND, OR, CMP тощо), операнди надсилаються до АЛП (арифметико-логічного пристрою), який обчислює результат.
Результат записується до призначення — або регістра, або комірки пам'яті.
Регістр прапорців (FLAGS) оновлюється відповідно до результату операції. Наприклад, якщо інструкція ADD дає нульовий результат, прапорець нуля (ZF) встановлюється в 1. Якщо результат переповнюється, встановлюється прапорець переповнення (OF).
Для інструкцій керування потоком виконання, таких як JMP або CALL, етап виконання змінює вказівник інструкцій (і, можливо, стек), а не виконує арифметичні дії.
Після завершення виконання цикл повертається до етапу вибірки, і процес починається знову з наступної інструкції.<br><img alt="Діаграма циклу вибірка-декодування-виконання" src="assets/l06-fetch-decode-execute.gif" target="_self">Цикл виконання інструкцій: нескінченний цикл вибірки наступної інструкції, декодування її коду операції та виконання операції.Не всі інструкції виконуються за однаковий час. Тактовий сигнал забезпечує основний часовий орієнтир — кожен такт є одним тактовим циклом — але різні інструкції потребують різної кількості циклів для завершення:Закономірність очевидна: операції, що залишаються в межах ЦП (регістр-регістр), є найшвидшими, тоді як операції, що включають доступ до пам'яті, значно повільніші. Це прямий наслідок фізичної реальності, яку ми обговорювали в Розділі 9 — доступ до даних з DRAM займає на порядки більше часу, ніж доступ до даних з регістра.На сучасних процесорах, що працюють на багатогігагерцових частотах, ця диспропорція стає ще більш драматичною. ЦП з частотою 3,4 ГГц завершує один тактовий цикл приблизно за 0,29 наносекунди. Проста інструкція регістр-регістр може виконатися за 1 цикл — але промах кешу, що вимагає звернення до DRAM, може затримати ЦП на 100-200 циклів. За ці 200 циклів ЦП теоретично міг би виконати сотні простих інструкцій. Цей "втрачений час" лежить в основі вузького місця фон Неймана, яке ми розглянемо в наступному розділі.<br>Для візуальної демонстрації того, як цикл вибірка-декодування-виконання працює на апаратному рівні, перегляньте <a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=Z5JC9Ve1sfI" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=Z5JC9Ve1sfI" target="_self">The Fetch-Execute Cycle: What's Your Computer Actually Doing?</a> від Тома Скотта. Це відео надає чудову анімовану покрокову демонстрацію проходження інструкцій через внутрішні компоненти ЦП.Варто зазначити, що в простіших, старших ЦП, таких як 8086, цикл інструкцій виконувався послідовно — ЦП повністю завершував одну інструкцію перед початком наступної. Сучасні ЦП використовують такі техніки, як конвеєрна обробка, для перекриття етапів різних інструкцій, тож поки одна інструкція виконується, наступна декодується, а третя вибирається з пам'яті — усе одночасно. Ми коротко представимо конвеєрну обробку в наступному розділі та повернемося до неї детальніше в наступних лекціях.Тепер ми побачили, як архітектура фон Неймана зберігає інструкції та дані в одній пам'яті (Розділ 11), як системна шина з'єднує ЦП з цією пам'яттю (Розділ 7), як ієрархія пам'яті забезпечує рівні поступово повільнішого сховища (Розділ 9) та як ЦП вибирає й виконує інструкції в безперервному циклі (Розділ 12). Усі ці концепції сходяться в одній з найважливіших проблем архітектури комп'ютерів: вузькому місці фон Неймана.Згадаймо з Розділу 11, що в архітектурі фон Неймана інструкції та дані використовують спільну пам'ять і передаються через одну шину. Цей підхід має елегантну простоту — програма може розглядати свої власні інструкції як дані, що забезпечує потужні можливості, такі як компілятори, інтерпретатори та самомодифікований код. Але він також створює фундаментальне обмеження.ЦП може використовувати шину лише для однієї справи одночасно. Він не може одночасно вибрати наступну інструкцію та прочитати операнд даних з пам'яті — обидва запити повинні проходити через одну й ту саму шину. Шина стає вузькою трубою, через яку повинна протікати вся інформація.Це не було б серйозною проблемою, якби ЦП і пам'ять працювали на подібних швидкостях. Але це не так. Протягом останніх кількох десятиліть тактові частоти ЦП зростали значно швидше, ніж швидкість доступу до пам'яті. Цю розбіжність іноді називають розривом продуктивності процесор-пам'ять:
На початку 1980-х років тактовий цикл ЦП і доступ до пам'яті займали приблизно однаковий час.
До 2000-х років швидкість ЦП зросла приблизно в 1000 разів, тоді як швидкість DRAM покращилася лише приблизно в 10 разів.
Сьогодні сучасний ЦП може виконувати кілька інструкцій за наносекунду, але доступ до DRAM все ще займає 50-100 наносекунд.
Результат: ЦП витрачає значну частину свого часу, просто очікуючи на надходження даних з пам'яті. Спільна шина — визначальна ознака архітектури фон Неймана — стає вузьким місцем, що обмежує загальну продуктивність системи.<br>Термін "вузьке місце фон Неймана" був популяризований <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/John_Backus" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/John_Backus" target="_self">Джоном Бекусом</a> (творцем FORTRAN) у його лекції з нагоди вручення премії Тюрінга ACM у 1977 році, де він описав це так:
"Безумовно, має існувати менш примітивний спосіб вносити великі зміни до сховища, ніж проштовхувати величезну кількість слів туди-сюди через вузьке місце фон Неймана."
Покажемо цю проблему в конкретних числах. У наступній таблиці наведено приблизний час доступу для кожного рівня ієрархії пам'яті в сучасній системі:Розглянемо, що означають ці числа на практиці. На частоті 3,4 ГГц один тактовий цикл займає приблизно 0,29 наносекунди. Проста операція регістр-регістр завершується за 1 цикл. Але коли ЦП потребує даних, які знаходяться лише в DRAM, він повинен чекати приблизно 200 тактових циклів, поки ці дані надійдуть. За ці 200 циклів ЦП міг би виконати приблизно 400 простих інструкцій — але натомість він простоює, очікуючи.А якщо дані було перенесено на SSD або (ще гірше) жорсткий диск, ЦП міг би виконати мільйони інструкцій за час одного доступу. Ось чому вичерпання оперативної пам'яті та використання файлу підкачки спричиняє таке різке уповільнення — розрив продуктивності процесор-пам'ять зростає з коефіцієнта 200 до мільйонів.Вузьке місце фон Неймана — це не просто теоретична проблема. Це домінуюче обмеження продуктивності більшості обчислювальних систем. Майже кожна значна архітектурна інновація в сучасних ЦП існує, принаймні частково, щоб пом'якшити це вузьке місце.Інженери розробили кілька стратегій для зменшення впливу вузького місця фон Неймана. Ці стратегії не усувають фундаментальну проблему — вони обходять її, намагаючись забезпечити, щоб дані, які потрібні ЦП, вже були поруч, коли вони знадобляться.1. Кеш-пам'ятьНайважливіший засіб пом'якшення — це кеш-пам'ять — невелика швидка пам'ять, розташована на кристалі ЦП або дуже близько до нього, яка зберігає копії часто використовуваних даних та інструкцій. Як ми обговорювали в Розділі 9, сучасні ЦП мають кілька рівнів кешу (L1, L2, L3), кожен більший, але повільніший за попередній.Кешування працює тому, що реальні програми демонструють дві ключові закономірності доступу до пам'яті: Часова локальність: якщо до певних даних нещодавно зверталися, ймовірно, до них звернуться знову найближчим часом. Уявіть змінну циклу, яка читається та оновлюється на кожній ітерації — коли вона завантажена в кеш, наступні звернення є швидкими. Просторова локальність: якщо до певних даних зверталися, до сусідніх даних, ймовірно, звернуться найближчим часом. Коли ЦП завантажує дані в кеш, він завантажує цілий рядок кешу (зазвичай 64 байти), захоплюючи сусідні дані безкоштовно. Ось чому послідовний прохід масивом набагато швидший, ніж доступ до випадкових комірок пам'яті. Коли ЦП знаходить потрібні дані в кеші (влучення в кеш), він уникає повільного звернення до DRAM. Сучасні програми досягають частоти влучень у кеш 95-99%, тобто лише 1-5% звернень до пам'яті фактично потребують звернення до DRAM. Це перетворює ефективний середній час доступу до пам'яті з ~100 циклів на лише кілька циклів.2. Роздільний кеш L1 (модифікована Гарвардська архітектура)Ось одне з найелегантніших рішень проблеми вузького місця: хоча комп'ютер зберігає архітектуру фон Неймана на системному рівні (інструкції та дані використовують спільну основну пам'ять і шину), ЦП внутрішньо використовує Гарвардську архітектуру на рівні кешу L1.Кеш L1 поділяється на два окремі кеші:
L1i (кеш інструкцій) — зберігає нещодавно вибрані інструкції
L1d (кеш даних) — зберігає нещодавно використані дані
Оскільки це окремі фізичні кеші з окремими внутрішніми шинами, ЦП може вибирати наступну інструкцію з L1i одночасно з читанням даних з L1d. Цей паралелізм безпосередньо вирішує проблему вузького місця фон Неймана — ЦП більше не повинен обирати між вибіркою інструкцій та доступом до даних, принаймні для даних, що знаходяться в кеші L1.Цей підхід іноді називають модифікованою Гарвардською архітектурою або архітектурою з роздільним кешем. Він дає нам найкраще з обох світів: гнучкість програмування архітектури фон Неймана (інструкції та дані в одному адресному просторі) з перевагою продуктивності Гарвардської архітектури (окремі шляхи для інструкцій та даних на рівні кешу).3. Попередня вибіркаЗамість того, щоб чекати, поки ЦП запросить дані, а потім їх отримувати (що гарантує затримку при кожному промаху кешу), сучасні ЦП проактивно здійснюють попередню вибірку даних, які, за прогнозами, знадобляться найближчим часом.Апаратний блок попередньої вибірки ЦП відстежує патерни доступу до пам'яті та виявляє закономірності:
Послідовний доступ: якщо ЦП читає адреси 1000, 1004, 1008, ..., блок попередньої вибірки передбачає, що 1012, 1016 тощо знадобляться далі, і починає завантажувати їх у кеш до того, як ЦП попросить.
Доступ з кроком: якщо ЦП читає адреси 1000, 1100, 1200, ..., блок попередньої вибірки виявляє крок 100 і здійснює попередню вибірку відповідно.
Програмні інструкції попередньої вибірки: сучасні архітектури набору інструкцій (включаючи x86) надають явні інструкції попередньої вибірки, які дозволяють програмістам або компіляторам підказати ЦП про майбутні потреби в даних.
Коли попередня вибірка працює добре, дані потрапляють у кеш саме перед тим, як ЦП їх потребує, фактично повністю приховуючи затримку пам'яті.4. Інші техніки (попередній огляд)Кілька додаткових архітектурних технік допомагають пом'якшити вузьке місце фон Неймана. Ми згадуємо їх тут коротко для ознайомлення — вони будуть розглянуті детальніше в наступних лекціях: Конвеєрна обробка: замість того, щоб завершувати одну інструкцію перед початком наступної, ЦП перекриває етапи вибірки, декодування та виконання різних інструкцій. Поки інструкція N виконується, інструкція N+1 декодується, а інструкція N+2 вибирається з пам'яті. Це збільшує пропускну здатність без підвищення тактової частоти. Класична аналогія — конвеєр прання: ви не чекаєте, поки перша партія буде випрана, висушена та складена, перш ніж завантажити другу партію в пральну машину. Передбачення розгалужень: умовні переходи (на кшталт JZ або JNE) створюють проблему для конвеєра — ЦП не знає, яку інструкцію вибирати наступною, доки не буде обчислена умова. Сучасні ЦП використовують складні блоки передбачення розгалужень, які вгадують результат умовних переходів на основі минулої поведінки. Коли передбачення правильне (що відбувається у 95-99% випадків на сучасному обладнанні), конвеєр продовжує працювати на повній швидкості. Коли неправильне, конвеєр потрібно очистити та перезапустити — це дорога штрафна затримка, яка називається помилковим передбаченням розгалуження. Позачергове виконання: якщо інструкція N очікує на дані з пам'яті, ЦП не просто зупиняється — він переглядає потік інструкцій наперед і виконує наступні інструкції, які готові до виконання. Результати потім фіксуються у вихідному порядку програми, тому програма поводиться так, ніби інструкції виконувалися послідовно. Ця техніка тримає виконавчі блоки ЦП зайнятими, навіть коли окремі інструкції затримуються через очікування пам'яті. Суперскалярне виконання: сучасні ЦП містять кілька виконавчих блоків (кілька АЛП, кілька блоків завантаження/збереження) і можуть виконувати декілька інструкцій одночасно за один тактовий цикл, за умови, що інструкції не залежать від результатів одна одної. Вузьке місце фон Неймана — це фундаментальне протиріччя в архітектурі комп'ютерів. Елегантна простота зберігання інструкцій та даних в одній пам'яті має свою ціну — спільний шлях з обмеженою пропускною здатністю між ЦП і пам'яттю. Оскільки ЦП стали швидшими, а пам'ять не встигає за ними, це вузьке місце стає все більш серйозним.Майже кожна значна архітектурна інновація в сучасних процесорах — ієрархії кешу, роздільні кеші L1, попередня вибірка, конвеєрна обробка, передбачення розгалужень, позачергове виконання — існує, принаймні частково, щоб пом'якшити це вузьке місце. Тому розуміння вузького місця фон Неймана — це не просто академічна вправа — це ключ до розуміння того, чому сучасні ЦП спроєктовані саме так, як вони є.Коли ви дивитесь на кристал сучасного процесора і бачите, що понад половина його транзисторів відведена під кеш-пам'ять, ви бачите вузьке місце фон Неймана, втілене в кремнії. Коли ви дізнаєтесь, що програміст оптимізував код, покращивши локальність даних, і отримав 10-кратне прискорення без зміни алгоритму, ви бачите вузьке місце фон Неймана в дії на рівні програмного забезпечення. Це нитка, що з'єднує апаратне проєктування з продуктивністю програм, і вона залишатиметься центральною темою протягом усього курсу. Інструкції бітового зсуву (SHL, SHR, SAL, SAR, ROL, ROR) забезпечують ефективне множення та ділення на степені двійки і є фундаментальними для низькорівневої маніпуляції даними. Закон Мура — емпіричне спостереження, що кількість транзисторів подвоюється приблизно кожні 2 роки — діяв понад 60 років і рухав еволюцію від простих інтегральних схем до сучасних процесорів з мільярдами транзисторів. Шлях від ІС до мікропроцесорів (інтегральні схеми -&gt; мікроконтролери -&gt; мікропроцесори) являє собою зростаючі рівні інтеграції, що завершуються повноцінними обчислювальними системами на одному кристалі (SoC). Системна шина (шина даних, адресна шина та шина керування) є комунікаційною основою, що з'єднує ЦП з пам'яттю та пристроями введення-виведення, синхронізована системним годинником. Ієрархія пам'яті (регістри -&gt; кеш -&gt; DRAM -&gt; диск) є інженерним рішенням компромісу швидкість-вартість: швидша пам'ять є дорожчою та меншою, тоді як більше сховище є повільнішим і дешевшим. Реальний режим проти захищеного режиму — реальний режим (8086) надає програмам необмежений доступ до всього 1 МБ пам'яті; захищений режим (80386+) забезпечує адресацію 4 ГБ, віртуальну пам'ять та ізоляцію на основі привілеїв. Архітектура фон Неймана використовує єдину спільну пам'ять для даних та інструкцій, тоді як Гарвардська архітектура використовує окремі пам'яті. Сучасні ЦП є гібридами: зовні — фон Неймана, внутрішньо — Гарвардська (роздільний кеш L1). Вузьке місце фон Неймана — спільна шина між ЦП і пам'яттю — є фундаментальним обмеженням продуктивності. Кеш, попередня вибірка, роздільний L1 та конвеєрна обробка є основними контрзаходами. Лабораторна робота 6: Масиви та режими адресаціїУ цій лабораторній роботі ви будете практикувати:
Використання різних режимів адресації: [BX], [SI], [BX+SI], [BX+SI+disp]
Оголошення та маніпулювання масивами за допомогою DB, DW, DD
Спостереження за розташуванням даних у пам'яті за допомогою вікна Dump у Turbo Debugger
Застосування інструкцій бітового зсуву з цієї лекції до задач обробки масивів <br><a data-tooltip-position="top" aria-label="https://xlogicx.net/Assembly_Is_Too_High-Level_-_Better_Call_SAL.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://xlogicx.net/Assembly_Is_Too_High-Level_-_Better_Call_SAL.html" target="_self">Assembly Is Too High-Level — Better Call SAL</a> — Прихована історія недокументованого коду операції інструкції SAL
<br><a data-tooltip-position="top" aria-label="http://pnx.tf/files/x86_opcode_structure_and_instruction_overview.pdf" rel="noopener nofollow" class="external-link is-unresolved" href="http://pnx.tf/files/x86_opcode_structure_and_instruction_overview.pdf" target="_self">x86 Opcode Structure and Instruction Overview</a> — Візуальний посібник зі структури байта коду операції 8086
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/X86_instruction_listings#Original_8086/8088_instructions" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/X86_instruction_listings#Original_8086/8088_instructions" target="_self">Original 8086/8088 Instructions — Wikipedia</a> — Повна довідкова таблиця оригінального набору інструкцій x86
<br><a data-tooltip-position="top" aria-label="https://ourworldindata.org/moores-law" rel="noopener nofollow" class="external-link is-unresolved" href="https://ourworldindata.org/moores-law" target="_self">What is Moore's Law? — Our World in Data</a> — Інтерактивна візуалізація зростання кількості транзисторів
<br><a data-tooltip-position="top" aria-label="https://www.apple.com/newsroom/2025/03/apple-reveals-m3-ultra-taking-apple-silicon-to-a-new-extreme/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.apple.com/newsroom/2025/03/apple-reveals-m3-ultra-taking-apple-silicon-to-a-new-extreme/" target="_self">Apple M3 Ultra — Apple Newsroom</a> — 184 мільярди транзисторів, сучасний приклад закону Мура
<br><a data-tooltip-position="top" aria-label="https://uk.wikipedia.org/wiki/%D0%9C%D1%96%D0%BA%D1%80%D0%BE%D1%81%D1%85%D0%B5%D0%BC%D0%B0" rel="noopener nofollow" class="external-link is-unresolved" href="https://uk.wikipedia.org/wiki/%D0%9C%D1%96%D0%BA%D1%80%D0%BE%D1%81%D1%85%D0%B5%D0%BC%D0%B0" target="_self">Мікросхема — Вікіпедія</a> — Огляд інтегральних схем (українською)
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/7400-series_integrated_circuits" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/7400-series_integrated_circuits" target="_self">7400-series Integrated Circuits — Wikipedia</a> — Серія ІС, що стала галузевим стандартом
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Microcontroller" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Microcontroller" target="_self">Microcontroller — Wikipedia</a> — Повноцінні комп'ютери на одному чіпі
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Texas_Instruments_TMS1000" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Texas_Instruments_TMS1000" target="_self">Texas Instruments TMS1000 — Wikipedia</a> — Перший комерційний мікроконтролер
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Microprocessor" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Microprocessor" target="_self">Microprocessor — Wikipedia</a> — Від дискретної логіки до інтегрованої обробки
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Motherboard" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Motherboard" target="_self">Motherboard — Wikipedia</a> — Конструкція та компоненти системної основи
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/System_bus" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/System_bus" target="_self">System Bus — Wikipedia</a> — Архітектура шини даних, адресної шини та шини керування
<br><a data-tooltip-position="top" aria-label="https://people.cs.pitt.edu/~melhem/courses/1502p/Unit5/Unit5.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://people.cs.pitt.edu/~melhem/courses/1502p/Unit5/Unit5.html" target="_self">Modelling the Memory — University of Pittsburgh</a> — Моделювання доступу до пам'яті та часових характеристик
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Cache_(computing)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Cache_(computing)" target="_self">Cache (computing) — Wikipedia</a> — Принципи роботи кешу, мотивація та ієрархія
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Harvard_architecture" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Harvard_architecture" target="_self">Harvard Architecture — Wikipedia</a> — Архітектура з окремою пам'яттю для інструкцій та даних
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Von_Neumann_architecture" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Von_Neumann_architecture" target="_self">Von Neumann Architecture — Wikipedia</a> — Архітектура з єдиною пам'яттю
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Instruction_cycle" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Instruction_cycle" target="_self">Instruction Cycle — Wikipedia</a> — Цикл вибірка-декодування-виконання
<br><a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=Z5JC9Ve1sfI" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=Z5JC9Ve1sfI" target="_self">The Fetch-Execute Cycle — YouTube</a> — Візуальне пояснення виконання інструкцій
]]></description><link>lectures/l06.-системна-архітектура.html</link><guid isPermaLink="false">Lectures/L06. Системна архітектура.md</guid><pubDate>Tue, 10 Mar 2026 06:01:50 GMT</pubDate><enclosure url="assets/l06-shl-shift-left.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l06-shl-shift-left.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L09-NOTES-ENG. ISA Design and Models of Computation]]></title><description><![CDATA[Quick reference for Lecture 9. See full materials in L09-ENG. ISA Design and Models of Computation.md.Purpose: Formal mathematical models that define what a "computing machine" is, stripping away implementation details.All models below are Turing-equivalent — they compute the same class of functions. Differences are about convenience and efficiency, not capability.Key progression: Each step adds a capability that brings the model closer to real hardware. Turing machine -&gt; registers (counter) -&gt; indirect addressing + accumulator (RAM) -&gt; stored program (RASP).Learn more:
<a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Church%E2%80%93Turing_thesis" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Church%E2%80%93Turing_thesis" target="_self">Church-Turing thesis (Wikipedia)</a>
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Turing_machine" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Turing_machine" target="_self">Turing machine (Wikipedia)</a>
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Counter_machine" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Counter_machine" target="_self">Counter machine (Wikipedia)</a>
<br>Definition: An abstract <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Register_machine" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Register_machine" target="_self">register machine</a> with random access (constant-time) to any register.Components: Unbounded registers (r1 = accumulator), fixed program (separate instruction table, read-only), program counter, read-only input tape, write-only output tape.Instruction set: LOAD/STORE, ADD/SUB/MULT/DIV (all accumulator-based), READ/WRITE (I/O tapes), JUMP/JZERO/JGTZ, HALT. Every arithmetic op uses the accumulator as implicit source and destination.Addressing modes:
Direct (LOAD 5): access register r5 (like mov eax, [var])
Indirect (LOAD *5): use value in r5 as address (like mov eax, [ebx]) — this is what makes RAM more powerful than a counter machine
Cost models: Uniform (1 unit per op, standard for algorithm analysis) vs Logarithmic (cost proportional to operand bit-width).Key property: Program is separate from data (Harvard architecture). Cannot modify own program or store return addresses.Learn more:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Random-access_machine" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Random-access_machine" target="_self">Random-access machine (Wikipedia)</a>
Definition: A RAM where the program is stored in registers alongside data — the von Neumann architecture. "A Universal Turing Machine built on a RAM chassis."Stored program consequences: Subroutines, self-modifying code, programs as data (OS loaders, interpreters, JIT compilers), and security risks (buffer overflows).Von Neumann bottleneck: Instructions and data compete for the same bus. Modern CPUs use modified Harvard at cache level (split L1I/L1D) with unified address space for the programmer.Learn more:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Random-access_stored-program_machine" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Random-access_stored-program_machine" target="_self">Random-access stored-program machine (Wikipedia)</a>
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Von_Neumann_architecture#Von_Neumann_bottleneck" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Von_Neumann_architecture#Von_Neumann_bottleneck" target="_self">Von Neumann bottleneck (Wikipedia)</a>
<br>Definition: The programmable interface of a CPU — a contract between software and hardware. First articulated by Fred Brooks for IBM <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/IBM_System/360" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/IBM_System/360" target="_self">System/360</a> (1960s).An ISA defines: data types, registers, memory model, addressing modes, instruction formats (fixed vs variable length), instruction set, interrupt/exception model, I/O model.Key properties:
Binary compatibility — programs run on any implementation of the ISA
Extensibility — backward-compatible additions (x86 -&gt; IA-32 -&gt; x86-64 -&gt; SSE -&gt; AVX)
ISA vs Microarchitecture — ISA = what the machine does; microarchitecture = how it does it
Implementations: CPU generations (8086 to Core i9), competing manufacturers (Intel vs AMD), emulators (DOSBox), virtual machines (JVM).Two classification axes: (1) by operand location, (2) by instruction complexity.Learn more:
<br><a data-tooltip-position="top" aria-label="https://uk.wikipedia.org/wiki/%D0%90%D1%80%D1%85%D1%96%D1%82%D0%B5%D0%BA%D1%82%D1%83%D1%80%D0%B0_%D1%81%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D0%B8_%D0%BA%D0%BE%D0%BC%D0%B0%D0%BD%D0%B4" rel="noopener nofollow" class="external-link is-unresolved" href="https://uk.wikipedia.org/wiki/%D0%90%D1%80%D1%85%D1%96%D1%82%D0%B5%D0%BA%D1%82%D1%83%D1%80%D0%B0_%D1%81%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D0%B8_%D0%BA%D0%BE%D0%BC%D0%B0%D0%BD%D0%B4" target="_self">Instruction set architecture (Wikipedia, UK)</a>
A = B + C across all five types:
Stack (zero-address): PUSH B / PUSH C / ADD / POP A — 4 instructions
Accumulator (one-address): LOAD B / ADD C / STORE A — 3 instructions
Register-memory (two-address): MOV AX,[B] / ADD AX,[C] / MOV [A],AX — 3 instructions
Load-store (three-address): LW $t0,B / LW $t1,C / ADD $t2,$t0,$t1 / SW $t2,A — 4 instructions
Memory-memory (three-address): ADD [A],[B],[C] — 1 instruction
Key insight: More instructions does not mean slower. Load-store needs 4 instructions vs memory-memory's 1, but wins in practice because simple fixed-length instructions pipeline efficiently.Industry convergence: Both register-memory (x86) and register-register (ARM, RISC-V) dominate. Stack machines survive in VMs (JVM, .NET, WebAssembly). Modern x86 internally decomposes to RISC-like micro-ops.
CISC — make hardware do maximum work per instruction (bridge the semantic gap)
RISC — simplify each instruction to accelerate the whole pipeline (David Patterson, 1980)
VLIW — bundle multiple independent ops into one wide instruction; compiler does scheduling
VLIW examples: Intel Itanium (failed commercially, discontinued 2021), Elbrus, TI TMS320 DSPs. Advantages: simpler hardware, predictable performance. Disadvantages: compiler must find parallelism; NOPs waste space; binary incompatibility.
x86 (CISC outside, RISC inside): Since Pentium Pro (1995), x86 CPUs translate CISC instructions into RISC-like micro-ops internally. The CISC ISA is now a compression format.
ARM getting complex: ARMv8 adds crypto instructions, SVE vector extensions, Apple AMX coprocessor.
What matters now: Microarchitecture (cache, pipeline depth, branch prediction, OoO execution), not CISC vs RISC label.
Learn more:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Complex_instruction_set_computer" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Complex_instruction_set_computer" target="_self">CISC (Wikipedia)</a>
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Reduced_instruction_set_computer" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Reduced_instruction_set_computer" target="_self">RISC (Wikipedia)</a>
<br><a data-tooltip-position="top" aria-label="https://riscv.org/" rel="noopener nofollow" class="external-link is-unresolved" href="https://riscv.org/" target="_self">RISC-V</a> — open, royalty-free ISA; base RV32I has just 47 instructions
<br>Proposed by <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Michael_J._Flynn" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Michael_J._Flynn" target="_self">Michael Flynn</a> (1966). Classifies architectures by number of instruction streams and data streams:
Classical von Neumann sequential execution
One control unit, one processing unit, one data stream
Pipelining and superscalar are internal optimizations, not a change of category
Examples: Intel 8086, 80486, single-core CPUs without SIMD extensions
Multitasking on SISD = concurrency (interleaved), not parallelism (simultaneous) Multiple processors execute different instructions on the same data
Rarest category; used for fault tolerance (triple modular redundancy)
Examples: Space Shuttle flight computer (5 redundant GPCs voted on outputs), systolic arrays (Google TPUs use these for matrix multiply) One instruction applied to many data elements simultaneously
Exploits data parallelism (same operation on every element)
Ideal for: image/audio processing, 3D graphics, scientific computing, machine learning
SIMD in modern CPUs: x86 extensions (MMX 64-bit -&gt; SSE 128-bit -&gt; AVX 256-bit -&gt; AVX-512 512-bit), ARM extensions (NEON 128-bit, SVE 128-2048-bit).
GPUs = extreme-scale SIMD (NVIDIA H100: 16,000+ CUDA cores)
Auto-vectorization: Compilers can convert scalar loops to SIMD with -O3 Multiple independent processors, each with own instruction and data streams
Dominant paradigm for all modern parallel computing
Two subtypes by memory model:
SMP challenge: Cache coherence (MESI protocol); limits scalability beyond ~64-128 cores
MMP challenge: Explicit data distribution and communication; network latency
Hybrid systems: Modern supercomputers combine SMP within nodes + MMP between nodes + SIMD within GPU/AVX units
Learn more:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Flynn%27s_taxonomy" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Flynn%27s_taxonomy" target="_self">Flynn's taxonomy (Wikipedia)</a> RAM and RASP are foundational abstractions. RAM = Harvard (program separate from data); RASP = von Neumann (stored program). Foundation for algorithm analysis and real computer design. ISA is the contract between hardware and software. Defines data types, registers, addressing modes, instruction formats. Everything above sees the ISA; everything below implements it. Operand location shapes ISA design. Stack, accumulator, register-memory, load-store, and memory-memory architectures trade off code density, hardware complexity, and pipelining. CISC vs RISC is a philosophy, not a binary. Modern x86 (CISC) internally decodes to RISC-like micro-ops; ARM (RISC) adds increasingly complex extensions. Flynn's taxonomy classifies parallelism. SISD (sequential), SIMD (vector/GPU), MISD (rare, fault-tolerant), MIMD (multicore/distributed). Modern systems are MIMD + SIMD. Lecture 10: Modern specialized architectures — GPUs, DSPs, ASICs, FPGAs, TPUs, neuromorphic processors, and quantum computing concepts.
Lab work continues applying x86 assembly concepts from earlier lectures.
Think about:
How does the CISC/RISC distinction affect the assembly code you write in labs?
Where does your laptop's processor fall in Flynn's taxonomy? (Hint: MIMD with SIMD extensions.)
Why did ARM's RISC approach win the mobile market while x86's CISC approach dominates desktops?
]]></description><link>lectures/eng/l09-notes-eng.-isa-design-and-models-of-computation.html</link><guid isPermaLink="false">Lectures/ENG/L09-NOTES-ENG. ISA Design and Models of Computation.md</guid><pubDate>Tue, 10 Mar 2026 06:00:29 GMT</pubDate></item><item><title><![CDATA[L08-NOTES-ENG. Memory Systems IO Devices and Low-Level Data Structures]]></title><description><![CDATA[Quick reference for Lecture 8. See full materials in L08-ENG. Memory Systems IO Devices and Low-Level Data Structures.md.
Part 1 (Sections 2-6): I/O devices — buses, keyboard controller, IN/OUT instructions, scan codes, port map
Part 2 (Sections 7-11): Memory hierarchy — cache levels, speed/cost tradeoffs, memory types, virtual memory
Part 3 (Sections 12-15): Low-level data structures — arrays, structs, linked lists, string arrays in x86 assembly
Three levels of keyboard access: DOS (INT 21h) -&gt; BIOS (INT 16h) -&gt; Hardware (IN AL, 60h)
The IO/M control line distinguishes I/O accesses from memory accesses on the same physical bus.Learn more:
<a data-tooltip-position="top" aria-label="https://toshiba.semicon-storage.com/ap-en/semiconductor/knowledge/e-learning/micro-intro/chapter2/types-of-bus-lines.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://toshiba.semicon-storage.com/ap-en/semiconductor/knowledge/e-learning/micro-intro/chapter2/types-of-bus-lines.html" target="_self">Bus Line Types</a>
Two I/O ports, four logical registers (direction determines which):Status register (port 64h) bits:Keypress path: Physical key -&gt; matrix scan -&gt; debounce -&gt; scan code -&gt; serial transmission -&gt; 8042 output buffer (port 60h) -&gt; OBF flag set -&gt; IRQ1 -&gt; INT 09h handler reads port 60h -&gt; translates to ASCII -&gt; BIOS keyboard buffer -&gt; INT 16h returns result.Learn more:
<br><a data-tooltip-position="top" aria-label="https://www.basicinputoutput.com/2024/11/the-keyboard-controller-interface.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.basicinputoutput.com/2024/11/the-keyboard-controller-interface.html" target="_self">The Keyboard Controller Interface, 0x60/0x64</a>
<br><a data-tooltip-position="top" aria-label="https://deeprajbhujel.blogspot.com/2015/12/keyboard-and-display-controller.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://deeprajbhujel.blogspot.com/2015/12/keyboard-and-display-controller.html" target="_self">Keyboard and Display Controller 8279</a>
; IN — read from I/O port into accumulator
in al, 60h ; Immediate port (0-255) -&gt; AL
in ax, 40h ; Immediate port -&gt; AX (word)
mov dx, 03F8h
in al, dx ; Register-indirect (full 64K range) ; OUT — write accumulator to I/O port
out 61h, al ; Immediate port
mov dx, 03F8h
out dx, al ; Register-indirect Only AL/AX (accumulator) and DX (port register) can be used
IN/OUT do not affect FLAGS
I/O port reads bypass the cache — always go to the device
Polling pattern:read_loop: in al, 64h ; Read status register test al, 01h ; Bit 0: output buffer full? jz read_loop ; No data — keep polling in al, 60h ; Read scan code
Learn more:
<br><a data-tooltip-position="top" aria-label="https://www.plantation-productions.com/Webster/www.artofasm.com/DOS/pdf/apndxc.pdf" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.plantation-productions.com/Webster/www.artofasm.com/DOS/pdf/apndxc.pdf" target="_self">Art of Assembly — Appendix C (scan codes, PDF)</a> Make code = key pressed (e.g., A = 1Eh)
Break code = key released = make code + 80h (e.g., A release = 9Eh)
Extended keys use E0h prefix (e.g., Right Arrow = E0h 4Dh)
Key keyboard commands (written to port 60h):; Example: Turn on all 3 LEDs
wait_cmd: in al, 64h test al, 02h ; Wait for IBF = 0 jnz wait_cmd mov al, 0EDh ; "Set LEDs" command out 60h, al
wait_data: in al, 64h test al, 02h jnz wait_data mov al, 00000111b ; All 3 LEDs on out 60h, al
Learn more:
<br><a data-tooltip-position="top" aria-label="https://wiki.osdev.org/PS/2_Keyboard#Scan_Code_Set_1" rel="noopener nofollow" class="external-link is-unresolved" href="https://wiki.osdev.org/PS/2_Keyboard#Scan_Code_Set_1" target="_self">PS/2 Keyboard Scan Code Set 1</a>
<br><a data-tooltip-position="top" aria-label="https://wiki.osdev.org/%228042%22_PS/2_Controller" rel="noopener nofollow" class="external-link is-unresolved" href="https://wiki.osdev.org/%228042%22_PS/2_Controller" target="_self">"8042" PS/2 Controller</a>
CMOS uses index-data protocol: write register number to 70h, read/write value at 71h.The memory wall: CPU speed grew ~60%/year, DRAM latency improved ~7%/year. The gap is solved by the hierarchy.Locality of reference:
Temporal — recently accessed data will be accessed again soon (loop variables)
Spatial — nearby addresses will be accessed soon (array iteration)
Each level acts as a cache for the level below it. Registers -&gt; L1 -&gt; L2 -&gt; L3 managed by hardware; RAM &lt;-&gt; disk managed by OS (virtual memory).Learn more:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Memory_hierarchy" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Memory_hierarchy" target="_self">Memory Hierarchy (Wikipedia)</a> Cache line = 64 bytes on x86. Reading 1 byte loads the entire 64-byte block (exploits spatial locality).
Inclusion policies: Inclusive (L1 data duplicated in L2), Exclusive (disjoint), Non-inclusive (no strict rule).
MESI coherence protocol states: Modified, Exclusive, Shared, Invalid.
Average Access Time (AAT):AAT = hit_time + (miss_rate x miss_penalty)
Example (L1 miss=10%, L2 miss=1%, L3 miss=1%, RAM=50ns):
No cache: 50 ns
L1 only: 1 + 0.10 x 50 = 6 ns
L1+L2: 1 + 0.10 x 5.5 = 1.55 ns
L1+L2+L3: ~1.51 ns (33x improvement)
Miss types: Cold (first access), Capacity (cache full), Conflict (address collision).Learn more:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Cache_hierarchy" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Cache_hierarchy" target="_self">Cache Hierarchy (Wikipedia)</a>
<br><a data-tooltip-position="top" aria-label="https://hazelcast.com/foundations/caching/caching/" rel="noopener nofollow" class="external-link is-unresolved" href="https://hazelcast.com/foundations/caching/caching/" target="_self">What Is Caching? (Hazelcast)</a>
Key insight: No single technology wins on all metrics. The hierarchy stacks technologies, each at a similar cost but exponentially different in size/speed.Learn more:
<br><a data-tooltip-position="top" aria-label="https://www.enterprisestorageforum.com/hardware/types-of-computer-memory/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.enterprisestorageforum.com/hardware/types-of-computer-memory/" target="_self">9 Types of Computer Memory</a>
<br><a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/computer-memory/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/computer-memory/" target="_self">Computer Memory (GeeksforGeeks)</a>
Learn more:
<br><a data-tooltip-position="top" aria-label="https://www.computerlanguage.com/results.php?definition=RAM+cache" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.computerlanguage.com/results.php?definition=RAM+cache" target="_self">RAM Cache (Computer Language Co.)</a>
Virtual memory gives each process its own address space and allows total virtual addresses to exceed physical RAM.Key concepts:
Virtual address — address used by the program
Physical address — actual location in RAM
Page — fixed-size block (typically 4 KB); virtual page maps to physical frame
Page table — per-process lookup table: virtual page number -&gt; physical frame number
MMU (Memory Management Unit) — hardware that translates virtual -&gt; physical on every access
Address translation steps:
CPU issues virtual address
MMU splits it into virtual page number + offset within page
MMU looks up virtual page number in the page table
If present: combine physical frame number + offset = physical address (page hit)
If not present: page fault — OS loads the page from disk into RAM, updates page table, retries
TLB (Translation Lookaside Buffer): Hardware cache of recent page table entries. Hit rate &gt;99% in typical workloads. TLB miss requires page table walk (slow).Page fault handling: OS selects a victim page (LRU), writes it to disk if dirty, loads requested page, updates page table, restarts the faulting instruction.Learn more:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Virtual_memory" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Virtual_memory" target="_self">Virtual Memory (Wikipedia)</a>
.data byteArr DB 10, 20, 30, 40, 50 ; 5-byte array wordArr DW 100, 200, 300 ; 3-word array
Address formula: address = base + index x element_size mov al, byteArr[2] ; AL = 30 (byte: index * 1) mov ax, wordArr[4] ; AX = 300 (word: index 2 * 2 = offset 4)
Address formula: address = base + (row x COLS + col) x element_size.data matrix DB 10,20,30,40 ; Row 0 (COLS=4) DB 50,60,70,80 ; Row 1 DB 90,100,110,120 ; Row 2 ; Access matrix[1][2] -&gt; value 70: mov bx, offset matrix mov al, 1 ; row mov cl, 4 ; COLS mul cl ; AX = row * COLS = 4 add bx, ax ; skip to row 1 add bx, 2 ; column 2 mov dl, [bx] ; DL = 70
For word-sized elements, add SHL AX, 1 after computing the linear index.Learn more:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Row-_and_column-major_order" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Row-_and_column-major_order" target="_self">Row- and column-major order (Wikipedia)</a>
Defining a template (no memory allocated):Person STRUC age DB ? ; Offset 0: 1 byte height DW ? ; Offset 1: 2 bytes
Person ENDS ; Total: 3 bytes
Creating instances (allocates memory):.data person1 Person &lt;25, 180&gt; person2 Person &lt;30, 170&gt;
Accessing fields: mov person1.age, 20 ; Direct: [person1+0] mov person1.height, 180 ; Direct: [person1+1] mov bx, offset person1 mov al, [bx].age ; Via register pointer mov dx, [bx].height
Key insight: STRUC/ENDS = template (cookie cutter, no bytes emitted). Instance declaration = actual memory. Use SIZE Person for byte count, OFFSET Person.height for field offset.No alignment padding in 16-bit TASM — fields are packed exactly as declared (unlike 32/64-bit C compilers).Node definition (struct with embedded struct + pointer):Node STRUC value Person &lt;&gt; ; 3 bytes (embedded struct) next DW ? ; 2-byte pointer to next node
Node ENDS ; Total: 5 bytes
Allocation and linking:.data head DB SIZE Node DUP(0) node2 DB SIZE Node DUP(0) node3 DB SIZE Node DUP(0) .code mov head.value.age, 25 mov head.next, OFFSET node2 mov node3.next, -1 ; FFFFh = NULL sentinel
Traversal pattern: mov bx, OFFSET head
traverse_list: mov al, [bx].value.age ; Process current node add al, 1 mov [bx].value.age, al mov bx, [bx].next ; Follow pointer cmp bx, -1 ; NULL check jne traverse_list
Common pitfalls:
Dereferencing NULL — always check cmp bx, -1 before accessing fields
Overwriting next pointer before reading it — read [bx].next first, then modify node
Learn more:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Linked_list" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Linked_list" target="_self">Linked list (Wikipedia)</a>
Problem: Strings have variable length — cannot use a flat array.
Solution: Array of word-sized pointers to strings (indirection)..data STR_COUNT EQU 5 strArray DW STR_COUNT DUP(0) ; 5 word-sized pointers str1 DB "Hello, World!$" str2 DB "Assembly Language$" ; ... more strings ...
Initialize pointers: mov strArray[0], OFFSET str1 ; strArray[0] -&gt; str1 mov strArray[2], OFFSET str2 ; strArray[1] (offset 2 = index*2) ; ...
Print loop: mov cx, STR_COUNT xor si, si
print_loop: mov dx, strArray[si] ; Load pointer mov ah, 09h int 21h ; Print $-terminated string add si, 2 ; Next pointer (word size) loop print_loop
This pattern appears everywhere: C's argv[], environment variable blocks, ELF string tables, C++ vtables. The key: indirection (fixed-size pointers to variable-size data).Learn more:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Virtual_method_table" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Virtual_method_table" target="_self">Virtual method table (Wikipedia)</a> I/O is port-based on x86. CPU uses IN/OUT with a separate 64K I/O address space. Each device has assigned port ranges.
The keyboard controller is a case study in hardware interfaces. Ports 60h (data) and 64h (command/status) provide bidirectional communication — the raw interface that INT 16h abstracts away.
The memory hierarchy trades speed for capacity. Registers (&lt; 1 ns) -&gt; L1/L2/L3 cache (1-10 ns) -&gt; RAM (50-100 ns) -&gt; SSD (100 us) -&gt; HDD (10 ms) -&gt; tape (seconds).
Memory types differ along multiple axes. Volatility, access type (random/sequential/block), technology (SRAM/DRAM/Flash/magnetic).
Virtual memory provides isolation and flexibility. MMU translates virtual -&gt; physical through page tables. TLB caches translations with &gt;99% hit rate.
High-level data structures map directly to memory. Structs define field offsets; linked lists use pointers to chain nodes; string arrays use pointer arrays for variable-length data. Work directly with video memory at segment B800h in text mode
Write characters + attribute bytes directly (bypassing INT 10h)
Screen position formula: offset = (row x 80 + col) x 2
Video memory is memory-mapped I/O (use MOV, not IN/OUT) — contrast with port-mapped keyboard
The 2D screen (80 x 25) uses the same row-major addressing from Section 12
]]></description><link>lectures/eng/l08-notes-eng.-memory-systems-io-devices-and-low-level-data-structures.html</link><guid isPermaLink="false">Lectures/ENG/L08-NOTES-ENG. Memory Systems IO Devices and Low-Level Data Structures.md</guid><pubDate>Tue, 10 Mar 2026 06:00:29 GMT</pubDate></item><item><title><![CDATA[L08-ENG. Memory Systems IO Devices and Low-Level Data Structures]]></title><description><![CDATA[
Lecture 8 of Computer Systems Architecture course at NaUKMA.
After this lecture you will be able to:
Explain how the CPU communicates with I/O devices through the system bus and dedicated I/O ports
Describe the keyboard controller architecture (ports 0x60/0x64) and use the IN/OUT instructions to interact with hardware devices
Describe the memory hierarchy from registers through cache levels to main memory, secondary storage, and tape
Compare memory technologies (SRAM, DRAM, Flash, ROM) by speed, cost, volatility, and access type
Explain the concept of virtual memory, paging, and address translation through page tables
Implement fundamental data structures in x86 assembly: arrays, 2D arrays, structs, linked lists, and string arrays
Over the past five lectures, you have been working inside the CPU. You learned to load values into registers and address memory using segment:offset pairs (L03), manipulate data with arithmetic and logical instructions (L04), organize programs with the stack and procedures (L05), examine the system architecture surrounding the CPU (L06), and interact with the operating system through software interrupts (L07). Along the way, you built real programs that read keyboard input, printed text to the screen, performed recursive calculations, and handled multiple function calls with proper stack discipline — all in 16-bit Real Mode assembly.But there is a vast layer of complexity sitting between your assembly instructions and the physical world that we have not yet explored.Consider what actually happens when you press a key in DOSBox. Your program calls INT 16h and gets back a scan code in AH and an ASCII character in AL. That is the software abstraction — clean and simple. On the hardware side, something far more interesting occurred. A physical key closed a circuit on a matrix grid inside the keyboard. A dedicated microcontroller (not the main CPU — a separate processor inside the keyboard itself) scanned that matrix, detected the change, waited for the electrical signal to stop bouncing, translated the row-column position into a numeric scan code, and transmitted that code over a serial link to another controller chip on the motherboard. That motherboard controller stored the code in a one-byte buffer and raised an electrical interrupt signal on the IRQ1 line. The CPU paused your program, jumped to a BIOS handler at the address stored in IVT entry 09h, which read the scan code from I/O port 60h using the IN instruction, translated it into an ASCII character using lookup tables and the current shift/lock state, stuffed the result into a 16-element circular buffer in the BIOS Data Area, sent an end-of-interrupt signal to the interrupt controller, and returned via IRET — all transparently, before your INT 16h call returns with the result. Your program never touched any of that hardware directly. The BIOS did it for you, communicating with the keyboard controller through something called I/O ports.Or consider memory. When you write a value with MOV [BX], AX, you think of it as a single step — the value goes "to memory." But the reality is far more layered. On a modern processor, that write might first land in the L1 cache (a tiny, extremely fast SRAM buffer holding perhaps 32 KB of recently accessed data), and the cache controller must eventually propagate the data to the L2 cache, then to the L3 cache, and finally to main DRAM. Each level is larger but slower than the one above it. The DRAM itself is not a simple array of flip-flops — it is organized in rows and columns of capacitors that must be periodically refreshed (re-charged) every 64 milliseconds to avoid losing their contents. And if the data were stored on a hard drive rather than RAM, access would be roughly a million times slower.These are not esoteric details. The memory hierarchy directly affects program performance. A program that accesses data in patterns friendly to the cache can run 10 to 100 times faster than one that constantly causes cache misses. Understanding why requires knowing how caches work, what SRAM and DRAM actually are, and how the hierarchy from registers to disk is structured. This lecture gives you that understanding.And then there are data structures. In a high-level language like C, you write struct Student { char name[32]; int age; } and the compiler handles memory layout, alignment, and pointer arithmetic. In assembly, you are the compiler. You decide where each field goes, how many bytes it occupies, how to navigate from one struct to the next in a linked list, and how to index into an array of such structs. Understanding how data structures map to raw bytes is one of the most practical skills you will take away from this course — it deepens your understanding of every data structure you will encounter in future courses on algorithms, operating systems, and systems programming.Think of it this way: until now, you have been learning the language of the CPU — its registers, instructions, and calling conventions. This lecture teaches you the environment the CPU lives in: the devices it talks to, the memory hierarchy it relies on, and the data layouts it navigates. A programmer who knows only instructions is like a speaker who knows vocabulary but has never left their house. This lecture takes you outside.To give you a concrete sense of where we are headed, here is a single example that touches all three themes of this lecture.Suppose you want to write a program that reads a key from the keyboard and stores the character in a struct field. At the highest level of abstraction (C code), this looks trivial:struct Record { char key; int count; };
struct Record r;
r.key = getchar();
At the BIOS level (which you know from L07), the keyboard read becomes:mov ah, 00h
int 16h ; AL = ASCII character
But at the hardware level (which you will learn in this lecture), the same operation involves reading the keyboard controller directly:wait_key: in al, 64h ; Read keyboard status port test al, 01h ; Is there a byte waiting? jz wait_key ; No — keep polling in al, 60h ; Yes — read the scan code
And the struct access, which the C compiler hides behind r.key, becomes an explicit offset calculation:Record STRUC key db ? ; offset 0 count dw ? ; offset 1
Record ENDS mov [bx + Record.key], al ; Store character at offset 0 of struct
Each level strips away an abstraction and reveals the mechanism underneath. By the end of this lecture, you will be comfortable working at all three levels — and you will understand exactly how each one maps to the ones below it.<img alt="Network switch with Ethernet cables" src="assets/l08-network-switch.jpeg" target="_self">A network switch — one of many I/O devices that communicates with the CPU through the system bus.This lecture — "Memory Systems, I/O Devices, and Low-Level Data Structures" — bridges the gap between the CPU-centric view of earlier lectures and the full system perspective. We will open the hood on how the CPU communicates with external devices, map out the complete memory hierarchy from registers to tape storage, and then bring everything back to assembly programming by implementing high-level data structures (structs, linked lists, string arrays) directly in memory.The three themes of this lecture — I/O, memory, and data structures — are more interconnected than they first appear. I/O devices sit on the same bus as memory, and the CPU communicates with both using similar mechanisms (addresses and data transfers). The memory hierarchy determines how fast your program can access data structures. And the data structures themselves must be laid out in memory in a way that aligns with the hardware's access patterns. By the end of this lecture, you will see these connections clearly.The lecture is organized in three parts:Part 1: I/O Devices (Sections 2-6). We begin by revisiting the system bus from L06 and then focus on how I/O devices connect to it. Section 2 reviews the three bus types and introduces the distinction between memory-mapped and port-mapped I/O. Section 3 uses the keyboard controller as a detailed, concrete case study — tracing the complete path from a physical keypress to a scan code in the BIOS buffer. Sections 4-5 introduce the IN and OUT assembly instructions, keyboard commands, and scan code tables. Section 6 presents the standard I/O port address map of the IBM PC. By the end of Part 1, you will understand how to communicate with hardware directly — bypassing BIOS and DOS entirely.Part 2: Memory Hierarchy and Types (Sections 7-11). We expand the memory hierarchy introduced briefly in L06 into a thorough treatment. Section 7 motivates the hierarchy with the fundamental speed-capacity trade-off. Section 8 examines cache architecture in detail: L1, L2, and L3 levels, split vs. unified caches, and how multi-core processors share cache. Section 9 quantifies the performance gaps between hierarchy levels with concrete bandwidth and latency numbers. Section 10 compares memory technologies (SRAM, DRAM, Flash, ROM, magnetic, optical) by speed, cost, volatility, and access type. Section 11 introduces virtual memory and paging — the mechanism that lets modern operating systems give every process the illusion of having the entire address space to itself.Part 3: Low-Level Data Structures (Sections 12-15). We return to assembly programming with a practical focus: how do the data structures you know from high-level languages — arrays, 2D arrays, structs, and linked lists — actually exist in memory? Section 12 revisits 1D and 2D arrays (building on L05) with emphasis on row-major layout and address calculation. Section 13 introduces the TASM STRUC/ENDS directives for defining structs. Section 14 implements linked lists using pointers (segment:offset pairs) in assembly. Section 15 covers string arrays and tables of pointers. We implement each structure in x86 assembly, showing the exact memory layout and the instructions needed to create and traverse them.In Lab 8, you will work directly with I/O ports for the first time, reading keyboard scan codes using the IN instruction instead of relying on INT 16h. You will also work with text-mode video memory at B800:0000, writing characters and attribute bytes directly into the video buffer. These are not artificial exercises — they are exactly how real operating systems and game engines interact with hardware. The BIOS keyboard handler that INT 16h invokes does precisely what you will learn to do by hand in this lecture: read a byte from port 60h, check the status register at port 64h, and translate the result.Later labs will require you to define complex data structures in assembly: arrays of strings, structs with mixed field types, and linked lists built from dynamically allocated memory blocks. The TASM STRUC/ENDS directives and pointer-chasing patterns introduced in Part 3 of this lecture will be directly applicable to that work.Beyond the labs, the concepts in this lecture have lasting value. When you study operating systems in a future course, topics like virtual memory paging and device driver architecture will build directly on what you learn here. When you encounter a "segmentation fault" in a C program, the mental model of raw pointers, memory layout, and address spaces that you build here will help you diagnose it faster than any debugger output. And when you profile a slow program and discover that cache misses are the bottleneck, the memory hierarchy section of this lecture will explain exactly why.This lecture sits at a crossroads in the course. It draws on nearly every topic from the first seven lectures, while introducing several entirely new concepts. Here is a map of what connects to what:
Segment:offset addressing (L03) — I/O port addresses and physical memory addresses both rely on the addressing concepts established in L03. We will reference segment:offset pairs when discussing video memory (B800:0000) and memory-mapped regions.
Number representations (L02/L03) — hexadecimal notation is used extensively for port addresses, scan codes, and memory addresses throughout this lecture. Bit manipulation (AND, OR, TEST) is used to check status register flags.
Arrays and the stack (L05) — we revisit 1D and 2D array access patterns from L05 and extend them to structs and linked lists. Stack frame conventions remain relevant when passing structure pointers to procedures.
System architecture and buses (L06) — the system bus (data, address, control) was introduced in L06 Section 7. Here we build directly on that foundation to explain how I/O devices attach to the bus and how port-mapped I/O differs from memory-mapped I/O.
Memory hierarchy (L06) — L06 Section 9 introduced the hierarchy from registers to disk in a single summary table. This lecture expands that overview into a full treatment with separate sections on cache architecture, memory technologies, speed/latency comparisons, and virtual memory.
Interrupts and INT 16h (L07) — in L07, you used INT 16h to read keyboard input through the BIOS. Now you will see what happens underneath that interrupt: the keyboard controller hardware, the IN instruction that reads port 60h, and the IRQ1 handler that bridges hardware and software.
Scan codes and IRQ1 (L07) — L07 mentioned that IRQ1 triggers INT 09h, which reads a scan code from port 60h. This lecture explains the full journey of a scan code from physical keypress to the BIOS circular buffer, and shows you how to read the port directly.
If any of these prior topics feel rusty, now is a good time to review the relevant sections. The I/O port material (Part 1) leans most heavily on L07 (interrupts) and L06 (buses). The memory hierarchy material (Part 2) extends L06 Section 9. The data structures material (Part 3) builds on L05 (arrays, stack frames) and L03 (memory addressing).A CPU without I/O devices is a calculator in a sealed box — it can compute, but it cannot receive input or produce output. The entire purpose of a computer system is to interact with the world: reading keystrokes, displaying images, storing files, sending network packets. All of these interactions pass through the system bus, the electrical highway that connects the CPU to everything else.In L06 (Section 7), we established this architecture and covered the three types of buses — data, address, and control. That section focused on how memory connects to the bus. Now we shift focus to I/O devices: how they attach to the same bus, how the CPU addresses them, and what distinguishes an I/O access from a memory access.Recall from L06 that the system bus consists of three logical sub-buses, each carrying a different type of signal:<br><img alt="CPU-Memory-IO bus architecture" src="assets/l08-cpu-bus-diagram.png" target="_self">The three system buses — data, address, and control — connecting CPU, memory, and I/O devices.The three buses work in concert for every data transfer. To read a byte from memory address 1234h, the CPU places 1234h on the address bus, asserts the RD (read) signal on the control bus, and then reads the byte that appears on the data bus. To write, it places the address, puts the data on the data bus, and asserts the WR (write) signal. Every device on the system bus — RAM, ROM, video cards, keyboard controllers, disk controllers — shares the same physical wires. When the CPU wants to communicate with a specific device, it places an address on the address bus and asserts the appropriate control signals. Only the device that recognizes that address responds.This is analogous to a postal system: everyone shares the same postal network, but each house has a unique address. A letter addressed to "123 Main Street" reaches only that house, even though it travels through the same sorting facilities as letters to every other address. Similarly, the byte placed on the data bus reaches only the device whose address decoder matches the value on the address bus.One control line deserves special attention: IO/M (I/O or Memory). On the 8086, this signal indicates whether the current bus cycle is accessing the I/O address space or the memory address space. When the CPU executes a MOV instruction, it drives IO/M low (Memory). When it executes an IN or OUT instruction, it drives IO/M high (I/O). This single signal line is what allows the system to have two separate address spaces using the same physical address bus wires. The address 0060h can refer to a memory location or an I/O port — the IO/M signal determines which.With the bus architecture in mind, we can now ask a more specific question: how does the CPU distinguish between accessing memory and accessing a device? There are two fundamentally different models, and the x86 architecture supports both.Memory-mapped I/O. The device's registers are assigned addresses within the normal memory address space. The CPU uses ordinary MOV instructions to read from and write to these addresses, and the bus hardware routes the access to the device rather than to RAM. Video memory at B800:0000 is a prime example you are already familiar with: when you execute MOV ES:[DI], AX with ES pointing to segment B800h, the data does not go to a RAM chip — it goes to the video adapter, which immediately interprets the bytes as a character and attribute and displays them on screen. From the CPU's perspective, it looks exactly like a memory write. You could even use REP STOSW to fill the entire screen — the string instruction writes to consecutive memory addresses, and every write goes to the video adapter.In the Real Mode memory map (from L06 Section 10), the regions A0000h-BFFFFh (128 KB) are reserved for video memory. The hardware does not place RAM at these addresses; instead, the video adapter's memory is mapped there. Reading from these addresses reads the current contents of the video buffer; writing to them updates the display.The advantage of memory-mapped I/O is flexibility: any instruction that can reference memory — MOV, ADD, CMP, TEST, even string instructions like STOSW — can interact with the device. You do not need special instructions. The disadvantage is that the device's registers consume addresses in the memory map, reducing the space available for actual RAM. On the 8086 with its 1 MB address space, this trade-off matters: the top 384 KB is reserved for ROM, video memory, and device buffers, leaving only 640 KB for conventional RAM (the famous "640K barrier").Port-mapped I/O (isolated I/O). The device is assigned a separate address in a dedicated I/O address space, completely distinct from the memory address space. The x86 I/O address space is 16 bits wide, providing 65,536 possible port addresses (0000h-FFFFh). The CPU uses special instructions — IN and OUT — to communicate with these ports. Critically, the control bus carries a signal (IO/M on the 8086, or M/IO on later processors) that tells the bus hardware whether the current access targets memory or I/O space. This means address 60h in I/O space and address 0060h in memory space are completely different locations accessed through completely different mechanisms.<br><img alt="Bus diagram with I/O portion highlighted" src="assets/l08-cpu-bus-io-highlight.png" target="_self">The same bus architecture with the I/O device interface highlighted — input and output ports connect external devices to the system bus.The keyboard controller at ports 60h and 64h uses the port-mapped model. So do the Programmable Interval Timer (ports 40h-43h), the Programmable Interrupt Controller (ports 20h-21h), the DMA controller (ports 00h-0Fh), and the serial ports (ports 3F8h-3FFh for COM1). In contrast, video memory (B800:0000 for text mode, A000:0000 for graphics mode) uses memory-mapped I/O, as does the BIOS Data Area and the ROM BIOS itself.Most real x86 systems use a combination of both models. The general pattern is: large data buffers (like video memory) are memory-mapped because transferring thousands of bytes through IN/OUT one byte at a time would be painfully slow; control and status registers (like keyboard ports) are port-mapped because only a few bytes need to be exchanged and the explicit IN/OUT instructions make the code's intent clear.It is worth noting that many other processor architectures — ARM, MIPS, RISC-V — use only memory-mapped I/O. They do not have a separate I/O address space at all. Every device is accessed through normal load/store instructions at specific memory addresses. The x86 family's dual-model approach (both memory-mapped and port-mapped) is somewhat unusual, a legacy of the Intel 8080/8085 ancestry. In this lecture, we focus primarily on port-mapped I/O because it introduces the IN and OUT instructions that you will use in lab work. We will also briefly work with memory-mapped video memory in the context of text-mode display.Every I/O device — keyboard, mouse, disk drive, network card, serial port, sound card — has a device controller: a dedicated chip or circuit that translates between the device's physical interface and the system bus protocol. The CPU never talks to the raw hardware directly. It talks to the controller, which in turn manages the device.Consider a hard disk drive. The physical interface involves spinning magnetic platters at 7200 RPM, positioning read/write heads over specific tracks with micrometer precision, and interpreting magnetic flux patterns to recover stored data. The CPU knows nothing about any of this. Instead, the disk controller exposes a set of I/O ports (for an IDE/ATA drive: ports 1F0h-1F7h) where the CPU can write commands ("read sector 1234 from track 56, head 2") and read back data and status information. The controller handles all the mechanical and electrical details internally. The CPU just sends commands and receives results through a handful of ports — the same IN/OUT instructions used for the keyboard controller.This controller-based architecture provides hardware abstraction: the CPU communicates through standardized interfaces, insulated from the physical details of each device. A keyboard controller handles scanning a key matrix thousands of times per second, debouncing signals, and translating physical key positions into scan codes. The CPU only needs to read the final scan code from a single I/O port. If a manufacturer redesigns the keyboard's internal circuitry, the controller still presents the same ports to the CPU, and no software changes are needed.The standardization of controller interfaces is what made the IBM PC clone industry possible. Any keyboard that speaks the same protocol as the Intel 8042 controller works with any PC motherboard, regardless of who manufactured either component. The same principle applies to IDE/ATA disk controllers, VGA video adapters, and 16550 UART serial controllers. Standards at the controller level enabled an entire ecosystem of interchangeable hardware.Here is a summary of the most common I/O devices on an IBM PC and how they connect:Notice the pattern: control and status registers are port-mapped (accessed via IN/OUT), while large data regions like video memory are memory-mapped (accessed via MOV and other memory instructions). You will encounter many of these devices in later sections and labs. We will return to the full I/O port address map in Section 6.With this framework in mind, let us examine a specific controller in detail: the keyboard controller.You have been pressing keys and reading their values since Lab 4. In L07, you learned that INT 16h invokes a BIOS handler that returns a scan code and ASCII character. But what is really happening between the moment your finger presses a key and the moment that scan code appears in the AH register? The answer involves a dedicated piece of hardware — the keyboard controller — that performs a surprising amount of work entirely on its own, independent of the CPU.We use the keyboard as our case study for I/O device communication because it is concrete and familiar. Every student has interacted with a keyboard thousands of times. The keyboard is also architecturally representative: it uses port-mapped I/O, has both data and status registers, communicates through interrupts, and requires a specific command protocol. By the end of this section, you will understand the complete hardware path from keypress to software, and you will have a mental model that applies to virtually every other I/O device in a computer.<br><img alt="Mechanical keyboard with key switch" src="assets/l08-mechanical-keyboard.png" target="_self">A mechanical keyboard with a key switch removed — each key is a physical switch in a matrix of rows and columns.<br>The original IBM PC (1981) used an <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Intel_MCS-48" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Intel_MCS-48" target="_self">Intel 8048</a> microcontroller embedded inside the keyboard itself. This small 8-bit processor scanned the key matrix and sent scan codes to the motherboard over a simple unidirectional serial protocol. Communication was one-way: the keyboard sent data to the PC, but the PC could not send commands back to the keyboard. There were no keyboard LEDs to control, no way to adjust the key repeat rate, and no way for the system to reset the keyboard programmatically. If the keyboard encountered an error, the system had no way to recover without a physical power cycle.<br>The IBM PC/AT (1984) introduced a fundamentally improved design with the <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Intel_8042" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Intel_8042" target="_self">Intel 8042</a> — a dedicated keyboard controller chip on the motherboard. This made communication bidirectional: the CPU could now send commands to the keyboard (set LED states, change scan code sets, adjust the typematic repeat rate and delay), and the keyboard could send responses back (acknowledgment bytes, error codes, self-test results, device identification). The 8042 became the standard keyboard controller interface for all IBM PC-compatible systems. Its register layout — data at port 60h, status/command at port 64h — has remained the standard for over four decades. Even modern systems that have integrated the 8042's functionality into the chipset (or the CPU's own south bridge) still present the same ports at the same addresses for backward compatibility.The keyboard itself also contains a microcontroller — a descendant of the 8048, or a modern equivalent like an 8051. So the complete keyboard subsystem actually involves two processors communicating with each other over a serial link: the keyboard-internal controller that scans the matrix and generates scan codes, and the motherboard controller (8042) that receives those codes and makes them available to the CPU through I/O ports. Neither of these processors is the main CPU. The keyboard has its own firmware, its own scan loop, and its own internal state machine — it is a small embedded computer in its own right.The 8042 keyboard controller sits as an intermediary between the PC system bus and the keyboard cable. It exposes four logical registers through just two I/O port addresses, using the direction of access (read vs. write) to distinguish between them:<br><img alt="Keyboard controller block diagram" src="assets/l08-keyboard-controller-block.png" target="_self">The keyboard controller sits between the PC system bus and the keyboard, using ports 60h and 64h for data, commands, and status.Let us examine each in detail.Output Buffer (port 60h, read). When the keyboard sends a scan code, the 8042 controller places it in the output buffer. The CPU reads this buffer by executing IN AL, 60h. Despite the name "output buffer," it is an output from the controller's perspective — data flowing out of the controller toward the CPU. From the CPU's perspective, it is input data. This naming convention follows the device's viewpoint and is a common source of confusion. Remember: the controller outputs data to the CPU by placing it in this buffer.Input Buffer (port 60h, write). When the CPU wants to send a command or data to the keyboard device itself (not to the controller — that distinction matters), it writes to port 60h using OUT 60h, AL. The controller picks up the byte from this buffer and forwards it to the keyboard over the serial link. The keyboard processes the command and may send back an acknowledgment byte (FAh for most commands). Again, "input" follows the controller's viewpoint: the buffer accepts input from the CPU destined for the keyboard.Status Register (port 64h, read). This 8-bit register reports the controller's current state. Reading port 64h with IN AL, 64h returns the status byte. The bit layout is:The two critical bits for everyday programming are bits 0 and 1. Before reading port 60h, a well-written program checks bit 0 of the status register to confirm that a byte is actually waiting. Before writing to port 60h or 64h, it checks bit 1 to ensure the controller has consumed the previous byte. Ignoring these checks can lead to lost data or garbled commands. Here is the typical polling pattern:; Wait until the output buffer has data (OBF = 1)
wait_obf: in al, 64h ; Read status register test al, 01h ; Check bit 0 (OBF) jz wait_obf ; Loop if buffer empty in al, 60h ; Now safe to read the scan code
; Wait until the input buffer is empty (IBF = 0)
wait_ibf: in al, 64h ; Read status register test al, 02h ; Check bit 1 (IBF) jnz wait_ibf ; Loop if buffer still full out 60h, al ; Now safe to write data to keyboard
These busy-wait loops are simple but effective for the keyboard controller, which typically responds within a few microseconds. In a real operating system, you would use interrupts instead of polling, but for direct hardware access in a DOS program, polling works well.Command Register (port 64h, write). Writing to port 64h sends a command to the controller itself (not to the keyboard). These are controller-level commands that manage the keyboard interface:The distinction between writing to port 60h (data goes through the controller to the keyboard) and writing to port 64h (command goes to the controller itself) is critical. They are different operations that happen to share adjacent port numbers. Confusing the two will produce unpredictable behavior.The keyboard itself is built around a key matrix — a grid of rows and columns with a switch at each intersection. A typical full-size keyboard has about 104 keys, arranged in a matrix that might be 16 columns by 8 rows (though the actual layout varies by manufacturer). Each key sits at a unique row-column intersection. No two keys share the same intersection, which is what allows the controller to identify exactly which key was pressed.<br><img alt="Keyboard matrix scanning diagram" src="assets/l08-keyboard-matrix-block.png" target="_self">Inside the keyboard: a decoder drives rows, an encoder reads columns, and a counter cycles through the matrix to detect key presses.Here is the complete sequence of events when you press a key:Step 1: Physical contact. The key switch closes, connecting a specific row to a specific column in the matrix. In a mechanical keyboard, this happens when a spring-loaded switch mechanism (Cherry MX, for example) reaches its actuation point — typically about 2 mm of travel. In a membrane keyboard, a rubber dome collapses and presses two conductive membrane layers together. In either case, the result is the same: an electrical connection between one row and one column.Step 2: Matrix scanning. The keyboard's internal microcontroller continuously scans the matrix by driving each row with a voltage in sequence and reading the column lines. When it detects that a particular column is active while a particular row is being driven, it identifies the row-column intersection where the key was pressed. This scanning happens thousands of times per second — far faster than any human can type — ensuring that no keypress is missed, even during fast typing.Step 3: Debouncing. Mechanical switches do not make clean contact. When metal contacts come together, they "bounce" — alternating rapidly between open and closed states for several milliseconds as the contacts settle. Without compensation, a single keypress would register as two, three, or even more presses. The keyboard controller solves this by waiting for the signal to stabilize (typically 5-25 ms) before reporting the keypress. This delay is imperceptible to the user but essential for reliable input.Step 4: Scan code generation. The controller translates the row-column position into a scan code — a unique numeric identifier for that physical key. This is not an ASCII code. The scan code identifies the physical key position on the keyboard, not the character it represents. The same physical key produces the same scan code regardless of the active keyboard layout or language setting. For Scan Code Set 1 (the default set used in Real Mode on IBM PC compatibles):When a key is pressed, the keyboard sends the make code. When it is released, it sends the break code, which is the make code with bit 7 set (make code OR 80h). This make/break distinction is what allows the system to detect key combinations: when you press Shift+A, the system first receives the make code for Shift (2Ah), then the make code for A (1Eh). Because no break code for Shift has arrived, the system knows Shift is still held when A is pressed, and it generates an uppercase 'A'. When you release both keys, the corresponding break codes arrive.If you hold a key down, the keyboard begins repeating the make code after a configurable delay (the typematic delay, typically 250-1000 ms), then continues at a configurable rate (the typematic rate, typically 2-30 characters per second). These parameters can be adjusted by sending the F3h command to the keyboard through port 60h.Step 5: Serial transmission. The scan code is transmitted from the keyboard to the motherboard controller over a synchronous serial protocol using two lines: a clock line and a data line. Each byte is sent as 11 bits: 1 start bit (always 0), 8 data bits (LSB first), 1 odd parity bit, and 1 stop bit (always 1). The keyboard generates the clock signal at approximately 10-16 kHz.Step 6: Buffering. The 8042 controller on the motherboard receives the serial data, reassembles it into a byte, checks the parity, and places the byte in its output buffer (port 60h). It sets the OBF (Output Buffer Full) flag — bit 0 of the status register at port 64h.Step 7: Interrupt. The controller asserts the IRQ1 line, which triggers a hardware interrupt. As we learned in L07, IRQ1 maps to interrupt vector 09h through the Programmable Interrupt Controller (PIC). The CPU suspends whatever it was executing, pushes FLAGS, CS, and IP onto the stack, and jumps to the keyboard interrupt handler whose address is stored at IVT entry 09h (physical address 0000:0024h).The BIOS keyboard handler (INT 09h) performs several tasks:
Reads the scan code from port 60h using IN AL, 60h
Checks whether it is a make code or break code (bit 7)
Updates the shift/control/alt state flags in the BIOS Data Area (at 0040:0017h)
Translates the scan code into an ASCII character using lookup tables (considering the current shift state, Caps Lock, Num Lock, etc.)
Places the resulting scan code / ASCII pair into the BIOS keyboard buffer — a 16-element circular queue located in the BIOS Data Area at addresses 0040:001Eh through 0040:003Dh
Sends an End-of-Interrupt (EOI) signal to the PIC by writing 20h to port 20h
Returns via IRET
Step 8: Application reads the key. When your program calls INT 16h with AH=00h, the BIOS keyboard service checks the circular buffer. If a keystroke is available, it removes the oldest entry and returns the scan code in AH and the ASCII code in AL. If the buffer is empty, INT 16h enters a tight loop (HLT + check), waiting until IRQ1 delivers the next keystroke.This entire chain — from switch closure to your program receiving the scan code — typically completes in under a millisecond for the hardware portion (steps 1 through 7). The software response time depends on how quickly your program calls INT 16h. The keyboard controller handles steps 1-6 entirely autonomously; the CPU only becomes involved at step 7, when the interrupt fires. This is a fundamental pattern in computer architecture: devices do their own work and only bother the CPU when they have something to report.Understanding the keyboard controller is valuable far beyond keyboards themselves. The pattern you see here — a device controller with data and status ports, an interrupt-driven notification mechanism, and a software buffer managed by the OS — repeats across virtually every I/O device in a computer:
Disk controllers have command registers (where you specify which sector to read), data registers (where the sector data appears), and status registers (where you check whether the operation is complete). They signal completion via a hardware interrupt.
Network adapters have buffer memory for incoming packets, status registers with link state and error flags, and interrupts for packet arrival and transmission completion.
Serial ports (UARTs like the 16550) have transmit and receive data registers, line status registers with "transmitter empty" and "data ready" flags, and interrupts in both directions.
Sound cards have registers for setting sample rates, buffer addresses, and playback control, with interrupts to signal when the buffer needs refilling.
Once you understand the keyboard controller's architecture — data port, status port, command port, interrupt-driven notification, software buffer — you have a template for understanding all hardware I/O. The specific details change (different port numbers, different status bit layouts, different command sets), but the fundamental pattern remains identical.In the next two sections, we will write assembly code that communicates directly with the keyboard controller using IN and OUT instructions, bypassing the BIOS entirely. You will see that direct hardware access is not dramatically more complex than using BIOS services — it just requires knowing the port numbers and respecting the protocol.So far in this course, you have been communicating with hardware through layers of abstraction. When you called INT 21h to print a string, DOS called a BIOS routine, which eventually talked to the video hardware. When you called INT 16h to read a keypress, BIOS handled the keyboard controller on your behalf. But what does "talking to hardware" actually mean at the lowest level? The answer involves two instructions you have not yet seen: IN and OUT.The CPU needs a way to exchange data with peripheral devices — the keyboard controller, the timer, the serial port, the disk controller, and so on. Computer architects have devised two fundamentally different approaches to this problem.<br>Memory-mapped I/O treats device registers as if they were memory locations. You already used this approach when writing directly to <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/VGA_text_mode" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/VGA_text_mode" target="_self">video memory at B800:0000</a> in text mode — every MOV to that address range didn't write to RAM at all; it sent data to the video adapter. The advantage is simplicity: any instruction that reads or writes memory can also interact with hardware. The disadvantage is that those memory addresses are "used up" and cannot hold normal data.Port-mapped I/O (also called isolated I/O) gives devices their own separate address space, completely independent of the memory address space. The CPU uses dedicated instructions — IN and OUT — to access this space. The x86 architecture provides a 64K I/O port address space (ports 0x0000 through 0xFFFF), where each port is like a tiny mailbox connected to a specific device register.Here is a side-by-side comparison:The x86 platform uses both approaches simultaneously. Video memory is memory-mapped. The keyboard controller, the timer chip, the interrupt controller, and most legacy devices are port-mapped. This dual approach is a historical artifact of the IBM PC design, and understanding it is essential for systems programming.Other architectures make different choices. ARM processors use exclusively memory-mapped I/O — they have no IN/OUT instructions at all. Every device register is accessed through regular load and store instructions to specific memory addresses. The x86 approach of maintaining two separate address spaces is somewhat unusual in the broader landscape of computer architecture, but it has the benefit of keeping the I/O space clearly separated from application memory.The IN instruction reads a byte (or word) from an I/O port into the accumulator register:in al, 60h ; Read one byte from port 0x60 into AL (immediate port, 0-255)
in ax, 40h ; Read one word (16 bits) from port 0x40 into AX mov dx, 03F8h ; Port 0x03F8 (COM1 serial port)
in al, dx ; Read one byte from the port specified in DX
There are two forms. The immediate form (IN AL, port) encodes the port number directly in the instruction as a single byte, so it only works for ports 0 through 255. The register-indirect form (IN AL, DX) takes the port number from the 16-bit DX register, which allows access to the full range of 65,536 ports. In practice, most common devices live in the low port range (below 0x100), so you will see both forms used frequently.Why must DX be the register for the indirect form? This is not an arbitrary choice. The x86 instruction encoding reserves a specific opcode for IN AL, DX (opcode ECh) and another for IN AX, DX (opcode EDh). Only DX can be used — not BX, not SI, not any other register. Similarly, the accumulator (AL for bytes, AX for words) is the only valid destination. These constraints are wired into the CPU's instruction decoder.The OUT instruction writes a byte (or word) from the accumulator to an I/O port:out 61h, al ; Write AL to port 0x61 (immediate port, 0-255)
out 43h, al ; Write AL to port 0x43 (PIT command register) mov dx, 03F8h ; Port 0x03F8 (COM1 serial port)
out dx, al ; Write AL to the port specified in DX
The same two forms apply: immediate for ports 0-255, and register-indirect via DX for the full range. Note the operand order is reversed compared to IN — the port comes first, the register second (for the immediate form). This is consistent with the "destination, source" convention used by most x86 instructions, where the port is the destination you are writing to.One important detail: IN and OUT do not affect the FLAGS register. They perform no comparison, set no zero flag, and trigger no overflow. They simply transfer data between the CPU and the I/O bus. If you need to check the value you just read (which you almost always do), you must use a separate instruction like TEST or CMP afterward.When the CPU executes an IN or OUT instruction, what happens at the hardware level is fundamentally different from a memory access. For a regular MOV instruction that reads memory, the CPU puts an address on the address bus, asserts the memory-read control line, and reads the data bus. For an IN instruction, the CPU puts the port number on the address bus but asserts the I/O read control line instead. This separate control signal is what distinguishes I/O accesses from memory accesses, even though they share the same address and data buses.The external hardware (the chipset, in modern systems) sees the I/O read signal and routes the request to the appropriate device based on the port address. Historically, this was done by discrete logic chips that decoded the address lines. On modern systems, the Platform Controller Hub (PCH) handles this routing internally. But the principle is the same: the port address acts like a device selector, and the I/O control signal tells the hardware "this is not a memory access."This is also why port-mapped I/O does not interact with the CPU cache. Memory reads may be served from L1/L2/L3 cache, but I/O port reads always go all the way to the device. Every IN instruction generates an actual bus transaction to the hardware. This makes I/O inherently slower than cached memory access, but it guarantees that you always read the device's current state.<br>Let us see IN in action. In Lecture 7, you learned that the keyboard generates an <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Interrupt_request" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Interrupt_request" target="_self">IRQ1 hardware interrupt</a> when a key is pressed, and the BIOS interrupt handler reads the scan code and places it in the keyboard buffer. But what if you want to bypass the BIOS entirely and read the keyboard hardware directly? Here is how:; Poll the keyboard controller and read a scan code directly
read_loop: in al, 64h ; Read keyboard status register (port 64h) test al, 1 ; Check bit 0: output buffer full? jz read_loop ; If bit 0 = 0, no data ready — keep polling in al, 60h ; Bit 0 = 1: scan code is waiting — read it from port 60h ; AL now contains the scan code of the key that was pressed or released
<br><img alt="TASM code reading keyboard scan codes" src="assets/l08-asm-read-scancode.png" target="_self">Assembly code that polls the keyboard status register (port 64h) and reads scan codes directly from port 60h.This pattern — check status, then read data — is fundamental to all port-mapped I/O. The keyboard controller has two ports that matter: port 64h is the status register (read-only from the CPU's perspective), and port 60h is the data register. Here is what the status register bits mean:Bit 0 is the one we check before reading: it tells you whether the controller has placed a byte in the output buffer for the CPU to read. If you read port 60h when bit 0 is clear, you will get stale data from the previous read. Bit 1 is the one we check before writing (as you will see in Section 5): it tells you whether the controller has finished processing the previous command byte.You might wonder: if we can poll the keyboard with a tight loop like this, why do we need interrupts at all? The answer is efficiency. Consider: a typical typist presses about 5-10 keys per second. A CPU running at even 4.77 MHz (the original IBM PC speed) executes millions of instructions per second. A polling loop would check the status port hundreds of thousands of times between each keypress, wasting all those CPU cycles on a bit that almost never changes. Hardware interrupts (IRQ1 for the keyboard) let the CPU do useful work and only respond when the keyboard actually has data.The BIOS keyboard handler, triggered by IRQ1, reads port 60h, translates the scan code, and stores it in the BIOS keyboard buffer — all without your program having to poll. Your program can focus on computation, drawing, or anything else, and only check the keyboard buffer (via INT 16h) when it is ready to process input.Direct port access via IN/OUT is still valuable in specific scenarios: writing device drivers, operating system kernels, bootloaders, or embedded firmware where you cannot rely on BIOS services. It is also indispensable for hardware that the BIOS does not know about, or when you need more control than the BIOS abstraction provides — as we will see in the next section with keyboard LED control.In Protected Mode operating systems like Linux and Windows, user programs are not allowed to execute IN and OUT instructions. Attempting to do so triggers a General Protection Fault (exception 13). Only the kernel (running at privilege level 0, or "Ring 0") can access I/O ports directly. This is an important security boundary: imagine the damage a malicious program could do if it could write arbitrary bytes to the disk controller or reprogram the interrupt controller. The OS mediates all hardware access through device drivers, which run in kernel mode.Now that you know how to read the keyboard controller's data port, let us look more closely at what that data actually means — and how to send commands back to the keyboard.<br>When you press a key on the keyboard, the keyboard's internal <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Intel_8042" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Intel_8042" target="_self">microcontroller</a> does not send an ASCII character. It sends a scan code — a number that identifies the physical key on the keyboard, regardless of what letter or symbol is printed on the keycap. This is a crucial distinction: the keyboard hardware knows nothing about characters, languages, or shift states. It only knows which physical key went down or came up.Why not just send ASCII characters? Consider: the keyboard has keys like Shift, Ctrl, Alt, F1-F12, arrow keys, and Num Lock — none of which correspond to printable characters. Moreover, the same physical key produces different characters depending on whether Shift is held (e.g., 'a' vs 'A', '1' vs '!'). The keyboard cannot know the correct character because it does not know the keyboard layout (QWERTY, AZERTY, Dvorak) or the current Shift state of the software. Scan codes solve this cleanly: the keyboard reports physical events (key down, key up), and the software decides what they mean.Every key has two scan codes:
A make code is sent when the key is pressed down. For example, the 'A' key sends make code 1Eh.
A break code is sent when the key is released. The break code is always the make code with bit 7 set (i.e., make code + 80h). So releasing 'A' sends 9Eh.
This make/break system is what allows the software to detect key combinations. When you press Ctrl+C, the system sees: make code for Ctrl (1Dh), then make code for C (2Eh). When you release them, it sees break codes 9Dh and AEh. The keyboard sends a continuous stream of these events, and it is up to the software (BIOS handler, OS driver, or your own code) to interpret them.If you hold a key down, the keyboard begins sending the make code repeatedly after a short delay — this is the typematic (auto-repeat) behavior. The stream looks like this: 1Eh... (pause ~500ms) ...1Eh, 1Eh, 1Eh, 1Eh... (repeated at ~10/sec) ...9Eh (release). You can actually configure the repeat rate and delay by sending a command to the keyboard, as we will see shortly.Extended keys on the 101/102-key keyboard (the arrow keys, Insert, Delete, Home, End, Page Up/Down, and the right Ctrl and Alt keys) send two-byte scan codes: an E0h prefix byte followed by the actual code. For example:The E0h prefix tells the keyboard handler that an extended key is involved, not the regular key that shares the same second byte. For instance, the numpad '4' key and the Left Arrow key would both send 4Bh without the prefix — the E0h distinguishes them. The Pause/Break key is even more unusual — it sends a multi-byte sequence (E1h 1Dh 45h) and has no break code at all.<br>The following table shows the standard <a data-tooltip-position="top" aria-label="https://wiki.osdev.org/PS/2_Keyboard#Scan_Code_Set_1" rel="noopener nofollow" class="external-link is-unresolved" href="https://wiki.osdev.org/PS/2_Keyboard#Scan_Code_Set_1" target="_self">scan code set 1</a> used by IBM PC-compatible keyboards. Each physical key position has a unique number:<br><img alt="PC Keyboard Scan Codes table" src="assets/l08-scancode-table.png" target="_self">Table 90 from Art of Assembly: PC keyboard scan codes showing make (key-down) and break (key-up) values in hexadecimal.Some important scan codes to know:Notice that scan codes are assigned by physical key position on the keyboard, not by alphabetical order. The number row is 02h-0Bh, the top letter row (QWERTY) is 10h-19h, the home row (ASDF) is 1Eh-28h, and the bottom row (ZXCV) is 2Ch-35h. This reflects the order in which the keyboard scans its key matrix — hence the name "scan code."Remember from Lecture 7: when you called INT 16h, AH=00h, the BIOS returned the scan code in AH and the ASCII character in AL. Now you understand what the BIOS was doing behind the scenes — it read the raw scan code from port 60h, looked up whether Shift or Ctrl was held (by tracking make/break codes for modifier keys), and translated the result into an ASCII character for your convenience.This layering explains a subtle detail you may have noticed: INT 16h function 00h only recognizes 83 keys (the original PC/AT keyboard), while function 10h recognizes the full 101/102-key extended keyboard. The difference is that function 10h properly handles the E0h-prefixed extended scan codes. If you use function 00h with an extended keyboard, pressing keys like the right Ctrl or the arrow keys may produce unexpected results. This is why modern programs should use function 10h instead.Communication with the keyboard is bidirectional. Not only does the keyboard send scan codes to the CPU, but the CPU can send commands to the keyboard's microcontroller by writing to port 60h. This allows you to control keyboard behavior: set the LED indicators, change the repeat rate, enable or disable the keyboard, or even reset it.This bidirectional nature is why the status register has two separate buffer-status bits. Bit 0 (Output Buffer Full) tells the CPU "I have data for you to read." Bit 1 (Input Buffer Full) tells the CPU "I am still processing your last write — do not send anything yet." The terms "output" and "input" are from the controller's perspective, not the CPU's: the controller's "output buffer" is what the CPU reads, and the controller's "input buffer" is what the CPU writes to.Before sending a command, you must check that the keyboard controller is ready to receive data. Bit 1 of the status register (port 64h) indicates whether the input buffer is full. You must wait until this bit is clear before writing:; Set keyboard LEDs: turn on all three (Scroll Lock, Num Lock, Caps Lock)
wait_cmd: in al, 64h ; Read status register test al, 10b ; Check bit 1: input buffer full? jnz wait_cmd ; If full, wait mov al, 0EDh ; Command 0xED = "Set LEDs" out 60h, al ; Send command to keyboard wait_data: in al, 64h ; Read status again test al, 10b ; Wait for input buffer to empty jnz wait_data mov al, 00000111b ; Data byte: bit 0 = Scroll Lock, bit 1 = Num Lock, bit 2 = Caps Lock out 60h, al ; Send LED state to keyboard
<br><img alt="TASM code setting keyboard LEDs" src="assets/l08-asm-set-leds.png" target="_self">Assembly code that sends the Set LEDs command (0xED) to the keyboard via port 60h, turning on all three indicator LEDs.This is a two-step protocol: first send the command byte (0xED), then send the data byte (07h to light all three LEDs, or 00h to turn them all off). The LED data byte format is:So 00000101b (05h) turns on Scroll Lock and Caps Lock but leaves Num Lock off. 00000010b (02h) turns on only Num Lock — which is the default state on most systems at boot.The keyboard controller acknowledges each byte by sending back 0xFAh (ACK) through port 60h. A robust program would read this acknowledgment and check for errors (the keyboard sends 0xFEh to request a resend if it did not understand the command). In simple programs, the acknowledgment is often ignored, but production-quality keyboard drivers always check it.The keyboard microcontroller recognizes a number of commands beyond LED control:<br><img alt="Keyboard microcontroller commands" src="assets/l08-keyboard-commands-table.png" target="_self">Keyboard microcontroller commands (port 60h) — the LED command (0xED) and other frequently used commands.<br><img alt="Full keyboard command reference" src="assets/l08-keyboard-commands-full.png" target="_self">The complete keyboard command reference (Table 95) covering commands 0xED through 0xFF.The Set Typematic Rate command (0xF3) is particularly interesting — it lets you control how fast held keys repeat. The data byte encodes both the repeat delay and the repeat rate in a single byte:Bits 6-5: Delay before repeat begins 00 = 250 ms 01 = 500 ms 10 = 750 ms 11 = 1000 ms Bits 4-0: Repeat rate 00000 = 30.0 chars/sec (fastest) 01111 = 8.0 chars/sec 11111 = 2.0 chars/sec (slowest)
For example, sending 0x00 after 0xF3 gives the fastest possible repeat: 250ms delay then 30 characters per second. Sending 0x7F gives the slowest: 1000ms delay then 2 characters per second. You may have noticed that different computers seem to have different key repeat speeds — this is the command that controls it. Many BIOS setup utilities include a "Typematic Rate" setting that sends this exact command during boot.<br>These commands reveal something important about the keyboard: it is not a passive device. It contains its own microcontroller (historically the <a data-tooltip-position="top" aria-label="https://wiki.osdev.org/%228042%22_PS/2_Controller" rel="noopener nofollow" class="external-link is-unresolved" href="https://wiki.osdev.org/%228042%22_PS/2_Controller" target="_self">Intel 8042</a> or compatible) that runs its own firmware, maintains its own state, and responds to a protocol of commands and acknowledgments. The CPU and keyboard are two independent processors communicating over a shared bus.This architecture — a main processor communicating with intelligent peripheral controllers through a command/response protocol — is a recurring pattern in computer systems. Your hard disk has its own controller. Your network adapter has its own processor. Even your USB hub has a microcontroller. The IN/OUT instructions you learned in this lecture are the simplest form of this inter-processor communication. More modern interfaces like PCI and USB use more sophisticated protocols, but the fundamental idea — send a command, wait for a response, read the data — remains the same.You have now seen how the CPU uses IN and OUT to communicate with the keyboard controller through ports 60h and 64h. But the keyboard is just one of many devices that live in the I/O port address space. Let us zoom out and look at the full picture.<br>The x86 I/O address space spans 65,536 ports (0x0000 through 0xFFFF), completely separate from the memory address space. Over the decades since the IBM PC, <a data-tooltip-position="top" aria-label="https://wiki.osdev.org/I/O_Ports" rel="noopener nofollow" class="external-link is-unresolved" href="https://wiki.osdev.org/I/O_Ports" target="_self">standard port assignments</a> have been established so that every device gets its own range:<br><img alt="I/O port address ranges" src="assets/l08-io-port-address-list.png" target="_self">Standard I/O port address assignments on the IBM PC — from the DMA controller at 0x0000 through the keyboard controller at 0x0060–0x0064.Several of these devices should be familiar from earlier lectures. The PIC (Programmable Interrupt Controller) at ports 20h-21h is the chip that manages hardware interrupts — you learned in Lecture 7 that IRQ1 (keyboard) maps to INT 09h through the PIC. The PIT (Programmable Interval Timer) at ports 40h-47h is the system timer that generates IRQ0 at approximately 18.2 times per second — this is the heartbeat of the system, used by DOS to update the time-of-day counter at BIOS Data Area address 0040:006Ch. The DMA Controller at ports 00h-1Fh allows devices like the floppy drive to transfer data directly to memory without involving the CPU, which we will discuss in more detail in a later lecture.Notice the pattern: most of the critical system devices cluster in the low port range (below 0x0400). This is the legacy I/O space inherited from the original IBM PC. The first 1,024 ports (0x0000-0x03FF) were defined by the original IBM PC/AT specification, and every PC-compatible computer since 1984 has maintained these assignments. This is why you can write IN AL, 60h on any x86 PC and be confident you are talking to the keyboard controller — it has been at that address for over 40 years.Higher port ranges (0x0400 and above) are used by add-on cards such as sound cards, secondary disk controllers, and network adapters. The classic Sound Blaster card, for example, used ports 220h-22Fh. Network cards (NE2000 compatibles) typically used 300h-31Fh. These addresses were usually configurable via jumpers or DIP switches on the card — and getting them right was a notorious source of hardware conflicts in the DOS era (the dreaded "IRQ and I/O address conflicts").On modern systems, many of these devices have migrated away from port-mapped I/O entirely, using memory-mapped I/O through the PCI/PCIe configuration space instead. But the core legacy devices — the PIC, PIT, keyboard controller, and CMOS — remain at their original port addresses, even in the newest x86 processors. Backward compatibility is the defining characteristic of the x86 platform.Each device at its port range speaks its own protocol. The keyboard controller uses a status-then-data pattern (check port 64h, read/write port 60h). The PIT uses a command port (43h) and three counter ports (40h-42h). The PIC has an initialization sequence involving multiple writes to ports 20h and 21h. The CMOS/RTC uses an index-data pattern: write a register number to port 70h, then read or write the value at port 71h.You do not need to memorize all of these protocols. What matters is the mental model: the I/O port space is a bus with many devices connected to it, each listening on its own address range. The CPU uses IN and OUT to send and receive individual bytes. Everything else — the meaning of those bytes, the sequence of reads and writes, the timing requirements — is defined by the specific device's datasheet.<br>Here is a quick example of reading the <a data-tooltip-position="top" aria-label="https://wiki.osdev.org/CMOS" rel="noopener nofollow" class="external-link is-unresolved" href="https://wiki.osdev.org/CMOS" target="_self">CMOS Real-Time Clock</a> to get the current hour, just to show that the same IN/OUT pattern works for any device:; Read the current hour from the CMOS Real-Time Clock
mov al, 04h ; Register 04h = hours
out 70h, al ; Write register number to CMOS index port
in al, 71h ; Read the value from CMOS data port
; AL now contains the current hour in BCD format (e.g., 14h = 2:00 PM)
Two instructions. That is all it takes to read the time from the hardware clock. The CMOS uses an index-data protocol: write the register number you want to port 70h, then read (or write) the value at port 71h. Simple, but effective.The CMOS chip contains 128 bytes of battery-backed memory. Registers 0x00 through 0x09 hold the real-time clock values (seconds, minutes, hours, day, month, year). Registers 0x10 through 0x3F hold BIOS configuration data — the floppy drive type, hard disk parameters, amount of installed memory, and boot device order. This is the data you configure in the BIOS Setup utility (the screen you reach by pressing F2 or Del during boot). Every time you change a BIOS setting, the firmware writes to CMOS ports 70h and 71h.You might wonder why we spend time on port-mapped I/O when modern operating systems abstract all of this behind device drivers. There are several reasons. First, understanding IN/OUT is essential if you ever work on operating system kernels, embedded systems, or bootloaders — all environments where there are no drivers yet, and you must talk to hardware directly. Second, it completes your mental model of the system. You now understand the full chain from application code down to the physical bus transactions. Third, the concept of device registers accessed through a protocol of reads and writes applies universally, even on systems that use memory-mapped I/O instead of port-mapped I/O. The registers have different addresses, but the patterns — status polling, command/data pairs, index/data pairs — are the same everywhere.Let us tie together everything from this section of the lecture by tracing the complete journey of a single keypress:
Physical key press — your finger pushes the 'A' key on the keyboard
Key matrix — the keyboard's internal circuitry detects which row and column closed
Keyboard microcontroller — the 8042 (or compatible) identifies the key, generates scan code 1Eh, and places it in its output buffer
Signal to CPU — the keyboard controller asserts the IRQ1 line, which reaches the PIC (port 20h/21h), which signals the CPU's interrupt pin
CPU responds — the CPU finishes its current instruction, pushes FLAGS/CS/IP onto the stack, and looks up IVT entry 09h (IRQ1 maps to INT 09h)
BIOS handler — the INT 09h handler executes IN AL, 60h to read the scan code from the keyboard data port
Translation — the handler checks the state of modifier keys (Shift, Ctrl, etc.) and translates the scan code to an ASCII character
<br>Keyboard buffer — the scan code and ASCII character are stored in the <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/BIOS_interrupt_call" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/BIOS_interrupt_call" target="_self">BIOS keyboard buffer</a> (a circular buffer in the BIOS Data Area at 0040:001Eh)
Application reads — your program calls INT 16h, AH=00h, which pulls the next entry from the keyboard buffer and returns it in AX
Alternatively, if you bypass the BIOS entirely (steps 6-9), you can poll port 64h and read port 60h directly — which is exactly what we did in the polling example earlier.Notice how many concepts from earlier lectures converge in this single pathway: the stack (step 5 — FLAGS/CS/IP are pushed), the IVT (step 5 — looking up the handler address), hardware interrupts (step 4 — IRQ1 and the PIC), I/O ports (step 6 — IN from port 60h), and the segment:offset memory model (step 8 — the buffer at 0040:001E). A single keypress exercises nearly every layer of the system we have studied.This end-to-end journey illustrates a central theme of this course: computer systems are built in layers. Physical signals become electrical pulses, which become scan codes, which become interrupt requests, which become buffer entries, which become the characters your program processes. Each layer provides an abstraction that simplifies the work of the layer above it — and each layer can be peeled back when you need direct control.To consolidate what you have learned across Lectures 7 and 8, here are the three levels at which you can interact with the keyboard, from highest abstraction to lowest:Each level builds on the one below it. DOS calls BIOS. BIOS reads hardware ports. The hardware ports connect to the keyboard controller's registers. Now you understand all three levels.In a modern operating system like Linux or Windows, there is a fourth level — the OS keyboard driver — sitting between the hardware and the application. The driver reads scan codes (like the BIOS handler), translates them using a configurable keymap (supporting any language or layout), and delivers the result to applications through an event queue. The BIOS keyboard buffer is no longer used. But the fundamental hardware interface — IN from port 60h, status at port 64h — remains exactly the same as it was on the original IBM PC in 1981.In Lecture 6, we introduced the memory hierarchy as a key architectural concept — a layered system of storage that bridges the enormous speed gap between the CPU and bulk storage. We noted the basic levels (registers, cache, RAM, disk) and their rough access times. Now it is time to go much deeper. Understanding the memory hierarchy is not just an academic exercise — it is one of the most practically important topics in computer architecture, because the way your program accesses memory can make a 100x or even 1000x difference in performance.<br><img alt="Motherboard closeup" src="assets/l08-motherboard-closeup.jpeg" target="_self">A motherboard with CPU socket and surrounding components — the physical realization of the memory hierarchy, with cache on the CPU die and RAM slots nearby.Here is the fundamental problem. Modern CPUs can execute billions of operations per second. A single clock cycle on a 4 GHz processor takes 0.25 nanoseconds. But accessing main memory (DRAM) takes roughly 50–100 nanoseconds — that is 200 to 400 clock cycles of waiting. If the CPU had to go to main memory for every instruction and every piece of data, it would spend the vast majority of its time doing absolutely nothing, just waiting for data to arrive.This mismatch between processor speed and memory speed is known as the memory wall. It has been a central challenge in computer design since the 1980s, when CPU clock speeds began to outpace DRAM improvements. Processors got faster at a rate of roughly 60% per year, while DRAM latency improved at only about 7% per year. The gap kept widening, and architects had to find a solution.To put this in perspective: if you were a CPU core running at 4 GHz, and each clock cycle took one second of your subjective time, then waiting for main memory would feel like waiting three to five minutes. Waiting for an SSD would feel like waiting several days. Waiting for a hard drive would feel like waiting several months. The CPU is absurdly fast compared to the storage it depends on.The memory wall is not just a historical curiosity — it continues to shape processor design today. Modern CPUs dedicate a staggering amount of their transistor budget to caches and memory management. On many contemporary processors, more than half the die area is occupied by cache memory. Look at a die photo of any modern CPU, and you will see that the actual computation logic (ALUs, execution units) occupies a surprisingly small fraction of the chip — the rest is predominantly cache.The memory wall is also the primary reason CPUs have become so complex: techniques like out-of-order execution, speculative execution, and hardware prefetching all exist largely to keep the CPU busy while waiting for memory. Out-of-order execution allows the CPU to work on independent instructions while a memory request is pending. Speculative execution lets the CPU guess which branch a program will take and start working on it before the answer is known. Hardware prefetching detects sequential access patterns and loads data into cache before the program requests it. All of these techniques add enormous complexity to the processor, but they are necessary to mitigate the memory wall.The solution exploits a fortunate property of real programs: they do not access memory randomly. Instead, programs exhibit locality of reference — they tend to access the same data and nearby data repeatedly over short periods of time. This locality comes in two forms:
Temporal locality: if you accessed a memory address recently, you are likely to access it again soon. Think of a loop variable — you read it, increment it, and read it again on every iteration.
Spatial locality: if you accessed a memory address, you are likely to access nearby addresses soon. Think of iterating through an array — you read element 0, then element 1, then element 2, and so on.
These patterns are not accidental. They arise naturally from how programs are structured: loops repeat over the same code and data, arrays and structs store related data contiguously, and the stack grows and shrinks in a localized region.Consider a simple example: a loop that sums an array of 1000 integers. The loop counter variable is read and written on every iteration — that is temporal locality. The array elements are accessed sequentially, one after another — that is spatial locality. Both the code (the loop instructions themselves) and the data (the array and the counter) exhibit strong locality. The memory hierarchy is specifically designed to exploit exactly these patterns.There is also a related concept called sequential locality — a special case of spatial locality where access follows a strictly sequential pattern, such as reading bytes from a file or executing instructions in order. Sequential locality is the easiest pattern for hardware to detect and optimize through a technique called prefetching, where the cache proactively loads the next block of data before it is even requested.How strong is locality in practice? Studies of real workloads have shown that typical programs access only about 10% of their total address space during any given interval of execution. Furthermore, a large fraction of memory accesses go to a very small set of "hot" addresses — the stack, the current function's local variables, and the data structures being actively processed. This concentration of accesses is what makes the memory hierarchy work: even though the cache can only hold a tiny fraction of total memory, it holds the fraction that matters most right now.The memory hierarchy exploits locality by placing small amounts of very fast (and very expensive) memory close to the CPU, backed by progressively larger, slower, and cheaper storage. When the CPU needs data, it checks the fastest level first. If the data is there (a hit), access is nearly instantaneous. If not (a miss), the system fetches the data from a lower level and brings it up into the faster level, betting that locality means it will be needed again soon.This is analogous to how you might organize information in your daily life. You keep the phone number you are about to call in your short-term memory (fast, tiny capacity). You keep frequently used contacts in your phone's favorites list (fast to access, limited space). You keep all your contacts in your phone's address book (larger, takes a bit longer to search). You keep old records in a filing cabinet (much larger, much slower). And you keep archived documents in a storage unit across town (enormous capacity, but it takes a trip to retrieve anything). Each level has more capacity but requires more effort to access. You naturally keep the most frequently needed information at the most accessible level — and the memory hierarchy does the same.<br><img alt="Computer Memory Hierarchy pyramid" src="assets/l08-memory-hierarchy-pyramid.png" target="_self">The memory hierarchy pyramid: smaller and faster at the top (registers, cache), larger and slower at the bottom (hard drives, tape backup).Here is the full pyramid, from fastest to slowest:Notice the enormous range: from sub-nanosecond register access to multi-second tape access — a factor of over 10 billion. Each level in the hierarchy effectively acts as a cache for the level below it. Registers cache values from L1, L1 caches data from L2, L2 from L3, L3 from RAM, RAM from SSD, and so on. The operating system and hardware collaborate to manage this transparently, so that from the programmer's perspective, you simply access memory addresses and the system does its best to serve them from the fastest available level.The upper levels of the hierarchy (registers through RAM) are volatile — they lose their contents when power is removed. The lower levels (SSD, HDD, tape) are non-volatile — data persists without power. This is why your computer needs both RAM and a hard drive: RAM provides fast working storage while the system is running, while the drive provides permanent storage that survives reboots and power failures.It is worth noting that the management of these levels is split between hardware and software. The cache levels (L1, L2, L3) are managed entirely by hardware — the CPU decides what to keep in cache and what to evict, with no software involvement. The boundary between RAM and disk is managed by the operating system through a mechanism called virtual memory (which we will discuss in a later lecture). The boundary between local storage and archival tape is managed by specialized backup software. Each layer of management adds complexity but is invisible to the layers above it.This transparency is a key design principle. A programmer writing mov eax, [address] does not need to know whether that address is currently in L1 cache, L3 cache, or main memory. The hardware handles the lookup automatically. Similarly, a program accessing a memory-mapped file does not need to know whether the data is currently in RAM or still on disk — the OS handles the page fault and loads it transparently. This layered abstraction is what makes the hierarchy usable despite its complexity.However, transparency does not mean that programmers can be ignorant of the hierarchy. While the hardware makes it invisible in terms of correctness (your program produces the correct result regardless of cache behavior), it is very much visible in terms of performance. A cache-friendly program and a cache-hostile program are both correct, but one may run 10x faster than the other. We will explore these performance implications further in Section 9.It is helpful to understand the physical location of each hierarchy level. Registers are built directly into the CPU core, right next to the ALU and control logic — they are literally part of the execution engine. L1 cache is on the same silicon die as the core, physically adjacent to it — typically within a fraction of a millimeter. L2 cache is also on-die but slightly farther from the core logic. L3 cache occupies a large portion of the CPU die and is shared among all cores.Main memory (DRAM) lives in separate chips, typically mounted on DIMMs (Dual In-line Memory Modules) that plug into slots on the motherboard. The physical distance from the CPU to the DRAM chips is several centimeters, and signals must travel across the motherboard's copper traces — this physical distance is one reason DRAM access takes so much longer than cache access.SSDs and HDDs are connected via cables or PCIe slots, placing them even farther from the CPU. Tape storage is typically in a separate robotic library, possibly in a different room or building. The physical distance at each level mirrors the increasing latency — data literally has farther to travel.When evaluating any level of the hierarchy, architects care about several properties:
Latency: how long it takes to retrieve a single piece of data. This is the number that determines how long the CPU stalls on a miss.
Bandwidth: how much data can be transferred per second. High bandwidth allows large blocks of data to move efficiently between levels.
Capacity: how much data can be stored. Larger capacity means more data can be kept close to the CPU.
Cost per bit: what it costs to store each bit. This is why we cannot simply build terabytes of SRAM — it would be prohibitively expensive.
Volatility: whether data persists when power is removed. Everything from registers through RAM is volatile; SSDs, HDDs, and tape are non-volatile.
Energy per access: how much power is consumed per read or write operation. This has become increasingly important in mobile devices and data centers where thermal and power budgets are tight. Accessing DRAM consumes significantly more energy than accessing SRAM cache, and accessing an SSD or HDD consumes more still.
These metrics are interrelated. Physically, placing memory closer to the processor core reduces latency (shorter wire delay) and energy (shorter signal path), but the space close to the core is limited, constraining capacity. Moving to a denser technology increases capacity but adds latency. Every level in the hierarchy represents a different balance point in this multi-dimensional tradeoff space.The hierarchy exists because no single technology optimizes all of these properties simultaneously. SRAM is fast but expensive and low-density. DRAM is cheaper and denser but slower. Flash is cheaper still but much slower. Magnetic storage is the cheapest per bit but the slowest. By layering these technologies, architects create the illusion of a large, fast, cheap memory system — one that appears to the programmer as if it were both enormous and instant, even though no single technology can deliver both.<br>The concept of a CPU cache was first proposed by <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Maurice_Wilkes" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Maurice_Wilkes" target="_self">Maurice Wilkes</a> in 1965, who called it "slave memory" — a small, fast buffer sitting between the processor and main memory. The idea was simple: keep the most recently used data in a fast store so the CPU does not have to wait for slow main memory on every access. The first commercial computer to include a cache was the IBM System/360 Model 85, released in 1968, which had a 16–32 KB cache. Today, caches are not just a single buffer but a multi-level hierarchy of caches, each with a distinct role. Every modern processor — from your laptop to a data center server to the chip in your smartphone — includes multiple levels of cache. It is one of the most universal features of contemporary CPU design.You might wonder: why not just build one big, fast cache? The answer comes down to physics and economics. A very small cache (say, 32 KB) can be built extremely fast — it can be accessed in roughly 1 nanosecond because the data has a short physical distance to travel and the lookup logic is simple. But 32 KB is tiny; a program's working set often exceeds that, leading to frequent cache misses.On the other hand, a large cache (say, 32 MB) can hold much more data, reducing misses. But the larger the cache, the longer the lookup takes — the electrical signals must travel farther, and the selection logic is more complex. A 32 MB cache might take 10 nanoseconds to access — still much faster than DRAM, but 10 times slower than a small 32 KB cache.There is also a power consideration. Larger caches consume more energy per lookup because they drive longer wires and activate more circuitry. For a battery-powered device like a laptop or smartphone, the energy cost of cache access is a significant fraction of total power consumption. This is another reason architects do not simply make caches as large as possible — there is a power budget to respect.The solution is to use multiple levels, each representing a different point on the size-speed tradeoff curve. A miss at a fast, small level falls through to a larger, slightly slower level before finally reaching main memory. Think of it as a series of safety nets: if the tightest, closest net misses the data, a wider net below catches most of what slipped through, and a still wider net below that catches nearly all the rest. Only a tiny fraction of accesses ever falls all the way down to main memory.<br><img alt="Single-core cache hierarchy" src="assets/l08-cache-hierarchy-single.png" target="_self">Cache hierarchy for a single CPU core: registers → L1 cache (on-chip, banked) → L2 unified → L3 unified → main memory.The L1 cache is the smallest and fastest cache, sitting closest to the CPU core. Its defining characteristics:
Size: typically 32–64 KB per core.
Latency: approximately 1 nanosecond (3–5 clock cycles on a modern processor).
<br>Split design: most modern CPUs split L1 into two separate caches — L1D (data cache) for memory operands, and L1I (instruction cache) for the instruction stream. This is called a <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Harvard_architecture" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Harvard_architecture" target="_self">Harvard architecture</a> at the cache level, even though the main memory uses a unified (von Neumann) address space. Splitting L1 allows the CPU to fetch an instruction and read/write data simultaneously without contention.
Private: each core has its own dedicated L1 cache.
Because L1 is so small, it relies heavily on temporal and spatial locality. The data you accessed one cycle ago is almost certainly still in L1. The next element in an array you are iterating through is very likely in L1 as well, because caches load data in blocks called cache lines (typically 64 bytes on x86 processors), not individual bytes.This cache line concept is crucial. When you read a single byte from memory, the hardware does not fetch just that one byte — it fetches the entire 64-byte block (the cache line) that contains it. This is the hardware's way of exploiting spatial locality: if you read byte N, you will probably read bytes N+1, N+2, and so on, and they are already in the cache. For a 32 KB L1D cache with 64-byte lines, the cache holds 512 lines at any given time.Why 64 bytes? This is a carefully chosen tradeoff. Larger cache lines exploit more spatial locality (more neighboring bytes are prefetched), but they also waste more bandwidth when the CPU only needs a few bytes from the line. Smaller lines reduce wasted bandwidth but miss out on spatial locality. The 64-byte line size has been the standard on x86 processors for over two decades because it balances these concerns well for typical workloads.The L2 cache sits behind L1, serving as a backstop for L1 misses:
Size: typically 256 KB – 1 MB per core.
Latency: approximately 5 nanoseconds (10–20 clock cycles).
Unified: unlike L1, the L2 cache is usually unified, holding both instructions and data.
Private or semi-shared: on most modern designs, L2 is per-core. Some architectures share L2 between a pair of cores.
When the CPU requests data that is not in L1 (an L1 miss), the request goes to L2. If L2 has it (an L2 hit), the data is returned and also placed into L1 for future fast access. The 5 ns L2 latency is five times slower than L1, but it is still 10–20 times faster than main memory.The relationship between L1 and L2 depends on the cache inclusion policy:
In an inclusive design, every line in L1 is also present in L2. This simplifies coherence (to check if data is in L1, you only need to check L2), but it means L2's effective additional capacity is reduced — it duplicates what L1 already holds.
In an exclusive design, L1 and L2 hold disjoint data. When a line is evicted from L1, it moves to L2 (and vice versa when promoted). This maximizes total cache capacity — L1 + L2 together hold more unique data.
In a non-inclusive (or NINE — Non-Inclusive Non-Exclusive) design, there is no strict policy — a line may or may not be present in both levels.
Modern AMD processors use an exclusive L3 design, while Intel has shifted from inclusive to non-inclusive policies in recent generations. These are internal architectural decisions that affect both effective capacity and the complexity of cache coherence protocols.The L3 cache is the last level of cache before main memory:
Size: typically 8–32 MB, sometimes more on server processors.
Latency: approximately 10 nanoseconds (20–40 clock cycles).
Unified and shared: L3 is shared across all cores on the processor. This is critical for multi-core performance, because if one core writes data that another core needs, it can be found in the shared L3 without going all the way to main memory.
<br><img alt="Multi-core cache hierarchy" src="assets/l08-cache-multicore.png" target="_self">Multi-core cache organization: each core has private L1 caches (split into data and instruction), with shared L2 and L3 caches and common main memory.In a modern multi-core processor, the cache hierarchy typically looks like this: each core has its own private L1D, L1I, and L2. All cores share a single large L3. Main memory sits beyond L3. Some high-end server CPUs add an L4 cache (sometimes implemented in eDRAM) for even more capacity, but L3 is the last level in most consumer processors.The L3 cache also plays a critical role in inter-core communication. When two threads running on different cores need to share data, the shared L3 provides a much faster path than going through main memory. If Core 0 writes a value and Core 2 reads it, the data can be served from L3 in ~10 ns rather than from RAM in ~100 ns. This makes the size and speed of L3 particularly important for multi-threaded applications.On modern server processors, L3 caches can be enormous. AMD's EPYC processors offer up to 256 MB or more of L3 cache. AMD's consumer Ryzen 7 5800X3D, a gaming-oriented chip, famously added an extra 64 MB of stacked L3 cache (called 3D V-Cache) on top of the existing 32 MB, for a total of 96 MB. This extra cache improved gaming performance by 15–20% because many game workloads have data sets that fit in 96 MB but not in 32 MB — the cache was just large enough to eliminate a critical class of capacity misses.The performance benefit of caches is quantified by the Average Access Time formula:AAT = hit_time + (miss_rate × miss_penalty)
Here, hit_time is the time for a successful lookup at this cache level, miss_rate is the fraction of accesses that miss, and miss_penalty is the time to fetch data from the next level down (which may itself be the AAT of that next level). For multi-level caches, the formula is applied recursively — the miss penalty for L1 is the AAT of L2, and the miss penalty for L2 is the AAT of L3, and so on. This recursive structure is what makes multi-level caches so effective: each level reduces the miss penalty for the level above it.Let us work through a concrete example. Suppose main memory access takes 50 ns, and we have these cache parameters:Without any cache: every access goes to main memory. AAT = 50 ns.With L1 only: AAT = 1 + (0.10 × 50) = 1 + 5 = 6 ns. That is an 8.3x improvement over no cache.With L1 and L2: The miss penalty for L1 is now the AAT of L2, which is 5 + (0.01 × 50) = 5.5 ns. So: AAT = 1 + (0.10 × 5.5) = 1 + 0.55 = 1.55 ns. Another 3.9x improvement.With L1, L2, and L3: The miss penalty for L2 is now the AAT of L3 = 10 + (0.01 × 50) = 10.5 ns. The miss penalty for L1 is the AAT of L2 = 5 + (0.01 × 10.5) = 5.105 ns. So: AAT = 1 + (0.10 × 5.105) = 1 + 0.5105 ≈ 1.51 ns. A modest further improvement, but every fraction of a nanosecond matters at billions of operations per second.The progression is striking: we went from 50 ns (no cache) to 1.51 ns (three-level cache) — a 33x improvement in average access time. And note that the biggest win came from adding L1 (50 ns to 6 ns). Each subsequent level provides diminishing but still meaningful returns. This is why every modern CPU has at least two cache levels, and most have three.The AAT formula also reveals a critical insight: even a small miss rate at L1 matters enormously if the miss penalty is large. A 5% L1 miss rate with a 100 ns miss penalty (no L2 or L3) would give AAT = 1 + (0.05 × 100) = 6 ns — meaning 83% of the effective access time comes from the rare misses, not the frequent hits. This is sometimes called Amdahl's Law for memory: the overall performance is dominated by the slowest path, weighted by how often it is taken. Reducing the miss rate by even a small percentage can have a dramatic impact on overall throughput.A cache hit occurs when the requested data is found in the cache — the CPU gets its data quickly and continues execution. A cache miss occurs when the data is not in the cache, forcing a fetch from a slower level. There are three types of misses:
Cold (compulsory) misses: the very first access to a block of data. The cache starts empty, so the first access to any address always misses.
Capacity misses: the cache is full and cannot hold all the data the program needs. Older data gets evicted to make room.
Conflict misses: occur in caches that are not fully associative — two addresses map to the same cache slot, and they keep evicting each other. This is an artifact of the cache's internal organization.
Programs that exhibit strong locality have high hit rates (95–99% at L1 is common for well-optimized code). Programs with poor locality — such as those that jump randomly through large data structures — suffer frequent misses and run much slower.Understanding these miss types helps you optimize. Cold misses are unavoidable — you must access data for the first time (though prefetching can hide their latency by loading data before it is requested). Capacity misses can be reduced by restructuring your algorithm to work on smaller chunks of data at a time. Conflict misses can be reduced by changing data alignment or by using cache-friendly data layouts. In practice, capacity misses are the most common concern for application programmers.How does the cache decide which line to evict when it is full and a new line must be loaded? This is the cache replacement policy. The most common policy is LRU (Least Recently Used): evict the line that has not been accessed for the longest time. The rationale is temporal locality — if a line has not been used recently, it is probably not going to be needed soon.True LRU is expensive to implement in hardware for highly associative caches (tracking the exact access order of 12 ways requires substantial state), so most real caches use an approximation called pseudo-LRU that tracks access patterns with fewer bits. Other policies include random replacement (surprisingly effective — it avoids pathological worst cases that can trip up LRU) and FIFO (first in, first out — evict the oldest line regardless of recent usage).When an evicted line has been modified (written to), it must be written back to the next level of the hierarchy before being discarded — this is called a write-back. If the line has only been read and not modified, it can simply be discarded (since a valid copy exists at the next level). Some caches use a write-through policy instead, where every write immediately updates both the cache and the next level. Write-through is simpler and guarantees consistency, but it generates more memory traffic. Write-back is more common in modern L1 and L2 caches because it reduces the number of slow writes to lower levels.In a multi-core processor, each core has its own private L1 and usually L2, while L3 is shared. This design reflects a tradeoff:
Private caches (L1, L2) give each core fast, dedicated access without contention from other cores.
Shared caches (L3) allow data to be exchanged between cores without going to main memory. If Core 0 writes a value and Core 1 reads it, the shared L3 can supply it directly.
<br>However, sharing introduces a challenge: cache coherence. If Core 0 has a copy of some address in its private L1, and Core 1 modifies that same address, Core 0's copy is now stale. Hardware protocols (such as <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/MESI_protocol" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/MESI_protocol" target="_self">MESI</a>) track the state of each cache line across all cores and ensure that all cores always see a consistent view of memory. The <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/MESI_protocol" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/MESI_protocol" target="_self">MESI protocol</a> assigns each cache line one of four states:
Modified (M): this core has changed the line; no other core has a valid copy. The data must eventually be written back to memory.
Exclusive (E): this core has the only copy, and it has not been modified. It can be modified without notifying other cores.
Shared (S): multiple cores may have copies, all unmodified. To modify it, a core must first invalidate other copies.
Invalid (I): this copy is stale and must not be used. Any access will result in a cache miss.
This happens automatically in hardware, but it has performance implications. When one core modifies a cache line that other cores also hold, the hardware must invalidate those other copies and potentially write back the modified data. This takes time and consumes bus bandwidth. Excessive sharing of data between cores (called false sharing when it happens unintentionally — for example, when two unrelated variables happen to share the same 64-byte cache line) can degrade performance significantly, as the cache line bounces back and forth between cores.To make these numbers concrete, here is the cache configuration of a typical Intel Core i7 (12th generation, Alder Lake) processor:The "associativity" column describes how flexible the cache is in placing data, and this concept deserves explanation. Imagine the cache is divided into sets, and each memory address maps to exactly one set. Within that set, there are multiple ways — slots where the data can be stored. A 12-way set-associative cache means each memory address maps to one set, and within that set, there are 12 possible slots.At the extremes:
A direct-mapped cache (1-way) maps each address to exactly one slot. This is fast to look up (just check one location) but suffers from conflict misses when two frequently-used addresses happen to map to the same slot.
A fully associative cache allows any address to go in any slot. This eliminates conflict misses entirely but requires checking every slot on every access, which is slow and power-hungry.
N-way set-associative is the compromise used in practice. It keeps lookup reasonably fast (check N locations) while greatly reducing conflict misses compared to direct-mapped.
The L1 caches use high associativity (8–12 ways) to minimize misses despite their small size. The tradeoff is slightly higher latency and power consumption per lookup, but for a cache this small and fast, that cost is acceptable.By now you have seen the hierarchy and the cache levels. Let us step back and look at the broader picture: why does this hierarchy exist, and what does it mean for you as a programmer?There is an iron law in memory design: faster memory is smaller and more expensive. This is not an arbitrary engineering choice — it is a consequence of physics and circuit design.Fast memory (SRAM) uses six transistors per bit in a cross-coupled configuration that holds its state as long as power is applied. No refresh is needed, and access is nearly instantaneous. But six transistors per bit means very low density — you cannot pack much SRAM onto a chip, and the manufacturing cost per bit is high.DRAM uses one transistor and one capacitor per bit. The capacitor stores charge to represent a 0 or 1, and the transistor controls access. This is far denser than SRAM — roughly 6 to 10 times more bits per unit area. But capacitors leak charge, so DRAM must be periodically refreshed (re-read and re-written) thousands of times per second, which adds latency and complexity. Reading a DRAM cell is also destructive — the act of reading drains the capacitor, so the data must be rewritten after every read.Flash memory (used in SSDs) stores charge in floating-gate transistors that trap electrons behind an insulating layer. This allows data to persist without power — flash is non-volatile. The density is even higher than DRAM, and the cost per bit is much lower. But reading takes microseconds (not nanoseconds), and writing is slower still because it requires erasing an entire block before writing new data. Flash cells also wear out after a limited number of write cycles (typically 1,000–100,000 depending on the type), which is why SSDs include complex wear-leveling logic.Magnetic storage (HDDs, tape) achieves the highest density and lowest cost by encoding data as magnetic orientations on a spinning platter or a tape ribbon. Access is glacially slow — the read/write head must physically move to the right location (seek time), and then wait for the platter to rotate to the right position (rotational latency). These mechanical operations take milliseconds, which is millions of times slower than electronic circuits.The key insight is that each of these technologies occupies a distinct niche in the speed-cost spectrum. No technology dominates all others on every metric. If it did, we would simply use that one technology for everything. Instead, we stack them in a hierarchy, each technology serving its best purpose.Researchers continue to explore new memory technologies that might narrow these gaps. MRAM (Magnetoresistive RAM) and ReRAM (Resistive RAM) promise non-volatile memory with near-DRAM speeds, which could eventually blur the line between memory and storage. 3D XPoint (marketed as Intel Optane) offered latencies around 10x faster than NAND flash, though still 10x slower than DRAM — it occupied a niche between DRAM and SSD that no previous technology had filled. HBM (High Bandwidth Memory) stacks DRAM chips vertically using through-silicon vias (TSVs) to dramatically increase bandwidth, reaching over 1 TB/s in HBM3 — crucial for GPUs and AI accelerators that need to feed vast amounts of data to thousands of parallel processing units.These emerging technologies are adding new layers to the hierarchy rather than eliminating it. The fundamental physics — faster access requires smaller size and higher cost — is unlikely to change. What changes is the number and nature of the layers, and the specific technologies at each level. The hierarchy is not a temporary engineering compromise; it is a permanent feature of computer architecture.Here is the complete picture in one table:<br><img alt="Memory speed pyramid" src="assets/l08-memory-speed-pyramid.jpeg" target="_self">The speed hierarchy: CPU-level storage (registers, cache) is fastest; RAM is in the middle; mass storage (disk) is slowest.Look at the cost column. SRAM used for L1 cache costs roughly $1,000 or more per gigabyte. DRAM costs about $3–5 per gigabyte — a factor of 200–300 cheaper. An HDD costs about $0.02 per gigabyte — another factor of 150 cheaper than DRAM. If we could build 64 GB of SRAM at L1 speed, it would cost tens of thousands of dollars just for the memory, and the chip would need to be impractically large to accommodate all those transistors.The hierarchy exists because economics forces us to use a mix of technologies. A modern computer might contain: 1 KB of registers, 256 KB of L1 cache, 4 MB of L2 cache, 32 MB of L3 cache, 32 GB of DRAM, 1 TB of SSD, and perhaps an external 4 TB HDD for backups. Each level costs roughly the same amount of money (a few tens of dollars) but differs by orders of magnitude in size and speed. This is the magic of the hierarchy — equal investment at each level, but exponentially more capacity as you go down.The memory hierarchy has evolved significantly over the decades. In the early days of computing (1950s–1960s), there was essentially no hierarchy — the processor accessed main memory directly, and that was it. The first caches appeared in the late 1960s. Two-level caches (L1 + L2) became common in the 1990s. Three-level caches (L1 + L2 + L3) became standard in the 2000s.Meanwhile, the technologies at each level have also evolved. DRAM has gone through multiple generations: SDRAM, DDR, DDR2, DDR3, DDR4, and now DDR5 — each generation roughly doubling bandwidth over its predecessor. DDR5, for example, offers peak bandwidth of over 50 GB/s per channel, compared to roughly 25 GB/s for DDR4. However, latency has not improved nearly as much — DDR5 latency is actually slightly worse than DDR4 in absolute terms (nanoseconds), even though it is better in terms of clock cycles. This asymmetry between bandwidth improvement and latency stagnation is a recurring theme throughout the hierarchy.Flash storage has evolved from SLC (single-level cell, 1 bit per cell) to MLC (2 bits), TLC (3 bits), and QLC (4 bits), each generation offering higher density and lower cost at the expense of endurance and speed. Modern SSDs also stack cells vertically in 3D NAND configurations with over 200 layers, dramatically increasing capacity per chip.The trend at every level has been toward greater capacity and bandwidth, while latency improvements have been more modest — reinforcing the need for the hierarchy. The memory wall is not shrinking; if anything, it is growing wider, making caches and hierarchical design more important than ever.<br><img alt="Cache bandwidth and latency diagram" src="assets/l08-cache-bandwidth-latency.png" target="_self">As you move down the hierarchy from CPU to mass storage, bandwidth increases but latency and capacity also increase — the fundamental speed-size tradeoff.Two metrics define how data moves through the hierarchy:
Latency measures how long you wait for the first byte of data. It increases as you move down the hierarchy. Registers respond in less than a nanosecond; a hard drive might take 10 milliseconds — a factor of 10 million.
Bandwidth measures how many bytes per second can flow between levels. It generally decreases as you move down. The connection between the CPU and L1 cache might sustain hundreds of gigabytes per second. The bus to main memory might deliver 50 GB/s. A SATA SSD might manage 500 MB/s, and a hard drive around 200 MB/s.
Both metrics matter, but in different scenarios. Latency dominates when you are making many small, random accesses — each one pays the full latency cost. Bandwidth dominates when you are streaming large blocks of sequential data — the initial latency is amortized over many bytes.A useful analogy: latency is like the time it takes a delivery truck to drive from the warehouse to your house. Bandwidth is like the size of the truck. If you are ordering one small package, you care mostly about the travel time (latency). If you are moving your entire apartment, you care mostly about how much fits in each trip (bandwidth). Optimizing memory-intensive code requires thinking about which metric matters more for each access pattern.Here are some approximate bandwidth figures for context:Notice that the bandwidth drops by roughly an order of magnitude at each major boundary. The CPU-to-L1 connection might be 1,000 times faster than the connection to a SATA SSD. This is why keeping data in cache is so important — not just for latency, but also for throughput.It is worth noting the recent rise of NVMe SSDs, which bypass the older SATA interface and connect directly to the CPU via PCIe lanes. This has narrowed the bandwidth gap between SSDs and RAM from roughly 100x (SATA era) to roughly 10x (NVMe era). Latency has also improved, dropping from ~100 μs for SATA SSDs to ~10–20 μs for the best NVMe drives. Modern NVMe drives using PCIe 5.0 can sustain sequential reads above 10 GB/s — fast enough that the bottleneck is shifting from the storage device to the CPU's ability to process data.This trend is gradually blurring the boundary between storage and memory. Some systems now use persistent memory technologies that sit between DRAM and flash in both speed and cost. Others use CXL (Compute Express Link), a new interconnect standard that allows remote DRAM pools to be accessed almost as if they were local memory. The hierarchy is not static — it continues to evolve as new technologies emerge.<br><img alt="Detailed memory hierarchy flowchart" src="assets/l08-memory-hierarchy-detailed.png" target="_self">A detailed view of the memory hierarchy showing capacity, access time, and transfer unit at each level — from CPU registers (hundreds of bytes, &lt;10 ns) through magnetic tape (unlimited, seconds).You might think the memory hierarchy is purely a hardware concern and that software developers can ignore it. That would be a costly mistake. The difference between code that works with the cache hierarchy and code that works against it can be enormous — factors of 10x or more on the same hardware.Here are the practical takeaways:Sequential access is fast; random access is slow. Iterating through an array in order exploits spatial locality perfectly. The first access loads an entire 64-byte cache line, and the next 15 accesses (for 4-byte integers) all hit in cache. Jumping randomly through a large array defeats this — every access might be a cache miss.Keep your working set small. If your frequently-accessed data fits in L1 (64 KB), your program will be blazingly fast. If it fits in L2 (1 MB), still fast. If it fits in L3 (32 MB), still decent. If it spills into main memory, performance drops noticeably. If it spills to disk, it drops catastrophically.Data structure choice matters. An array of structs (where each struct contains all the fields you need) often outperforms a struct of arrays (where each field is a separate array) for sequential processing, because it keeps related data together in memory. Conversely, if you only access one field of each struct, a struct-of-arrays layout avoids loading unused fields into cache.Reuse data while it is still in cache. If you need to process a dataset multiple times, try to do all your work on each block before moving to the next, rather than making multiple full passes over the entire dataset. This is called blocking or tiling, and it is the key optimization technique in high-performance numerical computing.Avoid pointer-chasing patterns. Linked lists, trees, and graphs store nodes scattered across memory, connected by pointers. Following a chain of pointers — where each pointer could point anywhere in memory — produces a pattern of essentially random access that defeats both spatial and temporal locality. This is why arrays and array-based data structures (hash tables, flat vectors) tend to dramatically outperform pointer-based structures for many workloads, despite their theoretical algorithmic disadvantages in some operations.Align data to cache line boundaries. If a 4-byte integer straddles two cache lines (partly in one, partly in the next), reading it requires loading two cache lines instead of one. Compilers generally handle basic alignment automatically, but when you are designing performance-critical data structures or working with SIMD (Single Instruction, Multiple Data) operations, manual attention to alignment can make a measurable difference.Be aware of the "cliff" effects. Performance often degrades smoothly as your working set grows — until it exceeds a cache level's capacity, at which point there is a sudden, sharp increase in access time. A data structure that fits in L1 (32 KB) runs fast. One that grows to 40 KB and overflows L1 into L2 will see a noticeable slowdown. One that exceeds L3 and spills into main memory may become dramatically slower. Understanding where these cliffs lie for your target hardware allows you to size your data structures accordingly.These are not abstract concerns. Database engines, game engines, scientific simulation codes, and operating system kernels are all carefully designed to work with the memory hierarchy. Professional game developers spend significant time ensuring that hot data structures fit within L1 or L2 cache. Database engines organize their on-disk data in page-sized blocks that map efficiently to memory pages and cache lines. Scientific computing libraries like BLAS implement matrix multiplication using blocking strategies that can achieve 90%+ of theoretical peak performance — versus perhaps 5-10% for a naive implementation that ignores the cache.As a computer systems architect, understanding these tradeoffs is essential. The memory hierarchy is not just a hardware curiosity; it is one of the most important factors determining the real-world performance of the software you write.The memory hierarchy is a brilliant engineering compromise born from a fundamental constraint: we cannot build memory that is simultaneously fast, large, cheap, and non-volatile. Instead, we layer multiple technologies, each optimized for a different point in the tradeoff space, and use locality of reference to keep the most relevant data at the fastest levels.From the programmer's perspective, the key lessons are:
Know the numbers. Understand the approximate latency and size of each level in the hierarchy. This lets you predict where performance bottlenecks will occur.
Exploit locality. Structure your data and algorithms to access memory sequentially and reuse data while it is still cached.
Measure, do not guess. Use profiling tools (like perf stat on Linux or Instruments on macOS) to measure cache hit rates and identify whether your program is memory-bound or compute-bound.
The hierarchy is your friend. It exists to make your programs fast — but only if you work with it, not against it.
In earlier lectures, you encountered specific memory types in context: SRAM in caches (L03), DRAM in main memory (L03, L06), ROM holding the BIOS firmware (L07). You also saw the memory hierarchy table in L06, which showed how each level trades off speed, size, and cost. But we have not yet stepped back to survey the full landscape of computer memory technologies and the principles that distinguish them.This section provides that comprehensive classification, organizing memory types by six different criteria: volatility, power consumption, read/write capability, access pattern, physical integration, and underlying technology. Understanding these distinctions is not merely academic — they explain why computer systems use a hierarchy of different memory technologies rather than a single universal one, and they will help you reason about performance trade-offs in real systems. The classification also provides vocabulary you will encounter in datasheets, technical documentation, and job interviews throughout your career.The most fundamental division in memory technology is volatility — whether the memory retains its contents when power is removed.Volatile memory loses all stored data the moment electrical power is cut. This category includes:
SRAM (Static Random-Access Memory): Uses a circuit of six transistors per bit arranged as a bistable latch (a pair of cross-coupled inverters). The circuit maintains its state as long as power is supplied, with no need for periodic refreshing. SRAM is used for CPU caches (L1, L2, L3) and CPU registers. The "static" in the name refers to the fact that the stored value does not change over time — it remains stable as long as power flows, unlike DRAM where the stored charge decays.
DRAM (Dynamic Random-Access Memory): Stores each bit as a charge on a tiny capacitor, with one transistor serving as an access gate. Because the capacitor charge leaks over time (typically within milliseconds), DRAM must be refreshed periodically — the memory controller reads each cell and rewrites its value thousands of times per second. The "dynamic" in the name refers to this constantly changing (decaying) charge. Despite this complexity, DRAM is the dominant technology for main memory (RAM) in modern systems because its one-transistor-per-bit design is vastly denser and cheaper per bit than SRAM's six-transistor design.
The key trade-offs between the two volatile technologies are worth summarizing:This table explains a question students often ask: "Why not make all memory out of the faster type?" The answer is economics and physics. SRAM's six-transistor cell is roughly six times larger than DRAM's one-transistor cell, making it six times more expensive per bit and six times less dense. Building 16 GB of SRAM main memory would be prohibitively expensive and physically enormous. Conversely, DRAM is too slow for the innermost cache levels where the CPU needs data within a single clock cycle. The memory hierarchy exploits the strengths of each technology by using them in complementary roles.The volatile nature of RAM is why your computer "forgets" everything in memory when it loses power — and why the boot process described in L07 is necessary every time you turn the machine on. The contents of DRAM cells decay within milliseconds without refresh, and SRAM cells lose their state immediately when the supply voltage drops.Non-volatile memory retains data without power. This category is far more diverse:
ROM (Read-Only Memory): Permanently programmed during manufacturing. The original BIOS chips were mask ROM — the data was literally etched into the silicon during fabrication.
PROM (Programmable ROM): Can be programmed once after manufacturing using a special device that physically burns fuse links inside the chip. Once programmed, the data is permanent.
EPROM (Erasable Programmable ROM): Can be erased by exposing the chip to ultraviolet light through a small quartz window on the package, then reprogrammed electrically. This was a common technology for BIOS chips in the 1980s and 1990s — you may have seen the distinctive ceramic packages with round quartz windows.
EEPROM (Electrically Erasable Programmable ROM): Can be erased and reprogrammed electrically, byte by byte, without removing the chip from the circuit. Slower than Flash but allows individual byte modification.
Flash memory: A descendant of EEPROM that erases and writes in larger blocks rather than individual bytes. This trade-off — losing byte-level granularity in exchange for much higher density and speed — made Flash the dominant non-volatile technology. Invented by Fujio Masuoka at Toshiba in 1980 and commercialized in the late 1980s, Flash memory now powers SSDs, USB drives, SD cards, and smartphone storage. The name "Flash" was suggested by Masuoka's colleague Shoji Ariizumi because the erasure process reminded him of the flash of a camera.
Magnetic storage: Hard disk drives (HDDs) and magnetic tape store data as patterns of magnetization on a spinning platter or a thin ribbon. Data persists indefinitely without power.
Optical storage: CDs, DVDs, and Blu-ray discs encode data as patterns of pits and lands on a reflective surface, read by a laser beam.
The volatility divide explains the fundamental two-tier structure of every computer: fast, volatile memory for active computation, and slower, non-volatile storage for persistent data. The boot process is the bridge between them — it copies the operating system from non-volatile storage into volatile RAM so the CPU can execute it.<br>It is worth noting that these categories are not entirely fixed — they represent the current technological landscape, not fundamental physical laws. Emerging technologies have attempted to blur the volatile/non-volatile boundary. <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Magnetoresistive_RAM" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Magnetoresistive_RAM" target="_self">MRAM (Magnetoresistive RAM)</a> stores bits as magnetic orientations in nanoscale magnetic tunnel junctions, offering non-volatility with near-SRAM speed. <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/3D_XPoint" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/3D_XPoint" target="_self">Intel's Optane persistent memory</a>, based on a technology called 3D XPoint, offered non-volatile memory with latencies only 2–3x slower than DRAM — dramatically faster than Flash. While Optane was discontinued in 2022 (it proved too expensive and niche to be commercially viable at scale), the quest for "universal memory" — fast, dense, non-volatile, and cheap — continues to drive research in materials science and semiconductor engineering. If such a technology were to succeed, it could fundamentally reshape the memory hierarchy by eliminating the volatile/non-volatile divide.Another useful distinction is between primary memory and secondary memory (sometimes called primary storage and secondary storage). Primary memory is directly addressable by the CPU — the processor can issue a MOV instruction with a memory address and access the data. This includes RAM (both SRAM and DRAM), ROM, and cache. Secondary memory is not directly addressable — the CPU cannot issue a simple load or store instruction to access data on a hard drive or SSD. Instead, it must go through an I/O controller, issue commands through a device driver, and wait for the data to be transferred into primary memory before it can be processed. This distinction matters for understanding performance: primary memory access is measured in nanoseconds and involves a single instruction, while secondary memory access is measured in microseconds or milliseconds and involves hundreds of instructions (the device driver code, the I/O controller protocol, and interrupt handling).Different memory technologies have dramatically different power characteristics, which directly affect system design — especially in battery-powered devices like laptops and smartphones.SRAM is the most power-hungry memory technology per bit when active. Each bit requires six transistors that must be continuously powered to maintain state. The cross-coupled inverters draw current whenever they are holding a value (this is called static power dissipation — power consumed even when no reads or writes are occurring), and switching state (writing) requires driving the bit lines against the current latch state. Modern SRAM cells in advanced process nodes (5 nm, 3 nm) also suffer from increasing leakage current — current that flows through transistors even when they are "off," a growing problem as transistors shrink. This is one reason caches are kept small: a 64 MB L3 cache would consume significant power even when idle, generating heat that would be difficult to dissipate within the processor package.DRAM consumes less power per bit than SRAM in steady state — the single capacitor holding a charge is essentially passive storage. However, DRAM's refresh requirement adds a continuous power overhead. Every few milliseconds, the memory controller must read and rewrite every cell in the entire DRAM array, consuming power proportional to the total capacity. In a modern laptop with 16 GB of DDR5, the refresh cycles alone consume a meaningful fraction of the system's idle power budget. DRAM manufacturers continuously refine refresh algorithms (partial array refresh, temperature-compensated refresh intervals) to reduce this overhead.Flash memory represents a dramatic improvement in idle power: a Flash chip consuming essentially zero power when not being accessed. Power is consumed only during read and write operations — and writes are particularly expensive because they involve high voltages to inject or remove electrons from the floating gate of each transistor. This near-zero idle power is why smartphones and tablets can retain their data for weeks on standby: the Flash storage consumes virtually no power between accesses.Magnetic and optical storage have yet another power profile. HDDs consume moderate power when the platters are spinning (typically 3–8 watts), but can enter low-power standby modes where the platters stop. Optical drives consume power only when the disc is spinning and the laser is active. For tape drives, a cartridge sitting on a shelf consumes exactly zero power — the ultimate in energy-efficient storage, assuming you can tolerate the access time.The power hierarchy — SRAM &gt; DRAM &gt; Flash &gt; magnetic/optical — mirrors the speed hierarchy almost perfectly. Faster memory technologies consume more power, which is a fundamental physical trade-off: maintaining data in a rapidly accessible state requires continuous energy input. This is not a coincidence — it reflects a deep connection between thermodynamics and information theory. Keeping data "ready" (low latency) requires maintaining physical states that are easy to read quickly, and sustaining those states costs energy.Not all memory is equally easy to read and write. The read/write characteristics of a memory technology determine how it can be used in a system.Read/Write memory (RAM) can be freely read from and written to at the individual byte or word level, with roughly equal speed for both operations. Both SRAM and DRAM fall into this category. This is the memory your programs operate on directly — every MOV [address], value instruction you have written in assembly performs a write to RAM, and every MOV reg, [address] performs a read.<br>Flash memory occupies an interesting middle ground. It can be read at the individual byte level, just like RAM. But writes are constrained: Flash can only be written in blocks (typically 4 KB to 256 KB), and a block must be fully erased before any part of it can be rewritten. Erasing sets all bits to 1, and writing selectively changes some bits to 0. You cannot change a 0 back to a 1 without erasing the entire block. This asymmetry between reads and writes — and the block-level granularity of writes — is why SSDs need sophisticated <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Flash_memory_controller" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Flash_memory_controller" target="_self">firmware controllers</a> with wear leveling, garbage collection, and write amplification management. It is also why the operating system cannot treat an SSD as simply "fast RAM" despite Flash being a semiconductor technology.Read-only memory comes in several gradations, as described in Section 10.1:The progression from ROM to EEPROM to Flash represents a steady increase in write flexibility, though at the cost of endurance: Flash cells can typically survive 1,000 to 100,000 write cycles before wearing out, depending on the type of Flash. This is still orders of magnitude less durable than DRAM, which can be written essentially unlimited times.<br>The endurance issue deserves elaboration because it has practical consequences you may encounter. Modern <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Flash_memory#NAND_flash" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Flash_memory#NAND_flash" target="_self">NAND Flash</a> comes in several variants that trade endurance for density:Packing more bits per cell increases density and decreases cost, but each additional bit makes the voltage levels closer together, increasing error rates and reducing how many times the cell can be reliably rewritten. SSD firmware compensates through wear leveling (distributing writes evenly across all cells), over-provisioning (reserving extra capacity to replace worn cells), and error-correcting codes (detecting and correcting bit errors). Despite these mitigations, the finite write endurance of Flash is a real constraint — a heavily written SSD will eventually wear out, though modern consumer SSDs typically last many years under normal use.How a memory technology allows you to locate and retrieve data has profound implications for performance.Random access means any memory location can be read or written in the same amount of time, regardless of which location was accessed previously. There is no need to traverse intervening locations to reach a target address. SRAM and DRAM are true random-access memories — the address decoding circuitry selects the target row and column directly, and every access takes the same number of clock cycles (ignoring cache effects). This is where the term "RAM" (Random-Access Memory) comes from, though in modern usage, "RAM" almost always refers specifically to DRAM.Inside a DRAM chip, the memory cells are arranged in a two-dimensional grid of rows and columns. To access a particular bit, the memory controller first sends a row address (activating an entire row of cells, copying them into a row buffer), then sends a column address (selecting a specific cell within the activated row). This row-then-column scheme is why DRAM access has two distinct timing components: RAS (Row Address Strobe) latency and CAS (Column Address Strobe) latency. Accessing a different column within the same row is faster than accessing a new row, because the row is already activated — this is the origin of the performance benefit of sequential memory access patterns even in "random-access" memory.Sequential access means data must be read in order — to reach a particular location, you must pass through all preceding locations. The canonical example is magnetic tape. To read data at the middle or end of a tape, the drive must physically spool through all the tape before it, which can take seconds to minutes depending on the position. This makes tape terrible for random lookups but perfectly acceptable for its primary use case: backup and archival storage, where data is written sequentially and read back in the same order (or not read at all, ideally — the best backup is one you never need to restore).<br>Direct access (sometimes called block access) falls between random and sequential. The device can jump directly to a general area or block, then access data sequentially within that block. Hard disk drives are the primary example: the read/write head can <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Hard_disk_drive_performance_characteristics#Seek_time" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Hard_disk_drive_performance_characteristics#Seek_time" target="_self">seek</a> to any track on the platter (direct access to the block), but then must wait for the desired sector to rotate under the head (sequential access within the track). A typical 7,200 RPM drive completes one rotation in about 8.3 milliseconds, so the average rotational latency (waiting for the right sector) is half that — about 4.2 ms. The seek time (moving the head to the right track) averages 5–10 ms. Combined, a single random read from an HDD takes roughly 10–15 ms, which is why HDD performance is characterized by both seek time (the direct-access component) and rotational latency (the sequential component).Optical drives (CDs, DVDs, Blu-ray) work similarly: the laser assembly can jump to any track on the spiral, but then reads sequentially along that spiral. Optical drives are generally slower than HDDs because the disc spins at lower speeds and the seek mechanism is less precise.SSDs are an interesting case: because they are built from Flash chips that support random access at the page level, SSDs provide true random access without seek time or rotational latency. This is the single biggest performance advantage of SSDs over HDDs and explains why replacing an HDD with an SSD produces such a dramatic improvement in responsiveness — the operating system makes many small random reads (loading program files, reading configuration data, accessing metadata), and the access pattern that cripples HDDs is handled effortlessly by SSDs.The access type of a memory device is closely related to how the CPU communicates with it. CPU-memory communication relies on three types of buses to connect the processor to memory devices. The address bus carries the address of the memory location being accessed (the width of this bus determines the maximum addressable memory — a 32-bit address bus can address 2^32 = 4 GB). The data bus carries the actual data being read or written (its width determines how many bits can be transferred per cycle — 64 bits on modern systems). The control bus carries signals that coordinate the operation: read/write commands, clock signals, chip-select lines, and acknowledgment signals. For random-access memory, the address bus selects a location, the control bus specifies the operation, and the data bus transfers the result — all in a few nanoseconds. For sequential or block-access devices, additional protocols handle the more complex access patterns.Where memory physically resides in the system determines its speed, capacity, and role.On-chip memory is fabricated directly on the CPU die itself. This includes:
Registers: The fastest storage in the system, directly wired into the execution units with zero-cycle latency for most operations. An x86-64 processor has 16 general-purpose registers (64 bits each), plus segment registers, the FLAGS register, the instruction pointer, and various special-purpose registers — totaling a few hundred bytes in all. Despite their tiny capacity, registers are the most critical memory in the system because they are the only storage that the ALU can operate on directly.
L1 cache: Typically 32–64 KB per core, split into instruction cache (L1i) and data cache (L1d). Access latency: ~1 nanosecond (3–5 clock cycles). The split between instruction and data caches allows the CPU to fetch an instruction and load data simultaneously, which would be impossible with a single unified cache.
L2 cache: Typically 256 KB to 1 MB per core, usually unified (holding both instructions and data). Access latency: ~3–5 nanoseconds (10–15 cycles).
Being on the same die as the CPU means these memories communicate over extremely short, wide internal buses with no off-chip delays. The physical distance between a register and the ALU may be less than a millimeter. This matters more than you might expect: electrical signals travel at roughly two-thirds the speed of light in copper, which is about 20 centimeters per nanosecond. At a clock frequency of 4 GHz (one cycle = 0.25 ns), a signal can travel only about 5 centimeters in a single clock cycle. This physical constraint is a fundamental reason why on-chip memory must be small and close to the execution units — the speed of light itself limits how far data can travel within one clock cycle.On-board memory resides on the motherboard but not on the CPU die:
L3 cache: In modern processors, L3 is typically on the CPU die as well (shared across all cores), but in older designs or chiplet architectures, it may be on a separate die within the CPU package. Sizes range from 8 MB to 96 MB or more (AMD's Ryzen 7 5800X3D notably features 96 MB of L3 cache using 3D V-Cache stacking technology). Access latency: ~10–15 nanoseconds.
<br>Main memory (DRAM): Installed in <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/DIMM" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/DIMM" target="_self">DIMM</a> (Dual In-line Memory Module) slots on the motherboard. A DIMM is a small circuit board populated with multiple DRAM chips, plugged into a standardized slot. Capacities range from 4 GB to 256 GB or more in consumer systems, with server systems supporting even more. Access latency: ~50–100 nanoseconds — roughly 50 to 100 times slower than L1 cache. The current generation standard is DDR5 (Double Data Rate 5), which transfers data on both the rising and falling edges of the clock signal, effectively doubling the data rate compared to the clock frequency.
The communication between the CPU and DRAM occurs over the memory bus (also called the front-side bus in older systems, or the memory channel in modern architectures). This bus is much narrower and slower than the on-chip interconnects, and the signals must travel through the CPU package, across the motherboard's PCB traces, through the DIMM socket connector, and along the traces on the DIMM module itself — a total distance of 10–20 centimeters. At the speed of light in copper, this round-trip alone takes about 1–2 nanoseconds, but the protocol overhead (address setup, row activation, CAS latency, data transfer) adds much more. This physical and protocol overhead explains the large latency gap between on-chip caches and off-chip DRAM.External memory connects to the system through I/O interfaces:
SSDs and HDDs: Connected via SATA (up to 600 MB/s), NVMe over PCIe (up to 7,000+ MB/s for PCIe 4.0, 14,000+ MB/s for PCIe 5.0), or USB interfaces. Access latencies range from ~25 microseconds (NVMe SSD random read) to ~10 milliseconds (HDD with seek). The difference between SATA SSDs and NVMe SSDs is significant: NVMe bypasses the SATA protocol's overhead (which was designed for spinning disks) and communicates directly over the PCIe bus, achieving much higher throughput and lower latency.
Optical drives: Connected via SATA or USB.
Tape drives: Connected via SAS or Fibre Channel.
Network storage: NAS (Network-Attached Storage) and cloud storage are accessed over network protocols, adding network latency on top of storage latency. A NAS on a local gigabit Ethernet might add ~0.5 ms of network overhead. Cloud storage (Amazon S3, Google Cloud Storage) adds the latency of an internet round-trip — typically 10–100 ms depending on geographic distance. Despite the high latency, cloud storage offers effectively unlimited capacity and automatic redundancy, making it the dominant choice for data that does not need to be accessed at CPU speed.
Each level outward introduces more latency, more capacity, lower cost per bit, and greater physical distance from the CPU. This is the memory hierarchy in physical form — and it is a recurring theme throughout this course. The hierarchy can be extended conceptually even further: cloud storage adds another level beyond local external storage, with latencies measured in tens of milliseconds but capacities measured in petabytes.Cutting across the categories above, memory technologies can also be grouped by the physical phenomenon used to store data.<br>Semiconductor memory stores data using electronic circuits fabricated on silicon wafers, typically using <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/CMOS" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/CMOS" target="_self">CMOS</a> (Complementary Metal-Oxide-Semiconductor) technology. The major semiconductor memory types are:
SRAM (bistable transistor latches)
DRAM (capacitor charges)
Flash/EEPROM (trapped electrons in floating gates)
ROM/PROM (hardwired or fused transistor connections)
Semiconductor memories dominate the upper levels of the hierarchy because they have no moving parts, operate at electronic speeds (nanoseconds), and can be directly addressed by the CPU. All semiconductor memory chips share a common physical structure: a two-dimensional array of memory cells, with peripheral circuitry for address decoding (row and column decoders), sense amplifiers (to detect the tiny signals from individual cells), and I/O buffers. The fundamental unit is the memory cell — whether it is a six-transistor SRAM cell, a one-transistor-one-capacitor DRAM cell, or a floating-gate Flash cell.Magnetic memory stores data as patterns of magnetization on a ferromagnetic surface:
Hard disk drives (HDDs): One or more spinning platters coated with magnetic material, read/written by a tiny head floating nanometers above the surface. Densities exceed 1 terabit per square inch in modern drives. The read/write head does not touch the platter — it flies on a cushion of air just 5–10 nanometers above the surface (for comparison, a human hair is about 75,000 nanometers thick). This engineering precision is why even a small shock to an HDD while it is operating can cause the head to contact the platter surface, destroying data — a phenomenon called a "head crash."
<br>Magnetic tape: A long, thin strip of plastic coated with magnetic material, wound on reels or housed in cartridges. Despite being one of the oldest storage technologies (dating to the 1951 <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/UNIVAC_I" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/UNIVAC_I" target="_self">UNIVAC I</a>), tape remains in active use for archival and backup storage because of its unmatched cost per gigabyte and its durability — tape cartridges can reliably store data for 30 years or more. Modern LTO (Linear Tape-Open) cartridges can store up to 18 TB per tape (or 45 TB compressed), at a cost of roughly $0.01 per gigabyte — an order of magnitude cheaper than HDDs and two orders cheaper than SSDs.
Optical memory stores data as physical patterns on a disc surface, read by a laser:
CD (Compact Disc): ~700 MB capacity, uses an infrared laser (780 nm wavelength).
DVD (Digital Versatile Disc): 4.7 GB (single-layer) to 8.5 GB (dual-layer), uses a red laser (650 nm).
Blu-ray Disc: 25 GB (single-layer) to 50 GB (dual-layer), uses a blue-violet laser (405 nm). The shorter wavelength allows tighter pit spacing and higher density.
Each optical format comes in three variants:
Read-only (ROM): Pressed at a factory with pits stamped into a master disc — similar in principle to vinyl records. These are the commercial CDs, DVDs, and Blu-rays you buy.
Write-once (R): Uses a dye layer that is permanently altered by a high-power laser during writing. Once written, the data cannot be changed.
Rewritable (RW/RE): Uses a phase-change material that can be switched between crystalline (reflective) and amorphous (less reflective) states, allowing the disc to be erased and rewritten hundreds to thousands of times.
The naming progression — infrared to red to blue — reflects the physical principle: shorter wavelengths can resolve smaller features, enabling higher data density. Although optical media has largely been displaced by USB drives and streaming services for consumer use, it remains relevant in archival contexts: pressed Blu-ray discs are extremely durable, resistant to magnetic fields, and can be stored for decades.Emerging technologies that do not fit neatly into the three categories above include:
MRAM (Magnetoresistive RAM): Uses magnetic tunnel junctions — a semiconductor technology that stores data magnetically. Combines aspects of semiconductor (fabricated on silicon, nanosecond access) and magnetic (non-volatile, stores data as magnetic orientation) storage.
PCM (Phase-Change Memory): Uses a chalcogenide glass that can be switched between amorphous and crystalline phases by heating with electrical pulses. The same phase-change principle used in rewritable optical discs, but implemented at the nanoscale on a silicon chip.
ReRAM (Resistive RAM): Stores data by changing the resistance of a thin film. Still largely in the research and early production stage, but promising for applications requiring dense, non-volatile, fast memory.
These technologies represent the frontier of memory research, each attempting to combine the best properties of existing technologies — the speed of SRAM, the density of DRAM, and the non-volatility of Flash — in a single device.A few observations about this table:
The latency column spans roughly 12 orders of magnitude from registers (&lt;1 ns) to tape (minutes). This is one of the largest dynamic ranges in all of engineering — comparable to the difference between the diameter of an atom and the diameter of the Earth.
Cost per gigabyte and capacity are inversely correlated with speed, which is the economic force that drives the memory hierarchy. If all memory technologies cost the same, we would simply use the fastest one. The hierarchy exists because they do not.
The "endurance" row (write cycles before failure) is relevant mainly for Flash, where it is a genuine design constraint. All other technologies in the table can be rewritten effectively unlimited times for practical purposes.
The table makes the trade-offs starkly visible: no single technology excels on all dimensions simultaneously. Fast memory is expensive and small; cheap memory is large and slow. This is precisely why the memory hierarchy exists — it allows the system to provide the illusion of fast, large, cheap memory by automatically keeping frequently used data in the faster levels and pushing rarely used data to the slower ones.Notice also the enormous latency gaps between levels. Going from L1 cache (1 ns) to DRAM (100 ns) is a 100x slowdown. Going from DRAM to an HDD (10 ms) is a 100,000x slowdown. To put this in human terms: if an L1 cache access took 1 second (the time to grab a book from the shelf next to your desk), a DRAM access would take about 1.5 minutes (a walk to the next room), and an HDD access would take about 115 days (a voyage across the ocean and back). These dramatic ratios explain why caching and virtual memory are so critical — even a small improvement in cache hit rate or a modest reduction in page faults translates into enormous performance gains, because the penalty for a miss is so severe.You were first introduced to virtual memory in L07, in the context of Protected Mode: the 80386 processor added a paging unit that divides memory into 4 KB pages, uses a TLB (Translation Lookaside Buffer) with a greater than 99% hit rate to speed up translations, and supports demand paging so that not all of a program's memory needs to be physically present in RAM at once. That introduction explained what paging is and why it was added to Protected Mode. Now we go deeper into the mechanics: how virtual addresses are translated to physical addresses step by step, what happens when a page is not in memory, how multi-level page tables make the scheme practical, and how the hardware and operating system cooperate to manage memory transparently. By the end of this section, you should be able to trace the complete path of a memory access from virtual address to physical location, including the exceptional case where the data is not in RAM at all.To understand why virtual memory was invented, consider the alternative — a system without it. In the early days of computing, and in the Real Mode environment you program in during labs, programs access physical memory directly. If your program writes to address 0x8000, it writes to physical byte 0x8000 in the RAM chips. This creates several serious problems as systems grow more complex:Problem 1: No isolation between programs. If two programs are loaded into memory simultaneously, nothing prevents one from reading or overwriting the other's data. A buggy program can corrupt the operating system itself — or worse, a malicious program can deliberately read passwords and private data from other programs' memory. In your DOSBox environment, you could (if you were careless) overwrite the interrupt vector table at address 0000:0000 and crash the entire system — and there is no mechanism to prevent this. In a multi-user system (a university server, a cloud computing platform), this is a security and reliability catastrophe.Problem 2: Memory fragmentation. Suppose you have 16 MB of RAM and three programs are loaded: Program A occupies addresses 0–4 MB, Program B occupies 4–8 MB, and Program C occupies 8–10 MB. Now Program B exits, freeing the 4–8 MB range. You want to load Program D, which requires 6 MB of contiguous memory — but the largest contiguous free block is only 4 MB (where B was). The 6 MB region at the top (10–16 MB) is available, but Program D needs 6 MB contiguous. The total free memory (10 MB) is more than sufficient, but it is fragmented into non-contiguous pieces. Without virtual memory, you would need to physically relocate Program C in memory to consolidate the free space — a slow operation that also requires updating every pointer and address in Program C, which is essentially impossible if the program is running.This problem is called external fragmentation, and it plagues every system that allocates contiguous physical memory to processes. Over time, as programs start and stop, memory becomes increasingly fragmented until large allocations become impossible despite ample total free memory. Virtual memory eliminates this problem entirely: since virtual-to-physical mapping is done at the page level, the OS can assemble a "contiguous" virtual allocation from physically scattered frames without the program knowing.Problem 3: Programs limited to physical RAM size. If your machine has 512 MB of RAM, no single program can use more than 512 MB (minus what the OS and other programs consume). For applications like video editing, scientific simulation, or large databases, this is a crippling limitation. Before virtual memory, programmers had to manually divide large programs into "overlays" — sections of code that were explicitly loaded into and out of memory by the programmer. This was tedious, error-prone, and placed an enormous burden on programmers to manage memory manually. Virtual memory automates this process entirely.Problem 4: Address conflicts. Every program must be compiled or linked to run at a specific memory address (or be position-independent, which adds complexity and overhead). If two programs are both compiled to start at address 0x10000, they cannot run simultaneously without relocation.<br>Problem 5: No fine-grained memory protection. Even if you could isolate programs into separate physical regions, there is no mechanism to enforce permissions at the page level. You cannot mark certain pages as read-only (to protect code from being overwritten), or as non-executable (to prevent data buffers from being executed as code — a common security exploit known as a <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Buffer_overflow" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Buffer_overflow" target="_self">buffer overflow attack</a>). Without hardware-enforced page-level protection, the operating system has no way to prevent a user program from modifying kernel data structures or executing arbitrary code injected into a data buffer. The page table entries in a paging system include permission bits for each page, allowing the hardware to enforce read/write/execute restrictions on every single memory access — the foundation of modern operating system security.<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Virtual_memory" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Virtual_memory" target="_self">Virtual memory</a> solves all five problems through a single elegant mechanism: indirection. Instead of letting programs access physical memory directly, the system interposes a translation layer. Each program operates in its own virtual address space — a private, contiguous range of addresses that belongs to that program alone. The hardware translates every memory access from the virtual address the program uses to the physical address where the data actually resides in RAM.From the program's perspective, it has a large, contiguous, private memory space starting at address 0 (or, more precisely, from the lowest user-accessible address — the exact layout depends on the OS). From the hardware's perspective, the program's data may be scattered across non-contiguous physical frames, or may not even be in RAM at all (it could be on disk, waiting to be loaded on demand). The translation layer maintains this illusion transparently — the program never needs to know where its data physically resides, and it cannot discover or interfere with other programs' memory.This is the same principle of abstraction you have seen in other contexts: just as the segment:offset addressing in Real Mode abstracted physical addresses behind a segmentation layer, virtual memory abstracts physical memory behind a paging layer. The difference is that paging is far more powerful and flexible.<br>A brief history. The concept of virtual memory was first proposed by German physicist <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Fritz-Rudolf_G%C3%BCntsch" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Fritz-Rudolf_G%C3%BCntsch" target="_self">Fritz-Rudolf Güntsch</a> in his 1956 doctoral dissertation. The first working implementation appeared in the <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Atlas_(computer)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Atlas_(computer)" target="_self">Atlas Computer</a> at the University of Manchester in 1962, designed by Tom Kilburn and his team. The Atlas had only 16,384 words of fast magnetic core memory (the equivalent of RAM) but 98,304 words of slower magnetic drum storage (the equivalent of disk). Its "one-level storage" system automatically moved data between the two, giving programmers the illusion of a single, large, fast memory. The Atlas supervisor software — one of the earliest operating systems — handled all the transfers transparently, including page replacement decisions. This was a revolutionary idea at the time, and it is essentially the same principle used by every modern operating system today, more than sixty years later. The Atlas team's insight that hardware and system software could cooperate to create a useful abstraction became one of the most influential ideas in the history of computing.The most common implementation of virtual memory is paging, which divides both virtual and physical address spaces into fixed-size blocks. (An alternative approach, segmentation, divides memory into variable-size segments based on logical program structure — code segment, data segment, stack segment. The x86 architecture supports both, but as we discussed, modern operating systems use paging almost exclusively because fixed-size pages avoid external fragmentation.)Pages and frames. The virtual address space is divided into pages, and the physical address space is divided into frames (also called "page frames"). Pages and frames are always the same size — on x86 systems, the standard page size is 4 KB (4,096 bytes = 2^12 bytes), though larger page sizes (2 MB "large pages" and 1 GB "huge pages") are also supported for specialized use cases.Using 4 KB pages, a 32-bit virtual address space (4 GB) contains exactly 1,048,576 pages (2^20), and a system with 4 GB of physical RAM contains the same number of frames. But here is the key insight: the number of virtual pages can vastly exceed the number of physical frames, because not all virtual pages need to be in RAM at the same time.Why 4 KB? The page size is a design trade-off. Smaller pages reduce internal fragmentation (wasted space within a page — on average, the last page of any allocation wastes half a page), give finer-grained memory protection, and allow the OS to load only the specific data needed. But smaller pages mean more page table entries (consuming more memory), more TLB pressure (each entry covers less address space), and higher overhead for disk I/O (each page fault loads less data per disk access). Larger pages reduce these overheads but waste more memory due to internal fragmentation. The 4 KB size, chosen by the Intel 80386 designers in the mid-1980s, represented a balanced compromise, and it has persisted as the default ever since — though, as noted above, 2 MB and 1 GB large pages are available for workloads where the trade-off favors larger granularity.Address translation. The core of paging is the translation from virtual addresses to physical addresses. Here is how it works for a 32-bit system with 4 KB pages:A 32-bit virtual address is split into two fields:The page number identifies which page the address falls in. The offset identifies the specific byte within that page. Because pages are 4 KB = 2^12 bytes, the offset needs exactly 12 bits.The CPU uses the virtual page number to look up the corresponding physical frame number in a data structure called the page table. Each process has its own page table, maintained by the operating system. The page table entry for a given virtual page contains:
The physical frame number where this page currently resides in RAM
A present bit indicating whether the page is currently in physical memory
Permission bits (read, write, execute) for memory protection
An accessed bit (set by hardware when the page is read or written) and a dirty bit (set by hardware when the page is written to) — both used by the OS for page replacement decisions
A user/supervisor bit indicating whether the page is accessible by user-mode code or only by kernel code
Once the frame number is found, the physical address is constructed by combining the frame number with the original offset:The offset is identical in both the virtual and physical addresses — this is a direct consequence of pages and frames being the same size. Only the page/frame number changes during translation. This is an elegant design: the offset bits pass through the translation unchanged, and only the high-order bits (the page number) need to be looked up. This means the TLB only needs to store mappings for page numbers, not for every individual byte address.<br><img alt="Paging address translation" src="assets/l08-paging-address-translation.jpeg" target="_self">Address translation through paging: the CPU generates a logical address (page number + offset), the page table maps it to a physical frame number, and the offset selects the byte within the frame.The paging address translation process: the CPU generates a logical address, which is split into a page number and an offset. The page number indexes into the page table to retrieve the corresponding frame number. The frame number is combined with the original offset to produce the physical address used to access main memory.A concrete example. Suppose your program accesses virtual address 0x00403A7C. In binary, the upper 20 bits are 0x00403 (page number = 1,027) and the lower 12 bits are 0xA7C (offset = 2,684). The CPU looks up page 1,027 in the page table and finds that it maps to physical frame 5,891 (0x01703). The resulting physical address is 0x01703A7C — frame 5,891, offset 2,684. The program has no idea this translation happened; it simply issued a MOV instruction targeting 0x00403A7C, and the hardware transparently redirected it.Multi-level page tables. There is a practical problem with the scheme described above. A flat page table for a 32-bit address space with 4 KB pages would need 2^20 = 1,048,576 entries. If each entry is 4 bytes, the page table itself consumes 4 MB of contiguous physical memory — per process. With dozens or hundreds of processes running on a modern system, this is unacceptable. The solution is multi-level page tables. Instead of one large flat table, the virtual page number is split into two (or more) indices. On the 32-bit x86, the 20-bit page number is split into a 10-bit page directory index and a 10-bit page table index. The page directory (1,024 entries) points to page tables, and each page table (1,024 entries) points to physical frames. Most page tables for unused regions of the address space do not need to exist at all — the page directory entry is simply marked "not present." This typically reduces the total page table memory from 4 MB to a few tens of kilobytes for a typical process that uses only a small fraction of its 4 GB virtual address space.On 64-bit systems (x86-64), the address space is so vast (2^48 bytes = 256 TB in current implementations) that four levels of page tables are used: PML4 (Page Map Level 4), PDPT (Page Directory Pointer Table), PD (Page Directory), and PT (Page Table). Each level adds 9 bits of indexing, plus 12 bits for the offset, giving 9+9+9+9+12 = 48 bits. Recent Intel and AMD processors support a fifth level (PML5), extending the virtual address space to 2^57 bytes = 128 PB, though no current system has anywhere near that much physical memory. The multi-level scheme ensures that even with these enormous virtual address spaces, the actual memory consumed by page tables remains proportional to the memory the process actually uses, not the size of the address space it could theoretically address.The translation described above works smoothly when the target page is present in physical RAM. But what happens when it is not?The page fault. When the CPU looks up a virtual page in the page table and finds the present bit is 0, the page is not currently in physical memory. The CPU raises a page fault exception — a special type of interrupt (specifically, exception 14 on x86). This transfers control to the operating system's page fault handler, a kernel routine specifically designed to handle this situation.The page fault handler determines what to do based on the faulting address and the process's memory map: Valid page, not loaded yet (demand paging): The page exists on disk (in the program's executable file or in the swap area) but has not been loaded into RAM. The OS allocates a free physical frame, reads the page contents from disk into that frame, updates the page table entry to point to the new frame and sets the present bit to 1, then restarts the instruction that caused the fault. The program never knows the fault occurred — it just experienced a brief delay. Valid page, swapped out: The page was previously in RAM but was evicted to the swap area on disk to free the frame for another process. The OS loads it back from swap, exactly as in case 1. Invalid access: The virtual address does not correspond to any allocated memory. This is a genuine error — the program accessed memory it should not have. On Linux, the OS delivers a segmentation fault (SIGSEGV) signal to the process, typically terminating it. On Windows, this produces an "Access Violation" error. This is the dreaded segfault you may have encountered (or will encounter) when programming in C. Walking through a page fault step by step. Consider what happens when your program calls a function that has not been executed yet. The function's code resides on a page that was never loaded into RAM (demand paging means the OS does not load pages until they are actually needed):
The CPU fetches the instruction at the function's virtual address.
The MMU looks up the virtual page number in the TLB — miss.
The MMU walks the page table in memory — finds the present bit is 0.
The CPU raises exception 14 (page fault), saving the faulting address in the CR2 register.
The OS page fault handler runs. It checks the process's memory map and determines this is a valid code page from the executable file.
The OS finds a free physical frame (or evicts a victim page to make room).
The OS issues a disk I/O request to read the 4 KB page from the executable file on disk.
The disk I/O completes (this step alone takes ~100 microseconds for an SSD, or ~10 milliseconds for an HDD — an eternity in CPU time). During this wait, the OS typically switches to running a different process, so the CPU is not idle — it is doing useful work for another program while the disk transfer happens in the background.
The OS updates the page table entry: sets the frame number, sets the present bit to 1, and marks the page as read-only (since it is code).
The OS returns from the exception handler, and the CPU restarts the faulting instruction.
This time, the TLB lookup still misses, but the page table walk succeeds — the page is present. The TLB is updated, and the instruction executes normally.
From the program's perspective, nothing unusual happened — the function call just took a bit longer than usual (perhaps 100 microseconds instead of a few nanoseconds, but the program has no way to notice this unless it is explicitly measuring time). From the OS's perspective, it performed a complex sequence of operations involving exception handling, disk I/O scheduling, context switching, and page table manipulation, all triggered by a single instruction fetch. This is demand paging in action — the OS loads pages only when they are actually needed, avoiding the overhead of loading an entire program into memory at startup. Most programs have large portions of code that are rarely or never executed (error handlers, seldom-used features), and demand paging ensures those pages never consume physical memory.<br>Swapping and page replacement. When the OS needs a free physical frame to satisfy a page fault, but all frames are occupied, it must evict an existing page. The OS selects a victim page using a page replacement algorithm — commonly an approximation of <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Page_replacement_algorithm#Least_recently_used" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Page_replacement_algorithm#Least_recently_used" target="_self">LRU (Least Recently Used)</a>, which evicts the page that has not been accessed for the longest time. The "ideal" algorithm (known as Belady's optimal algorithm) would evict the page that will not be used for the longest time in the future — but this requires knowledge of future memory accesses, which is obviously impossible in practice. LRU approximates this by using the past as a predictor of the future: a page that has not been used recently is likely to not be needed soon.The hardware assists the OS in making this decision through the accessed bit and dirty bit in each page table entry. The CPU automatically sets the accessed bit whenever a page is read or written, and sets the dirty bit whenever a page is written. The OS periodically clears the accessed bits and checks which pages have been re-accessed — those that have not are candidates for eviction. Pages with the dirty bit clear are cheaper to evict because they do not need to be written back to disk.If the victim page has been modified since it was loaded (the dirty bit is set), it must be written back to disk before the frame can be reused. If it has not been modified (the dirty bit is clear), the frame can be immediately reused because the disk still has a valid copy.The region of disk used to store evicted pages is called the swap space (on Linux) or pagefile (on Windows). On Linux, swap is typically a dedicated disk partition, though it can also be a file. On Windows, the pagefile (pagefile.sys) is a hidden file on the system drive. In both cases, the swap area acts as an extension of physical memory, albeit one that is thousands of times slower.<br>Thrashing. If the system has too little physical RAM for the combined working sets of all running processes, it can enter a pathological state called <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Thrashing_(computer_science)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Thrashing_(computer_science)" target="_self">thrashing</a>: the OS spends most of its time moving pages between RAM and disk, and very little time actually executing program instructions. Each page loaded for one process evicts a page that another process (or even the same process) will need almost immediately, triggering yet another page fault. The system appears to freeze — the disk light stays on continuously, but nothing gets done.You can observe thrashing on a real system: open far more applications than your RAM can support, and the computer becomes sluggish to the point of being nearly unusable. On Linux, the vmstat command shows page-in and page-out rates; on Windows, Task Manager's "Memory" tab shows the commit charge relative to physical memory. When page faults per second spike and the disk I/O light stays on, you are witnessing thrashing.The solutions to thrashing include:
Add more physical RAM — the most direct fix, allowing more pages to stay resident simultaneously.
Reduce the number of running processes — fewer processes means fewer competing working sets.
Optimize programs to use less memory — better algorithms and data structures can dramatically reduce a program's working set size.
Use faster storage (SSD instead of HDD) — this does not prevent thrashing, but it makes each page fault less expensive (100 μs instead of 10 ms), so the system degrades more gracefully.
The TLB (Translation Lookaside Buffer). If every memory access required a page table lookup, the overhead would be devastating — each instruction fetch and each data access would require an additional memory read just to consult the page table, effectively halving the CPU's speed. The solution is the TLB, a small, fast hardware cache built into the CPU's Memory Management Unit (MMU) that stores recent page-table translations.When the CPU needs to translate a virtual address, it first checks the TLB. If the translation is found there (a TLB hit), the physical frame number is available immediately — no memory access needed. Only on a TLB miss does the CPU walk the page table in memory, and it then caches the result in the TLB for future use.<br>The TLB is remarkably effective. A typical TLB holds 64 to 1,024 entries. With 4 KB pages, even a 512-entry TLB covers 2 MB of memory, and because programs exhibit strong <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Locality_of_reference" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Locality_of_reference" target="_self">locality of reference</a> (they tend to access the same pages repeatedly over short time intervals), the TLB achieves hit rates above 99% in practice. This means the translation overhead is negligible for the vast majority of memory accesses.There is one important complication: because each process has its own page table, the TLB entries from one process are invalid for another. When the OS performs a context switch (switching from running Process A to running Process B), it must either flush the entire TLB or use Address Space Identifiers (ASIDs) to tag TLB entries with the process they belong to. Modern x86 processors support ASIDs (called "Process Context Identifiers" or PCIDs on Intel), allowing TLB entries from multiple processes to coexist, which significantly reduces the performance cost of context switches. Without ASIDs, every context switch would cause a burst of TLB misses as the new process rebuilds the TLB from scratch — a measurable performance penalty on systems that switch frequently between many processes.This is also why large pages (2 MB or 1 GB instead of 4 KB) are useful for certain workloads: a single TLB entry covers a much larger memory region, so the same number of TLB entries can cover far more memory. Databases and virtual machines that work with large, contiguous memory regions often benefit significantly from large pages.<br><img alt="Virtual memory mapping" src="assets/l08-virtual-memory-mapping.png" target="_self">Virtual memory maps each process's virtual address space to physical RAM — some pages reside in RAM, others are swapped to disk.Virtual memory mapping: each process has its own virtual address space, and the operating system maps virtual pages to physical frames in RAM. Pages not currently needed can reside on disk. The mapping is maintained per-process, providing complete isolation — Process A's page 0 and Process B's page 0 can map to entirely different physical frames.Virtual memory is one of the most important abstractions in computer architecture, and it is realized through a partnership between hardware and software:The hardware provides:
The MMU (Memory Management Unit), integrated into the CPU, which intercepts every memory access and translates virtual addresses to physical addresses. The MMU operates transparently — the program issues a MOV instruction with a virtual address, and the MMU converts it to a physical address before the memory bus even sees it.
The TLB, which caches recent translations so the common case (TLB hit) adds zero overhead.
The page fault exception mechanism, which automatically transfers control to the OS when a page is not in RAM.
Protection bits in each page table entry, enforced by the hardware — a user-mode process physically cannot access a kernel page, even if it knows the virtual address.
The operating system provides:
Page table management: The OS builds and maintains the page tables for each process, deciding which virtual pages map to which physical frames. The x86 CR3 register holds the physical address of the current process's top-level page directory; when the OS switches between processes, it updates CR3, effectively switching the entire virtual address space.
The page fault handler: When the hardware raises a page fault, the OS decides whether to load the page from disk, allocate a new page, or terminate the process for an illegal access.
Page replacement policy: When physical memory is full, the OS decides which pages to evict, using algorithms that approximate "evict the page least likely to be needed soon."
Swap space management: The OS manages the area on disk where evicted pages are stored, tracking which disk blocks correspond to which virtual pages.
Process creation and destruction: When a new process is created, the OS sets up a new page table for it. When a process exits, the OS reclaims all of its physical frames and frees the page table memory.
This hardware-software partnership is a recurring theme in computer architecture. The hardware provides fast mechanisms (translation, caching, exception generation), while the software provides flexible policies (which pages to load, which to evict, how to handle errors). Neither alone would be sufficient — pure hardware could not make complex policy decisions, and pure software could not intercept every memory access without catastrophic performance overhead.Virtual memory enables several powerful techniques beyond basic isolation and swapping.Copy-on-write (COW). When a process creates a child process (via the fork() system call on Unix/Linux), the OS does not immediately copy all of the parent's memory. Instead, it sets up the child's page table to point to the same physical frames as the parent, but marks all shared pages as read-only in both page tables. Both processes continue executing, reading from the shared frames with no overhead. Only when one of them tries to write to a shared page does the hardware trigger a page fault (because the page is marked read-only). The OS then copies that single page to a new frame, updates the writing process's page table to point to the copy, and marks it read/write. This "lazy copying" approach is far more efficient than duplicating all memory upfront, because many pages (especially code pages) are never modified by the child process.Memory-mapped files and shared memory. The OS can also map a file on disk directly into a process's virtual address space, so that reading from those virtual addresses reads the file and writing to them writes the file — no explicit read() or write() system calls needed. Similarly, two processes can have page table entries that point to the same physical frames, allowing them to share data through a common region of memory. Both techniques are widely used in modern operating systems and would be impossible without virtual memory. The OS can map a file on disk directly into a process's virtual address space, so that reading from those virtual addresses reads the file and writing to them writes the file — no explicit read() or write() system calls needed. Similarly, two processes can have page table entries that point to the same physical frames, allowing them to share data through a common region of memory. Both techniques are widely used in modern operating systems and would be impossible without virtual memory.<br>The x86 paging history illustrates this evolution. The original 8086 (1978) and 80286 (1982) had no paging support at all — they relied on segmentation for memory management. The <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Intel_80386" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Intel_80386" target="_self">Intel 80386</a> (1985) was the first x86 processor to include a paging unit, adding it on top of the existing segmentation mechanism. In Protected Mode on the 386, an address goes through two translations: first segmentation (logical address to linear address), then paging (linear address to physical address). Modern x86-64 operating systems effectively bypass segmentation (by setting all segment bases to 0 and limits to maximum) and rely entirely on paging for memory management. The segmentation legacy remains in the architecture for backward compatibility, but paging does all the real work. This is an instructive example of how hardware architectures evolve: features that seemed essential in one era (segmentation in the 80286) can become vestigial in the next, while newer features (paging in the 80386) prove so successful that they become the foundation of all modern memory management.Virtual memory also closes the loop on the memory hierarchy from L06. Recall the hierarchy: registers, L1 cache, L2 cache, L3 cache, main memory (DRAM), storage (SSD/HDD). Virtual memory is the mechanism that connects the bottom two levels — DRAM and disk. Just as the cache hardware automatically moves data between DRAM and the cache levels, the virtual memory system automatically moves data between disk and DRAM. At every level, the principle is the same: keep frequently used data close to the CPU, and let rarely used data sink to slower, cheaper, larger storage. The result is a system that behaves almost as fast as the fastest level and almost as large as the largest level — the best of both worlds, achieved through the elegant use of indirection and caching at every layer.Consider the complete path of a single memory access in a modern system. Your program executes MOV EAX, [address]. The CPU first checks the L1 data cache — hit? Done in 1 ns. Miss? Check L2 — done in 5 ns. Miss? Check L3 — done in 15 ns. Miss? Access DRAM — done in 100 ns. But the MMU is operating in parallel: the virtual address must be translated to a physical address. The TLB handles this in one cycle for the 99%+ common case. On a TLB miss, the CPU walks the multi-level page table (additional DRAM accesses). On a page fault, the OS loads the page from disk — microseconds for an SSD, milliseconds for an HDD. Each layer is a cache for the one below, each managed by progressively more complex mechanisms (hardware logic for L1–L3, hardware-software cooperation for virtual memory). The beauty of this design is that the program sees none of this complexity — it simply accesses a memory address, and the system delivers the data as quickly as the hardware allows. This layered transparency, where each level of the hierarchy is invisible to the one above it, is one of the great achievements of computer systems architecture and a principle you will encounter repeatedly throughout your career in computing.In Part 3 of this lecture, we shift from hardware back to software. The memory hierarchy we just explored — registers, cache, DRAM, disk — is the stage on which all your data lives. Now the question becomes: how do you organize that data? High-level languages give you arrays, structs, linked lists, and hash maps with convenient syntax. But underneath, every one of these structures is just a pattern of bytes laid out in contiguous or pointer-linked memory. In this section and the three that follow, we will implement these patterns directly in x86 assembly, so you can see exactly what the compiler does on your behalf.We start with arrays — a topic you already encountered in L05. This section is intentionally brief: we review the essentials, then extend to two-dimensional arrays with a concrete access formula and a word-sized matrix example.In L05, you learned that an array in assembly is simply a sequence of values stored at consecutive memory addresses. There is no runtime bounds checking, no .length property, no type metadata — just raw bytes. You declare an array using DB, DW, or DD, and you access elements by calculating their offset from the base address..data byteArray DB 10, 20, 30, 40, 50 ; 5 bytes, each element is 1 byte wordArray DW 100, 200, 300 ; 3 words, each element is 2 bytes
Static access is straightforward — the assembler computes the offset at assemble time:.code mov al, byteArray ; First element = 10 (offset 0) mov al, byteArray+2 ; Third element = 30 (offset 2)
Dynamic access requires a register to hold the calculated offset: mov bx, offset wordArray mov ax, [bx] ; First element = 100 (offset 0) mov dx, [bx+2] ; Second element = 200 (offset 2) mov dx, [bx+4] ; Third element = 300 (offset 4)
The universal formula is: address = base + index x element_size. For a byte array, element_size is 1, so the index is the offset. For a word array, element_size is 2, so element 3 lives at base + 6. This formula is worth memorizing — it reappears in every language and every architecture.<br><img alt="TASM 1D array code" src="assets/l08-asm-1d-array.png" target="_self">Declaring and accessing 1D arrays in TASM: byte arrays, word arrays, and direct element access with constant offsets and register indexing.A 2D array (a matrix) adds one layer of complexity: you have rows and columns, but memory is one-dimensional. How do you flatten a 2D structure into a 1D sequence of bytes?<br>The standard solution is <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Row-_and_column-major_order" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Row-_and_column-major_order" target="_self">row-major order</a> — you store row 0 first, then row 1, then row 2, and so on. This is the convention used by C, Python (NumPy), and most other languages. (Fortran famously uses column-major order instead, storing column 0 first, then column 1, etc. The formulas differ, but the underlying idea is the same.).data matrix DB 10, 20, 30, 40 ; Row 0 (4 columns) DB 50, 60, 70, 80 ; Row 1 DB 90, 100, 110, 120 ; Row 2 ROWS EQU 3 COLS EQU 4
<br><img alt="TASM 2D array declarations" src="assets/l08-asm-2d-array-declare.png" target="_self">Declaring 2D arrays in TASM: a 3x4 byte matrix, a 2x3 word matrix, and an uninitialized buffer — all stored in row-major order.In memory, these 12 bytes are stored contiguously. If we visualize the raw memory starting at the base address of matrix, we see:Offset: 0 1 2 3 4 5 6 7 8 9 10 11
Value: 10 20 30 40 50 60 70 80 90 100 110 120 ── Row 0 ── ── Row 1 ── ── Row 2 ──
Row 0 occupies offsets 0-3, row 1 occupies offsets 4-7, and row 2 occupies offsets 8-11. The row boundaries are invisible to the hardware — the CPU sees a flat sequence of 12 bytes. It is your code that interprets them as a 3x4 grid. To access element matrix[row][col], you need the formula:address = base + (row x COLS + col) x element_sizeThe expression row x COLS converts the 2D coordinates into a linear index — it skips over all complete rows before the one you want. Adding col then indexes within that row. For byte-sized elements, element_size is 1, so you can drop the final multiplication. For word-sized elements, you multiply by 2 at the end.Let us access matrix[1][2], which should be 70 (second row, third column, counting from zero):.code ; Access matrix[1][2] — expected value: 70 mov bx, offset matrix mov al, 1 ; row = 1 mov ah, 0 ; clear AH for MUL mov cl, COLS ; COLS = 4 mul cl ; AX = row * COLS = 1 * 4 = 4 add bx, ax ; BX = base + 4 (start of row 1) add bx, 2 ; BX = base + 4 + 2 (column 2) mov dl, [bx] ; DL = 70
<br><img alt="TASM 2D array element access" src="assets/l08-asm-2d-array-access.png" target="_self">Accessing matrix[1][2]: compute the offset as base + (row x COLS) + col, then load the value.Walk through it step by step: MUL CL computes 1 x 4 = 4 in AX. Adding that to BX moves us past row 0 (4 bytes) to the start of row 1. Adding 2 moves us to column 2 within row 1. The byte at that address is 70 — exactly matrix[1][2].To verify manually: the linear index is 1 x 4 + 2 = 6. Looking at our memory layout above, offset 6 holds the value 70. The formula works.For word-sized elements, you would multiply by 2 at the end (or shift left by 1), since each element occupies 2 bytes. The formula is the same; only the element size changes.One subtlety worth noting: the MUL instruction always uses AL (or AX) as an implicit operand and stores the result in AX (or DX:AX). This means you must load the row number into AL and clear AH before multiplying. If you forget mov ah, 0, any leftover value in AH will corrupt the multiplication result — a common bug in student code.For larger matrices with word-sized elements, the formula requires one more step:.data wMatrix DW 1, 2, 3 ; Row 0 (3 columns, each element = 2 bytes) DW 4, 5, 6 ; Row 1 WCOLS EQU 3 ; Access wMatrix[1][1] — expected value: 5 mov bx, offset wMatrix mov al, 1 ; row = 1 mov ah, 0 mov cl, WCOLS ; WCOLS = 3 mul cl ; AX = 1 * 3 = 3 (linear index) shl ax, 1 ; AX = 3 * 2 = 6 (multiply by element size) add bx, ax ; BX = base + 6 add bx, 2 ; + col * 2 = + 1 * 2 (column 1, word-sized) mov dx, [bx] ; DX = 5
The SHL AX, 1 is equivalent to multiplying by 2 but faster than a second MUL. You will see this shift-instead-of-multiply optimization frequently in hand-written assembly and even in compiler output for power-of-two element sizes. For a doubleword array (4-byte elements), you would use SHL AX, 2 instead.A note on performance. The MUL instruction is relatively slow on the 8086 (taking 70-77 clock cycles for a byte multiply). In performance-critical code that iterates over a 2D array, programmers often avoid MUL entirely. Instead of recalculating row x COLS from scratch on each iteration, they maintain a running pointer and simply add COLS to it each time they advance to the next row. This transforms a multiplication into a single addition — a technique you will see in optimized inner loops.This is all the machinery behind statements like int matrix[3][4]; ... matrix[1][2] = 70; in C. The compiler generates exactly this kind of address computation every time you index into a multidimensional array.Arrays are useful when every element has the same type and size. But real-world data rarely fits that mold. A student record has a name (string), an age (byte), and a GPA (floating-point number). A pixel has red, green, and blue channels (three bytes). A network packet header has fields of different widths packed together. In high-level languages, you use a struct (C) or a class (Java/Python) to group these heterogeneous fields. In assembly, the TASM assembler provides the STRUC / ENDS directive pair to accomplish the same thing.A STRUC definition tells the assembler "here is a blueprint for a data layout." It does not allocate any memory — it only defines field names and their offsets within the structure.Person STRUC age DB ? ; Offset 0: 1 byte height DW ? ; Offset 1: 2 bytes (word)
Person ENDS
; Total size: 3 bytes per Person
<br><img alt="TASM struct definition and usage" src="assets/l08-asm-struct-define.png" target="_self">Defining a Person struct with STRUC/ENDS, creating two instances, and accessing fields with dot notation — person1.age and person1.height.This is analogous to writing struct Person { uint8_t age; uint16_t height; }; in C. The assembler records three things: (1) the total size of Person is 3 bytes, (2) the field age lives at offset 0 within the structure, and (3) the field height lives at offset 1. No bytes are emitted into the program yet — the STRUC block is purely a compile-time construct.The ? initializer means "uninitialized" — the fields have no default value. You could also specify defaults: age DB 0 would make zero the default age for any instance that does not override it. When you later create an instance with Person &lt;&gt; (empty angle brackets), the default values are used. When you write Person &lt;25, 180&gt;, you override the defaults with specific values.To actually allocate memory for a Person, you declare an instance in the .data segment:.data person1 Person &lt;?, ?&gt; ; Allocate 3 bytes (age=?, height=?) person2 Person &lt;?, ?&gt; ; Another 3 bytes
The angle-bracket syntax &lt; &gt; provides initial values for each field, in order. Using ? means "leave uninitialized" (the assembler may fill in zeros or whatever the default is). You could also write person1 Person &lt;25, 180&gt; to initialize age to 25 and height to 180 at assemble time.Here is where the struct syntax pays off. Instead of remembering that height lives at offset 1 from the start of the structure — and manually adding that offset every time — you write the field name directly:.code ; Write values to person1 mov person1.age, 20 ; Equivalent to: mov [person1+0], 20 mov person1.height, 180 ; Equivalent to: mov [person1+1], 180 ; Write values to person2 mov person2.age, 30 mov person2.height, 170
<br><img alt="Annotated struct code" src="assets/l08-asm-struct-annotated.png" target="_self">Key distinction: the STRUC block is only a template (no memory allocated); the instance declarations (person1, person2) are what actually reserve bytes in memory.The dot notation person1.age is purely an assembler convenience — syntactic sugar, if you like. At the machine level, person1.age resolves to the address of person1 plus the offset of the age field (which is 0), and person1.height resolves to the address of person1 plus the offset of height (which is 1). The CPU sees ordinary memory addresses — it has no concept of "fields" or "structures." If you disassemble the compiled code, you will see plain MOV instructions with numeric addresses, with no trace of the dot notation.The benefit is entirely for the programmer: field names make your intent clear and prevent the kind of off-by-one errors that arise when you manually compute offsets.Let us trace exactly what sits in memory after executing the code above. Suppose person1 is allocated at address 0100h:<br>Notice that height occupies two bytes in <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Endianness" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Endianness" target="_self">little-endian</a> order (low byte first), as is standard on x86. The value 180 is 00B4h: the low byte B4h is stored first (at the lower address 0101h), and the high byte 00h follows at 0102h. If you ever inspect memory in a debugger and see B4 00 where you expected to see 180, this is why — little-endian byte ordering.<br>The two Person instances are packed contiguously with no padding — unlike C compilers on 32-bit or 64-bit platforms, which often insert <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Data_structure_alignment" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Data_structure_alignment" target="_self">alignment padding</a> between fields or between structures to ensure that multi-byte fields start at addresses that are multiples of their size. In 16-bit Real Mode with TASM, what you declare is exactly what you get. The assembler does not add any hidden bytes.When you have a pointer to a struct (for example, the address of a node in a linked list), you access fields using the bracket-dot syntax: mov bx, offset person1 mov al, [bx].age ; Read age (byte at BX+0) mov dx, [bx].height ; Read height (word at BX+1)
This pattern is essential for the linked list traversal we will write in Section 14. When BX holds the address of any Person, [bx].age always reads the right byte, regardless of which instance BX points to.This distinction trips up many students, so let us be explicit. The STRUC/ENDS block is a template — think of it as a cookie cutter. It defines a shape but produces nothing tangible. The person1 Person &lt;?, ?&gt; declaration is an instance — it is the actual cookie, the physical bytes in your program's data segment.You can define as many templates as you like without increasing your program's memory footprint by a single byte. Only instances consume memory. This is exactly how struct definitions work in C: struct Person { ... }; defines a type but allocates nothing; struct Person p1; allocates memory.One more practical point: you can use SIZE Person to get the total byte count of the structure (3 in our case) and OFFSET Person.height to get the field's offset within the template (1 in our case). These are assembler-time constants — they do not generate any runtime instructions. They are useful when writing general-purpose routines that need to iterate over arrays of structures, where you advance a pointer by SIZE Person bytes per iteration.<br>With structs in hand, we can tackle a more ambitious data structure: the <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Linked_list" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Linked_list" target="_self">linked list</a>. In a linked list, each element (called a node) contains some data and a pointer to the next node. Unlike an array, the nodes do not need to be contiguous in memory — they can be scattered anywhere, connected only by their pointers.Why does this matter at the assembly level? Arrays have a fixed size determined at declaration time. Inserting an element in the middle of an array requires shifting all subsequent elements — and in assembly, that means writing a byte-by-byte or word-by-word copy loop, which is both tedious and slow for large arrays. Linked lists, by contrast, allow O(1) insertion and deletion at any point — you just adjust a few pointers. The tradeoff is that you lose O(1) random access: to reach the n-th element, you must traverse the list from the beginning, following each next pointer one by one.In high-level languages, the standard library hides these details. In assembly, you see every pointer dereference, every NULL check, and every field offset calculation. This transparency is the point of the exercise.In assembly, a "pointer" is simply a 16-bit value — an offset within the data segment. There is no special pointer type, no automatic dereferencing, and no garbage collector. You manage every pointer manually. This makes linked list code in assembly more verbose than in C, but it also makes every step completely transparent — there is no hidden machinery.We build on the Person struct from Section 13 by embedding it inside a Node that also contains a next pointer:Node STRUC value Person &lt;&gt; ; Embedded Person struct (3 bytes: age + height) next DW ? ; Pointer to next node (2-byte offset)
Node ENDS
; Total size: 5 bytes per Node (3 for Person + 2 for next)
<br><img alt="Linked list node definition" src="assets/l08-asm-linkedlist-define.png" target="_self">Defining a Node struct that contains a Person value (3 bytes) and a next pointer (2 bytes), with three statically allocated nodes.The Person &lt;&gt; syntax creates an embedded instance with default values. Notice that TASM allows you to nest structures: the Node template contains a Person template as its first field. This is conceptually identical to embedding one struct inside another in C (struct Node { struct Person value; uint16_t next; };).The next field is a word-sized offset that will hold the address of the following node in the list, or a sentinel value (-1, i.e., FFFFh) to mark the end of the list.Since we are working in a simple .COM-style program without dynamic memory allocation (no malloc), we pre-allocate the nodes in the .data segment:.data head DB SIZE Node DUP(0) ; Node 1 (head of the list) node2 DB SIZE Node DUP(0) ; Node 2 node3 DB SIZE Node DUP(0) ; Node 3
The SIZE Node operator returns 5 (the number of bytes in one Node), so each declaration allocates 5 zero-initialized bytes. We use DB ... DUP(0) rather than Node &lt;...&gt; here to show that the allocation is really just a block of bytes — the Node label is a human convenience, not a hardware feature. You could equally write head Node &lt;, ?&gt; using the struct syntax — the result in memory is identical.Now we fill in the data and wire up the next pointers to form the chain: head -&gt; node2 -&gt; node3 -&gt; NULL. This is the assembly equivalent of building a linked list by hand in C — assigning values to fields and setting each node's next pointer to the address of the following node..code ; Node 1 (head): age=25, height=175, next -&gt; node2 mov head.value.age, 25 mov head.value.height, 175 mov head.next, OFFSET node2 ; Node 2: age=30, height=180, next -&gt; node3 mov node2.value.age, 30 mov node2.value.height, 180 mov node2.next, OFFSET node3 ; Node 3 (tail): age=22, height=165, next = NULL mov node3.value.age, 22 mov node3.value.height, 165 mov node3.next, -1 ; FFFFh = NULL sentinel
<br><img alt="Linked list initialization" src="assets/l08-asm-linkedlist-init.png" target="_self">Initializing the linked list: setting each node's data fields and linking them with OFFSET — head -&gt; node2 -&gt; node3 -&gt; NULL (-1).A few things to notice. The nested dot syntax head.value.age works because TASM resolves it step by step: head gives the base address of the first node, .value adds the offset of the embedded Person (which is 0, since it is the first field), and .age adds the offset of age within Person (also 0). So head.value.age resolves to the very first byte of head. Meanwhile, head.value.height resolves to head + 0 + 1 = head + 1, and head.next resolves to head + 3 (skipping the 3-byte Person).We use -1 (FFFFh) as the NULL sentinel rather than 0 because address 0 could theoretically be a valid offset in the data segment. Using FFFFh as an invalid marker is a common convention in 16-bit code, though you could use any agreed-upon sentinel. In 32-bit and 64-bit C, NULL is typically 0, but the concept is the same: a reserved value that means "this pointer does not point to any valid node."The canonical linked list operation is traversal: start at the head, process each node, follow the next pointer, and stop when you hit NULL. This is the assembly equivalent of a while loop that walks through every node. Here we increment the age of every person in the list: mov bx, OFFSET head ; BX = pointer to current node
traverse_list: ; Process current node: increment age by 1 mov al, [bx].value.age ; Read current age add al, 1 ; Increment mov [bx].value.age, al ; Store updated age ; Follow the next pointer mov bx, [bx].next ; BX = address of next node cmp bx, -1 ; Is it NULL (FFFFh)? jne traverse_list ; If not, continue to next node ; Loop ends when BX = FFFFh (end of list)
<br><img alt="Linked list traversal code" src="assets/l08-asm-linkedlist-traverse.png" target="_self">Traversing the linked list: follow the next pointer from node to node, incrementing each person's age, until reaching the NULL sentinel.Step through this mentally. On the first iteration, BX points to head. We read head.value.age (25), add 1 to get 26, and store it back. Then mov bx, [bx].next loads the word at head + 3, which is the offset of node2. BX now points to node2. On the second iteration, we increment node2's age from 30 to 31 and load node2.next, which is the offset of node3. On the third iteration, we increment node3's age from 22 to 23 and load node3.next, which is FFFFh. The CMP sets the zero flag, JNE does not jump, and the loop ends.This is exactly the pattern that a C compiler generates for:Node* current = head;
while (current != NULL) { current-&gt;value.age++; current = current-&gt;next;
}
The only difference is that in C, the compiler manages the pointer arithmetic and the NULL check for you. In assembly, you do it all explicitly — which is precisely why studying this builds deep understanding of how data structures actually work at the machine level.Let us visualize the complete memory layout after initialization. Suppose head starts at offset 0200h:Address Bytes Interpretation
------- ----- --------------
0200h 19 head.value.age = 25 (19h)
0201h AF 00 head.value.height = 175 (00AFh, little-endian)
0203h 05 02 head.next = 0205h (offset of node2) 0205h 1E node2.value.age = 30 (1Eh)
0206h B4 00 node2.value.height = 180 (00B4h)
0208h 0A 02 node2.next = 020Ah (offset of node3) 020Ah 16 node3.value.age = 22 (16h)
020Bh A5 00 node3.value.height = 165 (00A5h)
020Dh FF FF node3.next = FFFFh (NULL)
Each node is exactly 5 bytes: 1 for age, 2 for height, and 2 for the next pointer. The nodes happen to be contiguous here (because we declared them one after another in .data), but that is not a requirement. The beauty of the linked list is that head.next could point to any address in the data segment — the nodes could be scattered across memory, and the traversal code would work identically. The pointer is what creates the logical sequence, not the physical address order.Notice also that if you wanted to insert a new node between head and node2, you would only need to change two pointers: set the new node's next to point to node2, and set head.next to point to the new node. No data needs to be shifted. Compare this to inserting into the middle of an array, where every element after the insertion point must move one position — an O(n) operation that the linked list avoids entirely.Two mistakes appear frequently in student linked list code: Forgetting to check for NULL before dereferencing. If BX contains FFFFh (our NULL sentinel) and you execute mov al, [bx].value.age, you will read a byte from address FFFFh — which is the very last byte of the 64KB data segment. This will not crash (the 8086 does not have memory protection), but it will silently return garbage. Always check cmp bx, -1 before accessing fields. Overwriting the next pointer before reading it. If your processing code modifies the node's memory (for example, zeroing it out), and the next pointer is part of that node, you will lose the link to the rest of the list. Always read [bx].next into a register before doing anything destructive to the current node. These are not just assembly pitfalls — they are the exact same bugs that plague C programs with linked lists (null pointer dereference, use-after-free). Seeing them at the byte level here will make you a better debugger in any language.Our final data structure combines two ideas you already know — strings and pointers — into a pattern that appears everywhere in systems programming: an array of pointers to strings.Suppose you want to store five strings. If you try to put them in a regular array, you immediately hit a problem: strings have variable length. "Hi" is 2 characters; "Assembly Language" is 17 characters. You cannot declare stringArray DB 5 DUP(...) because each element would need a different number of bytes.You could pad every string to some maximum length — say, 32 bytes — and store them in a flat array of 5 x 32 = 160 bytes. But this approach wastes memory (short strings leave most of their slot empty) and complicates processing (you need to know the maximum length everywhere). Worse, if even one string exceeds the maximum, you have to resize the entire array. There is a better way.The standard solution — used by C, by operating systems, and by virtually every language runtime — is indirection: store the strings themselves somewhere in memory, and then create a separate array of pointers (offsets) that point to the start of each string. Each pointer is a fixed-size word (2 bytes in 16-bit mode), so the pointer array is a regular, uniform array that you can index with the formulas from Section 12. The strings can be any length — the pointer array does not need to know or care..data STR_COUNT EQU 5 strArray DW STR_COUNT DUP(0) ; Array of 5 word-sized pointers str1 DB "Hello, World!$" str2 DB "Assembly Language$" str3 DB "Array of Strings$" str4 DB "Pointers Example$" str5 DB "TASM Program$"
<br><img alt="String array data declarations" src="assets/l08-asm-stringarray-data.png" target="_self">Declaring an array of string pointers and the string data itself — each string is $-terminated for DOS INT 21h output.The strings are declared separately, each terminated with $ (the DOS string terminator for INT 21h, function 09h). Note that $ is not the same as C's null terminator (\0); it is a DOS convention specific to the print-string function. The pointer array strArray is initially filled with zeros — we will set the pointers in the .code segment at runtime.Each entry in strArray must hold the offset of the corresponding string:.code mov strArray[0], OFFSET str1 ; strArray[0] -&gt; "Hello, World!$" mov strArray[2], OFFSET str2 ; strArray[1] -&gt; "Assembly Language$" mov strArray[4], OFFSET str3 ; strArray[2] -&gt; "Array of Strings$" mov strArray[6], OFFSET str4 ; strArray[3] -&gt; "Pointers Example$" mov strArray[8], OFFSET str5 ; strArray[4] -&gt; "TASM Program$"
<br><img alt="String array pointer initialization" src="assets/l08-asm-stringarray-init.png" target="_self">Initializing the pointer array: each word-sized entry receives the OFFSET of its corresponding string.Notice the indices: strArray[0], strArray[2], strArray[4], and so on — incrementing by 2 because each pointer is a word (2 bytes). This is the base + index x element_size formula from Section 12 in action, with element_size = 2.After initialization, the pointer array in memory looks like this:strArray: [offset of str1] [offset of str2] [offset of str3] [offset of str4] [offset of str5] ── 2 bytes ── ── 2 bytes ── ── 2 bytes ── ── 2 bytes ── ── 2 bytes ──
Each 2-byte slot contains the starting address of the corresponding string. The strings themselves can be anywhere in the data segment and can have wildly different lengths — the pointer array does not care.<br>Now we can iterate through the pointer array and print each string using <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/DOS_API#DOS_INT_21h_services" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/DOS_API#DOS_INT_21h_services" target="_self">DOS interrupt 21h, function 09h</a> (which expects the string address in DX and prints characters until it encounters the $ terminator): mov cx, STR_COUNT ; Loop counter = 5 xor si, si ; SI = byte offset into strArray (starts at 0)
print_loop: mov dx, strArray[si] ; DX = pointer to current string mov ah, 09h ; DOS function: print string int 21h ; Print the string pointed to by DX ; Print a newline (carriage return + line feed) mov ah, 02h ; DOS function: print character mov dl, 0Dh ; Carriage return int 21h mov dl, 0Ah ; Line feed int 21h add si, 2 ; Advance to next pointer (word size) loop print_loop ; Decrement CX and repeat if CX != 0
<br><img alt="String array print loop" src="assets/l08-asm-stringarray-print.png" target="_self">Printing all strings: loop through the pointer array, load each address into DX, and call DOS INT 21h function 09h.The output looks like this:Hello, World!
Assembly Language
Array of Strings
Pointers Example
TASM Program
<br><img alt="DOS string array output" src="assets/l08-dos-stringarray-output.png" target="_self">The output: five strings printed sequentially, demonstrating that the pointer array successfully references each string's location in memory.Notice how the loop body is agnostic to string lengths. It does not need to know that "Hello, World!" is 13 characters and "TASM Program" is 12. It simply loads a pointer and hands it to DOS. The INT 21h handler walks through memory starting at that pointer, printing characters until it hits $. The variable-length complexity is entirely hidden behind the pointer indirection.This pattern — an array of pointers to variable-length data — is one of the most fundamental structures in systems programming. Consider:
In C, main(int argc, char* argv[]) receives command-line arguments as an array of character pointers — exactly the structure we just built. Each argv[i] is a pointer to a null-terminated string, and argc tells you how many entries the array has.
<br>The <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Environment_variable" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Environment_variable" target="_self">environment variable block</a> in both DOS and Unix is stored the same way: an array of pointers to KEY=VALUE strings.
<br>String tables in executable file formats (<a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Executable_and_Linkable_Format" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Executable_and_Linkable_Format" target="_self">ELF</a>, PE) use offset-based indexing into a block of packed strings — a variation of the same idea.
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Virtual_method_table" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Virtual_method_table" target="_self">Virtual method tables</a> (vtables) in C++ are arrays of function pointers — structurally identical to our string pointer array, except the pointers point to code rather than data.
The key insight is that indirection (storing a pointer to data rather than the data itself) solves the variable-size problem. Each pointer is a uniform 2 bytes, so the pointer array is easy to index. The actual strings can be any length, stored anywhere in memory. This separation of "index structure" from "payload data" appears at every level of computer systems, from file system inodes to database indexes to virtual memory page tables.Look at how much ground we have covered in this part of the lecture. We started with flat arrays (Section 12), where every element has the same size and lives at a predictable offset. We then introduced structs (Section 13) to group fields of different sizes under named offsets. We combined structs with pointers to build linked lists (Section 14), where nodes reference each other through stored addresses. And finally, we used pointer arrays (this section) to manage variable-length data by adding a layer of indirection.Each of these structures exists in memory as nothing more than a sequence of bytes. The CPU does not know whether a particular byte is an age field, a pointer to the next node, or the letter 'H' in a string. All the meaning comes from how your code interprets those bytes — which offsets you read, how many bytes you fetch, and which address you follow next.That is the fundamental lesson of low-level data structures: structure is not a property of the data, but of the code that reads it. The same 5 bytes could be a Node (with age, height, and a next pointer) or 5 unrelated byte values — the CPU cannot tell the difference. It is the programmer (or the compiler, on the programmer's behalf) who imposes meaning on raw memory. Understanding this dissolves the mystery of how high-level data structures "really work" — they work exactly like this, just with more syntactic sugar on top. I/O is port-based on x86. The CPU communicates with devices through a separate I/O address space using IN and OUT instructions. Each device (keyboard, timer, DMA controller) has assigned port ranges. The keyboard controller is a case study in hardware interfaces. Ports 0x60 (data) and 0x64 (command/status) provide bidirectional communication — reading scan codes, sending LED commands, and checking status. This is the raw interface that BIOS INT 16h abstracts away. The memory hierarchy trades speed for capacity. Registers (&lt; 1 ns) → L1/L2/L3 cache (1-10 ns) → RAM (50-100 ns) → SSD (100 μs) → HDD (10 ms) → tape (seconds). Each level caches the one below it, exploiting temporal and spatial locality. Memory types differ along multiple axes. Volatility (SRAM/DRAM lose data without power; Flash/ROM retain it), access type (random vs. sequential vs. block), technology (semiconductor vs. magnetic vs. optical), and integration level (on-chip vs. on-board vs. external). Virtual memory provides isolation and flexibility. The MMU translates virtual addresses to physical addresses through page tables. Each process gets its own address space; pages can be swapped to disk when RAM is full. The TLB caches recent translations with &gt;99% hit rate. High-level data structures map directly to memory. Structs (STRUC/ENDS) define field offsets; linked lists use word-sized pointers to chain nodes; string arrays use pointer arrays to handle variable-length data. Understanding these mappings reveals what compilers generate from your C/Python code. What you'll do:
Work directly with video memory at segment B800h in text mode
Write characters and attributes directly to video memory (bypassing INT 10h)
Calculate screen positions using the formula: offset = (row × 80 + col) × 2
Create colored text output, draw borders, and potentially simple animations
Connection to this lecture:
Video memory at B800:0000 is an example of memory-mapped I/O — the display adapter maps its RAM into the CPU's address space, so you write to it with regular MOV instructions rather than IN/OUT
This contrasts with the port-mapped I/O approach (keyboard controller) covered in this lecture
The 2D screen layout (80 columns × 25 rows) uses the same row-major addressing formula as the 2D arrays in Section 12 <br><a data-tooltip-position="top" aria-label="https://toshiba.semicon-storage.com/ap-en/semiconductor/knowledge/e-learning/micro-intro/chapter2/types-of-bus-lines.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://toshiba.semicon-storage.com/ap-en/semiconductor/knowledge/e-learning/micro-intro/chapter2/types-of-bus-lines.html" target="_self">Bus Line Types</a> — Toshiba semiconductor e-learning on data, address, and control buses
<br><a data-tooltip-position="top" aria-label="https://deeprajbhujel.blogspot.com/2015/12/keyboard-and-display-controller.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://deeprajbhujel.blogspot.com/2015/12/keyboard-and-display-controller.html" target="_self">Keyboard and Display Controller 8279</a> — Introduction to the Intel 8279 keyboard/display controller
<br><a data-tooltip-position="top" aria-label="https://www.basicinputoutput.com/2024/11/the-keyboard-controller-interface.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.basicinputoutput.com/2024/11/the-keyboard-controller-interface.html" target="_self">The Keyboard Controller Interface, 0x60/0x64</a> — Detailed walkthrough of keyboard controller ports, status register, and commands
<br><a data-tooltip-position="top" aria-label="https://www.plantation-productions.com/Webster/www.artofasm.com/DOS/pdf/apndxc.pdf" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.plantation-productions.com/Webster/www.artofasm.com/DOS/pdf/apndxc.pdf" target="_self">Art of Assembly Language — Appendix C</a> — Keyboard scan codes and microcontroller commands (PDF)
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Memory_hierarchy" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Memory_hierarchy" target="_self">Memory Hierarchy</a> — Wikipedia overview of the computer memory hierarchy
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Cache_hierarchy" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Cache_hierarchy" target="_self">Cache Hierarchy</a> — Wikipedia on multi-level cache design, AAT calculations, and evolution
<br><a data-tooltip-position="top" aria-label="https://hazelcast.com/foundations/caching/caching/" rel="noopener nofollow" class="external-link is-unresolved" href="https://hazelcast.com/foundations/caching/caching/" target="_self">What Is Caching?</a> — Hazelcast overview of caching concepts and limitations
<br><a data-tooltip-position="top" aria-label="https://www.computerlanguage.com/results.php?definition=RAM+cache" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.computerlanguage.com/results.php?definition=RAM+cache" target="_self">RAM Cache</a> — Computer Language Company definition of RAM cache
<br><a data-tooltip-position="top" aria-label="https://www.enterprisestorageforum.com/hardware/types-of-computer-memory/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.enterprisestorageforum.com/hardware/types-of-computer-memory/" target="_self">9 Types of Computer Memory Defined</a> — Comprehensive overview of memory technologies
<br><a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/computer-memory/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/computer-memory/" target="_self">Computer Memory</a> — GeeksforGeeks guide to memory types and hierarchy
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Virtual_memory" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Virtual_memory" target="_self">Virtual Memory</a> — Wikipedia on virtual memory concepts, paging, and history
<br><a data-tooltip-position="top" aria-label="https://afteracademy.com/blog/what-are-paging-and-segmentation/" rel="noopener nofollow" class="external-link is-unresolved" href="https://afteracademy.com/blog/what-are-paging-and-segmentation/" target="_self">What are Paging and Segmentation?</a> — AfterAcademy explanation of paging mechanisms
]]></description><link>lectures/eng/l08-eng.-memory-systems-io-devices-and-low-level-data-structures.html</link><guid isPermaLink="false">Lectures/ENG/L08-ENG. Memory Systems IO Devices and Low-Level Data Structures.md</guid><pubDate>Tue, 10 Mar 2026 06:00:29 GMT</pubDate><enclosure url="assets/l08-network-switch.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l08-network-switch.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L07-ENG. Interrupts and OS Interaction]]></title><description><![CDATA[
Lecture 7 of Computer Systems Architecture course at NaUKMA.
After this lecture you will be able to:
Describe the complete boot process from pressing the power button through POST, BIOS/UEFI, MBR, and OS kernel loading
Explain the difference between hardware interrupts and software interrupts
Describe the structure of the Interrupt Vector Table (IVT) and how the CPU uses it to dispatch interrupt handlers
Explain how the INT and IRET instructions work, including the state saved on the stack
Use BIOS interrupts (INT 10h for video, INT 16h for keyboard) and DOS interrupts (INT 21h) in assembly programs
Describe Linux system calls as the modern equivalent of DOS interrupts
Explain the limitations of Real Mode and the improvements introduced by Protected Mode
Describe protection rings (Ring 0–3) and how they enforce security boundaries between the kernel and user programs
Every time you press the power button on a computer, a precisely choreographed sequence unfolds. Within seconds, a machine that had no instructions in its working memory boots an entire operating system, complete with device drivers, filesystems, and a user interface. Yet at the moment of power-on, the CPU is in its most primitive state: it knows nothing about the hard drive, nothing about the keyboard, and nothing about the operating system installed on the disk. How does a processor that can only execute instructions — one at a time, from memory — pull itself up from nothing?This is the <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Bootstrapping" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Bootstrapping" target="_self">bootstrapping</a> problem, named after the absurd image of "pulling oneself up by one's bootstraps." The solution is a layered handoff: a tiny program in firmware loads a slightly larger program from disk, which loads a yet larger program, until finally the full operating system is running. Each layer trusts that the previous layer did its job correctly, and each layer is more capable than the last.Lecture 7 — titled "Boot Process and OS Functions: Parallelism, Protection, and Large Memory" (Процес завантаження і функції OS. Паралелізм, захист, і великі об'єми RAM) — explores the mechanisms that make this layered handoff work, and then examines the fundamental interface between software and hardware: interrupts.The lecture is organized in three parts:Part 1: The Boot Process and Bootstrapping. We trace the full journey from pressing the power button to a running operating system. This involves the CPU's reset vector, the Power-On Self-Test (POST), BIOS firmware, the Master Boot Record, and finally kernel initialization. Understanding the boot process is important not only as a systems concept, but because it directly explains why the Real Mode environment you have been programming in works the way it does.Part 2: Interrupts — IVT, INT/IRET, BIOS and DOS Services, Linux System Calls. We examine the interrupt mechanism in detail: how the CPU dispatches interrupts through the Interrupt Vector Table (IVT), how the INT and IRET instructions work at the stack level, and how both BIOS and DOS expose services through software interrupts. We then connect this to the modern world by looking at Linux system calls as the Protected Mode successor to the DOS interrupt interface.Part 3: Protected Mode — Security Rings, Large Memory, and Parallelism. We revisit the Real Mode vs. Protected Mode distinction introduced in L06 and go deeper: examining the hardware privilege ring system (Ring 0 through Ring 3), how Protected Mode enables addressing far more than 1 MB of memory, and how it provides the foundation for multitasking. This section bridges the conceptual gap between the single-program DOS environment of your current lab work and the multi-process Linux environment you will transition to later in the course.You might wonder why a course focused on assembly programming spends time on the boot process. The reason is direct and practical: the Real Mode environment you have been programming in — DOSBox running DOS — exists precisely because of the boot process we are about to describe. When DOSBox starts, it emulates the state of an IBM PC that has completed booting into DOS. The memory map your programs use (the IVT at 0000:0000, video memory at B800:0000, the program segment prefix at the start of your .COM files) is the memory map that the BIOS and DOS set up during boot. The interrupt services you call (INT 21h for DOS functions, INT 10h for video) were installed during boot. Understanding where these structures come from transforms them from arbitrary conventions into logical consequences of the boot sequence.Furthermore, when we transition to Linux assembly programming later in the course (Labs 11-15), you will be working in a fundamentally different environment — one that resulted from a different boot path. Understanding both paths will clarify why the two environments differ so dramatically.This lecture draws on several concepts from earlier in the course:
Segment:offset addressing (L03) — the BIOS, IVT, and MBR all rely on the Real Mode segmented memory model. We will reference segment:offset pairs throughout, but will not re-derive the physical address calculation.
Stack operations (L05) — the INT instruction saves state to the stack in a manner closely analogous to CALL, and IRET restores it like a specialized RET. Understanding PUSH, POP, and the stack pointer is essential.
The FLAGS register (L04) — particularly the IF (Interrupt Enable) flag, which controls whether the CPU responds to hardware interrupts.
Real Mode vs. Protected Mode (L06, Section 10) — we introduced the basic distinction and the memory map. This lecture extends that introduction with the interrupt mechanism and the privilege ring system.
When a computer is powered off, all of its software — the operating system, applications, and user data — exists only on non-volatile storage: hard drives, SSDs, or flash memory. The CPU's registers are cleared. RAM is empty (or, more precisely, contains unpredictable values — the residual electrical charges in DRAM cells decay over time without refresh). The processor has no operating system loaded, no device drivers, and no notion of what hardware surrounds it.This creates a fundamental chicken-and-egg problem. The operating system is a program stored on disk, but reading from disk requires a disk driver, which is part of the operating system. The CPU needs instructions to follow, but the instructions are on a storage device it does not yet know how to access. Even RAM, where programs must reside to execute, needs to be initialized — the memory controller must be configured, the DRAM timing parameters set, and the memory tested for errors before it can be reliably used.Yet the CPU is a simple machine: it fetches an instruction from memory, executes it, and advances to the next one. It cannot "look for an operating system" or "initialize hardware" on its own — those are software concepts, and there is no software running yet. It can only follow the instruction at the address pointed to by its instruction pointer. So the very first question of the boot process is: where does the CPU find its first instruction?The answer, as we will see, is elegantly simple: the CPU is hardwired to look at a specific, fixed address. At that address, the motherboard's chipset ensures that a non-volatile memory chip (the BIOS/UEFI firmware) is mapped — not RAM, which is unreliable at this point, but a flash ROM chip that retains its contents without power. Whatever instruction is stored there is the first instruction the computer will ever execute. From that single instruction, the entire boot process unfolds.<br>The answer is the <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Reset_vector" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Reset_vector" target="_self">reset vector</a> — a fixed physical address that the CPU is hardwired to jump to after a reset. This is not a configurable setting; it is not stored in any register or memory location that software can modify. It is built into the processor's silicon itself, part of the chip's design since manufacture. Every x86 processor, from the original 8086 to the latest designs, begins execution at a predetermined address after power-on or a hardware reset.The concept of a reset vector is not unique to x86. Virtually every processor architecture has one, though the specific address varies: ARM processors typically start at 0x00000000, MIPS at 0xBFC00000, and the Motorola 68000 reads its starting address from 0x00000004. The x86 family's choice of placing the reset vector near the top of the address space (rather than at 0) is unusual and reflects the design goal of keeping low memory available for the interrupt vector table and other system data structures.The specific address has evolved with the processor family:Notice the pattern: each successive processor generation places the reset vector 16 bytes below the top of its addressable memory. Why 16 bytes? Because that is just enough space for a far JMP instruction — and a jump is all that needs to be there. The reset vector is not the start of the BIOS; it is a trampoline that redirects to the start of the BIOS.The 8086 in detail. The reset state sets CS = FFFFh and IP = 0000h, forming the segmented address FFFFh:0000h, which maps to physical address FFFF0h (recall the segment:offset formula from L03: physical = segment * 16 + offset). This lands 16 bytes below the top of the 8086's 1 MB address space. The instruction at that address is typically:JMP FAR F000:E05B ; Jump to BIOS initialization entry point
<br><img alt="Reset vector in Turbo Debugger" src="assets/l07-reset-vector-td.png" target="_self">Turbo Debugger showing the first instruction at F000:FFF0 — a JMP to the BIOS entry point at F000:E05B.This far jump redirects the CPU to the actual start of the BIOS code, which resides at a lower address within the F000h segment. The BIOS ROM occupies the top 64 KB of the 1 MB address space (segment F000h), which is why the motherboard BIOS appears at segment F000h in the conventional memory map.<br>The 80386 and later processors. The reset vector moves to FFFFFFF0h — 16 bytes below the 4 GB boundary. This raises a puzzle: the processor starts in Real Mode, which nominally limits it to 1 MB of address space. How can it access an address near 4 GB? The answer involves a subtle hardware trick: after reset, the base portion of the CS register is initialized to FFFF0000h rather than the normal Real Mode value. This allows the initial instruction fetch to reach the BIOS flash chip mapped near the top of the 32-bit address space. The motherboard's chipset (historically, the <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Northbridge_(computing)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Northbridge_(computing)" target="_self">northbridge</a>) intercepts this memory access and routes it to the BIOS flash chip rather than to RAM. After the first far jump executes, CS reverts to normal Real Mode behavior, and the BIOS continues execution within the standard 1 MB address range.This design is elegant in its simplicity: no matter what state the system was in before power-on, the CPU always starts at the same fixed address, and that address always contains firmware code that knows how to initialize the hardware. The bootstrap chain begins.<br>The code at the reset vector belongs to the <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/BIOS" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/BIOS" target="_self">BIOS</a> — the Basic Input/Output System. The BIOS is firmware: software that is permanently stored on a chip on the motherboard (originally in ROM, later in flash memory that can be updated). It is the first software the CPU executes, and it has two critical responsibilities:
Hardware initialization and testing — the BIOS must discover what hardware is present, verify that it functions correctly, and configure it into a known good state.
Boot loading — once hardware is initialized, the BIOS must find and load the operating system (or, more precisely, the first-stage boot loader) from a storage device.
<br><img alt="BIOS EPROM chips" src="assets/l07-bios-chip-photo.jpg" target="_self">BIOS firmware stored on EPROM chips. The bottom chip (AMD AM27C256, 1986) has a quartz window for UV erasure — a precursor to modern flash-based firmware storage.The BIOS also provides a library of basic hardware interaction routines — the "Basic Input/Output" in its name — that are accessible through software interrupts. These BIOS interrupt services (INT 10h for video, INT 13h for disk, INT 16h for keyboard, and others) provide a standardized interface that early operating systems like DOS relied on heavily. We will examine these in detail in the interrupts section of this lecture.The BIOS code resides in the upper portion of the Real Mode address space. Recall the memory map from L06 (Section 10.1): the BIOS ROM is mapped at segment F000h (F0000h-FFFFFh), occupying the top 64 KB of the 1 MB Real Mode address space. This is permanent, read-only memory — your programs cannot overwrite it, and it remains available throughout the entire time the machine is running in Real Mode. When your assembly program executes INT 10h to print a character, the CPU looks up the interrupt handler address in the IVT, and that address points into the BIOS ROM at segment F000h.The name "BIOS" comes from the CP/M operating system (1975), where it referred to the hardware abstraction layer. When IBM developed the original IBM PC in 1981, they adopted both the concept and the name. The IBM BIOS was proprietary, but companies like Compaq, Phoenix Technologies, and AMI reverse-engineered compatible versions, enabling the entire IBM PC clone industry. This de facto standardization meant that any operating system written for the IBM PC BIOS interface would work on any compatible machine.<br>UEFI: The Modern Successor. Modern computers have largely replaced the traditional BIOS with <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/UEFI" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/UEFI" target="_self">UEFI</a> (Unified Extensible Firmware Interface). UEFI addresses many limitations of the legacy BIOS: it supports booting from disks larger than 2 TB (the MBR partition table is limited to 2 TB due to its 32-bit sector addressing), provides a richer pre-boot environment with graphical interfaces and mouse support, can run in 32-bit or 64-bit mode from the start, and eliminates the 512-byte MBR constraint entirely. However, the conceptual role remains identical — UEFI is firmware that initializes hardware and loads the operating system. For this course, we focus on the legacy BIOS boot process because it directly corresponds to the Real Mode environment you work in and because understanding the legacy path makes the UEFI improvements easier to appreciate.<br>The first major task the BIOS performs after receiving control is the <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Power-on_self-test" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Power-on_self-test" target="_self">Power-On Self-Test</a>, or POST. This is a systematic diagnostic process that verifies the computer's essential hardware components are functional before attempting to boot an operating system. If critical hardware has failed, there is no point in trying to load an OS — the POST catches these failures early.When the CPU begins executing BIOS code, the firmware must first establish its own working environment. The scope of this initialization depends on the type of boot:
Cold boot (full power-on after the machine was off) — the BIOS executes the complete POST sequence from scratch. All hardware must be discovered, tested, and initialized. This is the slow path.
Warm boot (triggered by Ctrl+Alt+Del, or by software calling INT 19h) — the BIOS detects this condition through a special flag value stored in non-volatile memory (CMOS) and can bypass the lengthy memory test and some hardware re-initialization. The system was just running, so the BIOS can reasonably assume that hardware that was working a moment ago is still functional.
This cold/warm distinction is why restarting your computer (warm boot) is noticeably faster than turning it on from a fully powered-off state (cold boot). Modern UEFI systems have further optimized this with "fast boot" options that skip even more hardware checks on the assumption that hardware rarely fails between reboots.The POST proceeds through a structured sequence of hardware checks. The principal duties include:
CPU register verification — confirm that the processor's registers can be written and read correctly. A CPU with a stuck register bit cannot reliably execute any software.
BIOS code integrity check — the firmware verifies its own checksum to ensure the BIOS code itself has not been corrupted. A corrupted BIOS cannot be trusted to correctly test anything else.
RAM check — the POST sizes and tests the system's main memory. This is one of the most time-consuming POST steps: the BIOS writes test patterns (such as alternating 0x55 and 0xAA bytes) to each memory location and reads them back to verify correctness. Any location that returns a different value than what was written indicates a faulty memory cell. This is why older computers displayed a "memory count" during boot — you could watch the POST testing each block of RAM, with the count incrementing from 0 to the total installed memory. Modern systems with gigabytes of RAM would take minutes to test every byte, so fast boot options often skip or abbreviate this check.
Basic component verification — the DMA (Direct Memory Access) controller, system timer, and interrupt controller are tested. These are fundamental to all subsequent hardware operations.
Hardware polling and initialization — the POST discovers and initializes other system components: the chipset, system buses, video card, keyboard, storage controllers, and other integrated peripherals. Later BIOS versions also scan for extension ROMs (option ROMs) — firmware embedded on expansion cards like video adapters or network cards — and execute their initialization routines as well.
What happens if POST detects a failure? The answer depends on which component failed and when in the POST sequence the failure was detected.At the early stages of POST, the video system may not yet be initialized, so the BIOS cannot display an error message on screen. Instead, it uses the motherboard's built-in speaker to produce beep codes — specific patterns of beeps that indicate which component failed. For example, one long beep followed by three short beeps might indicate a video card failure, while continuous short beeps might indicate a RAM failure (the exact codes vary by BIOS manufacturer — AMI, Award, and Phoenix each use different patterns). This is a clever engineering solution: the PC speaker is connected directly to the system timer and requires no complex initialization, so it can be driven even when most of the system is non-functional.If the failure occurs after the video system is initialized, the BIOS can display an error message on screen, such as "CMOS checksum error" or "Keyboard error." Some failures are fatal (the POST halts and the system cannot boot), while others are non-fatal (the BIOS displays a warning but continues booting).Many modern BIOS and UEFI implementations hide the POST process behind a manufacturer's logo screen (a "quiet boot" splash screen). The traditional text-mode POST output — showing memory counts, detected drives, and component tests — can usually be re-enabled in the BIOS settings for diagnostic purposes.If the POST completes successfully, the BIOS has established that the machine's core hardware is working. RAM is mapped and tested. The video system is initialized (you can see text on screen). The keyboard controller is active. Storage devices have been detected. The BIOS setup prompt window has passed (the brief moment where pressing Del or F2 enters configuration). The machine is now ready for the next phase: finding and loading the operating system.<br>After POST completes, the BIOS calls <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/BIOS_interrupt_call" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/BIOS_interrupt_call" target="_self">INT 19h</a> — the bootstrap loader interrupt — to begin the boot device search. The BIOS maintains a boot order list, stored in non-volatile CMOS memory and configurable by the user through the BIOS setup utility (the screen you see when pressing Del or F2 during startup). This list specifies the priority of devices to check:
Hard disk drive (or SSD)
USB flash drive
Optical disc (CD/DVD)
Network (PXE boot)
Other devices
The BIOS examines each device in order, attempting to read the first sector (512 bytes) from the device. It does this using its own disk access routines — at this stage, these are the only way to read from storage, since no operating system or driver is loaded yet. For a device to be considered bootable, this first sector must end with a specific two-byte signature: 0x55 followed by 0xAA at byte offsets 510 and 511. This is the boot signature (sometimes called the "magic number"):Offset: 0x000 ... 0x1FD 0x1FE 0x1FF
Content: [boot code...] 0x55 0xAA
If the BIOS reads a sector that does not end with this signature, it considers the device non-bootable, skips it, and tries the next one in the boot order.If no bootable device is found at all, older BIOSes would display "No ROM BASIC" (a relic of the original IBM PC, which had a BASIC interpreter in ROM as a fallback) or "No bootable disk found." Modern firmware simply enters the BIOS configuration utility or displays an error prompt.<br>Network booting. One boot option worth noting is <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Preboot_Execution_Environment" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Preboot_Execution_Environment" target="_self">PXE</a> (Preboot Execution Environment) network booting. When the BIOS tries a network adapter as a boot device, the adapter's firmware (or the BIOS's built-in PXE support) performs the following sequence:
Broadcasts a DHCP request on the local network to obtain an IP address
Receives a DHCP response that includes the address of a TFTP server and a boot image filename
Downloads that boot image from the server using TFTP (Trivial File Transfer Protocol)
Loads the downloaded image into memory and transfers control to it, just as it would to an MBR loaded from disk
This allows diskless workstations to boot from a central server — a technique widely used in university computer labs, corporate environments, and cloud data centers where centralized management of operating system images is more practical than maintaining individual disks on each machine.<br>When the BIOS finds a bootable device, it loads the first 512-byte sector into memory at physical address 0x7C00 (or in segment:offset notation, 0000:7C00) and transfers control to that address. This sector is the <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Master_boot_record" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Master_boot_record" target="_self">Master Boot Record</a> (MBR), and it has a carefully defined structure:The MBR boot loader code is a first-stage boot loader — an extremely constrained program that must fit in just 446 bytes. Running in 16-bit Real Mode, it faces a remarkable engineering challenge: within those 446 bytes, it must parse the partition table, identify the active (bootable) partition, load the first sector of that partition (the Volume Boot Record), and transfer control to it. It must also handle error cases (no active partition found, disk read failure) and display minimal error messages — all within a space smaller than a single tweet.Why 0x7C00? The choice of loading address 0x7C00 is a historical artifact from the original IBM PC 5150 (1981). The IBM PC shipped with a minimum of 32 KB of RAM. The BIOS developers at IBM needed a location that would leave enough room below for the interrupt vector table and BIOS data area (which occupy the first ~1.25 KB), and enough room above for the boot loader to use as a stack and working space. The address 0x7C00 is exactly 1 KB below the 32 KB mark (32 KB = 0x8000, minus 1 KB = 0x7C00), providing the boot sector with approximately 30 KB of usable memory below it and leaving the top of the 32 KB for the stack. This seemingly arbitrary address has persisted for over four decades of x86 computing.The constraint is severe but not insurmountable. The original IBM PC MBR code, written by David Litton at IBM, accomplished all of this in about 200 bytes of hand-optimized 8086 assembly — a testament to the art of efficient low-level programming.This is the second link in the bootstrap chain. The BIOS, which is firmware on the motherboard, has loaded a tiny program from the disk. That tiny program will in turn load a larger program — the second-stage boot loader.<br>The second-stage boot loader (such as <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/GNU_GRUB" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/GNU_GRUB" target="_self">GRUB</a> on Linux, or BOOTMGR on Windows) is not subject to the 512-byte limitation. It can occupy kilobytes or even megabytes of disk space, giving it room to implement sophisticated functionality:
Filesystem support — the second-stage loader understands filesystem structures (FAT, ext4, NTFS) and can locate files by name rather than by raw sector number.
Interactive boot menu — if multiple operating systems are installed, the loader presents a menu allowing the user to choose which one to boot.
Kernel loading — the loader reads the OS kernel image from the filesystem, loads it into the appropriate memory location, and prepares any initial RAM filesystem (initrd/initramfs) the kernel needs.
Boot parameter passing — the loader can pass configuration parameters to the kernel, such as the root filesystem device, boot flags, or debugging options.
<br>Security features — modern boot loaders can verify cryptographic signatures on the kernel image before loading it, ensuring that the kernel has not been tampered with. This is part of the <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/UEFI#Secure_Boot" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/UEFI#Secure_Boot" target="_self">Secure Boot</a> chain of trust.
A typical GRUB boot menu might look like this:GNU GRUB version 2.06 Ubuntu, with Linux 5.15.0-91-generic Ubuntu, with Linux 5.15.0-91-generic (recovery mode) Ubuntu, with Linux 5.15.0-88-generic Windows Boot Manager (on /dev/sda1) Use the ↑ and ↓ keys to select which entry is highlighted. Press enter to boot the selected OS, `e` to edit the commands before booting or `c` for a command-line.
The user selects an entry, and the boot loader loads the corresponding kernel. If no selection is made within a configurable timeout (usually a few seconds), the default entry boots automatically.<br>UEFI boot path. On UEFI systems, the process is somewhat different. UEFI does not use the MBR at all; instead, it reads the boot loader directly from a dedicated <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/EFI_system_partition" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/EFI_system_partition" target="_self">EFI System Partition</a> (ESP) formatted as FAT32. The UEFI firmware itself understands the FAT filesystem, so it can locate and load the boot loader file (such as \EFI\BOOT\BOOTX64.EFI) without any first-stage intermediary. This eliminates the 446-byte constraint entirely and is one of the key advantages of UEFI over legacy BIOS.Once the boot loader has loaded the operating system kernel into RAM and transferred control to it, the kernel takes over. The kernel's initial tasks include:
Mode transition — one of the kernel's first actions is to switch the CPU from Real Mode into Protected Mode (or directly into 64-bit Long Mode on modern systems). This is a critical, irreversible moment: the processor transitions from the 1 MB, no-protection environment of the BIOS into the full 4 GB (or larger) address space with memory protection and privilege levels enabled. After this switch, the BIOS interrupt services (INT 10h, INT 13h, etc.) are no longer available — the kernel must use its own drivers from this point forward. We will explore Protected Mode in detail in Part 3 of this lecture.
Device initialization — the kernel probes for hardware devices and loads the appropriate drivers. Unlike the BIOS, which provides only basic hardware access, the kernel loads full-featured drivers that can exploit all capabilities of the hardware.
Filesystem mounting — the kernel mounts the root filesystem, making the disk's directory structure accessible to the system. Before this point, the system could only read raw disk sectors; after mounting, it can access files by name and path. This is often a two-step process: the kernel first mounts an initial RAM disk (initramfs) that contains just enough drivers and tools to find and mount the real root filesystem on disk.
Service and daemon startup — the kernel launches the first user-space process, which has process ID 1 (PID 1). On modern Linux this is systemd; on traditional Unix systems it was init. This first process then starts all other system services: networking, audio, print spoolers, SSH servers, display managers, and any other daemons the system requires.
Runtime environment — finally, the system presents a user interface — a login prompt, a desktop environment, or a command shell — and the computer is ready for use.
At this point, the boot process is complete. The operating system is in full control, running in Protected Mode (or Long Mode), with hardware access mediated through drivers and system calls rather than through BIOS interrupts. The BIOS firmware, the MBR, and the boot loader have all served their purpose and are no longer active — though the BIOS ROM remains mapped in memory (at segment F000h) for historical and compatibility reasons.The entire sequence, from the CPU's first instruction at the reset vector to a fully operational desktop, typically takes only a few seconds on modern hardware with NVMe storage and optimized firmware. Yet it involves a remarkable chain of trust and handoff: silicon hardwired address to firmware to disk sector to boot loader to kernel to user space. Each layer is more capable than the one before it, and each depends on the previous layer having done its job correctly. A failure at any stage breaks the entire chain — which is why understanding the boot process is essential for diagnosing "my computer won't start" problems.Notice how each stage of the boot process is constrained by what the previous stage has set up. The CPU can only jump to the reset vector because that is hardwired. The BIOS can only test hardware using code baked into firmware. The MBR can only use BIOS interrupt services (like INT 13h for disk reads) because no other disk access mechanism exists yet. The second-stage boot loader can use those same BIOS services to read filesystem structures. And the kernel, once loaded, replaces all of this with its own drivers and takes full ownership of the machine. Each layer bootstraps the next.The full boot process can be summarized as a series of handoffs:<br><img alt="Boot sequence timeline" src="assets/l07-boot-sequence-timeline.png" target="_self">The complete boot timeline: BIOS initialization through MBR and boot loader in Real Mode, then the switch to Protected Mode for kernel initialization and user-mode processes.Each transition moves from a simpler, more constrained environment to a richer one. The CPU starts knowing nothing; the BIOS gives it hardware access; the MBR gives it disk access; the boot loader gives it filesystem access; and the kernel gives it everything else. This is bootstrapping — the machine literally pulls itself up, layer by layer, from nothing to a fully functional operating system.You may have noticed that one term keeps appearing in the boot process description: interrupt. The BIOS calls INT 19h to start the boot search. The boot sector is loaded at a specific address. BIOS services like INT 10h and INT 13h are available for the MBR and boot loader to use. What exactly are these interrupts, and how do they work? That is the subject of the next section.In the previous sections we traced the boot process from the moment the PC is powered on: the CPU begins executing BIOS code from ROM, the BIOS performs POST and initializes hardware, and the Interrupt Vector Table is populated with addresses of BIOS service routines. But the BIOS itself does not contain an operating system. Its final act in the boot sequence is to load a tiny but critical piece of code from the very first sector of the boot disk: the Master Boot Record (MBR).<br>The <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Master_boot_record" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Master_boot_record" target="_self">Master Boot Record</a> is a 512-byte data structure occupying the first sector (sector 0) of a partitioned storage device. Introduced in 1983 with PC DOS 2.0, the MBR serves two purposes simultaneously: it describes how the disk is divided into partitions, and it contains a small executable program — the boot loader — that begins the process of loading an operating system.The MBR's 512 bytes are organized into three regions:<br><img alt="MBR hex dump" src="assets/l07-mbr-hex-dump.png" target="_self">Hex dump of an actual MBR. The boot code occupies the first 446 bytes. At offset 0x1BE, the partition table entries begin. The highlighted 55 AA at offset 0x1FE marks the boot signature. Note the error messages embedded in the code: "Invalid partition table", "Error loading operating system", "Missing operating system".The boot signature (0x55AA) at the end of the sector serves as a validity marker. When the BIOS reads sector 0 from the boot disk, it checks for this signature. If the signature is absent, the BIOS concludes that the disk is not bootable and tries the next device in the boot order.The partition table occupies 64 bytes and contains exactly four entries, each 16 bytes long, at fixed offsets:Each partition entry contains a boot indicator byte: 0x80 marks the partition as active (bootable), while 0x00 marks it as inactive. Only one partition should be marked active at a time. The entry also contains a System ID byte that identifies the filesystem type:<br>The four-entry limit means the MBR scheme supports at most four primary partitions per disk. The use of 32-bit sector addresses limits the maximum addressable disk size to 2 TiB (2^32 x 512 bytes). These limitations led to the development of the <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/GUID_Partition_Table" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/GUID_Partition_Table" target="_self">GUID Partition Table</a> (GPT) scheme, which has largely replaced MBR on modern systems.The 446-byte bootstrap code region contains a small real-mode program. When the BIOS finishes its initialization, it reads the MBR's 512 bytes into physical memory at address 0x7C00 and transfers control to that address by jumping to it. The CPU is in real mode at this point, so the bootstrap code is 16-bit x86 machine code.The bootstrap program performs a simple but essential task:
Scan the partition table for the entry marked as active (0x80)
Read the first sector of that active partition — its Volume Boot Record (VBR)
Transfer control to the VBR code
<br>This handoff from MBR to VBR is called <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Chain_loading" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Chain_loading" target="_self">chain loading</a>. The VBR contains a more specialized boot loader that understands the filesystem on its partition and can locate and load the actual operating system kernel. For example, the VBR of a Windows partition knows how to find and load NTLDR or bootmgr, while a Linux VBR might load GRUB's second stage.If the bootstrap code cannot find an active partition, or if the VBR fails to load, the MBR displays one of several familiar error messages: "Invalid partition table", "Error loading operating system", or "Missing operating system".To summarize where the MBR fits in the boot process we have studied so far:
Power on — CPU begins executing BIOS code from ROM
POST — BIOS tests and initializes hardware
IVT population — BIOS fills the Interrupt Vector Table with service routine addresses
MBR load — BIOS reads the first sector of the boot disk into memory at 0x7C00
Bootstrap execution — MBR code finds the active partition and loads its VBR
VBR / OS loader — The operating system's boot loader takes over
OS initialization — The OS kernel loads, replaces BIOS interrupt handlers with its own services
Step 7 is where the interrupt table story continues. Once the operating system loads, it installs its own service routines — which brings us to the next section.We have now seen how the system starts: the BIOS initializes hardware, populates the IVT, and chain-loads the operating system via the MBR. But what happens to the interrupt table after the OS loads? And how do programs running under the OS access system services? This section explains the layered service architecture that makes the interrupt mechanism so powerful.<br>During the boot process, the BIOS does more than just test hardware. It loads a collection of service functions into RAM and registers their addresses in the <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Interrupt_vector_table" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Interrupt_vector_table" target="_self">Interrupt Vector Table</a>. These are the BIOS interrupt services — standardized routines that provide low-level hardware access:
<br>INT 10h — <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/INT_10H" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/INT_10H" target="_self">Video services</a>: set video mode, write characters, draw pixels
INT 13h — Disk services: read/write disk sectors, get drive parameters
<br>INT 16h — <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/INT_16H" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/INT_16H" target="_self">Keyboard services</a>: read keystrokes, check keyboard buffer status
INT 14h — Serial port services
INT 17h — Printer services
These BIOS services form the lowest layer of the system's service architecture. They operate in real mode and talk directly to hardware through I/O ports and memory-mapped registers. Any program running in real mode can invoke them using the INT instruction.When the operating system loads, it does not discard the BIOS services entirely. Instead, it builds on top of them by loading its own service functions into RAM and updating the IVT to point to these new routines. The OS may: Add new entries — The OS defines interrupt numbers that the BIOS did not use. For example, MS-DOS claims INT 21h for its own comprehensive set of services. Replace existing entries — The OS may install its own handlers for interrupts previously handled by the BIOS. For instance, an OS might replace the BIOS disk handler (INT 13h) with a more sophisticated version that supports larger drives or implements caching. Chain to BIOS handlers — In some cases, the OS handler performs additional processing and then calls the original BIOS handler to do the actual hardware work. The OS saves the original IVT entry before overwriting it, so it can call through to the BIOS when needed. This layered approach means the IVT is a living data structure that evolves as the system boots. After the BIOS initializes it, the OS modifies it, and device drivers may modify it further. The result is a unified dispatch table where each interrupt number routes to the appropriate handler for the current system state.<br>BIOS <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/INT_10H" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/INT_10H" target="_self">INT 10h</a> provides fundamental video services. To use it, a program loads AH with a subfunction number, sets up other registers with parameters, and executes INT 10h. Some commonly used subfunctions:For example, to print the letter 'A' in teletype mode:mov ah, 0Eh ; Teletype output subfunction
mov al, 'A' ; Character to print
mov bh, 0 ; Page number 0
int 10h ; Call BIOS video service
INT 10h is relatively slow because it processes one character at a time through the BIOS. For performance-critical applications like games, programmers bypass INT 10h and write directly to video RAM (starting at segment B800h in text mode). But for general-purpose output during boot or in simple programs, INT 10h provides a hardware-independent interface that works across different video cards.<br>BIOS <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/INT_16H" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/INT_16H" target="_self">INT 16h</a> handles keyboard input. Its most important subfunctions:Reading a single keypress:mov ah, 00h ; Read key press subfunction
int 16h ; Call BIOS keyboard service
; Now: AL = ASCII code of the key, AH = scan code
The distinction between AH=00h and AH=01h is important. AH=00h blocks — it halts program execution until a key is pressed. AH=01h is a non-blocking check: it sets the Zero Flag if no key is available, allowing the program to continue doing other work and poll the keyboard periodically.<br>When MS-DOS loads, it installs a comprehensive set of services under <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/DOS_API" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/DOS_API" target="_self">INT 21h</a>. Where BIOS interrupts provide raw hardware access, DOS interrupts provide higher-level abstractions: file operations, directory management, string I/O, memory allocation, and program control.You have already used INT 21h in your lab assignments. Now we can explain why it works the way it does. The convention is the same as BIOS services: load AH with the subfunction number, set up parameters in other registers, and execute INT 21h.Key DOS subfunctions:A complete example — printing a string and exiting:.model small
.stack 100h .data
msg db 'Hello from DOS!$' .code
main proc mov ax, @data mov ds, ax ; Print the string using INT 21h, AH=09h mov ah, 09h lea dx, msg int 21h ; Terminate the program using INT 21h, AH=4Ch mov ah, 4Ch mov al, 0 ; Return code 0 (success) int 21h
main endp
end main
<br><img alt="DOS Hello World with INT 21h" src="assets/l07-dos-hello-world-int21h.png" target="_self">A familiar program: the Hello World example uses INT 21h twice — with AH=09h to print a string and AH=4Ch to exit. The green arrows highlight the interrupt calls.This is the same pattern you have been using since Lab 5. The mov ah, 4Ch / int 21h sequence does not just "end the program" — it invokes DOS interrupt service 21h, subfunction 4Ch, which performs an orderly shutdown: closes open files, frees allocated memory, and returns control to the DOS command interpreter.The complete picture forms a hierarchy of service layers:Each layer builds on the one below it. DOS file operations ultimately call BIOS disk routines, which ultimately read and write hardware ports. The interrupt mechanism provides the "glue" between these layers, and the IVT is the lookup table that connects interrupt numbers to handler addresses.<br>We have used the word "interrupt" throughout this lecture in the context of service calls — software deliberately invoking INT 21h or INT 10h. But <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Interrupt" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Interrupt" target="_self">interrupts</a> are a broader and more fundamental concept in computer architecture. They are the CPU's primary mechanism for responding to events, whether those events originate from external hardware or from the program itself.An interrupt is a request for the processor to suspend its currently executing code so that a specific event can be handled. When the CPU accepts an interrupt, it:
Finishes the current instruction (interrupts are recognized at instruction boundaries)
Saves the current processor state (FLAGS and CS:IP) onto the stack
Looks up the handler address in the Interrupt Vector Table using the interrupt number
Transfers control to the interrupt handler (also called an Interrupt Service Routine, or ISR)
When the handler completes, it executes the IRET (Interrupt Return) instruction, which restores FLAGS and CS:IP from the stack, resuming execution exactly where it left off.This mechanism is what makes the INT instruction work for service calls — but the same mechanism also handles keyboard presses, timer ticks, disk completion signals, and error conditions.Interrupts divide into two fundamental categories based on their origin:Hardware interrupts are signals sent to the CPU by external device controllers via IRQ (Interrupt Request) lines. When a hardware event occurs — a key is pressed, a disk transfer completes, the system timer ticks — the device controller asserts its IRQ line to get the CPU's attention.<br>The original IBM PC used an Intel 8259 <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Programmable_interrupt_controller" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Programmable_interrupt_controller" target="_self">Programmable Interrupt Controller</a> (PIC) to manage hardware interrupts. The PIC accepts interrupt signals from multiple devices and presents them to the CPU one at a time, in priority order. The standard IRQ assignments on the original PC:Notice that hardware interrupts are mapped to interrupt numbers 08h-0Fh in the IVT. When IRQ 1 fires (keyboard), the CPU looks up IVT entry 09h and jumps to the keyboard interrupt handler. This handler reads the scan code from the keyboard controller (I/O port 60h), translates it, places it in the keyboard buffer, and returns via IRET. All of this happens transparently — the running program has no idea it was interrupted.<br><img alt="Hardware interrupt handling" src="assets/l07-hardware-interrupts-diagram.png" target="_self">Hardware interrupts: device controllers (timer, USB, keyboard) signal the CPU through an interrupt controller. Each device has a dedicated interrupt handler routine.Key characteristic: asynchronous. Hardware interrupts arrive at unpredictable times, independent of what the CPU is currently doing. The timer interrupt (IRQ 0) fires approximately 18.2 times per second regardless of what program is running. The keyboard interrupt fires whenever the user presses or releases a key. The CPU must be ready to handle these events at any instruction boundary.Hardware interrupts can be masked (temporarily disabled) using the CLI (Clear Interrupt Flag) instruction, and re-enabled with STI (Set Interrupt Flag). When interrupts are masked, the CPU ignores incoming IRQ signals. This is useful when executing a critical section of code that must not be interrupted:cli ; Disable hardware interrupts
; ... critical section: modify shared data structures ...
sti ; Re-enable hardware interrupts
However, some interrupts cannot be masked. A Non-Maskable Interrupt (NMI) signals a critical hardware condition, such as a memory parity error or watchdog timer expiration, that must be handled immediately regardless of the interrupt flag state.Software interrupts are triggered deliberately by the INT n instruction, where n is the interrupt number (0-255). The CPU handles a software interrupt using exactly the same mechanism as a hardware interrupt: push FLAGS and CS:IP to the stack, look up the handler address in IVT entry n, and jump to it. The only difference is the trigger — an explicit instruction rather than an external signal.Software interrupts serve two purposes:1. System service calls. This is what we have been studying throughout this lecture. The INT instruction is the gateway through which programs request services from the BIOS and OS:; Software interrupt as a service call
mov ah, 02h ; DOS: output character
mov dl, 'X' ; Character to output
int 21h ; Invoke DOS service — SOFTWARE interrupt
2. CPU exceptions. Certain error conditions cause the CPU to generate a software interrupt automatically. These are not triggered by an INT instruction in the program but by the CPU detecting an abnormal condition:When the CPU executes INT n, the following sequence occurs:1. Push FLAGS onto the stack
2. Clear IF (Interrupt Flag) and TF (Trap Flag)
3. Push CS onto the stack
4. Push IP onto the stack
5. Load CS:IP from IVT entry n (address = n * 4)
6. Begin executing the interrupt handler
And when the handler executes IRET:1. Pop IP from the stack
2. Pop CS from the stack
3. Pop FLAGS from the stack (restoring IF and TF)
4. Resume execution at the restored CS:IP
This is very similar to a CALL / RET pair, with one crucial difference: INT also saves and restores the FLAGS register. This is necessary because the interrupt handler may modify flags during its operation, and the interrupted code must resume with its original flag state intact.Recall from Lecture 5 that CALL pushes only the return address (CS:IP) onto the stack. The INT instruction pushes three values: FLAGS, CS, and IP. This means the stack frame for an interrupt looks like: +-----------+
SP -&gt; | IP | (return address offset) +-----------+ | CS | (return address segment) +-----------+ | FLAGS | (saved processor flags) +-----------+ | ... | (previous stack contents)
A critical insight is that the CPU handles hardware and software interrupts identically once the interrupt number is determined. Whether IRQ 1 fires (mapped to INT 09h) or a program executes INT 09h, the CPU performs exactly the same steps: save state, look up IVT entry 09h, jump to the handler. The handler itself cannot tell whether it was invoked by hardware or software.This design unification is elegant and powerful. It means that:
The same IVT serves both hardware and software interrupts
Interrupt handlers do not need separate mechanisms for hardware vs. software invocation
A program can deliberately trigger a hardware interrupt's handler by executing the corresponding INT instruction (though this is rarely useful in practice)
The OS can install a single handler that services both hardware events and software requests for the same interrupt number
The interrupt mechanism is the thread that ties together everything in this lecture:
The IVT is the dispatch table, populated first by the BIOS and then modified by the OS
The MBR boot process leads to the OS loading, which installs new interrupt handlers
BIOS services (INT 10h, INT 16h) are accessed via software interrupts
DOS services (INT 21h) are accessed via software interrupts
Hardware devices (keyboard, timer) signal the CPU via hardware interrupts
All of these use the same CPU mechanism: save state, look up handler, execute, IRET
This unified architecture is one of the most influential design decisions in the x86 platform. It provides a single, consistent mechanism for everything from handling a keypress to requesting a file operation to recovering from a divide-by-zero error.By this point in the course, you have already used int 21h in your lab programs — to print strings with AH=09h and to terminate with AH=4Ch. Now it is time to understand the full picture of what DOS interrupt 21h actually provides and how it works under the hood.DOS reserves interrupt vectors 20h through 2Fh for its own use. Of these, INT 21h is the primary service dispatcher — the main API that application programs use to interact with the operating system. It provides over 80 different functions covering console I/O, file operations, memory management, and program control.The calling convention is straightforward: place the function number in AH, load any additional parameters into the appropriate registers, and execute int 21h. The CPU saves flags and the return address on the stack, looks up the handler address from the IVT at position 21h (memory address 0000:0084), and transfers control to the DOS kernel code that implements the requested function.The table below lists the functions you are most likely to encounter and use:Error handling: Many file-related functions signal errors by setting the Carry Flag (CF). When CF=1 after the call, AX contains an error code. When CF=0, the operation succeeded and AX holds the result (e.g., file handle or byte count). Always check the carry flag after file operations.To display the letter 'A' on screen:mov ah, 02h ; Function 02h: display character
mov dl, 'A' ; DL = character to display
int 21h ; Call DOS — 'A' appears on screen
Function 02h writes the character in DL to standard output. It also checks for Ctrl+C, so the user can break out of a program that outputs in a loop.This pattern should be familiar from your lab work since Lab 03:.data greeting db 'Hello, NaUKMA!', 0Dh, 0Ah, '$' .code mov ax, @data mov ds, ax mov ah, 09h ; Function 09h: display string mov dx, offset greeting ; DS:DX points to the string int 21h ; Print everything up to '$'
Note the '$' terminator — this is a quirk inherited from CP/M. The dollar sign marks the end of the string, which means you cannot use function 09h to print a literal $ character. The 0Dh, 0Ah sequence is carriage return + line feed, which moves the cursor to the beginning of the next line. mov ah, 01h ; Function 01h: read character with echo int 21h ; Wait for keypress; AL = ASCII code ; AL now holds the character the user typed ; The character was also displayed on screen automatically mov bl, al ; Save the character for later use mov ah, 02h ; Function 02h: display character mov dl, 0Dh ; Carriage return int 21h mov dl, 0Ah ; Line feed int 21h mov dl, bl ; The character we saved int 21h ; Display it again on the next line
If you want to read a character without displaying it (e.g., for password input), use function 07h instead of 01h.Every DOS program must end by calling function 4Ch. You have been doing this in every lab: mov ah, 4Ch ; Function 4Ch: terminate program mov al, 0 ; Return code 0 (success) int 21h ; Control returns to DOS (or the parent process)
The return code in AL can be checked by batch files (ERRORLEVEL) or by a parent process using function 4Dh. A return code of 0 conventionally means success; nonzero values indicate errors.While DOS interrupts provide high-level OS services, BIOS interrupts operate one layer below — they talk directly to hardware through firmware routines burned into the system ROM. Two of the most important BIOS interrupts are INT 10h for video services and INT 16h for keyboard services.<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/INT_10H" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/INT_10H" target="_self">INT 10h</a> is the 17th entry in the interrupt vector table (10h = 16 decimal, but IVT entries are zero-indexed, making it the 17th vector). The BIOS sets up a real-mode handler at this vector that provides video services: setting the video mode, positioning the cursor, outputting characters and strings, and reading/writing individual pixels in graphics mode.The calling convention is the same as DOS: load AH with the subfunction number, set other registers as required, and execute int 10h.Performance note: INT 10h is relatively slow because each call involves the full interrupt mechanism plus BIOS firmware code. For this reason, performance-critical programs (especially games) often bypass BIOS calls entirely and write directly to video memory at segment B800h (text mode) or A000h (graphics mode).<br>Mode restriction: On modern x86 systems, BIOS interrupt calls only work in real mode or virtual 8086 mode. They cannot be used in protected mode (32-bit) or long mode (64-bit) without expensive mode switches. Modern operating systems use dedicated video drivers instead. In UEFI systems, INT 10h is replaced by the <a data-tooltip-position="top" aria-label="https://uefi.org/specs/UEFI/2.10/12_Protocols_Console_Support.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://uefi.org/specs/UEFI/2.10/12_Protocols_Console_Support.html" target="_self">UEFI Graphics Output Protocol (GOP)</a>.To clear the entire screen by scrolling the whole window up with zero lines (which BIOS interprets as "clear"): mov ah, 06h ; Function 06h: scroll up window mov al, 0 ; AL=0 means clear entire window mov bh, 07h ; Attribute: white on black mov cx, 0000h ; Upper-left corner: row 0, col 0 mov dh, 24 ; Lower-right row (24 for 25-line display) mov dl, 79 ; Lower-right column (79 for 80-column display) int 10h ; Clear the screen ; Position the cursor at row 10, column 20 mov ah, 02h ; Function 02h: set cursor position mov bh, 0 ; Page 0 mov dh, 10 ; Row 10 mov dl, 20 ; Column 20 int 10h ; Print 'X' in bright green at the cursor position mov ah, 09h ; Function 09h: write char + attribute mov al, 'X' ; Character to write mov bh, 0 ; Page 0 mov bl, 0Ah ; Color attribute: bright green on black mov cx, 1 ; Write it once int 10h
The color attribute byte in BL has the format: bits 7-4 = background color, bits 3-0 = foreground color. Common values: 07h = white on black (default), 0Ah = bright green on black, 4Fh = white on red.<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/INT_16H" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/INT_16H" target="_self">INT 16h</a> is the keyboard BIOS interrupt. It lets programs read keystrokes, check whether a key is waiting in the buffer, and query the state of modifier keys (Shift, Ctrl, Alt, Caps Lock, etc.).Scan codes vs. ASCII: When a user presses a key, INT 16h returns two values. AL contains the ASCII code (e.g., 41h for 'A'), while AH contains the scan code — a hardware-level code that identifies the physical key regardless of what character it represents. For special keys (arrows, function keys, Home, End), AL is 00h or E0h, and the scan code in AH identifies which key was pressed. mov ah, 00h ; Function 00h: read key press (waits) int 16h ; Blocks until a key is pressed ; AH = scan code of the key ; AL = ASCII code (0 if special key) cmp al, 1Bh ; Was it Escape? (ASCII 1Bh) je exit_program cmp al, 0 ; Special key? (arrows, F-keys, etc.) je handle_special ; Otherwise, AL contains the typed character
Sometimes you need to check if a key is available without waiting (e.g., in a game loop):game_loop: ; ... update game state ... mov ah, 01h ; Function 01h: check buffer (non-blocking) int 16h jz no_key ; ZF=1 means no key waiting — skip input mov ah, 00h ; Key is available — read it int 16h ; Now AH=scan code, AL=ASCII ; ... process the key ... no_key: jmp game_loop
This pattern is essential for real-time programs: check if input is available, process it if so, and continue regardless.DOS functions (INT 21h) provide higher-level services. For example, function 01h reads a character, handles Ctrl+C, and echoes to the screen — all in one call. Under the hood, DOS itself calls BIOS interrupts to do the actual hardware work.BIOS functions (INT 10h, INT 16h) provide lower-level access. They give you direct control over cursor positioning, colors, and non-blocking input, but they do not provide file I/O, string handling, or program management. Use BIOS calls when you need precise control over the screen or keyboard beyond what DOS offers.Moving from the 16-bit DOS world to 32-bit Linux, the concept remains the same — application programs request OS services through a controlled gateway — but the mechanism and naming conventions differ. In Linux, these OS-provided functions are called system calls (syscalls).On 32-bit x86 Linux, the traditional way to invoke a system call is through INT 80h. The pattern mirrors what you have seen with DOS INT 21h:
Place the syscall number in EAX (not AH — Linux uses the full 32-bit register).
Place arguments in EBX, ECX, EDX, ESI, EDI, EBP (up to 6 parameters).
Execute int 80h.
The return value appears in EAX (negative values indicate errors).
Notice that Linux does not use a special string terminator. Instead, you always pass both a pointer to the data and its length — a more robust design.Like Unix file descriptors: 0 = stdin, 1 = stdout, 2 = stderr.<br>The full syscall table can be found in the kernel source file arch/x86/entry/syscalls/syscall_32.tbl, or by consulting references such as <a data-tooltip-position="top" aria-label="https://syscalls.w3challs.com/" rel="noopener nofollow" class="external-link is-unresolved" href="https://syscalls.w3challs.com/" target="_self">syscalls.w3challs.com</a>.; hello.asm — Linux x86 (32-bit) using int 80h
; Assemble: nasm -f elf32 hello.asm
; Link: ld -m elf_i386 -o hello hello.o section .data msg db 'Hello, World!', 0Ah ; 0Ah = newline len equ $ - msg ; Calculate string length section .text global _start _start: ; write(1, msg, len) mov eax, 4 ; syscall #4 = write mov ebx, 1 ; fd = 1 (stdout) mov ecx, msg ; pointer to the string mov edx, len ; number of bytes to write int 80h ; invoke the kernel ; exit(0) mov eax, 1 ; syscall #1 = exit mov ebx, 0 ; exit code 0 int 80h ; invoke the kernel (does not return)
Compare this to the DOS version. The structure is nearly identical — load a function number, set up parameters in registers, trigger the interrupt — but Linux uses 32-bit registers, explicit lengths instead of terminators, and a different interrupt vector. Also note that Linux programs begin at _start (not main, unless using the C library), and use section directives instead of .model / .data / .code.The int 80h mechanism works but is relatively slow. Each int instruction involves a full interrupt sequence: saving flags, looking up the IVT, switching privilege levels, and restoring state afterward.Modern x86 processors provide dedicated fast system call instructions that bypass the IVT entirely:
SYSENTER / SYSEXIT (Intel, introduced with Pentium II) — uses Model-Specific Registers (MSRs) to store the kernel entry point, avoiding the IVT lookup.
SYSCALL / SYSRET (AMD, adopted by Intel for 64-bit) — the standard mechanism on all x86-64 systems.
On 64-bit Linux, user programs invoke system calls through the SYSCALL instruction. The conventions also change: the syscall number goes in RAX, and arguments use RDI, RSI, RDX, R10, R8, R9 (different from the int 80h convention).In practice, application programmers rarely write int 80h or syscall directly. The C library (glibc, musl) wraps every system call in a regular function, so a C program simply calls write(), read(), or exit() and the library handles the low-level details. But understanding the raw mechanism is essential for systems programmers, security researchers, and anyone debugging at the assembly level.In Section 9, we saw that interrupts are the mechanism by which the CPU transfers control from a running program to a dedicated handler routine. But how does the CPU know where that handler is? In Real Mode, the answer is a simple and elegant data structure that occupies the very first kilobyte of memory: the Interrupt Vector Table (IVT).The x86 Real Mode IVT is an array of 256 entries located at the absolute beginning of physical memory, starting at address 0000:0000. Each entry is a 4-byte far pointer containing the segment:offset address of the corresponding interrupt handler (also called an Interrupt Service Routine, or ISR). The table layout is straightforward:With 256 entries at 4 bytes each, the IVT occupies exactly 256 x 4 = 1024 bytes (1 KB) of memory, spanning addresses 0000:0000 through 0000:03FF.<br><img alt="IVT structure" src="assets/l07-ivt-structure.png" target="_self">The Interrupt Vector Table occupies the first 1024 bytes of memory (addresses 000000h–0003FFh). Each 4-byte entry holds a segment:offset pair. The first 17 entries (INT 0–16) are reserved for CPU exceptions; entries 32–255 are available for user-defined handlers.<br><img alt="IVT memory layout" src="assets/l07-ivt-memory-layout.png" target="_self">The IVT in memory: 256 entries of 4 bytes each (IP low, IP high, CS low, CS high), starting at address 00000000. Each row shows 4 interrupt entries side by side.To find the IVT entry for interrupt number N, the CPU computes:Entry address = N x 4The first two bytes at that address are the offset (loaded into IP), and the next two bytes are the segment (loaded into CS). Because x86 is little-endian, the low byte of the offset comes first, followed by the high byte, then the low byte of the segment, and the high byte of the segment.The beginning of memory in Real Mode looks like this:Address Byte Content Interrupt
─────────────────────────────────────────────
0000:0000 [IP low] [IP high] INT 00h (Divide by Zero)
0000:0002 [CS low] [CS high] handler address
0000:0004 [IP low] [IP high] INT 01h (Single Step / Trap)
0000:0006 [CS low] [CS high] handler address
0000:0008 [IP low] [IP high] INT 02h (NMI)
0000:000A [CS low] [CS high] handler address
0000:000C [IP low] [IP high] INT 03h (Breakpoint)
0000:000E [CS low] [CS high] handler address ... ... ...
0000:0080 [IP low] [IP high] INT 20h (DOS: Terminate Program)
0000:0082 [CS low] [CS high] handler address
0000:0084 [IP low] [IP high] INT 21h (DOS: System Services)
0000:0086 [CS low] [CS high] handler address ... ... ...
0000:03FC [IP low] [IP high] INT FFh
0000:03FE [CS low] [CS high] handler address
Several notable entries in the IVT are assigned by the BIOS and DOS:Let us trace exactly how the CPU finds the handler for INT 21h, the DOS system call interrupt we have been using in our programs (for example, with AH=09h to print a string or AH=4Ch to exit).Step 1: Compute the IVT entry address.Entry address = 0x21 × 4 = 0x84
So the 4-byte vector for INT 21h is stored at physical address 0000:0084.Step 2: Read the 4 bytes at that address.Suppose we examine memory at 0000:0084 and find the following bytes:Address Contents
0000:0084 8A 12 76 02
Remember that x86 stores values in little-endian order. The first two bytes form the offset, and the next two form the segment:Offset (IP): bytes 8A 12 → 0x128A (low byte first)
Segment (CS): bytes 76 02 → 0x0276 (low byte first)
Step 3: Form the handler address.The DOS INT 21h handler is located at 0276:128A. Using the segment:offset formula from Lecture 3:Physical address = 0x0276 × 16 + 0x128A = 0x2760 + 0x128A = 0x39EA
When the CPU executes INT 21h, it will transfer execution to address 0276:128A (physical 0x39EA), where the DOS kernel's system call dispatcher begins.<br><img alt="IVT in Turbo Debugger" src="assets/l07-ivt-memory-dump-td.png" target="_self">Memory dump at 0000:0000 in Turbo Debugger showing the IVT. The green annotation traces INT 21h: entry 21h × 4 = 84h, so the vector is at offset 0084. Reading 4 bytes gives the CS:IP of the DOS interrupt handler.Now let us examine exactly what happens when the CPU encounters an INT N instruction. The INT instruction performs a sequence of operations that is similar to a far CALL (which pushes CS and IP, as covered in Lecture 5) but with the critical addition of saving and modifying the FLAGS register.Before the INT instruction executes, suppose we have the following state:CS:IP → points to the INT instruction
SS:SP → points to the current top of stack
FLAGS → current processor flags (IF=1, meaning interrupts enabled)
When INT N executes, the CPU performs these steps atomically (as a single indivisible operation): Push FLAGS onto the stack. The entire 16-bit FLAGS register is pushed, preserving the current state of all flags, including IF (Interrupt Flag) and TF (Trap Flag). Clear IF and TF in FLAGS. The Interrupt Flag is cleared to prevent further hardware interrupts from firing while the handler sets up its own context. The Trap Flag is cleared to prevent single-step traps within the handler. This is a safety measure: the handler needs uninterrupted time to save its own state before it can safely re-enable interrupts. Push CS onto the stack. The current code segment is saved so the handler can return to the correct segment. Push IP onto the stack. The IP of the next instruction after INT is pushed (not the IP of the INT instruction itself). This is the return address. Load CS:IP from the IVT. The CPU reads the 4-byte entry at address N × 4 and loads the offset into IP and the segment into CS. Execution continues at the handler. The CPU begins fetching and executing instructions at the new CS:IP. After these steps, the stack contains three additional words:Stack (growing downward): Address
┌──────────────────────┐
│ FLAGS │ ← SS:SP+4 (pushed first, so highest address)
├──────────────────────┤
│ CS │ ← SS:SP+2
├──────────────────────┤
│ IP │ ← SS:SP (pushed last, so current top)
└──────────────────────┘
Note the push order: FLAGS first, then CS, then IP. SP decreases by 2 for each push (recall from Lecture 5 that the stack grows downward on x86), so SP has decreased by 6 total.The similarity between INT and CALL/RET is intentional. Both are mechanisms for transferring control to another code location while preserving a return address. Here is a precise comparison:Think of INT N as a "far CALL with FLAGS": it does everything a far CALL does, plus it saves and modifies the processor's flag state. This extra step is essential because interrupt handlers may need to run with interrupts disabled and must guarantee that the interrupted program's flags are perfectly restored upon return.The counterpart to INT is IRET (Interrupt Return). Every interrupt handler must end with IRET to return control to the interrupted program. IRET performs the exact reverse of INT's stack operations: Pop IP from the stack. Restores the instruction pointer to the instruction that follows the original INT. Pop CS from the stack. Restores the code segment to its original value. Pop FLAGS from the stack. Restores the entire FLAGS register, including the IF flag. Since the saved FLAGS had IF=1 (interrupts were enabled before the INT), restoring FLAGS automatically re-enables interrupts. ; Simplified structure of an interrupt handler
my_handler: push ax ; Save any registers the handler will use push bx push ds ; ... handler logic ... pop ds ; Restore registers in reverse order pop bx pop ax iret ; Pop IP, CS, FLAGS — return to caller
The symmetry is elegant: INT pushes FLAGS, CS, IP (in that order), and IRET pops IP, CS, FLAGS (in reverse order). Because the stack is LIFO, the values come off in exactly the right sequence.Let us trace a concrete example. Suppose a program at CS=1000h, IP=0050h executes INT 21h, with SP=00FEh and FLAGS=0202h (IF=1):BEFORE (program is running):CS:IP = 1000:0050 (pointing to the INT 21h instruction)
SS:SP = 2000:00FE (stack pointer)
FLAGS = 0202h (IF=1, interrupts enabled)
DURING (INT 21h executes — CPU performs 6 steps):Step 1: PUSH FLAGS → memory at SS:00FC = 0202h, SP = 00FC
Step 2: Clear IF, TF → FLAGS = 0002h
Step 3: PUSH CS → memory at SS:00FA = 1000h, SP = 00FA
Step 4: PUSH IP → memory at SS:00F8 = 0052h, SP = 00F8 (0052h = instruction AFTER the 2-byte INT 21h)
Step 5: Read IVT[0x84] → gets CS=0276h, IP=128Ah
Step 6: CS:IP = 0276:128A
The stack now looks like:SS:00F8 │ 0052h │ ← SP (saved IP — return address)
SS:00FA │ 1000h │ (saved CS)
SS:00FC │ 0202h │ (saved FLAGS)
SS:00FE │ ... │ (previous stack contents)
AFTER (handler executes IRET):Step 1: POP IP → IP = 0052h, SP = 00FA
Step 2: POP CS → CS = 1000h, SP = 00FC
Step 3: POP FLAGS → FLAGS = 0202h, SP = 00FE Result: CS:IP = 1000:0052 (instruction after INT 21h) FLAGS = 0202h (IF=1, interrupts re-enabled) SP = 00FE (stack restored to original state)
The program resumes at exactly the instruction following the INT, with all flags (including IF) restored to their pre-interrupt values. From the program's perspective, the interrupt handler was invisible except for whatever side effects it intentionally performed (printing a character, reading keyboard input, etc.).The INT/IRET mechanism in three stages. Before: IP points to int 0x10, stack is empty. During: FLAGS, CS, and IP have been pushed onto the stack; execution has jumped to the handler at 3000:0148. After: IRET has popped the saved values; execution resumes at cmp ax,0 (the instruction after the original INT).The IVT's simplicity is one of its greatest strengths. Because it is just an array of 4-byte pointers at a fixed memory location, several powerful capabilities emerge: The OS can install its own handlers by writing new addresses into the IVT entries. When DOS loads, it writes the address of its system call dispatcher into entry 21h, its print-screen handler into entry 05h, and so on. Programs can chain or replace handlers. A program can read the current vector (saving the old handler address), write its own handler address into the IVT entry, and have its handler call the old handler when it is done. This is the foundation of TSR (Terminate and Stay Resident) programs and device drivers in DOS. Hardware interrupts use the same mechanism. The Programmable Interrupt Controller (PIC) translates hardware signals into interrupt numbers, and the CPU uses the same IVT lookup to find the hardware interrupt handler. This unifies hardware and software interrupt handling into a single, consistent mechanism. However, this simplicity also carries significant risks. Because the IVT sits at a fixed, unprotected location in memory, any program can overwrite any interrupt vector at any time. A buggy program could accidentally corrupt the IVT and crash the entire system. This lack of protection is one of the fundamental limitations of Real Mode, which we examine next.Everything we have studied so far — segment:offset addressing from Lecture 3, the IVT, direct hardware access — operates in the 8086's original Real Mode. This was the only operating mode of the original 8086/8088 processor, and for backward compatibility, every x86 processor since then (including the latest Intel and AMD chips) still starts up in Real Mode before transitioning to Protected Mode.Real Mode was adequate for the single-user, single-program computing environment of the early 1980s. But as software grew more complex and users demanded multitasking, networking, and larger programs, four fundamental limitations became increasingly painful:In Real Mode, only one program runs at a time. There is no hardware support for multitasking — no mechanism to automatically switch between programs, no way to give each program its own virtual address space, and no timer-based preemption to prevent one program from monopolizing the CPU.DOS worked around this to a limited extent with TSR (Terminate and Stay Resident) programs, which hooked into interrupt vectors and stayed in memory after the user returned to the command prompt. But TSRs are not true multitasking: they only activate when an interrupt fires. There is no concurrent execution, no time-slicing, and no way for two programs to actively run simultaneously.This means that if a program enters an infinite loop, the entire system freezes. There is no operating system scheduler running in the background to forcibly preempt the misbehaving program.The 8086's 20-bit address bus limits the entire physical address space to 2^20 = 1,048,576 bytes = 1 MB. This is not merely a limit on installed RAM; it is a hard ceiling on all addressable memory, which must be shared between:
System areas (IVT, BIOS data area)
DOS kernel and device drivers
The running application's code, data, and stack
Video memory and ROM BIOS (mapped into the upper 384 KB)
In practice, a DOS program typically has access to only about 640 KB of conventional memory (the "conventional memory barrier"), with the rest reserved for system purposes. By the late 1980s, even this was insufficient for applications like Lotus 1-2-3, AutoCAD, and early graphical programs, leading to elaborate workarounds like EMS (Expanded Memory Specification) and XMS (Extended Memory Specification).This is arguably the most dangerous limitation. In Real Mode, every program has unrestricted access to the entire 1 MB address space. Any program can read or write any memory location, including:
The IVT at 0000:0000 (overwriting interrupt vectors)
The BIOS data area
DOS kernel code and data
Another program's memory (if a TSR is resident)
Video RAM (directly manipulating the screen)
There is no hardware mechanism to prevent a program from corrupting the operating system, other programs, or the IVT. A single stray pointer or buffer overflow can overwrite critical system data and crash the machine. The CPU does not distinguish between "this program's memory" and "the OS's memory" — in Real Mode, all memory is equally accessible to all code.This is in stark contrast to Protected Mode, where the CPU enforces memory boundaries in hardware. If a Protected Mode program tries to access memory outside its allocated regions, the CPU generates a General Protection Fault and the OS can terminate the offending program without affecting the rest of the system.Related to memory protection, Real Mode provides no privilege levels and no access control. Every piece of code runs with full access to everything:
Any program can execute privileged instructions (like CLI to disable interrupts or HLT to halt the CPU)
Any program can access I/O ports directly (communicating with hardware devices)
Any program can modify segment registers to point anywhere in memory
There is no distinction between "kernel mode" and "user mode"
Whatever access restrictions exist in a Real Mode system are purely software conventions enforced by the operating system, not hardware barriers. A cooperative program might voluntarily use only DOS system calls to access files, but a malicious or buggy program can bypass DOS entirely and write directly to disk, reconfigure hardware, or take over the system.Protected Mode addresses this by introducing four privilege rings (0 through 3), where the OS kernel runs at Ring 0 (most privileged) and user programs run at Ring 3 (least privileged). The CPU hardware checks every memory access and instruction execution against the current privilege level, preventing user programs from performing dangerous operations.<br>These limitations explain why Intel introduced <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Protected_mode" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Protected_mode" target="_self">Protected Mode</a> with the 80286 processor in 1982, and significantly expanded it with the 80386 in 1985. Protected Mode did not replace the instruction set we have learned — the same MOV, ADD, PUSH, CALL, and INT instructions are still used. Instead, it added a hardware enforcement layer that makes the processor aware of which code should be allowed to do what. Understanding Real Mode's limitations is essential for appreciating why modern processors and operating systems are designed the way they are.In L06 (Section 10), we introduced Protected Mode as the modern operating mode that replaced the limitations of Real Mode. We noted the larger address space, memory protection, and privilege rings — but treated them as a summary. Now we go deeper: how exactly does Protected Mode extend addressing, and what is the virtual memory mechanism that makes modern multitasking possible?Recall from L03 that Real Mode addressing uses the segment:offset scheme to produce a 20-bit physical address: Physical = Segment * 16 + Offset. This gives a maximum addressable range of 1 MB. By the early 1980s, this was already insufficient. Programs were growing larger, and users wanted to run multiple applications simultaneously. The 1 MB ceiling forced elaborate workarounds — expanded memory (EMS), extended memory (XMS), overlays — that were fragile and complex.Protected Mode was designed to solve this problem at the hardware level, not through software tricks.Each generation of x86 processors expanded the addressable memory dramatically:<br>The <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Intel_80286" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Intel_80286" target="_self">80286</a> introduced Protected Mode with a 24-bit address bus, allowing up to 16 MB of physical memory — a 16-fold increase over the 8086. However, the 80286's Protected Mode had significant limitations: it could not easily switch back to Real Mode (requiring a CPU reset), and it lacked paging support.<br>The <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Intel_80386" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Intel_80386" target="_self">80386</a> was the true breakthrough. With a full 32-bit address bus, it supported 4 GB of physical memory and introduced paging — the mechanism that makes virtual memory possible. The 80386 is the processor that made modern operating systems like Windows and Linux practical on the x86 platform.<br>In the 64-bit era (<a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/X86-64" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/X86-64" target="_self">x86-64</a>, introduced by AMD in 2003), the theoretical virtual address space is 16 exabytes (2^64 bytes), though current implementations use 48-bit virtual addresses (256 TB) and up to 52-bit physical addresses (4 PB). This is far more than any current system needs, providing room for decades of growth.<br>A curious historical artifact of this transition is the <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/A20_line" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/A20_line" target="_self">A20 gate</a>. The 8086 had a 20-bit address bus (lines A0 through A19). Some DOS programs relied on the fact that addresses above 1 MB would "wrap around" to address 0. When the 80286 added address line A20 (enabling access above 1 MB), IBM had to add a hardware gate to disable this line for backward compatibility. Early in the boot process, the BIOS or bootloader must explicitly enable the A20 line before accessing memory above 1 MB — a legacy quirk that persisted for decades.<br>Protected Mode retains the concept of segmentation from Real Mode, but transforms it fundamentally. In Real Mode, a segment register holds a raw base address (shifted left by 4 bits). In Protected Mode, a segment register holds a selector — an index into a <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Global_Descriptor_Table" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Global_Descriptor_Table" target="_self">descriptor table</a> that contains the segment's full definition.Each entry in the descriptor table (called a segment descriptor) specifies:
Base address — the starting physical (or linear) address of the segment (32 bits on the 80386, allowing any location within 4 GB)
Limit — the maximum size of the segment (up to 4 GB with granularity scaling)
Access rights — permissions including read/write/execute, privilege level required to access the segment, and whether the segment is present in memory
This is a critical upgrade over Real Mode. In Real Mode, any program can set any segment register to any value and access any memory. In Protected Mode, the CPU checks every memory access against the descriptor table: Does this segment exist? Does this program have permission to access it? Is the offset within the segment's limit? If any check fails, the CPU raises a General Protection Fault (GPF) rather than silently allowing the access.<br>There are two main descriptor tables. The <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Global_Descriptor_Table" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Global_Descriptor_Table" target="_self">Global Descriptor Table (GDT)</a> is shared across the entire system and typically defines the operating system's memory segments. The <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Local_Descriptor_Table" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Local_Descriptor_Table" target="_self">Local Descriptor Table (LDT)</a> can be per-process, allowing each process to have its own segment definitions. The operating system sets up these tables during boot and maintains them as processes are created and destroyed.Entering Protected Mode from Real Mode requires the operating system (or bootloader) to perform a specific sequence: create a valid GDT in memory, load its address into the GDTR register using the LGDT instruction, set the PE (Protection Enable) bit in control register CR0, and then perform a far jump to flush the instruction pipeline and begin executing in Protected Mode. This transition is irreversible without a CPU reset on the 80286; the 80386 and later processors can switch back to Real Mode by clearing the PE bit, though this requires careful handling.<br>While segmentation provides access control, <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Memory_paging" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Memory_paging" target="_self">paging</a> provides the real power of Protected Mode: virtual memory.<br>With paging enabled (available from the 80386 onward), the CPU adds an extra translation layer. The address produced by segmentation is no longer a physical address — it is a linear address (also called a virtual address). The CPU's <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Memory_management_unit" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Memory_management_unit" target="_self">Memory Management Unit (MMU)</a> then translates this linear address into a physical address using page tables — data structures maintained by the operating system in memory.The translation works as follows:
The program generates a virtual address (through segmentation or, in modern flat-memory models, directly).
The MMU splits this virtual address into a page number and an offset within the page. The standard page size on x86 is 4 KB (4,096 bytes).
The MMU looks up the page number in the page table to find the corresponding physical frame number — the actual location in physical RAM.
The physical address is formed by combining the physical frame number with the offset.
This indirection has profound consequences:Each process gets its own virtual address space. The operating system maintains separate page tables for each process. Process A and Process B can both use virtual address 0x00400000, but their page tables map it to different physical frames. Neither process can see or modify the other's memory — the hardware simply does not provide a mapping.<br>Memory can exceed physical RAM. If the system runs low on physical memory, the operating system can swap (or page out) infrequently used pages to disk. The page table entry is marked "not present." If the program later accesses that page, the CPU raises a page fault — a special exception that transfers control to the operating system. The kernel's page fault handler determines what happened: if the page was swapped to disk, it loads the page back into a free physical frame, updates the page table, and lets the program continue — all transparently. The program never knows the page was absent. This is <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Virtual_memory" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Virtual_memory" target="_self">virtual memory</a> in action: programs can collectively use more memory than the machine physically has.This technique is called demand paging — pages are loaded into physical memory only when they are actually accessed, not when they are first allocated. When you start a large program, the operating system does not load the entire executable into RAM immediately. Instead, it sets up page table entries pointing to the executable file on disk. As the program executes and touches each page for the first time, a page fault occurs, and the kernel loads just that page. Pages that are never accessed (such as error-handling code that never triggers) are never loaded, saving physical memory.<br><img alt="Virtual memory with paging" src="assets/l07-virtual-memory-diagram.png" target="_self">Virtual memory in action: a running program sees a contiguous virtual address space. The paging table maps virtual pages to physical memory frames. Pages not currently in physical memory are stored in the hard disk swap file and loaded on demand.Physical memory fragmentation is hidden. A program sees a contiguous virtual address space, but the underlying physical frames can be scattered anywhere in RAM. The page table handles the mapping, so neither the program nor the programmer needs to worry about physical memory layout.Shared libraries become efficient. If multiple processes use the same shared library (such as a C standard library), the operating system can map the same physical frames into each process's virtual address space. The library code exists only once in physical memory but appears in every process that needs it.<br>Copy-on-write optimization. When a process creates a child process (via the fork system call on Unix/Linux), the operating system does not immediately duplicate all of the parent's memory. Instead, both processes share the same physical pages, with the page table entries marked as read-only. Only when one process attempts to write to a shared page does the CPU raise a page fault, and the kernel creates a private copy of that page for the writing process. This <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Copy-on-write" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Copy-on-write" target="_self">copy-on-write</a> technique makes process creation fast and memory-efficient — most pages of a forked process are never modified and never need to be copied.<br>Page table lookups require reading data structures in memory, which is slow — every memory access by the program would require additional memory accesses just to perform the translation. To solve this performance problem, the CPU includes a specialized cache called the <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Translation_lookaside_buffer" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Translation_lookaside_buffer" target="_self">Translation Lookaside Buffer (TLB)</a>. The TLB stores recently used virtual-to-physical page mappings. When the MMU needs to translate an address, it first checks the TLB. If the mapping is found (a TLB hit), the translation is nearly instantaneous. Only on a TLB miss does the CPU walk the page table structures in memory.The TLB is critical for performance. Modern processors achieve TLB hit rates above 99% for typical workloads, meaning that the overhead of paging is negligible in practice. However, when the operating system switches between processes and loads a new set of page tables (by writing a new value to the CR3 register), the TLB entries from the previous process are no longer valid and must be flushed. This TLB flush is one of the real costs of a context switch.To summarize, a memory access in full Protected Mode passes through two translation stages:Logical Address → Segmentation → Linear (Virtual) Address → Paging → Physical Address
(Selector:Offset) (GDT/LDT) (32-bit or 64-bit) (Page (actual RAM Tables) location)
In modern operating systems running on the 80386 and later, segmentation is often configured as a flat memory model — all segments have base 0 and limit 4 GB, effectively making the logical address equal to the linear address. This simplifies programming (no segment arithmetic needed) and relies entirely on paging for memory management and protection. The x86-64 architecture enforces this flat model: in 64-bit Long Mode, segmentation is essentially disabled for code and data segments.Protected Mode is not only about accessing more memory. It provides the hardware foundations for two capabilities that define modern computing: running multiple programs simultaneously (parallelism/multitasking) and preventing those programs from interfering with each other or with the operating system (security).The virtual memory system described in Section 12 is the first pillar of parallelism. Because each process has its own page tables, each process sees its own private virtual address space. One process cannot read or write another process's memory — not because the operating system politely asks it not to, but because the hardware provides no mapping to the other process's physical pages.This isolation means that a bug in one program — say, a buffer overflow that writes to a wild pointer — corrupts only that program's own memory. The operating system detects the violation (through a page fault or general protection fault), terminates the offending process, and all other processes continue running undisturbed. Compare this to Real Mode, where a single stray write could overwrite the interrupt vector table and crash the entire machine.<br>The x86 Protected Mode architecture provides a dedicated hardware mechanism for task switching: the <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Task_state_segment" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Task_state_segment" target="_self">Task State Segment (TSS)</a>. The TSS is a special data structure that stores the complete execution context of a task (process):
All general-purpose registers (EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP)
The instruction pointer (EIP) — where the task was executing when it was suspended
The FLAGS register — the complete processor state flags
Segment registers (CS, DS, ES, FS, GS, SS)
The CR3 register — the page directory base, which points to the task's page tables (this is how each process gets its own virtual address space)
An I/O permission bitmap — specifying which I/O ports this task is allowed to access
Stack pointers for each privilege level — so the CPU knows which stack to switch to when transitioning between rings
When the operating system decides to switch from Task A to Task B, the CPU can save Task A's entire state into its TSS and load Task B's state from its TSS in a single operation. This is called a hardware task switch. The processor handles the register save/restore, the page table switch (via CR3), and the stack switch automatically.A context switch — the act of suspending one process and resuming another — involves several steps regardless of whether hardware or software switching is used:
Save the current process's register state (or let the CPU save it to the TSS).
Save the current process's page table pointer (CR3).
Load the new process's page table pointer — this instantly changes the entire virtual address space visible to the CPU.
Load the new process's register state, including its instruction pointer (EIP/RIP).
Resume execution at the point where the new process was last suspended.
Because loading a new CR3 invalidates the TLB (see Section 12.5), context switches have a measurable performance cost. This is one reason why operating systems try to avoid switching too frequently while still maintaining responsive multitasking.In practice, most modern operating systems (Linux, Windows) perform software task switching instead of using the hardware TSS mechanism — they save and restore registers manually because it gives them finer control over what is saved and is often faster. However, the TSS remains essential: it is still used to store the kernel stack pointer for each task (the CPU needs this during privilege level transitions) and the I/O permission bitmap. Every x86 operating system running in Protected Mode must have at least one TSS defined.<br>The second pillar of security in Protected Mode is the <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Protection_ring" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Protection_ring" target="_self">protection ring</a> system — a hardware-enforced hierarchy of privilege levels. The x86 architecture defines four rings:The current privilege level (CPL) is encoded in the lowest two bits of the CS (Code Segment) register. The CPU checks this privilege level on every instruction:
Privileged instructions — such as HLT (halt the CPU), LGDT (load the GDT register), MOV to/from control registers (CR0, CR3), and IN/OUT (direct I/O port access) — can only be executed in Ring 0. If a Ring 3 program attempts to execute HLT, the CPU immediately raises a General Protection Fault.
Memory access — every segment descriptor includes a Descriptor Privilege Level (DPL). A program can only access segments whose DPL is greater than or equal to its own CPL. Ring 3 code cannot read or write Ring 0 data segments.
I/O port access — the TSS contains an I/O permission bitmap that specifies which ports a given task can access. Even if I/O instructions were not inherently privileged, this bitmap provides fine-grained control.
<br><img alt="Protection rings" src="assets/l07-protection-rings.png" target="_self">The x86 protection ring hierarchy. Ring 0 (kernel) has full hardware access. Ring 3 (applications) is the most restricted. Rings 1 and 2 were designed for device drivers but are unused by modern operating systems, which use only Ring 0 and Ring 3.<br>In practice, most modern operating systems use only Ring 0 and Ring 3. Rings 1 and 2 were intended for device drivers and system services, but the two-ring model (kernel vs. user) proved simpler and sufficient. The overhead of managing four privilege levels added complexity without clear benefit — if a device driver in Ring 1 crashes, it can still take down the system because it has access to critical hardware. Intel even proposed <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/X86S" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/X86S" target="_self">x86S</a> in 2023 (later cancelled), which would have formally removed Rings 1 and 2 from the architecture — acknowledging that no mainstream operating system uses them.One important exception to the two-ring model is virtualization. Hypervisors (such as VMware, Hyper-V, or KVM) need to run guest operating systems that believe they are in Ring 0, while the hypervisor itself maintains true control. Early virtualization solutions used Rings 1-2 for this purpose. Modern processors solve this differently with dedicated virtualization extensions (Intel VT-x, AMD-V), which create a new privilege level below Ring 0 — sometimes called "Ring -1" — for the hypervisor.If user programs run in Ring 3 and the kernel runs in Ring 0, how does a program request kernel services? It cannot simply CALL a kernel function — the CPU would reject the jump as a privilege violation. Instead, the architecture provides controlled entry points:<br>Software interrupts (INT instruction). As we covered in earlier sections, executing INT 21h in DOS or INT 80h in 32-bit Linux causes the CPU to switch from Ring 3 to Ring 0, save the user's state, and jump to a handler defined by the operating system. The interrupt mechanism is inherently a privilege transition — the CPU automatically elevates to Ring 0 when dispatching through the <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Interrupt_descriptor_table" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Interrupt_descriptor_table" target="_self">Interrupt Descriptor Table (IDT)</a> (the Protected Mode successor to the IVT).<br>The SYSCALL/SYSENTER instructions. Modern x86-64 systems use the SYSCALL instruction instead of INT 80h for system calls. SYSCALL is a faster mechanism — it does not go through the IDT but instead uses dedicated <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Model-specific_register" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Model-specific_register" target="_self">Model-Specific Registers (MSRs)</a> to determine the Ring 0 entry point. The effect is the same: a controlled, hardware-mediated transition from user mode to kernel mode.<br>Call gates. The x86 architecture also supports <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Call_gate_(Intel)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Call_gate_(Intel)" target="_self">call gates</a> — special descriptor table entries that allow controlled far calls across privilege levels. A Ring 3 program can CALL a far pointer that references a call gate, and the CPU will verify permissions, switch to Ring 0, and jump to the predefined kernel entry point. Call gates were the original x86 mechanism for system calls but have been largely superseded by SYSENTER/SYSCALL due to performance.In all cases, the critical property is the same: the user program cannot choose where in the kernel to jump. It can only invoke predefined entry points that the kernel has registered. This prevents a malicious program from jumping into the middle of a kernel routine and bypassing security checks.Connecting this back to what we covered in the interrupts section: when a user program in Ring 3 executes INT 21h (DOS) or SYSCALL (Linux), the following privilege transition occurs:
The CPU recognizes the ring transition (Ring 3 to Ring 0).
The CPU switches to the kernel stack — the stack pointer for Ring 0 is loaded from the TSS. This is essential because the kernel cannot trust the user's stack.
The user's SS, ESP, EFLAGS, CS, and EIP are pushed onto the kernel stack (the return context).
The CPU loads CS and EIP from the IDT entry (or from MSRs for SYSCALL), which points to the kernel's handler code.
Execution continues in Ring 0 — the kernel processes the request.
When finished, the kernel executes IRET (or SYSRET), which restores the saved state, switches back to Ring 3, and resumes the user program.
This mechanism is the hardware foundation for every system call in every modern operating system running on x86. It ensures that the transition between user and kernel mode is always controlled, auditable, and reversible.We have now examined the individual hardware mechanisms that Protected Mode provides: virtual memory with paging, process isolation through separate address spaces, hardware task support via the TSS, and privilege enforcement through protection rings. The kernel is the software that ties all of these mechanisms together into a coherent, working system.<br>A <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Kernel_(operating_system)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Kernel_(operating_system)" target="_self">kernel</a> is the core program of an operating system — the one piece of software that is always resident in memory and always has complete control over the hardware. Its responsibilities include:Resource management. The kernel arbitrates access to all hardware resources: CPU time, physical memory, disk storage, network interfaces, and peripheral devices. When multiple processes compete for the same resource, the kernel decides who gets what, when, and for how long.Process scheduling. The kernel decides which process runs on which CPU core at any given moment. It implements a scheduling algorithm that balances fairness (every process gets CPU time), responsiveness (interactive programs react quickly), and throughput (batch jobs complete efficiently). The scheduler uses the hardware timer interrupt (the same mechanism we discussed in the interrupts section) to regain control from running processes at regular intervals — this is preemptive multitasking. The process being interrupted does not need to cooperate or even be aware that it was suspended; from its perspective, execution simply pauses and later resumes exactly where it left off. This is fundamentally different from cooperative multitasking (used in early Windows and classic Mac OS), where programs had to voluntarily yield control — a single misbehaving program could freeze the entire system.Providing services through system calls. User programs cannot access hardware directly (they run in Ring 3). Instead, they request services from the kernel through system calls — the controlled Ring 3-to-Ring 0 transitions we described in Section 13. Need to read a file? The program invokes the read system call. Need to allocate memory? The mmap or brk system call. Need to create a new process? The fork system call. The kernel validates each request, performs the operation on behalf of the program, and returns the result.Using the CPU for security. The kernel configures and maintains the protection ring system. It runs in Ring 0, so it can execute privileged instructions — loading page tables (via CR3), configuring the GDT and IDT, managing I/O permissions in the TSS. User programs in Ring 3 are confined: they cannot modify page tables, cannot change interrupt handlers, cannot access I/O ports directly, and cannot execute privileged instructions. The kernel is the gatekeeper.Virtualization and parallelism. The kernel creates the illusion that each process has the entire machine to itself: its own virtual address space (via paging), its own CPU (via scheduling), and its own set of open files and resources (via kernel data structures). This virtualization is what allows hundreds of processes to run "simultaneously" on a machine with a handful of CPU cores.<br><img alt="Kernel architecture" src="assets/l07-kernel-architecture.png" target="_self">The kernel sits between hardware (CPU, memory, devices) and user applications, mediating all access through system calls and drivers.Hardware abstraction through device drivers. The kernel includes (or loads) device drivers — specialized code modules that know how to communicate with specific hardware devices. A program that wants to read from a file does not need to know whether the file resides on an SSD, a spinning hard drive, or a network share. The program makes a generic read system call, and the kernel routes the request to the appropriate driver. This abstraction layer is the modern descendant of the BIOS services we discussed earlier — but far more capable and extensible.As we discussed in the boot process sections, the kernel is the final link in the bootstrap chain: BIOS loads the bootloader, the bootloader loads the kernel. Once the kernel receives control, it performs its own initialization sequence:
Switches the CPU from Real Mode to Protected Mode (setting the PE bit in CR0).
Sets up the GDT and IDT with the system's segment and interrupt descriptors.
Enables paging by creating initial page tables and loading CR3.
Initializes the scheduler, memory manager, and device drivers.
Creates the first user-space process (on Linux, this is traditionally init or systemd).
From this point on, the kernel is in full control. All subsequent programs are created by the kernel, run in Ring 3, and interact with hardware only through system calls.The kernel occupies a protected region of memory — kernel space — that is inaccessible from Ring 3 code. On a typical 32-bit Linux system, the 4 GB virtual address space is split: the lower 3 GB (addresses 0x00000000 through 0xBFFFFFFF) belong to user space, and the upper 1 GB (addresses 0xC0000000 through 0xFFFFFFFF) is reserved for the kernel. The page table entries for the kernel region are marked as Ring 0-only, so any attempt by a user program to read or write kernel memory triggers a page fault.This separation means that a crashing user program cannot corrupt kernel data structures. The kernel remains stable even when applications fail — which is why a modern operating system can run for months without rebooting, despite individual programs crashing regularly.Interestingly, the kernel's memory is mapped into every process's virtual address space (in the upper region), even though user code cannot access it. This design allows the kernel to quickly access its own data structures during a system call without needing to switch page tables. When a system call transitions from Ring 3 to Ring 0, the same page tables remain loaded — only the privilege level changes, unlocking access to the kernel-space pages.Not all kernels are organized the same way. The two principal designs are:<br>Monolithic kernels. All kernel functionality — process management, memory management, file systems, device drivers, networking — runs in a single address space in Ring 0. This design prioritizes performance: function calls within the kernel are simple procedure calls, not cross-process messages. The <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Linux_kernel" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Linux_kernel" target="_self">Linux kernel</a> is the most prominent example. Linux is technically monolithic but also modular — it can load and unload kernel modules (device drivers, file system support) at runtime without rebooting.<br>Microkernels. Only the most essential functions — basic scheduling, inter-process communication (IPC), and low-level memory management — run in Ring 0. Everything else (device drivers, file systems, network stacks) runs in Ring 3 as separate user-space processes. If a device driver crashes, only that driver's process dies — the kernel itself remains unaffected. <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Minix" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Minix" target="_self">MINIX</a> and <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/QNX" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/QNX" target="_self">QNX</a> are microkernel systems. The tradeoff is performance: every request that in a monolithic kernel would be a simple function call now requires an IPC message between user-space processes, which is slower.Most production operating systems today — Linux, Windows, macOS — use monolithic (or hybrid monolithic) kernels, accepting the reliability tradeoff in favor of performance. Windows NT and macOS use a hybrid approach: the kernel is largely monolithic for performance, but some subsystems run in user space for modularity. The microkernel idea remains influential in embedded and real-time systems where reliability is paramount — for example, the QNX microkernel powers many automotive and medical device systems where a crash could be life-threatening.The debate between monolithic and microkernel designs is one of the oldest in operating systems research. In a famous 1992 Usenet exchange, Linus Torvalds and Andrew Tanenbaum (creator of MINIX) argued about whether Linux's monolithic design was "a giant step back into the 1970s." Three decades later, Linux dominates servers, smartphones (via Android), and supercomputers — while microkernel principles have found their niche in safety-critical embedded systems.The kernel is where every hardware mechanism we have discussed in this lecture converges:Without Protected Mode, none of this would work. In Real Mode — the environment you program in for the first part of this course — there is no memory protection, no privilege rings, and no virtual memory. DOS runs as a single-tasking system where any program can overwrite any memory location, including the operating system itself. This is exactly why a bug in a DOS program can freeze the entire machine.Protected Mode gave hardware architects and operating system designers the tools to build the reliable, secure, multitasking systems we depend on today. The journey from pressing the power button (Section 2), through the BIOS and boot process (Sections 3-4), to interrupts (Sections 5-10), to the Real Mode limitations and evolution to Protected Mode (Section 11), and finally to the kernel itself — this is the complete path from bare hardware to a functioning operating system.For the remainder of this course, you will experience both sides of this divide. In Labs P1 through P10, you continue working in Real Mode under DOS, where you have direct access to hardware and can see every mechanism at work without abstraction. In Labs P11 through P15, you will transition to Linux on x86-64 — a fully Protected Mode environment where your programs run in Ring 3, use virtual addresses, and communicate with the kernel exclusively through system calls. Understanding the hardware mechanisms described in this lecture will help you appreciate why the Linux environment works the way it does, and what the operating system is doing on your behalf beneath the surface. The boot process is a chain of trust: Power-on triggers the reset vector, which starts BIOS/UEFI firmware. POST tests hardware, the bootstrap loader finds the MBR, which loads the OS kernel. Each stage hands control to the next. Interrupts are the universal service mechanism: Both hardware devices and software programs use interrupts to request services. The CPU handles both types identically — look up the handler address in the IVT and transfer control. INT/IRET extends the CALL/RET pattern: INT pushes FLAGS, CS, and IP onto the stack, then jumps to the handler address from the IVT. IRET reverses the process. This is like a far CALL that also saves and restores the processor flags. The IVT is a simple lookup table: 256 entries × 4 bytes each, starting at address 0000:0000. Entry N is at address N×4. BIOS populates it first; the OS can override entries later. DOS INT 21h and BIOS INT 10h/16h are your tools for I/O: Function number goes in AH, parameters in other registers. These interrupts provide standardized access to screen, keyboard, files, and system services. Real Mode has fundamental limitations: 1 MB of RAM, no memory protection, no multitasking, no hardware security. Any program can overwrite any memory, including the OS itself. Protected Mode solves Real Mode's problems with hardware support: Virtual memory (paging) provides large address spaces, protection rings enforce security boundaries, and the TSS enables hardware-assisted task switching. The kernel sits at Ring 0: It manages all hardware resources, schedules processes, and provides services through system calls. User programs run at Ring 3 and must use interrupts/syscalls to request kernel services. What you'll do:
Examine the IVT in memory using Turbo Debugger (Dump at 0000:0000)
Read interrupt vectors and trace them to handler code
Explore the BIOS Data Area (0040:0000): keyboard buffer, timer ticks
Call BIOS interrupts: INT 10h (video), INT 16h (keyboard)
Call DOS interrupts: INT 21h (functions 01h, 02h, 09h, 4Ch)
Hook an interrupt vector: save the original, install your handler, restore on exit <br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Bootloader" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Bootloader" target="_self">Bootloader</a> — Wikipedia — Overview of the boot process and boot loader stages
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Power-on_self-test" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Power-on_self-test" target="_self">Power-on self-test</a> — Wikipedia — POST process and hardware initialization
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Reset_vector" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Reset_vector" target="_self">Reset vector</a> — Wikipedia — First instruction addresses for x86 processors
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/BIOS" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/BIOS" target="_self">BIOS</a> — Wikipedia — Firmware role in system startup and boot process
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Master_boot_record" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Master_boot_record" target="_self">Master boot record</a> — Wikipedia — MBR structure and system bootstrapping
<br><a data-tooltip-position="top" aria-label="https://mrszeto.net/CIT/interrupts.htm" rel="noopener nofollow" class="external-link is-unresolved" href="https://mrszeto.net/CIT/interrupts.htm" target="_self">DOS Interrupts</a> — INT 21h function reference for lab programming
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/INT_10H" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/INT_10H" target="_self">INT 10h</a> — Wikipedia — BIOS video services
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/INT_16H" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/INT_16H" target="_self">INT 16h</a> — Wikipedia — BIOS keyboard services
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Interrupt" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Interrupt" target="_self">Interrupt</a> — Wikipedia — Hardware and software interrupt types
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Interrupt_vector_table" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Interrupt_vector_table" target="_self">Interrupt vector table</a> — Wikipedia — IVT structure and handling methods
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Protected_mode" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Protected_mode" target="_self">Protected mode</a> — Wikipedia — x86 protected mode and privilege levels
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Virtual_memory" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Virtual_memory" target="_self">Virtual memory</a> — Wikipedia — Virtual memory concepts and benefits
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Protection_ring" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Protection_ring" target="_self">Protection ring</a> — Wikipedia — Hierarchical protection domains
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Kernel_(operating_system)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Kernel_(operating_system)" target="_self">Kernel (operating system)</a> — Wikipedia — Kernel responsibilities and architecture
<br><a data-tooltip-position="top" aria-label="https://syscalls.w3challs.com/" rel="noopener nofollow" class="external-link is-unresolved" href="https://syscalls.w3challs.com/" target="_self">Syscall Table for x86 Linux</a> — Linux system call reference for assembly programming
]]></description><link>lectures/eng/l07-eng.-interrupts-and-os-interaction.html</link><guid isPermaLink="false">Lectures/ENG/L07-ENG. Interrupts and OS Interaction.md</guid><pubDate>Tue, 10 Mar 2026 06:00:29 GMT</pubDate><enclosure url="assets/l07-reset-vector-td.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l07-reset-vector-td.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L06-ENG. System Architecture]]></title><description><![CDATA[
Lecture 6 of Computer Systems Architecture course at NaUKMA.
After this lecture you will be able to:
Use bit shift instructions (SHL, SHR, SAL, SAR, ROL, ROR) and explain the difference between logical and arithmetic shifts
Navigate x86 instruction reference resources to look up instruction encodings and behavior
Explain Moore's Law and its significance for the evolution of computing hardware
Describe the progression from integrated circuits to microcontrollers to microprocessors
Identify the main components of a computer system: CPU, motherboard, memory, and I/O devices
Explain the role of the system bus (data, address, and control buses) and the system clock
Describe the memory hierarchy from registers through cache to DRAM and disk storage
Compare Real Mode and Protected Mode operation of the x86 processor
Contrast the Von Neumann and Harvard architectures
Describe the instruction execution cycle (fetch-decode-execute) and explain the Von Neumann bottleneck
This lecture marks a turning point in the course. Over the first five lectures, we have built a solid foundation in x86 assembly programming: registers and memory addressing (L03), arithmetic and logical instructions (L04), and program organization with the stack and procedures (L05). We can now write complete assembly programs with functions, parameters, and local variables. But we have been working inside the machine without stepping back to see the machine itself.Lecture 6 — titled "From Moore's Law to Microprocessors" (Від закону Мура до мікропроцесорів) — shifts our perspective from programming the processor to understanding the system that surrounds it. Why can a modern chip hold billions of transistors? How does the CPU communicate with memory and peripherals? Why is accessing RAM thousands of times slower than accessing a register? These are system architecture questions, and answering them will deepen your understanding of every program you write.The lecture is organized in two parts:Part A: Completing the Instruction Set ("Dotting the I's"). Before moving to system architecture, we tie up loose ends from previous lectures. We briefly revisit the C/C++ calling convention from Lecture 5, then cover the remaining instruction category we have not yet examined in depth: bit shift and rotate operations. We also point to comprehensive x86 instruction references for your ongoing study.Part B: System Architecture. The main body of the lecture traces a path from Moore's Law and the evolution of integrated circuits through to the structure of a complete computer system. We examine the von Neumann architecture (CPU, memory, bus, I/O), the three types of system buses, the system clock, the memory hierarchy from registers to disk, the fetch-decode-execute instruction cycle, the von Neumann bottleneck, and the Harvard architecture as an alternative design.In Lecture 5, we established the C calling convention as a standardized protocol for function calls. To briefly recap: the caller saves volatile registers (AX, CX, DX) before the call and pushes parameters onto the stack in reverse order (right-to-left). The callee saves BP, sets up a stack frame (push bp; mov bp, sp), allocates space for local variables, saves any non-volatile registers it modifies, and returns the result in AX (or DX:AX for 32-bit values). After the call returns, the caller cleans up the parameters from the stack. This convention ensures that assembly functions written by different programmers — or generated by different compilers — can interoperate correctly. We will not revisit the details here; refer to Lecture 5, Section 5 for the full explanation and examples.With calling conventions settled, we now turn to the last major gap in our instruction set coverage: bit shift operations.In Lecture 4, we covered the bitwise logical operations AND, OR, XOR, and NOT. These operate on individual bits in place — they combine corresponding bit positions of two operands. Bit shift operations are different: they move all bits within a register left or right by a specified number of positions, discarding bits that fall off one end and filling the vacated positions at the other end. This seemingly simple operation turns out to be one of the most useful tools in a systems programmer's repertoire.Shift operations serve three critical purposes: Fast multiplication and division. Shifting a binary number left by one position is equivalent to multiplying by 2; shifting right is equivalent to dividing by 2. Because shift instructions execute in a single clock cycle (compared to the dozens of cycles required by MUL and DIV), compilers routinely replace multiplication and division by powers of 2 with shift instructions. Bit field extraction and insertion. Hardware registers, network protocols, and file formats often pack multiple values into a single word. Shifts combined with masks (AND) let you extract or insert specific bit fields efficiently. Low-level algorithms. Cryptographic algorithms, hash functions, CRC calculations, and graphics routines rely heavily on shifts and rotates to manipulate data at the bit level. The x86 instruction set provides two families of shift operations: linear shifts (bits fall off one end and are lost) and rotates (bits wrap around from one end to the other).The SHL (Shift Left) instruction moves every bit in the operand one or more positions to the left. The vacated positions on the right are filled with zeros. The last bit shifted out of the most significant position is placed into the Carry Flag (CF).Syntax:SHL destination, count ; count = 1 or CL
On the 8086, the count must be either the literal value 1 or the CL register. (Later processors allow an immediate count up to 31.)Example — shifting 10101111 left by 1:Consider the value 10101111 in binary (175 in decimal, or AFh). Before the shift, CF has some previous value:Before: CF [1 0 1 0 1 1 1 1] MSB LSB SHL AH, 1 After: CF [0 1 0 1 1 1 1 0] 1 shifted left, 0 fills from right
Left: SHL moves bits toward MSB, filling LSB with 0. Right: SHR moves bits toward LSB, filling MSB with 0.The MSB (1) was shifted into CF. All other bits moved one position left. A zero filled the vacated LSB. The result is 01011110 in binary (94 in decimal, or 5Eh).Code example:mov ah, 10101111b ; AH = AFh = 175
shl ah, 1 ; AH = 5Eh = 94, CF = 1 (old MSB)
Notice that 175 as an unsigned number does not simply become 350 (which would be the expected result of multiplying by 2), because 350 exceeds the 8-bit range (0-255). The result 94 is 350 mod 256 — the multiplication "overflowed." The Carry Flag signals this overflow.Shifting by multiple positions:mov cl, 3
shl ax, cl ; Shift AX left by 3 positions = multiply by 8
Each left shift by 1 doubles the value (for values that fit in the register). Shifting left by N positions multiplies by 2^N:The SHR (Shift Right) instruction moves every bit to the right. Zeros fill from the left (MSB side). The last bit shifted out of the LSB goes into CF.Syntax:SHR destination, count ; count = 1 or CL
Example — shifting 10101111 right by 1:Before: [1 0 1 0 1 1 1 1] CF MSB LSB SHR AH, 1 After: [0 1 0 1 0 1 1 1] CF 0 fills from left 1 (old LSB)
The result is 01010111 in binary (87 in decimal, or 57h). The original value was 175; dividing by 2 gives 87.5, and the integer result is 87 (the remainder, 1, went into CF). This is unsigned integer division by 2 — it always fills with zeros from the left.Code example:mov ah, 10101111b ; AH = AFh = 175
shr ah, 1 ; AH = 57h = 87, CF = 1 (remainder)
Logical shifts treat the operand as a sequence of bits with no special meaning assigned to any position. Arithmetic shifts are designed for signed numbers in two's complement representation, where the MSB is the sign bit.Here is a surprise: SAL is identical to SHL. The same operation, the same result, the same flags. In fact, on all x86 processors, SAL reg, 1 and SHL reg, 1 produce the exact same machine code when assembled by most assemblers.Why does this make sense? Consider what happens when you shift a signed number left by 1. In two's complement, the sign bit is the MSB. When we shift left, the MSB falls off into CF, and a zero fills from the right — exactly what SHL does. The mathematical effect is multiplication by 2, regardless of whether the number is interpreted as signed or unsigned. The bit pattern is the same; only our interpretation differs.For example, consider the signed value -81 (10101111 in 8-bit two's complement). Shifting left by 1:mov ah, 10101111b ; AH = -81 (signed) or 175 (unsigned)
sal ah, 1 ; AH = 01011110b = 94 ; As signed: 94 (overflow — we lost the sign!) ; CF = 1, OF = 1 (sign changed)
The Overflow Flag (OF) is set because the sign bit changed — this signals signed overflow, just as it does for ADD. The instruction is identical to SHL; SAL exists as a mnemonic alias so that programmers can signal their intent to work with signed data.There is a fascinating footnote in x86 history here. When Intel designed the opcode map for shift instructions under the D0 opcode byte, they assigned eight slots (called ModR/M register extensions /0 through /7):Slot /6 is conspicuously missing from Intel's official documentation. But if you manually construct a machine code byte using D0 /6, the processor executes it — and it behaves exactly like SHL (/4). This is SAL's "hidden" encoding. Intel documented SAL as a mnemonic synonym for SHL and left /6 as an undocumented but functional opcode. Most assemblers emit /4 (the SHL encoding) for both SHL and SAL mnemonics, so /6 is never generated by standard tools, yet the processor has always supported it. The article "<a data-tooltip-position="top" aria-label="https://xlogicx.net/Assembly_Is_Too_High-Level_-_Better_Call_SAL.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://xlogicx.net/Assembly_Is_Too_High-Level_-_Better_Call_SAL.html" target="_self">Assembly Is Too High-Level — Better Call SAL</a>" explores this curiosity in detail, calling it a deliberate omission hidden in plain sight.<br><img alt="Better Call SAL — Breaking Bad parody referencing the hidden SAL instruction" src="assets/l06-better-call-sal.png" target="_self">"Better Call SAL" — a nod to the hidden SAL instruction in x86 architecture.Unlike SAL (which is identical to SHL), SAR differs from SHR in an important way: it preserves the sign bit. When SAR shifts bits to the right, it fills the vacated MSB position with a copy of the original sign bit, not with zero.Syntax:SAR destination, count ; count = 1 or CL
Example — SAR vs SHR on a negative number:Let us compare how SHR and SAR handle the value 10101111 (which is -81 in signed 8-bit, or 175 unsigned):SHR: [1 0 1 0 1 1 1 1] → [0 1 0 1 0 1 1 1] = 87 unsigned, 87 signed fills with 0 ─────────^ SAR: [1 0 1 0 1 1 1 1] → [1 1 0 1 0 1 1 1] = D7h unsigned, -41 signed sign bit copied ──────^
<br><img alt="SAR preserves the sign bit when shifting right" src="assets/l06-sar-shift-arithmetic-right.png" target="_self">SAR (Shift Arithmetic Right): the sign bit (MSB) is copied rather than filled with zero, preserving the sign of negative numbers.With SHR, the result is 87 — correct for unsigned 175 / 2 = 87. But if we intended the value to be signed (-81), we expect -81 / 2 = -40 or -41. SHR gives us the wrong answer (87 is positive!). SAR gives -41, which is the correct signed division by 2, rounding toward negative infinity.Code example:; Unsigned division by 2
mov ah, 10101111b ; AH = 175 (unsigned)
shr ah, 1 ; AH = 87, correct for unsigned ; Signed division by 2
mov ah, 10101111b ; AH = -81 (signed)
sar ah, 1 ; AH = -41 (signed), CF = 1
Key insight for positive numbers: When the MSB is 0 (positive signed numbers), SAR and SHR produce identical results, because copying a 0 into the MSB is the same as filling with 0. SAR and SHR diverge only for negative numbers (MSB = 1).The key takeaway: Left shifts are the same for signed and unsigned (SHL = SAL). Right shifts differ: SHR for unsigned values, SAR for signed values. Choose the wrong one and negative numbers produce incorrect results.Shift instructions are dramatically faster than MUL and DIV. On the 8086, MUL takes 70-160 clock cycles depending on operand size, while SHL takes just 2 cycles. Even on modern processors where the difference is smaller, compilers aggressively use this optimization.Multiplying by non-power-of-2 constants using shifts and adds:; Multiply AX by 10 (= 8 + 2 = 2^3 + 2^1)
mov bx, ax ; BX = AX (save original)
mov cl, 3
shl ax, cl ; AX = AX * 8
shl bx, 1 ; BX = original * 2
add ax, bx ; AX = original * 8 + original * 2 = original * 10
Dividing by powers of 2:; Unsigned divide by 16
shr ax, 4 ; (on 8086: mov cl, 4 / shr ax, cl) ; Signed divide by 4
sar ax, 2 ; (on 8086: mov cl, 2 / sar ax, cl)
Extracting a bit field:; Extract bits 4-7 from AL (a 4-bit field)
mov cl, 4
shr al, cl ; Shift the field to bits 0-3
and al, 0Fh ; Mask off everything above bit 3
Sometimes you need to shift bits without losing them. Rotate instructions wrap bits around: instead of falling off one end and disappearing, the bit re-enters from the other end.ROL shifts all bits left by one position. The MSB, instead of being discarded, wraps around to become the new LSB. The MSB is also copied to CF.Before: [1 0 1 0 1 1 1 1] CF = ? MSB LSB ROL AH, 1 After: [0 1 0 1 1 1 1 1] CF = 1 ^ ^ | └── old MSB wraps here └── old bit 6 is now MSB
Code example:mov ah, 10101111b ; AH = AFh
rol ah, 1 ; AH = 01011111b = 5Fh, CF = 1
Unlike SHL (which would produce 01011110), ROL places the old MSB into the LSB position, giving 01011111.Left: ROL wraps the MSB around to become the new LSB. Right: ROR wraps the LSB around to become the new MSB.ROR shifts all bits right by one position. The LSB wraps around to become the new MSB. The LSB is also copied to CF.Before: [1 0 1 0 1 1 1 1] CF = ? MSB LSB ROR AH, 1 After: [1 1 0 1 0 1 1 1] CF = 1 ^ ^ └── old LSB wraps ───┘
Code example:mov ah, 10101111b ; AH = AFh
ror ah, 1 ; AH = 11010111b = D7h, CF = 1
RCL (Rotate through Carry Left) and RCR (Rotate through Carry Right) include the Carry Flag as an extra bit in the rotation. For an 8-bit operand, this creates a 9-bit rotation loop: 8 register bits plus CF.RCL operation:Before: CF [b7 b6 b5 b4 b3 b2 b1 b0] RCL reg, 1 After: b7 [b6 b5 b4 b3 b2 b1 b0 CF_old] ↑ ↑ bits shift left old CF enters from right
The old MSB goes to CF, old CF enters the LSB position, and all other bits shift left by one.RCR operation:Before: [b7 b6 b5 b4 b3 b2 b1 b0] CF RCR reg, 1 After: [CF_old b7 b6 b5 b4 b3 b2 b1] b0 ↑ ↑ old CF enters from left old LSB goes to CF
Code example — multi-byte shift using RCL:One powerful application of RCL is shifting values wider than 16 bits. Suppose you want to shift a 32-bit value stored in DX:AX left by 1:; Shift DX:AX left by 1 (multiply 32-bit value by 2)
shl ax, 1 ; Shift low word left; MSB of AX goes to CF
rcl dx, 1 ; Shift high word left; old CF (from AX) enters LSB of DX
This two-instruction sequence correctly propagates the carry from the low word into the high word, performing a 32-bit shift using 16-bit instructions. Without RCL, you would need several extra instructions to achieve the same result.Code example — multi-byte shift using RCR:; Shift DX:AX right by 1 (divide 32-bit unsigned value by 2)
shr dx, 1 ; Shift high word right; LSB of DX goes to CF
rcr ax, 1 ; Shift low word right; old CF (from DX) enters MSB of AX
<br><img alt="Summary of all x86 shift and rotate instructions" src="assets/l06-shift-rotate-summary-table.png" target="_self">Complete summary of shift and rotate instructions: logical shifts (SHL, SHR), arithmetic shifts (SAL, SAR), and rotates (ROL, ROR, RCL, RCR).With the shift and rotate instructions covered, we have now examined all major categories of the original 8086 instruction set: data movement (MOV, PUSH, POP, LEA), arithmetic (ADD, SUB, MUL, DIV, INC, DEC, NEG), logical (AND, OR, XOR, NOT), shift and rotate (SHL, SHR, SAL, SAR, ROL, ROR, RCL, RCR), control flow (JMP, Jcc, LOOP, CALL, RET), and string operations (MOVSB, CMPSB, etc.).For a comprehensive reference of all 8086 instructions — including those we have not covered in lectures (such as BCD adjustment instructions AAA/AAS/AAM/AAD, string operations with REP prefixes, and I/O instructions IN/OUT) — the following resources are invaluable:
<br>
Wikipedia: x86 instruction listings (<a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/X86_instruction_listings#Original_8086/8088_instructions" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/X86_instruction_listings#Original_8086/8088_instructions" target="_self">Original 8086/8088 instructions</a>) — A complete table of every 8086 instruction with opcode encodings, operand formats, and brief descriptions. This is one of the best quick-reference resources available. <br>
x86 Opcode Structure and Instruction Overview (<a data-tooltip-position="top" aria-label="http://pnx.tf/files/x86_opcode_structure_and_instruction_overview.pdf" rel="noopener nofollow" class="external-link is-unresolved" href="http://pnx.tf/files/x86_opcode_structure_and_instruction_overview.pdf" target="_self">pnx.tf reference</a>) — A detailed guide to how x86 machine code is structured, including the ModR/M byte, SIB byte, and prefix bytes. Essential reading if you want to understand the encoding we discussed for SAL/SHL. These references will be useful throughout the rest of the course and in your lab work. You do not need to memorize every instruction; knowing where to look is the practical skill.The Intel 8086 processor shipped with a manageable set of approximately 100 basic instruction mnemonics. While that number may seem large at first glance, the instructions fall into a small number of logical categories, and many share similar encoding patterns. Learning to navigate an instruction reference is one of the most important practical skills for anyone working with assembly language — you will never memorize every instruction, but you should be able to look up any instruction quickly and understand its encoding, operands, and effects on flags.<br><img alt="x86 opcode map showing all instruction encodings in a 16x16 grid" src="assets/l06-x86-opcode-map.png" target="_self">The x86 opcode map: each cell represents one of the 256 possible first-byte opcodes, color-coded by instruction category.The original 8086/8088 instructions are grouped by function:Each instruction has several key attributes you will find in a reference:
Mnemonic — the human-readable name (e.g., ADD)
Opcode — the machine code byte(s) the CPU actually executes (e.g., 0x00...0x05 for ADD)
Operands — what the instruction acts on (register, memory address, immediate value)
Affected flags — which bits in the FLAGS register change after execution (e.g., ZF, CF, OF)
Understanding these attributes connects directly to what we learned in Lecture 5 about reading machine code byte sequences. When you see a byte like 0x90 in a memory dump, an instruction reference tells you that this is NOP (no operation) — which is actually encoded as XCHG AX, AX. When you see 0xE8 followed by two bytes, you know it is a near CALL with a 16-bit relative offset.Let us walk through how to read a typical instruction reference entry. Consider ADD:
Mnemonic: ADD — meaning "Add"
Operand forms: (1) r += r/m/imm; — add a register, memory value, or immediate to a register; (2) m += r/imm; — add a register or immediate to a memory location
Opcodes: 0x00...0x05, 0x80/0...0x81/0, 0x83/0
Flags affected: OF, SF, ZF, AF, CF, PF
The notation r/m/imm is standard shorthand: r means register, m means memory address, and imm means immediate (a constant value embedded in the instruction). The slash notation in opcodes like 0x80/0 means "opcode byte 0x80 with the reg field of the ModR/M byte set to 0" — a detail we explored in Lecture 5 when decoding machine code.Notice that ADD has multiple opcode bytes. This is because a single mnemonic can encode several different operand combinations: byte vs. word, register-to-register vs. register-to-memory, with or without an immediate value. The CPU selects the appropriate encoding based on the operand types and sizes.A few instructions in the reference deserve special mention because they reveal interesting design choices:
NOP (0x90) — "No operation" does nothing, yet it is one of the most useful instructions. It is used for alignment, timing delays, and as a placeholder during debugging. Its opcode is actually shared with XCHG AX, AX, which swaps AX with itself — a clever reuse of encoding space.
LEA (Load Effective Address) — Despite its name, LEA does not access memory at all. It computes an address and stores it in a register. Programmers often use it as a fast way to perform arithmetic (e.g., LEA BX, [SI+DI+4] computes SI + DI + 4 without a memory read).
XLAT (0xD7) — A single-byte instruction that performs AL = [BX + AL], implementing a 256-entry lookup table in one instruction. It is a reminder of how 8086 designers optimized for common patterns.
INT (Software Interrupt) — The gateway to operating system services. INT 21h in DOS and INT 80h in Linux are how assembly programs request OS functions. We will use these extensively in labs.
Two resources are particularly useful when studying the 8086 instruction set:
<br>
x86 Opcode Structure and Instruction Overview (<a data-tooltip-position="top" aria-label="http://pnx.tf/files/x86_opcode_structure_and_instruction_overview.pdf" rel="noopener nofollow" class="external-link is-unresolved" href="http://pnx.tf/files/x86_opcode_structure_and_instruction_overview.pdf" target="_self">PDF</a>) — This document shows how opcode bytes are structured internally: which bits specify the operation, which bits indicate the direction (register-to-memory or memory-to-register), and which bits encode the operand size. It is invaluable for understanding why instructions are encoded the way they are, rather than just memorizing opcode values. <br>
Wikipedia: Original 8086/8088 Instructions (<a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/X86_instruction_listings#Original_8086/8088_instructions" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/X86_instruction_listings#Original_8086/8088_instructions" target="_self">link</a>) — This is the best quick-reference table for the original instruction set. It lists every mnemonic alphabetically with its meaning, operand notation, and opcode bytes. When you need to look up what CBW does or what opcode STOSB uses, this is the place to go. It is worth noting that as the x86 architecture evolved over the following decades — from the 80286, 80386, and 80486 through the Pentium family and into modern x86-64 processors — the instruction set grew enormously. Modern x86-64 processors support thousands of instructions, including extensions for floating-point arithmetic (x87, SSE, AVX), cryptography (AES-NI), and vector processing (AVX-512). However, at its core, the fundamental patterns established by the original 8086 remain: data movement, arithmetic, logic, control flow, and string manipulation. By mastering the original instruction set and learning to use references effectively, you build a foundation that transfers directly to understanding even the most complex modern extensions.In 1965, Gordon E. Moore — then director of R&amp;D at Fairchild Semiconductor, and later co-founder of Intel — published a short article in Electronics magazine that would become one of the most consequential predictions in the history of technology. Based on just a handful of data points, Moore observed that the number of transistors that could be cost-effectively placed on an integrated circuit was doubling approximately every year. He projected that this trend would continue "for at least ten years."In 1975, Moore revised his estimate: the doubling period was closer to every two years. This revised observation — the number of transistors on a chip doubles approximately every two years — became known as Moore's Law. It is worth noting that Moore's original paper was remarkably brief and pragmatic. He was not attempting to formulate a grand theory — he was making a business forecast about what was economically optimal. The "law" emerged only in retrospect, as the industry continued to hit the predicted targets year after year.It is crucial to understand what Moore's Law is and what it is not. Moore's Law is not a law of physics. There is no equation in quantum mechanics or thermodynamics that mandates transistor counts must double on a fixed schedule. Instead, Moore's Law is an empirical observation about the semiconductor industry's sustained ability to miniaturize transistors and improve manufacturing processes.<br><img alt="Gordon Moore's original 1965 graph showing component count doubling" src="assets/l06-moores-law-original-graph.png" target="_self">Gordon Moore's original 1965 graph: the number of components per integrated circuit, extrapolated to 1975. The observation started with just a handful of data points.What makes it remarkable is its longevity. Moore originally predicted the trend would hold for a decade. It has now held for over 60 years. The reason is not that silicon obeys some doubling law — it is that the semiconductor industry organized itself around this prediction. Moore's Law became a self-fulfilling prophecy: chip manufacturers set their R&amp;D roadmaps to meet the expected doubling, equipment vendors designed lithography tools to enable it, and software developers wrote programs that assumed hardware would keep getting faster. The entire ecosystem aligned around this exponential trajectory.To put the doubling in concrete terms: if you start with 2,300 transistors (the Intel 4004 in 1971) and double every two years, after 54 years (27 doublings) you would expect roughly 2,300 x 2^27 = approximately 309 billion transistors. The actual count for the Apple M3 Ultra in 2025 — 184 billion — is within the same order of magnitude, confirming that the exponential trend has held remarkably well across more than five decades of semiconductor innovation.The growth in transistor counts across processor generations is staggering. Consider how far we have come since the 8086:From 29,000 transistors in the 8086 to 184 billion in the Apple M3 Ultra — that is a factor of over 6 million in 47 years. If you plot these numbers on a logarithmic scale, the data points fall remarkably close to a straight line, which is precisely the signature of exponential growth.<br><img alt="Transistor count in microprocessors over time, showing exponential growth" src="assets/l06-transistor-count-chart.png" target="_self"><br>Transistor count in commercial microprocessors from 1971 to present, plotted on a logarithmic scale. The doubling trend has held remarkably steady for over 50 years. Source: <a data-tooltip-position="top" aria-label="https://ourworldindata.org/moores-law" rel="noopener nofollow" class="external-link is-unresolved" href="https://ourworldindata.org/moores-law" target="_self">Our World in Data</a>.A key skill when studying Moore's Law is reading logarithmic graphs. On a standard (linear) graph, the jump from 29,000 to 184 billion would make the early data points invisible — they would all cluster at the bottom axis. A logarithmic scale compresses large ranges by using powers of 10 as equal intervals: 10^3, 10^4, 10^5, and so on each get the same vertical space. On such a graph, a straight line means constant percentage growth — exactly what doubling every two years produces.To appreciate what Moore's Law has made possible, consider the Apple M3 Ultra, announced in March 2025 for the Mac Studio. This is not a theoretical research chip — it is a commercially available processor that professionals use daily for video editing, 3D rendering, and machine learning development. This single chip contains:
184 billion transistors — connected via Apple's UltraFusion packaging technology, which bonds two M3 Max dies together with over 10,000 signal connections and 2.5 TB/s of interprocessor bandwidth
32 CPU cores (24 performance + 8 efficiency) — more processing cores than the most powerful servers had just two decades ago
80 GPU cores — enabling professional graphics workloads and real-time ray tracing
32 Neural Engine cores — dedicated hardware for machine learning inference
Up to 512 GB unified memory with over 800 GB/s bandwidth — enough to run large language models with 600+ billion parameters entirely on-device
The M3 Ultra is manufactured on a 3-nanometer process. To put that in perspective: a single transistor gate at 3 nm is roughly 15 silicon atoms wide. We are operating at a scale where individual atoms matter.Compare this to the 8086, which we studied in earlier lectures: 29,000 transistors, a single core, no GPU, no neural engine, and a 3-micrometer process (1,000 times larger than 3 nm). The M3 Ultra's transistor count is approximately 6.3 million times that of the 8086 — and yet the chip fits in a compact desktop form factor and consumes a fraction of the power per transistor.The M3 Ultra also illustrates an important modern trend: heterogeneous computing. Rather than using all its transistors for general-purpose CPU cores, it dedicates large portions of its die area to specialized hardware — GPU cores for graphics and parallel computation, Neural Engine cores for machine learning inference, media engines for video encoding and decoding, and dedicated controllers for Thunderbolt 5 connectivity (up to 120 Gb/s per port). This specialization is only possible because Moore's Law has provided enough transistors to build all these accelerators alongside the CPU on a single chip.<br><img alt="Apple M3 Ultra die shot showing 184 billion transistors" src="assets/l06-apple-m3-ultra-die.jpeg" target="_self">Apple M3 Ultra die shot (March 2025): 184 billion transistors, 32 CPU cores, 80 GPU cores, and 32 Neural Engine cores on a single package.Moore's Law is often discussed in isolation, but for most of its history it worked in tandem with another key observation: Dennard Scaling, formulated by Robert Dennard of IBM in 1974. Dennard observed that as transistors shrank, their power consumption decreased proportionally — smaller transistors could switch faster while using less voltage and current. This meant that each new generation of chips could be both faster and more power-efficient.Dennard Scaling broke down around 2006. By that point, transistors had become so small that reducing voltage further caused unacceptable levels of current leakage. The result was the "power wall" — chips could still pack more transistors, but each transistor was no longer cheaper to power. This is precisely why the industry shifted from making single cores faster (the "GHz race" of the early 2000s, which peaked around 3-4 GHz) to putting multiple cores on a single chip. If you cannot make one core faster without overheating, you add more cores and distribute the work. This architectural shift — from frequency scaling to parallelism — is one of the most important consequences of Dennard Scaling's end, and it directly shapes how modern software must be written to take full advantage of hardware.The engine behind Moore's Law is photolithography — the process by which circuit patterns are projected onto silicon wafers using light. Each new generation of lithography equipment can print smaller features, allowing more transistors to fit in the same area.The progression of process nodes tells this story:
In the 1970s, transistor features were measured in micrometers (um) — the 8086 used a 3 um process
By the 2000s, the industry reached nanometer (nm) scale — Intel's Pentium 4 used a 90 nm process
Today, leading-edge chips use 3 nm processes (TSMC, used by Apple), with 2 nm in development
Alongside lithography improvements, several other advances contribute:
New transistor architectures: The shift from planar transistors to FinFET (3D fin-shaped transistors) around 2012 allowed continued scaling when traditional planar designs hit physical limits. The next transition — to Gate-All-Around (GAA) nanosheet transistors — is underway for 2 nm and beyond.
Advanced packaging: Technologies like Apple's UltraFusion and TSMC's CoWoS (Chip-on-Wafer-on-Substrate) allow multiple dies to be connected into a single package, effectively increasing transistor counts without requiring every transistor to be on one monolithic die.
EUV lithography: Extreme Ultraviolet lithography, using 13.5 nm wavelength light (compared to 193 nm for previous deep UV), enabled the jump to 7 nm and below. EUV was decades in development and is one of the most complex manufacturing technologies ever created.
The economics of Moore's Law are just as important as the physics. Shrinking transistors means more transistors per wafer, which reduces the cost per transistor — even if the wafer itself is more expensive. This economic incentive is what keeps the industry investing tens of billions of dollars per year in R&amp;D. As long as the cost per transistor continues to fall (or at least, the value per transistor continues to rise through specialization), the incentive to push further remains strong.The question of whether Moore's Law is "ending" has been debated for decades — and the answer depends on what exactly you measure. Several physical challenges are very real:
Quantum tunneling: As transistor gates shrink below 5 nm, electrons can tunnel through barriers that should be insulating. This creates leakage current, wasting power and generating heat even when the transistor is supposed to be "off."
Heat dissipation: More transistors in the same area means more heat per square millimeter. Power density has become a primary constraint on chip design — a challenge sometimes called the "power wall."
Atomic limits: At 3 nm, we are working with structures just tens of atoms wide. There is a hard physical floor: you cannot make a transistor smaller than an atom.
Economic costs: Each new process node requires more expensive equipment. A cutting-edge semiconductor fab now costs over $20 billion to build.
In terms of raw transistor doubling, the pace has indeed slowed. The gap between process node generations has stretched from the historical two years to closer to three. However, the industry has found creative ways to continue delivering more transistors per chip — multi-die packaging, 3D stacking, and chiplet architectures all contribute to sustaining the overall trend even as the per-transistor scaling becomes harder.It is also worth noting that "process node" names have become increasingly marketing-driven. A "3 nm" chip does not literally have 3 nm features everywhere — the name reflects a generation of manufacturing technology, not a direct physical measurement. What matters for Moore's Law is whether the industry continues to deliver roughly twice as many transistors per chip every two to three years, and by that measure, the trend continues — albeit with greater engineering effort and cost per generation.The exponential growth described by Moore's Law is not merely an abstract metric about transistor density. It is the fundamental force that has driven the entire digital revolution. Consider what this exponential trajectory has enabled:
Personal computing: The computing power that once filled a room in the 1960s now fits in your pocket. A modern smartphone contains more transistors than all the computers that existed in the world in 1970 combined.
Artificial intelligence: Training large language models and neural networks requires enormous computational capacity. The fact that a single M3 Ultra chip can run 600+ billion parameter models on-device was unthinkable even five years ago — Moore's Law made it possible.
Cloud computing: Massive data centers running millions of processors power the internet services we use daily. Each server rack today has more processing power than the entire computing infrastructure of most countries in the 1990s.
Scientific computing: Climate modeling, protein folding (AlphaFold), genome sequencing, and particle physics simulations all depend on exponential growth in available computation.
Cost reduction: The cost per transistor has fallen by roughly a factor of one billion since the 1960s. This economic effect is just as important as the performance gains — it is why a $300 smartphone today contains more computing power than a $10 million supercomputer from the 1990s.
A useful thought experiment illustrates the scale of exponential growth: if the automotive industry had progressed at the same rate as semiconductors since the 1970s, a car today would travel at roughly the speed of light, get millions of miles per gallon, and cost less than a cup of coffee. No other industry in human history has sustained this pace of improvement for this long.Beyond transistor density, related exponential trends reinforce these advances. Computational capacity has doubled approximately every 1.5 years since 1975. Computing efficiency (measured as computations per unit of energy) has halved its energy requirements every 1.5 years over six decades. Storage costs have declined exponentially since the 1950s — a trend that continues today with SSDs and cloud storage.For students of computer systems architecture, Moore's Law provides essential context for understanding why processors are designed the way they are. The 8086 we study in this course was a product of its era — 29,000 transistors meant every gate was precious, leading to the compact but sometimes irregular instruction set we explored in the previous section. The segmented memory model, the limited number of registers, the reliance on CISC (Complex Instruction Set Computing) design — all of these were shaped by the transistor budgets available in 1978.As transistor budgets grew exponentially, processor designers could afford entirely new approaches: pipelining, out-of-order execution, branch prediction, multiple levels of cache, and eventually multiple cores on a single die. Each of these innovations is a direct consequence of having more transistors to work with. Understanding Moore's Law therefore helps you understand not just what modern processors look like, but why they evolved from the architectures we study in this course.Moore's Law, whether it continues in its original form or evolves into new metrics, has shaped the world we live in. Understanding it is essential context for anyone studying computer systems architecture, because it explains not just where processors have been, but the engineering pressures and innovations that will define where they go next.In previous lectures, we studied the fundamental building blocks of digital systems: logic gates, flip-flops, registers, and arithmetic circuits. But how do these components come together in real hardware? The answer lies in integrated circuits (ICs) — the technology that made it possible to pack thousands, then millions, then billions of transistors onto a single chip.This section traces the path from the first ICs to the microprocessors that power modern computers, showing how ever-greater integration led from simple logic chips to complete computers on a single piece of silicon.<br>An integrated circuit (IC), also called a chip or microchip, is a set of electronic circuits on a single, small piece of semiconductor material — usually <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Silicon" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Silicon" target="_self">silicon</a>. The principle of creating ICs was developed in 1958 by two American inventors working independently: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Jack_Kilby" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Jack_Kilby" target="_self">Jack Kilby</a> at Texas Instruments (who used germanium) and <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Robert_Noyce" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Robert_Noyce" target="_self">Robert Noyce</a> at Fairchild Semiconductor (who used silicon). Both were trying to solve the same problem: how to fit the maximum number of components into the minimum amount of space.In 1959, both received patents on their inventions, leading to a rivalry between the two companies that eventually ended in a cross-licensing agreement. By 1961, Fairchild Semiconductor began selling ICs commercially, and they were immediately adopted in calculators and computers, replacing discrete transistors and dramatically reducing device size while increasing performance.Why ICs matter:
<br>Cost: ICs are manufactured using <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Photolithography" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Photolithography" target="_self">photolithography</a>, a process that effectively "prints" circuits rather than assembling them from individual components. This makes mass production extremely cheap.
Speed: Components on an IC are tiny and closely spaced, so electrical signals travel very short distances, enabling much faster switching.
Power efficiency: Small transistors consume less power and generate less heat than discrete components.
Reliability: Fewer external connections means fewer points of failure.
The main disadvantage of ICs is the high initial cost of design and fabrication of photomasks. This makes ICs commercially viable only at sufficiently large production volumes.Levels of integration:As manufacturing technology advanced, engineers were able to fit more and more transistors onto a single chip. The industry classifies ICs by their level of integration:<br>Today's processors contain billions of transistors on a chip the size of a fingernail. This extraordinary progress is described by <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Moore%27s_law" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Moore%27s_law" target="_self">Moore's Law</a>, the observation that the number of transistors on a chip roughly doubles every two years.Manufacturing process nodes: The "size" of transistors on a chip is often described by the manufacturing process node. In the 1970s, the minimum feature size was 2-8 micrometers (um). By the 1990s, Intel Pentium processors were manufactured at 500-600 nm. The early 2000s brought 180 nm and 90 nm processes, and by the 2010s the industry had reached 14 nm and 10 nm. As of 2024, leading-edge chips from Apple, AMD, and others use 3 nm processes. Each shrink allows more transistors per chip, higher performance, and lower power consumption.Example — IC 7408: A simple SSI-level IC containing four AND gates on one chip. Each gate has two inputs and one output, plus shared power and ground pins. This is exactly the kind of gate we studied in Lectures 1 and 2 — but now packaged as an IC rather than built from discrete transistors.<br><img alt="IC 7408: DIP package and internal schematic showing four AND gates" src="assets/l06-ic-7408-and-gate.png" target="_self">The 7408 integrated circuit: a 14-pin DIP package (left) containing four independent 2-input AND gates (right). This is the same AND gate we studied in Lecture 1, now packaged as an IC.<br>The <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/7400-series_integrated_circuits" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/7400-series_integrated_circuits" target="_self">7400 series</a> is one of the most important product families in the history of digital electronics. It is a family of <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Transistor%E2%80%93transistor_logic" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Transistor%E2%80%93transistor_logic" target="_self">transistor-transistor logic</a> (TTL) integrated circuits that became the de facto standard for building digital systems.History:
In 1964, Texas Instruments introduced the SN5400 series in ceramic military-grade packages.
In 1966, a cheaper plastic-packaged SN7400 series was released for commercial applications.
The 7400 series quickly captured over 50% of the logic chip market and became the standard building blocks of digital electronics for decades.
<br>The first chip in the series, the 7400, contains four two-input NAND gates in a 14-pin <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Dual_in-line_package" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Dual_in-line_package" target="_self">dual in-line package</a> (DIP). Each gate uses two input pins and one output pin, with the remaining two pins for power (+5 V) and ground.What does the 7400 series contain?The series includes hundreds of different devices providing:
Basic logic gates (AND, OR, NAND, NOR, XOR, NOT)
Flip-flops and latches
Counters and shift registers
Multiplexers and demultiplexers
Encoders and decoders
<br>Arithmetic logic units (ALUs), such as the <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/74181" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/74181" target="_self">74181</a>
Bus transceivers and special-purpose interface chips
These are precisely the building blocks we studied in Lectures 1 through 5 — gates, flip-flops, registers, counters, ALUs — but packaged as standardized ICs that engineers could combine on printed circuit boards to build complete computers.Sub-families:Over the decades, many pin-compatible sub-families were introduced with different trade-offs between speed and power consumption:The original TTL parts operated at 5 V and could switch in about 10-22 nanoseconds. Later CMOS versions offered the same logic functions with significantly lower power consumption and wider operating voltage ranges, making them suitable for battery-powered devices.The 5400 and 7400 series were used in many popular minicomputers of the 1970s and early 1980s. For example, some models of the DEC PDP series used the 74181 ALU chip as the main computing element in their CPU. The original 7400 NAND gate chip cost about $22 in 1965 (military grade); by 2007, commercial-grade chips could be purchased for approximately $0.25 each. Today, surface-mounted CMOS versions of the 7400 series are still used in various applications, and through-hole DIP versions remain popular for breadboard prototyping and education.A microcontroller (MCU) is a small computer on a single integrated circuit. Unlike a simple IC that implements a few logic gates, a microcontroller contains:
One or more CPU cores (processors)
Memory: both ROM (for program storage) and RAM (for data)
Programmable I/O peripherals: timers, serial interfaces, analog-to-digital converters, etc.
<br>In modern terminology, a microcontroller is similar to a <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/System_on_a_chip" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/System_on_a_chip" target="_self">System on Chip</a> (SoC), though SoCs typically integrate more advanced peripherals like GPU cores, Wi-Fi modules, or coprocessors.Microcontrollers are everywhere. A typical home in a developed country has only four or so general-purpose microprocessors (in PCs and laptops) but around three dozen microcontrollers — in the washing machine, microwave oven, remote controls, smoke alarms, and more. A typical mid-range automobile contains about 30 microcontrollers managing everything from engine control to power windows.Key characteristics:
Designed for embedded applications — they are built into and control other devices
Include all essential components on a single chip — no external memory or I/O chips required
Optimized for low cost and low power consumption
Programmed once (or via flash memory) to perform a specific task
Example — TMS1000 (Texas Instruments, 1974):<br>The <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/TMS1000" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/TMS1000" target="_self">TMS1000</a> holds a special place in computing history as the first commercial general-purpose microcontroller. It was a true "computer on a chip":
<br>4-bit CPU with a <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Harvard_architecture" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Harvard_architecture" target="_self">Harvard architecture</a> (separate program and data memory)
1,024 x 8 bits of ROM for program storage (mask-programmed at the factory)
64 x 4 bits of RAM for data
23 I/O lines for interfacing with the outside world
Clock speed of approximately 0.3 MHz
Available in a 28-pin DIP package
Priced at approximately $2 per chip in volume (1974)
By 1979, Texas Instruments was selling approximately 26 million TMS1000-family parts per year. The chip was used in iconic products including Texas Instruments' own Speak &amp; Spell educational toy, the Big Trak programmable vehicle, and the electronic game Simon.The TMS1000 used mask-programmed ROM: once the customer had a debugged program, they sent it to Texas Instruments, who created a special photomask to burn the program into the on-chip ROM during manufacturing. While this had a high initial setup cost, the per-unit production cost was extremely low — ideal for high-volume consumer products.<br><img alt="Texas Instruments TMS1000 chip in DIP package" src="assets/l06-tms1000-chip.jpeg" target="_self">The Texas Instruments TMS1000 (1974): the first commercial microcontroller. A complete 4-bit computer — CPU, ROM, RAM, and I/O — in a single 28-pin DIP package.Microcontrollers today:<br>The microcontroller market has grown enormously since the TMS1000. In 2002, about 55% of all CPUs sold worldwide were 8-bit microcontrollers. Modern microcontrollers range from simple 8-bit devices costing under $0.03 to powerful 32-bit ARM Cortex-M processors with built-in floating-point units, Wi-Fi, Bluetooth, and advanced peripherals — all on a single chip for just a few dollars. The popular <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Arduino" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Arduino" target="_self">Arduino</a> and <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/ESP32" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/ESP32" target="_self">ESP32</a> development boards are modern examples of microcontroller-based platforms widely used in education and IoT applications.<br>A microprocessor is a computer processor in which the data processing logic and control circuitry are integrated onto a single integrated circuit (or a small number of ICs). It contains the arithmetic, logic, and control circuits required to perform the functions of a computer's <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Central_processing_unit" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Central_processing_unit" target="_self">central processing unit</a> (CPU).Key properties of a microprocessor:
Multipurpose: can execute any program stored in memory
Clock-driven: operations are synchronized by an external clock signal
Register-based: uses internal registers for fast data storage and manipulation
Accepts binary data as input, processes it according to instructions (also binary), and produces binary results as output
Contains both combinational logic (for computation) and sequential logic (for state and control)
The key distinction: Microcontroller vs. MicroprocessorThis is one of the most important distinctions in computer architecture:
A microcontroller is a complete computer on a chip — it includes the CPU, memory (ROM + RAM), and I/O peripherals, all integrated on one IC. It is designed for embedded, typically single-purpose applications.
A microprocessor is just the CPU — it needs external memory chips, I/O controllers, and other support circuitry to form a complete computer system. It is designed for general-purpose computing.
Think of it this way: a microcontroller is like a Swiss Army knife (everything in one package), while a microprocessor is like a chef's knife — specialized and powerful, but it needs the rest of the kitchen to be useful.Brief history of microprocessors:<br>The first commercially available microprocessor, the Intel 4004 (1971), was designed by a team led by <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Federico_Faggin" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Federico_Faggin" target="_self">Federico Faggin</a> using his silicon-gate MOS technology, along with Marcian Hoff, Stanley Mazor, and Masatoshi Shima. It was originally created for Busicom, a Japanese calculator manufacturer, who had asked Intel to build a chipset for high-performance desktop calculators. Hoff proposed simplifying Busicom's original seven-chip design into a four-chip architecture with a general-purpose CPU, and Faggin made the single-chip CPU a reality. Intel recognized its general-purpose potential and began marketing it broadly.Before microprocessors, small computers were built using racks of circuit boards containing many medium- and small-scale ICs — typically from the 7400 TTL series we discussed earlier. For example, the DEC PDP minicomputers used the 74181 ALU chip as their main computational element. Microprocessors combined all of that CPU functionality into one or a few large-scale ICs, dramatically reducing cost, size, and power consumption while increasing reliability.<br>The integration of an entire CPU onto a single chip using <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Very_Large_Scale_Integration" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Very_Large_Scale_Integration" target="_self">VLSI</a> technology was transformative. Since integrated circuit processors are produced in large numbers by highly automated MOS fabrication processes, the unit cost is relatively low. And as chip designs improve, the cost of manufacturing a chip of the same physical size generally stays the same (<a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Rock%27s_law" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Rock%27s_law" target="_self">Rock's law</a>), even as it gains more transistors and capabilities — a virtuous cycle that has driven six decades of exponential progress in computing.Comparison: IC vs. Microcontroller vs. MicroprocessorNow that we understand how transistors are integrated into chips, and how chips evolved from simple logic gates to complete processors, we can look at the main components that make up a modern computer system. This section provides a brief overview; the following sections will examine each component in greater detail.A computer, at its most fundamental level, consists of four major components:The CPU is the "brain" of the computer. It is the component responsible for:
Fetching instructions from memory
Decoding those instructions to determine what operation to perform
Executing the operations (arithmetic, logic, data movement, control flow)
Modern CPUs are microprocessors — single-chip implementations of the processor architecture. They contain the ALU, control unit, registers, and cache memory that we have been studying throughout this course. We will explore CPU architecture in much greater depth in later lectures.The motherboard is the backbone of the computer. It is the main printed circuit board (PCB) that:
Provides the physical and electrical connections between all other components
Houses the CPU socket, memory slots, expansion slots, and I/O connectors
Contains the chipset — a set of ICs that manages data flow between the CPU, memory, and peripherals
Distributes power to all components
Contains the clock generator that synchronizes all operations
The motherboard is essentially the "nervous system" of the computer, ensuring that data can travel between the CPU, memory, and all I/O devices. We will examine the motherboard and its bus architecture in Section 7, and the clock system in Section 8.Memory stores both the data being processed and the instructions (programs) that tell the CPU what to do. Computer memory is organized in a hierarchy based on speed, cost, and capacity:
Registers (inside the CPU): fastest, smallest, most expensive per bit
Cache (on or near the CPU): very fast, small
Main memory (RAM): fast, moderate capacity — this is where running programs and their data reside
Secondary storage (SSD, HDD): slower, large capacity, non-volatile — for permanent data storage
The key distinction is between volatile memory (like RAM, which loses its contents when power is off) and non-volatile memory (like flash storage, which retains data without power). We will explore memory technology and hierarchy in detail in Section 9.I/O devices are the interface between the computer and the outside world. They allow the computer to:
Receive data (input): keyboard, mouse, microphone, camera, sensors, network interface
Send data (output): display, speakers, printer, network interface, actuators
Store data (storage I/O): SSD, HDD, USB drives, optical drives
Some devices serve dual roles — a network interface card both sends and receives data, and a touchscreen is both an input and output device.I/O devices communicate with the CPU through I/O controllers connected to the motherboard's bus system. Managing I/O efficiently is one of the key challenges in computer architecture, as I/O devices operate at vastly different speeds — from a keyboard (a few bytes per second) to a modern NVMe SSD (gigabytes per second).These four components — CPU, motherboard, memory, and I/O devices — form the foundation of every computer system, from the smallest embedded microcontroller to the largest supercomputer. The difference lies in scale and complexity:In the following sections, we will examine the motherboard (Section 7), clock system (Section 8), and memory hierarchy (Section 9) in much greater detail.In the previous sections we examined how computing hardware evolved from simple integrated circuits through microcontrollers to full microprocessors. But a microprocessor by itself is just a chip — it needs a platform that connects it to memory, storage, input/output devices, and a power supply. That platform is the motherboard.<br>The motherboard (also called the mainboard, system board, or logic board) is the main <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Printed_circuit_board" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Printed_circuit_board" target="_self">printed circuit board</a> (PCB) in a general-purpose computer. It serves as the backbone for data transmission between all major system components. The motherboard holds and provides electrical connections between the CPU, memory, storage controllers, expansion slots, and peripheral connectors.A typical modern motherboard contains:
CPU socket — a mechanical and electrical interface where the processor is installed. The socket type must match the CPU family (e.g., Intel LGA 1700 for 12th/13th/14th-gen Core processors, AMD AM5 for Ryzen 7000-series).
Memory slots — connectors for DRAM modules (typically DDR4 or DDR5 DIMMs). Desktop motherboards commonly have 2 or 4 memory slots.
Chipset — one or two ICs that form the interface between the CPU, main memory, and peripheral buses. The chipset determines which features and I/O capabilities the motherboard supports.
Expansion slots — PCIe (Peripheral Component Interconnect Express) slots for graphics cards, network adapters, storage controllers, and other add-in cards.
Storage connectors — SATA ports and M.2 slots for SSDs and hard drives.
Power connectors — receive power from the power supply unit (PSU) and distribute it to the CPU, memory, and other components.
Clock generator — produces the system clock signal that synchronizes all components (more on this in Section 8).
Firmware storage — non-volatile memory (flash ROM) containing the BIOS or UEFI firmware that initializes hardware during boot.
Integrated peripherals — modern motherboards integrate USB controllers, audio codecs, Ethernet controllers, Wi-Fi modules, and other peripherals directly onto the board.
The motherboard is where all components come together. Data flows between components through communication channels called buses — the electrical pathways etched into the PCB's copper layers. The most important of these is the system bus.<br><img alt="Schematic motherboard layout showing CPU, RAM, expansion slots, and ports" src="assets/l06-motherboard-layout.png" target="_self">Schematic layout of a typical motherboard: CPU socket, RAM slots, expansion slots (PCI), rear I/O ports, and SATA connectors for storage devices.The system bus is the primary communication pathway through which the CPU exchanges data with memory and other devices. Conceptually, it is a bundle of parallel electrical lines that carry different types of signals. The system bus is divided into three logical sub-buses, each with a distinct role:1. Data BusThe data bus carries the actual data being transferred — the bits representing instructions, operands, or results. The width of the data bus determines how many bits can be transferred in a single operation:
The Intel 8086 has a 16-bit data bus, meaning it transfers 16 bits (2 bytes) at a time.
The Intel 80386 introduced a 32-bit data bus (4 bytes at a time).
Modern x86-64 processors use a 64-bit data bus (8 bytes at a time).
A wider data bus means higher throughput — more data per transfer cycle. The data bus is bidirectional: data can flow from the CPU to memory (write) or from memory to the CPU (read).2. Address BusThe address bus carries the memory address specifying where data should be read from or written to. The CPU places an address on the address bus, and the memory subsystem uses that address to select the correct memory location. The width of the address bus determines the maximum addressable memory:
The 8086 has a 20-bit address bus: 2^20 = 1,048,576 bytes = 1 MB of addressable memory.
The 80386 has a 32-bit address bus: 2^32 = 4,294,967,296 bytes = 4 GB.
Modern x86-64 CPUs typically implement 48-bit physical addressing: 2^48 = 256 TB (though most systems use far less).
The address bus is unidirectional in simple systems — it carries addresses from the CPU to memory. In more complex systems with bus mastering or cache snooping, it may be bidirectional.3. Control BusThe control bus carries control and status signals that coordinate the operation of all components. These signals determine the direction and timing of data flow. Key control signals include:
Read/Write — indicates whether the current operation is a memory read or a memory write.
Clock signal — the system clock that synchronizes all bus operations.
Interrupt request (IRQ) — signals from I/O devices requesting the CPU's attention.
Bus request / Bus grant — signals used for bus arbitration when multiple devices need bus access.
Memory ready — a signal from memory indicating that data is available on the data bus.
The control bus consists of individual signal lines, each carrying a specific control signal. Unlike the data and address buses, the control bus is a collection of separate, individual lines rather than a single multi-bit value.<br><img alt="System bus diagram: data, address, and control buses connecting CPU, RAM, and I/O" src="assets/l06-system-bus-diagram.png" target="_self">The three components of the system bus: the data bus carries information, the address bus specifies where, and the control bus coordinates the timing and direction of transfers.Understanding the system bus is essential because it explains many real-world constraints. For example, the reason the 8086 can only address 1 MB of memory is not a limitation of the CPU's internal capabilities — it is determined by the 20 physical address lines on the bus. Similarly, the reason 32-bit operating systems cannot use more than 4 GB of RAM traces directly back to the 32-bit address bus of the 80386 architecture.
Connection to L03: Recall that in Lecture 3 we learned about segment:offset addressing, where a 16-bit segment register and a 16-bit offset are combined to produce a 20-bit physical address. Now we can see why this scheme exists: the CPU's internal registers are 16 bits wide, but the address bus requires 20-bit addresses. The segment:offset mechanism bridges this gap.
With the motherboard providing the physical platform and the system bus providing the communication channels, there is one more essential piece to consider: how do all these components stay synchronized? The answer is the system clock.The system clock is a small quartz crystal oscillator on the motherboard that generates a continuous stream of electrical pulses at a precise frequency. Think of it as a metronome for the entire computer system — it provides a steady beat that all components follow.Without the clock, components would have no way to coordinate their actions. The CPU would not know when data on the bus is valid. Memory would not know when to respond. The clock solves this by establishing discrete clock cycles — uniform time intervals during which one unit of work can occur.<br><img alt="Clock signal: square wave showing one clock period" src="assets/l06-clock-signal.gif" target="_self">A clock signal is a square wave alternating between 0 and 1. One complete cycle (high + low) is one clock period. At 3.4 GHz, each period is approximately 0.29 nanoseconds.CPU speed is expressed as a frequency — the number of clock cycles per second, measured in Hertz (Hz):A single clock cycle is the fundamental unit of time inside the computer. Each instruction takes one or more clock cycles to execute. Simple operations like register-to-register MOV might complete in 1 cycle, while complex operations like integer division can take dozens of cycles.It is tempting to equate clock speed with performance — "a 4 GHz CPU is faster than a 3 GHz CPU." This is an oversimplification. Performance depends on many factors:
Instructions per cycle (IPC): A CPU that completes 2 instructions per cycle at 3 GHz outperforms one that completes 1 instruction per cycle at 4 GHz.
Architecture and pipeline depth: Modern CPUs use pipelining, out-of-order execution, and branch prediction to accomplish more work per cycle.
Cache size and hierarchy: Fast cache memory (covered in Section 9) can dramatically reduce the time the CPU spends waiting for data.
Memory bandwidth: Even the fastest CPU stalls if it cannot get data from memory quickly enough.
This is why modern CPU generations often deliver significant performance improvements at the same or even lower clock speeds than their predecessors.Overclocking is the practice of running the system clock faster than the manufacturer's rated specification. For example, a CPU rated at 3.6 GHz might be overclocked to 4.2 GHz. This increases the number of operations per second, but comes with trade-offs:
Increased heat generation: faster switching means more power consumption and thermal output.
Potential instability: components may not reliably complete operations within the shortened clock cycle, leading to errors or crashes.
Reduced component lifespan: sustained higher temperatures and voltages can degrade hardware over time.
Overclocking is common among enthusiasts and is supported by many modern motherboards and CPUs (notably Intel's "K"-series and AMD's Ryzen processors with unlocked multipliers).We have now assembled the core components of a computer system: the CPU executes instructions, the motherboard provides the physical platform, the system bus enables communication, and the clock keeps everything synchronized. But there is a critical question we have not yet answered: where is the data stored?Every operation the CPU performs involves data — both the instructions it executes and the operands it processes. This data must be stored somewhere accessible. But storage technologies face a fundamental engineering trade-off:
Fast memory is expensive and small (e.g., CPU registers: a handful of bytes, but accessible in under 1 nanosecond).
Large memory is slow and cheap (e.g., hard disk drives: terabytes of storage, but access times measured in milliseconds).
No single storage technology provides both the speed and the capacity that modern computers need. The solution is a memory hierarchy — a layered system where each level offers a different balance of speed, cost, and capacity. Data that the CPU needs most urgently is kept in the fastest (and smallest) memory, while less frequently needed data resides in slower (but larger) storage.Computer storage can be broadly categorized into three tiers:SRAM (Static Random-Access Memory)<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Static_random-access_memory" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Static_random-access_memory" target="_self">SRAM</a> is the fastest type of volatile memory. It is used for CPU registers and cache memory (L1, L2, L3). Key characteristics:
Very fast access times (sub-nanosecond for registers, 1-10 ns for cache).
Does not require periodic refreshing — data is held by a bistable flip-flop circuit (hence "static").
Expensive to manufacture and physically large per bit (each SRAM cell uses 6 transistors, compared to 1 transistor + 1 capacitor for DRAM).
Used in small quantities where speed is critical.
DRAM (Dynamic Random-Access Memory)<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Dynamic_random-access_memory" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Dynamic_random-access_memory" target="_self">DRAM</a> is what people commonly mean when they say "computer memory" or "RAM." It is the main working memory of the system:
Slower than SRAM but much cheaper and denser (more bits per chip area).
Stores each bit as a charge on a tiny capacitor. Because capacitors leak charge, DRAM must be periodically refreshed (re-read and re-written) — hence "dynamic."
Typical modern systems have 8-64 GB of DRAM.
Access latency: approximately 50-100 nanoseconds (which translates to 100-200 clock cycles on a modern CPU — a significant delay).
Disk StorageDisk storage is rarely called "memory" in technical contexts — it is categorized as storage or drives:
HDD (Hard Disk Drive): Stores data on spinning magnetic platters. Very high capacity (up to 20+ TB) but slow access (5-10 ms seek time). The mechanical nature of HDDs makes them orders of magnitude slower than electronic memory.
SSD (Solid State Drive): Uses NAND flash memory with no moving parts. Much faster than HDDs (0.05-0.1 ms access), but still far slower than DRAM.
NVMe SSD: SSDs connected via the PCIe bus using the NVMe protocol, offering even higher throughput and lower latency than SATA SSDs.
<br><img alt="Memory hierarchy pyramid from registers to disk drive" src="assets/l06-memory-hierarchy-pyramid.png" target="_self">The memory hierarchy: each level trades speed for capacity and cost. Registers at the top are fastest but smallest; disk drives at the bottom are largest but slowest.To understand why memory access is the major performance bottleneck in modern computers, let us examine what happens when the CPU needs to read a value from DRAM. The process follows a 4-step protocol:Step 1: Place Address on the Address BusThe CPU places the target memory address onto the address bus. For the 8086, this is a 20-bit address; for modern CPUs, it is a 48-bit or wider address.Step 2: Set the READ SignalThe CPU asserts (sets to 1) the read signal on the control bus. This tells the memory subsystem that the CPU wants to read data from the specified address, not write to it.Step 3: Wait for the Memory to RespondThis is the slow part. The memory controller receives the address, locates the correct row and column in the DRAM chip, reads the charge on the target capacitor(s), amplifies the signal, and places the data onto the data bus. This process takes approximately 100-200 clock cycles on a modern CPU.During this time, the CPU is waiting — it cannot proceed with the instruction that requested the data. This wait time is called memory latency, and it is the single most significant bottleneck in modern computing.Step 4: Copy Data to DestinationOnce the memory has placed the requested data on the data bus and signaled "ready" on the control bus, the CPU latches (copies) the data from the data bus into the target register or internal buffer.This 4-step protocol reveals a critical insight: even though modern CPUs can execute billions of operations per second, they are frequently stalled waiting for data from DRAM. If the CPU runs at 4 GHz (0.25 ns per cycle), and a DRAM access takes 50 ns (200 cycles), the CPU sits idle for 200 cycles every time it needs data from main memory. This is an enormous waste of computational potential.<br><img alt="DRAM read timing diagram showing Clock, Address, MemRead, MemWait, and DataIn signals" src="assets/l06-dram-read-timing.jpeg" target="_self">DRAM read timing: the CPU places an address and asserts MemRead, then waits for the memory controller to signal data ready. The wait period can span hundreds of clock cycles.The solution to this problem is cache memory.Cache memory is a small, extremely fast SRAM buffer placed between the CPU and main memory (DRAM). Its purpose is to store copies of frequently accessed data so that the CPU can retrieve it without waiting for the slow DRAM access.Modern CPUs organize cache in a multi-level hierarchy. Each level is larger but slightly slower than the one above it. A typical modern processor, such as an Intel Core i7, has:The split between L1i (instruction cache) and L1d (data cache) is a hardware-level optimization: since the CPU fetches instructions and data simultaneously but through different internal pathways, having separate caches for each avoids contention.<br><img alt="Cache hierarchy: two cores with private L1 and L2 caches sharing an L3 cache" src="assets/l06-cache-hierarchy.png" target="_self">Cache hierarchy in a dual-core processor: each core has private L1 and L2 caches for speed, while a larger shared L3 cache serves both cores.Cache is effective because real programs exhibit two key behaviors:
Temporal locality: If a memory address was accessed recently, it is likely to be accessed again soon. For example, a loop counter variable is read and written on every iteration.
Spatial locality: If a memory address was accessed, nearby addresses are likely to be accessed soon. For example, when iterating through an array, elements are stored in consecutive memory addresses.
When the CPU requests data, it checks the cache first:
Cache hit: The data is found in the cache, and the CPU gets it quickly (e.g., 4 cycles from L1 instead of 200 cycles from DRAM). This is the fast path.
Cache miss: The data is not in the cache. The CPU must fetch it from a lower level (L2, L3, or ultimately DRAM). When fetched, the data is also stored in the cache for future access.
On a cache miss, the cache does not just fetch the single requested byte — it fetches an entire cache line (typically 64 bytes). This exploits spatial locality: if the program is accessing address N, it likely will access addresses N+1, N+2, ... N+63 in the near future.A crucial point: programs generally do not have direct access to cache memory. You cannot write an assembly instruction that says "load from L1 cache." Instead, the cache is managed automatically by sophisticated hardware algorithms built into the CPU. When you write MOV AX, [myVar], the CPU's cache controller transparently checks whether myVar is in cache and handles the memory access accordingly.This transparency means that cache accelerates almost all programs without requiring any changes to the software. However, programmers who understand cache behavior can write code that is significantly more cache-friendly — for example, accessing arrays sequentially rather than randomly, which dramatically improves spatial locality and cache hit rates.At the very top of the memory hierarchy sit the CPU registers — the memory locations that assembly code accesses directly. As we learned in Lecture 3, the 8086 has general-purpose registers (AX, BX, CX, DX), index registers (SI, DI), pointer registers (BP, SP), segment registers (CS, DS, SS, ES), the instruction pointer (IP), and the FLAGS register.Key characteristics of registers:
Fastest access: typically 1 clock cycle (sub-nanosecond). No bus transaction is needed because registers are part of the CPU itself.
Smallest capacity: the 8086 has only 14 registers, each 16 bits wide — a total of just 28 bytes of register storage.
Each core has its own set: in a modern multi-core CPU, each core has its own independent register file. Assembly programs are abstracted from this — the code runs on whichever core the operating system assigns it to.
Special purposes: as we covered in L03, many registers serve specific roles in certain instructions (CX as a counter for LOOP, DX:AX for MUL results, SI/DI for string operations, etc.).
The following table summarizes the full memory hierarchy, from fastest to slowest:Notice the enormous gaps between levels. Going from L1 cache to DRAM is roughly a 100x slowdown. Going from DRAM to HDD is roughly a 100,000x slowdown. This is why the memory hierarchy exists: each level acts as a buffer for the level below it, ensuring that the most frequently used data is available at the fastest speed.
Analogy: Imagine you are a researcher at a desk. Your registers are like the notes in your hand — instantly accessible but very limited. L1 cache is like the papers on your desk — fast to reach but limited space. DRAM is like a bookshelf across the room — you have to walk over, find the right book, and bring it back. An HDD is like a library across campus — you have to drive there, search the catalog, and return. Each level takes progressively longer to access but can store progressively more.
The memory hierarchy is not just an academic concept — it has profound practical implications: The Von Neumann Bottleneck: The CPU is fundamentally limited by its ability to get data from memory. Most modern CPUs spend a significant fraction of their time waiting for memory. We will explore this further when we discuss the Von Neumann architecture in a later section. Software Performance: Code that accesses memory in cache-friendly patterns (sequential access, good spatial locality) can run orders of magnitude faster than code that accesses memory randomly. This is why, for example, iterating over a 2D array row-by-row is faster than column-by-column in C (where arrays are stored in row-major order). Hardware Design: Much of the complexity and transistor budget of a modern CPU is dedicated to the cache hierarchy and memory management. Intel's Core i9-13900K, for example, has 36 MB of L3 cache — a substantial portion of the die is devoted to SRAM. System Balance: A well-designed system matches its components appropriately. Pairing an extremely fast CPU with slow memory or a tiny cache wastes the CPU's potential, as it will spend most of its time waiting for data. Throughout the first five practicals, you have been writing assembly programs in DOSBox using TASM. You may have noticed something striking: your programs can read and write to any memory address. You can peek at the Interrupt Vector Table at address 0000:0000, write directly to video memory at B800:0000, and inspect BIOS data structures. Nothing stops you. There is no operating system saying "access denied." This is because the 8086 processor — and DOSBox, which emulates it — operates in Real Mode.But the computer you are sitting at right now does not work this way. Your web browser cannot read the memory of your password manager. A crashing game does not take down the operating system. That isolation exists because modern processors run in Protected Mode. Understanding the distinction between these two modes is essential, because it explains both why our DOSBox environment works the way it does and how modern operating systems achieve security and stability.Real Mode is the original operating mode of the Intel 8086 processor, introduced in 1978. Every x86 processor, even the latest ones, starts up in Real Mode when powered on — the BIOS runs in Real Mode before transitioning to Protected Mode to load the operating system.Address space: 1 MB (20-bit addresses). As we covered in L03, the 8086 has a 20-bit address bus. Using the segment:offset scheme, the processor can address 2^20 = 1,048,576 bytes of memory. The physical address is calculated as (Segment * 16) + Offset, exactly as you have been doing in your lab exercises.No memory protection. This is the most important characteristic of Real Mode. Every program has unrestricted access to the entire 1 MB address space. There are no access controls, no privilege levels, and no boundaries between programs. A single errant MOV instruction can overwrite critical system data. Consider what your programs can freely access:
Interrupt Vector Table (IVT) at 0000:0000 through 0000:03FF — the 256 interrupt vectors, each a 4-byte far pointer. Your program can read these, modify them, or replace them entirely (which is exactly how TSR programs and custom interrupt handlers work).
BIOS Data Area at 0040:0000 — keyboard buffer, timer counter, display mode, disk status, and more. Your program can read the timer tick count or change the keyboard repeat rate by writing to the right address.
Video memory at B800:0000 (text mode) or A000:0000 (graphics mode) — as you have seen in labs, writing bytes here immediately changes what appears on screen. There is no API call required, no permission check.
ROM BIOS at F000:0000 through F000:FFFF — the firmware code itself is mapped here.
No multitasking. In Real Mode, one program "owns" the entire machine. There is no mechanism for the processor to switch between programs, protect one program from another, or preempt a running program. DOS is not a multitasking operating system — when your .COM or .EXE program runs, DOS essentially steps aside and waits for the program to return control via INT 21h (function 4Ch).Real Mode memory map (0x00000 to 0xFFFFF):This is the environment you have been programming in. When you write MOV AX, 0B800h / MOV ES, AX / MOV ES:[0], 'A', you are directly manipulating the hardware. No system call, no driver, no permission — just raw memory access.The limitations of Real Mode became acute as computing evolved. By the early 1980s, users wanted to run multiple programs simultaneously, and they needed those programs to be isolated from one another. If a word processor could accidentally (or maliciously) overwrite the memory of a spreadsheet, the system would be unreliable and insecure.<br>Intel addressed this with Protected Mode, introduced in a basic form on the <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Intel_80286" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Intel_80286" target="_self">Intel 80286</a> (1982) and fully realized on the <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Intel_80386" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Intel_80386" target="_self">Intel 80386</a> (1985). The 80386 is the processor that made modern operating systems like Windows and Linux practical.Address space: up to 4 GB (32-bit addresses). The 80386 extended the address bus to 32 bits, allowing 2^32 = 4,294,967,296 bytes of addressable memory — a 4,096-fold increase over Real Mode's 1 MB.<br>Virtual memory. Each program receives its own virtual address space. The program "thinks" it has access to a large, contiguous block of memory starting at address 0, but in reality, the operating system and the processor's <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Memory_management_unit" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Memory_management_unit" target="_self">Memory Management Unit (MMU)</a> translate these virtual addresses to physical addresses. Two programs can both use virtual address 0x00400000 without conflict, because each maps to a different physical location. Pages of memory that are not actively needed can be "swapped" to disk and brought back when accessed, making the effective memory larger than physical RAM.Memory protection. Programs cannot access memory that does not belong to them. If a program attempts to read or write outside its allocated pages, the processor raises a General Protection Fault (GPF), and the operating system terminates the offending program rather than allowing it to corrupt other programs or the OS itself.Privilege rings. Protected Mode introduces a hardware-enforced privilege system with four levels, numbered Ring 0 through Ring 3:In practice, most modern operating systems (Windows, Linux, macOS) use only two rings: Ring 0 for the kernel and Ring 3 for everything else. Rings 1 and 2 exist in hardware but are rarely used because the two-ring model is simpler and sufficient.When your program in Protected Mode needs to perform a privileged operation — reading a file, allocating memory, communicating with a device — it cannot do so directly. Instead, it makes a system call (also called a syscall): a controlled transition from Ring 3 to Ring 0, where the kernel performs the operation on the program's behalf and returns the result. This is fundamentally different from Real Mode, where your program issues INT 21h not because it must, but because DOS provides convenient functions — nothing prevents you from bypassing DOS and accessing hardware directly.<br>Descriptor tables instead of simple segment:offset. In Protected Mode, the segment registers no longer hold a direct memory address. Instead, they hold a selector — an index into a table called the <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Global_Descriptor_Table" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Global_Descriptor_Table" target="_self">Global Descriptor Table (GDT)</a>. Each entry in this table (called a segment descriptor) specifies the base address, limit (size), and access permissions of a memory segment. The processor checks every memory access against these descriptors, enforcing protection at the hardware level.Understanding both modes is important because you will experience both during this course:Practicals P1 through P10 (Real Mode, DOSBox + TASM): You have been — and will continue — working in Real Mode. This is pedagogically valuable because you see the "whole machine" without abstraction layers. When you write to video memory, you see the character appear. When you modify the IVT, you change how interrupts behave. There is no operating system hiding the hardware from you. This directness builds deep understanding of how computers actually work at the lowest level.Practicals P11 through P15 (Protected Mode, Linux x86-64): Later in the course, you will transition to writing assembly on Linux, a modern Protected Mode environment. Here, the rules change:
You will use system calls (the syscall instruction on x86-64) instead of DOS interrupts (INT 21h) or direct hardware access.
Your programs will operate in Ring 3 — you will not be able to touch hardware directly.
Memory addresses will be virtual — the addresses your program sees are translated by the MMU before reaching physical RAM.
The instruction set will expand to 64-bit registers (RAX, RBX, ... R8-R15) and new addressing modes.
The transition from Real Mode to Protected Mode mirrors the historical transition that the computing industry itself went through during the 1980s and 1990s — from single-user DOS systems to multitasking, multi-user operating systems. By experiencing both, you gain an appreciation for why Protected Mode exists and what problems it solves.So far, we have been studying how the CPU works — its registers, instructions, and operating modes. But how is the CPU connected to the rest of the computer? How do instructions and data flow between the processor and memory? These questions lead us to two fundamental architectural models that define how computers are organized at the highest level: the Von Neumann architecture and the Harvard architecture.<br>The Von Neumann architecture (also called the Princeton architecture) was described by mathematician <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/John_von_Neumann" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/John_von_Neumann" target="_self">John von Neumann</a> in 1945, in his landmark document <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/First_Draft_of_a_Report_on_the_EDVAC" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/First_Draft_of_a_Report_on_the_EDVAC" target="_self">First Draft of a Report on the EDVAC</a>. Although the ideas were developed collaboratively with J. Presper Eckert and John Mauchly at the University of Pennsylvania's Moore School of Electrical Engineering, von Neumann's name became attached to the design because his report was the first to be widely circulated.The core principle is simple and profound: instructions and data are stored in the same memory and travel over the same bus.Before the Von Neumann architecture, computers like ENIAC were programmed by physically rewiring patch cables and setting switches — a process that could take weeks for a single program. The stored-program concept meant that a program could be loaded into memory just like data, making computers reprogrammable without hardware changes.Components of the Von Neumann architecture:
Central Processing Unit (CPU), consisting of: Arithmetic Logic Unit (ALU) — performs computations (as studied in L02)
Control Unit (CU) — fetches and decodes instructions, coordinates execution
Registers — small, fast storage inside the CPU (as studied in L03) Memory — a single, unified store for both program instructions and data
Input/Output (I/O) — mechanisms for the computer to communicate with the outside world (keyboard, display, disk, network)
System bus — the shared communication pathway connecting CPU, memory, and I/O
The 8086 processor we have been studying follows the Von Neumann architecture. When your TASM program runs, both your instructions (the machine code) and your data (variables in the .data segment) reside in the same physical memory space and are accessed through the same bus system. The processor fetches instructions from one memory location, then fetches or stores data at another memory location, but it does both through the same memory interface.<br><img alt="Von Neumann architecture: CPU with Control Unit and ALU, unified Memory, Input and Output" src="assets/l06-von-neumann-architecture.png" target="_self">Von Neumann architecture: the CPU (containing the Control Unit and ALU) connects to a single unified memory that holds both instructions and data.<br>The Von Neumann bottleneck. Because instructions and data share a single bus, the CPU cannot fetch an instruction and read/write data at the same time. It must take turns: fetch instruction, then access data, then fetch the next instruction, and so on. This fundamental limitation was identified by <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/John_Backus" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/John_Backus" target="_self">John Backus</a> in his 1977 Turing Award lecture, where he called it the "von Neumann bottleneck." As CPU speeds increased far beyond memory speeds over the decades, this bottleneck became increasingly significant — the processor spends more and more time waiting for memory. Cache memory (which we will discuss in the next section) is one of the primary solutions to this problem.Why it matters: The ability to treat instructions as data is not just an implementation detail — it is what makes compilers, assemblers, linkers, and loaders possible. An assembler reads text, produces machine code, and writes that code into a file. A loader reads that file and places the code into memory where the CPU can execute it. Self-modifying code — programs that rewrite their own instructions at runtime — is also possible in a Von Neumann architecture, though modern operating systems typically prohibit it for security reasons.The Harvard architecture takes a different approach: instructions and data have separate memory spaces and separate buses.<br>The name comes from the <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Harvard_Mark_I" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Harvard_Mark_I" target="_self">Harvard Mark I</a> computer, built at Harvard University in the 1940s, which stored instructions on punched tape and data in electromechanical counters — two physically distinct storage systems. (Historically, the term "Harvard architecture" was actually coined decades later in the context of microcontroller design and retrospectively applied to the Harvard machines.)Components of the Harvard architecture:
CPU (ALU + Control Unit + Registers) — same as Von Neumann
Instruction memory — stores program code
Data memory — stores variables and runtime data
Instruction bus — connects CPU to instruction memory
Data bus — connects CPU to data memory
I/O — connected to the data bus
The critical advantage is parallelism: because there are two independent buses, the CPU can fetch the next instruction from instruction memory at the same time as it reads or writes data from data memory. This eliminates the Von Neumann bottleneck for these operations and can significantly improve throughput.<br><img alt="Harvard architecture: separate instruction memory and data memory connected to the control unit" src="assets/l06-harvard-architecture.png" target="_self">Harvard architecture: instruction memory and data memory are physically separate, each with its own bus to the control unit, allowing simultaneous access.Where is Harvard architecture used? You will not typically find a pure Harvard architecture in the general-purpose computer on your desk. Instead, it is common in specialized, performance-critical, or resource-constrained systems:
Microcontrollers — such as the PIC and AVR families (used in Arduino boards, embedded systems, appliances, automotive electronics). These often store program code in Flash memory and data in SRAM, with separate buses for each.
Digital Signal Processors (DSPs) — specialized processors for audio, video, and telecommunications processing. DSPs often have multiple data buses to allow parallel data access for demanding real-time algorithms.
Embedded systems — where the predictable timing behavior of Harvard architecture (no contention between instruction and data fetches) is valuable for real-time requirements.
Here is a key insight: modern CPUs are actually a hybrid of both architectures.Externally, a modern x86 or ARM processor presents a Von Neumann architecture — instructions and data live in the same unified main memory (RAM). Your compiler writes an executable file containing both code and data, and the operating system loads it into a single address space.But internally, the processor uses a Harvard-style design for its cache hierarchy. The L1 cache (the fastest, smallest cache closest to the CPU core) is split into two separate caches:
L1i (instruction cache) — stores recently fetched instructions
L1d (data cache) — stores recently accessed data
These two caches have separate access paths, allowing the CPU to fetch an instruction from L1i and read/write data from L1d simultaneously — just like a Harvard architecture. This eliminates the Von Neumann bottleneck for cached accesses, which account for the vast majority of memory operations.<br>This hybrid design is called a <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Modified_Harvard_architecture" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Modified_Harvard_architecture" target="_self"><strong></strong></a>Modified Harvard architecture, and it gives modern processors the best of both worlds: the programming flexibility of a unified memory model (Von Neumann) with the performance benefits of separate instruction and data paths (Harvard). We will revisit this split-cache architecture in more detail in the next section on the instruction execution cycle.Everything we have discussed so far in this lecture — the CPU, the system bus, memory hierarchy, Von Neumann and Harvard architectures — comes together in one fundamental process: the instruction execution cycle. This is the heartbeat of every computer. From the moment a CPU receives power until the moment it is shut down, it repeats the same basic sequence over and over: fetch an instruction, decode it, execute it, then move on to the next one.Understanding this cycle is essential because it connects the abstract concept of "running a program" to the concrete mechanisms of hardware. When you write MOV AX, 5 in assembly, the CPU does not simply "know" what to do — it must physically retrieve those bytes from memory, interpret their meaning, and carry out the operation using its internal circuits.The instruction execution cycle — also known as the fetch-decode-execute cycle or simply the fetch-execute cycle — consists of three main stages that repeat continuously.Stage 1: FetchThe CPU must first retrieve the next instruction from memory. This stage involves several internal steps:
The Instruction Pointer (IP) holds the address of the next instruction to execute. In real mode, the full address is computed from CS:IP (Code Segment : Instruction Pointer), as we discussed in Lecture 3.
The CPU places this address onto the address bus and sends a read signal on the control bus (as we saw in Section 7, the system bus has three components working together).
The instruction bytes travel back to the CPU over the data bus and are loaded into the Instruction Register (IR) — a special internal register that holds the instruction currently being processed.
The IP is then incremented to point to the next instruction in memory. For the 8086, the increment depends on the length of the current instruction — a 1-byte instruction like NOP advances IP by 1, while a 3-byte instruction like MOV AX, 0005h advances IP by 3.
At this point, the CPU has a copy of the instruction in its Instruction Register and is ready to determine what the instruction means.Stage 2: DecodeDuring the decode stage, the CPU's Control Unit (CU) examines the bits of the fetched instruction to determine three things: What operation to perform — The instruction's opcode (operation code) identifies the type of operation. For example, opcode 0xB8 means "load an immediate 16-bit value into AX." The Control Unit uses internal logic (historically a lookup table or microcode ROM) to translate the opcode into the specific control signals needed to orchestrate the operation. What operands are involved — The remaining bytes of the instruction specify the source and destination of data. This could be a register number, an immediate value embedded in the instruction itself, or a memory address. Whether additional memory access is needed — If an operand is in memory (e.g., MOV AX, [BX]), the CPU must perform an additional memory access to fetch the operand's value before it can execute the instruction. This extra fetch adds clock cycles to the instruction's total execution time. The decode stage is where the CPU transforms raw binary data into actionable internal signals — essentially translating machine language into the electrical impulses that drive the processor's circuits.Stage 3: ExecuteThe CPU now performs the actual operation specified by the instruction:
If the instruction involves arithmetic or logic (ADD, SUB, AND, OR, CMP, etc.), the operands are sent to the ALU (Arithmetic Logic Unit), which computes the result.
The result is written to the destination — either a register or a memory location.
The FLAGS register is updated to reflect the outcome of the operation. For example, if an ADD instruction produces a zero result, the Zero Flag (ZF) is set to 1. If the result overflows, the Overflow Flag (OF) is set.
For control flow instructions like JMP or CALL, the execute stage modifies the Instruction Pointer (and possibly the stack) rather than performing arithmetic.
Once execution completes, the cycle returns to the fetch stage and the process begins again with the next instruction.<br><img alt="Fetch-Decode-Execute cycle diagram" src="assets/l06-fetch-decode-execute.gif" target="_self">The instruction execution cycle: an endless loop of fetching the next instruction, decoding its opcode, and executing the operation.Not all instructions take the same amount of time to execute. The clock provides the fundamental timing reference — each tick of the clock is one clock cycle — but different instructions require different numbers of cycles to complete:The pattern is clear: operations that stay within the CPU (register-to-register) are fastest, while operations that involve memory access are significantly slower. This is a direct consequence of the physical reality we discussed in Section 9 — accessing data from DRAM takes orders of magnitude longer than accessing data from a register.On modern processors running at multi-gigahertz speeds, the disparity becomes even more dramatic. A 3.4 GHz CPU completes one clock cycle in approximately 0.29 nanoseconds. A simple register-to-register instruction might complete in 1 cycle — but a cache miss that forces a DRAM access can stall the CPU for 100-200 cycles. During those 200 cycles, the CPU could theoretically have executed hundreds of simple instructions. This "lost time" is at the heart of the Von Neumann bottleneck, which we will examine in the next section.<br>For a visual demonstration of how the fetch-decode-execute cycle works at the hardware level, see <a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=Z5JC9Ve1sfI" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=Z5JC9Ve1sfI" target="_self">The Fetch-Execute Cycle: What's Your Computer Actually Doing?</a> by Tom Scott. This video provides an excellent animated walkthrough of instructions flowing through the CPU's internal components.It is worth noting that in simpler, older CPUs like the 8086, the instruction cycle was executed sequentially — the CPU completed one instruction entirely before starting the next. Modern CPUs use techniques like pipelining to overlap the stages of different instructions, so while one instruction is being executed, the next is being decoded, and a third is being fetched — all simultaneously. We will introduce pipelining briefly in the next section and return to it in greater detail in later lectures.We have now seen how the Von Neumann architecture stores both instructions and data in the same memory (Section 11), how the system bus connects the CPU to that memory (Section 7), how the memory hierarchy provides layers of progressively slower storage (Section 9), and how the CPU fetches and executes instructions in a continuous cycle (Section 12). All of these concepts converge in one of the most important problems in computer architecture: the Von Neumann bottleneck.Recall from Section 11 that in the Von Neumann architecture, instructions and data share the same memory and travel over the same bus. This design has an elegant simplicity — a program can treat its own instructions as data, enabling powerful capabilities like compilers, interpreters, and self-modifying code. But it also creates a fundamental limitation.The CPU can only use the bus for one thing at a time. It cannot simultaneously fetch the next instruction and read a data operand from memory — both requests must go through the same bus. The bus becomes a narrow pipe through which all information must flow.This would not be a serious problem if the CPU and memory operated at similar speeds. But they do not. Over the past several decades, CPU clock speeds have increased far more rapidly than memory access speeds. This divergence is sometimes called the processor-memory performance gap:
In the early 1980s, a CPU cycle and a memory access took roughly the same amount of time.
By the 2000s, CPU speeds had increased by a factor of roughly 1000x, while DRAM speeds had improved by only about 10x.
Today, a modern CPU can execute several instructions per nanosecond, but a DRAM access still takes 50-100 nanoseconds.
The result: the CPU spends a significant fraction of its time simply waiting for data to arrive from memory. The shared bus — the defining feature of the Von Neumann architecture — becomes the bottleneck that limits overall system performance.<br>The term "Von Neumann bottleneck" was popularized by <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/John_Backus" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/John_Backus" target="_self">John Backus</a> (the creator of FORTRAN) in his 1977 ACM Turing Award lecture, where he described it as follows:
"Surely there must be a less primitive way of making big changes in the store than by pushing vast numbers of words back and forth through the von Neumann bottleneck."
Let us put concrete numbers to this problem. The following table shows approximate access times for each level of the memory hierarchy on a modern system:Consider what these numbers mean in practice. At 3.4 GHz, one clock cycle takes about 0.29 nanoseconds. A simple register-to-register operation completes in 1 cycle. But when the CPU needs data that is only in DRAM, it must wait approximately 200 clock cycles for that data to arrive. During those 200 cycles, the CPU could have executed roughly 400 simple instructions — but instead, it sits idle, waiting.And if the data has been swapped to an SSD or (worse) a hard drive, the CPU could execute millions of instructions in the time it takes for a single access. This is why running out of RAM and relying on swap space causes such dramatic slowdowns — the processor-memory gap grows from a factor of 200 to a factor of millions.The Von Neumann bottleneck is not merely a theoretical concern. It is the dominant performance limitation in most computing systems. Almost every major architectural innovation in modern CPUs exists, at least in part, to mitigate this bottleneck.Engineers have developed several strategies to reduce the impact of the Von Neumann bottleneck. These strategies do not eliminate the fundamental problem — they work around it by trying to ensure that the data the CPU needs is already close at hand when it is needed.1. Cache MemoryThe single most important mitigation is the memory cache — small, fast memory located on or very near the CPU die that stores copies of frequently accessed data and instructions. As we discussed in Section 9, modern CPUs have multiple levels of cache (L1, L2, L3), each larger but slower than the previous level.Caching works because real programs exhibit two key patterns of memory access: Temporal locality: If a piece of data was accessed recently, it is likely to be accessed again soon. Think of a loop variable that is read and updated on every iteration — once it is loaded into cache, subsequent accesses are fast. Spatial locality: If a piece of data was accessed, nearby data is likely to be accessed soon. When the CPU loads data into cache, it loads an entire cache line (typically 64 bytes), bringing neighboring data along for free. This is why iterating through an array sequentially is much faster than accessing random memory locations. When the CPU finds the data it needs in cache (a cache hit), it avoids the slow trip to DRAM entirely. Modern programs achieve cache hit rates of 95-99%, meaning that only 1-5% of memory accesses actually need to go to DRAM. This transforms the effective average memory access time from ~100 cycles to just a few cycles.2. Split L1 Cache (Modified Harvard Architecture)Here is one of the most elegant solutions to the bottleneck: while the computer maintains a Von Neumann architecture at the system level (instructions and data share the same main memory and bus), the CPU internally uses a Harvard-like architecture at the L1 cache level.The L1 cache is split into two separate caches:
L1i (instruction cache) — holds recently fetched instructions
L1d (data cache) — holds recently accessed data
Because these are separate physical caches with separate internal buses, the CPU can fetch the next instruction from L1i while simultaneously reading data from L1d. This parallelism directly addresses the Von Neumann bottleneck — the CPU no longer has to choose between fetching instructions and accessing data, at least for data that is in L1 cache.This design is sometimes called a Modified Harvard architecture or a split-cache architecture. It gives us the best of both worlds: the programming flexibility of Von Neumann (instructions and data in the same address space) with the performance advantage of Harvard (separate pathways for instructions and data at the cache level).3. PrefetchingRather than waiting for the CPU to request data and then fetching it (which guarantees a delay on every cache miss), modern CPUs proactively prefetch data they predict will be needed soon.The CPU's hardware prefetcher monitors memory access patterns and detects regularities:
Sequential access: If the CPU is reading addresses 1000, 1004, 1008, ..., the prefetcher predicts that 1012, 1016, etc. will be needed next and begins loading them into cache before the CPU asks.
Strided access: If the CPU is reading addresses 1000, 1100, 1200, ..., the prefetcher detects the stride of 100 and prefetches accordingly.
Software prefetch instructions: Modern ISAs (including x86) provide explicit prefetch instructions that allow programmers or compilers to hint to the CPU about future memory needs.
When prefetching works well, the data arrives in cache just before the CPU needs it, effectively hiding the memory latency entirely.4. Other Techniques (Preview)Several additional architectural techniques help mitigate the Von Neumann bottleneck. We mention them here briefly for awareness — they will be covered in more detail in later lectures: Pipelining: Instead of completing one instruction before starting the next, the CPU overlaps the fetch, decode, and execute stages of different instructions. While instruction N is being executed, instruction N+1 is being decoded, and instruction N+2 is being fetched. This increases throughput without increasing the clock speed. A classic analogy is a laundry pipeline: you do not wait for the first load to be washed, dried, and folded before starting the second load in the washer. Branch prediction: Conditional jumps (like JZ or JNE) create a problem for the pipeline — the CPU does not know which instruction to fetch next until the condition is evaluated. Modern CPUs use sophisticated branch predictors that guess the outcome of conditional jumps based on past behavior. When the prediction is correct (which happens 95-99% of the time on modern hardware), the pipeline continues at full speed. When wrong, the pipeline must be flushed and restarted — a costly penalty called a branch misprediction. Out-of-order execution: If instruction N is waiting for data from memory, the CPU does not simply stall — it looks ahead in the instruction stream and executes later instructions that are ready to go. The results are then committed in the original program order, so the program behaves as if instructions executed sequentially. This technique keeps the CPU's execution units busy even when individual instructions are stalled on memory. Superscalar execution: Modern CPUs contain multiple execution units (multiple ALUs, multiple load/store units) and can execute several instructions simultaneously in a single clock cycle, provided the instructions do not depend on each other's results. The Von Neumann bottleneck is the fundamental tension in computer architecture. The elegant simplicity of storing instructions and data in the same memory comes at the cost of a shared, bandwidth-limited pathway between the CPU and memory. As CPUs have grown faster while memory has not kept pace, this bottleneck has become increasingly severe.Almost every major architectural innovation in modern processors — cache hierarchies, split L1 caches, prefetching, pipelining, branch prediction, out-of-order execution — exists, at least in part, to mitigate this bottleneck. Understanding the Von Neumann bottleneck is therefore not just an academic exercise — it is the key to understanding why modern CPUs are designed the way they are.When you look at a modern processor die and see that over half its transistors are devoted to cache memory, you are seeing the Von Neumann bottleneck made visible in silicon. When you learn that a programmer optimized code by improving data locality and saw a 10x speedup without changing the algorithm, you are seeing the Von Neumann bottleneck at work in software. It is the thread that connects hardware design to software performance, and it will remain a central theme throughout this course. Bit shift instructions (SHL, SHR, SAL, SAR, ROL, ROR) provide efficient multiplication and division by powers of 2, and are fundamental to low-level data manipulation. Moore's Law — the empirical observation that transistor count doubles approximately every 2 years — has held for 60+ years and drove the evolution from simple ICs to modern processors with billions of transistors. The progression from ICs to microprocessors (integrated circuits → microcontrollers → microprocessors) represents increasing levels of integration, culminating in complete computing systems on a single chip (SoC). The system bus (data, address, and control buses) is the communication backbone connecting the CPU to memory and I/O devices, synchronized by the system clock. Memory hierarchy (registers → cache → DRAM → disk) is the engineering solution to the speed-cost tradeoff: faster memory is more expensive and smaller, while larger storage is slower and cheaper. Real Mode vs Protected Mode — Real Mode (8086) gives programs unrestricted access to all 1 MB of memory; Protected Mode (80386+) provides 4 GB addressing, virtual memory, and privilege-based isolation. Von Neumann architecture uses a single shared memory for data and instructions, while Harvard architecture uses separate memories. Modern CPUs are hybrids: Von Neumann externally, Harvard internally (split L1 cache). The Von Neumann bottleneck — the shared bus between CPU and memory — is the fundamental performance limitation. Cache, prefetch, split L1, and pipelining are the main countermeasures. Lab 6: Arrays and Addressing ModesIn this lab you will practice:
Using different addressing modes: [BX], [SI], [BX+SI], [BX+SI+disp]
Declaring and manipulating arrays with DB, DW, DD
Observing memory layout using Turbo Debugger's Dump window
Applying bit shift instructions from this lecture to array processing tasks <br><a data-tooltip-position="top" aria-label="https://xlogicx.net/Assembly_Is_Too_High-Level_-_Better_Call_SAL.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://xlogicx.net/Assembly_Is_Too_High-Level_-_Better_Call_SAL.html" target="_self">Assembly Is Too High-Level — Better Call SAL</a> — The hidden story of the SAL instruction's undocumented opcode
<br><a data-tooltip-position="top" aria-label="http://pnx.tf/files/x86_opcode_structure_and_instruction_overview.pdf" rel="noopener nofollow" class="external-link is-unresolved" href="http://pnx.tf/files/x86_opcode_structure_and_instruction_overview.pdf" target="_self">x86 Opcode Structure and Instruction Overview</a> — Visual guide to 8086 opcode byte layout
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/X86_instruction_listings#Original_8086/8088_instructions" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/X86_instruction_listings#Original_8086/8088_instructions" target="_self">Original 8086/8088 Instructions — Wikipedia</a> — Complete reference table of the original x86 instruction set
<br><a data-tooltip-position="top" aria-label="https://ourworldindata.org/moores-law" rel="noopener nofollow" class="external-link is-unresolved" href="https://ourworldindata.org/moores-law" target="_self">What is Moore's Law? — Our World in Data</a> — Interactive visualization of transistor count growth
<br><a data-tooltip-position="top" aria-label="https://www.apple.com/newsroom/2025/03/apple-reveals-m3-ultra-taking-apple-silicon-to-a-new-extreme/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.apple.com/newsroom/2025/03/apple-reveals-m3-ultra-taking-apple-silicon-to-a-new-extreme/" target="_self">Apple M3 Ultra — Apple Newsroom</a> — 184 billion transistors, modern Moore's Law example
<br><a data-tooltip-position="top" aria-label="https://uk.wikipedia.org/wiki/%D0%9C%D1%96%D0%BA%D1%80%D0%BE%D1%81%D1%85%D0%B5%D0%BC%D0%B0" rel="noopener nofollow" class="external-link is-unresolved" href="https://uk.wikipedia.org/wiki/%D0%9C%D1%96%D0%BA%D1%80%D0%BE%D1%81%D1%85%D0%B5%D0%BC%D0%B0" target="_self">Мікросхема — Вікіпедія</a> — Integrated circuits overview (Ukrainian)
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/7400-series_integrated_circuits" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/7400-series_integrated_circuits" target="_self">7400-series Integrated Circuits — Wikipedia</a> — The IC series that became the industry standard
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Microcontroller" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Microcontroller" target="_self">Microcontroller — Wikipedia</a> — Complete computers on a single chip
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Texas_Instruments_TMS1000" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Texas_Instruments_TMS1000" target="_self">Texas Instruments TMS1000 — Wikipedia</a> — The first commercial microcontroller
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Microprocessor" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Microprocessor" target="_self">Microprocessor — Wikipedia</a> — From discrete logic to integrated processing
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Motherboard" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Motherboard" target="_self">Motherboard — Wikipedia</a> — System backbone design and components
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/System_bus" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/System_bus" target="_self">System Bus — Wikipedia</a> — Data, address, and control bus architecture
<br><a data-tooltip-position="top" aria-label="https://people.cs.pitt.edu/~melhem/courses/1502p/Unit5/Unit5.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://people.cs.pitt.edu/~melhem/courses/1502p/Unit5/Unit5.html" target="_self">Modelling the Memory — University of Pittsburgh</a> — Memory access modeling and timing
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Cache_(computing)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Cache_(computing)" target="_self">Cache (computing) — Wikipedia</a> — Cache operation, motivation, and hierarchy
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Harvard_architecture" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Harvard_architecture" target="_self">Harvard Architecture — Wikipedia</a> — Separate instruction and data memory design
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Von_Neumann_architecture" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Von_Neumann_architecture" target="_self">Von Neumann Architecture — Wikipedia</a> — Unified memory architecture
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Instruction_cycle" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Instruction_cycle" target="_self">Instruction Cycle — Wikipedia</a> — The fetch-decode-execute loop
<br><a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=Z5JC9Ve1sfI" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=Z5JC9Ve1sfI" target="_self">The Fetch-Execute Cycle — YouTube</a> — Visual explanation of instruction execution
]]></description><link>lectures/eng/l06-eng.-system-architecture.html</link><guid isPermaLink="false">Lectures/ENG/L06-ENG. System Architecture.md</guid><pubDate>Tue, 10 Mar 2026 06:00:29 GMT</pubDate><enclosure url="assets/l06-shl-shift-left.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l06-shl-shift-left.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L09-Flynn-MIMD]]></title><description><![CDATA[<img src="assets/l09-flynn-mimd.png" target="_self">]]></description><link>assets/l09-flynn-mimd.html</link><guid isPermaLink="false">assets/L09-Flynn-MIMD.png</guid><pubDate>Mon, 09 Mar 2026 19:20:39 GMT</pubDate><enclosure url="assets/l09-flynn-mimd.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l09-flynn-mimd.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L09-Flynn-SIMD]]></title><description><![CDATA[<img src="assets/l09-flynn-simd.png" target="_self">]]></description><link>assets/l09-flynn-simd.html</link><guid isPermaLink="false">assets/L09-Flynn-SIMD.png</guid><pubDate>Mon, 09 Mar 2026 19:20:39 GMT</pubDate><enclosure url="assets/l09-flynn-simd.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l09-flynn-simd.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L09-Flynn-MISD]]></title><description><![CDATA[<img src="assets/l09-flynn-misd.png" target="_self">]]></description><link>assets/l09-flynn-misd.html</link><guid isPermaLink="false">assets/L09-Flynn-MISD.png</guid><pubDate>Mon, 09 Mar 2026 19:20:39 GMT</pubDate><enclosure url="assets/l09-flynn-misd.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l09-flynn-misd.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L09-Flynn-SISD]]></title><description><![CDATA[<img src="assets/l09-flynn-sisd.png" target="_self">]]></description><link>assets/l09-flynn-sisd.html</link><guid isPermaLink="false">assets/L09-Flynn-SISD.png</guid><pubDate>Mon, 09 Mar 2026 19:20:39 GMT</pubDate><enclosure url="assets/l09-flynn-sisd.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l09-flynn-sisd.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L09-Flynn-Taxonomy-Table]]></title><description><![CDATA[<img src="assets/l09-flynn-taxonomy-table.png" target="_self">]]></description><link>assets/l09-flynn-taxonomy-table.html</link><guid isPermaLink="false">assets/L09-Flynn-Taxonomy-Table.png</guid><pubDate>Mon, 09 Mar 2026 19:20:39 GMT</pubDate><enclosure url="assets/l09-flynn-taxonomy-table.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l09-flynn-taxonomy-table.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L09-ISA-Register-Memory-And-Register-Register]]></title><description><![CDATA[<img src="assets/l09-isa-register-memory-and-register-register.png" target="_self">]]></description><link>assets/l09-isa-register-memory-and-register-register.html</link><guid isPermaLink="false">assets/L09-ISA-Register-Memory-And-Register-Register.png</guid><pubDate>Mon, 09 Mar 2026 19:20:39 GMT</pubDate><enclosure url="assets/l09-isa-register-memory-and-register-register.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l09-isa-register-memory-and-register-register.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L09-ISA-Accumulator-Based]]></title><description><![CDATA[<img src="assets/l09-isa-accumulator-based.png" target="_self">]]></description><link>assets/l09-isa-accumulator-based.html</link><guid isPermaLink="false">assets/L09-ISA-Accumulator-Based.png</guid><pubDate>Mon, 09 Mar 2026 19:20:39 GMT</pubDate><enclosure url="assets/l09-isa-accumulator-based.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l09-isa-accumulator-based.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L09-ISA-Stack-Based]]></title><description><![CDATA[<img src="assets/l09-isa-stack-based.png" target="_self">]]></description><link>assets/l09-isa-stack-based.html</link><guid isPermaLink="false">assets/L09-ISA-Stack-Based.png</guid><pubDate>Mon, 09 Mar 2026 19:20:39 GMT</pubDate><enclosure url="assets/l09-isa-stack-based.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l09-isa-stack-based.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L09-RASP-Machine-Architecture]]></title><description><![CDATA[<img src="assets/l09-rasp-machine-architecture.png" target="_self">]]></description><link>assets/l09-rasp-machine-architecture.html</link><guid isPermaLink="false">assets/L09-RASP-Machine-Architecture.png</guid><pubDate>Mon, 09 Mar 2026 19:20:39 GMT</pubDate><enclosure url="assets/l09-rasp-machine-architecture.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l09-rasp-machine-architecture.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L09-RAM-Machine-Architecture]]></title><description><![CDATA[<img src="assets/l09-ram-machine-architecture.png" target="_self">]]></description><link>assets/l09-ram-machine-architecture.html</link><guid isPermaLink="false">assets/L09-RAM-Machine-Architecture.png</guid><pubDate>Mon, 09 Mar 2026 19:20:39 GMT</pubDate><enclosure url="assets/l09-ram-machine-architecture.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l09-ram-machine-architecture.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Консультація - 2026-03-07]]></title><description><![CDATA[Дата: 7 березня 2026 р. (субота)На консультації було розглянуто три питання: налаштування середовища DosBox для роботи одночасно з лабораторними та проєктом, використання процедур в асемблері без параметрів, а також механізм «магічного заголовка» в проєкті «Менеджер паролів».Контекст. На початку курсу було налаштовано конфігураційний файл DosBox (startdosbox.sh) із прив'язкою до одного репозиторію (лабораторні роботи). З появою окремого репозиторію для проєкту виникло питання: як зручно перемикатися між ними?Рекомендовані підходи (від простішого до гнучкішого): Додатковий mount у конфігу. У конфігураційному файлі DosBox уже є рядок mount d &lt;шлях_до_лабораторних&gt;. Достатньо додати ще один рядок, наприклад mount i &lt;шлях_до_проєкту&gt;. Після запуску DosBox диск D: відповідатиме лабораторним, а диск I: — проєкту. Перемикання — просто введення I: у командному рядку DosBox. Окремий конфігураційний файл. Можна скопіювати існуючий конфіг і змінити в ньому шлях на каталог проєкту. Тоді запуск з різними конфігами відкриватиме DosBox з потрібним робочим каталогом. Спільна батьківська папка. Обидва репозиторії розміщуються в одній папці, яка монтується в DosBox. Навігація між ними — через команди cd. Висновок. Найзручніший варіант — додати один рядок mount у наявний конфіг. Це дає доступ і до лабораторних, і до проєкту в межах одного сеансу DosBox без потреби перезапуску.Контекст. На лекціях процедури розглядалися у зв'язку зі стековим механізмом передачі параметрів (push параметрів перед call, пролог push bp / mov bp, sp). Виникло питання: чи обов'язково виконувати ці дії, якщо процедура не приймає параметрів?Відповідь: Процедура — це, по суті, стрибок (call) з поверненням (ret). Якщо параметри не передаються і локальні змінні не потрібні, пролог/епілог із push bp / mov bp, sp не є обов'язковими. Конструкція зі збереженням BP та встановленням стекового фрейму — це конвенція (домовленість, calling convention). Її мета — уніфікувати спосіб доступу до параметрів і локальних змінних через регістр BP. Завдяки цій домовленості будь-хто, хто читає код, одразу розуміє, як функція працює і які регістри змінює. Практична порада: якщо ви вирішили не використовувати стековий фрейм — не використовуйте його ніде в програмі. Якщо вирішили використовувати — робіть це послідовно у всіх процедурах. Змішування підходів ускладнює розуміння коду. Контекст. Один із варіантів проєкту (жовтий №20) — «Менеджер паролів». У його специфікації згадується «магічний заголовок». Виникло питання щодо його призначення.Де шукати деталі. Для кожного проєкту є окрема сторінка з описом ігрової логіки. Саме там детально розписано алгоритм XOR-шифрування з циклічним ключем та механізм магічного заголовка.Суть механізму:
Перед шифруванням на початок даних додається фіксований 4-байтовий маркер — наприклад, PWM + 0x1A.
Коли дані потрібно розшифрувати, користувач вводить пароль. Програма виконує XOR-дешифрування і перевіряє перші 4 байти результату.
Якщо перші 4 байти після дешифрування збігаються з очікуваним маркером (PWM1A) — пароль правильний. Маркер відкидається, далі працюємо з розшифрованими даними.
Якщо перші 4 байти — «сміття» (не збігаються з маркером) — пароль неправильний. Програма одразу повідомляє про помилку, подальша обробка не потрібна.
Ключова перевага: пароль ніде не зберігається ні у відкритому, ні у хешованому вигляді. Верифікація відбувається виключно через спробу дешифрування та перевірку магічного заголовка.Алгоритм роботи (коротко):
Шифрування: дані → додати PWM1A на початок → XOR з паролем-ключем → зберегти зашифроване.
Дешифрування: зашифроване → XOR з введеним паролем → перевірити перші 4 байти → якщо PWM1A — пароль вірний, відкинути маркер, показати дані; інакше — повідомити про помилку.
Примітка: буде підготовлено додаткове відео з інструкцією щодо налаштування DosBox для роботи з проєктом.]]></description><link>office-hours/консультація-2026-03-07.html</link><guid isPermaLink="false">Office-hours/Консультація - 2026-03-07.md</guid><pubDate>Sun, 08 Mar 2026 15:44:09 GMT</pubDate></item><item><title><![CDATA[Project]]></title><description><![CDATA[
Курс: Архітектура обчислювальних систем
Оголошення: Тиждень 7 (лекція/практика 8)
Платформа: TASM / DOSBox / Turbo Debugger
Вага в оцінці: 20% (тільки для повного курсу — 6 ECTS)
Протягом курсу ви крок за кроком опановували окремі елементи: регістри, арифметику, стек, процедури, переривання, відеопам'ять. Підсумковий проєкт — це можливість зібрати ці знання воєдино та створити працюючу програму, яка демонструє ваше розуміння архітектури x86.Проєкт перевіряє вашу здатність:
самостійно спроєктувати та реалізувати програму на асемблері (LO3);
обирати між різними підходами та обґрунтовувати рішення (LO7);
пояснити, як ваша програма взаємодіє з апаратним забезпеченням (LO4).
Проєкт наполегливо рекомендується виконувати в парі. Парна робота дозволяє:
реалізувати більш амбітний проєкт;
отримати досвід командної розробки на рівні системного програмування;
розвинути навички розподілу задач та інтеграції коду.
Очікуваний обсяг — приблизно 60 годин самостійної роботи на кожного учасника.Бонус за рівний внесок. Якщо пара демонструє збалансований внесок обох учасників (підтверджений історією комітів, індивідуальними відео та перехресними питаннями на захисті), обидва учасники отримують бонус +10% до оцінки за проєкт.Якщо ви обираєте індивідуальну роботу — це допускається, але вимоги до обсягу та якості проєкту залишаються незмінними.Ви обираєте одну тему з <a class="internal-link" data-href="../Projects/README.md" href="projects/readme.html" target="_self" rel="noopener nofollow"><strong></strong></a>меню проєктів.Кожна тема в меню містить:
короткий опис задачі та очікуваної функціональності;
перелік архітектурних концепцій, які тема задіює;
мінімальні вимоги та можливі розширення.
Теми мають позначку складності:
🟢 Зелений — менший обсяг, чіткіша специфікація. Підходить, якщо асемблер все ще здається складним.
🟡 Жовтий — стандартна складність. Потребує впевненого володіння всіма концепціями курсу.
🔴 Червоний — амбіційний. Складні структури даних, обмеження реального часу. Парна робота наполегливо рекомендується.
Максимум 3 команди на тему — хто перший, того й місце.<br>Крок 1. Створіть репозиторій проєкту через <a data-tooltip-position="top" aria-label="https://classroom.github.com/a/LyoYCuiA" rel="noopener nofollow" class="external-link is-unresolved" href="https://classroom.github.com/a/LyoYCuiA" target="_self">GitHub Classroom assignment</a>. Це потрібно зробити і парам, і тим, хто працює індивідуально:
Пара: перший учасник створює команду та дає їй назву, другий — приєднується до існуючої команди.
Індивідуально: створіть команду з одного учасника.
GitHub Classroom створить репозиторій project-&lt;назва-команди&gt; в організації курсу.<br>Крок 2. Оберіть тему в <a data-tooltip-position="top" aria-label="https://github.com/ukma-fin-csa-2026/projects/issues" rel="noopener nofollow" class="external-link is-unresolved" href="https://github.com/ukma-fin-csa-2026/projects/issues" target="_self">репозиторії проєктів</a> (доступний з 18:00, 28 лютого). Кожна тема — окремий Issue. Залиште коментар у Issue обраної теми із посиланням на ваш репозиторій:Команда: &lt;назва-команди&gt;
Репозиторій: &lt;посилання на project-...&gt;
Якщо в Issue вже є 3 зареєстровані команди — оберіть іншу тему.
==Дедлайн реєстрації: кінець тижня 8 (КТ-1). Після цього зареєструватися на проєкт буде неможливо! ==
Проєкт розрахований на 8 тижнів.Щотижня ви фіксуєте прогрес окремим Loom-відео (до 3 хвилин). Це відео є окремим від відео до лабораторних робіт. Контрольні точки є обов'язковою складовою оцінювання (див. розділ 6).Відео контрольної точки має показувати:
що зроблено за тиждень (демонстрація екрану);
з якими труднощами зіткнулися;
що плануєте на наступний тиждень.
Парні проєкти: достатньо одного спільного відео на тиждень — записує один з учасників. Однак для отримання бонусу за рівний внесок (див. розділ 6) загальна кількість відео має бути розподілена приблизно порівну між обома учасниками протягом 7 тижнів. Альтернативно, пара може записувати два відео щотижня (по одному від кожного учасника) — це не впливає на оцінку.Оцінювання: бінарне — відео подано вчасно та містить змістовний контент = повний бал за цю контрольну точку. Відсутнє або порожнє відео = 0 балів. Усього 7 контрольних точок, кожна оцінюється окремо.
Чому це важливо? Проєкт на асемблері — амбітна задача. Регулярна робота протягом 8 тижнів набагато ефективніша за марафон в останні дні. Контрольні точки винагороджують послідовну працю, навіть якщо фінальний результат не повністю завершений.
Обсяг — орієнтир, не жорстка вимога. Якісна програма у 300 рядків краща за роздуту у 700.Вимоги до коду:
Програма компілюється TASM та працює в DOSBox без модифікацій.
Код структурований: використання процедур, зрозумілі імена міток та змінних.
Коментарі пояснюють чому (логіку рішень), а не що (очевидні дії).
Стек збалансований: кожен PUSH має відповідний POP, процедури коректно зберігають та відновлюють регістри. Обсяг: 3–5 сторінок
Формат: Markdown (README.md у репозиторії)
Звіт має містити:
Опис програми — що вона робить, як користувач з нею взаємодіє.
Використані концепції — які архітектурні механізми з курсу ви застосували (регістри, стек, переривання, відеопам'ять тощо) та як саме.
Схема організації пам'яті — діаграма розміщення даних, коду та стеку у пам'яті вашої програми. Де розташовані ключові структури даних.
Аналіз «підхід A проти підходу B» — опишіть конкретний експеримент або порівняння, яке ви провели. Наприклад: виведення через INT 10h проти прямого запису у відеопам'ять — що швидше і чому. Або: зберігання ігрового поля як масив байтів проти масиву слів — які компроміси.
Ретроспектива — що б ви зробили інакше, якби починали проєкт заново.
Для парних проєктів: розподіл роботи — хто що робив, як інтегрували код. Фінальне відео: до 5 хвилин на кожного учасника
Платформа: Loom
Фінальне відео — це не те саме, що тижневі контрольні точки. Це підсумкова презентація:
Демонстрація працюючої програми.
Огляд ключових фрагментів коду: покажіть 2–3 місця, якими ви пишаєтесь або які були найскладнішими.
Коротке пояснення головного архітектурного рішення.
Кожен учасник записує власне відео, де пояснює свою частину роботи.На практиці 15 (тиждень 15) відбувається живий захист:
Коротка демонстрація (~3 хв).
Запитання від інструктора (~5–7 хв). Питання адресуються кожному учаснику пари окремо, у тому числі про частини, які писав партнер.
Як оцінюється «Код: реалізація» (50%):
25% — поточне оцінювання протягом проєкту. Інструктор переглядає стан коду на кожній контрольній точці та оцінює прогрес, якість нового коду та відповідність плану. Ця частина накопичується лише за вчасної здачі щотижня. Коміти мають відповідати вимогам розділу 7.4 (регулярні, гранулярні, у форматі Conventional Commits).
25% — фінальне оцінювання на момент захисту. Програма працює, функціональність відповідає вимогам теми, код структурований та добре прокоментований.
Бонус за рівний внесок (парні проєкти): +10% до оцінки за проєкт, якщо виконуються всі умови:
Історія комітів показує регулярні внески обох учасників.
Індивідуальні відео демонструють глибоке розуміння всього проєкту, а не лише «своєї» частини.
На захисті обидва учасники впевнено відповідають на запитання про будь-яку частину коду.
Бонус за розширення (усі проєкти): до +20% до оцінки за проєкт. Кожна тема в меню містить перелік запропонованих розширень. Якщо ви реалізували більшість із них — отримуєте повний бонус. Часткова реалізація розширень оцінюється пропорційно. Розширення оцінюються лише за умови, що базова функціональність проєкту працює.Для проєкту створюється окремий репозиторій project-&lt;назва-команди&gt; в організації курсу через GitHub Classroom (див. розділ 3, «Реєстрація на тему»). Це стосується і парних, і індивідуальних проєктів.project-&lt;назва-команди&gt;/
├── README.md # Інформація про команду → потім звіт
├── checkpoints.md # Тижневі контрольні точки (посилання на Loom)
├── src/
│ ├── main.asm # Основний файл (назви можуть відрізнятися)
│ └── ... # Додаткові .asm файли за потреби
├── video_presentation.txt # Посилання на фінальне Loom-відео (solo)
├── video_student1.txt # (парні) Відео першого учасника
└── video_student2.txt # (парні) Відео другого учасника
Одразу після створення репозиторію (КТ-1) заповніть README.md інформацією про команду. Це ваш перший коміт. Формат:# Назва проєкту Тема: [G01](https://github.com/ukma-fin-csa-2026/projects/issues/XX) ## Команда - Прізвище Ім'я — email@ukma.edu.ua
- Прізвище Ім'я — email@ukma.edu.ua
Обов'язково:
Email має бути корпоративним (@ukma.edu.ua) — за ним ми ідентифікуємо автора.
Код теми має відповідати Issue, в якому ви зареєструвалися.
<br>README має містити посилання на опис вашої теми (Issue в <a data-tooltip-position="top" aria-label="https://github.com/ukma-fin-csa-2026/projects/issues" rel="noopener nofollow" class="external-link is-unresolved" href="https://github.com/ukma-fin-csa-2026/projects/issues" target="_self">репозиторії проєктів</a>). Наприклад: Тема: [Y07](https://github.com/ukma-fin-csa-2026/projects/issues/15).
Згодом README.md стає звітом проєкту (див. розділ 5.2). До фінальної здачі він має містити всі розділи звіту: опис програми, використані концепції, схему пам'яті, аналіз «A проти B» та ретроспективу.<br><img alt="Git commit messages over time" src="assets/git-commit-message-meme.png" target="_self" style="width: 450px; max-width: 100%;">Проєкт — це нагода практикувати індустріальну культуру роботи з Git. Коміти мають бути регулярними, гранулярними та зрозумілими.Гранулярність. Один коміт = одна логічна зміна. Не накопичуйте роботу за кілька днів в одному гігантському коміті. І навпаки, не дробіть до абсурду — зміна одного символу не варта окремого коміту.Приклади правильної гранулярності:
feat: add keyboard input handler using INT 16h — одна нова функціональність
fix: correct stack imbalance in draw_border procedure — одне виправлення
refactor: extract score display into separate procedure — одне структурне покращення
docs: add memory layout diagram to report — одне оновлення документації
<br>Повідомлення комітів. Дотримуйтесь формату <a data-tooltip-position="top" aria-label="https://www.conventionalcommits.org/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.conventionalcommits.org/" target="_self">Conventional Commits</a> та класичних правил оформлення (див. <a data-tooltip-position="top" aria-label="https://cbea.ms/git-commit/" rel="noopener nofollow" class="external-link is-unresolved" href="https://cbea.ms/git-commit/" target="_self">How to Write a Git Commit Message</a>):&lt;тип&gt;: &lt;короткий опис&gt; &lt;тіло — за потреби&gt;
Правила оформлення:
Тема (перший рядок) — до 50 символів (включно з префіксом). Це змушує формулювати суть стисло.
Наказовий спосіб (imperative mood) в описі: «add», «fix», «remove» — не «added», «fixed», «removed». Тест: «If applied, this commit will [your description]».
Без крапки в кінці теми — економте місце.
Тіло відокремлюється порожнім рядком. Для простих комітів тіло не обов'язкове. Для складніших — поясніть що і чому, а не як (код показує «як» сам).
Тіло — рядки до 72 символів. Git не переносить рядки автоматично.
Основні типи:Мова комітів: англійська (це індустріальний стандарт).GitHub Issues (рекомендовано). Для планування та відстеження роботи рекомендуємо створювати Issues у вашому репозиторії. Це особливо корисно для парних проєктів — Issues стають спільним бек-логом задач.Приклади Issues:
Implement game loop with INT 1Ch timer
Add color attributes to score display
Bug: snake doesn't grow after eating food
Посилайтесь на Issues у комітах: feat: implement game loop (#3) — GitHub автоматично зв'яжє коміт з Issue.
Чому це важливо? Історія комітів — це основний доказ вашої роботи над проєктом. Один коміт «add project» за день до дедлайну не демонструє ані процес, ані розуміння. Для парних проєктів це також основне джерело для оцінки внеску кожного учасника.
Посилання на тижневі Loom-відео контрольних точок додавайте у файл checkpoints.md:## КТ-1 (Тиждень 8)
- Loom: [посилання]
- Записав(ла): Ім'я
- Що зроблено: обрано тему, написано план ## КТ-2 (Тиждень 9)
- Loom: [посилання]
- Записав(ла): Ім'я
- Що зроблено: створено скелет програми, працює базове виведення
Для парних проєктів вказуйте, хто записував відео. Розподіліть відео приблизно порівну між учасниками (наприклад, 4 + 3 або 3 + 4). Якщо пара обирає записувати два відео на тиждень — додайте обидва посилання.Початок практики 15 (тиждень 15). Код та звіт мають бути закомічені до початку заняття.Дедлайн є дедлайн. Пізні подання не приймаються (див. загальну політику курсу у Syllabus, розділ 5.1).На проєкт поширюються ті ж вимоги академічної доброчесності, що й на всі інші роботи курсу.Дозволено:
Використовувати матеріали курсу (лекції, лабораторні, приклади коду з занять).
Консультуватися з документацією (довідники TASM, переривань BIOS/DOS).
Обговорювати загальні підходи з іншими студентами («Як краще організувати ігровий цикл?»).
Використовувати AI-інструменти для пояснення концепцій та допомоги у налагодженні.
Заборонено:
Копіювати код з інших студентських проєктів (поточних або минулих років).
Подавати код, згенерований AI, як власний.
У парних проєктах: подавати роботу, яку виконав лише один учасник.
Контроль:
Історія комітів має демонструвати поступову розробку (див. розділ 7.3). Проєкт із одним-двома комітами за день до дедлайну не отримає відповідних балів за поступовий прогрес, а також буде перевірятися особливо ретельно.
Тижневі відео контрольних точок підтверджують авторство та процес роботи.
На захисті інструктор задає запитання про конкретні рядки коду. Якщо у вас виникли сумніви щодо допустимості певної дії — запитайте інструктора до того, як це зробите.
Чи можна обрати тему, якої немає в меню?<br>
Ні. Обирайте з <a data-tooltip-position="top" aria-label="https://github.com/ukma-fin-csa-2026/projects/issues" rel="noopener nofollow" class="external-link is-unresolved" href="https://github.com/ukma-fin-csa-2026/projects/issues" target="_self">офіційного меню</a>. Теми підібрані так, щоб забезпечити рівномірну складність та покриття результатів навчання.Чи можна змінити тему після вибору?
Так, але тільки до кінця тижня 9 та за погодженням з інструктором. Напишіть новий коментар в Issue нової теми та повідомте інструктора.Я не знайшов(-ла) пару. Що робити?
Можете працювати самостійно. Вимоги до обсягу та якості проєкту залишаються незмінними.Мій проєкт не працює повністю. Чи варто подавати?
Так. Подайте те, що є. Система оцінювання спеціально побудована так, щоб винагороджувати послідовну працю: тижневий прогрес, якість коду, звіт та захист можна отримати навіть без повної функціональності. Частково працюючий проєкт із регулярними контрольними точками та вдумливим звітом отримає значно більше балів, ніж нічого. У звіті опишіть, що працює, що ні, і чому.Чи обов'язкові тижневі відео контрольних точок?
Так. Контрольні точки становлять 15% оцінки за проєкт (оцінювання бінарне: подано вчасно = повний бал). Це найпростіший спосіб гарантувати собі частину балів — просто працюйте регулярно, комітьте, та фіксуйте прогрес щотижня.Чи може пара складатися зі студентів різних лабораторних груп?
Так, обмежень на групи немає.]]></description><link>labs/project.html</link><guid isPermaLink="false">Labs/Project.md</guid><pubDate>Fri, 06 Mar 2026 14:06:59 GMT</pubDate><enclosure url="assets/git-commit-message-meme.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/git-commit-message-meme.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y24-Rhythm-Game]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🟡 Жовтий
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 400–550 (соло) / 650–850 (пара), 2–5 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/pc-speaker.md" href="projects/how-to/pc-speaker.html" target="_self" rel="noopener nofollow">Програмування портів вводу/виводу (PC Speaker)</a> — PIT канал 2 (порти 42h/43h) та вентиль динаміка (порт 61h) для генерації тонів при влученні по ноті — L08/P08
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять</a> — анімація падаючих нот, зона влучення, рахунок та зворотний зв'язок у B800h — L08/P08
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви в сегменті даних</a> — зберігання пісень як масивів подій (такт, доріжка, частота), масив активних нот на екрані — L06/P06
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури та стекові фрейми</a> — spawn_note, update_notes, check_hit, draw_lanes як окремі підпрограми — L05/P05
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід через INT 16h</a> — неблокуюча перевірка клавіш (AH=01h) для миттєвого реагування на натискання під час гри — L07/P07
<br><a class="internal-link" data-href="How-to/timer-delays.md" href="projects/how-to/timer-delays.html" target="_self" rel="noopener nofollow">Таймер та затримка</a> — хронометраж через тіки таймера BIOS (0040:006Ch) для визначення моменту появи нот та їхнього руху — L07/P07
<br><a class="internal-link" data-href="How-to/arithmetic.md" href="projects/how-to/arithmetic.html" target="_self" rel="noopener nofollow">Арифметика</a> — обчислення дільника PIT (1193180 / freq), рахунку, відсотка точності, вікна влучення — L04/P04
<br><a class="internal-link" data-href="How-to/bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Побітові операції</a> — встановлення/скидання бітів вентиля динаміка через AND/OR на порту 61h — L04/P04, L05/P05
<br><a class="internal-link" data-href="How-to/addressing-modes.md" href="projects/how-to/addressing-modes.html" target="_self" rel="noopener nofollow">Режими адресації</a> — індексований доступ до масивів пісень, обчислені зміщення для позицій нот — L06/P06
<br><a class="internal-link" data-href="How-to/game-logic/Y24.md" href="projects/how-to/game-logic/y24.html" target="_self" rel="noopener nofollow">Ігрова логіка: Ритм-гра</a> — механіка падаючих нот, вікно влучення, підрахунок очок, комбо-система
<br>Див. також <a class="internal-link" data-href="Y16-Music-Player.md" href="projects/y16-music-player.html" target="_self" rel="noopener nofollow">Y16 Музичний програвач</a> — також використовує PC Speaker та PIT для генерації звуку. Ритм-гра зосереджується на взаємодії гравця з музикою в реальному часі замість пасивного відтворення.Програма відкривається екраном вибору пісні: три або більше назв пісень відображені у списку, з поточним вибором підсвіченим яскравим кольором. Гравець обирає пісню клавішами-стрілками та Enter.Після вибору відкривається ігровий екран з чотирма вертикальними доріжками, позначеними клавішами D, F, J, K. Кольорові блоки-ноти (символи █) з'являються вгорі відповідних доріжок і плавно рухаються вниз. Біля нижнього краю екрану розташована яскраво підсвічена «зона влучення» — рядок зі стрілками або рамкою, що вказує, де потрібно натискати клавіші.Коли нота досягає зони влучення, гравець має натиснути відповідну клавішу. При точному натисканні PC Speaker грає частоту цієї ноти (короткий тон ~100 мс), на екрані спалахує "PERFECT!" яскравим кольором, і нота зникає. При натисканні з невеликим запізненням або випередженням — "GOOD", при промаху — "MISS" та нота пролітає повз. Якщо гравець натискає клавішу без ноти в зоні — також "MISS".Праворуч від доріжок відображається панель інформації: назва пісні, поточний рахунок, серія влучень (комбо), відсоток точності та індикатор прогресу пісні. Комбо-лічильник збільшується за кожне послідовне влучення (PERFECT або GOOD) і скидається при промаху. Чим довша серія — тим більший множник очок.Коли пісня закінчується, з'являється екран результатів: фінальний рахунок, кількість PERFECT/GOOD/MISS, максимальне комбо та загальний відсоток точності. Гравець може натиснути Enter для повернення до меню або Esc для виходу.Усі мелодії зберігаються у сегменті даних як масиви подій (такт появи, номер доріжки, частота). Увесь інтерфейс працює в текстовому режимі 80x25 з кольоровими атрибутами для створення динамічної ритм-гри.<br><img alt="Ігровий процес з падаючими нотами, зоною влучення та панеллю рахунку" src="assets/y24-screen-gameplay.png" target="_self"><br><img alt="Екран результатів після завершення пісні" src="assets/y24-screen-results.png" target="_self">
Підхід A: Головний цикл зчитує лічильник тіків BIOS (0040:006Ch, ~18.2 тіків/сек) і оновлює гру, коли пройшов наступний тік. Простіше, не потребує модифікації таблиці векторів переривань.
Підхід B: Встановити обробник INT 1Ch, який виставляє прапорець tick_flag при кожному тіку таймера. Головний цикл реагує на прапорець. Складніше (PUSH/POP, IRET, відновлення вектора), але відокремлює хронометраж від ігрової логіки.
Що вимірювати/спостерігати: Стабільність швидкості гри, складність реалізації, безпечність (ризик пошкодження регістрів в обробнику переривань). Підхід A простіший та достатній для цього проєкту; підхід B — більш професійний. Підхід A: Масив з фіксованим максимумом одночасних нот (наприклад, 16 записів). Кожен запис: (доріжка, рядок, частота, активна). Нова нота записується в першу неактивну клітинку. Пошук O(N) для вставки та перевірки влучення.
Підхід B: Кільцевий буфер з покажчиками голови та хвоста. Нові ноти додаються до голови, застарілі (промахи) видаляються з хвоста. Вставка та видалення O(1), але потребує модулярної арифметики.
Що вимірювати/спостерігати: Кількість інструкцій для вставки та видалення ноти, складність коду, поведінка при переповненні буфера. Масив простіший для налагодження; кільцевий буфер ефективніший, коли нот багато. Підхід A: Порівнюєте рядок ноти з рядком зони влучення: |нотарядок - хітрядок| ≤ 1 → PERFECT, ≤ 2 → GOOD, інакше MISS. Простий та візуально інтуїтивний.
Підхід B: Вираховуєте різницю в тіках таймера між ідеальним моментом натискання та фактичним. Точніше, але потребує зберігання мітки часу для кожної ноти.
Що вимірювати/спостерігати: Суб'єктивна справедливість оцінки при різних швидкостях гри, складність реалізації. Підхід A прив'язаний до швидкості відображення; підхід B — апаратно-незалежний.
<br>Розподіл модулів (див. <a class="internal-link" data-href="How-to/include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">Підключення файлів: INCLUDE</a>):
Студент A: Аудіорушій та ігрова логіка — програмування PIT, генерація тонів при влученні, хронометраж появи нот, оновлення позицій активних нот, перевірка влучень (PERFECT/GOOD/MISS), підрахунок очок та комбо, екран результатів (фінальний рахунок, статистика, літерна оцінка). Надає: play_hit_tone(freq), silence_speaker, spawn_notes, update_notes, check_hit(lane), get_score, draw_results.
Студент B: Відображення, UI та дані пісень — екран вибору пісні, малювання доріжок та зони влучення, анімація падаючих нот, відображення рахунку/комбо/точності, зворотний зв'язок (PERFECT/GOOD/MISS), опитування клавіатури, структури даних пісень (масиви трійок для 3+ мелодій). Надає: draw_lanes, draw_notes, draw_hit_zone, draw_score_panel, draw_feedback, poll_keyboard, дані 3+ пісень.
Точка інтеграції: Спільні змінні для масиву активних нот, рахунку, комбо, стану гри (MENU/PLAYING/RESULTS). Студент A оновлює позиції нот та визначає результат влучення; Студент B зчитує ці дані для рендерингу та надає дані пісень, які A використовує для spawn_notes. Обидва працюють в одному головному циклі: оновити ноти, опитати клавіатуру, перевірити влучення, оновити дисплей. <br><a class="internal-link" data-href="How-to/game-logic/Y24.md" href="projects/how-to/game-logic/y24.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: механіка падаючих нот, вікно влучення, підрахунок очок, комбо-система.
<br><a class="internal-link" data-href="How-to/pc-speaker.md" href="projects/how-to/pc-speaker.html" target="_self" rel="noopener nofollow">Генерація тону через PIT:</a> (1) обчислити дільник = 1193180 / частота; (2) відправити командний байт 0B6h на порт 43h; (3) відправити молодший байт дільника на порт 42h; (4) відправити старший байт дільника на порт 42h; (5) зчитати порт 61h, виконати OR з 03h, записати назад. Для вимкнення: зчитати порт 61h, виконати AND з 0FCh, записати назад.
<br><a class="internal-link" data-href="How-to/timer-delays.md" href="projects/how-to/timer-delays.html" target="_self" rel="noopener nofollow">Таймер BIOS:</a> Лічильник тіків — DWORD за адресою 0040:006Ch. Один тік = ~55 мс, 18 тіків ≈ 1 секунда. Використовуйте для хронометражу руху нот та тривалості тону.
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Неблокуюча перевірка клавіші:</a> MOV AH, 01h / INT 16h / JZ no_key. Якщо ZF=0, зчитайте клавішу за допомогою MOV AH, 00h / INT 16h. ASCII-коди клавіш: D=44h/64h, F=46h/66h, J=4Ah/6Ah, K=4Bh/6Bh.
Розташування доріжок: 4 доріжки з центром на стовпцях, наприклад: 20, 30, 40, 50. Ширина кожної доріжки — 5–7 символів. Зона влучення на рядку 20–21.
Структура даних пісні: масив трійок (тактпояви, номердоріжки, частота). Маркер кінця: такт = 0FFFFh. Поле «такт» — номер тіку таймера BIOS від початку пісні, коли нота має з'явитися вгорі екрану.
<br><a class="internal-link" data-href="How-to/arithmetic.md" href="projects/how-to/arithmetic.html" target="_self" rel="noopener nofollow">Арифметика рахунку:</a> PERFECT = 100 × (1 + комбо/10), GOOD = 50 × (1 + комбо/10). Використовуйте MUL та DIV. Відсоток точності = (perfect + good) × 100 / всього_нот.
Стандартні частоти нот: C4=262, D4=294, E4=330, F4=349, G4=392, A4=440, B4=494, C5=523. Визначте їх як константи EQU.
Тривалість тону при влученні: ~2 тіки BIOS (~110 мс) — достатньо для чіткого звуку, не заважає наступним нотам.
Точки синхронізації:
Кінець тижня 9: узгодити спільний .DATA (масив active_notes зі структурою: доріжка DB, рядок DB, частота DW, прапорець DB; score DW 0, combo DW 0, game_state DB), формат масивів пісень (трійки DW: такт, доріжка, частота) та прототипи процедур
Кінець тижня 11: перша інтеграція — ноти падають, клавіші працюють, звучить тон при влученні, рахунок оновлюється
Кінець тижня 12: Студент B надає дані 3 пісень у узгодженому форматі (трійки DW), Студент A інтегрує їх у spawn_notes
Кінець тижня 13: програма повністю працює (3 пісні, меню, влучення з оцінкою, комбо, звук, екран результатів), обидва розділи README готові
Кінець тижня 14: фінальне тестування на різних налаштуваннях cycles DOSBox, обидва Loom-відео записані
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/y24-rhythm-game.html</link><guid isPermaLink="false">Projects/Y24-Rhythm-Game.md</guid><pubDate>Sat, 28 Feb 2026 17:02:43 GMT</pubDate><enclosure url="assets/y24-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y24-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y24-Screen-Results]]></title><description><![CDATA[<img src="assets/y24-screen-results.png" target="_self">]]></description><link>assets/y24-screen-results.html</link><guid isPermaLink="false">assets/Y24-Screen-Results.png</guid><pubDate>Sat, 28 Feb 2026 16:58:13 GMT</pubDate><enclosure url="assets/y24-screen-results.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y24-screen-results.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[README]]></title><description><![CDATA[Курс: Архітектура комп'ютерних систем / Computer Systems Architecture
Платформа: TASM / DOSBox / Turbo Debugger
Деталі проєкту: <a class="internal-link" data-href="../Labs/Project.md" href="labs/project.html" target="_self" rel="noopener nofollow">Project.md</a>
<br>Спочатку створіть репозиторій проєкту та сформуйте команду — див. <a class="internal-link" data-href="../Labs/Project.md#реєстрація-на-тему" href="labs/project.html#реєстрація-на-тему" target="_self" rel="noopener nofollow">розділ «Реєстрація на тему»</a> в основному документі проєкту.
Перегляньте теми нижче. Натисніть на будь-яку тему, щоб переглянути повну специфікацію.
Кожна тема має позначку складності: Зелений 🟢 — Менший обсяг, чіткіша специфікація, менше рухомих частин. Підходить, якщо асемблер все ще здається складним.
Жовтий 🟡 — Стандартна складність. Потребує впевненого володіння всіма концепціями курсу, розглянутими до тижня 7.
Червоний 🔴 — Амбіційний. Складні структури даних, обмеження реального часу або розширена системна взаємодія. Парна робота наполегливо рекомендується. Максимум 3 команди на тему (хто перший — того й місце). Обирайте рано для кращої доступності.
Кожна специфікація включає мінімальні вимоги (що потрібно здати), рекомендовані розширення (додаткові цілі) та ідеї «Підхід A vs B» для звіту.
]]></description><link>projects/readme.html</link><guid isPermaLink="false">Projects/README.md</guid><pubDate>Sat, 28 Feb 2026 16:51:44 GMT</pubDate></item><item><title><![CDATA[Y24-Screen-Gameplay]]></title><description><![CDATA[<img src="assets/y24-screen-gameplay.png" target="_self">]]></description><link>assets/y24-screen-gameplay.html</link><guid isPermaLink="false">assets/Y24-Screen-Gameplay.png</guid><pubDate>Sat, 28 Feb 2026 16:51:12 GMT</pubDate><enclosure url="assets/y24-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y24-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y24]]></title><description><![CDATA[Алгоритми механіки падаючих нот, вікна влучення, підрахунку очок з комбо-системою та хронометражу появи нот.
Потрібно реалізувати падаючі ноти, що рухаються зверху вниз по доріжках.
Потрібно визначити точність натискання клавіші відносно позиції ноти (PERFECT/GOOD/MISS).
Потрібно підрахувати очки з комбо-множником та відтворити звук при влученні.
Пісня — це послідовність подій «поява ноти». Кожна подія зберігає: коли нота з'являється, на якій доріжці, та яку частоту грати при влученні.; Трійки DW: такт_появи, доріжка (0-3), частота
song1 DW 10, 0, 262 ; тік 10, доріжка D, C4 DW 14, 1, 294 ; тік 14, доріжка F, D4 DW 18, 2, 330 ; тік 18, доріжка J, E4 DW 22, 3, 349 ; тік 22, доріжка K, F4 DW 28, 0, 392 ; тік 28, доріжка D, G4 DW 0FFFFh ; маркер кінця
Поле «такт» — це кількість ігрових тіків від початку пісні. Один ігровий тік = один або кілька тіків BIOS (залежно від швидкості гри).Константи для зручності:LANE_D EQU 0
LANE_F EQU 1
LANE_J EQU 2
LANE_K EQU 3 C4 EQU 262
D4 EQU 294
E4 EQU 330
F4 EQU 349
G4 EQU 392
A4 EQU 440
B4 EQU 494
C5 EQU 523
Активні ноти — це ноти, які зараз видимі на екрані та рухаються вниз. Зберігаються в масиві фіксованого розміру:MAX_NOTES EQU 16 ; Структура одного запису: 6 байт
; Зміщення 0: доріжка (DB)
; Зміщення 1: рядок на екрані (DB)
; Зміщення 2: частота (DW)
; Зміщення 4: активна (DB, 1=так, 0=ні)
; Зміщення 5: резерв (DB)
NOTE_SIZE EQU 6 active_notes DB MAX_NOTES * NOTE_SIZE DUP(0)
Кожен ігровий тік перевіряємо, чи настав час для нової ноти:song_ptr = вказівник на поточну позицію в масиві пісні spawn_notes:
1. Зчитати такт_появи = [song_ptr]
2. Якщо такт_появи = 0FFFFh → пісня закінчилась, вийти
3. Якщо game_tick &lt; такт_появи → ще рано, вийти
4. Зчитати доріжку = [song_ptr + 2]
5. Зчитати частоту = [song_ptr + 4]
6. Знайти першу неактивну клітинку в active_notes
7. Записати: доріжка, рядок = START_ROW (наприклад, 2), частота, активна = 1
8. song_ptr += 6 (перейти до наступної трійки)
9. Повторити з кроку 1 (може бути кілька нот на одному тіку!)
Кожен ігровий тік зсуваємо всі активні ноти на один рядок вниз:update_notes: Для кожного запису i в active_notes (i = 0..MAX_NOTES-1): Якщо active_notes[i].активна = 0 → пропустити active_notes[i].рядок += 1 Якщо active_notes[i].рядок &gt; BOTTOM_ROW: ; Нота пролетіла повз зону влучення → MISS active_notes[i].активна = 0 combo = 0 miss_count += 1
Обчислення зміщення запису в масиві:; Для запису i (в BX):
; Зміщення = i * NOTE_SIZE = i * 6
mov ax, bx
mov cl, NOTE_SIZE ; 6
mul cl ; AX = i * 6
lea si, active_notes
add si, ax ; SI = адреса запису i
Коли гравець натискає клавішу доріжки:check_hit(lane): best_note = -1 ; індекс найближчої ноти best_dist = 255 ; найменша відстань Для кожного активного запису i: Якщо active_notes[i].доріжка ≠ lane → пропустити dist = |active_notes[i].рядок - HIT_ROW| Якщо dist &lt; best_dist: best_dist = dist best_note = i Якщо best_note = -1: ; Немає ноти на цій доріжці → ігнорувати або MISS Повернути MISS Якщо best_dist ≤ 1: ; PERFECT active_notes[best_note].активна = 0 combo += 1 score += 100 * (1 + combo / 10) play_hit_tone(active_notes[best_note].частота) Повернути PERFECT Якщо best_dist ≤ 2: ; GOOD active_notes[best_note].активна = 0 combo += 1 score += 50 * (1 + combo / 10) play_hit_tone(active_notes[best_note].частота) Повернути GOOD Інакше: ; Нота є, але занадто далеко → MISS combo = 0 Повернути MISS
Обчислення абсолютної різниці (|A - B|) без знакового ділення:; AL = рядок ноти, AH = HIT_ROW
cmp al, ah
jae @@above
xchg al, ah ; гарантувати AL &gt;= AH
@@above:
sub al, ah ; AL = |різниця|
Комбо збільшується за кожне послідовне влучення (PERFECT або GOOD) та скидається при MISS:При влученні: combo += 1 множник = 1 + combo / 10 base_score = 100 (PERFECT) або 50 (GOOD) score += base_score * множник При промаху: combo = 0
Обчислення в асемблері:; AX = base_score (100 або 50)
; [combo] = поточна серія
push ax
mov ax, [combo]
mov bl, 10
xor dx, dx
div bl ; AL = combo / 10
inc al ; AL = множник = 1 + combo/10
mov bl, al
pop ax
mul bl ; AX = base_score * множник
add [score], ax
Короткий тон (~100 мс = ~2 тіки BIOS) грає при PERFECT або GOOD:play_hit_tone(freq):
1. Обчислити дільник = 1193180 / freq (32-бітне ділення)
2. Програмувати PIT канал 2 (OUT 43h, 0B6h; OUT 42h, lo; OUT 42h, hi)
3. Увімкнути динамік (IN AL, 61h / OR AL, 03h / OUT 61h, AL)
4. Зберегти мітку часу: tone_start = поточний тік BIOS
5. Встановити прапорець: tone_active = 1, tone_end = tone_start + 2 В головному циклі (кожен тік): Якщо tone_active = 1 і поточний тік &gt;= tone_end: Вимкнути динамік (AND AL, 0FCh) tone_active = 0
Це дозволяє тону не блокувати ігровий цикл — динамік працює паралельно, а головний цикл вимикає його через 2 тіки.game_loop:
1. Зчитати поточний тік BIOS (0040:006Ch)
2. Якщо тік не змінився з минулого разу → перейти до кроку 5 (обробляємо не більше одного ігрового кроку за тік) 3. Ігровий крок: a. game_tick += 1 b. spawn_notes() ; перевірити, чи з'являються нові ноти c. update_notes() ; зсунути ноти вниз на 1 рядок d. check_tone_end() ; вимкнути динамік, якщо час минув e. draw_notes() ; оновити екран f. draw_score_panel() ; оновити рахунок 4. Перевірити кінець пісні: Якщо song_ptr на маркері кінця І всі active_notes неактивні: → перейти до екрану результатів 5. Опитати клавіатуру (INT 16h AH=01h): Якщо клавіша натиснута: Зчитати клавішу (AH=00h) Якщо Esc → вимкнути динамік, вийти Якщо D/F/J/K → check_hit(відповідна доріжка) draw_feedback(результат) 6. JMP game_loop
Примітка: опитування клавіатури (крок 5) виконується ЩОРАЗУ, а не лише при новому тіку. Це забезпечує миттєву реакцію на натискання.; --- Перевірка влучення для однієї доріжки ---
; Вхід: BL = номер доріжки (0-3)
; Вихід: AL = результат (0=MISS, 1=GOOD, 2=PERFECT)
; Змінює: score, combo якщо влучення
check_hit PROC push bx push cx push si push dx mov cl, 0FFh ; best_dist = 255 mov ch, 0FFh ; best_idx = -1 xor dx, dx ; DL = індекс (0..MAX_NOTES-1) lea si, active_notes @@scan_loop: cmp dl, MAX_NOTES jae @@scan_done cmp byte ptr [si+4], 1 ; активна? jne @@next cmp byte ptr [si], bl ; доріжка = BL? jne @@next ; Обчислити відстань: |рядок - HIT_ROW| mov al, [si+1] ; рядок ноти ; ... ваш код для обчислення |AL - HIT_ROW| ... cmp al, cl ; порівняти з best_dist jae @@next mov cl, al ; оновити best_dist mov ch, dl ; оновити best_idx @@next: add si, NOTE_SIZE inc dl jmp @@scan_loop @@scan_done: cmp ch, 0FFh je @@miss_no_note ; немає ноти cmp cl, 1 jbe @@perfect cmp cl, 2 jbe @@good jmp @@miss @@perfect: ; ... деактивувати ноту, збільшити combo та score ... ; ... викликати play_hit_tone ... mov al, 2 ; повернути PERFECT jmp @@done @@good: ; ... деактивувати ноту, збільшити combo та score ... ; ... викликати play_hit_tone ... mov al, 1 ; повернути GOOD jmp @@done @@miss:
@@miss_no_note: mov word ptr [combo], 0 mov al, 0 ; повернути MISS ; ... ваш код тут ... @@done: pop dx pop si pop cx pop bx ret
check_hit ENDP ; --- Вимкнути динамік (викликати перед виходом!) ---
silence_speaker PROC in al, 61h and al, 0FCh out 61h, al ret
silence_speaker ENDP Забули встановити DX перед DIV. 1193180 = 0012:34DCh. Якщо DX не дорівнює 0012h, результат ділення буде хибним або спричинить переповнення (INT 0).
Забули вимкнути динамік при виході. Без AND AL, 0FCh / OUT 61h, AL перед завершенням програми динамік продовжить гудіти.
Не зчитали клавішу після INT 16h AH=01h. Функція 01h лише перевіряє буфер, але не видаляє клавішу. Потрібен додатковий виклик AH=00h.
Блокуючий тон замість неблокуючого. Якщо play_hit_tone чекає в циклі поки тон не закінчиться, ноти перестають рухатися та клавіатура не реагує. Використовуйте прапорець tone_active і перевіряйте його в головному циклі.
Кілька нот на одному тіку. Якщо в пісні дві ноти з однаковим тактом (акорд / одночасні доріжки), spawn_notes має додати обидві, а не лише першу. Потрібен цикл: додавати ноти, поки такт = game_tick.
Нота «перестрибує» зону влучення. Якщо ноти рухаються більш ніж на один рядок за тік, вони можуть пролетіти повз HIT_ROW і не бути виявленими. Рухайте на 1 рядок за тік. <a class="internal-link" data-href="../pc-speaker.md" href="projects/how-to/pc-speaker.html" target="_self" rel="noopener nofollow">Звук через PC Speaker</a>
<br><a class="internal-link" data-href="../timer-delays.md" href="projects/how-to/timer-delays.html" target="_self" rel="noopener nofollow">Таймер та затримка</a>
<br><a class="internal-link" data-href="../arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви в сегменті даних</a>
<br><a class="internal-link" data-href="../keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід</a>
<br><a class="internal-link" data-href="Y16.md" href="projects/how-to/game-logic/y16.html" target="_self" rel="noopener nofollow">Y16 Ігрова логіка: Музичний плеєр</a> — спільна тема: PIT-програмування та хронометраж
<br>
Корисно для: <a class="internal-link" data-href="../../Y24-Rhythm-Game.md" href="projects/y24-rhythm-game.html" target="_self" rel="noopener nofollow">Y24 Ритм-гра</a>
]]></description><link>projects/how-to/game-logic/y24.html</link><guid isPermaLink="false">Projects/How-to/game-logic/Y24.md</guid><pubDate>Sat, 28 Feb 2026 16:49:50 GMT</pubDate></item><item><title><![CDATA[Консультація - 2026-02-28]]></title><description><![CDATA[Дата: 28 лютого 2026 р.
Формат: офісна година (office hours), онлайн
Тривалість: ~60 хвКонсультацію було розпочато з організаційних оголошень перед тижнем самостійної роботи.Issues та реєстрація на проєкти. О 18:00 того ж дня мали відкритися issues (картки задач) у репозиторіях — студенти зможуть записуватися на проєкти. На момент консультації зареєструвалося близько 50 студентів із 130, тобто було сформовано приблизно 25 команд.Конкурс як альтернатива проєкту. Паралельно протягом тижня самостійної роботи оголошується конкурс. Можна перейти з проєкту на конкурс, але лише в один бік — повернутися назад не можна. Теоретично можна спробувати робити і проєкт, і конкурс одночасно, але це буде непросто. Також дозволяється робити кілька проєктів послідовно — спершу завершити один, потім взятися за інший.Обмеження на команди. На один проєкт записується не більше трьох команд, щоб забезпечити різноманітність. Команди не бачать репозиторії одна одної, щоб уникнути нечесної конкуренції. Водночас це дозволить цікаво порівняти різні підходи до однієї задачі.Приклади проєктів. Серед проєктів є як ігри (наприклад, Tetris), так і утиліти: Hex Editor / Hex Viewer файлів (перегляд байтів конкретного файлу, щось на кшталт навігації по файлу аналогічно до NAVDUMP), переглядач секторів диску тощо. Обговорювалася також ідея об'єднуючої «обгортки» — меню-лаунчера, що запускатиме різні міні-програми (ігри та утиліти), створені різними командами.Було поставлене питання: як в асемблері реалізувати масив, що зберігає елементи різних типів (числа, рядки тощо), аналогічно до списків у Python?Проблема. На рівні байтів число 5 та зміщення (offset) 5 виглядають однаково — процесор їх не розрізняє. Потрібен додатковий механізм — дескриптор типу.Рішення через масив посилань (офсетів). Найкращий підхід — створити масив офсетів (посилань) на об'єкти. Кожен елемент масиву займає фіксований розмір (наприклад, 2 байти — зміщення в сегменті). За цим зміщенням перший байт є дескриптором типу:
0x00 — наступні 2 байти інтерпретуємо як ціле число (int)
0x01 — наступні байти до нуль-термінатора є рядком
0x02 — інший тип (за домовленістю)
Альтернативний підхід — паралельні масиви. Можна мати окремий масив типів (по 1 байту на елемент) та окремий масив зміщень.Аналогія з Python. У Python кожна змінна теж внутрішньо має дескриптор типу, хоча програміст його явно не вказує. Ніякої магії — під капотом відбувається рівно те саме, що ви зробили б в асемблері.Що таке купа. Купа — це окрема область пам'яті (окремий сегмент або частина сегмента даних), де розміщуються динамічно створювані об'єкти різного розміру.Таблиця дескрипторів. Окрема структура даних (зазвичай на початку або в кінці купи) зберігає інформацію про те, що і за яким зміщенням лежить у купі: назву змінної, її зміщення, тип, розмір тощо.Підрахунок посилань і Garbage Collector. Дескриптор може також зберігати кількість посилань на кожен об'єкт. Якщо кількість посилань дорівнює нулю — пам'ять можна звільнити та використати повторно. Саме так працює збирач сміття (Garbage Collector) у мовах високого рівня.Як це працює в різних мовах:
У Java є віртуальна машина (JVM), яка виконує роль операційної системи для програми — збирач сміття, менеджер купи тощо вже вбудовані.
У компільованих мовах (наприклад, C/C++) компілятор генерує код керування купою, і ці сервісні функції вкомпільовуються безпосередньо в програму.
В асемблері все це потрібно реалізовувати вручну, написавши відповідні обслуговуючі процедури.
Значну частину консультації було присвячено розбору студентської реалізації HashMap — було показано код на Python (як референсну реалізацію) та обговорено перенесення на асемблер.Студент обрав closed addressing зі списками для колізій: є головний масив, де за хешем зберігається зміщення. Якщо виникає колізія, елементи додаються послідовно (linked list). Кожен елемент зберігає зміщення на пару ключ-значення (key-value), а пара — зміщення на власне ключ і значення.Реалізовано найпростішу хеш-функцію: сума ASCII-кодів символів рядка. Для невеликої кількості елементів (до 10) цього достатньо для навчальних цілей.Основна проблема, з якою зіткнувся студент — плутанина між рівнями зміщень: «зміщення на зміщення на зміщення». Складно відстежити, де ми маємо саме значення, а де — лише посилання на нього.Порада — використовувати STRUC. На лекції було показано директиву STRUC в асемблері, яка дозволяє оголошувати структури з іменованими полями (аналог struct у C). Це суттєво спрощує роботу з багаторівневою адресацією — замість ручного підрахунку зміщень можна звертатися через .node, .value, .key тощо.Було наголошено на принципі розділення відповідальностей (separation of concerns): HashMap має концентруватися виключно на операціях get та put довільних даних. Робота з конкретними типами (виведення рядків, їх копіювання тощо) має бути винесена в окремі функції, які працюють незалежно від HashMap. Це запобігає наростанню експоненційної складності, коли кілька «concerns» перетинаються в одному місці.Рекомендований підхід — поступове «розгортання» високорівневого коду:
Почати з робочої реалізації на Python.
Свідомо уникати високорівневих викликів (append, вбудовані структури) — переписати їх на більш низькорівневі конструкції.
Поступово зводити код до все нижчого рівня абстракції.
Коли код займе 3–4 сторінки низькорівневого псевдокоду — його вже легко перевести на асемблер.
Також було згадано книгу "Об'єктно-орієнтоване програмування на асемблері" (Том Свен), де детально розписано реалізацію ООП-механізмів (поліморфізм, перевантаження методів) на асемблерних структурах.Значну частину консультації було присвячено ревізії карток задач (issue cards), які студент підготував для свого проєкту HashMap.Кожна картка задачі (issue) має містити:
Чіткий опис того, що саме потрібно зробити.
Acceptance Criteria — конкретні критерії прийому: за якими умовами задачу можна вважати виконаною.
Test Scenarios — конкретні тестові сценарії з визначеними входами та очікуваними виходами (не абстрактні описи, а конкретні приклади).
Edge Cases — граничні випадки та окремі кейси (наприклад: що має відбуватися, якщо ключ не існує? Які типи ключів підтримуються, а які — ні?).
Артефакти — схеми, діаграми пам'яті, приклади розташування даних (особливо якщо не використовуються STRUC і структура даних визначається лише кодом).
Картки мають бути написані настільки чітко, щоб інша людина (напарник по команді) могла виконати задачу, не ставлячи додаткових питань. Це мінімізує непорозуміння та простір для неоднозначного тлумачення. Навіть для ручного тестування (manual QA) потрібно заздалегідь знати, що перевіряти.Для тестування окремих частин HashMap можна використовувати mock-об'єкти. Наприклад, якщо хеш-функція ще не реалізована, можна замінити її на «заглушку», що видає числа від 1 до 9 послідовно. Це дозволяє тестувати put і get незалежно від хеш-функції. Так само можна вручну заповнити структуру даними та тестувати лише get, не маючи готового put.На конкурсі (змаганні) будуть автоматичні тести на асемблерному коді — програмі подаватимуться вхідні дані, а виходи порівнюватимуться з очікуваними.Відбулася дискусія про співвідношення документації та коду в комерційній розробці.Історичний контекст. На початку 2000-х був період, коли індустрія писала дуже багато документації і мало коду — це призвело до поганих результатів. Як реакція, з'явився Agile з акцентом на «менше документації, більше коду». Останні 10–15 років у комерційній розробці документації пишеться мало: є задачі (картки), є самодокументуючий код, є автоматично згенеровані специфікації (OpenAPI/Swagger тощо).Коли потрібна документація. Баланс залежить від складності проєкту та розміру команди. Для простих проєктів достатньо карток і тестів. Для масштабних (аналогія: «як зробити Starship») — документації може бути більше, ніж коду. Документація особливо важлива, коли людина, яка знає контекст рішення, може покинути команду.Тести як документація. Автоматичні тести можуть частково замінити документацію: якщо нова фіча ламає наявний тест — це сигнал, що потрібно розібратися, чи стара поведінка все ще потрібна.Роль стейкхолдера. У продуктовому IT (на відміну від аутсорсу з конкретним замовником) стейкхолдер має вести документацію про те, які фічі мають бути та як вони працюють — інакше через півроку ніхто не пам'ятатиме, навіщо щось було зроблено.Наприкінці консультації обговорювалися типові практики організації роботи в IT-командах.Стендапи (standups). Щоденні короткі зустрічі (зазвичай стоячи), де кожен учасник команди каже кілька речень: що зроблено, що планується, чи є блокери. Решта слухає і оцінює, чи немає конфліктів з їхньою роботою. Мета — уникнути ситуацій, коли люди «розбігаються по кутках» і роблять щось неузгоджене.Синки (syncs). Тижневі або двотижневі синхронізації — більш детальне обговорення стану речей. Частота залежить від масштабу проєкту: тримісячний проєкт — раз на тиждень; простіша задача — раз на день або два.Епізоди. Раз на квартал команда збирається для обговорення довгострокових планів: що зроблено, що планується на наступний квартал. Іноді такі зустрічі плавно переходять у тімбілдинг.Planning Poker. Гейміфіковане планування: скрам-майстер готує картки з рівнями складності та часу, команда оцінює кожну задачу. Допомагає знайти компроміс між важливістю та трудомісткістю задач.
Читати Open Source — вивчати, як влаштовані реальні проєкти; це формує «надивленість» і шаблонні рішення в голові.
Читати технічні статті від компаній (Amazon, Netflix та інших) про їхні архітектурні рішення — наприклад, перехід від serverless до монолітної архітектури та навпаки.
Вчитися оцінювати задачі (estimation) — один із ключових навичок, бо в комерційній розробці завжди є обмежений бюджет і дедлайни.
Планувати перед кодуванням — детально продумувати логіку на папері або в псевдокоді перед імплементацією, особливо для складних задач на асемблері. Том Свен — «Об'єктно-орієнтоване програмування на асемблері» (OOP in Assembly)
МРПС — курс «Методи розробки програмних систем» (3-й курс КН), що охоплює інженерні практики розробки
Курс з ООП на старших курсах — об'єктно-орієнтований дизайн (OOD), архітектурні паттерни
]]></description><link>office-hours/консультація-2026-02-28.html</link><guid isPermaLink="false">Office-hours/Консультація - 2026-02-28.md</guid><pubDate>Sat, 28 Feb 2026 14:43:32 GMT</pubDate></item><item><title><![CDATA[R07-Screen-Checkmate]]></title><description><![CDATA[<img src="assets/r07-screen-checkmate.png" target="_self">]]></description><link>assets/r07-screen-checkmate.html</link><guid isPermaLink="false">assets/R07-Screen-Checkmate.png</guid><pubDate>Sat, 28 Feb 2026 14:34:44 GMT</pubDate><enclosure url="assets/r07-screen-checkmate.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/r07-screen-checkmate.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[R07-Screen-Check]]></title><description><![CDATA[<img src="assets/r07-screen-check.png" target="_self">]]></description><link>assets/r07-screen-check.html</link><guid isPermaLink="false">assets/R07-Screen-Check.png</guid><pubDate>Sat, 28 Feb 2026 14:34:44 GMT</pubDate><enclosure url="assets/r07-screen-check.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/r07-screen-check.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[R07-Screen-Gameplay]]></title><description><![CDATA[<img src="assets/r07-screen-gameplay.png" target="_self">]]></description><link>assets/r07-screen-gameplay.html</link><guid isPermaLink="false">assets/R07-Screen-Gameplay.png</guid><pubDate>Sat, 28 Feb 2026 14:32:16 GMT</pubDate><enclosure url="assets/r07-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/r07-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[R07]]></title><description><![CDATA[Алгоритми руху 6 типів фігур з таблицями напрямків, перевірка чистоти шляху ковзних фігур, is_square_attacked зворотним скануванням, перевірка шаху/мату/пату.
Потрібно реалізувати рух 6 типів фігур з різними правилами
Потрібно перевіряти чистоту шляху для ковзних фігур (тура, слон, ферзь)
Потрібно визначати, чи клітинка атакована ворожою фігурою
Потрібно фільтрувати ходи, що залишають власного короля під шахом
Потрібно виявляти мат та пат
Дошка зберігається як масив із 64 байт. Індекс клітинки: row * 8 + col (row 0 = верхній рядок = рядок 8 на шахівниці, row 7 = нижній = рядок 1).Кодування кожного байта:Біт 3 — колір: 0 = білий, 1 = чорний
Біти 0-2 — тип фігури: 0 = порожня клітинка 1 = пішак (P/p) 2 = кінь (N/n) 3 = слон (B/b) 4 = тура (R/r) 5 = ферзь (Q/q) 6 = король (K/k)
Приклади:Побітові перевірки:
TEST AL, 08h — чи фігура чорна (біт 3)?
AND AL, 07h — отримати тип фігури (маскувати колір)
TEST AL, 07h — чи є фігура взагалі (тип != 0)?
Замість окремого коду для кожної фігури використовуємо таблиці зміщень (row_delta, col_delta):; Прямі напрямки (тура, ферзь, король)
rook_dirs DB -1, 0, 1, 0, 0,-1, 0, 1 ; N, S, W, E ; Діагональні напрямки (слон, ферзь, король)
bishop_dirs DB -1,-1, -1, 1, 1,-1, 1, 1 ; NW, NE, SW, SE ; Ферзь = rook_dirs + bishop_dirs (всі 8) ; Ходи коня (фіксовані зміщення, не ковзні)
knight_offs DB -2,-1, -2, 1, -1,-2, -1, 2 DB 1,-2, 1, 2, 2,-1, 2, 1 ; 8 L-ходів
Три шаблони руху:
Ковзні (тура, слон, ферзь): ітерувати по напрямку, поки не вийдемо за межі або не зустрінемо фігуру
Одноклітинні (король, кінь): перевірити лише одну клітинку в кожному напрямку
Пішак: спеціальна логіка (напрямок залежить від кольору, рух ≠ взяття)
Один універсальний цикл для тури, слона та ферзя:generate_sliding_moves(row, col, dir_table, num_dirs): Для кожного напрямку d = 0..num_dirs-1: dr = dir_table[d*2] dc = dir_table[d*2+1] nr = row + dr nc = col + dc Поки (nr, nc) у межах дошки (0..7): piece = board[nr * 8 + nc] Якщо piece == EMPTY: додати (row, col, nr, nc) до move_list Якщо piece — ворожа фігура: додати (row, col, nr, nc) до move_list ; взяття ВИХІД з циклу напрямку Якщо piece — своя фігура: ВИХІД з циклу напрямку nr += dr nc += dc
Тура використовує rook_dirs (4 напрямки), слон — bishop_dirs (4), ферзь — обидві таблиці (8 напрямків).Кінь має 8 фіксованих зміщень. Не ковзає — просто перевіряємо 8 клітинок:generate_knight_moves(row, col): Для кожного зміщення i = 0..7: nr = row + knight_offs[i*2] nc = col + knight_offs[i*2+1] Якщо (nr, nc) у межах (0..7): piece = board[nr * 8 + nc] Якщо piece == EMPTY або piece — ворожа: додати (row, col, nr, nc) до move_list
Пішак — найскладніша фігура через асиметрію руху та взяття:generate_pawn_moves(row, col, color): ; Напрямок залежить від кольору Якщо color == WHITE: dir = -1 ; білий рухається вгору (row зменшується) Якщо color == BLACK: dir = +1 ; чорний рухається вниз (row збільшується) ; 1. Рух на 1 клітинку вперед nr = row + dir Якщо (nr, nc=col) у межах І board[nr][col] == EMPTY: додати (row, col, nr, col) до move_list ; 2. Подвійний крок з початкової позиції start_row = 6 для білих, 1 для чорних Якщо row == start_row: nr2 = row + 2 * dir Якщо board[nr2][col] == EMPTY: додати (row, col, nr2, col) до move_list ; 3. Діагональне взяття Для dc = -1, +1: nc = col + dc Якщо (nr, nc) у межах І board[nr][nc] — ворожа: додати (row, col, nr, nc) до move_list
Перетворення пішака: якщо nr == 0 (для білого) або nr == 7 (для чорного), хід потребує вибору фігури (ферзь, тура, слон, кінь).Ключова процедура. Використовуємо зворотне сканування — «дивимося» з заданої клітинки в усіх напрямках і перевіряємо, чи є там відповідна ворожа фігура:is_square_attacked(row, col, by_color): ; 1. Перевірити 4 прямі напрямки (ворожа тура або ферзь) Для кожного напрямку з rook_dirs: Ковзати від (row, col), поки не вийдемо за межі: piece = перша зустрінута фігура Якщо piece — ворожа тура або ферзь: ПОВЕРНУТИ TRUE Якщо piece — будь-яка інша: зупинити цей напрямок ; 2. Перевірити 4 діагональні напрямки (ворожий слон або ферзь) Для кожного напрямку з bishop_dirs: Ковзати від (row, col): piece = перша зустрінута фігура Якщо piece — ворожий слон або ферзь: ПОВЕРНУТИ TRUE Якщо piece — будь-яка інша: зупинити цей напрямок ; 3. Перевірити 8 позицій коня Для кожного зміщення з knight_offs: Якщо board[row+dr][col+dc] — ворожий кінь: ПОВЕРНУТИ TRUE ; 4. Перевірити 2 діагоналі пішака pawn_dir = -1 якщо by_color == BLACK, +1 якщо by_color == WHITE ; (ворожий пішак атакує з протилежного напрямку) Для dc = -1, +1: Якщо board[row+pawn_dir][col+dc] — ворожий пішак: ПОВЕРНУТИ TRUE ; 5. Перевірити 8 клітинок навколо (ворожий король) Для кожного з 8 сусідів: Якщо board[nr][nc] — ворожий король: ПОВЕРНУТИ TRUE ПОВЕРНУТИ FALSE
Після генерації всіх «псевдолегальних» ходів фільтруємо ті, що залишають короля під шахом:filter_legal_moves(move_list, move_count, color): legal_count = 0 Для кожного ходу (from_r, from_c, to_r, to_c): ; 1. Зберегти стан saved_from = board[from] saved_to = board[to] ; 2. Виконати хід тимчасово board[to] = board[from] board[from] = EMPTY ; 3. Знайти позицію свого короля ; (якщо рухали короля, його нова позиція = to) king_r, king_c = позиція короля кольору color ; 4. Перевірити шах Якщо NOT is_square_attacked(king_r, king_c, протилежний_колір): legal_list[legal_count] = цей хід legal_count++ ; 5. Відкотити хід board[from] = saved_from board[to] = saved_to
Коли сторона має хід, але всі кандидатні ходи відфільтровані (жоден не рятує від шаху або всі ставлять під шах):Якщо legal_count == 0: Якщо is_in_check(color): МАТ (протилежна сторона виграла) Інакше: ПАТ (нічия)
Не потрібен окремий алгоритм — відсутність легальних ходів автоматично визначає результат.; --- Константи ---
EMPTY EQU 0
W_PAWN EQU 1
W_KNIGHT EQU 2
W_BISHOP EQU 3
W_ROOK EQU 4
W_QUEEN EQU 5
W_KING EQU 6
BLACK_BIT EQU 08h ; біт 3 — колір board DB 64 DUP(0) ; Таблиці напрямків (dr, dc пари)
rook_dirs DB -1, 0, 1, 0, 0,-1, 0, 1
bishop_dirs DB -1,-1, -1, 1, 1,-1, 1, 1
knight_offs DB -2,-1, -2, 1, -1,-2, -1, 2 DB 1,-2, 1, 2, 2,-1, 2, 1 ; Ваги фігур для ШІ (індекс = тип)
piece_values DB 0, 1, 3, 3, 5, 9, 0 ; -, P, N, B, R, Q, K ; Список ходів: до 128 записів по 4 байти
move_list DB 512 DUP(0)
move_count DW 0 ; Позиції королів (для швидкої перевірки шаху)
wking_row DB ?
wking_col DB ?
bking_row DB ?
bking_col DB ?
; --- Генерація ходів ковзної фігури ---
; Вхід: AL = row, AH = col, SI = адреса таблиці напрямків, CX = кількість напрямків
; Вихід: ходи додані до move_list, move_count оновлений
generate_sliding PROC PUSH BX PUSH DX ; Для кожного напрямку (CX штук): ; 1. Завантажити (dr, dc) з [SI] ; 2. Ковзати: nr = row + dr, nc = col + dc ; 3. Поки (nr, nc) в межах 0..7: ; a. piece = board[nr*8 + nc] ; b. EMPTY -&gt; додати хід, продовжити ; c. Ворожа -&gt; додати хід (взяття), зупинити ; d. Своя -&gt; зупинити ; e. nr += dr, nc += dc ; (див. алгоритм генерації ходів ковзних фігур вище) ; ... ваш код тут ... POP DX POP BX RET
generate_sliding ENDP
; --- Перевірка: чи атакована клітинка? ---
; Вхід: AL = row, AH = col, DL = колір атакуючого (00h=білий, 08h=чорний)
; Вихід: CF = 1 якщо атакована, CF = 0 якщо ні
is_square_attacked PROC PUSH BX PUSH CX PUSH SI ; 1. Перевірити 4 прямі напрямки (ворожа тура або ферзь): ; LEA SI, rook_dirs / MOV CX, 4 ; Для кожного напрямку ковзати від (row, col) ; Перша зустрінута фігура: тура(4)/ферзь(5) потрібного кольору -&gt; STC, RET ; 2. Перевірити 4 діагоналі (ворожий слон або ферзь): ; LEA SI, bishop_dirs / MOV CX, 4 ; Аналогічно: слон(3)/ферзь(5) -&gt; STC, RET ; 3. Перевірити 8 ходів коня: ; LEA SI, knight_offs / MOV CX, 8 ; board[row+dr][col+dc] == кінь(2) потрібного кольору -&gt; STC, RET ; 4. Перевірити пішака (2 діагоналі): ; pawn_dir = -1 для чорних, +1 для білих ; board[row+pawn_dir][col±1] == пішак(1) потрібного кольору -&gt; STC, RET ; 5. Перевірити короля (8 сусідів): ; board[row+dr][col+dc] == король(6) потрібного кольору -&gt; STC, RET ; (див. алгоритм is_square_attacked вище) ; ... ваш код тут ... CLC ; не атакована JMP @@done
@@attacked: STC ; атакована
@@done: POP SI POP CX POP BX RET
is_square_attacked ENDP
; --- Перевірка легальності ходу (не залишає короля під шахом) ---
; Вхід: from_r, from_c, to_r, to_c — координати ходу
; Вихід: CF = 1 якщо легальний, CF = 0 якщо залишає під шахом
check_legal PROC PUSH AX PUSH BX ; 1. Зберегти board[from] та board[to] ; 2. Виконати хід: board[to] = board[from], board[from] = EMPTY ; 3. Знайти позицію свого короля (якщо рухали короля — нова позиція = to) ; 4. Викликати is_square_attacked(king_row, king_col, ворожий_колір) ; 5. Відкотити: board[from] = збережене, board[to] = збережене ; 6. Якщо не атакована: STC (легальний), інакше CLC ; ... ваш код тут ... POP BX POP AX RET
check_legal ENDP Ферзь не рухається правильно. Ферзь — це комбінація тури і слона. Якщо ви пишете окремий код для ферзя, помилки подвоюються. Використовуйте один generate_sliding з 8 напрямками (об'єднайте rook_dirs + bishop_dirs). Пішак може рухатись назад. Напрямок пішака (dir = -1 для білого, +1 для чорного) має бути жорстко прив'язаний до кольору. Якщо ви плутаєте напрямки, білі пішаки рухатимуться вниз. Хід короля дозволений на атаковану клітинку. is_square_attacked має враховувати, що король переміщується — його стара позиція стає порожньою. Якщо ви перевіряєте атаку до виконання ходу (без тимчасової модифікації дошки), король може «ховатися за собою». Подвійний крок пішака через фігуру. Пішак може зробити подвійний крок лише з початкової позиції І лише якщо обидві клітинки перед ним порожні. Забування перевірити першу клітинку дозволяє пішаку перестрибувати через фігури. is_square_attacked не перевіряє пішаків. Пішак атакує по діагоналі, а не прямо. Напрямок атаки протилежний напрямку руху: білий пішак атакує клітинки на рядок вище, тому ворожий білий пішак загрожує клітинкам знизу. Уважно з напрямками. Відкочування ходу для перевірки шаху забуває про взяття. При тимчасовому виконанні ходу захоплена фігура зникає з board[to]. Якщо ви не зберегли її перед ходом, при відкочуванні клітинка призначення стане порожньою замість повернення захопленої фігури. <a class="internal-link" data-href="../arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви</a>
<br><a class="internal-link" data-href="../procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури</a>
<br><a class="internal-link" data-href="../flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Прапорці та умовні переходи</a>
<br><a class="internal-link" data-href="../lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиці пошуку</a>
<br><a class="internal-link" data-href="../bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Побітові операції</a>
<br><a class="internal-link" data-href="Y12.md" href="projects/how-to/game-logic/y12.html" target="_self" rel="noopener nofollow">Ігрова логіка: Шашки</a>
<br>
Корисно для: <a class="internal-link" data-href="../../R07-Chess.md" href="projects/r07-chess.html" target="_self" rel="noopener nofollow">R07 Шахи (Chess)</a>
]]></description><link>projects/how-to/game-logic/r07.html</link><guid isPermaLink="false">Projects/How-to/game-logic/R07.md</guid><pubDate>Sat, 28 Feb 2026 13:58:26 GMT</pubDate></item><item><title><![CDATA[R07-Chess]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🔴 Червоний
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 600-800 (соло) / 800-1100 (пара), 3-6 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Пряме записування у відеопам'ять</a> (B800h) — рендеринг дошки 8x8 з кольоровими клітинками, фігурами, підсвічуванням курсора та дозволених ходів (L08/P08)
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Двовимірний масив та індексація</a> — дошка 8x8 збережена як 64 байти, доступ через row * 8 + col; кожна клітинка кодує тип фігури та колір (L06/P06)
<br><a class="internal-link" data-href="How-to/flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Багатошарова валідація ходів</a> — перевірка шаблону руху фігури → перевірка чистоти шляху (для ковзних фігур) → перевірка, що хід не залишає власного короля під шахом — три рівні умовної логіки (L04/P04)
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури з параметрами та глибокі ланцюжки викликів</a> — get_legal_moves(row, col), execute_move(from, to), is_square_attacked(row, col, color), is_in_check(color), ai_turn() — глибока ієрархія: ШІ → генерація ходів → валідація → перевірка шаху (L05/P05)
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Дисципліна стеку</a> — збереження/відновлення регістрів у глибоких ланцюжках; тимчасове збереження захопленої фігури для скасування ходу при перевірці шаху (L05/P05)
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід</a> (INT 16h) — навігація курсором, вибір фігури, підтвердження ходу, вибір фігури при перетворенні пішака (L07/P07)
<br><a class="internal-link" data-href="How-to/segment-registers.md" href="projects/how-to/segment-registers.html" target="_self" rel="noopener nofollow">Сегментні регістри</a> — ES для відеосегменту B800h, DS для даних (L06/P08)
<br><a class="internal-link" data-href="How-to/lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиці пошуку</a> — таблиці зміщень руху для кожного типу фігури (6 типів × різна кількість напрямків); таблиця символів фігур; таблиця кольорів (L06/P06)
<br><a class="internal-link" data-href="How-to/bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Побітові операції</a> — кодування стану клітинки (тип фігури + колір) у байтах з бітовими прапорцями, перевірка через AND/TEST (L04/P05)
<br><a class="internal-link" data-href="How-to/loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Пошук та перелічення</a> — сканування дошки для пошуку атакованих клітинок, генерації всіх легальних ходів, виявлення шаху та мату (L04/P06)
<br><a class="internal-link" data-href="How-to/game-logic/R07.md" href="projects/how-to/game-logic/r07.html" target="_self" rel="noopener nofollow">Ігрова логіка: Шахи</a> — 6 типів фігур з унікальними правилами руху, ковзні фігури з перевіркою шляху, перевірка шаху/мату, рокіровка, взяття на проході
<br>Див. також <a class="internal-link" data-href="Y12-Checkers.md" href="projects/y12-checkers.html" target="_self" rel="noopener nofollow">Y12 Шашки</a> — також покрокова гра на дошці 8x8 з курсорною навігацією та ШІ-суперником. Шахи значно складніші: 6 типів фігур замість 2, ковзні фігури потребують перевірки чистоти шляху, кожен хід потребує перевірки шаху, спеціальні ходи (рокіровка, взяття на проході, перетворення пішака).Програма запускається у DOSBox і відображає повну шахову дошку 8x8, відрендерену у текстовому режимі відеопам'яті. Дошка використовує чергування світлих та темних кольорових клітинок — кожна клітинка займає 4 символи завширшки та 2 рядки заввишки. Білі фігури відображаються яскравими символами латинських букв (K, Q, R, B, N, P) на тлі клітинки, чорні — аналогічними літерами іншого кольору. Ряди позначені цифрами 1-8, стовпці — літерами a-h. Інформаційна панель праворуч показує чий хід, список взятих фігур та базові інструкції.Гравець керує курсором за допомогою клавіш-стрілок. Обрана клітинка підсвічується зміною фону. Після натискання Enter на своїй фігурі всі легальні ходи підсвічуються кольором: порожні клітинки одним відтінком, клітинки з ворожими фігурами (взяття) — іншим. Гравець переміщує курсор до бажаної клітинки та натискає Enter для підтвердження ходу. Нелегальні ходи (порушення правил руху фігури, хід, що залишає короля під шахом) неможливі — вони просто не з'являються серед дозволених ходів. Якщо королю оголошено шах, це відображається на панелі статусу.Після ходу гравця ШІ обирає свій хід. У базовій версії ШІ надає пріоритет взяттям (обирає взяття з найціннішою здобиччю), а інакше обирає випадковий легальний хід. Коли пішак досягає останнього ряду, з'являється меню вибору фігури для перетворення (ферзь, тура, слон, кінь).Гра закінчується матом (відображається переможець), патом (нічия) або за згодою (гравець натискає Esc для виходу). Вся графіка побудована через прямий запис у відеопам'ять за сегментом B800h.Основна складність цього проєкту — у багатошаровій валідації ходів. Кожен з 6 типів фігур має унікальний шаблон руху. Ковзні фігури (тура, слон, ферзь) потребують перевірки, чи шлях не заблокований. Але навіть після цих перевірок хід може бути нелегальним, якщо він залишає власного короля під шахом — тобто кожен кандидатний хід потрібно "пробувати", перевіряти шах, і відкочувати. Ця тришарова валідація (шаблон → шлях → шах) відрізняє шахи від шашок і робить проєкт червоним. Парна робота наполегливо рекомендується.<br><img alt="Ігровий процес" src="assets/r07-screen-gameplay.png" target="_self"><br><img alt="Шах" src="assets/r07-screen-check.png" target="_self"><br><img alt="Мат" src="assets/r07-screen-checkmate.png" target="_self">
Підхід A: Для кожного типу ковзної фігури зберігається таблиця зміщень напрямків (наприклад, слон: 4 діагональних напрямки). Цикл ітерує по напрямках, для кожного просуваючись по клітинках, поки не зустрінеться стіна, своя або ворожа фігура. Один універсальний цикл для тури, слона та ферзя.
Підхід B: Окремий блок коду для кожного напрямку (розгорнутий): check_north, check_south, check_east, check_west тощо. Кожен блок — лінійна послідовність CMP/JE/INC без вкладених циклів.
Що вимірювати/спостерігати: Розмір коду (A — ~30 інструкцій для всіх ковзних фігур; B — ~150 інструкцій), зрозумілість (A — один цикл, складніший для розуміння; B — прямолінійний, легший для відлагодження), та ризик помилок (A — одна помилка ламає всі напрямки; B — помилка в одному напрямку не впливає на інші). Підхід A: Після кожного кандидатного ходу знаходить позицію свого короля та перевіряє, чи ця клітинка атакована будь-якою ворожою фігурою (скануючи всю дошку або перевіряючи всі напрямки атаки від клітинки короля).
Підхід B: Перед виконанням ходу перевіряє, чи фігура, що рухається, є зв'язаною (pinned) — чи стоїть вона на лінії між своїм королем та ворожою ковзною фігурою. Якщо зв'язана, дозволяє рух лише вздовж лінії зв'язки.
Що вимірювати/спостерігати: Складність реалізації (A — один універсальний is_square_attacked, ~50-80 інструкцій; B — потребує попереднього обчислення зв'язок, складніший, але ефективніший). Кількість звернень до масиву дошки за один хід. Підхід A рекомендується для мінімальних вимог; B — для оптимізації. Підхід A: Зібрати всі легальні ходи у список, обрати один випадково (або перше взяття, якщо є).
Підхід B: Для кожного легального ходу обчислити матеріальну оцінку результуючої позиції (сума ваг своїх фігур мінус ворожих: P=1, N/B=3, R=5, Q=9). Обрати хід з найвищою оцінкою.
Що вимірювати/спостерігати: Якість гри ШІ (кількість програних партій за 5 ігор), складність коду, час обчислення. Обговорити зв'язок з мінімаксом та чому навіть один рівень перегляду різко покращує гру.
<br>Цей проєкт наполегливо рекомендується для пар. Шість типів фігур з унікальними правилами, перевірка шаху та ШІ створюють значний обсяг коду — впоратися самостійно дуже складно (див. <a class="internal-link" data-href="How-to/include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">Підключення файлів: INCLUDE</a>).
Студент A — Відображення та інтерфейс: рендеринг дошки (draw_board, draw_piece, draw_cursor, highlight_moves), обробка клавіатурного вводу, потік вибору фігури та підтвердження ходу, меню перетворення пішака, індикатор ходу, відображення взятих фігур та шаху, екрани мату/пату/виходу. Файли: display.asm, input.asm.
Студент B — Ігрова логіка та ШІ: структура даних дошки, правила руху кожного типу фігури (get_legal_moves), перевірка чистоти шляху для ковзних фігур, execute_move, перевірка шаху (is_square_attacked, is_in_check), виявлення мату та пату, вибір ходу ШІ, перетворення пішака (логіка). Файли: game.asm, ai.asm.
Спільне: main.asm з ініціалізацією та ігровим циклом, сегмент даних з масивом дошки, лічильниками, змінною ходу. Узгодьте кодування клітинок: наприклад, 0 = порожня, біт 3 — колір (0=біла, 1=чорна), біти 0-2 — тип фігури (1=пішак, 2=кінь, 3=слон, 4=тура, 5=ферзь, 6=король).
Точки інтеграції: Студент B надає get_legal_moves(row, col) -&gt; move_list, count та execute_move(from, to) -&gt; captured_piece. Студент A викликає get_legal_moves для підсвічування та execute_move після підтвердження. Для ходів ШІ ai_turn() Студента B повертає обраний хід, а Студент A анімує його. <br>
<a class="internal-link" data-href="How-to/game-logic/R07.md" href="projects/how-to/game-logic/r07.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: таблиці напрямків для 6 типів фігур, перевірка чистоти шляху ковзних фігур, is_square_attacked зворотним скануванням, перевірка мату/пату. <br>
<a class="internal-link" data-href="How-to/bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Кодування клітинок дошки:</a> Рекомендоване кодування: біт 3 — колір (0=білий, 1=чорний), біти 0-2 — тип фігури (0=порожня, 1=пішак, 2=кінь, 3=слон, 4=тура, 5=ферзь, 6=король). Приклади: 01h=білий пішак, 09h=чорний пішак, 06h=білий король, 0Eh=чорний король. Перевірки: TEST AL, 08h — чи чорна фігура; AND AL, 07h — отримати тип; TEST AL, 07h — чи є фігура взагалі. <br>
<a class="internal-link" data-href="How-to/lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиці напрямків руху:</a> Для ковзних фігур визначте:
rook_dirs DB 0,-1, 0,1, -1,0, 1,0 ; N, S, W, E
bishop_dirs DB -1,-1, -1,1, 1,-1, 1,1 ; NW, NE, SW, SE
queen_dirs = всі 8 напрямків (об'єднання rook + bishop)
knight_offs DB -2,-1, -2,1, -1,-2, -1,2, 1,-2, 1,2, 2,-1, 2,1 ; 8 L-ходів Кінь та король — нековзні: перевіряємо лише одну клітинку в кожному напрямку. Ковзні: цикл по напрямку, поки не вийдемо за межі або не зустрінемо фігуру. <br>
<a class="internal-link" data-href="How-to/flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Перевірка <code></code>:</a>is_square_attacked Зворотне сканування від заданої клітинки. Для кожного з 4 прямих напрямків: ковзати, поки не зустрінемо фігуру — якщо це ворожа тура або ферзь, клітинка атакована. Для кожного з 4 діагональних: аналогічно для ворожого слона або ферзя. Перевірити 8 позицій коня. Перевірити 2 діагоналі для ворожого пішака (напрямок залежить від кольору). Перевірити 8 клітинок навколо для ворожого короля. Перевірка мату та пату: Згенерувати всі легальні ходи для сторони, що ходить. Якщо список порожній: якщо король під шахом — мат, інакше — пат. Не потрібен окремий алгоритм. <br>
<a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Символи фігур:</a> K=Король, Q=Ферзь, R=Тура, B=Слон, N=Кінь, P=Пішак. Білі фігури: яскраво-білий атрибут (0Fh або 6Fh на коричневому тлі). Чорні: темний атрибут (00h або 60h на коричневому). Збережіть у таблиці: piece_chars DB ' PNBRQK' (індексований за типом фігури 0-6). <br>
<a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Відеопам'ять</a> для клітинки (row, col) дошки: кожна клітинка 4 символи x 2 рядки, початок дошки на екранному рядку BOARD_TOP, стовпці BOARD_LEFT. Екранна позиція: screen_row = BOARD_TOP + row * 2, screen_col = BOARD_LEFT + col * 4. Зміщення у відеопам'яті: offset = (screen_row * 80 + screen_col) * 2. <br>
<a class="internal-link" data-href="How-to/random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">Випадковий вибір ШІ:</a> Підрахуйте легальні ходи в move_count, згенеруйте випадкове число R = timer_ticks MOD move_count (через INT 1Ah AH=00h), виконайте move_list[R]. Для пріоритету взяттям: спочатку шукайте ходи-взяття; якщо є — обирайте найціннішу здобич через piece_values DB 0,1,3,3,5,9,0 (індекс = тип фігури). Точки синхронізації:
Кінець тижня 9: узгодити кодування клітинок (бітові прапорці), формат move_list (4 байти: from_row, from_col, to_row, to_col), прототипи get_legal_moves/execute_move/is_in_check
Кінець тижня 11: перша спільна демонстрація — дошка рендериться зі стартовою позицією, кінь та король рухаються коректно, ковзні фігури працюють
Кінець тижня 12: перевірка шаху працює, нелегальні ходи фільтруються, ШІ робить ходи
Кінець тижня 13: мат/пат визначаються коректно, повна партія від початку до завершення; узгодити README
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/r07-chess.html</link><guid isPermaLink="false">Projects/R07-Chess.md</guid><pubDate>Sat, 28 Feb 2026 13:56:41 GMT</pubDate><enclosure url="assets/r07-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/r07-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Lab08]]></title><description><![CDATA[
Курс: Архітектура обчислювальних систем
Тиждень: 7*
Тривалість: 2 академічні години
Тип: Практична робота під наглядом інструктора
Після завершення практики студент зможе:
Записувати символи та атрибути кольору безпосередньо у відеопам'ять (B800:0000), без використання INT 10h.
Обчислювати зміщення у відеопам'яті за формулою (row × 80 + col) × 2.
Пояснити різницю між Memory-Mapped I/O (відеопам'ять) та Port-Mapped I/O (клавіатура).
Малювати рамки та кольорові текстові дисплеї засобами прямого запису.
Писати процедури для роботи з відеопам'яттю.
Перегляньте лекційні матеріали L08 (пристрої, ієрархія пам'яті). Зверніть увагу на розділ «Preparation for Lab 8»: відеопам'ять за адресою B800h, формула обчислення зміщення, memory-mapped I/O.У текстовому режимі (80×25) відеопам'ять починається за адресою B800:0000.Кожна позиція на екрані = 2 байти: символ + атрибут кольору.Адреса: B800:0000 B800:0002 B800:0004 ... ┌──┬──┐ ┌──┬──┐ ┌──┬──┐ │Ch│At│ │Ch│At│ │Ch│At│ └──┴──┘ └──┴──┘ └──┴──┘ (0,0) (0,1) (0,2)
Загальний розмір: 80 × 25 × 2 = 4000 байтів (0FA0h).offset = (row × 80 + col) × 2
mov ax, 0B800h
mov es, ax ; ES → відеопам'ять
Після цього запис через ES:[DI]:; Записати 'A' жовтим на синьому в позицію (0,0)
xor di, di ; offset = 0
mov al, 'A' ; символ
mov ah, 1Eh ; атрибут: жовтий (E) на синьому (1)
mov es:[di], ax ; [DI]=символ, [DI+1]=атрибут Чому це працює як одна інструкція MOV? Відеопам'ять — це Memory-Mapped I/O: адаптер дисплея проєктує свою пам'ять у адресний простір CPU. Тому звичайний MOV записує дані прямо в буфер відеоадаптера, а зображення на екрані оновлюється автоматично. Це на відміну від клавіатури, яка використовує Port-Mapped I/O (IN/OUT). Біт: 7 6 5 4 3 2 1 0 [BL] [ Фон ] [I] [ Текст ]
Приклади: 1Fh = білий на синьому, 4Eh = жовтий на червоному, 0Fh = яскраво-білий на чорному.
Примітка. У стандартному режимі DOSBox біт 7 = мерехтіння тексту (blink). Тому для фону доступні кольори 0–7. Яскраві фонові кольори (8–F) спричинять мерехтіння.
Повна таблиця символів (включно з псевдографікою): <a rel="noopener nofollow" class="external-link is-unresolved" href="https://www.asciitable.com/" target="_self">https://www.asciitable.com/</a> — коди 80h–FFh у секції «Extended».Створіть папку Lab08. Скопіюйте STUB.ASM з попередньої роботи.Для кожного завдання: копіюйте STUB.ASM у новий файл з відповідним ім'ям.Скопіюйте STUB.ASM → 1VRAM.ASM. Для цієї частини код не обов'язковий — головне дослідження у TD.Скомпілюйте та відкрийте 1VRAM.ASM у TD. Відкрийте вікно Dump (Alt+D або View → Dump).Перейдіть до адреси відеопам'яті: Ctrl+G → введіть 0B800:0000 (зверніть увагу на початковий 0 — TD потребує його для розпізнавання шістнадцяткового числа).
Важливо. Те, що ви бачите — це вміст відеобуфера в реальному часі. Оскільки TD сам малює свій інтерфейс у тій самій відеопам'яті, ви побачите байти інтерфейсу TD, а не «чистий» екран DOSBox. Якщо ви рухаєте курсор миші — атрибути змінюються (TD підсвічує елементи під курсором). Щоб побачити актуальний стан, натисніть F5 двічі (програма виконається до наступної точки і TD перечитає буфер).
Знайдіть:
Перший символ верхнього рядка. Який його ASCII-код? Який атрибут? (Це буде символ інтерфейсу TD.)
Перейдіть до зміщення 00A0 (0B800:00A0) — це початок другого рядка. Чому саме 160 (A0h)?
Спробуйте ідентифікувати фрагменти тексту інтерфейсу TD у байтах Dump.
Запишіть у коментарях 1VRAM.ASM: які байти ви бачите, як чергуються символи та атрибути (кожний парний байт — символ, непарний — атрибут), який атрибут використовує TD для свого інтерфейсу.Скопіюйте STUB.ASM → 2WRITE.ASM.Додайте до .CODE (після mov ds, ax): ; Set up ES for video memory mov ax, 0B800h mov es, ax ; Clear screen: fill with spaces, white on black xor di, di mov cx, 80 * 25 ; 2000 positions mov ax, 0720h ; AH=07 (white on black), AL=20h (space)
@cls: mov es:[di], ax add di, 2 loop @cls ; Write 'H' in white on blue at position (0,0) xor di, di ; offset = 0 mov al, 'H' mov ah, 1Fh ; white on blue mov es:[di], ax ; Write 'i' at position (0,1) mov di, 2 ; next position = offset 2 mov al, 'i' mov ah, 1Fh mov es:[di], ax
Запустіть (Ctrl+F9). Екран має очиститися, а у верхньому лівому куті з'явиться "Hi" білим на синьому.Додайте після попереднього коду: ; Write '*' in yellow on red at position (12, 40) — center of screen ; offset = (12 * 80 + 40) * 2 = 2000 = 07D0h mov di, 07D0h mov al, '*' mov ah, 4Eh ; yellow (E) on red (4) mov es:[di], ax
Запустіть — у центрі екрана має з'явитися жовта зірочка на червоному фоні.Самостійно: напишіть цикл, що виводить слово "HELLO" починаючи з позиції (12, 37) — приблизно центр екрана. Кожна літера — різним кольором.
Згадайте аналогічне завдання з P7 (INT 10h: курсор AH=02h + символ AH=09h). Тут замість двох переривань на кожну літеру — один MOV у відеопам'ять. Позиціонування курсора не потрібне: ви самі обчислюєте адресу.
Підказки:
Оголосіть у .DATA: hello DB 'HELLO' та colors DB 0Ch, 0Ah, 0Eh, 09h, 0Dh (5 різних кольорів)
Обчисліть початковий offset: (12 × 80 + 37) × 2 = 1994 = 07CAh
У циклі: читайте символ з hello, атрибут з colors, записуйте у ES:[DI], збільшуйте DI на 2
Скопіюйте STUB.ASM → 3VSTR.ASM.Напишіть процедуру vram_str, яка виводить null-terminated рядок (закінчується байтом 0, не $) у відеопам'ять.Параметри (стек):
[BP+4] = offset рядка (null-terminated)
[BP+6] = стовпець (col)
[BP+8] = рядок (row)
[BP+10] = атрибут кольору
Каркас:vram_str proc push bp mov bp, sp push ax push bx push si push di push es mov ax, 0B800h mov es, ax ; Calculate offset: (row * 80 + col) * 2 mov ax, [bp+8] ; row mov bx, 80 mul bx ; AX = row * 80 add ax, [bp+6] ; + col shl ax, 1 ; * 2 mov di, ax mov si, [bp+4] ; string offset mov ah, byte ptr [bp+10] ; attribute (stays in AH for entire loop) ; --- YOUR CODE HERE (5-6 lines) --- ; Loop through the string character by character: ; 1. LODSB — loads byte from [DS:SI] into AL, increments SI ; (DS points to data segment, so this reads the next char of the string) ; 2. Check if AL = 0 (null terminator) — if so, jump to @@done ; 3. Write AX to ES:[DI] — this writes char (AL) + attr (AH) to video memory ; (AH still holds the attribute from before the loop) ; 4. Advance DI by 2 (next screen position) ; 5. Repeat @@done: pop es pop di pop si pop bx pop ax pop bp ret
vram_str endp Примітка. LODSB читає байт з DS:SI → AL і збільшує SI, але не змінює AH. Оскільки DS вказує на сегмент даних, а ES — на відеопам'ять, ми одночасно читаємо рядок (DS:SI) і пишемо на екран (ES:DI). Кожна ітерація: AL = новий символ, AH = той самий атрибут → MOV ES:[DI], AX записує пару char+attr.
Додайте дані та виклики у main:.DATA
msg1 DB 'Hello, Video Memory!', 0
msg2 DB 'Direct writes are fast!', 0 .CODE ; ... (set up DS, clear screen as in Part 2) ... push 1Fh ; attribute: white on blue push 5 ; row 5 push 10 ; col 10 push offset msg1 call vram_str add sp, 8 push 2Ah ; attribute: green on black push 7 ; row 7 push 10 ; col 10 push offset msg2 call vram_str add sp, 8
Перевірте в TD та DOSBox.Додайте ще рядки та перевірте граничні випадки:
Рядок у позиції (0, 0)
Рядок у останньому рядку екрана (row = 24)
Порожній рядок (тільки байт 0)
Скопіюйте наступний код цілком у 4BOX.ASM:.MODEL small
.STACK 256 .DATA
BOX_R1 EQU 5 ; Top row
BOX_C1 EQU 20 ; Left column
BOX_R2 EQU 18 ; Bottom row
BOX_C2 EQU 60 ; Right column
BOX_CLR EQU 1Fh ; White on blue .CODE
main: mov ax, @data mov ds, ax mov ax, 0B800h mov es, ax ; === Clear screen === xor di, di mov cx, 80 * 25 mov ax, 0720h
@cls: mov es:[di], ax add di, 2 loop @cls ; === TOP BORDER (given) === mov di, (BOX_R1 * 80 + BOX_C1) * 2 mov ah, BOX_CLR mov al, 0C9h ; ╔ top-left corner mov es:[di], ax add di, 2 mov cx, BOX_C2 - BOX_C1 - 1 mov al, 0CDh ; ═ horizontal line
@top: mov es:[di], ax add di, 2 loop @top mov al, 0BBh ; ╗ top-right corner mov es:[di], ax ; === SIDE BORDERS === ; --- YOUR CODE HERE (8-10 lines) --- ; Draw ║ (BAh) at left (col=BOX_C1) and right (col=BOX_C2) ; for each row from BOX_R1+1 to BOX_R2-1. ; ; Approach: ; 1. Set DI = ((BOX_R1+1) * 80 + BOX_C1) * 2 ; 2. Set CX = BOX_R2 - BOX_R1 - 1 (number of rows) ; 3. In a loop: ; - Write ║ at ES:[DI] (left border) ; - Write ║ at ES:[DI + (BOX_C2-BOX_C1)*2] (right border) ; - Add 160 to DI (next row = 80*2) ; === BOTTOM BORDER === ; --- YOUR CODE HERE (6-8 lines) --- ; Same structure as top border, but use: ; ╚ (C8h) for bottom-left corner ; ═ (CDh) for horizontal line ; ╝ (BCh) for bottom-right corner ; ; Hint: start with DI = (BOX_R2 * 80 + BOX_C1) * 2, then ; repeat the same pattern as the top border with different corners. ; === Wait for keypress === mov ah, 00h int 16h mov ah, 4Ch int 21h END main
Ваше завдання: допишіть код у двох секціях YOUR CODE HERE:
Бічні рамки (8–10 рядків): цикл, що малює ║ зліва та справа для кожного рядка
Нижня рамка (6–8 рядків): аналогічно до верхньої, але з іншими кутами
Перевірка: запустіть програму. На екрані має з'явитися подвійна рамка білим на синьому.
Підказка. Відстань між лівою та правою рамкою: (BOX_C2 - BOX_C1) * 2 = 80 байтів у відеопам'яті. Відстань між рядками: 160 байтів (80 символів × 2 байти).
До кінця заняття:
1VRAM.ASM — дослідження відеопам'яті, спостереження у коментарях
2WRITE.ASM — прямий запис, очищення екрана, кольоровий рядок
3VSTR.ASM — процедура vram_str, тестування з різними рядками
4BOX.ASM — рамка з подвійною лінією (дописана)
Закомічено всі файли
Дедлайн: 23:59 у день перед Практикою 9Напишіть програму, що створює кольорову інформаційну панель на екрані, використовуючи тільки прямий запис у відеопам'ять:
Очистити екран (заповнити пробілами з атрибутом 07h)
<br>Намалювати подвійну рамку (╔═╗║╚╝, коди символів — див. <a data-tooltip-position="top" aria-label="https://www.asciitable.com/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.asciitable.com/" target="_self">asciitable.com</a>, секція Extended) з координатами верхнього лівого кута (3, 10) та нижнього правого (20, 70), атрибут — білий на синьому (1Fh)
Вивести заголовок на верхній лінії рамки, по центру — перезаписати частину символів ═ текстом заголовка (наприклад, ╔══════ My Panel ══════╗). Заголовок — тим самим атрибутом, що й рамка
Вивести щонайменше 4 рядки тексту всередині рамки, кожний з різним кольоровим атрибутом. Наприклад: Name: [ваше ім'я] — білий на чорному (0Fh)
Group: [ваша група] — зелений (0Ah)
Course: Computer Systems Architecture — жовтий (0Eh)
Lab: 08 - Video Memory — блакитний (0Bh) Вивести підказку Press any key to exit... в рядку 23 (поза рамкою), темно-сірим (08h)
Чекати на натискання клавіші (INT 16h), потім завершити програму
Вимоги:
Весь текстовий вивід — тільки через пряму відеопам'ять (НЕ використовувати INT 10h чи INT 21h/09h)
Використати процедуру vram_str з параметрами через стек (з Частини 3)
Мінімум 4 різні атрибути кольору
Дозволено INT 16h для клавіатури та INT 21h/4Ch для виходу
Тест: візуальна перевірка — рамка відображається правильно, текст читається, кольори різні, Q завершує програму.Поєднайте навички з P7 (перехоплення переривання) та P8 (пряма відеопам'ять):
Перехопіть INT 1Ch (таймер, ~18.2 Гц)
В обробнику таймера: зсувайте вміст одного рядка відеопам'яті (наприклад, рядок 24) на одну позицію вліво
Текст має «бігти» зліва направо з циклічним повтором
У головному циклі чекайте натискання Q для виходу
Відновіть оригінальний вектор перед завершенням
Підказки:
Зсув рядка: копіюйте байти ES:[DI+2] → ES:[DI] для всіх 80 позицій, останню позицію заповніть першим символом тексту (циклічний буфер)
Для зсуву використовуйте REP MOVSW або ручний цикл
Оскільки обробник не може використовувати INT 21h, пряма відеопам'ять — єдиний спосіб оновити екран з переривання
Обробник повинен зберігати та відновлювати всі регістри, що використовує (включно з ES, DI, SI, DS)
Запишіть відео, у якому:
Продемонструйте PANEL.ASM — покажіть результат: рамку, кольоровий текст, натискання клавіші
Поясніть формулу зміщення: як обчислити адресу у відеопам'яті для позиції (row, col)? Покажіть приклад
Поясніть: чим пряма робота з відеопам'яттю відрізняється від INT 10h? Коли краще використовувати кожний підхід?
УВАГА! Вимога про 5хв максимально важлива! Brevity is the soul of wit. Це частина оцінки.Lab08/
├── STUB.ASM ← шаблон
├── 1VRAM.ASM ← дослідження відеопам'яті
├── 2WRITE.ASM ← прямий запис у відеопам'ять
├── 3VSTR.ASM ← процедура vram_str
├── 4BOX.ASM ← малювання рамки (дописаний)
├── PANEL.ASM ← домашнє: інформаційна панель
├── MARQUEE.ASM ← бонус: біжучий рядок
└── video.txt ← посилання на Loom <br><a data-tooltip-position="top" aria-label="https://wiki.osdev.org/Text_UI" rel="noopener nofollow" class="external-link is-unresolved" href="https://wiki.osdev.org/Text_UI" target="_self">VGA Text Mode — OSDev Wiki</a> — повний опис текстового режиму VGA
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Code_page_437" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Code_page_437" target="_self">Code Page 437 — Wikipedia</a> — таблиця символів DOS (включно з рамковими)
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Memory-mapped_I/O_and_port-mapped_I/O" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Memory-mapped_I/O_and_port-mapped_I/O" target="_self">Memory-Mapped I/O — Wikipedia</a> — порівняння MMIO та PMIO
<br><a data-tooltip-position="top" aria-label="https://wiki.osdev.org/VGA_Hardware" rel="noopener nofollow" class="external-link is-unresolved" href="https://wiki.osdev.org/VGA_Hardware" target="_self">VGA Hardware — OSDev Wiki</a> — технічні деталі відеоадаптера
]]></description><link>labs/lab08.html</link><guid isPermaLink="false">Labs/Lab08.md</guid><pubDate>Sat, 28 Feb 2026 07:29:11 GMT</pubDate></item><item><title><![CDATA[L08-DOS-StringArray-Output]]></title><description><![CDATA[<img src="assets/l08-dos-stringarray-output.png" target="_self">]]></description><link>assets/l08-dos-stringarray-output.html</link><guid isPermaLink="false">assets/L08-DOS-StringArray-Output.png</guid><pubDate>Fri, 27 Feb 2026 18:41:41 GMT</pubDate><enclosure url="assets/l08-dos-stringarray-output.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l08-dos-stringarray-output.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L08-ASM-StringArray-Print]]></title><description><![CDATA[<img src="assets/l08-asm-stringarray-print.png" target="_self">]]></description><link>assets/l08-asm-stringarray-print.html</link><guid isPermaLink="false">assets/L08-ASM-StringArray-Print.png</guid><pubDate>Fri, 27 Feb 2026 18:41:41 GMT</pubDate><enclosure url="assets/l08-asm-stringarray-print.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l08-asm-stringarray-print.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L08-ASM-StringArray-Init]]></title><description><![CDATA[<img src="assets/l08-asm-stringarray-init.png" target="_self">]]></description><link>assets/l08-asm-stringarray-init.html</link><guid isPermaLink="false">assets/L08-ASM-StringArray-Init.png</guid><pubDate>Fri, 27 Feb 2026 18:41:41 GMT</pubDate><enclosure url="assets/l08-asm-stringarray-init.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l08-asm-stringarray-init.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L08-ASM-StringArray-Data]]></title><description><![CDATA[<img src="assets/l08-asm-stringarray-data.png" target="_self">]]></description><link>assets/l08-asm-stringarray-data.html</link><guid isPermaLink="false">assets/L08-ASM-StringArray-Data.png</guid><pubDate>Fri, 27 Feb 2026 18:41:41 GMT</pubDate><enclosure url="assets/l08-asm-stringarray-data.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l08-asm-stringarray-data.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L08-ASM-LinkedList-Traverse]]></title><description><![CDATA[<img src="assets/l08-asm-linkedlist-traverse.png" target="_self">]]></description><link>assets/l08-asm-linkedlist-traverse.html</link><guid isPermaLink="false">assets/L08-ASM-LinkedList-Traverse.png</guid><pubDate>Fri, 27 Feb 2026 18:41:41 GMT</pubDate><enclosure url="assets/l08-asm-linkedlist-traverse.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l08-asm-linkedlist-traverse.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L08-ASM-LinkedList-Init]]></title><description><![CDATA[<img src="assets/l08-asm-linkedlist-init.png" target="_self">]]></description><link>assets/l08-asm-linkedlist-init.html</link><guid isPermaLink="false">assets/L08-ASM-LinkedList-Init.png</guid><pubDate>Fri, 27 Feb 2026 18:41:41 GMT</pubDate><enclosure url="assets/l08-asm-linkedlist-init.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l08-asm-linkedlist-init.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L08-ASM-LinkedList-Define]]></title><description><![CDATA[<img src="assets/l08-asm-linkedlist-define.png" target="_self">]]></description><link>assets/l08-asm-linkedlist-define.html</link><guid isPermaLink="false">assets/L08-ASM-LinkedList-Define.png</guid><pubDate>Fri, 27 Feb 2026 18:41:41 GMT</pubDate><enclosure url="assets/l08-asm-linkedlist-define.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l08-asm-linkedlist-define.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L08-ASM-Struct-Annotated]]></title><description><![CDATA[<img src="assets/l08-asm-struct-annotated.png" target="_self">]]></description><link>assets/l08-asm-struct-annotated.html</link><guid isPermaLink="false">assets/L08-ASM-Struct-Annotated.png</guid><pubDate>Fri, 27 Feb 2026 18:41:41 GMT</pubDate><enclosure url="assets/l08-asm-struct-annotated.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l08-asm-struct-annotated.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L08-ASM-Struct-Define]]></title><description><![CDATA[<img src="assets/l08-asm-struct-define.png" target="_self">]]></description><link>assets/l08-asm-struct-define.html</link><guid isPermaLink="false">assets/L08-ASM-Struct-Define.png</guid><pubDate>Fri, 27 Feb 2026 18:41:41 GMT</pubDate><enclosure url="assets/l08-asm-struct-define.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l08-asm-struct-define.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L08-ASM-2D-Array-Access]]></title><description><![CDATA[<img src="assets/l08-asm-2d-array-access.png" target="_self">]]></description><link>assets/l08-asm-2d-array-access.html</link><guid isPermaLink="false">assets/L08-ASM-2D-Array-Access.png</guid><pubDate>Fri, 27 Feb 2026 18:41:41 GMT</pubDate><enclosure url="assets/l08-asm-2d-array-access.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l08-asm-2d-array-access.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L08-ASM-2D-Array-Declare]]></title><description><![CDATA[<img src="assets/l08-asm-2d-array-declare.png" target="_self">]]></description><link>assets/l08-asm-2d-array-declare.html</link><guid isPermaLink="false">assets/L08-ASM-2D-Array-Declare.png</guid><pubDate>Fri, 27 Feb 2026 18:41:41 GMT</pubDate><enclosure url="assets/l08-asm-2d-array-declare.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l08-asm-2d-array-declare.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L08-ASM-1D-Array]]></title><description><![CDATA[<img src="assets/l08-asm-1d-array.png" target="_self">]]></description><link>assets/l08-asm-1d-array.html</link><guid isPermaLink="false">assets/L08-ASM-1D-Array.png</guid><pubDate>Fri, 27 Feb 2026 18:41:41 GMT</pubDate><enclosure url="assets/l08-asm-1d-array.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l08-asm-1d-array.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L08-Paging-Address-Translation]]></title><description><![CDATA[<img src="assets/l08-paging-address-translation.jpeg" target="_self">]]></description><link>assets/l08-paging-address-translation.html</link><guid isPermaLink="false">assets/L08-Paging-Address-Translation.jpeg</guid><pubDate>Fri, 27 Feb 2026 18:41:41 GMT</pubDate><enclosure url="assets/l08-paging-address-translation.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l08-paging-address-translation.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L08-Virtual-Memory-Mapping]]></title><description><![CDATA[<img src="assets/l08-virtual-memory-mapping.png" target="_self">]]></description><link>assets/l08-virtual-memory-mapping.html</link><guid isPermaLink="false">assets/L08-Virtual-Memory-Mapping.png</guid><pubDate>Fri, 27 Feb 2026 18:41:41 GMT</pubDate><enclosure url="assets/l08-virtual-memory-mapping.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l08-virtual-memory-mapping.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L08-Memory-Hierarchy-Detailed]]></title><description><![CDATA[<img src="assets/l08-memory-hierarchy-detailed.png" target="_self">]]></description><link>assets/l08-memory-hierarchy-detailed.html</link><guid isPermaLink="false">assets/L08-Memory-Hierarchy-Detailed.png</guid><pubDate>Fri, 27 Feb 2026 18:41:41 GMT</pubDate><enclosure url="assets/l08-memory-hierarchy-detailed.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l08-memory-hierarchy-detailed.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L08-Cache-Bandwidth-Latency]]></title><description><![CDATA[<img src="assets/l08-cache-bandwidth-latency.png" target="_self">]]></description><link>assets/l08-cache-bandwidth-latency.html</link><guid isPermaLink="false">assets/L08-Cache-Bandwidth-Latency.png</guid><pubDate>Fri, 27 Feb 2026 18:41:41 GMT</pubDate><enclosure url="assets/l08-cache-bandwidth-latency.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l08-cache-bandwidth-latency.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L08-Memory-Speed-Pyramid]]></title><description><![CDATA[<img src="assets/l08-memory-speed-pyramid.jpeg" target="_self">]]></description><link>assets/l08-memory-speed-pyramid.html</link><guid isPermaLink="false">assets/L08-Memory-Speed-Pyramid.jpeg</guid><pubDate>Fri, 27 Feb 2026 18:41:41 GMT</pubDate><enclosure url="assets/l08-memory-speed-pyramid.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l08-memory-speed-pyramid.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L08-Cache-Multicore]]></title><description><![CDATA[<img src="assets/l08-cache-multicore.png" target="_self">]]></description><link>assets/l08-cache-multicore.html</link><guid isPermaLink="false">assets/L08-Cache-Multicore.png</guid><pubDate>Fri, 27 Feb 2026 18:41:41 GMT</pubDate><enclosure url="assets/l08-cache-multicore.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l08-cache-multicore.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L08-Cache-Hierarchy-Single]]></title><description><![CDATA[<img src="assets/l08-cache-hierarchy-single.png" target="_self">]]></description><link>assets/l08-cache-hierarchy-single.html</link><guid isPermaLink="false">assets/L08-Cache-Hierarchy-Single.png</guid><pubDate>Fri, 27 Feb 2026 18:41:41 GMT</pubDate><enclosure url="assets/l08-cache-hierarchy-single.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l08-cache-hierarchy-single.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L08-Memory-Hierarchy-Pyramid]]></title><description><![CDATA[<img src="assets/l08-memory-hierarchy-pyramid.png" target="_self">]]></description><link>assets/l08-memory-hierarchy-pyramid.html</link><guid isPermaLink="false">assets/L08-Memory-Hierarchy-Pyramid.png</guid><pubDate>Fri, 27 Feb 2026 18:41:41 GMT</pubDate><enclosure url="assets/l08-memory-hierarchy-pyramid.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l08-memory-hierarchy-pyramid.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L08-Motherboard-Closeup]]></title><description><![CDATA[<img src="assets/l08-motherboard-closeup.jpeg" target="_self">]]></description><link>assets/l08-motherboard-closeup.html</link><guid isPermaLink="false">assets/L08-Motherboard-Closeup.jpeg</guid><pubDate>Fri, 27 Feb 2026 18:41:41 GMT</pubDate><enclosure url="assets/l08-motherboard-closeup.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l08-motherboard-closeup.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L08-IO-Port-Address-List]]></title><description><![CDATA[<img src="assets/l08-io-port-address-list.png" target="_self">]]></description><link>assets/l08-io-port-address-list.html</link><guid isPermaLink="false">assets/L08-IO-Port-Address-List.png</guid><pubDate>Fri, 27 Feb 2026 18:41:41 GMT</pubDate><enclosure url="assets/l08-io-port-address-list.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l08-io-port-address-list.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L08-Keyboard-Commands-Full]]></title><description><![CDATA[<img src="assets/l08-keyboard-commands-full.png" target="_self">]]></description><link>assets/l08-keyboard-commands-full.html</link><guid isPermaLink="false">assets/L08-Keyboard-Commands-Full.png</guid><pubDate>Fri, 27 Feb 2026 18:41:41 GMT</pubDate><enclosure url="assets/l08-keyboard-commands-full.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l08-keyboard-commands-full.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L08-Keyboard-Commands-Table]]></title><description><![CDATA[<img src="assets/l08-keyboard-commands-table.png" target="_self">]]></description><link>assets/l08-keyboard-commands-table.html</link><guid isPermaLink="false">assets/L08-Keyboard-Commands-Table.png</guid><pubDate>Fri, 27 Feb 2026 18:41:41 GMT</pubDate><enclosure url="assets/l08-keyboard-commands-table.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l08-keyboard-commands-table.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L08-ASM-Set-LEDs]]></title><description><![CDATA[<img src="assets/l08-asm-set-leds.png" target="_self">]]></description><link>assets/l08-asm-set-leds.html</link><guid isPermaLink="false">assets/L08-ASM-Set-LEDs.png</guid><pubDate>Fri, 27 Feb 2026 18:41:41 GMT</pubDate><enclosure url="assets/l08-asm-set-leds.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l08-asm-set-leds.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L08-Scancode-Table]]></title><description><![CDATA[<img src="assets/l08-scancode-table.png" target="_self">]]></description><link>assets/l08-scancode-table.html</link><guid isPermaLink="false">assets/L08-Scancode-Table.png</guid><pubDate>Fri, 27 Feb 2026 18:41:41 GMT</pubDate><enclosure url="assets/l08-scancode-table.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l08-scancode-table.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L08-ASM-Read-Scancode]]></title><description><![CDATA[<img src="assets/l08-asm-read-scancode.png" target="_self">]]></description><link>assets/l08-asm-read-scancode.html</link><guid isPermaLink="false">assets/L08-ASM-Read-Scancode.png</guid><pubDate>Fri, 27 Feb 2026 18:41:41 GMT</pubDate><enclosure url="assets/l08-asm-read-scancode.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l08-asm-read-scancode.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L08-Keyboard-Matrix-Block]]></title><description><![CDATA[<img src="assets/l08-keyboard-matrix-block.png" target="_self">]]></description><link>assets/l08-keyboard-matrix-block.html</link><guid isPermaLink="false">assets/L08-Keyboard-Matrix-Block.png</guid><pubDate>Fri, 27 Feb 2026 18:41:41 GMT</pubDate><enclosure url="assets/l08-keyboard-matrix-block.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l08-keyboard-matrix-block.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L08-Keyboard-Controller-Block]]></title><description><![CDATA[<img src="assets/l08-keyboard-controller-block.png" target="_self">]]></description><link>assets/l08-keyboard-controller-block.html</link><guid isPermaLink="false">assets/L08-Keyboard-Controller-Block.png</guid><pubDate>Fri, 27 Feb 2026 18:41:41 GMT</pubDate><enclosure url="assets/l08-keyboard-controller-block.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l08-keyboard-controller-block.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L08-CPU-Bus-IO-Highlight]]></title><description><![CDATA[<img src="assets/l08-cpu-bus-io-highlight.png" target="_self">]]></description><link>assets/l08-cpu-bus-io-highlight.html</link><guid isPermaLink="false">assets/L08-CPU-Bus-IO-Highlight.png</guid><pubDate>Fri, 27 Feb 2026 18:41:41 GMT</pubDate><enclosure url="assets/l08-cpu-bus-io-highlight.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l08-cpu-bus-io-highlight.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L08-CPU-Bus-Diagram]]></title><description><![CDATA[<img src="assets/l08-cpu-bus-diagram.png" target="_self">]]></description><link>assets/l08-cpu-bus-diagram.html</link><guid isPermaLink="false">assets/L08-CPU-Bus-Diagram.png</guid><pubDate>Fri, 27 Feb 2026 18:41:41 GMT</pubDate><enclosure url="assets/l08-cpu-bus-diagram.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l08-cpu-bus-diagram.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L08-Mechanical-Keyboard]]></title><description><![CDATA[<img src="assets/l08-mechanical-keyboard.png" target="_self">]]></description><link>assets/l08-mechanical-keyboard.html</link><guid isPermaLink="false">assets/L08-Mechanical-Keyboard.png</guid><pubDate>Fri, 27 Feb 2026 18:41:41 GMT</pubDate><enclosure url="assets/l08-mechanical-keyboard.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l08-mechanical-keyboard.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L08-Network-Switch]]></title><description><![CDATA[<img src="assets/l08-network-switch.jpeg" target="_self">]]></description><link>assets/l08-network-switch.html</link><guid isPermaLink="false">assets/L08-Network-Switch.jpeg</guid><pubDate>Fri, 27 Feb 2026 18:41:41 GMT</pubDate><enclosure url="assets/l08-network-switch.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l08-network-switch.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[G08-Morse-Code-Translator]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🟢 Зелений
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 400-550 рядків коду (соло) / 650-900 рядків коду (пара), 2-5 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиці пошуку та індексований доступ</a> — таблиця азбуки Морзе як масив записів змінної довжини або таблиця фіксованого розміру; XLAT або обчислювана адресація — L06/P06
<br><a class="internal-link" data-href="How-to/pc-speaker.md" href="projects/how-to/pc-speaker.html" target="_self" rel="noopener nofollow">Портовий ввід/вивід для PC Speaker</a> — пряме керування апаратурою через інструкції OUT до портів 42h, 43h та 61h — L07 (взаємодія з апаратурою)
<br><a class="internal-link" data-href="How-to/timer-delays.md" href="projects/how-to/timer-delays.html" target="_self" rel="noopener nofollow">Таймінг та цикли затримки</a> — контроль тривалості звукового сигналу каліброваними циклами затримки — L04/P04 (конструкції циклів)
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Обробка рядків</a> — розбір послідовностей крапок/тире, побудова вихідних рядків символ за символом — L06/P06
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури та модульний дизайн</a> — окремі процедури для кодування, декодування, відтворення тону, оновлення дисплея — L05/P05
<br><a class="internal-link" data-href="How-to/bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Побітові операції</a> — маніпуляція бітами порту 61h для увімкнення/вимкнення динаміка; потенційне компактне кодування Морзе за допомогою бітових шаблонів — L04/P04
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять (B800h)</a> — двопанельне відображення з кольоровими атрибутами — L07/P07
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Використання стека для передачі параметрів</a> — передача кодів символів, тривалостей та позицій відображення процедурам — L05/P05
<br><a class="internal-link" data-href="How-to/game-logic/G08.md" href="projects/how-to/game-logic/g08.html" target="_self" rel="noopener nofollow">Ігрова логіка: перекладач Морзе</a> — таблиця Морзе, кодування/декодування, таймінг ITU, керування PC Speaker
Програма є двонаправленим перекладачем азбуки Морзе, що працює в текстовому режимі DOSBox з аудіовиводом через PC Speaker. Екран поділено на дві половини: ліва панель показує простий текст, а права — відповідний код Морзе (крапки та тире). У режимі тексту в Морзе користувач вводить повідомлення символ за символом. Коли кожна буква вводиться, її еквівалент Морзе з'являється на правій панелі (наприклад, .- .-.. .-.. для «ALL»), а PC Speaker відтворює відповідну послідовність коротких сигналів (крапки) та довших сигналів (тире) з відповідними паузами між ними.У режимі Морзе в текст користувач вводить код Морзе, набираючи крапки (.) та тире (-), натискаючи Space для розділення букв та подвійний Space (або /) для розділення слів. Коли код кожної букви завершений, декодований символ з'являється на лівій текстовій панелі. Таблиця пошуку охоплює A-Z та 0-9 (36 символів). Довідкова таблиця з повним алфавітом Морзе відображається внизу екрана для зручності.Регулювання швидкості дозволяє користувачу збільшити або зменшити тривалість сигналу (WPM — слів за хвилину) клавішами +/-. Поточне налаштування швидкості показується в рядку стану. Візуальне відображення використовує кольорові атрибути: крапки та тире рендеряться різними кольорами (наприклад, жовті крапки, блакитні тире), щоб зробити шаблон коду візуально зрозумілим навіть без звуку.<br><img alt="Перекладач азбуки Морзе — текстова та кодова панелі з довідковою таблицею" src="assets/g08-screen-translator.png" target="_self">
Підхід A: Таблиця записів фіксованого розміру зберігає кожну букву з її рядком Морзе, доповненим до максимальної довжини (наприклад, DB 'A', '.- ', 0 з 6-байтовим полем Морзе).
Підхід B: Кодування двійковим деревом використовує саму структуру азбуки Морзе: починаємо з кореневого вузла, рухаємось ліворуч для крапки, праворуч для тире — буква у вузлі призначення є декодованим символом. Деревоподібний підхід безпосередньо відображає те, як була спроєктована азбука Морзе.
Що виміряти/спостерігати: Використання пам'яті, швидкість пошуку для кодування (текст у Морзе вимагає пошуку в обох, але дерево — O(довжина_коду) для декодування), складність коду. Підхід A: Цикл затримки (MOV CX, N / delay: LOOP delay) створює затримку, пропорційну N, але фактична тривалість залежить від швидкості ЦП (яка варіюється в налаштуваннях DOSBox).
Підхід B: Зчитування лічильника тактів BIOS (18,2 тактів/сек за адресою 0040h:006Ch) та очікування проходження цільової кількості тактів.
Що виміряти/спостерігати: Точність, портативність між налаштуваннями швидкості DOSBox (спробуйте Normal проти Max), складність коду. Виміряйте фактичну тривалість сигналу секундоміром при різних налаштуваннях CPU cycles DOSBox. Підхід A: Прямий доступ до портів програмує таймер 8253 (порт 43h для встановлення режиму, порт 42h для встановлення дільника частоти, порт 61h для увімкнення динаміка). Це дає точний контроль частоти та тривалості.
Підхід B: Можна спробувати простіші підходи (наприклад, на рівні BIOS або через абстраговані процедури).
Що виміряти/спостерігати: Кількість рядків коду, якість звукового виводу та чи є таймінг надійнішим в одному підході. Метод прямого доступу до портів потребує більше коду, але дає точний контроль.
<br>Розподіл модулів (див. <a class="internal-link" data-href="How-to/include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">Підключення файлів: INCLUDE</a>):
Студент A: Рушій кодування/декодування та аудіо — таблиця пошуку Морзе, процедура кодування тексту в Морзе, процедура декодування Морзе в текст, керування PC Speaker (генерація тону, таймінг). Відповідає за структуру даних таблиці Морзе, всю логіку перетворення та код маніпулювання портами динаміка.
Студент B: Відображення та взаємодія з користувачем — розміщення екрана (дві панелі, довідкова таблиця, рядок стану), обробка клавіатурного вводу, перемикання режимів, інтерфейс регулювання швидкості, кольорове відображення Морзе. Відповідає за всі записи у відеопам'ять та головний цикл вводу.
Точки інтеграції: Студент A надає процедури на кшталт encode_char(char) -&gt; morse_string_ptr, decode_morse(morse_string_ptr) -&gt; char та play_morse(morse_string_ptr, speed). Студент B викликає їх з циклу вводу та рендерить результати. Спільний буфер (наприклад, current_morse DB 8 DUP(0)) передає закодований рядок Морзе між модулями.
Паралельна розробка: Студент A може тестувати кодування/декодування простим консольним виводом через INT 21h та тестувати аудіо незалежно. Студент B може побудувати повний інтерфейс з жорстко заданими зразковими даними до інтеграції. <br><a class="internal-link" data-href="How-to/game-logic/G08.md" href="projects/how-to/game-logic/g08.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: структура таблиці Морзе, кодування тексту в Морзе, декодування назад, таймінг ITU (1/3/7), керування PC Speaker.
<br><a class="internal-link" data-href="How-to/lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Розміщення таблиці Морзе:</a> Один підхід: morse_table DB 'A', 2, '.', '-', 0, 0, 0, 0 — кожний запис 8 байтів (1 буква, 1 довжина, до 6 символів коду). Це дозволяє пряму індексацію: запис для букви N знаходиться за morse_table + (N - 'A') * 8. Цифри (0-9) слідують після 26 записів букв.
Примітка щодо програмування динаміка: Підказки для реалізації нижче надають точні послідовності портового вводу/виводу для генерації тону. Дотримуйтесь їх покроково. Розуміння внутрішньої роботи таймера PIT є цінним, але не обов'язковим для Зеленої складності — надані послідовності працюватимуть у DOSBox.
<br><a class="internal-link" data-href="How-to/pc-speaker.md" href="projects/how-to/pc-speaker.html" target="_self" rel="noopener nofollow">Генерація тону PC Speaker:</a> Щоб створити тон: (1) Відправте командний байт у порт 43h: MOV AL, 0B6h / OUT 43h, AL. (2) Відправте дільник частоти у порт 42h (молодший байт, потім старший): дільник = 1193180 / бажана_частота. Для тону ~800 Гц: дільник = 1491 = 05D3h. (3) Увімкніть динамік: IN AL, 61h / OR AL, 03h / OUT 61h, AL. Щоб зупинити: IN AL, 61h / AND AL, 0FCh / OUT 61h, AL.
<br><a class="internal-link" data-href="How-to/timer-delays.md" href="projects/how-to/timer-delays.html" target="_self" rel="noopener nofollow">Таймінг крапки/тире:</a> Визначте базову одиницю (наприклад, DOT_DURATION EQU 3 в тактах BIOS, приблизно 165 мс). Тире = 3 одиниці, внутрішньосимвольний проміжок = 1 одиниця, міжбуквений проміжок = 3 одиниці, міжслівний проміжок = 7 одиниць. Процедура затримки зчитує лічильник тактів BIOS та циклічно чекає, доки не мине цільова кількість тактів.
<br><a class="internal-link" data-href="How-to/lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Декодування Морзе в текст:</a> Збирайте крапки та тире у буфер, доки користувач не натисне Space. Потім шукайте відповідний запис у таблиці Морзе. Лінійний пошук по 36 записах достатньо швидкий.
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Кольорові крапки та тире у відеопам'яті:</a> Жовтий на чорному = 0Eh, Блакитний на чорному = 0Bh. Записуйте символи крапок з одним атрибутом, а тире — з іншим, розміщуючи їх на панелі Морзе.
<br><a class="internal-link" data-href="How-to/bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Перетворення регістру для вводу:</a> Якщо користувач набирає малими літерами, конвертуйте у великі через AND AL, 0DFh перед пошуком у таблиці Морзе.
Довідкова таблиця: Згенеруйте її програмно при запуску, перебираючи таблицю Морзе та записуючи кожний запис у обчислену позицію екрана. Це уникає жорстко заданих даних екрана та забезпечує узгодженість з фактичною таблицею, що використовується для перекладу.
Точки синхронізації:
Кінець тижня 8: узгодити формат morse_table (розмір запису, порядок полів), спільний буфер current_morse, інтерфейси процедур (encode_char, decode_morse, play_morse)
Кінець тижня 9: обидва модулі компілюються окремо; PC Speaker працює (тестовий сигнал); include-файл з константами панелей та таймінгу зафіксовано
Кінець тижня 11: перша інтеграція — режим «текст у Морзе» працює наскрізно (ввід → кодування → звук → відображення)
Кінець тижня 13: код зафіксовано; обидва режими працюють; динамік гарантовано вимикається при будь-якому варіанті виходу
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/g08-morse-code-translator.html</link><guid isPermaLink="false">Projects/G08-Morse-Code-Translator.md</guid><pubDate>Tue, 24 Feb 2026 16:03:06 GMT</pubDate><enclosure url="assets/g08-screen-translator.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/g08-screen-translator.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y23-Calendar-Planner]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🟡 Жовтий
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 450-600 (соло) / 650-850 (пара), 2-5 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/arithmetic.md" href="projects/how-to/arithmetic.html" target="_self" rel="noopener nofollow">Цілочисельна арифметика</a> для обчислення дат (конґруенція Целлера: множення, ділення, операції MOD, що відповідають MUL, DIV на 8086) — L04/P04
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять</a> (ES=B800h, відображення сітки календаря з рамковими символами та кольоровими атрибутами) — L08/P08
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви записів фіксованого розміру</a> (записи подій по 45 байтів, обчислення зміщень з MUL та ADD) — L06/P06
<br><a class="internal-link" data-href="How-to/addressing-modes.md" href="projects/how-to/addressing-modes.html" target="_self" rel="noopener nofollow">Режими адресації</a> (база+індекс+зміщення для доступу до полів у записах подій) — L06/P06
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури зі стековими фреймами</a> (обчислення дня тижня, відображення сітки, CRUD подій, файловий ввід/вивід як окремі процедури) — L05/P05
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід через INT 16h</a> (клавіші-стрілки, Page Up/Down, Enter, літерні клавіші для команд) — L07/P07
<br><a class="internal-link" data-href="How-to/dos-services.md" href="projects/how-to/dos-services.html" target="_self" rel="noopener nofollow">Сервіси DOS</a> — INT 21h AH=2Ah для читання системної дати, <a class="internal-link" data-href="How-to/file-io.md" href="projects/how-to/file-io.html" target="_self" rel="noopener nofollow">файловий ввід/вивід</a> для збереження — L07/P07, L09/P09
<br><a class="internal-link" data-href="How-to/flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Умовна логіка</a> (<a class="internal-link" data-href="How-to/lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">таблиця пошуку</a> довжини місяця, перевірка високосного року, перевірка меж для навігації) — L04/P04
<br><a class="internal-link" data-href="How-to/lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиці пошуку</a> (назви місяців, дні в місяці, назви днів тижня) — L06/P06
<br><a class="internal-link" data-href="How-to/segment-registers.md" href="projects/how-to/segment-registers.html" target="_self" rel="noopener nofollow">Сегментні регістри</a> (DS для даних, ES для відеопам'яті) — L03/P03
<br><a class="internal-link" data-href="How-to/game-logic/Y23.md" href="projects/how-to/game-logic/y23.html" target="_self" rel="noopener nofollow">Ігрова логіка: календар</a> — конґруенція Целлера, перевірка високосного року, навігація між місяцями, зберігання подій у масиві записів
Програма реалізує інтерактивний календарний додаток у текстовому режимі DOSBox. При запуску на екрані відображається поточний місяць у знайомій сітковій розкладці: рядок заголовка з назвою місяця та роком ("February 2026"), рядок скорочень днів тижня (Mon, Tue, ..., Sun) та 4-6 рядків числових дат, розташованих у 7-стовпцевій сітці. Сітка відображається безпосередньо у відеопам'яті за адресою B800h з рамковими символами, що утворюють межі комірок. Кожна комірка дати достатньо велика (приблизно 10 символів шириною та 3 рядки заввишки), щоб показати номер дня та невеликий маркер, що вказує, чи існують події для цієї дати.Навігація інтуїтивна: клавіші-стрілки вліво та вправо переміщують підсвічування між днями місяця, а спеціальні клавіші (напр., Page Up / Page Down або &lt; / &gt;) перемикають на попередній або наступний місяць. Календар коректно обробляє різну довжину місяців (28-31 день), високосні роки та початковий день тижня для будь-якого місяця — обчислений за допомогою цілочисельного алгоритму, такого як конґруенція Целлера, що вимагає лише додавання, віднімання, множення та ділення (MOD). Сьогоднішня дата (жорстко задана або прочитана з DOS через INT 21h AH=2Ah) підсвічена окремим кольором фону. Дати, до яких прив'язані події, позначені кольоровою крапкою або символом.Натискання Enter на вибраній даті відкриває денний перегляд, що показує всі події для цієї дати. Кожна подія — це запис фіксованого розміру: дата (3 байти: зміщення року, місяць, день), заголовок (до 40 байтів) та час (2 байти: година, хвилина) — 45 байтів на подію. Події зберігаються у масиві в пам'яті та зберігаються у файлі через INT 21h. Користувач може додавати, редагувати та видаляти події. Рядок стану внизу показує вибрану дату, кількість подій та доступні команди. Кольорове кодування розрізняє вихідні (напр., червоні номери днів), поточну дату (яскраво-жовте підсвічування) та дні з подіями (зелений маркер).<br><img alt="Місячний перегляд календаря з сіткою та маркерами подій" src="assets/y23-screen-monthly.png" target="_self"><br><img alt="Денний перегляд зі списком подій" src="assets/y23-screen-dayview.png" target="_self">
Примітка: Файловий ввід/вивід (файлові сервіси INT 21h) розглядається у L09/P09 (тиждень 9). Протягом тижнів 7-8 зосередьтеся на основній логіці з жорстко заданими тестовими даними. Реалізуйте файлові операції, коли матеріал P09 стане доступним. Підхід A: Реалізує формулу Целлера безпосередньо: h = (q + floor(13(m+1)/5) + K + floor(K/4) + floor(J/4) - 2J) mod 7, де кожен доданок потребує інструкцій MUL та DIV на 8086.
Підхід B: Обчислює загальну кількість днів від відомої дати (напр., 1 січня 2000 року, що було суботою) сумуючи роки * 365 + високосні дні + дні попередніх місяців + день, потім бере MOD 7.
Що виміряти/спостерігати: Кількість інструкцій MUL та DIV на обчислення (Целлер використовує близько 5; кумулятивний — більше, але з простішими окремими кроками), читабельність коду та легкість перевірки коректності (протестувати обидва з відомими датами, як 1 січня 2026 = четвер). Обговорити: який підхід краще узагальнюється для обчислення дня року або кількості днів між двома датами. Підхід A: Попередньо обчислює таблицю з 42 записів (6 рядків x 7 стовпців) зміщень на екрані, по одному на комірку в сітці календаря. Малювання дня N шукає зміщення з таблиці та записує безпосередньо.
Підхід B: Обчислює зміщення на екрані під час виконання для кожної комірки: offset = grid_start + row * row_stride + col * cell_width * 2.
Що виміряти/спостерігати: Витрати пам'яті (42 слова = 84 байти для таблиці проти 0 байтів), швидкість рендерингу (пошук у таблиці — 1 MOV проти кілька MUL/ADD) та гнучкість при зміні розміру комірки (підхід A потребує перегенерації таблиці; підхід B лише змінює константу). Підхід A: Сканує весь масив подій для кожної дати: цикл через усі 50 записів, порівнюючи 3-байтове поле дати.
Підхід B: Створює індекс на 366 записів (по одному на день року), що зберігає зміщення першої події для цієї дати, формуючи просту хеш/кошикову структуру.
Що виміряти/спостерігати: Кількість інструкцій для пошуку подій на задану дату (підхід A: до 50 порівнянь; підхід B: один пошук + короткий ланцюжок), витрати пам'яті (підхід B використовує 732 додаткових байти для індексу) та складність підтримки індексу при додаванні або видаленні подій. Виміряти загальну кількість інструкцій для рендерингу місячного перегляду з 15 подіями, розподіленими по датах.
<br>Розподіл модулів (див. <a class="internal-link" data-href="How-to/include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">Підключення файлів: INCLUDE</a>):
Студент A — Модуль логіки календаря: обчислення дня тижня (Целлер або еквівалент), таблиця пошуку довжини місяця з обробкою високосного року, навігація по датах (наступний/попередній місяць, межі вибору дня), порівняння дат для зіставлення подій. Файл: calendar.asm.
Студент B — Модуль відображення та подій: відображення сітки місяця (рамкові символи, номери днів, підсвічування, маркери подій), керування записами подій (додавання, видалення, підтримка масиву), відображення списку подій (денний перегляд), збереження/завантаження файлу, клавіатурний ввід та диспетчеризація команд. Файл: display.asm.
Спільний головний модуль: ініціалізація (читання системної дати через INT 21h, завантаження подій з файлу), головний цикл подій (переходи місячний перегляд &lt;-&gt; денний перегляд), спільні оголошення даних. Файл: main.asm.
Точки інтеграції:
Узгодити спільні змінні: cur_year (слово), cur_month (байт), cur_day (байт, вибраний день), today_year/month/day (фактична поточна дата), масив events, event_count (слово).
Студент A надає get_day_of_week(year, month, day) -&gt; AL та get_month_length(year, month) -&gt; AL. Студент B викликає їх при відображенні сітки.
Студент B надає has_events(year, month, day) -&gt; ZF для можливого використання студентом A, або студент B використовує це внутрішньо при відображенні комірок дат.
Визначити формат запису подій спільно: year_off DB ? (зміщення від 2000), month DB ?, day DB ?, hour DB ?, minute DB ?, title DB 40 DUP(0) = 45 байтів на запис. <br><a class="internal-link" data-href="How-to/game-logic/Y23.md" href="projects/how-to/game-logic/y23.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: конґруенція Целлера, перевірка високосного року, навігація між місяцями, зберігання подій.
<br><a class="internal-link" data-href="How-to/dos-services.md" href="projects/how-to/dos-services.html" target="_self" rel="noopener nofollow">Читання системної дати:</a> MOV AH, 2Ah / INT 21h повертає: CX = рік (напр., 2026), DH = місяць (1-12), DL = день (1-31), AL = день тижня (0=неділя). Це дає вам «сьогодні» для підсвічування.
<br><a class="internal-link" data-href="How-to/arithmetic.md" href="projects/how-to/arithmetic.html" target="_self" rel="noopener nofollow">Конґруенція Целлера</a> (спрощена для 2000-2099):
; Вхідні дані: year (слово), month (байт 1-12), day (байт 1-31)
; Якщо month &lt;= 2: month += 12, year -= 1
; q = day, m = month, K = year MOD 100, J = year / 100
; h = (q + 13*(m+1)/5 + K + K/4 + J/4 + 5*J) MOD 7
; Результат: h=0 субота, 1 неділя, ..., 6 п'ятниця Кожен доданок використовує MUL або DIV з константними дільниками. Використовувати регістр для накопичення суми, додаючи кожен доданок.
<br><a class="internal-link" data-href="How-to/lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиця довжини місяців:</a> month_days DB 31,28,31,30,31,30,31,31,30,31,30,31. Для лютого у високосному році додати 1 до значення за індексом 1. Перевірка високосного року: year AND 3 = 0 означає ділиться на 4 (достатньо для 2000-2099).
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Розкладка сітки:</a> 7-стовпцева сітка з комірками шириною 10 символів займає 70 символів + 8 символів рамки = 78 стовпців (центровано з 1 стовпцем відступу). Кожна комірка висотою 3 рядки: верхня рамка, рядок вмісту (номер дня + маркер подій), нижня рамка. З 2-рядковим заголовком (місяць/рік + назви днів), 6 тижнів сітки = 6*3 + 1 (спільні рамки) = 19 рядків. Всього: 2 + 19 = 21 рядок, залишаючи рядки 22-24 для денного перегляду або рядка стану.
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Зіставлення подій для маркерів:</a> При відображенні кожної комірки дня пройти <a class="internal-link" data-href="How-to/loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">циклом</a> по масиву подій та перевірити, чи дата будь-якої події збігається з датою поточної комірки. Якщо так, записати маркерний символ (напр., кулька "\x07" або зірочка) поруч з номером дня.
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Рамкові символи (CP437):</a> Горизонтальна: C4h (одинарна) або CDh (подвійна). Вертикальна: B3h або BAh. Кути: DAh/BFh/C0h/D9h (одинарні). Т-з'єднання: C2h (верхнє-Т), C1h (нижнє-Т), C3h (ліве-Т), B4h (праве-Т). Перехрестя: C5h.
<br><a class="internal-link" data-href="How-to/flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Навігація між місяцями:</a> При натисканні «наступний місяць»: інкрементувати cur_month. Якщо перевищує 12, встановити на 1 та інкрементувати cur_year. Скинути cur_day на 1 (або обмежити до довжини нового місяця, якщо зберігаєте день). Перерахувати день тижня для 1-го та перемалювати сітку.
<br><a class="internal-link" data-href="How-to/file-io.md" href="projects/how-to/file-io.html" target="_self" rel="noopener nofollow">Формат файлу:</a> Зберегти event_count (2 байти), потім event_count * 45 байтів даних подій. Ім'я файлу: CALENDAR.DAT. При завантаженні спочатку прочитати кількість, потім прочитати дані подій одним блоком.
Точки синхронізації:
Кінець тижня 9: узгодити формат запису подій (45 байтів), спільні змінні (cur_year, cur_month, cur_day, events, event_count) та прототипи процедур (get_day_of_week(year, month, day) -&gt; AL, get_month_length(year, month) -&gt; AL, has_events(year, month, day) -&gt; ZF)
Кінець тижня 11: перша інтеграція — main.asm викликає обидва модулі; сітка коректно відображає поточний місяць із правильним розташуванням днів
Кінець тижня 12: повний цикл роботи з подіями — додавання, перегляд, видалення, маркери у місячному перегляді
Кінець тижня 13: файловий ввід/вивід працює, програма зберігає та завантажує події; обидва студенти мають чорнові версії звіту
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/y23-calendar-planner.html</link><guid isPermaLink="false">Projects/Y23-Calendar-Planner.md</guid><pubDate>Tue, 24 Feb 2026 16:03:02 GMT</pubDate><enclosure url="assets/y23-screen-monthly.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y23-screen-monthly.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[G07-Number-Base-Converter]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🟢 Зелений
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 400-500 рядків коду (соло) / 600-750 рядків коду (пара), 2-4 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/arithmetic.md" href="projects/how-to/arithmetic.html" target="_self" rel="noopener nofollow">Інструкцію DIV та вилучення залишку</a> — основний алгоритм перетворення побудований на послідовному діленні, що безпосередньо відпрацьовує інструкцію ділення ЦП — L04/P04
<br><a class="internal-link" data-href="How-to/arithmetic.md" href="projects/how-to/arithmetic.html" target="_self" rel="noopener nofollow">Інструкцію MUL</a> — перетворення з вихідної основи у внутрішнє двійкове подання (наприклад, розбір шістнадцяткового вводу вимагає множення накопиченого значення на 16) — L04/P04
Подання чисел та системи числення — увесь проєкт є практичною реалізацією теорії з L02-L03
<br><a class="internal-link" data-href="How-to/bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Побітові операції (SHL, SHR, AND)</a> — відображення двійкового числа через вилучення бітів, групування напівбайтів, вилучення шістнадцяткових цифр масками — L04/P04
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Керування масивами та буферами</a> — буфери вводу, буфери вихідних рядків, покрокований журнал як масив записів — L06/P06
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури та передача параметрів через стек</a> — модульні процедури перетворення (по одній на цільову основу), процедури відображення — L05/P05
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять (B800h)</a> — рендеринг кількох панелей відображення з кольоровими атрибутами — L07/P07
<br><a class="internal-link" data-href="How-to/addressing-modes.md" href="projects/how-to/addressing-modes.html" target="_self" rel="noopener nofollow">Режими адресації</a> — індексований доступ до таблиць пошуку цифр, базово-відносний доступ до вмісту буферів — L06/P06
<br><a class="internal-link" data-href="How-to/game-logic/G07.md" href="projects/how-to/game-logic/g07.html" target="_self" rel="noopener nofollow">Ігрова логіка: конвертер систем числення</a> — розбір чисел у довільній основі, перетворення через DIV, покрокова візуалізація
Програма є інтерактивним інструментом перетворення систем числення, що працює в текстовому режимі DOSBox. Екран представляє чистий чотирипанельний дисплей: коли користувач вводить число в будь-якій підтримуваній основі (двійковій, вісімковій, десятковій або шістнадцятковій), усі чотири подання з'являються одночасно у відповідних панелях. Наприклад, введення FF у шістнадцятковому режимі миттєво показує 11111111 на панелі двійкових чисел, 377 на панелі вісімкових, 255 на панелі десяткових та FF на панелі шістнадцяткових, усе акуратно вирівняне.Визначна функція — покроковий режим. Коли його увімкнено, перетворення не просто показує результат — воно показує роботу алгоритму. Для перетворення десяткового 25 у двійкове студент бачить кожне послідовне ділення: 25 / 2 = 12 remainder 1, 12 / 2 = 6 remainder 0, 6 / 2 = 3 remainder 0, 3 / 2 = 1 remainder 1, 1 / 2 = 0 remainder 1 — із залишками, зібраними знизу вгору для формування 11001. Кожний крок з'являється на новому рядку у покроковій панелі, розфарбований для підсвітки частки, дільника та залишку. Користувач може натискати клавішу для покрокового просування або показати все одразу.Двійковий вивід відображається з групуванням напівбайтів (1111 0000 замість 11110000), шістнадцяткові значення мають префікс 0x, а вісімкові — 0o. Поле вводу приймає цифри, допустимі для обраної основи (0-1 для двійкової, 0-9 для десяткової, 0-9 та A-F для шістнадцяткової) та негайно відхиляє недопустимі символи. Рядок стану показує поточну обрану основу вводу та доступні команди.<br><img alt="Головний екран з чотирма панелями систем числення" src="assets/g07-screen-main.png" target="_self"><br><img alt="Покроковий режим перетворення з алгоритмом ділення" src="assets/g07-screen-steps.png" target="_self">
Підхід A: Для перетворення внутрішнього 16-бітного значення у двійкове послідовно ділити на 2 та збирати залишки. Аналогічно, вилучення шістнадцяткових цифр може використовувати DIV на 16.
Підхід B: Використовувати SHR та AND 1 для вилучення бітів від старшого до молодшого (або від молодшого до старшого). Для шістнадцяткового — SHR на 4 + AND 0Fh.
Що виміряти/спостерігати: Кількість інструкцій на цифру, чи є SHR швидшим/простішим за DIV, як кожний підхід обробляє ведучі нулі. Провести бенчмарк підрахунком інструкцій у Turbo Debugger. Підхід A: 16-байтова таблиця пошуку (hex_digits DB '0123456789ABCDEF' та XLAT або індексований MOV) перетворює значення напівбайта (0-15) у його ASCII шістнадцятковий символ.
Підхід B: Арифметичне перетворення (якщо значення &lt; 10, додати '0'; інакше додати 'A'-10).
Що виміряти/спостерігати: Розмір коду, кількість інструкцій на цифру, простота підтримки малих шістнадцяткових літер. Виміряйте, який підхід дає менше байтів коду. Підхід A: Попереднє обчислення зберігає всі кроки ділення в масиві записів, потім цикл відображення рендерить їх.
Підхід B: Обчислення-та-відображення чергує розрахунок з виводом на екран, не потребуючи проміжного сховища.
Що виміряти/спостерігати: Використання пам'яті (масив записів кроків проти нічого), структура коду (розділення відповідальностей проти чергування логіки), можливість повернутися «назад» на крок (можливо лише з попередньо обчисленим сховищем). Оцініть, який дизайн легше розширити.
<br>Розподіл модулів (див. <a class="internal-link" data-href="How-to/include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">Підключення файлів: INCLUDE</a>):
Студент A: Рушій перетворення — усі процедури перетворення основ (розбір вводу для кожної основи, внутрішнє значення в кожну вихідну основу), покрокова логіка обчислення. Відповідає за процедури розбору, процедури перетворення та структуру даних покрокових записів.
Студент B: Відображення та взаємодія — розміщення екрана, чотири панелі виводу, покрокова візуалізація з кольоровою підсвіткою, перемикання режиму вводу, рядок стану. Відповідає за всі записи у відеопам'ять та логіку клавіатурної взаємодії.
Точки інтеграції: Процедури перетворення Студента A повертають результати в узгоджених буферах (наприклад, буфери вихідних рядків для кожної основи, масив покрокових записів). Студент B зчитує ці буфери та рендерить їх. Визначте формати буферів та угоди про виклик у спільному include-файлі.
Незалежне тестування: Студент A може перевірити коректність перетворення, записуючи результати в консоль через INT 21h (простий текст). Студент B може тестувати відображення з жорстко заданими зразковими даними до інтеграції. <br><a class="internal-link" data-href="How-to/game-logic/G07.md" href="projects/how-to/game-logic/g07.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: розбір числа в довільній основі, перетворення через послідовне ділення, покрокова візуалізація, таблиця HEX-цифр.
<br><a class="internal-link" data-href="How-to/arithmetic.md" href="projects/how-to/arithmetic.html" target="_self" rel="noopener nofollow">Розбір вводу в будь-якій основі:</a> Накопичуйте значення зліва направо: value = value * base + digit_value. Використовуйте MUL з основою у регістрі. Після кожного MUL перевіряйте, чи результат не перевищує 65535 (перенос у DX означає переповнення).
<br><a class="internal-link" data-href="How-to/arithmetic.md" href="projects/how-to/arithmetic.html" target="_self" rel="noopener nofollow">Перетворення в цільову основу:</a> Послідовно DIV на цільову основу. Залишок — наступна цифра (спочатку найменш значуща). Кладіть кожний залишок на стек PUSH, потім знімайте POP для отримання порядку від найбільш значущої. Це той самий алгоритм, що показаний в теорії L02. Див. також <a class="internal-link" data-href="How-to/number-display.md" href="projects/how-to/number-display.html" target="_self" rel="noopener nofollow">відображення числа</a>.
<br><a class="internal-link" data-href="How-to/lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиця шістнадцяткових цифр:</a> hex_table DB '0123456789ABCDEF'. Для перетворення напівбайта: MOV BX, OFFSET hex_table, потім XLAT (використовує AL як індекс у таблиці за DS:BX, замінює AL знайденим байтом).
<br><a class="internal-link" data-href="How-to/bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Двійкове відображення з групуванням напівбайтів:</a> Вилучайте біти циклом з 16 ітерацій: SHL AX, 1 переміщує старший біт у CF; ADC DL, '0' перетворює CF (0 або 1) у '0' або '1'. Вставляйте символ пробілу після кожного 4-го біту.
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Розміщення панелей екрана:</a> Визначте константи як PANEL_BIN_ROW EQU 4, PANEL_OCT_ROW EQU 8 тощо. Кожна панель — підписаний діапазон рядків. Використовуйте процедуру write_string_at(row, col, string, color) для всього виводу на екран.
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Вибір основи:</a> Використовуйте INT 16h для зчитування клавіш. Прив'яжіть конкретні клавіші (наприклад, F2, F3, F4, F5 або числові клавіші 2, 8, 0, 6) для встановлення змінної current_base. Валідатор вводу та розбірник перевіряють цю змінну.
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Покроковий запис:</a> step_record STRUC з полями: dividend DW ?, divisor DW ?, quotient DW ?, remainder DW ?. Виділіть масив із 16 таких записів (максимум для перетворення 16-бітного числа в основу 2).
Точки синхронізації:
Кінець тижня 8: узгодити формати вихідних буферів (рядки з нуль-термінатором), структуру step_record, угоди про виклик процедур перетворення
Кінець тижня 9: обидва модулі компілюються окремо; спільний include-файл з константами панелей та розмірами буферів зафіксовано
Кінець тижня 11: перша інтеграція — ввід десяткового числа → відображення у всіх чотирьох основах одночасно
Кінець тижня 13: код зафіксовано; покроковий режим працює; всі чотири основи вводу та виводу протестовані (0, 255, 65535, граничні значення)
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/g07-number-base-converter.html</link><guid isPermaLink="false">Projects/G07-Number-Base-Converter.md</guid><pubDate>Tue, 24 Feb 2026 16:03:00 GMT</pubDate><enclosure url="assets/g07-screen-main.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/g07-screen-main.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y18-Mini-Grep]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🟡 Жовтий
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 400–600 (соло) / 650–900 (пара), 2–5 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/file-io.md" href="projects/how-to/file-io.html" target="_self" rel="noopener nofollow">Файловий ввід/вивід</a> через INT 21h — відкриття (3Dh), читання (3Fh), закриття (3Eh) та опціонально переміщення (42h) для читання частинами — L07/P07, P09
Розбір аргументів командного рядка — зчитування імені файлу зі зміщення 80h PSP — P09
Рядкові операції — пошук підрядка (побайтове порівняння), виявлення рядків (сканування на 0Dh/0Ah) — L04/P04, L06/P06
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять</a> — рендеринг результатів з кольоровою підсвіткою у B800h — L08/P08
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Керування буфером</a> — зберігання вмісту файлу у буфері сегмента даних, відстеження меж рядків — L06/P06
<br><a class="internal-link" data-href="How-to/addressing-modes.md" href="projects/how-to/addressing-modes.html" target="_self" rel="noopener nofollow">Режими адресації</a> — [SI], [DI], [BX+SI] для сканування файлового буфера та шаблону — L06/P06
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури</a> та стекові фрейми — процедура пошуку, процедура відображення, процедура вводу як окремі модулі — L05/P05
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід</a> — INT 16h для клавіш навігації, <a class="internal-link" data-href="How-to/dos-services.md" href="projects/how-to/dos-services.html" target="_self" rel="noopener nofollow">INT 21h</a> AH=0Ah для рядкового вводу (шаблон пошуку) — L07/P07
<br><a class="internal-link" data-href="How-to/arithmetic.md" href="projects/how-to/arithmetic.html" target="_self" rel="noopener nofollow">Арифметика</a> — підрахунок рядків, підрахунок збігів, обчислення зміщення прокрутки — L03/P03, L04/P04
<br><a class="internal-link" data-href="How-to/flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Порівняння та умовні переходи</a> — посимвольне зіставлення шаблону, ланцюжки CMP/JE/JNE — L04/P04
<br><a class="internal-link" data-href="How-to/game-logic/Y18.md" href="projects/how-to/game-logic/y18.html" target="_self" rel="noopener nofollow">Ігрова логіка: Mini Grep</a> — пошук підрядка, виявлення меж рядків, підсвічування збігів
Програма запускається і або зчитує ім'я файлу з командного рядка, або просить користувача ввести його. Вона відкриває текстовий файл через файлові дескриптори INT 21h і зчитує його вміст у буфер пам'яті. Внизу екрану з'являється запрошення пошуку, запрошуючи користувача ввести шаблон пошуку. Потім програма сканує кожен рядок завантаженого файлу на входження шаблону, використовуючи точний пошук підрядка.Результати з'являються на екрані миттєво: кожен рядок із збігом відображається з номером рядка у лівому полі, і кожне входження шаблону пошуку всередині рядка підсвічується контрастним кольором (напр., яскраво-жовтий на темному фоні). Частини рядка без збігів залишаються у кольорі за замовчуванням. Якщо результатів більше, ніж вміщується на екрані, PgUp/PgDn прокручують їх. Рядок стану внизу показує загальну кількість рядків зі збігами, загальну кількість окремих збігів та загальну кількість рядків у файлі.Користувач може натиснути клавішу для введення нового шаблону пошуку без перезавантаження файлу — програма повторно сканує та оновлює результати. Натискання Esc завершує програму. Увесь вивід на дисплей здійснюється через прямий запис у відеопам'ять, надаючи інтерфейсу швидкий, чутливий відгук. Весь досвід імітує спрощену версію класичної Unix-утиліти grep, але з кольоровим візуальним зворотним зв'язком, створеним для текстового режиму.<br><img alt="Результати пошуку з підсвіченими збігами та статистикою" src="assets/y18-screen-results.png" target="_self"><br><img alt="Запрошення пошуку з завантаженням файлу" src="assets/y18-screen-prompt.png" target="_self">
Примітка: Файловий ввід/вивід (файлові сервіси INT 21h) розглядається у L09/P09 (тиждень 9). Протягом тижнів 7-8 зосередьтеся на основній логіці з жорстко заданими тестовими даними. Реалізуйте файлові операції, коли матеріал P09 стане доступним. Підхід A: Сканує весь буфер побайтово, перевіряючи шаблон на кожній позиції, та визначає номери рядків на ходу, підраховуючи зустрінуті символи нового рядка.
Підхід B: Спочатку робить прохід по буферу для побудови індексу рядків (масив зміщень, де починається кожен рядок), потім шукає в кожному рядку окремо.
Що вимірювати/спостерігати: Час на початковий прохід (Підхід B має накладні витрати), але швидкість повторного пошуку (Підхід B може перескакувати на початки рядків), та складність коду. Це ілюструє компроміс між вартістю попередньої обробки та ефективністю повторних запитів. Підхід A: Відображає рядки зі збігами безпосередньо при знаходженні — просто, але робить зворотну прокрутку неможливою без повторного пошуку.
Підхід B: Зберігає масив пар (номеррядка, зміщенняв_буфері) для всіх збігів, потім рендерить результати з цього масиву, забезпечуючи вільну прокрутку в обох напрямках.
Що вимірювати/спостерігати: Пам'ять, використана масивом індексів, проти часу повторного пошуку при прокрутці вгору. Це класичний компроміс простір-час. Підхід A: Виділяє фіксований 20-байтовий буфер для шаблону пошуку — просто, але мовчки обрізає довгі шаблони.
Підхід B: Зчитує шаблон у буфер із префіксом довжини (INT 21h AH=0Ah) і використовує фактичну довжину для порівняння, підтримуючи шаблони до 60+ символів.
Що вимірювати/спостерігати: Простота коду, марно витрачена пам'ять та ризик переповнення буфера. Це демонструє, чому буфери з перевіркою довжини важливі навіть в асемблері.
<br>(див. <a class="internal-link" data-href="How-to/include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">Підключення файлів: INCLUDE</a>)
Студент A: Файловий ввід/вивід та пошуковий рушій — завантаження файлу (розбір командного рядка, INT 21h відкриття/читання), керування буфером, виявлення меж рядків, алгоритм пошуку підрядка та індекс результатів. Надає: load_file, build_line_index, search_pattern, get_match(index).
Студент B: Відображення та взаємодія з користувачем — рендеринг рядків зі збігами з кольоровою підсвіткою у відеопам'ять, логіка прокрутки, панель статистики, ввід запрошення пошуку та клавіатурна навігація. Надає: draw_results_page, highlight_pattern, draw_status_bar, read_search_input, scroll_up, scroll_down.
Точка інтеграції: Спільний сегмент даних, що містить файловий буфер, масив індексів рядків, масив результатів збігів, поточний шаблон пошуку та позицію прокрутки. Студент A заповнює буфер та результати; Студент B зчитує їх для рендерингу. Обидва поділяють головний цикл: відобразити результати, очікувати клавішу, або прокрутити, або новий пошук, або вийти. <br><a class="internal-link" data-href="How-to/game-logic/Y18.md" href="projects/how-to/game-logic/y18.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: пошук підрядка brute-force, побудова індексу рядків, підсвічування збігів.
<br><a class="internal-link" data-href="How-to/file-io.md" href="projects/how-to/file-io.html" target="_self" rel="noopener nofollow">Файлові операції INT 21h:</a> AH=3Dh (відкриття, AL=0 для тільки читання, DS:DX=ім'я файлу ASCIIZ, повертає дескриптор в AX). AH=3Fh (читання, BX=дескриптор, CX=байтів для читання, DS:DX=буфер, повертає зчитаних байтів в AX). AH=3Eh (закриття, BX=дескриптор).
Хвіст командного рядка PSP: байт за PSP:0080h — це довжина; рядок починається з PSP:0081h (включає початковий пробіл). У файлі .COM DS=CS=PSP, тому доступ як [0080h] та [0081h]. Видаліть початковий пробіл та додайте нульовий байт для створення ASCIIZ для INT 21h.
<br>Пошук підрядка (повний перебір): для кожної позиції i у вихідному рядку порівнюйте source[i..i+patlen-1] із pattern[0..patlen-1] за допомогою CMPSB з префіксом REP або вручну побайтовим <a class="internal-link" data-href="How-to/loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">циклом</a>. При розбіжності збільшіть i на 1 та спробуйте знову.
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Підсвічене відображення:</a> записуйте символи без збігу з атрибутом 07h (сірий на чорному), а символи зі збігом з атрибутом 0Eh (жовтий на чорному) або 70h (чорний на сірому, інверсне відео). Проходьте рядком, перемикаючи атрибут при вході/виході з області збігу.
<br>Для побудови <a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">індексу рядків</a>: скануйте буфер на байти 0Ah. Записуйте зміщення кожного байту після 0Ah (це початок наступного рядка). Перший рядок починається зі зміщення 0.
<br>Для <a class="internal-link" data-href="How-to/dos-services.md" href="projects/how-to/dos-services.html" target="_self" rel="noopener nofollow">INT 21h AH=0Ah</a> (буферизований ввід): DS:DX вказує на буфер, де byte[0] = максимальна довжина, byte[1] = фактична повернута довжина, byte[2..] = рядок. Встановіть byte[0] перед викликом.
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Компонування екрану:</a> рядки 0-21 для результатів (22 рядки зі збігами на сторінку), рядок 22 для рядка стану, рядок 23 для запрошення пошуку, рядок 24 як рядок підказок клавіш.
Точки синхронізації:
Кінець тижня 8: узгодити формат спільного .DATA (файловий буфер, масив індексу рядків, масив результатів, буфер шаблону) та прототипи процедур
Кінець тижня 10: Студент A передає search_pattern із заповненим масивом результатів; Студент B має draw_results_page з підсвіткою — перший наскрізний пошук із відображенням на жорстко заданих даних
Кінець тижня 12: повна інтеграція: завантаження файлу -&gt; пошук -&gt; відображення -&gt; прокрутка -&gt; новий пошук; обидва тестують на реальних текстових файлах
Кінець тижня 14: код зафіксований, звіт та відео готові
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A vs B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/y18-mini-grep.html</link><guid isPermaLink="false">Projects/Y18-Mini-Grep.md</guid><pubDate>Tue, 24 Feb 2026 16:02:57 GMT</pubDate><enclosure url="assets/y18-screen-results.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y18-screen-results.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y22-Mandelbrot-Viewer]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🟡 Жовтий
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 400-550 (соло) / 600-800 (пара), 2-4 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/arithmetic.md" href="projects/how-to/arithmetic.html" target="_self" rel="noopener nofollow">Арифметика з фіксованою точкою</a> (представлення дробових чисел як масштабованих цілих, множення з повторним вирівнюванням зсувом) — L04/P04
<br><a class="internal-link" data-href="How-to/arithmetic.md" href="projects/how-to/arithmetic.html" target="_self" rel="noopener nofollow">Цілочисельне множення та бітовий зсув</a> (IMUL для добутку 16x16-&gt;32 біти, SHR/SAR для повторного вирівнювання фіксованої точки) — L04/P04
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять</a> (відображення кожної обчисленої комірки як символ+атрибут за адресою B800h) — L08/P08
<br><a class="internal-link" data-href="How-to/loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Вкладені цикли</a> (зовнішній цикл по рядках, внутрішній цикл по стовпцях, найвнутрішніший цикл для ітерацій) — L04/P04
<br><a class="internal-link" data-href="How-to/flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Виявлення арифметичного переповнення</a> (перевірка чи |z|^2 &gt; 4.0 у представленні з фіксованою точкою; перевірка радіуса виходу) — L04/P04
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід через INT 16h</a> (клавіші-стрілки для панорамування, +/- для масштабування) — L07/P07
<br><a class="internal-link" data-href="How-to/lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиці пошуку</a> (відображення кількості ітерацій на ASCII-символ, відображення кількості ітерацій на атрибут кольору) — L06/P06
<br><a class="internal-link" data-href="How-to/segment-registers.md" href="projects/how-to/segment-registers.html" target="_self" rel="noopener nofollow">Сегментні регістри</a> (ES=B800h для виводу на екран) — L03/P03
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури</a> (множення з фіксованою точкою, render_screen, оновлення координат панорамування/масштабування) — L05/P05
<br><a class="internal-link" data-href="How-to/game-logic/Y22.md" href="projects/how-to/game-logic/y22.html" target="_self" rel="noopener nofollow">Ігрова логіка: Мандельброт</a> — арифметика з фіксованою точкою 4.12, ітерація escape-time, відображення координат, масштабування
Програма відображає множину Мандельброта у текстовому режимі DOSBox, використовуючи цілочисельну арифметику з фіксованою точкою — модуль з рухомою точкою не потрібен. Екран 80x25 стає вікном у комплексну площину. Кожна символьна комірка представляє точку (c = x + yi), і програма ітерує рекурентне співвідношення z(n+1) = z(n)^2 + c, щоб визначити, чи «втікає» ця точка в нескінченність. Кількість ітерацій до виходу (або досягнення максимуму) відображається на ASCII-символ з градієнтної шкали щільності — наприклад, пробіл (всередині множини), потім ., :, -, =, +, *, #, %, @ (найшвидший вихід). Результат — розпізнаваний текстовий рендер культової форми Мандельброта: основна кардіоїда, булб періоду 2 та складна деталь межі — все намальовано кольоровими символами.Оскільки 8086 не має апаратного FPU, доступного в реальному режимі через прості інструкції, вся арифметика використовує представлення з фіксованою точкою. Рекомендований формат — 4.12 (4 цілих біти, 12 дробових бітів, упаковані у 16-бітове слово), що дає діапазон від -8.0 до +7.999 з точністю близько 0.00024 — достатньо для початкового виду, що охоплює приблизно від -2.5 до +1.0 по горизонталі та від -1.25 до +1.25 по вертикалі. Множення двох чисел з фіксованою точкою використовує IMUL для отримання 32-бітового результату в DX:AX, потім зсув вправо на 12 для повторного вирівнювання десяткової точки. Це концептуальне ядро проєкту: реалізація арифметики «дійсних чисел» виключно цілочисельними інструкціями.Переглядач підтримує інтерактивну навігацію. Клавіші-стрілки панорамують вид (зсувають координатне вікно), + та - збільшують і зменшують масштаб (звужують або розширюють діапазон координат), а окрема клавіша регулює максимальну кількість ітерацій (більше = більше деталей, але повільніше). Рядок стану (рядок 24) показує поточні координати центру, рівень масштабування та ліміт ітерацій у зручному для читання форматі (перетворення з фіксованої точки назад у десяткове для відображення). Збільшення масштабу розкриває більш дрібні деталі межі — аж до межі точності вибраного формату з фіксованою точкою, після чого зображення стає блочним, що створює природний урок про компроміси точності.<br><img alt="Перегляд множини Мандельброта у текстовому режимі" src="assets/y22-screen-viewer.png" target="_self"><br><img alt="Збільшений фрагмент межі множини Мандельброта" src="assets/y22-screen-zoomed.png" target="_self">
Підхід A: Використовує формат 8.8 (8 цілих бітів, 8 дробових бітів), що дає діапазон від -128 до +127 з точністю ~0.004.
Підхід B: Використовує формат 4.12 (4 цілих біти, 12 дробових бітів), що дає діапазон від -8 до +7.999 з точністю ~0.00024.
Що виміряти/спостерігати: Візуальна якість відображеної множини при масштабі за замовчуванням (4.12 показує тонші деталі межі), максимальна корисна глибина масштабування (4.12 може масштабувати глибше до піксельності) та арифметика: множення 8.8 зсувається на 8 (просто — взяти старший байт 16-бітового проміжного результату), тоді як 4.12 зсувається на 12 (потребує 12-бітового зсуву 32-бітового значення, що вимагає більше інструкцій). Обговорити: чому координатний діапазон множини Мандельброта (від -2.5 до +1) зручно вміщується у 4 цілих біти, що робить діапазон 8.8 надлишковим. Підхід A: Завжди обчислює x^2 + y^2 (два множення, одне додавання, одне порівняння).
Підхід B: Спочатку перевіряє, чи |x| &gt; 2 або |y| &gt; 2 (два абсолютних значення та два порівняння — без множення), і обчислює x^2 + y^2 лише якщо швидка перевірка пройдена.
Що виміряти/спостерігати: Середня кількість ітерацій за секунду для всього екрану (виміряти часом через INT 1Ah). Попередня перевірка додає розгалуження, але уникає дорогого MUL для точок, що явно вийшли. Обговорити: наслідки передбачення переходів на сучасних процесорах проти простий конвеєр 8086 без черги. Підхід A: Відображає зліва направо, зверху вниз — користувач бачить, як зображення зростає рядок за рядком.
Підхід B: Спочатку відображає кожен 4-й рядок і кожен 4-й стовпець (заповнюючи комірки великими блоковими символами), потім заповнює проміжки в наступних проходах — користувач бачить грубе зображення швидко, яке поступово уточнюється.
Що виміряти/спостерігати: Час до першого корисного зображення (підхід B виграє), загальний час рендерингу (майже ідентичний, оскільки обидва обчислюють ті самі комірки) та складність коду (підхід B вимагає відстеження, які комірки вже обчислені). Обговорити: це повторює прогресивний рендеринг JPEG, пов'язуючи проєкт зі стратегіями відображення зображень реального світу.
<br>Розподіл модулів (див. <a class="internal-link" data-href="How-to/include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">Підключення файлів: INCLUDE</a>):
Студент A — Модуль математики та ітерацій: процедура множення з фіксованою точкою, цикл ітерації Мандельброта для комірки (z = z^2 + c з перевіркою виходу), відображення координат (позиція екрану на координати комплексної площини), процедури оновлення координат масштабування/панорамування. Файл: math.asm.
Студент B — Модуль відображення та UI: повноекранний цикл рендерингу (викликає ітерацію студента A для кожної комірки), таблиця пошуку символів/кольорів та відображення, рендеринг рядка стану, обробка клавіатурного вводу, десяткове відображення координат з фіксованою точкою. Файл: display.asm.
Спільний головний модуль: ініціалізація (встановлення координат та масштабу за замовчуванням), головний цикл подій (рендеринг, очікування вводу, оновлення координат, повторний рендеринг). Файл: main.asm.
Точки інтеграції:
Узгодити формат фіксованої точки (напр., 4.12) та спільні змінні: center_x, center_y (слова з фіксованою точкою), scale (слово з фіксованою точкою, що представляє ширину виду), max_iter (слово).
Процедура ітерації студента A приймає x_coord та y_coord (фіксована точка) і повертає кількість ітерацій у AX. Студент B викликає її для кожної комірки екрану.
Визначити таблиці пошуку символів та кольорів як спільні дані: char_ramp DB ' ', '.', ':', '-', '=', '+', '*', '#', '%', '@' та паралельний масив color_ramp. <br><a class="internal-link" data-href="How-to/game-logic/Y22.md" href="projects/how-to/game-logic/y22.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: множення з фіксованою точкою 4.12, ітерація escape-time, відображення координат екрану на комплексну площину, масштабування.
<br><a class="internal-link" data-href="How-to/arithmetic.md" href="projects/how-to/arithmetic.html" target="_self" rel="noopener nofollow">Представлення 4.12 з фіксованою точкою:</a> Значення 1.0 зберігається як 1000h (4096). Значення -2.5 зберігається як -2.5 * 4096 = -10240 = D800h (як знакове 16-бітове ціле). Для перетворення десяткової константи у 4.12 під час асемблювання не можна використовувати дробові значення у TASM безпосередньо, тому попередньо обчисліть масштабоване ціле: neg2_5 DW -10240.
<br><a class="internal-link" data-href="How-to/arithmetic.md" href="projects/how-to/arithmetic.html" target="_self" rel="noopener nofollow">Процедура множення з фіксованою точкою:</a> IMUL BX з множеним у AX дає 32-бітовий знаковий результат у DX:AX. Для зсуву вправо на 12: спочатку зсунути DX вліво на 4 у тимчасову змінну, потім зсунути AX вправо на 12 та об'єднати їх через OR. Або простіше: зсунути 32-бітове значення вправо 12 разів у циклі SAR DX, 1 / RCR AX, 1. Результат у AX. (На 8086 багатобітові зсуви вимагають CL; на 186+ можна використовувати безпосередні лічильники зсуву.)
Ядро ітерації Мандельброта: Для кожної комірки з комплексною координатою c = (cx, cy): встановити zx=0, zy=0. Цикл: обчислити zx^2 та zy^2 (множення з фіксованою точкою). Якщо zx^2 + zy^2 &gt; 4.0 (= 4000h у 4.12), вихід. Інакше обчислити new_zx = zx^2 - zy^2 + cx, new_zy = 2zxzy + cy. «2*» — це просто зсув вліво на 1 (SHL AX, 1 після результату множення). Інкрементувати лічильник ітерацій. Якщо лічильник &gt;= max_iter, точка належить множині.
<br><a class="internal-link" data-href="How-to/addressing-modes.md" href="projects/how-to/addressing-modes.html" target="_self" rel="noopener nofollow">Відображення координат:</a> Комірка екрану зі стовпцем c (0-79) та рядком r (0-23) відповідає комплексним координатам: x = center_x + (c - 40) * step_x, y = center_y + (r - 12) * step_y, де step_x = scale / 80 та step_y = scale / 25 (попередньо поділені, або обчислені з наближенням зсувом, оскільки 80 ~ 64+16).
<br><a class="internal-link" data-href="How-to/lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Градієнтна шкала символів:</a> Визначити масив байтів: ramp DB ' .:-=+*#%@'. Індексація: якщо кількість ітерацій n та max_iter M, використовувати n * ramp_length / M як індекс. Оскільки ділення дороге, наблизити: якщо max_iter=32 та шкала має 10 записів, зсунути кількість ітерацій вправо на 2 (поділити на 4) та обмежити до 9.
<br><a class="internal-link" data-href="How-to/lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Колір за діапазоном ітерацій:</a> Визначити діапазони: ітерації 0-7 = синій, 8-15 = блакитний, 16-23 = зелений, 24-31 = жовтий, 32 (у множині) = чорний фон. Зберегти байти атрибутів у таблиці пошуку паралельно з градієнтною шкалою символів.
Панорамування та масштабування: Панорамування: додати/відняти step_x * 10 до center_x для ліво/право, step_y * 5 до center_y для вгору/вниз. Збільшення масштабу: помножити scale на 3/4 (помножити на 3, зсунути вправо на 2). Зменшення масштабу: помножити scale на 4/3 (помножити на 4, поділити на 3). Після зміни координат перемалювати весь екран.
Точки синхронізації:
Кінець тижня 9: узгодити формат фіксованої точки (4.12), сигнатуру iterate_cell (вхід: cx, cy як слова фіксованої точки; вихід: кількість ітерацій у AX), спільні змінні (center_x, center_y, scale, max_iter)
Кінець тижня 11: перша інтеграція — render_screen студента B викликає iterate_cell та map_coords студента A; множина Мандельброта відображається на екрані
Кінець тижня 12: інтерактивна навігація працює наскрізно (панорамування, масштабування, зміна ліміту ітерацій з перемальовуванням)
Кінець тижня 14: код зафіксовано, звіт та відео готові
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/y22-mandelbrot-viewer.html</link><guid isPermaLink="false">Projects/Y22-Mandelbrot-Viewer.md</guid><pubDate>Tue, 24 Feb 2026 16:02:51 GMT</pubDate><enclosure url="assets/y22-screen-viewer.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y22-screen-viewer.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[G06-Hangman]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🟢 Зелений
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 400-550 рядків коду (соло) / 600-850 рядків коду (пара), 2-5 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Обхід та пошук у масиві</a> — сканування слова для пошуку відповідних букв, сканування списку використаних букв на дублікати — L06/P06
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Байтові структури даних (DB)</a> — словник як упакований масив рядків фіксованої довжини, маска відкриття як масив байтів-прапорців — L06/P06
<br><a class="internal-link" data-href="How-to/flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Умовна логіка та FLAGS</a> — CMP для збігу букв, розгалуження JE/JNE при збігу/незбігу — L04/P04
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури та модульний дизайн</a> — окремі процедури для малювання шибениці, оновлення відображення слова, перевірки перемоги/поразки — L05/P05
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять (B800h)</a> — розміщення символів з кольоровими атрибутами на обчислених позиціях екрана — L07/P07
<br><a class="internal-link" data-href="How-to/random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">Генерація псевдовипадкових чисел</a> — зчитування лічильника тактів таймера BIOS для випадкового зерна, простий ЛКГ або модулярна арифметика — L07/P07
<br><a class="internal-link" data-href="How-to/loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Конструкції циклів (LOOP, DEC+JNZ)</a> — ітерація по символах слова, записах словника — L04/P04
<br><a class="internal-link" data-href="How-to/bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Побітові операції</a> — потенційне використання бітових масок для 26-літерного алфавіту (які букви вже вгадані) — L04/P04
<br><a class="internal-link" data-href="How-to/game-logic/G06.md" href="projects/how-to/game-logic/g06.html" target="_self" rel="noopener nofollow">Ігрова логіка: шибениця</a> — сканування слова на збіг, маска відкриття, відстеження букв, етапи шибениці
Програма реалізує класичну гру «Шибениця» з вгадуванням слів у текстовому режимі DOSBox. Коли гра починається, слово обирається випадково з вбудованого словника, збереженого в сегменті даних. Екран поділено на кілька окремих областей: ліва частина відображає поетапно мальовану фігуру шибениці в ASCII-арті (6-7 етапів від порожньої шибениці до повної фігури), права частина показує частково відкрите слово з підкресленнями замість невгаданих букв, а нижче розташований список вже вгаданих букв, розфарбованих зеленим для правильних та червоним для неправильних. Лічильник залишених спроб виразно відображений.Гравець натискає клавішу з буквою для вгадування. Якщо буква є у слові, всі її входження відкриваються одночасно з короткою зеленою підсвіткою. Якщо букви немає у слові, наступна частина тіла домальовується на фігурі шибениці, а буква з'являється червоною на панелі "Used letters". Гра завершується, коли слово повністю відкрите (перемога) або фігура шибениці завершена (поразка). В обох випадках повне слово відображається, і гравцю пропонується зіграти ще раз.Поточний рахунок відстежує перемоги та поразки протягом кількох раундів. Між раундами наступне слово обирається зі словника, уникаючи повторів протягом сесії. Увесь інтерфейс рендериться через прямий запис у відеопам'ять з кольоровими атрибутами — жодного блимаючого курсора, що блукає по екрану, лише чистий кольоровий текст, що з'являється у фіксованих позиціях.<br><img alt="Ігровий процес — фігура шибениці, частково відкрите слово та використані букви" src="assets/g06-screen-gameplay.png" target="_self"><br><img alt="Кінець гри — повністю намальована шибениця та відкрите слово" src="assets/g06-screen-game-over.png" target="_self">
Підхід A: Масив байтів використовує guessed_letters DB 26 DUP(0) та перевіряє/встановлює окремі байти.
Підхід B: Бітова маска зберігає всі 26 букв у одному подвійному слові (або двох словах), використовуючи BT/BTS або SHL+TEST для перевірки/встановлення бітів.
Що виміряти/спостерігати: Використання пам'яті (26 байтів проти 4 байти), складність коду для операцій перевірки/встановлення, швидкість пошуку «чи вгадана ця буква?». Протестуйте обидва в Turbo Debugger та підрахуйте інструкції на один пошук. Підхід A: Фіксована довжина (наприклад, 10 байтів на слово, доповнені пробілами) дозволяє пряму індексацію: word_n = dictionary + n * 10.
Підхід B: Рядки з префіксом довжини (DB 5, 'APPLE') економлять пам'ять, але вимагають послідовного сканування для досягнення слова n.
Що виміряти/спостерігати: Витрати пам'яті (доповнення), простота випадкового доступу, складність коду для вибору слова та порівняння букв. Виміряйте загальний розмір словника для тих самих 20 слів у кожному форматі. Підхід A: Повне перемалювання очищує та перемальовує всі області екрана кожний хід.
Підхід B: Часткове оновлення модифікує лише конкретні клітинки, що змінились (нова відкрита буква, нова частина тіла, новий запис використаної букви).
Що виміряти/спостерігати: Кількість записів у відеопам'ять за хід, візуальна плавність (чи спричиняє повне перемалювання видиме мерехтіння?), складність коду. Підрахуйте інструкції MOV у відеопам'ять для кожного підходу на типовому ході.
<br>Розподіл модулів (див. <a class="internal-link" data-href="How-to/include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">Підключення файлів: INCLUDE</a>):
Студент A: Модуль ігрової логіки — вибір слова, збіг букв, виявлення перемоги/поразки, відстеження рахунку. Відповідає за дані словника, масив маски відкриття, відстеження вгаданих букв та основну процедуру process_guess.
Студент B: Модуль відображення — розміщення екрана, поетапне малювання шибениці в ASCII-арті, відображення слова з підкресленнями, панель використаних букв, відображення рахунку, екрани перемоги/поразки.
Точки інтеграції: Студент A викликає процедури відображення Студента B після кожного вгадування, передаючи поточний стан гри (маска відкриття, кількість неправильних, вгадана буква, прапорець збігу). Визначте чіткий інтерфейс: наприклад, draw_hangman_stage(stage_number), update_word_display(word_ptr, mask_ptr, length), add_used_letter(letter, is_correct).
Спільні константи: Межі областей екрана (рядки/стовпці для кожної панелі), значення кольорових атрибутів, максимальна довжина слова, кількість етапів шибениці. <br><a class="internal-link" data-href="How-to/game-logic/G06.md" href="projects/how-to/game-logic/g06.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: сканування слова на збіг букв, маска відкриття, відстеження використаних букв (масив проти бітова маска), поетапне малювання шибениці.
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Зберігання словника:</a> dictionary DB 'COMPUTER ', 'ASSEMBLY ', 'KEYBOARD ', ... з кожним записом, доповненим до фіксованої довжини (наприклад, 10 байтів). Обчислюйте зміщення як index * WORD_LENGTH з LEA або MUL.
<br><a class="internal-link" data-href="How-to/random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">Випадковий вибір:</a> Зчитайте лічильник тактів таймера: MOV AX, [0040h:006Ch] (встановіть ES на 0040h спочатку, потім MOV AX, ES:[006Ch]). Поділіть на розмір словника за допомогою DIV; залишок у DX — випадковий індекс.
ASCII-арт шибениці: Зберігайте кожний етап як набір записів у конкретні позиції (рядок, стовпець). Наприклад, етап 0 малює каркас шибениці, етап 1 додає голову ('O' на рядку 3, стовпці 5), етап 2 додає тулуб ('|') тощо. Таблиця вказівників на дані малювання за етапом тримає код чистим.
<br><a class="internal-link" data-href="How-to/loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Перевірка вгадування:</a> Перебирайте слово з SI як індексом. Порівнюйте кожний байт [word + SI] з вгаданою буквою. При збігу встановіть revealed_mask[SI] = 1 та встановіть прапорець «знайдено».
Перевірка перемоги: Переберіть revealed_mask; якщо всі байти — 1 (або ненульові), гравець переміг. Один нуль означає, що гра продовжується.
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Кольорові атрибути для відеопам'яті:</a> Зелений на чорному = 02h, Червоний на чорному = 04h, Білий на чорному = 07h, Яскраво-білий = 0Fh. Записуйте байт атрибуту за [video_offset + 1].
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Фільтрація вводу:</a> Після INT 16h AH=00h, AL містить ASCII-код. Перевірте, чи він у діапазоні 'A'-'Z' або 'a'-'z'. Конвертуйте нижній регістр у верхній через <a class="internal-link" data-href="How-to/bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">AND з 0DFh</a> (очищує біт 5).
Точки синхронізації:
Кінець тижня 8: узгодити інтерфейси процедур відображення (draw_hangman_stage, update_word_display, add_used_letter) — які параметри, як передаються (регістри чи стек)
Кінець тижня 9: спільний сегмент .DATA зафіксовано; include-файл з константами (межі панелей, кольори, MAX_WORD_LEN, MAX_STAGES) готовий
Кінець тижня 11: перша інтеграція — повний раунд працює: вибір слова → вгадування → відображення → перемога/поразка
Кінець тижня 13: код зафіксовано; спільне тестування всіх сценаріїв (перемога, поразка, повторна буква, вичерпання словника)
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/g06-hangman.html</link><guid isPermaLink="false">Projects/G06-Hangman.md</guid><pubDate>Tue, 24 Feb 2026 16:02:51 GMT</pubDate><enclosure url="assets/g06-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/g06-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y17-Maze-Generator-Solver]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🟡 Жовтий
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 450–600 (соло) / 650–900 (пара), 2–5 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
Явна стекова структура даних — генерація лабіринту використовує вручну керований стек у сегменті даних (<a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">масив</a> + вказівник), реалізуючи рекурсивний бектрекінг без апаратної CALL/RET рекурсії; ілюструє, як рекурсія відображається на стекові операції — L05/P05
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять</a> — малювання стін, шляхів, відвіданих комірок, шляху розв'язку у B800h із кольоровими атрибутами — L08/P08
<br>Представлення <a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">2D-масиву</a> — сітка лабіринту зберігається як байтовий масив у сегменті даних; рядкова індексація з обчисленими зміщеннями — L06/P06
<br><a class="internal-link" data-href="How-to/addressing-modes.md" href="projects/how-to/addressing-modes.html" target="_self" rel="noopener nofollow">Режими адресації</a> — [BX+SI], [BX+disp] для навігації 2D-масивом сітки — L06/P06
<br><a class="internal-link" data-href="How-to/bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Побітові операції</a> — кодування наявності стін як бітових прапорців на комірку (стіни північ/південь/схід/захід) — L04/P04, L05/P05
<br><a class="internal-link" data-href="How-to/random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">Генерація псевдовипадкових чисел</a> — випадковий вибір напрямку за допомогою тіків таймера BIOS або лінійного конгруентного генератора — L07/P07
<br><a class="internal-link" data-href="How-to/flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Умовні переходи</a> та скінченні автомати — перевірка напрямків, валідація стін, відстеження відвіданих комірок — L04/P04
Керування стеком — розуміння, чому глибока апаратна рекурсія переповнюється і як явний стек у сегменті даних вирішує це — L05/P05
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури</a> — окремі підпрограми для генерації, розв'язання, відображення, вводу — L05/P05
<br><a class="internal-link" data-href="How-to/game-logic/Y17.md" href="projects/how-to/game-logic/y17.html" target="_self" rel="noopener nofollow">Ігрова логіка: лабіринт</a> — DFS з явним стеком, генерація бектрекінгом, реконструкція шляху
Програма відкривається меню з двома опціями: згенерувати новий лабіринт або завантажити попередньо згенерований. Коли генерація починається, екран заповнюється суцільною сіткою символів стін — потім, комірка за коміркою, проходи прорізаються в реальному часі. Студент спостерігає, як працює алгоритм рекурсивного бектрекінгу: "різач" рухається сіткою, зносячи стіни на своєму шляху, повертаючись назад, коли потрапляє у глухі кути, завжди залишаючи слід кольорово закодованих відвіданих комірок. Сама генерація — це перше візуальне видовище.Коли лабіринт готовий, він відображається на екрані символами для малювання рамок — стіни одним кольором, відкриті шляхи іншим. Маркер старту (верхній лівий кут) та маркер фінішу (нижній правий кут) підсвічені. Потім користувач може натиснути клавішу, щоб запустити розв'язувач. Розв'язувач — алгоритм слідування за стіною або пошук у глибину/ширину — анімує своє дослідження: відвідані комірки змінюють колір, коли алгоритм їх розглядає, зворотний хід із глухих кутів показується згасаючим кольором, а фінальний шлях розв'язку загоряється яскраво-зеленим або жовтим, коли вихід знайдено.Елементи керування дозволяють користувачу перемикатися між покроковим режимом (натискання клавіші просуває на одну комірку) та автоматичним режимом (безперервна анімація з регульованою швидкістю). Після розв'язання користувач може натиснути клавішу для генерації нового лабіринту і спробувати знову. Увесь рендеринг використовує прямий запис у відеопам'ять, створюючи захопливий візуальний досвід у текстовому режимі сітки 80x25.<br><img alt="Анімована генерація лабіринту з візуалізацією бектрекінгу" src="assets/y17-screen-generation.png" target="_self"><br><img alt="Розв'язаний лабіринт з підсвіченим шляхом розв'язку" src="assets/y17-screen-solved.png" target="_self">
Підхід A: Використовує простий 2D байтовий масив, де кожен байт представляє стан комірки (стіна=1, шлях=0, відвідана=2, розв'язок=3). Легко індексувати, але витрачає пам'ять і потребує окремої логіки для стіни проти проходу.
Підхід B: Упаковує чотири прапорці стін (N, S, E, W) у молодші 4 біти кожного байту комірки, з бітами 4-7 для стану. Це зменшує ефективний розмір масиву вдвічі і робить зняття стіни однією операцією AND, але ускладнює індексацію.
Що вимірювати/спостерігати: Обсяг пам'яті, складність коду (кількість інструкцій для "чи є стіна на сході?") та простота додавання бітів стану розв'язувача. Підхід A (обов'язковий для мінімуму): Симулює рекурсію за допомогою явного масиву стеку в сегменті даних — масив stack_data та змінна-вказівник stack_top замінюють апаратний стек. Це обов'язковий підхід, оскільки він повністю виключає переповнення стеку: сегмент даних може безпечно вмістити тисячі записів.
Підхід B: Використовує справжні інструкції CALL/RET для рекурсивного бектрекінгу — кожен рекурсивний виклик заштовхує адресу повернення, координати комірки та збережені регістри на апаратний стек. Чистіший і природніший для читання, але стандартний стек TASM лише 256-512 байтів, і навіть зі STACK 8192 лабіринт 20x12 може рекурсувати на 240 рівнів вглиб (~2400 байтів), роблячи переповнення реальним ризиком.
Що вимірювати/спостерігати: Зрозумілість коду (CALL/RET природніше), максимальний безпечний розмір лабіринту та кількість інструкцій на рекурсивний крок. Це порівняння безпосередньо ілюструє, як рекурсія відображається на використання апаратного стеку і чому системні програмісти іноді замінюють рекурсію явними стеками. Підхід A: Зчитує лічильник тіків BIOS (0040:006Ch) і використовує MOD 4 для випадкового вибору напрямку. Просто, але може створювати зміщені лабіринти, якщо алгоритм працює швидше за частоту тіків (послідовні зчитування повертають те саме значення).
Підхід B: Реалізує простий LCG (next = (a * current + c) MOD m), ініціалізований початковим значенням таймера.
Що вимірювати/спостерігати: Якість лабіринту (візуальна перевірка на очевидні закономірності), складність реалізації та як зміщення проявляється у згенерованих лабіринтах.
<br>(див. <a class="internal-link" data-href="How-to/include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">Підключення файлів: INCLUDE</a>)
Студент A: Алгоритми лабіринту — реалізує алгоритм генерації (рекурсивний бектрекінг) та алгоритм розв'язання (DFS або BFS). Працює безпосередньо зі структурою даних лабіринту. Надає: generate_maze, solve_maze, is_wall(row, col, direction), remove_wall(row, col, direction).
Студент B: Відображення та взаємодія — рендерить сітку лабіринту у відеопам'ять, обробляє кольорове кодування для різних станів комірок, реалізує анімацію (покроковий/автоматичний режими), клавіатурний ввід, меню та цикл перегенерації. Надає: draw_cell(row, col, state), draw_maze, animate_step, get_user_input.
Точка інтеграції: Спільний масив лабіринту в сегменті даних та набір констант стану (WALL, PATH, VISITED, SOLUTION, CURRENT). Студент A змінює стани комірок у масиві; Студент B зчитує масив для рендерингу. Для анімації Студент A викликає animate_step Студента B після кожної зміни стану або встановлює прапорець, який підхоплює головний цикл. <br>
<a class="internal-link" data-href="How-to/game-logic/Y17.md" href="projects/how-to/game-logic/y17.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: DFS з явним стеком, генерація бектрекінгом, розв'язання DFS/BFS, реконструкція шляху. <br>
<a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Розміри сітки лабіринту:</a> для екрану 80x25 компонування комірка+стіна використовує 2 символи на комірку в кожному вимірі. Лабіринт 39x12 комірок (зі стінами) вміщується у 79x25 символів. Більш консервативний варіант 19x11 або 15x10 дає комфортний відступ. <br>
<a class="internal-link" data-href="How-to/lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиці дельти напрямків</a> спрощують рух: delta_row DB -1, 1, 0, 0 та delta_col DB 0, 0, -1, 1 для Північ, Південь, Захід, Схід. Для руху в напрямку D: новий_рядок = рядок + delta_row[D], новий_стовпець = стовпець + delta_col[D]. <br>
Для рекурсивного бектрекінгу: (1) позначити поточну комірку як відвідану; (2) створити список невідвіданих сусідів; (3) перемішати список (<a class="internal-link" data-href="How-to/random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">випадковий порядок</a>); (4) для кожного сусіда: якщо невідвіданий, зняти стіну між поточною коміркою та сусідом, потім рекурсувати на сусіді. <br>
Скелет явного стеку (обов'язковий для генерації лабіринту). Замість CALL/RET рекурсії керуйте власним стеком у <a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">сегменті даних</a>:
; Сегмент даних
stack_data DW 300 DUP(?) ; кожен запис містить закодовані (row, col)
stack_top DW 0 ; індекс наступного вільного слоту ; Push: зберегти значення, збільшити top
push_stack PROC ; AX = значення для push (напр., row*256 + col) MOV BX, stack_top SHL BX, 1 ; записи розміру word -&gt; байтове зміщення MOV stack_data[BX], AX INC stack_top RET
push_stack ENDP ; Pop: зменшити top, зчитати значення
pop_stack PROC DEC stack_top MOV BX, stack_top SHL BX, 1 MOV AX, stack_data[BX] ; AX = витягнуте значення RET
pop_stack ENDP ; Перевірка на порожність: порівняти stack_top з 0
; Головний цикл: заштовхнути стартову комірку, потім поки стек не порожній:
; подивитися на вершину -&gt; поточна комірка
; якщо поточна має невідвіданих сусідів -&gt; обрати випадкового,
; зняти стіну, заштовхнути сусіда
; інакше -&gt; витягнути (бектрекінг) Символи рамок у кодовій сторінці DOS 437: 0C9h (верхній лівий кут), 0CDh (горизонталь), 0BAh (вертикаль), 0BBh (верхній правий), 0C8h (нижній лівий), 0BCh (нижній правий), 0CEh (хрест). Або просто використовуйте 0DBh (повний блок) для стін та пробіл (20h) для шляхів. <br>
Для розв'язувача BFS потрібна черга. Реалізуйте кільцевий буфер у <a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">сегменті даних</a>: дві змінні-індекси (head, tail) та масив фіксованого розміру пар (row, col). Enqueue: зберегти за tail, збільшити tail MOD розмір. Dequeue: зчитати з head, збільшити head MOD розмір. <br>
Для перемішування 4 напрямків: згенерувати <a class="internal-link" data-href="How-to/random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">випадкове число</a> 0-3, обміняти елемент 0 з цим індексом; згенерувати 0-2, обміняти елемент 1 з цим індексом; згенерувати 0-1, обміняти елемент 2 з цим індексом. Це Fisher-Yates для 4 елементів. Точки синхронізації:
Кінець тижня 8: узгодити формат масиву maze_grid, константи стану (WALL, PATH, VISITED, SOLUTION, CURRENT) та угоди виклику процедур
Кінець тижня 10: Студент A передає працюючий generate_maze із викликом animate_step; Студент B має draw_cell із 4 кольорами — перша наскрізна анімація генерації
Кінець тижня 12: повна інтеграція генерації + розв'язання + покроковий/автоматичний режими; обидва тестують разом на лабіринтах різних розмірів
Кінець тижня 14: код зафіксований, звіт та відео готові
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A vs B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/y17-maze-generator-solver.html</link><guid isPermaLink="false">Projects/Y17-Maze-Generator-Solver.md</guid><pubDate>Tue, 24 Feb 2026 16:02:49 GMT</pubDate><enclosure url="assets/y17-screen-generation.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y17-screen-generation.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[G05-Calculator-With-History]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🟢 Зелений
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 400-550 рядків коду (соло) / 600-800 рядків коду (пара), 2-5 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Стекові операції (PUSH/POP)</a> для обчислення у ЗПН або для тимчасового зберігання під час розбору інфіксних виразів — L05/P05
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури та угоди про виклик</a> — модульний дизайн з окремими процедурами для розбору, обчислення, відображення — L05/P05
<br><a class="internal-link" data-href="How-to/arithmetic.md" href="projects/how-to/arithmetic.html" target="_self" rel="noopener nofollow">Арифметичні інструкції (ADD, SUB, MUL, IDIV)</a>, включаючи знакову арифметику та перевірку FLAGS (CF, OF, ZF) — L04/P04
<br><a class="internal-link" data-href="How-to/flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Умовні переходи та потік керування</a> — диспетчеризація операторів, гілки обробки помилок — L04/P04
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Обробка рядків/масивів</a> — керування буфером вводу, історія як кільцевий буфер рядків — L06/P06
<br><a class="internal-link" data-href="How-to/dos-services.md" href="projects/how-to/dos-services.html" target="_self" rel="noopener nofollow">Сервіси DOS INT 21h</a> — клавіатурний ввід (AH=01h, AH=0Ah) — L07/P07
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять (B800h)</a> — рендеринг розміщення дисплея з кольоровими атрибутами — L07/P07
<br><a class="internal-link" data-href="How-to/segment-registers.md" href="projects/how-to/segment-registers.html" target="_self" rel="noopener nofollow">Сегментні регістри (DS, ES)</a> — ES вказує на відеосегмент, поки DS зберігає дані — L03/P03
<br><a class="internal-link" data-href="How-to/game-logic/G05.md" href="projects/how-to/game-logic/g05.html" target="_self" rel="noopener nofollow">Ігрова логіка: калькулятор</a> — обчислення виразів (ЗПН/інфікс), розбір чисел, кільцевий буфер історії
Програма представляє інтерактивний калькулятор у текстовому режимі DOSBox. Екран поділено на дві області: верхня частина показує редактор виразів, де користувач будує обчислення, а нижня частина показує прокручуваний журнал історії попередніх обчислень та їхніх результатів. Коли користувач вводить цифри та оператори, поточний вираз з'являється символ за символом у верхній частині екрана. Натискання Enter обчислює вираз, і результат з'являється на новому рядку, тоді як уся пара вираз-результат переміщується вниз у панель історії.Студент обирає один із двох режимів обчислення при проєктуванні: ЗПН (Зворотна польська нотація), де користувач вводить операнди спочатку, а потім оператори (обчислення на основі стека), або інфіксний режим, де користувач вводить звичайні вирази на кшталт 12 + 34 * 5, і програма обробляє пріоритет операторів. В обох режимах калькулятор підтримує щонайменше чотири базові операції (+, -, *, /) над знаковими 16-бітними цілими числами. Ділення на нуль, переповнення та некоректний ввід створюють чіткі повідомлення про помилки окремим кольором замість збою.Панель історії зберігає останні 10-20 обчислень. Користувач може прокручувати історію клавішами-стрілками. Кольорові атрибути розрізняють результати (наприклад, білий для звичайних результатів, червоний для помилок). Простий рядок стану внизу показує поточний режим ("RPN" / "Infix") та доступні команди (наприклад, "C=Clear, Q=Quit, Up/Down=Scroll").<br><img alt="Інфіксний режим калькулятора з історією обчислень" src="assets/g05-screen-main.png" target="_self"><br><img alt="Режим зворотної польської нотації (ЗПН) зі стеком та історією" src="assets/g05-screen-rpn.png" target="_self">
Підхід A: ЗПН напряму відповідає апаратному стеку (PUSH операнд, PUSH операнд, POP обидва та застосувати оператор).
Підхід B: Інфіксний режим вимагає або сортувальної станції з явним стеком операторів, або рекурсивного спуску.
Що виміряти/спостерігати: Складність коду (рядки, кількість процедур), глибина стека під час обчислення, простота додавання нових операторів. Виміряйте максимальну глибину стека для еквівалентних виразів в обох режимах. Підхід A: INT 21h AH=01h відлунює символ та обробляє базове редагування.
Підхід B: INT 16h AH=00h дає сирі скан-коди без відлуння, вимагаючи ручного оновлення екрана.
Що виміряти/спостерігати: Гнучкість (чи може INT 16h виявити клавіші-стрілки, які INT 21h не може?), складність коду для обробки спеціальних клавіш, вплив на керування дисплеєм. Виміряйте, скільки рядків коду потрібно для еквівалентної обробки вводу. Підхід A: Коли історія заповнена, лінійний буфер зсуває всі записи вгору (O(n) переміщень пам'яті).
Підхід B: Кільцевий буфер просуває вказівник (O(1)).
Що виміряти/спостерігати: Складність коду, кількість операцій з пам'яттю на новий запис, простота реалізації прокрутки. Спостерігайте в Turbo Debugger, скільки інструкцій MOV виконується на оновлення історії в кожному підході.
<br>Розподіл модулів (див. <a class="internal-link" data-href="How-to/include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">Підключення файлів: INCLUDE</a>):
Студент A: Модуль вводу та розбору — обробка клавіатурного вводу, токенізація виразів та логіка обчислення. Відповідає за буфер вводу, процедуру токенізатора та стек обчислення.
Студент B: Модуль відображення та історії — розміщення екрана, рендеринг областей виразу/результату та буфер історії з прокруткою.
Точки інтеграції: Спільна структура даних (наприклад, запис результату, що містить рядок виразу, числовий результат та прапорець помилки), яку Студент A створює, а Студент B використовує для відображення та збереження в історії.
Спільний заголовковий файл, що визначає константи (розміри екрана, кольорові атрибути, розміри буферів) та формат запису результату. <br><a class="internal-link" data-href="How-to/game-logic/G05.md" href="projects/how-to/game-logic/g05.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: обчислення ЗПН та інфіксне з пріоритетом операторів, розбір багатозначних чисел, кільцевий буфер історії.
<br><a class="internal-link" data-href="How-to/dos-services.md" href="projects/how-to/dos-services.html" target="_self" rel="noopener nofollow">INT 21h AH=01h</a> зчитує один символ з відлунням; AH=0Ah зчитує буферизований рядок. Для посимвольного вводу з негайним контролем відображення AH=01h або INT 16h AH=00h є більш гнучкими.
<br><a class="internal-link" data-href="How-to/arithmetic.md" href="projects/how-to/arithmetic.html" target="_self" rel="noopener nofollow">Розбір чисел:</a> Проходьте буфер вводу зліва направо. Для кожної цифри: result = result * 10 + (char - '0'). Використовуйте MUL з константою 10 у регістрі. Слідкуйте за переповненням після кожного MUL.
<br><a class="internal-link" data-href="How-to/number-display.md" href="projects/how-to/number-display.html" target="_self" rel="noopener nofollow">Відображення чисел:</a> Послідовно діліть на 10, зберігайте залишки на стеку PUSH, потім POP та додайте '0' для перетворення в ASCII. Це природно обертає порядок цифр.
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Розміщення відеопам'яті:</a> Кожна символьна клітинка — це 2 байти за адресою B800h:зміщення. Зміщення = (рядок 80 + стовпець) 2. Перший байт = ASCII-код, другий байт = атрибут (колір переднього плану + фону).
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Кільцевий буфер:</a> Підтримуйте змінні head та count. Новий запис записується за адресою (head + count) MOD capacity. Коли заповнений, просувайте head та тримайте count на ємності.
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Стек ЗПН:</a> Простий масив слів зі вказівником стека (змінна-індекс). PUSH інкрементує вказівник та зберігає; оператор знімає два, обчислює, кладе результат.
Очищення області екрана: Цикл по діапазону рядків, записуючи символ пробілу (20h) з потрібним байтом атрибуту.
Точки синхронізації:
Кінець тижня 8: узгодити формат RESULT_RECORD (рядок виразу, числовий результат, прапорець помилки) та угоди про виклик процедур
Кінець тижня 9: обидва модулі компілюються окремо; спільний сегмент .DATA та include-файл з константами зафіксовані
Кінець тижня 11: перша інтеграція — main.asm викликає evaluate_expr → draw_result → add_to_history; основний цикл працює наскрізно
Кінець тижня 13: код зафіксовано; обидва модулі протестовані разом на всіх граничних випадках
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/g05-calculator-with-history.html</link><guid isPermaLink="false">Projects/G05-Calculator-With-History.md</guid><pubDate>Tue, 24 Feb 2026 16:02:43 GMT</pubDate><enclosure url="assets/g05-screen-main.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/g05-screen-main.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[R06-Lights-Out]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🔴 Червоний
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 450-600 (соло) / 650-900 (пара), 2-5 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Пряме записування у відеопам'ять</a> (B800h) — рендеринг сітки 5x5 як великих кольорових блоків, підсвічування курсору та панелей інтерфейсу — L08/P08
<br><a class="internal-link" data-href="How-to/bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Побітові операції</a> (XOR для перемикання) — XOR є природною операцією для Lights Out: state XOR 1 перемикає світло — L04/P05
Упаковане бітове представлення дошки — сітка 5x5 вміщується у 25 біт, що можна зберегти в одній 32-бітній парі (DX:AX) або 4-байтовому масиві, забезпечуючи побітову маніпуляцію всією дошкою — L04/P05
Матричні операції над GF(2) — автоматичний розв'язувач реалізує метод Гауса з використанням XOR як додавання та AND як множення — L04/P05
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви та режими адресації</a> — таблиця головоломок, масиви статистики, матриця розв'язувача — L06/P06
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури з параметрами на стеку</a> — toggle_cell(row, col), check_win(), solve_board(initial_state) — L05/P05
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід</a> (INT 16h) — навігація курсором та перемикання клітинки — L07/P07
<br><a class="internal-link" data-href="How-to/segment-registers.md" href="projects/how-to/segment-registers.html" target="_self" rel="noopener nofollow">Керування сегментними регістрами</a> — ES для B800h, DS для даних — L06/P08
<br><a class="internal-link" data-href="How-to/loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Конструкції циклів</a> — вкладені цикли для рендерингу сітки, зведення рядків розв'язувача — L04/P04
<br><a class="internal-link" data-href="How-to/dos-services.md" href="projects/how-to/dos-services.html" target="_self" rel="noopener nofollow">Рядковий вивід DOS</a> (INT 21h) — екрани меню, повідомлення, відображення статистики — L07/P07
<br><a class="internal-link" data-href="How-to/game-logic/R06.md" href="projects/how-to/game-logic/r06.html" target="_self" rel="noopener nofollow">Ігрова логіка: Lights Out</a> — XOR-перемикання з масками, метод Гауса над GF(2), генерація розв'язних головоломок
Програма відкривається у DOSBox і показує центровану сітку 5x5 "вогнів", відрендерену безпосередньо у відеопам'яті. Кожне світло — це великий, візуально помітний блок — 4 символи завширшки та 2 рядки заввишки — що робить сітку помітною частиною екрану 80x25. Увімкнене ("on") світло світиться як яскраво-жовтий блок (\xDB з атрибутом 0Eh або 6Eh), а вимкнене ("off") — як тьмяний темно-синій або чорний блок. Курсор (миготлива рамка або підсвічений контур) позначає поточну обрану клітинку. Клавіші-стрілки переміщують курсор; Enter або Space перемикає обрану клітинку та її чотирьох ортогональних сусідів (вгору, вниз, вліво, вправо), з обгортанням або обмеженням на краях за класичними правилами.Мета — вимкнути всі вогні. Лічильник ходів внизу відстежує кількість перемикань гравця. Екран також відображає номер поточної головоломки та підказку щодо мінімальної довжини розв'язку. Меню вибору рівня на початку дозволяє гравцю обрати одну з кількох визначених головоломок (щонайменше 8), кожна — відома розв'язна стартова конфігурація. Коли гравець вимикає всі вогні, з'являється вітальне повідомлення з кількістю ходів.Цей проєкт має складність Червоний через компонент математичного розв'язувача: реалізація методу Гауса над GF(2) (полем з двох елементів, де XOR є додаванням, а AND — множенням) потребує розуміння понять лінійної алгебри, що виходять за межі типового програмування на асемблері. Оскільки базова логіка головоломки відносно компактна, проєкт досягає повного обсягу 60 годин через розв'язувач, генератор випадкових головоломок з гарантією розв'язності та екран статистики, що відстежує розв'язані головоломки та найкращу кількість ходів.<br><img alt="Ігровий процес" src="assets/r06-screen-gameplay.png" target="_self"><br><img alt="Перемога" src="assets/r06-screen-win.png" target="_self">Ці компоненти потрібні для досягнення повного обсягу проєкту, але є явно просунутими — студентам слід братися за них після того, як базова гра працює правильно.
Підхід A: Масив з 25 байт, де кожен байт — 0 (вимкнено) або 1 (увімкнено). Перемикання: XOR byte [board + index], 1. Перевірка перемоги: цикл по 25 байтах, OR їх разом.
Підхід B: Упакувати 25 біт у подвійне слово (DX:AX або 4-байтова змінна). Перемикання клітинки (r,c) шляхом XOR з попередньо обчисленою бітовою маскою. Перевірка перемоги: порівняти слово з нулем.
Що вимірювати/спостерігати: Кількість інструкцій для "перемикання + перевірка перемоги" в кожному підході. Підхід B робить всю дошку одним значенням, яке можна порівняти, скопіювати та XOR за одну-дві інструкції, тоді як підхід A зрозуміліший. Обговорити, як бітове упакування дозволяє розв'язувачу GF(2) працювати з рядками як з окремими словами. Підхід A: Спробувати всі 2^25 (~33 мільйони) можливих комбінацій перемикань та перевірити, яка розв'язує дошку. Очевидно нездійсненний навіть як теоретичне порівняння.
Підхід B: Змоделювати головоломку як систему 25 лінійних рівнянь над GF(2) та розв'язати методом Гауса за O(25^3) = 15 625 операцій XOR/AND.
Що вимірювати/спостерігати: Порівняння повчальне саме тому, що підхід A нездійсненний: воно демонструє, чому алгоритмічне мислення важливе і чому метод Гауса потужний. У звіті обчисліть кількість операцій для кожного та обговоріть, яка тактова частота потрібна для повного перебору менш ніж за хвилину. Підхід A: Для клітинки (r,c) обчислити індекси до 5 клітинок (сама + 4 сусіди) з перевіркою меж (if r &gt; 0 then toggle (r-1,c) тощо). Чотири умовних розгалуження.
Підхід B: Попередньо обчислити таблицю "масок перемикання" з 25 записів, де mask[i] — 25-бітне значення з 1 на позиції i та її сусідів. Перемикання стає board XOR mask[i].
Що вимірювати/спостерігати: Інструкції на операцію перемикання. Підхід B швидший (один XOR), але потребує 25 x 4 = 100 байт попередньо обчислених даних. Обговорити компроміс час-пам'ять та як таблиці пошуку є фундаментальною технікою оптимізації в системному програмуванні.
<br>Розподіл модулів (див. <a class="internal-link" data-href="How-to/include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">Підключення файлів: INCLUDE</a>):
Студент A — Інтерфейс та відображення: рендеринг сітки (draw_grid, draw_cell), керування курсором, система меню (вибір рівня, екран статистики), відображення лічильника ходів, екрани перемоги/кінця гри, підтримка колірних тем. Файли: display.asm, menu.asm.
Студент B — Ігрова логіка, розв'язувач та генератор: структура даних дошки, логіка перемикання, виявлення перемоги, розв'язувач методом Гауса над GF(2), послідовність візуалізації розв'язку, генератор випадкових головоломок (застосування випадкових перемикань до розв'язаної дошки), керування даними статистики. Файли: game.asm, solver.asm.
Спільне: main.asm з ініціалізацією, головним ігровим циклом та сегментом даних. Узгодьте представлення дошки (байтовий масив або бітове упакування), конвенції координат та спосіб передачі розв'язку від розв'язувача до модуля відображення.
Точки інтеграції: Студент B надає toggle(row, col), що модифікує стан дошки, та solve(board_state) -&gt; solution_mask. Студент A викликає toggle при вводі користувача та передає дошку в solve, коли гравець запитує підказку або автоматичний розв'язок. <br><a class="internal-link" data-href="How-to/game-logic/R06.md" href="projects/how-to/game-logic/r06.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: XOR-перемикання з таблицею масок, упаковане бітове представлення, метод Гауса над GF(2), генерація розв'язних головоломок.
<br><a class="internal-link" data-href="How-to/lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиця масок перемикання:</a> попередньо обчисліть 25 подвійних слів. Для клітинки (r,c) на бітовій позиції r*5+c маска має біт r*5+c встановленим, плюс біти для кожного дійсного сусіда. Крайні клітинки просто пропускають біт сусіда за межами.
<br><a class="internal-link" data-href="How-to/bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Метод Гауса над GF(2):</a> створіть бітову матрицю 25 рядків x 26 стовпців (розширену). Стовпці 0-24 = маска перемикання для кожної клітинки (на які вогні впливає натискання клітинки j). Стовпець 25 = початковий стан дошки (які вогні увімкнені?). Зведення рядків: для кожного стовпця j знайдіть рядок з бітом j встановленим (опорний), поміняйте його з рядком j, потім виконайте XOR цього рядка з кожним іншим рядком, що має біт j встановленим. Після зведення стовпець 25 містить розв'язок (1 = натиснути цю клітинку).
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Зберігання матриці:</a> кожен рядок — 26 біт, вміщується у подвійне слово (32 біти). Зберігайте як matrix DD 25 DUP(?). Операції з рядками — це одна інструкція XOR на подвійних словах. Використовуйте інструкції 386 (XOR EAX, [matrix + EBX*4]), якщо доступні, або працюйте з парами DX:AX для строгого режиму 8086.
<br><a class="internal-link" data-href="How-to/random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">Випадкове число для генерації головоломок:</a> зчитайте лічильник тактів BIOS за адресою 0040:006Ch, використовуйте як початкове значення для ЛКГ. Для генерації випадкової розв'язної головоломки: повторіть 10-20 разів, оберіть випадкову клітинку (seed MOD 25), застосуйте її маску перемикання до дошки.
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Рендеринг великих клітинок:</a> кожна клітинка займає 4 стовпці та 2 рядки на екрані. Для малювання клітинки (r,c): обчисліть зміщення верхнього лівого кута у відеопам'яті, запишіть 4 пари символ-атрибут для верхнього рядка, потім ще 4 для нижнього рядка (+160 байт зміщення для наступного рядка екрану). Використовуйте REP STOSW для ефективності.
<br><a class="internal-link" data-href="How-to/bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Перевірка перемоги з бітовим упакуванням:</a> якщо дошка зберігається як одне значення в DX:AX, умова перемоги — OR AX, DX / JZ win (технічно спершу AND DX, 01FFh для маскування невикористаних бітів, якщо використовується 32 біти для 25 біт).
Точки синхронізації:
Кінець тижня 8: узгодити представлення дошки (байтовий масив чи бітове упакування), формат таблиці масок перемикання, сигнатури toggle/check_win/solve
Кінець тижня 10: перша спільна демонстрація — сітка рендериться з правильними кольорами, перемикання змінює стан та відображення
Кінець тижня 11: повний цикл гри працює — вибір головоломки → перемикання → виявлення перемоги
Кінець тижня 13: розв'язувач коректно знаходить розв'язок для всіх головоломок; узгодити формат передачі розв'язку від solve до модуля візуалізації
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/r06-lights-out.html</link><guid isPermaLink="false">Projects/R06-Lights-Out.md</guid><pubDate>Tue, 24 Feb 2026 16:02:42 GMT</pubDate><enclosure url="assets/r06-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/r06-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y16-Music-Player]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🟡 Жовтий
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 400–550 (соло) / 650–850 (пара), 2–5 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/pc-speaker.md" href="projects/how-to/pc-speaker.html" target="_self" rel="noopener nofollow">Програмування портів вводу/виводу (PC Speaker)</a> — PIT канал 2 (порти 42h/43h) та вентиль динаміка (порт 61h) для генерації тонів — L08/P08
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять</a> — відображення плейлиста, індикатор прогресу, візуалізація нот у B800h — L08/P08
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви в сегменті даних</a> — зберігання мелодій як паралельних масивів або struct-подібних записів (частота, тривалість) — L06/P06
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури та стекові фрейми</a> — play_note, draw_screen, handle_input як окремі підпрограми — L05/P05
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід через INT 16h</a> — неблокуюча перевірка клавіш (AH=01h) для керування під час відтворення — L07/P07
<br><a class="internal-link" data-href="How-to/timer-delays.md" href="projects/how-to/timer-delays.html" target="_self" rel="noopener nofollow">Таймер та затримка</a> — хронометраж тривалості ноти через активне очікування або тіки таймера BIOS (0040:006Ch) — L07/P07
<br><a class="internal-link" data-href="How-to/arithmetic.md" href="projects/how-to/arithmetic.html" target="_self" rel="noopener nofollow">Арифметика</a> — перетворення частоти у дільник PIT (1193180 / freq), 32-бітне ділення — L04/P04
<br><a class="internal-link" data-href="How-to/bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Побітові операції</a> — встановлення/скидання бітів вентиля динаміка через AND/OR на порту 61h — L04/P04, L05/P05
<br><a class="internal-link" data-href="How-to/addressing-modes.md" href="projects/how-to/addressing-modes.html" target="_self" rel="noopener nofollow">Режими адресації</a> — індексований доступ до масивів мелодій, обчислені зміщення для відображення плейлиста — L06/P06
<br><a class="internal-link" data-href="How-to/game-logic/Y16.md" href="projects/how-to/game-logic/y16.html" target="_self" rel="noopener nofollow">Ігрова логіка: Музичний плеєр</a> — обчислення дільника PIT, хронометраж нот, керування темпом, FSM пауза/відтворення
Програма відкривається екраном плейлиста: три або більше назв пісень перераховані, з поточним вибором підсвіченим яскравим кольором. Користувач обирає пісню клавішами-стрілками та Enter, і відтворення починається негайно — мелодія грає через PC Speaker (системний динамік), тоді як екран оживає візуальним відображенням. Індикатор прогресу показує, наскільки просунулося відтворення поточної пісні. Над ним назва поточної ноти (напр., "C4", "G#5") відображається великим текстом, оновлюючись з кожною нотою. Декоративна візуалізація — або прокручувана "клавіатура фортепіано" з підсвіченою активною клавішею, або "еквалайзер" зі стрибаючими стовпчиками — заповнює верхню частину екрану, все рендериться через прямий запис у відеопам'ять.Під час відтворення користувач має повний контроль: пробіл призупиняє та відновлює, стрілки вліво/вправо переключають на попередню або наступну пісню, а клавіші +/- регулюють темп (роблячи мелодію швидшою або повільнішою без зміни висоти тону). Рядок стану показує назву пісні, поточний відсоток темпу та поточну/загальну кількість нот. Коли пісня закінчується, відтворення переходить до наступної у плейлисті, обертаючись до початку.Усі мелодії зберігаються у сегменті даних як масиви пар частота/тривалість, що робить додавання нових пісень простим — достатньо визначити нові таблиці даних. Увесь інтерфейс працює в текстовому режимі 80x25 з щедрим використанням кольорових атрибутів для створення привабливої естетики плеєра.<br><img alt="Відтворення мелодії з плейлистом, візуалізацією та індикатором прогресу" src="assets/y16-screen-playback.png" target="_self">
Підхід A: Використовує калібровану інструкцію LOOP для очікування тривалості ноти — просто, але чутливо до швидкості cycles DOSBox.
Підхід B: Зчитує лічильник тіків BIOS за адресою 0040:006Ch (інкрементується ~18.2 разів/сек) і очікує, поки мине відповідна кількість тіків.
Що вимірювати/спостерігати: Наскільки стабільна тривалість ноти при зміні cycles DOSBox з 3000 на 30000? Підхід з тіками таймера залишається стабільним, тоді як активне очікування дає кардинально різні результати. Це ілюструє апаратно-залежний проти апаратно-незалежний хронометраж. Підхід A: Зберігає частоти в одному масиві, а тривалості в окремому паралельному масиві (freqs DW 262, 294, 330, ... / durs DW 500, 500, 250, ...), звертаючись до них за відповідним індексом.
Підхід B: Чергує їх як пари (melody DW 262, 500, 294, 500, 330, 250, ...), звертаючись до частоти за [SI] та тривалості за [SI+2], потім збільшуючи SI на 4.
Що вимірювати/спостерігати: Дружність до кешу (теоретична), зрозумілість коду, простота додавання пауз та скільки регістрів потрібно для відстеження позиції. Це рішення про компонування даних з реальними наслідками. Підхід A: Попередньо обчислює таблицю всіх частот нот та їхніх назв (напр., 262="C4", 294="D4"), потім шукає в таблиці найближче відповідність.
Підхід B: Використовує логарифмічну математику або послідовне ділення навпіл для визначення октави та півтону з будь-якої частоти.
Що вимірювати/спостерігати: Розмір таблиці проти складність коду, точність для нестандартних частот та швидкість виконання. На практиці на 8086 без модуля з рухомою комою таблиця пошуку набагато практичніша — гарний урок про вибір правильного підходу для апаратного забезпечення.
<br>Розподіл модулів (див. <a class="internal-link" data-href="How-to/include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">Підключення файлів: INCLUDE</a>):
Студент A: Аудіорушій — програмування PIT, генерація тонів, хронометраж тривалості нот, структури даних мелодій, логіка відтворення/паузи/наступна/попередня. Надає: play_note(freq, duration), silence_speaker, next_note, get_current_note.
Студент B: Відображення та UI — екран плейлиста, індикатор прогресу, відображення назви ноти, візуалізація (фортепіано або еквалайзер), опитування клавіатури, UI регулювання темпу. Надає: draw_playlist, draw_progress, draw_note_name, draw_visualization, poll_keyboard.
Точка інтеграції: Спільні змінні для індексу поточної пісні, індексу поточної ноти всередині пісні, стану відтворення (грає/пауза) та множника темпу. Студент A оновлює позицію ноти; Студент B зчитує її для оновлення дисплея. Обидва працюють в одному головному циклі: відтворити одну ноту, опитати клавіатуру, оновити дисплей, повторити. <br><a class="internal-link" data-href="How-to/game-logic/Y16.md" href="projects/how-to/game-logic/y16.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: обчислення дільника PIT, хронометраж тривалості ноти, керування темпом, FSM пауза/відтворення.
<br><a class="internal-link" data-href="How-to/pc-speaker.md" href="projects/how-to/pc-speaker.html" target="_self" rel="noopener nofollow">Генерація тону через PIT:</a> (1) обчислити дільник = 1193180 / частота; (2) відправити командний байт 0B6h на порт 43h (команда PIT: канал 2, режим 3, двійковий); (3) відправити молодший байт дільника на порт 42h; (4) відправити старший байт дільника на порт 42h; (5) зчитати порт 61h, виконати OR з 03h, записати назад на порт 61h (увімкнути динамік). Для вимкнення: зчитати порт 61h, виконати AND з 0FCh, записати назад.
<br><a class="internal-link" data-href="How-to/timer-delays.md" href="projects/how-to/timer-delays.html" target="_self" rel="noopener nofollow">Таймер BIOS:</a> Лічильник тіків — це DWORD за адресою 0040:006Ch. Для зчитування: MOV AX, 0040h / MOV ES, AX / MOV EAX, ES:[006Ch] (або зчитати як два слова на 8086). Один тік = ~55 мс, тому 18 тіків — це приблизно 1 секунда.
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Неблокуюча перевірка клавіші:</a> MOV AH, 01h / INT 16h / JZ no_key. Якщо ZF=0, зчитайте клавішу за допомогою MOV AH, 00h / INT 16h.
Стандартні частоти нот (налаштування A4 = 440 Гц): C4=262, D4=294, E4=330, F4=349, G4=392, A4=440, B4=494, C5=523. Визначте їх як константи EQU.
<br><a class="internal-link" data-href="How-to/arithmetic.md" href="projects/how-to/arithmetic.html" target="_self" rel="noopener nofollow">Арифметика індикатора прогресу:</a> Якщо у пісні N нот і ви на ноті i, заповніть i * ширина_індикатора / N символів блочним символом (0DBh), а решту — легшим символом (0B0h). Використовуйте MUL та DIV для цього обчислення.
<br><a class="internal-link" data-href="How-to/lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиця пошуку назв нот:</a> Визначте таблицю пар (частота, зміщення_назви), відсортовану за частотою. Шукайте найближчу відповідність лінійним проходом — таблиця матиме щонайбільше ~40 записів для 3+ октав.
Точки синхронізації:
Кінець тижня 9: узгодити спільний .DATA (current_song DW 0, current_note_idx DW 0, play_state DB 1, tempo_multiplier DW 100), формат масивів мелодій (пари DW freq, dur) та прототипи процедур
Кінець тижня 11: перша інтеграція — мелодія грає з одночасним оновленням екрану (плейлист, прогрес, назва ноти), неблокуючий ввід працює
Кінець тижня 13: програма повністю працює (3 мелодії, пауза/відтворення, темп, візуалізація), обидва розділи README готові
Кінець тижня 14: фінальне тестування на різних налаштуваннях cycles DOSBox, обидва Loom-відео записані
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/y16-music-player.html</link><guid isPermaLink="false">Projects/Y16-Music-Player.md</guid><pubDate>Tue, 24 Feb 2026 16:02:41 GMT</pubDate><enclosure url="assets/y16-screen-playback.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y16-screen-playback.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y12-Checkers]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🟡 Жовтий
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 500-650 (соло) / 700-900 (пара), 2-5 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Пряме записування у відеопам'ять</a> (B800h) — рендеринг дошки 8x8 з атрибутами кольору для клітинок, фішок, підсвічування — L08/P08
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Двовимірний масив та індексація</a> — дошка 8x8, збережена як 64 байти, доступ через row * 8 + col; лише темні клітинки (32 з 64) активні — L06/P06
<br><a class="internal-link" data-href="How-to/flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Багатокрокова валідація ходів</a> — діагональний рух, перевірки меж, виявлення стрибків, ланцюжок множинних стрибків потребують багатошарової умовної логіки — L04/P04
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури з параметрами</a> — get_legal_moves(row, col), execute_move(from_r, from_c, to_r, to_c), ai_turn() — L05/P05
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Дисципліна стеку</a> — зберігання списків ходів, збереження регістрів у глибоких ланцюжках викликів (обчислення ШІ викликає валідацію, яка викликає перевірку меж) — L05/P05
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід</a> (INT 16h) — навігація курсором, вибір фішки, підтвердження ходу — L07/P07
<br><a class="internal-link" data-href="How-to/segment-registers.md" href="projects/how-to/segment-registers.html" target="_self" rel="noopener nofollow">Сегментні регістри</a> — ES для відеосегменту B800h, DS для даних — L06/P08
<br><a class="internal-link" data-href="How-to/bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Побітові операції</a> — кодування стану фішки (порожня/біла/червона/дамка) у байтах клітинок з використанням бітових прапорців, перевірка через AND/TEST — L04/P05
<br><a class="internal-link" data-href="How-to/loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Пошук та перелічення</a> — сканування всіх фішок для пошуку дозволених ходів, особливо для примусового взяття — L04/P06
<br><a class="internal-link" data-href="How-to/game-logic/Y12.md" href="projects/how-to/game-logic/y12.html" target="_self" rel="noopener nofollow">Ігрова логіка: Шашки</a> — примусове взяття, ланцюжки стрибків, перетворення в дамку, таблиця напрямків
Програма запускається у DOSBox і відображає повну шашкову дошку 8x8, відрендерену у текстовому режимі відеопам'яті. Дошка використовує чергування темних і світлих кольорових клітинок — темні клітинки намальовані як блокові символи з коричневим або темно-червоним атрибутом, світлі клітинки — як контрастний світліший відтінок. Візуальний ефект безпомилково нагадує шашкову дошку. Фішки розташовуються лише на темних клітинках: фішки одного гравця з'являються як яскраво-білі символи O, фішки суперника (ШІ) — як яскраво-червоні символи O. Дамки відрізняються символом K або яскравішим/іншим атрибутом.Курсор підсвічує поточну обрану клітинку відмінним кольором рамки або зміною фону. Коли хід людського гравця, він переміщується клавішами-стрілками до однієї зі своїх фішок та натискає Enter для вибору. Після вибору всі дозволені ходи для цієї фішки підсвічуються як виділені клітинки призначення. Гравець переміщується до призначення та натискає Enter для підтвердження ходу. Якщо взяття (стрибок) доступне будь-де на дошці, гра примусово вимагає взяття: пропонуються лише ходи-стрибки. Множинні стрибки (ланцюжок взяттів за один хід) підтримуються. Після ходу людини ШІ робить свій хід — у базовій версії він надає пріоритет взяттю, а інакше обирає випадковий дозволений хід. Інформаційна панель збоку або під дошкою показує, чий хід, кількість взятих фішок кожної сторони та базові інструкції.Гра закінчується, коли в одного гравця не залишається фішок або дозволених ходів. Відображається повідомлення про перемогу або поразку.<br><img alt="Ігровий процес" src="assets/y12-screen-gameplay.png" target="_self"><br><img alt="Множинний стрибок" src="assets/y12-screen-multijump.png" target="_self"><br><img alt="Кінець гри" src="assets/y12-screen-gameover.png" target="_self">
Підхід A: Масив 64 байти, індексований через row * 8 + col. Світлі клітинки завжди порожні (марнування місця), але адресація проста: будь-яка пара (row, col) відображається безпосередньо в індекс.
Підхід B: Масив 32 байти, що містить лише темні клітинки. Обчислення індексу складніше: темна клітинка на (row, col) відображається в індекс row * 4 + col / 2 (приблизно). Економить 32 байти, але кожен доступ потребує функції відображення.
Що вимірювати/спостерігати: Розмір коду функції get_cell(row, col) у кожному підході. Ілюструє компроміс між ефективністю пам'яті та простотою адресації. На 8086 з 64KB сегментами економія 32 байтів незначна, але складність індексації підходу B вводить помилки та витрачає інструкції. Підхід A: Коли гравець обирає фішку, сканувати 2 (або 4 для дамок) діагональних напрямки для пошуку дозволених ходів на ходу. При перевірці примусового взяття сканувати всі 12 дружніх фішок кожного разу.
Підхід B: На початку кожного ходу попередньо обчислити всі дозволені ходи для всіх дружніх фішок у масив списку ходів. Потім використовувати цей список для підсвічування, валідації та примусового взяття.
Що вимірювати/спостерігати: Швидкість реакції (обидва миттєві на 8086 для 12 фішок), але порівняти простоту коду. Підхід B чітко розділяє "генерацію ходів" та "вибір ходу", роблячи примусове взяття тривіальним (просто перевірити, чи є в списку ходів стрибки). Підхід A змішує їх. Підхід A: Зібрати всі дозволені ходи у список, обрати один випадково (або перше взяття, якщо є).
Підхід B: Для кожного дозволеного ходу зімітувати його на копії дошки, потім оцінити результуючу дошку функцією оцінки (наприклад, підрахунок матеріалу: звичайна фішка = 1, дамка = 2). Обрати хід з найвищою оцінкою.
Що вимірювати/спостерігати: Якість ШІ (відсоток перемог проти людини-новачка), складність коду. Обговорити, як це пов'язано з пошуком мінімаксу в ігровому ШІ та чому навіть перегляд на один крок різко покращує гру.
<br>Розподіл модулів (див. <a class="internal-link" data-href="How-to/include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">Підключення файлів: INCLUDE</a>):
Студент A — Відображення та інтерфейс: рендеринг дошки (draw_board, draw_piece, draw_cursor, highlight_moves), обробка клавіатурного вводу, потік вибору фішки та підтвердження ходу, індикатор ходу, відображення лічильника взятих фішок, екрани меню та кінця гри. Файли: display.asm, input.asm.
Студент B — Ігрова логіка та ШІ: структура даних дошки, валідація ходів (is_legal_move, get_legal_moves), виконання ходу (execute_move), логіка взяття та множинних стрибків, примусове взяття, перетворення в дамку, вибір ходу ШІ, виявлення кінця гри. Файли: game.asm, ai.asm.
Спільне: main.asm з ініціалізацією та ігровим циклом, сегмент даних з масивом дошки, лічильниками фішок, змінною ходу. Узгодьте кодування клітинок дошки: наприклад, 0 = порожня, 1 = біла звичайна, 2 = біла дамка, 3 = червона звичайна, 4 = червона дамка.
Точки інтеграції: Студент B надає get_legal_moves(row, col) -&gt; move_list, count та execute_move(from, to) -&gt; captures, promoted?. Студент A викликає get_legal_moves для підсвічування призначень та execute_move після підтвердження гравцем. Для ходів ШІ ai_turn() Студента B повертає обраний хід, а Студент A анімує його. <br><a class="internal-link" data-href="How-to/game-logic/Y12.md" href="projects/how-to/game-logic/y12.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: сканування примусового взяття, ланцюжки множинних стрибків, перетворення в дамку, таблиця діагональних напрямків.
<br><a class="internal-link" data-href="How-to/bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Кодування клітинок дошки:</a> 0 = порожня, 1 = біла фішка, 2 = біла дамка, 5 = червона фішка, 6 = червона дамка. Використовуйте TEST AL, 04h для перевірки, чи фішка червона (біт 2 встановлений) та TEST AL, 02h для перевірки, чи це дамка (біт 1 встановлений). Це кодування дозволяє прості побітові перевірки.
<br><a class="internal-link" data-href="How-to/lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиця діагональних напрямків:</a> dir_table DB -1,-1, -1,1, 1,-1, 1,1 (row_delta, col_delta для NW, NE, SW, SE). Білі рухаються вперед = рядки зменшуються (індекси 0-1). Червоні рухаються вперед = рядки збільшуються (індекси 2-3). Дамки використовують всі 4 (індекси 0-3).
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Колір клітинки у відеопам'яті:</a> темні клітинки (де (row + col) непарне) отримують атрибут 60h (коричневий на коричневому = суцільний коричневий блок). Світлі клітинки отримують атрибут 70h (білий на білому = суцільний світлий блок). Фішки малюються поверх атрибуту темної клітинки: \x4F (O) з атрибутом 6Fh (яскраво-білий на коричневому) для білих фішок.
<br><a class="internal-link" data-href="How-to/loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Сканування примусового взяття:</a> цикл по всіх 64 клітинках. Для кожної клітинки з дружньою фішкою перевірити 2 або 4 діагональних напрямки: якщо сусідня клітинка має ворога і клітинка за ним порожня, взяття доступне. Встановіть must_capture = 1 та вийдіть зі сканування.
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Структура списку ходів:</a> move_list DB 48 DUP(4 DUP(?)) — до 48 ходів, кожен зберігається як {from_row, from_col, to_row, to_col}. Змінна move_count відстежує кількість дійсних. При генерації ходів, якщо must_capture встановлений, додавати лише ходи-стрибки.
<br><a class="internal-link" data-href="How-to/random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">Випадковий вибір ШІ:</a> підрахуйте дозволені ходи в move_count, згенеруйте випадкове число R = timer_ticks MOD move_count, виконайте move_list[R].
Точки синхронізації:
Кінець тижня 8: узгодити кодування клітинок дошки (бітові прапорці), формат move_list (4 байти на хід: from_row, from_col, to_row, to_col), прототипи get_legal_moves/execute_move
Кінець тижня 10: перша спільна демонстрація — дошка рендериться зі стартовою позицією, get_legal_moves повертає коректні ходи для обраної фішки
Кінець тижня 11: повний ігровий цикл працює — хід людини з вибором фішки → хід ШІ → чергування до кінця гри
Кінець тижня 13: множинні стрибки, примусове взяття та перетворення в дамку працюють коректно; узгодити структуру README
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/y12-checkers.html</link><guid isPermaLink="false">Projects/Y12-Checkers.md</guid><pubDate>Tue, 24 Feb 2026 16:02:41 GMT</pubDate><enclosure url="assets/y12-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y12-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y21-ASCII-Art-Editor]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🟡 Жовтий
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 500-650 (соло) / 700-900 (пара), 2-5 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять</a> (кожна операція малювання записує пари символ+атрибут до ES:DI за адресою B800h) — L08/P08
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Адресація двовимірних масивів</a> (позиція на полотні (рядок, стовпець) відповідає зміщенню = рядок 160 + стовпець 2; постійне використання цієї формули) — L06/P06
<br><a class="internal-link" data-href="How-to/bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Побітові операції</a> (маніпуляція атрибутом кольору: передній план = attr AND 0Fh, фон = (attr AND 70h) SHR 4, компонування атрибутів з OR) — L04/P04
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Стекоподібна структура скасувань</a> (push/pop 3840-байтових знімків полотна у LIFO-стек в пам'яті) — L05/P05
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід через INT 16h</a> (клавіші-стрілки, функціональні клавіші, друковані символи, комбінації Ctrl+клавіша) — L07/P07
<br><a class="internal-link" data-href="How-to/file-io.md" href="projects/how-to/file-io.html" target="_self" rel="noopener nofollow">Файловий ввід/вивід DOS через INT 21h</a> (збереження/завантаження полотна як двійкового файлу) — L09/P09
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури з параметрами</a> (draw_line, draw_rect, flood_fill як процедури, що викликаються з координатами на стеку або в регістрах) — L05/P05
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Рекурсивні або стекові алгоритми</a> (заливка використовує явний стек, щоб уникнути переповнення апаратного стеку) — L05/P05
<br><a class="internal-link" data-href="How-to/segment-registers.md" href="projects/how-to/segment-registers.html" target="_self" rel="noopener nofollow">Сегментні регістри</a> (ES=B800h для відеопам'яті, DS для даних; перемикання ES для файлового вводу/виводу та блочного копіювання) — L03/P03
REP MOVSB/MOVSW (блочне копіювання для знімків скасувань: копіювання 3840 байтів між відеопам'яттю та буферами сегмента даних) — L06/P06
<br><a class="internal-link" data-href="How-to/game-logic/Y21.md" href="projects/how-to/game-logic/y21.html" target="_self" rel="noopener nofollow">Ігрова логіка: редактор ASCII-арту</a> — малювання ліній/прямокутників, flood fill з явним стеком, знімки для скасування
Програма реалізує повноекранне полотно для малювання ASCII-графіки у текстовому режимі DOSBox. Увесь екран 80x25 використовується: верхні 24 рядки утворюють полотно (80x24 = 1920 символьних комірок), а нижній рядок слугує рядком стану, що показує позицію курсора (рядок, стовпець), поточний вибраний символ для малювання, поточні кольори переднього плану та фону як кольоровий зразок, назву активного інструменту та ім'я файлу. Блимаючий курсор — відображений як інвертоване підсвічування комірки під ним — вільно переміщується по полотну клавішами-стрілками. Кожна комірка на полотні — це пара байтів у відеопам'яті: ASCII-символ та атрибут кольору.Редактор пропонує кілька інструментів малювання, які перемикаються клавішею Tab: Freehand (введення символу розміщує його на курсорі та просувається вправо), Line (горизонтальна або вертикальна: позначити початкову точку, перемістити до кінця, натиснути Enter для малювання заповненої лінії поточним символом), Rectangle (позначити один кут, перемістити до протилежного кута, натиснути Enter для малювання контурного або заповненого прямокутника), Eraser (переміщується по полотну, замінюючи кожну комірку пробілом) та Flood Fill (заповнює зв'язану область ідентичних символів поточним символом та кольором). Колір переднього плану циклічно змінюється серед 16 значень клавішею F1, фон серед 8 значень клавішею F2. Вибір символу дозволяє користувачу вибрати будь-який ASCII-символ, ввівши його безпосередньо, або циклічно переглядати підібрану палітру (блокові символи, рамкові символи, затінення) клавішами + та -.Стан полотна (1920 байтів символів + 1920 байтів атрибутів = 3840 байтів) можна зберегти у файл і завантажити назад. Стек скасувань записує повний стан полотна перед кожною деструктивною операцією інструменту (freehand є безперервним, тому він зберігає стан на початку штриха і відновлює при Ctrl+Z). З глибиною стеку 3-5 станів це коштує 12-19 КБ пам'яті — цілком реалістично у 64-КБ сегменті даних при ретельному плануванні.<br><img alt="Полотно редактора з малюнком у режимі вільного малювання" src="assets/y21-screen-editor.png" target="_self"><br><img alt="Демонстрація інструментів: лінія, прямокутник, заливка, палітра кольорів" src="assets/y21-screen-tools.png" target="_self">
Підхід A: Реалізує заливку як рекурсивну процедуру: для кожної комірки CALL себе для чотирьох сусідів. На великій області (напр., 40x20 = 800 комірок) це записує ~4 слова на виклик (адреса повернення, збережені регістри) x 800 глибина = 3200 слів, що може переповнити стандартний сегмент стеку.
Підхід B: Використовує явний стек у сегменті даних: масив слів, де кожен запис містить упаковану координату (рядок, стовпець), з ручним індексом push/pop.
Що виміряти/спостерігати: Максимальна площа заповнення до збою (протестувати обидва на порожньому полотні), простота коду (рекурсивний — ~10 рядків; ітеративний потребує явного керування стеком) та використання сегмента стеку (спостерігати за SP у Turbo Debugger під час рекурсивної заливки). Підхід A: Зберігає буфер полотна у тому ж черезрядковому форматі, що й відеопам'ять (3840 байтів: char0, attr0, char1, attr1, ...), тому збереження/завантаження та скасування можуть використовувати одне REP MOVSW між буфером і B800h.
Підхід B: Використовує два окремих масиви: 1920 байтів символів та 1920 байтів атрибутів, що спрощує операції лише із символами (наприклад, порівняння при заливці), оскільки немає байта атрибуту, який треба пропускати.
Що виміряти/спостерігати: Кількість інструкцій для порівняння при заливці (підхід A повинен читати кожен другий байт; підхід B читає послідовні байти), ефективність збереження/завантаження (підхід A — одне блочне копіювання; підхід B потребує двох) та загальне використання пам'яті (обидва використовують 3840 байтів). Підхід A: Малює горизонтальну лінію, обчислюючи початкове зміщення (рядок 160 + стовпець 2), потім інкрементуючи на 2 у циклі для кожної наступної комірки — одне ADD на комірку.
Підхід B: Повторно обчислює повне зміщення для кожної комірки (рядок 160 + стовпець 2 з інкрементуванням стовпця) — одне MUL на комірку.
Що виміряти/спостерігати: Кількість інструкцій на намальовану комірку (підрахувати в Turbo Debugger), складність коду для розширення на діагональні лінії (підхід B узагальнюється природніше) та ризик накопичення помилок зміщення у підході A, якщо лічильник циклу неправильний.
<br>Розподіл модулів (див. <a class="internal-link" data-href="How-to/include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">Підключення файлів: INCLUDE</a>):
Студент A — Модуль інструментів та логіки: алгоритм малювання ліній, алгоритм малювання прямокутників, алгоритм заливки, керування стеком скасувань (push/pop знімків полотна), дані палітри символів. Файл: tools.asm.
Студент B — Модуль UI та вводу/виводу: відображення та переміщення курсора, відображення рядка стану, циклічна зміна кольорів, перемикання інструментів, диспетчеризація клавіатурного вводу, збереження/завантаження файлу, підказка підтвердження виходу. Файл: ui.asm.
Спільний головний модуль: ініціалізація (очищення полотна, встановлення кольорів за замовчуванням), головний цикл подій з диспетчеризацією до інструментів або UI, оголошення буфера полотна, спільні змінні стану. Файл: main.asm.
Точки інтеграції:
Узгодити спільні змінні: cursor_row, cursor_col (байти), cur_char (байт, поточний символ малювання), cur_attr (байт, об'єднані передній план+фон), cur_tool (байт, перерахування інструментів 0-4).
Узгодити, що всі інструменти зчитують поточний стан з цих змінних і записують безпосередньо у відеопам'ять за адресою B800h. Інструменти студента A повертаються до головного циклу, який потім викликає update_status_bar студента B.
Система скасувань (студент A) повинна бути викликаною з головного циклу перед будь-якою операцією інструменту: push_undo копіює B800h у буфер скасувань, pop_undo копіює назад.
Сплануйте розкладку пам'яті ДО написання коду. Спочатку виділіть усі основні буфери в сегменті даних, потім будуйте процедури навколо них.Бюджет пам'яті (полотно 80x24): Буфер полотна (символ+атрибут): 3 840 байтів Буфери скасувань (3 рівні): 11 520 байтів Палітра/стан інструменту: 200 байтів Буфер файлового вводу/виводу: 512 байтів ──────────────────────────────────────── Всього сегмент даних: ~16 072 байтів (вміщується у 64 КБ) <br><a class="internal-link" data-href="How-to/game-logic/Y21.md" href="projects/how-to/game-logic/y21.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: малювання ліній та прямокутників, заливка (flood fill) з явним стеком, знімки полотна для скасування.
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Відображення курсора:</a> Курсор — це візуальне підсвічування, а не апаратний курсор. Зберігати символ та атрибут на поточній позиції. Для малювання курсора XOR-увати байт атрибуту з 7Fh (інвертує передній план та фон). Для стирання курсора перед переміщенням відновити оригінальний атрибут. Це дозволяє уникнути необхідності читати з відеопам'яті, якщо ви зберігаєте атрибут «під курсором» у змінній.
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Формула полотно-до-зміщення:</a> Зміщення в B800h для позиції (рядок, стовпець) = рядок 160 + стовпець 2. Це використовується постійно. Попередньо обчислити рядок * 160 за допомогою SHL (рядок 128 + рядок 32 = рядок &lt;&lt; 7 + рядок &lt;&lt; 5), щоб уникнути повільного MUL. Зберегти результат у DI, потім ADD стовпець * 2.
<br><a class="internal-link" data-href="How-to/bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Циклічна зміна кольорів:</a> Передній план циклічно змінюється 0-15 (4 біти), фон циклічно змінюється 0-7 (3 біти). Компонування атрибуту: attr = (bg SHL 4) OR fg. Показати зразкову комірку на рядку стану з цим атрибутом та блоковим символом (DBh).
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Алгоритм заливки (явний стек):</a> Записати у стек початкову (рядок, стовпець). Цикл: витягти комірку; якщо вона відповідає цільовому символу, намалювати новий символ+колір і записати у стек чотирьох сусідів (вгору, вниз, вліво, вправо), якщо вони в межах. Позначати відвідані комірки, малюючи їх негайно — заповнена комірка більше не відповідає цільовому, тому не буде записана у стек повторно.
<br><a class="internal-link" data-href="How-to/file-io.md" href="projects/how-to/file-io.html" target="_self" rel="noopener nofollow">Формат файлу:</a> Зберігати сирі 3840 байтів відеопам'яті (рядки 0-23, 80 стовпців, 2 байти кожен) як двійковий файл. При завантаженні читати 3840 байтів безпосередньо у B800h:0000. Ім'я файлу може запитуватися або бути жорстко заданим (напр., DRAWING.BIN).
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Виявлення функціональних клавіш:</a> Після INT 16h, якщо AL=0, перевірити AH: F1=3Bh, F2=3Ch, F3=3Dh тощо. Використовувати їх для циклічної зміни кольорів та команд інструментів. Tab (AL=09h) може перемикати інструменти.
<br><a class="internal-link" data-href="How-to/segment-registers.md" href="projects/how-to/segment-registers.html" target="_self" rel="noopener nofollow">Блочне копіювання для скасування:</a> Налаштувати DS:SI на B800h:0000 та ES:DI на буфер скасувань у сегменті даних (або навпаки). Встановити CX=1920. Виконати REP MOVSW. Не забути зберегти та відновити DS та ES при зміні сегментів.
Точки синхронізації:
Кінець тижня 9: узгодити спільні змінні (cursor_row, cursor_col, cur_char, cur_attr, cur_tool), бюджет пам'яті сегмента даних та прототипи процедур
Кінець тижня 11: перша інтеграція — головний цикл подій викликає інструменти студента A та UI студента B; всі інструменти читають стан зі спільних змінних і пишуть у B800h
Кінець тижня 12: всі 5 інструментів працюють (freehand, лінія, прямокутник, ластик, заливка); збереження/завантаження файлу працює
Кінець тижня 14: код зафіксовано, звіт та відео готові
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/y21-ascii-art-editor.html</link><guid isPermaLink="false">Projects/Y21-ASCII-Art-Editor.md</guid><pubDate>Tue, 24 Feb 2026 16:02:40 GMT</pubDate><enclosure url="assets/y21-screen-editor.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y21-screen-editor.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y11-Frogger]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🟡 Жовтий
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 400-550 (соло) / 600-800 (пара), 2-5 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/interrupt-handlers.md" href="projects/how-to/interrupt-handlers.html" target="_self" rel="noopener nofollow">Встановлення обробника переривання таймера</a> (INT 1Ch) — керування рухом смуг зі стабільною частотою, незалежною від швидкості CPU — L07/P07
<br><a class="internal-link" data-href="How-to/interrupt-handlers.md" href="projects/how-to/interrupt-handlers.html" target="_self" rel="noopener nofollow">Збереження/відновлення вектора переривань</a> — правильна інсталяція та деінсталяція обробника переривань — L07/P07
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Пряме записування у відеопам'ять</a> (B800h) — малювання смуг, транспорту, жаби та безпечних зон як кольорових символів — L08/P08
<br><a class="internal-link" data-href="How-to/segment-registers.md" href="projects/how-to/segment-registers.html" target="_self" rel="noopener nofollow">Керування сегментними регістрами</a> — ES вказує на B800h для відображення, DS для даних; ретельне збереження/відновлення в обробнику переривань — L06/P08
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Дані смуг на основі масивів</a> — кожна смуга описана структурою: напрямок, швидкість, патерн транспорту, поточне зміщення — L06/P06
<br><a class="internal-link" data-href="How-to/arithmetic.md" href="projects/how-to/arithmetic.html" target="_self" rel="noopener nofollow">Модулярна арифметика</a> для прокрутки — зміщення смуг обгортаються за допомогою MOD 80 (або AND з маскою) для створення нескінченної прокрутки — L04/P04
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід</a> (INT 16h) — неблокуюче опитування для швидкого реагування руху жаби — L07/P07
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури та кадри стеку</a> — draw_lane, move_lanes, check_collision, reset_frog як окремі процедури — L05/P05
<br><a class="internal-link" data-href="How-to/flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Умовна логіка</a> — виявлення зіткнень, обмеження меж, перевірки підвищення рівня — L04/P04
<br><a class="internal-link" data-href="How-to/lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Параметризовані структури даних</a> — таблиця дескрипторів смуг забезпечує проєктування рівнів, кероване даними — L06/P06
<br><a class="internal-link" data-href="How-to/game-logic/Y11.md" href="projects/how-to/game-logic/y11.html" target="_self" rel="noopener nofollow">Ігрова логіка: Frogger</a> — прокрутка смуг з різними швидкостями, виявлення зіткнень, прогресія рівнів
<br>Див. також <a class="internal-link" data-href="Y10-Space-Invaders.md" href="projects/y10-space-invaders.html" target="_self" rel="noopener nofollow">Y10 Space Invaders</a> — також використовує рух, керований таймером, та зіткнення з сутностями. Frogger зосереджується на прокрутці смуг з різними швидкостями та проєктуванні на основі таблиць.Програма запускається у DOSBox і відображає кольоровий екран текстового режиму 80x25, що представляє вид зверху на дорогу з кількома смугами руху. Нижній рядок — це "стартова зона" — безпечна зелена смуга, де починає жаба гравця (наприклад, символ \x02 смайлик або \x04 ромб у яскраво-зеленому кольорі). Вище — кілька смуг руху: кожна смуга — це повний рядок символів, що прокручується вліво або вправо з постійною швидкістю, представляючи автомобілі, вантажівки або перешкоди у вигляді кольорових блокових символів (\xDB, \xB2) різної довжини (2-4 символи). Між групами смуг руху є тонкі рядки безпечних зон (смуга іншого кольору), де жаба може відпочити без небезпеки. Верхній рядок — "зона фінішу" — досягнення його дає очко та повертає жабу донизу.Жаба рухається у відповідь на натискання клавіш-стрілок: одна клітинка за натискання, в будь-якому з чотирьох кардинальних напрямків. Рух дискретний та миттєвий. Тим часом транспорт рухається безперервно, керований перериванням таймера (INT 1Ch). Якщо жаба займає ту саму клітинку, що й рухомий транспорт, одне життя втрачається: жаба коротко блимає, потім з'являється знову в стартовій зоні. Гравець починає з 3 життів. Рядок стану відображає рахунок (успішні перетини), залишок життів та поточний рівень.Після кожного успішного перетину рівень збільшується і транспорт рухається швидше (дільник тактів для руху смуг зменшується). Гра продовжується, поки не будуть втрачені всі життя, після чого з'являється екран GAME OVER з фінальним рахунком.<br><img alt="Ігровий процес" src="assets/y11-screen-gameplay.png" target="_self"><br><img alt="Кінець гри" src="assets/y11-screen-gameover.png" target="_self">
Підхід A: Кожен такт повністю переписувати весь рядок смуги з 80 символів у відеопам'ять на основі патерну смуги та поточного зміщення.
Підхід B: Зсунути вміст рядка на одну позицію (використовуючи REP MOVSW для зсуву 79 слів вліво/вправо у відеопам'яті) і вставити лише щойно з'явлений символ на краю.
Що вимірювати/спостерігати: Кількість записів у відеопам'ять за такт на смугу (160 байт для A проти~162 байти для зсуву слів + 2 байти для B — майже однаково, але B уникає перерахунку патерну). Обговорити, чи REP MOVSW з перекриттям джерела/призначення працює правильно (працює, якщо прапорець напрямку встановлений відповідно). Підхід B імітує апаратні регістри прокрутки реального обладнання VGA. Підхід A: Перевіряти зіткнення лише коли жаба рухається (при натисканні клавіші). Між рухами жаба "безпечна", навіть якщо автомобіль прокручується в її клітинку.
Підхід B: Перевіряти зіткнення кожного разу, коли будь-яка смуга рухається (кожен такт таймера). Жаба може бути збита автомобілем, що прокручується в неї.
Що вимірювати/спостерігати: Як часто виникають хибні "безпечні" моменти у підході A (автомобіль візуально перекриває жабу, але смерті немає). Підхід B вірніший оригінальному Frogger, але потребує перевірок зіткнень з частотою обробника (~18 Гц на смугу). Обговорити компроміс: підхід B коректніший, але додає роботу на критичному шляху синхронізації. Підхід A: Написати окремий код руху для кожної смуги зі швидкістю та напрямком, вбудованими як безпосередні значення.
Підхід B: Визначити таблицю дескрипторів смуг, де кожен запис — структура {direction DB, speed DB, pattern_offset DW, current_pos DB}. Одна процедура move_lane обробляє будь-яку смугу, зчитуючи її дескриптор.
Що вимірювати/спостерігати: Розмір коду (A зростає лінійно з кількістю смуг, B залишається постійним), зусилля для додавання нової смуги (A: написати новий блок коду, B: додати один запис у таблицю). Обговорити, як проєктування на основі таблиць відповідає реальним мікропрограмам та таблицям конфігурації.
<br>Розподіл модулів (див. <a class="internal-link" data-href="How-to/include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">Підключення файлів: INCLUDE</a>):
Студент A — Відображення та ввід: проєктування розташування екрану, draw_lane (рендеринг однієї смуги з патерну + зміщення), draw_frog, draw_status_bar, draw_game_over, опитування клавіатури та логіка руху жаби, обмеження меж. Файли: display.asm, input.asm.
Студент B — Ігровий рушій та синхронізація: налаштування/деінсталяція обробника INT 1Ch, логіка руху смуг (move_lanes), виявлення зіткнень (check_collision), керування життями/рахунком, прогресія рівнів (налаштування швидкості), проєктування таблиці дескрипторів смуг, ГВЧ для розширень. Файли: engine.asm, timer.asm.
Спільне: main.asm з ініціалізацією та ігровим циклом, сегмент даних з дескрипторами смуг, позицією жаби, змінними стану гри. Обидва студенти мають узгодити формат дескриптора смуги та змінні позиції жаби.
Точки інтеграції: Студент B оновлює lane[i].current_pos кожен такт і викликає check_collision(frog_row, frog_col) -&gt; hit?. Студент A зчитує значення current_pos для рендерингу кожної смуги. Змінні позиції жаби (frog_row, frog_col) записуються Студентом A (ввід) і зчитуються Студентом B (зіткнення). <br><a class="internal-link" data-href="How-to/game-logic/Y11.md" href="projects/how-to/game-logic/y11.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: таблиця дескрипторів смуг, прокрутка з обгортанням MOD 80, виявлення зіткнень, прогресія рівнів.
<br><a class="internal-link" data-href="How-to/lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиця дескрипторів смуг:</a> визначте як lane_table із записами: DB direction (0=вліво, 1=вправо), DB speed_divisor, DW pattern_offset, DB current_pos. Разом ~5 байт на смугу. Використовуйте LEA SI, [lane_table + BX] для <a class="internal-link" data-href="How-to/addressing-modes.md" href="projects/how-to/addressing-modes.html" target="_self" rel="noopener nofollow">індексації в таблицю</a>, де BX = lane_index * 5.
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Патерни транспорту:</a> визначте патерн кожної смуги як 80 байт, наприклад, lane1_pat DB 20 DUP(0), 0DBh,0DBh,0DBh, 17 DUP(0), 0DBh,0DBh, 38 DUP(0) — два транспортних засоби з проміжками. Байт атрибуту для кожного символу в смузі визначає колір транспорту.
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прокрутка:</a> для рендерингу смуги i на рядку екрану r зі зміщенням off: для кожного стовпця c (0-79) зчитати pattern[(off + c) MOD 80], записати символ та атрибут до B800:((r * 80 + c) * 2).
<br><a class="internal-link" data-href="How-to/flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Виявлення зіткнень:</a> коли жаба в позиції (frog_row, frog_col), визначити, якій смузі відповідає цей рядок. Зчитати патерн смуги за (current_pos + frog_col) MOD 80. Якщо байт патерну не є "порожнім" символом (наприклад, пробілом), це транспорт — зіткнення.
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Неблокуюча клавіатура:</a> MOV AH, 01h / INT 16h (перевірка буфера) потім MOV AH, 00h / INT 16h (зчитання клавіші). Клавіші-стрілки: AL=00h, AH=скан-код (48h=Up, 50h=Down, 4Bh=Left, 4Dh=Right).
<br><a class="internal-link" data-href="How-to/interrupt-handlers.md" href="projects/how-to/interrupt-handlers.html" target="_self" rel="noopener nofollow">Ланцюжок таймера:</a> у вашому обробнику, після встановлення tick_flag, виконайте JMP FAR [old_1ch_vector] для ланцюжка до оригінального обробника. Це зберігає роботу годинника BIOS. Зберігайте старий вектор як old_1ch DD ? в сегменті даних.
Точки синхронізації:
Кінець тижня 8: узгодити формат дескриптора смуги (5 байт: direction, speed_divisor, pattern_offset DW, current_pos), змінні frog_row/frog_col, угоди про виклик процедур
Кінець тижня 10: перша спільна демонстрація — смуги прокручуються з правильними зміщеннями, жаба малюється на екрані (поки без інтеграції)
Кінець тижня 11: інтеграція працює — жаба рухається серед прокручуваних смуг, зіткнення виявляються
Кінець тижня 13: повна гра з прогресією рівнів, життями та рахунком; узгодити структуру README
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/y11-frogger.html</link><guid isPermaLink="false">Projects/Y11-Frogger.md</guid><pubDate>Tue, 24 Feb 2026 16:02:38 GMT</pubDate><enclosure url="assets/y11-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y11-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y15-Sorting-Visualizer]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🟡 Жовтий
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 400–550 (соло) / 600–800 (пара), 2–5 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять</a> — малювання стовпчиків на обчислених позиціях у B800h, пари символ+атрибут — L08/P08
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Робота з масивами</a> — масиви DB/DW, доступ до елементів за індексом, обмін елементів — L06/P06
<br><a class="internal-link" data-href="How-to/addressing-modes.md" href="projects/how-to/addressing-modes.html" target="_self" rel="noopener nofollow">Режими адресації</a> — [BX+SI] для обходу елементів масиву, LEA для обчислення адрес — L06/P06
<br><a class="internal-link" data-href="How-to/flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Порівняння та умовні переходи</a> — CMP для порівняння елементів, умовні переходи для логіки сортування — L04/P04
<br><a class="internal-link" data-href="How-to/loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Конструкції циклів</a> — вкладені цикли для O(n^2) сортувань, шаблони LOOP/DEC+JNZ — L04/P04
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури та стекові фрейми</a> — окремі процедури для кожного алгоритму сортування, процедура малювання, процедура затримки — L05/P05
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід через INT 16h</a> — покроковий режим (очікування клавіші), перемикання режимів, керування швидкістю — L07/P07
<br><a class="internal-link" data-href="How-to/arithmetic.md" href="projects/how-to/arithmetic.html" target="_self" rel="noopener nofollow">Арифметика</a> — обчислення висоти стовпчика за значенням, позиції на екрані за індексом масиву — L03/P03, L04/P04
<br><a class="internal-link" data-href="How-to/timer-delays.md" href="projects/how-to/timer-delays.html" target="_self" rel="noopener nofollow">Таймер/затримка</a> — використання INT 15h AH=86h або циклу активного очікування для швидкості анімації — L07/P07
<br><a class="internal-link" data-href="How-to/game-logic/Y15.md" href="projects/how-to/game-logic/y15.html" target="_self" rel="noopener nofollow">Ігрова логіка: Візуалізатор сортування</a> — алгоритми сортування як процедури, покрокова візуалізація, рендеринг стовпчиків
Програма відображає масив значень у вигляді кольорових вертикальних стовпчиків на весь екран. Висота кожного стовпчика пропорційна його значенню; стовпчики рендеряться за допомогою блочних символів (таких як DB 0DBh, символ повного блоку), що записуються безпосередньо у відеопам'ять за адресою B800h. Стовпчики заповнюють екран зліва направо, кожен шириною в один-два стовпці, створюючи візуальну гістограму. Невідсортовані стовпчики відображаються одним кольором, два стовпчики, що порівнюються, блимають кольором підсвітки, а стовпчики, що досягли своєї фінальної відсортованої позиції, змінюють колір на "готово".Два алгоритми сортування працюють пліч-о-пліч або по черзі (на вибір користувача). У покроковому режимі кожне натискання пробілу просуває на одне порівняння або обмін — студент може спостерігати, які саме елементи алгоритм перевіряє і чому. В автоматичному режимі сортування відбувається безперервно з налаштовуваною затримкою між кроками. Панель статистики внизу екрану відстежує порівняння та обміни в реальному часі, оновлюючись із кожною операцією.Після завершення сортування програма пропонує перемішати масив і почати знову, за бажанням змінивши алгоритм. Візуальний контраст між сортуванням O(n^2) (як бульбашкове сортування) та більш ефективним (як сортування вставками на майже відсортованих даних) стає разюче очевидним завдяки анімації.<br><img alt="Візуалізація бульбашкового сортування зі стовпчиками, статистикою та підсвіткою порівнюваних елементів" src="assets/y15-screen-visualization.png" target="_self">
Підхід A: Зберігає кожне значення як один байт (0–255), обмежуючи висоту стовпчиків до 25 рядків, але використовуючи вдвічі менше пам'яті та дозволяючи 8-бітні порівняння (CMP AL, [SI]).
Підхід B: Використовує 16-бітні слова, дозволяючи більший діапазон значень, але потребуючи 16-бітних порівнянь та подвоюючи використання пам'яті.
Що вимірювати/спостерігати: Підрахуйте загальну кількість байтів інструкцій для циклу сортування в кожній версії та порівняйте кількість тактів на одне порівняння. Це досліджує компроміс ширини даних на рівні ISA. Підхід A: Перемальовує всю діаграму стовпчиків після кожного порівняння/обміну — простий код, але багато записів у відеопам'ять.
Підхід B: Стирає та перемальовує лише два стовпчики, залучені до поточної операції.
Що вимірювати/спостерігати: Підрахуйте кількість байтів, записаних у B800h на крок у кожному підході. Для масиву з 60 елементів із 25-рядковими стовпчиками повне перемальовування записує ~3000 пар символ+атрибут проти ~100 при цільовому перемальовуванні. Це демонструє мінімізацію вводу/виводу. Підхід A: Використовує рахований цикл (MOV CX, N / delay_loop: LOOP delay_loop) для затримки анімації.
Підхід B: Викликає INT 15h функцію 86h, яка очікує вказану кількість мікросекунд.
Що вимірювати/спостерігати: Використання процесора (активне очікування максимально навантажує процесор), переносимість при різних налаштуваннях cycles у DOSBox та точність хронометражу. Це ілюструє опитування проти очікування на основі переривань.
<br>Розподіл модулів (див. <a class="internal-link" data-href="How-to/include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">Підключення файлів: INCLUDE</a>):
Студент A: Рушій сортування — реалізує алгоритми сортування як процедури, що приймають вказівник на масив та його розмір. Кожен алгоритм викликає спільний callback notify_step (або встановлює спільні прапорці) після кожного порівняння та обміну, щоб відображення могло реагувати. Також реалізує перемішування масиву (Fisher-Yates або подібний).
Студент B: Відображення та UI — рендерить стовпчики у відеопам'ять, обробляє клавіатурний ввід (перемикання режимів, керування швидкістю, вибір алгоритму), малює панель статистики та реалізує механізм затримки.
Точка інтеграції: Спільний сегмент даних з масивом, розміром масиву, лічильниками порівнянь/обмінів та прапорцем "тип кроку" (COMPARE або SWAP з індексами). Студент A записує в них після кожної операції; Студент B зчитує їх для оновлення відображення. <br><a class="internal-link" data-href="How-to/game-logic/Y15.md" href="projects/how-to/game-logic/y15.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: bubble/selection/insertion sort як процедури, покрокова візуалізація, рендеринг стовпчиків.
Блочні символи для стовпчиків: DB 0DBh (повний блок), DB 0B0h (легке затінення), DB 0B1h (середнє затінення). Використовуйте повний блок для активних стовпчиків та символи затінення для ефекту підсвітки "порівнюваних".
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Позиція у відеопам'яті:</a> зміщення = (рядок * 80 + стовпець) * 2. Для малювання стовпчика висотою H, починаючи зі стовпця C від базового рядка R: запишіть символ блоку в рядки R, R-1, R-2, ..., R-H+1 у стовпці C.
<br><a class="internal-link" data-href="How-to/random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">Перемішування Fisher-Yates:</a> Для рандомізації масиву ітеруйте від останнього елемента до першого, обмінюючи кожен із випадковим попереднім елементом. Для простого псевдовипадкового числа зчитайте тік таймера BIOS за адресою 0040:006Ch та використовуйте модульну арифметику.
<br><a class="internal-link" data-href="How-to/timer-delays.md" href="projects/how-to/timer-delays.html" target="_self" rel="noopener nofollow">Затримка INT 15h AH=86h:</a> CX:DX = мікросекунди очікування. Для затримки ~50 мс: CX=0, DX=50000.
Кольорові атрибути: використовуйте 0Ah (яскраво-зелений) для невідсортованих, 0Ch (яскраво-червоний) для поточних порівнюваних, 0Bh (яскраво-бірюзовий) для відсортованих. Біти фону 4-6 можуть додати кольорову фонову підсвітку.
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурне меню:</a> Для створення меню вибору алгоритму очистіть екран, заповнивши B800h пробілами, потім запишіть текст меню на конкретних позиціях. Очікуйте INT 16h для зчитування вибору користувача.
Точки синхронізації:
Кінець тижня 9: узгодити спільний .DATA (array DB 40 DUP(?), array_size DW 40, cmp_count DW 0, swap_count DW 0, step_type DB ?, step_idx1 DW ?, step_idx2 DW ?) та прототипи процедур
Кінець тижня 11: перша інтеграція — покрокове сортування з візуалізацією працює наскрізно, обидва алгоритми анімуються
Кінець тижня 13: програма повністю працює (покроковий + автоматичний режими, 2 алгоритми, скидання), обидва розділи README готові
Кінець тижня 14: фінальне тестування на різних початкових даних, обидва Loom-відео записані
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/y15-sorting-visualizer.html</link><guid isPermaLink="false">Projects/Y15-Sorting-Visualizer.md</guid><pubDate>Tue, 24 Feb 2026 16:02:35 GMT</pubDate><enclosure url="assets/y15-screen-visualization.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y15-screen-visualization.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[G04-Quiz-Trivia-Game]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🟢 Зелений
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 400-600 (соло) / 650-900 (пара), 2-5 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять</a> (багатоелементне розташування екрана: заголовок, індикатор прогресу, запитання, варіанти відповідей, зворотний зв'язок — усе за адресою B800h) — L08/P08
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Структуровані дані в сегменті даних</a> (запитання, збережені як записи фіксованого розміру: текст запитання + 4 тексти варіантів + байт правильної відповіді) — L06/P06
<br><a class="internal-link" data-href="How-to/addressing-modes.md" href="projects/how-to/addressing-modes.html" target="_self" rel="noopener nofollow">Режими адресації</a> (база + зміщення для доступу до полів усередині запису запитання; індексний регістр для ітерації по запитаннях) — L06/P06
<br><a class="internal-link" data-href="How-to/segment-registers.md" href="projects/how-to/segment-registers.html" target="_self" rel="noopener nofollow">Сегментні регістри</a> (ES = B800h для відеопам'яті, DS для даних запитань) — L03/P03
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури з параметрами на стеку</a> (draw_question, check_answer, show_results, draw_progress_bar) — L05/P05
<br><a class="internal-link" data-href="How-to/arithmetic.md" href="projects/how-to/arithmetic.html" target="_self" rel="noopener nofollow">Арифметика: MUL та DIV</a> (обчислення відсотків, підрахунок рахунку) — L04/P04
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Відображення рядків</a> (копіювання рядків DB у відеопам'ять з чергуванням байтів атрибутів) — L06/P06, L08/P08
<br><a class="internal-link" data-href="How-to/flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Умовна логіка</a> (CMP натиснутої клавіші з правильною відповіддю, розгалуження для зворотного зв'язку) — L04/P04
<br><a class="internal-link" data-href="How-to/timer-delays.md" href="projects/how-to/timer-delays.html" target="_self" rel="noopener nofollow">Системний годинник</a> (INT 1Ah для затримки та загального часу) — L07/P07
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід</a> (INT 16h для меню та вибору відповіді) — L07/P07
<br><a class="internal-link" data-href="How-to/game-logic/G04.md" href="projects/how-to/game-logic/g04.html" target="_self" rel="noopener nofollow">Ігрова логіка: вікторина</a> — валідація відповідей, підрахунок балів, індикатор прогресу, пороги результатів
Програма є інтерактивною грою-вікториною з варіантами відповідей, що працює в текстовому режимі 80x25. При запуску титульний екран малюється через прямий запис у B800h, відображаючи назву гри великими блочними літерами (з використанням символів рамки або блочних символів) з кольоровою фоновою смугою. Під заголовком меню категорій показує 3 категорії (наприклад, "1. Computer Science", "2. Geography", "3. Mix") з окремими кольоровими мітками. Гравець натискає 1, 2 або 3 для початку.Після вибору категорії гра послідовно представляє 10 запитань. Кожний екран запитання має чітку структуру: індикатор прогресу в рядку 0 (наприклад, "Question 4/10" плюс візуальна смуга з 10 блочних символів, заповнені зеленим і порожні темно-сірим), поточний рахунок у рядку 0 з правого краю ("Score: 30/40"), текст запитання на рядках 5-7 яскраво-білим та чотири варіанти відповідей (A-D) на рядках 10-13, кожний на окремому рядку з кольоровим літерним префіксом (яскраво-блакитний «A.» / «B.» / «C.» / «D.»), за яким йде текст відповіді білим. Увесь рендеринг використовує прямий запис у відеопам'ять.Гравець натискає A, B, C або D для відповіді. При правильній відповіді: обраний варіант спалахує зеленим (атрибут змінюється на яскраво-зелений фон), індикатор «+10» з'являється біля рахунку, і після 1-секундної паузи завантажується наступне запитання. При неправильній відповіді: обраний варіант стає червоним, правильна відповідь одночасно підсвічується зеленим, і після 1,5-секундної паузи завантажується наступне запитання. Бали за неправильні відповіді не нараховуються.Після 10 запитань екран результатів показує: фінальний рахунок як "NN / 100", відсоток, кількість правильних, кількість неправильних, повідомлення про результат ("Excellent!" для 80%+, "Good job!" для 60-79%, "Keep learning!" для менше 60%) та загальний час проходження вікторини. Гравець може натиснути R для повтору тієї самої категорії (запитання перемішані), M для повернення до меню або Q для виходу в DOS.<br><img alt="Головне меню з вибором категорії" src="assets/g04-screen-menu.png" target="_self"><br><img alt="Екран запитання з варіантами відповідей та індикатором прогресу" src="assets/g04-screen-question.png" target="_self"><br><img alt="Екран результатів вікторини з підсумковою статистикою" src="assets/g04-screen-results.png" target="_self">
Підхід A: Зберігати кожне запитання як запис фіксованого розміру, скажімо, 256 байтів: байти 0-79 = текст запитання (доповнений пробілами), байти 80-119 = варіант A (40 символів), байти 120-159 = варіант B, байти 160-199 = варіант C, байти 200-239 = варіант D, байт 240 = індекс правильної відповіді (0-3), байти 241-255 = доповнення. Запитання N починається зі зміщення N * 256.
Підхід B: Зберігати рядки змінної довжини, завершені '$' або 0, та створити таблицю вказівників типу word: q_ptrs DW OFFSET q1_text, OFFSET q1_optA, OFFSET q1_optB, ....
Що виміряти/спостерігати: Записи фіксованого розміру витрачають пам'ять на доповнення, але дозволяють доступ O(1) з простим MUL; таблиці вказівників економлять пам'ять, але вимагають двох рівнів непрямості (завантажити вказівник, потім завантажити рядок). Виміряйте загальну кількість байтів сегмента даних для 10 запитань кожним підходом. Обговоріть, який легше розширити новими запитаннями, та який нагадує роботу структур C проти зв'язаних записів на машинному рівні. Підхід A: Очищення екрана між запитаннями через BIOS MOV AX, 0600h / MOV BH, 07h / XOR CX, CX / MOV DX, 184Fh / INT 10h.
Підхід B: Заповнення всіх 4000 байтів відеопам'яті пробілом (20h) та атрибутом (07h) за допомогою REP STOSW (CX=2000, AX=0720h, ES:DI = B800:0000).
Що виміряти/спостерігати: Виміряйте час (у тактах) для очищення екрана 200 разів кожним методом. REP STOSW — це одна інструкція, яка переміщує дані на швидкості шини, тоді як INT 10h включає накладні витрати переривання та цикли прошивки BIOS. Обговоріть, коли ви б віддали перевагу виклику BIOS (наприклад, зміна відеорежимів) проти прямому підходу. Підхід A: Валідація відповіді гравця ланцюжком: CMP AL, 'A' / JE option_a / CMP AL, 'a' / JE option_a / CMP AL, 'B' / ... (8 пар порівняння-переходу для A-D без урахування регістру).
Підхід B: Спочатку нормалізувати ввід (AND AL, 0DFh для примусового переведення у верхній регістр), відняти 'A' для отримання індексу 0-3, перевірити межі за допомогою CMP AL, 3 / JA invalid, потім порівняти індекс з байтом правильної відповіді.
Що виміряти/спостерігати: Підхід A потребує 8 пар CMP+JE (16 інструкцій); підхід B потребує 4 інструкції незалежно від кількості варіантів. Обговоріть, як підхід пошуку/нормалізації масштабується при додаванні варіантів E та F, та що трюк AND AL, 0DFh розкриває про кодування ASCII (біт 5 — це біт регістру).
<br>Розподіл модулів (див. <a class="internal-link" data-href="How-to/include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">Підключення файлів: INCLUDE</a>):
Студент A — Модуль відображення: рендеринг титульного екрана, розміщення екрана запитання (індикатор прогресу, текст запитання, варіанти, рахунок), анімація зворотного зв'язку на відповідь (зміна кольорів), екран результатів, очищення екрана між запитаннями. Файл: display.asm.
Студент B — Модуль даних та логіки: визначення даних запитань (усі 30+ запитань у записах фіксованого розміру або таблицях вказівників), логіка вибору категорії, перевірка відповідей, обчислення рахунку, обчислення відсотків, перемішування запитань, таймінг. Файл: quiz.asm.
Спільний головний модуль: ініціалізація програми, цикл головного меню, цикл вікторини (ітерація по 10 запитаннях з викликом процедур відображення та логіки), перенаправлення після результатів. Файл: main.asm.
Точки інтеграції:
Узгодьте формат запису запитання та його розмір на KT-2. Обидва партнери повинні знати, як обчислити зміщення запитання N та отримати доступ до його полів.
Визначте сигнатуру draw_question: SI = вказівник на запис запитання, викликається циклом вікторини. Студент A рендерить; Студент B готує вказівник.
Визначте process_answer: AL = натиснута клавіша гравця, SI = вказівник на поточний запис запитання. Повертає: AX = 1, якщо правильно, 0, якщо неправильно. Студент B реалізує; головний цикл (спільний) використовує результат для виклику процедури зворотного зв'язку Студента A.
Обидва партнери повинні використовувати спільну утиліту draw_string_at: DS:SI = вказівник на рядок, BH = рядок, BL = стовпець, AH = атрибут, записує у відеопам'ять. Реалізуйте це першим та використовуйте спільно. <br>
<a class="internal-link" data-href="How-to/game-logic/G04.md" href="projects/how-to/game-logic/g04.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: валідація відповідей, підрахунок балів, індикатор прогресу, пороги результатів. Розміщення запису запитання (приклад фіксованого розміру):
Q_SIZE EQU 256
Q_TEXT EQU 0 ; 80 байтів, доповнені пробілами
Q_OPT_A EQU 80 ; 40 байтів
Q_OPT_B EQU 120 ; 40 байтів
Q_OPT_C EQU 160 ; 40 байтів
Q_OPT_D EQU 200 ; 40 байтів
Q_ANSWER EQU 240 ; 1 байт: 0=A, 1=B, 2=C, 3=D Доступ до варіанту B запитання 3: MOV SI, 3 / MOV AX, Q_SIZE / MUL SI / ADD AX, OFFSET questions / ADD AX, Q_OPT_B / MOV SI, AX. <br>
<a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Малювання індикатора прогресу:</a> В рядку 0, стовпці 20-29, запишіть 10 клітинок. Для запитання N (з 1), клітинки 1 до N використовують символ DBh з атрибутом 0Ah (зелений на чорному); клітинки N+1 до 10 використовують DBh з атрибутом 08h (темно-сірий). Обчисліть зміщення: (20 + i) * 2 для стовпця i. Запишіть символ за ES:[зміщення], атрибут за ES:[зміщення+1]. Швидкий довідник кольорових атрибутів: 0Ah = яскраво-зелений на чорному (правильно), 0Ch = яскраво-червоний на чорному (неправильно), 0Fh = яскраво-білий на чорному (текст запитання), 0Bh = яскраво-блакитний на чорному (літери варіантів), 1Fh = яскраво-білий на синьому (рядок стану), 2Fh = яскраво-білий на зеленому (фон правильного варіанту), 4Fh = яскраво-білий на червоному (фон неправильного варіанту). <br>
<a class="internal-link" data-href="How-to/bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Ввід без урахування регістру:</a> AND AL, 0DFh працює, тому що в ASCII 'A'=41h і 'a'=61h відрізняються лише бітом 5 (20h). Очищення біту 5 перетворює обидва на 41h. Після цього SUB AL, 'A' дає 0 для A, 1 для B, 2 для C, 3 для D. Перевірте CMP AL, 3 / JA invalid для значень поза діапазоном. <br>
<a class="internal-link" data-href="How-to/timer-delays.md" href="projects/how-to/timer-delays.html" target="_self" rel="noopener nofollow">Затримка між запитаннями:</a> Зчитайте лічильник тактів через MOV AH, 00h / INT 1Ah (повертає CX:DX). Збережіть DX як start_tick. Цикл: зчитайте такти знову, SUB DX, start_tick, CMP DX, 18 (приблизно 1 секунда), JB loop. Це простіше, ніж підключення INT 1Ch, і достатньо для покрокових пауз. <br>
<a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Копіювання рядка у відеопам'ять:</a> Напишіть утилітну процедуру, яка копіює рядок, завершений ''/JE done/STOSW` (записує AL=символ, потім AH=атрибут як слово, просуваючи DI на 2). Це можна повторно використовувати для всього відображення тексту в проєкті. Точки синхронізації:
Кінець тижня 9: формат запису запитання (Q_SIZE, зміщення полів), сигнатура draw_string_at (SI=рядок, BH=рядок, BL=стовпець, AH=атрибут), сигнатура process_answer (AL=клавіша, SI=запис → AX=результат)
Кінець тижня 11: перша інтеграція — main.asm запускає повний цикл вікторини (меню → 10 запитань → рахунок), ввід A/B/C/D працює, зворотний зв'язок відображається
Кінець тижня 13: код повністю працює (3 категорії, індикатор прогресу, зворотний зв'язок, екран результатів, R/M/Q), чернетка звіту
Кінець тижня 14: звіт фіналізовано, відео записані, перевірені всі 30+ запитань та граничні випадки
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/g04-quiz-trivia-game.html</link><guid isPermaLink="false">Projects/G04-Quiz-Trivia-Game.md</guid><pubDate>Tue, 24 Feb 2026 16:02:35 GMT</pubDate><enclosure url="assets/g04-screen-menu.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/g04-screen-menu.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y10-Space-Invaders]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🟡 Жовтий
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 450-600 (соло) / 650-900 (пара), 2-5 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/interrupt-handlers.md" href="projects/how-to/interrupt-handlers.html" target="_self" rel="noopener nofollow">Встановлення обробника переривання таймера</a> (INT 1Ch) — інсталяція власного обробника переривань, що спрацьовує ~18.2 рази/секунду для керування ігровим циклом — L07/P07
<br><a class="internal-link" data-href="How-to/interrupt-handlers.md" href="projects/how-to/interrupt-handlers.html" target="_self" rel="noopener nofollow">Маніпуляція таблицею векторів переривань</a> — збереження оригінального вектора INT 1Ch та його відновлення при виході — L07/P07
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Пряме записування у відеопам'ять</a> (B800h) — рендеринг прибульців, корабля, куль та рахунку без накладних витрат BIOS — L08/P08
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід</a> (INT 16h) — неблокуюча перевірка клавіш (AH=01h) для швидкого реагування керування всередині ігрового циклу — L07/P07
<br><a class="internal-link" data-href="How-to/segment-registers.md" href="projects/how-to/segment-registers.html" target="_self" rel="noopener nofollow">Керування сегментними регістрами</a> — ES для відеосегменту B800h, DS для даних, збереження/відновлення в обробнику переривань — L06/P08
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Двовимірний масив</a> з бітовими прапорцями — формація прибульців, збережена як масив байтів (живий/мертвий), індексована за рядком та стовпцем — L06/P06
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури та дисципліна стеку</a> — обробник переривань повинен зберегти (PUSH) всі використовувані регістри та виконати IRET; ігрові процедури використовують стандартний CALL/RET — L05/P05
<br><a class="internal-link" data-href="How-to/arithmetic.md" href="projects/how-to/arithmetic.html" target="_self" rel="noopener nofollow">Арифметика виявлення зіткнень</a> — порівняння позиції кулі з позиціями сітки прибульців, обчисленими від початку формації + зміщення — L04/P04
<br><a class="internal-link" data-href="How-to/flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Умовні конструкції</a> та <a class="internal-link" data-href="How-to/loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">цикли</a> — логіка напрямку руху прибульців, виявлення меж, опускання формації — L04/P04
<br><a class="internal-link" data-href="How-to/game-logic/Y10.md" href="projects/how-to/game-logic/y10.html" target="_self" rel="noopener nofollow">Ігрова логіка: Space Invaders</a> — рух формації, стрільба прибульців, сіткове виявлення зіткнень, прискорення
<br>Див. також <a class="internal-link" data-href="Y11-Frogger.md" href="projects/y11-frogger.html" target="_self" rel="noopener nofollow">Y11 Frogger</a> — також використовує рух, керований таймером, та виявлення зіткнень на основі смуг. Space Invaders зосереджується на механіці стрільби та ШІ формації прибульців.Програма запускається у DOSBox у повноекранному текстовому режимі 80x25, показуючи спрощену гру Space Invaders. У верхній частині екрану рядки символів прибульців (наприклад, \xE8/\xEA або подібні гліфи кодової сторінки 437) марширують у формації — блок, скажімо, 5 рядків на 8 стовпців. Вони рухаються на одну клітинку вправо кожен такт; коли крайній правий прибулець досягає краю екрану, вся формація опускається на один рядок і змінює напрямок. Прибульці рендеряться яскравими кольорами (зелений, блакитний) на чорному тлі, і їх повільне, але невблаганне наближення створює напругу.У нижній частині екрану корабель гравця (символ \x1E стрілка вгору або власний малюнок) стоїть на базовій лінії, рухаючись вліво та вправо клавішами-стрілками. Натискання Space стріляє кулею вгору — один символ |, що рухається на один рядок за такт. Коли куля потрапляє в прибульця, прибулець зникає, рахунок збільшується, а короткий спалах позначає уражену клітинку. Якщо гравець вже має кулю в польоті, Space нічого не робить (одна куля за раз). Прибульці періодично стріляють вниз — випадковий прибулець у нижньому рядку формації запускає символ ! у напрямку гравця. Якщо він досягає рядка гравця в стовпці гравця, одне життя втрачається.Рядок стану вгорі або внизу показує рахунок, залишок життів (3) та поточний рівень. Коли всі прибульці знищені, з'являється нова хвиля — швидша. Коли всі життя втрачені, з'являється GAME OVER. Ігровий цикл керується встановленням обробника INT 1Ch (такт таймера), що забезпечує стабільний ритм ~18.2 Гц для руху та анімації.<br><img alt="Ігровий процес" src="assets/y10-screen-gameplay.png" target="_self"><br><img alt="Кінець гри" src="assets/y10-screen-gameover.png" target="_self">
Підхід A: Встановити обробник INT 1Ch так, щоб обробник переривань встановлював tick_flag. Головний цикл опитує прапорець і, коли він встановлений, обробляє один ігровий кадр (рух прибульців, рух куль, перевірка зіткнень), потім скидає його.
Підхід B: Головний цикл опитує клавіатуру, потім виконує затримку на основі LOOP для наближення синхронізації, не торкаючись переривань.
Що вимірювати/спостерігати: Стабільність швидкості гри при різних налаштуваннях циклів CPU DOSBox. Підхід B прискорюватиметься або сповільнюватиметься; підхід A залишається прив'язаним до 18.2 Гц. Демонструє синхронізацію, керовану апаратним забезпеченням (A) проти програмно оціненою синхронізацією (B). Підхід A: Байтовий масив ROWS x COLS, де 1 = живий, 0 = мертвий. Рух ітерує по всьому масиву, пропускаючи мертвих прибульців.
Підхід B: Зв'язаний список (масив структур з вказівниками на наступний), що містить лише живих прибульців. Рух ітерує лише по живих, але видалення потребує оновлення вказівників.
Що вимірювати/спостерігати: Інструкції на кадр для руху/рендерингу при знищенні прибульців. Масив простий та дружній до кешу (хоча 8086 не має кешу, концепція застосовна). Зв'язаний список складніший, але уникає сканування мертвих клітинок. Порівняйте складність коду. Підхід A: Для кулі в позиції (row, col) обчислити, якій клітинці сітки прибульців вона відповідає, потім перевірити, чи цей прибулець живий. O(1) на кулю.
Підхід B: Ітерувати по всіх живих прибульцях і порівнювати позицію на екрані кожного прибульця з позицією кулі. O(n) на кулю.
Що вимірювати/спостерігати: Кількість інструкцій для виявлення зіткнень, коли живих 40 прибульців проти коли залишилось 5. Підхід A має постійний час; підхід B масштабується з кількістю прибульців, але простіший для міркувань, коли прибульці на непозиціях сітки (сценарій розширення).
<br>Розподіл модулів (див. <a class="internal-link" data-href="How-to/include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">Підключення файлів: INCLUDE</a>):
Студент A — Рендеринг та ввід: процедури малювання у відеопам'яті (draw_aliens, draw_ship, draw_bullet, draw_score, draw_game_over), обробка клавіатурного вводу, розташування екрану. Файли: display.asm, input.asm.
Студент B — Ігрова логіка та синхронізація: налаштування/деінсталяція обробника INT 1Ch, алгоритм руху прибульців, рух куль, виявлення зіткнень, керування рахунком/життями, прогресія рівнів, ГВЧ для стрільби прибульців. Файли: game.asm, timer.asm.
Спільне: main.asm з ініціалізацією, ігровим циклом та сегментом даних. Узгодьте спільні дані: масив alien_grid, player_x, bullet_x/y, alien_bullet_x/y, score, lives, tick_flag.
Точки інтеграції: Студент B встановлює tick_flag та оновлює масиви стану гри. Студент A зчитує ці масиви кожен кадр та рендерить. Обидва мають узгодити точне розташування пам'яті для alien_grid та змінних куль. <br><a class="internal-link" data-href="How-to/game-logic/Y10.md" href="projects/how-to/game-logic/y10.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: рух формації прибульців, стрільба прибульців, виявлення зіткнень через сітку, прискорення за кількістю живих.
<br><a class="internal-link" data-href="How-to/interrupt-handlers.md" href="projects/how-to/interrupt-handlers.html" target="_self" rel="noopener nofollow">Налаштування обробника INT 1Ch:</a> зчитайте поточний вектор з 0000:0070 (4 байти: зміщення, сегмент), збережіть його. Запишіть адресу свого обробника до 0000:0070. Використовуйте CLI перед модифікацією таблиці векторів переривань та STI після. У своєму обробнику, після вашого коду, JMP FAR [old_vector] для ланцюжка до оригінального обробника (зберігає роботу системного годинника).
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Неблокуюча перевірка клавіатури:</a> MOV AH, 01h / INT 16h встановлює ZF, якщо клавіша не очікує. Якщо ZF скинутий, викличте MOV AH, 00h / INT 16h для зчитування клавіші. Це уникає блокування ігрового циклу.
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Розташування сітки прибульців:</a> alien_grid DB 40 DUP(1) — 5 рядків x 8 стовпців, усі живі спочатку. Індекс: row * 8 + col. Позиція на екрані прибульця (r, c): screen_row = formation_top + r * 2, screen_col = formation_left + c * 4 (відступи для зручності читання).
Рух формації: підтримуйте formation_left (зміщення стовпця) та direction (+1 або -1). Кожен такт: formation_left += direction. Якщо крайній правий живий прибулець виходить за екран, встановіть direction = -1 та formation_top += 1. Аналогічно для лівого.
<br><a class="internal-link" data-href="How-to/random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">ГВЧ для стрільби прибульців:</a> зчитайте 0040:006Ch (такт таймера), використовуйте молодший байт XOR з поточним початковим значенням. Оберіть стовпець для стрільби: seed MOD num_columns, потім знайдіть найнижчого живого прибульця в цьому стовпці.
<br><a class="internal-link" data-href="How-to/number-display.md" href="projects/how-to/number-display.html" target="_self" rel="noopener nofollow">Відображення рахунку:</a> перетворіть 16-бітний рахунок у десятковий ASCII повторним діленням: DIV 10, залишок + 30h дає одну цифру. Зберігайте цифри у зворотному порядку, потім запишіть у відеопам'ять.
Точки синхронізації:
Кінець тижня 8: узгодити точне розташування спільних змінних у .DATA (alien_grid, player_x, bullet_x/y, alien_bullet_x/y, tick_flag), угоди про виклик та формат масиву alien_grid
Кінець тижня 10: Студент B має працюючий обробник INT 1Ch, що встановлює tick_flag; Студент A використовує цей прапорець у головному циклі для синхронізації рендерингу
Кінець тижня 11: перша наскрізна інтеграція — прибульці рухаються, корабель керується, куля летить та знищує прибульців, рахунок оновлюється
Кінець тижня 13: код зафіксований; відновлення INT 1Ch перевірено; чернетка звіту з аналізом «Підхід A проти B» готова для взаємного рев'ю
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/y10-space-invaders.html</link><guid isPermaLink="false">Projects/Y10-Space-Invaders.md</guid><pubDate>Tue, 24 Feb 2026 16:02:34 GMT</pubDate><enclosure url="assets/y10-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y10-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y20-Password-Manager]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🟡 Жовтий
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 450-600 (соло) / 650-850 (пара), 2-5 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Побітові XOR-операції</a> (побайтовий цикл XOR-шифрування/дешифрування, циклічний ключ) — L04/P04
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять</a> (ES=B800h, пари символ+атрибут, маскований показ пароля) — L08/P08
<br><a class="internal-link" data-href="How-to/file-io.md" href="projects/how-to/file-io.html" target="_self" rel="noopener nofollow">Файловий ввід/вивід DOS через INT 21h</a> (створення, відкриття, читання, запис, закриття для постійного зашифрованого сховища) — L09/P09
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви записів фіксованого розміру</a> (обчислення зміщень у 80-байтових записах, доступ до полів через зміщення) — L06/P06
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури зі стековими фреймами</a> (шифрування/дешифрування, додавання/редагування/видалення, процедури відображення як модульні процедури) — L05/P05
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід через INT 16h</a> (введення пароля без відлуння, навігація клавішами-стрілками, командні клавіші) — L07/P07
<br><a class="internal-link" data-href="How-to/flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Умовна логіка та прапорці</a> (перевірка цілісності після дешифрування, валідація введення, диспетчеризація меню) — L04/P04
<br><a class="internal-link" data-href="How-to/segment-registers.md" href="projects/how-to/segment-registers.html" target="_self" rel="noopener nofollow">Сегментні регістри</a> (DS для даних, ES для відеопам'яті або блочних операцій) — L03/P03
<br><a class="internal-link" data-href="How-to/loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Конструкції циклів</a> (LOOP або DEC+JNZ для циклічного XOR, сканування записів, відображення) — L04/P04
<br><a class="internal-link" data-href="How-to/game-logic/Y20.md" href="projects/how-to/game-logic/y20.html" target="_self" rel="noopener nofollow">Ігрова логіка: менеджер паролів</a> — XOR-шифрування з циклічним ключем, автентифікація через магічний заголовок, пошук за підрядком
<br>Див. також <a class="internal-link" data-href="Y19-Address-Book.md" href="projects/y19-address-book.html" target="_self" rel="noopener nofollow">Y19 Адресна книга</a> — подібний патерн CRUD + збереження у файл, але без шифрування. Унікальна складність менеджера паролів — побайтовий XOR-шифр та безпечне введення.Програма реалізує зашифрований менеджер паролів, що працює у текстовому режимі DOSBox. Під час запуску на екрані з'являється центрована підказка для автентифікації на темному фоні: "Enter master password:", а за нею — блимаючий курсор. Коли користувач друкує, замість справжніх символів з'являються зірочки — пароль ніколи не показується відкритим текстом. Введені байти зберігаються у буфері та використовуються як ключ XOR-шифрування протягом усієї сесії. При першому запуску «правильного» майстер-пароля не існує — програма створює новий зашифрований файл даних. При наступних запусках програма намагається дешифрувати наявний файл введеним паролем. Якщо дешифровані дані проходять просту перевірку цілісності (відомий заголовок з магічним числом), доступ надається; інакше на екрані з'являється червоне повідомлення "Invalid password" і програма завершується.Після автентифікації екран перетворюється на структурований інтерфейс, повністю відображений через прямий запис у відеопам'ять за адресою B800h. Кольорова панель заголовка показує "Password Manager" та кількість записів. Основна область показує прокручуваний список записів, де кожен відображає назву сайту та ім'я користувача звичайним текстом, а стовпець пароля замінений рядком зірочок. Вибір запису клавішами-стрілками та натискання Enter розкриває справжній дешифрований пароль для цього запису, підсвічений яскраво-зеленим на темному фоні — натискання будь-якої клавіші знову приховує його. Рядок стану внизу показує команди: Add, Edit, Delete, Search, Reveal, Quit (save &amp; exit).Кожен запис складається з трьох полів фіксованого розміру: назва сайту (32 байти), ім'я користувача (24 байти) та пароль (24 байти) — загалом 80 байтів на запис. Весь масив записів XOR-шифрується побайтово майстер-паролем (циклічно проходячи символи пароля) перед записом на диск і дешифрується при завантаженні. Це не є криптографічно надійним, але інтенсивно тренує побітові XOR-операції та демонструє симетричну властивість XOR-шифрування (шифрування та дешифрування — одна й та сама операція).<br><img alt="Список записів із замаскованими паролями" src="assets/y20-screen-main.png" target="_self"><br><img alt="Екран введення майстер-пароля" src="assets/y20-screen-login.png" target="_self"><br><img alt="Розкриття пароля для вибраного запису" src="assets/y20-screen-reveal.png" target="_self">
Примітка: Файловий ввід/вивід (файлові сервіси INT 21h) розглядається у L09/P09 (тиждень 9). Протягом тижнів 7-8 зосередьтеся на основній логіці з жорстко заданими тестовими даними. Реалізуйте файлові операції, коли матеріал P09 стане доступним. Підхід A: Циклічно проходить майстер-пароль по одному байту: data[i] XOR key[i MOD key_len], що вимагає операції ділення з остачею (DIV) або лічильника зі скиданням на кожній ітерації.
Підхід B: Спочатку розширює ключ у фіксований 80-байтовий блок (повторюючи пароль для заповнення), потім XOR-ує кожен запис з цим попередньо побудованим блоком — без побайтового ділення з остачею.
Що виміряти/спостерігати: Кількість інструкцій на зашифрований байт (підрахувати в Turbo Debugger), загальний час шифрування 30 записів (виміряти через INT 1Ah) та витрати пам'яті (підхід B використовує 80 додаткових байтів). Обговорити: який підхід легше підтримувати, якщо розмір запису зміниться. Підхід A: Зберігає паролі відкритим текстом у масиві записів і контролює видимість виключно через атрибути відеопам'яті — «приховані» паролі записуються на екран з кольором переднього плану = кольору фону (напр., чорний на чорному), а «розкриття» змінює байт атрибуту на зелений на чорному без перезапису символьних даних.
Підхід B: Ніколи не записує справжні символи пароля у відеопам'ять у прихованому режимі; натомість записує зірочки, а при розкритті перезаписує їх справжніми символами з масиву даних.
Що виміряти/спостерігати: Складність коду (підхід A змінює лише 1 байт на символьну комірку; підхід B повинен перемальовувати з масиву даних), наслідки для безпеки (підхід A має відкритий текст у відеопам'яті навіть коли «приховано» — це видно у перегляді пам'яті Turbo Debugger) та візуальна коректність. Підхід A: Додає 4-байтовий магічний рядок ("PWM\x1A") на початок файлу перед шифруванням; після дешифрування перевіряє, чи ці 4 байти коректні.
Підхід B: Обчислює просту контрольну суму (сума всіх байтів даних, збережена як слово в кінці файлу) перед шифруванням і перевіряє контрольну суму після дешифрування.
Що виміряти/спостерігати: Частота хибних спрацьовувань (наскільки ймовірно, що неправильний пароль дасть правильні магічні байти проти правильну контрольну суму), складність коду та який підхід виявляє часткове пошкодження файлу.
<br>Розподіл модулів (див. <a class="internal-link" data-href="How-to/include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">Підключення файлів: INCLUDE</a>):
Студент A — Модуль крипто та даних: введення майстер-пароля (приховане відлуння), процедури XOR-шифрування/дешифрування, збереження/завантаження файлу із заголовком цілісності, керування масивом записів (додавання, редагування, видалення, пошук). Файли: crypto.asm та data.asm.
Студент B — Модуль відображення та UI: екран автентифікації, відображення основного перегляду списку, відображення деталей/розкриття, накладні форми для введення, навігація клавіатурою та диспетчеризація команд, рядок стану. Файл: display.asm.
Спільний головний модуль: ініціалізація, потік автентифікації, головний цикл подій, послідовність виходу. Файл: main.asm.
Точки інтеграції:
Узгодити формат запису (80 байтів, зміщення полів 0/32/56) та спільні змінні: масив entries, count, selected, буфер master_pw та pw_len.
Буфер майстер-пароля заповнюється підпрограмою введення студента B, але використовується процедурами шифрування/дешифрування студента A. Визначити розташування буфера та змінну довжини як спільні.
Студент A надає encrypt_data та decrypt_data, що працюють з усім масивом записів на місці; студент B викликає draw_list та draw_reveal після завершення операцій з даними. <br><a class="internal-link" data-href="How-to/game-logic/Y20.md" href="projects/how-to/game-logic/y20.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: XOR-шифрування з циклічним ключем, автентифікація за магічним заголовком, пошук за підрядком.
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Приховане введення пароля:</a> Використовувати INT 16h AH=00h (очікує натискання клавіші, без відлуння). Для кожного натискання зберігати символ у буфері пароля і записувати зірочку ('*') на екран через B800h. Обробляти Backspace (ASCII 08h) зменшенням індексу буфера і заміною зірочки пробілом.
<br><a class="internal-link" data-href="How-to/bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Цикл XOR-шифрування:</a> Завантажити кожен байт з масиву даних, XOR-увати з поточним байтом ключа, зберегти назад. Використовувати SI для вказівника на дані та окрему індексну змінну для позиції ключа. Ключ повторюється циклічно: коли індекс ключа досягає довжини пароля, скинути на 0. Процедура дешифрування ідентична — XOR є власною оберненою операцією.
Магічний заголовок: Додати 4 байти ("PWM" + 1Ah) на початок даних перед шифруванням. Після дешифрування перевірити ці 4 байти. Якщо вони не збігаються, пароль неправильний. Використовувати CMPSB або ручні побайтові порівняння.
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Відображення зірочками:</a> При відображенні списку для стовпця поля пароля записувати '*', повторений на довжину поля (або фіксовану кількість, наприклад 8 зірочок), замість справжніх байтів пароля. Лише при явному розкритті перезаписати ці позиції екрану справжніми символами з масиву даних.
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Структура запису:</a> site DB 32 DUP(0), user DB 24 DUP(0), pass DB 24 DUP(0) = 80 байтів. Зберігати 30 записів послідовно. Доступ до запису N за зміщенням entries + N * 80. Усередині запису: назва сайту за +0, ім'я користувача за +32, пароль за +56.
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Розкладка екрану:</a> Рядок 0: панель заголовка (яскраво-білий на синьому). Рядки 1-22: список записів (3 стовпці — сайт, ім'я користувача, маскований пароль). Рядок 23: область розкриття (показує справжній пароль на запит). Рядок 24: рядок стану з клавішами команд.
Константа цілісності 1Ah: Це маркер кінця файлу DOS (Ctrl+Z). Розміщуючи його у заголовку, ви також запобігаєте випадковому відображенню файлу як тексту командою TYPE — приємний побічний ефект, який варто згадати у звіті.
Точки синхронізації:
Кінець тижня 9: узгодити формат запису (80 байтів, зміщення 0/32/56), спільні змінні (entries, count, selected, master_pw, pw_len) та прототипи процедур
Кінець тижня 11: перша інтеграція — main.asm викликає модулі обох студентів; буфер майстер-пароля заповнюється кодом студента B, використовується процедурами шифрування студента A
Кінець тижня 12: повний цикл працює наскрізно (автентифікація → перегляд → CRUD → збереження у файл → завантаження)
Кінець тижня 14: код зафіксовано, звіт та відео готові
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/y20-password-manager.html</link><guid isPermaLink="false">Projects/Y20-Password-Manager.md</guid><pubDate>Tue, 24 Feb 2026 16:02:31 GMT</pubDate><enclosure url="assets/y20-screen-main.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y20-screen-main.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y06-Sokoban]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🟡 Жовтий
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 450-600 рядків коду (соло) / 650-900 рядків коду (пара), 2-6 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
Стек як структура даних (не лише для CALL/RET) — стек історії ходів для скасування, реалізований як масив з вказівником (L05/P05)
Дизайн рівнів на основі даних — кілька рівнів, збережених як <a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">масиви</a> байтів у сегменті даних; завантажувач рівнів копіює дані в робочу сітку (L06/P06)
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Маніпулювання двовимірними масивами</a> — обхід сітки, виявлення зіткнень, штовхання ящиків (L06/P06)
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять</a> (B800h) — рендеринг стін, підлоги, ящиків, цілей та гравця різними символами та кольорами (L08/P08)
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід</a> (INT 16h) — клавіші зі стрілками для руху, гарячі клавіші для скасування та перезапуску (L07/P07)
<br><a class="internal-link" data-href="How-to/addressing-modes.md" href="projects/how-to/addressing-modes.html" target="_self" rel="noopener nofollow">Режими адресації</a> — базова+зміщення для доступу до сітки, індексна адресація для стека ходів (L06/P06)
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури з параметрами</a> — try_move(direction), undo_move(), load_level(level_number), draw_grid() (L05/P05)
<br><a class="internal-link" data-href="How-to/flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Умовна логіка</a> — багатоумовні перевірки зіткнень (стіна? ящик? ящик+стіна? ящик+ящик?) (L04/P04)
<br><a class="internal-link" data-href="How-to/game-logic/Y06.md" href="projects/how-to/game-logic/y06.html" target="_self" rel="noopener nofollow">Ігрова логіка: Сокобан</a> — алгоритми штовхання ящиків, стек скасування та визначення завершення рівня
Програма — це головоломка Сокобан з штовханням ящиків у текстовому режимі DOSBox. При запуску перший рівень завантажується з сегмента даних: екран показує кімнату зі стін (суцільні блокові символи), зону підлоги, цільові позиції, позначені крапками або ромбами, ящики, відображені кольоровими квадратними символами, та персонажа гравця. Рядок стану показує номер поточного рівня, лічильник ходів та лічильник штовхань.Гравець рухається стрілками. Коли гравець наступає на ящик, ящик штовхається на одну клітинку в тому ж напрямку — але лише якщо клітинка за ящиком є порожньою підлогою або ціллю. Ящики не можна штовхати через стіни або через інші ящики. Коли всі ящики стоять на цільових позиціях, рівень завершено і завантажується наступний. Гра містить щонайменше 5 попередньо визначених рівнів зростаючої складності, кожен збережений як масив байтів у сегменті даних.Ключова особливість — скасування: натискання 'U' відміняє останній хід. Це реалізовано через стек — кожен хід записує дію (напрямок, чи був штовхнутий ящик) у стек історії ходів, а скасування вилучає її та скасовує дію. Це дає студентам конкретне, нетривіальне використання стекових структур даних поза викликами функцій. Натискання 'R' перезапускає поточний рівень.<br><img alt="Ігровий процес" src="assets/y06-screen-gameplay.png" target="_self"><br><img alt="Завершення рівня" src="assets/y06-screen-complete.png" target="_self">
Підхід A: Записувати компактний запис на кожен хід (1-2 байти: напрямок + прапорець штовхання ящика) та скасувати дію при відкаті.
Підхід B: Зберігати повний стан сітки (наприклад, 200 байтів) перед кожним ходом та відновлювати при скасуванні.
Що виміряти/спостерігати: Використання пам'яті для 100 кроків скасування (200 байтів проти 20 000 байтів), складність логіки скасування та максимальна глибина скасування, яку може підтримати сегмент стека. Компактний підхід значно ефективніший за пам'яттю, але потребує ретельного скасування кожної дії. Підхід A: Зберігати кожен рівень як повний масив байтів W*H (наприклад, 200 байтів для 20x10).
Підхід B: Використовувати кодування довжин серій, де тайли, що повторюються (особливо стіни та підлога), стискаються.
Що виміряти/спостерігати: Використання сегмента даних для 5 рівнів, складність завантажувача рівнів та зручність додавання нових рівнів. RLE ефективніше за пам'яттю, але потребує процедури декодера. Підхід A: Перевірити цільову клітинку (та клітинку за нею для штовхання ящика) перед оновленням будь-яких позицій.
Підхід B: Попередньо перемістити гравця, перевірити валідність стану та відкотити, якщо ні.
Що виміряти/спостерігати: Складність коду, кількість зчитувань сітки на хід та ризик залишити сітку в неконсистентному стані, якщо логіка відкату має помилку. Підхід з попередньою перевіркою безпечніший та простіший.
<br>Розподіл модулів (див. <a class="internal-link" data-href="How-to/include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">Підключення файлів: INCLUDE</a>):
Студент A: Ігрова логіка — рух та виявлення зіткнень, штовхання ящиків, перевірка умови перемоги, стек скасування (запис/вилучення записів ходів), логіка завершення рівня та прогресії
Студент B: Керування рівнями та дисплей — зберігання даних рівнів у сегменті даних, процедура завантажувача рівнів, рендеринг сітки у відеопам'ять, відображення символів/кольорів для кожного типу тайлу, рядок стану, функціональність перезапуску, екрани меню
Точки інтеграції: робочий масив сітки та змінні позиції гравця. Студент A модифікує сітку (рух гравця, штовхання ящика); Студент B зчитує для рендерингу. Визначте формат сітки (байтові значення для кожного типу тайлу: 0=підлога, 1=стіна, 2=ящик, 3=ціль, 4=гравець, 5=ящик на цілі, 6=гравець на цілі) разом.
Незалежне тестування: Студент A може тестувати рух та скасування, виводячи сітку як текст через INT 21h після кожного ходу. Студент B може рендерити жорстко задані дані рівня для перевірки візуального компонування та відображення кольорів. <br><a class="internal-link" data-href="How-to/game-logic/Y06.md" href="projects/how-to/game-logic/y06.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: логіка руху та зіткнень, оновлення складених тайлів, стек скасування, визначення завершення рівня.
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Формат рівня:</a> кожен рівень збережений як послідовність байтів, рядок за рядком, із заголовком (ширина, висота). Приклад: level1 DB 10, 8 з наступними 80 байтами кодів тайлів. Завантажувач рівнів копіює це в робочу сітку.
<br><a class="internal-link" data-href="How-to/lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиця рендерингу тайлів:</a> визначте таблицю пошуку, що відображає коди тайлів на пари (символ, атрибут). Приклад: tile_chars DB 20h, 0DBh, 0FEh, 0FAh, 02h, 0FEh, 02h та tile_attrs DB 07h, 08h, 0Eh, 0Ch, 0Fh, 0Ah, 0Fh для підлоги, стіни, ящика, цілі, гравця, ящика на цілі, гравця на цілі.
<br><a class="internal-link" data-href="How-to/flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Логіка руху:</a> при натисканні стрілки обчисліть цільову клітинку (player_row + dr, player_col + dc). Перевірте: якщо стіна — нічого. Якщо порожня/ціль — перемістити гравця. Якщо ящик — перевірте клітинку за ящиком (player_row + 2dr, player_col + 2dc): якщо порожня/ціль — штовхнути ящик і перемістити гравця. Інакше — нічого.
<br><a class="internal-link" data-href="How-to/bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Запис скасування:</a> 1 байт для напрямку (0-3) та прапорців (біт 2: ящик штовхнутий, біт 3: ящик потрапив на ціль, біт 4: гравець залишив ціль). При скасуванні відкочуємо хід: переміщуємо гравця назад, якщо ящик був штовхнутий — переміщуємо ящик назад.
Стандартні рівні Сокобан можна знайти в багатьох онлайн-колекціях. Переведіть їх у ваш байтовий формат. Класичні рівні «Microban» від David Skinner маленькі та добрі для тестування.
<br><a class="internal-link" data-href="How-to/loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Перевірка перемоги:</a> пройдіть по всіх клітинках TARGET (код 3). Якщо будь-яка клітинка TARGET залишається (не покрита ящиком), рівень ще не завершено. Альтернативно підрахуйте, скільки клітинок BOX_ON_TARGET дорівнює загальній кількості цілей.
Точки синхронізації:
Кінець тижня 9: узгодити формат робочої сітки (grid DB 400 DUP(0)), коди тайлів (0-6), прототипи процедур (try_move, load_level, draw_grid) та спільні змінні (player_row, player_col, move_count, current_level)
Кінець тижня 11: перша інтеграція — ігровий цикл (ввід → рух/штовхання → перемалювання → рядок стану) працює наскрізно; скасування та перезапуск функціонують
Кінець тижня 12: всі 5 рівнів прохідні; прогресія рівнів та check_win працюють; обидва студенти проходять рівні та шукають помилки
Кінець тижня 14: звіт та відео завершені; взаємна перевірка коду перед здачею
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/y06-sokoban.html</link><guid isPermaLink="false">Projects/Y06-Sokoban.md</guid><pubDate>Tue, 24 Feb 2026 16:02:31 GMT</pubDate><enclosure url="assets/y06-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y06-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[R05-TSR-Hotkey-Utility]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🔴 Червоний
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 350-500 (соло) / 500-700 (пара), 2-4 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/interrupt-handlers.md" href="projects/how-to/interrupt-handlers.html" target="_self" rel="noopener nofollow">Перехоплення векторів переривань</a> (INT 08h/1Ch, INT 09h) — встановлення власних обробників переривань та ланцюжкове з'єднання з оригінальними обробниками; основа програмування TSR (L07/P07)
<br><a class="internal-link" data-href="How-to/interrupt-handlers.md" href="projects/how-to/interrupt-handlers.html" target="_self" rel="noopener nofollow">Дисципліна обробників переривань</a> — збереження ВСІХ регістрів (включаючи FLAGS), налаштування DS всередині ISR, мінімізація обробника (L07/P07)
<br><a class="internal-link" data-href="How-to/segment-registers.md" href="projects/how-to/segment-registers.html" target="_self" rel="noopener nofollow">Керування сегментними регістрами</a> — ISR працює з довільним DS; потрібно використовувати дані відносно CS або явно завантажувати DS зі збереженого значення (L03/P03, L06/P06)
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять</a> (B800h) — запис відображення годинника зсередини ISR, використовуючи ES:DI для адресації B800h (L08/P08)
Доступ до області даних BIOS — зчитування часу через INT 1Ah або лічильник тактів BIOS за адресою 0040:006Ch, зчитування прапорців стану клавіатури за адресою 0040:0017h (L07)
<br><a class="internal-link" data-href="How-to/dos-services.md" href="projects/how-to/dos-services.html" target="_self" rel="noopener nofollow">Механіка TSR</a> (INT 27h або функція DOS 31h) — обчислення розміру резидентної частини, повернення до DOS із збереженням коду в пам'яті (L09)
Портовий ввід/вивід (інструкція IN) — зчитування скан-коду клавіатури з порту 60h всередині обробника INT 09h (L07)
Обмеження взаємодії DOS та BIOS — розуміння того, що функції DOS (INT 21h) НЕ безпечні для виклику зсередини ISR (реентрантність) (L07)
Розкладка пам'яті — розуміння того, що залишається резидентним, а що може бути звільнено; розміщення коду та даних ISR на початку програми (L09)
<br><a class="internal-link" data-href="How-to/game-logic/R05.md" href="projects/how-to/game-logic/r05.html" target="_self" rel="noopener nofollow">Ігрова логіка: TSR-утиліта</a> — дисципліна регістрів у ISR, виявлення гарячої клавіші, встановлення TSR та ланцюжкове з'єднання переривань
Програма є утилітою TSR (завершитися і залишитися резидентним), яка встановлюється в пам'яті та продовжує працювати після повернення до командного рядка DOS. Після встановлення у верхньому правому куті екрана з'являється відображення годинника в реальному часі у форматі HH:MM:SS, що оновлюється щосекунди. Годинник зберігається при запуску інших програм — користувач може набрати DIR, запустити EDIT або запустити інші програми, і годинник продовжує йти в куті. Комбінація гарячих клавіш (Ctrl+Alt+T) перемикає відображення годинника увімкнено/вимкнено. При вимкненні годинник зникає і кут екрана повертається до нормального стану; при повторному увімкненні він з'являється знову та продовжує оновлюватися.TSR досягає цього перехопленням двох векторів переривань. Переривання такту таймера (INT 1Ch або INT 08h) спрацьовує ~18,2 рази на секунду; обробник TSR інкрементує лічильник і кожні 18 тактів (~1 секунда) зчитує поточний час та записує його безпосередньо у відеопам'ять за адресою B800h. Переривання клавіатури (INT 09h) спрацьовує при кожному натисканні клавіші; обробник TSR зчитує скан-код з порту 60h, перевіряє прапорці клавіатури за адресою 0040:0017h на стан модифікаторів Ctrl+Alt, і перемикає годинник, якщо виявлено гарячу клавішу, потім передає керування оригінальному обробнику INT 09h, щоб нормальна робота клавіатури не порушувалася.Цей проєкт принципово відрізняється від інших Червоних тем. Немає ігрового циклу, немає користувацького інтерфейсу для проєктування, і відносно мало рядків коду. Складність є виключно архітектурною: код студента працює асинхронно всередині обробників переривань, у середовищі, де DS не вказує на сегмент даних програми, де псування будь-якого регістра пошкоджує будь-яку програму, що наразі працює, і де відлагодження є виключно складним, оскільки сам Turbo Debugger використовує ті самі переривання. Концепція TSR тренує глибше розуміння сегментації x86, обробки переривань та взаємодії з апаратним забезпеченням, ніж будь-який інший проєкт у меню. Ця тема найкраще підходить для студентів, які більше цікавляться системним програмуванням, ніж створенням візуальних застосунків. Парна робота наполегливо рекомендується — одне лише відлагодження це виправдовує.<br><img alt="Відображення годинника" src="assets/r05-screen-gameplay.png" target="_self"><br><img alt="Встановлення TSR" src="assets/r05-screen-install.png" target="_self">
Підхід A: Зчитує 32-бітний лічильник тактів з області даних BIOS за адресою 0040:006Ch (18,2 такти/секунду з півночі) та перетворює на HH:MM:SS через ділення (такти / 65543 = години, залишок / 1092 = хвилини, залишок / 18 = секунди — приблизно).
Підхід B: Зчитує апаратний годинник реального часу безпосередньо: запис номера регістра в порт 70h, зчитування значення BCD з порту 71h (регістр 00h=секунди, 02h=хвилини, 04h=години).
Що виміряти/спостерегти: Точність (лічильник BIOS є приблизним і може дрейфувати; CMOS RTC є точним), складність коду (A вимагає 32-бітного ділення; B вимагає перетворення BCD-в-ASCII, але без ділення) та портативність (A використовує стандартний інтерфейс BIOS; B звертається до апаратного забезпечення безпосередньо). Реалізуйте обидва та порівняйте відображуваний час. Підхід A: INT 1Ch викликається зсередини стандартного обробника INT 08h — це "користувацьке перехоплення", спеціально призначене для застосунків. Перехоплення INT 1Ch простіше, оскільки оригінальний обробник INT 08h вже надіслав EOI (кінець переривання) до PIC.
Підхід B: Пряме перехоплення INT 08h дає більше контролю, але вимагає самостійної відправки EOI (OUT 20h, 20h) та правильного ланцюжкового з'єднання з оригінальним обробником INT 08h для підтримки системної синхронізації.
Що виміряти/спостерегти: Складність коду (обробник INT 1Ch на ~5 інструкцій коротший), ризик нестабільності системи (помилки INT 08h можуть заморозити систему) та гнучкість (INT 08h може пропустити оригінальний обробник для зміни частоти таймера, що INT 1Ch не може). Це змістовне порівняння системного рівня. Підхід A: Зчитує байт стану модифікаторів клавіатури за адресою 0040:0017h всередині обробника INT 09h для перевірки, чи утримуються Ctrl та Alt, потім перевіряє скан-код клавіші T.
Підхід B: Підтримує власні змінні стану, відстежуючи події натискання Ctrl та Alt з необроблених скан-кодів, отриманих з порту 60h (1Dh = Ctrl натиснуто, 38h = Alt натиснуто тощо), та виявляє комбінацію, коли T (14h) надходить, поки обидва прапорці встановлені.
Що виміряти/спостерегти: Складність коду (A набагато простіший — одне зчитування пам'яті та інструкція TEST проти кілька змінних стану та відстеження кодів натискання/відпускання), надійність (B може пропустити комбінації, якщо клавіші натискаються в незвичному порядку) та освітня цінність (B навчає структурі скан-кодів у деталях).
Цей проєкт наполегливо рекомендується для пар. Сам процес відлагодження — де точки зупинки INT 3 взаємодіють з перехопленими перериваннями та спричиняють потрійні помилки — значно легший удвох: друга людина переглядає код, спостерігає за налагоджувачем та перевіряє збереження регістрів.
Студент A: Таймер та система годинника — перехоплення та обробник INT 1Ch (або INT 08h), підрахунок тактів, зчитування часу (з області даних BIOS або CMOS RTC), перетворення часу в ASCII, записи у відеопам'ять для відображення годинника, стан перемикання годинника.
Студент B: Клавіатура та система встановлення — перехоплення та обробник INT 09h, виявлення гарячої клавіші (зчитування порту 60h + перевірка прапорців модифікаторів), сигнал перемикання обробнику годинника Студента A, встановлення TSR (обчислення резидентного розміру, виклик завершення-та-збереження), виявлення повторного встановлення, код ініціалізації (збереження оригінальних векторів, відображення повідомлення про встановлення).
Точки інтеграції: Спільний резидентний блок даних, що містить: оригінальний вектор INT 1Ch, оригінальний вектор INT 09h, прапорець clock_visible (перемикається обробником Студента B, зчитується обробником Студента A) та збережене значення DS для резидентного сегмента даних. Цей блок повинен бути в резидентній частині програми. Визначте його структуру на КТ-2.
Незалежне тестування: Студент A може тестувати обробник таймера, встановивши лише перехоплення INT 1Ch (без перехоплення клавіатури) та перевіривши, що годинник з'являється та оновлюється. Студент B може тестувати обробник клавіатури, перехопивши лише INT 09h та перемикаючи видимий символ на екрані (наприклад, змінюючи колір комірки (0,0)) при натисканні гарячої клавіші.
Примітка: Кількість рядків нижча, ніж в інших Червоних проєктах, оскільки складність полягає в природі коду (обробники переривань, сегментація, конкурентність), а не в його обсязі. Кожен рядок в ISR несе більше ризику, ніж 10 рядків звичайного коду.
<br>
<a class="internal-link" data-href="How-to/game-logic/R05.md" href="projects/how-to/game-logic/r05.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: дисципліна регістрів у ISR, виявлення Ctrl+Alt+T, ланцюжкове з'єднання переривань, встановлення TSR. Структура програми для TSR:
; === РЕЗИДЕНТНА СЕКЦІЯ (залишається в пам'яті) ===
saved_1ch DD ? ; оригінальний вектор INT 1Ch
saved_09h DD ? ; оригінальний вектор INT 09h
saved_ds DW ? ; наш сегмент даних
clock_on DB 1 ; прапорець перемикання
tick_count DB 0 ; рахує до 18
signature DB 'CLK1' ; для перевірки повторного встановлення timer_handler PROC FAR ; ... код ISR ... JMP FAR CS:[saved_1ch]
timer_handler ENDP kbd_handler PROC FAR ; ... код ISR ... JMP FAR CS:[saved_09h]
kbd_handler ENDP end_of_resident LABEL BYTE
; === НЕРЕЗИДЕНТНА СЕКЦІЯ (перезаписується після TSR) ===
install: ; збереження оригінальних векторів, перехоплення нових, вивід повідомлення, перехід у резидентний режим <br>
<a class="internal-link" data-href="How-to/interrupt-handlers.md" href="projects/how-to/interrupt-handlers.html" target="_self" rel="noopener nofollow">Скелет ISR таймера:</a>
PUSH AX / PUSH BX / PUSH CX / PUSH DX / PUSH DS / PUSH ES
MOV DS, CS:[saved_ds]
INC tick_count
CMP tick_count, 18
JB @@skip
MOV tick_count, 0
CMP clock_on, 1
JNE @@skip
; ... зчитування часу, запис у відеопам'ять ...
@@skip:
POP ES / POP DS / POP DX / POP CX / POP BX / POP AX
JMP FAR CS:[saved_1ch] <br>
<a class="internal-link" data-href="How-to/timer-delays.md" href="projects/how-to/timer-delays.html" target="_self" rel="noopener nofollow">Зчитування часу з області даних BIOS:</a> Встановіть ES на 0040h (MOV AX, 0040h / MOV ES, AX). Зчитайте DWORD лічильник тактів за ES:[006Ch]. Перетворення: hours = ticks / 65543, minutes = (ticks MOD 65543) / 1092, seconds = (ticks MOD 1092) / 18. Це вимагає 32-бітного ділення (DX:AX / дільник). Альтернативно, використовуйте INT 1Ah AH=00h з ISR — це безпечно, оскільки INT 1Ah є викликом BIOS, а не DOS. Зчитування часу з CMOS RTC: MOV AL, 04h / OUT 70h, AL / IN AL, 71h зчитує години (BCD). MOV AL, 02h / OUT 70h, AL / IN AL, 71h зчитує хвилини. MOV AL, 00h / OUT 70h, AL / IN AL, 71h зчитує секунди. BCD в ASCII: старший напівбайт = SHR AL, 4 + '0', молодший напівбайт = AND AL, 0Fh + '0'. <br>
<a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Виявлення гарячої клавіші в обробнику клавіатури:</a> Зчитайте скан-код: IN AL, 60h. Перевірте, чи це 14h (make-код клавіші T). Якщо так, перевірте прапорці модифікаторів: MOV AX, 0040h / MOV ES, AX / TEST BYTE PTR ES:[0017h], 04h (Ctrl) та TEST BYTE PTR ES:[0017h], 08h (Alt). Якщо обидва встановлені і клавіша — T, перемкніть clock_on (XOR clock_on, 1). <br>
<a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Позиція годинника у відеопам'яті:</a> Рядок 0, стовпець 71 для 8-символьного відображення "HH:MM:SS". Зміщення = (0 * 80 + 71) * 2 = 142. Використовуйте атрибут 1Fh (яскраво-білий на синьому) для видимості на будь-якому фоні. <br>
<a class="internal-link" data-href="How-to/interrupt-handlers.md" href="projects/how-to/interrupt-handlers.html" target="_self" rel="noopener nofollow">Збереження/відновлення векторів переривань:</a> MOV AH, 35h / MOV AL, 1Ch / INT 21h повертає оригінальний вектор в ES:BX. Збережіть: MOV WORD PTR [saved_1ch], BX / MOV WORD PTR [saved_1ch+2], ES. Встановіть новий вектор: MOV AH, 25h / MOV AL, 1Ch / LEA DX, timer_handler / INT 21h. <br>
<a class="internal-link" data-href="How-to/dos-services.md" href="projects/how-to/dos-services.html" target="_self" rel="noopener nofollow">Перехід у резидентний режим:</a> MOV AH, 31h / MOV AL, 0 / MOV DX, resident_paragraphs / INT 21h. Значення DX дорівнює (OFFSET end_of_resident + 15) / 16 — це округлює вгору до наступної межі параграфа. Додайте розмір PSP (10h параграфів), якщо він ще не врахований. Точки синхронізації:
Кінець тижня 9: узгодити точну розкладку резидентної секції (порядок змінних, зміщення signature, мітка end_of_resident) та формат спільного прапорця clock_on
Кінець тижня 11: перша працююча інтеграція — годинник відображається, Ctrl+Alt+T перемикає його, TSR коректно залишається резидентним
Кінець тижня 12: TSR стабільний при запуску інших програм, повторне встановлення заблоковано, всі регістри коректно зберігаються
Кінець тижня 13: код повністю готовий, обидва підходи A-vs-B реалізовані та порівняні, README містить діаграму пам'яті
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/r05-tsr-hotkey-utility.html</link><guid isPermaLink="false">Projects/R05-TSR-Hotkey-Utility.md</guid><pubDate>Tue, 24 Feb 2026 16:02:29 GMT</pubDate><enclosure url="assets/r05-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/r05-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y14-Hex-Viewer-Editor]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🟡 Жовтий
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 450–600 (соло) / 650–850 (пара), 2–5 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/segment-registers.md" href="projects/how-to/segment-registers.html" target="_self" rel="noopener nofollow">Сегментні регістри</a> та far-вказівники — завантаження сегмента відеопам'яті (B800h) в ES, даних файлу у буфери відносно DS — L03/P03, L08/P08
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять</a> — обчислення зміщень рядок/стовпець, запис пар символ+атрибут — L08/P08
<br><a class="internal-link" data-href="How-to/file-io.md" href="projects/how-to/file-io.html" target="_self" rel="noopener nofollow">Файловий ввід/вивід через INT 21h</a> — відкриття (3Dh), читання (3Fh), запис (40h), переміщення (42h), закриття (3Eh) — L07/P07, P09
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід через INT 16h</a> — розпізнавання клавіш-стрілок (розширені скан-коди), звичайні ASCII-клавіші — L07/P07
<br><a class="internal-link" data-href="How-to/number-display.md" href="projects/how-to/number-display.html" target="_self" rel="noopener nofollow">Перетворення hex/decimal</a> — відображення двійкових даних у hex, арифметика зміщень — L03/P03, L04/P04
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури на основі стеку</a> — модульні процедури відображення, обробники вводу — L05/P05
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Керування масивом/буфером</a> — файловий буфер у сегменті даних, обчислення адрес — L06/P06
<br><a class="internal-link" data-href="How-to/addressing-modes.md" href="projects/how-to/addressing-modes.html" target="_self" rel="noopener nofollow">Режими адресації</a> — [BX+SI], [BX+DI+disp] для одночасної навігації буфером та відеопам'яттю — L06/P06
<br><a class="internal-link" data-href="How-to/game-logic/Y14.md" href="projects/how-to/game-logic/y14.html" target="_self" rel="noopener nofollow">Ігрова логіка: Hex-переглядач</a> — hex-ASCII перетворення, буферизація файлових блоків, трьохколонковий layout, навігація по зміщенню
Після запуску програма запитує ім'я файлу (або зчитує його з командного рядка). Екран заповнюється трьохколонковим hex-дампом у стилі класичних DOS-утиліт: ліве поле показує зміщення у файлі в шістнадцятковому форматі, центральна частина містить 16 hex-байтів на рядок, а праве поле відображає ASCII-інтерпретацію тих самих байтів — друковані символи відображаються як є, а недруковані значення показуються як крапки. Увесь дисплей рендериться через прямий запис у відеопам'ять, тому екран заповнюється миттєво.Курсор підсвітки позначає поточний обраний байт. Користувач навігує клавішами-стрілками, PgUp/PgDn та командою "Go to offset" (Ctrl+G або клавіша G), яка відкриває невелику панель введення. Різні класи байтів отримують різні кольори: нульові байти виглядають тьмяно, друковані ASCII-символи — яскраво-білі, а недруковані керуючі байти світяться жовтим. Рядок стану внизу показує поточне зміщення, розмір файлу та десяткове/шістнадцяткове значення обраного байту.У розширеній версії натискання Enter або призначеної клавіші перемикає режим редагування: користувач вводить нові hex-цифри на позиції курсору, буфер у пам'яті оновлюється, а змінений байт підсвічується контрастним кольором. Команда збереження (Ctrl+S або F2) записує змінений буфер назад на диск через файлові дескриптори INT 21h.<br><img alt="Режим перегляду з трьохколонковим hex-дампом та кольоровим кодуванням байтів" src="assets/y14-screen-viewer.png" target="_self"><br><img alt="Режим редагування з діалогом переходу до зміщення" src="assets/y14-screen-editor.png" target="_self">
Примітка: Файловий ввід/вивід (файлові сервіси INT 21h) розглядається у L09/P09 (тиждень 9). Протягом тижнів 7-8 зосередьтеся на основній логіці з жорстко заданими тестовими даними. Реалізуйте файлові операції, коли матеріал P09 стане доступним. Підхід A: Перемальовує всі 25 рядків при кожному натисканні клавіші, записуючи весь буфер екрану в B800h.
Підхід B: Відстежує, які комірки змінилися (курсор перемістився, відбулася прокрутка) і оновлює лише ці байти.
Що вимірювати/спостерігати: Підрахуйте загальну кількість записів у відеопам'ять на одне натискання клавіші в кожному підході та оцініть сприйняту швидкодію. Це ілюструє компроміс між простотою коду та ефективністю вводу/виводу. Підхід A: Зчитує весь файл у суцільний буфер сегмента даних (обмежений ~60 КБ у програмі .COM).
Підхід B: Зчитує лише вікно (напр., 4 КБ) і використовує INT 21h seek для завантаження нових блоків при прокрутці.
Що вимірювати/спостерігати: Максимальний підтримуваний розмір файлу, кількість викликів INT 21h під час навігації. Це безпосередньо демонструє концепцію ієрархії пам'яті з L06 та компроміс між використанням пам'яті та накладними витратами вводу/виводу. Підхід A: Зберігає 16-байтову таблицю пошуку ("0123456789ABCDEF") та індексує в неї значенням тетради.
Підхід B: Використовує умовну арифметику: якщо тетрада &lt; 10, додати '0'; інакше додати 'A'-10.
Що вимірювати/спостерігати: Розмір коду, кількість інструкцій на конвертований байт. Це класичний компроміс простір-час, видимий на рівні інструкцій.
<br>Розподіл модулів (див. <a class="internal-link" data-href="How-to/include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">Підключення файлів: INCLUDE</a>):
Студент A: Модуль файлового вводу/виводу та буфера — відкриття/закриття файлів, читання блоків, переміщення, керування буфером у пам'яті, розбір аргументів командного рядка. Надає: file_open, file_read_chunk, file_seek, file_save.
Студент B: Модуль відображення та навігації — рендеринг hex-дампу у відеопам'ять, керування курсором, цикл обробки клавіатурного вводу, логіка кольорових атрибутів. Надає: draw_screen, move_cursor, get_key, goto_offset.
Точка інтеграції: Спільний сегмент даних, що містить файловий буфер, поточне зміщення у файлі, розмір буфера та прапорці змін. Обидва модулі читають/записують цей спільний стан через узгоджені адреси в пам'яті. <br><a class="internal-link" data-href="How-to/game-logic/Y14.md" href="projects/how-to/game-logic/y14.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: hex-ASCII mapping, буферизація файлових блоків, трьохколонковий layout, навігація "Go to offset".
<br><a class="internal-link" data-href="How-to/file-io.md" href="projects/how-to/file-io.html" target="_self" rel="noopener nofollow">Файлові операції INT 21h:</a> Функція 3Dh відкриває файл (AL=режим, DS:DX=ім'я файлу ASCIIZ); повертає файловий дескриптор в AX. Функція 3Fh читає байти; функція 42h переміщує (AL=0 від початку, AL=1 від поточної позиції).
<br><a class="internal-link" data-href="How-to/bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Побітові операції для hex-парсингу:</a> Для функції "Go to offset" потрібен невеликий парсер hex-рядка-в-ціле-число. Обробляйте символи по одній тетраді за раз із SHL на 4 та OR.
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Позиція у відеопам'яті:</a> Формула: адреса = B800h:((рядок * 80 + стовпець) * 2). Байт за цією адресою — символ; наступний байт — атрибут.
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіші-стрілки:</a> INT 16h AH=00h повертає AL=0 для розширених клавіш; скан-код в AH ідентифікує клавішу (48h=вгору, 50h=вниз, 4Bh=вліво, 4Dh=вправо, 49h=PgUp, 51h=PgDn).
<br><a class="internal-link" data-href="How-to/lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиця пошуку для hex-конвертації:</a> Для конвертації тетради (0–15) у hex ASCII-символ використовуйте 16-байтову таблицю пошуку за відомою міткою: hex_chars DB '0123456789ABCDEF' — потім XLAT або MOV AL, [hex_chars + BX].
Для кольорових атрибутів: фон у бітах 6–4, передній план у бітах 3–0. Приклад: яскраво-білий на чорному = 0Fh, тьмяно-сірий = 08h, жовтий на чорному = 0Eh.
Точки синхронізації:
Кінець тижня 9: узгодити спільний сегмент .DATA (file_buffer DB 4096 DUP(?), file_size DW ?, current_offset DW ?, modified_flag DB ?) та прототипи всіх процедур
Кінець тижня 11: перша інтеграція — main.asm викликає обидва модулі, навігація по реальному файлу працює наскрізно
Кінець тижня 13: код повністю працює (перегляд + редагування + збереження), обидва розділи README з аналізом A-vs-B готові
Кінець тижня 14: фінальне тестування на спільних тестових файлах, обидва Loom-відео записані
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/y14-hex-viewer-editor.html</link><guid isPermaLink="false">Projects/Y14-Hex-Viewer-Editor.md</guid><pubDate>Tue, 24 Feb 2026 16:02:29 GMT</pubDate><enclosure url="assets/y14-screen-viewer.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y14-screen-viewer.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[G03-Memory-Card-Game]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🟢 Зелений
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 400-550 (соло) / 600-800 (пара), 2-4 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять</a> (малювання лицьових сторін карток, рамок та рядка стану за адресою B800h) — L08/P08
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Керування 2D-масивом</a> (сітка 4x4, збережена як плоский 16-байтовий масив з індексацією за рядками: індекс = рядок * 4 + стовпець) — L06/P06
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Кілька паралельних масивів</a> (card_symbols[16] для прихованого символу, card_state[16] для статусу лицем донизу/відкритий/знайдений) — L06/P06
<br><a class="internal-link" data-href="How-to/segment-registers.md" href="projects/how-to/segment-registers.html" target="_self" rel="noopener nofollow">Використання сегментних регістрів</a> (ES = B800h для відеопам'яті, DS для масивів даних) — L03/P03
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід</a> (INT 16h AH=00h; розширені скан-коди для клавіш-стрілок: 48h/50h/4Bh/4Dh) — L07/P07
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури зі стековими кадрами</a> (draw_card, flip_card, check_match, shuffle) — L05/P05
<br><a class="internal-link" data-href="How-to/random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">Генерація псевдовипадкових чисел</a> (простий ЛКГ або зерно з INT 1Ah для перемішування) — L04/P04 (арифметика, побітові операції)
<br><a class="internal-link" data-href="How-to/timer-delays.md" href="projects/how-to/timer-delays.html" target="_self" rel="noopener nofollow">Затримка через опитування системного годинника</a> (INT 1Ah для паузи між відкриттям невідповідних карток) — L07/P07
<br><a class="internal-link" data-href="How-to/addressing-modes.md" href="projects/how-to/addressing-modes.html" target="_self" rel="noopener nofollow">Обчислювана адресація</a> (відображення позиції сітки на зміщення у відеопам'яті вимагає множення та додавання) — L06/P06
<br><a class="internal-link" data-href="How-to/game-logic/G03.md" href="projects/how-to/game-logic/g03.html" target="_self" rel="noopener nofollow">Ігрова логіка: гра на запам'ятовування</a> — перемішування Фішера-Йетса, автомат станів картки, перевірка збігу пар
Програма реалізує класичну гру на запам'ятовування карток «Концентрація» у текстовому режимі 80x25. Сітка карток лицем донизу (4x4 = 16 карток за замовчуванням, що утворюють 8 пар) відображається в центрі екрана через прямий запис у відеопам'ять. Кожна картка лицем донизу з'являється як однотонний кольоровий прямокутник (наприклад, блок 5 стовпців на 3 рядки зі світло-блакитних блочних символів, DBh). Коли картка відкривається, блок змінюється, показуючи окремий символ (букву A-H або графічний символ CP437, як-от серце, ромб, нота, сонце) на контрастному кольоровому фоні.Гравець використовує клавіші-стрілки для переміщення підсвітки курсора по сітці. Поточно обрана картка має яскраво-жовту рамку або атрибут блимання. Натискання Enter або Space перевертає обрану картку, показуючи її символ. Після перевертання двох карток гра робить коротку паузу (приблизно 1,5 секунди, тайміровану через опитування INT 1Ah), щоб гравець міг запам'ятати символи. Якщо два символи збігаються, обидві картки залишаються відкритими та змінюють вигляд на «знайдену пару» (наприклад, зелений фон). Якщо вони не збігаються, обидві картки повертаються у стан лицем донизу.Рядок стану в рядку 0 показує "Moves: NN" (збільшується кожного разу при перевертанні пари) та "Pairs: N/8" через прямий запис у відеопам'ять з атрибутами білий-на-синьому. Коли всі 8 пар знайдені, повідомлення з привітанням з'являється в центрі екрана з загальною кількістю ходів. Гравець може натиснути R для перемішування та повторної гри або Q для виходу в DOS.<br><img alt="Ігровий процес — сітка 4x4 з відкритими та закритими картками" src="assets/g03-screen-gameplay.png" target="_self"><br><img alt="Екран перемоги — всі пари знайдено" src="assets/g03-screen-win.png" target="_self">
Підхід A: Використати два окремі байтові масиви — card_symbols DB 16 DUP(?) та card_state DB 16 DUP(?). Для доступу до картки N завантажити символ з [card_symbols + N] та стан з [card_state + N].
Підхід B: Чергувати в один масив 2-байтових структур: cards DB 32 DUP(?), де картка N має символ за [cards + N*2] та стан за [cards + N*2 + 1].
Що виміряти/спостерігати: Порівняйте, який підхід простіший для індексації (паралельні масиви використовують той самий індекс; структури вимагають множення на 2), який має кращу локальність даних (структури зберігають пов'язані дані поруч) та який легше розширити третім полем (наприклад, стан анімації). Підрахуйте інструкції для доступу до символу та стану однієї картки в кожному підході. Підхід A: Реалізувати Фішера-Йетса (ітерація від індексу 15 до 1; для кожного i генерувати випадкове j у [0, i], обміняти cards[i] та cards[j]).
Підхід B: Виконати 50 випадкових обмінів (обрати два випадкові індекси, обміняти їх).
Що виміряти/спостерігати: Фішер-Йетс гарантує рівномірний розподіл за рівно 15 обмінів; підхід із випадковими обмінами може залишити деякі картки на початкових позиціях і потребує більше ітерацій для порівнянної випадковості. Запустіть обидва 10 разів та зазначте, як часто верхня ліва картка залишається на місці. Обговоріть, яка мінімальна кількість випадкових обмінів забезпечує «достатню» рандомізацію, та чому Фішер-Йетс є архітектурно елегантним (обмежена кількість ітерацій). Підхід A: Обчислювати зміщення у відеопам'яті для картки (рядок, стовпець) під час виконання за допомогою MUL — зміщення = (початковийрядоксітки + рядок висота_картки) 160 + (початковийстовпецьсітки + стовпець ширина_картки) 2.
Підхід B: Попередньо обчислити 16-елементний масив слів card_offsets DW offset0, offset1, ... при запуску програми та безпосередньо шукати [card_offsets + card_index * 2].
Що виміряти/спостерігати: Підхід A є загальним та адаптується до різних розмірів сітки; підхід B уникає повторного MUL (який займає 70-160 тактів на 8086), але використовує 32 байти додаткових даних. Виміряйте повне перемалювання сітки кожним підходом за допомогою тактів INT 1Ah за 100 перемалювань.
<br>Розподіл модулів (див. <a class="internal-link" data-href="How-to/include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">Підключення файлів: INCLUDE</a>):
Студент A — Модуль відображення: малювання окремих карток (стани лицем донизу, відкритий, знайдений), рендеринг підсвітки курсора, рядок стану, повідомлення про завершення, очищення екрана. Файл: display.asm.
Студент B — Модуль ігрової логіки: ініціалізація масивів карток, процедура перемішування, обробка ходів (логіка перевертання, перевірка збігу), лічильник ходів, виявлення завершення гри, генератор псевдовипадкових чисел. Файл: game.asm.
Спільний головний модуль: запуск програми, цикл вводу (обробка клавіш-стрілок, перевертання через Enter/Space), виклик процедур відображення та логіки, перезапуск/вихід. Файл: main.asm.
Точки інтеграції:
Узгодьте розміщення сегмента даних на KT-2: точні байтові масиви для символів та станів, плюс спільні змінні (cursor_row, cursor_col, first_flip_index, second_flip_index, move_count, pairs_found).
Визначте сигнатуру draw_card: індекс картки в BL, викликається після будь-якої зміни стану. Студент A реалізує малювання; Студент B викликає його після перевертань та знаходжень пар.
Головний цикл вводу (Студент A або спільний) викликає process_flip (Студент B), який оновлює стан та повертає код результату (0 = перша картка перевернута, 1 = пара знайдена, 2 = невідповідність). <br><a class="internal-link" data-href="How-to/game-logic/G03.md" href="projects/how-to/game-logic/g03.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: перемішування Фішера-Йетса, автомат станів картки, перевірка збігу пар, затримка для запам'ятовування.
Символи CP437 для лицьових сторін карток: Хороші характерні символи: 03h (серце), 04h (ромб), 05h (трефа), 06h (піка), 0Dh (нота), 0Eh (сонце), 0Fh (зірковий вибух), 01h (смайлик), 02h (інвертований смайлик). Використовуйте 8 з них для 8 пар.
Вигляд картки лицем донизу: Заповніть прямокутник картки символом DBh (повний блок) з одним кольоровим атрибутом (наприклад, 19h = яскраво-синій на синьому, створюючи суцільний синій блок). Це надає картці вигляд суцільної кольорової плитки.
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Малювання прямокутника картки:</a> Щоб намалювати картку 5 завширшки на 3 заввишки на позиції відеопам'яті (рядок, стовпець): обчисліть базове зміщення = рядок 160 + стовпець 2. Запишіть 5 пар символ+атрибут за адресами base, base+160 (наступний рядок) та base+320 (рядок після). Використовуйте вкладений цикл або розгорнуті записи.
Підсвітка курсора: Змініть байт атрибуту клітинок рамки обраної картки на яскравий колір (наприклад, 0Eh = жовтий). Коли курсор переміщується, спочатку відновіть атрибут старої картки, потім підсвітіть нову.
<br><a class="internal-link" data-href="How-to/random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">Перемішування Фішера-Йетса:</a> Почніть з card_symbols = {A,A,B,B,C,C,D,D,E,E,F,F,G,G,H,H}. Цикл i від 15 до 1: генерувати j = random MOD (i+1), обміняти card_symbols[i] та card_symbols[j]. Використовуйте XCHG або тимчасовий регістр для обміну.
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Очищення буфера клавіатури:</a> Перед поверненням до циклу вводу після паузи очистіть клавіші, які гравець міг натиснути під час очікування. MOV AH, 01h / INT 16h перевіряє, чи доступна клавіша (ZF=0 означає так). Якщо доступна, MOV AH, 00h / INT 16h видаляє її. Повторюйте, доки ZF=1.
Точки синхронізації:
Кінець тижня 9: формат масивів card_symbols/card_state, значення станів (0/1/2), сигнатура draw_card (BL=індекс картки), угода про виклик process_flip
Кінець тижня 11: перша інтеграція — main.asm відображає сітку 4x4, стрілки переміщують курсор, Enter перевертає картки, перевірка збігу працює
Кінець тижня 13: код повністю працює (перемішування, пари, пауза, лічильники, привітання, перезапуск), чернетка звіту
Кінець тижня 14: звіт фіналізовано, відео записані, фінальне тестування граничних випадків завершене
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/g03-memory-card-game.html</link><guid isPermaLink="false">Projects/G03-Memory-Card-Game.md</guid><pubDate>Tue, 24 Feb 2026 16:02:28 GMT</pubDate><enclosure url="assets/g03-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/g03-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y09-Othello-Reversi]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🟡 Жовтий
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 450-600 (соло) / 650-850 (пара), 2-5 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Пряме записування у відеопам'ять</a> (B800h) — рендеринг дошки та анімація перевертання дисків без викликів BIOS — L08/P08
<br><a class="internal-link" data-href="How-to/segment-registers.md" href="projects/how-to/segment-registers.html" target="_self" rel="noopener nofollow">Керування сегментними регістрами</a> (ES для відео, DS для даних) — підтримка двох сегментних контекстів протягом роботи програми — L06/P08
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Обхід двовимірного масиву</a> — дошка 8x8, збережена як масив 64 байти, індексована через row * 8 + col — L06/P06
<br><a class="internal-link" data-href="How-to/lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Вектори напрямків</a> та <a class="internal-link" data-href="How-to/loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">сканування на основі циклу</a> — перевірка 8 напрямків від поставленого диска використовує таблицю напрямків пар (delta_row, delta_col) та цикл сканування — L04/P04, L06/P06
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури з параметрами на стеку</a> — is_valid_move(row, col, player), flip_discs(row, col, player), count_score() — L05/P05
<br><a class="internal-link" data-href="How-to/bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Побітові операції</a> та <a class="internal-link" data-href="How-to/flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">прапорці</a> — кодування стану клітинки (порожня/біла/червона) у 2 бітах, використання TEST та CMP для валідації ходів — L04/P05
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід</a> (INT 16h) — навігація клавішами-стрілками та Enter для розміщення диска — L07/P07
<br><a class="internal-link" data-href="How-to/flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Умовне розгалуження</a> — інтенсивне використання CMP/JE/JNE для сканування напрямків, валідації ходів, виявлення пропуску/кінця гри — L04/P04
<br><a class="internal-link" data-href="How-to/game-logic/Y09.md" href="projects/how-to/game-logic/y09.html" target="_self" rel="noopener nofollow">Ігрова логіка: Отелло</a> — сканування дійсних ходів у 8 напрямках, перевертання дисків, виявлення пропуску/кінця гри
Програма відкривається у DOSBox і показує центровану дошку Отелло 8x8, відрендерену безпосередньо у відеопам'яті. Кожна клітинка намальована як блок шириною у два символи (щоб виглядати приблизно квадратною у текстовому режимі 80x25), використовуючи блокові символи (наприклад, DB / \xDB) з атрибутами кольору для розрізнення двох гравців: яскраво-білі фішки та яскраво-червоні фішки на темно-зеленому тлі, що представляє дошку. Чотири початкові фішки займають центр у класичній стартовій позиції Отелло.Колір поточного гравця позначений на інформаційній панелі праворуч або під дошкою, разом з лічильником рахунку в реальному часі, що показує кількість дисків для кожної сторони. Коли хід людського гравця, миготливий або підсвічений курсор позначає поточну обрану клітинку. Клітинки, де гравець має дозволений хід, ненав'язливо підсвічені (наприклад, тьмяна крапка або інший відтінок фону), що робить гру зрозумілою для новачків. Клавіші-стрілки переміщують курсор, а Enter ставить диск. Коли диск поставлений, усі захоплені диски суперника у всіх дійсних напрямках (до 8) перевертаються на колір поточного гравця у короткій послідовній анімації — клітинки змінюють колір одна за одною з короткою затримкою, роблячи каскад видимим.Коли гравець не має дійсних ходів, гра автоматично передає хід і відображає коротке повідомлення. Коли жоден з гравців не може ходити, гра закінчується: дошка показує фінальний рахунок, оголошує переможця (або нічию) і чекає натискання клавіші для виходу.<br><img alt="Ігровий процес" src="assets/y09-screen-gameplay.png" target="_self"><br><img alt="Кінець гри" src="assets/y09-screen-gameover.png" target="_self">
Підхід A: Написати 8 окремих блоків коду, по одному для кожного напрямку (N, NE, E, SE, S, SW, W, NW), кожен зі своїми перевірками меж та циклом.
Підхід B: Зберегти 8 пар напрямків у таблиці dir_table DB -1,0, -1,1, 0,1, 1,1, 1,0, 1,-1, 0,-1, -1,-1 та ітерувати за допомогою одного параметризованого циклу сканування.
Що вимірювати/спостерігати: Розмір коду в байтах, кількість умовних переходів, простота додавання 9-го напрямку (гіпотетично). Протиставляє розмір коду та розмір даних і ілюструє, як підхід, керований даними (B), замінює повторюване розгалуження (A). Рекомендація: Студентам першого курсу слід реалізувати Підхід B (цикл на основі таблиці) як основний. Він коротший, простіший для налагодження та легший у підтримці. Підхід A (розгорнуті 8 блоків) служить корисною точкою порівняння для звіту — реалізуйте його другим, якщо є час, щоб можна було виміряти обидва, але починайте з B. Підхід A: Один байт на клітинку (0=порожня, 1=біла, 2=червона). 64 байти. Простий доступ через MOV/CMP.
Підхід B: 2 біти на клітинку, 16 байт для всієї дошки. Потребує SHR/AND для читання, AND/OR/SHL для запису.
Що вимірювати/спостерігати: Обсяг пам'яті (64 проти 16 байт), кількість інструкцій на доступ до клітинки, час валідації всіх ходів. Обговорити, чому 64 байти є незначними на 8086 (немає кешу, немає переваг від вирівнювання), але упакування 2-бітне може мати значення на системах з обмеженою пам'яттю. Підхід A: Для кожного дійсного ходу порахувати, скільки дисків буде перевернуто; обрати максимум.
Підхід B: Присвоїти вагу кожній позиції дошки (кути — високо, суміжні з кутами — від'ємно, краї — помірно), оцінити кожен дійсний хід як перевертання + вага_позиції.
Що вимірювати/спостерігати: Відсоток перемог проти людини за кілька ігор, складність коду. Обговорити, як це паралелить реальні компроміси між простими евристиками та складнішими функціями оцінки в ігровому ШІ.
<br>Розподіл модулів (див. <a class="internal-link" data-href="How-to/include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">Підключення файлів: INCLUDE</a>):
Студент A — Відображення дошки та інтерфейс: рендеринг у відеопам'яті (draw_board, draw_piece, highlight_moves, draw_score), обробка вводу курсору, анімація перевертання, екран меню. Файли: display.asm, input.asm.
Студент B — Ігрова логіка та ШІ: структура даних дошки, валідація ходів (is_valid_move, find_all_valid_moves), перевертання дисків (flip_discs), виявлення пропуску/кінця гри, вибір ходу ШІ. Файли: game.asm, ai.asm.
Спільне: main.asm з ігровим циклом, сегментом даних, константами. Узгодьте угоди про виклик: наприклад, рядок у DL, стовпець у DH, гравець у CL; повернення прапорців у CF або AL.
Точки інтеграції: Студент B надає find_all_valid_moves(player) -&gt; count (заповнює спільний масив valid_moves) та ai_choose_move() -&gt; (row, col). Студент A викликає їх для підсвічування ходів та виконання ходу ШІ. <br><a class="internal-link" data-href="How-to/game-logic/Y09.md" href="projects/how-to/game-logic/y09.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: сканування 8 напрямків через таблицю дельт, перевертання дисків, виявлення пропуску та кінця гри.
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Відображення дошки на екран:</a> якщо верхня ліва клітинка дошки знаходиться на рядку екрану sr, стовпці екрану sc, і кожна клітинка має ширину 2 символи та висоту 1 рядок, то клітинка (r, c) відображається на зміщення у відеопам'яті ((sr + r) * 80 + sc + c * 2) * 2. Зберігайте sr та sc як константи.
Блоковий символ: 0DBh — символ повного блоку у кодовій сторінці 437. Біла фішка: атрибут 0Fh (яскраво-білий на чорному) або 2Fh (яскраво-білий на зеленому). Червона фішка: атрибут 0Ch (яскраво-червоний на чорному) або 2Ch (яскраво-червоний на зеленому). Порожня зелена клітинка: 0DBh з атрибутом 02h (зелений на чорному).
<br><a class="internal-link" data-href="How-to/lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиця напрямків:</a> dir_table DB -1,0, -1,1, 0,1, 1,1, 1,0, 1,-1, 0,-1, -1,-1 (row_delta, col_delta для N, NE, E, SE, S, SW, W, NW). Завантажуйте через MOV AL, [dir_table + BX] / MOV AH, [dir_table + BX + 1], потім ADD BX, 2 для переходу до наступного.
<br><a class="internal-link" data-href="How-to/loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Сканування дійсних ходів:</a> для кожної порожньої клітинки, для кожного з 8 напрямків, рухатись у напрямку, поки клітинка містить фішку суперника, підраховуючи перевертання. Якщо рух закінчується на дружній фішці і рахунок &gt; 0, хід дійсний з такою кількістю перевертань у цьому напрямку.
Підрахунок рахунку: REP SCASB може підрахувати входження конкретного байта в масиві дошки 64 байти, коли ES:DI вказує на дошку та CX=64. Або просто цикл з CMP [SI], player_byte / JNE skip / INC count.
Жадібний ШІ: ітерувати по списку valid_moves, викликати count_flips(row, col, player) для кожного, відстежувати максимум. Нічиї можна розв'язувати за першим знайденим.
Точки синхронізації:
Кінець тижня 8: узгодити формат клітинки (байт на клітинку проти 2 біти), угоди про виклик (рядок у DL, стовпець у DH, гравець у CL), формат масиву valid_moves
Кінець тижня 10: Студент B надає робочу is_valid_move та flip_discs зі стабільним інтерфейсом, щоб Студент A міг інтегрувати обробку ходу в ігровий цикл
Кінець тижня 11: перша наскрізна інтеграція — повний цикл від підсвічування дійсних ходів до перевертання та оновлення рахунку працює наскрізно
Кінець тижня 13: код зафіксований; чернетка звіту з аналізом «Підхід A проти B» готова для взаємного рев'ю
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/y09-othello-reversi.html</link><guid isPermaLink="false">Projects/Y09-Othello-Reversi.md</guid><pubDate>Tue, 24 Feb 2026 16:02:27 GMT</pubDate><enclosure url="assets/y09-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y09-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[R04-Pac-Man]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🔴 Червоний
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 550-700 (соло) / 800-1000 (пара), 3-6 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/interrupt-handlers.md" href="projects/how-to/interrupt-handlers.html" target="_self" rel="noopener nofollow">Перехоплення переривання таймера</a> (INT 1Ch) — весь рух сутностей керується таймером; кілька сутностей оновлюються за такт (L07/P07)
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід</a> (INT 16h) — неблокуючий опитування клавіші для вводу напрямку в реальному часі; черга зміни напрямку (L07/P07)
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять</a> (B800h) — відображення лабіринту, точок, сутностей, рахунку та життів (L08/P08)
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Обробка 2D масивів</a> — лабіринт зберігається як 2D байтовий масив; виявлення зіткнень та запити пошуку шляху активно використовують цей масив (L06/P06)
<br><a class="internal-link" data-href="How-to/segment-registers.md" href="projects/how-to/segment-registers.html" target="_self" rel="noopener nofollow">Маніпуляція сегментними регістрами</a> (ES, DS) — ES для відеопам'яті, DS для ігрових даних (L03/P03, L06/P06)
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури та дисципліна стеку</a> — модульний дизайн: move_pacman, move_ghost, check_wall, eat_dot, check_collision, draw_entity (L05/P05)
Скінченні автомати — режими поведінки привидів (переслідування/патрулювання/переляканий) та стани гри (гра/загибель/кінець гри/перемога) (L04/P04)
<br><a class="internal-link" data-href="How-to/lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиці пошуку</a> — зіставлення напрямку зі зміщенням, властивості тайлів лабіринту, таблиці кольорів привидів (L06/P06)
<br><a class="internal-link" data-href="How-to/random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">Генерація псевдовипадкових чисел</a> — випадковий привид на перехрестях, розміщення силових пілюль (L07)
<br><a class="internal-link" data-href="How-to/flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Прапорці та умовна логіка</a> — зіткнення зі стінами, зіткнення сутностей, переходи між режимами, підрахунок точок (L04/P04)
<br><a class="internal-link" data-href="How-to/game-logic/R04.md" href="projects/how-to/game-logic/r04.html" target="_self" rel="noopener nofollow">Ігрова логіка: Pac-Man</a> — зіткнення в лабіринті, ШІ привидів (переслідування/випадковий/переляканий), споживання точок та порядок оновлення сутностей
Програма є спрощеною аркадною грою Pac-Man, що працює в текстовому режимі 80x25 у DOSBox. Після запуску гравець бачить лабіринт, відображений кольоровими символами рамкового малювання, коридори, заповнені точками (маленькі символи-кулі), та яскраво-жовтого персонажа Pac-Man у початковій позиції. Один або два привиди, кожен відображений унікальним кольором (наприклад, червоний та блакитний), займають свої стартові позиції в лабіринті. Відображення рахунку та лічильник життів розташовані під лабіринтом. Вся сцена малюється через прямий запис у відеопам'ять за адресою B800h.Гра відбувається в реальному часі, керована перериванням системного таймера (INT 1Ch). Гравець переміщує Pac-Man по лабіринту клавішами-стрілками, з'їдаючи точки на своєму шляху. Кожна точка додається до рахунку. Дві-чотири "силові пілюлі" (більші, блимаючі символи) розміщені в кутах лабіринту; коли Pac-Man з'їдає одну з них, активується режим "переляку", коли привиди стають синіми та тікають від Pac-Man, тимчасово стаючи їстівними для бонусних очків. Привиди рухаються автономно за таймером — мінімум, один привид переслідує Pac-Man безпосередньо (рухаючись до поточної позиції Pac-Man на кожному перехресті), а один привид рухається випадково (обираючи випадковий допустимий напрямок на кожному перехресті). Якщо привид торкається Pac-Man, гравець втрачає життя (з короткою анімацією загибелі). Втрата всіх 3 життів закінчує гру. З'ївши всі точки, виграєте рівень.Основна складність цього проєкту полягає в керуванні кількома одночасно рухомими сутностями, кожна з яких взаємодіє з лабіринтом та одна з одною. Pac-Man реагує на клавіатурний ввід; привиди дотримуються логіки ШІ; всі повинні поважати стіни лабіринту; а зміна стану режиму переляку впливає на поведінку привидів, зовнішній вигляд та результати зіткнень одночасно. Це значно складніша проблема керування станом, ніж Snake або Breakout. Пошук шляху привидів, навіть у спрощеній формі, вимагає алгоритмічного мислення. Парна робота наполегливо рекомендується — можна стверджувати, що вона необхідна для повного набору функцій.<br><img alt="Ігровий процес" src="assets/r04-screen-gameplay.png" target="_self"><br><img alt="Кінець гри" src="assets/r04-screen-gameover.png" target="_self">
Підхід A: На кожному перехресті привид-переслідувач порівнює Манхеттенську відстань до Pac-Man для кожного допустимого напрямку та обирає напрямок з найменшою відстанню. Це O(1) на рішення — кілька інструкцій CMP — але може застрягти в тупиках або коливатися в певних геометріях лабіринту.
Підхід B: Привид запускає пошук у ширину через масив лабіринту для знаходження найкоротшого шляху до Pac-Man та робить перший крок цього шляху. Це завжди знаходить оптимальний маршрут, але має складність O(n), де n — кількість комірок лабіринту, та вимагає масиву відвіданих комірок та черги.
Що виміряти/спостерегти: Складність коду (A — ~20 інструкцій; B — ~80-100 з циклом BFS), якість поведінки привида (протестуйте обидва в одному лабіринті та спостерігайте, як часто кожен застряє), та час CPU на хід привида (підрахуйте інструкції в Turbo Debugger). Жадібний підхід рекомендується для мінімальних вимог; BFS є чудовим розширенням. Підхід A: Кожна комірка лабіринту — один байт, що кодує тип тайлу (0=стіна, 1=точка, 2=порожній коридор, 3=силова пілюля, 4=загін привидів). Це використовує ~300 байтів для лабіринту 20x15, але робить запити тайлів тривіальними (CMP [maze + offset], 0).
Підхід B: Стіни зберігаються як бітовий масив (один біт на комірку, ~38 байтів для 20x15), а точки відстежуються в окремому бітовому масиві. Це економить пам'ять, але вимагає операцій бітового зсуву (SHR, TEST) для запиту окремих комірок.
Що виміряти/спостерегти: Використання пам'яті, складність коду для запиту "чи є ця комірка стіною?", та чи має значення економія пам'яті в 64 КБ сегменті. Спостерігайте різницю в кількості інструкцій для 100 запитів стін у Turbo Debugger. Підхід A: Перед переміщенням сутності записує "фоновий" символ (точка, порожньо або стіна) на її стару позицію, потім малює сутність на новій позиції. Це вимагає знання того, що було в тій комірці до появи сутності.
Підхід B: Перед малюванням сутності зберігає символ та атрибут у пункті призначення (зчитування з відеопам'яті або з даних лабіринту), потім малює сутність. Коли сутність переміщується, відновлює збережений фон на старій позиції.
Що виміряти/спостерегти: Який підхід коректно обробляє проходження двох сутностей через одну комірку (A може стерти одну сутність при русі іншої; B зберігає шари). Протестуйте з двома привидами, що проходять через одну комірку.
Цей проєкт дуже наполегливо рекомендується для пар — це, мабуть, найскладніший проєкт у Червоній категорії. Поєднання керування лабіринтом, кількох сутностей зі ШІ та модального ігрового процесу (нормальний проти переляканий) створює суттєву взаємодіючу складність.
Студент A: Логіка сутностей та ШІ — рух Pac-Man (з чергою зміни напрямку), ШІ привидів (поведінка переслідування та випадкова), поведінка привидів у режимі переляку (розворот, стають їстівними, тікають), виявлення зіткнень сутність-сутність, таймер режиму переляку, керування життями та загибеллю.
Студент B: Світ та відображення — дизайн та завантаження даних лабіринту, керування точками та силовими пілюлями (відстеження залишених точок, підрахунок для умови перемоги), відображення у відеопам'яті (стіни лабіринту, точки, сутності з правильними кольорами, відображення рахунку/життів, екрани перемоги/кінця гри), обробник клавіатурного вводу, перехоплення/відновлення переривання таймера, керування станом гри (гра/загибель/перемога/кінець гри).
Точки інтеграції: Спільний сегмент даних, що містить: масив лабіринту, позиції сутностей (Pac-Man x/y, привид1 x/y, привид2 x/y), напрямки сутностей, режим привида (нормальний/переляканий), кількість залишених точок, рахунок, життя, прапорець стану гри, збережений вектор INT 1Ch. Визначте точну структуру на КТ-2.
Незалежне тестування: Студент A може тестувати ШІ привидів, виводячи обрані напрямки в консоль у спрощеному лабіринті (без відображення). Студент B може тестувати відображення зі статичними сутностями (намалювати лабіринт, розмістити жорстко закодовані точки та сутності, перевірити прокручування та кольори атрибутів). <br><a class="internal-link" data-href="How-to/game-logic/R04.md" href="projects/how-to/game-logic/r04.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: зіткнення зі стінами лабіринту, жадібне переслідування привида, випадковий рух, режим переляку, споживання точок.
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Кодування лабіринту:</a> Використовуйте плоский байтовий масив: maze DB 20 DUP(1), ... де кожен байт — тип тайлу (0=коридор, 1=стіна, 2=точка, 3=силова пілюля). Для перевірки, чи комірка (x, y) є стіною: offset = y * MAZE_WIDTH + x, потім CMP [maze + offset], 1. Використовуйте константи: WALL EQU 1, DOT EQU 2 тощо.
<br><a class="internal-link" data-href="How-to/lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиця напрямок-зміщення:</a> Визначте dir_dx DB 0, 0, -1, 1 та dir_dy DB -1, 1, 0, 0 для Вгору, Вниз, Ліворуч, Праворуч. Для обчислення наступної позиції в напрямку D: new_x = x + dir_dx[D], new_y = y + dir_dy[D]. Використовуйте BX як індекс у таблиці.
<br><a class="internal-link" data-href="How-to/arithmetic.md" href="projects/how-to/arithmetic.html" target="_self" rel="noopener nofollow">Жадібне переслідування привида:</a> На кожному ході обчисліть Манхеттенську відстань від кожного допустимого сусіда до Pac-Man: dist = |ghost_x + dx - pac_x| + |ghost_y + dy - pac_y|. Оберіть напрямок з найменшою відстанню. Використовуйте NEG + умовний перехід (або SUB + JNS) для абсолютного значення.
<br><a class="internal-link" data-href="How-to/timer-delays.md" href="projects/how-to/timer-delays.html" target="_self" rel="noopener nofollow">INT 1Ch</a> спрацьовує ~18,2 рази/секунду. Pac-Man може рухатися кожні 3 такти (~6 рухів/секунду), привиди кожні 4 такти (~4,5 рухи/секунду). Використовуйте окремі лічильники тактів для Pac-Man та привидів, щоб надати їм різні швидкості. У режимі переляку збільшіть інтервал тактів привидів, щоб сповільнити їх.
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Неблокуючий клавіатурний ввід:</a> MOV AH, 01h / INT 16h — ZF=0 означає, що клавіша очікує. Клавіші-стрілки: AL=0, скан-код в AH (48h=Вгору, 50h=Вниз, 4Bh=Ліворуч, 4Dh=Праворуч).
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Відображення сутностей:</a> Перед малюванням сутності запам'ятайте, що лабіринт має в цій позиції (стіна/точка/порожньо). Коли сутність переміщується, відновіть цей символ. Альтернативно, завжди малюйте спочатку з даних лабіринту, потім накладайте сутності зверху — це добре працює, якщо ви перемальовуєте стару та нову комірки сутності кожен кадр.
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Відеопам'ять</a> для рядка R, стовпця C: offset = (R * 80 + C) * 2. Якщо лабіринт починається з рядка 1, стовпця 5 екрана, то комірка лабіринту (mx, my) відповідає позиції екрана (1 + my, 5 + mx).
Символ Pac-Man: Використовуйте власний символ або просто &lt;, &gt;, v, ^ (3Ch, 3Eh, 76h, 5Eh) залежно від поточного напрямку, з яскраво-жовтим атрибутом (0Eh). Привиди: M або символ 02h (усміхнене обличчя) у відповідних кольорах.
Виявлення перехрестя: Комірка є перехрестям, якщо має 3 або більше сусідів-нестін. Привиди переоцінюють свій напрямок лише на перехрестях; у прямих коридорах вони продовжують у поточному напрямку. Це спрощує ШІ та запобігає розвороту привидів у коридорах.
Тривалість режиму переляку: ~100 тактів (~5,5 секунд). Зберігайте залишені такти в лічильнику. Привиди в режимі переляку використовують атрибут 01h (синій) або 09h (яскраво-синій).
Точки синхронізації:
Кінець тижня 9: узгодити точну структуру спільного .DATA сегмента (масив лабіринту, позиції сутностей, frightened_timer, score, lives, dots_remaining) та прототипи всіх PROC
Кінець тижня 11: перша працююча інтеграція — Pac-Man рухається лабіринтом, привиди рухаються автономно, зіткнення працюють
Кінець тижня 12: режим переляку повністю інтегрований — з'їдання силової пілюлі змінює поведінку та вигляд привидів
Кінець тижня 13: код повністю готовий, обидва студенти тестують спільно, README містить аналіз A-vs-B
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/r04-pac-man.html</link><guid isPermaLink="false">Projects/R04-Pac-Man.md</guid><pubDate>Tue, 24 Feb 2026 16:02:25 GMT</pubDate><enclosure url="assets/r04-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/r04-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y05-2048]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🟡 Жовтий
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 450-600 рядків коду (соло) / 650-850 рядків коду (пара), 2-5 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/number-display.md" href="projects/how-to/number-display.html" target="_self" rel="noopener nofollow">Відображення багатоцифрових чисел</a> — перетворення двійкових значень (до 2048+) у десяткові рядки ASCII; центрування тексту в клітинці фіксованої ширини (L04/P04)
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Маніпулювання масивами</a> з напрямковою логікою — зсув та злиття плиток у чотирьох напрямках з різним порядком обходу (L06/P06)
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять</a> (B800h) — рендеринг багаторядкових, багатоколонкових клітинок плиток з атрибутами кольорів (L08/P08)
<br><a class="internal-link" data-href="How-to/lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиці пошуку</a> — відображення значень плиток (степені двійки) на атрибути кольорів (L06/P06)
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід</a> (INT 16h) — обробка клавіш зі стрілками для чотирьох напрямків (L07/P07)
<br><a class="internal-link" data-href="How-to/arithmetic.md" href="projects/how-to/arithmetic.html" target="_self" rel="noopener nofollow">Арифметичні операції</a> — додавання для рахунку, множення/ділення для перетворення числа в рядок, SHL для подвоєння значень плиток (L04/P04)
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури з параметрами на стеку</a> — slide_row(direction), merge_tiles(), draw_tile(row, col, value) (L05/P05)
<br><a class="internal-link" data-href="How-to/random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">Генерація псевдовипадкових чисел</a> — розміщення нових плиток у випадкових порожніх позиціях (L07)
<br><a class="internal-link" data-href="How-to/game-logic/Y05.md" href="projects/how-to/game-logic/y05.html" target="_self" rel="noopener nofollow">Ігрова логіка: 2048</a> — алгоритми зсуву, стиснення, злиття плиток та визначення кінця гри
Програма реалізує головоломку 2048 із зсувом чисел у текстовому режимі DOSBox. Екран показує сітку 4x4 плиток, кожна відображена як багатосимвольна клітинка (наприклад, 7 символів завширшки та 3 рядки заввишки) для розміщення чисел до 4 цифр. Порожні клітинки виглядають як пусті обрамлені прямокутники. Плитки з різними значеннями мають різні кольори: 2 може бути білою на темно-сірому, 4 — світло-жовтою, 8 — помаранчевою, 16 — червоною, 32 — яскраво-червоною тощо, все досягається через байти атрибутів відеопам'яті.Гравець натискає клавіші зі стрілками, щоб зсунути всі плитки в обраному напрямку. Плитки зсуваються, поки не зіткнуться зі стіною або іншою плиткою. Якщо дві сусідні плитки з однаковим значенням зіткнуться під час зсуву, вони зливаються в одну плитку з подвоєним значенням, а рахунок збільшується на значення злитої плитки. Після кожного допустимого ходу нова плитка (значення 2 або 4, причому 2 ймовірніше) з'являється у випадковій порожній клітинці. Рахунок помітно відображається у верхній частині екрана.Гра закінчується, коли не залишається допустимих ходів — кожна клітинка зайнята і жодні сусідні клітинки не мають однакового значення. Екран "GAME OVER" показує фінальний рахунок. Якщо гравець створює плитку 2048, з'являється вітальне повідомлення "YOU WIN", хоча гравець може продовжити гру. Візуальна якість досягається завдяки градієнту кольорів за значеннями плиток та акуратно відформатованим багатоцифровим числам, центрованим у кожній клітинці.<br><img alt="Ігровий процес" src="assets/y05-screen-gameplay.png" target="_self"><br><img alt="Кінець гри" src="assets/y05-screen-gameover.png" target="_self">
Підхід A: Зберігати значення плитки безпосередньо (2, 4, 8, 16, ..., 2048) як слово (DW). Злиття означає додавання двох рівних значень. Відображення означає перетворення 16-бітного числа в десятковий рядок.
Підхід B: Зберігати показник степеня (1, 2, 3, 4, ..., 11). Злиття означає збільшення показника на 1. Відображення означає пошук значення в таблиці (powers DW 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048).
Що виміряти/спостерігати: Складність коду для злиття, перетворення для відображення та пошуку в таблиці кольорів. Підхід з показниками спрощує пошук кольору (показник є прямим індексом), але додає рівень непрямого доступу для відображення. Підхід A: Спочатку стиснути всі плитки до одного краю (прибравши проміжки), потім сканувати сусідні рівні пари та злити їх, потім стиснути знову.
Підхід B: Обробляти плитки одну за одною від переднього краю, зливаючи по ходу.
Що виміряти/спостерігати: Коректність для граничних випадків (наприклад, [2,2,2,2] має стати [4,4,0,0], а не [8,0,0,0]), зрозумілість коду та кількість проходів по рядку. Двопрохідний підхід легший для аналізу; однопрохідний ефективніший, але потребує ретельного прапорця одноразового злиття. Підхід A: Ділити значення на 10 повторно, витягуючи цифри.
Підхід B: Використовувати попередньо обчислену таблицю рядків ("2", "4", "8", ..., "2048").
Що виміряти/спостерігати: Обсяг коду, час виконання та гнучкість (що якщо плитки можуть перевищити 2048?). Таблиця пошуку швидша та простіша для невеликого фіксованого набору значень; підхід з діленням узагальнюється на будь-яке число.
<br>Розподіл модулів (див. <a class="internal-link" data-href="How-to/include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">Підключення файлів: INCLUDE</a>):
Студент A: Ігрова логіка — алгоритм зсуву (стиснення + злиття) для всіх чотирьох напрямків, розміщення нової плитки, визначення кінця гри, визначення перемоги, обчислення рахунку
Студент B: Рендеринг та ввід — рендеринг клітинок плиток (багатосимвольні клітинки з рамками та центрованими числами), таблиця атрибутів кольорів, обробник клавіатури, відображення рахунку, екрани кінця гри/перемоги, перетворення числа в рядок
Точки інтеграції: масив поля (4x4 слів або байтів) та стан гри (рахунок, фаза гри). Студент A оновлює поле; Студент B зчитує його. Визначте формат поля (рядковий масив з 16 слів) та угоду про координати разом.
Незалежне тестування: Студент A може тестувати логіку зсуву/злиття, жорстко задаючи стани поля, викликаючи процедуру зсуву та виводячи результат через INT 21h. Студент B може рендерити жорстко задане поле з різними значеннями плиток для перевірки форматування та кольорів. <br><a class="internal-link" data-href="How-to/game-logic/Y05.md" href="projects/how-to/game-logic/y05.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: стиснення та злиття рядка, чотири напрямки через один алгоритм, визначення порожнього ходу, визначення кінця гри.
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Структура поля:</a> board DW 16 DUP(0). Клітинка (row, col) знаходиться за board[(row * 4 + col) * 2] (розмір слова). Або використовуйте DB і зберігайте показники степеня (0-11) в одному байті кожен.
<br><a class="internal-link" data-href="How-to/loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Алгоритм зсуву вліво:</a> для кожного рядка обробляйте стовпці зліва направо. Стиснення: зберіть усі ненульові плитки у тимчасовий масив, доповніть нулями. Злиття: скануйте стиснутий масив; якщо temp[i] == temp[i+1], подвоїть temp[i], встановіть temp[i+1] = 0, додайте temp[i] до рахунку. Стисніть знову. Запишіть назад у рядок.
Інші напрямки: зсув вправо = перевернути рядок, зсунути вліво, перевернути назад. Зсув вгору = трактувати кожен стовпець як рядок, зсунути вліво. Зсув вниз = трактувати кожен стовпець як рядок, зсунути вправо. Це зводить чотири напрямки до одного базового алгоритму.
<br><a class="internal-link" data-href="How-to/lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиця кольорів:</a> tile_colors DB 07h, 0Fh, 0Eh, 0Ch, 0Ch, 04h, 0Eh, 0Eh, 0Ch, 0Ch, 0Ah, 0Bh — індексація за показником (значення 2 = показник 1 = індекс 1).
<br><a class="internal-link" data-href="How-to/number-display.md" href="projects/how-to/number-display.html" target="_self" rel="noopener nofollow">Перетворення числа в рядок:</a> ділити значення на 10 повторно, записуючи залишки в стек, потім вилучати та додавати 30h ('0') для отримання цифр ASCII. Або використовуйте таблицю пошуку попередньо відформатованих рядків для 11 можливих значень.
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Рендеринг клітинки:</a> кожна клітинка плитки може бути 7 символів завширшки x 3 рядки заввишки. Верхній рядок: рамка. Середній рядок: центроване число. Нижній рядок: рамка. Поле займає 28 стовпців x 12 рядків, центроване на екрані.
<br><a class="internal-link" data-href="How-to/random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">Випадкове 2 проти 4:</a> згенеруйте випадковий байт. Якщо менше 230 (90% від 256), поставте 2; інакше поставте 4.
Точки синхронізації:
Кінець тижня 9: узгодити формат board DW 16 DUP(0), угоду про координати (row*4+col)*2, прототипи процедур (slide_left, draw_board, spawn_tile) та спільні змінні .DATA (score DW 0, game_phase DB 0)
Кінець тижня 11: перша інтеграція — main.asm викликає обидва модулі, ігровий цикл (ввід → зсув → spawn → перемалювання) працює наскрізно
Кінець тижня 12: повна гра працює з перевіркою перемоги, кінця гри та порожнього ходу; обидва студенти грають і шукають баги
Кінець тижня 14: звіт та відео завершені; взаємна перевірка коду перед здачею
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/y05-2048.html</link><guid isPermaLink="false">Projects/Y05-2048.md</guid><pubDate>Tue, 24 Feb 2026 16:02:24 GMT</pubDate><enclosure url="assets/y05-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y05-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[R03-Mini-Text-Editor]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🔴 Червоний
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 500-650 (соло) / 700-900 (пара), 3-6 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/file-io.md" href="projects/how-to/file-io.html" target="_self" rel="noopener nofollow">Файловий ввід/вивід через INT 21h</a> — відкриття, читання, запис та створення файлів за допомогою служб DOS (AH=3Dh, 3Fh, 40h, 3Ch, 3Eh) (L09/P09)
<br><a class="internal-link" data-href="How-to/dos-services.md" href="projects/how-to/dos-services.html" target="_self" rel="noopener nofollow">Розбір аргументів командного рядка</a> — зчитування імені файлу з PSP (Program Segment Prefix) за зміщенням 80h (L09/P09)
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять (B800h)</a> — відображення текстового вмісту, курсора та рядка статусу без накладних витрат BIOS (L08/P08)
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Керування динамічними структурами даних</a> — таблиця рядків або буфер з розривом як основне представлення тексту (L06/P06)
<br><a class="internal-link" data-href="How-to/segment-registers.md" href="projects/how-to/segment-registers.html" target="_self" rel="noopener nofollow">Маніпуляція сегментними регістрами (ES, DS)</a> — ES для відеопам'яті, DS для даних, потенційне використання сегментної арифметики для великих буферів (L03/P03, L06/P06)
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід (INT 16h)</a> — обробка як друкованих символів, так і спеціальних клавіш (стрілки, Home, End, PgUp, PgDn, Delete) через скан-коди (L07/P07)
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури та дисципліна стеку</a> — модульний дизайн: завантаження файлу, збереження файлу, вставка символу, видалення символу, розділення рядка, малювання екрана, малювання рядка статусу (L05/P05)
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Зсув масивів та керування індексами</a> — вставка та видалення в буферах рядків вимагає переміщення байтів (L06/P06)
<br><a class="internal-link" data-href="How-to/flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Прапорці та умовні переходи</a> — розгалуження для диспетчеризації клавіш, перевірки меж та відстеження зміненого файлу (L04/P04)
<br><a class="internal-link" data-href="How-to/game-logic/R03.md" href="projects/how-to/game-logic/r03.html" target="_self" rel="noopener nofollow">Ігрова логіка: текстовий редактор</a> — рядковий буфер, вставка/видалення зі зсувом, розділення та об'єднання рядків, прокручування viewport
Програма є мінімальним текстовим редактором — спрощеною версією nano або DOS EDIT — що працює в текстовому режимі 80x25 у DOSBox. При запуску з іменем файлу як аргументом командного рядка (або без аргументу для нового файлу) редактор відображає вміст файлу в області редагування 80x24, з рядком статусу на рядку 25 (рядок 24), що показує ім'я файлу, номери поточного рядка та стовпця, індикатор "modified" та комбінації клавіш. Весь дисплей відображається через прямий запис у відеопам'ять за адресою B800h — текст білим на чорному, рядок статусу в інвертованому відео (чорний на білому або чорний на блакитному).Користувач переміщується клавішами-стрілками. Курсор рухається посимвольно (ліворуч/праворуч) та порядково (вгору/вниз), з прокручуванням екрана по вертикалі, коли курсор виходить за межі видимих 24 рядків. Home та End переміщують на початок та кінець поточного рядка. Page Up та Page Down прокручують на повний екран (24 рядки). Введення друкованого символу вставляє його в позиції курсора, зсуваючи наступні символи рядка праворуч. Backspace видаляє символ перед курсором, підтягуючи символи ліворуч. Клавіша Delete видаляє символ у позиції курсора. Натискання Enter розділяє поточний рядок у позиції курсора, створюючи новий рядок. Ctrl+S зберігає файл на диск через файлові служби INT 21h. Ctrl+Q виходить, пропонуючи зберегти, якщо файл було змінено.Фундаментальна складність полягає в структурі даних текстового буфера. Наївні підходи — наприклад, зберігання всього файлу як плоского байтового масиву та зсув тисяч байтів при кожному натисканні клавіші — стають непрацездатно повільними зі зростанням файлу. Рекомендований підхід — рядковий буфер (масив вказівників або зміщень до окремих буферів рядків) або буфер з розривом (єдиний буфер із "розривом" на курсорі, що дешево поглинає вставки). Це архітектурне рішення є серцем проєкту та основною темою аналізу A-vs-B. Парна робота наполегливо рекомендується.<br><img alt="Редагування тексту" src="assets/r03-screen-editing.png" target="_self"><br><img alt="Діалог збереження" src="assets/r03-screen-savedialog.png" target="_self">
Примітка: Файловий ввід/вивід (файлові служби INT 21h) розглядається в L09/P09 (тиждень 9). Завантаження/збереження файлів є обов'язковою частиною мінімальних вимог цього проєкту, але студенти, які починають на тижнях 7-8, повинні спочатку створити редактор у пам'яті — переміщення курсора, вставка/видалення, розділення/об'єднання рядків із жорстко закодованим текстом у сегменті даних. Додайте завантаження та збереження файлів, коли матеріал P09 стане доступним. Підхід A: Зберігає весь файл як один безперервний байтовий масив. Вставка символу в середину вимагає зсуву всіх наступних байтів на один — операція O(n), де n — розмір файлу.
Підхід B: Зберігає масив дескрипторів рядків (кожен містить вказівник на буфер рядка фіксованого розміру та поточну довжину рядка). Вставка символу зсуває байти лише в межах поточного рядка — O(m), де m — довжина рядка (макс. ~80).
Що виміряти/спостерегти: Час вставки для файлу з 50 рядками (виміряйте підрахунком інструкцій у Turbo Debugger або хронометражем за тактами INT 1Ah), накладні витрати пам'яті (A не має накладних витрат; B використовує ~4 байти на рядок для таблиці дескрипторів) та складність клавіші Enter (A повинен зсунути весь хвіст буфера; B лише потрібно розділити один рядок та вставити новий запис у таблицю дескрипторів). Це визначальне архітектурне рішення проєкту. Підхід A: Перемальовує всі 24 видимі рядки при кожному натисканні клавіші.
Підхід B: Відстежує, який рядок було змінено, і перемальовує лише цей єдиний рядок (або максимум уражені рядки під час прокручування).
Що виміряти/спостерегти: Кількість записів у відеопам'ять за натискання клавіші (A = ~1920 записів символів; B = ~80), сприйнята швидкість відгуку (на практиці обидва швидкі в DOSBox, але вимірювання різниці є повчальним) та складність коду (B вимагає відстеження діапазону "брудних" рядків). Підрахуйте кількість інструкцій MOV до ES:[DI] за натискання клавіші в кожному підході. Підхід A: Обробляє кожну клавішу послідовністю порівнянь CMP AL, key / JE handler — просто і читабельно, але O(k) для k прив'язок клавіш.
Підхід B: Будує 256-байтову (або розріджену) таблицю переходів, що зіставляє скан-коди з адресами обробників, виконуючи диспетчеризацію за O(1) одним індексованим переходом.
Що виміряти/спостерегти: Довжина коду, інструкції, виконані за натискання клавіші, та легкість додавання нових прив'язок клавіш. Виміряйте кількість інструкцій CMP, виконаних для останньої клавіші в ланцюжку (підхід A) порівняно з постійною вартістю табличного пошуку (підхід B).
<br>Цей проєкт наполегливо рекомендується для пар. Підсистема керування буфером та підсистема відображення/вводу є природно незалежними модулями з чіткою межею інтерфейсу (див. <a class="internal-link" data-href="How-to/include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">Підключення файлів: INCLUDE</a>).
Студент A: Двигун буфера — структура даних текстового буфера (таблиця рядків або буфер з розривом), вставка символу, видалення символу, розділення рядка (Enter), об'єднання рядків (Backspace на початку рядка, Delete на кінці рядка), завантаження файлу (зчитування файлу в буфер), збереження файлу (запис буфера у файл), розбір аргументів командного рядка.
Студент B: Інтерфейс редактора — відображення у відеопам'яті (малювання видимих рядків, рядок статусу), керування курсором (відстеження позиції, прокручування viewport), обробник клавіатурного вводу (диспетчеризація до операцій буфера), керування прапорцем зміни, діалог збереження, очищення та ініціалізація екрана.
Точки інтеграції: Чітко визначений API буфера. Студент A надає процедури на кшталт buf_insert_char(line, col, char), buf_delete_char(line, col), buf_split_line(line, col), buf_get_line(line_num) -&gt; pointer, length, buf_line_count() -&gt; count. Студент B викликає ці процедури і ніколи безпосередньо не звертається до внутрішньої структури даних буфера. Визначте цей API на КТ-2.
Незалежне тестування: Студент A може тестувати двигун буфера, викликаючи вставку/видалення/розділення та виводячи вміст буфера через INT 21h. Студент B може тестувати інтерфейс з жорстко закодованим масивом рядків, перевіряючи прокручування та переміщення курсора до готовності реального буфера. <br><a class="internal-link" data-href="How-to/game-logic/R03.md" href="projects/how-to/game-logic/r03.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: структура рядкового буфера, вставка/видалення символів зі зсувом, розділення та об'єднання рядків, прокручування viewport.
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Таблиця дескрипторів рядків:</a> Визначте MAX_LINES EQU 100 та MAX_LINE_LEN EQU 128. Виділіть line_buf DB MAX_LINES * MAX_LINE_LEN DUP(0) та line_len DW MAX_LINES DUP(0). Рядок N починається з line_buf + N * MAX_LINE_LEN і має довжину line_len[N*2]. Це уникає вказівників повністю — лише обчислені зміщення.
Вставка символу в позицію (line, col): Обчисліть адресу символу за (line, col). Використовуйте REP MOVSB (або ручний цикл з STD для зворотного копіювання) для зсуву всіх символів від col до кінця рядка праворуч на один байт. Запишіть новий символ у col. Збільшіть довжину рядка. Перевірте відносно MAX_LINE_LEN перед вставкою.
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Читання клавіш INT 16h:</a> MOV AH, 00h / INT 16h повертає: AL = ASCII-код (ненульовий для друкованих символів, 0Dh для Enter, 08h для Backspace), AH = скан-код. Для спеціальних клавіш (стрілки, Home, End, PgUp, PgDn, Delete) AL = 00h або E0h, а скан-код в AH ідентифікує клавішу: 48h=Вгору, 50h=Вниз, 4Bh=Ліворуч, 4Dh=Праворуч, 47h=Home, 4Fh=End, 49h=PgUp, 51h=PgDn, 53h=Delete. Ctrl+S: AL=13h. Ctrl+Q: AL=11h.
<br><a class="internal-link" data-href="How-to/file-io.md" href="projects/how-to/file-io.html" target="_self" rel="noopener nofollow">Файловий ввід/вивід (доступний з тижня 9):</a> Відкриття: MOV AH, 3Dh / MOV AL, 02h / LEA DX, filename / INT 21h (дескриптор в AX). Читання: MOV AH, 3Fh / MOV BX, handle / MOV CX, bytes / LEA DX, buffer / INT 21h. Запис: MOV AH, 40h. Закриття: MOV AH, 3Eh. Створення: MOV AH, 3Ch / MOV CX, 0 / LEA DX, filename / INT 21h.
Розбір завантаженого файлу на рядки: Зчитайте весь файл у тимчасовий буфер. Скануйте на 0Dh 0Ah (CR+LF, закінчення рядка DOS) або 0Ah (LF). Кожен сегмент між закінченнями рядків стає одним рядком у таблиці рядків. Скопіюйте кожен сегмент у його буфер рядка та запишіть його довжину.
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Відеопам'ять</a> для рядка R, стовпця C: offset = (R * 80 + C) * 2. Символ за зміщенням, атрибут за зміщенням+1. Рядок статусу на рядку 24: використовуйте контрастний атрибут, наприклад 70h (чорний на білому).
Відображення курсора: Використовуйте BIOS INT 10h AH=02h для позиціонування апаратного курсора (блимаючого підкреслення) в поточній позиції екрана. Це простіше, ніж намагатися відобразити власний курсор через відеопам'ять.
Розташування PSP: У COM-файлі PSP починається зі зміщення 0000h сегмента коду. Хвіст команди знаходиться зі зміщення 80h (байт довжини) та 81h (рядок хвоста). У EXE-файлі DS та ES вказують на PSP при вході; збережіть їх перед встановленням DS на ваш сегмент даних.
Точки синхронізації:
Кінець тижня 9: узгодити API буфера (прототипи buf_insert_char, buf_delete_char, buf_split_line, buf_join_lines, buf_get_line, buf_line_count), структуру .DATA сегмента та include-файл зі спільними визначеннями
Кінець тижня 11: перша повна інтеграція — текст відображається на екрані, символи вставляються/видаляються, Enter розділяє рядки, курсор навігує з прокручуванням
Кінець тижня 12: файловий ввід/вивід працює — файл завантажується з диска, редагується та зберігається назад; обидва партнери тестують повний цикл разом
Кінець тижня 13: програма повністю працює від запуску до виходу; Ctrl+S/Ctrl+Q працюють коректно, включаючи діалог збереження
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/r03-mini-text-editor.html</link><guid isPermaLink="false">Projects/R03-Mini-Text-Editor.md</guid><pubDate>Tue, 24 Feb 2026 16:02:23 GMT</pubDate><enclosure url="assets/r03-screen-editing.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/r03-screen-editing.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y08-Battleship]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🟡 Жовтий
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 450-600 (соло) / 650-850 (пара), 2-5 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Пряме записування у відеопам'ять</a> (B800h) — малювання та оновлення двох сіток без мерехтіння — L08/P08
<br><a class="internal-link" data-href="How-to/segment-registers.md" href="projects/how-to/segment-registers.html" target="_self" rel="noopener nofollow">Сегментні регістри</a> (ES) — спрямування ES на B800h для відео, DS на сегмент даних — L06/P08
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід через INT 16h</a> — небуферизована обробка клавіш-стрілок та Enter під час розміщення та стрільби — L07/P07
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури та кадри стеку</a> — модульні підпрограми: draw_grid, place_ship, check_hit, ai_turn — L05/P05
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви</a> та <a class="internal-link" data-href="How-to/addressing-modes.md" href="projects/how-to/addressing-modes.html" target="_self" rel="noopener nofollow">режими адресації</a> — два байтових масиви 10x10 (дошка гравця, дошка ворога), індексовані через [BX+SI] або обчислені зміщення — L06/P06
<br><a class="internal-link" data-href="How-to/bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Побітові операції</a> — упакування стану клітинки (порожня/корабель/влучення/промах/потоплений) у ніббли або бітові прапорці — L04/P05
<br><a class="internal-link" data-href="How-to/random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">Генерація псевдовипадкових чисел</a> — ЛКГ або RNG з ініціалізацією тактами таймера для розміщення та стрільби ШІ — L07/P07
<br><a class="internal-link" data-href="How-to/dos-services.md" href="projects/how-to/dos-services.html" target="_self" rel="noopener nofollow">Сервіси DOS</a> (INT 21h) — вихід з програми, опціональний рядковий вивід — L07/P07
<br><a class="internal-link" data-href="How-to/flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Умовна логіка</a> та <a class="internal-link" data-href="How-to/loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">цикли</a> — цикли валідації для розміщення, цикли сканування для виявлення потоплення — L04/P04
<br><a class="internal-link" data-href="How-to/game-logic/Y08.md" href="projects/how-to/game-logic/y08.html" target="_self" rel="noopener nofollow">Ігрова логіка: морський бій</a> — валідація розміщення кораблів, обробка влучень/промахів/потоплень, ШІ «полювання/ціль»
Програма запускається у DOSBox і представляє повноекранне поле бою у текстовому режимі 80x25. Дві сітки 10x10 намальовані поруч: ліва сітка — це карта власного флоту гравця, а права — цільова (ворожа) карта. Кольорова рамка розділяє їх, а рядок стану внизу показує поточний хід, кількість влучень/промахів та інструкції.Під час фази розміщення гравець позиціонує п'ять кораблів (розміри 2, 3, 3, 4, 5) по одному. Клавіші-стрілки переміщують підсвічений контур корабля по сітці гравця; клавіша (наприклад, R) перемикає між горизонтальною та вертикальною орієнтацією. Enter підтверджує розміщення. Недійсні позиції (за межами сітки, накладання на інший корабель) відхиляються кольоровим спалахом або звуковим сигналом. Після того як гравець закінчить, ШІ мовчки розміщує свій флот випадковим чином, дотримуючись тих самих правил заборони накладання.Потім гра чергує ходи. Гравець переміщує курсор по цільовій сітці та натискає Enter для пострілу. Влучення підсвічується червоним, промах — сіро-синім, а коли всі клітинки корабля уражені, слово "SUNK" коротко блимає. ШІ відповідає — у базовій версії він обирає випадкову клітинку, по якій ще не стріляли. Після того як одна сторона втрачає всі кораблі, банер перемоги/поразки заповнює центр екрану, і гравець може натиснути клавішу для виходу.<br><img alt="Розміщення кораблів" src="assets/y08-screen-placement.png" target="_self"><br><img alt="Ігровий процес" src="assets/y08-screen-gameplay.png" target="_self"><br><img alt="Кінець гри" src="assets/y08-screen-gameover.png" target="_self">
Підхід A: Кожна клітинка — один байт з бітовими полями (біт 0 = є корабель, біт 1 = стріляли, біти 2-3 = ID корабля). Один масив 100 байт на дошку.
Підхід B: Окремі булеві масиви — ship_present[100], fired[100], ship_id[100]. Три масиви на дошку, загалом 300 байт.
Що вимірювати/спостерігати: Кількість інструкцій для операції "перевірити та оновити клітинку", різниця у розмірі коду та зручність налагодження у Turbo Debugger. Архітектурно цікаво, оскільки відпрацьовує маскування AND/OR/SHL (A) проти прості порівняння байтів (B). Підхід A: Після кожної дії очистити та перемалювати обидві сітки повністю (200 клітинок x 2 байти кожна = 800 записаних байтів).
Підхід B: Відстежувати, які клітинки змінились, та записати лише ці 2 байти у відеопам'ять.
Що вимірювати/спостерігати: Підрахувати кількість записів у відеопам'ять за хід. Спостерігати, чи повне перемалювання спричиняє видиме мерехтіння у DOSBox. Обговорити, чому це важливіше на реальному апаратному забезпеченні CGA з артефактами "снігу". Підхід A: Обрати випадкову позицію та орієнтацію, перевірити валідність, повторити, якщо недійсна. Простий код, але потенційно багато повторів для останнього корабля.
Підхід B: Побудувати список всіх валідних розміщень, обрати одне випадково, видалити конфліктні слоти. Складніше, але гарантовано за один прохід.
Що вимірювати/спостерігати: Середня кількість повторів для підходу A (інструментувати лічильником). Порівняти складність коду (рядки, процедури).
<br>Розподіл модулів (див. <a class="internal-link" data-href="How-to/include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">Підключення файлів: INCLUDE</a>):
Студент A — Модуль відображення та інтерфейсу: малювання сітки, логіка атрибутів кольору, рух курсору, інтерфейс фази розміщення, рендеринг рядка стану. Файли: display.asm, input.asm.
Студент B — Модуль ігрової логіки та ШІ: структури даних дошки, логіка влучення/промаху/потоплення, алгоритм розміщення ШІ, логіка стрільби ШІ, перевірка умови перемоги. Файли: game.asm, ai.asm.
Спільне: main.asm з ігровим циклом, визначеннями сегменту даних, загальними константами (EQU для позицій сітки, кольорів). Узгодьте угоди про виклик (які регістри є аргументами, які зберігаються).
Точки інтеграції: Студент B надає check_hit(row, col) -&gt; result та ai_fire() -&gt; (row, col, result); Студент A викликає їх з ігрового циклу та оновлює відображення. <br><a class="internal-link" data-href="How-to/game-logic/Y08.md" href="projects/how-to/game-logic/y08.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: валідація розміщення кораблів, обробка пострілу (влучення/промах/потоплення), ШІ «полювання/ціль».
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Формула зміщення у відеопам'яті:</a> для клітинки в рядку r сітки, стовпці c сітки, з верхнім лівим кутом сітки на рядку екрану sr, стовпці екрану sc: offset = ((sr + r) * 80 + (sc + c)) * 2. Використовуйте LEA та SHL для ефективного обчислення.
Атрибути кольору: використовуйте байт 4Fh (білий на червоному) для влучень, 1Fh (білий на синьому) для промахів, 2Fh (білий на зеленому) для неушкоджених кораблів на дошці гравця, 07h (нормальний) для порожньої води.
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Виявлення клавіш-стрілок:</a> INT 16h/AH=00h повертає скан-код в AH. Розширені клавіші (стрілки) повертають 00h в AL зі скан-кодами: Up=48h, Down=50h, Left=4Bh, Right=4Dh.
<br><a class="internal-link" data-href="How-to/random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">Випадкове число:</a> зчитайте 32-бітний лічильник тактів за адресою 0040:006C (використовуйте ES=0040h), використовуйте молодше слово як початкове значення. Простий ЛКГ: next = seed * 25173 + 13849. Візьміть mod 10 для рядка/стовпця, mod 2 для орієнтації.
<br><a class="internal-link" data-href="How-to/lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиця дескрипторів кораблів:</a> ship_table DB 2,3,3,4,5 для довжин. Під час розміщення побудуйте паралельний масив (початковийрядок, початковийстовпець, орієнтація, залишок_влучень) для кожного корабля.
Перевірка потоплення: після фіксації влучення по board[row][col] отримайте ID корабля з цієї клітинки, проскануйте дошку для всіх клітинок з цим ID корабля, перевірте, чи всі позначені як уражені.
Точки синхронізації:
Кінець тижня 8: узгодити формат стану клітинки (біти чи окремі масиви), угоди про виклик (які регістри — аргументи, які зберігаються), константи EQU для кольорів та позицій сіток
Кінець тижня 10: Студент B надає робочу place_ship зі стабільним інтерфейсом (row, col, length, orient) -&gt; success/fail, щоб Студент A міг інтегрувати фазу розміщення
Кінець тижня 11: перша наскрізна інтеграція — main.asm викликає check_hit та ai_fire, Студент A оновлює відображення за результатами. Обидва перевіряють повний цикл гри
Кінець тижня 13: код зафіксований для фінального тестування; чернетка звіту з аналізом «Підхід A проти B» готова для взаємного рев'ю
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/y08-battleship.html</link><guid isPermaLink="false">Projects/Y08-Battleship.md</guid><pubDate>Tue, 24 Feb 2026 16:02:23 GMT</pubDate><enclosure url="assets/y08-screen-placement.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y08-screen-placement.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y19-Address-Book]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🟡 Жовтий
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 500–650 (соло) / 700–900 (пара), 2–5 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви записів фіксованого розміру</a> (обчислення зміщень у записах по 96 байтів за допомогою MUL та ADD) — L06/P06
<br>Усі <a class="internal-link" data-href="How-to/addressing-modes.md" href="projects/how-to/addressing-modes.html" target="_self" rel="noopener nofollow">режими адресації</a> 8086 (база+індекс+зміщення для доступу до полів усередині записів) — L06/P06
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять</a> (ES=B800h, пари символ+атрибут, кольорово розмежовані області UI) — L08/P08
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід через INT 16h</a> (клавіші-стрілки для навігації, літерні клавіші для команд) — L07/P07
<br><a class="internal-link" data-href="How-to/file-io.md" href="projects/how-to/file-io.html" target="_self" rel="noopener nofollow">Файловий ввід/вивід DOS</a> через INT 21h (AH=3Ch створення, 3Dh відкриття, 3Fh читання, 40h запис, 3Eh закриття) — L09/P09
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури зі стековими фреймами</a> (окремі процедури для додавання, редагування, видалення, пошуку, сортування, малювання) — L05/P05
<br><a class="internal-link" data-href="How-to/bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Побітові операції</a> (OR з 20h для порівняння без урахування регістру під час пошуку підрядка) — L04/P04
<br><a class="internal-link" data-href="How-to/flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Умовна логіка та прапорці</a> (CMP + умовні переходи для диспетчеризації меню, зіставлення пошуку, порівнянь сортування) — L04/P04
Блочні операції з пам'яттю (REP MOVSB для копіювання/зсуву записів під час вставки, видалення, обмінів сортування) — L06/P06
<br><a class="internal-link" data-href="How-to/segment-registers.md" href="projects/how-to/segment-registers.html" target="_self" rel="noopener nofollow">Сегментні регістри</a> (DS для сегмента даних, ES перемикається між B800h для відображення та DS для блочних копій) — L03/P03
<br><a class="internal-link" data-href="How-to/game-logic/Y19.md" href="projects/how-to/game-logic/y19.html" target="_self" rel="noopener nofollow">Ігрова логіка: адресна книга</a> — CRUD на записах фіксованого розміру, сортування вставками, пошук підрядка
<br>Див. також <a class="internal-link" data-href="Y20-Password-Manager.md" href="projects/y20-password-manager.html" target="_self" rel="noopener nofollow">Y20 Менеджер паролів</a> — подібний шаблон CRUD + файлове збереження, але додає XOR-шифрування та автентифікацію майстер-паролем. Адресна книга фокусується на сортуванні та пошуку.Програма представляє інтерактивний додаток адресної книги в текстовому режимі DOSBox. При запуску екран розділяється на дві області: панель списку зліва (приблизно 50 стовпців шириною), що показує імена контактів у прокручуваному списку, та панель деталей справа, що показує повний запис для поточного виділеного контакту — ім'я, номер телефону та коротку нотатку. Смуга виділення (рядок із інвертованими кольорами) переміщується вгору і вниз клавішами-стрілками. Увесь дисплей рендериться через прямий запис у відеопам'ять за адресою B800h з кольорово розмежованими секціями: синя панель заголовка в рядку 0, білий на темно-синьому для записів списку, яскраво-бірюзовий для обраного запису та панель деталей із зеленим відтінком.Внизу екрану рядок стану показує доступні команди: Add, Edit, Delete, Search, Order (сортування), Write (зберегти), Load, Quit. Натискання A відкриває просту форму-накладку, де користувач по черзі вводить поля імені, телефону та нотатки, кожне поле показане як підписаний рядок вводу. Натискання E на виділеному контакті відкриває ту саму форму, попередньо заповнену поточними значеннями для редагування на місці. Клавіша D видаляє обраний контакт після запиту підтвердження Y/N. Клавіша S запитує рядок пошуку і фільтрує список, показуючи лише контакти, чиї імена містять підрядок (зіставлення без урахування регістру через побітове OR для приведення до нижнього регістру).Контакти зберігаються як записи фіксованого розміру в масиві пам'яті (96 байтів кожен: 32 для імені, 16 для телефону, 48 для нотатки). Функція сортування (викликається по O) впорядковує масив за алфавітом за іменем, використовуючи сортування вставками — зручний алгоритм, що потребує обміну блоків пам'яті фіксованого розміру. Файлове збереження використовує INT 21h для запису всього масиву записів у файл .DAT і повторного завантаження при наступному запуску, тому адресна книга зберігається між сесіями.<br><img alt="Список контактів із панеллю деталей" src="assets/y19-screen-list.png" target="_self"><br><img alt="Форма додавання нового контакту" src="assets/y19-screen-form.png" target="_self">
Примітка: Файловий ввід/вивід (файлові сервіси INT 21h) розглядається у L09/P09 (тиждень 9). Протягом тижнів 7-8 зосередьтеся на основній логіці з жорстко заданими тестовими даними. Реалізуйте файлові операції, коли матеріал P09 стане доступним. Підхід A: Обчислює зміщення для запису N як N * 96 за допомогою MUL (або послідовності зсувів та додавань, оскільки 96 = 64 + 32).
Підхід B: Підтримує окремий масив слів із 50 вказівників (зміщень) на кожен запис, тому доступ до запису N — просте табличне звернення: завантажити N-те слово з таблиці вказівників у SI.
Що вимірювати/спостерігати: Кількість інструкцій для доступу до запису (MUL повільний на 8086, але не потребує додаткової пам'яті), накладні витрати на підтримку таблиці вказівників при вставках та видаленнях (переіндексація) та який підхід спрощує сортування (обмін 2-байтових вказівників проти обмін 96-байтових записів). Підхід A: Виконує сортування вставками, фізично переміщуючи 96-байтові записи в пам'яті за допомогою REP MOVSB.
Підхід B: Сортує масив індексів (50 слів), залишаючи записи на місці — кожен обмін переміщує 2 байти замість 96.
Що вимірювати/спостерігати: Загальна кількість переміщених байтів під час сортування 30 контактів (виміряйте в Turbo Debugger, підрахувавши ітерації REP), складність коду для процедури відображення (прямий обхід масиву проти непрямий пошук через таблицю індексів) та компроміс між швидкістю сортування та простотою відображення. Підхід A: Перевіряє кожне ім'я контакту посимвольно проти запиту (вкладений цикл).
Підхід B: Будує таблицю з 26 записів, що зіставляє кожну початкову літеру з першим записом, що починається з цієї літери, тому пошук перевіряє лише записи, що починаються з тієї ж літери, що й запит.
Що вимірювати/спостерігати: Кількість виконаних інструкцій CMP при пошуку серед 40 контактів із 3-символьним запитом, розмір коду кожного підходу та чи витрати на підтримку індексної таблиці (оновлення після додавання/видалення/сортування) переважують виграш від пошуку.
<br>Розподіл модулів (див. <a class="internal-link" data-href="How-to/include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">Підключення файлів: INCLUDE</a>):
Студент A — Модуль даних та логіки: керування масивом записів, операції додавання/редагування/видалення, алгоритм пошуку, алгоритм сортування, збереження/завантаження файлів. Файл: data.asm.
Студент B — Модуль відображення та вводу: рендеринг компонування екрану (панель списку, панель деталей, панель заголовка, рядок стану), навігація смугою виділення, форма-накладка для вводу, диспетчеризація клавішних команд. Файл: display.asm.
Спільний головний модуль: ініціалізація, головний цикл подій, виклики процедур даних та відображення. Файл: main.asm.
Точки інтеграції:
Узгодити формат запису (96 байтів, зміщення полів 0/32/48) та спільні змінні: contacts (байтовий масив), count (слово, кількість контактів), selected (слово, індекс виділеного контакту).
Визначити прототипи процедур до контрольної точки KT-2: add_contact, delete_contact, sort_contacts, search_contacts (від Студента A); draw_list, draw_detail, draw_form, get_input_string (від Студента B).
Головний цикл викликає обробник вводу Студента B, який повертає код команди; головний цикл потім передає керування процедурам даних Студента A та викликає перемальовування Студента B. <br><a class="internal-link" data-href="How-to/game-logic/Y19.md" href="projects/how-to/game-logic/y19.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: CRUD-операції над записами, сортування вставками, пошук підрядка без урахування регістру.
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Компонування запису:</a> Визначте суцільний байтовий масив: contacts DB 50 DUP(96 DUP(0)). Доступ до запису N за зміщенням contacts + N * 96. Усередині запису ім'я починається з байту 0, телефон — з байту 32, нотатка — з байту 48. Заповнюйте невикористані байти нулями. Використовуйте LEA SI, [contacts] та додайте обчислене зміщення.
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Компонування екрану:</a> Зарезервуйте рядок 0 для панелі заголовка, рядки 1-22 для списку (ліва половина) та деталей (права половина), рядки 23-24 для рядка стану. Панель списку може показувати 22 контакти одночасно. Підтримуйте змінну scroll_top: відображайте контакти від індексу scroll_top до scroll_top + 21.
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Виявлення клавіш-стрілок:</a> INT 16h AH=00h повертає AL=0 для розширених клавіш, зі скан-кодом в AH. Стрілка вгору: AH=48h. Стрілка вниз: AH=50h. Звичайні літерні клавіші повертають свій ASCII в AL.
<br><a class="internal-link" data-href="How-to/bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Порівняння без урахування регістру:</a> Для порівняння двох символів без урахування регістру виконайте OR кожного з 20h перед порівнянням. Це працює для ASCII-літер, оскільки 'A' (41h) OR 20h = 'a' (61h), і 'a' OR 20h = 'a'.
<br>Сортування вставками на записах: Зовнішній <a class="internal-link" data-href="How-to/loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">цикл</a> від i=1 до count-1. Скопіюйте record[i] у тимчасовий буфер (96 байтів). Внутрішній цикл: поки j &gt; 0 та record[j-1].name &gt; temp.name, скопіюйте record[j-1] у record[j]. Нарешті, скопіюйте temp у record[j]. Порівняння рядків: цикл по байтах імені з CMPSB або ручний цикл CMP по байтах.
<br><a class="internal-link" data-href="How-to/file-io.md" href="projects/how-to/file-io.html" target="_self" rel="noopener nofollow">Формат файлу:</a> Зберігайте count як перші 2 байти (слово), за ними count 96 байтів даних записів. При завантаженні зчитайте перші 2 байти для отримання count, потім зчитайте count 96 байтів у масив contacts. Ім'я файлу можна задати жорстко: db 'ADDRBOOK.DAT', 0.
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Рядковий ввід:</a> Напишіть <a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">процедуру</a> get_string, яка зчитує символи по одному через INT 16h, відображає їх на конкретну позицію екрану через <a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">B800h</a>, обробляє Backspace (зменшити індекс буфера, перезаписати пробілом) та повертає по Enter. Приймайте параметр максимальної довжини для запобігання переповненню буфера.
Точки синхронізації:
Кінець тижня 8: узгодити формат запису (96 байтів, зміщення полів), спільні змінні (contacts, count, selected, scroll_top, display_indices) та прототипи процедур
Кінець тижня 10: Студент A передає працюючі add_contact та delete_contact; Студент B має draw_list зі смугою виділення — перша наскрізна інтеграція: додати контакт -&gt; побачити у списку
Кінець тижня 12: повна інтеграція всіх CRUD-операцій + пошук + сортування + файлове збереження/завантаження; обидва тестують разом із заповненим списком контактів
Кінець тижня 14: код зафіксований, звіт та відео готові
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/y19-address-book.html</link><guid isPermaLink="false">Projects/Y19-Address-Book.md</guid><pubDate>Tue, 24 Feb 2026 16:02:22 GMT</pubDate><enclosure url="assets/y19-screen-list.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y19-screen-list.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y13-Wolfram-Cellular-Automaton]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🟡 Жовтий
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 400-550 (соло) / 600-800 (пара), 2-4 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Побітові операції</a> (AND, OR, SHL, SHR, TEST) — вся оцінка правила є бітовою маніпуляцією: виділення 3-бітового сусідства, використання його як індексу для перевірки конкретного біту байта правила — L04/P04
<br><a class="internal-link" data-href="How-to/bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Бітове представлення даних</a> — правило є 8-бітовим числом, де кожен біт має семантичне значення; масив клітин природно є бітовим масивом (1 біт на клітину) — L04/P04
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять</a> (ES=B800h, відображення клітин як кольорових блокових символів) — L08/P08
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Обробка масивів</a> (масив покоління: 80 байтів, по одному на клітину, ітерація зліва направо кожне покоління) — L06/P06
<br><a class="internal-link" data-href="How-to/loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Цикли та ітерація</a> (зовнішній цикл по поколіннях/рядках, внутрішній цикл по 80 стовпцях, вкладене виділення бітів) — L04/P04
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід через INT 16h</a> (командні клавіші, числовий ввід номера правила) — L07/P07
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури</a> (compute_generation, display_row, display_rule_table як модульні процедури) — L05/P05
<br><a class="internal-link" data-href="How-to/number-display.md" href="projects/how-to/number-display.html" target="_self" rel="noopener nofollow">Числовий ввід/вивід</a> (читання десяткового номера правила 0-255 з клавіатури, відображення його у десятковому та двійковому вигляді) — L04/P04
<br><a class="internal-link" data-href="How-to/random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">Генерація псевдовипадкових чисел</a> (системний годинник INT 1Ah як початкове значення для випадкового початкового стану) — L07/P07
<br><a class="internal-link" data-href="How-to/segment-registers.md" href="projects/how-to/segment-registers.html" target="_self" rel="noopener nofollow">Сегментні регістри</a> (ES для відеопам'яті) — L03/P03
<br><a class="internal-link" data-href="How-to/game-logic/Y13.md" href="projects/how-to/game-logic/y13.html" target="_self" rel="noopener nofollow">Ігрова логіка: клітинний автомат Вольфрама</a> — бітове індексування правила за сусідством, обробка меж, обчислення покоління
<br>Див. також <a class="internal-link" data-href="Y02-Conways-Game-of-Life.md" href="projects/y02-conways-game-of-life.html" target="_self" rel="noopener nofollow">Y02 Гра «Життя» Конвея</a> — також клітинний автомат, але двовимірний з набором правил B3/S23 та інтерактивним редактором. Вольфрам зосереджується на бітовому кодуванні правил та еволюції одновимірних патернів.Програма досліджує елементарні клітинні автомати Вольфрама — сімейство з 256 одновимірних правил, що генерують напрочуд складні патерни з тривіально простої логіки. Екран у текстовому режимі DOSBox показує еволюцію автомату: рядок 0 зверху містить початкове покоління (одна увімкнена клітина в центрі або випадковий патерн), а кожен наступний рядок показує наступне покоління, обчислене за вибраним правилом. Патерн зростає вниз, заповнюючи 24 рядки 80-стовпцевого екрану, створюючи візуальний вивід від однорідного та нецікавого (правило 0) до самоподібних фракталів (правило 90 — трикутник Серпінського) до хаотичних псевдовипадкових полів (правило 30).Ключове розуміння — і те, що робить цей проєкт архітектурно цікавим — полягає в тому, що кожне з 256 правил кодується одним 8-бітовим числом. 3-клітинне сусідство (ліва, центральна, права) утворює 3-бітовий індекс (0-7), і відповідний біт у номері правила визначає вихідну клітину. Наприклад, правило 110 = 01101110 у двійковому: сусідство 111 дає 0, 110 дає 1, 101 дає 1 і так далі. Вся оцінка правила зводиться до: зсув за індексом сусідства, AND з байтом правила, перевірка біту. Це безпосередньо та красиво відображається на побітові операції, які студенти вивчили — AND, SHR, TEST — що робить абстрактну концепцію правил клітинних автоматів відчутною на рівні машинних інструкцій.Для відображення використовуються блокові символи та кольорові атрибути для рендерингу «живих» та «мертвих» клітин як суцільних та порожніх блоків, створюючи щільні візуальні патерни. Рядок стану в нижньому рядку показує номер поточного правила (у десятковому та двійковому вигляді), лічильник поколінь та команди: ввести новий номер правила (0-255), натиснути R для перезапуску з одного зерна, натиснути A для випадкового початкового стану, натиснути пробіл для кроку на одне покоління, натиснути Enter для безперервного виконання та натиснути Q для виходу. Режим аналізу (перемикається клавішею T) замінює рядок стану візуальною таблицею правила, що показує всі 8 відображень сусідство-&gt;вихід як рядок з 8 малих діаграм, допомагаючи студенту зрозуміти, що саме робить правило.<br><img alt="Еволюція правила 110 з таблицею правила та рядком стану" src="assets/y13-screen-evolution.png" target="_self">
Підхід A: Оцінює правило динамічно: формує 3-бітовий індекс сусідства, завантажує байт правила у регістр, зсуває вправо на величину індексу (використовуючи CL), потім TEST з 1 для перевірки вихідного біту. Це потребує зсуву зі змінним лічильником (MOV CL, index / SHR rule, CL).
Підхід B: Попередньо обчислює 8-байтову таблицю пошуку при виборі правила: rule_table[i] = (rule &gt;&gt; i) AND 1 для i=0..7. Оцінка стає простим пошуком у таблиці: MOV AL, [rule_table + neighborhood].
Що виміряти/спостерігати: Кількість інструкцій на оцінку клітини (підхід A: ~5 інструкцій включно зі змінним зсувом; підхід B: ~2 інструкції, але потребує 8 байтів підготовки таблиці), загальний час обчислення 24 поколінь 80 клітин (виміряти через INT 1Ah) та який підхід простіше пояснити у звіті. Підхід A: Використовує 80 байтів на покоління (один байт на клітину, значення 0 або 1). Виділення сусідства: завантажити три послідовних байти та об'єднати.
Підхід B: Використовує 10 байтів на покоління (80 бітів, упакованих у 10 байтів). Виділення сусідства вимагає адресації на рівні бітів: індекс байту = col / 8, індекс біту = col MOD 8, і сусідство може перетинати два байти.
Що виміряти/спостерігати: Пам'ять на покоління (80 байтів проти 10 байтів, суттєво при зберіганні буфера історії), складність коду для виділення сусідства (підхід A: 3 MOV; підхід B: кілька операцій SHR/AND/OR з обробкою межі байтів) та яке представлення краще моделює те, як реальне обладнання зберігає прапорці та бітові карти. Підхід A: Коли на екрані заповнено 24 рядки і потрібно показати нове покоління, фізично прокручує відеопам'ять: використовує REP MOVSW для копіювання рядків 1-23 на рядки 0-22, потім малює нове покоління на рядку 23. Це переміщує 23 80 2 = 3680 байтів кожне покоління.
Підхід B: Підтримує вказівник top_row, що інкрементується по модулю 24; нове покоління перезаписує рядок на поточному top_row, і візуальний ефект прокрутки створюється обгортанням. Однак відеопам'ять нативно не підтримує віртуальний початок у текстовому режимі, тому підхід B вимагав би або підтримки окремого буфера з копіюванням усього екрану кожен кадр, або прийняття стрибаючого відображення.
Що виміряти/спостерігати: Візуальна плавність, кількість інструкцій на операцію прокрутки та чи достатньо швидким є грубе копіювання підходу A у DOSBox на нормальній швидкості (виміряти час на прокрутку через INT 1Ah).
<br>Розподіл модулів (див. <a class="internal-link" data-href="How-to/include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">Підключення файлів: INCLUDE</a>):
Студент A — Модуль рушія: процедура оцінки правила, обчислення покоління (ітерація по 80 стовпцях, формування сусідств, застосування правила), керування масивом поколінь (буфери поточного та наступного покоління), обробка меж (обгортання або мертві), генерація випадкового початкового стану. Файл: engine.asm.
Студент B — Модуль відображення та UI: рендеринг рядка у відеопам'ять (відображення значень клітин на блокові символи та кольори), рядок стану (номер правила у десятковому та двійковому, лічильник поколінь, команди), введення правила (читання 1-3 цифрового числа з клавіатури), режим аналізу (візуалізація таблиці правила), прокрутка при заповненні екрану, диспетчеризація клавіатурних команд. Файл: display.asm.
Спільний головний модуль: ініціалізація, головний цикл (покроковий або безперервний режим), спільні дані (масиви поколінь, байт правила, лічильник поколінь). Файл: main.asm.
Точки інтеграції:
Узгодити спільні змінні: cur_gen та next_gen (80-байтові масиви), rule (байт, 0-255), gen_count (слово).
Студент A надає compute_next_gen, яка читає cur_gen, застосовує rule, записує next_gen, потім обмінює вказівники (або копіює next у current).
Студент B надає display_row(row_number, gen_array_ptr), яка читає 80 байтів і записує 80 пар символ+атрибут у вказаний рядок екрану.
Головний цикл викликає compute_next_gen, потім display_row для поточного рядка екрану, потім інкрементує лічильник рядків. <br><a class="internal-link" data-href="How-to/game-logic/Y13.md" href="projects/how-to/game-logic/y13.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: бітове індексування правила за 3-бітовим сусідством, обробка граничних клітин, обчислення покоління.
<br><a class="internal-link" data-href="How-to/bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Ядро оцінки правила:</a> Сусідство клітини у стовпці C: left = gen[C-1], center = gen[C], right = gen[C+1]. Формування 3-бітового індексу: index = (left SHL 2) OR (center SHL 1) OR right. Тепер перевірити біт index байта правила: MOV CL, index / MOV AL, rule / SHR AL, CL / AND AL, 1. Якщо AL=1, нова клітина жива; якщо AL=0, мертва.
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Блокові символи для відображення:</a> Використовувати DB (повний блок, 0DBh) для живих клітин та пробіл (20h) для мертвих. Або використовувати верхній напівблок (0DFh) та нижній напівблок (0DCh) для упакування двох рядків клітин в один текстовий рядок, подвоюючи вертикальну роздільність до 48 поколінь на екрані. Це цікаве розширення, але не обов'язкове.
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Подвійна буферизація поколінь:</a> Підтримувати два масиви: cur_gen DB 80 DUP(0) та next_gen DB 80 DUP(0). Обчислити next_gen з cur_gen. Після обчислення обміняти їх: або скопіювати next_gen у cur_gen (80 байтів з REP MOVSB), або обміняти вказівники (XCHG SI, DI при адресації через регістри). Обмін вказівників швидший.
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Початковий стан з одним зерном:</a> Очистити масив покоління нулями, потім встановити cur_gen[40] = 1 (центральна клітина). Це дає класичний симетричний патерн для більшості правил.
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прокрутка з REP MOVSW:</a> Для прокрутки екрану на один рядок вгору: встановити DS:SI на B800h:00A0h (рядок 1, зміщення = 802 = 160 = 0A0h), ES:DI на B800h:0000h (рядок 0), CX = 2380 = 1840 слів. Виконати REP MOVSW. Потім намалювати нове покоління на останньому рядку. Не забути зберегти/відновити <a class="internal-link" data-href="How-to/segment-registers.md" href="projects/how-to/segment-registers.html" target="_self" rel="noopener nofollow">сегментні регістри</a>.
<br><a class="internal-link" data-href="How-to/number-display.md" href="projects/how-to/number-display.html" target="_self" rel="noopener nofollow">Двійкове відображення:</a> Для показу правила 110 як "01101110": MOV AL, rule / цикл 8 разів: SHL AL, 1 / JC write_one / записати '0' / JMP next_bit / write_one: записати '1' / next_bit: продовжити. Записувати кожну цифру на рядок стану через B800h.
<br><a class="internal-link" data-href="How-to/bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Візуалізація таблиці правила:</a> Для кожного з 8 сусідств (індекс 0-7) розкласти індекс на 3 біти (3 вхідних клітини) та показати вихідний біт. Відобразити як рядок: для індексу 7 (111) намалювати три заповнених блоки та стрілку, потім вихідний блок (заповнений, якщо біт 7 правила = 1, порожній інакше). Використовувати <a class="internal-link" data-href="How-to/loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">цикл</a> від 7 до 0, розкладаючи кожен індекс за допомогою SHR та AND.
<br><a class="internal-link" data-href="How-to/random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">Псевдовипадковий лінійний конґруентний генератор:</a> MOV AX, [seed] / MOV BX, 25173 / MUL BX / ADD AX, 13849 / MOV [seed], AX. Використовувати TEST AX, 1 для отримання одного випадкового біту для кожної клітини. Ініціалізувати seed з лічильника тактів INT 1Ah.
Точки синхронізації:
Кінець тижня 9: узгодити формат спільних даних (cur_gen, next_gen — 80-байтові масиви, rule — байт, gen_count — слово) та прототипи процедур (compute_next_gen, display_row(row_number, gen_array_ptr))
Кінець тижня 11: перша інтеграція — головний цикл у main.asm обчислює покоління та відображає рядки; правило 110 із зерном у центрі дає коректний візуальний патерн
Кінець тижня 12: повна функціональність — ввід правила, обидва початкових стани, покроковий та безперервний режими, прокрутка працює
Кінець тижня 13: код завершений, обидва студенти мають чорнові версії звіту з аналізом підходів A проти B
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/y13-wolfram-cellular-automaton.html</link><guid isPermaLink="false">Projects/Y13-Wolfram-Cellular-Automaton.md</guid><pubDate>Tue, 24 Feb 2026 16:02:21 GMT</pubDate><enclosure url="assets/y13-screen-evolution.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y13-screen-evolution.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[G02-Typing-Speed-Trainer]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🟢 Зелений
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 450-600 (соло) / 650-850 (пара), 2-4 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять</a> (пари символ + атрибут за адресою B800h для кольорового зворотного зв'язку) — L08/P08
<br><a class="internal-link" data-href="How-to/segment-registers.md" href="projects/how-to/segment-registers.html" target="_self" rel="noopener nofollow">Сегментні регістри</a> (ES = B800h для відеопам'яті, DS для рядків у сегменті даних) — L03/P03
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Рядкові дані в сегменті даних</a> (масиви DB із текстами завдань, завершені нулем або '$') — L06/P06
<br><a class="internal-link" data-href="How-to/timer-delays.md" href="projects/how-to/timer-delays.html" target="_self" rel="noopener nofollow">Опитування системного годинника</a> (INT 1Ah AH=00h для вимірювання часу) — L07/P07
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід без відлуння</a> (INT 16h AH=00h або INT 21h AH=07h) — L07/P07
<br><a class="internal-link" data-href="How-to/arithmetic.md" href="projects/how-to/arithmetic.html" target="_self" rel="noopener nofollow">Арифметика: MUL та DIV</a> (обчислення WPM та точності потребують 16-бітного множення та ділення) — L04/P04
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури з параметрами</a> (окремі процедури для малювання, таймінгу, обчислення статистики) — L05/P05
<br><a class="internal-link" data-href="How-to/flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Умовні переходи та FLAGS</a> (CMP для порівняння символів, JE/JNE для розгалуження правильно/неправильно) — L04/P04
<br><a class="internal-link" data-href="How-to/addressing-modes.md" href="projects/how-to/addressing-modes.html" target="_self" rel="noopener nofollow">Режими адресації</a> (SI як індекс у рядку завдання, DI як вказівник у відеопам'ять) — L06/P06
<br><a class="internal-link" data-href="How-to/game-logic/G02.md" href="projects/how-to/game-logic/g02.html" target="_self" rel="noopener nofollow">Ігрова логіка: тренажер друку</a> — обчислення WPM, відсоток точності, порівняння символів у реальному часі
Програма є тренажером швидкості набору тексту, що працює в текстовому режимі 80x25. При запуску екран меню (намальований через прямий запис у B800h) дозволяє гравцю обрати рівень складності: Easy (слова з 3-4 букв), Medium (слова з 5-7 букв) або Hard (повні речення з 40-60 символів). Меню використовує кольорово-підсвічені варіанти, і гравець навігує клавішами-стрілками або натискає 1/2/3.Після вибору складності екран очищується та відображає рядок завдання в рядку 8 яскраво-білим текстом — це текст, який гравець повинен набрати. Безпосередньо нижче (рядок 10) блимаючий курсор позначає місце, де з'являється введення гравця. Коли гравець набирає кожний символ, він з'являється в реальному часі: правильно набрані символи показуються яскраво-зеленим, а неправильні символи — яскраво-червоним із червоним спалахом фону. Рядок завдання підсвічує поточний очікуваний символ підкресленням або інвертованим атрибутом, щоб гравець завжди знав, де він знаходиться.Таймер у верхньому правому куті (рядки 0-1, стовпці 65-79) рахує минулі секунди та оновлюється в реальному часі за допомогою системного годинника (лічильник тактів INT 1Ah, опитується між натисканнями клавіш, або підключений через INT 1Ch). Коли гравець завершує набір усього рядка завдання (або натискає Escape для раннього виходу), програма зупиняє таймер і переходить до екрана статистики. Цей екран показує: слів за хвилину (WPM), відсоток точності (правильні натискання / загальні натискання * 100), загальний час у секундах та загальну кількість помилок. Уся статистика рендериться кольорово у фіксованих позиціях екрана через відеопам'ять. Гравець може натиснути R для повтору того самого тексту, N для нового тексту на тій самій складності, M для повернення до меню або Q для виходу в DOS.<br><img alt="Головне меню з вибором складності" src="assets/g02-screen-menu.png" target="_self"><br><img alt="Процес набору тексту з кольоровим зворотним зв'язком та таймером" src="assets/g02-screen-typing.png" target="_self"><br><img alt="Екран статистики з результатами WPM та точністю" src="assets/g02-screen-stats.png" target="_self">
Підхід A: Між кожним натисканням клавіші викликати INT 1Ah (AH=00h) для зчитування лічильника тактів та оновлення таймера на екрані, якщо минула секунда.
Підхід B: Встановити власний обробник INT 1Ch, який спрацьовує 18,2 рази на секунду, інкрементує лічильник у сегменті даних та оновлює відображення таймера зсередини обробника переривань.
Що виміряти/спостерігати: Порівняйте точність відображеного часу (опитування може пропустити оновлення, якщо гравець набирає швидко), складність коду (обробник переривань повинен зберігати/відновлювати всі регістри) та поведінку під час довгих пауз між натисканнями клавіш (опитування оновлюється лише при подіях клавіш; обробник переривань оновлюється безперервно). Наберіть той самий текст повільно та швидко; чи показує версія з опитуванням правильний фінальний час в обох випадках? Підхід A: Завантажити очікуваний символ через MOV AL, [SI+prompt], завантажити набраний символ з клавіатури, потім CMP AL, typed_char з умовним переходом.
Підхід B: Зберігати набрані символи в буфері і після тесту використати REP CMPSB (DS:SI проти ES:DI) для підрахунку невідповідностей масово.
Що виміряти/спостерігати: Порівняйте, який підхід дає компактніший код, який легше розширити для точності за словами, та чи надає CMPSB будь-яку перевагу в швидкості для фінального підрахунку точності. Покроково пройдіть обидва підходи в Turbo Debugger та підрахуйте інструкції на символ. Підхід A: Після кожного натискання клавіші перемальовувати весь рядок завдання та весь рядок набраного тексту з нуля (перебирати всі символи, записуючи пари символ+атрибут).
Підхід B: Після кожного натискання клавіші записувати лише один новий символ у поточну позицію (один MOV у ES:[DI]) та переміщувати підсвітку на тексті завдання, записуючи два байти атрибуту (попередня позиція відновлена, нова позиція підсвічена).
Що виміряти/спостерігати: Підрахуйте кількість записів у пам'ять за натискання клавіші для кожного підходу. Для 50-символьного завдання підхід A записує ~200 байтів за натискання, тоді як підхід B записує ~6 байтів. Обговоріть, коли повне перемалювання стає необхідним (після backspace, зміни розміру вікна в інших середовищах).
<br>Розподіл модулів (див. <a class="internal-link" data-href="How-to/include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">Підключення файлів: INCLUDE</a>):
Студент A — Модуль відображення та інтерфейсу: рендеринг екрана меню, відображення тексту завдання з підсвіткою, відображення набраних символів з кольоровим зворотним зв'язком, відображення таймера, макет екрана статистики. Файл: display.asm.
Студент B — Модуль логіки та таймінгу: цикл обробки натискань клавіш, порівняння символів, підрахунок помилок, таймінг (старт/стоп, обчислення часу), обчислення WPM та точності, вибір тексту за рівнем складності. Файл: logic.asm.
Спільний головний модуль: ініціалізація програми (встановлення відеорежиму, налаштування сегментів), цикл головного меню, перехід до вибору складності та тесту набору, логіка перезапуску/виходу. Файл: main.asm.
Точки інтеграції:
Узгодьте розміщення сегмента даних: де зберігаються рядки завдань, формат змінних «поточного стану» (current_index, error_count, start_ticks, correct_count).
Визначте сигнатуру процедури draw_char_at завчасно: рядок у BH, стовпець у BL, символ у AL, атрибут у AH. Обидва партнери будуть її викликати.
Студент B надає compute_stats (повертає WPM у AX, точність у BX, час у CX, помилки у DX); Студент A надає display_stats (приймає ці значення та рендерить їх). Узгодьте угоди щодо регістрів на KT-2. <br><a class="internal-link" data-href="How-to/game-logic/G02.md" href="projects/how-to/game-logic/g02.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: обчислення WPM через цілочислову арифметику, відсоток точності, кольоровий зворотний зв'язок.
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Зберігання рядків завдань:</a> Організуйте завдання як таблицю вказівників. Визначте кожний рядок через DB, потім створіть масив слів: easy_ptrs DW OFFSET easy1, OFFSET easy2, .... Щоб обрати завдання N, завантажте [easy_ptrs + N*2] у SI. Це чистіше, ніж обчислення зміщень у одному великому буфері.
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Зчитування клавіш без відлуння:</a> Використовуйте MOV AH, 00h / INT 16h, що очікує натискання клавіші та повертає її в AL без відображення. Це дає повний контроль над тим, що з'являється на екрані.
Довідник байтів атрибутів: 0Ah = яскраво-зелений на чорному, 0Ch = яскраво-червоний на чорному, 0Fh = яскраво-білий на чорному, 70h = чорний на сірому (інвертований), 1Fh = яскраво-білий на синьому. Це найкорисніші атрибути для цього проєкту.
<br><a class="internal-link" data-href="How-to/number-display.md" href="projects/how-to/number-display.html" target="_self" rel="noopener nofollow">Відображення десяткового числа:</a> Для відображення числа на позиції екрана напишіть процедуру print_dec: послідовно DIV на 10, PUSH залишки на стек, потім POP та додайте 30h для отримання цифр ASCII. Запишіть кожну цифру у відеопам'ять з потрібним атрибутом.
<br><a class="internal-link" data-href="How-to/arithmetic.md" href="projects/how-to/arithmetic.html" target="_self" rel="noopener nofollow">Формула WPM у цілочисловій арифметиці:</a> Стандартна WPM = (символи / 5) / (хвилини). У тактах: WPM = (символи 12) / (5 минулихтактів / 182). Перегрупувавши для уникнення дробових проміжних значень: WPM = (символи 12 182) / (5 * минулихтактів) = (символи 2184) / (5 минулих_тактів). Використовуйте MUL для чисельника (результат у DX:AX), потім DIV для знаменника.
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Підсвітка поточного символу:</a> Щоб підсвітити позицію I у рядку завдання на екрані, обчисліть зміщення у відеопам'яті для байта атрибуту цього символу: (prompt_row * 160) + (prompt_col * 2) + (I * 2) + 1. Запишіть атрибут підсвітки туди. Пам'ятайте відновити атрибут попереднього символу до звичайного.
Точки синхронізації:
Кінець тижня 9: сигнатура draw_char_at (BH=рядок, BL=стовпець, AL=символ, AH=атрибут), формат змінних стану (current_index, error_count, start_ticks, correct_count), угода про повернення compute_stats
Кінець тижня 11: перша інтеграція — main.asm запускає повний цикл набору (меню → текст → введення → статистика), таймер оновлюється між натисканнями
Кінець тижня 13: код повністю працює (всі 3 рівні, R/N/M/Q, Backspace, Escape), чернетка звіту з аналізом підходів
Кінець тижня 14: звіт фіналізовано, відео записані, перевірені граничні випадки арифметики
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/g02-typing-speed-trainer.html</link><guid isPermaLink="false">Projects/G02-Typing-Speed-Trainer.md</guid><pubDate>Tue, 24 Feb 2026 16:02:20 GMT</pubDate><enclosure url="assets/g02-screen-menu.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/g02-screen-menu.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[R02-Breakout]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🔴 Червоний
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 500-650 (соло) / 700-900 (пара), 3-6 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/interrupt-handlers.md" href="projects/how-to/interrupt-handlers.html" target="_self" rel="noopener nofollow">Перехоплення переривання таймера</a> (INT 1Ch) — рух м'яча та ігрова синхронізація керуються обробником переривань; ігровий процес зі змінною швидкістю (L07/P07)
<br><a class="internal-link" data-href="How-to/arithmetic.md" href="projects/how-to/arithmetic.html" target="_self" rel="noopener nofollow">Арифметика з фіксованою точкою</a> — субкоміркова точність позиціонування м'яча з використанням цілочисельної математики для імітації дробових координат (L04/P04)
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід</a> (INT 16h) — неблокуюче опитування клавіші для керування платформою в реальному часі (L07/P07)
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять</a> (B800h) — відображення цеглин, платформи, м'яча, рахунку та життів без накладних витрат BIOS (L08/P08)
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Обробка 2D масивів</a> — сітка цеглин зберігається як 2D байтовий масив; виявлення зіткнень з елементами масиву (L06/P06)
<br><a class="internal-link" data-href="How-to/segment-registers.md" href="projects/how-to/segment-registers.html" target="_self" rel="noopener nofollow">Маніпуляція сегментними регістрами</a> (ES) — ES вказує на B800h для відеопам'яті, DS для даних (L03/P03, L06/P06)
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури та дисципліна стеку</a> — модульний дизайн: оновлення м'яча, перевірки зіткнень, рух платформи, знищення цеглин, процедури малювання (L05/P05)
<br><a class="internal-link" data-href="How-to/flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Прапорці та умовна логіка</a> — рішення щодо напрямку відбиття, перевірки меж, виявлення влучання в цеглину (L04/P04)
<br><a class="internal-link" data-href="How-to/bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Побітові операції</a> — потенційно корисні для упаковки сітки цеглин або вилучення з фіксованої точки (L04/P04)
<br><a class="internal-link" data-href="How-to/game-logic/R02.md" href="projects/how-to/game-logic/r02.html" target="_self" rel="noopener nofollow">Ігрова логіка: Breakout</a> — фіксована точка 8.8 для позиції м'яча, зіткнення зі стінами/платформою/цеглинами, кути відбиття
<br>Див. також <a class="internal-link" data-href="R01-Tetris.md" href="projects/r01-tetris.html" target="_self" rel="noopener nofollow">R01 Тетріс</a> — також керується таймером з 2D виявленням зіткнень. Breakout зосереджений на фізиці м'яча з фіксованою точкою; Тетріс зосереджений на обертанні фігур та очищенні рядків.Програма є аркадною грою Breakout (у стилі Arkanoid), що працює в текстовому режимі 80x25 у DOSBox. Після запуску гравець бачить поле кольорових цеглин, розташованих рядами у верхній третині екрана, платформу внизу (горизонтальну панель з 8-10 символів) і м'яч (один яскравий символ), готовий до запуску. Весь дисплей відображається через прямий запис у відеопам'ять за адресою B800h — цеглини як кольорові блокові символи, платформа як контрастна панель, а м'яч як характерний символ.Натискання пробілу запускає м'яч вгору по діагоналі. М'яч рухається безперервно, керований перериванням системного таймера (INT 1Ch), відбиваючись від лівої, правої та верхньої стін. Коли м'яч влучає в цеглину, цеглина знищується (стирається з екрана та зі структур даних гри), рахунок збільшується, і м'яч відбивається. Коли м'яч досягає рядка платформи, він відбивається від платформи — при цьому кут відбиття залежить від того, в яке місце платформи потрапив м'яч (удар по краях платформи направляє м'яч під більш крутим кутом, удар по центру — більш вертикально). Якщо м'яч проходить нижче платформи, гравець втрачає життя. Три життя відображаються на панелі статусу. Рівень виграний, коли всі цеглини знищені.Основна складність цього проєкту полягає в плавному русі м'яча. М'яч, який переміщується на одну повну символьну комірку за такт таймера, виглядатиме уривчасто і матиме обмежену різноманітність кутів. Правильне рішення вимагає субкоміркової точності — відстеження позиції м'яча в арифметиці з фіксованою точкою (наприклад, формат 8.8), щоб дробовий рух накопичувався протягом кількох тактів, створюючи плавний діагональний рух під різними кутами. Ця єдина вимога підвищує Breakout із жовтого рівня складності до червоного. Парна робота наполегливо рекомендується.<br><img alt="Ігровий процес" src="assets/r02-screen-gameplay.png" target="_self"><br><img alt="Кінець гри" src="assets/r02-screen-gameover.png" target="_self">
Підхід A: Переміщує м'яч рівно на одну комірку по x та одну по y за такт — прості операції INC/DEC, але м'яч може рухатися лише під кутом 45 градусів і рухається уривчасто.
Підхід B: Зберігає x та y як 16-бітні значення з фіксованою точкою (старший байт = комірка, молодший байт = дробова частина) і додає компоненти швидкості (dx, dy) за такт, переміщуючи відображуваний символ лише коли ціла частина змінюється.
Що виміряти/спостерегти: Різноманітність кутів (A має лише 4 можливі напрямки; B може досягти будь-якого співвідношення), сприйнята плавність, складність коду (A — ~5 інструкцій; B вимагає ADD з розповсюдженням переносу та SHR для вилучення цілої частини). Це визначальний архітектурний вибір проєкту. Підхід A: Використовує 2D байтовий масив (рядки x стовпці), де кожен байт зберігає стан цеглини (0=порожньо, 1=звичайна, 2=один удар для багатоударних).
Підхід B: Пакує один біт на цеглину в слова (одне слово на рядок, якщо &lt;= 16 стовпців), використовуючи TEST для перевірки зіткнень та AND/XOR для знищення.
Що виміряти/спостерегти: Використання пам'яті (80 байтів проти 10 слів = 20 байтів для сітки 8x10), простота виявлення влучання (A використовує індексований MOV + CMP; B використовує зсув + TEST), та чи практичні багатоударні цеглини в підході B (вони не є без розширення до 2 бітів на цеглину). Виміряйте кількість інструкцій для "перевірити, чи існує цеглина в (рядок, стовпець)". Підхід A: Ділить платформу на 3-5 фіксованих зон, кожна зіставлена з конкретним кутом відбиття (таблиця пошуку пар швидкостей dx/dy).
Підхід B: Обчислює горизонтальне зміщення м'яча від центру платформи і визначає горизонтальну складову швидкості пропорційно (наприклад, dx = (hit_offset - paddle_center) * scale).
Що виміряти/спостерегти: Складність коду, гранулярність кутів (A має 3-5 кутів; B має безперервну варіацію) та відчуття геймплею (протестуйте обидва та повідомте, який більш керований). Обидва підходи дійсні; порівняння розкриває компроміс між простотою та точним контролем.
<br>Цей проєкт наполегливо рекомендується для пар. Фізика м'яча з фіксованою точкою та система зіткнень з цеглинами кожна є суттєвою; впоратися з обома на самоті вимагає дуже ретельного керування часом (див. <a class="internal-link" data-href="How-to/include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">Підключення файлів: INCLUDE</a>).
Студент A: Фізика м'яча та двигун зіткнень — позиція та швидкість м'яча у фіксованій точці, відбиття від стін, виявлення зіткнень з цеглинами та їх знищення (оновлення масиву цеглин), зіткнення з платформою з обчисленням кута, промах м'яча та керування життями.
Студент B: Відображення, ввід та ігровий потік — процедури малювання у відеопам'яті (рамка, цеглини за кольором, платформа, м'яч, відображення рахунку/життів, екрани перемоги/кінця гри), обробник клавіатурного вводу для руху платформи та ігрових елементів керування (запуск, пауза, перезапуск), ініціалізація рівня, перехоплення/відновлення переривання таймера.
Точки інтеграції: Спільний сегмент даних, що містить: масив цеглин, позицію м'яча (x_fp, y_fp) та швидкість (dx_fp, dy_fp), позицію та ширину платформи, рахунок, життя, прапорець стану гри. Визначте формат фіксованої точки разом на КТ-2 (наприклад, 8.8 означає старший байт — координата комірки, молодший байт — дробова частина).
Незалежне тестування: Студент A може тестувати фізику м'яча, виводячи координати через INT 21h, перевіряючи, що м'яч правильно відбивається в консольному режимі. Студент B може тестувати відображення з жорстко закодованою статичною сценою (намальовані цеглини, платформа, нерухомий м'яч). <br><a class="internal-link" data-href="How-to/game-logic/R02.md" href="projects/how-to/game-logic/r02.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: фіксована точка 8.8 для позиції/швидкості м'яча, зіткнення зі стінами/платформою/цеглинами, кути відбиття від платформи.
<br><a class="internal-link" data-href="How-to/arithmetic.md" href="projects/how-to/arithmetic.html" target="_self" rel="noopener nofollow">Формат фіксованої точки 8.8:</a> Зберігайте позицію як 16-бітне слово, де старший байт — ціле число (координата комірки), а молодший байт — дробова частина. Додавання швидкості (також 8.8) — одна інструкція ADD. Для отримання цілої частини для відображення: MOV AL, AH (або SHR AX, 8). Приклад: позиція 0A40h = комірка 10, дробова частина 64/256 = 10.25. Швидкість FF80h (знакова) = -0,5 комірки/такт.
Знакова швидкість: Компоненти швидкості м'яча (dx, dy) повинні бути знаковими. При відбитті м'яча інвертуйте відповідний компонент: NEG dx_fp або NEG dy_fp. Знакове ADD коректно працює як для додатних, так і для від'ємних швидкостей.
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Розкладка сітки цеглин:</a> Якщо цеглини починаються з рядка 2 екрана і кожна цеглина має висоту 1 рядок та ширину 3 символи (наприклад, стовпці 5-34 для 10 цеглин), то цеглина (r, c) займає рядок екрана 2+r у стовпцях 5 + c*3 до 5 + c*3 + 2. Зіставте цілу позицію м'яча з індексом сітки цеглин: brick_row = ball_y - 2, brick_col = (ball_x - 5) / 3. Перевірте межі перед індексуванням.
<br><a class="internal-link" data-href="How-to/interrupt-handlers.md" href="projects/how-to/interrupt-handlers.html" target="_self" rel="noopener nofollow">INT 1Ch</a> спрацьовує ~18,2 рази/секунду. Для руху м'яча ~18 оновлень/секунду обробляйте один крок м'яча за такт. Для повільнішого м'яча використовуйте лічильник тактів і оновлюйте кожен 2-й або 3-й такт. Тримайте ISR мінімальним: інкрементуйте лічильник і IRET. Всю ігрову логіку виконуйте в основному циклі.
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Неблокуючий клавіатурний ввід:</a> MOV AH, 01h / INT 16h — ZF=0 означає, що клавіша очікує. Клавіші-стрілки: AL=0, скан-код в AH (4Bh=Ліворуч, 4Dh=Праворуч). Пробіл: AL=20h.
<br><a class="internal-link" data-href="How-to/lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Кути відбиття від платформи:</a> Поділіть платформу на зони. Для 10-символьної платформи позиції 0-1 можуть встановити dx на -2 (крутий ліворуч), 2-3 на -1, 4-5 на поточний напрямок, 6-7 на +1, 8-9 на +2. Збережіть їх як таблицю пошуку значень dx (у фіксованій точці). Завжди встановлюйте dy від'ємним (вгору).
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Відеопам'ять</a> для рядка R, стовпця C: offset = (R * 80 + C) * 2. Символ за зміщенням, атрибут за зміщенням+1.
<br><a class="internal-link" data-href="How-to/lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Кольори цеглин за рядком:</a> Збережіть таблицю пошуку з 8 байтів атрибутів (один на рядок): наприклад, 0Ch (червоний), 0Eh (жовтий), 0Ah (зелений), 09h (синій) тощо. Індексуйте за рядком цеглини для унікального кольору кожного рядка.
Символ м'яча: Розгляньте використання символу 07h (куля) або 04h (ромб) або 0Fh (сонце) з яскравим білим атрибутом (0Fh) для видимості.
Точки синхронізації:
Кінець тижня 9: узгодити формат фіксованої точки 8.8, структуру .DATA сегмента (позиція/швидкість м'яча, масив цеглин, стан платформи), прототипи процедур
Кінець тижня 11: перша повна інтеграція — м'яч рухається, відбивається від стін та платформи, знищує цеглини на екрані
Кінець тижня 13: програма повністю працює від запуску м'яча до перемоги/кінця гри; обидва партнери тестують граничні випадки зіткнень разом
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/r02-breakout.html</link><guid isPermaLink="false">Projects/R02-Breakout.md</guid><pubDate>Tue, 24 Feb 2026 16:02:20 GMT</pubDate><enclosure url="assets/r02-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/r02-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[R01-Tetris]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🔴 Червоний
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 550-700 (соло) / 750-950 (пара), 3-6 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/interrupt-handlers.md" href="projects/how-to/interrupt-handlers.html" target="_self" rel="noopener nofollow">Перехоплення переривання таймера</a> (INT 1Ch) — падіння фігур під дією гравітації зі змінною швидкістю; обробник переривань встановлює прапорець, основний цикл обробляє ігровий такт (L07/P07)
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід</a> (INT 16h) — неблокуюче опитування клавіші (AH=01h) для керування в реальному часі під час ігрового циклу (L07/P07)
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять</a> (B800h) — відображення ігрового поля, фігур, попереднього перегляду та рахунку без накладних витрат BIOS (L08/P08)
<br><a class="internal-link" data-href="How-to/segment-registers.md" href="projects/how-to/segment-registers.html" target="_self" rel="noopener nofollow">Маніпуляція сегментними регістрами</a> (ES) — спрямування ES на B800h для запису у відеопам'ять, керування DS для доступу до даних (L03/P03, L06/P06)
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Обробка 2D масивів</a> — ігрове поле є байтовим масивом 10x20; форми фігур є масивами 4x4 або еквівалентними; широке використання <a class="internal-link" data-href="How-to/addressing-modes.md" href="projects/how-to/addressing-modes.html" target="_self" rel="noopener nofollow">індексної адресації</a> (L06/P06)
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури та дисципліна стеку</a> — модульний дизайн: spawn, rotate, move, collision check, lock, line clear, draw (L05/P05)
<br><a class="internal-link" data-href="How-to/flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Прапорці та умовні переходи</a> — виявлення зіткнень після кожної спроби руху або повороту (L04/P04)
<br><a class="internal-link" data-href="How-to/lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиці пошуку</a> — дані форм тетроміно як таблиці пошуку, індексовані за типом фігури та станом повороту (L06/P06)
<br><a class="internal-link" data-href="How-to/random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">Генерація псевдовипадкових чисел</a> — вибір наступної фігури за допомогою лічильника таймера або лінійного конгруентного генератора (L07)
Багатобайтові структури даних — кожне визначення фігури кодує позиції блоків; поле кодує як заповнення, так і колір (L06/P06)
<br><a class="internal-link" data-href="How-to/game-logic/R01.md" href="projects/how-to/game-logic/r01.html" target="_self" rel="noopener nofollow">Ігрова логіка: Тетріс</a> — обертання фігур через таблиці пошуку, зіткнення за принципом "перевір-потім-зафіксуй", очищення рядків, гравітація
<br>Див. також <a class="internal-link" data-href="R02-Breakout.md" href="projects/r02-breakout.html" target="_self" rel="noopener nofollow">R02 Breakout</a> — також керується таймером з 2D виявленням зіткнень. Тетріс зосереджений на обертанні фігур та очищенні рядків; Breakout зосереджений на фізиці м'яча з фіксованою точкою.Програма є класичною грою Тетріс, що працює в текстовому режимі 80x25 у DOSBox. Після запуску гравець бачить центроване ігрове поле — колодязь із 10 стовпців та 20 рядків, обрамлений символами подвійного рамкового малювання — з вікном попереднього перегляду "Next Piece" праворуч та панеллю відображення рахунку/рівня/ліній поряд. Вся візуальна презентація побудована через прямий запис у відеопам'ять за сегментом B800h, де кожен заповнений блок відображається як суцільний блоковий символ (DB 219) кольором, призначеним типу тетроміно.Тетроміно з'являється у верхній частині колодязя та починає падати зі швидкістю, контрольованою перериванням системного таймера (INT 1Ch). Гравець використовує клавіші зі стрілками для переміщення фігури ліворуч або праворуч, стрілку вгору для обертання, стрілку вниз для м'якого скидання (прискорене падіння) та пробіл для жорсткого скидання (миттєве розміщення). Присутні всі сім стандартних тетроміно — I (блакитний), O (жовтий), T (фіолетовий), S (зелений), Z (червоний), J (синій), L (помаранчевий) — кожне з 4 станами повороту. Коли фігура приземляється, повністю заповнені рядки виявляються, очищуються з коротким візуальним спалахом, і всі рядки вище зсуваються вниз. Рахунок збільшується залежно від кількості одночасно очищених ліній (1 лінія = 100, 2 = 300, 3 = 500, 4 = 800, масштабовано за рівнем). Кожні 10 очищених ліній підвищують рівень, що зменшує інтервал падіння, роблячи гру прогресивно швидшою. Гра закінчується, коли нова фігура не може з'явитися через зайняті верхні рядки; з'являється повідомлення "GAME OVER" з фінальним рахунком, і гравець може перезапустити або вийти.Це один із найскладніших проєктів у меню. Поєднання руху з таймером, обертання з виявленням зіткнень та очищення рядків зі зсувом рядків створює мережу взаємодіючих систем, які всі мають працювати правильно, щоб гра відчувалася коректно. Студентам, які обирають цю тему, слід бути впевненими у роботі з масивами та обробці переривань, і парна робота наполегливо рекомендується.<br><img alt="Ігровий процес" src="assets/r01-screen-gameplay.png" target="_self"><br><img alt="Кінець гри" src="assets/r01-screen-gameover.png" target="_self">
Підхід A: Зберігає всі 4 стани повороту кожної фігури явно як 4 окремі масиви координат блоків (7 фігур x 4 повороти = 28 записів).
Підхід B: Зберігає лише стан повороту 0 та обчислює решту, застосовуючи формулу повороту на 90 градусів: (x, y) -&gt; (-y, x) відносно центру фігури.
Що виміряти/спостерегти: Використання пам'яті (A використовує ~112 байтів для форм проти B використовує ~28), складність коду процедури обертання (A — проста таблиця пошуку; B вимагає арифметики SUB/NEG на кожен блок) та ризик помилок (A схильний до помилок під час введення даних; B може створювати помилки off-by-one навколо центру обертання). Це справжній і значущий вибір дизайну. Підхід A: Використовує байтовий масив 10x20, де кожна комірка зберігає 0 (порожньо) або байт атрибуту кольору блоку (1 байт на комірку, 200 байтів загалом).
Підхід B: Використовує бітове поле з 20 слів, де кожен біт позначає зайнято/порожньо (20 слів = 40 байтів), з відтворенням кольору або однорідним кольором.
Що виміряти/спостерегти: Використання пам'яті (200 байтів проти 40 байтів), складність виявлення очищення рядка (B може перевірити повний рядок одним CMP з 03FFh; A повинен проходити через 10 байтів) та складність відображення (A може безпосередньо записати колір у відеопам'ять; B потребує додаткової логіки). Підрахуйте кількість інструкцій для виявлення очищення рядка в кожному підході. Підхід A: Перемальовує всі 200 комірок кожен ігровий такт.
Підхід B: Відстежує, які комірки змінилися (фігура перемістилася, рядок очищено), і перемальовує лише їх.
Що виміряти/спостерегти: Кількість записів у відеопам'ять за такт (A = мінімум 200 записів; B = ~8-20 записів для типового руху), візуальна плавність (A може мерехтіти, якщо не впорядковано ретельно; B за своєю природою не мерехтить) та складність коду (B вимагає прапорця "dirty" або порівняння між старою та новою позиціями фігури). Підрахуйте записи у відеопам'ять за кадр у Turbo Debugger.
<br>Цей проєкт наполегливо рекомендується для пар. Взаємодіючі підсистеми (обертання, зіткнення, очищення рядків, відображення) створюють достатню складність, тому розділення роботи майже необхідне для завершення за 8 тижнів (див. <a class="internal-link" data-href="How-to/include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">Підключення файлів: INCLUDE</a>).
Студент A: Ігровий двигун — перехоплення/відновлення переривання таймера, система гравітації та швидкості, виявлення зіткнень, генерація фігур, логіка руху та обертання фігур, очищення рядків і зсув рядків, скінченний автомат стану гри (гра/кінець гри).
Студент B: Відображення, ввід і дані — процедури малювання у відеопам'яті (рамка поля, заповнені комірки, активна фігура, попередній перегляд наступної фігури, відображення рахунку/рівня/ліній, екран кінця гри), обробник клавіатурного вводу, таблиці даних форм тетроміно, випадковий вибір фігури, обчислення рахунку.
Точки інтеграції: Спільний сегмент даних, що містить: масив ігрового поля 10x20, поточну фігуру (тип, поворот, x, y), тип наступної фігури, змінні рахунку/рівня/ліній та прапорець стану гри. Визначте цю структуру разом на КТ-2 перед написанням будь-якого коду.
Незалежне тестування: Студент A може тестувати рух та зіткнення, виводячи координати фігури через INT 21h до створення рендерера. Студент B може тестувати відображення, жорстко закодувавши частково заповнене поле та намалювавши його. <br><a class="internal-link" data-href="How-to/game-logic/R01.md" href="projects/how-to/game-logic/r01.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: обертання фігур через таблиці пошуку, виявлення зіткнень "перевір-потім-зафіксуй", очищення рядків знизу вгору, гравітація через лічильник тактів.
<br><a class="internal-link" data-href="How-to/lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Представлення тетроміно:</a> Кожен стан повороту можна зберігати як 4 пари (x,y) відносно центру, використовуючи знакові байти. Наприклад, T-фігура в повороті 0 може бути: (0,0), (-1,0), (1,0), (0,-1). Збережіть усі 28 станів у плоскій таблиці та індексуйте за piece_type * 4_rotations * 4_blocks * 2_coords + rotation * 4_blocks * 2_coords. Альтернативно, збережіть кожен стан як бітову маску 4x4 (одне слово на стан: 16 бітів для 16 комірок).
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Виявлення зіткнень:</a> Маючи кандидатну позицію фігури (тип, поворот, x, y), отримайте 4 зміщення блоків, додайте (x,y) до кожного та перевірте: (a) всі x-координати в межах 0-9, (b) всі y-координати в межах 0-19, (c) жоден блок не перетинає зайняту комірку в масиві ігрового поля. Якщо будь-яка перевірка не пройшла, рух/обертання відхиляється.
<br><a class="internal-link" data-href="How-to/interrupt-handlers.md" href="projects/how-to/interrupt-handlers.html" target="_self" rel="noopener nofollow">INT 1Ch</a> спрацьовує ~18,2 рази/секунду. Рівень 1 може скидати фігуру кожні 18 тактів (~1 секунда). Рівень 5 може скидати кожні 8 тактів. Рівень 10 може скидати кожні 3 такти. Використовуйте <a class="internal-link" data-href="How-to/lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">таблицю пошуку</a> інтервалів тактів, індексовану за рівнем.
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Неблокуючий клавіатурний ввід:</a> MOV AH, 01h / INT 16h — ZF=0 означає, що клавіша очікує. Потім MOV AH, 00h / INT 16h для її зчитування. Клавіші-стрілки: AL=0, скан-код в AH (48h=Вгору, 50h=Вниз, 4Bh=Ліворуч, 4Dh=Праворуч). Пробіл: AL=20h.
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Адреса відеопам'яті</a> для рядка R, стовпця C: offset = (R * 80 + C) * 2. Символ за цим зміщенням, атрибут за зміщенням+1. Для малювання блоку в комірці ігрового поля (px, py) обчисліть позицію на екрані, додавши зміщення верхнього лівого кута поля на екрані.
<br><a class="internal-link" data-href="How-to/random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">Випадковий вибір фігури:</a> Зчитайте лічильник тактів таймера через INT 1Ah AH=00h (повертає CX:DX). Використовуйте DX MOD 7 (через DIV) для вибору наступної фігури. Для кращого розподілу використовуйте перемішування пакету: заповніть масив значеннями 0-6, перемішайте, видавайте фігури з масиву, перезаповніть, коли порожній.
Перевірка очищення рядків: Після фіксації фігури лише рядки, які фігура займає, могли стати повними. Перевіряйте лише ці 1-4 рядки, а не всі 20 — це простіше та швидше.
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Байти атрибутів для кольорів фігур:</a> I=0Bh (блакитний), O=0Eh (жовтий), T=0Dh (малиновий), S=0Ah (зелений), Z=0Ch (червоний), J=09h (синій), L=06h (коричневий/помаранчевий). Збережіть їх у 7-байтовій таблиці, індексованій за типом фігури.
Точки синхронізації:
Кінець тижня 9: узгодити структуру .DATA сегмента (масив поля, стан фігури, лічильники), прототипи процедур та угоди про виклик
Кінець тижня 11: перша повна інтеграція — фігура падає, керується клавіатурою, відображається на екрані, фіксується та запускає очищення рядків
Кінець тижня 13: програма повністю працює від появи фігури до кінця гри; обидва партнери тестують разом усі 7 типів фігур та граничні випадки
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/r01-tetris.html</link><guid isPermaLink="false">Projects/R01-Tetris.md</guid><pubDate>Tue, 24 Feb 2026 16:02:17 GMT</pubDate><enclosure url="assets/r01-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/r01-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y04-Connect-Four]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🟡 Жовтий
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 400-550 рядків коду (соло) / 600-800 рядків коду (пара), 2-5 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Двовимірний масив</a> — поле 7x6, збережене як масив байтів; стовпцевий або рядковий порядок з обчисленими зміщеннями — L06/P06
<br><a class="internal-link" data-href="How-to/addressing-modes.md" href="projects/how-to/addressing-modes.html" target="_self" rel="noopener nofollow">Сканування перемоги в чотирьох напрямках</a> — перевірка горизонталі, вертикалі та обох діагоналей з різними кроками індексу (+1, +7, +8, +6 для рядкового порядку з шириною 7) — L06/P06
<br><a class="internal-link" data-href="How-to/loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Симуляція гравітації</a> — сканування стовпця знизу вгору для знаходження першого порожнього рядка — L04/P04
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять</a> (B800h) — кольорові фішки, рамка поля, рядок стану, підсвічування перемоги — L08/P08
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід через INT 16h</a> — вибір стовпця, підтвердження ходу — L07/P07
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури з параметрами</a> — drop_piece(col, player), check_win(row, col), draw_board() — L05/P05
<br><a class="internal-link" data-href="How-to/flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Простий ШІ з умовною логікою</a> — прийняття рішень на основі пріоритетів через CMP та умовні переходи — L04/P04
<br><a class="internal-link" data-href="How-to/loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Циклічні конструкції</a> — сканування рядків у стовпці (гравітація), сканування чотирьох напрямків для перемоги — L04/P04
<br><a class="internal-link" data-href="How-to/game-logic/Y04.md" href="projects/how-to/game-logic/y04.html" target="_self" rel="noopener nofollow">Ігрова логіка: З'єднай чотири</a> — гравітація, визначення перемоги в 4 напрямках, простий ШІ з пріоритетами
<br>Див. також <a class="internal-link" data-href="G01-Tic-Tac-Toe.md" href="projects/g01-tic-tac-toe.html" target="_self" rel="noopener nofollow">G01 Хрестики-нулики</a> (Зелена складність) — подібна логіка визначення перемоги на меншому полі без гравітації. «Чотири в ряд» додає симуляцію гравітації, більший простір пошуку та розумніший ШІ.Програма — це гра «Чотири в ряд» для двох гравців (або одного гравця проти простого ШІ), що працює в текстовому режимі DOSBox. Екран відображає поле з 7 стовпців на 6 рядків, намальоване символами псевдографіки з кольоровими фішками. Фішки гравця 1 — яскраво-червоні кола (або блокові символи з червоними атрибутами), а гравця 2 — яскраво-жовті. Поле має видиму рамку, а стовпці пронумеровані 1-7 зверху.Селектор стовпця (підсвічена стрілка або маркер над полем) рухається вліво та вправо стрілками. Натискання Enter кидає фішку у вибраний стовпець. Фішка «падає» до найнижчого порожнього рядка — візуально це може бути миттєвим або анімованим для кращого ефекту. Програма перевіряє перемогу після кожного ходу: чотири в ряд горизонтально, вертикально або по діагоналі в будь-якому напрямку. Чотири переможні фішки підсвічуються (наприклад, миготливим атрибутом). Нічия визначається, коли всі 42 клітинки заповнені без переможця.Якщо студент реалізує режим ШІ, комп'ютерний опонент застосовує стратегію на основі пріоритетів: спочатку перевірити, чи може виграти негайно, потім заблокувати переможний хід суперника, потім віддати перевагу центральному стовпцю, а потім вибрати випадково серед допустимих стовпців. Рядок стану показує, чия черга та кількість ходів.<br><img alt="Ігровий процес" src="assets/y04-screen-gameplay.png" target="_self"><br><img alt="Екран перемоги" src="assets/y04-screen-win.png" target="_self">
Підхід A: Рядковий порядок, клітинка (row, col) знаходиться за row * 7 + col.
Підхід B: Стовпцевий порядок, клітинка (row, col) знаходиться за col * 6 + row.
Що виміряти/спостерігати: Гравітація (знаходження найнижчої порожньої клітинки в стовпці) потребує сканування послідовних байтів при стовпцевому порядку, але кроковий доступ при рядковому. Перевірка перемоги по горизонталі потребує послідовних байтів при рядковому порядку, але кроковий доступ при стовпцевому. Проаналізуйте, яка структура робить яку операцію простішою та потребує менше інструкцій. Реалізуйте одну і обговоріть компроміси іншої. Підхід A: Написати чотири окремі процедури (або блоки коду): scan_horizontal, scan_vertical, scan_diag_right, scan_diag_left.
Підхід B: Написати одну процедуру scan_direction(row, col, row_delta, col_delta) і викликати її чотири рази з різними дельтами.
Що виміряти/спостерігати: Обсяг коду, використання регістрів та зрозумілість. Узагальнений підхід елегантніший, але потребує передачі більше параметрів через стек. Підхід A: Евристичний ШІ перевіряє кілька правил по порядку (перемога, блокування, центр, випадковий).
Підхід B: ШІ з передбаченням пробує кожен допустимий стовпець, симулює кидок, перевіряє, чи результатне поле є перемогою або програшем, та оцінює кожен хід.
Що виміряти/спостерігати: Кількість виконаних інструкцій на хід ШІ, якість гри та складність коду. Передбачення ретельніше, але обчислювально важче.
<br>Розподіл модулів (див. <a class="internal-link" data-href="How-to/include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">Підключення файлів: INCLUDE</a>):
Студент A: Ігрова логіка та ШІ — структура даних поля, drop_piece, логіка гравітації, визначення перемоги (сканування 4 напрямків), визначення нічиєї, процедура прийняття рішень ШІ
Студент B: Дисплей та взаємодія — рендеринг поля символами псевдографіки, розфарбування фішок, селектор стовпця, обробник клавіатурного вводу, індикатор черги, лічильник ходів, анімація підсвічування перемоги, екран меню та перезапуску
Точки інтеграції: масив поля (7x6 байтів) та змінні стану гри (поточний гравець, кількість ходів, фаза гри). Обидва студенти читають поле; Студент A записує в нього (drop_piece), Студент B читає його (draw_board). Визначте структуру поля та систему координат разом.
Незалежне тестування: Студент A може тестувати визначення перемоги, жорстко задаючи стани поля та викликаючи check_win, виводячи результати через INT 21h. Студент B може рендерити жорстко задане поле для перевірки візуального компонування. <br><a class="internal-link" data-href="How-to/game-logic/Y04.md" href="projects/how-to/game-logic/y04.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: симуляція гравітації, визначення перемоги в 4 напрямках, простий ШІ з пріоритетами.
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Представлення поля:</a> board DB 42 DUP(0). Значення 0 = порожнє, 1 = гравець 1, 2 = гравець 2. Рядковий порядок: клітинка (row, col) = board[row * 7 + col]. Рядок 0 зверху, рядок 5 знизу.
<br><a class="internal-link" data-href="How-to/loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Гравітація:</a> щоб кинути фішку в стовпець C, скануйте board[5*7+C], board[4*7+C], ..., board[0*7+C]. Перший знайдений 0 — цільовий рядок.
<br><a class="internal-link" data-href="How-to/flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Визначення перемоги:</a> після розміщення фішки в (R, C) перевірте чотири напрямки. Для кожного напрямку (dr, dc) з {(0,1), (1,0), (1,1), (1,-1)} підрахуйте послідовні фішки, що збігаються, в обох напрямках від (R, C). Якщо загальний підрахунок &gt;= 4, це перемога.
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Рендеринг поля:</a> використовуйте символи псевдографіки: 0C9h (верхній лівий кут), 0CDh (горизонталь), 0BBh (верхній правий), 0BAh (вертикаль), 0C8h (нижній лівий), 0BCh (нижній правий). Фішки: DB 04h (ромб) або DB 0Fh (сонце) з відповідними атрибутами кольорів.
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Логіка ШІ:</a> для кожного стовпця 0-6: спробувати поставити фішку ШІ, перевірити чи виграє -&gt; зробити цей хід. для кожного стовпця 0-6: спробувати поставити фішку суперника, перевірити чи суперник виграє -&gt; заблокувати. якщо стовпець 3 вільний, обрати його. інакше обрати випадковий допустимий стовпець.
Атрибути кольорів: Гравець 1 (червона фішка): атрибут 0Ch (яскраво-червоний на чорному). Гравець 2 (жовта фішка): атрибут 0Eh (яскраво-жовтий на чорному). Рамка поля: атрибут 07h (сірий на чорному).
Точки синхронізації:
Кінець тижня 8: узгодити структуру поля (рядковий порядок row * 7 + col), значення клітинок (0/1/2), систему координат (рядок 0 зверху, рядок 5 знизу)
Кінець тижня 10: обидва модулі компілюються незалежно; перевірити, що draw_board коректно читає масив board після drop_piece
Кінець тижня 11: перша повна інтеграція — ігровий цикл для двох гравців працює наскрізно (кидок, перевірка перемоги, нічия)
Кінець тижня 13: код завершений, ШІ працює коректно, README чернетка готова; спільне тестування граничних випадків (діагональні перемоги, повні стовпці, нічия)
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/y04-connect-four.html</link><guid isPermaLink="false">Projects/Y04-Connect-Four.md</guid><pubDate>Tue, 24 Feb 2026 16:02:17 GMT</pubDate><enclosure url="assets/y04-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y04-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y07-15-Puzzle]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🟡 Жовтий
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 400-550 (соло) / 600-800 (пара), 2-5 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Маніпуляція масивами</a> з обмінами — переміщення плиток шляхом обміну елементів масиву; індексна адресація (L06/P06)
<br><a class="internal-link" data-href="How-to/loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Вкладені цикли</a> для підрахунку інверсій — цикл порівняння O(n^2), що реалізує математичну умову розв'язності (L04/P04, L06/P06)
<br><a class="internal-link" data-href="How-to/timer-delays.md" href="projects/how-to/timer-delays.html" target="_self" rel="noopener nofollow">Взаємодія з таймером</a> (INT 1Ch або INT 1Ah) — відстеження та відображення часу, що минув (L07/P07)
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Пряме записування у відеопам'ять</a> (B800h) — рендеринг сітки з рамкою, центрованими числами та атрибутами кольору (L08/P08)
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід</a> (INT 16h) — обробка клавіш-стрілок для переміщення плиток (L07/P07)
<br><a class="internal-link" data-href="How-to/number-display.md" href="projects/how-to/number-display.html" target="_self" rel="noopener nofollow">Перетворення числа в рядок</a> — відображення номерів плиток (1-15) як ASCII у кожній клітинці (L04/P04)
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури з кадрами стеку</a> — shuffle, check_solvable, draw_grid, slide_tile (L05/P05)
<br><a class="internal-link" data-href="How-to/random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">Генерація псевдовипадкових чисел</a> — перемішування Фішера-Єйтса з використанням RNG, ініціалізованого таймером (L07)
<br><a class="internal-link" data-href="How-to/flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Умовна логіка та прапорці</a> — перевірки парності, виявлення перемоги, валідація меж для ходів (L04/P04)
<br><a class="internal-link" data-href="How-to/game-logic/Y07.md" href="projects/how-to/game-logic/y07.html" target="_self" rel="noopener nofollow">Ігрова логіка: П'ятнашки</a> — підрахунок інверсій, перемішування Фішера-Єйтса, переміщення плиток та визначення перемоги
Програма реалізує класичну гру «П'ятнашки» (також відому як головоломка з рухомими плитками) у текстовому режимі DOSBox. На екрані відображається сітка 4x4 з плитками, пронумерованими від 1 до 15, та одним порожнім місцем. Кожна плитка відображається як прямокутник з рамкою, використовуючи символи для малювання рамок, достатньо великий, щоб вмістити двозначне число, центроване всередині. Порожнє місце відображається як порожня клітинка з рамкою та іншим відтінком фону. Рядок стану під сіткою показує лічильник ходів та час, що минув.Гравець використовує клавіші-стрілки для переміщення плитки на порожнє місце. Натискання "Вгору" переміщує плитку під порожнім місцем угору на позицію порожнього місця (а порожнє місце рухається вниз). Сітка оновлюється миттєво. Коли всі плитки розташовані в порядку (1-15 зліва направо, зверху вниз, порожнє місце в правому нижньому куті), гра відображає повідомлення "YOU WIN" з фінальною кількістю ходів та часом.Критична алгоритмічна вимога полягає в тому, що перемішування має створювати розв'язну конфігурацію. Нерозв'язний початковий стан назавжди позбавить гравця шансу на перемогу. Студент має реалізувати перевірку парності підрахунку інверсій: головоломка розв'язна тоді і тільки тоді, коли кількість інверсій (пар плиток, де плитка з більшим номером передує плитці з меншим номером у порядку читання) має певну парність відносно рядка порожнього місця. Це конкретна вправа з перекладу математичної умови в код асемблера з вкладеними циклами та умовним підрахунком.<br><img alt="Ігровий процес" src="assets/y07-screen-gameplay.png" target="_self"><br><img alt="Екран перемоги" src="assets/y07-screen-win.png" target="_self">
Підхід A: Випадково перемішати, потім порахувати інверсії за допомогою вкладеного циклу для перевірки парності, перемішуючи повторно, якщо нерозв'язна.
Підхід B: Почати з розв'язаного стану та виконати велику кількість випадкових дійсних ходів (наприклад, 1000 випадкових переміщень), що гарантує розв'язність за побудовою.
Що вимірювати/спостерігати: Складність коду (вкладений цикл з умовним підрахунком проти простий цикл ходів), час генерації дошки та якість випадковості. Підхід з обмінами від розв'язаного стану завжди створює розв'язну дошку, але може генерувати менш рівномірно розподілені головоломки. Підхід A: Один масив з 16 елементів, де board[i] повертає номер плитки на позиції i. Знаходження порожнього місця потребує лінійного пошуку.
Підхід B: Два масиви: board[i] та tile_pos[t], де tile_pos[0] завжди повертає позицію порожнього місця.
Що вимірювати/спостерігати: Вартість знаходження порожнього місця при кожному ході (O(16) сканування проти O(1) пошук), накладні витрати на підтримку двох масивів синхронізованими, та простота реалізації перевірки перемоги. Підхід A: При кожному ході перемалювати всі 16 клітинок.
Підхід B: Перемалювати лише дві клітинки, які змінились (плитку, що перемістилась, та нову позицію порожнього місця).
Що вимірювати/спостерігати: Кількість записів у відеопам'ять за хід. Для сітки 4x4 з клітинками 7 символів x 3 рядки, повне перемалювання записує 336 пар символ+атрибут; оновлення обміну записує 42. Це конкретна різниця у 8 разів.
<br>Розподіл модулів (див. <a class="internal-link" data-href="How-to/include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">Підключення файлів: INCLUDE</a>):
Студент A: Ігрова логіка — представлення дошки, процедура перемішування, перевірка розв'язності (підрахунок інверсій), логіка переміщення плиток, виявлення перемоги, лічильник ходів
Студент B: Відображення та взаємодія — рендеринг сітки з символами рамок, форматування та центрування чисел, атрибути кольору, обробник клавіатури, відображення таймера, екран перемоги, логіка перезапуску
Точки інтеграції: масив дошки (16 байт) та змінні стану гри (позиція порожнього місця, кількість ходів, час початку, фаза гри). Студент A модифікує дошку; Студент B читає її для рендерингу. Визначте розташування дошки (за рядками, індексація з 0, 0 = порожнє місце) разом.
Незалежне тестування: Студент A може тестувати перемішування та розв'язність, друкуючи дошку та кількість інверсій через INT 21h. Студент B може рендерити жорстко закодовану дошку для перевірки візуального розташування, відступів та кольорів. <br><a class="internal-link" data-href="How-to/game-logic/Y07.md" href="projects/how-to/game-logic/y07.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: перевірка розв'язності через підрахунок інверсій, перемішування Фішера-Єйтса, переміщення плиток, визначення перемоги.
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Представлення дошки:</a> board DB 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,0. Значення 0 = порожнє місце. Позиція 0 = верхній лівий кут, позиція 15 = нижній правий кут. За рядками: позиція row * 4 + col.
<br><a class="internal-link" data-href="How-to/loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Підрахунок інверсій:</a> вкладений цикл. Зовнішній: i від 0 до 14. Внутрішній: j від i+1 до 15. Якщо board[i] &gt; board[j] і жоден не дорівнює 0 (порожнє місце), збільшити лічильник інверсій. Використовуйте CMP та умовний JMP.
Правило розв'язності для 4x4: нехай inv = підрахунок інверсій, blank_row_from_bottom = 4 - (blank_position / 4). Розв'язна тоді і тільки тоді, коли (inv + blank_row_from_bottom) парне.
<br><a class="internal-link" data-href="How-to/random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">Перемішування Фішера-Єйтса:</a> for i = 15 downto 1: j = random(0..i); swap board[i], board[j]. Використовуйте підрахунок тактів INT 1Ah як початкове значення; генеруйте псевдовипадкові числа за допомогою простого ЛКГ або модулярної арифметики.
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Переміщення плитки:</a> знайдіть позицію порожнього місця (пошук 0 у масиві, або відстежуйте у змінній). При натисканні клавіші-стрілки: обчисліть позицію плитки для переміщення. Якщо в межах, обміняйте board[blank_pos] з board[tile_pos], оновіть blank_pos.
<br><a class="internal-link" data-href="How-to/timer-delays.md" href="projects/how-to/timer-delays.html" target="_self" rel="noopener nofollow">Відображення таймера:</a> збережіть підрахунок тактів (з INT 1Ah AH=00h, CX:DX) на початку гри. При кожному кадрі зчитайте поточний підрахунок тактів, відніміть початковий, поділіть на 18, щоб отримати приблизні секунди. Відобразіть як MM:SS.
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Символи рамок:</a> верхній лівий кут = 0C9h, горизонтальна лінія = 0CDh, верхній правий = 0BBh, вертикальна лінія = 0BAh, нижній лівий = 0C8h, нижній правий = 0BCh, T-з'єднання = 0CBh (верхнє-T), 0CAh (нижнє-T), 0CCh (ліве-T), 0B9h (праве-T), хрестоподібне = 0CEh.
Точки синхронізації:
Кінець тижня 9: узгодити формат дошки (board DB 16 DUP(?)), конвенцію 0=порожнє місце, прототипи процедур (shuffle_board, slide_tile, draw_grid, draw_cell) та спільні змінні (blank_pos, move_count, game_phase)
Кінець тижня 11: перша інтеграція — ігровий цикл (перемішування → рендеринг → ввід → рух → перемалювання) працює наскрізно; плитки рухаються коректно
Кінець тижня 12: повна гра працює з перевіркою перемоги, таймером та перезапуском; обидва студенти грають і перевіряють розв'язність кожного перемішування
Кінець тижня 14: звіт та відео завершені; взаємна перевірка коду перед здачею
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/y07-15-puzzle.html</link><guid isPermaLink="false">Projects/Y07-15-Puzzle.md</guid><pubDate>Tue, 24 Feb 2026 16:02:14 GMT</pubDate><enclosure url="assets/y07-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y07-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[G01-Tic-Tac-Toe]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🟢 Зелений
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 400-550 (соло) / 600-800 (пара), 2-4 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять</a> (запис слова: символ + байт атрибуту за адресою B800h) — L08/P08
<br><a class="internal-link" data-href="How-to/segment-registers.md" href="projects/how-to/segment-registers.html" target="_self" rel="noopener nofollow">Сегментні регістри</a> (ES встановлюється на B800h для доступу до відеопам'яті) — L03/P03
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масив як стан гри</a> (9-байтовий масив у сегменті даних, значення 0/1/2 для порожнього/X/O) — L06/P06
<br><a class="internal-link" data-href="How-to/addressing-modes.md" href="projects/how-to/addressing-modes.html" target="_self" rel="noopener nofollow">Режими адресації</a> (база + індекс для відображення позиції на дошці у зміщення у відеопам'яті) — L06/P06
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури зі стековими кадрами</a> (draw_board, check_win, ai_move як CALL/RET з параметрами на стеку) — L05/P05
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід через INT 16h</a> (AH=00h, зчитування клавіші) — L07/P07
<br><a class="internal-link" data-href="How-to/dos-services.md" href="projects/how-to/dos-services.html" target="_self" rel="noopener nofollow">Сервіси DOS</a> (INT 21h для завершення програми, виведення рядків як запасний варіант) — L07/P07
<br><a class="internal-link" data-href="How-to/flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Умовна логіка та прапорці</a> (CMP + умовні переходи для виявлення перемоги, валідації вводу) — L04/P04
<br><a class="internal-link" data-href="How-to/timer-delays.md" href="projects/how-to/timer-delays.html" target="_self" rel="noopener nofollow">Системний годинник через INT 1Ah</a> (AH=00h, зчитування лічильника тактів для затримки) — L07/P07
<br><a class="internal-link" data-href="How-to/game-logic/G01.md" href="projects/how-to/game-logic/g01.html" target="_self" rel="noopener nofollow">Ігрова логіка: хрестики-нулики</a> — виявлення перемоги через таблицю пошуку, ШІ на основі правил, визначення нічиї
<br>Див. також <a class="internal-link" data-href="Y04-Connect-Four.md" href="projects/y04-connect-four.html" target="_self" rel="noopener nofollow">Y04 Connect Four</a> (Жовта складність) — подібне виявлення перемоги на сітці, але додає механіку гравітації та більшу дошку. Якщо хочете більше виклику, розгляньте Y04.Програма відображає сітку 3x3 для гри в хрестики-нулики, намальовану безпосередньо у відеопам'яті за сегментом B800h. Сітка розташована по центру текстового екрана 80x25, з використанням символів рамки (одинарні або подвійні лінії) для формування меж клітинок. Кожна клітинка достатньо велика для відображення символу «X» або «O» з кольоровим атрибутом — наприклад, X яскраво-блакитним і O яскраво-червоним на темному фоні.Гравець використовує клавіші з цифрами 1-9 (які відповідають позиціям на сітці, відображеним у порожніх клітинках як тьмяно-сірі підказки) для розміщення свого знака. Комп'ютерний супротивник відповідає негайно своїм ходом. ШІ використовує стратегію на основі правил: спочатку перевіряє, чи може виграти, потім перевіряє, чи потрібно заблокувати суперника, потім віддає перевагу центру, потім кутам, потім ребрам. Після кожного ходу дошка оновлюється миттєво через прямий запис у відеопам'ять. Коли гра завершується (перемога або нічия), виграшна лінія підсвічується окремим кольором фону, і повідомлення з результатом з'являється під сіткою.Табло рахунку у верхній частині екрана (рядок 0) показує "Player: NN Computer: NN Draws: NN" через прямий запис у відеопам'ять. Після відображення результату протягом двох секунд (опитування через системний годинник INT 1Ah) програма виводить запит "Play again? (Y/N)" і або скидає дошку, або коректно завершує роботу через DOS за допомогою INT 21h/4Ch.<br><img alt="Ігровий процес — сітка 3x3 з табло рахунку та повідомленням про перемогу" src="assets/g01-screen-gameplay.png" target="_self">
Підхід A: Малювання всієї дошки за допомогою викликів переривань BIOS (INT 10h з позиціонуванням курсора через AH=02h, потім виведення символу через AH=09h).
Підхід B: Прямі записи MOV у ES:[DI], де ES=B800h.
Що виміряти/спостерігати: Підрахуйте кількість тактів годинника (INT 1Ah) для перемалювання всієї дошки 100 разів кожним методом. Прямий запис у пам'ять має бути значно швидшим, оскільки кожний виклик BIOS включає накладні витрати переривання (збереження прапорців, збереження CS:IP, пошук у таблиці векторів переривань, IRET). Обговоріть: коли виклики BIOS все ж будуть кращими (портативність між відеорежимами). Підхід A: Зберігання стану гри як плоского масиву DB із 9 байтів (індекс = рядок*3 + стовпець).
Підхід B: Масив 3x3 зі слів (DW, 18 байтів, одне слово на клітинку).
Що виміряти/спостерігати: Порівняйте складність коду для перевірки перемоги (плоский масив використовує обчислювані зміщення з ADD; матриця слів має інший крок), використання пам'яті (9 байтів проти 18 байтів) та як кожний підхід відображається на обчислення зміщення у відеопам'яті. Спостерігайте, який підхід дає простішу адресацію та менше інструкцій у процедурі перевірки перемоги. Підхід A: Ланцюжок перевірок CMP/JE, що тестує перемогу-потім-блокування-потім-центр-потім-кут послідовно.
Підхід B: Цикл, що оцінює кожну порожню клітинку (наприклад, +10, якщо виграє, +8, якщо блокує, +5 за центр, +3 за кут, +1 за ребро) та обирає найвищу оцінку.
Що виміряти/спостерігати: Порівняйте кількість виконаних інструкцій за хід ШІ (трасування в Turbo Debugger), довжину коду та наскільки легко кожний підхід узагальнюється для більшої дошки.
<br>Розподіл модулів (див. <a class="internal-link" data-href="How-to/include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">Підключення файлів: INCLUDE</a>):
Студент A — Модуль відображення та вводу: малювання сітки, рендеринг X/O/підказок з атрибутами, табло, зчитування клавіатурного вводу, валідація вводу, запит на повторну гру. Файл: display.asm.
Студент B — Модуль ігрової логіки та ШІ: керування масивом стану гри, процедура виявлення перемоги/нічиї, процедура вибору ходу ШІ, відстеження рахунку. Файл: game.asm.
Спільний головний модуль: ігровий цикл (чергування ходів, виклик процедур відображення та гри), ініціалізація, завершення програми. Файл: main.asm.
Точки інтеграції:
Узгодьте формат масиву стану гри (зміщення, значення байтів для порожнього/X/O) як спільний контракт даних.
Визначте угоди про виклик процедур завчасно: які регістри містять параметри, які зберігаються викликаною процедурою.
Головний цикл викликає draw_board, get_player_move, ai_move та check_win — обидва партнери повинні узгодити сигнатури цих процедур до контрольної точки KT-2. <br><a class="internal-link" data-href="How-to/game-logic/G01.md" href="projects/how-to/game-logic/g01.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: виявлення перемоги через таблицю 8 ліній, ШІ на основі правил (виграш/блокування/центр/кут), визначення нічиї.
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Розміщення відеопам'яті:</a> У текстовому режимі 3 (80x25) сегмент B800h містить 4000 байтів. Кожна символьна клітинка — це 2 байти: байт 0 = ASCII-код, байт 1 = атрибут. Формат атрибуту: біти 7-4 = колір фону, біти 3-0 = колір переднього плану. Використовуйте MOV AX, 0B800h / MOV ES, AX для <a class="internal-link" data-href="How-to/segment-registers.md" href="projects/how-to/segment-registers.html" target="_self" rel="noopener nofollow">налаштування сегмента</a>.
Символи рамки: CP437 включає символи одинарної рамки: горизонтальний C4h, вертикальний B3h, верхній лівий DAh, верхній правий BFh, нижній лівий C0h, нижній правий D9h, хрест C5h, Т-елементи (C2h, C1h, B4h, C3h). Вони створюють акуратні лінії сітки.
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатура через INT 16h:</a> MOV AH, 00h / INT 16h очікує натискання клавіші. AL повертає ASCII-код. Клавіші '1'-'9' мають ASCII-коди 31h-39h. Відніміть 31h, щоб отримати індекс дошки з нуля.
<br><a class="internal-link" data-href="How-to/timer-delays.md" href="projects/how-to/timer-delays.html" target="_self" rel="noopener nofollow">Системний годинник для затримки:</a> MOV AH, 00h / INT 1Ah повертає лічильник тактів у CX:DX (18,2 такти на секунду). Щоб зачекати ~2 секунди, запишіть поточний такт, потім опитуйте в <a class="internal-link" data-href="How-to/loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">циклі</a>, доки не мине 36 тактів.
<br>Відображення дошки на екран: Якщо верхня ліва клітинка сітки починається на рядку 8 екрана, стовпці 20, і кожна клітинка має ширину 6 стовпців та висоту 3 рядки, то клітинка (r, c) відображається на позицію екрана (8 + r4, 20 + c8) приблизно. Використовуйте <a class="internal-link" data-href="How-to/addressing-modes.md" href="projects/how-to/addressing-modes.html" target="_self" rel="noopener nofollow">режими адресації</a> для обчислення зміщень. Заплануйте точне розташування на папері спочатку.
<br><a class="internal-link" data-href="How-to/lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиця пошуку виграшних ліній:</a> Визначте win_lines DB 0,1,2, 3,4,5, ... (24 байти загалом для 8 трійок). У <a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">процедурі</a> перевірки використовуйте SI для проходу по таблиці, завантажуючи три індекси за ітерацію та <a class="internal-link" data-href="How-to/flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">порівнюючи</a> значення дошки за цими позиціями.
Точки синхронізації:
Кінець тижня 9: формат масиву board (зміщення, значення 0/1/2), сигнатури всіх 4 ключових процедур (draw_board, get_player_move, ai_move, check_win), угоди про збереження регістрів
Кінець тижня 11: перша інтеграція — main.asm викликає обидва модулі, ігровий цикл працює від початку до кінця (хід гравця → хід ШІ → перевірка перемоги)
Кінець тижня 13: код повністю працює (рахунок, повторна гра, підсвітка перемоги), чернетка звіту з діаграмою пам'яті
Кінець тижня 14: звіт фіналізовано, відео записані, фінальне тестування завершене
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/g01-tic-tac-toe.html</link><guid isPermaLink="false">Projects/G01-Tic-Tac-Toe.md</guid><pubDate>Tue, 24 Feb 2026 16:02:12 GMT</pubDate><enclosure url="assets/g01-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/g01-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y03-Minesweeper]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🟡 Жовтий
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 450-600 рядків коду (соло) / 650-900 рядків коду (пара), 2-5 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Двовимірний масив з кількома паралельними масивами</a> — окремі масиви для позицій мін, підрахунку сусідів, стану відкриття та стану прапорців — L06/P06
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Рекурсивна заливка через стек</a> — реалізація рекурсії (або явної ітерації на основі стека) для каскадного відкриття нульових клітинок — L05/P05
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять</a> (B800h) — кольорові числа, підсвічування курсору, рендеринг клітинок — L08/P08
<br><a class="internal-link" data-href="How-to/addressing-modes.md" href="projects/how-to/addressing-modes.html" target="_self" rel="noopener nofollow">Режими адресації</a> — базово-індексна адресація для доступу до 2D-сітки зі змінними розмірами — L06/P06
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід через INT 16h</a> — рух курсору, відкриття, перемикання прапорця — L07/P07
<br><a class="internal-link" data-href="How-to/random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">Генерація псевдовипадкових чисел</a> — випадкове розміщення мін при старті гри — L07
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури з параметрами</a> — reveal(row, col), count_neighbors(row, col), draw_cell(row, col) — L05/P05
<br><a class="internal-link" data-href="How-to/flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Умовна логіка та прапорці</a> — широке використання CMP/JE/JNE для перевірок стану гри — L04/P04
<br><a class="internal-link" data-href="How-to/timer-delays.md" href="projects/how-to/timer-delays.html" target="_self" rel="noopener nofollow">Таймер</a> (INT 1Ah або INT 1Ch) — відстеження часу для відображення статусу — L07/P07
<br><a class="internal-link" data-href="How-to/game-logic/Y03.md" href="projects/how-to/game-logic/y03.html" target="_self" rel="noopener nofollow">Ігрова логіка: Сапер</a> — заливка з явним стеком, безпека першого кліку, підрахунок сусідніх мін
Програма — це повноцінний клон гри «Сапер» у текстовому режимі DOSBox. При запуску гравець обирає рівень складності (наприклад, 9x9 з 10 мінами або 16x16 з 40 мінами). Ігрове поле з'являється на екрані як сітка нерозкритих клітинок, кожна відображена як затінений блоковий символ. Курсор, підсвічений окремим кольором фону, переміщується стрілками.Натискання Enter відкриває клітинку під курсором. Якщо вона містить міну, всі міни показуються і гра закінчується. Якщо клітинка має сусідні міни, з'являється кольорове число (1 — синій, 2 — зелений, 3 — червоний тощо, відповідно до класичної кольорової схеми «Сапера» — все досягається через байти атрибутів відеопам'яті). Якщо клітинка не має сусідніх мін, алгоритм заливки рекурсивно відкриває всі з'єднані порожні клітинки, створюючи ефектний каскад розкритої території. Натискання 'F' перемикає прапорець на нерозкритій клітинці, позначаючи підозрювану міну.Рядок стану показує кількість мін без прапорців та час, що минув. Гра визначає перемогу, коли всі клітинки без мін відкриті. Фінальний екран показує поле та вітальне повідомлення або повідомлення про програш з можливістю зіграти знову.<br><img alt="Ігровий процес" src="assets/y03-screen-gameplay.png" target="_self"><br><img alt="Кінець гри" src="assets/y03-screen-gameover.png" target="_self">
Підхід A: Справжня рекурсія використовує апаратний стек: кожен виклик записує IP, BP та локальні змінні, споживаючи значний стековий простір для великих порожніх регіонів.
Підхід B: Явний підхід використовує окремий масив байтів як чергу/стек пар (row, col).
Що виміряти/спостерігати: Максимальна площа заливки до переповнення стека, зрозумілість коду та продуктивність. На сітці 16x16 глибина рекурсії може досягати 256 — кожен кадр використовує 6-10 байтів стека, що означає 1.5-2.5 КБ стекового простору, який може переповнитися зі стандартним сегментом стека. Підхід A: Упакувати весь стан клітинки в один байт (наприклад, біт 7 = міна, біти 3-0 = підрахунок, біт 6 = відкрита, біт 5 = з прапорцем).
Підхід B: Використовувати окремі масиви для кожної властивості.
Що виміряти/спостерігати: Використання пам'яті, складність коду для доступу до кожного поля (побітові AND/OR/SHR проти простий індекс масиву) та зручність налагодження в Turbo Debugger. Підхід A: Після заливки, що відкриває 30 клітинок, перемалювати всі 81 клітинку (9x9) у відеопам'ять.
Підхід B: Відстежувати, які клітинки змінилися, та оновлювати лише їх.
Що виміряти/спостерігати: Записи у відеопам'ять для типового відкриття в середині гри. Різниця особливо помітна на більших полях (16x16 = 256 клітинок).
<br>Розподіл модулів (див. <a class="internal-link" data-href="How-to/include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">Підключення файлів: INCLUDE</a>):
Студент A: Ігрова логіка — розміщення мін, обчислення підрахунку сусідів, логіка відкриття із заливкою, визначення перемоги/програшу, перемикання прапорців, керування станом гри
Студент B: Дисплей та ввід — рендеринг у відеопам'ять (сітка, числа, прапорці, міни, курсор), таблиця кольорових атрибутів, обробник клавіатури, рядок стану (кількість мін, таймер), екран меню, екрани кінця гри/перемоги
Точки інтеграції: спільний сегмент даних з масивами ігрового поля та змінними стану гри (позиція курсору, фаза гри, кількість мін). Узгодьте формат масиву (рядковий порядок, один байт на клітинку або паралельні масиви) перед розділенням роботи.
Незалежне тестування: Студент A може тестувати заливку, відкриваючи клітинки і виводячи стан поля в консоль через INT 21h. Студент B може рендерити жорстко задане поле для перевірки кольорів та компонування. <br><a class="internal-link" data-href="How-to/game-logic/Y03.md" href="projects/how-to/game-logic/y03.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: заливка (flood fill) з явним стеком, безпека першого кліку, підрахунок сусідніх мін.
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Структура даних сітки:</a> для сітки 9x9 використовуйте mines DB 81 DUP(0), counts DB 81 DUP(0), revealed DB 81 DUP(0), flagged DB 81 DUP(0). Клітинка (r,c) знаходиться за зміщенням r * width + c.
<br><a class="internal-link" data-href="How-to/random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">Розміщення мін:</a> використовуйте лічильник, що розміщує N мін. Генеруйте випадкову позицію (лічильник тактів INT 1Ah, за модулем розміру сітки). Якщо вже замінована, спробуйте наступну клітинку.
<br><a class="internal-link" data-href="How-to/loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Підрахунок сусідів:</a> після розміщення всіх мін пройдіть по кожній клітинці. Для кожної клітинки без міни підрахуйте міни в 8 сусідніх клітинках. Збережіть підрахунок у масиві counts.
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Заливка:</a> при відкритті клітинки (r,c) і counts[r*w+c] == 0, запишіть усіх 8 сусідів у стек (окремий масив), потім цикл: вилучити клітинку, якщо не відкрита і не міна, відкрити. Якщо її підрахунок дорівнює 0, записати сусідів. Продовжувати, поки стек не порожній.
<br><a class="internal-link" data-href="How-to/lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиця кольорів для чисел:</a> num_colors DB 09h, 0Ah, 0Ch, 0Ch, 01h, 04h, 06h, 0Fh (1=яскраво-синій, 2=зелений, 3=червоний, 4=яскраво-червоний, 5=темно-синій, 6=темно-червоний, 7=коричневий, 8=білий). Використовуйте підрахунок сусідів як індекс.
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Підсвічування курсору:</a> при малюванні клітинки курсору застосуйте OR до байта атрибуту з 70h (білий фон) для підсвічування. Відновіть оригінальний атрибут, коли курсор переміститься.
<br><a class="internal-link" data-href="How-to/timer-delays.md" href="projects/how-to/timer-delays.html" target="_self" rel="noopener nofollow">Таймер:</a> INT 1Ah AH=00h повертає лічильник тактів у CX:DX. Збережіть початковий такт; відніміть для отримання тактів, що минули. Поділіть на 18 для наближення секунд.
Точки синхронізації:
Кінець тижня 8: узгодити структуру масивів (паралельні масиви проти упакований байт), формат зміщення (r * width + c), угоди про виклик процедур
Кінець тижня 10: обидва модулі компілюються незалежно; перевірити, що draw_cell коректно читає масиви counts, revealed, flagged
Кінець тижня 11: перша повна інтеграція — ігровий цикл працює наскрізно (відкриття, заливка, кінець гри)
Кінець тижня 13: код завершений, README чернетка готова; спільне тестування граничних випадків (перший клік на міну, повна заливка)
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/y03-minesweeper.html</link><guid isPermaLink="false">Projects/Y03-Minesweeper.md</guid><pubDate>Tue, 24 Feb 2026 16:02:12 GMT</pubDate><enclosure url="assets/y03-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y03-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y02-Conways-Game-of-Life]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🟡 Жовтий
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 450-600 рядків коду (соло) / 650-850 рядків коду (пара), 2-5 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Маніпулювання двовимірними масивами</a> — представлення та індексування сітки 80x25 (або 80x50) як плоского масиву байтів — L06/P06
<br><a class="internal-link" data-href="How-to/addressing-modes.md" href="projects/how-to/addressing-modes.html" target="_self" rel="noopener nofollow">Режими адресації</a> — базово-індексна адресація для доступу до grid[row][col] через обчислені зміщення — L06/P06
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять</a> (B800h) — ефективний рендеринг сітки через запис пар символ+атрибут — L08/P08
<br><a class="internal-link" data-href="How-to/interrupt-handlers.md" href="projects/how-to/interrupt-handlers.html" target="_self" rel="noopener nofollow">Переривання таймера</a> (INT 1Ch) — керування безперервною симуляцією з контрольованою частотою кадрів — L07/P07
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід через INT 16h</a> — неблокуючі перевірки для перемикання режимів, руху курсору, розміщення патернів — L07/P07
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури та модульний дизайн</a> — окремі процедури для підрахунку сусідів, застосування правил, обміну сіток, рендерингу — L05/P05
<br><a class="internal-link" data-href="How-to/bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Побітові операції</a> — потенційне використання упакованих бітових масивів для ефективного зберігання сітки в пам'яті — L04/P04
<br><a class="internal-link" data-href="How-to/loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Вкладені цикли</a> з обчисленням індексів — ітерація по всіх клітинках та їх 8 сусідах з перевіркою меж — L04/P04, L06/P06
<br><a class="internal-link" data-href="How-to/game-logic/Y02.md" href="projects/how-to/game-logic/y02.html" target="_self" rel="noopener nofollow">Ігрова логіка: гра «Життя»</a> — підрахунок сусідів, правила B3/S23, подвійна буферизація, розміщення патернів
<br>Див. також <a class="internal-link" data-href="Y13-Wolfram-Cellular-Automaton.md" href="projects/y13-wolfram-cellular-automaton.html" target="_self" rel="noopener nofollow">Y13 Клітинний автомат Вольфрама</a> — також клітинний автомат, але одновимірний із 256 правилами та побітовою оцінкою правил. Гра Конвея — двовимірна з підрахунком сусідів та інтерактивним редактором.Програма реалізує гру «Життя» Конвея як інтерактивний клітинний автомат у текстовому режимі DOSBox. При запуску екран показує сітку 80x25 (або 80x50, якщо студент перемикає на 50-рядковий режим через INT 10h). Кожна клітинка або жива (відображається як суцільний блоковий символ, наприклад DB 219), або мертва (порожній пробіл). Рядок стану внизу показує лічильник поколінь та поточний режим.Програма має два режими. У режимі редактора користувач переміщує курсор стрілками та перемикає клітинки між живим і мертвим станом пробілом. Кілька попередньо визначених патернів доступні через гарячі клавіші: натискання 'G' розміщує глайдер у позиції курсору, 'B' розміщує мигалку тощо. Патерни зберігаються як масиви байтів у сегменті даних. У режимі симуляції (перемикається натисканням Enter) автомат еволюціонує згідно зі стандартними правилами B3/S23: мертва клітинка з рівно 3 живими сусідами народжується; жива клітинка з 2 або 3 живими сусідами виживає; решта гине. Симуляція може виконуватися покроково (натискання 'S' просуває на одне покоління) або безперервно з частотою, керованою таймером.Візуальний результат зачаровує: патерни еволюціонують, глайдери рухаються по діагоналі через екран, а осцилятори пульсують. Студент створює щось, що виглядає як жива система, із нічого, крім масивів байтів та умовної логіки.<br><img alt="Режим симуляції" src="assets/y02-screen-simulation.png" target="_self"><br><img alt="Режим редактора" src="assets/y02-screen-editor.png" target="_self">
Підхід A: Зберігати кожну клітинку як повний байт (0 або 1).
Підхід B: Упакувати 8 клітинок в один байт.
Що виміряти/спостерігати: Використання пам'яті (2000 байтів проти 250 байтів для 80x25), складність підрахунку сусідів (простий індекс масиву проти витягування бітів через AND/SHR) та обсяг коду. Байтовий підхід простий; бітовий заощаджує пам'ять, але потребує більше інструкцій на кожен доступ до клітинки. Виміряйте кількість виконаних інструкцій на крок покоління для кожного підходу. Підхід A: Після обчислення кожного нового покоління перемалювати всі 2000 клітинок у відеопам'ять.
Підхід B: Порівняти нову сітку зі старою і записати лише клітинки, що змінилися.
Що виміряти/спостерігати: Кількість записів у відеопам'ять на покоління для типового патерну (наприклад, глайдера). При розріджених змінах диференціальний рендеринг записує значно менше байтів. Для хаотичних патернів різниця зменшується. Підхід A: Координати замикаються за модулем розмірів сітки (клітинка в стовпці 79 дивиться на стовпець 0).
Підхід B: Клітинки за межами вважаються завжди мертвими.
Що виміряти/спостерігати: Складність коду обчислення індексів та вплив на патерни, як-от глайдери, що досягають краю.
<br>Розподіл модулів (див. <a class="internal-link" data-href="How-to/include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">Підключення файлів: INCLUDE</a>):
Студент A: Рушій симуляції — структури даних сітки (поточний та наступний масиви), процедура підрахунку сусідів, застосування правил, крокування поколінь, логіка обміну сіток
Студент B: Інтерфейс користувача — режим редактора (рендеринг курсору, рух, перемикання клітинки), обробник клавіатури, розміщення патернів з попередньо визначених, конвеєр рендерингу (сітка у відеопам'ять), відображення рядка стану
Точки інтеграції: Обидва студенти спільно використовують масиви даних сітки та змінну стану (редактор проти симуляція). Визначте формат сітки (масив байтів, рядковий порядок, початкова адреса) та прапорець режиму разом перед початком кодування.
Незалежне тестування: Студент A може перевірити рушій симуляції, жорстко задавши патерн глайдера, виконавши 4 покоління і вивівши сітку через INT 21h. Студент B може тестувати рендеринг та ввід зі статичною сіткою. <br><a class="internal-link" data-href="How-to/game-logic/Y02.md" href="projects/how-to/game-logic/y02.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: підрахунок 8 сусідів, правила B3/S23, подвійна буферизація, розміщення патернів.
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Структура сітки:</a> плоский масив байтів розміром 8025 = 2000 байтів. Клітинка (row, col) знаходиться за зміщенням `row 80 + col. Оголосіть як grid1 DB 2000 DUP(0)таgrid2 DB 2000 DUP(0)`.
<br><a class="internal-link" data-href="How-to/loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Підрахунок сусідів:</a> для клітинки (r, c) перевірте 8 позицій від (r-1,c-1) до (r+1,c+1), пропускаючи (r,c). Підсумуйте значення. Використовуйте вкладений цикл або розгорніть 8 перевірок.
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Відображення у відеопам'ять:</a> клітинка (r, c) відповідає зміщенню відео (r * 80 + c) * 2. Запишіть символ (DB 219 для живої, 20h для мертвої) та байт атрибуту.
Попередньо визначені патерни: зберігайте як масиви пар (зміщеннярядка, зміщеннястовпця) відносно курсору. Глайдер — це 5 клітинок: glider DB 0,1, 1,2, 2,0, 2,1, 2,2 (або подібне). Пройдіть по парах, встановіть grid[cursor_row + row_off][cursor_col + col_off] = 1.
<br><a class="internal-link" data-href="How-to/addressing-modes.md" href="projects/how-to/addressing-modes.html" target="_self" rel="noopener nofollow">Обмін сіток:</a> замість копіювання 2000 байтів, обміняйте два вказівники (або поміняйте, яку мітку ви вважаєте «поточною»). Використовуючи SI для бази поточної сітки та DI для бази наступної, а потім обмінюючи SI та DI після кожного покоління, ви уникаєте копіювання.
50-рядковий режим (необов'язково): MOV AX, 1112h / INT 10h перемикає на шрифт 8x8, що дає 50 рядків. Сітка стає 80x50 = 4000 байтів.
Точки синхронізації:
Кінець тижня 8: узгодити формат масивів сітки (рядковий порядок, row * 80 + col), прапорець режиму, угоди про виклик процедур
Кінець тижня 10: обидва модулі компілюються незалежно; перевірити сумісність сигнатур count_neighbors, draw_grid, swap_grids
Кінець тижня 11: перша повна інтеграція — main.asm викликає обидва модулі, покроковий режим працює наскрізно
Кінець тижня 13: код завершено, README чернетка готова; спільний перегляд перед фінальним тестуванням
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/y02-conways-game-of-life.html</link><guid isPermaLink="false">Projects/Y02-Conways-Game-of-Life.md</guid><pubDate>Tue, 24 Feb 2026 16:02:07 GMT</pubDate><enclosure url="assets/y02-screen-simulation.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y02-screen-simulation.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y01-Snake]]></title><description><![CDATA[
Курс: Архітектура комп'ютерних систем
Складність: 🟡 Жовтий
Макс. команд: 3
Платформа: TASM / DOSBox / Turbo Debugger
Орієнтовний обсяг: 400-550 рядків коду (соло) / 600-800 рядків коду (пара), 2-5 файлів .asm
Працюючи над цим проєктом, ви практикуватимете:
<a class="internal-link" data-href="How-to/interrupt-handlers.md" href="projects/how-to/interrupt-handlers.html" target="_self" rel="noopener nofollow">Перехоплення переривання таймера</a> (INT 1Ch) — перехоплення вектора апаратного переривання, написання обробника переривань, ланцюговий виклик оригінального обробника (L07/P07)
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід</a> (INT 16h) — неблокуюча перевірка клавіш (AH=01h) та зчитування клавіші (AH=00h) для вводу в реальному часі (L07/P07)
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять</a> (B800h) — пари символ + байт атрибуту для відображення ігрового поля без накладних витрат BIOS (L08/P08)
<br><a class="internal-link" data-href="How-to/segment-registers.md" href="projects/how-to/segment-registers.html" target="_self" rel="noopener nofollow">Маніпулювання сегментними регістрами</a> (ES) — встановлення ES на B800h для запису у відеопам'ять (L03/P03, L06/P06)
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви</a> та <a class="internal-link" data-href="How-to/addressing-modes.md" href="projects/how-to/addressing-modes.html" target="_self" rel="noopener nofollow">режими адресації</a> — тіло змійки зберігається як масив; індексна та базово-індексна адресація для обходу (L06/P06)
<br><a class="internal-link" data-href="How-to/procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури та дисципліна стека</a> — модульна ігрова логіка: малювання, рух, зіткнення, розміщення їжі (L05/P05)
<br><a class="internal-link" data-href="How-to/flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Прапорці та умовні переходи</a> — виявлення зіткнень, перевірка напрямку (L04/P04)
<br><a class="internal-link" data-href="How-to/random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">Генерація псевдовипадкових чисел</a> — використання лічильника тактів таймера або простого лінійного конгруентного генератора для розміщення їжі (L07)
<br><a class="internal-link" data-href="How-to/game-logic/Y01.md" href="projects/how-to/game-logic/y01.html" target="_self" rel="noopener nofollow">Ігрова логіка: Змійка</a> — циклічний буфер тіла, зіткнення зі стінами та тілом, розміщення їжі, захист від протилежного напрямку
Програма — це класична гра «Змійка», що працює в текстовому режимі 80x25 у DOSBox. Після запуску гравець бачить обрамлене ігрове поле з короткою змійкою (3-4 сегменти), що рухається вправо. Шматочок їжі, відображений як яскравий символ, розміщений у випадковій позиції на полі. Змійка рухається з постійною швидкістю, керованою системним перериванням таймера (INT 1Ch), тому гра працює незалежно від того, як швидко гравець натискає клавіші.Клавіші зі стрілками змінюють напрямок змійки. Поїдання їжі збільшує змійку на один сегмент, збільшує рахунок і розміщує новий шматочок їжі у випадковому порожньому місці. Рахунок відображається у верхній частині екрана через прямий запис у відеопам'ять за сегментом B800h. Якщо змійка стикається зі стіною або з власним тілом, гра зупиняється і з'являється повідомлення "GAME OVER" разом з фінальним рахунком. Гравець може натиснути клавішу для перезапуску або виходу.Візуальний стиль — текстовий: тіло змійки може складатися з блокових символів (DB 219) зеленого кольору, голова — іншого кольору, їжа — червона зірочка, а рамка намальована символами псевдографіки. Результат — чутлива, плавна аркадна гра, повністю побудована на 16-бітному асемблері.<br><img alt="Ігровий процес" src="assets/y01-screen-gameplay.png" target="_self"><br><img alt="Кінець гри" src="assets/y01-screen-gameover.png" target="_self">
Підхід A: Перехопити INT 1Ch, щоб гра просувалася на кожний N-й такт таймера.
Підхід B: Використати затримку на основі LOOP в основному циклі.
Що виміряти/спостерігати: Чутливість до клавіатурного вводу, стабільність швидкості гри на різних машинах, використання процесора. Підхід із таймером відділяє швидкість гри від швидкості процесора; підхід з активним очікуванням простіший, але крихкий. Підхід A: Зберігати позиції тіла змійки в циклічному буфері (індекси голови/хвоста просуваються, дані не переміщуються).
Підхід B: Зсувати всі елементи кожен кадр (переміщувати кожну позицію вперед, записувати нову голову на початок).
Що виміряти/спостерігати: Кількість записів у пам'ять на кадр зі зростанням змійки. Циклічний буфер — O(1) на кадр; підхід зі зсувом — O(n). Це конкретний, вимірюваний архітектурний компроміс. Підхід A: Перемалювати всі 80x25 клітинок кожен ігровий такт.
Підхід B: Оновлювати лише дві клітинки, що змінилися (старий хвіст стирається, нова голова малюється).
Що виміряти/спостерігати: Кількість записів у відеопам'ять на кадр. Для змійки довжиною 50 повне перемалювання записує 2000 слів; інкрементне — 2 слова. Це демонструє вплив мінімізації вводу/виводу на продуктивність. Студент A: Ігровий рушій — перехоплення/відновлення переривання таймера, основний ігровий цикл, логіка руху змійки, виявлення зіткнень, керування станом гри (гра/пауза/кінець гри)
Студент B: Рендеринг та ввід — процедури малювання у відеопам'ять (рамка, змійка, їжа, рахунок, екран кінця гри), обробник клавіатури, розміщення їжі з генерацією випадкових чисел
Точки інтеграції: Спільний сегмент даних, що містить масив тіла змійки, поточний напрямок, рахунок та прапорець стану гри. Обидва модулі читають/записують цей спільний стан. Визначте структуру даних разом заздалегідь.
Незалежне тестування: Студент A може тестувати логіку руху, виводячи координати через INT 21h до готовності рендерера. Студент B може тестувати процедури малювання з жорстко заданими позиціями. <br><a class="internal-link" data-href="How-to/game-logic/Y01.md" href="projects/how-to/game-logic/y01.html" target="_self" rel="noopener nofollow">Ігрова логіка:</a> Детальний опис алгоритмів: циклічний буфер для тіла змійки, перевірка зіткнень, розміщення їжі, захист від розвороту.
<br><a class="internal-link" data-href="How-to/timer-delays.md" href="projects/how-to/timer-delays.html" target="_self" rel="noopener nofollow">INT 1Ch</a> спрацьовує ~18,2 рази на секунду. Щоб рухати змійку ~4 рази на секунду, рахуйте такти і просувайте змійку кожні 4-5 тактів.
<br><a class="internal-link" data-href="How-to/keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Неблокуюча перевірка клавіатури:</a> MOV AH, 01h / INT 16h — ZF=0 означає, що клавіша очікує. Потім MOV AH, 00h / INT 16h для споживання. Клавіші зі стрілками: AL=0, скан-код в AH (48h/50h/4Bh/4Dh).
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Адреса відеопам'яті</a> для рядка R, стовпця C: offset = (R * 80 + C) * 2. Символ за цим зміщенням, атрибут за зміщенням+1.
<br><a class="internal-link" data-href="How-to/arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Циклічний буфер:</a> підтримуйте head_index та tail_index у масиві пар (X,Y). При русі: просуньте headindex (mod розмірбуфера), запишіть нову позицію голови. Якщо їжу не з'їдено, просуньте tail_index і зітріть цю клітинку. Якщо їжу з'їдено, не просувайте tail_index.
<br><a class="internal-link" data-href="How-to/random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">Випадкове число:</a> зчитайте лічильник тактів таймера з області даних BIOS (0040:006Ch, DWORD) або використовуйте INT 1Ah AH=00h. Застосуйте модульну арифметику для отримання позиції в межах поля.
<br><a class="internal-link" data-href="How-to/interrupt-handlers.md" href="projects/how-to/interrupt-handlers.html" target="_self" rel="noopener nofollow">Збереження/відновлення векторів переривань:</a> використовуйте INT 21h AH=35h (отримати вектор) та AH=25h (встановити вектор), або читайте/записуйте таблицю векторів переривань напряму за адресою 0000:(1Ch*4).
<br><a class="internal-link" data-href="How-to/video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Байти атрибутів:</a> зелений на чорному = 0Ah, червоний на чорному = 0Ch, білий на чорному = 0Fh. Старший напівбайт = фон, молодший напівбайт = передній план.
Точки синхронізації:
Кінець тижня 9: узгодити точну структуру спільного сегмента .DATA (циклічний буфер snake_buf, head_idx, tail_idx, direction, score, game_state, збережений вектор INT 1Ch) та сигнатури всіх PROC
Кінець тижня 11: перша працююча інтеграція — змійка рухається у рамці за таймером, стрілки змінюють напрямок, їжа з'являється
Кінець тижня 12: гра повністю придатна до гри — зростання, зіткнення, рахунок, кінець гри та перезапуск працюють
Кінець тижня 13: код повністю готовий, обидва студенти тестують спільно, README містить аналіз A-vs-B
Project/
├── src/
│ ├── main.asm
│ └── ...
├── README.md
├── checkpoints.md
└── video_presentation.txt Код компілюється TASM і запускається в DOSBox без модифікацій
Звіт (README.md) з аналізом «Підхід A проти B»
Щотижневі контрольні відео у checkpoints.md
Фінальна відеопрезентація (посилання у video_presentation.txt)
]]></description><link>projects/y01-snake.html</link><guid isPermaLink="false">Projects/Y01-Snake.md</guid><pubDate>Tue, 24 Feb 2026 16:02:02 GMT</pubDate><enclosure url="assets/y01-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y01-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y02]]></title><description><![CDATA[Алгоритми підрахунку 8 сусідів, застосування правил B3/S23, подвійної буферизації та розміщення патернів у сітці.
Потрібно обчислити наступне покоління клітинного автомата без спотворення поточного стану
Потрібно коректно підрахувати живих сусідів для кожної клітинки, враховуючи краї сітки
Потрібно розмістити попередньо визначений патерн (глайдер, мигалка тощо) у довільній позиції
Сітка 80x25 зберігається як плоский масив із 2000 байтів. Кожен байт: 0 = мертва, 1 = жива. Потрібні два масиви — поточне та наступне покоління:grid_current DB 2000 DUP(0) ; читаємо з цього
grid_next DB 2000 DUP(0) ; пишемо в цей
Клітинка (row, col) знаходиться за зміщенням row * 80 + col. Після обчислення всього покоління масиви міняються ролями — замість копіювання 2000 байтів, достатньо обміняти два вказівники (SI та DI).Крок покоління: SI → grid_current (читання) SI → grid_next (тепер поточна) DI → grid_next (запис) DI → grid_current (тепер наступна) XCHG SI, DI
Для клітинки (r, c) потрібно перевірити 8 позицій навколо неї. Зміщення сусідів відносно поточної клітинки у плоскому масиві:Перед доступом до сусіда потрібна перевірка меж: якщо r=0, сусідів зверху немає; якщо c=79, сусідів справа немає тощо. Альтернатива — тороїдальна сітка з модульною арифметикою.Після підрахунку сусідів (значення 0-8) застосовуємо правила:Підрахунок = кількість живих сусідів
Якщо клітинка жива: 2 або 3 сусіди → жива (виживає) інакше → мертва (гине)
Якщо клітинка мертва: рівно 3 сусіди → жива (народжується) інакше → мертва (залишається)
Це можна спростити: наступний стан = 1, якщо (count == 3) АБО (count == 2 І клітинка жива).Патерн зберігається як масив пар (dr, dc) — зміщень відносно позиції курсору. Наприклад, глайдер із 5 клітинок:Глайдер: Масив зміщень: . X . glider DB 0,1, 1,2, 2,0, 2,1, 2,2 . . X glider_len EQU 5 X X X
Алгоритм розміщення: для кожної пари (dr, dc) обчислити абсолютну позицію (cursor_row + dr, cursor_col + dc), перевірити межі, записати 1 у сітку.; --- Дані ---
grid_current DB 2000 DUP(0)
grid_next DB 2000 DUP(0) ; --- Підрахунок живих сусідів клітинки ---
; Вхід: SI = база поточної сітки, BX = зміщення клітинки (row*80+col)
; DH = row, DL = col
; Вихід: AL = кількість живих сусідів (0-8)
count_neighbors PROC PUSH BX PUSH CX PUSH DX ; Для кожного з 8 сусідів: перевірити межі, додати [SI+BX+зміщення] до AL ; Зміщення сусідів: -81, -80, -79, -1, +1, +79, +80, +81 ; див. таблицю зміщень в алгоритмі вище ; ... ваш код тут ... POP DX POP CX POP BX RET
count_neighbors ENDP ; --- Застосування правил B3/S23 ---
; Вхід: AL = кількість сусідів, AH = поточний стан (0 або 1)
; Вихід: AL = новий стан (0 або 1)
apply_rules PROC ; Жива, якщо (count == 3) АБО (count == 2 І клітинка жива) ; див. правила B3/S23 в алгоритмі вище ; ... ваш код тут ... RET
apply_rules ENDP ; --- Обмін сіток після покоління ---
; SI = поточна сітка, DI = наступна сітка
swap_grids PROC XCHG SI, DI RET
swap_grids ENDP Оновлення сітки «на місці». Якщо записувати нові стани в той самий масив, з якого читаємо сусідів, результат залежить від порядку обходу. Завжди читайте з поточної сітки, пишіть у наступну. Пропущена перевірка меж для крайніх клітинок. Клітинка (0, 0) має лише 3 сусідів, а не 8. Без перевірки row &gt; 0 / col &gt; 0 код читає пам'ять за межами масиву. Копіювання масиву замість обміну вказівників. Копіювання 2000 байтів кожне покоління марнує час. XCHG SI, DI виконується за 1 такт. Патерн виходить за межі сітки. Якщо курсор у позиції (23, 78), а патерн має зміщення (2, 2), запис вийде за межі масиву. Перевіряйте кожну клітинку патерну окремо. <a class="internal-link" data-href="../arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви</a>
<br><a class="internal-link" data-href="../loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Цикли</a>
<br><a class="internal-link" data-href="../bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Побітові операції</a>
<br><a class="internal-link" data-href="../addressing-modes.md" href="projects/how-to/addressing-modes.html" target="_self" rel="noopener nofollow">Режими адресації</a>
<br><a class="internal-link" data-href="Y13.md" href="projects/how-to/game-logic/y13.html" target="_self" rel="noopener nofollow">Ігрова логіка: клітинний автомат Вольфрама</a>
<br>
Корисно для: <a class="internal-link" data-href="../../Y02-Conways-Game-of-Life.md" href="projects/y02-conways-game-of-life.html" target="_self" rel="noopener nofollow">Y02 Гра «Життя» Конвея</a>
]]></description><link>projects/how-to/game-logic/y02.html</link><guid isPermaLink="false">Projects/How-to/game-logic/Y02.md</guid><pubDate>Tue, 24 Feb 2026 14:16:58 GMT</pubDate></item><item><title><![CDATA[Y13]]></title><description><![CDATA[Алгоритми бітового індексування правила за 3-бітовим сусідством, обчислення покоління та обробки граничних клітин (обгортання або нульові межі).
Потрібно оцінити правило елементарного клітинного автомату: за 3 сусідніми клітинами визначити стан нової клітини.
Потрібно обчислити ціле покоління з 80 клітин за попереднім поколінням.
Потрібно обробити граничні клітини (стовпець 0 та стовпець 79), у яких бракує одного сусіда.
Потрібно відобразити номер правила у двійковому вигляді або візуалізувати таблицю правила.
Кожне з 256 правил Вольфрама кодується одним байтом. Три сусідні клітини (ліва, центральна, права) утворюють 3-бітовий індекс (0-7). Відповідний біт байта правила визначає вихідну клітину.Приклад для правила 110 (двійково: 01101110):Алгоритм оцінки для клітини у стовпці C:left = gen[C - 1]
center = gen[C]
right = gen[C + 1] index = (left SHL 2) OR (center SHL 1) OR right ; 3-бітовий індекс 0-7 result = (rule SHR index) AND 1 ; перевірити відповідний біт
На 8086 зсув на змінну величину вимагає лічильник у CL:MOV CL, index
MOV AL, rule
SHR AL, CL
AND AL, 1 ; AL = нова клітина (0 або 1)
Замість зсуву під час оцінки кожної клітини можна попередньо побудувати 8-байтову таблицю при виборі правила:; Побудувати таблицю: rule_table[i] = (rule &gt;&gt; i) AND 1
MOV CX, 8
MOV AL, [rule]
MOV SI, OFFSET rule_table
build_loop: MOV BL, AL AND BL, 1 MOV [SI], BL SHR AL, 1 INC SI LOOP build_loop
Оцінка стає одним зверненням до пам'яті: MOV AL, [rule_table + BX], де BX = індекс сусідства.Стовпець 0 не має лівого сусіда, стовпець 79 — правого. Є два підходи:Обгортання (wrap-around): лівий сусід стовпця 0 — це стовпець 79, правий сусід стовпця 79 — стовпець 0. Візуально цікавіше, бо масив клітин утворює кільце.Нульові межі (dead boundary): відсутній сусід вважається 0 (мертвий). Простіше реалізувати.; Обгортання для стовпця 0
cmp bx, 0
jne not_left_edge
mov al, [cur_gen + 79] ; лівий сусід = останній стовпець
jmp got_left
not_left_edge:
mov al, [cur_gen + bx - 1] ; звичайний лівий сусід
got_left:
Повний цикл обчислює 80 нових клітин із попереднього покоління:Для C = 0 до 79: 1. Визначити left, center, right (з обробкою меж) 2. Сформувати 3-бітовий індекс 3. Оцінити правило 4. Записати результат у next_gen[C]
Обміняти cur_gen і next_gen
Використовуються два буфери (cur_gen, next_gen) по 80 байтів. Після обчислення буфери обмінюються (копіювання через REP MOVSB або обмін вказівників).; --- Дані ---
GEN_SIZE EQU 80
cur_gen DB GEN_SIZE DUP(0)
next_gen DB GEN_SIZE DUP(0)
rule DB 110 ; номер правила (0-255)
; --- Обчислення одного покоління ---
; Вхід: cur_gen (80 байтів), rule (байт)
; Вихід: next_gen (80 байтів) заповнений, потім обмін буферів
compute_next_gen PROC push bx push cx push dx push si push di ; ... ваш код тут ... ; Для кожного стовпця 0-79: ; 1. Отримати left, center, right (з обгортанням на межах) ; 2. Сформувати 3-бітовий індекс — див. алгоритм вище ; 3. Оцінити правило: SHR rule, CL; AND AL, 1 ; 4. Записати результат у next_gen ; Після циклу: скопіювати next_gen у cur_gen (REP MOVSB) pop di pop si pop dx pop cx pop bx ret
compute_next_gen ENDP Читання за межами масиву на граничних клітинах. Доступ до gen[-1] або gen[80] зчитує випадкові дані з пам'яті. Завжди перевіряти стовпець 0 та 79 окремо.
Лічильник зсуву не в CL. Інструкція SHR AL, CL вимагає лічильник саме у регістрі CL. Якщо індекс сусідства залишився в іншому регістрі, зсув буде неправильним.
Запис у cur_gen під час обчислення. Якщо записувати нові значення у той самий масив, з якого читаються сусіди, результат буде спотворений. Обов'язково використовувати два окремих буфери.
Неправильний порядок бітів при формуванні індексу. Індекс = (left SHL 2) OR (center SHL 1) OR right. Якщо переплутати порядок (наприклад, right у старших бітах), правило оцінюватиметься дзеркально. <a class="internal-link" data-href="../bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Побітові операції</a>
<br><a class="internal-link" data-href="../arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви</a>
<br><a class="internal-link" data-href="../loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Цикли</a>
<br><a class="internal-link" data-href="../video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять</a>
<br><a class="internal-link" data-href="../number-display.md" href="projects/how-to/number-display.html" target="_self" rel="noopener nofollow">Числовий ввід/вивід</a>
<br><a class="internal-link" data-href="Y02.md" href="projects/how-to/game-logic/y02.html" target="_self" rel="noopener nofollow">Y02 Гра «Життя» — ігрова логіка</a>
<br>
Корисно для: <a class="internal-link" data-href="../../Y13-Wolfram-Cellular-Automaton.md" href="projects/y13-wolfram-cellular-automaton.html" target="_self" rel="noopener nofollow">Y13 Дослідник клітинних автоматів Вольфрама</a>
]]></description><link>projects/how-to/game-logic/y13.html</link><guid isPermaLink="false">Projects/How-to/game-logic/Y13.md</guid><pubDate>Tue, 24 Feb 2026 14:16:55 GMT</pubDate></item><item><title><![CDATA[Y12]]></title><description><![CDATA[Алгоритми сканування примусового взяття, ланцюжків множинних стрибків, перетворення в дамку та правил напрямку руху за типом фішки.
Потрібно визначити всі дозволені ходи для фішки з урахуванням діагональних напрямків
Потрібно реалізувати примусове взяття та ланцюжки множинних стрибків
Потрібно розрізняти рух звичайних фішок (лише вперед) та дамок (в усіх напрямках)
Дошка зберігається як масив із 64 байт. Індекс клітинки: row * 8 + col. Фішки розміщуються лише на темних клітинках (де (row + col) непарне).Кодування кожного байта:0 = порожня клітинка
1 = біла фішка (біт 0 встановлений)
2 = біла дамка (біти 0 і 1 встановлені)
5 = червона фішка (біт 2 встановлений)
6 = червона дамка (біти 1 і 2 встановлені)
Побітові перевірки:
TEST AL, 04h — чи фішка червона (біт 2)?
TEST AL, 02h — чи це дамка (біт 1)?
TEST AL, 01h — чи є фішка взагалі?
Замість жорстко закодованих перевірок 4 напрямків використовується таблиця:dir_table DB -1, -1 ; NW (вперед-вліво для білих) DB -1, 1 ; NE (вперед-вправо для білих) DB 1, -1 ; SW (вперед-вліво для червоних) DB 1, 1 ; SE (вперед-вправо для червоних)
Білі рухаються вперед = рядки зменшуються (напрямки 0-1). Червоні — рядки збільшуються (напрямки 2-3). Дамки використовують усі 4 напрямки (0-3).На початку кожного ходу проскануйте всі 64 клітинки. Для кожної дружньої фішки перевірте її діагональних сусідів:must_capture = 0
Для кожної клітинки (r, c) з дружньою фішкою: Для кожного дозволеного напрямку (dr, dc): nr = r + dr, nc = c + dc ; сусідня клітинка jr = r + 2*dr, jc = c + 2*dc ; клітинка за сусідом Якщо (nr, nc) у межах дошки І board[nr][nc] містить ворожу фішку І (jr, jc) у межах дошки І board[jr][jc] порожня: must_capture = 1 ВИХІД
Якщо must_capture = 1, дозволяються лише ходи-стрибки.Після виконання стрибка фішка може стрибнути знову, якщо з нової позиції доступне інше взяття:execute_jump(from, to): Перемістити фішку з from до to Видалити перестрибнуту фішку Перевірити перетворення в дамку Викликати get_legal_jumps(to_r, to_c) Якщо є ще стрибки: Запропонувати гравцю обрати наступний стрибок Повторити execute_jump Інакше: Хід закінчено
Важливо: після стрибка перевіряються лише стрибки (не звичайні ходи) і лише для тієї самої фішки.Біла фішка, що досягає рядка 0, стає дамкою. Червона фішка, що досягає рядка 7, стає дамкою:Якщо фішка біла І рядок == 0: board[row][col] = 2 ; біла дамка
Якщо фішка червона І рядок == 7: board[row][col] = 6 ; червона дамка
Дамка отримує доступ до всіх 4 діагональних напрямків замість 2.Для обраної фішки на (r, c):move_count = 0
Для кожного дозволеного напрямку (dr, dc): nr = r + dr, nc = c + dc ; Звичайний хід Якщо NOT must_capture І (nr, nc) у межах І board[nr][nc] порожня: додати (r, c, nr, nc) до move_list ; Стрибок jr = r + 2*dr, jc = c + 2*dc Якщо (nr, nc) у межах І board[nr][nc] ворожа І (jr, jc) у межах І board[jr][jc] порожня: додати (r, c, jr, jc) до move_list
; --- Константи ---
EMPTY EQU 0
WHITE EQU 1
WHITE_K EQU 2
RED EQU 5
RED_K EQU 6 board DB 64 DUP(0)
dir_table DB -1, -1, -1, 1, 1, -1, 1, 1 ; NW, NE, SW, SE ; Список ходів: до 48 записів по 4 байти
move_list DB 192 DUP(0)
move_count DB 0
must_capture DB 0 ; --- Сканування примусового взяття ---
; Вхід: DL = колір поточного гравця (04h для червоних, 01h для білих)
; Вихід: must_capture = 0 або 1
scan_captures PROC PUSH SI PUSH BX PUSH CX MOV [must_capture], 0 ; Для кожної клітинки 0..63: ; 1. Пропустити порожні та чужі фішки (TEST AL, DL) ; 2. Обчислити row/col: DIV 8 ; 3. Визначити діапазон напрямків з dir_table ; (дамка: 4, звичайна біла: 0-1, червона: 2-3) ; 4. Для кожного напрямку перевірити стрибок: ; сусід ворожий І клітинка за ним порожня І в межах ; (див. алгоритм сканування примусового взяття вище) ; ... ваш код тут ... POP CX POP BX POP SI RET
scan_captures ENDP ; --- Виконання стрибка ---
; Вхід: from_r, from_c, to_r, to_c -- координати
execute_jump PROC PUSH AX PUSH BX ; 1. Обчислити індекси from та to: row*8 + col ; 2. Перемістити фішку: board[to] = board[from], board[from] = EMPTY ; 3. Видалити перестрибнуту фішку: mid = (from + to) / 2 ; 4. Перевірити перетворення в дамку ; (біла на рядку 0 -&gt; WHITE_K, червона на рядку 7 -&gt; RED_K) ; (див. алгоритм ланцюжка стрибків вище) ; ... ваш код тут ... POP BX POP AX RET
execute_jump ENDP Змішування напрямків для білих і червоних. Білі рухаються вгору (row зменшується), червоні — вниз (row збільшується). Якщо перевірка напрямку залежить від кольору, забутий ADD SI, 4 для червоних зламає всю логіку руху. Дозвіл звичайного ходу при наявності стрибка. Якщо must_capture = 1, але код не фільтрує список ходів, гравець зможе уникнути обов'язкового взяття. Перевіряйте прапорець перед додаванням звичайних ходів. Продовження ланцюжка стрибків іншою фішкою. Після стрибка перевірка наступних стрибків повинна стосуватися лише тієї самої фішки на новій позиції, а не всіх фішок на дошці. Неправильне обчислення середньої клітинки. Перестрибнута фішка знаходиться на ((from_r + to_r)/2, (from_c + to_c)/2). Якщо використовувати SHR на знаковому від'ємному результаті замість SAR, координата буде хибною. Оскільки координати 0-7, сума завжди додатна — SHR безпечний. <a class="internal-link" data-href="../arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви</a>
<br><a class="internal-link" data-href="../procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури</a>
<br><a class="internal-link" data-href="../flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Прапорці та умовні переходи</a>
<br><a class="internal-link" data-href="../lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиці пошуку</a>
<br><a class="internal-link" data-href="Y04.md" href="projects/how-to/game-logic/y04.html" target="_self" rel="noopener nofollow">Ігрова логіка: Connect Four</a>
<br>
Корисно для: <a class="internal-link" data-href="../../Y12-Checkers.md" href="projects/y12-checkers.html" target="_self" rel="noopener nofollow">Y12 Шашки (Checkers)</a>
]]></description><link>projects/how-to/game-logic/y12.html</link><guid isPermaLink="false">Projects/How-to/game-logic/Y12.md</guid><pubDate>Tue, 24 Feb 2026 14:16:53 GMT</pubDate></item><item><title><![CDATA[R06]]></title><description><![CDATA[Алгоритми XOR-перемикання клітинок із сусідами, представлення дошки як бітового слова, розв'язувача методом Гауса над GF(2) та генерації розв'язних головоломок.
Потрібно реалізувати механіку перемикання клітинки разом із чотирма сусідами
Потрібно побудувати автоматичний розв'язувач системи лінійних рівнянь над GF(2)
Потрібно генерувати випадкові головоломки з гарантією розв'язності
У Lights Out натискання клітинки (r, c) перемикає її стан та стан 4 ортогональних сусідів. Замість обчислення адрес сусідів з перевіркою меж, можна попередньо обчислити 25 бітових масок:Сітка 5x5, біти нумеруються 0..24: біт = r * 5 + c Маска для клітинки (1,2) = біт 7: встановлені біти: 7 (сама), 2 (вгору), 12 (вниз), 6 (вліво), 8 (вправо) mask[7] = 0000 0000 0001 0001 1100 0100b
Крайні клітинки просто не мають відповідного сусіда в масці (наприклад, клітинка (0,0) не має сусідів вгору та вліво).Перемикання: board = board XOR mask[i] — одна операція, замість 5 окремих перевірок.25 клітинок сітки 5x5 вміщуються у 25 біт. Зберігаємо як подвійне слово (DX:AX або 32-бітну змінну):Біт 0 = клітинка (0,0) Біт 1 = (0,1) ... Біт 4 = (0,4)
Біт 5 = клітинка (1,0) Біт 6 = (1,1) ... Біт 9 = (1,4)
...
Біт 20 = клітинка (4,0) Біт 21 = (4,1) ... Біт 24 = (4,4)
Перевірка перемоги (всі вогні вимкнено): OR AX, DX / маскувати зайві біти / JZ win.Перемикання клітинки i: board XOR= mask[i] — операція XOR на парі слів.GF(2) — поле з двох елементів {0, 1}, де додавання = XOR, множення = AND. Система 25 рівнянь:Для кожної клітинки j (0..24): стовпці 0-24 матриці = маска перемикання клітинки j стовпець 25 = початковий стан дошки (біт j)
Розширена матриця: 25 рядків x 26 стовпців. Кожен рядок зберігається як одне подвійне слово (32 біти; достатньо для 26 стовпців).Алгоритм зведення:Для кожного стовпця j = 0..24: 1. Знайти рядок pivot &gt;= j, де біт j встановлений 2. Поміняти рядок pivot з рядком j 3. Для кожного рядка i != j: якщо біт j в рядку i встановлений: row[i] = row[i] XOR row[j]
Після зведення біт 25 кожного рядка j містить відповідь: 1 = натиснути клітинку j, 0 = не натискати.Починаємо з порожньої дошки (всі вимкнені) та застосовуємо 10-20 випадкових перемикань:board = 0
Повторити N разів: cell = random() MOD 25 board = board XOR mask[cell]
Будь-яка дошка, отримана перемиканнями від нульової, гарантовано розв'язна — достатньо повторити ті самі перемикання.; --- Таблиця масок перемикання (25 подвійних слів) ---
; Кожна маска містить біти клітинки та її сусідів
toggle_masks DD 00000023h ; (0,0): біти 0,1,5 DD 00000047h ; (0,1): біти 0,1,2,6 DD 0000008Eh ; (0,2): біти 1,2,3,7 DD 0000011Ch ; (0,3): біти 2,3,4,8 DD 00000218h ; (0,4): біти 3,4,9 ; ... ще 20 записів ... board_state DD 0 ; упакований стан дошки (25 біт)
matrix DD 25 DUP(0) ; розширена матриця 25x26 для розв'язувача ; --- Перемикання клітинки ---
; Вхід: BX = індекс клітинки (0-24)
toggle_cell PROC PUSH EAX ; Зчитати маску з toggle_masks[BX*4] ; XOR board_state з маскою (див. алгоритм XOR-перемикання вище) ; ... ваш код тут ... POP EAX RET
toggle_cell ENDP ; --- Метод Гауса над GF(2) ---
; Вхід: matrix заповнена (рядки = маски, стовпець 25 = стан дошки)
; Вихід: біт 25 кожного рядка = чи натискати цю клітинку
gauss_solve PROC PUSH EAX PUSH EBX PUSH ECX PUSH EDX PUSH SI PUSH DI ; Для кожного стовпця j = 0..24: ; 1. Знайти pivot рядок &gt;= j з встановленим бітом j (BT) ; 2. Поміняти рядки j та pivot (XCHG або через тимчасову змінну) ; 3. XOR рядка j з усіма іншими рядками, де біт j встановлений ; (див. алгоритм зведення матриці вище) ; ... ваш код тут ... POP DI POP SI POP EDX POP ECX POP EBX POP EAX RET
gauss_solve ENDP Неправильна нумерація бітів у масках. Біт клітинки (r,c) = r*5 + c. Помилка на одиницю призводить до перемикання сусідньої клітинки. Перевіряйте кожну маску в Turbo Debugger. Забули маскувати зайві біти при перевірці перемоги. Подвійне слово має 32 біти, використовуються лише 25. Якщо біти 25-31 ненульові через випадкові дані, перевірка CMP EAX, 0 дасть хибний результат. Маскуйте: AND EAX, 01FFFFFFh. Генерація нерозв'язних головоломок. Випадковий стан дошки (рандомні біти) не гарантує розв'язність. Завжди генеруйте через послідовність перемикань від нульової дошки. Плутанина рядків та стовпців у матриці розв'язувача. Рядок j матриці містить маску перемикання клітинки j (не навпаки). Стовпець 25 — це початковий стан. Після зведення стовпець 25 дає розв'язок. <a class="internal-link" data-href="../bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Побітові операції</a>
<br><a class="internal-link" data-href="../arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви</a>
<br><a class="internal-link" data-href="../loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Цикли</a>
<br><a class="internal-link" data-href="../lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиці пошуку</a>
<br><a class="internal-link" data-href="../random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">Випадкові числа</a>
<br>
Корисно для: <a class="internal-link" data-href="../../R06-Lights-Out.md" href="projects/r06-lights-out.html" target="_self" rel="noopener nofollow">R06 Lights Out</a>
]]></description><link>projects/how-to/game-logic/r06.html</link><guid isPermaLink="false">Projects/How-to/game-logic/R06.md</guid><pubDate>Tue, 24 Feb 2026 14:09:10 GMT</pubDate></item><item><title><![CDATA[R05]]></title><description><![CDATA[Алгоритми дисципліни регістрів у ISR, виявлення гарячої клавіші (порт 60h + прапорці BIOS), встановлення TSR та ланцюжкового з'єднання переривань.
Потрібно написати обробник переривань, який не псує регістри програми, що виконується
Потрібно виявити комбінацію Ctrl+Alt+T зсередини обробника INT 09h
Потрібно встановити програму як резидентну (TSR) з правильним розрахунком розміру
Потрібно передати керування оригінальному обробнику переривань (ланцюжкове з'єднання)
ISR (Interrupt Service Routine) спрацьовує під час виконання будь-якої програми. Якщо ISR змінить будь-який регістр без збереження, поточна програма отримає пошкоджені дані.Правило: зберегти ВСЕ на вході, відновити ВСЕ на виході.ISR_entry: PUSHF ; прапорці (якщо використовуємо CMP, TEST тощо) PUSH AX PUSH BX ... PUSH DS PUSH ES ; --- тіло ISR --- POP ES POP DS ... POP BX POP AX POPF JMP FAR CS:[original_vector] ; або IRET
Послідовність POP — строго зворотна до PUSH. Одна пропущена пара — і система нестабільна.Коли ISR спрацьовує, DS належить перерваній програмі. Звернення MOV AL, [my_var] зчитає сміття. Рішення — зберегти DS при встановленні та відновлювати його в ISR через CS override:; Резидентні дані (доступні через CS)
saved_ds DW ? ; заповнюється при встановленні ; Всередині ISR: PUSH DS MOV DS, CS:[saved_ds] ; тепер DS = наш сегмент даних ; ... працюємо з нашими змінними нормально ... POP DS
Альтернатива: розмістити всі дані ISR в сегменті коду і використовувати CS: override для кожного звернення. Це уникає зміни DS, але захаращує код.Обробник INT 09h спрацьовує при кожному натисканні клавіші. Алгоритм виявлення комбінації:1. Зчитати скан-код з порту 60h
2. Якщо скан-код != 14h (T) -&gt; передати оригінальному обробнику
3. Зчитати байт стану модифікаторів за адресою 0040:0017h
4. Перевірити біт 2 (Ctrl) та біт 3 (Alt)
5. Якщо обидва встановлені -&gt; перемкнути clock_on
6. Передати керування оригінальному обробнику INT 09h
Таблиця бітів стану клавіатури (0040:0017h):Два патерни передачі керування оригінальному обробнику:Патерн JMP FAR (рекомендований): ; виконати свою роботу ; відновити ВСІ збережені регістри JMP FAR CS:[saved_vector] ; керування НІКОЛИ не повернеться сюди ; оригінальний обробник виконає IRET
Патерн CALL FAR: PUSHF ; імітувати INT (PUSHF + CALL FAR) CALL FAR CS:[saved_vector] ; керування повернеться сюди після IRET оригіналу ; ... додаткова робота після оригінального обробника ... ; відновити регістри IRET
Патерн JMP FAR простіший — не потрібен подвійний IRET. Але відновити регістри потрібно до JMP.Розкладка пам'яті програми TSR:+---------------------------+ &lt;-- CS:0000 (початок PSP)
| PSP (256 байтів = 10h пар)|
+---------------------------+ &lt;-- CS:0100
| Резидентні дані |
| (saved_ds, saved_vectors, |
| clock_on, tick_count, |
| signature) |
+---------------------------+
| ISR таймера |
| ISR клавіатури |
+---------------------------+ &lt;-- end_of_resident
| Код ініціалізації | ← цю частину можна перезаписати
| (збереження векторів, |
| перехоплення, повідомлення)|
+---------------------------+
Розрахунок резидентного розміру:resident_paragraphs = (OFFSET end_of_resident + 15) / 16 + 10h ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^ округлення вгору до параграфа PSP
Без перевірки подвійний запуск створює циклічний ланцюг переривань (ваш обробник -&gt; ваш обробник -&gt; ...), що заморожує систему.1. Отримати поточний вектор INT 1Ch -&gt; ES:BX
2. Прочитати 4 байти за зміщенням signature_offset від ES:BX
3. Порівняти з сигнатурою 'CLK1'
4. Якщо збігається -&gt; вивести "Already installed", вийти
5. Якщо не збігається -&gt; продовжити встановлення
; Резидентні дані (доступні через CS)
saved_ds DW ?
saved_09h DD ? ; оригінальний вектор INT 09h
saved_1ch DD ? ; оригінальний вектор INT 1Ch
clock_on DB 0
tick_count DB 0
; --- Обробник клавіатури: виявлення Ctrl+Alt+T ---
kbd_handler PROC FAR push ax push ds push es ; ... ваш код тут ... ; 1. IN AL, 60h -- зчитати скан-код ; 2. Порівняти з 14h (T); якщо ні -&gt; @@chain ; 3. Перевірити біти Ctrl та Alt в 0040:0017h; див. таблицю вище ; 4. Якщо обидва встановлені -&gt; XOR [clock_on], 1 @@chain: pop es pop ds pop ax jmp FAR CS:[saved_09h] ; передати оригінальному обробнику
kbd_handler ENDP
; --- ISR таймера з оновленням годинника ---
timer_handler PROC FAR pushf push ax push bx push cx push dx push ds push es mov ds, cs:[saved_ds] ; ... ваш код тут ... ; 1. Інкрементувати tick_count, порівняти з 18; див. алгоритм вище ; 2. Якщо clock_on != 1 -&gt; @@done ; 3. Зчитати години/хвилини/секунди з CMOS (порти 70h/71h) ; 4. Перетворити BCD -&gt; ASCII, записати у відеопам'ять 0B800h @@done: pop es pop ds pop dx pop cx pop bx pop ax popf jmp FAR CS:[saved_1ch]
timer_handler ENDP
; --- Встановлення TSR ---
install: ; ... ваш код тут ... ; 1. Зберегти DS: mov [saved_ds], ds ; 2. Зберегти вектори INT 1Ch та INT 09h (AH=35h) ; 3. Встановити свої обробники (AH=25h) ; 4. Перехід у резидентний режим (AH=31h); див. розрахунок розміру вище
install ENDP Забули зберегти DS в ISR. MOV AL, [clock_on] зчитає довільний байт з DS перерваної програми. Завжди відновлюйте DS через CS:[saved_ds] на початку ISR. Регістри відновлені в неправильному порядку. PUSH AX / PUSH BX ... POP AX / POP BX — неправильно. POP має бути строго зворотним: POP BX / POP AX. Одна помилка — і стек пошкоджено. JMP FAR перед POP регістрів. Після JMP FAR CS:[saved_vector] керування не повернеться, тому всі POP мають бути до JMP, а не після. Резидентний розмір занадто малий. Код ініціалізації перезаписує хвіст ISR — система падає через кілька хвилин, коли наступна програма займе цю пам'ять. Завжди ставте мітку end_of_resident після останнього байта коду/даних ISR. Виклик INT 21h зсередини ISR. DOS не є реентрантною. Якщо таймер спрацьовує під час DOS-виклику, повторний INT 21h зсередини ISR пошкоджує внутрішні структури DOS. Використовуйте лише прямий доступ до апаратного забезпечення (відеопам'ять, порти). <a class="internal-link" data-href="../interrupt-handlers.md" href="projects/how-to/interrupt-handlers.html" target="_self" rel="noopener nofollow">Обробники переривань</a>
<br><a class="internal-link" data-href="../segment-registers.md" href="projects/how-to/segment-registers.html" target="_self" rel="noopener nofollow">Сегментні регістри</a>
<br><a class="internal-link" data-href="../video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Відеопам'ять</a>
<br><a class="internal-link" data-href="../keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід</a>
<br><a class="internal-link" data-href="../dos-services.md" href="projects/how-to/dos-services.html" target="_self" rel="noopener nofollow">Служби DOS</a>
<br>
Корисно для: <a class="internal-link" data-href="../../R05-TSR-Hotkey-Utility.md" href="projects/r05-tsr-hotkey-utility.html" target="_self" rel="noopener nofollow">R05 TSR-утиліта з гарячою клавішею</a>
]]></description><link>projects/how-to/game-logic/r05.html</link><guid isPermaLink="false">Projects/How-to/game-logic/R05.md</guid><pubDate>Tue, 24 Feb 2026 13:53:23 GMT</pubDate></item><item><title><![CDATA[R04]]></title><description><![CDATA[Алгоритми зіткнень у лабіринті, ШІ привидів (переслідування / випадковий / переляканий) та споживання точок для текстового Pac-Man.
Привид повинен обрати напрямок на перехресті (жадібне переслідування або випадковий вибір)
Потрібно перевірити, чи може сутність (Pac-Man або привид) рухатися в заданому напрямку
Pac-Man з'їв силову пілюлю і всі привиди мають перейти в режим переляку
Потрібно відстежувати, скільки точок залишилося для умови перемоги
Лабіринт зберігається як плоский байтовий масив. Кожна сутність перед рухом перевіряє тайл у напрямку руху.WALL EQU 1 ; Обчислення зміщення комірки (x, y):
offset = y * MAZE_WIDTH + x
tile = maze[offset]
якщо tile == WALL -&gt; рух заборонено
Таблиця зміщень напрямків спрощує перевірку для всіх чотирьох напрямків:dir_dx DB 0, 0, -1, 1
dir_dy DB -1, 1, 0, 0
Привиди переоцінюють напрямок лише на перехрестях. Комірка є перехрестям, якщо має 3 або більше сусідів, що не є стінами:count = 0
для кожного напрямку D = 0..3: nx = x + dir_dx[D] ny = y + dir_dy[D] якщо maze[ny * WIDTH + nx] != WALL: count++
якщо count &gt;= 3 -&gt; це перехрестя
У прямих коридорах привид продовжує рух у поточному напрямку без переоцінки.На перехресті привид-переслідувач обирає напрямок, що мінімізує Манхеттенську відстань до Pac-Man:best_dist = 0FFFFh
best_dir = поточний напрямок для кожного напрямку D = 0..3: якщо D == протилежний(поточний) -&gt; пропустити ; привиди не розвертаються nx = ghost_x + dir_dx[D] ny = ghost_y + dir_dy[D] якщо maze[ny * WIDTH + nx] == WALL -&gt; пропустити dist = |nx - pac_x| + |ny - pac_y| якщо dist &lt; best_dist: best_dist = dist best_dir = D ghost_dir = best_dir
Абсолютне значення в TASM: SUB AX, BX / JNS skip / NEG AX / skip:.На перехресті випадковий привид збирає допустимі напрямки та обирає один випадково:count = 0
для кожного напрямку D = 0..3: якщо D == протилежний(поточний) -&gt; пропустити nx = ghost_x + dir_dx[D] ny = ghost_y + dir_dy[D] якщо maze[ny * WIDTH + nx] != WALL: valid[count] = D count++ rand_index = random() MOD count
ghost_dir = valid[rand_index]
Коли Pac-Man з'їдає силову пілюлю:
Встановити глобальний лічильник frightened_timer = 100 (~5.5 секунд)
Для кожного привида: встановити прапорець mode = FRIGHTENED, розвернути напрямок
Кожен такт таймера: зменшити frightened_timer
Коли frightened_timer == 0: скинути всі режими на NORMAL
Привид у режимі переляку:
Використовує випадковий вибір напрямку (навіть привид-переслідувач)
Рухається повільніше (збільшений інтервал тактів, наприклад, 6 замість 4)
Відображається синім кольором (атрибут 09h)
При зіткненні з Pac-Man: привид "з'їдений" (повертається в загін), Pac-Man отримує бонус
; Після руху Pac-Man на (px, py):
offset = py * MAZE_WIDTH + px
tile = maze[offset] якщо tile == DOT: maze[offset] = EMPTY score += 10 dots_remaining--
якщо tile == POWER_PILL: maze[offset] = EMPTY score += 50 dots_remaining-- активувати режим переляку якщо dots_remaining == 0 -&gt; ПЕРЕМОГА
Лічильник dots_remaining ініціалізується програмно при старті рівня (пройти весь масив, підрахувати DOT та POWER_PILL).Фіксований порядок запобігає пропуску зіткнень:
Обробити клавіатурний ввід (оновити бажаний напрямок Pac-Man)
Перемістити Pac-Man (з перевіркою стін)
Перевірити Pac-Man проти точки/пілюлі
Перемістити кожного привида (з перевіркою стін та ШІ на перехрестях)
Перевірити кожного привида проти Pac-Man (зіткнення сутностей)
Зменшити frightened_timer якщо &gt; 0
; Таблиці зміщень напрямків (спільні для всіх сутностей)
dir_dx DB 0, 0, -1, 1 ; Вгору, Вниз, Ліворуч, Праворуч
dir_dy DB -1, 1, 0, 0 WALL EQU 1
MAZE_WIDTH EQU 28
; --- Жадібне переслідування: обрати напрямок до Pac-Man ---
; Вхід: ghost_x, ghost_y, ghost_dir, pac_x, pac_y
; Вихід: ghost_dir оновлено
choose_chase_dir PROC push ax push bx push cx push dx push si ; ... ваш код тут ... ; Для кожного напрямку D = 0..3: ; - пропустити протилежний (XOR 1); див. алгоритм вище ; - обчислити кандидатну позицію через dir_dx/dir_dy ; - викликати get_tile, пропустити якщо WALL ; - обчислити Манхеттенську відстань |nx-pac_x| + |ny-pac_y| ; - якщо dist &lt; best_dist -&gt; оновити best_dist, best_dir pop si pop dx pop cx pop bx pop ax ret
choose_chase_dir ENDP
; --- Перевірка тайлу за координатами (x, y) ---
; Вхід: AL = x, AH = y
; Вихід: AL = значення тайлу
get_tile PROC push bx push cx ; ... ваш код тут ... ; offset = y * MAZE_WIDTH + x; див. алгоритм вище ; AL = maze[offset] pop cx pop bx ret
get_tile ENDP Привид розвертається в коридорі. Якщо не перевіряти протилежний напрямок, привид коливатиметься між двома комірками. Завжди виключайте протилежний(поточний_напрямок) зі списку кандидатів. Зіткнення сутностей пропущено через порядок оновлення. Якщо привид і Pac-Man міняються місцями за один такт, обидва проходять один через одного. Перевіряйте зіткнення після кожного переміщення привида, а не лише в кінці. Кількість точок не збігається з масивом. Жорстко закодований dots_remaining не відповідає фактичному лабіринту — рівень ніколи не завершується. Завжди підраховуйте точки програмно при ініціалізації. Режим переляку не скидається коректно. Якщо з'їдено другу пілюлю під час переляку, таймер повинен скидатися на повну тривалість, а не додаватися. Просто перезаписуйте frightened_timer = 100. ШІ привида працює в ISR. Обчислення ШІ (особливо BFS) всередині обробника переривань таймера блокує систему. Тримайте ISR мінімальним (інкремент лічильника), а всю логіку виконуйте в основному циклі. <a class="internal-link" data-href="../arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви</a>
<br><a class="internal-link" data-href="../random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">Генерація випадкових чисел</a>
<br><a class="internal-link" data-href="../procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури</a>
<br><a class="internal-link" data-href="../lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиці пошуку</a>
<br><a class="internal-link" data-href="../interrupt-handlers.md" href="projects/how-to/interrupt-handlers.html" target="_self" rel="noopener nofollow">Обробники переривань</a>
<br><a class="internal-link" data-href="../flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Прапорці та умовні переходи</a>
<br><a class="internal-link" data-href="Y01.md" href="projects/how-to/game-logic/y01.html" target="_self" rel="noopener nofollow">Змійка: ігрова логіка</a> — схожі алгоритми зіткнень та руху
<br>
Корисно для: <a class="internal-link" data-href="../../R04-Pac-Man.md" href="projects/r04-pac-man.html" target="_self" rel="noopener nofollow">R04 Pac-Man</a>
]]></description><link>projects/how-to/game-logic/r04.html</link><guid isPermaLink="false">Projects/How-to/game-logic/R04.md</guid><pubDate>Tue, 24 Feb 2026 13:52:27 GMT</pubDate></item><item><title><![CDATA[R01]]></title><description><![CDATA[Алгоритми обертання тетроміно через таблиці пошуку, виявлення зіткнень за принципом "перевір-потім-зафіксуй", очищення заповнених рядків зі зсувом та гравітація через лічильник тактів.
Потрібно зберегти 7 фігур x 4 повороти й швидко отримувати форму за індексом
Фігура не повинна проходити крізь стіни, підлогу або вже зафіксовані блоки
Заповнені рядки мають зникати, а верхні рядки — зсуватися вниз
Фігура має падати автоматично зі швидкістю, що залежить від рівня
Кожне тетроміно має 4 стани повороту. Зберігаємо всі стани як 4 пари координат (dx, dy) відносно центру фігури. Загалом 7 фігур x 4 повороти x 4 блоки x 2 координати = 224 байти.Індекс у таблиці обчислюється так:offset = piece_type * 32 + rotation * 8
Кожен стан займає 8 байтів (4 блоки по 2 координати). Координати — знакові байти (DB зі знаком).Приклад даних для T-фігури (тип 2):Для обертання: обчисліть новий індекс повороту (rotation + 1) MOD 4, знайдіть нові зміщення в таблиці, перевірте зіткнення з новими координатами — і лише тоді оновіть стан.Головне правило: ніколи не змінюйте стан фігури до перевірки зіткнення. Алгоритм:
Обчислити кандидатну позицію (нові x, y, rotation)
Отримати 4 зміщення блоків із таблиці для кандидатного стану
Для кожного блоку: абсолютна позиція = (piece_x + dx, piece_y + dy)
Перевірити три умови: 0 &lt;= abs_x &lt;= 9 (межі поля по горизонталі)
0 &lt;= abs_y &lt;= 19 (межі поля по вертикалі)
field[abs_y * 10 + abs_x] == 0 (комірка порожня) Якщо всі 4 блоки пройшли — зафіксувати новий стан; інакше — відхилити рух
Псевдокод:check_collision(type, rot, px, py): offset = type * 32 + rot * 8 FOR i = 0 TO 3: bx = px + shape_table[offset + i*2] by = py + shape_table[offset + i*2 + 1] IF bx &lt; 0 OR bx &gt; 9: RETURN collision IF by &lt; 0 OR by &gt; 19: RETURN collision IF field[by * 10 + bx] != 0: RETURN collision RETURN no_collision
Після фіксації фігури скануємо поле знизу вгору. Це гарантує правильну обробку множинних заповнених рядків:
row = 19 (найнижчий рядок)
Перевірити, чи всі 10 комірок рядка ненульові
Якщо рядок повний: Зсунути всі рядки від row-1 до 0 на один рядок вниз (використовуючи REP MOVSB)
Заповнити рядок 0 нулями
Не зменшувати row (перевірити цей самий індекс повторно, бо туди зсунувся новий рядок)
Збільшити лічильник очищених рядків Якщо рядок не повний: row = row - 1
Повторювати, поки row &gt;= 0
Обробник INT 1Ch лише інкрементує лічильник. Основний цикл порівнює лічильник із порогом для поточного рівня:Рівень 1: поріг = 18 тактів (~1 сек)
Рівень 5: поріг = 8 тактів (~0.44 сек)
Рівень 10: поріг = 3 такти (~0.16 сек)
Пороги зберігаються в таблиці пошуку (10 байтів для 10 рівнів).; --- Перевірка зіткнення для кандидатної позиції ---
; Вхід: AL=тип, AH=поворот, CL=x, CH=y
; Вихід: CF=1 якщо зіткнення
check_collision PROC push bx push si push di ; Обчислити зміщення в таблиці форм: type * 32 + rotation * 8 ; див. алгоритм "offset = piece_type * 32 + rotation * 8" ; ... ваш код тут ... mov di, 4 ; 4 блоки
@@check_block: ; Для кожного блоку: обчислити абсолютну позицію (piece_x + dx, piece_y + dy) ; Перевірити межі поля (0..9 по x, 0..19 по y) та чи комірка порожня ; див. псевдокод check_collision вище ; ... ваш код тут ... add si, 2 dec di jnz @@check_block clc ; немає зіткнення jmp @@done
@@collision: stc ; є зіткнення
@@done: pop di pop si pop bx ret
check_collision ENDP
; --- Очищення заповнених рядків знизу вгору ---
; Вихід: AL = кількість очищених рядків
clear_lines PROC push bx push cx push si push di xor ah, ah ; лічильник очищених mov bx, 19 ; починаємо з нижнього рядка
@@check_row: cmp bx, 0 jl @@done ; Перевірити, чи всі 10 комірок рядка bx ненульові ; див. алгоритм "Очищення рядків знизу вгору" крок 2 ; ... ваш код тут ... ; Якщо рядок повний: зсунути рядки row-1..0 вниз (REP MOVSB), ; заповнити рядок 0 нулями, НЕ зменшувати bx ; див. алгоритм кроки 3-4 ; ... ваш код тут ... @@not_full: dec bx jmp @@check_row @@done: mov al, ah ; повернути кількість pop di pop si pop cx pop bx ret
clear_lines ENDP Фіксація до перевірки. Змінюєте поворот або позицію фігури перед перевіркою зіткнення — фігура "вгризається" у стіни. Завжди працюйте з копією стану.
Зсув рядків зверху вниз. При очищенні кількох рядків одночасно наївний зсув зверху вниз пропускає рядки. Скануйте знизу вгору і не інкрементуйте індекс після очищення.
Тяжкий ISR. Виконання ігрової логіки в обробнику INT 1Ch може зайняти довше 55 мс і зламати синхронізацію. ISR має лише інкрементувати лічильник.
Помилки в таблиці форм. Одна неправильна координата серед 224 байтів таблиці — і фігура виглядає зламаною. Перед грою відобразіть усі 28 станів на екрані для візуальної перевірки.
Кінець гри при появі. Якщо фігура з'являється в межах видимого поля, гра може закінчитися зарано. Генеруйте фігуру вище видимої області (рядки -1, -2). <a class="internal-link" data-href="../lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиці пошуку</a>
<br><a class="internal-link" data-href="../arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Обробка масивів</a>
<br><a class="internal-link" data-href="../interrupt-handlers.md" href="projects/how-to/interrupt-handlers.html" target="_self" rel="noopener nofollow">Перехоплення переривань</a>
<br><a class="internal-link" data-href="../flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Прапорці та умовні переходи</a>
<br><a class="internal-link" data-href="R02.md" href="projects/how-to/game-logic/r02.html" target="_self" rel="noopener nofollow">R02 Breakout — ігрова логіка</a>
<br>
Корисно для: <a class="internal-link" data-href="../../R01-Tetris.md" href="projects/r01-tetris.html" target="_self" rel="noopener nofollow">R01 Тетріс</a>
]]></description><link>projects/how-to/game-logic/r01.html</link><guid isPermaLink="false">Projects/How-to/game-logic/R01.md</guid><pubDate>Tue, 24 Feb 2026 13:52:09 GMT</pubDate></item><item><title><![CDATA[Y01]]></title><description><![CDATA[Алгоритми циклічного буфера для тіла змійки, перевірки зіткнень (стіни та саме тіло), розміщення їжі та захисту від протилежного напрямку.
Потрібно зберігати тіло змійки так, щоб рух виконувався за O(1), а не O(n)
Потрібно перевірити, чи голова змійки вдарилася у стіну або у власне тіло
Потрібно розмістити їжу у випадковій порожній клітинці, що не перетинається з тілом
Потрібно ігнорувати натискання клавіші, що розвертає змійку на 180 градусів
Тіло зберігається як масив пар (X, Y) фіксованого розміру з двома індексами: head_idx та tail_idx. Це дозволяє рухати змійку за O(1) — без зсуву всіх елементів.MAX_LENGTH EQU 200 snake_x DB MAX_LENGTH DUP(0) ; X-координати сегментів
snake_y DB MAX_LENGTH DUP(0) ; Y-координати сегментів
head_idx DW 0 ; індекс голови
tail_idx DW 0 ; індекс хвоста
snake_len DW 3 ; поточна довжина
Рух на один крок:1. Обчислити нову позицію голови: new_x = snake_x[head_idx] + dx[direction] new_y = snake_y[head_idx] + dy[direction] 2. Просунути head_idx: head_idx = (head_idx + 1) MOD MAX_LENGTH 3. Записати нову голову: snake_x[head_idx] = new_x snake_y[head_idx] = new_y 4. Якщо їжу НЕ з'їдено: Стерти хвіст з екрана (клітинка snake_x[tail_idx], snake_y[tail_idx]) Просунути tail_idx: tail_idx = (tail_idx + 1) MOD MAX_LENGTH 5. Якщо їжу з'їдено: НЕ просувати tail_idx (змійка зростає) snake_len++ score++ Розмістити нову їжу
Операція MOD MAX_LENGTH робить буфер циклічним: коли індекс досягає кінця масиву, він повертається на початок.Ігрове поле обмежене рамкою. Після обчислення нової позиції голови:; Межі поля (всередині рамки)
LEFT EQU 1
RIGHT EQU 78 ; 80 - 2
TOP EQU 2 ; рядок 1 = рахунок, рядок 2 = верхня рамка
BOTTOM EQU 23 ; 25 - 2 якщо new_x &lt; LEFT або new_x &gt; RIGHT -&gt; GAME OVER
якщо new_y &lt; TOP або new_y &gt; BOTTOM -&gt; GAME OVER
Чотири порівняння CMP/JB/JA — це вся перевірка.Нова позиція голови порівнюється з кожним сегментом тіла:i = tail_idx
поки i != head_idx: ; пройти все тіло (без нової голови) якщо snake_x[i] == new_x І snake_y[i] == new_y: -&gt; GAME OVER i = (i + 1) MOD MAX_LENGTH
Для довгої змійки (100+ сегментів) це O(n), але в текстовому режимі 80x25 максимум ~1800 клітинок, що цілком прийнятно.Альтернативний підхід (O(1)): підтримувати 2D масив field[80][25], де кожна клітинка позначена як зайнята/вільна. Перевірка зводиться до одного зчитування, але потрібно оновлювати масив при кожному русі.Їжа має з'явитися у випадковій клітинці, що не зайнята тілом змійки:повторювати: food_x = random() MOD (RIGHT - LEFT + 1) + LEFT food_y = random() MOD (BOTTOM - TOP + 1) + TOP ; Перевірити, чи клітинка порожня зайнята = false i = tail_idx поки i != (head_idx + 1) MOD MAX_LENGTH: якщо snake_x[i] == food_x І snake_y[i] == food_y: зайнята = true вийти з циклу i = (i + 1) MOD MAX_LENGTH
поки зайнята == true
Для генерації випадкового числа зчитайте лічильник тактів BIOS з 0040:006Ch та застосуйте DIV для отримання залишку в потрібному діапазоні.Якщо змійка рухається праворуч і гравець натисне "ліворуч", змійка не повинна розвернутися — це миттєве зіткнення з тілом.; Таблиця протилежних напрямків
; UP=0, DOWN=1, LEFT=2, RIGHT=3
opposite DB 1, 0, 3, 2 ; UP&lt;-&gt;DOWN, LEFT&lt;-&gt;RIGHT ; При обробці клавіатурного вводу:
new_dir = отриманий напрямок з клавіші
якщо new_dir == opposite[current_dir]: ігнорувати ввід
інакше: current_dir = new_dir
Зміщення напрямків:; Дані циклічного буфера
MAX_LENGTH EQU 200
snake_x DB MAX_LENGTH DUP(0)
snake_y DB MAX_LENGTH DUP(0)
head_idx DW 0
tail_idx DW 0
snake_len DW 3 ; Таблиці напрямків
dir_dx DB 0, 0, -1, 1
dir_dy DB -1, 1, 0, 0
opposite DB 1, 0, 3, 2
; --- Рух змійки на один крок ---
; Вхід: head_idx, tail_idx, direction
; Вихід: позиція голови оновлена, хвіст стертий (якщо без їжі)
move_snake PROC push ax push bx push si ; ... ваш код тут ... ; 1. Обчислити new_x, new_y через dir_dx/dir_dy; див. алгоритм вище ; 2. Просунути head_idx циклічно (MOD MAX_LENGTH) ; 3. Записати нову голову в snake_x/snake_y ; 4. Порівняти з food_x/food_y: ; - з'їдено: score++, snake_len++, call place_food ; - не з'їдено: стерти хвіст, просунути tail_idx ; 5. Намалювати нову голову pop si pop bx pop ax ret
move_snake ENDP
; --- Перевірка зіткнення із самим тілом ---
; Вхід: AL = new_x, BL = new_y
; Вихід: CF = 1 якщо зіткнення
check_self_collision PROC push cx push si ; ... ваш код тут ... ; Пройти від tail_idx до head_idx (snake_len-1 ітерацій) ; Порівняти кожен сегмент з (AL, BL); див. алгоритм вище ; STC якщо збіг, CLC якщо ні pop si pop cx ret
check_self_collision ENDP Циклічний буфер не обгортається. Забули перевірити head_idx &gt;= MAX_LENGTH — індекс виходить за межі масиву, перезаписуючи сусідні дані. Завжди виконуйте MOD MAX_LENGTH (або CMP + обнулення) після інкременту. Їжа з'являється на тілі змійки. Генерація випадкової позиції без перевірки на зайнятість. Завжди проходьте по тілу змійки після генерації координат і повторюйте, якщо клітинка зайнята. Зіткнення з тілом не виявлено через неповний обхід. Цикл перевірки проходить не всі сегменти (наприклад, пропускає обгортання циклічного буфера). Використовуйте лічильник snake_len замість порівняння індексів для контролю кількості ітерацій. Змійка розвертається на 180 градусів. Ввід "ліворуч" при русі "праворуч" миттєво зіштовхує голову з першим сегментом тіла. Перевіряйте таблицю протилежних напрямків перед зміною current_dir. Хвіст не стирається з екрана. Після просування tail_idx стара клітинка хвоста залишається намальованою — змійка залишає "слід". Стирайте клітинку хвоста (пробіл) до просування tail_idx. <a class="internal-link" data-href="../arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви</a>
<br><a class="internal-link" data-href="../random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">Генерація випадкових чисел</a>
<br><a class="internal-link" data-href="../loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Цикли</a>
<br><a class="internal-link" data-href="../interrupt-handlers.md" href="projects/how-to/interrupt-handlers.html" target="_self" rel="noopener nofollow">Обробники переривань</a>
<br><a class="internal-link" data-href="../video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Відеопам'ять</a>
<br><a class="internal-link" data-href="R04.md" href="projects/how-to/game-logic/r04.html" target="_self" rel="noopener nofollow">Pac-Man: ігрова логіка</a> — схожі алгоритми руху та зіткнень у лабіринті
<br>
Корисно для: <a class="internal-link" data-href="../../Y01-Snake.md" href="projects/y01-snake.html" target="_self" rel="noopener nofollow">Y01 Змійка</a>
]]></description><link>projects/how-to/game-logic/y01.html</link><guid isPermaLink="false">Projects/How-to/game-logic/Y01.md</guid><pubDate>Tue, 24 Feb 2026 13:51:33 GMT</pubDate></item><item><title><![CDATA[G08]]></title><description><![CDATA[Алгоритми кодування тексту в Морзе та декодування назад, структура таблиці пошуку, таймінг сигналів ITU та керування PC Speaker.
Потрібно перетворити літеру або цифру у послідовність крапок та тире за стандартом Морзе.
Потрібно декодувати введену послідовність крапок/тире назад у символ.
Потрібно відтворити сигнал Морзе через PC Speaker з правильними часовими інтервалами.
Таблиця зберігає 36 записів (A-Z, 0-9) фіксованого розміру. Кожний запис містить символ, довжину коду та саму послідовність:MORSE_ENTRY_SIZE EQU 8 ; байтів на запис ; Формат: символ, довжина, код (до 6 символів, доповнений нулями)
morse_table DB 'A', 2, '.', '-', 0, 0, 0, 0 DB 'B', 4, '-', '.', '.', '.', 0, 0 DB 'C', 4, '-', '.', '-', '.', 0, 0 ; ... (решта алфавіту) DB '0', 5, '-', '-', '-', '-', '-', 0 DB '1', 5, '.', '-', '-', '-', '-', 0 ; ... (решта цифр)
Пряма індексація для літер:; AL = буква 'A'..'Z'
sub al, 'A' ; AL = індекс 0..25
mov ah, MORSE_ENTRY_SIZE
mul ah ; AX = зміщення в таблиці
lea bx, morse_table
add bx, ax ; BX → запис для цієї букви
Для цифр (0-9) записи починаються після 26 літер:; AL = цифра '0'..'9'
sub al, '0'
add al, 26 ; зсув після 26 літер
mov ah, MORSE_ENTRY_SIZE
mul ah
lea bx, morse_table
add bx, ax
Для кожного символу введеного тексту:
Знайти запис у таблиці за прямою індексацією.
Зчитати довжину коду (другий байт запису).
Для кожного елемента коду (крапка або тире): відтворити звук та вивести символ на екран.
Між елементами одного символу — пауза 1 одиниця.
Між символами — пауза 3 одиниці.
Між словами (пробіл у тексті) — пауза 7 одиниць.
для кожного символу c у тексті: якщо c == ' ': пауза(7 одиниць) продовжити запис = знайти_в_таблиці(c) для i від 0 до запис.довжина - 1: якщо запис.код[i] == '.': тон(1 одиниця) ; крапка інакше: тон(3 одиниці) ; тире якщо i &lt; запис.довжина - 1: тиша(1 одиниця) ; між елементами тиша(3 одиниці) ; між символами
Користувач вводить крапки та тире, натискає Space для завершення букви. Алгоритм:
Збирати символи . та - у буфер, поки не натиснуто Space.
Лінійний пошук по таблиці: порівняти буфер з кодом кожного запису.
Якщо збіг знайдено — вивести відповідний символ. Інакше — вивести ?.
; Буфер для введеного коду Морзе
morse_buf DB 8 DUP(0)
morse_buf_len DB 0
Порівняння буфера з записом таблиці:для кожного запису в morse_table: якщо запис.довжина != morse_buf_len: продовжити якщо запис.код[0..довжина-1] == morse_buf[0..довжина-1]: повернути запис.символ
повернути '?' ; невідомий код
Стандарт ITU визначає пропорції таймінгу:Базова одиниця визначається через такти таймера BIOS (18.2 тактів/сек):DOT_TICKS EQU 3 ; ~165 мс на одиницю (~7 WPM)
Процедура затримки зчитує лічильник тактів BIOS та чекає потрібну кількість тактів:; Зчитати лічильник: ES = 0040h, [ES:006Ch] = 32-бітний лічильник
mov ax, 0040h
mov es, ax
mov ax, es:[006Ch] ; поточне значення
add ax, потрібні_такти
@@wait: cmp es:[006Ch], ax jb @@wait
Послідовність увімкнення тону:; 1. Налаштувати таймер PIT (канал 2) на потрібну частоту
MOV AL, 0B6h
OUT 43h, AL ; командний байт: канал 2, режим 3
; 2. Встановити дільник частоти (1193180 / частота)
; Для ~800 Гц: дільник = 1491 = 05D3h
MOV AX, 05D3h
OUT 42h, AL ; молодший байт дільника
MOV AL, AH
OUT 42h, AL ; старший байт дільника
; 3. Увімкнити динамік
IN AL, 61h
OR AL, 03h
OUT 61h, AL
Вимкнення:IN AL, 61h
AND AL, 0FCh ; скинути біти 0 та 1
OUT 61h, AL
; --- Дані таблиці Морзе ---
MORSE_ENTRY_SIZE EQU 8
DOT_TICKS EQU 3 ; ~165 мс (~7 WPM) ; Формат запису: символ, довжина, код (до 6 символів, доповнений нулями)
morse_table DB 'A', 2, '.', '-', 0, 0, 0, 0 DB 'B', 4, '-', '.', '.', '.', 0, 0 ; ... (решта алфавіту та цифри 0-9)
; --- Кодування одного символу в Морзе та відтворення звуку ---
; Вхід: AL = символ ('A'..'Z' або '0'..'9')
; Вихід: звук через PC Speaker, символи на екрані encode_and_play PROC push bx push cx push dx push si ; 1. Обчислити зміщення в morse_table за символом ; (див. пряму індексацію в алгоритмі вище) ; 2. Зчитати довжину коду з запису ; 3. Для кожного елемента ('.' або '-'): викликати play_tone ; з правильною тривалістю (1 або 3 одиниці) ; 4. Між елементами — пауза 1 одиниця (див. таймінг ITU) ; ... ваш код тут ... pop si pop dx pop cx pop bx ret
encode_and_play ENDP
; --- Відтворити тон заданої тривалості ---
; Вхід: BX = тривалість у тактах BIOS
; Дія: увімкнути динамік (OR 03h → порт 61h), чекати, вимкнути (AND 0FCh) play_tone PROC push ax ; 1. Увімкнути динамік (див. керування PC Speaker вище) ; 2. Викликати delay_ticks ; 3. Вимкнути динамік ; ... ваш код тут ... pop ax ret
play_tone ENDP
; --- Затримка на BX тактів таймера BIOS ---
; Використовує лічильник за адресою 0040h:006Ch delay_ticks PROC push ax push es ; Зчитати поточний лічильник, додати BX, чекати в циклі ; (див. процедуру затримки в алгоритмі вище) ; ... ваш код тут ... pop es pop ax ret
delay_ticks ENDP Динамік не вимкнено при виході. Якщо програма завершується без AND AL, 0FCh / OUT 61h, AL, динамік продовжує гудіти в DOSBox. Завжди вимикайте динамік у коді виходу. Невідповідність таблиць кодування та декодування. Якщо таблиця для кодування та пошук для декодування використовують різні дані, перетворення туди-назад дасть хибний результат. Використовуйте одну таблицю для обох напрямків. Не враховано пропорції таймінгу. Якщо пауза між буквами така ж, як пауза між елементами, код Морзе звучатиме злито та нерозбірливо. Дотримуйтесь пропорцій 1:3:7. Лінійний пошук не перевіряє довжину. При декодуванні Морзе потрібно спочатку порівнювати довжину буфера з довжиною запису, а потім вміст. Інакше . може хибно збігтися з .- (код літери A), якщо порівнюється лише початок. <a class="internal-link" data-href="../lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиці пошуку</a> — структура таблиць та індексований доступ
<br><a class="internal-link" data-href="../pc-speaker.md" href="projects/how-to/pc-speaker.html" target="_self" rel="noopener nofollow">PC Speaker</a> — програмування портів динаміка
<br><a class="internal-link" data-href="../timer-delays.md" href="projects/how-to/timer-delays.html" target="_self" rel="noopener nofollow">Затримки на таймері</a> — калібрування часових інтервалів
<br><a class="internal-link" data-href="../bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Побітові операції</a> — маніпуляція бітами порту 61h
<br><a class="internal-link" data-href="G05.md" href="projects/how-to/game-logic/g05.html" target="_self" rel="noopener nofollow">G05 Калькулятор</a> — подібна структура таблиць для диспетчеризації
<br>
Корисно для: <a class="internal-link" data-href="../../G08-Morse-Code-Translator.md" href="projects/g08-morse-code-translator.html" target="_self" rel="noopener nofollow">G08 Перекладач азбуки Морзе</a>
]]></description><link>projects/how-to/game-logic/g08.html</link><guid isPermaLink="false">Projects/How-to/game-logic/G08.md</guid><pubDate>Tue, 24 Feb 2026 13:51:30 GMT</pubDate></item><item><title><![CDATA[Y06]]></title><description><![CDATA[Алгоритми штовхання ящиків, стек скасування ходів, перевірка зіткнень та визначення завершення рівня.
Реалізуєте логіку руху гравця зі штовханням ящиків
Потрібен стек скасування, що коректно скасовує ходи з ящиками та цілями
Перевіряєте умову завершення рівня (всі ящики на цілях)
Обробляєте зіткнення: стіна, ящик, ящик за ящиком
При натисканні клавіші-стрілки виконується ланцюжок перевірок:Вхід: напрямок (dr, dc) -- зміна рядка та стовпця 1. Обчислити цільову клітинку: target = (player_row + dr, player_col + dc) 2. Якщо target -- стіна → нічого не робити 3. Якщо target -- підлога або ціль → перемістити гравця 4. Якщо target -- ящик або ящик_на_цілі: a. Обчислити клітинку за ящиком: beyond = (player_row + 2*dr, player_col + 2*dc) b. Якщо beyond -- стіна, ящик або за межами → нічого не робити c. Якщо beyond -- підлога або ціль → штовхнути ящик і перемістити гравця
Таблиця зміщень для напрямків:Тайли мають складений стан: ящик може стояти на цілі, гравець може стояти на цілі. При переміщенні потрібно коректно відновлювати тайл під об'єктом:Коди тайлів: FLOOR=0, WALL=1, BOX=2, TARGET=3, PLAYER=4, BOX_ON_TARGET=5, PLAYER_ON_TARGET=6 Переміщення гравця (без ящика): Стара клітинка гравця: якщо була PLAYER → стає FLOOR якщо була PLAYER_ON_TARGET → стає TARGET Нова клітинка гравця: якщо була FLOOR → стає PLAYER якщо була TARGET → стає PLAYER_ON_TARGET Штовхання ящика: 1. Стара клітинка гравця → FLOOR або TARGET (як вище) 2. Клітинка ящика (= нова клітинка гравця): якщо була BOX → стає PLAYER якщо була BOX_ON_TARGET → стає PLAYER_ON_TARGET 3. Нова клітинка ящика: якщо була FLOOR → стає BOX якщо була TARGET → стає BOX_ON_TARGET
Кожен хід кодується в 1 байт:Біт 7 6 5 4 3 2 1 0 ─ ─ ─ ─ player box_push direction left_ (1=так) (0-3) target (1=ціль) Біт 3: клітинка, з якої пішов гравець, була TARGET
Біт 2: цей хід штовхнув ящик
Біти 1-0: напрямок (0=вгору, 1=вниз, 2=вліво, 3=вправо)
Для штовхання ящика додатково потрібен ще один біт — чи потрапив ящик на ціль (біт 4):Біт 4: ящик потрапив на TARGET (нова клітинка ящика була TARGET)
Стек реалізується як масив з вказівником:undo_stack DB 512 DUP(0) ; масив записів
undo_top DW 0 ; індекс вершини
При скасуванні відкочуємо дію: переміщуємо гравця у зворотному напрямку, якщо був штовхнутий ящик — повертаємо його назад. Біти 3-4 допомагають коректно відновити тайли FLOOR/TARGET.Після кожного ходу зі штовханням ящика перевіряємо: чи всі цілі покриті ящиками?Підрахувати кількість клітинок зі значенням TARGET (3) у робочій сітці.
Якщо count == 0 → всі цілі покриті (BOX_ON_TARGET), рівень завершено.
Альтернативно: підтримувати лічильник непокритих цілей. При штовханні ящика НА ціль — зменшити. При зштовхуванні ящика З цілі (скасування) — збільшити. Коли лічильник дорівнює 0 — рівень завершено.; --- Коди тайлів ---
FLOOR EQU 0
WALL EQU 1
BOX EQU 2
TARGET EQU 3
PLAYER EQU 4
BOX_ON_TARGET EQU 5
PLAYER_ON_TARGET EQU 6 ; --- Таблиці зміщень напрямків ---
dir_dr DB -1, 1, 0, 0 ; вгору, вниз, вліво, вправо
dir_dc DB 0, 0, -1, 1 ; --- Спроба переміщення гравця у заданому напрямку ---
; Вхід: AL = напрямок (0=вгору, 1=вниз, 2=вліво, 3=вправо)
try_move PROC push bp mov bp, sp push bx push cx push dx push si ; ... ваш код тут ... ; 1. Знайти (dr, dc) через таблиці dir_dr/dir_dc ; 2. Обчислити цільову клітинку, перевірити межі ; 3. Прочитати тайл: WALL → вихід, FLOOR/TARGET → перемістити гравця ; 4. BOX/BOX_ON_TARGET → перевірити beyond, штовхнути якщо вільно ; 5. Записати хід у стек скасування ; див. алгоритм руху та зіткнень вище pop si pop dx pop cx pop bx pop bp ret
try_move ENDP Забули відновити TARGET під гравцем/ящиком. Коли гравець сходить з клітинки TARGET, вона має повернутися до TARGET, а не стати FLOOR. Те саме для ящиків. Використовуйте складені коди тайлів (PLAYER_ON_TARGET, BOX_ON_TARGET). Стек скасування не зберігає стан цілей. При скасуванні штовхання ящика потрібно знати, чи стояв ящик на цілі. Без цієї інформації скасування пошкодить стан сітки. Перевірка перемоги після кожного ходу замість лише після штовхання. Стан цілей змінюється тільки коли ящик рухається. Перевірка після простого руху гравця — зайва робота (хоча й не помилка). Модифікація оригінальних даних рівня. При перезапуску рівня потрібна незмінена копія. Завжди працюйте з робочою сіткою, копіюючи з оригіналу при завантаженні та перезапуску. Вихід за межі сітки при перевірці клітинки за ящиком. Якщо ящик стоїть біля краю, beyond може вийти за межі масиву. Завжди перевіряйте межі перед зчитуванням тайлу. <a class="internal-link" data-href="../arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви</a>
<br><a class="internal-link" data-href="../procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури</a>
<br><a class="internal-link" data-href="../flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Прапорці та переходи</a>
<br><a class="internal-link" data-href="../bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Побітові операції</a> — кодування записів скасування
<br><a class="internal-link" data-href="Y05.md" href="projects/how-to/game-logic/y05.html" target="_self" rel="noopener nofollow">Ігрова логіка: 2048</a> — інша робота з 2D-сіткою
<br><a class="internal-link" data-href="Y07.md" href="projects/how-to/game-logic/y07.html" target="_self" rel="noopener nofollow">Ігрова логіка: П'ятнашки</a> — обмін елементів у масиві
<br>
Корисно для: <a class="internal-link" data-href="../../Y06-Sokoban.md" href="projects/y06-sokoban.html" target="_self" rel="noopener nofollow">Y06 Сокобан</a>
]]></description><link>projects/how-to/game-logic/y06.html</link><guid isPermaLink="false">Projects/How-to/game-logic/Y06.md</guid><pubDate>Tue, 24 Feb 2026 13:50:52 GMT</pubDate></item><item><title><![CDATA[Y11]]></title><description><![CDATA[Алгоритми прокрутки смуг руху з різними швидкостями та напрямками, виявлення зіткнень жаби з транспортом та прогресії рівнів.
Потрібно рухати кілька смуг транспорту з різними швидкостями та напрямками одночасно
Потрібно визначити, чи жаба перекривається з рухомим транспортом
Потрібно реалізувати нескінченну горизонтальну прокрутку з обгортанням
Кожна смуга руху описана структурою фіксованого розміру в таблиці. Одна процедура move_lane обробляє будь-яку смугу, зчитуючи параметри з дескриптора:Структура дескриптора (5 байт на смугу): Байт 0: direction — 0 = вліво, 1 = вправо Байт 1: speed_div — дільник тактів (1 = кожен такт, 3 = кожен 3-й) Байт 2-3: pattern_off — зміщення на масив патерну (DW) Байт 4: current_pos — поточне зміщення прокрутки (0-79)
Таблиця з 8-12 записів повністю визначає рівень. Щоб додати нову смугу, достатньо додати один запис — код не змінюється.Кожна смуга має патерн транспорту довжиною 80 байт. Для рендерингу смуги на екрані використовується модулярна індексація:Для кожного стовпця c (0..79): index = (current_pos + c) MOD 80 symbol = pattern[index] записати symbol у відеопам'ять на позицію (row, c)
Щоб зсунути смугу на один крок:
Вліво: current_pos = (current_pos + 1) MOD 80
Вправо: current_pos = (current_pos + 79) MOD 80 (еквівалент -1 mod 80)
Оскільки 80 не є степенем двійки, AND не працює — потрібно DIV або порівняння з межею.Обробник INT 1Ch викликається ~18.2 разів на секунду. Щоб різні смуги рухались із різною швидкістю, використовується глобальний лічильник тактів і дільник:tick_counter++
Для кожної смуги i: якщо tick_counter MOD lane[i].speed_div == 0: зсунути lane[i].current_pos
Швидша смуга має speed_div = 1 (рух кожен такт), повільніша — speed_div = 3 (кожен третій).Жаба знаходиться в клітинці (frog_row, frog_col). Щоб перевірити зіткнення:
Визначити, якій смузі відповідає frog_row (пропускаючи безпечні зони).
Якщо це безпечна зона — зіткнення немає.
Якщо це смуга руху — зчитати символ патерну за (current_pos + frog_col) MOD 80.
Якщо символ не є пробілом (тобто це транспорт) — зіткнення.
Перевірку слід робити і після руху жаби, і після кожного зсуву смуги (підхід B з опису проєкту), щоб транспорт, що наїжджає на нерухому жабу, також виявлявся.Після успішного перетину (жаба досягає верхнього рядка):score++
level++
Для кожної смуги i: якщо lane[i].speed_div &gt; 1: lane[i].speed_div--
Повернути жабу в стартову зону
Мінімальний дільник обмежується значенням 1, щоб уникнути ділення на нуль і збереження іграбельності.; --- Таблиця дескрипторів смуг ---
LANE_SIZE EQU 5
NUM_LANES EQU 8 lane_table DB 0, 2, 00h, 00h, 0 ; смуга 0: вліво, speed=2, pattern offset=0000h DB 1, 3, 50h, 00h, 0 ; смуга 1: вправо, speed=3, offset=0050h ; ... ще 6 записів ... tick_count DW 0 ; --- Зсув однієї смуги ---
; Вхід: SI -&gt; дескриптор смуги
move_one_lane PROC PUSH AX PUSH BX ; 1. Перевірити дільник тактів: tick_count MOD speed_div ; 2. Якщо не час рухатись — вийти ; 3. Зсунути current_pos залежно від direction ; (обгортання: 0..79, див. алгоритм MOD 80 вище) ; ... ваш код тут ... POP BX POP AX RET
move_one_lane ENDP ; --- Перевірка зіткнення жаби ---
; Вхід: frog_row, frog_col -- глобальні змінні
; Вихід: AL = 1 якщо зіткнення, 0 якщо безпечно
check_collision PROC PUSH SI PUSH BX ; 1. Визначити індекс смуги за frog_row ; 2. Якщо безпечна зона — AL = 0, вихід ; 3. Завантажити дескриптор: lane_table + index * LANE_SIZE ; 4. Обчислити (current_pos + frog_col) MOD 80 ; 5. Зчитати символ патерну; пробіл = безпечно ; (див. алгоритм виявлення зіткнень вище) ; ... ваш код тут ... POP BX POP SI RET
check_collision ENDP Використання AND замість DIV для MOD 80. Оскільки 80 не є степенем двійки, AND AL, 7Fh дає MOD 128, а не MOD 80. Потрібно або DIV з дільником 80, або порівняння з межею (CMP AL, 80 / JB). Забування зберігати регістри в обробнику INT 1Ch. Обробник переривання спрацьовує асинхронно. Якщо він змінює AX, BX, SI без PUSH/POP, регістри головного циклу будуть зіпсовані. Перевірка зіткнень лише при русі жаби. Якщо транспорт наїжджає на нерухому жабу, це також має бути зіткнення. Перевіряйте після кожного зсуву смуги. Дільник тактів = 0 після прогресії. Якщо дільник зменшується нижче 1, DIV з нульовим дільником спричиняє апаратне переривання. Завжди обмежуйте мінімальний дільник значенням 1. <a class="internal-link" data-href="../lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиці пошуку</a>
<br><a class="internal-link" data-href="../arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви</a>
<br><a class="internal-link" data-href="../timer-delays.md" href="projects/how-to/timer-delays.html" target="_self" rel="noopener nofollow">Затримки на основі таймера</a>
<br><a class="internal-link" data-href="../interrupt-handlers.md" href="projects/how-to/interrupt-handlers.html" target="_self" rel="noopener nofollow">Обробники переривань</a>
<br><a class="internal-link" data-href="../video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Відеопам'ять</a>
<br><a class="internal-link" data-href="Y10.md" href="projects/how-to/game-logic/y10.html" target="_self" rel="noopener nofollow">Ігрова логіка: Space Invaders</a>
<br>
Корисно для: <a class="internal-link" data-href="../../Y11-Frogger.md" href="projects/y11-frogger.html" target="_self" rel="noopener nofollow">Y11 Фроґґер (Frogger)</a>
]]></description><link>projects/how-to/game-logic/y11.html</link><guid isPermaLink="false">Projects/How-to/game-logic/Y11.md</guid><pubDate>Tue, 24 Feb 2026 13:50:12 GMT</pubDate></item><item><title><![CDATA[Y20]]></title><description><![CDATA[Алгоритми побайтового XOR-шифрування з циклічним ключем, автентифікація за магічним заголовком та перевірка цілісності дешифрованих даних.
Реалізуєте XOR-шифрування/дешифрування масиву записів із майстер-паролем змінної довжини
Потрібно перевірити, чи правильний майстер-пароль (автентифікація через магічний заголовок)
Реалізуєте пошук за підрядком у масиві записів фіксованого розміру
XOR-шифрування працює побайтово: кожен байт даних XOR-ується з відповідним байтом ключа (майстер-пароля). Коли ключ коротший за дані, він повторюється циклічно:Дані: D0 D1 D2 D3 D4 D5 D6 D7 ...
Ключ: K0 K1 K2 K0 K1 K2 K0 K1 ... (довжина ключа = 3)
Результат: D0^K0 D1^K1 D2^K2 D3^K0 D4^K1 ...
Головна властивість XOR: шифрування та дешифрування — одна й та сама операція. Якщо E = D XOR K, то D = E XOR K.Циклічний індекс ключа реалізується двома способами:Спосіб з лічильником зі скиданням краще підходить для TASM, бо уникає повільної інструкції DIV.Перед шифруванням на початок даних додається 4-байтовий маркер (наприклад, "PWM" + 1Ah). Після дешифрування перевіряємо ці байти:Файл (зашифрований): [зашифровані 4 байти заголовка] [зашифровані записи...] Після дешифрування правильним паролем: ['P'] ['W'] ['M'] [1Ah] [запис 0] [запис 1] ... Після дешифрування неправильним паролем: [сміття] [сміття] [сміття] [сміття] [сміття] ...
Перевірка: побайтово порівняти перші 4 байти з еталоном. Якщо всі збігаються — пароль правильний. Інакше — відхилити.Символ 1Ah (Ctrl+Z) — маркер кінця файлу DOS, тому зашифрований файл не можна випадково прочитати командою TYPE.Для пошуку запису за назвою сайту потрібен алгоритм збігу підрядка. Простий підхід — "ковзне вікно":Для кожного запису i (від 0 до count-1): Для кожної позиції p у полі site (від 0 до 31 - search_len): Порівняти search_str з site[p..p+search_len-1] Якщо збіг → додати запис до результатів Перейти до наступного запису (один збіг достатній)
Оскільки поля мають фіксований розмір (32 байти для site), зміщення обчислюється як entries + i * 80 + p.; --- XOR-шифрування блоку даних із циклічним ключем ---
; Вхід: SI = адреса даних, CX = довжина даних (байти)
; master_pw = буфер ключа, pw_len = довжина ключа (байт)
; Вихід: дані за адресою SI зашифровані/дешифровані на місці
xor_encrypt PROC push si push bx push cx push dx ; ... ваш код тут ... ; Для кожного байта: XOR із відповідним байтом ключа ; Скидайте індекс ключа на 0, коли досягне pw_len ; див. алгоритм "Лічильник зі скиданням" вище pop dx pop cx pop bx pop si ret
xor_encrypt ENDP ; --- Перевірка магічного заголовка після дешифрування ---
; Вхід: SI = адреса дешифрованих даних
; Вихід: ZF=1 якщо заголовок коректний, ZF=0 якщо ні
MAGIC_HDR DB 'PWM', 1Ah ; еталон для порівняння check_magic PROC ; ... ваш код тут ... ; Побайтово порівняти [SI]..[SI+3] із MAGIC_HDR ; див. алгоритм "Автентифікація: магічний заголовок" вище ret
check_magic ENDP Помилка на одиницю при скиданні індексу ключа. Якщо скидати індекс при key_idx == pw_len замість key_idx &gt;= pw_len, все працює, але якщо перевіряти key_idx &gt; pw_len, один байт XOR-ується з нулем або сміттям. Перевірте в Turbo Debugger із 3-символьним паролем, що byte[3] використовує key[0]. Шифрування на місці без копії для відображення. Якщо зашифрувати масив записів перед завершенням відображення, на екрані з'явиться сміття. Тримайте дані дешифрованими протягом сесії; шифруйте лише безпосередньо перед записом у файл при виході. XOR із порожнім ключем. Якщо pw_len = 0, цикл ніколи не завершиться або читатиме сміття. Перевіряйте pw_len &gt; 0 перед викликом xor_encrypt. Магічний заголовок не шифрується. Заголовок має бути частиною шифрованого блоку даних, а не зберігатися окремо у відкритому вигляді. Інакше перевірка не залежить від пароля і завжди проходить. Переповнення буфера при введенні пароля. Без обмеження довжини введення можна перезаписати сусідні змінні. Завжди порівнюйте поточну довжину з максимумом перед збереженням символу. <a class="internal-link" data-href="../bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Побітові операції</a>
<br><a class="internal-link" data-href="../file-io.md" href="projects/how-to/file-io.html" target="_self" rel="noopener nofollow">Файловий ввід/вивід</a>
<br><a class="internal-link" data-href="../loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Цикли</a>
<br><a class="internal-link" data-href="../arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви</a>
<br><a class="internal-link" data-href="../keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід</a>
<br>
Корисно для: <a class="internal-link" data-href="../../Y20-Password-Manager.md" href="projects/y20-password-manager.html" target="_self" rel="noopener nofollow">Y20 Менеджер паролів</a>
]]></description><link>projects/how-to/game-logic/y20.html</link><guid isPermaLink="false">Projects/How-to/game-logic/Y20.md</guid><pubDate>Tue, 24 Feb 2026 13:50:09 GMT</pubDate></item><item><title><![CDATA[Y07]]></title><description><![CDATA[Алгоритми перевірки розв'язності через підрахунок інверсій, перемішування Фішера-Єйтса, обмеження переміщення плиток та визначення перемоги.
Генеруєте початкову конфігурацію дошки та хочете гарантувати розв'язність
Реалізуєте переміщення плиток відносно порожнього місця
Потрібно визначити, чи гравець зібрав головоломку
Алгоритм створює рівномірно розподілену випадкову перестановку:board = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0] Для i від 15 до 1: j = random(0 .. i) ; включно обміняти board[i] та board[j]
Важливо: j може дорівнювати i (елемент обмінюється сам із собою). Це необхідно для коректного розподілу.Не кожна перестановка плиток є розв'язною. Для сітки 4x4 правило таке:Інверсія — це пара плиток (a, b), де a стоїть перед b у порядку читання (зліва направо, зверху вниз), але a &gt; b. Порожнє місце (0) ігнорується.Підрахунок інверсій (вкладений цикл): inv = 0 Для i від 0 до 14: якщо board[i] == 0 → пропустити Для j від i+1 до 15: якщо board[j] == 0 → пропустити якщо board[i] &gt; board[j] → inv += 1
Правило розв'язності для 4x4:blank_row_from_bottom = 4 - (blank_position / 4) Розв'язна тоді і тільки тоді, коли (inv + blank_row_from_bottom) парне.
Якщо конфігурація нерозв'язна, найпростіше виправлення — обміняти будь-які дві ненульові плитки (наприклад, перші дві). Це змінить парність інверсій на протилежну.Гравець натискає стрілку, і плитка рухається на порожнє місце:Зверніть увагу: "стрілка вгору" означає "плитка рухається вгору" = "порожнє місце рухається вниз". Це інтуїтивніший варіант для гравця.Перевірка меж:Вгору: blank_pos + 4 &lt;= 15 (не в останньому рядку)
Вниз: blank_pos - 4 &gt;= 0 (не в першому рядку)
Вліво: blank_pos % 4 != 3 (не в останньому стовпці)
Вправо: blank_pos % 4 != 0 (не в першому стовпці)
Переміщення — це просто обмін двох елементів масиву:tile_pos = blank_pos + offset
swap board[blank_pos], board[tile_pos]
blank_pos = tile_pos
Перевірити, чи board[i] == i + 1 для i від 0 до 14, та board[15] == 0:Для i від 0 до 14: якщо board[i] != i + 1 → гра не завершена
Якщо board[15] == 0 → ПЕРЕМОГА
; --- Підрахунок інверсій для перевірки розв'язності ---
; Вхід: board -- масив 16 байтів
; Вихід: AX = кількість інверсій
count_inversions PROC push bp mov bp, sp push bx push cx push dx push si ; ... ваш код тут ... ; Вкладений цикл: для кожної пари (i, j) де j &gt; i, ; пропустити нулі, порівняти board[i] &gt; board[j] → інверсія ; див. алгоритм підрахунку інверсій вище pop si pop dx pop cx pop bx pop bp ret
count_inversions ENDP ; --- Перевірка розв'язності ---
; Вхід: board, blank_pos
; Вихід: AX = 1 якщо розв'язна, 0 якщо ні
check_solvable PROC push bp mov bp, sp ; ... ваш код тут ... ; 1. Викликати count_inversions → AX ; 2. Обчислити blank_row_from_bottom = 4 - (blank_pos / 4) ; 3. Розв'язна якщо (inv + blank_row_from_bottom) парне ; див. правило розв'язності вище pop bp ret
check_solvable ENDP Нерозв'язна початкова конфігурація. Якщо перемішати масив випадково без перевірки розв'язності, з ймовірністю 50% гравець ніколи не зможе зібрати головоломку. Завжди перевіряйте і виправляйте парність після перемішування. Помилка на одиницю у Фішера-Єйтса. Якщо random(0..i) генерує числа від 0 до i-1 замість 0 до i, перемішування буде зміщеним (елемент ніколи не залишиться на місці). Переконайтесь, що діапазон включає i. Інвертований напрямок стрілок. "Стрілка вгору" має рухати плитку вгору (тобто порожнє місце рухається вниз). Якщо переплутати, керування буде контрінтуїтивним. Порожнє місце виходить за межі при перевірці стовпця. Перевірка blank_pos - 1 &gt;= 0 недостатня для "вправо" — потрібно перевіряти blank_pos % 4 != 0, щоб порожнє місце не "перескочило" на попередній рядок. Порожнє місце враховується в підрахунку інверсій. Значення 0 (порожнє місце) повинно ігноруватися і в зовнішньому, і у внутрішньому циклі. Якщо його враховувати, результат перевірки розв'язності буде хибним. <a class="internal-link" data-href="../arithmetic.md" href="projects/how-to/arithmetic.html" target="_self" rel="noopener nofollow">Арифметичні операції</a>
<br><a class="internal-link" data-href="../loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Цикли</a>
<br><a class="internal-link" data-href="../random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">Генерація псевдовипадкових чисел</a>
<br><a class="internal-link" data-href="../flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Прапорці та переходи</a>
<br><a class="internal-link" data-href="Y05.md" href="projects/how-to/game-logic/y05.html" target="_self" rel="noopener nofollow">Ігрова логіка: 2048</a> — схожа робота з сіткою 4x4
<br><a class="internal-link" data-href="Y06.md" href="projects/how-to/game-logic/y06.html" target="_self" rel="noopener nofollow">Ігрова логіка: Сокобан</a> — складніша система зіткнень
<br>
Корисно для: <a class="internal-link" data-href="../../Y07-15-Puzzle.md" href="projects/y07-15-puzzle.html" target="_self" rel="noopener nofollow">Y07 П'ятнашки</a>
]]></description><link>projects/how-to/game-logic/y07.html</link><guid isPermaLink="false">Projects/How-to/game-logic/Y07.md</guid><pubDate>Tue, 24 Feb 2026 13:49:57 GMT</pubDate></item><item><title><![CDATA[Y22]]></title><description><![CDATA[Арифметика з фіксованою точкою у форматі 4.12, ітерація escape-time для множини Мандельброта, відображення координат екрану на комплексну площину та масштабування.
Реалізуєте множення та інші операції над числами з фіксованою точкою без FPU
Потрібен цикл ітерації z = z^2 + c з перевіркою виходу
Реалізуєте перетворення позиції (row, col) на екрані у координати комплексної площини
Потрібен алгоритм масштабування (zoom in/out) та панорамування
У форматі 4.12 число зберігається як 16-бітове знакове ціле, де 4 старших біти — ціла частина, а 12 молодших — дробова. Масштабний множник: 4096 (= 2^12).Діапазон: від -8.0 до +7.999... Точність: ~0.000244 (1/4096). Множина Мандельброта лежить у межах x: [-2.5, 1.0], y: [-1.25, 1.25] — це зручно вміщується у 4 цілих біти.Множення двох чисел формату 4.12 дає результат формату 8.24 у парі DX:AX. Щоб повернути результат у формат 4.12, потрібно зсунути DX:AX вправо на 12 біт:A (4.12) * B (4.12) = результат (8.24) у DX:AX
Зсув вправо на 12 → результат (4.12) у AX Приклад: 1.5 * 2.0 AX = 1800h (1.5), BX = 2000h (2.0) IMUL BX → DX:AX = 0300:0000h Зсув вправо на 12 → AX = 3000h (= 3.0) ✓
Два підходи до зсуву на 12:Спосіб 1 — комбінування бітів (швидкий):Результат = (DX SHL 4) OR (AX SHR 12)
Зсуваємо DX вліво на 4 (залишаємо молодші біти у правильній позиції) та AX вправо на 12 (залишаємо старші біти), потім об'єднуємо через OR.Спосіб 2 — циклічний зсув (простий):Повторити 12 разів: SAR DX, 1 / RCR AX, 1
Повільніше (12 ітерацій), але простіше для розуміння та налагодження.Для кожної комірки екрану з координатою c = (cx, cy):zx = 0, zy = 0
Для iter від 0 до max_iter-1: zx2 = fix_mul(zx, zx) ; zx^2 zy2 = fix_mul(zy, zy) ; zy^2 Якщо zx2 + zy2 &gt; 4.0 (= 4000h) → вихід, повернути iter new_zy = 2 * fix_mul(zx, zy) + cy ; "2*" = SHL 1 new_zx = zx2 - zy2 + cx zx = new_zx zy = new_zy
Повернути max_iter ; точка належить множині
Порядок обчислень важливий: new_zy використовує старе значення zx, тому обчислюйте new_zy до перезапису zx.Комірка (row, col) перетворюється на координати:step_x = scale / 80 ; крок по горизонталі
step_y = scale / 25 ; крок по вертикалі
cx = center_x + (col - 40) * step_x
cy = center_y + (row - 12) * step_y
Оскільки ділення на 80 та 25 дорогі, можна наблизити:
scale / 80 — ділення на 64+16 складне; простіше: зберігати step_x як окрему змінну та перераховувати при зміні масштабу
Або зберігати масштаб як step_x безпосередньо (крок на комірку), а повний діапазон обчислювати як step_x * 80
Множення на 3/4: IMUL на 3, потім SAR на 2. Множення на 4/3: SHL на 2, потім ділення на 3 через IDIV.Межа масштабування: якщо step_x або step_y стає 0 після ділення — точність вичерпана. Відмовте у подальшому збільшенні та покажіть "Precision limit" на рядку стану.; --- Тимчасові змінні для ітерації ---
zx2 DW ? ; zx^2
zy2 DW ? ; zy^2 ; --- Множення з фіксованою точкою (формат 4.12) ---
; Вхід: AX = множене (4.12), BX = множник (4.12)
; Вихід: AX = результат (4.12)
fix_mul PROC imul bx ; DX:AX = результат (8.24) ; ... ваш код тут ... ; Зсунути DX:AX вправо на 12 біт → результат у AX ; див. "Спосіб 1 -- комбінування бітів" вище ret
fix_mul ENDP ; --- Ітерація Мандельброта для однієї точки ---
; Вхід: cx_coord = координата Re(c), cy_coord = координата Im(c)
; max_iter = максимум ітерацій
; Вихід: AX = кількість ітерацій до виходу
mandelbrot_iter PROC push bx push cx push dx push si push di xor si, si ; SI = zx = 0 xor di, di ; DI = zy = 0 xor cx, cx ; CX = лічильник ітерацій @@iter_loop: ; ... ваш код тут ... ; 1. zx2 = fix_mul(zx, zx), zy2 = fix_mul(zy, zy) ; 2. Якщо zx2 + zy2 &gt; 4000h → вихід (escaped) ; 3. new_zy = 2 * fix_mul(zx, zy) + cy (обчислити ДО new_zx!) ; 4. new_zx = zx2 - zy2 + cx ; 5. inc cx, порівняти з max_iter ; див. алгоритм "Ітерація escape-time" вище ; Точка належить множині mov ax, cx jmp @@done @@escaped: mov ax, cx ; AX = кількість ітерацій @@done: pop di pop si pop dx pop cx pop bx ret
mandelbrot_iter ENDP Використання MUL замість IMUL. Координати Мандельброта можуть бути від'ємними. MUL (беззнакове множення) дає неправильний результат для від'ємних значень. Завжди використовуйте IMUL та SAR (арифметичний зсув). Переповнення при zx^2 + zy^2. Якщо не перевіряти вихід вчасно, zx та zy зростають, і при наступному множенні результат переповнює 16 біт. Перевіряйте zx2 + zy2 &gt; 4000h перед обчисленням нового z. Некоректний зсув після IMUL. Зсув DX:AX вправо на 12 — найпоширеніше джерело помилок. Протестуйте: 1.0 * 2.0 має дати 2.0 (1000h * 2000h → зсув → 2000h). Якщо результат неправильний — помилка у процедурі зсуву. Обчислення new_zx перезаписує zx до обчислення new_zy. Формула new_zy = 2*zx*zy + cy використовує старе значення zx. Обчислюйте new_zy першим або зберігайте zx у тимчасовій змінній. Масштабування до step = 0. При глибокому збільшенні крок на комірку може округлитись до нуля у фіксованій точці. Весь екран показуватиме одну й ту саму точку. Перевіряйте step &gt; 0 після кожного масштабування. <a class="internal-link" data-href="../arithmetic.md" href="projects/how-to/arithmetic.html" target="_self" rel="noopener nofollow">Арифметика</a>
<br><a class="internal-link" data-href="../bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Побітові операції</a>
<br><a class="internal-link" data-href="../loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Цикли</a>
<br><a class="internal-link" data-href="../lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиці пошуку</a>
<br><a class="internal-link" data-href="../video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Відеопам'ять</a>
<br>
Корисно для: <a class="internal-link" data-href="../../Y22-Mandelbrot-Viewer.md" href="projects/y22-mandelbrot-viewer.html" target="_self" rel="noopener nofollow">Y22 Переглядач множини Мандельброта</a>
]]></description><link>projects/how-to/game-logic/y22.html</link><guid isPermaLink="false">Projects/How-to/game-logic/Y22.md</guid><pubDate>Tue, 24 Feb 2026 13:49:52 GMT</pubDate></item><item><title><![CDATA[G05]]></title><description><![CDATA[Алгоритми обчислення виразів (ЗПН та інфіксний режим), розбору чисел та керування кільцевим буфером історії.
Потрібно обчислити арифметичний вираз, введений користувачем, з правильним порядком операцій.
Потрібно зберігати попередні обчислення у буфері фіксованого розміру з автоматичним видаленням найстаріших записів.
Потрібно перетворити рядок ASCII-цифр у 16-бітне ціле число і навпаки.
ЗПН використовує стек операндів. Користувач спочатку вводить числа, потім оператор. Алгоритм:
Якщо токен — число: покласти на стек (PUSH).
Якщо токен — оператор (+, -, *, /): зняти два операнди зі стека, виконати операцію, покласти результат.
Після обробки всіх токенів на стеку лишається одне значення — результат.
Ввід: 12 34 + 5 *
Крок 1: стек = [12]
Крок 2: стек = [12, 34]
Крок 3 (+): 12 + 34 = 46 → стек = [46]
Крок 4: стек = [46, 5]
Крок 5 (*): 46 * 5 = 230 → стек = [230]
Результат: 230
Стек реалізується як масив слів зі змінною-вказівником:eval_stack DW 16 DUP(0) ; масив на 16 елементів
stack_ptr DW 0 ; індекс наступної вільної позиції
Для виразів виду 12 + 34 * 5 потрібен алгоритм сортувальної станції (Shunting Yard). Він використовує два стеки: стек операндів та стек операторів.Правила пріоритету:Алгоритм для кожного токена:
Число — покласти на стек операндів.
Оператор — поки на стеку операторів є оператор з пріоритетом &gt;= поточного, зняти його та виконати операцію. Потім покласти поточний оператор.
Кінець виразу — зняти та виконати всі оператори, що залишились.
Ввід: 12 + 34 * 5
→ 12 операнди=[12] оператори=[]
→ + операнди=[12] оператори=[+]
→ 34 операнди=[12,34] оператори=[+]
→ * пріоритет(*) &gt; пріоритет(+), тому просто кладемо операнди=[12,34] оператори=[+,*]
→ 5 операнди=[12,34,5] оператори=[+,*]
→ кінець виконуємо *: 34*5=170 операнди=[12,170] оператори=[+] виконуємо +: 12+170=182 операнди=[182] оператори=[]
Результат: 182
Перетворення рядка цифр у 16-бітне значення виконується зліва направо:value = 0
для кожного символу c у рядку: value = value * 10 + (c - '0')
Після кожного MUL перевіряйте, чи не виникло переповнення (DX != 0 означає, що число перевищує 65535).Кільцевий буфер дозволяє зберігати останні N записів без зсуву пам'яті. Структура:ENTRY_SIZE EQU 42 ; 40 символів виразу + 2 байти результату
HIST_CAP EQU 16 ; ємність буфера history DB HIST_CAP * ENTRY_SIZE DUP(0)
hist_head DW 0 ; індекс найстарішого запису
hist_count DW 0 ; поточна кількість записів
Додавання нового запису:позиція = (hist_head + hist_count) MOD HIST_CAP
якщо hist_count &lt; HIST_CAP: hist_count++
інакше: hist_head = (hist_head + 1) MOD HIST_CAP
записати дані за адресою: history + позиція * ENTRY_SIZE
Зчитування i-го запису (від найновішого):індекс = (hist_head + hist_count - 1 - i) MOD HIST_CAP
адреса = history + індекс * ENTRY_SIZE
; --- Дані стека операндів ---
eval_stack DW 16 DUP(0)
stack_ptr DW 0 ; --- Дані кільцевого буфера історії ---
ENTRY_SIZE EQU 42
HIST_CAP EQU 16
history DB HIST_CAP * ENTRY_SIZE DUP(0)
hist_head DW 0
hist_count DW 0
; --- Обчислення операції ЗПН: зняти два операнди, виконати, покласти результат ---
; Вхід: AL = код оператора ('+', '-', '*', '/')
; Вихід: результат покладено на стек операндів (eval_stack / stack_ptr) rpn_apply_op PROC push bx push cx push dx ; 1. Зняти два операнди зі стека (див. алгоритм ЗПН вище) ; 2. Виконати диспетчеризацію за оператором (+, -, *, /) ; 3. Покласти результат назад на стек ; ... ваш код тут ... pop dx pop cx pop bx ret
rpn_apply_op ENDP
; --- Додавання запису до кільцевого буфера історії ---
; Вхід: SI = вказівник на рядок виразу, AX = результат
; Вихід: запис додано до history, лічильники оновлено history_add PROC push bx push cx push di ; 1. Обчислити позицію: (hist_head + hist_count) MOD HIST_CAP ; 2. Обчислити адресу: history + позиція * ENTRY_SIZE ; 3. Скопіювати рядок виразу та записати результат ; 4. Оновити hist_count або просунути hist_head (див. алгоритм кільцевого буфера вище) ; ... ваш код тут ... pop di pop cx pop bx ret
history_add ENDP Не перевірили стек на порожність перед POP. Якщо користувач ввів оператор без достатньої кількості операндів, зняття зі стека зчитає сміття. Перевіряйте stack_ptr &gt;= 2 перед виконанням оператора. Забули CWD перед IDIV. Без розширення знаку DX:AX містить неправильне значення для від'ємних чисел. Наприклад, -10 / 3 дасть хибний результат, якщо DX = 0 замість FFFFh. Помилка MOD у кільцевому буфері. На 8086 немає інструкції MOD — використовуйте DIV або порівняння з ємністю. Часта помилка: забути обробити випадок, коли індекс дорівнює ємності (потрібно скинути на 0). Переповнення при розборі числа. Якщо value * 10 перевищує 65535, DX після MUL буде ненульовим. Ігнорування цього призведе до мовчки хибного результату. <a class="internal-link" data-href="../arithmetic.md" href="projects/how-to/arithmetic.html" target="_self" rel="noopener nofollow">Арифметичні інструкції</a> — MUL, DIV, IMUL, IDIV
<br><a class="internal-link" data-href="../procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури</a> — стекові операції та угоди про виклик
<br><a class="internal-link" data-href="../arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви</a> — робота з буферами та кільцевими структурами
<br><a class="internal-link" data-href="../number-display.md" href="projects/how-to/number-display.html" target="_self" rel="noopener nofollow">Відображення чисел</a> — перетворення числа в ASCII-рядок
<br>
Корисно для: <a class="internal-link" data-href="../../G05-Calculator-With-History.md" href="projects/g05-calculator-with-history.html" target="_self" rel="noopener nofollow">G05 Калькулятор з історією</a>
]]></description><link>projects/how-to/game-logic/g05.html</link><guid isPermaLink="false">Projects/How-to/game-logic/G05.md</guid><pubDate>Tue, 24 Feb 2026 13:49:50 GMT</pubDate></item><item><title><![CDATA[R03]]></title><description><![CDATA[Алгоритми структури рядкового буфера, вставки/видалення символів зі зсувом, розділення та об'єднання рядків і вертикального прокручування viewport.
Потрібно організувати текстовий буфер із швидким доступом до будь-якого рядка
Вставка/видалення символу не повинні зсувати весь файл, а лише поточний рядок
Enter розділяє рядок на два; Backspace на початку рядка об'єднує з попереднім
Курсор має прокручувати viewport при виході за межі 24 видимих рядків
Замість одного плоского масиву використовуємо таблицю рядків: кожен рядок — це окремий буфер фіксованого розміру.MAX_LINES EQU 100
MAX_LINE_LEN EQU 128 line_buf DB MAX_LINES * MAX_LINE_LEN DUP(0) ; 12800 байтів
line_len DW MAX_LINES DUP(0) ; 200 байтів
num_lines DW 1 ; кількість рядків
Адреса рядка N обчислюється без вказівників:address = line_buf + N * MAX_LINE_LEN
length = line_len[N * 2]
Переваги порівняно з плоским масивом:
Вставка символу зсуває максимум 128 байтів (один рядок), а не весь файл
Доступ до рядка за номером — O(1) через множення
Розділення рядка — локальна операція на двох буферах
При вставці символу в позицію (line, col):
Обчислити адресу рядка: addr = line_buf + line * MAX_LINE_LEN
Перевірити: line_len[line] &lt; MAX_LINE_LEN - 1 (є місце)
Зсунути байти від col до кінця рядка на одну позицію праворуч
Записати символ у позицію col
Збільшити line_len[line]
Зсув виконуємо з кінця (щоб не затерти дані):Для i від line_len-1 вниз до col: buffer[i+1] = buffer[i]
buffer[col] = new_char
Видалення — зворотна операція: зсуваємо байти від col+1 до кінця ліворуч на одну позицію.Натискання Enter у позиції (line, col):
Створити місце для нового рядка: зсунути всі дескриптори рядків від line+1 до num_lines-1 на одну позицію вниз у таблиці
Скопіювати хвіст поточного рядка (від col до кінця) в новий рядок line+1
Встановити довжину нового рядка: old_len - col
Обрізати поточний рядок: line_len[line] = col
Збільшити num_lines
Перемістити курсор на (line+1, 0)
Backspace у позиції (line, 0) з line &gt; 0:
Запам'ятати new_col = line_len[line-1] (майбутня позиція курсора)
Додати вміст рядка line в кінець рядка line-1
Оновити довжину: line_len[line-1] += line_len[line]
Видалити рядок line: зсунути всі дескриптори від line+1 до num_lines-1 на одну позицію вгору
Зменшити num_lines
Перемістити курсор на (line-1, new_col)
Зсув дескрипторів при видаленні рядка — це копіювання кожного буфера рядка (128 байтів) та його довжини. Альтернатива — використовувати масив вказівників замість фіксованих буферів, тоді зсувається лише масив вказівників (4 байти на рядок).Viewport визначається змінною viewport_top (номер першого видимого рядка). Видимий діапазон: [viewport_top, viewport_top + 23].Після кожної операції, що змінює позицію курсора:IF cursor_line &lt; viewport_top: viewport_top = cursor_line
IF cursor_line &gt; viewport_top + 23: viewport_top = cursor_line - 23
Page Down: viewport_top += 24, обмежити зверху num_lines - 24. Page Up: viewport_top -= 24, обмежити знизу 0.Після зміни viewport — перемалювати всі 24 рядки екрана.; Структура даних рядкового буфера
MAX_LINES EQU 100
MAX_LINE_LEN EQU 128 line_buf DB MAX_LINES * MAX_LINE_LEN DUP(0) ; буфери рядків
line_len DW MAX_LINES DUP(0) ; довжини рядків
num_lines DW 1 ; кількість рядків
; --- Вставка символу в рядок ---
; Вхід: AX = номер рядка, BX = стовпець, CL = символ
; Вихід: CF=1 якщо рядок переповнений
buf_insert_char PROC push si push di push dx ; 1. Перевірити: line_len[line] &lt; MAX_LINE_LEN - 1 ; 2. Обчислити адресу: line_buf + line * MAX_LINE_LEN ; 3. Зсунути байти від col до кінця праворуч (з кінця, STD + REP MOVSB) ; 4. Записати символ у позицію col, збільшити line_len ; див. алгоритм "Вставка символу зі зсувом" вище ; ... ваш код тут ... clc pop dx pop di pop si ret
@@overflow: stc pop dx pop di pop si ret
buf_insert_char ENDP
; --- Розділення рядка (Enter) ---
; Вхід: AX = номер рядка, BX = стовпець
buf_split_line PROC push si push di push cx ; 1. Зсунути дескриптори рядків від num_lines-1 до line+1 вниз (з кінця!) ; 2. Скопіювати хвіст рядка (від col) в новий рядок line+1 ; 3. Встановити довжини: новий = old_len - col, поточний = col ; 4. Збільшити num_lines ; див. алгоритм "Розділення рядка (Enter)" вище ; ... ваш код тут ... pop cx pop di pop si ret
buf_split_line ENDP
; --- Обмеження курсора після операцій ---
clamp_cursor PROC ; 1. cursor_line: обмежити 0..num_lines-1 ; 2. cursor_col: обмежити 0..line_len[cursor_line] ; 3. Прокручування viewport: ; IF cursor_line &lt; viewport_top: viewport_top = cursor_line ; IF cursor_line &gt; viewport_top + 23: viewport_top = cursor_line - 23 ; див. алгоритм "Вертикальне прокручування viewport" вище ; ... ваш код тут ... ret
clamp_cursor ENDP Зсув рядків при розділенні в неправильному порядку. Зсувати дескриптори потрібно з кінця масиву, інакше дані затираються. Аналогічно вставці в масив — починайте з останнього елемента.
Забутий clamp_cursor. Після об'єднання рядків, видалення або прокручування курсор може вказувати за межі рядка або файлу. Викликайте clamp_cursor після кожної модифікації.
Плоский масив замість рядкового буфера. Вставка одного символу в плоский масив на 2000 байтів зсуває в середньому 1000 байтів. У рядковому буфері зсувається максимум 128 байтів.
Obj'єднання без перевірки довжини. При об'єднанні двох рядків сумарна довжина може перевищити MAX_LINE_LEN. Перевіряйте перед копіюванням і відхиляйте операцію, якщо немає місця.
Viewport не оновлюється після Page Up/Down. Зміна viewport_top без перемалювання всіх 24 рядків — екран показує застарілі дані. Після будь-якої зміни viewport перемалюйте весь екран. <a class="internal-link" data-href="../arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Обробка масивів</a>
<br><a class="internal-link" data-href="../procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури</a>
<br><a class="internal-link" data-href="../keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід</a>
<br><a class="internal-link" data-href="../file-io.md" href="projects/how-to/file-io.html" target="_self" rel="noopener nofollow">Файловий ввід/вивід</a>
<br><a class="internal-link" data-href="../video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Відеопам'ять</a>
<br>
Корисно для: <a class="internal-link" data-href="../../R03-Mini-Text-Editor.md" href="projects/r03-mini-text-editor.html" target="_self" rel="noopener nofollow">R03 Міні-текстовий редактор</a>
]]></description><link>projects/how-to/game-logic/r03.html</link><guid isPermaLink="false">Projects/How-to/game-logic/R03.md</guid><pubDate>Tue, 24 Feb 2026 13:49:46 GMT</pubDate></item><item><title><![CDATA[Y05]]></title><description><![CDATA[Алгоритми зсуву, стиснення та злиття плиток, правило одноразового злиття за хід, визначення порожнього ходу та кінця гри.
Реалізуєте зсув плиток у будь-якому з чотирьох напрямків
Хочете коректно обробити злиття однакових плиток без подвійного злиття
Потрібно визначити, чи змінив хід стан поля (порожній хід)
Реалізуєте перевірку кінця гри (немає доступних ходів)
Будь-який зсув зводиться до обробки одного рядка з чотирьох елементів. Алгоритм для зсуву вліво складається з трьох кроків:
Стиснення — зібрати всі ненульові значення на початок, заповнити решту нулями
Злиття — пройти зліва направо; якщо два сусідніх елементи рівні, подвоїти лівий, обнулити правий
Повторне стиснення — зібрати ненульові значення знову (після злиття можуть з'явитися проміжки)
Приклад для рядка [0, 2, 2, 4]:Крок 1 (стиснення): [2, 2, 4, 0]
Крок 2 (злиття): [4, 0, 4, 0] (рахунок += 4)
Крок 3 (стиснення): [4, 4, 0, 0]
Ключове правило: кожна плитка зливається щонайбільше один раз за хід. Завдяки двопрохідному підходу (стиснення-злиття-стиснення) це правило виконується автоматично, адже злиття проходить рівно один раз.Граничний випадок [2, 2, 2, 2]:Крок 1 (стиснення): [2, 2, 2, 2] (без змін)
Крок 2 (злиття): [4, 0, 4, 0] (рахунок += 4+4)
Крок 3 (стиснення): [4, 4, 0, 0] ← правильно, а не [8, 0, 0, 0]
Замість чотирьох окремих процедур, реалізуйте базовий slide_row_left та трансформуйте координати:Альтернативний підхід: передавати у процедуру початковий індекс та крок (stride). Для рядка 0 вліво: start=0, stride=1. Для стовпця 0 вгору: start=0, stride=4. Для рядка 0 вправо: start=3, stride=-1.Перед зсувом збережіть копію поля (16 слів = 32 байти). Після зсуву порівняйте з копією:; Порівняння масивів по словах
mov cx, 16
lea si, board_copy
lea di, board
compare_loop: mov ax, [si] cmp ax, [di] jne board_changed add si, 2 add di, 2 loop compare_loop
; Якщо дійшли сюди -- поле не змінилося, хід порожній
jmp skip_new_tile
board_changed: ; Хід валідний, додати нову плитку
Якщо поле не змінилося, нову плитку не додаємо і рахунок не оновлюємо.Гра закінчується, коли одночасно:
Усі 16 клітинок зайняті (немає нулів)
Жодна пара сусідніх клітинок не має однакового значення
Перевірка: Для кожної клітинки (i): якщо board[i] == 0 → гра НЕ закінчена якщо i не в останньому стовпці і board[i] == board[i+1] → НЕ закінчена якщо i не в останньому рядку і board[i] == board[i+4] → НЕ закінчена Якщо жодна умова не спрацювала → GAME OVER
; --- Тимчасовий буфер для стиснення ---
temp DW 4 DUP(0) ; --- Стиснення рядка: зібрати ненульові значення на початок ---
; Вхід: SI = адреса рядка (4 слова)
; Вихід: рядок стиснутий на місці (ненульові зліва, нулі справа)
compress_row PROC push bp mov bp, sp push si push di push cx ; ... ваш код тут ... ; 1. Скопіювати ненульові елементи з [SI] у temp ; 2. Заповнити решту temp нулями ; 3. Скопіювати temp назад у рядок ; див. алгоритм стиснення вище pop cx pop di pop si pop bp ret
compress_row ENDP ; --- Злиття сусідніх однакових у рядку ---
; Вхід: SI = адреса рядка (4 слова)
; Вихід: AX = додана сума (для рахунку)
merge_row PROC push bp mov bp, sp push si push cx ; ... ваш код тут ... ; Пройти 3 пари [0,1], [1,2], [2,3]: ; якщо обидва ненульові та рівні → подвоїти лівий, обнулити правий ; див. алгоритм злиття вище pop cx pop si pop bp ret
merge_row ENDP Подвійне злиття за один хід. Рядок [2,2,4,4] має дати [4,8,0,0], а не [16,0,0,0]. Двопрохідний алгоритм (стиснення-злиття-стиснення) вирішує цю проблему автоматично. Неправильний порядок обходу для зсуву вправо. При зсуві вправо плитки мають оброблятися справа наліво. Якщо обробляти зліва направо, злиття відбуватиметься в неправильному напрямку. Порожній хід додає нову плитку. Забули перевірити, чи змінився стан поля. Завжди порівнюйте стан до і після зсуву. Хибне визначення кінця гри. Перевіряють лише горизонтальних сусідів, забувши про вертикальних. Потрібно перевірити і board[i] == board[i+1], і board[i] == board[i+4]. Переплутані індекси стовпців та рядків. При зсуві вгору/вниз stride дорівнює 4 (для масиву слів — 8 байтів), а не 1. <a class="internal-link" data-href="../arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви</a>
<br><a class="internal-link" data-href="../loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Цикли</a>
<br><a class="internal-link" data-href="../procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури</a>
<br><a class="internal-link" data-href="Y06.md" href="projects/how-to/game-logic/y06.html" target="_self" rel="noopener nofollow">Ігрова логіка: Сокобан</a> — схожа робота з 2D-сіткою
<br><a class="internal-link" data-href="Y07.md" href="projects/how-to/game-logic/y07.html" target="_self" rel="noopener nofollow">Ігрова логіка: П'ятнашки</a> — обмін елементів у масиві
<br>
Корисно для: <a class="internal-link" data-href="../../Y05-2048.md" href="projects/y05-2048.html" target="_self" rel="noopener nofollow">Y05 2048</a>
]]></description><link>projects/how-to/game-logic/y05.html</link><guid isPermaLink="false">Projects/How-to/game-logic/Y05.md</guid><pubDate>Tue, 24 Feb 2026 13:49:42 GMT</pubDate></item><item><title><![CDATA[Y19]]></title><description><![CDATA[Алгоритми CRUD-операцій над записами фіксованого розміру, сортування вставками масиву записів та пошук підрядка без урахування регістру.
Потрібно зберігати набір записів фіксованого розміру та виконувати додавання, редагування, видалення
Потрібно відсортувати масив великих записів за текстовим полем
Потрібно шукати підрядок у полі запису без урахування регістру
Кожен контакт — запис фіксованого розміру 96 байтів:Зміщення Поле Розмір +0 Ім'я 32 байти (доповнене нулями) +32 Телефон 16 байтів +48 Нотатка 48 байтів = 96 байтів загалом
Масив контактів:contacts DB 50 DUP(96 DUP(0)) ; 4800 байтів
count DW 0 ; поточна кількість контактів
Доступ до запису N: зміщення = N * 96. Оскільки 96 = 64 + 32, множення можна замінити зсувами:; AX = індекс запису N
; Результат: SI = зміщення запису від початку contacts
MOV BX, AX
SHL AX, 6 ; AX = N * 64
SHL BX, 5 ; BX = N * 32
ADD AX, BX ; AX = N * 96
LEA SI, contacts
ADD SI, AX ; SI -&gt; початок запису N
Додавання — найпростіша операція. Новий запис записується в кінець масиву:1. Перевірити: count &lt; 50 (максимум). Якщо ні -- повідомлення про помилку.
2. Обчислити зміщення: offset = count * 96
3. Скопіювати дані полів форми в contacts[offset..offset+95]
4. Збільшити count на 1
Видалення запису N потребує зсуву всіх наступних записів на 96 байтів назад:1. Кількість записів для зсуву: move_count = count - N - 1
2. ЯКЩО move_count &gt; 0: a. SI = адреса запису N+1 b. DI = адреса запису N c. CX = move_count * 96 d. REP MOVSB (ES = DS для копіювання в тому ж сегменті)
3. Зменшити count на 1
Критичний момент: перед REP MOVSB переконатися, що ES = DS (не B800h від попереднього виведення на екран).Сортування вставками добре підходить для малих масивів (до 50 елементів). Алгоритм переміщує 96-байтові блоки:temp DB 96 DUP(?) ; тимчасовий буфер для одного запису Для i = 1 до count - 1: 1. Скопіювати record[i] у temp (96 байтів, REP MOVSB) 2. j = i 3. ПОКИ j &gt; 0 І record[j-1].name &gt; temp.name: a. Скопіювати record[j-1] у record[j] (96 байтів) b. j = j - 1 4. Скопіювати temp у record[j]
Порівняння імен — побайтове порівняння до 32 символів:; SI -&gt; ім'я запису A, DI -&gt; ім'я запису B
; Результат: прапорці як після CMP
compare_names: MOV CX, 32
@@cmp: MOV AL, [SI] MOV AH, [DI] ; Привести до нижнього регістру для порівняння OR AL, 20h OR AH, 20h CMP AL, AH JNE @@done ; прапорці встановлені INC SI INC DI LOOP @@cmp
@@done:
Пошук перевіряє поле імені кожного контакту. Порівняння виконується після приведення обох символів до нижнього регістру через OR 20h:Для кожного контакту k = 0 до count - 1: SI = contacts + k * 96 ; початок поля імені Для i = 0 до 32 - query_len: збіг = true Для j = 0 до query_len - 1: a = SI[i + j] OR 20h b = query[j] OR 20h ЯКЩО a != b: збіг = false, BREAK ЯКЩО збіг: додати k до display_indices[], BREAK (достатньо одного збігу)
Масив display_indices зберігає індекси контактів, що відповідають пошуку. У звичайному режимі (без фільтра) він містить 0, 1, 2, ..., count-1.; --- Константи запису ---
RECORD_SIZE EQU 96
NAME_OFF EQU 0
PHONE_OFF EQU 32
NOTE_OFF EQU 48
MAX_CONTACTS EQU 50 .DATA
contacts DB MAX_CONTACTS DUP(RECORD_SIZE DUP(0))
count DW 0
temp_record DB RECORD_SIZE DUP(?) .CODE
; --- Отримати вказівник на запис ---
; Вхід: AX = індекс запису
; Вихід: SI = зміщення в масиві contacts
get_record_ptr PROC PUSH BX PUSH DX ; ... ваш код тут ... ; SI = contacts + AX * 96; використайте зсуви: 96 = 64 + 32 POP DX POP BX RET
get_record_ptr ENDP ; --- Видалення запису ---
; Вхід: AX = індекс запису для видалення
delete_contact PROC PUSH SI PUSH DI PUSH CX PUSH ES ; ... ваш код тут ... ; Зсуньте записи після AX на 96 байтів назад (REP MOVSB) ; Не забудьте: ES = DS перед копіюванням; див. алгоритм вище DEC count POP ES POP CX POP DI POP SI RET
delete_contact ENDP ; --- Порівняння імен без урахування регістру ---
; Вхід: SI -&gt; ім'я A, DI -&gt; ім'я B
; Вихід: прапорці (ZF, CF) як після CMP
compare_names PROC PUSH CX PUSH SI PUSH DI ; ... ваш код тут ... ; Побайтово порівняйте 32 символи; OR 20h для регістру POP DI POP SI POP CX RET
compare_names ENDP Неправильне множення на 96. Помилка в зсувах (наприклад, SHL AX, 7 замість SHL AX, 6) дає зміщення 128 замість 96 — і кожен запис зчитується не з тієї адреси. Визначте процедуру get_record_ptr і використовуйте її скрізь. ES = B800h під час REP MOVSB для зсуву записів. REP MOVSB записує в ES:DI. Якщо ES залишилась B800h від попереднього виведення на екран, дані запишуться у відеопам'ять. Завжди робіть PUSH DS / POP ES перед блочними копіями. OR 20h ламає порівняння нелітерних символів. OR 20h перетворює 'A'-'Z' на 'a'-'z', але також змінює інші символи (цифри, пробіли). Для простої адресної книги це прийнятно, бо імена зазвичай містять лише літери. Для коректного рішення — перевіряйте діапазон 41h-5Ah перед OR. Забути оновити display_indices після додавання/видалення. Якщо пошуковий фільтр активний і користувач додає новий контакт, він не з'явиться у відфільтрованому списку, поки фільтр не застосується повторно. Поле не доповнене нулями при редагуванні. Якщо старе ім'я "Олександр" (10 байт), а нове "Олег" (4 байти), залишок "ксандр" все ще в пам'яті. Перед записом нового значення заповніть поле нулями: MOV CX, 32 / XOR AL, AL / REP STOSB, потім запишіть нове ім'я. <a class="internal-link" data-href="../arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви</a>
<br><a class="internal-link" data-href="../file-io.md" href="projects/how-to/file-io.html" target="_self" rel="noopener nofollow">Файловий ввід/вивід</a>
<br><a class="internal-link" data-href="../procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури</a>
<br><a class="internal-link" data-href="../bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Побітові операції</a>
<br><a class="internal-link" data-href="../addressing-modes.md" href="projects/how-to/addressing-modes.html" target="_self" rel="noopener nofollow">Режими адресації</a>
<br><a class="internal-link" data-href="Y18.md" href="projects/how-to/game-logic/y18.html" target="_self" rel="noopener nofollow">Ігрова логіка: Mini Grep</a> — також використовує пошук підрядка
<br><a class="internal-link" data-href="Y20.md" href="projects/how-to/game-logic/y20.html" target="_self" rel="noopener nofollow">Ігрова логіка: менеджер паролів</a> — подібний шаблон CRUD
<br>
Корисно для: <a class="internal-link" data-href="../../Y19-Address-Book.md" href="projects/y19-address-book.html" target="_self" rel="noopener nofollow">Y19 Адресна книга</a>
]]></description><link>projects/how-to/game-logic/y19.html</link><guid isPermaLink="false">Projects/How-to/game-logic/Y19.md</guid><pubDate>Tue, 24 Feb 2026 13:49:39 GMT</pubDate></item><item><title><![CDATA[R02]]></title><description><![CDATA[Алгоритми позиціонування м'яча з фіксованою точкою 8.8, зіткнення м'яча зі стінами/платформою/цеглинами та обчислення кутів відбиття.
М'яч має рухатися плавно під різними кутами, а не лише 45 градусів
Потрібно правильно визначати, звідки м'яч влучив у цеглину (збоку чи зверху/знизу)
Кут відбиття від платформи має залежати від місця влучання
Цеглини мають зникати при влученні та коректно оновлюватися в масиві
Позиція та швидкість м'яча зберігаються як 16-бітні слова у форматі 8.8:Старший байт = ціла частина (координата комірки)
Молодший байт = дробова частина (0..255 = 0..0.996)
Приклади значень:Рух м'яча — одна інструкція на вісь:ADD [ball_x], dx_fp ; x += швидкість_x
ADD [ball_y], dy_fp ; y += швидкість_y
Для отримання цілої частини (номер комірки для відображення):MOV AX, [ball_x]
MOV AL, AH ; AL = ціла частина
XOR AH, AH ; AX = номер стовпця
Після оновлення позиції перевіряємо межі. Ціла частина визначає комірку:screen_x = ball_x &gt;&gt; 8
screen_y = ball_y &gt;&gt; 8 Якщо screen_x &lt;= left_wall: NEG dx_fp, ball_x = left_wall &lt;&lt; 8
Якщо screen_x &gt;= right_wall: NEG dx_fp, ball_x = right_wall &lt;&lt; 8
Якщо screen_y &lt;= top_wall: NEG dy_fp, ball_y = top_wall &lt;&lt; 8
Важливо: після відбиття коригуємо позицію, щоб м'яч не "застряг" за стіною.М'яч стикається з платформою, коли:
screen_y == paddle_row (м'яч на рядку платформи)
paddle_x &lt;= screen_x &lt;= paddle_x + paddle_width - 1
dy_fp &gt; 0 (м'яч летить вниз)
Кут відбиття залежить від місця влучання. Поділяємо платформу на зони та використовуємо таблицю пошуку для нового dx:Позиція на платформі: 0-1 2-3 4-5 6-7 8-9
Нове dx (фікс. точка): FE00 FF00 curr 0100 0200
Опис: -2 -1 без +1 +2 зміни
dy завжди стає від'ємним (м'яч летить вгору): NEG dy_fp (якщо dy &gt; 0).Цеглини зберігаються в масиві (наприклад, 8 рядків x 10 стовпців). Алгоритм:
Обчислити індекс цеглини за позицією м'яча:
brick_row = screen_y - BRICK_START_ROW
brick_col = (screen_x - BRICK_START_COL) / BRICK_WIDTH Перевірити межі: якщо brick_row або brick_col поза діапазоном — немає зіткнення
Перевірити масив: bricks[brick_row * COLS + brick_col]
Якщо цеглина існує: Видалити її з масиву (записати 0)
Стерти з екрана (записати пробіли у відеопам'ять)
Збільшити рахунок
Визначити напрямок відбиття Визначення осі відбиття: Використовуємо напрямок швидкості м'яча:Якщо м'яч рухається вправо (dx &gt; 0) і цеглина справа: NEG dx_fp
Якщо м'яч рухається вліво (dx &lt; 0) і цеглина зліва: NEG dx_fp
Якщо м'яч рухається вниз (dy &gt; 0) і цеглина знизу: NEG dy_fp
Якщо м'яч рухається вгору (dy &lt; 0) і цеглина зверху: NEG dy_fp
Спрощений підхід: перевірити, по якій осі м'яч увійшов у комірку цеглини. Якщо попередній screen_x був поза цеглиною, а новий — всередині, відбити dx. Аналогічно для dy. Якщо обидві осі змінились (кутовий удар) — відбити обидві.; --- Оновлення позиції м'яча та перевірка стін ---
update_ball PROC push ax push bx ; Рух м'яча: додати швидкість до позиції (формат 8.8) ; ADD [ball_x_fp], dx_fp / ADD [ball_y_fp], dy_fp ; ... ваш код тут ... ; Перевірка стін: витягти цілу частину (старший байт), ; порівняти з LEFT_WALL / RIGHT_WALL / TOP_WALL, ; при зіткненні: NEG швидкість + скоригувати позицію на межу ; див. алгоритм "Зіткнення зі стінами" вище ; ... ваш код тут ... pop bx pop ax ret
update_ball ENDP
; --- Зіткнення з платформою ---
; Вхід: AL = screen_x м'яча, BL = screen_y м'яча
; Вихід: CF=1 якщо відбито
check_paddle PROC ; 1. Перевірити: dy &gt; 0? М'яч на PADDLE_ROW? В межах paddle_x..paddle_x+width? ; 2. Обчислити зону влучання та взяти нове dx з paddle_angle_table ; 3. NEG dy_fp (м'яч летить вгору) ; див. алгоритм "Зіткнення з платформою" вище ; ... ваш код тут ... stc ret
@@no_hit: clc ret
check_paddle ENDP ; Таблиця кутів відбиття від платформи (5 зон)
paddle_angle_table DW 0FE00h ; зона 0: крутий ліворуч (-2.0) DW 0FF00h ; зона 1: помірний ліворуч (-1.0) DW 00000h ; зона 2: вертикально (dx=0) DW 00100h ; зона 3: помірний праворуч (+1.0) DW 00200h ; зона 4: крутий праворуч (+2.0)
; --- Перевірка зіткнення з цеглиною ---
; Вхід: AL = screen_x, BL = screen_y
; Вихід: CF=1 якщо цеглину знищено
check_brick PROC push si ; 1. Обчислити brick_row = screen_y - BRICK_START_ROW (перевірити межі) ; 2. Обчислити brick_col = (screen_x - BRICK_START_COL) / BRICK_WIDTH (перевірити межі) ; 3. Індекс = brick_row * BRICK_COLS + brick_col ; 4. Якщо bricks[індекс] != 0: знищити, збільшити score, відбити ; див. алгоритм "Зіткнення з цеглинами" вище ; ... ваш код тут ... pop si stc ret
@@no_brick: pop si clc ret
check_brick ENDP М'яч рухається лише під 45 градусів. Використання цілочисельних позицій замість фіксованої точки обмежує м'яч чотирма напрямками. Формат 8.8 вирішує цю проблему.
М'яч застрягає за стіною. Після NEG dx_fp м'яч може залишитись за межею та відбиватися нескінченно. Після відбиття обов'язково коригуйте позицію на межу.
Неоднозначність осі при влученні в цеглину. Без аналізу напрямку руху м'яч може відбитися по неправильній осі. Порівнюйте попередню та нову позицію, щоб визначити вісь входу.
Зіткнення з платформою зі зміщенням на 1. Неточне порівняння y-координати — м'яч проходить крізь платформу або відбивається на рядок вище. Перевіряйте точні координати в Turbo Debugger.
Знакова арифметика при від'ємній швидкості. dx_fp та dy_fp мають бути знаковими. ADD коректно працює для знакових значень, але SHR для витягнення цілої частини не зберігає знак — використовуйте MOV AL, AH з CBW або SAR. <a class="internal-link" data-href="../arithmetic.md" href="projects/how-to/arithmetic.html" target="_self" rel="noopener nofollow">Арифметика</a>
<br><a class="internal-link" data-href="../arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Обробка масивів</a>
<br><a class="internal-link" data-href="../bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Побітові операції</a>
<br><a class="internal-link" data-href="../interrupt-handlers.md" href="projects/how-to/interrupt-handlers.html" target="_self" rel="noopener nofollow">Перехоплення переривань</a>
<br><a class="internal-link" data-href="../lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиці пошуку</a>
<br><a class="internal-link" data-href="R01.md" href="projects/how-to/game-logic/r01.html" target="_self" rel="noopener nofollow">R01 Тетріс — ігрова логіка</a>
<br>
Корисно для: <a class="internal-link" data-href="../../R02-Breakout.md" href="projects/r02-breakout.html" target="_self" rel="noopener nofollow">R02 Breakout</a>
]]></description><link>projects/how-to/game-logic/r02.html</link><guid isPermaLink="false">Projects/How-to/game-logic/R02.md</guid><pubDate>Tue, 24 Feb 2026 13:49:27 GMT</pubDate></item><item><title><![CDATA[Y21]]></title><description><![CDATA[Алгоритми малювання ліній та прямокутників, заливка зв'язаної області (flood fill) з явним стеком та знімки полотна для скасування.
Реалізуєте малювання горизонтальних/вертикальних ліній між двома точками
Реалізуєте контурний або заповнений прямокутник за двома кутами
Потрібен алгоритм заливки (flood fill), який не переповнює апаратний стек
Реалізуєте механізм скасування через LIFO-стек знімків полотна
Для горизонтальної або вертикальної лінії алгоритм простий: обчислити початкове зміщення у відеопам'яті та рухатися з фіксованим кроком.Зміщення комірки (row, col) у B800h: offset = row * 160 + col * 2.Перед малюванням відсортуйте координати: якщо start &gt; end, обміняйте їх. Це гарантує, що цикл завжди рухається у додатному напрямку.Малювання горизонтальної лінії: Якщо col_start &gt; col_end → XCHG col_start, col_end DI = row * 160 + col_start * 2 CX = col_end - col_start + 1 Цикл CX разів: ES:[DI] ← символ ES:[DI+1] ← атрибут DI += 2
Контурний прямокутник — це чотири лінії. Після сортування кутів (r1, c1) та (r2, c2):1. Верхня горизонтальна: (r1, c1) → (r1, c2)
2. Нижня горизонтальна: (r2, c1) → (r2, c2)
3. Ліва вертикальна: (r1+1, c1) → (r2-1, c1)
4. Права вертикальна: (r1+1, c2) → (r2-1, c2)
Бокові лінії починаються з r1+1 та закінчуються на r2-1, щоб не перезаписувати кутові символи. Якщо r1 == r2, прямокутник вироджується у горизонтальну лінію; якщо c1 == c2 — у вертикальну.Для заповненого прямокутника замість чотирьох ліній малюємо r2 - r1 + 1 горизонтальних ліній:Для r від r1 до r2: Малювати горизонтальну лінію (r, c1) → (r, c2)
Рекурсивна заливка на великій площі (до 1920 комірок) переповнить апаратний стек. Замість неї використовуйте явний стек у сегменті даних.Кожен елемент стеку — слово, де старший байт = row, молодший байт = col:fill_stack DW 2000 DUP(?) ; макс. 2000 елементів = 4000 байтів
stack_top DW 0 ; індекс вершини
Алгоритм:1. Зчитати символ під курсором → target_char
2. Якщо target_char == new_char → вийти (нічого заливати)
3. PUSH (cursor_row, cursor_col) у fill_stack
4. Поки stack_top &gt; 0: a. POP (r, c) з fill_stack b. Зчитати символ із відеопам'яті за (r, c) c. Якщо символ != target_char → пропустити (вже залито або інший символ) d. Записати new_char + new_attr за адресою (r, c) e. Для кожного сусіда (r-1,c), (r+1,c), (r,c-1), (r,c+1): - Якщо в межах полотна (0 &lt;= r &lt; 24, 0 &lt;= col &lt; 80): PUSH (сусід) у fill_stack
Крок (c) запобігає повторному додаванню: комірка, у яку вже записано new_char, не проходить перевірку символ != target_char, тому не генерує нових сусідів.Перед деструктивною операцією зберігаємо повний знімок полотна (3840 байтів) у буфер:Структура стеку скасувань (3 рівні): undo_buf DB 3 * 3840 DUP(?) ; 11520 байтів undo_top DW 0 ; 0, 1, 2 або 3
Копіювання найефективніше через REP MOVSW (1920 слів):push_undo: DS:SI = B800h:0000 ; джерело -- відеопам'ять ES:DI = undo_buf + top*3840 ; призначення CX = 1920 REP MOVSW pop_undo: DS:SI = undo_buf + top*3840 ; джерело ES:DI = B800h:0000 ; призначення -- відеопам'ять CX = 1920 REP MOVSW
Не забудьте зберегти та відновити DS та ES, бо REP MOVSW змінює обидва сегментні регістри.; --- Дані для явного стеку заливки ---
fill_stack DW 2000 DUP(?) ; макс. 2000 елементів (слово = упакований row:col)
stack_top DW 0 ; поточна кількість елементів
target DB ? ; символ, який заливаємо ; --- Заливка (flood fill) з явним стеком ---
; Вхід: DH = row, DL = col курсора
; cur_char = новий символ, cur_attr = новий атрибут
; Вихід: область однакових символів залита новим символом
flood_fill PROC push bx push cx push dx push si push di ; ... ваш код тут ... ; 1. Зчитати символ під курсором → target ; 2. Якщо target == cur_char → вийти ; 3. PUSH початкову позицію у fill_stack ; 4. Цикл: POP → перевірити → записати → PUSH сусідів ; див. алгоритм "Заливка з явним стеком" вище pop di pop si pop dx pop cx pop bx ret
flood_fill ENDP Переповнення стеку при рекурсивній заливці. Рекурсивний виклик для 1920 комірок потребує ~8 КБ апаратного стеку. Завжди використовуйте явний стек у сегменті даних. Заливка зациклюється, бо не перевіряє target == new. Якщо цільовий символ і новий символ однакові, кожна залита комірка знову потрапляє у стек. Перевірте target_char != new_char на початку. Координати кутів прямокутника не відсортовані. Якщо користувач позначив правий верхній кут першим, а лівий нижній другим, цикл рахує у від'ємному напрямку або не виконується взагалі. Завжди сортуйте: if r1 &gt; r2 then XCHG. Скасування зберігає знімок на кожне натискання клавіші. У режимі вільного малювання стек скасувань заповнюється миттєво. Зберігайте знімок один раз на початку штриха (при активації інструменту), а не на кожному символі. Забули відновити DS/ES після REP MOVSW. Блочне копіювання між сегментом даних та B800h потребує зміни DS або ES. Якщо не відновити — наступні операції з даними зламаються. <a class="internal-link" data-href="../video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Відеопам'ять</a>
<br><a class="internal-link" data-href="../procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури</a>
<br><a class="internal-link" data-href="../arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви</a>
<br><a class="internal-link" data-href="../bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Побітові операції</a>
<br><a class="internal-link" data-href="../keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід</a>
<br>
Корисно для: <a class="internal-link" data-href="../../Y21-ASCII-Art-Editor.md" href="projects/y21-ascii-art-editor.html" target="_self" rel="noopener nofollow">Y21 Редактор ASCII-графіки</a>
]]></description><link>projects/how-to/game-logic/y21.html</link><guid isPermaLink="false">Projects/How-to/game-logic/Y21.md</guid><pubDate>Tue, 24 Feb 2026 13:49:08 GMT</pubDate></item><item><title><![CDATA[G04]]></title><description><![CDATA[Алгоритми валідації відповідей, підрахунку балів, рендерингу індикатора прогресу та визначення порогів результатів.
Потрібно зберігати та індексувати запитання з варіантами відповідей
Потрібно перевіряти відповідь гравця та показувати зворотний зв'язок
Потрібно відображати індикатор прогресу та фінальну статистику
Кожне запитання зберігається як запис фіксованого розміру (256 байтів):Зміщення Розмір Поле
0 80 Текст запитання (доповнений пробілами, '$'-terminated)
80 40 Варіант A ('$'-terminated)
120 40 Варіант B
160 40 Варіант C
200 40 Варіант D
240 1 Правильна відповідь (0=A, 1=B, 2=C, 3=D)
241 15 Зарезервовано
Доступ до запитання N: зміщення = N * 256. Це дозволяє пряму адресацію через MUL.Гравець натискає A/B/C/D (або a/b/c/d). Нормалізація та перевірка за 4 інструкції:1. AND AL, 0DFh ; 'a' -&gt; 'A', 'b' -&gt; 'B' (очистити біт 5)
2. SUB AL, 'A' ; 'A'-&gt;0, 'B'-&gt;1, 'C'-&gt;2, 'D'-&gt;3
3. CMP AL, 3 ; перевірка діапазону
4. JA invalid ; &gt; 3 -- некоректна клавіша, ігнорувати
Після нормалізації порівняти з байтом правильної відповіді:CMP AL, [SI + Q_ANSWER] ; SI вказує на початок запису
JE correct
JMP wrong +10 балів за правильну відповідь, 0 за неправильну
Максимум: 10 запитань * 10 = 100 балів
Відсоток: score * 100 / max_score, або просто score (бо max = 100)
Кількість правильних: score / 10
Пороги повідомлень:CMP score, 80
JAE excellent ; "Excellent!" для 80+
CMP score, 60
JAE good ; "Good job!" для 60-79
JMP keep_learning ; "Keep learning!" для &lt; 60
10 блочних символів у рядку 0. Для запитання N (від 1 до 10):Стовпці 20-29: кожна клітинка = 2 байти у відеопам'яті
Клітинка i (0-9): зміщення = (20 + i) * 2 = 40 + i * 2 якщо i &lt; N: символ DBh, атрибут 0Ah (зелений) якщо i &gt;= N: символ DBh, атрибут 08h (темно-сірий)
Це проста процедура з одним циклом на 10 ітерацій.1. Обрати категорію (меню)
2. Ініціалізувати: score=0, correct_count=0, question_num=0
3. Зберегти start_ticks (INT 1Ah)
4. Цикл (10 разів): a. Очистити екран b. Намалювати прогрес-бар c. Намалювати рахунок d. Завантажити запис запитання N: SI = questions + N * Q_SIZE e. Відобразити запитання та варіанти f. Зчитати клавішу, валідувати g. Перевірити відповідь h. Показати зворотний зв'язок (зелений/червоний), пауза i. N++
5. Зберегти end_ticks
6. Показати екран результатів
; --- Константи ---
Q_SIZE EQU 256
Q_TEXT EQU 0
Q_OPT_A EQU 80
Q_OPT_B EQU 120
Q_OPT_C EQU 160
Q_OPT_D EQU 200
Q_ANSWER EQU 240 ; --- Дані ---
score DW 0
correct_count DB 0
wrong_count DB 0
question_num DB 0 msg_excellent DB 'Excellent!', '$'
msg_good DB 'Good job!', '$'
msg_learn DB 'Keep learning!', '$' ; --- Валідація та перевірка відповіді ---
; Вхід: AL = натиснута клавіша, SI = вказівник на запис запитання
; Вихід: AL = 1 (правильно), AL = 0 (неправильно), AL = FFh (некоректна клавіша)
; AH = індекс правильної відповіді (0-3)
check_answer PROC PUSH BX ; Нормалізувати регістр: AND AL, 0DFh ; Перетворити на індекс: SUB AL, 'A' ; Перевірити діапазон 0-3 ; Порівняти з [SI + Q_ANSWER] ; Див. алгоритм «Валідація відповіді» вище ; ... ваш код тут ... POP BX RET
check_answer ENDP ; --- Малювання індикатора прогресу ---
; Вхід: CL = номер поточного запитання (1-10)
draw_progress PROC PUSH DI PUSH CX PUSH AX MOV DI, 40 ; стовпець 20 * 2 = зміщення 40 у рядку 0 MOV CH, 0 ; лічильник клітинок (0-9) ; Цикл 10 ітерацій: символ DBh, атрибут 0Ah (якщо CH &lt; CL) або 08h ; Див. алгоритм «Рендеринг індикатора прогресу» вище ; ... ваш код тут ... POP AX POP CX POP DI RET
draw_progress ENDP ; --- Визначення повідомлення результату ---
; Вхід: AX = рахунок (0-100)
; Вихід: SI = вказівник на рядок повідомлення
get_result_msg PROC ; Порівняти AX з порогами 80 та 60 ; Див. алгоритм «Підрахунок балів та відсотків» вище ; ... ваш код тут ... RET
get_result_msg ENDP MUL затирає DX. При обчисленні зміщення запитання MUL Q_SIZE записує старше слово в DX. Якщо DX використовувався як лічильник — його значення втрачено. Зберігайте DX перед MUL. Чутливість до регістру. Без AND AL, 0DFh натискання 'a' не розпізнається як відповідь A. Біт 5 в ASCII — це біт регістру. Рядок довший за 80 стовпців. Запитання понад 80 символів «переливається» на наступний рядок у відеопам'яті автоматично (бо відеопам'ять лінійна), але макет зламається. Або обмежте довжину запитань, або додайте перенесення рядка в процедуру виведення. Переповнення при обчисленні відсотків. correct * 100 для 10 правильних = 1000, що вміщується в 16 біт. Але якщо використовувати байтову арифметику (MUL з 8-бітним операндом), результат обмежений 16 бітами в AX — це працює, але потрібно спочатку розширити correct_count до слова. <a class="internal-link" data-href="../arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви</a>
<br><a class="internal-link" data-href="../video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять</a>
<br><a class="internal-link" data-href="../flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Прапорці та умовні переходи</a>
<br><a class="internal-link" data-href="G03.md" href="projects/how-to/game-logic/g03.html" target="_self" rel="noopener nofollow">Ігрова логіка: Memory Card</a>
<br>
Корисно для: <a class="internal-link" data-href="../../G04-Quiz-Trivia-Game.md" href="projects/g04-quiz-trivia-game.html" target="_self" rel="noopener nofollow">G04 Вікторина</a>
]]></description><link>projects/how-to/game-logic/g04.html</link><guid isPermaLink="false">Projects/How-to/game-logic/G04.md</guid><pubDate>Tue, 24 Feb 2026 13:48:39 GMT</pubDate></item><item><title><![CDATA[G03]]></title><description><![CDATA[Алгоритми перемішування Фішера-Йетса, автомат станів картки, перевірка збігу пар та затримка для запам'ятовування.
Потрібно випадково розмістити пари символів на сітці
Потрібно реалізувати логіку перевертання карток та перевірки збігу
Потрібно керувати станами карток (лицем вниз / відкрита / знайдена)
Гра використовує два паралельні масиви по 16 байтів:card_symbols DB 'A','A','B','B','C','C','D','D' DB 'E','E','F','F','G','G','H','H'
card_state DB 16 DUP(0)
Стани картки: 0 = лицем вниз, 1 = відкрита (щойно перевернута), 2 = знайдена пара.Позиція на сітці 4x4: індекс = рядок * 4 + стовпець (0..15).Алгоритм гарантує рівномірний розподіл за рівно 15 обмінів:for i = 15 downto 1: j = random(0..i) ; включно swap(card_symbols[i], card_symbols[j])
Для генерації random(0..i):
Отримати наступне значення ЛКГ: seed = (seed * 25173 + 13849) AND FFFFh
Взяти seed MOD (i + 1) через DIV
Зерно ініціалізується з молодшого слова лічильника тактів INT 1Ah при першому натисканні клавіші гравцем.Кожна картка проходить через стани:[Лицем вниз] --Enter--&gt; [Відкрита] | збіг? ні збігу? | | [Знайдена] [Лицем вниз] (після паузи)
Логіка ходу:1. Гравець натискає Enter на картці з state = 0
2. Встановити state = 1, перемалювати картку (показати символ)
3. Це перше чи друге перевертання? - Перше: зберегти first_index, чекати другого - Друге: зберегти second_index, перейти до перевірки
4. Перевірка збігу: card_symbols[first_index] == card_symbols[second_index]? - Так: обидва state = 2 (знайдена пара), pairs_found++ - Ні: пауза ~1.5 сек, обидва state = 0 (назад лицем вниз)
5. move_count++
; first_index та second_index вже збережені
MOV BL, first_index
XOR BH, BH
MOV AL, [card_symbols + BX] ; символ першої картки MOV BL, second_index
CMP AL, [card_symbols + BX] ; порівняти з другою
JE match_found
; --- не збіг: перевернути назад після паузи ---
Пауза ~1.5 секунди (27 тактів) через опитування INT 1Ah:1. INT 1Ah AH=00h -&gt; зберегти DX як start_tick
2. Цикл: a. INT 1Ah AH=00h -&gt; поточний DX b. SUB DX, start_tick c. CMP DX, 27 d. JB цикл
3. Очистити буфер клавіатури перед поверненням до гри
Під час паузи НЕ зчитувати клавіші — інакше гравець може випадково натиснути Enter і спровокувати дії.; --- Дані ---
card_symbols DB 'A','A','B','B','C','C','D','D' DB 'E','E','F','F','G','G','H','H'
card_state DB 16 DUP(0)
first_index DB 0FFh ; FFh = жодна картка не перевернута
second_index DB 0
pairs_found DB 0
move_count DW 0
rng_seed DW 0 ; --- Перемішування Фішера-Йетса ---
shuffle PROC PUSH SI PUSH CX PUSH BX MOV CX, 15 ; i = 15 downto 1
@@loop: ; Генерувати j = random(0..CX) через ЛКГ: seed = (seed * 25173 + 13849) AND FFFFh ; Потім j = seed MOD (CX + 1) ; Обміняти card_symbols[CX] та card_symbols[j] ; Див. алгоритм «Перемішування Фішера-Йетса» вище ; ... ваш код тут ... LOOP @@loop POP BX POP CX POP SI RET
shuffle ENDP ; --- Обробка перевертання картки ---
; Вхід: BL = індекс картки (0-15)
; Вихід: AL = результат (0 = перше перевертання,
; 1 = пара знайдена, 2 = не збіг, FFh = ігнорувати)
process_flip PROC PUSH BX ; Перевірити, чи картка лицем вниз (card_state[BX] == 0) ; Якщо ні -- ігнорувати ; ... ваш код тут ... ; Перевернути: card_state[BX] = 1 ; Визначити, перше чи друге перевертання (first_index == FFh?) ; Якщо перше -- зберегти first_index, повернути AL=0 ; Якщо друге -- порівняти card_symbols[first] з card_symbols[second] ; Див. алгоритм «Автомат станів картки» вище ; ... ваш код тут ... POP BX RET
process_flip ENDP Перевертання тієї самої картки двічі. Якщо гравець натискає Enter на вже відкритій картці, first_index == second_index, що дає хибний збіг. Перевіряйте CMP BL, first_index перед другим перевертанням. Натискання клавіш під час паузи. Якщо цикл паузи також зчитує клавіші, гравець може випадково перевернути наступну картку до завершення анімації. Під час паузи опитуйте лише INT 1Ah, а після паузи очистіть буфер клавіатури. Однакове перемішування щоразу. Якщо зерно ГВЧ ініціалізується константою, кожна гра буде ідентичною. Використовуйте лічильник тактів INT 1Ah як зерно, зчитуючи його при першому натисканні клавіші гравцем. MOD через DIV без очищення DX. Перед DIV BX для отримання залишку в DX потрібно XOR DX, DX, інакше DX містить сміття від попередніх операцій. <a class="internal-link" data-href="../random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">Генерація псевдовипадкових чисел</a>
<br><a class="internal-link" data-href="../arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви</a>
<br><a class="internal-link" data-href="../timer-delays.md" href="projects/how-to/timer-delays.html" target="_self" rel="noopener nofollow">Затримки через системний годинник</a>
<br>
Корисно для: <a class="internal-link" data-href="../../G03-Memory-Card-Game.md" href="projects/g03-memory-card-game.html" target="_self" rel="noopener nofollow">G03 Гра на запам'ятовування карток</a>
]]></description><link>projects/how-to/game-logic/g03.html</link><guid isPermaLink="false">Projects/How-to/game-logic/G03.md</guid><pubDate>Tue, 24 Feb 2026 13:48:39 GMT</pubDate></item><item><title><![CDATA[G01]]></title><description><![CDATA[Алгоритми виявлення перемоги через таблицю пошуку, ШІ на основі правил (виграш/блокування/центр/кут) та визначення нічиї.
Потрібно перевірити, чи хтось виграв після кожного ходу
Потрібно реалізувати комп'ютерного супротивника, який грає розумно
Потрібно визначити, коли гра закінчилась нічиєю
Дошка зберігається як плоский масив із 9 байтів у сегменті даних. Кожен байт має значення: 0 = порожньо, 1 = X (гравець), 2 = O (комп'ютер).Індекси масиву: Позиції клавіш: 0 | 1 | 2 1 | 2 | 3 --------- --------- 3 | 4 | 5 4 | 5 | 6 --------- --------- 6 | 7 | 8 7 | 8 | 9
Гравець натискає клавішу 1-9, програма віднімає 1, щоб отримати індекс 0-8 у масиві.Замість 8 окремих блоків CMP, усі виграшні комбінації зберігаються в одній таблиці пошуку (24 байти = 8 трійок по 3 індекси):win_lines DB 0,1,2 ; верхній рядок DB 3,4,5 ; середній рядок DB 6,7,8 ; нижній рядок DB 0,3,6 ; лівий стовпець DB 1,4,7 ; центральний стовпець DB 2,5,8 ; правий стовпець DB 0,4,8 ; діагональ \ DB 2,4,6 ; діагональ /
Алгоритм перевірки: пройти циклом по 8 рядках таблиці. Для кожної трійки (a, b, c) завантажити board[a], board[b], board[c]. Якщо всі три рівні та не дорівнюють нулю — це перемога.Комп'ютер перевіряє правила в порядку пріоритету й обирає перший доступний хід:
Виграш. Перебрати 8 ліній. Якщо в лінії 2 клітинки = O і 1 порожня — поставити туди.
Блокування. Перебрати 8 ліній. Якщо в лінії 2 клітинки = X і 1 порожня — заблокувати.
Центр. Якщо клітинка 4 порожня — зайняти центр.
Кут. Перевірити клітинки 0, 2, 6, 8 — зайняти першу порожню.
Ребро. Перевірити клітинки 1, 3, 5, 7 — зайняти першу порожню.
Кроки 1 та 2 використовують ту саму таблицю win_lines — різниця лише в тому, чий символ шукаємо (O для виграшу, X для блокування).Після кожного ходу, якщо перемогу не виявлено, перевірити лічильник ходів. Якщо зроблено 9 ходів — нічия. Альтернативно, пройти масив і перевірити, чи є хоча б одна клітинка зі значенням 0.; --- Таблиця виграшних ліній ---
win_lines DB 0,1,2, 3,4,5, 6,7,8 DB 0,3,6, 1,4,7, 2,5,8 DB 0,4,8, 2,4,6
board DB 9 DUP(0)
corner_list DB 0, 2, 6, 8
edge_list DB 1, 3, 5, 7 ; --- Перевірка перемоги ---
; Вхід: нічого (працює з масивом board)
; Вихід: AL = 0 (немає переможця), 1 (X виграв), 2 (O виграв)
check_win PROC PUSH SI PUSH BX PUSH CX LEA SI, win_lines MOV CX, 8 ; 8 виграшних ліній ; Цикл по win_lines: завантажити 3 індекси, порівняти board[a], board[b], board[c] ; Якщо всі три рівні і не нуль -- перемога ; Див. алгоритм «Виявлення перемоги» вище ; ... ваш код тут ... POP CX POP BX POP SI RET
check_win ENDP ; --- Вибір ходу ШІ ---
; Вхід: нічого
; Вихід: AL = індекс клітинки (0-8) для ходу O
ai_move PROC PUSH SI PUSH CX ; Крок 1: спробувати виграти (шукати лінію з 2×O + порожня) ; Крок 2: заблокувати гравця (шукати лінію з 2×X + порожня) ; Кроки 1-2 використовують ту саму таблицю win_lines ; Див. алгоритм «ШІ на основі правил» вище ; ... ваш код тут ... ; Крок 3: центр (клітинка 4) ; Крок 4: кути (corner_list) ; Крок 5: ребра (edge_list) ; ... ваш код тут ... POP CX POP SI RET
ai_move ENDP Не перевіряти порожність клітинки перед ходом ШІ. Якщо ШІ обирає зайняту клітинку, він перезапише символ гравця. Кожен кандидат на хід повинен мати перевірку CMP board[BX], 0. Жорстко задані 8 блоків if замість таблиці. Це працює, але будь-яка помилка в одному з індексів важко помітна. Таблиця win_lines + цикл коротша та надійніша. Забути зберегти регістри в процедурі перевірки. check_win змінює SI, BX, CX — без PUSH/POP виклик зламає зовнішній цикл. Плутати індекс масиву та ASCII-код клавіші. Клавіша '1' має ASCII 31h. Індекс масиву 0 = клавіша '1'. Потрібно SUB AL, 31h одразу після зчитування INT 16h. <a class="internal-link" data-href="../lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиці пошуку</a>
<br><a class="internal-link" data-href="../arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви</a>
<br><a class="internal-link" data-href="../flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Прапорці та умовні переходи</a>
<br><a class="internal-link" data-href="Y04.md" href="projects/how-to/game-logic/y04.html" target="_self" rel="noopener nofollow">Ігрова логіка: Connect Four</a>
<br>
Корисно для: <a class="internal-link" data-href="../../G01-Tic-Tac-Toe.md" href="projects/g01-tic-tac-toe.html" target="_self" rel="noopener nofollow">G01 Хрестики-нулики з ШІ-супротивником</a>
]]></description><link>projects/how-to/game-logic/g01.html</link><guid isPermaLink="false">Projects/How-to/game-logic/G01.md</guid><pubDate>Tue, 24 Feb 2026 13:48:34 GMT</pubDate></item><item><title><![CDATA[G02]]></title><description><![CDATA[Алгоритми обчислення WPM через цілочислову арифметику, відсотка точності, порівняння символів у реальному часі з кольоровим зворотним зв'язком.
Потрібно обчислити швидкість друку (WPM) без дробових чисел
Потрібно відстежувати правильність кожного натиснутого символу в реальному часі
Потрібно коректно працювати з таймером на основі тактів INT 1Ah
Під час набору програма тримає два вказівники:
SI — індекс у рядку завдання (очікуваний символ)
DI — позиція у відеопам'яті для відображення набраного символу
Після кожного натискання клавіші:1. Зчитати клавішу через INT 16h (AL = ASCII)
2. Якщо AL &lt; 20h і AL != 08h (Backspace) — ігнорувати
3. Якщо AL = 08h — обробити Backspace
4. Порівняти AL з [task_string + input_index]
5. Якщо рівні: записати символ з атрибутом 0Ah (зелений) Якщо різні: записати символ з атрибутом 0Ch (червоний), збільшити error_count
6. Збільшити input_index, total_keys
7. Перемістити підсвітку на наступний очікуваний символ
Кольоровий зворотний зв'язок працює через запис пари (символ, атрибут) у відеопам'ять. Набраний символ завжди відображається — навіть якщо він неправильний.Стандартна формула: WPM = (кількість символів / 5) / хвилини.У тактах INT 1Ah (18.2 такти/сек):WPM = (chars * 12 * 182) / (5 * elapsed_ticks * 10) = (chars * 2184) / (50 * elapsed_ticks)
Спрощений варіант без переповнення:WPM = (chars * 1092) / (25 * elapsed_ticks)
Порядок операцій у 16-бітній арифметиці:
MUL chars на 1092 — результат у DX:AX (32 біти, максимум ~80 символів * 1092 = 87360, вміщується в AX)
Обчислити знаменник: 25 * elapsed_ticks
DIV на знаменник — частка в AX = WPM
Для довших текстів (100+ символів) проміжний результат може не вміститися в AX, тому використовуємо 32-бітне ділення: DIV reg16 ділить DX:AX на reg16.accuracy = (correct_count * 100) / total_keys
У TASM:
MOV AX, correct_count
MOV BX, 100
MUL BX — результат у DX:AX (максимум 10000)
MOV BX, total_keys
DIV BX — частка в AX = відсоток
Захист від ділення на нуль: перед DIV завжди перевіряти CMP BX, 0 / JE skip_div.Час вимірюється через лічильник тактів INT 1Ah:1. Перший символ набрано -&gt; зберегти start_ticks (DX від INT 1Ah)
2. Останній символ набрано -&gt; зберегти end_ticks
3. elapsed = end_ticks - start_ticks
4. Секунди = elapsed * 10 / 182 (або elapsed / 18 для грубого наближення)
Таймер на екрані оновлюється між натисканнями клавіш: перед кожним INT 16h перевіряємо INT 16h AH=01h (чи є клавіша в буфері). Якщо клавіші немає — зчитуємо INT 1Ah та оновлюємо дисплей таймера.; --- Структури даних ---
start_ticks DW 0
total_keys DW 0
correct_count DW 0
error_count DW 0
input_index DW 0
task_string DB 'The quick brown fox jumps', 0
TASK_LEN EQU 25 ; --- Обробка одного натискання клавіші ---
; Вхід: AL = ASCII-код натиснутої клавіші
; Використовує: input_index, task_string
; Результат: оновлює correct_count або error_count, записує символ у відеопам'ять
process_key PROC PUSH BX PUSH DI MOV BX, input_index CMP BX, TASK_LEN ; вже дійшли до кінця? JAE @@done ; Порівняти AL з task_string[BX], обрати атрибут (0Ah/0Ch) ; Записати пару (символ, атрибут) у відеопам'ять ; Див. алгоритм «Порівняння символів у реальному часі» вище ; ... ваш код тут ... @@done: POP DI POP BX RET
process_key ENDP ; --- Обчислення WPM ---
; Вхід: CX = кількість символів, DX = elapsed_ticks
; Вихід: AX = WPM
calc_wpm PROC PUSH BX PUSH DX ; Захист від ділення на нуль CMP DX, 0 JE @@zero_time ; WPM = (chars * 1092) / (25 * elapsed_ticks) ; Див. алгоритм «Обчислення WPM» вище ; ... ваш код тут ... JMP @@done @@zero_time: MOV AX, 0
@@done: POP DX POP BX RET
calc_wpm ENDP Переповнення при множенні. chars * 1092 для 60 символів = 65520, що ледь вміщується в 16 біт. Для 61+ символів потрібно враховувати DX:AX як 32-бітний результат MUL. Ділення на нуль. Якщо гравець натисне Escape одразу, elapsed_ticks = 0 або total_keys = 0. Інструкція DIV спричинить виключення INT 00h. Завжди перевіряйте знаменник перед DIV. Backspace нижче нуля. Якщо input_index = 0 і гравець натискає Backspace, декремент дасть FFFFh. Перевіряйте CMP input_index, 0 / JE ignore. Таймер не оновлюється під час очікування клавіші. INT 16h AH=00h блокує виконання. Щоб таймер оновлювався, використовуйте цикл: INT 16h AH=01h (перевірка буфера) -&gt; якщо ZF=1, оновити таймер і повторити -&gt; якщо ZF=0, зчитати клавішу через AH=00h. <a class="internal-link" data-href="../arithmetic.md" href="projects/how-to/arithmetic.html" target="_self" rel="noopener nofollow">Арифметика: MUL та DIV</a>
<br><a class="internal-link" data-href="../timer-delays.md" href="projects/how-to/timer-delays.html" target="_self" rel="noopener nofollow">Затримки через системний годинник</a>
<br><a class="internal-link" data-href="../keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід</a>
<br>
Корисно для: <a class="internal-link" data-href="../../G02-Typing-Speed-Trainer.md" href="projects/g02-typing-speed-trainer.html" target="_self" rel="noopener nofollow">G02 Тренажер швидкості набору</a>
]]></description><link>projects/how-to/game-logic/g02.html</link><guid isPermaLink="false">Projects/How-to/game-logic/G02.md</guid><pubDate>Tue, 24 Feb 2026 13:47:57 GMT</pubDate></item><item><title><![CDATA[Y16]]></title><description><![CDATA[Алгоритми обчислення дільника PIT за частотою, хронометраж тривалості ноти, керування темпом та автомат станів пауза/відтворення.
Потрібно відтворити мелодію через PC Speaker із заданими частотами та тривалостями нот.
Потрібно реалізувати керування відтворенням (пауза, наступна/попередня пісня, зміна темпу) без блокування.
Потрібно обчислити дільник PIT із 32-бітного ділення 1193180 / freq.
Таймер 8253 (PIT) генерує прямокутну хвилю з частотою = 1193180 / дільник. Для отримання потрібної ноти обчислюємо дільник:дільник = 1193180 / частота_Гц
Число 1193180 = 1234DCh не вміщується у 16-бітний регістр, тому потрібне 32-бітне ділення через DX:AX / BX:MOV DX, 0012h ; старше слово 1193180 = 0012h
MOV AX, 34DCh ; молодше слово
MOV BX, [frequency] ; частота ноти
DIV BX ; AX = дільник, DX = залишок
Таблиця перевірки:Увага: якщо BX = 0 (пауза/тиша), ділення спричинить виняток INT 0. Перевіряйте частоту перед діленням:CMP BX, 0
JE silence ; частота 0 = тиша
Мелодія — це послідовність пар (частота, тривалість). Два варіанти компонування:Варіант A: чергований масив (рекомендовано)melody1 DW 262, 500 ; C4, 500 мс DW 294, 500 ; D4, 500 мс DW 330, 250 ; E4, 250 мс DW 0, 250 ; тиша, 250 мс DW 0FFFFh ; маркер кінця
Доступ: частота = [SI], тривалість = [SI+2], перехід = ADD SI, 4.Варіант B: паралельні масивиfreqs1 DW 262, 294, 330, 0
durs1 DW 500, 500, 250, 250
len1 DW 4
Доступ за спільним індексом: freq = freqs1[BX], dur = durs1[BX].Чергований масив зручніший для додавання нових нот та не потребує синхронізації двох масивів.Після увімкнення тону потрібно витримати тривалість ноти. Стабільний спосіб — використовувати тіки таймера BIOS (0040:006Ch, ~18.2 тіків/с):Перетворення мілісекунд у тіки: тіки = тривалість_мс * 182 / 10000 (приблизно) або спрощено: тіки = тривалість_мс / 55 Алгоритм очікування:
1. Зчитати поточний тік: start = [0040:006Ch]
2. Обчислити кінцевий: end = start + тіки
3. Цикл: a. Опитати клавіатуру (неблокуюче) b. Якщо натиснута клавіша — обробити c. Зчитати поточний тік d. Якщо поточний &gt;= end — вийти з циклу
Цей підхід дозволяє опитувати клавіатуру під час очікування, не блокуючи ввід.Темп змінює тривалість нот, але НЕ висоту тону (дільник PIT залишається незмінним):tempo_pct DW 100 ; відсоток: 100 = нормально, 50 = повільно, 200 = швидко Фактична тривалість = тривалість_ноти * 100 / tempo_pct Приклад: нота 500 мс при темпі 150%: 500 * 100 / 150 = 333 мс (швидше)
Обчислення в асемблері:; AX = тривалість ноти з масиву
mov bx, 100
mul bx ; DX:AX = тривалість * 100
mov bx, [tempo_pct]
div bx ; AX = фактична тривалість
Плеєр має декілька станів, між якими переключається за командами користувача: ┌──────────────────┐ │ MENU │ ← вибір пісні └───────┬──────────┘ │ Enter v ┌──────────────────┐ ┌────&gt;│ PLAYING │──── Esc ───&gt; вимкнути динамік, вихід │ └───────┬──────────┘ │ │ Space │ v │ ┌──────────────────┐ │ │ PAUSED │ │ └───────┬──────────┘ │ │ Space │ └──&gt; PLAYING │ │ Кінець пісні ──&gt; наступна пісня (або MENU) └──── Next/Prev
Реалізація через змінну стану:STATE_MENU EQU 0
STATE_PLAYING EQU 1
STATE_PAUSED EQU 2 play_state DB STATE_MENU main_loop: cmp [play_state], STATE_MENU je handle_menu cmp [play_state], STATE_PLAYING je handle_playing cmp [play_state], STATE_PAUSED je handle_paused jmp main_loop
Під час відтворення потрібно перевіряти клавіші без зупинки музики:1. MOV AH, 01h / INT 16h ; перевірити, чи є клавіша
2. JZ no_key ; ZF=1 → немає клавіші
3. MOV AH, 00h / INT 16h ; зчитати клавішу (видалити з буфера!)
4. Обробити клавішу
no_key:
5. Продовжити відтворення
Критично: крок 3 обов'язковий. Без нього та сама клавіша буде виявлятися знову і знову.; --- Відтворення однієї ноти з неблокуючим опитуванням ---
; Вхід: SI = вказівник на пару (частота DW, тривалість DW)
; Вихід: SI += 4 (наступна нота)
; Змінює: play_state якщо натиснута клавіша
play_note PROC push bx push dx mov bx, [si] ; BX = частота cmp bx, 0 je @@silence ; --- Обчислити дільник PIT та програмувати канал 2 --- ; ... ваш код тут ... ; див. алгоритм: DX:AX = 1193180, DIV BX ; підказка: OUT 43h (команда 0B6h), OUT 42h (молодший), OUT 42h (старший) ; --- Увімкнути динамік (OR 03h на порт 61h) --- ; ... ваш код тут ... jmp @@wait @@silence: ; --- Вимкнути динамік (AND 0FCh на порт 61h) --- ; ... ваш код тут ... @@wait: ; --- Обчислити тривалість з урахуванням темпу --- ; ... ваш код тут ... ; див. алгоритм: тривалість * 100 / tempo_pct, потім / 55 для тіків ; --- Цикл очікування з неблокуючим опитуванням клавіатури --- ; ... ваш код тут ... ; див. алгоритм хронометражу: порівняти тіки [0040:006Ch] ; підказка: INT 16h AH=01h для перевірки, AH=00h для зчитування @@note_done: add si, 4 pop dx pop bx ret
play_note ENDP ; --- Вимкнути динамік (викликати перед виходом!) ---
silence_speaker PROC in al, 61h and al, 0FCh out 61h, al ret
silence_speaker ENDP Забули встановити DX перед DIV. 1193180 = 0012:34DCh. Якщо DX не дорівнює 0012h, результат ділення буде хибним або спричинить переповнення (INT 0).
Забули вимкнути динамік при виході. Без AND AL, 0FCh / OUT 61h, AL перед завершенням програми динамік продовжить гудіти. Додайте очищення і в обробник Ctrl+Break (INT 23h).
Не зчитали клавішу після INT 16h AH=01h. Функція 01h лише перевіряє буфер, але не видаляє клавішу. Потрібен додатковий виклик AH=00h, інакше та сама клавіша виявлятиметься нескінченно.
Зміна темпу впливає на висоту тону. Темп має змінювати лише тривалість очікування, а не дільник PIT. Тримайте ці два обчислення окремо. <a class="internal-link" data-href="../pc-speaker.md" href="projects/how-to/pc-speaker.html" target="_self" rel="noopener nofollow">Звук через PC Speaker</a>
<br><a class="internal-link" data-href="../timer-delays.md" href="projects/how-to/timer-delays.html" target="_self" rel="noopener nofollow">Таймер та затримка</a>
<br><a class="internal-link" data-href="../lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиці пошуку</a>
<br><a class="internal-link" data-href="../keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід</a>
<br><a class="internal-link" data-href="Y15.md" href="projects/how-to/game-logic/y15.html" target="_self" rel="noopener nofollow">Y15 Ігрова логіка: Візуалізатор сортування</a> — спільна тема: затримки та таймери
<br>
Корисно для: <a class="internal-link" data-href="../../Y16-Music-Player.md" href="projects/y16-music-player.html" target="_self" rel="noopener nofollow">Y16 Музичний плеєр</a>
]]></description><link>projects/how-to/game-logic/y16.html</link><guid isPermaLink="false">Projects/How-to/game-logic/Y16.md</guid><pubDate>Tue, 24 Feb 2026 13:47:22 GMT</pubDate></item><item><title><![CDATA[Y15]]></title><description><![CDATA[Алгоритми бульбашкового сортування, сортування вибором та вставками як процедури, покрокова візуалізація та рендеринг стовпчиків у відеопам'яті.
Потрібно реалізувати алгоритм сортування як окрему процедуру з покроковим виконанням.
Потрібно відобразити масив як стовпчикову діаграму у відеопам'яті.
Потрібно анімувати кожну операцію порівняння та обміну з паузою між кроками.
У звичайній реалізації сортування працює до кінця без зупинок. Для візуалізації потрібно після кожного порівняння або обміну зупинитися, оновити екран і дочекатися натискання клавіші (покроковий режим) або витримати затримку (автоматичний режим).Підхід: спільні змінні стану; Спільні змінні
step_type DB 0 ; 0 = порівняння, 1 = обмін
idx_a DW 0 ; індекс першого елемента
idx_b DW 0 ; індекс другого елемента
cmp_count DW 0 ; лічильник порівнянь
swap_count DW 0 ; лічильник обмінів Після кожного CMP або XCHG:
1. Записати idx_a, idx_b, step_type
2. Збільшити відповідний лічильник
3. CALL update_display ; перемалювати стовпчики
4. CALL wait_for_step ; пауза або очікування клавіші
Порівнює сусідні елементи та обмінює, якщо вони у неправильному порядку. Після кожного проходу найбільший елемент "спливає" на своє місце.Для i = n-1 до 1 (зовнішній цикл): swapped = false Для j = 0 до i-1 (внутрішній цикл): [візуалізація: порівняння j та j+1] Якщо arr[j] &gt; arr[j+1]: обміняти arr[j] та arr[j+1] [візуалізація: обмін j та j+1] swapped = true Якщо swapped == false: масив відсортований, вихід
Важливо: зовнішній та внутрішній цикли обидва використовують лічильники. LOOP працює з CX, тому один лічильник тримайте на стеку або у змінній пам'яті.Знаходить мінімальний елемент у невідсортованій частині та ставить його на початок.Для i = 0 до n-2: min_idx = i Для j = i+1 до n-1: [візуалізація: порівняння j та min_idx] Якщо arr[j] &lt; arr[min_idx]: min_idx = j Якщо min_idx != i: обміняти arr[i] та arr[min_idx] [візуалізація: обмін i та min_idx]
Вставляє кожен елемент у правильну позицію серед уже відсортованих.Для i = 1 до n-1: key = arr[i] j = i - 1 Поки j &gt;= 0 і arr[j] &gt; key: [візуалізація: порівняння j та j+1] arr[j+1] = arr[j] ; зсув вправо [візуалізація: зсув j → j+1] j = j - 1 arr[j+1] = key
Кожен елемент масиву відображається як вертикальний стовпчик. Висота пропорційна значенню.Формула позиції (для масиву з N елементів, ширина стовпчика = 1 символ):Базовий рядок = 23 (передостанній рядок екрану)
Для елемента з індексом i та значенням v: стовпець = (80 - N) / 2 + i ; центрувати на екрані Малювати блочні символи (DBh) від рядка (базовий - v + 1) до базового: Для row = (базовий - v + 1) до базового: зміщення = (row * 80 + стовпець) * 2 ES:[зміщення] = 0DBh ; символ повного блоку ES:[зміщення+1] = атрибут ; колір
Стирання стовпчика — записати пробіли (20h) від рядка 0 до базового у тому самому стовпці.Кольори:Для автоматичного режиму після кожного кроку потрібна пауза:; INT 15h AH=86h: пауза на CX:DX мікросекунд
; Для ~50 мс: CX=0, DX=50000
wait_delay PROC mov ah, 86h mov cx, 0 mov dx, [delay_us] ; змінна, що контролює швидкість int 15h ret
wait_delay ENDP
Для покрокового режиму замість затримки чекати INT 16h AH=00h.; --- Бульбашкове сортування з покроковою візуалізацією ---
; Вхід: arr = масив DB, arr_size = розмір
; Змінює: idx_a, idx_b, step_type, cmp_count, swap_count
bubble_sort PROC push cx push si mov cx, [arr_size] dec cx ; CX = n-1 (зовнішній лічильник)
@@outer: push cx ; зберегти зовнішній лічильник xor si, si ; SI = індекс j mov dx, 0 ; прапорець обміну @@inner: cmp si, cx jge @@inner_done ; --- Візуалізація + порівняння arr[j] та arr[j+1] --- ; ... ваш код тут ... ; див. псевдокод бульбашкового сортування вище ; підказка: записати idx_a/idx_b, викликати update_display ; --- Обмін, якщо потрібно --- ; ... ваш код тут ... ; підказка: XCHG або пара MOV; встановити DX=1 @@no_swap: inc si jmp @@inner
@@inner_done: pop cx cmp dx, 0 je @@done loop @@outer
@@done: pop si pop cx ret
bubble_sort ENDP ; --- Малювання одного стовпчика ---
; Вхід: BL = індекс елемента, BH = атрибут кольору
draw_bar PROC push ax push di ; --- Обчислити стовпець та висоту --- ; ... ваш код тут ... ; див. формулу позиції в розділі "Рендеринг стовпчиків" ; --- Цикл: малювати 0DBh від базового рядка вгору --- ; ... ваш код тут ... ; підказка: зміщення = (рядок * 80 + стовпець) * 2 pop di pop ax ret
draw_bar ENDP Конфлікт лічильників CX у вкладених циклах. Бульбашкове сортування має два вкладених цикли. Якщо обидва використовують CX через LOOP, внутрішній цикл затре зовнішній лічильник. Зберігайте зовнішній CX на стеку (PUSH/POP).
Стовпчик росте донизу замість вгору. Рядки екрану нумеруються зверху (0) донизу (24). Стовпчик висотою H має починатися з рядка базовий - H + 1 і закінчуватися на базовому рядку.
Забули стерти старий стовпчик перед перемальовуванням. Якщо значення зменшилося після обміну, верхня частина старого стовпчика залишиться на екрані. Спершу запишіть пробіли по всій висоті стовпця.
Різна швидкість при різних cycles DOSBox. Цикл активного очікування працює по-різному при 3000 та 30000 cycles. Використовуйте INT 15h AH=86h для стабільної затримки. <a class="internal-link" data-href="../procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури</a>
<br><a class="internal-link" data-href="../arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви</a>
<br><a class="internal-link" data-href="../timer-delays.md" href="projects/how-to/timer-delays.html" target="_self" rel="noopener nofollow">Таймер та затримка</a>
<br><a class="internal-link" data-href="../video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять</a>
<br><a class="internal-link" data-href="../loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Цикли</a>
<br><a class="internal-link" data-href="Y16.md" href="projects/how-to/game-logic/y16.html" target="_self" rel="noopener nofollow">Y16 Ігрова логіка: Музичний плеєр</a> — спільна тема: затримки та таймери
<br>
Корисно для: <a class="internal-link" data-href="../../Y15-Sorting-Visualizer.md" href="projects/y15-sorting-visualizer.html" target="_self" rel="noopener nofollow">Y15 Візуалізатор сортування</a>
]]></description><link>projects/how-to/game-logic/y15.html</link><guid isPermaLink="false">Projects/How-to/game-logic/Y15.md</guid><pubDate>Tue, 24 Feb 2026 13:47:21 GMT</pubDate></item><item><title><![CDATA[Y18]]></title><description><![CDATA[Алгоритми пошуку підрядка (brute-force), виявлення меж рядків (CR/LF) та підсвічування збігів у відеопам'яті.
Потрібно знайти всі входження шаблону в текстовому буфері
Потрібно розбити буфер на рядки, обробляючи різні закінчення (CR+LF та LF)
Потрібно відобразити результати пошуку з кольоровою підсвіткою збігів
Перш ніж шукати, корисно побудувати масив зміщень початку кожного рядка. Це дозволяє швидко знаходити номер рядка та повторно шукати без повторного сканування на CR/LF:line_starts DW 2000 DUP(?) ; зміщення початку кожного рядка в буфері
line_count DW 0 ; кількість рядків Алгоритм:
1. line_starts[0] = 0 ; перший рядок завжди з позиції 0
2. Для кожного байту буфера з індексом i: a. ЯКЩО buffer[i] == 0Ah (LF): - line_count++ - line_starts[line_count] = i + 1
3. Якщо останній байт != 0Ah, збільшити line_count на 1 (останній рядок без LF)
Символ 0Dh (CR) перед 0Ah ігнорується при відображенні, але не впливає на логіку розбиття: термінатор рядка — завжди 0Ah.Для кожного рядка окремо шукаємо всі входження шаблону. Алгоритм перевіряє шаблон на кожній можливій позиції:Вхід: source (початок рядка), src_len, pattern, pat_len
Вихід: масив позицій збігів у рядку Для i = 0 до src_len - pat_len: збіг = true Для j = 0 до pat_len - 1: ЯКЩО source[i + j] != pattern[j]: збіг = false BREAK ЯКЩО збіг: записати позицію i в масив результатів
Довжина рядка обчислюється як line_starts[n+1] - line_starts[n] - 1 (мінус 1 для LF; мінус 2, якщо перед LF стоїть CR).Масив результатів зберігає індекси рядків зі збігами для прокрутки:match_lines DW 500 DUP(?) ; індекси рядків зі збігами
match_count DW 0 ; кількість рядків зі збігами
total_matches DW 0 ; загальна кількість окремих збігів
Після повного пошуку match_lines містить відсортований список рядків, де є хоча б один збіг. Прокрутка працює з цим масивом: PgDn збільшує scroll_offset на 22 (рядків на сторінку), PgUp зменшує.Під час виведення рядка зі збігом на екран потрібно перемикати атрибут кольору:Для кожного символу рядка на позиції i: ЯКЩО i потрапляє в діапазон [match_pos, match_pos + pat_len): атрибут = 0Eh ; жовтий на чорному (збіг) ІНАКШЕ: атрибут = 07h ; сірий на чорному (звичайний текст) Записати символ + атрибут у B800h
Один рядок може мати кілька збігів. Найпростіший підхід — для поточного рядка повторно виконати пошук підрядка та зберегти позиції збігів у тимчасовий масив, потім при виведенні перевіряти кожну позицію.Рядки довші за ~73 символи (80 мінус 7 для номера рядка) обрізаються. Замість переносу показується маркер обрізки:ЯКЩО довжина рядка &gt; MAX_DISPLAY_WIDTH: вивести перші MAX_DISPLAY_WIDTH - 3 символи вивести "..."
; --- Дані для пошуку ---
.DATA
line_starts DW 2000 DUP(?) ; зміщення початку кожного рядка
line_count DW 0
match_lines DW 500 DUP(?) ; індекси рядків зі збігами
match_count DW 0 .CODE
; --- Пошук підрядка в одному рядку ---
; Вхід: DS:SI = початок рядка, CX = довжина рядка
; DS:DI = шаблон, DX = довжина шаблону
; Вихід: AX = позиція першого збігу (-1 якщо не знайдено)
find_substr PROC PUSH BX PUSH SI PUSH DI PUSH CX ; ... ваш код тут ... ; Для кожної позиції i порівняйте pat_len байтів; див. алгоритм вище ; Якщо не знайдено: MOV AX, 0FFFFh POP CX POP DI POP SI POP BX RET
find_substr ENDP ; --- Побудова індексу рядків ---
; Вхід: DS:SI = буфер, CX = розмір буфера
; Результат: заповнює line_starts[], line_count
build_line_index PROC PUSH SI PUSH BX PUSH CX ; ... ваш код тут ... ; Скануйте буфер; при кожному 0Ah записуйте наступну позицію в line_starts POP CX POP BX POP SI RET
build_line_index ENDP Не обробляти CR перед LF. Файли DOS мають закінчення 0Dh 0Ah. Якщо ігнорувати 0Dh, він відобразиться як видимий символ на екрані. Перевіряйте: якщо байт перед 0Ah == 0Dh, зменшіть довжину рядка на 1. Пошук за межами рядка. Якщо шаблон має довжину 5, а рядок — 3, вираз src_len - pat_len стає від'ємним. CX при цьому отримує велике значення (unsigned). Перевіряйте: CMP CX, DX; JB skip. Прокрутка за межі масиву результатів. PgDn при scroll_offset + 22 &gt;= match_count має обмежуватися. Без перевірки програма виведе сміття з пам'яті. Змішування абсолютних зміщень із відносними позиціями. line_starts[] зберігає абсолютні зміщення в буфері. Позиція збігу від find_substr — відносна від початку рядка. При підсвічуванні використовуйте відносну позицію від початку рядка, не абсолютну. Файловий буфер без нульового термінатора. INT 21h AH=3Fh не додає нуль у кінці. Після зчитування запишіть 0 за останнім прочитаним байтом, або працюйте лише з поверненою довжиною. <a class="internal-link" data-href="../loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Цикли</a>
<br><a class="internal-link" data-href="../file-io.md" href="projects/how-to/file-io.html" target="_self" rel="noopener nofollow">Файловий ввід/вивід</a>
<br><a class="internal-link" data-href="../arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви</a>
<br><a class="internal-link" data-href="../video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять</a>
<br><a class="internal-link" data-href="../dos-services.md" href="projects/how-to/dos-services.html" target="_self" rel="noopener nofollow">Сервіси DOS</a>
<br><a class="internal-link" data-href="Y19.md" href="projects/how-to/game-logic/y19.html" target="_self" rel="noopener nofollow">Ігрова логіка: адресна книга</a> — також використовує пошук підрядка
<br>
Корисно для: <a class="internal-link" data-href="../../Y18-Mini-Grep.md" href="projects/y18-mini-grep.html" target="_self" rel="noopener nofollow">Y18 Mini Grep</a>
]]></description><link>projects/how-to/game-logic/y18.html</link><guid isPermaLink="false">Projects/How-to/game-logic/Y18.md</guid><pubDate>Tue, 24 Feb 2026 13:47:07 GMT</pubDate></item><item><title><![CDATA[Y17]]></title><description><![CDATA[Алгоритми генерації ідеального лабіринту (DFS з явним стеком), розв'язання (DFS/BFS) та реконструкції шляху.
Потрібно згенерувати ідеальний лабіринт (рівно один шлях між будь-якими двома комірками)
Потрібно розв'язати лабіринт та візуалізувати знайдений шлях
Потрібно реалізувати рекурсивний алгоритм без апаратної рекурсії (явний стек у сегменті даних)
Кожна комірка лабіринту зберігається як один байт. Молодші 4 біти кодують наявність стін у чотирьох напрямках, біт 4 позначає відвідану комірку:Біт: 4 3 2 1 0 visited South East West North
Спочатку всі комірки мають значення 0Fh — усі 4 стіни на місці, не відвідана. Зняття стіни — це AND з інвертованою маскою. Наприклад, зняти стіну на північ: AND cell, 11111110b.Таблиці дельти спрощують рух:delta_row DB -1, 1, 0, 0 ; N, S, W, E
delta_col DB 0, 0, -1, 1
wall_bit DB 01h, 02h, 04h, 08h ; біт стіни для кожного напрямку
opp_bit DB 02h, 01h, 08h, 04h ; біт протилежної стіни (у сусіда)
При знятті стіни між поточною коміркою та сусідом треба прибрати стіну в обох: у поточній — wall_bit[D], у сусіді — opp_bit[D].Алгоритм рекурсивного бектрекінгу (Recursive Backtracker) генерує ідеальний лабіринт. Замість CALL/RET рекурсії використовується масив у сегменті даних як стек:1. Заштовхнути стартову комірку (0, 0) на стек
2. Позначити її як відвідану
3. ПОКИ стек не порожній: a. Поточна = подивитися на вершину стеку b. Знайти всіх невідвіданих сусідів поточної c. ЯКЩО є невідвідані сусіди: - Обрати випадкового сусіда - Зняти стіну між поточною та сусідом - Позначити сусіда як відвіданого - Заштовхнути сусіда на стек d. ІНАКШЕ: - Витягнути зі стеку (бектрекінг)
Випадковий вибір сусіда: зібрати невідвіданих сусідів у тимчасовий масив (до 4 елементів), потім обрати за індексом random MOD count.Альтернативний спосіб — перемішати масив напрямків [0, 1, 2, 3] і пробувати кожен по черзі:dirs DB 0, 1, 2, 3 ; N, S, W, E ; Fisher-Yates для 4 елементів:
; i = 3: обміняти dirs[3] з dirs[rand MOD 4]
; i = 2: обміняти dirs[2] з dirs[rand MOD 3]
; i = 1: обміняти dirs[1] з dirs[rand MOD 2]
Розв'язувач також використовує явний стек. Відмінність від генерації — перед переходом до сусіда перевіряється не стіна, а її відсутність:1. Очистити масив відвідувань розв'язувача
2. Заштовхнути стартову комірку
3. ПОКИ стек не порожній: a. Поточна = витягнути зі стеку b. ЯКЩО поточна == фініш: реконструювати шлях, КІНЕЦЬ c. Позначити поточну як відвідану d. Для кожного напрямку D: - ЯКЩО стіни немає (біт wall_bit[D] = 0) І сусід не відвіданий: - Записати parent[сусід] = поточна - Заштовхнути сусіда на стек
Масив parent (по одному слову на комірку) зберігає, звідки ми прийшли. Після досягнення фінішу шлях відновлюється у зворотному порядку: від фінішу по parent до старту.Кожна комірка лабіринту займає блок 2x2 символи на екрані (включаючи стіни):Комірка (r, c) --&gt; екранна позиція (2*r + 1, 2*c + 1)
Стіни розташовані на парних координатах: - Горизонтальна стіна між (r, c) і (r-1, c): екранний рядок 2*r - Вертикальна стіна між (r, c) і (r, c-1): екранний стовпець 2*c - Перетини стін: парний рядок, парний стовпець
; --- Дані для DFS-генерації лабіринту ---
.DATA
MAZE_ROWS EQU 12
MAZE_COLS EQU 15
maze DB MAZE_ROWS * MAZE_COLS DUP(0Fh) ; всі стіни
stack_data DW 200 DUP(?) ; стек: кожен запис = row*256 + col
stack_top DW 0 delta_row DB -1, 1, 0, 0 ; N, S, W, E
delta_col DB 0, 0, -1, 1
wall_bit DB 01h, 02h, 04h, 08h
opp_bit DB 02h, 01h, 08h, 04h .CODE
; --- Push на явний стек ---
; Вхід: AX = закодована позиція (row*256 + col)
push_stack PROC ; ... ваш код тут ... ; Використайте stack_top як індекс у stack_data RET
push_stack ENDP ; --- Pop з явного стеку ---
; Вихід: AX = закодована позиція
pop_stack PROC ; ... ваш код тут ... ; Зворотна операція до push_stack RET
pop_stack ENDP ; --- Зняти стіну між двома комірками ---
; Вхід: SI = зміщення поточної комірки, DI = зміщення сусіда, DL = напрямок (0-3)
remove_wall PROC PUSH BX ; ... ваш код тут ... ; Використайте wall_bit[DL] та opp_bit[DL]; див. алгоритм вище POP BX RET
remove_wall ENDP ; --- Отримати зміщення комірки в масиві maze ---
; Вхід: AH = row, AL = col
; Вихід: SI = зміщення
get_cell_offset PROC PUSH AX PUSH DX ; ... ваш код тут ... ; SI = row * MAZE_COLS + col POP DX POP AX RET
get_cell_offset ENDP Забути зняти стіну з обох боків. При знятті стіни між комірками A і B треба прибрати біт стіни і в A, і в B. Якщо зняти тільки з одного боку, генератор працюватиме, але розв'язувач побачить стіну з боку сусіда. Переповнення апаратного стеку при рекурсії. Лабіринт 15x12 = 180 комірок, глибина рекурсії може сягати 180. Кожен CALL + PUSH регістрів = ~10 байт, загалом ~1800 байт. Стандартний TASM-стек 256-512 байт — це замало. Саме тому обов'язковий явний стек у сегменті даних. Неправильне відображення координат лабіринту на екранні позиції. Комірка (0,0) — це не позиція (0,0) на екрані. Стіни займають окремі рядки і стовпці. Формула: екранний рядок = 2*row + 1, екранний стовпець = 2*col + 1. Зміщений генератор через повторні однакові значення таймера. Якщо зчитувати тіки BIOS (INT 1Ah) кожен крок, декілька послідовних зчитувань дадуть те саме значення. Використовуйте LCG, ініціалізований від таймера. Масив parent для розв'язувача не очищений між запусками. Якщо запустити розв'язувач вдруге без обнулення parent, шлях реконструюється з даними попереднього запуску. <a class="internal-link" data-href="../arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви</a>
<br><a class="internal-link" data-href="../random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">Генерація псевдовипадкових чисел</a>
<br><a class="internal-link" data-href="../procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури</a>
<br><a class="internal-link" data-href="../video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять</a>
<br><a class="internal-link" data-href="../lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиці пошуку</a>
<br>
Корисно для: <a class="internal-link" data-href="../../Y17-Maze-Generator-Solver.md" href="projects/y17-maze-generator-solver.html" target="_self" rel="noopener nofollow">Y17 Генератор та розв'язувач лабіринтів</a>
]]></description><link>projects/how-to/game-logic/y17.html</link><guid isPermaLink="false">Projects/How-to/game-logic/Y17.md</guid><pubDate>Tue, 24 Feb 2026 13:46:45 GMT</pubDate></item><item><title><![CDATA[G07]]></title><description><![CDATA[Алгоритми розбору чисел у довільній основі, перетворення через послідовне ділення та покрокова візуалізація процесу.
Потрібно перетворити число з однієї системи числення в іншу (BIN, OCT, DEC, HEX).
Потрібно показати користувачу покрокове ділення, а не лише кінцевий результат.
Потрібно коректно розбирати ввід у різних основах (зокрема шістнадцятковий з літерами A-F).
Будь-яке число у будь-якій основі перетворюється у внутрішнє 16-бітне значення одним алгоритмом:value = 0
для кожного символу c у рядку: digit = значення_цифри(c) ; '0'..'9' → 0..9, 'A'..'F' → 10..15 value = value * base + digit
Валідація цифри залежить від основи:Для шістнадцяткового вводу потрібна окрема гілка розбору:якщо c &gt;= 'A' і c &lt;= 'F': digit = c - 'A' + 10
інакше якщо c &gt;= '0' і c &lt;= '9': digit = c - '0'
інакше: помилка: недопустимий символ
Основний алгоритм — послідовне ділення на цільову основу. Залишки дають цифри від найменш значущої до найбільш значущої:value = 255, base = 16 Крок 1: 255 / 16 = 15, остача 15 → 'F'
Крок 2: 15 / 16 = 0, остача 15 → 'F'
Результат (залишки знизу вгору): FF
Для двійкового (base = 2) замість DIV ефективніше використовувати зсув бітів:для i від 15 до 0: SHL AX, 1 ; старший біт → CF ADC DL, '0' ; DL = '0' + CF = '0' або '1' вивести DL якщо i MOD 4 == 0: вивести пробіл ; групування напівбайтів
Для покрокового режиму зберігайте кожний крок ділення у масиві записів:step_record STRUC s_dividend DW ? ; ділене s_divisor DW ? ; дільник (основа) s_quotient DW ? ; частка s_remainder DW ? ; остача (цифра)
step_record ENDS steps step_record 16 DUP(&lt;&gt;) ; максимум 16 кроків (base 2)
step_count DW 0
Алгоритм заповнення:i = 0
поки value &gt; 0: steps[i].dividend = value steps[i].divisor = base steps[i].quotient = value / base steps[i].remainder = value MOD base value = value / base i++
step_count = i
Відображення кроків від першого до останнього:255 / 16 = 15, остача 15 (F) 15 / 16 = 0, остача 15 (F)
──────────────────────────────
Результат: FF
Для перетворення значення 0-15 у символ ASCII використовуйте таблицю пошуку:hex_digits DB '0123456789ABCDEF' ; Перетворення: AL = значення 0..15 → AL = ASCII-символ
mov bx, OFFSET hex_digits
xlat ; AL = hex_digits[AL]
; --- Таблиця шістнадцяткових цифр ---
hex_digits DB '0123456789ABCDEF'
; --- Перетворення 16-бітного значення у рядок у заданій основі ---
; Вхід: AX = значення, BL = основа (2, 8, 10, 16)
; DI = вказівник на буфер виводу
; Вихід: буфер заповнений ASCII-рядком (завершений 0) convert_to_base PROC push bx push cx push dx push si ; 1. Послідовно ділити AX на BX, зберігаючи остачі на стеку ; (див. алгоритм перетворення через DIV вище) ; 2. Зняти остачі зі стека та перетворити на ASCII через hex_digits + XLAT ; 3. Записати нуль-термінатор ; ... ваш код тут ... pop si pop dx pop cx pop bx ret
convert_to_base ENDP
; --- Розбір числа у заданій основі з рядка ---
; Вхід: SI = вказівник на рядок, BL = основа
; Вихід: AX = 16-бітне значення, CF = 1 якщо помилка parse_number PROC push bx push cx push dx ; 1. Для кожного символу: визначити значення цифри (0-9 або A-F) ; 2. Перевірити, що цифра &lt; основи ; 3. value = value * base + digit (див. алгоритм розбору вище) ; 4. Перевірити переповнення (DX != 0 після MUL або CF після ADD) ; ... ваш код тут ... pop dx pop cx pop bx ret
parse_number ENDP Не обнулили DX перед DIV. Перед DIV BX (де BX = основа) ділене — це DX:AX. Якщо DX != 0, результат буде хибним або виникне Division Overflow. Завжди XOR DX, DX перед кожним кроком ділення. Плутанина ASCII-коду літери з її значенням. 'A' = 41h, але шістнадцяткова цифра A має значення 10. Потрібно SUB AL, 'A' та ADD AL, 10, а не просто SUB AL, '0'. Зворотній порядок цифр. DIV дає цифри від молодшої до старшої. Якщо не використовувати стек (PUSH/POP) або не заповнювати буфер з кінця, число виведеться задом наперед. Неповний двійковий вивід. Для значення 5 алгоритм DIV дасть 101, але очікується 0000 0000 0000 0101. Для двійкового виводу краще завжди виводити всі 16 біт через SHL. <a class="internal-link" data-href="../arithmetic.md" href="projects/how-to/arithmetic.html" target="_self" rel="noopener nofollow">Арифметичні інструкції</a> — MUL, DIV для перетворення основ
<br><a class="internal-link" data-href="../number-display.md" href="projects/how-to/number-display.html" target="_self" rel="noopener nofollow">Відображення чисел</a> — виведення десяткових чисел
<br><a class="internal-link" data-href="../bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Побітові операції</a> — SHL/SHR для двійкового виводу
<br><a class="internal-link" data-href="../lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиці пошуку</a> — таблиця шістнадцяткових цифр
<br><a class="internal-link" data-href="../loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Цикли</a> — ітерація по цифрах числа
<br>
Корисно для: <a class="internal-link" data-href="../../G07-Number-Base-Converter.md" href="projects/g07-number-base-converter.html" target="_self" rel="noopener nofollow">G07 Конвертер систем числення</a>
]]></description><link>projects/how-to/game-logic/g07.html</link><guid isPermaLink="false">Projects/How-to/game-logic/G07.md</guid><pubDate>Tue, 24 Feb 2026 13:46:18 GMT</pubDate></item><item><title><![CDATA[G06]]></title><description><![CDATA[Алгоритми сканування слова на збіг букв, керування маскою відкриття, відстеження використаних букв та поетапне малювання шибениці.
Потрібно перевірити, чи є введена буква у загаданому слові, та відкрити всі її входження одночасно.
Потрібно відстежувати, які букви вже вгадані, і не дозволяти повторний ввід.
Потрібно поетапно малювати фігуру шибениці залежно від кількості помилок.
Коли гравець вводить букву, потрібно пройти все слово та відкрити всі позиції, де ця буква зустрічається:знайдено = false
для i від 0 до довжина_слова - 1: якщо word[i] == введена_буква: revealed_mask[i] = 1 знайдено = true
якщо НЕ знайдено: wrong_count++ намалювати наступну частину шибениці
Маска відкриття — це масив байтів тієї ж довжини, що й слово. Початково заповнений нулями (усі букви приховані). Одиниця означає, що буква відкрита.word DB 'ASSEMBLY' ; загадане слово
word_len DB 8
revealed_mask DB 8 DUP(0) ; 0 = прихована, 1 = відкрита
Два підходи:Підхід A — масив із 26 байтів:guessed DB 26 DUP(0) ; індекс = буква - 'A', значення: 0/1/2 ; Перевірка: чи буква вже вгадана?
mov bl, al ; AL = буква ('A'..'Z')
sub bl, 'A' ; BL = індекс 0..25
xor bh, bh
cmp guessed[bx], 0
jne already_guessed ; вже вгадана
Підхід B — 32-бітна бітова маска (два слова):guessed_lo DW 0 ; біти 0-15 (букви A-P)
guessed_hi DW 0 ; біти 16-25 (букви Q-Z) ; Встановити біт для букви AL
mov cl, al
sub cl, 'A' ; CL = номер біта (0..25)
mov ax, 1
shl ax, cl ; AX = маска біта
; Для літер A-P (cl &lt; 16):
test guessed_lo, ax
jnz already_guessed
or guessed_lo, ax ; позначити як вгадану
Бітова маска економить пам'ять (4 байти замість 26), але код складніший через розділення на два слова.Кожна неправильна відповідь збільшує лічильник wrong_count. Коли він досягає максимуму (зазвичай 6 або 7), гра закінчується поразкою:Етап 0: порожня шибениця (каркас)
Етап 1: голова (O)
Етап 2: тулуб (|)
Етап 3: ліва рука (/)
Етап 4: права рука (\)
Етап 5: ліва нога (/)
Етап 6: права нога (\)
Дані для малювання зберігаються як таблиця записів (рядок, стовпець, символ):; Кожний етап -- набір записів: рядок, стовпець, ASCII-код, 0 (термінатор)
stage1 DB 3, 5, 'O', 0 ; голова
stage2 DB 4, 5, '|', 0 ; тулуб
stage3 DB 4, 4, '/', 0 ; ліва рука
stage4 DB 4, 6, '\', 0 ; права рука
stage5 DB 5, 4, '/', 0 ; ліва нога
stage6 DB 5, 6, '\', 0 ; права нога ; Таблиця вказівників на етапи
stage_table DW OFFSET stage1, OFFSET stage2, OFFSET stage3 DW OFFSET stage4, OFFSET stage5, OFFSET stage6
Після кожного вдалого вгадування перебираємо маску відкриття. Якщо всі байти ненульові — гравець переміг:перемога = true
для i від 0 до word_len - 1: якщо revealed_mask[i] == 0: перемога = false break
; --- Дані гри ---
word DB 'ASSEMBLY'
word_len DB 8
revealed_mask DB 8 DUP(0) ; 0 = прихована, 1 = відкрита
wrong_count DB 0
guessed DB 26 DUP(0) ; індекс = буква - 'A'
; --- Обробка вгадування букви ---
; Вхід: AL = введена буква (великий регістр 'A'..'Z')
; Вихід: revealed_mask оновлена, wrong_count збільшено якщо немає збігу process_guess PROC push bx push cx push si ; 1. Пройти все слово, порівняти кожну позицію з введеною буквою ; (див. алгоритм сканування вище — НЕ зупинятись на першому збігу!) ; 2. Якщо знайдено збіг — встановити revealed_mask[i] = 1 ; 3. Якщо жодного збігу — збільшити wrong_count ; 4. Якщо знайдено — викликати check_win ; ... ваш код тут ... pop si pop cx pop bx ret
process_guess ENDP
; --- Перевірка перемоги ---
; Вихід: AL = 1 якщо всі букви відкриті, 0 інакше check_win PROC push bx push cx ; Перебрати revealed_mask[0..word_len-1] ; Якщо всі != 0, повернути AL = 1 (див. алгоритм перевірки перемоги вище) ; ... ваш код тут ... pop cx pop bx ret
check_win ENDP Зупинка сканування після першого збігу. Якщо слово APPLE і гравець вводить P, потрібно відкрити обидві позиції. Цикл повинен пройти все слово до кінця, а не виходити при першому збігу. Не конвертували регістр букви. Якщо словник зберігає великі літери, а гравець вводить малу a, порівняння CMP не знайде збігу. Конвертуйте ввід у верхній регістр через AND AL, 0DFh перед обробкою. Помилка індексації у таблиці етапів шибениці. Таблиця stage_table містить слова (DW), тому індекс wrong_count потрібно множити на 2: mov bx, [wrong_count] / shl bx, 1 / mov si, stage_table[bx]. Лічильник помилок перевищує кількість етапів. Якщо не перевірити wrong_count &lt; MAX_STAGES перед зверненням до таблиці, програма зчитає дані за межами масиву. <a class="internal-link" data-href="../arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви</a> — сканування масивів та робота з масками
<br><a class="internal-link" data-href="../bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Побітові операції</a> — бітові маски для відстеження букв
<br><a class="internal-link" data-href="../loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Цикли</a> — ітерація по символах слова
<br><a class="internal-link" data-href="../random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">Випадкові числа</a> — вибір слова зі словника
<br><a class="internal-link" data-href="../video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Відеопам'ять</a> — малювання ASCII-арту шибениці
<br>
Корисно для: <a class="internal-link" data-href="../../G06-Hangman.md" href="projects/g06-hangman.html" target="_self" rel="noopener nofollow">G06 Шибениця</a>
]]></description><link>projects/how-to/game-logic/g06.html</link><guid isPermaLink="false">Projects/How-to/game-logic/G06.md</guid><pubDate>Tue, 24 Feb 2026 13:46:05 GMT</pubDate></item><item><title><![CDATA[Y10]]></title><description><![CDATA[Алгоритми руху формації прибульців, стрільби прибульців, сіткового виявлення зіткнень та прискорення за кількістю живих.
Потрібно рухати групу прибульців як єдину формацію з відбиванням від країв
Потрібно обирати, який прибулець стрілятиме, та рухати кулі
Потрібно ефективно перевіряти зіткнення куль з прибульцями та гравцем
Потрібно прискорювати гру, коли прибульців стає менше
Формація прибульців — байтовий масив ROWS x COLS (наприклад, 5x8 = 40 байт). Кожен байт: 1 = живий, 0 = мертвий.ALIEN_ROWS EQU 5
ALIEN_COLS EQU 8 alien_grid DB ALIEN_ROWS * ALIEN_COLS DUP(1) ; усі живі
formation_top DW 2 ; рядок екрану верхнього лівого кута
formation_left DW 4 ; стовпець екрану верхнього лівого кута
direction DB 1 ; +1 = вправо, -1 = вліво (FFh)
alive_count DB 40 ; кількість живих прибульців
Позиція прибульця (r, c) на екрані:screen_row = formation_top + r * 2 ; відступ 2 рядки між рядами
screen_col = formation_left + c * 4 ; відступ 4 стовпці між колонками
Щотакту формація зсувається на 1 стовпець у поточному напрямку. Алгоритм:move_formation: ; 1. Знайти межі живих прибульців leftmost_col = 7 ; мінімальний стовпець живого rightmost_col = 0 ; максимальний стовпець живого для r = 0..ROWS-1: для c = 0..COLS-1: якщо alien_grid[r * COLS + c] = 1: якщо c &lt; leftmost_col: leftmost_col = c якщо c &gt; rightmost_col: rightmost_col = c ; 2. Перевірити зіткнення з краєм екрану right_edge = formation_left + rightmost_col * 4 left_edge = formation_left + leftmost_col * 4 якщо direction = +1 та right_edge &gt;= 76: ; правий край direction = -1 (FFh) formation_top += 1 ; опуститись якщо direction = -1 та left_edge &lt;= 1: ; лівий край direction = +1 formation_top += 1 ; 3. Зсунути формацію formation_left += direction
Перевірка меж враховує лише живих прибульців. Якщо весь правий стовпець знищено, формація може рухатись далі вправо, перш ніж відіб'ється.Кожні N тактів один прибулець стріляє. Обираємо випадковий стовпець і знаходимо найнижчого живого прибульця у ньому:alien_shoot: col = random() MOD ALIEN_COLS ; Знайти найнижчого живого у стовпці col для r = ROWS-1 вниз до 0: якщо alien_grid[r * COLS + col] = 1: ; Запустити кулю з позиції цього прибульця alien_bullet_row = formation_top + r * 2 + 1 alien_bullet_col = formation_left + col * 4 повернути ; Стовпець порожній — спробувати інший стовпець або пропустити
Куля гравця -&gt; прибулець (O(1) за сіткою):Замість перебору всіх прибульців, обчислюємо, якій клітинці сітки відповідає позиція кулі:check_bullet_vs_aliens: ; Куля на (bullet_row, bullet_col) grid_r = (bullet_row - formation_top) / 2 grid_c = (bullet_col - formation_left) / 4 ; Перевірити, що координати в межах формації якщо grid_r &lt; 0 або grid_r &gt;= ROWS: промах якщо grid_c &lt; 0 або grid_c &gt;= COLS: промах ; Перевірити точне вирівнювання (залишок від ділення = 0) якщо (bullet_row - formation_top) MOD 2 != 0: промах якщо (bullet_col - formation_left) MOD 4 != 0: промах ; Перевірити, чи прибулець живий якщо alien_grid[grid_r * COLS + grid_c] = 1: alien_grid[grid_r * COLS + grid_c] = 0 alive_count -= 1 score += 10 знищити кулю
Куля прибульця -&gt; гравець:check_alien_bullet_vs_player: якщо alien_bullet_row = player_row: якщо |alien_bullet_col - player_col| &lt;= 1: ; ширина корабля lives -= 1 знищити кулю
Класична механіка Space Invaders: чим менше прибульців, тим швидше вони рухаються. Реалізація через дільник тактів:tick_counter DB 0
move_divider DB 3 ; рух кожні N тактів on_tick: tick_counter += 1 якщо tick_counter &lt; move_divider: пропустити рух tick_counter = 0 ; Оновити дільник залежно від кількості живих якщо alive_count &gt; 30: move_divider = 4 якщо alive_count &gt; 15: move_divider = 3 якщо alive_count &gt; 5: move_divider = 2 інакше: move_divider = 1 ; кожен такт
Альтернативно — таблиця пошуку з 40 записів, де speed_table[alive_count] дає дільник.ALIEN_ROWS EQU 5
ALIEN_COLS EQU 8 ; --- Рух формації прибульців ---
; Читає/змінює: formation_left, formation_top, direction
; Використовує: alien_grid, ALIEN_ROWS, ALIEN_COLS
move_formation PROC PUSH AX PUSH BX PUSH CX PUSH SI ; ... ваш код тут ... ; 1. Знайти leftmost/rightmost живий стовпець (див. алгоритм вище) ; 2. Обчислити екранні позиції країв: col * 4 + formation_left ; 3. Якщо край &gt;= 76 або &lt;= 1: змінити direction, збільшити formation_top ; 4. formation_left += direction (CBW для знакового розширення) POP SI POP CX POP BX POP AX RET
move_formation ENDP ; --- Виявлення зіткнення кулі з прибульцем ---
; Вхід: bullet_row (DL), bullet_col (DH)
; Вихід: AL = 1 якщо влучення, 0 якщо промах
check_hit PROC PUSH BX PUSH CX ; ... ваш код тут ... ; 1. grid_r = (bullet_row - formation_top) / 2 (див. алгоритм вище) ; 2. grid_c = (bullet_col - formation_left) / 4 ; 3. Перевірити межі та вирівнювання (TEST на MOD) ; 4. Якщо alien_grid[grid_r * COLS + grid_c] = 1 -&gt; влучення POP CX POP BX RET
check_hit ENDP Перевірка краю формації по номінальній межі, а не по живих прибульцях. Якщо весь правий стовпець знищено, формація відіб'ється від стіни раніше, ніж потрібно. Скануйте крайній живий стовпець для визначення реальної межі. Куля перескакує прибульця. Якщо куля рухається на 2+ рядки за такт, вона може пролетіти крізь прибульця. У базовій версії рухайте кулю рівно на 1 рядок за такт. Виконання ігрової логіки всередині обробника INT 1Ch. Обробник переривань повинен лише встановити прапорець (tick_flag = 1) та виконати IRET. Вся логіка (рух, зіткнення, рендеринг) — у головному циклі, який перевіряє цей прапорець. Забули відновити оригінальний вектор INT 1Ch при виході. Без відновлення DOSBox залишиться з вказівником на неіснуючий обробник. Зберігайте оригінальний вектор при старті та записуйте його назад перед INT 21h/4Ch. <a class="internal-link" data-href="../arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви</a>
<br><a class="internal-link" data-href="../random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">Генерація псевдовипадкових чисел</a>
<br><a class="internal-link" data-href="../interrupt-handlers.md" href="projects/how-to/interrupt-handlers.html" target="_self" rel="noopener nofollow">Обробники переривань</a>
<br><a class="internal-link" data-href="../arithmetic.md" href="projects/how-to/arithmetic.html" target="_self" rel="noopener nofollow">Арифметичні інструкції</a>
<br><a class="internal-link" data-href="Y08.md" href="projects/how-to/game-logic/y08.html" target="_self" rel="noopener nofollow">Ігрова логіка: морський бій</a>
<br>
Корисно для: <a class="internal-link" data-href="../../Y10-Space-Invaders.md" href="projects/y10-space-invaders.html" target="_self" rel="noopener nofollow">Y10 Космічні загарбники</a>
]]></description><link>projects/how-to/game-logic/y10.html</link><guid isPermaLink="false">Projects/How-to/game-logic/Y10.md</guid><pubDate>Tue, 24 Feb 2026 07:29:41 GMT</pubDate></item><item><title><![CDATA[Y04]]></title><description><![CDATA[Алгоритми симуляції гравітації, визначення перемоги в 4 напрямках та простого ШІ з пріоритетами.
Потрібно реалізувати падіння фішки в найнижчий порожній рядок стовпця
Потрібно перевірити чотири в ряд горизонтально, вертикально та по обох діагоналях
Потрібно реалізувати ШІ-опонента, що блокує та атакує
Поле 7 стовпців x 6 рядків зберігається як плоский масив із 42 байтів у рядковому порядку. Значення: 0 = порожньо, 1 = гравець 1, 2 = гравець 2.board DB 42 DUP(0) Індексація: cell(row, col) = board[row * 7 + col] col: 0 1 2 3 4 5 6
row 0: [0] [1] [2] [3] [4] [5] [6] ← верх
row 1: [7] [8] [9] ...
row 2: ...
row 3: ...
row 4: ...
row 5: [35][36][37][38][39][40][41] ← низ
Коли гравець обирає стовпець C, фішка «падає» до найнижчого порожнього рядка. Скануємо стовпець знизу вгору:ПРОЦЕДУРА drop_piece(col, player): ДЛЯ row = 5 ВНИЗ ДО 0: ЯКЩО board[row * 7 + col] == 0: board[row * 7 + col] = player ПОВЕРНУТИ row ; рядок, куди впала фішка ПОВЕРНУТИ -1 ; стовпець повний
Якщо board[0 * 7 + col] != 0, стовпець повний — хід відхиляється.Після кожного ходу в позицію (R, C) перевіряємо чотири в ряд в 4 напрямках. Для кожного напрямку рахуємо послідовні фішки того ж гравця в обидві сторони від (R, C):Напрямки (dr, dc): Горизонталь: (0, +1) крок у масиві: +1 Вертикаль: (+1, 0) крок у масиві: +7 Діагональ \: (+1, +1) крок у масиві: +8 Діагональ /: (+1, -1) крок у масиві: +6
Алгоритм для одного напрямку:ПРОЦЕДУРА count_direction(R, C, dr, dc, player): count = 1 ; рахуємо саму фішку ; Рахуємо в додатному напрямку nr = R + dr, nc = C + dc ПОКИ nr в [0..5] І nc в [0..6] І board[nr*7+nc] == player: count = count + 1 nr = nr + dr, nc = nc + dc ; Рахуємо у від'ємному напрямку nr = R - dr, nc = C - dc ПОКИ nr в [0..5] І nc в [0..6] І board[nr*7+nc] == player: count = count + 1 nr = nr - dr, nc = nc - dc ПОВЕРНУТИ count
Якщо для будь-якого напрямку count &gt;= 4, це перемога.Замість 4 окремих процедур, зберігаємо дельти у таблиці і викликаємо одну узагальнену процедуру:directions DB 0,1, 1,0, 1,1, 1,-1 ; 4 пари (dr, dc)
Цикл по 4 записах таблиці, для кожного — виклик count_direction.ШІ обирає стовпець за такими правилами (перше спрацьоване правило виграє):1. ВИГРАШ: для кожного стовпця 0-6: якщо стовпець не повний: тимчасово поставити фішку ШІ якщо check_win → обрати цей стовпець прибрати тимчасову фішку 2. БЛОКУВАННЯ: для кожного стовпця 0-6: тимчасово поставити фішку СУПЕРНИКА якщо check_win → заблокувати цей стовпець прибрати тимчасову фішку 3. ЦЕНТР: якщо стовпець 3 не повний → обрати 3 4. ВИПАДКОВИЙ: обрати випадковий стовпець серед неповних
Кроки 1 і 2 використовують ту саму логіку drop_piece + check_win, різниця лише в тому, чию фішку ставимо. Після перевірки обов'язково прибрати тимчасову фішку (записати 0).; --- Дані ---
board DB 42 DUP(0)
directions DB 0,1, 1,0, 1,1, 1,-1 ; 4 пари (dr, dc) ; --- Кинути фішку в стовпець ---
; Вхід: BL = стовпець (0-6), BH = гравець (1 або 2)
; Вихід: AL = рядок, куди впала (0-5), або FFh якщо повний
drop_piece PROC PUSH CX PUSH DI ; Сканувати стовпець від рядка 5 вгору до 0 ; Знайти перший рядок, де board[row*7+col] == 0 ; Записати фішку гравця; повернути FFh якщо повний ; див. алгоритм гравітації вище ; ... ваш код тут ... POP DI POP CX RET
drop_piece ENDP ; --- Перевірка перемоги після ходу ---
; Вхід: DH = row, DL = col щойно зробленого ходу
; Вихід: AL = 1 (перемога), 0 (немає)
check_win PROC PUSH BX PUSH CX PUSH SI ; 1. Визначити гравця з board[row*7+col] ; 2. Для кожного з 4 напрямків у таблиці directions: ; - count = 1 ; - рахувати послідовні фішки у додатному напрямку (+dr, +dc) ; - рахувати послідовні фішки у від'ємному напрямку (-dr, -dc) ; - якщо count &gt;= 4 → перемога ; див. алгоритм count_direction вище ; ... ваш код тут ... POP SI POP CX POP BX RET
check_win ENDP Сканування перемоги виходить за межі поля. Без перевірки row &gt;= 0, row &lt; 6, col &gt;= 0, col &lt; 7 перед кожним кроком, код читає чужу пам'ять і може хибно виявити «перемогу». Помилка гравітації: фішка потрапляє не в той рядок. Якщо сканувати зверху вниз (від рядка 0), перша знайдена порожня клітинка — верхня, а не нижня. Завжди починайте з рядка 5 (низ) і скануйте вгору. ШІ не прибирає тимчасову фішку. Після перевірки check_win з тимчасовою фішкою потрібно обов'язково записати 0 назад у клітинку, інакше поле залишиться спотвореним. Рахуємо лише в одному напрямку. Якщо перевіряти лише вправо від позиції, то послідовність _ X X [X] X (де [X] — щойно зроблений хід) дасть count = 2 замість 4. Потрібно рахувати в обидві сторони. Перевірка перемоги лише для щойно зробленого ходу пропущена. Достатньо перевіряти перемогу лише навколо щойно розміщеної фішки, а не для всього поля — це і швидше, і простіше. <a class="internal-link" data-href="../arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви</a>
<br><a class="internal-link" data-href="../loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Цикли</a>
<br><a class="internal-link" data-href="../flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Прапорці та умовні переходи</a>
<br><a class="internal-link" data-href="../procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури</a>
<br><a class="internal-link" data-href="G01.md" href="projects/how-to/game-logic/g01.html" target="_self" rel="noopener nofollow">Ігрова логіка: хрестики-нулики</a>
<br>
Корисно для: <a class="internal-link" data-href="../../Y04-Connect-Four.md" href="projects/y04-connect-four.html" target="_self" rel="noopener nofollow">Y04 Чотири в ряд</a>
]]></description><link>projects/how-to/game-logic/y04.html</link><guid isPermaLink="false">Projects/How-to/game-logic/Y04.md</guid><pubDate>Tue, 24 Feb 2026 07:29:32 GMT</pubDate></item><item><title><![CDATA[Y23]]></title><description><![CDATA[Алгоритми обчислення дня тижня (конґруенція Целлера), перевірки високосного року, навігації між місяцями та зберігання подій у масиві записів.
Потрібно визначити, з якого дня тижня починається будь-який місяць, щоб правильно розмістити числа у сітці календаря.
Потрібно коректно обробляти лютий у високосних та звичайних роках.
Потрібно переміщуватися між місяцями (наступний / попередній) із коректним перерахунком розкладки.
Потрібно зберігати, шукати та видаляти події у масиві записів фіксованого розміру.
Формула визначає день тижня для будь-якої дати. На 8086 кожен доданок обчислюється через MUL / DIV із константними дільниками.Важливо: січень та лютий вважаються місяцями 13 і 14 попереднього року. Перед обчисленням треба скоригувати:Якщо month &lt;= 2: month = month + 12 year = year - 1
Формула:q = day
m = month ; після коригування
K = year MOD 100 ; рік у столітті
J = year / 100 ; століття h = (q + 13*(m+1)/5 + K + K/4 + J/4 + 5*J) MOD 7
Результат: h = 0 — субота, 1 — неділя, ..., 6 — п'ятниця.Для діапазону 2000-2099 достатньо спрощеного правила: рік високосний, якщо ділиться на 4. На 8086 це перевіряється однією побітовою операцією:TEST year_low_byte, 03h ; перевірити 2 молодших біти
JZ is_leap ; якщо обидва нулі — високосний
Повне правило (якщо потрібна загальність): ділиться на 4, але не на 100, окрім кратних 400.month_days DB 31,28,31,30,31,30,31,31,30,31,30,31
Для лютого високосного року додати 1 до елемента з індексом 1. Доступ: MOV AL, [month_days + BX], де BX = місяць - 1.При переході до наступного місяця:
Інкрементувати cur_month.
Якщо cur_month &gt; 12 — встановити 1 та інкрементувати cur_year.
Скинути cur_day на 1 (або обмежити довжиною нового місяця).
Перерахувати день тижня для 1-го числа нового місяця.
Аналогічно для попереднього місяця (декремент, перехід 1 -&gt; 12 із декрементом року).Кожна подія — запис фіксованого розміру (45 байтів):Зміщення запису N у масиві: offset = N * 45. На 8086:MOV AX, N
MOV BX, 45
MUL BX ; AX = зміщення
ADD AX, OFFSET events
Пошук подій для дати — лінійне сканування з порівнянням 3 байтів (year_off, month, day).; --- Дані ---
month_days DB 31,28,31,30,31,30,31,31,30,31,30,31 EVENT_SIZE EQU 45 ; розмір одного запису події
MAX_EVENTS EQU 50
events DB MAX_EVENTS * EVENT_SIZE DUP(0)
event_count DW 0
; --- Конґруенція Целлера: день тижня для 1-го числа місяця ---
; Вхід: cur_year (слово), cur_month (байт)
; Вихід: AL = день тижня (0=Сб, 1=Нд, ..., 6=Пт)
get_day_of_week PROC push bx push cx push dx ; ... ваш код тут ... ; Крок 1: коригування місяця/року — див. алгоритм вище ; Крок 2: обчислити K, J, потім усі 6 доданків формули ; Крок 3: взяти MOD 7 pop dx pop cx pop bx ret
get_day_of_week ENDP
; --- Перевірка високосного року (діапазон 2000-2099) ---
; Вхід: cur_year (слово)
; Вихід: AL = 1 якщо високосний, 0 якщо ні
is_leap_year PROC ; ... ваш код тут ... ; Підказка: TEST al, 03h — див. алгоритм вище ret
is_leap_year ENDP
; --- Пошук подій для заданої дати ---
; Вхід: AL = year_off, AH = month, BL = day
; Вихід: CX = кількість знайдених подій
find_events PROC push si push dx ; ... ваш код тут ... ; Лінійне сканування: порівняти 3 байти кожного запису ; див. структуру запису та алгоритм вище pop dx pop si ret
find_events ENDP Січень і лютий без коригування Целлера. Формула вимагає, щоб січень = місяць 13, лютий = місяць 14 попереднього року. Без цього дні тижня для січня та лютого будуть хибними.
Від'ємний проміжний результат у формулі Целлера. На 8086 ділення без знаку. Якщо сума стає від'ємною, MOD 7 дасть неправильний результат. Додайте достатнє кратне 7 до суми перед операцією MOD (наприклад, + 7*100).
Переповнення масиву подій. Якщо не перевіряти event_count &gt;= MAX_EVENTS перед додаванням, запис за межами масиву перезапише інші дані.
Забутий перенос при MUL. Інструкція MUL BX записує результат у DX:AX. Якщо DX не враховується, великі добутки (наприклад, 50 * 45 = 2250) обрізаються. <a class="internal-link" data-href="../arithmetic.md" href="projects/how-to/arithmetic.html" target="_self" rel="noopener nofollow">Цілочисельна арифметика</a>
<br><a class="internal-link" data-href="../arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви</a>
<br><a class="internal-link" data-href="../lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиці пошуку</a>
<br><a class="internal-link" data-href="../file-io.md" href="projects/how-to/file-io.html" target="_self" rel="noopener nofollow">Файловий ввід/вивід</a>
<br><a class="internal-link" data-href="../video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять</a>
<br><a class="internal-link" data-href="../loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Цикли</a>
<br>
Корисно для: <a class="internal-link" data-href="../../Y23-Calendar-Planner.md" href="projects/y23-calendar-planner.html" target="_self" rel="noopener nofollow">Y23 Календар-планувальник</a>
]]></description><link>projects/how-to/game-logic/y23.html</link><guid isPermaLink="false">Projects/How-to/game-logic/Y23.md</guid><pubDate>Tue, 24 Feb 2026 07:29:25 GMT</pubDate></item><item><title><![CDATA[Y09]]></title><description><![CDATA[Алгоритми сканування дійсних ходів у 8 напрямках, перевертання дисків та виявлення пропуску/кінця гри.
Потрібно визначити, які ходи дозволені для поточного гравця
Потрібно перевернути захоплені диски суперника після розміщення
Потрібно обробити ситуацію, коли гравець не має ходів (пропуск або кінець гри)
Дошка — байтовий масив 64 байти (8x8). Кожен байт:EMPTY EQU 0
WHITE EQU 1
RED EQU 2
Індекс клітинки (row, col) = row * 8 + col, де row та col у діапазоні 0-7.Стартова позиція (D4/E5 = білі, D5/E4 = червоні):board DB 64 DUP(0)
; Під час ініціалізації:
; board[3*8+3] = WHITE, board[4*8+4] = WHITE
; board[3*8+4] = RED, board[4*8+3] = RED
Таблиця 8 напрямків — пари (delta_row, delta_col) зі знаком:dir_table DB -1, 0 ; N (вгору) DB -1, 1 ; NE DB 0, 1 ; E (вправо) DB 1, 1 ; SE DB 1, 0 ; S (вниз) DB 1,-1 ; SW DB 0,-1 ; W (вліво) DB -1,-1 ; NW
Хід дійсний, якщо:
Клітинка порожня
Хоча б в одному з 8 напрямків є послідовність суперник, суперник, ..., свій (без порожніх клітинок між ними)
is_valid_move(row, col, player): якщо board[row*8+col] != EMPTY: повернути НЕВАЛІДНИЙ opponent = 3 - player ; якщо player=1, opponent=2 і навпаки для кожного напрямку (dr, dc) з dir_table: r = row + dr, c = col + dc count = 0 поки 0 &lt;= r &lt;= 7 та 0 &lt;= c &lt;= 7: якщо board[r*8+c] = opponent: count += 1 r += dr, c += dc інакше: вийти з циклу якщо count &gt; 0 та 0 &lt;= r &lt;= 7 та 0 &lt;= c &lt;= 7: якщо board[r*8+c] = player: повернути ВАЛІДНИЙ повернути НЕВАЛІДНИЙ
Для пошуку всіх ходів — пройти всі 64 клітинки та для кожної порожньої викликати is_valid_move. Зберегти дійсні ходи у масив:valid_moves DB 64 DUP(?) ; пари (row, col)
valid_count DB 0
Після розміщення диска потрібно перевернути захоплені диски суперника у кожному з 8 напрямків:flip_discs(row, col, player): opponent = 3 - player для кожного напрямку (dr, dc) з dir_table: ; Спочатку перевірити, чи є що перевертати r = row + dr, c = col + dc count = 0 поки 0 &lt;= r &lt;= 7 та 0 &lt;= c &lt;= 7 та board[r*8+c] = opponent: count += 1 r += dr, c += dc ; Перевернути лише якщо лінія закінчується своїм диском якщо count &gt; 0 та 0 &lt;= r &lt;= 7 та 0 &lt;= c &lt;= 7 та board[r*8+c] = player: ; Повернутись і перевернути count дисків r = row + dr, c = col + dc повторити count разів: board[r*8+c] = player r += dr, c += dc
Важливо: перевертання відбувається у два проходи. Перший прохід — підрахувати та перевірити, що лінія закінчується своїм диском. Другий прохід — власне перевертання. Якщо робити в один прохід, можна перевернути диски в напрямку, де лінія не завершується.Після кожного ходу:consecutive_passes DB 0 after_move: переключити гравця знайти дійсні ходи для нового гравця якщо valid_count &gt; 0: consecutive_passes = 0 гравець робить хід інакше: consecutive_passes += 1 якщо consecutive_passes &gt;= 2: КІНЕЦЬ ГРИ (підрахувати рахунок) інакше: показати повідомлення "Пропуск" переключити гравця назад
EMPTY EQU 0
WHITE EQU 1
RED EQU 2 ; --- Таблиця напрямків (8 пар: delta_row, delta_col) ---
dir_table DB -1, 0, -1, 1, 0, 1, 1, 1 DB 1, 0, 1,-1, 0,-1,-1,-1 ; --- Підрахунок перевертань в одному напрямку ---
; Вхід: DL = row, DH = col, CL = player, SI = зміщення в dir_table
; Вихід: AL = кількість дисків для перевертання (0 = невалідний напрямок)
count_flips_dir PROC PUSH BX PUSH DX ; ... ваш код тут ... ; 1. Обчислити opponent = 3 - player ; 2. Завантажити дельти dr, dc з dir_table (див. алгоритм вище) ; 3. Крокувати у напрямку: перевірка меж (CMP reg, 8 / JAE) ; 4. Рахувати opponent-диски, зупинитись на player-диску або порожній POP DX POP BX RET
count_flips_dir ENDP ; --- Перевірка дійсності ходу ---
; Вхід: DL = row, DH = col, CL = player
; Вихід: AL = загальна кількість дисків для перевертання (0 = невалідний)
is_valid_move PROC PUSH SI PUSH CX PUSH DX ; ... ваш код тут ... ; 1. Перевірити, що board[row*8+col] = EMPTY ; 2. Цикл по 8 напрямках: CALL count_flips_dir, накопичити суму ; (див. алгоритм вище) POP DX POP CX POP SI RET
is_valid_move ENDP Перевертання без перевірки кінцевого диска. Якщо лінія суперник, суперник, порожня — перевертати не можна. Лінія обов'язково повинна закінчуватись диском поточного гравця (board[r*8+c] = player). Перевірка меж лише на початку, а не на кожному кроці. Сканування у напрямку може вийти за межі дошки (row &lt; 0 або row &gt; 7). Оскільки координати зберігаються як беззнакові байти, -1 стає 255 — перевірка CMP DL, 8 / JAE ловить обидва випадки (і &gt; 7, і обгортання). Забули лічильник consecutive_passes. Без нього гра або пропускає ходи нескінченно, або не виявляє кінець гри, коли обидва гравці не можуть ходити. Плутанина WHITE та RED при переключенні гравця. Якщо WHITE=1, RED=2, то формула opponent = 3 - player працює коректно для обох значень. Не використовуйте XOR 1 — це дасть 0 для RED. <a class="internal-link" data-href="../arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви</a>
<br><a class="internal-link" data-href="../lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиці пошуку</a>
<br><a class="internal-link" data-href="../loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Цикли</a>
<br><a class="internal-link" data-href="../procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури</a>
<br><a class="internal-link" data-href="../flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Прапорці та умовні переходи</a>
<br>
Корисно для: <a class="internal-link" data-href="../../Y09-Othello-Reversi.md" href="projects/y09-othello-reversi.html" target="_self" rel="noopener nofollow">Y09 Отелло / Реверсі</a>
]]></description><link>projects/how-to/game-logic/y09.html</link><guid isPermaLink="false">Projects/How-to/game-logic/Y09.md</guid><pubDate>Tue, 24 Feb 2026 07:29:21 GMT</pubDate></item><item><title><![CDATA[Y14]]></title><description><![CDATA[Алгоритми перетворення hex-ASCII, буферизації файлових блоків, трьохколонкового компонування екрану та навігації по зміщенню.
Потрібно відобразити довільні двійкові дані у зручному для людини hex-форматі.
Потрібно організувати трьохколонковий дисплей (зміщення | hex | ASCII) у відеопам'яті.
Потрібно завантажувати та переглядати файли, більші за один екран, із прокруткою.
Кожен байт (00h–FFh) складається з двох тетрад (nibble): старшої (біти 7–4) та молодшої (біти 3–0). Перетворення кожної тетради у символ '0'–'F' — базова операція hex-переглядача.Два підходи:Алгоритм з таблицею пошуку (рекомендовано):1. Завантажити байт у AL
2. Скопіювати AL у AH (зберегти оригінал)
3. SHR AL, 4 ; старша тетрада
4. LEA BX, hex_chars
5. XLAT ; AL = hex_chars[AL]
6. Записати AL у відеопам'ять (старша цифра)
7. MOV AL, AH
8. AND AL, 0Fh ; молодша тетрада
9. XLAT ; AL = hex_chars[AL]
10. Записати AL у відеопам'ять (молодша цифра)
Кожен рядок hex-дампу показує 16 байтів і має три зони:Стовпці екрану (80 символів):
[0..7] зміщення у файлі (8 hex-цифр)
[9] розділювач ':'
[10..57] 16 hex-байтів (кожен 2 цифри + пробіл = 3 символи)
[59..74] 16 ASCII-символів
Формула для позиції hex-байта з індексом i (0–15) у рядку:стовпець_hex = 10 + i * 3
стовпець_ascii = 59 + i
Для запису у відеопам'ять (B800h) зміщення обчислюється:зміщення = (рядок * 80 + стовпець) * 2
Кожна позиція у відеопам'яті — це два байти: символ + атрибут кольору.Різні класи байтів отримують різні атрибути:Для кожного байта:
1. CMP AL, 0 → JE null_byte → атрибут = 08h
2. CMP AL, 20h → JB ctrl_byte → атрибут = 0Eh
3. CMP AL, 7Fh → JAE ctrl_byte → атрибут = 0Eh
4. Інакше: атрибут = 0Fh
Для файлів більших за один екран потрібна стратегія буферизації. Один екран (24 рядки по 16 байтів) показує 384 байти. Практичний розмір буфера — 4096 байтів (одна сторінка).Алгоритм віконного буфера:file_offset DW 0, 0 ; 32-бітне поточне зміщення (DX:AX для seek)
buffer DB 4096 DUP(0)
buf_size DW 0 ; скільки байтів реально прочитано Завантажити блок:
1. MOV AH, 42h ; lseek MOV AL, 0 ; від початку файлу MOV CX, [file_offset+2] MOV DX, [file_offset] INT 21h
2. MOV AH, 3Fh ; read MOV CX, 4096 LEA DX, buffer INT 21h
3. MOV [buf_size], AX ; AX = реально прочитано
При прокрутці PgDn — збільшити file_offset на 384 (24 * 16) і перезавантажити блок. При PgUp — зменшити, не допускаючи від'ємного значення.Для переходу до довільного зміщення потрібен міні-парсер hex-рядка:Парсинг hex-рядка у 16-бітне число: result = 0 Для кожного символу вхідного рядка: result = result SHL 4 Якщо символ &gt;= '0' і &lt;= '9': тетрада = символ - '0' Якщо символ &gt;= 'A' і &lt;= 'F': тетрада = символ - 'A' + 10 Якщо символ &gt;= 'a' і &lt;= 'f': тетрада = символ - 'a' + 10 result = result OR тетрада
; --- Таблиця для перетворення тетради у hex-символ ---
hex_chars DB '0123456789ABCDEF' ; --- Перетворення байта AL у два hex-символи у відеопам'ять ---
; Вхід: AL = байт, ES:DI = позиція у відеопам'яті, AH = атрибут кольору
; Вихід: DI збільшено на 4 (2 символи * 2 байти)
byte_to_hex PROC push bx push ax mov bx, OFFSET hex_chars ; --- Старша тетрада: SHR + XLAT + STOSB --- ; ... ваш код тут ... ; див. алгоритм вище: кроки 3-6 ; --- Молодша тетрада: AND 0Fh + XLAT + STOSB --- ; ... ваш код тут ... ; див. алгоритм вище: кроки 7-10 pop bx ret
byte_to_hex ENDP ; --- Відображення одного рядка hex-дампу ---
; Вхід: DS:SI = вказівник на 16 байтів даних
; ES:DI = початок рядка у відеопам'яті
; CX = кількість валідних байтів (1..16)
draw_hex_row PROC push cx push si push di add di, 20 ; пропустити зону зміщення (10 стовпців * 2) mov dx, cx xor bx, bx
@@hex_loop: cmp bx, dx jge @@hex_done ; --- Визначити колір байта (див. таблицю кольорового кодування) --- ; ... ваш код тут ... ; підказка: CMP AL,0 / CMP AL,20h / CMP AL,7Fh → вибір атрибута ; --- Записати hex-пару та пробіл --- ; ... ваш код тут ... inc bx jmp @@hex_loop
@@hex_done: pop di pop si pop cx ret
draw_hex_row ENDP Забули SHR на 4 для старшої тетради. Без зсуву старша тетрада лишається у бітах 7–4 і дає індекс 16–255 замість 0–15 — вихід за межі таблиці пошуку.
Помилка на одиницю в останньому рядку. Якщо файл має, наприклад, 20 байтів, другий рядок містить лише 4 байти. Потрібно доповнити hex-зону пробілами та обмежити ASCII-колонку.
32-бітне зміщення для seek. INT 21h функція 42h приймає зміщення у CX:DX (32 біти). Для файлів менше 64 КБ CX=0, але для більших файлів потрібно правильно встановити обидва регістри.
XLAT потребує BX. Інструкція XLAT використовує DS:[BX+AL]. Якщо BX не вказує на таблицю, результат буде хибним. Завжди встановлюйте BX = OFFSET hex_chars перед циклом. <a class="internal-link" data-href="../lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиці пошуку</a>
<br><a class="internal-link" data-href="../file-io.md" href="projects/how-to/file-io.html" target="_self" rel="noopener nofollow">Файловий ввід/вивід</a>
<br><a class="internal-link" data-href="../arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви</a>
<br><a class="internal-link" data-href="../video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Прямий запис у відеопам'ять</a>
<br><a class="internal-link" data-href="../bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Побітові операції</a>
<br>
Корисно для: <a class="internal-link" data-href="../../Y14-Hex-Viewer-Editor.md" href="projects/y14-hex-viewer-editor.html" target="_self" rel="noopener nofollow">Y14 Hex-переглядач/редактор</a>
]]></description><link>projects/how-to/game-logic/y14.html</link><guid isPermaLink="false">Projects/How-to/game-logic/Y14.md</guid><pubDate>Tue, 24 Feb 2026 07:29:03 GMT</pubDate></item><item><title><![CDATA[Y03]]></title><description><![CDATA[Алгоритми заливки (flood fill) з явним стеком, безпеки першого кліку, підрахунку сусідніх мін та розміщення мін.
Потрібно каскадно відкрити всі порожні клітинки при натисканні на клітинку з нулем сусідніх мін
Потрібно гарантувати, що перший клік гравця не потрапить на міну
Потрібно коректно підрахувати кількість мін навколо кожної клітинки
Для сітки NxM (наприклад, 9x9 = 81 клітинка) використовуємо окремі масиви для кожної властивості клітинки:mines DB 81 DUP(0) ; 1 = міна, 0 = безпечна
counts DB 81 DUP(0) ; кількість сусідніх мін (0-8)
revealed DB 81 DUP(0) ; 1 = відкрита, 0 = закрита
flagged DB 81 DUP(0) ; 1 = прапорець, 0 = без прапорця
Клітинка (row, col) знаходиться за зміщенням row * width + col. Альтернатива — упакувати все в один байт (біт 7 = міна, біти 3-0 = підрахунок, біт 6 = відкрита, біт 5 = прапорець), але паралельні масиви простіші для налагодження.Потрібно розмістити N мін у випадкових позиціях. Простий підхід — генерувати випадкову позицію через лічильник тактів (INT 1Ah) та перевіряти, чи вона вже зайнята:placed = 0
ПОКИ placed &lt; N: pos = random() MOD (width * height) ЯКЩО mines[pos] == 0: mines[pos] = 1 placed = placed + 1
Після розміщення всіх мін, для кожної клітинки без міни обчислюємо кількість мін серед 8 сусідів:ДЛЯ кожної клітинки (r, c): ЯКЩО mines[r*w+c] == 1: пропустити count = 0 ДЛЯ кожного сусіда (nr, nc) з 8 напрямків: ЯКЩО nr &gt;= 0 І nr &lt; height І nc &gt;= 0 І nc &lt; width: count = count + mines[nr*w + nc] counts[r*w+c] = count
Допоміжна процедура get_mine(row, col) повертає 0 для координат за межами — це спрощує код і запобігає помилкам на краях.Якщо перший клік потрапляє на міну, потрібно перемістити її:ЯКЩО це перший хід І mines[pos] == 1: mines[pos] = 0 знайти першу позицію, де mines[i] == 0 mines[i] = 1 перерахувати counts[] для всіх клітинок
Рекурсивна заливка через CALL/RET ризикує переповнити апаратний стек (на сітці 16x16 глибина може досягти 256, кожен кадр ~6-10 байтів). Безпечніший підхід — явний стек у масиві даних:flood_stack DB 512 DUP(?) ; масив пар (row, col)
flood_sp DW 0 ; вказівник стека (індекс у масиві)
Алгоритм:ПРОЦЕДУРА reveal_cell(r, c): ЯКЩО revealed[r*w+c] == 1: повернути ; вже відкрита ЯКЩО flagged[r*w+c] == 1: повернути ; є прапорець revealed[r*w+c] = 1 ЯКЩО counts[r*w+c] &gt; 0: повернути ; показати число і зупинити ; Клітинка порожня (0 сусідів) → запустити заливку flood_sp = 0 ЗАПИСАТИ (r, c) у flood_stack flood_sp = flood_sp + 2 ПОКИ flood_sp &gt; 0: flood_sp = flood_sp - 2 ЗЧИТАТИ (cr, cc) з flood_stack[flood_sp] ДЛЯ кожного сусіда (nr, nc) з 8 напрямків: ЯКЩО nr, nc в межах сітки І revealed[nr*w+nc] == 0 І mines[nr*w+nc] == 0: revealed[nr*w+nc] = 1 ЯКЩО counts[nr*w+nc] == 0: ЗАПИСАТИ (nr, nc) у flood_stack flood_sp = flood_sp + 2
Ключовий момент: клітинки з числом &gt; 0 відкриваються, але їхні сусіди не додаються в стек. Це створює «межу» з чисел навколо порожньої зони.; --- Дані ---
flood_stack DB 512 DUP(?) ; масив пар (row, col)
flood_sp DW 0 ; вказівник стека (індекс у масиві) ; --- Заливка з явним стеком ---
; Вхід: DH = row, DL = col початкової клітинки
; grid_w = ширина сітки
; Використовує: flood_stack, flood_sp, revealed[], mines[], counts[]
flood_fill PROC PUSH AX PUSH BX PUSH CX PUSH SI ; 1. Ініціалізувати flood_sp = 0, записати (DH, DL) у flood_stack ; 2. Цикл: поки flood_sp &gt; 0 ; - зняти (row, col) зі стека ; - для кожного з 8 сусідів (dr від -1 до +1, dc від -1 до +1): ; - пропустити (0,0), перевірити межі ; - обчислити зміщення nr*w+nc ; - якщо не відкрита і не міна → відкрити ; - якщо counts[зміщення] == 0 → додати в стек ; див. алгоритм заливки вище ; ... ваш код тут ... POP SI POP CX POP BX POP AX RET
flood_fill ENDP Рекурсивна заливка через CALL/RET переповнює стек. На великій порожній ділянці (наприклад, 16x16 без мін) глибина рекурсії досягає 256 кадрів, що споживає ~2 КБ стека. Використовуйте явний стек у сегменті даних. Забули позначити клітинку як відкриту перед додаванням у стек. Без цього одна клітинка може потрапити в стек кілька разів від різних сусідів, що призводить до переповнення стека та зайвих операцій. Перша клітинка — міна. Без перевірки безпеки першого кліку гравець може програти на першому ході, що робить гру несправедливою. Перемістіть міну після першого відкриття. Підрахунок сусідів не враховує краї. Клітинка (0, 0) має лише 3 сусідів. Без перевірки меж код читає дані за межами масиву, що дає хибний підрахунок. Заливка відкриває клітинки з прапорцями. Клітинки, позначені прапорцем, мають залишатися закритими навіть під час заливки. <a class="internal-link" data-href="../procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури</a>
<br><a class="internal-link" data-href="../arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви</a>
<br><a class="internal-link" data-href="../random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">Випадкові числа</a>
<br><a class="internal-link" data-href="../flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Прапорці та умовні переходи</a>
<br><a class="internal-link" data-href="../lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиці пошуку</a>
<br>
Корисно для: <a class="internal-link" data-href="../../Y03-Minesweeper.md" href="projects/y03-minesweeper.html" target="_self" rel="noopener nofollow">Y03 Сапер</a>
]]></description><link>projects/how-to/game-logic/y03.html</link><guid isPermaLink="false">Projects/How-to/game-logic/Y03.md</guid><pubDate>Tue, 24 Feb 2026 07:29:02 GMT</pubDate></item><item><title><![CDATA[Y08]]></title><description><![CDATA[Алгоритми валідації розміщення кораблів, відстеження влучень/промахів/потоплень та режими ШІ «полювання/ціль».
Потрібно перевірити, чи корабель можна розмістити у вибраній позиції без накладання та виходу за межі
Потрібно визначити результат пострілу: промах, влучення, потоплення корабля
Потрібно реалізувати ШІ, який обирає клітинку для пострілу
Дошка — байтовий масив 10x10 (100 байт). Кожен байт кодує стан клітинки:CELL_EMPTY EQU 0 ; вода, не стріляли
CELL_MISS EQU 1 ; промах (стріляли, але пусто)
CELL_SHIP EQU 10h ; корабель (старший ніббл = ID корабля, 1-5)
CELL_HIT EQU 80h ; прапорець влучення (OR з CELL_SHIP)
Наприклад, клітинка з кораблем #3 = 30h. Після влучення = B0h (30h OR 80h). Для перевірки:
чи є корабель: TEST cell, 0F0h (старший ніббл != 0)
чи влучено: TEST cell, 80h
ID корабля: (cell AND 70h) SHR 4
Паралельно — таблиця дескрипторів 5 кораблів (по 5 байт кожен):; ship_id, start_row, start_col, orientation (0=гор, 1=верт), length
ship_table DB 1, 0, 0, 0, 2 ; катер (2) DB 2, 0, 0, 0, 3 ; есмінець (3) DB 3, 0, 0, 0, 3 ; підводний човен (3) DB 4, 0, 0, 0, 4 ; крейсер (4) DB 5, 0, 0, 0, 5 ; авіаносець (5)
Перед розміщенням потрібно перевірити дві умови:
Межі сітки. Горизонтальний: col + length - 1 &lt;= 9. Вертикальний: row + length - 1 &lt;= 9.
Накладання. Кожна клітинка, яку займе корабель, повинна бути CELL_EMPTY.
validate_placement(row, col, length, orientation): якщо orientation = 0 (горизонтальний): якщо col + length &gt; 10: повернути НЕВАЛІДНИЙ для i = 0..length-1: якщо board[row * 10 + col + i] != EMPTY: повернути НЕВАЛІДНИЙ якщо orientation = 1 (вертикальний): якщо row + length &gt; 10: повернути НЕВАЛІДНИЙ для i = 0..length-1: якщо board[(row + i) * 10 + col] != EMPTY: повернути НЕВАЛІДНИЙ повернути ВАЛІДНИЙ
Після валідації — записати ID корабля у відповідні клітинки.Постріл по клітинці (row, col) на дошці суперника:
Обчислити індекс: idx = row * 10 + col
Якщо board[idx] вже стріляли (MISS або HIT) — відхилити
Якщо board[idx] = EMPTY — записати MISS, повернути «промах»
Якщо є корабель — встановити прапорець 80h (OR), потім перевірити потоплення
Перевірка потоплення:check_sunk(ship_id): для кожної клітинки дошки (0..99): якщо (cell AND 70h) SHR 4 = ship_id: якщо NOT (cell AND 80h): повернути НЕ ПОТОПЛЕНИЙ повернути ПОТОПЛЕНИЙ
Альтернативно, у дескрипторі корабля зберігайте hits_remaining. Після кожного влучення зменшуйте лічильник; якщо він став 0 — корабель потоплений.Базовий ШІ (випадковий): обрати випадкову клітинку, по якій ще не стріляли.Розумний ШІ (полювання/ціль):ШІ має два режими, які працюють як скінченний автомат:Стан: HUNT (полювання) -- стріляти випадково TARGET (ціль) -- стріляти по сусідніх клітинках останнього влучення Якщо HUNT: обрати випадкову нестріляну клітинку якщо влучення -&gt; зберегти координати, перейти в TARGET Якщо TARGET: спробувати сусідні клітинки (вгору, вниз, ліво, право) від збережених координат якщо влучення -&gt; оновити напрямок, продовжити вздовж лінії якщо промах або вичерпано сусідів -&gt; повернутись у HUNT якщо потоплено -&gt; повернутись у HUNT
Для відстеження стану ШІ потрібні змінні:ai_mode DB 0 ; 0 = HUNT, 1 = TARGET
ai_hit_row DB ? ; рядок першого влучення
ai_hit_col DB ? ; стовпець першого влучення
ai_dir DB 0 ; поточний напрямок спроби (0-3)
CELL_EMPTY EQU 0
CELL_MISS EQU 1
CELL_SHIP EQU 10h
CELL_HIT EQU 80h ; --- Постріл по дошці суперника ---
; Вхід: DL = рядок, DH = стовпець, SI = адреса дошки
; Вихід: AL = 0 (промах), 1 (влучення), 2 (потоплений)
fire_shot PROC PUSH BX PUSH CX ; ... ваш код тут ... ; 1. Обчислити індекс: row * 10 + col (див. алгоритм вище) ; 2. Перевірити стан клітинки (TEST на 0F0h) ; 3. Промах -&gt; записати CELL_MISS, влучення -&gt; OR 80h ; 4. При влученні: витягти ship_id та CALL check_sunk POP CX POP BX RET
fire_shot ENDP ; --- Перевірка потоплення корабля ---
; Вхід: CL = ship_id, SI = адреса дошки
; Вихід: AL = 1 якщо потоплений, 0 якщо ні
check_sunk PROC PUSH BX PUSH DX ; ... ваш код тут ... ; Пройти 100 клітинок дошки (див. алгоритм вище) ; Для кожної: якщо ID збігається і немає прапорця 80h -&gt; не потоплений POP DX POP BX RET
check_sunk ENDP Не зберігати ID корабля у клітинці дошки. Якщо всі клітинки з кораблями мають однакове значення, після влучення неможливо визначити, який саме корабель уражений, а отже — неможливо перевірити потоплення. Записуйте ID (1-5) у старший ніббл. Помилка на одиницю при перевірці меж. Корабель довжиною 5 у стовпці 6 горизонтально: 6 + 5 - 1 = 10, що виходить за індекс 9. Правильна перевірка: col + length &lt;= 10 (або col + length - 1 &lt;= 9). Повторний постріл по тій самій клітинці. Без перевірки ШІ може нескінченно стріляти по вже стріляних клітинках. Перед пострілом перевіряйте TEST cell, 80h (влучення) та CMP cell, CELL_MISS (промах). ШІ у режимі TARGET забуває повернутись у HUNT. Якщо корабель потоплений або всі сусідні клітинки випробувані, ШІ повинен скинути ai_mode у 0. Інакше він застрягне, намагаючись стріляти по неіснуючих сусідах. <a class="internal-link" data-href="../arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви</a>
<br><a class="internal-link" data-href="../random-numbers.md" href="projects/how-to/random-numbers.html" target="_self" rel="noopener nofollow">Генерація псевдовипадкових чисел</a>
<br><a class="internal-link" data-href="../flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Прапорці та умовні переходи</a>
<br><a class="internal-link" data-href="../bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Побітові операції</a>
<br><a class="internal-link" data-href="../procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури</a>
<br><a class="internal-link" data-href="Y10.md" href="projects/how-to/game-logic/y10.html" target="_self" rel="noopener nofollow">Ігрова логіка: Space Invaders</a>
<br>
Корисно для: <a class="internal-link" data-href="../../Y08-Battleship.md" href="projects/y08-battleship.html" target="_self" rel="noopener nofollow">Y08 Морський бій</a>
]]></description><link>projects/how-to/game-logic/y08.html</link><guid isPermaLink="false">Projects/How-to/game-logic/Y08.md</guid><pubDate>Tue, 24 Feb 2026 07:28:51 GMT</pubDate></item><item><title><![CDATA[interrupt-handlers]]></title><description><![CDATA[Як встановлювати власні обробники переривань через DOS, зберігати та відновлювати старі вектори, і коректно передавати керування за ланцюжком.
Потрібно виконувати дію автоматично за таймером (наприклад, інкрементувати лічильник 18.2 рази/с через INT 1Ch).
Потрібно перехопити апаратне переривання клавіатури (INT 09h) або обробити Ctrl+C (INT 23h).
Потрібно додати свою логіку до системного переривання, зберігши оригінальну поведінку (ланцюжок обробників).
У реальному режимі за адресою 0000:0000 розташована Interrupt Vector Table -- 256 записів по 4 байти кожен. Кожен запис містить адресу обробника у форматі сегмент:зміщення:Коли процесор виконує INT n, він знаходить адресу обробника за зміщенням n * 4 у IVT.Безпечний спосіб -- використовувати функції DOS (INT 21h), які тимчасово забороняють переривання:
Зберегти старий вектор (AH=35h) у змінній (4 байти: offset + segment).
Встановити свій обробник (AH=25h).
У вашому обробнику (ISR): виконати роботу, потім JMP FAR [old_vector] -- це передає керування оригінальному обробнику.
Перед виходом з програми -- відновити старий вектор (AH=25h з адресою зі збереженої змінної).
Якщо ваш обробник повністю замінює оригінальний -- замість JMP FAR використовуйте IRET.
Зберігайте всі регістри, які використовуєте (PUSH AX, PUSH BX тощо).
Тримайте обробник коротким -- ви всередині переривання, і система чекає.
Не викликайте DOS (INT 21h) з апаратного ISR -- DOS не є реентрантною.
Завершуйте через IRET (якщо не передаєте далі) або JMP FAR [old_vector] (якщо ланцюжок).
Відновіть старий вектор перед завершенням програми!
Перехоплення INT 1Ch для інкременту лічильника. Основна програма опитує лічильник і виводить секунди на екран. Перед виходом -- відновлення старого вектора..MODEL small
.STACK 256 .DATA
counter DW 0 ; лічильник тіків (інкрементується в ISR)
msg DB 'Seconds: $'
digit DB '0', 0Dh, 0Ah, '$' ; поточна цифра секунд
prev_sec DB 0 ; попереднє значення секунд .CODE
old_vec DD 0 ; старий вектор у сегменті коду (для CS:) ; ===== Наш обробник INT 1Ch =====
new_1ch PROC FAR push ax push ds mov ax, @data ; відновлюємо DS -- він може mov ds, ax ; вказувати куди завгодно inc word ptr [counter] ; +1 тік (18.2 рази/с) pop ds pop ax jmp dword ptr cs:[old_vec] ; передаємо керування старому обробнику
new_1ch ENDP ; ===== Головна програма =====
start: mov ax, @data mov ds, ax ; --- Зберегти старий вектор INT 1Ch --- mov ah, 35h mov al, 1Ch int 21h ; ES:BX = старий обробник mov word ptr cs:[old_vec], bx ; зміщення mov word ptr cs:[old_vec+2], es ; сегмент ; --- Встановити наш обробник --- push ds mov ax, seg new_1ch mov ds, ax lea dx, new_1ch mov ah, 25h mov al, 1Ch int 21h ; DS:DX = новий обробник pop ds ; --- Основний цикл: виводимо секунди 0..9 ---
main_loop: mov ax, [counter] xor dx, dx mov bx, 18 ; 18 тіків ~ 1 секунда div bx ; AX = секунди cmp al, 10 jae done ; після 10 секунд -- вихід cmp al, [prev_sec] je main_loop ; секунда не змінилась -- чекаємо mov [prev_sec], al ; --- Виводимо повідомлення --- add al, '0' ; перетворюємо число в ASCII mov [digit], al lea dx, msg mov ah, 09h int 21h lea dx, digit mov ah, 09h int 21h jmp main_loop done: ; --- Відновити старий вектор INT 1Ch --- push ds mov dx, word ptr cs:[old_vec] ; зміщення старого обробника mov ax, word ptr cs:[old_vec+2] ; сегмент старого обробника mov ds, ax ; DS:DX = збережена адреса mov ah, 25h mov al, 1Ch int 21h pop ds ; --- Завершення програми --- mov ax, 4C00h int 21h END start Забули відновити старий вектор перед виходом. Після завершення програми IVT все ще вказує на адресу в звільненій пам'яті. Наступний тік таймера спричинить аварію системи.
Виклик INT 21h з апаратного ISR. DOS не є реентрантною -- якщо переривання сталося під час виклику DOS, повторний виклик зіпсує внутрішні структури. Результат -- випадкові зависання.
Забули зберегти/відновити DS в ISR. Коли спрацьовує апаратне переривання, DS може вказувати на будь-який сегмент (не обов'язково на ваш .DATA). Без PUSH DS / MOV DS, @data ви запишете дані за хибною адресою. <a class="internal-link" data-href="timer-delays.md" href="projects/how-to/timer-delays.html" target="_self" rel="noopener nofollow">Системний таймер: INT 1Ah</a>
<br><a class="internal-link" data-href="keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Клавіатурний ввід: INT 16h</a>
<br><a class="internal-link" data-href="segment-registers.md" href="projects/how-to/segment-registers.html" target="_self" rel="noopener nofollow">Сегментні регістри (ES, DS)</a> Детальніше: L07, P07
]]></description><link>projects/how-to/interrupt-handlers.html</link><guid isPermaLink="false">Projects/How-to/interrupt-handlers.md</guid><pubDate>Tue, 24 Feb 2026 06:36:02 GMT</pubDate></item><item><title><![CDATA[file-io]]></title><description><![CDATA[Як створювати, відкривати, читати та записувати файли через переривання INT 21h за допомогою файлових дескрипторів.
Потрібно зберегти результат роботи програми у файл або завантажити дані з файлу.
Потрібно працювати з файлами так само, як з консоллю (через дескриптори).
Потрібно прочитати конфігурацію, ігрове поле або інші дані з диска.
Файловий ввід/вивід у DOS працює через дескриптори (handles) -- числові ідентифікатори відкритих файлів, подібні до файлових дескрипторів Unix. Три стандартні дескриптори доступні без відкриття:Імена файлів мають закінчуватися нулем (0), а не '$'. Такий формат називається ASCIIZ:fname DB 'output.txt', 0 ; ASCIIZ -- для файлових функцій
msg DB 'Hello$' ; '$'-рядок -- лише для AH=09h
Після кожного файлового виклику перевіряйте CF (Carry Flag). CF=1 означає помилку, код помилки -- в AX:int 21h
jc error_handler ; CF=1 -&gt; помилка, код у AX
.MODEL small
.STACK 256
.DATA
fname DB 'hello.txt', 0 ; ім'я файлу (ASCIIZ)
text DB 'Hello' ; текст для запису
textLen EQU $ - text ; довжина тексту
handle DW ?
errMsg DB 'Error!$'
.CODE
start: mov ax, @data mov ds, ax ; --- Створити файл (AH=3Ch) --- mov ah, 3Ch xor cx, cx ; CX=0: звичайний файл lea dx, fname int 21h jc err ; CF=1 -&gt; помилка mov [handle], ax ; зберегти дескриптор ; --- Записати текст (AH=40h) --- mov ah, 40h mov bx, [handle] mov cx, textLen lea dx, text int 21h jc err ; --- Закрити файл (AH=3Eh) --- mov ah, 3Eh mov bx, [handle] int 21h ; --- Завершити програму --- mov ax, 4C00h int 21h
err: mov ah, 09h lea dx, errMsg int 21h mov ax, 4C01h ; код помилки 1 int 21h
END start
.MODEL small
.STACK 256
.DATA
fname DB 'hello.txt', 0
buffer DB 256 DUP(?) ; буфер для читання
handle DW ?
cntMsg DB 'Bytes read: $'
errMsg DB 'Error!$'
.CODE
start: mov ax, @data mov ds, ax ; --- Відкрити файл для читання (AH=3Dh, AL=0) --- mov ah, 3Dh mov al, 0 ; режим: лише читання lea dx, fname int 21h jc err mov [handle], ax ; --- Прочитати до 256 байт (AH=3Fh) --- mov ah, 3Fh mov bx, [handle] mov cx, 256 lea dx, buffer int 21h jc err push ax ; зберегти кількість байт ; --- Закрити файл --- mov ah, 3Eh mov bx, [handle] int 21h ; --- Вивести повідомлення --- mov ah, 09h lea dx, cntMsg int 21h ; --- Вивести кількість як символ (0-9) --- pop ax add al, 30h ; число -&gt; ASCII-цифра mov dl, al mov ah, 02h int 21h ; --- Завершення --- mov ax, 4C00h int 21h
err: mov ah, 09h lea dx, errMsg int 21h mov ax, 4C01h int 21h
END start Використання '$' замість нуля в імені файлу. Файлові функції очікують ASCIIZ-рядок (завершений байтом 0). Символ '$' працює лише для AH=09h. З '$' DOS не знайде кінець імені і поверне помилку.
Не перевіряєте CF після виклику. Якщо файл не існує або диск переповнений -- CF=1, код помилки в AX. Без перевірки програма працюватиме з невалідним дескриптором.
Не закриваєте файл після роботи. DOS підтримує ~20 одночасно відкритих дескрипторів. Без закриття нові виклики AH=3Ch/3Dh повертатимуть помилку. <a class="internal-link" data-href="dos-services.md" href="projects/how-to/dos-services.html" target="_self" rel="noopener nofollow">Сервіси DOS</a>
<br><a class="internal-link" data-href="arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви в сегменті даних</a>
<br><a class="internal-link" data-href="number-display.md" href="projects/how-to/number-display.html" target="_self" rel="noopener nofollow">Відображення чисел</a> Детальніше: L07, P07
]]></description><link>projects/how-to/file-io.html</link><guid isPermaLink="false">Projects/How-to/file-io.md</guid><pubDate>Tue, 24 Feb 2026 06:35:11 GMT</pubDate></item><item><title><![CDATA[pc-speaker]]></title><description><![CDATA[Як генерувати звуки заданої частоти та тривалості, програмуючи таймер 8253 (PIT) і керуючи динаміком через порти вводу/виводу.
Потрібно додати звукові ефекти до програми (сигнал, мелодія, азбука Морзе).
Потрібно відтворити тон певної частоти через вбудований динамік PC.
Потрібно попрактикуватись у роботі з інструкціями IN/OUT та портами.
PC Speaker — одно-бітний динамік, керований каналом 2 таймера 8253 (PIT). Три кроки для відтворення тону:1. Налаштувати канал 2 PIT: MOV AL, 0B6h / OUT 43h, AL (режим 3, square wave).2. Задати частоту: дільник = 1193180 / частота_Гц. Молодший байт першим:MOV AX, divisor ; наприклад, 05D3h для ~800 Гц
OUT 42h, AL ; молодший байт -&gt; порт 42h
MOV AL, AH
OUT 42h, AL ; старший байт -&gt; порт 42h
3. Увімкнути динамік: IN AL, 61h / OR AL, 03h / OUT 61h, AL (біти 0 і 1).Вимкнути: IN AL, 61h / AND AL, 0FCh / OUT 61h, AL (очистити біти 0 і 1).Таблиця частот і дільників:Інструкції IN/OUT: IN AL, port — зчитати байт; OUT port, AL — записати байт у порт.Тривалість контролюється паузою між увімкненням та вимкненням (INT 1Ah, див. <a class="internal-link" data-href="timer-delays.md" href="projects/how-to/timer-delays.html" target="_self" rel="noopener nofollow">timer-delays.md</a>).
Важливо: завжди вимикайте динамік перед завершенням програми! Інакше він продовжить гудіти.
Літера R азбукою Морзе (крапка-тире-крапка) на частоті 1000 Гц:.MODEL small
.STACK 256
.DATA
DOT_TICKS EQU 2 ; ~0.1 с (крапка)
DASH_TICKS EQU 6 ; ~0.3 с (тире)
PAUSE_TICKS EQU 3 ; ~0.15 с (пауза)
FREQ_DIV EQU 04A9h ; 1000 Гц
msg DB 'Morse R: dot-dash-dot', 0Dh, 0Ah, '$'
.CODE ; --- Увімкнути звук (дільник в AX) ---
sound_on PROC push ax mov al, 0B6h ; канал 2, режим 3 out 43h, al pop ax out 42h, al ; молодший байт дільника mov al, ah out 42h, al ; старший байт дільника in al, 61h or al, 03h ; увімкнути динамік out 61h, al ret
sound_on ENDP ; --- Вимкнути звук ---
sound_off PROC in al, 61h and al, 0FCh ; вимкнути динамік out 61h, al ret
sound_off ENDP ; --- Пауза на BX тактів таймера (INT 1Ah) ---
delay PROC mov ah, 00h int 1Ah mov si, dx ; SI = початковий лічильник
wait_lp: mov ah, 00h int 1Ah sub dx, si cmp dx, bx jb wait_lp ret
delay ENDP start: mov ax, @data mov ds, ax lea dx, msg mov ah, 09h int 21h ; --- Крапка --- mov ax, FREQ_DIV call sound_on mov bx, DOT_TICKS call delay call sound_off mov bx, PAUSE_TICKS call delay ; --- Тире --- mov ax, FREQ_DIV call sound_on mov bx, DASH_TICKS call delay call sound_off mov bx, PAUSE_TICKS call delay ; --- Крапка --- mov ax, FREQ_DIV call sound_on mov bx, DOT_TICKS call delay call sound_off mov ax, 4C00h int 21h
END start
Як працює: sound_on програмує PIT і вмикає динамік, delay чекає задану кількість тактів через INT 1Ah, sound_off вимикає динамік.
Забули вимкнути динамік перед виходом. Без виклику sound_off динамік гудітиме до перезавантаження.
Неправильний порядок байтів дільника. Спочатку передається молодший байт у порт 42h, потім старший. Якщо переплутати, частота буде хибною.
Зміна інших бітів порту 61h. Обов'язково зчитуйте поточне значення через IN перед OR/AND, щоб не зіпсувати інші біти. <br><a class="internal-link" data-href="timer-delays.md" href="projects/how-to/timer-delays.html" target="_self" rel="noopener nofollow">Системний таймер: INT 1Ah</a>
<br><a class="internal-link" data-href="bitwise-operations.md" href="projects/how-to/bitwise-operations.html" target="_self" rel="noopener nofollow">Побітові операції</a>
<br><a class="internal-link" data-href="loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Цикли в асемблері</a> Детальніше: L07, P07
]]></description><link>projects/how-to/pc-speaker.html</link><guid isPermaLink="false">Projects/How-to/pc-speaker.md</guid><pubDate>Tue, 24 Feb 2026 06:23:25 GMT</pubDate></item><item><title><![CDATA[random-numbers]]></title><description><![CDATA[Як отримати випадкове число в потрібному діапазоні для ігрової логіки -- вибір, перемішування, розподіл.
Потрібно перемішати масив (карти, слова, елементи вікторини) у випадковому порядку.
Потрібно обрати випадкову позицію, напрямок або елемент зі списку.
Потрібно додати непередбачуваність у гру, щоб кожен запуск відрізнявся.
Генератор працює детерміновано: один і той самий seed дає однакову послідовність. Щоб кожен запуск відрізнявся, seed беруть з INT 1Ah (лічильник тактів від півночі):mov ah, 00h
int 1Ah ; CX:DX = тики від півночі
mov [seed], dx ; молодше слово -- достатньо для seed
Ще краще -- зчитати таймер у момент першого натискання клавіші. Час реакції користувача додає ентропії.Формула: seed = (seed * 25173 + 13849) AND FFFFh. Константи 25173 і 13849 -- перевірені значення для 16-бітного LCG з повним періодом 65536. AND FFFFh відбувається автоматично, бо seed -- слово (DW).Після генерації seed -- число 0..65535. Для діапазону 0..N-1 беремо остачу від ділення:xor dx, dx ; обнулити DX (беззнакове ділення!)
div bx ; BX = N; DX = остача = число в [0, N)
для i від N-1 до 1: j = random(i + 1) ; випадковий індекс 0..i обміняти array[i] та array[j]
Кожна перестановка рівноймовірна.Перемішування масиву з 16 елементів (8 пар карт для гри Memory):.MODEL small
.STACK 256
.DATA
seed DW 0 ; поточне значення генератора
cards DB 0,0, 1,1, 2,2, 3,3 ; 8 пар (двічі кожна) DB 4,4, 5,5, 6,6, 7,7
N EQU 16
msg DB 'Shuffled: $'
nl DB 0Dh, 0Ah, '$'
.CODE
start: mov ax, @data mov ds, ax ; --- Seed від таймера --- mov ah, 00h int 1Ah ; CX:DX = тики від півночі mov [seed], dx ; --- Fisher-Yates shuffle --- mov si, N - 1 ; i = 15
shuffle_loop: cmp si, 0 je shuffle_done mov bx, si inc bx ; BX = i + 1 call random_range ; AX = j в [0, i] mov di, ax ; --- Обмін cards[SI] та cards[DI] --- mov al, [cards + si] mov ah, [cards + di] mov [cards + si], ah mov [cards + di], al dec si jmp shuffle_loop
shuffle_done: ; --- Вивести перемішаний масив --- lea dx, msg mov ah, 09h int 21h mov cx, N mov si, 0
print_loop: mov al, [cards + si] add al, '0' ; цифра -&gt; ASCII mov dl, al mov ah, 02h int 21h mov dl, ' ' int 21h inc si loop print_loop lea dx, nl mov ah, 09h int 21h mov ax, 4C00h int 21h ;--------------------------------------------------
; random_range -- випадкове число в [0, BX-1]
; Вхід: BX = верхня межа (не включно)
; Вихід: AX = випадкове число 0..BX-1
; Змінює: AX, DX
;--------------------------------------------------
random_range PROC push cx mov ax, [seed] mov cx, 25173 mul cx ; DX:AX = seed * 25173 add ax, 13849 mov [seed], ax ; зберегти новий seed xor dx, dx ; обнулити DX перед DIV div bx ; DX = остача = результат mov ax, dx pop cx ret
random_range ENDP END start
Кожен запуск дає іншу послідовність, бо seed ініціалізується від поточного часу. Приклад виведення: Shuffled: 3 7 1 5 0 2 6 4 4 6 2 0 5 1 7 3 Сталий seed при кожному запуску. Без INT 1Ah змінна seed завжди починається з 0, і послідовність буде однаковою. Обов'язково ініціалізуйте seed від таймера перед першим викликом random_range. Забули XOR DX, DX перед DIV у random_range. Після MUL у DX лежить старше слово добутку. Якщо далі одразу DIV BX, ділене буде DX:AX (величезне число), що спричинить Division Overflow. Потрібно обнулити DX, бо нас цікавить лише молодше слово seed. Не оновили seed після виклику. Якщо забути MOV [seed], AX, генератор щоразу повертатиме те саме значення, бо seed не змінюється. <a class="internal-link" data-href="timer-delays.md" href="projects/how-to/timer-delays.html" target="_self" rel="noopener nofollow">Системний таймер</a> -- як працює INT 1Ah
<br><a class="internal-link" data-href="loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Цикли</a> -- організація циклу перемішування
<br><a class="internal-link" data-href="arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви</a> -- робота з масивами елементів
<br><a class="internal-link" data-href="arithmetic.md" href="projects/how-to/arithmetic.html" target="_self" rel="noopener nofollow">Арифметичні інструкції</a> -- MUL/DIV для генератора Детальніше: L04, P04 (арифметика); L07, P07 (INT 1Ah)
]]></description><link>projects/how-to/random-numbers.html</link><guid isPermaLink="false">Projects/How-to/random-numbers.md</guid><pubDate>Tue, 24 Feb 2026 06:23:20 GMT</pubDate></item><item><title><![CDATA[bitwise-operations]]></title><description><![CDATA[Як маніпулювати окремими бітами значень для масок, зсувів, швидкої арифметики та роботи з портами вводу/виводу.
Потрібно виділити або змінити окремі біти у байті (наприклад, керувати портами пристроїв).
Потрібно швидко помножити або поділити на степінь двійки.
Потрібно перетворити регістр у нуль, змінити регістр літери або витягти hex-цифру.
Побітові операції працюють з кожним бітом значення окремо:Обмеження 8086: SHL/SHR/ROL/ROR приймають лічильник лише 1 або CL. Щоб зсунути на 4, потрібно: MOV CL, 4 / SHR AX, CL.Таблиця типових рецептів:Вивести 16-бітне значення у двійковому вигляді (рядок з 16 символів '0' та '1'):.MODEL small
.STACK 256 .DATA
value DW 0B5A3h ; тестове значення = 1011010110100011b
buf DB 16 DUP('0'), '$' ; буфер для 16 символів .CODE
start: mov ax, @data mov ds, ax mov ax, [value] ; AX = число для виведення mov cx, 16 ; 16 бітів lea di, buf ; DI = початок буфера bit_loop: shl ax, 1 ; старший біт -&gt; CF mov dl, '0' adc dl, 0 ; DL = '0' + CF (0 або 1) mov [di], dl inc di dec cx jnz bit_loop ; --- Вивести результат --- lea dx, buf mov ah, 09h int 21h mov ax, 4C00h int 21h END start
Як працює: SHL AX, 1 витісняє старший біт у CF. Потім ADC DL, 0 додає значення CF до символу '0', отримуючи '0' або '1'.
Плутанина між SHR і SAR. SHR заповнює старший біт нулем (логічний зсув, для беззнакових). SAR копіює знаковий біт (арифметичний зсув, зберігає знак). SHR 0FF00h, 1 = 7F80h, а SAR 0FF00h, 1 = FF80h.
Зсув на константу &gt; 1 на 8086. Інструкція SHL AX, 4 працює лише на 80186+. На 8086 потрібно: MOV CL, 4 / SHL AX, CL.
AND замість TEST. AND змінює операнд-приймач, TEST — ні. Для перевірки біта без зміни значення завжди використовуйте TEST. <a class="internal-link" data-href="flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Прапорці та умовні переходи</a>
<br><a class="internal-link" data-href="loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Цикли в асемблері</a>
<br><a class="internal-link" data-href="pc-speaker.md" href="projects/how-to/pc-speaker.html" target="_self" rel="noopener nofollow">Звук через PC Speaker</a> Детальніше: L04, P04
]]></description><link>projects/how-to/bitwise-operations.html</link><guid isPermaLink="false">Projects/How-to/bitwise-operations.md</guid><pubDate>Tue, 24 Feb 2026 06:22:13 GMT</pubDate></item><item><title><![CDATA[arithmetic]]></title><description><![CDATA[Як множити та ділити цілі числа на 8086 -- де шукати результат і що при цьому затирається.
Потрібно обчислити відсоток, координату або адресу за формулою (наприклад, рядок * 160 + стовпець * 2).
Потрібно отримати остачу від ділення (наприклад, виділити окрему цифру числа або отримати число в діапазоні).
Працюєте з виразами вигляду (A * B) / C, де проміжний добуток не вміщується в 16 біт. Ключовий момент: MUL r/m16 завжди записує 32-бітний результат у DX:AX. Навіть якщо добуток вміщується в AX, DX буде затертий (у ньому буде 0). Якщо DX містив потрібне значення -- воно пропаде.
Перед DIV r/m16 ділене -- це пара DX:AX. Якщо DX містить сміття, результат буде хибним або виникне переповнення.CWD (Convert Word to Doubleword) копіює знаковий біт AX в усі біти DX. Якщо AX &gt;= 0 -- DX = 0000h, якщо AX &lt; 0 -- DX = FFFFh.MUL дає результат у DX:AX, а DIV бере ділене з DX:AX. Це дозволяє обчислити (A * B) / C без втрати точності:MOV AX, A ; AX = A
MUL B ; DX:AX = A * B (до 32 біт)
DIV C ; AX = (A * B) / C, DX = (A * B) MOD C
Не потрібно нічого робити з DX між MUL і DIV -- він вже містить старше слово добутку.Якщо частка не вміщується в регістр призначення (AL для байтового, AX для слова), процесор генерує INT 00h (Division Overflow) і програма аварійно завершується. Частий випадок: забули обнулити DX перед DIV, і DX:AX виявляється завеликим.Обчислення відсотка: percentage = (correct * 100) / total. Наприклад: 7 з 12 = 58%..MODEL small
.STACK 256 .DATA
correct DW 7
total DW 12
msg DB 'Score: '
tens DB '0' ; десятки відсотка
ones DB '0' ; одиниці відсотка
pct DB '%', 0Dh, 0Ah, '$' .CODE
start: mov ax, @data mov ds, ax ; --- Обчислення: (correct * 100) / total --- mov ax, [correct] ; AX = 7 mov cx, 100 mul cx ; DX:AX = 7 * 100 = 700 div [total] ; AX = 700 / 12 = 58, DX = 700 MOD 12 = 4 ; --- Перетворення AX (0-100) на дві ASCII-цифри --- xor dx, dx ; обнулити DX перед наступним DIV! mov cx, 10 div cx ; AX = 5 (десятки), DX = 8 (одиниці) add al, '0' mov [tens], al add dl, '0' mov [ones], dl ; --- Вивести результат --- lea dx, msg mov ah, 09h int 21h ; --- Завершення --- mov ax, 4C00h int 21h END start
Результат: Score: 58% Не обнулили DX перед беззнаковим DIV. Ділене для DIV r/m16 -- це DX:AX. Якщо перед цим не було MUL (який сам заповнює DX), потрібно явно XOR DX, DX. Інакше -- або хибний результат, або Division Overflow. Забули CWD перед знаковим IDIV. Якщо AX = -5 (FFFBh), а DX = 0, то DX:AX = 0000FFFBh = +64507, а не -5. Правильно: CWD перетворить DX:AX на FFFFFFFB = -5. Не врахували, що MUL затирає DX. MUL BX записує старше слово добутку в DX, навіть якщо результат вміщується в AX. Якщо DX містив потрібне значення (наприклад, раніше збережену остачу) -- воно буде втрачене. <a class="internal-link" data-href="number-display.md" href="projects/how-to/number-display.html" target="_self" rel="noopener nofollow">Відображення чисел</a> -- як вивести результат обчислень на екран
<br><a class="internal-link" data-href="flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Прапорці та умовні переходи</a> -- перевірка результатів після обчислень
<br><a class="internal-link" data-href="procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури</a> -- як оформити обчислення у процедуру Детальніше: L04, P04
]]></description><link>projects/how-to/arithmetic.html</link><guid isPermaLink="false">Projects/How-to/arithmetic.md</guid><pubDate>Tue, 24 Feb 2026 06:21:49 GMT</pubDate></item><item><title><![CDATA[procedures]]></title><description><![CDATA[Як організувати код у процедури з параметрами на стеку, локальними змінними та коректним поверненням.
Ви хочете винести повторюваний код (малювання дошки, перевірка перемоги) в окрему процедуру
Потрібно передати параметри (рядок, стовпець) через стек, а не через регістри
Хочете зрозуміти, чому SP/BP змінюються після CALL і як правильно до них звертатися
my_proc PROC ; тіло процедури RET
my_proc ENDP
TASM використовує PROC / ENDP для оголошення процедур. Це лише мітки для асемблера -- інструкції CALL і RET працюють з адресами на стеку.CALL my_proc --&gt; PUSH IP (адресу повернення) JMP my_proc RET --&gt; POP IP (повернутися за збереженою адресою)
Для простих процедур (1-2 параметри) -- передавайте через регістри. Для складніших -- через стек.Стандартна послідовність на початку процедури:my_proc PROC PUSH BP ; зберегти старий BP MOV BP, SP ; BP = поточна вершина стека ; Тепер параметри доступні через [BP+...]: ; [BP+0] = збережений BP ; [BP+2] = адреса повернення (від CALL) ; [BP+4] = перший параметр (останній PUSH перед CALL) ; [BP+6] = другий параметр SUB SP, 4 ; виділити 4 байти для локальних змінних ; [BP-2] = перша локальна змінна ; [BP-4] = друга локальна змінна ; ... тіло процедури ... MOV SP, BP ; звільнити локальні змінні POP BP ; відновити старий BP RET 4 ; повернутися + зняти 4 байти параметрів зі стека
my_proc ENDP Високі адреси ┌──────────────────────┐ │ другий параметр │ [BP+6] ├──────────────────────┤ │ перший параметр │ [BP+4] ├──────────────────────┤ │ адреса повернення │ [BP+2] &lt;-- CALL зберіг IP сюди ├──────────────────────┤ │ збережений BP │ [BP+0] &lt;-- BP вказує сюди ├──────────────────────┤ │ локальна змінна 1 │ [BP-2] ├──────────────────────┤ │ локальна змінна 2 │ [BP-4] &lt;-- SP вказує сюди └──────────────────────┘ Низькі адреси
Якщо процедура змінює регістри, які використовує код, що її викликав, -- збережіть їх:PUSH AX ; зберегти перед використанням
PUSH BX
; ... працюємо з AX, BX ...
POP BX ; відновити у зворотному порядку!
POP AX
Процедура calc_vram_offset приймає рядок і стовпець через стек та повертає зміщення у відеопам'яті в DI..MODEL small
.STACK 256 .DATA msg DB 'OK', 0 .CODE
start: MOV AX, @data MOV DS, AX MOV AX, 0B800h MOV ES, AX ; ES -&gt; відеопам'ять ; --- Викликаємо процедуру: рядок=10, стовпець=35 --- MOV AX, 10 PUSH AX ; параметр 2: рядок MOV AX, 35 PUSH AX ; параметр 1: стовпець CALL calc_vram_offset ; результат у DI ; --- Пишемо "OK" у обчислену позицію --- MOV AL, 'O' MOV AH, 0Eh ; жовтий на чорному MOV ES:[DI], AX MOV AL, 'K' MOV ES:[DI+2], AX MOV AX, 4C00h INT 21h ;-------------------------------------------------
; calc_vram_offset -- обчислює зміщення у відеопам'яті
; Параметри (на стеку):
; [BP+4] = стовпець (0-79)
; [BP+6] = рядок (0-24)
; Результат:
; DI = зміщення в сегменті B800h
;-------------------------------------------------
calc_vram_offset PROC PUSH BP MOV BP, SP PUSH AX ; зберігаємо AX (процедура його змінює) ; DI = рядок * 160 MOV AX, [BP+6] ; AX = рядок MOV DI, 160 MUL DI ; AX = рядок * 160 (DX:AX, але рядок &lt; 25) MOV DI, AX ; DI = рядок * 160 ; DI += стовпець * 2 MOV AX, [BP+4] ; AX = стовпець SHL AX, 1 ; AX = стовпець * 2 ADD DI, AX ; DI = рядок*160 + стовпець*2 POP AX ; відновлюємо AX POP BP RET 4 ; повертаємося + знімаємо 4 байти параметрів
calc_vram_offset ENDP END start Забули POP BP перед RET. Без POP BP інструкція RET зніме зі стека збережений BP замість адреси повернення -- програма "полетить" на випадкову адресу. Неправильні зміщення параметрів. Часта помилка -- забути, що на стеку лежать BP та адреса повернення. Перший параметр починається з [BP+4], а не з [BP+2] чи [BP+0]. Непарні PUSH/POP в циклі. Якщо процедура викликається в циклі і PUSH/POP не збалансовані, стек "з'їдається" з кожною ітерацією. Перевіряйте значення SP у Turbo Debugger до і після виклику -- воно має бути однаковим. <a class="internal-link" data-href="video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">video-memory.md</a> -- запис у відеопам'ять, де ця процедура стає в пригоді
<br><a class="internal-link" data-href="flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">flags-and-jumps.md</a> -- умовні переходи для перевірок у процедурах
<br><a class="internal-link" data-href="include-files.md" href="projects/how-to/include-files.html" target="_self" rel="noopener nofollow">include-files.md</a> -- як винести процедури в окремі файли
Детальніше: L05, P05]]></description><link>projects/how-to/procedures.html</link><guid isPermaLink="false">Projects/How-to/procedures.md</guid><pubDate>Tue, 24 Feb 2026 06:16:12 GMT</pubDate></item><item><title><![CDATA[include-files]]></title><description><![CDATA[Як розділити програму на кілька .asm файлів і зібрати їх в один виконуваний файл за допомогою директиви INCLUDE.
Програма стає завеликою для одного файлу (300+ рядків) і ви хочете розділити логіку на модулі
Працюєте в парі: кожен студент пише свій файл, головний файл їх об'єднує
Хочете перевикористати процедури (наприклад, calc_vram_offset) у кількох проєктах
TASM підставляє вміст файлу прямо в місце, де стоїть INCLUDE — ніби ви скопіювали текст вручну. Це відбувається на етапі асемблювання, до генерації машинного коду.main.asm Після INCLUDE
┌──────────┐ ┌──────────────────┐
│ .CODE │ │ .CODE │
│ start: │ │ start: │
│ ... │ -&gt; │ ... │
│ INCLUDE │ │ ;--- display.asm │
│ display │ │ draw_board PROC │
│ │ │ ... │
│ END start│ │ draw_board ENDP │
└──────────┘ │ END start │ └──────────────────┘
Типова організація для проєкту з 2-3 файлів:src/
├── main.asm ; точка входу, ігровий цикл, дані
├── display.asm ; процедури відображення
└── game.asm ; ігрова логіка, ШІ
Файли, що підключаються, містять тільки процедури — без .MODEL, .STACK, .DATA, .CODE, END. Ці директиви мають бути лише в головному файлі.Розміщуйте INCLUDE після вашого основного коду, але перед END start:.CODE
start: ; --- основний код --- MOV AX, 4C00h INT 21h INCLUDE display.asm ; процедури відображення
INCLUDE game.asm ; ігрова логіка END start
Порядок між INCLUDE не має значення, якщо процедури не викликають одна одну до свого оголошення (TASM робить два проходи, тому мітки вирішуються автоматично).main.asm — головний файл:.MODEL small
.STACK 256 .DATA board DB 9 DUP(0) ; стан дошки msg DB 'Game Over$' .CODE
start: MOV AX, @data MOV DS, AX MOV AX, 0B800h MOV ES, AX ; ES -&gt; відеопам'ять ; --- ігровий цикл --- CALL draw_board ; з display.asm CALL check_win ; з game.asm MOV AX, 4C00h INT 21h INCLUDE display.asm
INCLUDE game.asm END start
display.asm — модуль відображення:;=========================================
; display.asm -- процедури відображення
;========================================= draw_board PROC PUSH DI PUSH AX XOR DI, DI ; початок відеопам'яті MOV AL, '*' MOV AH, 0Fh ; білий на чорному MOV ES:[DI], AX ; записати символ POP AX POP DI RET
draw_board ENDP
game.asm — модуль логіки:;=========================================
; game.asm -- ігрова логіка
;========================================= check_win PROC PUSH SI PUSH CX LEA SI, board ; адреса масиву дошки MOV CX, 9 ; 9 клітинок
@@check_loop: CMP byte ptr [SI], 0 JNE @@not_empty ; ... логіка перевірки ...
@@not_empty: INC SI LOOP @@check_loop POP CX POP SI RET
check_win ENDP
tasm main.asm
tlink main.obj
Компілюється тільки main.asm — файли display.asm та game.asm підтягуються автоматично через INCLUDE. Дублювання .MODEL / .DATA / .CODE у підключеному файлі. Якщо display.asm містить .MODEL small, TASM видасть помилку або створить некоректний код. Ці директиви мають бути тільки в main.asm. INCLUDE після END start. Директива END повідомляє TASM, що файл закінчився. Все після END ігнорується, включно з INCLUDE. Розміщуйте INCLUDE перед END start. Підключений файл використовує змінні, яких немає. Процедура в game.asm звертається до board, але board оголошений у .DATA в main.asm. Це працює, бо INCLUDE підставляє текст у той самий файл — але переконайтеся, що імена змінних збігаються точно. <a class="internal-link" data-href="procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури</a> — як оформити код у PROC/ENDP перед винесенням у окремий файл
<br><a class="internal-link" data-href="segment-registers.md" href="projects/how-to/segment-registers.html" target="_self" rel="noopener nofollow">Сегментні регістри</a> — налаштування DS та ES в головному файлі
Детальніше: L05, P05]]></description><link>projects/how-to/include-files.html</link><guid isPermaLink="false">Projects/How-to/include-files.md</guid><pubDate>Tue, 24 Feb 2026 06:15:38 GMT</pubDate></item><item><title><![CDATA[keyboard-input]]></title><description><![CDATA[Як зчитувати натискання клавіш, розрізняти ASCII-код і скан-код та перевіряти коректність вводу.
Гравець має натиснути клавішу (1-9 для вибору клітинки, Y/N для підтвердження)
Потрібно перевірити, чи натиснута клавіша, без зупинки програми (неблокуючий ввід)
Хочете конвертувати натиснуту цифру в індекс масиву (наприклад, '3' -&gt; 2)
Важливо: після AH=01h клавіша залишається в буфері. Щоб її забрати, потрібно викликати AH=00h.Після INT 16h (AH=00h):
AL = ASCII-код символу (для друкованих клавіш)
AH = скан-код клавіші (апаратний код, унікальний для кожної фізичної клавіші)
Для спеціальних клавіш (стрілки, F1-F12) AL = 0, і розрізняти їх можна лише за скан-кодом в AH.Клавіші '1'-'9' мають ASCII-коди 31h-39h. Щоб отримати індекс 0-8:ASCII '1' = 31h --&gt; 31h - 31h = 0
ASCII '5' = 35h --&gt; 35h - 31h = 4
ASCII '9' = 39h --&gt; 39h - 31h = 8
Формула: індекс = AL - 31hПрограма чекає, доки гравець натисне клавішу 1-9, перевіряє коректність вводу та конвертує в індекс масиву дошки..MODEL small
.STACK 256 .DATA board DB 9 DUP(0) ; ігрове поле: 0=порожньо, 1=X, 2=O prompt DB 'Press 1-9: $' errmsg DB 'Invalid!$' okmsg DB 'Cell: $' .CODE
start: MOV AX, @data MOV DS, AX get_input: ; --- Виводимо підказку --- LEA DX, prompt MOV AH, 09h INT 21h ; --- Чекаємо натискання клавіші --- MOV AH, 00h ; функція: чекати клавішу INT 16h ; AL = ASCII, AH = скан-код ; --- Перевіряємо діапазон '1'-'9' --- CMP AL, '1' ; менше за '1'? JB invalid CMP AL, '9' ; більше за '9'? JA invalid ; --- Конвертуємо ASCII -&gt; індекс 0..8 --- SUB AL, '1' ; AL = індекс (0-8) XOR AH, AH ; AX = індекс (очищуємо AH) MOV SI, AX ; SI = індекс для адресації масиву ; --- Перевіряємо, чи клітинка вільна --- CMP byte ptr [board+SI], 0 JNE invalid ; клітинка зайнята -- повторити ; --- Клітинка вільна: записуємо хід гравця --- MOV byte ptr [board+SI], 1 ; 1 = X ; --- Повідомлення про успіх --- LEA DX, okmsg MOV AH, 09h INT 21h ; Виводимо номер клітинки (1-9) MOV DX, SI ADD DL, '1' ; перетворюємо індекс назад в ASCII MOV AH, 02h ; функція DOS: вивести символ INT 21h JMP done invalid: LEA DX, errmsg MOV AH, 09h INT 21h JMP get_input ; запитуємо знову done: MOV AX, 4C00h INT 21h END start
Якщо потрібно перевіряти клавішу без зупинки гри (наприклад, для анімації):game_loop: ; --- Оновлюємо екран, анімацію тощо --- CALL update_screen ; --- Перевіряємо, чи є клавіша в буфері --- MOV AH, 01h ; неблокуюча перевірка INT 16h JZ game_loop ; ZF=1 -&gt; немає клавіші, продовжуємо цикл ; --- Клавіша є! Забираємо її з буфера --- MOV AH, 00h INT 16h ; тепер AL = ASCII, AH = скан-код ; --- Обробляємо натискання --- CMP AL, 1Bh ; Escape? JE exit_game ; ... інша обробка ... JMP game_loop Не очищений буфер клавіатури. Якщо перед INT 16h (AH=00h) в буфері залишилися попередні натискання, програма прочитає стару клавішу. Для очищення: викликайте AH=01h / INT 16h у циклі, доки ZF не стане 1, кожен раз забираючи клавішу через AH=00h. Плутанина між AL і AH. Після INT 16h ASCII-код -- в AL, скан-код -- в AH. Часта помилка -- порівнювати AH з ASCII-значенням. Крім того, INT 16h затирає попереднє значення AH, тому не розраховуйте на нього після виклику. Ігнорування великого/малого регістру. Якщо перевіряєте 'Y'/'N', не забудьте перевірити і 'y'/'n'. Швидкий спосіб -- перед порівнянням зробити OR AL, 20h, що перетворює будь-яку латинську літеру в малу. <a class="internal-link" data-href="dos-services.md" href="projects/how-to/dos-services.html" target="_self" rel="noopener nofollow">dos-services.md</a> -- сервіси DOS через INT 21h для виведення тексту
<br><a class="internal-link" data-href="flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">flags-and-jumps.md</a> -- умовні переходи CMP/JE/JB для валідації вводу
Детальніше: L07, P07]]></description><link>projects/how-to/keyboard-input.html</link><guid isPermaLink="false">Projects/How-to/keyboard-input.md</guid><pubDate>Tue, 24 Feb 2026 06:10:08 GMT</pubDate></item><item><title><![CDATA[loops]]></title><description><![CDATA[Як організувати повторення дій за допомогою LOOP, ручних циклів CMP+JNE та обходу масивів.
Потрібно обробити кожен елемент масиву (наприклад, 9 клітинок ігрового поля).
Потрібно повторити дію задану кількість разів.
Потрібно перебрати lookup-таблицю виграшних комбінацій.
LOOP мітка -- зменшує CX на 1, переходить на мітку, якщо CX != 0. CX треба встановити заздалегідь.Ручний цикл (CMP + JNE) -- гнучкіший: будь-який регістр як лічильник, будь-яка умова виходу.; LOOP ; Ручний цикл MOV CX, 5 MOV SI, 0
repeat: next: ; ... тіло ... ; ... тіло ... LOOP repeat INC SI CMP SI, 9 JNE next
Вкладені цикли: LOOP завжди використовує CX, тому зовнішній лічильник зберігають на стеку: MOV CX, 3 ; зовнішній цикл
outer: PUSH CX ; зберегти MOV CX, 3 ; внутрішній цикл
inner: ; ... тіло ... LOOP inner POP CX ; відновити LOOP outer
Обхід масиву: регістр SI як індекс, адреса елемента -- [масив + SI].Підрахунок зайнятих клітинок на ігровому полі (ручний цикл з SI):.MODEL small
.STACK 256
.DATA
board DB 'X', 0, 'O', 'X', 0, 0, 0, 'X', 'O'
result DB 'Occupied: ?$' ; ? замінимо на цифру
.CODE
start: mov ax, @data mov ds, ax mov si, 0 ; індекс у масиві mov cl, 0 ; лічильник зайнятих mov ch, 9 ; кількість клітинок
count_loop: cmp byte ptr [board+si], 0 je skip ; порожня -- пропустити inc cl ; зайнята -- +1
skip: inc si dec ch jnz count_loop ; якщо ch != 0 -- продовжити add cl, '0' ; число -&gt; ASCII mov [result+10], cl mov ah, 09h lea dx, result int 21h mov ax, 4C00h int 21h
END start
Перебір таблиці виграшних ліній (LOOP + PUSH/POP для збереження CX):.MODEL small
.STACK 256
.DATA
board DB 'X','X','X', 0,0,0, 0,0,0
win_lines DB 0,1,2, 3,4,5, 6,7,8 ; рядки DB 0,3,6, 1,4,7, 2,5,8 ; стовпці DB 0,4,8, 2,4,6 ; діагоналі
win_msg DB 'Winner!$'
no_msg DB 'No winner.$'
.CODE
start: mov ax, @data mov ds, ax mov si, 0 ; зміщення в win_lines mov cx, 8 ; 8 комбінацій
check_line: push cx mov bl, [win_lines+si] ; 1-й індекс mov bh, 0 mov al, [board+bx] cmp al, 0 je next_line ; порожня -- пропустити mov bl, [win_lines+si+1] ; 2-й індекс cmp al, [board+bx] jne next_line mov bl, [win_lines+si+2] ; 3-й індекс cmp al, [board+bx] jne next_line pop cx ; виграш знайдено! mov ah, 09h lea dx, win_msg int 21h jmp quit
next_line: add si, 3 pop cx loop check_line mov ah, 09h lea dx, no_msg int 21h
quit: mov ax, 4C00h int 21h
END start Зміна CX всередині LOOP. Якщо тіло циклу використовує CX, лічильник буде зіпсовано. Зберігайте через PUSH/POP.
CX = 0 перед LOOP. LOOP спочатку робить DEC CX: 0 стане FFFFh (65535) -- цикл виконається 65536 разів. Перевіряйте JCXZ skip перед циклом.
Забули зберегти CX у вкладеному циклі. Без PUSH/POP внутрішній цикл затре зовнішній лічильник. <a class="internal-link" data-href="flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Прапорці та умовні переходи</a>
<br><a class="internal-link" data-href="arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви в асемблері</a>
<br><a class="internal-link" data-href="lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Lookup-таблиці</a> Детальніше: L04, P04
]]></description><link>projects/how-to/loops.html</link><guid isPermaLink="false">Projects/How-to/loops.md</guid><pubDate>Tue, 24 Feb 2026 06:09:51 GMT</pubDate></item><item><title><![CDATA[lookup-tables]]></title><description><![CDATA[Як використовувати попередньо обчислені масиви даних для швидкого доступу замість складних ланцюгів умов.
Потрібно перевірити кілька комбінацій (наприклад, виграшні лінії у хрестиках-нуликах) без великого CMP/JE ланцюга.
Потрібно швидко перетворити одне значення на інше (індекс → колір, стан → повідомлення).
Дані фіксовані й відомі заздалегідь — їх можна зберегти як константний масив.
Таблиця пошуку (lookup table) — це масив, оголошений у сегменті .DATA, який містить попередньо підготовлені значення. Замість обчислення або розгалуження програма просто зчитує потрібний елемент за індексом.Оголошення таблиці:; Байтова таблиця (DB) — кожен елемент займає 1 байт
colors DB 1Fh, 2Fh, 4Fh, 0Eh ; 4 атрибути кольорів ; Слівна таблиця (DW) — кожен елемент займає 2 байти
offsets DW 0, 160, 320, 480 ; зміщення рядків екрана
Доступ до елемента за індексом:Для ітерації по таблиці використовуйте SI як вказівник і збільшуйте його на розмір елемента після кожного кроку.Класичний приклад — виграшні лінії хрестиків-нуликів:Ігрове поле (9 клітинок, індекси 0-8): 0 | 1 | 2
---+---+--- 3 | 4 | 5
---+---+--- 6 | 7 | 8
8 виграшних ліній можна зберегти як таблицю з трійок індексів:win_lines DB 0,1,2, 3,4,5, 6,7,8 ; рядки DB 0,3,6, 1,4,7, 2,5,8 ; стовпці DB 0,4,8, 2,4,6 ; діагоналі
NUM_LINES EQU 8
Щоб перевірити перемогу, ітеруємо по таблиці, зчитуючи 3 індекси на кожному кроці, і порівнюємо відповідні клітинки поля.Перевірка виграшу у хрестиках-нуликах. Поле зберігається як масив із 9 байтів ('X', 'O' або ' '):.MODEL small
.STACK 256 .DATA
; --- Ігрове поле (тестовий стан: X виграє по діагоналі) ---
board DB 'X', 'O', ' ' DB ' ', 'X', 'O' DB ' ', ' ', 'X' ; --- Таблиця виграшних ліній (8 трійок індексів) ---
win_lines DB 0,1,2, 3,4,5, 6,7,8 ; рядки DB 0,3,6, 1,4,7, 2,5,8 ; стовпці DB 0,4,8, 2,4,6 ; діагоналі
NUM_LINES EQU 8 msg_win DB 'X wins!', 0Dh, 0Ah, '$'
msg_no DB 'No winner yet.', 0Dh, 0Ah, '$' ; --- Таблиця кольорів для різних станів клітинки ---
cell_colors DB 1Fh ; ' ' — біле на синьому DB 4Fh ; 'X' — біле на червоному DB 2Fh ; 'O' — біле на зеленому .CODE
start: mov ax, @data mov ds, ax ; --- Перевірити всі виграшні лінії --- lea si, win_lines ; SI → початок таблиці mov cx, NUM_LINES ; лічильник ліній check_line: push cx ; зберегти лічильник ; --- Завантажити 3 індекси поточної лінії --- xor bh, bh mov bl, [si] ; BL = перший індекс mov al, board[bx] ; AL = значення першої клітинки mov bl, [si+1] ; BL = другий індекс mov ah, board[bx] ; AH = значення другої клітинки mov bl, [si+2] ; BL = третій індекс mov dl, board[bx] ; DL = значення третьої клітинки ; --- Перевірити: всі три однакові і не пробіл? --- cmp al, ' ' je next_line ; пуста клітинка — не перемога cmp al, ah jne next_line ; перша != друга cmp ah, dl jne next_line ; друга != третя ; --- Знайдено перемогу! --- pop cx lea dx, msg_win mov ah, 09h int 21h jmp done next_line: add si, 3 ; перейти до наступної трійки pop cx loop check_line ; --- Жодна лінія не виграшна --- lea dx, msg_no mov ah, 09h int 21h done: mov ax, 4C00h int 21h END start
У цьому прикладі 'X' стоїть на позиціях 0, 4, 8 (головна діагональ), тому програма виведе "X wins!".
Хибний розмір кроку. Якщо таблиця оголошена через DW, а SI збільшується на 1 замість 2, зчитуються хибні дані. Для DB крок = 1, для DW крок = 2.
Помилки в даних таблиці. Друкарська помилка в індексі (наприклад, 0,4,7 замість 0,4,8 для діагоналі) призводить до хибної логіки, яку важко знайти при налагодженні.
Не обнулили BH перед MOV BL, [SI]. Якщо BH містить сміття, board[BX] зчитає дані з хибної адреси. Завжди робіть XOR BH, BH перед використанням BL як індексу. <a class="internal-link" data-href="arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">Масиви</a>
<br><a class="internal-link" data-href="loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Цикли</a>
<br><a class="internal-link" data-href="flags-and-jumps.md" href="projects/how-to/flags-and-jumps.html" target="_self" rel="noopener nofollow">Прапорці та умовні переходи</a> Детальніше: L06, P06
]]></description><link>projects/how-to/lookup-tables.html</link><guid isPermaLink="false">Projects/How-to/lookup-tables.md</guid><pubDate>Tue, 24 Feb 2026 06:09:34 GMT</pubDate></item><item><title><![CDATA[number-display]]></title><description><![CDATA[Як перетворити двійкове число з регістра на десяткові ASCII-цифри для виведення на екран.
Потрібно показати рахунок, координати або інше числове значення на екрані.
Працюєте з відеопам'яттю і хочете записати цифри у конкретну позицію.
Функції DOS (INT 21h) виводять лише символи, а не числа — потрібна конвертація.
Число в регістрі (наприклад, 42 = 002Ah) — це двійкове значення. Щоб вивести його як текст "42", потрібно отримати окремі десяткові цифри та перетворити кожну на ASCII-код.Алгоритм (ділення на 10):
Ділимо число на 10.
Остача — це молодша цифра. Додаємо 30h → ASCII-код цифри.
Частка — це решта числа. Повторюємо, поки частка &gt; 0.
Цифри виходять у зворотному порядку (від молодшої до старшої).
Приклад для числа 42:Порядок отримання: '2', '4'. Записуємо у буфер справа наліво: '4', '2'.Інструкція DIV:
При діленні слова (16-бітний дільник) обов'язково обнуліть DX перед DIV, інакше у старшому слові буде сміття, і результат буде хибним або станеться переповнення.
Процедура для відображення рахунку (0-99) у відеопам'яті. Число в AX, позиція в ES:DI, колір в BL:.MODEL small
.STACK 256 .DATA
score DW 37 ; тестове значення рахунку .CODE
start: mov ax, @data mov ds, ax ; --- Налаштувати сегмент відеопам'яті --- mov ax, 0B800h mov es, ax ; --- Вивести рахунок у рядку 0, стовпці 38 --- mov di, 76 ; зміщення = 0 * 160 + 38 * 2 = 76 mov ax, [score] ; AX = число для відображення mov bl, 1Eh ; атрибут: жовтий на синьому call show_number ; --- Очікування клавіші --- mov ah, 00h int 16h ; --- Завершення --- mov ax, 4C00h int 21h ;--------------------------------------------
; show_number: вивести число 0-99 у відеопам'ять
; Вхід: AX = число (0-99)
; ES:DI = адреса у відеопам'яті
; BL = атрибут кольору
; Змінює: AX, DX
;--------------------------------------------
show_number PROC xor dx, dx ; обнулити DX перед діленням mov cx, 10 div cx ; AX = десятки, DX = одиниці ; --- Записати десятки --- add al, 30h ; частка → ASCII mov es:[di], al ; символ десятків mov es:[di+1], bl ; атрибут ; --- Записати одиниці --- add dl, 30h ; остача → ASCII mov es:[di+2], dl ; символ одиниць mov es:[di+3], bl ; атрибут ret
show_number ENDP END start
Якщо score = 37, на екрані з'явиться 37 жовтим кольором на синьому фоні. Якщо score = 5, з'явиться 05 (з ведучим нулем — зручно для табло).
Не обнулили DX перед DIV. При діленні 16-бітним дільником ділене — це DX:AX. Якщо DX містить сміття, результат буде хибним або виникне виняток переповнення (Division Overflow).
Зворотний порядок цифр. Алгоритм видає цифри від молодшої до старшої. Якщо записувати їх підряд зліва направо, число 42 стане "24". Записуйте справа наліво або спочатку зберігайте на стек.
Забули обробити нуль. Якщо число = 0, цикл ділення одразу завершиться (частка = 0). Переконайтеся, що хоча б одна цифра '0' буде виведена. <a class="internal-link" data-href="video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Відеопам'ять: прямий запис у B800h</a>
<br><a class="internal-link" data-href="procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури</a>
<br><a class="internal-link" data-href="dos-services.md" href="projects/how-to/dos-services.html" target="_self" rel="noopener nofollow">Сервіси DOS</a> Детальніше: L08, P08
]]></description><link>projects/how-to/number-display.html</link><guid isPermaLink="false">Projects/How-to/number-display.md</guid><pubDate>Tue, 24 Feb 2026 06:08:53 GMT</pubDate></item><item><title><![CDATA[flags-and-jumps]]></title><description><![CDATA[Як використовувати прапорці процесора, інструкції CMP/TEST і умовні переходи для розгалуження програми.
Потрібно порівняти два значення і виконати різний код залежно від результату.
Потрібно перевірити окремі біти значення (наприклад, чи натиснута клавіша, чи зайнята клітинка).
Реалізуєте логіку if/else або перевірку умови виграшу у грі.
Процесор має регістр FLAGS, окремі біти якого встановлюються після арифметичних і логічних операцій:CMP dst, src -- виконує віднімання dst - src, встановлює прапорці, але не зберігає результат. Використовується перед умовними переходами.TEST dst, src -- виконує побітове AND dst AND src, встановлює прапорці (ZF, SF), але не зберігає результат. Зручна для перевірки окремих бітів.Таблиця умовних переходів:Типовий шаблон: CMP + умовний перехід:CMP AX, BX ; порівняти AX і BX
JE equal ; якщо AX == BX, перейти до equal
; ... код для AX != BX ...
equal:
; ... код для AX == BX ...
Перевірка клітинки ігрового поля (чи зайнята) та визначення переможця:.MODEL small
.STACK 256 .DATA
board DB 'X', 0, 'O', 'X', 0, 0, 0, 'X', 'O' ; ігрове поле 3x3
win_msg DB 'X wins!$'
no_msg DB 'No win.$' .CODE
start: mov ax, @data mov ds, ax ; --- Перевірка: чи клітинка [1] порожня? --- mov si, 1 cmp byte ptr [board+si], 0 jne occupied ; якщо не нуль -- клітинка зайнята ; ... тут код для порожньої клітинки ... occupied: ; --- Перевірка діагоналі: board[0], board[4], board[8] --- mov al, [board+0] ; перша клітинка діагоналі cmp al, 0 je no_win ; порожня -- не виграш cmp al, [board+4] ; порівняти з центром jne no_win ; не збігається -- не виграш cmp al, [board+8] ; порівняти з правим нижнім кутом jne no_win ; не збігається -- не виграш ; --- Усі три збігаються: виграш! --- mov ah, 09h lea dx, win_msg int 21h jmp quit no_win: mov ah, 09h lea dx, no_msg int 21h quit: mov ax, 4C00h int 21h END start Знакові переходи для беззнакових даних. JG/JL враховують SF і OF (для чисел зі знаком). Для байтових значень 0-255 (наприклад, ASCII-кодів) використовуйте JA/JB.
Подвійне CMP без збереження прапорців. CMP перезаписує прапорці. Не можна зробити два CMP поспіль і очікувати, що прапорці від першого збережуться. Зберігайте проміжний результат або перевіряйте прапорці одразу після CMP.
Плутанина між JA (беззнакове "вище") і JG (знакове "більше"). Для порівняння 0FFh і 01h: JA вважає FFh &gt; 01h (255 &gt; 1), а JG вважає FFh &lt; 01h (-1 &lt; 1). <a class="internal-link" data-href="loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Цикли в асемблері</a>
<br><a class="internal-link" data-href="procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">Процедури та стек</a> Детальніше: L04, P04
]]></description><link>projects/how-to/flags-and-jumps.html</link><guid isPermaLink="false">Projects/How-to/flags-and-jumps.md</guid><pubDate>Tue, 24 Feb 2026 06:08:39 GMT</pubDate></item><item><title><![CDATA[arrays]]></title><description><![CDATA[Як оголошувати масиви в .DATA, заповнювати їх оператором DUP та звертатися до елементів за індексом.
Потрібно зберігати набір однотипних значень (ігрове поле, таблицю, буфер).
Потрібно читати або змінювати елемент за номером (індексом).
Працюєте з рядками як масивами байтів.
У сегменті .DATA масиви створюються директивами DB (байти) та DW (слова):; Масив байтів із початковими значеннями
scores DB 10, 20, 30, 40, 50 ; Масив слів із початковими значеннями
values DW 1000, 2000, 3000 ; Масив із 9 нульових байтів (наприклад, ігрове поле 3x3)
board DB 9 DUP(0) ; Масив із 100 слів, заповнених FFFFh
buffer DW 100 DUP(0FFFFh) ; Масив без ініціалізації (знак питання)
temp DB 64 DUP(?)
Формула зміщення від початку масиву:для DB: зміщення = індекс
для DW: зміщення = індекс * 2
Пряма адресація (індекс відомий на етапі компіляції):mov al, scores[2] ; третій елемент (= 30)
mov ax, values[4] ; третій елемент (= 3000, зміщення 2*2=4)
Регістрова адресація (індекс обчислюється під час виконання):lea bx, board ; BX = адреса початку масиву
mov si, 4 ; індекс елемента (для DB)
mov al, [bx + si] ; читання board[4]
mov [bx + si], dl ; запис у board[4]
Для масивів DW потрібно подвоїти індекс:lea bx, values
mov si, 2 ; логічний індекс елемента
shl si, 1 ; SI = SI * 2 (розмір слова)
mov ax, [bx + si] ; читання values[2]
Ігрове поле 3x3 (9 байтів). Запис значень у клітинки та виведення вмісту однієї клітинки:.MODEL small
.STACK 256 .DATA
board DB 9 DUP(0) ; поле 3x3, заповнене нулями ; індекси: 0 1 2 ; 3 4 5 ; 6 7 8
msgX DB 'Cell = X', 13, 10, '$'
msgO DB 'Cell = O', 13, 10, '$'
msgE DB 'Cell = .', 13, 10, '$' MARK_X EQU 1
MARK_O EQU 2 .CODE
start: mov ax, @data mov ds, ax ; --- Запис X у центр поля (індекс 4) --- lea bx, board mov si, 4 mov byte ptr [bx + si], MARK_X ; --- Запис O у верхній правий кут (індекс 2) --- mov si, 2 mov byte ptr [bx + si], MARK_O ; --- Читання клітинки 4 та виведення результату --- mov si, 4 mov al, [bx + si] cmp al, MARK_X je show_x cmp al, MARK_O je show_o ; Клітинка порожня lea dx, msgE jmp print show_x: lea dx, msgX jmp print show_o: lea dx, msgO print: mov ah, 09h int 21h ; --- Завершення програми --- mov ax, 4C00h int 21h END start Забули множити індекс на 2 для масивів DW. values[3] — це зміщення 3 байти, тобто середина другого елемента. Правильно: values[6] або SHL SI, 1 перед зверненням.
Помилка на одиницю (off-by-one). Масив із 9 елементів має індекси 0-8. Звернення до індексу 9 — це вихід за межі масиву, що читає або перезаписує сусідні дані.
Використання SI з DW без подвоєння. Якщо SI = номер елемента, для DW-масиву потрібно SHL SI, 1 або ADD SI, SI перед [BX + SI]. <a class="internal-link" data-href="addressing-modes.md" href="projects/how-to/addressing-modes.html" target="_self" rel="noopener nofollow">Режими адресації</a>
<br><a class="internal-link" data-href="lookup-tables.md" href="projects/how-to/lookup-tables.html" target="_self" rel="noopener nofollow">Таблиці підстановки</a> Детальніше: L06, P06
]]></description><link>projects/how-to/arrays.html</link><guid isPermaLink="false">Projects/How-to/arrays.md</guid><pubDate>Tue, 24 Feb 2026 06:08:21 GMT</pubDate></item><item><title><![CDATA[timer-delays]]></title><description><![CDATA[Як створювати паузи у програмі, зчитуючи системний лічильник тактів через INT 1Ah.
Потрібно зробити затримку між подіями (наприклад, пауза між ходами у грі).
Потрібно відміряти час, що не залежить від швидкості процесора.
Порожній цикл затримки (DEC CX / JNZ) працює по-різному на різних машинах, а таймер — однаково.
Системний таймер DOS працює на частоті 18.2 тактів на секунду. Переривання INT 1Ah з AH=00h повертає кількість тактів від півночі:Повне 32-бітне значення: CX:DX (CX &lt;&lt; 16 + DX).Розрахунок затримки:Алгоритм очікування (polling):
Зчитати початковий лічильник (INT 1Ah) → зберегти DX.
У циклі зчитувати поточний лічильник.
Обчислити різницю: поточний DX - початковий DX.
Якщо різниця &gt;= цільова кількість тактів → вийти з циклу. Для коротких затримок (до ~30 хв.) порівняння лише DX працює коректно, бо молодше слово обертається кожні ~3600 с. Для тривалих затримок потрібно порівнювати повне значення CX:DX.
Вивести повідомлення, зачекати ~2 секунди, потім вивести друге повідомлення:.MODEL small
.STACK 256 .DATA
msg1 DB 'Starting timer...', 0Dh, 0Ah, '$'
msg2 DB '2 seconds passed!', 0Dh, 0Ah, '$' DELAY_TICKS EQU 36 ; ~2 секунди (18.2 * 2) .CODE
start: mov ax, @data mov ds, ax ; --- Вивести перше повідомлення --- lea dx, msg1 mov ah, 09h int 21h ; --- Зчитати початковий час --- mov ah, 00h int 1Ah ; CX:DX = лічильник тактів mov bx, dx ; BX = початкове значення DX ; --- Цикл очікування ---
wait_loop: mov ah, 00h int 1Ah ; CX:DX = поточний лічильник sub dx, bx ; DX = різниця тактів cmp dx, DELAY_TICKS jb wait_loop ; якщо менше 36 тактів — чекаємо ; --- Вивести друге повідомлення --- lea dx, msg2 mov ah, 09h int 21h ; --- Завершення програми --- mov ax, 4C00h int 21h END start Порожній цикл замість таймера. MOV CX, 0FFFFh / delay: DEC CX / JNZ delay дає різну затримку залежно від швидкості CPU. Таймер INT 1Ah працює однаково на будь-якій машині.
Зміна DX між зчитуваннями. DX використовується і для лічильника тактів, і для INT 21h, AH=09h. Зберігайте початковий час у регістрі, який не затирається (BX, SI, або змінна в пам'яті).
Ігнорування CX при довгих затримках. Якщо затримка перевищує ~60 с, молодше слово DX може переповнитися. Для таких випадків потрібно порівнювати повне 32-бітне значення CX:DX. <a class="internal-link" data-href="keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Введення з клавіатури</a>
<br><a class="internal-link" data-href="loops.md" href="projects/how-to/loops.html" target="_self" rel="noopener nofollow">Цикли</a> Детальніше: L07, P07
]]></description><link>projects/how-to/timer-delays.html</link><guid isPermaLink="false">Projects/How-to/timer-delays.md</guid><pubDate>Tue, 24 Feb 2026 06:08:19 GMT</pubDate></item><item><title><![CDATA[addressing-modes]]></title><description><![CDATA[Як процесор знаходить дані: від константи в інструкції до складних обчислень адреси в пам'яті.
Ви працюєте з масивами або таблицями і хочете звертатися до елементів за індексом
Потрібно обчислити зміщення у відеопам'яті за рядком та стовпцем
Хочете зрозуміти, чому MOV AX, [BX+SI+2] працює, а MOV AX, [AX] -- ні
В 8086 не будь-який регістр може стояти в квадратних дужках. Дозволені лише такі комбінації: База: BX або BP Індекс: SI або DI [BX] [BP] [SI] [DI] -- один регістр [BX+SI] [BX+DI] [BP+SI] [BP+DI] -- база + індекс Будь-яку комбінацію можна доповнити зміщенням: [BX+SI+10] Увага: [AX], [CX], [DX], [SP] -- НЕ допустимі на 8086! [BX], [SI], [DI] -- використовують сегмент DS
[BP] -- використовує сегмент SS (стек!)
Якщо ви пишете MOV AX, [BP+4], процесор читає з SS:BP+4, а не з DS:BP+4. Це важливо для роботи зі стековими кадрами (див. <a class="internal-link" data-href="procedures.md" href="projects/how-to/procedures.html" target="_self" rel="noopener nofollow">procedures.md</a>).Програма демонструє різні режими адресації: записує символи з масиву у відеопам'ять, обчислюючи позицію через базовий регістр та індекс..MODEL small
.STACK 256 .DATA ; Масив із 5 символів для виведення symbols DB 'H', 'e', 'l', 'l', 'o' count DB 5 attr DB 1Eh ; жовтий на синьому .CODE
start: MOV AX, @data MOV DS, AX ; DS -&gt; сегмент даних MOV AX, 0B800h MOV ES, AX ; ES -&gt; відеопам'ять ; --- Безпосередній: задаємо рядок 12, стовпець 37 --- MOV BX, 12 * 160 ; BX = зміщення рядка (база) MOV DI, 37 * 2 ; DI = зміщення стовпця (індекс) ; --- Прямий: читаємо атрибут із змінної --- MOV AH, [attr] ; AH = байт атрибуту ; --- Непрямий + цикл: проходимо масив symbols --- LEA SI, symbols ; SI -&gt; початок масиву MOV CL, [count] ; CL = кількість символів XOR CH, CH ; CX = кількість (розширюємо до 16 біт) draw_loop: ; --- Непрямий регістровий: читаємо символ --- MOV AL, [SI] ; AL = поточний символ із масиву ; --- База + індекс: пишемо у відеопам'ять --- ; ES:[BX+DI] = рядок_зміщення + стовпець_зміщення MOV ES:[BX+DI], AL ; записуємо символ MOV ES:[BX+DI+1], AH ; записуємо атрибут (база+індекс+зміщення) INC SI ; наступний символ масиву ADD DI, 2 ; наступна позиція на екрані (2 байти) LOOP draw_loop ; --- Завершення програми --- MOV AX, 4C00h INT 21h END start Недопустимий регістр у дужках. MOV AX, [AX] або MOV AX, [CX+DI] -- процесор 8086 не підтримує ці комбінації. Використовуйте лише BX/BP як базу і SI/DI як індекс. Забутий сегмент для BP. Якщо ви пишете MOV AX, [BP+6] і думаєте, що це читає з DS -- це не так. [BP] за замовчуванням працює з SS. Для явного доступу до DS пишіть MOV AX, DS:[BP+6]. Неврахований множник 2 для відеопам'яті. Кожна позиція на екрані займає 2 байти (символ + атрибут). Стовпець C має зміщення C * 2, а не просто C. Рядок R -- зміщення R * 160, а не R * 80. <br><a class="internal-link" data-href="video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">video-memory.md</a> -- запис у відеопам'ять за адресою B800h
<br><a class="internal-link" data-href="arrays.md" href="projects/how-to/arrays.html" target="_self" rel="noopener nofollow">arrays.md</a> -- робота з масивами в асемблері
Детальніше: L06, P06]]></description><link>projects/how-to/addressing-modes.html</link><guid isPermaLink="false">Projects/How-to/addressing-modes.md</guid><pubDate>Tue, 24 Feb 2026 06:08:19 GMT</pubDate></item><item><title><![CDATA[dos-services]]></title><description><![CDATA[Як використовувати переривання INT 21h для введення/виведення, роботи з файлами та завершення програми.
Потрібно вивести текстове повідомлення або прочитати символ із клавіатури.
Потрібно створити, відкрити, прочитати або записати файл засобами DOS.
Потрібно коректно завершити програму з кодом повернення.
INT 21h -- це головний інтерфейс системних викликів DOS. Номер функції передається в регістрі AH, параметри -- в інших регістрах. DOS працює на вищому рівні, ніж BIOS (INT 10h, INT 16h): BIOS керує апаратурою напряму, а DOS надає абстракцію файлів, консолі тощо.Основні функції INT 21h:Порядок роботи з файлами: створити/відкрити (отримати дескриптор у AX) -&gt; читати/писати (передати дескриптор у BX) -&gt; закрити.Вивести повідомлення на екран і завершити програму:.MODEL small
.STACK 256 .DATA
msg DB 'Hello, DOS!$' ; рядок ОБОВ'ЯЗКОВО закінчується '$' .CODE
start: mov ax, @data mov ds, ax ; встановити DS на сегмент даних ; --- Вивести рядок (AH=09h) --- mov ah, 09h lea dx, msg ; DS:DX = адреса рядка int 21h ; --- Завершити програму (AH=4Ch, AL=00h) --- mov ax, 4C00h int 21h END start
Запит "Press Y/N" з перевіркою відповіді:.MODEL small
.STACK 256 .DATA
prompt DB 'Continue? (Y/N): $'
yes_msg DB 0Dh, 0Ah, 'OK!$'
no_msg DB 0Dh, 0Ah, 'Bye!$' .CODE
start: mov ax, @data mov ds, ax ; --- Вивести запит --- mov ah, 09h lea dx, prompt int 21h ; --- Зчитати символ (AH=01h) --- mov ah, 01h int 21h ; результат у AL ; --- Порівняти з 'Y' та 'y' --- cmp al, 'Y' je say_yes cmp al, 'y' je say_yes ; --- Будь-яка інша відповідь = ні --- mov ah, 09h lea dx, no_msg int 21h jmp quit say_yes: mov ah, 09h lea dx, yes_msg int 21h quit: mov ax, 4C00h int 21h END start Забули '$' наприкінці рядка. Функція AH=09h виводить символи, доки не зустріне '$'. Без нього DOS виводитиме сміття з пам'яті.
Не встановили DS перед виведенням рядка. Функція AH=09h читає рядок за адресою DS:DX. Якщо DS не вказує на сегмент даних, програма виведе вміст хибної адреси або зависне.
Забули код повернення в AL при завершенні. MOV AH, 4Ch / INT 21h без встановлення AL залишає випадкове значення як код повернення. Використовуйте MOV AX, 4C00h для нормального завершення (код 0). <a class="internal-link" data-href="keyboard-input.md" href="projects/how-to/keyboard-input.html" target="_self" rel="noopener nofollow">Введення з клавіатури</a>
<br><a class="internal-link" data-href="segment-registers.md" href="projects/how-to/segment-registers.html" target="_self" rel="noopener nofollow">Сегментні регістри (DS, ES)</a> Детальніше: L07, P07
]]></description><link>projects/how-to/dos-services.html</link><guid isPermaLink="false">Projects/How-to/dos-services.md</guid><pubDate>Tue, 24 Feb 2026 06:08:05 GMT</pubDate></item><item><title><![CDATA[segment-registers]]></title><description><![CDATA[Як працюють сегментні регістри, навіщо потрібні DS та ES, і як правильно їх налаштовувати у програмі на TASM.
На початку .COM або .EXE програми треба правильно налаштувати DS.
Потрібно працювати з відеопам'яттю або іншим сегментом через ES.
Виникає помилка доступу до даних — можливо, сегментний регістр не встановлено.
У реальному режимі x86 є чотири сегментні регістри:Процесор обчислює 20-бітну фізичну адресу за формулою:фізична адреса = сегмент * 16 + зміщення
Наприклад, DS = 1234h, зміщення 0056h:1234h * 10h + 0056h = 12340h + 0056h = 12396h
Процесор не підтримує команду MOV DS, 1234h (безпосереднє значення в сегментний регістр). Завжди потрібен проміжний регістр:; Неправильно — не скомпілюється:
; mov ds, @data ; Правильно — через AX: mov ax, @data mov ds, ax
Директива .MODEL small + .DATA повідомляє TASM, де зберігаються дані. Але сам регістр DS потрібно встановити на початку програми: mov ax, @data ; @data = адреса сегмента .DATA mov ds, ax
ES потрібен, коли працюєте з іншим сегментом одночасно з DS. Типовий випадок — відеопам'ять: mov ax, 0B800h ; сегмент відеопам'яті mov es, ax
ASSUME повідомляє TASM, який сегмент пов'язаний з яким регістром, щоб компілятор правильно генерував адреси:ASSUME DS:@data, ES:NOTHING
Це не змінює значення регістрів — лише підказка для асемблера.Копіювання рядка з сегмента даних у відеопам'ять (рядок 0, стовпець 0, білий на чорному):.MODEL small
.STACK 256 .DATA
msg DB 'CSA 2026', 0 ; рядок із нуль-термінатором .CODE
start: ; --- Налаштування DS на сегмент даних --- mov ax, @data mov ds, ax ; --- Налаштування ES на відеопам'ять --- mov ax, 0B800h mov es, ax ; --- Копіювання рядка з DS:SI у відеопам'ять ES:DI --- lea si, msg ; SI = зміщення рядка у DS xor di, di ; DI = 0 (верхній лівий кут екрана) mov ah, 0Fh ; атрибут: білий текст, чорний фон copy_loop: lodsb ; AL = DS:[SI], SI += 1 cmp al, 0 ; перевірка на кінець рядка je done mov es:[di], ax ; AL = символ, AH = атрибут add di, 2 ; перехід до наступної клітинки jmp copy_loop done: ; --- Очікування натискання клавіші --- mov ah, 00h int 16h ; --- Завершення програми --- mov ax, 4C00h int 21h END start Забули встановити DS на початку. Без MOV AX, @data / MOV DS, AX звернення до змінних у .DATA читатимуть сміття з хибного сегмента.
Перезаписали ES випадково. Якщо після налаштування ES на відеопам'ять викликати функцію DOS, яка змінює ES, — подальший запис у ES:[DI] потрапить не туди. Після INT перевіряйте ES.
Плутанина між DS та ES. Без явного перевизначення сегмента (ES:[DI]) процесор за замовчуванням використовує DS. Якщо дані у відеопам'яті — обов'язково пишіть ES:. <a class="internal-link" data-href="video-memory.md" href="projects/how-to/video-memory.html" target="_self" rel="noopener nofollow">Відеопам'ять: прямий запис у B800h</a>
<br><a class="internal-link" data-href="addressing-modes.md" href="projects/how-to/addressing-modes.html" target="_self" rel="noopener nofollow">Режими адресації</a> Детальніше: L03, P03
]]></description><link>projects/how-to/segment-registers.html</link><guid isPermaLink="false">Projects/How-to/segment-registers.md</guid><pubDate>Tue, 24 Feb 2026 06:07:50 GMT</pubDate></item><item><title><![CDATA[video-memory]]></title><description><![CDATA[Як виводити кольоровий текст на екран, записуючи символи безпосередньо у відеопам'ять текстового режиму.
Потрібно вивести символ у конкретну позицію екрана (рядок, стовпець) без INT 21h.
Потрібно задати колір тексту та фону для кожного символу окремо.
Створюєте гру або інтерфейс, де важлива швидкість перемальовування.
У текстовому режимі 3 (80x25) відеопам'ять починається з сегмента B800h. Кожна клітинка екрана займає 2 байти:Атрибут кольору (1 байт): Біт: 7 6 5 4 3 2 1 0 └──┬──┘ └──┬──┘ фон (0-7) текст (0-Fh) Біт 7 = блимання (або яскравий фон, залежно від налаштувань)
Таблиця кольорів:
Коди 8h-Fh для фону доступні лише при вимкненому блиманні.
Формула зміщення для позиції (рядок, стовпець):зміщення = рядок * 160 + стовпець * 2
Рядки нумеруються 0-24, стовпці 0-79. Усього: 80 x 25 x 2 = 4000 байт.Виведення рядка "Hello" жовтим по синьому у рядку 10, стовпці 35:.MODEL small
.STACK 256 .CODE
start: ; --- Налаштування сегмента відеопам'яті --- mov ax, 0B800h mov es, ax ; --- Обчислення зміщення: рядок 10, стовпець 35 --- ; зміщення = 10 * 160 + 35 * 2 = 1600 + 70 = 1670 mov di, 1670 ; --- Атрибут: синій фон (1h &lt;&lt; 4) + жовтий текст (Eh) = 1Eh --- mov ah, 1Eh ; --- Запис символів у відеопам'ять --- mov al, 'H' mov es:[di], ax ; символ + атрибут add di, 2 mov al, 'e' mov es:[di], ax add di, 2 mov al, 'l' mov es:[di], ax add di, 2 mov al, 'l' mov es:[di], ax add di, 2 mov al, 'o' mov es:[di], ax ; --- Очікування натискання клавіші --- mov ah, 00h int 16h ; --- Завершення програми --- mov ax, 4C00h int 21h END start Забули помножити стовпець на 2. Кожна клітинка — це 2 байти. Якщо стовпець = 5, зміщення стовпця = 10, а не 5.
Переплутали порядок байтів. Парна адреса — символ, непарна — атрибут. Якщо записати MOV ES:[DI], AX, то AL йде в парну адресу (символ), AH — у непарну (атрибут).
Не встановили ES. Без MOV AX, 0B800h / MOV ES, AX запис потрапить у хибний сегмент. <a class="internal-link" data-href="segment-registers.md" href="projects/how-to/segment-registers.html" target="_self" rel="noopener nofollow">Сегментні регістри (ES, DS)</a>
<br><a class="internal-link" data-href="addressing-modes.md" href="projects/how-to/addressing-modes.html" target="_self" rel="noopener nofollow">Режими адресації</a> Детальніше: L08, P08
]]></description><link>projects/how-to/video-memory.html</link><guid isPermaLink="false">Projects/How-to/video-memory.md</guid><pubDate>Tue, 24 Feb 2026 06:07:18 GMT</pubDate></item><item><title><![CDATA[Lab07]]></title><description><![CDATA[
Курс: Архітектура обчислювальних систем
Тиждень: 7
Тривалість: 2 академічні години
Тип: Практична робота під наглядом інструктора
Після завершення практики студент зможе:
Знаходити та декодувати записи таблиці переривань (IVT) у вікні Dump Turbo Debugger.
Використовувати переривання DOS (INT 21h) для введення та виведення символів і рядків.
Використовувати переривання BIOS INT 10h для керування екраном та INT 16h для читання клавіатури.
Пояснити механізм INT/IRET: що кладеться на стек, чим відрізняється від CALL.
Перехоплювати вектор переривання: зберегти оригінал, встановити власний обробник, відновити.
Перегляньте лекційні матеріали за Тиждень 7 (переривання, IVT, BIOS/DOS сервіси). Зверніть увагу на таблицю переривань та механізм INT/IRET.<a rel="noopener nofollow" class="external-link is-unresolved" href="https://www.asciitable.com/" target="_self">https://www.asciitable.com/</a>256 записів × 4 байти = 1024 байти за адресою 0000:0000–0000:03FF.Кожний запис: 2 байти offset (IP) + 2 байти segment (CS), little-endian.Адреса обробника для INT N: читаємо 4 байти за адресою N × 4.Номер функції в AH:Байт атрибуту кольору: біти 7–4 = фон, біти 3–0 = текст.Приклад: 1Eh = жовтий текст (E) на синьому фоні (1).INT додатково очищує прапорці IF та TF (забороняє апаратні переривання під час входу в обробник).Створіть папку Lab07. Скопіюйте STUB.ASM з попередньої роботи.Для кожного завдання: копіюйте STUB.ASM у новий файл з відповідним ім'ям.Скопіюйте STUB.ASM → 1IVT.ASM. Для цієї частини код не обов'язковий — головне дослідження у TD.Скомпілюйте та відкрийте 1IVT.ASM у TD. Відкрийте вікно Dump (Alt+D або View → Dump).Перейдіть до адреси 0000:0000 (Ctrl+G → введіть 0000:0000).Ви бачите таблицю переривань — 256 записів по 4 байти кожний.Декодуйте вектор INT 21h (DOS):
Адреса в IVT: 21h × 4 = 84h, тобто 0000:0084
Прочитайте 4 байти починаючи з 0000:0084
Перші 2 байти = offset (IP), наступні 2 = segment (CS), обидва little-endian
Запишіть у коментарях: ; INT 21h handler: SSSS:OOOO
Аналогічно декодуйте:
INT 10h (відео): адреса 0000:0040
INT 00h (ділення на нуль): адреса 0000:0000
Перейдіть у Dump до адреси 0040:0000 — це BIOS Data Area.Цікаві зміщення:
0040:006C — лічильник тактів таймера (DWORD, збільшується ~18.2 разів/с)
0040:001A та 0040:001C — покажчики клавіатурного буфера
Натисніть кілька клавіш і спостерігайте зміни у 0040:001A–0040:003D.Запишіть у коментарях 1IVT.ASM: адреси обробників INT 21h, INT 10h, INT 00h, та що ви спостерігали в BIOS Data Area.Скопіюйте STUB.ASM → 2DOS.ASM.Додайте до .DATA:msg DB 'Hello, Interrupts!', 0Dh, 0Ah, '$'
prompt DB 'Enter a char: $'
Додайте до .CODE (після mov ds, ax): ; Print string mov ah, 09h mov dx, offset msg int 21h
Запустіть програму. Перевірте, що рядок виводиться у вікні DOSBox.Додайте після попереднього коду: ; Print prompt mov ah, 09h mov dx, offset prompt int 21h ; Read character (with echo) mov ah, 01h int 21h ; AL = entered character ; Save char and print it again mov dl, al mov ah, 02h int 21h ; Display the same char once more ; Newline mov dl, 0Dh mov ah, 02h int 21h mov dl, 0Ah mov ah, 02h int 21h
Пройдіть покроково в TD (F7). Зверніть увагу: який стан регістрів AH, AL перед і після кожного INT 21h?Додайте до .DATA:prompt2 DB 'Type text, press Enter to finish: $'
result DB 'You typed XX chars', 0Dh, 0Ah, '$'
Напишіть код, що:
Виводить prompt2
У циклі читає символи (AH=01h) і підраховує їх у CX
Коли натиснуто Enter (AL = 0Dh), виходить з циклу
Виводить повідомлення з кількістю (підказка: перетворіть CX на два ASCII-символи десяткового числа і запишіть замість XX у result)
Спрощений варіант: якщо перетворення числа складне, просто збережіть лічильник у CX і подивіться його значення у TD після циклу.Скопіюйте STUB.ASM → 3BIOS.ASM.Додайте до .CODE: ; Clear screen (scroll up entire window with 0 lines = clear) mov ah, 06h ; Scroll up mov al, 0 ; AL=0 means clear mov bh, 07h ; Attribute: white on black mov cx, 0000h ; Upper-left corner: row 0, col 0 mov dx, 184Fh ; Lower-right: row 24 (18h), col 79 (4Fh) int 10h
Запустіть — екран DOSBox має очиститися.Після очищення екрана додайте: ; Set cursor to row 12, col 30 mov ah, 02h mov dh, 12 ; Row mov dl, 30 ; Column mov bh, 0 ; Video page 0 int 10h ; Write colored character 'A' in yellow on blue mov ah, 09h mov al, 'A' ; Character mov bl, 1Eh ; Attribute: yellow (E) on blue (1) mov bh, 0 ; Video page 0 mov cx, 1 ; Write 1 time int 10h
Запустіть — у центрі екрана має з'явитися жовта літера A на синьому фоні.Самостійно: виведіть слово "Hello" посимвольно різними кольорами. Для кожної літери: встановіть курсор (AH=02h), виведіть літеру з кольором (AH=09h), зсуньте стовпець на 1.Додайте наприкінці: ; Wait for keypress mov ah, 00h int 16h ; AH = scan code, AL = ASCII code ; Display the ASCII code of pressed key mov dl, al mov ah, 02h int 21h
Пройдіть покроково в TD. Запишіть у коментарях: який scan code та ASCII code повертає INT 16h для клавіш A, Enter, Esc?Скопіюйте наступний код цілком у 4HOOK.ASM:.MODEL small
.STACK 256 .DATA
tick_count DW 0
msg_ticks DB 'Ticks: $'
msg_exit DB 0Dh, 0Ah, 'Press Q to quit...', 0Dh, 0Ah, '$'
orig_ip DW ? ; Original INT 1Ch handler offset
orig_cs DW ? ; Original INT 1Ch handler segment .CODE ;----------------------------------------------
; Custom handler for INT 1Ch (timer tick)
; Called ~18.2 times per second by hardware
;----------------------------------------------
my_handler proc far push ax push ds mov ax, @data mov ds, ax ; --- YOUR CODE HERE (1 line) --- ; Increment tick_count by 1 pop ds pop ax ; --- YOUR CODE HERE (1 line) --- ; Return from interrupt (NOT ret, NOT retf!)
my_handler endp main: mov ax, @data mov ds, ax ; === Step 1: Save original INT 1Ch vector === cli ; Disable interrupts while modifying IVT push es xor ax, ax mov es, ax ; ES = 0000 (IVT segment) ; --- YOUR CODE HERE (4 lines) --- ; Read original vector from IVT at offset 1Ch*4 = 70h ; Save to orig_ip and orig_cs ; Hint: mov ax, word ptr es:[0070h] / mov orig_ip, ax (then same for +2 → orig_cs) ; === Step 2: Install our handler === ; --- YOUR CODE HERE (4 lines) --- ; Write address of my_handler into IVT (same positions you just read from) ; Hint: reverse of Step 1 — load value into AX, then store to IVT ; TASM keywords: "offset my_handler" gives IP, "seg my_handler" gives CS pop es sti ; Re-enable interrupts ; === Main loop: display ticks, wait for 'q' === mov ah, 09h mov dx, offset msg_exit int 21h wait_loop: ; Display tick_count (simplified: just show low byte as hex) mov ah, 02h mov dl, 0Dh ; Carriage return (stay on same line) int 21h mov ah, 09h mov dx, offset msg_ticks int 21h mov ax, tick_count ; Print AH as hex digit mov dl, ah shr dl, 4 add dl, '0' cmp dl, '9' jle @d1 add dl, 7 ; A-F
@d1: mov ah, 02h int 21h mov ax, tick_count mov dl, ah and dl, 0Fh add dl, '0' cmp dl, '9' jle @d2 add dl, 7
@d2: mov ah, 02h int 21h mov ax, tick_count mov dl, al shr dl, 4 add dl, '0' cmp dl, '9' jle @d3 add dl, 7
@d3: mov ah, 02h int 21h mov ax, tick_count mov dl, al and dl, 0Fh add dl, '0' cmp dl, '9' jle @d4 add dl, 7
@d4: mov ah, 02h int 21h ; Check if key pressed (non-blocking) mov ah, 01h int 16h jz wait_loop ; No key — continue loop ; Read the key mov ah, 00h int 16h cmp al, 'q' je done cmp al, 'Q' je done jmp wait_loop done: ; === Step 3: Restore original INT 1Ch vector === cli push es xor ax, ax mov es, ax ; --- YOUR CODE HERE (4 lines) --- ; Restore original vector from orig_ip and orig_cs ; Hint: mov ax, orig_ip / mov word ptr es:[0070h], ax (then same for orig_cs → +2) pop es sti mov ah, 4Ch int 21h END main
Ваше завдання: допишіть 14 рядків коду у секціях YOUR CODE HERE:
Інкремент tick_count (1 рядок)
Повернення з обробника переривання (1 рядок)
Збереження оригінального вектора (4 рядки: offset і segment через регістр)
Встановлення нового обробника (4 рядки: зворотний до кроку 3 — значення через регістр)
Відновлення оригінального вектора (4 рядки: offset і segment через регістр)
Перевірка: запустіть програму (Ctrl+F9 — не покроково!). Лічильник на екрані має постійно зростати. Натисніть Q для виходу. Якщо система зависає після виходу — ви не відновили оригінальний вектор.
Примітка. Обробник переривання не може використовувати INT 21h (DOS не є реентрантною). Тому ми лише інкрементуємо лічильник, а відображаємо його у головному циклі.
До кінця заняття:
1IVT.ASM — адреси обробників INT 21h, INT 10h, INT 00h у коментарях
2DOS.ASM — рядковий вивід, читання символу, підрахунок
3BIOS.ASM — очищення екрана, кольоровий вивід, клавіатура
4HOOK.ASM — перехоплення INT 1Ch, лічильник тактів
Закомічено всі файли
Дедлайн: ==23:59 у день перед Практикою 9 (через позачергову практику №8) ==Напишіть програму, що поєднує INT 21h, INT 10h та INT 16h:
Очистити екран (INT 10h, AH=06h)
Вивести меню у верхній частині екрана:
=== Color Demo ===
1 - Red message
2 - Green message
3 - Yellow message
Q - Quit Прочитати клавішу (INT 16h, AH=00h)
Залежно від клавіші: встановити курсор у рядок 10 (INT 10h, AH=02h) і вивести кольорове повідомлення (INT 10h, AH=09h)
Повторювати, поки не натиснуто Q
Вимоги:
Використайте процедуру print_at з параметрами через стек: [BP+4] = offset рядка (для виводу через INT 21h/09h)
[BP+6] = стовпець (DL)
[BP+8] = рядок (DH) Процедура встановлює курсор і виводить рядок
Кольоровий вивід повідомлень — через окремий виклик INT 10h/09h після позиціонування
Перед кожним виведенням повідомлення очищуйте рядок 10 (або весь нижній блок)
Тест: візуальна перевірка в DOSBox — меню з'являється, клавіші обирають опцію, кольорове повідомлення з'являється у правильній позиції, Q завершує програму.Розширте програму 4HOOK.ASM:
Перехопіть INT 1Ch (таймер, ~18.2 Гц)
Відображайте лічильник у форматі MM:SS у фіксованій позиції екрана
Для відображення використовуйте пряме записування у відеопам'ять (B800:0000 + offset), де offset = (row × 80 + col) × 2
Перетворіть кількість тактів на секунди (діліть на 18), потім на хвилини та секунди
Кожна цифра — це ASCII-код (30h–39h), записаний у відеопам'ять разом із байтом атрибуту
Лічильник оновлюється в реальному часі, поки головна програма чекає на натискання Q
Відновіть оригінальний вектор перед виходом
Підказки:
Для ділення на 18: XOR DX,DX / MOV BX, 18 / DIV BX → AX = секунди
Для MM:SS: поділіть секунди на 60 → AX = хвилини, DX = секунди
Кожну цифру перетворіть: DIV на 10 → AX = десятки, DX = одиниці → додайте 30h
У відеопам'яті кожна позиція = 2 байти: символ + атрибут
Запишіть відео, у якому:
Поясніть 4HOOK.ASM — пройдіться по коду та поясніть: Що робить обробник my_handler? Чому він закінчується IRET, а не RET?
Навіщо CLI/STI при зміні IVT?
Чому обробник не може використовувати INT 21h для виводу на екран? Продемонструйте MENU.ASM: Покажіть меню, натисніть кілька клавіш, покажіть кольорові повідомлення Поясніть: що відбувається покроково, коли процесор виконує INT 21h? (FLAGS → стек, IF/TF очищуються, CS → стек, IP → стек, CS:IP ← IVT[84h]) УВАГА! Вимога про 5хв максимально важлива! Brevity is the soul of wit. Це частина оцінки.Lab07/
├── STUB.ASM ← шаблон
├── 1IVT.ASM ← дослідження IVT
├── 2DOS.ASM ← переривання DOS
├── 3BIOS.ASM ← переривання BIOS
├── 4HOOK.ASM ← перехоплення переривання (дописаний)
├── MENU.ASM ← домашнє: інтерактивне меню
├── TIMER.ASM ← бонус: лічильник часу
└── video.txt ← посилання на Loom <br><a data-tooltip-position="top" aria-label="https://www.ctyme.com/rbrown.htm" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.ctyme.com/rbrown.htm" target="_self">Ralf Brown's Interrupt List</a> — повний довідник переривань DOS/BIOS
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/INT_10H" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/INT_10H" target="_self">INT 10h — Wikipedia</a> — відеосервіси BIOS
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/INT_16H" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/INT_16H" target="_self">INT 16h — Wikipedia</a> — клавіатурні сервіси BIOS
<br><a data-tooltip-position="top" aria-label="https://mrszeto.net/CIT/interrupts.htm" rel="noopener nofollow" class="external-link is-unresolved" href="https://mrszeto.net/CIT/interrupts.htm" target="_self">DOS INT 21h Functions</a> — таблиця функцій DOS
]]></description><link>labs/lab07.html</link><guid isPermaLink="false">Labs/Lab07.md</guid><pubDate>Thu, 26 Feb 2026 13:43:09 GMT</pubDate></item><item><title><![CDATA[Git-Commit-Message-Meme]]></title><description><![CDATA[<img src="assets/git-commit-message-meme.png" target="_self">]]></description><link>assets/git-commit-message-meme.html</link><guid isPermaLink="false">assets/Git-Commit-Message-Meme.png</guid><pubDate>Tue, 24 Feb 2026 17:27:32 GMT</pubDate><enclosure url="assets/git-commit-message-meme.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/git-commit-message-meme.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Syllabus]]></title><description><![CDATA[НАЦІОНАЛЬНИЙ УНІВЕРСИТЕТ «КИЄВО-МОГИЛЯНСЬКА АКАДЕМІЯ»
Факультет інформатики
Кафедра інформатики <img alt="Logo" src="assets/ukma-logo.png" referrerpolicy="no-referrer" target="_self" style="width: 75px; max-width: 100%;"> ---НАЗВА ДИСЦИПЛІНИ: Архітектура обчислювальних систем
Computer Systems ArchitectureГалузь знань: 12 Інформаційні технології
Спеціальність: 122 Комп'ютерні науки
Освітньо-професійна програма: «Комп'ютерні науки», бакалавр1. Обов'язкові компоненти ОП&nbsp;
1.1 Нормативні дисципліниВИКЛАДАЧІ:&nbsp; <br>Медвідь С.О., старший викладач кафедри інформатики, email:&nbsp;<a data-tooltip-position="top" aria-label="mailto:s.medvid@ukma.edu.ua" rel="noopener nofollow" class="external-link is-unresolved" href="mailto:s.medvid@ukma.edu.ua" target="_self">s.medvid@ukma.edu.ua</a> (лекції, лабораторні)
<br>Макарець А.О., старший викладач кафедри інформатики, email: <a data-tooltip-position="top" aria-label="mailto:a.makarets@ukma.edu.ua" rel="noopener nofollow" class="external-link is-unresolved" href="mailto:a.makarets@ukma.edu.ua" target="_self">a.makarets@ukma.edu.ua</a> (лабораторні)
<br>Войлов Б.С., асистент кафедри інформатики, email: <a data-tooltip-position="top" aria-label="mailto:bohdan.voilov@ukma.edu.ua" rel="noopener nofollow" class="external-link is-unresolved" href="mailto:bohdan.voilov@ukma.edu.ua" target="_self">bohdan.voilov@ukma.edu.ua</a> (лабораторні)
ЗАГАЛЬНЕ НАВАНТАЖЕННЯ (Повний курс: КН, ІПЗ):&nbsp;6 ECTS / 180 год.
Заняття в аудиторії:&nbsp;60 год. (15 лекцій і 15 практичних занять по 2 академічні години)&nbsp;
Самостійна робота слухачів курсу: 120 год.
Форма контролю: іспит
ЗАГАЛЬНЕ НАВАНТАЖЕННЯ (Скорочений курс: АВІС, АКІТР):&nbsp;4 ECTS / 120 год.
Заняття в аудиторії:&nbsp;60 год. (15 лекцій і 15 практичних занять по 2 академічні години)
Самостійна робота слухачів курсу: 60 год.
Форма контролю: іспит Примітка. Студенти скороченого курсу (4 ECTS) відвідують усі 15 лекцій та 15 практичних занять нарівні з повним курсом. Різниця полягає в обсязі самостійної роботи та структурі оцінювання: повний курс додатково включає підсумковий проєкт (див. розділ 4).
ПРЕРЕКВІЗИТИ: Відсутні. Курс є дисципліною першого року навчання.<br>РОЗКЛАД ЗАНЯТЬ: Актуальний розклад лекцій та лабораторних занять доступний на порталі <a data-tooltip-position="top" aria-label="https://my.ukma.edu.ua" rel="noopener nofollow" class="external-link is-unresolved" href="https://my.ukma.edu.ua" target="_self">my.ukma.edu.ua</a> у розділі «Розклад занять».Як комп'ютер виконує програму? Що відбувається між натисканням Enter і появою результату на екрані? Цей курс відповідає на ці питання, розкриваючи принципи роботи комп'ютера «зсередини» — від логічних вентилів до взаємодії з операційною системою.Ви навчитесь:
розуміти, як процесор виконує інструкції та працює з пам'яттю;
писати програми мовою асемблера x86;
аналізувати продуктивність програм та знаходити «вузькі місця».
Практика: дві платформи — DOSBox (прямий доступ до «заліза») та Linux x86-64 (сучасне середовище з профілюванням та SIMD).Курс є базовим для вивчення операційних систем, системного програмування та оптимізації. Пререквізити відсутні.How does a computer execute a program? What happens between pressing Enter and seeing the result? This course answers these questions by revealing how computers work "from the inside" — from logic gates to operating system interaction.You will learn to:
understand how the processor executes instructions and manages memory;
write programs in x86 assembly language;
analyze program performance and identify bottlenecks.
Hands-on: two platforms — DOSBox (direct hardware access) and Linux x86-64 (modern environment with profiling and SIMD).This course provides the foundation for Operating Systems, Systems Programming, and software optimization. No prerequisites required.Мета&nbsp;дисципліни полягає у формуванні в студентів глибокого розуміння принципів побудови та функціонування сучасних комп'ютерних архітектур, що дозволяє приймати обґрунтовані інженерні рішення при розробці програмного забезпечення з урахуванням апаратних обмежень та можливостей. Курс спрямований на подолання «чорної скриньки» – розуміння того, як високорівневий код перетворюється на машинні інструкції та взаємодіє з апаратним забезпеченням.Предмет&nbsp;навчальної дисципліни «Архітектура обчислювальних систем» охоплює: фізичні основи обчислень та цифрову логіку; архітектуру фон Неймана та її сучасні розширення; представлення чисел та даних на машинному рівні; системи команд процесорів (ISA) та програмування мовою асемблер; організацію пам'яті (ієрархія, кешування, віртуальна пам'ять); взаємодію з операційною системою (переривання, привілеї); паралелізм на різних рівнях (конвеєризація, SIMD, багатопоточність); сучасні гетерогенні архітектури (GPU, TPU, FPGA) та питання енергоефективності.Практична частина курсу використовує два середовища:
TASM + DOSBox + Turbo Debugger&nbsp;(практики P1–P10): 16-бітний реальний режим x86, де студенти бачать «всю машину» – IVT за адресою 0000:0000, BIOS Data Area, пряму роботу з відеопам'яттю, переривання BIOS/DOS. Turbo Debugger забезпечує інтуїтивну візуалізацію регістрів, пам'яті, стеку та прапорців.
NASM + Linux + GDB/GEF&nbsp;(практики P11–P15): 64-бітний захищений режим, сучасні інструменти профілювання (perf, Valgrind), аналіз віртуальної пам'яті, SIMD-оптимізація – з'єднання фундаментальних знань із сучасною практикою.
Знання та практичний досвід, отримані в процесі вивчення курсу, дозволять студентам писати ефективний код з урахуванням архітектурних особливостей, діагностувати проблеми продуктивності, оптимізувати критичні ділянки програм та приймати обґрунтовані рішення щодо вибору архітектурних платформ для різних задач.Основні завдання:
Розуміти принципи побудови комп'ютерних систем від цифрової логіки до архітектури процесорів та ієрархії пам'яті.
Аналізувати та перетворювати дані між різними форматами представлення на машинному рівні (цілі, дійсні числа, адреси).
Писати, налагоджувати та оптимізувати програми мовою асемблер з використанням TASM (16-bit) та NASM (64-bit).
Пояснювати механізми взаємодії апаратного забезпечення з операційною системою: переривання, віртуальна пам'ять, режими привілеїв.
Оцінювати продуктивність обчислювальних систем, виявляти архітектурні вузькі місця та застосовувати методи оптимізації.
Описувати принципи паралелізму на рівні інструкцій, даних та потоків, а також гетерогенних обчислень.
Аргументовано обирати архітектурні рішення з урахуванням продуктивності, енергоефективності та масштабованості.
Методологічні засади проєктування курсуДисципліну спроєктовано відповідно до принципу&nbsp;конструктивного узгодження (constructive alignment), за яким результати навчання, методи викладання та форми оцінювання взаємно узгоджені та спрямовані на досягнення запланованих програмних результатів навчання.Формулювання результатів навчання. Результати навчання дисципліни сформульовано з використанням таксономії освітніх цілей Б. Блума з акцентом на когнітивні рівні&nbsp;застосування, аналізу, оцінювання та синтезу, що відповідає вимогам бакалаврського рівня підготовки з комп’ютерних наук.Вивчення дисципліни «Архітектура обчислювальних систем» забезпечує формування таких загальних компетентностей освітньо-професійної програми:Дисципліна формує такі фахові компетентності спеціальності 122 «Комп'ютерні науки»:Курс «Архітектура обчислювальних систем» забезпечує досягнення таких програмних результатів навчання освітньо-професійної програми.Після завершення курсу студент повинен продемонструвати здатність:<br>Зміст дисципліни узгоджується з рекомендаціями&nbsp;<a data-tooltip-position="top" aria-label="https://dl.acm.org/doi/book/10.1145/3664191" rel="noopener nofollow" class="external-link is-unresolved" href="https://dl.acm.org/doi/book/10.1145/3664191" target="_self">Computer Science Curricula 2023</a>, охоплюючи ключові знаннєві одиниці таких Knowledge Areas:
Це забезпечує відповідність курсу сучасним міжнародним вимогам до підготовки фахівців у галузі комп'ютерних наук.
Результати навчання дисципліни «Архітектура обчислювальних систем» узгоджуються з ключовими&nbsp;Student Outcomes (SO), що використовуються при акредитації освітніх програм з комп'ютерних наук за стандартами&nbsp;ABET&nbsp;(Accreditation Board for Engineering and Technology).ABET-орієнтація курсу полягає у формуванні в студентів здатності аналізувати обчислювальні системи, приймати обґрунтовані інженерні рішення, оцінювати продуктивність і обмеження архітектурних рішень, а також відповідально застосовувати системні знання у професійній діяльності.SO1&nbsp;реалізується через аналіз архітектури процесора, пам'яті та системних обмежень при розв'язанні обчислювальних задач.&nbsp;SO2&nbsp;забезпечується практичними та проєктними роботами, що вимагають розробки низькорівневих програм і оцінки їх ефективності з урахуванням архітектури.&nbsp;SO3&nbsp;підтримується обов'язковими усними поясненнями, відео-презентаціями та аргументацією прийнятих рішень.&nbsp;SO4&nbsp;інтегрується через розгляд питань безпеки, надійності, енергоефективності та сталого розвитку комп'ютерних систем.&nbsp;SO5&nbsp;формується в процесі виконання складних практичних і підсумкових завдань, що потребують координації та відповідальності.
Таким чином, дисципліна «Архітектура обчислювальних систем» робить вагомий внесок у досягнення результатів навчання, необхідних для відповідності освітньої програми вимогам ABET, і підтримує підготовку випускників до професійної діяльності у сфері комп'ютерних наук.
Оцінювання результатів навчання з дисципліни «Архітектура обчислювальних систем» здійснюється за модульно-рейтинговою системою відповідно до 100-бальної шкали.Підсумкова оцінка формується як сума балів, отриманих студентом за всі види навчальної діяльності протягом семестру, та включає:
поточну роботу на практичних заняттях;
практичні контрольні заходи;
підсумковий проєкт (тільки 6 ECTS);
підсумковий іспит з теорії.
Оцінювання побудоване за принципом безперервного контролю з акцентом на практичні навички, що відповідає системному характеру дисципліни та дозволяє поетапно перевіряти досягнення результатів навчання (LO).Характер оцінювання.
Система оцінювання у курсі поєднує&nbsp;формативні&nbsp;(поточні практичні роботи, вбудовані практичні контрольні заходи) та&nbsp;підсумкові&nbsp;(підсумковий проєкт для 6 ECTS, іспит) форми оцінювання, що забезпечує поетапний зворотний зв’язок і підтримує поступове досягнення результатів навчання.Для отримання допуску до підсумкового іспиту студент повинен:
брати участь у лекційних та практичних заняттях;
виконати та здати не менше 70% обов’язкових практичних робіт;
пройти всі передбачені практичні контрольні заходи;
здати підсумковий проєкт у встановлені терміни (для 6 ECTS).
Студент, який не виконав зазначені умови, до іспиту не допускається.Оцінювання здійснюється за ваговими моделями для повного курсу (6 ECTS) та скороченого курсу (4 ECTS). Обидва курси мають однаковий обсяг аудиторних занять (60 год.), однакові практичні роботи (P1–P15) та два практичних контрольних заходи. Різниця полягає в обсязі самостійної роботи (120 год. проти 60 год.): повний курс (6 ECTS) додатково включає підсумковий проєкт (~60 год.), що відображено у вагах оцінювання.
Обидва курси мають однакові практичні роботи, контрольні заходи та іспит. Повний курс (6 ECTS) додатково включає підсумковий проєкт (20%), що зменшує відносну вагу лабораторних (30% замість 40%) та іспиту (20% замість 30%).
Рефлексія та зворотний зв’язок
Пояснення прийнятих рішень у межах практичних робіт, відео-захистів та проєкту (6 ECTS) сприяє формуванню навичок&nbsp;рефлексії, самоконтролю та професійної відповідальності, а також дозволяє студентам коригувати власні підходи на основі отриманого зворотного зв’язку.
Критерії оцінювання практичних робіт, контрольних заходів та проєкту (6 ECTS) доводяться до відома студентів заздалегідь у вигляді чітких handout-документів.
Зворотний зв’язок надається у письмовій формі через систему подання робіт та під час усних обговорень на лабораторних заняттях.Практичні роботи виконуються протягом семестру та перевіряють сформованість базових навичок низькорівневого програмування та розуміння архітектурних механізмів.Обидва курси виконують та здають практики P1–P15 (40% загальної оцінки для 4 ECTS, 30% для 6 ECTS).Оцінювання враховує коректність реалізації, дотримання вимог завдання, якість коду, своєчасність подання та пояснення прийнятих рішень.Практичний контроль I проводиться в межах лабораторного заняття №10.Контроль перевіряє засвоєння практичних навичок за матеріалами практик P3–P9:
робота з регістрами та арифметикою;
керування потоком виконання (умови, цикли);
використання стеку та процедур;
масиви та режими адресації;
переривання BIOS/DOS;
робота з відеопамʼяттю;
налагодження коду у Turbo Debugger.
Лабораторне заняття повністю присвячене виконанню контрольного завдання.Практичний контроль II проводиться в межах лабораторного заняття №15 для обох курсів.Контроль перевіряє засвоєння практичних навичок за матеріалами практик P11–P14:
перехід до x86-64 та Linux;
експерименти з кеш-пам'яттю та профілювання;
віртуальна пам'ять;
арифметика з рухомою комою (IEEE 754);
основи SIMD.
Для 6 ECTS практичний контроль II поєднується із захистом підсумкового проєкту на тому ж занятті.Підсумковий проєкт є компонентом оцінювання тільки для повного курсу (6 ECTS) і спрямований на застосування архітектурних знань для розв'язання комплексної практичної задачі. Студент обирає тему з запропонованого меню та реалізує працюючу програму на TASM / DOSBox з архітектурним звітом.Платформа: TASM / DOSBox / Turbo Debugger.Формат роботи: парний проєкт (рекомендовано) або індивідуальний (~60 годин самостійної роботи на кожного учасника). Парні проєкти розміщуються в окремому репозиторії організації курсу (GitHub Classroom).Календар:
Тиждень 7 (L08/P08): оголошення проєкту та меню тем.
Тиждень 8 (самостійна робота): вибір теми, формування пар, початок роботи.
Тижні 8–15: розробка з обов'язковими щотижневими контрольними точками (Loom-відео до 3 хвилин, 15% оцінки проєкту).
Тиждень 15 (P15): дедлайн та захист.
Результати, що подаються:
Працюючий код на асемблері (TASM).
Архітектурний звіт (Markdown, 3–5 сторінок).
Фінальне Loom-відео (до 5 хвилин; для парних проєктів — індивідуальне від кожного учасника).
Живий захист з Q&amp;A на P15.
Вимоги до роботи з Git:
<br>Регулярні, гранулярні коміти з повідомленнями у форматі <a data-tooltip-position="top" aria-label="https://www.conventionalcommits.org/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.conventionalcommits.org/" target="_self">Conventional Commits</a> (англійською).
Використання GitHub Issues для планування задач (рекомендовано).
Історія комітів є основним джерелом для оцінки індивідуального внеску в парних проєктах.
Критерії оцінювання: код — реалізація (50%, з них 25% поточне + 25% фінальне), тижневий прогрес — 7 обов'язкових контрольних точок (15%), звіт (20%), відео та захист (15%). Бонуси: +10% за рівний внесок у парі, до +20% за реалізацію розширень.<br>
Детальний опис проєкту, критерії оцінювання, вимоги до звіту та приклади тем наведено у документі <a class="internal-link" data-href="Labs/Project.md" href="labs/project.html" target="_self" rel="noopener nofollow">Project.md</a>.
Підсумковий іспит з теорії охоплює весь теоретичний матеріал курсу та інтегрує питання, що раніше перевірялися теоретичними контрольними заходами.
Для обох курсів: лекції 1–15 (студенти обох курсів відвідують усі лекції).
Підсумкова оцінка з дисципліни визначається як сума балів за всі компоненти оцінювання.
Мінімальна кількість балів для успішного завершення курсу — 60 зі 100.
Іспит вважається не складеним, якщо сумарна кількість балів з дисципліни менша за 60.
Перерахунок підсумкової оцінки, отриманої за 100-бальною університетською шкалою, у національну чотирибальну шкалу та шкалу ECTS здійснюється відповідно до затверджених нормативів НаУКМА.
Мінімальний пороговий рівень оцінки для допуску до підсумкового іспиту становить 30 балів.
У разі отримання оцінки FX студент має право на повторне складання іспиту відповідно до встановленого порядку.
У разі отримання оцінки F студент зобов’язаний повторно вивчити дисципліну.
Після повторного складання максимальна підсумкова оцінка не може перевищувати рівень E («достатньо»).
Обидва курси мають однаковий обсяг аудиторних занять (60 год.), але різний бюджет самостійної роботи. Нижче наведено орієнтовний розподіл годин самостійної роботи за видами діяльності.
Для студентів 4 ECTS: бюджет самостійної роботи (60 год.) розподіляється між виконанням лабораторних робіт та підготовкою до лекцій, контролів і іспиту. Підсумковий проєкт для цього курсу не передбачений — натомість більше ваги мають лабораторні роботи (40% оцінки). Для студентів 6 ECTS: додатковий бюджет (60 год.) дозволяє виконати поглиблений підсумковий проєкт (~60 годин, рекомендовано в парі), що становить 20% загальної оцінки.
Орієнтовний розподіл годин домашньої роботи за практиками наведено нижче (включно з підготовкою Loom-відео):Орієнтовний розподіл самостійної роботи по тижнях (підготовка до лекцій/контролів/іспиту розподілена рівномірно, ~2 год./тиждень):
Примітка. Години проєкту розподілені орієнтовно; реальне навантаження залежить від обраної теми та графіка роботи пари. Тижні 8–10 — найкращий час для активного старту проєкту, поки навантаження з лабораторних менше.
Для забезпечення рівномірності навчання, що є необхідним для успішного опанування дисципліни, всі завдання мають граничні терміни здачі на оцінювання.
Всі лабораторні результати (та етапи проєкту для 6 ECTS) повинні бути подані до відповідних дедлайнів.
Дедлайн є дедлайном.
Єдиний виняток – поважна причина, підтверджена медичною довідкою за відповідні дати.
Автентичність довідки перевіряється Медичним відділом НаУКМА.
У час дедлайну автоматичний скрипт "збере" ваші репозиторії для оцінювання.
Пізні подання без поважних медичних причин не приймаються. Кожен студент підтримуватиме єдиний Git репозиторій для зберігання і подання коду практичних завдань.
Результати повинні бути чітко організовані в папки, згідно з вимогами, наданими на лабораторних заняттях.
Всі файли подання та посилання на відео-описи повинні бути закомічені до дедлайну. <br>На додаток до коду кожен студент повинен подати коротку відео-презентацію із використанням&nbsp;<a data-tooltip-position="top" aria-label="https://www.loom.com/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.loom.com/" target="_self">Loom</a>(максимум 5 хвилин, це важливо) для кожного основного результату завдання.
Мета: показати ваш індивідуальний внесок.
Відео повинно включати: Демонстрацію екрану вашого коду / документа / тесту / конфігурації тощо.
Коротке голосове пояснення того, що ви зробили, ключові рішення та як це вписується в завдання. Формат подання: Створіть текстовий файл у папці репозиторію з вашим посиланням на відео Loom.
Назва файлу: video_presenation.txt
Приклад: Lab02/video_presentation.txt
Не завантажуйте відео в репозиторій.&nbsp;Потрібне лише посилання.
<br>Безкоштовних акаунтів&nbsp;<a data-tooltip-position="top" aria-label="https://www.loom.com/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.loom.com/" target="_self">Loom</a>&nbsp;достатньо для цієї мети. Лабораторні розроблені для практичної роботи та моніторингу прогресу.
Фіналізовані результати повинні бути подані в репозиторії, навіть у випадках коли більша частина роботи виконується вдома.
За результатами лабораторного заняття має бути як мінімум один коміт протягом заняття.
Історія комітів репозиторію буде перевірятися для підтвердження активної участі студентів. Лекції: Очно або гібридно з демонстраціями на дошці/коду
Лабораторні заняття: Практичні сесії із низькорівневим програмуванням на Assembly
Підтримка: Консультаційні години, груповий чат, GitHub репозиторій зі стартовим кодом
Мова викладання та оцінювання –&nbsp;українська, з використанням англомовної технічної термінології та документації.
Для виконання практичних робіт студенти використовують персональний комп’ютер з можливістю запуску DOSBox та сучасного Linux-оточення, а також інструменти розробки (асемблер, відлагоджувач, компілятор), систему контролю версій GitHub.Курс реалізує&nbsp;студентоцентровану модель навчання, у якій основний акцент зроблено на активну практичну діяльність студентів, самостійне прийняття інженерних рішень, експериментування та аналіз результатів власної роботи.Курс реалізується у змішаному форматі (blended learning), що забезпечує доступність для студентів із різними потребами:
Лекції транслюються онлайн у реальному часі через MS Teams. Записи лекцій зберігаються та доступні для перегляду.
Лабораторні заняття можна виконувати дистанційно — студенти можуть приєднатися до практики онлайн у разі хвороби чи інших поважних обставин.
Консультації проводяться онлайн через MS Teams.
Усі матеріали (завдання, слайди, додаткові ресурси) доступні в електронному вигляді через Distedu.
Такий формат адаптований для студентів, які тимчасово не можуть відвідувати заняття очно (хвороба, відрядження, інші поважні причини), а також для студентів з особливими освітніми потребами, що потребують гнучкого графіку навчання.Студенти з особливими потребами можуть звернутися до викладача для узгодження індивідуальних умов проходження курсу.
* Тиждень 7 містить дві пари лекцій та лабораторних (L07/P07 + L08/P08). Тиждень 8 — тиждень самостійної роботи згідно з графіком університету. Загальна кількість аудиторних занять не змінюється: 15 лекцій + 15 лабораторних.
Тема:&nbsp;Обчислення як фізичний процес, транзистори, логічні вентилі, базова пам'ять
Що таке обчислення: фізичні процеси, енергія та інформація
Чому електроніка: швидкість, надійність, масштабування
Чому двійкова система: завадостійкість, булева алгебра
Транзистор як перемикач: основа цифрової логіки
Логічні вентилі: NOT, AND, OR, NAND, XOR
Від вентилів до пам'яті: D-тригер як 1-бітна комірка
Еволюція: від механічних машин до VLSI
AR KUs:&nbsp;AR-Logic (Fundamental building blocks, gates)
SF KUs:&nbsp;SF-Foundations (Digital vs Analog systems, Simple logic gates)
LO:&nbsp;LO1Тема:&nbsp;Комбінаційна та послідовна логіка, регістри, примітивний ALU
Комбінаційна логіка: мультиплексори, декодери, суматори
Послідовна логіка: тригери, регістри, лічильники
Тактування: clock signal, синхронізація, race conditions
Двійкова система числення. Прямий, обернений, доповняльний код.
Масштабування пам'яті: від 1-біта до байтів
Примітивний ALU: opcodes, керуючі сигнали
Перший погляд на машинний код
AR KUs:&nbsp;AR-Logic (Combinational vs sequential logic), AR-Assembly (Control unit basics), AR-Representation (Signed/unsigned, two's complement)
SF KUs:&nbsp;SF-Foundations (Clocks, State, Sequencing)
LO:&nbsp;LO1Тема: Шістнадцяткова система, кодування інструкцій, регістри та основи RAM
Шістнадцяткова система: зв'язок з двійковою
Кодування інструкцій: binary decoder, «шнурування» параметрів
Акумулятор: R0 як результат ALU-операцій
Регістри x86: 16/32/64-біт (AX→EAX→RAX), призначення
Обмеження регістрів: швидкість vs кількість
RAM: квадратна сітка бітів, адресація через binary decoder
Шина даних (data bus): концепція
SRAM vs DRAM: огляд
Сегментна модель 8086: segment:offset, обчислення адрес
Регістр FLAGS: огляд
AR KUs: AR-Representation (Hex, bits/bytes), AR-Assembly (Registers, instruction formats), AR-Memory (RAM basics, SRAM/DRAM intro), AR-Logic (Binary decoder)
SF KUs: SF-Foundations (Memory organization basics)
LO: LO1, LO2Тема: Інструкції ALU, умовні переходи, доповняльний код, бітові операції
Покажчик інструкцій (IP): fetch-decode-execute
Регістр FLAGS: CF, ZF, SF, OF
Інструкції ALU: ADD, SUB, INC, DEC, NEG
MUL, IMUL, DIV, IDIV: множення та ділення
Інструкції переходу: JMP, умовні переходи (JE, JNE, JG, JL...)
Знакові цілі: доповнення до двох
Арифметика: переповнення, прапорці
Бітові операції: AND (маскування), OR (встановлення), XOR (перемикання)
Бітові зсуви: SHL, SHR, SAL, SAR
AR KUs: AR-Assembly (Control instructions, ALU instructions, data manipulation), AR-Representation (Two's complement, overflow)
LO:&nbsp;LO2, LO3Тема:&nbsp;Стек, CALL/RET, стекові фрейми, конвенції виклику
Структура програми: сегменти коду, даних, стеку
Стек: принцип LIFO, SS:SP, напрямок росту
Операції PUSH/POP
Процедури: CALL і RET, адреса повернення
Стекові фрейми: BP, локальні змінні, параметри
Конвенції виклику: cdecl, передача параметрів
AR KUs:&nbsp;AR-Assembly (Subroutine call/return, stack segments)
SF KUs:&nbsp;SF-Foundations (State and state transition)
LO:&nbsp;LO3, LO4Тема:&nbsp;Архітектура фон Неймана, шини, ієрархія пам'яті, режими процесора
Закон Мура: емпіричне масштабування, сучасні приклади
Від інтегральних схем до мікропроцесорів: ІС, мікроконтролери, SoC
Архітектура фон Неймана: CPU, пам'ять, шина, I/O
Шини: data bus, address bus, control bus
Системний годинник
Ієрархія пам'яті: регістри → кеш → RAM → диск
Реальний vs захищений режим (огляд)
Порівняння з Гарвардською архітектурою
Цикл виконання інструкцій: fetch → decode → execute
Bottleneck фон Неймана: проблема та сучасні рішення (L1i/L1d, prefetch)
AR KUs:&nbsp;AR-Organization (Von Neumann architecture, buses), AR-Memory (Memory hierarchy)
SF KUs:&nbsp;SF-Performance (Memory hierarchy introduction)&nbsp;LO:&nbsp;LO1, LO4Тема:&nbsp;Процес завантаження, IVT, переривання BIOS/DOS, привілеї
Процес завантаження: POST → BIOS → MBR → OS
Таблиця векторів переривань (IVT): 0000:0000
Апаратні vs програмні переривання
INT та IRET: механізм виклику
BIOS-переривання: INT 10h, INT 16h
DOS-переривання: INT 21h
Кільця захисту (Ring 0–3)
AR KUs:&nbsp;AR-Assembly (I/O and interrupts), AR-IO (Interrupt structures)
OS KUs:&nbsp;OS-Principles (Privilege levels)
LO:&nbsp;LO4Тема:&nbsp;Типи пам'яті, I/O порти, віртуальна пам'ять (вступ)
Технології пам'яті: SRAM, DRAM, Flash, ROM
Характеристики: латентність, пропускна здатність, вартість
Пристрої вводу-виводу: контролери, порти
Інструкції IN/OUT
Відеопам'ять: B800:0000, текстовий режим
Віртуальна пам'ять: концепція, сторінки (вступ)
AR KUs:&nbsp;AR-Memory (Main memory, persistent memory), AR-IO (I/O devices, ports)
SF KUs:&nbsp;SF-Performance (Memory types)
LO:&nbsp;LO1, LO4, LO5Тема:&nbsp;RAM/RASP моделі, CISC vs RISC, таксономія Флінна
Формальні моделі: RAM-машина, RASP-машина
Класифікація ISA: стекова, акумуляторна, регістрова
CISC: x86, багато інструкцій, змінна довжина
RISC: ARM, RISC-V, фіксована довжина, багато регістрів
Таксономія Флінна: SISD, SIMD, MISD, MIMD
SMP та MMP системи
AR KUs:&nbsp;AR-Assembly (ISA design, CISC/RISC)
PDC KUs:&nbsp;PDC-Programs (SIMD/MIMD concepts)
LO:&nbsp;LO1, LO6, LO7Тема:&nbsp;GPU, DSP, ASIC, FPGA, TPU, нейроморфні та квантові обчислення
GPU: масивний паралелізм, SIMT, CUDA
DSP: обробка сигналів у реальному часі
ASIC: спеціалізовані мікросхеми
FPGA: програмована логіка
TPU: тензорні обчислення для ML
Нейроморфні процесори: Intel Loihi, IBM TrueNorth
Квантові обчислення: кубіти, алгоритми Шора та Гровера (огляд)
AR KUs:&nbsp;AR-Heterogeneity (Domain-specific architectures), AR-Quantum (Principles)
LO:&nbsp;LO1, LO6, LO7Тема:&nbsp;Кеші, локальність, когерентність кешу (MESI – концептуально)
Принцип ієрархії пам'яті: швидкість vs ємність vs вартість
Локальність звернень: темпоральна та просторова
Структура кешу: рядки, теги, множини, асоціативність
Політики заміщення: LRU, FIFO, Random
Політики запису: write-through vs write-back
Когерентність кешу в багатопроцесорних системах: протокол MESI
Формула AMAT: Average Memory Access Time
AR KUs:&nbsp;AR-Memory (Cache memories, Multiprocessor cache coherence, Temporal/spatial locality)
SF KUs:&nbsp;SF-Performance (Caches and cache coherency, Formula for average memory access time)
LO:&nbsp;LO5Тема:&nbsp;Сторінкова організація, TLB, трансляція адрес, page faults
Концепція віртуальної пам'яті: ізоляція, абстракція, ефективність
Сторінкова організація: сторінки, фрейми, таблиці сторінок
Багаторівневі таблиці сторінок: економія пам'яті
TLB (Translation Lookaside Buffer): кешування трансляцій
Трансляція адрес: віртуальна → фізична, алгоритм
Page faults: типи, обробка, page replacement algorithms
Захист пам'яті: біти доступу, NX-біт
AR KUs:&nbsp;AR-Memory (Virtual memory hardware support, Fault handling)
OS KUs:&nbsp;OS-Memory (Virtual memory, Paging, TLB)
SF KUs:&nbsp;SF-Performance (Levels of indirection, Methods for implementing virtual memory)
LO:&nbsp;LO4, LO5Тема:&nbsp;IEEE 754 детально, режими округлення, числові помилки, x87 vs SIMD
Стандарт IEEE 754: формати single, double, extended
Структура: знак, експонента, мантиса
Спеціальні значення: ±0, ±∞, NaN, денормалізовані числа
Арифметичні операції: додавання, множення, ділення
Режими округлення: to nearest, toward zero, up, down
Числові помилки: втрата точності, catastrophic cancellation
x87 FPU vs SSE/AVX: історія та порівняння
AR KUs:&nbsp;AR-Representation (Fixed-point, Floating-point representation)
SF KUs:&nbsp;SF-Reliability (Error detection and correction)
LO:&nbsp;LO2, LO5Тема:&nbsp;Конвеєризація, hazards, суперскалярне виконання, SMT
Конвеєризація: етапи (IF, ID, EX, MEM, WB), throughput vs latency
Hazards: структурні, data hazards (RAW, WAR, WAW), control hazards
Вирішення hazards: forwarding, stalls, branch prediction
Суперскалярне виконання: кілька інструкцій за такт
Out-of-order execution: алгоритм Томасуло, reorder buffer
Спекулятивне виконання: переваги та ризики (Spectre/Meltdown)
SMT (Simultaneous Multithreading): Hyper-Threading
AR KUs:&nbsp;AR-Organization (Instruction pipelining, ILP, hazard detection), AR-Performance-Energy (Branch prediction, speculative execution, Hardware support for multithreading)
PDC KUs:&nbsp;PDC-Programs (Thread-level parallelism)
LO:&nbsp;LO5, LO6Тема:&nbsp;Закон Амдала (переглянуто), dark silicon, гетерогенність, сталий розвиток
Закон Амдала: межі паралелізму, сильне vs слабке масштабування
Закон Густафсона: альтернативний погляд на масштабування
Енергоспоживання: динамічне vs статичне, P = CV²f
DVFS: динамічне керування напругою та частотою
Dark silicon: проблема тепловідведення, частково активні чіпи
Гетерогенні обчислення: big.LITTLE, CPU+GPU+NPU
Сталий розвиток: вуглецевий слід, e-waste, енергоефективні дата-центри
Майбутні тенденції: chiplets, 3D IC, нейроморфні системи
AR KUs:&nbsp;AR-Performance-Energy (DVFS, Dark Silicon, Scalability), AR-SEP (Environmental impacts, Carbon footprint, Energy footprint of data centers)
SF KUs:&nbsp;SF-Evaluation (Amdahl's Law), SF-Design (Common criteria: scalability, robustness)
SEP KUs:&nbsp;SEP-Sustainability
LO:&nbsp;LO5, LO6, LO7
Середовище: Digital Logic Sim (тиждень 1), потім TASM, TLINK, Turbo Debugger у DOSBox
Мета: Від логічних вентилів до повної прозорості системи – IVT, BIOS Data Area, відеопам'ять
Тема: Симулятор цифрової логіки, побудова базових елементів, налаштування репозиторію
Встановлення Digital Logic Sim (Sebastian Lague)
Побудова базових вентилів: OR, NAND, XOR із примітивів
Самостійна побудова 4-бітного суматора (за відео)
Web-альтернатива: Falstad Circuit Simulator
Створення приватного GitHub репозиторію за вимогами курсу
Запрошення викладача до репозиторію
ДЗ: відео-демонстрація XOR та 4-bit adder у Loom
AR KUs: AR-Logic (Combinational logic, Building blocks)
SE KUs: SE-Tools (Version control)
LO: LO1Тема: Комбінаційна та секвенційна логіка, суматори, елементи пам'яті, 4-бітний ALU
Комбінаційна vs секвенційна логіка: без пам'яті vs з пам'яттю
Двійкова арифметика: unsigned, signed (two's complement)
Half Adder: S = A XOR B, C = A AND B
Full Adder: 2 × Half Adder + OR, carry chain
4-bit Adder: каскадування Full Adder
SR Latch: cross-coupled OR+AND, Set/Reset, forbidden state
D Latch: Data + Write Enable
Binary Decoder (2-to-4): n входів → 2^n виходів (one-hot)
4-bit ALU: ADD, SUB, INC, DEC з вибором операції через opcode
ДЗ: відео-демонстрація структури компонентів (drill-down) та роботи ALU
AR KUs: AR-Logic (Combinational vs sequential logic, Building blocks)
SF KUs: SF-Foundations (Clocks, State, Sequencing)
LO: LO1Тема: Встановлення середовища, регістри 8086, переміщення даних, базова арифметикаЧастина 1: Налаштування середовища
Встановлення DOSBox, монтування робочої директорії
Структура TASM: tasm.exe, tlink.exe, td.exe
Перша програма: .COM файл, що завершується через INT 20h
Компіляція та запуск: tasm → tlink → виконання
Знайомство з Turbo Debugger: вікна CPU, Registers, Stack, Dump
Базові команди TD: F7 (step), F8 (step over), F9 (run), breakpoints
Частина 2: Регістри та арифметика
Регістри 8086: AX (AH, AL), BX, CX, DX, SI, DI, BP, SP
Сегментні регістри: CS, DS, SS, ES
Спостереження регістрів у реальному часі в TD
Інструкції MOV: між регістрами, з пам'яттю, immediate
Інструкція XOR для очищення регістрів
Арифметика: ADD, SUB, INC, DEC, NEG – спостереження FLAGS
Практичне завдання: обчислення арифметичного виразу з використанням регістрів
AR KUs: AR-Assembly (Toolchain overview, Instruction formats, Data manipulation), AR-Representation (Bits, bytes, words)
SE KUs: SE-Tools (Development environments)
LO: LO1, LO2, LO3Тема: FLAGS, умовні переходи, цикли, MUL/DIV, переповненняЧастина 1: Потік керування
Регістр FLAGS: CF, ZF, SF, OF – значення та візуалізація в TD
Інструкції CMP, TEST: як вони впливають на прапорці
Умовні переходи: JE, JNE, JG, JL, JA, JB, JC, JO
Безумовний перехід: JMP (short, near)
Реалізація if-else, switch на асемблері
Цикли: LOOP, власна реалізація через CMP/JNE
Частина 2: Цілочисельна арифметика MUL: беззнакове множення, результат у DX:AX IMUL: знакове множення, варіанти (1, 2, 3 операнди) DIV: беззнакове ділення, частка в AL/AX, остача в AH/DX IDIV: знакове ділення CBW, CWD: знакове розширення перед діленням Виявлення переповнення: CF, OF Практичне завдання: калькулятор з циклом та обробкою переповнення AR KUs: AR-Assembly (Control instructions, Machine language programming), AR-Representation (Unsigned, signed, two's complement)
SEC KUs: SEC-Coding (Integer overflow vulnerabilities)
LO: LO2, LO3Тема: PUSH/POP, CALL/RET, стекові фрейми, передача параметрів, бітові маски
Стек у реальному режимі: SS:SP, напрямок росту
Візуалізація стеку у Turbo Debugger
PUSH/POP, обмін та ротація значень
Бітові маски: AND, OR, TEST, SHR — виділення нібблів, перевірка та встановлення бітів
CALL/RET, адреса повернення
Стековий фрейм: BP, параметри через стек
Трасування стеку: передбачення стану стеку без виконання
Домашнє завдання: підрахунок бітів (процедура), рекурсивний факторіал, бонус — рекурсивний Фібоначчі
AR KUs: AR-Assembly (Subroutine call and return mechanisms)
LO: LO3, LO4Тема: Режими адресації, робота з масивами, вікно Dump в TD
Режими адресації: [BX], [SI], [BX+SI], [BX+SI+disp]
Оголошення масивів: DB, DW, DD у сегменті даних
Візуалізація масиву у вікні Dump (Alt+D)
Обчислення ефективної адреси: LEA
Обхід масиву: з індексом (SI) та з вказівником (BX)
Багатовимірні масиви: row-major, обчислення індексу
Практичне завдання: сортування бульбашкою з візуалізацією
AR KUs: AR-Assembly (Addressing modes), AR-Representation (Representation of arrays)
LO: LO2, LO3Тема: IVT, BIOS Data Area, виклик BIOS-функцій
Структура IVT: 256 векторів × 4 байти за адресою 0000:0000
Читання вектора переривання в TD (Dump 0000:0000)
BIOS Data Area (0040:0000): keyboard buffer, timer ticks
Виклик BIOS: INT 10h (відео), INT 16h (клавіатура)
Виклик DOS: INT 21h (функції 01h, 02h, 09h, 4Ch)
Демонстрація: підміна вектора переривання (hook), відновлення
Практичне завдання: власний обробник клавіатури
AR KUs: AR-Assembly (I/O and interrupts), AR-IO (Interrupt structures)
OS KUs: OS-Principles (Interrupt handling)
LO: LO4Тема: Текстовий режим, B800:0000, атрибути символів
Відеопам'ять текстового режиму: B800:0000 (CGA/EGA/VGA)
Структура: символ (молодший байт) + атрибут (старший байт)
Атрибути: колір тексту (0–15), колір фону (0–7), blink
Обчислення позиції: offset = (row × 80 + col) × 2
Пряме записування у відеопамʼять – миттєвий візуальний результат
Порівняння: INT 10h vs пряма робота з памʼяттю (швидкість)
Практичне завдання: кольорове виведення, рамка, анімація
AR KUs: AR-IO (I/O devices), AR-Memory (Memory-mapped I/O concept)
LO: LO3, LO4Тема: COM vs EXE, PSP, завантаження програм, аргументи командного рядка
Формати виконуваних файлів DOS: COM: обмеження 64KB, єдиний сегмент, ORG 100h
EXE: заголовок MZ, релокації, множинні сегменти Program Segment Prefix (PSP): структура PSP (256 байт перед програмою)
INT 20h за адресою PSP:0000
командний рядок за зміщенням 80h (довжина + текст)
environment segment за зміщенням 2Ch Як DOS завантажує програму в памʼять: пошук вільного блоку MCB (Memory Control Block)
створення PSP, копіювання environment
завантаження коду, налаштування сегментних регістрів Практичне завдання: програма, що читає та обробляє аргументи командного рядка
Завершальний етап роботи в DOS перед практичним контролем (P10) та переходом до Linux (P11–P15).AR KUs: AR-Assembly (Program structure, DOS internals), AR-Memory (Memory layout)
LO: LO1, LO4Тема: Контрольне завдання за матеріалами практик P3–P9
Індивідуальне завдання, що перевіряє: роботу з регістрами та арифметикою;
керування потоком виконання (умови, цикли);
використання стеку та процедур;
масиви та режими адресації;
переривання BIOS/DOS;
роботу з відеопамʼяттю;
налагодження коду у Turbo Debugger. Коротке письмове пояснення логіки програми.
Лабораторне заняття повністю присвячене виконанню контрольного завдання. Практичний контроль II проводиться на занятті №15 (див. п. 4.3.3).AR KUs: AR-Assembly (integrated low-level programming)
LO: LO2, LO3, LO4
Середовище: NASM, ld, GDB + GEF/pwndbg, perf, Valgrind на Linux (Codespaces/WSL)
Мета: Перехід до сучасної архітектури, інструменти аналізу продуктивності, зв'язок із реальними системами
Тема: NASM/Linux, GDB+GEF, вимірювання cache performance
Встановлення NASM, GDB, GEF/pwndbg у Codespaces/WSL
Перша програма x86-64: syscall замість INT 21h
Порівняння: 16-bit сегментна модель vs 64-bit flat model
Регістри x86-64: RAX, RBX... R8-R15, розширення
Інструменти профілювання: perf stat, cachegrind
Експеримент: послідовний vs випадковий доступ до масиву
Вимірювання cache hits/misses, аналіз результатів
Практичне завдання: оптимізація множення матриць
AR KUs: AR-Memory (Cache memories, Locality), AR-Performance-Energy (Performance evaluation)
SF KUs: SF-Performance (Caches and effects of locality), SF-Evaluation (Performance figures of merit)
LO: LO5Тема: Адресний простір процесу, page faults, ASLR
Аналіз /proc/[pid]/maps: сегменти, бібліотеки, stack, heap
Інструменти: pmap, vmstat, /proc/meminfo
ASLR: Address Space Layout Randomization – демонстрація
Вимірювання page faults: perf stat -e page-faults
Minor vs major page faults: коли виникають
(Демо) Huge pages (2MB): концепція та вплив на TLB
Практичне завдання: порівняння page faults для послідовного vs випадкового доступу до памʼяті
AR KUs: AR-Memory (Virtual memory hardware support)
OS KUs: OS-Memory (Address space, Page faults)
SF KUs: SF-Performance (Virtual memory implementation)
LO: LO4, LO5Тема: IEEE 754 на практиці, помилки округлення, catastrophic cancellation
Дослідження бітового представлення float/double (Python struct)
Експеримент 1: machine epsilon – найменша відносна похибка
Експеримент 2: (a+b)+c ≠ a+(b+c) – втрата асоціативності
Експеримент 3: catastrophic cancellation (x - y при x ≈ y)
Експеримент 4: накопичення помилок у циклі підсумовування
Практичне завдання: реалізація Kahan summation та порівняння точності з наївним підсумовуванням
AR KUs: AR-Representation (Floating-point representation and precision)
SF KUs: SF-Reliability (Error detection)
LO: LO2, LO5Тема: Розширення оптимізації матричного множення з P11 за допомогою SIMD
Огляд SIMD: SSE (128-bit), AVX (256-bit) — обробка кількох елементів за такт
Два шляхи до SIMD: Auto-vectorization: -O3 -march=native -ftree-vectorize, аналіз асемблерного виводу
Manual intrinsics: _mm256_add_ps, _mm256_mul_ps (демонстрація на простому прикладі) Перевірка підтримки: /proc/cpuinfo, прапорці sse, avx, avx2
Вимірювання: порівняння baseline → cache-optimized (P11) → cache+SIMD
Обмеження: alignment (alignas(32)), data dependencies
Практичне завдання: додати SIMD-оптимізацію до матричного множення з P11 (intrinsics або auto-vec)
AR KUs: AR-Heterogeneity (SIMD architectures), AR-Performance-Energy (Vector processors)
PDC KUs: PDC-Programs (SIMD programming)
LO: LO5, LO6Тема: Практичний контроль II (обидва курси) та захист підсумкового проєкту (6 ECTS)Ця лабораторна робота поєднує:
практичний контроль II (обидва курси): індивідуальне контрольне завдання за матеріалами P11–P14;
захист підсумкового проєкту (6 ECTS): демонстрація програми TASM/DOSBox, Q&amp;A від інструктора (~8–10 хв на проєкт).
Контрольний компонент (обидва курси):
аналіз baseline-реалізації;
профілювання (perf, cachegrind);
обґрунтування застосованих оптимізацій;
демонстрація отриманого прискорення.
Проєктний компонент (6 ECTS):
демонстрація працюючої програми;
пояснення архітектурних рішень та організації пам'яті;
для парних проєктів: індивідуальні запитання кожному учаснику, у тому числі про частини, написані партнером.
<br>
Детальні вимоги до проєкту (6 ECTS) див. у <a class="internal-link" data-href="Labs/Project.md" href="labs/project.html" target="_self" rel="noopener nofollow">Project.md</a>.
LO: LO3, LO5, LO7
Distedu: Завдання, план лекцій+лаб, матеріали, оцінки.
MS Teams&nbsp;є основним каналом комунікації. Використовується для&nbsp;групових оголошень&nbsp;та&nbsp;групового чату.
Прохання уникати приватних (p2p) чатів: всі питання, що можуть бути корисними для групи, ставте у груповому чаті. Електронна пошта&nbsp;використовується для&nbsp;особистих та індивідуальних питань. Відповіді надаються протягом робочих днів. Консультаційні години (office hours): щосуботи о 15:00, зустріч у команді MS Teams. Участь є&nbsp;опційною.
Рекомендується приєднуватися, якщо у вас є питання щодо курсу, завдань чи матеріалу. Виконання навчальних завдань і робота в курсі має відповідати вимогам «Положення про Академічну доброчесність здобувачів освіти у НаУКМА» (затверджене наказом № 112 від 07.03.2018 року)<br><a rel="noopener nofollow" class="external-link is-unresolved" href="https://www.ukma.edu.ua/index.php/about-us/sogodennya/dokumenty-naukma/cat_view/1-dokumenty-naukma/12-normatyvna-baza-naukma/6-systema-zabezpechennia-iakosti-osvitnoi-diialnosti-ta-iakosti-vyshchoi-osvity/71-normatyvni-dokumenty" target="_self">https://www.ukma.edu.ua/index.php/about-us/sogodennya/dokumenty-naukma/cat_view/1-dokumenty-naukma/12-normatyvna-baza-naukma/6-systema-zabezpechennia-iakosti-osvitnoi-diialnosti-ta-iakosti-vyshchoi-osvity/71-normatyvni-dokumenty</a>
<br>(⭐) Hall, B. R. &amp; Slonka, K. J. (2020). <a data-tooltip-position="top" aria-label="https://www.prospectpressvt.com/textbooks/hall-assembly-programming-2-0" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.prospectpressvt.com/textbooks/hall-assembly-programming-2-0" target="_self">Assembly Programming and Computer Architecture for Software Engineers</a> (2nd ed.)
<br>(⭐) Penn Brumm, Don Brumm (1989). 80386, a programming and design handbook (<a data-tooltip-position="top" aria-label="https://archive.org/details/80386programming0000brum/mode/2up" rel="noopener nofollow" class="external-link is-unresolved" href="https://archive.org/details/80386programming0000brum/mode/2up" target="_self">archive.org</a>; <a data-tooltip-position="top" aria-label="https://libarch.nmu.org.ua/handle/GenofondUA/81646" rel="noopener nofollow" class="external-link is-unresolved" href="https://libarch.nmu.org.ua/handle/GenofondUA/81646" target="_self">рос. переклад</a>)
<br>Paul A. Carter. <a data-tooltip-position="top" aria-label="https://www.scs.stanford.edu/05au-cs240c/lab/pcasm-book.pdf" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.scs.stanford.edu/05au-cs240c/lab/pcasm-book.pdf" target="_self">PC Assembly Language</a>
Тарарака В.Д. Архітектура комп'ютерних систем: навчальний посібник. – Житомир: ЖДТУ, 2018. – 383 с. Олецький О.В. Принципи роботи комп'ютерних систем: Навч. посібник. – К.: Вид.дім «КМ Академія», 2003.
<br>John L. Hennessy, David A. Patterson (2017). <a data-tooltip-position="top" aria-label="https://www.amazon.com/Computer-Architecture-Quantitative-Approach-Kaufmann/dp/0128119055" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.amazon.com/Computer-Architecture-Quantitative-Approach-Kaufmann/dp/0128119055" target="_self">Computer Architecture: A Quantitative Approach</a> (6th ed.)
<br>Daniel Kusswurm (2023). <a data-tooltip-position="top" aria-label="https://www.amazon.com/Modern-X86-Assembly-Language-Programming-dp-1484296028/dp/1484296028" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.amazon.com/Modern-X86-Assembly-Language-Programming-dp-1484296028/dp/1484296028" target="_self">Modern X86 Assembly Language Programming</a>: Covers X86 64-bit, AVX, AVX2, and AVX-512
<br>Tom Swan. <a data-tooltip-position="top" aria-label="https://www.amazon.com/exec/obidos/ASIN/0672305267/acmorg-20" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.amazon.com/exec/obidos/ASIN/0672305267/acmorg-20" target="_self">Mastering Turbo Assembler</a> (2nd ed.) — <a data-tooltip-position="top" aria-label="https://archive.org/details/bitsavers_borlandtureringTurboAssembler2ed1995_80572557" rel="noopener nofollow" class="external-link is-unresolved" href="https://archive.org/details/bitsavers_borlandtureringTurboAssembler2ed1995_80572557" target="_self">онлайн-версія</a>
<br>(⭐) <a data-tooltip-position="top" aria-label="https://github.com/Captainarash/The_Holy_Book_of_X86/blob/master/book_vol_1.txt" rel="noopener nofollow" class="external-link is-unresolved" href="https://github.com/Captainarash/The_Holy_Book_of_X86/blob/master/book_vol_1.txt" target="_self">The Holy Book of X86, Vol. 1</a>, <a data-tooltip-position="top" aria-label="https://github.com/Captainarash/The_Holy_Book_of_X86/blob/master/book-vol-2.pdf" rel="noopener nofollow" class="external-link is-unresolved" href="https://github.com/Captainarash/The_Holy_Book_of_X86/blob/master/book-vol-2.pdf" target="_self">Vol. 2</a> (для поглибленого вивчення)
Глибовець М.М. «Алгоритми та структури даних», с. 15–39 <br><a data-tooltip-position="top" aria-label="https://www.cs.virginia.edu/~evans/cs216/guides/x86.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.cs.virginia.edu/~evans/cs216/guides/x86.html" target="_self">Guide to x86 Assembly</a> — компактний довідник з основних інструкцій
<br><a data-tooltip-position="top" aria-label="http://bitsavers.informatik.uni-stuttgart.de/pdf/borland/turbo_assembler/Turbo_Assembler_Version_5_Users_Guide.pdf" rel="noopener nofollow" class="external-link is-unresolved" href="http://bitsavers.informatik.uni-stuttgart.de/pdf/borland/turbo_assembler/Turbo_Assembler_Version_5_Users_Guide.pdf" target="_self">Turbo Assembler Version 5 User's Guide</a> — офіційна документація TASM
<br><a data-tooltip-position="top" aria-label="https://bitsavers.org/pdf/borland/turbo_debugger/Turbo_Debugger_3.0_for_Windows_Users_Guide_1991.pdf" rel="noopener nofollow" class="external-link is-unresolved" href="https://bitsavers.org/pdf/borland/turbo_debugger/Turbo_Debugger_3.0_for_Windows_Users_Guide_1991.pdf" target="_self">Turbo Debugger 3.0 User's Guide</a> — документація відлагоджувача
<br><a data-tooltip-position="top" aria-label="https://www.nasm.us/xdoc/2.16.01/html/nasmdoc0.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.nasm.us/xdoc/2.16.01/html/nasmdoc0.html" target="_self">NASM Documentation</a> — документація асемблера для Linux-частини курсу <br><a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/microprocessor-tutorials/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/microprocessor-tutorials/" target="_self">Microprocessor Tutorials (GeeksforGeeks)</a>
<br><a data-tooltip-position="top" aria-label="https://hpc-wiki.info/hpc/Computer_architecture_for_software_developers" rel="noopener nofollow" class="external-link is-unresolved" href="https://hpc-wiki.info/hpc/Computer_architecture_for_software_developers" target="_self">Computer architecture for software developers (HPC Wiki)</a> <br><a data-tooltip-position="top" aria-label="https://github.com/brianrhall/Assembly/" rel="noopener nofollow" class="external-link is-unresolved" href="https://github.com/brianrhall/Assembly/" target="_self">github.com/brianrhall/Assembly</a> — приклади з книги Hall &amp; Slonka
<br><a data-tooltip-position="top" aria-label="https://github.com/TomSwan/collections/raw/master/data/mastering-turbo-assembler2.zip" rel="noopener nofollow" class="external-link is-unresolved" href="https://github.com/TomSwan/collections/raw/master/data/mastering-turbo-assembler2.zip" target="_self">Mastering Turbo Assembler examples</a> — приклади з книги Swan Примітка: Матеріали курсу (завдання, слайди, оцінки) розміщені в системі Distedu. Позначені (⭐) джерела особливо рекомендовані для поглибленого розуміння.
Силабус затверджено на засіданні кафедри інформатики
Факультету інформатики НаУКМА
«_» __ 202 р. (Протокол № _)Завідувач кафедри: __ / ___ /]]></description><link>syllabus.html</link><guid isPermaLink="false">Syllabus.md</guid><pubDate>Tue, 24 Feb 2026 17:20:37 GMT</pubDate><enclosure url="assets/ukma-logo.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/ukma-logo.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y23-Screen-Monthly]]></title><description><![CDATA[<img src="assets/y23-screen-monthly.png" target="_self">]]></description><link>assets/y23-screen-monthly.html</link><guid isPermaLink="false">assets/Y23-Screen-Monthly.png</guid><pubDate>Tue, 24 Feb 2026 15:53:47 GMT</pubDate><enclosure url="assets/y23-screen-monthly.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y23-screen-monthly.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[R05-Screen-Gameplay]]></title><description><![CDATA[<img src="assets/r05-screen-gameplay.png" target="_self">]]></description><link>assets/r05-screen-gameplay.html</link><guid isPermaLink="false">assets/R05-Screen-Gameplay.png</guid><pubDate>Tue, 24 Feb 2026 15:53:32 GMT</pubDate><enclosure url="assets/r05-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/r05-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[R05-Screen-Install]]></title><description><![CDATA[<img src="assets/r05-screen-install.png" target="_self">]]></description><link>assets/r05-screen-install.html</link><guid isPermaLink="false">assets/R05-Screen-Install.png</guid><pubDate>Tue, 24 Feb 2026 15:53:30 GMT</pubDate><enclosure url="assets/r05-screen-install.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/r05-screen-install.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[R04-Screen-GameOver]]></title><description><![CDATA[<img src="assets/r04-screen-gameover.png" target="_self">]]></description><link>assets/r04-screen-gameover.html</link><guid isPermaLink="false">assets/R04-Screen-GameOver.png</guid><pubDate>Tue, 24 Feb 2026 15:53:30 GMT</pubDate><enclosure url="assets/r04-screen-gameover.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/r04-screen-gameover.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y22-Screen-Zoomed]]></title><description><![CDATA[<img src="assets/y22-screen-zoomed.png" target="_self">]]></description><link>assets/y22-screen-zoomed.html</link><guid isPermaLink="false">assets/Y22-Screen-Zoomed.png</guid><pubDate>Tue, 24 Feb 2026 15:53:30 GMT</pubDate><enclosure url="assets/y22-screen-zoomed.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y22-screen-zoomed.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[R01-Screen-Gameplay]]></title><description><![CDATA[<img src="assets/r01-screen-gameplay.png" target="_self">]]></description><link>assets/r01-screen-gameplay.html</link><guid isPermaLink="false">assets/R01-Screen-Gameplay.png</guid><pubDate>Tue, 24 Feb 2026 15:53:29 GMT</pubDate><enclosure url="assets/r01-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/r01-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[R06-Screen-Win]]></title><description><![CDATA[<img src="assets/r06-screen-win.png" target="_self">]]></description><link>assets/r06-screen-win.html</link><guid isPermaLink="false">assets/R06-Screen-Win.png</guid><pubDate>Tue, 24 Feb 2026 15:53:29 GMT</pubDate><enclosure url="assets/r06-screen-win.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/r06-screen-win.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y23-Screen-DayView]]></title><description><![CDATA[<img src="assets/y23-screen-dayview.png" target="_self">]]></description><link>assets/y23-screen-dayview.html</link><guid isPermaLink="false">assets/Y23-Screen-DayView.png</guid><pubDate>Tue, 24 Feb 2026 15:53:28 GMT</pubDate><enclosure url="assets/y23-screen-dayview.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y23-screen-dayview.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[R04-Screen-Gameplay]]></title><description><![CDATA[<img src="assets/r04-screen-gameplay.png" target="_self">]]></description><link>assets/r04-screen-gameplay.html</link><guid isPermaLink="false">assets/R04-Screen-Gameplay.png</guid><pubDate>Tue, 24 Feb 2026 15:53:28 GMT</pubDate><enclosure url="assets/r04-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/r04-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y22-Screen-Viewer]]></title><description><![CDATA[<img src="assets/y22-screen-viewer.png" target="_self">]]></description><link>assets/y22-screen-viewer.html</link><guid isPermaLink="false">assets/Y22-Screen-Viewer.png</guid><pubDate>Tue, 24 Feb 2026 15:53:28 GMT</pubDate><enclosure url="assets/y22-screen-viewer.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y22-screen-viewer.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[R01-Screen-GameOver]]></title><description><![CDATA[<img src="assets/r01-screen-gameover.png" target="_self">]]></description><link>assets/r01-screen-gameover.html</link><guid isPermaLink="false">assets/R01-Screen-GameOver.png</guid><pubDate>Tue, 24 Feb 2026 15:53:27 GMT</pubDate><enclosure url="assets/r01-screen-gameover.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/r01-screen-gameover.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y19-Screen-List]]></title><description><![CDATA[<img src="assets/y19-screen-list.png" target="_self">]]></description><link>assets/y19-screen-list.html</link><guid isPermaLink="false">assets/Y19-Screen-List.png</guid><pubDate>Tue, 24 Feb 2026 15:53:27 GMT</pubDate><enclosure url="assets/y19-screen-list.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y19-screen-list.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[R06-Screen-Gameplay]]></title><description><![CDATA[<img src="assets/r06-screen-gameplay.png" target="_self">]]></description><link>assets/r06-screen-gameplay.html</link><guid isPermaLink="false">assets/R06-Screen-Gameplay.png</guid><pubDate>Tue, 24 Feb 2026 15:53:26 GMT</pubDate><enclosure url="assets/r06-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/r06-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y19-Screen-Form]]></title><description><![CDATA[<img src="assets/y19-screen-form.png" target="_self">]]></description><link>assets/y19-screen-form.html</link><guid isPermaLink="false">assets/Y19-Screen-Form.png</guid><pubDate>Tue, 24 Feb 2026 15:52:43 GMT</pubDate><enclosure url="assets/y19-screen-form.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y19-screen-form.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y21-Screen-Tools]]></title><description><![CDATA[<img src="assets/y21-screen-tools.png" target="_self">]]></description><link>assets/y21-screen-tools.html</link><guid isPermaLink="false">assets/Y21-Screen-Tools.png</guid><pubDate>Tue, 24 Feb 2026 15:52:43 GMT</pubDate><enclosure url="assets/y21-screen-tools.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y21-screen-tools.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[R02-Screen-GameOver]]></title><description><![CDATA[<img src="assets/r02-screen-gameover.png" target="_self">]]></description><link>assets/r02-screen-gameover.html</link><guid isPermaLink="false">assets/R02-Screen-GameOver.png</guid><pubDate>Tue, 24 Feb 2026 15:52:41 GMT</pubDate><enclosure url="assets/r02-screen-gameover.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/r02-screen-gameover.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[R03-Screen-SaveDialog]]></title><description><![CDATA[<img src="assets/r03-screen-savedialog.png" target="_self">]]></description><link>assets/r03-screen-savedialog.html</link><guid isPermaLink="false">assets/R03-Screen-SaveDialog.png</guid><pubDate>Tue, 24 Feb 2026 15:52:41 GMT</pubDate><enclosure url="assets/r03-screen-savedialog.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/r03-screen-savedialog.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y21-Screen-Editor]]></title><description><![CDATA[<img src="assets/y21-screen-editor.png" target="_self">]]></description><link>assets/y21-screen-editor.html</link><guid isPermaLink="false">assets/Y21-Screen-Editor.png</guid><pubDate>Tue, 24 Feb 2026 15:52:40 GMT</pubDate><enclosure url="assets/y21-screen-editor.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y21-screen-editor.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[R02-Screen-Gameplay]]></title><description><![CDATA[<img src="assets/r02-screen-gameplay.png" target="_self">]]></description><link>assets/r02-screen-gameplay.html</link><guid isPermaLink="false">assets/R02-Screen-Gameplay.png</guid><pubDate>Tue, 24 Feb 2026 15:52:29 GMT</pubDate><enclosure url="assets/r02-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/r02-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[R03-Screen-Editing]]></title><description><![CDATA[<img src="assets/r03-screen-editing.png" target="_self">]]></description><link>assets/r03-screen-editing.html</link><guid isPermaLink="false">assets/R03-Screen-Editing.png</guid><pubDate>Tue, 24 Feb 2026 15:52:23 GMT</pubDate><enclosure url="assets/r03-screen-editing.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/r03-screen-editing.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y20-Screen-Reveal]]></title><description><![CDATA[<img src="assets/y20-screen-reveal.png" target="_self">]]></description><link>assets/y20-screen-reveal.html</link><guid isPermaLink="false">assets/Y20-Screen-Reveal.png</guid><pubDate>Tue, 24 Feb 2026 15:52:15 GMT</pubDate><enclosure url="assets/y20-screen-reveal.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y20-screen-reveal.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y20-Screen-Main]]></title><description><![CDATA[<img src="assets/y20-screen-main.png" target="_self">]]></description><link>assets/y20-screen-main.html</link><guid isPermaLink="false">assets/Y20-Screen-Main.png</guid><pubDate>Tue, 24 Feb 2026 15:52:07 GMT</pubDate><enclosure url="assets/y20-screen-main.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y20-screen-main.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y20-Screen-Login]]></title><description><![CDATA[<img src="assets/y20-screen-login.png" target="_self">]]></description><link>assets/y20-screen-login.html</link><guid isPermaLink="false">assets/Y20-Screen-Login.png</guid><pubDate>Tue, 24 Feb 2026 15:51:57 GMT</pubDate><enclosure url="assets/y20-screen-login.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y20-screen-login.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y14-Screen-Editor]]></title><description><![CDATA[<img src="assets/y14-screen-editor.png" target="_self">]]></description><link>assets/y14-screen-editor.html</link><guid isPermaLink="false">assets/Y14-Screen-Editor.png</guid><pubDate>Tue, 24 Feb 2026 15:44:35 GMT</pubDate><enclosure url="assets/y14-screen-editor.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y14-screen-editor.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y08-Screen-GameOver]]></title><description><![CDATA[<img src="assets/y08-screen-gameover.png" target="_self">]]></description><link>assets/y08-screen-gameover.html</link><guid isPermaLink="false">assets/Y08-Screen-GameOver.png</guid><pubDate>Tue, 24 Feb 2026 15:43:21 GMT</pubDate><enclosure url="assets/y08-screen-gameover.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y08-screen-gameover.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y17-Screen-Solved]]></title><description><![CDATA[<img src="assets/y17-screen-solved.png" target="_self">]]></description><link>assets/y17-screen-solved.html</link><guid isPermaLink="false">assets/Y17-Screen-Solved.png</guid><pubDate>Tue, 24 Feb 2026 15:43:20 GMT</pubDate><enclosure url="assets/y17-screen-solved.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y17-screen-solved.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y08-Screen-Gameplay]]></title><description><![CDATA[<img src="assets/y08-screen-gameplay.png" target="_self">]]></description><link>assets/y08-screen-gameplay.html</link><guid isPermaLink="false">assets/Y08-Screen-Gameplay.png</guid><pubDate>Tue, 24 Feb 2026 15:43:20 GMT</pubDate><enclosure url="assets/y08-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y08-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y17-Screen-Generation]]></title><description><![CDATA[<img src="assets/y17-screen-generation.png" target="_self">]]></description><link>assets/y17-screen-generation.html</link><guid isPermaLink="false">assets/Y17-Screen-Generation.png</guid><pubDate>Tue, 24 Feb 2026 15:43:19 GMT</pubDate><enclosure url="assets/y17-screen-generation.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y17-screen-generation.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y08-Screen-Placement]]></title><description><![CDATA[<img src="assets/y08-screen-placement.png" target="_self">]]></description><link>assets/y08-screen-placement.html</link><guid isPermaLink="false">assets/Y08-Screen-Placement.png</guid><pubDate>Tue, 24 Feb 2026 15:43:18 GMT</pubDate><enclosure url="assets/y08-screen-placement.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y08-screen-placement.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y13-Screen-Evolution]]></title><description><![CDATA[<img src="assets/y13-screen-evolution.png" target="_self">]]></description><link>assets/y13-screen-evolution.html</link><guid isPermaLink="false">assets/Y13-Screen-Evolution.png</guid><pubDate>Tue, 24 Feb 2026 15:43:17 GMT</pubDate><enclosure url="assets/y13-screen-evolution.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y13-screen-evolution.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y14-Screen-Viewer]]></title><description><![CDATA[<img src="assets/y14-screen-viewer.png" target="_self">]]></description><link>assets/y14-screen-viewer.html</link><guid isPermaLink="false">assets/Y14-Screen-Viewer.png</guid><pubDate>Tue, 24 Feb 2026 15:42:41 GMT</pubDate><enclosure url="assets/y14-screen-viewer.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y14-screen-viewer.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y16-Screen-Playback]]></title><description><![CDATA[<img src="assets/y16-screen-playback.png" target="_self">]]></description><link>assets/y16-screen-playback.html</link><guid isPermaLink="false">assets/Y16-Screen-Playback.png</guid><pubDate>Tue, 24 Feb 2026 15:42:24 GMT</pubDate><enclosure url="assets/y16-screen-playback.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y16-screen-playback.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y11-Screen-Gameplay]]></title><description><![CDATA[<img src="assets/y11-screen-gameplay.png" target="_self">]]></description><link>assets/y11-screen-gameplay.html</link><guid isPermaLink="false">assets/Y11-Screen-Gameplay.png</guid><pubDate>Tue, 24 Feb 2026 15:42:21 GMT</pubDate><enclosure url="assets/y11-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y11-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y15-Screen-Visualization]]></title><description><![CDATA[<img src="assets/y15-screen-visualization.png" target="_self">]]></description><link>assets/y15-screen-visualization.html</link><guid isPermaLink="false">assets/Y15-Screen-Visualization.png</guid><pubDate>Tue, 24 Feb 2026 15:41:54 GMT</pubDate><enclosure url="assets/y15-screen-visualization.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y15-screen-visualization.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y12-Screen-Gameover]]></title><description><![CDATA[<img src="assets/y12-screen-gameover.png" target="_self">]]></description><link>assets/y12-screen-gameover.html</link><guid isPermaLink="false">assets/Y12-Screen-Gameover.png</guid><pubDate>Tue, 24 Feb 2026 15:41:50 GMT</pubDate><enclosure url="assets/y12-screen-gameover.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y12-screen-gameover.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y12-Screen-Multijump]]></title><description><![CDATA[<img src="assets/y12-screen-multijump.png" target="_self">]]></description><link>assets/y12-screen-multijump.html</link><guid isPermaLink="false">assets/Y12-Screen-Multijump.png</guid><pubDate>Tue, 24 Feb 2026 15:41:49 GMT</pubDate><enclosure url="assets/y12-screen-multijump.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y12-screen-multijump.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y12-Screen-Gameplay]]></title><description><![CDATA[<img src="assets/y12-screen-gameplay.png" target="_self">]]></description><link>assets/y12-screen-gameplay.html</link><guid isPermaLink="false">assets/Y12-Screen-Gameplay.png</guid><pubDate>Tue, 24 Feb 2026 15:41:48 GMT</pubDate><enclosure url="assets/y12-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y12-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y18-Screen-Prompt]]></title><description><![CDATA[<img src="assets/y18-screen-prompt.png" target="_self">]]></description><link>assets/y18-screen-prompt.html</link><guid isPermaLink="false">assets/Y18-Screen-Prompt.png</guid><pubDate>Tue, 24 Feb 2026 15:41:44 GMT</pubDate><enclosure url="assets/y18-screen-prompt.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y18-screen-prompt.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y11-Screen-Gameover]]></title><description><![CDATA[<img src="assets/y11-screen-gameover.png" target="_self">]]></description><link>assets/y11-screen-gameover.html</link><guid isPermaLink="false">assets/Y11-Screen-Gameover.png</guid><pubDate>Tue, 24 Feb 2026 15:41:43 GMT</pubDate><enclosure url="assets/y11-screen-gameover.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y11-screen-gameover.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y18-Screen-Results]]></title><description><![CDATA[<img src="assets/y18-screen-results.png" target="_self">]]></description><link>assets/y18-screen-results.html</link><guid isPermaLink="false">assets/Y18-Screen-Results.png</guid><pubDate>Tue, 24 Feb 2026 15:41:42 GMT</pubDate><enclosure url="assets/y18-screen-results.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y18-screen-results.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y07-Screen-Win]]></title><description><![CDATA[<img src="assets/y07-screen-win.png" target="_self">]]></description><link>assets/y07-screen-win.html</link><guid isPermaLink="false">assets/Y07-Screen-Win.png</guid><pubDate>Tue, 24 Feb 2026 15:41:39 GMT</pubDate><enclosure url="assets/y07-screen-win.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y07-screen-win.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y09-Screen-Gameover]]></title><description><![CDATA[<img src="assets/y09-screen-gameover.png" target="_self">]]></description><link>assets/y09-screen-gameover.html</link><guid isPermaLink="false">assets/Y09-Screen-Gameover.png</guid><pubDate>Tue, 24 Feb 2026 15:41:10 GMT</pubDate><enclosure url="assets/y09-screen-gameover.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y09-screen-gameover.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y10-Screen-GameOver]]></title><description><![CDATA[<img src="assets/y10-screen-gameover.png" target="_self">]]></description><link>assets/y10-screen-gameover.html</link><guid isPermaLink="false">assets/Y10-Screen-GameOver.png</guid><pubDate>Tue, 24 Feb 2026 15:41:10 GMT</pubDate><enclosure url="assets/y10-screen-gameover.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y10-screen-gameover.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y09-Screen-Gameplay]]></title><description><![CDATA[<img src="assets/y09-screen-gameplay.png" target="_self">]]></description><link>assets/y09-screen-gameplay.html</link><guid isPermaLink="false">assets/Y09-Screen-Gameplay.png</guid><pubDate>Tue, 24 Feb 2026 15:41:09 GMT</pubDate><enclosure url="assets/y09-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y09-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y10-Screen-Gameplay]]></title><description><![CDATA[<img src="assets/y10-screen-gameplay.png" target="_self">]]></description><link>assets/y10-screen-gameplay.html</link><guid isPermaLink="false">assets/Y10-Screen-Gameplay.png</guid><pubDate>Tue, 24 Feb 2026 15:41:06 GMT</pubDate><enclosure url="assets/y10-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y10-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y07-Screen-Gameplay]]></title><description><![CDATA[<img src="assets/y07-screen-gameplay.png" target="_self">]]></description><link>assets/y07-screen-gameplay.html</link><guid isPermaLink="false">assets/Y07-Screen-Gameplay.png</guid><pubDate>Tue, 24 Feb 2026 15:40:24 GMT</pubDate><enclosure url="assets/y07-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y07-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y03-Screen-Gameover]]></title><description><![CDATA[<img src="assets/y03-screen-gameover.png" target="_self">]]></description><link>assets/y03-screen-gameover.html</link><guid isPermaLink="false">assets/Y03-Screen-Gameover.png</guid><pubDate>Tue, 24 Feb 2026 15:34:41 GMT</pubDate><enclosure url="assets/y03-screen-gameover.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y03-screen-gameover.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y03-Screen-Gameplay]]></title><description><![CDATA[<img src="assets/y03-screen-gameplay.png" target="_self">]]></description><link>assets/y03-screen-gameplay.html</link><guid isPermaLink="false">assets/Y03-Screen-Gameplay.png</guid><pubDate>Tue, 24 Feb 2026 15:33:52 GMT</pubDate><enclosure url="assets/y03-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y03-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y04-Screen-Win]]></title><description><![CDATA[<img src="assets/y04-screen-win.png" target="_self">]]></description><link>assets/y04-screen-win.html</link><guid isPermaLink="false">assets/Y04-Screen-Win.png</guid><pubDate>Tue, 24 Feb 2026 15:33:28 GMT</pubDate><enclosure url="assets/y04-screen-win.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y04-screen-win.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y04-Screen-Gameplay]]></title><description><![CDATA[<img src="assets/y04-screen-gameplay.png" target="_self">]]></description><link>assets/y04-screen-gameplay.html</link><guid isPermaLink="false">assets/Y04-Screen-Gameplay.png</guid><pubDate>Tue, 24 Feb 2026 15:33:22 GMT</pubDate><enclosure url="assets/y04-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y04-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[G06-Screen-Game-Over]]></title><description><![CDATA[<img src="assets/g06-screen-game-over.png" target="_self">]]></description><link>assets/g06-screen-game-over.html</link><guid isPermaLink="false">assets/G06-Screen-Game-Over.png</guid><pubDate>Tue, 24 Feb 2026 15:31:52 GMT</pubDate><enclosure url="assets/g06-screen-game-over.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/g06-screen-game-over.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[G04-Screen-Question]]></title><description><![CDATA[<img src="assets/g04-screen-question.png" target="_self">]]></description><link>assets/g04-screen-question.html</link><guid isPermaLink="false">assets/G04-Screen-Question.png</guid><pubDate>Tue, 24 Feb 2026 15:31:51 GMT</pubDate><enclosure url="assets/g04-screen-question.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/g04-screen-question.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y06-Screen-Complete]]></title><description><![CDATA[<img src="assets/y06-screen-complete.png" target="_self">]]></description><link>assets/y06-screen-complete.html</link><guid isPermaLink="false">assets/Y06-Screen-Complete.png</guid><pubDate>Tue, 24 Feb 2026 15:31:51 GMT</pubDate><enclosure url="assets/y06-screen-complete.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y06-screen-complete.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[G06-Screen-Gameplay]]></title><description><![CDATA[<img src="assets/g06-screen-gameplay.png" target="_self">]]></description><link>assets/g06-screen-gameplay.html</link><guid isPermaLink="false">assets/G06-Screen-Gameplay.png</guid><pubDate>Tue, 24 Feb 2026 15:31:50 GMT</pubDate><enclosure url="assets/g06-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/g06-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[G04-Screen-Menu]]></title><description><![CDATA[<img src="assets/g04-screen-menu.png" target="_self">]]></description><link>assets/g04-screen-menu.html</link><guid isPermaLink="false">assets/G04-Screen-Menu.png</guid><pubDate>Tue, 24 Feb 2026 15:31:49 GMT</pubDate><enclosure url="assets/g04-screen-menu.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/g04-screen-menu.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y02-Screen-Editor]]></title><description><![CDATA[<img src="assets/y02-screen-editor.png" target="_self">]]></description><link>assets/y02-screen-editor.html</link><guid isPermaLink="false">assets/Y02-Screen-Editor.png</guid><pubDate>Tue, 24 Feb 2026 15:31:21 GMT</pubDate><enclosure url="assets/y02-screen-editor.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y02-screen-editor.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y02-Screen-Simulation]]></title><description><![CDATA[<img src="assets/y02-screen-simulation.png" target="_self">]]></description><link>assets/y02-screen-simulation.html</link><guid isPermaLink="false">assets/Y02-Screen-Simulation.png</guid><pubDate>Tue, 24 Feb 2026 15:31:19 GMT</pubDate><enclosure url="assets/y02-screen-simulation.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y02-screen-simulation.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y06-Screen-Gameplay]]></title><description><![CDATA[<img src="assets/y06-screen-gameplay.png" target="_self">]]></description><link>assets/y06-screen-gameplay.html</link><guid isPermaLink="false">assets/Y06-Screen-Gameplay.png</guid><pubDate>Tue, 24 Feb 2026 15:31:19 GMT</pubDate><enclosure url="assets/y06-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y06-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[G08-Screen-Translator]]></title><description><![CDATA[<img src="assets/g08-screen-translator.png" target="_self">]]></description><link>assets/g08-screen-translator.html</link><guid isPermaLink="false">assets/G08-Screen-Translator.png</guid><pubDate>Tue, 24 Feb 2026 15:31:13 GMT</pubDate><enclosure url="assets/g08-screen-translator.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/g08-screen-translator.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y05-Screen-GameOver]]></title><description><![CDATA[<img src="assets/y05-screen-gameover.png" target="_self">]]></description><link>assets/y05-screen-gameover.html</link><guid isPermaLink="false">assets/Y05-Screen-GameOver.png</guid><pubDate>Tue, 24 Feb 2026 15:31:12 GMT</pubDate><enclosure url="assets/y05-screen-gameover.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y05-screen-gameover.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y05-Screen-Gameplay]]></title><description><![CDATA[<img src="assets/y05-screen-gameplay.png" target="_self">]]></description><link>assets/y05-screen-gameplay.html</link><guid isPermaLink="false">assets/Y05-Screen-Gameplay.png</guid><pubDate>Tue, 24 Feb 2026 15:31:11 GMT</pubDate><enclosure url="assets/y05-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y05-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y01-Screen-GameOver]]></title><description><![CDATA[<img src="assets/y01-screen-gameover.png" target="_self">]]></description><link>assets/y01-screen-gameover.html</link><guid isPermaLink="false">assets/Y01-Screen-GameOver.png</guid><pubDate>Tue, 24 Feb 2026 15:31:10 GMT</pubDate><enclosure url="assets/y01-screen-gameover.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y01-screen-gameover.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[G07-Screen-Steps]]></title><description><![CDATA[<img src="assets/g07-screen-steps.png" target="_self">]]></description><link>assets/g07-screen-steps.html</link><guid isPermaLink="false">assets/G07-Screen-Steps.png</guid><pubDate>Tue, 24 Feb 2026 15:30:55 GMT</pubDate><enclosure url="assets/g07-screen-steps.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/g07-screen-steps.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[G07-Screen-Main]]></title><description><![CDATA[<img src="assets/g07-screen-main.png" target="_self">]]></description><link>assets/g07-screen-main.html</link><guid isPermaLink="false">assets/G07-Screen-Main.png</guid><pubDate>Tue, 24 Feb 2026 15:30:54 GMT</pubDate><enclosure url="assets/g07-screen-main.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/g07-screen-main.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[G04-Screen-Results]]></title><description><![CDATA[<img src="assets/g04-screen-results.png" target="_self">]]></description><link>assets/g04-screen-results.html</link><guid isPermaLink="false">assets/G04-Screen-Results.png</guid><pubDate>Tue, 24 Feb 2026 15:30:34 GMT</pubDate><enclosure url="assets/g04-screen-results.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/g04-screen-results.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[G05-Screen-RPN]]></title><description><![CDATA[<img src="assets/g05-screen-rpn.png" target="_self">]]></description><link>assets/g05-screen-rpn.html</link><guid isPermaLink="false">assets/G05-Screen-RPN.png</guid><pubDate>Tue, 24 Feb 2026 15:30:30 GMT</pubDate><enclosure url="assets/g05-screen-rpn.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/g05-screen-rpn.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[G03-Screen-Win]]></title><description><![CDATA[<img src="assets/g03-screen-win.png" target="_self">]]></description><link>assets/g03-screen-win.html</link><guid isPermaLink="false">assets/G03-Screen-Win.png</guid><pubDate>Tue, 24 Feb 2026 15:30:27 GMT</pubDate><enclosure url="assets/g03-screen-win.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/g03-screen-win.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Y01-Screen-Gameplay]]></title><description><![CDATA[<img src="assets/y01-screen-gameplay.png" target="_self">]]></description><link>assets/y01-screen-gameplay.html</link><guid isPermaLink="false">assets/Y01-Screen-Gameplay.png</guid><pubDate>Tue, 24 Feb 2026 15:30:26 GMT</pubDate><enclosure url="assets/y01-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/y01-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[G05-Screen-Main]]></title><description><![CDATA[<img src="assets/g05-screen-main.png" target="_self">]]></description><link>assets/g05-screen-main.html</link><guid isPermaLink="false">assets/G05-Screen-Main.png</guid><pubDate>Tue, 24 Feb 2026 15:30:24 GMT</pubDate><enclosure url="assets/g05-screen-main.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/g05-screen-main.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[G03-Screen-Gameplay]]></title><description><![CDATA[<img src="assets/g03-screen-gameplay.png" target="_self">]]></description><link>assets/g03-screen-gameplay.html</link><guid isPermaLink="false">assets/G03-Screen-Gameplay.png</guid><pubDate>Tue, 24 Feb 2026 15:30:23 GMT</pubDate><enclosure url="assets/g03-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/g03-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[G02-Screen-Menu]]></title><description><![CDATA[<img src="assets/g02-screen-menu.png" target="_self">]]></description><link>assets/g02-screen-menu.html</link><guid isPermaLink="false">assets/G02-Screen-Menu.png</guid><pubDate>Tue, 24 Feb 2026 15:26:53 GMT</pubDate><enclosure url="assets/g02-screen-menu.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/g02-screen-menu.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[G01-Screen-Gameplay]]></title><description><![CDATA[<img src="assets/g01-screen-gameplay.png" target="_self">]]></description><link>assets/g01-screen-gameplay.html</link><guid isPermaLink="false">assets/G01-Screen-Gameplay.png</guid><pubDate>Tue, 24 Feb 2026 15:26:52 GMT</pubDate><enclosure url="assets/g01-screen-gameplay.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/g01-screen-gameplay.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[G02-Screen-Stats]]></title><description><![CDATA[<img src="assets/g02-screen-stats.png" target="_self">]]></description><link>assets/g02-screen-stats.html</link><guid isPermaLink="false">assets/G02-Screen-Stats.png</guid><pubDate>Tue, 24 Feb 2026 15:21:13 GMT</pubDate><enclosure url="assets/g02-screen-stats.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/g02-screen-stats.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[G02-Screen-Typing]]></title><description><![CDATA[<img src="assets/g02-screen-typing.png" target="_self">]]></description><link>assets/g02-screen-typing.html</link><guid isPermaLink="false">assets/G02-Screen-Typing.png</guid><pubDate>Tue, 24 Feb 2026 15:18:32 GMT</pubDate><enclosure url="assets/g02-screen-typing.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/g02-screen-typing.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L07-NOTES-ENG. Interrupts and OS Interaction]]></title><description><![CDATA[Quick reference for Lecture 07. See full materials in L07-ENG. Interrupts and OS Interaction.md.Power button → Reset Vector → BIOS/UEFI → POST → Bootstrap → OSBoot order: BIOS settings determine which device to try first (HDD, USB, network).Learn more:
<a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Bootloader" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Bootloader" target="_self">Bootloader</a> (Wikipedia)
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Power-on_self-test" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Power-on_self-test" target="_self">Power-on self-test</a> (Wikipedia)
512-byte structure at sector 0 of the boot disk:Each partition entry: boot indicator (80h = active), CHS start/end, System ID, LBA start, total sectors.Max 4 primary partitions (or 3 primary + 1 extended with logical volumes inside).<br>Learn more: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Master_boot_record" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Master_boot_record" target="_self">Master boot record</a> (Wikipedia)Hardware interrupts: triggered by devices (keyboard, timer, disk). Asynchronous — can fire at any time.Software interrupts: triggered by INT N instruction. Synchronous — occur at a specific point in code.IF flag in FLAGS controls maskable hardware interrupts: STI enables, CLI disables.<br>Learn more: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Interrupt" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Interrupt" target="_self">Interrupt</a> (Wikipedia)Function number in AH. Key functions:mov ah, 09h
mov dx, offset msg ; DS:DX → '$'-terminated string
int 21h ; Print string
<br>Learn more: <a data-tooltip-position="top" aria-label="https://mrszeto.net/CIT/interrupts.htm" rel="noopener nofollow" class="external-link is-unresolved" href="https://mrszeto.net/CIT/interrupts.htm" target="_self">DOS Interrupts</a>Color attribute byte: bits 7-4 = background, bits 3-0 = foreground (e.g., 07h = white on black).<br>Learn more: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/INT_10H" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/INT_10H" target="_self">INT 10h</a>, <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/INT_16H" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/INT_16H" target="_self">INT 16h</a> (Wikipedia)Syscall number in EAX, args in EBX, ECX, EDX, ESI, EDI, EBP. Return in EAX (negative = error).mov eax, 4 ; write
mov ebx, 1 ; stdout
mov ecx, msg ; pointer
mov edx, len ; length
int 80h
Modern 64-bit Linux uses SYSCALL instruction (faster, different register convention: RDI, RSI, RDX, R10, R8, R9).<br>Learn more: <a data-tooltip-position="top" aria-label="https://syscalls.w3challs.com/" rel="noopener nofollow" class="external-link is-unresolved" href="https://syscalls.w3challs.com/" target="_self">Syscall Table</a>256 entries × 4 bytes = 1024 bytes at 0000:0000–0000:03FF.Each entry: 2-byte offset (IP) + 2-byte segment (CS), little-endian.Finding handler for INT N: read 4 bytes at address N × 4.<br>Learn more: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Interrupt_vector_table" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Interrupt_vector_table" target="_self">Interrupt vector table</a> (Wikipedia)
PUSH FLAGS → 2. Clear IF, TF → 3. PUSH CS → 4. PUSH IP (of next instr) → 5. Load CS:IP from IVT[N×4] → 6. Execute handler POP IP → 2. POP CS → 3. POP FLAGS
<br>Learn more: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Protected_mode" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Protected_mode" target="_self">Protected mode</a> (Wikipedia)Segmentation: segment registers hold selectors → index into GDT/LDT → descriptor has base, limit, access rights. CPU checks every access.Paging (80386+): virtual address → page table → physical address. Page size = 4 KB.Key benefits:
Each process gets isolated virtual address space (separate page tables)
Demand paging: pages loaded only when accessed; page faults handled by OS
Swap: inactive pages moved to disk, loaded back transparently
TLB cache: &gt;99% hit rate makes translation fast
<br>Learn more: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Virtual_memory" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Virtual_memory" target="_self">Virtual memory</a>, <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Memory_paging" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Memory_paging" target="_self">Memory paging</a> (Wikipedia)4 rings (only 0 and 3 used in practice):Ring transition: INT/SYSCALL → CPU switches to Ring 0, loads kernel stack from TSS, saves user state.Kernel responsibilities: process scheduling, memory management, device drivers, system call interface.Kernel architectures: Monolithic (Linux — fast, all in Ring 0) vs. Microkernel (MINIX, QNX — drivers in user space, more reliable).Kernel vs. User space (32-bit Linux): lower 3 GB = user, upper 1 GB = kernel (mapped in every process but Ring 0-only).<br>Learn more: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Protection_ring" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Protection_ring" target="_self">Protection ring</a>, <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Kernel_(operating_system)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Kernel_(operating_system)" target="_self">Kernel</a> (Wikipedia)
Boot chain: Reset Vector → BIOS/POST → MBR → Bootloader → Kernel
Interrupts unify hardware events and software service requests via the same IVT mechanism
INT pushes FLAGS+CS+IP, IRET pops them back — like far CALL but with flag preservation
IVT = 256 × 4-byte entries at 0000:0000; entry N at address N×4
DOS INT 21h (AH=function) and BIOS INT 10h/16h are the primary I/O tools in Real Mode
Real Mode: 1 MB, no protection, no multitasking
Protected Mode: virtual memory (paging), protection rings (0-3), hardware task switching (TSS)
The kernel (Ring 0) manages everything; user programs (Ring 3) use syscalls
What you'll do:
Examine IVT in Turbo Debugger (memory dump at 0000:0000)
Trace interrupt vectors to handler code
Explore BIOS Data Area (0040:0000): keyboard buffer, timer ticks
Call BIOS interrupts: INT 10h (video), INT 16h (keyboard)
Call DOS interrupts: INT 21h (functions 01h, 02h, 09h, 4Ch)
Hook an interrupt vector: save original, install custom handler, restore on exit
]]></description><link>lectures/eng/l07-notes-eng.-interrupts-and-os-interaction.html</link><guid isPermaLink="false">Lectures/ENG/L07-NOTES-ENG. Interrupts and OS Interaction.md</guid><pubDate>Mon, 23 Feb 2026 14:41:00 GMT</pubDate></item><item><title><![CDATA[L07-Kernel-Architecture]]></title><description><![CDATA[<img src="assets/l07-kernel-architecture.png" target="_self">]]></description><link>assets/l07-kernel-architecture.html</link><guid isPermaLink="false">assets/L07-Kernel-Architecture.png</guid><pubDate>Mon, 23 Feb 2026 13:58:33 GMT</pubDate><enclosure url="assets/l07-kernel-architecture.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l07-kernel-architecture.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L07-Protection-Rings]]></title><description><![CDATA[<img src="assets/l07-protection-rings.png" target="_self">]]></description><link>assets/l07-protection-rings.html</link><guid isPermaLink="false">assets/L07-Protection-Rings.png</guid><pubDate>Mon, 23 Feb 2026 13:58:33 GMT</pubDate><enclosure url="assets/l07-protection-rings.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l07-protection-rings.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L07-Virtual-Memory-Diagram]]></title><description><![CDATA[<img src="assets/l07-virtual-memory-diagram.png" target="_self">]]></description><link>assets/l07-virtual-memory-diagram.html</link><guid isPermaLink="false">assets/L07-Virtual-Memory-Diagram.png</guid><pubDate>Mon, 23 Feb 2026 13:58:33 GMT</pubDate><enclosure url="assets/l07-virtual-memory-diagram.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l07-virtual-memory-diagram.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L07-INT-IRET-After]]></title><description><![CDATA[<img src="assets/l07-int-iret-after.png" target="_self">]]></description><link>assets/l07-int-iret-after.html</link><guid isPermaLink="false">assets/L07-INT-IRET-After.png</guid><pubDate>Mon, 23 Feb 2026 13:58:33 GMT</pubDate><enclosure url="assets/l07-int-iret-after.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l07-int-iret-after.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L07-INT-IRET-During]]></title><description><![CDATA[<img src="assets/l07-int-iret-during.png" target="_self">]]></description><link>assets/l07-int-iret-during.html</link><guid isPermaLink="false">assets/L07-INT-IRET-During.png</guid><pubDate>Mon, 23 Feb 2026 13:58:33 GMT</pubDate><enclosure url="assets/l07-int-iret-during.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l07-int-iret-during.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L07-INT-IRET-Before]]></title><description><![CDATA[<img src="assets/l07-int-iret-before.png" target="_self">]]></description><link>assets/l07-int-iret-before.html</link><guid isPermaLink="false">assets/L07-INT-IRET-Before.png</guid><pubDate>Mon, 23 Feb 2026 13:58:33 GMT</pubDate><enclosure url="assets/l07-int-iret-before.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l07-int-iret-before.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L07-IVT-Memory-Dump-TD]]></title><description><![CDATA[<img src="assets/l07-ivt-memory-dump-td.png" target="_self">]]></description><link>assets/l07-ivt-memory-dump-td.html</link><guid isPermaLink="false">assets/L07-IVT-Memory-Dump-TD.png</guid><pubDate>Mon, 23 Feb 2026 13:58:33 GMT</pubDate><enclosure url="assets/l07-ivt-memory-dump-td.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l07-ivt-memory-dump-td.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L07-IVT-Structure]]></title><description><![CDATA[<img src="assets/l07-ivt-structure.png" target="_self">]]></description><link>assets/l07-ivt-structure.html</link><guid isPermaLink="false">assets/L07-IVT-Structure.png</guid><pubDate>Mon, 23 Feb 2026 13:58:33 GMT</pubDate><enclosure url="assets/l07-ivt-structure.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l07-ivt-structure.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L07-IVT-Memory-Layout]]></title><description><![CDATA[<img src="assets/l07-ivt-memory-layout.png" target="_self">]]></description><link>assets/l07-ivt-memory-layout.html</link><guid isPermaLink="false">assets/L07-IVT-Memory-Layout.png</guid><pubDate>Mon, 23 Feb 2026 13:58:33 GMT</pubDate><enclosure url="assets/l07-ivt-memory-layout.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l07-ivt-memory-layout.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L07-Hardware-Interrupts-Diagram]]></title><description><![CDATA[<img src="assets/l07-hardware-interrupts-diagram.png" target="_self">]]></description><link>assets/l07-hardware-interrupts-diagram.html</link><guid isPermaLink="false">assets/L07-Hardware-Interrupts-Diagram.png</guid><pubDate>Mon, 23 Feb 2026 13:58:33 GMT</pubDate><enclosure url="assets/l07-hardware-interrupts-diagram.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l07-hardware-interrupts-diagram.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L07-DOS-Hello-World-INT21h]]></title><description><![CDATA[<img src="assets/l07-dos-hello-world-int21h.png" target="_self">]]></description><link>assets/l07-dos-hello-world-int21h.html</link><guid isPermaLink="false">assets/L07-DOS-Hello-World-INT21h.png</guid><pubDate>Mon, 23 Feb 2026 13:58:33 GMT</pubDate><enclosure url="assets/l07-dos-hello-world-int21h.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l07-dos-hello-world-int21h.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L07-MBR-Hex-Dump]]></title><description><![CDATA[<img src="assets/l07-mbr-hex-dump.png" target="_self">]]></description><link>assets/l07-mbr-hex-dump.html</link><guid isPermaLink="false">assets/L07-MBR-Hex-Dump.png</guid><pubDate>Mon, 23 Feb 2026 13:58:33 GMT</pubDate><enclosure url="assets/l07-mbr-hex-dump.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l07-mbr-hex-dump.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L07-Boot-Sequence-Timeline]]></title><description><![CDATA[<img src="assets/l07-boot-sequence-timeline.png" target="_self">]]></description><link>assets/l07-boot-sequence-timeline.html</link><guid isPermaLink="false">assets/L07-Boot-Sequence-Timeline.png</guid><pubDate>Mon, 23 Feb 2026 13:58:33 GMT</pubDate><enclosure url="assets/l07-boot-sequence-timeline.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l07-boot-sequence-timeline.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L07-BIOS-Chip-Photo]]></title><description><![CDATA[<img src="assets/l07-bios-chip-photo.jpg" target="_self">]]></description><link>assets/l07-bios-chip-photo.html</link><guid isPermaLink="false">assets/L07-BIOS-Chip-Photo.jpg</guid><pubDate>Mon, 23 Feb 2026 13:58:33 GMT</pubDate><enclosure url="assets/l07-bios-chip-photo.jpg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l07-bios-chip-photo.jpg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L07-Reset-Vector-TD]]></title><description><![CDATA[<img src="assets/l07-reset-vector-td.png" target="_self">]]></description><link>assets/l07-reset-vector-td.html</link><guid isPermaLink="false">assets/L07-Reset-Vector-TD.png</guid><pubDate>Mon, 23 Feb 2026 13:58:33 GMT</pubDate><enclosure url="assets/l07-reset-vector-td.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l07-reset-vector-td.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Консультація - 2026-02-21]]></title><description><![CDATA[Дата: субота, 21 лютого 2026 р.
Формат: онлайнПідсумковий проєкт стосується студентів, які слухають курс на 6 кредитів (тобто майже всіх, крім ~12 осіб на 4-кредитній програмі). Меню тем буде опубліковано протягом наступного тижня після узгодження між викладачами. Офіційне оголошення — на 8-й лекції (практичній).Очікуваний обсяг залучення на проєкт — ~60 годин на кожного студента, починаючи з 8-го тижня, з розрахунку приблизно 8 годин на тиждень.
Рекомендовано працювати в парах — це додатковий «неявний скіл» курсу: навички командної роботи (тімворк).
Соло-робота допускається для тих, хто не знайшов пару, але вимоги до обсягу та якості залишаються незмінними.
Для парних проєктів: рекомендовано рівний внесок обох учасників (~50/50). За доведений рівний внесок нараховується бонус +5% від оцінки за проєкт.
Способи підтвердження рівного внеску: історія комітів, індивідуальне відео з розумінням усього проєкту, відповіді на захисті по всьому проєкту (не лише по своїй частині).
Рекомендований обсяг — 600–900 рядків коду. Не роздувати, а робити те, що потрібно.Дедлайн один, але є проміжні контрольні точки, які впливають на кінцеву оцінку (оцінюється рівномірність прогресу). Приклади:
тема обрана, пара сформована, опублікований короткий план у readme.
скелет програми — основна структура, сегменти, точка входу.
основна функціональність працює.
програма працює повністю.
Всі контрольні точки описано на <a class="internal-link" data-href="Project" href="labs/project.html" target="_self" rel="noopener nofollow">загальній сторінці проєкту</a>. На кожній контрольній точці потрібно записувати короткі Loom-відео (в парах можна чергуватися: один тиждень — один записує, наступний — інший). Відео є обов'язковими; кілька тижнів підряд без відео негативно вплине на оцінку.Потрібно написати звіт із розділами: концепція, схема організації пам'яті, які підходи розглядалися, ретроспектива. Для парних проєктів — також розподіл роботи та як інтегрувався код. Це не бюрократія, а саморефлексія.
Парні проєкти матимуть один репозиторій на двох. Буде окремий лінк для створення: перший з пари створює, другий приєднується, потрібно придумати назву команди.
Conventional Commits — обов'язковий формат комітів (тип + короткий опис, починається з дієслова). Це стандартна індустріальна практика. Окремий гайд буде опублікований. Меню тем формується, студенти можуть впливати на його наповнення (пропозиції приймаються).
Обрати тему, якої немає в меню, — не можна, але можна запропонувати її для включення.
Змінити тему після вибору можна лише протягом тижня та з узгодженням із інструктором.
Теми будуть відкалібровані за складністю (аналогічно до минулого триместру: зелені — простіші з лімітом 95 балів, жовті — стандартні, червоні — складні).
Використання AI дозволено, але заборонено подавати AI-код як власний. На першому курсі дуже важливо «набивати руку» самостійно. AI-генерований код буде видно при перевірці.Все одно варто подавати. Проєкт — велика частина балів за курс. Навіть неповний проєкт — краще за нуль.Оголошення буде на наступному тижні. Суть:
Написати відносно складну програму на асемблері за 2–4 дні.
Далі — порівнюються розміри програм різних учасників: виграє той, у кого програма проходить всі тести і має найменший розмір (рахунок іде на байти — сотні, можливо тисячі).
Є лідерборд. 1-ше місце — ~+10 балів до курсу; 2-ге та 3-тє місця — також додаткові бали; можливо, навіть 4-те.
Участь у змаганні звільняє від проєкту та, можливо, від частини практичних, що перетинаються зі змаганням.
Учасники змагання також звільняються від домашніх завдань на час участі. Але якщо студент зійде з дистанції — повертається до проєкту, втрачене вже не повернути.
Не рекомендується брати участь у змаганні й робити проєкт одночасно, хоча минулого року 3–4 особи це зробили (один навіть зайняв призове місце). Наступна субота — єдина субота з парами в розкладі цього курсу цього триместру.
Формат — повністю онлайн.
Лекція буде одна на всіх (тема за силабусом: відеопам'ять та суміжні теми), після чого — практичні заняття (можливо, з розподілом по кімнатах).
Конкретна логістика буде оголошена у вівторок.
Деяким групам пару могли поставити на п'ятницю через обмеження розкладу (13 груп — усіх офіційно не вдалося розмістити в одну суботу).
Причина суботніх пар: уникнення розсинхрону між лекціями та практиками (15 тижнів у триместрі, але 14 робочих — тиждень самостійної роботи «з'їдає» один тиждень).
Порада: використовуйте тиждень самостійної роботи радше для роботи, ніж для відпочинку — навантаження наростатиме ближче до кінця триместру.
Курс архітектур і курс алгоритмів — по 6 кредитів кожен, разом 12 із 20 за триместр (60% навантаження).
Для 6-кредитної програми: самостійна робота ~60 год на проєкт + ~2 години на опрацювання кожної лекції + підготовка до контролів.
Для 4-кредитної програми (АКІТР): проєкту немає, але є опрацювання лекцій, домашні, підготовка до іспиту.
Навантаження по проєкту значно зростає з 8-го тижня — наполеглива рекомендація розподіляти рівномірно, не залишати все на кінець.
Хто відставатиме на 2–4 тижні — ризикує потрапити в ситуацію, де треба буде витратити 20–40 годин за тиждень, що нереально. SI (Source Index) та DI (Destination Index) — спеціалізовані індексні регістри.
Використовуються інструкціями копіювання пам'яті (LODSB, MOVSB тощо), які ефективно переміщують дані з Source Index у Destination Index.
Якщо ці інструкції не використовуються — SI та DI можна вільно використовувати як звичайні регістри загального призначення (з урахуванням того, що деякі інші інструкції, як-от MUL, на них не зав'язані).
Інструкція LOOP: одночасно зменшує CX і стрибає, якщо CX &gt; 0. Стек зберігається в стек-сегменті (SS).
У 16-бітному режимі максимальний розмір сегменту — 65 535 байтів.
На практиці для лабораторних достатньо і сотні байтів.
Великий стек може знадобитися при глибокій рекурсії. Загальний обсяг адресної пам'яті в реальному режимі — 1 мегабайт.
Одну й ту саму фізичну адресу можна задати різними комбінаціями «сегмент:зміщення» (кількість варіантів кінцева, залежить від конкретної адреси; для адреси N — приблизно N/16 варіантів).
Це дозволяє гнучко розміщувати програми: якщо програмі потрібно менше 64 КБ, їй виділяється менше, а решта доступна іншим програмам.
DOS при завантаженні присвоює значення сегментних регістрів (CS, DS, SS) та виділяє пам'ять; перед програмою є спеціальна структура даних із описом виділеної пам'яті.
«Чемна поведінка» програми — працювати лише з тією пам'яттю, яку їй виділили.
Домашні завдання залишаються і після запуску проєкту, але вони свідомо прості, щоб не перевантажувати студентів. Перелік завдань: мінімум/максимум, суми, рядки, матриці, сортування вставкою. Більшість із цього вже проходили на практиках. Сортування вставкою має нюанс зі зсувом елементів.З понеділка починається подання відстаючих студентів на відсіювання з курсу. Протягом тижня буде можливість підтягнутися. Тим, хто суттєво відстає і не зможе надолужити, рекомендовано не мучитися і краще підготуватися на наступний рік.Курси архітектур і алгоритмів закладають фундамент із різних сторін, які перекликаються: алгоритми — теоретично, що можна обчислити; архітектури — фізично, який обчислювач може це зробити. Ці два фундаменти сходяться.]]></description><link>office-hours/консультація-2026-02-21.html</link><guid isPermaLink="false">Office-hours/Консультація - 2026-02-21.md</guid><pubDate>Mon, 23 Feb 2026 06:39:36 GMT</pubDate></item><item><title><![CDATA[Lab06]]></title><description><![CDATA[
Курс: Архітектура обчислювальних систем
Тиждень: 6
Тривалість: 2 академічні години
Тип: Практична робота під наглядом інструктора
Після завершення практики студент зможе:
Оголошувати масиви в сегменті даних (DB, DW) та знаходити їх у пам'яті через вікно Dump Turbo Debugger.
Використовувати режими адресації 8086 для доступу до елементів масиву.
Обчислювати ефективну адресу за допомогою інструкції LEA.
Писати процедури для обходу та обробки масивів (сума, сортування).
Працювати з двовимірними масивами у вигляді плоского масиву (row-major).
Перегляньте лекційні матеріали за Тиждень 6 (бітові зсуви, системна архітектура). Зверніть увагу на інструкцію LEA та режими адресації..DATA
arr_b DB 10h, 20h, 30h, 40h ; array of 4 bytes
arr_w DW 1000h, 2000h, 3000h ; array of 3 words
msg DB 'Hello', '$' ; string is also a byte array
zeros DB 100 DUP(0) ; 100 zeros (DUP)
Масиви займають суміжні комірки пам'яті. Слова (DW) зберігаються у порядку little-endian: молодший байт за меншою адресою. Наприклад, 1234h → в пам'яті: 34h, 12h.arr DW 10, 20, 30, 40, 50
ARR_N EQU ($ - arr) / 2 ; $ = current address; ($ - arr) = size in bytes ; / 2 because each word = 2 bytes -&gt; ARR_N = 5
Обмеження 8086: для бази — тільки BX або BP; для індексу — тільки SI або DI. Не можна: [AX], [CX], [DX], [SI+DI].
Примітка. BP за замовчуванням використовує сегмент SS (стек), решта — DS.
LEA обчислює адресу, не звертаючись до пам'яті:LEA BX, [arr_w] ; BX = offset arr_w (equivalent to MOV BX, OFFSET arr_w)
LEA SI, [BX+DI+4] ; SI = BX + DI + 4 (computed in one cycle)
; Index-based (array + SI) ; Pointer-based (BX as pointer)
xor si, si lea bx, [arr_w]
mov ax, arr_w[si] ; 1st element mov ax, [bx] ; 1st element
add si, 2 ; next add bx, 2 ; next
mov ax, arr_w[si] ; 2nd element mov ax, [bx] ; 2nd element
Створіть папку Lab06. Скопіюйте STUB.ASM з Lab04 або Lab05.Для кожного завдання: копіюйте STUB.ASM у новий файл з відповідним ім'ям.Скопіюйте STUB.ASM → 1ARRAY.ASM.Додайте до .DATA:arr_b DB 0Ah, 0Bh, 0Ch, 0Dh, 0Eh
arr_w DW 1234h, 5678h, 0ABCDh
msg DB 'Hi!', 0
Скомпілюйте, відкрийте в TD. Відкрийте вікно Dump (Alt+D або View → Dump).Перейдіть до адреси DS:0000 (у вікні Dump натисніть Ctrl+G, введіть DS:0).Запишіть у коментарях:
За якими зміщеннями (offset) починаються arr_b, arr_w, msg?
Як виглядає 5678h у пам'яті? (яка послідовність байтів?)
Де закінчується один масив і починається наступний?
Додайте код у .CODE (після mov ds, ax): ; Direct mov al, [arr_b] ; AL = ? ; Register indirect mov bx, offset arr_w mov ax, [bx] ; AX = ? ; Indexed mov si, 2 mov al, arr_b[si] ; AL = ? ; Base + displacement mov bx, offset arr_w mov ax, [bx+2] ; AX = ? ; LEA lea bx, [arr_w] mov ax, [bx+4] ; AX = ?
Пройдіть покроково в TD (F7). Для кожної інструкції MOV запишіть у коментарі очікуване значення, потім перевірте.Скопіюйте STUB.ASM → 2SUM.ASM.Додайте до .DATA:data_w DW 10, 20, 30, 40, 50
N EQU ($ - data_w) / 2
Напишіть код, що підсумовує всі елементи data_w у AX, використовуючи цикл з SI: xor ax, ax xor si, si mov cx, N
sum_loop: add ax, data_w[si] add si, 2 loop sum_loop ; AX = ? (expect 150 = 0096h)
Перевірте в TD.Напишіть процедуру array_sum:
Параметри (стек): [BP+4] = offset масиву, [BP+6] = кількість елементів
Результат: AX = сума елементів
Повний стековий фрейм (PUSH BP / MOV BP, SP / ... / POP BP / RET)
Виклик: push N push offset data_w call array_sum add sp, 4 ; AX should be 0096h (150)
Тести:
Масив 10, 20, 30, 40, 50 → AX = 150 (0096h)
Додайте другий масив DW 7 (один елемент) і перевірте → AX = 7
Скопіюйте наступний код цілком у 3BSORT.ASM:.MODEL small
.STACK 256 .DATA
arr DW 64, 34, 25, 12, 22, 11, 90
N EQU ($ - arr) / 2 .CODE
main: mov ax, @data mov ds, ax ; Check array in Dump BEFORE sorting ; (set breakpoint here: F2) push N push offset arr call bubble_sort add sp, 4 ; Check array in Dump AFTER sorting ; Expected: 11, 12, 22, 25, 34, 64, 90 mov ah, 4Ch int 21h ;----------------------------------------------
; bubble_sort - sorts a word array ascending
; Params: [BP+4] = array offset
; [BP+6] = element count
; Sorts the array in place
;----------------------------------------------
bubble_sort proc push bp mov bp, sp push si push cx push dx mov cx, [bp+6] ; CX = N dec cx ; N-1 passes jcxz bs_done bs_outer: push cx ; save outer loop counter mov si, [bp+4] ; SI = array start mov dx, cx ; DX = comparisons this pass bs_inner: ; --- YOUR CODE HERE (4-5 lines) --- bs_no_swap: add si, 2 ; next pair dec dx jnz bs_inner pop cx ; restore outer counter loop bs_outer bs_done: pop dx pop cx pop si pop bp ret
bubble_sort endp END main
Ваше завдання: допишіть 4–5 рядків у секції bs_inner (між коментарем YOUR CODE HERE та міткою bs_no_swap).Підказки:
Завантажте лівий елемент [SI] у регістр.
Порівняйте його з правим [SI+2] (CMP).
Який умовний перехід пропустить обмін, якщо елементи вже у правильному порядку?
Для обміну двох комірок пам'яті потрібен хоча б один регістр як посередник.
Перевірка в TD:
Поставте breakpoint (F2) на рядку push N (перед викликом).
Відкрийте Dump (Alt+D) → Ctrl+G → DS:0 — запишіть вигляд масиву до сортування.
Виконайте до рядку mov ah, 4Ch (F4 = Execute to cursor).
Подивіться Dump знову — масив має бути відсортований за зростанням.
Скопіюйте STUB.ASM → 4MATRIX.ASM.Двовимірний масив зберігається як одновимірний, рядок за рядком (row-major):matrix 3×4: col 0 col 1 col 2 col 3 row 0 [ 1, 2, 3, 4 ] row 1 [ 5, 6, 7, 8 ] row 2 [ 9, 10, 11, 12 ] У пам'яті: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12
Формула адреси елемента [row][col] для масиву байтів:offset = row × COLS + col
Додайте до .DATA:matrix DB 1, 2, 3, 4 DB 5, 6, 7, 8 DB 9, 10, 11, 12
COLS EQU 4
Напишіть код для читання елемента matrix[row][col]: ; Example: matrix[1][2] -&gt; expect 7 mov al, 1 ; row = 1 mov bl, COLS mul bl ; AX = row * COLS = 4 add al, 2 ; + col -&gt; AL = 6 xor ah, ah mov si, ax mov al, [matrix + si] ; AL = 7
Самостійно прочитайте:
matrix[0][3] → очікуємо: ?
matrix[2][1] → очікуємо: ?
Перевірте результати у TD. Подивіться на матрицю у вікні Dump — знайдіть кожний елемент.Напишіть код, що обчислює суму всіх 12 елементів матриці в AX (один цикл від 0 до ROWS×COLS−1).Очікуваний результат: AX = 78 (004Eh).До кінця заняття:
1ARRAY.ASM — масиви, Dump, режими адресації з коментарями
2SUM.ASM — обхід масиву, процедура array_sum
3BSORT.ASM — дописаний bubble sort, перевірений у Dump
4MATRIX.ASM — доступ до елементів матриці, сума
Закомічено всі файли
Дедлайн: 23:59 у день перед Практикою 7Напишіть процедуру find_min_max, яка знаходить мінімальне та максимальне значення у масиві знакових слів.Інтерфейс:
Параметри (стек): [BP+4] = offset масиву, [BP+6] = кількість елементів
Результат: AX = мінімум, BX = максимум
Повний стековий фрейм
Алгоритм: Ініціалізувати min і max першим елементом масиву. У циклі порівнювати кожний наступний елемент з поточним min (JL) та max (JG).Тести (виклик з main):Вимоги:
Знакові порівняння (JL/JG, не JB/JA) — інакше від'ємні числа (наприклад, FFF9h = −7) будуть інтерпретовані як великі беззнакові значення
Працює для будь-якої кількості елементів ≥ 1
Параметри через стек
Напишіть процедуру row_sum, яка обчислює суму елементів одного рядка матриці. Матриця зберігається як плоский масив байтів (row-major).Інтерфейс:
Параметри (стек): [BP+4] = offset матриці
[BP+6] = кількість стовпців
[BP+8] = номер рядка (0-based) Результат: AX = сума елементів рядка
Повний стековий фрейм
Дані для тестування:matrix DB 1, 2, 3, 4 DB 5, 6, 7, 8 DB 9, 10, 11, 12
COLS EQU 4
Виклик: push 1 ; row 1 push COLS ; column count push offset matrix ; matrix address call row_sum add sp, 6 ; clean 3 params ; AX = 001Ah (26)
Тести:Підказка: Обчисліть початок рядка: start = row × COLS. Потім у циклі додайте COLS байтів починаючи з [matrix + start]. Зверніть увагу: елементи — байти (DB), а сума — слово (AX). Для додавання байта до слова обнуліть старший байт перед додаванням.Реалізуйте сортування вставкою (insertion sort) для масиву знакових слів.Інтерфейс:
Параметри (стек): [BP+4] = offset масиву, [BP+6] = кількість елементів
Повний стековий фрейм
Алгоритм:для i від 1 до N-1: key = arr[i] j = i - 1 поки j &gt;= 0 та arr[j] &gt; key: arr[j+1] = arr[j] ← зсув вправо j = j - 1 arr[j+1] = key ← вставити на місце
Тести:Підказки:
Індекси i та j у коді — це байтові зміщення (для DW: i×2, j×2). Наприклад, arr[i] = [BX + SI], де SI = i × 2.
Внутрішній цикл зсуває елементи вправо: arr[j+1] = arr[j] → MOV DX, [BX+DI] / MOV [BX+DI+2], DX.
Для перевірки j &gt;= 0 використовуйте знакове порівняння: CMP DI, 0 / JL.
Зберігайте key у регістрі AX протягом усього внутрішнього циклу.
Запишіть відео, у якому:
Покажіть 3BSORT.ASM в TD: Відкрийте Dump і покажіть масив до сортування.
Пройдіть 2–3 ітерації внутрішнього циклу — покажіть момент обміну елементів.
Покажіть масив після сортування. Покажіть MINMAX.ASM в TD: Пройдіть кілька ітерацій циклу — покажіть, як оновлюються min та max.
Продемонструйте результат для масиву з від'ємними числами. Поясніть: яка різниця між знаковим (JL/JG) та беззнаковим (JB/JA) порівнянням? Що станеться з результатом MINMAX, якщо замінити JL на JB?
Lab06/
├── STUB.ASM ← шаблон
├── 1ARRAY.ASM ← масиви, Dump, режими адресації
├── 2SUM.ASM ← обхід масиву, процедура array_sum
├── 3BSORT.ASM ← сортування бульбашкою (дописаний)
├── 4MATRIX.ASM ← двовимірні масиви
├── MINMAX.ASM ← домашнє: мінімум/максимум
├── ROWSUM.ASM ← домашнє: сума рядка матриці
├── ISORT.ASM ← бонус: сортування вставкою
└── video.txt ← посилання на Loom <a data-tooltip-position="top" aria-label="https://www.cs.virginia.edu/~evans/cs216/guides/x86.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.cs.virginia.edu/~evans/cs216/guides/x86.html" target="_self">x86 Addressing Modes — CS:APP Guide</a> — огляд режимів адресації
<br><a data-tooltip-position="top" aria-label="https://visualgo.net/en/sorting" rel="noopener nofollow" class="external-link is-unresolved" href="https://visualgo.net/en/sorting" target="_self">Bubble Sort Visualization — VisuAlgo</a> — інтерактивна анімація сортувань
<br><a data-tooltip-position="top" aria-label="https://www.felixcloutier.com/x86/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.felixcloutier.com/x86/" target="_self">Instruction Set Reference — felixcloutier.com</a> — довідник інструкцій x86
]]></description><link>labs/lab06.html</link><guid isPermaLink="false">Labs/Lab06.md</guid><pubDate>Tue, 17 Feb 2026 08:18:49 GMT</pubDate></item><item><title><![CDATA[L06-NOTES-ENG. System Architecture]]></title><description><![CDATA[Quick reference for Lecture 6. See full materials in L06-ENG. System Architecture.md.Lecture 6 shifts from programming the processor to understanding the system around it. Part A completes the instruction set (bit shifts, instruction reference). Part B covers system architecture: Moore's Law, ICs, motherboard/bus, memory hierarchy, CPU modes, Von Neumann vs Harvard, and the Von Neumann bottleneck.Two families: linear shifts (bits lost) and rotates (bits wrap around).shl ax, 1 ; AX = AX * 2 (CF = old MSB)
shr ax, 1 ; AX = AX / 2 (CF = old LSB)
mov cl, 3
shl ax, cl ; AX = AX * 8
SAR preserves the sign bit — use for negative numbers. SHR fills with 0, giving wrong results for signed negatives.; 32-bit shift left using RCL (DX:AX)
shl ax, 1 ; CF = old MSB of AX
rcl dx, 1 ; old CF enters LSB of DX
Learn more:
<a data-tooltip-position="top" aria-label="https://xlogicx.net/Assembly_Is_Too_High-Level_-_Better_Call_SAL.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://xlogicx.net/Assembly_Is_Too_High-Level_-_Better_Call_SAL.html" target="_self">Assembly Is Too High-Level — Better Call SAL</a>
~100 basic 8086 mnemonics in categories: Data Movement, Arithmetic, Logic, Shift/Rotate, Control Flow, String Ops, Flag Control, I/O, Misc/System.Each instruction has: mnemonic, opcode (machine code), operands, affected flags.Notable: NOP (0x90) = XCHG AX, AX; LEA computes addresses without memory access; XLAT does AL = [BX+AL].Learn more:
<br><a data-tooltip-position="top" aria-label="http://pnx.tf/files/x86_opcode_structure_and_instruction_overview.pdf" rel="noopener nofollow" class="external-link is-unresolved" href="http://pnx.tf/files/x86_opcode_structure_and_instruction_overview.pdf" target="_self">x86 Opcode Structure and Instruction Overview (PDF)</a>
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/X86_instruction_listings#Original_8086/8088_instructions" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/X86_instruction_listings#Original_8086/8088_instructions" target="_self">Wikipedia: Original 8086/8088 Instructions</a>
Transistor count doubles approximately every 2 years (Gordon Moore, 1965/1975). Empirical observation, not a physical law. Self-fulfilling: industry R&amp;D roadmaps aligned to it.Dennard Scaling (1974): smaller transistors use less power. Broke down ~2006 → "power wall" → shift from frequency scaling to multi-core parallelism.Drivers: photolithography improvements, FinFET → GAA transistors, advanced packaging (UltraFusion, CoWoS), EUV lithography.Learn more:
<br><a data-tooltip-position="top" aria-label="https://ourworldindata.org/moores-law" rel="noopener nofollow" class="external-link is-unresolved" href="https://ourworldindata.org/moores-law" target="_self">What is Moore's Law? — Our World in Data</a>
<br><a data-tooltip-position="top" aria-label="https://www.apple.com/newsroom/2025/03/apple-reveals-m3-ultra-taking-apple-silicon-to-a-new-extreme/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.apple.com/newsroom/2025/03/apple-reveals-m3-ultra-taking-apple-silicon-to-a-new-extreme/" target="_self">Apple M3 Ultra — Apple Newsroom</a>
First microcontroller: TMS1000 (1974) — 4-bit CPU, 1KB ROM, 64x4 RAM, ~$2/chip.
First microprocessor: Intel 4004 (1971) — 2,300 transistors.Learn more:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/7400-series_integrated_circuits" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/7400-series_integrated_circuits" target="_self">7400-series ICs — Wikipedia</a>
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Texas_Instruments_TMS1000" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Texas_Instruments_TMS1000" target="_self">TMS1000 — Wikipedia</a>
Motherboard contains: CPU socket, memory slots, chipset, PCIe slots, storage connectors, clock generator, firmware (BIOS/UEFI).8086: 20-bit address → 1 MB. 80386: 32-bit → 4 GB. x86-64: 48-bit → 256 TB.Learn more:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/System_bus" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/System_bus" target="_self">System Bus — Wikipedia</a>
Quartz crystal oscillator generating clock pulses. One cycle = one unit of work.Clock speed ≠ performance. Also depends on: IPC, pipeline depth, cache size, memory bandwidth.Overclocking: running faster than rated spec → more heat, potential instability, reduced lifespan.DRAM access protocol: (1) place address on bus → (2) set READ signal → (3) wait ~200 cycles → (4) copy data.Cache: exploits temporal locality (reuse) and spatial locality (nearby access). L1 split: L1i (instructions) + L1d (data). Cache is transparent to programs.Learn more:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Cache_(computing)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Cache_(computing)" target="_self">Cache (computing) — Wikipedia</a>
<br><a data-tooltip-position="top" aria-label="https://people.cs.pitt.edu/~melhem/courses/1502p/Unit5/Unit5.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://people.cs.pitt.edu/~melhem/courses/1502p/Unit5/Unit5.html" target="_self">Modelling the Memory — Univ. of Pittsburgh</a>
Real Mode memory map: IVT (0–3FF), BDA (400–4FF), conventional memory (500–9FFFF), video (A0000–BFFFF), ROM (F0000–FFFFF).Protected Mode: segment registers hold selectors → GDT entries with base, limit, permissions. Programs use system calls to access hardware.In our course: Labs P1–P10 use Real Mode (DOSBox/TASM), P11–P15 use Protected Mode (Linux x86-64).Modified Harvard: modern CPUs are Von Neumann externally (unified RAM) but Harvard internally (split L1i/L1d cache).Learn more:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Von_Neumann_architecture" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Von_Neumann_architecture" target="_self">Von Neumann Architecture — Wikipedia</a>
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Harvard_architecture" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Harvard_architecture" target="_self">Harvard Architecture — Wikipedia</a>
Fetch → Decode → Execute (repeats continuously):
Fetch: IP → address bus → instruction bytes → Instruction Register; IP incremented
Decode: Control Unit reads opcode → determines operation, operands, extra memory access
Execute: ALU computes → result to destination → FLAGS updated → back to Fetch
Learn more:
<br><a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=Z5JC9Ve1sfI" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=Z5JC9Ve1sfI" target="_self">The Fetch-Execute Cycle — YouTube (Tom Scott)</a>
Shared bus means CPU can't fetch instructions and access data simultaneously. CPU-memory speed gap has grown dramatically (CPU ~1000x faster since 1980s, DRAM only ~10x).Mitigations:
Cache memory — 95–99% hit rate reduces effective access time
Split L1 cache — Modified Harvard: L1i + L1d allow parallel access
Prefetching — hardware predicts and pre-loads data
Pipelining — overlap fetch/decode/execute of different instructions
Branch prediction — guess jump outcomes (95–99% accuracy)
Out-of-order execution — execute ready instructions while waiting
Superscalar — multiple execution units run in parallel SHL/SHR for unsigned, SAL/SAR for signed shift operations (SAL = SHL)
Moore's Law: transistor count doubles ~every 2 years (60+ years)
ICs → microcontrollers → microprocessors = increasing integration
System bus = data + address + control buses, synchronized by clock
Memory hierarchy bridges the speed-cost gap (registers → cache → DRAM → disk)
Real Mode: no protection, 1 MB. Protected Mode: isolation, 4 GB, privilege rings
Von Neumann: unified memory. Harvard: separate. Modern CPUs: hybrid
Von Neumann bottleneck is the dominant performance limitation
Lab 6: Arrays and Addressing ModesWhat you'll do:
Use addressing modes: [BX], [SI], [BX+SI], [BX+SI+disp]
Declare arrays with DB, DW, DD
Observe memory layout in Turbo Debugger's Dump window
Apply bit shift instructions to array processing
]]></description><link>lectures/eng/l06-notes-eng.-system-architecture.html</link><guid isPermaLink="false">Lectures/ENG/L06-NOTES-ENG. System Architecture.md</guid><pubDate>Sun, 15 Feb 2026 15:07:00 GMT</pubDate></item><item><title><![CDATA[L06-Fetch-Decode-Execute]]></title><description><![CDATA[<img src="assets/l06-fetch-decode-execute.gif" target="_self">]]></description><link>assets/l06-fetch-decode-execute.html</link><guid isPermaLink="false">assets/L06-Fetch-Decode-Execute.gif</guid><pubDate>Sun, 15 Feb 2026 14:17:09 GMT</pubDate><enclosure url="assets/l06-fetch-decode-execute.gif" length="0" type="image/gif"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l06-fetch-decode-execute.gif"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L06-Von-Neumann-Architecture]]></title><description><![CDATA[<img src="assets/l06-von-neumann-architecture.png" target="_self">]]></description><link>assets/l06-von-neumann-architecture.html</link><guid isPermaLink="false">assets/L06-Von-Neumann-Architecture.png</guid><pubDate>Sun, 15 Feb 2026 14:17:09 GMT</pubDate><enclosure url="assets/l06-von-neumann-architecture.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l06-von-neumann-architecture.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L06-Harvard-Architecture]]></title><description><![CDATA[<img src="assets/l06-harvard-architecture.png" target="_self">]]></description><link>assets/l06-harvard-architecture.html</link><guid isPermaLink="false">assets/L06-Harvard-Architecture.png</guid><pubDate>Sun, 15 Feb 2026 14:17:09 GMT</pubDate><enclosure url="assets/l06-harvard-architecture.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l06-harvard-architecture.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L06-Memory-Hierarchy-Pyramid]]></title><description><![CDATA[<img src="assets/l06-memory-hierarchy-pyramid.png" target="_self">]]></description><link>assets/l06-memory-hierarchy-pyramid.html</link><guid isPermaLink="false">assets/L06-Memory-Hierarchy-Pyramid.png</guid><pubDate>Sun, 15 Feb 2026 14:17:09 GMT</pubDate><enclosure url="assets/l06-memory-hierarchy-pyramid.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l06-memory-hierarchy-pyramid.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L06-Cache-Hierarchy]]></title><description><![CDATA[<img src="assets/l06-cache-hierarchy.png" target="_self">]]></description><link>assets/l06-cache-hierarchy.html</link><guid isPermaLink="false">assets/L06-Cache-Hierarchy.png</guid><pubDate>Sun, 15 Feb 2026 14:17:09 GMT</pubDate><enclosure url="assets/l06-cache-hierarchy.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l06-cache-hierarchy.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L06-DRAM-Read-Timing]]></title><description><![CDATA[<img src="assets/l06-dram-read-timing.jpeg" target="_self">]]></description><link>assets/l06-dram-read-timing.html</link><guid isPermaLink="false">assets/L06-DRAM-Read-Timing.jpeg</guid><pubDate>Sun, 15 Feb 2026 14:17:09 GMT</pubDate><enclosure url="assets/l06-dram-read-timing.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l06-dram-read-timing.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L06-Clock-Signal]]></title><description><![CDATA[<img src="assets/l06-clock-signal.gif" target="_self">]]></description><link>assets/l06-clock-signal.html</link><guid isPermaLink="false">assets/L06-Clock-Signal.gif</guid><pubDate>Sun, 15 Feb 2026 14:17:09 GMT</pubDate><enclosure url="assets/l06-clock-signal.gif" length="0" type="image/gif"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l06-clock-signal.gif"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L06-System-Bus-Diagram]]></title><description><![CDATA[<img src="assets/l06-system-bus-diagram.png" target="_self">]]></description><link>assets/l06-system-bus-diagram.html</link><guid isPermaLink="false">assets/L06-System-Bus-Diagram.png</guid><pubDate>Sun, 15 Feb 2026 14:17:09 GMT</pubDate><enclosure url="assets/l06-system-bus-diagram.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l06-system-bus-diagram.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L06-Motherboard-Layout]]></title><description><![CDATA[<img src="assets/l06-motherboard-layout.png" target="_self">]]></description><link>assets/l06-motherboard-layout.html</link><guid isPermaLink="false">assets/L06-Motherboard-Layout.png</guid><pubDate>Sun, 15 Feb 2026 14:17:09 GMT</pubDate><enclosure url="assets/l06-motherboard-layout.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l06-motherboard-layout.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L06-TMS1000-Chip]]></title><description><![CDATA[<img src="assets/l06-tms1000-chip.jpeg" target="_self">]]></description><link>assets/l06-tms1000-chip.html</link><guid isPermaLink="false">assets/L06-TMS1000-Chip.jpeg</guid><pubDate>Sun, 15 Feb 2026 14:17:09 GMT</pubDate><enclosure url="assets/l06-tms1000-chip.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l06-tms1000-chip.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L06-IC-7408-AND-Gate]]></title><description><![CDATA[<img src="assets/l06-ic-7408-and-gate.png" target="_self">]]></description><link>assets/l06-ic-7408-and-gate.html</link><guid isPermaLink="false">assets/L06-IC-7408-AND-Gate.png</guid><pubDate>Sun, 15 Feb 2026 14:17:09 GMT</pubDate><enclosure url="assets/l06-ic-7408-and-gate.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l06-ic-7408-and-gate.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L06-Apple-M3-Ultra-Die]]></title><description><![CDATA[<img src="assets/l06-apple-m3-ultra-die.jpeg" target="_self">]]></description><link>assets/l06-apple-m3-ultra-die.html</link><guid isPermaLink="false">assets/L06-Apple-M3-Ultra-Die.jpeg</guid><pubDate>Sun, 15 Feb 2026 14:17:09 GMT</pubDate><enclosure url="assets/l06-apple-m3-ultra-die.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l06-apple-m3-ultra-die.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L06-Transistor-Count-Chart]]></title><description><![CDATA[<img src="assets/l06-transistor-count-chart.png" target="_self">]]></description><link>assets/l06-transistor-count-chart.html</link><guid isPermaLink="false">assets/L06-Transistor-Count-Chart.png</guid><pubDate>Sun, 15 Feb 2026 14:17:09 GMT</pubDate><enclosure url="assets/l06-transistor-count-chart.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l06-transistor-count-chart.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L06-Moores-Law-Original-Graph]]></title><description><![CDATA[<img src="assets/l06-moores-law-original-graph.png" target="_self">]]></description><link>assets/l06-moores-law-original-graph.html</link><guid isPermaLink="false">assets/L06-Moores-Law-Original-Graph.png</guid><pubDate>Sun, 15 Feb 2026 14:17:09 GMT</pubDate><enclosure url="assets/l06-moores-law-original-graph.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l06-moores-law-original-graph.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L06-x86-Opcode-Map]]></title><description><![CDATA[<img src="assets/l06-x86-opcode-map.png" target="_self">]]></description><link>assets/l06-x86-opcode-map.html</link><guid isPermaLink="false">assets/L06-x86-Opcode-Map.png</guid><pubDate>Sun, 15 Feb 2026 14:17:09 GMT</pubDate><enclosure url="assets/l06-x86-opcode-map.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l06-x86-opcode-map.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L06-Shift-Rotate-Summary-Table]]></title><description><![CDATA[<img src="assets/l06-shift-rotate-summary-table.png" target="_self">]]></description><link>assets/l06-shift-rotate-summary-table.html</link><guid isPermaLink="false">assets/L06-Shift-Rotate-Summary-Table.png</guid><pubDate>Sun, 15 Feb 2026 14:17:09 GMT</pubDate><enclosure url="assets/l06-shift-rotate-summary-table.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l06-shift-rotate-summary-table.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L06-ROR-Rotate-Right]]></title><description><![CDATA[<img src="assets/l06-ror-rotate-right.png" target="_self">]]></description><link>assets/l06-ror-rotate-right.html</link><guid isPermaLink="false">assets/L06-ROR-Rotate-Right.png</guid><pubDate>Sun, 15 Feb 2026 14:17:09 GMT</pubDate><enclosure url="assets/l06-ror-rotate-right.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l06-ror-rotate-right.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L06-ROL-Rotate-Left]]></title><description><![CDATA[<img src="assets/l06-rol-rotate-left.png" target="_self">]]></description><link>assets/l06-rol-rotate-left.html</link><guid isPermaLink="false">assets/L06-ROL-Rotate-Left.png</guid><pubDate>Sun, 15 Feb 2026 14:17:09 GMT</pubDate><enclosure url="assets/l06-rol-rotate-left.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l06-rol-rotate-left.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L06-Better-Call-SAL]]></title><description><![CDATA[<img src="assets/l06-better-call-sal.png" target="_self">]]></description><link>assets/l06-better-call-sal.html</link><guid isPermaLink="false">assets/L06-Better-Call-SAL.png</guid><pubDate>Sun, 15 Feb 2026 14:17:09 GMT</pubDate><enclosure url="assets/l06-better-call-sal.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l06-better-call-sal.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L06-SAR-Shift-Arithmetic-Right]]></title><description><![CDATA[<img src="assets/l06-sar-shift-arithmetic-right.png" target="_self">]]></description><link>assets/l06-sar-shift-arithmetic-right.html</link><guid isPermaLink="false">assets/L06-SAR-Shift-Arithmetic-Right.png</guid><pubDate>Sun, 15 Feb 2026 14:17:09 GMT</pubDate><enclosure url="assets/l06-sar-shift-arithmetic-right.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l06-sar-shift-arithmetic-right.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L06-SHR-Shift-Right]]></title><description><![CDATA[<img src="assets/l06-shr-shift-right.png" target="_self">]]></description><link>assets/l06-shr-shift-right.html</link><guid isPermaLink="false">assets/L06-SHR-Shift-Right.png</guid><pubDate>Sun, 15 Feb 2026 14:17:09 GMT</pubDate><enclosure url="assets/l06-shr-shift-right.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l06-shr-shift-right.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L06-SHL-Shift-Left]]></title><description><![CDATA[<img src="assets/l06-shl-shift-left.png" target="_self">]]></description><link>assets/l06-shl-shift-left.html</link><guid isPermaLink="false">assets/L06-SHL-Shift-Left.png</guid><pubDate>Sun, 15 Feb 2026 14:17:09 GMT</pubDate><enclosure url="assets/l06-shl-shift-left.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l06-shl-shift-left.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Консультація - 2026-02-14]]></title><description><![CDATA[Дата: 14 лютого 2026 р.
Формат: онлайн (відеоконференція)
Тривалість: ~49 хвНа початку консультації було обговорено нові додаткові матеріали, які з'явилися на навчальній платформі Distedu (Moodle). Кожен додаток оформлено як Книгу (Book) з трьома розділами-модальностями:
Відео — з візуалізаціями та презентаціями.
Аудіо (подкаст) — у форматі бесіди двох ведучих.
Текст — детальний опис теми, що містить також приклад коду на асемблері для самостійного випробування у Turbo Debugger.
Усі три модальності згенеровано за допомогою NotebookLM від Google на основі авторського тексту, який було попередньо написано та перевірено викладачем. Між розділами книги можна перемикатися через навігацію праворуч.Матеріали стосуються двох ключових тем:
Стекові фрейми (додаток до лекції 5, тиждень 5): <a data-tooltip-position="top" aria-label="https://distedu.ukma.edu.ua/mod/book/view.php?id=180719" rel="noopener nofollow" class="external-link is-unresolved" href="https://distedu.ukma.edu.ua/mod/book/view.php?id=180719" target="_self">посилання на Distedu</a>
<br>Сегментація пам'яті в Intel 8086 (додаток до лекції 3, тиждень 3): <a data-tooltip-position="top" aria-label="https://distedu.ukma.edu.ua/mod/book/view.php?id=180726" rel="noopener nofollow" class="external-link is-unresolved" href="https://distedu.ukma.edu.ua/mod/book/view.php?id=180726" target="_self">посилання на Distedu</a>
Студенти, які вже переглянули матеріали, дали позитивний відгук: аналогії (зокрема порівняння з дорогою — «маючи тулсет для побудови велодоріжок, потрібно побудувати потужну дорогу») допомогли зрозуміти теми на інтуїтивному рівні. Рекомендовано переглянути і відео, і аудіо, оскільки вони доповнюють одне одного: аудіо — детальніше, відео — більш сконцентроване, із візуальним супроводом.Важливо: якщо студенти самостійно генерують матеріали через ШІ — бути обережними з можливими неточностями. Викладач гарантує якість лише тих матеріалів, які він особисто перевірив.Якщо формат себе виправдовує, аналогічні матеріали будуть готуватися й далі — по ключових темах курсу.Значну частину консультації було присвячено поглибленому розбору роботи стека у процесорі 8086.Стек росте «вгору» у візуальному поданні, але адреси при цьому зменшуються — така конвенція прийнята в процесорі 8086 і більшості інших архітектур.
SP (Stack Pointer) — вказує на поточну вершину стека. Змінюється при кожній операції push/pop.
BP (Base Pointer) — фіксується на початку процедури і залишається незмінним протягом усього виконання функції. Це дозволяє звертатися до параметрів і локальних змінних за сталими зміщеннями, незалежно від подальших push/pop.
Покроковий процес виклику функції (call):
Параметри функції кладуться у стек (push).
Команда CALL зберігає адресу повернення (return address) у стеці.
Старий BP зберігається у стеці (push BP) — це критично для каскадних та рекурсивних викликів.
BP встановлюється на поточну позицію SP (mov BP, SP).
Локальні змінні розміщуються далі у стеку.
Оскільки адреса в 8086 займає 2 байти, параметри функції доступні починаючи зі зміщення BP+4 (BP+2 — це збережена адреса повернення).При каскадних викликах (функція викликає функцію, яка викликає ще одну) кожна функція має свій BP. Щоб при поверненні відновити контекст батьківської функції, старий BP обов'язково зберігається у стеку.Цей механізм — універсальний. Саме так працюють виклики функцій абсолютно у всіх програмах на C/C++ і подібних мовах. Конкретні назви регістрів можуть відрізнятися в інших архітектурах, але принцип ідентичний.У текстовому розділі Книги на Distedu є готовий код з англомовними коментарями (щоб уникнути проблем з кодуванням у Turbo Debugger), який можна скопіювати, запустити, натискати F8 і покроково спостерігати за станом стека.До 8086 процесори Intel (8080, 8085) були 8-бітними з 16-бітною шиною адреси, що давало доступ до 64 КБ (65 536 байт) пам'яті. Коли Intel створив 16-бітний процесор 8086, адресна шина була розширена до 20 біт — це дало 1 МБ пам'яті (2²⁰ байт).Проблема: усі обчислення всередині процесора відбувалися в 16-бітних регістрах (AX, BX, CX, DX тощо), а адресувати потрібно було 20-бітний простір. Зробити всі операції 20-бітними означало б зламати сумісність з усім існуючим програмним забезпеченням.Рішення — сегментація: 20-бітна фізична адреса формується з двох 16-бітних значень: адреси сегменту × 16 + зміщення. Програма «думає» лише про свій 16-бітний сегмент і не знає, де саме в мегабайті пам'яті вона розміщена. Зміщення всередині сегменту залишаються незмінними незалежно від фізичного розташування.Це рішення забезпечило повну зворотну сумісність: програми, написані для 8-бітних процесорів, продовжували працювати на 8086 без перекомпіляції. Intel зберігає цю сумісність до сьогодні — сучасні процесори при ввімкненні стартують у режимі, сумісному з 8086, а потім переключаються в сучасний режим. Це економить колосальні ресурси людства, адже інакше при кожному новому поколінні процесорів весь накопичений софт довелось би перекомпілювати.
Реальний режим (Real Mode) — те, що вивчається зараз на курсі. Програма може звертатися до будь-якої частини пам'яті без обмежень.
Захищений режим (Protected Mode) — з'явився з процесором 80386 (початок 90-х, 32 біти, 4 ГБ пам'яті). Процесор апаратно ізолює програми одна від одної: спроба однієї програми прочитати/записати пам'ять іншої програми призводить до апаратного виключення (exception). Це буде розглянуто окремо далі в курсі.
Перехід від 32 до 64 біт — менш драматичний: розширення шин, додавання нових можливостей (SIMD — Single Instruction Multiple Data), окрема лекція буде присвячена цій темі.
Було обговорено, як пов'язані стекові фрейми з відомою помилкою Stack Overflow. Якщо програма занадто глибоко вкладає виклики функцій (наприклад, нескінченна рекурсія), стек переповнюється:
У захищеному режимі процесор сам генерує виключення при виході за межі виділеної пам'яті.
У високорівневих мовах (Java тощо) віртуальна машина контролює розмір стека і зупиняє програму з відповідним повідомленням — StackOverflowException.
Стек як структура даних на процесорному рівні використовується для спеціальних стрибків: CALL/RET, а також INT/IRET (переривання — окрема тема наступних лекцій).
Було згадано Клода Шеннона (Claude Shannon) — математика та інформатика з MIT, чия магістерська дипломна робота вважається однією з найвизначніших у історії. Він показав, що на базових логічних гейтах (AND, OR, NOT) можна побудувати будь-яке обчислення — саме це студенти робили у перших двох лабораторних роботах. Цікавий факт: ШІ-модель Claude від Anthropic названа на його честь.Університет Берклі створив BSD (Berkeley Software Distribution) — Unix-подібну операційну систему, яка стала основою для ядра macOS від Apple. Це приклад того, як університетські проєкти стають фундаментом для комерційних продуктів світового масштабу.
Самостійна контрольна робота (спочатку запланована на 6-й тиждень) — перенесена на початок березня, коли повернеться офлайн-навчання. Формат: практичні завдання, що потребують розуміння базової теорії (формула обчислення фізичної адреси через сегмент і зміщення, порівняння адрес тощо).
Тести з теоретичних питань проводитимуться в аудиторії для мотивації — потрібно повторити матеріал лекцій.
Конкурс-змагання (competition) з програмування на асемблері — орієнтовно через 1–2 тижні. Буде лідерборд. Завдання — реалізувати складний алгоритм (на кшталт задачі комівояжера) на асемблері. Reference implementation надаватиметься на Python або Java.
В останній третині консультації обговорювалися алгоритмічні аспекти задачі Traveling Salesperson Problem (TSP) — знайти найкоротший маршрут, що проходить через усі N міст рівно по одному разу.
Точні алгоритми (наприклад, повний перебір — n!) — гарантують оптимальний результат, але при зростанні кількості міст стають обчислювально нереальними (експоненціальна складність).
Евристичні підходи (Nearest Neighbor, апроксимаційні алгоритми) — дають «достатньо хороший» результат (good enough) за прийнятний час.
Concorde — алгоритм від Університету Waterloo (branch and bound), який зміг точно обчислити TSP для 86 000 вузлів.
На прикладі з бензином: якщо ідеальний маршрут потребує 0,9 бака, а знайдений евристикою — 1 бак, витрачати всі обчислювальні ресурси світу заради економії 0,1 бака — недоцільно. Аналогічно, кубик Рубіка 3×3×3 має ~490 квінтильйонів варіантів і його можна розв'язати точно за ~9 годин (CPython) або ~30 хвилин (PyPy), але для кубика 4×4×4 точний розв'язок вже потребує трильйонів років.Для студентів, які хочуть справжнього виклику, є необов'язкове підвищеної складності завдання (так зване «завдання від Тайлера Дердена»): реалізувати TSP-подібну задачу на асемблері з використанням рекурсії (що безпосередньо пов'язано з темою стекових фреймів). Умова задачі передбачає зберігання пройдених міст у бітових масках. Аналогічна задача на LeetCode — «Shortest Path Visiting All Nodes» (рівень Hard). Для навчальних цілей кількість вузлів обмежена (до 12).
<br>Обов'язково переглянути додаткові Книги на Distedu (<a data-tooltip-position="top" aria-label="https://distedu.ukma.edu.ua/mod/book/view.php?id=180719" rel="noopener nofollow" class="external-link is-unresolved" href="https://distedu.ukma.edu.ua/mod/book/view.php?id=180719" target="_self">стекові фрейми</a>, <a data-tooltip-position="top" aria-label="https://distedu.ukma.edu.ua/mod/book/view.php?id=180726" rel="noopener nofollow" class="external-link is-unresolved" href="https://distedu.ukma.edu.ua/mod/book/view.php?id=180726" target="_self">сегментація</a>) — усі три розділи: відео, аудіо, текст. Аудіо можна прискорити до 1,5×.
Спробувати код із текстового розділу Книги у Turbo Debugger — покроково простежити, як змінюється стек при виклику функцій.
Повторити теорію перед тестами: формула фізичної адреси (сегмент × 16 + зміщення), адресація через BP.
Готуватися до конкурсу — розібратися з TSP, написати reference implementation на високорівневій мові, потім переписати на асемблер.
Залишати фідбек у відповідь на повідомлення з матеріалами — це допомагає вдосконалювати формат і мотивує інших студентів.
]]></description><link>office-hours/консультація-2026-02-14.html</link><guid isPermaLink="false">Office-hours/Консультація - 2026-02-14.md</guid><pubDate>Sat, 14 Feb 2026 14:15:04 GMT</pubDate></item><item><title><![CDATA[L03-Segments. Сегментна адресація — покрокове пояснення]]></title><description><![CDATA[
Додатковий матеріал до Лекції 03. Тут розглядається тільки адресація пам'яті в реальному режимі: навіщо потрібні сегменти, як обчислюється фізична адреса, які сегментні регістри для чого використовуються, і як це все виглядає в Turbo Debugger.
У 1978 році Intel створила процесор 8086 з 16-бітними регістрами. Один 16-бітний регістр може зберігати значення від 0 до 65 535 (= FFFFh). Це означає, що одним регістром можна адресувати щонайбільше 64 КБ пам'яті (2^16 = 65 536 байтів).Але інженери Intel хотіли адресувати 1 МБ пам'яті (2^20 = 1 048 576 байтів). Для цього потрібна 20-бітна адреса. А регістри лише 16-бітні.16 біт → максимум 64 КБ (FFFF = 65 535)
20 біт → максимум 1 МБ (FFFFF = 1 048 575) Не вистачає 4 біти!
Питання: Як отримати 20-бітну адресу з двох 16-бітних значень?Intel запропонувала формулу, яка комбінує два 16-бітні числа в одну 20-бітну адресу:Фізична адреса = Сегмент × 16 + Зміщення
Або, що те саме:Фізична адреса = Сегмент × 10h + Зміщення
Що означає «× 16»? У шістнадцятковій системі множення на 16 — це просто дописування нуля справа:Сегмент: 1234h
× 16: 1234h → 12340h (дописали 0)
Тобто формула на практиці виглядає так: Сегмент: 1234h Дописуємо 0: 12340h ← це 20-бітна «базова адреса сегмента»
+ Зміщення: + 0056h ─────── Фізична адреса: 12396h ← це реальна адреса в пам'яті
Цю пару записують як 1234h:0056h (або просто 1234:0056). Сегмент: 0000h × 16: 00000h
+ Зміщення: + 7C00h ─────── Фізична: 07C00h
Це знаменита адреса завантажувального сектора — BIOS завантажує перший сектор диска саме сюди. Сегмент: 07C0h × 16: 07C00h
+ Зміщення: + 0000h ─────── Фізична: 07C00h ← та сама адреса!
Важливий висновок: різні пари Сегмент:Зміщення можуть вказувати на ту саму фізичну адресу. 0000:7C00 і 07C0:0000 — це один і той самий байт у пам'яті. Сегмент: F000h × 16: F0000h
+ Зміщення: + FFFDh ─────── Фізична: FFFFDh ← майже вершина 1 МБ
Дано: CS = 1254h, IP = 0000h. Яка фізична адреса першої інструкції?
Відповідь 1254h × 16 = 12540h
+ IP = 0000h ────── Фізична: 12540h
Дано: DS = 087Dh, змінна msg має зміщення 0003h. Яка фізична адреса msg?
Відповідь 087Dh × 16 = 087D0h
+ зміщення = 0003h ────── Фізична: 087D3h
Програма складається з різних типів даних: інструкції (код), змінні (дані), стек. Процесор повинен знати, де в пам'яті лежить кожна з цих частин. Для цього є 4 сегментні регістри, кожен з яких «вказує» на свою область:Процесор сам обирає потрібний сегментний регістр залежно від операції. Програмісту не потрібно щоразу вказувати сегмент:Ключове правило: якщо ви звертаєтесь до даних — процесор використовує DS. Якщо працюєте зі стеком або через BP — SS. Код виконується з CS. Вам не потрібно про це думати — процесор робить це автоматично.Уявімо, що DOS завантажила нашу програму і встановила сегментні регістри:CS = 1254h DS = 1234h SS = 1244h SP = 0100h IP = 0000h
Тоді в пам'яті все розташоване так: Фізична адреса Що тут лежить ────────────── ───────────── 12340h ┐ 12341h │ Сегмент даних (.data) 12342h │ DS = 1234h → база = 12340h ... │ Тут змінні: msg, counter, ... 1243Fh ┘ 12440h ┐ 12441h │ Сегмент стека (.stack) 12442h │ SS = 1244h → база = 12440h ... │ SP = 0100h → вершина = 12540h 1253Fh ┘ 12540h ┐ 12541h │ Сегмент коду (.code) 12542h │ CS = 1254h → база = 12540h ... │ IP = 0000h → перша інструкція = 12540h 1263Fh ┘
Зверніть увагу на відстані між сегментами:DS = 1234h
SS = 1244h різниця = 10h сегментів = 10h × 16 = 100h байтів = 256 байтів
CS = 1254h різниця = 10h сегментів = 100h байтів
Ці відстані залежать від розміру кожного сегмента у вашій програмі.Коли DOS завантажує .EXE програму, вона автоматично встановлює CS (сегмент коду) та SS (сегмент стека). Але DS (сегмент даних) вона не встановлює на ваш .data — замість цього DS вказує на PSP (Program Segment Prefix) — службову структуру DOS.Тому в кожній програмі TASM перші дві інструкції — це:mov ax, @data ; AX = адреса сегмента даних
mov ds, ax ; DS = AX
Чому дві інструкції, а не одна? Процесор 8086 не дозволяє завантажувати безпосереднє значення прямо в сегментний регістр. Інструкції mov ds, @data не існує. Тому потрібен проміжний крок через регістр загального призначення (AX).mov ds, 1234h ← ПОМИЛКА! Такої інструкції не існує
mov ax, 1234h ← OK
mov ds, ax ← OK (з регістра в сегментний регістр)
Що буде, якщо забути? DS вказуватиме не на ваші дані, а на PSP. Усі звернення до змінних (mov ax, [msg]) читатимуть «сміття» з неправильної ділянки пам'яті. Програма поведеться непередбачувано.Сегменти в 8086 перекриваються. Кожен новий сегмент починається через 16 байтів (один «параграф») після попереднього. Це означає:Сегмент 0000h → база 00000h
Сегмент 0001h → база 00010h (на 16 байтів далі)
Сегмент 0002h → база 00020h (ще на 16 байтів далі)
...
Сегмент FFFFh → база FFFF0h
Кожен сегмент покриває 64 КБ (від бази до бази + FFFFh). Оскільки сусідні сегменти починаються лише через 16 байтів, вони перекриваються на 65 520 байтів (64 КБ - 16 байтів).Візуально: Фізична пам'ять (1 МБ) ────────────────────── 00000h ─┬── Сегмент 0000h ────────────────────┐ 64 КБ │ │ 00010h ─┼── Сегмент 0001h ───────────────────┐│ 64 КБ │ ││ 00020h ─┼── Сегмент 0002h ──────────────────┐││ │ ... │││ │ │││ 0FFFFh ─┘────────────────────────────────────┘│ 1000Fh ──────────────────────────────────────┘│ 1001Fh ───────────────────────────────────────┘
Наслідок: одна і та сама фізична адреса може бути записана багатьма різними парами Сегмент:Зміщення. Наприклад, фізична адреса 07C00h:Усі чотири — це один і той самий байт у пам'яті.Два найважливіші «тандеми» сегментних та звичайних регістрів:Фізична адреса наступної інструкції = CS × 16 + IP CS — сегмент, де лежить код
IP — зміщення поточної інструкції в межах цього сегмента
Після виконання кожної інструкції IP автоматично збільшується на її довжину
JMP, CALL змінюють IP (а далекий перехід — ще й CS)
Приклад:CS = 1254h, IP = 0005h Фізична адреса = 12540h + 0005h = 12545h ↑ Процесор прочитає інструкцію звідси
Фізична адреса вершини стека = SS × 16 + SP SS — сегмент, де лежить стек
SP — зміщення вершини стека (змінюється при PUSH/POP)
Стек росте вниз (PUSH зменшує SP, POP збільшує)
Приклад:SS = 1244h, SP = 00FEh Фізична адреса вершини = 12440h + 00FEh = 1253Eh ↑ Останнє PUSH записало сюди
Відкрийте CPU view (Alt+V, C). Подивіться на панель Registers (праворуч вгорі):AX=0000 BX=0000 CX=0050 DX=0000 SP=0100 BP=0000
DS=1234 ES=1234 SS=1244 CS=1254 IP=0000
Що перевірити: Обчисліть фізичну адресу першої інструкції: CS = 1254h, IP = 0000h → 1254h × 16 + 0 = 12540h
У панелі коду (ліворуч вгорі) перша інструкція має бути за цією адресою Виконайте mov ax, @data та mov ds, ax (F8 двічі): Після першого F8: AX = значення @data (наприклад, 087Dh)
Після другого F8: DS = те саме значення
Тепер DS вказує на ваш .data сегмент Подивіться на Dump (ліворуч внизу): Натисніть на панель Dump, потім Ctrl+G і введіть DS:0000
Ви побачите вміст вашого .data сегмента — рядки, числа, які ви визначили Порівняйте сегменти: Різниця між DS і CS покаже, скільки параграфів (по 16 байтів) займають дані
Різниця між SS і CS покаже відстань до стека Скопіюйте цей код у файл SEGM.ASM, скомпілюйте і відкрийте в Turbo Debugger. У CPU view (Alt+V, C) покроково (F8) пройдіть кожну інструкцію, обчислюючи фізичні адреси вручну і перевіряючи їх у дебагері.; SEGM.ASM - Segment addressing demo
; Compile: tasm SEGM
; Link: tlink SEGM
; Debug: td SEGM.EXE
;
; Open CPU view (Alt+V, C). Step with F8.
;
; EXERCISE: At each step, compute the physical address by hand
; and verify it matches what TD shows. .model small
.stack 100h .data
val1 dw 1234h ; a word variable at offset 0000
val2 dw 5678h ; a word variable at offset 0002
msg db 'Hi!$' ; a string at offset 0004 .code
main proc ; --- Step 1: Observe initial register values --- ; Before any instruction executes, note CS, DS, SS, SP, IP. ; DS does NOT point to .data yet! ; ; EXERCISE: compute physical address of this instruction ; = CS * 16 + IP ; --- Step 2: Initialize DS --- mov ax, @data ; AX = segment address of .data mov ds, ax ; DS now points to .data ; EXERCISE: after this, compute DS * 16 = base of data segment ; Try Ctrl+G in Dump pane, type DS:0000 ; You should see: 34 12 78 56 48 69 21 24 ; (val1=1234h and val2=5678h in little-endian, then 'Hi!$') ; --- Step 3: Read data through DS --- mov ax, [val1] ; AX = 1234h (read from DS:0000) mov bx, [val2] ; BX = 5678h (read from DS:0002) ; The CPU computes: physical addr = DS*16 + offset(val1) ; --- Step 4: Add and store result --- add ax, bx ; AX = 1234h + 5678h = 68ACh mov [val1], ax ; write back to DS:0000 ; EXERCISE: check Dump at DS:0000 — it should show AC 68 ; --- Step 5: Stack operations (SS:SP) --- push ax ; SP decreases by 2 push bx ; SP decreases by 2 ; EXERCISE: compute physical address of stack top ; = SS * 16 + SP ; Check the Stack pane (bottom right of CPU view) pop cx ; CX = 5678h (last pushed) pop dx ; DX = 68ACh (first pushed) ; SP is back to original value ; --- Exit --- mov ax, 4C00h int 21h
main endp
end main
Що перевіряти в TD:┌──────────────────────────────────────────────────────────────────┐
│ Фізична адреса (20 біт) │
│ │
│ ┌──────────┐ │
│ Сегмент: │ 1234h │ │
│ └──────────┘ │
│ │ × 16 (дописати 0) │
│ ▼ │
│ ┌───────────┐ │
│ │ 12340h │ ← база сегмента (20 біт) │
│ └───────────┘ │
│ │ │
│ │ + зміщення │
│ │ │
│ ┌──────────┐ │
│ Зміщення: │ 0056h │ │
│ └──────────┘ │
│ │ │
│ ▼ │
│ ┌───────────┐ │
│ │ 12396h │ ← фізична адреса в RAM │
│ └───────────┘ │
│ │
│ Процесор автоматично обирає сегментний регістр: │
│ Інструкції → CS:IP │
│ Дані → DS:зміщення │
│ Стек → SS:SP │
└──────────────────────────────────────────────────────────────────┘
Головне, що потрібно зрозуміти:
Фізична адреса = Сегмент × 16 + Зміщення (два 16-бітні → одне 20-бітне)
CS, DS, SS — три сегментні регістри для трьох частин програми (код, дані, стек)
Процесор автоматично обирає правильний сегмент залежно від операції
DS потрібно ініціалізувати вручну (mov ax, @data / mov ds, ax)
Різні пари Сегмент:Зміщення можуть вказувати на одну й ту саму фізичну адресу Лекція 03: L03. Архітектура x86 — регістри та пам'ять.md — повний матеріал
<a data-tooltip-position="top" aria-label="https://thestarman.pcministry.com/asm/debug/Segments.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://thestarman.pcministry.com/asm/debug/Segments.html" target="_self">Removing the Mystery from SEGMENT:OFFSET Addressing</a> — детальне пояснення англійською
<br><a data-tooltip-position="top" aria-label="https://faculty.kfupm.edu.sa/COE/shazli/coe205/Help/asm_tutorial_09.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://faculty.kfupm.edu.sa/COE/shazli/coe205/Help/asm_tutorial_09.html" target="_self">8086 Stack Tutorial</a>
]]></description><link>lectures/l03-segments.-сегментна-адресація-—-покрокове-пояснення.html</link><guid isPermaLink="false">Lectures/L03-Segments. Сегментна адресація — покрокове пояснення.md</guid><pubDate>Sat, 14 Feb 2026 09:39:19 GMT</pubDate></item><item><title><![CDATA[L05-Stack-Frames. Стекові фрейми — покрокове пояснення]]></title><description><![CDATA[
Додатковий матеріал до Лекції 05. Тут розглядається тільки механіка стекового фрейму: навіщо потрібні SP і BP, що таке пролог/епілог, і як виглядає стек на кожному кроці виклику процедури. Усі діаграми відповідають виду в Turbo Debugger (вікно Stack: молодші адреси зверху, старші — знизу).
В x86 стек росте від старших адрес до молодших. Якщо молодші адреси (початок памʼяті) зображати вверху, то стек – це стос тарілок, які ми ставимо одна на одну. Спочатку він пустий, додали тарілку – її адреса зменшила адресу верхівки стеку.
Верхівка стеку - це адреса в памʼяті, що завжди записується в регістр SP (Stack Pointer). Адреса Значення ────── ──────── ┌─ 0048h ... ← молодші адреси (верх пам'яті в TD) │ 004Ah ... │ 004Ch ... │ 004Eh ... стек росте ↑ СЮДИ │ 0050h [останній PUSH] ← SP вказує сюди │ 0052h ... │ 0054h ... └─ 0056h ... ← старші адреси (низ пам'яті в TD) PUSH → SP зменшується на 2, значення записується за новою адресою
POP → значення читається з поточної адреси SP, SP збільшується на 2
Процесор, при роботі з PUSH та POP використовує регістр SP.Регістр BP = Base Pointer використовується компіляторами і низькорівневими програмістами (це ми на цьому курсі!) для більш зручного використання стеку при виклику процедур.Уявімо процедуру add_two, яка приймає два параметри через стек. Для цього викликач (caller) кладе в стек два параметри і робить CALL, який "стрибає" у нашу процедуру. Одразу після виконання CALL add_two стек виглядає так: Адреса Значення ────── ──────── 0098h [ret addr] ← SP (верхівка стеку, вказує на адресу повернення) 009Ah [парам. 1] SP+2 009Ch [парам. 2] SP+4
Здається, все добре: параметр 1 лежить за адресою [SP+2], параметр 2 — за [SP+4].Але уявімо, що всередині процедури нам потрібно тимчасово зберегти регістр: push bx ; зберігаємо BX
Тепер стек змінився: Адреса Значення ────── ──────── 0096h [saved BX] ← SP (новий!) 0098h [ret addr] SP+2 009Ah [парам. 1] SP+4 (було SP+2!) 009Ch [парам. 2] SP+6 (було SP+4!)
Проблема: зміщення параметрів змінилися! Кожен PUSH/POP зсуває SP, і всі [SP+N] «їдуть». І тепер, щоб звернутися до першого параметру, нам потрібно додати +4 до SP, і це значення буде змінюватися щоразу як ми вирішили використати стек!
Якщо процедура складна (кілька PUSH, виділення локальних змінних, тощо) — відстежити правильні зміщення стає кошмаром.Рішення: на початку процедури зафіксувати поточну позицію стека в регістрі BP (Base Pointer). Після цього параметри завжди доступні через [BP+N], незалежно від того, що відбувається з SP далі. І наша N буде сталою для кожного окремого параметру.Ось для чого потрібен BP — це «якір», закріплений на початку фрейму.Розглянемо конкретний приклад. Код викликача: push 20 ; другий параметр push 10 ; перший параметр call add_two add sp, 4 ; очищення стека
Код процедури:add_two proc push bp ; ← пролог mov bp, sp ; ← пролог mov ax, [bp+4] ; перший параметр (10) add ax, [bp+6] ; другий параметр (20) pop bp ; ← епілог ret ; ← епілог
add_two endp
Що таке пролог? Перші дві інструкції процедури (push bp / mov bp, sp) називаються прологом. Пролог робить дві речі: push bp — зберігає старе значення BP на стек. Це потрібно тому, що викликач сам використовує BP для доступу до своїх параметрів і локальних змінних. Якщо ми просто перезапишемо BP, викликач після повернення втратить свою точку відліку. Тому ми спочатку зберігаємо його значення, а в кінці (в епілозі) відновимо через pop bp. mov bp, sp — фіксує поточну позицію стека в BP. З цього моменту BP стає нашим «якорем»: параметри завжди на [BP+4], [BP+6] і т.д., незалежно від подальших PUSH/POP. Далі — діаграми, що показують стан стека на кожному кроці. Припустимо, що початковий SP = 0100h. Адреса Значення ────── ──────── 0100h ... ← SP = 0100h 0102h ...
SP вказує на поточну вершину стека. BP має якесь значення від попередньої процедури (наприклад, BP = 0200h). Адреса Значення ────── ──────── 00FEh 0014h (=20) ← SP = 00FEh 0100h ...
SP зменшився на 2. Значення 20 (= 0014h) записане за адресою 00FEh. Адреса Значення ────── ──────── 00FCh 000Ah (=10) ← SP = 00FCh ← перший парам. 00FEh 0014h (=20) ← другий парам. 0100h ...
CALL автоматично робить PUSH IP (адресу наступної інструкції після CALL — це адреса повернення): Адреса Значення ────── ──────── 00FAh [ret addr] ← SP = 00FAh ← адреса повернення 00FCh 000Ah (=10) ← перший парам. 00FEh 0014h (=20) ← другий парам. 0100h ...
Тепер IP переходить на початок add_two. Ми всередині процедури.Зберігаємо старе значення BP (нехай це було 0200h) на стек: Адреса Значення ────── ──────── 00F8h 0200h ← SP = 00F8h ← збережений BP 00FAh [ret addr] ← адреса повернення 00FCh 000Ah (=10) ← перший парам. 00FEh 0014h (=20) ← другий парам. 0100h ... Адреса Значення Зміщення від BP ────── ──────── ──────────────── 00F8h 0200h ← SP = BP = 00F8h [BP+0] збережений BP 00FAh [ret addr] [BP+2] адреса поверн. 00FCh 000Ah (=10) [BP+4] перший парам. 00FEh 0014h (=20) [BP+6] другий парам. 0100h ...
Це момент істини. Тепер BP = SP = 00F8h. Подивіться на зміщення:Ці зміщення, які нам потрібні для доступу до параметрів, відносно нашого нового значення BP ніколи в нашій процедурі не зміняться, навіть якщо ми далі будемо робити PUSH/POP — бо BP зафіксований. mov ax, [bp+4] ; AX = 10 (перший параметр) add ax, [bp+6] ; AX = 10 + 20 = 30
SP і BP не змінюються. AX = 001Eh (= 30). Результат готовий.Що таке епілог? Це дзеркальне відображення прологу — дії, які «розбирають» фрейм перед поверненням до викликача:
pop bp — знімає зі стека збережене значення BP викликача і відновлює його. Тепер викликач знову зможе використовувати свій BP як і раніше.
ret — знімає адресу повернення зі стека і переходить за нею (повертає керування викликачу).
Якщо процедура мала локальні змінні (через sub sp, N), перед pop bp додають mov sp, bp — це «скидає» SP назад до BP, автоматично прибираючи все, що було виділено для локальних змінних.Отже, після pop bp: Адреса Значення ────── ──────── 00FAh [ret addr] ← SP = 00FAh 00FCh 000Ah (=10) 00FEh 0014h (=20) 0100h ... BP = 0200h (відновлений!)
RET робить POP IP — знімає адресу повернення зі стека і переходить за нею: Адреса Значення ────── ──────── 00FCh 000Ah (=10) ← SP = 00FCh 00FEh 0014h (=20) 0100h ...
Ми повернулися до викликача. IP тепер вказує на інструкцію add sp, 4.Викликач «прибирає» параметри зі стека, просто зсунувши SP: Адреса Значення ────── ──────── 0100h ... ← SP = 0100h (повернувся!)
SP повернувся до початкового значення. Стек чистий. AX = 30.Після прологу (push bp / mov bp, sp) фрейм стека завжди має таку структуру: Молодші адреси (верх у TD) ────────────────────────── ... ← SP (може зсуватися далі) [Локальні змінні] ← BP-2, BP-4, ... (якщо є) [Збережений старий BP] ← BP+0 (= BP) [Адреса повернення] ← BP+2 [Параметр 1] ← BP+4 [Параметр 2] ← BP+6 [Параметр 3] ← BP+8 ... ────────────────────────── Старші адреси (низ у TD)
Правило зміщень (16-бітний режим, near call):push bp ; 1. Зберегти BP викликача
mov bp, sp ; 2. BP = SP → фіксуємо «якір»
sub sp, N ; 3. Зарезервувати N байт для локальних змінних (якщо потрібні)
mov sp, bp ; 1. Відкинути локальні змінні (SP повертається до BP)
pop bp ; 2. Відновити BP викликача
ret ; 3. Повернутися (POP IP)
Якщо локальних змінних немає, рядки sub sp, N та mov sp, bp можна пропустити:; Спрощений пролог (без локальних змінних)
push bp
mov bp, sp ; Спрощений епілог
pop bp
ret
; Виклик: обчислити (a + b) * 2, результат у AX
; push 5 ; b = 5
; push 3 ; a = 3
; call calc
; add sp, 4 calc proc push bp mov bp, sp sub sp, 2 ; зарезервувати локальну змінну temp (2 байти) ; резервуємо, віднімаючи від "голови" стеку 2 байти ; [bp+2] = адреса повернення ; [bp+4] = a = 3 (перший параметр процедури) ; [bp+6] = b = 5 (другий параметр процедури) ; і.т.д.: параметри процедури із знаком "+" до BP, розташовані після ; адреси повернення ; [bp-2] = temp (локальна змінна) ; і.т.д.: локальні змінні процедури із знаком "-" до BP mov ax, [bp+4] ; AX = a = 3 add ax, [bp+6] ; AX = a + b = 8 mov [bp-2], ax ; temp = 8 (зберігаємо проміжний результат) shl ax, 1 ; AX = 8 * 2 = 16 mov sp, bp ; прибрати локальні змінні (миттєво "вичистили" стек ; від всіх змінних) pop bp ret
calc endp
Стек після sub sp, 2 на початку процедури (припустимо, початковий SP = 0100h): Адреса Значення Зміщення ────── ──────── ──────── 00F6h ???? ← SP = 00F6h [BP-2] temp зарезервовано 00F8h [old BP] ← BP = 00F8h [BP+0] збережений BP 00FAh [ret addr] [BP+2] адреса поверн. 00FCh 0003h (=3) [BP+4] a (1-й парам.) 00FEh 0005h (=5) [BP+6] b (2-й парам.) 0100h ... ← початковий SP до виклику процедури і розміщення для неї параметрів
Зверніть увагу: SP зсунувся вище BP (до молодших адрес), але BP залишається на місці. Параметри все одно доступні через [BP+4] і [BP+6].Коли одна процедура викликає іншу, кожна створює свій фрейм. Регістр BP утворює ланцюжок — кожен збережений BP вказує на попередній фрейм: Адреса Значення ────── ──────── ┌─ Фрейм inner() 0080h ... ← SP│ 0082h [old BP=0090h]← BP = 0082h ──┐ [BP inner] 0084h [ret addr] │ │ 0086h [парам.] │ │ └───────────────│────────── ┌─ Фрейм outer() │ 0088h ... │ │ 0090h [old BP=...] ← старий BP = 0090h ◄────┘ [BP outer] 0092h [ret addr] │ 0094h [парам.] │ └──────────────────
У Turbo Debugger ви можете «пройти» по ланцюжку: подивитися [BP] — це адреса попереднього фрейму; за тією адресою лежить ще один збережений BP, і так далі до самого main.bad_proc proc mov bp, sp ; BP перезаписали, але старе значення втрачене! mov ax, [bp+4] ret ; BP викликача зіпсований назавжди
bad_proc endp
Наслідок: після повернення BP викликача має невірне значення. Усі його [BP+N] тепер читають сміття.bad_proc2 proc push bp mov bp, sp push bx ; зберегли BX push cx ; зберегли CX ; ... робота ... pop bp ; ПОМИЛКА! Зняли CX, а не BP ret ; RET зніме BX замість адреси повернення → крах
bad_proc2 endp
Правило: кожному PUSH має відповідати POP у зворотному порядку. Або використовуйте mov sp, bp перед pop bp, щоб автоматично «скинути» все зайве.; Виклик: push 20 / push 10 / call proc
; Усередині proc після пролога:
mov ax, [bp+2] ; ПОМИЛКА! Це адреса повернення, не параметр!
Нагадування: [BP+2] — це завжди адреса повернення. Параметри починаються з [BP+4].Використовуйте вікно CPU (Alt+V, C). Воно має 5 панелей: код (ліворуч вгорі), регістри (праворуч вгорі), прапорці, дамп пам'яті (ліворуч внизу) та стек (праворуч внизу). Панель стека показує вміст пам'яті навколо SS:SP і оновлюється після кожного PUSH/POP — саме те, що потрібно для спостереження за фреймами.Як працювати:
Відкрийте CPU view: Alt+V, C
Покроково (F8) виконуйте інструкції
Після кожного кроку дивіться: Registers (праворуч вгорі): значення SP та BP
Stack (праворуч внизу): вміст стека — адреси та значення Порівняйте з діаграмами вище
Що шукати:
Після push bp: SP зменшився на 2, на вершині стека — старе значення BP
Після mov bp, sp: BP = SP (однакові значення!)
Параметри: подивіться за адресою BP+4, BP+6 — там мають бути ваші числа
Після pop bp: BP повернувся до попереднього значення
Після ret: SP «перестрибнув» адресу повернення ПЕРЕД ВИКЛИКОМ ПІСЛЯ ПРОЛОГУ ПІСЛЯ ЕПІЛОГУ + RET ────────────── ────────────── ─────────────────── [лок. змінні] ← SP [old BP] ← BP [ret addr] [парам. 1] ← SP [парам. 1] [парам. 1] ← SP [парам. 2] [парам. 2] [парам. 2] ... ... ... Дія: CALL Дія: push bp Дія: mov sp,bp ↓ mov bp, sp pop bp [ret addr] ← SP sub sp, N ret [парам. 1] [парам. 2] BP — фіксований «якір» SP повернувся, SP може зсуватися BP відновлений
Головне, що потрібно зрозуміти:
SP — рухливий вказівник, він змінюється при кожному PUSH/POP
BP — фіксований «якір» фрейму, встановлюється один раз у пролозі
Параметри завжди доступні через [BP+4], [BP+6], ...
Локальні змінні — через [BP-2], [BP-4], ...
Пролог (push bp / mov bp, sp) і епілог (pop bp / ret) — це обов'язковий шаблон
Скопіюйте цей код у файл FRAME.ASM, скомпілюйте (tasm FRAME / tlink FRAME) і відкрийте у Turbo Debugger (td FRAME.EXE). Покроково (F8) пройдіть через кожну інструкцію, спостерігаючи за SP, BP у вікні CPU (Alt+V, C) — панель стека праворуч внизу показує вміст стека.; FRAME.ASM - Stack frame step-by-step demo
; Compile: tasm FRAME
; Link: tlink FRAME
; Debug: td FRAME.EXE
;
; This program demonstrates:
; 1. add_two(10, 20) - simple procedure with stack frame
; 2. calc(3, 5) - procedure with a local variable
;
; Open CPU view (Alt+V, C). Step with F8. Watch:
; - Registers (top right): SP and BP values
; - Stack pane (bottom right): raw stack contents around SS:SP .model small
.stack 100h .code
main proc ; ===== Example 1: add_two(10, 20) ===== ; Expected result: AX = 30 push 20 ; push second parameter push 10 ; push first parameter call add_two ; call procedure add sp, 4 ; caller cleans up (2 params x 2 bytes) ; &gt;&gt;&gt; Check: AX should be 001Eh (=30) ; ===== Example 2: calc(3, 5) ===== ; Computes (a + b) * 2, expected result: AX = 16 push 5 ; push b = 5 push 3 ; push a = 3 call calc ; call procedure add sp, 4 ; caller cleans up ; &gt;&gt;&gt; Check: AX should be 0010h (=16) ; ===== Exit to DOS ===== mov ax, 4C00h int 21h
main endp ; -------------------------------------------------
; add_two: returns (param1 + param2) in AX
;
; Stack frame after prologue:
; [BP+0] = saved BP
; [BP+2] = return address
; [BP+4] = first parameter
; [BP+6] = second parameter
; -------------------------------------------------
add_two proc ; -- Prologue -- push bp ; save caller's BP mov bp, sp ; BP = SP (anchor the frame) ; At this point: BP = SP ; Try: in TD, check that [BP+4]=000Ah and [BP+6]=0014h ; -- Body -- mov ax, [bp+4] ; AX = first param (10) add ax, [bp+6] ; AX = AX + second param (20) = 30 ; -- Epilogue -- pop bp ; restore caller's BP ret ; pop return address, jump back
add_two endp ; -------------------------------------------------
; calc: returns (a + b) * 2 in AX
; Uses one local variable: temp at [BP-2]
;
; Stack frame after prologue:
; [BP-2] = temp (local variable)
; [BP+0] = saved BP
; [BP+2] = return address
; [BP+4] = a (first parameter)
; [BP+6] = b (second parameter)
; -------------------------------------------------
calc proc ; -- Prologue -- push bp ; save caller's BP mov bp, sp ; anchor the frame sub sp, 2 ; reserve 2 bytes for local var 'temp' ; SP is now 2 below BP. Local var at [BP-2]. ; -- Body -- mov ax, [bp+4] ; AX = a = 3 add ax, [bp+6] ; AX = a + b = 3 + 5 = 8 mov [bp-2], ax ; temp = 8 (store intermediate result) shl ax, 1 ; AX = 8 * 2 = 16 ; -- Epilogue -- mov sp, bp ; discard local variables (SP back to BP) pop bp ; restore caller's BP ret ; return to caller
calc endp end main
Що перевіряти в TD (покроково, F8):
Лекція 05: L05. Організація програми — стек і процедури.md — повний матеріал
Практика 05: Labs/Lab05.md — завдання з процедурами та стековими фреймами
<a data-tooltip-position="top" aria-label="https://faculty.kfupm.edu.sa/COE/shazli/coe205/Help/asm_tutorial_09.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://faculty.kfupm.edu.sa/COE/shazli/coe205/Help/asm_tutorial_09.html" target="_self">8086 Stack Tutorial</a>
]]></description><link>lectures/l05-stack-frames.-стекові-фрейми-—-покрокове-пояснення.html</link><guid isPermaLink="false">Lectures/L05-Stack-Frames. Стекові фрейми — покрокове пояснення.md</guid><pubDate>Sat, 14 Feb 2026 07:03:36 GMT</pubDate></item><item><title><![CDATA[Lab05]]></title><description><![CDATA[
Курс: Архітектура обчислювальних систем
Тиждень: 5
Тривалість: 2 академічні години
Тип: Практична робота під наглядом інструктора
Після завершення практики студент зможе:
Пояснити принцип роботи стеку: LIFO, SS:SP, напрямок росту.
Використовувати PUSH/POP для збереження та обміну значень.
Писати процедури з CALL/RET та розуміти роль адреси повернення.
Будувати стековий фрейм (BP) для передачі параметрів через стек.
Застосовувати бітові маски (AND, OR, TEST, SHR) для обробки даних.
Перегляньте лекційні матеріали за Тиждень 5 (стек, CALL/RET, стекові фрейми, конвенції виклику).Програма на TASM складається із сегментів: .CODE (код), .DATA (дані), .STACK (стек). Директива .MODEL small означає один сегмент коду та один сегмент даних. Сегментні регістри: CS → код, DS → дані, SS → стек.Ключове: стек росте вниз (до менших адрес). SS:SP завжди вказує на останній записаний елемент.Увага: RET переходить туди, куди вказує вершина стеку. Якщо стек «зіпсований» — програма «впаде».; Пролог
push bp ; зберегти BP викличної функції
mov bp, sp ; встановити базу фрейму
sub sp, N ; резерв для локальних змінних (якщо потрібні) ; Доступ до параметрів
mov ax, [bp+4] ; перший параметр
mov bx, [bp+6] ; другий параметр ; Епілог
mov sp, bp ; звільнити локальні змінні
pop bp ; відновити BP
ret
Створіть папку Lab05. Скопіюйте STUB.ASM з Lab04.Для кожного завдання: копіюйте STUB.ASM у новий файл з відповідним ім'ям.Скопіюйте STUB.ASM → 1STACK.ASM.Напишіть код, який:
Завантажує: AX = 000Ah, BX = 000Bh, CX = 000Ch
Виконує: PUSH AX, PUSH BX, PUSH CX
Виконує: POP DX, POP SI, POP DI
Перед запуском передбачте: яке значення буде в DX? в SI? в DI?Перевірте в TD. Спостерігайте за змінами SP у вікні Registers та вмістом вікна Stack (Alt+V, S).Допишіть у тому самому файлі (після попереднього коду, розділіть коментарем):Задача А. Обміняйте значення AX та BX, використовуючи тільки PUSH та POP (без MOV, без XCHG).
Вхід: AX = 1234h, BX = 5678h
Результат: AX = 5678h, BX = 1234h
Задача Б. Циклічна ротація: AX → BX → CX → AX.
Вхід: AX = 11h, BX = 22h, CX = 33h
Результат: AX = 33h, BX = 11h, CX = 22h
Скопіюйте STUB.ASM → 2BITS.ASM.Дано: MOV AL, 7Bh (0111 1011 у двійковій).
Це перша інструкція, може бути довільне число XY.Виділіть:
Молодший ніббл (0Bh) → збережіть у BL
Старший ніббл (07h) → збережіть у BH
У регістрі BX у вас має вийти 070B (або 0x0y для довільного числа xy в регістрі AL).Підказка: AND для маскування, SHR для зсуву.Дано: MOV AL, 35h (0011 0101).
За допомогою TEST перевірте, чи встановлений біт 0. Якщо число непарне — запишіть 1 у BL, інакше 0. Використайте умовний перехід.
Встановіть біт 7 в AL за допомогою OR. Яке нове значення AL?
Очистіть біти 3–0 в AL за допомогою AND. Яке значення залишиться?
Перевірте всі результати в TD.Скопіюйте STUB.ASM → 3PROC.ASM.Напишіть процедуру abs_val, яка повертає абсолютне значення числа в AX:
Якщо AX ≥ 0 — нічого не змінювати
Якщо AX &lt; 0 — змінити знак (NEG AX)
Використайте CMP та умовний перехід (з Практики 4)
Синтаксис процедури:abs_val proc ; ваш код ret
abs_val endp
Виклик: mov ax, -5 call abs_val ; AX = ?
У TD покроково пройдіть через CALL (F7):
Запишіть значення SP перед CALL
Яке значення з'явилося на стеку після CALL? (це адреса повернення)
Що відбувається з SP після RET?
Протестуйте з AX = -5, AX = 7, AX = 0.Напишіть процедуру add_two, яка приймає два параметри через стек та повертає їх суму в AX:
Використовуйте пролог/епілог зі стековим фреймом (див. теоретичний мінімум)
Параметри доступні через [BP+4] та [BP+6]
Виклик: push 20 ; другий параметр push 10 ; перший параметр call add_two add sp, 4 ; caller очищує стек ; AX має бути 30
У TD пройдіть покроково та запишіть у коментарях стан стеку після mov bp, sp:; [BP+6] = ?
; [BP+4] = ?
; [BP+2] = ? (return address)
; [BP+0] = ? (saved BP)
Напишіть процедуру max_of_two, яка приймає два параметри через стек та повертає більше значення в AX.
Повний пролог/епілог (PUSH BP / MOV BP, SP / ... / POP BP / RET)
Використовуйте CMP та умовний перехід
Тести:
push 8, push 15 → call max_of_two → AX = 000Fh (15)
push 42, push 3 → call max_of_two → AX = 002Ah (42)
push 7, push 7 → call max_of_two → AX = 0007h (7)
Скопіюйте STUB.ASM → 4TRACE.ASM. Напишіть цей код: mov ax, 3 push ax call double_it add sp, 2 ; Точка D: AX = ? mov ah, 4Ch int 21h double_it proc push bp mov bp, sp ; Точка A: намалюйте стек (4 верхні слова) mov ax, [bp+4] shl ax, 1 ; Точка B: AX = ? pop bp ; Точка C: SP = ? (відносно початкового) ret
double_it endp
Без запуску в TD, запишіть у коментарях:
Точка A: вміст стеку від BP вниз (saved BP, return address, параметр)
Точка B: значення AX
Точка C: значення SP відносно моменту перед push ax
Точка D: значення AX після повернення
Потім перевірте себе в TD.До кінця заняття:
1STACK.ASM — PUSH/POP, обмін, ротація
2BITS.ASM — виділення нібблів, перевірка/встановлення бітів
3PROC.ASM — abs_val, add_two, max_of_two
4TRACE.ASM — трасування стеку з коментарями
Закомічено всі файли
Дедлайн: 23:59 у день перед Практикою 6Напишіть процедуру bit_count, яка підраховує кількість встановлених бітів (1) у слові.Інтерфейс:
Параметр передається через стек
Результат повертається в AX
Повний стековий фрейм (BP)
Алгоритм: У циклі: перевірити молодший біт (TEST або AND) → якщо 1, інкрементувати лічильник → SHR на 1 → повторити поки значення ≠ 0.Тести (виклик з main):Вимоги:
Процедура має працювати для будь-якого 16-бітного значення
Виклик і повернення через стек (не через регістри)
Напишіть рекурсивну процедуру factorial:
Параметр N передається через стек
Результат повертається в AX
Базовий випадок: N ≤ 1 → повернути 1
Рекурсивний випадок: N × factorial(N-1)
Вимоги:
Повний стековий фрейм у кожному виклику (push bp / mov bp, sp / ... / pop bp / ret)
Зберігайте регістри, які використовуєте (PUSH/POP)
Тести:Обмеження: 7! = 5040 — останнє значення, що поміщається в 16 біт.Підказка щодо структури:factorial: prologue отримати N з [bp+4] якщо N &lt;= 1 → записати 1 в AX, перейти до epilogue інакше: push (N-1) call factorial add sp, 2 помножити AX на N ; AX = factorial(N-1), потрібно AX * N epilogue
Дана програма:main: mov ax, 5 push ax call square add sp, 2 push ax push 3 call add_two add sp, 4 ; Точка 4: AX = ? mov ah, 4Ch int 21h square proc ; Точка 1: стан стеку тут (до пролога) push bp mov bp, sp mov ax, [bp+4] mul ax ; Точка 2: DX = ?, AX = ? pop bp ret
square endp add_two proc push bp mov bp, sp ; Точка 3: намалюйте стек від BP до параметрів mov ax, [bp+4] add ax, [bp+6] pop bp ret
add_two endp
Без запуску в TD визначте:
Точка 1: вміст стеку (від SP вгору) на вході в square
Точка 2: значення DX та AX після MUL AX
Точка 3: вміст стеку від BP: [BP+0], [BP+2], [BP+4], [BP+6]
Точка 4: фінальне значення AX
Оформіть відповіді у текстовому файлі TRACE.TXT.Напишіть рекурсивну процедуру fibonacci:
Параметр N передається через стек
Результат повертається в AX
fib(0) = 0, fib(1) = 1
fib(N) = fib(N-1) + fib(N-2)
Тест: fib(10) = 55 (0037h)Складність: Кожний виклик породжує два рекурсивні виклики. Потрібно зберегти результат fib(N-1) на стеку перед викликом fib(N-2), а потім додати обидва результати.Запишіть відео, у якому:
Покажіть BITCNT.ASM в TD — пройдіть 2-3 ітерації циклу, поясніть, як TEST + SHR виділяють біти.
Покажіть FACTR.ASM в TD: Виклик factorial(3) — пройдіть до глибини рекурсії 2-3.
Покажіть стековий фрейм: де saved BP, де адреса повернення, де параметр. Поясніть TRACE.TXT — як ви визначили фінальне значення AX у Точці 4.
Lab05/
├── STUB.ASM ← шаблон (з Lab04)
├── 1STACK.ASM ← стек: PUSH/POP, обмін, ротація
├── 2BITS.ASM ← бітові маски: ніббли, TEST, OR, AND
├── 3PROC.ASM ← процедури: abs_val, add_two, max_of_two
├── 4TRACE.ASM ← трасування стеку з коментарями
├── BITCNT.ASM ← домашнє: підрахунок бітів
├── FACTR.ASM ← домашнє: рекурсивний факторіал
├── TRACE.TXT ← домашнє: трасування стеку
├── FIB.ASM ← бонус: рекурсивний Фібоначчі
└── video.txt ← посилання на Loom <a data-tooltip-position="top" aria-label="https://faculty.kfupm.edu.sa/COE/shazli/coe205/Help/asm_tutorial_09.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://faculty.kfupm.edu.sa/COE/shazli/coe205/Help/asm_tutorial_09.html" target="_self">8086 Stack Tutorial</a> — стек та операції зі стеком
<br><a data-tooltip-position="top" aria-label="https://blog.codefarm.me/2017/12/23/call-ret-x86-assembly/" rel="noopener nofollow" class="external-link is-unresolved" href="https://blog.codefarm.me/2017/12/23/call-ret-x86-assembly/" target="_self">CALL/RET in x86 Assembly</a> — механізм виклику процедур
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/X86_calling_conventions" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/X86_calling_conventions" target="_self">x86 Calling Conventions</a> — конвенції виклику
]]></description><link>labs/lab05.html</link><guid isPermaLink="false">Labs/Lab05.md</guid><pubDate>Mon, 09 Feb 2026 19:50:36 GMT</pubDate></item><item><title><![CDATA[L05-NOTES-ENG. Program Organization - Stack and Procedures]]></title><description><![CDATA[Quick reference for Lecture 05. See full materials in L05-ENG. Program Organization - Stack and Procedures.md.Four segments:Minimal template:.model small
.stack 100h
.data msg db 'Hello$'
.code
main proc mov ax, @data ; Initialize DS (required!) mov ds, ax ; ... code ... mov ax, 4C00h ; DOS exit int 21h
main endp
end main
Compilation flow: Source → tasm → .OBJ → tlink → .EXE → Loader → RAM (registers initialized)Physical address = Segment × 16 + Offset (20-bit, 1 MB in real mode)Learn more: <a data-tooltip-position="top" aria-label="https://faculty.kfupm.edu.sa/COE/shazli/coe205/Help/asm_tutorial_09.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://faculty.kfupm.edu.sa/COE/shazli/coe205/Help/asm_tutorial_09.html" target="_self">8086 Stack Tutorial</a>Opcode example: mov ax, 087Dh → bytes B8 7D 08
B8 = opcode for "MOV imm16 → AX"
7D 08 = value in little-endian (LSB first)
Little-endian (x86): LSB at lowest addressValue 0x1C2C3B4D in memory: 4D 3B 2C 1C
Instruction Pointer (IP): Auto-increments by instruction length after each execution. CS:IP = next instruction address.JMP uses relative offset, not absolute address:EB 08 → IP = IP + 2 + 08h
Labels are assembler abstractions — they don't exist in machine code.Data sizes:Decimal → Binary: Repeated division by 2, read remainders bottom-to-top23 ÷ 2 = 11 r1, 11 ÷ 2 = 5 r1, 5 ÷ 2 = 2 r1, 2 ÷ 2 = 1 r0, 1 ÷ 2 = 0 r1
→ 10111₂
Fractional binary: 0.101₂ = 2⁻¹ + 2⁻³ = 0.5 + 0.125 = 0.625₁₀
Convert decimal fraction → binary: repeated multiplication by 2
Octal (base 8): Each digit = 3 bits. Used in Unix permissions (chmod 755).Two's complement: Invert all bits, add 1. See L02 for details.Three practical patterns (see L04 for truth tables):Key idioms:
test al, 1 → check odd/even (sets flags, no result stored)
xor rax, rax → fastest way to zero a register
AND+OR combo → replace a bit field
<br>Learn more: <a data-tooltip-position="top" aria-label="https://adacomputerscience.org/concepts/proglang_bitwise_manipulation" rel="noopener nofollow" class="external-link is-unresolved" href="https://adacomputerscience.org/concepts/proglang_bitwise_manipulation" target="_self">Bitwise Manipulation — Ada CS</a>Declare: db (byte), dw (word), dd (dword)buffer db 16 dup(0AAh) ; 16 bytes initialized to AAh
empty db 100 dup(?) ; 100 uninitialized bytes
Three operations:mov dx, offset msg ; 1. Get base address
mov al, [msg] ; 2. Read element (index 0)
mov [msg + 1], 0FFh ; 3. Write element (index 1)
Address formula: Element addr = Base + (Index × ElementSize)Dynamic indexing:mov bx, 5
mov al, [msg + bx] ; Access msg[5]
2D arrays — stored in row-major order:array db 5*10 dup(0) ; 5 rows × 10 columns
2D address formula: Offset = (row × numColumns) + columnPrinciple: LIFO (Last In, First Out) — push 2,3,4 → pop returns 4,3,2Registers: SS (Stack Segment) + SP (Stack Pointer) → SS:SP = top of stackCritical: x86 stack grows downward (toward lower addresses)
PUSH decrements SP
POP increments SP
PUSH operation (2 steps):SUB SP, 2 ; 1. Decrement SP
MOV [SS:SP], AX ; 2. Write value
POP operation (2 steps):MOV AX, [SS:SP] ; 1. Read value
ADD SP, 2 ; 2. Increment SP
Bulk operations:
PUSHA — push all general-purpose registers (AX, CX, DX, BX, SP, BP, SI, DI)
POPA — pop all in reverse order
Swap idiom:PUSH AX ; Stack: [AX]
PUSH BX ; Stack: [AX, BX]
POP AX ; AX = old BX
POP BX ; BX = old AX
<br>Learn more: <a data-tooltip-position="top" aria-label="https://faculty.kfupm.edu.sa/COE/shazli/coe205/Help/asm_tutorial_09.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://faculty.kfupm.edu.sa/COE/shazli/coe205/Help/asm_tutorial_09.html" target="_self">8086 Stack Operations</a>Functions don't exist at CPU level — only CALL and RET.CALL = PUSH return address + JMP to function
RET = POP return address + JMP to it call myFunc ; Push next IP, jump to myFunc ; continues here after ret myFunc proc ; ... do work ... ret ; Pop address, jump back
myFunc endp
Warning: RET jumps to whatever is on top of the stack — if you push data without popping it, RET will jump to garbage!Parameter passing via stack (pushed right-to-left):push 20 ; Second param
push 10 ; First param
call add_numbers
add sp, 4 ; Caller cleans up (C convention)
Stack frame layout: [Parameter 2] ← BP+6 [Parameter 1] ← BP+4 [Return Address] ← BP+2 [Saved BP] ← BP [Local vars] ← BP-2, BP-4, ...
SP→ [...]
Function prologue/epilogue:; Prologue
push bp ; Save caller's BP
mov bp, sp ; Set up frame pointer
sub sp, N ; Reserve local variable space ; Epilogue
mov sp, bp ; Discard locals
pop bp ; Restore caller's BP
ret
Responsibilities:<br>Learn more: <a data-tooltip-position="top" aria-label="https://blog.codefarm.me/2017/12/23/call-ret-x86-assembly/" rel="noopener nofollow" class="external-link is-unresolved" href="https://blog.codefarm.me/2017/12/23/call-ret-x86-assembly/" target="_self">CALL/RET in x86 Assembly</a>
TASM segments (.data, .code, .stack) map to segment registers (DS, CS, SS)
Machine code = opcode bytes + parameter bytes in little-endian
IP auto-advances; JMP adds relative offset to IP
Arrays = contiguous memory; 2D uses row-major: offset = row × cols + col
Stack is LIFO, grows downward; PUSH decrements SP, POP increments SP
CALL = push return address + JMP; RET = pop + JMP
C calling convention: params on stack (right-to-left), BP for frame, AX for return value
What you'll practice:
Observing stack operations in Turbo Debugger (SS, SP, memory changes)
Implementing procedures with CALL/RET
Passing parameters via registers and via the stack
Declaring and accessing arrays in memory
Before lab:
Know how PUSH/POP affect SP and memory
Understand CALL/RET vs JMP
Practice reading stack frame diagrams
]]></description><link>lectures/eng/l05-notes-eng.-program-organization-stack-and-procedures.html</link><guid isPermaLink="false">Lectures/ENG/L05-NOTES-ENG. Program Organization - Stack and Procedures.md</guid><pubDate>Sun, 08 Feb 2026 18:06:23 GMT</pubDate></item><item><title><![CDATA[L05-ENG. Program Organization - Stack and Procedures]]></title><description><![CDATA[
Lecture 5 of Computer Systems Architecture course at NaUKMA.
After this lecture you will be able to:
Describe the TASM program structure (.data, .code, .bss, .stack segments) and explain how a program is loaded into memory
Read and interpret x86 machine code byte sequences, including opcode encoding and little-endian parameter storage
Explain how the Instruction Pointer (IP) advances through program execution and how JMP uses relative offsets
Declare and access one-dimensional and two-dimensional arrays in assembly using db, dup, and offset calculations
Explain the stack data structure (LIFO principle) and its CPU implementation using SS and SP registers
Use PUSH and POP instructions to save and restore register values
Describe how CALL and RET implement function calls using the stack
Explain calling conventions (C/C++ protocol): parameter passing, return values, and stack frame structure (BP)
This lecture marks a pivotal point in our study of computer systems architecture. We shift from understanding individual instructions and basic assembly operations to examining how complete programs are organized, stored in memory, and executed by the processor.The lecture title includes the Ukrainian phrase "Крапки над ї" (literally "dots over the letter i"), an idiom meaning "to dot the i's and cross the t's" or "to clarify the fine details". This reflects our pedagogical approach: we will revisit several fundamental topics from previous lectures—program structure, binary encoding, number systems, and logical operations—but now with the deeper understanding needed to see how these concepts work together in real programs.In earlier lectures, we examined assembly instructions in isolation: how MOV transfers data, how ADD performs arithmetic, how conditional jumps alter program flow. But real programs are not just sequences of instructions—they are structured entities with distinct regions for code, data, and runtime state. Understanding this organization is essential for: Memory Management: Different types of program content (executable instructions, initialized data, runtime variables) require different handling by the operating system and processor. Procedure Calls: Modern software is built from functions and procedures. The stack provides the mechanism for calling procedures, passing parameters, preserving state, and returning results. Runtime Behavior: When you compile and run a program, the assembler and operating system perform a complex sequence of operations to transform your source code into executable bytes in RAM, initialize segment registers, and begin execution. Debugging and Optimization: Understanding how programs are laid out in memory and how the stack operates is crucial for diagnosing errors and improving performance. This lecture is organized into four major sections:Section 1-2: "Dotting the I's" on Assembly Fundamentals — We revisit program structure, examining how TASM (Turbo Assembler) programs are organized into segments (.data, .code, .stack), how these segments are compiled into byte codes, and how the resulting program is loaded into RAM with segment registers initialized. We also clarify real mode addressing, which we introduced in Lecture 3 but now see in its full context.Section 3: Arrays in Memory — We examine how contiguous data structures are stored and accessed in memory, building on our understanding of addressing modes from Lecture 4.Section 4: The Stack in Modern Processors — We introduce the stack, a fundamental data structure used by all modern processors. We examine its LIFO (Last-In-First-Out) principle, the role of the stack segment (SS) and stack pointer (SP), stack growth direction, and the essential PUSH and POP operations.Section 5: Implementing Functions with the Stack — We explore how high-level language functions are implemented using assembly procedures. This includes the CALL and RET instructions, return addresses, stack frames, the base pointer (BP), local variables, parameters, and calling conventions like cdecl.By the end of this lecture, you will be able to:
Explain how TASM programs are structured into segments and how these segments map to memory
Describe the compilation and loading process that transforms assembly source code into executable bytes in RAM
Understand the role of segment registers (CS, DS, SS) in real mode addressing
Explain the LIFO principle and how the stack grows in memory
Use PUSH and POP operations correctly in assembly programs
Implement procedure calls using CALL and RET instructions
Understand how stack frames support local variables and parameter passing
Apply calling conventions to write correct and interoperable procedures
Let us begin by examining the structure of TASM programs and how they are loaded into memory.A typical TASM (Turbo Assembler) program is not simply a flat sequence of instructions. Instead, it is organized into distinct segments, each serving a specific purpose. This segmented structure mirrors the way the 8086 processor manages memory and allows the operating system to handle different types of program content appropriately.A basic TASM program consists of four primary segments:
.data segment — Initialized data (variables with initial values)
.code segment — Executable program instructions
.bss segment — Reserved uninitialized data (buffer space)
.stack segment — Explicit stack initialization
Let's examine a minimal TASM program template to see these segments in action:.model small
.stack 100h ; Reserve 256 bytes for stack .data
message db 'Hello, World!$'
counter dw 0 .bss
buffer db 100 dup(?) ; Reserve 100 uninitialized bytes .code
main proc mov ax, @data ; Initialize DS to point to data segment mov ds, ax ; Program code here mov ah, 09h ; DOS print string function lea dx, message int 21h mov ax, 4C00h ; DOS exit function int 21h
main endp
end main
The .data segment contains variables and constants that have explicit initial values. When the assembler processes this segment, it generates the actual byte values that will be stored in memory when the program loads.For example, consider these data declarations:.data
message db 'Hello$' ; 6 bytes: 48 65 6C 6C 6F 24 (ASCII codes)
number dw 1234h ; 2 bytes: 34 12 (little-endian format)
array db 10, 20, 30 ; 3 bytes: 0A 14 1E
The assembler converts each declaration into concrete bytes:
db 'Hello$' becomes the byte sequence: 48h, 65h, 6Ch, 6Ch, 6Fh, 24h (ASCII values for H, e, l, l, o, $)
dw 1234h becomes: 34h, 12h (note the little-endian byte order—least significant byte first, which we covered in Lecture 3)
db 10, 20, 30 becomes: 0Ah, 14h, 1Eh (decimal values converted to hexadecimal)
When the program loads into memory, these bytes are copied into RAM at the address specified by the DS (Data Segment) register.The .code segment contains the program's executable instructions. Each assembly language instruction is translated by the assembler into machine code—a sequence of bytes that the processor can directly execute.Consider this simple code sequence:.code
main proc mov ax, 1234h ; B8 34 12 (opcode B8, immediate value 34 12) add ax, 5 ; 05 05 00 (opcode 05, immediate value 05 00) ret ; C3 (opcode C3)
main endp
The assembler generates the following byte codes:
mov ax, 1234h → B8 34 12 (B8 is the opcode for "MOV AX, immediate", followed by the 16-bit value 34h 12h in little-endian)
add ax, 5 → 05 05 00 (05 is the opcode for "ADD AX, immediate", followed by 05h 00h)
ret → C3 (single-byte opcode for return)
As we discussed in Lecture 4, the processor fetches these bytes from memory (pointed to by CS:IP), decodes them to determine what operation to perform, and executes them. The Instruction Pointer (IP) automatically advances to the next instruction after each execution.The .bss segment (Block Started by Symbol, a historical name from early assemblers) is used for declaring uninitialized data—buffer space that will be used at runtime but doesn't need specific initial values..bss
buffer db 256 dup(?) ; Reserve 256 bytes, uninitialized
input dw ? ; Reserve 2 bytes, uninitialized
The key advantage of .bss is efficiency: the assembler doesn't need to generate 256 bytes of zeros for buffer. Instead, it simply notes that 256 bytes should be reserved. When the program loads, the operating system allocates this space (typically initialized to zero) without increasing the executable file size.The .stack segment explicitly reserves space for the program's stack. As we'll explore in detail in Section 4, the stack is a critical data structure used for procedure calls, local variables, and temporary storage..stack 100h ; Reserve 256 (100h) bytes for stack
This directive tells the assembler to allocate 256 bytes of memory for stack operations. The SS (Stack Segment) register will point to this region, and SP (Stack Pointer) will be initialized to the top of this space.Understanding how a TASM program is transformed from source code into executable bytes is essential for debugging and optimization. The process involves several distinct phases:When you invoke the assembler (e.g., tasm program.asm), it performs the following operations: Parsing: The assembler reads your source file, tokenizing directives (.data, .code), labels (main, message), and instructions (mov, add). Symbol Table Construction: It builds a symbol table mapping each label to its offset within its segment. For example, if message is the first variable in .data, it gets offset 0. If counter follows (and message was 7 bytes), counter gets offset 7. Instruction Encoding: Each assembly instruction is converted to its machine code equivalent: Opcode selection: The assembler looks up the appropriate opcode byte(s) for each instruction.
Operand encoding: Operands (registers, memory addresses, immediate values) are encoded according to the instruction format.
Address resolution: References to labels are resolved to actual offsets. Object File Generation: The assembler produces an object file (.OBJ) containing the encoded segments, relocation information, and symbol table. The linker (e.g., tlink program.obj) combines one or more object files into a single executable: Segment Combination: If multiple object files are linked, their .data segments are combined, .code segments are combined, etc. Address Fixup: References between segments (e.g., code accessing data) are resolved to final offsets. Executable Generation: The linker produces an .EXE or .COM file containing all segments with proper headers for the operating system loader. When you run the program (e.g., program.exe), the DOS or Windows loader performs these operations: Memory Allocation: The operating system allocates a block of memory large enough to hold all segments plus the Program Segment Prefix (PSP), a 256-byte control structure. Segment Loading: The contents of each segment from the executable file are copied into RAM: .data bytes are copied to the data region
.code bytes are copied to the code region
.stack space is allocated (typically filled with zeros)
.bss space is allocated and zeroed Register Initialization: The operating system initializes the segment registers: DS (Data Segment) is set to point to the .data segment base address
CS (Code Segment) is set to point to the .code segment base address
SS (Stack Segment) is set to point to the .stack segment base address
IP (Instruction Pointer) is set to 0 (or the entry point offset within the code segment)
SP (Stack Pointer) is set to the top of the stack (the size specified in .stack directive) Execution Start: Control transfers to CS:IP, and the processor begins fetching and executing instructions. As we introduced in Lecture 3, the 8086 processor uses segmented memory addressing. Each segment register holds a 16-bit segment base address, which is multiplied by 16 (shifted left 4 bits) to form a 20-bit physical address.Here's how segment registers map to program segments:The full address for any memory access is formed by combining a segment register with an offset:Physical Address = (Segment × 16) + OffsetFor example, if CS = 1000h and IP = 0020h:Physical Address = (1000h × 16) + 0020h = 10000h + 0020h = 10020hThis is the actual location in RAM where the processor fetches the next instruction.When you load a program into Turbo Debugger (TD), you can observe the register initialization that the loader performed. Immediately after loading (before executing any instructions), you might see:AX=0000 BX=0000 CX=0050 DX=0000 SP=0100 BP=0000 SI=0000 DI=0000
DS=1234 ES=1234 SS=1244 CS=1254 IP=0000
Notice:
IP = 0000: The instruction pointer is at offset 0 in the code segment (the first instruction)
CS = 1254h: Code segment base (this value varies depending on where DOS loaded the program)
DS = 1234h: Data segment base (typically set by the loader or by program initialization code)
SS = 1244h: Stack segment base
SP = 0100h: Stack pointer at the top of the 256-byte (100h) stack
The full address of the first instruction is CS:IP = 1254h:0000h = 12540h.Modern x86 processors (including Intel Core and AMD Ryzen CPUs) still support real mode, the original 8086 operating mode. When these processors power on, they start in real mode for backward compatibility. Real mode has several defining characteristics: Segmented Memory Addressing: Uses segment registers (CS, DS, SS, ES) as described above. 1 MB Address Space: The 20-bit physical address space limits addressable memory to 2^20 = 1,048,576 bytes (1 MB). No Memory Protection: Programs can access any memory location, including the operating system and other programs. This simplicity was acceptable in the single-tasking DOS era but is dangerous in modern multitasking systems. Direct Hardware Access: Programs can directly execute privileged instructions and access I/O ports without restriction. Although modern operating systems (Windows, Linux, macOS) run in protected mode or long mode (64-bit), real mode is still used:
BIOS Initialization: When a PC boots, the BIOS runs in real mode to initialize hardware before loading the operating system.
DOS Emulation: Windows' DOS emulator (for .COM programs in the command prompt) runs in a virtual real mode environment.
Embedded Systems: Some microcontrollers and embedded x86 systems operate exclusively in real mode.
Understanding real mode is valuable because:
It reveals the fundamental architecture of the x86 processor family
It simplifies learning—real mode has no virtual memory, paging, or privilege levels
Many embedded systems and bootloaders still use it
It provides insight into how modern protected mode evolved
<img alt="Real Mode Memory Addressing" src="assets/l05-real-mode-memory-addressing.png" target="_self">Real mode memory map: segment registers CS, DS, SS each point to a 64 KB segment within the 1 MB address space. The physical address is calculated as Segment × 16 + Offset.Let's examine a complete example that demonstrates all four segments:.model small
.stack 100h .data
prompt db 'Enter a number: $'
result db 'Result: $'
value dw 0 .bss
buffer db 10 dup(?) .code
main proc ; Initialize DS to point to data segment mov ax, @data mov ds, ax ; Print prompt mov ah, 09h lea dx, prompt int 21h ; Read input (simplified) mov ah, 01h int 21h sub al, '0' ; Convert ASCII to number mov byte ptr value, al ; Perform calculation mov al, byte ptr value add al, 10 mov byte ptr buffer, al ; Print result mov ah, 09h lea dx, result int 21h ; Exit mov ax, 4C00h int 21h
main endp
end main
Memory Layout After Loading:+------------------+ Low Memory
| PSP (256 bytes) | &lt;- DOS Program Segment Prefix
+------------------+
| .data segment | &lt;- DS points here
| prompt | "Enter a number: $"
| result | "Result: $"
| value | 00 00 (2 bytes)
+------------------+
| .bss segment | &lt;- Still part of data segment in small model
| buffer | (10 bytes, zeroed)
+------------------+
| .code segment | &lt;- CS points here, IP = 0
| main proc | B8 xx xx ... (machine code bytes)
+------------------+
| .stack segment | &lt;- SS points here, SP at top
| (256 bytes) | (100h bytes of stack space)
+------------------+ High Memory
When the program runs: DS initialization: The first two instructions (mov ax, @data and mov ds, ax) ensure DS points to the data segment. The assembler provides @data as a predefined symbol representing the data segment's paragraph address. Code execution: The processor fetches instructions from CS:IP, decoding and executing each one. Data access: Instructions like lea dx, prompt use DS as the default segment, so prompt resolves to DS:offset_of_prompt. Stack operations: If we used PUSH or CALL (which we'll cover later), they would use SS:SP. This clear separation of code, data, and stack is fundamental to understanding program organization and will be essential when we examine procedure calls and stack frames in later sections.When we write assembly language, we're working at a level of abstraction that's still far removed from what the CPU actually sees. The CPU doesn't understand mnemonics like mov or jmp—it only understands numbers. These numbers are called opcodes (operation codes), and they represent the actual machine instructions that the processor executes.Let's examine what happens when we compile a simple assembly instruction. Consider the first instruction in a typical DOS program:mov ax, @data
When the assembler compiles this instruction, it produces three bytes in memory:B8 7D 08
Let's break this down byte by byte:
B8 is the opcode for "move immediate word value into AX register"
7D 08 is the parameter—the actual value to move into AX (which represents the segment address where our data begins)
Notice something unusual about the parameter? The value appears to be "reversed"—we see 7D 08 in memory, but this actually represents the value 087D in hexadecimal (or 2173 in decimal). This reversal is not a mistake—it's a fundamental characteristic of x86 architecture called little-endian byte ordering.When you load a program in Turbo Debugger and switch to the CPU view, you can see exactly what the processor sees:cs:0000 B87D08 mov ax,087D
cs:0003 8ED8 mov ds,ax
cs:0005 ... ...
The left column shows the memory address (segment:offset), the middle column shows the raw bytes in memory, and the right column shows the human-readable assembly equivalent that the debugger has disassembled for us.One of the most important concepts in understanding machine code is byte ordering, also known as endianness. This determines how multi-byte values are stored in memory.In big-endian byte ordering, the most significant byte comes first in memory, just like how we normally write numbers. If we want to store the 32-bit hexadecimal value 1C2C3B4D in big-endian format:Memory Address: 0x1000 0x1001 0x1002 0x1003
Byte Value: 1C 2C 3B 4D (MSB) (LSB)
This is called "big-endian" because the "big end" (most significant byte) comes first. This ordering is used by some processor architectures like Motorola 68000, PowerPC (in big-endian mode), and many network protocols.In little-endian byte ordering (used by x86 processors), the least significant byte comes first in memory. The same value 1C2C3B4D would be stored as:Memory Address: 0x1000 0x1001 0x1002 0x1003
Byte Value: 4D 3B 2C 1C (LSB) (MSB)
This is called "little-endian" because the "little end" (least significant byte) comes first.The choice of little-endian in x86 architecture dates back to the Intel 8080 and 8086 processors. There are several practical advantages:
Easier address calculation: When reading a multi-byte value, you can start at a given address and process bytes in increasing order, with the least significant byte at the lowest address
Backwards compatibility: A 16-bit value can be read as an 8-bit value by just reading the first byte at the same address
Type casting simplification: Converting between different integer sizes is simpler in little-endian
However, little-endian can be confusing when examining memory dumps, because the bytes appear "backwards" compared to how we write hexadecimal numbers.<br><img alt="Big-endian vs Little-endian byte ordering" src="assets/l05-big-endian-vs-little-endian.png" target="_self">Big-endian stores bytes in the order we write them (most significant first). Little-endian (used by x86) reverses the byte order in memory — the least significant byte occupies the lowest address.Let's see this in action with our earlier instruction:mov ax, 087Dh
The value 087D needs to be stored as a 16-bit parameter. In little-endian format:
Least significant byte: 7D
Most significant byte: 08
So in memory, we see: B8 7D 08When the CPU reads this instruction, it knows that opcode B8 requires a 16-bit parameter, so it reads the next two bytes (7D 08) and interprets them in little-endian order as 087D.<br><img alt="Endianness memory layout comparison" src="assets/l05-endianness-memory-layout.png" target="_self">Side-by-side memory layout for value 0A0B0C0Dh showing address offsets in big-endian vs little-endian storage.The Instruction Pointer (IP) is a special register that keeps track of where the CPU should fetch the next instruction. Together with the Code Segment (CS) register, it forms the complete address of the next instruction to execute.Let's trace through the execution of our first two instructions:Initial state:CS = 0x1000 (example value)
IP = 0x0000
Current instruction address: CS:IP = 0x1000:0x0000
First instruction:Memory at cs:0000: B8 7D 08 → mov ax, 087Dh
This instruction is 3 bytes long (1 byte opcode + 2 bytes parameter). After the CPU executes this instruction:AX = 087Dh (value loaded)
IP = IP + 3 = 0x0003 (automatically incremented)
Next instruction address: cs:0003
Second instruction:Memory at cs:0003: 8E D8 → mov ds, ax
This instruction is 2 bytes long (1 byte opcode + 1 byte parameter encoding). The opcode 8E means "move to segment register," and the byte D8 encodes which registers are involved—in this case, "from AX to DS."After execution:DS = 087Dh (value from AX)
IP = IP + 2 = 0x0005
Next instruction address: cs:0005
The key insight is that IP automatically increments by the length of each instruction after it executes. The CPU doesn't need to "know" in advance how long an instruction is—the opcode itself determines this.You might wonder: why do we need the two-instruction sequence to load a value into DS?mov ax, @data ; Why can't we just do: mov ds, @data?
mov ds, ax
The answer lies in how x86 opcodes work. Let's examine the second instruction more closely:Opcode: 8E
Parameter: D8
The opcode 8E means "move word to segment register." But how does the CPU know we're moving from AX to DS? The answer is in the parameter byte D8.The byte D8 is not a value to move—it's a register encoding that specifies:
Source register: AX (binary: 000)
Destination register: DS (binary: 11)
Addressing mode: register-to-register
This encoding is packed into a single byte using a specific format called the ModR/M (Mode-Register-Memory) byte.So why can't we write mov ds, 087Dh directly? The assembler (TASM) will actually accept this syntax without complaint—but when you try to assemble it, you'll get an error or incorrect code. The reason is simple: there is no opcode for "load immediate value into segment register."This isn't an oversight—it's a deliberate design choice. The x86 instruction set has hundreds of opcodes, but there's a limit to how many can fit in the available encoding space. Each additional opcode requires:
Hardware wiring: Physical circuits on the CPU chip to decode and execute that instruction
Die space: More transistors mean larger chips and higher costs
Complexity: More instructions make the processor design more complex
The Intel engineers decided that loading immediate values into segment registers was uncommon enough that it didn't warrant a dedicated opcode. Instead, they expected programmers to use a general-purpose register as an intermediary:mov ax, immediate_value ; Opcode exists: B8
mov ds, ax ; Opcode exists: 8E
This is a classic example of a trade-off in computer architecture: convenience versus hardware complexity.While printed opcode tables exist (and there are many websites with complete x86 opcode references), Turbo Debugger gives us a powerful way to explore opcodes interactively.When you open the CPU view in Turbo Debugger (View → CPU), you see three panels:
Code panel: Shows disassembled instructions with their addresses and byte codes
Registers panel: Shows current values of all CPU registers
Dump panel: Shows raw memory contents
As you step through your program instruction by instruction (using F7 or F8), you can watch:
The bytes in memory (left side of code panel)
The assembly equivalent (right side of code panel)
The IP register incrementing
The effects on other registers and memory
Let's trace what happens when we execute our two-instruction sequence:Before execution:CS:0000 B87D08 mov ax,087D
IP = 0000
AX = ???? (undefined)
DS = ???? (undefined)
After first instruction:CS:0003 8ED8 mov ds,ax
IP = 0003
AX = 087D
DS = ???? (still unchanged)
After second instruction:CS:0005 ...
IP = 0005
AX = 087D
DS = 087D
Now that DS points to our data segment, if you switch the dump window to show DS:0000, you'll see the actual data from your program—for example, the string "Hello" if that's what you defined in your data segment.The jump instruction is where we see most clearly how assembly language is an abstraction. When we write:jmp exit
We think we're telling the CPU to "jump to the label called exit." But labels don't exist at the machine code level. They're purely an assembler concept—a convenience for humans.At the hardware level, JMP is just an instruction that modifies the Instruction Pointer. Specifically, for a short jump (within ±127 bytes), the instruction:jmp exit
Becomes:EB 08
Where:
EB is the opcode for "short jump" (add signed 8-bit offset to IP)
08 is the offset value
Let's be very precise about what this means: when the CPU executes this instruction, it performs the operation IP = IP + 2 + 08h. The "+2" accounts for the fact that IP has already advanced past the JMP instruction itself (which is 2 bytes long).Consider this code fragment:cs:0005 EB08 jmp exit
cs:0007 ... ...
cs:000F ... exit: mov ax, 4C00h
The assembler sees the label exit at offset 000F. The jump is at offset 0005. After the jump instruction executes, IP will be 0007 (pointing to the byte after the jump). To reach 000F, we need to add 08h more bytes.So the assembler calculates: offset_to_exit - (current_IP + instruction_length) = 000F - 0007 = 08hThis value 08 is what gets encoded as the parameter to the JMP instruction.Let's compare what humans see versus what the CPU sees:What humans see (assembly listing): jmp exit ; ... other code ...
exit: mov ax, 4C00h
What the CPU sees (raw memory):EB 08 ... [6 bytes of other code] ... B8 00 4C
The CPU has no concept of "exit" as a destination. It only knows:
EB is an opcode meaning "add the next byte to IP"
08 is the value to add
After execution, IP will point 8 bytes ahead of where it would have been
<br><img alt="CPU view vs human view in Turbo Debugger" src="assets/l05-cpu-view-annotated.png" target="_self">Turbo Debugger's CPU view, annotated to show what the processor sees (left panel: raw bytes and disassembly) versus what humans see (right panels: formatted register values and decoded memory). The green label "процесор" marks the CPU's perspective; the red label "людина" marks the human-readable interpretation.Understanding that labels are assembler constructs is crucial for several reasons:
Position-independent code: If you move code around in memory, jumps may break unless you recalculate offsets
Debugging: When examining memory dumps, you won't see labels—only addresses and offsets
Understanding limits: Short jumps can only reach ±127 bytes; longer jumps require different opcodes with 16-bit offsets
Self-modifying code: In very advanced (and dangerous) techniques, programs can modify their own jump instructions by changing the offset bytes
Let's walk through a complete example of how a jump works at the byte level.Suppose we have this code:cs:0005 EB 08 jmp exit
cs:0007 B4 09 mov ah, 09h
cs:0009 BA 00 00 mov dx, 0000h
cs:000C CD 21 int 21h
cs:000E EB 00 jmp $
cs:0010 B8 00 4C mov ax, 4C00h ; ← this is "exit"
Wait—the instruction at cs:0010 doesn't match our earlier claim that exit is at cs:000F. Let's recalculate.Actually, let me correct this. If we carefully count bytes:cs:0005 EB 08 jmp exit ; 2 bytes
cs:0007 B4 09 mov ah, 09h ; 2 bytes
cs:0009 BA 00 00 mov dx, 0000h ; 3 bytes
cs:000C CD 21 int 21h ; 2 bytes
cs:000E 90 nop ; 1 byte (filler)
cs:000F B8 00 4C mov ax, 4C00h ; 3 bytes ← exit label here
Before executing JMP:IP = 0005
Current address: cs:0005
Current bytes: EB 08
CPU decodes:
Read byte at cs:0005 → EB → "This is a short jump instruction"
Read next byte → 08 → "The offset is +8"
Calculate new IP: current IP (0005) + instruction length (2) + offset (08) = 000F
After executing JMP:IP = 000F
Next instruction: mov ax, 4C00h
The CPU has successfully "jumped" to the exit label—but it did so using only arithmetic, with no knowledge that a human programmer named this location "exit."The offset 08h represents how many bytes to skip after the jump instruction completes. Let's count them:cs:0007 B4 ← byte 1
cs:0008 09 ← byte 2
cs:0009 BA ← byte 3
cs:000A 00 ← byte 4
cs:000B 00 ← byte 5
cs:000C CD ← byte 6
cs:000D 21 ← byte 7
cs:000E 90 ← byte 8
cs:000F ← land here!
Exactly 8 bytes, as encoded in the instruction.This deep dive into opcodes, byte ordering, and jump mechanics reveals a fundamental truth about computer programming: every abstraction has a reality beneath it.When you write:mov ds, @data
You're using three levels of abstraction:
The label @data: An assembler construct representing a memory address
The mnemonic mov: A human-readable name for opcode B8 or 8E
The register name ds: A human-readable name for register encoding 11 in the ModR/M byte
At the bottom of all these abstractions are just numbers: bytes in memory that the CPU's digital circuits interpret as electrical signals, which trigger specific data paths and operations.Understanding this reality helps you:
Debug more effectively: When something goes wrong, you can look at the actual bytes
Understand performance: You can see which instructions are larger or more complex
Appreciate limitations: You understand why certain operations require multiple instructions
Write better code: You can make informed decisions about instruction choice and code organization
As we continue our exploration of the stack and procedure calls, keep this perspective in mind. Everything—from local variables to function calls to recursion—ultimately reduces to opcodes, memory addresses, and register manipulations. The art of assembly programming is learning to think fluently at both the abstract level (procedures, variables, control flow) and the concrete level (bytes, registers, addresses).This section revisits and completes our understanding of number systems in computer architecture. While we've covered binary representation and two's complement in Lecture 2, and hexadecimal notation in Lecture 3, here we'll fill in the remaining gaps: octal notation, fractional binary numbers, memory organization details, and systematic conversion algorithms. Think of this as "dotting the i's" on number representation.Before diving deeper into number systems, let's formalize the standard data sizes used in x86-64 architecture:Each multi-byte value has a most significant byte (MSB) containing the highest-order bits and a least significant byte (LSB) containing the lowest-order bits. For individual bits within a byte, we similarly refer to the most significant bit (MSB) and least significant bit (LSB).Endianness determines how multi-byte values are stored in memory:
Little-endian (x86-64, ARM in most modes): LSB stored at the lowest memory address. The value 0x12345678 in memory appears as 78 56 34 12.
Big-endian (many network protocols, some older processors): MSB stored at the lowest memory address. The value 0x12345678 in memory appears as 12 34 56 78.
Example: Storing the DWORD value 0xDEADBEEF at address 0x1000:Little-endian:
Address: 0x1000 0x1001 0x1002 0x1003
Value: EF BE AD DE Big-endian:
Address: 0x1000 0x1001 0x1002 0x1003
Value: DE AD BE EF
x86-64 uses little-endian exclusively. This matters when examining memory dumps or interfacing with network protocols (which typically use big-endian "network byte order").Recall from Lecture 2 that binary numbers use positional notation with powers of 2. To convert binary to decimal, we use positional expansion:110001₂ = 1×2⁵ + 1×2⁴ + 0×2³ + 0×2² + 0×2¹ + 1×2⁰ = 32 + 16 + 0 + 0 + 0 + 1 = 49₁₀
This method works by evaluating each bit position's contribution to the total value.Converting from decimal to binary requires a systematic algorithm. The repeated division by 2 method (also called the division-remainder method) works as follows:
Divide the decimal number by 2
Record the remainder (0 or 1) — this becomes a bit in the result
Use the quotient for the next iteration
Repeat until the quotient is 0
Read the remainders in reverse order (last remainder = MSB)
Example: Convert 23₁₀ to binary:23 ÷ 2 = 11 remainder 1 (LSB)
11 ÷ 2 = 5 remainder 1
5 ÷ 2 = 2 remainder 1
2 ÷ 2 = 1 remainder 0
1 ÷ 2 = 0 remainder 1 (MSB) Reading bottom-to-top: 23₁₀ = 10111₂
This algorithm is essentially Horner's method in reverse. Horner's method evaluates polynomials efficiently by factoring:10111₂ = ((((1)×2 + 0)×2 + 1)×2 + 1)×2 + 1
The division method reverses this process, extracting coefficients (bits) from least significant to most significant.<br><img alt="Decimal to binary conversion by repeated division" src="assets/l05-decimal-to-binary-conversion.png" target="_self" style="width: 400px; max-width: 100%;">Converting 23₁₀ to binary by repeated division by 2. The remainders read bottom-to-top give 10111₂.So far, we've only considered integer binary numbers. But binary can also represent fractional values using fixed-point notation, where a binary point separates integer and fractional parts (analogous to the decimal point in base-10).Just as decimal fractions use negative powers of 10 (0.25₁₀ = 2×10⁻¹ + 5×10⁻²), binary fractions use negative powers of 2:1011010.101₂ = 1×2⁶ + 0×2⁵ + 1×2⁴ + 1×2³ + 0×2² + 1×2¹ + 0×2⁰ + 1×2⁻¹ + 0×2⁻² + 1×2⁻³ = 64 + 0 + 16 + 8 + 0 + 2 + 0 + 0.5 + 0 + 0.125 = 90.625₁₀
The fractional part uses positions with weights: 2⁻¹ = 0.5, 2⁻² = 0.25, 2⁻³ = 0.125, 2⁻⁴ = 0.0625, and so on.Important limitation: Not all decimal fractions can be represented exactly in binary. For example, 0.1₁₀ has no exact binary representation (it requires an infinite repeating binary fraction: 0.0001100110011...₂). This is why floating-point arithmetic in computers sometimes produces unexpected results.To convert a decimal fraction to binary, we use repeated multiplication by 2:
Multiply the fractional part by 2
The integer part of the result (0 or 1) becomes the next bit
Keep the fractional part for the next iteration
Repeat until the fractional part becomes 0 (or until desired precision)
Read the integer parts in order (first = most significant fractional bit)
Example: Convert 0.116₁₀ to binary (to 10 bits precision):0.116 × 2 = 0.232 → bit 0 (2⁻¹ position)
0.232 × 2 = 0.464 → bit 0 (2⁻² position)
0.464 × 2 = 0.928 → bit 0 (2⁻³ position)
0.928 × 2 = 1.856 → bit 1 (2⁻⁴ position)
0.856 × 2 = 1.712 → bit 1 (2⁻⁵ position)
0.712 × 2 = 1.424 → bit 1 (2⁻⁶ position)
0.424 × 2 = 0.848 → bit 0 (2⁻⁷ position)
0.848 × 2 = 1.696 → bit 1 (2⁻⁸ position)
0.696 × 2 = 1.392 → bit 1 (2⁻⁹ position)
0.392 × 2 = 0.784 → bit 0 (2⁻¹⁰ position) Result: 0.116₁₀ ≈ 0.0001110110₂
Verification: 0.0001110110₂ = 2⁻⁴ + 2⁻⁵ + 2⁻⁶ + 2⁻⁸ + 2⁻⁹ = 0.0625 + 0.03125 + 0.015625 + 0.00390625 + 0.001953125 = 0.115234375₁₀ (close but not exact).Modern computers typically use IEEE 754 floating-point representation (covered in later lectures) for general-purpose fractional arithmetic, but fixed-point binary is still used in embedded systems, digital signal processing, and financial calculations where exact decimal representation isn't required.While hexadecimal (base-16) is the dominant alternative number system in computing (see Lecture 3 for the complete hexadecimal table and conversion methods), octal (base-8) occasionally appears in certain contexts:
Unix file permissions (e.g., chmod 755)
Some legacy assembly language syntaxes
Certain embedded systems documentation
Octal uses digits 0-7, where each octal digit represents exactly 3 bits:Converting octal to binary: Replace each octal digit with its 3-bit binary equivalent.Example: 2541₈ to binary:2 5 4 1
↓ ↓ ↓ ↓
010 101 100 001 Result: 2541₈ = 010101100001₂
Converting binary to octal: Group bits in threes from the right, then convert each group.Example: 1101110101₂ to octal:1 101 110 101 (group in threes, pad left if needed)
↓ ↓ ↓ ↓
1 5 6 5 Result: 1101110101₂ = 1565₈
Recall from Lecture 3 that hexadecimal uses 4 bits per digit (0-F), making it more compact than octal for representing binary data. This is why hexadecimal has largely supplanted octal in modern practice — a byte (8 bits) requires exactly 2 hex digits but needs 3 octal digits with bit misalignment.As covered extensively in Lecture 2 Section 3, two's complement is the standard representation for signed integers in modern computers. Quick reference for conversion:Positive to negative (two's complement):
Invert all bits (bitwise NOT)
Add 1
Example: +5 → -5 in 8-bit two's complement:+5 = 00000101
Invert: 11111010
Add 1: 11111011 = -5
For detailed comparison with sign-magnitude and ones' complement representations, refer to Lecture 2 Section 3.Recall from Lecture 4 Section 5 that we introduced the fundamental bitwise logical operations: AND, OR, XOR, and NOT, along with their truth tables. In this section, we move beyond the theory to explore practical bit manipulation patterns — the essential techniques for working with individual bits and bit fields in low-level programming.For reference, here are the operators we'll use (see Lecture 4 for complete truth tables):
AND (&amp;): Result bit is 1 only if both input bits are 1
OR (|): Result bit is 1 if at least one input bit is 1
XOR (^): Result bit is 1 if input bits are different
NOT (~): Inverts each bit (1→0, 0→1)
In assembly language (x86-64):
and rax, rbx — AND operation
or rax, rbx — OR operation
xor rax, rbx — XOR operation
not rax — NOT operation
test rax, rbx — AND without storing result (sets flags only)
The AND operation is the fundamental tool for isolating specific bits while zeroing out others. This is called masking because you apply a "mask" that determines which bits to keep.Key principle: x AND 1 = x and x AND 0 = 0. Therefore, a mask with 1s in positions of interest and 0s elsewhere will preserve those bits while clearing the rest.Example 1: Extracting the lower nibble (4 bits)Value: 01011101
Mask: 00001111 (AND) --------
Result: 00001101
The mask 00001111 preserves the rightmost 4 bits and clears the upper 4 bits. This is commonly used to extract hex digits, BCD values, or low-order portions of a value.Example 2: Checking the most significant bit (sign bit)Value: 10011111
Mask: 10000000 (AND) --------
Result: 10000000 (non-zero, so MSB was 1)
If the result is zero, the MSB was 0; if non-zero, the MSB was 1. This technique determines the sign of a two's complement number without arithmetic.Example 3: Testing if a number is odd or evenThe least significant bit determines parity: LSB=1 means odd, LSB=0 means even.Value: ???????X (X is LSB)
Mask: 00000001 (AND) --------
Result: 0000000X (1 if odd, 0 if even)
Assembly implementation using TEST:The test instruction performs AND but only updates flags without storing the result:test al, 1 ; Check if AL is odd
jnz odd_label ; Jump if non-zero (odd)
; ... even handling
odd_label:
; ... odd handling
Common masks:
0x0F or 0b00001111: Extract lower nibble
0xF0 or 0b11110000: Extract upper nibble
0x01 or 0b00000001: Extract LSB (parity check)
0x80 or 0b10000000: Extract MSB (sign check for bytes)
0xFF00: Extract upper byte of a 16-bit word
0x00FF: Extract lower byte of a 16-bit word
The OR operation sets specific bits to 1 while leaving others unchanged.Key principle: x OR 1 = 1 and x OR 0 = x. Therefore, a mask with 1s in positions to set and 0s elsewhere will force those bits to 1.Example 1: Setting the most significant bitValue: 00101011
Mask: 10000000 (OR) --------
Result: 10101011
This pattern is used to set flags, enable features, or mark values. For instance, setting the sign bit can convert a positive value's bit pattern to its two's complement negative equivalent's range.Example 2: Setting multiple bits (permission flags)Value: 00000000 (no permissions)
Mask: 01100100 (OR - enable read, write, execute bits) --------
Result: 01100100
In Unix-like systems, file permissions use bit fields where OR operations grant permissions: read (bit 2), write (bit 1), execute (bit 0) for each of user/group/other.Example 3: Combining AND and OR for selective modificationTo set specific bits while clearing others, use AND followed by OR:Original: 10110101
Step 1: 10110101 11110000 (AND - clear lower nibble) -------- 10110000
Step 2: 10110000 00001100 (OR - set bits 2 and 3) --------
Result: 10111100
This pattern replaces a bit field with a new value: first clear the field (AND with inverted mask), then insert new value (OR with new bits).Assembly example:and al, 0xF0 ; Clear lower nibble
or al, 0x0C ; Set bits 2 and 3
The XOR operation flips specific bits (0→1, 1→0) while leaving others unchanged.Key principle: x XOR 1 = NOT x and x XOR 0 = x. Therefore, a mask with 1s in positions to toggle and 0s elsewhere will flip those bits.Example 1: Toggling specific bitsValue: 10100010
Mask: 11101011 (XOR) --------
Result: 01001001
Each bit position where the mask has a 1 gets flipped; positions with 0 remain unchanged.Example 2: Inverting all bits (bitwise NOT using XOR)Value: 10100010
Mask: 11111111 (XOR - all 1s) --------
Result: 01011101
XORing with all 1s is equivalent to the NOT operation. This is sometimes used when a dedicated NOT instruction isn't available or when conditional inversion is needed.Example 3: Toggling a LED state or status flagSuppose bit 3 controls an LED (1=on, 0=off):Current: ????1??? (LED on)
Mask: 00001000 (XOR - toggle bit 3) --------
Result: ????0??? (LED off) Next: ????0??? (LED off)
Mask: 00001000 (XOR - toggle bit 3 again) --------
Result: ????1??? (LED on)
Repeated XOR with the same mask toggles the bit back and forth, useful for animation, blinking indicators, or state machines.Special property: XOR for swappingXOR can swap two values without a temporary variable:a = a XOR b
b = a XOR b (now b = original a)
a = a XOR b (now a = original b)
This works because XOR is self-inverse: x XOR y XOR y = x. While clever, modern compilers typically use move instructions for swapping, making this technique more of a curiosity than a practical optimization.Assembly example:xor al, 0x80 ; Toggle MSB (bit 7)
xor rax, rax ; Common idiom: clear register (x XOR x = 0)
The xor rax, rax idiom is a famous optimization: XORing a value with itself always yields zero, and on x86-64, this is faster and shorter than mov rax, 0.Checking conditions:
test al, 1 → check if odd
test al, 0x80 → check if negative (byte)
test rax, rax → check if zero
Extracting fields:
and al, 0x0F → extract lower nibble
and rax, 0xFF → extract lower byte (zero-extend)
Setting flags/permissions:
or al, 0x80 → set MSB (mark as negative, enable flag)
or rax, 0x02 → set bit 1 (enable feature)
Clearing bits:
and al, 0xFE → clear LSB (make even)
and rax, ~0x04 → clear bit 2 (disable feature)
Toggling states:
xor al, 0xFF → invert all bits
xor al, 0x08 → toggle bit 3
Efficient initialization:
xor rax, rax → fastest way to zero a register
These patterns form the foundation of bit manipulation in systems programming, device drivers, cryptography, compression, and graphics. Mastering them is essential for understanding how software controls hardware at the bit level.In x86 real mode, program code and data share the same memory space. When you assemble a program, the machine code instructions are written to memory first, and any data you define follows immediately after. This creates a continuous memory layout where your executable code transitions seamlessly into your data section.If you examine a program's memory using a debugger like Turbo Debugger, you'll see the raw bytes of your program. The CODE area contains the actual machine instructions (opcodes and operands), while the DATA area contains whatever values you've predefined — strings, numbers, or arrays. For example, a classic "Hello world!$" string appears as the corresponding ASCII bytes: 48h 65h 6Ch 6Ch 6Fh...This memory layout is fundamental to understanding how data structures work at the machine level. When we talk about arrays, we're really talking about consecutive bytes in this DATA region, accessible through their memory addresses.To create data in assembly, we use data definition directives. The most basic is db (Define Byte), which reserves one or more bytes in memory and optionally initializes them to specific values.Basic syntax:msg db 0AAh ; Reserve 1 byte, initialize to AAh
If you need multiple bytes, you can list them with commas:msg db 0AAh, 0AAh, 0AAh, 0AAh, 0AAh
But this approach becomes tedious very quickly. What if you need to reserve 1600 bytes? Typing each value individually is impractical and error-prone. This is where assembly provides a more elegant solution.The dup operator (short for "duplicate") allows you to reserve multiple identical values with a single statement. It's the assembly equivalent of array declaration in high-level languages.Syntax:label db count dup(value)
Examples:buffer db 16 dup(0AAh) ; 16 bytes, all initialized to AAh
zeros db 100 dup(0) ; 100 bytes, all initialized to 0
empty db 1600 dup(?) ; 1600 uninitialized bytes
The ? symbol indicates "don't care" — the memory is reserved but not explicitly initialized, leaving whatever values happened to be there. For exercise purposes, you might initialize with a distinctive pattern like AAh to make the data visible in a debugger.Why this matters:
Efficiency: One line replaces hundreds of individual declarations
Clarity: The intent (create an array of N elements) is immediately obvious
Flexibility: Easy to adjust size by changing a single number
This is how you create arrays in assembly. An array is simply a contiguous block of memory, and db count dup(value) gives you exactly that.Once you've defined an array, you need three fundamental operations: getting its address, reading elements, and writing elements.Important reminder for TASM: Always set the DS (Data Segment) register to your data segment before accessing data! The segment registers don't automatically initialize to your program's data segment..model small
.data msg db 16 dup(0AAh) ; Our array .code
start: mov ax, @data ; Get data segment address mov ds, ax ; Initialize DS ; Now we can safely access data
To work with an array, you often need its starting address (base address):mov dx, offset msg
The offset operator returns the offset address of msg within the data segment. This address points to the first byte of the array. You can now use DX to pass this address to procedures, calculate element positions, or perform I/O operations.To read the first element (index 0):mov al, [msg]
The square brackets [] denote a memory access — "read the byte at the address stored in msg." This loads the value at the array's base address into AL. The result: AL now contains 0AAh (the value we initialized).To write to a specific element, use indexed addressing:mov [msg + 1], 0FFh
This writes 0FFh to the second element (index 1). The expression msg + 1 calculates the address: base address of msg plus 1 byte offset. After this instruction, the array contains: AAh, FFh, AAh, AAh, AAh...If you step through these instructions in Turbo Debugger, you'll see the memory change in real-time: the second byte changes from AA to FF, and the AL register holds AA (the first element's value).Array element access follows a simple mathematical formula:Element address = Base address + (Index × Element size)For byte arrays (element size = 1 byte), this simplifies to:Element address = Base address + IndexThis is why msg + 1 accesses the second element — we add 1 to the base address.Examples:msg db 10 dup(0) mov al, [msg + 0] ; First element (base + 0)
mov al, [msg + 1] ; Second element (base + 1)
mov al, [msg + 9] ; Tenth element (base + 9)
For larger element sizes:If you have an array of words (2 bytes each):numbers dw 10 dup(0) ; Array of 10 words mov ax, [numbers + 0] ; First element (base + 0*2)
mov ax, [numbers + 2] ; Second element (base + 1*2)
mov ax, [numbers + 4] ; Third element (base + 2*2)
Notice that to access the Nth word, you must multiply the index by 2 (the word size). This is why pointer arithmetic in C/C++ automatically scales by element size — the compiler is doing exactly this calculation for you.Dynamic indexing with registers:For variable indices, use a register:mov bx, 5 ; Index = 5
mov al, [msg + bx] ; Access msg[5]
Or even:mov bx, offset msg ; BX = base address
mov si, 5 ; SI = index
mov al, [bx + si] ; Access msg[5]
This is the foundation of all array operations in assembly and, by extension, in every high-level language. When you write array[i] in C, the compiler generates exactly these kinds of address calculations.2D arrays are a natural extension of 1D arrays. Instead of a single sequence, you have rows and columns — but in memory, it's still stored as a single contiguous block.Declaration with calculation:array db 5*10 dup(0) ; 5 rows × 10 columns = 50 bytes
The assembler evaluates 5*10 at build time and reserves 50 bytes. This is convenient for making the array dimensions explicit in code.Explicit initialization by rows:For clarity, you can initialize each row separately:array db 10 dup(01h) ; Row 0: all 01h db 10 dup(02h) ; Row 1: all 02h db 10 dup(03h) ; Row 2: all 03h db 10 dup(04h) ; Row 3: all 04h db 10 dup(05h) ; Row 4: all 05h
This creates a 5×10 array where each row is filled with a distinct value, making it easy to visually identify rows in a debugger.Understanding how 2D arrays are stored is crucial for calculating element addresses. In x86 assembly (and most C-family languages), 2D arrays use row-major order: rows are stored sequentially in memory.Visualization:Logical view (5 rows × 10 columns): Row 0: 01 01 01 01 01 01 01 01 01 01
Row 1: 02 02 02 02 02 02 02 02 02 02
Row 2: 03 03 03 03 03 03 03 03 03 03
Row 3: 04 04 04 04 04 04 04 04 04 04
Row 4: 05 05 05 05 05 05 05 05 05 05 Memory layout (linear, 50 bytes): [01 01 01 01 01 01 01 01 01 01 02 02 02 02 02 02 02 02 02 02 ...] ^--- Row 0 -------------------^ ^--- Row 1 -------------------^
<br><img alt="2D array memory layout" src="assets/l05-2d-array-memory-layout.png" target="_self">Each row occupies 10 consecutive bytes. If the array starts at offset 120h (288₁₀), then:
Row 0 starts at: 288
Row 1 starts at: 288 + 10 = 298
Row 2 starts at: 288 + 20 = 308 (134h)
Row 3 starts at: 288 + 30 = 318
Row 4 starts at: 288 + 40 = 328
This row-major order is identical to C/C++ array storage, which is why multi-dimensional array access patterns transfer directly between assembly and high-level languages.To access an element at array[row][col], we need to calculate its offset from the base address.For 1-byte elements:Offset = (row × numberOfColumns) + column
For N-byte elements:Offset = (row × numberOfColumns × N) + (column × N)
The logic:
Skip row complete rows: each row has numberOfColumns elements of size N
Within the target row, skip column elements of size N
Worked example: Accessing array[2][3] in a 5×10 byte array
Row = 2
Column = 3
Number of columns = 10
Element size = 1 byte
Calculation:Offset = (2 × 10 × 1) + (3 × 1) = 20 + 3 = 23
Element address = Base address + 23In assembly:array db 5*10 dup(0) ; 5×10 array ; Access array[2][3]
mov bx, 0 ; Clear BX for index calculation
mov al, 2 ; AL = row number (0-indexed)
mov ah, 10 ; AH = total columns per row
mul ah ; AX = AL × AH = 2 × 10 = 20
add bx, ax ; BX = 20 (start of row 2)
add bx, 3 ; BX = 23 (column 3 within row 2) ; Now access the element
mov al, [array + bx] ; Read array[2][3] into AL
Step-by-step:
mul ah: Multiply row (2) by columns (10) → AX = 20
add bx, ax: BX now points to the start of row 2
add bx, 3: Advance BX by 3 columns
[array + bx]: Access the byte at base address + 23
This is the exact calculation your C compiler performs when you write array[2][3]. The high-level syntax is syntactic sugar for this low-level address arithmetic.1. Always initialize DS in TASMForgetting to set DS is one of the most common assembly mistakes:mov ax, @data
mov ds, ax
Without this, your data segment references will point to the wrong memory region, causing unpredictable behavior.2. Arrays are zero-indexedJust like in C/C++, the first element is at index 0. An array of 10 elements has valid indices 0-9, not 1-10. Off-by-one errors are common when transitioning from 1-indexed thinking (like in Pascal or MATLAB).3. Element size mattersWhen working with word (2-byte) or dword (4-byte) arrays, remember to scale your indices:numbers dw 10 dup(0)
mov bx, 3 ; Index 3
shl bx, 1 ; Multiply by 2 (word size)
mov ax, [numbers + bx] ; Access numbers[3]
Or use the assembler's type system:mov ax, [numbers + 3*2] ; Assembler calculates 3*2 = 6
4. Bounds checking is your responsibilityUnlike high-level languages, assembly won't stop you from accessing array[100] when the array only has 10 elements. You'll simply read whatever happens to be at that memory location — potentially critical data or code. Always validate indices before use.5. Use meaningful labelsInstead of cryptic names like d1 or buf, use descriptive labels:studentScores db 30 dup(0)
tempBuffer db 256 dup(?)
matrixData db 10*10 dup(0)
This makes code maintainable and reduces errors.6. Visualize with a debuggerTools like Turbo Debugger let you watch memory change as you step through code. This is invaluable for understanding array operations — seeing those AAh bytes change to FFh makes the abstract concepts concrete.Everything we've covered here is the foundation of how arrays work in C, C++, and other languages:C array declaration:char msg[16];
Equivalent assembly:msg db 16 dup(?)
C array access:msg[3] = 0xFF;
Equivalent assembly:mov [msg + 3], 0FFh
C 2D array:int matrix[5][10];
Equivalent assembly (assuming 2-byte ints):matrix dw 5*10 dup(0)
C 2D array access:x = matrix[2][3];
Equivalent assembly:mov ax, 2
mov bx, 10
mul bx ; AX = 2 * 10 = 20
mov bx, ax
shl bx, 1 ; Multiply by 2 (int size)
add bx, 3*2 ; Add column offset
mov ax, [matrix + bx]
When you understand assembly-level array operations, you gain insight into:
Why array access is O(1) time complexity (simple address arithmetic)
Why buffer overflows are possible (no automatic bounds checking)
Why multi-dimensional arrays can have cache performance issues (row-major traversal vs. column-major)
Why pointers and arrays are so closely related in C
This is the machine-level reality beneath every data structure in computing. Arrays are the simplest and most fundamental, but the principles extend to linked lists, trees, hash tables, and beyond — all built on these same address calculation techniques.The stack is one of the most fundamental data structures in computer science, characterized by a simple yet powerful principle: Last In, First Out (LIFO), also called First In, Last Out (FILO). Elements are added to and removed from only one end, called the "top" of the stack.<br><img alt="Stack Push and Pop step-by-step animation" src="assets/l05-stack-push-pop-steps.png" target="_self">Step-by-step animation of push 2, 3, 4, 5, 6 then pop 6, 5, 4, 3, 2 — demonstrating FILO behavior.Consider a sequence of operations:
Push 2 → Stack: [2]
Push 3 → Stack: [2, 3]
Push 4 → Stack: [2, 3, 4]
Push 5 → Stack: [2, 3, 4, 5]
Push 6 → Stack: [2, 3, 4, 5, 6]
Pop → Returns 6, Stack: [2, 3, 4, 5]
Pop → Returns 5, Stack: [2, 3, 4]
Pop → Returns 4, Stack: [2, 3]
Pop → Returns 3, Stack: [2]
Pop → Returns 2, Stack: []
Notice that the retrieval order (6, 5, 4, 3, 2) is exactly the reverse of the insertion order (2, 3, 4, 5, 6). This FILO behavior makes stacks ideal for tracking nested contexts, such as function calls, temporary variable storage, and expression evaluation.<br><img alt="Stack operations visual" src="assets/l05-stack-operations-visual.png" target="_self">Visual showing an empty stack, push 1, 2, 3, then pop 3 — with colored boxes representing stack elements.In practice, a stack is implemented as a contiguous region of memory, much like a one-dimensional array. The stack has a fixed capacity determined at program initialization, and elements are stored sequentially within this reserved space.The stack data structure is particularly well-suited for saving and restoring variable states. When a program needs to temporarily preserve the current state of registers or local variables—such as when calling a function—it can push these values onto the stack. Later, when the function returns, it pops the values back, restoring the original state.In assembly language programs, the stack must be explicitly reserved. For example, in Turbo Assembler (TASM), the directive:.stack 50
reserves 50 bytes of memory for the stack. This becomes a dedicated memory segment that the program can use for push and pop operations. The stack segment is identified by the SS (Stack Segment) register, which we'll discuss shortly.While a stack could theoretically be implemented using array-like formulas with manual pointer arithmetic, the x86 CPU provides dedicated hardware support for stack operations. Why?The answer lies in the frequency and criticality of stack usage. Context saving and restoring occur constantly in any nontrivial program:
Function calls must save return addresses
Function calls often save caller's register state
Nested function calls create a chain of contexts that must be unwound in reverse order
Interrupt handling requires immediate preservation of CPU state
Because these operations are so common and performance-critical, the CPU provides:
Specialized registers (SS and SP) for stack management
Dedicated instructions (PUSH, POP, CALL, RET) that manipulate the stack efficiently
Automatic pointer updates that handle stack growth/shrinkage
This hardware support makes stack operations both faster and less error-prone than manual implementation.The x86 architecture uses two key registers for stack management:SS (Stack Segment Register)
Contains the segment address of the stack
Defines where in memory the stack resides
Works in conjunction with SP to form complete memory addresses
SP (Stack Pointer)
Contains the offset of the current top of stack
Points to the last item pushed onto the stack
Automatically updated by PUSH and POP operations
The complete address of the top of the stack is formed by combining these registers: SS:SP.Here's a crucial concept that often surprises newcomers: on x86 architecture, the stack grows downward, meaning toward lower memory addresses.When a program starts:
SP is initialized to point to the end (bottom) of the allocated stack space—the highest address in the stack region
As items are pushed, SP decreases
As items are popped, SP increases
This may seem counterintuitive (why does the "top" move to lower addresses?), but it's a standard convention in x86 architecture. Think of it as stacking plates on a table: you start at the top of the available space and work your way down.High Memory | | [Initial SP position - bottom of stack] | ↓ [Stack grows this way] | | [Current SP position - top of stack] |
Low Memory
The PUSH instruction performs two operations atomically:
Decrement SP by the size of the operand (typically 2 bytes for 16-bit values)
Write the value to memory at the new SS:SP address
In essence, PUSH AX is equivalent to:SUB SP, 2 ; Move stack pointer down by 2 bytes
MOV [SS:SP], AX ; Write AX to the new top of stack
<br><img alt="Push and Pop equivalence to SUB/MOV and MOV/ADD" src="assets/l05-push-pop-equivalence.png" target="_self">PUSH is equivalent to SUB SP, 2 followed by MOV; POP is equivalent to MOV followed by ADD SP, 2.The PUSH instruction can accept various operands [4]:
General-purpose registers: AX, BX, CX, DX, DI, SI, BP, SP
Segment registers: DS, ES, SS, CS
Memory operands: Variables or memory addresses
Immediate values: Constants (on 80186 and later processors)
Example:MOV AX, 0FFFFh ; AX = FFFFh
PUSH AX ; Push AX onto stack
Assuming SP started at 0010h:
SP becomes 000Eh (0010h - 2)
Memory at SS:000Eh now contains FFFFh (stored in little-endian: FF FF)
The POP instruction also performs two operations atomically:
Read the value from memory at SS:SP
Increment SP by the size of the operand (typically 2 bytes)
In essence, POP AX is equivalent to:MOV AX, [SS:SP] ; Read value from top of stack into AX
ADD SP, 2 ; Move stack pointer up by 2 bytes
The POP instruction accepts the same operands as PUSH, with one exception: you cannot pop into the CS (Code Segment) register [4], as this would disrupt program execution in unpredictable ways.Example:POP BX ; Pop top of stack into BX
Assuming SP is at 000Eh:
BX receives the value stored at SS:000Eh
SP becomes 0010h (000Eh + 2)
Let's trace through a concrete example to visualize stack behavior:Initial State:SP = 0010h
Memory at SS:0010h is undefined (stack is empty)
After MOV AX, 0FFFFh:AX = FFFFh
SP = 0010h (unchanged)
After PUSH AX:SP = 000Eh (decreased by 2)
Memory at SS:000Eh: FF FFh (little-endian storage)
The stack has grown toward lower addresses:Address Value
-----------------
[0010] [undefined - below stack top]
[000E] FF FFh ← SP now points here (top of stack)
[000C] [undefined]
...
After POP BX:BX = 0FFFFh (value retrieved from stack)
SP = 0010h (increased by 2, back to original position)
The stack is now empty again, with SP back at its initial position.A classic demonstration of stack behavior is swapping two register values using the stack as temporary storage:; Initial values
MOV AX, 0FF55h ; AX = FF55h
MOV BX, 0CCDDh ; BX = CCDDh ; Swap using stack
PUSH AX ; Stack: [FF55h]
PUSH BX ; Stack: [FF55h, CCDDh]
POP AX ; AX = CCDDh, Stack: [FF55h]
POP BX ; BX = FF55h, Stack: [] ; Final values: AX = CCDDh, BX = FF55h
Let's trace the stack state:
After PUSH AX: Stack contains FF55h (from AX)
After PUSH BX: Stack contains FF55h, then CCDDh (BX on top)
After POP AX: AX receives CCDDh (top of stack), leaving FF55h
After POP BX: BX receives FF55h, stack is now empty
The LIFO behavior naturally reverses the order: what was pushed first (AX) comes out last, and what was pushed last (BX) comes out first. The result is a swap!The x86 architecture provides several instructions for stack manipulation:Basic Operations:
PUSH operand — Push a single value onto the stack
POP operand — Pop a single value from the stack
Bulk Operations:
PUSHA — Push all general-purpose registers (AX, CX, DX, BX, SP, BP, SI, DI) onto the stack in one operation
POPA — Pop all general-purpose registers in reverse order
The PUSHA and POPA instructions are particularly useful when entering and leaving functions that need to preserve the entire CPU register state. Instead of eight individual PUSH and POP instructions, a single instruction handles the entire context save/restore operation.Control Flow Operations:
CALL address — Push return address onto stack, then jump to address
RET — Pop return address from stack and jump to it
These instructions form the foundation of function calls in assembly language, which we'll explore in detail in the next section.Let's walk through a real example using Turbo Debugger to observe stack behavior in action.Program Setup:.stack 50 ; Reserve 50 bytes for stack
.code MOV AX, 55h PUSH AX POP BX
Initial State (after .stack 50 directive):
SP = 0032h (50 decimal in hexadecimal)
SS = 087Dh (example segment address)
The stack has 50 bytes of capacity, from SS:0000h to SS:0031h
SP points to 0032h, which is just beyond the allocated stack space (the "bottom")
After MOV AX, 55h:
AX = 0055h
SP = 0032h (no change; MOV doesn't affect the stack)
After PUSH AX:
SP = 0030h (decreased by 2: 0032h - 2 = 0030h)
Memory at SS:0030h contains: 55 00 (little-endian representation of 0055h)
The stack has grown toward lower addresses
If we examine memory at DS:0030h (assuming DS = SS), we see:Address Value
-----------------
[0032] [undefined - below stack top]
[0030] 55 00 ← SP points here
[002E] [undefined]
Notice the little-endian storage: the value 0055h is stored as 55 00, with the least significant byte (55h) at the lower address.After POP BX:
BX = 0055h (value retrieved from stack)
SP = 0032h (increased by 2: 0030h + 2 = 0032h)
Stack returns to initial empty state
This debugging walkthrough demonstrates several key concepts:
SP starts at the high end of the stack region (bottom)
PUSH decrements SP before writing
Values are stored in little-endian format
POP reads before incrementing SP
SP returns to its original position when the stack is empty
For programs compiled as COM files, the stack pointer SP is initialized to 0FFFEh [4], which provides approximately 64 KB of stack space (the maximum for a 64 KB segment). This generous allocation ensures that typical programs won't overflow the stack during normal execution.In contrast, programs that use explicit .stack directives (common in EXE files) specify their own stack size based on anticipated needs. Allocating too little stack space risks overflow; allocating too much wastes memory.
Stack is LIFO/FILO: Last In, First Out behavior is fundamental to understanding stack operations
Hardware support is essential: The CPU provides dedicated registers (SS, SP) and instructions (PUSH, POP) because stack operations are so common
Growth direction matters: x86 stacks grow toward lower addresses (PUSH decrements SP)
PUSH = decrement then write: SP is decreased before the value is stored
POP = read then increment: The value is retrieved before SP is increased
Little-endian storage applies: Multi-byte values are stored with the least significant byte at the lower address
Bulk operations save time: PUSHA/POPA handle all general-purpose registers at once
Context preservation is the killer app: The stack's ability to save and restore state makes function calls and interrupt handling practical
Understanding the stack is crucial for mastering assembly language programming. Nearly every nontrivial program relies on stack operations for function calls, local variable storage, and context switching. The concepts covered here form the foundation for procedure calls and parameter passing, which we'll explore in the next section.At the high level of languages like Java or C, we work with functions and procedures as fundamental building blocks. A function accepts parameters, performs some computation, and returns a value. A procedure does the same work but doesn't return a value (or returns void). Consider this simple Java example:public static int add(int a, int b) { return a + b;
}
This abstraction is so natural that we rarely think about what happens at the machine level. But here's the crucial insight: functions don't exist at the CPU level. The processor knows nothing about parameters, return values, or local variables. Instead, all function behavior is built from just two instructions: CALL and RET.One of the defining characteristics of a function is that after execution completes, control returns to the call site — the exact location where the function was invoked. This must work regardless of where the call originated. Consider this Java code:public static void main(String[] args) { int x = add(3, 5); // First call site int y = add(10, 20); // Second call site
}
The same add() function must return to different locations depending on where it was called from. How does the CPU know where to return? The answer is the return address, and the stack is the perfect place to store it.The x86 architecture provides two instructions that work together to implement function calls:CALL — invokes a function:
Pushes the return address (address of the next instruction) onto the stack
Jumps to the function's starting address
RET — returns from a function:
Pops an address from the stack
Jumps to that address
Here's a simple example in TASM syntax:; Main program mov ax, 5 call procedure ; Invoke the function ; Execution continues here after RET procedure proc add ax, 10 ret ; Return to caller
procedure endp
This looks simple, but the elegance is in what happens behind the scenes.The CALL instruction is essentially a combination of PUSH and JMP. When the CPU executes call multiply, it performs these operations:; What "call multiply" does internally:
sub sp, 2 ; Make room on stack (16-bit address)
mov SS:[sp], [ip+3] ; Push return address (IP + instruction length)
jmp multiply ; Jump to function
The return address is IP + 3 because the CALL instruction itself occupies 3 bytes (1 byte opcode + 2 byte address). After CALL executes, the stack contains the address of the instruction immediately following the CALL, and the instruction pointer has jumped to the function's entry point.Stack state after CALL:SP → [Return Address] ← Top of stack [Previous Data] ...
The RET instruction is the inverse — it's essentially a POP followed by a JMP. When the CPU executes ret, it performs:; What "ret" does internally:
jmp [SS:[sp]] ; Jump to address at stack top
add sp, 2 ; Remove address from stack
Note the order: RET reads the address from the current stack top, jumps to it, then adjusts the stack pointer. This means RET treats whatever is on top of the stack as a return address. If you've pushed data onto the stack during the function without popping it back, RET will jump to garbage!This equivalence means you could implement function calls manually:; Manual function call (equivalent to "call multiply")
lea bx, return_point ; Load return address
push bx ; Push it onto stack
jmp multiply ; Jump to function
return_point: ; Continue here multiply: ; Function body pop bx ; Get return address jmp bx ; Jump back
But CALL and RET do this efficiently in hardware.Now we understand how to call a function and return from it. But functions need to communicate with their callers — they need parameters (input) and return values (output). Remember: parameters don't exist at the CPU level. The CPU has no concept of "the first parameter" or "the return value." These are conventions we must implement ourselves.There are two primary approaches to passing parameters: via registers and via the stack.The simplest approach is to agree that parameters will be passed in specific registers:; Caller mov ax, 055h ; First parameter in AX mov bx, 0AAh ; Second parameter in BX call multiply ; Result returned in DX:AX ; Callee
multiply proc mul bx ; AX = AX * BX, upper bits in DX ret ; Return with result in DX:AX
multiply endp
This is fast and simple, but has severe limitations:
Limited parameters: x86 has only 8 general-purpose registers. What if you need 10 parameters?
No recursion: If multiply calls another function, those registers get overwritten
Fragile state: Every function must carefully manage which registers it modifies
This approach works for simple cases but doesn't scale.The solution is to use the stack — it can hold unlimited data and naturally supports nested calls:; Caller push 10 ; Push second parameter push 5 ; Push first parameter call multiply add sp, 4 ; Clean up stack (2 params × 2 bytes) ; Result in AX
Note the reverse order: we push parameters right-to-left. This is a convention (specifically, the C calling convention) that ensures the first parameter ends up closest to the return address on the stack.Here's where it gets tricky. Inside the function, when we try to access parameters from the stack, we must remember that the return address is on top:Stack when multiply starts:
SP → [Return Address] ← CALL pushed this! [Parameter 1: 5] [Parameter 2: 10] ...
This means we can't simply pop ax to get the first parameter — we'd get the return address instead! One solution is to temporarily remove and restore the return address:multiply proc pop bx ; Pop return address (save it) pop ax ; Pop first parameter (5) pop dx ; Pop second parameter (10) push bx ; Push return address back! mul dx ; AX = AX * DX ret ; Now RET will find the return address
multiply endp
This works, but it's error-prone. We must remember to restore the return address in the correct position, and if the function has multiple return paths, each must do this correctly.Professional code uses standardized calling conventions — agreements about how functions communicate. The C calling convention (used by C, C++, and many other languages) solves the parameter access problem elegantly using the base pointer register.The convention divides responsibilities between caller and callee:Caller Responsibilities:
Save registers AX, CX, DX if their values are needed after the call (these are caller-saved)
Push parameters onto stack in reverse order (right-to-left)
Execute CALL
Clean up parameters from stack after return (in C convention; not in Pascal convention)
Find return value in AX (or DX:AX for 32-bit values)
Callee Responsibilities:
Save the old base pointer: push bp
Set up new base pointer: mov bp, sp (BP now points to saved BP; BP+2 is return address; BP+4 is first parameter)
Reserve stack space for local variables: sub sp, N
Save any registers it will modify (except AX, CX, DX which caller saves)
Perform computation
Store return value in AX
Restore saved registers
Restore stack pointer: mov sp, bp
Restore base pointer: pop bp
Execute RET
Following this convention creates a stack frame — a structured region of stack memory for each function call. Here's what the stack looks like during execution of a function:Higher addresses [Parameter 3] ← BP+16 [Parameter 2] ← BP+12 [Parameter 1] ← BP+8 [Return Address] ← BP+4 [Saved BP] ← BP (current base pointer points here) [Local Variable 1] ← BP-2 [Local Variable 2] ← BP-4 [Saved Registers]
SP→ [...] ← Current stack top
Lower addresses
<br><img alt="C/C++ calling convention stack frame diagram" src="assets/l05-stack-frame-diagram.png" target="_self">Stack frame layout showing saved registers, local variables, saved EBP, return address, and parameters with [ebp+N] annotations.The beauty of using BP is that it provides a stable reference point. Even as the function pushes and pops data (changing SP), BP remains fixed, allowing consistent access to parameters and local variables:
Parameters: [bp+8], [bp+12], [bp+16], ...
Local variables: [bp-2], [bp-4], [bp-6], ...
Here's a complete implementation of a function following the C calling convention:; Caller code push 20 ; Second parameter push 10 ; First parameter call add_numbers add sp, 4 ; Clean up stack ; Result now in AX ; Callee code
add_numbers proc ; Function prologue push bp ; Save old base pointer mov bp, sp ; Set up new base pointer sub sp, 4 ; Reserve space for 2 local variables ; Access parameters mov ax, [bp+4] ; First parameter (10) mov bx, [bp+6] ; Second parameter (20) ; Computation add ax, bx ; AX = 10 + 20 = 30 ; Function epilogue mov sp, bp ; Restore stack pointer pop bp ; Restore base pointer ret ; Return with result in AX
add_numbers endp
The sequence push bp / mov bp, sp at the start is called the function prologue, and mov sp, bp / pop bp at the end is the function epilogue. You'll see this pattern in virtually every compiled C function.Understanding calling conventions is essential for several reasons:
Debugging: When you examine assembly code in a debugger, you need to understand stack frames to navigate function calls and inspect variables
Interfacing: If you write assembly functions called from C, or call C functions from assembly, you must follow the convention exactly
Performance: Knowing what happens during function calls helps you understand their cost and make informed optimization decisions
Security: Many exploits (like buffer overflows) work by corrupting stack frames to hijack the return address
Every time you write int x = add(3, 5); in C, the compiler generates code that:
Pushes 5 then 3 onto the stack
Executes CALL to push return address and jump
Sets up a stack frame with BP
Accesses parameters via [bp+8] and [bp+12]
Places result in AX
Tears down stack frame
Executes RET to pop return address and jump back
Cleans up parameters from stack
What seems like a simple function call involves a carefully orchestrated sequence of stack operations, all built on just two instructions: CALL and RET. This is the foundation of how all modern software executes — from your operating system kernel to web browsers to game engines. Understanding it gives you insight into what your code actually does at the machine level.<br>For more details on CALL and RET mechanics and the C calling convention, see <a data-tooltip-position="top" aria-label="https://blog.codefarm.me" rel="noopener nofollow" class="external-link is-unresolved" href="https://blog.codefarm.me" target="_self">[5] CALL/RET in x86 Assembly</a> and <a data-tooltip-position="top" aria-label="https://youtu.be/" rel="noopener nofollow" class="external-link is-unresolved" href="https://youtu.be/" target="_self">[9] Calling Convention Video</a>.
Program structure matters — TASM organizes code into segments (.data, .code, .stack) that map directly to memory segments addressed by DS, CS, and SS
Machine code is bytes — every instruction is an opcode byte followed by parameter bytes; the assembler translates mnemonics to these bytes
Little-endian storage — x86 stores multi-byte values with the least significant byte first in memory
IP drives execution — the Instruction Pointer advances by the size of each instruction; JMP simply adds an offset to IP
Arrays are contiguous memory — 1D arrays use db/dup; 2D arrays are stored row-major with offset = (row × columns) + column
The stack is fundamental — a LIFO structure implemented in hardware via SS:SP, used for temporary storage, function calls, and parameter passing
PUSH decrements SP, POP increments SP — the stack grows toward lower addresses on x86
Functions are CALL + RET — CALL pushes the return address and jumps; RET pops and jumps back. There are no "functions" at the CPU level
Calling conventions are agreements — the C/C++ convention uses the stack for parameters (pushed right-to-left), BP for the stack frame, and AX for return values
What you'll practice:
Working with the stack in Turbo Debugger: observing SS, SP, and stack memory changes
Implementing procedures with CALL and RET
Passing parameters via registers and via the stack
Declaring and accessing arrays in memory
Before lab:
Know how PUSH and POP affect SP and memory
Understand the difference between CALL/RET and JMP
Practice reading stack frame diagrams (where parameters, return address, and local variables are located) <br><a data-tooltip-position="top" aria-label="https://math.libretexts.org/Workbench/Numerical_Methods_with_Applications_(Kaw)/02%3A_Differentiation_of_Continuous_Functions/2.01%3A_Binary_Representation_of_Numbers" rel="noopener nofollow" class="external-link is-unresolved" href="https://math.libretexts.org/Workbench/Numerical_Methods_with_Applications_(Kaw)/02%3A_Differentiation_of_Continuous_Functions/2.01%3A_Binary_Representation_of_Numbers" target="_self">Fixed-Point Binary Representation of Numbers</a> — Binary number conversions and fixed-point representation
<br><a data-tooltip-position="top" aria-label="https://math.libretexts.org/Bookshelves/Combinatorics_and_Discrete_Mathematics/Applied_Discrete_Structures_(Doerr_and_Levasseur)/01%3A_Set_Theory/1.04%3A_Binary_Representation_of_Positive_Integers" rel="noopener nofollow" class="external-link is-unresolved" href="https://math.libretexts.org/Bookshelves/Combinatorics_and_Discrete_Mathematics/Applied_Discrete_Structures_(Doerr_and_Levasseur)/01%3A_Set_Theory/1.04%3A_Binary_Representation_of_Positive_Integers" target="_self">Hexadecimal (base 16)</a> — Hexadecimal number system
<br><a data-tooltip-position="top" aria-label="https://uk.wikipedia.org/wiki/%D0%9F%D1%80%D1%8F%D0%BC%D0%B8%D0%B9_%D0%BA%D0%BE%D0%B4" rel="noopener nofollow" class="external-link is-unresolved" href="https://uk.wikipedia.org/wiki/%D0%9F%D1%80%D1%8F%D0%BC%D0%B8%D0%B9_%D0%BA%D0%BE%D0%B4" target="_self">Прямий код (Wikipedia UK)</a> — Sign-magnitude representation
<br><a data-tooltip-position="top" aria-label="https://faculty.kfupm.edu.sa/COE/shazli/coe205/Help/asm_tutorial_09.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://faculty.kfupm.edu.sa/COE/shazli/coe205/Help/asm_tutorial_09.html" target="_self">8086 Assembler Tutorial: Stack</a> — Stack operations in 8086 assembly
<br><a data-tooltip-position="top" aria-label="https://blog.codefarm.me/2017/12/23/call-ret-x86-assembly/" rel="noopener nofollow" class="external-link is-unresolved" href="https://blog.codefarm.me/2017/12/23/call-ret-x86-assembly/" target="_self">Calling Convention in x86 Assembly</a> — CALL/RET and calling conventions
<br><a data-tooltip-position="top" aria-label="https://uk.wikipedia.org/wiki/%D0%9E%D0%B1%D0%B5%D1%80%D0%BD%D0%B5%D0%BD%D0%B8%D0%B9_%D0%BA%D0%BE%D0%B4" rel="noopener nofollow" class="external-link is-unresolved" href="https://uk.wikipedia.org/wiki/%D0%9E%D0%B1%D0%B5%D1%80%D0%BD%D0%B5%D0%BD%D0%B8%D0%B9_%D0%BA%D0%BE%D0%B4" target="_self">Обернений код (Wikipedia UK)</a> — Ones' complement representation
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Two%27s_complement" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Two%27s_complement" target="_self">Two's complement (Wikipedia)</a> — Two's complement representation
<br><a data-tooltip-position="top" aria-label="https://adacomputerscience.org/concepts/proglang_bitwise_manipulation" rel="noopener nofollow" class="external-link is-unresolved" href="https://adacomputerscience.org/concepts/proglang_bitwise_manipulation" target="_self">Bitwise manipulation — Ada Computer Science</a> — Practical bitwise operations
<br><a data-tooltip-position="top" aria-label="https://youtu.be/QZwneRb-zqA?t=734" rel="noopener nofollow" class="external-link is-unresolved" href="https://youtu.be/QZwneRb-zqA?t=734" target="_self">Empirical explanation of two's complement</a> — Visual demonstration (YouTube)
]]></description><link>lectures/eng/l05-eng.-program-organization-stack-and-procedures.html</link><guid isPermaLink="false">Lectures/ENG/L05-ENG. Program Organization - Stack and Procedures.md</guid><pubDate>Sun, 08 Feb 2026 18:03:33 GMT</pubDate><enclosure url="assets/l05-real-mode-memory-addressing.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l05-real-mode-memory-addressing.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L05-CPU-View-Annotated]]></title><description><![CDATA[<img src="assets/l05-cpu-view-annotated.png" target="_self">]]></description><link>assets/l05-cpu-view-annotated.html</link><guid isPermaLink="false">assets/L05-CPU-View-Annotated.png</guid><pubDate>Sun, 08 Feb 2026 17:43:18 GMT</pubDate><enclosure url="assets/l05-cpu-view-annotated.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l05-cpu-view-annotated.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L05-2D-Array-Memory-Layout]]></title><description><![CDATA[<img src="assets/l05-2d-array-memory-layout.png" target="_self">]]></description><link>assets/l05-2d-array-memory-layout.html</link><guid isPermaLink="false">assets/L05-2D-Array-Memory-Layout.png</guid><pubDate>Sun, 08 Feb 2026 17:43:18 GMT</pubDate><enclosure url="assets/l05-2d-array-memory-layout.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l05-2d-array-memory-layout.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L05-Stack-Frame-Diagram]]></title><description><![CDATA[<img src="assets/l05-stack-frame-diagram.png" target="_self">]]></description><link>assets/l05-stack-frame-diagram.html</link><guid isPermaLink="false">assets/L05-Stack-Frame-Diagram.png</guid><pubDate>Sun, 08 Feb 2026 17:43:18 GMT</pubDate><enclosure url="assets/l05-stack-frame-diagram.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l05-stack-frame-diagram.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L05-Push-Pop-Equivalence]]></title><description><![CDATA[<img src="assets/l05-push-pop-equivalence.png" target="_self">]]></description><link>assets/l05-push-pop-equivalence.html</link><guid isPermaLink="false">assets/L05-Push-Pop-Equivalence.png</guid><pubDate>Sun, 08 Feb 2026 17:43:18 GMT</pubDate><enclosure url="assets/l05-push-pop-equivalence.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l05-push-pop-equivalence.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L05-Stack-Operations-Visual]]></title><description><![CDATA[<img src="assets/l05-stack-operations-visual.png" target="_self">]]></description><link>assets/l05-stack-operations-visual.html</link><guid isPermaLink="false">assets/L05-Stack-Operations-Visual.png</guid><pubDate>Sun, 08 Feb 2026 17:43:18 GMT</pubDate><enclosure url="assets/l05-stack-operations-visual.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l05-stack-operations-visual.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L05-Stack-Push-Pop-Steps]]></title><description><![CDATA[<img src="assets/l05-stack-push-pop-steps.png" target="_self">]]></description><link>assets/l05-stack-push-pop-steps.html</link><guid isPermaLink="false">assets/L05-Stack-Push-Pop-Steps.png</guid><pubDate>Sun, 08 Feb 2026 17:43:18 GMT</pubDate><enclosure url="assets/l05-stack-push-pop-steps.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l05-stack-push-pop-steps.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L05-Decimal-To-Binary-Conversion]]></title><description><![CDATA[<img src="assets/l05-decimal-to-binary-conversion.png" target="_self">]]></description><link>assets/l05-decimal-to-binary-conversion.html</link><guid isPermaLink="false">assets/L05-Decimal-To-Binary-Conversion.png</guid><pubDate>Sun, 08 Feb 2026 17:43:18 GMT</pubDate><enclosure url="assets/l05-decimal-to-binary-conversion.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l05-decimal-to-binary-conversion.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L05-Real-Mode-Memory-Addressing]]></title><description><![CDATA[<img src="assets/l05-real-mode-memory-addressing.png" target="_self">]]></description><link>assets/l05-real-mode-memory-addressing.html</link><guid isPermaLink="false">assets/L05-Real-Mode-Memory-Addressing.png</guid><pubDate>Sun, 08 Feb 2026 17:43:18 GMT</pubDate><enclosure url="assets/l05-real-mode-memory-addressing.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l05-real-mode-memory-addressing.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L05-Endianness-Memory-Layout]]></title><description><![CDATA[<img src="assets/l05-endianness-memory-layout.png" target="_self">]]></description><link>assets/l05-endianness-memory-layout.html</link><guid isPermaLink="false">assets/L05-Endianness-Memory-Layout.png</guid><pubDate>Sun, 08 Feb 2026 17:43:18 GMT</pubDate><enclosure url="assets/l05-endianness-memory-layout.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l05-endianness-memory-layout.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L05-Big-Endian-vs-Little-Endian]]></title><description><![CDATA[<img src="assets/l05-big-endian-vs-little-endian.png" target="_self">]]></description><link>assets/l05-big-endian-vs-little-endian.html</link><guid isPermaLink="false">assets/L05-Big-Endian-vs-Little-Endian.png</guid><pubDate>Sun, 08 Feb 2026 17:43:18 GMT</pubDate><enclosure url="assets/l05-big-endian-vs-little-endian.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l05-big-endian-vs-little-endian.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Консультація - 2026-01-24]]></title><description><![CDATA[Дата: 24 січня 2026 року
Тривалість: ~1 година
Формат: онлайн-консультація (office hours)
Важливо здавати роботи вчасно, навіть з урахуванням поточної ситуації (відключення світла тощо).
Якщо є об'єктивні причини для затримки — потрібно попереджати заздалегідь, а не за 14 хвилин до дедлайну.
Рекомендація: заливати роботи, коли є світло, не чекаючи останнього дня.
Поточне завдання можна виконати приблизно за 4,5 хвилини «з нуля» — завдання відносно прості на початку курсу.
Якщо пропустили дедлайн — все одно треба здавати пропущені роботи. Курс проводиться у змішаному форматі (онлайн + офлайн).
Плюси дистанційного формату: під час консультацій можна ділитися екраном, і всі бачать обговорення. Силабус доступний на курсі та періодично оновлюється.
Важливо підтримувати зв'язок з одногрупниками.
Старостам рекомендовано «смикати» тих, хто не відгукується на повідомлення.
Якщо хтось зник — варто вийти на зв'язок, це важливо не тільки для засвоєння матеріалу, а й для підтримки одне одного.
У журналі курсу для кожного студента відображаються такі оцінки:Приклади:
2 бали з 2 можливих → успішність 100%
1,5 бали з 2 можливих → успішність 75%
2,2 бали з 2 можливих → успішність все одно 100% (більше 100 балів за курс виставити буде неможливо)
Підсумкова оцінка = сума балів за лабораторні + бонуси + контрольні + проєкт + іспит.
Перша контрольна: 6-й тиждень
Друга контрольна: останній тиждень Робота з ручкою та папером (без комп'ютера, щоб не відволікатися).
Завдання друкуються індивідуально — багато варіантів (60 або більше), згенерованих за допомогою ШІ та перевірених викладачами.
Приклади завдань (детальні будуть надані завчасно): Накидати невеличку схему (AND, OR, NOT тощо)
Заповнити пробіли (fill the gaps)
Встановити відповідність Планується не на всю пару, а приблизно на половину.
Решта пари — відповідна тема у спрощеному форматі.
Примітка: Якщо онлайн-формат продовжиться, можливо, буде інший формат проведення контрольних.Як інтерпретувати виходи схеми віднімання (subtractor) — коли результат від'ємний, а коли додатній?
Схема не знає, чи працює з від'ємними чи додатніми числами — це питання інтерпретації.
Доповняльний код (two's complement): останній біт інтерпретується як від'ємна степінь двійки. Якщо 4 біти: біти означають 1, 2, 4, -8 (замість 8)
Приклад: 1110 = -8 + 4 + 2 = -2 Перевага доповняльного коду: одна й та сама схема працює і для додатніх, і для від'ємних чисел — змінюється лише інтерпретація.
Всі сучасні архітектури використовують доповняльний код для цілих чисел саме через цю властивість.
int x; // трактує останній біт як від'ємний (-2^31)
unsigned int y; // трактує останній біт як додатній (+2^31)
Код виконується однаково, різниця лише у виведенні/інтерпретації результату.Називати входи/виходи за їхньою вагою: A1, A2, A4, A8 (або A1, A2, A4, A-8 для доповняльного коду) — так легше рахувати.
Git працює виключно з файлами, не з порожніми папками.
Якщо потрібно створити структуру папок без файлів — додайте файл-заглушку (.gitkeep).
Git не створює папки — він лише робить «знімки» (snapshots) існуючої структури всіх файлів в репозиторії.
Спочатку створіть папки та файли засобами ОС, потім додайте їх у Git.
Двокроковий процес (add → commit) дозволяє групувати зміни логічно. Git не створює папки — він лише робить «знімки» (snapshots) існуючої структури всіх файлів в репозиторії.
Спочатку створіть папки та файли засобами ОС, потім додайте їх у Git.
Двокроковий процес (add → commit) дозволяє групувати зміни логічно.
Буде увага на правильне іменування комітів — важливо для роботи в команді.Навіщо в схемі Full Adder на виході carry використовується OR?При додаванні в стовпчик у двійковій системі:
Входи: A, B, Carry-in (з попереднього розряду)
Виходи: Sum, Carry-out
Carry-out виникає, коли:
A + B дає перенос, АБО
(A + B) + Carry-in дає перенос, АБО
обидва випадки одночасно
Це логічна операція OR: якщо хоча б один з двох половинних суматорів дав перенос — загальний перенос є.Carry-out = Carry₁ OR Carry₂
Схема Full Adder повністю реалізує алгоритм додавання в стовпчик для одного розряду.Чому в процесорі використовується пам'ять на транзисторах (SRAM), а в оперативній пам'яті — на конденсаторах (DRAM)?
Сучасні процесори мають транзистори розміром у нанометри — конденсатор туди просто не влізе.
Пам'яті потрібно багато (гігабайти, терабайти) — вона має десь фізично розміститися.
Ієрархія кешу (L0 → L1 → L2 → L3 → RAM) — компроміс між швидкістю та об'ємом.
В сучасних архітектурах (наприклад, Apple M-серія) все більше пам'яті інтегрується безпосередньо на чіп процесора.Фізична кнопка при натисканні дає нестабільний сигнал (bouncing) — напруга «стрибає» між 0 і 1. Завдання: зробити схему, яка видає стабільний сигнал (чітко 0 або 1), використовуючи лише транзистори та резистори.
Використання тригера (flip-flop/latch) для «запам'ятовування» стану.
Побудова тригера з логічних елементів.
Заміна логічних елементів на транзистори та резистори. Втрата напруги на схемі (10В на вході → 4В на виході).
Нестабільність при заміні всіх елементів на транзистори. Використовувати MOSFET-транзистори (різні типи).
Додати операційний підсилювач (Operational Amplifier) для компенсації втрат напруги.
Слідкувати за узгодженням вхідної та вихідної напруги.
Примітка: Це завдання виходить за межі основного курсу архітектур і відноситься до курсу фізики обчислень.
Архітектура x86
Регістри та пам'ять
Адресація пам'яті (рядки, колонки, декодери)
Відмінності SRAM та DRAM на концептуальному рівні
Ієрархія кешування: L0, L1, L2, L3 Асемблер та низькорівневе програмування
Архітектури фон Неймана та Гарвардська
System on Chip (SoC), GPU Встановлення віртуальної машини для роботи з асемблером
Робота з адресацією пам'яті Силабус курсу — на сторінці курсу
Симулятор Falstad — для моделювання електронних схем
Документація Falstad — опис усіх функцій симулятора (знайти через Google) ✅ Здавайте роботи до дедлайну
✅ Попереджайте заздалегідь про проблеми
✅ Підтримуйте зв'язок з одногрупниками
✅ Питання можна ставити в загальному чаті або на консультаціях
✅ Використовуйте thumbs down для зворотного зв'язку щодо оцінок
Конспект підготовлено на основі запису консультації. Для студентів, які були присутні — як нагадування, для відсутніх — як повноцінний огляд обговорених тем.]]></description><link>office-hours/консультація-2026-01-24.html</link><guid isPermaLink="false">Office-hours/Консультація - 2026-01-24.md</guid><pubDate>Sat, 07 Feb 2026 19:21:10 GMT</pubDate></item><item><title><![CDATA[Консультація - 2026-01-17]]></title><description><![CDATA[Дата: 17 січня 2026 р.
Формат: Офісні години / Консультація
Для кого: Студенти 1-го курсу бакалавратуПроцес здачі домашнього завдання складається з двох комітів:
Перший коміт — файл README.md з обов'язковою інформацією: Ім'я та прізвище
E-mail
Група Другий коміт — файл video.txt з посиланням на відеозвіт
Шляхи до файлів згідно з вимогами, описаними у файлі лаб. роботи.
Платформа: Loom (рекомендовано). YouTube — лише якщо є нездоланні технічні проблеми.
Тривалість: не більше 5 хвилин.
Чому Loom? Викладачі можуть залишати фідбек безпосередньо у відео, прив'язуючи коментарі до конкретних моментів.
Loom дає безкоштовний період з можливістю запису відео до 5 хвилин — це збігається з вимогами курсу. Якщо виникають технічні проблеми з Loom, можна використати YouTube, але бажано спробувати вирішити проблему з викладачем.
Пишіть грамотні назви комітів (commit subjects) — це загальноприйнята практика в індустрії.
Чіткі назви комітів допомагають і вам через півроку, і колегам у команді, і викладачам при перевірці.
Можна отримати бонуси за:
Гарне оформлення README з використанням Markdown
Креативність (про себе, про навчання, про очікування, ASCII-арт тощо)
Використання Falstad (факультативно)
Важливо: Falstad — корисний інструмент для візуалізації, але переконайтесь, що володієте основним інструментом Digital Logic Sim, оскільки далі буде складніше, і Falstad не завжди зможе наглядно показати складніші схеми (наприклад, ALU).
Оцінки виставляються в Distedu.
Викладачі можуть залишати коментарі в журналі та в Loom.
Файл з оцінками оновлюється після перевірки кожної практичної та домашньої роботи.
Чому важливі дедлайни? Кожна наступна робота базується на попередній. Якщо щось не зрозуміло — обов'язково питайте, бо далі буде важче наздогнати.Підхід курсу — будувати власноруч і розуміти, яка архітектура для чого підходить:
Починаємо з мінімальних суматорів
Переходимо до ALU (Arithmetic Logic Unit)
З кожним заняттям «піднімаємось» на порядок вище (у 10–100 разів більше транзисторів)
Транзистор — це контролюючий елемент з трьома «ногами»:
Вхід — звідки надходить сигнал (або вода в аналогії з водяним комп'ютером)
Вихід — куди йде сигнал
Контроль — визначає, чи пропускати сигнал
Аналогія з водяним комп'ютером:
«Тупий» комп'ютер — просто бак, злив і труба
«Розумний» комп'ютер — додаємо клапан, який контролює потік
Вакуумна лампа також має три «ноги» і працює за тим же принципом, що й транзистор.Послідовність побудови:
Транзистори → базовий елемент
Логічні гейти (AND, NOT, NAND, XOR) → складаються з 1–2 транзисторів
Базис AND-NOT (або NAND) → будь-яку логічну функцію можна виразити через цей базис
Арифметичні операції: XOR ідентичний додаванню в двійковій системі (1+1=0 з переносом)
Додавання → віднімання → множення (багатократне додавання) → ділення (багатократне віднімання) Висновок: Транзистор — це основа будь-якого комп'ютера та будь-якої обчислювальної системи.Проблема «курки і яйця»: щоб написати код, потрібен інтерпретатор, який теж хтось написав…Відповідь: Найнижчий рівень — це теж код, тільки фізичний. Топологічна структура з'єднаних транзисторів — це і є «перша мова». Коли ми будуємо структуру і подаємо на неї енергію, сама структура стає кодом.Процесор — це певним чином поєднані між собою транзистори. Це теж обчислювальна система, яка є кодом як таким, тільки у фізичній формі.SR-Latch або D-Flip-Flop — це один біт пам'яті, реалізований через транзистори та зворотні зв'язки.Для створення комп'ютера з пам'яттю, входами та виходами достатньо:
Транзисторів
З'єднань між транзисторами
Клок — осцилятор, який з певною частотою посилає сигнали для синхронізації роботи схеми.
Теоретично можна обійтися без клока, підбираючи довжини провідників.
Практично це дуже складно при великих кількостях транзисторів.
Клок значно спрощує реалізацію синхронізації.
Carry (біт переносу) потрібен для:
Розширення: з двох 4-бітних суматорів можна зробити 8-бітний, з двох 8-бітних — 16-бітний тощо.
Визначення Overflow (переповнення): carry показує, що результат не вміщується в задану кількість бітів.
Прапорець overflow в ALU впливає на подальші обчислення — наприклад, інструкція «jump if overflow» виконується залежно від цього прапорця.Для концептуального комп'ютера достатньо транзисторів та з'єднань. Для реального пристрою потрібні:
Резистори та конденсатори (для правильного розподілу струму)
Діоди (для дебагу)
Корпус, кнопки тощо
Магнітні диски, оптичні диски, перфокарти — це альтернативні способи зберігання бітів, але не частина обчислювального ядра.На наступних заняттях:
Побудова ALU
Створення 4-інструкційного мікропроцесора власноруч
Розширення до ~40 інструкцій
Початок програмування на асемблері реальної машини
Наприкінці курсу навіть згадаємо про LLM із чітким розумінням, як вони працюють на транзисторах
Масштаб: Від кількох десятків транзисторів (ваша перша лабораторна) до кількох мільярдів (сучасний процесор).
Якщо не можете бути на парі — обов'язково дивіться записи та питайте викладачів.
Перший корпус завжди має світло, тепло і можна підзарядитися.
Не соромтеся писати викладачам — краще спитати, ніж не розуміти далі.
Ця база допоможе на будь-якому рівні: від Python до розробки власної мови програмування.
Цей документ є підсумком офісних годин і призначений як для учасників консультації, так і для тих, хто не мав змоги приєднатися.]]></description><link>office-hours/консультація-2026-01-17.html</link><guid isPermaLink="false">Office-hours/Консультація - 2026-01-17.md</guid><pubDate>Sat, 07 Feb 2026 19:21:03 GMT</pubDate></item><item><title><![CDATA[Консультація - 2026-01-31]]></title><description><![CDATA[Дата: 31 січня 2026 р.
Курс: 1-й рік бакалаврату
Тривалість: ~1 година
Цього року лабораторну роботу було спрощено порівняно з минулим роком, щоб полегшити входження в матеріал.
Студентам необхідно переконатися, що DOSBox та Turbo Debugger коректно налаштовані.
Третя лекція була опублікована на сайті курсу (Obsidian Publish) — раніше був відсутній publish. За активність на парі можна отримати +10% бонусу.
За виконання супер-задачі (додаткового завдання підвищеної складності) — ще +20%.
Супер-задачі видаються студентам, які завершили основне завдання раніше за інших. Один 16-бітний регістр може адресувати лише 64 КБ пам'яті (2¹⁶ = 65 536 байт).
Для архітектури 8086 потрібно було адресувати 1 МБ (мегабайт). Використовуються два 16-бітних регістри: сегментний та зсув (offset).
Якби просто об'єднати два регістри, отримали б 32 біти = 4 ГБ, що було надлишковим на той час.
Замість цього застосували 20-бітну адресацію: Сегментний регістр зсувається на 4 біти вліво.
Додається offset.
Результат: 2²⁰ = 1 048 576 байт = 1 МБ. Сегменти починаються кожні 16 байт (параграф).
Це дозволило мати велику кількість сегментів при обмеженому обсязі пам'яті.
Сегменти можуть перекриватися, що забезпечує гнучкість розподілу пам'яті.Фізична адреса = (Сегмент × 16) + Offset INT (Interrupt) — переривання, механізм виклику функцій операційної системи.
21h — номер переривання, зарезервований для DOS (Disk Operating System).
Номер функції передається в регістрі AH.
MOV AX, 4C00h ; AH = 4Ch (terminate), AL = 00h (error code)
INT 21h ; виклик DOS
Пояснення:
4Ch — код функції «завершити програму».
AL містить error code (код помилки), який повертається в середовище.
0 означає успішне завершення, інші значення (1–255) — різні помилки.
MOV AH, 09h ; функція виведення рядка
MOV DX, OFFSET MSG ; адреса рядка
INT 21h ; виклик DOS
Особливості:
Рядок має закінчуватися символом $ (долар).
0Dh, 0Ah — коди для переведення рядка (Carriage Return + Line Feed).
Якщо не завершити програму коректно:
Процесор продовжить виконувати «сміття» в пам'яті після останньої інструкції.
Програма зависне або почне виводити випадкові символи. Кожна програма повертає error code при завершенні.
Його можна перевірити у BAT-файлі через змінну ERRORLEVEL.
Приклад: перевірка успішності виконання програми в скрипті. SHL (Shift Left) — еквівалент множення на 2ⁿ.
SHR (Shift Right) — еквівалент ділення на 2ⁿ.
Приклади:
SHL 1 = множення на 2
SHL 5 = множення на 32 У C — це бітові операції зсуву.
У C++ для cout та cin — це перевантажені оператори (operator overloading).
Перевантаження операторів — вид поліморфізму.
Приклад:cout &lt;&lt; "Hello"; // &lt;&lt; тут не бітовий зсув, а виведення
cin &gt;&gt; x; // &gt;&gt; тут не бітовий зсув, а введення
Детальніше про перевантаження операторів — на курсі C++ (2-й рік навчання).В асемблері немає типової декларації змінних як у мовах високого рівня. Натомість:
Визначаються іменовані області пам'яті в сегменті даних.
Директива DB (Define Byte) резервує байти.
DATA SEGMENT MSG DB 'Hello, Assembly!', 0Dh, 0Ah, '$'
DATA ENDS
Пояснення:
MSG — мітка (ім'я області пам'яті).
DB — визначає послідовність байтів.
Кожен символ зберігається як його ASCII-код.
0Dh, 0Ah — символи нового рядка.
$ — термінатор рядка для функції 09h.
MOV DX, OFFSET MSG OFFSET повертає зміщення мітки відносно початку сегмента.
Якщо MSG — перша змінна, її offset = 0. Кожен символ має числовий код у ASCII-таблиці.
При виведенні DOS інтерпретує байти як символи.
Проблема: Якщо в регістрі AX лежить число FF33h, функція 09h не виведе «FF33». Вона виведе символи з кодами FFh та 33h (нечитабельні).Рішення: Потрібен алгоритм конвертації числа у послідовність ASCII-символів:
FF33h → символи 'F', 'F', '3', '3'.
Цей алгоритм буде розглянуто на наступних лабораторних.
На процесорах без апаратного множення або для оптимізації використовують бітові зсуви та додавання.50 = 32 + 16 + 2 = 2⁵ + 2⁴ + 2¹
; Припустимо, число в AX
MOV BX, AX ; копія
SHL AX, 5 ; × 32
SHL BX, 4 ; × 16
ADD AX, BX ; 32 + 16 = 48
MOV BX, ... ; оригінальне значення
SHL BX, 1 ; × 2
ADD AX, BX ; 48 + 2 = 50
На консультації було продемонстровано розв'язок супер-задачі: обчислення складного арифметичного виразу з множеннями на 20, 30, 50 лише через зсуви та додавання.Результат: 23 змінні, верифікація через Python-скрипт.Інструкція NEG (Negate) змінює знак числа:NEG AX ; AX = -AX (в доповнювальному коді) Інвертувати всі біти (0 → 1, 1 → 0).
Додати 1 до результату. 5 = 0000 0101
Інверсія: 1111 1010 +1: 1111 1011 = -5 Одна інструкція виконує складну операцію.
Швидко, бо реалізовано апаратно.
Симптом: Миша «зависає» під час покрокового виконання.Причина:
DOS не підтримує багатозадачність.
Якщо рухати мишею під час виконання інструкції (F7/F8), драйвер миші може втратити синхронізацію.
Рішення:
Натиснути Alt-X для виходу з дебагера.
Перезапустити Turbo Debugger.
Не рухати мишу під час натискання F7/F8.
Для коректного відображення вихідного коду в дебагері:TASM /ZI program.asm
TLINK /V program.obj
TD program.exe
Пояснення:
/ZI — включає символьну інформацію (назви змінних, коментарі).
/V — verbose mode для лінкера.
Без цих параметрів дебагер покаже лише машинний код.
START: MOV AX, @DATA ; адреса сегмента DATA MOV DS, AX ; завантажити в DS @DATA — псевдозмінна, що містить адресу сегмента даних.
Не можна напряму записати в DS (обмеження архітектури).
Спочатку значення завантажується в AX, потім переноситься в DS. Функції DOS (наприклад, 09h) очікують, що DS:DX вказує на дані.
Без правильної ініціалізації DS програма не знайде змінні. Пункт 13.3 у силабусі — повна документація TASM версії 5.
Містить опис усіх параметрів командного рядка.
Детальний опис директив, інструкцій, повідомлень про помилки. Turbo Assembler Version 5 User Guide — основний довідник.
x86 Holybook — детальний опис архітектури x86.
Документація доступна через посилання в силабусі курсу. JMP — безумовний перехід (аналог goto).
Умовні переходи — JE, JNE, JG, JL тощо.
Реалізація конструкцій: if, for, while, switch. Текстовий режим — прямий запис в буфер екрана.
Графічний режим — малювання пікселями.
Приклад проєкту: симуляція «Matrix screen» з падаючими символами. Планується на 6–7 тижні.
Перевірка розуміння теоретичного матеріалу.
Теми: сегментація пам'яті, адресація, базові інструкції. Сегментація — історичний спосіб розширення адресного простору з 64 КБ до 1 МБ.
INT 21h — основний інтерфейс взаємодії з DOS; номер функції в AH.
Змінні в асемблері — це іменовані області пам'яті, не типізовані структури.
ASCII — кожен символ = числовий код; виведення чисел потребує конвертації.
Множення через зсуви — ефективний спосіб без апаратного множника.
Конспект підготовлено на основі запису консультації. Рекомендується ознайомитися з матеріалами лекцій та документацією TASM для поглибленого вивчення.]]></description><link>office-hours/консультація-2026-01-31.html</link><guid isPermaLink="false">Office-hours/Консультація - 2026-01-31.md</guid><pubDate>Sat, 07 Feb 2026 19:18:57 GMT</pubDate></item><item><title><![CDATA[Консультація - 2026-02-07]]></title><description><![CDATA[Курси: «Архітектури обчислювальних систем» та «Алгоритми і структури даних»
Дата: 7 лютого 2026 р.
Формат: спільна онлайн-сесія для студентів 1-го курсу бакалаврату
Тривалість: ≈ 1 годинаКурс поступово ускладнює роботу з асемблером (середовище TASM / Turbo Debugger у DOSBox). На момент консультації студенти працюють зі звичайними командами та починають вивчати команди переходів. Найближчий план:
Наступна практика — робота з пам'яттю: запис у пам'ять, читання з пам'яті, організація структур даних (масиви).
Через одну практику — стек і процедури (підпрограми, виклики call/ret).
Далі — написання повноцінних задач, початок роботи над проєктом, а згодом — змагання з асемблера (аналогічно до попередніх двох років), які дають можливість звільнитися від частини практичних.
Було поставлено питання: як в асемблері підключити код з іншого файлу (наприклад, щоб використати окрему функцію виведення поруч із Bubble Sort)?Ключові тези:
Директива include працює як пряма вставка вмісту файлу в те місце, де вона написана — аналогічно до #include у С.
Не слід через include підключати окрему повноцінну програму (з власним сегментом даних і коду). Правильний підхід — підключати процедури (функції).
Процедури та механізм передачі даних через стек будуть детально розглянуті через одну-дві лекції (пам'ять → стек → процедури). Після цього стане зрозуміло, де тримати змінні та як організовувати модульний код.
Якщо комбінація Alt+X не працює для виходу з дебагера, можливі причини та рішення:
Перевірити розкладку клавіатури — має бути англійська (українська блокує комбінацію).
Альтернатива: Alt+F → меню → вибрати «Exit» внизу.
Ctrl+F10 — звільнення миші в DOSBox.
Також можна просто закрити вікно DOSBox.
Обговорювалися регістри, доступні в 16-бітному режимі (Turbo Debugger):
Основні (загального призначення): AX, BX, CX, DX — підтримують найбільшу кількість інструкцій, розділяються на піврегістри (AL/AH, BL/BH тощо).
Індексні: SI (Source Index), DI (Destination Index) — можна використовувати для власних потреб, поки не задіяні складні стрінгові інструкції.
Сегментні: CS, DS, ES, SS — для адресації сегментів коду, даних, додаткового, стеку.
Спеціальні: IP (Instruction Pointer) — не можна змінити напряму через mov; на нього впливають jmp, call, ret, int.
У 32-бітних архітектурах регістри розширюються до EAX, ESI тощо (префікс E — Extended), у 64-бітних — до RAX, RSI (префікс R), плюс додаються R8–R15.
Все, що видно в Turbo Debugger — це всі доступні регістри у 16-бітному режимі. Нічого прихованого немає.
Грань між «основними» і «спеціальними» регістрами — умовна. Різниця лише в тому, на скільки інструкцій вони «зав'язані» і чи розділяються на піврегістри.
Ключове концептуальне пояснення для розуміння низькорівневої архітектури:
У сегменті даних лежать просто байти. Директива db (define bytes) записує конкретні байти в пам'ять. Наприклад, db 'Hello' записує ASCII-коди символів H, e, l, l, o.
Ім'я змінної (label) — це лише позначка зміщення (offset) від початку сегменту. message db 'Hello' означає: «починаючи зі зміщення, яке ми назвали message, лежать ці байти». Це зручність для програміста, не більше.
EQ — це макрос (аналог #define у С). N EQ 8 означає: перед компіляцією всі входження N замінити на 8 у вихідному файлі. Це не змінна у пам'яті, а текстова підстановка на етапі препроцесора.
Макроси замінюються на рівні вихідного файлу (перший прохід асемблера), після чого код транслюється у машинні байти.
Різниця з іменованими змінними: змінна — це поіменована область пам'яті; макрос EQ — це препроцесорна підстановка, яка не займає пам'яті. Сегмент даних: байти записуються дослівно, як визначено (ASCII-коди, числа тощо).
Сегмент коду: мнемоніки (mov, jmp тощо) транслюються у відповідні байти машинних інструкцій (наприклад, mov ax, ... → B8 ...).
При завантаженні програми всі ці байти просто переміщуються з файлу в оперативну пам'ять.
Процесор бачить тільки байти та регістри. Якби CS:IP вказував на сегмент даних, процесор спробував би виконати «Hello World» як інструкції — з очевидно безглуздим результатом.
Це безпосередньо пов'язує курс із тим, що студенти робили на перших лабораторних (побудова логічних схем, дешифратори, ALU): процесор — це ті ж самі «провідки», тільки дуже швидкі.
Один зі студентів продемонстрував працюючу реалізацію Bubble Sort у TASM із поясненнями:
Структура: CX — лічильник зовнішнього циклу, DX — лічильник внутрішнього циклу. SI та DI — покажчики на поточний та наступний елементи масиву. AX і BX — поточне і наступне числа для порівняння.
Адресація масиву: array + 0 → перший елемент, array + 2 → другий (16-бітні числа, крок 2 байти), array + 14 → останній.
Порівняння: якщо AX &gt; BX (поточний більший за наступний), перехід на мітку first_greater, де відбувається swap.
Демонстрація в Turbo Debugger: у вікні DUMP видно, як поступово байти масиву впорядковуються. Працює і з від'ємними числами (JG коректно обробляє знакові порівняння).
Корисний трюк із Turbo Debugger: використання breakpoints (F2 — toggle, F9 — Run до breakpoint) замість покрокового виконання F8. Це суттєво зручніше при налагодженні програм із циклами.Обговорено, чому вивід числа з регістра на екран — нетривіальна задача:
В регістрі AX лежить число, наприклад, 1234h (= 4660 у десятковій).
Функція DOS (INT 21h, AH=9) вміє виводити рядок символів, але не число.
Потрібно написати процедуру, яка: послідовно ділить число на 10, зберігає остачі як ASCII-коди цифр у пам'яті, ставить $ в кінці, після чого викликає INT 21h/09h.
Коли така процедура написана один раз, її можна багаторазово викликати для будь-якого значення в AX.
Цього року таку задачу студенти не зобов'язані робити одразу — курс побудований так, щоб знання нарощувалися поступово.
На лабораторній розглядалася робота з файлами. Уточнення:
За замовчуванням (коли вказано лише ім'я файлу без шляху) Java читає/пише з поточної директорії, яка зазвичай збігається з розташуванням .java-файлу.
Щоб працювати з файлом з іншого місця на диску, потрібно вказати повний (абсолютний) шлях.
Концептуальні відмінності:
Масив — виділена область пам'яті з пронумерованими елементами (є індекс: 0-й, 1-й, 2-й…). Дублікати дозволені.
Множина (Set) — набір елементів без гарантованого порядку. Елемент або є (true), або його немає (false). Дублікати неможливі.
Реалізація Set всередині може бути різною: масив, бітовий рядок тощо.
Також коротко згадано:
List у Python — без ключа (аналог масиву).
Dictionary у Python — асоціативний масив (пари ключ-значення).
Розібрано тестове питання: «Що означає accepting state?»
Accepting state означає, що вхідні дані визнані коректними (прийнятими) — наприклад, email відповідає шаблону регулярного виразу.
Це не те саме, що «машина успішно завершила роботу». Автомат успішно завершує роботу в будь-якому випадку — він або приймає (accept), або відхиляє (reject) вхідний рядок. Обидва результати означають, що машина виконала своє завдання.
Приклад: автомат для перевірки email отримує на вхід адресу з трьома символами @. Він завершує роботу reject-ом — але при цьому він успішно виконав свою задачу (визначив, що дані некоректні).
Зазначено важливий концептуальний зв'язок двох курсів:
Алгоритми дають теоретичну основу того, що можна обчислити (формальні визначення, алгоритми Маркова, автомати).
Архітектури показують, як саме це фізично реалізується: від комбінаційної логіки (миттєві обчислення) до секвенційної логіки (sequential — з часовим аспектом, race conditions, потреба у синхронізації).
Комбінаційна логіка → секвенційна логіка → пам'ять → процесор — це природний шлях від фізики до обчислювальної машини.
При демонстрації алгоритму знаходження найбільшого спільного дільника (НСД / GCD) виникло обговорення:
Студент записав алгоритм як рекурсію (зверху), а реалізував у асемблері через jmp (цикл).
Уточнення: рекурсія — це коли функція викликає сама себе (call). Якщо замість цього використовується jmp (перехід), це цикл. У даному випадку рекурсія була «розпластана» (tail-call optimization у ручному варіанті), що є коректним і навіть ефективнішим підходом.
Важливо: після редагування вихідного файлу потрібно перекомпілювати (TASM → TLINK) перед повторним запуском у дебагері, інакше дебагер показуватиме старий код, а виконуватиме новий.
Файли практичних розшарені включно із <a class="internal-link" data-href="Lab04-TD" href="labs/lab04-td.html" target="_self" rel="noopener nofollow">tasks from Tyler Durden</a> (доступні студентам) — можна переглядати.
Контрольна з архітектур (для КН) вже відбулася; контрольна з алгоритмів ще попереду.
Результати тестів уже відкриті — можна переглядати свої відповіді.
На наступній лекції з архітектур — пам'ять; на практиці — робота з масивами в асемблері.
]]></description><link>office-hours/консультація-2026-02-07.html</link><guid isPermaLink="false">Office-hours/Консультація - 2026-02-07.md</guid><pubDate>Sat, 07 Feb 2026 19:13:46 GMT</pubDate></item><item><title><![CDATA[Lab04-TD]]></title><description><![CDATA[
Для кого: студенти, які прийшли з виконаними завданнями і нудьгують.
Тривалість: розраховано на 2 години.
Правило: різним студентам — різні варіанти.
Складність: ⭐⭐ (Medium)Напишіть програму GCD.ASM, яка обчислює найбільший спільний дільник (НСД) двох чисел за алгоритмом Евкліда.while B ≠ 0: temp = B B = A mod B A = temp
return A Вхід: AX = перше число, BX = друге число
Результат: AX = GCD(A, B)
Не використовуйте .DATA — тільки регістри. A mod B — це остача від ділення (після DIV вона в DX)
Не забудьте очистити DX перед DIV
Що робити, якщо B = 0 на початку?
Складність: ⭐⭐⭐ (Hard)Напишіть програму TODEC.ASM, яка конвертує 16-бітне беззнакове число в рядок десяткових цифр ASCII.
Вхід: AX = число для конвертації
Результат: рядок ASCII-цифр у пам'яті (buffer), завершений '$'
Використовуйте .DATA для буфера Ділимо число на 10, остача — молодша цифра
Повторюємо, поки число ≠ 0
Цифри виходять у зворотному порядку — треба розвернути (або писати з кінця буфера)
Додаємо '0' (30h) до кожної цифри для ASCII
Після конвертації виведіть рядок на екран через INT 21h / AH=09h.
Буфер: мінімум 6 байт (5 цифр + '$')
Найпростіше: писати цифри з кінця буфера
Або: записати у зворотному порядку, потім розвернути
Складність: ⭐⭐⭐ (Hard)Напишіть програму BSORT.ASM, яка сортує масив знакових 16-бітних цілих чисел за зростанням методом бульбашки.
Вхід: масив у .DATA, кількість елементів — константа
Результат: той самий масив, відсортований
Використовуйте знакові порівняння (JG/JL)
for i = 0 to n-2: for j = 0 to n-2-i: if arr[j] &gt; arr[j+1]: swap(arr[j], arr[j+1])
.DATA arr dw -5, 12, 3, -8, 7, 0, -1, 15 n equ 8
Очікуваний результат: -8, -5, -1, 0, 3, 7, 12, 15
Адресація масиву: [SI], [SI+2] (слова = 2 байти)
Swap через регістр: MOV AX, [SI] / XCHG AX, [SI+2] / MOV [SI], AX
Зовнішній цикл — CX, внутрішній — окремий лічильник (BX?)
Збережіть CX зовнішнього циклу перед внутрішнім (PUSH/POP)
Складність: ⭐⭐⭐⭐ (Very Hard)Напишіть програму ISQRT.ASM, яка обчислює цілу частину квадратного кореня: floor(√N).
Вхід: AX = N (беззнакове, 0..65535)
Результат: AX = floor(√N)
Не використовуйте .DATA — тільки регістри.
low = 0, high = 256 (або 255, бо √65535 ≈ 255.99)
while low &lt; high: mid = (low + high + 1) / 2 if mid * mid &lt;= N: low = mid else: high = mid - 1
return low mid * mid може переповнити 16 біт — результат у DX:AX
Порівнюйте 32-бітний результат (DX:AX) з N (де DX має бути 0)
Якщо DX ≠ 0, то mid² &gt; 65535 &gt; N, тож mid завеликий
Складність: ⭐⭐⭐ (Hard)Напишіть програму PRIME.ASM, яка:
Перевіряє, чи є число простим
(Бонус) Знаходить N-те просте число Вхід: AX = число для перевірки
Результат: BX = 1, якщо просте; BX = 0, якщо ні
if N &lt; 2: return false
if N == 2: return true
if N is even: return false
for d = 3 to √N step 2: if N mod d == 0: return false
return true
Знайдіть N-те просте число (нумерація з 1):
1-ше просте = 2
10-те просте = 29
100-те просте = 541 Перевірка парності: TEST AX, 1 + JZ
Для √N: можна перевіряти d * d &lt;= N замість обчислення кореня
Оптимізація: достатньо перевіряти дільники до √N Роздача варіантів: студенти тягнуть номер (1-5) або викладач призначає
Допомога: давати підказки з розділу "Підказки", не більше
Оцінювання: бонус до оцінки за Lab04 (на розсуд викладача)
Перевірка: студент має продемонструвати роботу в TD
]]></description><link>labs/lab04-td.html</link><guid isPermaLink="false">Labs/Lab04-TD.md</guid><pubDate>Mon, 02 Feb 2026 17:14:15 GMT</pubDate></item><item><title><![CDATA[L04. Архітектура x86 — основні типи інструкцій]]></title><description><![CDATA[Курс: Архітектура комп'ютерних систем
Тема: Інструкції АЛП, умовні переходи, прапорці, доповняльний код
Результати навчання: РН2 (Представлення даних), РН3 (Програмування на асемблері)Після цієї лекції ви зможете:
Пояснити роль регістрів спеціального призначення (DS, CS:IP, SS:SP, BP, ES) у сегментній моделі пам'яті 8086
Використовувати інструкцію MOV з усіма п'ятьма комбінаціями типів операндів
Описати регістр прапорців FLAGS та пояснити, коли встановлюється кожен прапорець стану
Писати код на асемблері з використанням умовних переходів для реалізації керування потоком виконання програми
Застосовувати інструкції АЛП (ADD, SUB, MUL, DIV, AND, OR, XOR, INC, DEC, NEG) у програмах на асемблері
Розуміти представлення чисел у доповняльному коді та розрізняти знакове та беззнакове переповнення
Читати та інтерпретувати базове машинне кодування інструкцій x86
У попередній лекції ми дослідили архітектуру регістрів та модель пам'яті процесора 8086. Тепер звернемо увагу на те, що робить процесор справді корисним: його здатність виконувати інструкції, які маніпулюють даними, приймають рішення та керують потоком виконання програми. Перш ніж заглибитися в ці типи інструкцій, коротко згадаймо, чому Intel 8086 займає таку ключову позицію в історії обчислень.Коли Intel випустила мікропроцесор 8086 8 червня 1978 року, вони створили те, що стало основою архітектури x86. Цей 16-бітний процесор, реалізований приблизно на 20 000 транзисторів, встановив патерни проєктування та формати інструкцій, які зберігаються в кожному сучасному процесорі x86-64, що виробляється сьогодні. 8086 був 16-бітною архітектурою зі здатністю адресувати 1 МБ пам'яті через інноваційну схему адресації сегмент:зміщення.<img alt="Intel 8086 microprocessor in 40-pin ceramic DIP package" src="assets/l04-intel-8086-chip.jpeg" target="_self">Процесор Intel 8086 (1978) у характерному 40-контактному DIP-корпусі (Dual In-line Package). Маркування показує «C8086» та «© INTEL '78». Цей чіп заснував архітектуру x86, яка живить практично всі настільні комп'ютери та сервери сьогодні.Рішення зберегти зворотну сумісність з попередніми процесорами Intel (8008, 8080 та 8085) виявилося трансформаційним для індустрії. Програмісти могли переносити свій існуючий код на нову платформу, і ця філософія зворотної сумісності визначала розвиток x86 з тих пір. Кожен процесор Intel Core та AMD Ryzen досі підтримує набір інструкцій, встановлений у 1978 році.Вивчення 8086 надає кілька суттєвих переваг для освоєння архітектури комп'ютерів:Прозорість. Архітектура 8086 достатньо проста, щоб її зрозуміти повністю, але водночас достатньо складна, щоб проілюструвати всі фундаментальні концепції. Сучасні процесори виконують сотні інструкцій одночасно, використовуючи складне позачергове виконання, передбачення переходів та спекулятивне виконання. Натомість 8086 виконує одну інструкцію за раз у простому циклі вибірка-декодування-виконання. Ця простота робить зв'язок між кодом на асемблері та поведінкою апаратного забезпечення видимим та зрозумілим.Той самий базовий набір інструкцій. Інструкції, які ми вивчаємо на 8086 (ADD, SUB, MOV, JMP, PUSH, POP), мають прямі еквіваленти в сучасному x86-64. Синтаксис майже ідентичний, а концепції переносяться безпосередньо. Вивчення асемблера 8086 — це не археологічна вправа; це вивчення основи, на якій будується сучасний асемблер.Turbo Debugger. Наша практична робота використовує Turbo Debugger (TD) — потужне середовище налагодження, яке забезпечує візуалізацію регістрів, пам'яті, стека та прапорців у реальному часі. У цьому середовищі ви можете спостерігати, як саме кожна інструкція впливає на стан процесора. Цей негайний зворотний зв'язок прискорює навчання та формує інтуїцію щодо поведінки процесора, яку важко розвинути лише через читання.Прямий доступ до апаратного забезпечення. У середовищі DOSBox з 8086 ми маємо безпосередній доступ до машини. Немає ядра операційної системи, що приховує апаратне забезпечення, немає віртуальної пам'яті, що абстрагує фізичні адреси, немає кілець захисту, що запобігають прямій маніпуляції. Ми можемо досліджувати та модифікувати будь-яку комірку пам'яті, взаємодіяти безпосередньо з апаратним забезпеченням та спостерігати повний стан системи. Цей прямий доступ, неможливий у сучасних захищених середовищах, надає безцінне розуміння того, як насправді працюють комп'ютери.У цій лекції ми досліджуємо фундаментальні типи інструкцій, які перетворюють процесор з пасивної схеми на активну обчислювальну машину:
Регістр прапорців FLAGS та як арифметичні операції впливають на його біти стану
Інструкції АЛП для арифметики: ADD, SUB, INC, DEC, NEG
Множення та ділення за допомогою MUL, IMUL, DIV, IDIV
Представлення у доповняльному коді для знакових цілих чисел
Інструкції переходу для керування потоком виконання програми
Побітові операції для маніпуляції окремими бітами
Інструкції бітового зсуву для ефективного множення та вилучення бітів
Наприкінці цієї лекції ви зрозумієте, як процесор виконує обчислення, як він відстежує результати цих обчислень через прапорці та як він приймає рішення на основі цих результатів для керування виконанням програми.У Лекції 3 ми детально розглянули регістри загального призначення процесора 8086. Коротко згадаємо їхню структуру, перш ніж досліджувати регістри спеціального призначення, які доповнюють картину.8086 надає чотири основні 16-бітні регістри загального призначення, кожен з яких поділяється на дві 8-бітні половини:Додатково 8086 надає чотири індексні та вказівникові регістри, які не можуть бути розділені на 8-бітні компоненти:Ці регістри загального призначення зберігають дані, якими маніпулюють програми. Але процесору потрібно більше, ніж просто зберігання даних. Йому потрібно знати, де шукати інструкції, де шукати дані, де знаходиться стек та що сталося під час останньої операції. Тут у гру вступають регістри спеціального призначення.8086 використовує сегментні регістри для розширення своєї 16-бітної архітектури регістрів для адресації повного 1 МБ пам'яті. Як ми дізналися в Лекції 3, формула фізичної адреси така:Фізична адреса = (Сегмент x 16) + Зміщення8086 надає чотири сегментні регістри, кожен з яких служить окремій меті:Ці регістри не є регістрами загального призначення. Ви не можете виконувати над ними арифметичні операції або використовувати їх як тимчасове сховище. Вони існують виключно для надання сегментної частини адрес пам'яті, і процесор автоматично вибирає, який сегментний регістр використовувати, залежно від типу доступу до пам'яті.Регістр сегмента даних (DS) вказує на сегмент, де розташовані змінні вашої програми. Коли ви звертаєтеся до пам'яті для операцій з даними, процесор автоматично використовує DS як сегмент за замовчуванням.Розглянемо читання змінної з пам'яті:mov ax, [bx] ; Читаємо слово з DS:BX в AX
Ця інструкція читає 16-бітне значення з пам'яті. Процесор автоматично обчислює фізичну адресу як (DS x 16) + BX. Вам не потрібно явно вказувати DS; це сегмент за замовчуванням для операцій доступу до даних, що використовують BX, SI або DI.Ініціалізація DS. При запуску програми DS автоматично не вказує на ваш сегмент даних. Ви повинні ініціалізувати його явно. Стандартний патерн такий:mov ax, SEG @data ; Отримуємо адресу сегмента даних
mov ds, ax ; Ініціалізуємо регістр DS
Директива асемблера SEG @data повертає адресу сегмента, куди асемблер розмістив ваші оголошені змінні. Ми не можемо переміщувати безпосереднє значення напряму в сегментний регістр (це апаратне обмеження), тому повинні використовувати регістр загального призначення як посередник.Чому таке обмеження? Сегментні регістри з'єднані безпосередньо зі схемою генерації адрес. Їх завантаження негайно запускає перерахунок меж адрес. Розробники апаратного забезпечення обмежили завантаження сегментних регістрів, щоб запобігти певним часовим проблемам та спростити декодер інструкцій.Регістри CS (сегмент коду) та IP (вказівник інструкцій) працюють як нерозривна пара для визначення того, звідки процесор вибирає свою наступну інструкцію:Фізична адреса наступної інструкції = (CS x 16) + IPНа відміну від інших пар сегмент:зміщення, ви не можете безпосередньо модифікувати CS та IP через прості інструкції MOV. Це обмеження існує з вагомих причин: довільна зміна того, звідки процесор вибирає інструкції, була б небезпечною та могла б призвести до непередбачуваної поведінки.IP змінюється автоматично, коли процесор виконує інструкції. Після вибірки інструкції IP просувається на кількість байтів у цій інструкції. Проста однобайтова інструкція просуває IP на 1; складна багатобайтова інструкція може просунути IP на 5 або 6.CS та IP змінюються разом лише через інструкції керування потоком виконання:
Близькі переходи (near jumps) модифікують лише IP, залишаючись у поточному сегменті коду
Далекі переходи (far jumps) модифікують і CS, і IP, дозволяючи переходи будь-куди в 1 МБ адресного простору
CALL зберігає адресу повернення в стек, а потім модифікує CS:IP
RET відновлює адресу повернення назад у CS:IP
Коли ви бачите CS:IP, відображений у Turbo Debugger як щось на кшталт 0F78:0100, ви можете обчислити, що наступна інструкція буде вибрана з фізичної адреси (0F78h x 16) + 0100h = 0F780h + 0100h = 0F880h.Регістри SS (сегмент стека) та SP (вказівник стека) керують стеком часу виконання:Фізична адреса вершини стека = (SS x 16) + SPСтек — це критична структура даних, що використовується для:
Зберігання адрес повернення під час викликів функцій
Передачі параметрів процедурам
Виділення локальних змінних у процедурах
Тимчасового збереження значень регістрів
Напрямок зростання стека. Стек x86 зростає вниз, у бік менших адрес пам'яті. Ця конвенція походить з ранніх комп'ютерних розробок і зберігається донині:
PUSH спочатку зменшує SP, потім зберігає значення
POP спочатку отримує значення, потім збільшує SP
Коли ви виконуєте PUSH AX, процесор зменшує SP на 2 (тому що AX — це 16 біт = 2 байти), потім записує AX у комірку пам'яті SS:SP. Коли ви виконуєте POP BX, процесор читає з SS:SP у BX, потім збільшує SP на 2.Ініціалізація стека. Сегмент стека та вказівник повинні бути належно ініціалізовані перед будь-якими операціями зі стеком:.STACK 100h ; Оголошуємо 256-байтовий стек у TASM
Ця директива повідомляє асемблеру зарезервувати місце для стека та відповідно налаштувати SS:SP при завантаженні програми.Базовий вказівник (BP) забезпечує стабільну точку посилання всередині стека, особливо корисну під час виконання процедури. Тоді як SP постійно змінюється при додаванні та вилученні значень, BP зазвичай залишається фіксованим протягом виконання процедури.Стандартна конвенція виклику використовує BP для створення «кадру стека» (це буде розглянуто в наступній лекції).Додатковий сегмент (ES) надає додатковий сегмент даних, який програміст може використовувати для будь-якої мети. Найчастіше він використовується з рядковими операціями, що копіюють дані між областями пам'яті:; Копіюємо рядок з DS:SI в ES:DI
mov si, OFFSET source_string
mov di, OFFSET dest_buffer
mov cx, string_length
rep movsb ; Переміщуємо CX байтів з DS:SI в ES:DI
Інструкція MOVSB читає з DS:SI та записує в ES:DI, потім автоматично збільшує (або зменшує, залежно від прапорця напрямку) і SI, і DI. Це дозволяє ефективні передачі пам'ять-пам'ять без використання регістрів загального призначення як посередників.ES повинен бути ініціалізований, як DS:mov ax, SEG @data ; Або будь-яка інша адреса сегмента
mov es, ax ; Ініціалізуємо ES
Розглянемо кілька прикладів для закріплення розуміння адресації сегмент:зміщення.Приклад 1: Якщо DS = 1234h і BX = 5678h, до якої фізичної адреси звертається mov ax, [bx]?Фізична адреса = (DS x 16) + BX = (1234h x 16) + 5678h = 12340h + 5678h = 179B8h
Приклад 2: Якщо SS = 2000h і SP = FFFEh, де знаходиться вершина стека?Вершина стека = (SS x 16) + SP = (2000h x 16) + FFFEh = 20000h + FFFEh = 2FFFEh
Приклад 3: Повна ініціалізація сегментів програми:.MODEL SMALL
.STACK 100h .DATA message DB 'Hello, World!', 0 .CODE
main PROC ; Ініціалізуємо сегмент даних mov ax, @data ; Отримуємо адресу сегмента даних mov ds, ax ; DS тепер вказує на наші дані ; ES часто ініціалізують тим самим сегментом для простоти mov es, ax ; ES = DS (необов'язково, залежить від потреб) ; Тепер ми можемо звертатися до 'message' через DS-відносну адресацію mov si, OFFSET message ; SI = зміщення message в межах DS ; Код програми тут... ; Вихід до DOS mov ax, 4C00h int 21h
main ENDP
END main
Цей код демонструє важливу послідовність ініціалізації. Після mov ds, ax будь-яке посилання на дані, оголошені в сегменті .DATA, буде коректно перетворюватися на відповідну фізичну адресу.Набір регістрів 8086 поділяється на чіткі функціональні категорії:Розуміння цих регістрів та їхніх взаємозв'язків готує нас до дослідження того, як інструкції насправді маніпулюють даними та керують потоком виконання програми. Регістр прапорців FLAGS, який ми детально розглянемо в наступному розділі, забезпечує механізм, за допомогою якого процесор передає результати операцій назад до програми, уможливлюючи умовне виконання та інтелектуальне прийняття рішень.Найфундаментальніша операція в будь-якій комп'ютерній програмі — це переміщення даних з одного місця в інше. Перш ніж додати два числа, ви маєте завантажити їх у регістри. Перш ніж зберегти результат, ви маєте передати його в пам'ять. Інструкція, яка виконує всі ці передачі в асемблері x86, — це MOV.Інструкція MOV копіює значення другого операнда (джерела) до першого операнда (приймача):MOV destination, source
Після виконання приймач містить копію значення джерела. Джерело залишається незмінним.Інструкція MOV підтримує п'ять фундаментальних комбінацій типів операндів:Зверніть увагу на відсутність MOV memory, memory. Ви не можете безпосередньо копіювати з однієї комірки пам'яті в іншу; ви маєте використовувати регістр як посередник. Це обмеження існує тому, що кодування інструкції має місце щонайбільше для однієї адреси пам'яті.Операнд REG може бути будь-яким регістром загального призначення:
16-бітні регістри: AX, BX, CX, DX, SI, DI, BP, SP
8-бітні половини регістрів: AH, AL, BH, BL, CH, CL, DH, DL
Обидва операнди мають бути однакового розміру:MOV AX, BL ; ПОМИЛКА: AX — 16-бітний, BL — 8-бітний
MOV AL, BL ; OK: обидва 8-бітні
MOV AX, BX ; OK: обидва 16-бітні
Операнд memory використовує квадратні дужки для позначення адреси пам'яті:MOV AX, BX ; Копіювати значення, ЩО В BX, до AX
MOV AX, [BX] ; Копіювати значення ЗА АДРЕСОЮ BX до AX
Процесор 8086 підтримує адресацію пам'яті з використанням комбінацій BX, BP, SI та DI:; Адресація одним регістром
MOV AX, [BX] ; адреса з BX (використовує сегмент DS)
MOV AX, [BP] ; адреса з BP (використовує сегмент SS) ; Адресація база + індекс
MOV AX, [BX+SI] ; адреса = BX + SI
MOV AX, [BP+DI] ; адреса = BP + DI ; Зі зміщенням (константний зсув)
MOV AX, [BX+SI+7] ; адреса = BX + SI + 7
Критичне правило: BX і BP не можуть з'являтися разом, і SI та DI не можуть з'являтися разом. Ви також можете використовувати імена змінних безпосередньо: MOV AX, myVariable.Безпосередні значення — це константи, вбудовані безпосередньо в інструкцію:Для шістнадцяткових чисел, що починаються з A-F, додайте нуль на початку: MOV AX, 0FFh.Ви не можете завантажити безпосереднє значення безпосередньо в сегментний регістр:MOV DS, 1234h ; ПОМИЛКА
MOV AX, 1234h ; спочатку завантажити в регістр загального призначення
MOV DS, AX ; потім передати в сегментний регістр
Що ще важливіше, ви взагалі не можете завантажити CS за допомогою MOV. Пара CS:IP визначає, яка інструкція виконуватиметься наступною, і зміна CS потребує спеціальних інструкцій (JMP, CALL, RET).Вікно Dump у Turbo Debugger відображає вміст пам'яті в шістнадцятковому форматі:DS:0100 48 65 6C 6C 6F 20 57 6F-72 6C 64 00 00 00 00 00 Hello World.....
Кожен рядок показує 16 байтів: адреса зліва, шістнадцяткові значення посередині, ASCII-інтерпретація справа.Кожна операція АЛП виробляє не лише результат, але й інформацію про цей результат. Процесор фіксує цю інформацію в 16-бітному регістрі під назвою FLAGS. Окремі біти служать двійковими індикаторами, кожен називається прапорцем, оскільки він або встановлений (1), або скинутий (0).<br><img alt="8086 FLAGS register layout showing bit positions for CF, PF, AF, ZF, SF, TF, IF, DF, and OF" src="assets/l04-flags-register-layout.jpeg" target="_self">Структура 16-бітного регістра FLAGS, що показує прапорці умов (CF, PF, AF, ZF, SF, OF) та прапорці керування (TF, IF, DF). Біти, позначені X, зарезервовані або не використовуються. Прапорці умов встановлюються арифметичними та логічними операціями; прапорці керування впливають на поведінку процесора.Bit: 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
Flag: - - - - OF DF IF TF SF ZF - AF - PF - CF
Прапорці поділяються на дві категорії: прапорці умов (встановлюються операціями) та прапорці керування (керують поведінкою процесора).Прапорець переносу вказує на беззнакове переповнення — коли арифметична операція генерує перенос зі старшого біта:mov al, 255
add al, 1 ; CF=1 (беззнакове переповнення: 256 не вміщується в байт) ; AL = 0 (молодші 8 бітів від 256)
Для віднімання CF вказує, що було потрібне позичання:mov al, 0
sub al, 1 ; AL = 255, CF = 1 (відбулося позичання)
Прапорець нуля встановлюється, коли операція дає результат, рівний нулю:mov al, 5
sub al, 5 ; ZF=1 (результат дорівнює нулю)
ZF є важливим для порівнянь на рівність та керування циклами.Прапорець знаку копіює старший біт (MSB) результату. У знаковій арифметиці MSB=1 означає від'ємне число:mov al, 5
sub al, 10 ; AL = -5 (FBh), SF = 1 (MSB встановлено)
Прапорець переповнення вказує на знакове переповнення — коли результат виходить за межі діапазону, що представляється для знакових чисел (від -128 до +127 для байта):mov al, 100
add al, 50 ; OF=1 (знакове переповнення: 150 &gt; 127) ; AL = 150 = -106 у знаковій інтерпретації
OF і CF є незалежними:
CF виявляє беззнакове переповнення (результат перевищує 255 для байта)
OF виявляє знакове переповнення (результат поза межами від -128 до +127)
mov al, 200 ; 200 беззнаково, -56 знаково
add al, 100 ; CF=1 (300 &gt; 255), OF=0 (-56 + 100 = 44, коректне знакове)
Прапорець парності встановлюється, якщо результат має парну кількість одиничних бітів. Допоміжний прапорець вказує на перенос з біта 3 до біта 4 (використовується для BCD-арифметики). Обидва є застарілими функціями, які рідко використовуються в сучасному програмуванні.Прапорець переривань контролює, чи реагує процесор на апаратні переривання:CLI ; Скинути IF (переривання вимкнено)
STI ; Встановити IF (переривання увімкнено)
Прапорець напрямку контролює напрямок рядкових операцій: DF=0 обробляє вперед (зі зростанням адрес), DF=1 обробляє назад:CLD ; Скинути DF (вперед)
STD ; Встановити DF (назад)
Turbo Debugger відображає прапорці за допомогою компактної нотації:NV UP EI PL NZ NA PO NC
Де: NV/OV (переповнення), UP/DN (напрямок), EI/DI (переривання), PL/NG (знак), NZ/ZR (нуль), NA/AC (допоміжний), PO/PE (парність), NC/CY (перенос).Приклад 1: Беззнакове переповненняmov al, 255
add al, 1 ; AL=0, CF=1, ZF=1, SF=0, OF=0
Результат дорівнює 0 (ZF=1), беззнакове переповнення (CF=1), але немає знакового переповнення (255 — це -1, і -1+1=0 є коректним).Приклад 2: Знакове переповненняmov al, 100
add al, 50 ; AL=150, CF=0, ZF=0, SF=1, OF=1
Беззнаковий результат (150) вміщується в байт (CF=0), але знаковий результат перевищує +127 (OF=1). Старший біт встановлено (SF=1).Приклад 3: Нульовий результатmov al, 5
sub al, 5 ; AL=0, CF=0, ZF=1, SF=0, OF=0
Регістр FLAGS забезпечує умовне виконання. Інструкція CMP віднімає операнди без збереження результату, лише встановлюючи прапорці:CMP AX, BX ; виконує AX - BX, встановлює прапорці, відкидає результат
JE equal_label ; перехід, якщо ZF=1 (значення були рівні)
JA above_label ; перехід, якщо CF=0 і ZF=0 (AX &gt; BX, беззнаково)
JG greater_label ; перехід, якщо SF=OF і ZF=0 (AX &gt; BX, знаково)
Розуміння того, які прапорці перевіряє кожен умовний перехід, є критично важливим для правильної логіки розгалуження.Тепер, коли ми розуміємо прапорці, можемо дослідити їхнє основне призначення: забезпечення умовного виконання. Прапорці є основою прийняття будь-яких рішень у мові асемблера.У мовах високого рівня ми пишемо умовні оператори на кшталт if (x == 5). Процесор покладається на двоетапний процес:
Виконати операцію, яка встановлює прапорці (арифметичну, порівняння або логічну)
Перевірити значення прапорців, щоб вирішити, чи переходити до іншої інструкції
Це розділення є фундаментальним. Порівняння та рішення на його основі завжди є окремими інструкціями.Кожен умовний перехід перевіряє один або кілька прапорців і приймає рішення:
Якщо умова ІСТИННА: Перейти до вказаної мітки (змінити IP)
Якщо умова ХИБНА: Продовжити виконання наступної інструкції
Jcc label ; cc = код умови (Z, NZ, C, NC тощо)
Інструкції JZ та JE мають ідентичні коди операцій — різні назви допомагають висловити намір. Використовуйте JZ, коли перевіряєте, чи результат дорівнює нулю; використовуйте JE, коли перевіряєте рівність після CMP.При порівнянні знакових чисел потрібно враховувати разом прапорець знаку та прапорець переповнення:Чому XOR? Операція XOR скасовує «хибні» індикації знаку, коли стається переповнення:MOV AL, 127 ; AL = 7Fh
CMP AL, -1 ; Обчислює 127 - (-1) = 128 ; SF = 1 (біт 7 встановлено), OF = 1 (переповнення!) ; SF XOR OF = 0, отже 127 &gt;= -1 (правильно!)
Для беззнакових чисел враховуємо лише прапорець переносу (позики) та прапорець нуля:Термінологія: Above/Below (вище/нижче) = беззнакові порівняння. Greater/Less (більше/менше) = знакові порівняння.<br><img alt="8086 conditional jump instructions reference table" src="assets/l04-conditional-jump-table.png" target="_self">Повна довідкова таблиця інструкцій умовного переходу 8086, організована за категоріями: загальні операції (на основі прапорців), знакові операції (для порівняння знакових цілих чисел) та беззнакові операції (для порівняння беззнакових цілих чисел). Стовпець умови показує стани прапорців, які викликають кожен перехід.; Знайти максимум двох знакових чисел MOV AX, -50 ; Перше число MOV BX, 30 ; Друге число CMP AX, BX ; Порівняти AX - BX JGE ax_is_greater ; Перехід якщо AX &gt;= BX (знакове) MOV AX, BX ; AX було менше, копіюємо BX в AX
ax_is_greater: ; AX тепер містить більше значення (30)
Розуміння кодування переходів розкриває важливі аспекти структури програми.Умовний перехід складається з:
Байт коду операції — визначає тип переходу (JZ, JC тощо)
Байт(и) зміщення — вказують, на скільки далеко переходити
Для коротких переходів зміщення є знаковим байтом: діапазон від -128 до +127 байтів від кінця інструкції.Адреса Код Інструкція
1000h 72 04 JC +4
1002h B0 00 MOV AL, 0
1004h EB 02 JMP +2
1006h B0 FF MOV AL, FFh
1008h ... (продовження)
JC за адресою 1000h: код операції 72h = JC, зміщення 04h = перехід на 4 байти вперед.Важливо: Зміщення обчислюється від КІНЦЯ поточної інструкції:Ціль = 1002h (кінець JC) + 04h = 1006h
Переходи назад використовують від'ємні (доповняльний код) зміщення:Адреса Код Інструкція
1000h B0 00 MOV AL, 0
1002h 04 01 ADD AL, 1
1004h 3C 0A CMP AL, 10
1006h 72 FA JC -6
1008h ...
Зміщення FAh = -6: Ціль = 1008h + (-6) = 1002h. Це створює цикл.Короткі переходи є кращими: менше пам'яті, швидше виконання. Асемблери вибирають автоматично, але можна примусово вказати:JMP SHORT label ; Примусово короткий перехід
JMP NEAR PTR label ; Примусово близький перехід
JMP завжди передає керування — жодних умов не перевіряється:JMP label ; Безумовний перехід
Пропуск блоків коду: CMP AX, 0 JZ handle_zero ; Звичайний випадок JMP done ; Пропустити обробник нуля
handle_zero: ; Випадок нуля
done:
Нескінченні цикли:forever: ; Тіло циклу JMP forever
Цикл while:while_start: CMP CX, 0 JE while_end ; Вихід якщо CX == 0 DEC CX JMP while_start
while_end:
Цикл do-while (ефективніший):do_start: DEC CX JNZ do_start ; Повторити якщо CX != 0 MOV AL, 255 ADD AL, 1 ; AL = 0, CF = 1, ZF = 1 JC carry_set ; Перехід якщо прапорець переносу встановлено JMP test_zero
carry_set: ; Обробка випадку переносу
test_zero: MOV AL, 5 SUB AL, 5 ; ZF = 1 (результат 0) JZ zero_set ; Перехід якщо нуль
zero_set: ; Продовження
Кілька прапорців можуть бути встановлені одночасно. Об'єднуйте умовні переходи в ланцюжок для обробки складних умов.JCXZ виконує перехід, якщо CX дорівнює нулю, не впливаючи на прапорці:JCXZ skip_loop ; Перехід якщо CX == 0
loop_start: ; Тіло циклу LOOP loop_start
skip_loop:
Корисно для пропуску циклів, коли лічильник вже дорівнює нулю.Арифметико-логічний пристрій (АЛП) є обчислювальним серцем кожного процесора. У Лекції 2 ми побудували АЛП з базових логічних вентилів, створюючи схеми, здатні виконувати додавання, віднімання, AND, OR та XOR над двійковими значеннями. Тепер ми розглянемо, як асемблер x86 надає доступ до цих операцій АЛП через машинні інструкції. Процесор 8086 організовує інструкції АЛП у окремі групи залежно від шаблонів операндів та неявного використання регістрів.Інструкції Групи 1 мають спільну характеристику: вони приймають ті самі комбінації операндів, що й інструкція MOV, яку ми вивчали раніше. Ця однорідність спрощує навчання, оскільки ви можете застосовувати ті самі знання про режими адресації до різних типів інструкцій.Інструкції АЛП Групи 1 підтримують п'ять комбінацій операндів:Зверніть увагу, що операції пам'ять-пам'ять не дозволені. Це фундаментальне обмеження архітектури x86: щонайбільше один операнд може бути посиланням на пам'ять. Якщо вам потрібно додати два значення з комірок пам'яті, спочатку треба завантажити одне в регістр, а потім додати з іншої комірки пам'яті.Інструкція ADD обчислює суму двох операндів, зберігаючи результат у приймачі (першому операнді):destination = destination + source
Приклади:mov ax, 10 ; AX = 10
add ax, 5 ; AX = AX + 5 = 15 mov bx, 100
add ax, bx ; AX = AX + BX = 115 mov word [data], 50
add [data], 10 ; [data] = 50 + 10 = 60
ADD виконує двійкове додавання незалежно від того, чи ви розглядаєте значення як знакові чи беззнакові. Процесор не «знає» вашу інтерпретацію; він просто додає бітові послідовності. Різниця полягає в тому, як ви інтерпретуєте прапорці після операції.Наприклад, додавання 7Fh (127 у десятковій системі, максимальне додатне знакове значення байта) плюс 1:mov al, 7Fh ; AL = 0111 1111 binary = 127 decimal
add al, 1 ; AL = 1000 0000 binary = 128 unsigned, -128 signed
Результат 80h є правильним для беззнакової арифметики (127 + 1 = 128), але представляє переповнення у знаковій арифметиці (127 + 1 повинно бути 128, але діапазон знакового байта від -128 до +127). Прапорець переповнення (OF) вказує на цю умову знакового переповнення.SUB віднімає операнд-джерело від операнда-приймача:destination = destination - source
Приклади:mov ax, 100 ; AX = 100
sub ax, 25 ; AX = 100 - 25 = 75 mov bx, 30
sub ax, bx ; AX = 75 - 30 = 45 mov al, 5
sub al, 10 ; AL = 5 - 10 = -5 (= FBh як беззнакове, 251)
Віднімання у двійковій системі зазвичай реалізується як додавання доповняльного коду. Коли ви пишете SUB AX, BX, процесор внутрішньо обчислює AX + (-BX), де -BX — це доповняльний код BX. Тому той самий апаратний суматор може виконувати як додавання, так і віднімання.Прапорець переносу (CF) встановлюється, якщо віднімання потребує позики (для беззнакової інтерпретації), а прапорець переповнення (OF) встановлюється при знаковому переповненні.CMP виконує віднімання, але відкидає результат, оновлюючи лише прапорці:flags = destination - source ; результат не зберігається
CMP є важливою для умовної логіки. Вона відповідає на запитання типу «чи AX більше за BX?», виконуючи віднімання та дозволяючи вам перевірити прапорці, не знищуючи жодного з операндів.Приклади:mov ax, 10
mov bx, 10
cmp ax, bx ; Обчислює 10 - 10 = 0, встановлює ZF=1 (рівні) ; AX і BX не змінюються mov ax, 15
cmp ax, 10 ; Обчислює 15 - 10 = 5, встановлює ZF=0, SF=0 (додатне) ; AX все ще дорівнює 15 mov ax, 5
cmp ax, 10 ; Обчислює 5 - 10 = -5, встановлює ZF=0, SF=1 (від'ємне)
Після CMP інструкції умовного переходу перевіряють прапорці для відповідного розгалуження:cmp ax, bx
je equal_label ; Jump if Equal (ZF=1)
jg greater_label ; Jump if Greater (знакове порівняння)
ja above_label ; Jump if Above (беззнакове порівняння)
jl less_label ; Jump if Less (знакове порівняння)
jb below_label ; Jump if Below (беззнакове порівняння)
Розрізнення між «greater/less» (знакові) та «above/below» (беззнакові) відображає дві можливі інтерпретації тих самих бітових послідовностей.Логічні інструкції виконують побітові операції, де кожен біт результату залежить лише від відповідних бітів операндів. На відміну від арифметичних операцій, що розглядають усе значення з переносами між бітовими позиціями, логічні операції обробляють кожен біт незалежно.AND видає біт 1 лише там, де обидва відповідні вхідні біти дорівнюють 1: 1010 1100
&amp; 1111 0000
----------- 1010 0000
Приклади:mov al, 0ACh ; AL = 1010 1100 binary
and al, 0F0h ; AL = AL AND 1111 0000 = 1010 0000 = A0h
AND зазвичай використовується для маскування — виділення конкретних бітів зі значення, водночас примусово обнуляючи інші. «Маска» має 1 у позиціях, які ви хочете зберегти, і 0 у позиціях, які хочете очистити.Практичне застосування — виділення старшого ніббла:mov al, 3Fh ; AL = 0011 1111 (містить якесь значення)
and al, 0F0h ; Маска зберігає старші 4 біти: 0011 0000 = 30h
shr al, 4 ; Зсув вправо дає: 0000 0011 = 03h
Практичне застосування — очищення конкретних бітів:; Очистити біт 7 регістра AL (забезпечити додатне значення у знаковій інтерпретації)
and al, 7Fh ; 7Fh = 0111 1111, очищає біт 7, зберігає біти 0-6
TEST виконує AND, але відкидає результат, оновлюючи лише прапорці:flags = destination AND source ; результат не зберігається
TEST використовується для перевірки, чи встановлені конкретні біти, без зміни операнда.Приклади:; Перевірити, чи встановлений біт 0 регістра AL (чи число непарне?)
test al, 01h ; AND з 0000 0001
jnz is_odd ; Jump if Not Zero (біт 0 був встановлений) ; Перевірити, чи встановлений біт 7 (чи знакове значення від'ємне?)
test al, 80h ; AND з 1000 0000
jnz is_negative ; Jump if bit 7 was set ; Перевірити, чи AL дорівнює нулю
test al, al ; AL AND AL = AL, але важливі лише прапорці
jz is_zero ; Jump if Zero
Ідіома TEST AL, AL є стандартним способом перевірки, чи регістр дорівнює нулю. Вона еквівалентна CMP AL, 0, але на один байт коротша в закодованій інструкції.OR видає біт 1 там, де один або обидва відповідні вхідні біти дорівнюють 1: 1010 1100
| 0011 0011
----------- 1011 1111
Приклади:mov al, 0ACh ; AL = 1010 1100
or al, 33h ; AL = 1010 1100 OR 0011 0011 = 1011 1111 = BFh
OR зазвичай використовується для встановлення конкретних бітів в 1, зберігаючи інші. Маска має 1 у позиціях, які ви хочете встановити, і 0 у позиціях, які хочете зберегти.Практичне застосування — встановлення прапорців:; Встановити біт 7 регістра AL (примусово зробити значення від'ємним у знаковій інтерпретації)
or al, 80h ; 80h = 1000 0000, встановлює біт 7, зберігає інші ; Об'єднати два нібли в байт
mov al, 03h ; Значення старшого ніббла: 0000 0011
shl al, 4 ; Зсув вліво: 0011 0000
mov bl, 0Fh ; Значення молодшого ніббла: 0000 1111
or al, bl ; Об'єднання: 0011 1111 = 3Fh
XOR видає біт 1 там, де відповідні вхідні біти різні: 1010 1100
^ 0011 0011
----------- 1001 1111
Приклади:mov al, 0ACh ; AL = 1010 1100
xor al, 33h ; AL = 1010 1100 XOR 0011 0011 = 1001 1111 = 9Fh
XOR має унікальні математичні властивості, що роблять її цінною у програмуванні:
Самообернена: A XOR B XOR B = A (XOR з тим самим значенням двічі повертає оригінал)
Комутативна: A XOR B = B XOR A
Асоціативна: (A XOR B) XOR C = A XOR (B XOR C)
Ідіома обнулення через XOR:xor ax, ax ; AX = AX XOR AX = 0
Це найкращий спосіб обнулити регістр в асемблері x86. Будь-яке значення, XOR з самим собою, дає нуль. Ця інструкція насправді швидша за MOV AX, 0 на більшості процесорів, тому що:
Закодована інструкція коротша (2 байти проти 3 байтів для MOV)
Сучасні процесори розпізнають цей шаблон і спеціально оптимізують його
Вона розриває ланцюжки залежностей (процесор знає, що результат буде нуль незалежно від попереднього значення AX)
Практичне застосування — перемикання бітів:; Перемкнути біт 0 (перемикання між парним і непарним)
xor al, 01h ; Якщо біт 0 був 0, стає 1; якщо 1, стає 0 ; Просте шифрування/дешифрування
mov al, 'A' ; AL = 41h
xor al, 55h ; «Шифрування»: AL = 14h
xor al, 55h ; «Дешифрування»: AL = 41h = 'A' знову
Усі інструкції Групи 1 модифікують той самий набір прапорців у регістрі FLAGS:Важлива відмінність:
CF вказує на беззнакове переповнення (результат не вміщується у беззнаковій інтерпретації)
OF вказує на знакове переповнення (результат не вміщується у знаковій інтерпретації)
Процесор встановлює обидва прапорці після кожної арифметичної операції, дозволяючи вам обрати, яку інтерпретацію використовувати для подальших умовних переходів.Інструкції АЛП Групи 2 обробляють множення та ділення. Ці операції фундаментально відрізняються від інструкцій Групи 1 у способі обробки операндів, оскільки їхні результати можуть перевищувати розмір вхідних операндів. Множення двох 16-бітних чисел може дати 32-бітний результат; ділення потребує діленого, що може бути вдвічі більшим за дільник.Для розміщення цих більших значень інструкції Групи 2 використовують неявні регістри — конкретні регістри, що автоматично залучаються без явного зазначення в інструкції.На відміну від інструкцій Групи 1, що приймають два явні операнди, інструкції Групи 2 вказують лише один операнд (множник або дільник):MUL operand ; операнд може бути REG або memory
DIV operand ; НЕ безпосередні значення
Інший операнд (множене або ділене) та місця для результату є неявними, визначеними розміром операнда.8086 надає дві інструкції множення:
MUL — беззнакове множення
IMUL — знакове множення (Integer MULtiply)
Коли операнд є байтом (8-бітний регістр або байтова комірка пам'яті):AX = AL × operand
Неявне множене — це AL, а 16-бітний результат записується в AX.Приклади:mov al, 10 ; AL = 10 (множене)
mov bl, 5 ; BL = 5 (множник)
mul bl ; AX = AL × BL = 10 × 5 = 50 ; AX = 0032h (50 у шістнадцятковій системі)
Хоча обидва операнди вміщуються в 8 бітів і результат (50) також вміщується в 8 бітів, повний 16-бітний результат зберігається в AX. Якщо AL=200 і BL=3, результат 600 (0258h) перевищує 255 і потребує повного 16-бітного AX.mov al, 200 ; AL = 200 (C8h)
mov bl, 3 ; BL = 3
mul bl ; AX = 200 × 3 = 600 = 0258h ; AH = 02h, AL = 58h
Коли операнд є словом (16-бітний регістр або комірка пам'яті розміром слово):DX:AX = AX × operand
Неявне множене — це AX, а 32-бітний результат охоплює два регістри: старше слово в DX, молодше слово в AX.Приклади:mov ax, 1000 ; AX = 1000 (множене)
mov bx, 100 ; BX = 100 (множник)
mul bx ; DX:AX = AX × BX = 1000 × 100 = 100000 ; 100000 = 186A0h ; DX = 0001h (старше слово) ; AX = 86A0h (молодше слово)
Для перевірки: 0001 86A0h = 1×65536 + 34464 = 65536 + 34464 = 100000. Правильно.Приклад з більшими числами:mov ax, 0FFFFh ; AX = 65535 (максимальне беззнакове 16-бітне значення)
mov bx, 0FFFFh ; BX = 65535
mul bx ; DX:AX = 65535 × 65535 = 4,294,836,225 ; = FFFE0001h ; DX = FFFEh, AX = 0001h
IMUL працює ідентично до MUL, але інтерпретує операнди як знакові значення у доповняльному коді:mov al, -5 ; AL = FBh (-5 у доповняльному коді)
mov bl, 10 ; BL = 0Ah (10)
imul bl ; AX = (-5) × 10 = -50 ; AX = FFCEh (-50 у 16-бітному доповняльному коді)
Для додатних операндів у межах діапазону MUL та IMUL дають ідентичні бітові послідовності. Різниця проявляється, коли операнди від'ємні або при інтерпретації прапорців.MUL та IMUL впливають лише на два прапорці:
CF та OF встановлюються, якщо результат виходить за межі молодшої половини (AH для множення байтів, DX для множення слів)
CF та OF скидаються, якщо старша половина дорівнює нулю (для MUL) або є знаковим розширенням молодшої половини (для IMUL)
Це повідомляє вам, чи повний результат вміщується в той самий розмір, що й операнди:mov al, 10
mov bl, 5
mul bl ; AX = 50, вміщується в AL → CF=0, OF=0 mov al, 100
mov bl, 10
mul bl ; AX = 1000, перевищує 255 → CF=1, OF=1
Ділення складніше за множення, оскільки воно дає два результати: частку та остачу.
DIV — беззнакове ділення
IDIV — знакове ділення (Integer DIVide)
Коли операнд є байтом:AL = AX / operand (частка)
AH = AX mod operand (остача)
Неявне ділене — це повний регістр AX (16 біт), поділений на 8-бітний операнд.Приклади:mov ax, 100 ; AX = 100 (ділене)
mov bl, 7 ; BL = 7 (дільник)
div bl ; AL = 100 / 7 = 14 (частка) ; AH = 100 mod 7 = 2 (остача) ; Перевірка: 14 × 7 + 2 = 98 + 2 = 100 ✓
mov ax, 255 ; AX = 255 (FFh)
mov bl, 16 ; BL = 16 (10h)
div bl ; AL = 255 / 16 = 15 ; AH = 255 mod 16 = 15 ; Перевірка: 15 × 16 + 15 = 240 + 15 = 255 ✓
Коли операнд є словом:AX = DX:AX / operand (частка)
DX = DX:AX mod operand (остача)
Неявне ділене — це 32-бітне значення, утворене DX:AX (DX — старше слово, AX — молодше слово), поділене на 16-бітний операнд.Критично: Перед виконанням ділення слів з беззнаковим діленим, що вміщується в 16 біт, ви повинні очистити DX:mov dx, 0 ; Очистити старше слово діленого
mov ax, 10000 ; AX = 10000 (молодше слово діленого)
mov bx, 300 ; BX = 300 (дільник)
div bx ; AX = 10000 / 300 = 33 (частка) ; DX = 10000 mod 300 = 100 (остача) ; Перевірка: 33 × 300 + 100 = 9900 + 100 = 10000 ✓
Якщо ви забудете очистити DX, ділення використовуватиме будь-яке «сміттєве» значення в DX як частину набагато більшого діленого, даючи некоректні результати або викликаючи переповнення при діленні.Ділення справжнього 32-бітного діленого:; Поділити 100,000 на 1000
; 100,000 = 186A0h = DX:AX, де DX=0001h, AX=86A0h
mov dx, 0001h
mov ax, 86A0h ; DX:AX = 100,000
mov bx, 1000 ; дільник = 1000 (3E8h)
div bx ; AX = 100000 / 1000 = 100 ; DX = 100000 mod 1000 = 0
IDIV інтерпретує ділене та дільник як знакові значення. Для знакового ділення 16-бітного значення в AX ви повинні розширити знак в DX за допомогою інструкції CWD (Convert Word to Doubleword):mov ax, -100 ; AX = FF9Ch (-100 у доповняльному коді)
cwd ; Розширити знак AX в DX:AX ; DX = FFFFh (бо AX від'ємний)
mov bx, 7 ; дільник = 7
idiv bx ; AX = -100 / 7 = -14 (частка) ; DX = -100 mod 7 = -2 (остача)
Примітка: При знаковому діленні остача має той самий знак, що й ділене.Ділення потребує уважності для уникнення помилок виконання:1. Ділення на нуль викликає виняток процесора:mov ax, 100
mov bl, 0
div bl ; ЗБІЙ! Генерує переривання 0 (помилка ділення)
У програмах DOS це зазвичай завершує програму з повідомленням про помилку. У сучасних операційних системах процес отримує сигнал (SIGFPE в Unix) або виняток.2. Переповнення частки також викликає той самий виняток:mov ax, 1000 ; Ділене = 1000
mov bl, 1 ; Дільник = 1
div bl ; Частка = 1000, але AL може містити лише 0-255! ; ЗБІЙ! Переповнення при діленні
Якщо частка не вміщується в регістр-приймач (AL для ділення байтів, AX для ділення слів), процесор генерує помилку ділення, таку саму, як при діленні на нуль.3. Невизначені прапорці:На відміну від більшості інструкцій, DIV та IDIV залишають прапорці в невизначеному стані. Не покладайтеся на значення прапорців після ділення. Якщо вам потрібно перевірити частку або остачу, використовуйте CMP або TEST явно:div bl
cmp al, 0 ; Явно перевірити, чи частка дорівнює нулю
jz quotient_zero ; Тепер ми можемо безпечно перейти
Асиметрія між результатами множення (завжди подвійної ширини) та діленими для ділення (завжди подвійної ширини) дозволяє ланцюжити ці операції: результат множення в DX:AX може стати діленим для подальшого ділення.Третя група інструкцій АЛП включає унарні операції, що працюють з одним операндом. Вони змінюють один регістр або комірку пам'яті на місці, що робить їх корисними для лічильників, маніпуляцій з адресами та перетворення знаку.Операндом може бути регістр або комірка пам'яті (не безпосереднє значення, оскільки результат має бути збережений назад).inc ax ; AX = AX + 1
dec cx ; CX = CX - 1
inc word [count] ; слово в пам'яті збільшено на 1
dec byte [si] ; байт в пам'яті зменшено на 1
Прапорці, що змінюються: ZF, SF, OF, PF, AF встановлюються відповідно до результату.Важливо: CF (прапорець переносу) НЕ змінюється інструкціями INC та DEC. Це навмисне проєктне рішення дозволяє використовувати їх у циклах без порушення переносу від багатоточної арифметики:; Багатоточне додавання в циклі
mov si, 10 ; Лічильник
process_loop: add ax, [data_low] ; Додати молодше слово, може встановити CF adc dx, [data_high] ; Додати старше слово З переносом dec si ; Зменшити лічильник (CF збережено!) jnz process_loop ; Цикл, якщо лічильник != 0
Шаблон лічильника циклу: mov cx, 10 ; Ініціалізація лічильника
loop_start: ; ... тіло циклу ... dec cx ; CX = CX - 1, встановлює ZF, коли CX стає 0 jnz loop_start ; Перейти назад, якщо CX != 0
NOT інвертує кожен біт: 0 стає 1, а 1 стає 0.mov al, 11110000b ; AL = F0h
not al ; AL = 00001111b = 0Fh mov al, 00000101b ; AL = 05h (десяткове 5)
not al ; AL = 11111010b = FAh (десяткове 250)
NOT НЕ впливає на жодні прапорці. Це суто побітова операція без арифметичної інтерпретації.NEG обчислює заперечення в доповняльному коді: якщо операнд представляє N, NEG видає -N.mov al, 5 ; AL = 00000101b = +5
neg al ; AL = 11111011b = -5 (доповняльний код) ; Обчислення: NOT(5) + 1 = 11111010 + 1 = 11111011 mov al, 1
neg al ; AL = FFh = -1 mov ax, 100
neg ax ; AX = FF9Ch = -100 (знакове 16-бітне)
Математично: NEG(x) = NOT(x) + 1 = 0 - xПрапорці, що змінюються NEG:Особливі випадки:mov al, 0
neg al ; AL = 0, CF = 0, ZF = 1 mov al, 80h ; AL = -128 (найменше від'ємне значення байта)
neg al ; AL = 80h (без змін!), OF = 1 ; Неможливо представити +128 у знаковому байті
Як процесор представляє від'ємні числа, і як та сама інструкція ADD може працювати як для додатних, так і для від'ємних значень? Відповідь — доповняльний код, одна з найелегантніших конвенцій в архітектурі комп'ютерів.Історичні підходи та їхні проблеми:
Знак-модуль: Один біт для знаку, решта — для модуля. Проблема: два нулі (+0 і -0), складні арифметичні схеми.
Обернений код: Заперечення через інверсію бітів. Проблема: все ще два нулі, потребує циклічного переносу.
Доповняльний код: Заперечення через інверсію та додавання 1. Переможець: один нуль, однакове апаратне забезпечення для знакової та беззнакової арифметики.
Старший біт має від'ємну вагу. Для 8 бітів b7...b0:значення = -b7 × 128 + b6 × 64 + b5 × 32 + b4 × 16 + b3 × 8 + b2 × 4 + b1 × 2 + b0 × 1
Приклад: Інтерпретація 11111011:= -1×128 + 1×64 + 1×32 + 1×16 + 1×8 + 0×4 + 1×2 + 1×1
= -128 + 123 = -5
Зверніть увагу на асиметрію: на одне від'ємне число більше, ніж додатних (нуль займає невід'ємний слот).Метод: Інвертувати всі біти та додати 1Приклад: Перетворити +5 на -5 у 8 бітах:+5 = 00000101
NOT = 11111010
+1 = 11111011 = -5
Саме це і робить інструкція NEG.Те саме апаратне забезпечення для додавання працює як для знакової, так і для беззнакової арифметики: 11111011 (-5 у доповняльному коді)
+ 00001000 (+8)
---------- 00000011 (+3, правильно!)
Перенос відкидається, і ми отримуємо правильний знаковий результат, використовуючи ту саму схему, що додає беззнакові числа.Процесор «не знає», чи біти знакові, чи беззнакові. Патерн 11111011 — це:
Беззнакове: 251
Знакове: -5
Ось чому x86 має два набори умовних переходів:
CF (прапорець переносу): Беззнакове переповнення. Результат не вміщується в 0 до 2^n - 1.
OF (прапорець переповнення): Знакове переповнення. Результат не вміщується в -2^(n-1) до 2^(n-1) - 1.
Приклад 1: Лише беззнакове переповненняmov al, 200 ; 200 беззнакове, -56 знакове
add al, 100 ; Результат: 44 (300 mod 256)
; CF = 1 (200 + 100 = 300 &gt; 255)
; OF = 0 (-56 + 100 = 44, коректне знакове)
Приклад 2: Лише знакове переповненняmov al, 100 ; 100 в обох випадках
add al, 50 ; Результат: 150
; CF = 0 (150 &lt; 256)
; OF = 1 (150 &gt; 127, некоректне знакове)
Результат 10010110b = 150 беззнакове, але -106 знакове (неправильно).Приклад 3: Обидва переповненняmov al, 128 ; 80h = 128 беззнакове, -128 знакове
add al, 128 ; Результат: 0
; CF = 1 (256 &gt; 255)
; OF = 1 (-128 + -128 = -256 &lt; -128)
Практичне правило: Використовуйте CF для беззнакових даних (адреси, лічильники), OF для знакових даних (координати, температури).При переміщенні знакових значень до більших регістрів, реплікуйте знаковий біт:mov al, -5 ; AL = FBh = 11111011
cbw ; Convert Byte to Word: AX = FFFBh = -5 (16-бітне) ; Без розширення знаку, 00FBh = +251 (неправильно!) Один нуль: Всі біти = 0.
Уніфіковане апаратне забезпечення: ADD/SUB працюють як для знакових, так і для беззнакових.
Старший біт = знак: Старший біт = 1 означає від'ємне у знаковій інтерпретації.
Заперечення: NEG(x) = NOT(x) + 1.
Асиметричний діапазон: На одне від'ємне більше, ніж додатних.
Два прапорці переповнення: CF для беззнакових, OF для знакових.
Дослідивши різні типи інструкцій, доступних у x86 — від переміщення даних до арифметичних операцій і керування потоком виконання — тепер розглянемо, як ці інструкції записуються та організовуються в програмах на мові асемблера. Розуміння структури мови асемблера є необхідним для читання, написання та налагодження низькорівневого коду.На найбільш фундаментальному рівні процесор виконує машинний код: необроблені двійкові дані, де кожна інструкція представлена послідовністю байтів. Декодер інструкцій процесора читає ці байти та визначає, яку операцію виконати, до яких регістрів чи комірок пам'яті звертатися та де знаходяться операнди.Машинний код — це єдина мова, яку ЦП дійсно розуміє. Він складається з числових кодів операцій та закодованих операндів, що зберігаються як шістнадцяткові значення в пам'яті:B8 04 00 ; Три байти: код операції B8, за ним безпосереднє значення 0004
CD 21 ; Два байти: код операції CD, за ним номер переривання 21
Хоча процесори виконують машинний код безпосередньо, люди не можуть практично писати програми як послідовності шістнадцяткових чисел. Відповідність між двійковими шаблонами та операціями не є інтуїтивною, і одна помилково введена цифра може спричинити катастрофічні помилки.Мова асемблера вирішує цю проблему, надаючи людино-читабельне представлення машинного коду. Кожна інструкція асемблера має однозначну відповідність з інструкцією машинного коду. Коли ви пишете MOV AX, 4, асемблер транслює це безпосередньо в байти B8 04 00. Немає абстракції, немає інтерпретації, немає високорівневого перетворення. Асемблер — це просто машинний код, записаний у формі, яку люди можуть читати та писати.Ця однозначна відповідність відрізняє асемблер від мов вищого рівня. У C оператор x = a + b * c може компілюватися в десятки машинних інструкцій залежно від компілятора, рівня оптимізації та типів даних. В асемблері кожен рядок, який ви пишете, стає рівно однією машинною інструкцією (за деякими винятками для псевдоінструкцій, які асемблер розгортає).Асемблер — це програма, яка транслює вихідний код мови асемблера у виконуваний машинний код. Асемблер виконує кілька ключових функцій:
Трансляція мнемонік: Перетворює людино-читабельні назви інструкцій (MOV, ADD, JMP) у відповідні двійкові коди операцій
Кодування операндів: Транслює назви регістрів та режими адресації у відповідні бітові шаблони
Розв'язання символів: Перетворює символічні мітки та імена змінних в фактичні адреси пам'яті
Перетворення літералів: Трансформує числові літерали з десяткової, шістнадцяткової чи двійкової нотації у двійкові значення
Наприклад, коли асемблер зустрічає:start: mov ax, 1234h jmp start
Він повинен:
Призначити адресу мітці start
Перетворити mov ax, 1234h у байти B8 34 12 (зверніть увагу на порядок байтів little-endian)
Обчислити відносне зміщення від інструкції JMP назад до start
Закодувати інструкцію JMP з цим зміщенням
Результатом роботи асемблера зазвичай є об'єктний файл, що містить машинний код, який компонувальник потім об'єднує з іншими об'єктними файлами та бібліотеками для створення кінцевого виконуваного файлу.Кожна інструкція асемблера дотримується єдиного формату:[мітка:] мнемоніка [операнди] [; коментар]
Розглянемо кожен компонент:Мітка (необов'язкова): Символічне ім'я, що позначає місце в коді. Мітки супроводжуються двокрапкою та слугують цілями для інструкцій переходу та виклику. Вони також можуть позначати розташування даних.loop_start: ; Ця мітка позначає початок циклу dec cx jnz loop_start ; Перехід назад до loop_start, якщо CX не нуль
Мнемоніка: Назва інструкції. Це людино-читабельне представлення операції, наприклад MOV (переміщення даних), ADD (додавання), SUB (віднімання), JMP (перехід) або CALL (виклик процедури). Мнемоніка є обов'язковою для кожної інструкції.Операнди: Нуль, один або два значення, над якими інструкція виконує операції. Кількість і тип операндів залежать від конкретної інструкції:
Нуль операндів: RET (повернення), NOP (порожня операція), PUSHF (збереження прапорців)
Один операнд: INC AX (інкремент), PUSH BX (збереження в стек), JMP label (перехід)
Два операнди: MOV AX, BX (переміщення), ADD CX, 5 (додавання), CMP DX, SI (порівняння)
Коли присутні два операнди, перший зазвичай є операндом-приймачем, а другий — операндом-джерелом (у синтаксисі Intel, який ми використовуємо в цьому курсі).Коментар (необов'язковий): Текст після крапки з комою ігнорується асемблером. Коментарі документують код для людей-читачів:mov ah, 9 ; Функція DOS 9: вивести рядок
mov dx, offset message ; DX вказує на рядок для виводу
int 21h ; Виклик переривання DOS
Розглянемо повну робочу програму на асемблері, яка виводить "Hello, World!", використовуючи переривання DOS:; Програма Hello World для DOS
; Демонструє базову структуру програми та введення/виведення DOS .model small ; Використовувати малу модель пам'яті (код + дані &lt; 128KB)
.stack 100h ; Зарезервувати 256 байт для стека .data ; Початок сегмента даних msg db 'Hello, World!$' ; Рядок, що закінчується символом $ для DOS .code ; Початок сегмента коду
main proc ; Ініціалізація сегмента даних mov ax, @data ; Отримати адресу сегмента даних mov ds, ax ; Завантажити її в регістр DS ; Вивести повідомлення mov ah, 9 ; Функція DOS 9: вивести рядок mov dx, offset msg ; DX = адреса повідомлення int 21h ; Виклик переривання DOS ; Коректне завершення програми mov ax, 4C00h ; Функція DOS 4Ch: завершити програму ; AL = 00h: код повернення 0 (успіх) int 21h ; Виклик переривання DOS
main endp end main ; Точка входу — 'main'
Цей приклад ілюструє кілька важливих концепцій:Директиви: Рядки, що починаються з крапки (.model, .stack, .data, .code), є директивами асемблера, а не інструкціями ЦП. Вони вказують асемблеру, як організувати програму, але безпосередньо не генерують машинний код.Модель пам'яті: Директива .model small вказує асемблеру, що весь код поміщається в один 64KB сегмент, і всі дані поміщаються в інший. Це впливає на те, як асемблер обчислює адреси.Сегменти: Директиви .data та .code розділяють оголошення змінних від виконуваних інструкцій. Ця організація відображає те, як ЦП використовує різні сегментні регістри (DS для даних, CS для коду).Визначення даних: Директива db (define byte — визначити байт) резервує пам'ять та ініціалізує її байтами рядка. Символ $ завершує рядок для функції DOS 9.Структура процедури: proc та endp позначають початок і кінець процедури. Хоча вони не є строго обов'язковими для простих програм, вони допомагають організувати код та дозволяють асемблеру керувати локальними мітками.Точка входу: Директива end main вказує компонувальнику, де має починатися виконання програми.Типова програма на асемблері для DOS має таку структуру:.model small ; Оголошення моделі пам'яті
.stack 100h ; Розмір сегмента стека .data ; Початок сегмента даних ; Оголошення змінних тут buffer db 256 dup(?) ; 256 неініціалізованих байт count dw 0 ; Слово, ініціалізоване 0 table db 1,2,3,4,5 ; Масив байтів .code ; Початок сегмента коду
main proc ; Ініціалізація програми mov ax, @data mov ds, ax ; Основна логіка програми тут ; Завершення програми mov ax, 4C00h int 21h
main endp ; Додаткові процедури можна визначити тут
helper proc ; Код процедури ret
helper endp end main ; Вказати точку входу
Мова асемблера надає кілька директив для визначення даних:Приклади:byte_var db 42 ; Один байт, значення 42
word_var dw 1000 ; Слово (2 байти), значення 1000
dword_var dd 100000 ; Подвійне слово (4 байти)
string_var db 'Hello',0 ; Рядок з нульовим термінатором
array db 10 dup(0) ; 10 байт, усі ініціалізовані 0
buffer db 256 dup(?) ; 256 неініціалізованих байт
Оператор dup створює кілька копій: 10 dup(0) означає "10 копій 0", тоді як dup(?) вказує на неініціалізоване сховище.Розуміння структури мови асемблера має кілька практичних застосувань: Читання дизасембльованого коду: Налагоджувачі та інструменти реверс-інжинірингу відображають машинний код як асемблер. Розпізнавання форматів інструкцій допомагає зрозуміти, що робить програма. Написання ефективного коду: Навіть при програмуванні мовами високого рівня розуміння асемблера допомагає писати код, який компілюється ефективно. Налагодження: Коли програма аварійно завершується, налагоджувач показує інструкції асемблера. Розуміння структури допомагає знайти проблему. Системне програмування: Ядра операційних систем, драйвери пристроїв та завантажувачі часто потребують асемблера для апаратно-специфічних операцій. Ми бачили, що мова асемблера надає людино-читабельне представлення машинного коду з однозначною відповідністю між інструкціями асемблера та їх двійковими кодуваннями. Тепер розглянемо, як це кодування насправді працює: як інструкції стають байтами і як ці байти організовані та інтерпретуються процесором.Кожна інструкція, яку виконує процесор, представлена як послідовність байтів у пам'яті. Перший байт (або байти) інструкції — це код операції (opcode), який визначає, яку операцію виконати. Залежно від інструкції, можуть слідувати додаткові байти для вказання операндів, адрес чи інших параметрів.Коли процесор вибирає інструкцію, він читає код операції, визначає тип і довжину інструкції, вибирає будь-які необхідні додаткові байти, а потім виконує операцію. Цей процес повторюється мільярди разів на секунду в сучасних процесорах.На відміну від деяких архітектур (таких як MIPS або ARM), де кожна інструкція має рівно 4 байти, x86 використовує інструкції змінної довжини від 1 до 15 байт. Ця конструкція відображає еволюцію архітектури від 8-бітних процесорів 8080 та 16-бітних 8086, де пам'ять була дорогоцінною і компактні кодування були необхідними.Конструкція зі змінною довжиною має як переваги, так і недоліки:Переваги:
Поширені інструкції можуть бути дуже короткими (1 байт для NOP, RET, одиночного INC/DEC для регістра)
Пам'ять використовується ефективно, оскільки прості операції не витрачають байти
Зворотна сумісність підтримується при додаванні нових інструкцій
Недоліки:
Декодування інструкцій складніше, оскільки процесор має визначати довжину інструкції
Паралельне декодування складніше, оскільки межі інструкцій не вирівняні
Сучасні процесори витрачають значну кількість кремнію на визначення довжини інструкцій
Найпростіші інструкції x86 кодуються повністю в одному байті. Це зазвичай операції, які або не мають операндів, або мають неявний операнд, закодований у самому коді операції:Зверніть увагу, як споріднені інструкції часто мають сусідні коди операцій: PUSHF (9C) і POPF (9D), SAHF (9E) і LAHF (9F). Ця організація не випадкова. Вона відображає апаратну конструкцію, де певні біти коду операції вибирають між спорідненими операціями.Багато однобайтових інструкцій кодують операнд-регістр безпосередньо в коді операції. 8086 використовує просту схему, де три молодші біти певних кодів операцій вказують, який регістр використовувати:Це кодування з'являється в інструкціях INC, DEC, PUSH і POP для 16-бітних регістрів:Щоб знайти код операції для PUSH BX, ми беремо базовий код операції 50h і додаємо код регістра для BX (011 двійкове = 3). Результат: 50h + 3 = 53h.Ця схема кодування елегантна, оскільки апаратне декодування може просто направляти три молодші біти до селектора регістра, тоді як п'ять старших бітів визначають операцію.Коли інструкція включає безпосереднє (константне) значення, це значення слідує за кодом операції в потоці інструкцій. Наприклад, інструкція MOV для завантаження безпосереднього значення в 8-бітний регістр використовує коди операцій B0-B7:Шаблон продовжується для 16-бітних регістрів (B8-BF), де безпосереднє значення займає два байти:Приклад кодування: Щоб закодувати MOV AL, 42 (завантажити десяткове 42 в AL):
Код операції для MOV AL, imm8 — це B0
42 десяткове = 2A шістнадцяткове
Повна інструкція: B0 2A (два байти)
Інший приклад: Щоб закодувати MOV AX, 1234h:
Код операції для MOV AX, imm16 — це B8
Безпосереднє значення 1234h зберігається в порядку little-endian: 34 12
Повна інструкція: B8 34 12 (три байти)
Складніші інструкції, які можуть оперувати різними комбінаціями регістрів або адресами пам'яті, використовують додатковий байт, що називається байт ModR/M (також записується як ModRM або Mod-Reg-R/M). Цей байт слідує за кодом операції та вказує:
Mod (біти 7-6): Режим адресації (прямий регістр, пам'ять зі зміщенням тощо)
Reg (біти 5-3): Операнд-регістр або розширення коду операції
R/M (біти 2-0): Регістр або операнд пам'яті
Наприклад, інструкція ADD AX, BX кодується як 01 D8:
Код операції 01: ADD r/m16, r16 (додати регістр до регістра/пам'яті)
ModR/M D8 = 11 011 000 двійкове Mod = 11: Регістр-до-регістра (без доступу до пам'яті)
Reg = 011: BX є джерелом
R/M = 000: AX є приймачем Байт ModR/M дозволяє одному коду операції представляти багато різних комбінацій операндів, зберігаючи набір інструкцій компактним, водночас підтримуючи гнучкість.Повний простір кодів операцій x86 можна візуалізувати як таблицю 16x16, де рядок відповідає старшому напівбайту (біти 7-4) коду операції, а стовпець відповідає молодшому напівбайту (біти 3-0): 0 1 2 3 4 5 6 7 8 9 A B C D E F +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ 0 |ADD|ADD|ADD|ADD|ADD|ADD|PSH|POP|OR |OR |OR |OR |OR |OR |PSH|-- | 1 |ADC|ADC|ADC|ADC|ADC|ADC|PSH|POP|SBB|SBB|SBB|SBB|SBB|SBB|PSH|POP| 2 |AND|AND|AND|AND|AND|AND|ES:|DAA|SUB|SUB|SUB|SUB|SUB|SUB|CS:|DAS| 3 |XOR|XOR|XOR|XOR|XOR|XOR|SS:|AAA|CMP|CMP|CMP|CMP|CMP|CMP|DS:|AAS| 4 |INC|INC|INC|INC|INC|INC|INC|INC|DEC|DEC|DEC|DEC|DEC|DEC|DEC|DEC| 5 |PSH|PSH|PSH|PSH|PSH|PSH|PSH|PSH|POP|POP|POP|POP|POP|POP|POP|POP| 6 | (Reserved / 32-bit extensions) | 7 |Jcc|Jcc|Jcc|Jcc|Jcc|Jcc|Jcc|Jcc|Jcc|Jcc|Jcc|Jcc|Jcc|Jcc|Jcc|Jcc| 8 |Imm|Imm|Imm|Imm|TST|TST|XCH|XCH|MOV|MOV|MOV|MOV|MOV|LEA|MOV|POP| 9 |NOP|XCH|XCH|XCH|XCH|XCH|XCH|XCH|CBW|CWD|CAL|WAI|PSF|POF|SAH|LAH| A |MOV|MOV|MOV|MOV|MVS|CMS|MVS|CMS|TST|TST|STO|STO|LOD|LOD|SCA|SCA| B |MOV|MOV|MOV|MOV|MOV|MOV|MOV|MOV|MOV|MOV|MOV|MOV|MOV|MOV|MOV|MOV| C |Shf|Shf|RET|RET|LES|LDS|MOV|MOV|ENT|LEV|RTF|RTF|INT|INT|INT|IRT| D |Shf|Shf|Shf|Shf|AAM|AAD|-- |XLT|ESC|ESC|ESC|ESC|ESC|ESC|ESC|ESC| E |LOP|LOP|LOP|LOP|IN |IN |OUT|OUT|CAL|JMP|JMP|JMP|IN |IN |OUT|OUT| F |LCK|-- |REP|REP|HLT|CMC|Grp|Grp|CLC|STC|CLI|STI|CLD|STD|Grp|Grp| +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
Ця таблиця (спрощена для ілюстрації) показує, як організовані коди операцій. Рядок 4 (40-4F) містить INC та DEC для регістрів. Рядок 5 (50-5F) містить PUSH та POP. Рядок B (B0-BF) містить MOV з безпосереднім значенням у регістр. Рядок 7x містить умовні переходи.Розуміння цієї організації допомагає при перегляді дампів пам'яті або налагодженні, оскільки шаблони стають впізнаваними з практикою.<br><img alt="Comprehensive x86 Opcode Structure and Instruction Overview" src="assets/l04-x86-opcode-table.png" target="_self">Структура кодів операцій x86 та оглядова таблиця інструкцій від Fraunhofer FKIE. Ця кольорово-кодована довідка показує всі коди операцій x86, організовані за напівбайтами, з різними кольорами, що вказують на категорії інструкцій: арифметико-логічні (зелений), операції з пам'яттю (жовтий), стекові операції (помаранчевий), керування потоком виконання (рожевий) та системні/введення-виведення (фіолетовий). Ця таблиця є безцінним довідником для розуміння машинного коду.Розуміння кодування машинного коду має кілька практичних застосувань:1. Налагодження з шістнадцятковими дампамиПри перегляді аварійного дампу або вмісту пам'яті розпізнавання шаблонів кодів операцій допомагає відрізнити код від даних:B8 00 4C CD 21 ; MOV AX, 4C00h / INT 21h (вихід DOS)
2. Обчислення відстаней переходівІнструкції переходу часто використовують відносні зміщення. Знання розмірів інструкцій допомагає обчислити, куди потрапляє перехід:EB 05 ; JMP SHORT +5 (пропустити наступні 5 байт)
90 ; NOP (1 байт)
90 ; NOP (1 байт)
90 ; NOP (1 байт)
90 ; NOP (1 байт)
90 ; NOP (1 байт) — перехід потрапляє після цього
3. Бінарне патченняДослідники безпеки та розробники іноді повинні модифікувати виконувані файли. Розуміння кодів операцій дозволяє точні виправлення:
Змінити JNZ (75) на JZ (74), щоб інвертувати умову
Замінити інструкції на NOP (90), щоб вимкнути їх
Модифікувати безпосередні значення, щоб змінити поведінку програми
4. Розуміння виводу компілятораПри оптимізації коду перегляд згенерованого асемблера показує, що створив компілятор. Знання кодувань інструкцій допомагає зрозуміти розмір коду та потенційні оптимізації.Кодування інструкцій x86 ставало дедалі складнішим протягом десятиліть:
8086 (1978): Базові однобайтові коди операцій з ModR/M для складної адресації
80386 (1985): Додано префікс 0F для двобайтових кодів операцій (нові інструкції)
Pentium Pro (1995): Додано інструкції умовного переміщення
x86-64 (2003): Додано префікс REX для 64-бітних операцій та додаткових регістрів
AVX (2011): Додано префікс VEX для SIMD-розширень
AVX-512 (2016): Додано префікс EVEX для ще більшої кількості регістрів та можливостей
Кожне розширення додавало префіксні байти, які модифікують інтерпретацію наступних байтів, створюючи багаторівневу схему кодування, яка підтримує зворотну сумісність, водночас уможливлюючи нові можливості.Ця історична еволюція пояснює, чому кодування x86 здається непослідовним: воно зберігає сумісність з кодом, написаним для процесорів чотири десятиліття тому, водночас підтримуючи сучасні можливості. Програма, скомпільована для оригінального 8086, все ще працює на найновішому процесорі Intel або AMD. Сегментні регістри організовують пам'ять 8086 в логічні області: DS для даних, CS для коду, SS для стека, ES для додаткових даних. Фізичні адреси обчислюються як сегмент × 16 + зміщення. MOV — фундаментальна інструкція передачі даних з п'ятьма комбінаціями операндів. Вона не може безпосередньо змінювати CS або IP. Регістр FLAGS відображає стан останньої операції АЛП. Прапорці умов (CF, ZF, SF, OF) забезпечують умовне виконання; прапорці керування (IF, DF) керують поведінкою системи. Умовні переходи перевіряють конкретні значення прапорців для прийняття рішень про розгалуження. Використовуйте знакові порівняння (JG, JL) для знакових значень і беззнакові порівняння (JA, JB) для беззнакових значень. Інструкції АЛП організовані в три групи: Група 1 (ADD, SUB, CMP, AND, OR, XOR, TEST) підтримує всі п'ять типів операндів; Група 2 (MUL, DIV) використовує неявні регістри AX/DX; Група 3 (INC, DEC, NOT, NEG) працює з одним операндом. Доповняльний код дозволяє одному апаратному забезпеченню виконувати як знакову, так і беззнакову арифметику. CF вказує на беззнакове переповнення; OF вказує на знакове переповнення. Асемблер надає читабельні мнемоніки, що відповідають один до одного кодам операцій машинного коду. Розуміння кодування опкодів допомагає при відлагодженні та зворотному інженерінгу. Що ви практикуватимете:
Поведінка регістра FLAGS з різними арифметичними операціями
Умовні переходи (JE, JNE, JG, JL, JA, JB тощо) на основі станів прапорців
Цілочисельна арифметика, включаючи множення та ділення
Реалізація конструкцій if-else та циклів на асемблері
Відлагодження та спостереження за змінами прапорців у Turbo Debugger
Рекомендована підготовка:
Повторіть схему бітів регістра FLAGS
Попрактикуйте ментальне перетворення доповняльного коду для малих чисел
Зрозумійте різницю між знаковими та беззнаковими порівняннями <br>
<a data-tooltip-position="top" aria-label="https://yassinebridi.github.io/asm-docs/asm_tutorial_01.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://yassinebridi.github.io/asm-docs/asm_tutorial_01.html" target="_self">8086 Registers Tutorial</a> — вичерпний посібник з регістрів 8086 та їх призначення <br>
<a data-tooltip-position="top" aria-label="https://yassinebridi.github.io/asm-docs/asm_tutorial_02.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://yassinebridi.github.io/asm-docs/asm_tutorial_02.html" target="_self">Memory Access Tutorial</a> — детальне висвітлення інструкції MOV та режимів адресації <br>
<a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/types-of-flags-in-8086/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/types-of-flags-in-8086/" target="_self">Types of Flags in 8086</a> — візуальне пояснення структури регістра FLAGS <br>
<a data-tooltip-position="top" aria-label="https://roboticelectronics.in/branch-instructions-in-8086/" rel="noopener nofollow" class="external-link is-unresolved" href="https://roboticelectronics.in/branch-instructions-in-8086/" target="_self">Branch Instructions in 8086</a> — довідник з усіх інструкцій умовних і безумовних переходів <br>
<a data-tooltip-position="top" aria-label="https://yassinebridi.github.io/asm-docs/asm_tutorial_06.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://yassinebridi.github.io/asm-docs/asm_tutorial_06.html" target="_self">ALU Instructions Tutorial</a> — детальний посібник з арифметичних і логічних інструкцій <br>
<a data-tooltip-position="top" aria-label="https://net.cs.uni-bonn.de/fileadmin/user_upload/plohmann/x86_opcode_structure_and_instruction_overview.pdf" rel="noopener nofollow" class="external-link is-unresolved" href="https://net.cs.uni-bonn.de/fileadmin/user_upload/plohmann/x86_opcode_structure_and_instruction_overview.pdf" target="_self">x86 Opcode Structure</a> — вичерпна таблиця опкодів x86 (PDF) <br>
<a data-tooltip-position="top" aria-label="https://compsecurityconcepts.wordpress.com/2014/09/22/8086-microprocessor-basics/" rel="noopener nofollow" class="external-link is-unresolved" href="https://compsecurityconcepts.wordpress.com/2014/09/22/8086-microprocessor-basics/" target="_self">8086 Microprocessor Basics</a> — огляд архітектури 8086 та набору інструкцій ]]></description><link>lectures/l04.-архітектура-x86-—-основні-типи-інструкцій.html</link><guid isPermaLink="false">Lectures/L04. Архітектура x86 — основні типи інструкцій.md</guid><pubDate>Tue, 03 Feb 2026 18:36:24 GMT</pubDate><enclosure url="assets/l04-intel-8086-chip.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l04-intel-8086-chip.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L04-NOTES. Архітектура x86 — основні типи інструкцій]]></title><description><![CDATA[Короткий довідник до Лекції 04. Повні матеріали див. у файлі L04. Архітектура x86 — основні типи інструкцій.md.Регістри загального призначення (16-бітні, поділяються на 8-бітні частини):Індексні/вказівникові регістри:
SI (індекс джерела), DI (індекс приймача) — рядкові операції
SP (вказівник стека), BP (базовий вказівник) — доступ до стека
IP (вказівник інструкцій) — адреса наступної інструкції
Сегментні регістри (16-бітні):Фізична адреса = Сегмент × 16 + Зміщення (20-бітна, адресується до 1 МБ)Дізнатися більше: <a data-tooltip-position="top" aria-label="https://yassinebridi.github.io/asm-docs/asm_tutorial_01.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://yassinebridi.github.io/asm-docs/asm_tutorial_01.html" target="_self">8086 Registers Tutorial</a>П'ять комбінацій операндів:Обмеження:
Немає MEM → MEM (використовуйте регістр як проміжний)
Немає IMM → сегментний регістр (спочатку завантажте у регістр загального призначення)
Не можна напряму змінювати CS або IP
<br>Дізнатися більше: <a data-tooltip-position="top" aria-label="https://yassinebridi.github.io/asm-docs/asm_tutorial_02.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://yassinebridi.github.io/asm-docs/asm_tutorial_02.html" target="_self">Memory Access Tutorial</a>Прапорці керування: IF (переривань), DF (напрямку), TF (трасування)Ключова відмінність: CF = беззнакове переповнення, OF = знакове переповнення<br>Дізнатися більше: <a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/types-of-flags-in-8086/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/types-of-flags-in-8086/" target="_self">Types of Flags in 8086</a>Беззнакові порівняння (після CMP):Знакові порівняння (після CMP):Переходи за прапорцями:<br>Дізнатися більше: <a data-tooltip-position="top" aria-label="https://roboticelectronics.in/branch-instructions-in-8086/" rel="noopener nofollow" class="external-link is-unresolved" href="https://roboticelectronics.in/branch-instructions-in-8086/" target="_self">Branch Instructions in 8086</a>Двоопераднді інструкції (ті самі правила операндів, що й MOV, за винятком IMM як приймача):Ідіома обнулення XOR: xor ax, ax — найшвидший спосіб обнулити регістр<br>Дізнатися більше: <a data-tooltip-position="top" aria-label="https://yassinebridi.github.io/asm-docs/asm_tutorial_06.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://yassinebridi.github.io/asm-docs/asm_tutorial_06.html" target="_self">ALU Instructions Tutorial</a>Неявне використання регістрів:Критично: Перед діленням слова очистіть DX (беззнакове) або використовуйте CWD (знакове)Небезпеки:
Ділення на нуль → виняток процесора
Переповнення частки → виняток процесора (так само як ділення на нуль)
DIV/IDIV залишають прапорці невизначеними
INC/DEC зберігають CF — корисно в циклах з багатобайтною арифметикоюОсобливий випадок NEG: NEG найменшого від'ємного значення (-128 для байта) встановлює OF=1Діапазон для n бітів: -2^(n-1) до 2^(n-1) - 1Перетворення: Інвертувати всі біти, додати 1
+5 = 00000101 → NOT → 11111010 → +1 → 11111011 = -5
Чому це працює: Та сама схема додавання працює для знакових і беззнакових чиселРозширення знаку: Використовуйте CBW (байт→слово) або CWD (слово→подвійне слово) для збереження знакуФормат інструкції:[мітка:] мнемоніка [операнди] [; коментар]
Директиви визначення даних:Шаблон програми:.model small
.stack 100h
.data var db 0
.code
main proc mov ax, @data mov ds, ax ; ... код ... mov ax, 4C00h int 21h
main endp
end main
x86 використовує інструкції змінної довжини (1-15 байтів)Кодування регістрів (біти 2-1-0):Поширені однобайтові коди операцій:
40-47: INC reg16
48-4F: DEC reg16
50-57: PUSH reg16
58-5F: POP reg16
90: NOP
C3: RET
<br>Дізнатися більше: <a data-tooltip-position="top" aria-label="https://net.cs.uni-bonn.de/fileadmin/user_upload/plohmann/x86_opcode_structure_and_instruction_overview.pdf" rel="noopener nofollow" class="external-link is-unresolved" href="https://net.cs.uni-bonn.de/fileadmin/user_upload/plohmann/x86_opcode_structure_and_instruction_overview.pdf" target="_self">x86 Opcode Structure (PDF)</a>
Сегментні регістри організовують пам'ять: DS (даних), CS (коду), SS (стека), ES (додатковий)
MOV має 5 комбінацій операндів; немає MEM→MEM або прямої зміни CS/IP
FLAGS відображає останню операцію АЛП; CF для беззнакового, OF для знакового переповнення
Умовні переходи: Використовуйте JA/JB для беззнакових, JG/JL для знакових порівнянь
Групи АЛП: Група 1 (двооперандні), Група 2 (неявні регістри), Група 3 (унарні)
Доповняльний код дозволяє використовувати те саме обладнання для знакової/беззнакової арифметики
Асемблер має відповідність 1:1 з кодами операцій машинного коду
Що ви будете практикувати:
Спостереження за змінами FLAGS при арифметичних операціях у Turbo Debugger
Реалізація умовних переходів (if-else, цикли)
Множення та ділення цілих чисел з неявними регістрами
Арифметика в доповняльному коді
Перед лабораторною:
Знайте позиції бітів у регістрі FLAGS
Розумійте різницю між знаковими та беззнаковими порівняннями
Попрактикуйте перетворення в доповняльний код для малих чисел
]]></description><link>lectures/l04-notes.-архітектура-x86-—-основні-типи-інструкцій.html</link><guid isPermaLink="false">Lectures/L04-NOTES. Архітектура x86 — основні типи інструкцій.md</guid><pubDate>Tue, 03 Feb 2026 18:34:34 GMT</pubDate></item><item><title><![CDATA[L04-NOTES-ENG. x86 Architecture - Basic Instruction Types]]></title><description><![CDATA[Quick reference for Lecture 04. See full materials in L04-ENG. x86 Architecture - Basic Instruction Types.md.General-Purpose Registers (16-bit, split into 8-bit halves):Index/Pointer Registers:
SI (Source Index), DI (Destination Index) — string operations
SP (Stack Pointer), BP (Base Pointer) — stack access
IP (Instruction Pointer) — next instruction address
Segment Registers (16-bit):Physical address = Segment × 16 + Offset (20-bit, 1MB addressable)Learn more: <a data-tooltip-position="top" aria-label="https://yassinebridi.github.io/asm-docs/asm_tutorial_01.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://yassinebridi.github.io/asm-docs/asm_tutorial_01.html" target="_self">8086 Registers Tutorial</a>Five operand combinations:Restrictions:
No MEM → MEM (use register as intermediate)
No IMM → segment register (load to general register first)
Cannot modify CS or IP directly
<br>Learn more: <a data-tooltip-position="top" aria-label="https://yassinebridi.github.io/asm-docs/asm_tutorial_02.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://yassinebridi.github.io/asm-docs/asm_tutorial_02.html" target="_self">Memory Access Tutorial</a>Control Flags: IF (Interrupt), DF (Direction), TF (Trap)Key distinction: CF = unsigned overflow, OF = signed overflow<br>Learn more: <a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/types-of-flags-in-8086/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/types-of-flags-in-8086/" target="_self">Types of Flags in 8086</a>Unsigned comparisons (after CMP):Signed comparisons (after CMP):Flag-based jumps:<br>Learn more: <a data-tooltip-position="top" aria-label="https://roboticelectronics.in/branch-instructions-in-8086/" rel="noopener nofollow" class="external-link is-unresolved" href="https://roboticelectronics.in/branch-instructions-in-8086/" target="_self">Branch Instructions in 8086</a>Two-operand instructions (same operand rules as MOV, except no IMM destination):XOR zeroing idiom: xor ax, ax — fastest way to zero a register<br>Learn more: <a data-tooltip-position="top" aria-label="https://yassinebridi.github.io/asm-docs/asm_tutorial_06.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://yassinebridi.github.io/asm-docs/asm_tutorial_06.html" target="_self">ALU Instructions Tutorial</a>Implicit register usage:Critical: Before word division, clear DX (unsigned) or use CWD (signed)Hazards:
Division by zero → CPU exception
Quotient overflow → CPU exception (same as div by zero)
DIV/IDIV leave flags undefined
INC/DEC preserve CF — useful in loops with multi-precision arithmeticNEG special case: NEG of most negative value (-128 for byte) sets OF=1Range for n bits: -2^(n-1) to 2^(n-1) - 1Conversion: Invert all bits, add 1
+5 = 00000101 → NOT → 11111010 → +1 → 11111011 = -5
Why it works: Same addition circuit handles signed and unsignedSign extension: Use CBW (byte→word) or CWD (word→dword) to preserve signInstruction format:[label:] mnemonic [operands] [; comment]
Data definition directives:Program template:.model small
.stack 100h
.data var db 0
.code
main proc mov ax, @data mov ds, ax ; ... code ... mov ax, 4C00h int 21h
main endp
end main
x86 uses variable-length instructions (1-15 bytes)Register encoding (bits 2-1-0):Common single-byte opcodes:
40-47: INC reg16
48-4F: DEC reg16
50-57: PUSH reg16
58-5F: POP reg16
90: NOP
C3: RET
<br>Learn more: <a data-tooltip-position="top" aria-label="https://net.cs.uni-bonn.de/fileadmin/user_upload/plohmann/x86_opcode_structure_and_instruction_overview.pdf" rel="noopener nofollow" class="external-link is-unresolved" href="https://net.cs.uni-bonn.de/fileadmin/user_upload/plohmann/x86_opcode_structure_and_instruction_overview.pdf" target="_self">x86 Opcode Structure (PDF)</a>
Segment registers organize memory: DS (data), CS (code), SS (stack), ES (extra)
MOV has 5 operand combinations; no MEM→MEM or direct CS/IP modification
FLAGS reflect last ALU operation; CF for unsigned, OF for signed overflow
Conditional jumps: Use JA/JB for unsigned, JG/JL for signed comparisons
ALU Groups: Group 1 (two-operand), Group 2 (implicit regs), Group 3 (unary)
Two's complement enables same hardware for signed/unsigned arithmetic
Assembly has 1:1 correspondence with machine code opcodes
What you'll practice:
Observing FLAGS changes with arithmetic operations in Turbo Debugger
Implementing conditional branching (if-else, loops)
Integer multiplication and division with implicit registers
Two's complement arithmetic
Before lab:
Know the FLAGS register bit positions
Understand signed vs unsigned comparisons
Practice two's complement conversions for small numbers
]]></description><link>lectures/eng/l04-notes-eng.-x86-architecture-basic-instruction-types.html</link><guid isPermaLink="false">Lectures/ENG/L04-NOTES-ENG. x86 Architecture - Basic Instruction Types.md</guid><pubDate>Tue, 03 Feb 2026 18:31:55 GMT</pubDate></item><item><title><![CDATA[L04-ENG. x86 Architecture - Basic Instruction Types]]></title><description><![CDATA[Course: Computer Systems Architecture
Topic: ALU Instructions, Conditional Jumps, Flags, Two's Complement
Learning Outcomes: LO2 (Data representation), LO3 (Assembly programming)After this lecture you will be able to:
Explain the role of special-purpose registers (DS, CS:IP, SS:SP, BP, ES) in the 8086 segmented memory model
Use the MOV instruction with all five operand type combinations
Describe the FLAGS register and explain when each condition flag is set
Write assembly code using conditional jumps to implement program flow control
Apply ALU instructions (ADD, SUB, MUL, DIV, AND, OR, XOR, INC, DEC, NEG) in assembly programs
Understand two's complement representation and distinguish signed from unsigned overflow
Read and interpret basic x86 machine code encodings
In the previous lecture, we explored the 8086's register architecture and memory model. Now we turn our attention to what makes a processor truly useful: its ability to execute instructions that manipulate data, make decisions, and control program flow. Before diving into these instruction types, let us briefly revisit why the Intel 8086 holds such a pivotal position in computing history.When Intel released the 8086 microprocessor on June 8, 1978, they created what would become the foundation of the x86 architecture. This 16-bit processor, implemented with approximately 20,000 transistors, established design patterns and instruction formats that persist in every modern x86-64 processor manufactured today. The 8086 was a 16-bit architecture with the ability to address 1 MB of memory through its innovative segment:offset addressing scheme.<img alt="Intel 8086 microprocessor in 40-pin ceramic DIP package" src="assets/l04-intel-8086-chip.jpeg" target="_self">The Intel 8086 processor (1978) in its characteristic 40-pin Dual In-line Package (DIP). The markings show "C8086" and "© INTEL '78". This chip established the x86 architecture that powers virtually all desktop and server computers today.The decision to maintain backward compatibility with earlier Intel processors (the 8008, 8080, and 8085) proved transformative for the industry. Programmers could migrate their existing code to the new platform, and this philosophy of backward compatibility has defined x86 development ever since. Every Intel Core and AMD Ryzen processor still supports the instruction set established in 1978.Studying the 8086 provides several distinct advantages for learning computer architecture:Transparency. The 8086's architecture is simple enough to understand completely, yet sophisticated enough to illustrate all fundamental concepts. Modern processors execute hundreds of instructions simultaneously using complex out-of-order execution, branch prediction, and speculative execution. The 8086, by contrast, executes one instruction at a time in a straightforward fetch-decode-execute cycle. This simplicity makes the relationship between assembly code and hardware behavior visible and comprehensible.Same Instruction Set Base. The instructions we learn on the 8086 (ADD, SUB, MOV, JMP, PUSH, POP) have direct equivalents in modern x86-64. The syntax is nearly identical, and the concepts transfer directly. Learning 8086 assembly is not an archaeological exercise; it is learning the foundation that modern assembly builds upon.Turbo Debugger. Our practical work uses Turbo Debugger (TD), a powerful debugging environment that provides real-time visualization of registers, memory, the stack, and flags. In this environment, you can watch exactly how each instruction affects the processor state. This immediate feedback accelerates learning and builds intuition about processor behavior that is difficult to develop through reading alone.Direct Hardware Access. In the DOSBox environment with the 8086, we have unmediated access to the machine. There is no operating system kernel hiding the hardware, no virtual memory abstracting physical addresses, no protection rings preventing direct manipulation. We can examine and modify any memory location, interact directly with hardware, and observe the complete state of the system. This direct access, impossible in modern protected environments, provides invaluable insight into how computers actually work.In this lecture, we explore the fundamental instruction types that transform a processor from a passive circuit into an active computing machine:
The FLAGS register and how arithmetic operations affect its status bits
ALU instructions for arithmetic: ADD, SUB, INC, DEC, NEG
Multiplication and division with MUL, IMUL, DIV, IDIV
Two's complement representation for signed integers
Jump instructions for controlling program flow
Bitwise operations for manipulating individual bits
Bit shift instructions for efficient multiplication and bit extraction
By the end of this lecture, you will understand how the processor performs calculations, how it tracks the results of those calculations through flags, and how it makes decisions based on those results to control program execution.In Lecture 3, we examined the 8086's general-purpose registers in detail. Let us briefly recall their structure before exploring the special-purpose registers that complete the picture.The 8086 provides four primary 16-bit general-purpose registers, each divisible into two 8-bit halves:Additionally, the 8086 provides four index and pointer registers that cannot be split into 8-bit components:These general-purpose registers hold the data that programs manipulate. But a processor needs more than data storage. It needs to know where to find instructions, where to find data, where the stack lives, and what happened during the last operation. This is where special-purpose registers come into play.The 8086 uses segment registers to extend its 16-bit register architecture to address a full 1 MB of memory. As we learned in Lecture 3, the physical address formula is:Physical Address = (Segment x 16) + OffsetThe 8086 provides four segment registers, each serving a distinct purpose:These registers are not general-purpose. You cannot perform arithmetic on them or use them as temporary storage. They exist solely to provide the segment portion of memory addresses, and the processor automatically selects which segment register to use based on the type of memory access.The Data Segment register (DS) points to the segment where your program's variables reside. When you access memory for data operations, the processor automatically uses DS as the default segment.Consider reading a variable from memory:mov ax, [bx] ; Read word from DS:BX into AX
This instruction reads a 16-bit value from memory. The processor automatically calculates the physical address as (DS x 16) + BX. You do not need to specify DS explicitly; it is the default segment for data access operations involving BX, SI, or DI.Initializing DS. At program startup, DS does not automatically point to your data segment. You must initialize it explicitly. The standard pattern is:mov ax, SEG @data ; Get segment address of data segment
mov ds, ax ; Initialize DS register
The assembler directive SEG @data returns the segment address where the assembler placed your declared variables. We cannot move an immediate value directly into a segment register (this is a hardware limitation), so we must use a general-purpose register as an intermediary.Why this limitation? Segment registers connect directly to the address generation circuitry. Loading them triggers immediate recalculation of address boundaries. The hardware designers restricted segment register loading to prevent certain timing hazards and simplify the instruction decoder.The CS (Code Segment) and IP (Instruction Pointer) registers work as an inseparable pair to determine where the processor fetches its next instruction:Physical Address of Next Instruction = (CS x 16) + IPUnlike other segment:offset pairs, you cannot directly modify CS and IP through simple MOV instructions. This restriction exists for good reason: arbitrarily changing where the processor fetches instructions would be dangerous and could lead to unpredictable behavior.IP changes automatically as the processor executes instructions. After fetching an instruction, IP advances by the number of bytes in that instruction. A simple one-byte instruction advances IP by 1; a complex multi-byte instruction might advance IP by 5 or 6.CS and IP change together only through control flow instructions:
Near jumps modify only IP, staying within the current code segment
Far jumps modify both CS and IP, allowing jumps anywhere in the 1 MB address space
CALL pushes the return address and then modifies CS:IP
RET pops the return address back into CS:IP
When you see CS:IP displayed in Turbo Debugger as something like 0F78:0100, you can calculate that the next instruction will be fetched from physical address (0F78h x 16) + 0100h = 0F780h + 0100h = 0F880h.The SS (Stack Segment) and SP (Stack Pointer) registers manage the runtime stack:Physical Address of Stack Top = (SS x 16) + SPThe stack is a critical data structure used for:
Storing return addresses during function calls
Passing parameters to procedures
Allocating local variables within procedures
Saving register values temporarily
Stack Growth Direction. The x86 stack grows downward, toward lower memory addresses. This convention dates to early computer designs and persists today:
PUSH decrements SP first, then stores the value
POP retrieves the value first, then increments SP
When you execute PUSH AX, the processor decrements SP by 2 (because AX is 16 bits = 2 bytes), then writes AX to the memory location SS:SP. When you execute POP BX, the processor reads from SS:SP into BX, then increments SP by 2.Stack Initialization. The stack segment and pointer must be properly initialized before any stack operations:.STACK 100h ; Declare 256-byte stack in TASM
This directive tells the assembler to reserve space for the stack and set up SS:SP appropriately when the program loads.The Base Pointer (BP) provides a stable reference point within the stack, particularly useful during procedure execution. While SP constantly changes as values are pushed and popped, BP typically remains fixed throughout a procedure's execution.The standard calling convention uses BP to create a "stack frame" (it will be covered on a later lecture).The Extra Segment (ES) provides an additional data segment that the programmer can use for any purpose. Its most common use is with string operations that copy data between memory regions:; Copy string from DS:SI to ES:DI
mov si, OFFSET source_string
mov di, OFFSET dest_buffer
mov cx, string_length
rep movsb ; Move CX bytes from DS:SI to ES:DI
The MOVSB instruction reads from DS:SI and writes to ES:DI, then automatically increments (or decrements, depending on the Direction Flag) both SI and DI. This allows efficient memory-to-memory transfers without using general-purpose registers as intermediaries.ES must be initialized like DS:mov ax, SEG @data ; Or any other segment address
mov es, ax ; Initialize ES
Let us work through several examples to solidify understanding of segment:offset addressing.Example 1: If DS = 1234h and BX = 5678h, what physical address does mov ax, [bx] access?Physical Address = (DS x 16) + BX = (1234h x 16) + 5678h = 12340h + 5678h = 179B8h
Example 2: If SS = 2000h and SP = FFFEh, where is the top of the stack?Stack Top = (SS x 16) + SP = (2000h x 16) + FFFEh = 20000h + FFFEh = 2FFFEh
Example 3: Complete program segment initialization:.MODEL SMALL
.STACK 100h .DATA message DB 'Hello, World!', 0 .CODE
main PROC ; Initialize data segment mov ax, @data ; Get data segment address mov ds, ax ; DS now points to our data ; ES often initialized to same segment for simplicity mov es, ax ; ES = DS (optional, depends on needs) ; Now we can access 'message' using DS-relative addressing mov si, OFFSET message ; SI = offset of message within DS ; Program code here... ; Exit to DOS mov ax, 4C00h int 21h
main ENDP
END main
This code demonstrates the essential initialization sequence. After mov ds, ax, any reference to data declared in the .DATA segment will correctly resolve to the appropriate physical address.The 8086's register set divides into clear functional categories:Understanding these registers and their relationships prepares us for exploring how instructions actually manipulate data and control program flow. The FLAGS register, which we will examine in detail in the next section, provides the mechanism by which the processor communicates the results of operations back to the program, enabling conditional execution and intelligent decision-making.The most fundamental operation in any computer program is moving data from one location to another. Before you can add two numbers, you must load them into registers. Before you can store a result, you must transfer it to memory. The instruction that performs all these transfers in x86 assembly is MOV.The MOV instruction copies the value of its second operand (the source) to its first operand (the destination):MOV destination, source
After execution, the destination contains a copy of the source value. The source remains unchanged.The MOV instruction supports five fundamental combinations of operand types:Notably absent is MOV memory, memory. You cannot directly copy from one memory location to another; you must use a register as an intermediary. This limitation exists because the instruction encoding has space for at most one memory address.The REG operand can be any general-purpose register:
16-bit registers: AX, BX, CX, DX, SI, DI, BP, SP
8-bit register halves: AH, AL, BH, BL, CH, CL, DH, DL
Both operands must be the same size:MOV AX, BL ; ERROR: AX is 16-bit, BL is 8-bit
MOV AL, BL ; OK: both are 8-bit
MOV AX, BX ; OK: both are 16-bit
The memory operand uses bracket notation to indicate a memory address:MOV AX, BX ; Copy the value IN BX to AX
MOV AX, [BX] ; Copy the value AT ADDRESS BX to AX
The 8086 supports memory addressing using combinations of BX, BP, SI, and DI:; Single register addressing
MOV AX, [BX] ; address from BX (uses DS segment)
MOV AX, [BP] ; address from BP (uses SS segment) ; Base + Index addressing
MOV AX, [BX+SI] ; address = BX + SI
MOV AX, [BP+DI] ; address = BP + DI ; With displacement (constant offset)
MOV AX, [BX+SI+7] ; address = BX + SI + 7
Critical rule: BX and BP cannot appear together, and SI and DI cannot appear together. You can also use variable names directly: MOV AX, myVariable.Immediate values are constants embedded directly in the instruction:For hexadecimal numbers starting with A-F, prefix with zero: MOV AX, 0FFh.You cannot load an immediate value directly into a segment register:MOV DS, 1234h ; ERROR
MOV AX, 1234h ; first load into general register
MOV DS, AX ; then transfer to segment register
More critically, you cannot load CS with MOV at all. The CS:IP pair determines which instruction executes next, and changing CS requires special instructions (JMP, CALL, RET).Turbo Debugger's Dump view displays memory contents in hexadecimal format:DS:0100 48 65 6C 6C 6F 20 57 6F-72 6C 64 00 00 00 00 00 Hello World.....
Each row shows 16 bytes: address on the left, hex values in the middle, ASCII interpretation on the right.Every ALU operation produces not only a result but also information about that result. The processor captures this information in a 16-bit register called FLAGS. Individual bits serve as binary indicators, each called a flag because it is either set (1) or clear (0).<br><img alt="8086 FLAGS register layout showing bit positions for CF, PF, AF, ZF, SF, TF, IF, DF, and OF" src="assets/l04-flags-register-layout.jpeg" target="_self">The 16-bit FLAGS register layout showing condition flags (CF, PF, AF, ZF, SF, OF) and control flags (TF, IF, DF). Bits marked X are reserved/unused. Condition flags are set by arithmetic and logical operations; control flags affect processor behavior.Bit: 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
Flag: - - - - OF DF IF TF SF ZF - AF - PF - CF
Flags fall into two categories: condition flags (set by operations) and control flags (control processor behavior).The Carry Flag indicates unsigned overflow, when an arithmetic operation generates a carry out of the most significant bit:mov al, 255
add al, 1 ; CF=1 (unsigned overflow: 256 doesn't fit in byte) ; AL = 0 (the low 8 bits of 256)
For subtraction, CF indicates a borrow was needed:mov al, 0
sub al, 1 ; AL = 255, CF = 1 (borrow occurred)
The Zero Flag is set when an operation produces a result of zero:mov al, 5
sub al, 5 ; ZF=1 (result is zero)
ZF is essential for equality comparisons and loop control.The Sign Flag copies the most significant bit (MSB) of the result. In signed arithmetic, MSB=1 indicates a negative number:mov al, 5
sub al, 10 ; AL = -5 (FBh), SF = 1 (MSB is set)
The Overflow Flag indicates signed overflow, when the result exceeds the representable range for signed numbers (-128 to +127 for a byte):mov al, 100
add al, 50 ; OF=1 (signed overflow: 150 &gt; 127) ; AL = 150 = -106 in signed interpretation
OF and CF are independent:
CF detects unsigned overflow (result exceeds 255 for a byte)
OF detects signed overflow (result outside -128 to +127)
mov al, 200 ; 200 unsigned, -56 signed
add al, 100 ; CF=1 (300 &gt; 255), OF=0 (-56 + 100 = 44, valid signed)
The Parity Flag is set if the result has an even number of 1-bits. The Auxiliary Carry Flag indicates a carry from bit 3 to bit 4 (used for BCD arithmetic). Both are legacy features rarely used in modern programming.The Interrupt Flag controls whether the processor responds to hardware interrupts:CLI ; Clear IF (interrupts disabled)
STI ; Set IF (interrupts enabled)
The Direction Flag controls string operation direction: DF=0 processes forward (increasing addresses), DF=1 processes backward:CLD ; Clear DF (forward)
STD ; Set DF (backward)
Turbo Debugger displays flags using compact notation:NV UP EI PL NZ NA PO NC
Where: NV/OV (Overflow), UP/DN (Direction), EI/DI (Interrupt), PL/NG (Sign), NZ/ZR (Zero), NA/AC (Auxiliary), PO/PE (Parity), NC/CY (Carry).Example 1: Unsigned overflowmov al, 255
add al, 1 ; AL=0, CF=1, ZF=1, SF=0, OF=0
Result is 0 (ZF=1), unsigned overflow (CF=1), but no signed overflow (255 is -1, and -1+1=0 is valid).Example 2: Signed overflowmov al, 100
add al, 50 ; AL=150, CF=0, ZF=0, SF=1, OF=1
Unsigned result (150) fits in a byte (CF=0), but signed result exceeds +127 (OF=1). The MSB is set (SF=1).Example 3: Zero resultmov al, 5
sub al, 5 ; AL=0, CF=0, ZF=1, SF=0, OF=0
The FLAGS register enables conditional execution. The CMP instruction subtracts operands without storing the result, only setting flags:CMP AX, BX ; performs AX - BX, sets flags, discards result
JE equal_label ; jump if ZF=1 (values were equal)
JA above_label ; jump if CF=0 and ZF=0 (AX &gt; BX, unsigned)
JG greater_label ; jump if SF=OF and ZF=0 (AX &gt; BX, signed)
Understanding which flags each conditional jump tests is crucial for correct branching logic.Now that we understand flags, we can explore their primary purpose: enabling conditional execution. Flags are the foundation of all decision-making in assembly language.In high-level languages, we write conditional statements like if (x == 5). The CPU relies on a two-step process:
Perform an operation that sets flags (arithmetic, comparison, or logical)
Check flag values to decide whether to jump to a different instruction
This separation is fundamental. The comparison and the decision based on it are always separate instructions.Every conditional jump tests one or more flags and makes a decision:
If condition is TRUE: Jump to the specified label (modify IP)
If condition is FALSE: Continue to the next instruction
Jcc label ; cc = condition code (Z, NZ, C, NC, etc.)
Instructions like JZ and JE are identical opcodes - the different names help express intent. Use JZ when checking if a result is zero; use JE when checking equality after CMP.When comparing signed numbers, we must consider both the sign flag and overflow flag together:Why XOR? The XOR operation cancels out "false" sign indications when overflow occurs:MOV AL, 127 ; AL = 7Fh
CMP AL, -1 ; Compute 127 - (-1) = 128 ; SF = 1 (bit 7 set), OF = 1 (overflow!) ; SF XOR OF = 0, so 127 &gt;= -1 (correct!)
For unsigned numbers, we only consider carry flag (borrow) and zero flag:Terminology: Above/Below = unsigned comparisons. Greater/Less = signed comparisons.<br><img alt="8086 conditional jump instructions reference table" src="assets/l04-conditional-jump-table.png" target="_self">Complete reference table for 8086 conditional jump instructions, organized by category: common operations (flag-based), signed operations (for signed integer comparisons), and unsigned operations (for unsigned integer comparisons). The condition column shows the flag states that trigger each jump.; Find maximum of two signed numbers MOV AX, -50 ; First number MOV BX, 30 ; Second number CMP AX, BX ; Compare AX - BX JGE ax_is_greater ; Jump if AX &gt;= BX (signed) MOV AX, BX ; AX was smaller, copy BX to AX
ax_is_greater: ; AX now contains the larger value (30)
Understanding how jumps are encoded reveals important insights about program structure.A conditional jump consists of:
Opcode byte - identifies the jump type (JZ, JC, etc.)
Displacement byte(s) - specifies how far to jump
For short jumps, displacement is a signed byte: range -128 to +127 bytes from the instruction's end.Address Code Instruction
1000h 72 04 JC +4
1002h B0 00 MOV AL, 0
1004h EB 02 JMP +2
1006h B0 FF MOV AL, FFh
1008h ... (continues)
The JC at 1000h: opcode 72h = JC, displacement 04h = jump 4 bytes forward.Critical: Displacement is from the END of the current instruction:Target = 1002h (end of JC) + 04h = 1006h
Backward jumps use negative (two's complement) displacements:Address Code Instruction
1000h B0 00 MOV AL, 0
1002h 04 01 ADD AL, 1
1004h 3C 0A CMP AL, 10
1006h 72 FA JC -6
1008h ...
Displacement FAh = -6: Target = 1008h + (-6) = 1002h. This creates a loop.Short jumps are preferred: less memory, faster execution. Assemblers choose automatically, but you can force:JMP SHORT label ; Force short jump
JMP NEAR PTR label ; Force near jump
JMP always transfers control - no conditions tested:JMP label ; Unconditional jump
Skip over code blocks: CMP AX, 0 JZ handle_zero ; Normal case JMP done ; Skip zero handler
handle_zero: ; Zero case
done:
Infinite loops:forever: ; Loop body JMP forever
While loop:while_start: CMP CX, 0 JE while_end ; Exit if CX == 0 DEC CX JMP while_start
while_end:
Do-while loop (more efficient):do_start: DEC CX JNZ do_start ; Repeat if CX != 0 MOV AL, 255 ADD AL, 1 ; AL = 0, CF = 1, ZF = 1 JC carry_set ; Jump if carry flag is set JMP test_zero
carry_set: ; Handle carry case
test_zero: MOV AL, 5 SUB AL, 5 ; ZF = 1 (result is 0) JZ zero_set ; Jump if zero
zero_set: ; Continue
Multiple flags can be set simultaneously. Chain conditional jumps to handle complex conditions.JCXZ jumps if CX is zero, without affecting flags:JCXZ skip_loop ; Jump if CX == 0
loop_start: ; Loop body LOOP loop_start
skip_loop:
Useful for skipping loops when count is already zero.The Arithmetic Logic Unit (ALU) is the computational heart of every processor. In Lecture 2, we built an ALU from fundamental logic gates, creating circuits that could add, subtract, AND, OR, and XOR binary values. Now we examine how x86 assembly language provides access to these ALU operations through machine instructions. The 8086 organizes ALU instructions into distinct groups based on their operand patterns and implicit register usage.Group 1 instructions share a common characteristic: they accept the same operand combinations as the MOV instruction we studied earlier. This uniformity simplifies learning since you can apply the same addressing mode knowledge across multiple instruction types.Group 1 ALU instructions support five operand combinations:Notice that memory-to-memory operations are not permitted. This is a fundamental limitation of x86 architecture: at most one operand can be a memory reference. If you need to add two values from memory locations, you must first load one into a register, then add from the other memory location.The ADD instruction computes the sum of two operands, storing the result in the destination (first operand):destination = destination + source
Examples:mov ax, 10 ; AX = 10
add ax, 5 ; AX = AX + 5 = 15 mov bx, 100
add ax, bx ; AX = AX + BX = 115 mov word [data], 50
add [data], 10 ; [data] = 50 + 10 = 60
ADD performs binary addition regardless of whether you consider the values signed or unsigned. The processor does not "know" your interpretation; it simply adds the bit patterns. What differs is how you interpret the flags after the operation.For example, adding 7Fh (127 decimal, maximum positive signed byte) plus 1:mov al, 7Fh ; AL = 0111 1111 binary = 127 decimal
add al, 1 ; AL = 1000 0000 binary = 128 unsigned, -128 signed
The result 80h is correct for unsigned arithmetic (127 + 1 = 128) but represents overflow in signed arithmetic (127 + 1 should be 128, but the signed byte range is -128 to +127). The Overflow Flag (OF) indicates this signed overflow condition.SUB subtracts the source operand from the destination:destination = destination - source
Examples:mov ax, 100 ; AX = 100
sub ax, 25 ; AX = 100 - 25 = 75 mov bx, 30
sub ax, bx ; AX = 75 - 30 = 45 mov al, 5
sub al, 10 ; AL = 5 - 10 = -5 (= FBh as unsigned, 251)
Subtraction in binary is typically implemented as addition of the two's complement. When you write SUB AX, BX, the processor internally computes AX + (-BX), where -BX is the two's complement of BX. This is why the same hardware adder can perform both addition and subtraction.The Carry Flag (CF) is set if the subtraction requires a borrow (for unsigned interpretation), and the Overflow Flag (OF) is set if signed overflow occurs.CMP performs subtraction but discards the result, only updating the flags:flags = destination - source ; result not stored
CMP is essential for conditional logic. It answers questions like "is AX greater than BX?" by performing the subtraction and letting you examine the flags, without destroying either operand.Examples:mov ax, 10
mov bx, 10
cmp ax, bx ; Computes 10 - 10 = 0, sets ZF=1 (equal) ; AX and BX unchanged mov ax, 15
cmp ax, 10 ; Computes 15 - 10 = 5, sets ZF=0, SF=0 (positive) ; AX still equals 15 mov ax, 5
cmp ax, 10 ; Computes 5 - 10 = -5, sets ZF=0, SF=1 (negative)
After CMP, conditional jump instructions examine the flags to branch accordingly:cmp ax, bx
je equal_label ; Jump if Equal (ZF=1)
jg greater_label ; Jump if Greater (signed comparison)
ja above_label ; Jump if Above (unsigned comparison)
jl less_label ; Jump if Less (signed comparison)
jb below_label ; Jump if Below (unsigned comparison)
The distinction between "greater/less" (signed) and "above/below" (unsigned) reflects the two possible interpretations of the same bit patterns.Logical instructions perform bitwise operations, where each bit of the result depends only on the corresponding bits of the operands. Unlike arithmetic operations that consider the entire value with carries between bit positions, logical operations process each bit independently.AND produces a 1 bit only where both corresponding input bits are 1: 1010 1100
&amp; 1111 0000
----------- 1010 0000
Examples:mov al, 0ACh ; AL = 1010 1100 binary
and al, 0F0h ; AL = AL AND 1111 0000 = 1010 0000 = A0h
AND is commonly used for masking — extracting specific bits from a value while forcing others to zero. The "mask" operand has 1s in positions you want to preserve and 0s in positions you want to clear.Practical application — extracting the high nibble:mov al, 3Fh ; AL = 0011 1111 (contains some value)
and al, 0F0h ; Mask keeps high 4 bits: 0011 0000 = 30h
shr al, 4 ; Shift right to get: 0000 0011 = 03h
Practical application — clearing specific bits:; Clear bit 7 of AL (ensure value is positive in signed interpretation)
and al, 7Fh ; 7Fh = 0111 1111, clears bit 7, preserves bits 0-6
TEST performs AND but discards the result, only updating flags:flags = destination AND source ; result not stored
TEST is used to check whether specific bits are set without modifying the operand.Examples:; Check if bit 0 of AL is set (is the number odd?)
test al, 01h ; AND with 0000 0001
jnz is_odd ; Jump if Not Zero (bit 0 was set) ; Check if bit 7 is set (is the signed value negative?)
test al, 80h ; AND with 1000 0000
jnz is_negative ; Jump if bit 7 was set ; Check if AL is zero
test al, al ; AL AND AL = AL, but only flags matter
jz is_zero ; Jump if Zero
The TEST AL, AL idiom is the standard way to check if a register is zero. It is equivalent to CMP AL, 0 but is one byte shorter in the encoded instruction.OR produces a 1 bit where either or both corresponding input bits are 1: 1010 1100
| 0011 0011
----------- 1011 1111
Examples:mov al, 0ACh ; AL = 1010 1100
or al, 33h ; AL = 1010 1100 OR 0011 0011 = 1011 1111 = BFh
OR is commonly used to set specific bits to 1 while preserving others. The mask has 1s in positions you want to set and 0s in positions you want to preserve.Practical application — setting flags:; Set bit 7 of AL (force value negative in signed interpretation)
or al, 80h ; 80h = 1000 0000, sets bit 7, preserves others ; Combine two nibbles into a byte
mov al, 03h ; High nibble value: 0000 0011
shl al, 4 ; Shift left: 0011 0000
mov bl, 0Fh ; Low nibble value: 0000 1111
or al, bl ; Combine: 0011 1111 = 3Fh
XOR produces a 1 bit where the corresponding input bits are different: 1010 1100
^ 0011 0011
----------- 1001 1111
Examples:mov al, 0ACh ; AL = 1010 1100
xor al, 33h ; AL = 1010 1100 XOR 0011 0011 = 1001 1111 = 9Fh
XOR has unique mathematical properties that make it valuable in programming:
Self-inverse: A XOR B XOR B = A (XORing the same value twice returns the original)
Commutative: A XOR B = B XOR A
Associative: (A XOR B) XOR C = A XOR (B XOR C)
The XOR zeroing idiom:xor ax, ax ; AX = AX XOR AX = 0
This is the preferred way to zero a register in x86 assembly. Any value XORed with itself produces zero. This instruction is actually faster than MOV AX, 0 on most processors because:
The encoded instruction is shorter (2 bytes vs 3 bytes for MOV)
Modern processors recognize this pattern and optimize it specially
It breaks dependency chains (the processor knows the result is zero regardless of the previous AX value)
Practical application — toggling bits:; Toggle bit 0 (flip between odd and even)
xor al, 01h ; If bit 0 was 0, becomes 1; if 1, becomes 0 ; Simple encryption/decryption
mov al, 'A' ; AL = 41h
xor al, 55h ; "Encrypt": AL = 14h
xor al, 55h ; "Decrypt": AL = 41h = 'A' again
All Group 1 instructions modify the same set of flags in the FLAGS register:Important distinction:
CF indicates unsigned overflow (result does not fit in unsigned interpretation)
OF indicates signed overflow (result does not fit in signed interpretation)
The processor sets both flags after every arithmetic operation, letting you choose which interpretation to use for subsequent conditional jumps.Group 2 ALU instructions handle multiplication and division. These operations differ fundamentally from Group 1 instructions in their operand handling because their results can exceed the size of the input operands. Multiplying two 16-bit numbers can produce a 32-bit result; dividing requires a dividend that may be twice the size of the divisor.To accommodate these larger values, Group 2 instructions use implicit registers — specific registers that are automatically involved without being explicitly named in the instruction.Unlike Group 1 instructions that take two explicit operands, Group 2 instructions specify only one operand (the multiplier or divisor):MUL operand ; operand can be REG or memory
DIV operand ; NOT immediate values
The other operand (multiplicand or dividend) and the result locations are implicit, determined by the operand size.The 8086 provides two multiplication instructions:
MUL — unsigned multiplication
IMUL — signed multiplication (Integer MULtiply)
When the operand is a byte (8-bit register or byte memory location):AX = AL × operand
The implicit multiplicand is AL, and the 16-bit result goes into AX.Examples:mov al, 10 ; AL = 10 (multiplicand)
mov bl, 5 ; BL = 5 (multiplier)
mul bl ; AX = AL × BL = 10 × 5 = 50 ; AX = 0032h (50 in hex)
Even though both operands fit in 8 bits and the result (50) also fits in 8 bits, the full 16-bit result is stored in AX. If AL=200 and BL=3, the result 600 (0258h) exceeds 255 and requires the full 16-bit AX.mov al, 200 ; AL = 200 (C8h)
mov bl, 3 ; BL = 3
mul bl ; AX = 200 × 3 = 600 = 0258h ; AH = 02h, AL = 58h
When the operand is a word (16-bit register or word memory location):DX:AX = AX × operand
The implicit multiplicand is AX, and the 32-bit result spans two registers: high word in DX, low word in AX.Examples:mov ax, 1000 ; AX = 1000 (multiplicand)
mov bx, 100 ; BX = 100 (multiplier)
mul bx ; DX:AX = AX × BX = 1000 × 100 = 100000 ; 100000 = 186A0h ; DX = 0001h (high word) ; AX = 86A0h (low word)
To verify: 0001 86A0h = 1×65536 + 34464 = 65536 + 34464 = 100000. Correct.Larger example:mov ax, 0FFFFh ; AX = 65535 (maximum unsigned 16-bit value)
mov bx, 0FFFFh ; BX = 65535
mul bx ; DX:AX = 65535 × 65535 = 4,294,836,225 ; = FFFE0001h ; DX = FFFEh, AX = 0001h
IMUL works identically to MUL but interprets operands as signed two's complement values:mov al, -5 ; AL = FBh (-5 in two's complement)
mov bl, 10 ; BL = 0Ah (10)
imul bl ; AX = (-5) × 10 = -50 ; AX = FFCEh (-50 in 16-bit two's complement)
For positive operands within range, MUL and IMUL produce identical bit patterns. The difference appears when operands are negative or when interpreting the flags.MUL and IMUL affect only two flags:
CF and OF are set if the result extends into the high half (AH for byte multiplication, DX for word multiplication)
CF and OF are clear if the high half is zero (for MUL) or sign-extension of the low half (for IMUL)
This tells you whether the full result fits in the same size as the operands:mov al, 10
mov bl, 5
mul bl ; AX = 50, fits in AL → CF=0, OF=0 mov al, 100
mov bl, 10
mul bl ; AX = 1000, exceeds 255 → CF=1, OF=1
Division is more complex than multiplication because it produces two results: a quotient and a remainder.
DIV — unsigned division
IDIV — signed division (Integer DIVide)
When the operand is a byte:AL = AX / operand (quotient)
AH = AX mod operand (remainder)
The implicit dividend is the full AX register (16 bits), divided by an 8-bit operand.Examples:mov ax, 100 ; AX = 100 (dividend)
mov bl, 7 ; BL = 7 (divisor)
div bl ; AL = 100 / 7 = 14 (quotient) ; AH = 100 mod 7 = 2 (remainder) ; Verification: 14 × 7 + 2 = 98 + 2 = 100 ✓
mov ax, 255 ; AX = 255 (FFh)
mov bl, 16 ; BL = 16 (10h)
div bl ; AL = 255 / 16 = 15 ; AH = 255 mod 16 = 15 ; Verification: 15 × 16 + 15 = 240 + 15 = 255 ✓
When the operand is a word:AX = DX:AX / operand (quotient)
DX = DX:AX mod operand (remainder)
The implicit dividend is the 32-bit value formed by DX:AX (DX is high word, AX is low word), divided by a 16-bit operand.Critical: Before performing word division with an unsigned dividend that fits in 16 bits, you must clear DX:mov dx, 0 ; Clear high word of dividend
mov ax, 10000 ; AX = 10000 (low word of dividend)
mov bx, 300 ; BX = 300 (divisor)
div bx ; AX = 10000 / 300 = 33 (quotient) ; DX = 10000 mod 300 = 100 (remainder) ; Verification: 33 × 300 + 100 = 9900 + 100 = 10000 ✓
If you forget to clear DX, the division uses whatever garbage value was in DX as part of a much larger dividend, producing incorrect results or triggering a divide overflow.Dividing a true 32-bit dividend:; Divide 100,000 by 1000
; 100,000 = 186A0h = DX:AX where DX=0001h, AX=86A0h
mov dx, 0001h
mov ax, 86A0h ; DX:AX = 100,000
mov bx, 1000 ; divisor = 1000 (3E8h)
div bx ; AX = 100000 / 1000 = 100 ; DX = 100000 mod 1000 = 0
IDIV interprets the dividend and divisor as signed values. For signed division of a 16-bit value in AX, you must sign-extend into DX using the CWD (Convert Word to Doubleword) instruction:mov ax, -100 ; AX = FF9Ch (-100 in two's complement)
cwd ; Sign-extend AX into DX:AX ; DX = FFFFh (because AX is negative)
mov bx, 7 ; divisor = 7
idiv bx ; AX = -100 / 7 = -14 (quotient) ; DX = -100 mod 7 = -2 (remainder)
Note: In signed division, the remainder has the same sign as the dividend.Division requires careful attention to avoid runtime errors:1. Division by zero causes a CPU exception:mov ax, 100
mov bl, 0
div bl ; CRASH! Generates interrupt 0 (divide error)
In DOS programs, this typically terminates the program with an error message. In modern operating systems, the process receives a signal (SIGFPE on Unix) or exception.2. Quotient overflow also causes the same exception:mov ax, 1000 ; Dividend = 1000
mov bl, 1 ; Divisor = 1
div bl ; Quotient = 1000, but AL can only hold 0-255! ; CRASH! Divide overflow
If the quotient does not fit in the destination register (AL for byte division, AX for word division), the processor triggers a divide error, the same as division by zero.3. Undefined flags:Unlike most instructions, DIV and IDIV leave the flags in an undefined state. Do not rely on flag values after division. If you need to test the quotient or remainder, use CMP or TEST explicitly:div bl
cmp al, 0 ; Explicitly test quotient for zero
jz quotient_zero ; Now we can safely branch
The asymmetry between multiplication results (always double-width) and division dividends (always double-width) allows chaining these operations: a multiplication result in DX:AX can become the dividend for a subsequent division.The third group of ALU instructions includes unary operations that work on a single operand. These modify one register or memory location in place, making them useful for counters, address manipulation, and sign conversion.The operand can be a register or memory location (not an immediate, since the result must be stored back).inc ax ; AX = AX + 1
dec cx ; CX = CX - 1
inc word [count] ; memory word incremented by 1
dec byte [si] ; memory byte decremented by 1
Flags affected: ZF, SF, OF, PF, AF are set according to the result.Critical: CF (Carry Flag) is NOT affected by INC and DEC. This deliberate design allows using them in loops without disturbing carry from multi-precision arithmetic:; Multi-precision addition in a loop
mov si, 10 ; Counter
process_loop: add ax, [data_low] ; Add low word, may set CF adc dx, [data_high] ; Add high word WITH carry dec si ; Decrement counter (CF preserved!) jnz process_loop ; Loop if counter != 0
Loop counter pattern: mov cx, 10 ; Initialize counter
loop_start: ; ... loop body ... dec cx ; CX = CX - 1, sets ZF when CX becomes 0 jnz loop_start ; Jump back if CX != 0
NOT flips every bit: 0 becomes 1, and 1 becomes 0.mov al, 11110000b ; AL = F0h
not al ; AL = 00001111b = 0Fh mov al, 00000101b ; AL = 05h (decimal 5)
not al ; AL = 11111010b = FAh (decimal 250)
NOT does NOT affect any flags. It is purely a bit manipulation operation with no arithmetic interpretation.NEG computes the two's complement negation: if operand represents N, NEG produces -N.mov al, 5 ; AL = 00000101b = +5
neg al ; AL = 11111011b = -5 (two's complement) ; Computation: NOT(5) + 1 = 11111010 + 1 = 11111011 mov al, 1
neg al ; AL = FFh = -1 mov ax, 100
neg ax ; AX = FF9Ch = -100 (signed 16-bit)
Mathematically: NEG(x) = NOT(x) + 1 = 0 - xFlags affected by NEG:Special cases:mov al, 0
neg al ; AL = 0, CF = 0, ZF = 1 mov al, 80h ; AL = -128 (most negative byte)
neg al ; AL = 80h (unchanged!), OF = 1 ; Cannot represent +128 in signed byte
How does the processor represent negative numbers, and how can the same ADD instruction work for both positive and negative values? The answer is two's complement, one of the most elegant conventions in computer architecture.Historical approaches and their problems:
Sign-magnitude: One bit for sign, rest for magnitude. Problem: two zeros (+0 and -0), complex arithmetic circuits.
Ones' complement: Negate by inverting bits. Problem: still two zeros, requires end-around carry.
Two's complement: Negate by inverting and adding 1. Winner: single zero, same hardware for signed and unsigned arithmetic.
The MSB has negative weight. For 8 bits b7...b0:value = -b7 × 128 + b6 × 64 + b5 × 32 + b4 × 16 + b3 × 8 + b2 × 4 + b1 × 2 + b0 × 1
Example: Interpreting 11111011:= -1×128 + 1×64 + 1×32 + 1×16 + 1×8 + 0×4 + 1×2 + 1×1
= -128 + 123 = -5
Note the asymmetry: one more negative number than positive (zero takes a non-negative slot).Method: Invert all bits and add 1Example: Convert +5 to -5 in 8 bits:+5 = 00000101
NOT = 11111010
+1 = 11111011 = -5
This is exactly what the NEG instruction does.The same addition hardware works for signed and unsigned arithmetic: 11111011 (-5 in two's complement)
+ 00001000 (+8)
---------- 00000011 (+3, correct!)
The carry out is discarded, and we get the correct signed result using the same circuit that adds unsigned numbers.The processor does not "know" if bits are signed or unsigned. The pattern 11111011 is:
Unsigned: 251
Signed: -5
This is why x86 has two sets of conditional jumps:
CF (Carry Flag): Unsigned overflow. Result does not fit in 0 to 2^n - 1.
OF (Overflow Flag): Signed overflow. Result does not fit in -2^(n-1) to 2^(n-1) - 1.
Example 1: Unsigned overflow onlymov al, 200 ; 200 unsigned, -56 signed
add al, 100 ; Result: 44 (300 mod 256)
; CF = 1 (200 + 100 = 300 &gt; 255)
; OF = 0 (-56 + 100 = 44, valid signed)
Example 2: Signed overflow onlymov al, 100 ; 100 both ways
add al, 50 ; Result: 150
; CF = 0 (150 &lt; 256)
; OF = 1 (150 &gt; 127, invalid signed)
Result 10010110b = 150 unsigned, but -106 signed (wrong).Example 3: Both overflowsmov al, 128 ; 80h = 128 unsigned, -128 signed
add al, 128 ; Result: 0
; CF = 1 (256 &gt; 255)
; OF = 1 (-128 + -128 = -256 &lt; -128)
Practical rule: Use CF for unsigned data (addresses, counts), OF for signed data (coordinates, temperatures).When moving signed values to larger registers, replicate the sign bit:mov al, -5 ; AL = FBh = 11111011
cbw ; Convert Byte to Word: AX = FFFBh = -5 (16-bit) ; Without sign extension, 00FBh = +251 (wrong!) Single zero: All bits = 0.
Unified hardware: ADD/SUB work for both signed and unsigned.
MSB = sign: MSB = 1 means negative in signed interpretation.
Negation: NEG(x) = NOT(x) + 1.
Asymmetric range: One more negative than positive.
Two overflow flags: CF for unsigned, OF for signed.
Having explored the various instruction types available in x86, from data movement to arithmetic to control flow, we now examine how these instructions are written and organized in assembly language programs. Understanding assembly language structure is essential for reading, writing, and debugging low-level code.At the most fundamental level, a processor executes machine code: raw binary data where each instruction is represented as a sequence of bytes. The processor's instruction decoder reads these bytes and determines what operation to perform, which registers or memory locations to access, and where to find operands.Machine code is the only language the CPU actually understands. It consists of numeric opcodes and encoded operands, stored as hexadecimal values in memory:B8 04 00 ; Three bytes: opcode B8, followed by immediate value 0004
CD 21 ; Two bytes: opcode CD, followed by interrupt number 21
While processors execute machine code directly, humans cannot practically write programs as sequences of hexadecimal numbers. The correspondence between binary patterns and operations is not intuitive, and a single mistyped digit can cause catastrophic errors.Assembly language solves this problem by providing a human-readable representation of machine code. Each assembly instruction has a one-to-one correspondence with a machine code instruction. When you write MOV AX, 4, an assembler translates this directly into the bytes B8 04 00. There is no abstraction, no interpretation, no high-level transformation. Assembly is simply machine code written in a form humans can read and write.This one-to-one correspondence distinguishes assembly from higher-level languages. In C, the statement x = a + b * c might compile to dozens of machine instructions depending on the compiler, optimization level, and data types. In assembly, each line you write becomes exactly one machine instruction (with a few exceptions for pseudo-instructions that the assembler expands).An assembler is a program that translates assembly language source code into executable machine code. The assembler performs several key functions:
Mnemonic translation: Converts human-readable instruction names (MOV, ADD, JMP) into their corresponding binary opcodes
Operand encoding: Translates register names and addressing modes into the appropriate bit patterns
Symbol resolution: Converts symbolic labels and variable names into actual memory addresses
Literal conversion: Transforms numeric literals from decimal, hexadecimal, or binary notation into binary values
For example, when the assembler encounters:start: mov ax, 1234h jmp start
It must:
Assign an address to the label start
Convert mov ax, 1234h into bytes B8 34 12 (note the little-endian byte order)
Calculate the relative offset from the JMP instruction back to start
Encode the JMP instruction with that offset
The assembler's output is typically an object file containing the machine code, which a linker then combines with other object files and libraries to produce the final executable.Every assembly instruction follows a consistent format:[label:] mnemonic [operands] [; comment]
Let us examine each component:Label (optional): A symbolic name that marks a location in the code. Labels are followed by a colon and serve as targets for jump and call instructions. They can also mark data locations.loop_start: ; This label marks the beginning of a loop dec cx jnz loop_start ; Jump back to loop_start if CX is not zero
Mnemonic: The instruction name. This is the human-readable representation of the operation, such as MOV (move data), ADD (addition), SUB (subtraction), JMP (jump), or CALL (call procedure). The mnemonic is required for every instruction.Operands: Zero, one, or two values that the instruction operates on. The number and type of operands depend on the specific instruction:
Zero operands: RET (return), NOP (no operation), PUSHF (push flags)
One operand: INC AX (increment), PUSH BX (push to stack), JMP label (jump)
Two operands: MOV AX, BX (move), ADD CX, 5 (add), CMP DX, SI (compare)
When two operands are present, the first is typically the destination and the second is the source (in Intel syntax, which we use in this course).Comment (optional): Text following a semicolon is ignored by the assembler. Comments document the code for human readers:mov ah, 9 ; DOS function 9: display string
mov dx, offset message ; DX points to the string to display
int 21h ; Call DOS interrupt
Let us examine a complete, working assembly program that displays "Hello, World!" using DOS interrupts:; Hello World program for DOS
; Demonstrates basic program structure and DOS I/O .model small ; Use small memory model (code + data &lt; 128KB)
.stack 100h ; Reserve 256 bytes for the stack .data ; Begin data segment msg db 'Hello, World!$' ; String terminated with $ for DOS .code ; Begin code segment
main proc ; Initialize data segment mov ax, @data ; Get address of data segment mov ds, ax ; Load it into DS register ; Display the message mov ah, 9 ; DOS function 9: print string mov dx, offset msg ; DX = address of message int 21h ; Call DOS interrupt ; Exit program cleanly mov ax, 4C00h ; DOS function 4Ch: terminate program ; AL = 00h: return code 0 (success) int 21h ; Call DOS interrupt
main endp end main ; Entry point is 'main'
This example illustrates several important concepts:Directives: Lines beginning with a dot (.model, .stack, .data, .code) are assembler directives, not CPU instructions. They tell the assembler how to organize the program but do not generate machine code directly.Memory model: The .model small directive tells the assembler that all code fits in one 64KB segment and all data fits in another. This affects how the assembler calculates addresses.Segments: The .data and .code directives separate variable declarations from executable instructions. This organization reflects how the CPU uses different segment registers (DS for data, CS for code).Data definition: The db (define byte) directive reserves memory and initializes it with the string bytes. The $ character terminates the string for DOS function 9.Procedure structure: proc and endp mark the beginning and end of a procedure. While not strictly required for simple programs, they help organize code and enable the assembler to manage local labels.Entry point: The end main directive tells the linker where program execution should begin.A typical assembly program for DOS follows this structure:.model small ; Memory model declaration
.stack 100h ; Stack segment size .data ; Data segment begins ; Variable declarations here buffer db 256 dup(?) ; 256 uninitialized bytes count dw 0 ; Word initialized to 0 table db 1,2,3,4,5 ; Array of bytes .code ; Code segment begins
main proc ; Program initialization mov ax, @data mov ds, ax ; Main program logic here ; Program termination mov ax, 4C00h int 21h
main endp ; Additional procedures can be defined here
helper proc ; Procedure code ret
helper endp end main ; Specify entry point
Assembly language provides several directives for defining data:Examples:byte_var db 42 ; Single byte, value 42
word_var dw 1000 ; Word (2 bytes), value 1000
dword_var dd 100000 ; Double word (4 bytes)
string_var db 'Hello',0 ; String with null terminator
array db 10 dup(0) ; 10 bytes, all initialized to 0
buffer db 256 dup(?) ; 256 uninitialized bytes
The dup operator creates multiple copies: 10 dup(0) means "10 copies of 0", while dup(?) indicates uninitialized storage.Understanding assembly language structure serves several practical purposes: Reading disassembled code: Debuggers and reverse engineering tools display machine code as assembly. Recognizing instruction formats helps you understand what a program does. Writing efficient code: Even when programming in high-level languages, understanding assembly helps you write code that compiles efficiently. Debugging: When a program crashes, the debugger shows assembly instructions. Understanding the structure helps locate the problem. System programming: Operating system kernels, device drivers, and boot loaders often require assembly for hardware-specific operations. We have seen that assembly language provides a human-readable representation of machine code, with a one-to-one correspondence between assembly instructions and their binary encodings. Now we examine how this encoding actually works: how instructions become bytes, and how those bytes are organized and interpreted by the processor.Every instruction the processor executes is represented as a sequence of bytes in memory. The first byte (or bytes) of an instruction is the opcode (operation code), which identifies what operation to perform. Depending on the instruction, additional bytes may follow to specify operands, addresses, or other parameters.When the processor fetches an instruction, it reads the opcode, determines the instruction type and length, fetches any additional bytes needed, then executes the operation. This process repeats billions of times per second in modern processors.Unlike some architectures (such as MIPS or ARM) where every instruction is exactly 4 bytes, x86 uses variable-length instructions ranging from 1 to 15 bytes. This design reflects the architecture's evolution from the 8-bit 8080 and 16-bit 8086 processors, where memory was precious and compact encodings were essential.The variable-length design has both advantages and disadvantages:Advantages:
Common instructions can be very short (1 byte for NOP, RET, single-register INC/DEC)
Memory is used efficiently since simple operations do not waste bytes
Backward compatibility is maintained as new instructions are added
Disadvantages:
Instruction decoding is more complex since the processor must determine instruction length
Parallel decoding is harder because instruction boundaries are not aligned
Modern processors spend significant silicon on instruction length detection
The simplest x86 instructions encode entirely within a single byte. These are typically operations that either have no operands or have an implicit operand encoded in the opcode itself:Notice how related instructions often have adjacent opcodes: PUSHF (9C) and POPF (9D), SAHF (9E) and LAHF (9F). This organization is not coincidental. It reflects the hardware design where certain opcode bits select between related operations.Many single-byte instructions encode the register operand directly in the opcode. The 8086 uses a simple scheme where the three low bits of certain opcodes specify which register to use:This encoding appears in the INC, DEC, PUSH, and POP instructions for 16-bit registers:To find the opcode for PUSH BX, we take the base opcode 50h and add the register code for BX (011 binary = 3). Result: 50h + 3 = 53h.This encoding scheme is elegant because decoding hardware can simply route the low three bits to a register selector while the high five bits determine the operation.When an instruction includes an immediate (constant) value, that value follows the opcode in the instruction stream. For example, the MOV instruction for loading an immediate into an 8-bit register uses opcodes B0-B7:The pattern continues for 16-bit registers (B8-BF), where the immediate is two bytes:Encoding example: To encode MOV AL, 42 (load decimal 42 into AL):
The opcode for MOV AL, imm8 is B0
42 decimal = 2A hexadecimal
The complete instruction is: B0 2A (two bytes)
Another example: To encode MOV AX, 1234h:
The opcode for MOV AX, imm16 is B8
The immediate 1234h is stored in little-endian order: 34 12
The complete instruction is: B8 34 12 (three bytes)
More complex instructions that can operate on different register combinations or memory locations use an additional byte called the ModR/M byte (also written as ModRM or Mod-Reg-R/M). This byte follows the opcode and specifies:
Mod (bits 7-6): Addressing mode (register direct, memory with displacement, etc.)
Reg (bits 5-3): Register operand or opcode extension
R/M (bits 2-0): Register or memory operand
For example, the instruction ADD AX, BX encodes as 01 D8:
Opcode 01: ADD r/m16, r16 (add register to register/memory)
ModR/M D8 = 11 011 000 binary Mod = 11: Register-to-register (no memory access)
Reg = 011: BX is the source
R/M = 000: AX is the destination The ModR/M byte enables a single opcode to represent many different operand combinations, keeping the instruction set compact while maintaining flexibility.The complete x86 opcode space can be visualized as a 16x16 table, where the row corresponds to the high nibble (bits 7-4) of the opcode and the column corresponds to the low nibble (bits 3-0): 0 1 2 3 4 5 6 7 8 9 A B C D E F +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ 0 |ADD|ADD|ADD|ADD|ADD|ADD|PSH|POP|OR |OR |OR |OR |OR |OR |PSH|-- | 1 |ADC|ADC|ADC|ADC|ADC|ADC|PSH|POP|SBB|SBB|SBB|SBB|SBB|SBB|PSH|POP| 2 |AND|AND|AND|AND|AND|AND|ES:|DAA|SUB|SUB|SUB|SUB|SUB|SUB|CS:|DAS| 3 |XOR|XOR|XOR|XOR|XOR|XOR|SS:|AAA|CMP|CMP|CMP|CMP|CMP|CMP|DS:|AAS| 4 |INC|INC|INC|INC|INC|INC|INC|INC|DEC|DEC|DEC|DEC|DEC|DEC|DEC|DEC| 5 |PSH|PSH|PSH|PSH|PSH|PSH|PSH|PSH|POP|POP|POP|POP|POP|POP|POP|POP| 6 | (Reserved / 32-bit extensions) | 7 |Jcc|Jcc|Jcc|Jcc|Jcc|Jcc|Jcc|Jcc|Jcc|Jcc|Jcc|Jcc|Jcc|Jcc|Jcc|Jcc| 8 |Imm|Imm|Imm|Imm|TST|TST|XCH|XCH|MOV|MOV|MOV|MOV|MOV|LEA|MOV|POP| 9 |NOP|XCH|XCH|XCH|XCH|XCH|XCH|XCH|CBW|CWD|CAL|WAI|PSF|POF|SAH|LAH| A |MOV|MOV|MOV|MOV|MVS|CMS|MVS|CMS|TST|TST|STO|STO|LOD|LOD|SCA|SCA| B |MOV|MOV|MOV|MOV|MOV|MOV|MOV|MOV|MOV|MOV|MOV|MOV|MOV|MOV|MOV|MOV| C |Shf|Shf|RET|RET|LES|LDS|MOV|MOV|ENT|LEV|RTF|RTF|INT|INT|INT|IRT| D |Shf|Shf|Shf|Shf|AAM|AAD|-- |XLT|ESC|ESC|ESC|ESC|ESC|ESC|ESC|ESC| E |LOP|LOP|LOP|LOP|IN |IN |OUT|OUT|CAL|JMP|JMP|JMP|IN |IN |OUT|OUT| F |LCK|-- |REP|REP|HLT|CMC|Grp|Grp|CLC|STC|CLI|STI|CLD|STD|Grp|Grp| +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
This table (simplified for illustration) shows how opcodes are organized. Row 4 (40-4F) contains INC and DEC for registers. Row 5 (50-5F) contains PUSH and POP. Row B (B0-BF) contains MOV immediate to register. The 7x row contains conditional jumps.Understanding this organization helps when examining memory dumps or debugging, as patterns become recognizable with practice.<br><img alt="Comprehensive x86 Opcode Structure and Instruction Overview" src="assets/l04-x86-opcode-table.png" target="_self">The x86 opcode structure and instruction overview chart from Fraunhofer FKIE. This color-coded reference shows all x86 opcodes organized by nibbles, with different colors indicating instruction categories: arithmetic/logic (green), memory operations (yellow), stack operations (orange), control flow (pink), and system/I/O (purple). This chart is an invaluable reference for understanding machine code.Understanding machine code encoding has several practical applications:1. Debugging with Hex DumpsWhen examining a crash dump or memory contents, recognizing opcode patterns helps identify code versus data:B8 00 4C CD 21 ; MOV AX, 4C00h / INT 21h (DOS exit)
2. Calculating Jump DistancesJump instructions often use relative offsets. Knowing instruction sizes helps calculate where a jump lands:EB 05 ; JMP SHORT +5 (skip next 5 bytes)
90 ; NOP (1 byte)
90 ; NOP (1 byte)
90 ; NOP (1 byte)
90 ; NOP (1 byte)
90 ; NOP (1 byte) - jump lands after this
3. Binary PatchingSecurity researchers and developers sometimes need to modify executables. Understanding opcodes enables precise patches:
Change JNZ (75) to JZ (74) to invert a condition
Replace instructions with NOP (90) to disable them
Modify immediate values to change program behavior
4. Understanding Compiler OutputWhen optimizing code, examining the generated assembly reveals what the compiler produced. Knowing instruction encodings helps understand code size and potential optimizations.The x86 instruction encoding has grown increasingly complex over the decades:
8086 (1978): Basic one-byte opcodes with ModR/M for complex addressing
80386 (1985): Added 0F prefix for two-byte opcodes (new instructions)
Pentium Pro (1995): Added conditional move instructions
x86-64 (2003): Added REX prefix for 64-bit operations and additional registers
AVX (2011): Added VEX prefix for SIMD extensions
AVX-512 (2016): Added EVEX prefix for even more registers and features
Each extension added prefix bytes that modify how subsequent bytes are interpreted, creating a layered encoding scheme that maintains backward compatibility while enabling new capabilities.This historical evolution explains why x86 encoding seems inconsistent: it preserves compatibility with code written for processors from four decades ago while supporting modern features. A program compiled for the original 8086 still runs on the latest Intel or AMD processor. Segment registers organize 8086 memory into logical regions: DS for data, CS for code, SS for stack, ES for extras. Physical addresses are calculated as segment × 16 + offset. MOV is the fundamental data transfer instruction with five operand combinations. It cannot modify CS or IP directly. FLAGS register reflects the state of the last ALU operation. Condition flags (CF, ZF, SF, OF) enable conditional execution; control flags (IF, DF) manage system behavior. Conditional jumps test specific flag values to make branching decisions. Use signed comparisons (JG, JL) for signed values and unsigned comparisons (JA, JB) for unsigned values. ALU instructions are organized into three groups: Group 1 (ADD, SUB, CMP, AND, OR, XOR, TEST) supports all five operand types; Group 2 (MUL, DIV) uses implicit AX/DX registers; Group 3 (INC, DEC, NOT, NEG) operates on a single operand. Two's complement allows the same hardware to perform both signed and unsigned arithmetic. CF indicates unsigned overflow; OF indicates signed overflow. Assembly language provides human-readable mnemonics that correspond one-to-one with machine code opcodes. Understanding opcode encoding helps with debugging and reverse engineering. What you'll practice:
FLAGS register behavior with different arithmetic operations
Conditional jumps (JE, JNE, JG, JL, JA, JB, etc.) based on flag states
Integer arithmetic including multiplication and division
Implementing if-else and loop constructs in assembly
Debugging and observing flag changes in Turbo Debugger
Recommended preparation:
Review the FLAGS register bit layout
Practice mental two's complement conversions for small numbers
Understand the difference between signed and unsigned comparisons <br>
<a data-tooltip-position="top" aria-label="https://yassinebridi.github.io/asm-docs/asm_tutorial_01.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://yassinebridi.github.io/asm-docs/asm_tutorial_01.html" target="_self">8086 Registers Tutorial</a> - Comprehensive guide to 8086 registers and their purposes <br>
<a data-tooltip-position="top" aria-label="https://yassinebridi.github.io/asm-docs/asm_tutorial_02.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://yassinebridi.github.io/asm-docs/asm_tutorial_02.html" target="_self">Memory Access Tutorial</a> - Detailed coverage of MOV instruction and addressing modes <br>
<a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/types-of-flags-in-8086/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/types-of-flags-in-8086/" target="_self">Types of Flags in 8086</a> - Visual explanation of the FLAGS register structure <br>
<a data-tooltip-position="top" aria-label="https://roboticelectronics.in/branch-instructions-in-8086/" rel="noopener nofollow" class="external-link is-unresolved" href="https://roboticelectronics.in/branch-instructions-in-8086/" target="_self">Branch Instructions in 8086</a> - Reference for all conditional and unconditional jump instructions <br>
<a data-tooltip-position="top" aria-label="https://yassinebridi.github.io/asm-docs/asm_tutorial_06.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://yassinebridi.github.io/asm-docs/asm_tutorial_06.html" target="_self">ALU Instructions Tutorial</a> - Detailed guide to arithmetic and logical instructions <br>
<a data-tooltip-position="top" aria-label="https://net.cs.uni-bonn.de/fileadmin/user_upload/plohmann/x86_opcode_structure_and_instruction_overview.pdf" rel="noopener nofollow" class="external-link is-unresolved" href="https://net.cs.uni-bonn.de/fileadmin/user_upload/plohmann/x86_opcode_structure_and_instruction_overview.pdf" target="_self">x86 Opcode Structure</a> - Comprehensive x86 opcode reference chart (PDF) <br>
<a data-tooltip-position="top" aria-label="https://compsecurityconcepts.wordpress.com/2014/09/22/8086-microprocessor-basics/" rel="noopener nofollow" class="external-link is-unresolved" href="https://compsecurityconcepts.wordpress.com/2014/09/22/8086-microprocessor-basics/" target="_self">8086 Microprocessor Basics</a> - Overview of 8086 architecture and instruction set ]]></description><link>lectures/eng/l04-eng.-x86-architecture-basic-instruction-types.html</link><guid isPermaLink="false">Lectures/ENG/L04-ENG. x86 Architecture - Basic Instruction Types.md</guid><pubDate>Tue, 03 Feb 2026 18:18:54 GMT</pubDate><enclosure url="assets/l04-intel-8086-chip.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l04-intel-8086-chip.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L04-x86-Opcode-Table]]></title><description><![CDATA[<img src="assets/l04-x86-opcode-table.png" target="_self">]]></description><link>assets/l04-x86-opcode-table.html</link><guid isPermaLink="false">assets/L04-x86-Opcode-Table.png</guid><pubDate>Tue, 03 Feb 2026 18:12:37 GMT</pubDate><enclosure url="assets/l04-x86-opcode-table.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l04-x86-opcode-table.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L04-Conditional-Jump-Table]]></title><description><![CDATA[<img src="assets/l04-conditional-jump-table.png" target="_self">]]></description><link>assets/l04-conditional-jump-table.html</link><guid isPermaLink="false">assets/L04-Conditional-Jump-Table.png</guid><pubDate>Tue, 03 Feb 2026 18:12:37 GMT</pubDate><enclosure url="assets/l04-conditional-jump-table.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l04-conditional-jump-table.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L04-FLAGS-Register-Layout]]></title><description><![CDATA[<img src="assets/l04-flags-register-layout.jpeg" target="_self">]]></description><link>assets/l04-flags-register-layout.html</link><guid isPermaLink="false">assets/L04-FLAGS-Register-Layout.jpeg</guid><pubDate>Tue, 03 Feb 2026 18:12:37 GMT</pubDate><enclosure url="assets/l04-flags-register-layout.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l04-flags-register-layout.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L04-Intel-8086-Chip]]></title><description><![CDATA[<img src="assets/l04-intel-8086-chip.jpeg" target="_self">]]></description><link>assets/l04-intel-8086-chip.html</link><guid isPermaLink="false">assets/L04-Intel-8086-Chip.jpeg</guid><pubDate>Tue, 03 Feb 2026 18:12:37 GMT</pubDate><enclosure url="assets/l04-intel-8086-chip.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l04-intel-8086-chip.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Lab04]]></title><description><![CDATA[
Курс: Архітектура обчислювальних систем
Тиждень: 4
Тривалість: 2 академічні години
Тип: Практична робота під наглядом інструктора
Після завершення практики студент зможе:
Пояснити, як арифметичні операції впливають на прапорці CF, ZF, SF, OF.
Передбачати стан прапорців після виконання інструкції.
Обирати правильний умовний перехід для знакових та беззнакових порівнянь.
Реалізовувати розгалуження та цикли в асемблерному коді.
Перегляньте лекційні матеріали за Тиждень 4 (регістр FLAGS, умовні переходи).CMP dest, src — порівнює два операнди (виконує dest - src, але не зберігає результат, лише встановлює прапорці).CMP AX, BX ; обчислює AX - BX, встановлює FLAGS
Важливо: Перед 16-бітним діленням очистіть DX: XOR DX, DX (unsigned) або CWD (signed, sign-extend AX → DX:AX).Створіть папку Lab04 та файл STUB.ASM:; STUB.ASM - template
.MODEL small
.STACK 100h .CODE
start: ; === Your code here === ; === End of your code === mov ah, 4Ch int 21h
end start
Скомпілюйте та переконайтесь, що TD запускається:tasm /zi stub.asm
tlink /v stub.obj
td stub.exe
У TD відкрийте вікно CPU (Alt+V, C) — внизу ви побачите рядок з прапорцями.Для кожного наступного завдання: копіюйте STUB.ASM у новий файл з відповідним ім'ям.Скопіюйте STUB.ASM → 1FLAGS.ASM.Всі експерименти — в одному файлі, розділені коментарями: ; --- Experiment 1.1: Zero Flag --- mov ax, 7 sub ax, 7 ; --- Experiment 1.2: Carry Flag --- mov al, 250 add al, 10 ; ... і так далі
Для кожного експерименту: спочатку передбачте результат, потім перевірте в TD. mov ax, 7 sub ax, 7
Яке значення ZF після SUB? Чому? mov al, 250 add al, 10 Яке значення в AL? (порахуйте: 250 + 10 = 260, але AL — 8 біт...)
CF = ? Чому? mov al, 100 add al, 50 AL = ? (у hex)
SF = ? OF = ?
100 + 50 = 150. Чи є 150 "правильним" результатом для signed byte (-128..127)?
Придумайте інструкцію, яка встановить одночасно CF = 1 і ZF = 1.Скопіюйте STUB.ASM → 2JUMP.ASM.Це ключовий експеримент. Розберіться, що відбувається: mov ax, 0FFFFh ; Яке це число? signed: ___ unsigned: ___ cmp ax, 1 jg is_greater ; signed comparison: перехід буде? jmp not_greater is_greater: mov bx, 1 ; маркер: "перейшли" jmp done not_greater: mov bx, 0 ; маркер: "не перейшли" done: ; перевірте BX у TD
Тепер замініть jg на ja і повторіть. Поясніть різницю.Скопіюйте STUB.ASM → 2MAX2.ASM.Напишіть код, який знаходить більше з двох чисел.
Вхід: AX, BX (два числа)
Результат: CX (більше з двох)
Не використовуйте .DATA — тільки регістри.
Використовуйте CMP та умовний перехід.Протестуйте з парами: (10, 5), (3, 8), (7, 7), (-5, 3) — всі тести в одному файлі, послідовно. Для кожного тесту змінюйте значення через MOV AX, ... / MOV BX, ....Скопіюйте STUB.ASM → 3LOOP.ASM.Спочатку проаналізуйте без запуску: mov cx, 5 xor ax, ax
again: add ax, cx dec cx jnz again Скільки разів виконається тіло циклу?
Яке фінальне значення AX? (порахуйте: 5+4+3+2+1 = ?)
Що станеться, якщо початкове CX = 0? (скільки ітерацій?)
Перевірте в TD. Для випадку CX=0 змініть код і подивіться.Скопіюйте STUB.ASM → 3EVEN.ASM.Напишіть код, який обчислює суму парних чисел від 2 до 10:
2 + 4 + 6 + 8 + 10 = 30
Результат: AX = 001Eh (30)
Не використовуйте .DATA — тільки регістри.
Скопіюйте STUB.ASM → 4MATH.ASM. Всі експерименти — в одному файлі, розділені коментарями. mov ax, 1000 mov bx, 100 mul bx
Де результат? Чому він не поміщається в AX? Перевірте DX:AX. mov ax, 1000 xor dx, dx ; чому це важливо? mov bx, 7 div bx
Де частка? Де остача? Перевірте: 1000 = 7 × + Спробуйте в TD (програма "впаде"): mov ax, 1000 mov bl, 3 div bl ; 8-bit division: AX / BL
Чому "Division overflow"? Підказка: 1000 / 3 = 333, а максимум для AL = ?До кінця заняття:
Створено та перевірено STUB.ASM
Виконано експерименти: 1FLAGS.ASM, 2JUMP.ASM, 3LOOP.ASM, 4MATH.ASM
Написано код для завдань: 2MAX2.ASM, 3EVEN.ASM
Закомічено всі файли
Дедлайн: 23:59 у день перед Практикою 5Напишіть програму, яка знаходить максимум серед трьох знакових чисел.
Вхід: AX, BX, CX (три числа, завантажте через mov)
Результат: AX (максимальне з трьох)
Не використовуйте .DATA — тільки регістри.
Тестові значення: A = 15, B = -8, C = 23 → очікуваний результат AX = 0017h (23)Вимоги:
Програма має працювати для будь-яких знакових 16-бітних чисел
Використовуйте правильні умовні переходи для signed порівняння
Напишіть програму, яка обчислює N! (факторіал).
Вхід: CX = N (завантажте через mov)
Результат: AX = N!
Не використовуйте .DATA — тільки регістри.
Тестові значення: N = 5 → очікуваний результат AX = 0078h (120)Обмеження: 7! = 5040 — останнє значення, що поміщається в 16 біт.Модифікуйте програму для обчислення факторіалу у 32-бітному результаті (DX:AX).
Результат: DX:AX = N! (32-бітне значення)
Тест: N = 10 → DX:AX = 0037:5F00h (3628800)
Максимум: 12! = 479001600 — останнє значення, що поміщається в 32 біти.
Підказка: MUL на 16-бітному операнді дає 32-бітний результат у DX:AX. Складність у тому, щоб помножити вже 32-бітне значення (DX:AX) на наступний множник.Запишіть відео, у якому: Поясніть експерименти з пари: (1.3) Чому виникає OF (або не виникає) при ADD AL, 50?
(2.1) Чому JG і JA дають різні результати для 0FFFFh?
(3.1) Що станеться з циклом, якщо CX = 0?
(4.3) Чому DIV BL викликає overflow при діленні 1000 на 3? Продемонструйте домашні програми: MAX.ASM з трьома різними наборами даних (включно з від'ємними)
FACT.ASM — покажіть результат у TD Lab04/
├── STUB.ASM ← шаблон
├── 1FLAGS.ASM ← експерименти з прапорцями
├── 2JUMP.ASM ← експеримент: signed vs unsigned
├── 2MAX2.ASM ← завдання: більше з двох
├── 3LOOP.ASM ← експеримент: аналіз циклу
├── 3EVEN.ASM ← завдання: сума парних
├── 4MATH.ASM ← експерименти: MUL/DIV
├── MAX.ASM ← домашнє: максимум з трьох
├── FACT.ASM ← домашнє: факторіал
└── video.txt ← посилання на Loom <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/FLAGS_register" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/FLAGS_register" target="_self">FLAGS Register</a> — детальний опис прапорців
<br><a data-tooltip-position="top" aria-label="https://www.felixcloutier.com/x86/jcc" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.felixcloutier.com/x86/jcc" target="_self">x86 Conditional Jumps</a> — повний список умовних переходів
<br><a data-tooltip-position="top" aria-label="https://www.gabrielececchetti.it/Teaching/CalcolatoriElettronici/Docs/i8086_instruction_set.pdf" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.gabrielececchetti.it/Teaching/CalcolatoriElettronici/Docs/i8086_instruction_set.pdf" target="_self">8086 Instruction Set</a>
]]></description><link>labs/lab04.html</link><guid isPermaLink="false">Labs/Lab04.md</guid><pubDate>Mon, 02 Feb 2026 18:25:46 GMT</pubDate></item><item><title><![CDATA[L03-NOTES. Архітектура x86 — регістри та пам'ять]]></title><description><![CDATA[Короткий довідник до Лекції 3. Детальніше дивіться у слайдах презентації.Система з основою 16, що використовує цифри 0-9 та A-F.Чому шістнадцяткова?
Одна шістнадцяткова цифра = рівно 4 двійкові біти
Компактне представлення двійкових даних
FF = 1111 1111 = 255
Групування: 8 бітів = 1 байт = 2 шістнадцяткові цифри. Архітектури: 8, 16, 32, 64 біти.Інструкції кодуються як бітові шаблони, що «з'єднуються» з конкретними апаратними модулями.Приклад структури коду операції:
00** **** — Маршрутизація до АЛП
0001 **** — Вибір інструкції SUB
**** 00** — Перший параметр (Регістр 0)
**** **01 — Другий параметр (Регістр 1)
0001 0001 = 11h = SUB R0, R1Ключова ідея: Кількість бітів для вибору регістра визначає, скільки регістрів підтримує архітектура. 2 біти → 4 регістри.R0 = Акумулятор (AX)
Апаратно з'єднаний для отримання арифметичних результатів
Немає потреби переміщувати дані після операцій
Зручний для послідовних обчислень
ADD та SUB автоматично «з'єднані назад» до R0.Кожен можна розділити: AX = AH (старший байт) + AL (молодший байт)
EAX, EBX, ECX, EDX (E = Extended)
ESI, EDI (індексні регістри)
ESP (вказівник стека), EBP (базовий вказівник) RAX, RBX, RCX, RDX
R8–R15 (додаткові регістри)
128-бітні XMM0–XMM15 (SIMD)
Детальніше:
<a data-tooltip-position="top" aria-label="https://www.cs.virginia.edu/~evans/cs216/guides/x86.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.cs.virginia.edu/~evans/cs216/guides/x86.html" target="_self">Guide to x86 Assembly</a>
Чому регістри швидкі?
Безпосередньо з'єднані з АЛП
Не потрібне декодування адреси
Чому їх так мало?
Фізичні обмеження: кожен регістр потребує з'єднання з кожною інструкцією
Більше регістрів = експоненційно більше провідників
Результат: Дуже мало (4-16 загального призначення), але надзвичайно швидкі.Проблема: 1 МБ = 8 388 608 бітів. З'єднувати кожен біт окремо неможливо.Рішення: Розташувати біти у 2D-сітці (матриці).
1 000 000 бітів → ~2 000 провідників (√n × 2)
Декодер рядків + Декодер стовпців → вибір одного біта Двійковий декодер перетворює адресні біти на вибір рядка/стовпця
Керована засувка на кожному перетині зберігає один біт
Вентилі AND забезпечують відповідь лише адресованої комірки
Сигнали дозволу читання/запису керують напрямком даних
Шина даних: Один провідник (або набір провідників) передає дані туди/назад.Детальніше:
<br><a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=rM9BjciBLmg" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=rM9BjciBLmg" target="_self">How Computers Remember (Memory &amp; Addressing)</a> (YouTube)
Розрядність адресної шини визначає обсяг пам'яті:
20-бітна → 1 МБ (8086)
32-бітна → 4 ГБ
64-бітна → 16 ЕБ (теоретично)
Випущений Intel у 1978 році. 16-бітний процесор, який дав початок архітектурі x86.Чому вивчаємо 8086?
Найпростіша архітектура x86
Досі безпосередньо сумісний із сучасним x86-64
Усі сучасні можливості є розширеннями цих основ
Детальніше:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Intel_8086" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Intel_8086" target="_self">Intel 8086 - Wikipedia</a>
Проблема: 16-бітний регістр може адресувати лише 64 КБ (2^16 байтів). Але 8086 мав адресувати 1 МБ.Рішення: Об'єднати два 16-бітні значення:Абсолютна адреса = (Сегмент × 16) + Зміщення
Приклади обчислень:F000:FFFD → F0000 + FFFD ------ FFFFD (1 048 573 у десятковій) 923F:E2FF → 923F0 + E2FF ------ A06EF (657 135 у десятковій) Сегменти перекриваються кожні 16 байтів (один параграф)
Одна фізична адреса може мати ~4 096 різних пар сегмент:зміщення
Максимальна адреса: 10FFEFh (трохи більше 1 МБ)
Детальніше:
<br><a data-tooltip-position="top" aria-label="https://thestarman.pcministry.com/asm/debug/Segments.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://thestarman.pcministry.com/asm/debug/Segments.html" target="_self">Removing the Mystery from SEGMENT:OFFSET Addressing</a>
Типова структура програми:
Сегмент коду (CS) — інструкції програми
Сегмент даних (DS) — змінні та константи
Сегмент стека (SS) — виклики функцій, локальні змінні
Сегменти можуть перекриватися або бути окремими залежно від потреб програми.Прапорці стану, що встановлюються арифметичними/логічними операціями:Прапорці керування:
DF (Напрямок) — напрямок рядкових операцій
IF (Переривання) — увімкнення/вимкнення переривань
TF (Трасування) — покрокове налагодження Шістнадцяткова система зручна, бо 1 hex-цифра = 4 біти
Інструкції кодуються як бітові шаблони, що «з'єднуються» з апаратурою
Регістри швидкі (з'єднані з АЛП), але їх мало (дороге з'єднання)
Пам'ять використовує 2D-адресацію сіткою для зменшення кількості провідників
8086 використовує сегмент:зміщення для адресації 1 МБ 16-бітними регістрами
Сегментні регістри розділяють код, дані та стек
Прапорці зберігають результати операцій для умовних переходів
Ви будете:
Налаштовувати середовище TASM + DOSBox + Turbo Debugger
Писати свою першу програму на асемблері
Спостерігати за регістрами та пам'яттю в налагоджувачі
Подумайте над:
Чим mov ax, 5 відрізняється від mov ax, [5]?
Що станеться, якщо додати два числа, що переповнюють регістр?
Як обчислити фізичну адресу з CS:IP?
]]></description><link>lectures/l03-notes.-архітектура-x86-—-регістри-та-пам'ять.html</link><guid isPermaLink="false">Lectures/L03-NOTES. Архітектура x86 — регістри та пам'ять.md</guid><pubDate>Sat, 31 Jan 2026 19:02:37 GMT</pubDate></item><item><title><![CDATA[L03. Архітектура x86 — регістри та пам'ять]]></title><description><![CDATA[Курс: Архітектура комп'ютерних систем
Тема: Від зʼєднань до регістрів, адресація пам'яті
Результати навчання: LO1 (Принципи архітектури), LO2 (Представлення даних), LO3 (Програмування на асемблері)У попередній лекції ми побудували схеми, які оперують двійковими даними: тригери, що зберігають 0 та 1, суматори, що обробляють потоки бітів, та АЛП (арифметико-логічний пристрій), керований двійковими кодами операцій. Двійковий код є рідною мовою цифрового апаратного забезпечення, але він створює практичну проблему для програмістів: двійкові числа довгі та важкі для читання. Число 255 потребує вісім двійкових цифр (11111111), а 32-бітова адреса перетворюється на незручний рядок із тридцяти двох 0 та 1. Нам потрібна більш компактна нотація, яка водночас легко відображається на базове двійкове представлення.Шістнадцяткова (з основою 16) система числення забезпечує саме таку зручність. Шістнадцятковий запис став стандартною нотацією в системному програмуванні, відладчиках, дампах пам'яті та мові асемблера, оскільки пропонує ідеальний баланс: компактне представлення для людей при збереженні тривіального перетворення у двійковий формат та навпаки.Ключова ідея шістнадцяткової системи є математичною: 16 — це степінь 2 (а саме 2^4 = 16). Це означає, що рівно чотири двійкових біти відповідають одній шістнадцятковій цифрі. Такий зв'язок робить перетворення між двійковою та шістнадцятковою системами суто механічним, без потреби в арифметиці — лише групування та пошук у таблиці.Розгляньте альтернативу: десяткова система (з основою 10) не має чіткого зв'язку з двійковою. Перетворення між десятковою та двійковою системами вимагає багаторазового ділення або множення. Але з шістнадцятковою системою ви просто розбиваєте двійкове число на групи по чотири біти та перекладаєте кожну групу незалежно.Наприклад, двійкове число 1111 0101 перетворюється у шістнадцяткове шляхом пошуку кожної групи:
1111 = F (десяткове 15)
0101 = 5 (десяткове 5)
Результат: F5 у шістнадцятковому форматі
Жодних обчислень — лише зіставлення шаблонів. Це робить шістнадцятковий формат безцінним при вивченні вмісту пам'яті або машинного коду.Оскільки система з основою 16 потребує шістнадцять різних символів цифр, а наша десяткова система надає лише десять (0-9), шістнадцяткова система використовує літери від A до F для представлення значень від 10 до 15:<img alt="Шістнадцяткові цифри 0-F з їхніми 4-бітовими двійковими еквівалентами" src="assets/l03-hex-binary-conversion.png" target="_self">Кожна шістнадцяткова цифра безпосередньо відповідає унікальному 4-бітовому двійковому шаблону, що робить перетворення між шістнадцятковим та двійковим форматами суто механічним.Ця таблиця має стати другою натурою для кожного, хто працює з комп'ютерними системами. Запам'ятовування двійкових шаблонів для кожної шістнадцяткової цифри дозволяє миттєво перетворювати подумки між представленнями.Зверніть увагу, що шістнадцятковий формат не чутливий до регістру: FF, ff та Ff — усі представляють одне й те саме значення (десяткове 255). Різні інструменти та мови програмування мають різні домовленості, але значення ідентичне.Двійкове у шістнадцяткове:Щоб перетворити двійкове у шістнадцяткове, згрупуйте біти у набори по чотири, починаючи справа (від молодшого біту). Якщо найлівіша група має менше чотирьох бітів, доповніть провідними нулями.Приклад: Перетворіть 110101110 у шістнадцяткове.
Згрупуйте по чотири справа: 1 1010 1110
Доповніть найлівішу групу: 0001 1010 1110
Перетворіть кожну групу: 1, A, E
Результат: 1AE (що дорівнює 430 у десятковому)
Шістнадцяткове у двійкове:Просто розгорніть кожну шістнадцяткову цифру до її 4-бітового двійкового еквівалента.Приклад: Перетворіть 3F7 у двійкове.
3 = 0011
F = 1111
7 = 0111
Результат: 0011 1111 0111 = 001111110111
Шістнадцяткове у десяткове:Використовуйте позиційну систему числення зі степенями 16.Приклад: Перетворіть 2A3 у десяткове.2A3 = 2 × 16² + A × 16¹ + 3 × 16⁰ = 2 × 256 + 10 × 16 + 3 × 1 = 512 + 160 + 3 = 675
Десяткове у шістнадцяткове:Багаторазово ділимо на 16, збираючи остачі знизу догори.Приклад: Перетворіть 500 у шістнадцяткове.500 ÷ 16 = 31 остача 4
31 ÷ 16 = 1 остача 15 (F)
1 ÷ 16 = 0 остача 1
Читаючи знизу догори: 1F4
Перевірка: 1×256 + 15×16 + 4×1 = 256 + 240 + 4 = 500.Байт складається з 8 бітів, що зручно дорівнює рівно двом шістнадцятковим цифрам. Це не випадковість; байт став фундаментальною одиницею комп'ютерної пам'яті саме тому, що 8 бітів забезпечують корисний діапазон (0-255 для беззнакових значень або від -128 до +127 для знакових), залишаючись степенем 2.Оскільки одна шістнадцяткова цифра представляє 4 біти (ніббл або напівбайт), маємо:
1 байт = 8 бітів = 2 шістнадцяткові цифри (00 до FF)
16 бітів = 2 байти = 4 шістнадцяткові цифри (0000 до FFFF)
32 біти = 4 байти = 8 шістнадцяткових цифр (00000000 до FFFFFFFF)
64 біти = 8 байтів = 16 шістнадцяткових цифр
Типові розміри архітектур еволюціонували протягом історії комп'ютерів:
8-бітові архітектури (ранні мікропроцесори, такі як Intel 8080): регістри та шляхи даних шириною 8 біт
16-бітові архітектури (Intel 8086): 16-бітові регістри, основа x86
32-бітові архітектури (Intel 80386, часто називається «x86»): розширені регістри до 32 біт
64-бітові архітектури (AMD64/x86-64): сучасний стандарт із 64-бітовими регістрами
Кожне покоління подвоювало ширину регістрів, і шістнадцяткова нотація ідеально масштабувалась: 8-бітове значення вміщується у 2 шістнадцяткові цифри, 16-бітове — у 4, 32-бітове — у 8, а 64-бітове — у 16.Різні мови програмування та інструменти використовують різні префікси або суфікси для позначення шістнадцяткових чисел:У цьому курсі ми переважно використовуватимемо суфікс h, поширений у мові асемблера x86, де 11h означає шістнадцяткове 11 (десяткове 17), а не десяткове 11.Шістнадцятковий формат повсюдний у низькорівневому програмуванні з кількох практичних причин:
Адреси пам'яті відображаються у шістнадцятковому форматі у відладчиках та дампах пам'яті
Коди операцій машинного коду документуються та відображаються у шістнадцятковому форматі
Бітові шаблони для прапорців, масок та дозволів чіткіші у шістнадцятковому форматі
Значення кольорів у графіці використовують шістнадцятковий формат (RGB як три байти)
Мережеві протоколи специфікують байти у шістнадцятковому форматі
Коли ви бачите mov ax, 1234h у коді асемблера, ви одразу знаєте, що це 16-бітове значення (4 шістнадцяткові цифри = 16 бітів), яке завантажує конкретний бітовий шаблон. Переглядаючи дамп пам'яті, що показує 48 65 6C 6C 6F, ви можете розпізнати це як текст ASCII («Hello»), знаючи шістнадцяткові коди для літер.Шістнадцятковий формат — це не просто нотаційна зручність. Це стандартна мова для обговорення точних двійкових значень, якими комп'ютери насправді оперують.У попередній лекції ми бачили, як АЛП використовує двійковий декодер для вибору операції, яку потрібно виконати. Біти коду операції подаються на декодер, який активує рівно один функціональний блок — суматор, віднімач, схему AND або інший компонент. Це не абстрактна програмна концепція. Це фізичні зʼєднання: певні бітові шаблони в коді операції буквально направляють сигнали до конкретних апаратних модулів.Цей розділ досліджує, як інструкції кодуються як бітові шаблони та як ці шаблони визначають, що робить апаратне забезпечення. Розуміння кодування інструкцій розкриває, чому процесорні архітектури мають характеристики, які вони мають, включаючи кількість регістрів, формати інструкцій та обмеження, з якими ми стикаємось у програмуванні на асемблері.Кожна машинна інструкція є, по суті, командою для маршрутизації сигналів через схеми процесора. Коли ви пишете ADD AX, BX мовою асемблера, асемблер перекладає це у конкретний бітовий шаблон. Коли цей бітовий шаблон досягає процесора, різні поля в ньому контролюють різні аспекти виконання:
Деякі біти вибирають операцію (додавання, віднімання, переміщення тощо)
Деякі біти специфікують операнд(и)-джерело
Деякі біти специфікують операнд-приймач
Деякі біти можуть вказувати режими адресації або інші модифікатори
Ці біти безпосередньо з'єднуються зі схемами декодерів, які ми побудували у Лекції 2. 3-бітове поле коду операції подається на декодер 3-до-8, активуючи одну з восьми можливих операційних схем. 2-бітове поле регістра вибирає один із чотирьох регістрів. Кодування не є довільним; воно відображає фізичну структуру процесора.<br><img alt="Інструкції асемблера з анотаціями на рівні вентилів" src="assets/l03-instruction-types-gates.png" target="_self">Інструкції асемблера відображаються на конкретні конфігурації вентилів в апаратному забезпеченні процесора. Кожен тип інструкції активує різні шляхи через АЛП та файл регістрів.Розгляньмо спрощений 8-бітовий формат інструкції для гіпотетичного процесора з 4 регістрами (R0, R1, R2, R3) та 4 операціями:Позиції бітів: 7 6 5 4 3 2 1 0 [--op--][-src-][-dst-] Біти 7-6 (op): Код операції, вибирає одну з 4 операцій
Біти 5-4 (src): Регістр-джерело, вибирає один із 4 регістрів
Біти 3-2 (dst): Регістр-приймач, вибирає один із 4 регістрів
Біти 1-0: Зарезервовані або не використовуються
Визначимо операції:
00 = ADD (додати джерело до приймача)
01 = SUB (відняти джерело від приймача)
10 = MOV (перемістити джерело до приймача)
11 = AND (побітове AND)
І регістри:
00 = R0
01 = R1
10 = R2
11 = R3
Розгляньмо байт інструкції 01 00 01 00 (двійкове), що є 44h у шістнадцятковому.Розбиваємо на поля:
01 (біти 7-6) = операція SUB
00 (біти 5-4) = R0 як джерело
01 (біти 3-2) = R1 як приймач
00 (біти 1-0) = не використовується
Ця інструкція означає: SUB R0, R1 або «відняти R0 від R1, зберігаючи результат у R1».Розглянемо інший приклад: 00 01 00 01 двійкове = 11h шістнадцяткове.
00 = операція ADD
01 = R1 як джерело
00 = R0 як приймач
01 = не використовується
Це: ADD R1, R0 або «додати R1 до R0».<br><img alt="Кодування інструкції машинного коду, що показує поля коду операції та операндів" src="assets/l03-instruction-encoding.png" target="_self">Бітові поля всередині байта інструкції кодують тип операції, регістр-джерело та регістр-приймач. Кожне поле відображається на конкретні схеми декодерів в апаратному забезпеченні.Коли байт інструкції 11h надходить до процесора: Біти 7-6 (00) подаються на декодер операцій. Вихід 0 декодера переходить у стан HIGH, активуючи схему ADD, тоді як схеми SUB, MOV та AND залишаються неактивними. Біти 5-4 (01) подаються на селектор регістра-джерела. Це мультиплексор, який направляє вміст R1 на вхід «B» АЛП. Біти 3-2 (00) подаються на селектор регістра-приймача. Це направляє вихід АЛП назад до R0, а також направляє поточний вміст R0 на вхід «A» АЛП. АЛП виконує додавання на своїх входах A та B. Результат зберігається у регістрі-приймачі (R0). Кожен окремий біт має призначення. Змініть біт 6 з 0 на 1 — і інструкція стає SUB замість ADD. Змініть біти 5-4 з 01 на 10 — і R2 стає джерелом замість R1.<br><img alt="Блок-схема ЦП, що показує АЛП, декодер та зʼєднання регістрів" src="assets/l03-cpu-alu-decoder.png" target="_self">Декодер ЦП отримує біти інструкції та активує відповідну операцію АЛП і шляхи регістрів. Біти коду операції безпосередньо контролюють, який функціональний блок виконує обчислення.Ось критичне розуміння: кількість бітів, виділених для вибору регістра, визначає максимальну кількість регістрів, яку може мати архітектура.З 2 бітами для вибору регістра ми можемо адресувати 2^2 = 4 регістри (00, 01, 10, 11).
З 3 бітами ми могли б адресувати 2^3 = 8 регістрів.
З 4 бітами ми могли б адресувати 2^4 = 16 регістрів.Запитання для студентів: Якщо формат інструкції використовує 2 біти для специфікації кожного з двох регістрових операндів, скільки різних регістрів може підтримувати архітектура?Відповідь: Максимум чотири регістри. 2-бітове поле може кодувати лише значення 0, 1, 2 та 3.Це пояснює, чому ранні процесори мали так мало регістрів. Intel 8086 має лише 8 регістрів загального призначення (потребуючи 3 біти кожен для адресації), і навіть ця скромна кількість займає значний простір кодування інструкцій. Наявність 32 регістрів потребувала б 5 біт на поле регістра, роблячи інструкції довшими та складнішими для декодування.Сучасні процесори, такі як x86-64, розширились до 16 регістрів загального призначення, додавши префіксний байт (префікс REX) до інструкцій, фактично додаючи додаткові біти до полів вибору регістра. Цей зворотно-сумісний підхід дозволив нові можливості, зберігаючи сумісність з існуючим кодом для 8 регістрів.Окрім кодування інструкцій, регістри мають фізичні витрати: Складність зʼєднань: Кожен регістр повинен з'єднуватися з входами та виходами АЛП. З N регістрами та M-бітовим АЛП вам потрібні мультиплексори з N×M вхідними провідниками лише для маршрутизації даних до АЛП. Розмір декодера: Вибір регістра потребує декодерів з N входами, які зростають експоненціально за кількістю вентилів. Площа на чіпі: Кожен регістр складається з M тригерів плюс логіка маршрутизації. Більше регістрів означає більше площі кремнію. Споживання енергії: Більше транзисторів означає більше потужності, більше тепла та більші вимоги до охолодження. Ці обмеження пояснюють історичну еволюцію від машин з 4 регістрами до 8, потім 16 і тепер 32 у деяких RISC-архітектурах. Кожне розширення потребувало досягнень у технології виробництва, щоб зробити додаткові зʼєднання практичними.Intel 8086, який ми вивчатимемо детально, використовує інструкції змінної довжини зі складним кодуванням. Однак фундаментальний принцип залишається: конкретні бітові шаблони в машинному коді активують конкретні апаратні шляхи. Коли ви пишете ADD AX, BX і асемблер генерує байти 01 D8, ці байти містять закодовану інформацію про:
Операцію (ADD)
Розмір операнда (16 біт)
Регістр-джерело (BX)
Регістр-приймач (AX)
Навчання читати та розуміти ці кодування поглиблює ваше розуміння того, що процесор насправді робить, коли виконує ваш код.У нашій спрощеній архітектурі з 4 регістрами не всі регістри однакові. Регістр R0 відіграє особливу роль: це акумулятор. Таке призначення не є випадковим, а відображає обдумані рішення проєктування апаратного забезпечення, що беруть початок від найперших комп'ютерів і зберігаються в сучасних процесорах x86.Акумулятор — це регістр, який апаратно під'єднаний для отримання результатів арифметичних і логічних операцій. Коли АЛП (Арифметико-логічний пристрій) обчислює суму, різницю або логічний результат, вихідні дані автоматично потрапляють до акумулятора без необхідності явно вказувати призначення в кожній інструкції.У нашій прикладній архітектурі:
ADD R1 означає "додати R1 до R0, зберегти результат у R0"
SUB R2 означає "відняти R2 від R0, зберегти результат у R0"
Акумулятор (R0) є неявним. Він одночасно є операндом-джерелом і призначенням. Така конструкція має кілька важливих наслідків.1. Спрощене кодування інструкційЗ виділеним акумулятором арифметичним інструкціям потрібно вказувати лише один операнд, а не два. Замість кодування "ADD джерело, призначення" (що вимагає бітів для обох), ми кодуємо лише "ADD джерело" (призначенням завжди є акумулятор).У нашому гіпотетичному форматі:
З явним призначенням: [op:2][src:2][dst:2][unused:2] = 8 біт
З неявним акумулятором: [op:2][src:2][unused:4] = 8 біт, але 4 біти звільнено
Ці звільнені біти можна використати для кодування більшої кількості операцій, більшої кількості регістрів-джерел або додаткових режимів адресації.2. Зменшена складність з'єднаньВихід АЛП потребує лише одного постійного шляху: до акумулятора. Без акумуляторної конструкції вихід АЛП потребував би мультиплексора для маршрутизації результатів до будь-якого з N регістрів, що вимагає N×M проводів (N регістрів, M біт кожен) плюс логіка вибору.З акумулятором:
Вихід АЛП з'єднується безпосередньо з входом R0
Вихідний мультиплексор не потрібен
Простіше, швидше, менше транзисторів
<br><img alt="Цифровий симулятор, що показує регістри A, B, з'єднані з АЛП" src="assets/l03-digital-simulator-cpu.png" target="_self">У цьому вигляді цифрового симулятора регістр A (акумулятор) апаратно під'єднаний до виходу АЛП. Регістр B надає другий операнд для бінарних операцій. Це фіксоване з'єднання усуває потребу у вихідній мультиплексації.3. Швидше виконанняОскільки вихідний шлях фіксований, немає затримки мультиплексора на результаті. Ланцюг переносу в суматорі вже є критичним шляхом синхронізації; додавання затримки мультиплексора сповільнювало б кожну арифметичну операцію.Акумуляторна конструкція найкраще проявляє себе в послідовних обчисленнях. Розглянемо обчислення (a + b + c + d):Без акумулятора (двооперандні інструкції):MOV R0, a ; R0 = a
ADD R0, b ; R0 = a + b
ADD R0, c ; R0 = a + b + c
ADD R0, d ; R0 = a + b + c + d
З акумулятором (однооперандні інструкції):LOAD a ; ACC = a
ADD b ; ACC = a + b
ADD c ; ACC = a + b + c
ADD d ; ACC = a + b + c + d
Акумуляторна версія має коротші інструкції (один операнд замість двох) і природно "акумулює" проміжну суму — саме звідси походить назва.Цей патерн надзвичайно поширений у реальних програмах: прочитати значення, обробити його, додати до проміжної суми, повторити. Акумуляторна архітектура оптимізована саме для цього патерну.Коли Intel проєктував 8086, вони успадкували концепцію акумулятора від попередніх процесорів. Регістр AX (Accumulator eXtended — розширений акумулятор) виконує цю особливу роль: Множення та ділення неявно використовують AX (і DX для розширених результатів) MUL BX множить AX на BX, зберігаючи результат у DX:AX
DIV BX ділить DX:AX на BX, частка в AX, остача в DX Інструкції введення-виведення використовують виключно AX IN AL, port читає байт з порту вводу-виводу в AL
OUT port, AX записує AX у порт вводу-виводу Рядкові інструкції використовують AX як джерело/призначення для даних LODSB завантажує байт з пам'яті в AL
STOSW зберігає AX у пам'ять Деякі інструкції мають коротше кодування при використанні AX ADD AX, 5 може кодуватися меншою кількістю байтів, ніж ADD BX, 5 Хоча сучасні процесори x86 дозволяють більшість операцій з будь-яким регістром, спадщина акумулятора зберігається в кодуванні інструкцій та спеціалізованих інструкціях.Концепція акумулятора масштабувалася разом з розрядністю архітектури:
8-біт: AL (Accumulator Low byte — молодший байт акумулятора)
16-біт: AX (Accumulator eXtended — розширений акумулятор) = AH:AL
32-біт: EAX (Extended AX — розширений AX)
64-біт: RAX (Register AX — регістр AX у 64-бітному режимі)
Це не окремі регістри, а перекриваючі подання одного й того самого фізичного сховища:
Запис у AL впливає на молодші 8 біт AX, EAX і RAX
Запис в AX впливає на молодші 16 біт EAX і RAX
Запис у EAX розширюється нулями для заповнення RAX (у 64-бітному режимі)
Концепція акумулятора бере початок від найперших електронних комп'ютерів. ENIAC (1945) мав акумулятори. IBM 701 (1952) мав акумулятор. Ранні мікропроцесори, такі як Intel 8008 (1972) та 8080 (1974), мали один регістр-акумулятор під назвою A.Така конструкція мала сенс, коли транзистори були дорогими. Кожен регістр вимагав десятків транзисторів, тому мінімізація кількості регістрів була важливою. Наявність одного спеціального регістра, який отримував усі арифметичні результати, дозволяла простіші формати інструкцій і зменшувала кількість з'єднань.З подешевшанням транзисторів (згідно із законом Мура) архітектури додавали більше регістрів загального призначення. RISC-архітектури, такі як ARM і MIPS, мають 16-32 регістри, де будь-який регістр може бути джерелом або призначенням для будь-якої операції. Але навіть ці архітектури "завантаження-збереження" зберігають деякі акумуляторо-подібні конвенції для певних операцій.Акумуляторна конструкція передбачає компроміс:Сучасні процесори x86 підтримують обидва стилі: інструкції з неявним акумулятором (для зворотної сумісності та компактного кодування) та інструкції з явними операндами (для гнучкості). Цей гібридний підхід відображає еволюцію архітектури протягом чотирьох десятиліть.Розуміння концепції акумулятора допомагає пояснити, чому асемблерний код виглядає саме так, чому певні регістри мають особливе призначення, і чому деякі інструкції мають коротше кодування, ніж інші. Це вікно в інтерфейс апаратного та програмного забезпечення, що визначає, як програми насправді виконуються.Регістри — це найшвидші місця зберігання в будь-якій комп'ютерній системі — невеликі комірки пам'яті, вбудовані безпосередньо в ЦП, що зберігають дані, які зараз обробляються. Розуміння еволюції регістрів x86 від їхніх 16-бітних витоків до сучасних 64-бітних реалізацій розкриває як історію персональних комп'ютерів, так і фундаментальні архітектурні рішення, що продовжують впливати на розробку програмного забезпечення сьогодні.Коли Intel проєктував процесор 8086 у 1978 році, вони створили чотири основні регістри загального призначення, кожен шириною 16 біт. Ці регістри отримали назви, що відображали їхнє призначене спеціалізоване використання, хоча на практиці програмісти завжди використовували їх дещо взаємозамінно [1].Акумулятор (AX) отримав свою назву, оскільки арифметичні операції природно накопичували свої результати тут. Коли ви додаєте два числа або виконуєте множення, результат з'являється в AX (або в парі DX:AX для більших результатів). Багато інструкцій були оптимізовані для роботи саме з AX, що робило операції з цим регістром швидшими та вимагало коротшого машинного коду [1].Базовий регістр (BX) служив основним регістром для адресації пам'яті в оригінальній конструкції 8086. При доступі до даних, збережених у пам'яті, програмісти завантажували базову адресу структури даних у BX, а потім використовували зміщення для доступу до окремих елементів. Це робило BX необхідним для реалізації масивів і складних структур даних.Регістр-лічильник (CX) був спроєктований спеціально для циклічних операцій. Набір інструкцій 8086 включає спеціальні інструкції циклу (такі як LOOP), що автоматично зменшують CX і переходять назад, якщо лічильник не досяг нуля. Ця апаратна підтримка підрахунку зробила CX природним вибором для будь-якої повторюваної операції, включаючи маніпуляції з рядками, де та сама операція має застосовуватися до кожного символу.Регістр даних (DX) функціонує як розширення AX для операцій, що дають результати, занадто великі для одного 16-бітного регістра. При множенні двох 16-бітних чисел 32-бітний результат з'являється зі старшим словом у DX і молодшим словом у AX, утворюючи пару DX:AX. Аналогічно, DX зберігає старшу частину діленого перед діленням і остачу після нього. DX також служить регістром адреси порту для операцій вводу-виводу, дозволяючи процесору спілкуватися з до 65 536 різними апаратними пристроями.Однією з найелегантніших особливостей регістрової архітектури 8086 є можливість доступу до окремих байтів у цих 16-бітних регістрах. Кожен з чотирьох основних регістрів може бути розділений на дві 8-бітні половини, надаючи програмістам детальний контроль над своїми даними [1].Конвенція іменування використовує суфікси H (High — старший) і L (Low — молодший):Ця конструкція фактично надає вісім 8-бітних регістрів, використовуючи лише чотири 16-бітні фізичні регістри. Ключове розуміння полягає в тому, що AH, AL і AX всі посилаються на частини одного й того ж апаратного регістра. Коли ви модифікуєте AL, ви змінюєте молодші 8 біт AX. Коли ви модифікуєте AH, ви змінюєте старші 8 біт AX. Цей патерн перекриваючого доступу був революційним для свого часу і залишається фундаментальним для програмування x86.Практичні переваги байтового доступу є суттєвими. Маніпуляція символами стає простою — символи ASCII поміщаються в один байт, тому AL може зберігати один символ, а AH — інший. Програмісти можуть упакувати два байти даних в один регістр, обробити їх разом і ефективно витягти результати. При взаємодії з 8-бітними периферійними пристроями або обробці байт-орієнтованих структур даних можливість працювати з окремими байтами без операцій маскування та зсуву економить значний час.Розглянемо простий приклад: якщо AX містить шістнадцяткове значення 4142h, то AH містить 41h (ASCII-код для 'A'), а AL містить 42h (ASCII-код для 'B'). Модифікація лише AL на 43h змінює AX на 4143h, тоді як AH залишається незмінним як 41h.<br><img alt="Оригінальні 16-бітні регістри 8086" src="assets/l03-8086-registers-basic.png" target="_self">Оригінальний набір регістрів 8086: чотири 16-бітні регістри загального призначення (AX, BX, CX, DX) з доступом до старшого/молодшого байтів (AH/AL, BH/BL, CH/CL, DH/DL), плюс сегментні регістри для адресації пам'яті.Intel 80386, випущений у 1985 році, приніс 32-бітні обчислення в архітектуру x86. Замість проєктування абсолютно нового набору регістрів Intel розширив існуючі регістри, зберігаючи зворотну сумісність — рішення, що визначило еволюцію x86 відтоді [1].Стратегія розширення використовувала префікс E (від "Extended" — розширений) для створення 32-бітних версій усіх існуючих регістрів:Коли ви записуєте значення в EAX, ви використовуєте всі 32 біти. Молодші 16 біт EAX доступні як AX, і в межах AX окремі байти залишаються доступними як AH і AL. Однак старші 16 біт EAX (біти 31-16) не мають окремої назви — до них можна отримати доступ лише через сам EAX або через операції бітової маніпуляції.80386 також додав чотири додаткові 32-бітні регістри, що не мали 8-бітних підрозділів:ESI (індекс джерела) та EDI (індекс приймача) були спроєктовані для рядкових операцій — копіювання, порівняння або сканування блоків пам'яті. ESI зазвичай вказує на дані-джерело, тоді як EDI вказує на призначення. Ці регістри забезпечують ефективну реалізацію функцій на кшталт memcpy() і strcmp() у C.ESP (вказівник стека) — можливо, найкритичніший регістр у системі, який завжди вказує на поточну вершину стека. Кожен виклик функції, виділення локальних змінних та переривання покладаються на правильність ESP. Процесор автоматично зменшує ESP при записуванні даних у стек і збільшує при витягуванні. Через цю критичну роль ESP ніколи не слід використовувати для обчислень загального призначення [1].EBP (базовий вказівник) служить стабільною точкою відліку в межах кадрів стека. Коли функція викликається, EBP зазвичай встановлюється на поточне значення ESP, створюючи фіксовану систему відліку. Локальні змінні потім доступні як від'ємні зміщення від EBP (EBP-4, EBP-8 тощо), тоді як параметри функції з'являються як додатні зміщення (EBP+8, EBP+12 тощо). Ця конвенція значно полегшує налагодження і дозволяє функціям мати локальні дані змінного розміру [1].<br><img alt="32-бітні регістри загального призначення x86" src="assets/l03-x86-32bit-registers.png" target="_self">32-бітний набір регістрів x86, представлений з 80386: розширені версії оригінальних регістрів (EAX, EBX, ECX, EDX) плюс індексні та вказівникові регістри (ESI, EDI, ESP, EBP).Архітектура AMD64 (також відома як x86-64), представлена у 2003 році, розширила регістри до 64 біт і додала абсолютно нові регістри для задоволення зростаючих вимог сучасного програмного забезпечення. Це розширення принесло найзначніші зміни до набору регістрів x86 з часів оригінального 8086.64-бітне розширення використовує префікс R (від "Register" — регістр) на оригінальних регістрах:Зверніть увагу, що в 64-бітному режимі навіть індексні та вказівникові регістри отримали 8-бітний доступ до молодшого байта (SIL, DIL, SPL, BPL) — функція, що була недоступна в 32-бітному режимі.Архітектура також додала вісім абсолютно нових 64-бітних регістрів, пронумерованих від R8 до R15. Коли розробникам "забракло букв" для змістовних назв, вони просто прийняли числові ідентифікатори:Суфікси слідують послідовному патерну: D для "double word" (подвійне слово, 32 біти), W для "word" (слово, 16 біт) і B для "byte" (байт, 8 біт).Окрім регістрів загального призначення, процесори x86-64 включають XMM0 до XMM15 — шістнадцять 128-бітних регістрів, призначених для SIMD-операцій (Single Instruction, Multiple Data — одна інструкція, багато даних). Ці регістри забезпечують паралельну обробку кількох елементів даних одночасно, що є критичним для мультимедійних застосунків, наукових обчислень і криптографії. Сучасні процесори додатково розширюють їх 256-бітними YMM і 512-бітними ZMM регістрами для ще більшого паралелізму.Повну еволюцію регістрів у архітектурах x86 можна підсумувати наступним чином:Ця зворотно сумісна еволюція означає, що код, написаний для 8086, все ще може виконуватися на сучасних процесорах, хоча він не використовуватиме переваг більших регістрів чи додаткових можливостей. Багаторівневий доступ до регістрів (AL у межах AX у межах EAX у межах RAX) залишається визначальною характеристикою архітектури x86.<br><img alt="Структура 64-бітного регістра, що показує вкладений доступ" src="assets/l03-x64-register-layout.png" target="_self">Вкладена структура регістрів x86-64: RAX (64-біт) містить EAX (32-біт), який містить AX (16-біт), що розділений на AH і AL (по 8 біт кожен). Цей патерн багаторівневого доступу забезпечує зворотну сумісність між усіма поколіннями x86.Регістри займають унікальне місце в ієрархії памʼяті компʼютера — вони є найшвидшими доступними місцями зберігання, проте їхня кількість суворо обмежена. Розуміння причин цього компромісу розкриває фундаментальні принципи архітектури комп'ютерів, які застосовуються далеко за межами сімейства x86.Перевага регістрів у швидкості випливає з їхньої фізичної реалізації та розташування всередині процесора:Пряме зʼєднання з АЛП. Регістри напряму підключені до арифметико-логічного пристрою (АЛП) та інших функціональних блоків процесора. Немає декодування адреси, немає контролера памʼяті, немає арбітражу шини — лише прямі електричні зʼєднання. Коли інструкція вказує «додати EAX і EBX», дані негайно надходять з цих регістрів на входи АЛП, а результат повертається до регістра призначення.Без проходження шини памʼяті. Доступ до основної памʼяті вимагає надсилання адреси через шину памʼяті, очікування декодування адреси контролером памʼяті, очікування отримання даних мікросхемами памʼяті, а потім очікування повернення даних назад через шину. Цей цикл займає десятки або сотні тактів. Регістри обходять увесь цей процес, оскільки фізично розташовані всередині ядра процесора.Доступ за один такт. Сучасні процесори можуть читати з регістрів і записувати в них протягом одного такту — і часто виконують кілька операцій з регістрами одночасно. Типова інструкція може читати два регістри-джерела, виконувати операцію та записувати результат у регістр призначення — усе за один такт. Такий паралелізм можливий саме тому, що регістри мають виділені, завжди доступні зʼєднання.Фізична близькість. На кристалі процесора регістри розташовані лише за кілька мікрометрів від виконавчих блоків. Електричні сигнали поширюються зі значною часткою швидкості світла, але навіть на таких швидкостях відстань має значення. Кілька міліметрів до основної памʼяті створюють помітну затримку порівняно з мікроскопічними відстанями всередині ядра процесора.Щоб оцінити різницю у швидкості, розгляньмо типові часи доступу в сучасній системі:Регістри буквально на порядки швидші за будь-яке інше сховище в системі.Враховуючи їхню надзвичайну швидкість, чому процесори не мають тисячі регістрів? Відповідь повʼязана з кількома інженерними обмеженнями:Складність фізичних зʼєднань. Кожен регістр має бути зʼєднаний з кожним виконавчим блоком інструкцій, який може його використовувати. Якщо АЛП потрібно додати два регістри, обидва регістри повинні мати шляхи передачі даних до входів АЛП. Якщо будь-який з 16 регістрів загального призначення може бути джерелом або призначенням, то 16 окремих шляхів даних повинні існувати для кожної точки зʼєднання. Додавання більшої кількості регістрів означає експоненційно більше зʼєднувальних провідників.Розгляньмо спрощений розрахунок: з 8 регістрами потрібна логіка маршрутизації, яка може вибрати будь-яке з 8 джерел для кожного з двох входів АЛП — це 8 × 2 = 16 входів мультиплексора лише для АЛП. З 16 регістрами це подвоюється до 32 входів. Але регістри також зʼєднані з блоками памʼяті, блоками операцій з рухомою комою та різними іншими функціональними блоками. Загальна кількість зʼєднань швидко зростає з кожним додатковим регістром.Обмеження кодування інструкцій. Кожна інструкція повинна вказувати, які регістри використовувати, і ця специфікація займає біти в машинному коді. З 4 регістрами потрібно 2 біти для ідентифікації кожного регістра (оскільки 2² = 4). З 8 регістрами потрібно 3 біти (2³ = 8). З 16 регістрами потрібно 4 біти (2⁴ = 16).Інструкція, яка використовує три регістри (два джерела та одне призначення), потребуватиме 6 додаткових бітів з 8 регістрами проти 12 додаткових бітів з 64 регістрами. Більші інструкції означають більше пропускної здатності памʼяті, витраченої на завантаження коду, більший кеш інструкцій і потенційно повільніше виконання.Фізичний простір на кристалі. Кожен регістр потребує реальної площі кремнію — транзисторів для зберігання бітів, драйверів для виведення значень, приймачів для отримання входів. Хоча один регістр є крихітним порівняно з загальним кристалом процесора, логіка маршрутизації та мультиплексори, що зʼєднують регістри з виконавчими блоками, займають значну площу. Цей простір можна було б використати для більших кешів, більшої кількості виконавчих блоків або інших функцій, які могли б забезпечити вищу загальну продуктивність.Спадна віддача. Дослідження компіляторів показали, що більшість функцій можна ефективно скомпілювати, використовуючи лише скромну кількість регістрів. Після певної межі (приблизно 16-32 регістри для більшості навантажень) додавання більшої кількості регістрів дає мінімальну користь, оскільки компілятор уже може зберігати найчастіше використовувані змінні в регістрах. Додаткова складність не виправдовується граничним покращенням.Взаємозвʼязок між регістрами та памʼяттю представляє один з найфундаментальніших компромісів в архітектурі компʼютерів:Регістри: Надзвичайно швидкі (субнаносекундний доступ), але суворо обмежені в кількості (16 регістрів загального призначення в x86-64). Кожне значення в регістрі негайно доступне для обчислень.Памʼять: Значно повільніша (десятки до сотень наносекунд для основної памʼяті), але набагато більша за ємністю (гігабайти до терабайтів). Значення повинні бути явно завантажені з памʼяті перед використанням і збережені назад після модифікації.Цей компроміс формує практично кожен аспект архітектури компʼютерів і програмування:Ієрархія памʼяті. Щоб подолати розрив між регістрами та основною памʼяттю, сучасні процесори включають кілька рівнів кеш-памʼяті. Кеш першого рівня (десятки кілобайт) швидкий, але малий; кеш другого рівня (сотні кілобайт) більший, але повільніший; кеш третього рівня (мегабайти) ще більший, але ще повільніший. Ця ієрархія намагається тримати часто використовувані дані близько до процесора, водночас задовольняючи величезні обсяги даних, які потребують сучасні програми.Оптимізація компілятора. Одне з найважливіших завдань оптимізуючого компілятора — це розподіл регістрів — вирішення, які змінні повинні перебувати в регістрах, а які мають зберігатися в памʼяті. Змінні, до яких часто звертаються в межах циклу, в ідеалі повинні залишатися в регістрах протягом усього виконання циклу. Менш часто використовувані дані можна «витіснити» в памʼять. Хороший розподільник регістрів може драматично покращити продуктивність програми.Угоди про виклики. Обмежена кількість регістрів вимагає ретельної координації між функціями. Угоди про виклики визначають, які регістри функція може вільно використовувати (зберігаються викликачем), а які вона повинна зберегти (зберігаються викликаним). Без таких угод кожен виклик функції вимагав би збереження всіх регістрів у памʼять, знищуючи більшу частину переваг продуктивності, які дають регістри [1].Проєктування алгоритмів. Програмісти, які розуміють компроміс між регістрами та памʼяттю, пишуть ефективніший код. Слід уникати повторного доступу до того самого елемента масиву — краще завантажити його в змінну (яку компілятор може тримати в регістрі) і повторно використовувати цю змінну. Змінні циклу повинні бути простими цілими числами, які поміщаються в регістри. Складні структури даних слід проєктувати так, щоб сприяти хорошій поведінці кешу.Архітектура x86 з її 16 регістрами загального призначення в 64-бітному режимі представляє поточну відповідь Intel та AMD на цей компроміс. Інші архітектури роблять інший вибір: ARM64 надає 31 регістр загального призначення, тоді як деякі RISC-архітектури пропонують 32 або більше. Кожен вибір відображає різний баланс між складністю кодування інструкцій, площею кремнію та практичними потребами програмування.Розуміння цього фундаментального компромісу готує вас до наступної теми: як памʼять організована у величезну сітку місць зберігання, яка, хоч і повільніша за регістри, забезпечує ємність, необхідну для реальних програм і даних.Розгляньмо завдання побудови навіть скромного обсягу компʼютерної памʼяті. Один мегабайт — тривіально малий обсяг за сучасними стандартами — містить 8 388 608 окремих бітів. Якби ми проєктували нашу систему памʼяті наївно, даючи кожному біту власний виділений провідник для читання та запису, нам знадобилося б понад вісім мільйонів провідників лише для адресації цього крихітного обсягу сховища. Таку систему було б неможливо виготовити, неможливо розвести на друкованій платі, і вона займала б величезну кількість фізичного простору.Ця проблема масштабування стає ще очевиднішою, коли ми розглядаємо сучасні обсяги памʼяті. Типовий компʼютер сьогодні має від 8 до 32 гігабайт оперативної памʼяті — це від 64 до 256 мільярдів бітів. Підхід «провідник на біт» вимагав би сотень мільярдів окремих зʼєднань, що очевидно абсурдно. Інженерам потрібен був фундаментально розумніший підхід, щоб зробити практичні системи памʼяті можливими.Рішення приходить із красивого математичного спостереження: розташувати біти у двовимірній сітці замість одновимірного масиву. Замість адресації кожного біта власним провідником ми використовуємо комбінацію вибору рядка та стовпця.Розгляньмо математику. Для одновимірного розташування n бітів нам потрібно n провідників. Але для двовимірної квадратної сітки з n бітів нам потрібно лише приблизно 2√n провідників — приблизно √n для вибору рядка та √n для вибору стовпця.Подивімось, як драматично це зменшує складність зʼєднань:
1 000 000 бітів (125 КБ): замість 1 000 000 провідників нам потрібно лише близько 2 000 провідників (1 000 рядків × 1 000 стовпців)
1 000 000 000 бітів (125 МБ): замість мільярда провідників достатньо приблизно 64 000 провідників
8 000 000 000 бітів (1 ГБ): близько 180 000 провідників замість восьми мільярдів
Це квадратичне зменшення складності зʼєднань робить сучасну високоємнісну памʼять фізично здійсненною. Матрична організація перетворює неможливе інженерне завдання на керовану задачу.Ліворуч: Сітка 4x4 керованих засувок утворює базову структуру масиву памʼяті. Праворуч: Лінії вибору рядків і стовпців активують конкретні комірки на їхньому перетині.Матричне рішення ставить нове питання: як активувати рівно один рядок з тисячі можливих рядків, використовуючи найменшу кількість керуючих ліній? Відповідь криється в двійкових декодерах.Двійковий декодер — це схема, яка перетворює n вхідних бітів на 2^n вихідних ліній, де рівно один вихід активний у будь-який момент часу. Вхідні біти кодують двійкове число, і декодер активує вихідну лінію, що відповідає цьому числу.Наприклад, декодер 2-в-4 приймає 2 вхідні біти та видає 4 вихідні лінії:
Вхід 00 → активує лінію 0
Вхід 01 → активує лінію 1
Вхід 10 → активує лінію 2
Вхід 11 → активує лінію 3
<br><img alt="Двійковий декодер, що перетворює 2-бітний вхід на 4 вихідні лінії" src="assets/l03-binary-decoder.png" target="_self" style="width: 400px; max-width: 100%;">Двійковий декодер 2-в-4: 2-бітний вхід вибирає рівно одну з чотирьох вихідних ліній, забезпечуючи ефективне декодування адреси.Щоб знайти конкретну комірку памʼяті в нашій матриці, ми використовуємо два декодери: один для рядків і один для стовпців [2]. Адреса, подана на ці декодери, активує конкретний провідник рядка та конкретний провідник стовпця; перетин цих двох активних провідників ідентифікує унікальне місце в памʼяті.За цією схемою вибір однієї комірки серед мільйона вимагає лише близько 20 адресних бітів загалом — 10 бітів для декодера рядків (вибір одного з 1 024 рядків) і 10 бітів для декодера стовпців (вибір одного з 1 024 стовпців). Це значно ефективніше, ніж мільйон окремих провідників вибору.<br><img alt="Масив памʼяті з двійковим декодером для вибору адреси" src="assets/l03-memory-with-decoder.png" target="_self">Повна адресація памʼяті: двійкові декодери перетворюють вхідну адресу на сигнали вибору рядка та стовпця, ідентифікуючи унікальну комірку на їхньому перетині.У кожній точці перетину матриці памʼяті знаходиться керована засувка — фундаментальний елемент зберігання, який ми досліджували в Лекції 1. Керована засувка, як описано в [2], використовує два ключові входи:
Вхід даних: один провідник, що несе бітове значення для зберігання
Дозвіл запису: керуючий сигнал, що визначає, коли схема повинна захопити дані
Поведінка елегантно проста. Коли дозвіл запису встановлено в 0, сигнал даних повністю ігнорується — засувка зберігає будь-яке значення, яке вона раніше зберігала, через свій внутрішній зворотний звʼязок. Коли дозвіл запису переходить в 1, схема захоплює поточне значення на вході даних і перезаписує свою збережену памʼять [2].Цей механізм зворотного звʼязку є серцем компʼютерної памʼяті. Після запису значення петля зворотного звʼязку підтримує його невизначено довго (поки подається живлення), навіть після зміни оригінального вхідного сигналу.Матрична організація створює тонку проблему. Коли ми активуємо рядок, кожна комірка в цьому рядку бачить той самий сигнал рядка. Так само, коли ми активуємо стовпець, кожна комірка в цьому стовпці реагує. Як ми гарантуємо, що лише одна комірка на перетині вибраного рядка та стовпця фактично виконує операцію читання або запису?Рішення використовує вентилі І в кожному перетині [2]. Керуюча логіка кожної комірки включає вентилі І, які поєднують сигнал вибору рядка з сигналом вибору стовпця. Оскільки вентиль І видає високий вихід лише тоді, коли всі його входи високі, комірку можна вибрати лише тоді, коли і її рядок, і її стовпець одночасно активні.Це гарантує, що сигнали читання або запису досягають лише конкретної засувки, ідентифікованої повною адресою [2]. Якщо ви активуєте рядок 47 і стовпець 83, лише комірка в позиції (47, 83) реагує — усі інші комірки в рядку 47 та всі інші комірки в стовпці 83 залишаються неактивними.Памʼять повинна підтримувати дві фундаментальні операції: запис даних у комірку та читання даних з комірки. Обидві операції використовують систему адресації для вибору цільової комірки, але відрізняються напрямком потоку даних.Для операції запису система встановлює сигнал дозволу запису разом з цільовою адресою. Вибрана комірка захоплює будь-яке значення, що зʼявляється на лінії даних, і зберігає його у своїй внутрішній засувці. Усі інші комірки ігнорують і дозвіл запису, і сигнал даних, оскільки їхні вентилі І не повністю активовані.Для операції читання система встановлює сигнал дозволу читання разом з цільовою адресою. Сигнал дозволу читання, як пояснено в [2], перемикає лінію даних, щоб вона діяла як вихід, а не як вхід. Вибрана комірка виставляє своє збережене значення на лінію даних, де його може зчитати інша схемотехніка.Критична деталь реалізації: оскільки той самий провідник даних служить і входом (під час запису), і виходом (під час читання), тристабільні буфери запобігають конфліктам. Ці спеціальні схеми можуть не лише виводити високий і низький стани, але й стан «відʼєднано», дозволяючи провіднику керуватися тією коміркою чи зовнішнім джерелом, яке є відповідним у даний момент.Ліворуч: Комірка памʼяті з тристабільним буфером, що дозволяє двонаправлений потік даних по спільному провіднику. Праворуч: Під час операції читання вибрана комірка виставляє своє збережене значення на лінію даних.Спільний провідник, що переносить дані в памʼять і з памʼяті, називається шиною даних. Ця шина є спільною для всіх комірок памʼяті — мільйонів або мільярдів їх — але завдяки логіці адресації лише одна комірка взаємодіє з нею в будь-який момент часу.Термін «шина» (bus) походить від латинського «omnibus», що означає «для всіх», і влучно описує спільний комунікаційний шлях. Шина даних зʼєднує масив памʼяті з процесором та пристроями введення-виведення, слугуючи магістраллю для потоку інформації всередині компʼютера.Ширина шини даних впливає на те, скільки даних можна передати за одну операцію. Ранні компʼютери мали 8-бітні шини даних; сучасні системи зазвичай використовують 64-бітні шини даних, дозволяючи їм читати або записувати 8 байтів одночасно.Одна матриця пам'яті, як ми її описали, зберігає окремі біти — один біт на комірку на кожному перетині рядка та стовпця. Але комп'ютери працюють з байтами як базовою одиницею зберігання. Як же нам прочитати або записати цілий байт за один раз?Елегантне рішення, описане в [2], полягає в тому, щоб скласти вісім ідентичних матриць, які спільно використовують ті самі адресні лінії та керуючі сигнали. Коли ви вказуєте адресу та подаєте команду читання, усі вісім матриць одночасно видають свої збережені біти за цією адресою. Ці вісім бітів разом утворюють один байт.Уявіть це як вісім прозорих аркушів, накладених один на одного. Кожен аркуш має сітку бітів, і всі аркуші використовують однакову систему координат. Коли ви вказуєте на позицію (47, 83), ви вказуєте крізь усі вісім аркушів одночасно, отримуючи доступ до одного біта з кожного аркуша — вісім бітів загалом, один байт.Ця паралельна архітектура означає, що читання або запис байта займає не більше часу, ніж читання або запис одного біта. Пропускна здатність множиться, тоді як затримка залишається сталою — це повторювана тема в оптимізації комп'ютерної архітектури.У комп'ютері пам'ять використовує вісім матриць одночасно, які спільно використовують ті самі адресні та керуючі лінії, дозволяючи читати/записувати цілий байт за один раз [2].<br><img alt="Оперативна пам'ять як абстрактний компонент" src="assets/l03-ram-abstraction.png" target="_self">Оперативна пам'ять (ОП), абстрагована як компонент з адресними лініями (вибір байта для доступу) та лініями даних (передача значення байта).<br><img alt="Повний модуль оперативної пам'яті" src="assets/l03-ram-complete.png" target="_self">Повний модуль оперативної пам'яті, що показує адресний декодер, який перетворює двійкові адресні біти в сигнали вибору рядка/стовпця, забезпечуючи доступ до будь-якого байта в масиві пам'яті.Адресна шина — це набір провідників, які передають адресу комірки пам'яті, до якої здійснюється доступ. Так само як ширина шини даних визначає, скільки бітів можна передати за один раз, ширина адресної шини визначає, скільки унікальних адрес можна адресувати.Це співвідношення безпосередньо випливає з двійкового кодування: маючи n адресних бітів, ми можемо представити 2^n різних адрес. Кожна адреса відповідає одному байту пам'яті. Тому 32-бітна адресна шина може керувати приблизно 4 мільярдами байтів (4 ГБ) [2].<br><img alt="Формула обчислення адресного простору" src="assets/l03-address-space-formula.png" target="_self">Співвідношення між шириною адресної шини та адресованою пам'яттю: n адресних бітів можуть адресувати 2^n унікальних комірок. 32-бітна адресна шина дозволяє адресувати 4 ГБ (2^32 байтів).Перехід від 16-бітної до 64-бітної адресації являє собою драматичне розширення адресованої пам'яті. 16-бітна адресна шина обмежує систему 64 КБ — сьогодні ледве достатньо для складного документа. 32-бітний бар'єр у 4 ГБ став серйозним обмеженням для настільних комп'ютерів до середини 2000-х, спонукаючи перехід до 64-бітних архітектур.Сучасні 64-бітні системи теоретично можуть адресувати 16 ексабайтів (16 мільярдів гігабайтів), але практичні обмеження від дизайну материнських плат, обмеження операційних систем та економічні фактори тримають поточні системи значно нижче цієї межі.Не вся пам'ять однакова. Дві принципово різні технології домінують у проєктуванні комп'ютерної пам'яті: статична пам'ять (SRAM) (Static Random Access Memory) та динамічна пам'ять (DRAM) (Dynamic Random Access Memory).Статична пам'ять (SRAM) використовує кілька транзисторів на біт — зазвичай шість транзисторів, організованих у конфігурації тригера [2]. Ця конструкція створює стабільний цикл зворотного зв'язку, який підтримує збережене значення необмежено довго, поки подається живлення. Слово «статична» у назві SRAM вказує саме на цю стабільність; дані не потребують періодичного оновлення.Динамічна пам'ять (DRAM) досягає вищої щільності через радикальне спрощення: один транзистор MOSFET і крихітний конденсатор на біт [2]. Конденсатор зберігає заряд для представлення 1 або відсутність заряду для представлення 0. Ця мінімальна конструкція дозволяє DRAM упаковувати значно більше бітів на тій самій площі кремнію, що робить її набагато дешевшою за біт.<br><img alt="Порівняння SRAM та DRAM" src="assets/l03-sram-vs-dram.png" target="_self">Порівняння структур комірок SRAM та DRAM: SRAM використовує 6 транзисторів у стабільній конфігурації тригера, тоді як DRAM використовує лише 1 транзистор плюс конденсатор, жертвуючи складністю заради щільності.Критична відмінність полягає у вимогах до оновлення (регенерації). Конденсатор не є ідеальним пристроєм зберігання — він повільно втрачає заряд з часом, зазвичай втрачаючи своє збережене значення протягом мілісекунд. Для підтримки цілісності даних контролери DRAM повинні періодично зчитувати кожну комірку та перезаписувати її значення — процес, що називається оновленням (або регенерацією). Це відбувається тисячі разів на секунду, споживаючи пропускну здатність пам'яті та додаючи складності, але економія коштів від простіших комірок робить DRAM економічно необхідною для великих обсягів пам'яті.Швидкість SRAM та її простота (не потрібна схема оновлення) роблять її ідеальною для малих, швидких пам'ятей, де продуктивність є першочерговою. Щільність DRAM та низька вартість роблять її єдиним практичним вибором для гігабайтів основної пам'яті.Компроміс між SRAM та DRAM ілюструє фундаментальну істину комп'ютерної архітектури: не існує єдиної ідеальної технології пам'яті. Швидка пам'ять дорога та має низьку щільність. Дешева пам'ять високої щільності повільна. Інженери вирішують цю дилему через ієрархію пам'яті — багаторівневу систему, де кожен рівень пропонує різний компроміс між швидкістю, вартістю та ємністю.Ієрархія, від найшвидшого до найповільнішого: Регістри (десятки байтів): інтегровані безпосередньо в ядро процесора. Час доступу вимірюється в пікосекундах. Найвища швидкість, але надзвичайно обмежена кількість — лише 16-32 регістри загального призначення в більшості архітектур. Кеш (кілобайти до мегабайтів): побудований на SRAM, розташований на кристалі процесора або дуже близько до нього. Сучасні процесори мають кілька рівнів кешу (L1, L2, L3) зі зростаючим розміром та затримкою. Кеш L1 може мати доступ за 1-4 такти процесора; L3 може займати 30-50 тактів. Основна пам'ять (гігабайти): побудована на DRAM, підключена до процесора через контролери пам'яті та шини. Час доступу 50-100 наносекунд — сотні тактів процесора. Але ємність вимірюється в гігабайтах за розумну ціну. Накопичувачі (терабайти): SSD та жорсткі диски. Час доступу в мікросекундах (SSD) до мілісекунд (HDD). Значно повільніші за оперативну пам'ять, але енергонезалежні та надзвичайно ємні. Ця ієрархія використовує критично важливу властивість реальних програм: локальність звернень. Програми схильні звертатися до тих самих даних повторно (часова локальність) та звертатися до даних поблизу раніше використаних даних (просторова локальність). Зберігаючи нещодавно використані дані на швидких верхніх рівнях ієрархії, системи створюють ілюзію великої, швидкої пам'яті, фактично платячи здебільшого за велику, повільну пам'ять.Ієрархія пам'яті — це не просто оптимізація, вона є фундаментальною для комп'ютерної архітектури. Без неї комп'ютери були б або неприйнятно дорогими, або непридатно повільними. Розуміння того, як дані рухаються через цю ієрархію, є суттєвим для розуміння того, чому сучасні комп'ютери працюють так, як вони працюють, і це буде повторюваною темою протягом усього курсу.8 червня 1978 року Intel випустила мікропроцесор 8086 — чіп, який докорінно змінив комп'ютерну індустрію [3]. Цей 16-бітний процесор, реалізований приблизно з 20 000 активних транзисторів, дав початок тому, що ми зараз називаємо архітектурою x86, яка стала найуспішнішою та найдовговічнішою лінійкою процесорів Intel.Походження 8086 розкриває цікавий поворот корпоративної стратегії. Проєкт розпочався в травні 1976 року як те, що Intel планувала як тимчасову заміну, поки вони працювали над своїм справді амбітним проєктом: iAPX 432 [3]. iAPX 432 мав стати революційною 32-бітною архітектурою з передовими можливостями, такими як адресація на основі можливостей та апаратна підтримка об'єктно-орієнтованого програмування. Intel очікувала, що він стане їхньою флагманською лінійкою продуктів. Натомість «тимчасовий» 8086 став фундаментом обчислень на наступні п'ять десятиліть, тоді як iAPX 432 зник у небутті як комерційний провал.Успіх 8086 не був випадковим. Стівен Морс, який визначив архітектуру, прийняв критично важливе проєктне рішення: 8086 використовуватиме схожу архітектуру до існуючих 8-бітних мікропроцесорів Intel (8008, 8080 та 8085) [3]. Ця зворотна сумісність означала, що програми на асемблері могли безперешкодно мігрувати зі старих 8-бітних платформ на новий 16-бітний процесор. Програмістам не потрібно було вивчати цілком нову систему — вони могли використовувати свої наявні знання та код. Ця філософія проєктування підтримки зворотної сумісності характеризує розвиток x86 з тих пір.Часові рамки розробки також примітні: створення 8086 зайняло трохи більше двох років від початкової концепції до працюючого продукту [3]. В епоху до сучасних інструментів автоматизації електронного проєктування це було видатним досягненням, яке продемонструвало інженерні можливості Intel.<br><img alt="Чіп Intel 8086 у корпусі DIP-40" src="assets/l03-intel-8086-chip.jpeg" target="_self">Ви можете слушно запитати: навіщо вивчати процесор 1978 року в сучасному курсі комп'ютерної архітектури? Відповідь полягає в розумінні того, що 8086 представляє найпростішу версію архітектури x86, яка залишається безпосередньо актуальною для сучасних обчислень.Кожен процесор x86-64, з яким ви зустрічаєтесь сьогодні — чи то Intel Core i9, чи AMD Ryzen — підтримує зворотну сумісність з оригінальним набором інструкцій 8086. Коли ваш комп'ютер завантажується, він фактично запускається в режимі, який є по суті покращеним 8086 (так званий «реальний режим»), перш ніж перейти до сучасної 64-бітної роботи. BIOS-код, завантажувачі та певні низькорівневі системні функції досі працюють, використовуючи концепції, що безпосередньо походять з 1978 року.Сучасні можливості x86 є принципово розширеннями концепцій 8086, а не їх замінами:Розуміння 8086 — це як розуміння коріння дерева: ви не можете повністю осягнути складну розгалужену структуру над землею, не знаючи, що лежить під нею. Студенти, які опанували концепції 8086, виявляють, що вивчення сучасних можливостей x86-64 стає питанням розуміння розширень, а не вивчення чогось цілком нового.Тут ми стикаємося з однією з найцікавіших проєктних задач 8086. Процесор має 16-бітні регістри — кожен може містити значення від 0 до 65 535. Якби ми використовували один регістр для вказівки адрес пам'яті, ми могли б адресувати щонайбільше 2^16 = 65 536 байтів, або 64 КБ пам'яті.Але інженери Intel хотіли, щоб 8086 адресував більше пам'яті. Вони оснастили процесор 20-бітною адресною шиною, здатною адресувати 2^20 = 1 048 576 байтів = 1 МБ пам'яті. Відома (і, ймовірно, апокрифічна) цитата «640 КБ має бути достатньо для будь-кого» насправді стосувалася використовуваної частини цього 1 МБ адресного простору, з верхніми 384 КБ, зарезервованими для системних функцій.Це створило фундаментальну невідповідність: як генерувати 20-бітні адреси, коли ваш найбільший регістр може містити лише 16 бітів? Ви не можете просто додати більше бітів до регістрів — це змінило б усю архітектуру процесора. Рішення Intel було елегантним та впливовим: адресація сегмент:зміщення.Кількісно оцінімо проблему адресації, з якою зіткнулися інженери Intel [4]. 16-бітний регістр може зберігати значення від 0 до 65 535, тобто може безпосередньо адресувати щонайбільше 64 КБ пам'яті. Але 20-бітна адресна шина 8086 могла досягати 1 МБ пам'яті — у шістнадцять разів більше, ніж міг адресувати один регістр.Одним наївним рішенням було б поділити адресний простір 1 МБ на 16 фіксованих сегментів по 64 КБ кожен. Знадобилося б лише 4 біти для вибору сегмента (2^4 = 16), залишивши 16-бітний регістр для вказування зміщення в межах цього сегмента. Це працювало б, але було б негнучким — програми та дані мали б акуратно вміщуватися в межі 64 КБ, і не можна було б легко розмістити структуру даних, що охоплювала два сегменти.Intel обрала гнучкіший підхід: перекриття сегментів [4].Схема адресації сегмент:зміщення використовує два 16-бітні значення для вказування місця в пам'яті: значення сегмента та значення зміщення. Фізична (абсолютна) адреса обчислюється за такою формулою:Абсолютна адреса = (Сегмент × 16) + Зміщення<br><img alt="Візуалізація формули Сегмент:Зміщення" src="assets/l03-segment-offset-formula.png" target="_self">
Чому множимо на 16? У шістнадцяткових термінах множення на 16 еквівалентне зсуву вліво на одну шістнадцяткову цифру — по суті, дописування нуля справа від числа. Це перетворює 16-бітне значення сегмента на 20-бітну базову адресу, до якої ми додаємо 16-бітне зміщення.Загальноприйнятий запис — Сегмент:Зміщення, де обидва значення записуються у шістнадцятковій системі. Наприклад, F000:FFFD означає сегмент F000h зі зміщенням FFFDh.Розберімо кілька прикладів, щоб сформувати інтуїцію щодо цієї схеми адресації [4].Приклад 1: F000:FFFDЦя адреса представляє місце у верхній області пам'яті, яка зазвичай використовується для коду BIOS: Сегмент: F000 Сегмент × 16: F0000 (зсув вліво на одну шістнадцяткову цифру)
+ Зміщення: + FFFD ------- Абсолютна: FFFFD = 1 048 573 у десятковій системі
Ця адреса дуже близька до вершини адресного простору 1 МБ (який закінчується на FFFFF = 1 048 575).Приклад 2: 923F:E2FFБільш довільна адреса, що демонструє арифметику [4]: Сегмент: 923F Сегмент × 16: 923F0 (зсув вліво на одну шістнадцяткову цифру)
+ Зміщення: + E2FF ------- Абсолютна: A06EF = 657 135 у десятковій системі
Зверніть увагу, як додавання може породжувати переноси, що поширюються через шістнадцяткові цифри.Приклад 3: Максимально можлива адреса (FFFF:FFFF)Що станеться, коли ми максимізуємо і сегмент, і зміщення? Сегмент: FFFF Сегмент × 16: FFFF0 (зсув вліво на одну шістнадцяткову цифру)
+ Зміщення: + FFFF ------- Абсолютна: 10FFEF = 1 114 095 у десятковій системі
Це 21-бітна адреса! Вона перевищує межу 1 МБ (яка закінчується на FFFFF). Це переповнення створює так звану «Верхню область пам'яті» (High Memory Area, HMA) — додаткові 65 520 байтів, які стали доступними на пізніших процесорах. На оригінальному 8086 ця адреса «загорталася б» через обмеження 20-бітної адресної шини.Ключова характеристика цієї схеми адресації полягає в тому, що сегменти значно перекриваються [4]. Кожен новий сегмент починається лише через 16 байтів (один «параграф» у термінології x86) після попереднього. Оскільки весь адресний простір містить 1 048 576 байтів, а сегменти починаються кожні 16 байтів, існує 65 536 можливих початкових точок сегментів.<br><img alt="Як 64-КБ сегменти перекриваються в адресному просторі 8086" src="assets/l03-8086-segment-overlap.png" target="_self">
Ця природа перекриття означає, що одна фізична адреса може бути представлена багатьма різними парами сегмент:зміщення [4]. Фактично, для будь-якої заданої фізичної адреси може існувати до 4096 різних представлень сегмент:зміщення!Розгляньмо фізичну адресу 7C00h — історично значущу як місце, куди BIOS завантажує завантажувальний сектор. Цю адресу можна записати як:Усі вони посилаються на той самий фізичний байт у пам'яті. Код завантажувального сектора традиційно використовує або 0000:7C00 (на основі зміщення), або 07C0:0000 (на основі сегмента), і розуміння цієї еквівалентності є суттєвим при налагодженні або написанні низькорівневого коду.Схема сегмент:зміщення від Intel, хоча й здається складною, пропонувала значні переваги з урахуванням апаратних обмежень 1978 року:Переміщення програм: Програми можна було переміщувати в пам'яті просто змінюючи значення сегментних регістрів. Код на основі зміщень у програмі не потребував змін — лише базу сегмента потрібно було скоригувати. Це робило керування пам'яттю значно гнучкішим.Захист пам'яті (концептуальний): Різні сегменти можна було використовувати для різних цілей — код в одному сегменті, дані в іншому. Хоча 8086 не забезпечував дотримання меж сегментів апаратно, це концептуальне розділення заклало основу для захисту пам'яті у захищеному режимі в пізніших процесорах.Ефективні 16-бітні операції: Вся арифметика над зміщеннями використовувала 16-бітні операції, які процесор міг виконувати ефективно. Множення сегмента на 16 було простою операцією зсуву.8086 надає чотири спеціалізовані сегментні регістри, кожен з яких виконує певну функцію у виконанні програми:<br><img alt="Налагоджувач AFD показує сегменти коду та даних" src="assets/l03-afd-segments-annotated.png" target="_self">
Ці сегментні регістри не є регістрами загального призначення — ви не можете використовувати їх для арифметики або як тимчасове сховище. Вони існують виключно для надання сегментної частини адрес пам'яті. Процесор автоматично вибирає, який сегментний регістр використовувати, залежно від типу доступу до пам'яті.Коли процесор вибирає наступну інструкцію, він автоматично використовує CS. Коли ваш код читає або записує дані (наприклад, MOV AX, [BX]), він автоматично використовує DS. При додаванні до стека або вилученні зі стека він автоматично використовує SS. Цей автоматичний вибір спрощує програмування, зберігаючи гнучкість адресації на основі сегментів.Типова програма для 8086 організовує пам'ять у окремі сегменти, які можуть перекриватися або не перекриватися:Сегмент коду (CS) містить виконувані інструкції програми. Процесор вибирає інструкції з адрес, обчислених як (CS × 16) + IP. Цей сегмент зазвичай розглядається як тільки для читання під час виконання — код, що самомодифікується, існує, але загалом вважається поганою практикою.Сегмент даних (DS) містить змінні, константи та рядки програми. Коли ви оголошуєте змінну мовою асемблера та звертаєтеся до неї за іменем, асемблер генерує код, який використовує DS як сегмент за замовчуванням. Тут розміщуються як ініціалізовані дані (змінні з початковими значеннями), так і неініціалізовані дані (зарезервований простір).Сегмент стека (SS) містить стек часу виконання — область пам'яті, що використовується для викликів функцій, адрес повернення та локальних змінних. Стек працює за дисципліною LIFO (останнім прийшов — першим вийшов) і зростає вниз у пам'яті (до нижчих адрес).Додатковий сегмент (ES) надає другий сегмент даних, особливо корисний для операцій з рядками, які потребують копіювання даних з однієї області пам'яті в іншу. Інструкції на кшталт MOVSB (переміщення байта рядка) читають з DS:SI і записують в ES:DI, дозволяючи ефективну передачу пам'ять-пам'ять.У малих програмах усі ці сегменти можуть вказувати на одну й ту саму область пам'яті («крихітна» модель пам'яті). У більших програмах вони вказують на повністю окремі області по 64 КБ, дозволяючи безпосередньо адресувати до 256 КБ пам'яті (4 сегменти × 64 КБ).При налагодженні коду 8086 ви побачите значення сегментних регістрів, відображені в налагоджувачі. Розуміння того, як обчислювати фізичні адреси з цих значень, є суттєвим для відстеження виконання програми.Розгляньмо налагоджувач, що показує:
CS = 0F78h
DS = 0F7Bh
IP = 0100h
Фізична адреса наступної інструкції:(CS × 16) + IP = (0F78h × 16) + 0100h = 0F780h + 0100h = 0F880h
Щоб зрозуміти співвідношення між сегментами коду та даних:DS - CS = 0F7Bh - 0F78h = 3 (сегменти)
Фізичний проміжок = 3 × 16 = 48 байтів
Це означає, що сегмент даних починається через 48 байтів після початку сегмента коду. У малій програмі цей проміжок може містити рівно 48 байтів коду, а дані йдуть одразу після.<br><img alt="Налагоджувач AFD показує обчислення відстані між сегментами" src="assets/l03-afd-data-segment.png" target="_self">Комбінація CS (сегмент коду) та IP (вказівник інструкцій) визначає, звідки процесор вибирає свою наступну інструкцію. Ця пара працює як нероздільна одиниця:Фізична адреса наступної інструкції = (CS × 16) + IPПроцесор автоматично збільшує IP після вибірки кожної інструкції. Коли IP досягає FFFFh і збільшується, він переходить до 0000h — але CS автоматично не змінюється. Це означає, що виконання залишається в межах поточного 64-КБ сегмента коду, якщо далекий перехід або виклик явно не завантажить нове значення CS.Інструкції переходу бувають двох типів:
Близькі переходи змінюють лише IP, залишаючись у межах поточного сегмента коду
Далекі переходи змінюють і CS, і IP, дозволяючи переходити в будь-яке місце адресного простору 1 МБ
Розуміння цієї відмінності є критичним при написанні або аналізі завантажувального коду, драйверів пристроїв або будь-якого програмного забезпечення, що має працювати через межі сегментів.Регістри SS (сегмент стека) та SP (вказівник стека) працюють разом для керування стеком програми:Фізична адреса вершини стека = (SS × 16) + SPСтек має кілька характерних особливостей:Зростання вниз: На відміну від більшості структур даних, що зростають вгору (до вищих адрес), стек x86 зростає вниз. Коли ви додаєте значення (push), SP зменшується. Коли ви вилучаєте значення (pop), SP збільшується. Ця домовленість бере початок від ранніх конструкцій комп'ютерів і зберігається в x86 донині.Вирівнювання по слову: На 8086 операції зі стеком працюють з 16-бітними словами. PUSH зменшує SP на 2, потім зберігає значення. POP отримує значення, потім збільшує SP на 2.Автоматичне використання: Процесор автоматично використовує SS:SP для кількох операцій:
Інструкції PUSH та POP
CALL (додає адресу повернення) та RET (вилучає адресу повернення)
Обробка переривань (додає прапорці, CS та IP)
Типова ініціалізація стека може встановити SS для вказування на область пам'яті, а SP — на вершину цієї області (наприклад, SP = FFFEh для майже повного 64-КБ сегмента стека). Під час виконання програмою функцій і додавання даних SP зменшується. Якщо SP досягає 0000h і програма додає більше даних, відбувається переповнення стека — одна з найвідоміших помилок в історії обчислень.Співвідношення між SS та SP ілюструє філософію сегмент:зміщення: SS надає базове розташування стека у фізичній пам'яті, тоді як SP відстежує поточну позицію в межах цієї області стека. Зміна SS — і ви перемикаєтеся на цілком інший стек. Зміна SP — і ви переміщуєтеся в межах поточного стека.Окрім регістрів загального призначення та сегментних регістрів, які ми вже розглянули, процесор 8086 містить спеціальний 16-бітний регістр, який називається Регістр прапорців (Flags Register, також відомий як Status Register або Program Status Word). Цей регістр не зберігає дані у звичному розумінні. Натомість кожен окремий біт слугує прапорцем, який вказує на певну умову або керує певною поведінкою процесора.Регістр прапорців є основою для прийняття рішень процесором. Коли ви пишете код на кшталт if (x == y) мовою високого рівня, компілятор транслює це в машинні інструкції, які порівнюють значення, а потім перевіряють відповідні прапорці, щоб визначити, яку гілку виконувати. Без регістра прапорців процесори не змогли б виконувати умовні операції.16 бітів регістра прапорців використовуються не всі. В 8086 визначено лише 9 бітів, залишаючи 7 бітів зарезервованими для майбутнього розширення (які Intel справді використав у пізніших процесорах, таких як 80286, 80386 та наступних). Визначені прапорці поділяються на дві категорії: прапорці стану, що відображають результати арифметичних і логічних операцій, та прапорці керування, що змінюють поведінку певних інструкцій [6].<br><img alt="Повний набір регістрів 8086, включаючи регістр FLAGS" src="assets/l03-8086-registers-full.png" target="_self">Повний набір регістрів 8086, що показує регістри загального призначення, сегментні регістри, регістри вказівників/індексів та регістр FLAGS внизу.Прапорці стану автоматично встановлюються або скидаються процесором після більшості арифметичних і логічних операцій. Програміст зазвичай не встановлює ці прапорці безпосередньо. Натомість він спостерігає за прапорцями, щоб визначити, що сталося під час попередньої операції.<br><img alt="Бітова структура регістра FLAGS" src="assets/l03-flags-register.jpeg" target="_self">Бітова структура регістра FLAGS 8086, що показує позицію кожного прапорця в 16-бітному регістрі. Прапорці стану (CF, PF, AF, ZF, SF, OF) та прапорці керування (TF, IF, DF) займають певні позиції бітів.Розуміння різниці між Carry та Overflow:Різниця між прапорцем переносу (CF) та прапорцем переповнення (OF) часто збиває з пантелику студентів, але вона критично важлива для правильного програмування:
CF виявляє проблеми в беззнаковій арифметиці. Якщо ви додаєте два 16-бітні беззнакові числа і результат перевищує 65 535, прапорець переносу встановлюється.
OF виявляє проблеми в знаковій арифметиці. Якщо ви додаєте два 16-бітні знакові числа і результат перевищує 32 767 або стає меншим за -32 768, прапорець переповнення встановлюється.
Процесор завжди встановлює обидва прапорці відповідно до їхніх визначень. Відповідальність програміста — перевіряти відповідний прапорець залежно від того, чи дані інтерпретуються як знакові чи беззнакові.Приклад: Додавання FFFFh + 0001h (інтерпретуючи як беззнакові: 65535 + 1):
Результат: 0000h з CF = 1 (відбувся перенос)
Прапорець OF буде 0, оскільки знакове переповнення не відбулося (інтерпретуючи FFFFh як -1 і додаючи 1, отримуємо 0, що є коректним)
Прапорці керування встановлюються або скидаються програмістом для зміни способу виконання процесором певних інструкцій. На відміну від прапорців стану, вони не змінюються автоматично на основі результатів обчислень.<br><img alt="Панель регістрів відлагоджувача, що показує всі регістри ЦП" src="assets/l03-afd-registers-view.png" target="_self">Панель регістрів відлагоджувача, що показує всі регістри ЦП, включаючи регістри загального призначення, сегментні регістри, вказівник інструкцій (IP) та регістр FLAGS. Цей вигляд допомагає програмістам спостерігати за значеннями регістрів і станами прапорців під час виконання програми.Справжня сила регістра прапорців проявляється в поєднанні з інструкціями умовних переходів. Саме так процесори реалізують прийняття рішень — еквівалент операторів if-else у мовах високого рівня [1].Інструкція CMP (Compare) є робочим конем умовної логіки. Вона виконує віднімання своїх двох операндів, але відкидає результат, зберігаючи лише вплив на прапорці. Це дозволяє порівнювати два значення без модифікації жодного з них.CMP AX, BX ; Внутрішньо обчислює AX - BX ; Встановлює прапорці на основі результату ; І AX, і BX залишаються незмінними
Після інструкції CMP різні інструкції умовних переходів перевіряють прапорці:Зверніть увагу, як знакові порівняння (JG, JL тощо) використовують прапорець знаку та прапорець переповнення, тоді як беззнакові порівняння (JA, JB тощо) використовують прапорець переносу. Це відображає різне значення одних і тих самих бітових комбінацій при інтерпретації як знакових і беззнакових цілих чисел.Розглянемо цей код високого рівня:if (x &gt; y) { // зробити щось
} else { // зробити щось інше
}
В асемблері 8086, припускаючи, що x знаходиться в AX, а y — в BX: CMP AX, BX ; Порівняти x з y JLE else_part ; Якщо x &lt;= y, перейти до else ; ... код для "зробити щось" (випадок x &gt; y) JMP end_if ; Пропустити частину else
else_part: ; ... код для "зробити щось інше"
end_if: ; ... продовжити з рештою програми
Інструкція CMP встановлює прапорці, обчислюючи AX - BX. Якщо AX &gt; BX (знакове порівняння), інструкція JLE НЕ виконає перехід (оскільки умова «менше або рівне» хибна), і виконання продовжиться з гілкою «then». Якщо AX &lt;= BX, JLE переходить до мітки else_part.При використанні Turbo Debugger (TD) у лабораторній роботі 3 ви побачите регістр прапорців, відображений у спеціальному форматі. Зазвичай відлагоджувачі показують прапорці як рядок літер, де:
Велика літера означає, що прапорець ВСТАНОВЛЕНИЙ (1)
Мала літера (або інший символ) означає, що прапорець СКИНУТИЙ (0)
Наприклад: Oditsz C може означати OF=1, DF=0, IF=0, TF=0, SF=0, ZF=0, CF=1.Спостереження за тим, як прапорці змінюються під час покрокового виконання інструкцій, є одним з найбільш повчальних способів зрозуміти, як процесор насправді приймає рішення.Ця лекція охопила широкий ландшафт — від компактної нотації шістнадцяткової системи до тонкощів сегментації 8086. Ось сім ключових концепцій, які формують основу для програмування на асемблері:1. Шістнадцяткова система — друг програмістаОдна шістнадцяткова цифра відповідає рівно чотирьом двійковим бітам, що робить hex природним скороченням для двійкових даних. Перетворення між hex і двійковою системою тривіальне після запам'ятовування: кожна hex-цифра незалежно відображається на свій 4-бітний патерн. Ця компактність є причиною того, чому адреси пам'яті, коди операцій інструкцій і вміст регістрів майже завжди відображаються в шістнадцятковій системі, а не в двійковій чи десятковій. Опанування hex не є опціональним для низькорівневого програмування — воно необхідне [1].2. Інструкції кодуються як бітові комбінаціїКожна асемблерна інструкція, яку ви пишете, стає певною комбінацією бітів при асемблюванні в машинний код. Ці бітові комбінації не довільні. Різні поля в слові інструкції спрямовуються до різних апаратних модулів: деякі біти вибирають операцію (додати, відняти, перемістити), інші вибирають задіяні регістри, а ще інші можуть містити безпосередні значення або інформацію про адресацію пам'яті. Розуміння того, що коди операцій мають структуру, допомагає демістифікувати, як процесори декодують і виконують інструкції.3. Регістри швидкі, тому що вони підключені безпосередньо до АЛПНа відміну від пам'яті, яка вимагає декодування адрес, транзакцій шини та кількох тактових циклів для доступу, регістри є фіксованими з'єднаннями всередині ядра процесора. Коли ви вказуєте AX в інструкції, немає процесу пошуку. АЛП вже має виділені шляхи для читання з AX та запису в AX. Цей архітектурний ярлик є причиною того, чому регістри на порядки швидші за пам'ять. Однак те саме пряме з'єднання, яке робить регістри швидкими, також обмежує їх кількість. Фізичний простір на чипі та кількість доступних з'єднань обмежують те, скільки регістрів може мати процесор.4. Пам'ять використовує 2D-адресацію для зменшення складності з'єднаньЯкби ми намагалися індивідуально підключити кожен байт у мегабайті пам'яті, нам знадобилося б понад мільйон провідників лише для адресації. Рішення з квадратною сіткою драматично зменшує цю складність: матриця 1024x1024 потребує лише близько 2048 адресних ліній (1024 для рядків + 1024 для стовпців) плюс лінії керування та даних. Двійкові декодери перетворюють біти адреси в сигнали вибору рядка та стовпця, і вентилі AND на кожному перетині забезпечують, що лише одна вибрана комірка реагує на операції читання чи запису [2].5. 8086 використовує адресацію сегмент:зміщення для розширення своїх можливостейПроцесор 8086 має 16-бітні регістри, але потребував адресувати 1 МБ пам'яті, що вимагає 20-бітних адрес. Схема сегмент:зміщення вирішує цю невідповідність: два 16-бітні значення комбінуються за формулою Фізична адреса = (Сегмент x 16) + Зміщення, щоб отримати 20-бітний результат. Цей дизайн був прагматичним інженерним компромісом, який дозволив зворотну сумісність з 8-бітним програмним забезпеченням, одночасно розширюючи адресовану пам'ять. Одна й та сама фізична адреса може бути представлена багатьма різними парами сегмент:зміщення, оскільки сегменти перекриваються кожні 16 байтів [3], [4].6. Сегментні регістри розділяють області пам'яті коду, даних і стекаЧотири сегментні регістри (CS, DS, SS, ES) встановлюють чотири одночасні «вікна» в 1 МБ адресного простору. CS вказує на сегмент коду, звідки вибираються інструкції; DS вказує на сегмент даних, де зберігаються змінні; SS вказує на сегмент стека для викликів підпрограм і локальних змінних. Це розділення є більш ніж організаційною зручністю. Воно дозволяє переміщувати програми просто змінюючи значення сегментних регістрів, забезпечує обмежений захист пам'яті (код не може випадково перезаписати себе, якщо сегменти не перекриваються) і проклало шлях для захищеного режиму, введеного в 80286 [5].7. Прапорці записують результати операцій для прийняття рішеньРегістр прапорців — це короткочасна пам'ять процесора про те, що щойно сталося. Після кожної арифметичної чи логічної операції прапорці стану автоматично оновлюються, відображаючи, чи результат був нульовим, від'ємним, спричинив переповнення тощо. Інструкції умовних переходів потім перевіряють ці прапорці для визначення потоку програми. Цей механізм — те, як машинний код реалізує всі форми умовного виконання, від простих операторів if до складних циклів. Без прапорців процесори могли б виконувати лише прямолінійний код.У лабораторній роботі 3 ви налаштуєте повне середовище розробки 16-бітного асемблера. Це включає кілька інструментів, які працюють разом:TASM (Turbo Assembler):
Асемблер читає ваші вихідні файли .ASM, що містять людиночитабельний асемблерний код, і транслює їх у об'єктні файли .OBJ, що містять машинний код. TASM розуміє синтаксис Intel і підтримує макроси, умовне асемблювання та різні опції відлагодження. Коли ви запускаєте TASM на вашому вихідному файлі, він виконує лексичний аналіз, перевірку синтаксису, розв'язання символів і генерацію коду.TLINK (Turbo Linker):
Компонувальник бере один або більше файлів .OBJ і комбінує їх у виконуваний файл .EXE. Навіть для однофайлової програми компонувальник необхідний, оскільки він розв'язує адреси сегментів, налаштовує правильний заголовок виконуваного файлу, який очікує DOS, і може комбінувати ваш код з бібліотечними підпрограмами за потреби.DOSBox:
Сучасні операційні системи (Windows 10/11, macOS, Linux) не можуть безпосередньо запускати 16-бітні програми DOS. DOSBox — це емулятор x86, який відтворює середовище DOS, включаючи поведінку ЦП 8086/80286, структуру пам'яті та системні виклики DOS. Коли ви запускаєте вашу асемблльовану програму в DOSBox, вона виконується так, ніби на ПК 1980-х років.Turbo Debugger (TD):
TD — це візуальний відлагоджувач, який дозволяє проходити вашу програму по одній інструкції за раз. Ви можете бачити поточні значення всіх регістрів, спостерігати за змінами вмісту пам'яті, встановлювати точки зупинки та спостерігати, як кожна інструкція впливає на стан процесора. Це безцінно для розуміння того, що ваш код насправді робить, і для пошуку помилок.Лабораторне заняття проведе вас через повний цикл розробки: Написати вашу першу асемблерну програму — Мінімальна програма, що демонструє операції з регістрами, такі як завантаження значень у регістри та виконання простої арифметики. Асемблювати програму — Запустити TASM для перетворення вашого файлу .ASM у файл .OBJ. Навчитися інтерпретувати повідомлення про помилки та попередження асемблера. Скомпонувати програму — Запустити TLINK для створення виконуваного файлу .EXE з файлу .OBJ. Запустити програму — Виконати вашу програму в DOSBox і переконатися, що вона завершується без збоїв. Відлагодити програму — Завантажити вашу програму в Turbo Debugger і пройти її інструкція за інструкцією. Спостерігати за змінами регістрів, прапорцями та тим, як вказівник інструкцій просувається через ваш код. Перед відвідуванням лабораторної роботи приділіть час обдумуванню цих питань. Вони націлені на поширені помилкові уявлення і допоможуть вам глибше зануритися в матеріал:1. Режими адресації: Яка різниця між MOV AX, 5 і MOV AX, [5]?Це критична відмінність в асемблері:
MOV AX, 5 — це безпосередня адресація. Саме значення 5 завантажується безпосередньо в AX. Після цієї інструкції AX містить 0005h.
MOV AX, [5] — це пряма адресація пам'яті. Квадратні дужки вказують, що 5 — це адреса пам'яті, і процесор завантажує 16-бітне значення, збережене за цією адресою, в AX. Після цієї інструкції AX містить те значення, яке було в пам'яті за адресою DS:0005.
Квадратні дужки не є опціональними чи декоративними. Вони фундаментально змінюють значення інструкції.2. Переповнення: Що відбувається, коли ви додаєте два числа, які переповнюються?Розглянемо додавання FFFFh + 0001h у 16-бітному регістрі:
Математичний результат — 10000h, що вимагає 17 бітів
Але 16-бітний регістр може містити лише 16 бітів
Результат у регістрі — 0000h (молодші 16 бітів)
Прапорець переносу (CF) встановлюється в 1, вказуючи на беззнакове переповнення
Якщо інтерпретувати як знакові значення (FFFFh = -1, 0001h = 1), результат 0000h = 0 є правильним, і прапорець переповнення (OF) дорівнює 0
Процесор не зупиняється і не генерує помилку при переповненні. Він просто встановлює відповідні прапорці. Відповідальність програміста — перевіряти ці прапорці, якщо потрібне виявлення переповнення.3. Обчислення адреси: Маючи CS = 0F78h та IP = 0010h, яка фізична адреса наступної інструкції?Застосуємо формулу сегмент:зміщення:
Фізична адреса = (Сегмент x 16) + Зміщення
Фізична адреса = (0F78h x 10h) + 0010h
Фізична адреса = 0F780h + 0010h
Фізична адреса = 0F790h
У десятковій: (3960 x 16) + 16 = 63360 + 16 = 63376Наступна інструкція, яку процесор вибере, знаходиться за фізичною адресою 0F790h у пам'яті. Ця адреса була обчислена блоком шинного інтерфейсу процесора з використанням значень у CS та IP.
<br>[1] <a data-tooltip-position="top" aria-label="https://www.cs.virginia.edu/~evans/cs216/guides/x86.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.cs.virginia.edu/~evans/cs216/guides/x86.html" target="_self">Guide to x86 Assembly</a> — University of Virginia CS216. Чудовий вступ до програмування на асемблері x86, що охоплює регістри, адресацію пам'яті, інструкції та угоди про виклики. Написаний для 32-бітного x86, але фундаментальні концепції застосовуються безпосередньо до 16-бітного програмування. <br>[2] <a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=rM9BjciBLmg" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=rM9BjciBLmg" target="_self">How Computers Remember — Memory, Bus, and Addressing</a> (YouTube). Візуальне пояснення того, як оперативна пам'ять організована як 2D-матриця, як працюють декодери адрес і як дані переміщуються між процесором і пам'яттю. <br>[3] <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Intel_8086" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Intel_8086" target="_self">Intel 8086 — Wikipedia</a>. Всебічний історичний і технічний огляд процесора 8086, включаючи його архітектуру, набір інструкцій і тривалий вплив на сімейство x86. <br>[4] <a data-tooltip-position="top" aria-label="https://thestarman.pcministry.com/asm/debug/Segments.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://thestarman.pcministry.com/asm/debug/Segments.html" target="_self">Removing the Mystery from SEGMENT:OFFSET Addressing</a>. Детальний посібник, що пояснює, як працює адресація сегмент:зміщення, з численними прикладами та візуалізаціями сегментів, що перекриваються. Обов'язкове читання для розуміння адресації пам'яті 8086. [5] 8086 Microprocessor Basics (різні освітні джерела). Загальні посилання на архітектуру 8086, включаючи організацію регістрів, структуру шини та виконання інструкцій. <br>
[6] <a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/flag-register-8086-microprocessor/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/flag-register-8086-microprocessor/" target="_self">8086 Flags Register</a> — GeeksforGeeks. Детальне пояснення кожного прапорця в регістрі прапорців 8086, включаючи те, коли кожен прапорець встановлюється і як умовні переходи їх використовують. Ця лекція відповідає наступним результатам навчання з навчальної програми курсу:РН1: Пояснити принципи побудови та функціонування сучасних архітектур комп'ютерівСтуденти тепер повинні вміти:
Описати організацію та призначення регістрів x86 (загального призначення, сегментних, прапорців)
Пояснити, як пам'ять організована як 2D-матриця з адресацією рядків/стовпців
Сформулювати, чому регістри швидші за пам'ять (пряме з'єднання з АЛП проти декодування адрес)
Пояснити механізм адресації сегмент:зміщення та чому він був розроблений
РН2: Виконувати перетворення даних між форматами та аналізувати представлення на машинному рівніСтуденти тепер повинні вміти:
Вільно перетворювати між шістнадцятковим, двійковим і десятковим представленнями
Обчислювати фізичні адреси з пар сегмент:зміщення
Інтерпретувати значення регістра прапорців та розуміти їхнє значення
Відстежувати прості послідовності інструкцій і передбачати зміни регістрів/прапорців
РН3: Писати та відлагоджувати програми на асемблері x86Ця лекція забезпечує основу для:
Розуміння того, які регістри доступні та їхнє традиційне використання
Знання того, як адресується пам'ять у програмах 8086
Розпізнавання того, як умовне виконання працює через прапорці та умовні переходи
Підготовки до практичного програмування на асемблері в лабораторній роботі 3
Інформація про курс:
Національний університет «Києво-Могилянська академія»
Факультет інформатики
Освітня програма: Комп'ютерні науки, бакалаврат
Структура курсу:
Повний курс (Комп'ютерні науки, Інженерія програмного забезпечення): 6 ECTS / 180 годин
Скорочений курс (АКІТР): 4 ECTS / 120 годин
]]></description><link>lectures/l03.-архітектура-x86-—-регістри-та-пам'ять.html</link><guid isPermaLink="false">Lectures/L03. Архітектура x86 — регістри та пам'ять.md</guid><pubDate>Wed, 28 Jan 2026 21:16:16 GMT</pubDate><enclosure url="assets/l03-hex-binary-conversion.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-hex-binary-conversion.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[_temp_uk_part_06]]></title><description><![CDATA[Окрім регістрів загального призначення та сегментних регістрів, які ми вже розглянули, процесор 8086 містить спеціальний 16-бітний регістр, який називається Регістр прапорців (Flags Register, також відомий як Status Register або Program Status Word). Цей регістр не зберігає дані у звичному розумінні. Натомість кожен окремий біт слугує прапорцем, який вказує на певну умову або керує певною поведінкою процесора.Регістр прапорців є основою для прийняття рішень процесором. Коли ви пишете код на кшталт if (x == y) мовою високого рівня, компілятор транслює це в машинні інструкції, які порівнюють значення, а потім перевіряють відповідні прапорці, щоб визначити, яку гілку виконувати. Без регістра прапорців процесори не змогли б виконувати умовні операції.16 бітів регістра прапорців використовуються не всі. В 8086 визначено лише 9 бітів, залишаючи 7 бітів зарезервованими для майбутнього розширення (які Intel справді використав у пізніших процесорах, таких як 80286, 80386 та наступних). Визначені прапорці поділяються на дві категорії: прапорці стану, що відображають результати арифметичних і логічних операцій, та прапорці керування, що змінюють поведінку певних інструкцій [6].<img alt="Повний набір регістрів 8086, включаючи регістр FLAGS" src="assets/l03-8086-registers-full.png" target="_self">Повний набір регістрів 8086, що показує регістри загального призначення, сегментні регістри, регістри вказівників/індексів та регістр FLAGS внизу.Прапорці стану автоматично встановлюються або скидаються процесором після більшості арифметичних і логічних операцій. Програміст зазвичай не встановлює ці прапорці безпосередньо. Натомість він спостерігає за прапорцями, щоб визначити, що сталося під час попередньої операції.<br><img alt="Бітова структура регістра FLAGS" src="assets/l03-flags-register.jpeg" target="_self">Бітова структура регістра FLAGS 8086, що показує позицію кожного прапорця в 16-бітному регістрі. Прапорці стану (CF, PF, AF, ZF, SF, OF) та прапорці керування (TF, IF, DF) займають певні позиції бітів.Розуміння різниці між Carry та Overflow:Різниця між прапорцем переносу (CF) та прапорцем переповнення (OF) часто збиває з пантелику студентів, але вона критично важлива для правильного програмування:
CF виявляє проблеми в беззнаковій арифметиці. Якщо ви додаєте два 16-бітні беззнакові числа і результат перевищує 65 535, прапорець переносу встановлюється.
OF виявляє проблеми в знаковій арифметиці. Якщо ви додаєте два 16-бітні знакові числа і результат перевищує 32 767 або стає меншим за -32 768, прапорець переповнення встановлюється.
Процесор завжди встановлює обидва прапорці відповідно до їхніх визначень. Відповідальність програміста — перевіряти відповідний прапорець залежно від того, чи дані інтерпретуються як знакові чи беззнакові.Приклад: Додавання FFFFh + 0001h (інтерпретуючи як беззнакові: 65535 + 1):
Результат: 0000h з CF = 1 (відбувся перенос)
Прапорець OF буде 0, оскільки знакове переповнення не відбулося (інтерпретуючи FFFFh як -1 і додаючи 1, отримуємо 0, що є коректним)
Прапорці керування встановлюються або скидаються програмістом для зміни способу виконання процесором певних інструкцій. На відміну від прапорців стану, вони не змінюються автоматично на основі результатів обчислень.<br><img alt="Панель регістрів відлагоджувача, що показує всі регістри ЦП" src="assets/l03-afd-registers-view.png" target="_self">Панель регістрів відлагоджувача, що показує всі регістри ЦП, включаючи регістри загального призначення, сегментні регістри, вказівник інструкцій (IP) та регістр FLAGS. Цей вигляд допомагає програмістам спостерігати за значеннями регістрів і станами прапорців під час виконання програми.Справжня сила регістра прапорців проявляється в поєднанні з інструкціями умовних переходів. Саме так процесори реалізують прийняття рішень — еквівалент операторів if-else у мовах високого рівня [1].Інструкція CMP (Compare) є робочим конем умовної логіки. Вона виконує віднімання своїх двох операндів, але відкидає результат, зберігаючи лише вплив на прапорці. Це дозволяє порівнювати два значення без модифікації жодного з них.CMP AX, BX ; Внутрішньо обчислює AX - BX ; Встановлює прапорці на основі результату ; І AX, і BX залишаються незмінними
Після інструкції CMP різні інструкції умовних переходів перевіряють прапорці:Зверніть увагу, як знакові порівняння (JG, JL тощо) використовують прапорець знаку та прапорець переповнення, тоді як беззнакові порівняння (JA, JB тощо) використовують прапорець переносу. Це відображає різне значення одних і тих самих бітових комбінацій при інтерпретації як знакових і беззнакових цілих чисел.Розглянемо цей код високого рівня:if (x &gt; y) { // зробити щось
} else { // зробити щось інше
}
В асемблері 8086, припускаючи, що x знаходиться в AX, а y — в BX: CMP AX, BX ; Порівняти x з y JLE else_part ; Якщо x &lt;= y, перейти до else ; ... код для "зробити щось" (випадок x &gt; y) JMP end_if ; Пропустити частину else
else_part: ; ... код для "зробити щось інше"
end_if: ; ... продовжити з рештою програми
Інструкція CMP встановлює прапорці, обчислюючи AX - BX. Якщо AX &gt; BX (знакове порівняння), інструкція JLE НЕ виконає перехід (оскільки умова «менше або рівне» хибна), і виконання продовжиться з гілкою «then». Якщо AX &lt;= BX, JLE переходить до мітки else_part.При використанні Turbo Debugger (TD) у лабораторній роботі 3 ви побачите регістр прапорців, відображений у спеціальному форматі. Зазвичай відлагоджувачі показують прапорці як рядок літер, де:
Велика літера означає, що прапорець ВСТАНОВЛЕНИЙ (1)
Мала літера (або інший символ) означає, що прапорець СКИНУТИЙ (0)
Наприклад: Oditsz C може означати OF=1, DF=0, IF=0, TF=0, SF=0, ZF=0, CF=1.Спостереження за тим, як прапорці змінюються під час покрокового виконання інструкцій, є одним з найбільш повчальних способів зрозуміти, як процесор насправді приймає рішення.Ця лекція охопила широкий ландшафт — від компактної нотації шістнадцяткової системи до тонкощів сегментації 8086. Ось сім ключових концепцій, які формують основу для програмування на асемблері:1. Шістнадцяткова система — друг програмістаОдна шістнадцяткова цифра відповідає рівно чотирьом двійковим бітам, що робить hex природним скороченням для двійкових даних. Перетворення між hex і двійковою системою тривіальне після запам'ятовування: кожна hex-цифра незалежно відображається на свій 4-бітний патерн. Ця компактність є причиною того, чому адреси пам'яті, коди операцій інструкцій і вміст регістрів майже завжди відображаються в шістнадцятковій системі, а не в двійковій чи десятковій. Опанування hex не є опціональним для низькорівневого програмування — воно необхідне [1].2. Інструкції кодуються як бітові комбінаціїКожна асемблерна інструкція, яку ви пишете, стає певною комбінацією бітів при асемблюванні в машинний код. Ці бітові комбінації не довільні. Різні поля в слові інструкції спрямовуються до різних апаратних модулів: деякі біти вибирають операцію (додати, відняти, перемістити), інші вибирають задіяні регістри, а ще інші можуть містити безпосередні значення або інформацію про адресацію пам'яті. Розуміння того, що коди операцій мають структуру, допомагає демістифікувати, як процесори декодують і виконують інструкції.3. Регістри швидкі, тому що вони підключені безпосередньо до АЛПНа відміну від пам'яті, яка вимагає декодування адрес, транзакцій шини та кількох тактових циклів для доступу, регістри є фіксованими з'єднаннями всередині ядра процесора. Коли ви вказуєте AX в інструкції, немає процесу пошуку. АЛП вже має виділені шляхи для читання з AX та запису в AX. Цей архітектурний ярлик є причиною того, чому регістри на порядки швидші за пам'ять. Однак те саме пряме з'єднання, яке робить регістри швидкими, також обмежує їх кількість. Фізичний простір на чипі та кількість доступних з'єднань обмежують те, скільки регістрів може мати процесор.4. Пам'ять використовує 2D-адресацію для зменшення складності з'єднаньЯкби ми намагалися індивідуально підключити кожен байт у мегабайті пам'яті, нам знадобилося б понад мільйон провідників лише для адресації. Рішення з квадратною сіткою драматично зменшує цю складність: матриця 1024x1024 потребує лише близько 2048 адресних ліній (1024 для рядків + 1024 для стовпців) плюс лінії керування та даних. Двійкові декодери перетворюють біти адреси в сигнали вибору рядка та стовпця, і вентилі AND на кожному перетині забезпечують, що лише одна вибрана комірка реагує на операції читання чи запису [2].5. 8086 використовує адресацію сегмент:зміщення для розширення своїх можливостейПроцесор 8086 має 16-бітні регістри, але потребував адресувати 1 МБ пам'яті, що вимагає 20-бітних адрес. Схема сегмент:зміщення вирішує цю невідповідність: два 16-бітні значення комбінуються за формулою Фізична адреса = (Сегмент x 16) + Зміщення, щоб отримати 20-бітний результат. Цей дизайн був прагматичним інженерним компромісом, який дозволив зворотну сумісність з 8-бітним програмним забезпеченням, одночасно розширюючи адресовану пам'ять. Одна й та сама фізична адреса може бути представлена багатьма різними парами сегмент:зміщення, оскільки сегменти перекриваються кожні 16 байтів [3], [4].6. Сегментні регістри розділяють області пам'яті коду, даних і стекаЧотири сегментні регістри (CS, DS, SS, ES) встановлюють чотири одночасні «вікна» в 1 МБ адресного простору. CS вказує на сегмент коду, звідки вибираються інструкції; DS вказує на сегмент даних, де зберігаються змінні; SS вказує на сегмент стека для викликів підпрограм і локальних змінних. Це розділення є більш ніж організаційною зручністю. Воно дозволяє переміщувати програми просто змінюючи значення сегментних регістрів, забезпечує обмежений захист пам'яті (код не може випадково перезаписати себе, якщо сегменти не перекриваються) і проклало шлях для захищеного режиму, введеного в 80286 [5].7. Прапорці записують результати операцій для прийняття рішеньРегістр прапорців — це короткочасна пам'ять процесора про те, що щойно сталося. Після кожної арифметичної чи логічної операції прапорці стану автоматично оновлюються, відображаючи, чи результат був нульовим, від'ємним, спричинив переповнення тощо. Інструкції умовних переходів потім перевіряють ці прапорці для визначення потоку програми. Цей механізм — те, як машинний код реалізує всі форми умовного виконання, від простих операторів if до складних циклів. Без прапорців процесори могли б виконувати лише прямолінійний код.У лабораторній роботі 3 ви налаштуєте повне середовище розробки 16-бітного асемблера. Це включає кілька інструментів, які працюють разом:TASM (Turbo Assembler):
Асемблер читає ваші вихідні файли .ASM, що містять людиночитабельний асемблерний код, і транслює їх у об'єктні файли .OBJ, що містять машинний код. TASM розуміє синтаксис Intel і підтримує макроси, умовне асемблювання та різні опції відлагодження. Коли ви запускаєте TASM на вашому вихідному файлі, він виконує лексичний аналіз, перевірку синтаксису, розв'язання символів і генерацію коду.TLINK (Turbo Linker):
Компонувальник бере один або більше файлів .OBJ і комбінує їх у виконуваний файл .EXE. Навіть для однофайлової програми компонувальник необхідний, оскільки він розв'язує адреси сегментів, налаштовує правильний заголовок виконуваного файлу, який очікує DOS, і може комбінувати ваш код з бібліотечними підпрограмами за потреби.DOSBox:
Сучасні операційні системи (Windows 10/11, macOS, Linux) не можуть безпосередньо запускати 16-бітні програми DOS. DOSBox — це емулятор x86, який відтворює середовище DOS, включаючи поведінку ЦП 8086/80286, структуру пам'яті та системні виклики DOS. Коли ви запускаєте вашу асемблльовану програму в DOSBox, вона виконується так, ніби на ПК 1980-х років.Turbo Debugger (TD):
TD — це візуальний відлагоджувач, який дозволяє проходити вашу програму по одній інструкції за раз. Ви можете бачити поточні значення всіх регістрів, спостерігати за змінами вмісту пам'яті, встановлювати точки зупинки та спостерігати, як кожна інструкція впливає на стан процесора. Це безцінно для розуміння того, що ваш код насправді робить, і для пошуку помилок.Лабораторне заняття проведе вас через повний цикл розробки: Написати вашу першу асемблерну програму — Мінімальна програма, що демонструє операції з регістрами, такі як завантаження значень у регістри та виконання простої арифметики. Асемблювати програму — Запустити TASM для перетворення вашого файлу .ASM у файл .OBJ. Навчитися інтерпретувати повідомлення про помилки та попередження асемблера. Скомпонувати програму — Запустити TLINK для створення виконуваного файлу .EXE з файлу .OBJ. Запустити програму — Виконати вашу програму в DOSBox і переконатися, що вона завершується без збоїв. Відлагодити програму — Завантажити вашу програму в Turbo Debugger і пройти її інструкція за інструкцією. Спостерігати за змінами регістрів, прапорцями та тим, як вказівник інструкцій просувається через ваш код. Перед відвідуванням лабораторної роботи приділіть час обдумуванню цих питань. Вони націлені на поширені помилкові уявлення і допоможуть вам глибше зануритися в матеріал:1. Режими адресації: Яка різниця між MOV AX, 5 і MOV AX, [5]?Це критична відмінність в асемблері:
MOV AX, 5 — це безпосередня адресація. Саме значення 5 завантажується безпосередньо в AX. Після цієї інструкції AX містить 0005h.
MOV AX, [5] — це пряма адресація пам'яті. Квадратні дужки вказують, що 5 — це адреса пам'яті, і процесор завантажує 16-бітне значення, збережене за цією адресою, в AX. Після цієї інструкції AX містить те значення, яке було в пам'яті за адресою DS:0005.
Квадратні дужки не є опціональними чи декоративними. Вони фундаментально змінюють значення інструкції.2. Переповнення: Що відбувається, коли ви додаєте два числа, які переповнюються?Розглянемо додавання FFFFh + 0001h у 16-бітному регістрі:
Математичний результат — 10000h, що вимагає 17 бітів
Але 16-бітний регістр може містити лише 16 бітів
Результат у регістрі — 0000h (молодші 16 бітів)
Прапорець переносу (CF) встановлюється в 1, вказуючи на беззнакове переповнення
Якщо інтерпретувати як знакові значення (FFFFh = -1, 0001h = 1), результат 0000h = 0 є правильним, і прапорець переповнення (OF) дорівнює 0
Процесор не зупиняється і не генерує помилку при переповненні. Він просто встановлює відповідні прапорці. Відповідальність програміста — перевіряти ці прапорці, якщо потрібне виявлення переповнення.3. Обчислення адреси: Маючи CS = 0F78h та IP = 0010h, яка фізична адреса наступної інструкції?Застосуємо формулу сегмент:зміщення:
Фізична адреса = (Сегмент x 16) + Зміщення
Фізична адреса = (0F78h x 10h) + 0010h
Фізична адреса = 0F780h + 0010h
Фізична адреса = 0F790h
У десятковій: (3960 x 16) + 16 = 63360 + 16 = 63376Наступна інструкція, яку процесор вибере, знаходиться за фізичною адресою 0F790h у пам'яті. Ця адреса була обчислена блоком шинного інтерфейсу процесора з використанням значень у CS та IP.
<br>[1] <a data-tooltip-position="top" aria-label="https://www.cs.virginia.edu/~evans/cs216/guides/x86.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.cs.virginia.edu/~evans/cs216/guides/x86.html" target="_self">Guide to x86 Assembly</a> — University of Virginia CS216. Чудовий вступ до програмування на асемблері x86, що охоплює регістри, адресацію пам'яті, інструкції та угоди про виклики. Написаний для 32-бітного x86, але фундаментальні концепції застосовуються безпосередньо до 16-бітного програмування. <br>[2] <a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=rM9BjciBLmg" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=rM9BjciBLmg" target="_self">How Computers Remember — Memory, Bus, and Addressing</a> (YouTube). Візуальне пояснення того, як оперативна пам'ять організована як 2D-матриця, як працюють декодери адрес і як дані переміщуються між процесором і пам'яттю. <br>[3] <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Intel_8086" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Intel_8086" target="_self">Intel 8086 — Wikipedia</a>. Всебічний історичний і технічний огляд процесора 8086, включаючи його архітектуру, набір інструкцій і тривалий вплив на сімейство x86. <br>[4] <a data-tooltip-position="top" aria-label="https://thestarman.pcministry.com/asm/debug/Segments.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://thestarman.pcministry.com/asm/debug/Segments.html" target="_self">Removing the Mystery from SEGMENT:OFFSET Addressing</a>. Детальний посібник, що пояснює, як працює адресація сегмент:зміщення, з численними прикладами та візуалізаціями сегментів, що перекриваються. Обов'язкове читання для розуміння адресації пам'яті 8086. [5] 8086 Microprocessor Basics (різні освітні джерела). Загальні посилання на архітектуру 8086, включаючи організацію регістрів, структуру шини та виконання інструкцій. <br>
[6] <a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/flag-register-8086-microprocessor/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/flag-register-8086-microprocessor/" target="_self">8086 Flags Register</a> — GeeksforGeeks. Детальне пояснення кожного прапорця в регістрі прапорців 8086, включаючи те, коли кожен прапорець встановлюється і як умовні переходи їх використовують. Ця лекція відповідає наступним результатам навчання з навчальної програми курсу:РН1: Пояснити принципи побудови та функціонування сучасних архітектур комп'ютерівСтуденти тепер повинні вміти:
Описати організацію та призначення регістрів x86 (загального призначення, сегментних, прапорців)
Пояснити, як пам'ять організована як 2D-матриця з адресацією рядків/стовпців
Сформулювати, чому регістри швидші за пам'ять (пряме з'єднання з АЛП проти декодування адрес)
Пояснити механізм адресації сегмент:зміщення та чому він був розроблений
РН2: Виконувати перетворення даних між форматами та аналізувати представлення на машинному рівніСтуденти тепер повинні вміти:
Вільно перетворювати між шістнадцятковим, двійковим і десятковим представленнями
Обчислювати фізичні адреси з пар сегмент:зміщення
Інтерпретувати значення регістра прапорців та розуміти їхнє значення
Відстежувати прості послідовності інструкцій і передбачати зміни регістрів/прапорців
РН3: Писати та відлагоджувати програми на асемблері x86Ця лекція забезпечує основу для:
Розуміння того, які регістри доступні та їхнє традиційне використання
Знання того, як адресується пам'ять у програмах 8086
Розпізнавання того, як умовне виконання працює через прапорці та умовні переходи
Підготовки до практичного програмування на асемблері в лабораторній роботі 3
Інформація про курс:
Національний університет «Києво-Могилянська академія»
Факультет інформатики
Освітня програма: Комп'ютерні науки, бакалаврат
Структура курсу:
Повний курс (Комп'ютерні науки, Інженерія програмного забезпечення): 6 ECTS / 180 годин
Скорочений курс (АКІТР): 4 ECTS / 120 годин
]]></description><link>lectures/_temp_uk_part_06.html</link><guid isPermaLink="false">Lectures/_temp_uk_part_06.md</guid><pubDate>Wed, 28 Jan 2026 21:15:57 GMT</pubDate><enclosure url="assets/l03-8086-registers-full.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-8086-registers-full.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[_temp_uk_part_03]]></title><description><![CDATA[Регістри займають унікальне місце в ієрархії памʼяті компʼютера — вони є найшвидшими доступними місцями зберігання, проте їхня кількість суворо обмежена. Розуміння причин цього компромісу розкриває фундаментальні принципи архітектури комп'ютерів, які застосовуються далеко за межами сімейства x86.Перевага регістрів у швидкості випливає з їхньої фізичної реалізації та розташування всередині процесора:Пряме зʼєднання з АЛП. Регістри напряму підключені до арифметико-логічного пристрою (АЛП) та інших функціональних блоків процесора. Немає декодування адреси, немає контролера памʼяті, немає арбітражу шини — лише прямі електричні зʼєднання. Коли інструкція вказує «додати EAX і EBX», дані негайно надходять з цих регістрів на входи АЛП, а результат повертається до регістра призначення.Без проходження шини памʼяті. Доступ до основної памʼяті вимагає надсилання адреси через шину памʼяті, очікування декодування адреси контролером памʼяті, очікування отримання даних мікросхемами памʼяті, а потім очікування повернення даних назад через шину. Цей цикл займає десятки або сотні тактів. Регістри обходять увесь цей процес, оскільки фізично розташовані всередині ядра процесора.Доступ за один такт. Сучасні процесори можуть читати з регістрів і записувати в них протягом одного такту — і часто виконують кілька операцій з регістрами одночасно. Типова інструкція може читати два регістри-джерела, виконувати операцію та записувати результат у регістр призначення — усе за один такт. Такий паралелізм можливий саме тому, що регістри мають виділені, завжди доступні зʼєднання.Фізична близькість. На кристалі процесора регістри розташовані лише за кілька мікрометрів від виконавчих блоків. Електричні сигнали поширюються зі значною часткою швидкості світла, але навіть на таких швидкостях відстань має значення. Кілька міліметрів до основної памʼяті створюють помітну затримку порівняно з мікроскопічними відстанями всередині ядра процесора.Щоб оцінити різницю у швидкості, розгляньмо типові часи доступу в сучасній системі:Регістри буквально на порядки швидші за будь-яке інше сховище в системі.Враховуючи їхню надзвичайну швидкість, чому процесори не мають тисячі регістрів? Відповідь повʼязана з кількома інженерними обмеженнями:Складність фізичних зʼєднань. Кожен регістр має бути зʼєднаний з кожним виконавчим блоком інструкцій, який може його використовувати. Якщо АЛП потрібно додати два регістри, обидва регістри повинні мати шляхи передачі даних до входів АЛП. Якщо будь-який з 16 регістрів загального призначення може бути джерелом або призначенням, то 16 окремих шляхів даних повинні існувати для кожної точки зʼєднання. Додавання більшої кількості регістрів означає експоненційно більше зʼєднувальних провідників.Розгляньмо спрощений розрахунок: з 8 регістрами потрібна логіка маршрутизації, яка може вибрати будь-яке з 8 джерел для кожного з двох входів АЛП — це 8 × 2 = 16 входів мультиплексора лише для АЛП. З 16 регістрами це подвоюється до 32 входів. Але регістри також зʼєднані з блоками памʼяті, блоками операцій з рухомою комою та різними іншими функціональними блоками. Загальна кількість зʼєднань швидко зростає з кожним додатковим регістром.Обмеження кодування інструкцій. Кожна інструкція повинна вказувати, які регістри використовувати, і ця специфікація займає біти в машинному коді. З 4 регістрами потрібно 2 біти для ідентифікації кожного регістра (оскільки 2² = 4). З 8 регістрами потрібно 3 біти (2³ = 8). З 16 регістрами потрібно 4 біти (2⁴ = 16).Інструкція, яка використовує три регістри (два джерела та одне призначення), потребуватиме 6 додаткових бітів з 8 регістрами проти 12 додаткових бітів з 64 регістрами. Більші інструкції означають більше пропускної здатності памʼяті, витраченої на завантаження коду, більший кеш інструкцій і потенційно повільніше виконання.Фізичний простір на кристалі. Кожен регістр потребує реальної площі кремнію — транзисторів для зберігання бітів, драйверів для виведення значень, приймачів для отримання входів. Хоча один регістр є крихітним порівняно з загальним кристалом процесора, логіка маршрутизації та мультиплексори, що зʼєднують регістри з виконавчими блоками, займають значну площу. Цей простір можна було б використати для більших кешів, більшої кількості виконавчих блоків або інших функцій, які могли б забезпечити вищу загальну продуктивність.Спадна віддача. Дослідження компіляторів показали, що більшість функцій можна ефективно скомпілювати, використовуючи лише скромну кількість регістрів. Після певної межі (приблизно 16-32 регістри для більшості навантажень) додавання більшої кількості регістрів дає мінімальну користь, оскільки компілятор уже може зберігати найчастіше використовувані змінні в регістрах. Додаткова складність не виправдовується граничним покращенням.Взаємозвʼязок між регістрами та памʼяттю представляє один з найфундаментальніших компромісів в архітектурі компʼютерів:Регістри: Надзвичайно швидкі (субнаносекундний доступ), але суворо обмежені в кількості (16 регістрів загального призначення в x86-64). Кожне значення в регістрі негайно доступне для обчислень.Памʼять: Значно повільніша (десятки до сотень наносекунд для основної памʼяті), але набагато більша за ємністю (гігабайти до терабайтів). Значення повинні бути явно завантажені з памʼяті перед використанням і збережені назад після модифікації.Цей компроміс формує практично кожен аспект архітектури компʼютерів і програмування:Ієрархія памʼяті. Щоб подолати розрив між регістрами та основною памʼяттю, сучасні процесори включають кілька рівнів кеш-памʼяті. Кеш першого рівня (десятки кілобайт) швидкий, але малий; кеш другого рівня (сотні кілобайт) більший, але повільніший; кеш третього рівня (мегабайти) ще більший, але ще повільніший. Ця ієрархія намагається тримати часто використовувані дані близько до процесора, водночас задовольняючи величезні обсяги даних, які потребують сучасні програми.Оптимізація компілятора. Одне з найважливіших завдань оптимізуючого компілятора — це розподіл регістрів — вирішення, які змінні повинні перебувати в регістрах, а які мають зберігатися в памʼяті. Змінні, до яких часто звертаються в межах циклу, в ідеалі повинні залишатися в регістрах протягом усього виконання циклу. Менш часто використовувані дані можна «витіснити» в памʼять. Хороший розподільник регістрів може драматично покращити продуктивність програми.Угоди про виклики. Обмежена кількість регістрів вимагає ретельної координації між функціями. Угоди про виклики визначають, які регістри функція може вільно використовувати (зберігаються викликачем), а які вона повинна зберегти (зберігаються викликаним). Без таких угод кожен виклик функції вимагав би збереження всіх регістрів у памʼять, знищуючи більшу частину переваг продуктивності, які дають регістри [1].Проєктування алгоритмів. Програмісти, які розуміють компроміс між регістрами та памʼяттю, пишуть ефективніший код. Слід уникати повторного доступу до того самого елемента масиву — краще завантажити його в змінну (яку компілятор може тримати в регістрі) і повторно використовувати цю змінну. Змінні циклу повинні бути простими цілими числами, які поміщаються в регістри. Складні структури даних слід проєктувати так, щоб сприяти хорошій поведінці кешу.Архітектура x86 з її 16 регістрами загального призначення в 64-бітному режимі представляє поточну відповідь Intel та AMD на цей компроміс. Інші архітектури роблять інший вибір: ARM64 надає 31 регістр загального призначення, тоді як деякі RISC-архітектури пропонують 32 або більше. Кожен вибір відображає різний баланс між складністю кодування інструкцій, площею кремнію та практичними потребами програмування.Розуміння цього фундаментального компромісу готує вас до наступної теми: як памʼять організована у величезну сітку місць зберігання, яка, хоч і повільніша за регістри, забезпечує ємність, необхідну для реальних програм і даних.Розгляньмо завдання побудови навіть скромного обсягу компʼютерної памʼяті. Один мегабайт — тривіально малий обсяг за сучасними стандартами — містить 8 388 608 окремих бітів. Якби ми проєктували нашу систему памʼяті наївно, даючи кожному біту власний виділений провідник для читання та запису, нам знадобилося б понад вісім мільйонів провідників лише для адресації цього крихітного обсягу сховища. Таку систему було б неможливо виготовити, неможливо розвести на друкованій платі, і вона займала б величезну кількість фізичного простору.Ця проблема масштабування стає ще очевиднішою, коли ми розглядаємо сучасні обсяги памʼяті. Типовий компʼютер сьогодні має від 8 до 32 гігабайт оперативної памʼяті — це від 64 до 256 мільярдів бітів. Підхід «провідник на біт» вимагав би сотень мільярдів окремих зʼєднань, що очевидно абсурдно. Інженерам потрібен був фундаментально розумніший підхід, щоб зробити практичні системи памʼяті можливими.Рішення приходить із красивого математичного спостереження: розташувати біти у двовимірній сітці замість одновимірного масиву. Замість адресації кожного біта власним провідником ми використовуємо комбінацію вибору рядка та стовпця.Розгляньмо математику. Для одновимірного розташування n бітів нам потрібно n провідників. Але для двовимірної квадратної сітки з n бітів нам потрібно лише приблизно 2√n провідників — приблизно √n для вибору рядка та √n для вибору стовпця.Подивімось, як драматично це зменшує складність зʼєднань:
1 000 000 бітів (125 КБ): замість 1 000 000 провідників нам потрібно лише близько 2 000 провідників (1 000 рядків × 1 000 стовпців)
1 000 000 000 бітів (125 МБ): замість мільярда провідників достатньо приблизно 64 000 провідників
8 000 000 000 бітів (1 ГБ): близько 180 000 провідників замість восьми мільярдів
Це квадратичне зменшення складності зʼєднань робить сучасну високоємнісну памʼять фізично здійсненною. Матрична організація перетворює неможливе інженерне завдання на керовану задачу.Ліворуч: Сітка 4x4 керованих засувок утворює базову структуру масиву памʼяті. Праворуч: Лінії вибору рядків і стовпців активують конкретні комірки на їхньому перетині.Матричне рішення ставить нове питання: як активувати рівно один рядок з тисячі можливих рядків, використовуючи найменшу кількість керуючих ліній? Відповідь криється в двійкових декодерах.Двійковий декодер — це схема, яка перетворює n вхідних бітів на 2^n вихідних ліній, де рівно один вихід активний у будь-який момент часу. Вхідні біти кодують двійкове число, і декодер активує вихідну лінію, що відповідає цьому числу.Наприклад, декодер 2-в-4 приймає 2 вхідні біти та видає 4 вихідні лінії:
Вхід 00 → активує лінію 0
Вхід 01 → активує лінію 1
Вхід 10 → активує лінію 2
Вхід 11 → активує лінію 3
<img alt="Двійковий декодер, що перетворює 2-бітний вхід на 4 вихідні лінії" src="assets/l03-binary-decoder.png" target="_self" style="width: 400px; max-width: 100%;">Двійковий декодер 2-в-4: 2-бітний вхід вибирає рівно одну з чотирьох вихідних ліній, забезпечуючи ефективне декодування адреси.Щоб знайти конкретну комірку памʼяті в нашій матриці, ми використовуємо два декодери: один для рядків і один для стовпців [2]. Адреса, подана на ці декодери, активує конкретний провідник рядка та конкретний провідник стовпця; перетин цих двох активних провідників ідентифікує унікальне місце в памʼяті.За цією схемою вибір однієї комірки серед мільйона вимагає лише близько 20 адресних бітів загалом — 10 бітів для декодера рядків (вибір одного з 1 024 рядків) і 10 бітів для декодера стовпців (вибір одного з 1 024 стовпців). Це значно ефективніше, ніж мільйон окремих провідників вибору.<br><img alt="Масив памʼяті з двійковим декодером для вибору адреси" src="assets/l03-memory-with-decoder.png" target="_self">Повна адресація памʼяті: двійкові декодери перетворюють вхідну адресу на сигнали вибору рядка та стовпця, ідентифікуючи унікальну комірку на їхньому перетині.У кожній точці перетину матриці памʼяті знаходиться керована засувка — фундаментальний елемент зберігання, який ми досліджували в Лекції 1. Керована засувка, як описано в [2], використовує два ключові входи:
Вхід даних: один провідник, що несе бітове значення для зберігання
Дозвіл запису: керуючий сигнал, що визначає, коли схема повинна захопити дані
Поведінка елегантно проста. Коли дозвіл запису встановлено в 0, сигнал даних повністю ігнорується — засувка зберігає будь-яке значення, яке вона раніше зберігала, через свій внутрішній зворотний звʼязок. Коли дозвіл запису переходить в 1, схема захоплює поточне значення на вході даних і перезаписує свою збережену памʼять [2].Цей механізм зворотного звʼязку є серцем компʼютерної памʼяті. Після запису значення петля зворотного звʼязку підтримує його невизначено довго (поки подається живлення), навіть після зміни оригінального вхідного сигналу.Матрична організація створює тонку проблему. Коли ми активуємо рядок, кожна комірка в цьому рядку бачить той самий сигнал рядка. Так само, коли ми активуємо стовпець, кожна комірка в цьому стовпці реагує. Як ми гарантуємо, що лише одна комірка на перетині вибраного рядка та стовпця фактично виконує операцію читання або запису?Рішення використовує вентилі І в кожному перетині [2]. Керуюча логіка кожної комірки включає вентилі І, які поєднують сигнал вибору рядка з сигналом вибору стовпця. Оскільки вентиль І видає високий вихід лише тоді, коли всі його входи високі, комірку можна вибрати лише тоді, коли і її рядок, і її стовпець одночасно активні.Це гарантує, що сигнали читання або запису досягають лише конкретної засувки, ідентифікованої повною адресою [2]. Якщо ви активуєте рядок 47 і стовпець 83, лише комірка в позиції (47, 83) реагує — усі інші комірки в рядку 47 та всі інші комірки в стовпці 83 залишаються неактивними.Памʼять повинна підтримувати дві фундаментальні операції: запис даних у комірку та читання даних з комірки. Обидві операції використовують систему адресації для вибору цільової комірки, але відрізняються напрямком потоку даних.Для операції запису система встановлює сигнал дозволу запису разом з цільовою адресою. Вибрана комірка захоплює будь-яке значення, що зʼявляється на лінії даних, і зберігає його у своїй внутрішній засувці. Усі інші комірки ігнорують і дозвіл запису, і сигнал даних, оскільки їхні вентилі І не повністю активовані.Для операції читання система встановлює сигнал дозволу читання разом з цільовою адресою. Сигнал дозволу читання, як пояснено в [2], перемикає лінію даних, щоб вона діяла як вихід, а не як вхід. Вибрана комірка виставляє своє збережене значення на лінію даних, де його може зчитати інша схемотехніка.Критична деталь реалізації: оскільки той самий провідник даних служить і входом (під час запису), і виходом (під час читання), тристабільні буфери запобігають конфліктам. Ці спеціальні схеми можуть не лише виводити високий і низький стани, але й стан «відʼєднано», дозволяючи провіднику керуватися тією коміркою чи зовнішнім джерелом, яке є відповідним у даний момент.Ліворуч: Комірка памʼяті з тристабільним буфером, що дозволяє двонаправлений потік даних по спільному провіднику. Праворуч: Під час операції читання вибрана комірка виставляє своє збережене значення на лінію даних.Спільний провідник, що переносить дані в памʼять і з памʼяті, називається шиною даних. Ця шина є спільною для всіх комірок памʼяті — мільйонів або мільярдів їх — але завдяки логіці адресації лише одна комірка взаємодіє з нею в будь-який момент часу.Термін «шина» (bus) походить від латинського «omnibus», що означає «для всіх», і влучно описує спільний комунікаційний шлях. Шина даних зʼєднує масив памʼяті з процесором та пристроями введення-виведення, слугуючи магістраллю для потоку інформації всередині компʼютера.Ширина шини даних впливає на те, скільки даних можна передати за одну операцію. Ранні компʼютери мали 8-бітні шини даних; сучасні системи зазвичай використовують 64-бітні шини даних, дозволяючи їм читати або записувати 8 байтів одночасно.]]></description><link>lectures/_temp_uk_part_03.html</link><guid isPermaLink="false">Lectures/_temp_uk_part_03.md</guid><pubDate>Wed, 28 Jan 2026 21:15:19 GMT</pubDate><enclosure url="assets/l03-memory-grid-latches.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-memory-grid-latches.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[_temp_uk_part_02]]></title><description><![CDATA[У нашій спрощеній архітектурі з 4 регістрами не всі регістри однакові. Регістр R0 відіграє особливу роль: це акумулятор. Таке призначення не є випадковим, а відображає обдумані рішення проєктування апаратного забезпечення, що беруть початок від найперших комп'ютерів і зберігаються в сучасних процесорах x86.Акумулятор — це регістр, який апаратно під'єднаний для отримання результатів арифметичних і логічних операцій. Коли АЛП (Арифметико-логічний пристрій) обчислює суму, різницю або логічний результат, вихідні дані автоматично потрапляють до акумулятора без необхідності явно вказувати призначення в кожній інструкції.У нашій прикладній архітектурі:
ADD R1 означає "додати R1 до R0, зберегти результат у R0"
SUB R2 означає "відняти R2 від R0, зберегти результат у R0"
Акумулятор (R0) є неявним. Він одночасно є операндом-джерелом і призначенням. Така конструкція має кілька важливих наслідків.1. Спрощене кодування інструкційЗ виділеним акумулятором арифметичним інструкціям потрібно вказувати лише один операнд, а не два. Замість кодування "ADD джерело, призначення" (що вимагає бітів для обох), ми кодуємо лише "ADD джерело" (призначенням завжди є акумулятор).У нашому гіпотетичному форматі:
З явним призначенням: [op:2][src:2][dst:2][unused:2] = 8 біт
З неявним акумулятором: [op:2][src:2][unused:4] = 8 біт, але 4 біти звільнено
Ці звільнені біти можна використати для кодування більшої кількості операцій, більшої кількості регістрів-джерел або додаткових режимів адресації.2. Зменшена складність з'єднаньВихід АЛП потребує лише одного постійного шляху: до акумулятора. Без акумуляторної конструкції вихід АЛП потребував би мультиплексора для маршрутизації результатів до будь-якого з N регістрів, що вимагає N×M проводів (N регістрів, M біт кожен) плюс логіка вибору.З акумулятором:
Вихід АЛП з'єднується безпосередньо з входом R0
Вихідний мультиплексор не потрібен
Простіше, швидше, менше транзисторів
<img alt="Цифровий симулятор, що показує регістри A, B, з'єднані з АЛП" src="assets/l03-digital-simulator-cpu.png" target="_self">У цьому вигляді цифрового симулятора регістр A (акумулятор) апаратно під'єднаний до виходу АЛП. Регістр B надає другий операнд для бінарних операцій. Це фіксоване з'єднання усуває потребу у вихідній мультиплексації.3. Швидше виконанняОскільки вихідний шлях фіксований, немає затримки мультиплексора на результаті. Ланцюг переносу в суматорі вже є критичним шляхом синхронізації; додавання затримки мультиплексора сповільнювало б кожну арифметичну операцію.Акумуляторна конструкція найкраще проявляє себе в послідовних обчисленнях. Розглянемо обчислення (a + b + c + d):Без акумулятора (двооперандні інструкції):MOV R0, a ; R0 = a
ADD R0, b ; R0 = a + b
ADD R0, c ; R0 = a + b + c
ADD R0, d ; R0 = a + b + c + d
З акумулятором (однооперандні інструкції):LOAD a ; ACC = a
ADD b ; ACC = a + b
ADD c ; ACC = a + b + c
ADD d ; ACC = a + b + c + d
Акумуляторна версія має коротші інструкції (один операнд замість двох) і природно "акумулює" проміжну суму — саме звідси походить назва.Цей патерн надзвичайно поширений у реальних програмах: прочитати значення, обробити його, додати до проміжної суми, повторити. Акумуляторна архітектура оптимізована саме для цього патерну.Коли Intel проєктував 8086, вони успадкували концепцію акумулятора від попередніх процесорів. Регістр AX (Accumulator eXtended — розширений акумулятор) виконує цю особливу роль: Множення та ділення неявно використовують AX (і DX для розширених результатів) MUL BX множить AX на BX, зберігаючи результат у DX:AX
DIV BX ділить DX:AX на BX, частка в AX, остача в DX Інструкції введення-виведення використовують виключно AX IN AL, port читає байт з порту вводу-виводу в AL
OUT port, AX записує AX у порт вводу-виводу Рядкові інструкції використовують AX як джерело/призначення для даних LODSB завантажує байт з пам'яті в AL
STOSW зберігає AX у пам'ять Деякі інструкції мають коротше кодування при використанні AX ADD AX, 5 може кодуватися меншою кількістю байтів, ніж ADD BX, 5 Хоча сучасні процесори x86 дозволяють більшість операцій з будь-яким регістром, спадщина акумулятора зберігається в кодуванні інструкцій та спеціалізованих інструкціях.Концепція акумулятора масштабувалася разом з розрядністю архітектури:
8-біт: AL (Accumulator Low byte — молодший байт акумулятора)
16-біт: AX (Accumulator eXtended — розширений акумулятор) = AH:AL
32-біт: EAX (Extended AX — розширений AX)
64-біт: RAX (Register AX — регістр AX у 64-бітному режимі)
Це не окремі регістри, а перекриваючі подання одного й того самого фізичного сховища:
Запис у AL впливає на молодші 8 біт AX, EAX і RAX
Запис в AX впливає на молодші 16 біт EAX і RAX
Запис у EAX розширюється нулями для заповнення RAX (у 64-бітному режимі)
Концепція акумулятора бере початок від найперших електронних комп'ютерів. ENIAC (1945) мав акумулятори. IBM 701 (1952) мав акумулятор. Ранні мікропроцесори, такі як Intel 8008 (1972) та 8080 (1974), мали один регістр-акумулятор під назвою A.Така конструкція мала сенс, коли транзистори були дорогими. Кожен регістр вимагав десятків транзисторів, тому мінімізація кількості регістрів була важливою. Наявність одного спеціального регістра, який отримував усі арифметичні результати, дозволяла простіші формати інструкцій і зменшувала кількість з'єднань.З подешевшанням транзисторів (згідно із законом Мура) архітектури додавали більше регістрів загального призначення. RISC-архітектури, такі як ARM і MIPS, мають 16-32 регістри, де будь-який регістр може бути джерелом або призначенням для будь-якої операції. Але навіть ці архітектури "завантаження-збереження" зберігають деякі акумуляторо-подібні конвенції для певних операцій.Акумуляторна конструкція передбачає компроміс:Сучасні процесори x86 підтримують обидва стилі: інструкції з неявним акумулятором (для зворотної сумісності та компактного кодування) та інструкції з явними операндами (для гнучкості). Цей гібридний підхід відображає еволюцію архітектури протягом чотирьох десятиліть.Розуміння концепції акумулятора допомагає пояснити, чому асемблерний код виглядає саме так, чому певні регістри мають особливе призначення, і чому деякі інструкції мають коротше кодування, ніж інші. Це вікно в інтерфейс апаратного та програмного забезпечення, що визначає, як програми насправді виконуються.Регістри — це найшвидші місця зберігання в будь-якій комп'ютерній системі — невеликі комірки пам'яті, вбудовані безпосередньо в ЦП, що зберігають дані, які зараз обробляються. Розуміння еволюції регістрів x86 від їхніх 16-бітних витоків до сучасних 64-бітних реалізацій розкриває як історію персональних комп'ютерів, так і фундаментальні архітектурні рішення, що продовжують впливати на розробку програмного забезпечення сьогодні.Коли Intel проєктував процесор 8086 у 1978 році, вони створили чотири основні регістри загального призначення, кожен шириною 16 біт. Ці регістри отримали назви, що відображали їхнє призначене спеціалізоване використання, хоча на практиці програмісти завжди використовували їх дещо взаємозамінно [1].Акумулятор (AX) отримав свою назву, оскільки арифметичні операції природно накопичували свої результати тут. Коли ви додаєте два числа або виконуєте множення, результат з'являється в AX (або в парі DX:AX для більших результатів). Багато інструкцій були оптимізовані для роботи саме з AX, що робило операції з цим регістром швидшими та вимагало коротшого машинного коду [1].Базовий регістр (BX) служив основним регістром для адресації пам'яті в оригінальній конструкції 8086. При доступі до даних, збережених у пам'яті, програмісти завантажували базову адресу структури даних у BX, а потім використовували зміщення для доступу до окремих елементів. Це робило BX необхідним для реалізації масивів і складних структур даних.Регістр-лічильник (CX) був спроєктований спеціально для циклічних операцій. Набір інструкцій 8086 включає спеціальні інструкції циклу (такі як LOOP), що автоматично зменшують CX і переходять назад, якщо лічильник не досяг нуля. Ця апаратна підтримка підрахунку зробила CX природним вибором для будь-якої повторюваної операції, включаючи маніпуляції з рядками, де та сама операція має застосовуватися до кожного символу.Регістр даних (DX) функціонує як розширення AX для операцій, що дають результати, занадто великі для одного 16-бітного регістра. При множенні двох 16-бітних чисел 32-бітний результат з'являється зі старшим словом у DX і молодшим словом у AX, утворюючи пару DX:AX. Аналогічно, DX зберігає старшу частину діленого перед діленням і остачу після нього. DX також служить регістром адреси порту для операцій вводу-виводу, дозволяючи процесору спілкуватися з до 65 536 різними апаратними пристроями.Однією з найелегантніших особливостей регістрової архітектури 8086 є можливість доступу до окремих байтів у цих 16-бітних регістрах. Кожен з чотирьох основних регістрів може бути розділений на дві 8-бітні половини, надаючи програмістам детальний контроль над своїми даними [1].Конвенція іменування використовує суфікси H (High — старший) і L (Low — молодший):Ця конструкція фактично надає вісім 8-бітних регістрів, використовуючи лише чотири 16-бітні фізичні регістри. Ключове розуміння полягає в тому, що AH, AL і AX всі посилаються на частини одного й того ж апаратного регістра. Коли ви модифікуєте AL, ви змінюєте молодші 8 біт AX. Коли ви модифікуєте AH, ви змінюєте старші 8 біт AX. Цей патерн перекриваючого доступу був революційним для свого часу і залишається фундаментальним для програмування x86.Практичні переваги байтового доступу є суттєвими. Маніпуляція символами стає простою — символи ASCII поміщаються в один байт, тому AL може зберігати один символ, а AH — інший. Програмісти можуть упакувати два байти даних в один регістр, обробити їх разом і ефективно витягти результати. При взаємодії з 8-бітними периферійними пристроями або обробці байт-орієнтованих структур даних можливість працювати з окремими байтами без операцій маскування та зсуву економить значний час.Розглянемо простий приклад: якщо AX містить шістнадцяткове значення 4142h, то AH містить 41h (ASCII-код для 'A'), а AL містить 42h (ASCII-код для 'B'). Модифікація лише AL на 43h змінює AX на 4143h, тоді як AH залишається незмінним як 41h.<br><img alt="Оригінальні 16-бітні регістри 8086" src="assets/l03-8086-registers-basic.png" target="_self">Оригінальний набір регістрів 8086: чотири 16-бітні регістри загального призначення (AX, BX, CX, DX) з доступом до старшого/молодшого байтів (AH/AL, BH/BL, CH/CL, DH/DL), плюс сегментні регістри для адресації пам'яті.Intel 80386, випущений у 1985 році, приніс 32-бітні обчислення в архітектуру x86. Замість проєктування абсолютно нового набору регістрів Intel розширив існуючі регістри, зберігаючи зворотну сумісність — рішення, що визначило еволюцію x86 відтоді [1].Стратегія розширення використовувала префікс E (від "Extended" — розширений) для створення 32-бітних версій усіх існуючих регістрів:Коли ви записуєте значення в EAX, ви використовуєте всі 32 біти. Молодші 16 біт EAX доступні як AX, і в межах AX окремі байти залишаються доступними як AH і AL. Однак старші 16 біт EAX (біти 31-16) не мають окремої назви — до них можна отримати доступ лише через сам EAX або через операції бітової маніпуляції.80386 також додав чотири додаткові 32-бітні регістри, що не мали 8-бітних підрозділів:ESI (індекс джерела) та EDI (індекс приймача) були спроєктовані для рядкових операцій — копіювання, порівняння або сканування блоків пам'яті. ESI зазвичай вказує на дані-джерело, тоді як EDI вказує на призначення. Ці регістри забезпечують ефективну реалізацію функцій на кшталт memcpy() і strcmp() у C.ESP (вказівник стека) — можливо, найкритичніший регістр у системі, який завжди вказує на поточну вершину стека. Кожен виклик функції, виділення локальних змінних та переривання покладаються на правильність ESP. Процесор автоматично зменшує ESP при записуванні даних у стек і збільшує при витягуванні. Через цю критичну роль ESP ніколи не слід використовувати для обчислень загального призначення [1].EBP (базовий вказівник) служить стабільною точкою відліку в межах кадрів стека. Коли функція викликається, EBP зазвичай встановлюється на поточне значення ESP, створюючи фіксовану систему відліку. Локальні змінні потім доступні як від'ємні зміщення від EBP (EBP-4, EBP-8 тощо), тоді як параметри функції з'являються як додатні зміщення (EBP+8, EBP+12 тощо). Ця конвенція значно полегшує налагодження і дозволяє функціям мати локальні дані змінного розміру [1].<br><img alt="32-бітні регістри загального призначення x86" src="assets/l03-x86-32bit-registers.png" target="_self">32-бітний набір регістрів x86, представлений з 80386: розширені версії оригінальних регістрів (EAX, EBX, ECX, EDX) плюс індексні та вказівникові регістри (ESI, EDI, ESP, EBP).Архітектура AMD64 (також відома як x86-64), представлена у 2003 році, розширила регістри до 64 біт і додала абсолютно нові регістри для задоволення зростаючих вимог сучасного програмного забезпечення. Це розширення принесло найзначніші зміни до набору регістрів x86 з часів оригінального 8086.64-бітне розширення використовує префікс R (від "Register" — регістр) на оригінальних регістрах:Зверніть увагу, що в 64-бітному режимі навіть індексні та вказівникові регістри отримали 8-бітний доступ до молодшого байта (SIL, DIL, SPL, BPL) — функція, що була недоступна в 32-бітному режимі.Архітектура також додала вісім абсолютно нових 64-бітних регістрів, пронумерованих від R8 до R15. Коли розробникам "забракло букв" для змістовних назв, вони просто прийняли числові ідентифікатори:Суфікси слідують послідовному патерну: D для "double word" (подвійне слово, 32 біти), W для "word" (слово, 16 біт) і B для "byte" (байт, 8 біт).Окрім регістрів загального призначення, процесори x86-64 включають XMM0 до XMM15 — шістнадцять 128-бітних регістрів, призначених для SIMD-операцій (Single Instruction, Multiple Data — одна інструкція, багато даних). Ці регістри забезпечують паралельну обробку кількох елементів даних одночасно, що є критичним для мультимедійних застосунків, наукових обчислень і криптографії. Сучасні процесори додатково розширюють їх 256-бітними YMM і 512-бітними ZMM регістрами для ще більшого паралелізму.Повну еволюцію регістрів у архітектурах x86 можна підсумувати наступним чином:Ця зворотно сумісна еволюція означає, що код, написаний для 8086, все ще може виконуватися на сучасних процесорах, хоча він не використовуватиме переваг більших регістрів чи додаткових можливостей. Багаторівневий доступ до регістрів (AL у межах AX у межах EAX у межах RAX) залишається визначальною характеристикою архітектури x86.<br><img alt="Структура 64-бітного регістра, що показує вкладений доступ" src="assets/l03-x64-register-layout.png" target="_self">Вкладена структура регістрів x86-64: RAX (64-біт) містить EAX (32-біт), який містить AX (16-біт), що розділений на AH і AL (по 8 біт кожен). Цей патерн багаторівневого доступу забезпечує зворотну сумісність між усіма поколіннями x86.]]></description><link>lectures/_temp_uk_part_02.html</link><guid isPermaLink="false">Lectures/_temp_uk_part_02.md</guid><pubDate>Wed, 28 Jan 2026 21:15:19 GMT</pubDate><enclosure url="assets/l03-digital-simulator-cpu.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-digital-simulator-cpu.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[_temp_uk_part_01]]></title><description><![CDATA[Курс: Архітектура комп'ютерних систем
Тема: Від зʼєднань до регістрів, адресація пам'яті
Результати навчання: LO1 (Принципи архітектури), LO2 (Представлення даних), LO3 (Програмування на асемблері)У попередній лекції ми побудували схеми, які оперують двійковими даними: тригери, що зберігають 0 та 1, суматори, що обробляють потоки бітів, та АЛП (арифметико-логічний пристрій), керований двійковими кодами операцій. Двійковий код є рідною мовою цифрового апаратного забезпечення, але він створює практичну проблему для програмістів: двійкові числа довгі та важкі для читання. Число 255 потребує вісім двійкових цифр (11111111), а 32-бітова адреса перетворюється на незручний рядок із тридцяти двох 0 та 1. Нам потрібна більш компактна нотація, яка водночас легко відображається на базове двійкове представлення.Шістнадцяткова (з основою 16) система числення забезпечує саме таку зручність. Шістнадцятковий запис став стандартною нотацією в системному програмуванні, відладчиках, дампах пам'яті та мові асемблера, оскільки пропонує ідеальний баланс: компактне представлення для людей при збереженні тривіального перетворення у двійковий формат та навпаки.Ключова ідея шістнадцяткової системи є математичною: 16 — це степінь 2 (а саме 2^4 = 16). Це означає, що рівно чотири двійкових біти відповідають одній шістнадцятковій цифрі. Такий зв'язок робить перетворення між двійковою та шістнадцятковою системами суто механічним, без потреби в арифметиці — лише групування та пошук у таблиці.Розгляньте альтернативу: десяткова система (з основою 10) не має чіткого зв'язку з двійковою. Перетворення між десятковою та двійковою системами вимагає багаторазового ділення або множення. Але з шістнадцятковою системою ви просто розбиваєте двійкове число на групи по чотири біти та перекладаєте кожну групу незалежно.Наприклад, двійкове число 1111 0101 перетворюється у шістнадцяткове шляхом пошуку кожної групи:
1111 = F (десяткове 15)
0101 = 5 (десяткове 5)
Результат: F5 у шістнадцятковому форматі
Жодних обчислень — лише зіставлення шаблонів. Це робить шістнадцятковий формат безцінним при вивченні вмісту пам'яті або машинного коду.Оскільки система з основою 16 потребує шістнадцять різних символів цифр, а наша десяткова система надає лише десять (0-9), шістнадцяткова система використовує літери від A до F для представлення значень від 10 до 15:<img alt="Шістнадцяткові цифри 0-F з їхніми 4-бітовими двійковими еквівалентами" src="assets/l03-hex-binary-conversion.png" target="_self">Кожна шістнадцяткова цифра безпосередньо відповідає унікальному 4-бітовому двійковому шаблону, що робить перетворення між шістнадцятковим та двійковим форматами суто механічним.Ця таблиця має стати другою натурою для кожного, хто працює з комп'ютерними системами. Запам'ятовування двійкових шаблонів для кожної шістнадцяткової цифри дозволяє миттєво перетворювати подумки між представленнями.Зверніть увагу, що шістнадцятковий формат не чутливий до регістру: FF, ff та Ff — усі представляють одне й те саме значення (десяткове 255). Різні інструменти та мови програмування мають різні домовленості, але значення ідентичне.Двійкове у шістнадцяткове:Щоб перетворити двійкове у шістнадцяткове, згрупуйте біти у набори по чотири, починаючи справа (від молодшого біту). Якщо найлівіша група має менше чотирьох бітів, доповніть провідними нулями.Приклад: Перетворіть 110101110 у шістнадцяткове.
Згрупуйте по чотири справа: 1 1010 1110
Доповніть найлівішу групу: 0001 1010 1110
Перетворіть кожну групу: 1, A, E
Результат: 1AE (що дорівнює 430 у десятковому)
Шістнадцяткове у двійкове:Просто розгорніть кожну шістнадцяткову цифру до її 4-бітового двійкового еквівалента.Приклад: Перетворіть 3F7 у двійкове.
3 = 0011
F = 1111
7 = 0111
Результат: 0011 1111 0111 = 001111110111
Шістнадцяткове у десяткове:Використовуйте позиційну систему числення зі степенями 16.Приклад: Перетворіть 2A3 у десяткове.2A3 = 2 × 16² + A × 16¹ + 3 × 16⁰ = 2 × 256 + 10 × 16 + 3 × 1 = 512 + 160 + 3 = 675
Десяткове у шістнадцяткове:Багаторазово ділимо на 16, збираючи остачі знизу догори.Приклад: Перетворіть 500 у шістнадцяткове.500 ÷ 16 = 31 остача 4
31 ÷ 16 = 1 остача 15 (F)
1 ÷ 16 = 0 остача 1
Читаючи знизу догори: 1F4
Перевірка: 1×256 + 15×16 + 4×1 = 256 + 240 + 4 = 500.Байт складається з 8 бітів, що зручно дорівнює рівно двом шістнадцятковим цифрам. Це не випадковість; байт став фундаментальною одиницею комп'ютерної пам'яті саме тому, що 8 бітів забезпечують корисний діапазон (0-255 для беззнакових значень або від -128 до +127 для знакових), залишаючись степенем 2.Оскільки одна шістнадцяткова цифра представляє 4 біти (ніббл або напівбайт), маємо:
1 байт = 8 бітів = 2 шістнадцяткові цифри (00 до FF)
16 бітів = 2 байти = 4 шістнадцяткові цифри (0000 до FFFF)
32 біти = 4 байти = 8 шістнадцяткових цифр (00000000 до FFFFFFFF)
64 біти = 8 байтів = 16 шістнадцяткових цифр
Типові розміри архітектур еволюціонували протягом історії комп'ютерів:
8-бітові архітектури (ранні мікропроцесори, такі як Intel 8080): регістри та шляхи даних шириною 8 біт
16-бітові архітектури (Intel 8086): 16-бітові регістри, основа x86
32-бітові архітектури (Intel 80386, часто називається «x86»): розширені регістри до 32 біт
64-бітові архітектури (AMD64/x86-64): сучасний стандарт із 64-бітовими регістрами
Кожне покоління подвоювало ширину регістрів, і шістнадцяткова нотація ідеально масштабувалась: 8-бітове значення вміщується у 2 шістнадцяткові цифри, 16-бітове — у 4, 32-бітове — у 8, а 64-бітове — у 16.Різні мови програмування та інструменти використовують різні префікси або суфікси для позначення шістнадцяткових чисел:У цьому курсі ми переважно використовуватимемо суфікс h, поширений у мові асемблера x86, де 11h означає шістнадцяткове 11 (десяткове 17), а не десяткове 11.Шістнадцятковий формат повсюдний у низькорівневому програмуванні з кількох практичних причин:
Адреси пам'яті відображаються у шістнадцятковому форматі у відладчиках та дампах пам'яті
Коди операцій машинного коду документуються та відображаються у шістнадцятковому форматі
Бітові шаблони для прапорців, масок та дозволів чіткіші у шістнадцятковому форматі
Значення кольорів у графіці використовують шістнадцятковий формат (RGB як три байти)
Мережеві протоколи специфікують байти у шістнадцятковому форматі
Коли ви бачите mov ax, 1234h у коді асемблера, ви одразу знаєте, що це 16-бітове значення (4 шістнадцяткові цифри = 16 бітів), яке завантажує конкретний бітовий шаблон. Переглядаючи дамп пам'яті, що показує 48 65 6C 6C 6F, ви можете розпізнати це як текст ASCII («Hello»), знаючи шістнадцяткові коди для літер.Шістнадцятковий формат — це не просто нотаційна зручність. Це стандартна мова для обговорення точних двійкових значень, якими комп'ютери насправді оперують.У попередній лекції ми бачили, як АЛП використовує двійковий декодер для вибору операції, яку потрібно виконати. Біти коду операції подаються на декодер, який активує рівно один функціональний блок — суматор, віднімач, схему AND або інший компонент. Це не абстрактна програмна концепція. Це фізичні зʼєднання: певні бітові шаблони в коді операції буквально направляють сигнали до конкретних апаратних модулів.Цей розділ досліджує, як інструкції кодуються як бітові шаблони та як ці шаблони визначають, що робить апаратне забезпечення. Розуміння кодування інструкцій розкриває, чому процесорні архітектури мають характеристики, які вони мають, включаючи кількість регістрів, формати інструкцій та обмеження, з якими ми стикаємось у програмуванні на асемблері.Кожна машинна інструкція є, по суті, командою для маршрутизації сигналів через схеми процесора. Коли ви пишете ADD AX, BX мовою асемблера, асемблер перекладає це у конкретний бітовий шаблон. Коли цей бітовий шаблон досягає процесора, різні поля в ньому контролюють різні аспекти виконання:
Деякі біти вибирають операцію (додавання, віднімання, переміщення тощо)
Деякі біти специфікують операнд(и)-джерело
Деякі біти специфікують операнд-приймач
Деякі біти можуть вказувати режими адресації або інші модифікатори
Ці біти безпосередньо з'єднуються зі схемами декодерів, які ми побудували у Лекції 2. 3-бітове поле коду операції подається на декодер 3-до-8, активуючи одну з восьми можливих операційних схем. 2-бітове поле регістра вибирає один із чотирьох регістрів. Кодування не є довільним; воно відображає фізичну структуру процесора.<br><img alt="Інструкції асемблера з анотаціями на рівні вентилів" src="assets/l03-instruction-types-gates.png" target="_self">Інструкції асемблера відображаються на конкретні конфігурації вентилів в апаратному забезпеченні процесора. Кожен тип інструкції активує різні шляхи через АЛП та файл регістрів.Розгляньмо спрощений 8-бітовий формат інструкції для гіпотетичного процесора з 4 регістрами (R0, R1, R2, R3) та 4 операціями:Позиції бітів: 7 6 5 4 3 2 1 0 [--op--][-src-][-dst-] Біти 7-6 (op): Код операції, вибирає одну з 4 операцій
Біти 5-4 (src): Регістр-джерело, вибирає один із 4 регістрів
Біти 3-2 (dst): Регістр-приймач, вибирає один із 4 регістрів
Біти 1-0: Зарезервовані або не використовуються
Визначимо операції:
00 = ADD (додати джерело до приймача)
01 = SUB (відняти джерело від приймача)
10 = MOV (перемістити джерело до приймача)
11 = AND (побітове AND)
І регістри:
00 = R0
01 = R1
10 = R2
11 = R3
Розгляньмо байт інструкції 01 00 01 00 (двійкове), що є 44h у шістнадцятковому.Розбиваємо на поля:
01 (біти 7-6) = операція SUB
00 (біти 5-4) = R0 як джерело
01 (біти 3-2) = R1 як приймач
00 (біти 1-0) = не використовується
Ця інструкція означає: SUB R0, R1 або «відняти R0 від R1, зберігаючи результат у R1».Розглянемо інший приклад: 00 01 00 01 двійкове = 11h шістнадцяткове.
00 = операція ADD
01 = R1 як джерело
00 = R0 як приймач
01 = не використовується
Це: ADD R1, R0 або «додати R1 до R0».<br><img alt="Кодування інструкції машинного коду, що показує поля коду операції та операндів" src="assets/l03-instruction-encoding.png" target="_self">Бітові поля всередині байта інструкції кодують тип операції, регістр-джерело та регістр-приймач. Кожне поле відображається на конкретні схеми декодерів в апаратному забезпеченні.Коли байт інструкції 11h надходить до процесора: Біти 7-6 (00) подаються на декодер операцій. Вихід 0 декодера переходить у стан HIGH, активуючи схему ADD, тоді як схеми SUB, MOV та AND залишаються неактивними. Біти 5-4 (01) подаються на селектор регістра-джерела. Це мультиплексор, який направляє вміст R1 на вхід «B» АЛП. Біти 3-2 (00) подаються на селектор регістра-приймача. Це направляє вихід АЛП назад до R0, а також направляє поточний вміст R0 на вхід «A» АЛП. АЛП виконує додавання на своїх входах A та B. Результат зберігається у регістрі-приймачі (R0). Кожен окремий біт має призначення. Змініть біт 6 з 0 на 1 — і інструкція стає SUB замість ADD. Змініть біти 5-4 з 01 на 10 — і R2 стає джерелом замість R1.<br><img alt="Блок-схема ЦП, що показує АЛП, декодер та зʼєднання регістрів" src="assets/l03-cpu-alu-decoder.png" target="_self">Декодер ЦП отримує біти інструкції та активує відповідну операцію АЛП і шляхи регістрів. Біти коду операції безпосередньо контролюють, який функціональний блок виконує обчислення.Ось критичне розуміння: кількість бітів, виділених для вибору регістра, визначає максимальну кількість регістрів, яку може мати архітектура.З 2 бітами для вибору регістра ми можемо адресувати 2^2 = 4 регістри (00, 01, 10, 11).
З 3 бітами ми могли б адресувати 2^3 = 8 регістрів.
З 4 бітами ми могли б адресувати 2^4 = 16 регістрів.Запитання для студентів: Якщо формат інструкції використовує 2 біти для специфікації кожного з двох регістрових операндів, скільки різних регістрів може підтримувати архітектура?Відповідь: Максимум чотири регістри. 2-бітове поле може кодувати лише значення 0, 1, 2 та 3.Це пояснює, чому ранні процесори мали так мало регістрів. Intel 8086 має лише 8 регістрів загального призначення (потребуючи 3 біти кожен для адресації), і навіть ця скромна кількість займає значний простір кодування інструкцій. Наявність 32 регістрів потребувала б 5 біт на поле регістра, роблячи інструкції довшими та складнішими для декодування.Сучасні процесори, такі як x86-64, розширились до 16 регістрів загального призначення, додавши префіксний байт (префікс REX) до інструкцій, фактично додаючи додаткові біти до полів вибору регістра. Цей зворотно-сумісний підхід дозволив нові можливості, зберігаючи сумісність з існуючим кодом для 8 регістрів.Окрім кодування інструкцій, регістри мають фізичні витрати: Складність зʼєднань: Кожен регістр повинен з'єднуватися з входами та виходами АЛП. З N регістрами та M-бітовим АЛП вам потрібні мультиплексори з N×M вхідними провідниками лише для маршрутизації даних до АЛП. Розмір декодера: Вибір регістра потребує декодерів з N входами, які зростають експоненціально за кількістю вентилів. Площа на чіпі: Кожен регістр складається з M тригерів плюс логіка маршрутизації. Більше регістрів означає більше площі кремнію. Споживання енергії: Більше транзисторів означає більше потужності, більше тепла та більші вимоги до охолодження. Ці обмеження пояснюють історичну еволюцію від машин з 4 регістрами до 8, потім 16 і тепер 32 у деяких RISC-архітектурах. Кожне розширення потребувало досягнень у технології виробництва, щоб зробити додаткові зʼєднання практичними.Intel 8086, який ми вивчатимемо детально, використовує інструкції змінної довжини зі складним кодуванням. Однак фундаментальний принцип залишається: конкретні бітові шаблони в машинному коді активують конкретні апаратні шляхи. Коли ви пишете ADD AX, BX і асемблер генерує байти 01 D8, ці байти містять закодовану інформацію про:
Операцію (ADD)
Розмір операнда (16 біт)
Регістр-джерело (BX)
Регістр-приймач (AX)
Навчання читати та розуміти ці кодування поглиблює ваше розуміння того, що процесор насправді робить, коли виконує ваш код.]]></description><link>lectures/_temp_uk_part_01.html</link><guid isPermaLink="false">Lectures/_temp_uk_part_01.md</guid><pubDate>Wed, 28 Jan 2026 21:15:18 GMT</pubDate><enclosure url="assets/l03-hex-binary-conversion.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-hex-binary-conversion.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[_temp_uk_part_04]]></title><description><![CDATA[Одна матриця пам'яті, як ми її описали, зберігає окремі біти — один біт на комірку на кожному перетині рядка та стовпця. Але комп'ютери працюють з байтами як базовою одиницею зберігання. Як же нам прочитати або записати цілий байт за один раз?Елегантне рішення, описане в [2], полягає в тому, щоб скласти вісім ідентичних матриць, які спільно використовують ті самі адресні лінії та керуючі сигнали. Коли ви вказуєте адресу та подаєте команду читання, усі вісім матриць одночасно видають свої збережені біти за цією адресою. Ці вісім бітів разом утворюють один байт.Уявіть це як вісім прозорих аркушів, накладених один на одного. Кожен аркуш має сітку бітів, і всі аркуші використовують однакову систему координат. Коли ви вказуєте на позицію (47, 83), ви вказуєте крізь усі вісім аркушів одночасно, отримуючи доступ до одного біта з кожного аркуша — вісім бітів загалом, один байт.Ця паралельна архітектура означає, що читання або запис байта займає не більше часу, ніж читання або запис одного біта. Пропускна здатність множиться, тоді як затримка залишається сталою — це повторювана тема в оптимізації комп'ютерної архітектури.У комп'ютері пам'ять використовує вісім матриць одночасно, які спільно використовують ті самі адресні та керуючі лінії, дозволяючи читати/записувати цілий байт за один раз [2].<img alt="Оперативна пам'ять як абстрактний компонент" src="assets/l03-ram-abstraction.png" target="_self">Оперативна пам'ять (ОП), абстрагована як компонент з адресними лініями (вибір байта для доступу) та лініями даних (передача значення байта).<br><img alt="Повний модуль оперативної пам'яті" src="assets/l03-ram-complete.png" target="_self">Повний модуль оперативної пам'яті, що показує адресний декодер, який перетворює двійкові адресні біти в сигнали вибору рядка/стовпця, забезпечуючи доступ до будь-якого байта в масиві пам'яті.Адресна шина — це набір провідників, які передають адресу комірки пам'яті, до якої здійснюється доступ. Так само як ширина шини даних визначає, скільки бітів можна передати за один раз, ширина адресної шини визначає, скільки унікальних адрес можна адресувати.Це співвідношення безпосередньо випливає з двійкового кодування: маючи n адресних бітів, ми можемо представити 2^n різних адрес. Кожна адреса відповідає одному байту пам'яті. Тому 32-бітна адресна шина може керувати приблизно 4 мільярдами байтів (4 ГБ) [2].<br><img alt="Формула обчислення адресного простору" src="assets/l03-address-space-formula.png" target="_self">Співвідношення між шириною адресної шини та адресованою пам'яттю: n адресних бітів можуть адресувати 2^n унікальних комірок. 32-бітна адресна шина дозволяє адресувати 4 ГБ (2^32 байтів).Перехід від 16-бітної до 64-бітної адресації являє собою драматичне розширення адресованої пам'яті. 16-бітна адресна шина обмежує систему 64 КБ — сьогодні ледве достатньо для складного документа. 32-бітний бар'єр у 4 ГБ став серйозним обмеженням для настільних комп'ютерів до середини 2000-х, спонукаючи перехід до 64-бітних архітектур.Сучасні 64-бітні системи теоретично можуть адресувати 16 ексабайтів (16 мільярдів гігабайтів), але практичні обмеження від дизайну материнських плат, обмеження операційних систем та економічні фактори тримають поточні системи значно нижче цієї межі.Не вся пам'ять однакова. Дві принципово різні технології домінують у проєктуванні комп'ютерної пам'яті: статична пам'ять (SRAM) (Static Random Access Memory) та динамічна пам'ять (DRAM) (Dynamic Random Access Memory).Статична пам'ять (SRAM) використовує кілька транзисторів на біт — зазвичай шість транзисторів, організованих у конфігурації тригера [2]. Ця конструкція створює стабільний цикл зворотного зв'язку, який підтримує збережене значення необмежено довго, поки подається живлення. Слово «статична» у назві SRAM вказує саме на цю стабільність; дані не потребують періодичного оновлення.Динамічна пам'ять (DRAM) досягає вищої щільності через радикальне спрощення: один транзистор MOSFET і крихітний конденсатор на біт [2]. Конденсатор зберігає заряд для представлення 1 або відсутність заряду для представлення 0. Ця мінімальна конструкція дозволяє DRAM упаковувати значно більше бітів на тій самій площі кремнію, що робить її набагато дешевшою за біт.<br><img alt="Порівняння SRAM та DRAM" src="assets/l03-sram-vs-dram.png" target="_self">Порівняння структур комірок SRAM та DRAM: SRAM використовує 6 транзисторів у стабільній конфігурації тригера, тоді як DRAM використовує лише 1 транзистор плюс конденсатор, жертвуючи складністю заради щільності.Критична відмінність полягає у вимогах до оновлення (регенерації). Конденсатор не є ідеальним пристроєм зберігання — він повільно втрачає заряд з часом, зазвичай втрачаючи своє збережене значення протягом мілісекунд. Для підтримки цілісності даних контролери DRAM повинні періодично зчитувати кожну комірку та перезаписувати її значення — процес, що називається оновленням (або регенерацією). Це відбувається тисячі разів на секунду, споживаючи пропускну здатність пам'яті та додаючи складності, але економія коштів від простіших комірок робить DRAM економічно необхідною для великих обсягів пам'яті.Швидкість SRAM та її простота (не потрібна схема оновлення) роблять її ідеальною для малих, швидких пам'ятей, де продуктивність є першочерговою. Щільність DRAM та низька вартість роблять її єдиним практичним вибором для гігабайтів основної пам'яті.Компроміс між SRAM та DRAM ілюструє фундаментальну істину комп'ютерної архітектури: не існує єдиної ідеальної технології пам'яті. Швидка пам'ять дорога та має низьку щільність. Дешева пам'ять високої щільності повільна. Інженери вирішують цю дилему через ієрархію пам'яті — багаторівневу систему, де кожен рівень пропонує різний компроміс між швидкістю, вартістю та ємністю.Ієрархія, від найшвидшого до найповільнішого: Регістри (десятки байтів): інтегровані безпосередньо в ядро процесора. Час доступу вимірюється в пікосекундах. Найвища швидкість, але надзвичайно обмежена кількість — лише 16-32 регістри загального призначення в більшості архітектур. Кеш (кілобайти до мегабайтів): побудований на SRAM, розташований на кристалі процесора або дуже близько до нього. Сучасні процесори мають кілька рівнів кешу (L1, L2, L3) зі зростаючим розміром та затримкою. Кеш L1 може мати доступ за 1-4 такти процесора; L3 може займати 30-50 тактів. Основна пам'ять (гігабайти): побудована на DRAM, підключена до процесора через контролери пам'яті та шини. Час доступу 50-100 наносекунд — сотні тактів процесора. Але ємність вимірюється в гігабайтах за розумну ціну. Накопичувачі (терабайти): SSD та жорсткі диски. Час доступу в мікросекундах (SSD) до мілісекунд (HDD). Значно повільніші за оперативну пам'ять, але енергонезалежні та надзвичайно ємні. Ця ієрархія використовує критично важливу властивість реальних програм: локальність звернень. Програми схильні звертатися до тих самих даних повторно (часова локальність) та звертатися до даних поблизу раніше використаних даних (просторова локальність). Зберігаючи нещодавно використані дані на швидких верхніх рівнях ієрархії, системи створюють ілюзію великої, швидкої пам'яті, фактично платячи здебільшого за велику, повільну пам'ять.Ієрархія пам'яті — це не просто оптимізація, вона є фундаментальною для комп'ютерної архітектури. Без неї комп'ютери були б або неприйнятно дорогими, або непридатно повільними. Розуміння того, як дані рухаються через цю ієрархію, є суттєвим для розуміння того, чому сучасні комп'ютери працюють так, як вони працюють, і це буде повторюваною темою протягом усього курсу.8 червня 1978 року Intel випустила мікропроцесор 8086 — чіп, який докорінно змінив комп'ютерну індустрію [3]. Цей 16-бітний процесор, реалізований приблизно з 20 000 активних транзисторів, дав початок тому, що ми зараз називаємо архітектурою x86, яка стала найуспішнішою та найдовговічнішою лінійкою процесорів Intel.Походження 8086 розкриває цікавий поворот корпоративної стратегії. Проєкт розпочався в травні 1976 року як те, що Intel планувала як тимчасову заміну, поки вони працювали над своїм справді амбітним проєктом: iAPX 432 [3]. iAPX 432 мав стати революційною 32-бітною архітектурою з передовими можливостями, такими як адресація на основі можливостей та апаратна підтримка об'єктно-орієнтованого програмування. Intel очікувала, що він стане їхньою флагманською лінійкою продуктів. Натомість «тимчасовий» 8086 став фундаментом обчислень на наступні п'ять десятиліть, тоді як iAPX 432 зник у небутті як комерційний провал.Успіх 8086 не був випадковим. Стівен Морс, який визначив архітектуру, прийняв критично важливе проєктне рішення: 8086 використовуватиме схожу архітектуру до існуючих 8-бітних мікропроцесорів Intel (8008, 8080 та 8085) [3]. Ця зворотна сумісність означала, що програми на асемблері могли безперешкодно мігрувати зі старих 8-бітних платформ на новий 16-бітний процесор. Програмістам не потрібно було вивчати цілком нову систему — вони могли використовувати свої наявні знання та код. Ця філософія проєктування підтримки зворотної сумісності характеризує розвиток x86 з тих пір.Часові рамки розробки також примітні: створення 8086 зайняло трохи більше двох років від початкової концепції до працюючого продукту [3]. В епоху до сучасних інструментів автоматизації електронного проєктування це було видатним досягненням, яке продемонструвало інженерні можливості Intel.<br><img alt="Чіп Intel 8086 у корпусі DIP-40" src="assets/l03-intel-8086-chip.jpeg" target="_self">Ви можете слушно запитати: навіщо вивчати процесор 1978 року в сучасному курсі комп'ютерної архітектури? Відповідь полягає в розумінні того, що 8086 представляє найпростішу версію архітектури x86, яка залишається безпосередньо актуальною для сучасних обчислень.Кожен процесор x86-64, з яким ви зустрічаєтесь сьогодні — чи то Intel Core i9, чи AMD Ryzen — підтримує зворотну сумісність з оригінальним набором інструкцій 8086. Коли ваш комп'ютер завантажується, він фактично запускається в режимі, який є по суті покращеним 8086 (так званий «реальний режим»), перш ніж перейти до сучасної 64-бітної роботи. BIOS-код, завантажувачі та певні низькорівневі системні функції досі працюють, використовуючи концепції, що безпосередньо походять з 1978 року.Сучасні можливості x86 є принципово розширеннями концепцій 8086, а не їх замінами:Розуміння 8086 — це як розуміння коріння дерева: ви не можете повністю осягнути складну розгалужену структуру над землею, не знаючи, що лежить під нею. Студенти, які опанували концепції 8086, виявляють, що вивчення сучасних можливостей x86-64 стає питанням розуміння розширень, а не вивчення чогось цілком нового.Тут ми стикаємося з однією з найцікавіших проєктних задач 8086. Процесор має 16-бітні регістри — кожен може містити значення від 0 до 65 535. Якби ми використовували один регістр для вказівки адрес пам'яті, ми могли б адресувати щонайбільше 2^16 = 65 536 байтів, або 64 КБ пам'яті.Але інженери Intel хотіли, щоб 8086 адресував більше пам'яті. Вони оснастили процесор 20-бітною адресною шиною, здатною адресувати 2^20 = 1 048 576 байтів = 1 МБ пам'яті. Відома (і, ймовірно, апокрифічна) цитата «640 КБ має бути достатньо для будь-кого» насправді стосувалася використовуваної частини цього 1 МБ адресного простору, з верхніми 384 КБ, зарезервованими для системних функцій.Це створило фундаментальну невідповідність: як генерувати 20-бітні адреси, коли ваш найбільший регістр може містити лише 16 бітів? Ви не можете просто додати більше бітів до регістрів — це змінило б усю архітектуру процесора. Рішення Intel було елегантним та впливовим: адресація сегмент:зміщення.]]></description><link>lectures/_temp_uk_part_04.html</link><guid isPermaLink="false">Lectures/_temp_uk_part_04.md</guid><pubDate>Wed, 28 Jan 2026 21:15:09 GMT</pubDate><enclosure url="assets/l03-ram-abstraction.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-ram-abstraction.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[_temp_uk_part_05]]></title><description><![CDATA[Кількісно оцінімо проблему адресації, з якою зіткнулися інженери Intel [4]. 16-бітний регістр може зберігати значення від 0 до 65 535, тобто може безпосередньо адресувати щонайбільше 64 КБ пам'яті. Але 20-бітна адресна шина 8086 могла досягати 1 МБ пам'яті — у шістнадцять разів більше, ніж міг адресувати один регістр.Одним наївним рішенням було б поділити адресний простір 1 МБ на 16 фіксованих сегментів по 64 КБ кожен. Знадобилося б лише 4 біти для вибору сегмента (2^4 = 16), залишивши 16-бітний регістр для вказування зміщення в межах цього сегмента. Це працювало б, але було б негнучким — програми та дані мали б акуратно вміщуватися в межі 64 КБ, і не можна було б легко розмістити структуру даних, що охоплювала два сегменти.Intel обрала гнучкіший підхід: перекриття сегментів [4].Схема адресації сегмент:зміщення використовує два 16-бітні значення для вказування місця в пам'яті: значення сегмента та значення зміщення. Фізична (абсолютна) адреса обчислюється за такою формулою:Абсолютна адреса = (Сегмент × 16) + Зміщення<img alt="Візуалізація формули Сегмент:Зміщення" src="assets/l03-segment-offset-formula.png" target="_self">
Чому множимо на 16? У шістнадцяткових термінах множення на 16 еквівалентне зсуву вліво на одну шістнадцяткову цифру — по суті, дописування нуля справа від числа. Це перетворює 16-бітне значення сегмента на 20-бітну базову адресу, до якої ми додаємо 16-бітне зміщення.Загальноприйнятий запис — Сегмент:Зміщення, де обидва значення записуються у шістнадцятковій системі. Наприклад, F000:FFFD означає сегмент F000h зі зміщенням FFFDh.Розберімо кілька прикладів, щоб сформувати інтуїцію щодо цієї схеми адресації [4].Приклад 1: F000:FFFDЦя адреса представляє місце у верхній області пам'яті, яка зазвичай використовується для коду BIOS: Сегмент: F000 Сегмент × 16: F0000 (зсув вліво на одну шістнадцяткову цифру)
+ Зміщення: + FFFD ------- Абсолютна: FFFFD = 1 048 573 у десятковій системі
Ця адреса дуже близька до вершини адресного простору 1 МБ (який закінчується на FFFFF = 1 048 575).Приклад 2: 923F:E2FFБільш довільна адреса, що демонструє арифметику [4]: Сегмент: 923F Сегмент × 16: 923F0 (зсув вліво на одну шістнадцяткову цифру)
+ Зміщення: + E2FF ------- Абсолютна: A06EF = 657 135 у десятковій системі
Зверніть увагу, як додавання може породжувати переноси, що поширюються через шістнадцяткові цифри.Приклад 3: Максимально можлива адреса (FFFF:FFFF)Що станеться, коли ми максимізуємо і сегмент, і зміщення? Сегмент: FFFF Сегмент × 16: FFFF0 (зсув вліво на одну шістнадцяткову цифру)
+ Зміщення: + FFFF ------- Абсолютна: 10FFEF = 1 114 095 у десятковій системі
Це 21-бітна адреса! Вона перевищує межу 1 МБ (яка закінчується на FFFFF). Це переповнення створює так звану «Верхню область пам'яті» (High Memory Area, HMA) — додаткові 65 520 байтів, які стали доступними на пізніших процесорах. На оригінальному 8086 ця адреса «загорталася б» через обмеження 20-бітної адресної шини.Ключова характеристика цієї схеми адресації полягає в тому, що сегменти значно перекриваються [4]. Кожен новий сегмент починається лише через 16 байтів (один «параграф» у термінології x86) після попереднього. Оскільки весь адресний простір містить 1 048 576 байтів, а сегменти починаються кожні 16 байтів, існує 65 536 можливих початкових точок сегментів.<br><img alt="Як 64-КБ сегменти перекриваються в адресному просторі 8086" src="assets/l03-8086-segment-overlap.png" target="_self">
Ця природа перекриття означає, що одна фізична адреса може бути представлена багатьма різними парами сегмент:зміщення [4]. Фактично, для будь-якої заданої фізичної адреси може існувати до 4096 різних представлень сегмент:зміщення!Розгляньмо фізичну адресу 7C00h — історично значущу як місце, куди BIOS завантажує завантажувальний сектор. Цю адресу можна записати як:Усі вони посилаються на той самий фізичний байт у пам'яті. Код завантажувального сектора традиційно використовує або 0000:7C00 (на основі зміщення), або 07C0:0000 (на основі сегмента), і розуміння цієї еквівалентності є суттєвим при налагодженні або написанні низькорівневого коду.Схема сегмент:зміщення від Intel, хоча й здається складною, пропонувала значні переваги з урахуванням апаратних обмежень 1978 року:Переміщення програм: Програми можна було переміщувати в пам'яті просто змінюючи значення сегментних регістрів. Код на основі зміщень у програмі не потребував змін — лише базу сегмента потрібно було скоригувати. Це робило керування пам'яттю значно гнучкішим.Захист пам'яті (концептуальний): Різні сегменти можна було використовувати для різних цілей — код в одному сегменті, дані в іншому. Хоча 8086 не забезпечував дотримання меж сегментів апаратно, це концептуальне розділення заклало основу для захисту пам'яті у захищеному режимі в пізніших процесорах.Ефективні 16-бітні операції: Вся арифметика над зміщеннями використовувала 16-бітні операції, які процесор міг виконувати ефективно. Множення сегмента на 16 було простою операцією зсуву.8086 надає чотири спеціалізовані сегментні регістри, кожен з яких виконує певну функцію у виконанні програми:<br><img alt="Налагоджувач AFD показує сегменти коду та даних" src="assets/l03-afd-segments-annotated.png" target="_self">
Ці сегментні регістри не є регістрами загального призначення — ви не можете використовувати їх для арифметики або як тимчасове сховище. Вони існують виключно для надання сегментної частини адрес пам'яті. Процесор автоматично вибирає, який сегментний регістр використовувати, залежно від типу доступу до пам'яті.Коли процесор вибирає наступну інструкцію, він автоматично використовує CS. Коли ваш код читає або записує дані (наприклад, MOV AX, [BX]), він автоматично використовує DS. При додаванні до стека або вилученні зі стека він автоматично використовує SS. Цей автоматичний вибір спрощує програмування, зберігаючи гнучкість адресації на основі сегментів.Типова програма для 8086 організовує пам'ять у окремі сегменти, які можуть перекриватися або не перекриватися:Сегмент коду (CS) містить виконувані інструкції програми. Процесор вибирає інструкції з адрес, обчислених як (CS × 16) + IP. Цей сегмент зазвичай розглядається як тільки для читання під час виконання — код, що самомодифікується, існує, але загалом вважається поганою практикою.Сегмент даних (DS) містить змінні, константи та рядки програми. Коли ви оголошуєте змінну мовою асемблера та звертаєтеся до неї за іменем, асемблер генерує код, який використовує DS як сегмент за замовчуванням. Тут розміщуються як ініціалізовані дані (змінні з початковими значеннями), так і неініціалізовані дані (зарезервований простір).Сегмент стека (SS) містить стек часу виконання — область пам'яті, що використовується для викликів функцій, адрес повернення та локальних змінних. Стек працює за дисципліною LIFO (останнім прийшов — першим вийшов) і зростає вниз у пам'яті (до нижчих адрес).Додатковий сегмент (ES) надає другий сегмент даних, особливо корисний для операцій з рядками, які потребують копіювання даних з однієї області пам'яті в іншу. Інструкції на кшталт MOVSB (переміщення байта рядка) читають з DS:SI і записують в ES:DI, дозволяючи ефективну передачу пам'ять-пам'ять.У малих програмах усі ці сегменти можуть вказувати на одну й ту саму область пам'яті («крихітна» модель пам'яті). У більших програмах вони вказують на повністю окремі області по 64 КБ, дозволяючи безпосередньо адресувати до 256 КБ пам'яті (4 сегменти × 64 КБ).При налагодженні коду 8086 ви побачите значення сегментних регістрів, відображені в налагоджувачі. Розуміння того, як обчислювати фізичні адреси з цих значень, є суттєвим для відстеження виконання програми.Розгляньмо налагоджувач, що показує:
CS = 0F78h
DS = 0F7Bh
IP = 0100h
Фізична адреса наступної інструкції:(CS × 16) + IP = (0F78h × 16) + 0100h = 0F780h + 0100h = 0F880h
Щоб зрозуміти співвідношення між сегментами коду та даних:DS - CS = 0F7Bh - 0F78h = 3 (сегменти)
Фізичний проміжок = 3 × 16 = 48 байтів
Це означає, що сегмент даних починається через 48 байтів після початку сегмента коду. У малій програмі цей проміжок може містити рівно 48 байтів коду, а дані йдуть одразу після.<br><img alt="Налагоджувач AFD показує обчислення відстані між сегментами" src="assets/l03-afd-data-segment.png" target="_self">Комбінація CS (сегмент коду) та IP (вказівник інструкцій) визначає, звідки процесор вибирає свою наступну інструкцію. Ця пара працює як нероздільна одиниця:Фізична адреса наступної інструкції = (CS × 16) + IPПроцесор автоматично збільшує IP після вибірки кожної інструкції. Коли IP досягає FFFFh і збільшується, він переходить до 0000h — але CS автоматично не змінюється. Це означає, що виконання залишається в межах поточного 64-КБ сегмента коду, якщо далекий перехід або виклик явно не завантажить нове значення CS.Інструкції переходу бувають двох типів:
Близькі переходи змінюють лише IP, залишаючись у межах поточного сегмента коду
Далекі переходи змінюють і CS, і IP, дозволяючи переходити в будь-яке місце адресного простору 1 МБ
Розуміння цієї відмінності є критичним при написанні або аналізі завантажувального коду, драйверів пристроїв або будь-якого програмного забезпечення, що має працювати через межі сегментів.Регістри SS (сегмент стека) та SP (вказівник стека) працюють разом для керування стеком програми:Фізична адреса вершини стека = (SS × 16) + SPСтек має кілька характерних особливостей:Зростання вниз: На відміну від більшості структур даних, що зростають вгору (до вищих адрес), стек x86 зростає вниз. Коли ви додаєте значення (push), SP зменшується. Коли ви вилучаєте значення (pop), SP збільшується. Ця домовленість бере початок від ранніх конструкцій комп'ютерів і зберігається в x86 донині.Вирівнювання по слову: На 8086 операції зі стеком працюють з 16-бітними словами. PUSH зменшує SP на 2, потім зберігає значення. POP отримує значення, потім збільшує SP на 2.Автоматичне використання: Процесор автоматично використовує SS:SP для кількох операцій:
Інструкції PUSH та POP
CALL (додає адресу повернення) та RET (вилучає адресу повернення)
Обробка переривань (додає прапорці, CS та IP)
Типова ініціалізація стека може встановити SS для вказування на область пам'яті, а SP — на вершину цієї області (наприклад, SP = FFFEh для майже повного 64-КБ сегмента стека). Під час виконання програмою функцій і додавання даних SP зменшується. Якщо SP досягає 0000h і програма додає більше даних, відбувається переповнення стека — одна з найвідоміших помилок в історії обчислень.Співвідношення між SS та SP ілюструє філософію сегмент:зміщення: SS надає базове розташування стека у фізичній пам'яті, тоді як SP відстежує поточну позицію в межах цієї області стека. Зміна SS — і ви перемикаєтеся на цілком інший стек. Зміна SP — і ви переміщуєтеся в межах поточного стека.]]></description><link>lectures/_temp_uk_part_05.html</link><guid isPermaLink="false">Lectures/_temp_uk_part_05.md</guid><pubDate>Wed, 28 Jan 2026 21:14:30 GMT</pubDate><enclosure url="assets/l03-segment-offset-formula.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-segment-offset-formula.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L03-ENG. x86 Architecture - Registers and Memory]]></title><description><![CDATA[Course: Computer Systems Architecture
Topic: From Wiring to Registers, Memory Addressing
Learning Outcomes: LO1 (Architecture principles), LO2 (Data representation), LO3 (Assembly programming)In the previous lecture, we built circuits that operate on binary data: flip-flops storing 0s and 1s, adders processing streams of bits, and an ALU controlled by binary operation codes. Binary is the native language of digital hardware, but it presents a practical problem for human programmers: binary numbers are long and difficult to read. The number 255 requires eight binary digits (11111111), and a 32-bit address becomes an unwieldy string of thirty-two 0s and 1s. We need a more compact notation that still maps cleanly to the underlying binary representation.The hexadecimal (base-16) number system provides exactly this convenience. Hexadecimal has become the standard notation in systems programming, debuggers, memory dumps, and assembly language because it offers a perfect balance: compact representation for humans while maintaining a trivial conversion to and from binary.The key insight behind hexadecimal is mathematical: 16 is a power of 2 (specifically, 2^4 = 16). This means that exactly four binary bits correspond to one hexadecimal digit. This relationship makes conversion between binary and hexadecimal purely mechanical, requiring no arithmetic, just grouping and table lookup.Consider the alternative: decimal (base-10) has no clean relationship to binary. Converting between decimal and binary requires repeated division or multiplication. But with hexadecimal, you simply partition the binary number into groups of four bits and translate each group independently.For example, the binary number 1111 0101 converts to hexadecimal by looking up each group:
1111 = F (decimal 15)
0101 = 5 (decimal 5)
Result: F5 in hexadecimal
No calculation required, just pattern matching. This makes hexadecimal invaluable when examining raw memory contents or machine code.Since base-16 requires sixteen distinct digit symbols and our decimal system only provides ten (0-9), hexadecimal uses the letters A through F to represent values 10 through 15:<img alt="Hexadecimal digits 0-F with their 4-bit binary equivalents" src="assets/l03-hex-binary-conversion.png" target="_self">Each hexadecimal digit maps directly to a unique 4-bit binary pattern, making conversion between hex and binary purely mechanical.This table should become second nature to anyone working with computer systems. Memorizing the binary patterns for each hex digit enables instant mental conversion between representations.Note that hexadecimal is case-insensitive: FF, ff, and Ff all represent the same value (decimal 255). Different tools and programming languages have various conventions, but the meaning is identical.Binary to Hexadecimal:To convert binary to hexadecimal, group the bits into sets of four starting from the right (the least significant bit). If the leftmost group has fewer than four bits, pad with leading zeros.Example: Convert 110101110 to hexadecimal.
Group into fours from right: 1 1010 1110
Pad the leftmost group: 0001 1010 1110
Convert each group: 1, A, E
Result: 1AE (which equals 430 in decimal)
Hexadecimal to Binary:Simply expand each hex digit to its 4-bit binary equivalent.Example: Convert 3F7 to binary.
3 = 0011
F = 1111
7 = 0111
Result: 0011 1111 0111 = 001111110111
Hexadecimal to Decimal:Use positional notation with powers of 16.Example: Convert 2A3 to decimal.2A3 = 2 × 16² + A × 16¹ + 3 × 16⁰ = 2 × 256 + 10 × 16 + 3 × 1 = 512 + 160 + 3 = 675
Decimal to Hexadecimal:Repeatedly divide by 16, collecting remainders from bottom to top.Example: Convert 500 to hexadecimal.500 ÷ 16 = 31 remainder 4
31 ÷ 16 = 1 remainder 15 (F)
1 ÷ 16 = 0 remainder 1
Reading bottom to top: 1F4
Verification: 1×256 + 15×16 + 4×1 = 256 + 240 + 4 = 500.A byte consists of 8 bits, which conveniently equals exactly two hexadecimal digits. This is not coincidental; the byte became the fundamental unit of computer memory precisely because 8 bits provide a useful range (0-255 for unsigned values, or -128 to +127 for signed) while remaining a power of 2.Since one hex digit represents 4 bits (a "nibble"), we have:
1 byte = 8 bits = 2 hex digits (00 to FF)
16 bits = 2 bytes = 4 hex digits (0000 to FFFF)
32 bits = 4 bytes = 8 hex digits (00000000 to FFFFFFFF)
64 bits = 8 bytes = 16 hex digits
Common architecture sizes have evolved through computer history:
8-bit architectures (early microprocessors like Intel 8080): registers and data paths 8 bits wide
16-bit architectures (Intel 8086): 16-bit registers, the foundation of x86
32-bit architectures (Intel 80386, often called "x86"): extended registers to 32 bits
64-bit architectures (AMD64/x86-64): current standard with 64-bit registers
Each generation doubled the register width, and hexadecimal notation scaled perfectly: an 8-bit value fits in 2 hex digits, a 16-bit value in 4, a 32-bit value in 8, and a 64-bit value in 16.Different programming languages and tools use various prefixes or suffixes to indicate hexadecimal numbers:In this course, we will primarily use the h suffix convention common in x86 assembly language, where 11h means hexadecimal 11 (decimal 17), not decimal 11.Hexadecimal is ubiquitous in low-level programming for several practical reasons:
Memory addresses are displayed in hex in debuggers and memory dumps
Machine code opcodes are documented and displayed in hex
Bit patterns for flags, masks, and permissions are clearer in hex
Color values in graphics use hex (RGB as three bytes)
Network protocols specify bytes in hex
When you see mov ax, 1234h in assembly code, you immediately know this is a 16-bit value (4 hex digits = 16 bits) loading a specific bit pattern. When examining a memory dump showing 48 65 6C 6C 6F, you can recognize this as ASCII text ("Hello") by knowing the hex codes for letters.Hexadecimal is not merely a notational convenience. It is the standard language for discussing the precise binary values that computers actually manipulate.In the previous lecture, we saw how the ALU uses a binary decoder to select which operation to perform. The operation code (opcode) bits are fed into a decoder that activates exactly one functional unit, whether the adder, the subtractor, the AND circuit, or another component. This is not an abstract software concept. It is physical wiring: certain bit patterns in the opcode literally route signals to specific hardware modules.This section explores how instructions are encoded as bit patterns and how those patterns determine what the hardware does. Understanding instruction encoding reveals why processor architectures have the characteristics they do, including the number of registers, the instruction formats, and the limitations we encounter in assembly programming.Every machine instruction is fundamentally a command to route signals through the processor's circuitry. When you write ADD AX, BX in assembly language, the assembler translates this into a specific bit pattern. When that bit pattern reaches the processor, different fields within it control different aspects of execution:
Some bits select which operation (add, subtract, move, etc.)
Some bits specify the source operand(s)
Some bits specify the destination
Some bits may indicate addressing modes or other modifiers
These bits directly connect to the decoder circuits we built in Lecture 2. A 3-bit opcode field feeds into a 3-to-8 decoder, activating one of eight possible operation circuits. A 2-bit register field selects one of four registers. The encoding is not arbitrary; it reflects the physical structure of the processor.<br><img alt="Assembly instructions with gate-level annotations" src="assets/l03-instruction-types-gates.png" target="_self">Assembly instructions map to specific gate configurations in the processor hardware. Each instruction type activates different pathways through the ALU and register file.Consider a simplified 8-bit instruction format for a hypothetical processor with 4 registers (R0, R1, R2, R3) and 4 operations:Bit positions: 7 6 5 4 3 2 1 0 [--op--][-src-][-dst-] Bits 7-6 (op): Operation code, selects one of 4 operations
Bits 5-4 (src): Source register, selects one of 4 registers
Bits 3-2 (dst): Destination register, selects one of 4 registers
Bits 1-0: Reserved or unused
Let us define the operations:
00 = ADD (add source to destination)
01 = SUB (subtract source from destination)
10 = MOV (move source to destination)
11 = AND (bitwise AND)
And the registers:
00 = R0
01 = R1
10 = R2
11 = R3
Consider the instruction byte 01 00 01 00 (binary), which is 44h in hexadecimal.Breaking it into fields:
01 (bits 7-6) = SUB operation
00 (bits 5-4) = R0 as source
01 (bits 3-2) = R1 as destination
00 (bits 1-0) = unused
This instruction means: SUB R0, R1 or "subtract R0 from R1, storing result in R1."Let us trace another example: 00 01 00 01 binary = 11h hex.
00 = ADD operation
01 = R1 as source
00 = R0 as destination
01 = unused
This is: ADD R1, R0 or "add R1 to R0."<br><img alt="Machine code instruction encoding showing opcode and operand fields" src="assets/l03-instruction-encoding.png" target="_self">The bit fields within an instruction byte encode the operation type, source register, and destination register. Each field maps to specific decoder circuits in the hardware.When instruction byte 11h arrives at the processor: Bits 7-6 (00) feed into the operation decoder. The decoder's output 0 goes HIGH, activating the ADD circuit while keeping SUB, MOV, and AND circuits inactive. Bits 5-4 (01) feed into the source register selector. This is a multiplexer that routes the contents of R1 to the ALU's "B" input. Bits 3-2 (00) feed into the destination register selector. This routes the ALU output back to R0 and also routes R0's current contents to the ALU's "A" input. The ALU performs addition on its A and B inputs. The result is stored in the destination register (R0). Every single bit has a purpose. Change bit 6 from 0 to 1, and the instruction becomes SUB instead of ADD. Change bits 5-4 from 01 to 10, and R2 becomes the source instead of R1.<br><img alt="CPU block diagram showing ALU, decoder, and register connections" src="assets/l03-cpu-alu-decoder.png" target="_self">The CPU decoder receives instruction bits and activates the appropriate ALU operation and register pathways. The opcode bits directly control which functional unit performs the computation.Here is a critical insight: the number of bits allocated for register selection determines the maximum number of registers the architecture can have.With 2 bits for register selection, we can address 2^2 = 4 registers (00, 01, 10, 11).
With 3 bits, we could address 2^3 = 8 registers.
With 4 bits, we could address 2^4 = 16 registers.Question for students: If an instruction format uses 2 bits to specify each of two register operands, how many different registers can the architecture support?Answer: Four registers maximum. The 2-bit field can only encode values 0, 1, 2, and 3.This explains why early processors had so few registers. The Intel 8086 has only 8 general-purpose registers (requiring 3 bits each to address), and even this modest number consumes significant instruction encoding space. Having 32 registers would require 5 bits per register field, making instructions longer and more complex to decode.Modern processors like x86-64 extended to 16 general-purpose registers by adding a prefix byte (the REX prefix) to instructions, effectively adding extra bits to the register selection fields. This backward-compatible approach allowed new capabilities while maintaining compatibility with existing 8-register code.Beyond instruction encoding, registers have physical costs: Wiring complexity: Each register must connect to the ALU inputs and outputs. With N registers and an M-bit ALU, you need multiplexers with N×M input wires just for routing data to the ALU. Decoder size: Register selection requires N-input decoders, which grow exponentially in gate count. Area on chip: Each register consists of M flip-flops plus routing logic. More registers mean more silicon area. Power consumption: More transistors means more power, more heat, and more cooling requirements. These constraints explain the historical evolution from 4-register machines to 8, then 16, and now 32 in some RISC architectures. Each expansion required advances in manufacturing technology to make the additional wiring practical.The Intel 8086, which we will study in detail, uses variable-length instructions with complex encoding. However, the fundamental principle remains: specific bit patterns in the machine code activate specific hardware pathways. When you write ADD AX, BX and the assembler produces the bytes 01 D8, those bytes contain encoded information about:
The operation (ADD)
The operand size (16-bit)
The source register (BX)
The destination register (AX)
Learning to read and understand these encodings deepens your understanding of what the processor actually does when it executes your code.In our simplified 4-register architecture, all registers are not equal. Register R0 plays a special role: it is the accumulator. This designation is not arbitrary but reflects deliberate hardware design choices that date back to the earliest computers and persist in modern x86 processors.An accumulator is a register that is hardwired to receive the results of arithmetic and logic operations. When the ALU computes a sum, difference, or logical result, the output automatically goes to the accumulator without requiring an explicit destination specification in every instruction.In our example architecture:
ADD R1 means "add R1 to R0, store result in R0"
SUB R2 means "subtract R2 from R0, store result in R0"
The accumulator (R0) is implicit. It is both a source operand and the destination. This design has several important consequences.1. Simplified Instruction EncodingWith a dedicated accumulator, arithmetic instructions need only specify one operand, not two. Instead of encoding "ADD source, destination" (requiring bits for both), we encode only "ADD source" (the destination is always the accumulator).In our hypothetical format:
With explicit destination: [op:2][src:2][dst:2][unused:2] = 8 bits
With implicit accumulator: [op:2][src:2][unused:4] = 8 bits, but 4 bits are freed
Those freed bits could encode more operations, more source registers, or additional addressing modes.2. Reduced Wiring ComplexityThe ALU output needs only one permanent path: to the accumulator. Without an accumulator design, the ALU output would need a multiplexer to route results to any of the N registers, requiring N×M wires (N registers, M bits each) plus selection logic.With an accumulator:
ALU output connects directly to R0's input
No output multiplexer needed
Simpler, faster, fewer transistors
<br><img alt="Digital simulator showing registers A, B connected to ALU" src="assets/l03-digital-simulator-cpu.png" target="_self">In this digital simulator view, register A (the accumulator) is hardwired to the ALU output. Register B provides the second operand for binary operations. This fixed wiring eliminates the need for output multiplexing.3. Faster ExecutionBecause the output path is fixed, there is no multiplexer delay on the result. The carry chain in an adder is already the critical timing path; adding multiplexer delay would slow every arithmetic operation.The accumulator design shines for sequential calculations. Consider computing (a + b + c + d):Without accumulator (two-operand instructions):MOV R0, a ; R0 = a
ADD R0, b ; R0 = a + b
ADD R0, c ; R0 = a + b + c
ADD R0, d ; R0 = a + b + c + d
With accumulator (one-operand instructions):LOAD a ; ACC = a
ADD b ; ACC = a + b
ADD c ; ACC = a + b + c
ADD d ; ACC = a + b + c + d
The accumulator version has shorter instructions (one operand instead of two) and naturally "accumulates" the running total, which is exactly how the name originated.This pattern is extremely common in real programs: read a value, process it, add to a running total, repeat. The accumulator architecture optimizes for exactly this pattern.When Intel designed the 8086, they inherited the accumulator concept from earlier processors. The AX register (Accumulator eXtended) serves this special role: Multiplication and division use AX (and DX for extended results) implicitly MUL BX multiplies AX by BX, storing the result in DX:AX
DIV BX divides DX:AX by BX, quotient in AX, remainder in DX I/O instructions use AX exclusively IN AL, port reads a byte from an I/O port into AL
OUT port, AX writes AX to an I/O port String instructions use AX as the source/destination for data LODSB loads a byte from memory into AL
STOSW stores AX to memory Some instructions have shorter encodings when using AX ADD AX, 5 may encode in fewer bytes than ADD BX, 5 Even though modern x86 processors allow most operations with any register, the accumulator heritage persists in instruction encodings and specialized instructions.The accumulator concept scaled with architecture width:
8-bit: AL (Accumulator Low byte)
16-bit: AX (Accumulator eXtended) = AH:AL
32-bit: EAX (Extended AX)
64-bit: RAX (Register AX in 64-bit mode)
These are not separate registers but overlapping views of the same physical storage:
Writing to AL affects the low 8 bits of AX, EAX, and RAX
Writing to AX affects the low 16 bits of EAX and RAX
Writing to EAX zero-extends to fill RAX (in 64-bit mode)
The accumulator concept dates to the earliest electronic computers. The ENIAC (1945) had accumulators. The IBM 701 (1952) had an accumulator. Early microprocessors like the Intel 8008 (1972) and 8080 (1974) had a single accumulator register called A.This design made sense when transistors were expensive. Every register required dozens of transistors, so minimizing register count was essential. Having one special register that received all arithmetic results allowed simpler instruction formats and reduced wiring.As transistors became cheaper (following Moore's Law), architectures added more general-purpose registers. RISC architectures like ARM and MIPS have 16-32 registers where any register can serve as source or destination for any operation. But even these "load-store" architectures maintain some accumulator-like conventions for certain operations.The accumulator design involves a trade-off:Modern x86 processors support both styles: instructions with implicit accumulator (for backward compatibility and compact encoding) and instructions with explicit operands (for flexibility). This hybrid approach reflects the architecture's evolution over four decades.Understanding the accumulator concept helps explain why assembly language looks the way it does, why certain registers have special purposes, and why some instructions have shorter encodings than others. It is a window into the hardware-software interface that defines how programs actually execute.Registers are the fastest storage locations in any computer system—small memory cells built directly into the CPU that hold the data currently being processed. Understanding the evolution of x86 registers from their 16-bit origins to modern 64-bit implementations reveals both the history of personal computing and the fundamental architectural decisions that continue to influence software development today.When Intel designed the 8086 processor in 1978, they created four primary general-purpose registers, each 16 bits wide. These registers were given names that reflected their intended specialized purposes, though in practice programmers have always used them somewhat interchangeably [1].The Accumulator (AX) earned its name because arithmetic operations naturally accumulated their results here. When you add two numbers or perform a multiplication, the result appears in AX (or in the DX:AX pair for larger results). Many instructions were optimized to work specifically with AX, making operations on this register faster and requiring shorter machine code [1].The Base register (BX) served as the primary register for memory addressing in the original 8086 design. When accessing data stored in memory, programmers would load the base address of a data structure into BX and then use offsets to reach individual elements. This made BX essential for implementing arrays and complex data structures.The Counter register (CX) was designed specifically for loop operations. The 8086 instruction set includes special loop instructions (like LOOP) that automatically decrement CX and jump back if the count hasn't reached zero. This hardware support for counting made CX the natural choice for any repetitive operation, including string manipulations where the same operation must be applied to each character.The Data register (DX) functions as an extension of AX for operations that produce results too large for a single 16-bit register. When multiplying two 16-bit numbers, the 32-bit result appears with its high word in DX and its low word in AX, forming the DX:AX pair. Similarly, DX holds the high portion of the dividend before division and the remainder afterward. DX also serves as the port address register for I/O operations, allowing the processor to communicate with up to 65,536 different hardware devices.One of the most elegant features of the 8086 register architecture is the ability to access individual bytes within these 16-bit registers. Each of the four primary registers can be split into two 8-bit halves, giving programmers fine-grained control over their data [1].The naming convention uses H (High) and L (Low) suffixes:This design effectively provides eight 8-bit registers while using only four 16-bit physical registers. The key insight is that AH, AL, and AX all refer to portions of the same underlying hardware register. When you modify AL, you're changing the lower 8 bits of AX. When you modify AH, you're changing the upper 8 bits of AX. This overlapping access pattern was revolutionary for its time and remains fundamental to x86 programming.The practical benefits of byte-level access are substantial. Character manipulation becomes straightforward—ASCII characters fit in a single byte, so AL can hold one character while AH holds another. Programmers can pack two bytes of data into a single register, process them together, and extract results efficiently. When interfacing with 8-bit peripherals or processing byte-oriented data structures, the ability to work with individual bytes without masking and shifting operations saves significant time.Consider a simple example: if AX contains the hexadecimal value 4142h, then AH contains 41h (the ASCII code for 'A') and AL contains 42h (the ASCII code for 'B'). Modifying just AL to 43h changes AX to 4143h, while AH remains unchanged at 41h.<br><img alt="Original 8086 16-bit registers" src="assets/l03-8086-registers-basic.png" target="_self">The original 8086 register set: four 16-bit general-purpose registers (AX, BX, CX, DX) with high/low byte access (AH/AL, BH/BL, CH/CL, DH/DL), plus segment registers for memory addressing.The Intel 80386, released in 1985, brought 32-bit computing to the x86 architecture. Rather than designing an entirely new register set, Intel extended the existing registers while maintaining backward compatibility—a decision that has defined x86 evolution ever since [1].The extension strategy used the prefix E (for "Extended") to create 32-bit versions of all existing registers:When you write a value to EAX, you're using all 32 bits. The lower 16 bits of EAX are accessible as AX, and within AX, the individual bytes remain accessible as AH and AL. However, the upper 16 bits of EAX (bits 31-16) have no dedicated name—they can only be accessed through EAX itself or through bit manipulation operations.The 80386 also added four additional 32-bit registers that had no 8-bit subdivisions:ESI (Source Index) and EDI (Destination Index) were designed for string operations—copying, comparing, or scanning blocks of memory. ESI typically points to the source data, while EDI points to the destination. These registers enable efficient implementation of functions like memcpy() and strcmp() in C.ESP (Stack Pointer) is perhaps the most critical register in the system, always pointing to the current top of the stack. Every function call, local variable allocation, and interrupt relies on ESP being correct. The processor automatically decrements ESP when pushing data and increments it when popping. Because of this critical role, ESP should never be used for general-purpose computation [1].EBP (Base Pointer) serves as a stable reference point within stack frames. When a function is called, EBP is typically set to the current value of ESP, creating a fixed frame of reference. Local variables are then accessed as negative offsets from EBP (EBP-4, EBP-8, etc.), while function parameters appear as positive offsets (EBP+8, EBP+12, etc.). This convention makes debugging significantly easier and allows functions to have variable-size local data [1].<br><img alt="32-bit x86 general-purpose registers" src="assets/l03-x86-32bit-registers.png" target="_self">The 32-bit x86 register set introduced with the 80386: extended versions of the original registers (EAX, EBX, ECX, EDX) plus index and pointer registers (ESI, EDI, ESP, EBP).The AMD64 architecture (also known as x86-64), introduced in 2003, extended registers to 64 bits and added entirely new registers to address the increasing demands of modern software. This extension brought the most significant changes to the x86 register set since the original 8086.The 64-bit extension uses the prefix R (for "Register") on the original registers:Notice that in 64-bit mode, even the index and pointer registers gained 8-bit low-byte access (SIL, DIL, SPL, BPL)—a feature that wasn't available in 32-bit mode.The architecture also added eight completely new 64-bit registers, numbered R8 through R15. When designers "ran out of letters" for meaningful names, they simply adopted numeric identifiers:The suffixes follow a consistent pattern: D for "double word" (32 bits), W for "word" (16 bits), and B for "byte" (8 bits).Beyond the general-purpose registers, x86-64 processors include XMM0 through XMM15—sixteen 128-bit registers designed for SIMD (Single Instruction, Multiple Data) operations. These registers enable parallel processing of multiple data elements simultaneously, crucial for multimedia applications, scientific computing, and cryptography. Modern processors further extend these with 256-bit YMM and 512-bit ZMM registers for even greater parallelism.The complete register evolution across x86 architectures can be summarized as follows:This backward-compatible evolution means that code written for the 8086 can still execute on modern processors, though it won't take advantage of the larger registers or additional capabilities. The layered register access (AL within AX within EAX within RAX) remains a defining characteristic of the x86 architecture.<br><img alt="64-bit register layout showing nested access" src="assets/l03-x64-register-layout.png" target="_self">The nested structure of x86-64 registers: RAX (64-bit) contains EAX (32-bit), which contains AX (16-bit), which is split into AH and AL (8-bit each). This layered access pattern enables backward compatibility across all x86 generations.Registers occupy a unique position in the computer memory hierarchy—they are by far the fastest storage locations available, yet their quantity is severely constrained. Understanding why this tradeoff exists reveals fundamental principles about computer architecture that apply far beyond the x86 family.The speed advantage of registers stems from their physical implementation and location within the processor:Direct Connection to the ALU. Registers are hardwired directly to the Arithmetic Logic Unit and other functional units within the CPU. There is no address decoding, no memory controller, no bus arbitration—just direct electrical connections. When an instruction specifies "add EAX and EBX," the data flows immediately from those registers into the ALU inputs and the result flows back to the destination register.No Memory Bus Traversal. Accessing main memory requires sending an address across the memory bus, waiting for the memory controller to decode the address, waiting for the memory chips to retrieve the data, and then waiting for the data to travel back across the bus. This round trip takes dozens to hundreds of clock cycles. Registers bypass this entire process because they are physically located inside the CPU core itself.Single Clock Cycle Access. Modern processors can read from and write to registers within a single clock cycle—and often do multiple register operations simultaneously. A typical instruction might read two source registers, perform an operation, and write the result to a destination register, all in one clock cycle. This parallelism is possible precisely because registers have dedicated, always-available connections.Physical Proximity. On the CPU die, registers are located mere micrometers from the execution units. Electrical signals travel at a significant fraction of the speed of light, but even at these speeds, distance matters. The few millimeters to main memory represent a measurable delay compared to the microscopic distances within the CPU core.To put the speed difference in perspective, consider typical access times in a modern system:Registers are literally orders of magnitude faster than any other storage in the system.Given their extraordinary speed, why don't processors have thousands of registers? The answer involves multiple engineering constraints:Physical Wiring Complexity. Every register must be connected to every instruction execution unit that might use it. If the ALU needs to add two registers, both registers must have data paths leading to the ALU inputs. If any of the 16 general-purpose registers might be the source or destination, then 16 separate data paths must exist for each connection point. Adding more registers means exponentially more interconnect wiring.Consider a simplified calculation: with 8 registers, you need routing logic that can select any of 8 sources for each of two ALU inputs—that's 8 × 2 = 16 multiplexer inputs just for the ALU. With 16 registers, this doubles to 32 inputs. But registers also connect to memory units, floating-point units, and various other functional blocks. The total wiring grows rapidly with each additional register.Instruction Encoding Constraints. Every instruction must specify which registers to use, and this specification takes up bits in the machine code. With 4 registers, you need 2 bits to identify each register (since 2² = 4). With 8 registers, you need 3 bits (2³ = 8). With 16 registers, you need 4 bits (2⁴ = 16).An instruction that uses three registers (two sources and one destination) would need 6 additional bits with 8 registers versus 12 additional bits with 64 registers. Larger instructions mean more memory bandwidth consumed fetching code, larger instruction caches, and potentially slower execution.Physical Space on the Die. Each register requires actual silicon area—transistors to store the bits, drivers to output the values, receivers to accept inputs. While a single register is tiny compared to the overall CPU die, the routing logic and multiplexers that connect registers to execution units consume substantial area. This space could alternatively be used for larger caches, more execution units, or other features that might provide greater overall performance.Diminishing Returns. Compiler research has shown that most functions can be effectively compiled using only a modest number of registers. Beyond a certain point (roughly 16-32 registers for most workloads), adding more registers provides minimal benefit because the compiler can already keep the most frequently accessed variables in registers. The additional complexity isn't justified by the marginal improvement.The relationship between registers and memory represents one of the most fundamental tradeoffs in computer architecture:Registers: Extremely fast (sub-nanosecond access), but severely limited in quantity (16 general-purpose registers in x86-64). Every value in a register is immediately available for computation.Memory: Much slower (tens to hundreds of nanoseconds for main memory), but vastly larger in capacity (gigabytes to terabytes). Values must be explicitly loaded from memory before use and stored back after modification.This tradeoff shapes virtually every aspect of computer architecture and programming:Memory Hierarchy. To bridge the gap between registers and main memory, modern processors include multiple levels of cache memory. L1 cache (tens of kilobytes) is fast but small; L2 cache (hundreds of kilobytes) is larger but slower; L3 cache (megabytes) is even larger but slower still. This hierarchy attempts to keep frequently-used data close to the processor while accommodating the vast amounts of data modern programs require.Compiler Optimization. One of the most important tasks of an optimizing compiler is register allocation—deciding which variables should reside in registers and which must be stored in memory. Variables that are accessed frequently within a loop should ideally remain in registers throughout the loop's execution. Less frequently accessed data can be "spilled" to memory. A good register allocator can dramatically improve program performance.Calling Conventions. The limited number of registers forces careful coordination between functions. Calling conventions define which registers a function can use freely (caller-saved) and which it must preserve (callee-saved). Without such conventions, every function call would require saving all registers to memory, destroying much of the performance benefit registers provide [1].Algorithm Design. Programmers who understand the register-memory tradeoff write more efficient code. Accessing the same array element repeatedly should be avoided—load it into a variable (which the compiler can keep in a register) and reuse that variable. Loop variables should be simple integers that fit in registers. Complex data structures should be designed to facilitate good cache behavior.The x86 architecture, with its 16 general-purpose registers in 64-bit mode, represents Intel and AMD's current answer to this tradeoff. Other architectures make different choices: ARM64 provides 31 general-purpose registers, while some RISC architectures offer 32 or more. Each choice reflects a different balance between instruction encoding complexity, silicon area, and practical programming needs.Understanding this fundamental tradeoff prepares you for the next topic: how memory is organized into a vast grid of storage locations that, while slower than registers, provides the capacity needed for real programs and data.Consider the challenge of building even a modest amount of computer memory. One megabyte—a trivially small amount by modern standards—contains 8,388,608 individual bits. If we designed our memory system naively, giving each bit its own dedicated wire for reading and writing, we would need over eight million wires just to address this tiny amount of storage. Such a system would be impossible to manufacture, impossible to route on a circuit board, and would consume enormous amounts of physical space.This scaling problem becomes even more apparent when we consider modern memory capacities. A typical computer today has 8 to 32 gigabytes of RAM—that's 64 to 256 billion bits. A wire-per-bit approach would require hundreds of billions of individual connections, which is clearly absurd. Engineers needed a fundamentally smarter approach to make practical memory systems possible.The solution comes from a beautiful mathematical insight: arrange the bits in a two-dimensional grid rather than a one-dimensional array. Instead of addressing each bit with its own wire, we use a combination of row and column selection.Consider the mathematics. For a one-dimensional arrangement of n bits, we need n wires. But for a two-dimensional square grid of n bits, we only need approximately 2√n wires—roughly √n for row selection and √n for column selection.Let's see how dramatically this reduces wiring complexity:
1,000,000 bits (125 KB): Instead of 1,000,000 wires, we need only about 2,000 wires (1,000 rows × 1,000 columns)
1,000,000,000 bits (125 MB): Instead of a billion wires, approximately 64,000 wires suffice
8,000,000,000 bits (1 GB): About 180,000 wires instead of eight billion
This quadratic reduction in wiring complexity is what makes modern high-capacity memory physically realizable. The matrix organization transforms an impossible engineering challenge into a manageable one.Left: A 4x4 grid of gated latches forms the basic structure of a memory array. Right: Row and column selection lines activate specific cells at their intersection.The matrix insight raises a new question: how do we activate exactly one row out of a thousand possible rows using the fewest control lines? The answer lies in binary decoders.A binary decoder is a circuit that converts n input bits into 2^n output lines, where exactly one output is active at any time. The input bits encode a binary number, and the decoder activates the output line corresponding to that number.For example, a 2-to-4 decoder takes 2 input bits and produces 4 output lines:
Input 00 → activates line 0
Input 01 → activates line 1
Input 10 → activates line 2
Input 11 → activates line 3
<br><img alt="Binary decoder converting 2-bit input to 4 output lines" src="assets/l03-binary-decoder.png" target="_self" style="width: 400px; max-width: 100%;">A 2-to-4 binary decoder: the 2-bit input selects exactly one of four output lines, enabling efficient address decoding.To locate a specific memory cell in our matrix, we use two decoders: one for rows and one for columns [2]. An address input into these decoders activates a specific row wire and a specific column wire; the intersection of these two active wires identifies a unique memory location.With this scheme, selecting one cell among a million requires only about 20 address bits total—10 bits for the row decoder (selecting one of 1,024 rows) and 10 bits for the column decoder (selecting one of 1,024 columns). This is vastly more efficient than a million individual selection wires.<br><img alt="Memory array with binary decoder for address selection" src="assets/l03-memory-with-decoder.png" target="_self">Complete memory addressing: binary decoders convert the address input into row and column selection signals, identifying a unique cell at their intersection.At each intersection point in the memory matrix sits a gated latch—the fundamental storage element we explored in Lecture 1. The gated latch, as described in [2], uses two key inputs:
Data Input: a single wire that carries the bit value to be stored
Write Enable: a control signal that determines when the circuit should capture the data
The behavior is elegantly simple. When Write Enable is set to 0, the data signal is completely ignored—the latch maintains whatever value it previously stored through its internal feedback loop. When Write Enable transitions to 1, the circuit captures the current value on the Data Input and overwrites its stored memory [2].This feedback mechanism is the core of computer memory. Once a value is written, the feedback loop sustains it indefinitely (as long as power is supplied), even after the original input signal changes.The matrix organization creates a subtle problem. When we activate a row, every cell in that row sees the same row signal. Similarly, when we activate a column, every cell in that column responds. How do we ensure that only the single cell at the intersection of our chosen row and column actually performs a read or write operation?The solution employs AND gates at every intersection [2]. Each cell's control logic includes AND gates that combine the row selection signal with the column selection signal. Since an AND gate only produces a high output when all its inputs are high, a cell can only be selected when both its row and its column are simultaneously active.This ensures that read or write signals only reach the specific latch identified by the complete address [2]. If you activate row 47 and column 83, only the cell at position (47, 83) responds—all other cells in row 47 and all other cells in column 83 remain dormant.Memory must support two fundamental operations: writing data into a cell and reading data out of a cell. Both operations use the addressing system to select the target cell, but they differ in data flow direction.For a write operation, the system asserts the Write Enable signal along with the target address. The selected cell captures whatever value appears on the data line and stores it in its internal latch. All other cells ignore both the Write Enable and the data signal because their AND gates are not fully activated.For a read operation, the system asserts a Read Enable signal along with the target address. The read enable signal, as explained in [2], toggles the data line to act as an output rather than an input. The selected cell places its stored value onto the data line, where it can be sampled by other circuitry.A critical implementation detail: since the same data wire serves as both input (during writes) and output (during reads), tri-state buffers prevent conflicts. These special circuits can not only output high and low states but also a "disconnected" state, allowing the wire to be driven by whichever cell or external source is currently appropriate.Left: A memory cell with tri-state buffer allowing bidirectional data flow on a shared wire. Right: During a read operation, the selected cell drives its stored value onto the data line.The shared wire that carries data into and out of memory is called the Data Bus. This bus is shared by all memory cells—millions or billions of them—but thanks to the addressing logic, only one cell interacts with it at any given moment.The term "bus" comes from the Latin "omnibus" meaning "for all," and it aptly describes a shared communication pathway. The data bus connects the memory array to the CPU and to input/output devices, serving as the highway for information flow within the computer.The width of the data bus affects how much data can be transferred in a single operation. Early computers had 8-bit data buses; modern systems typically use 64-bit data buses, allowing them to read or write 8 bytes simultaneously.A single memory matrix, as we've described it, stores individual bits—one bit per cell at each row-column intersection. But computers work with bytes as their fundamental unit of storage. How do we read or write an entire byte at once?The elegant solution, as described in [2], is to stack eight identical matrices, all sharing the same address lines and control signals. When you specify an address and issue a read command, all eight matrices simultaneously output their stored bits at that address. These eight bits together form one byte.Think of it like eight transparent sheets stacked atop one another. Each sheet has a grid of bits, and all sheets use the same coordinate system. When you point to position (47, 83), you're pointing through all eight sheets at once, accessing one bit from each sheet—eight bits total, one byte.This parallel architecture means that reading or writing a byte takes no longer than reading or writing a single bit. The bandwidth multiplies while the latency stays constant—a recurring theme in computer architecture optimization.In a computer, memory uses eight matrices simultaneously, all sharing the same address and control lines, allowing read/write of an entire byte at once [2].<br><img alt="RAM as abstract component" src="assets/l03-ram-abstraction.png" target="_self">RAM abstracted as a component with address lines (selecting which byte to access) and data lines (carrying the byte value).<br><img alt="Complete RAM module" src="assets/l03-ram-complete.png" target="_self">Complete RAM module showing the address decoder that converts binary address bits into row/column selection signals, enabling access to any byte in the memory array.The address bus is the set of wires that carry the address of the memory location to be accessed. Just as the data bus width determines how many bits can be transferred at once, the address bus width determines how many unique locations can be addressed.The relationship follows directly from binary encoding: with n address bits, we can represent 2^n distinct addresses. Each address corresponds to one byte of memory. Therefore, a 32-bit address bus can manage approximately 4 billion bytes (4 GB) [2].<br><img alt="Address space calculation" src="assets/l03-address-space-formula.png" target="_self">The relationship between address bus width and addressable memory: n address bits can address 2^n unique locations. A 32-bit address bus enables addressing of 4 GB (2^32 bytes).The progression from 16-bit to 64-bit addressing represents a dramatic expansion in addressable memory. A 16-bit address bus limits a system to 64 KB—barely enough for a complex document today. The 32-bit barrier at 4 GB became a serious limitation for desktop computers by the mid-2000s, driving the transition to 64-bit architectures.Modern 64-bit systems can theoretically address 16 exabytes (16 billion gigabytes), but practical limits from motherboard design, operating system constraints, and economic factors keep current systems well below this ceiling.Not all memory is created equal. Two fundamentally different technologies dominate computer memory design: SRAM (Static Random Access Memory) and DRAM (Dynamic Random Access Memory).SRAM uses multiple transistors per bit—typically six transistors arranged in a flip-flop configuration [2]. This design creates a stable feedback loop that maintains the stored value indefinitely as long as power is applied. The "static" in SRAM refers to this stability; the data doesn't need to be periodically refreshed.DRAM achieves higher density through radical simplification: a single MOSFET transistor and a tiny capacitor per bit [2]. The capacitor stores charge to represent a 1 or no charge to represent a 0. This minimal design allows DRAM to pack far more bits into the same silicon area, making it much cheaper per bit.<br><img alt="SRAM vs DRAM comparison" src="assets/l03-sram-vs-dram.png" target="_self">Comparison of SRAM and DRAM cell structures: SRAM uses 6 transistors in a stable flip-flop configuration, while DRAM uses just 1 transistor plus a capacitor, trading complexity for density.The critical difference is refresh requirements. A capacitor is not a perfect storage device—it slowly leaks charge over time, typically losing its stored value within milliseconds. To maintain data integrity, DRAM controllers must periodically read each cell and rewrite its value, a process called refresh. This happens thousands of times per second, consuming memory bandwidth and adding complexity, but the cost savings from simpler cells make DRAM economically essential for large memories.SRAM's speed and simplicity (no refresh circuitry needed) make it ideal for small, fast memories where performance is paramount. DRAM's density and low cost make it the only practical choice for gigabytes of main memory.The SRAM/DRAM tradeoff illustrates a fundamental truth in computer architecture: there is no single perfect memory technology. Fast memory is expensive and low-density. Cheap, high-density memory is slow. Engineers solve this dilemma through the memory hierarchy—a layered system where each level offers a different speed-cost-capacity tradeoff.The hierarchy, from fastest to slowest: Registers (dozens of bytes): Integrated directly into the CPU core. Access time measured in picoseconds. The ultimate in speed, but extremely limited in quantity—just 16-32 general-purpose registers on most architectures. Cache (kilobytes to megabytes): Built from SRAM, located on or very near the CPU die. Modern processors have multiple cache levels (L1, L2, L3) with increasing size and latency. L1 cache might access in 1-4 CPU cycles; L3 might take 30-50 cycles. Main Memory (gigabytes): Built from DRAM, connected to the CPU via memory controllers and buses. Access times of 50-100 nanoseconds—hundreds of CPU cycles. But capacity measured in gigabytes at reasonable cost. Storage (terabytes): SSDs and hard disk drives. Access times in microseconds (SSD) to milliseconds (HDD). Vastly slower than RAM but non-volatile and enormously capacious. This hierarchy exploits a crucial property of real programs: locality of reference. Programs tend to access the same data repeatedly (temporal locality) and to access data near previously accessed data (spatial locality). By keeping recently-used data in fast upper levels of the hierarchy, systems achieve the illusion of large, fast memory while paying mostly for large, slow memory.The memory hierarchy is not merely an optimization—it is fundamental to computer architecture. Without it, either computers would be unaffordably expensive, or they would be unusably slow. Understanding how data moves through this hierarchy is essential to understanding why modern computers perform as they do, and it will be a recurring theme throughout this course.On June 8, 1978, Intel released the 8086 microprocessor—a chip that would fundamentally reshape the computing industry [3]. This 16-bit processor, implemented with approximately 20,000 active transistors, gave rise to what we now call the x86 architecture, which became Intel's most successful and enduring processor line.The origins of the 8086 reveal an interesting twist of corporate strategy. The project began in May 1976 as what Intel intended to be a temporary substitute while they worked on their truly ambitious project: the iAPX 432 [3]. The iAPX 432 was meant to be a revolutionary 32-bit architecture with advanced features like capability-based addressing and hardware support for object-oriented programming. Intel expected it to become their flagship product line. Instead, the "temporary" 8086 became the foundation of computing for the next five decades, while the iAPX 432 faded into obscurity as a commercial failure.The 8086's success wasn't accidental. Stephen P. Morse, who defined the architecture, made a crucial design decision: the 8086 would use a similar architecture to Intel's existing 8-bit microprocessors (the 8008, 8080, and 8085) [3]. This backward compatibility meant that assembly language programs could migrate seamlessly from the older 8-bit platforms to the new 16-bit processor. Programmers didn't need to learn an entirely new system—they could leverage their existing knowledge and code. This design philosophy of maintaining backward compatibility has characterized x86 development ever since.The development timeline is also noteworthy: the 8086 took a little more than two years from initial concept to working product [3]. In an era before modern electronic design automation tools, this was a remarkable achievement that demonstrated Intel's engineering capabilities.<br><img alt="Intel 8086 CPU chip in DIP-40 package" src="assets/l03-intel-8086-chip.jpeg" target="_self">You might reasonably ask: why study a processor from 1978 in a modern computer architecture course? The answer lies in understanding that the 8086 represents the simplest version of the x86 architecture that remains directly relevant to modern computing.Every x86-64 processor you encounter today—whether it's an Intel Core i9 or an AMD Ryzen—maintains backward compatibility with the original 8086 instruction set. When your computer boots, it actually starts in a mode that's essentially an enhanced 8086 (called "real mode") before transitioning to modern 64-bit operation. The BIOS code, bootloaders, and certain low-level system functions still operate using concepts that date directly back to 1978.Modern x86 features are fundamentally extensions of 8086 concepts rather than replacements:Understanding the 8086 is like understanding the roots of a tree—you can't fully comprehend the complex branching structure above without knowing what lies beneath. Students who master 8086 concepts find that learning modern x86-64 features becomes a matter of understanding extensions rather than learning something entirely new.Here we encounter one of the 8086's most interesting design challenges. The processor has 16-bit registers—each can hold values from 0 to 65,535. If we used a single register to specify memory addresses, we could address at most 2^16 = 65,536 bytes, or 64 KB of memory.But Intel's engineers wanted the 8086 to address more memory than that. They equipped the processor with a 20-bit address bus, capable of addressing 2^20 = 1,048,576 bytes = 1 MB of memory. The famous (and likely apocryphal) quote "640 KB should be enough for anyone" actually referred to the usable portion of this 1 MB address space, with the upper 384 KB reserved for system functions.This created a fundamental mismatch: how do you generate 20-bit addresses when your largest register can only hold 16 bits? You can't simply add more bits to the registers—that would change the entire processor architecture. Intel's solution was elegant and influential: segment:offset addressing.Let's quantify the addressing challenge that Intel's engineers faced [4]. A 16-bit register can hold values from 0 to 65,535, meaning it can directly address at most 64 KB of memory. But the 8086's 20-bit address bus could reach 1 MB of memory—sixteen times more than a single register could address.One naive solution would be to divide the 1 MB address space into 16 fixed segments of 64 KB each. You'd need just 4 bits to select which segment (2^4 = 16), leaving the 16-bit register to specify the offset within that segment. This would work, but it would be inflexible—programs and data would need to fit neatly within 64 KB boundaries, and you couldn't easily place a data structure that straddled two segments.Intel chose a more flexible approach: overlapping segments [4].The segment:offset addressing scheme uses two 16-bit values to specify a memory location: a Segment value and an Offset value. The physical (absolute) address is calculated using this formula:Absolute Address = (Segment × 16) + Offset<br><img alt="Segment:Offset formula visualization" src="assets/l03-segment-offset-formula.png" target="_self">
Why multiply by 16? In hexadecimal terms, multiplying by 16 is equivalent to shifting left by one hex digit—essentially appending a zero to the right side of the number. This transforms the 16-bit segment value into a 20-bit base address, to which we add the 16-bit offset.The notation commonly used is Segment:Offset, where both values are written in hexadecimal. For example, F000:FFFD means segment F000h with offset FFFDh.Let's work through several examples to build intuition about this addressing scheme [4].Example 1: F000:FFFDThis address represents a location in the high memory area, commonly used for BIOS code: Segment: F000 Segment × 16: F0000 (shift left one hex digit)
+ Offset: + FFFD ------- Absolute: FFFFD = 1,048,573 decimal
This address is very close to the top of the 1 MB address space (which ends at FFFFF = 1,048,575).Example 2: 923F:E2FFA more arbitrary address demonstrating the arithmetic [4]: Segment: 923F Segment × 16: 923F0 (shift left one hex digit)
+ Offset: + E2FF ------- Absolute: A06EF = 657,135 decimal
Notice how the addition can produce carries that ripple through the hexadecimal digits.Example 3: Maximum Possible Address (FFFF:FFFF)What happens when we maximize both segment and offset? Segment: FFFF Segment × 16: FFFF0 (shift left one hex digit)
+ Offset: + FFFF ------- Absolute: 10FFEF = 1,114,095 decimal
This is a 21-bit address! It exceeds the 1 MB boundary (which ends at FFFFF). This overflow creates what's called the "High Memory Area" (HMA)—an extra 65,520 bytes that became accessible on later processors. On the original 8086, this address would "wrap around" due to the 20-bit address bus limitation.A crucial characteristic of this addressing scheme is that segments overlap extensively [4]. Each new segment begins only 16 bytes (one "paragraph" in x86 terminology) after the previous one. Since the entire address space contains 1,048,576 bytes, and segments start every 16 bytes, there are 65,536 possible segment starting points.<br><img alt="How 64KB segments overlap in the 8086 address space" src="assets/l03-8086-segment-overlap.png" target="_self">
This overlapping nature means that a single physical address can be represented by many different segment:offset pairs [4]. In fact, there can be up to 4,096 different segment:offset representations for any given physical address!Consider the physical address 7C00h—historically significant as the location where the BIOS loads the boot sector. This address can be written as:All of these refer to the same physical byte in memory. Boot sector code traditionally uses either 0000:7C00 (offset-based) or 07C0:0000 (segment-based), and understanding this equivalence is essential when debugging or writing low-level code.Intel's segment:offset scheme, while seemingly complicated, offered significant advantages with 1978-era hardware constraints:Program Relocation: Programs could be moved in memory simply by changing the segment register values. The offset-based code within a program didn't need to change—only the segment base needed adjustment. This made memory management much more flexible.Memory Protection (conceptual): Different segments could be used for different purposes—code in one segment, data in another. While the 8086 didn't enforce segment boundaries in hardware, this conceptual separation laid the groundwork for protected mode's memory protection in later processors.Efficient 16-bit Operations: All arithmetic on offsets used 16-bit operations, which the processor could execute efficiently. The segment multiplication by 16 was a simple shift operation.The 8086 provides four dedicated segment registers, each serving a specific purpose in program execution:<br><img alt="AFD debugger showing code and data segments" src="assets/l03-afd-segments-annotated.png" target="_self">
These segment registers aren't general-purpose—you can't use them for arithmetic or as temporary storage. They exist solely to provide the segment portion of memory addresses. The processor automatically selects which segment register to use based on the type of memory access being performed.When the processor fetches the next instruction, it automatically uses CS. When your code reads or writes data (like MOV AX, [BX]), it automatically uses DS. When pushing to or popping from the stack, it automatically uses SS. This automatic selection simplifies programming while maintaining the flexibility of segment-based addressing.A typical 8086 program organizes memory into distinct segments that may or may not overlap:The Code Segment (CS) contains the program's executable instructions. The processor fetches instructions from addresses calculated as (CS × 16) + IP. This segment is typically treated as read-only during execution—self-modifying code exists but is generally considered poor practice.The Data Segment (DS) contains the program's variables, constants, and strings. When you declare a variable in assembly language and access it by name, the assembler generates code that uses DS as the default segment. Initialized data (variables with starting values) and uninitialized data (reserved space) both reside here.The Stack Segment (SS) holds the runtime stack—the region of memory used for function calls, return addresses, and local variables. The stack follows a Last-In-First-Out (LIFO) discipline and grows downward in memory (toward lower addresses).The Extra Segment (ES) provides a second data segment, particularly useful for string operations that need to copy data from one memory region to another. Instructions like MOVSB (move string byte) read from DS:SI and write to ES:DI, allowing efficient memory-to-memory transfers.In small programs, these segments might all point to the same memory region (a "tiny" memory model). In larger programs, they point to completely separate 64 KB regions, allowing up to 256 KB of directly addressable memory (4 segments × 64 KB).When debugging 8086 code, you'll see segment register values displayed in the debugger. Understanding how to calculate physical addresses from these values is essential for following program execution.Consider a debugger showing:
CS = 0F78h
DS = 0F7Bh
IP = 0100h
The physical address of the next instruction is:(CS × 16) + IP = (0F78h × 16) + 0100h = 0F780h + 0100h = 0F880h
To understand the relationship between code and data segments:DS - CS = 0F7Bh - 0F78h = 3 (segments)
Physical gap = 3 × 16 = 48 bytes
This means the data segment begins 48 bytes after the code segment starts. In a small program, this gap might contain exactly 48 bytes of code, with data immediately following.<br><img alt="AFD debugger showing segment distance calculation" src="assets/l03-afd-data-segment.png" target="_self">The combination of CS (Code Segment) and IP (Instruction Pointer) determines where the processor fetches its next instruction. This pair works as an inseparable unit:Physical address of next instruction = (CS × 16) + IPThe processor automatically increments IP after fetching each instruction. When IP reaches FFFFh and increments, it wraps around to 0000h—but CS doesn't automatically change. This means execution stays within the current 64 KB code segment unless a far jump or call explicitly loads a new CS value.Jump instructions come in two varieties:
Near jumps modify only IP, staying within the current code segment
Far jumps modify both CS and IP, allowing jumps anywhere in the 1 MB address space
Understanding this distinction is crucial when writing or analyzing boot code, device drivers, or any software that must work across segment boundaries.The SS (Stack Segment) and SP (Stack Pointer) registers work together to manage the program stack:Physical address of stack top = (SS × 16) + SPThe stack has several distinctive characteristics:Downward Growth: Unlike most data structures that grow upward (toward higher addresses), the x86 stack grows downward. When you push a value, SP decreases. When you pop a value, SP increases. This convention dates back to early computer designs and persists in x86 today.Word Alignment: On the 8086, stack operations work with 16-bit words. PUSH decrements SP by 2, then stores the value. POP retrieves the value, then increments SP by 2.Automatic Usage: The processor automatically uses SS:SP for several operations:
PUSH and POP instructions
CALL (pushes return address) and RET (pops return address)
Interrupt handling (pushes flags, CS, and IP)
A typical stack initialization might set SS to point to a memory region and SP to the top of that region (e.g., SP = FFFEh for a nearly-full 64 KB stack segment). As the program executes functions and pushes data, SP decreases. If SP reaches 0000h and the program pushes more data, a stack overflow occurs—one of the most notorious bugs in computing history.The relationship between SS and SP exemplifies the segment:offset philosophy: SS provides the base location of the stack in physical memory, while SP tracks the current position within that stack region. Change SS, and you switch to an entirely different stack. Change SP, and you move within the current stack.In addition to the general-purpose registers and segment registers we have explored, the 8086 processor contains a special 16-bit register called the Flags Register (also known as the Status Register or Program Status Word). This register does not store data in the conventional sense. Instead, each individual bit serves as a flag that indicates a specific condition or controls a specific behavior of the processor.The flags register is fundamental to how processors make decisions. When you write code like if (x == y) in a high-level language, the compiler translates this into machine instructions that compare the values and then check the appropriate flags to determine which branch to take. Without the flags register, processors would have no way to perform conditional operations.The 16 bits of the flags register are not all used. Only 9 bits are defined in the 8086, leaving 7 bits reserved for future expansion (which Intel indeed used in later processors like the 80286, 80386, and beyond). The defined flags fall into two categories: status flags that reflect the results of arithmetic and logical operations, and control flags that modify the behavior of certain instructions [6].<br><img alt="Complete 8086 register set including the FLAGS register" src="assets/l03-8086-registers-full.png" target="_self">The complete 8086 register set, showing general-purpose registers, segment registers, pointer/index registers, and the FLAGS register at the bottom.Status flags are automatically set or cleared by the processor after most arithmetic and logical operations. The programmer does not typically set these flags directly. Instead, they observe the flags to determine what happened during the previous operation.<br><img alt="FLAGS register bit layout" src="assets/l03-flags-register.jpeg" target="_self">The 8086 FLAGS register bit layout showing the position of each flag within the 16-bit register. Status flags (CF, PF, AF, ZF, SF, OF) and control flags (TF, IF, DF) occupy specific bit positions.Understanding Carry vs. Overflow:The distinction between the Carry Flag (CF) and Overflow Flag (OF) often confuses students, but it is critical for correct programming:
CF detects problems in unsigned arithmetic. If you add two 16-bit unsigned numbers and the result exceeds 65,535, the Carry Flag is set.
OF detects problems in signed arithmetic. If you add two 16-bit signed numbers and the result exceeds 32,767 or falls below -32,768, the Overflow Flag is set.
The processor always sets both flags according to their definitions. It is the programmer's responsibility to check the appropriate flag based on whether the data is being treated as signed or unsigned.Example: Adding FFFFh + 0001h (interpreted as unsigned: 65535 + 1):
Result: 0000h with CF = 1 (carry occurred)
The OF flag will be 0 because no signed overflow occurred (interpreting FFFFh as -1 and adding 1 gives 0, which is valid)
Control flags are set or cleared by the programmer to modify how the processor executes certain instructions. Unlike status flags, these do not change automatically based on computation results.<br><img alt="Debugger registers panel showing all CPU registers" src="assets/l03-afd-registers-view.png" target="_self">A debugger's registers panel showing all CPU registers including general-purpose registers, segment registers, instruction pointer (IP), and the FLAGS register. This view helps programmers observe register values and flag states during program execution.The true power of the flags register becomes apparent when combined with conditional jump instructions. This is how processors implement decision-making, the equivalent of if-else statements in high-level languages [1].The CMP (Compare) instruction is the workhorse of conditional logic. It performs a subtraction of its two operands but discards the result, keeping only the effect on the flags. This allows you to compare two values without modifying either of them.CMP AX, BX ; Computes AX - BX internally ; Sets flags based on the result ; Both AX and BX remain unchanged
After the CMP instruction, various conditional jump instructions examine the flags:Notice how the signed comparisons (JG, JL, etc.) use the Sign Flag and Overflow Flag, while unsigned comparisons (JA, JB, etc.) use the Carry Flag. This reflects the different meanings of the same bit patterns when interpreted as signed versus unsigned integers.Consider this high-level code:if (x &gt; y) { // do something
} else { // do something else
}
In 8086 assembly, assuming x is in AX and y is in BX: CMP AX, BX ; Compare x with y JLE else_part ; If x &lt;= y, jump to else ; ... code for "do something" (x &gt; y case) JMP end_if ; Skip the else part
else_part: ; ... code for "do something else"
end_if: ; ... continue with rest of program
The CMP instruction sets the flags by computing AX - BX. If AX &gt; BX (signed comparison), the JLE instruction will NOT jump (because the condition "less than or equal" is false), and execution continues with the "then" branch. If AX &lt;= BX, JLE jumps to the else_part label.When using Turbo Debugger (TD) in Lab 3, you will see the flags register displayed in a special format. Typically, debuggers show flags as a string of letters where:
An uppercase letter means the flag is SET (1)
A lowercase letter (or a different symbol) means the flag is CLEAR (0)
For example: Oditsz C might indicate OF=1, DF=0, IF=0, TF=0, SF=0, ZF=0, CF=1.Watching how flags change as you step through instructions is one of the most instructive ways to understand how the processor actually makes decisions.This lecture has covered a broad landscape, from the compact notation of hexadecimal to the intricate details of 8086 segmentation. Here are the seven key concepts that form the foundation for assembly language programming:1. Hexadecimal is the programmer's friendOne hexadecimal digit corresponds to exactly four binary bits, making hex the natural shorthand for binary data. Converting between hex and binary is trivial once memorized: each hex digit independently maps to its 4-bit pattern. This compactness is why memory addresses, instruction opcodes, and register contents are almost universally displayed in hexadecimal rather than binary or decimal. Mastering hex is not optional for low-level programming; it is essential [1].2. Instructions are encoded as bit patternsEvery assembly instruction you write becomes a specific pattern of bits when assembled into machine code. These bit patterns are not arbitrary. Different fields within the instruction word route to different hardware modules: some bits select the operation (add, subtract, move), others select the registers involved, and still others may contain immediate values or memory addressing information. Understanding that opcodes have structure helps demystify how processors decode and execute instructions.3. Registers are fast because they are wired directly to the ALUUnlike memory, which requires address decoding, bus transactions, and multiple clock cycles to access, registers are hardwired connections within the processor core. When you specify AX in an instruction, there is no lookup process. The ALU already has dedicated pathways to read from and write to AX. This architectural shortcut is why registers are orders of magnitude faster than memory. However, the same direct wiring that makes registers fast also limits their number. Physical space on the chip and the number of available connections constrain how many registers a processor can have.4. Memory uses 2D addressing to reduce wiring complexityIf we tried to individually wire each byte in a megabyte of memory, we would need over one million wires just for addressing. The square grid solution dramatically reduces this complexity: a 1024x1024 matrix needs only about 2,048 address lines (1,024 for rows + 1,024 for columns) plus control and data lines. Binary decoders convert the address bits into row and column selection signals, and AND gates at each intersection ensure that only the single selected cell responds to read or write operations [2].5. The 8086 uses segment:offset addressing to extend its reachThe 8086 processor has 16-bit registers but needed to address 1MB of memory, which requires 20-bit addresses. The segment:offset scheme solves this mismatch: two 16-bit values combine via the formula Physical Address = (Segment x 16) + Offset to produce a 20-bit result. This design was a pragmatic engineering compromise that allowed backward compatibility with 8-bit software while expanding addressable memory. The same physical address can be represented by many different segment:offset pairs, since segments overlap every 16 bytes [3], [4].6. Segment registers separate code, data, and stack memory regionsThe four segment registers (CS, DS, SS, ES) establish four simultaneous "windows" into the 1MB address space. CS points to the code segment where instructions are fetched; DS points to the data segment where variables are stored; SS points to the stack segment for subroutine calls and local variables. This separation is more than organizational convenience. It allows programs to be relocated simply by changing segment register values, enables limited memory protection (code cannot accidentally overwrite itself if segments do not overlap), and paved the way for the protected mode introduced in the 80286 [5].7. Flags record operation results for decision-makingThe flags register is the processor's short-term memory of what just happened. After every arithmetic or logical operation, the status flags automatically update to reflect whether the result was zero, negative, caused an overflow, and so on. Conditional jump instructions then examine these flags to determine program flow. This mechanism is how machine code implements all forms of conditional execution, from simple if statements to complex loops. Without flags, processors could only execute straight-line code.In Lab 3, you will set up a complete 16-bit assembly language development environment. This involves several tools that work together:TASM (Turbo Assembler):
The assembler reads your .ASM source files containing human-readable assembly code and translates them into .OBJ object files containing machine code. TASM understands Intel syntax and supports macros, conditional assembly, and various debugging options. When you run TASM on your source file, it performs lexical analysis, syntax checking, symbol resolution, and code generation.TLINK (Turbo Linker):
The linker takes one or more .OBJ files and combines them into an executable .EXE file. Even for a single-file program, the linker is necessary because it resolves segment addresses, sets up the proper executable header that DOS expects, and can combine your code with library routines if needed.DOSBox:
Modern operating systems (Windows 10/11, macOS, Linux) cannot directly run 16-bit DOS programs. DOSBox is an x86 emulator that recreates the DOS environment, including the 8086/80286 CPU behavior, memory layout, and DOS system calls. When you run your assembled program in DOSBox, it executes as if on a 1980s PC.Turbo Debugger (TD):
TD is a visual debugger that lets you step through your program one instruction at a time. You can see the current values of all registers, watch memory contents change, set breakpoints, and observe how each instruction affects the processor state. This is invaluable for understanding what your code actually does and for finding bugs.The lab session will take you through the complete development cycle: Write your first assembly program - A minimal program that demonstrates register operations, such as loading values into registers and performing simple arithmetic. Assemble the program - Run TASM to convert your .ASM file to an .OBJ file. Learn to interpret assembler error messages and warnings. Link the program - Run TLINK to produce an executable .EXE file from the .OBJ file. Run the program - Execute your program in DOSBox and verify it completes without crashing. Debug the program - Load your program into Turbo Debugger and step through it instruction by instruction. Watch the registers change, observe the flags, and see how the instruction pointer advances through your code. Before attending the lab, spend time considering these questions. They target common misconceptions and will help you engage more deeply with the material:1. Addressing modes: What is the difference between MOV AX, 5 and MOV AX, [5]?This is a critical distinction in assembly language:
MOV AX, 5 is immediate addressing. The value 5 itself is loaded directly into AX. After this instruction, AX contains 0005h.
MOV AX, [5] is direct memory addressing. The brackets indicate that 5 is a memory address, and the processor loads the 16-bit value stored at that address into AX. After this instruction, AX contains whatever value was in memory at location DS:0005.
The brackets are not optional or decorative. They fundamentally change the meaning of the instruction.2. Overflow: What happens when you add two numbers that overflow?Consider adding FFFFh + 0001h in a 16-bit register:
The mathematical result is 10000h, which requires 17 bits
But a 16-bit register can only hold 16 bits
The result in the register is 0000h (the lower 16 bits)
The Carry Flag (CF) is set to 1, indicating unsigned overflow
If interpreting as signed values (FFFFh = -1, 0001h = 1), the result 0000h = 0 is correct, and the Overflow Flag (OF) is 0
The processor does not stop or raise an error on overflow. It simply sets the appropriate flags. It is the programmer's responsibility to check these flags if overflow detection is needed.3. Address calculation: Given CS = 0F78h and IP = 0010h, what is the physical address of the next instruction?Apply the segment:offset formula:
Physical Address = (Segment x 16) + Offset
Physical Address = (0F78h x 10h) + 0010h
Physical Address = 0F780h + 0010h
Physical Address = 0F790h
In decimal: (3960 x 16) + 16 = 63360 + 16 = 63376The next instruction the processor will fetch is located at physical address 0F790h in memory. This address was computed by the processor's Bus Interface Unit using the values in CS and IP.
<br>[1] <a data-tooltip-position="top" aria-label="https://www.cs.virginia.edu/~evans/cs216/guides/x86.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.cs.virginia.edu/~evans/cs216/guides/x86.html" target="_self">Guide to x86 Assembly</a> - University of Virginia CS216. An excellent introduction to x86 assembly programming covering registers, memory addressing, instructions, and calling conventions. Written for 32-bit x86 but the foundational concepts apply directly to 16-bit programming. <br>[2] <a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=rM9BjciBLmg" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=rM9BjciBLmg" target="_self">How Computers Remember - Memory, Bus, and Addressing</a> (YouTube). Visual explanation of how RAM is organized as a 2D matrix, how address decoders work, and how data moves between processor and memory. <br>[3] <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Intel_8086" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Intel_8086" target="_self">Intel 8086 - Wikipedia</a>. Comprehensive historical and technical overview of the 8086 processor, including its architecture, instruction set, and lasting influence on the x86 family. <br>[4] <a data-tooltip-position="top" aria-label="https://thestarman.pcministry.com/asm/debug/Segments.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://thestarman.pcministry.com/asm/debug/Segments.html" target="_self">Removing the Mystery from SEGMENT:OFFSET Addressing</a>. A detailed tutorial explaining how segment:offset addressing works, with numerous examples and visualizations of overlapping segments. Essential reading for understanding 8086 memory addressing. [5] 8086 Microprocessor Basics (various educational sources). General references on 8086 architecture including register organization, bus structure, and instruction execution. <br>
[6] <a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/flag-register-8086-microprocessor/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/flag-register-8086-microprocessor/" target="_self">8086 Flags Register</a> - GeeksforGeeks. Detailed explanation of each flag in the 8086 flags register, including when each flag is set and how conditional jumps use them. This lecture addresses the following Learning Outcomes from the course syllabus:LO1: Explain the principles of construction and functioning of modern computer architecturesStudents should now be able to:
Describe the organization and purpose of x86 registers (general-purpose, segment, flags)
Explain how memory is organized as a 2D matrix with row/column addressing
Articulate why registers are faster than memory (direct ALU wiring vs. address decoding)
Explain the segment:offset addressing mechanism and why it was designed
LO2: Perform data conversions between formats and analyze machine-level representationStudents should now be able to:
Convert fluently between hexadecimal, binary, and decimal representations
Calculate physical addresses from segment:offset pairs
Interpret flag register values and understand their meaning
Trace through simple instruction sequences and predict register/flag changes
LO3: Write and debug programs in x86 assembly languageThis lecture provides the foundation for:
Understanding what registers are available and their conventional uses
Knowing how memory is addressed in 8086 programs
Recognizing how conditional execution works through flags and conditional jumps
Preparing for hands-on assembly programming in Lab 3
Course Information:
National University of Kyiv-Mohyla Academy
Faculty of Informatics
Educational Program: Computer Science, Bachelor's
Course Structure:
Full Course (CS, Software Engineering): 6 ECTS / 180 hours
Shortened Course (AKITR): 4 ECTS / 120 hours
]]></description><link>lectures/eng/l03-eng.-x86-architecture-registers-and-memory.html</link><guid isPermaLink="false">Lectures/ENG/L03-ENG. x86 Architecture - Registers and Memory.md</guid><pubDate>Wed, 28 Jan 2026 21:09:09 GMT</pubDate><enclosure url="assets/l03-hex-binary-conversion.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-hex-binary-conversion.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[README]]></title><description><![CDATA[Images not embedded in the lecture materials. Kept for potential future use.These images may be useful for:
Alternative explanations in Ukrainian lecture version
Lab materials requiring different visualizations
Slide presentations needing variety
Total embedded: 28 images
Total remaining: 7 images]]></description><link>assets/l03-temp/readme.html</link><guid isPermaLink="false">assets/L03-temp/README.md</guid><pubDate>Wed, 28 Jan 2026 21:07:01 GMT</pubDate></item><item><title><![CDATA[L03-FLAGS-Register]]></title><description><![CDATA[<img src="assets/l03-flags-register.jpeg" target="_self">]]></description><link>assets/l03-flags-register.html</link><guid isPermaLink="false">assets/L03-FLAGS-Register.jpeg</guid><pubDate>Wed, 28 Jan 2026 20:58:08 GMT</pubDate><enclosure url="assets/l03-flags-register.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-flags-register.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L03-AFD-Registers-View]]></title><description><![CDATA[<img src="assets/l03-afd-registers-view.png" target="_self">]]></description><link>assets/l03-afd-registers-view.html</link><guid isPermaLink="false">assets/L03-AFD-Registers-View.png</guid><pubDate>Wed, 28 Jan 2026 20:58:08 GMT</pubDate><enclosure url="assets/l03-afd-registers-view.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-afd-registers-view.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L03-8086-Registers-Full]]></title><description><![CDATA[<img src="assets/l03-8086-registers-full.png" target="_self">]]></description><link>assets/l03-8086-registers-full.html</link><guid isPermaLink="false">assets/L03-8086-Registers-Full.png</guid><pubDate>Wed, 28 Jan 2026 20:58:08 GMT</pubDate><enclosure url="assets/l03-8086-registers-full.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-8086-registers-full.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L03-AFD-Data-Segment]]></title><description><![CDATA[<img src="assets/l03-afd-data-segment.png" target="_self">]]></description><link>assets/l03-afd-data-segment.html</link><guid isPermaLink="false">assets/L03-AFD-Data-Segment.png</guid><pubDate>Wed, 28 Jan 2026 20:58:08 GMT</pubDate><enclosure url="assets/l03-afd-data-segment.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-afd-data-segment.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L03-AFD-Segments-Annotated]]></title><description><![CDATA[<img src="assets/l03-afd-segments-annotated.png" target="_self">]]></description><link>assets/l03-afd-segments-annotated.html</link><guid isPermaLink="false">assets/L03-AFD-Segments-Annotated.png</guid><pubDate>Wed, 28 Jan 2026 20:58:08 GMT</pubDate><enclosure url="assets/l03-afd-segments-annotated.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-afd-segments-annotated.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L03-Segment-Calc-Example2]]></title><description><![CDATA[<img src="assets/l03-segment-calc-example2.png" target="_self">]]></description><link>assets/l03-segment-calc-example2.html</link><guid isPermaLink="false">assets/L03-Segment-Calc-Example2.png</guid><pubDate>Wed, 28 Jan 2026 20:58:08 GMT</pubDate><enclosure url="assets/l03-segment-calc-example2.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-segment-calc-example2.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L03-Segment-Calc-Example1]]></title><description><![CDATA[<img src="assets/l03-segment-calc-example1.png" target="_self">]]></description><link>assets/l03-segment-calc-example1.html</link><guid isPermaLink="false">assets/L03-Segment-Calc-Example1.png</guid><pubDate>Wed, 28 Jan 2026 20:57:58 GMT</pubDate><enclosure url="assets/l03-segment-calc-example1.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-segment-calc-example1.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L03-Segment-Offset-Formula]]></title><description><![CDATA[<img src="assets/l03-segment-offset-formula.png" target="_self">]]></description><link>assets/l03-segment-offset-formula.html</link><guid isPermaLink="false">assets/L03-Segment-Offset-Formula.png</guid><pubDate>Wed, 28 Jan 2026 20:57:58 GMT</pubDate><enclosure url="assets/l03-segment-offset-formula.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-segment-offset-formula.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L03-8086-Segment-Overlap]]></title><description><![CDATA[<img src="assets/l03-8086-segment-overlap.png" target="_self">]]></description><link>assets/l03-8086-segment-overlap.html</link><guid isPermaLink="false">assets/L03-8086-Segment-Overlap.png</guid><pubDate>Wed, 28 Jan 2026 20:57:58 GMT</pubDate><enclosure url="assets/l03-8086-segment-overlap.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-8086-segment-overlap.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L03-Intel-8086-Chip]]></title><description><![CDATA[<img src="assets/l03-intel-8086-chip.jpeg" target="_self">]]></description><link>assets/l03-intel-8086-chip.html</link><guid isPermaLink="false">assets/L03-Intel-8086-Chip.jpeg</guid><pubDate>Wed, 28 Jan 2026 20:57:58 GMT</pubDate><enclosure url="assets/l03-intel-8086-chip.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-intel-8086-chip.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L03-SRAM-vs-DRAM]]></title><description><![CDATA[<img src="assets/l03-sram-vs-dram.png" target="_self">]]></description><link>assets/l03-sram-vs-dram.html</link><guid isPermaLink="false">assets/L03-SRAM-vs-DRAM.png</guid><pubDate>Wed, 28 Jan 2026 20:57:58 GMT</pubDate><enclosure url="assets/l03-sram-vs-dram.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-sram-vs-dram.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L03-Address-Space-Formula]]></title><description><![CDATA[<img src="assets/l03-address-space-formula.png" target="_self">]]></description><link>assets/l03-address-space-formula.html</link><guid isPermaLink="false">assets/L03-Address-Space-Formula.png</guid><pubDate>Wed, 28 Jan 2026 20:57:58 GMT</pubDate><enclosure url="assets/l03-address-space-formula.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-address-space-formula.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L03-RAM-Full-Diagram]]></title><description><![CDATA[<img src="assets/l03-temp/l03-ram-full-diagram.png" target="_self">]]></description><link>assets/l03-temp/l03-ram-full-diagram.html</link><guid isPermaLink="false">assets/L03-temp/L03-RAM-Full-Diagram.png</guid><pubDate>Wed, 28 Jan 2026 20:57:58 GMT</pubDate><enclosure url="assets/l03-temp/l03-ram-full-diagram.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-temp/l03-ram-full-diagram.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L03-RAM-Abstraction]]></title><description><![CDATA[<img src="assets/l03-ram-abstraction.png" target="_self">]]></description><link>assets/l03-ram-abstraction.html</link><guid isPermaLink="false">assets/L03-RAM-Abstraction.png</guid><pubDate>Wed, 28 Jan 2026 20:57:58 GMT</pubDate><enclosure url="assets/l03-ram-abstraction.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-ram-abstraction.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L03-Memory-Register-Display]]></title><description><![CDATA[<img src="assets/l03-temp/l03-memory-register-display.png" target="_self">]]></description><link>assets/l03-temp/l03-memory-register-display.html</link><guid isPermaLink="false">assets/L03-temp/L03-Memory-Register-Display.png</guid><pubDate>Wed, 28 Jan 2026 20:57:58 GMT</pubDate><enclosure url="assets/l03-temp/l03-memory-register-display.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-temp/l03-memory-register-display.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L03-Linear-Memory-Address]]></title><description><![CDATA[<img src="assets/l03-temp/l03-linear-memory-address.png" target="_self">]]></description><link>assets/l03-temp/l03-linear-memory-address.html</link><guid isPermaLink="false">assets/L03-temp/L03-Linear-Memory-Address.png</guid><pubDate>Wed, 28 Jan 2026 20:57:58 GMT</pubDate><enclosure url="assets/l03-temp/l03-linear-memory-address.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-temp/l03-linear-memory-address.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L03-RAM-Read-Operation]]></title><description><![CDATA[<img src="assets/l03-temp/l03-ram-read-operation.png" target="_self">]]></description><link>assets/l03-temp/l03-ram-read-operation.html</link><guid isPermaLink="false">assets/L03-temp/L03-RAM-Read-Operation.png</guid><pubDate>Wed, 28 Jan 2026 20:57:48 GMT</pubDate><enclosure url="assets/l03-temp/l03-ram-read-operation.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-temp/l03-ram-read-operation.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L03-RAM-With-Value]]></title><description><![CDATA[<img src="assets/l03-temp/l03-ram-with-value.png" target="_self">]]></description><link>assets/l03-temp/l03-ram-with-value.html</link><guid isPermaLink="false">assets/L03-temp/L03-RAM-With-Value.png</guid><pubDate>Wed, 28 Jan 2026 20:57:48 GMT</pubDate><enclosure url="assets/l03-temp/l03-ram-with-value.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-temp/l03-ram-with-value.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L03-Memory-Cell-Detail]]></title><description><![CDATA[<img src="assets/l03-temp/l03-memory-cell-detail.png" target="_self">]]></description><link>assets/l03-temp/l03-memory-cell-detail.html</link><guid isPermaLink="false">assets/L03-temp/L03-Memory-Cell-Detail.png</guid><pubDate>Wed, 28 Jan 2026 20:57:48 GMT</pubDate><enclosure url="assets/l03-temp/l03-memory-cell-detail.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-temp/l03-memory-cell-detail.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L03-RAM-Complete]]></title><description><![CDATA[<img src="assets/l03-ram-complete.png" target="_self">]]></description><link>assets/l03-ram-complete.html</link><guid isPermaLink="false">assets/L03-RAM-Complete.png</guid><pubDate>Wed, 28 Jan 2026 20:57:48 GMT</pubDate><enclosure url="assets/l03-ram-complete.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-ram-complete.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L03-Memory-Cell-Read]]></title><description><![CDATA[<img src="assets/l03-memory-cell-read.png" target="_self">]]></description><link>assets/l03-memory-cell-read.html</link><guid isPermaLink="false">assets/L03-Memory-Cell-Read.png</guid><pubDate>Wed, 28 Jan 2026 20:57:48 GMT</pubDate><enclosure url="assets/l03-memory-cell-read.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-memory-cell-read.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L03-Memory-Cell-Tristate]]></title><description><![CDATA[<img src="assets/l03-memory-cell-tristate.png" target="_self">]]></description><link>assets/l03-memory-cell-tristate.html</link><guid isPermaLink="false">assets/L03-Memory-Cell-Tristate.png</guid><pubDate>Wed, 28 Jan 2026 20:57:48 GMT</pubDate><enclosure url="assets/l03-memory-cell-tristate.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-memory-cell-tristate.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L03-Memory-With-Decoder]]></title><description><![CDATA[<img src="assets/l03-memory-with-decoder.png" target="_self">]]></description><link>assets/l03-memory-with-decoder.html</link><guid isPermaLink="false">assets/L03-Memory-With-Decoder.png</guid><pubDate>Wed, 28 Jan 2026 20:57:48 GMT</pubDate><enclosure url="assets/l03-memory-with-decoder.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-memory-with-decoder.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L03-Binary-Decoder]]></title><description><![CDATA[<img src="assets/l03-binary-decoder.png" target="_self">]]></description><link>assets/l03-binary-decoder.html</link><guid isPermaLink="false">assets/L03-Binary-Decoder.png</guid><pubDate>Wed, 28 Jan 2026 20:57:48 GMT</pubDate><enclosure url="assets/l03-binary-decoder.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-binary-decoder.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L03-Memory-Grid-Selected]]></title><description><![CDATA[<img src="assets/l03-memory-grid-selected.png" target="_self">]]></description><link>assets/l03-memory-grid-selected.html</link><guid isPermaLink="false">assets/L03-Memory-Grid-Selected.png</guid><pubDate>Wed, 28 Jan 2026 20:57:48 GMT</pubDate><enclosure url="assets/l03-memory-grid-selected.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-memory-grid-selected.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L03-Memory-Grid-Latches]]></title><description><![CDATA[<img src="assets/l03-memory-grid-latches.png" target="_self">]]></description><link>assets/l03-memory-grid-latches.html</link><guid isPermaLink="false">assets/L03-Memory-Grid-Latches.png</guid><pubDate>Wed, 28 Jan 2026 20:57:48 GMT</pubDate><enclosure url="assets/l03-memory-grid-latches.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-memory-grid-latches.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L03-x64-Register-Layout]]></title><description><![CDATA[<img src="assets/l03-x64-register-layout.png" target="_self">]]></description><link>assets/l03-x64-register-layout.html</link><guid isPermaLink="false">assets/L03-x64-Register-Layout.png</guid><pubDate>Wed, 28 Jan 2026 20:57:21 GMT</pubDate><enclosure url="assets/l03-x64-register-layout.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-x64-register-layout.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L03-x86-32bit-Registers]]></title><description><![CDATA[<img src="assets/l03-x86-32bit-registers.png" target="_self">]]></description><link>assets/l03-x86-32bit-registers.html</link><guid isPermaLink="false">assets/L03-x86-32bit-Registers.png</guid><pubDate>Wed, 28 Jan 2026 20:57:21 GMT</pubDate><enclosure url="assets/l03-x86-32bit-registers.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-x86-32bit-registers.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L03-8086-Registers-Basic]]></title><description><![CDATA[<img src="assets/l03-8086-registers-basic.png" target="_self">]]></description><link>assets/l03-8086-registers-basic.html</link><guid isPermaLink="false">assets/L03-8086-Registers-Basic.png</guid><pubDate>Wed, 28 Jan 2026 20:57:21 GMT</pubDate><enclosure url="assets/l03-8086-registers-basic.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-8086-registers-basic.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L03-Digital-Simulator-CPU]]></title><description><![CDATA[<img src="assets/l03-digital-simulator-cpu.png" target="_self">]]></description><link>assets/l03-digital-simulator-cpu.html</link><guid isPermaLink="false">assets/L03-Digital-Simulator-CPU.png</guid><pubDate>Wed, 28 Jan 2026 20:57:21 GMT</pubDate><enclosure url="assets/l03-digital-simulator-cpu.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-digital-simulator-cpu.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L03-CPU-ALU-Decoder]]></title><description><![CDATA[<img src="assets/l03-cpu-alu-decoder.png" target="_self">]]></description><link>assets/l03-cpu-alu-decoder.html</link><guid isPermaLink="false">assets/L03-CPU-ALU-Decoder.png</guid><pubDate>Wed, 28 Jan 2026 20:57:21 GMT</pubDate><enclosure url="assets/l03-cpu-alu-decoder.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-cpu-alu-decoder.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L03-Instruction-Encoding]]></title><description><![CDATA[<img src="assets/l03-instruction-encoding.png" target="_self">]]></description><link>assets/l03-instruction-encoding.html</link><guid isPermaLink="false">assets/L03-Instruction-Encoding.png</guid><pubDate>Wed, 28 Jan 2026 20:57:21 GMT</pubDate><enclosure url="assets/l03-instruction-encoding.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-instruction-encoding.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L03-Instruction-Types-Gates]]></title><description><![CDATA[<img src="assets/l03-instruction-types-gates.png" target="_self">]]></description><link>assets/l03-instruction-types-gates.html</link><guid isPermaLink="false">assets/L03-Instruction-Types-Gates.png</guid><pubDate>Wed, 28 Jan 2026 20:57:21 GMT</pubDate><enclosure url="assets/l03-instruction-types-gates.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-instruction-types-gates.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L03-Hex-Binary-Conversion]]></title><description><![CDATA[<img src="assets/l03-hex-binary-conversion.png" target="_self">]]></description><link>assets/l03-hex-binary-conversion.html</link><guid isPermaLink="false">assets/L03-Hex-Binary-Conversion.png</guid><pubDate>Wed, 28 Jan 2026 20:57:21 GMT</pubDate><enclosure url="assets/l03-hex-binary-conversion.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-hex-binary-conversion.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L03-Hex-Powers-Table]]></title><description><![CDATA[<img src="assets/l03-temp/l03-hex-powers-table.gif" target="_self">]]></description><link>assets/l03-temp/l03-hex-powers-table.html</link><guid isPermaLink="false">assets/L03-temp/L03-Hex-Powers-Table.gif</guid><pubDate>Wed, 28 Jan 2026 20:57:21 GMT</pubDate><enclosure url="assets/l03-temp/l03-hex-powers-table.gif" length="0" type="image/gif"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-temp/l03-hex-powers-table.gif"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L03-NOTES-ENG. x86 Architecture - Registers and Memory]]></title><description><![CDATA[Quick reference for Lecture 3. See full materials in presentation slides.Base-16 system using digits 0-9 and A-F.Why hexadecimal?
One hex digit = exactly 4 binary bits
Compact representation of binary data
FF = 1111 1111 = 255
Grouping: 8 bits = 1 byte = 2 hex digits. Architectures: 8, 16, 32, 64 bits.Instructions are encoded as bit patterns that "wire" to specific hardware modules.Example opcode structure:
00** **** — Routes to ALU
0001 **** — Selects SUB instruction
**** 00** — First parameter (Register 0)
**** **01 — Second parameter (Register 1)
0001 0001 = 11h = SUB R0, R1Key insight: The number of bits for register selection determines how many registers the architecture supports. 2 bits → 4 registers.R0 = Accumulator (AX)
Hardwired to receive arithmetic results
No need to move data after operations
Convenient for sequential calculations
ADD and SUB are "wired back" to R0 automatically.Each can be split: AX = AH (high byte) + AL (low byte)
EAX, EBX, ECX, EDX (E = Extended)
ESI, EDI (index registers)
ESP (stack pointer), EBP (base pointer) RAX, RBX, RCX, RDX
R8–R15 (additional registers)
128-bit XMM0–XMM15 (SIMD)
Learn more:
<a data-tooltip-position="top" aria-label="https://www.cs.virginia.edu/~evans/cs216/guides/x86.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.cs.virginia.edu/~evans/cs216/guides/x86.html" target="_self">Guide to x86 Assembly</a>
Why are registers fast?
Directly wired to ALU
No address decoding needed
Why so few?
Physical constraints: each register needs wiring to every instruction
More registers = exponentially more wires
Result: Very few (4-16 general purpose), but extremely fast.Problem: 1 MB = 8,388,608 bits. Wiring each bit individually is impossible.Solution: Arrange bits in a 2D grid (matrix).
1,000,000 bits → ~2,000 wires (√n × 2)
Row decoder + Column decoder → select single bit Binary decoder converts address bits to row/column selection
Gated latch at each intersection stores one bit
AND gates ensure only addressed cell responds
Read/Write Enable signals control data direction
Data Bus: Single wire (or set of wires) carries data in/out.Learn more:
<br><a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=rM9BjciBLmg" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=rM9BjciBLmg" target="_self">How Computers Remember (Memory &amp; Addressing)</a> (YouTube)
Address bus width determines memory capacity:
20-bit → 1 MB (8086)
32-bit → 4 GB
64-bit → 16 EB (theoretical)
Released by Intel in 1978. 16-bit processor that gave rise to the x86 architecture.Why study 8086?
Simplest x86 architecture
Still directly compatible with modern x86-64
All modern features are extensions of these basics
Learn more:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Intel_8086" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Intel_8086" target="_self">Intel 8086 - Wikipedia</a>
Problem: 16-bit register can address only 64 KB (2^16 bytes). But 8086 needed to address 1 MB.Solution: Combine two 16-bit values:Absolute Address = (Segment × 16) + Offset
Example calculations:F000:FFFD → F0000 + FFFD ------ FFFFD (1,048,573 decimal) 923F:E2FF → 923F0 + E2FF ------ A06EF (657,135 decimal) Segments overlap every 16 bytes (one paragraph)
Same physical address can have ~4,096 different segment:offset pairs
Maximum addressable: 10FFEFh (just over 1 MB)
Learn more:
<br><a data-tooltip-position="top" aria-label="https://thestarman.pcministry.com/asm/debug/Segments.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://thestarman.pcministry.com/asm/debug/Segments.html" target="_self">Removing the Mystery from SEGMENT:OFFSET Addressing</a>
Typical program layout:
Code segment (CS) — program instructions
Data segment (DS) — variables and constants
Stack segment (SS) — function calls, local variables
Segments can overlap or be separate depending on program needs.Status flags set by arithmetic/logic operations:Control flags:
DF (Direction) — string operation direction
IF (Interrupt) — enable/disable interrupts
TF (Trap) — single-step debugging Hexadecimal is convenient because 1 hex digit = 4 bits
Instructions are encoded as bit patterns that "wire" to hardware
Registers are fast (wired to ALU) but scarce (expensive to wire)
Memory uses 2D grid addressing to reduce wire count
8086 uses segment:offset to address 1 MB with 16-bit registers
Segment registers separate code, data, and stack
Flags record operation results for conditional branching
You will:
Set up TASM + DOSBox + Turbo Debugger environment
Write your first assembly program
Observe registers and memory in the debugger
Think about:
How does mov ax, 5 differ from mov ax, [5]?
What happens when you add two numbers that overflow a register?
How would you calculate the physical address from CS:IP?
]]></description><link>lectures/eng/l03-notes-eng.-x86-architecture-registers-and-memory.html</link><guid isPermaLink="false">Lectures/ENG/L03-NOTES-ENG. x86 Architecture - Registers and Memory.md</guid><pubDate>Wed, 28 Jan 2026 20:19:42 GMT</pubDate></item><item><title><![CDATA[Lab03-Setup]]></title><description><![CDATA[
🍎 macOS / Linux?
Див. [Налаштування для macOS / Linux] нижче, потім повертайтесь до кроку 4 тут.
<img alt="loom-logo.png" src="assets/loom-logo.png" target="_self" style="width: 70px; max-width: 100%;"><br>
Див. відео 🎥: <a data-tooltip-position="top" aria-label="https://www.loom.com/share/49ad5b14659843a5a8df8329c6e29eea" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.loom.com/share/49ad5b14659843a5a8df8329c6e29eea" target="_self">Налаштування DosBox у Windows</a><br>
А також: <a data-tooltip-position="top" aria-label="https://www.loom.com/share/2325bbb3e5f84c08aaaa416af44fbee0" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.loom.com/share/2325bbb3e5f84c08aaaa416af44fbee0" target="_self">Запуск і робота у TD</a>
<br>Завантажте CSA-DosBox.zip (<a data-tooltip-position="top" aria-label="https://distedu.ukma.edu.ua/mod/page/view.php?id=178239&amp;forceview=1" rel="noopener nofollow" class="external-link is-unresolved" href="https://distedu.ukma.edu.ua/mod/page/view.php?id=178239&amp;forceview=1" target="_self">посилання на Distedu</a>)
Розпакуйте архів у корінь диска C:\. Має створитися каталог C:\DosBox ⚠️ ‼️ Важливо! якщо розпакуєте у інший шлях, потрібно буде змінити шляхи у файлі dosbox-csa.conf Перевірте, що бачите: C:\DosBox\DOSBox.exe
C:\DosBox\TASM\BIN\tasm.exe
C:\DosBox\dosbox-csa.conf
C:\DosBox\START-DOSBOX.bat Відкрийте файл C:\DosBox\dosbox-csa.conf у текстовому редакторі (Notepad)
Знайдіть рядок:
mount d "C:\Users\YOUR_USERNAME\YOUR_REPO_FOLDER" Як узнати шлях до свого репозиторію? Запустіть Terminal у VS Code через Ctrl+` і там наберіть команду pwd.
☢️⚠️ Увага‼️ Якщо у вас у шляху до репозиторію будуть кирилічні символи, працювати не буде. Потрібно перемістити репозиторій так, щоб цього не було.‼️
Замініть шлях на шлях до вашого GitHub-репозиторію, наприклад:
mount d "C:\Users\Korniychuk\labs-korniychuk" Збережіть файл Двічі клацніть на START-DOSBOX.bat УВАГА! Він може "вхопити" вашу мишу (зникне курсор, якщо клікнете у вікні). Щоб "звільнити" – використовуйте Ctrl-F10 ТАКОЖ DosBox можна запускати з вашого терміналу у VS Code ось так: C:\DosBox\START-DOSBOX.bat
DOSBox відкриється з привітанням
Перевірка: Ви маєте бачити папки Lab01 та Lab02:
dir Якщо бачите ваші папки — все налаштовано правильно! Створіть папку Lab03 У папці Lab03 створіть файл TEST.ASM із таким змістом:
.MODEL small
.STACK 100h
.DATA msg db 'Setup OK!$'
.CODE
start: mov ax, @data mov ds, ax mov ah, 09h mov dx, offset msg int 21h mov ah, 4Ch int 21h
end start Скомпілюйте та запустіть:
tasm /zi test.asm
tlink /v test.obj
test.exe /zi та /v додають debug-інформацію для Turbo Debugger. Якщо бачите Setup OK! — все працює! Запустіть дебагер:
td test.exe Натискайте F7 для покрокового виконання
Натисніть Alt+X для виходу
Обовʼязково додайте у файл .gitignore в корені вашого репозиторію:# TASM compiled files
*.obj
*.OBJ
*.exe
*.EXE
*.com
*.COM
*.map
*.MAP
*.lst
*.LST
*.tr
*.TR
Це запобіжить потраплянню скомпільованих файлів у Git.Після налаштування:C:\DosBox\
├── DOSBox.exe
├── START-DOSBOX.bat
├── dosbox-csa.conf ← тут ви редагували шлях
└── TASM\ └── BIN\ ├── tasm.exe ├── tlink.exe └── td.exe C:\Users\Ivanov\csa-ivanov\ ← ваш репозиторій (приклад)
├── Lab01\
├── Lab02\
└── Lab03\ └── TEST.ASM
Ви автоматично починаєте на диску D:.Все разом:tasm /zi file.asm &amp;&amp; tlink /v file.obj &amp;&amp; file.exe
<br><img alt="loom-logo.png" src="assets/loom-logo.png" target="_self" style="width: 70px; max-width: 100%;"><br>
Див. відео 🎥: <a data-tooltip-position="top" aria-label="https://www.loom.com/share/ec09829611d04cdc9d3d5ba3c335b0d6" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.loom.com/share/ec09829611d04cdc9d3d5ba3c335b0d6" target="_self">Налаштування DosBox на MacOS</a>macOS:brew install --cask dosbox
DOSBox встановиться у /Applications/dosbox.app/Linux (Ubuntu/Debian):sudo apt install dosbox <br>Завантажте CSA-DosBox-Mac.zip (<a data-tooltip-position="top" aria-label="https://distedu.ukma.edu.ua/mod/page/view.php?id=178239&amp;forceview=1" rel="noopener nofollow" class="external-link is-unresolved" href="https://distedu.ukma.edu.ua/mod/page/view.php?id=178239&amp;forceview=1" target="_self">посилання на Distedu</a>)
Розпакуйте архів у домашню папку. Має створитися каталог ~/DosBox/
Перевірте, що бачите:
~/DosBox/start-dosbox.sh
~/DosBox/dosbox-csa.conf
~/DosBox/TASM/BIN/tasm.exe Відкрийте файл ~/DosBox/dosbox-csa.conf у текстовому редакторі
Знайдіть рядок:
mount d "~/YOUR_REPO_FOLDER" Як узнати шлях до свого репозиторію? Запустіть Terminal у VS Code через Ctrl-` і там наберіть команду pwd.
☢️⚠️ Увага‼️ Якщо у вас у шляху до репозиторію будуть кирилічні символи, працювати не буде. Потрібно перемістити репозиторій так, щоб цього не було.‼️
Замініть шлях на шлях до вашого GitHub-репозиторію, наприклад:
mount d "/Users/korniychuk/Projects/labs-korniychuk" Збережіть файл
Linux: Якщо DOSBox встановлено через apt, відредагуйте start-dosbox.sh — замініть шлях до DOSBox на просто dosbox.Це можна зробити як з VS Code терміналу, так і із загального.zsh ~/DosBox/start-dosbox.sh УВАГА! DOSBox може "вхопити" вашу мишу. Щоб "звільнити" – використовуйте Ctrl-F10
Ви маєте бачити папки Lab01 та Lab02. Якщо бачите — все налаштовано правильно!Далі виконайте ті ж кроки перевірки, що й для Windows (Крок 4-6).~/DosBox/
├── start-dosbox.sh ← скрипт запуску
├── dosbox-csa.conf ← тут ви редагували шлях
└── TASM/ └── BIN/ ├── tasm.exe ├── tlink.exe └── td.exe ~/Projects/csa-ivanov/ ← ваш репозиторій (приклад)
├── Lab01/
├── Lab02/
└── Lab03/
]]></description><link>labs/lab03-setup.html</link><guid isPermaLink="false">Labs/Lab03-Setup.md</guid><pubDate>Wed, 28 Jan 2026 13:46:56 GMT</pubDate><enclosure url="assets/loom-logo.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/loom-logo.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[loom-logo]]></title><description><![CDATA[<img src="assets/loom-logo.png" target="_self">]]></description><link>assets/loom-logo.html</link><guid isPermaLink="false">assets/loom-logo.png</guid><pubDate>Tue, 27 Jan 2026 16:31:47 GMT</pubDate><enclosure url="assets/loom-logo.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/loom-logo.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Lab03]]></title><description><![CDATA[
Курс: Архітектура обчислювальних систем
Тиждень: 3
Тривалість: 2 академічні години
Тип: Практична робота під наглядом інструктора
Після завершення практики студент зможе:
Налаштувати середовище розробки TASM у DOSBox.
Компілювати та запускати асемблерні програми (.EXE файли).
Використовувати Turbo Debugger для покрокового виконання та спостереження за станом процесора.
Працювати з регістрами 8086: переміщувати дані та виконувати базову арифметику.
Обов'язково виконайте до заняття!Дотримуйтесь інструкції: <a class="internal-link" data-href="Lab03-Setup.md" href="labs/lab03-setup.html" target="_self" rel="noopener nofollow">Lab03-Setup.md</a>Коротко:
Завантажте CSA-DosBox.zip з Distedu
Розпакуйте у C:\DosBox
Відредагуйте dosbox-csa.conf — вкажіть шлях до вашого репозиторію
Запустіть START-DOSBOX.bat
Перевірте, що бачите папки Lab01 та Lab02 командою dir
Оновіть .gitignore (див. інструкцію)
Сегментні регістри: CS (Code), DS (Data), SS (Stack), ES (Extra)Див. "Перед заняттям" вище по тексту. Переконайтесь, що оновили .gitignore!
Запустіть DOSBox через START-DOSBOX.bat Можна запустити з VS Code Terminal: C:\DosBox\START-DOSBOX.bat Переконайтесь, що бачите папки Lab01 та Lab02 набравши команду dir
Створіть папку для цієї практики (якщо ще не створили):
md Lab03
cd Lab03 Створіть файл HELLO.ASM у папці Lab03 (використовуйте VS Code або інший редактор):; HELLO.ASM: your first program
.MODEL small
.STACK 100h .DATA msg db 'Hello, Assembly!', 0Dh, 0Ah, '$' .CODE
start: mov ax, @data ; setup data segment mov ds, ax ; use these two in ALL your programs mov ah, 09h ; DOS function: string output mov dx, offset msg int 21h mov ah, 4Ch ; DOS function: end program mov al, 0 int 21h end start
Скомпілюйте та запустіть:tasm /zi hello.asm
tlink /v hello.obj
hello.exe /zi та /v додають debug-інформацію для Turbo Debugger. Запустіть програму у дебагері:
td hello.exe Відкриється вікно Module з вихідним кодом програми Відкрийте додаткові вікна через меню View: Alt+V, R → Registers (регістри)
Alt+V, D → Dump (дамп пам'яті)
Alt+V, W → Watches (спостереження за змінними) Виконайте програму покроково: F7 — виконати одну інструкцію
Спостерігайте за змінами регістрів AX, DS, DX
Після int 21h (виведення рядка) натисніть Alt+F5 щоб побачити екран програми
Натисніть будь-яку клавішу щоб повернутися до дебагера Alt+X — вийти з дебагера Створіть файл REGS.ASM:; REGS.ASM - experimenting with registers
.MODEL small
.STACK 100h .CODE
start: ; Cleaning registers xor ax, ax ; AX = 0 xor bx, bx ; BX = 0 ; Loading values mov ax, 1234h ; AX = 1234h (hex) mov bx, 100 ; BX = 100 (decimal = 64h) mov cl, 0FFh ; CL = FFh ; Arithmetics add ax, bx ; AX = AX + BX sub ax, 50 ; AX = AX - 50 inc bx ; BX = BX + 1 dec bx ; BX = BX - 1 neg ax ; AX = -AX ; Copying mov dx, ax ; DX = AX mov ah, al ; AH = AL ; Exit your program mov ah, 4Ch int 21h end start
Скомпілюйте та запустіть у TD:tasm /zi regs.asm
tlink /v regs.obj
td regs.exe
Обчисліть вираз: result = (A + B) * 2 - CДе: A = 25, B = 17, C = 10Підказка: Множення на 2 можна зробити через ADD AX, AX.Напишіть програму, збережіть результат у регістрі AX та перевірте у Turbo Debugger.Очікуваний результат: AX = 004Ah (74 в десятковій системі).До кінця заняття:
Налаштоване середовище DOSBox + TASM (включно з .gitignore)
Скомпільована та запущена HELLO.EXE
Виконано покрокове налагодження у Turbo Debugger
Написана програма REGS.ASM з експериментами над регістрами
Закомічені HELLO.ASM та REGS.ASM
Дедлайн: 23:59 у день перед Практикою 4Напишіть програму CALC.ASM, яка:
Обчислює вираз: result = (A * 2 + B) - (C + D)
Використовує константи: A = 15, B = 23, C = 8, D = 12
Зберігає результат у регістрі AX
Вимоги:
Код має бути з коментарями українською або англійською
Використовуйте осмислені імена міток (якщо потрібні)
Запишіть одне Loom-відео, у якому:
Відкриваєте CALC.ASM у Turbo Debugger і пояснюєте код (що робить кожна інструкція)
Демонструєте покрокове виконання (F7), коментуючи зміни регістрів
Показуєте фінальний результат у регістрі AX
Lab03/
├── HELLO.ASM ← з пари
├── REGS.ASM ← з пари
├── CALC.ASM ← домашнє завдання
└── video.txt ← посилання на Loom-відео
⚠️ Не комітьте .OBJ та .EXE файли! Перевірте .gitignore.
<br><a class="internal-link" data-href="Lab03-Setup.md" href="labs/lab03-setup.html" target="_self" rel="noopener nofollow">Lab03-Setup.md</a> — детальна інструкція з налаштування
<br><a data-tooltip-position="top" aria-label="https://www.cs.virginia.edu/~evans/cs216/guides/x86.html" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.cs.virginia.edu/~evans/cs216/guides/x86.html" target="_self">Guide to x86 Assembly</a> — компактний довідник
<br><a data-tooltip-position="top" aria-label="https://www.gabrielececchetti.it/Teaching/CalcolatoriElettronici/Docs/i8086_instruction_set.pdf" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.gabrielececchetti.it/Teaching/CalcolatoriElettronici/Docs/i8086_instruction_set.pdf" target="_self">8086 Instruction Set</a> — повний список інструкцій
]]></description><link>labs/lab03.html</link><guid isPermaLink="false">Labs/Lab03.md</guid><pubDate>Tue, 27 Jan 2026 13:22:27 GMT</pubDate></item><item><title><![CDATA[L03-Hex-counter]]></title><description><![CDATA[<img src="assets/l03-hex-counter.gif" target="_self">]]></description><link>assets/l03-hex-counter.html</link><guid isPermaLink="false">assets/L03-Hex-counter.gif</guid><pubDate>Sun, 25 Jan 2026 18:10:56 GMT</pubDate><enclosure url="assets/l03-hex-counter.gif" length="0" type="image/gif"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l03-hex-counter.gif"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L02-Notes. Від вентилів до АЛП]]></title><description><![CDATA[Короткий довідник до Лекції 2. Повні матеріали див. у L02. Від вентилів до АЛП.md.Два фундаментальні типи схем у цифровій електроніці:Ключова ідея: Лише комбінаційної логіки недостатньо для побудови комп'ютера. Нам потрібна послідовна логіка для запам'ятовування стану.Детальніше:
<a data-tooltip-position="top" aria-label="https://www.jaroeducation.com/blog/difference-between-combinational-and-sequential-circuits/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.jaroeducation.com/blog/difference-between-combinational-and-sequential-circuits/" target="_self">Combinational vs Sequential Circuits</a>
Кожна позиція представляє степінь 2:Приклад: 10110 = 1×16 + 0×8 + 1×4 + 1×2 + 0×1 = 22Детальніше:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Binary_number" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Binary_number" target="_self">Binary Number (Wikipedia)</a>
Три способи представлення від'ємних чисел у двійковій системі:Старший біт (MSB) = знаковий біт (0 = додатне, 1 = від'ємне), решта бітів = модуль.
Для заперечення: Інвертувати лише знаковий біт
Проблема: Два нулі (+0 = 0000, -0 = 1000)
Проблема: Додавання вимагає попередньої перевірки знаків
Діапазон (N біт): -(2^(N-1) - 1) до +(2^(N-1) - 1)
Від'ємне = інвертування всіх бітів додатного числа.
Для заперечення: NOT(x) — інвертувати всі біти
Проблема: Два нулі (+0 = 0000, -0 = 1111)
Проблема: Додавання потребує циклічного переносу
Діапазон (N біт): -(2^(N-1) - 1) до +(2^(N-1) - 1)
Стандарт. Від'ємне = інвертування всіх бітів + додавання 1.
Для заперечення: NOT(x) + 1
Лише один нуль (немає проблеми -0)
Та сама схема додавання для додатних і від'ємних
Діапазон (N біт): -2^(N-1) до +(2^(N-1) - 1)
8-біт: -128 до +127
Приклад: Перетворення +5 на -5 (8-біт)+5: 00000101
NOT: 11111010
+1: 11111011 ← це -5
Детальніше:
<br><a data-tooltip-position="top" aria-label="https://uk.wikipedia.org/wiki/%D0%9F%D1%80%D1%8F%D0%BC%D0%B8%D0%B9_%D0%BA%D0%BE%D0%B4" rel="noopener nofollow" class="external-link is-unresolved" href="https://uk.wikipedia.org/wiki/%D0%9F%D1%80%D1%8F%D0%BC%D0%B8%D0%B9_%D0%BA%D0%BE%D0%B4" target="_self">Прямий код (Wikipedia UK)</a>
<br><a data-tooltip-position="top" aria-label="https://uk.wikipedia.org/wiki/%D0%9E%D0%B1%D0%B5%D1%80%D0%BD%D0%B5%D0%BD%D0%B8%D0%B9_%D0%BA%D0%BE%D0%B4" rel="noopener nofollow" class="external-link is-unresolved" href="https://uk.wikipedia.org/wiki/%D0%9E%D0%B1%D0%B5%D1%80%D0%BD%D0%B5%D0%BD%D0%B8%D0%B9_%D0%BA%D0%BE%D0%B4" target="_self">Обернений код (Wikipedia UK)</a>
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Two%27s_complement" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Two%27s_complement" target="_self">Two's Complement (Wikipedia)</a> Пам'ять = вихід, з'єднаний назад із входом
Вентиль OR зі зворотним зв'язком: Може «запам'ятати» 1
Коли вихід стає 1, він залишається 1 (незалежно від іншого входу)
Вентиль AND зі зворотним зв'язком: Може «запам'ятати» 0
Коли вихід стає 0, він залишається 0
Поєднання OR та AND зі зворотним зв'язком створює засувку:
Вхід SET: Зберігає «1»
Вхід RESET: Зберігає «0»
Проблема: Заборонений стан, коли SET=1 і RESET=1 одночасно
Розв'язує проблему забороненого стану:
Вхід DATA: Біт для збереження (0 або 1)
WRITE ENABLE: Коли захоплювати дані
Детальніше:
<br><a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=rM9BjciBLmg" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=rM9BjciBLmg" target="_self">How Transistors Remember Data</a> (YouTube, дивитись до 8:45)
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Flip-flop_(electronics)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Flip-flop_(electronics)" target="_self">Flip-Flop (Wikipedia)</a>
Без синхронізації схеми страждають від:
Стан гонки: Сигнали конкурують непередбачувано
Метастабільність: Нестабільні проміжні стани
Неконтрольоване спрацювання: Дані поширюються раніше, ніж потрібно
Приклад проблеми: АЛП додає A+B, зберігає результат назад у A. Без контролю часу результат продовжує повертатися (3→4→5→...) неконтрольовано.Стійкі коливання між 0 і 1:
Передній фронт: Момент переходу тактового сигналу LOW→HIGH
Тактовий цикл: Один повний період 0→1→0
Сучасні ЦП: мільярди циклів на секунду (ГГц)
Пам'ять із фронтовим спрацюванням (захоплює лише на фронті тактового сигналу):Побудований з двох D-засувок послідовно:
Перша засувка: Захоплює дані, коли тактовий сигнал LOW
Друга засувка: Копіює на вихід, коли тактовий сигнал переходить у HIGH
Результат: Дані зберігаються лише в точні моменти, запобігаючи неконтрольованому зворотному зв'язку.D-тригер + керуюча логіка:
DATA: Значення для збереження
STORE: Сигнал дозволу (вирішує, чи оновлювати)
CLOCK: Сигнал синхронізації
Коли STORE=0, регістр повертає власний вихід назад (зберігає значення).
Коли STORE=1, нові дані надходять на наступному фронті тактового сигналу.8 паралельних 1-бітних регістрів зі спільними CLOCK і STORE = 8-бітний регістр (1 байт)Детальніше:
<br><a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=I0-izyq6q5s" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=I0-izyq6q5s" target="_self">How Do Computers Remember</a> (YouTube)
Перетворює n вхідних бітів на 2^n вихідних ліній (кодування «один із багатьох»):Використовується для: Вибору одного з багатьох варіантів (адреси пам'яті, операції АЛП тощо).Кілька арифметичних схем паралельно:
8-бітний суматор
8-бітний віднімач
Інкрементер (+1)
Декрементер (-1)
Код операції (Opcode): Біти, що вказують, яку операцію виконуватиІнструкція: 00 01 00 01 ↑↑ ↑↑ | └── Тип операції (01 = SUB) └───── Клас інструкції (00 = ALU)
Двійковий декодер активує рівно одну схему на основі коду операції. Операнд_1 Операнд_2 ↓ ↓ ┌─────────────────┐ │ АЛП │ ← Код операції └─────────────────┘ ↓ ↓ Результат Прапорці (НУЛЬ?, ВІД'ЄМНЕ?, ПЕРЕПОВНЕННЯ?)
Детальніше:
<br><a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=HjneAhCy2N4" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=HjneAhCy2N4" target="_self">How Transistors Run Code</a> (YouTube) Комбінаційна + Послідовна = повний набір будівельних блоків комп'ютера
Доповняльний код дозволяє використовувати ті самі схеми для +/- арифметики
Петлі зворотного зв'язку створюють пам'ять з логічних вентилів
Тактовий сигнал запобігає станам гонки та забезпечує синхронізацію
D-тригер = керована 1-бітна пам'ять з фронтовим спрацюванням
Регістри = кілька тригерів зі спільним керуванням
Двійковий декодер вибирає одну операцію за бітами коду операції
АЛП = кілька арифметичних схем + декодер для вибору
Обов'язковий перегляд:Подумайте:
Чому NOT(x) + 1 дає -x у доповняльному коді?
Як двійковий декодер вибирає, яку операцію АЛП виконувати?
]]></description><link>lectures/l02-notes.-від-вентилів-до-алп.html</link><guid isPermaLink="false">Lectures/L02-Notes. Від вентилів до АЛП.md</guid><pubDate>Thu, 22 Jan 2026 21:03:36 GMT</pubDate></item><item><title><![CDATA[L02. Від вентилів до АЛП]]></title><description><![CDATA[Курс: Архітектура комп'ютерних систем
Тема: Побудова обчислень та пам'яті з логічних вентилів
Результати навчання:
РН1: Пояснити принципи побудови та функціонування сучасних комп'ютерних архітектур
РН2: Пояснити принципи представлення даних у комп'ютерних системах
У Лекції 1 ми прослідкували шлях від фізики до логічних вентилів. Ми побачили, як транзистори — діючи як електрично керовані перемикачі — об'єднуються для формування фундаментальних булевих операцій: NOT, AND, OR, NAND та NOR. Ці вентилі утворюють абетку цифрових обчислень. Тепер ми маємо навчитися писати слова, речення і врешті-решт цілі програми цією абеткою.Ця лекція розглядає ключове питання: як побудувати схеми, які можуть обчислювати та запам'ятовувати? Відповідь вимагає розуміння двох принципово різних типів цифрових схем: комбінаційної та послідовної логіки [1].Комбінаційні схеми виробляють виходи, які залежать виключно від їхніх поточних входів. За однакових вхідних значень комбінаційна схема завжди видаватиме той самий вихід — негайно і без жодного поняття історії. Ці схеми не мають пам'яті; вони є чистими функціями в математичному сенсі.Приклади комбінаційних схем включають:
Суматори: Обчислюють суму двійкових чисел
Дешифратори: Перетворюють закодовані входи на більший набір виходів
Мультиплексори: Вибирають один вхід з багатьох на основі керуючих сигналів
Компаратори: Визначають відношення між числами (більше, менше, рівно)
Поведінку будь-якої комбінаційної схеми можна повністю описати таблицею істинності. Для n входів таблиця істинності має 2^n рядків, кожен з яких визначає вихід для однієї комбінації входів.Послідовні схеми, навпаки, мають виходи, що залежать як від поточних входів, так і від історії попередніх входів. Вони володіють пам'яттю — певним внутрішнім станом, який зберігається з часом. Саме ця пам'ять дозволяє комп'ютерам виконувати програми крок за кроком, зберігаючи проміжні результати [2].Приклади послідовних схем включають:
Тригери: Зберігають один біт інформації
Регістри: Зберігають кілька бітів (зазвичай 8, 16, 32 або 64)
Лічильники: Циклічно проходять послідовність станів
Скінченні автомати: Реалізують складні послідовні поведінки
<img alt="Комбінаційні та послідовні схеми" src="assets/l02-combinational-vs-sequential.png" target="_self">Порівняння структур комбінаційних та послідовних схем. Комбінаційні схеми не мають шляхів зворотного зв'язку, тоді як послідовні схеми використовують зворотний зв'язок для підтримання стану.Послідовні схеми зазвичай потребують тактового сигналу — періодичної форми сигналу, що синхронізує зміни стану. Цей тактовий сигнал забезпечує одночасне оновлення стану всіма частинами схеми, запобігаючи умовам гонитви та часовим небезпекам.У цій лекції ми побудуємо обидва типи схем з наших базових будівельних блоків рівня вентилів. Ми побудуємо:
Схеми двійкової арифметики (комбінаційні): напівсуматори, повні суматори, суматори з послідовним переносом
Елементи пам'яті (послідовні): SR-засувки, D-тригери, регістри
Арифметико-логічний пристрій (АЛП): поєднання арифметичних та логічних операцій
Наприкінці ми матимемо всі компоненти, необхідні для розуміння того, як ЦП (центральний процесор) виконує обчислення.Перш ніж заглиблюватися в реалізацію схем, варто зрозуміти теоретичні основи, що лежать в основі цифрових систем. Теорія автоматів надає формальну математичну структуру для розуміння обчислень — таку, що безпосередньо відображається на схеми, які ми будуємо [3].Автомат (множина: автомати) — це абстрактна математична модель обчислювальної машини. Вона складається з:
Набору станів, які машина може займати
Алфавіту вхідних символів
Правил переходу, що визначають, як входи спричиняють зміни стану
Початкового стану та (опціонально) приймаючих станів
Теорія автоматів класифікує обчислювальні машини за їхньою потужністю — які задачі вони можуть розв'язувати. Ця класифікація утворює ієрархію:<br><img alt="Ієрархія теорії автоматів" src="assets/l02-automata-theory-hierarchy.png" target="_self">Ієрархія автоматів: машини Тюрінга можуть симулювати будь-який автомат з магазинною пам'яттю, який може симулювати будь-який скінченний автомат, який містить комбінаційну логіку як окремий випадок.Скінченні автомати (СА) безпосередньо стосуються проєктування цифрових схем. СА має скінченну кількість станів і переходить між ними на основі входів. Це ідеально відображається на послідовні схеми, побудовані з тригерів та вентилів [4].Розглянемо простий контролер світлофора. Він циклічно проходить через стани (зелений → жовтий → червоний → зелений) на основі входів таймера. Це СА з трьома станами та детермінованими переходами — саме те, що ми реалізуємо за допомогою послідовних логічних схем.СА бувають двох видів:
Автомати Мура: Виходи залежать лише від поточного стану
Автомати Мілі: Виходи залежать від поточного стану ТА поточних входів
Кожну цифрову схему з пам'яттю можна змоделювати як СА. Ця теоретична основа говорить нам дещо глибоке: послідовні схеми, які ми будуємо, є математично чітко визначеними об'єктами, поведінку яких можна формально аналізувати та верифікувати.Розуміння теорії автоматів допомагає нам:
Розпізнавати можливості та обмеження наших схем
Проєктувати автомати систематично, а не ad hoc
Верифікувати правильність наших реалізацій
Розуміти, де цифрові комп'ютери знаходяться в ієрархії обчислювальних моделей
Щоб виконувати арифметичні операції, наші схеми повинні працювати з числами. Але транзистори нічого не знають про десяткову систему — вони розуміють лише два стани: увімкнено та вимкнено, високий та низький, 1 та 0. Це приводить нас до двійкової системи числення [5].Як десяткова, так і двійкова є позиційними системами числення, де значення цифри залежить від її позиції. У десятковій системі (з основою 10) число 347 означає:3 × 10² + 4 × 10¹ + 7 × 10⁰ = 300 + 40 + 7 = 347
У двійковій системі (з основою 2) застосовується той самий принцип зі степенями 2. Двійкове число 1101 означає:1 × 2³ + 1 × 2² + 0 × 2¹ + 1 × 2⁰ = 8 + 4 + 0 + 1 = 13
Запам'ятовування перших кількох степенів 2 є необхідним для роботи з цифровими системами:Для швидкої довідки наведено двійкові представлення чисел 0-15 (що вміщуються у 4 біти):<br><img alt="Таблиця перетворення шістнадцяткових, двійкових та десяткових чисел" src="assets/l02-hex-binary-decimal-table.png" target="_self">Повна таблиця перетворення, що показує шістнадцяткові (0-F), двійкові та десяткові еквіваленти. Шістнадцяткова система часто використовується як компактне представлення двійкових значень.Двійкове у десяткове: Помножте кожен біт на його позиційний степінь 2 та підсумуйте.
Приклад: 10110₂ = 1×16 + 0×8 + 1×4 + 1×2 + 0×1 = 22₁₀
Десяткове у двійкове: Послідовно ділите на 2, записуючи залишки знизу вгору.
Приклад: Перетворити 22 у двійкове: 22 ÷ 2 = 11 залишок 0
11 ÷ 2 = 5 залишок 1
5 ÷ 2 = 2 залишок 1
2 ÷ 2 = 1 залишок 0
1 ÷ 2 = 0 залишок 1
Читаючи знизу вгору: 10110₂ Двійкове додавання підпорядковується простим правилам:Зверніть увагу: 1 + 1 = 10 у двійковій системі (десяткове 2), так само як 5 + 5 = 10 у десятковій.Потенціал двійкової системи для обчислень був визнаний Готфрідом Вільгельмом Лейбніцем у 1703 році, який бачив філософське значення у зведенні всіх чисел до комбінацій 0 та 1 [6]. Однак саме магістерська дисертація Клода Шеннона 1937 року продемонструвала, як булева алгебра може бути реалізована за допомогою електричних перемикачів — заклавши основу для всіх цифрових комп'ютерів.Двійкова система ідеально підходить для електронної реалізації, оскільки:
Два стабільні стани: Транзистори природно мають два стани (увімкнено/вимкнено, провідний/непровідний)
Завадостійкість: Розрізнення між двома станами набагато надійніше, ніж розрізнення десяти
Булева алгебра: Математичні операції безпосередньо відображаються на логічні вентилі
Простота: Менша кількість значень цифр означає простіші схеми для арифметики
Досі ми розглядали лише додатні цілі числа (та нуль). Але реальні обчислення потребують від'ємних чисел. Як представити -5 або -128, використовуючи лише 0 та 1? [7]З n бітів ми можемо представити 2^n різних значень. Для 8 бітів це 256 значень. Ми повинні вирішити, як розподілити їх між додатними та від'ємними числами. Було запропоновано кілька схем, кожна зі своїми компромісами.Найінтуїтивніший підхід використовує крайній лівий біт як індикатор знаку [C]:
0 = додатне
1 = від'ємне
У 8-бітному представленні знак-модуль:
+5 = 0 0000101
-5 = 1 0000101
Щоб змінити знак: Просто інвертуйте біт знаку (старший біт).Проблеми з представленням знак-модуль:
Два нулі: І 00000000 (+0), і 10000000 (-0) представляють нуль
Складна арифметика: Додавання вимагає перевірки знаків та можливого віднімання
Асиметричний діапазон: З 8 бітів діапазон становить від -127 до +127 (один код витрачається на дублікат нуля)
В оберненому коді від'ємні числа формуються інвертуванням (перемиканням) усіх бітів [D]:
+5 = 00000101
-5 = 11111010 (кожен 0 стає 1, кожна 1 стає 0)
Щоб змінити знак: Інвертуйте всі біти (застосуйте NOT до кожного біта).Це спрощує віднімання (просто інвертуйте та додайте), але все ще має проблеми:
Два нулі: 00000000 (+0) та 11111111 (-0)
Циклічний перенос: Додавання вимагає спеціальної обробки, коли перенос переходить через кінець
Доповнення до двох — це універсальний стандарт для представлення цілих чисел зі знаком у сучасних комп'ютерах. Щоб змінити знак числа:
Інвертуйте всі біти (обернений код)
Додайте 1
Приклад: Перетворення +5 у -5 (8 бітів)+5 = 00000101
Інверсія: 11111010
Додати 1: 11111011 = -5 у доповненні до двох
Перевірка: -5 + 5 має дорівнювати 0 11111011 (-5)
+ 00000101 (+5)
---------- 100000000
9-й біт (перенос на виході) відкидається, залишаючи 00000000 = 0. Працює!Діапазон: Для n бітів діапазон становить від -2^(n-1) до +2^(n-1) - 1
8 бітів: від -128 до +127
16 бітів: від -32 768 до +32 767
32 біти: приблизно від -2,1 мільярда до +2,1 мільярда
Єдиний нуль: Існує рівно одне представлення нуля (всі біти = 0)Асиметричний діапазон: Від'ємних чисел на одне більше, ніж додатних (наприклад, -128 існує, але +128 не існує у 8 бітах)Ключові спостереження:
Додатні числа (від 0 до +7) мають 0 як старший біт (MSB)
Від'ємні числа (від -8 до -1) мають 1 як старший біт
Старший біт діє як індикатор знаку, але не у простий спосіб знак-модуль
<br><img alt="Колесо доповнення до двох" src="assets/l02-twos-complement-wheel.png" target="_self" style="width: 500px; max-width: 100%;">Кругове представлення доповнення до двох. Рух за годинниковою стрілкою додає 1; рух проти годинникової стрілки віднімає 1. Перехід від 0111 (+7) до 1000 (-8) демонструє переповнення.<br><img alt="Операція NEG у доповненні до двох" src="assets/l02-twos-complement-neg-example.png" target="_self">Операція NEG (зміна знаку): інвертувати всі біти (NOT), потім додати 1. Це перетворює додатні числа на від'ємні і навпаки.Чому доповнення до двох стало універсальним стандартом? Тому що та сама схема додавання працює як для знакових, так і для беззнакових чисел [8].Розгляньмо додавання -3 та +5 (очікуємо +2): 1101 (-3 у 4-бітному доповненні до двох)
+ 0101 (+5)
------ 10010
Відкидаючи біт переносу: 0010 = +2. Правильно!Це означає:
Не потрібна окрема схема віднімання (відняти A = додати -A)
Не потрібна логіка визначення знаку для базових операцій
АЛП може трактувати числа як беззнакові або знакові — біти та операції ідентичні
Одна складність: коли додавання дає недійсний результат (переповнення)?Переповнення відбувається, коли додавання двох додатних чисел дає «від'ємний» результат, або додавання двох від'ємних чисел дає «додатний» результат. Це трапляється, коли результат перевищує допустимий діапазон.Приклад (4 біти): 7 + 1 = ? 0111 (+7)
+ 0001 (+1)
------ 1000 (-8 у доповненні до двох, а не +8!)
Все, що ми побудували досі — від простих вентилів до потужного АЛП (арифметико-логічного пристрою) — має фундаментальне обмеження: це комбінаційні схеми. Їхні виходи повністю й миттєво залежать від поточних входів. Змінюється вхід — змінюється вихід. Прибираємо вхід — інформація зникає.Але обчислення потребують більшого, ніж миттєві розрахунки. Розгляньмо навіть найпростішу задачу: додавання послідовності чисел. Потрібно десь зберігати проміжну суму, поки ви отримуєте наступне число. Потрібна пам'ять — здатність зберігати інформацію протягом часу, незалежно від поточних входів.Це і є проблема пам'яті: як створити схеми, що можуть запам'ятовувати? [1]Ключове розуміння приходить із напрочуд простої ідеї: зворотний зв'язок. Що, якщо ми з'єднаємо вихід вентиля з його ж входом? Таке кільцеве з'єднання створює щось дивовижне — схему, вихід якої залежить не лише від зовнішніх входів, а й від її власного попереднього стану.Поекспериментуймо з найпростішим зворотним зв'язком. Візьмемо вентиль OR і з'єднаємо його вихід з одним із його входів [1]. ┌─────────┐ A ──┤ │ │ OR ├──┬── Вихід ┌───┤ │ │ │ └─────────┘ │ └────────────────┘
<br><img alt="Вентиль OR зі зворотним зв'язком" src="assets/l02-or-gate-with-feedback.png" target="_self">Вентиль OR з виходом, з'єднаним з одним із входів. Щойно вихід стає 1, він залишається 1 назавжди — зворотний зв'язок створює самопідтримувальну петлю.Розгляньмо, що відбувається:
Спочатку припустимо, що вихід дорівнює 0 (і вхід A дорівнює 0)
Вентиль OR бачить входи 0 і 0, тому вихід залишається 0
Тепер коротко подамо на A значення 1
Вентиль OR бачить 1 і 0, видає 1
Вихід 1 повертається на вхід
Навіть коли A повертається до 0, вентиль OR бачить 0 і 1, тому вихід залишається 1!
Ми створили схему, яка запам'ятовує! Щойно вихід стає 1, він залишається 1 необмежено довго — зворотний зв'язок створює самопідтримувальну петлю, що зберігає стан.Але ця пам'ять має критичну ваду: вона може запам'ятати лише «1». Після встановлення немає способу скинути її назад до 0. Таблиця істинності вентиля OR гарантує: якщо хоча б один вхід дорівнює 1, вихід дорівнює 1. Оскільки вихід постійно подає назад 1, схема застрягає назавжди [1].Можливо, вентиль AND працюватиме краще? Спробуймо таке саме з'єднання зі зворотним зв'язком: ┌─────────┐ A ──┤ │ │ AND ├──┬── Вихід ┌───┤ │ │ │ └─────────┘ │ └────────────────┘
Поведінка прямо протилежна:
Якщо вихід колись став 0, він залишається 0 (AND вимагає, щоб обидва входи були 1)
Щойно вихід фіксується на 0, жодна зміна входу A не може його врятувати
Це так само марно — пам'ять, яка може запам'ятати лише «0», нічим не краща за ту, що пам'ятає лише «1» [1].Прорив настає при поєднанні обох підходів. Нам потрібні дві операції:
SET (Встановлення): примусово записати в пам'ять 1
RESET (Скидання): примусово повернути пам'ять до 0
SR-засувка (засувка Set-Reset) досягає цього за допомогою двох перехресно з'єднаних вентилів NOR [1]: ┌─────────┐ R ────┤ │ │ NOR ├──┬───────────────── Q ┌───┤ │ │ │ └─────────┘ │ ┌─────────┐ │ └───┤ │ │ │ NOR ├──┬── Q̄ (не Q) │ ┌───┤ │ │ │ │ └─────────┘ │ └────────────────┼────────────────┘ │ S ─────────────────┘
Вихід кожного вентиля NOR подається на вхід іншого вентиля, створюючи стабільну систему зі зворотним зв'язком із двома комплементарними виходами: Q і Q̄.Як це працює:<br><img alt="Схема SR-засувки" src="assets/l02-sr-latch-set-reset.png" target="_self">SR-засувка, побудована з перехресно з'єднаних вентилів NOR. Вхід SET примусово встановлює Q = 1; вхід RESET примусово встановлює Q = 0. Коли обидва входи дорівнюють 0, засувка зберігає свій поточний стан.Коли S=1 і R=0, нижній вентиль NOR змушений видавати 0 (оскільки один вхід дорівнює 1). Цей 0 подається на верхній вентиль NOR, який тепер має входи R=0 і 0, що дає вихід 1. Схема стабілізується з Q=1.Коли S=0 і R=1, відбувається протилежне: верхній вентиль NOR видає 0, що поширюється і встановлює Q̄=1 та Q=0.Коли і S, і R дорівнюють 0, жоден вентиль не примушується, тому схема зберігає той стан, у якому перебувала — справжня пам'ять!Проблема забороненого стану:Коли S=1 і R=1 одночасно, обидва вентилі NOR змушені видавати 0. Це порушує фундаментальну вимогу, щоб Q і Q̄ були комплементарними. Гірше того, коли обидва входи повертаються до 0, схема входить у стан гонки (race condition) — кінцевий стан залежить від того, який вентиль відреагує швидше, що визначається мікроскопічними варіаціями виробництва. Результат непередбачуваний [1].<br><img alt="Недійсний стан SR-засувки" src="assets/l02-sr-latch-invalid-state.png" target="_self">Заборонений стан: коли і SET, і RESET дорівнюють 1, обидва виходи стають 0. Знак попередження вказує, що це недійсна та непередбачувана конфігурація.Проблема ввімкнення живлення:Коли живлення вперше подається, який стан приймає SR-засувка? Відповідь: це залежить від того, транзистори якого вентиля активуються швидше. Початковий стан по суті випадковий, визначений мікроскопічними виробничими відмінностями. Ось чому комп'ютерам потрібні явні послідовності ініціалізації при запуску [1].SR-засувка працює, але має незручний інтерфейс. Два окремих дроти керують встановленням і скиданням, і користувач повинен гарантувати, що вони ніколи не активні одночасно. Це схильне до помилок і ускладнює проєктування схем.Те, що нам справді потрібно, простіше:
Один дріт для значення DATA (Даних), яке ми хочемо зберегти (0 або 1)
Один дріт, щоб сказати ENABLE (Дозвіл) — «захопи дані зараз!»
Саме це забезпечує D-засувка (засувка даних) [1].D-засувка огортає SR-засувку додатковою логікою, яка унеможливлює вхід у заборонений стан: ┌─────────┐ ┌─────┤ │ │ │ AND ├────── R Enable ─────┼──┬──┤ │ ┌───────┐ │ │ └─────────┘ │ │ │ │ ┌───┤ SR ├─── Q │ │ ┌─────────┐ │ │ Latch │ Data ─────┼──┼──┤ NOT ├──┐ │ ┌─┤ ├─── Q̄ │ │ └─────────┘ │ │ │ └───────┘ │ │ │ │ │ │ │ ┌─────────┐ │ │ │ │ └──┤ │ │ │ │ │ │ AND ├──┼──────────┘ │ └─────┤ │ │ └─────────┘ │ S ──────────────────┘
Ключове розуміння: Data і NOT(Data) ніколи не можуть обидва бути 1. Застосовуючи AND кожного з Enable, ми створюємо сигнали Set і Reset, які є взаємовиключними за конструкцією [1].<br><img alt="D-засувка з дозволом запису" src="assets/l02-latch-write-enable.png" target="_self">Інтерфейс D-засувки: один вхід Data і керуючий сигнал Write Enable (або Enable). Цей спрощений інтерфейс запобігає забороненому стану за конструкцією.<br><img alt="Реалізація D-засувки на NAND" src="assets/l02-d-latch-nand-circuit.png" target="_self">D-засувка, побудована з вентилів NAND. Вхід D (Data) керується сигналом E (Enable). Коли дозволено, засувка захоплює D; коли заборонено, вона зберігає попереднє значення.Коли Enable = 0:
Обидва вентилі AND видають 0 (незалежно від Data)
S=0 і R=0, тому SR-засувка зберігає своє поточне значення
Вхід Data повністю ігнорується
Коли Enable = 1:
Якщо Data=1: S=1, R=0 → засувка встановлюється в 1
Якщо Data=0: S=0, R=1 → засувка скидається в 0
Засувка захоплює будь-яке значення, що є на лінії Data
<br><img alt="Реалізація D-засувки в NandGame" src="assets/l02-nandgame-d-latch-circuit.png" target="_self">Реалізація D-засувки з навчального симулятора NandGame. Схема використовує вентилі AND, вентиль NOT і вентилі NOR в ядрі SR-засувки.D-засувка елегантно розв'язує проблему забороненого стану — структура схеми робить S=R=1 неможливим. Вона надає саме той інтерфейс, який ми хотіли: вхід Data і керуючий сигнал Enable [1].D-засувка має одне суттєве обмеження, яке стає критичним у більших системах: вона є чутливою до рівня (також називається прозорою).Поки Enable=1, вихід Q безперервно слідує за входом Data. Засувка насправді не «засувається» в цей час — вона прозоро пропускає дані наскрізь. Тільки коли Enable повертається до 0, останнє значення справді захоплюється й утримується [2].Одна D-засувка зберігає один біт. Щоб зберігати більші значення — байти, слова, адреси — ми просто використовуємо кілька засувок зі спільним сигналом Enable [1]: Data[0] ──┬── D Latch ──── Q[0] │ Data[1] ──┼── D Latch ──── Q[1] │ Data[2] ──┼── D Latch ──── Q[2] │ Data[3] ──┼── D Latch ──── Q[3] │ Enable ─────────┴── (shared)
Це створює 4-бітний регістр. Усі чотири засувки захоплюють свої входи одночасно, коли Enable переходить у високий стан. Концепція масштабується тривіально: 8 засувок для байта, 32 для слова, 64 для регістрів сучасних процесорів.Але в цій конструкції криється серйозна проблема.Розглянемо фундаментальну обчислювальну операцію: інкрементування лічильника. Ми маємо регістр, що зберігає значення, суматор, який обчислює значення+1, і результат повертається назад до того самого регістра [2]: ┌─────────────────────────────────────┐ │ │ │ ┌──────────┐ ┌──────────┐ │ └────┤ Регістр ├────┤ Суматор ├─────┘ │ (D- │ │ (+1) │ │ засувки)│ └──────────┘ └────┬─────┘ │ Enable (Store)
З D-засувками подивіться, що відбувається, коли ми активуємо Enable для збереження інкрементованого значення:
Регістр містить 3, суматор видає 4
Enable переходить у HIGH
Регістр захоплює 4, тепер видає 4
Суматор тепер обчислює 4+1=5
Регістр захоплює 5 (Enable все ще високий!)
Суматор обчислює 5+1=6
Регістр захоплює 6...
Неконтрольоване зростання! Значення некеровано зростає [2]
Проблема є фундаментальною: поки D-засувка прозора (Enable=1), вона безперервно передає вхід на вихід. У будь-якій схемі зі зворотним зв'язком це створює нестабільну петлю. Значення регістра залежить від того, як довго Enable залишається високим — абсолютно непередбачувано та непридатно для використання.Рішення полягає в зміні моменту захоплення даних. Замість захоплення протягом усього періоду високого Enable, нам потрібно захоплювати в один точний момент [2].На сцену виходить тактовий сигнал: регулярна, періодична хвиля, що коливається між 0 і 1: ┌───┐ ┌───┐ ┌───┐ ┌───┐
Clock │ │ │ │ │ │ │ │ ──┘ └───┘ └───┘ └───┘ └── ↑ ↑ ↑ ↑ Передні Передні Передні Передні фронти фронти фронти фронти
<br><img alt="Осцилограма реального тактового сигналу" src="assets/l02-clock-signal-waveform.png" target="_self">Реальний тактовий сигнал, що показує час наростання, час спаду та невеликий викид. Ідеалізована прямокутна хвиля має неміттєві переходи у фізичних схемах.Ключове розуміння: замість спрацьовування за рівнем (тактовий сигнал високий чи низький), ми спрацьовуємо за фронтом — моментом переходу:
Передній фронт: мить переходу тактового сигналу з 0 до 1
Задній фронт: мить переходу тактового сигналу з 1 до 0
Фронт теоретично миттєвий — нескінченно короткий момент часу. За цю мить:
Дані зчитуються та захоплюються
Вікно захоплення настільки коротке, що зворотний зв'язок не встигає поширитися
З фронтовим спрацьовуванням наш лічильник працює коректно:
Регістр містить 3, суматор видає 4
Відбувається передній фронт — регістр захоплює рівно 4
Регістр тепер видає 4, суматор видає 5
Нічого не змінюється до НАСТУПНОГО переднього фронту
Наступний передній фронт — регістр захоплює рівно 5
Передбачувано, контрольовано, правильно [2]
Фронт тактового сигналу діє як затвор фотоапарата — захоплює знімок даних в один точний момент, не зважаючи на будь-які зміни, що відбуваються до чи після.Термінологія може бути заплутаною, тому будьмо точними:
D-засувка: спрацьовує за рівнем. Прозора, поки Enable високий.
D-тригер: спрацьовує за фронтом. Захоплює лише на фронті тактового сигналу.
D-тригер — це робоча конячка цифрової пам'яті. Він вирішує проблему гонки, що переслідує D-засувки в схемах зі зворотним зв'язком [2][3].Найелегантніший спосіб побудувати тригер із фронтовим спрацьовуванням — з двох D-засувок у конфігурації «ведучий-ведений» (master-slave) [3]: ┌─────────────────────────────────────────┐ │ │ Data ─────┼──┬── D-засувка ────┬── D-засувка ───┬───┼─── Q │ │ (Ведучий) │ (Ведений) │ │ │ │ │ │ │ Clock ────┼──┴── Enable └── Enable ──────┘ │ │ │ │ │ │ │ ┌───────┐ │ │ │ └────┤ NOT ├─────┘ │ │ └───────┘ │ └─────────────────────────────────────────┘
Критичне розуміння: дві засувки мають інвертовані сигнали дозволу:
Ведучий активний, коли Clock = 0 (LOW)
Ведений активний, коли Clock = 1 (HIGH)
Вони ніколи не бувають обидві прозорими одночасно [2][3].<br><img alt="D-тригер із двох D-засувок" src="assets/l02-nandgame-d-flipflop.png" target="_self">D-тригер, побудований із двох D-засувок у конфігурації «ведучий-ведений». Ведуча засувка активна, коли тактовий сигнал LOW; ведена засувка активна, коли тактовий сигнал HIGH.Прослідкуймо роботу протягом одного тактового циклу:Фаза 1: тактовий сигнал LOW
Ведуча засувка прозора — вона слідує за входом Data
Ведена засувка непрозора — вона утримує своє попереднє значення, ігноруючи вихід ведучої
Зовнішній вихід Q залишається стабільним
Фаза 2: тактовий сигнал переходить у HIGH (передній фронт)
Ведуча засувка стає непрозорою — вона заморожується, утримуючи те значення, яке мала
Ведена засувка стає прозорою — вона захоплює заморожене значення ведучої
Зовнішній вихід Q оновлюється до захопленого значення
Фаза 3: тактовий сигнал залишається HIGH
Ведуча все ще непрозора — зміни на вході Data ігноруються
Ведена прозора, але ведуча не змінюється, тому Q стабільний
Фаза 4: тактовий сигнал переходить у LOW (задній фронт)
Ведена стає непрозорою — вихід Q зафіксовано
Ведуча стає прозорою — починає знову слідувати за Data
Але ведена ігнорує ведучу, тому Q не змінюється
Кінцевий ефект: Q змінюється лише в момент переднього фронту, захоплюючи те значення Data, яке було в цю мить [2][3].«X» означає «байдуже» — значення входу не впливає на вихід, окрім моменту спрацьовування за фронтом.<br><img alt="D-тригер із керуванням збереженням" src="assets/l02-nandgame-d-flipflop-store.png" target="_self">D-тригер із входами DATA, STORE та CLOCK. Сигнал STORE дозволяє вибіркове оновлення — лише коли STORE активний, тригер захоплює нові дані на фронті тактового сигналу.D-тригер — це, мабуть, найважливіший будівельний блок у цифрових системах після базових логічних вентилів. Він забезпечує:
Передбачуваність тайму: стан змінюється лише на фронтах тактового сигналу
Імунітет до гонок: схеми зі зворотним зв'язком працюють коректно
Синхронізацію: усі тригери, що мають спільний тактовий сигнал, оновлюються одночасно
Ізоляцію станів: кожен тактовий цикл незалежний
Один D-тригер зберігає один біт. Об'єднання кількох тригерів зі спільним тактовим сигналом створює регістр — блок, що зберігає кілька бітів одночасно [1][2]: ┌─────────┐ Data[0] ─────────┤ D Q ├─────── Q[0] │ FF │ Clock ───────────┤&gt; │ └─────────┘ ┌─────────┐ Data[1] ─────────┤ D Q ├─────── Q[1] │ FF │ Clock ───────────┤&gt; │ └─────────┘ ┌─────────┐ Data[2] ─────────┤ D Q ├─────── Q[2] │ FF │ Clock ───────────┤&gt; │ └─────────┘ ... ... ...
Типові розміри регістрів:
8 тригерів = 8-бітовий регістр (1 байт)
16 тригерів = 16-бітовий регістр (півслово)
32 тригери = 32-бітовий регістр (слово, поширено в старих ЦП)
64 тригери = 64-бітовий регістр (подвійне слово, сучасні ЦП)
Ліворуч: 1-бітовий регістр із входами DATA, STORE та CLOCK. Праворуч: 4-бітовий регістр, побудований з чотирьох 1-бітових регістрів зі спільним тактовим сигналом та сигналом збереження.<br><img alt="Зберігання байта: 8 бітів" src="assets/l02-byte-8-bits.png" target="_self">Байт (8 бітів), збережений у 8 засувках. Кожна засувка містить один біт значення, що дозволяє зберігати числа від 0 до 255 (беззнакові) або від -128 до +127 (знакові).На кожному фронті тактового сигналу всі біти оновлюються одночасно, захоплюючи повне багатобітове значення атомарно.Іноді ми хочемо, щоб регістр утримував своє значення, навіть коли тактовий сигнал продовжує працювати. Не кожен тактовий цикл повинен оновлювати кожен регістр [2].Рішення додає мультиплексор перед кожним тригером: ┌─────────┐ Data ────────────┬────┤ 0 │ ┌─────────┐ │ │ MUX ├────┤ D Q ├──┬── Q ┌─────┼────┤ 1 │ │ FF │ │ │ │ └────┬────┘ │ │ │ │ │ │ └────┬────┘ │ │ │ Store │ │ │ │ Clock │ └─────┼────────────────────────────────┘ │ (feedback)
Принцип роботи:
Коли Store = 0: MUX вибирає шлях зворотного зв'язку; тригер повторно захоплює власний вихід; значення утримується
Коли Store = 1: MUX вибирає зовнішні дані (Data); тригер захоплює нове значення
Це дозволяє вибіркове оновлення: лише регістри з Store=1 змінюються на кожному фронті тактового сигналу. Всі інші зберігають свої значення необмежено довго [2].Тепер, коли ми розуміємо, як будувати логічні вентилі та елементи пам'яті, ми можемо перейти до однієї з найфундаментальніших операцій в обчисленнях: арифметики. Кожне обчислення, яке виконує ваш комп'ютер — від простого додавання до складного рендерингу графіки — зрештою зводиться до двійкових арифметичних операцій, що виконуються спеціалізованими схемами.Двійкове додавання слідує тим самим принципам, що й десяткове додавання, але з лише двома цифрами (0 і 1). Базові правила надзвичайно прості:Останній випадок є ключовим: коли ми додаємо 1 + 1, отримуємо 2, що в двійковій системі записується як «10» (один-нуль). Це означає, що ми записуємо 0 і переносимо 1 на наступну позицію — точно так само, як перенос у десятковій арифметиці, коли ми додаємо 7 + 5 = 12 [1].Розглянемо додавання 5 + 3 у двійковій системі: 101 (5 у двійковій системі) + 011 (3 у двійковій системі) ----- 1000 (8 у двійковій системі)
Переноси поширюються справа наліво, так само як у десятковому додаванні.Напівсуматор — це найпростіша арифметична схема, призначена для додавання двох однобітових чисел [1]. Він має:Входи:
A: перший біт для додавання
B: другий біт для додавання
Виходи:
Sum (Сума): біт результату (без переносу)
Carry (Перенос): біт переносу
Дивлячись на правила додавання, ми можемо визначити логіку:
Sum дорівнює 1, коли входи різні (один — 0, інший — 1)
Carry дорівнює 1, коли обидва входи — 1
Це безпосередньо відповідає вентилям, які ми вже знаємо:
Sum = A XOR B (XOR видає 1, коли входи різні)
Carry = A AND B (AND видає 1, коли обидва входи — 1)
Таблиця істинності напівсуматора:Схема напівсуматора (ASCII-представлення):A ----+---[XOR]---- Sum | |
B ----+-----+ | | +---[AND]---- Carry
<br><img alt="Схема напівсуматора" src="assets/l02-tc-half-adder.png" target="_self">Напівсуматор з гри Turing Complete. Вентиль XOR створює вихід Sum; вентиль AND створює вихід Carry.Напівсуматор чудово працює для додавання двох бітів, але має критичне обмеження: він не може приймати вхідний перенос від попереднього додавання. При додаванні багатобітових чисел кожна позиція (крім крайньої правої) повинна обробляти потенційний перенос від позиції праворуч.Повний суматор вирішує проблему вхідного переносу, приймаючи три входи замість двох [1]:Входи:
A: перший біт для додавання
B: другий біт для додавання
Cin (Carry-in, вхідний перенос): перенос з попередньої позиції
Виходи:
Sum (Сума): біт результату
Cout (Carry-out, вихідний перенос): перенос на наступну позицію
Повний суматор будується з двох напівсуматорів та вентиля OR:
Перший напівсуматор: Додає A і B, створюючи проміжну суму та проміжний перенос
Другий напівсуматор: Додає проміжну суму та Cin, створюючи кінцеву суму (Sum) та другий перенос
Вентиль OR: Об'єднує обидва проміжні переноси для створення Cout
Таблиця істинності повного суматора:Логіка Cout: перенос виникає, якщо принаймні два з трьох входів дорівнюють 1.<br><img alt="Схема повного суматора" src="assets/l02-tc-full-adder.png" target="_self">Повний суматор з гри Turing Complete. Побудований з двох напівсуматорів та вентиля OR. Вхідний перенос (Cin) дозволяє з'єднувати кілька суматорів разом.Для додавання багатобітових чисел ми з'єднуємо повні суматори разом, створюючи суматор з послідовним переносом (ripple carry adder) [1]. Для N-бітового суматора:
З'єднуємо N повних суматорів послідовно
Вихідний перенос (Cout) позиції i з'єднується з вхідним переносом (Cin) позиції i+1
Cin першого суматора зазвичай встановлюється в 0
Кожен суматор обробляє одну бітову позицію операндів
Структура 4-бітового суматора з послідовним переносом:A3 B3 A2 B2 A1 B1 A0 B0 | | | | | | | | v v v v v v v v
[FA-3]&lt;---[FA-2]&lt;---[FA-1]&lt;---[FA-0]&lt;--- Cin=0 | | | | v v v v S3 S2 S1 S0 Cout
Термін «послідовний» (ripple) описує, як перенос повинен проходити через кожен суматор послідовно. Це створює проблему синхронізації: перенос з позиції 0 повинен досягти позиції N-1, перш ніж кінцевий результат стане дійсним. Для 64-бітового суматора ця затримка може бути значною [1].<br><img alt="4-бітовий суматор з послідовним переносом" src="assets/l02-tc-4bit-adder.png" target="_self">4-бітовий суматор з послідовним переносом з гри Turing Complete. Чотири повні суматори з'єднані ланцюгом, де кожен Cout під'єднано до наступного Cin.Сучасні процесори використовують більш складні конструкції, такі як суматори з прискореним переносом (carry-lookahead adders), які передбачають переноси паралельно, значно зменшуючи затримку.Однією з елегантних особливостей представлення чисел у доповняльному коді є те, що віднімання стає додаванням [1]. Щоб обчислити A - B:
Обчислити доповняльний код B (інвертувати всі біти, потім додати 1)
Додати: A + (-B)
Апаратна реалізація надзвичайно ефективна:
Вентилі XOR умовно інвертують B: коли керуючий сигнал дорівнює 1, кожен біт B проходить через XOR з 1, що інвертує його (оскільки X XOR 1 = NOT X)
Встановити Cin = 1 для додавання «+1», необхідного для доповняльного коду
Та сама схема суматора виконує як додавання, так і віднімання
Ця конструкція подвійного призначення означає, що один блок суматора/віднімача обробляє обидві операції, вибираючи їх одним керуючим бітом.Переповнення виникає, коли арифметичний результат не може бути представлений у доступних бітах [1].Беззнакове переповнення:
Виявляється перевіркою кінцевого вихідного переносу
Якщо Cout = 1, результат перевищив максимальне беззнакове значення
Знакове переповнення:
Більш тонке: виникає, коли знак результату неправильний
Додавання двох додатних чисел не повинно давати від'ємний результат
Додавання двох від'ємних чисел не повинно давати додатний результат
Виявляється порівнянням переносу в старший біт (MSB) з переносом із старшого біта
Процесори встановлюють прапорці для індикації умов переповнення, дозволяючи програмному забезпеченню виявляти та обробляти арифметичні помилки.Декодери — це важливі будівельні блоки, які перетворюють двійкові коди на індивідуальні сигнали вибору. Вони з'являються всюди в цифрових системах, від адресації пам'яті до виконання інструкцій.Декодер приймає N-бітовий двійковий вхід і активує рівно один з 2^N виходів [1]. Це називається кодуванням «один із багатьох» (one-hot), оскільки лише один вихід є «гарячим» (активним/високим) у будь-який момент часу.Типові конфігурації декодерів:
Декодер 2-на-4: 2 вхідні біти вибирають один з 4 виходів
Декодер 3-на-8: 3 вхідні біти вибирають один з 8 виходів
Декодер 4-на-16: 4 вхідні біти вибирають один з 16 виходів
Декодер по суті «перекладає» двійкове число у фізичний вибір — вказуючи на один конкретний елемент серед багатьох можливих.Зверніть увагу, що двійковий вхід (A1 A0) безпосередньо вказує, який вихід активний: вхід «00» активує Y0, вхід «01» активує Y1, і так далі.<br><img alt="Приклад двійкового декодера" src="assets/l02-binary-decoder-example-2.png" target="_self">Декодер 2-на-4 зі входом «10» (десяткове 2). Лише Output_2 активний (показано зеленим), демонструючи принцип кодування «один із багатьох».Кожен вихід декодера є результатом AND відповідної комбінації входів та їх доповнень [1]:Y0 = NOT(A1) AND NOT(A0) (активний, коли A1=0, A0=0)
Y1 = NOT(A1) AND A0 (активний, коли A1=0, A0=1)
Y2 = A1 AND NOT(A0) (активний, коли A1=1, A0=0)
Y3 = A1 AND A0 (активний, коли A1=1, A0=1)
Схема декодера 2-на-4 (ASCII-представлення):A1 ----+----[NOT]---+---[AND]--- Y0 (коли 00) | | |
A0 ----+---[NOT]----+-----+ | | | | +-----+---[AND]--- Y1 (коли 01) | | | +------------+-----+ | | +---[NOT]----+---[AND]--- Y2 (коли 10) | | | +------------+---[AND]--- Y3 (коли 11)
<br><img alt="Схема двійкового декодера" src="assets/l02-binary-decoder-circuit.png" target="_self">Схема двійкового декодера 2-на-4. Вентилі NOT створюють доповнення входів; вентилі AND об'єднують входи та доповнення для створення виходів «один із багатьох».Декодери з'являються в численних контекстах у цифрових системах [1]:В АЛП (арифметико-логічному пристрої) декодер служить мостом між кодом операції (полем операції інструкції) та фактичним апаратним забезпеченням, що виконує операцію [1]:
ЦП витягує біти коду операції з інструкції
Ці біти надходять на декодер
Декодер активує рівно один вихід
Цей вихід вмикає відповідну схему операції
Результат вибраної схеми стає виходом АЛП
Цей механізм показує, як абстрактні програмні інструкції («додати ці числа») стають конкретними апаратними діями (активація схеми суматора).Арифметико-логічний пристрій (АЛП) є кульмінацією всього, що ми побудували: транзистори формують вентилі, вентилі формують суматори та декодери, і все це об'єднується в єдиний універсальний обчислювальний механізм у серці кожного процесора.АЛП — це «калькулятор» всередині кожного ЦП (центрального процесора) [1]. Він виконує дві категорії операцій:Арифметичні операції:
Додавання
Віднімання
Множення (часто в окремому блоці)
Ділення (часто в окремому блоці)
Інкремент/Декремент
Логічні операції:
AND (побітовий)
OR (побітовий)
XOR (побітовий)
NOT (побітовий)
Операції зсуву
Кожне обчислення, яке виконує комп'ютер — чи то рендеринг відео, шифрування даних або робота з електронною таблицею — зрештою розкладається на послідовності цих фундаментальних операцій АЛП.Типовий АЛП отримує три категорії входів [1]:Наприклад, 32-бітовий АЛП з 4-бітовим кодом операції може виконувати до 16 різних операцій над 32-бітовими числами.АЛП видає як результат, так і інформацію про стан [1]:Основний вихід:
Результат: Обчислене значення (такої ж розрядності, як операнди)
Виходи прапорців/стану:
Прапорець нуля (Z): Встановлюється в 1, якщо результат дорівнює нулю
Прапорець від'ємності (N): Встановлюється в 1, якщо результат від'ємний (MSB = 1 у знаковій інтерпретації)
Прапорець переносу (C): Встановлюється в 1, якщо сталося беззнакове переповнення
Прапорець переповнення (V): Встановлюється в 1, якщо сталося знакове переповнення
Ці прапорці критично важливі для умовних операцій. Наприклад, після віднімання (порівняння) прапорець нуля вказує на рівність, тоді як прапорці від'ємності та переповнення разом вказують на відношення «більше» або «менше».<br><img alt="Блок-схема АЛП" src="assets/l02-alu-block-diagram.png" target="_self">Блок-схема АЛП, що показує три категорії входів (операнди A і B, код операції) та дві категорії виходів (результат і прапорці стану).Базовий АЛП складається з трьох основних компонентів, що працюють разом [1]:1. Паралельні схеми операцій:
Кілька схем обчислюють свої операції одночасно:
Схема суматора/віднімача
Схема AND (побітова)
Схема OR (побітова)
Схема XOR (побітова)
Схема зсуву
2. Декодер операцій:
Перетворює код операції на сигнали вибору «один із багатьох»:
2-бітовий код операції → декодер 2-на-4 → 4 лінії вибору
3-бітовий код операції → декодер 3-на-8 → 8 ліній вибору
3. Вихідний мультиплексор:
Вибирає, результат якої операції з'являється на виході:
Всі операції обчислюються паралельно
Лише вибраний результат проходить далі
Вибір керується декодованим кодом операції
Розглянемо простий АЛП з 2-бітовим кодом операції, що підтримує чотири операції:Потік даних: A (N бітів) B (N бітів) | | v v +-------+-------+------+------+ | | | | | v v v v v [ADDER] [SUBTR] [AND] [OR] [тощо] | | | | v v v v +-------+-------+------+ | v [МУЛЬТИПЛЕКСОР 4-на-1] &lt;--- Вибір від декодера | v Результат (N бітів) Код операції (2 біти) ---&gt; [ДЕКОДЕР 2-на-4] ---&gt; Сигнали вибору
<br><img alt="8-бітові арифметичні компоненти" src="assets/l02-8bit-components.png" target="_self">Чотири базові арифметичні блоки: 8-бітовий СУМАТОР, ВІДНІМАЧ, ІНКРЕМЕНТОР та ДЕКРЕМЕНТОР. Кожен може бути реалізований за допомогою варіацій схеми суматора.Код операції є ключовим зв'язком між програмним та апаратним забезпеченням [1]:
Асемблерна інструкція ADD R1, R2, R3 асемблюється в машинний код
Машинний код включає біти коду операції (наприклад, «00» для ADD)
ЦП витягує інструкцію та виділяє код операції
Код операції надсилається на декодер АЛП
Декодер активує лінію вибору схеми ADD
Мультиплексор направляє вихід суматора на результат
Результат зберігається в регістрі призначення
Це відбувається за наносекунди — мільярди разів на секунду в сучасних процесорах.<br><img alt="Декодер інструкцій та АЛП" src="assets/l02-instruction-decoder-alu.png" target="_self">Повний тракт даних: регістр інструкцій подає на двійковий декодер, який вибирає відповідну операцію АЛП. Операнди проходять через АЛП для отримання результату.Повна операція АЛП відбувається за такою послідовністю [1]:
Вибірка операндів: Значення A і B надходять з регістрів або пам'яті
Паралельне обчислення: Всі схеми операцій обчислюють одночасно
Декодування: Код операції декодується в сигнали вибору
Вибір: Мультиплексор пропускає правильний результат
Генерація прапорців: Компаратори встановлюють прапорці стану на основі результату
Збереження результату: Вихід надходить до регістра призначення або пам'яті
Весь цей процес становить одну «операцію АЛП» і формує атомарну одиницю обчислень.Розглянемо подорож, яку ми здійснили через ці лекції [1]:На кожному рівні ми приховуємо складність нижнього рівня та представляємо простіший інтерфейс для верхнього. Програміст, який пише x = a + b, не повинен думати про поширення переносу чи перемикання транзисторів — рівні абстракції обробляють це.Цей принцип абстракції є, мабуть, найпотужнішою ідеєю в комп'ютерних науках. Він дозволяє нам будувати системи приголомшливої складності з простих, добре зрозумілих компонентів. АЛП прекрасно втілює це: мільярди транзисторів, організованих у мільйони вентилів, що реалізують тисячі операцій, — все це постає перед програмістом як прості арифметичні інструкції.Ця лекція провела нас шляхом від базових логічних вентилів до функціонального арифметико-логічного пристрою. Узагальнімо ключові концепції, які ми розглянули.Ми почали з встановлення фундаментальної відмінності між двома типами цифрових схем: Комбінаційна логіка: Вихід залежить лише від поточних входів. Ці схеми не мають пам'яті — при однакових входах вони завжди дають однакові виходи. Приклади: вентилі AND, OR, NOT, суматори та декодери. Послідовна логіка: Вихід залежить від поточних входів плюс збережений стан. Ці схеми мають пам'ять — вони можуть «запам'ятовувати» попередні значення. Приклади: засувки, тригери та регістри. Обидва типи є необхідними для обчислень. Комбінаційні схеми виконують розрахунки; послідовні схеми зберігають результати та підтримують стан з плином часу.Ми дослідили, як комп'ютери представляють числа, використовуючи лише два символи: Позиційна система з основою 2: Кожна позиція представляє степінь числа 2. Двійкове число 1011 дорівнює 1×8 + 0×4 + 1×2 + 1×1 = 11 у десятковій системі. Доповняльний код для знакових цілих чисел: Від'ємні числа представляються інвертуванням усіх бітів та додаванням 1. Ця елегантна система дозволяє використовувати однакове апаратне забезпечення додавання як для додатних, так і для від'ємних чисел. Апаратна ефективність: Та сама схема суматора обробляє знакову та беззнакову арифметику — інтерпретація залишається за програмістом. Ми виявили, що пам'ять виникає з простого принципу — подання виходу схеми назад на її вхід: SR-засувка: Найпростіший елемент пам'яті. Встановлення (S=1) зберігає 1; скидання (R=1) зберігає 0. Має заборонений стан (S=R=1), якого треба уникати. D-засувка: Спрощує інтерфейс, використовуючи єдиний вхід даних (D) та сигнал дозволу. Коли дозволено, D проходить наскрізь; коли заборонено, вихід зберігає попереднє значення. D-тригер: Керується фронтом, а не рівнем сигналу. Захоплює дані лише в точний момент фронту тактового сигналу, запобігаючи станам гонитви та забезпечуючи надійну синхронізацію. Регістри: Декілька тригерів із спільним тактовим сигналом, що дозволяє зберігати багатобітові значення, такі як байти або слова. Ми будували дедалі потужніше арифметичне апаратне забезпечення: Напівсуматор: Додає два одиничні біти, видаючи суму та перенос. Використовує один вентиль XOR (сума) та один вентиль AND (перенос). Повний суматор: Додає два біти плюс вхідний перенос, уможливлюючи ланцюгове з'єднання. Побудований з двох напівсуматорів та вентиля OR. Суматор з послідовним переносом: З'єднує в ланцюг декілька повних суматорів для додавання багатобітових чисел. Перенос «проходить хвилею» від молодшого біта до старшого. Віднімання через доповняльний код: Замість побудови окремого апаратного забезпечення для віднімання ми інвертуємо від'ємник, додаємо 1 і використовуємо той самий суматор. Ми завершили арифметико-логічним пристроєм (АЛП) — обчислювальним серцем ЦП (центрального процесора): Багатоопераційна конструкція: Поєднує арифметичні (ADD, SUB, INC, DEC) та логічні (AND, OR, XOR, NOT) операції в одній схемі. Вибір через код операції: Двійковий декодер інтерпретує код операції та активує відповідний функціональний блок. Мультиплексований вихід: Усі операції виконуються паралельно; мультиплексор обирає, який результат виводити на основі коду операції. Прапорці стану: АЛП видає прапорці (Zero, Negative, Carry, Overflow), які інформують ЦП про властивості результату. Розгляньмо, чого ми досягли за дві лекції:
Лекція 1: Від фізики до транзисторів до логічних вентилів
Лекція 2: Від вентилів до пам'яті до арифметики до АЛП
Тепер у нас є всі будівельні блоки, необхідні для повноцінного процесора: обчислення (АЛП), зберігання (регістри), і ми вже бачили натяки на керування (тактові сигнали, коди операцій). У наступних лекціях ми дослідимо, як ці компоненти поєднуються з пам'яттю та блоками керування, щоб утворити повноцінний комп'ютер, здатний виконувати програми.Перегляньте ці відео як обов'язкову підготовку перед відвідуванням лабораторної роботи 2:Ці відео надають візуальні пояснення та анімації, які значно полегшать виконання лабораторних вправ.Завдання в аудиторії:
Перевірити, що ваші напівсуматор, повний суматор та 4-бітовий суматор з лабораторної роботи 1 працюють коректно
Побудувати SR-засувку, використовуючи вентилі OR та AND зі зворотними зв'язками
Зрозуміти та протестувати заборонений стан
Домашні завдання:
D-засувка: Розширити SR-засувку логікою керування та входом даних
4-бітовий віднімач: Використати техніку доповняльного коду з вашим наявним суматором
4-бітовий інкремент: Побудувати схему, яка додає 1 до 4-бітового числа
4-бітовий декремент: Побудувати схему, яка віднімає 1 від 4-бітового числа
Двійковий декодер 2-на-4: Перетворити 2-бітовий вхід на один із чотирьох вихідних ліній
4-бітовий АЛП: Поєднати компоненти для підтримки операцій ADD, SUB, INC, DEC
Переконайтеся, що ви можете відповісти на ці питання перед приходом на лабораторну: Зворотний зв'язок і пам'ять: Як з'єднання виходу назад до входу створює здатність зберігати інформацію? Заборонений стан: Що відбувається, коли і S, і R дорівнюють 1 в SR-засувці, і чому це проблематично? Переваги D-засувки: Як єдиний вхід даних D-засувки запобігає забороненому стану? Керування фронтом проти рівня: Чому зберігання, кероване рівнем сигналу, спричиняє проблеми, і як керування фронтом їх вирішує? Робота декодера: Як двійковий декодер приймає n-бітовий вхід і активує рівно один із 2^n виходів? Вибір у АЛП: Як декодер і мультиплексор працюють разом, щоб код операції обирав, який результат операції з'являється на виході? <br>
[1] <a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=rM9BjciBLmg" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=rM9BjciBLmg" target="_self">How Transistors Remember Data</a> (YouTube) — Пояснює петлі зворотного зв'язку та як вони створюють пам'ять, побудова та робота SR-засувки та D-засувки <br>
[2] <a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=I0-izyq6q5s" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=I0-izyq6q5s" target="_self">How Do Computers Remember</a> (YouTube) — Охоплює конструкцію D-тригера, багатобітові регістри, синхронізацію тактовим сигналом та часові міркування <br>
[3] <a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=HjneAhCy2N4" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=HjneAhCy2N4" target="_self">How Transistors Run Code</a> (YouTube) — Демонструє схеми суматорів, конструкцію двійкового декодера, архітектуру АЛП та як коди операцій керують операціями <br>
[4] <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Flip-flop_(electronics)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Flip-flop_(electronics)" target="_self">Flip-flop (electronics)</a> — Повне охоплення типів засувок і тригерів, таблиць істинності, часових діаграм та практичних застосувань <br>
[5] <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Binary_number" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Binary_number" target="_self">Binary number</a> — Історія, теорія та практика двійкового представлення та арифметики <br>
[6] <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Two%27s_complement" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Two%27s_complement" target="_self">Two's complement</a> — Детальне пояснення представлення знакових чисел, методів перетворення та арифметичних властивостей <br>
[C] <a data-tooltip-position="top" aria-label="https://uk.wikipedia.org/wiki/%D0%9F%D1%80%D1%8F%D0%BC%D0%B8%D0%B9_%D0%BA%D0%BE%D0%B4" rel="noopener nofollow" class="external-link is-unresolved" href="https://uk.wikipedia.org/wiki/%D0%9F%D1%80%D1%8F%D0%BC%D0%B8%D0%B9_%D0%BA%D0%BE%D0%B4" target="_self">Sign-magnitude (Прямий код)</a> — Представлення прямим кодом (Вікіпедія UK) <br>
[D] <a data-tooltip-position="top" aria-label="https://uk.wikipedia.org/wiki/%D0%9E%D0%B1%D0%B5%D1%80%D0%BD%D0%B5%D0%BD%D0%B8%D0%B9_%D0%BA%D0%BE%D0%B4" rel="noopener nofollow" class="external-link is-unresolved" href="https://uk.wikipedia.org/wiki/%D0%9E%D0%B1%D0%B5%D1%80%D0%BD%D0%B5%D0%BD%D0%B8%D0%B9_%D0%BA%D0%BE%D0%B4" target="_self">One's complement (Обернений код)</a> — Представлення оберненим кодом (Вікіпедія UK) <br>
[7] <a data-tooltip-position="top" aria-label="https://uk.wikipedia.org/wiki/%D0%A2%D0%B5%D0%BE%D1%80%D1%96%D1%8F_%D0%B0%D0%B2%D1%82%D0%BE%D0%BC%D0%B0%D1%82%D1%96%D0%B2" rel="noopener nofollow" class="external-link is-unresolved" href="https://uk.wikipedia.org/wiki/%D0%A2%D0%B5%D0%BE%D1%80%D1%96%D1%8F_%D0%B0%D0%B2%D1%82%D0%BE%D0%BC%D0%B0%D1%82%D1%96%D0%B2" target="_self">Automata theory</a> — Формальні математичні основи поведінки послідовних схем та автоматів зі станами <br>
[8] <a data-tooltip-position="top" aria-label="https://www.jaroeducation.com/blog/difference-between-combinational-and-sequential-circuits/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.jaroeducation.com/blog/difference-between-combinational-and-sequential-circuits/" target="_self">Combinational vs Sequential Circuits</a> — Чітке порівняння двох фундаментальних категорій схем Ця лекція охоплює такі результати навчання курсу:Пояснювати принципи побудови та функціонування сучасних архітектур комп'ютерівПісля завершення цієї лекції студенти повинні вміти:
Розрізняти комбінаційну логіку (без пам'яті, вихід залежить лише від входів) та послідовну логіку (має пам'ять, вихід залежить від входів та збереженого стану)
Пояснювати, як петлі зворотного зв'язку в цифрових схемах створюють здатність зберігати інформацію
Описувати роботу та характеристики SR-засувки, D-засувки та D-тригера
Пояснювати призначення тактових сигналів та чому керування фронтом запобігає станам гонитви
Описувати, як регістри поєднують декілька тригерів для зберігання багатобітових значень даних
Пояснювати архітектуру АЛП та як він обирає серед декількох операцій
Пояснювати принципи представлення даних в комп'ютерних системахПісля завершення цієї лекції студенти повинні вміти:
Перетворювати числа між двійковою та десятковою системами числення в обох напрямках
Пояснювати представлення в доповняльному коді та чому воно спрощує апаратне забезпечення
Виконувати двійкове додавання та віднімання методом «олівець і папір»
Пояснювати, як та сама схема суматора обробляє і додавання, і віднімання через доповняльний код
Описувати, як прапорці стану (Zero, Negative, Carry, Overflow) кодують інформацію про результати арифметичних операцій
Інформація про курс:
Національний університет «Києво-Могилянська академія»
Факультет інформатики
Структура курсу:
Повний курс (КН, Інженерія програмного забезпечення): 6 ECTS (Європейська кредитно-трансферна система) / 180 годин
Скорочений курс (АКІТР): 4 ECTS / 120 годин
«Справжня проблема не в тому, чи думають машини, а в тому, чи думають люди.»
— Б. Ф. Скіннер]]></description><link>lectures/l02.-від-вентилів-до-алп.html</link><guid isPermaLink="false">Lectures/L02. Від вентилів до АЛП.md</guid><pubDate>Thu, 22 Jan 2026 19:03:27 GMT</pubDate><enclosure url="assets/l02-combinational-vs-sequential.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l02-combinational-vs-sequential.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Lab02]]></title><description><![CDATA[
Курс: Архітектура обчислювальних систем
Тиждень: 2
Тривалість: 2 академічні години
Тип: Практична робота під наглядом інструктора
Після завершення практики студент зможе:
Розуміти різницю між комбінаційною та секвенційною логікою.
Створювати базові елементи пам'яті: SR Latch, D Latch.
Будувати арифметичні компоненти: INC, DEC, SUB.
Використовувати Binary Decoder для вибору операцій.
Об'єднувати компоненти у 4-бітний ALU.
Перегляньте ці відео (обов'язково):Детальніше: <a data-tooltip-position="top" aria-label="https://www.jaroeducation.com/blog/difference-between-combinational-and-sequential-circuits/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.jaroeducation.com/blog/difference-between-combinational-and-sequential-circuits/" target="_self">Combinational vs Sequential Circuits</a>
Unsigned 4-bit: 0–15
Signed 4-bit (Two's complement): -8 до +7
Інвертування знаку: NOT(x) + 1
<br>Детальніше: <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Two%27s_complement" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Two%27s_complement" target="_self">Two's Complement</a>Впевніться, що у вас є наступні елементи із попередньої лабораторної HALF-ADDER
FULL-ADDER
4-BIT-ADDER
Побудуйте з OR та AND з feedback loop:
SET = 1 → запам'ятовує 1
RESET = 1 → запам'ятовує 0
Обидва = 1 → forbidden state!
Збережіть як SR-LATCH.Розширте SR Latch, додавши логіку для двох входів:
DATA — значення, яке хочемо записати
WRITE ENABLE — дозволяє запис (коли = 1)
Збережіть як D-LATCH.Побудуйте декодер:
2 входи (A, B) → 4 виходи (Y0, Y1, Y2, Y3)
Рівно один вихід активний (one-hot encoding)
Y0 = NOT(A) AND NOT(B), Y1 = NOT(A) AND B, ...
Збережіть як 2-TO-4-DECODER.Побудуйте інкремент (додавання 1):
Використайте ваш 4-BIT-ADDER
Другий операнд = 0001
Збережіть як 4-BIT-INC.До кінця заняття:
Перевірені: HALF-ADDER, FULL-ADDER, 4-BIT-ADDER (з Практики 1)
Побудовані та збережені: SR-LATCH, D-LATCH, 2-TO-4-DECODER, 4-BIT-INC
Продемонстровано роботу кожного компонента викладачу
Дедлайн: 23:59 у день перед Практикою 3Де знайти схеми? Усі необхідні схеми детально показані у відео з підготовки:
<br>D Latch, D Flip-Flop, регістри → <a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=rM9BjciBLmg" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=rM9BjciBLmg" target="_self">How Transistors Remember Data</a>, <a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=I0-izyq6q5s" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=I0-izyq6q5s" target="_self">How Do Computers Remember</a>
<br>Half/Full Adder, Binary Decoder, ALU → <a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=HjneAhCy2N4" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=HjneAhCy2N4" target="_self">How Transistors Run Code</a>
Побудуйте та збережіть:
4-bit SUB – віднімання через доповняльний код (NOT + 1 + ADD)
4-bit DEC – декремент (відняти 1)
4-bit ALU – об'єднайте компоненти у простий ALU: Операції: ADD, SUB, INC, DEC
Використайте Binary Decoder для вибору операції
2-бітний опкод визначає операцію Запишіть одне Loom-відео, у якому:
Показуєте структуру ключових компонентів – використовуйте "Open" у Digital Logic Sim, занурюючись у відповідні компоненти, щоб показати внутрішню будову (аж до базових вентилів).
Демонструєте роботу вашого 4-bit ALU (мінімум 2 різні операції).
Закомітьте посилання на відео у файл:
Lab02/video.txt
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Flip-flop_(electronics)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Flip-flop_(electronics)" target="_self">Flip-Flop (Wikipedia)</a>
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Binary_number" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Binary_number" target="_self">Binary Number (Wikipedia)</a>
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Two%27s_complement" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Two%27s_complement" target="_self">Two's Complement (Wikipedia)</a>
]]></description><link>labs/lab02.html</link><guid isPermaLink="false">Labs/Lab02.md</guid><pubDate>Wed, 21 Jan 2026 06:47:30 GMT</pubDate></item><item><title><![CDATA[L02-ENG. From Gates to ALU]]></title><description><![CDATA[Course: Computer Systems Architecture
Topic: Building Computation and Memory from Logic Gates
Learning Outcomes:
LO1: Explain the principles of construction and functioning of modern computer architectures
LO2: Explain principles of data representation in computer systems
In Lecture 1, we traced the path from physics to logic gates. We saw how transistors—acting as electrically controlled switches—combine to form the fundamental Boolean operations: NOT, AND, OR, NAND, and NOR. These gates form the alphabet of digital computation. Now we must learn to write words, sentences, and eventually entire programs with this alphabet.This lecture addresses a crucial question: how do we build circuits that can compute and remember? The answer requires understanding two fundamentally different types of digital circuits: combinational and sequential logic [1].Combinational circuits produce outputs that depend solely on their current inputs. Given the same input values, a combinational circuit will always produce the same output—immediately and without any notion of history. These circuits have no memory; they are pure functions in the mathematical sense.Examples of combinational circuits include:
Adders: Compute the sum of binary numbers
Decoders: Convert encoded inputs to a larger set of outputs
Multiplexers: Select one input from many based on control signals
Comparators: Determine relationships between numbers (greater, less, equal)
The behavior of any combinational circuit can be completely described by a truth table. For n inputs, the truth table has 2^n rows, each specifying the output for one input combination.Sequential circuits, by contrast, have outputs that depend on both current inputs and the history of previous inputs. They possess memory—some internal state that persists over time. This memory is what allows computers to execute programs step by step, maintaining intermediate results [2].Examples of sequential circuits include:
Flip-flops: Store a single bit of information
Registers: Store multiple bits (typically 8, 16, 32, or 64)
Counters: Cycle through a sequence of states
Finite state machines: Implement complex sequential behaviors
<img alt="Combinational vs Sequential Circuits" src="assets/l02-combinational-vs-sequential.png" target="_self">Comparison of combinational and sequential circuit structures. Combinational circuits have no feedback paths, while sequential circuits use feedback to maintain state.Sequential circuits typically require a clock signal—a periodic waveform that synchronizes state changes. This clock ensures that all parts of a circuit update their state simultaneously, preventing race conditions and timing hazards.In this lecture, we will build both types of circuits from our gate-level building blocks. We will construct:
Binary arithmetic circuits (combinational): half adders, full adders, ripple-carry adders
Memory elements (sequential): SR latches, D flip-flops, registers
The Arithmetic Logic Unit (ALU): combining arithmetic and logic operations
By the end, we will have all the components needed to understand how a CPU performs computation.Before diving into circuit implementations, it is valuable to understand the theoretical foundations that underpin digital systems. Automata theory provides a formal mathematical framework for understanding computation—one that directly maps to the circuits we build [3].An automaton (plural: automata) is an abstract mathematical model of a computing machine. It consists of:
A set of states the machine can occupy
An alphabet of input symbols
Transition rules that determine how inputs cause state changes
A starting state and (optionally) accepting states
Automata theory classifies computing machines by their power—what problems they can solve. This classification forms a hierarchy:<br><img alt="Automata Theory Hierarchy" src="assets/l02-automata-theory-hierarchy.png" target="_self">The hierarchy of automata: Turing machines can simulate any pushdown automaton, which can simulate any finite state machine, which contains combinational logic as a special case.Finite State Machines (FSMs) are directly relevant to digital circuit design. An FSM has a finite number of states and transitions between them based on inputs. This maps perfectly to sequential circuits built from flip-flops and gates [4].Consider a simple traffic light controller. It cycles through states (green → yellow → red → green) based on timer inputs. This is an FSM with three states and deterministic transitions—exactly what we implement with sequential logic circuits.FSMs come in two flavors:
Moore machines: Outputs depend only on the current state
Mealy machines: Outputs depend on current state AND current inputs
Every digital circuit with memory can be modeled as an FSM. This theoretical foundation tells us something profound: the sequential circuits we build are mathematically well-defined objects whose behavior can be formally analyzed and verified.Understanding automata theory helps us:
Recognize the capabilities and limitations of our circuits
Design state machines systematically rather than ad hoc
Verify that our implementations are correct
Understand where digital computers fit in the hierarchy of computational models
A computer built from finite state machines can simulate any Turing machine (given enough memory), which means it can compute anything that is computable. This is the theoretical foundation of general-purpose computing.To perform arithmetic, our circuits must operate on numbers. But transistors know nothing of the decimal system—they only understand two states: on and off, high and low, 1 and 0. This leads us to the binary number system [5].Both decimal and binary are positional number systems, where a digit's value depends on its position. In decimal (base 10), the number 347 means:3 × 10² + 4 × 10¹ + 7 × 10⁰ = 300 + 40 + 7 = 347
In binary (base 2), the same principle applies with powers of 2. The binary number 1101 means:1 × 2³ + 1 × 2² + 0 × 2¹ + 1 × 2⁰ = 8 + 4 + 0 + 1 = 13
Memorizing the first several powers of 2 is essential for working with digital systems:For quick reference, here are the binary representations of 0-15 (fitting in 4 bits):<br><img alt="Hexadecimal, Binary, and Decimal Conversion Table" src="assets/l02-hex-binary-decimal-table.png" target="_self">Complete conversion table showing hexadecimal (0-F), binary, and decimal equivalents. Hexadecimal is commonly used as a compact representation of binary values.Binary to Decimal: Multiply each bit by its positional power of 2 and sum.
Example: 10110₂ = 1×16 + 0×8 + 1×4 + 1×2 + 0×1 = 22₁₀
Decimal to Binary: Repeatedly divide by 2, recording remainders from bottom to top.
Example: Convert 22 to binary: 22 ÷ 2 = 11 remainder 0
11 ÷ 2 = 5 remainder 1
5 ÷ 2 = 2 remainder 1
2 ÷ 2 = 1 remainder 0
1 ÷ 2 = 0 remainder 1
Reading bottom-to-top: 10110₂ Binary addition follows simple rules:Notice: 1 + 1 = 10 in binary (decimal 2), just as 5 + 5 = 10 in decimal.The binary system's potential for computation was recognized by Gottfried Wilhelm Leibniz in 1703, who saw philosophical significance in reducing all numbers to combinations of 0 and 1 [6]. However, it was Claude Shannon's 1937 master's thesis that demonstrated how Boolean algebra could be implemented with electrical switches—laying the foundation for all digital computers.Binary is ideal for electronic implementation because:
Two stable states: Transistors naturally have two states (on/off, conducting/non-conducting)
Noise immunity: Distinguishing between two states is far more reliable than distinguishing ten
Boolean algebra: Mathematical operations map directly to logic gates
Simplicity: Fewer digit values mean simpler circuits for arithmetic
So far, we have considered only positive integers (and zero). But real computation requires negative numbers. How do we represent -5 or -128 using only 0s and 1s? [7]With n bits, we can represent 2^n distinct values. For 8 bits, that's 256 values. We must decide how to divide these between positive and negative numbers. Several schemes have been proposed, each with trade-offs.The most intuitive approach uses the leftmost bit as a sign indicator [C]:
0 = positive
1 = negative
In 8-bit sign-magnitude:
+5 = 0 0000101
-5 = 1 0000101
To negate: Simply flip the sign bit (MSB).Problems with sign-magnitude:
Two zeros: Both 00000000 (+0) and 10000000 (-0) represent zero
Complicated arithmetic: Addition requires checking signs and potentially subtracting
Asymmetric range: With 8 bits, range is -127 to +127 (wasting one code for duplicate zero)
In one's complement, negative numbers are formed by inverting (flipping) all bits [D]:
+5 = 00000101
-5 = 11111010 (each 0 becomes 1, each 1 becomes 0)
To negate: Invert all bits (apply NOT to each bit).This simplifies subtraction (just invert and add), but still has problems:
Two zeros: 00000000 (+0) and 11111111 (-0)
End-around carry: Addition requires special handling when carry wraps around
Two's complement is the universal standard for signed integer representation in modern computers. To negate a number:
Invert all bits (one's complement)
Add 1
Example: Converting +5 to -5 (8-bit)+5 = 00000101
Invert: 11111010
Add 1: 11111011 = -5 in two's complement
Verification: -5 + 5 should equal 0 11111011 (-5)
+ 00000101 (+5)
---------- 100000000
The 9th bit (carry out) is discarded, leaving 00000000 = 0. It works!Range: For n bits, the range is -2^(n-1) to +2^(n-1) - 1
8 bits: -128 to +127
16 bits: -32,768 to +32,767
32 bits: approximately -2.1 billion to +2.1 billion
Single zero: There is exactly one representation of zero (all bits = 0)Asymmetric range: There is one more negative number than positive (e.g., -128 exists but +128 does not in 8 bits)Key observations:
Positive numbers (0 to +7) have 0 as the most significant bit (MSB)
Negative numbers (-8 to -1) have 1 as the MSB
The MSB acts as a sign indicator, but not in the simple sign-magnitude way
<br><img alt="Two's Complement Wheel" src="assets/l02-twos-complement-wheel.png" target="_self" style="width: 500px; max-width: 100%;">Circular representation of two's complement. Moving clockwise adds 1; moving counter-clockwise subtracts 1. The transition from 0111 (+7) to 1000 (-8) demonstrates overflow.<br><img alt="Two's Complement NEG Operation" src="assets/l02-twos-complement-neg-example.png" target="_self">The NEG (negate) operation: invert all bits (NOT) then add 1. This converts positive numbers to negative and vice versa.Why did two's complement become the universal standard? Because the same addition circuit works for both signed and unsigned numbers [8].Consider adding -3 and +5 (expecting +2): 1101 (-3 in 4-bit two's complement)
+ 0101 (+5)
------ 10010
Discarding the carry-out bit: 0010 = +2. Correct!This means:
No separate subtraction circuit needed (subtract A = add -A)
No sign detection logic for basic operations
The ALU can treat numbers as unsigned or signed—the bits and operations are identical
One complexity: when does addition produce an invalid result (overflow)?Overflow occurs when adding two positive numbers yields a "negative" result, or adding two negative numbers yields a "positive" result. This happens when the result exceeds the representable range.Example (4-bit): 7 + 1 = ? 0111 (+7)
+ 0001 (+1)
------ 1000 (-8 in two's complement, not +8!)
The circuit must detect this condition and signal an overflow flag.Everything we have built so far—from simple gates to the mighty ALU—suffers from a fundamental limitation: they are combinational circuits. Their outputs depend entirely and immediately on their current inputs. Change the input, and the output changes. Remove the input, and the information vanishes.But computation requires more than instant calculation. Consider even the simplest task: adding a sequence of numbers. You need somewhere to store the running total while you fetch the next number. You need memory—the ability to retain information over time, independent of the current inputs.This is the memory problem: How do we make circuits that can remember? [1]The key insight comes from a surprisingly simple idea: feedback. What if we connected a gate's output back to its own input? This circular connection creates something remarkable—a circuit whose output depends not just on external inputs, but on its own previous state.Let's experiment with the simplest feedback loop. Take an OR gate and connect its output back to one of its inputs [1]. ┌─────────┐ A ──┤ │ │ OR ├──┬── Output ┌───┤ │ │ │ └─────────┘ │ └────────────────┘
<br><img alt="OR Gate with Feedback" src="assets/l02-or-gate-with-feedback.png" target="_self">An OR gate with its output fed back to one input. Once the output becomes 1, it stays 1 forever—the feedback creates a self-reinforcing loop.Consider what happens:
Initially, suppose the output is 0 (and input A is 0)
The OR gate sees inputs 0 and 0, so output stays 0
Now briefly pulse A to 1
OR gate sees 1 and 0, outputs 1
The output 1 feeds back to the input
Even when A returns to 0, the OR gate sees 0 and 1, so output stays 1!
We have created a circuit that remembers! Once the output becomes 1, it remains 1 indefinitely—the feedback creates a self-reinforcing loop that maintains the state.But this memory has a critical flaw: it can only remember "1". Once set, there is no way to reset it back to 0. The OR gate's truth table guarantees that if either input is 1, the output is 1. With the output permanently feeding back a 1, the circuit is stuck forever [1].Perhaps an AND gate would work better? Let's try the same feedback arrangement: ┌─────────┐ A ──┤ │ │ AND ├──┬── Output ┌───┤ │ │ │ └─────────┘ │ └────────────────┘
The behavior is exactly opposite:
If the output is ever 0, it stays 0 (AND requires both inputs to be 1)
Once the output locks to 0, no change to input A can rescue it
This is equally useless—a memory that can only remember "0" is no better than one that only remembers "1" [1].The breakthrough comes from combining both approaches. We need two operations:
SET: Force the memory to 1
RESET: Force the memory back to 0
The SR latch (Set-Reset latch) achieves this using two cross-coupled NOR gates [1]: ┌─────────┐ R ────┤ │ │ NOR ├──┬───────────────── Q ┌───┤ │ │ │ └─────────┘ │ ┌─────────┐ │ └───┤ │ │ │ NOR ├──┬── Q̄ (not Q) │ ┌───┤ │ │ │ │ └─────────┘ │ └────────────────┼────────────────┘ │ S ─────────────────┘
Each NOR gate's output feeds into the other gate's input, creating a stable feedback system with two complementary outputs: Q and Q̄.How it works:<br><img alt="SR Latch Circuit" src="assets/l02-sr-latch-set-reset.png" target="_self">SR Latch built from cross-coupled NOR gates. The SET input forces Q to 1; the RESET input forces Q to 0. When both inputs are 0, the latch holds its current state.When S=1 and R=0, the bottom NOR gate is forced to output 0 (since one input is 1). This 0 feeds to the top NOR gate, which now has inputs R=0 and 0, producing output 1. The circuit settles with Q=1.When S=0 and R=1, the opposite occurs: the top NOR gate outputs 0, which propagates to set Q̄=1 and Q=0.When both S and R are 0, neither gate is being forced, so the circuit maintains whatever state it was in—true memory!The Forbidden State Problem:When S=1 and R=1 simultaneously, both NOR gates are forced to output 0. This violates the fundamental requirement that Q and Q̄ be complements. Worse, when both inputs return to 0, the circuit enters a race condition—the final state depends on which gate responds faster, which is determined by microscopic manufacturing variations. The result is unpredictable [1].<br><img alt="SR Latch Invalid State" src="assets/l02-sr-latch-invalid-state.png" target="_self">The forbidden state: when both SET and RESET are 1, both outputs become 0. The warning symbol indicates this is an invalid and unpredictable configuration.The Power-On Problem:When power is first applied, what state does the SR latch assume? The answer is: it depends on which gate's transistors activate faster. The initial state is essentially random, determined by tiny manufacturing differences. This is why computers need explicit initialization sequences at startup [1].The SR latch works, but it has an awkward interface. Two separate wires control setting and resetting, and the user must ensure they're never both active simultaneously. This is error-prone and complicates circuit design.What we really want is simpler:
One wire for the DATA value we want to store (0 or 1)
One wire to say ENABLE—"capture the data now!"
This is exactly what the D latch (Data latch) provides [1].The D latch wraps the SR latch with additional logic that makes it impossible to enter the forbidden state: ┌─────────┐ ┌─────┤ │ │ │ AND ├────── R Enable ─────┼──┬──┤ │ ┌───────┐ │ │ └─────────┘ │ │ │ │ ┌───┤ SR ├─── Q │ │ ┌─────────┐ │ │ Latch │ Data ─────┼──┼──┤ NOT ├──┐ │ ┌─┤ ├─── Q̄ │ │ └─────────┘ │ │ │ └───────┘ │ │ │ │ │ │ │ ┌─────────┐ │ │ │ │ └──┤ │ │ │ │ │ │ AND ├──┼──────────┘ │ └─────┤ │ │ └─────────┘ │ S ──────────────────┘
The key insight: Data and NOT(Data) can never both be 1. By ANDing each with Enable, we create Set and Reset signals that are mutually exclusive by construction [1].<br><img alt="D Latch with Write Enable" src="assets/l02-latch-write-enable.png" target="_self">D Latch interface: a single Data input and a Write Enable (or Enable) control. This simplified interface prevents the forbidden state by construction.<br><img alt="D Latch NAND Implementation" src="assets/l02-d-latch-nand-circuit.png" target="_self">D Latch built from NAND gates. The D (Data) input is gated by E (Enable). When enabled, the latch captures D; when disabled, it holds the previous value.When Enable = 0:
Both AND gates output 0 (regardless of Data)
S=0 and R=0, so the SR latch holds its current value
The Data input is completely ignored
When Enable = 1:
If Data=1: S=1, R=0 → latch sets to 1
If Data=0: S=0, R=1 → latch resets to 0
The latch captures whatever value is on the Data line
<br><img alt="NandGame D Latch Implementation" src="assets/l02-nandgame-d-latch-circuit.png" target="_self">D Latch implementation from the NandGame educational simulator. The circuit uses AND gates, a NOT gate, and NOR gates in the SR latch core.The D latch elegantly solves the forbidden state problem—the circuit structure makes S=R=1 impossible. It provides exactly the interface we wanted: a Data input and an Enable control [1].The D latch has one significant limitation that becomes critical in larger systems: it is level-triggered (also called transparent).While Enable=1, the output Q continuously follows the Data input. The latch isn't really "latching" during this time—it's transparently passing data through. Only when Enable returns to 0 does the last value get truly captured and held [2].For simple applications, this works fine. But consider what happens in a circuit with feedback—where a register's output feeds back to its input through some logic. We'll explore this problem in the next section, and it will motivate a crucial improvement to our memory design.A single D latch stores one bit. To store larger values—bytes, words, addresses—we simply use multiple latches with a shared Enable signal [1]: Data[0] ──┬── D Latch ──── Q[0] │ Data[1] ──┼── D Latch ──── Q[1] │ Data[2] ──┼── D Latch ──── Q[2] │ Data[3] ──┼── D Latch ──── Q[3] │ Enable ─────────┴── (shared)
This creates a 4-bit register. All four latches capture their inputs simultaneously when Enable goes high. The concept scales trivially: 8 latches for a byte, 32 for a word, 64 for modern processor registers.But there's a serious problem lurking in this design.Consider a fundamental computing operation: incrementing a counter. We have a register that holds a value, an adder that computes value+1, and the result feeds back into the same register [2]: ┌─────────────────────────────────────┐ │ │ │ ┌──────────┐ ┌──────────┐ │ └────┤ Register ├────┤ Adder ├─────┘ │ (D │ │ (+1) │ │ Latches)│ └──────────┘ └────┬─────┘ │ Enable (Store)
With D latches, watch what happens when we assert Enable to store the incremented value:
Register holds 3, adder outputs 4
Enable goes HIGH
Register captures 4, now outputs 4
Adder now computes 4+1=5
Register captures 5 (Enable is still high!)
Adder computes 5+1=6
Register captures 6...
Runaway! The value races upward uncontrollably [2]
The problem is fundamental: while the D latch is transparent (Enable=1), it continuously passes input to output. In any circuit with feedback, this creates an unstable loop. The register's value depends on how long Enable stays high—completely unpredictable and unusable.The solution is to change when data is captured. Instead of capturing throughout a high Enable period, we need to capture at one precise instant [2].Enter the clock signal: a regular, periodic waveform that oscillates between 0 and 1: ┌───┐ ┌───┐ ┌───┐ ┌───┐
Clock │ │ │ │ │ │ │ │ ──┘ └───┘ └───┘ └───┘ └── ↑ ↑ ↑ ↑ Rising Rising Rising Rising edges edges edges edges
<br><img alt="Real Clock Signal Waveform" src="assets/l02-clock-signal-waveform.png" target="_self">A real clock signal showing rise time, fall time, and slight overshoot. The idealized square wave has non-instantaneous transitions in physical circuits.The key insight: instead of triggering on a level (clock being high or low), we trigger on an edge—the transition moment:
Rising edge: the instant clock transitions from 0 to 1
Falling edge: the instant clock transitions from 1 to 0
An edge is theoretically instantaneous—an infinitesimally brief moment in time. During that instant:
Data is sampled and captured
The capture window is so brief that feedback cannot propagate through
With edge triggering, our counter works correctly:
Register holds 3, adder outputs 4
Rising edge occurs—register captures exactly 4
Register now outputs 4, adder outputs 5
Nothing changes until the NEXT rising edge
Next rising edge—register captures exactly 5
Predictable, controlled, correct [2]
The clock edge acts like a photographer's shutter—capturing a snapshot of the data at one precise moment, immune to any changes that happen before or after.The terminology can be confusing, so let's be precise:
D Latch: Level-triggered. Transparent while Enable is high.
D Flip-Flop: Edge-triggered. Captures only at clock edge.
The D flip-flop is the workhorse of digital memory. It solves the racing problem that plagues D latches in feedback circuits [2][3].The most elegant way to build an edge-triggered flip-flop is from two D latches in a master-slave configuration [3]: ┌─────────────────────────────────────────┐ │ │ Data ─────┼──┬── D Latch ──────┬── D Latch ────┬────┼─── Q │ │ (Master) │ (Slave) │ │ │ │ │ │ │ Clock ────┼──┴── Enable └── Enable ─────┘ │ │ │ │ │ │ │ ┌───────┐ │ │ │ └────┤ NOT ├─────┘ │ │ └───────┘ │ └─────────────────────────────────────────┘
The critical insight: the two latches have inverted enable signals:
Master is enabled when Clock = 0 (LOW)
Slave is enabled when Clock = 1 (HIGH)
They are never both transparent at the same time [2][3].<br><img alt="D Flip-Flop from Two D Latches" src="assets/l02-nandgame-d-flipflop.png" target="_self">D Flip-Flop built from two D Latches in master-slave configuration. The master latch is enabled when clock is LOW; the slave latch is enabled when clock is HIGH.Follow the operation through one clock cycle:Phase 1: Clock is LOW
Master latch is transparent—it follows the Data input
Slave latch is opaque—it holds its previous value, ignoring Master's output
External output Q remains stable
Phase 2: Clock goes HIGH (rising edge)
Master latch becomes opaque—it freezes, holding whatever value it had
Slave latch becomes transparent—it captures Master's frozen value
External output Q updates to the captured value
Phase 3: Clock remains HIGH
Master is still opaque—changes to Data are ignored
Slave is transparent but Master isn't changing, so Q is stable
Phase 4: Clock goes LOW (falling edge)
Slave becomes opaque—output Q is locked
Master becomes transparent—starts following Data again
But Slave ignores Master, so Q doesn't change
The net effect: Q only changes at the rising edge moment, capturing whatever Data was at that instant [2][3].The "X" means "don't care"—the input value doesn't affect the output except at the triggering edge.<br><img alt="D Flip-Flop with Store Control" src="assets/l02-nandgame-d-flipflop-store.png" target="_self">D Flip-Flop with DATA, STORE, and CLOCK inputs. The STORE signal enables selective updating—only when STORE is active does the flip-flop capture new data at the clock edge.The D flip-flop is arguably the most important building block in digital systems after the basic logic gates. It provides:
Predictable timing: State changes only at clock edges
Race immunity: Feedback circuits work correctly
Synchronization: All flip-flops sharing a clock update together
State isolation: Each clock cycle is independent
Every register, every memory cell, every sequential circuit in modern computers ultimately relies on this edge-triggered behavior. The D flip-flop transforms chaotic asynchronous signals into orderly, synchronized digital computation [2][3].A single D flip-flop stores one bit. Combining multiple flip-flops with a shared clock creates a register—a unit that stores multiple bits simultaneously [1][2]: ┌─────────┐ Data[0] ─────────┤ D Q ├─────── Q[0] │ FF │ Clock ───────────┤&gt; │ └─────────┘ ┌─────────┐ Data[1] ─────────┤ D Q ├─────── Q[1] │ FF │ Clock ───────────┤&gt; │ └─────────┘ ┌─────────┐ Data[2] ─────────┤ D Q ├─────── Q[2] │ FF │ Clock ───────────┤&gt; │ └─────────┘ ... ... ...
Common register sizes:
8 flip-flops = 8-bit register (1 byte)
16 flip-flops = 16-bit register (half-word)
32 flip-flops = 32-bit register (word, common in older CPUs)
64 flip-flops = 64-bit register (double-word, modern CPUs)
Left: A 1-bit register with DATA, STORE, and CLOCK inputs. Right: A 4-bit register built from four 1-bit registers sharing a common clock and store signal.<br><img alt="Byte Storage: 8 Bits" src="assets/l02-byte-8-bits.png" target="_self">A byte (8 bits) stored in 8 latches. Each latch holds one bit of the value, allowing storage of numbers from 0 to 255 (unsigned) or -128 to +127 (signed).At each clock edge, all bits update simultaneously, capturing a complete multi-bit value atomically.Sometimes we want a register to hold its value even as the clock continues ticking. Not every clock cycle should update every register [2].The solution adds a multiplexer before each flip-flop: ┌─────────┐ Data ────────────┬────┤ 0 │ ┌─────────┐ │ │ MUX ├────┤ D Q ├──┬── Q ┌─────┼────┤ 1 │ │ FF │ │ │ │ └────┬────┘ │ │ │ │ │ │ └────┬────┘ │ │ │ Store │ │ │ │ Clock │ └─────┼────────────────────────────────┘ │ (feedback)
Operation:
When Store = 0: MUX selects the feedback path; flip-flop recaptures its own output; value holds
When Store = 1: MUX selects external Data; flip-flop captures new value
This allows selective updating: only registers with Store=1 change at each clock edge. All others maintain their values indefinitely [2].Now that we understand how to build logic gates and memory elements, we can tackle one of the most fundamental operations in computing: arithmetic. Every calculation your computer performs—from simple addition to complex graphics rendering—ultimately breaks down into binary arithmetic operations performed by specialized circuits.Binary addition follows the same principles as decimal addition, but with only two digits (0 and 1). The basic rules are remarkably simple:The last case is crucial: when we add 1 + 1, we get 2, which in binary is written as "10" (one-zero). This means we write down 0 and carry 1 to the next position—exactly like carrying in decimal arithmetic when we add 7 + 5 = 12 [1].Consider adding 5 + 3 in binary: 101 (5 in binary) + 011 (3 in binary) ----- 1000 (8 in binary)
The carries propagate from right to left, just as in decimal addition.The half adder is the simplest arithmetic circuit, designed to add two single-bit numbers [1]. It has:Inputs:
A: first bit to add
B: second bit to add
Outputs:
Sum: the result bit (without carry)
Carry: the carry-out bit
Looking at the addition rules, we can determine the logic:
Sum equals 1 when the inputs are different (one is 0, one is 1)
Carry equals 1 when both inputs are 1
This maps directly to gates we already know:
Sum = A XOR B (XOR outputs 1 when inputs differ)
Carry = A AND B (AND outputs 1 when both inputs are 1)
Half Adder Truth Table:Half Adder Circuit (ASCII representation):A ----+---[XOR]---- Sum | |
B ----+-----+ | | +---[AND]---- Carry
<br><img alt="Half Adder Circuit" src="assets/l02-tc-half-adder.png" target="_self">Half Adder from the Turing Complete game. An XOR gate produces the Sum output; an AND gate produces the Carry output.The half adder works perfectly for adding two bits, but it has a critical limitation: it cannot accept a carry-in from a previous addition. When adding multi-bit numbers, each position (except the rightmost) must handle a potential carry from the position to its right.The full adder solves the carry-in problem by accepting three inputs instead of two [1]:Inputs:
A: first bit to add
B: second bit to add
Cin (Carry-in): carry from the previous position
Outputs:
Sum: the result bit
Cout (Carry-out): carry to the next position
A full adder is constructed from two half adders and an OR gate:
First half adder: Adds A and B, producing an intermediate sum and intermediate carry
Second half adder: Adds the intermediate sum and Cin, producing the final Sum and a second carry
OR gate: Combines both intermediate carries to produce Cout
Full Adder Truth Table:The logic behind Cout: a carry is produced if at least two of the three inputs are 1.<br><img alt="Full Adder Circuit" src="assets/l02-tc-full-adder.png" target="_self">Full Adder from the Turing Complete game. Built from two Half Adders and an OR gate. The carry-in (Cin) allows chaining multiple adders together.To add multi-bit numbers, we chain full adders together to create a ripple carry adder [1]. For an N-bit adder:
Connect N full adders in sequence
The Carry-out of position i connects to the Carry-in of position i+1
The first adder's Cin is typically set to 0
Each adder handles one bit position of the operands
4-Bit Ripple Carry Adder Structure:A3 B3 A2 B2 A1 B1 A0 B0 | | | | | | | | v v v v v v v v
[FA-3]&lt;---[FA-2]&lt;---[FA-1]&lt;---[FA-0]&lt;--- Cin=0 | | | | v v v v S3 S2 S1 S0 Cout
The term "ripple" describes how the carry must propagate through each adder sequentially. This creates a timing issue: the carry from position 0 must reach position N-1 before the final result is valid. For a 64-bit adder, this delay can be significant [1].<br><img alt="4-Bit Ripple Carry Adder" src="assets/l02-tc-4bit-adder.png" target="_self">4-bit ripple carry adder from the Turing Complete game. Four Full Adders chained together, with each Cout connected to the next Cin.Modern processors use more sophisticated designs like carry-lookahead adders that predict carries in parallel, dramatically reducing delay.One elegant feature of two's complement representation is that subtraction becomes addition [1]. To compute A - B:
Compute the two's complement of B (invert all bits, then add 1)
Add: A + (-B)
The hardware implementation is remarkably efficient:
XOR gates conditionally invert B: when the control signal is 1, each bit of B passes through XOR with 1, which inverts it (since X XOR 1 = NOT X)
Set Cin = 1 to add the "+1" required for two's complement
The same adder circuit performs both addition and subtraction
This dual-use design means a single adder/subtractor unit handles both operations, selected by a single control bit.Overflow occurs when an arithmetic result cannot be represented in the available bits [1].Unsigned overflow:
Detected by examining the final carry-out
If Cout = 1, the result exceeded the maximum unsigned value
Signed overflow:
More subtle: occurs when the sign of the result is incorrect
Adding two positive numbers should not yield a negative result
Adding two negative numbers should not yield a positive result
Detected by comparing the carry into the MSB with the carry out of the MSB
Processors set flag bits to indicate overflow conditions, allowing software to detect and handle arithmetic errors.Decoders are essential building blocks that convert binary codes into individual selection signals. They appear throughout digital systems, from memory addressing to instruction execution.A decoder takes an N-bit binary input and activates exactly one of 2^N output lines [1]. This is called a "one-hot" encoding because only one output is "hot" (active/high) at any time.Common decoder configurations:
2-to-4 decoder: 2 input bits select one of 4 outputs
3-to-8 decoder: 3 input bits select one of 8 outputs
4-to-16 decoder: 4 input bits select one of 16 outputs
The decoder essentially "translates" a binary number into a physical selection—pointing to one specific item among many possibilities.Notice that the binary input (A1 A0) directly indicates which output is active: input "00" activates Y0, input "01" activates Y1, and so on.<br><img alt="Binary Decoder Example" src="assets/l02-binary-decoder-example-2.png" target="_self">A 2-to-4 decoder with input "10" (decimal 2). Only Output_2 is active (shown in green), demonstrating the one-hot encoding principle.Each decoder output is an AND of the appropriate combination of inputs and their complements [1]:Y0 = NOT(A1) AND NOT(A0) (active when A1=0, A0=0)
Y1 = NOT(A1) AND A0 (active when A1=0, A0=1)
Y2 = A1 AND NOT(A0) (active when A1=1, A0=0)
Y3 = A1 AND A0 (active when A1=1, A0=1)
2-to-4 Decoder Circuit (ASCII representation):A1 ----+----[NOT]---+---[AND]--- Y0 (when 00) | | |
A0 ----+---[NOT]----+-----+ | | | | +-----+---[AND]--- Y1 (when 01) | | | +------------+-----+ | | +---[NOT]----+---[AND]--- Y2 (when 10) | | | +------------+---[AND]--- Y3 (when 11)
<br><img alt="Binary Decoder Circuit" src="assets/l02-binary-decoder-circuit.png" target="_self">2-to-4 binary decoder circuit. NOT gates create complements of inputs; AND gates combine inputs and complements to produce one-hot outputs.Decoders appear in numerous contexts within digital systems [1]:In an ALU, the decoder serves as the bridge between the op-code (the instruction's operation field) and the actual hardware that performs the operation [1]:
The CPU extracts op-code bits from the instruction
These bits enter the decoder
The decoder activates exactly one output line
That line enables the corresponding operation circuit
The selected circuit's result becomes the ALU output
This mechanism is how abstract software instructions ("add these numbers") become concrete hardware actions (activating the adder circuit).The Arithmetic Logic Unit (ALU) represents the culmination of everything we have built: transistors form gates, gates form adders and decoders, and these combine into a single, versatile computation engine at the heart of every processor.The ALU is the "calculator" inside every CPU [1]. It performs two categories of operations:Arithmetic operations:
Addition
Subtraction
Multiplication (often in a separate unit)
Division (often in a separate unit)
Increment/Decrement
Logic operations:
AND (bitwise)
OR (bitwise)
XOR (bitwise)
NOT (bitwise)
Shift operations
Every computation a computer performs—whether rendering video, encrypting data, or running a spreadsheet—ultimately breaks down into sequences of these fundamental ALU operations.A typical ALU receives three categories of inputs [1]:For example, a 32-bit ALU with a 4-bit op-code can perform up to 16 different operations on 32-bit numbers.The ALU produces both a result and status information [1]:Primary output:
Result: The computed value (same width as operands)
Flag/Status outputs:
Zero flag (Z): Set to 1 if the result equals zero
Negative flag (N): Set to 1 if the result is negative (MSB = 1 in signed interpretation)
Carry flag (C): Set to 1 if unsigned overflow occurred
Overflow flag (V): Set to 1 if signed overflow occurred
These flags are critical for conditional operations. For example, after a subtraction (comparison), the Zero flag indicates equality, while the Negative and Overflow flags together indicate greater-than or less-than relationships.<br><img alt="ALU Block Diagram" src="assets/l02-alu-block-diagram.png" target="_self">ALU block diagram showing the three categories of inputs (operands A and B, op-code) and two categories of outputs (result and status flags).A basic ALU consists of three main components working together [1]:1. Parallel Operation Circuits:
Multiple circuits compute their operations simultaneously:
Adder/Subtractor circuit
AND circuit (bitwise)
OR circuit (bitwise)
XOR circuit (bitwise)
Shifter circuit
2. Operation Decoder:
Converts the op-code into one-hot selection signals:
2-bit op-code → 2-to-4 decoder → 4 selection lines
3-bit op-code → 3-to-8 decoder → 8 selection lines
3. Output Multiplexer:
Selects which operation's result appears at the output:
All operations compute in parallel
Only the selected result passes through
Selection controlled by the decoded op-code
Consider a simple ALU with a 2-bit op-code supporting four operations:Data flow: A (N bits) B (N bits) | | v v +-------+-------+------+------+ | | | | | v v v v v [ADDER] [SUBTR] [AND] [OR] [etc.] | | | | v v v v +-------+-------+------+ | v [4-to-1 MULTIPLEXER] &lt;--- Selection from Decoder | v Result (N bits) Op-code (2 bits) ---&gt; [2-to-4 DECODER] ---&gt; Selection signals
<br><img alt="8-bit Arithmetic Components" src="assets/l02-8bit-components.png" target="_self">The four basic arithmetic units: 8-bit ADDER, SUBTRACTER, INCREMENTER, and DECREMENTER. Each can be implemented using variations of the adder circuit.The op-code is the crucial link between software and hardware [1]:
Assembly instruction ADD R1, R2, R3 is assembled into machine code
Machine code includes op-code bits (e.g., "00" for ADD)
CPU fetches the instruction and extracts the op-code
Op-code is sent to the ALU's decoder
Decoder activates the ADD circuit's selection line
Multiplexer routes the adder's output to the result
Result is stored in the destination register
This happens in nanoseconds—billions of times per second in modern processors.<br><img alt="Instruction Decoder and ALU" src="assets/l02-instruction-decoder-alu.png" target="_self">Complete datapath: the instruction register feeds the binary decoder, which selects the appropriate ALU operation. Operands flow through the ALU to produce the result.A complete ALU operation follows this sequence [1]:
Operand fetch: Values A and B arrive from registers or memory
Parallel computation: All operation circuits compute simultaneously
Decoding: Op-code is decoded into selection signals
Selection: Multiplexer passes the correct result
Flag generation: Comparators set status flags based on result
Result storage: Output goes to a destination register or memory
This entire process constitutes one "ALU operation" and forms the atomic unit of computation.Consider the journey we have taken through these lectures [1]:At each level, we hide the complexity below and present a simpler interface above. A programmer writing x = a + b does not need to think about carry propagation or transistor switching—the abstraction layers handle it.This principle of abstraction is perhaps the most powerful idea in computer science. It allows us to build systems of staggering complexity from simple, well-understood components. The ALU embodies this beautifully: billions of transistors, organized into millions of gates, implementing thousands of operations, all appearing to the programmer as simple arithmetic instructions.From switches to software—this is how computers work.This lecture took us on a journey from basic logic gates to a functional Arithmetic Logic Unit. Let's consolidate the key concepts we covered.We began by establishing the fundamental distinction between two types of digital circuits: Combinational logic: Output depends only on current inputs. These circuits have no memory — given the same inputs, they always produce the same outputs. Examples include AND, OR, NOT gates, adders, and decoders. Sequential logic: Output depends on current inputs plus stored state. These circuits have memory — they can "remember" previous values. Examples include latches, flip-flops, and registers. Both types are essential for computation. Combinational circuits perform calculations; sequential circuits store results and maintain state over time.We explored how computers represent numbers using only two symbols: Base-2 positional notation: Each position represents a power of 2. The binary number 1011 equals 1×8 + 0×4 + 1×2 + 1×1 = 11 in decimal. Two's complement for signed integers: Negative numbers are represented by inverting all bits and adding 1. This elegant system allows the same addition hardware to work for both positive and negative numbers. Hardware efficiency: The same adder circuit handles signed and unsigned arithmetic — the interpretation is up to the programmer. We discovered that memory emerges from a simple principle — feeding a circuit's output back to its input: SR Latch: The simplest memory element. Set (S=1) stores a 1; Reset (R=1) stores a 0. Has a forbidden state (S=R=1) that must be avoided. D Latch: Simplifies the interface by using a single data input (D) and an enable signal. When enabled, D passes through; when disabled, the output holds its previous value. D Flip-Flop: Edge-triggered rather than level-triggered. Captures data only at the precise moment of a clock edge, preventing racing conditions and enabling reliable synchronization. Registers: Multiple flip-flops sharing a common clock signal, allowing storage of multi-bit values like bytes or words. We built progressively more capable arithmetic hardware: Half Adder: Adds two single bits, producing a sum and carry. Uses one XOR gate (sum) and one AND gate (carry). Full Adder: Adds two bits plus an incoming carry, enabling chain connections. Built from two half adders and an OR gate. Ripple Carry Adder: Chains multiple full adders to add multi-bit numbers. The carry "ripples" from the least significant bit to the most significant bit. Subtraction via two's complement: Rather than building separate subtraction hardware, we invert the subtrahend, add 1, and use the same adder. We culminated with the Arithmetic Logic Unit — the computational heart of the CPU: Multi-operation design: Combines arithmetic (ADD, SUB, INC, DEC) and logic (AND, OR, XOR, NOT) operations in one circuit. Op-code selection: A binary decoder interprets the operation code and activates the appropriate functional unit. Multiplexed output: All operations run in parallel; a multiplexer selects which result to output based on the op-code. Status flags: The ALU produces flags (Zero, Negative, Carry, Overflow) that inform the CPU about the result's properties. Consider what we've accomplished across two lectures:
Lecture 1: From physics to transistors to logic gates
Lecture 2: From gates to memory to arithmetic to the ALU
We now have all the building blocks needed for a complete processor: computation (ALU), storage (registers), and we've seen hints of control (clock signals, op-codes). In upcoming lectures, we'll explore how these components combine with memory and control units to form a complete computer that can execute programs.Watch these videos as mandatory preparation before attending Lab 2:These videos provide visual explanations and animations that will make the lab exercises much more approachable.In-class activities:
Verify that your Half Adder, Full Adder, and 4-bit Adder from Lab 1 work correctly
Build an SR Latch using OR and AND gates with feedback connections
Understand and test the forbidden state
Homework assignments:
D Latch: Extend the SR Latch with control logic and a data input
4-bit Subtractor: Use two's complement technique with your existing adder
4-bit Increment: Build a circuit that adds 1 to a 4-bit number
4-bit Decrement: Build a circuit that subtracts 1 from a 4-bit number
2-to-4 Binary Decoder: Convert a 2-bit input into one of four output lines
4-bit ALU: Combine components to support ADD, SUB, INC, DEC operations
Make sure you can answer these questions before coming to lab: Feedback and memory: How does connecting an output back to an input create the ability to store information? Forbidden state: What happens when both S and R are 1 in an SR Latch, and why is this problematic? D Latch advantages: How does the D Latch's single data input prevent the forbidden state? Edge vs level triggering: Why does level-triggered storage cause problems, and how does edge-triggering solve them? Decoder operation: How does a binary decoder take an n-bit input and activate exactly one of 2^n outputs? ALU selection: How do the decoder and multiplexer work together to let the op-code choose which operation's result appears at the output? <br>
[1] <a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=rM9BjciBLmg" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=rM9BjciBLmg" target="_self">How Transistors Remember Data</a> (YouTube) — Explains feedback loops and how they create memory, SR Latch and D Latch construction and operation <br>
[2] <a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=I0-izyq6q5s" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=I0-izyq6q5s" target="_self">How Do Computers Remember</a> (YouTube) — Covers D Flip-Flop design, multi-bit registers, clock signal synchronization, and timing considerations <br>
[3] <a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=HjneAhCy2N4" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=HjneAhCy2N4" target="_self">How Transistors Run Code</a> (YouTube) — Demonstrates adder circuits, binary decoder design, ALU architecture, and how op-codes control operations <br>
[4] <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Flip-flop_(electronics)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Flip-flop_(electronics)" target="_self">Flip-flop (electronics)</a> — Comprehensive coverage of latch and flip-flop types, truth tables, timing diagrams, and practical applications <br>
[5] <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Binary_number" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Binary_number" target="_self">Binary number</a> — History, theory, and practice of binary representation and arithmetic <br>
[6] <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Two%27s_complement" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Two%27s_complement" target="_self">Two's complement</a> — Detailed explanation of signed number representation, conversion methods, and arithmetic properties <br>
[C] <a data-tooltip-position="top" aria-label="https://uk.wikipedia.org/wiki/%D0%9F%D1%80%D1%8F%D0%BC%D0%B8%D0%B9_%D0%BA%D0%BE%D0%B4" rel="noopener nofollow" class="external-link is-unresolved" href="https://uk.wikipedia.org/wiki/%D0%9F%D1%80%D1%8F%D0%BC%D0%B8%D0%B9_%D0%BA%D0%BE%D0%B4" target="_self">Sign-magnitude (Прямий код)</a> — Sign-magnitude representation (Wikipedia UK) <br>
[D] <a data-tooltip-position="top" aria-label="https://uk.wikipedia.org/wiki/%D0%9E%D0%B1%D0%B5%D1%80%D0%BD%D0%B5%D0%BD%D0%B8%D0%B9_%D0%BA%D0%BE%D0%B4" rel="noopener nofollow" class="external-link is-unresolved" href="https://uk.wikipedia.org/wiki/%D0%9E%D0%B1%D0%B5%D1%80%D0%BD%D0%B5%D0%BD%D0%B8%D0%B9_%D0%BA%D0%BE%D0%B4" target="_self">One's complement (Обернений код)</a> — One's complement representation (Wikipedia UK) <br>
[7] <a data-tooltip-position="top" aria-label="https://uk.wikipedia.org/wiki/%D0%A2%D0%B5%D0%BE%D1%80%D1%96%D1%8F_%D0%B0%D0%B2%D1%82%D0%BE%D0%BC%D0%B0%D1%82%D1%96%D0%B2" rel="noopener nofollow" class="external-link is-unresolved" href="https://uk.wikipedia.org/wiki/%D0%A2%D0%B5%D0%BE%D1%80%D1%96%D1%8F_%D0%B0%D0%B2%D1%82%D0%BE%D0%BC%D0%B0%D1%82%D1%96%D0%B2" target="_self">Automata theory</a> — Formal mathematical foundations underlying sequential circuit behavior and state machines <br>
[8] <a data-tooltip-position="top" aria-label="https://www.jaroeducation.com/blog/difference-between-combinational-and-sequential-circuits/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.jaroeducation.com/blog/difference-between-combinational-and-sequential-circuits/" target="_self">Combinational vs Sequential Circuits</a> — Clear comparison of the two fundamental circuit categories This lecture addresses the following course learning outcomes:Explain the principles of construction and functioning of modern computer architecturesAfter completing this lecture, students should be able to:
Distinguish between combinational logic (no memory, output depends only on inputs) and sequential logic (has memory, output depends on inputs and stored state)
Explain how feedback loops in digital circuits create the ability to store information
Describe the operation and characteristics of SR Latch, D Latch, and D Flip-Flop
Explain the purpose of clock signals and why edge triggering prevents race conditions
Describe how registers combine multiple flip-flops to store multi-bit data values
Explain the architecture of an ALU and how it selects among multiple operations
Explain principles of data representation in computer systemsAfter completing this lecture, students should be able to:
Convert numbers between binary and decimal number systems in both directions
Explain two's complement representation and why it simplifies hardware design
Perform binary addition and subtraction using paper-and-pencil methods
Explain how the same adder circuit handles both addition and subtraction through two's complement
Describe how status flags (Zero, Negative, Carry, Overflow) encode information about arithmetic results
Course Information:
National University of Kyiv-Mohyla Academy
Faculty of Informatics
Course Structure:
Full Course (CS, Software Engineering): 6 ECTS / 180 hours
Shortened Course (AKITR): 4 ECTS / 120 hours
"The real problem is not whether machines think but whether men do."
— B.F. Skinner]]></description><link>lectures/eng/l02-eng.-from-gates-to-alu.html</link><guid isPermaLink="false">Lectures/ENG/L02-ENG. From Gates to ALU.md</guid><pubDate>Tue, 20 Jan 2026 07:29:36 GMT</pubDate><enclosure url="assets/l02-combinational-vs-sequential.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l02-combinational-vs-sequential.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L02-NOTES-ENG. From Gates to ALU]]></title><description><![CDATA[Quick reference for Lecture 2. See full materials in L02-ENG. From Gates to ALU.md.Two fundamental circuit types in digital electronics:Key insight: Combinational logic alone cannot build a computer. We need sequential logic to remember state.Learn more:
<a data-tooltip-position="top" aria-label="https://www.jaroeducation.com/blog/difference-between-combinational-and-sequential-circuits/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.jaroeducation.com/blog/difference-between-combinational-and-sequential-circuits/" target="_self">Combinational vs Sequential Circuits</a>
Each position represents a power of 2:Example: 10110 = 1×16 + 0×8 + 1×4 + 1×2 + 0×1 = 22Learn more:
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Binary_number" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Binary_number" target="_self">Binary Number (Wikipedia)</a>
Three ways to represent negative numbers in binary:MSB = sign bit (0 = positive, 1 = negative), remaining bits = magnitude.
To negate: Flip the sign bit only
Problem: Two zeros (+0 = 0000, -0 = 1000)
Problem: Addition requires checking signs first
Range (N bits): -(2^(N-1) - 1) to +(2^(N-1) - 1)
Negative = invert all bits of the positive number.
To negate: NOT(x) — flip all bits
Problem: Two zeros (+0 = 0000, -0 = 1111)
Problem: Addition needs end-around carry
Range (N bits): -(2^(N-1) - 1) to +(2^(N-1) - 1)
The standard. Negative = invert all bits + add 1.
To negate: NOT(x) + 1
Only one zero (no -0 problem)
Same addition circuit for positive and negative
Range (N bits): -2^(N-1) to +(2^(N-1) - 1)
8-bit: -128 to +127
Example: Convert +5 to -5 (8-bit)+5: 00000101
NOT: 11111010
+1: 11111011 ← this is -5
Learn more:
<br><a data-tooltip-position="top" aria-label="https://uk.wikipedia.org/wiki/%D0%9F%D1%80%D1%8F%D0%BC%D0%B8%D0%B9_%D0%BA%D0%BE%D0%B4" rel="noopener nofollow" class="external-link is-unresolved" href="https://uk.wikipedia.org/wiki/%D0%9F%D1%80%D1%8F%D0%BC%D0%B8%D0%B9_%D0%BA%D0%BE%D0%B4" target="_self">Sign-magnitude (Wikipedia UK)</a>
<br><a data-tooltip-position="top" aria-label="https://uk.wikipedia.org/wiki/%D0%9E%D0%B1%D0%B5%D1%80%D0%BD%D0%B5%D0%BD%D0%B8%D0%B9_%D0%BA%D0%BE%D0%B4" rel="noopener nofollow" class="external-link is-unresolved" href="https://uk.wikipedia.org/wiki/%D0%9E%D0%B1%D0%B5%D1%80%D0%BD%D0%B5%D0%BD%D0%B8%D0%B9_%D0%BA%D0%BE%D0%B4" target="_self">Ones' Complement (Wikipedia UK)</a>
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Two%27s_complement" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Two%27s_complement" target="_self">Two's Complement (Wikipedia)</a> Memory = output connected back to input
OR gate with feedback: Can "remember" a 1
Once output is 1, it stays 1 (regardless of other input)
AND gate with feedback: Can "remember" a 0
Once output is 0, it stays 0
Combining OR and AND with feedback creates a latch:
SET input: Stores "1"
RESET input: Stores "0"
Problem: Forbidden state when both SET=1 and RESET=1
Solves the forbidden state problem:
DATA input: The bit to store (0 or 1)
WRITE ENABLE: When to capture the data
Learn more:
<br><a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=rM9BjciBLmg" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=rM9BjciBLmg" target="_self">How Transistors Remember Data</a> (YouTube, watch to 8:45)
<br><a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Flip-flop_(electronics)" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Flip-flop_(electronics)" target="_self">Flip-Flop (Wikipedia)</a>
Without synchronization, circuits suffer from:
Race conditions: Signals compete unpredictably
Metastability: Unstable intermediate states
Uncontrolled triggering: Data propagates before it should
Example problem: ALU adds A+B, stores result back to A. Without timing control, the result keeps feeding back (3→4→5→...) uncontrollably.A steady oscillation between 0 and 1:
Rising edge: The moment clock transitions LOW→HIGH
Clock cycle: One complete 0→1→0 period
Modern CPUs: billions of cycles per second (GHz)
Edge-triggered memory (captures only at clock edge):Built from two D latches in series:
First latch: Captures data when clock is LOW
Second latch: Copies to output when clock goes HIGH
Result: Data stored only at precise moments, preventing runaway feedback.D Flip-Flop + control logic:
DATA: Value to store
STORE: Enable signal (decides whether to update)
CLOCK: Timing signal
When STORE=0, the register feeds its own output back (holds value).
When STORE=1, new data enters on next clock edge.8 parallel 1-bit registers with shared CLOCK and STORE = 8-bit register (1 byte)Learn more:
<br><a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=I0-izyq6q5s" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=I0-izyq6q5s" target="_self">How Do Computers Remember</a> (YouTube)
Converts n input bits into 2^n output lines (one-hot encoding):Used for: Selecting one of many options (memory addresses, ALU operations, etc.)Multiple arithmetic circuits in parallel:
8-bit Adder
8-bit Subtractor
Incrementer (+1)
Decrementer (-1)
Opcode: The bits that specify which operation to performInstruction: 00 01 00 01 ↑↑ ↑↑ | └── Operation type (01 = SUB) └───── Instruction class (00 = ALU)
Binary decoder activates exactly one circuit based on opcode. Operand_1 Operand_2 ↓ ↓ ┌─────────────────┐ │ ALU │ ← Opcode └─────────────────┘ ↓ ↓ Result Flags (ZERO?, NEGATIVE?, OVERFLOW?)
Learn more:
<br><a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=HjneAhCy2N4" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=HjneAhCy2N4" target="_self">How Transistors Run Code</a> (YouTube) Combinational + Sequential = complete computer building blocks
Two's complement allows same circuits for +/- arithmetic
Feedback loops create memory from logic gates
Clock signal prevents race conditions and enables synchronization
D Flip-Flop = edge-triggered, controlled 1-bit memory
Registers = multiple flip-flops with shared control
Binary decoder selects one operation from opcode bits
ALU = multiple arithmetic circuits + decoder for selection
Required viewing:Think about:
Why does NOT(x) + 1 give you -x in two's complement?
How does the Binary Decoder select which ALU operation to perform?
]]></description><link>lectures/eng/l02-notes-eng.-from-gates-to-alu.html</link><guid isPermaLink="false">Lectures/ENG/L02-NOTES-ENG. From Gates to ALU.md</guid><pubDate>Tue, 20 Jan 2026 07:25:43 GMT</pubDate></item><item><title><![CDATA[L02-TC-4bit-Adder]]></title><description><![CDATA[<img src="assets/l02-tc-4bit-adder.png" target="_self">]]></description><link>assets/l02-tc-4bit-adder.html</link><guid isPermaLink="false">assets/L02-TC-4bit-Adder.png</guid><pubDate>Mon, 19 Jan 2026 21:17:58 GMT</pubDate><enclosure url="assets/l02-tc-4bit-adder.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l02-tc-4bit-adder.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L02-TC-Full-Adder]]></title><description><![CDATA[<img src="assets/l02-tc-full-adder.png" target="_self">]]></description><link>assets/l02-tc-full-adder.html</link><guid isPermaLink="false">assets/L02-TC-Full-Adder.png</guid><pubDate>Mon, 19 Jan 2026 21:17:58 GMT</pubDate><enclosure url="assets/l02-tc-full-adder.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l02-tc-full-adder.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L02-TC-Half-Adder]]></title><description><![CDATA[<img src="assets/l02-tc-half-adder.png" target="_self">]]></description><link>assets/l02-tc-half-adder.html</link><guid isPermaLink="false">assets/L02-TC-Half-Adder.png</guid><pubDate>Mon, 19 Jan 2026 21:17:58 GMT</pubDate><enclosure url="assets/l02-tc-half-adder.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l02-tc-half-adder.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L02-ALU-Block-Diagram]]></title><description><![CDATA[<img src="assets/l02-alu-block-diagram.png" target="_self">]]></description><link>assets/l02-alu-block-diagram.html</link><guid isPermaLink="false">assets/L02-ALU-Block-Diagram.png</guid><pubDate>Mon, 19 Jan 2026 21:17:58 GMT</pubDate><enclosure url="assets/l02-alu-block-diagram.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l02-alu-block-diagram.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L02-Instruction-Decoder-ALU]]></title><description><![CDATA[<img src="assets/l02-instruction-decoder-alu.png" target="_self">]]></description><link>assets/l02-instruction-decoder-alu.html</link><guid isPermaLink="false">assets/L02-Instruction-Decoder-ALU.png</guid><pubDate>Mon, 19 Jan 2026 21:17:58 GMT</pubDate><enclosure url="assets/l02-instruction-decoder-alu.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l02-instruction-decoder-alu.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L02-Binary-Decoder-Example-2]]></title><description><![CDATA[<img src="assets/l02-binary-decoder-example-2.png" target="_self">]]></description><link>assets/l02-binary-decoder-example-2.html</link><guid isPermaLink="false">assets/L02-Binary-Decoder-Example-2.png</guid><pubDate>Mon, 19 Jan 2026 21:17:32 GMT</pubDate><enclosure url="assets/l02-binary-decoder-example-2.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l02-binary-decoder-example-2.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L02-Binary-Decoder-Circuit]]></title><description><![CDATA[<img src="assets/l02-binary-decoder-circuit.png" target="_self">]]></description><link>assets/l02-binary-decoder-circuit.html</link><guid isPermaLink="false">assets/L02-Binary-Decoder-Circuit.png</guid><pubDate>Mon, 19 Jan 2026 21:17:32 GMT</pubDate><enclosure url="assets/l02-binary-decoder-circuit.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l02-binary-decoder-circuit.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L02-8bit-Components]]></title><description><![CDATA[<img src="assets/l02-8bit-components.png" target="_self">]]></description><link>assets/l02-8bit-components.html</link><guid isPermaLink="false">assets/L02-8bit-Components.png</guid><pubDate>Mon, 19 Jan 2026 21:17:32 GMT</pubDate><enclosure url="assets/l02-8bit-components.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l02-8bit-components.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L02-NandGame-4bit-Register]]></title><description><![CDATA[<img src="assets/l02-nandgame-4bit-register.png" target="_self">]]></description><link>assets/l02-nandgame-4bit-register.html</link><guid isPermaLink="false">assets/L02-NandGame-4bit-Register.png</guid><pubDate>Mon, 19 Jan 2026 21:17:32 GMT</pubDate><enclosure url="assets/l02-nandgame-4bit-register.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l02-nandgame-4bit-register.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L02-NandGame-1bit-Register]]></title><description><![CDATA[<img src="assets/l02-nandgame-1bit-register.png" target="_self">]]></description><link>assets/l02-nandgame-1bit-register.html</link><guid isPermaLink="false">assets/L02-NandGame-1bit-Register.png</guid><pubDate>Mon, 19 Jan 2026 21:17:32 GMT</pubDate><enclosure url="assets/l02-nandgame-1bit-register.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l02-nandgame-1bit-register.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L02-NandGame-D-Flipflop-Store]]></title><description><![CDATA[<img src="assets/l02-nandgame-d-flipflop-store.png" target="_self">]]></description><link>assets/l02-nandgame-d-flipflop-store.html</link><guid isPermaLink="false">assets/L02-NandGame-D-Flipflop-Store.png</guid><pubDate>Mon, 19 Jan 2026 21:17:15 GMT</pubDate><enclosure url="assets/l02-nandgame-d-flipflop-store.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l02-nandgame-d-flipflop-store.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L02-NandGame-D-Flipflop]]></title><description><![CDATA[<img src="assets/l02-nandgame-d-flipflop.png" target="_self">]]></description><link>assets/l02-nandgame-d-flipflop.html</link><guid isPermaLink="false">assets/L02-NandGame-D-Flipflop.png</guid><pubDate>Mon, 19 Jan 2026 21:17:15 GMT</pubDate><enclosure url="assets/l02-nandgame-d-flipflop.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l02-nandgame-d-flipflop.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L02-Clock-Signal-Waveform]]></title><description><![CDATA[<img src="assets/l02-clock-signal-waveform.png" target="_self">]]></description><link>assets/l02-clock-signal-waveform.html</link><guid isPermaLink="false">assets/L02-Clock-Signal-Waveform.png</guid><pubDate>Mon, 19 Jan 2026 21:17:15 GMT</pubDate><enclosure url="assets/l02-clock-signal-waveform.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l02-clock-signal-waveform.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L02-NandGame-D-Latch-Circuit]]></title><description><![CDATA[<img src="assets/l02-nandgame-d-latch-circuit.png" target="_self">]]></description><link>assets/l02-nandgame-d-latch-circuit.html</link><guid isPermaLink="false">assets/L02-NandGame-D-Latch-Circuit.png</guid><pubDate>Mon, 19 Jan 2026 21:17:15 GMT</pubDate><enclosure url="assets/l02-nandgame-d-latch-circuit.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l02-nandgame-d-latch-circuit.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L02-Byte-8-Bits]]></title><description><![CDATA[<img src="assets/l02-byte-8-bits.png" target="_self">]]></description><link>assets/l02-byte-8-bits.html</link><guid isPermaLink="false">assets/L02-Byte-8-Bits.png</guid><pubDate>Mon, 19 Jan 2026 21:17:15 GMT</pubDate><enclosure url="assets/l02-byte-8-bits.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l02-byte-8-bits.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L02-D-Latch-NAND-Circuit]]></title><description><![CDATA[<img src="assets/l02-d-latch-nand-circuit.png" target="_self">]]></description><link>assets/l02-d-latch-nand-circuit.html</link><guid isPermaLink="false">assets/L02-D-Latch-NAND-Circuit.png</guid><pubDate>Mon, 19 Jan 2026 21:17:00 GMT</pubDate><enclosure url="assets/l02-d-latch-nand-circuit.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l02-d-latch-nand-circuit.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L02-Latch-Write-Enable]]></title><description><![CDATA[<img src="assets/l02-latch-write-enable.png" target="_self">]]></description><link>assets/l02-latch-write-enable.html</link><guid isPermaLink="false">assets/L02-Latch-Write-Enable.png</guid><pubDate>Mon, 19 Jan 2026 21:17:00 GMT</pubDate><enclosure url="assets/l02-latch-write-enable.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l02-latch-write-enable.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L02-SR-Latch-Invalid-State]]></title><description><![CDATA[<img src="assets/l02-sr-latch-invalid-state.png" target="_self">]]></description><link>assets/l02-sr-latch-invalid-state.html</link><guid isPermaLink="false">assets/L02-SR-Latch-Invalid-State.png</guid><pubDate>Mon, 19 Jan 2026 21:17:00 GMT</pubDate><enclosure url="assets/l02-sr-latch-invalid-state.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l02-sr-latch-invalid-state.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L02-SR-Latch-SET-RESET]]></title><description><![CDATA[<img src="assets/l02-sr-latch-set-reset.png" target="_self">]]></description><link>assets/l02-sr-latch-set-reset.html</link><guid isPermaLink="false">assets/L02-SR-Latch-SET-RESET.png</guid><pubDate>Mon, 19 Jan 2026 21:17:00 GMT</pubDate><enclosure url="assets/l02-sr-latch-set-reset.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l02-sr-latch-set-reset.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L02-OR-Gate-With-Feedback]]></title><description><![CDATA[<img src="assets/l02-or-gate-with-feedback.png" target="_self">]]></description><link>assets/l02-or-gate-with-feedback.html</link><guid isPermaLink="false">assets/L02-OR-Gate-With-Feedback.png</guid><pubDate>Mon, 19 Jan 2026 21:16:42 GMT</pubDate><enclosure url="assets/l02-or-gate-with-feedback.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l02-or-gate-with-feedback.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L02-Twos-Complement-NEG-Example]]></title><description><![CDATA[<img src="assets/l02-twos-complement-neg-example.png" target="_self">]]></description><link>assets/l02-twos-complement-neg-example.html</link><guid isPermaLink="false">assets/L02-Twos-Complement-NEG-Example.png</guid><pubDate>Mon, 19 Jan 2026 21:16:25 GMT</pubDate><enclosure url="assets/l02-twos-complement-neg-example.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l02-twos-complement-neg-example.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L02-Twos-Complement-Wheel]]></title><description><![CDATA[<img src="assets/l02-twos-complement-wheel.png" target="_self">]]></description><link>assets/l02-twos-complement-wheel.html</link><guid isPermaLink="false">assets/L02-Twos-Complement-Wheel.png</guid><pubDate>Mon, 19 Jan 2026 21:16:25 GMT</pubDate><enclosure url="assets/l02-twos-complement-wheel.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l02-twos-complement-wheel.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L02-Hex-Binary-Decimal-Table]]></title><description><![CDATA[<img src="assets/l02-hex-binary-decimal-table.png" target="_self">]]></description><link>assets/l02-hex-binary-decimal-table.html</link><guid isPermaLink="false">assets/L02-Hex-Binary-Decimal-Table.png</guid><pubDate>Mon, 19 Jan 2026 21:16:25 GMT</pubDate><enclosure url="assets/l02-hex-binary-decimal-table.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l02-hex-binary-decimal-table.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L02-Combinational-vs-Sequential]]></title><description><![CDATA[<img src="assets/l02-combinational-vs-sequential.png" target="_self">]]></description><link>assets/l02-combinational-vs-sequential.html</link><guid isPermaLink="false">assets/L02-Combinational-vs-Sequential.png</guid><pubDate>Mon, 19 Jan 2026 21:16:25 GMT</pubDate><enclosure url="assets/l02-combinational-vs-sequential.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l02-combinational-vs-sequential.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L02-Automata-Theory-Hierarchy]]></title><description><![CDATA[<img src="assets/l02-automata-theory-hierarchy.png" target="_self">]]></description><link>assets/l02-automata-theory-hierarchy.html</link><guid isPermaLink="false">assets/L02-Automata-Theory-Hierarchy.png</guid><pubDate>Mon, 19 Jan 2026 21:16:25 GMT</pubDate><enclosure url="assets/l02-automata-theory-hierarchy.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l02-automata-theory-hierarchy.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L01. Фізика обчислень]]></title><description><![CDATA[Курс: Архітектура комп'ютерних систем
Тема: Перші принципи — У пошуках основи
Результат навчання (LO1): Пояснити принципи побудови та функціонування сучасних комп'ютерних архітектурМислення від перших принципів — це підхід до розв'язання задач, який передбачає розкладання складних проблем на їхні найфундаментальніші істини та побудову рішення від них. Замість того, щоб міркувати за аналогією чи приймати загальноприйняту мудрість, цей метод запитує: «Що ми знаємо напевно? Що було доведено?»Давньогрецький філософ Арістотель визначив перший принцип як «першу основу, з якої пізнається річ». У сучасному розумінні це означає виявлення базових припущень, які не можуть бути виведені далі.Коли Ілон Маск заснував SpaceX у 2002 році, він зіткнувся з, здавалося б, нездоланним викликом: ракети коштували до 65 мільйонів доларів. Замість того, щоб прийняти це як факт, Маск застосував мислення від перших принципів:Він запитав: «З чого зроблена ракета?» Відповідь: алюмінієві сплави аерокосмічного класу, титан, мідь та вуглецеве волокно. Потім він запитав: «Яка вартість цих матеріалів на товарному ринку?» Дивовижна відповідь: сировина коштує лише близько 2% від типової ціни ракети.Міркуючи від перших принципів, а не приймаючи галузеві ціни, Маск зрозумів, що SpaceX може будувати ракети самостійно за частку вартості. Протягом кількох років SpaceX знизила ціни на запуски майже в 10 разів, при цьому залишаючись прибутковою.Метод можна узагальнити у чотирьох кроках:
Декомпозиція до фундаментальних основ — Розкладіть проблему на базові, перевірені компоненти
Ставте під сумнів усі припущення — Не приймайте «так завжди робилося»
Відбудуйте з нуля — Використовуйте лише перевірені фундаментальні істини як будівельні блоки
Думайте як науковець — Покладайтеся на логіку та докази, а не на традиції чи аналогії
Цей підхід є важливим для розуміння архітектури комп'ютерів, оскільки нам потрібно зрозуміти, чому комп'ютери працюють саме так, а не лише те, що вони працюють.Джерела:
James Clear: First Principles - The Building Blocks of True Knowledge
Farnam Street: First Principles - Elon Musk on Thinking for Yourself
Elon Musk Explains First Principles (YouTube)
Ключове питання, яке спрямовує всю цю лекцію — і справді весь цей курс — оманливо просте:
«Що таке комп'ютер за своєю суттю?»
Або точніше:
Як фізичний світ дозволяє нам обчислювати?
Щоб відповісти на це питання, ми повинні вийти за межі знайомих абстракцій програмного забезпечення, застосунків та інтерфейсів користувача. Нам потрібно зрозуміти, що кожне обчислення, яке ви коли-небудь виконували — кожен розрахунок, кожен пошук, кожен відеопотік — зрештою залежить від фізичних процесів, що відбуваються в реальному світі.Комп'ютер — це не магія. Це фізичний пристрій, який маніпулює фізичними об'єктами відповідно до фізичних законів. Розуміння цієї фундаментальної істини є основою, на якій побудована вся інформатика.Кожен фрагмент інформації, що зберігається в комп'ютері, існує як фізичний стан:
Електричні заряди в конденсаторі
Магнітні орієнтації на пластині жорсткого диска
Патерни світла в оптоволоконному кабелі
Рівні напруги в транзисторі
Як влучно зазначив фізик Рольф Ландауер: "Інформація є фізичною." Це не просто філософське спостереження — це фундаментальний принцип, який визначає, що комп'ютери можуть і чого не можуть робити.Задовго до появи електронних комп'ютерів люди створювали обчислювальні машини, використовуючи фізичні маніпуляції:Абак (бл. 2400 р. до н.е.): Можливо, найстаріший обчислювальний пристрій — абак виконує обчислення шляхом переміщення кісточок уздовж стрижнів. Кожна позиція кісточки представляє числове значення, а переміщення кісточок за правилами дає арифметичні результати. Фізичні позиції кісточок і є збереженими даними.Механічні калькулятори (XVII–XX ст.): Такі пристрої, як Паскаліна та ступінчастий калькулятор Лейбніца, використовували шестерні, колеса та механічні з'єднання для виконання обчислень. Обертання шестерні на певну величину відповідає додаванню або множенню чисел.Ключовий висновок із цих історичних пристроїв полягає в тому, що обчислення завжди було пов'язане з маніпулюванням фізичними об'єктами за певними правилами. Сучасні електронні комп'ютери просто роблять те саме в неймовірно меншому масштабі та з більшою швидкістю.Один із найбільш наочних способів зрозуміти, як працюють комп'ютери, — уявити собі побудову комп'ютера з водопровідних труб і клапанів. Ця метафора "водяного комп'ютера" слугує центральним концептуальним інструментом протягом усього курсу.Модель водяного комп'ютера:
Вода, що тече через трубу = сигнал "1" (стан ON)
Вода не тече = сигнал "0" (стан OFF)
Клапан = логічний вентиль (керує потоком на основі вхідних даних)
Розгалуження труб = маршрутизація сигналу та паралельна обробка
Це не просто навчальна аналогія — гідравлічні комп'ютери справді були побудовані та використовувалися для розв'язання диференціальних рівнянь, особливо в Радянському Союзі в середині XX століття.Метафора водяного комп'ютера ілюструє кілька критично важливих концепцій: Емерджентність: Складна поведінка виникає з простих правил. Подібно до того, як складні обчислення можуть виникати з води, що тече через труби та клапани, складні обчислення з'являються з простих операцій перемикання транзисторів. Фізичні обмеження: Водяний комп'ютер має властиві обмеження: Тиск води падає з відстанню
Труби мають опір потоку
Клапанам потрібен час для відкриття та закриття
Енергія втрачається через тертя та нагрівання Ці обмеження відображають реальні обмеження електронних комп'ютерів: затримки поширення сигналу, опір, час перемикання та розсіювання потужності. Аналогія з електронікою: Електрони, що рухаються по дротах, поводяться напрочуд схоже на воду, що тече по трубах: Напруга ≈ Тиск води
Струм ≈ Швидкість потоку
Опір ≈ Звуження труби
Транзистор ≈ Клапан Клапан, керований вхідним сигналом — коли вхід активний, вода протікає. Це саме те, як транзистор керує електричним струмом.Комп'ютери на кульках: Механічні комп'ютери, побудовані з використанням кульок, що котяться по доріжках, з вентилями, керованими позиціями кульок. Вони наочно демонструють двійкові логічні операції суто механічними засобами.Комп'ютери на доміно: Логічні вентилі можна реалізувати за допомогою падаючих кісточок доміно. Вентиль AND вимагає, щоб обидві вхідні кісточки впали, перш ніж активувати вихід; вентиль OR активується, якщо падає будь-яка з вхідних кісточок.Ці демонстрації підкріплюють ключовий висновок: обчислення — це фізичний процес, який може бути реалізований на багатьох різних фізичних носіях.Джерела:
[4a] Water Integrator / Hydraulic Computer (YouTube)
[4b] Marble Computer — Part 1 (YouTube)
Domino Computer (YouTube)
Якщо обчислення — це фізичні маніпуляції, чому ми не використовуємо механічні комп'ютери сьогодні? Чому перемогли електронні комп'ютери?Розглянемо характеристики різних обчислювальних середовищ:Електронні комп'ютери домінують з кількох фундаментальних причин:Швидкість: Електричні сигнали поширюються зі швидкістю приблизно 50-99% від швидкості світла через провідники. У комп'ютерному чипі сигнали поширюються приблизно на 15-30 сантиметрів за наносекунду (мільярдну частку секунди). Механічні компоненти не можуть наблизитися до таких швидкостей.Відсутність рухомих частин: На відміну від шестерень, які зношуються, або води, яка витікає, електрони, що протікають через твердотільні пристрої, не зазнають механічної втоми. Транзистор може перемикатися трильйони разів без деградації.Мініатюризація: Сучасні транзистори вимірюються в нанометрах — мільярдних частках метра. Один процесорний чип містить мільярди транзисторів. Жодна механічна технологія не може досягти такої щільності.Енергоефективність: Хоча електронні пристрої все ще споживають значну потужність, електронне перемикання є набагато ефективнішим, ніж механічний рух. Переміщувати електрони легше, ніж переміщувати атоми.Отже, відповідь на питання «чому електронні комп'ютери?»: швидкість, надійність і масштаб. Електронами можна маніпулювати швидше, надійніше та в менших масштабах, ніж будь-якою механічною альтернативою.Розуміння історії обчислень розкриває, як ми прийшли до сучасної комп'ютерної архітектури, і допомагає пояснити, чому поточні конструкції мають сенс.Винахідник: Готфрід Вільгельм Лейбніц, німецький математик і філософ<img alt="Калькулятор Лейбніца" src="assets/l01-leibniz-calculator.png" target="_self">Калькулятор Лейбніца — перший калькулятор, здатний виконувати всі чотири арифметичні операції.Калькулятор Лейбніца був першим калькулятором, здатним виконувати всі чотири основні арифметичні операції: додавання, віднімання, множення та ділення. Лейбніц розвинув ідеї попереднього калькулятора «Паскаліна» Блеза Паскаля, який міг лише додавати і віднімати.Ключові особливості:
Використовував десяткову систему числення (з основою 10)
Працював шляхом обертання ручки
Додавання і віднімання: один оберт ручки
Множення і ділення: повторні додавання/віднімання
Інновація: Калькулятор Лейбніца представив «колесо Лейбніца» — циліндр із зубцями різної довжини. Цей механізм дозволяв одній шестерні представляти будь-яку цифру від 0 до 9 за один оберт. Ця конструкція домінувала у виробництві калькуляторів протягом наступних 275 років.Історичне значення: Лейбніц також був активним прихильником двійкової системи числення, визнаючи її потенціал для механічних обчислень: «Двійкові числа ідеальні для машин, тому що вони потребують лише двох цифр, які легко можуть бути представлені станами вимкнено та увімкнено перемикача.» Однак його калькулятор використовував десяткову систему, а не двійкову, оскільки двійкова система потребувала б більше цифр і, отже, більшої механічної складності.Винахідник: Чарльз Беббідж, англійський математик<br><img alt="Різницева машина Беббіджа" src="assets/l01-babbage-difference-engine.jpeg" target="_self">Деталь Різницевої машини Беббіджа, що показує латунні числові колеса та зубчасті механізми.Беббідж спроектував Різницеву машину для автоматизації обчислення поліноміальних функцій, зокрема для створення безпомилкових математичних таблиць для навігації та інженерії.Проблема, яку вирішували: На початку 19 століття математичні таблиці (логарифми, тригонометрія, астрономічні позиції) обчислювалися вручну і часто містили помилки. Кораблі, що навігували за помилковими таблицями, могли заблукати або зазнати аварії. Беббідж прагнув механізувати виробництво таблиць, щоб усунути людські помилки.Ключові особливості:
Механічний комп'ютер, що використовував шестерні та важелі
Міг обчислювати поліноміальні функції методом скінченних різниць
Був спроектований для безпосереднього друку результатів, уникаючи помилок переписування
Результат: Беббідж ніколи не завершив повномасштабну Різницеву машину за свого життя через обмеження виробництва та проблеми з фінансуванням. Однак робоча модель була врешті-решт побудована в 1991 році за його оригінальними кресленнями, що довело правильність концепції.Пізніша конструкція Беббіджа, Аналітична машина, була революційним стрибком — по суті, проект універсального програмованого комп'ютера, більш ніж за 100 років до ENIAC.Особливості, що передбачали сучасні комп'ютери:
Окремі «сховище» (пам'ять) і «млин» (процесор)
Введення за допомогою перфокарт (запозичене з ткацьких верстатів Жаккара)
Умовне розгалуження (логіка якщо-тоді)
Цикли (ітерація)
Ада Лавлейс, яка працювала з Беббіджем, написала детальні нотатки, що описують, як можна програмувати Аналітичну машину, що робить її, можливо, першим комп'ютерним програмістом.Контекст: Розроблений під час Другої світової війни в Гарвардському університеті за фінансування IBMHarvard Mark I був електромеханічним комп'ютером — він використовував електричні компоненти для керування механічними реле.Ключові особливості:
Арифметика з основою 10 (десяткова)
51 фут завдовжки, 8 футів заввишки, вагою 5 тонн
Міг виконувати 3 додавання на секунду
Використовувався для обчислень на підтримку Мангеттенського проєкту
Історичне значення: Джон фон Нейман використовував Harvard Mark I для обчислень, пов'язаних із розробкою ядерної зброї. Машина представляла кульмінацію механічних обчислень перед тим, як електроніка взяла гору.Джерела:
Early Computing: Crash Course Computer Science #1 (PBS/YouTube)
History of Computing (YouTube)
Harvard Mark I (Wikipedia)
<br><img alt="Колекція вакуумних ламп" src="assets/l01-vacuum-tubes-collection.jpg" target="_self">Різноманітні вакуумні лампи, що використовувалися в ранніх комп'ютерах — електронні перемикачі, що передували транзисторам.Як працюють вакуумні лампи:Вакуумна лампа — це електронний пристрій, який керує потоком електронів через вакуум. Основний принцип — термоелектронна емісія: коли метал нагрівається, деякі електрони отримують достатньо енергії, щоб вирватися з поверхні металу.Тріодна вакуумна лампа має три елементи:
Катод — нагрівана нитка, що випромінює електрони
Анод (пластина) — позитивно заряджений електрод, що притягує електрони
Сітка — дротяна сітка між катодом та анодом, що контролює потік електронів
Прикладаючи невелику напругу до сітки, можна керувати значно більшим струмом, що тече від катода до анода. Це дозволяє вакуумним лампам підсилювати сигнали або працювати як перемикачі.Вакуумні лампи в обчислювальній техніці:Вакуумні лампи уможливили створення перших електронних комп'ютерів, оскільки вони могли перемикатися між увімкненим та вимкненим станами без механічного руху — електронами можна було керувати суто електрично.ENIAC (1945):
Перший електронний комп'ютер загального призначення
Використовував приблизно 18 000 вакуумних ламп
Міг виконувати 5 000 додавань на секунду (у 1000 разів швидше за електромеханічні машини)
Важив 30 тонн
Споживав 150 кіловат потужності
Потребував власного електрогенератора
Зліва: Два оператори програмують ENIAC, з'єднуючи патч-кабелі. Справа: Світні панелі вакуумних ламп ENIAC, що показують масштаб обчислень на лампах.Проблеми вакуумних ламп:
Частота відмов: Вакуумні лампи перегоряють, як електричні лампочки. ENIAC виходив з ладу через лампи кожні кілька годин, що потребувало постійного обслуговування.
Розмір: Кожна лампа була приблизно розміром з електричну лампочку. Побудова складних схем потребувала величезного простору.
Споживання енергії: Лампи потребують значної потужності для нагрівання катода, генеруючи суттєве тепло.
Відведення тепла: Вся ця потужність перетворювалася на тепло, яке потрібно було відводити, що вимагало розгалуженої системи охолодження.
Досвід роботи з ENIAC:Експлуатація ENIAC була складною. 18 000 ламп машини означали, що статистично лампа виходила з ладу кожні кілька годин. Томмі Флауерс, який побудував комп'ютер Colossus (секретну британську машину для дешифрування), виявив, що постійне утримання ламп під напругою насправді покращувало надійність — лампи найчастіше виходили з ладу під час циклів увімкнення/вимкнення живлення.Незважаючи на ці труднощі, комп'ютери на вакуумних лампах довели життєздатність електронних обчислень та продемонстрували, що електронні швидкості завжди перевершуватимуть механічні альтернативи.Винахід (1947):Транзистор було винайдено в Bell Labs Вільямом Шоклі, Джоном Бардіном та Волтером Браттейном. Слово «транзистор» поєднує «transfer» (передача) та «resistor» (резистор), відображаючи те, як пристрій передає опір від одного виводу до іншого.<br><img alt="Транзистори різних розмірів" src="assets/l01-transistors-various-sizes.jpg" target="_self">Транзистори різних епох: сучасні компоненти поверхневого монтажу (зліва), транзистори з виводами (в центрі) та силові транзистори (справа). Зверніть увагу на драматичне зменшення розмірів з часом.Чому транзистори перемогли:Ключові переваги:
Твердотільний: Немає вакууму для підтримки, немає крихкої скляної оболонки
Без часу прогрівання: Працює миттєво при подачі живлення
Мініатюризація: Може бути надзвичайно малим
Низьке енергоспоживання: На порядки менше, ніж у ламп
Надійність: Немає нитки розжарення, яка може перегоріти
Зліва: Зображення транзистора з електронного мікроскопа в масштабі 5 мкм. Справа: Кристал сучасного ЦП, що показує мільярди транзисторів, організованих у функціональні блоки (кеш, логічні блоки) у псевдокольорах.Перехід:Перший повністю транзисторний комп'ютер, TX-0, з'явився у 1956 році. До 1960-х років транзистори фактично замінили вакуумні лампи в обчислювальній техніці. Цей перехід уможливив мініатюризацію, яка врешті-решт привела до персональних комп'ютерів, смартфонів та сучасного цифрового світу.Джерела:
History of Computing (YouTube)
Why The First Computers Were Made Out Of Light Bulbs (Veritasium/YouTube)
Якщо транзистори теоретично можуть розрізняти кілька рівнів напруги (0В, 1В, 2В, 3В...), чому комп'ютери використовують лише два стани? Навіщо обмежувати себе двійковою системою?1. ЗавадостійкістьУ реальних схемах електричні сигнали ніколи не бувають ідеально чистими. Шум від джерел живлення, електромагнітні завади, теплові флуктуації та інші джерела постійно спотворюють рівні напруги.Розглянемо систему, яка намагається розрізнити 10 рівнів напруги (0-9):
У системі з напругою 5В кожен рівень відповідає 0,5В
Шумовий сплеск лише 0,3В може спричинити помилкове зчитування
Рівень помилок буде неприйнятно високим
Двійкові системи мають значно більші запаси:
Лише два стани: зазвичай 0-1В відповідає «0», а 3-5В відповідає «1»
«Заборонена зона» в 2В між ними поглинає шум
Сигнал може значно відхилятися без зміни значення
Ось чому двійкові сигнали мають завадостійкість — невеликі варіації не впливають на інтерпретацію. Доки сигнал залишається у правильному діапазоні напруги, двійкове значення залишається чітким.2. Проста реалізаціяСтворити схему, яка надійно розрізняє два стани, набагато легше, ніж створити таку, що розрізняє багато станів:
Два стани потребують одного порогового компаратора
N станів потребують N-1 точно відкаліброваних компараторів
Виробничі допуски роблять точні багаторівневі схеми складними
У двійковій системі транзистор перебуває або в:
Режимі відсічення: Струм не тече (відповідає «0»)
Режимі насичення: Тече максимальний струм (відповідає «1»)
Немає потреби працювати в лінійній (підсилювальній) області, що значно спрощує проектування.3. Основа булевої алгебриДвійкові обчислення мають готовий математичний фундамент: булеву алгебру, розроблену Джорджем Булем у 1840-х роках — понад століття до появи електронних комп'ютерів.Булева алгебра забезпечує:
Строгі логічні операції: AND, OR, NOT
Алгебраїчні закони для спрощення виразів
Теореми повноти, що доводять можливість побудови будь-якої логічної функції з базових операцій
Усталені методи доведення для верифікації правильності схем
Магістерська дисертація Клода Шеннона 1937 року «A Symbolic Analysis of Relay and Switching Circuits» продемонструвала, що булева алгебра може бути безпосередньо реалізована в електричних схемах. Це забезпечило теоретичний фундамент для проектування цифрових комп'ютерів.У нашій метафорі водяного комп'ютера двійкова система має ще більше сенсу:
Вода тече = 1
Вода не тече = 0
Спроба розрізнити «половинний потік» або «чвертьовий потік» була б ненадійною — клапани підтікають, тиск змінюється, потік турбулентний. Але «тече проти не тече» — це надійне розрізнення, яке витримує недосконалості реального світу.Джерела:
[10] Why Use Binary? (YouTube)
Згадаймо нашу метафору водяного комп'ютера. Клапан контролює потік води:
Клапан відкритий: Вода протікає
Клапан закритий: Вода заблокована
Транзистор робить те саме з електричним струмом:
Транзистор увімкнений (насичення): Струм тече від колектора до емітера
Транзистор вимкнений (відсічення): Струм не тече
Ключове розуміння полягає в тому, що транзистор використовує один електричний сигнал для керування іншим — так само як наш клапан використовує один механізм (обертання ручки) для керування іншим (потоком води).Найпоширеніший тип транзистора для розуміння цифрової логіки — це NPN біполярний транзистор:<br><img alt="Схема та символ BJT транзистора" src="assets/l01-transistor-diagram-symbol.png" target="_self">Схематичний символ NPN транзистора з позначенням трьох виводів, поруч із фізичним корпусом транзистора.Три виводи:
База (B): Керуючий вхід — невеликий струм тут контролює основний струм
Колектор (C): Сюди входить основний струм
Емітер (E): Звідси виходить основний струм
Принцип роботи:
Коли через базу не тече струм, струм від колектора до емітера не тече (транзистор ВИМКНЕНИЙ)
Коли через базу тече достатній струм, струм може текти від колектора до емітера (транзистор УВІМКНЕНИЙ)
Невеликий струм бази керує значно більшим струмом колектор-емітер
Цифрова робота:Для цифрових схем транзистори працюють лише у двох режимах:Режим відсічення (ВИМКНЕНО/Логічний 0):
Напруга на базі нижча за порогову (~0.7В для кремнію)
Струм бази не тече
Транзистор поводиться як розімкнений перемикач
Струм від колектора до емітера не тече
Напруга на колекторі висока (підключено до джерела живлення)
Режим насичення (УВІМКНЕНО/Логічна 1):
Напруга на базі перевищує порогову
Тече достатній струм бази
Транзистор поводиться як замкнений перемикач
Тече максимальний струм від колектора до емітера
Напруга на колекторі падає майже до нуля
<br><img alt="Транзистор як перемикач" src="assets/l01-transistor-as-switch.png" target="_self">Транзистор, що працює як перемикач: коли База=0 (зліва), струм не тече і вихід високий; коли База=1 (справа), струм тече і вихід стає низьким.У нашій гідравлічній аналогії:
Струм бази = Сила відкривання клапана
Шлях колектор-емітер = Основна водопровідна труба
Відсічення = Клапан повністю закритий
Насичення = Клапан повністю відкритий
Так само як невелика сила на ручці клапана може контролювати великий потік води, невеликий струм бази керує великим струмом колектор-емітер. Це властивість підсилення, яка робить транзистори такими корисними.Транзистори також можуть працювати в «активній» області, де вони пропорційно підсилюють аналогові сигнали. Але для цифрової логіки ми навмисно уникаємо цієї області:
Нам потрібні лише два стани: 0 та 1
Активна область чутлива до шуму: Невеликі зміни на вході спричиняють зміни на виході
Двійкова робота є надійною: Або повністю увімкнено, або повністю вимкнено
Це навмисне обмеження — ставлення до складного аналогового пристрою як до простого перемикача увімкнено/вимкнено — це те, що робить надійні цифрові обчислення можливими.Логічний вентиль — це схема, яка виконує булеву логічну операцію над одним або кількома двійковими входами для отримання одного двійкового виходу. Логічні вентилі є фундаментальними будівельними блоками, з яких створюються всі цифрові схеми.Функція: Видає протилежне значення входуТаблиця істинності:<br><img alt="Реалізація вентиля NOT" src="assets/l01-gate-not.png" target="_self">Вентиль NOT: транзисторна реалізація з джерелом живлення Vcc, логічний символ та таблиця істинності.Реалізація: Найпростіший логічний вентиль. Коли вхід високий, транзистор вмикається, з'єднуючи вихід із землею (низький рівень). Коли вхід низький, транзистор вимкнений, і вихід підтягується до високого рівня через резистор.Водяна аналогія: «Нормально відкритий» клапан, який пропускає воду, коли не натиснутий, але закривається, коли подається керуючий сигнал.Функція: Видає 1 лише тоді, коли ВСІ входи дорівнюють 1Таблиця істинності (2 входи):Водяна аналогія: Два клапани послідовно — вода протікає лише якщо ОБИДВА клапани відкриті.Функція: Видає 1, коли БУДЬ-ЯКИЙ вхід дорівнює 1Таблиця істинності (2 входи):Водяна аналогія: Два клапани паралельно — вода протікає, якщо БУДЬ-ЯКИЙ клапан відкритий (або обидва).Функція: NOT-AND — видає 0 лише тоді, коли ВСІ входи дорівнюють 1Таблиця істинності (2 входи):<br><img alt="Реалізація вентиля NAND" src="assets/l01-gate-nand.png" target="_self">Вентиль NAND: транзисторна реалізація, логічний символ та таблиця істинності. Це універсальний вентиль, з якого можна побудувати всі інші вентилі.Це просто вентиль AND, за яким іде вентиль NOT.Функція: NOT-OR — видає 1 лише тоді, коли ВСІ входи дорівнюють 0Таблиця істинності (2 входи):Функція: Видає 1, коли входи РІЗНІТаблиця істинності (2 входи):XOR є важливим для арифметичних операцій — він є основою двійкового додавання.Ось один з найглибших фактів у цифровій логіці:
Будь-яка булева функція може бути реалізована, використовуючи лише вентилі NAND.
Це означає, що маючи лише один тип компонента — вентиль NAND — ви можете побудувати будь-який комп'ютер, будь-який процесор, будь-яку цифрову схему, яку тільки можна уявити. NAND називають "універсальним вентилем" завдяки цій надзвичайній властивості.NOT з NAND:
Просто з'єднайте обидва входи вентиля NAND разом:
Якщо вхід = 0: NAND(0,0) = 1
Якщо вхід = 1: NAND(1,1) = 0
Це в точності функція NOT!AND з NAND:
Використайте два вентилі NAND:
Перший NAND: обчислюємо NAND(A,B)
Другий NAND (як NOT): обчислюємо NOT(NAND(A,B)) = AND(A,B)
OR з NAND:
Використайте три вентилі NAND:
Перший NAND (як NOT): обчислюємо NOT(A)
Другий NAND (як NOT): обчислюємо NOT(B)
Третій NAND: обчислюємо NAND(NOT(A), NOT(B)) = OR(A,B)
(Тут використовується закон де Моргана: A OR B = NOT(NOT(A) AND NOT(B)))XOR з NAND:
Може бути побудований лише з 4 вентилів NAND завдяки хитрому розташуванню.Для теорії:
Універсальність NAND доводить, що однієї фундаментальної операції достатньо для всіх обчислень. Це аналогічно тому, як невеликий набір аксіом може породити всю математику.Для практики:
Виробництво спрощується, коли потрібен лише один тип вентиля. У реальних інтегральних схемах інженери часто реалізують все, використовуючи NAND (або NOR, який також є універсальним), оскільки:
Стандартизовані процеси виробництва
Спрощені процедури тестування
Зменшений асортимент компонентів
Легша оптимізація
Для цього курсу:
Розуміння того, що лише вентилі NAND можуть побудувати будь-що — арифметичні схеми, пам'ять, процесори — забезпечує конкретну основу для розуміння архітектури комп'ютера. Все складне будується з цієї однієї простої цеглини.Думайте про вентилі NAND як про блоки LEGO. Маючи достатньо ідентичних блоків LEGO, ви можете будувати замки, транспортні засоби, мости — будь-що. Так само, маючи достатньо вентилів NAND, ви можете побудувати суматори, помножувачі, комірки пам'яті, цілі ЦП.У цьому сила цифрової абстракції: з одного простого компонента виникає довільна складність.Логічні вентилі, які ми обговорювали досі, є "комбінаційними" — їхні виходи залежать лише від поточних входів. Але комп'ютерам потрібна пам'ять: здатність зберігати інформацію та відтворювати її пізніше.Як змусити схему "запам'ятовувати"?Пам'ять виникає зі зворотного зв'язку — з'єднання виходу вентиля назад з його входом. Це створює стабільні стани, які зберігаються навіть коли входи змінюються.Засувка із встановленням-скиданням (SR) є найпростішим елементом пам'яті, побудованим з двох вентилів NOR (або двох вентилів NAND) з перехресним зворотним зв'язком:
Два стабільні стани: Q=0 або Q=1
Встановлення (S=1, R=0): Примусово встановлює Q в 1
Скидання (S=0, R=1): Примусово скидає Q в 0
Утримання (S=0, R=0): Зберігає поточний стан
З'єднання зворотного зв'язку створюють бістабільність — схема природно встановлюється в один з двох станів і залишається там, поки не буде переведена в інший.D-тригер (тригер даних) додає важливі функції до базової засувки:Входи:
D (Data, дані): Значення, яке потрібно зберегти
CLK (Clock, тактовий сигнал): Керує тим, КОЛИ значення зберігається
Виходи:
Q: Збережене значення
Q̄ (Q-інверсне): Доповнення Q
Робота:
Коли тактовий сигнал неактивний: Вихід Q зберігає своє попереднє значення незалежно від D
На фронті тактового сигналу (передньому або задньому): Вихід Q приймає будь-яке значення, яке D має в цей момент
Після фронту тактового сигналу: Q утримує це значення до наступного фронту
Зліва: Схема D-тригера, побудована з вентилів NAND, з входами D (дані) та E (дозвіл/тактовий сигнал). Справа: Таблиця істинності, що показує, як вихід Q слідує за входом D, коли дозволено.Критична особливість: D-тригер захоплює вхід у конкретний момент (на фронті тактового сигналу) та утримує це значення до наступного захоплення. Це є основою синхронізованої цифрової пам'яті.Один D-тригер зберігає рівно один біт інформації:
Q = 0: Пам'ять зберігає "нуль"
Q = 1: Пам'ять зберігає "одиницю"
Побудова більших пам'ятей:
8 D-тригерів = 1 байт пам'яті
8 × 1024 = 8 192 D-тригери = 1 кілобайт
Сучасні комп'ютери містять мільярди комірок пам'яті
D-тригер забезпечує послідовну логіку — схеми, поведінка яких залежить від історії, а не лише від поточних входів.Приклад: Лічильник
З'єднайте кілька D-тригерів послідовно, де вихід кожного подається на наступний. Схема рахує тактові імпульси, запам'ятовуючи, скільки їх відбулося.Приклад: Регістр
Кілька D-тригерів зі спільним тактовим сигналом утворюють регістр — багатобітну пам'ять, що використовується для зберігання даних під час операцій процесора.Приклад: Статична пам'ять (SRAM)
Кеш-пам'ять ЦП використовує структури, подібні до тригерів. Кожна комірка пам'яті використовує 6 транзисторів для зберігання одного біта. Конфігурація зворотного зв'язку підтримує збережене значення, поки подається живлення.Розуміння D-тригера є важливим, оскільки:
Він показує, як логічні вентилі створюють пам'ять — якісно нову можливість
Він вводить тактові сигнали — серцебиття синхронізованих обчислень
Це реальний компонент у справжніх комп'ютерах — регістри ЦП є масивами тригерів
Він демонструє емерджентність — складна поведінка (пам'ять) з простих правил (логіка вентилів + зворотний зв'язок)
Повертаючись до нашого підходу від перших принципів: якщо обчислення є фізичними, тоді фізичні закони накладають фундаментальні обмеження на те, чого можуть досягти комп'ютери.Жоден сигнал не може поширюватися швидше за світло (приблизно 300 000 км/с). У практичних схемах:
Сигнали поширюються зі швидкістю 50-99% швидкості світла у провідниках
У процесорі з частотою 3 ГГц (3 мільярди циклів на секунду) світло проходить лише ~10 см за цикл
Це обмежує розмір чипів при збереженні синхронізації
За будь-якої температури вище абсолютного нуля атоми випадково коливаються. Цей тепловий рух створює електричний шум, який фундаментально обмежує точність сигналу.Енергетична шкала теплових флуктуацій становить kT (стала Больцмана × температура):
За кімнатної температури (300K): kT ≈ 0,026 еВ ≈ 4,1 × 10⁻²¹ джоулів
Сигнали мають бути значно більшими за цей тепловий шум, щоб їх можна було надійно розрізнити.У 1961 році фізик IBM Рольф Ландауер відкрив глибокий зв'язок між інформацією та термодинамікою:
Стирання одного біта інформації вимагає розсіювання щонайменше kT ln(2) енергії у вигляді тепла.
За кімнатної температури ця межа Ландауера становить приблизно:
0,018 еВ або 2,9 × 10⁻²¹ джоулів на стертий біт
Чому це важливо:
Це ФУНДАМЕНТАЛЬНА межа — жодна винахідливість не може її подолати
Сучасні комп'ютери використовують приблизно в 1 мільярд разів більше енергії, ніж ця межа
У міру продовження мініатюризації ми повільно наближаємося до межі Ландауера
Це пов'язує обробку інформації з другим законом термодинаміки
Фізичний зміст:
Коли ви стираєте біт, ви зменшуєте інформаційний вміст системи. Згідно з другим законом, це має збільшити ентропію десь інде — а саме, у вигляді тепла, розсіяного в навколишнє середовище.Усі реальні обчислення генерують тепло понад мінімум Ландауера через:
Опір у проводах (втрати I²R)
Перехідні процеси перемикання (заряджання/розряджання ємностей)
Струми витоку в транзисторах
Сучасні процесори розсіюють 50-150 ватів — достатньо, щоб обігріти невелику кімнату. Охолодження стало основним обмеженням продуктивності процесорів.Ці фізичні обмеження показують, що обчислення не є безкоштовними. Кожен розрахунок має фізичну вартість:
Час (обмежений поширенням сигналу)
Енергія (обмежена термодинамікою)
Простір (обмежений мініатюризацією)
Точність (обмежена шумом)
Розуміння цих обмежень допомагає пояснити, чому комп'ютери спроєктовані саме так — і які вдосконалення можуть бути можливими в майбутньому. Обчислення є фізичними Кожен розрахунок передбачає маніпулювання фізичними об'єктами
Інформація зберігається як фізичні стани
Метафора водяного комп'ютера наочно це ілюструє Електроніка перемогла завдяки фізиці Електромагнітні сигнали швидкі (близькі до швидкості світла)
Твердотільні пристрої не мають рухомих частин (надійні)
Транзистори можна мініатюризувати до нанометрових масштабів Двійкова система була обрана свідомо Два стани забезпечують завадостійкість
Двійкові схеми просто побудувати надійно
Булева алгебра забезпечує теоретичну основу Від транзисторів до логіки Транзистор = електронний перемикач (керований клапан)
Логічні вентилі = комбінації транзисторів
NOT, AND, OR, NAND, NOR, XOR — базові вентилі NAND є універсальним Будь-яку логічну функцію можна побудувати лише з вентилів NAND
Однієї «цеглинки» достатньо для довільної складності
Це є фундаментальним для проєктування цифрових схем Вентилі створюють пам'ять Зворотний зв'язок забезпечує стабільні стани
D-тригер зберігає один біт
Пам'ять є основою послідовних обчислень У Лекції 2: Від вентилів до ЦП ми:
Побудуємо складніші комбінаційні схеми (суматори, мультиплексори)
Зрозуміємо послідовну логіку (регістри, лічильники)
Побачимо, як тактові сигнали синхронізують операції
Отримаємо перше уявлення про те, як насправді працює ЦП
Цей курс дотримується підходу «знизу догори»:
Ми почали з фізики (чому електроніка? чому двійкова система?)
Ми будуємо через вентилі та пам'ять
Ми дійдемо до процесорів, ієрархій пам'яті та сучасних архітектур
Кожна концепція спирається на основи, які ми заклали
Це мислення від перших принципів, застосоване до архітектури комп'ютерів.Обов'язковий перегляд:<br>Перегляньте відео "<a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=QZwneRb-zqA" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=QZwneRb-zqA" target="_self">Exploring How Computers Work</a>" (джерело [11]). Це відео демонструє:
Як працюють логічні вентилі
Як вентилі об'єднуються для створення корисних схем
Як виглядають логічні діаграми
Діяльність:
Робота з симулятором логічних вентилів
Побудова базових логічних схем: Вентилі NOT, AND, OR
NAND як універсальний вентиль
Проста схема суматора Налаштування репозиторію GitHub для подання домашніх завдань
<br><img alt="Схема напівсуматора" src="assets/l01-half-adder.png" target="_self">Схема напівсуматора: вентиль XOR створює суму, вентиль AND створює перенос. Це те, що ви будуватимете на лабораторній.<br><img alt="Full-adder circuit" src="assets/l01-full-adder.png" target="_self"><br><img alt="Full-adder circuit" src="assets/l01-ripple-carry-adder-8bit.png" target="_self">Концепції для розуміння:
Таблиці істинності та як їх читати
Як з'єднувати вентилі для створення функцій
Зв'язок між вентилями та булевими виразами
Перед лабораторною роботою подумайте:
Якби у вас були лише вентилі NAND, як би ви створили вентиль OR?
Яка таблиця істинності для XOR? Чому він корисний для додавання?
Як можна перевірити, чи схема працює правильно?
Що станеться, якщо з'єднати два інвертори послідовно?
Попередня підготовка з концептуальним розумінням допоможе вам зосередитися на практичній роботі під час лабораторного заняття.Джерела:
[11] Exploring How Computers Work (YouTube) <br>[1] James Clear: <a data-tooltip-position="top" aria-label="https://jamesclear.com/first-principles" rel="noopener nofollow" class="external-link is-unresolved" href="https://jamesclear.com/first-principles" target="_self">First Principles: The Building Blocks of True Knowledge</a>
<br>[2] Farnam Street: <a data-tooltip-position="top" aria-label="https://fs.blog/first-principles/" rel="noopener nofollow" class="external-link is-unresolved" href="https://fs.blog/first-principles/" target="_self">First Principles: Elon Musk on the Power of Thinking for Yourself</a>
<br>[3] <a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=NV3sBlRgzTI" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=NV3sBlRgzTI" target="_self">Elon Musk explains First Principles</a> (YouTube) <br>[4a] <a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=IgF3OX8nT0w" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=IgF3OX8nT0w" target="_self">Water Integrator / Hydraulic Computer</a> (YouTube) — Метафора водяного комп'ютера
<br>[4b] <a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=TTK-vcwSm7s" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=TTK-vcwSm7s" target="_self">Marble Computer — Part 1</a> (YouTube) — Демонстрація механічної логіки
<br>[5] <a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=w6E7aQnA4Ws" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=w6E7aQnA4Ws" target="_self">Domino Computer</a> (YouTube) — Логічні вентилі з доміно <br>[6] <a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=O5nskjZ_GoI" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=O5nskjZ_GoI" target="_self">Early Computing: Crash Course Computer Science #1</a> (PBS/YouTube)
<br>[7] <a data-tooltip-position="top" aria-label="https://youtu.be/LN0ucKNX0hc?t=61" rel="noopener nofollow" class="external-link is-unresolved" href="https://youtu.be/LN0ucKNX0hc?t=61" target="_self">History of Computing</a> (YouTube)
<br>[8] <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Harvard_Mark_I" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Harvard_Mark_I" target="_self">Harvard Mark I</a> (Wikipedia) <br>[9] <a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=FU_YFpfDqqA" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=FU_YFpfDqqA" target="_self">Why The First Computers Were Made Out Of Light Bulbs</a> (Veritasium/YouTube)
<br>[10] <a data-tooltip-position="top" aria-label="https://youtu.be/gI-qXk7XojA?t=101" rel="noopener nofollow" class="external-link is-unresolved" href="https://youtu.be/gI-qXk7XojA?t=101" target="_self">Why Use Binary?</a> (YouTube) <br>[11] <a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=QZwneRb-zqA" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=QZwneRb-zqA" target="_self">Exploring How Computers Work</a> (YouTube) — Перегляньте перед лабораторним заняттям
Ця лекція відповідає Результату навчання 1 (РН1): Пояснювати принципи побудови та функціонування сучасних комп'ютерних архітектур.Зокрема, студенти тепер повинні вміти:
Пояснювати, чому обчислення є принципово фізичним процесом
Описувати, чому було обрано електронну реалізацію (швидкість, надійність, масштабованість)
Обґрунтовувати використання двійкової системи (завадостійкість, простота, основа булевої алгебри)
Пояснювати, як транзистори функціонують як перемикачі
Описувати функції базових логічних вентилів (NOT, AND, OR, NAND, NOR, XOR)
Пояснювати, чому NAND називають універсальним вентилем
Описувати, як D-тригери зберігають один біт інформації
Визначати фізичні обмеження, що впливають на обчислення
Інформація про курс:
Національний університет «Києво-Могилянська академія»
Факультет інформатики
Освітня програма: Комп'ютерні науки, бакалавр
Структура курсу:
Повний курс (КН, Програмна інженерія): 6 ECTS (Європейська кредитно-трансферна система) / 180 годин
Скорочений курс (АКІТР): 4 ECTS (Європейська кредитно-трансферна система) / 120 годин
«Я можу лише показати тобі двері. Пройти через них маєш ти сам.»
— Морфеус, Матриця (Вачовскі)]]></description><link>lectures/l01.-фізика-обчислень.html</link><guid isPermaLink="false">Lectures/L01. Фізика обчислень.md</guid><pubDate>Sat, 17 Jan 2026 21:50:47 GMT</pubDate><enclosure url="assets/l01-watersim-flow-on.jpg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l01-watersim-flow-on.jpg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L01-ENG. Physics of Computing]]></title><description><![CDATA[Course: Computer Systems Architecture
Topic: First Principles — Seeking the Foundation
Learning Outcome (LO1): Explain the principles of construction and functioning of modern computer architecturesFirst principles thinking is a problem-solving approach that involves breaking down complex problems to their most fundamental truths and building up from there. Rather than reasoning by analogy or accepting conventional wisdom, this method asks: "What do we know to be absolutely true? What has been proven?"The ancient Greek philosopher Aristotle defined a first principle as "the first basis from which a thing is known." In modern terms, it means identifying the foundational assumptions that cannot be deduced any further.When Elon Musk founded SpaceX in 2002, he faced a seemingly insurmountable challenge: rockets cost up to $65 million to purchase. Instead of accepting this as fact, Musk applied first principles thinking:He asked: "What is a rocket made of?" The answer: aerospace-grade aluminum alloys, titanium, copper, and carbon fiber. He then asked: "What is the value of those materials on the commodity market?" The surprising answer: the raw materials cost only about 2% of the typical rocket price.By reasoning from first principles rather than accepting industry pricing, Musk realized SpaceX could build rockets themselves at a fraction of the cost. Within a few years, SpaceX had cut launch prices by nearly 10x while still making a profit.The method can be summarized in four steps:
Decompose to fundamentals — Break down the problem to its basic, proven components
Question all assumptions — Don't accept "that's how it's always been done"
Rebuild from the ground up — Use only verified fundamentals as building blocks
Think like a scientist — Rely on logic and evidence, not tradition or analogy
This approach is essential for understanding computer architecture because we need to understand why computers work the way they do, not just that they work.Sources:
James Clear: First Principles - The Building Blocks of True Knowledge
Farnam Street: First Principles - Elon Musk on Thinking for Yourself
Elon Musk Explains First Principles (YouTube)
The key question that guides this entire lecture—and indeed this course—is deceptively simple:
"What is a computer, at its essence?"
Or more precisely:
How does the physical world allow us to compute?
To answer this, we must go beyond the familiar abstractions of software, apps, and user interfaces. We need to understand that every computation you've ever performed—every calculation, every search, every video stream—ultimately depends on physical processes occurring in the real world.A computer is not magic. It is a physical device that manipulates physical objects according to physical laws. Understanding this fundamental truth is the foundation upon which all computer science is built.Every piece of information stored in a computer exists as a physical state:
Electrical charges in a capacitor
Magnetic orientations on a hard drive platter
Patterns of light in a fiber optic cable
Voltage levels in a transistor
As physicist Rolf Landauer famously stated: "Information is physical." This isn't merely a philosophical observation—it's a fundamental principle that constrains what computers can and cannot do.Long before electronic computers, humans built calculating machines using physical manipulation:The Abacus (c. 2400 BCE): Perhaps the oldest computing device, the abacus performs calculations by sliding beads along rods. Each bead position represents a numerical value, and moving beads according to rules produces arithmetic results. The physical positions of beads are the stored data.Mechanical Calculators (17th-20th century): Devices like the Pascaline and Leibniz's Stepped Reckoner used gears, wheels, and mechanical linkages to perform calculations. Rotating a gear by a certain amount corresponds to adding or multiplying numbers.The key insight from these historical devices is that computation has always been about manipulating physical objects according to rules. Modern electronic computers are simply doing this same thing at an incredibly smaller scale and faster speed.One of the most illuminating ways to understand how computers work is to imagine building a computer using water pipes and valves. This "water computer" metaphor serves as a central conceptual tool throughout this course.The Water Computer Model:
Water flowing through a pipe = signal "1" (ON state)
No water flowing = signal "0" (OFF state)
Valve = logic gate (controls flow based on input)
Pipe junction/split = signal routing and parallel processing
This isn't just a teaching analogy—hydraulic computers have actually been built and used for solving differential equations, particularly in the Soviet Union in the mid-20th century.The water computer metaphor illustrates several critical concepts: Emergence: Complex behaviors emerge from simple rules. Just as sophisticated calculations can arise from water flowing through pipes and valves, complex computations emerge from simple transistor switching operations. Physical Constraints: The water computer has inherent limitations: Water pressure drops over distance
Pipes have flow resistance
Valves take time to open and close
Energy is lost to friction and heat These constraints mirror the real limitations in electronic computers: signal propagation delays, resistance, switching times, and power dissipation. The Analogy to Electronics: Electrons flowing through wires behave remarkably like water flowing through pipes: Voltage ≈ Water pressure
Current ≈ Flow rate
Resistance ≈ Pipe narrowness
Transistor ≈ Valve A valve controlled by an input signal—when the input is active, water flows through. This is exactly how a transistor controls electrical current.Marble Computers: Mechanical computers built using marbles rolling down tracks, with gates controlled by marble positions. These vividly demonstrate binary logic operations using purely mechanical means.Domino Computers: Logic gates can be implemented using falling dominoes. An AND gate requires both input dominoes to fall before triggering the output; an OR gate triggers if either input falls.These demonstrations reinforce the key insight: computation is a physical process that can be implemented in many different physical substrates.Sources:
[4a] Water Integrator / Hydraulic Computer (YouTube)
[4b] Marble Computer — Part 1 (YouTube)
Domino Computer (YouTube)
If computation is physical manipulation, why don't we use mechanical computers today? Why did electronic computers win?Consider the characteristics of different computing substrates:Electronic computers dominated for several fundamental reasons:Speed: Electrical signals travel at approximately 50-99% the speed of light through conductors. In a computer chip, signals propagate at roughly 15-30 centimeters per nanosecond (billionth of a second). Mechanical components cannot approach these speeds.No Moving Parts: Unlike gears that wear out or water that leaks, electrons flowing through solid-state devices experience no mechanical fatigue. A transistor can switch trillions of times without degradation.Miniaturization: Modern transistors are measured in nanometers—billionths of a meter. A single processor chip contains billions of transistors. No mechanical technology can achieve this density.Energy Efficiency: While still consuming significant power, electronic switching is far more efficient than mechanical motion. Moving electrons is easier than moving atoms.The answer to "why electronic computers?" is therefore: speed, reliability, and scale. Electrons can be manipulated faster, more reliably, and at smaller scales than any mechanical alternative.Understanding the history of computing reveals how we arrived at modern computer architecture and helps explain why current designs make sense.Inventor: Gottfried Wilhelm Leibniz, German mathematician and philosopher<img alt="Leibniz's Stepped Reckoner" src="assets/l01-leibniz-calculator.png" target="_self">Leibniz's Stepped Reckoner—the first calculator capable of all four arithmetic operations.The Stepped Reckoner was the first calculator capable of performing all four basic arithmetic operations: addition, subtraction, multiplication, and division. Leibniz built upon Blaise Pascal's earlier Pascaline calculator, which could only add and subtract.Key Features:
Used a decimal number system (base-10)
Operated by turning a hand crank
Addition and subtraction: one crank turn
Multiplication and division: repeated additions/subtractions
Innovation: The Stepped Reckoner introduced the "Leibniz wheel"—a cylinder with teeth of varying lengths. This mechanism allowed a single gear to represent any digit from 0-9 in one revolution. This design dominated calculator construction for the next 275 years.Historical Significance: Leibniz was also a strong advocate of the binary number system, recognizing its potential for mechanical calculation: "Binary numbers are ideal for machines because they require only two digits, which can easily be represented by the on and off states of a switch." However, his calculator used decimal, not binary, because binary would require more digits and thus more mechanical complexity.Inventor: Charles Babbage, English mathematician<br><img alt="Babbage's Difference Engine" src="assets/l01-babbage-difference-engine.jpeg" target="_self">Detail of Babbage's Difference Engine showing the brass number wheels and gear mechanisms.Babbage designed the Difference Engine to automate the calculation of polynomial functions, specifically to produce error-free mathematical tables for navigation and engineering.The Problem Being Solved: In the early 19th century, mathematical tables (logarithms, trigonometry, astronomical positions) were calculated by hand and often contained errors. Ships navigating by faulty tables could become lost or shipwrecked. Babbage sought to mechanize table production to eliminate human error.Key Features:
Mechanical computer using gears and levers
Could calculate polynomial functions using the method of finite differences
Designed to print results directly, avoiding transcription errors
Outcome: Babbage never completed a full-scale Difference Engine during his lifetime due to manufacturing limitations and funding issues. However, a working model was eventually constructed in 1991 using his original designs, proving the concept was sound.Babbage's later design, the Analytical Engine, was a revolutionary leap—essentially a design for a general-purpose programmable computer, over 100 years before ENIAC.Features anticipating modern computers:
Separate "store" (memory) and "mill" (processor)
Punch card input (borrowed from Jacquard looms)
Conditional branching (if-then logic)
Loops (iteration)
Ada Lovelace, who worked with Babbage, wrote detailed notes describing how the Analytical Engine could be programmed, making her arguably the first computer programmer.Context: Developed during World War II at Harvard University with IBM fundingThe Harvard Mark I was an electromechanical computer—it used electrical components to control mechanical relays.Key Features:
Base-10 (decimal) arithmetic
51 feet long, 8 feet high, weighing 5 tons
Could perform 3 additions per second
Used for calculations supporting the Manhattan Project
Historical Significance: John von Neumann used the Harvard Mark I for calculations related to nuclear weapons development. The machine represented the culmination of mechanical computing before electronics took over.Sources:
Early Computing: Crash Course Computer Science #1 (PBS/YouTube)
History of Computing (YouTube)
Harvard Mark I (Wikipedia)
<br><img alt="Collection of vacuum tubes" src="assets/l01-vacuum-tubes-collection.jpg" target="_self">Various vacuum tubes used in early computers—the electronic switches that preceded transistors.How Vacuum Tubes Work:A vacuum tube is an electronic device that controls the flow of electrons through a vacuum. The basic principle is thermionic emission: when a metal is heated, some electrons gain enough energy to escape the metal surface.A triode vacuum tube has three elements:
Cathode — Heated filament that emits electrons
Anode (Plate) — Positively charged electrode that attracts electrons
Grid — A wire mesh between cathode and anode that controls electron flow
By applying a small voltage to the grid, you can control a much larger current flowing from cathode to anode. This allows vacuum tubes to amplify signals or act as switches.Vacuum Tubes in Computing:Vacuum tubes enabled the first electronic computers because they could switch between on and off states without mechanical motion—electrons could be controlled purely electrically.ENIAC (1945):
First general-purpose electronic computer
Used approximately 18,000 vacuum tubes
Could perform 5,000 additions per second (1,000× faster than electromechanical machines)
Weighed 30 tons
Consumed 150 kilowatts of power
Required its own electrical generator
Left: Two operators programming ENIAC by connecting patch cables. Right: The glowing vacuum tube panels of ENIAC, showing the scale of tube-based computing.Problems with Vacuum Tubes:
Failure rate: Vacuum tubes burn out like light bulbs. ENIAC experienced tube failures every few hours, requiring constant maintenance.
Size: Each tube was roughly the size of a light bulb. Building complex circuits required enormous space.
Power consumption: Tubes require significant power to heat the cathode, generating substantial heat.
Heat dissipation: All that power became heat that needed to be removed, requiring extensive cooling.
The ENIAC Experience:Operating ENIAC was challenging. The machine's 18,000 tubes meant that statistically, a tube would fail every few hours. Tommy Flowers, who built the Colossus computer (a secret British codebreaking machine), discovered that keeping tubes powered on continuously actually improved reliability—tubes failed most often during power cycling.Despite these challenges, vacuum tube computers proved the viability of electronic computing and demonstrated that electronic speeds would always outpace mechanical alternatives.The Invention (1947):The transistor was invented at Bell Labs by William Shockley, John Bardeen, and Walter Brattain. The word "transistor" combines "transfer" and "resistor," reflecting how the device transfers resistance from one terminal to another.<br><img alt="Transistors of various sizes" src="assets/l01-transistors-various-sizes.jpg" target="_self">Transistors from different eras: modern surface-mount devices (left), through-hole transistors (center), and power transistors (right). Note the dramatic size reduction over time.Why Transistors Won:Key Advantages:
Solid-state: No vacuum to maintain, no fragile glass envelope
No warm-up time: Works instantly when powered
Miniaturization: Can be made incredibly small
Low power: Orders of magnitude less than tubes
Reliability: No filament to burn out
Left: Electron microscope image of a transistor at 5μm scale. Right: Modern CPU die showing billions of transistors organized into functional blocks (cache, logic units) in false color.The Transition:The first fully transistorized computer, the TX-0, appeared in 1956. By the 1960s, transistors had essentially replaced vacuum tubes in computing. This transition enabled the miniaturization that eventually led to personal computers, smartphones, and the modern digital world.Sources:
History of Computing (YouTube)
Why The First Computers Were Made Out Of Light Bulbs (Veritasium/YouTube)
If transistors can theoretically distinguish multiple voltage levels (0V, 1V, 2V, 3V...), why do computers use only two states? Why limit ourselves to binary?1. Noise ImmunityIn real circuits, electrical signals are never perfectly clean. Noise from power supplies, electromagnetic interference, thermal fluctuations, and other sources constantly perturbs voltage levels.Consider a system trying to distinguish 10 voltage levels (0-9):
In a 5V system, each level represents 0.5V
A noise spike of just 0.3V could cause a misread
Error rates would be unacceptably high
Binary systems have much larger margins:
Only two states: typically 0-1V represents "0" and 3-5V represents "1"
The 2V "forbidden zone" in between absorbs noise
A signal can drift significantly without changing meaning
This is why binary signals have noise immunity—small variations don't affect interpretation. As long as the signal stays in the correct voltage band, the binary value remains clear.2. Simple ImplementationCreating a circuit that reliably distinguishes two states is far easier than creating one that distinguishes many states:
Two states requires one threshold comparator
N states requires N-1 precisely calibrated comparators
Manufacturing variations make precise multi-level circuits difficult
With binary, a transistor is either:
Cut-off: No current flows (represents "0")
Saturation: Maximum current flows (represents "1")
There's no need to operate in the linear (amplifying) region, which simplifies design enormously.3. Boolean Algebra FoundationBinary computing has a ready-made mathematical foundation: Boolean algebra, developed by George Boole in the 1840s—over a century before electronic computers.Boolean algebra provides:
Rigorous logical operations: AND, OR, NOT
Algebraic laws for simplifying expressions
Completeness theorems proving any logical function can be built from basic operations
Established proof techniques for verifying circuit correctness
Claude Shannon's 1937 master's thesis, "A Symbolic Analysis of Relay and Switching Circuits," demonstrated that Boolean algebra could be directly implemented in electrical circuits. This provided the theoretical foundation for digital computer design.In our water computer metaphor, binary makes even more sense:
Water flowing = 1
No water flowing = 0
Trying to distinguish "half flowing" or "quarter flowing" would be unreliable—valves drip, pressure varies, flow is turbulent. But "flowing vs. not flowing" is a robust distinction that survives real-world imperfections.Sources:
[10] Why Use Binary? (YouTube)
Recall our water computer metaphor. A valve controls water flow:
Valve open: Water flows through
Valve closed: Water is blocked
A transistor does the same thing with electrical current:
Transistor on (saturation): Current flows from collector to emitter
Transistor off (cutoff): No current flows
The crucial insight is that a transistor uses one electrical signal to control another—just as our valve uses one mechanism (turning a handle) to control another (water flow).The most common transistor type for understanding digital logic is the NPN bipolar junction transistor:<br><img alt="BJT transistor symbol and structure" src="assets/l01-transistor-diagram-symbol.png" target="_self">Schematic symbol for an NPN transistor with its three terminals labeled, alongside a physical transistor package.Three terminals:
Base (B): Control input — a small current here controls the main current
Collector (C): Main current enters here
Emitter (E): Main current exits here
Operating Principle:
When zero current flows into the base, no current flows from collector to emitter (transistor is OFF)
When sufficient current flows into the base, current can flow from collector to emitter (transistor is ON)
A small base current controls a much larger collector-emitter current
Digital Operation:For digital circuits, transistors operate in just two modes:Cutoff Mode (OFF/Logic 0):
Base voltage is below threshold (~0.7V for silicon)
No base current flows
Transistor behaves like an open switch
No current flows from collector to emitter
Collector voltage is high (connected to power supply)
Saturation Mode (ON/Logic 1):
Base voltage exceeds threshold
Sufficient base current flows
Transistor behaves like a closed switch
Maximum current flows from collector to emitter
Collector voltage drops nearly to zero
<br><img alt="Transistor as a switch" src="assets/l01-transistor-as-switch.png" target="_self">A transistor operating as a switch: when Base=0 (left), no current flows and the output is high; when Base=1 (right), current flows and the output goes low.In our hydraulic analogy:
Base current = Opening force on the valve
Collector-Emitter path = Main water pipe
Cutoff = Valve fully closed
Saturation = Valve fully open
Just as a small force on a valve handle can control a large water flow, a small base current controls a large collector-emitter current. This is the amplification property that makes transistors so useful.Transistors can also operate in an "active" region where they amplify analog signals proportionally. But for digital logic, we deliberately avoid this region:
We only want two states: 0 and 1
Active region is noise-sensitive: Small input changes cause output changes
Binary operation is robust: Either fully on or fully off
This deliberate restriction—treating a sophisticated analog device as a simple on/off switch—is what makes reliable digital computation possible.A logic gate is a circuit that performs a Boolean logic operation on one or more binary inputs to produce a single binary output. Logic gates are the fundamental building blocks from which all digital circuits are constructed.Function: Outputs the opposite of its inputTruth Table:
| Input | Output |
|-------|--------|
| 0 | 1 |
| 1 | 0 |<br><img alt="NOT gate implementation" src="assets/l01-gate-not.png" target="_self">NOT gate: transistor implementation with Vcc power supply, logic symbol, and truth table.Implementation: The simplest logic gate. When input is high, the transistor turns on, connecting output to ground (low). When input is low, the transistor is off, and output is pulled high through a resistor.Water Analogy: A "normally open" valve that lets water through when not pressed, but closes when the control signal is applied.Function: Outputs 1 only when ALL inputs are 1Truth Table (2-input):
| A | B | Output |
|---|---|--------|
| 0 | 0 | 0 |
| 0 | 1 | 0 |
| 1 | 0 | 0 |
| 1 | 1 | 1 |Water Analogy: Two valves in series—water flows through only if BOTH valves are open.Function: Outputs 1 when ANY input is 1Truth Table (2-input):Water Analogy: Two valves in parallel—water flows if EITHER valve is open (or both).Function: NOT-AND — outputs 0 only when ALL inputs are 1Truth Table (2-input):<br><img alt="NAND gate implementation" src="assets/l01-gate-nand.png" target="_self">NAND gate: transistor implementation, logic symbol, and truth table. This is the universal gate from which all other gates can be built.This is simply an AND gate followed by a NOT gate.Function: NOT-OR — outputs 1 only when ALL inputs are 0Truth Table (2-input):Function: Outputs 1 when inputs are DIFFERENTTruth Table (2-input):XOR is essential for arithmetic operations—it forms the basis of binary addition.Here is one of the most profound facts in digital logic:
Any Boolean function whatsoever can be implemented using only NAND gates.
This means that with just one type of component—the NAND gate—you can build any computer, any processor, any digital circuit imaginable. NAND is called a "universal gate" because of this remarkable property.NOT from NAND:
Simply connect both inputs of a NAND gate together:
If input = 0: NAND(0,0) = 1
If input = 1: NAND(1,1) = 0
This is exactly the NOT function!AND from NAND:
Use two NAND gates:
First NAND: compute NAND(A,B)
Second NAND (as NOT): compute NOT(NAND(A,B)) = AND(A,B)
OR from NAND:
Use three NAND gates:
First NAND (as NOT): compute NOT(A)
Second NAND (as NOT): compute NOT(B)
Third NAND: compute NAND(NOT(A), NOT(B)) = OR(A,B)
(This uses De Morgan's law: A OR B = NOT(NOT(A) AND NOT(B)))XOR from NAND:
Can be built with just 4 NAND gates through clever arrangement.For Theory:
NAND universality proves that one fundamental operation is sufficient for all computation. This is analogous to how a small set of axioms can generate all of mathematics.For Practice:
Manufacturing becomes simpler when only one gate type is needed. In real integrated circuits, engineers often implement everything using NAND (or NOR, which is also universal) because:
Standardized manufacturing processes
Simplified testing procedures
Reduced inventory of components
Easier optimization
For This Course:
Understanding that NAND gates alone can build anything—arithmetic circuits, memory, processors—provides a concrete foundation for understanding computer architecture. Everything complex is built from this one simple brick.Think of NAND gates as LEGO bricks. With enough identical LEGO bricks, you can build castles, vehicles, bridges—anything. Similarly, with enough NAND gates, you can build adders, multipliers, memory cells, entire CPUs.This is the power of digital abstraction: from one simple component, arbitrary complexity emerges.Logic gates we've discussed so far are "combinational"—their outputs depend only on current inputs. But computers need memory: the ability to store information and recall it later.How do we make a circuit "remember"?Memory emerges from feedback—connecting a gate's output back to its input. This creates stable states that persist even when inputs change.The Set-Reset (SR) latch is the most basic memory element, built from two NOR gates (or two NAND gates) with cross-coupled feedback:
Two stable states: Q=0 or Q=1
Set (S=1, R=0): Forces Q to 1
Reset (S=0, R=1): Forces Q to 0
Hold (S=0, R=0): Maintains current state
The feedback connections create bistability—the circuit naturally settles into one of two states and stays there until pushed to the other.The D (Data) flip-flop adds crucial features to basic latching:Inputs:
D (Data): The value to be stored
CLK (Clock): Controls WHEN the value is stored
Outputs:
Q: The stored value
Q̄ (Q-bar): The complement of Q
Operation:
When clock is inactive: Output Q maintains its previous value regardless of D
At the clock edge (rising or falling): Output Q takes on whatever value D has at that moment
After the clock edge: Q holds this value until the next clock edge
Left: D flip-flop circuit built from NAND gates with D (data) and E (enable/clock) inputs. Right: Truth table showing how output Q follows input D when enabled.The Critical Feature: The D flip-flop captures the input at a specific instant (the clock edge) and holds that value until the next capture. This is the foundation of synchronized digital memory.A single D flip-flop stores exactly one bit of information:
Q = 0: The memory stores "zero"
Q = 1: The memory stores "one"
Building Larger Memories:
8 D flip-flops = 1 byte of memory
8 × 1024 = 8,192 D flip-flops = 1 kilobyte
Modern computers contain billions of memory cells
The D flip-flop enables sequential logic—circuits whose behavior depends on history, not just current inputs.Example: A Counter
Connect several D flip-flops in sequence, each one's output feeding the next. The circuit counts clock pulses, remembering how many have occurred.Example: A Register
Multiple D flip-flops with shared clock form a register—a multi-bit memory used to hold data during processor operations.Example: Static RAM (SRAM)
CPU cache memory uses flip-flop-like structures. Each memory cell uses 6 transistors to store one bit. The feedback configuration maintains the stored value as long as power is supplied.Understanding the D flip-flop is essential because:
It shows how logic gates create memory — a qualitatively new capability
It introduces clock signals — the heartbeat of synchronized computation
It's the actual component in real computers — CPU registers are arrays of flip-flops
It demonstrates emergence — complex behavior (memory) from simple rules (gate logic + feedback)
Returning to our first principles approach: if computation is physical, then physical laws impose fundamental limits on what computers can achieve.No signal can propagate faster than light (approximately 300,000 km/s). In practical circuits:
Signals travel at 50-99% of light speed in conductors
In a 3 GHz processor (3 billion cycles/second), light travels only ~10 cm per cycle
This constrains how large chips can be while maintaining synchronization
At any temperature above absolute zero, atoms vibrate randomly. This thermal motion creates electrical noise that fundamentally limits signal precision.The energy scale of thermal fluctuations is kT (Boltzmann's constant × temperature):
At room temperature (300K): kT ≈ 0.026 eV ≈ 4.1 × 10⁻²¹ joules
Signals must be significantly larger than this thermal noise to be reliably distinguished.In 1961, IBM physicist Rolf Landauer discovered a profound connection between information and thermodynamics:
Erasing one bit of information requires dissipating at least kT ln(2) energy as heat.
At room temperature, this Landauer limit is approximately:
0.018 eV or 2.9 × 10⁻²¹ joules per bit erased
Why This Matters:
This is a FUNDAMENTAL limit—no cleverness can overcome it
Current computers use ~1 billion times more energy than this limit
As miniaturization continues, we're slowly approaching Landauer's bound
This connects information processing to the second law of thermodynamics
The Physical Meaning:
When you erase a bit, you're reducing the information content of the system. By the second law, this must increase entropy somewhere else—specifically, as heat dissipated to the environment.All real computation generates heat beyond the Landauer minimum due to:
Resistance in wires (I²R losses)
Switching transients (charging/discharging capacitances)
Leakage currents in transistors
Modern processors dissipate 50-150 watts—enough to heat a small room. Cooling has become a major constraint on processor performance.These physical limits reveal that computation is not free. Every calculation has physical costs:
Time (limited by signal propagation)
Energy (limited by thermodynamics)
Space (limited by miniaturization)
Precision (limited by noise)
Understanding these limits helps explain why computers are designed the way they are—and what improvements might be possible in the future. Computation is Physical Every calculation involves manipulating physical objects
Information is stored as physical states
The water computer metaphor illustrates this clearly Electronics Won Because of Physics Electromagnetic signals are fast (near light-speed)
Solid-state devices have no moving parts (reliable)
Transistors can be miniaturized to nanometer scales Binary Was Chosen Deliberately Two states provide noise immunity
Binary circuits are simple to build reliably
Boolean algebra provides theoretical foundation From Transistors to Logic Transistor = electronic switch (controlled valve)
Logic gates = combinations of transistors
NOT, AND, OR, NAND, NOR, XOR are basic gates NAND is Universal Any logic function can be built from NAND gates alone
One "brick" is sufficient for arbitrary complexity
This is fundamental to digital circuit design Gates Create Memory Feedback enables stable states
D flip-flop stores one bit
Memory is the foundation of sequential computation In Lecture 2: From Gates to CPU, we will:
Build more complex combinational circuits (adders, multiplexers)
Understand sequential logic (registers, counters)
See how clock signals synchronize operations
Get our first glimpse of how a CPU actually works
This course follows a "bottom-up" approach:
We started with physics (why electronic? why binary?)
We're building up through gates and memory
We'll reach processors, memory hierarchies, and modern architectures
Every concept rests on foundations we've established
This is First Principles thinking applied to computer architecture.Required Viewing:<br>Watch the video "<a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=QZwneRb-zqA" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=QZwneRb-zqA" target="_self">Exploring How Computers Work</a>" (source [11]). This video demonstrates:
How logic gates function
How gates combine to form useful circuits
The visual appearance of logic diagrams
Activities:
Work with a logic gate simulator
Build basic logic circuits: NOT, AND, OR gates
NAND as universal gate
Simple adder circuit Set up GitHub repository for homework submissions
<br><img alt="Half-adder circuit" src="assets/l01-half-adder.png" target="_self">A half-adder circuit: XOR gate produces the Sum, AND gate produces the Carry. This is what you'll build in the lab.<br><img alt="Full-adder circuit" src="assets/l01-full-adder.png" target="_self"><br><img alt="Full-adder circuit" src="assets/l01-ripple-carry-adder-8bit.png" target="_self">Concepts to Understand:
Truth tables and how to read them
How to connect gates to create functions
The relationship between gates and Boolean expressions
Before the lab, think about:
If you had only NAND gates, how would you create an OR gate?
What's the truth table for XOR? Why is it useful for addition?
How could you test whether a circuit is working correctly?
What happens if you connect two inverters in series?
Being prepared with conceptual understanding will help you focus on the hands-on work during the lab.Sources:
[11] Exploring How Computers Work (YouTube) <br>[1] James Clear: <a data-tooltip-position="top" aria-label="https://jamesclear.com/first-principles" rel="noopener nofollow" class="external-link is-unresolved" href="https://jamesclear.com/first-principles" target="_self">First Principles: The Building Blocks of True Knowledge</a>
<br>[2] Farnam Street: <a data-tooltip-position="top" aria-label="https://fs.blog/first-principles/" rel="noopener nofollow" class="external-link is-unresolved" href="https://fs.blog/first-principles/" target="_self">First Principles: Elon Musk on the Power of Thinking for Yourself</a>
<br>[3] <a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=NV3sBlRgzTI" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=NV3sBlRgzTI" target="_self">Elon Musk explains First Principles</a> (YouTube) <br>[4a] <a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=IgF3OX8nT0w" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=IgF3OX8nT0w" target="_self">Water Integrator / Hydraulic Computer</a> (YouTube) — The water computer metaphor
<br>[4b] <a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=TTK-vcwSm7s" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=TTK-vcwSm7s" target="_self">Marble Computer — Part 1</a> (YouTube) — Mechanical logic demonstration
<br>[5] <a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=w6E7aQnA4Ws" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=w6E7aQnA4Ws" target="_self">Domino Computer</a> (YouTube) — Logic gates from dominoes <br>[6] <a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=O5nskjZ_GoI" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=O5nskjZ_GoI" target="_self">Early Computing: Crash Course Computer Science #1</a> (PBS/YouTube)
<br>[7] <a data-tooltip-position="top" aria-label="https://youtu.be/LN0ucKNX0hc?t=61" rel="noopener nofollow" class="external-link is-unresolved" href="https://youtu.be/LN0ucKNX0hc?t=61" target="_self">History of Computing</a> (YouTube)
<br>[8] <a data-tooltip-position="top" aria-label="https://en.wikipedia.org/wiki/Harvard_Mark_I" rel="noopener nofollow" class="external-link is-unresolved" href="https://en.wikipedia.org/wiki/Harvard_Mark_I" target="_self">Harvard Mark I</a> (Wikipedia) <br>[9] <a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=FU_YFpfDqqA" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=FU_YFpfDqqA" target="_self">Why The First Computers Were Made Out Of Light Bulbs</a> (Veritasium/YouTube)
<br>[10] <a data-tooltip-position="top" aria-label="https://youtu.be/gI-qXk7XojA?t=101" rel="noopener nofollow" class="external-link is-unresolved" href="https://youtu.be/gI-qXk7XojA?t=101" target="_self">Why Use Binary?</a> (YouTube) <br>[11] <a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=QZwneRb-zqA" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=QZwneRb-zqA" target="_self">Exploring How Computers Work</a> (YouTube) — Watch before lab session
This lecture addresses Learning Outcome 1 (LO1): Explain the principles of construction and functioning of modern computer architectures.Specifically, students should now be able to:
Explain why computation is fundamentally a physical process
Describe why electronic implementation was chosen (speed, reliability, scale)
Justify why binary is used (noise immunity, simplicity, Boolean algebra foundation)
Explain how transistors function as switches
Describe the function of basic logic gates (NOT, AND, OR, NAND, NOR, XOR)
Explain why NAND is called a universal gate
Describe how D flip-flops store one bit of information
Identify the physical limits constraining computation
Course Information:
National University of Kyiv-Mohyla Academy
Faculty of Informatics
Educational Program: Computer Science, Bachelor's
Course Structure:
Full Course (CS, Software Engineering): 6 ECTS / 180 hours
Shortened Course (AKITR): 4 ECTS / 120 hours
"I can only show you the door. You're the one that has to walk through it."
— Morpheus, The Matrix (Wachowskis)]]></description><link>lectures/eng/l01-eng.-physics-of-computing.html</link><guid isPermaLink="false">Lectures/ENG/L01-ENG. Physics of Computing.md</guid><pubDate>Sat, 17 Jan 2026 21:50:47 GMT</pubDate><enclosure url="assets/l01-watersim-flow-on.jpg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l01-watersim-flow-on.jpg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L01-NOTES. Фізика обчислень]]></title><description><![CDATA[Короткий довідник до Лекції 1. Повні матеріали див. у ENG/L01-ENG. Physics of Computing.md.Розкладіть проблеми на фундаментальні істини, потім відбудуйте. Чотири кроки:
Декомпозиція до фундаментальних основ
Ставте під сумнів усі припущення
Відбудуйте з нуля
Думайте як науковець
Приклад: Ракети SpaceX — сировина коштує 2% від типової ціни ракети. Мислення від перших принципів дозволило знизити вартість у 10 разів.Дізнатися більше:
<a data-tooltip-position="top" aria-label="https://jamesclear.com/first-principles" rel="noopener nofollow" class="external-link is-unresolved" href="https://jamesclear.com/first-principles" target="_self">First Principles: The Building Blocks of True Knowledge</a>
<br><a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=NV3sBlRgzTI" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=NV3sBlRgzTI" target="_self">Elon Musk explains First Principles</a> (YouTube) «Інформація є фізичною.» — Рольф Ландауер
Кожен біт існує як фізичний стан: заряди, магнітні орієнтації, рівні напруги. Комп'ютер маніпулює фізичними об'єктами згідно з фізичними законами.Історичні приклади: Абак (кісточки), Паскаліна (шестерні), калькулятор Лейбніца.Центральна аналогія для цього курсу:Гідравлічні комп'ютери справді були побудовані (Радянський Союз, середина XX століття).Дізнатися більше:
<br><a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=IgF3OX8nT0w" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=IgF3OX8nT0w" target="_self">Water Integrator / Hydraulic Computer</a> (YouTube)
<br><a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=TTK-vcwSm7s" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=TTK-vcwSm7s" target="_self">Marble Computer</a> (YouTube)
<br><a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=w6E7aQnA4Ws" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=w6E7aQnA4Ws" target="_self">Domino Computer</a> (YouTube)
Відповідь: Швидкість, надійність і масштаб. Електронами можна маніпулювати швидше та в менших масштабах, ніж будь-якою механічною альтернативою.Ада Лавлейс писала програми для Аналітичної машини — можливо, перший програміст.Дізнатися більше:
<br><a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=O5nskjZ_GoI" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=O5nskjZ_GoI" target="_self">Early Computing: Crash Course CS #1</a> (YouTube)
<br><a data-tooltip-position="top" aria-label="https://youtu.be/LN0ucKNX0hc?t=61" rel="noopener nofollow" class="external-link is-unresolved" href="https://youtu.be/LN0ucKNX0hc?t=61" target="_self">History of Computing</a> (YouTube)
Вакуумні лампи (ENIAC, 1945): 18 000 ламп, 30 тонн, 150 кВт, виходили з ладу кожні кілька годин.Транзистори (1947, Bell Labs): Твердотільні, без часу прогрівання, мініатюризовані, надійні.Дізнатися більше:
<br><a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=FU_YFpfDqqA" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=FU_YFpfDqqA" target="_self">Why The First Computers Were Made Out Of Light Bulbs</a> (Veritasium)
Три причини:
Завадостійкість — Великий проміжок напруги між 0 та 1 поглинає шум
Прості схеми — Потрібен лише один пороговий компаратор
Булева алгебра — Математичний фундамент від Джорджа Буля (1840-ві), застосований до схем Клодом Шенноном (1937)
У термінах води: «тече чи не тече» — надійно. «Наполовину тече» — ні.Дізнатися більше:
<br><a data-tooltip-position="top" aria-label="https://youtu.be/gI-qXk7XojA?t=101" rel="noopener nofollow" class="external-link is-unresolved" href="https://youtu.be/gI-qXk7XojA?t=101" target="_self">Why Use Binary?</a> (YouTube)
Біполярний транзистор = клапан, керований напругою
База (B): Керуючий вхід
Колектор (C): Вхід струму
Емітер (E): Вихід струму
Лише два режими:
Відсічення (OFF): Струм не тече → Логічний 0
Насичення (ON): Максимальний струм тече → Логічна 1
<br><img alt="Транзистор як перемикач" src="assets/l01-transistor-as-switch.png" target="_self">
Будь-яка булева функція може бути побудована лише з вентилів NAND.
<br><img alt="Вентиль NAND" src="assets/l01-gate-nand.png" target="_self">
NOT з NAND: З'єднайте обидва входи разом
AND з NAND: NAND → NOT
OR з NAND: NOT(A), NOT(B) → NAND
Однієї «цеглинки» достатньо для довільної складності.Проблема: Вентилі є комбінаційними (вихід залежить лише від поточного входу). Як запам'ятовувати?Рішення: Зворотний зв'язок створює стабільні стани.D-тригер:
D (Data, дані): Значення для збереження
CLK (Clock, тактовий сигнал): Коли захоплювати
Q: Збережений вихід
<br><img alt="Схема D-тригера" src="assets/l01-d-flip-flop.png" target="_self">Захоплює D на фронті тактового сигналу, утримує до наступного фронту. Це 1-бітна пам'ять.8 тригерів = 1 байт. Сучасні комп'ютери мають мільярди.Ключовий висновок: Обчислення не є безкоштовними. Кожен розрахунок має фізичну вартість.
Обчислення — це фізичні маніпуляції
Електроніка перемогла: швидкість, надійність, масштаб
Двійкова система: завадостійкість + булева алгебра
Транзистор = електронний перемикач
Лише NAND може побудувати будь-що
Зворотний зв'язок створює пам'ять (D-тригер)
Фізика накладає фундаментальні обмеження
<br>Обов'язковий перегляд: <a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=QZwneRb-zqA" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=QZwneRb-zqA" target="_self">Exploring How Computers Work</a>Ви будете:
Використовувати симулятор логічних вентилів
Будувати схеми NOT, AND, OR, NAND
Будувати простий суматор
Подумайте:
Як створити OR лише з вентилів NAND?
Яка таблиця істинності для XOR? Чому корисна для додавання?
]]></description><link>lectures/l01-notes.-фізика-обчислень.html</link><guid isPermaLink="false">Lectures/L01-NOTES. Фізика обчислень.md</guid><pubDate>Sat, 17 Jan 2026 21:26:05 GMT</pubDate><enclosure url="assets/l01-watersim-flow-on.jpg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l01-watersim-flow-on.jpg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L01-WaterSim-Flow-On]]></title><description><![CDATA[<img src="assets/l01-watersim-flow-on.jpg" target="_self">]]></description><link>assets/l01-watersim-flow-on.html</link><guid isPermaLink="false">assets/L01-WaterSim-Flow-On.jpg</guid><pubDate>Sat, 17 Jan 2026 21:23:43 GMT</pubDate><enclosure url="assets/l01-watersim-flow-on.jpg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l01-watersim-flow-on.jpg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L01-NOTES-ENG. Physics of Computing]]></title><description><![CDATA[Quick reference for Lecture 1. See full materials in L01-ENG. Physics of Computing.md.Break problems down to fundamental truths, then rebuild. Four steps:
Decompose to fundamentals
Question all assumptions
Rebuild from ground up
Think like a scientist
Example: SpaceX rockets — raw materials cost 2% of typical rocket price. Reasoning from first principles enabled 10× cost reduction.Learn more:
<a data-tooltip-position="top" aria-label="https://jamesclear.com/first-principles" rel="noopener nofollow" class="external-link is-unresolved" href="https://jamesclear.com/first-principles" target="_self">First Principles: The Building Blocks of True Knowledge</a>
<br><a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=NV3sBlRgzTI" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=NV3sBlRgzTI" target="_self">Elon Musk explains First Principles</a> (YouTube) "Information is physical." — Rolf Landauer
Every bit exists as a physical state: charges, magnetic orientations, voltage levels. A computer manipulates physical objects according to physical laws.Historical examples: Abacus (beads), Pascaline (gears), Leibniz's Stepped Reckoner.Central analogy for this course:Hydraulic computers were actually built (Soviet Union, mid-20th century).Learn more:
<br><a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=IgF3OX8nT0w" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=IgF3OX8nT0w" target="_self">Water Integrator / Hydraulic Computer</a> (YouTube)
<br><a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=TTK-vcwSm7s" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=TTK-vcwSm7s" target="_self">Marble Computer</a> (YouTube)
<br><a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=w6E7aQnA4Ws" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=w6E7aQnA4Ws" target="_self">Domino Computer</a> (YouTube)
Answer: Speed, reliability, and scale. Electrons can be manipulated faster and smaller than any mechanical alternative.Ada Lovelace wrote programs for the Analytical Engine — arguably the first programmer.Learn more:
<br><a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=O5nskjZ_GoI" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=O5nskjZ_GoI" target="_self">Early Computing: Crash Course CS #1</a> (YouTube)
<br><a data-tooltip-position="top" aria-label="https://youtu.be/LN0ucKNX0hc?t=61" rel="noopener nofollow" class="external-link is-unresolved" href="https://youtu.be/LN0ucKNX0hc?t=61" target="_self">History of Computing</a> (YouTube)
Vacuum tubes (ENIAC, 1945): 18,000 tubes, 30 tons, 150 kW, failed every few hours.Transistors (1947, Bell Labs): Solid-state, no warm-up, miniaturizable, reliable.Learn more:
<br><a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=FU_YFpfDqqA" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=FU_YFpfDqqA" target="_self">Why The First Computers Were Made Out Of Light Bulbs</a> (Veritasium)
Three reasons:
Noise immunity — Large voltage gap between 0 and 1 absorbs noise
Simple circuits — Only need one threshold comparator
Boolean algebra — Mathematical foundation by George Boole (1840s), applied to circuits by Claude Shannon (1937)
In water terms: "flowing vs not flowing" is robust. "Half flowing" is not.Learn more:
<br><a data-tooltip-position="top" aria-label="https://youtu.be/gI-qXk7XojA?t=101" rel="noopener nofollow" class="external-link is-unresolved" href="https://youtu.be/gI-qXk7XojA?t=101" target="_self">Why Use Binary?</a> (YouTube)
BJT transistor = voltage-controlled valve
Base (B): Control input
Collector (C): Current in
Emitter (E): Current out
Two modes only:
Cutoff (OFF): No current flows → Logic 0
Saturation (ON): Max current flows → Logic 1
<br><img alt="Transistor as switch" src="assets/l01-transistor-as-switch.png" target="_self">
Any Boolean function can be built using only NAND gates.
<br><img alt="NAND gate" src="assets/l01-gate-nand.png" target="_self">
NOT from NAND: Connect both inputs together
AND from NAND: NAND → NOT
OR from NAND: NOT(A), NOT(B) → NAND
One "brick" is sufficient for arbitrary complexity.Problem: Gates are combinational (output depends only on current input). How to remember?Solution: Feedback creates stable states.D Flip-Flop:
D (Data): Value to store
CLK (Clock): When to capture
Q: Stored output
<br><img alt="D Flip-Flop circuit" src="assets/l01-d-flip-flop.png" target="_self">Captures D at clock edge, holds until next edge. This is 1-bit memory.8 flip-flops = 1 byte. Modern computers have billions.Key insight: Computation is not free. Every calculation has physical costs.
Computation is physical manipulation
Electronics won: speed, reliability, scale
Binary: noise immunity + Boolean algebra
Transistor = electronic switch
NAND alone can build anything
Feedback creates memory (D flip-flop)
Physics imposes fundamental limits
<br>Required viewing: <a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=QZwneRb-zqA" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=QZwneRb-zqA" target="_self">Exploring How Computers Work</a>You will:
Use a logic gate simulator
Build NOT, AND, OR, NAND circuits
Build a simple adder
Think about:
How to make OR from only NAND gates?
What's the XOR truth table? Why useful for addition?
]]></description><link>lectures/eng/l01-notes-eng.-physics-of-computing.html</link><guid isPermaLink="false">Lectures/ENG/L01-NOTES-ENG. Physics of Computing.md</guid><pubDate>Sat, 17 Jan 2026 21:20:59 GMT</pubDate><enclosure url="assets/l01-watersim-flow-on.jpg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l01-watersim-flow-on.jpg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L01-D-Flip-Flop-Truth-Table]]></title><description><![CDATA[<img src="assets/l01-d-flip-flop-truth-table.png" target="_self">]]></description><link>assets/l01-d-flip-flop-truth-table.html</link><guid isPermaLink="false">assets/L01-D-Flip-Flop-Truth-Table.png</guid><pubDate>Sat, 17 Jan 2026 20:53:57 GMT</pubDate><enclosure url="assets/l01-d-flip-flop-truth-table.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l01-d-flip-flop-truth-table.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L01-D-Flip-Flop]]></title><description><![CDATA[<img src="assets/l01-d-flip-flop.png" target="_self">]]></description><link>assets/l01-d-flip-flop.html</link><guid isPermaLink="false">assets/L01-D-Flip-Flop.png</guid><pubDate>Sat, 17 Jan 2026 20:53:57 GMT</pubDate><enclosure url="assets/l01-d-flip-flop.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l01-d-flip-flop.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L01-Ripple-Carry-Adder-8bit]]></title><description><![CDATA[<img src="assets/l01-ripple-carry-adder-8bit.png" target="_self">]]></description><link>assets/l01-ripple-carry-adder-8bit.html</link><guid isPermaLink="false">assets/L01-Ripple-Carry-Adder-8bit.png</guid><pubDate>Sat, 17 Jan 2026 20:53:57 GMT</pubDate><enclosure url="assets/l01-ripple-carry-adder-8bit.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l01-ripple-carry-adder-8bit.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L01-Full-Adder]]></title><description><![CDATA[<img src="assets/l01-full-adder.png" target="_self">]]></description><link>assets/l01-full-adder.html</link><guid isPermaLink="false">assets/L01-Full-Adder.png</guid><pubDate>Sat, 17 Jan 2026 20:53:57 GMT</pubDate><enclosure url="assets/l01-full-adder.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l01-full-adder.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L01-Half-Adder]]></title><description><![CDATA[<img src="assets/l01-half-adder.png" target="_self">]]></description><link>assets/l01-half-adder.html</link><guid isPermaLink="false">assets/L01-Half-Adder.png</guid><pubDate>Sat, 17 Jan 2026 20:53:57 GMT</pubDate><enclosure url="assets/l01-half-adder.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l01-half-adder.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L01-Gate-NAND]]></title><description><![CDATA[<img src="assets/l01-gate-nand.png" target="_self">]]></description><link>assets/l01-gate-nand.html</link><guid isPermaLink="false">assets/L01-Gate-NAND.png</guid><pubDate>Sat, 17 Jan 2026 20:53:57 GMT</pubDate><enclosure url="assets/l01-gate-nand.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l01-gate-nand.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L01-Gate-AND]]></title><description><![CDATA[<img src="assets/l01-gate-and.png" target="_self">]]></description><link>assets/l01-gate-and.html</link><guid isPermaLink="false">assets/L01-Gate-AND.png</guid><pubDate>Sat, 17 Jan 2026 20:53:57 GMT</pubDate><enclosure url="assets/l01-gate-and.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l01-gate-and.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L01-Gate-OR]]></title><description><![CDATA[<img src="assets/l01-gate-or.png" target="_self">]]></description><link>assets/l01-gate-or.html</link><guid isPermaLink="false">assets/L01-Gate-OR.png</guid><pubDate>Sat, 17 Jan 2026 20:53:57 GMT</pubDate><enclosure url="assets/l01-gate-or.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l01-gate-or.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L01-Gate-NOT]]></title><description><![CDATA[<img src="assets/l01-gate-not.png" target="_self">]]></description><link>assets/l01-gate-not.html</link><guid isPermaLink="false">assets/L01-Gate-NOT.png</guid><pubDate>Sat, 17 Jan 2026 20:53:57 GMT</pubDate><enclosure url="assets/l01-gate-not.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l01-gate-not.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L01-Transistor-As-Switch]]></title><description><![CDATA[<img src="assets/l01-transistor-as-switch.png" target="_self">]]></description><link>assets/l01-transistor-as-switch.html</link><guid isPermaLink="false">assets/L01-Transistor-As-Switch.png</guid><pubDate>Sat, 17 Jan 2026 20:53:57 GMT</pubDate><enclosure url="assets/l01-transistor-as-switch.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l01-transistor-as-switch.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L01-Transistor-Diagram-Symbol]]></title><description><![CDATA[<img src="assets/l01-transistor-diagram-symbol.png" target="_self">]]></description><link>assets/l01-transistor-diagram-symbol.html</link><guid isPermaLink="false">assets/L01-Transistor-Diagram-Symbol.png</guid><pubDate>Sat, 17 Jan 2026 20:53:57 GMT</pubDate><enclosure url="assets/l01-transistor-diagram-symbol.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l01-transistor-diagram-symbol.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L01-Transistor-Microscopic]]></title><description><![CDATA[<img src="assets/l01-transistor-microscopic.png" target="_self">]]></description><link>assets/l01-transistor-microscopic.html</link><guid isPermaLink="false">assets/L01-Transistor-Microscopic.png</guid><pubDate>Sat, 17 Jan 2026 20:53:57 GMT</pubDate><enclosure url="assets/l01-transistor-microscopic.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l01-transistor-microscopic.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L01-Transistors-Various-Sizes]]></title><description><![CDATA[<img src="assets/l01-transistors-various-sizes.jpg" target="_self">]]></description><link>assets/l01-transistors-various-sizes.html</link><guid isPermaLink="false">assets/L01-Transistors-Various-Sizes.jpg</guid><pubDate>Sat, 17 Jan 2026 20:53:57 GMT</pubDate><enclosure url="assets/l01-transistors-various-sizes.jpg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l01-transistors-various-sizes.jpg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L01-ENIAC-Vacuum-Tube-Panel]]></title><description><![CDATA[<img src="assets/l01-eniac-vacuum-tube-panel.jpg" target="_self">]]></description><link>assets/l01-eniac-vacuum-tube-panel.html</link><guid isPermaLink="false">assets/L01-ENIAC-Vacuum-Tube-Panel.jpg</guid><pubDate>Sat, 17 Jan 2026 20:53:57 GMT</pubDate><enclosure url="assets/l01-eniac-vacuum-tube-panel.jpg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l01-eniac-vacuum-tube-panel.jpg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L01-ENIAC-Operators]]></title><description><![CDATA[<img src="assets/l01-eniac-operators.jpg" target="_self">]]></description><link>assets/l01-eniac-operators.html</link><guid isPermaLink="false">assets/L01-ENIAC-Operators.jpg</guid><pubDate>Sat, 17 Jan 2026 20:53:57 GMT</pubDate><enclosure url="assets/l01-eniac-operators.jpg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l01-eniac-operators.jpg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L01-Vacuum-Tubes-Collection]]></title><description><![CDATA[<img src="assets/l01-vacuum-tubes-collection.jpg" target="_self">]]></description><link>assets/l01-vacuum-tubes-collection.html</link><guid isPermaLink="false">assets/L01-Vacuum-Tubes-Collection.jpg</guid><pubDate>Sat, 17 Jan 2026 20:53:57 GMT</pubDate><enclosure url="assets/l01-vacuum-tubes-collection.jpg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l01-vacuum-tubes-collection.jpg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L01-Babbage-Difference-Engine]]></title><description><![CDATA[<img src="assets/l01-babbage-difference-engine.jpeg" target="_self">]]></description><link>assets/l01-babbage-difference-engine.html</link><guid isPermaLink="false">assets/L01-Babbage-Difference-Engine.jpeg</guid><pubDate>Sat, 17 Jan 2026 20:53:57 GMT</pubDate><enclosure url="assets/l01-babbage-difference-engine.jpeg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l01-babbage-difference-engine.jpeg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L01-Leibniz-Calculator]]></title><description><![CDATA[<img src="assets/l01-leibniz-calculator.png" target="_self">]]></description><link>assets/l01-leibniz-calculator.html</link><guid isPermaLink="false">assets/L01-Leibniz-Calculator.png</guid><pubDate>Sat, 17 Jan 2026 20:53:57 GMT</pubDate><enclosure url="assets/l01-leibniz-calculator.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l01-leibniz-calculator.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L01-Domino-Computing]]></title><description><![CDATA[<img src="assets/l01-domino-computing.png" target="_self">]]></description><link>assets/l01-domino-computing.html</link><guid isPermaLink="false">assets/L01-Domino-Computing.png</guid><pubDate>Sat, 17 Jan 2026 20:53:57 GMT</pubDate><enclosure url="assets/l01-domino-computing.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l01-domino-computing.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L01-Marble-Computer]]></title><description><![CDATA[<img src="assets/l01-marble-computer.png" target="_self">]]></description><link>assets/l01-marble-computer.html</link><guid isPermaLink="false">assets/L01-Marble-Computer.png</guid><pubDate>Sat, 17 Jan 2026 20:53:57 GMT</pubDate><enclosure url="assets/l01-marble-computer.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l01-marble-computer.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L01-CPU-Die-Shot]]></title><description><![CDATA[<img src="assets/l01-cpu-die-shot.jpg" target="_self">]]></description><link>assets/l01-cpu-die-shot.html</link><guid isPermaLink="false">assets/L01-CPU-Die-Shot.jpg</guid><pubDate>Sat, 17 Jan 2026 20:53:57 GMT</pubDate><enclosure url="assets/l01-cpu-die-shot.jpg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l01-cpu-die-shot.jpg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L01-WaterSim-OR-Gate]]></title><description><![CDATA[<img src="assets/l01-watersim-or-gate.jpg" target="_self">]]></description><link>assets/l01-watersim-or-gate.html</link><guid isPermaLink="false">assets/L01-WaterSim-OR-Gate.jpg</guid><pubDate>Sat, 17 Jan 2026 20:53:28 GMT</pubDate><enclosure url="assets/l01-watersim-or-gate.jpg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l01-watersim-or-gate.jpg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L01-WaterSim-AND-Gate]]></title><description><![CDATA[<img src="assets/l01-watersim-and-gate.jpg" target="_self">]]></description><link>assets/l01-watersim-and-gate.html</link><guid isPermaLink="false">assets/L01-WaterSim-AND-Gate.jpg</guid><pubDate>Sat, 17 Jan 2026 20:53:28 GMT</pubDate><enclosure url="assets/l01-watersim-and-gate.jpg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l01-watersim-and-gate.jpg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L01-WaterSim-Valve-On]]></title><description><![CDATA[<img src="assets/l01-watersim-valve-on.jpg" target="_self">]]></description><link>assets/l01-watersim-valve-on.html</link><guid isPermaLink="false">assets/L01-WaterSim-Valve-On.jpg</guid><pubDate>Sat, 17 Jan 2026 20:53:28 GMT</pubDate><enclosure url="assets/l01-watersim-valve-on.jpg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l01-watersim-valve-on.jpg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L01-WaterSim-Valve-Off]]></title><description><![CDATA[<img src="assets/l01-watersim-valve-off.jpg" target="_self">]]></description><link>assets/l01-watersim-valve-off.html</link><guid isPermaLink="false">assets/L01-WaterSim-Valve-Off.jpg</guid><pubDate>Sat, 17 Jan 2026 20:53:28 GMT</pubDate><enclosure url="assets/l01-watersim-valve-off.jpg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l01-watersim-valve-off.jpg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L01-WaterSim-Flow-Off]]></title><description><![CDATA[<img src="assets/l01-watersim-flow-off.jpg" target="_self">]]></description><link>assets/l01-watersim-flow-off.html</link><guid isPermaLink="false">assets/L01-WaterSim-Flow-Off.jpg</guid><pubDate>Sat, 17 Jan 2026 20:53:28 GMT</pubDate><enclosure url="assets/l01-watersim-flow-off.jpg" length="0" type="image/jpeg"/><content:encoded>&lt;figure&gt;&lt;img src="assets/l01-watersim-flow-off.jpg"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L01-Physics-of-Computing-Materials]]></title><description><![CDATA[Renamed, please follow this link:
<a data-href="L01-ENG. Physics of Computing" href="lectures/eng/l01-eng.-physics-of-computing.html" class="internal-link" target="_self" rel="noopener nofollow">L01-ENG. Physics of Computing</a>]]></description><link>lectures/eng/l01-physics-of-computing-materials.html</link><guid isPermaLink="false">Lectures/ENG/L01-Physics-of-Computing-Materials.md</guid><pubDate>Sat, 17 Jan 2026 20:22:33 GMT</pubDate></item><item><title><![CDATA[Lab01]]></title><description><![CDATA[
Курс: Архітектура обчислювальних систем Тиждень: 1 Тривалість: 2 академічні години Тип: Практична робота під наглядом інструктора Після завершення практики студент зможе:
Будувати базові логічні елементи (AND, NOT, OR, XOR) з єдиного примітиву NAND.
Використовувати побудовані елементи як компоненти для складніших схем.
Працювати з Git-репозиторієм (commit, push) для здачі робіт. Перегляньте відео: <a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=QZwneRb-zqA" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=QZwneRb-zqA" target="_self">Exploring How Computers Work – YouTube</a>
<br>Встановіть Digital Logic Sim: <a data-tooltip-position="top" aria-label="https://sebastian.itch.io/digital-logic-sim" rel="noopener nofollow" class="external-link is-unresolved" href="https://sebastian.itch.io/digital-logic-sim" target="_self">sebastian.itch.io/digital-logic-sim</a> <br>⚠️ На Mac можуть бути проблеми – див. <a data-tooltip-position="top" aria-label="https://www.loom.com/share/9958eb1e2bbc415abde4070145a08de0?sid=276e7c98-cacd-4172-89f1-6bd57b23b2cf" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.loom.com/share/9958eb1e2bbc415abde4070145a08de0?sid=276e7c98-cacd-4172-89f1-6bd57b23b2cf" target="_self">Loom відео</a> <br>‼️ Якщо ще не маєте аккаунту – зареєструйтесь на <a data-tooltip-position="top" aria-label="https://github.com" rel="noopener nofollow" class="external-link is-unresolved" href="https://github.com" target="_self">GitHub</a> (якщо ще не маєте акаунту). <br><img alt="Lab01-DigiLoSim-Download.png" src="assets/lab01-digilosim-download.png" target="_self" style="width: 500px; max-width: 100%;"><br><img alt="Lab01-GitHub-Register.png" src="assets/lab01-github-register.png" target="_self" style="width: 500px; max-width: 100%;">
<br>Перейдіть <a data-tooltip-position="top" aria-label="https://classroom.github.com/a/-fRqd2i5" rel="noopener nofollow" class="external-link is-unresolved" href="https://classroom.github.com/a/-fRqd2i5" target="_self">за посиланням GitHub Classroom assignment</a>
Прийміть завдання – репозиторій створюється автоматично. Репозиторій створиться із кнопкою "Open in Codespaces" – це хмарне середовище розробки прямо у браузері, не потребує локального налаштування. Рекомендуємо зберегти цю кнопку у README-файлі для майбутніх практик.
⚠️ Обов'язково відредагуйте README.md, додавши:
Вашу корпоративну email-адресу (@ukma.edu.ua) Ваше ім'я та прізвище кирилицею Ваш факультет і групу <br>Приклади (уже написані в форматі Markdown, це важливо, якщо це для вас нове – <a data-tooltip-position="top" aria-label="https://www.markdownguide.org/basic-syntax/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.markdownguide.org/basic-syntax/" target="_self">ознайомтесь з Markdown</a>!):# Іван Петренко i.petrenko@ukma.edu.ua
ІПЗ, Група 2 [тут кнопка Codespaces]
# Марія Коваленко m.kovalenko@ukma.edu.ua
КН, Група 3 [тут кнопка Codespaces] ... тут опційно цікавинки -- про себе, творіть у Markdown, підніміть настрій
# Олег Шевченко o.shevchenko@ukma.edu.ua
АВІС, Група 1 [тут кнопка Codespaces] [тут Markdown art]
Симулятор містить лише один базовий елемент – NAND. Це універсальний гейт, з якого можна побудувати будь-яку логічну схему!Крок 1: Спочатку побудуйте базові елементи з NAND:
<br>NOT gate: <a data-tooltip-position="top" aria-label="https://www.geeksforgeeks.org/electronics-engineering/implementation-of-not-gate-using-nand-gate/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.geeksforgeeks.org/electronics-engineering/implementation-of-not-gate-using-nand-gate/" target="_self">як це зробити</a>
AND gate (додати NOT після NAND)
Крок 2: Використовуючи побудовані AND та NOT, створіть:
OR gate
XOR gate
💡 Мета – зрозуміти, як складні елементи будуються з простіших, і що NAND є функціонально повним (universal gate).<br>Альтернативний, більш реалістичний симулятор: <a data-tooltip-position="top" aria-label="https://www.falstad.com/circuit/" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.falstad.com/circuit/" target="_self">falstad.com/circuit</a>Приклади схем:
<br><a data-tooltip-position="top" aria-label="https://www.falstad.com/circuit/circuitjs.html?ctz=CQAgjCAMB0l3BWEAWATLBBOAzM5kxVJsAObbEBSSlChAUwFowwAoAGRADYEuRsuJbpCEChqKCABmAQwA2AZ3o1IrMAmQgA7JE2lNJMH32SJ1BGqrbdKLgaO3N1M5Q7ctE-NS4iUcEC6yispIqgDu3Lz8gpF8yCTU4bEoCdaa8YmsETzGMTp6MUlcHn7U+aVQWe6e-sWe8ZXZJXhC5S2NIIZxdp0NyD1JXfwkBg0mqgCyvUL9BpjdmhLoFlMjoiOd88OLAdAWAB6dXJgomNQkJEjIOCgBmgAKrIckWhDYWgYv-Fpat6iaAGUnpQdN8kAgSMYtDM7iAAILAhCg-AnCFxQh-TQAIWBgiQ2DAQkMb1QEEW1EAuCCABhBAPwggDUQQBMIIBBEEAwiCAHhAqYAhEEAfCCAARBgVp3vwztotMYbppCSAABoAeQASgKhchXqKrh9blK4QA5AAirCAA" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.falstad.com/circuit/circuitjs.html?ctz=CQAgjCAMB0l3BWEAWATLBBOAzM5kxVJsAObbEBSSlChAUwFowwAoAGRADYEuRsuJbpCEChqKCABmAQwA2AZ3o1IrMAmQgA7JE2lNJMH32SJ1BGqrbdKLgaO3N1M5Q7ctE-NS4iUcEC6yispIqgDu3Lz8gpF8yCTU4bEoCdaa8YmsETzGMTp6MUlcHn7U+aVQWe6e-sWe8ZXZJXhC5S2NIIZxdp0NyD1JXfwkBg0mqgCyvUL9BpjdmhLoFlMjoiOd88OLAdAWAB6dXJgomNQkJEjIOCgBmgAKrIckWhDYWgYv-Fpat6iaAGUnpQdN8kAgSMYtDM7iAAILAhCg-AnCFxQh-TQAIWBgiQ2DAQkMb1QEEW1EAuCCABhBAPwggDUQQBMIIBBEEAwiCAHhAqYAhEEAfCCAARBgVp3vwztotMYbppCSAABoAeQASgKhchXqKrh9blK4QA5AAirCAA" target="_self">Приклад 1</a> <br><a data-tooltip-position="top" aria-label="https://www.falstad.com/circuit/circuitjs.html?ctz=CQAgjCAMB0l3BWcAmWDLMgZgBxmcgCxg4CcS6IShWVApgLRhgBQAMiA4TpCIXJwTIcfAciggAZgEMANgGc6VKO06EAbIRBZ1IhkJE6R43jIVKkkFmEoMskLd15NS4pxJNVrCLXYejnMFcAjwkEFgB3NU1tXU4sHViRKxtfGjcwdXjMPkzQ3nCbZ3SqYWyCst5PcI4ubj4scX0yks8zRWUrKL8tI3j-XC0u8tysntGVbvtHPPGNIcj+3ri7RL7hrh4QzYLCBe73HzTGqj3JtS3+YpOr89Wsvvu+UiyNhKzCF6Xn18W6kRKXBuJw2ewBJ3+VHwdzBUKaJQQ0LeOQMnCC4lRKSEaOCnzGXzx+S83XR2wJAg2pMRTSplRYAFlOASvpsfiBxKhwoyGOTigCxOzoOEAB6CdRILAAdklgk+2ku7K0AAUWKL9D55bR1eJcB8UCAAJaqzgxVGshBYCBaaEGgA6AAdWGrJRQCIJSKVqIqQCrnbQsKRnAgZQHaFoiCAAMrG-TqD6QD3q3jENzegCCMYQcb4xEElD2jm9ACEYzodcgxshEdpkB7wyJAIQggGEQQA8IIAGEEAQiCAPhBAAIgpdIHotSDswaouD47N4aYAcgARft15BarBZvgVic5AAaAHkAEoxzIYjBok7oL05Pcx5CSq04PQEOukK2TkAz+dq69uSD32u5Z9by8gA" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.falstad.com/circuit/circuitjs.html?ctz=CQAgjCAMB0l3BWcAmWDLMgZgBxmcgCxg4CcS6IShWVApgLRhgBQAMiA4TpCIXJwTIcfAciggAZgEMANgGc6VKO06EAbIRBZ1IhkJE6R43jIVKkkFmEoMskLd15NS4pxJNVrCLXYejnMFcAjwkEFgB3NU1tXU4sHViRKxtfGjcwdXjMPkzQ3nCbZ3SqYWyCst5PcI4ubj4scX0yks8zRWUrKL8tI3j-XC0u8tysntGVbvtHPPGNIcj+3ri7RL7hrh4QzYLCBe73HzTGqj3JtS3+YpOr89Wsvvu+UiyNhKzCF6Xn18W6kRKXBuJw2ewBJ3+VHwdzBUKaJQQ0LeOQMnCC4lRKSEaOCnzGXzx+S83XR2wJAg2pMRTSplRYAFlOASvpsfiBxKhwoyGOTigCxOzoOEAB6CdRILAAdklgk+2ku7K0AAUWKL9D55bR1eJcB8UCAAJaqzgxVGshBYCBaaEGgA6AAdWGrJRQCIJSKVqIqQCrnbQsKRnAgZQHaFoiCAAMrG-TqD6QD3q3jENzegCCMYQcb4xEElD2jm9ACEYzodcgxshEdpkB7wyJAIQggGEQQA8IIAGEEAQiCAPhBAAIgpdIHotSDswaouD47N4aYAcgARft15BarBZvgVic5AAaAHkAEoxzIYjBok7oL05Pcx5CSq04PQEOukK2TkAz+dq69uSD32u5Z9by8gA" target="_self">Приклад 2</a>
⚠️ Falstad складніший, тому базову практику краще робити у Digital Logic Sim.До кінця заняття:
Побудовані NOT, AND, OR, XOR з примітиву NAND у Digital Logic Sim.
Налаштований GitHub-репозиторій з заповненим README. Дедлайн: 23:59 у день перед Практикою 2Запишіть одне Loom-відео, у якому:
Показуєте процес побудови XOR Gate з нуля. Демонструєте результат створення 4-бітного суматора (як у відео з підготовки) та коротко пояснюєте, як він працює. → Закомітьте посилання на відео у файл:
Lab01/video.txt⚠️ Не забувайте робити push – перевіряйте на сайті GitHub!
⚠️🔔 5 хвилин – це жорсткий ліміт на відео. Brevity is the soul of wit! 🔔⚠️]]></description><link>labs/lab01.html</link><guid isPermaLink="false">Labs/Lab01.md</guid><pubDate>Wed, 14 Jan 2026 17:56:41 GMT</pubDate><enclosure url="assets/lab01-digilosim-download.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/lab01-digilosim-download.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Lab01-GitHub-Register]]></title><description><![CDATA[<img src="assets/lab01-github-register.png" target="_self">]]></description><link>assets/lab01-github-register.html</link><guid isPermaLink="false">assets/Lab01-GitHub-Register.png</guid><pubDate>Wed, 07 Jan 2026 15:17:05 GMT</pubDate><enclosure url="assets/lab01-github-register.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/lab01-github-register.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[Lab01-DigiLoSim-Download]]></title><description><![CDATA[<img src="assets/lab01-digilosim-download.png" target="_self">]]></description><link>assets/lab01-digilosim-download.html</link><guid isPermaLink="false">assets/Lab01-DigiLoSim-Download.png</guid><pubDate>Wed, 07 Jan 2026 15:15:50 GMT</pubDate><enclosure url="assets/lab01-digilosim-download.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/lab01-digilosim-download.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[UKMA-logo]]></title><description><![CDATA[<img src="assets/ukma-logo.png" target="_self">]]></description><link>assets/ukma-logo.html</link><guid isPermaLink="false">assets/UKMA-logo.png</guid><pubDate>Thu, 25 Dec 2025 07:17:49 GMT</pubDate><enclosure url="assets/ukma-logo.png" length="0" type="image/png"/><content:encoded>&lt;figure&gt;&lt;img src="assets/ukma-logo.png"&gt;&lt;/figure&gt;</content:encoded></item><item><title><![CDATA[L1-NOTES. Фізика обчислень]]></title><description><![CDATA[Короткий довідник до Лекції 1. Повні матеріали див. у ENG/L01-ENG. Physics of Computing.md.Розкладіть проблеми на фундаментальні істини, потім відбудуйте. Чотири кроки:
Декомпозиція до фундаментальних основ
Ставте під сумнів усі припущення
Відбудуйте з нуля
Думайте як науковець
Приклад: Ракети SpaceX — сировина коштує 2% від типової ціни ракети. Мислення від перших принципів дозволило знизити вартість у 10 разів.Дізнатися більше:
<a data-tooltip-position="top" aria-label="https://jamesclear.com/first-principles" rel="noopener nofollow" class="external-link is-unresolved" href="https://jamesclear.com/first-principles" target="_self">First Principles: The Building Blocks of True Knowledge</a>
<br><a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=NV3sBlRgzTI" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=NV3sBlRgzTI" target="_self">Elon Musk explains First Principles</a> (YouTube) «Інформація є фізичною.» — Рольф Ландауер
Кожен біт існує як фізичний стан: заряди, магнітні орієнтації, рівні напруги. Комп'ютер маніпулює фізичними об'єктами згідно з фізичними законами.Історичні приклади: Абак (кісточки), Паскаліна (шестерні), калькулятор Лейбніца.Центральна аналогія для цього курсу:Гідравлічні комп'ютери справді були побудовані (Радянський Союз, середина XX століття).Дізнатися більше:
<br><a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=IgF3OX8nT0w" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=IgF3OX8nT0w" target="_self">Water Integrator / Hydraulic Computer</a> (YouTube)
<br><a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=TTK-vcwSm7s" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=TTK-vcwSm7s" target="_self">Marble Computer</a> (YouTube)
<br><a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=w6E7aQnA4Ws" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=w6E7aQnA4Ws" target="_self">Domino Computer</a> (YouTube)
Відповідь: Швидкість, надійність і масштаб. Електронами можна маніпулювати швидше та в менших масштабах, ніж будь-якою механічною альтернативою.Ада Лавлейс писала програми для Аналітичної машини — можливо, перший програміст.Дізнатися більше:
<br><a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=O5nskjZ_GoI" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=O5nskjZ_GoI" target="_self">Early Computing: Crash Course CS #1</a> (YouTube)
<br><a data-tooltip-position="top" aria-label="https://youtu.be/LN0ucKNX0hc?t=61" rel="noopener nofollow" class="external-link is-unresolved" href="https://youtu.be/LN0ucKNX0hc?t=61" target="_self">History of Computing</a> (YouTube)
Вакуумні лампи (ENIAC, 1945): 18 000 ламп, 30 тонн, 150 кВт, виходили з ладу кожні кілька годин.Транзистори (1947, Bell Labs): Твердотільні, без часу прогрівання, мініатюризовані, надійні.Дізнатися більше:
<br><a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=FU_YFpfDqqA" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=FU_YFpfDqqA" target="_self">Why The First Computers Were Made Out Of Light Bulbs</a> (Veritasium)
Три причини:
Завадостійкість — Великий проміжок напруги між 0 та 1 поглинає шум
Прості схеми — Потрібен лише один пороговий компаратор
Булева алгебра — Математичний фундамент від Джорджа Буля (1840-ві), застосований до схем Клодом Шенноном (1937)
У термінах води: «тече чи не тече» — надійно. «Наполовину тече» — ні.Дізнатися більше:
<br><a data-tooltip-position="top" aria-label="https://youtu.be/gI-qXk7XojA?t=101" rel="noopener nofollow" class="external-link is-unresolved" href="https://youtu.be/gI-qXk7XojA?t=101" target="_self">Why Use Binary?</a> (YouTube)
Біполярний транзистор = клапан, керований напругою
База (B): Керуючий вхід
Колектор (C): Вхід струму
Емітер (E): Вихід струму
Лише два режими:
Відсічення (OFF): Струм не тече → Логічний 0
Насичення (ON): Максимальний струм тече → Логічна 1 Будь-яка булева функція може бути побудована лише з вентилів NAND. NOT з NAND: З'єднайте обидва входи разом
AND з NAND: NAND → NOT
OR з NAND: NOT(A), NOT(B) → NAND
Однієї «цеглинки» достатньо для довільної складності.Проблема: Вентилі є комбінаційними (вихід залежить лише від поточного входу). Як запам'ятовувати?Рішення: Зворотний зв'язок створює стабільні стани.D-тригер:
D (Data, дані): Значення для збереження
CLK (Clock, тактовий сигнал): Коли захоплювати
Q: Збережений вихід
Захоплює D на фронті тактового сигналу, утримує до наступного фронту. Це 1-бітна пам'ять.8 тригерів = 1 байт. Сучасні комп'ютери мають мільярди.Ключовий висновок: Обчислення не є безкоштовними. Кожен розрахунок має фізичну вартість.
Обчислення — це фізичні маніпуляції
Електроніка перемогла: швидкість, надійність, масштаб
Двійкова система: завадостійкість + булева алгебра
Транзистор = електронний перемикач
Лише NAND може побудувати будь-що
Зворотний зв'язок створює пам'ять (D-тригер)
Фізика накладає фундаментальні обмеження
<br>Обов'язковий перегляд: <a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=QZwneRb-zqA" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=QZwneRb-zqA" target="_self">Exploring How Computers Work</a>Ви будете:
Використовувати симулятор логічних вентилів
Будувати схеми NOT, AND, OR, NAND
Будувати простий суматор
Подумайте:
Як створити OR лише з вентилів NAND?
Яка таблиця істинності для XOR? Чому корисна для додавання?
]]></description><link>lectures/l1-notes.-фізика-обчислень.html</link><guid isPermaLink="false">Lectures/L1-NOTES. Фізика обчислень.md</guid><pubDate>Sat, 17 Jan 2026 20:21:43 GMT</pubDate></item><item><title><![CDATA[L1-NOTES-ENG. Physics of Computing]]></title><description><![CDATA[Quick reference for Lecture 1. See full materials in L01-ENG. Physics of Computing.md.Break problems down to fundamental truths, then rebuild. Four steps:
Decompose to fundamentals
Question all assumptions
Rebuild from ground up
Think like a scientist
Example: SpaceX rockets — raw materials cost 2% of typical rocket price. Reasoning from first principles enabled 10× cost reduction.Learn more:
<a data-tooltip-position="top" aria-label="https://jamesclear.com/first-principles" rel="noopener nofollow" class="external-link is-unresolved" href="https://jamesclear.com/first-principles" target="_self">First Principles: The Building Blocks of True Knowledge</a>
<br><a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=NV3sBlRgzTI" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=NV3sBlRgzTI" target="_self">Elon Musk explains First Principles</a> (YouTube) "Information is physical." — Rolf Landauer
Every bit exists as a physical state: charges, magnetic orientations, voltage levels. A computer manipulates physical objects according to physical laws.Historical examples: Abacus (beads), Pascaline (gears), Leibniz's Stepped Reckoner.Central analogy for this course:Hydraulic computers were actually built (Soviet Union, mid-20th century).Learn more:
<br><a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=IgF3OX8nT0w" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=IgF3OX8nT0w" target="_self">Water Integrator / Hydraulic Computer</a> (YouTube)
<br><a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=TTK-vcwSm7s" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=TTK-vcwSm7s" target="_self">Marble Computer</a> (YouTube)
<br><a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=w6E7aQnA4Ws" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=w6E7aQnA4Ws" target="_self">Domino Computer</a> (YouTube)
Answer: Speed, reliability, and scale. Electrons can be manipulated faster and smaller than any mechanical alternative.Ada Lovelace wrote programs for the Analytical Engine — arguably the first programmer.Learn more:
<br><a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=O5nskjZ_GoI" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=O5nskjZ_GoI" target="_self">Early Computing: Crash Course CS #1</a> (YouTube)
<br><a data-tooltip-position="top" aria-label="https://youtu.be/LN0ucKNX0hc?t=61" rel="noopener nofollow" class="external-link is-unresolved" href="https://youtu.be/LN0ucKNX0hc?t=61" target="_self">History of Computing</a> (YouTube)
Vacuum tubes (ENIAC, 1945): 18,000 tubes, 30 tons, 150 kW, failed every few hours.Transistors (1947, Bell Labs): Solid-state, no warm-up, miniaturizable, reliable.Learn more:
<br><a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=FU_YFpfDqqA" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=FU_YFpfDqqA" target="_self">Why The First Computers Were Made Out Of Light Bulbs</a> (Veritasium)
Three reasons:
Noise immunity — Large voltage gap between 0 and 1 absorbs noise
Simple circuits — Only need one threshold comparator
Boolean algebra — Mathematical foundation by George Boole (1840s), applied to circuits by Claude Shannon (1937)
In water terms: "flowing vs not flowing" is robust. "Half flowing" is not.Learn more:
<br><a data-tooltip-position="top" aria-label="https://youtu.be/gI-qXk7XojA?t=101" rel="noopener nofollow" class="external-link is-unresolved" href="https://youtu.be/gI-qXk7XojA?t=101" target="_self">Why Use Binary?</a> (YouTube)
BJT transistor = voltage-controlled valve
Base (B): Control input
Collector (C): Current in
Emitter (E): Current out
Two modes only:
Cutoff (OFF): No current flows → Logic 0
Saturation (ON): Max current flows → Logic 1 Any Boolean function can be built using only NAND gates. NOT from NAND: Connect both inputs together
AND from NAND: NAND → NOT
OR from NAND: NOT(A), NOT(B) → NAND
One "brick" is sufficient for arbitrary complexity.Problem: Gates are combinational (output depends only on current input). How to remember?Solution: Feedback creates stable states.D Flip-Flop:
D (Data): Value to store
CLK (Clock): When to capture
Q: Stored output
Captures D at clock edge, holds until next edge. This is 1-bit memory.8 flip-flops = 1 byte. Modern computers have billions.Key insight: Computation is not free. Every calculation has physical costs.
Computation is physical manipulation
Electronics won: speed, reliability, scale
Binary: noise immunity + Boolean algebra
Transistor = electronic switch
NAND alone can build anything
Feedback creates memory (D flip-flop)
Physics imposes fundamental limits
<br>Required viewing: <a data-tooltip-position="top" aria-label="https://www.youtube.com/watch?v=QZwneRb-zqA" rel="noopener nofollow" class="external-link is-unresolved" href="https://www.youtube.com/watch?v=QZwneRb-zqA" target="_self">Exploring How Computers Work</a>You will:
Use a logic gate simulator
Build NOT, AND, OR, NAND circuits
Build a simple adder
Think about:
How to make OR from only NAND gates?
What's the XOR truth table? Why useful for addition?
]]></description><link>lectures/eng/l1-notes-eng.-physics-of-computing.html</link><guid isPermaLink="false">Lectures/ENG/L1-NOTES-ENG. Physics of Computing.md</guid><pubDate>Sat, 17 Jan 2026 20:21:43 GMT</pubDate></item></channel></rss>