У чому секрет швидкості NodeJS?

Пропонуємо вам переклад статті Євгена Обрезкова, в якій він коротко і по справі розповідає про причини високої швидкості NodeJS: потоки, event loop, оптимізуючий компілятор і, звичайно ж, порівняння з PHP. Куди вже без нього.



В черговий статтею про NodeJS хочу поговорити про ще одну перевагу програмної платформи: про швидкість виконання коду.

Що ми маємо на увазі під швидкістю виконання
Обчислити послідовність Фибоначи або надіслати запит до бази даних?

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

Як тільки ви зрозумієте, що відбувається в сервері NodeJS під час виконання запиту, вам стане ясно, чому це відбувається так швидко.

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

Від чого страждає PHP
Ось список того, що зменшує швидкість виконання коду PHP:

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

Тепер ми подивимося, як NodeJS справляється з подібними завданнями.

Магія NodeJS
NodeJS однопоточна і асинхронна. Будь-яка операція вводу-виводу не блокує роботу. Це означає, що ти можеш читати файли, відправляти електронні листи, запитувати базу даних та вчиняти інші дії… одночасно.

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

Віртуальна машина в NodeJS (V8), яка виконує JavaScript, має JIT компіляцію. Коли віртуальна машина отримує вихідний код, вона може скомпілювати його прямо під час роботи. Це означає, що операції, які викликаються часто, можуть бути скомпільовані в машинний код. І це значно поліпшить швидкість виконання.

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

Розумійте вашу асинхронність
Вашій увазі пропоную приклад концепції асинхронної обробки (спасибі Кирилу Яковенко).

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

Синхронне виконання означає для вас втрату часу. Ви чекаєте, коли куля виявиться біля основи.

Асинхронне виконання схоже на те, що у вас з'являється 1000 додаткових рук. І ви можете запустити всі кулі одночасно. Після чого чекаєте тільки повідомлення про те, що всі вони внизу, і збираєте результати.

Як асинхронне виконання допомагає веб-сервісу працювати?

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

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

Як асинхронний спосіб реалізований в NodeJS?

Event Loop
Event loop – це конструкція, яка відповідальна за обробку подій у якійсь програмі. Event loop майже завжди працює асинхронно щодо джерела повідомлень. Коли ви викликаєте операцію вводу-виводу, NodeJS зберігає коллбек, пов'язаний з цією операцією, і продовжує обробку інших подій. Коллбек буде викликаний, коли всі необхідні дані будуть отримані.

Найбільш розгорнуте визначення event loop:

Event loop, message dispatcher, message loop, message pump або run loop – це програмна конструкція, яка чекає і обробляє події або повідомлення в програмі. Конструкція працює шляхом створення запитів до внутрішньої або зовнішньої службі доставки повідомлень (яка зазвичай блокує запит до тих пір, поки повідомлення не отримано), після чого вона викликає обробник відповідної події («обробляє подію»). Event loop може бути використана в зв'язці з reactor, якщо джерело подій має такий же інтерфейс, як і файли, до якого можна зробити запит виду select або poll (poll в сенсі Unix system call). Event loop майже завжди працює асинхронно по відношенню до джерела повідомлень.

Давайте подивимося на просту ілюстрацію, яка пояснює, як event loop працює в NodeJS.



Event Loop у NodeJS
Коли веб-сервіс отримує запит, той відправляється в event loop. Event loop реєструє операцію в пулі потоків з потрібним коллбеком. Коллбек буде викликаний, коли обробка запиту завершиться. Ваш коллбек може також робити інші «важкі» операції, такі як запити до бази даних. Але робить це таким же способом – реєструє операцію в пулі потоків з потрібним коллбеком.

Як щодо виконання коду і його швидкості? Ми збираємося поговорити про віртуальній машині і про те, як вона виконує JavaScript код. Тобто про V8.

V8 оптимізує ваш код?
Wingolog описано, як працює віртуальна машина V8. Я спростив викладений там матеріал і пропоную вижимки.

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

V8 має три типу компілятора, але обговоримо тільки два: Full і Crankshaft (третій компілятор називається Turbofun).

Full-компілятор працює швидко і виробляє «типовий код». У функції Javascript він бере AST (Abstract Syntax Tree) і переводить його у типовій нативний код. На цьому етапі застосовується тільки одна оптимізація – інлайн кешування.

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

Після того, як V8 визначила, які функції використовуються часто, і отримала звіт про використання типів, вона намагається запустити модифікований AST через оптимізуючий компілятор – Crankshaft.

На відміну від Full-компілятора, Crunshaft працює не так швидко, але намагається виробляти оптимізований код. Cranshaft складається з двох компонентів: Hydrogen і Lithium.

Hydrogen-компілятор створює CFG (Flow Control Graph з AST (на підставі звіту про використання типів). Цей граф представлений у формі SSA (Static Single Assignment). На підставі простої структури ХІР (High-Level Intermediate Representation) і форми SSA, компілятор може застосовувати багато оптимізацій, таких як constant folding, method inlining, і так далі…

Lithium-компиллятор переводить оптимізований ХІР в LIR (Low-Level Intermediate Representation). LIR концептуально схожий на машинний код, але в більшості випадків не залежить від платформи. В протилежність ХІР, форма LIR — ближче до three-address кодом.

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

Корисні посилання
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.