Быстро парсим float #скорость #решение
Нашёл тут для себя задачку по парсингу большого количества значений из большого файла. В файле перечислены города и температуры в них. В принципе, ничего особенного -
Этот набор байтиков мы, конечно, как знающие люди, можем запихнуть в эффективный
И вот я, значит, написал этот высокоэффективный код, смотрю с помощью dotTrace на предмет, а что, собственно, можно ещё улучшить. Если кто не знает, эта такая утилитка, которая показывает, где и на что мы тратим время в коде. И, к моему большому удивлению оказалось, что куча времени тратится на
Я как-то даже немного подрасстроился. Ну, думаю, уж эту штуку должны были написать круто. Может быть я как-то не так готовлю
Я человек прошареный, поэтому искал решения в том числе на C++. Оказалось, что есть такая интересная штука как fast_float. И, о чудо, эта имплементация нашлась и на C#. Бенчмарк, замеры… Да, оказалось сильно быстрее, аж в пять раз. Человеку, который это придумал - респект. Код будет в комментариях.
Библиотеку слишком плотно не тестировал, но в деле решения ускорения исходной задачи она сильно помогает. Возможно, кто-то знает какие-то более классные решения - буду рад посмотреть.
P.S.: Сергей, спасибо за задачку!
P.P.S: Сравнение с
Нашёл тут для себя задачку по парсингу большого количества значений из большого файла. В файле перечислены города и температуры в них. В принципе, ничего особенного -
название_города;температура
. Температура это число с плавающей запятой (например, -2.33, 7.4, 0.3). В принципе, понятно, что если извлечь строку и поделить её по символу точка-с-запятой, то мы получим набор байтиков с числом.Этот набор байтиков мы, конечно, как знающие люди, можем запихнуть в эффективный
float.Parse
. Код получается примерно вот такой:
ReadOnlySpan<byte> row = …;
int separator = row.IndexOf(‘;’); // байтик
ReadOnlySpan<byte> temperatureBytes = row[(separator + 1)..];
float temperature = float.Parse(temperatureBytes, Culture);
И вот я, значит, написал этот высокоэффективный код, смотрю с помощью dotTrace на предмет, а что, собственно, можно ещё улучшить. Если кто не знает, эта такая утилитка, которая показывает, где и на что мы тратим время в коде. И, к моему большому удивлению оказалось, что куча времени тратится на
float.Parse
. На 100 миллионах строк это видно отчётливо…Я как-то даже немного подрасстроился. Ну, думаю, уж эту штуку должны были написать круто. Может быть я как-то не так готовлю
float.Parse
? Или что-то не докрутил с настройками (там они есть, их несколько)? Масса вопросов. Взял себя в руки, пошёл искать в интернетах, мол, что люди делают в такой ситуации. Я человек прошареный, поэтому искал решения в том числе на C++. Оказалось, что есть такая интересная штука как fast_float. И, о чудо, эта имплементация нашлась и на C#. Бенчмарк, замеры… Да, оказалось сильно быстрее, аж в пять раз. Человеку, который это придумал - респект. Код будет в комментариях.
Библиотеку слишком плотно не тестировал, но в деле решения ускорения исходной задачи она сильно помогает. Возможно, кто-то знает какие-то более классные решения - буду рад посмотреть.
P.S.: Сергей, спасибо за задачку!
P.P.S: Сравнение с
Utf8Parser.TryParse
(который, вроде как, реализует fast_float) тут. Евгений, спасибо!🔥20👍3😁1
Словари: поиск по ключу #бенч
Хотелось бы напомнить про скорость извлечения данных из словарей в .NET. Напомню, что словарей у нас много и все они немного разные -
Для того, чтобы грамотно выбрать словарь, я рекомендую послушать один из последних выпусков RadioDotNet (00:54:30). Там неплохо рассказали о том, как появились имплементации словарей, как они работают, где их слабые и сильные стороны.
При чтении бенчмарка прошу обратить внимание на то, что я использую интерфейс
Обратите внимание, что этот бенчмарк касается только извлечения данных по ключу. При выборе конкретной имплементацию этого бенчмарка не достаточно, и нужно обязательно обратить внимание на тип ключа и ваш профиль нагрузки - часто добавление значения в коллекцию полностью нивелирует скорость извлечения данных.
P.S.: Также, коллеги напоминают, что производительность FrozenDictionary очень зависит от ключа.
P.P.S: Сравнение скорости работы, когда ключ это строка тут. Там возможности FrozenDictionary раскрываются во всей красе.
Хотелось бы напомнить про скорость извлечения данных из словарей в .NET. Напомню, что словарей у нас много и все они немного разные -
Dictionary
, ConcurrentDictionary
, FrozenDictionary
, ImmutableDictionary
и ReadonlyDictionary
. У всех у них чуть-чуть разные задачи и немного разная производительность на разных ключах (в том числе на содержимом ключей). Для того, чтобы грамотно выбрать словарь, я рекомендую послушать один из последних выпусков RadioDotNet (00:54:30). Там неплохо рассказали о том, как появились имплементации словарей, как они работают, где их слабые и сильные стороны.
При чтении бенчмарка прошу обратить внимание на то, что я использую интерфейс
IDictionary
, а не явные имплементации словарей. Мотивация создания именно такого бенчмарка - поиск пути безопасной подмены имплементации одного словаря на другой в уже работающей кодовой базе. Естественно, при написании нового кода, я бы предпочёл использовать явные имплементаци (т.е. классы, а не интерфейс). Более того, IDE явно на это намекает.
CA1859: используйте конкретные типы, если это возможно для повышения производительности.
Обратите внимание, что этот бенчмарк касается только извлечения данных по ключу. При выборе конкретной имплементацию этого бенчмарка не достаточно, и нужно обязательно обратить внимание на тип ключа и ваш профиль нагрузки - часто добавление значения в коллекцию полностью нивелирует скорость извлечения данных.
P.S.: Также, коллеги напоминают, что производительность FrozenDictionary очень зависит от ключа.
P.P.S: Сравнение скорости работы, когда ключ это строка тут. Там возможности FrozenDictionary раскрываются во всей красе.
👍14🔥4❤1
Бесплатные Rider и WebStorm
Ого, решительно поддерживаю! Обещают, что приложение будет полностью функциональным и ни чем не отличаться от платной версии. Хорошее решение для пет-проектов и не только.
Злые языки утверждают, что такой шаг со стороны чехов - последствия снижения качества, низких продаж и жаркого дыхания VS и VSCode в спину Rider'у. Но я желаю парням успехов.
Ещё интернетах пишут, что эти IDE уже можно скачать через некий uTorrent. Не знаю, что это за приложение - надо попробовать. Увы, официальный Toolbox что-то перестал скачивать обновления - видимо, высокая нагрузка.
P.S.: И вот ещё новая информация, о блокировании пользователей, которые уже оплатили лицензии.
Ого, решительно поддерживаю! Обещают, что приложение будет полностью функциональным и ни чем не отличаться от платной версии. Хорошее решение для пет-проектов и не только.
Злые языки утверждают, что такой шаг со стороны чехов - последствия снижения качества, низких продаж и жаркого дыхания VS и VSCode в спину Rider'у. Но я желаю парням успехов.
Ещё интернетах пишут, что эти IDE уже можно скачать через некий uTorrent. Не знаю, что это за приложение - надо попробовать. Увы, официальный Toolbox что-то перестал скачивать обновления - видимо, высокая нагрузка.
P.S.: И вот ещё новая информация, о блокировании пользователей, которые уже оплатили лицензии.
😁27👍9🔥3👏1🤯1
Атрибут DoesNotReturn #решение
Когда мы пишем оптимальный код, мы пытаемся вынести выброс Exception за пределы тела метода, чтобы упростить inline. Типичная ситуация выглядит следующим образом:
В этом случае статический анализатор (конечно, при включённом nullable annotations) вежливо подсказывает -
Некоторе, в подобной ситуации, кричат на код ("я знаю что делаю, дурацкая ты железка"), просто делая
Есть, однако, другое решение, которое я подсмотрел в BCL - атрибут DoesNotReturn. Этот атрибут указывает компилятору, что метод или свойство никогда не возвращает значение. Другими словами, он всегда создаёт исключение (выполнение вызывающего метода прекратится).
Обладая этими нехитрыми знаниями, мы просто добавляем атрибут
Более подробно про помощь компилятору можно прочитать тут и тут. Там же много других полезных атрибутов, которые помогут меньше кричать на код, а значит сделать его красивее и понятнее. Кажется, что все эти атрибуты особенно важны для библиотечного кода, тогда как в обычном коде они представляются мало востребованными.
Когда мы пишем оптимальный код, мы пытаемся вынести выброс Exception за пределы тела метода, чтобы упростить inline. Типичная ситуация выглядит следующим образом:
public object MyMethod() {
var data = ...
if (data == null) {
Error.MyError("msg");
}
return data;
}
public static Error {
public static void MyError(string msg) {
throw new Exception(msg);
}
}
В этом случае статический анализатор (конечно, при включённом nullable annotations) вежливо подсказывает -
data
может быть null
(см. скриншот). Это правильно и нормально, так как возвращаемое значение метода не отмечено как object?
, то есть оно не допускает значение null.Некоторе, в подобной ситуации, кричат на код ("я знаю что делаю, дурацкая ты железка"), просто делая
return data!
. Однако, когда код будет меняться и разрастаться, логика первоначального создателя может изменится или утратиться, а криков на код станет больше.Есть, однако, другое решение, которое я подсмотрел в BCL - атрибут DoesNotReturn. Этот атрибут указывает компилятору, что метод или свойство никогда не возвращает значение. Другими словами, он всегда создаёт исключение (выполнение вызывающего метода прекратится).
Обладая этими нехитрыми знаниями, мы просто добавляем атрибут
DoesNotReturn
для всех методов, которые выбрасывают ошибку и убираем крики на код. Просто и красиво.[DoesNotReturn]
public static void MyError(string msg) {
throw new Exception(msg);
}
Более подробно про помощь компилятору можно прочитать тут и тут. Там же много других полезных атрибутов, которые помогут меньше кричать на код, а значит сделать его красивее и понятнее. Кажется, что все эти атрибуты особенно важны для библиотечного кода, тогда как в обычном коде они представляются мало востребованными.
👍50🔥8🤨3
SkipLocalsInit #скорость
Мы часто используем
Это работает быстро, но можно сделать быстрее. В данном случае, местом ускорения будет создание буфера. В платформе есть правило - каждая переменная перед использованием должна быть инициализирована. В данном случае, платформа осуществляет инициализацию
Атрибут
Я бы не стал использовать этот атрибут в обычном коде, но для библиотечного кода (который, надеюсь, обложен тестами) этот подход может весьма существенно ускорить работу с промежуточными буферами.
Также, немаловажно то, что чем больше буфер, тем более выгодно брать его из
Больше подробностей о том, как работает инициализация в C# с
Код бенчмарка в комментариях.
P.S.: Если же мы хотим пропускать инициализацию модно и молодёжно, не помечая сборку как unsafe, мы должны использовать
Мы часто используем
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
.👍28❤5
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:
- в доисторическом фреймворке в LOH также попадают массивы
- можно попросить сборщик мусора скомпактить LOH:
- При запуске программы с memory limit LOH не может употребить всю доступную память. OutOfMemory случится до достижения лимита
- Популярные причины аллокаций в LOH: промежуточные коллекции, создаваемые внутри Linq методов, MemoryStream
Pinned Object Heap (.NET 5) — куча, объекты в которой сборщик мусора гарантированно никогда не перемещает. В ней нет аналога
Аллокация подобных объектов в обычной куче и их пиннинг (запрет на перемещение) через
Также ходят легенды, что доступ к массиву через указатель без bound checks (проверок на выход за границу массива) быстрее, но это скорее легенда, приводящая к багам, чем реальность.
Через публичный API в Pinned Object Heap можно создавать только массивы:
Объекты в POH собираются сборщиком мусора, и при неаккуратном использовании POH может стать фрагментированным.
Frozen Object Heap (NonGC Object Heap. .NET 8) — куча для объектов, которые живут всё время жизни программы, никогда не собираются сборщиком мусора, не ссылаются на объекты вне FOH, и никогда не изменяются. В ней хранятся объекты, создаваемые рантаймом дотнета: строковые литералы, объекты
@epeshkblog
Объекты в .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
👍24❤10🗿2
Советы робота #отдых
Решил я тут снова попробовать GPT для банальных подсказок по работе. Задал вопрос - расскажи о простых и понятных способах оптимизации производительности на C#.
И вот, среди более-менее нормальных советов а-ля "подбери алгоритм", "используй правильные коллекции", нашёл странный - минимизируй использование переменных.
Я такой, типа, об-бъяснитесь, мьсё! Приведите пример! Ну он и выдал... А я опешил.
Из его объяснения выходило, что обычный
Вот это да!... Да? А вот нет. То есть да, работает быстрее, но дело, конечно же, не в "количестве переменных", а в векторизации, которую легко найти в исходном коде BCL. Получается, что робот как бы прав в результате, но не прав (причём сильно) в предпосылках и объяснениях этого результата.
Какой же можно сделать вывод? Да, роботы стали умнее собирать информацию и выдавать её нам. Но насколько нам полезен результат, если предпосылки ошибочные? Мне ночью приснился MR от молодого разработчика, который увидел этот ответ робота, проверил его, восхитился и... стал вычищать C# код от лишних переменных. Брр. А если бы это был экономист? Или юрист? Или медик? Короче говоря, роботов всё ещё нужно использовать аккуратно, внимательно проверяя и перепроверяя. Особенно по профессиональным вопросам.
Бенч элементарный, но он в комментах.
P.S.: Ну и вишенка на торте - робот ошибку признал. Что ему мешало сразу сказать правильно - загадка. Надеюсь, что это поправят.
Решил я тут снова попробовать GPT для банальных подсказок по работе. Задал вопрос - расскажи о простых и понятных способах оптимизации производительности на C#.
И вот, среди более-менее нормальных советов а-ля "подбери алгоритм", "используй правильные коллекции", нашёл странный - минимизируй использование переменных.
Я такой, типа, об-бъяснитесь, мьсё! Приведите пример! Ну он и выдал... А я опешил.
Из его объяснения выходило, что обычный
for
с промежуточной переменной ("первый способ" на картинке) должен работать медленнее, чем Sum()
("второй способ"), так как там нет переменных. Проверить - дело не хитрое, я написал бенч. Предложение создавать Enumerable.Range(1,100)
в бенчмарке я, конечно, отверг - явная ошибка, спишем на молодость робота. Cоздал массив заранее, запустил и... "второй способ" и правда работает быстрее!Вот это да!... Да? А вот нет. То есть да, работает быстрее, но дело, конечно же, не в "количестве переменных", а в векторизации, которую легко найти в исходном коде BCL. Получается, что робот как бы прав в результате, но не прав (причём сильно) в предпосылках и объяснениях этого результата.
Какой же можно сделать вывод? Да, роботы стали умнее собирать информацию и выдавать её нам. Но насколько нам полезен результат, если предпосылки ошибочные? Мне ночью приснился MR от молодого разработчика, который увидел этот ответ робота, проверил его, восхитился и... стал вычищать C# код от лишних переменных. Брр. А если бы это был экономист? Или юрист? Или медик? Короче говоря, роботов всё ещё нужно использовать аккуратно, внимательно проверяя и перепроверяя. Особенно по профессиональным вопросам.
Бенч элементарный, но он в комментах.
P.S.: Ну и вишенка на торте - робот ошибку признал. Что ему мешало сразу сказать правильно - загадка. Надеюсь, что это поправят.
👍25🔥8😁5
Структура как Span #решение #память #скорость
Разобравшись с InlineArray и SkipLocalsInit мы можем пойти дальше. Например, мы можем представить любую структуру как
Сделать Span из структуры достаточно просто:
Представляется, что примерно так работает представление структуры, отмеченной InlineArrayAttribute, когда мы говорим
Представление структуры как
При этом, нам надо быть крайне аккуратными, так как методы статического класса
Разобравшись с 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.: По моим данным, подчёркиваю. Как получаются именно мои наблюдения, я написал тут, повторяться не буду.
Взял тут. Откуда они взяли цифры - не знаю, но канал весьма авторитетный. В принципе, с моими данными эти цифры плюс-минус бьются. Среднее арифметическое значение по этой табличке:
Lead: 417k (если это он)
Senior: 316k
Middle: 276k
Максимумы:
Lead: 600k
Senior: 750k
Middle: 500k
Я к этим цифрам добавлю, что по моим знакомым и в последнее время всё больше заметно следующее. В компаниях, которые считаются "солью IT", зарплаты не очень конкурентные. То есть коллеги выезжают, во многом, благодаря ранее сформированному HR-бренду. У них именно среднее арифметическое.
Зарплаты близкие к максимальным чаще всего относятся к т.н. "ноу нейм" компаниям.
P.S.: По моим данным, подчёркиваю. Как получаются именно мои наблюдения, я написал тут, повторяться не буду.
Telegram
Банкста
Зарплаты айти-специалистов в центром федеральном округе. @banksta
🔥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 тут.
Наш сервис формирует отчёты в формате 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🔥18❤5
Forwarded from 📓 Записки программера
Тут на соседнем канале зашла речь про ускорение некоторых алгоритмов с помощью 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
Первый блок на скриншоте - просто мап на 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🔥5❤3
ByReferenceStringComparer #скорость
Недавно появилась задача быстро искать цифры по словарю. Ключ - строки, которые приходят извне, но которые всегда из определённого списка. Значение - число. В исходном коде на каждый запрос происходил поход в БД, создавался словарик, после этого по нему десятки (если не сотни) раз осуществлялся поиск по ключу.
Оптимизация первая. Ходить в БД за этими цифрами, очевидно, долго, да и данные обновляются редко, поэтому первая оптимизация - кэш через словарик на микросервисе. Это дало 50% прироста производительности. Неплохо. В принципе, заказчик уже был доволен, но мне хотелось большего.
Оптимизация вторая. Как я уже заметил, словарик, по ходу запроса, используется очень-очень часто. Это было отлично заметно в профайлере. Причём основное время тратилось на получение хэша и сравнение строк.
Я прогнал все входящие строки и все ключи из БД через штуку а-ля
Однако, сравнение через ссылку не давало мне покоя. Я вспомнил, что некто Евгений рассказывал, что Serilog не эффективно использует кэш темплейтов для получения подготовленного сообщения. Мол, в качестве
Не скажу, что последняя оптимизация внесла весомый вклад, но, тем не менее, она была достаточно полезная. Во всяком случае ещё 3-5% скорости могут помочь в ситуации высокой нагрузки.
Бенчмарк в комментариях.
Замечу, что подобный подход может помочь только (!) в случаях, когда набор строк ограничен (например, список названий областей страны) и по ним часто и много ищут. То есть не нужно использовать подход, если у вас разные строки, либо строка в алгоритме используется всего один раз. Пропуская все строки через аналог
Недавно появилась задача быстро искать цифры по словарю. Ключ - строки, которые приходят извне, но которые всегда из определённого списка. Значение - число. В исходном коде на каждый запрос происходил поход в БД, создавался словарик, после этого по нему десятки (если не сотни) раз осуществлялся поиск по ключу.
Оптимизация первая. Ходить в БД за этими цифрами, очевидно, долго, да и данные обновляются редко, поэтому первая оптимизация - кэш через словарик на микросервисе. Это дало 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 #скорость
Если хорошенько подумать, то
Напомню, что типы хранятся в приложении постоянно (скорее всего, исключая сценарии загрузки и выгрузки
Таким образом, если у нас есть
Бенчмарк в комментариях.
Запуск на MacBook.
P.S.: Если кого-то волнует, работает ли подобный подход в сценарии, когда мы создаём тип "налету" (а-ля
P.P.S.: Остаётся открытым вопрос, почему так не делают в .net по-умолчанию. Скорее всего, есть какой-то нюанс, который я упускаю (напр., тут). Возможно, мы вместе найдём на него ответ. Предположительный ответ.
Если хорошенько подумать, то
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. Следовательно, результат боксинга можно разместить на стеке.
Понимание механики работы этой оптимизации важно, так как, например, позволяет решить проблему следующего кода:
Дело в том, что в конкретно этом случае, по мнению JIT, в методе может быть создано слишком много "забокшеных" value-типов, что, в свою очередь, значит, что оптимизация применена не будет.
Есть и иное предположение. Оно основано на том, что функция
Кажется, что решение очевидно: нам нужно создать промежуточный метод, который будет определять типы (для понимания размера) и границы создаваемых временных переменных. Некий контекст, который подскажет JIT'у, что боксинг временный и нужен только на одну итерацию for.
Но, увы, это не сработает, так как в дело включается другая оптимизация - method inlining. Для JIT'a этот метод - прекрасный случай для автоматического инлайнинга. Увы, это ломает нашу прекрасную идею с обозначением контекста, в рамках которого будут жить наши boxed value-типы.
Значит, мы должны не только создать отдельный метод, но и прямо указать, что делать ему "инлайн" не нужно. Благо, у нас есть специальный атрибут для подобных указаний -
Код бенчмарка тут. Если нужно больше подробностей, то я написал этот пост под впечатлениями вот отсюда.
Наверное, многие слышали, что .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 #память #скорость
Несколько лет назад я устраивался в компанию, которая дала тестовое задание. Его суть - показать максимальную скорость и минимальную аллокацию при обработке большого объема данных. Что-то вроде "посчитать количество слов в документе" (я упрощаю).
Одна из основных проблем в подобной задаче - поиск ключа (слова) в большом словаре
В современном .NET 9 эта задача решается максимально просто, так как нам предоставили прекрасный метод словаря
В данном примере, сравнение
Бенчмарк в комментариях. Он содержит сравнение наивного подхода с реализацией через AlternateLookup. В наивном подходе мы создаём строки для поиска наличия ключа, а в случае с 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👍18❤2
Чтение из БД: Dapper, Linq2Db, EF #хранилище
Недавно в соседнем канале я наткнулся на обсуждение скорости работы ORM. Естественно, разговор крутился вокруг Dapper, Linq2Db и EntityFramework (EF, EF Core). Заявлялось, что библиотека
Я взял классическую задачу - есть блоги, у блогов есть посты. Я развернул базу данных PostgreSQL в Docker, добавил в неё 100 блогов по 200 постов. Код с использованием EF я написал быстро, с Linq2Db пришлось немного сложнее, но помогла документация, а вот Dapper дался сильно не сразу - именно поэтому по нему два бенчмарка.
Замечу, что я замерял только чтение (SELECT). Измерения скорости добавления и удаления я, возможно, произведу позже.
Тем более, что результаты чтения меня удивили, так как Linq2Db работает очень эффективно. При этом код, который необходимо написать для получения блогов и их постов, подозрительно краток и лаконичен. Ещё более меня удивило то, что скорость близка к Dapper (замер с двумя запросами) и немного выше, чем у рекомендованного для данного случая
Мой фаворит, по результатам замеров - Linq2Db, который весьма неплохо справляется с чтением данных из БД, при этом код устойчив к изменениям и прост, в отличии от того же Dapper. EF традиционно отстаёт, но проигрывает не сильно. При выполнении SQL-запросов через метод
Пока готовил бенчмарки, с ужасом осознал, что "готовить" EF я более-менее могу, а вот с Dapper и Linq2Db у меня сложности. Поэтому, если уважаемые читатели заметят какие-либо проблемы с кодом и его оптимальностью, я буду счастлив. Дело в том, что этим бенчмарком я получил неожиданные для себя результаты. Хотелось бы вернуть мир на место.
Кода много, поэтому он тут. Я думаю, что интересующимся не составит труда самостоятельно поднять PG в Docker'e и наполнить БД данными через миграцию в EF.
P.S.: Бенчи Ef_ToArray и Ef_ToList отдельно рассмотрены тут. То, что их результаты разные в данном измерении - скорее всего погрешность измерителя. Они должны быть плюс-минус одинаковыми.
Недавно в соседнем канале я наткнулся на обсуждение скорости работы 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🔥5❤3
EF: ToArray vs ToList #отдых
Бился в соседнем канале на тему поста выше. Появилось интересное предположение, что в EF лучше материализовать коллекции через ToList, так как ToArray имеет следующий код:
То есть в самом начале выполняется ToList, а потом, из уже созданного списка, выполняется ToArray. Программирование подсказывает нам, что будет создано две коллекции, одна из которых - лишняя.
Но на бенчмарке этого не заметно: скорость выполнения и аллокация почти идентичные. Как так получается и где программирование сломалось - загадка.
Бенч (сравнение ToList, ToArray) в комментах. Бенч из предыдущего поста, но на ToList, тут. Как и предполагалось скорость и аллокация одинаковые. Но почему?
Предполагаю, что его размер просто потерялся при 100 блогах и 200 постах. Если сделать один блог и один пост, и написать материализацию через
Бился в соседнем канале на тему поста выше. Появилось интересное предположение, что в 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👍6❤5
Новый params #скорость #память
Как многие знают, начиная с .NET 9 (C# 13) появилась возможность по новому взглянуть на ключевое слово
Многие давно ждали, когда params можно будет использовать с
Начиная с .NET 9 (C# 13) компилятор делает это сам.
Результаты хорошие - решение с
Бенчмарк в комментариях.
P.S.: Для особо пытливых, которые хотят понять, а почему есть разница между
Как многие знают, начиная с .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 и предложить самый популярный вариант". Роботы прямо предлагали решения из вот этой вот темы.
Это решение мягко говоря сомнительное и не оптимальное (см. Linq в результатах бенчмарка). Я уже встречался с таким лет 10 назад, когда stackoverflow-junior-программист предлагал подобный код. Впрочем, немного побеседовав с роботом, я таки получил предложение использовать современное решение на основе метода
Далее я попробовал узнать, а какое решение по созданию строки является криптостойким в .NET. Ответ был из всё той же темы на stackoverflow, мол, используй
В принципе, общение меня обрадовало. Теперь робот не бездумно настаивает на своём решении, а пытается предложить варианты и альтернативы, если предложенное им решение в чём-то не нравится пользователю. Это успех. С удовольствием продолжу наблюдать за роботами.
Также, я воспользовался случаем и написал бенчмарк о том, какое решение по созданию строки более оптимально для случаев строки в 12 символов, так это ограничение одного из методов (
P.S.: Иван, спасибо! Я попробовал.
P.P.S: Список тестов в этом посте - то и только то, что предлагали роботы. Речь не про самый оптимальный способ сгенерить строку, выбрать оптимальный из того, что предлагали роботы.
P.P.P.S: Всё дело было в промпте. Если задать грамотный вопрос, то робот возвращает грамотный ответ. То есть меня подвело то, что я мало работал с этим инструментом.
Недавно я снова упражнялся с роботами (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: Всё дело было в промпте. Если задать грамотный вопрос, то робот возвращает грамотный ответ. То есть меня подвело то, что я мало работал с этим инструментом.
👍15❤4😁4🔥2
Forwarded from AI-Driven Development. Родион Мостовой
А теперь, для любителей локальных моделей: 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 (сорри за жаргон, если вы не дотнетчик ). Но в целом, у меня впечатления отличные!
А как у вас себя ведут локальные модельки? С какими задачами справляются, а с какими нет? И какие модели вы используете локально? (если вообще используете)
Что-то мы все про проприетарщину да и проприетарщину. А что насчет локальных моделей?
Надо сказать, что на этом поприще у маленькмх опенсорных моделей тоже наблюдается какой-то фантастический буст. Например, 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