C# Heppard
1.56K subscribers
74 photos
2 files
122 links
25 способов эффективно использовать .NET

Поддержать канал можно тут: https://sponsr.ru/sharp_heppard
Download Telegram
SkipLocalsInit #скорость

Мы часто используем Span для ускорения сборки строк - создаём буфер на стеке, складываем туда кусочки будущей строки (чтобы не плодить промежуточные), а потом превращаем всё это в строку. Пример с ValueStringBuilder будет выглядеть следующим образом:

public string MyMethod() {
Span<char> buffer = stackallock char[1024];
var vsb = new ValueStringBuilder(buffer);
...
}


Это работает быстро, но можно сделать быстрее. В данном случае, местом ускорения будет создание буфера. В платформе есть правило - каждая переменная перед использованием должна быть инициализирована. В данном случае, платформа осуществляет инициализацию Span , то есть заполняет его элементы значениями по умолчанию. Это достаточно ресурсоёмкая операция (см. Span против SpanSkipInit на бенчмарке), которую мы можем не делать, воспользовавшись атрибутом SkipLocalsInit.

Атрибут SkipLocalsInit, применённый к методу, заставляет компилятор не использовать инициализацию локальных переменных этого метода. Важно понимать, что это не безопасная штука, так как в созданной переменной могут содержаться значения из памяти, которые были в памяти до этого. Именно поэтому, применение атрибута SkipLocalsInit требует объявить всю сборку как unsafe.

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

Также, немаловажно то, что чем больше буфер, тем более выгодно брать его из ArrayPool. Это отчётливо видно на бенчмарке: буфер размером более 4096 char'ов уже стоит брать оттуда. Это понятнее для разработчиков и не требует применения unsafe.

Больше подробностей о том, как работает инициализация в C# с SkipLocalsInitAttribute, есть тут и тут.

Код бенчмарка в комментариях.

P.S.: Если же мы хотим пропускать инициализацию модно и молодёжно, не помечая сборку как unsafe, мы должны использовать Unsafe.SkipInit.
👍285
Forwarded from .NET epeshk blog
🤡 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

- в доисторическом фреймворке в 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
👍2410🗿2
Советы робота #отдых

Решил я тут снова попробовать GPT для банальных подсказок по работе. Задал вопрос - расскажи о простых и понятных способах оптимизации производительности на C#.

И вот, среди более-менее нормальных советов а-ля "подбери алгоритм", "используй правильные коллекции", нашёл странный - минимизируй использование переменных.

Я такой, типа, об-бъяснитесь, мьсё! Приведите пример! Ну он и выдал... А я опешил.

Из его объяснения выходило, что обычный for с промежуточной переменной ("первый способ" на картинке) должен работать медленнее, чем Sum() ("второй способ"), так как там нет переменных. Проверить - дело не хитрое, я написал бенч. Предложение создавать Enumerable.Range(1,100) в бенчмарке я, конечно, отверг - явная ошибка, спишем на молодость робота. Cоздал массив заранее, запустил и... "второй способ" и правда работает быстрее!

Вот это да!... Да? А вот нет. То есть да, работает быстрее, но дело, конечно же, не в "количестве переменных", а в векторизации, которую легко найти в исходном коде BCL. Получается, что робот как бы прав в результате, но не прав (причём сильно) в предпосылках и объяснениях этого результата.

Какой же можно сделать вывод? Да, роботы стали умнее собирать информацию и выдавать её нам. Но насколько нам полезен результат, если предпосылки ошибочные? Мне ночью приснился MR от молодого разработчика, который увидел этот ответ робота, проверил его, восхитился и... стал вычищать C# код от лишних переменных. Брр. А если бы это был экономист? Или юрист? Или медик? Короче говоря, роботов всё ещё нужно использовать аккуратно, внимательно проверяя и перепроверяя. Особенно по профессиональным вопросам.

Бенч элементарный, но он в комментах.

