Самый быстрый способ (с точки зрения производительности) превратить строку в массив byte [] в C # с использованием кодировки символов ASCII - PullRequest
7 голосов
/ 27 августа 2009

Какой самый быстрый способ превратить строку в массив byte [] в C #? Я посылаю тонны строковых данных через сокеты, и мне нужно оптимизировать каждую операцию. В настоящее время я преобразую строки в массивы byte [] перед отправкой, используя:

private static readonly Encoding encoding = new ASCIIEncoding();
//...
byte[] bytes = encoding.GetBytes(someString);
socket.Send(bytes);
//...

Ответы [ 9 ]

15 голосов
/ 27 августа 2009

Если все ваши данные действительно будут ASCII, то вы можете сделать это немного быстрее, чем ASCIIEncoding, который имеет различные (вполне разумные) биты обработки ошибок и т. Д. Вы можете также можно ускорить его, избегая постоянного создания новых байтовых массивов. Предполагая, что у вас есть верхняя граница, под которой будут находиться все ваши сообщения:

void QuickAndDirtyAsciiEncode(string chars, byte[] buffer)
{
    int length = chars.Length;
    for (int i = 0; i < length; i++)
    {
        buffer[i] = (byte) (chars[i] & 0x7f);
    }
}

Вы бы тогда сделали что-то вроде:

readonly byte[] Buffer = new byte[8192]; // Reuse this repeatedly
...
QuickAndDirtyAsciiEncode(text, Buffer);
// We know ASCII takes one byte per character
socket.Send(Buffer, text.Length, SocketFlags.None);

Это довольно отчаянная оптимизация. Я буду придерживаться ASCIIEncoding до тех пор, пока не докажу , что это было узким местом (или, по крайней мере, этот неуместный хак не помогает).

9 голосов
/ 27 августа 2009

Я бы сказал, что то, как вы делаете это сейчас, очень хорошо. Если вы действительно озабочены оптимизацией на очень низком уровне, лучшая рекомендация, которую я могу дать, - это приобрести Reflector. С помощью рефлектора вы можете посмотреть код самостоятельно (большую часть времени) и посмотреть, что это за алгоритмы. Если рефлектор не показывает вас, вы всегда можете скачать Microsoft SSCLI (общеязыковая инфраструктура общего языка), чтобы увидеть код C ++ за методами MethodImplOptions.InternalCall.

Для справки, вот фактическая реализация Encoding.ASCII.GetBytes:

public override int GetBytes(string chars, int charIndex, int charCount, byte[] bytes, int byteIndex)
{
    if ((chars == null) || (bytes == null))
    {
        throw new ArgumentNullException();
    }
    if ((charIndex < 0) || (charCount < 0))
    {
        throw new ArgumentOutOfRangeException();
    }
    if ((chars.Length - charIndex) < charCount)
    {
        throw new ArgumentOutOfRangeException();
    }
    if ((byteIndex < 0) || (byteIndex > bytes.Length))
    {
        throw new ArgumentOutOfRangeException();
    }
    if ((bytes.Length - byteIndex) < charCount)
    {
        throw new ArgumentException();
    }
    int num = charIndex + charCount;
    while (charIndex < num)
    {
        char ch = chars[charIndex++];
        if (ch >= '\x0080')
        {
            ch = '?';
        }
        bytes[byteIndex++] = (byte) ch;
    }
    return charCount;
}
1 голос
/ 25 июля 2018

Характеристика производительности при реализации функции библиотеки memcpy общего назначения с использованием регистра SIMD значительно более яркая, чем при эквивалентной реализации с использованием регистра общего назначения ...

- Справочное руководство по оптимизации архитектур Intel 64 и IA-32 (апрель 2018 г.) §3.7.6.1


Для обеспечения максимальной скорости преобразования средних и больших по размеру фрагментов данных между 8-битным byte[] и «широким» (16-битным, Unicode) текстом вам следует рассмотреть решения, в которых используется SIMD инструкции PUNPCKLBW + PUNPCKHBW (расширение) и PACKUSWB (сужение). В .NET эти новые функции доступны как x64 JIT intritstics, испускаемые для аппаратно-ускоренных типов System.Numerics Vector и Vector<T> (см. здесь для получения дополнительной информации). Общая версия Vector<T> определена в пакете System.Numerics.Vectors, который в настоящее время находится в стадии довольно активной разработки. Как показано ниже, вы также, вероятно, захотите включить пакет System.Runtime.CompilerServices.Unsafe, поскольку это предпочтительный метод загрузки / хранения SIMD, рекомендованный авторами Vector<T>.

