.NET epeshk blog @epeshkblog Channel on Telegram

.NET epeshk blog

@epeshkblog


Канал с заметками о C# и .NET

Поддержать канал: https://t.me/blog_donate/2
Обратная связь: https://forms.gle/3uRz7FmzUA26Kw4y5

.NET epeshk blog (Russian)

Вы занимаетесь программированием на C# и .NET? Тогда канал ".NET epeshk blog" идеально подойдет вам! Здесь вы найдете заметки, советы и полезные материалы по этим темам. Разбираясь в деталях разработки на C# и во всем, что связано с платформой .NET, вы сможете значительно улучшить свои навыки и стать более эффективным специалистом. Кроме того, канал предлагает возможность поддержать его, перейдя по ссылке https://t.me/blog_donate/2. Здесь вы также можете оставить свои комментарии, предложения и отзывы, чтобы канал был еще более полезным и интересным для вас. Присоединяйтесь к .NET epeshk blog и станьте частью сообщества разработчиков, которые стремятся к постоянному росту и совершенствованию своих навыков!

.NET epeshk blog

14 Jan, 19:08


⚰️ FluentAssertions

Начиная с версии 8 стал платной для коммерческого использования библиотекой за ~$130 на юзера

.NET epeshk blog

24 Dec, 11:47


Быстрый и экономный xlsx #память #скорость

Наш сервис формирует отчёты в формате xlsx (Excel). Отчёты скачиваются часто и активно, некоторые из них могут быть достаточно большие (> 50 Мб). Для формирования xlsx мы использовали EPPlus, который на тот момент знали лучше. Функционал был реализован, что очень обрадовало заказчика.

Однако, мы были не очень рады. Во-первых, бесплатный EPPlus давно не обновлялся (последний коммит аж 4 года назад). Во-вторых он потреблял много памяти, что иногда приводило к OutOfMemoryException. В принципе, мы знали с чем имеем дело, поэтому подготовились заранее - написали собственную "обёртку" вокруг формирования xlsx, чтобы иметь возможность перейти на другую библиотеку в будущем.

Месяц назад этот момент настал. Да, до этого мы уже делали попытки изучить библиотеки, которые, в перспективе, могут дать буст производительности и снизить аллокацию. Увы, некоторые не подходили нам по возможностям стилизации ячеек excel-файла.

Изучив несколько известных библиотек (ClosedXML и Open XML) мы сделали замеры - увы, эти библиотеки хоть и были лучше бесплатного EPPlus, но не давали нужного прироста экономии памяти и производительности. К этому моменту мы предприняли попытку самостоятельного написания библиотеки для формирования xlsx и уже примерно представляли, что с использованием современных подходов C# можно лучше.

Написание собственной библиотеки остановилось, когда мы нашли SpreadCheetah. Его результаты вы наблюдаете на бенчмарке. Спустя пару-тройку недель мы перешли на него и выкатились в PROD. В принципе, результаты нас устраивают.

Выводы:
1. Современный .NET может быть очень очень быстрым.
2. Если у вас проблемы с производительностью, то попробуйте найти готовое решение, прежде чем писать свой велосипед.
3. Писать велосипед полезно, чтобы знать ключевые слова для поиска и референсные значения производительности.

P.S.: Бенч в другом месте - слишком много кода. Туда же выложил результаты. Либы из бенчмарка в комментах.

.NET epeshk blog

12 Nov, 19:54


🎉 .NET 9

https://devblogs.microsoft.com/dotnet/announcing-dotnet-9/

https://dotnet.microsoft.com/en-us/download/dotnet/9.0

Вышел .NET 9

В этот релиз вошла фича, придуманная в этом блоге: метод CountBy в LINQ

.NET epeshk blog

11 Nov, 15:04


из мира дотнета: столько лет в rabbitmq клиенте sync over async было и тут видимо под 4.0 rabbitmq переписали и клиента, хорошие новости :)

.NET epeshk blog

08 Nov, 08:39


🤡 Clown Object Heap — очередная новая куча в .NET 9 для вопросов на собеседованиях

Объекты в .NET делятся по четырём кучам. С двумя старыми приходится работать постоянно, про две относительно новые обычно вспоминают только на собесах (зачем про них спрашивают?)

