C# Heppard
1.63K subscribers
79 photos
1 video
2 files
130 links
25 способов эффективно использовать .NET

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

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

В этом, не самом здоровом желании, нам, конечно, поможет reflection. Код будет тривиальный, что-то вроде typeof(Entry).GetField(...) . Этот код найдёт поле в типе и вернёт нам его объектное представление.

Особо упорные ребята могут делать подобную операцию каждый вызов метода, но это крайне не оптимально на горячих участках кода. Более умные коллеги выполнят поиск поля один раз, положат его в статическую переменную и будут получать значение через fieldInfo.GetValue(_entry), а задавать через fieldInfo.SetValue(_entry, value).

Те, кто читал про ExpressionTree, вполне резонно будут получать и задавать значение через заранее сформированные делегаты: для получения будет что-то вроде Expression.Lambda<Func<Entry, int>>(fieldExpression, getterParameters).Compile(), а для записи - Expression.Lambda<Action<Entry, int>>(Expression.Assign(fieldExpression, valueParameter), setterParameters).Compile(). Этот подход значительно быстрее (раз эдак в 20) и, на моей практике, довольно часто используется.

Однако, современный .NET 8+ может предложить ещё более быстрое решение - UnsafeAccessorAttribute. Код его использования лаконичнее и проще:
private static class Accessor {
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = FieldName)]
public static extern ref int GetFieldValue(Entry entry);
}


Как мы можем заметить, доступ осуществляется через метод с ключевым словом extern, которое чаще всего ассоциируется с использованием кода внешних библиотек на других языках программирования. В данном же случае, при аннотации extern-метода с помощью UnsafeAccessor, поиск будет происходить в сборках нашего приложения.

Сигнатура проста. UnsafeAccessorKind указывает, какой тип члена класса мы ищем (поле, метод, конструктор). Имя говорит... про имя члена класса. Ну а первый аргумент метода - в каком типе мы ищем.

Использование этого подхода примерно в 2 раза быстрее, чем с помощью ExpressionTree. В том числе за счёт того, что мы можем получить ref на поле, что, в свою очередь, означает, что мы можем менять его по ссылке и с минимальными накладными расходами. Если у вас современный .NET и существует жгучее желание по новому взглянуть на чудеса а-ля Reflection, я бы хорошенько присмотрелся к UnsafeAccessor'у. Его скорость работы сильно впечатляет. Хотя, конечно, он имеет известные ограничения.

Прочитать больше и подробнее можно у Эндрю Лока.

P.S.: Код и результаты бенчмарка в комментариях. В этом посте много кода, а ТГ, при вставлении картинки, делает сообщение узким.
1🔥28👍104
FastEnum #скорость

Иногда нам нужно не просто быстро, а очень быстро. В этот момент в дело вступают самые необычные оптимизации. Например, оптимизация работы с enum. Типичный сценарий - получить строку из enum'a или, наоборот, enum из строки.

Многие знают, что получение строкового представления enum'a лучше делать через Enum.GetName(MyEnum.Value), а вариант MyEnum.Value.ToString() аллоцирует новую строку и несколько медленнее.

В случае сложной логики получения строкового представления enum'a, чаще всего используется кэш (который, кстати, есть в Enum.GetName). Для этой цели можно написать свой класс, который содержит два словаря. В случае получения строки из enum'a мы идём по одному словарю, а в случае получения enum'a из строки - по другому. Код примерно такой:
private static readonly FrozenDictionary<string, T> StringToValues;
private static readonly FrozenDictionary<T, string> ValuesToString;

static EnumUtils() {
var enums = Enum.GetValues<T>();
StringToValues = enums.ToFrozenDictionary(...);
ValuesToString = enums.ToFrozenDictionary();
}

public static string ToString(T value) {
return ValuesToString.TryGetValue(value, out var stringValue)
? stringValue
: Throw<string>($"Value '{value}' is not defined");
}

Конечно, никакого ноу-хау тут нет, и для таких оптимизаций уже существует несколько готовых библиотек. Одна из них - FastEnum. Она показывает значительный прирост скорости не только по сравнению с обычными способами работы с enum в .NET (почти в два раза), но и по сравнению с другими подобными библиотеками: от Эндрю Лока и Enums.NET. FastEnum и библиотека от Эндрю Лока используют source generators.

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

Картинка бенчмарка тут.
Сам бенчмарк тут.
🔥11👍7🥱1🐳1