Сохранёнки программиста
6.54K subscribers
1.18K photos
59 videos
10 files
1.88K links
Заметки и ссылки на будущее, чтобы изучить когда будет время.

Разместить рекламу: @tproger_sales_bot

Правила общения: https://tprg.ru/rules

Другие каналы: @tproger_channels

Другие наши проекты: https://tprg.ru/med
Download Telegram
Половину времени компиляции двух пакетов занимал memcpy, потому что элемент вектора весил 136 байт

Николас Незеркот, десять лет занимающийся производительностью компилятора Rust, подвёл 31 июля итоги восьми месяцев работы. Средняя экономия времени сборки с 3 декабря 2025 по 29 июля 2026 составила 5,59 процента, но примерно половину этого дал не сам компилятор, а генератор документации: там среднее время упало на 37,92 процента, а по всем его тестам суммарно на 28. Без учёта документации выигрыш компилятора 2,90 процента.

Самая наглядная история из отчёта как раз про memcpy. Автору показали пару пакетов, где новый решатель типажей резко тормозил. Профилирование через Cachegrind показало, что почти половина времени уходит на копирование памяти:

🔘 виноват оказался горячий вектор с элементом размером 136 байт: LLVM для значений больше 128 байт вставляет вызов memcpy вместо обычных инструкций;
🔘 автор убрал две трети перемещений и ужал тип до 104 байт, чтобы порог не срабатывал, и время сборки двух худших пакетов упало на 40 процентов;
🔘 отдельная линия — узлы синтаксического дерева: выражения ужали с 72 до 64 байт, чтобы они помещались в кэш-линию, и на деревоёмких тестах время упало более чем на 10 процентов, а доля промахов кэша до 29;
🔘 автор помнит времена, когда узел выражения весил 104 байта: хорошая производительность набирается такими шагами годами;
🔘 в Clippy нашлась классическая проблема виртуальных вызовов: сотни проверок, каждая объявляет несколько методов обхода, и на каждом узле дерева вызывались все, включая пустые. После объединения проходов время упало на 10–30 процентов, а неверные предсказания ветвлений — на 20–80, а на одном стресс-тесте на 97;
🔘 новый решатель типажей за три месяца прошёл путь с 27 секунд до менее чем секунды на одном из своих тестов.

Автор оговаривает, что цифры по Clippy получены локально: в постоянном замере на CI он пока не участвует. Забавная деталь про Clippy: параллельно ту же проблему нашёл другой человек и прислал более аккуратное решение на несколько дней раньше, так что правка автора не влилась.

Полная статья: https://nnethercote.github.io/2026/07/31/how-to-speed-up-the-rust-compiler-in-july-2026.html

@prog_stuff
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
Лимит одновременных транзакций подняли с тысячи до десяти тысяч, чтобы убрать ошибки, и получили шестнадцать минут деградации

Лиз ван Дейк из PlanetScale разобрала 7 августа инцидент, в котором продакшен-база MySQL складывалась на шестнадцать минут. Ошибки шли по нарастающей: на нулевой минуте 5 ошибок в минуту при 15 000 запросов в секунду, на десятой 1400 ошибок и 1500 запросов, потом транзакцию убил таймаут, накопленное разгреблось примерно за тридцать секунд, и пропускная способность подскочила до 8500.

Повод банальный: пакетная задача открыла транзакцию к горячей таблице, взяла блокировки строк и держала их пятнадцать минут без коммита. Интересно то, что происходило вокруг неё. Запросы, копившиеся следом, в основном на блокировках вообще не стояли: это обычные чтения, которым InnoDB отдаёт согласованный снимок. Но построение снимка требует пройти назад по истории версий каждой затронутой строки, а эта история росла все пятнадцать минут.

🔘 чтения, которые обычно занимают миллисекунды, начали упираться в потолок выполнения в 90 секунд, приложение повторяло их в тесном цикле;
🔘 за минуты внутри движка хранения скопилось больше десяти тысяч запросов, каждый восстанавливал всё более длинную цепочку версий;
🔘 чтения страниц обогнали способность буферного пула освобождать память, и падать начали запросы к совершенно другим таблицам, никак не связанным с заблокированными строками;
🔘 предыстория в том, что нагрузку переехали с прежней базы, где перед сервером стоял пул потоков: он ограничивал число одновременных запросов примерно тысячей, а остальных ставил в очередь на миллисекунды;
🔘 на новом месте пул при переполнении не ставит в очередь, а ждёт фиксированное время и возвращает ошибку. Приложение таких ошибок никогда не видело и обрабатывать их не умело, поэтому лимит подняли до десяти тысяч. Число выбрали не по расчёту, а потому что с ним ошибки прекратились;
🔘 лечение оказалось обратным: лимит вернули примерно к тысяче и заменили ошибку ожиданием в очереди с увеличенным таймаутом, то есть повторили поведение прежнего пула потоков.