Small Object Heap — обычная куча для объектов, разделённая на 3 поколения — нулевое, первое, и второе. С каждой сборкой мусора, выживший объект продвигается в следующее поколение. Сборки мусора в нулевом поколении быстрые и частые, во втором — медленные, блокирующие, редкие. Во время блокирующих сборок мусора содержимое SOH компактится (живые объекты копируются подряд, чтобы между ними не было дырок и память использовалась эффективно, ссылки на эти объекты обновляются, для этого и нужна блокировка потоков)

Large Object Heap — куча для больших объектов. Отличается тем, что по-умолчанию не компактится, т.к. копировать большие объекты долго. Иногда LOH считают абсолютным злом, но без него GC паузы при сборке мусора второго поколения стали бы совсем неприличными.

Основная проблема, связанная с LOH — фрагментация. Если аллоцировать короткоживущие объекты в LOH, особенно если размер объектов разный, после сборок мусора в LOH будут оставаться дырки. Далее окажется, что эти дырки слишком маленькие, чтобы заполнить их новыми объектами и программа начнёт раздуваться по памяти. Обычно проблема решается переиспользованием объектов

- в LOH попадают объекты размером >= 85000 байт
- в основном в LOH хранятся массивы, т.к. другие большие объекты в реальности не встречаются. Тем не менее, вручную можно сделать объект-не массив, который попадёт в LOH:
[StructLayout(LayoutKind.Sequential, Size = 84977)]
class LOHject { int _; }

Console.WriteLine(GC.GetGeneration(new LOHject())); // 2

- в доисторическом фреймворке и 32-битном рантайме в LOH также попадают массивы double от 1000 элементов. Сделано этого для оптимизации работы с double на 32-битных системах, т.к. объекты в LOH на них выравнены по границе 8 байт. Этот факт полезен только на собеседованиях и квизах на конференциях
- можно попросить сборщик мусора скомпактить LOH:
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;

- При запуске программы с memory limit LOH не может употребить всю доступную память. OutOfMemory случится до достижения лимита
- Популярные причины аллокаций в LOH: промежуточные коллекции, создаваемые внутри Linq методов, MemoryStream

Pinned Object Heap (.NET 5) — куча, объекты в которой сборщик мусора гарантированно никогда не перемещает. В ней нет аналога CompactOnce и нет требований к размеру объекта. Нужна для массивов, указатели на которые передаются в нативный код.

Аллокация подобных объектов в обычной куче и их пиннинг (запрет на перемещение) через fixed или GCHandle создаёт проблемы для компактящего сборщика мусора. Поэтому для них сделали отдельную кучу.

Также ходят легенды, что доступ к массиву через указатель без bound checks (проверок на выход за границу массива) быстрее, но это скорее легенда, приводящая к багам, чем реальность.

Через публичный API в Pinned Object Heap можно создавать только массивы:
GC.AllocateArray<int>(128, pinned: true);
GC.AllocateUninitializedArray<int>(128, pinned: true);


Объекты в POH собираются сборщиком мусора, и при неаккуратном использовании POH может стать фрагментированным.

Frozen Object Heap (NonGC Object Heap. .NET 8) — куча для объектов, которые живут всё время жизни программы, никогда не собираются сборщиком мусора, не ссылаются на объекты вне FOH, и никогда не изменяются. В ней хранятся объекты, создаваемые рантаймом дотнета: строковые литералы, объекты Type, Array.Empty<>(), ... Публичного API для создания объектов в FOH нет. Нужна для оптимизаций — 1) GC не просматривает эти объекты, 2) JIT может генерировать более эффективный код, зная что объект никуда не переместится, не изменится, и что о новых ссылках на него не нужно сообщать сборщику мусора

@epeshkblog

.NET epeshk blog

03 Nov, 14:12


А подробнее про попытку внедрить green threads в CLR и будущее async рассказал на DotNext и SpbDotNet: https://www.youtube.com/watch?v=wCZE8f-fmFc&list=PLbxr_aGL4q3QUNRtZjlDArZeTvYB_qp0v

