Лингвофорум

Лингвоблоги => Личные блоги => Блоги => Алексей Гринь => Тема начата: Алексей Гринь от января 5, 2015, 21:19

Название: ГриньСкрипт и нативный код
Отправлено: Алексей Гринь от января 5, 2015, 21:19
Замутил сегодня возможность превращать Гринь-замыкания в обычные С-callback'и.
Обычно ГриньСкрипт старается избегать динамической кодогенерации (кроме той, что в самом начале, на старте), однако здесь этого избежать нельзя (см. ниже).

Сначала опишу, что же есть замыкания.
Гринь-замыкания делятся на две категории: интерфейс и реализации.

Что есть замыкания

Интерфейс замыкания, т.н. «метод-класс» (то, что в C# называется Delegate) — это самый обычный абстрактный класс, у которого автоматически генерируется один виртуальный метод — "invoke", у которого сигнатура зависит от сигнатуры самого метод-класса.

Реализация замыкания — это автоматически сгенерированный ad hoc дериват от абстрактного класса, который реализует метод invoke (тело замыкания).

Самое главная фишка замыканий — возможность захвата переменных в собственный контекст, да так, что, например, локальные переменные функции могут «жить» даже после того, как сама функция завершилась. В ГриньСкрипте это реализуется с помощью дополнительных автоматически генерируемых классов под внутренним названием ClosureEnv. Любые переменные функции, к которым обращаются замыкания, трансформируются в поля объекта ClosureEnv. В каждой функции — создаётся свой отдельный ClosureEnv (если нужно). Каждое замыкание имеет внутреннее поле, которое ссылается на ClosureEnv. Я разделил данные замыканий от самих замыканий для того, чтобы несколько замыканий внутри одной функции могли ссылаться на один и тот же ClosureEnv.

Таким образом, после того как замыкание создано, возвращённое значение — это по сути самый обычный указатель на самый обычный Гринь-объект. Тело замыкания — это переопределённый метод invoke, контекст замыкания — это внутреннее поле ClosureEnv внутри объекта. То есть всё, что нам нужно, чтобы запустить замыкание — это найти его указатель и вызвать метод invoke.

Thunking

Если же брать нативный код, то в C/x86 callback'и реализованы иначе. C-коллбэк это просто указатель на код. Отсутствует возможность передавать контекст данных внутрь C-callback'а, если сам C-callback не позволяет этого. Что же делать, как же трансформировать Гринь-замыкание в C-callback? Ведь Гринь-замыкание — это указатель на данные, C-callback — это указатель на код.

Делается это, как можно догадаться, с помощью thunks (как по-русски это вообще?) — динамически генерируемых небольших кусочков вспомогательного кода. Допустим, если взять сигнатуру int(*)(int, int), то нативный код ожидает указатель на код, который pop'ит 2 аргумента с нативного стека и возвращает результат через eax. Метод invoke у Гринь-замыканий же имеет внутренне другую сигнатуру: int(*)(void*, int, int), где первый указатель есть указатель this.

Для каждого замыкания генерируется свой thunk: он попросту push'ит все аргументы на стек ещё раз (те аргументы, что заpush'ил нативный код, ожидая обычный C-callback), а затем — самое главное — push'ит указатель this на наше замыкание и дальше производит вызов самого тела Гринь-замыкания. Вот как это примерно выглядит для указанной выше сигнатуры (+ cdecl):

push ebp
mov ebp, esp
mov eax, [ebp+0000000C] <-- аргумент номер 2
push eax
mov eax, [ebp+00000008] <-- аргумент номер 1
push eax
mov eax, 0134A670 <-- добавленный указатель this (наше замыкание)
push eax
mov eax, 0138CE1B <-- указатель на тело замыкания
call eax
add esp, 0000000C
leave
ret

Код, конечно, глуповат, его можно оптимизировать и оптимизировать, однако для простоты и моей уверенности в том, что багов нету, я решил сделать вот так, самым простейшим способом.

