JSON умеет считать сам себя — удобно, как в Excel, но без вранья с типами

JSON, который считает сам себя. Ставишь = в ячейку — и она становится формулой. На диске при этом остаётся обычный JSON, который откроет кто угодно.

JSON, который считает сам себя. Ставишь = в ячейку — и она становится формулой. На диске при этом остаётся обычный JSON, который откроет кто угодно.

Всем привет!
В мае я опубликовал на Хабре первую статью о MarkMello (легкий MD-просмотрщик). Изначальная задача была простой: сделать лёгкое приложение, которое быстро открывает локальный Markdown-файл сразу в режиме чтения.
Хочу рассказать вам о развитии этого проекта, во что он превратился на сегодняшний день, а так же поблагодарить всех контрибьюторов за участие проекте.

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

Одни вызовы после OrderBy проходят набор один раз, другие приводят к полной сортировке. По коду разницы не видно.
Давайте проверим счётчиком обращений к компаратору — на четырёх машинах и четырёх рантаймах.

Перегрузка Span.Sort с компаратором-структурой должна была работать быстрее обычной. Замер показал обратное: памяти она расходует больше всех, а времени тратит больше, чем компаратор-класс — на .NET 8, 9 и 10.
В .NET 11 результат меняется, но не везде.

Один try/finally внутри метода — и он работает в разы медленнее. Сам блок тут ни при чём, он не выполняет ни одной лишней инструкции.
Причина в решении JIT. В .NET 10 его поменяли — но не для всех методов и не при любых настройках.

Chunk делит коллекцию на массивы — одна строка кода. Но есть размер чанка, после которого он замедляется в разы при тех же данных и той же памяти. А на массиве и на List<int> внутри разный код, и в самой строке этого не видно.

StringBuilder делит текст на чанки по 8000 символов — вчетверо ниже порога кучи больших объектов. Запас такой, что попасть туда невозможно.
Но чанк на 400 КБ в этой куче возможен и получить его можно разными способами.

Создаю собственный DSL на C#: в этой части добавим инструменты диагностики. Построим инспектор дерева компонентов, научимся изменять состояния и параметры прямо во время работы приложения, подключим метрики через System.Diagnostics и настроим #line, чтобы исключения указывали на исходный .akbura-файл, а не на сгенерированный C#.

ArrayPool используют ради экономии на аллокациях. Но есть размеры, где он делает обратное: массив уходит в LOH, а через new остаётся в нулевом поколении.
Один такой размер зашит в .NET по умолчанию — им копируются потоки. Проверил на четырёх машинах и трёх рантаймах.

ZLinq — замена LINQ без аллокаций. На .NET 8 и .NET 9 время одинаковое. На .NET 10 иначе: массив тот же, но если параметр объявлен как IEnumerable, foreach перебирает его в 2,58–3,89 раза быстрее ZLinq. Причина видна в машинном коде, память замерена отдельно.

В документации к PropertyNameCaseInsensitive есть предупреждение про накладные расходы, но не сказано, когда они появятся. Замерил на четырёх машинах, трёх рантаймах и трёх размерах входного JSON: пока через настройки идёт один регистр ключей, флаг не добавляет ничего — 0,92–1,09. Политика именования camelCase тоже.
Разницу до ×4,3 даёт другое: какие ещё написания этих ключей прошли через настройки раньше. Два экземпляра JsonSerializerOptions, созданные через new с одинаковыми полями, делят один кеш имён.
Внутри: пять историй с таблицами по четырём машинам, листинги из dotnet/runtime, предел кеша в 64 записи и веб-настройки, на которых эта разница не видна.

