Стендап Сьогодні

Що я зробив, що я хочу зробити, і що це все значить.
Повсякденні здобутки в форматі стендапу.
Детальніше в статті

Підписатись на RSS
📢 Канал в Telegram @stendap_sogodni
🦣 @stendap_sogodni@shevtsov.me в Федиверсі

08.03.2024

Прискорення 1BRC на Ruby

Ось мій наївний розвʼязок, давайте з ним щось зробимо. (Тільки вивід результатів тут поки примітивний, але на швидкодію він не впливає.)

  1. З таким кодом пошук міста відбувається щоразу для кожного поля. Натомість можна один раз знаходити дані для міста, та решту операцій робити вже над ними. Прискорення 14%

  2. Ані хеш, ані обʼєкт — не найшвидша структура даних в Ruby. Найшвидшою буде масив. Хеш з містами доведеться залишити, а от дані для кожного міста можна організувати в масив з 4 елементів. Прискорення 12% (А ще — так само швидко буде зі Struct, бо Struct всередині має масив. В реальному коді я б саме Struct і вжив, а в цьому вона заважала профайлеру, тож зробив з масивом.)

  3. Операція split в топі наших витрат. Вона буде швидше, якщо вказати кількість сегментів, які ми хочемо отримати. В цьому разі .split(';', 2) прискорює на неймовірні 10%.

  4. Замість явної перевірки на наявність даних в хеші можна створити хеш зі даними за замовчуванням. Дрібниці, але 1% прискорення це дає.

  5. Виявилося, що якщо потрібно взяти з масиву всі 4 значення, то швидше це робити з spread operator ніж по одному. Отримуємо ще 1% прискорення.

  6. Тепер, беремося за читання з файлу. По-перше, читання в UTF-8 (тобто за замовчуванням) буде уповільнено необхідною перевіркою на багатобайтові символи. Щоб знайти крапки з комою та нові рядки, нам це не потрібно. Перехід до двійкового кодування (або ж ASCII8) дає дуже приємні 7%.

  7. Нарешті, взагалі можна позбавитись розбиття рядка, якщо відразу читати окремо назву міста та температуру. Для того в метод readline можна передати символ “кінця рядка”, який цілком може бути крапкою з комою. Є тільки один нюанс: насправді readline повертає рядок з кінцевим символом. Поки що нам це не заважало тому, що операція to_f відкидає всі зайві символи. А що робити з зайвою крапкою з комою в кінці назви? Обрізати її вийшло надто дорого; тому я залишаю назви з зайвим символом, а обрізаю вже під час виводу. Такий підхід дає ще 9% прискорення.

  8. Що не дало ніяких результатів: вкладення перевірки на max в перевірку на min; заміна split на rindex; використання цілих чисел замість дробових. Взагалі, всякі нестандартні операції не окупають додаткових витрат — наприклад, на ручний розбір температури в ціле число. До того ж арифметика на дробових та цілих числах в Ruby практично однакова за швидкістю.

  9. Також нічого не дала явна буферизація файлу: ані з експериментальним класом IO::Buffer, ані з читанням великого (128Мб!) рядка в памʼять та подальшою обробкою через StringIO. Роблю висновок, що нормальне читання з файлу вже й так достатньо буферизоване.

  10. Остаточне покращення: з 10 хвилин до 5 з половиною. Непогано, хоча все ще в 3 рази повільніше ніж наївний розвʼязок на Go. Це повинно продемонструвати, що “Ruby повільна мова” та такі задачі, як 1BRC, краще відразу робити якоюсь іншою. Проте, з іншого боку, навіть таку щільну задачу можна простими способами прискорити у 2 рази — чого на практиці може бути достатньо.


07.03.2024

1 Billion Row Challenge... на Ruby?

До моєї уваги потрапив 1 Billion Row Challenge - нещодавнє мінізмагання з програмування, де потрібно якнайшвидше агрегувати мільярд рядків з інформацією. Хоча офіційно розвʼязок повинен бути на Java, проте вже зʼявилися й спроби на Go, Rust і так далі. Але на Ruby нічого немає. Мені стало цікаво — чому так та що з цього може вийти?

Почав з того, щоб зрозуміти межі можливого. Взяв версію на Go з цієї статті (бо Go мені більш зрозуміло ніж Java.) На моїй машині найшвидша версія без паралелізму займає близько 30 секунд.

Потім зробив тривіальну версію на Ruby (бо над розвʼязком задачі взагалі думати не треба — складність в оптимізації.) Вийшло 586 секунд. А тривіальний розвʼязок на Go - 105 секунд. Цифри хоч би однакового порядку, тобто можна й погратись. Можна обчислити, що гарним результатом для оптимізованого розвʼязку на Ruby буде десь 160 секунд.

