Обеспечивающие перевод программы на машинный код

Все предметы

Биология

География

Физика

Химия

История

Обществознание

Русский язык

Литература

Экономика

Право

Математика

Алгебра

Геометрия

Информатика

Английский язык

Українська мова

Українська література

Другие предметы

Беларуская мова

Қазақ тiлi

Немецкий язык

Окружающий мир

Французский язык

Музыка

МХК

ОБЖ

Психология

Оʻzbek tili

Кыргыз тили

Астрономия

Физкультура и спорт

Мегамозг.com

maria2216

+45

Ответ дан

3 года назад

Информатика

10 — 11 классы

Обеспечивающие перевод программы на машинный код ____________
разделяются на 2 типа:___________ и компиляторы​

Ответ

2.5/5
(2 оценки)

2

pkxosksksososos
3 года назад

Светило науки — 6 ответов — 0 раз оказано помощи

Ответ:

1.компилятор

2.ассемблер

Оцените пользу ответа

Мозг
Отвечающий

Остались вопросы?

Задать вопрос

#статьи


  • 0

Что такое компилятор и как он работает

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

Иллюстрация: Оля Ежак для Skillbox Media

Дмитрий Зверев

Любитель научной фантастики и технологического прогресса. Хорошо сочетает в себе заумного технаря и утончённого гуманитария. Пишет про IT и радуется этому.

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

В этой статье разберёмся:

  • для чего нужны компиляторы;
  • как они работают;
  • на каких языках их пишут;
  • почему у одного языка может быть несколько компиляторов;
  • какие они бывают;
  • в чём их отличие от интерпретаторов и трансляторов;
  • какие плюсы и минусы есть у компилируемых языков;
  • где узнать про компиляторы подробнее.

Выдвинем дерзкое утверждение: компьютеры очень глупы, они не понимают человеческого языка — и, в частности, языков программирования. Всё, что они умеют, — это принимать электрические сигналы и как-то на них реагировать.

Если упрощать, то компьютер — это коробка с миллиардами переключателей. Дёрнули одни — сложили два числа, дёрнули другие — записали данные на жёсткий диск. И хотя современные компьютеры с аппаратной точки зрения устроены сложнее, принцип остаётся похожим.

Когда мы пишем код, то используем понятные для людей слова, такие как print, string, import, Процедура и Исключение. Нам их значение кажется очевидным: здесь вывели результат на печать, а там объявили строковую переменную. Но для компьютера эти слова ничего не значат.

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

Компилятор понимает, что значит слово print — и даже умеет сказать компьютеру, как его правильно обработать. Таким образом, он решает три задачи:

  • разбирает синтаксис написанного;
  • анализирует его;
  • генерирует машинный код.

На вход компилятор принимает исходный код, а отдаёт исполняемый файл — программу, которая готова к работе.

Звучит просто. Но к компиляторам есть много вопросов — например, на каких языках их пишут, как они устроены внутри и каких видов бывают. Обо всём этом расскажем в статье. И начнём с того, как работают компиляторы.

Итак, ещё раз: чтобы компьютеры выполняли команды программистов, им нужны переводчики с человеческого на машинный. Рассмотрим процесс перевода — сначала в общих чертах, а потом подробно.

🏃‍♂️ Коротко

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

Чтобы преобразовать исходный код, компилятор использует собственный словарь с определениями — например, оператор if меняет на 11010011100110, а сложение — на 101011. Он делает это, пока не закончатся все строки в файле. Получается исполнительный файл, который выглядит так:

001011011010010101110101010101010100001100001110111100110100001010001001110…

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

🤔 Подробнее. Компилирование состоит из пяти этапов: синтаксического анализа, парсинга, семантического анализа, оптимизации и генерации кода. Давайте разберём каждую стадию.

1️⃣ Синтаксический анализ. Это что-то вроде разбора грамматики языка. Когда мы пишем код, то следуем определённым правилам — синтаксису. Например, в Java между командами ставим точку с запятой. Если этого не сделать, то получим ошибку.

На этапе синтаксического анализа компилятор проверяет, соответствует ли код правилам конкретного языка программирования. И пока он не думает о том, что именно написано, — проверка идёт только по формальным признакам.

