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

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

Підписатись на RSS · 📢 Канал в Telegram @stendap_sogodni

22.09.2024

Ще про ізоляцію даних у Swift

Ох і Swift! Такої суворої системи ізоляції я ще не бачив (ну, хіба в Rust, але там я далеко не заходив.) Не очікував такого від мови, яка для екосистеми Apple значить те ж саме, що JavaScript для браузерів — тобто це перша та головна мова, з якою стикаються всі.

Так, я звик до async/await в JavaScript. Як і до “розфарбовування функцій” - тобто обовʼязкового маркування функцій, де є await. Але якщо в JS async/await зʼявляється практично тільки там, де є ввід/вивід — то у Swift будь-яка заявка на рівночасність потребує async/await та розфарбовування.

Причому рівночасність у Swift нам потрібна, щоб робити програми, де не гальмує інтерфейс. Втім, я б радив почати з @MainActor всюди, а потім впроваджувати акторів обережно та обмежено, бо вони досипають в код несподіваних ускладнень. Наприклад:


21.09.2024

Оновлення до Swift 6 та його моделі рівночасності

Оновлював свої застосунки на Swift до свіжого Swift 6. Це вимагає дотримання моделі рівночасності. Досвід був цікавий. Простий застосунок майже не потребує змін. Але деякі функції вимагають переписування, навіть якщо в моєму коді немає акторів.

Мій застосунок з HealthKit був найбільш неприємним. HealthKit отримав новий набір API з асинхронністю, що чудово. От тільки старі API викликають непрозору помилку в асемблерному коді та ще й без стека. Суть помилки, як я зрештою зрозумів — що колбек був викликаний не в очікуваній “черзі”. А саме, в черзі @MainActor, а не в фоновій. Довелось переписати весь код з колбеками на API з async, причому розшукувати його вручну. Результатом я задоволений, але досвід розробки максимально вражий.

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

Взагалі ментальна модель безпеки Swift така, що кожна змінна належить одному з акторів. Тому ми думаємо не про послідовність та синхронізацію операцій, а про власність над даними. Та цікаво, що власність не залежить від розташування змінних, наприклад, посередині нашого актора може бути атрибут чи функція з власністю @MainActor.

Та другий аспект — це типи Sendable - які дозволено передавати з актора в актора, або захопити в замиканнях з іншого актору. З цим доводиться помізкувати. Наприклад, мав проблеми з AVAssetWriterInput. Там API з колбеком “дай мені ще кадрів”. Цей колбек важко уявити без зовнішнього стану. Але ось проблема: якщо я викликаю метод всередині актору, то колбек все одно буде викликаний в іншій черзі — та в ньому не дозволено змінювати захоплені змінні. Знайшов цікаве рішення, де будь-яку змінну можна зробити Sendable, якщо додати до неї обгортку та замок.

Принаймні це оновлення є хорошою вправою та новим для мене способом думати про безпеку рівночасних операцій.


20.09.2024

Кожний сеанс продукту — це тестування на юзабіліті

Я собі давно виробив звичку, що коли хтось інший використовує мій продукт, а я опиняюсь в ролі спостерігача, то звертаю увагу на незручні аспекти та занотовую їх. Звісно, це ціла дисципліна — перевірка на користувачах — але не обовʼязково для того збирати фокус-групи.

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

Мені особливо подобається спостерігати за тестувальниками. Дорогою до багів можна помітити погану навігацію чи нестачу інформації на сторінці. А час від часу й баг виявляється тільки незрозумілою фічею.

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

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


19.09.2024

SIMD: оптимізація, але не для нашого коду

В темі прискорення арифметичного коду зʼявляється (принаймні, в мене) ідея застосування інструкцій SIMD: “одна інструкція на багато значень”. Я вже писав, що NumPy їх використовує, але ж так само можна й власноруч оптимізувати код. В теорії…

Проблема перша: що ці інструкції потребують суворого дотримання формату вхідних даних. В деяких випадках це легко. Якщо працюєш з 3D-координатами або з кольорами, то багато операцій легко перекладаються на SIMD (що вже зробили популярні бібліотеки.). Ось гарна стаття. Але в загальному випадку доведеться на кожну арифметичну операцію дивитися окремо. До того ж достатній обсяг пересувань перекриє виграш від SIMD.

Проблема друга: операції SIMD працюють не над всім вашим масивом, а тільки над фіксованою частиною (наприклад, 256 байтів.) Це не так, як з NumPy, де скалярна арифметика “просто” стає векторною. Доведеться думати, як ділити масив та куди зберігати проміжні значення.

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

