bmstu-iu9 / bmstu-iu9/refal-5-lambda
Рассмотреть переход с C++ на C и сделать выводы
- Dominant language
- C++
- Stars
- 97
- Forks
- 40
- PR merge metrics
- No merged PRs in 30d
Description
Эта задача — подзадача для #185.
Гипотеза: на языке Си получаются более эффективные программы, чем на Си++. Предпосылки.
* Си менее развит для создания абстракций (классы, пространства имён…), поэтому многослойную программу на нём писать сложнее, чем на Си++. Поэтому слоёв меньше и меньше накладных расходов на слои абстракций.
* Язык Си не скрывает сложность — вызов функции в нём выглядит как вызов функции, а не как встроенный оператор, не говоря о неявных вызовах конструкторов копирования и деструкторов.
Поэтому ради повышения быстродействия разумно _рассмотреть_ вариант переписывания на Си. И посмотреть, к каким следствиям это приведёт.
# Очевидные изменения
Очевидно, что в рантайме и `Library` придётся отказаться от классов, пространств имён и стандартной библиотеки Си++.
Классы придётся заменять на структуры, методы — на функции, принимающие указатели на класс. Пространства имён придётся заменять префиксами.
Вместо контейнеров стандартной библиотеки придётся писать свои велосипеды, либо делать интруизивные контейнеры — просто предусматривать, что объекты могут объединяться в список и добавлять для этого поля вроде `next`.
Возникнет головная боль с явным освобождением памяти и других ресурсов.
Но это всё по большей части тривиальный рефакторинг, не вносящий содержательную сложность (но, по опыту разработки Рефала-05, довольно увлекательный).
Будет два очевидных положительных последствия:
* Компиляторов Си больше, чем компиляторов Си++, поэтому диапазон применимых компиляторов расширится.
* Компиляция файлов рантайма и библиотек ускорится, поскольку Си проще и хедеры у него компактнее.
# Нетривиальные изменения
Нетривиальной здесь будет компиляция файлов с нативным кодом. Сейчас она завязана на вызов конструкторов глобальных объектов — возможность, которая есть в стандартном Си++ и которой нет в стандартном Си (есть непереносимые средства в различных реализациях).
## Что у нас есть
Текущее представление развивалось эволюционно, путём постепенного переноса части знаний из Си++ в RASL, внесения реентерантности и т.д. Поэтому (как дальше будет видно), оно кривоватое.
Сгенерированный файл можно условно разбить на несколько частей:
* Комментарий о том, что файл сгенерён автоматически.
* `#include "refalrts.h"`
* `#define cookie_ns cookie_ns_MMMMM_NNNNN` — объявление уникального имени пространства имён, поскольку Open Watcom криво работает с безымянными пространствами имён.
* Определения идентификаторов (типа `IdentReference`), которые используются в нативном коде. В исходнике они должны быть обозначены как `$LABEL`. Имена декорируются
```
static const refalrts::IdentReference ident_END("END");
```
* Определения ссылок на внешние функции (типа `ExternalReference`) вида
```
static refalrts::ExternalReference ref_Addm_Nat("Add-Nat", 4112458675U, 1433710504U);
static refalrts::ExternalReference ref_Br("Br", 0U, 0U);
```
Ссылки даются на все функции из области видимости: как объявленные в `$EXTERN`'ах, так и определённые в файле. Для ссылок указывается имя функции и идентификатор области видимости (два нуля для глобальных, extern или entry, ненулевые cookie для локальных функций).
* Глобальные нативные вставки и определения нативных функций. Последние имеют вид
```
static refalrts::FnResult func_Addm_Digits(
refalrts::VM *vm, refalrts::Iter arg_begin, refalrts::Iter arg_end
) {
(void) vm;
#line 174 "Library.ref"
…
#line 299 "Library.cpp"
}
static refalrts::NativeReference nat_ref_Addm_Digits(
"Add-Digits", 4112458675U, 1433710504U, func_Addm_Digits
);
```
(длинные строки разбил)
* В глобальных нативных вставках могут встретиться глобальные переменные
```
refalrts::GlobalRef g_file_handles(cMaxFileHandles);
```
Все эти объекты — `IdentReference`, `ExternalReference`, `NativeReference`, `GlobalRef<>` полагаются на свои конструкторы для инициализации.
При этом, рантайм у нас реентерантный (#163). А это значит, что два независимых домена могут загрузить один и тот же модуль, и значения, доступные через эти переменные, должны быть в каждом домене свои. Эти значения хранятся в доменах, а в `IdentReference`, `ExternalReference` и `GlogalRef` находятся только смещения для внутреннего массива.
А что же делают глобальные конструкторы. Для типов `*Reference` они провязывают их в односвязный список, голову помещают в глобальную переменную. Для первых двух сущностей устанавливается глобальный счётчик. Для `GlobalGen<>` вычисляется смещение этой переменной во внутримодульной памяти. После инициализации эти объекты более не меняются. Результаты их инициализации (головы списков, максимумы счётчиков) сохраняются в глобальной структуре `NativeModule g_module` — собственно, конструкторы кладут себя на голову списка и увеличивают счётчики в этой структуре.
При загрузке модуля в смысле Рефала (#167) по содержимому `g_module` инициализируются вектора для идентификаторов и внешних ссылок, из списка нативных функций по именам извлекаются указатели на них, для глобальных переменных выделяется соответствующий объём памяти. Модуль может быть загружен в несколько доменов — соответствующие вектора везде будут свои, но индексы одни и те же.
К идентификаторам, внешним функциям и глобальным переменным приходится обращаться вызовом вида `object.ref(vm)`, для того, чтобы обратиться к соответствующему массиву, доступному внутри виртуальной машины. `NativeReference` в пользовательском коде не вызываются.
Все эти объекты возникли эволюционно — сначала они содержали реальные данные, а вместо `GlobalRef<>` были обычные глобальные переменные. Потом с реализацией реентерантности (#163) они стали содержать ссылки.
Недостаток очевиден — сложность и куча уровней косвенности. Он станет ещё очевиднее, когда мы далее рассмотрим более простое и эффективное решение.
## Что с этим делать
Все четыре вида сущностей нужно инициализировать при загрузке модуля (имеются ввиду I- и N-модули). Но конструкторами мы их инициализировать не можем, потому что у нас Си. Можем только константами во время компиляции. Нам нужно, чтобы кто-то дёргал функции инициализации для них.
Отложим на секунду вопрос о том, кто же их дёргает. Подумаем: как?
Допустим, в файле можно использовать объект с конструктором, но только один. Что можно сделать? Можно сформировать массивы указателей соответствующих объектов, и адреса этих массивов передать инициализатору. Примерно так:
```
static IdentReference ident_BEGIN = { 0, "BEGIN" };
static IdentReference ident_END = { 0, "END" };
static ExternalReference ref_Addm_Nat = { 0, "Add-Nat", 4112458675U, 1433710504U };
static ExternalReference ref_Br = { 0, "Br", 0U, 0U };
static FnResult func_Addm_Digits(…) {…}
static NativeReference nat_ref_Addm_Digits = {
"Add-Digits", 4112458675U, 1433710504U, func_Addm_Digits
};
static FnResult func_Subm_Digits(…) {…}
static NativeReference nat_ref_Subm_Digits = {
"Sub-Digits", 4112458675U, 1433710504U, func_Subm_Digits
};
static GlobalRef g_file_handles = { 0, sizeof(FILE*) * cMaxFileHandles };
static IdentReference *idents[] = { &ident_BEGIN, &ident_END, NULL };
static ExternalReference *externals[] = { &ref_Addm_Nat, &ref_Br, NULL };
static NativeReference *natives[] = { &nat_ref_Addm_Digits, &nat_ref_Subm_Digits, NULL };
static GlobalRef *globals[] = { &g_file_handles, NULL }
static Initializer init(idents, externals, natives, globals);
```
Нули символизируют значения счётчика-смещения, `NULL` в конце массива — признак конца. Провязка ссылками `next` здесь уже не нужна (поскольку они доступны из массивов). Остальная логика остаётся неизменной.
Теперь, что делать с инициализатором. Он дёргает глобальные сущности, а кто его дёргает? Чтобы его кто-то дёрнул, он должен быть доступен извне. Плюс его имя должно быть уникальным (чтобы не было конфликта имён с другими единицами трансляции). Плюс его имя должно быть известно извне.
У каждого модуля есть свой уникальный идентификатор — пара cookies. Их можно использовать для формирования уникального имени:
```
…
Initializer init_AAAAA_BBBBB = { idents, externals, natives, globals };
```
На стадии компоновки можно обеспечить знание идентификаторов каждого из модулей, а это значит, можно сделать их «дёргалку»:
```
external Initializer init_AAAAA_BBBBB;
external Initializer init_CCCCC_DDDDD;
bool module_initialized = false;
Initializer *all_initializers[] = { &init_AAAAA_BBBBB, &init_CCCCC_DDDDD, NULL };
```
А рантайм уже знает о том, что в любом модуле с нативным кодом есть глобальный массив `all_initializers`, и по нему нужно пробежать при запуске.
Собственно, вот минимальное изменение инициализации нативных модулей, которое работает без конструкторов глобальных переменных. Фактически, это перенос того, что делает рантайм Си++, в рантайм Рефала. C++, конечно, использует не предопределённые имена переменных (вроде `all_initializers`), а имена секций, но суть не меняется.
С глобальными конструкторами множество всех объектов обходилось дважды — рантаймом C++ для вызова конструкторов и рантаймом Рефала для уже своей инициализации. Список глобальных объектов не был виден рантайму Рефала, поэтому на первом проходе объекты провязывались в списки (доступные рантайму Рефала).
В предлагаемой реализации объекты складываются в массивы, которые видны рантайму Рефала, а значит, (а) обе фазы инициализации выполняются за один проход, (б) провязка ссылками становится не нужна.
Флаг `module_initialized` нужен для обеспечения реентерантности — чтобы избежать повторной инициализации этих объектов из другого домена. Возникает проблема многопоточности, но мы закроем на неё глаза. В дальнейшем мы откажемся от этого флага.
Но это решение почти ничего не упрощает по сравнению с имеющимся — ну, нет односвязных ссылок, и что? А то, что дальше пойдут оптимизации.
## Оптимизации нового представления
Во-первых, можно заменить глобальные объекты и массивы указателей на одни только массивы:
```
enum {
ident_BEGIN,
ident_END,
};
static IdentReference idents[] = {
{ 0, "BEGIN" },
{ 0, "END" },
{ 0, NULL },
};
static FnResult func_…(…) {
… ident_left(ident_ref(BEGIN), bb[1], be[1]) …
}
```
где `ident_ref` определена как
```
#define ident_ref(name) ident_by_id(vm, idents[ident_ ## name].id)
```
А функция `Identifier *ident_by_id(VM *vm, unsigned id)` будет находить по `id` соответствующий идентификатор. Аналогично делаем с `ExternalReference`.
По факту нативный код в этой ситуации идентифицирует объекты по целочисленному индексу. И эти индексы свои для каждой единицы трансляции. По этим индексам из массивов `idents` или `externals` извлекается другой индекс, по которому уже производится обращение ко внутренней таблице модуля.
Почему так сложно? Потому что раньше все объекты объединялись в один список и получали последовательные глобальные номера. Различить объекты из разных единиц трансляции было нельзя. А теперь можно это обеспечить — достаточно `initializer_AAAAA_BBBBB` инициализировать куками.
Лишний уровень косвенности пропадает — у каждой единицы трансляции теперь будут свои массивы идентификаторов и внешних функций, индексация будет одна.
Но ведь у каждой единицы трансляции уже есть свои массивы с локальной нумерацией! При генерации RASL’а строятся таблицы внешних функций и идентификаторов, а в команды кода помещаются номера из этих таблиц.
А это значит, что параллельных массивов (для RASL’а и нативного кода) строить не надо. Достаточно использовать те же номера. `IdentReference` и `ExternalReference` отпадают за ненадобностью.
Как можно упростить `GlobalRef`? Что делает сейчас `GlobalRef<>` — его инициализатор определяет смещение в памяти для значения + подсчитывает суммарный объём памяти. Но если память глобальных переменных выделять не для модуля, а для единицы трансляции, то и смещения, и суммарный объём можно определить на стадии компиляции. Достаточно все переменные сложить в одну структуру.
Получаем такую картину:
```
enum { /* номера те же, что и в RASL */
ident_True = 1,
ident_False = 2,
…
func_Addm_Nat = 1,
func_Prout = 2,
…
};
#line 11, "Library.ref"
enum { MAX_FILE_HANDLE = 40 };
struct Globals {
…
FILE *file_handles[MAX_FILE_HANDE];
…
};
#define VAR_TYPE struct Globals
#line 1234, "Library.c"
static FnResult func_Addm_Nat(…) {…}
static FnResult func_Prout(…) {…}
static FnResult func_Putout(…) {
struct Globals *globals = vm->globals;
…
… globals->file_handles[k] …
…
}
static const NativeReference natives[] = {
{ func_Addm_Nat, "#Add-Nat" },
{ func_Prout, "*Prout" },
…
{ 0, 0 },
};
#if defined(VAR_TYPE)
const Initializer init_AAAAA_BBBBB = { AAAAA, BBBB, natives, sizeof(VAR_TYPE) };
#else
const Initializer init_AAAAA_BBBBB = { AAAAA, BBBB, natives, 0 };
#endif
```
Поскольку в инициализаторе хранятся куки, их не нужно хранить в `NativeReference`. Если пользователь описал структуру, то он должен определить макрос `VAR_TYPE`, раскрывающийся в имя этой структуры. Если макрос определён, то размер соответствующего типа заносится в `Initializer`.
Заметим, что теперь все структуры данных инициализируются на стадии компиляции и для рантайма они могут быть доступны только для чтения. Никакого `module_initialized` более не нужно.
# Вывод
Даже если не переходить на Си, то поразмышлять об этом полезно. В частности, можно прийти к существенному упрощению компиляции нативного кода. Можно даже обойтись без списка инициализаторов на стадии компоновки, а инициализировать `Initializer` конструктором с побочным эффектом. Всё равно будет польза.
Более того, если полностью реализовать этот подход, упростится реализация модулей типа N (#170), ведь нативный код по сути — ни что иное, как ссылка на массив `initializers`. Возникают нюансы с передачей функций рантайма — их можно передавать как указатели на функции в виртуальной машине.
* [x] Упростить генерацию нативного кода (#199).
* [ ] Компоновка единиц трансляции на Си (#324).
* [ ] Проектирование (#312) и реализация (#311) C API рантайма.
* [ ] Переписывание нативных функций библиотек на подмножество Си языка C++.
* [ ] Поддержка пар `.rasl` + `.c` как единиц трансляции, переписывание генератора кода на C++.
* [ ] Переписывание рантайма на Си (#311).
* [ ] Продолжить размышлять над компиляцией в Си.
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.