yet another dev – Telegram
yet another dev
234 subscribers
141 photos
1 video
107 links
Самый скучный канал про разработку
Download Telegram
👩‍💻 Пишем производительный C# код при работе с коллекциями

Публикую следующую часть статьи про производительность коллекций. Сегодня про Enumerator.

---

Предположим, у нас есть массив _transactionsArray и список _transactionsList. Сама транзакция выглядит следующим образом:

public record class Transaction(
Guid Id,
int Amount,
string Denoscription);


Существует множество способов пройтись по двум вышеупомянутым коллекциям:

// Индексатор массива
var sum = 0;
for (var i = 0; i < _transactionsArray.List; i++) {
sum += _transactionsArray[i].Amount;
}

// Индексатор списка
var sum = 0;
for (var i = 0; i < _transactionsList.Count; i++) {
sum += _transactionsList[i].Amount;
}

// Энумератор IEnumerable<T>
var sum = 0;
var collection = (IEnumerable<T>)_transactionsArray;
foreach (var item in collection) {
sum += item.Amount;
}

var sum = 0;
var collection = (IEnumerable<T>)_transactionsList;
foreach (var item in collection) {
sum += item.Amount;
}

// Энумератор List<T>
var sum = 0;
foreach (var item in _transactionsList) {
sum += item.Amount;
}


Один из этих способов аллоцирует больше памяти, чем остальные – это приведение _transactionsList к IEnumerable<T>. Причина кроется в реализации энумератора для списков. У типа List<T> есть собственный энумератор-структура.

Если использовать foreach с типом List<T>, то проблем не возникает. Компилятор C# сгенерирует код, который будет использовать эту структуру напрямую.

// Исходный код
var nums= new List<int>();
var sum = 0;
foreach (n in nums) sum +=n;

// Код после компиляции
var nums = new List<int>();
var num = 0;
List<int>.Enumerator enumerator =
nums.GetEnumerator();
while (enumerator.MoveNext()) {
num += enumerator.Current;
}


Но если привести List<T>, например, к IList<T> или IReadOnlyList<T>, т.е. к любому интерфейсу, реализующему IEnumerable<T>, то произойдёт неявная упаковка List<T>.Enumerator. Это происходит из-за того, что IEnumerable<T>.GetEnumerator() возвращает интерфейс IEnumerator<T>.

// Исходный код
var nums = new List<int>();
var collection = (IEnumerable<int>) nums;
var sum = 0;
foreach (n in collection) sum +=n;

// Код после компиляции
var nums = new List<int>();
var collection = ((IEnumerable<int>)nums);
var num = 0;
IEnumerator<int>.Enumerator enumerator =
// упаковка
collection.GetEnumerator();
while (enumerator.MoveNext()) {
num += enumerator.Current;
}


Аналогичный подход с энумераторами также встречается и других коллекциях: LinkedList<T>, Stack<T>, Queue<T> и т.д. Исключением являются, например, массивы.

Насколько упаковка влияет на производительность можно понять из графиков. Бенчмарк 100 раз прошёлся по каждой из коллекции. На первом графике ось Y отображает проценты от бенчмарка с массивом, а на втором — миллисекунды. Шкала оси X в обоих случаях логарифмическая.

Если говорить об абсолютных значениях, разница не так велика — десятки миллисекунд для коллекций размером 100 000 элементов и более. Наибольшее коварство такое поведение представляет, когда у вас много небольших коллекций. Я в своей практике встречал проблему, когда упаковка энумератора приводила к аллокациям десятков и сотен мегабайт. Это было большое дерево директорий, а доступ к дочерним директориям был только через IReadOnlyList<T>.
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
👍9
Используйте диапазоны (Range) только со Span<int>

В предыдущем посте я писал о скрытой аллокации, возникающей из-за упаковки энумератора. В этом посте расскажу об ещё одной скрытой аллокации.