В прошлой статье я рассказывал, как мы делаем онлайн IDE для .NET — дизайн, компиляция и запуск приложений происходят прямо в браузере, без сервера. Проект живёт по адресу xaml.io. С тех пор мы много чего добавили, и в какой-то момент у нас стало получаться загружать в IDE довольно крупные .NET проекты — десятки XAML, сотни C# файлов, несколько референсных проектов и пакетов NuGet.
Одна из главных возможностей — это визуальный UI-дизайнер пользовательских приложений. И у него есть особенность: чтобы дизайнер вообще что-то показывал, приложение должно быть скомпилировано, причём в актуальном состоянии. Не «когда-нибудь», а после каждой правки.
Открыли как-то в IDE один реальный проект (FamilyShow — классическое демонстрационное WPF-приложение), поправили одну строку в .cs и 84 секунды наблюдали за анимацией прогресса. С таким UX никакой интерактивный дизайн невозможен.
Сейчас та же правка занимает 5 секунд, а правка XAML-разметки на типичной странице — меньше секунды. В этой статье — как мы к этому пришли, и какие сюрпризы нам приготовили по дороге Roslyn, OpenSilver, WASM и наш собственный код.

Проверить строку регулярным выражением в .NET можно по-разному, но совет всегда один: включите RegexOptions.Compiled или возьмите генератор. Замеры это подтверждают.
А вот подготовка выражения оказалась не там, где её меряют: конструктор занимает 10 микросекунд, а первая же проверка на этом объекте — ещё 1307. Разобрался, куда уходит разница, и заодно нашёл границу, на которой статический Regex.IsMatch резко замедляется.

FrozenDictionary сделан под словари, которые заполняют один раз при старте, а дальше только читают. Замерил на четырёх машинах и трёх рантаймах — разброс вышел от ×0,76 до ×1,9, и зависит он не от версии .NET, а от того, сколько ключей и как они устроены.
Разобрал, куда уходит время в поиске по строке, вытащил из рантайма имена реализаций, которые он подбирает под конкретный набор ключей, и посмотрел в дизасме, за счёт чего Frozen выигрывает — и почему на маленьких наборах проигрывает.

Структура в роли ключа Dictionary — частый случай. Забыл IEquatable — и каждый TryGetValue в 4,4–5,7 раза медленнее, плюс 96 байт в куче на каждый поиск.
Проверил на четырёх машинах и трёх рантаймах, снял дизасм FindValue и разобрал, откуда берутся ровно три упаковки, почему override Equals не спасает и при чём тут хэш record struct. Сам рантайм этот разрыв не закроет — и это не баг.

Если искать в интернете «мощный DI-контейнер для .NET», почти в каждом списке встретится Autofac. У него заслуженная репутация зрелого и функционального контейнера. На этом фоне Pure.DI иногда воспринимают как узкоспециализированный генератор, который выигрывает в скорости, но должен уступать классическому DI-контейнеру по возможностям.
Автор объясняет, как он использовал скрипты Roslyn и режим сокетов Slack для проверки и отладки запущенного приложения и взаимодействия с ним.

«Лучший ChatGPT за $100», «китайцы обучили GPT-4 за шесть миллионов», «модель с нуля на одной игровой карте за вечер» — за последний год такие заголовки идут потоком. Почти все цифры в них честные. Беда в том, что они про разные вещи.
За словами «обучить LLM» прячется лестница в семь порядков: от десятков долларов на карте под столом до миллиарда на фронтире. На каждой ступени за деньги покупают разное, и перепутать ступени дорого. Я работаю CTO ML-команды. Расчет «взять открытые веса / дообучить / обучить с нуля» мы проделываем регулярно, и почти всегда он заканчивается не там, где ждет заказчик.
Ниже — вся лестница с числами и первоисточниками: что реально обучить на RTX 5090, где начинается аренда кластера, из чего складывается смета фронтир-модели и почему «модель за $6 млн» и «компания с инфраструктурой на миллиард» — правда одновременно.

Pure.DI — это генератор кода для внедрения зависимостей (Dependency Injection), который работает на этапе компиляции. Pure.DI развивает идею «чистого DI»: вместо контейнера и рефлексии вы получаете обычный C#‑код, который создаёт композиции объектов. В этой статье — новые возможности из релизов 2.3.5–2.5.1: от union types в роли DI‑контрактов до сценариев внедрения зависимостей без лишних аллокаций.