P.S.: Ну и вишенка на торте - робот ошибку признал. Что ему мешало сразу сказать правильно - загадка. Надеюсь, что это поправят.
👍25🔥8😁5
Структура как Span #решение #память #скорость

Разобравшись с InlineArray и SkipLocalsInit мы можем пойти дальше. Например, мы можем представить любую структуру как Span. Напомню, что Span это простой указатель на адрес в памяти + отступ, умноженный на размер элемента Span'a.

Сделать Span из структуры достаточно просто:
private struct MyStruct {  
private int _item0;
...
private int _item9;
}

// Пропускаем инициализацию структуры
Unsafe.SkipInit(out MyStruct myStruct);

// Получаем ссылку на структуру на стеке
ref var reference = ref Unsafe.As<MyStruct, int>(ref myStruct);

// Получаем структуру как спан
var span = MemoryMarshal.CreateSpan(ref reference, MyStructItemCount);


Представляется, что примерно так работает представление структуры, отмеченной InlineArrayAttribute, когда мы говорим Span<int> span = myInineArray. Бенчмарк подтверждает это, так как скорость доступа к элементам структуры близка к прямому доступу через индексатор InlineArray.

Представление структуры как Span позволяет проще обращаться к элементам структуры (например, без монструозных switch), а именно её заполнение или чтение.

При этом, нам надо быть крайне аккуратными, так как методы статического класса Unsafe очень не зря находятся в этом классе. Пусть нас не обманывает простота их использования - фактически это тот же unsafe-код.
👍13🔥6🤔1
Зарплаты в IT #деньги

Взял тут. Откуда они взяли цифры - не знаю, но канал весьма авторитетный. В принципе, с моими данными эти цифры плюс-минус бьются. Среднее арифметическое значение по этой табличке:

Lead: 417k (если это он)
Senior: 316k
Middle: 276k

Максимумы:

Lead: 600k
Senior: 750k
Middle: 500k

Я к этим цифрам добавлю, что по моим знакомым и в последнее время всё больше заметно следующее. В компаниях, которые считаются "солью IT", зарплаты не очень конкурентные. То есть коллеги выезжают, во многом, благодаря ранее сформированному HR-бренду. У них именно среднее арифметическое.

Зарплаты близкие к максимальным чаще всего относятся к т.н. "ноу нейм" компаниям.

P.S.: По моим данным, подчёркиваю. Как получаются именно мои наблюдения, я написал тут, повторяться не буду.
🔥11👍8
Быстрый и экономный 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.: Бенч в другом месте - слишком много кода. Туда же выложил результаты. Либы из бенчмарка в комментах.
P.P.S: Сравнение с NPOI тут.
👍26🔥185
Тут на соседнем канале зашла речь про ускорение некоторых алгоритмов с помощью SIMD и я побыстрому накидал реализацию двух - косинусное сходство и корреляцию Пирсона (на скриншоте бенчи для него, для косинусного сходства - в камментах в gist). Алгоритмы как будто прямо таки созданы для Single Instruction/Multiple Data :)

Первый блок на скриншоте - просто мап на Vector<double> и дальнейшие операции, ничо сложного, но даже это даёт 6-кратный буст. Второй блок с float, тут ещё побыстрее, просто потому что элемент в 2 раза тоньше и за один чпок забирается в два раза больше элементов по сравнению с double.

Но вот дальше там был ещё один кейс, когда входные данные короче И double И float - например short. И вот тут становица всё ещё интереснее: отмапленый в Vector256<short> забирает сразу 16 элементов входного массива. Напрямую в Vector256<float> такое не смапиш конечно, поэтому операция двухэтапная - сначала GetLower/GetUpper по 8 элементов экспандяца до int (32 бита = 256 бит), а потом кастяца до float (тоже 256 бит).

Вроде выглядит некоторыми костылями, но это даёт 14-кратный буст даже на длинных массивах, которые гарантированно не влезают в L2 кэш. Если кастить в 32-битный float конечно, с double ситуация пожиже - там буст ровно в два раза хуже (~x7), что вполне логичо :))

