Написаний вами код не запускається одразу, а потребує певної обробки перед виконанням?
Компілятор – це програма, яка перетворює вихідний код, написаний мовою програмування високого рівня, у машинний код, зрозумілий процесору.
У цій статті ми розглянемо, як працює компілятор, у чому його відмінність від інтерпретатора та дамо практичні поради для початківців.
Що таке компілятор?
Знайомимося з програмою, яка вміє створювати інші програми для того, щоб створювати ще більше програм.
Компілятор – це програма, яка переводить вихідний код мовою програмування в машинний код. Якщо цього не зробити, комп’ютер не зрозуміє, як виконати інструкції розробника. Тому ми віддаємо компілятору рядки коду, а він порівнює їх зі своїм словником, враховує контекст і видає набір із нулів та одиниць.
Для чого потрібен компілятор
Висунемо зухвале твердження: комп’ютери дуже дурні, вони не розуміють людської мови – і, зокрема, мов програмування. Усе, що вони вміють, – це приймати електричні сигнали і якось на них реагувати.
Якщо спрощувати, то комп’ютер – це коробка з мільярдами перемикачів. Смикнули одні – склали два числа, смикнули інші – записали дані на жорсткий диск. І хоча сучасні комп’ютери з апаратної точки зору влаштовані складніше, принцип залишається схожим.
Коли ми пишемо код, то використовуємо зрозумілі для людей слова, такі як 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. Тому доведеться додатково брати інший компілятор і починати процес заново – або використовувати крос-компілятори.
Висновок
Отже, компілятор є ключовою ланкою між мовою програмування і машинним виконанням. Його роль полягає не лише в “перекладі” коду, а й у його оптимізації, перевірці на помилки та забезпеченні ефективної роботи програм.
Компільовані мови дають розробнику більше контролю над продуктивністю, хоча потребують ретельнішої підготовки. У підсумку, знання про компіляцію допомагає не лише писати код, а й усвідомлено підходити до розробки ефективних і надійних програм.