На следующее утро конфигурация прошла проверку на другом шарде, который держит на порядок больше трафика: на пике около 25 тысяч заявок на слот в секунду при тысяче слотов, ноль ошибок и ровные 60 тысяч запросов в секунду. За весь день отклонена одна транзакция, а внутри MySQL в каждый момент выполнялось меньше двухсот команд: по закону Литтла это примерно три миллисекунды на запрос.

Автор оговаривает, что это не универсальный совет всегда зажимать параллелизм. Совет работает там, где есть пессимистичные блокировки и общее состояние: горячие строки, SELECT ... FOR UPDATE по популярным ключам, счётчики, балансы, очереди задач. И это не свойство MySQL: в PostgreSQL бывает то же самое, а роль ограничителя там играет PgBouncer.

Полная статья: https://planetscale.com/blog/concurrency-vs-throughput-vitess-mysql

@prog_stuff
Please open Telegram to view this post
VIEW IN TELEGRAM
Полгода Tailscale ловила порчу баз: 19 случаев, ни одного общего признака — ни шарда, ни клиента, ни времени суток. Воспроизвести не удавалось, оставалось ждать следующего раза с диагностикой наготове.

Зацепку дал побочный инструмент. Чтобы восстанавливаться без потери данных, команда стала писать журнал всех изменяющих запросов и проигрывать его поверх копии. Дважды журнал не проигрался: данные, зафиксированные одной транзакцией, оказались невидимы для следующих — без единой ошибки.

Виновата гонка в самом SQLite: при совпадении записи с контрольной точкой часть страниц считается перенесённой в основной файл, хотя туда не попала. Багу оказалось не меньше шестнадцати лет, он настолько редкий, что для тестов его приходится вызывать намеренно. Tailscale попадала в него чаще других, потому что берёт контрольные точки под ручное управление и делает их очень часто.

Вывод авторов: скучная технология в нестандартном режиме перестаёт быть скучной. Всё было по документации — просто в стороне от протоптанной дороги.

Как искали и что нашли

@prog_stuff
👍3
Новак Калуджерович писал арифметику больших чисел для криптографической библиотеки и нашёл ошибку в Algorithm D — том самом длинном делении из книги Кнута, которое перепечатано в сотнях реализаций.

Суть места, где всё ломается: очередная цифра частного сначала оценивается по старшим разрядам, а потом оценка исправляется. Кнут утверждает, что после коррекции она гарантированно помещается в один разряд. Контрпример нашёлся в системе счисления по основанию 3: делим 45 на 16, правильное частное 2, а пробная оценка 5. После двух коррекций оценка всё ещё двухразрядная, и дальнейшее умножение берёт только младший разряд, вычитая фактически ноль.

Ошибка десятилетиями оставалась незаметной, потому что зависит от того, как именно ведёт себя аппаратное деление на конкретной архитектуре. На современных машинах с основанием два в шестьдесят четвёртой она практически не проявляется.

Автор написал Кнуту и получил тот самый гексадецимальный доллар — чек за найденную ошибку — с рукописной пометкой, что Algorithm D читают чаще многих других алгоритмов книги.

@prog_stuff
👏3
Что будет, если в одной транзакции PostgreSQL накопится больше 64 вложенных? Просядет весь кластер — включая соединения, которые об этой транзакции ничего не знают и работают с другими таблицами.

PlanetScale разобрали механику. У каждого процесса есть кэш идентификаторов вложенных транзакций, обычно на 64 записи. Переполнился — снимок помечается переполненным, и проверка видимости каждой строки идёт через служебную структуру pg_subtrans: блокировка, а при промахе ещё и диск.

Вложенные транзакции при этом появляются сами: их создаёт каждый блок PL/pgSQL с секцией EXCEPTION, а не только явный SAVEPOINT.

Вторая беда приходит позже: записи о работающих транзакциях уезжают в журнал неполными, и новая реплика не может построить снимок. Она проигрывает журнал, но соединения не принимает — то есть поднятая на пике нагрузки реплика не даёт никакой ёмкости.

Следить можно через pg_stat_get_backend_subxact() и счётчики pg_stat_slru. Готового способа предотвратить переполнение в PostgreSQL 18 нет.

