Методы исполнения программ
Вступление
Все мы окружены различными устройствами, которые исполняют различные программы. Телефоны, планшеты, микроволновки, стиральные машины, телевизоры, автомобили. Все эти устройства оснащены такой вещью как центральный процессор (CPU)
Но прежде чем процессор выполнит код, он должен от куда-то появится. И для этого существуют различные языки программирования. Но процессор не понимает языки программирования, процессор понимает только машинные инструкции. Чтобы программа превратилась в машинные инструкции, которые понимает процессор, существуют различные методы преобразования исходного кода программ в машинный код. И об этом я сегодня расскажу.
Компиляция
Первый способ это компиляция программ. Компиляция — это преобразование исходного кода программы в другую сущность: машинный код, байт код, другой язык программирования.
Компиляция в машинный код (AOT)
Не все языки программирования поддерживают такой способ компиляции, но вот некоторые из ник: C, C++, Rust, Zig, Haskell, Crystal, Fortran
Компиляция происходит в несколько этапов:
- Обработка компилятором. Компилятор выполняет вычисления, которые можно выполнить в компайл тайм. В языке C/C++ это обработка препроцессором, проверка типов переменных, coercion, мономорфизация
- Создание абстрактного синтаксического дерева. AST — структура данных создаваемая на основе выражений языка.
- Непосредственно компиляция — преобразование AST в ассемблерный код.
- Ассемблирование — создание объектного файла содержащего машинные инструкции на основе ассемблерного листинга
- Линковка — создание конечного исполняемого файла, связывание символов с внешними зависимостями
Рассмотрим простой пример на языке C:
//C #include <stdio.h> int main(void) { puts("Camels"); return 0; }
А вот как этот же код выглядит после компиляции в машинный код
.LC0: .string "Camels" main: push rbp mov rbp, rsp mov edi, OFFSET FLAT:.LC0 call puts mov eax, 0 pop rbp ret
Это непосредственно листинг функции, листинг всей программы гораздо больше. Но этот код уже понимает процессор и может его исполнять.
Использование языков программирования компилируемых непосредственно в машинный код, означает, что необходимо перекомпилировать программу каждый раз, когда её нужно запустить но новой операционной системе или архитектуре процессора
Компиляция в байт код
Ещё один способ компиляции — это компиляция в байт код. Байт-код это промежуточное представление программы. Это не машинный код, но и не исходный код. Такой метод компиляции используют такие языки как Java (и все языки использующие JVM) C# (и все языки использующие .NET), Erlang/Elixir, Dart
Такой способ компиляции позволяет создавать платформо и архитектурно независимый код, который исполняется специальной программой под названием виртуальная машина. Это позволяет один раз написать программу и запускать везде где доступна её среда исполнения.
Плюсы такой компиляции — это кросплатформенность. Минусы — скорость исполнения программы.
Рассмотрим несколько примеров байт кода на разных языках программирования
//Java class Main { public static void main(String[] args) { System.out.println("Camels"); } }
Вот так выглядит откомпилированный байткод программы на Java
class Main { Main(); 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); 0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #13 // String Camels 5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return }
#python print("Camels")
Байт код языка Python
0 0 RESUME 0 1 2 PUSH_NULL 4 LOAD_NAME 0 (print) 6 LOAD_CONST 0 ('Camels') 8 CALL 1 16 POP_TOP 18 RETURN_CONST 1 (None)
%Erlang -module(square). -export([hello_camels/0]). hello_camels() -> io:fwrite("Camels\n").
{module, square}. %% version = 0 {exports, [{hello_camels,0},{module_info,0},{module_info,1}]}. {attributes, []}. {labels, 7}. {function, hello_camels, 0, 2}. {label,1}. {func_info,{atom,square},{atom,hello_camels},0}. {label,2}. {move,{literal,"Camels\n"},{x,0}}. {call_ext_only,1,{extfunc,io,fwrite,1}}. {function, module_info, 0, 4}. {label,3}. {func_info,{atom,square},{atom,module_info},0}. {label,4}. {move,{atom,square},{x,0}}. {call_ext_only,1,{extfunc,erlang,get_module_info,1}}. {function, module_info, 1, 6}. {label,5}. {func_info,{atom,square},{atom,module_info},1}. {label,6}. {move,{x,0},{x,1}}. {move,{atom,square},{x,0}}. {call_ext_only,2,{extfunc,erlang,get_module_info,2}}.
Байт код этих языков может быть выполнен как путем интерпретации, то есть каждую строку отдельно исполняет виртуальная машина. Либо может для оптимизации и увеличения скорости работы использоваться JIT компиляция, то есть создание машинного кода во время выполнения программы.
Транспайлинг: компиляция программ в другие языки программирования
Транспайлинг достаточно популярный способ компиляции программ. Такие языки как V, Nim, Vala, Haxe, Dart могут создавать программы на других языках, таких как C, C++, JavaScript. Язык Haxe это монстр в мире транспайлинга и позволяет компилировать программы на Haxe в JavaScript, C++, C#, Java, JVM, Python, Lua, PHP, Flash
Рассмотрим пример на языке Vala
//vala int square (int num) { return num * num; } static int main () { return square (3); }
Этот код преобразуется в следующий на языке C
//C VALA_EXTERN gint square (gint num); static gint _vala_main (void); gint square (gint num) { gint result; result = num * num; return result; } static gint _vala_main (void) { gint result; result = square (3); return result; } int main (int argc, char ** argv) { return _vala_main (); }
Затем этот код будет скомпилирован компилятором языка C в машинный код.
Такой способ позволяет создавать высокоуровневые языки программирования без необходимости написания всего бэкенд для компиляции. А так же автоматически использовать всю инфраструктуру языка C
Интерпретация
Ещё один способ исполнения программ это интерпретация. То есть пошаговое исполнения исходного кода программы интерпретатором. Этот способ используют такие языки как PHP, Perl, Lua, Ruby, Python, JavaScript и ещё много других языков.
Минусы такого способа — это очень медленное исполнение программ, по этому большинство интерпретируемых языков генерируют на лету байт код и потом его исполняют, так же для оптимизации используют JIT компиляцию.
Виды компиляции
Выше уже упоминались такие виды компиляции как JIT и AOT
JIT (Just In Time) — компиляция на лету, во время выполнения программы. Может создаваться как байт код так и машинный код. Большинство языков программирования использующих байт код используют JIT для ускорения исполнения кода
AOT (Ahead Of Time) — преждевременная компиляция. Или компиляция перед исполнением. Этот способ используют языки, которые непосредственно компилируются в машинный код. Так же такие языки как Java и C# используют AOT компиляцию в байт код, а во время выполнения дополнительно используют JIT