Сгенерированный thunk хранится в новом внутреннем поле замыкания под названием m_codeOffset. Сборщик мусора был немного изменён, теперь он распознаёт замыкания и проверяет m_codeOffset, чтобы удалить thunk, когда удаляется само замыкание.
Это вводит известную для C# проблему ранней деаллокации thunk'ов, когда нативный код, возможно, ещё продолжает пользоваться thunk'ом, что приводит, ясным образом, к segfault'ам. Для того, чтобы это не произошло, нужно удостовериться, что ссылка на делегат трасируется от GC-корней. Или можно просто вызвать метод GC::addRoot и не париться.

Чтобы создать thunk из-под Гринь-скрипта, создан новый метод Marshal::codeOffset. Его название ортогонально методу Marshal::dataOffset, который извлекает указатель на непосредственные данные объекта (минуя vtable или count у массивов).
Название: ГриньСкрипт и нативный код
Отправлено: Wolliger Mensch от января 5, 2015, 21:23
Гринь, у вас опять замыкание? ;D
Название: ГриньСкрипт и нативный код
Отправлено: Алексей Гринь от января 5, 2015, 21:25
Цитата: Wolliger Mensch от января  5, 2015, 21:23
Гринь, у вас опять замыкание? ;D
(wiki/ru) Замыкание_(программирование) (http://ru.wikipedia.org/wiki/%D0%97%D0%B0%D0%BC%D1%8B%D0%BA%D0%B0%D0%BD%D0%B8%D0%B5_%28%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5%29)

:)
Название: ГриньСкрипт и нативный код
Отправлено: Wolliger Mensch от января 5, 2015, 21:30
Цитата: Алексей Гринь от января  5, 2015, 21:25
(wiki/ru) Замыкание_(программирование) (http://ru.wikipedia.org/wiki/%D0%97%D0%B0%D0%BC%D1%8B%D0%BA%D0%B0%D0%BD%D0%B8%D0%B5_%28%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5%29)

ЦитироватьЭта статья или раздел нуждается в переработке.

:yes:
Название: ГриньСкрипт и нативный код
Отправлено: Алексей Гринь от января 5, 2015, 21:35
https://en.wikipedia.org/wiki/Closure_(computer_programming)

Я, в принципе, не люблю русскоязычную терминологию, потому что это клоунада, как вы можете сами убедиться. Гринь-closure уже звучит не так глупо.

По теме — я могу рассказать ещё множество изумительных историй по поводу того, как ГриньСкрипт общается с нативным кодом.
Название: ГриньСкрипт и нативный код
Отправлено: Wolliger Mensch от января 5, 2015, 22:00
Цитата: Алексей Гринь от января  5, 2015, 21:35
Я, в принципе, не люблю русскоязычную терминологию, потому что это клоунада, как вы можете сами убедиться. Гринь-closure уже звучит не так глупо.

Тоже не очень. На клозет похоже. :3tfu:
Название: ГриньСкрипт и нативный код
Отправлено: Алексей Гринь от января 9, 2015, 12:43
Добавил поддержку STDCALL (чего требует Windows API) при конвертации Гринь-замыканий в C-callback'и. Пришлось изменить пару структур опять. Дело в том, что calling convention у меня определяется атрибутами к методу (как в P/Invoke). Когда определяется класс делегата, то все атрибуты класса автоматически переходят на атрибут автоматически сгенерированного метода invoke:

Цитировать[callConv=stdcall]
method class (ThreadProc arg: intptr): int;

есть на самом деле синтаксический сахар к:
Цитировать[callConv=stdcall]
class ThreadProc {
[callConv=stdcall]
method (invoke  arg: intptr): int;
}

Замыкания есть ad-hoc дериваты базового класса, с переопределённым invoke. О чём я при дизайне системы не подумал, так это о том, что атрибуты переопределённых методов должны наследовать атрибуты базовых методов. Иначе у замыкания переопределённый invoke не будет иметь атрибут "stdcall", что чревато при кодогенерации.

Что касается thunk'ов для STDCALL, всё отличие от стандартного CDECL заключается в отсутствии "add esp, X".

И вот что я подумал ещё. Например, если взять CreateThread из WinAPI.  Мой скриптовый язык имеет «доменную» архитектуру, то есть вся экосистема делится на изолированные потоки, которые приравниваются к изолированным экземплярам виртуальной машины (для более безопасной реализации многопоточности). Однако же, если нативный код типа CreateThread берёт Гринь-замыкание и создаёт новый поток, то Гринь-код внутри нового потока ВНЕЗАПНО окажется вне своего родного домена, где он был создан. Это чревато вылетами, так как Гринь-код предполагает наличие VM-структур, привязанных к текущему потоку (а их в этом случае нет).

Вопрос: гарантировать ли инъекцию нового Гринь-домена в девственный поток в таких случаях? Мы можем сделать проверку и инъекцию прямо внутри thunk'ов. Правда, я пока не знаю, как потом удалить такой домен. Не знаю, как поймать событие «поток N завершён»

Это было бы интересно реализовать, но это не приоритет.
Название: ГриньСкрипт и нативный код
Отправлено: Алексей Гринь от января 9, 2015, 13:09
О, я думаю, инъецировать Гринь-домен в девственное лоно Windows-потоков я не буду. Вместо этого я буду проверять внутри thunk'ов — внутри родного ли домена мы находимся и каким-то образом делать "fail fast" если это не так, т.е. фэйлить по-тихому, пока чего плохого не произошло. Но непонятно как это делать. Нативный поток может быть критичным. То есть завершать его не следовало бы. Однако, в любом случае, самый первый аборт на стороне Гринь-скрипта уронит поток, так как в дественном С-потоке будут отсутствовать хэндлеры для sjlj-исключений, на которых основаны аборты. Отсутствие хендлера приведёт к падению всего процесса, в том числе к падению всех якобы изолированных друг от друга доменов, что совершенно не желательно. Поэтому было бы умно убить текущий поток заранее — тогда мы хотя бы сохраняем жизнь другим независимым доменам в текущем процессе, пусть и засчёт какого-то левого убитого C-потока.

С другой стороны, возможность неза'sandbox'енного домена вызывать нативные методы сама по себе небезопасная фича, что, казалось бы, говорит нам о том, что заморачиваться не стоит. Однако у нативных функции интерфейсы явно указаны в хедерах, что предсказуемо и в какой-то мере безопасно. Если же брать callback'и, то ты никогда не знаешь, где этот callback может очутиться, потому что реализация скрыта. Поэтому, я думаю, всё-таки, проверка на наличие привязанного к текущему потоку Гринь-домена оправданна, несмотря на что, что работа с нативным кодом сама по себе небезопасная.
Название: ГриньСкрипт и нативный код
Отправлено: Алексей Гринь от января 9, 2015, 13:28
О нет, новая идея. Убивать рандомный поток и игнорировать это нельзя! Вместо этого, если определено, что замыкание вызвано вне своего домена, мы будем показывать MessageBox с текстом типа  (чисто для debug'инга.) «Замыкание вызвано native-кодом вне своего родного домена. Работа процесса с данного момента может быть нестабильной. Продолжить?»

Вот так вот ваще гениально. Я молодец.
Название: ГриньСкрипт и нативный код
Отправлено: Алексей Гринь от января 10, 2015, 16:57
Подсчитал количество абстракций и реализации при обёртывании событий моего С++-фреймворка Гринь-скриптом:

1) C++ код вызывает Event::Fire(..) который проходит под внутреннему массиву EventHandler's и вызывает их Invoke(..), где аргументы зависят от C++-шаблона;

2) EventHandler в этом случае реализован как частный случай EventHandlerWrapper — обёртка вокруг C-callback'а EVENTHANDLER для межмодульного взаимодействия;