5 лет назад в C# 8.0 появились индексы и диапазоны, которые позволяют получать часть коллекции с помощью удобного синтаксиса. Например, в следующем примере из массива извлекаются все элементы, кроме 2-х первых и 2-х последних:

int[] arr = [1, 2, 3, 4, 5, 6];
var subarr = arr[2..^2]; // [3, 4]


Это удобно, особенно по сравнению с аналогичной записью через LINQ:

var subarr = arr.Skip(2)
.Take(arr.Length - 2 - 2)
.ToArray();


В случае с LINQ мы явно создаём массив с помощью метода ToArray. Однако это можно и не делать, если нужно просто пройтись по выбранным элементам.

Теперь посмотрим на C#-код без синтаксического сахара. При использовании с массивом оператор диапазона всегда создаёт новый массив. При компиляции он преобразуется в вызов RuntimeHelpers.GetSubArray, который и создаёт новый массив.

// int[] subarr = arr[2..^2]
int[] subArray = RuntimeHelpers
.GetSubArray(array, new Range(2,
new Index(2, true)));


Если нет необходимости сохранять подмассив, лучше вызвать AsSpan() перед использованием диапазонов.

// Исходный код
int[] arr = [1, 2, 3, 4, 5, 6];
var subarr = arr.AsSpan()[2..^2]; // [3, 4]

// После компиляции
Span<int> span = MemoryExtensions.AsSpan(array);
Span<int> subarr = span.Slice(2,
span.Length - 2 - 2);


С точки зрения производительности, создание нового массива и копирование элементов – это затратная операция, особенно в сравнении со слайсами спанов, которые выполняются практически мгновенно. Рассмотрим следующий код:

start = Length / 4;
end = Length * 3 / 4;
var sum = 0;
// 2-3 times faster:
// _transactionsArray.AsSpan()[_start.._end];
var sliced = _transactionsArray[start..end];
foreach (var t in sliced) sum += t.Amount;
return sum;


Добавление AsSpan() перед использованием оператора диапазона позволяет сократить время выполнения в среднем в 2-3 раза (см. график).
👍7
EventFlow – опенсорс библиотека для DDD, Event Sourcing и CQRS

Немного отвлечёмся от бенчмарков. Уже 2 недели работаю над pet-проектом с DDD + ES + CQRS. Для этого искал подходящую библиотеку, т.к. хотелось сосредоточиться на бизнес-проблемах, а не изобретать велосипед. Нашёл фреймворк EventFlow. В библиотеке есть всё готовое:

- базовые классы для агрегатов и доменных событий;
- команды, запросы;
- обработчики команд, запросов, событий;
- функционал для паттерна «сага»;
- функционал для миграций событий.

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

Документация написана достаточно подробно, главное внимательно читать. Я, например, проглядел, что по умолчанию фреймворк «проглатывает» исключения, выброшенные в хэндлерах команд. Из-за этого долго не мог понять, почему некоторые тестовые моки интерфейсов выбрасывают исключения, но это не приводит к поломке тестов.

Альтернативы

В процессе поиска нашёл несколько других проектов по теме. Возможно, кому-то пригодится, поэтому делюсь списком.

1. Revo – ещё одна библиотека для DDD, Event Sourcing и CQRS. Изначально я попытался запустить проект именно с ней, но мне она не понравилась по нескольким причинам:

- Проект крэшился сразу после запуска. Связано это с тем, что Revo построен вокруг Ninject – сторонней библиотеки для внедрения зависимостей, а не встроенного в .NET DI. Исправить я это не смог, из-за плохо написанной документации.

- Функционал библиотеки разбит на множество Nuget-пакетов. Пришлось несколько раз возвращаться к документации, чтобы понять какой очередной пакет установить, чтобы проект наконец-то уже собрался.

У проекта много звёзд, так что, возможно, библиотека не так плоха, а это я не смог разобраться. 🤷‍♂️

2. Marten DB – Event Store построенный вокруг PostgreSQL. Подходит, если вы хотите самостоятельно разработать базовые классы для агрегатов, событий, команд, запросов, шин сообщений и т. д.

