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

🤖🚫 Контент вільний від AI. Цей пост на 100% написаний людиною, як і все на моєму блозі. Насолоджуйтесь!

09.03.2024

1BRC: чому Ruby повільний

Таке просте (не легке!) завдання, як 1 Billion Row Challenge, дає нагоду роздивитися причини повільності мови. Однак ruby-prof приховує їх — бо більшість часу цей скрипт витрачає не в Ruby. Тому взяв в руки профайлер коду C та пішов глибше.

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

Є один очевидний спосіб уникнути цього: обробляти рядок на місці байт за байтом. Тут зʼявляється друга причина — цикли в Ruby мають ненульову ціну. Робити ітерацію на кожний рядок — нормально. А на кожний байт — тобто на порядок більше ітерацій — вже стає найдорожчою частиною програми.

Нарешті, я знайшов як працювати з рядками без виділення памʼяті. А саме: операцією String#byteslice, яка копіює байти з рядка в рядок. Якщо рядок-отримувач має достатньо місткості, памʼять виділятися не буде. (Тут варто знати, що в рядків в Ruby є довжина, а є ще місткість, тобто розмір вже виділеної памʼяті.)

Додати цю операцію до StringScanner#skip_until та IO#readpartial та отримуємо сканування файлу абсолютно без виділення памʼяті! От тільки код стає дуже плутаним (ось він) та — головне — працює все одно повільніше за вчорашній розвʼязок через кількість необхідних додаткових операцій.

Отже, нічого нового: Ruby непрактично повільний на циклах з мільярдів ітерацій, або там, де потрібні мільярди виділень памʼяті. Якщо проєкт такого потребує — пора перейти на низькорівневу мову.