Краткий вывод: ждём async2, который будет не синтаксическим сахаром, а частью рантайма. Но зарелизится это счастье точно не в .NET 10

.NET epeshk blog

03 Nov, 11:20


Green threads & async2

В JVM заехал Project loom — реализация асинхронности на основе virtual threads. Она отличается от привычного нам async/await тем, что синхронный код не отличается от асинхронного вообще, а способ его работы выбирается в рантайме, в зависимости от того, запущен он на обычном или виртуальном потоке.

.NET epeshk blog

25 Oct, 11:22


Бесплатный Rider

https://blog.jetbrains.com/blog/2024/10/24/webstorm-and-rider-are-now-free-for-non-commercial-use/

JetBrains выпустили бесплатную версию .NET IDE Rider для некоммерческого использования.

Теперь у Visual Studio на одно конкурентное преимущество меньше

.NET epeshk blog

16 Aug, 13:06


RuntimeHelpers.GetHashCode

Как известно, если объекты равны, и методы Equals/GetHashCode определены корректно, то и их хэш-коды равны. a.Equals(b) => a.GetHashCode() == b.GetHashCode().

Объекты можно сравнивать по-разному, поэтому и хэш-кодов у объектов в .NET несколько.

Первый хэш-код хранится в заголовке объекта (или в sync block, если на объекте ещё и берётся лок). Он не меняется на протяжении жизни объекта, даже если его переместит GC при compaction кучи (именно из-за такой возможности нельзя использовать адрес в памяти как хэш-код). Если GetHashCode не переопределён, то возвращается это значение.

Второй хэш-код появляется, если переопределён метод GetHashCode. Этот хэш-код всегда вычисляется заново при вызове метода (объекты ведь могут быть мутабельными). И в том числе, для строк, которые могут быть длинными. Про это стоит помнить при использовании строк в качестве ключей хэш-таблиц.

Если GetHashCode переопределён, то первый хэш-код из заголовка объекта все равно можно извлечь — используя метод RuntimeHelpers.GetHashCode(object). Иногда это полезно, например так оптимизирован кэш message template в Serilog, в котором ключи — константные строки.

Другие хэш-коды берутся из реализаций IEqualityComparer. Например, у строк будут разные хэш-коды для StringComparer.Ordinal/OrdinalIgnoreCase/InvariantCulture/.... Эти хэш-коды тоже вычисляются заново при вызове GetHashCode.

Для структур хэш-кода первого типа нет, т.к. у структур нет заголовка объекта. В итоге хэш-код пересчитывается всегда, причём неэффективным способом. В целом, стандартные реализации Equals/GetHashCode для структур настолько плохи, что лучше всегда реализовывать IEquatable<T>, или сразу использовать record.

@epeshkblog
🚀💲 Поддержать канал 💲 🚀

.NET epeshk blog

16 Aug, 11:27


Интересно, почему by design сделали возможность залочиться на Lock, как на обычный объект с помощью Monitor?

Если убрать возможность написать lock ((object)_lock) { } после релиза .NET 9 — будет breaking change. Неужели ассерт на тип объекта, переданного в Monitor.Enter настолько дорогой?

Почему хотя бы не сделали подобный код ошибкой компиляции, а не warning-ом? Специально ради новых вопросов для собесов?

.NET epeshk blog

16 Aug, 10:13


System.Threading.Lock

В .NET 9/C# 13 появился новый примитив синхронизации System.Threading.Lock — managed-реализация блокировки на основе spin-wait + AutoResetEvent.

Теперь вместо блокировки вида object o = new object(); lock (o) { ... }, предлагается писать так:

Lock @lock = new Lock();

using (@lock.EnterScope()) { ... }

// или, также поддержана конструкция lock
lock (@lock) { ... }


Поводом для добавления нового типа стало желание упростить реализацию лока.

lock на основе нативного Monitor использует то же поле из заголовка объекта, что и хэшкод — это приводит к багам и проблемам с производительностью. Вспоминается, однажды баг в локе привёл и к падению рантайма, увы, не нашел сейчас этот issue.

Однако, новый Lock, в том виде, в каком его планируется добавить в .NET 9, не выглядит проработанным до конца. В основном его проблемы связаны с тем, что было решено поддержать lock statement для нового типа.