3. YesSQL – интерфейс, имитирующий документоориентированную БД. Библиотека построена поверх EF Core и работает c SQLite, PostgreSQL, SQL Server и MySQL. Думаю, что тоже подходит для создания Event Store.
👍7
❄️ C# Advent Calendar 2024

Около месяца назад я вписался в C# Advent Calendar — онлайн-мероприятие, организованное Мэтью Гроувсом, энтузиастом C# и автором книги «Аспектно-ориентированное программирование на .NET».

Суть мероприятия в следующем: с 1 по 25 декабря на сайте csadvent.christmas дважды в день публикуются статьи от авторов о разработке на C# и .NET. Моя статья выйдет 9 декабря. Я как раз завершаю над ней работу. В статье я расскажу об одном интересном паттерне для высокой производительности: будут примеры реализации паттерна на C#, разбор внутренностей, бенчмарки и графики. Статья будет на английском, но сюда обязательно опубликую перевод.

Если в адвенте будут интересные статьи, то раз в неделю буду делиться подборкой.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍21
❄️ Самое интересное на C# Advent 2024. Часть 1.

Как и обещал, выкладываю самое интересное за первую неделю на C# Advent 2024.

🟡A .NET coding puzzle: Can strings change?

Очень интересный челлендж со строками. Нужно сделать так, что бы программа ниже вывела “Advent of C#”.

Initialize();

var happyHolidays = "Merry Christmas";

// should output: Advent of C#
Console.WriteLine(happyHolidays);

static void Initialize() {
// Your implementation here
}


Сложность в том, что:

- Изменения можно вносить только в метод Initialize.
- Нельзя менять сигнатуру метода.
- Нельзя использовать Console внутри метода Initialize.
- Нельзя использовать стандартный выходной поток.

Автор рассматривает несколько решений. Все они связаны с поведением .NET при работе со строками-литералами, пулом интернирования строк и указателями.

🟡DI-Cluttering Your Code

Статья о том, как избежать проблем с множеством зависимостей в классах. Но я бы назвал её «Как не надо проектировать системы». Честно говоря, я не представляю, как можно получить контроллер, в который внедряются 34 зависимости. 😵‍💫 Это не шутка – смотрите пример из статьи.

Благодаря Minimal API и Primary Constructors автор решает проблему с большим количеством зависимостей, но мне кажется, корень проблемы лежит немного в другой плоскости.

---

В понедельник утром публикуется моя статья. Версию на русском опубликую на Хабре.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍10
👩‍💻 Пулы объектов в C#: примеры, устройство и производительность

English is below.

Пул объектов (Object Pool) — это паттерн, который позволяет повторно использовать объекты вместо создания новых. Это может быть полезно в ситуациях, когда инициализация объектов затратна. Паттерн широко применяется в разработке игр и приложениях, где важно минимизировать использование памяти. В этой статье мы рассмотрим, как этот шаблон реализован в C#, и как он может улучшить производительность.

🔼 Статья опубликована на Хабре. Поддержите плюсом.

Object Pools in C#: Examples, Internals and Performance Benchmarks

Object Pool is a design pattern that allows reusing objects instead of creating new ones. This can be very useful in scenarios where object initialization is expensive. It is widely used, especially in game development and applications where low memory usage is critically important. In this article, we will look at how this pattern is implemented in C# and how it can improve performance.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍9
❄️ Самое интересное на C# Advent 2024. Часть 2.

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

🟡Introducing HybridCache in .NET 9

В .NET 9 появился гибридный кэш, объединяющий преимущества локального и распределённого кэшей. Локальный кэш доступен в рамках одного процесса, а распределённый - нескольким процессам или серверам. Гибридный кэш сочетает плюсы обоих. В статье объясняется базовая настройка, а для детального изучения можно обратиться к документации Microsoft.

🟡How and why to write mutation tests in C#

