Генерация кода во время исполнения или «пишем свой jit-компилятор»

Интерпретаторы

Программируя на интерпретируемом языке, мы пишем программу не для выполнения в процессоре, а для выполнения программой-интерпретатором. Ее также называют виртуальной машиной.

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

При интерпретации выполнение кода происходит последовательно строка за строкой (от инструкции до инструкции). Операционная система взаимодействует с интерпретатором, а не исходным кодом.

Примеры интерпретируемых языков: PHP, JavaScript, C#, Python.

Скомпилированные программы работают быстрее, но при этом очень много времени тратится на компиляция исходного кода.

Программы же, рассчитанные на интерпретаторы, могут выполняться в любой системе, где таковой интерпретатор присутствует. Типичный пример — код JavaScript. Интерпретатором его выступает любой современный браузер. Вы можете однократно написать код на JavaScript, включив его в html-файл, и он будет одинаково выполняться в любой среде, где есть браузер

Не важно, будет ли это Safari в Mac OS, или же Internet Explorer в Windows

Генерация кода

Генерация машинного кода

Большинство компиляторов переводит программу с некоторого высокоуровневого языка программирования в машинный код, который может быть непосредственно выполнен физическим процессором. Как правило, этот код также ориентирован на исполнение в среде конкретной операционной системы, поскольку использует предоставляемые ею возможности (системные вызовы, библиотеки функций). Архитектура (набор программно-аппаратных средств), для которой компилируется (собирается) машинно-ориентированная программа, называется целевой машиной.

Результат компиляции — исполнимый программный модуль — обладает максимально возможной производительностью, однако привязан к конкретной операционной системе (семейству или подсемейству ОС) и процессору (семейству процессоров) и не будет работать на других.

Для каждой целевой машины (IBM, Apple, Sun, Эльбрус и т. д.) и каждой операционной системы или семейства операционных систем, работающих на целевой машине, требуется написание своего компилятора. Существуют также так называемые кросс-компиляторы, позволяющие на одной машине и в среде одной ОС генерировать код, предназначенный для выполнения на другой целевой машине и/или в среде другой ОС. Кроме того, компиляторы могут оптимизировать код под разные модели из одного семейства процессоров (путём поддержки специфичных для этих моделей особенностей или расширений наборов команд). Например, код, скомпилированный под процессоры семейства Pentium, может учитывать особенности распараллеливания инструкций и использовать их специфичные расширения — MMX, SSE и т. п.

Некоторые компиляторы переводят программу с языка высокого уровня не прямо в машинный код, а на язык ассемблера. (Пример: PureBasic, транслирующий бейсик-код в ассемблер FASM.) Это делается для упрощения части компилятора, отвечающей за генерацию кода, и повышения его переносимости (задача окончательной генерации кода и привязки его к требуемой целевой платформе перекладывается на ассемблер), либо для возможности контроля и исправления результата компиляции (в том числе ручной оптимизации) программистом.

Генерация байт-кода

Результатом работы компилятора может быть программа на специально созданном низкоуровневом языке двоично-кодовых команд, выполняемых виртуальной машиной. Такой язык называется псевдокодом или байт-кодом. Как правило, он не есть машинный код какого-либо компьютера и программы на нём могут исполняться на различных архитектурах, где имеется соответствующая виртуальная машина, но в некоторых случаях создаются аппаратные платформы, напрямую выполняющие псевдокод какого-либо языка. Например, псевдокод языка Java называется байт-кодом Java и выполняется в Java Virtual Machine, для его прямого исполнения была создана спецификация процессора picoJava. Для платформы .NET Framework псевдокод называется Common Intermediate Language (CIL), а среда исполнения — Common Language Runtime (CLR).

Некоторые реализации интерпретируемых языков высокого уровня (например, Perl) используют байт-код для оптимизации исполнения: затратные этапы синтаксического анализа и преобразование текста программы в байт-код выполняются один раз при загрузке, затем соответствующий код может многократно использоваться без перекомляции.

Динамическая компиляция

Основная статья: Динамическая компиляция (англ.)