1. Разные локи

Объект Lock можно присвоить переменной типа object. Вот так:
private object _sync = new Lock();

В таком случае lock(_sync) будет использовать Monitor lock вместо managed lock. Выходит, что под новый Lock может зайти два потока сразу — один честно, через managed блокировку, второй — ошибочно, через Monitor.

Чтобы избежать этого бага предусмотрен warning компилятора. Если решите использовать Lock, рекомендую сразу включить <WarningsAsErrors>CS9216</WarningsAsErrors>. Warning отлавливает не все варианты скастить Lock к object, например можно скастить через generic-метод вида object ToObject<T>(T obj) => obj;.
CS9216 : A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking in 'lock' statement.


2. Поддержка других видов блокировок в `lock`

Изначально планировалась возможность расширения конструкции lock произвольными типами. Например — lock(SpinLock), lock (Semaphore), lock(MyDistributedLock)... — любые типы, имеющие методы Enter/Exit, или специальный интерфейс для блокировки. Но в финальную версию вошла поддержка только типа System.Threading.Lock, а расширяемость оставлена на будущее.

При желании можно обмануть компилятор и всё же подсунуть ему свою реализацию (также можно сделать и с методами класса Monitor
namespace System.Threading {
public class Lock {
public Scope EnterScope() {
Console.WriteLine("EnterScope");
return default;
}

public ref struct Scope {
public void Dispose() => Console.WriteLine("Dispose");
}
}
}


3. Отсутствие аналогов Monitor.Wait/Pulse/PulseAll

Оставлено на будущее. API proposal: ConditionVariable

==========

Итого, чтобы избежать багов с новым типом Lock лучше сразу включить <WarningsAsErrors>CS9216</WarningsAsErrors>, до того, как кто-нибудь попробует им воспользоваться.

API proposal: https://github.com/dotnet/runtime/issues/34812
C# 13 release notes: https://devblogs.microsoft.com/dotnet/csharp-13-explore-preview-features/#lock-object
C# language proposal (наиболее подробный):
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/lock-object
https://github.com/dotnet/csharplang/blob/main/proposals/lock-object.md

@epeshkblog
🚀💲
Поддержать канал 💲 🚀

.NET epeshk blog

09 Aug, 07:03


Очень интересная статья про AVX512 - в частности как он был реализован AMD в ядре Zen4 и в Zen5 mobile (две операции по 256 бит) и как его сделали в ядре Zen5 (честные 512 бит). Но, есть конечно и физика, от которой никуда не деться

Therefore, this behavior is consistent with the earlier observation that Zen5 can run AVX512 at full clock speed provided there is thermal headroom. Somehow Zen5 manages to keep all that extra hardware on standby and can wake it up instantly.

Но очень удивлён как это реализовано в Intel процессорах (далее мой перевод):

Для процессоров Intel эти переходы [от обычного кода к коду с AVX512] обрабатываются в два этапа:
1) При переходе от кода низкой интенсивности (low-intensity) к коду высокой интенсивности (high-intensity), код высокой интенсивности работает с резко сниженной пропускной способностью, чтобы уменьшить его интенсивность.
2) После длительного периода в ~50 000 циклов код с более высокой интенсивностью переключается на полную пропускную способность.

Как упоминалось ранее, процессоры Intel не могут запускать AVX512 на полной скорости, так как они выйдут из строя [ниже в статье есть упоминание про Vdroop и я пока не понял - то ли напряжение падает то ли наоборот подскакивает]. Поэтому, прежде чем он сможет запустить AVX512, ему сначала нужно снизить тактовую частоту.

Снижения тактовой частоты выполняется тактовым генератором и регулятором напряжения и это занимает время ~50 000 циклов. Также требуется дополнительные аппаратные модули, которые включаются и используются только с 512-битными инструкциями.
...
На более высоких тактовых частотах включены только нижние 128 бит 512-битного оборудования. На этой [полной] скорости включение верхних 384 бит вызовет [повышение?] vdroop, которое может вывести ядро ​​из строя. Только на более низких скоростях могут быть включены все 512 бит. Но во время ожидания, пока процессор переключается на более низкую частоту - код может выполнять 512 битные инструкции, используя нижние 128 бит, что занимает в 4 раза больше времени, но это лучше чем вообще ничего не делать.