Але здебільшого, SIMD використовується в серйозних бібліотеках для обробки масивів даних. Не тільки математичних — наприклад, є simdjson для розбору JSON. Або будь-який аудіо/відео кодек — саме для кодеків й придумали SIMD, до речі. Або нейронні мережі, бо там все зводиться до множення матриць.

А коли в нас задача на кшталт “виконати більш-менш складний арифметичний вираз над кожним елементом масиву”, то SIMD не допоможе.


18.09.2024

Оптимізація паралельними обчисленнями

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

Вийшло знову несподівано. Я очікував, що, оскільки програма складається на 100% з арифметики, то розпаралелити її можна практично ідеально, тобто ділити час виконання на число ядер.

Технічно програма працює за ідіомами Go: декілька горутін, в кожної є канал на вхід та канал на вихід. Кожна горутіна обробляє частину масиву даних; також є головний потік, який керує процесом та агрегує результати.

Вийшло так, що впровадження горутін уповільнило програму приблизно на 60%. Це якщо горутіна одна, що мало б бути еквівалентно непаралельному рішенню. Стільки часу уходить на синхронізацію. Особливість нашої програми в тому, що кожна ітерація спирається на показник, похідний від усіх результатів попередньої. Виходить, що десь 3000 разів на секунду контроль переходить між головною та робочою горутіною.

Хороші новини, що збільшення кількості горутін все ж дає очікуваний пропорційний приріст.. Тобто в чотири потоки програма все ж на 60% швидше за послідовну — але не на 75, як хотілося б. Більше в мене ядер немає, але гадаю, чим більше ядер — тим виправданіше паралелізація.


17.09.2024

Як безпечно показати HTML?

Уявіть що вам потрібно показати на сайті фрагмент чужого HTML. Сподіваюся, очевидно, що це відкриває загрозу злому: в першу чергу, через виконання JavaScript можна вкрасти авторизаційні кукі, іншу інформацію, та навіть викликати API та завантажувати сторінки сайту. Хочу розглянути способи захисту.

Найтрадиційніший спосіб, відомий по форумах — обмеження змісту HTML до безпечних тегів та атрибутів. Обовʼязково це повинен бути саме список безпечних, а не небезпечних елементів — але й так існує ризик всього не передбачити.

До того ж обмеження змісту передбачає контроль HTML на вході; вже готовий HTML можна зачищати. Але будь-який санітайзер HTML обовʼязково змінюватиме його, принаймні щоб виправити можливі помилки. Тож ми вже не побачимо вхідний HTML “як було” - що може бути небажано.

Другий поширений спосіб: віддавати сторінку HTML з іншого домену. Тоді ця сторінка не отримає кукі з нашого сайту. Але нюанс: тоді й авторизацію доведеться придумати суто за URL. Як відоме рішення, є підписані URL на S3.

Ізоляції даних можна досягнути й через використання Blob URL, колись я про це згадував. Його можна відкрити як на новій сторінці, так і у фреймі. Це рішення потребує JavaScript, зате не потребує додаткової авторизації.

До речі, у <iframe> є корисний атрибут sandbox, яким можна зовсім вимкнути JavaScript та інші функції — наприклад, форми. Підтримка його наразі цілих 97% - але я б все одно використовував його разом з іншими методами.

Нарешті, є радикальна опція “Opera Mini”: взагалі відображати HTML в ізольованому браузері на сервері, а клієнтові віддавати тільки його зображення. Так втрачаємо всяку інтерактивність, зате в нападників просто не залишається шансів.


16.09.2024

Чому "переписувати на мову" в принципі погана ідея?

Коли програма працює повільно, традиційно чуєш ідею — перепишім на мові Y! На мою думку, так варто робити тільки після того, як вичерпано можливості оптимізації. Яких завжди знаходиться багато. Звісно, переписати іншою мовою легше (в наш час, певно, майже автоматично) - але потенційний виграш тут менше, а підтримка такого рішення значно ускладниться, особливо коли йдеться про живий продукт, а не іграшкову програму.

Переписав вчорашній приклад на Go. Це не найшвидша мова, але мені найзручніша. Повільність Go здебільшого стосується збірки сміття, якого в цьому прикладі можна взагалі уникнути. Також програму на Go мені буде легко зробити паралельною. Ось gist з всіма розвʼязками.

