В идущем процессе написания "небайтного" процессора наткнулся на одну мысль, КМК, вполне годную и сейчас для каких-нить "нормальных" процессоров и контроллеров без всякой привязки к размерам слова.
Простая идея даёт некоторые ощутимые выгоды по сравнению с RISC (в каком-то смысле, это развитие RISC) и упрощает написание очень даже годного суперскалярного процессора до уровня, когда это посильно одиночке-любителю. А заодно значительно ускоряет выполнение, снижает нагрузку на регистры и повышает плотность кода. Одновременно. И бесплатно.

Предлагаю попинать ногами и показать на недостатки.
Начну чуть издалека, чтобы было понятно, почему и нафига.
Аксиоматично, любую команду программы можно исполнить только исполнив все команды, от которых она зависит. Этакий направленый граф, скорость продвижения по которому нужно максимизировать (для универсальных процессоров это возможно за счёт максимального числа параллельно выполняемых команд). Вопрос лишь в том, как оптимально представить этот граф в коде. Сори, но придётся прожевать общеизвестное.
Традиционно "стыковку" вершин рёбрами во всех популярных архитектурах делают через регистры. Например:
0001 add A, B,
C
0002 add B, D, D
0005 sub
C, E, F
Неважно, сколько команд находится между ними, 3-я команда зависит от первой, и это выражено через регистр R3: первая команда туда пишет, третья читает.
В традиционном RISC-суперскаляре это используется для внеочередного исполнения команд: если вторая команда не использует как аргумент результат первой, то её можно выполнять вне очереди.
Но это реально дорого - нужно в аппаратуре отслеживать эти зависимости, приходится переименовывать (внтури, аппаратурой) регистры. Но по сути
хранение результата в регистре С нужно только для того чтобы связать команды 1 и 3 и передать результат из первой команды в третью. Только для этого. По сути - это не хранилище, это просто метка связи команд "результат отсюда нужно использовать здесь, здесь, а ещё здесь".
То есть, у нас написано, что "загрузить А, прибавить B,
записать в С, потом загрузить С, вычесть Е, записать в F" а по факту нам нужно "загрузить А, прибавить B, вычесть Е, записать в F" (возможно, с записью и промежуточного результата, но это чуть позже). То есть, железо реально исполняет (тратит время, энергию) операции, которые нужны только для логической связи команд.
...
Теперь смотрим на безнадёжно устаревшую аккумуляторную архитектуру.
Фактически, аккумулятор там - параметр неявный, он почти всегда подразумевается, и часто используется как результат. Получается длинная сопля связанных команд, которые сложно разделить. Всем нужен аккумулятор, внеочередное исполнение очень сложно и редко когда в таком коде можно вычислить, малый размер единичной команды даёт всё равно низкую плотность кода из-за постоянной пересылки данных из и в аккумулятор... и т.д. и т.п.
...
А теперь допустим, что у нас код состоит из фрагментов, каждый из которых подразумевает загрузку чего-то, какую-то операцию и передачу результата дальше.
Допустим, у нас есть 3-6 команд (которые читаются пачкой в одном 32-бит или 64-битном слове), которые зависят друг от друга и поэтому
обязаны выполняться последовательно. Не просто потому, что они так записаны, а потому что по сути кода они зависят друг от друга.
Мы загружаем некий регистр, что-то прибавляем, кладём результат в другой регистр, что-то вычитаем, умножаем и т.п.
То есть, вместо закодированной в слове команды (RISC) A -> вычесть B -> A
у нас в слове закодирована цепочка (именно последовательная, зависимая цепочка!) команд A -> вычесть B -> прибавить D -> сохранить в Е -> вычесть F -> A
Формат отдельной команды при этом предельно простой, на зависть любому RISC:
либо [opcode] [R], если команда имеет 2 аргумента и 1 результат
либо [0000] [opcode], если команда имеет 0-1 аргумент и 0-1 результат.
А размер достаточно мал: в 10-битной команде помещается 31+32 = 63 опкода.
Декодер предельно прост.
...
Что получается?
- Во-первых, мы резко сокращаем работу железа по переупорядочиванию инструкций, у нас же явно указана зависимость коротких цепочек команд. Точнее, нам эта работа почти не нужна теперь, можно и без неё. Отдельные цепочки могут выполняться одновременно (просто ожидая друг друга, если возникают взаимозависимости). "Нанонити" на уровне выполнения команд.
- Во-вторых, на каждой команде мы экономим адрес регистра (если сравнивать с компактной записью add A, B; A<-A+B) или дважды адрес регистра (если сравнивать с add A, B, C).
- В-третьих, мы резко экономим обращения к пулу регистров. Мы зачитали в одном слове на исполнение 6 команд, которые должны исполняться последовательно, мы можем коммутировать ИУ непосредственно друг на друга (или на буфера внутри, неважно), для передачи данных из команды в команду нам не нужно обращаться к регистрам и мешать другим, мы делаем это внутри блока. К регистрам мы обращаемся только когда нам реально нужно что-то взять из регистра или положить в регистр. Реально, резко! Для типичной команды - втрое!
- В-четвёртых, мы чуток экономим сами регистры (почти не нужны временные "разменные" регистры; в основном только регистры-переменные, в которых реально хранится что-то более осмысленное).
- В-пятых, простой и прямой декодер (команды почти-равны микрокоду) позволяет исполнять несколько
последовательных простых команд за такт, просто коммутируя ИУ друг на друга. Доступа к внешним шинам не нужно же, всё внутри. Это критичное ускорение, сравнимое с тем, что делают очень сложными способами монструозные процессоры.
- В-пятых, и самое интересное для меня: резко упрощается реализация суперскалярности. Поскольку каждое слово состоит из просто (напрямую логикой за один такт) декодируемых команд, нужно только пометить регистры, которые будут изменены после прохождения блока, после изменения этого регистра - снимаем блок. Как только в качестве аргумента команды видим хоть один блокированный регистр, просто ждём. Как только деблокируется - едем дальше.
Каждое новое слово мы начинаем исполнять сразу, как только видим отсутствие блокированых операндов (или - об этом чуть позже - он не содержит метку зависимости).
Команды у нас
уже упорядочены компилятором по зависимости, так что у нас почти никогда не возникает ситуации "мы можем что-то исполнить вне очереди". У нас ВСЁ по очереди.
- В-шестых, есть ещё выгоды по главной беде суперскаляров - переходам и долгому заполнению конвееров при неправильном предсказании, есть ещё в-седьмых и далее... но это обсуждаемо после.
Главные недостатки аккумуляторной архитектуры - невозможность одновременного исполнения команд завязаных на аккумулятор и мучения с пересылкой данных из и в аккумулятор. Но тут их нет. Первого - потому что у каждой короткой(!) цепочки команд аккумулятор свой. Второго нет по той же причине: каждая цепочка берёт регистр, что-то там внутри короткое-взаимозависимое делает и отдаёт регистр, зависимых команд в коде много, сформировать из них короткие цепочки можно всегда.
И даже если не получится найти достаточно длинную цепочку зависимых команд (ха! где такой код?), повышенная в 1.5-2 раза плотность кода позволяет даже ставить каждую вторую-третью команду пустой (но я не думаю, что это реальный случай; просто не представляю такой реальный код).
...
Но главное: система в реализации ПРОЩЕ типичного суперскаляра, требует меньше железа в реализации и (в частных случаях) быстрее.