Когда использовать ReadOnlySpan<T> против явных/перегруженных типов

Ради последующего вопроса предположим гипотетически, что я хочу преобразовать строку обычного текста в шестнадцатеричную строку; например, «Привет, мир!» в «48656c6c6f2c20576f726c6421».

Для этого требуется два шага:

  1. Преобразуйте обычную текстовую строку в массив байтов.
  2. Преобразуйте массив байтов в шестнадцатеричную строку.

Например:

string plainText = "Hello, World!";
byte[] bytes = Encoding.Default.GetBytes(plainText);
string hexadecimalString = Convert.ToHexString(bytes);

Далее я хочу обернуть эту функциональность в служебную функцию. До версии .NET Core 2.1 (и я удивлен, что ReadOnlySpan<T> настолько стар) наиболее гибким подходом было бы наличие перегрузок для string и char[]; например:

public static string ToHexString(string value, Encoding? encoding = null) =>
    ToHexString(value.ToCharArray(), encoding);
    
public static string ToHexString(char[] value, Encoding? encoding = null) =>
  Convert.ToHexString((encoding ?? Encoding.Default).GetBytes(value));

Хотя этот подход относительно тривиален, он не особенно удобен с точки зрения распределения, поскольку требуется три распределения:

  1. ToCharArray() выделит нового char[].
  2. GetBytes выделю нового byte[]
  3. ToHexString выделит нового string.

Введение ReadOnlySpan<T> потенциально может улучшить ситуацию за счет сокращения количества выделений (хотя и только на 1), но также с точки зрения удобства сопровождения оно требует меньше перегрузок методов, поскольку string и char[] неявно конвертируются в ReadOnlySpan<char>; например (в идеальном мире):

public static string ToHexString(ReadOnlySpan<char> value, Encoding? encoding = null) =>
  Convert.ToHexString((encoding ?? Encoding.Default).GetBytes(value));

По иронии судьбы, хотя ReadOnlySpan<T> теоретически должен уменьшить количество выделений, проблема здесь в том, что GetBytes не имеет перегрузки, которая требует ReadOnlySpan<char>, поэтому единственный способ реализовать это — повторно ввести выделение; например:

public static string ToHexString(ReadOnlySpan<char> value, Encoding? encoding = null) =>
  Convert.ToHexString((encoding ?? Encoding.Default).GetBytes(value.ToArray()));

В настоящее время единственный способ сократить выделение ресурсов — это предоставить перегрузки методов и реализовать их все отдельно; например:

public static string ToHexString(string value, Encoding? encoding = null) =>
  Convert.ToHexString((encoding ?? Encoding.Default).GetBytes(value));
    
public static string ToHexString(char[] value, Encoding? encoding = null) =>
  Convert.ToHexString((encoding ?? Encoding.Default).GetBytes(value));

Теперь у нас есть методы, которые приведут к меньшему количеству выделений, поскольку нет преобразований из string в char[] или ReadOnlySpan<char> в char[], но компромисс заключается в том, что это приводит к более высоким затратам на сопровождение, поскольку теперь мне приходится поддерживать два метода и меньшую гибкость. API для вызывающего абонента.

Итак, мой вопрос: когда правильно использовать ReadOnlySpan<T> в качестве параметра метода, а не использовать более явные/перегруженные типы? Должен ли компромисс быть смещен в сторону удобства сопровождения и надежного API, опирающегося на неявное преобразование типов, или в сторону производительности? Есть ли какие-либо рекомендации по этому поводу?

GetBytesтакая перегрузка.
Etienne de Martel 29.04.2024 02:40

Перегрузка ReadOnlySpan недоступна до версии Net Core 2.1.

beautifulcoder 29.04.2024 04:26

@EtiennedeMartel, я не хочу сказать, что у GetBytes есть перегрузка для string. Дело в том, что у него нет перегрузки для ReadOnlySpan<char>, тогда как у Convert.ToHexString она есть.

Matthew Layton 29.04.2024 07:57