Из-за необходимости интерпретации байт-код выполняется значительно медленнее машинного кода сравнимой функциональности, однако он более переносим (не зависит от операционной системы и модели процессора). Чтобы ускорить выполнение байт-кода, используется динамическая компиляция, когда виртуальная машина транслирует псевдокод в машинный код непосредственно перед его первым исполнением (и при повторных обращениях к коду исполняется уже скомпилированный вариант).

Наиболее популярной разновидностью динамической компиляции является JIT. Другой разновидностью является .

CIL-код также компилируется в код целевой машины JIT-компилятором, а библиотеки .NET Framework компилируются заранее.

Теория

Во-первых, несмотря на то, что код ваших программ находится в файлах .cpp, эти файлы добавляются в проект. Проект содержит все необходимые файлы вашей программы, а также сохраняет указанные вами настройки вашей IDE. Каждый раз, при открытии проекта, он запускается с того момента, на котором вы остановились в прошлый раз. При компиляции программы, проект говорит компилятору и линкеру, какие файлы нужно скомпилировать, а какие связать. Стоит отметить, что файлы проекта одной IDE не будут работать в другой IDE. Вам придется создать новый проект (в другой IDE).

Во-вторых, есть разные типы проектов. При создании нового проекта, вам нужно будет выбрать его тип. Все проекты, которые мы будем создавать на данных уроках, будут консольного типа. Это означает, что они запускаются в консоли (аналог командной строки). По умолчанию, консольные приложения не имеют графического интерфейса пользователя — GUI (сокр. от «Graphical User Interface») и компилируются в автономные исполняемые файлы. Это идеальный вариант для изучения языка C++, так как он сводит всю сложность к минимуму.

В-третьих, при создании нового проекта большинство IDE автоматически добавят ваш проект в рабочее пространство. Рабочее пространство — это своеобразный контейнер, который может содержать один или несколько связанных проектов. Несмотря на то, что вы можете добавить несколько проектов в одно рабочее пространство, все же рекомендуется создавать отдельное рабочее пространство для каждой программы. Это намного упрощает работу для новичков.

Традиционно, первой программой на новом языке программирования является всеми известная программа «Hello, world!». Мы не будем нарушать традиции

Виды компиляторов

  • Векторизующий. Базируется на трансляторе, транслирующем исходный код в машинный код компьютеров, оснащённых векторным процессором.
  • Гибкий. Сконструирован по модульному принципу, управляется таблицами и запрограммирован на языке высокого уровня или реализован с помощью компилятора компиляторов.
  • Диалоговый. См.: диалоговый транслятор.
  • Инкрементальный. Пересобирает программу, заново транслируя только измененные фрагменты программы без перетрансляции всей программы.
  • Интерпретирующий (пошаговый). Последовательно выполняет независимую компиляцию каждого отдельного оператора (команды) исходной программы.
  • Компилятор компиляторов. Транслятор, воспринимающий формальное описание языка программирования и генерирующий компилятор для этого языка.
  • Отладочный. Устраняет отдельные виды синтаксических ошибок.
  • Резидентный. Постоянно находится в оперативной памяти и доступен для повторного использования многими задачами.
  • Самокомпилируемый. Написан на том же языке программирования, с которого осуществляется трансляция.
  • Универсальный. Основан на формальном описании синтаксиса и семантики входного языка. Составными частями такого компилятора являются: ядро, синтаксический и семантический загрузчики.

Особенности реализации

JIT-компиляция может быть применена как ко всей программе, так и к её отдельным частям. Например, текстовый редактор может на лету компилировать регулярные выражения для более быстрого поиска по тексту. С AOT-компиляцией такое сделать не представляется возможным для случаев, когда данные предоставляются во время исполнения программы, а не в момент компиляции. JIT используется в реализациях Java, JavaScript, .NET Framework, в одной из реализаций Python — PyPy. Существующие наиболее распространённые интерпретаторы языков PHP, Ruby, Perl, Python и им подобных, имеют ограниченные или неполные JIT.