Мутационное тестирование - это способ проверки тестов. Суть в том, что исходный код подвергается небольшим изменениям, называемым "мутантами". Если после изменений тесты не ломаются, значит они недостаточно надёжны. В статье рассматривается библиотека Stryker (отсылка к X-Men?), которая позволяет делать мутационное тестирование C# программ.

🟡Using vectorization in C# to boost performance

Рассказ о том, как разработчик С# оптимизировал метод IEnumerable<T>.Sum с помощью векторизации. Эти изменения оказались полезными и его пул-реквест был принят в репозиторий .NET. Хороший пример, как умение писать высокопроизводительный код может пригодиться в жизни.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍11
🎄 Итоги года

В конце прошлого года на этом канале было 19 человек. Сейчас 174. Это же целая аудитория в университете! Круто, что меня читает так много инженеров. Спасибо вам за интерес и поддержку в течение этого года.

За год было написано много статей, большая часть которых публиковалась здесь. Полноценные статьи также выходили на Хабре, а переводы на Reddit. Часть из них была высоко оценена сообществом. Например, статью про FrozenDictionary репостили многие каналы и разбирали на подкасте DotNetRu.

Моя цель — сделать канал ещё полезнее для инженеров, продолжить обсуждать сложные темы простым языком. Буду рад вашим идеям и пожеланиям.

🪟 Это последний пост в этом году. Всех с наступающими праздниками! Мир вашему дому.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍29
🇷🇸 Жизнь в Сербии. Часть 1. Стоимость жизни.

Давно хотел рассказать про жизнь в Сербии, поэтому начнём этот год с нетехнической статьи.

Предыстория

В Сербию мы впервые приехали в августе 2022 года. Тогда я ещё работал на проекте Fujitsu. Мы обеспечивали работу приложений в Японии, Южной Корее и Австралии. На тот момент серьёзно ходили слухи об отключении России от глобального интернета. Поскольку компания должна была обеспечивать непрерывную поддержку сервисов, инженеров периодически направляли в офис в Сербии. В августе пришла очередь и до меня. После этого было короткое возвращение на Родину, месячная командировка в Индию, смена работы и окончательный переезд в Сербию в октябре 2022 года.

Жильё

Мы снимаем «евродвушку» площадью 45 кв.м. за 650 € в месяц. Это медианная цена для района Новый Белград. Знаю людей, которые находили квартиры чуть больше за те же деньги, а также тех, кто платил значительно больше за аналогичное жильё. Кстати, понятие «евродвушка» в Сербии отсутствует — такие квартиры считаются однокомнатными.

Коммунальные услуги

Стоимость коммунальных услуг сильно зависит от сезона. Зима здесь мягкая, но отопление всё равно необходимо — оно составляет добрую половину платежа. Например, в декабре 2024 года отопление обошлось в 50 €. Летом же стоимость высока из-за кондиционера, так как 40-градусная жара в июле и августе — обычное явление в Белграде. Средняя стоимость коммуналки за последний год составила 97 €.

Связь


Интернет в квартире обходится в 14 € в месяц, при этом скорость составляет 500 Мбит/с. Мобильная связь в среднем стоит около 10 € на человека.

Транспорт

С 1 января 2025 года общественный транспорт в Белграде стал бесплатным. До этого единый безлимитный проездной на автобусы, троллейбусы и трамваи стоил около 19 € в месяц (2200 динар).
Такси я использую редко — за весь прошлый год было всего шесть поездок, каждая обошлась от 5 € до 22 €.

Продукты

Большую часть продуктов мы покупаем в супермаркетах Lidl, поэтому подсчитать расходы было просто — я просто сложил все чеки из приложения. Средний месячный расход на продукты за 2024 год составил около 300 €.

Медицина

Медицина в Сербии делится на государственную и частную. Государственная медицина практически бесплатная — за приём врача нужно заплатить около 0.5 €.

Частная медицина дороже. Без страховки на приём врача лучше заложить 50 €. Если потребуется диагностика, например УЗИ или МРТ, то стоимость может увеличиться до 100–200 €.

Итог

Сложим основные ежемесячные расходы:

- Аренда: 650 €
- Коммунальные услуги: 90 €
- Интернет: 14 €
- Мобильная связь (на двоих): 20 €
- Продукты: 300 €

Итоговая сумма: 1074 € в месяц

Эта цифра включает только базовые траты и может варьироваться в зависимости от личных потребностей и стиля жизни. В следующих частях я расскажу про другие аспекты жизни в Сербии.
👍15
Сообщество DotNetRu на подкасте разбирает статью про пулы объектов в C#

Сегодня ребята из DotNetRu выложили подкаст, на котором разобрали мою статью про паттерн ObjectPool.

Если вы пропустили эту статью и вам интересен формат подкаста, то вот ссылка.
👍9
Оптимизация JIT-компилятора при работе с коллекциями

Помните я писал статью про производительность foreach при работе с коллекциями через интерфейс IEnumerable?

Вкратце напомню в чём дело. Когда мы используем, например массивы и списки через интерфейс IEnumerable<T>, то их энумератор приводится к интерфейсу IEnumerator. У массивов и списков энумератор – это структура. Следовательно, при приведении происходит упаковка.

Так вот, команда .NET собирается добавить в .NET 10 оптимизацию для таких случаев, по крайней мере для массивов.

По моему, это круто. Теоретически, работа с массивами через LINQ должна ускориться. Будет интересно проверить этот функционал, когда появится первый релиз .NET 10.
👍7
FluentAssertions стала платной

Коллеги, если вы вдруг пропустили новость, то сообщаю. Библиотека для тестирования FluentAssertions стала платной около месяца назад. Теперь стандартная лицензия для 1 разработчика стоит $129.95 в год, а для небольших компаний – $49.95 в год.

Новость довольно неприятная. Мы, например, используем её повсеместно на наших проектах.

Какие есть альтернативы:

1. Остаться на FluentAssertions не выше версии 7.0.0.

2. Перейти на AwesomeAssertions – форк FluentAssertions с лицензией Apache 2.0, который продолжит развиваться сообществом.

3. Перейти на Shouldly – бесплатную библиотеку с лицензией BSD.

4. Перейти на NFluent – ещё одну бесплатную библиотеку.

5. Вернуться к классическому Assert.

Если вы уже используете FluentAssertions, самым простым вариантом будет переход на AwesomeAssertions.
👍11
👩‍💻 Генераторы кода в C#

English is below.

Возвращаюсь к работе после отпуска.

В ноябре я рассказывал про EventFlow — библиотеку, предназначенную для DDD и Event Sourcing. Наша команда активно её использует. Спустя несколько месяцев работы с ней я заметил, что часто приходится писать повторяющийся код, что довольно утомительно.

// Чтобы создать событие для агрегата OrderAggregate, 
// приходится каждый раз указывать тип агрегата и тип его ID
public class OrderCreated :
IAggregateEvent<OrderAggregate, OrderAggregateId>

// При подписке на события необходимо
// указывать тип события, агрегата и ID
public class OrderAggregateSubscribers :
ISubscribeSynchronousTo<OrderAggregate, OrderAggregateId, OrderCreated>


🔴Как избежать бойлерплейта?

Можно объявить абстрактный класс для всех событий агрегата и использовать его:

public abstract class OrderAggregateEvent : 
IAggregateEvent<OrderAggregate, OrderAggregateId>;


Создать новый интерфейс, принимающий только тип события:

public interface ISubscribeSynchronousTo<TEvent> : 
ISubscribeSynchronousTo<OrderAggregate, OrderAggregateId, TEvent>


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

«Никогда не тратьте 10 минут, чтобы сделать что-то вручную, если можно потратить 10 часов пытаясь это автоматизировать».


Поэтому в начале февраля я занялся написанием генератора кода для EventFlow. Через 2 недели и 7 коммитов библиотека EventFlow.SourceGenerators была готова и на днях Расмус Микелсен (автор EventFlow) принял изменения.

🔴Как помогают генераторы кода?