@prog_stuff
👍3
Колонка status в таблице пользователей отвечает ровно на один вопрос: что сейчас. На вопросы «кого отклонили во вторник», «сколько человек провёл в статусе» и «кому после отказа всё-таки одобрили» она не отвечает никак.

Тиаго Араужу Силва показывает альтернативу: отдельная таблица переходов, куда каждое изменение добавляется новой строкой и где ничего не обновляется и не удаляется. Текущий статус — это просто самая свежая запись, то есть запрос к временной шкале, а не хранимое значение.

Дальше идёт разбор, ради которого стоит читать. Флаг current автор отвергает: его придётся сбрасывать и выставлять при каждом переходе, то есть возвращается второй источник истины. MAX(id) тоже не годится — ломается при заливке исторических данных. Правило выражается через дату и идентификатор как устойчивый разделитель при одинаковых отметках времени.

Четыре способа достать последнюю запись сравниваются честно, вплоть до планов: DISTINCT ON на списке пользователей заставляет базу прочитать и отсортировать всю таблицу статусов, а LATERAL JOIN с подходящим индексом читает по одной строке на пользователя.

@prog_stuff
Простой запрос — сумма колонки на 500 миллионов чисел. PostgreSQL считает его 20 секунд, тот же цикл на Rust — 358 миллисекунд. Автор проекта pgrust показывает по шагам, из чего складывается разрыв, собирая уменьшенную копию исполнителя запросов PostgreSQL и добавляя к ней оптимизации по одной.

Исходная модель отдаёт строки по одной, через метод next() у каждого узла плана — так устроен и настоящий PostgreSQL. Это 1,3 секунды. Дальше: выдавать сразу пачку по 1024 значения в буфер на стеке — 480 мс. Слепить сканирование и суммирование в один узел — 358 мс, то есть ровно обычный цикл. Векторные инструкции — 135 мс.

Честная оговорка от автора: слипшийся узел вшит под конкретный запрос, и в общем случае так не выйдет — нужна компиляция плана на лету. А PostgreSQL медленнее ещё и потому, что делает вокруг запроса много другой работы: блокировки, разбор формата хранения.

@prog_stuff
👍2
За неделю сервер отдал 1,28 млн полных страниц, а аналитика насчитала 5977 просмотров: на каждый видимый просмотр приходится около 214 загрузок, которых в статистике нет. Ник Грей воевал со скраперами на сайте из 1,5 млн профилей и описал, что сработало.

22 апреля пришло 3,6 млн запросов с 361 844 IP-адресов, почти все из Китая; Cloudflare за десять часов выдал 1,18 млн проверок. Потом headless Chrome из дата-центров США и сеть домашних IP с Chrome 118–120 образца 2023 года.

Метрика, к которой пришёл автор: страниц на одного приведённого посетителя. Google — 46. Поисковый бот Anthropic за неделю прочитал 420 680 страниц и привёл 12 человек, бот Amazon читал 117 000 страниц в день и не привёл никого; оба заблокированы, первый сбавил с 60 000 запросов в день до 25.

Скрипт JavaScript Detections от Cloudflare стоил 2875 мс на среднем телефоне при собственном JS сайта в 278 мс; без него Lighthouse вырос с 58 до 99. Из 106 437 проверок за двое суток решены 252, то есть 0,24%. Правила WAF в конце.

@prog_stuff
2
В интернете появился репозиторий, опровергающий гипотезу Коллатца на Lean — без единой заглушки, то есть формально проверенный. Оказалось, что это не прорыв в математике, а эксплуатация ошибки в самом проверяющем ядре.

Леонардо де Моура, автор Lean, написал разбор случившегося. Киран Гопинатан свёл находку к короткому доказательству False — то есть в системе можно было доказать что угодно. Ошибка сидела в обработке вложенного индуктивного типа с фиктивными параметрами: параметры терялись во вспомогательном типе и не проверялись. Исправление выпустили примерно через час после минимального воспроизведения.

Отдельно автор объясняет, почему это нельзя закрыть запретами. Внешняя часть языка такой случай ловила, но это не спасает: подсунуть можно и заранее собранный файл, и напрямую построенный терм доказательства. Ядро обязано отвергать плохие объявления само.

Отдельный урок про независимую проверку: сторонний проверяющий на Rust это место проверял, но содержал собственную ошибку и пропускал специально сконструированное выражение. Две реализации помогают, только если обе свежие.