Большинство реализаций JIT имеют последовательную структуру: сначала приложение компилируется в байт-код виртуальной машины среды исполнения (AOT-компиляция), а потом JIT компилирует байт-код непосредственно в машинный код. В итоге при запуске приложения тратится лишнее время, что впоследствии компенсируется более быстрой его работой.

пример

Для программистов, поступающих из GCC или Clang в Visual Studio, или программистов, более удобных с командной строкой в ​​целом, вы можете использовать компилятор Visual C ++ из командной строки, а также из среды IDE.

Обратите внимание, что аргументы для чувствительны к регистру, а аргументы для — нет. [Следует иметь в виду, что в некоторых из следующих примеров при указании абсолютных имен пути используется переменная «текущий каталог» оболочки Windows,

Для тех, кто не знаком с этой переменной, он расширяется до текущего рабочего каталога. Из командной строки это будет каталог, в котором вы были, когда вы запускаете , и указывается в командной строке по умолчанию (если ваша командная строка — , например, то — ).]

Предполагая, что в текущей папке имеется один исходный файл с именем , команда для компиляции и ссылки на неоптимизированный исполняемый файл (полезный для начальной разработки и отладки) является (используйте любой из следующих):

Предположим, что дополнительный файл-источник «niam.cpp» в том же каталоге использует следующее:

Вы также можете использовать подстановочные знаки, как и следовало ожидать:

Чтобы переименовать или переместить исполняемый файл, выполните одно из следующих действий:

Оба параметра и передают свой параметр (назовем его ), чтобы как , добавив соответствующее расширение (обычно или ) в «имя» s по мере необходимости. Хотя оба и , насколько мне известно, идентичны по функциональности, последний предпочтительнее для Visual Studio. отмечен как устаревший и, как представляется, в основном предоставляется программистам, более знакомым с GCC или Clang.

Обратите внимание, что, хотя пространство между и указанной папкой и / или именем является необязательным, между и указанным папок и / или именем не может быть пробела. Аналогично, для создания оптимизированного исполняемого файла (для использования в производстве) используйте:

Аналогично, для создания оптимизированного исполняемого файла (для использования в производстве) используйте:

Любое из вышеперечисленного даст исполняемый файл с именем, указанным или , или если ни один из них не указан, с именем, идентичным первому источнику или объекту, указанному компилятору.

Чтобы скомпилировать файл (ы) без ссылки, используйте:

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

Существуют и другие важные параметры командной строки, которые было бы очень полезно для пользователей:

Для тех, кто более знаком с системами * nix и / или GCC / Clang, , и другими инструментами командной строки Visual Studio, могут принимать параметры, указанные с помощью дефиса (например, ), вместо косой черты (например, ). Кроме того, Windows распознает либо косую черту, либо обратную косую черту как допустимый разделитель путей, поэтому можно использовать также пути в стиле nix. Это упрощает преобразование простых команд компилятора из или в , или наоборот, с минимальными изменениями.

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

Примечание. В более старых версиях этого компилятора были доступны специальные флаги функций, однако в основном это использовалось для предварительного просмотра новых функций.

Previous
Next

Литература

Литературная компиляция более всего отвечает дословному переводу данного термина («кража»). Подобная деятельность заключается в сочинении и составлении неких научных или учебных текстов, основываясь на чужих источниках. При этом не производится их творческая обработка, новое осмысление. В редких случаях компиляцией может являться и литературное произведение, причём как полностью, так и частично.

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

Литературные компиляции обычно преследуют учебные или просветительские цели. Как отдельный вид литературного искусства сформировалась ещё в те годы, когда законодательно установленных положений об авторском праве ещё не существовало. В русской классике к компиляциям в ранние годы своего творчества часто прибегали такие авторы, как М. Лермонтов, К. Батюшков, И. Козлов.

Японская анимация. Полнометражный или короткометражный мультипликационный фильм, созданный по мотивам определённого сериала, в жанре «аниме».

Структура компилятора

Процесс компиляции состоит из следующих этапов:

  1. Трансляция программы — трансляция всех или только изменённых модулей исходной программы.
  2. компоновка машинно-ориентированной программы.

