Стендап Сьогодні 📢 Канал в Telegram @stendap_sogodni

🤖🚫 AI-free content. This post is 100% written by a human, as is everything on my blog. Enjoy!

Пости з тегом #Ruby

31.03.2023

Як я зробив css_parser у 3 рази швидше

Є така хороша бібліотека для Ruby - css_parser. З 88 мільйонами завантажень, це вірний вибір для розбору CSS з середовища Ruby. Зазвичай вона працює добре, але ми виявили, що не для будь-яких документів. На деяких вхідних документах швидкодія падала безнадійно. Ну як — безнадійно? Насправді надія є, можна взяти та зʼясувати, що не так — адже код відкритий.

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

Власне, головна проблема оригінального коду була в використанні регулярних виразів. В більшості випадків вони працювали нормально; але якщо тільки в документі присутня велика кількість пробілів, як інколи трапляється, то маємо вищезгадане уповільнення — замість частки секунди розбір тривав хвилини.

Але регулярка не робила нічого магічного; вона розділяла вираз CSS на властивості — крапкою з комою, а потім на імʼя та значення — двокрапкою. Це досить легко зробити за допомогою простіших методів String#split та String#index.

Далі за допомогою профайлера RubyProf та його режиму відстеження виділення памʼяті я переробив код циклу так, щоб уникнути виділення памʼяті повністю. (Щоб побачити, що проблема в регулярці, теж був потрібен RubyProf, але в більш звичному режимі відстеження часу виконання.)

Ці зміни не тільки покращили швидкість розбору в 3.5 рази та використання памʼяті в 3 рази, але й уникнули тих випадкових проблем з регулярками, з яких все починалось. Як бачите, проблема була не в тому, що Ruby - повільна мова, а в тому, що підхід обраний неоптимальний. Регулярні вирази - потужний інструмент, але ж потужність приходить з ціною; для простих потреб треба брати прості, явні операції на рядках.


12.09.2023

Використання StringScanner для прискорення парсера на Ruby

Вже писав, як я прискорив бібліотеку для Ruby css_parser прибиранням регулярних виразів. Проте це було не єдине покращення того сезону. Нещодавня стаття Аарона Патерсона про клас StringScanner нагадала, що я теж залучав StringScanner, щоб прискорити парсер на 80%.

Зміни можна подивитись у цьому коміті. Як бачиш, вони мінімальні: просто замість методу String#scan використовую клас StringScanner. Цей клас існує ще з Ruby 1.8 - саме час його вивчити. :)

Бо мій канал не для того, щоб просто сказати, який клас хороший. Розберімося, чому. Перший ключ в тому, що він цілком написаний на C. Проте так само і String#scan. Взагалі в Ruby багато низькорівневих методів реалізовані на C, що трохи розвіює міф про те, що Ruby обовʼязково має бути повільніше за мови, що компілюються.

На жаль, профілювання коду Ruby не пояснить нам різницю між цими реалізаціями. Зате ось gist з бенчмарком, який показує, що на тривіальному прикладі StringScanner навіть на 170% швидше. Тобто різниця полягає не в виконанні регулярного виразу (тут тривіалізованому), а в тому, що відбувається окрім нього. Єдине, що обидва методи роблять окрім регулярки — це повертають знайдений рядок.

Я здогадуюсь, що різниця у виділенні памʼяті. Якщо пропустити мій бенчмарк через benchmark-memory, то виявиться, що String#scan виділяє в 5 разів більше памʼяті. Здається (без подальшого профілювання), що String#scan (тобто rb_str_scan) містить більш узагальнений код, а StringScanner#scan (тобто strscan_scan) - зоптимізований рівно під повертання результату з початкової позиції. (Так, до речі, є різниця в тому, що StringScanner шукає тільки на початку рядка — але зазвичай це саме те, що нам потрібно.)

Висновок тут, як і в попередній історії, такий, що виділення памʼяті всередині циклу може бути вузьким місцем та впливати не тільки на витрати ОЗУ, але також й на швидкість виконання.


29.07.2024

Покращення до css_parser - тепер в PR

Нарешті знайшов час зробити два PR до бібліотеки css_parser для Ruby, про які я писав рік тому, а зробив ще у 2021-му. Кілька спостережень.


15.01.2025

Чому в Rails крута модель

Вчорашній пост змусив мене знову подумати, чому ж в Ruby on Rails з моделлю працювати зручніше, ніж будь-де. ActiveRecord залишається неперевершеним в простоті використання, навіть для побудови складних та потужних бізнес-процесів.

Отже. В Ruby немає різниці між методами та атрибутами, а точніше, весь публічний інтерфейс обʼєктів є виключно методами. Це приховує силу-силенну складності — аспекти реалізації не обтяжують споживача. Наприклад, User.all.map(&:name) елегантно та непомітно залізе в базу. (Але краще User.pluck(:name), що відразу витягне масив імен.) Фактично в Rails база виглядає, як “внутрішня”, а не “зовнішня” структура даних. Інколи це підштовхує писати неоптимальний код — але, як каже Роб Пайк, не варто оптимізувати код наперед.

Ніякі класи не потребують явно вказаного підключення до бази чи інших параметрів. Практично в будь-якому іншому середовищі використовується Dependency Injection - принаймні для того, щоб класи можна було тестувати без справжньої бази тощо. В Rails всі залежності приховані, а нам ніколи не доводиться про них думати.

Можна подумати, що це робить код Rails неможливим для перевірки. Але, в Ruby весь код живе у спільному просторі імен та доступний для змін під час виконання. Тому практично кожний метод, змінну чи клас можна замінити на тестову копію. Це насправді дуже потужно, та ми постійно цим користуємося. Для прикладу, можна навіть змінювати константи.

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