Тепер — абсолютний мінімум. Просто підрахувати рядки в цьому файлі на Ruby займає 80 секунд. Я пішов ще далі: просто порахувати до мільярда - 23 секунди.

Моєю першою ідеєю було — замість ітерування рядками зробити такий собі побайтовий процесор зі скінченним автоматом. Мене чекала несподіванка — читання файлу байтами, а не рядками триває набагато довше - 350 секунд! (Може, ця ідея і вкотить на Go, потім спробую.)

Інтуїція, що читання байтами “простіше” та тому швидше, не спрацювала ось чому: в Ruby читання є функцією на С, причому досить складною, з купою перевірок. Робити їх на кожний байт дуже дорого.

Щоб поки зупинитись, зазначу: будь-який ефективний розвʼязок на Ruby буде використовувати більш високорівневі абстракції, де логіки менше в Ruby та більше в C. Можливо, StringScanner.


06.03.2024

Очікування та як з ним боротись

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

Що робити? Делегувати очікування комп’ютера. Шукати спосіб, як автоматично перевіряти готовність; якщо очікування потребує регулярних дій, то їх також автоматизувати. Тоді можна буде дійсно перемикнутися на іншу задачу та чекати сповіщення про готовність.

Наприклад: під час виправлення випадково невдалих тестів доведеться багато разів ті тести запускати, поки не вийде невдача. Подекуди десятки разів. Робити це вручну просто знущання з себе; кожні десять секунд натискати дві кнопки. А можна замість того запустити цикл та вийти з нього, коли зʼявиться помилка:

for i in {0..100}; do run_my_tests || break; done

Тривалі хмарні процеси — в першу чергу CI/CD - завжди кращі, коли по завершенню приходить сповіщення. Можна навіть автоматично перезапускати збірку, якщо це має сенс.

А якщо хочеться перейти на індустріальний рівень автоматичного очікування — звертаю увагу на Huginn.


05.03.2024

Традиційний застосунок — найкращий хобі-проєкт для вебінженера?

Якщо ти заробляєш веброзробкою, то голова генерує ідеї вебсайтів та вебзастосунків. Хочу звернути увагу, що, може, то не найкращий вибір для маленького проєкту, та краще спробувати себе в “традиційних”, тобто “локальних” застосунках. Для iPhone, для Android, для настільних ОС — взагалі, до того, чим користуєшся. Мої аргументи:


04.03.2024

Ще 10 днів зі стохастичним таймтрекером

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

По-перше, сам факт, що поки я продовжую заносити записи в трекер. Майже всі! Цьому сприяє легкість занесення — відкрити телефон та вказати пару тегів це справа на десять секунд. Плюс, про цей трекер не потрібно памʼятати — він сам нагадує. Нагадує нечасто, тому не дратує.

По-друге, такий метод таймтрекінгу точно віддає перевагу “великій картині”, а не дрібницям. Щоб тег засвітився в статистиці, він має трапитись багато разів. Тобто це ті активності, які повторюються кожен день. Та вони, абсолютно точно, є ключем до розуміння та покращення витрат власного часу.

Одне діло думати “я надто багато часу сиджу в телефоні”, та зовсім інше — бачити, що сидіння в телефоні у топ-10 активностей за тиждень (топ-1 виходить “сон”, до речі, а топ-2 “сидіння за компʼютером”… теж є над чим подумати.) Причому це більш точний показник ніж функція телефону “екранний час”, бо тег “в телефоні” значить, що я буквально в цей час дивився в телефон… відмазок немає.

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

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

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


03.03.2024

Особливості Swift Charts

Робота над стохастичним таймтрекером продовжується. Графіки та аналітика — це те, що наділяє такий продукт сенсом, тож я знов багато часу провожу за вивірянням коду графіків.

Swift Charts - трохи незвична для мене бібліотека (а звична то D3 або Highcharts). Вона є повністю декларативною та високорівневою, тому коригування вигляду відбувається високорівневими та абстрактними засобами.

Ну, наприклад: щоб спрямувати шкалу у зворотному напрямку, потрібно… передати їй обʼєкт “області визначення”, та в ньому вказати напрямок. А щоб шкала точно включала значення від 0 до 24, в цю область визначення додаються ці межі:

.chartYScale(domain: .automatic(reversed: true, dataType: Double.self) { domain in
  domain.append(0)
  domain.append(24)
})

Коли вже знаєш розвʼязок. то він ніби логічний. Проте щоб дійти його, доводиться “реверс-інжинірити” власні вимоги з простого візуала у бізнес-модель графіка. (Або, іншими словами, навчитися користуватися цією бібліотекою.)