@prog_stuff
🔥1
Servo — попытка написать браузерный движок с нуля на Rust. Проект то замирал, то оживал, и по ежемесячным отчётам хорошо видно, как выглядит эта работа изнутри.

В июньском отчёте — 558 коммитов, снова рекорд, и вчетверо больше, чем на перезапуске в 2023 году. Совместимость измеряется не процентами, а конкретными сайтами: Google Photos работает, Google Maps и OpenStreetMap рисуются, но плохо реагируют на нажатия.

Из внутренних изменений: структура, описывающая прямоугольник в разметке, ужалась с 288 до 240 байт, двумерный холст стал потреблять до 23 процентов меньше энергии, одинаковые векторные картинки перестали растеризоваться повторно.

Отдельная строка — фаззинг. Один участник несколько месяцев гоняет движок случайными данными, и только за июнь по его находкам закрыли шестнадцать падений.

@prog_stuff
❤‍🔥1
Перевести номер дня в день недели — это (rd + 4) % 7 с поправкой на отрицательные числа, и разговаривать вроде не о чем. Бен Джоффе показал, что под капотом это задача про быстрый остаток от деления, и собрал набор функций, которые обгоняют вывод компилятора.

Любимая у автора — три инструкции плюс загрузка константы: imul на (1 << 32) / 7, lea с константой 0x93000000 и сдвиг на 29. Работает на всём знаковом 32-битном диапазоне, а константа в третьей строке по сути задаёт угол поворота: замена одного числа даёт ISO-нумерацию [1..7] вместо [0..6] бесплатно.

Всего собрано 280 вариантов под разные цели: пропускная способность против задержки, x86 против ARM. 8-, 16- и 32-битные версии проверены полным перебором заявленного диапазона, 64-битные — четырьмя блоками по миллиарду дат. По сводке автора на Ryzen 9 и M4 Pro новые функции тратят 0,3–0,5 времени алгоритма Нери 2024 года, который до этого считался пределом; на отдельных сочетаниях функции и платформы разрыв доходит до 5,9 раза. Техника обобщается на x % (2^N − 1), а отдельно выведены быстрые остатки для 24 и 60.

@prog_stuff
Автор впервые всерьёз берётся за быстрое преобразование Фурье и по шагам показывает, что видит.

Берёт три секунды синусоиды в 5 герц, снятой сто раз в секунду, получает 300 комплексных чисел — и первым делом удивляется, почему пиков два, на плюс и минус пяти герцах. Дальше по порядку: нулевая частота это среднее значение сигнала, действительная часть отвечает за косинусную составляющую, мнимая за синусную, спектр зеркален, поэтому обычно рисуют половину.

Самое полезное — про грабли. Если оборвать сигнал не на трёх секундах, а на 2,9, чистый пик размазывается по соседним частотам. Оконная функция это лечит, но добавляет собственную размытость. Импульс в данных поднимает шумовой пол на всех частотах сразу, ступенька собирается у низких, прямоугольная волна даёт нечётные гармоники.

В конце он применяет всё это к годовому ряду почасовых данных с придуманной историей про стройку: в спектре видны годовой тренд, суточный и недельный циклы, а рабочая неделя проявляется гармониками с периодами 3,5, 2,4, 1,4 и 1,2 дня.

@prog_stuff
Видеокарта на 8 ГиБ, игра просит 9 — работает, средний кадр 19,6 мс. Просит 10 — кадр уже 29,8 мс со всплесками за 33,3.

Автор разбирает, что происходит за границей физической видеопамяти в Linux, на уровне amdgpu и TTM. Как только данные начинают ездить через PCIe, бюджет считается в лоб: у PCIe 4.0 x16 меньше 32 ГиБ/с, значит при 30 кадрах в секунду на кадр приходится около 1,075 ГиБ обмена. На RDNA3 обращение через шину оказалось примерно в 7,3 раза медленнее попадания в Infinity Cache и в 4,6 раза медленнее чтения из видеопамяти.

Отдельная беда в буферах вывода на экран: они обязаны лежать физически непрерывно, и ради примерно 32 МиБ такой памяти драйвер выселял до 4 ГиБ, тратя на одну передачу около 130 мс.

Цифры автор просит не считать универсальными: всё зависит от шаблона обращений и конкретной карты, часть работы в ядре ещё не прошла upstream, а экспериментальные ветки для SteamOS он предлагает трогать на свой риск.

