Сохранёнки программиста
Ir al canal en Telegram
Заметки и ссылки на будущее, чтобы изучить когда будет время. Разместить рекламу: @tproger_sales_bot Правила общения: https://tprg.ru/rules Другие каналы: @tproger_channels Другие наши проекты: https://tprg.ru/med
Mostrar más6 529
Suscriptores
-124 horas
-157 días
-5630 días
Archivo de publicaciones
Список, который стоит открывать каждый раз, когда садитесь писать что-то с датами: «Заблуждения программистов о времени» Ноа Сассмана. Тридцать четыре утверждения, каждое из которых кажется очевидно верным и каждое неверно. Во второй части их ещё семьдесят девять.
Выборочно: в сутках не всегда 24 часа. Часовой пояс машины не совпадает с поясом пользователя. Часовые пояса меняются политическим решением, а переходы на летнее время не постоянны. Часы клиента и сервера не совпадают. Минута на часах не всегда равна минуте реального времени. Временные метки не обязаны быть уникальными. Время события, время записи в журнал и время получения сообщения — три разных момента.
Любимый пример оттуда: виртуальная машина, приостановленная на два часа, после запуска продолжила считать, что всё ещё час дня.
Вторая часть добирается до високосных секунд, разницы между настенными и монотонными часами и до того, почему
sleep(1000) не значит «ровно секунда».
@prog_stuffСтатический анализатор выдал 147 643 предупреждения о работе с неинициализированной памятью в ядре Linux. Подтвердились и были исправлены 52.
Эта цифра — отправная точка работы группы из Калифорнийского университета в Риверсайде, представленной на OSDI в июле. Проблема известна всем, кто пробовал внедрить анализатор в большой проект: покрытие огромное, а доля настоящих находок такая, что список никто не разбирает.
Идея авторов: проверять каждое предупреждение отдельно, исполняя подозрительный участок кода по-настоящему. Для этого произвольный набор функций на C и C++ собирается в самостоятельный исполняемый файл без правки исходников, а дальше по нему идёт символьное исполнение. Запускать всё ядро или готовить окружение не нужно.
По цифрам: для Linux 6.16.0 удалось собрать 88,9 процента участков, для Android LTS 5.10.240 — 96,2 процента. Разбор одного предупреждения занимал в среднем 0,32 секунды против 5,14 у сравниваемого подхода, а до вердикта доводилось 95,46 процента случаев против 41,29.
@prog_stuff
Телефон переставал играть музыку в Bluetooth-наушниках, как только на компьютере открывалась вкладка AliExpress. Закрыть вкладку — звук возвращается, замьютить вкладку или всю систему — не помогает.
Автор блога laserphile обернул конструктор
AudioContext и нашёл два скрытых аудиоконтекста в состоянии running, подключённых к destination, — при том что ни <audio>, ни <video>, ни вызовов play() на странице нет. Создают их collina.js и fireyejs.js из каталога AWSC, антифрод-обвязки Alibaba.
Граф в обоих одинаковый: пилообразный осциллятор, AnalyserNode, ScriptProcessorNode, GainNode с нулевым усилением, выход в destination. Слышно ничего, но подключение к destination заставляет браузер обсчитывать граф по-настоящему, и аудиопуть остаётся занятым.
Звук здесь одна из мерок отпечатка. Рядом снимаются canvas и toDataURL(), данные WebGL, размеры экрана, hardwareConcurrency, поведение WebRTC, события мыши и скролла, показания акселерометра.
@prog_stuffИдея, которую в девяностых довели до рабочего состояния, а потом почти все забыли: файловая система, которая одновременно является базой данных.
Даниэль Козенца разбирает Be File System — ту, что досталась Haiku от BeOS. У файла там есть не только имя и содержимое, но и типизированные именованные атрибуты: у аудиофайла исполнитель и альбом, у письма отправитель и статус. По выбранным атрибутам строятся индексы, к тому можно писать предикаты, а живые запросы шлют приложению сообщение, когда подходящий файл появился, исчез или изменился. Почтовый клиент, который просто показывает результат запроса к файловой системе, — это оттуда.
Автор при этом честно очерчивает границы. Индекс принадлежит конкретному тому: наличие его на загрузочном диске ничего не говорит про соседний. Результат запроса не вечен — файл могут переименовать или удалить. Соединений таблиц, ссылочной целостности и транзакций над несколькими записями тут нет.
И главная ловушка: при переименовании атрибуты следуют за файлом, а вот при копировании наружу как повезёт — архиваторы и средства переноса ведут себя по-разному, и на файловой системе без поддержки атрибутов они просто теряются. Байты остались, смысл потерялся.
@prog_stuff
Час, который стоит потратить: доклад Рича Хики «Simple Made Easy» со Strange Loop 2011.
Весь доклад держится на разведении двух слов, которые в русском тоже слиплись. Простое — это то, что не переплетено с другими вещами, свойство самой конструкции. Лёгкое — это то, что близко и знакомо лично вам, свойство вашего опыта. Выбирая лёгкое, команда набирает сложность, которую потом невозможно распутать.
Хики вводит слово complecting — сплетать вместе то, что могло бы жить раздельно. Изменяемая переменная сплетает значение и время. Наследование сплетает тип и реализацию. Объект сплетает данные и поведение. Всё это удобно писать и тяжело менять через полгода.
Отдельный удар по привычным успокоительным: тесты, рефакторинг и система типов повышают безопасность, но не делают дизайн проще. Они ловят ошибки, а не распутывают связи.
Практическое, что можно унести на завтра: при проектировании развести вопросы «что», «кто», «как», «когда», «где» и «почему» и следить, чтобы в одном месте не отвечали сразу на несколько.
@prog_stuff
Микаэль Лагерквист собирал корпус из 434 201 судоку для экспериментов с программированием в ограничениях, потом захотел в них поиграть — и сделал игровые версии девяти типов головоломок, где все задачи сгенерированы решателем.
Общий приём один: начать с готового решения или картинки, а потом добавлять, двигать и удалять подсказки, пока решение не станет единственным. Для судоку это выглядит так: сгенерировать полную таблицу, убирать подсказки и каждый раз проверять уникальность.
Сложность судоку определяется не числом подсказок, а самым слабым набором правил распространения ограничений, которого хватает решателю без перебора вариантов. У других головоломок свои измерения, и метки сравнимы только внутри одного типа.
Японские кроссворды описаны регулярным выражением вида
Empty* Filled{3} Empty+ Filled{2} Empty*, которое среда сама превращает в конечный автомат. Замкнутая линия в Loopy выражается через готовое ограничение на подциклы, путь в Zip — через ограничение на гамильтонов цикл.
И приятная деталь: браузер решателя не запускает вовсе, ему отдают заранее сгенерированные головоломки вместе с решениями.
@prog_stuffПосле одной перенастройки контроллера памяти
&x != &x: два разных адреса начинают указывать в одну ячейку DRAM, притом что таблицы страниц, TLB и физические адреса, которые видит ядро, никто не трогал.
Автор показывает на AMD Family 16h, что финальное преобразование адреса живёт уже за пределами всего, что защищает операционная система. Он проходит цепочку целиком: MMU, EPT и NPT, IOMMU, кэши, когерентная шина, контроллер памяти и только потом чередование по каналам, рангам и банкам. Один переключатель в конфигурации bank-swizzle схлопывает разные адреса в одну физическую координату.
Само преобразование не документировано, поэтому автор восстанавливает его как линейное отображение над GF(2) и решает систему линейной алгеброй и Z3. Дальше в README идут доступ к областям PSP, SMM и SMRAM, к C6 save area и микрокоду.
Оговорка у автора честная: почти всё проверено на Family 16h, для 17h и новее нужные регистры уже не документированы, а сама документация AMD неполна — порядок XOR-преобразований и стадии MMIO зависят от модели. Переносимость на другие поколения он не обещает.
@prog_stuffВ BGP есть поле ORIGIN: оно говорит, как маршрут попал в протокол, и по стандарту менять его не должен никто, кроме того, кто маршрут объявил. Cloudflare проверили, как это соблюдается, и обнаружили, что примерно у 70 процентов наблюдаемых путей значение отличается от исходного.
Мотив денежный. При выборе лучшего пути маршрутизатор смотрит на ORIGIN рано, ещё до сравнения других признаков, и предпочитает меньшее значение. Переписав поле в самое приоритетное, транзитный провайдер повышает шанс, что трафик — и оплата за него — пойдёт через него.
Эксперимент простой: объявить шесть блоков адресов с разными значениями и посмотреть, что дойдёт до публичных коллекторов. Переписывают немногие, 64 системы из 606 разобранных, но это в основном крупные игроки: шесть из шестнадцати сетей верхнего уровня.
Вывод авторов радикальный: осмысленной роли у этого поля в современном интернете не осталось.
@prog_stuff
На iOS браузеры за пределами ЕС обязаны работать на WebKit, а прокси-браузеры вроде Tor-клиентов настраивают прокси через
WKWebsiteDataStore.proxyConfigurations: весь трафик страницы должен уходить через прокси. Исследователи Mysk нашли три функции WebKit, которые эту настройку обходят и ходят в сеть напрямую с устройства.
<link rel="dns-prefetch"> резолвит имя через системный DNS, а не через прокси: сайт вставляет уникальное имя на посетителя и смотрит, с какого резолвера прилетает запрос. WebAuthn Related Origin Requests заставляет системный сервис учётных данных скачать файл проверки напрямую, раскрывая реальный IP. WebTransport поднимает прямое HTTP/3-соединение мимо прокси. Функции появились в iOS 26.0, 18.0 и 26.4 соответственно.
Всё это происходит вне обычного цикла загрузки страницы, поэтому утечки задевают и iCloud Private Relay; VPN не затронуты, они тянут трафик на уровне системы. Началось с одного бага от пользователя, у которого DNS утекал только на некоторых сайтах: без тега prefetch запроса просто не было. Авторы делают собственный браузер Psylo: в 1.3.1 prefetch блокируется, а WebTransport и WebAuthn выключены по умолчанию и включаются отдельной настройкой на сайт. PoC лежит на leaks.psylo.app.
@prog_stuffЕсть характеристика процессора, которой нет ни в одной спецификации, а на скорость программы она влияет сильнее модных цифр: сколько промахов мимо кэша ядро способно держать одновременно.
Поход в оперативную память стоит около 100 наносекунд — примерно 300 тактов простоя. Задержка эта за десять лет не улучшилась, а ухудшилась. Спасает то, что ядро не ждёт первый ответ, а успевает отправить следующие запросы.
Даниэль Лемир измерил, сколько именно, гоняя погоню по указателям в массиве на гигабайт: сначала одну цепочку, потом всё больше независимых, пока добавление новой не перестаёт помогать. У Intel предел вырос с 10 до 30 запросов, у AMD — с 15 до 58, у Graviton — с 6 до 19.
Практический смысл: структура с длинными цепочками указателей упирается не в задержку памяти, которая почти не меняется, а вот в этот предел. И он у разных процессоров различается в разы.
@prog_stuff
Инженер из Тринидада и Тобаго объясняет, почему спор об элегантности набора инструкций выглядит совсем иначе, когда доставка микросхемы за доллар стоит от 60 до 200 долларов.
Весь его аргумент про доступность железа. CH32V003 стоит десять центов: RV32EC, 16 регистров, без умножения и деления, 2 КБ SRAM и 16 КБ флеша, только machine mode. На другом конце того же RISC-V у него CH32H417 с двумя ядрами на 400 и 144 МГц, 896 КБ SRAM, 960 КБ флеша, USB 3.2 Gen1 и стомегабитным Ethernet, а ещё дальше VexRISC-V с MMU, на котором запускают Xous, seL4 и Linux. Одна база набора команд и один toolchain на всём этом пути, тогда как ARM разводит Cortex-M и Cortex-A по разным профилям и лицензиям.
С критикой RISC-V по существу автор не спорит: фрагментацию расширений и неудобства вокруг Zcb и Zicsr он признаёт сам и решения комитета не защищает. Это личный инженерный ответ с примерами собственных проектов, а не нейтральное сравнение архитектур.
@prog_stuff
Джулия Эванс рассказывает, чему научилась, гоняя небольшой сайт на SQLite. Это не «десять причин выбрать SQLite», а список мест, где она споткнулась.
Полнотекстовый поиск по таблице на четыре тысячи строк вдруг выполнялся пять секунд. Помогла одна команда
ANALYZE: планировщик, видимо, случайно свалился в квадратичное поведение. Автор прямо пишет, что уверенно читать план запроса пока не умеет и точную причину не знает.
Массовое удаление старых строк занимало те же пять секунд с лишним, и в это время второй обработчик не мог записать данные и падал по тайм-ауту, утаскивая за собой виртуальную машину. Временное решение — удалять маленькими пачками. Именно на этом месте, пишет она, стало понятно, зачем PostgreSQL вообще разрешает нескольким писателям работать одновременно.
Отдельная история с резервными копиями: VACUUM INTO с архивацией, restic в S3, который иногда падал по памяти, потом Litestream — и снова честное «не знаю, работает ли заданное хранение».
@prog_stuffGitHub разобрал аварию 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
Автор трижды в трёх компаниях строил одну и ту же систему: принять вебхуки от Stripe или провайдера авторизации и держать у себя копию их данных. Каждый раз задача на вечер разрасталась в неделю: проверка подписи, таблица дедупликации, буфер для событий, пришедших не по порядку, стартовый импорт с блокировками и ночной cron сверки. Он разобрал, почему так выходит всегда.
Вебхук — это уведомление «что-то случилось» с доставкой at-least-once, без порядка, без полноты и без способа узнать, что ты что-то пропустил. Для запуска побочного эффекта это идеально, а для репликации данных нет: у автора отмена подписки клиента потерялась по дороге, и база месяцами считала его активным. Провайдер при этом держит у себя упорядоченный лог, режет его на POST-запросы и рассылает, а каждый получатель собирает лог обратно, со своими багами.
Ночной cron автор называет письменным признанием: «я не доверяю копии и буду пересобирать её с нуля каждую ночь». Альтернатива в статье — перевернуть стрелку: один URL на коллекцию, курсор, полное состояние объекта в каждом событии, tombstone на удаление,
Prefer: stream для живого потока и checksum состояния в конце. Дедуп, буфер и стартовый импорт исчезают. Stripe и WorkOS уже отдают похожие Events API, но каждый со своей семантикой; автор оформил идею как черновик протокола SCROLL и сам оговаривает, что это предложение, а не стандарт.
@prog_stuffУчастник команды Zig описал устройство инкрементальной сборки: компилятор понимает, какие объявления изменились, пересобирает только их и вписывает готовые байты прямо в собранный бинарник. Холодная сборка приложения — 5 секунд, каждая следующая — 50–70 миллисекунд.
Разбор идёт по слоям: разбор файлов кэшируется целиком, промежуточное представление пишется одним системным вызовом без шага сериализации, машинный код вообще не кэшируется, а компоновщик встроен в компилятор — это и позволяет обойти главную сложность инкрементальной компоновки.
На картинке трассировка одного обновления. Обновление заняло 37 мс, из них полезной работы примерно 1,6 мс, а 31 мс ушла на обход графа ссылок, который в этот раз вообще не менялся. Автор пишет, что мог бы это оптимизировать перед публикацией ради красивой цифры, но показывает как есть: видно, сколько запаса лежит на поверхности.
Работает пока только под Linux на x86_64, возможны ложные ошибки компиляции.
@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
Автор впервые всерьёз берётся за быстрое преобразование Фурье и по шагам показывает, что видит.
Берёт три секунды синусоиды в 5 герц, снятой сто раз в секунду, получает 300 комплексных чисел — и первым делом удивляется, почему пиков два, на плюс и минус пяти герцах. Дальше по порядку: нулевая частота это среднее значение сигнала, действительная часть отвечает за косинусную составляющую, мнимая за синусную, спектр зеркален, поэтому обычно рисуют половину.
Самое полезное — про грабли. Если оборвать сигнал не на трёх секундах, а на 2,9, чистый пик размазывается по соседним частотам. Оконная функция это лечит, но добавляет собственную размытость. Импульс в данных поднимает шумовой пол на всех частотах сразу, ступенька собирается у низких, прямоугольная волна даёт нечётные гармоники.
В конце он применяет всё это к годовому ряду почасовых данных с придуманной историей про стройку: в спектре видны годовой тренд, суточный и недельный циклы, а рабочая неделя проявляется гармониками с периодами 3,5, 2,4, 1,4 и 1,2 дня.
@prog_stuff
Перевести номер дня в день недели — это
(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_stuffServo — попытка написать браузерный движок с нуля на Rust. Проект то замирал, то оживал, и по ежемесячным отчётам хорошо видно, как выглядит эта работа изнутри.
В июньском отчёте — 558 коммитов, снова рекорд, и вчетверо больше, чем на перезапуске в 2023 году. Совместимость измеряется не процентами, а конкретными сайтами: Google Photos работает, Google Maps и OpenStreetMap рисуются, но плохо реагируют на нажатия.
Из внутренних изменений: структура, описывающая прямоугольник в разметке, ужалась с 288 до 240 байт, двумерный холст стал потреблять до 23 процентов меньше энергии, одинаковые векторные картинки перестали растеризоваться повторно.
Отдельная строка — фаззинг. Один участник несколько месяцев гоняет движок случайными данными, и только за июнь по его находкам закрыли шестнадцать падений.
@prog_stuff
В интернете появился репозиторий, опровергающий гипотезу Коллатца на Lean — без единой заглушки, то есть формально проверенный. Оказалось, что это не прорыв в математике, а эксплуатация ошибки в самом проверяющем ядре.
Леонардо де Моура, автор Lean, написал разбор случившегося. Киран Гопинатан свёл находку к короткому доказательству
False — то есть в системе можно было доказать что угодно. Ошибка сидела в обработке вложенного индуктивного типа с фиктивными параметрами: параметры терялись во вспомогательном типе и не проверялись. Исправление выпустили примерно через час после минимального воспроизведения.
Отдельно автор объясняет, почему это нельзя закрыть запретами. Внешняя часть языка такой случай ловила, но это не спасает: подсунуть можно и заранее собранный файл, и напрямую построенный терм доказательства. Ядро обязано отвергать плохие объявления само.
Отдельный урок про независимую проверку: сторонний проверяющий на Rust это место проверял, но содержал собственную ошибку и пропускал специально сконструированное выражение. Две реализации помогают, только если обе свежие.
@prog_stuff