@beautifulcoder Я знаю. Вот почему в посте конкретно упоминается Core 2.1 и что мне придется реализовать перегрузки для string и char[], а не реализовать ReadOnlySpan<char>.

Matthew Layton 29.04.2024 07:59

Примечание. Не используйте Encoding.Default. Его значение меняется в зависимости от того, используете ли вы .NET Framework или .NET Core/.NET 5+. В .NET Framework его значение зависит от кодовой страницы, настроенной на компьютере, на котором выполняется код.

canton7 29.04.2024 09:44

@canton7 Спасибо, я этого не знал. Будет ли UTF8 лучшим вариантом по умолчанию в этом случае?

Matthew Layton 29.04.2024 13:23

Конечно. UTF-8 стал стандартом де-факто во многих местах.

canton7 29.04.2024 13:59

@MatthewLayton Но... у GetBytes есть перегрузка для ReadOnlySpan<char>. Или, может быть, я неправильно понимаю, о чем вы говорите?

Etienne de Martel 30.04.2024 02:28

@EtiennedeMartel Методы GetBytes, которые принимают string или char[], возвращают byte[]. Напротив, метод GetBytes, который принимает ReadOnlySpan<char>, требует второго аргумента Span<byte> и возвращает int (количество записанных байтов), поэтому его не так просто использовать. Ответ Александра Петрова демонстрирует этот метод.

Matthew Layton 30.04.2024 10:31

Ну да, потому что весь смысл использования промежутков состоит в том, чтобы избежать выделения памяти. Зачем избегать только половины из них?

Etienne de Martel 30.04.2024 16:49

@EtiennedeMartel Вам все равно придется где-то выделить несколько байтов, независимо от того, выделены ли они в стеке или в куче. Очевидно, что первый вариант более эффективен, при условии, что вы не столкнетесь с переполнением стека. Тот факт, что вы пишете в Span<byte>, а не возвращаете byte[], кажется неважным.

Matthew Layton 01.05.2024 12:09

Ну нет, в том-то и дело: память стека уже выделена. Вот как работают стеки. Вам не придется больше беспокоить сборщика мусора.

Etienne de Martel 01.05.2024 19:55
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
12
102
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Рассмотрим следующий код

public static string ToHexString(ReadOnlySpan<char> value, Encoding? encoding = null)
{
    encoding ??= Encoding.Default;

    int count = encoding.GetByteCount(value);
    Span<byte> bytes = stackalloc byte[count];

    encoding.GetBytes(value, bytes);
    return Convert.ToHexString(bytes);
}

Вы также можете переоценить размер массива и преобразовать его с помощью ToHexString(Byte[], Int32, Int32), если не хотите дважды сканировать исходный буфер.

Jeremy Lakeman 29.04.2024 06:32

Не выделяйте в стеке количество байтов, определенное пользователем-контроллером — это простой способ вызвать переполнение стека.

canton7 29.04.2024 09:42

Если нам нужна производительность, мы можем проверить, является ли count маленьким (тогда мы используем stackalloc) или большим (и мы используем ArrayPool)

Dmitry Bychenko 29.04.2024 12:26
Ответ принят как подходящий

Итак, мой вопрос: когда правильно использовать ReadOnlySpan в качестве параметра метода, а не использовать более явные/перегруженные типы? Должен ли компромисс быть смещен в сторону удобства сопровождения и надежного API, опирающегося на неявное преобразование типов, или в сторону производительности? Есть ли какие-либо рекомендации по этому поводу?

Хотя это может быть в некоторой степени основано на мнениях, существует распространенная поговорка: Преждевременная оптимизация — корень всех зол.

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

Но вам следует профилировать код, чтобы найти части, чувствительные к производительности, и оптимизировать эти части. Это может включать или не включать вашу функцию ToHex. Хотя профилировать распределение может быть сложнее, чем производительность, профилировщик должен сообщить вам, по крайней мере, такие вещи, как скорость выделения и время, проведенное в сборщике мусора. Вы также можете добавить ручные инструменты, если вам нужны более подробные данные.

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

Другие вопросы по теме