Соответствующее ускорение SIMD включено только для способных процессоров в режиме x64 , но в остальном .NET обеспечивает прозрачный откат к коду эмуляции в библиотеке System.Numerics.Vectors, поэтому код, показанный здесь , делает действительно надежно функционирует в более широкой экосистеме .NET, возможно, с пониженной производительностью. Для проверки кода, показанного ниже, я использовал консольное приложение на полной .NET Framework 4.7.2 («рабочий стол») в x64 (SIMD) и x86 (эмулируемые) режимы.

Поскольку я не хотел бы лишать кого-либо возможности изучать соответствующие техники, я буду использовать Vector.Widen, чтобы проиллюстрировать направление от byte[] до char[] в C # 7 . Из этого примера сделать обратное - , т.е. , используя Vector.Narrow для реализации направления сужения - просто и оставлено в качестве упражнения для читателя.

предупреждение:
Методы, предлагаемые здесь, полностью не кодируются, они просто сокращают / расширяют - или сужают / расширяют - необработанные байты в / из необработанных байтов без учета отображения символов, кодировки текста или других языковых свойств. При расширении избыточные байты устанавливаются в ноль, а при сужении лишние байты отбрасываются.

Другие обсуждали n̲u̲m̲e̲r̲o̲u̲s̲ h̲a̲z̲a̲r̲d̲s̲ , связанные с этой практикой, на этой странице и в других местах, поэтому, пожалуйста, внимательно просмотрите и поймите характер этой операции, прежде чем подумать, подходит ли она для вашего ситуация. Для ясности, встроенная проверка исключается из примера кода, показанного ниже, но он может быть добавлен к самому внутреннему циклу с минимальным влиянием на преимущество SIMD.

Вы были предупреждены . Несмотря на то, что SIMD не ускоряется, канонические методы, использующие подходящий Encoding экземпляр , рекомендуются почти для всех реалистичных сценариев приложения Хотя ФП действительно специально запрашивает максимальную производительность (или, более явно, как последнее усилие по прививке против негативных голосов со стороны полиции с унижением), затем я должным образом суммирую надлежащие санкционированные методы, которые обычно должны использоваться.

Чтобы расширить байтовый массив до .NET String, вызовите метод GetString () в подходящем экземпляре байт-ориентированной кодировки:

String Encoding.ASCII.GetString(byte[] bytes)

Чтобы сузить .NET String до байтового массива (например, Ascii), вызовите метод GetBytes () в подходящем экземпляре байт-ориентированного кодирования:

byte[] Encoding.ASCII.GetBytes(char[] chars)


Хорошо, теперь самое интересное - чрезвычайно быстрый код с поддержкой SIMD («векторизация») C # для «немого» расширения массива байтов. Напоминаем, что есть некоторые зависимости, на которые следует ссылаться:

// ... 
using System.Numerics;                  // nuget: System.Numerics.Vectors
using System.Runtime.CompilerServices;  // nuget: System.Runtime.CompilerServices.Unsafe
// ... 

Вот чеФункция публичной точки входа. Если вы предпочитаете версию, которая возвращает char[] вместо String, она указана в конце этого поста.

/// <summary>
/// 'Widen' each byte in 'bytes' to 16-bits with no consideration for
/// character mapping or encoding.
/// </summary>
public static unsafe String ByteArrayToString(byte[] bytes)
{
    // note: possible zeroing penalty; consider buffer pooling or 
    // other ways to allocate target?
    var s = new String('\0', bytes.Length);

    if (s.Length > 0)
        fixed (char* dst = s)
        fixed (byte* src = bytes)
            widen_bytes_simd(dst, src, s.Length);
    return s;
}

Далее идет основной рабочий цикл тела. Обратите внимание на цикл пролога, который выравнивает место назначения по 16-байтовой границе памяти, если необходимо, путем побайтового копирования до 15 исходных байтов. Это обеспечивает наиболее эффективную работу основного цикла " quad-quadwise ", который с помощью одной пары инструкций SIMD PUNPCKLBW/PUNPCKHBW записывает 32 байта одновременно (16 исходных байтов выбираются и затем сохраняются как 16 широких символов, занимающих 32 байта). Предварительное выравнивание плюс выбор выравнивания dst (в отличие от src ) являются официальными рекомендациями из руководства Intel, приведенного выше. Аналогично, выровненная операция влечет за собой то, что, когда основной цикл завершается, источник может иметь до 15 остаточных завершающих байтов; они заканчиваются короткой петлей эпилога.