Судя по всему выполнение SIMD инструкций тут отлично сочетается с асинхронностью L1/L2-кэша - пока локальные данные кастяца, множаца и складываюца - в кэш подтягиваются следующие порции данных и к моменту следующей итерации они уже там. #simd
👍18🔥53
ByReferenceStringComparer #скорость

Недавно появилась задача быстро искать цифры по словарю. Ключ - строки, которые приходят извне, но которые всегда из определённого списка. Значение - число. В исходном коде на каждый запрос происходил поход в БД, создавался словарик, после этого по нему десятки (если не сотни) раз осуществлялся поиск по ключу.

Оптимизация первая. Ходить в БД за этими цифрами, очевидно, долго, да и данные обновляются редко, поэтому первая оптимизация - кэш через словарик на микросервисе. Это дало 50% прироста производительности. Неплохо. В принципе, заказчик уже был доволен, но мне хотелось большего.

Оптимизация вторая. Как я уже заметил, словарик, по ходу запроса, используется очень-очень часто. Это было отлично заметно в профайлере. Причём основное время тратилось на получение хэша и сравнение строк.

Я прогнал все входящие строки и все ключи из БД через штуку а-ля string.Intern. Таким образом я получил строки, идентичные по ссылке, что, кажется, должно облегчить сравнение строк. Я надеялся, что FrozenDictionary это заметит и применит какие-то оптимизации. Увы, нет. Тем не менее, я получил небольшой прирост производительности, когда перешёл на Frozen.

Однако, сравнение через ссылку не давало мне покоя. Я вспомнил, что некто Евгений рассказывал, что Serilog не эффективно использует кэш темплейтов для получения подготовленного сообщения. Мол, в качестве IEqualityComparer<string> для ключа словаря можно было бы сравнивать строки по ссылке, а хэш получать из заголовка инстанса строки. В принципе, для этого сценария у меня уже всё было готово, и я начал создавать Frozen с указанием вот этого компарера:

class ByRefStringComparer : IEqualityComparer<string>
{
public bool Equals(string? x, string? y)
{
return ReferenceEquals(x, y);
}

public int GetHashCode(string obj)
{
return RuntimeHelpers.GetHashCode(obj);
}
}


Не скажу, что последняя оптимизация внесла весомый вклад, но, тем не менее, она была достаточно полезная. Во всяком случае ещё 3-5% скорости могут помочь в ситуации высокой нагрузки.

Бенчмарк в комментариях.

Замечу, что подобный подход может помочь только (!) в случаях, когда набор строк ограничен (например, список названий областей страны) и по ним часто и много ищут. То есть не нужно использовать подход, если у вас разные строки, либо строка в алгоритме используется всего один раз. Пропуская все строки через аналог string.Intern вы просто переложите поиск строки из одного места в другое.
👍30🔥3
ByReferenceTypeComparer #скорость

Если хорошенько подумать, то ByReferenceComparer из предыдущего поста может хорошо помочь при сравнении всего, что существует в приложении от начала и до конца его жизни. Например, тип Type, который часто используется для логеров, сериализаторов и ветвления логики, в зависимости от типа входящего параметра.

Напомню, что типы хранятся в приложении постоянно (скорее всего, исключая сценарии загрузки и выгрузки Assembly) и представляются одним и только одним инстансом. Это очень хорошо ложится на сценарий, аналогичного строке, которая хранится в таблице интернирования.

Таким образом, если у нас есть Dictionary<Type, ???>, то мы можем ускорить его работу на 20-30%, просто передав ему сравниватель, который будет проверять равенство по ссылке, а hash брать из заголовка типа.


private sealed class ByReferenceComparer<T> : IEqualityComparer<T>
where T : class
{
public static readonly IEqualityComparer<T> Instance = new ByReferenceComparer<T>();

private ByReferenceComparer()
{
}

public bool Equals(T? x, T? y) => ReferenceEquals(x, y);

public int GetHashCode(T obj) => RuntimeHelpers.GetHashCode(obj);
}


