Компілятор

Компілятор Код

Написаний вами код не запускається одразу, а потребує певної обробки перед виконанням?

Компілятор – це програма, яка перетворює вихідний код, написаний мовою програмування високого рівня, у машинний код, зрозумілий процесору.

У цій статті ми розглянемо, як працює компілятор, у чому його відмінність від інтерпретатора та дамо практичні поради для початківців.

Що таке компілятор?

Знайомимося з програмою, яка вміє створювати інші програми для того, щоб створювати ще більше програм.

Компілятор – це програма, яка переводить вихідний код мовою програмування в машинний код. Якщо цього не зробити, комп’ютер не зрозуміє, як виконати інструкції розробника. Тому ми віддаємо компілятору рядки коду, а він порівнює їх зі своїм словником, враховує контекст і видає набір із нулів та одиниць.

Для чого потрібен компілятор

Висунемо зухвале твердження: комп’ютери дуже дурні, вони не розуміють людської мови – і, зокрема, мов програмування. Усе, що вони вміють, – це приймати електричні сигнали і якось на них реагувати.

Якщо спрощувати, то комп’ютер – це коробка з мільярдами перемикачів. Смикнули одні – склали два числа, смикнули інші – записали дані на жорсткий диск. І хоча сучасні комп’ютери з апаратної точки зору влаштовані складніше, принцип залишається схожим.

Коли ми пишемо код, то використовуємо зрозумілі для людей слова, такі як print, string, import, Процедура і Виняток. Нам їхнє значення здається очевидним: тут вивели результат на друк, а там оголосили строкову змінну. Але для комп’ютера ці слова нічого не означають.

Комп’ютер бачить слово print і сприймає його так само, як ви сприймаєте слова з будь-якої невідомої вам мови. Нічого не зрозуміло, але якийсь сенс у них точно є. Тому комп’ютеру, як і нам, потрібен перекладач – або компілятор.

Компілятор розуміє, що означає слово print – і навіть вміє сказати комп’ютеру, як його правильно обробити. Таким чином, він вирішує три завдання:

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

На вхід компілятор приймає вихідний код, а віддає виконуваний файл – програму, яка готова до роботи.

Звучить просто. Але до компіляторів є багато запитань – наприклад, якими мовами їх пишуть, як вони влаштовані всередині та яких видів бувають. Про все це розповімо в статті. І почнемо з того, як працюють компілятори.

Як працюють компілятори

Отже, ще раз: щоб комп’ютери виконували команди програмістів, їм потрібні перекладачі з людської на машинну. Розглянемо процес перекладу – спочатку в загальних рисах, а потім детально.

Коротко

Компілятор отримує на вхід файл із кодом якоюсь мовою програмування. Він перетворює конструкції мови у формат, зрозумілий комп’ютеру, і повертає файл, який той зможе виконати.

Щоб перетворити вихідний код, компілятор використовує власний словник із визначеннями – наприклад, оператор if змінює на 11010011100110, а додавання – на 101011. Він робить це, поки не закінчаться всі рядки у файлі. Виходить виконавчий файл, який має такий вигляд.

У такому форматі комп’ютеру вже зручно читати інструкції та виконувати їх. А отже, компілятор зробив свою роботу добре.

Детальніше

Компілювання складається з п’яти етапів: синтаксичного аналізу, парсингу, семантичного аналізу, оптимізації та генерації коду. Давайте розберемо кожну стадію.

Синтаксичний аналіз. Це щось на зразок розбору граматики мови. Коли ми пишемо код, то дотримуємося певних правил – синтаксису. Наприклад, у Java між командами ставимо крапку з комою. Якщо цього не зробити, то отримаємо помилку.

На етапі синтаксичного аналізу компілятор перевіряє, чи відповідає код правилам конкретної мови програмування. І поки він не думає про те, що саме написано, – перевірка йде тільки за формальними ознаками.