static unsafe void widen_bytes_simd(char* dst, byte* src, int c)
{
    for (; c > 0 && ((long)dst & 0xF) != 0; c--)
        *dst++ = (char)*src++;

    for (; (c -= 0x10) >= 0; src += 0x10, dst += 0x10)
        Vector.Widen(Unsafe.AsRef<Vector<byte>>(src),
                     out Unsafe.AsRef<Vector<ushort>>(dst + 0),
                     out Unsafe.AsRef<Vector<ushort>>(dst + 8));

    for (c += 0x10; c > 0; c--)
        *dst++ = (char)*src++;
}

Вот и все, что нужно сделать! Он работает как шарм и, как вы увидите ниже, он «кричит» как рекламируется .

Но сначала, отключив опцию отладчика vs2017 «Отключить JIT-оптимизацию», мы можем проверить собственный поток команд SIMD, который x64 JIT генерирует для сборки 'release' на .NET 4.7.2 . Вот соответствующая часть основного внутреннего цикла, которая обрабатывает данные по 32 байта за раз. Обратите внимание, что JIT удалось создать теоретически минимальный шаблон выборки / хранения.

L_4223  mov         rax,rbx  
L_4226  movups      xmm0,xmmword ptr [rax] ; fetch 16 bytes
L_4229  mov         rax,rdi  
L_422C  lea         rdx,[rdi+10h]  
L_4230  movaps      xmm2,xmm0  
L_4233  pxor        xmm1,xmm1  
L_4237  punpcklbw   xmm2,xmm1               ; interleave 8-to-16 bits (lo)
L_423B  movups      xmmword ptr [rax],xmm2  ; store 8 bytes (lo) to 8 wide chars (16 bytes)
L_423E  pxor        xmm1,xmm1  
L_4242  punpckhbw   xmm0,xmm1               ; interleave 8-to-16 bits (hi)
L_4246  movups      xmmword ptr [rdx],xmm0  ; store 8 bytes (hi) to 8 wide chars (16 bytes)
L_4249  add         rbx,10h  
L_424D  add         rdi,20h  
L_4251  add         esi,0FFFFFFF0h  
L_4254  test        esi,esi  
L_4256  jge         L_4223  
L_4258  ...

Результаты тестов производительности:
Я проверил код SIMD с четырьмя другими методами, которые выполняют ту же функцию. Для перечисленных ниже кодеров .NET это был вызов метода GetChars(byte[], int, int).

  • наивная реализация C # небезопасного байтового цикла
  • .NET-кодировка для кодовой страницы "Windows-1252"
  • .NET кодировка для ASCII
  • .NET-кодировка для UTF-8 (без спецификации, без выбрасывания)
  • Код SIMD, показанный в этой статье

Тестирование включало в себя одинаковую работу для всех и проверку одинаковых результатов для всех тестируемых блоков. Тестовые байты были случайными и только ASCII ([0x01 - 0x7F]), чтобы гарантировать идентичный результат для всех тестовых блоков. Размер входного файла был случайным, максимум 1 МБ, с журналом 2 смещения в сторону меньших размеров, так что средний размер был около 80K.

Справедливости ради, порядок выполнения систематически менялся на 5 единиц для каждой итерации. Для прогрева тайминги отбрасывались и сбрасывались в ноль один раз, на итерации 100. Испытательный жгут не выполняет никаких распределений во время фазы тестирования, и полный GC принудительно ожидают каждые 10000 итераций.

                 Relative ticks, normalized to best result
                  .NET Framework 4.7.3056.0 x64 (release)
   iter      naive      win-1252         ascii         utf-8          simd
-------   -----------  ------------  ------------  ------------   -----------
  10000 |    131.5         294.5         186.2         145.6         100.0
  20000 |    137.7         305.3         191.9         149.4         100.0
  30000 |    139.2         308.5         195.8         151.5         100.0
  40000 |    141.8         312.1         198.5         153.2         100.0
  50000 |    142.0         313.8         199.1         154.1         100.0
  60000 |    140.5         310.6         196.7         153.0         100.0
  70000 |    141.1         312.9         197.3         153.6         100.0
  80000 |    141.6         313.7         197.8         154.1         100.0
  90000 |    141.3         313.7         197.9         154.3         100.0
 100000 |    141.1         313.3         196.9         153.7         100.0