Бенчмарк в комментариях.
Запуск на MacBook.

P.S.: Если кого-то волнует, работает ли подобный подход в сценарии, когда мы создаём тип "налету" (а-ля typeof(List<>).MakeGenericType(typeof(int))), то да, так тоже работает.

P.P.S.: Остаётся открытым вопрос, почему так не делают в .net по-умолчанию. Скорее всего, есть какой-то нюанс, который я упускаю (напр., тут). Возможно, мы вместе найдём на него ответ. Предположительный ответ.
👍13🤯4🔥2
Аллокация объектов на стеке #память

Наверное, многие слышали, что .NET 9 теперь старается не размещать короткоживущие объекты в куче, если они являются boxed value-типами. Теперь результат "боксинга" таких типов располагается на стеке, что позволяет разгрузить GC и увеличить производительность.

Конечно же, есть "но". Дело в том, что runtime должен быть уверен, что результат боксинга не выходит за границы метода. В этом и только в этом случае, результат боксинга value-типа (фактически object) будет располагаться на стеке. Это очень похоже на Rust, где есть встроенная в язык функция наблюдения за временем жизни переменной. Выход из метода "удаляет" все сущности, которые были созданы внутри него, но не используются вне него.

В примере ниже, как и раньше до .NET 9, произойдёт боксинг цифр "3" и "4" при вызове метода Compare. Однако runtime (JIT) "видит", что эти объекты не выходят за пределы метода RunIt. Следовательно, результат боксинга можно разместить на стеке.

static bool Compare(object? x, object? y)
{
if (x == null || y == null)
{
return x == y;
}

return x.Equals(y);
}

public static int RunIt()
{
bool result = Compare(3, 4);
return result ? 0 : 100;
}


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

var result = 0;
foreach ((int a, int b) in _values)
{
result += Compare(a, b) ? 1 : -1;
}
return result;


Дело в том, что в конкретно этом случае, по мнению JIT, в методе может быть создано слишком много "забокшеных" value-типов, что, в свою очередь, значит, что оптимизация применена не будет.

Есть и иное предположение. Оно основано на том, что функция Compare принимает, условно, всё, что угодно. Это, в свою очередь, значит, что JIT справедливо полагает: размер данных в аргументах функции может быть разным на любой итерации for. А это означает, что невозможно вызывать Compare с уравниванием всех возможных типов аргументов по размеру (см. вот этот комментарий).

Кажется, что решение очевидно: нам нужно создать промежуточный метод, который будет определять типы (для понимания размера) и границы создаваемых временных переменных. Некий контекст, который подскажет JIT'у, что боксинг временный и нужен только на одну итерацию for.

bool CloseContext(int a, int b) => Compare(a, b);


Но, увы, это не сработает, так как в дело включается другая оптимизация - method inlining. Для JIT'a этот метод - прекрасный случай для автоматического инлайнинга. Увы, это ломает нашу прекрасную идею с обозначением контекста, в рамках которого будут жить наши boxed value-типы.

Значит, мы должны не только создать отдельный метод, но и прямо указать, что делать ему "инлайн" не нужно. Благо, у нас есть специальный атрибут для подобных указаний - [MethodImpl(MethodImplOptions.NoInlining)]. В этом случае, боксинг происходит, но его результат остаётся на стеке. Результаты хорошо видны на бенчмарке.

Код бенчмарка тут. Если нужно больше подробностей, то я написал этот пост под впечатлениями вот отсюда.
👍37🔥17👎1
Dictionary.AlternateLookup #память #скорость

Несколько лет назад я устраивался в компанию, которая дала тестовое задание. Его суть - показать максимальную скорость и минимальную аллокацию при обработке большого объема данных. Что-то вроде "посчитать количество слов в документе" (я упрощаю).