Вместо приостановки выполнения на ~50 000 циклов, процессоры Intel разбивают более широкие инструкции и "многократно перекачивают" (multi-pump) их в модули, которые уже включены и готовы к использованию на текущей тактовой частоте.
(конец цитаты)

🤦‍♂️ походу костыли не только в софте бывают :))) Буду искать подробности ещё. #simd

.NET epeshk blog

01 Aug, 15:27


Принёс книжку по concurrency

.NET epeshk blog

01 Aug, 12:51


Мифы .NET: ValueTask

Миф: ValueTask полезен только если метод чаще всего завершается синхронно

Реальность: ValueTask позволяет избежать аллокаций, даже если метод выполняется асинхронно.

1. С помощью пулинга объекта асинхронной стейт машины.

Для этого нужно пометить асинхронный метод, возвращающий ValueTask атрибутом:
[AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] для ValueTask
[AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))] для ValueTask<T>

В старых версиях .NET была env переменная, включающая пулинг глобально, но сейчас это уже не работает.

2. ValueTask может оборачивать переиспользуемый объект IValueTaskSource, умеющий сигнализировать об окончании асинхронного ожидания. Примеры таких объектов:

- AsyncOperation<T> в System.Threading.Channels
- ManualResetValueTaskSourceCore<T> — структура с логикой для упрощения создания своей реализации IValueTaskSource. По сути — переиспользуемый аналог TaskCompletionSource
- SemaphoreCompletionSource в ConcurrencyToolkit

Возможность переиспользования накладывает ограничение — ValueTask должен await-иться только один раз. Точнее, метод .GetResult() должен вызываться ровно один раз, т.к. именно в нём реализуется логика по освобождению IValueTaskSource для переиспользования.

Если ValueTask не await-ится, и на нём не вызывается .GetResult() — это тоже плохо. Значит IValueTaskSource не будет переиспользоваться

————
Также, в отличие от обычного Task, ValueTask не реализует интерфейс IDisposable. Это полезно, если результат асинхронного метода предполагается использовать в using.

Пример:

struct LockHolder : IDisposable;

Task<LockHolder> LockAsync();
ValueTask<LockHolder> ValueLockAsync();

using (LockAsync()) // баг, диспоузится Task
using (ValueLockAsync()) // ошибка компиляции, ValueTask<T> не IDisposable
using (await ValueLockAsync()) // OK


https://devblogs.microsoft.com/dotnet/how-async-await-really-works/#and-valuetasks

@epeshkblog

🚀💲
Поддержать канал 💲 🚀

.NET epeshk blog

30 Jul, 12:07


🚀 .NET Open Source: Perfolizer

[Blogpost] [Github] [Nuget]

Почти все .NET-разработчики хотя бы раз запускали бенчмарки на основе BenchmarkDotNet, или хотя бы видели markdown-таблицы с результатами замеров производительности.

BenchmarkDotNet стал стандартом для написания тестов на производительность в дотнете, и если он не используется — это повод задуматься о корректности полученных результатов.

С развитием фреймворка BenchmarkDotNet из него выделилась библиотека Perfolizer. В неё вошли код для работы с системным таймером, и реализации алгоритмов математической статистики:

- Change point detectors: полезны для автоматического анализа метрик — позволяют определить, когда распределение значений метрики значительно изменилось. Например, время обработки запросов подскочило после релиза с багом

- Outlier detectors: позволяют находить выбросы — значения, выбивающиеся из выборки. Например, во время одной из итераций бенчмарка разработчик от скуки запустил браузер, что испортило результаты замера

- Quantile estimators: позволяют считать квантили (перцентили) без хранения всех значений в памяти и сортировки

- Quick select: поиск K-ого значения в массиве из N элементов без сортировки за O(N)

и многое другое.

Больше по теме — в блоге Андрея Акиньшина aakinshin.net [archive]

@epeshkblog

🚀💲
Поддержать канал 💲 🚀

.NET epeshk blog

