Главное меню
Мы солидарны с Украиной. Узнайте здесь, как можно поддержать Украину.

ГриньСкрипт и нативный код

Автор Алексей Гринь, января 5, 2015, 21:19

0 Пользователи и 1 гость просматривают эту тему.

Алексей Гринь

Цитата: Тайльнемер от января 11, 2015, 17:41
Ух ты!
Я как-то кое-кому анонимно показывал, все блеванули от моего синтаксиса, хотя он мне кажется довольно ортогональным (в ущерб визуальной элегантности). Думаю решить это с помощью шаблонов потом как-нибудь. Дело в том, что в языке отсутствуют понятия типа if/then или for/next, как в Smalltalk'е. Вместо этого используется вызов методов + замыкания (как в Smalltak'е, но при этом язык не динамический). Сам вызов методов имеет синтаксис как в Haskell'е, а синтаксис замыканий — смесь Паскаля с Obj-C. Поэтому код с control flow имеет чуть больше знаков препинания, чем людям хотелось бы. Но мне главное не синтаксис, а семантика, так что...

Поэтому, я боюсь, вы разочаруетесь тоже. Но мне было бы приятно, если бы кто-то это дело просто потестил. Я вообще не знаю, запустится ли у кого-то это, кроме меня. Мне это важно знать.
肏! Τίς πέπορδε;

Wolliger Mensch

Цитата: Алексей Гринь от января  5, 2015, 21:19
ГриньСкрипт и нативный код

Nātīvus > natị̄vo > nadīvo > naðīvə > naðív > naðíf > naïf. Code naïf. Как-то так. ;D
«Вот интересно, каких лингвистических жемчуг можно найти в море отодвинутых книг», Ян Гавлиш.
«Впредь прошу помнить, что придумал игру не для любых ассоциаций, а для семантически оправданных. Например, чтó это такое: ,,рулетке" — ,,выпечке"?? Тем более, что сей ляпсус я сам совершил...», Марбол
«Ветхий Завет написан на иврите и частично на армейском», Vesle Anne
«МЛ(ять)КО ... ПЛ(ять)NЪ», Тася
«Вот откроет этот спойлер, например, Марго, ничего не подозревая, а потом будут по всему форуму блюющие смайлики...», Авал
«Томан приличный мужчина. Правда по патриархальным меркам слегка голодранец», Vesle Anne
«Возможен ли фонетический переход "ж" в "п с придыханием"», forest

Тайльнемер

Я так и читаю: «ГриньСкрипт и наивный код».
Не знал, что когнаты.

Алексей Гринь

Насчёт долгой стартовой компиляции есть дикая задумка.

Начать интерпретацию сразу, а в параллельном потоке начать компиляцию (с помощью TCC, по старой схеме). Интерпретатор при каждом заходе в функции должен проверять: завершена ли компиляция? Если компиляция завершена, то генерируется trampoline, который прыгает в скомпилированный код. Когда control flow вернётся из компилированной функции, он опять окажется в коде интерпретатора, который вызвал эту функцию. Интерпретатор пойдёт дальше по коду и опять «нырнёт» куда-то в скомпилированный код, и снова «вынырнет». Подразумевается, что в конце концов когда-нибудь виртуальный стек интерпретатора «размотается» и окажется где-то в одной из самых верхних функций, и тогда очередное ныряние в нативный код переведёт скрипт в почти полностью скомпилированный режим.

(Я думаю, Java делает это умнее, она скорей всего полностью генерирует заново весь нативный стек на основе виртуального, как будто так и было. Но я так не могу, потому что то, как TCC генерирует стекфреймы, вне моей юрисдикции. Мне, получается, нужно знать точный layout нативных фреймов, которые создаёт TCC, что слишком муторно.)

