Вузькі місця інтерпретаторів

Вузькі місця інтерпретаторів

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

У наш час, у зв'язку з потенційно не поганими зарплатами та офісного типу роботою, програмування стало досить популярним серед молоді. До того ж попитом користуються досить не складні для первісного освоєння мови програмування: JavaScript, PHP, Perl, Python, Java, C#, Basic,… (як видно всі вони одного сімейства - інтерпретатори). В результаті з'явилася досить велика кількість працівників цієї галузі, які спеціально програмуванню ніде не навчалися. Був потрібен програміст на мову «X», купили книгу «X за 2 тижні» і через 3 тижні - ми вже пишемо якийсь проект на «X». А через кілька тисяч рядків коду або після того, як база даних обросла реальними даними, проект починає нещадно гальмувати. Можна, звичайно, «піти пограти на барабанах», поки залізо доросте до вашого проекту, але не завжди і не всіх цей варіант влаштовує.

У чому ж зазвичай основна проблема? Зазвичай у відсутності розуміння: що насправді відбувається при виконанні команди «Y». Мови програмування як мови спілкування - одне і те ж можна пояснити різними словами. Але у випадку з комп'ютерами, буде краще, якщо пояснення буде максимально лаконічним. «Краще» - в сенсі «швидкості виконання». Причому лаконічність повинна бути на рівні мови, зрозумілої центральному процесору, а не вам. Я маю на увазі, що короткість назви функції, яку ви викликаєте в тій чи іншій мові програмування, не впливає на продуктивність (є винятки); на продуктивність впливає те, що творить ця функція у своїх надрах. А для цього варто розуміти, як насправді комп'ютер працює з числами, рядками, масивами, функціями і так далі.

Щоб ця замітка не надто розросталася, я не буду тут описувати, що і як працює на низькому рівні. У деяких мовах це пояснення може відрізнятися. Детальну інформацію можна пошукати в інтернеті, книгах або розібратися самому. При бажанні можу відповісти на запитання в коментарях і зібрати їх всі в окрему замітку. А поки поясню тільки «звідки ноги ростуть» у поширених упущеннях і на що варто звернути увагу.

Для початку давайте розберемося в класах мов програмування. Я б їх розбив на 3 групи (можливо, вони так і розбиваються):

  • Ассемблер
  • Компілятори
  • Інтерпретатори

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