Окрема задачка — це відображення графіка з розривами. Наприклад, якщо в деякий тиждень тег не був використаний, тобто даних немає — а на сусідні тижні є — то за замовчуванням Swift Charts просто інтерполює проміжок. Щоб того уникнути, я знаю тільки один спосіб: кожний проміжок без розривів має стати окремою серією. Серія є параметром для точки. Тож мусимо вручну визначити, що для нас є розривом, розбити дані на безперервні проміжки, призначити кожному проміжку ключ, та передавати цей ключ параметром серії. Це все виглядає набагато складніше, ніж має бути — проте працює.


02.03.2024

Любов до таблиць

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

Наприклад, потрібно мені було нещодавно залучити модуль golang.org/x/text/encoding для перекладу тексту з одного кодування в інше. Але ось в чому справа — цей модуль немає абстракції, де кодування визначається назвою. Натомість там є низка реалізацій типу Decoder для різних кодувань; виходить, що ми маємо наперед знати, яку саме Реалізацію взяти. А я знаю тільки назву кодування.

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

…Знаходжу в інтернеті гарний документ, близький до стандартного, з переліком кодувань та їх можливих назв (бо у назв є синоніми: наприклад, ISO-8859-1 та latin1.) Тут включається любов до таблиць, я забуваю про пріоритети та починаю перекладати документ в код. Весь. Хоча ні — цього разу помітив та зупинився, звідки й пост.

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

Так само мене спокушають переклади коду з мови на мову, інтеграції з API, специфікації форматів файлів… все, де можна відключити мозок та писати код рядок за рядком.


01.03.2024

Sync та Async: покрокове програмування

Натрапив на цікаве питання: чому ми кажемо синхронний код на той, що виконується послідовно, та асинхронний — якщо рівночасно? Ніби ж навпаки, “синхронно” має значити “в один час”, тобто виклик, наприклад, Promise простою мовою скоріше “синхронний” ніж “асинхронний”..? Спробую пояснити.

Може, я переграв у Baldur’s Gate 3, але виконання рівночасних програм нагадує покрокову гру. Програма складається з послідовних блоків, які виконуються по черзі. (Принаймні, абстракція така — що там насправді відбувається, нам невідомо — процесор, ОС, та середовище виконання всі накладають свої нюанси.)

Як часто ти думаєш про час при написанні? Не час як момент історії, а як розклад виконання. Напевно, при синхронному програмуванні — взагалі ніколи. Хоч кожна інструкція неодмінно займає реальний час. Ми пишемо синхронні програми так, ніби час зупинився. І це просто чудово, бо звільняє нас від всіх труднощів синхронізації (наша програма вже синхронна, вона не потребує синхронізації, тобто злагодження в часі.) Тільки асинхронне програмування змушує нас згадати, що час існує — реальний час, з затримками та чергуванням, а не просто послідовність дій.

…Так само в покроковій грі, зміст кроку відбувається послідовно, але без обігу реального часу поза діями поточного гравця, тобто синхронно. Має сенс?


29.02.2024

Майже порожній аркуш

Широко відома проблема порожнього аркуша в дизайні інтерфейсів — тобто, що робити з застосунком за браком даних, на початку роботи. Проте, поки даних зовсім немає, зазвичай зрозуміло, чим їх замінити. Картинка з прикладом заповнення, відеопосібник, просто текст — за прикладами далеко ходити не треба.

Помітив, що існує й друга, більш підступна проблема. В деяких інтерфейсах одного чи двох початкових записів не вистачає для повноцінної роботи. Наприклад, я зіткнувся з цим у WeightPlot: щоб побудувати графік ваги, потрібно хоч тиждень даних. Але що показувати користувачу весь цей перший тиждень?

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

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

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


28.02.2024

Водяне охолодження — керування

🎛️ Як я згадував в пості про компоненти, в системи охолодження немає власного керування. Всі активні компоненти — помпа та вентилятори на радіаторі — керуються звичайним PWM з самого компʼютера.

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

Мета керування схожа на повітряне охолодження: збільшувати швидкість моторів відповідно до температури, щоб нічого не перегрівалось. Проте з повітрям ми зазвичай маємо один вентилятор на один датчик… а тут датчиків два. Якщо процесор або відеокарта нагріваються — маємо прискорити помпу. Такого мій BIOS робити не вміє — та й не знаю, який вміє, якщо врахувати, що датчик відеокарти знаходиться не на материнській платі.

Щоб розвʼязати цю головоломку, знайшов надзвичайну програму FanControl. Вона вміє встановити швидкість вентиляторів як функцію комбінації датчиків. Замість простої кривої, як в BIOS, тут справжній ЦОС. Єдине, що FanControl запускається вже з Windows, а до того в BIOS виставлено вмикати помпу на безпечно високу швидкість.

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