29 Jul, 14:42


Недокументированная особенность Dictionary и HashSet

Енумераторы Dictionary и HashSet перечисляют элементы в порядке добавления, если из коллекции не было удалений.

var hs = new HashSet<int> { 1, 4, 2, 5, 3 };
Console.WriteLine(string.Join(", ", hs));

// output: 1, 4, 2, 5, 3


Это недокументированная особенность реализации. Хэш-таблица внутри Dictionary и HashSet организована в виде двух массивов: _buckets и _entries. Значения хранятся в массиве _entries, который заполняется от начала к концу, а при увеличении размера коллекции копируется как есть.

При удалении в массиве _entries образуется пустое место, которое будет занято новым элементом, что нарушит упорядоченность.

Хоть возможность и недокументированная, она используется и в BCL, например в реализации нового LINQ-метода .CountBy(). Даже есть тест на случай, если устройство Dictionary изменится в будущем.

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

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

@epeshkblog

🚀💲
Поддержать канал и выход новых ХАБР статей 💲 🚀

.NET epeshk blog

29 Jul, 07:25


LLM в разработке

Когда появился Chat GPT, я с большим недоверием относился к возможности использовать его для практических задач — постоянные отказы отвечать из-за цензуры, длинные нравоучения вместо генерации кода, даже если просить "without any explanations"

Ситуация поменялась с выходом Claude 3 Opus и Claude 3.5 Sonnet. Наконец-то LLM может генерировать код по описанию, делать ревью, тестовые задания, заниматься скукой по типу конвертации конфигов из xml в json, и консультировать по архитектуре, не советуя обратиться к настоящему программисту. AI просмотрел в процессе обучения тысячи туториалов и этим надо пользоваться

Для тех, кто ограничен в отправке кода проекта в облако в последних версиях Rider завезли работающий локально full line code completion. Он не такой умный как Claude, но хорошо справляется с повторяющимся кодом, иногда угадывает LINQ запрос по названию переменной, генерирует тестовые данные.

Чтобы быть в курсе AI-фич для программирования рекомендую подписаться на канал Родиона Мостового Пробелов.NET. А ещё у автора канала будет доклад на осеннем Dotnext

А в треде — мемы от Клода

.NET epeshk blog

24 Jul, 18:33


Краткие факты о StringBuilder

1. StringBuilder устроен внутри не так, как List<char>, внутри которого массив, увеличивающийся в 2 раза. Точнее, таким он был в древние времена (до .NET Framework 4.0), а сейчас внутри связный список объектов, ссылающихся на массивы char[]. Размер этих массивов растёт по мере наполнения билдера, но ограничен, чтобы они не попадали в LOH.

2. Объект StringBuilder можно переиспользовать. Перед этим нужно сбросить его содержимое вызовом .Clear() или .Count = 0. При этом связный список выбрасывается, и вместо него аллоцируется один большой массив такого же capacity. И этот новый массив уже может попасть в LOH. Отсюда вывод: если StringBuilder не переиспользуется — не нужно очищать его вручную.

3. Не обязательно вызывать в конце работы со StringBuilder метод .ToString(). Например, можно использовать .CopyTo(Span<char>), или пройтись по списку ReadOnlyMemory<char>, соответствующих элементам связного списка через енумератор .GetChunks()

4. Если StringBuilder переиспользуется, то скорее всего, внутри у него связный список из одного элемента с большим массивом, который содержит всю строку-результат. Её можно извлечь без копирования с помощью того же енумератора. Полученный Memory будет валиден до следующих модификаций билдера, по аналогии с методами из CollectionsMarshal:

public static bool TryGetMemory(this StringBuilder sb, out ReadOnlyMemory<char> memory) {
var enumerator = sb.GetChunks();
if (!enumerator.MoveNext()) {
memory = ReadOnlyMemory<char>.Empty;
return true;
}
memory = enumerator.Current;
return !enumerator.MoveNext();
}


5. StringBuilder оптимизирован под добавление в конец (Append). Методы Insert, Remove, Replace не только алгоритмически сложны, но и могут испортить внутреннюю структуру связного списка, что приведёт к увеличению сложности других операций с билдером или лишним аллокациям. Особенно не стоит использовать эти методы, если StringBuilder переиспользуется