При интерпретации было бы ещё желательно представлять объекты как словари ключ<=>значение, потому что вычисление правильного layout'а объектов требует предварительного анализа всей иерархии классов, что затратно. То есть, все объекты до завершения компиляции -- это просто словари значений, как в динамических языках. Когда компиляция в параллельном потоке наконец-то завершена, при первом переходе из интерпретатора в машинный код должна произойти тотальная конвертация всех объектов. Все объекты, которые ранее были динамически словарями, должны переконвертироваться в строго определённые layout'ы. Я не знаю, насколько долгой будет задержка при конвертации всех объектов; надеюсь, что в пределах времени на сборку мусора. После конвертации скомпилированный код может обращаться к объектом как это делается сейчас, как к обычным C-структурам. Как я уже написал выше, после завершения компиляции, control flow всё равно будет из разу в раз выпрыгивать обратно в интерпретатор. Поэтому интерпретатор должен иметь второй режим интерпретации, когда обращения к полям и методам происходит со знанием того, что объекты с конвертированы и имеют другое представление.

При этой схеме интерпретатор должен полностью блокироваться, если ему нужно вызвать функцию с inline C-кодом, потому что приходится ждать результатов компиляции. Вызов внешних нативных библиотек (сокращённо -- ECalls) тоже проблематичен. ECalls работают с C-структурами, а не со словарями, поэтому тут как-то нужно выкручиваться по типу создания временных буферов, куда будет временно конвертироваться объект из представления "словарь" в представление "С-структура" прямо перед вызовом. А также приходится ввести итеративный анализ иерархий, чтобы верно знать, как эти С-структуры правильно нужно от layout'ить.

Такая вот мудрёная задумка у меня. Что думаете, если кто что понял? Возможно, это слишком мудрено, а эффективности кот наплакал.
肏! Τίς πέπορδε;

Алексей Гринь



В общем, постепенно инфраструктура исполнения превращается в такой гибрид:

* Основной костяк — C-компилятор в RAM (из-за фичи под названием inline C).

* Чтобы снизить давление на С-компилятор, отдельные специализированные предсказуемые функции генерируются на основе простейших шаблонов машинного кода (кодовое имя — ThunkJIT). Это, например, геттеры—обёртки вокруг полей; в будущем — тела get/set у массивов и т.д. Затем всё это link'ится с остальным кодом, сгенерированным by С. С точки зрения C-компилятора такие функции мало чем отличаются от icalls (т.е. вызовов из скрипта внутрь кишок VM). Выигрываем по скорости и памяти, потому что отсекается промежуточное представление в C, код генерируется напрямую (если паттерн задетектен).

* Дальнейшим шагом будет алгоритм, который я описал выше — код запускается сразу в режиме интерпретации, пока на заднем фоне он компилируется в машинный код, после чего интерпретатор входит в специальный режим, в котором при вызове функции вместо её интерпретации он берёт указатель на сгенерированный машинный код и прыгает в него. Затем исполнение выпрыгивает из машинного кода обратно в интерпретатор, когда такая функция закончится, и будет происходить интерпретация до тех пор, пока не произойдёт очередного скачка в сгенерированный машинный код. Такие скачки будут происходить до тех пор, пока виртуальный стек интерпретатора не размотается до top-level функции, и тогда скрипт перейдёт в полностью машинный режим. Такая реализация позволит реализовать переход в машинный код наиболее простым, портируемым способом. Есть проблема с циклами (если ничего с ними не делать, они слишком поздно или вообще никогда не получат шанс быть скомпилируемыми), буду решать.

Отличие от других JIT'ов заключается в том, что компилируется сразу ведь код, а не инкрементально метод за методом. Так реализация получится проще; и если судить по Java, для больших приложений нет разницы — инкрементальная компиляция или нет, Java в любом случае жёстко тормозит на startup'е, т.е. их инкрементальная компиляция, грубо говоря, проваливается в тотальную компиляцию, т.к. куча кода вызывает кучу кода сразу на стартапе.
肏! Τίς πέπορδε;

Алексей Гринь

Продолжаю JIT-хобби, теперь работает вызов методов через рефлексию! Ура.

Как и в Java/C#, вызов через рефлексию выражается в том, что методу скармливается массив any (что равносильно Object в Java/C#), и этот массив должен транслироваться в аргументы на нативном стеке в таком порядке, в каком его ожидает C-функция. Алгоритм такой (реализация метода "Method::invoke"):