В первом случае компилятор представляет собой пакет программ, включающий в себя трансляторы с разных языков программирования и компоновщики. Такой компилятор может компилировать программу, разные части исходного текста которой написаны на разных языках программирования. Нередко такие компиляторы управляются встроенным интерпретатором того или иного командного языка. Яркий пример таких компиляторов — имеющийся во всех UNIX-системах (в частности в Linux) компилятор make.

Во втором случае компилятор де-факто выполняет только трансляцию и далее вызывает компоновщик как внешнюю подпрограмму, который и компонует машинно-ориентированную программу. Этот факт нередко служит поводом считать компилятор разновидностью транслятора, что естественно неверно, — все современные компиляторы такого типа поддерживают организацию импорта программой процедуры (функции) из уже оттранслированого программного модуля, написанного на другом языке программирования. Так в программу на С/С++ можно импортировать функцию написанную например Pascal или Fortran. Аналогично и напротив написанная на С/С++ функция может быть импортирована в Pascal- или Fortran-программу соответственно. Это как правило было бы невозможно без поддержки многими современными компиляторами организации обработки входных данных в процедуру (функций) в соответствии с соглашениями других языков программирования. Например современные компиляторы с языка Pascal помимо соглашения самого Pascal поддерживают организацию обработки процедурой/функцией входных в соответствии с соглашениями языка С/С++. (Чтобы на уровне машинного кода написанная на Pascal процедура/функция работала с входными параметрами в соответствии с соглашениями языка С/С++, — оператор объявления такой Pascal-процедуры/Pascal-функции должен содержать ключевое слово cdecl.) Примерами таких компиляторов являются компиляторы со всех без исключения языков программирования, используемые непосредственно.

Трансляция программы как неотъемлемая составляющая компиляции включает в себя:

  1. Лексический анализ. На этом этапе последовательность символов исходного файла преобразуется в последовательность лексем.
  2. Синтаксический (грамматический) анализ. Последовательность лексем преобразуется в дерево разбора.
  3. Семантический анализ. Дерево разбора обрабатывается с целью установления его семантики (смысла) — например, привязка идентификаторов к их декларациям, типам, проверка совместимости, определение типов выражений и т. д. Результат обычно называется «промежуточным представлением/кодом», и может быть дополненным деревом разбора, новым деревом, абстрактным набором команд или чем-то ещё, удобным для дальнейшей обработки.
  4. Оптимизация. Выполняется удаление излишних конструкций и упрощение кода с сохранением его смысла. Оптимизация может быть на разных уровнях и этапах — например, над промежуточным кодом или над конечным машинным кодом.
  5. Генерация кода. Из промежуточного представления порождается код на целевом машинно-ориентированном языке.

Описание

В языках, таких как Java, PHP, C#, Lua, Perl, GNU CLISP, исходный код транслируется в одно из промежуточных представлений, называемое байт-кодом. Байт-код не является машинным кодом какого-либо конкретного процессора и может переноситься на различные компьютерные архитектуры и исполняться точно так же. Байт-код интерпретируется (исполняется) виртуальной машиной. JIT читает байт-код из некоторых секторов (редко сразу из всех) и компилирует их в машинный код. Этим сектором может быть файл, функция или любой фрагмент кода. Однажды скомпилированный код может кэшироваться и в дальнейшем повторно использоваться без перекомпиляции.

Динамически компилируемая среда — это среда, в которой компилятор может вызываться приложением во время выполнения. Например, большинство реализаций Common Lisp содержат функцию compile, которая может создать функцию во время выполнения; в Python это функция eval. Это удобно для программиста, так как он может контролировать, какие части кода действительно подлежат компиляции. Также с помощью этого приёма можно компилировать динамически сгенерированный код, что в некоторых случаях приводит даже к лучшей производительности, чем реализация в статически скомпилированном коде. Однако стоит помнить, что подобные функции могут быть опасны, особенно когда данные передаются из недоверенных источников.

