Стендап Сьогодні 📢 Канал в Telegram @stendap_sogodni
🤖🚫 AI-free content. This post is 100% written by a human, as is everything on my blog. Enjoy!09.03.2024
1BRC: чому Ruby повільний
Таке просте (не легке!) завдання, як 1 Billion Row Challenge, дає нагоду роздивитися причини повільності мови. Однак ruby-prof
приховує їх — бо більшість часу цей скрипт витрачає не в Ruby. Тому взяв в руки профайлер коду C та пішов глибше.
Виходить, що переважну більшість часу збігає на виділення (та вивільнення) памʼяті. Виявилося, що в операціях над рядками дуже важко уникнути виділення памʼяті практично на кожному кроці. Там, де в низькорівневій мові можна створити сталий буфер та використовувати його повторно, в Ruby на кожне читання з файлу утворюється новий. Навіть якщо переписати алгоритм з буферизацією — все одно на витягання кожного елемента потрібні нові рядки.
Є один очевидний спосіб уникнути цього: обробляти рядок на місці байт за байтом. Тут зʼявляється друга причина — цикли в Ruby мають ненульову ціну. Робити ітерацію на кожний рядок — нормально. А на кожний байт — тобто на порядок більше ітерацій — вже стає найдорожчою частиною програми.
Нарешті, я знайшов як працювати з рядками без виділення памʼяті. А саме: операцією String#byteslice, яка копіює байти з рядка в рядок. Якщо рядок-отримувач має достатньо місткості, памʼять виділятися не буде. (Тут варто знати, що в рядків в Ruby є довжина, а є ще місткість, тобто розмір вже виділеної памʼяті.)
Додати цю операцію до StringScanner#skip_until та IO#readpartial та отримуємо сканування файлу абсолютно без виділення памʼяті! От тільки код стає дуже плутаним (ось він) та — головне — працює все одно повільніше за вчорашній розвʼязок через кількість необхідних додаткових операцій.
Отже, нічого нового: Ruby непрактично повільний на циклах з мільярдів ітерацій, або там, де потрібні мільярди виділень памʼяті. Якщо проєкт такого потребує — пора перейти на низькорівневу мову.