3) C-callback на самом деле есть сгенерированный C-to-ГриньСкрипт transition thunk, который inject'ит "this" и передаёт управление в скомпилированный машинный код уже на стороне ГриньСкрипта;

4) Функция, где мы теперь оказываемся, есть т.н. Virtual Call Helper, смысл которой есть проверять "this" на нуль в мягком режиме, разыменовывать vtable по заhardcod'енному индексу и переходить по полученному адресу;

5) Мы наконец-то находимся в обработчике события на стороне ГриньСкрипт.
Название: ГриньСкрипт и нативный код
Отправлено: Rachtyrgin от января 10, 2015, 17:41
Цитата: Алексей Гринь от января  5, 2015, 21:19
Самое главная фишка замыканий — возможность захвата переменных в собственный контекст, да так, что, например, локальные переменные функции могут «жить» даже после того, как сама функция завершилась.

Это что - демонстративное "фе" принципам ООП? Ваши замыкания и до private переменных добираться будут?
Название: ГриньСкрипт и нативный код
Отправлено: Алексей Гринь от января 10, 2015, 18:04
Цитата: Rachtyrgin от января 10, 2015, 17:41
Это что - демонстративное "фе" принципам ООП? Ваши замыкания и до private переменных добираться будут?
Почему демонстративное «фе»? Инкапсуляция никак не нарушается. Если замыкание создано внутри метода, у которого есть доступ к переменной (приватной), то почему нет? Метод объекта (который имеет доступ к приватным данным) неявно разрешил замыканию доступ к переменным в текущем контексте самим фактом создания замыкания. Более того, к захваченным переменным, на которые ссылается замыкание, нет доступа извне замыкания. Замыкание это тот же самый обычный код, что и код метода, и правила доступа те же; разница только в том, что на замыкание как на код можно взять ссылку. Посторонний код, который пользуется замыканием, ничего не знает о том, какие переменные оно захватило. Он просто вызывает его как функцию. Это и есть инкапсуляция.