2️⃣ Парсинг. На этом этапе компилятор разбивает код на маленькие кусочки — токены. Каждый токен — это какое-то слово или символ, например if, while, int или (.

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

Давайте посмотрим, как выглядит такое дерево.

Допустим, у нас есть простой код со сложением двух чисел:

x = 5 + 3

Здесь пять токенов: x, =, 5, + и 3. Пробелы считать не будем. Из этих токенов строится такое дерево:

Обычно синтаксические деревья намного сложнее — очень намного.
Изображение: Skillbox Media

Мы видим, что на вершине находится главная операция — присваивание переменной x результата сложения двух чисел. От неё отходит две ветви — сама переменная x и символ сложения, который ветвится на слагаемые числа.

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

3️⃣ Семантический анализ. Компилятор начинает вдумываться в то, что написано в коде, анализируя составленное синтаксическое дерево. Например, если мы объявили переменную, он понимает, что это значит и какие операции можно с ней выполнить.

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

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

Самый простой пример оптимизации — умножение на ноль. Например, у нас есть фрагмент кода:

int x = sin(126) * cos(54) + tan(78);
int y = x * 0;

Чтобы определить значение переменной y, потребуется сначала вычислить сложную формулу для переменной x. Но мы, люди, сразу видим, что при умножении на ноль, результат будет нулём, а значит, смысла считать переменную x нет. Компилятор тоже видит такие вещи — и не будет вычислять то, что вычислять бесполезно. Он просто заменит эти две строки кода на одну:

int y = 0;

Удобно, правда? Но это сработает только в том случае, если переменная x не пригодится нам в программе дальше.

Это возможно из-за особенностей работы компилятора — он не выполняет код, а сначала читает его и ищет способы оптимизации программы.

5️⃣ Генерация кода. Синтаксис разобран, анализ проведён, код оптимизирован — пора перевести его на язык компьютера. На этом этапе все команды, что мы писали на языке программирования, переводятся в машинные инструкции.

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

👉 Подождите, раз компилятор переводит исходный код в машинный, а сам он является программой, то на каком языке тогда он написан? Какой-то замкнутый круг получается.

На самом деле всё не так сложно. Компиляторы можно писать на любом языке — хоть на Python, хоть на языке ассемблера. Но есть нюанс.

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

  • разработчик пишет код на ассемблере;
  • компилятор переводит его в машинные инструкции;
  • компьютер запускает эти инструкции.

Получается, что компилятор на ассемблере — это другая программа на нём же, которая умеет переводить код. Например, она подставляет вместо команды jmp строку 001110111, которая запускает нужные шестерёнки внутри процессора.

После уже появились языки более высокого уровня — например, C. Компилятор для C написан на том же ассемблере. Работает он похожим образом:

  • разработчик пишет программу на C;
  • компилятор переводит команды на языке программирования с C в машинные инструкции;
  • компьютер запускает эти инструкции.

Дальше — вверх по высокоуровневости языков программирования. Компилятор на С++ написан на C, а для JavaScript — на C++. Но если спускаться по цепочке, то мы рано или поздно придём к ассемблеру.

👉 Стойте, а зачем тогда языкам программирования несколько компиляторов? Почему бы всем не использовать только один?

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

Его компилятор написан на ассемблере, а сделал это Деннис Ритчи. Он исходил из принципов, что одни команды языка должны конвертироваться в одни инструкции для ассемблера, а другие — в другие. Но, возможно, это была не лучшая реализация: в каких-то местах компилятор мог работать медленно, а в каких-то и вовсе не справлялся. Поэтому сторонние разработчики решили написать свои версии «переводчика» кода на C.

Например, кто-то мог взглянуть на код компилятора C и подумать: «Да тут же нет сборщика мусора, это что такое-то?!» — и пойти написать свою версию, которая будет залатывать все утечки памяти и чистить неиспользуемые переменные.

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

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

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

В зависимости от задач компиляторы можно разделить на несколько групп. Например, по направлению перевода кода.

Умеют переводить код на языке программирования в машинный. Именно о них мы преимущественно и говорили в этой статье. Пример — компилятор g++ для языка C++.

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

К кросс-компиляторам относят GCC (GNU Compiler Collection). Он поддерживает C++, Objective-C, Java, Fortran и Go и разную архитектуру процессоров.

Кадр: сериал «Кремниевая долина» / HBO

Преобразуют исходный код языка высокого уровня в исходный код другого языка высокого уровня. Например, транспайлер Babel преобразует ECMAScript 2015+ в JavaScript.

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

Компиляторы — это не единственный способ перевести исходный код в машинный. Ещё есть интерпретаторы и JIT-компиляторы. Давайте коротко расскажем, в чём различия между ними.

Интерпретатор. Это как синхронный переводчик. Он читает исходный код и сразу же выполняет его построчно. Интерпретатор не создаёт дополнительных файлов и не строит синтаксические деревья, а выполняет инструкции на лету, переводя их в байт-код. Например, так работает CPython для языка Python.

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

Отдельно стоит упомянуть байт-код. Это специальный код, который запускается на виртуальной машине. Можно сказать, что он занимает промежуточное положение между кодом, написанным на языке программирования, и машинным кодом. Его реализацию можно найти в Java или Python.

Давайте посмотрим на список аргументов за и против для компилируемых языков — то есть тех, которые используют компиляторы. Примеры таких языков: C++, Haskell, Fortran, Rust, Swift и Go.

Быстрота выполнения. Компилятор переводит исходный код в машинный всего один раз. А дальше — всё уже оптимизировано и готово к запуску. Поэтому такие программы работают быстрее, так как компьютеру не приходится тратить время на их повторный перевод.

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

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

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

⛔️ Сложность исправления ошибок. Обычно ошибки при компилировании выглядят устрашающе из-за запутанного описания проблемы. Просто попробуйте не поставить точку с запятой в файле с C++ и убедитесь, что ничего хуже вы не видели.

⛔️ Зависимость от платформы. Если скомпилировать программу для Windows, то её никак нельзя будет запустить на macOS. Поэтому придётся дополнительно брать другой компилятор и начинать процесс заново — или использовать кросс-компиляторы.

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

  • Компиляторы: принципы, технологии и инструментарий Альфреда Ахо и Моники Лам.
  • «Конструирование компиляторов», Сергея Свердлова.
  • Бесплатный курс «Языки программирования и компиляторы» от Computer Science Center.

Научитесь: Специалист по кибербезопас­ности
Узнать больше

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

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

Языки программирования, для перевода которых используются компиляторы, называются компилируемыми.

Для чего нужен компилятор

Изначально компьютер не понимает смысл написанного на любом языке программирования. Язык компьютера — машинные коды, нули и единицы, в которых зашифрована информация и команды. Писать на машинных кодах программы практически невозможно: даже простейшее действие будет отнимать много часов работы программиста. Поэтому появились языки программирования, более понятные для людей, и специальные программы, которые переводят эти языки в машинные коды. Эти программы и есть компиляторы.

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

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

Компилятор и интерпретатор: в чем разница

Компиляция — не единственный подход к «переводу» человекопонятного языка программирования на машинный. Еще есть интерпретаторы и байт-код, но там технологии совсем другие.

Интерпретатор — это тоже программа, которая «переводит» текст на высокоуровневом языке программирования, но делает это иначе. Она не собирает весь код в один исполняемый файл для последующего запуска, а исполняет код сразу, построчно. Это чуть медленнее, но иногда удобнее. Языки, использующие интерпретаторы, называются интерпретируемыми.

Байт-код — «промежуточное звено» между подходами компиляции и интерпретации. Программа преобразуется в особый код, который запускается под специальной виртуальной машиной. Языков, которые работают так, относительно немного, самый известный и яркий пример — Java.

В каких языках используются компиляторы

Среди популярных сегодня языков компилируемыми являются Swift и Go, а также C / C++ и Objective-C. Другие примеры — Visual Basic, Haskell, Pascal / Delphi, Rust, а также Lisp, Prolog и прочие менее известные языки. Разумеется, компилируемым является и язык ассемблера — очень низкоуровневый и написанный напрямую на машинных кодах.

Отдельно можно выделить языки, которые трансформируются в байт-код — это тоже своего рода компиляция. К ним относятся Java, Scala и Kotlin, а также C# и языки платформы .NET.

На каких языках пишут компиляторы

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

Например, один из компиляторов языка Go частично написан на C++, самый первый компилятор C++ — на ассемблере, а уже ассемблер — на машинных кодах.

Тот же язык. Написать компилятор для языка программирования можно на других версиях того же языка — такой подход разрешен и активно используется в разработке. Это нужно, чтобы компиляторы были более гибкими и «умными» и могли поддерживать больше возможностей, — ассемблер довольно примитивен и не решает всех задач.

Выглядит это так:

  • первый, более простой компилятор пишется на ассемблере;
  • второй пишется уже на нужном языке и компилируется первым компилятором;
  • переведенный в машинные коды второй компилятор компилирует свои же исходники — получается более новая и мощная версия его же.

Как пишут компиляторы

Например, большинство современных компиляторов для C / C++ написано на C / C++. Такие компиляторы называют самокомпилируемыми.

Почему у одного языка может быть несколько компиляторов

У большинства языков программирования несколько компиляторов. Их еще называют реализациями. Изначальную реализацию пишет создатель языка, потом со временем появляются альтернативные. Зачем это делается? Цели могут быть разными:

  • написать более современный и функциональный компилятор, обновить язык;
  • оптимизировать язык и сделать его эффективнее;
  • создать свободную реализацию, которую сможет дополнять сообщество;
  • исправить ошибки, которые есть в существующих реализациях;
  • перенести язык на другую платформу, и так далее.

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

Какими бывают компиляторы

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

Один компилятор может «знать» несколько языков программирования. Яркий пример такого решения — GCC, или GNU Compiler Collection, кросс-компилятор для нескольких операционных систем и языков, полностью бесплатный и свободный. На нем написано программное обеспечение GNU.

Существуют и так называемые компиляторы компиляторов. Они генерируют компиляторы для языка на основе его формального описания.

Как устроены и работают компиляторы

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

Сама компиляция может быть:

  • построчной — в машинный код по очереди переводится каждая строка, что похоже на интерпретацию, но отличается технически;
  • пакетной — код разбивается на блоки, или пакеты, и компилируется поблочно;
  • условной — особенности компиляции зависят от условий, которые прописаны в исходном коде компилируемой программы.

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

схема работы компилятора

Преимущества компилируемых языков

  • Компилируемые языки обычно быстрее, чем интерпретируемые, и их легче оптимизировать.
  • Итоговый размер кода у компилируемых языков, как правило, меньше, чем у интерпретируемых.
  • В компилируемых языках намного шире возможность контролировать аппаратные ресурсы. Это не значит, что они все низкоуровневые, но обратное — верно: практически все низкоуровневые языки — компилируемые.
  • Когда процессоры становятся мощнее, именно компилируемые языки могут в должной мере задействовать их преимущества.
  • Код после компилятора лучше оптимизируется под конкретные устройства, архитектуру «железа», эффективно задействует память и не тратит лишних ресурсов.
  • Компилируемые языки обычно мощные и высокопроизводительные, поэтому на них пишут игры и другие серьезно нагруженные приложения.

Недостатки компилируемых языков

  • В отличие от интерпретируемых языков, компилируемые не выполняют код сразу — его сначала нужно собрать, а это лишний шаг и лишнее время.
  • Код сложнее в отладке: приходится заново компилировать его при каждом, даже небольшом изменении. Сам процесс поиска и устранения ошибок бывает довольно неочевидным.
  • Машинный код жестко связан с архитектурой платформы и различается в зависимости от системы. Поэтому компилируемые языки — по умолчанию не кроссплатформенные. Для переноса языка на другую операционную систему понадобится писать новый компилятор. Правда, есть исключения в виде универсальных кросс-компиляторов, работающих под разными платформами, но они подходят не для всего.
  • Для новичков проблема еще и в том, что компилируемые языки часто сложнее, чем интерпретируемые. Изучать их с нуля может быть тяжело, хотя и тут есть исключения.

Как пользоваться компилятором

Начинающий разработчик редко взаимодействует с компилятором напрямую. Он скачивает язык программирования, в том числе его компилятор, а потом работает в редакторе кода или IDE. Среда разработки сама запускает компилятор каждый раз, когда пользователь кликает на кнопку сборки или выполнения программы. Для этого его не нужно вызывать вручную. Иногда среда может сама включать в себя несколько компиляторов и выбирать подходящий в каждом случае.

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

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

Узнайте больше об устройстве и работе языков программирования на курсах — получите новую профессию и станьте востребованным IT-специалистом.

Транслятор, компилятор, интерпретатор

Транслятор(англ. translator —
переводчик) — это программа-переводчик.
Она преобразует программу, написанную
на одном из языков высокого уровня, в
программу, состоящую из машинных команд.
Транслятор обычно выполняет также
диагностику ошибок, формирует словари
идентификаторов, выдаёт для печати
тексты программы и т. д. Язык, на котором
представлена входная программа,
называется исходным языком, а сама
программа — исходным кодом. Выходной
язык называется целевым языком или
объектным кодом.

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

Виды трансляторов

  1. Диалоговый. Обеспечивает использование
    языка программирования в режиме
    разделения времени (англ.).

  2. Синтаксически-ориентированный
    (синтаксически-управляемый). Получает
    на вход описание синтаксиса и семантики
    языка и текст на описанном языке, который
    и транслируется в соответствии с
    заданным описанием.

  3. Однопроходной. Формирует объектный
    модуль за один последовательный просмотр
    исходной программы.

  4. Многопроходной. Формирует объектный
    модуль за несколько просмотров исходной
    программы.

  5. Оптимизирующий. Выполняет оптимизацию
    кода в создаваемом объектном модуле.

  6. Тестовый. Набор макрокоманд языка
    ассемблера, позволяющих задавать
    различные отладочные процедуры в
    программах, составленных на языке
    ассемблера.

  7. Обратный. Для программы в машинном коде
    выдаёт эквивалентную программу на
    каком-либо языке программирования
    (см.: дизассемблер, декомпилятор).

Трансляторы реализуются в виде
компиляторов или интерпретаторов. С
точки зрения выполнения работы компилятор
и интерпретатор существенно различаются.

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

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

  • Векторизующий. Транслирует исходный
    код в машинный код компьютеров, оснащённых
    векторным процессором.

  • Гибкий. Сконструирован по модульному
    принципу, управляется таблицами и
    запрограммирован на языке высокого
    уровня или реализован с помощью
    компилятора компиляторов.

  • Диалоговый. См.: диалоговый транслятор.

  • Инкрементальный. Повторно транслирует
    фрагменты программы и дополнения к ней
    без перекомпиляции всей программы.

  • Интерпретирующий (пошаговый).
    Последовательно выполняет независимую
    компиляцию каждого отдельного оператора
    (команды) исходной программы.

  • Компилятор компиляторов. Транслятор,
    воспринимающий формальное описание
    языка программирования и генерирующий
    компилятор для этого языка.

  • Отладочный. Устраняет отдельные виды
    синтаксических ошибок.

  • Резидентный. Постоянно находится в
    оперативной памяти и доступен для
    повторного использования многими
    задачами.

  • Самокомпилируемый. Написан на том же
    языке, с которого осуществляется
    трансляция.

  • Универсальный. Основан на формальном
    описании синтаксиса и семантики входного
    языка. Составными частями такого
    компилятора являются: ядро, синтаксический
    и семантический загрузчики.

Время на прочтение
13 мин

Количество просмотров 45K

С рождением PHP 7 не прекращаются споры об абстрактных синтаксических деревьях, just-in-time компиляторах, статическом анализе и т. д. Но что означают все эти термины? Это какие-то волшебные свойства, делающие PHP гораздо производительнее? И если да, то как это всё работает? В этой статье мы рассмотрим основы работы языков программирования и разъясним для себя процесс, который должен выполняться до того, как компьютер запустит, например, ваш PHP-скрипт.

Интерпретируя код

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

  • каждая строка представляет собой выражение,
  • каждое выражение состоит из команды (оператора)
  • и любого количества значений (операндов), которыми оперирует команда.

Пример:

set a 1
set b 2
add a b c
print c

Это простой язык, так что мы можем без опаски предположить, что этот код всего лишь выводит на экран 3. Оператор set берёт переменную и присваивает ей число (совсем как $a=1 в PHP). Оператор add берёт две переменные для добавления и сохраняет результат в третьей. Оператор print выводит её на экран.

Теперь давайте напишем программу, которая считывает каждое «выражение», находит оператор и операнды, а затем что-то с ними делает, в зависимости от конкретного оператора. Это довольно просто реализовать на PHP, как вы можете видеть на примере листинга 1.

Листинг 1

01. <?php
02.
03. $lines = file($argv[1]);
04.
05. $linenr = 0;
06. foreach ($lines as $line) {
07. 		$linenr++;
08. 		$operands = explode(" ", trim($line));
09. 		$command = array_shift($operands);
10.
11. 		switch ($command) {
12. 			case 'set' :
13. 				$vars[$operands[0]] = $operands[1];
14. 				break;
15. 			case 'add' :
16. 				$vars[$operands[2]] = $vars[$operands[0]] + $vars[$operands[1]];
17. 				break;
18. 			case 'print' :
19. 				print $vars[$operands[0]] . "\n";
20. 				break;
21. 			default :
22. 				throw new Exception(sprintf("Unknown command in line %s\n", $linenr));
23. 		}
24. }

Это очень простая программа, и вам не придётся писать своё следующее веб-приложение на вашем новом языке. Но данный пример помогает понять, как легко можно создать новый язык и получить программу, которая способна считывать и выполнять этот язык. В нашем случае она построчно считывает исходный файл и выполняет код в зависимости от текущего оператора. Для запуска приложения нам не нужно преобразовывать его в ассемблер или двоичный код, оно и так прекрасно работает. Этот метод выполнения программ называется интерпретированием. Например, таким образом часто выполняются программы на Basic: каждое выражение считывается и сразу же выполняется в высокоуровневом режиме.

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

  • Какой оператор нужно выполнить?
  • Это правильный оператор?
  • Есть ли у него нужное количество операндов?

А ведь нам нельзя забывать и о других задачах. Например, оператор set может присваивать переменным только числовые значения или строковые тоже? Или даже значения других переменных? Чтобы правильно обработать каждое выражение, нужно ответить на все эти вопросы. Что произойдёт, если написать set 1 4? Короче, таким образом практически невозможно создавать быстро работающие приложения.

Но, несмотря на неторопливость, у интерпретирования есть преимущества: мы можем сразу запускать программу после каждого внесённого изменения. Для внимательных: когда я что-то меняю в PHP-скрипте, я сразу могу его выполнить и увидеть изменения; означает ли это, что PHP — интерпретируемый язык? На данный момент будем считать, что да. PHP-скрипт интерпретируется подобно нашему гипотетическому простому языку. Но в следующих разделах мы ещё к этому вернёмся!

Транскомпилирование

Как можно заставить нашу программу «работать быстро»? Это можно сделать разными способами. Один из них, разработанный в Facebook, называется HipHop (я имею в виду «старую» систему HipHop, а не используемую сегодня HHVM). HipHop преобразовывал один язык (PHP) в другой (С++). Результат преобразования можно было с помощью компилятора С++ превратить в двоичный код. Его компьютер способен понять и выполнить без дополнительной нагрузки в виде интерпретатора. В результате экономится ОГРОМНОЕ количество вычислительных ресурсов и приложение работает гораздо быстрее.

Этот метод называется source-to-source компилированием, или транскомпилированием, или даже транспилированием (transpiling). На самом деле происходит не компилирование в двоичный код, а преобразование в то, что может быть скомпилировано в машинный код существующими компиляторами.

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

Транскомпилирование также используется для того, чтобы сделать «жёсткие» языки более простыми и динамичными. Например, браузеры не понимают код, написанный на LESS, SASS и SCSS. Но зато его можно транспилировать в CSS, который браузеры понимают. Поддерживать CSS проще, но приходится дополнительно транскомпилировать.

Компилирование

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

К сожалению, написание компилятора — одна из труднейших задач в информатике. Например, при компилировании в двоичный код нужно учитывать, на каком компьютере он будет выполняться: на 32-битной Linux, или 64-битной Windows, или вообще на OS X. Зато интерпретируемый скрипт может легко выполняться где угодно. Как и в PHP, нам не нужно переживать о том, где выполняется наш скрипт. Хотя может встречаться и код, предназначенный для конкретной ОС, что сделает невозможным выполнение скрипта на других системах, но это не вина интерпретатора.

Но даже если мы избавимся от стадии транскомпилирования, нам никуда не деться от компилирования. Например, большие программы, написанные на С (компилируемый язык), могут компилироваться чуть ли не час. Представьте, что вы написали приложение на PHP и вам нужно ждать ещё десять минут, прежде чем увидеть, работают ли внесённые изменения.

Используя всё лучшее

Если интерпретирование подразумевает медленное выполнение, а компилирование сложно в реализации и требует больше времени при разработке, то как работают языки вроде PHP, Python или Ruby? Они довольно быстрые!

Это потому, что они используют и интерпретирование, и компилирование. Давайте посмотрим, как это получается.

Что, если бы мы могли преобразовывать наш выдуманный язык не напрямую в двоичный код, а в нечто, очень на него похожее (это называется «байт-код»)? И если бы этот байт-код был так близок к тому, как работает компьютер, что его интерпретирование выполнялось бы очень быстро (например, миллионы байт-кодов в секунду)? Это сделало бы наше приложение почти таким же быстрым, как и компилируемое, при этом сохранились бы все преимущества интерпретируемых языков. Самое главное, нам не пришлось бы компилировать скрипты при каждом изменении.

Выглядит очень заманчиво. По сути, подобным образом работают многие языки — PHP, Ruby, Python и даже Java. Вместо считывания и поочерёдного интерпретирования строк исходного кода, в этих языках используется другой подход:

  • Шаг 1. Считать скрипт (PHP) целиком в память.
  • Шаг 2. Целиком преобразовать/компилировать скрипт в байт-код.
  • Шаг 3. Выполнить байт-код посредством интерпретатора (PHP).

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

Процесс можно легко оптимизировать: предположим, что мы запустили веб-сервер и каждый запрос выполняет скрипт index.php. Зачем каждый раз грузить его в память? Лучше закешировать файл, чтобы можно было быстро преобразовывать его при каждом запросе.

Ещё одна оптимизация: после генерирования байт-кода мы можем использовать его при всех последующих запросах. Так что можно закешировать и его (главное, убедитесь, что при изменении исходного файла байт-код будет перекомпилироваться). Именно это делают кеши кода операций (opcode caches), вроде расширения OPCache в PHP: кешируют скомпилированные скрипты, чтобы их можно было быстро выполнить при последующих запросах без избыточных загрузок и компилирования в байт-код.

Наконец, последний шаг к высокой скорости — выполнение байт-кода нашим PHP-интерпретатором. В следующей части мы сравним это с обычными интерпретаторами. Во избежание путаницы: подобный интерпретатор байт-кода часто называется «виртуальной машиной», потому что в определённой степени он копирует работу машины (компьютера). Не надо путать это с виртуальными машинами, запускаемыми на компьютерах, вроде VirtualBox или VMware. Речь идёт о таких вещах, как JVM (Java Virtual Machine) в мире Java и HHVM (HipHop Virtual Machine) в мире PHP. Свои виртуальные машины есть у Python и Ruby. В некотором роде все они являются высокоспециализированными и производительными интерпретаторами байт-кода.

Каждая ВМ выполняет собственный байт-код, генерируемый конкретным языком, и они несовместимы между собой. Вы не можете выполнять байт-код PHP на ВМ Python, и наоборот. Однако теоретически возможно создать программу, компилирующую PHP-скрипты в байт-код, который будет понятен ВМ Python. Так что в теории вы можете запускать PHP-скрипты в Python (серьёзный вызов!).

Байт-код

Как выглядит и работает байт-код? Рассмотрим два примера. Возьмём PHP-код:

$a = 3;
echo "hello world";
print $a + 1;

Посмотреть его байт-код можно с помощью 3v4l.org или установив расширение VLD. Получим следующее:

Теперь возьмём аналогичный пример на Python:

def foobar():
a = 1
print "hello world",
print a + 4

Python может напрямую сгенерировать коды операций ©python:

dis.dis(func):

У нас есть два простых скрипта и их байт-коды. Обратите внимание, что байт-коды похожи на язык, который мы «создали» в начале статьи: каждая строка представляет собой оператор с любым количеством операндов. В байт-коде PHP к переменным добавляется префикс !, поэтому !0 означает переменную 0. Байт-коду не важно, что вы используете переменную $a: в ходе компилирования имена переменных теряют значение и преобразуются в числа. Это облегчает и ускоряет их обработку виртуальной машиной. Большинство необходимых «проверок» выполняются на стадии компилирования, что также снимает нагрузку с виртуальной машины и увеличивает скорость её работы.

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

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

От исходного кода к байт-коду

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

Рассмотрим следующие PHP-выражения:

$a = 1;

$a=1;

$a
=
1;

Все они одинаково верны и должны быть преобразованы в одинаковые байт-коды. Но как мы их считываем? Ведь в нашем собственном интерпретаторе мы парсим команды, разделяя их пробелами. Это означает, что программист должен писать код в одном стиле, в отличие от PHP, где вы можете в одной строке использовать отступления или пробелы, скобки в одной строке или переносить на вторую строку и т. д. В первую очередь компилятор попытается преобразовать ваш исходный код в токены. Этот процесс называется лексингом (lexing) или токенизацией.

Лексинг

Токенизация (лексинг) заключается в преобразовании исходного PHP-кода — без понимания его значения — в длинный список токенов. Это сложный процесс, но в PHP вы можете довольно легко сделать нечто подобное. Представленный в листинге 2 код выдаёт следующий результат:

T_OPEN_TAG <?php
T_VARIABLE $a
T_WHITESPACE
=
T_WHITESPACE
T_LNUMBER 3
;
T_WHITESPACE
T_ECHO echo
T_WHITESPACE
T_CONSTANT_ENCAPSED_STRING "hello world"
;

Строковое значение преобразуется в токены:

  • <?php преобразован в токен T_OPEN_TAG,
  • $a преобразован в токен T_VARIABLE, который содержит значение $a.

Токенизатор знает об этом, когда при чтении кода обнаруживает знак $ с буквой a, после которых может следовать любое количество букв и цифр. Числа токенизируются в виде T_LNUMBER и могут быть одно- и более разрядными. Токенизация позволяет представить исходный код в более структурированном виде, не заставляя делать это самого программиста. Но, как уже упоминалось, токенизатор не понимает значение токенов. Он идеально токенизирует и $a = 1, и 1 = $a. А в следующей части мы научимся парсить — задавать значение потоку токенов.

Парсинг

При парсинге токенов мы должны следовать некоторым «правилам», составляющим наш язык. Например, может быть правило: первый обнаруженный токен в программе должен быть T_OPEN_TAG (соответствует <?php).

Ещё одно возможное правило: присваивание может состоять из любого T_VARIABLE, после которого идёт символ =, а затем T_LNUMBER, T_VARIABLE или T_CONSTANT_ENCAPSED_STRING. Иными словами, мы разрешаем $a = 1, или $a = $b, или $a = ‘foobar’, но не 1 = $a. Если парсер обнаруживает серию токенов, не удовлетворяющих какому-то из правил, автоматически будет выдана ошибка синтаксиса. В общем, парсинг — это процесс, определяющий язык и позволяющий нам создавать синтаксические правила.

Посмотреть список правил, используемых в PHP, можно по адресу. Если ваш PHP-скрипт удовлетворяет синтаксическим правилам, то проводятся дополнительные проверки, чтобы подтвердить, что синтаксис не только правильный, но и осмысленный: определение public abstract final final private class foo() {} может быть корректным, но не имеет смысла с точки зрения PHP. Токенизация и парсинг — хитрые процессы, и зачастую для их выполнения берут сторонние приложения. Нередко используются инструменты вроде flex и bison (в PHP тоже). Их можно рассматривать и в качестве транскомпиляторов: они преобразуют ваши правила в С-код, который будет автоматически компилироваться, когда вы компилируете PHP.

Парсеры и токенизаторы полезны и в других сферах. Например, они используются для парсинга SQL-выражений в базах данных, и на PHP также написано немало парсеров и токенизаторов. У объектно-реляционного маппера Doctrine есть свой парсер для DQL-выражений, а также «транскомпилятор» для преобразования DQL в SQL. Многие движки шаблонов, в том числе Twig, используют собственные токенизаторы и парсеры для «компилирования» файлов шаблонов обратно в PHP-скрипты. По сути, эти движки тоже транскомпиляторы!

Абстрактное синтаксическое дерево

После токенизации и парсинга нашего языка мы можем генерировать байт-код. Вплоть до PHP 5.6 он генерировался во время парсинга. Но привычнее было бы добавить в процесс отдельную стадию: пусть парсер генерирует не байт-код, а так называемое абстрактное синтаксическое дерево (Abstract Syntax Tree, AST). Это древовидная структура, в которой абстрактно представлена вся программа. AST не только упрощает генерирование байт-кода, но и позволяет нам вносить изменения в дерево, прежде чем оно будет преобразовано. Дерево всегда генерируется особым образом. Узел дерева, представляющий собой выражение if, обязательно имеет под собой три элемента:

  • первый содержит условие (вроде $a == true);
  • второй содержит выражения, которые должны быть выполнены, если соблюдается условие true;
  • третий содержит выражения, которые должны быть выполнены, если соблюдается условие false (выражение else).

Даже если else отсутствует, элемента три, просто третий будет пустым.

В результате мы можем «переписать» программу до того, как она будет преобразована в байт-код. Иногда это используется для оптимизации кода. Если мы обнаружим, что разработчик раз за разом перевычислял переменную внутри цикла, и мы знаем, что переменная всегда имеет одно и то же значение, то оптимизатор может переписать AST так, чтобы создать временную переменную, которую не нужно каждый раз вычислять заново. Дерево можно использовать для небольшой реорганизации кода, чтобы он работал быстрее: удалить ненужные переменные и т. п. Это не всегда возможно, но когда у нас есть дерево всей программы, то такие проверки и оптимизации выполнять куда легче. Внутри AST можно посмотреть, объявляются ли переменные до их использования или используется ли присваивание в условном блоке (if ($a = 1) {}). И при обнаружении потенциально ошибочных структур выдать предупреждение. С помощью дерева можно даже анализировать код с точки зрения информационной безопасности и предупреждать пользователей во время выполнения скрипта.

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

В PHP 7.0 появился новый движок парсинга (Zend 3.0), который тоже генерирует AST во время парсинга. Поскольку он достаточно свежий, с его помощью можно сделать не так много. Но сам факт его наличия означает, что мы можем ожидать появления в ближайшем будущем самых разных возможностей. Функция token_get_all() уже принимает новую, недокументированную константу TOKEN_PARSE, которая в будущем может использоваться для возвращения не только токенов, но и отпарсенного AST. Сторонние расширения вроде php-ast позволяют просматривать и редактировать дерево прямо в PHP. Полная переработка движка Zend и реализации AST откроет PHP для самых разных новых задач.

JIT

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

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

Мы получаем скорость компилируемого кода и наслаждаемся преимуществами кода интерпретируемого. Подобные системы могут работать быстрее обычного интерпретируемого байт-кода, иногда гораздо быстрее. Речь идёт о JIT-компиляторах (just-in-time, точно в срок). Название подходит как нельзя лучше. Система обнаруживает, какие части байт-кода могут быть хорошими кандидатами на компилирование в двоичный код, и делает это в тот момент, когда нужно выполнять эти самые части. То есть — точно в срок. Программа может стартовать немедленно, не нужно ждать завершения компилирования. В двоичный код преобразуются только самые эффективные части кода, так что процесс компилирования автоматизируется и ускоряется.

Хотя не все JIT-компиляторы работают таким образом. Некоторые компилируют все методы на лету; другие пытаются только определить, какие функции нужно скомпилировать на ранней стадии; третьи будут компилировать функции, если они вызываются два и больше раза. Но все JIT’ы используют один принцип: компилировать маленькие куски кода, когда они действительно нужны.

Ещё одно преимущество JIT’ов по сравнению с обычным компилированием заключается в том, что они способны лучше прогнозировать и оптимизировать на основании текущего состояния приложения. JIT’ы могут динамически анализировать код во время runtime и делать предположения, на которые неспособны обычные компиляторы. Ведь во время компиляции у нас нет информации о текущем состоянии программы, а JIT’ы компилируют на стадии выполнения.

Если вам доводилось работать с HHVM, то вы уже использовали JIT-компилятор: PHP-код (и надмножественный язык Hack) преобразуется в байт-код, запускаемый на виртуальной машине HHVM. Машина обнаруживает блоки, которые могут быть безопасно преобразованы в двоичный код; если это ещё не было сделано, она это делает и запускает их. По окончании запуска ВМ переходит к следующим байт-кодам, которые могут быть преобразованы в двоичный код.

PHP 7 не выполняется на JIT-компиляторе, но зато его новая система превосходит все предыдущие релизы. Сейчас во всех его компонентах проводятся эксперименты со статическим анализом, динамической оптимизацией, и даже есть простые JIT-системы. Так что не исключено, что однажды даже PHP 7 окажется позади!

  • Обердерфер перевод с немецкого
  • Обеспечивать соблюдение закона перевод на английский
  • Обенто перевод текста 8 класс
  • Обеспеченный перевод на татарский язык
  • Обелиск перевод на английский