Вычисление кода Julia
Одним из самых сложных моментов в изучении выполнения кода на языке Julia является понимание совместной работы всех частей для выполнения блока кода.
Каждый фрагмент кода обычно проходит через множество этапов с потенциально незнакомыми названиями, такими как (в произвольном порядке): использование интерпретатора flisp, AST, C++, LLVM, eval
, typeinf
, macroexpand
, sysimg (или образ системы), начальная загрузка, компиляция, анализ, выполнение, JIT, интерпретация, упаковка, распаковка, использование встроенной функции и примитивной функции, прежде чем стать желаемым результатом (как хотелось бы надеяться).
Определения
|
Выполнение кода Julia
Далее приводится общее описание процесса.
-
Пользователь запускает
julia
. -
Вызывается функция C
main()
изcli/loader_exe.c
. Эта функция обрабатывает аргументы командной строки , заполняя структуруjl_options
и задавая переменнуюARGS
. Затем она инициализирует Julia (вызывая функциюjulia_init
вinit.c
, которая может загрузить ранее скомпилированный образ системы, sysimg). Наконец, она передает управление Julia, вызываяBase._start()
. -
Когда
_start()
принимает управление, последующая последовательность команд зависит от заданных аргументов командной строки. Например, если было указано имя файла, будет выполнен этот файл. В противном случае будет запущен интерактивный цикл REPL. -
Опуская подробности о том, как REPL взаимодействует с пользователем, скажем лишь, что программа завершается блоком кода, который она хочет выполнить.
-
Если блок кода для выполнения находится в файле, вызывается
jl_load(char *filename)
для загрузки файла и его анализа. Затем каждый фрагмент кода передаетсяeval
для выполнения. -
Каждый фрагмент кода (или AST) передается в
eval()
для преобразования в результат. -
eval()
takes each code fragment and tries to run it injl_toplevel_eval_flex()
. -
jl_toplevel_eval_flex()
определяет, является ли код действием верхнего уровня (например,using
илиmodule
), которое будет недопустимо внутри функции. Если это так, код передается интерпретатору верхнего уровня. -
Затем
jl_toplevel_eval_flex()
расширяет код, чтобы исключить любые макросы и понизить AST, чтобы упростить его выполнение. -
После этого
jl_toplevel_eval_flex()
использует некоторые простые эвристические процедуры, чтобы решить, выполнять JIT-компиляцию для AST или интерпретировать его напрямую. -
Основную часть работы по интерпретации кода выполняет
eval
вinterpreter.c
. -
Если же код компилируется, основную часть работы выполняет
codegen.cpp
. Всякий раз, когда функция Julia вызывается в первый раз с заданным набором типов аргументов, для нее будет выполняться вывод типов. Эта информация используется на этапе генерации кода (codegen) для создания более быстрого кода. -
В конечном счете пользователь выходит из REPL или достигается конец программы и возвращается метод
_start()
. -
Перед самым выходом
main()
вызываетjl_atexit_hook(exit_code)
. При этом вызывается функцияBase._atexit()
(которая вызывает любые функции, зарегистрированные вatexit()
внутри Julia). Затем вызывается функцияjl_gc_run_all_finalizers()
. В итоге она корректно очищает все обработчикиlibuv
и ждет, пока они не будут сброшены и закрыты.
Анализ
Анализатор Julia — это небольшая программа lisp, написанная на языке femtolisp, исходный код которой находится внутри Julia в папке src/flisp.
Ее интерфейсные функции определены в основном в jlfrontend.scm
. Код в ast.c
обрабатывает эту передачу на стороне Julia.
Другими важными файлами на этом этапе являются julia-parser.scm
, который обрабатывает разметку кода Julia и преобразует его в AST, и julia-syntax.scm
, который обрабатывает преобразование сложных представлений AST в простые пониженные представления AST, более подходящие для анализа и выполнения.
Если вы хотите протестировать анализатор без полной пересборки Julia, можно самостоятельно запустить интерфейсную часть следующим образом.
$ cd src $ flisp/flisp > (load "jlfrontend.scm") > (jl-parse-file "<filename>")
Расширение макроса
Когда функция eval()
обнаруживает макрос, она расширяет этот узел AST, прежде чем попытаться вычислить выражение. Расширение макроса предполагает передачу из eval()
(в Julia) в функцию анализатора jl_macroexpand()
(записанную в flisp
) к самому макросу Julia (записанному где-то в Julia) с помощью функции fl_invoke_julia_macro()
и обратно.
Обычно расширение активируется в качестве первого шага во время вызова Meta.lower()
/jl_expand()
, хотя оно также может быть инициировано напрямую вызовом macroexpand()
/jl_macroexpand()
.
Вывод типов
В Julia вывод типов реализуется с помощью функции typeinf()
в файле compiler/typeinfer.jl
. Вывод типов — это процесс исследования функции Julia и определения границ типов каждой ее переменной, а также границ типа возвращаемого значения функции. Это позволяет внедрять многие будущие меры оптимизации, такие как распаковка известных неизменяемых значений, и поднимать во время компиляции различные вычислительные операции, такие как вычисление смещений полей и указателей функций. Вывод типа может также содержать другие шаги, такие как распространение констант и встраивание.
Еще больше определений
|
JIT-генерация кода
Генерация кода — это процесс преобразования AST Julia в собственный машинный код.
JIT-среда инициализируется заблаговременным вызовом jl_init_codegen
в codegen.cpp
.
По запросу метод Julia преобразуется в собственную функцию с помощью функции emit_function(jl_method_instance_t*)
. (Обратите внимание, что при использовании MCJIT (в LLVM v3.4 и более поздних версиях) каждая функция должна быть JIT в новый модуль.) Эта функция рекурсивно вызывает функцию emit_expr()
до тех пор, пока не будет выдана вся функция.
Оставшаяся большая часть этого документа посвящена различным выполняемым вручную оптимизациям конкретных шаблонов кода. Например, функция emit_known_call()
знает, как встраивать многие примитивные функции (определенные в builtins.c
) для различных сочетаний типов аргументов.
Другие части процесса генерации кода обрабатываются различными вспомогательными файлами.
-
Обрабатывает обратные трассировки для JIT-функций
-
Обрабатывает FFI ccall и llvmcall, а также различные файлы
abi_*.cpp
-
Обрабатывает выдачу различных низкоуровневых внутренних функций
Bootstrapping
Процесс создания образа системы называется начальной загрузкой (bootstrapping). |
Это слово происходит от английской фразы pulling oneself up by the bootstraps (добиться всего своими силами) и означает идею начать с очень ограниченного набора доступных функций и определений и закончить созданием полнофункциональной среды.
Образ системы
Образ системы представляет собой предварительно скомпилированный архив набора файлов Julia. Файл sys.ji
, распространяемый с Julia, является одним из таких образов системы, созданным путем выполнения файла sysimg.jl
и сериализации полученной среды (включая типы, функции, модули и все другие определенные значения) в файл. Поэтому он содержит статичную версию модулей Main
, Core
и Base
(и всего остального, что было в среде в конце начальной загрузки). Этот сериализатор или десериализатор реализован с помощью функции jl_save_system_image
или jl_restore_system_image
в файле staticdata.c
.
Если файл sysimg отсутствует (jl_options.image_file == NULL
), это также означает, что в командной строке был указан параметр --build
, поэтому конечным результатом должен быть новый файл sysimg. Во время инициализации Julia создаются минимальные модули Core
и Main
. Затем из текущего каталога вычисляется файл с именем boot.jl
. После этого Julia вычисляет любой файл, заданный в качестве аргумента командной строки, пока не дойдет до конца. Наконец, она сохраняет результирующую среду в файл sysimg для использования в качестве отправной точки для будущего выполнения.