PerformanceBehaviour #скорость #решение
Давайте вспомним, как замерять производительность на уровне "плюс-минус лапоть". Иногда это полезно для верхнеуровневой оценки того, что в нашем приложении выполняется медленно.
Например, если мы пользуемся MediatR, то мы должны знать о концепции Bahaviour - эта такая штука, которая как бы "оборачивает" остальной pipeline выполнения наших handler'ов, pre и post-процессоров.
Чтобы написать behaviour для замеров, мы просто делаем следующее:
Его можно сделать обобщенным и заставить MediatR вставлять его во все запросы.
Минусы:
1.
2. В зависимости от реализации
3. Нужно обращать внимание на порядок регистрации behaviour - это поведение должно быть самым первым.
4. Stopwatch это класс. В принципе, чтобы избежать аллокации мы можем воспользоваться разницей
P.S.: Да, я знаю, что всё это можно сделать с помощью Middleware, если у нас сетевое приложение. Увы, MediatR не всегда используется только в серверных приложениях. Ещё можно посмотреть в сторону Activity и Telemetry.
P.P.S.: Я не рекомендую MediatR, но я учитываю, что его использование распространено.
Давайте вспомним, как замерять производительность на уровне "плюс-минус лапоть". Иногда это полезно для верхнеуровневой оценки того, что в нашем приложении выполняется медленно.
Например, если мы пользуемся MediatR, то мы должны знать о концепции Bahaviour - эта такая штука, которая как бы "оборачивает" остальной pipeline выполнения наших handler'ов, pre и post-процессоров.
Чтобы написать behaviour для замеров, мы просто делаем следующее:
public sealed class PerformanceBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse> {
private static readonly TimeSpan RequestLimit = TimeSpan.FromSeconds(1);
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken ct) {
var timer = Stopwatch.StartNew();
try
{
return await next();
}
finally
{
timer.Stop();
if (timer.Elapsed > RequestLimit)
{
// пишем, что медленно
}
}
}
}
Его можно сделать обобщенным и заставить MediatR вставлять его во все запросы.
Минусы:
1.
RequestLimit
не может быть меньше, чем Stopwatch.Frequency
. Короче говоря, это подходит для замеров "плюс-минус лапоть" (я бы поставил 1 секунду). Чисто для того, чтобы заметить резкий всплеск в логах и углубиться в исследование.2. В зависимости от реализации
IMediator
(а они, внезапно, бывают разные), наличие или отсутствие behaviour может само по себе накладывать перформансный эффект на выполнение pipeline'a. Однако, это скорее мили- и нано-секунды.3. Нужно обращать внимание на порядок регистрации behaviour - это поведение должно быть самым первым.
4. Stopwatch это класс. В принципе, чтобы избежать аллокации мы можем воспользоваться разницей
Environment.TickCount64
в начале и при окончании операции, чтобы определить прошедшее время. Но, чаще всего, это излишние микрооптимизации в замерах "на глазок". Либо, мы можем воспользоваться ValueStopwatch, как предлагает коллега.P.S.: Да, я знаю, что всё это можно сделать с помощью Middleware, если у нас сетевое приложение. Увы, MediatR не всегда используется только в серверных приложениях. Ещё можно посмотреть в сторону Activity и Telemetry.
P.P.S.: Я не рекомендую MediatR, но я учитываю, что его использование распространено.
👍16❤1
ConcurrentDictionary #собес #память
Коллеги, задачка. Прямо с собеса, с пылу-с-жару. Сколько раз значение будет выведено в консоль?
Усложняем:
1. Теперь заменим тип значения у
2. А теперь изменим тип значения на
Как же так? Надо пояснить в чём, собственно, дело. Можно скопировать и воспроизвести у себя. И я настоятельно (!) рекомендую это сделать.
---
С одной стороны, суть происходящего проста: ConcurrentDictionary не гарантирует, что элемент по ключу будет создан один и только один раз. Это делается для исключения проблем, которые могут возникнуть при выполнении неизвестного кода под блокировкой, что верно заметил коллега.
А другой стороны, суть происходящего в сложном сценарии не так проста, но всё ещё, кстати, логична.
Когда мы отпускаем
Причём скорость зависит (c) от всего сразу: размера данных, количества процессоров, разрядности процессора и скорости памяти. Например, размер
Мораль всего этого такова:осторожнее на собеседованиях. Нет, мораль другая - если мы пишем асинхронный код, то было бы неплохо протестировать наши результаты не только у себя, но, также, на целевой машине и у того парня. Возможно, наш гениальный код будет работать тут, но не там, а вот там - он вообще всё сломает.
Коллеги, задачка. Прямо с собеса, с пылу-с-жару. Сколько раз значение будет выведено в консоль?
const string Key = "SomeKey";
var topLevel = new ConcurrentDictionary<string, string>();
for (var i = 1; i < 100; i++) {
var thread = new Thread(Add);
thread.Start(topLevel);
}
static void Add(object? val) {
var dict = (ConcurrentDictionary<string, string>)val!;
dict.GetOrAdd(Key, x => {
lock (Lock.Obj)
{
if (dict.TryGetValue(Key, out var value)) return value;
var res = Guid.NewGuid().ToString();
Console.WriteLine(res);
return res;
}
});
}
Усложняем:
1. Теперь заменим тип значения у
ConcurrentDictionary
на Guid
. Естественно строку var res = Guid.NewGuid().ToString();
нужно исправить как var res = Guid.NewGuid()
.2. А теперь изменим тип значения на
int
.Как же так? Надо пояснить в чём, собственно, дело. Можно скопировать и воспроизвести у себя. И я настоятельно (!) рекомендую это сделать.
---
С одной стороны, суть происходящего проста: ConcurrentDictionary не гарантирует, что элемент по ключу будет создан один и только один раз. Это делается для исключения проблем, которые могут возникнуть при выполнении неизвестного кода под блокировкой, что верно заметил коллега.
А другой стороны, суть происходящего в сложном сценарии не так проста, но всё ещё, кстати, логична.
Когда мы отпускаем
lock
, то железке нужно время записать данные в память. И чем бОльший объем данных мы записываем в память, тем это дольше, что подтверждает эмпирический опыт коллеги и дальнейшее развитие рассуждений.Причём скорость зависит (c) от всего сразу: размера данных, количества процессоров, разрядности процессора и скорости памяти. Например, размер
int
это 4, а string
- 8. И, если повезёт, это, в свою очередь, равно sizeof(IntPtr)
или nint
(в зависимости от разрядности процессора). Если разрядность позволяет записать данные в память за одну операцию, то это дает другим потокам (ядрам) больше времени посоревноваться на скорость. Если поток всего один - то конкуренции нет.Мораль всего этого такова:
🔥6👍2❤1👏1
Random #скорость
Когда мы используем
Более того, чаще всего Random используется просто для некоторой случайности или её имитации. Например, в тестах, UI или компьютерных играх. Последние особенно круто вводят игроков в заблуждение, поскольку у этих ребят каждый кадр на счету.
Ни для кого не будет открытием, что
Но что, если нам нужно быстрее? Ответ, как всегда, есть.
Если нам нужна некоторая последовательность чисел, которая просто случайная для 5-10 секунд, то нам поможет их запись в отдельный массив. Да, последовательность цифр будет одинаковая, но за счёт того, что они берутся в разное время для разных сущностей или ситуаций, мы получаем... случайность.
Естественно, раз в некоторый такт системы (см. метод
Надо ли говорить, что такой подход сильно быстрее тех алгоритмов, которые существуют в .NET "из коробки". Естественно, с ограничениями, которые можно заметить на сравнении 100 и 1000 в бенчмарке.
Код в комментариях.
P.S.: Я этот подход явно где-то подглядел. Если кто-то знает научное название, то напишите в комментах - всем будет приятнее и понятнее.
P.P.S: Коллега прогнал бенч на Intel + Win11 - результат тут. Он интересный.
Когда мы используем
Random
, мы явно не имеем ввиду ничего криптостойкого. Собственно, в документации прямо так и сказано: генерирует последовательность чисел, отвечающую определенным статистическим критериям случайности. Это так называемые псевдо-случайные числа.Более того, чаще всего Random используется просто для некоторой случайности или её имитации. Например, в тестах, UI или компьютерных играх. Последние особенно круто вводят игроков в заблуждение, поскольку у этих ребят каждый кадр на счету.
Ни для кого не будет открытием, что
Random.Shared
быстрее обычного Random
за счёт имплементации более интересного алгоритма.Но что, если нам нужно быстрее? Ответ, как всегда, есть.
Если нам нужна некоторая последовательность чисел, которая просто случайная для 5-10 секунд, то нам поможет их запись в отдельный массив. Да, последовательность цифр будет одинаковая, но за счёт того, что они берутся в разное время для разных сущностей или ситуаций, мы получаем... случайность.
Естественно, раз в некоторый такт системы (см. метод
Dice.Update
, который обновляет внутреннюю коллекцию псевдо-случайных чисел) мы можем обновить случайную последовательность, что и создаёт иллюзию случайности вообще.Надо ли говорить, что такой подход сильно быстрее тех алгоритмов, которые существуют в .NET "из коробки". Естественно, с ограничениями, которые можно заметить на сравнении 100 и 1000 в бенчмарке.
Код в комментариях.
P.S.: Я этот подход явно где-то подглядел. Если кто-то знает научное название, то напишите в комментах - всем будет приятнее и понятнее.
P.P.S: Коллега прогнал бенч на Intel + Win11 - результат тут. Он интересный.
👍19🔥2❤1
LeetCode #скорость
Когда вы тренируете свои навыки в области алгоритмов с помощью сторонних ресурсов - это прекрасно! Честь и хвала таким ресурсам! Там есть задачи, на которых можно тренировать навыки, которые всегда понадобятся намв разработке на собеседовании.
Однако, есть один неприятный нюанс. Даже три. Два понятных, а один - такой себе.
Например, задачка TwoSum. Это тот самый "изян", который спрашивают на собеседованиях в разные конторы. Так что, собственно, мне не нравится?
Во-первых, наше решение запускаются на непонятных машинах. Например, на моём скриншоте решение задачи выполнялось аж 156 миллисекунд.
Да, согласен, у меня банальный брутфорс, решение "в лоб", да и вообще, я это всё написал для примера. Но... 156, да ещё и миллисекунд, перебор. То есть то, на чём запускается наш алгоритм будет, скорее всего, не самой мощной и не самой нормальной тачкой. Следовательно, доверять измерениям скорости нельзя.
Во-вторых, я употребил аж 47 Мб. Гайс, ну, коммон. Я даже ничего не аллоцировал в своём решении. Откуда 47 Мб? Хотя, наверное, это всё та же машина (или процесс), которая поверхностно измеряется в плане потребляемой памяти. Скорее всего, это ценник за весь дотнет вообще. Странно, конечно, но понятно, и тоже принимается.
В-третьих, и, видимо, в главных. Я надеюсь, что в этих задачах никто не гонится за первенство в скорости или потребления памяти. Да, судя по графику, там есть люди, которые выполнили задачу за 80 миллисекунд или за 45 мегабайт... Но по факту, все эти цифры невероятно завышены. Я бы сказал, на порядки. И взяты с потолка. Ну то есть вообще.
Причём даже относительные цифры сомнительны, верить им нельзя. Так, мой алгоритм выдавал то 104, то 156 мс при разных запусках. Так и в чём соревнование? Неужели в предложении купить подписку?... Нет, надеюсь, что это не так.
Код бенчмарка, который работает 105 наносекунд и использует 448 байт памяти без всяких оптимизаций - в комментах. Задачка, ещё раз, вот эта.
P.S.: Предположение о том, почему "47 МБ" - высказано коллегой тут.
Когда вы тренируете свои навыки в области алгоритмов с помощью сторонних ресурсов - это прекрасно! Честь и хвала таким ресурсам! Там есть задачи, на которых можно тренировать навыки, которые всегда понадобятся нам
Однако, есть один неприятный нюанс. Даже три. Два понятных, а один - такой себе.
Например, задачка TwoSum. Это тот самый "изян", который спрашивают на собеседованиях в разные конторы. Так что, собственно, мне не нравится?
Во-первых, наше решение запускаются на непонятных машинах. Например, на моём скриншоте решение задачи выполнялось аж 156 миллисекунд.
Да, согласен, у меня банальный брутфорс, решение "в лоб", да и вообще, я это всё написал для примера. Но... 156, да ещё и миллисекунд, перебор. То есть то, на чём запускается наш алгоритм будет, скорее всего, не самой мощной и не самой нормальной тачкой. Следовательно, доверять измерениям скорости нельзя.
Во-вторых, я употребил аж 47 Мб. Гайс, ну, коммон. Я даже ничего не аллоцировал в своём решении. Откуда 47 Мб? Хотя, наверное, это всё та же машина (или процесс), которая поверхностно измеряется в плане потребляемой памяти. Скорее всего, это ценник за весь дотнет вообще. Странно, конечно, но понятно, и тоже принимается.
В-третьих, и, видимо, в главных. Я надеюсь, что в этих задачах никто не гонится за первенство в скорости или потребления памяти. Да, судя по графику, там есть люди, которые выполнили задачу за 80 миллисекунд или за 45 мегабайт... Но по факту, все эти цифры невероятно завышены. Я бы сказал, на порядки. И взяты с потолка. Ну то есть вообще.
Причём даже относительные цифры сомнительны, верить им нельзя. Так, мой алгоритм выдавал то 104, то 156 мс при разных запусках. Так и в чём соревнование? Неужели в предложении купить подписку?... Нет, надеюсь, что это не так.
Код бенчмарка, который работает 105 наносекунд и использует 448 байт памяти без всяких оптимизаций - в комментах. Задачка, ещё раз, вот эта.
P.S.: Предположение о том, почему "47 МБ" - высказано коллегой тут.
👍13❤4🔥1
Как читать бенчмарки #бенч
Давайте мы немного вспомним основы, и напомним, как читать результаты BenchmarkDotNet.
Итак, когда мы создали первый бенчмарк мы получаем примерно такую табличку с данными. Казалось бы, ну чего тут такого. Рассказываю.
1. В заголовке мы видим версию BenchmarkDotNet. Это важно, так как .NET меняется, а значит меняется и прибор, с помощью которого его измеряют.
2. Далее следует версия ОС. Это важно. Например, запуск на Windows и Linux может отличаться.
3. Далее идёт информация о процессоре. В данном случае она "Unknown processor", так как это контейнер. Однако, мы не можем сомневаться в том, что тип и разрядность процессора влияют на скорость работы.
4. Далее идёт версия .NET. Напомню, что разница производительности некоторых версий .NET поразительна. Иногда есть деградация, иногда - прорыв. На неё нужно обращать внимание.
5.
6.
7.
8.
9.
10.
Подробно о том, что есть что - пишется после каждого бенчмарка.
Давайте мы немного вспомним основы, и напомним, как читать результаты BenchmarkDotNet.
Итак, когда мы создали первый бенчмарк мы получаем примерно такую табличку с данными. Казалось бы, ну чего тут такого. Рассказываю.
1. В заголовке мы видим версию BenchmarkDotNet. Это важно, так как .NET меняется, а значит меняется и прибор, с помощью которого его измеряют.
2. Далее следует версия ОС. Это важно. Например, запуск на Windows и Linux может отличаться.
3. Далее идёт информация о процессоре. В данном случае она "Unknown processor", так как это контейнер. Однако, мы не можем сомневаться в том, что тип и разрядность процессора влияют на скорость работы.
4. Далее идёт версия .NET. Напомню, что разница производительности некоторых версий .NET поразительна. Иногда есть деградация, иногда - прорыв. На неё нужно обращать внимание.
5.
Method
- имя бенчмарка. Каждый бенчмарк запускается изолировано и сопровождается отдельным прогревом (подробности пока оставим). Мы можем быть уверенными в том, что бенчмарки не влияют друг на друга.6.
Mean
- это время выполнения бенчмарка. Замечу, что по-умолчанию, это усреднённое время выполнения 15 бенчмарков. Иногда - большего количества, если в процессе их выполнения были обнаружены статистические выбросы - тогда количество повторений увеличится вплоть до 100.7.
Ratio
- это отклонение скорости работы отдельных бенчмарков относительно основного (Baseline
). Его можно узнать по цифре "1.00". В данном случае это Storage
.8.
Gen0
, Gen1
, Gen2
- среднее количество сборок мусора по поколению на одно исполнение бенчмарка. Особенно важно, что эта статистика указывает, насколько наш GC (см. ОС и процессор) в нашем сценарии должен успевать собирать мусор.9.
Allocated
- общее количество аллоцированых данных в памяти. Помогает оценить верхнюю границу памяти, затраченной на один бенчмарк.10.
Alloc Ratio
- относительное количество затраченной памяти. Помогает оценивать работу алгоритма относительно Baseline
.Подробно о том, что есть что - пишется после каждого бенчмарка.
👍26
Разные платформы и процессоры #бенч
В продолжение разговора о разных ОС и процессорах, который был начат в посте про Random, необходимо понимать следующее.
И это нужно знать. Это нужно проверять. И с этим нужно смириться.
Ваш код на разных платформах и на разных процессорах будет исполняться по разному. Такова реальность. Особенно, если дело касается микрооптимизаций.
Вот, например, у нас простейший бенчмарк. Это старый бенчмарк, который я периодически запускаю на разных версиях .NET и на разных компьютерах. Бенчмарк пытается воспроизвести Array bound check elimination (оптимизацию JIT, которая позволяет избегать проверки границ массива). Штука уже не очень актуальная, но я люблю этот бенч, так как я подсмотрел его аж в 2015 году у некого Андрея.
И вот, мы снова запускаем его в 2024 году на одном .NET 8, но на разных машинах.
Что мы видим:
1. Версия BenchmakDotNet и самого .NET одинаковая.
2. Разные ОС (Windows 11 и MacOS 14.5).
3. Разные процессоры (Ryzen 5800H и M3 Max).
4. Разная скорость исполнения (46 ns против 29 ns).
5. И совершенно разные относительные результаты.
Если на Windows результат
IL код в моей IDE одинаковый.
Вывод: знайте результаты на целевой ОС и целевых процессорах. В идеале, нужно встроить проверку работы ключевых алгоритмов прямо в CI/CD. Всё иное (я говорю про микрооптимизации) - результат на вашей и только вашей машине.
Код бенчмарка в комментариях.
В продолжение разговора о разных ОС и процессорах, который был начат в посте про Random, необходимо понимать следующее.
И это нужно знать. Это нужно проверять. И с этим нужно смириться.
Ваш код на разных платформах и на разных процессорах будет исполняться по разному. Такова реальность. Особенно, если дело касается микрооптимизаций.
Вот, например, у нас простейший бенчмарк. Это старый бенчмарк, который я периодически запускаю на разных версиях .NET и на разных компьютерах. Бенчмарк пытается воспроизвести Array bound check elimination (оптимизацию JIT, которая позволяет избегать проверки границ массива). Штука уже не очень актуальная, но я люблю этот бенч, так как я подсмотрел его аж в 2015 году у некого Андрея.
И вот, мы снова запускаем его в 2024 году на одном .NET 8, но на разных машинах.
Что мы видим:
1. Версия BenchmakDotNet и самого .NET одинаковая.
2. Разные ОС (Windows 11 и MacOS 14.5).
3. Разные процессоры (Ryzen 5800H и M3 Max).
4. Разная скорость исполнения (46 ns против 29 ns).
5. И совершенно разные относительные результаты.
Если на Windows результат
_array.Length
и константы примерно одинаковый, то на MacOS результаты разные, что несколько внезапно и совершенно не понятно.IL код в моей IDE одинаковый.
Вывод: знайте результаты на целевой ОС и целевых процессорах. В идеале, нужно встроить проверку работы ключевых алгоритмов прямо в CI/CD. Всё иное (я говорю про микрооптимизации) - результат на вашей и только вашей машине.
Код бенчмарка в комментариях.
👍14🔥4❤3
Аутстафф #философия
Коллеги, у меня нет предубеждения перед аутстаффом. Ну, я про работу на галеры. Про компании, которые продают разработчиков другим компаниям. Я лишь хочу указать на некоторые нюансы для коллег с той и с другой стороны.
Итак, в чём преимущества быть аутстаффом:
1. Можно обучиться работать с разными технологиями и проектами. Быстро. Много. Это очень ценный опыт. Резюме будет прекрасным!
2. Рост от джуна до сеньора для серьезных мужиков и девчонок может быть стремительным. Если, конечно, слушать и запоминать. Проверено на собственном опыте.
3. Если проект или технологии не нравятся - их можно быстро сменить. Просто говоришь начальнику галеры, и он договаривается с покупателем. Ну или просит остаться за бонус. Всем хорошо.
4. Вроде как неплохо принимают джунов.
В чем плюсы аутстаффа для покупателя:
1. Экстренный ресурс. Например, быстро закрыть потребность в разработчиках на критических стадиях проекта.
2. Легко уволить. Надо просто не продлевать контракт. Никаких проблем с отработками и дополнительными выплатами - по законодательству аутстафф работает на другую компанию.
3. Чаще всего персонал как минимум уровня middle. Это значит, что коллеги умеют и будут копать. Наверное. Возможно. Если не подсунут джуна.
В чём минусы быть аутстаффом:
1. Могут не найти проект. В этом случае надо будет раскладывать пасьянс. Я серьёзно. Просто ходишь на работу и, например, пишешь какие-то внутренние документы, которые никто не читает. Или прям пасьянс.
2. Иногда ЗП зависит от прибыли с проекта. Например, ЗП это МРОТ, а всё остальное - бонус. В этом случае, получить нормальные деньги можно только тогда, когда есть проект. Иначе - пасьянс.
3. Мутные схемы работы. Например, иногда надо работать под акаунтом человека, который давно уволился. Но покупатель об этом не знает. И надо скрывать. И соответствовать тому чуваку. Помню, я уволился, а под моим акаунтом в одной американской компании ещё лет 5 сидели 10 разных людей.
4. Иногда надо работать на два проекта. Но каждый из покупателей об этом не знает. Например, вы работаете на проект и компанию А, а в Б внезапно надо быстро что-то сделать. Без погружения, без всяких сложностей. Дали задачу, дали доступ к репо - вперёд, есть ночь.
5. Иногда в резюме для покупателя пишут не то, что умеешь, а потом надо как-то притворяться тем, кем ты не являешься.
6. Увольнение стремительное и без объяснений.
7. В своё время в одной крупной американской компании, которая имела деятельность на территории РФ, были бейджики двух разных цветов. И снять его было нельзя. Надо ли говорить, что права разных бейджиков были разными?
В чем минусы аутстаффа для покупателя:
1. Иногда middle не нужен. А senior’ов нет, так как все они ушли в продуктовые компании. Поэтому будут подсовывать мидлов под видом сеньёров.
2. Обучение нового сотрудника не быстрое, а его могут просто не продлить на следующий месяц. И всё обучение «в трубу».
3. Врут в резюме. Или можно наткнуться на кандидата, который вообще не имеет опыта в нужных технологиях, но оплата была за каждого, кто был предоставлен, а поэтому гонят всех подряд. Без оценки. C++ и C#, ну какая, к бесу, разница?
4. Каждого кандидата надо собеседовать. Также, как и обычных. Иначе можно попасть в ситуацию, что подослали джуна с питоном, который работает на нескольких проектах.
5. Ад с доступами. Аутстафф не всегда человек по версии ИБ - нужна куча дополнительных согласований. И, кстати, не факт, что успешных.
6. Нужны хорошие процессы производства. Если у покупателя задачи в трекере описаны тезиснно, документации нет, тестов мало - будет большой проблемой объяснить аутстаффу, а что, собственно, нужно сделать.
Как это связано с производительностью? Если вы лид, то напрямую - вашу команду усиливают, а значит будут требовать результаты. Знайте плюсы и минусы. Ну, а если вы аутстафф - знайте то, в какой ситуации вам надо высекать каменный цветок.
P.S.: Привет моим коллегам с галеры! Это было крутое время!
Коллеги, у меня нет предубеждения перед аутстаффом. Ну, я про работу на галеры. Про компании, которые продают разработчиков другим компаниям. Я лишь хочу указать на некоторые нюансы для коллег с той и с другой стороны.
Итак, в чём преимущества быть аутстаффом:
1. Можно обучиться работать с разными технологиями и проектами. Быстро. Много. Это очень ценный опыт. Резюме будет прекрасным!
2. Рост от джуна до сеньора для серьезных мужиков и девчонок может быть стремительным. Если, конечно, слушать и запоминать. Проверено на собственном опыте.
3. Если проект или технологии не нравятся - их можно быстро сменить. Просто говоришь начальнику галеры, и он договаривается с покупателем. Ну или просит остаться за бонус. Всем хорошо.
4. Вроде как неплохо принимают джунов.
В чем плюсы аутстаффа для покупателя:
1. Экстренный ресурс. Например, быстро закрыть потребность в разработчиках на критических стадиях проекта.
2. Легко уволить. Надо просто не продлевать контракт. Никаких проблем с отработками и дополнительными выплатами - по законодательству аутстафф работает на другую компанию.
3. Чаще всего персонал как минимум уровня middle. Это значит, что коллеги умеют и будут копать. Наверное. Возможно. Если не подсунут джуна.
В чём минусы быть аутстаффом:
1. Могут не найти проект. В этом случае надо будет раскладывать пасьянс. Я серьёзно. Просто ходишь на работу и, например, пишешь какие-то внутренние документы, которые никто не читает. Или прям пасьянс.
2. Иногда ЗП зависит от прибыли с проекта. Например, ЗП это МРОТ, а всё остальное - бонус. В этом случае, получить нормальные деньги можно только тогда, когда есть проект. Иначе - пасьянс.
3. Мутные схемы работы. Например, иногда надо работать под акаунтом человека, который давно уволился. Но покупатель об этом не знает. И надо скрывать. И соответствовать тому чуваку. Помню, я уволился, а под моим акаунтом в одной американской компании ещё лет 5 сидели 10 разных людей.
4. Иногда надо работать на два проекта. Но каждый из покупателей об этом не знает. Например, вы работаете на проект и компанию А, а в Б внезапно надо быстро что-то сделать. Без погружения, без всяких сложностей. Дали задачу, дали доступ к репо - вперёд, есть ночь.
5. Иногда в резюме для покупателя пишут не то, что умеешь, а потом надо как-то притворяться тем, кем ты не являешься.
6. Увольнение стремительное и без объяснений.
7. В своё время в одной крупной американской компании, которая имела деятельность на территории РФ, были бейджики двух разных цветов. И снять его было нельзя. Надо ли говорить, что права разных бейджиков были разными?
В чем минусы аутстаффа для покупателя:
1. Иногда middle не нужен. А senior’ов нет, так как все они ушли в продуктовые компании. Поэтому будут подсовывать мидлов под видом сеньёров.
2. Обучение нового сотрудника не быстрое, а его могут просто не продлить на следующий месяц. И всё обучение «в трубу».
3. Врут в резюме. Или можно наткнуться на кандидата, который вообще не имеет опыта в нужных технологиях, но оплата была за каждого, кто был предоставлен, а поэтому гонят всех подряд. Без оценки. C++ и C#, ну какая, к бесу, разница?
4. Каждого кандидата надо собеседовать. Также, как и обычных. Иначе можно попасть в ситуацию, что подослали джуна с питоном, который работает на нескольких проектах.
5. Ад с доступами. Аутстафф не всегда человек по версии ИБ - нужна куча дополнительных согласований. И, кстати, не факт, что успешных.
6. Нужны хорошие процессы производства. Если у покупателя задачи в трекере описаны тезиснно, документации нет, тестов мало - будет большой проблемой объяснить аутстаффу, а что, собственно, нужно сделать.
Как это связано с производительностью? Если вы лид, то напрямую - вашу команду усиливают, а значит будут требовать результаты. Знайте плюсы и минусы. Ну, а если вы аутстафф - знайте то, в какой ситуации вам надо высекать каменный цветок.
P.S.: Привет моим коллегам с галеры! Это было крутое время!
👍26❤2🥱2
Микросервисы vs монолит #доклад
Что-то я пропустил доклад некого Станислава о монолитах, через микросервисы и обратно, но уже в модули. Всем, кого данная тема волнует, я рекомендую это видео.
1. Глубоко, как это принято у Станислава, затронута история вопроса и предпосылки, которые толкают нас от монолитов к микросервисам.
2. Отмечен чисто эмпирический эффект, когда разработчики (или требуют заказчики) закладывают ресурсы на каждый микросервис без понимания границы ресурсов кластера при наличии горизонтального размножения сервисов.
3. Рассказана байка о "черной пятнице", когда взрывной рост нагрузки вызывает каскадный эффект на сотне микросервисов, которые пытаются удвоить потребление ресурсов.
4. Также, подробно, как Станислав любит, рассказана история слияния микросервисов обратно в монолит.
5. Продемонстрирована предварительная статистика слияния 11 сервисов в один, что привело к снижению потребления ОЗУ в 5 раз, а процессора - в 2 раза.
6. Затронут вопрос о том, что взаимодействие сервисов тоже стоит процессорного времени и ОЗУ. И это ещё мы забываем про то, что наличие микросервисов ест ресурсы внешних систем - очередей, балансеров, различных демонов и прочее-прочее.
7. Модульная архитектура позволяет отложить вопрос о том, нужен нам модульный монолит или всё-таки микросервисы, так как вы можете принять решение в моменте.
Код Станислав выложил вот сюда.
Также, я настоятельно рекомендую раскрутить некого Руслана ещё раз рассказать, но уже на камеру, вот этот доклад про путь от микросервисов к модулям.
Что-то я пропустил доклад некого Станислава о монолитах, через микросервисы и обратно, но уже в модули. Всем, кого данная тема волнует, я рекомендую это видео.
1. Глубоко, как это принято у Станислава, затронута история вопроса и предпосылки, которые толкают нас от монолитов к микросервисам.
2. Отмечен чисто эмпирический эффект, когда разработчики (или требуют заказчики) закладывают ресурсы на каждый микросервис без понимания границы ресурсов кластера при наличии горизонтального размножения сервисов.
3. Рассказана байка о "черной пятнице", когда взрывной рост нагрузки вызывает каскадный эффект на сотне микросервисов, которые пытаются удвоить потребление ресурсов.
4. Также, подробно, как Станислав любит, рассказана история слияния микросервисов обратно в монолит.
5. Продемонстрирована предварительная статистика слияния 11 сервисов в один, что привело к снижению потребления ОЗУ в 5 раз, а процессора - в 2 раза.
6. Затронут вопрос о том, что взаимодействие сервисов тоже стоит процессорного времени и ОЗУ. И это ещё мы забываем про то, что наличие микросервисов ест ресурсы внешних систем - очередей, балансеров, различных демонов и прочее-прочее.
7. Модульная архитектура позволяет отложить вопрос о том, нужен нам модульный монолит или всё-таки микросервисы, так как вы можете принять решение в моменте.
Код Станислав выложил вот сюда.
Также, я настоятельно рекомендую раскрутить некого Руслана ещё раз рассказать, но уже на камеру, вот этот доклад про путь от микросервисов к модулям.
YouTube
Станислав Сидристый — Гибридная архитектура: слияние микросервисов в монолит по необходимости
Подробнее о конференции DotNext: https://jrg.su/3WmFRE
— —
При необходимости работать в различных окружениях — и на дистанции в несколько сотен серверов, и на одном сервере на вообще все сервисы — возникает целый ряд проблем, совершенно неспецифичных в обычной…
— —
При необходимости работать в различных окружениях — и на дистанции в несколько сотен серверов, и на одном сервере на вообще все сервисы — возникает целый ряд проблем, совершенно неспецифичных в обычной…
👍24❤6
ObjectPool #память #решение
Есть такая работа - ObjectPool. Это когда мы не позволяем GC уничтожать объекты, которые живут очень короткое время, а используем их снова и снова. Это очень помогает в экономии памяти.
Если кто не знал, то
Существует хорошая библиотека, которая позволяет это делать. Причём не от кого-нибудь, а от вендора. Это быстрая и хорошо написанное решение.
Но что делать, если нам нужно быстрее?
Ответ, как всегда, есть.
1. Велосипед. Это
2. Можно воспользоваться той самой "другой" библиотекой. Скорость выше не на порядки, но, тем не менее, выше. А хорошее и подробное описание кода может погрузить нас в прекрасный мир высокой производительности.
В результате, мы получим либо аналогичную производительность (но без зависимостей), либо производительность чуть-чуть выше, чем у того, что предлагает нам вендор.
При этом, внимательный читатель наверняка заметил, что самым эффективным пулом для маленьких объектов является некий
Если кому-то мало комментариев Евгения и хочется большего, то могу сообщить дополнительно, что имплементация ConcurrentQueue в .NET выросла вот отсюда. Она же, чаще всего, лежит в основах примеров в интернете.
Кода много, поэтому он тут.
Есть такая работа - ObjectPool. Это когда мы не позволяем GC уничтожать объекты, которые живут очень короткое время, а используем их снова и снова. Это очень помогает в экономии памяти.
Если кто не знал, то
new
- дорогая операция (хотя есть мнение, что нет), которая требует выделить памяти в heap (для классов, конечно же). Ну в куче, которую контролирует GC. Чтобы не заставлять GC работать (а его работа это дорогой обход дерева), мы можем помещать объекты, которые живут не долго, в специальное место - пул. Извлекая их оттуда, мы их переиспользуем, то есть не заставляем их снова и снова появляться в куче.Существует хорошая библиотека, которая позволяет это делать. Причём не от кого-нибудь, а от вендора. Это быстрая и хорошо написанное решение.
Но что делать, если нам нужно быстрее?
Ответ, как всегда, есть.
1. Велосипед. Это
Manual
в бенчмарке. В принципе, там нет ничего особенного - я списал какие-то, с моей точки зрения, важные вещи с реализации Microsoft и из другой билиблиотеки. Это решение лаконично и понятно.2. Можно воспользоваться той самой "другой" библиотекой. Скорость выше не на порядки, но, тем не менее, выше. А хорошее и подробное описание кода может погрузить нас в прекрасный мир высокой производительности.
В результате, мы получим либо аналогичную производительность (но без зависимостей), либо производительность чуть-чуть выше, чем у того, что предлагает нам вендор.
При этом, внимательный читатель наверняка заметил, что самым эффективным пулом для маленьких объектов является некий
ConcurrentToolkitLite
. Это внутренний класс библиотеки ConcurrentToolkit. Его реализация проста, и основана на том, что существует всего один ThreadStatic объект, который и содержит данные пула. Вот так просто и элегантно.Если кому-то мало комментариев Евгения и хочется большего, то могу сообщить дополнительно, что имплементация ConcurrentQueue в .NET выросла вот отсюда. Она же, чаще всего, лежит в основах примеров в интернете.
Кода много, поэтому он тут.
👍12🔥5❤4
ContinueWith #скорость
Не секрет, что мы можем использовать метод ContinueWith для небольшого увеличения производительности. Давно об этом знал, но всё руки не доходили протестировать. Так вот, докладываю.
Делается это просто - мы можем вызвать наш асинхронный метод, а затем, не используя
Это будет несколько быстрее, чем:
Отлично применяется с известной многим сущностью
Напомню, что минусом применения подхода с
P.S.: Бенчмарк в комментариях.
P.P.S: Алексею и Игорю спасибо) Было весело это всё отлаживать.
Не секрет, что мы можем использовать метод ContinueWith для небольшого увеличения производительности. Давно об этом знал, но всё руки не доходили протестировать. Так вот, докладываю.
Делается это просто - мы можем вызвать наш асинхронный метод, а затем, не используя
await
, написать что-то вроде:
MyAsyncMethod(cancellation).ContinueWith(task => DoSomething(task.Result), cancellation);
Это будет несколько быстрее, чем:
var result = await MyAsyncMethod(cancellation);
DoSomething(result);
Отлично применяется с известной многим сущностью
Result<T>
, где, в зависимости от этого результата нужно что-то сделать или не сделать.Напомню, что минусом применения подхода с
ContinueWith
является то, что логи с ошибками становятся немного... плохо читаемыми.P.S.: Бенчмарк в комментариях.
P.P.S: Алексею и Игорю спасибо) Было весело это всё отлаживать.
🔥16👍7❤1
Логика на throw #скорость #память
Известно, что логика на
Типа, всем известно, что выброс ошибки, её перехват и раскручивание стека вызова - дорогая операция. Но меня давно интересовало, а, собственно, насколько "дорого" строить логику на throw? Как раз недавно, на собеседовании, был затронут этот вопрос.
Итак, докладываю. Бенчмарк будет в комментариях.
1. Обычный
2. Выброс ошибки не только в восемь тыщ (!) раз медленнее, но и аллоцирует. Немного, в Gen0, но очень неприятно в горячих местах кода.
3. Если возвращать ошибку в Result (очень популярная фишка из функциональщины), то это чуть-чуть медленнее обычного
Выводы: не надо строить логику на ошибках (а кто бы сомневался), ну а если нам очень надо всё-таки возвращать ошибку коду выше, но без
Казалось бы, очевидно. Но нет, иногда таки встречается в реальном коде.
P.S.: Сергей, спасибо за вопрос.
P.P.S.: Коллега напоминает, что про дорогой выброс ошибки ещё писали вот тут.
Известно, что логика на
throw
- не очень. Ну, это когда мы выбрасываем ошибку в методе, окружаем его вызов try/catch и, в зависимости от того, была ли ошибка, выбираем тот или иной сценарий выполнения.Типа, всем известно, что выброс ошибки, её перехват и раскручивание стека вызова - дорогая операция. Но меня давно интересовало, а, собственно, насколько "дорого" строить логику на throw? Как раз недавно, на собеседовании, был затронут этот вопрос.
Итак, докладываю. Бенчмарк будет в комментариях.
1. Обычный
if/else
вне конкуренции.2. Выброс ошибки не только в восемь тыщ (!) раз медленнее, но и аллоцирует. Немного, в Gen0, но очень неприятно в горячих местах кода.
3. Если возвращать ошибку в Result (очень популярная фишка из функциональщины), то это чуть-чуть медленнее обычного
if
.Выводы: не надо строить логику на ошибках (а кто бы сомневался), ну а если нам очень надо всё-таки возвращать ошибку коду выше, но без
throw
, то делаем это с помощью Result
. Казалось бы, очевидно. Но нет, иногда таки встречается в реальном коде.
P.S.: Сергей, спасибо за вопрос.
P.P.S.: Коллега напоминает, что про дорогой выброс ошибки ещё писали вот тут.
👍33🔥6❤1
Танцы вокруг Enumerable.ToArray #скорость #память
Если мы пишем код по гайдлайнам, то наши методы часто возвращают
При получении
В этой ситуации некоторые программисты допускают первую логическую ошибку. Они подсматривают код метода, который возвращает интерфейс коллекции, видят, что за ним на самом деле скрывается массив, и делают предположение о том, что им тоже нужно использовать исходную коллекцию, скрытую за интерфейсом.
Далее действия могут быть различными и зависеть от знаний глубин .NET'a.
Например, некоторые коллеги верят, что если они вызовут метод
Другие коллеги будут более упорными в желании добраться до исходной коллекции, и создадут метод
Скорость сильно выше, никаких дополнительных аллокаций сделано не будет (см. бенчмарк), а значит будет сделан вывод, что это идеальное решение для ситуаций работы с приходящим
Казалось бы win-win. Но нет. И это вторая логическая ошибка.
Если мы ожидаем массив, исходный метод делает массив и мы придумали костыль, чтоб всё-таки получить массив... не лучше ли просто возвращать массив? Зачем эти танцы с бубном?
Если это наш код и нам ну очень нужен конкретный тип коллекции - давайте просто его использовать. Да, если до этого был IEnumerable, а теперь стал массив - это ломающее изменение и оно не подойдёт для библиотеки с тыщами потребителей... но уж внутри нашего приложения медиаторный хэндлер мы поправить в состоянии.
Ещё одним неприятным моментом будет изменение исходной коллекции. Например, мы получили перечисление, скастили его к массиву и изменили... А оно, например, служит неким кэшем. Как результат, возможно очень странное поведение в других местах кода, которые полагаются на неизменяемость исходной коллекции.
Короче говоря, мне кажется, что иногда не нужно изобретать велосипед. В данном случае, я в этом почти уверен.
P.S.: Александр, спасибо!
P.P.S.: Для серьёзных ребят Денис сделал собственный перечислитель для разных случаев. Кажется, что это очень хорошее решение, которое ликвидирует минусы подобного подхода.
Если мы пишем код по гайдлайнам, то наши методы часто возвращают
IEnumerable<T>
, IReadonlyCollection<T>
и прочие интерфейсы коллекций. Это необходимо для того, чтобы сигнатура метода не изменялась при изменении логики метода, что, в свою очередь, является фундаментом для создания устойчивого к изменениям кода. В принципе, очень полезная и весьма здравая мысль.При получении
IEnumerable
из какого-либо метода, мы часто делаем ToList
или ToArray
. Например, чтобы получить возможность пробежаться по перечислению более чем один раз (см. multiple enumerations в случае IEnumerable). Или, например, мы не хотим аллоцировать Enumerator
при пробегании по IReadonlyCollection
. Короче говоря, по каким-то перформансным соображениям, нам интерфейсы не подходят.В этой ситуации некоторые программисты допускают первую логическую ошибку. Они подсматривают код метода, который возвращает интерфейс коллекции, видят, что за ним на самом деле скрывается массив, и делают предположение о том, что им тоже нужно использовать исходную коллекцию, скрытую за интерфейсом.
Далее действия могут быть различными и зависеть от знаний глубин .NET'a.
Например, некоторые коллеги верят, что если они вызовут метод
ToArray
, то произойдёт магия: мол, dotnet знает настоящий тип коллекции, которая возвращается из метода, а значит просто его и вернёт. Увы, это не так. Если мы посмотрим код, то можно заметить, что при вызове Enumerable.ToArray
создаётся новый массив с копией данных исходного, который и возвращается потребителю.Другие коллеги будут более упорными в желании добраться до исходной коллекции, и создадут метод
AsArray
. Он, я уверен, есть во многих проектах. Этот метод прост, он проверяет тип, и, если это действительно массив, просто возвращает его. Если же это другой тип коллекции, то будет использоваться стандартный Enumerable.ToArray
.Скорость сильно выше, никаких дополнительных аллокаций сделано не будет (см. бенчмарк), а значит будет сделан вывод, что это идеальное решение для ситуаций работы с приходящим
IEnumerable
. Код, думаю, будет примерно таким:
public static T[] AsArray<T>(this IEnumerable<T> collection)
{
return collection as T[] ?? collection.ToArray();
}
Казалось бы win-win. Но нет. И это вторая логическая ошибка.
Если мы ожидаем массив, исходный метод делает массив и мы придумали костыль, чтоб всё-таки получить массив... не лучше ли просто возвращать массив? Зачем эти танцы с бубном?
Если это наш код и нам ну очень нужен конкретный тип коллекции - давайте просто его использовать. Да, если до этого был IEnumerable, а теперь стал массив - это ломающее изменение и оно не подойдёт для библиотеки с тыщами потребителей... но уж внутри нашего приложения медиаторный хэндлер мы поправить в состоянии.
Ещё одним неприятным моментом будет изменение исходной коллекции. Например, мы получили перечисление, скастили его к массиву и изменили... А оно, например, служит неким кэшем. Как результат, возможно очень странное поведение в других местах кода, которые полагаются на неизменяемость исходной коллекции.
Короче говоря, мне кажется, что иногда не нужно изобретать велосипед. В данном случае, я в этом почти уверен.
P.S.: Александр, спасибо!
P.P.S.: Для серьёзных ребят Денис сделал собственный перечислитель для разных случаев. Кажется, что это очень хорошее решение, которое ликвидирует минусы подобного подхода.
👍15👎10❤7
Боксинг IEnumerator #скорость #память #бенч
Кажется, что не всем понятна борьба за использование исходной коллекции в .NET. Поясняю весьма избитую истину - бежать по
Позволю себе напомнить внутреннюю работу .NET на примере List<T>.
Класс
Но стоит нам скастить List<T> к IList<T> - ситуация изменится. В этом случае foreach вызовет метод интерфейса
Насколько всё это будет медленнее - см. результаты бенчмарка.
Кажется, что не всем понятна борьба за использование исходной коллекции в .NET. Поясняю весьма избитую истину - бежать по
IEnumerable
(IList, IReadOnlyCollection и т.п.) сильно дороже, чем по исходной коллекции. Как по памяти, так и по скорости. Позволю себе напомнить внутреннюю работу .NET на примере List<T>.
Класс
List<T>
обладает методом GetEnumerator
, который вызывается при попытке сделать по нему foreach
. Как можно заметить, List<T>.Enumerator
- структура. Это важно, поскольку структура создаётся на стеке, а её методы вызываются напрямую (см. call и callvirt). Это позволяет пробегать по списку быстро и без затрат на выделение места в куче, а значит без работы GC.Но стоит нам скастить List<T> к IList<T> - ситуация изменится. В этом случае foreach вызовет метод интерфейса
IEnumerable.GetEnumerator
, который возвращает IEnumerator<T>
, то есть интерфейс перечислителя. В случае List’a это будет та же самая структура, но размещённая в куче и доступом к её методам через callvirt. То есть мы не только создадим небольшой memory traffic, но и сильно замедлим перебор коллекции.Насколько всё это будет медленнее - см. результаты бенчмарка.
👍29❤6🔥1
Кажется, надо попробовать! Мистеру успехов в развитии канала.
Я также, как и Евгений, достаточно скептично отношусь к способности современных "чатов" делать что-то полезное для программиста. Помню давным-давно пробовал, но меня не впечатлило. Кажется, было много ошибок, а я получал какой-то код уровня джуна с курсов "Шарп за неделю".
Возможно, сейчас настала пора попробовать снова. Тем более и циферки появились (бенчмарки), и инфраструктуру какую-никакую для IDE уже создали, да и сама технология более-менее избавилась от первых проблем.
https://t.iss.one/probelov_net/21
Я также, как и Евгений, достаточно скептично отношусь к способности современных "чатов" делать что-то полезное для программиста. Помню давным-давно пробовал, но меня не впечатлило. Кажется, было много ошибок, а я получал какой-то код уровня джуна с курсов "Шарп за неделю".
Возможно, сейчас настала пора попробовать снова. Тем более и циферки появились (бенчмарки), и инфраструктуру какую-никакую для IDE уже создали, да и сама технология более-менее избавилась от первых проблем.
https://t.iss.one/probelov_net/21
Telegram
Пробелов.NET – AI в программировании и .NET
Лучшая модель для кодирования - Claude Sonnet 3.5
Вы уже наверняка слышали, что Anthropic на днях выпустили новую LLM, которая во многих бенчмарках обходит gpt-4o. Таким образом, Claude Sonnet 3.5 становится лучшей моделей для написания кода на сегодня.…
Вы уже наверняка слышали, что Anthropic на днях выпустили новую LLM, которая во многих бенчмарках обходит gpt-4o. Таким образом, Claude Sonnet 3.5 становится лучшей моделей для написания кода на сегодня.…
🔥6🥰1
TryGetNonEnumerated #память
Хотелось бы напомнить про такую банальную, но весьма полезную оптимизацию, как создание списка с заранее известным размером.
Напомню, что первоначально
Однако, увы, некоторые методы возвращают
Это точно не
В нашем случае он может быть применён вот так:
Почему этот подход не используется в конструкторе того-же List’a (который принимает IEnumerable) и в его же методе
P.S.: Для желающих посмотреть, что метод TryGetNonEnumerated действительно ничего не перебирает, а просто возвращает значение - в комментариях есть бенчмарк.
Хотелось бы напомнить про такую банальную, но весьма полезную оптимизацию, как создание списка с заранее известным размером.
Напомню, что первоначально
List<T>
создаётся с внутренним массивом размера 0. При последующем добавлении элементов происходит проверка, и, если места не хватает, внутренний массив расширяется на свой размер, умноженный на 2. «Расширение», в данном случае, означает, что создаётся новый массив, а содержимое старого массива копируется в новый. Таким образом, если мы заранее создадим List с внутренним массивом в 200 элементов, то мы избежим аллокаций шести массивов - это весьма солидно .Однако, увы, некоторые методы возвращают
IEnumerable<T>
, из которого весьма проблематично узнать размер. Да, за IEnumerable<T> может скрываться любая из коллекций, имплементирующая ICollection<T>
, и тогда проблем с выяснением первоначального размера нашей коллекции нет. Но что если нам пришёл результат чего-то вот такого?
_data = array // первоначальный массив
.Skip(1)
.Take(Count / 2)
.Order()
.Select(static i => i * i % 10 == 0 ? "да" : "нет");
Это точно не
ICollection<string>
, то есть выяснить размер мы не сможем. Вернее, всё-таки сможем. Если заглянуть в недра .NET, то мы узнаем, что это некий Enumerable.SelectIPartitionIterator
, который, на наше счастье, реализует внутренний интерфейс IIListProvider
. Чтобы попытаться воспользоваться его методом GetCount
, нам поможет метод TryGetNonEnumeratedCount, который появился аж в .NET 6.В нашем случае он может быть применён вот так:
var capacity = _data.TryGetNonEnumeratedCount(out var count)
? count
: ваша_эвристическая_константа;
var list = new List<string>(capacity);
list.AddRange(_data);
Почему этот подход не используется в конструкторе того-же List’a (который принимает IEnumerable) и в его же методе
AddRange
- загадка. Наверное, у коллег пока просто не дошли руки.P.S.: Для желающих посмотреть, что метод TryGetNonEnumerated действительно ничего не перебирает, а просто возвращает значение - в комментариях есть бенчмарк.
👍24❤5
Работа с ArrayPool и MemoryPool #память
Пул массивов - классная штука, но есть проблема. И даже две.
Первая проблема. Когда мы пытаемся получить
Соответственно, при попытке итерироваться по полученному массиву, мы можем столкнуться с пустыми элементами, либо элементами, которые содержат предыдущие данные (очистка массива перед возвратом в пул это право, а не обязанность потребителя).
Вариантов решения этой проблемы несколько. Во-первых, мы можем воспользоваться
Теперь мы можем передавать ArraySegment в нужные методы, и избавить потребителей от необходимости знать о том, какого же размера массив был запрошен и необходим.
Во-вторых, мы можем воспользоваться
В-третьих, можно использовать
Memory это обычная структура, а значит нет никаких проблем передавать её в
Вторая проблема при работе с ArrayPool - возврат использованного массива. Когда мы закончили работу с массивом из пула, логично было бы вернуть его в пул в том же методе, в котором мы его получили. Более того, мне кажется, что это правильное решение с точки зрения архитектуры.
Если мы обратим внимание на использование MemoryPool, то он возвращает не Memory, а
Замечу ещё раз, этот способ не совсем правильный и является выходом из ситуации, когда нам всё-таки не удобно возвращать массив в пул по месту получения.
Другие способы (ArraySegment или Span) такой конструкцией не обладают, и мы должны самостоятельно придумывать костыли для возврата массива в пул. Например, можно написать микс ArraySegment’a с IMemoryOwner, который обладает возможностью возврата массива в пул при вызове Dispose. Его код будет в комментариях. А можно ещё и вот так.
Используя подобный велосипед, вы сможете передавать в методы-потребители структуру
Пул массивов - классная штука, но есть проблема. И даже две.
Первая проблема. Когда мы пытаемся получить
ArrayPool<T>.Shared.Rent
массив размером, допустим, в 4 элемента, мы получаем массив размером в 16 элементов. Если запросим 16, то получим 16, а вот если нам нужно 17 элементов, то мы получим аж 32. Таким образом, при запросе массива, мы всегда получаем массив размером не менее нужного. Это сделано специально, чтобы не аллоцировать большое количество массивов разного размера. Соответственно, при попытке итерироваться по полученному массиву, мы можем столкнуться с пустыми элементами, либо элементами, которые содержат предыдущие данные (очистка массива перед возвратом в пул это право, а не обязанность потребителя).
Вариантов решения этой проблемы несколько. Во-первых, мы можем воспользоваться
ArraySegment
- это структура, которая знает размер необходимого нам сегмента массива и следит за тем, чтобы мы не вышли за его пределы при итерировании.var pool = ArrayPool<int>.Shared;
var array = pool.Rent(length);
var segment = new ArraySegment<int>(array, 0, length);
Теперь мы можем передавать ArraySegment в нужные методы, и избавить потребителей от необходимости знать о том, какого же размера массив был запрошен и необходим.
Во-вторых, мы можем воспользоваться
Span
. Это отличный вариант, когда мы передаём кусочек массива в методы, которые не являются async
. Напомню, нам запрещено работать с ref struct
в асинхронных методах.…
var span = array.AsSpan(..length);
В-третьих, можно использовать
Memory
- структурой, которая так же как и ArraySegment, указывает на область памяти. Конечно, при данном подходе, логичнее использовать не ArrayPool, а MemoryPool, который работает примерно так же, как и ArrayPool.var pool = MemoryPool<int>.Shared;
using var memoryOwner = pool.Rent(length);
var memory = memoryOwner.Memory[..length];
Memory это обычная структура, а значит нет никаких проблем передавать её в
async
методы и не думать в них о том, какой же реальный размер памяти мы используем. Напомню, что взаимодействие с Memory осуществляется через Span (memory.Span
).Вторая проблема при работе с ArrayPool - возврат использованного массива. Когда мы закончили работу с массивом из пула, логично было бы вернуть его в пул в том же методе, в котором мы его получили. Более того, мне кажется, что это правильное решение с точки зрения архитектуры.
var pool = ArrayPool<int>.Shared;
var array = pool.Rent(length);
DoSomething(array.AsSpan(..length));
pool.Return(array);
Если мы обратим внимание на использование MemoryPool, то он возвращает не Memory, а
IMemoryOwner
, что как бы намекает: есть владелец памяти, а есть методы, которые память используют. Передавая в методы использования и Memory, и IMemoryOwner’a, мы делаем утверждение, что теперь другой метод является владельцем области памяти, а значит именно он ответственен за её очистку. var pool = MemoryPool<int>.Shared;
var memoryOwner = pool.Rent(length);
var memory = memoryOwner.Memory[..length];
DoSomething(memory, memoryOwner);
Замечу ещё раз, этот способ не совсем правильный и является выходом из ситуации, когда нам всё-таки не удобно возвращать массив в пул по месту получения.
Другие способы (ArraySegment или Span) такой конструкцией не обладают, и мы должны самостоятельно придумывать костыли для возврата массива в пул. Например, можно написать микс ArraySegment’a с IMemoryOwner, который обладает возможностью возврата массива в пул при вызове Dispose. Его код будет в комментариях. А можно ещё и вот так.
Используя подобный велосипед, вы сможете передавать в методы-потребители структуру
PooledArray<T>
и в нужном месте вызывать Dispose, который вернёт массив в пул. В принципе, весьма неплохое решение. Если смотреть на бенчмарк всего сценария, то получается даже быстро.👍19❤3
Быстро парсим 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