@prog_stuff
2
Участник команды Zig описал устройство инкрементальной сборки: компилятор понимает, какие объявления изменились, пересобирает только их и вписывает готовые байты прямо в собранный бинарник. Холодная сборка приложения — 5 секунд, каждая следующая — 50–70 миллисекунд.

Разбор идёт по слоям: разбор файлов кэшируется целиком, промежуточное представление пишется одним системным вызовом без шага сериализации, машинный код вообще не кэшируется, а компоновщик встроен в компилятор — это и позволяет обойти главную сложность инкрементальной компоновки.

На картинке трассировка одного обновления. Обновление заняло 37 мс, из них полезной работы примерно 1,6 мс, а 31 мс ушла на обход графа ссылок, который в этот раз вообще не менялся. Автор пишет, что мог бы это оптимизировать перед публикацией ради красивой цифры, но показывает как есть: видно, сколько запаса лежит на поверхности.

Работает пока только под Linux на x86_64, возможны ложные ошибки компиляции.

@prog_stuff
🤔3
Автор трижды в трёх компаниях строил одну и ту же систему: принять вебхуки от Stripe или провайдера авторизации и держать у себя копию их данных. Каждый раз задача на вечер разрасталась в неделю: проверка подписи, таблица дедупликации, буфер для событий, пришедших не по порядку, стартовый импорт с блокировками и ночной cron сверки. Он разобрал, почему так выходит всегда.

Вебхук — это уведомление «что-то случилось» с доставкой at-least-once, без порядка, без полноты и без способа узнать, что ты что-то пропустил. Для запуска побочного эффекта это идеально, а для репликации данных нет: у автора отмена подписки клиента потерялась по дороге, и база месяцами считала его активным. Провайдер при этом держит у себя упорядоченный лог, режет его на POST-запросы и рассылает, а каждый получатель собирает лог обратно, со своими багами.

Ночной cron автор называет письменным признанием: «я не доверяю копии и буду пересобирать её с нуля каждую ночь». Альтернатива в статье — перевернуть стрелку: один URL на коллекцию, курсор, полное состояние объекта в каждом событии, tombstone на удаление, Prefer: stream для живого потока и checksum состояния в конце. Дедуп, буфер и стартовый импорт исчезают. Stripe и WorkOS уже отдают похожие Events API, но каждый со своей семантикой; автор оформил идею как черновик протокола SCROLL и сам оговаривает, что это предложение, а не стандарт.

@prog_stuff
🔥2
GitHub разобрал аварию 17 августа. Инцидент длился 7 часов 47 минут и задел Issues, пул-реквесты, API, Actions и Copilot; на пике веб и API отдавали примерно 20% ошибок, а скачивание архивов и сырых файлов — около 50%. Большинство сервисов пришло в норму к 16:36 UTC, Actions деградировал примерно до 18:03, а выдача токенов Copilot восстановилась к 21:02.

Началось с того, что sidecar Istio упёрся в предел одновременных запросов и не отмасштабировался: политика следила за лимитами сервиса, но не за лимитами самого sidecar. Отказ пошёл каскадом, четыре узла HAProxy исчерпали лимиты соединений, и сломался путь аутентификации через шлюз.

Дальше вмешались повторные попытки. Часть трафика перевели из Central US в Северную Вирджинию, где задержка ответов вскрыла скрытый баг ретраев в VS Code, и нагрузка выросла примерно в десять раз. Помогло не масштабирование, а обратное: паузу на проблемных узлах HAProxy и блокировка запросов кодом 403 на балансировщиках, после чего трафик возвращали постепенно.

@prog_stuff
❤‍🔥1👌1
Джулия Эванс рассказывает, чему научилась, гоняя небольшой сайт на SQLite. Это не «десять причин выбрать SQLite», а список мест, где она споткнулась.

Полнотекстовый поиск по таблице на четыре тысячи строк вдруг выполнялся пять секунд. Помогла одна команда ANALYZE: планировщик, видимо, случайно свалился в квадратичное поведение. Автор прямо пишет, что уверенно читать план запроса пока не умеет и точную причину не знает.

Массовое удаление старых строк занимало те же пять секунд с лишним, и в это время второй обработчик не мог записать данные и падал по тайм-ауту, утаскивая за собой виртуальную машину. Временное решение — удалять маленькими пачками. Именно на этом месте, пишет она, стало понятно, зачем PostgreSQL вообще разрешает нескольким писателям работать одновременно.

Отдельная история с резервными копиями: VACUUM INTO с архивацией, restic в S3, который иногда падал по памяти, потом Litestream — и снова честное «не знаю, работает ли заданное хранение».

@prog_stuff