Основная цель использования JIT — достичь и превзойти производительность статической компиляции, сохраняя при этом преимущества динамической компиляции:

  • Большинство тяжеловесных операций, таких как парсинг исходного кода и выполнение базовых оптимизаций, происходит во время компиляции (до развёртывания), в то время как компиляция в машинный код из байт-кода происходит быстрее, чем из исходного кода.
  • Байт-код более переносим (в отличие от машинного кода).
  • Среда может контролировать выполнение байт-кода после компиляции, поэтому приложение может быть запущено в песочнице (стоит отметить, что для нативных программ такая возможность тоже существует, но реализация данной технологии сложнее).
  • Компиляторы из байт-кода в машинный код легче в реализации, так как большинство работы по оптимизации уже было проделано компилятором.

JIT, как правило, эффективней, чем интерпретация кода. К тому же в некоторых случаях JIT может показывать большую производительность по сравнению со статической компиляцией за счёт оптимизаций, возможных только во время исполнения:

  1. Компиляция может осуществляться непосредственно для целевого процессора и операционной системы, на которой запущено приложение. Например, JIT может использовать векторные SSE2 расширения процессора, если он обнаружит их поддержку. Однако, до сих пор нет основных реализаций JIT, где этот подход бы использовался, ведь чтобы обеспечить подобный уровень оптимизации, сравнимый со статическими компиляторами, потребовалось бы либо поддерживать бинарный файл под каждую платформу, либо включать в одну библиотеку оптимизаторы под каждую платформу.[источник не указан 434 дня]
  2. Среда может собирать статистику о работающей программе и производить оптимизации с учётом этой информации. Некоторые статические компиляторы также могут принимать на вход информацию о предыдущих запусках приложения.
  3. Среда может делать глобальные оптимизации кода (например, встраивание библиотечных функций в код) без потери преимуществ динамической компиляции и без накладных расходов, присущих статическим компиляторам и линкерам.
  4. Более простое перестраивание кода для лучшего использования кэша

Виды компиляции

Виды компиляции:

  • Пакетная. Компиляция нескольких исходных модулей в одном задании.
  • Построчная. Машинный код порождается и затем исполняется для каждой завершённой грамматической конструкции языка. Внешне воспринимается как интерпретация, но устройство имеет иное.
  • Условная. Компиляция, при которой транслируемый текст зависит от условий, заданных в исходной программе директивами компилятора. (Яркий пример — работа препроцессора языка С и производных от него.) Так, в зависимости от значения некой константы некая заданная часть исходного текста программы транслируется или не транслируется.

Компиляция программ Linux

Первое что нам понадобиться — это исходники самой программы. В этом примере мы будем собирать самую последнюю версию vim. Это вполне нейтральная программа, достаточно простая и нужная всем, поэтому она отлично подойдет для примера.

Получение исходников

Первое что нам понадобиться, это исходные коды программы, которые можно взять на GitHub. Вы можете найти исходники для большинства программ Linux на GitHub. Кроме того, там же есть инструкции по сборке:

Давайте загрузим сами исходники нашей программы с помощью утилиты git:

Также, можно было скачать архив на сайте, и затем распаковать его в нужную папку, но так будет удобнее. Утилита создаст папку с именем программы, нам нужно сделать ее рабочей:

Настройка configure

Дальше нам нужно запустить скрипт, который проверит нашу программу на совместимость с системой и настроит параметры компиляции. Он называется configure и поставляется разработчиками программы вместе с исходниками. Весь процесс компиляции описан в файле Makefile, его будет создавать эта утилита.

Если configure нет в папке с исходниками, вы можете попытаться выполнить такие скрипты чтобы его создать:

Также для создания этого скрипта можно воспользоваться утилитой automake:

Утилита automake и другие из ее набора генерируют необходимые файлы на основе файла Mackefile.am. Этот файл обязательно есть в большинстве проектов.

После того как вы получили configure мы можем переходить к настройке. Одним из огромных плюсов ручной сборки программ есть то, что вы можете сами выбрать с какими опциями собирать программу, где она будет размещена и какие дополнительные возможности стоит включить. Все это настраивается с помощью configure. Полный набор опций можно посмотреть, выполнив:

