Зарплаты в 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
Быстрый и экономный 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 тут.
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
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
вы просто переложите поиск строки из одного места в другое.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 по-умолчанию. Скорее всего, есть какой-то нюанс, который я упускаю (напр., тут). Возможно, мы вместе найдём на него ответ. Предположительный ответ.
Аллокация объектов на стеке #память
Наверное, многие слышали, что .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)]
. В этом случае, боксинг происходит, но его результат остаётся на стеке. Результаты хорошо видны на бенчмарке.Код бенчмарка тут. Если нужно больше подробностей, то я написал этот пост под впечатлениями вот отсюда.
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 строки создаются только тогда, когда запись в словаре отсутствует. Более сложные сравнения с созданием специального словаря я опущу, хотя в этом случае, как мне кажется, всё-таки возможно выжать ещё немного скорости.
Чтение из БД: 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 отдельно рассмотрены тут. То, что их результаты разные в данном измерении - скорее всего погрешность измерителя. Они должны быть плюс-минус одинаковыми.
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 КБ, но она есть). Другое объяснение, что всё оптимизировали настолько, что просто магия.Новый 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
размером в количество элементов (про него я писал тут).Случайная строка из 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: Всё дело было в промпте. Если задать грамотный вопрос, то робот возвращает грамотный ответ. То есть меня подвело то, что я мало работал с этим инструментом.
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 (
А как у вас себя ведут локальные модельки? С какими задачами справляются, а с какими нет? И какие модели вы используете локально? (если вообще используете)
StorageS3 0.6.4 #решение #хранилище
На прошлой неделе выпустил новую версию библиотеки для экономного доступа к S3. Новая версия включает просьбы коллег по пробросу в библиотеку кастомного пула для массивов. Это было необходимо для случаев, когда стандартное поведение
Также, добавлены исправления от других коллег, за что им большое спасибо!
По традиции, обновил бенчмарки. Как оказалось,
Новая версия уже в nuget.
На прошлой неделе выпустил новую версию библиотеки для экономного доступа к S3. Новая версия включает просьбы коллег по пробросу в библиотеку кастомного пула для массивов. Это было необходимо для случаев, когда стандартное поведение
ArrayPool
не устраивало.Также, добавлены исправления от других коллег, за что им большое спасибо!
По традиции, обновил бенчмарки. Как оказалось,
AWS
(4.0.0) значительно улучшил производительность и потребление памяти. Minio тоже не стоит на месте, но всё ещё является не самой лучшей библиотекой для оптимального доступа к S3. Предыдущие замеры можно посмотреть тут.Новая версия уже в nuget.
Быстрый код #отдых #философия
Основатель компании id Software, гениальный и легендарный программист Джон Кармак признался, что вся современная индустрия компьютеров существует едва ли не благодаря не очень добросовестным программистам.
В своем микроблоге в заблокированной в России американской соцсети Х (ранее Twitter) он написал, что миллиарды людей запросто могли бы работать на старых компьютерах и не тратить деньги на новые, если бы разработчики ставили в приоритет оптимизацию своего программного кода.
Кармак открыто написал в своем микроблоге, что очень многие пользователи могли бы не тратить деньги на регулярное обновление ПК или покупку нового ноутбука.
"Большое количество пользователей по всему миру, чем многие могут себе представить, могли бы пользоваться устаревшим оборудованием, если бы оптимизация ПО на самом деле была приоритетом", – заявил создатель Doom.
P.S.: Это пост из канала banksta.
Основатель компании id Software, гениальный и легендарный программист Джон Кармак признался, что вся современная индустрия компьютеров существует едва ли не благодаря не очень добросовестным программистам.
В своем микроблоге в заблокированной в России американской соцсети Х (ранее Twitter) он написал, что миллиарды людей запросто могли бы работать на старых компьютерах и не тратить деньги на новые, если бы разработчики ставили в приоритет оптимизацию своего программного кода.
Кармак открыто написал в своем микроблоге, что очень многие пользователи могли бы не тратить деньги на регулярное обновление ПК или покупку нового ноутбука.
"Большое количество пользователей по всему миру, чем многие могут себе представить, могли бы пользоваться устаревшим оборудованием, если бы оптимизация ПО на самом деле была приоритетом", – заявил создатель Doom.
P.S.: Это пост из канала banksta.
IList as Span #скорость #память
В соседнем канале снова подняли вопрос по поводу разницы в скорости итерации по
Если кратко, то при итерации по
С тех пор, когда я про это писал, прошло много времени. Теперь я использую другой подход для случаев, когда коллеги используютвыпендриться бежать по нему быстро.
Этот метод есть в BCL, но является internal. Он весьма неплохо оптимизирован и используется, например, для случаев, когда нужно сделать
Вот код:
В принципе, всё весьма очевидно, кроме использования
Если нам нужно поддержать другую реализацию
Бенчмарк тут. Результаты на картинке.
P.S.: Обратите внимание на комментарий в коде BCL (
В соседнем канале снова подняли вопрос по поводу разницы в скорости итерации по
List<T>
и IList<T>
. Напомню, что я уже писал про это, но давно. Если кратко, то при итерации по
IList
возникает проблема с боксингом получаемого List<T>.Enumerator
, так как он кастится к IEnumerator<T>
. Это даёт 40 лишних байт аллокации. Также, вызов методов IEnumerator<T>
приводит к поиску конкретной реализации по таблице виртуальных методов (callvirt
в IL). Что, как не трудно догадаться, медленно, если существует более чем одна (это важно!) имплементация IEnumerator<T>
. С тех пор, когда я про это писал, прошло много времени. Теперь я использую другой подход для случаев, когда коллеги используют
IList
, а мне ну уж очень надо Этот метод есть в BCL, но является internal. Он весьма неплохо оптимизирован и используется, например, для случаев, когда нужно сделать
IEnumerable<T>.Sum
. Код меня более чем устраивает, так как позволяет избавиться от аллокации (боксинг List<T>.Enumerator
) и немного поднять скорость (не использовать callvirt
, про который я писал тут). В production-коде я редко встречаю собственные реализации IList
, поэтому метод работает очень неплохо.Вот код:
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool TryGetSpan<T>(this IEnumerable<T> source, out ReadOnlySpan<T> span)
{
bool result = true;
if (source.GetType() == typeof(T[]))
{
span = Unsafe.As<T[]>(source);
}
else if (source.GetType() == typeof(List<T>))
{
span = CollectionsMarshal.AsSpan(Unsafe.As<List<T>>(source));
}
else
{
span = default;
result = false;
}
return result;
}
В принципе, всё весьма очевидно, кроме использования
CollectionsMarshal
(о нём писал тут) для случая превращения List<T>
в Span<T>
. По скорости получается плюс-минус так же, как если бы я сделал CollectionsMarshal.AsSpan
, только с небольшой щепоткой unsafe в виде Unsafe.As
(для быстрого каста ссылочных типов).Если нам нужно поддержать другую реализацию
IList
, нужно просто добавить этот случай (source.GetType() == typeof(MyList)
) в этот метод. Обратите внимание, что это extension, что позволяет использовать его весьма удобно. Бенчмарк тут. Результаты на картинке.
P.S.: Обратите внимание на комментарий в коде BCL (
this could be changed to a cast in the future if the JIT starts to recognize it
). Я понимаю это так, что чаяния разработчиков, которые хотят, чтобы компилятор выполнял код из этого метода на этапе компиляции... как минимум коллегами имеются ввиду и могут быть реализованы. Но в будущем. Наверное. Хотя, может быть, я выдаю желаемое за действительное.