Теперь вся рутинная работа ложится на компилятор, и можно писать меньше кода, который к тому же лучше читается.

// Добавляем атрибут AggregateExtensions к агрегату
[AggregateExtensions]
public class OrderAggregate(OrderAggregateId id) :
AggregateRoot<OrderAggregate, OrderAggregateId>(id)

// Объявляем события
// OrderAggregateEvent создан автоматически
public class OrderCreated : OrderAggregateEvent;

// Подписываемся на события
// Новый интерфейс создан автоматически
public class OrderAggregateSubscribers :
ISubscribeSynchronousTo<OrderCreated>


Как видите, код стал заметно чище, и теперь нам не приходится снова и снова указывать тип агрегата и тип ID.

🔴Сложности при работе с генераторами кода

Стоит отметить, что генераторы кода — непростая тема. Существует множество ограничений и сложностей. Например, с чем я столкнулся:

1. Генераторы можно писать только под .NET Standard 2.0.
2. Возможны коллизии названий сгенерированных объектов.
3. Тестирование сгенерированного кода имеет свои особенности.

🔴Что дальше?

Если вам интересна тема генераторов кода, рекомендую начать с этого видео от Microsoft.

Затем изучите следующие материалы:

- Creating an incremental generator.
- Introducing C# Source Generators.
- Incremental Generators Cookbook.

В качестве примера можно заглядывать в исходный код библиотеки EventFlow.SourceGenerators и её тестов. Она довольна простая. Больше примеров её применения можно найти в документации.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍14
🧳 Рынок труда

Наверное, многие слышали о сокращениях в российской IT-сфере. Например, пару месяцев назад уволили мою однокурсницу из «Купера». По её словам, там сократили ещё множество сотрудников. Кроме того, я заметил, что HR почти перестали писать мне с предложениями о работе. Хотя не исключаю, что компании просто избегают релокантов. Так или иначе, мне стало интересно, какие вакансии для C# разработчиков сейчас публикуются на сайтах по поиску работы. Я решил написать скрепер для hh.ru.

Методология сбора данных


Данные собирается по следующим параметрам:

1. Общее количество вакансий.
2. Количество вакансий по месту работы (офис/удалёнка).
3. Количество вакансий по образованию.
4. Количество вакансий по опыту.
5. Количество вакансий по указанной зарплате.

В выборку попадают все вакансии, где указан навык C#. Это не только разработчики, но и, например, тимлиды и AQA.

Важно учитывать, что:

- Одна вакансия может быть на несколько позиций. Например, когда требуется нанять сразу несколько C# программистов на один проект.
- Разные вакансии могут быть на одну и ту же позицию, но размещены в нескольких городах.
- В зарплатах указываются как нижний, так и верхний предел вилки.

Поэтому диаграммы лишь приблизительно отражают реальную картину.

Результаты

1. Без фильтров hh.ru показывает 2574 вакансий с навыком C#. Большая часть из них — разработка. С фильтрами, почему-то суммарное количество получается немного другое. Например, если разбивать по образованию, то суммарное количество вакансий будет 2624 — на 40 больше.

2. Удалёнка разрешена в 36% вакансий.

3. Для 28% вакансий требуется высшее образование.

4. Для начинающих айтишников доступно 11% вакансий.

5. Зарплата указана для 33% вакансий. Судя по распределению, медианная зарплата находится где-то в районе 150к - 175к рублей.

Что дальше

Интересно понаблюдать за вакансиями в динамике. Также планирую анализировать вакансии с Хабр Карьеры. Если знаете другие популярные сайты с вакансиями — напишите в комментариях.
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8
Уже вторую неделю встречаю комментарии в духе: «Если C# такой быстрый и хороший, то почему Microsoft переписала компилятор TypeScript на Go, а не на C#?»

Среди множества спекуляций на эту тему мне попался один комментарий на Хабре, который, как мне кажется, ясно объясняет причину.

Btw, компилятор C# (Roslyn) написан на C#.
👍8