Парсинг. На цьому етапі компілятор розбиває код на маленькі шматочки – токени. Кожен токен – це якесь слово або символ, наприклад if, while, int або (.

З токенів будується синтаксичне дерево, яке містить слова і символи, і стане в пригоді на наступному етапі – семантичному аналізі. Кожен вузол дерева – це або операція, наприклад додавання, або змінна. Зазвичай, коли ми доходимо до змінної, то далі гілки не розростаються.

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

Припустимо, у нас є простий код зі складанням двох чисел:

x = 5 + 3

Тут п’ять токенів: x, =, 5, + і 3. Пробіли рахувати не будемо.

Ми бачимо, що на вершині знаходиться головна операція – присвоювання змінній x результату додавання двох чисел. Від неї відходить дві гілки – сама змінна x і символ додавання, який розгалужується на доданки числа.

У процесі парсингу компілятор не розуміє, навіщо потрібен кожен із токенів. Поки що він машинально виконує свою роботу – думати буде на наступному етапі.

Семантичний аналіз. Компілятор починає вдумуватися в те, що написано в коді, аналізуючи складене синтаксичне дерево. Наприклад, якщо ми оголосили змінну, він розуміє, що це означає і які операції можна з нею виконати.

Ще компілятор на цьому етапі може припускати, які саме дії зі змінною можливі. Якщо він бачить, що у нас є змінна незмінюваного типу, наприклад константа, то під час спроби коду її змінити, видасть помилку.

Оптимізація. Коли синтаксис розібрано і стало зрозуміло, що робить програма, час прискорити роботу коду. Компілятор шукає способи підвищити швидкість його виконання або зменшити кількість займаної ним пам’яті.

Найпростіший приклад оптимізації – множення на нуль.

Щоб визначити значення змінної y, потрібно спочатку обчислити складну формулу для змінної x. Але ми, люди, одразу бачимо, що під час множення на нуль, результат буде нулем, а отже, сенсу рахувати змінну x немає. Компілятор теж бачить такі речі – і не буде обчислювати те, що обчислювати марно. Він просто замінить ці два рядки коду на один.

Зручно, правда? Але це спрацює тільки в тому разі, якщо змінна x не знадобиться нам у програмі далі.

Це можливо через особливості роботи компілятора – він не виконує код, а спочатку читає його і шукає способи оптимізації програми.

Генерація коду. Синтаксис розібрано, аналіз проведено, код оптимізовано – час перекласти його мовою комп’ютера. На цьому етапі всі команди, що ми писали мовою програмування, перекладаються в машинні інструкції.

Після перекладу ми отримуємо виконуваний файл, наприклад у форматі .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 та різну архітектуру процесорів.

Транспайлери

Перетворюють вихідний код мови високого рівня на вихідний код іншої мови високого рівня. Наприклад, транспайлер Babel перетворює ECMAScript 2015+ на JavaScript.

Зворотні компілятори

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

Різниця між компілятором, інтерпретатором та транслятором

Компілятори – це не єдиний спосіб перевести вихідний код у машинний. Ще є інтерпретатори та JIT-компілятори. Давайте коротко розповімо, у чому відмінності між ними.

Інтерпретатор. Це як синхронний перекладач. Він читає вихідний код і відразу ж виконує його порядково. Інтерпретатор не створює додаткових файлів і не будує синтаксичні дерева, а виконує інструкції на льоту, переводячи їх у байт-код. Наприклад, так працює CPython для мови Python.

JIT-компілятор. Це гібрид компілятора та інтерпретатора. Він починає працювати як інтерпретатор і виконує команди під час читання коду. Але частину команд переводить у машинний код, щоб використовувати їх у тих випадках, якщо вони повторюватимуться в майбутньому. Це прискорює роботу програми, оскільки дає змогу не виконувати одну й ту саму дію повторно.

Окремо варто згадати байт-код. Це спеціальний код, який запускається на віртуальній машині. Можна сказати, що він займає проміжне положення між кодом, написаним мовою програмування, і машинним кодом. Його реалізацію можна знайти в Java або Python.

Переваги та недоліки мов

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

Плюси:

  • Швидкість виконання. Компілятор переводить вихідний код у машинний лише один раз. А далі – все вже оптимізовано і готово до запуску. Тому такі програми працюють швидше, оскільки комп’ютеру не доводиться витрачати час на їх повторний переклад.
  • Ефективне використання ресурсів. Один з етапів компілювання – це оптимізація коду. А оскільки компілятори пишуть або творці мови, або досвідчені розробники, то продуктивність таких програм буде високою.
  • Приховування вихідного коду. Це неочевидний плюс, але це правда перевага. Після того як програма скомпільована, її вихідний код зрозуміти важко. Це допомагає уникнути зломів і убезпечити дані.

Мінуси:

  • Довга компіляція. Процес компіляції може займати дуже багато часу. Для невеликих проєктів це не так страшно, але коли кількість рядків коду у проєкту перевалює за мільйон, то зайвий раз запускати компіляцію не хочеться.
  • Складність виправлення помилок. Зазвичай помилки під час компілювання мають страхітливий вигляд через заплутаний опис проблеми. Просто спробуйте не поставити крапку з комою у файлі з C++ і переконайтеся, що нічого гіршого ви не бачили.
  • Залежність від платформи. Якщо скомпілювати програму для Windows, то її ніяк не можна буде запустити на macOS. Тому доведеться додатково брати інший компілятор і починати процес заново – або використовувати крос-компілятори.

Висновок

Отже, компілятор є ключовою ланкою між мовою програмування і машинним виконанням. Його роль полягає не лише в “перекладі” коду, а й у його оптимізації, перевірці на помилки та забезпеченні ефективної роботи програм.

Компільовані мови дають розробнику більше контролю над продуктивністю, хоча потребують ретельнішої підготовки. У підсумку, знання про компіляцію допомагає не лише писати код, а й усвідомлено підходити до розробки ефективних і надійних програм.

Павлов Максим

Founder & CEO Onpage School

Оцініть автора
Onpage School