Стендап Сьогодні
Що я зробив, що я хочу зробити, і що це все значить.
Повсякденні здобутки в форматі стендапу.
Детальніше в статті
Підписатись на RSS
📢
Канал в Telegram @stendap_sogodni
🦣
@stendap_sogodni@shevtsov.me в Федиверсі
24.08.2024
Функціональні структури даних
Теоретична частина: поставив собі питання — яким чином функціональні мови здатні… функціонувати, коли вони ніби повинні без кінця копіювати значення для збереження незмінності? Це ж повинно бути надзвичайно неефективно, особливо коли структури даних більшають.
Коротка відповідь: функціональні мови використовують особливі структури даних, які зокрема дозволяють зберігати незмінену частину без копіювання. Як виявилося, існує величезна академічна та практична база за цим питанням.Направляю у вікіпедію за подробицями.
Та другий аспект — як я зрозумів, ліниві обчислення насамперед корисні тим, що дозволяють групувати низку перетворень, а значить — уникати зберігання проміжних результатів.
Виходить, що чисто функціональна мова (або чисто функціональна бібліотека) здатна хоча б наблизитись за швидкістю до імперативної. Що досить суттєвий аргумент за використання функціональної мови “від природи” замість ФП імперативною мовою, що я відразу вирішив перевірити на JavaScript.
Та тут практична частина: зробив невеличкий тестовий скрипт; задача — підрахувати кількістю ключів у SteamDB, довших за 5 символів. Така досить класична функціональна задача на поважному обсязі в 163 Мб даних. Що маємо:
- Функціональне рішення лише у 2 рази повільніше за імперативне (яке не приводжу заради стислості):
json.reduce((a, r) => a + Object.keys(r).filter((k) => k.length > 5).length, 0);
-
Таке саме рішення, але з використанням відомої бібліотеки Immutable, в 10 разів повільніше за попереднє! Висновок: на практиці ціна функціональних структур даних перевищує витрати на копіювання традиційних.
-
Та саме цікаве: думаю, ну добре, але ось ClojureScript точно повинен робити все правильно та ефективно. Але ні: код на ClojureScript був ще в 4 рази повільніше за Immutable, тобто у 80 разів повільніший за імперативне рішення!
(reduce + 0 (map
(fn [r]
(count (filter #(> (count %) 5) (keys r))))
json))
Висновок: я вірю, що якщо взяти низькорівневу мову та реалізувати функціональні структури близько до заліза, можливо, з власним керуванням памʼяттю, то це може бути швидко (Тобто, наприклад, варіант Haskell, а можливо й Clojure без -script.) Але в імперативній мові вони навряд чи зроблять функціональний код швидше.
23.08.2024
Дизайн та естетики
Ділюся ще досі сирими, але впевненими міркуваннями про те, як зрозуміти дизайн, якщо пощастило бути програмістом.
Є таке модне сучасне слово: естетика. (Тобто слово старе, а значення надсучасне.) Естетика — це, моїми словами, сукупність відчуттів від якогось предмета. Є зміст, а є естетика. На відміну від інших схожих слів, естетика фіксує точку в просторі, навколо якої скупчуються схожі предмети. Є ціла енциклопедія естетик.
Естетики є в інтерʼєрі, одязі, вебсайтах… є вони й в коді. Наприклад “естетика Ruby on Rails” багато у свій час наробила хвиль. Естетику складно описати словами, вона невербальна, легше побачити. Тим важче формалізувати: правила на кшталт “роби функції не довше 10 рядків” корисні, але не навчать тебе програмувати зі стилем; це приходить з досвідом.
Так само й в дизайні, будь-який дизайн не створюється в вакуумі з голови, а існує в культурному просторі. Щоб навчитися робити щось гарне, треба знайти естетику, яка тобі до вподоби, збирати приклади та відтворювати їх. В запозиченні немає нічого ганебного: поки починаєш, твоя задача привʼязатися до свого орієнтира.
Я про це пишу, бо в мене була така хиба, що ніби дизайн утворюється з чистого листа, як то кажуть, “з першооснов”… якщо не виходить — значить немає хисту. Проте спостерігати, сприймати та відтворювати естетики здатний кожний успішний програміст — не треба цього боятися.
22.08.2024
Мова програмування С як основа
Я мимохідь сказав, що серед всіх мов С найважливіша для розуміння компʼютера. Так склалося, що у C особливе положення: це фундамент та спільна база для безлічі інших мов. (А C++ - це зовсім інша мова, а ніяк не просто “доповнення до С”, як може здатися!)
C з першого погляду здається схожим на будь-яку сучасну мову. Тут є функції, цикли, масиви. Структури! Проте все не так, як здається: кожний елемент мови C максимально наближений до машинної реалізації, тож замість зручності використання ми отримуємо близькість до заліза.
Ну, наприклад: масиви в С мають фіксований розмір. Бо масив — це ж просто блок памʼяті: хочеш змінити розмір — виділяй новий блок. Памʼять не тільки виділяється, а й звільняється вручну, що з одного боку веде до купи помилок, а з іншого, відкриває незрівнянну гнучкість.
Структури — це теж просто блок памʼяті, тільки поділений не на елементи, а на поля; причому розташування поля в памʼяті настільки передбачувано, що структурами можна розбирати двійкові файли чи мережеві повідомлення. Це дуже просто: беремо вказівник на масив прочитаного буфера, та призначаємо його вказівнику на структуру потрібного типу. Ось вам і справжній zero-copy (тільки не забудьте не звільнити один з вказівників, бо буде біда!)
А ніякого ООП тут немає. Принцип виклику функцій теж найпростіший: передали аргументи, викликали, отримали результат. Настільки простий, що саме модель виклику C є lingua franca всіх мов: саме через неї Ruby може викликати бібліотеку, написану на Go чи Rust.
За проєкт для вивчення C я б порадив зробити гру або навіть демку. Причому взяти для цього найпростішу графічну бібліотеку raylib та веселитися, як у 1999-му! Або почати з читання вічної та епічної книги “Мова програмування C” Кернігана та Річі. `
21.08.2024
Високорівневі мови та навіщо вони потрібні
Насправді низькорівнева мова здатна виконати всі високорівневі функції: принаймні тому, що будь-яка високорівнева мова спирається на низькорівневу всередині. Що ставить запитання: а навіщо тоді всі ці високорівневі мови?
Я б хотів сказати, що вони розкривають якісь нові парадигми, але на практиці популярні високорівневі мови тримаються у фарватері “ООП + імперативний код + трохи функціонального”. А ті, що особливі, залишаються нішевими.
Тому натомість відзначу, що найголовнішим кроком є відхід від явної типізації. Багато коли “типи або без типів” є питанням смаку, але в одній області динамічна типізація суттєво полегшує життя. Це робота з часткою складної структури даних — наприклад — з чужим API чи з файлом експорту в JSON. Я не хочу писати типи — я тільки хочу отримати дані за шляхом! Взагалі існування формату JSON спирається на динамічну типізацію.
До того ж низькорівневі типи — теж не ідеал. Найпотужніші системи типів розглядають категорії значень, а не розміщення даних в памʼяті. Моя улюблена - в TypeScript. Вона мало має спільного з апаратною моделлю, зате чудово спрощує функціональне програмування на JavaScript.
Знання високорівневої мови допоможе розвʼязувати задачі “на гнучкість” швидше. Це може бути як проста задача (наприклад, три дні тому, щоб згенерувати потрібний JSON, я написав один рядок Ruby, а ніяк не програму на Go), так і цілий проєкт (наприклад, “традиційні” вебдодатки, де Ruby залишається непереможеним). Не кажучи вже про галузі, де мова визначена за нас — наприклад, JavaScript в браузері чи Python для аналізу даних.
20.08.2024
Низькорівневі мови та навіщо вони потрібні
Взагалі, якщо відкрити Вікіпедію, то там написано, що єдина низькорівнева мова — це асемблер. Але на мою думку це не корисне розрізнення, та буду говорити про більш розмовне, колоквіальне розуміння низькорівневості.
На якій би ми мові не писали, всі програми зводяться до виконання інструкцій процесора. Чим вище рівень мови, тим менше помітне це апаратне підґрунтя. Те різноманіття мов програмування, яке зараз існує, підтверджує потребу у відході від апаратної моделі. Але кожен крок нагору дається ціною.
Щоб розуміти цю ціну, напевно, спочатку треба ознайомитися якщо не з асемблером, то принаймні з C та принципами його роботи. Бо тоді стає зрозуміло, чого не може зробити високорівнева мова.
Як простий приклад: виклик функції “так, як це задумано” покладається на аргументи відомого розміру. А динамічна типізація не дозволяє нам знати розмір аргументу заздалегідь, Тому мова робить додаткові, непомітні нам дії, щоб викликати кожну функцію. Та зараз динамічні мови всі впроваджують JIT - тобто відокремлюють випадки, де аргументи відомі, та роблять для них “простий” виклик.
Низькорівневі мови дозволяють позбавитись цього гандикапу — інколи якісно суттєвого, ціною наближення до машинного коду. Процедурного стиля програмування, статичної типізації (тобто оголошення розміру значень заздалегідь), більш явного керування памʼяттю тощо.
Та, навіть коли твоя головна мова — високорівнева, варто знати деяку низькорівневу для тих випадків, коли потрібно витиснути з системи все. А також, можливо, щоб розуміти, як воно все працює всередині. Чи буде це С, С++, Rust, Go чи D - вже менш важливо.
19.08.2024
XcodeGen: генератор для проєктів XCode
Засоби розробки для платформ Apple знаходяться десь посередині драбини відкритості. З одного боку, вони доступні для автоматизації та використовують текстові файли конфігурації. З іншого, для користувача продумана тільки верхня обгортка.
Наприклад, головний файл проєкту project.pbxproj
є текстовим, але редагують його тільки через графічний інтерфейс XCode. Причому зміни не обовʼязково утворюють зрозумілу історію. Та й побачити “що ми там наклацали” складно. Плюс мене зокрема дратувало, що XCode не використовує нормальну ієрархію файлів та каталогів, а має власну, внутрішню.
Тому приємно було дізнатися з сусіднього блогу про існування утиліти XcodeGen, яка просто бере та генерує всю конфігурацію проєкту з “рукописного” YAML. А файлову структуру зчитує з диска. Таке я люблю!
Єдине, що складно, це переносити проєкти, які вже існують, бо як я вже казав, не так легко розібратися, що з їхньої конфігурації змістовно. Та й на жаль перша генерація повністю перегортає весь project.pbxproj
, тож не легше зʼясувати, що залишилося за бортом. Але мало-помалу переїхати вдасться, а далі конфігурація вже буде стабільною.
18.08.2024
Словники в Go: безплатних сніданків не існує
В продовження історії з модулем unique
для Go 1.23, хотів на практиці перевірити різницю між використанням символу та рядка.
Вимірював витрати памʼяті map[unique.Handle[string]]int
проти map[string]int
. Щоб ключ повторювався, наробив JSON з мільйоном обʼєктів {"foo": 123}
. Дійсно побачив різницю: 204 Мб проти 289 Мб… тобто 85 байтів на економії на елемент. По-перше, 85 байтів це якось незрівнянно багато. По друге… чому взагалі той масив займає в памʼяті 200 Мб? Це у два рази більше, ніж текст JSON.
Почав дивитися, як зміняться витрати, якщо зробити більш ніж один ключ. А ніяк! Виявилося, що Go резервує в новому словнику місце під 8 записів. Тоді вже ясніше: насправді надлишок на ключ-рядок проти ключа-вказівника лише 11 байтів: вказівник займає в памʼяті 8 байтів, а рядок - 16 байтів, плюс 3 байти на сам зміст. (Тобто короткі рядки теж неочікувано неефективні.)
Що, взагалі, меркне в порівнянні з базовими витратами у 200 байтів на кожний малесенький словничок. Мораль історії така: низькорівнева мова не робить програму чарівно швидкою та ефективною. Роздуті структури даних та вкладені цикли можна писати будь-якою мовою.
Звісно, в Go можна зробити набагато краще: оголосити структуру. Скільки займає struct { Foo int }
? 8 байтів. Але я не завжди звертав увагу на місця, де словники можна було замінити структурами. Особливо важко, коли паралельно програмуєш на мові, де між ними практично немає різниці. Таке.
17.08.2024
Модуль unique в Golang 1.23: глобально-унікальні символи
Не встиг я подати ідею про заміну повторюваних рядків на глобально-унікальні символи (interning), як вийшов Go 1.23, де зʼявився модуль unique, який буквально те й робить. Приємна несподіванка!
Метод unique.Make
підтримує не тільки рядки, а й будь-який порівнюваний тип, та замінює значення на глобально унікальні вказівники. Рядки-літерали в Go і так є константами, хіба що кожна копія має власний 16-байтовий заголовок.
А от якщо доводиться отримувати багато повторюваних рядків ззовні, модуль unique
стане до нагоди.
Наприклад, ми завантажуємо з бази фічафлаґи для поточного користувача. Кожний виклик утворює рядки наново та залишає за собою купу сміття. Якщо зробити їх типом unique.Handle[string]
, то ловимо двох зайців: фічафлаґи займають менше памʼяті, але до того ж тепер їхнє порівняння зводиться до порівняння вказівників. Виходить практично те ж саме, що замінити фічафлаґи на чисельні константи (доволі очевидна оптимізація), тільки повністю автоматично.
Може виникнути питання, чи безпечно брати рядки ззовні та глобалізувати — чи не призведе це до роздуття памʼяті від переповнення словника значень? Я перевірив, та виходить, що ні: модуль unique
автоматично очищає невживані значення, тобто роздуття буде не більше, як без глобалізації. А можливо, й менше.
До речі, для того використовуються слабкі вказівники — тип weak.Pointer. То є вказівники, які не впливають на збирання сміття. Тобто все, що робить функція очищення — це чекає, поки значення звільнить прибиральник, після чого видаляє вивільнені значення зі словника.
16.08.2024
Навіть прості справи варто записувати
Пʼятничний пост про GTD. Те, що варто не тримати все в голові, а краще записувати, це зрозуміло. Але… може хоч памʼятати “що зробити” це корисно? Цього тижня зловив розуміння, що як раз ні, навпаки.
Задача була така: “зателефонувати виконробу”. Ходив я з нею декілька днів. Скільки не згадував, так і не знайшов нагоди зателефонувати. Можна було б пояснити це прокрастинацією, тобто простими словами, лінню.
Проте от що я помітив: перед тим, як робити дзвінок, раптом виявлялося, що взагалі-то, ще не знаю, про що говорити. Задача не була хибною: дзвінок дійсно мав відбутися. Але вона не була наступною дією. Наступна дія, при ближчому розгляді, знайшлася на кшталт “уточнити з дружиною вибір фарби”. А це зовсім інший контекст. Та поки цього не усвідомиш, виглядало так, ніби я прокрастиную, та треба було лише “зібрати волю”. А причина була зовсім не у волі, а в браку осмислення.
Та іронія в тому, що саме такі задачі й застряють в голові. Ті, що прості, зробиш та забудеш. Тому коли в голові крутиться те що треба зробити, та ще й не один день крутиться, то це вірний знак що пора його записати та обміркувати за схемою.
Це і є те, про що йдеться в GTD: не тримай справи в голові, записуй. Навіть якщо справа не велика — зовсім не обовʼязково її можна “взяти та зробити”.
15.08.2024
Cap'n Proto - серіалізація без копіювання
Роздивлявся формат серіалізації Cap’n Proto. Його особливістю є відсутність копіювання даних. Це значить, що коли у вас є буфер даних (та схема до нього), то значення можна забирати прямо з буфера. Фактично Cap’n Proto не стільки серіалізатор, скільки формат організації даних у масиві памʼяті.
Схожих рішень не так багато. Можу відзначити flatbuffers від Google як справжню альтернативу. Тут треба обовʼязково роздивлятися, як воно підтримує вашу платформу (в моєму випадку - Go.) Взагалі я б сказав, що рідко трапляються обставини, щоб саме серіалізація була вузьким місцем.
Проте мені подобається те, що з масивом даних можна працювати по шматочках. Наприклад, якщо у вас є JSON в 1 Гб, то доведеться весь його завантажити та розібрати — навіть коли з нього потрібне одне значення. А якщо Cap’n Proto, то можна прицільно забирати конкретні записи та значення. Це гарно працює з mmap.
Також за моїми вимірами масив даних Cap’n Proto займає суттєво менше памʼяті, ніж еквівалентні структури Go. Особливо по витратах на керування памʼяттю. Там, де у звичайних структур спостерігаю надлишок майже в 100%, у Cap’n Proto його практично немає. Та й не дивно, бо в памʼяті утримується єдиний звичайнісінький масив байтів.
З незручного: Cap’n Proto не підтримує зміну довжини масивів (без перестворення). Також відсутній тип-словник. Словників взагалі в zero-copy не бачу — ймовірно, тому, що для ефективної роботи їх доведеться пересортовувати. Але словник завжди можна замінити пошуком за відсортованим списком.
Наразі автор Cap’n Proto працює у Cloudflare, а сам формат широко використовується у Cloudflare Workers, зокрема для зберігання даних.