Threaded code представляет собой уникальный подход к организации программного кода и его выполнению, который заслуживает подробного рассмотрения. Его корни связаны с языком Forth, где концепция обеспечения компактности, расширяемости и простоты исполнения является краеугольным камнем разработки. Раскрывая суть threaded code, важно понимать, что он основан на разбиении программ на два основных типа подпрограмм — leaf routines и twig routines, которые в русском языке можно адаптировать как «листовые» и «ветвевые» процедуры. Листовые процедуры — это непосредственно исполняемый машинный код, который занимается вычислениями и вводом-выводом, не вызывая других подпрограмм. В то время как ветвевые процедуры, напротив, состоят исключительно из вызовов других процедур, управляющих ходом программы.
Такой подход кажется на первый взгляд устаревшим или неэффективным из-за отказа от инлайнинга функций и непосредственного написания машинного кода. Однако у него есть свои уникальные достоинства и особенности, которые обеспечивают определённые преимущества в реализации интерпретаторов и компиляторов. После трансформации программы таким образом, что тело ветвевой процедуры представляет собой упорядоченный список адресов вызываемых слов, можно избавиться от вызовов в явном виде и заменить их только этим списком. Тогда «ветвь» перестаёт быть классическим кодом, поскольку содержит не инструкции, а адреса, которые должны быть интерпретированы особым образом. Это требует введения некоего промежуточного элемента интерпретации — например, небольшой процедуры, которая именуется NEXT в терминологии Forth.
NEXT отвечает за последовательный выбор следующего адреса в списке и переход по этому адресу, будь то листовое слово с машинным кодом или же очередная ветвь. Главная сложность возникает при вложенных вызовах ветвевых процедур: чтобы не потерять адрес возврата, необходимо хранить указатель интерпретации (instruction pointer) в отдельном стеке вызовов, помимо стандартного стека данных. Именно для этих целей в threaded code используется специальный стек возвратов (return stack), на который помещается текущее положение в вызывающей процедуре перед переходом к новой. В Forth этот механизм реализован через небольшие процедуры — NEST (вход в новую процедуру) и UNNEST (выход и возврат к предыдущей). Таким образом, запуск программы сводится к установке указателя интерпретации на начало списка адресов, а далее управление передаётся NEXT, который и осуществляет последовательную «интерпретацию» кода.
Такой подход называют direct-threaded code (DTC), то есть прямой потоковой реализацией. В DTC NEXT непосредственно получает из списка адрес кода и переходит к его выполнению напрямую. Однако, для современных процессоров с глубокой конвейеризацией и дефицитом эффективности на множественные ветвления такая реализация иногда может привести к значительным накладным расходам из-за неэффективных переходов. Для смягчения этой проблемы был предложен вариант indirect-threaded code (ITC), где в списке хранятся указатели не на код напрямую, а на адреса, содержащие команды, таким образом добавляется уровень косвенной адресации. NEXT в этом случае сначала извлекает адрес из списка, затем получает из этого адреса фактический указатель на код и уже затем переходит к выполнению.
Такая схема упрощает некоторые аспекты реализации, позволяя, к примеру, стандартизировать заголовки слов и оптимизировать операции загрузки команд процессором. Кроме того, ITC требует модификации представления листовых слов: теперь они начинают не с машинного кода, а с указателя на машинный код, так называемого code pointer, и только затем следует тело кода. Этот подход вводит новое смысловое разделение — code field (кодовое поле) и parameter field (параметрическое поле). Code field содержит указатель на исполняемый код, а parameter field хранит данные, используемые этим кодом. Интересно, что такой «поточный» подход в threaded code легко распространяется и на данные, которые в Forth рассматриваются как исполняемые объекты.
Таким образом, константы, переменные и даже более сложные структуры данных реализуются подобно процедурам с собственным поведением при выполнении. Константа, например, при своей «исполнении» помещает своё значение в стек данных, а переменная — указатель на свой параметр, который затем может быть прочитан или изменён специальными словами @ и !. Такая унификация позволяет создавать расширяемые и лёгкие для модификации языки, где и код и данные — это исполняемые «слова», которые можно комбинировать и переопределять. Одним из несомненных преимуществ threaded code является его простота и универсальность. Благодаря своей структуре процесс реализации языка становится более упорядоченным и модульным, что облегчает портирование на новые архитектуры.
Высокий уровень абстракции предоставляет большие возможности для пользователей создавать новые конструкции без изменения внутреннего механизма компилятора или интерпретатора. Другой значимый плюс — компактность программ. Поскольку все операции вынесены в отдельные слова, код больше не дублируется, что экономит место в памяти и упрощает сопровождение. Несмотря потерю инлайнинга и возможные накладные расходы на переключение между словами, threaded code иногда выигрывает по производительности за счёт снижения количества инструкций вызова и возврата — частое явление в традиционных программах. Однако есть и минусы.
Прежде всего, это скорость исполнения. Отсутствие прямого инлайнинга и дополнительные переходы снижают производительность в сравнении с написанным «родным» машинным кодом. Также размер адресов в некоторых архитектурах может увеличить объем программ при бюджетных ресурсах памяти. Ещё одна проблема — невозможность оптимально использовать хвостовую рекурсию из-за отсутствия инструмента для преобразования вызова в переход, что иногда приводит к неэффективному использованию стека. В целом, threaded code представляет собой изящный компромисс между простотой реализации, универсальностью и расширяемостью с одной стороны, и скоростью исполнения с другой.
Это делает его популярным выбором для интерпретации новых языков, систем с ограниченными ресурсами и обучающих проектов, демонстрирующих фундаментальные принципы программирования. Понимание внутренней архитектуры — от разделения на листовые и ветвевые слова до работы stэков return stack и data stack — открывает широкие горизонты для создания собственных интерпретаторов и компиляторов на базе threaded code. Кроме того, threaded code позволяет легко внедрять новые структуры данных и управлять ими как исполняемыми элементами, что придает языкоподобным системам большую гибкость. Можно сказать, что благодаря своей элегантности и однообразию, threaded code стал ключевым элементом в развитии языков типа Forth и повлиял на дизайн множества интерпретаторов, демонстрируя альтернативный взгляд на исполнение программ. Для глубокого освоения threaded code рекомендуется изучение его вариаций, особенности работы с литералами, управляющими конструкциями и компиляторами.
Осознание разницы между call и branch-and-link в контексте threaded code помогает лучше понять организацию низкоуровневого управления потоком и переходами. Наконец, threaded code – это не только способ написания программ, а целая философия взаимодействия кода и данных, упрощающая создание собственных языков и систем с высокой степенью адаптивности и контроля над процессом исполнения.