В ГриньСкрипте с инкапсуляцией всё в порядке, она у нас, наоборот, более форсированная, чем в некоторых языках: нет не-приватных полей вообще. Только владелец поля может к нему обращаться. У нас даже не предусмотрено синтаксиса для обращения к полям чужих объектов. Чтобы обратиться к полю в чужом объекте, нужно или вручную написать публичные getter/setter-методы, или использовать генеративный синтаксический сахар "property", который автоматически создаёт их.
Название: ГриньСкрипт и нативный код
Отправлено: Rachtyrgin от января 10, 2015, 18:16
А, замыкание внутри метода? Тогда ладно. Из начала вашего топика не уловил. А у вас язык предполагается чисто скриптовый, или все-таки будет промежуточная компиляция в бинарный код в духе Java?
Название: ГриньСкрипт и нативный код
Отправлено: Алексей Гринь от января 10, 2015, 18:48
Цитата: Rachtyrgin от января 10, 2015, 18:16
А у вас язык предполагается чисто скриптовый, или все-таки будет промежуточная компиляция в бинарный код в духе Java?
Скриптовый в том плане, что вручную не нужно что-то компилировать, линковать и т.д. — просто запустил, и всё. Однако в конечном счёте перед запуском он компилируется в бинарный код и имеет скорость неоптимизированного Си.

Компилирую я это весьма экзотическим способом — код генерируется в памяти в промежуточный код на С, который затем компилируется высокоскоростным TCC, опять же, в памяти (TCC не генерирует AST и почти ничего не проверяет, отсутствует basic blocks и нет аллокации регистров — он генерирует код на лету по мере парсинга; так, кстати, делали ранние версии Google Chrome). Можно сказать, это у нас это и есть такой промежуточный высокоуровневый байткод.

Благодаря тому, что бэкенд у меня — Си, язык поддерживает inline C, а также inline asm внутри inline C.

Я пока сильно бенчмарками не занимался, и сужу по дебаг-билду, но, как ни странно, парсинг и анализ Гринь-скрипта занимает больше времени, чем компиляция Си. Так что TCC хорошо оптимизирован и его использование вполне оправданно, несмотря на кажущуюся громоздкость идеи.

Так как язык строго типизированный, перед запуском приходится анализировать полностью весь исходный код, разрешать все ссылки и т.д., что довольно трудозатратно, по сравнению с динамическими языками, и меня волнует то, что по мере роста скрипта, растёт задержка при запуске (сейчас в пределе 100 мс в моих тестах, но это дебаг-билд). Поэтому в будущем думаю как-то кэшировать скомпилированный результат на диске.
Название: ГриньСкрипт и нативный код
Отправлено: Rachtyrgin от января 10, 2015, 18:54
Скорость Си? Впечатлен. А как насчет кроссплатформенности? Или только Windows?
Название: ГриньСкрипт и нативный код
Отправлено: Алексей Гринь от января 10, 2015, 19:03
Цитата: Rachtyrgin от января 10, 2015, 18:54
Скорость Си? Впечатлен. А как насчет кроссплатформенности? Или только Windows?
Не тестил, но там Windows-моментов не так много. Портировать на Линукс можно за пару вечеров.