https://habr.com/ru/articles/172689/
https://andrewlock.net/a-deep-dive-on-stringbuilder-part-1-the-overall-design-and-first-look-at-the-internals/

@epeshkblog
| Поддержать канал

.NET epeshk blog

24 Jul, 12:36


https://habr.com/ru/articles/789592/

Пост должен быть в разделе ненормального программирования, в хорошем смысле. Вполне работоспособная игра на десктоп-фреймворке Avalonia, без Unity и других специализированных игровых движков.

Вывод автора — на практике доказано что на Avalonia можно разрабатывать игру двадцатилетней давности. WPF, увы, из коробки не вывозит (но всегда можно поискать оптимизации!).

.NET epeshk blog

19 Jul, 12:02


🚀 params Span/ReadOnlySpan<T>

Ура! Джуны могут чуть расслабиться — их больше не будут бить по рукам за аллокации params-массивов в методах, принимающих несколько аргументов.

А всё благодаря тому, что в C# 13 появятся params-спаны! И они уже доступны в превью .NET 9.

Method(1, 2, 3, 4, 5, 6, 7);

void Method(params Span<int> span) { }


Надеюсь, фича успешно доживёт до релиза!

❗️В целом, бить джунов по рукам не надо. Даже если они написали кучу Linq — лучше выразить свой гнев по поводу низкой производительности в пулл реквестах в dotnet/runtime

@epeshkblog | Поддержать канал

.NET epeshk blog

18 Jul, 13:42


А какие репозитории у вас?

У серилога по репозиторию на каждую либу, неудобно навигироваться по коду, видимо сделаю для себя один солюшен с проектами из нескольких репозиториев.

.NET epeshk blog

18 Jul, 09:45


Удивительный мир дотнетных локализаций

Результат .StartsWith(string) зависит от локали, даже если передана строка из одного ASCII-символа.