Рассмотрим наиболее часто используемые, стандартные для всех программ опции:

  • —prefix=PREFIX — папка для установки программы, вместо /, например, может быть /usr/local/, тогда все файлы будут распространены не по основной файловой системе, а в /usr/local;
  • —bindir=DIR — папка для размещения исполняемых файлов, должна находится в PREFIX;
  • —libdir=DIR — папка для размещения и поиска библиотек по умолчанию, тоже в PREFIX;
  • —includedir=DIR — папка для размещения man страниц;
  • —disable-возможность — отключить указанную возможность;
  • —enable-возможность — включить возможность;
  • —with-библиотека — подобно enable активирует указанную библиотеку или заголовочный файл;
  • —without-библиотека — подобное disable отключает использование библиотеки.

Вы можете выполнить configure без опций, чтобы использовать значения по умолчанию, но также можете вручную указать нужные пути. В нашем случае ./configure есть, и мы можем его использовать:

Во время настройки утилита будет проверять, есть ли все необходимые библиотеки в системе, и если нет, вам придется их установить или отключить эту функцию, если это возможно. Например, может возникнуть такая ошибка: no terminal library found checking for tgetent()… configure: error: NOT FOUND!

В таком случае нам необходимо установить требуемую библиотеку. Например, программа предлагает ncurses, поэтому ставим:

Приставка lib всегда добавляется перед библиотеками, а -dev — означает, что нам нужна библиотека со всеми заголовочными файлами. После удовлетворения всех зависимостей настройка пройдет успешно.

Сборка программы

Когда настройка будет завершена и Makefile будет готов, вы сможете перейти непосредственно к сборке программы. На этом этапе выполняется непосредственно преобразование исходного кода в машинный. Утилита make на основе Makefile сделает все необходимые действия:

Дальше осталось установить саму программу, если вы использовали опцию prefix, чтобы не устанавливать программу в основную файловую систему, то можно применить стандартную опцию make:

После этого программа будет установлена в указанную вами папку, и вы сможете ее использовать. Но более правильный путь — создавать пакет для установки программы, это делается с помощью утилиты checkinstall, она позволяет создавать как deb, так и rpm пакеты, поэтому может использоваться не только в Ubuntu. Вместо make install выполните:

Затем просто установите получившийся пакет с помощью dpkg:

После этого сборка программы полностью завершена и установлена, так что вы можете переходить к полноценному использованию.

Если вы устанавливали программу с помощью make install, то удалить ее можно выполнив в той же папке обратную команду:

Команда удалит все файлы, которые были скопированы в файловую систему.

Происхождение

Появление компиляции обусловлено просветительской и учебно-пропагандистской деятельностью, а также отсутствием четкого представления об авторстве и авторском праве на сочинение, составленное из фрагментов текстов других произведений, в античных традициях.

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

“Черкесы”. Рукопись поэмы Лермонтова.

Слово «компиляция» перешло в русскую литературу в начале XIX века и обозначало произведения, содержащие отрывки трудов других мастеров. Компиляции встречаются в раннем творчестве М. Ю. Лермонтова, в частности в поэме «Черкесы» (1828 г.) сочетаются части сочинений К. Н. Батюшкова, И. И. Дмитриева и других поэтов с авторским текстом.

Заключение

Для многих программистов языки C/C++ ассоциируются как нечто «долго компилирующееся». И на это есть свои причины: выбранный в свое время способ трансляции, метапрограммирование (для C++), тысячи их. Благодаря описанным методам оптимизации можно лишить себя подобных предрассудков о чрезмерно долгой компиляции. В частности, время сборки нашего ядра анализатора PVS-Studio для анализа C и C++ кода удалось снизить с 1 минуты 30 секунд до 40 секунд путем интеграции Single Compilation Units и переработки заголовочных и исходных файлов. Более того, если бы до начала оптимизаций не были использованы параллельная компиляция и предкомпилированные заголовки, нами было бы получено семикратное уменьшение времени сборки!

В окончание хочется добавить, что об этой проблеме прекрасно помнят в комитете стандартизации и полный ходом идет решение данной проблемы: все мы ждем нового стандарта C++20, который, возможно, одним из нововведений «завезёт» модули в любимый многими язык и сделает жизнь C++ программистов гораздо проще.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Adblock
detector