🟰 Наївний переклад працює на 10% швидше за Паскаль. Зазначу що це вже не найгірше рішення — воно створює масиви заздалегідь, а не на кожну ітерацію. Але не бачу сенсу штучно погіршувати програму, порівняно з оригіналом. Різниця в 10% досить передбачувана — обидві мови типізовані, компілюються; не здивуюся, якщо машинний код програм майже однаковий.

Перша оптимізація коду з купою арифметики: не повторюймо операції! Особливо ділення — найповільнішу з операцій. Інтуїція підказує, що арифметика — це швидко; але коли вся програма — це одна арифметика, мусимо звернути увагу на кожний вираз. Дещо можна було навіть винести з циклу та викликати у 200 000 разів рідше.

➖ Також приклад робив зайві обчислення заради виводу результату; вивід відбувався раз на 100 ітерацій, а обчислення — щоразу. Усунення повторюваних та зайвих операцій прискорило розвʼязок у 2 рази!

✖️ Друга оптимізація: використовуйте локальні змінні. Тут сам здивувався. Початкова програма весь стан тримала в глобальних змінних. Так, мені здавалося, я заощаджу на керуванні памʼяттю. Але ось що виявилось: перенесення всіх змінних всередину функції прискорив розвʼязок ще в 3 рази! В 3 рази! Я знаю, що глобальні змінні розташовуються в BSS-сегменті, а локальні — на стеку, але ніяк не очікував таку величезну перевагу останнього. Перевіряйте свої переконання!

➕ Отже, переписування зробило програму швидше на 10%, а оптимізація — на 82%. Та ті ж самі нехитрі оптимізації можна було зробити й на Паскалі. І це тільки найпростіша програма, без кешування, всяких неефективних структур даних, чи алгоритмів з експоненційною складністю. Не поспішайте переписувати — розумійте базові принципи, які роблять програми швидкими або повільними.


15.09.2024

З Pascal в NumPy

Натрапив на питання про те, як прискорити програму з обчисленнями на Pascal. Чудова задача на оптимізацію, подумав я, та вирішив спробувати NumPy, як топове рішення для обробки математичних даних… з яким я не мав практичного досвіду.

🐍 Перший очевидний результат: наївне переписування на Python уповільнює програму у 160 разів. Очевидний тому, що в Python повільний тип “масив” (насправді “список”) та й взагалі, все що я писав про повільність Ruby, стосується й пітону.

⛴️ Проте так само очевидно, що Python беруть для використання бібліотек на кшталт NumPy. Головною ідеєю тут є векторизація. Так називають два різних підходи. На високому рівні — це виклик функцій низькорівневої мови для обробки цілих масивів замість окремих значень. На низькому рівні — це використання спеціальних інструкцій процесора SIMD: єдина інструкція — багато даних. В разі NumPy обидва підходи мають місце.

🚤 Версія з NumPy за швидкістю близька до оригіналу на Pascal. Що досить непоганий результат, якщо врахувати, що програма написана на Python, де не потрібно оголошувати змінні та можна запускати програму по шматках в Jupyter.

↔️ Єдиним неясним місцем в перекладі було фільтрування масивів. Як виявилось, в NumPy для того є дві семантики: take вибирає з масиву за масивом індексів, а extract - за масивом-“маскою”. Але було неочевидно, що функції filter немає. Зате приємно здивувало, що можна прочитати файл з рядками чисел однією функцією readfile.

🐢 Чому вона не швидше? Для мене відкриттям стало, що NumPy не включає майже ніякого паралелізму. Тільки для окремих операцій — особливо над матрицями. А на цьому прикладі я бачив тільки завантаження одного ядра на 100%. Тобто, в цілому, як програма на Pascal, так і на Python/NumPy робила однакову кількість арифметичних операцій — не дивно, що швидкість приблизно однакова.

🐇 Можливо, десь SIMD чи інші оптимізації й дали прискорення, але з іншого боку, програма на Pascal виділяє памʼять тільки один раз — а на Python нові масиви створюються декілька разів на ітерацію. Що точно дає затримку.

🤔 Залишилося перевірити, якої оптимізації можна було досягнути низькорівневим рішенням.


14.09.2024

Чим зайнятись на телефоні?

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

Та я б хотів знайти активності (та звички), щоб робити в ті моменти очікування. На читання чи слухання чогось вагомого зазвичай не вистачає фокусу. А ось що поки придумав:


13.09.2024

SwiftUI на macOS: спостереження

За тиждень неквапливого програмування зробив собі на SwiftUI застосуночок для ведення списків по GTD. Декілька спостережень та висновків:

🦀 Нарешті, щоб було веселіше, залишу вас зі статтею: Swift is a more convenient Rust.