Компілятори - це більш дружні програмісту мови: C, C++, Pascal,… У них набагато простіше що-небудь писати за рахунок того, що такі речі як умовні переходи, цикли, робота зі змінними і функціями виведені в синтаксис мови. В результаті чого більше не потрібно писати безліч команд центрального процесора, для реалізації складного циклу. Плюс до всього, в них реалізовані різні конструкції, про які ЦПУ (центральний процесорний пристрій) взагалі не в курсі. Але які суттєво полегшують структурувати логіку роботи програми (класи, об'єкти, записи, масиви,...). При компілюванні програма перекладається на мову, зрозумілу конкретному ЦПУ. Так як Ассемблер і мова, зрозуміла ЦПУ, по суті одне і те ж, ви завжди можете перекласти відкомпільовану програму на Ассемблер (дизасемблювання). Перекласти відкомпільовану програму на компільовану мову - значно складніше завдання, оскільки деякі конструкції мови процесора не завжди можна нормально перенести в спрощений синтаксис компільованих мов. До того ж всі найменування змінних і функцій при компіляції втрачаються, і відновити їх не представляється можливим (за винятком, коли програма відкомпільована в режимі зневадження).

Інтерпретатори - ці мови - це вищий щабель еволюції: JavaScript, PHP, Perl, Python, Java, C#, Basic… Їх особливість полягає в потенційній незалежності від платформи виконання додатку.

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

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

Програм же, які були написані інтерпретованими мовами, виконуються якоюсь програмою-прошарком, яка в реальному часі читає ваш код і перекладає його на мову, зрозумілу виконуваним ЦПУ. У результаті питання переносимості вашого додатку розробники інтерпретаторів взяли на себе. Тепер їм доводиться виготовляти цей прошарок для різних систем, щоб ваша програма працювала на них усіх. Але оскільки всі системи досить відрізняються, реалізувати абсолютну незалежність не завжди вдається. Якщо під Linux є функція «Z», а під Windows її немає, то вам доведеться або обійтися без неї, або ваша програма буде працювати тільки під Linux (наприклад, функції роботи з файловою системою).

Основний недолік інтерпретованих мов - це швидкість їх виконання. Цілком очевидно, що програма, відкомпільована на мову, зрозумілому ЦПУ, при виконанні відразу обробляється ЦПУ, в той час як програма, написана інтерпретованою мовою, спочатку повинна бути розпізнана і перекладена на мову зрозумілу ЦПУ, і тільки потім ЦПУ починає її виконувати. Сучасні інтерпретатори обзавелися низкою заходів боротьби з цим недоліком. Крім досить якісного оптимізатора і системи кешування, вони переводять вашу програму в байт-код (або в реальному часі, або імітуючи компіляцію). Тепер програмі-прошарку не потрібно щоразу розпізнавати ваш «рукописний текст». Вона це робить або тільки 1 раз, або взагалі не робить (якщо програма вже була переведена в байт-код). Замість вашого «рукопису», вона працює з байт-кодом вашої програми. Байт-код дуже схожий на мову ЦПУ, але це не мова ЦПУ (вона більш платформо-незалежна). Його все ще необхідно перекладати мовою ЦПУ. Тому очевидно, що чутки про Java, яка працює швидше C++, помітно перебільшені. І це залишиться так, поки процесори не навчаться розуміти байт-код Java.

Тепер, після невеликого загального опису роботи інтерпретаторів, я б хотів відзначити 3 теми, які можна пропустити при написанні невеликого проекту, але які, часом, можуть дати істотний приріст продуктивності при їх розумінні і правильному використанні.

Функції мови, які вже відкомпільовані

Мови програмування - це не тільки синтаксис. Це ще й набір готових бібліотек функцій для роботи з різними даними та пристроями. У компільованих мовах вони особливо нічим не відрізняються, а ось в інтерпретаторах є різниця. У деяких мовах, таких як Java, ці функції написані мовою самого інтерпретатора. А в деяких, таких як JavaScript або PHP, на компільованих мовах, тобто вони, на момент виконання програми, вже відкомпільовані і не потребують додатково обробки. Таким чином, їх виклик не буде вимагати якихось додаткових обробок, в результаті чого їх виконання буде значно більш швидким, ніж якщо ви напишете теж на цій інтерпретованій мові. Тому якщо у вас є можливість виконати такого роду вбудовану функцію, нехай навіть вона робить щось зайве, але вирішує ваше завдання, спробуйте скористатися нею, замість того, щоб писати свої складні або не дуже конструкції. Наприклад, для розбиття рядка на набір підрядків зі складною умовою краще скористатися регулярним виразом, ніж писати свій цикл з ручною обробкою цього ж рядка.

Складні за структурою, але легкі у використанні, фреймворки

Крім набору бібліотек функцій деякі ентузіасти намагаються ще й перетворити синтаксис і логіку мов, привносячи якісь свої ідеї, які щось спрощують при роботі зі структурами і/або даними (jQuery, LINQ, ORM,...). Якщо мова компільована, то це не так страшно. А ось в інтерпретаторах сліпе занурення в сторонні абстрактні функції згубно. Так, часто з такими перетворювачами дійсно зручніше, але ця зручність майже завжди досягається за рахунок швидкості роботи. Досить поглянути у вихідний код цих «помічників» і переконатися, що часом набагато ефективніше викликати пару вбудованих в мову функцій, що виконують конкретно те, що вам потрібно, ніж одну універсальну сторонню, яка всередині виконає «тонну» коду, перш ніж зрозуміє, що ж ви від неї хочете і, нарешті, зробить це. Наприклад, в JavaScript'e для отримання всіх DIV'ів, ви можете безпосередньо викликати вбудовану функцію "document.getElementsByTagName (" DIV ")", яка відразу поверне вам що треба, або викликати красиву функція jQuery "$ (" DIV ")", яка виконає кілька формованих виразів, виразних виразів, формальних виразів, формальних виразів, формальних виразів, формальних виразів ".

Робота з рядками

І, нарешті, останнє, на чому я хотів приділити вашу увагу - це робота з рядками. В інтерпретованих мовах робота з рядками стала настільки прозорою, що той факт, що це одні з найбільш ресурсо-витратних операцій, абсолютно не очевидний. Цей факт відомий, зазвичай, тільки тим, хто працював з ними хоча б у компільованих мовах вручну (там теж є функції, що полегшує цю роботу). Проблема в тому, що при майже будь-якій операції з рядками (створення рядка, об'єднання рядків, розбиття на підрядки, видалення підрядка, заміна підрядка), включається пошук вільного місця в пам'яті, необхідної довгості, під новий рядок, і копіювання результуючих даних в нове місце. Навіть такі прості, з першого погляду, операції як пошук по рядку, з приходом таких складних форматів як UTF-8, не є особливо швидкими. Порівняно з роботою у форматі ASCII. Тому не слід зловживати рядками, де можна обійтися без них. Наприклад, асоціативні масиви - якщо є можливість обійтися нумерованим масивом, обійдіться!

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

Успіхів!