Стендап Сьогодні
Що я зробив, що я хочу зробити, і що це все значить.
Повсякденні здобутки в форматі стендапу.
Детальніше в статті
Підписатись на RSS
📢
Канал в Telegram @stendap_sogodni
🦣
@stendap_sogodni@shevtsov.me в Федиверсі
03.03.2023
Витрати памʼяті Ruby та Go, продовження: структури
Продовжую вчорашні тести. Доробив код, зробив зручну функцію для вимірювання, щоб прибрати копіпасту. На Go згодилися узагальнені типи; безумовно, корисна можливість, та ще й така що не погіршує швидкодію програми — про це далі. Сьогодні роздивимось структури. За тестовий приклад я обрав структуру з трьох чисельних значень; тоді розмір самих значень відомий - 24 байти, та можна підрахувати накладні витрати від самого механізму структури.
Починаючи з Ruby, найбільш очевидним здається клас Struct. Тут на кожний атрибут додаткові витрати ставлять 22 байти. Такі самі витрати будуть, якщо створити власний клас, без Struct. Тобто схоже, що Struct не оптимізує, а тільки спрощує код.
Хеш інтуїтивно буде менш ефективним. Тест це підтверджує: тут, на перший погляд, витрати аж в 3 рази більше. Але ось що цікаво: той самий хеш розміром у 224 байти спроможний зберігати до 8 значень без додаткового виділення памʼяті!. Схоже на те, що памʼять виділяється блоками, бо з девʼятого значення використання стрибає у 2 рази. Виходить, що для маленьких структур краще клас, а для великих можна не перейматись, бо хеш не додає зайвих витрат. Також, хеш, де ключі — це рядкові константи, важить так само як і хеш з символами. Ось скільки дивних нюансів такого простого, на перший погляд, типу.
Як щодо Go, то тут структури вбудовані в мову, та в традиції системних мов програмування, звичайна структура не створює жодних додаткових витрат — займає рівно стільки, скільки значення, які вона містить. Сподіваюсь, це пояснює, чому для потреб, де памʼять критична, беруть Go, а не Ruby.
Вказівник на структуру додасть свої очевидні 8 байтів. Більш цікаво, що перетворення типу в interface{} додає 9 байтів — тобто якщо використати конкретні типи, то вони не мають додаткових витрат, а інтерфейсний тип — має. Що підкреслює цінність впровадження узагальнених типів, бо як раз узагальнені типи працюють так само як еквівалентний код, де всі типи підставлені вручну.
Про асоціативні масиви в Go коротко скажу, що вони дуже схожі на хеші в Ruby. Таке саме виділення памʼяті блоками, та приблизно такі самі витрати на елемент — десь 24 байти. Різниця тільки в тому, що в Go структури набагато вигідніше в порівнянні. Наприклад, якщо використати тип map[string]interface{} для лінивої обробки JSON, то платити доведеться не тільки за масив, а ще й за інтерфейсний тип. Так що інтуїція по Go - якщо форма структури відома заздалегідь, завжди треба брати структури, а якщо ні — тільки тоді масиви.
Завтра — все то саме, тільки для JavaScript.
02.03.2023
Порівняння витрат памʼяті Ruby та Go на масиви
Хотів написати пост про те, як Go краще витрачає памʼять, ніж Ruby, та ще й з прикладами. Але вийшла несподіванка — не такий вже Ruby й поганий! На масив чисел якимсь чином витрачає десь стільки ж памʼяті (на 2% менше навіть!), а от на масив рядків вже на 50% більше, що більш зрозуміло. Код прикладів у gist. Тут треба більше розбиратись.
По-перше, що вимірювати як витрати пам’яті. На мою думку, краще за все дивитись на загальний розмір памʼяті, виділений процесом, тобто RSS - Resident Set Size. Ані в Ruby, ані в Go немає вбудованих засобів дізнатись цей розмір, але його можна легко отримати командою оболонки ps -o rss $PID_OF_PROGRAM. Корисно, що цей показник не спирається на особливості мови чи платформи, тому таке порівняння більш справедливе. Залишається тільки виділяти достатньо памʼяті. Та про всяк випадок викликати збирач сміття перед вимірюваннями; хоча в такому простому тесті це не має ніякого ефекту. Збирати сміття варто, якщо вимірювати витрати цілого алгоритму.
Так от, виходить, що як Go, так і Ruby зберігають масив цілих чисел у 64-бітному відображенні, та практично без додаткових витрат. Причому в Go кількість бітів у типі задається явно, а от від Ruby можна було очікувати й 32-бітних чисел, бо мої значення влазять в 32 біти. Втім, виглядає так, що Ruby завжди використовує 64-бітні числа, що логічно на моїй 64-бітній системі.
Рядки на Go витрачають, окрім байта на кожний символ, додатково 8 байтів на вказівник, та 8 байтів на довжину рядка (або так це виглядає). В Ruby, напевно, рядок містить ще якесь значення — можливо, кодування.
Можемо також підтвердити економію, яку Go отримує на тому, що рядки — є константи]. А саме, якщо масив наповнити не різними рядками, а одним й тим самим рядком, то витрати памʼяті будуть такі самі, як на масив чисел. І це незалежно від довжини цього рядка. Це пояснюється тим, що фактично масив містить вказівники на один та той самий рядок, а вказівник займає ті самі 8 байтів, як і число. В Ruby можна досягнути такої самої економії, якщо робити масив символів, тобто :symbol. Що зайвий раз підкреслює важливість використання символів там, де одне й те саме значення повторюватиметься багато разів — наприклад, при декодуванні JSON.
Але масиви — то надто просто. Цікавіше буде порівняти витрати на структури або хеші — фундамент сучасного програмування. Про це — наступного разу.
01.03.2023
Коли кешування зробити неможливо
Сьогодні коротенькі міркування про той випадок, коли кешування зробити неможливо.
Часто майбутні проблеми зі швидкодією бази даних планують вирішувати впровадженням кешу (принаймні я). Типу, зараз і так все добре, а коли база не буде витягувати — додамо кеш. От натрапив на таку ситуацію, коли кешування, хоч технічно можливо, але нічого абсолютно не дасть.
Уявимо собі що ми робимо інформаційно-погрозливе табло для автостради в стилі “Чорного дзеркала”. Треба розпізнати номер автівки та, якщо по ньому є штрафи, то показати водію попередження. Для цього у нас є централізована база штрафів. Пілотний проєкт працює відмінно. Отримали замовлення від Укравтодору на тисячі таких табло та сотні тисяч автівок. Хочемо уникнути ризику, що база не витягне такого навантаження.
Так от, проблема в тому, що кожна автівка проїжджає повз табло тільки один раз, тож і кожний запит до бази робиться один раз. Хоч ми й можемо закешувати результат, але цей кеш знадобиться тільки в разових крайніх випадках.
Можна зробити інакше та кешувати всю базу заздалегідь, а потім регулярно оновлювати. Таке рішення насправді нерідко зустрічається, наприклад, антивіруси завантажують локальну копію вірусної бази, а не питають кожного разу в сервера.
Якщо ж нас не влаштовує локальна копія — може, ми хочемо оновлювати інформацію якнайшвидше — тоді все ж таки треба робити центральну базу швидкою, без кешу. От нещодавно дізнався, що AWS DynamoDB обіцяє не тільки необмежене масштабування, але й швидкість відповіді в межах 10 мілісекунд.
28.02.2023
Як запустити будь-який скрипт на AWS ECS
Забув вчора розповісти про головне. А саме — бенчмарки треба було запускати на AWS ECS, в умовах, наближених до реальних. От тільки є проблема — на ECS запускаються тільки додатки, які вже запаковані в образи Docker та завантажені у реєстр. Що не сприяє запуску випадкових скриптів та швидким ітераціям розробки.
Але вихід є — при запуску контейнера ECS дозволяє замінити його командний рядок. Тоді скрипт можна розмістити цілком в новій команді, та таким чином запустити. Тільки вручну це робити складно, тому ми в команді зробили утиліту ecs_run, яка власне й робить те, що я написав. Утиліта ecs_run дозволяє нам запускати усілякі rake скрипти, а також просто довільний код на Ruby. (Щоб виконати Ruby-код, вона загортає його у виклик інтерпретатора ruby.)
Тобто, у нас вже є сервіс ECS, в якому міститься додаток, готовий до запуску — код, залежності, необхідні змінні оточення та дозволи. Я командою ecr_run <benchmark.rb запускаю свій бенчмарк безпосередньо в цьому середовищі, тому він має доступ до всього, що є в додатку.
Ось тільки заміна командного рядка в ECS обмежена розміром 8KB. (Як я дізнався вчора; раніше це не заважало.) Що тоді робити? Я придумав розміщати сам скрипт у секретному Gist, а в контейнер передавати обгортку, яка завантажить його та запустить. Якщо цю ідею розвинути, то можна взагалі пакувати всі змінені файли в архів, архів передавати в контейнер, розпаковувати, та мати “неофіційну” версію додатка без зайвого розгортування.
А ще є ідея з іншого боку — що якщо паралельно відкривати локальний тунель, наприклад ngrok, а контейнер нехай підʼєднається до нього та відкриє інтерактивну консоль. В теорії зробити це нескладно, але готових реалізацій я не знаю. Втім, інтерактивна консоль — одна з найцінніших можливостей Heroku, якої часто бракує на ECS.
27.02.2023
Поради по бенчмарках на Ruby
Сьогодні вдалося зробити чудовий бенчмарк для моїх поробок на Redshift. Власне, сам бенчмарк робився на Ruby, та порівнював швидкодію різних запитів в базу. Поки писав, знайшов пару цікавих рішень.
-
Обовʼязково треба знати про бібліотеку benchmark-ips. На відміну від стандартного модуля
Benchmark, ця бібліотека запускає приклади багато разів, та визначає середню швидкодію в ітераціях на секунду (IPS), а також порівнює її проміж прикладами. Я тільки з цією бібліотекою бенчмарки й роблю. -
Бенчмарк бази ускладнюється тим, що треба підготувати репрезентабельний набір даних, а також уникнути кешування результатів — як повного, так і часткового. Тому вирішив вимірювати на реальних даних, а щоб було чесніше — для кожної ітерації підставляв випадкові параметри фільтрів. Для цього, звісно, є вбудований шаблонізатор ERB. До речі, у класу
ERBє непомітний методERB#result_with_hash. Він підставляє у шаблон дані з хешу, а не з об’єкта, що мені майже завжди зручніше. -
Але ж, якщо параметри запитів випадкові, то як можна порівнювати результати проміж варіантами? Можна, якщо генератор випадкових чисел перезаряджати перед прикладом. (Випадкові числа — то є передбачувана послідовність, яка починається з початкового значення — зерна. Знаючи зерно, відтвориш всю послідовність.) Для цього треба створити генератор командою
@rng = Random.new, а перезаряджати, відповідно, командою@rng = Random.new(@rng.seed), щоб зберегти початкове значення. -
До речі, для цього у
benchmark-ipsє можливість вказати власний модуль з хуками, які запускатимуться перед прикладом.
26.02.2023
Де StackOverflow для ремонту?
Вихідні пройшли за ремонтом, а точніше — оновленням проводки та домашньої мережі. Проєкт на 7 розеток, з яких одна - Ethernet, а інша — контрольована, а також два Ethernet кабелі через всю квартиру, та пересування роутера з кімнати на поличку в коридорі (яку теж довелося встановити.)
Найскладніше в ремонті, як на мене — то виправлення помилок. Коли звик до програмування, де будь-які зміни можна відкотити, то посунути на декілька міліметрів отвір в стіні, або зробити довше надто короткий дріт може бути фізично неможливо — втім, якийсь компроміс доведеться робити. Друге найскладніше — то дебаг, тобто зʼясування, що саме не працює. От сьогодні довго порався та декілька разів перетискав мережевий кабель, бо не міг отримати сигналу. А виявилось, що це тому, що я перевіряв Ethernet-адаптером та ноутбуком, а він недостатньо потужний. (Так, якщо кабель задовгий — то вже треба щось с власним джерелом живлення — наприклад, комутатор.)
Мене дуже дратує, що в інтернеті не вистачає інформації по ремонту. Типові тексти — то статті-SEО для різних агенцій. Або погані переклади. Або переклади статей SEO. Все це жахливо читати, але доводиться, бо, скажімо, де ще знайти дані щодо максимальних навантажень на шуруп. Ще зустрічаються стрічки на форумах, де половина постів — то суперечки про те, хто кращий майстер.ʼ
Проблема погіршується тим, що існує купа регіональних особливостей. Всі знають, що розетки в США інші; але також інші стандарти труб та фітингів — навіть кран з польської IKEA не вдасться просто взяти та встановити в Україні. Якщо про програмування можна відразу шукати англійською, то з ремонтом це загрожує неприємними регіональними сюрпризами.
Що з всім цим робити — я не знаю. Може хоч колись поділюся своїми нотатками. Що і вам раджу зробити.
25.02.2023
Інтуїція про швидкодію мов програмування
Яка мова програмування швидка, яка ні? Загальне правило тут старе та просте: інтерпретовані, тобто скриптові, мови — повільні, компільовані — швидкі. Але сучасна арена технологій складніша. Будь-яка успішна інтерпретована мова — будь то Ruby, JavaScript, або Perl - компілюють код під час виконання, щоб його прискорити.
Взагалі програми повільні через невизначеність. Чим більш чітко мова задає програму та її структури даних — тим краще буде працювати. (Компіляція під час виконання саме й аналізує код на наявність “визначеної” частини.) Наочний приклад: Rust проти Go; в Rust треба явно назначати “власність” памʼяті, проте це дозволяє уникнути збірки сміття, яку вимушений робити Go; тож Rust - швидша мова. Керування памʼяттю — це найголовніший фактор невизначеності, що робить програми повільними; системною мовою програмування С вся памʼять виділяється та звільняється вручну — абсолютно визначено та без зайвих витрат часу. Але ручне керування робити складно, та призводить до критичних помилок. Тому більшість сучасних програмістів користуються мовами, що звільняють памʼять самі, неявно. Проте ефективне автоматичне керування памʼяттю — досі не вирішене питання (про що також свідчить існування мови Rust.)
Окрім керування памʼяттю, дорого коштує відсутність типів та усіляка гнучкість. Наприклад, програма на Go під час виконання чітко знає, де в памʼяті знаходиться та чи інша функція; а програма на Ruby має спочатку перевірити, чи не заманкіпатчили її, та взагалі, чи є така функція у даного обʼєкту.
Скриптові мови існують, бо не завжди потрібна максимальна швидкодія. Здатність писати простіші (та менш визначені) програми теж дуже цінна, бо це дозволяє досягнути складнішої логіки меншим кодом. До того ж будь-яка скриптова мова — це неодмінно тільки шар, під яким знаходиться швидкий компільований код. Причому шар порівняно тонкий. Наприклад, може здатись, що в вебі переважають саме скриптові мови. Але ж за додатком на Ruby або PHP або Python стоїть база даних, написана на мові C. А перед ним — балансувальник навантаження, теж на мові C. Тож наш додаток не так вже й багато робить сам по собі.
24.02.2023
Погано чи ненормально?
За результатами вимірювань, інструмент для виявлення SpamAssassin обробляє один лист за 25 секунд. Це добре чи погано?
…Питання з каверзою. Головне, що це ненормально. Пробачте за відсутність кращих термінів, але я маю на увазі, що якщо вимірювання показали такий результат, то щось пішло не так. Звідки це випливає? Наприклад, з того, що SpamAssassin - широко використовувана система, а листи приходять частіше, ніж раз на 25 секунд, тож з такою швидкодією вона просто має не впоратись.
Тож в такому випадку треба зупинитись та зрозуміти, що саме в процесі вимірювань працює не так. Може, клієнт не може під’єднатися до сервісу SpamAssassin, та просто повертає порожній результат після тайм-ауту… Та багато чого може бути, головне, що цей результат надто ненормальний, щоб з ним працювати далі (наприклад, базувати оцінки, або шукати шляхи оптимізації.)
Я вже писав про чуття на проблеми. Це саме той випадок. Звісно, погані показники — це норма, тож зазвичай нічого вирішувати не треба. Звісно, немає ніякого алгоритму, щоб зрозуміти, де саме звернути увагу. Тут як раз потрібна інтуїція. Та, може, ще сміливість побачити, що поточне, зручне розуміння хибне.
23.02.2023
Декілька думок про гарний сон
Сьогодні намагаюсь лягти спати раніше. Як я колись писав, сон — найґрунтовніший фактор якості життя, разом з харчуванням та фізичною активністю. Але ж знати це простіше, ніж влаштувати собі якісний та довгий сон. Кілька думок про це.
В першу чергу все ж таки треба лягати спати до 12; за моїм досвідом такий сон більше відновлює та потрібно його менше. Але ж “просто” лягти раніше не так просто; в мене це значить виправити весь розклад дня, тож ніяк не виходить. Намагаюсь нагадувати себе, що якісний сон покращує все інше, що трапляється в житті, тож “економити” на ньому безглуздо. Ще, хронічна недостача сну занижує когнітивні здатності, тобто реально робить нас дурнішими. Подумай про це наступного разу, як не розвʼязується складна проблема на роботі.
Окрім тривалості, та очевидних рекомендацій типу — купити комфортну постіль, добре зашторювати штори, та заклеїти яскраві індикатори непрозорою плівкою, додам: щоб спалося спокійніше варто перед сном сісти та записати все, що вертиться в голові. Або ж якщо думки ніяк не дають заснути, то встати та записати їх — теж допомагає.
Також, я не доктор, але можу порадити препарати магнія+B6 для покращення якості сну. Причому краще над усе знайти таурат магнію (magnesium taurate). Магній також допомагає концентрації, тож для роботи головою це важлива добавка. (Але магнієм єдиним, на жаль, всього не виправиш.)
22.02.2023
Mermaid - формат побудови схем
Якщо доводиться писати документацію у Markdown, раджу ознайомитись з генератором схем Mermaid. Бо в багатьох редакторах з Markdown підтримка Mermaid вбудована; достатньо помітити блок з вихідним кодом мовою '''mermaid та він буде замінений на відповідну схему. Так роблять GitHub, Obsidian, та навіть попередній перегляд Markdown у VSCode.
Я навіть не писав “якщо доводиться робити схеми”. Бо якщо схеми треба малювати вручну, то не будеш цього робити без великої потреби. В цьому сила Mermaid: якщо вже знаєш синтаксис, то вставити схему в текст майже не складніше, ніж писати сам текст. Тож це ефективний та разючий спосіб покращити наочність повсякденної документації.
Ще, випробував сьогодні Redshift Serverless для запуску тестів на CI. Ой, погана це ідея. Виходить замість $216 на місяць за кластер близько $0.72 за хвилину тестів, причому швидше працювати вони не стали. На мою думку, Serverless підійде тільки для випадків, коли базою користуються в ручному режимі та рідко — може, щоб згенерувати звіт раз на день.