1) Количество аргументов, типы аргументов в массиве проверяются на соответствие сигнатуре метода + много другой верификации.
2) Создаётся временный бинарный буфер, и в него по очереди копируются аргументы из массива any. Boxed valuetypes автоматически распаковываются и кладутся в этот буфер уже распакованными (потому что в массиве всё приводится к any, и valuetypes автоматически оборачиваются boxed-обёртками в куче, и нужна распаковка).
3) Далее, происходит обращение к ThunkManager, который генерирует специализированный reflection thunk для конкретно текущего метода. Этот thunk принимает два аргумента: ссылку this (если есть) и ссылку на временный буфер с сериализованными аргументами (описанный выше). Получается нечто вроде этого:

push ebp
mov ebp, esp
mov eax, [ebp+0C] ; запоминаем аргументный буфер в регистр eax, чтобы проще было обращаться
push [eax] ; извлекаем первый аргумент из аргументного буфера и кладём на стек
push [ebp+8] ; кладём на стек первый аргумент thunk'а: ссылку на this
mov eax, 0077a7B6
call eax ; вызываем реальный метод
add esp, 8 ; подчищаем, ибо cdecl
leave
ret


Таким образом мы конвертируем heap-allocated массив обобщённых any в реальные значения на стеке.

4) После того, как мы вышли из thunk'а, мы имеем результат функции или в регистре eax, или в st0 (для вещественных чисел). Тут уже дальше идёт реализация на С, которая селективно кастит вызываемым thunk во что-то из этого:

typedef void*(*FReflectionThunkP)(void* thisObj, void* array);
typedef float(*FReflectionThunkF)(void* thisObj, void* array);
typedef int(*FReflectionThunkI)(void* thisObj, void* array);

Мы опираемся на кодогенератор С для корректного извлечения результата оборачиваемой функции из нужного регистра.

5) В общем, после того, как у нас есть некое значение, его нужно забоксить в any, потому что таков контракт метода Method::Invoke. Для reference type это не проблема, просто возвращай то, что есть. Для valuetypes мы просим текущий домен найти boxed-обёртку, и затем вызываем его конструктор.

Ограничения
a) Non-primitive valuetypes не поддерживаются пока. То же самое с binary blobs
б) Нужен "force boxed" в некоторых случаях. В будущем boxed-обёртки буду генерироваться on demand, что решит эту проблемку.
в) Не поддерживаю stdcall пока

На всё-про-всё ушло 270 строк кода (включая чуть-чуть комментов).
肏! Τίς πέπορδε;

Алексей Гринь

Продолжаю JIT-чудеса.
Теперь конструкторы замыканий и vtable's генерируется вне baseline C compiler, а вручную через ThunkManager.

Для каждого конструктора замыканий создаётся такой thunk:
push ebp
mov ebp, esp
push [ebp+8]
push CLOSURE_CTOR_INFO
mov eax, HELPER_FUNC
call eax
add esp, 8
leave ret


CLOSURE_CTOR_INFO - это специальная структура, кэширующая значения, создаётся своя для каждого конструктора замыканий.
HELPER_FUNC - это вспомогательная функция, которая принимает CLOSURE_CTOR_INFO.
[ebp+8] это указатель на ClosureEnv.

Вспомогательная функция проверяет внутри CLOSURE_CTOR_INFO, есть ли у данного замыкания vtable, и если нет — создаёт его (это отличает замыкания от других классов, у которых vtable предгенерированы на старте).

Далее вспомогательная функция просто обращается к менеджеру памяти, чтобы тот создал замыкание, указывая размер структуры и новосозданный (или закэшированный) vtable.
Также внутрь замыкания прописывается env (который по ebp+8 заpushен был).

Таким образом я постепенно облегчаю работу baseline C compiler, переводя всё больше функций на ThunkManager. На экстремальном тесте в 7 мегабайт скрипт-кода (biased towards куча замыканий) скорость компиляции кода под юрисдикцией baseline C compiler уменьшилась с ~15 секунд до 7. Употребление памяти снизилось с 450 МБ до 377 (с временным пиком в 413)
肏! Τίς πέπορδε;

Быстрый ответ

Обратите внимание: данное сообщение не будет отображаться, пока модератор не одобрит его.

Имя:
Имейл:
Проверка:
Оставьте это поле пустым:
Наберите символы, которые изображены на картинке
Прослушать / Запросить другое изображение

Наберите символы, которые изображены на картинке:

√36:
ALT+S — отправить
ALT+P — предварительный просмотр