Одна из основных проблем в подобной задаче - поиск ключа (слова) в большом словаре Dictionary<string, int> и инкремент значения (количества). В то время мне пришлось взять код обычного Dictionary и модернизировать его так, чтобы он принимал ReadOnlySpan<char> в качестве ключа - так я пытался не аллоцировать строки, которые уже существуют в словаре. Инкремент был выполнен в стиле современного CollectionsMarshal.GetValueRefOrAddDefault. Решение коллегам понравилась и меня взяли на работу.

В современном .NET 9 эта задача решается максимально просто, так как нам предоставили прекрасный метод словаря GetAlternateLookup, возвращающий специальную структуру, которая может принимать в качестве ключа то, что сам программист считает сравнимым с ключом словаря.

var dic = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
var lookup = dic.GetAlternateLookup<ReadOnlySpan<char>>();

foreach (ReadOnlySpan<char> word in wordCollection)
{
CollectionsMarshal.GetValueRefOrAddDefault(lookup, word, out _)++;
}


В данном примере, сравнение ReadonlySpan<char> с типом string возможно потому, что у StringComparer существует реализация интерфейса IAlternateEqualityComparer<ReadOnlySpan<char>, string>. Если ознакомиться с его кодом, то мы видим, что этот интерфейс требует не только реализовать операции сравнения, но и метод создания string из ReadOnlySpan<char>. Таким образом, lookup имеет возможность не только сравнивать значения ключа, но и, в случае его отсутствия, создавать в словаре запись с этим ключом.

Бенчмарк в комментариях. Он содержит сравнение наивного подхода с реализацией через AlternateLookup. В наивном подходе мы создаём строки для поиска наличия ключа, а в случае с AlternateLookup строки создаются только тогда, когда запись в словаре отсутствует. Более сложные сравнения с созданием специального словаря я опущу, хотя в этом случае, как мне кажется, всё-таки возможно выжать ещё немного скорости.
🔥24👍182
Чтение из БД: Dapper, Linq2Db, EF #хранилище

Недавно в соседнем канале я наткнулся на обсуждение скорости работы ORM. Естественно, разговор крутился вокруг Dapper, Linq2Db и EntityFramework (EF, EF Core). Заявлялось, что библиотека Linq2db не только удобна как EF, но и быстра, как Dapper. Это меня удивило, так как в моём мировоззрении скорость Dapper достижима только при отказе от удобства уровня EF и при написании чистого SQL (хотя, возможно, кому-то нравится писать запросы руками).

Я взял классическую задачу - есть блоги, у блогов есть посты. Я развернул базу данных PostgreSQL в Docker, добавил в неё 100 блогов по 200 постов. Код с использованием EF я написал быстро, с Linq2Db пришлось немного сложнее, но помогла документация, а вот Dapper дался сильно не сразу - именно поэтому по нему два бенчмарка.

Замечу, что я замерял только чтение (SELECT). Измерения скорости добавления и удаления я, возможно, произведу позже.

Тем более, что результаты чтения меня удивили, так как Linq2Db работает очень эффективно. При этом код, который необходимо написать для получения блогов и их постов, подозрительно краток и лаконичен. Ещё более меня удивило то, что скорость близка к Dapper (замер с двумя запросами) и немного выше, чем у рекомендованного для данного случая QueryAsync<Blog, Post, Blog> с маппингом и splitOn.

Мой фаворит, по результатам замеров - Linq2Db, который весьма неплохо справляется с чтением данных из БД, при этом код устойчив к изменениям и прост, в отличии от того же Dapper. EF традиционно отстаёт, но проигрывает не сильно. При выполнении SQL-запросов через метод FromSqlRaw EF показывает впечатляющие результаты, не очень сильно отставая от Dapper.

Пока готовил бенчмарки, с ужасом осознал, что "готовить" EF я более-менее могу, а вот с Dapper и Linq2Db у меня сложности. Поэтому, если уважаемые читатели заметят какие-либо проблемы с кодом и его оптимальностью, я буду счастлив. Дело в том, что этим бенчмарком я получил неожиданные для себя результаты. Хотелось бы вернуть мир на место.