gcServer=False; LatencyMode.Interactive; Vector.IsHardwareAccelerated=True 

На предпочтительной платформе x64 , когда включена оптимизация JIT и доступна SIMD, соревнования не проводились. Код SIMD работает примерно на 150% быстрее, чем следующий соперник. Encoding.Default, обычно это кодовая страница Windows-1252, работает особенно плохо, примерно в 3 раза медленнее, чем код SIMD.

Ранее я упоминал, что распределение размеров тестовых данных было сильно смещено в сторону нуля. Без этого шага - означающего равномерное распределение размеров от 0 до 1 048 576 байт (средний размер теста 512 КБ) - SIMD продолжает опережать пакет со всеми другими единицами, показавшими относительную хуже по сравнению с кодом, показанным выше.

naive       153.45%
win-1252    358.84%
ascii       221.38%
utf-8       161.62%
simd        100.00%

Что касается случая без SIMD (эмуляции), UTF-8 и SIMD чрезвычайно близки - обычно в пределах 3-4% друг от друга - и намного лучше, чем остальные. Я нахожу этот результат вдвойне удивительным: исходный код UTF8Encoding был настолько быстрым (много быстрой оптимизации), а затем и то, что универсальный код эмуляции SIMD смог соответствовать этой цели. настроенный код.




Добавление:

В приведенном выше коде я упомянул возможное снижение производительности O ( n ) (связанное с избыточным повторным обнулением) из-за использования конструктора new String(Char,int) для выделения целевой строки. Для полноты, вот альтернативная точка входа, которая может избежать проблемы, вместо этого возвращая расширенные данные как ushort[]:

/// <summary>
/// 'Widen' each byte in 'bytes' to 16-bits with no consideration for
/// character mapping or encoding
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe char[] WidenByteArray(byte[] bytes)
{
    var rgch = new char[bytes.Length];
    if (rgch.Length > 0)
        fixed (char* dst = rgch)
        fixed (byte* src = bytes)
            widen_bytes_simd(dst, src, rgch.Length);
    return rgch;
}
1 голос
/ 27 августа 2009

Не зная ваших требований к параллелизму (или чего-либо еще): можете ли вы порождать некоторые потоки в ThreadPool, которые преобразуют строки в байтовые массивы и помещают их в очередь, а еще один поток наблюдает за очередью и отправляет данные

1 голос
/ 27 августа 2009

Что вы пытаетесь оптимизировать? ЦПУ? Пропускная способность

Если вы хотите оптимизировать пропускную способность, попробуйте заранее сжать строковые данные.

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

1 голос
/ 27 августа 2009

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

РЕДАКТИРОВАТЬ - Вы знаете, я не знаю, быстрее это или нет. Но вот еще один метод, использующий BinaryFormatter:

BinaryFormatter bf = new BinaryFormatter();
MemoryStream ms = new MemoryStream();
bf.Serialize(ms, someString);
byte[] bytes =  ms.ToArray();
ms.Close();
socket.Send(bytes);

Причина, по которой я думаю, что может быть быстрее, заключается в том, что он пропускает этап кодирования. Я также не совсем уверен, что это будет работать правильно. Но вы можете попробовать и посмотреть. Конечно, если вам нужна кодировка ascii, это не поможет.

У меня просто была другая мысль. Я считаю, что этот код будет возвращать удвоенное количество байтов, чем при использовании GetBytes с кодировкой ASCII. Причина в том, что все строки в .NET используют юникод за кулисами. И, конечно, Unicode использует 2 байта на символ, тогда как ASCII использует только 1. Так что BinaryFormatter, вероятно, не то, что нужно использовать в этом случае, потому что вы удвоите объем данных, отправляемых через сокет.

0 голосов
/ 08 октября 2009

Еще один совет: я не знаю, как вы создаете свои исходные строки, но помните, что StringBuilder.Append («что-то») действительно быстрее, чем что-то вроде myString + = «что-то».

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

Ben

0 голосов
/ 27 августа 2009

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

0 голосов
/ 27 августа 2009

Как уже говорили другие, класс Encoding уже оптимизирован для этой задачи, поэтому, вероятно, будет трудно сделать это быстрее. Есть одна микрооптимизация, которую вы можете сделать: используйте Encoding.ASCII вместо new ASCIIEncoding(). Но, как все знают, микрооптимизации плохи;)

...