Например, при культурозависимом сравнении приравниваются различные виды Unicode-цифр и ASCII цифры, неточка с запятой; некавычка` и знак температуры в Кельвинах к букве K. Однако со злосчастными '@' и '$', которые используются в Serilog, в моей локали ничего не совпало.
var s = ((char)8490).ToString(); // Kelvin sign
Console.WriteLine(s.StartsWith("K")); // true
Console.WriteLine(s.StartsWith('K')); // false


Console.WriteLine(s.StartsWith("K", StringComparison.Ordinal)); // false


Кстати, .StartsWith(string, StringComparison.Ordinal) уже оптимизирован для односимвольных строк. В Serilog это даже учли, но Pull Request пока не влит.

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

@epeshkblog | Поддержать канал

.NET epeshk blog

18 Jul, 06:09


Производительность Serilog и StartsWith

В чате заинтересовались, уступает ли log4net по производительности современным библиотекам для логирования, т.е. Serilog и NLog. Нашелся комплект бенчмарков от разработчиков NLog, и оказалось, в них Serilog значительно проигрывает NLog'у — в 3.5 раза

Оказалось, причина в том, что прослойка между Microsoft.Extensions.Logging и Serilog использует .StartsWith("$") вместо .StartsWith('$'). В первом случае передаётся строка, во втором — символ.

Я почему-то думал, что для строки из одного ASCII-символа дотнет справится оптимизировать это сам. Но нет. Интересно, это просто пока не реализовано, или есть случаи, когда результат будет зависеть от локали?

@epeshkblog | Поддержать канал

.NET epeshk blog

16 Jul, 14:13


🎉 Guid.CreateVersion7()

В .NET 9 наконец-то появится Uuid с timestamp, призванный решить проблемы производительности из-за использования полностью случайных Guid V4 как ключей в БД. В preview 6 пока этого метода нет, можно попробовать в альфа-билде

Заехал он с огромными холиварами, и наверняка реализация ещё успеет поменяться.
Также в Guid добавили новые свойства:
- Version — позволяет отличить 4 и 7 версию
- Variant — "the most significant 4 bits of the 8th byte". Не придумал лучшего описания этому свойству, взял из документации.

====

Увы, хоть возможность сгенерировать Time-Uuid и добавили, другие недостатки у Guid остались:

- остался legacy порядок байт (https://t.me/epeshkblog/49)

- алгоритм генерации только один (48-битный таймстемп в миллисекундах + случайная часть)

- для случайной части используются не самые быстрые CoCreateGuid (Windows) и /dev/urandom (Unix-like). В некоторых ситуациях допустимо обменять случайность на производительность, а значит споры об алгоритме генерации гуида точно не закончатся. Быстрый способ реализован, например, в библиотеке Cysharp/Ulid

@epeshkblog | Поддержать канал

.NET epeshk blog

16 Jul, 12:16


Central Package Management

CPM — фича nuget, позволяющая задавать версии пакетов не для каждого проекта отдельно, а один раз в solution, в Directory.Packages.props

<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
</Project>


После этого, в файлах проектов можно не указывать версию:
<PackageReference Include="Newtonsoft.Json" />


По необходимости, в файле проекта по прежнему можно указать другую версию:
<PackageReference Include="Newtonsoft.Json" VersionOverride="13.0.2" />


CentralPackageTransitivePinningEnabled — обновляет транзитивные зависимости до версий, указанных в Directory.Packages.props

GlobalPackageReference — подключает пакет во все проекты, которые импортируют Directory.Packages.props

====

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

Из-за legacy-проектов пришлось указывать слишком много VersionOverride — без них солюшен даже не собирался. Риск обновить в них пакеты разом и словить в рантайме на проде баги и просадки производительности тоже выглядит большим.
Но для новых проектов, must have, учитывая, что поддержка в IDE сейчас уже стала гораздо лучше

А вы уже используете Central Package Management?

UPD: автоматический конвертер PackageReference

@epeshkblog | Поддержать канал

.NET epeshk blog

14 Jul, 04:36


В последних версиях OpenTelemetry есть занятная ситуация. Ребята, видимо хапнув от майкрософта бюджета (или хлыста) на улучшение безопасности, решили основательно ей заняться, и обрубили в трассировке все квери параметры в урлах.

Если раньше у вас в трейсах были, например, гет запросы domain.com?method=create&type=user&shouldcreate=true, то после обновления они стали domain.com?method=REDUCTED&type=REDUCTED&shouldcreate=REDUCTED. Да, они вырезают все данные без разбора, не пытаясь разобраться, что же там происходит или что это за данные. Вам просто ломают ноги и говорят, что это для безопасности, чтобы вы случайно с моста не прыгнули.

Ну хорошо, наверное это как-то настраивается? Можно же было что-то придумать, например предоставить редактируемый список ключей, которые надо REDUCTED, или сделать фичу с шифрованием параметров вместо их вырезания. Можно было хотя бы дать опцию отключить эту их заботу о моей безопасности.

Но мир не такой приятный, как хотелось бы. Из всего этого нам действительно предоставили опцию на отключение такого поведения.

Вот она:
internal bool DisableUrlQueryRedaction { get; set; }

Ничего не замечаете? Да, она internal. Она лежит в классе InstrumentationOptions, прямо там, где вы удобно настраиваете остальные публичные свойства. Это единственная опция, которая internal. Лежит у вас в шаге от доступности, щекочет вам пальцы, но к себе не подпускает. Документация дает отключать эту опцию только через env var. В обход стандартных подходов к конфигурации приложений. В обход вашего кода, из которого ее нельзя поменять. Если вы пишите либу, то вообще без вариантов. Я вообще первый раз вижу настолько наглую попытку сказать мне, как именно должен работать мой код.

Конечно, это обходится. Можно рефлексией, можно сорс геном. Но сам факт, когда тебе вставляют палки в колеса на ровном месте, очень раздражает. Я думаю, что они это откатят. Им надают по голове серьезные клиенты, но это будет очень не скоро, а пока похоже будем страдать.

.NET epeshk blog

11 Jun, 15:22


https://www.reddit.com/r/dotnet/comments/1dcq2l4/those_of_you_whove_built_popular_net_libraries/

.NET epeshk blog

11 Jun, 15:22


https://fxtwitter.com/jbogard/status/1800216696139186328?s=46