spamsink: (Default)
[personal profile] spamsink
Почти три года назад я писал про систему нижнего уровня базы данных, которая работала с помощью микрокоманд. За прошедшие три года я с ней практически разобрался, и с точки зрения структуры данных она ничего особенного не представляет. Ну B+tree, ну записи с возможностью фрагментации на экстенты, ну иерархичность. В этом посте речь пойдёт про механизм работы микрокоманд, подобного которому я раньше не встречал. Дальше много технических деталей.



Представьте себе программу, записываемую в виде байт-кода, разбитого на слова по 8 байт в каждом. Для удобства пользователей код 00 - NOP.

У виртуальной машины, которая исполняет этот байт-код, есть 64 регистра, у большинства из которых назначение фиксировано (потому что практически все команды работы с данными не имеют операндов, а извлекают данные и пишут результаты в конкретные регистры - так, например, по ссылке выше коды 14 и 15 выполняют сравнение двух фиксированных регистров, содержимое которых "искомый ключ" и "найденный ключ"). Доступны для произвольного применения от силы полдюжины.

В частности, один из регистров "текущее командное слово" (IW). Перед запуском машины нужно извне записать командное слово в этот регистр и стартовать её. Самостоятельно выбирать следующее командное слово из памяти машина не умеет, поэтому по умолчанию после выполнения всех 8 команд в слове машина останавливается.

Для выполнения программ, состоящих из более чем 8 команд, существует команда CHAIN nn с аргументом (следующим за ней байтом), означающим номер регистра, в котором находится указатель на следующее командное слово. По этой команде происходит IW = *R[nn]++ и выполнение только что измененного командного слова начинается с первой команды в нём. Регистр 00 доступен для этой команды; если байт-код CHAIN - последний в слове, то считается, что его операнд - 00.

Таким образом, линейные программы любой разумной длины выполняются, например, с помощью IW = CHAIN; R[0] = program; start, где program - массив слов, в каждом из которых может быть вплоть до 7 полезных байт-кодов, а последний (или последний ненулевой) байт-код - CHAIN. Кроме последнего слова программы, разумеется.

Также из простых команд имеется команда LOOP - начать выполнение IW сначала, команда ASSIGN aa bb, делающая R[aa] = R[bb], команда SKIP - пропустить следующий байт-код, и команда STOP - завершить выполнение программы.

Пример использования команды STOP: последовательность кодов в микропрограмме модификации записи по ссылке выше FIND NOMATCH NOP (ALLOC INSERT STOP) UPDATE. В скобках - команды, которые выполняются если FIND не нашел запись с искомым ключом; тогда в базе резервируется место для содержимого записи и переписываются данные (ALLOC), связываются со вставляемым ключом (INSERT), и хорош. Иначе выполняется только UPDATE: новые данные переписываются в поле данных найденной записи с искомым ключом.

Команд, имеющих хоть какое-то условное действие, не так много. Упомянутые выше команды 14 (MATCH) и 15 (NOMATCH) выполняются так: если регистры KEY и CURKEY равны/неравны (MATCH/NOMATCH), то продолжить выполнение как ни в чём не бывало; если же условие не выполнено, а следующая команда - NOP, и за ней ещё есть ненулевые команды, то пропустить этот NOP и ещё 3 байт-кода; иначе диагностировать ошибку.
Так же работают и команды инкремента/декремента итератора по базе.

Ещё есть команда сравнения содержимого базы с памятью, которая при несовпадении пропускает 1 байт-код (надо думать, ради CMP CHAIN NOP ... - переход на следующее слово по совпадению, CMP SKIP CHAIN NOP ... - переход по несовпадению), и команда, которая при некотором условии пропускает 4 байт-кода.

Все эти различные количества пропускаемых байт-кодов - явно подгонка под какие-то конкретные действия для экономии, потому что теоретически в общем случае условный переход делается с помощью пропуска двух байт-кодов (CHAIN nn).

Казалось бы, и что? Как из этих даже нескольких счетчиков адреса команды можно сделать что-то обобщённое, если команда LOOP работает только в пределах одного командного слова? А вот как: ещё есть команда LOADINC xx yy, которая делает ++R[yy]; R[xx] = *(R[yy]-1); и с её помощью (главное, с помощью магической самоссылающейся LOADINC xx xx), насколько я понимаю, можно организовать передачи управления произвольного вида.

Вопрос вот в чём. Кто-нибудь видел где-нибудь что-нибудь подобное?

Безотносительно к ответу на предыдущий вопрос, есть ли идеи, как преобразовывать control flow произвольного вида в набор массивов для CHAIN и LOADINC, чтобы не корпеть над ручным программированием на этих палках и верёвках?

Ах да, самое главное не сказал: регистр с номером, совпадающим с байт-кодом LOADINC, доступен для использования. Это явно неспроста.

Date: 2025-01-17 07:56 pm (UTC)
sab123: (Default)
From: [personal profile] sab123
Как работает LOADINC, я так и не понял. Ну, читает слово по адресу, а как переход-то? Или это имеется в виду, что LOADINC читает следующий указатель на слово из вектора указателей, на который потом идет CHAIN?

А напоминает это ранний Бейсик или Фортран. Для команд скорее всего был какой-нибудь компилятор?

Кстати, не одно ли и то же ли (++R[yy]-1) и R[yy]++ ?

Date: 2025-01-17 09:56 pm (UTC)
sab123: (Default)
From: [personal profile] sab123
По поводу неоднозначности, мне кажется, что в стандартном Си неоднозначность будет и так и так. Но тут у нас не Си, поэтому можно определить удобный нам порядок выполнения. В частности, в PDP-11 операция "MOV (R1)+, R1" имела порядок выполнения, в котором автоинкремент выполнялся до присваивания. И, кстати, вроде и в до-стандартном компиляторе Си - де-факто "x = *x++" тоже имело такой порядок выполнения.

Date: 2025-01-17 09:59 pm (UTC)
sab123: (Default)
From: [personal profile] sab123
Вообще это выглядит как традиционное решение, где программа компилируется в промежуточный код некоей виртуальной машины, который интерпретируется. И скорее всего виртуальная машина была подогнана под одновременно скорость исполнения интерпретатором и удобство конкретного компилятора. Потому могут быть и разные специальные случаи, что они соответствуют конкретному коду в компиляторе.

Date: 2025-01-17 11:09 pm (UTC)
sab123: (Default)
From: [personal profile] sab123
Фортовский "шитый код" - тоже аналогичная конструкция.

Date: 2025-01-18 04:23 am (UTC)
sab123: (Default)
From: [personal profile] sab123
До меня наконец-то дошло, что да, LOADINC - это аналог вызова функции. И видимо да, программа строилась из таких готовых функций. Более того, он больше всего похож на асинхронное программирование с futures/promises, когда во фьючере передается адрес следующей функции для вызова после возврата из вызываемой сейчас, и строятся цепочки таких фьючеров. И даже современным фьючерам можно у него поучиться.
Edited Date: 2025-01-18 04:25 am (UTC)

Profile

spamsink: (Default)
spamsink

June 2025

S M T W T F S
1 2 34567
89 1011 121314
1516 1718192021
222324 25262728
2930     

Most Popular Tags

Page Summary

Style Credit

Expand Cut Tags

No cut tags
Page generated Jul. 6th, 2025 12:55 am
Powered by Dreamwidth Studios