Частина 4: QFTASM та Cogol
Огляд архітектури
Словом, наш комп'ютер має 16-бітну асинхронну архітектуру RISC Гарварду. Створюючи процесор вручну, архітектура RISC ( скорочений набір інструкцій ) є практично необхідною умовою. У нашому випадку це означає, що кількість опкодів невелика і, що набагато важливіше, що всі інструкції обробляються дуже схожим чином.
Для довідки комп'ютер Wireworld використовував транспортно-спрацьовану архітектуру , в якій єдиною інструкцією було проведено MOV
обчислення, записавши / прочитавши спеціальні регістри. Хоча ця парадигма призводить до дуже простої у виконанні архітектури, результат також є непридатним для кордону: всі арифметичні / логічні / умовні операції потребують трьох інструкцій. Нам було зрозуміло, що ми хочемо створити набагато менш езотеричну архітектуру.
Для того, щоб наш процесор був простим, збільшуючи зручність використання, ми прийняли кілька важливих дизайнерських рішень:
- Немає реєстрів. Кожна адреса оперативної пам'яті трактується однаково і може бути використана як будь-який аргумент для будь-якої операції. У певному сенсі це означає, що вся оперативна пам'ять може оброблятися як регістри. Це означає, що немає спеціальних інструкцій щодо завантаження / зберігання.
- У подібному руслі відображення пам'яті. Все, що можна було написати або прочитати з спільної схеми адресації. Це означає, що лічильник програм (ПК) - це адреса 0, і єдина відмінність між звичайними інструкціями та інструкціями контрольного потоку полягає в тому, що в інструкціях контрольного потоку використовується адреса 0.
- Дані є послідовними в передачі, паралельно зберігаються. Завдяки нашому комп'ютеру, що базується на електронах, додавання та віднімання значно легше здійснити, коли дані передаються у серійному мало-ендіанському (найменш значущому біті спочатку). Крім того, серійні дані знімають потребу в громіздких шинах даних, які дійсно широкі і громіздкі за часом належним чином (для того, щоб дані залишалися разом, усі "смуги" шини повинні мати однакові затримки в дорозі).
- Гарвардська архітектура, що означає поділ між пам'яттю програми (ROM) та пам'яттю даних (RAM). Хоча це і знижує гнучкість процесора, це допомагає оптимізувати розміри: довжина програми набагато більша за обсяг оперативної пам’яті, який нам знадобиться, тому ми можемо розділити програму на ПЗУ, а потім зосередитись на стисканні ПЗУ , що набагато простіше, коли він доступний лише для читання.
- 16-бітна ширина даних. Це найменша потужність з двох, яка ширша від стандартної дошки Tetris (10 блоків). Це дає нам діапазон даних від -32768 до +32767 та максимальну довжину програми - 65536 інструкцій. (2 ^ 8 = 256 інструкцій вистачає для більшості простих речей, які ми можемо хотіти робити процесором іграшок, але не Tetris.)
- Асинхронний дизайн. Замість того, щоб мати центральний годинник (або, що еквівалентно, кілька годин), що диктує час роботи комп'ютера, усі дані супроводжуються "синхросигналом", який рухається паралельно з даними, коли він обтікає комп'ютер. Окремі шляхи можуть бути коротшими, ніж інші, і хоча це створює труднощі для централізованої конструкції, асинхронна конструкція може легко справлятися з операціями зі змінним часом.
- Всі інструкції мають однаковий розмір. Ми вважали, що архітектура, в якій кожна інструкція має 1 опкод з 3 операндами (значення значення призначення), була найбільш гнучким варіантом. Це охоплює операції з бінарними даними, а також умовні ходи.
- Проста система режиму адресації. Наявність різноманітних режимів адресації дуже корисно для підтримки таких речей, як масиви чи рекурсія. Нам вдалося реалізувати кілька важливих режимів адресації за допомогою відносно простої системи.
Ілюстрація нашої архітектури міститься в оглядовому пості.
Функціональність та операції ALU
Звідси було вирішено визначити, якою функціональністю повинен володіти наш процесор. Особлива увага приділялася простоті виконання, а також універсальності кожної команди.
Умовні рухи
Умовні ходи є дуже важливими і служать як дрібномасштабним, так і великомасштабним контрольним потоком. "Маломасштабний" відноситься до його здатності контролювати виконання певного руху даних, тоді як "великомасштабний" відноситься до його використання як умовної операції стрибка для передачі потоку управління на будь-який довільний фрагмент коду. Немає спеціальних операцій стрибків, оскільки, завдяки картографуванню пам'яті, умовний хід може як скопіювати дані в звичайну оперативну пам'ять, так і скопіювати адресу призначення на ПК. Ми також вирішили відмовитись як від безумовних рухів, так і від безумовних стрибків з аналогічної причини: обидва можуть бути реалізовані як умовний хід з умовою, важко кодованою для ІСТИНИ.
Ми вибрали два різних типи умовних рухів: "перемістити, якщо не нуль" ( MNZ
) і "перемістити, якщо менше нуля" ( MLZ
). Функціонально - MNZ
це перевірка того, чи є будь-який біт у даних 1, в той час як MLZ
зводиться до перевірки, чи є бітовий знак 1. Вони корисні для рівностей і порівнянь відповідно. Причиною того, як ми обрали цих двох над іншими, такими як "рухатись, якщо нуль" ( MEZ
) або "рухатись, якщо більший за нуль" ( MGZ
), було те, MEZ
що вимагатиме створення ІСТИЧНОГО сигналу з порожнього сигналу, в той час MGZ
як це більш складна перевірка, що вимагає знак біта буде 0, хоча принаймні один інший біт буде 1.
Арифметика
Наступною найважливішою інструкцією щодо керування процесором є основні арифметичні операції. Як я вже згадував раніше, ми використовуємо серійні дані з малої ендіанської обробкою, при цьому вибір витримки визначається легкістю операцій додавання / віднімання. Маючи найменш значущий біт, який надходить першим, арифметичні одиниці можуть легко відслідковувати біт переносу.
Ми вирішили використати подання доповнення 2 для від'ємних чисел, оскільки це робить додавання і віднімання більш послідовними. Варто зазначити, що комп'ютер Wireworld використовував додаток 1.
Додавання і віднімання - це ступінь арифметичної підтримки нашого комп’ютера (крім бітових зрушень, про які мова піде далі). Інші операції, такі як множення, є надто складними, щоб обробляти нашу архітектуру, і повинні бути реалізовані в програмному забезпеченні.
Побітові операції
Наш процесор має AND
, OR
і XOR
інструкції , які робити те , що можна було б очікувати. Замість того, щоб мати NOT
інструкцію, ми вибрали інструкцію "і-не" ( ANT
). Складність з NOT
інструкцією знову ж таки полягає в тому, що він повинен створювати сигнал від нестачі сигналу, що складно з стільниковими автоматами. ANT
Інструкція повертає 1 , тільки якщо перший аргумент біт дорівнює 1 , а другий аргумент біт дорівнює 0. Таким чином, NOT x
еквівалентно ANT -1 x
(а також XOR -1 x
). Крім того, ANT
він універсальний і має свою головну перевагу в маскуванні: у випадку програми Tetris ми використовуємо його для стирання тетроміно.
Зсув бітів
Операції з переміщенням бітів - це найскладніші операції, якими управляє АЛУ. Вони беруть два введення даних: значення для зсуву та кількість для його зміщення. Незважаючи на їх складність (через мінливу кількість зрушень), ці операції мають вирішальне значення для багатьох важливих завдань, включаючи безліч "графічних" операцій, що беруть участь у тетрісі. Зсуви бітів також послужать основою для ефективних алгоритмів множення / ділення.
У нашому процесорі є три операції зсуву бітів: "зрушення вліво" ( SL
), "логічний зсув праворуч" ( SRL
) і "арифметика зсуву вправо" ( SRA
). Перші два бітові зсуви ( SL
і SRL
) заповнюють нові біти усіма нулями (це означає, що від'ємне число, зміщене вправо, більше не буде від'ємним). Якщо другий аргумент зрушення знаходиться поза діапазоном від 0 до 15, результатом є всі нулі, як ви могли очікувати. Для останнього бітового зсуву, SRA
бітовий зсув зберігає знак введення, і тому діє як справжній поділ на два.
Інструкція Трубопровід
Зараз час поговорити про деякі суворі деталі архітектури. Кожен цикл процесора складається з наступних п'яти кроків:
1. Отримайте поточну інструкцію з ПЗУ
Поточне значення ПК використовується для отримання відповідної інструкції з ПЗУ. Кожна інструкція має один опкод і три операнди. Кожен операнд складається з одного слова даних та одного режиму адресації. Ці частини розділені одна від одної, коли вони читаються з ПЗУ.
Опкод - це 4 біти для підтримки 16 унікальних опкодів, з яких 11 призначено:
0000 MNZ Move if Not Zero
0001 MLZ Move if Less than Zero
0010 ADD ADDition
0011 SUB SUBtraction
0100 AND bitwise AND
0101 OR bitwise OR
0110 XOR bitwise eXclusive OR
0111 ANT bitwise And-NoT
1000 SL Shift Left
1001 SRL Shift Right Logical
1010 SRA Shift Right Arithmetic
1011 unassigned
1100 unassigned
1101 unassigned
1110 unassigned
1111 unassigned
2. Запишіть результат (при необхідності) попередньої інструкції в ОЗУ
Залежно від умови попередньої інструкції (наприклад, значення першого аргументу для умовного переміщення) виконується запис. Адреса запису визначається третім операндом попередньої інструкції.
Важливо зазначити, що написання відбувається після отримання інструкцій. Це призводить до створення слота затримки гілки, в якому інструкція відразу після інструкції гілки (будь-яка операція, яка записується на ПК) виконується замість першої інструкції в цілі гілки.
У деяких випадках (як безумовні стрибки) слот затримки гілки можна оптимізувати. В інших випадках це не вдається, і інструкція після гілки повинна залишатися порожньою. Крім того, цей тип слота затримки означає, що гілки повинні використовувати ціль гілки, яка на 1 адресу менша, ніж фактична цільова інструкція, для обліку приросту ПК, який виникає.
Якщо коротко, оскільки результат попередньої інструкції записується в оперативну пам'ять після отримання наступної інструкції, умовні стрибки повинні мати після них порожню інструкцію, інакше ПК не буде оновлений належним чином для стрибка.
3. Прочитайте дані аргументів поточної інструкції з оперативної пам'яті
Як було сказано раніше, кожен з трьох операндів складається як з даних даних, так і з режиму адресації. Слово даних - 16 біт, така ж ширина, як оперативна пам'ять. Режим адресації - 2 біти.
Режими адресації можуть бути джерелом значної складності для такого процесора, оскільки багато реальних режимів адресації передбачають багатоетапні обчислення (наприклад, додавання компенсацій). У той же час універсальні режими адресації відіграють важливу роль у зручності використання процесора.
Ми прагнули уніфікувати концепції використання твердо кодованих чисел як операндів та використання адрес даних як операндів. Це призвело до створення режимів адресації на основі лічильника: режим адресації операнда - це просто число, що представляє, скільки разів дані повинні бути надіслані навколо циклу зчитування ОЗУ. Це охоплює негайне, пряме, опосередковане та подвійне непряме звернення.
00 Immediate: A hard-coded value. (no RAM reads)
01 Direct: Read data from this RAM address. (one RAM read)
10 Indirect: Read data from the address given at this address. (two RAM reads)
11 Double-indirect: Read data from the address given at the address given by this address. (three RAM reads)
Після того, як проводиться розмежування, три операнди інструкції виконують різні ролі. Перший операнд, як правило, є першим аргументом для двійкового оператора, але також служить умовою, коли поточна інструкція є умовним переміщенням. Другий операнд служить другим аргументом для двійкового оператора. Третій операнд служить адресою призначення для результату інструкції.
Оскільки перші дві інструкції служать даними, а третя - адресою, режими адресації мають дещо різні інтерпретації залежно від того, в якому положенні вони використовуються. Наприклад, прямий режим використовується для зчитування даних з фіксованої адреси ОЗУ (оскільки потрібне одне зчитування ОЗУ), але негайний режим використовується для запису даних на фіксовану RAM-адресу (оскільки читання оперативної пам'яті не потрібно).
4. Обчисліть результат
Опкод і перші два операнди надсилаються в ALU для виконання бінарної операції. Для арифметичних, бітових та зсувних операцій це означає виконання відповідної операції. Для умовних рухів це означає просто повернути другий операнд.
Опкод і перший операнд використовуються для обчислення умови, яка визначає, записувати результат у пам'ять чи ні. У випадку умовних ходів це означає або визначення того, чи є якийсь біт в операнді 1 (для MNZ
), або визначення, чи є бітовий знак 1 (для MLZ
). Якщо опкод не є умовним переміщенням, то запис завжди виконується (умова завжди вірна).
5. Збільшення лічильника програми
Нарешті, лічильник програм зчитується, збільшується та записується.
Через позицію приросту ПК на ПК між записом читання та записом інструкції, це означає, що інструкція, що збільшує ПК на 1, є неоперативною. Інструкція, що копіює ПК на себе, призводить до того, що наступна інструкція буде виконуватися двічі поспіль. Але будьте попереджені, кілька інструкцій на ПК підряд можуть спричинити складні ефекти, включаючи нескінченне циклічне циклічне завершення, якщо ви не звернете уваги на інструкцію.
Квест Тетріс Асамблеї
Ми створили нову мову збірки під назвою QFTASM для нашого процесора. Ця мова збірки відповідає 1-до-1 машинному коду в ПЗУ комп'ютера.
Будь-яка програма QFTASM пишеться у вигляді серії інструкцій, по одній на рядок. Кожен рядок відформатований так:
[line numbering] [opcode] [arg1] [arg2] [arg3]; [optional comment]
Список опкодів
Як було обговорено раніше, існує одинадцять опкодів, підтримуваних комп'ютером, кожен з яких має три операнди:
MNZ [test] [value] [dest] – Move if Not Zero; sets [dest] to [value] if [test] is not zero.
MLZ [test] [value] [dest] – Move if Less than Zero; sets [dest] to [value] if [test] is less than zero.
ADD [val1] [val2] [dest] – ADDition; store [val1] + [val2] in [dest].
SUB [val1] [val2] [dest] – SUBtraction; store [val1] - [val2] in [dest].
AND [val1] [val2] [dest] – bitwise AND; store [val1] & [val2] in [dest].
OR [val1] [val2] [dest] – bitwise OR; store [val1] | [val2] in [dest].
XOR [val1] [val2] [dest] – bitwise XOR; store [val1] ^ [val2] in [dest].
ANT [val1] [val2] [dest] – bitwise And-NoT; store [val1] & (![val2]) in [dest].
SL [val1] [val2] [dest] – Shift Left; store [val1] << [val2] in [dest].
SRL [val1] [val2] [dest] – Shift Right Logical; store [val1] >>> [val2] in [dest]. Doesn't preserve sign.
SRA [val1] [val2] [dest] – Shift Right Arithmetic; store [val1] >> [val2] in [dest], while preserving sign.
Режими адресації
Кожен з операндів містить як значення даних, так і адресний хід. Значення даних описується десятковим числом у діапазоні від -32768 до 32767. Режим адресації описується однобуквеним префіксом до значення даних.
mode name prefix
0 immediate (none)
1 direct A
2 indirect B
3 double-indirect C
Приклад коду
Послідовність Фібоначчі в п'яти рядках:
0. MLZ -1 1 1; initial value
1. MLZ -1 A2 3; start loop, shift data
2. MLZ -1 A1 2; shift data
3. MLZ -1 0 0; end loop
4. ADD A2 A3 1; branch delay slot, compute next term
Цей код обчислює послідовність Фібоначчі, RAM-адреса 1 містить поточний термін. Він швидко переповнює після 28657.
Сірий код:
0. MLZ -1 5 1; initial value for RAM address to write to
1. SUB A1 5 2; start loop, determine what binary number to covert to Gray code
2. SRL A2 1 3; shift right by 1
3. XOR A2 A3 A1; XOR and store Gray code in destination address
4. SUB B1 42 4; take the Gray code and subtract 42 (101010)
5. MNZ A4 0 0; if the result is not zero (Gray code != 101010) repeat loop
6. ADD A1 1 1; branch delay slot, increment destination address
Ця програма обчислює сірий код і зберігає код у послідовних адресах, починаючи з адреси 5. Ця програма використовує кілька важливих функцій, таких як непряма адресація та умовний стрибок. Він зупиняється, як тільки виникає результат сірого коду 101010
, що відбувається для введення 51 за адресою 56.
Інтернет-перекладач
El'endia Starman створив дуже корисний онлайн - перекладач тут . Ви можете переглядати код, встановлювати точки перерви, виконувати ручне записування в ОЗУ та візуалізувати ОЗУ як дисплей.
Cogol
Після того, як архітектура та мова складання були визначені, наступним кроком на «програмній» стороні проекту було створення мови вищого рівня, щось підходяще для Tetris. Таким чином я створив Cogol . Назва є як каламбур на "COBOL", так і абревіатура для "C of Game of Life", хоча варто відзначити, що Cogol - це C, що наш комп'ютер - власне комп'ютер.
Cogol існує на рівні трохи вище мови складання. Як правило, більшість рядків у програмі Cogol відповідають одному рядку складання, але є деякі важливі особливості мови:
- Основні функції включають названі змінні з призначеннями та оператори, які мають більш читабельний синтаксис. Наприклад,
ADD A1 A2 3
стає z = x + y;
, коли компілятор відображає змінні на адреси.
- Циклічні конструкції, такі як
if(){}
, while(){}
і do{}while();
так, компілятор обробляє розгалуження.
- Одновимірні масиви (з арифметикою вказівника), які використовуються для дошки Tetris.
- Підпрограми та стек дзвінків. Вони корисні для запобігання дублювання великих фрагментів коду та для підтримки рекурсії.
Компілятор (який я написав з нуля) дуже базовий / наївний, але я намагався оптимізувати декілька мовних конструкцій, щоб досягти короткої тривалості складеної програми.
Ось декілька коротких оглядів роботи різних функцій мови:
Токенізація
Вихідний код токенізується лінійно (однопрохідний), використовуючи прості правила, щодо яких символи можуть бути суміжними в токені. Коли зустрічається символ, який не може бути суміжним з останнім символом поточного маркера, поточний маркер вважається завершеним, а новий символ починає новий маркер. Деякі символи (наприклад, {
або ,
) не можуть примикати до будь-яких інших символів і тому є їх власним маркером. Інші (як >
і =
) дозволяється тільки бути поруч з іншими персонажами в рамках свого класу, і таким чином можуть утворювати маркери , такі як >>>
, ==
або >=
, але не подобається =2
. Символи пробілів примушують межу між маркерами, але самі не включаються в результат. Найскладніший персонаж для токенізації - це-
тому що він може представляти як віднімання, так і одинарне заперечення, і, отже, вимагає певного кожуха.
Розбір
Парсинг також робиться в один прохід. У компіляторі є методи обробки кожної з різних мовних конструкцій, і маркери вискакують із глобального списку токенів, оскільки вони використовуються різними методами компілятора. Якщо компілятор коли-небудь бачить маркер, якого він не очікує, він викликає синтаксичну помилку.
Розподіл глобальної пам'яті
Компілятор призначає кожній глобальній змінній (слову чи масиву) свої власні призначені RAM-адреси. Необхідно оголосити всі змінні за допомогою ключового словаmy
щоб компілятор знав виділити для нього простір. Набагато крутіше, ніж названі глобальні змінні, - це управління пам'яттю адреси подряпин. Багато інструкцій (зокрема умовні умови та багато доступу до масиву) вимагають тимчасових "подряпин" адрес для зберігання проміжних обчислень. Під час процесу компіляції компілятор при необхідності виділяє та виділяє адреси подряпин. Якщо компілятору потрібно більше адрес нуля, він буде виділяти більше оперативної пам’яті як адреси скретчу. Я вважаю, що для програми типово потрібно лише кілька адрес нуля, хоча кожна адреса нуля буде використовуватися багато разів.
IF-ELSE
Заяви
Синтаксис if-else
висловлювань є стандартною формою С:
other code
if (cond) {
first body
} else {
second body
}
other code
При перетворенні в QFTASM код розташовується так:
other code
condition test
conditional jump
first body
unconditional jump
second body (conditional jump target)
other code (unconditional jump target)
Якщо перший корпус виконаний, другий корпус пропускається. Якщо перший корпус пропущений, другий корпус виконується.
У зборі тест на стан зазвичай є лише відніманням, і ознака результату визначає, чи слід робити стрибок або виконувати тіло. MLZ
Інструкція використовується для обробки нерівності , такі як >
або <=
. Для MNZ
обробки використовується інструкція ==
, оскільки вона перестрибує тіло, коли різниця не дорівнює нулю (і, отже, аргументи не рівні). Наразі багатовиразні умови не підтримуються.
Якщо else
оператор пропущено, безумовний стрибок також опущений, і код QFTASM виглядає так:
other code
condition test
conditional jump
body
other code (conditional jump target)
WHILE
Заяви
Синтаксис while
висловлювань також є стандартною формою С:
other code
while (cond) {
body
}
other code
При перетворенні в QFTASM код розташовується так:
other code
unconditional jump
body (conditional jump target)
condition test (unconditional jump target)
conditional jump
other code
Тестування стану та умовний стрибок знаходяться в кінці блоку, а це означає, що вони виконуються після кожного виконання блоку. Коли стан повертається помилковим, тіло не повторюється і цикл закінчується. Під час початку виконання циклу контрольний потік стрибає через тіло циклу до коду умови, тому тіло ніколи не виконується, якщо умова вперше помилкова.
MLZ
Інструкція використовується для обробки нерівності , такі як >
або <=
. На відміну від if
висловлювань, MNZ
для обробки використовується інструкція !=
, оскільки вона стрибає до тіла, коли різниця не дорівнює нулю (а отже, коли аргументи не рівні).
DO-WHILE
Заяви
Єдина відмінність між while
і do-while
полягає в тому, що do-while
тіло циклу спочатку не пропускається, тому воно завжди виконується принаймні один раз. Я, як правило, використовую do-while
оператори, щоб зберегти пару рядків коду складання, коли я знаю, що цикл ніколи не потрібно буде повністю пропускати.
Масиви
Одновимірні масиви реалізуються як суміжні блоки пам'яті. Усі масиви мають фіксовану довжину на основі їх декларації. Масиви оголошуються так:
my alpha[3]; # empty array
my beta[11] = {3,2,7,8}; # first four elements are pre-loaded with those values
Для масиву це можливе відображення оперативної пам'яті, що показує, як адреси 15-18 зарезервовані для масиву:
15: alpha
16: alpha[0]
17: alpha[1]
18: alpha[2]
Позначена адреса alpha
заповнюється вказівником на місце розташування alpha[0]
, тому в цьому випадку випадок 15 містить значення 16. alpha
Змінна може використовуватися всередині коду Cogol, можливо, як вказівник стека, якщо ви хочете використовувати цей масив як стек .
Доступ до елементів масиву здійснюється за допомогою стандартних array[index]
позначень. Якщо значення index
є постійним, ця посилання автоматично заповнюється абсолютною адресою цього елемента. В іншому випадку він виконує деяку арифметику вказівника (просто додавання), щоб знайти потрібну абсолютну адресу. Також можливо індексація гнізд, наприклад alpha[beta[1]]
.
Підпрограми та виклики
Підпрограми - це блоки коду, які можна викликати з декількох контекстів, запобігаючи дублюванню коду і дозволяючи створювати рекурсивні програми. Ось програма з рекурсивною підпрограмою для генерування чисел Фібоначчі (в основному найповільніший алгоритм):
# recursively calculate the 10th Fibonacci number
call display = fib(10).sum;
sub fib(cur,sum) {
if (cur <= 2) {
sum = 1;
return;
}
cur--;
call sum = fib(cur).sum;
cur--;
call sum += fib(cur).sum;
}
Підпрограма оголошується ключовим словом sub
, а підпрограму можна розмістити в будь-якому місці програми. Кожна підпрограма може мати кілька локальних змінних, які оголошуються як частина її списку аргументів. Цим аргументам також можуть бути задані значення за замовчуванням.
Для обробки рекурсивних викликів локальні змінні підпрограми зберігаються у стеці. Остання статична змінна в оперативній пам'яті - вказівник стека викликів, а вся пам'ять після цього служить стеком викликів. Коли викликається підпрограма, вона створила новий фрейм на стеку викликів, який включає всі локальні змінні, а також зворотну (ROM) адресу. Кожній підпрограмі в програмі надається одна статична RAM-адреса, яка служить вказівником. Цей покажчик дає розташування "поточного" виклику підпрограми в стеці викликів. Посилання на локальну змінну проводиться за допомогою значення цього статичного вказівника плюс зміщення для надання адреси цієї конкретної локальної змінної. Також у стеці виклику міститься попереднє значення статичного вказівника. Ось '
RAM map:
0: pc
1: display
2: scratch0
3: fib
4: scratch1
5: scratch2
6: scratch3
7: call
fib map:
0: return
1: previous_call
2: cur
3: sum
Одна цікава річ у підпрограмах - це те, що вони не повертають якоїсь особливої цінності. Швидше за все, всі локальні змінні підпрограми можуть бути прочитані після виконання підпрограми, тому з виклику підпрограми можна отримати різноманітні дані. Це досягається за допомогою збереження вказівника на той конкретний виклик підпрограми, який потім може бути використаний для відновлення будь-якої локальної змінної зсередини (нещодавно розміщеного) кадру стека.
Існує кілька способів викликати підпрограму, використовуючи call
ключове слово:
call fib(10); # subroutine is executed, no return vaue is stored
call pointer = fib(10); # execute subroutine and return a pointer
display = pointer.sum; # access a local variable and assign it to a global variable
call display = fib(10).sum; # immediately store a return value
call display += fib(10).sum; # other types of assignment operators can also be used with a return value
Будь-яка кількість значень може бути задана як аргумент для виклику підпрограми. Будь-який аргумент, що не надається, буде заповнений його значенням за замовчуванням, якщо воно є. Аргумент, який не надається і не має значення за замовчуванням, не очищається (щоб зберегти інструкції / час), тому потенційно може набути будь-якого значення на початку підпрограми.
Покажчики - це спосіб доступу до кількох локальних змінних підпрограми, хоча важливо зазначити, що вказівник є лише тимчасовим: дані, на які вказує вказівник, будуть знищені, коли буде здійснено інший виклик підпрограми.
Налагодження міток
Будь- {...}
якому кодовому блоку в програмі Cogol може передувати багатословна описова мітка. Ця мітка додається як коментар до складеного коду асемблери та може бути дуже корисною для налагодження, оскільки полегшує пошук конкретних фрагментів коду.
Оптимізація слота затримки філії
Щоб підвищити швидкість компільованого коду, компілятор Cogol виконує деяку дійсно основну оптимізацію слота затримки як остаточне проходження коду QFTASM. Для будь-якого безумовного стрибка з порожнім слотом затримки гілки, проміжок затримки може бути заповнений першою інструкцією в пункті стрибка, а пункт стрибка збільшується на один, щоб вказувати на наступну інструкцію. Це, як правило, економить один цикл кожного разу, коли виконується безумовний стрибок.
Написання коду тетрісу в Коголі
Остаточна програма Tetris була написана на Cogol, а вихідний код доступний тут . Складений код QFTASM доступний тут . Для зручності тут надається постійна посилання: Tetris in QFTASM . Оскільки мета полягала в тому, щоб збільшити код збірки (а не код Когола), результат кого Когола непростий. Багато частин програми зазвичай розташовуються в підпрограмах, але ці підпрограми насправді були досить короткими, що дублювання коду зберігало інструкції надcall
заяви. Кінцевий код має лише одну підпрограму на додаток до основного коду. Крім того, багато масивів було видалено і замінено або на еквівалентно довгий список окремих змінних, або на безліч твердо кодованих чисел у програмі. Остаточний складений код QFTASM знаходиться під 300 інструкціями, хоча він лише трохи довший, ніж сам джерело Cogol.