Кода много, поэтому он тут. Я думаю, что интересующимся не составит труда самостоятельно поднять PG в Docker'e и наполнить БД данными через миграцию в EF.

P.S.: Бенчи Ef_ToArray и Ef_ToList отдельно рассмотрены тут. То, что их результаты разные в данном измерении - скорее всего погрешность измерителя. Они должны быть плюс-минус одинаковыми.
👍20🔥53
EF: ToArray vs ToList #отдых

Бился в соседнем канале на тему поста выше. Появилось интересное предположение, что в EF лучше материализовать коллекции через ToList, так как ToArray имеет следующий код:

public static async Task<TSource[]> ToArrayAsync<TSource>(
this IQueryable<TSource> source,
CancellationToken cancellationToken = default)
=> (await source.ToListAsync(cancellationToken).ConfigureAwait(false)).ToArray();


То есть в самом начале выполняется ToList, а потом, из уже созданного списка, выполняется ToArray. Программирование подсказывает нам, что будет создано две коллекции, одна из которых - лишняя.

Но на бенчмарке этого не заметно: скорость выполнения и аллокация почти идентичные. Как так получается и где программирование сломалось - загадка.

Бенч (сравнение ToList, ToArray) в комментах. Бенч из предыдущего поста, но на ToList, тут. Как и предполагалось скорость и аллокация одинаковые. Но почему?

Предполагаю, что его размер просто потерялся при 100 блогах и 200 постах. Если сделать один блог и один пост, и написать материализацию через AsAsyncEnumerable, то вроде как его видно (разница 0.3 КБ, но она есть). Другое объяснение, что всё оптимизировали настолько, что просто магия.
😁17👍65
Новый params #скорость #память

