Характеристика производительности при реализации функции библиотеки 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;
}