Бо́льшая проблема в том, что только x86 в данный момент. Я бы попробовал x64, да нет возможности, времени и сил.

В любом случае, есть Wine и WOW64.
Название: ГриньСкрипт и нативный код
Отправлено: Алексей Гринь от января 10, 2015, 19:10
Я вот тут описал базовую архитектуру: Доменная инфраструктура в ГриньСкрипте (http://lingvoforum.net/index.php/topic,73261.0.html)
Там полотна текста, но, может быть, кому-то задумка будет интересна.
Название: ГриньСкрипт и нативный код
Отправлено: Rachtyrgin от января 10, 2015, 19:14
Цитата: Алексей Гринь от января 10, 2015, 19:03

Бо́льшая проблема в том, что только x86 в данный момент. Я бы попробовал x64, да нет возможности, времени и сил.

В любом случае, есть Wine и WOW64.

Понимаю. Я одну свою программулину (Java + FireBird) пытался вести одновременно на x86 и x64, потом плюнул и ушел на x64 полностью. А "полотна " вашего текста я почитаю. Любопытно.
Название: ГриньСкрипт и нативный код
Отправлено: Bhudh от января 11, 2015, 00:44
Цитата: Алексей Гринь от января 10, 2015, 18:48Благодаря тому, что бэкенд у меня — Си, язык поддерживает inline C, а также inline asm внутри inline C.

Когда-то обсуждали старую статью: «Наш ответ Чемберлену» (http://lingvoforum.net/index.php/topic,28846.0.html)
Вот из неё цитата:
Цитата: Следующим хитом станет, сдается мне, язык, который будет написан "для того, чтобы избавить автора и его друзей от программирования на...". (На чем-то уровня С/С++, Pascal, Delphi). При этом, как хорошие среды разработки содержат возможность ассемблерных вставок, так и новая среда будет содержать возможность вставок на заменяемом языке, да еще и про ASM не забудет.

Понятия нового языка должны представлять собой уровень уже более высокий, чем даже Delphi. Потому что, какой смысл изобретать еще один Delphi, еще один SQL, еще один С-клон?
Название: ГриньСкрипт и нативный код
Отправлено: Тайльнемер от января 11, 2015, 04:41
Цитата: Алексей Гринь от января 10, 2015, 18:48
Компилирую я это весьма экзотическим способом — код генерируется в памяти в промежуточный код на С, который затем компилируется высокоскоростным TCC, опять же, в памяти.
...и меня волнует то, что по мере роста скрипта, растёт задержка при запуске (сейчас в пределе 100 мс в моих тестах, но это дебаг-билд). Поэтому в будущем думаю как-то кэшировать скомпилированный результат на диске.
А если поддерживается только одна архитектура, почему бы сразу не компилировать в файл?
Название: ГриньСкрипт и нативный код
Отправлено: Алексей Гринь от января 11, 2015, 06:13
Цитата: Тайльнемер от января 11, 2015, 04:41
А если поддерживается только одна архитектура, почему бы сразу не компилировать в файл?
Ну это типа скрипт же. Если и компилировать в файл, то только в качестве кэша, чтобы экономить время запуска при следующих запусках, если файл не менялся.
Мне так проще, пока что. Много вспомогательных функций, которые линкятся динамически, код же постоянно бегает:

бинарный ГриньСкрипт-код >> runtime helpers >> внутренние кишки (классы Class, Method, Domain в C++) >> и обратно

и оно как-то попроще если линковать всё это в динамике, в памяти. TCC немного баговатая штука, при определённых сочетаниях вылетает и работает не как надо, я пока что это не трогаю, потому что don't fix it, if ain't broke.

Другая вещь заключается в том, что многопоточность у меня заключается в том, чтобы создавать отдельный изолированный экземпляр виртуальной машины (т.н. домен, по аналогии с C#'овским AppDomain), и при запуске этот новый экземпляр компилирует собственный код, собственную версию всего. И фишка в том, что можно создавать домены из строк, как eval в JavaScript'ах (однако у меня есть встроенная система безопасности, так что если новый домен помечен как sandboxed, первая же попытка попытаться в нём что-то сделать опасное убьёт домен с ошибкой "Code access denied"). Так вот, тут неразбериха полная будет, если каждый запускаемый домен (т.е. поток) будет генерировать собственный исполняемый файл на диске.

Задержка при запуске растёт не из-за компиляции как таковой, а из-за того, что строго типизированному языку нужно проанализировать всю иерархию классов, прежде чем что-то делать: чтобы код в одном файле мог правильно ресолвить ссылку в другом файле, чтобы layout vtabl'ов сделать, чтобы провести Hierarchy Analysis и схлопнуть одинокие виртуальные методы в статические вызовы, чтобы всё двадцать раз проверить (например, ECall-вызовы, то есть вызовы к внешним нативным библиотекам, разрешены только в unsafe-контекстах в fulltrust-доменах) и т.д. Поэтому как вариант я думал о том, чтобы кэшировать такую runtime-информацию, а не сам скомпилированный бинарный код.
Название: ГриньСкрипт и нативный код
Отправлено: Алексей Гринь от января 11, 2015, 10:39
Только что успешно проверил inline ASM внутри inline C. Можно из-под чистого ГриньСкрипта, не используя сторонних инструментов, обратиться к процессору напрямую и получить название модели. Вот что мне возвратил процессор:

Intel(R) Core(TM)2 Quad  CPU   Q8200  @ 2.33GHz
Название: ГриньСкрипт и нативный код
Отправлено: Bhudh от января 11, 2015, 10:45
Хм. А в какой кодировке там записываются строки? ANSI?
Название: ГриньСкрипт и нативный код
Отправлено: Алексей Гринь от января 11, 2015, 11:14
Цитата: Bhudh от января 11, 2015, 10:45
Хм. А в какой кодировке там записываются строки? ANSI?
Алгоритм такой: записываешь в eax по очереди значения 80000002H, 80000003H и 80000004H и каждый раз вызываешь инструкцию cpuid. Каждый раз он возвращает по четыре байта в eax, ebx, ecx и edx.

Эти байты просто аккумулируешь в буфере, затем добавляешь терминирующий нуль, вызываешь ГриньСкриптовское Marshal::utf8ToString и получаешь строку. Да, это обычный ANSI, он совпадает с UTF8 для английский букв, поэтому я использовал utf8ToString.

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

Поэтому, я боюсь, вы разочаруетесь тоже. Но мне было бы приятно, если бы кто-то это дело просто потестил. Я вообще не знаю, запустится ли у кого-то это, кроме меня. Мне это важно знать.
Название: ГриньСкрипт и нативный код
Отправлено: Wolliger Mensch от января 11, 2015, 19:43
Цитата: Алексей Гринь от января  5, 2015, 21:19
ГриньСкрипт и нативный код

Nātīvus > natị̄vo > nadīvo > naðīvə > naðív > naðíf > naïf. Code naïf. Как-то так. ;D
Название: ГриньСкрипт и нативный код
Отправлено: Тайльнемер от января 12, 2015, 14:56
Я так и читаю: «ГриньСкрипт и наивный код».
Не знал, что когнаты.
Название: ГриньСкрипт и нативный код
Отправлено: Алексей Гринь от января 12, 2015, 19:48
Насчёт долгой стартовой компиляции есть дикая задумка.

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

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

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

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

Такая вот мудрёная задумка у меня. Что думаете, если кто что понял? Возможно, это слишком мудрено, а эффективности кот наплакал.
Название: ГриньСкрипт и нативный код
Отправлено: Алексей Гринь от февраля 1, 2015, 02:27
(http://i1.kym-cdn.com/entries/icons/original/000/002/830/sad_frog.jpg)

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

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

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

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

Отличие от других JIT'ов заключается в том, что компилируется сразу ведь код, а не инкрементально метод за методом. Так реализация получится проще; и если судить по Java, для больших приложений нет разницы — инкрементальная компиляция или нет, Java в любом случае жёстко тормозит на startup'е, т.е. их инкрементальная компиляция, грубо говоря, проваливается в тотальную компиляцию, т.к. куча кода вызывает кучу кода сразу на стартапе.
Название: ГриньСкрипт и нативный код
Отправлено: Алексей Гринь от февраля 4, 2015, 01:22
Продолжаю 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 строк кода (включая чуть-чуть комментов).
Название: ГриньСкрипт и нативный код
Отправлено: Алексей Гринь от февраля 5, 2015, 04:37
Продолжаю 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)