Как многие знают, начиная с .NET 9 (C# 13) появилась возможность по новому взглянуть на ключевое слово params. Напомню, что это ключевое слово позволяет программисту указывать несколько аргументов метода одного типа через запятую, которые, в самом методе, будут представлены в виде коллекции. Ранее этой коллекцией мог быть только массив. Теперь это может быть ReadOnlySpan, Span, List и даже IEnumerable.

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

Начиная с .NET 9 (C# 13) компилятор делает это сам.

Результаты хорошие - решение с params ReadOnlySpan<T> значительно быстрее, чем params T[] и, к тому же, вообще не аллцирует память в куче. См. скриншот.

Бенчмарк в комментариях.

P.S.: Для особо пытливых, которые хотят понять, а почему есть разница между ReadOnlySpan и Span, я рекомендую посмотреть low-level C# (в Rider, например). В первом случае используется RuntimeHelpers.CreateSpan, а во втором случае создаётся InlineArray размером в количество элементов (про него я писал тут).
20👍13🔥9
Случайная строка из 12 символов #отдых

Недавно я снова упражнялся с роботами (LLM, GPT). Мне сказали, что они стали значительно умнее за последние 6 месяцев. Действительно, беседы на темы, где я не специалист выглядели весьма убедительно. Роботы стали предлагать более умные решения, исправляться и уточнять, если меня что-то не устраивает, признавать ошибки и предлагать альтернативные варианты.

При этом, конечно, в областях, где я специалист, всё по прежнему не очень радужно. Так, простая задача по созданию случайной строки (random string) всё ещё решается в стиле "посмотреть на stackoverflow и предложить самый популярный вариант". Роботы прямо предлагали решения из вот этой вот темы.

const string chars = "чарики";
return new string(Enumerable
.Repeat(chars, length)
.Select(s => s[random.Next(s.Length)])
.ToArray());


Это решение мягко говоря сомнительное и не оптимальное (см. Linq в результатах бенчмарка). Я уже встречался с таким лет 10 назад, когда stackoverflow-junior-программист предлагал подобный код. Впрочем, немного побеседовав с роботом, я таки получил предложение использовать современное решение на основе метода Random.GetItems.

Далее я попробовал узнать, а какое решение по созданию строки является криптостойким в .NET. Ответ был из всё той же темы на stackoverflow, мол, используй RNGCryptoServiceProvider. Я удивился, так как этот класс указан в документации как obsolete аж с .NET 6. Я указал на это роботу, после чего получил вполне вменяемый и современный совет по использованию RandomNumberGenerator.

В принципе, общение меня обрадовало. Теперь робот не бездумно настаивает на своём решении, а пытается предложить варианты и альтернативы, если предложенное им решение в чём-то не нравится пользователю. Это успех. С удовольствием продолжу наблюдать за роботами.

Также, я воспользовался случаем и написал бенчмарк о том, какое решение по созданию строки более оптимально для случаев строки в 12 символов, так это ограничение одного из методов (Path.GetRandomFileName), который тестировался в бенчмарке. В реальности создание тысячи символов из того же Guid потребует бОльшего количества кода и бОльших расходов ресурсов.

P.S.: Иван, спасибо! Я попробовал.
P.P.S: Список тестов в этом посте - то и только то, что предлагали роботы. Речь не про самый оптимальный способ сгенерить строку, выбрать оптимальный из того, что предлагали роботы.
P.P.P.S: Всё дело было в промпте. Если задать грамотный вопрос, то робот возвращает грамотный ответ. То есть меня подвело то, что я мало работал с этим инструментом.
👍154😁4🔥2
А теперь, для любителей локальных моделей: Gemma 3 QAT

Что-то мы все про проприетарщину да и проприетарщину. А что насчет локальных моделей?
Надо сказать, что на этом поприще у маленькмх опенсорных моделей тоже наблюдается какой-то фантастический буст. Например, Gemma 3 27B в кодинге показывает результаты, сопоставимые с GPT-4o-mini.
А из ризонинг моделей, как упоминал ранее, QwQ 32B на уровне Claude 3.7 Sonnet Thinking, а DeepCoder 14B (это новая спец. моделька от создателей DeepSeek) на уровне o3-mini (low).
Ну, и опять эксклюзив - на агентских задачах по кодингу, неожиданно вырвалась вперед моделька OpenHands LM 32B от ребят из OpenHands, которые дотренировали ее из Qwen Coder 2.5 Instruct 32B на своем "тренажере для агентов" SWE-Gym, опередив в итоге в SWE-bench даже огромную Deepseek V3 0324. В общем, OpenHands молодцы! Кстати, недавно их Code-агент взял новую соту (SoTA - State of The Art) в SWE-bench Verified. Так что, могу всем смело рекомендовать их блог.

Ух, ну и перенасытил я вас всего лишь одним абзацем!

В общем, что сказать-то хотел - ребята из Google посмотрели, значит, на свою Gemma 3 и увидели, что, при всей своей красоте, она довольно тяжелая все равно оказалась для консьюмерских ПК/GPU, ну и разразились они какой-то крутой квантизацией, которая называется QAT (Quantization-Aware Training). Что это за QAT такой мы тут разбираться не будем - просто для нас важно знать, что эта хитрая техника квантизации уменьшает требования моделей к железу до 4-х раз, при этом почти не влияя на уровень "интеллекта" модели.
Действительно ли это так? Давайте проверим на примере Gemma 12B IT QAT (4bit). Кстати, специальные MLX-квант-веса, оптимизированные для маководов (я) доступны по ссылке.
Так вот, моделька эта запускается через LMStudio в две кнопки.
В итоге, ответы действительно у нее неплохие, какую-то несложную кодогенерацию она явно вытянет. На, и русский язык ее оказался безупречным (см. скрины). Более того, после моего замечания она, как будто, даже вывезла задачу с параллельной генерацией эмбеддингов (сама решила взять для этого SemaphoreSlim). С использованием Parallel уже не справилась, т. к. начала await юзать внутри Parallel.For (сорри за жаргон, если вы не дотнетчик). Но в целом, у меня впечатления отличные!

А как у вас себя ведут локальные модельки? С какими задачами справляются, а с какими нет? И какие модели вы используете локально? (если вообще используете)
🔥13👍6