Инструкция SSE для проверки, равен ли массив байтов нулям C#
Предположим, у меня есть byte[]
и хотите проверить, все ли байты являются нулями. Для цикла это очевидный способ сделать это, и LINQ All()
это модный способ сделать это, но наивысшая производительность имеет решающее значение.
Как я могу использовать Mono.Simd для ускорения проверки, заполнен ли байтовый массив нулями? Я ищу передовой подход, а не просто правильное решение.
1 ответ
Лучший код представлен ниже. Другие методы и измерения времени доступны в полном объеме.
static unsafe bool BySimdUnrolled (byte[] data)
{
fixed (byte* bytes = data) {
int len = data.Length;
int rem = len % (16 * 16);
Vector16b* b = (Vector16b*)bytes;
Vector16b* e = (Vector16b*)(bytes + len - rem);
Vector16b zero = Vector16b.Zero;
while (b < e) {
if ((*(b) | *(b + 1) | *(b + 2) | *(b + 3) | *(b + 4) |
*(b + 5) | *(b + 6) | *(b + 7) | *(b + 8) |
*(b + 9) | *(b + 10) | *(b + 11) | *(b + 12) |
*(b + 13) | *(b + 14) | *(b + 15)) != zero)
return false;
b += 16;
}
for (int i = 0; i < rem; i++)
if (data [len - 1 - i] != 0)
return false;
return true;
}
}
В конце концов он был побит этим кодом:
static unsafe bool ByFixedLongUnrolled (byte[] data)
{
fixed (byte* bytes = data) {
int len = data.Length;
int rem = len % (sizeof(long) * 16);
long* b = (long*)bytes;
long* e = (long*)(bytes + len - rem);
while (b < e) {
if ((*(b) | *(b + 1) | *(b + 2) | *(b + 3) | *(b + 4) |
*(b + 5) | *(b + 6) | *(b + 7) | *(b + 8) |
*(b + 9) | *(b + 10) | *(b + 11) | *(b + 12) |
*(b + 13) | *(b + 14) | *(b + 15)) != 0)
return false;
b += 16;
}
for (int i = 0; i < rem; i++)
if (data [len - 1 - i] != 0)
return false;
return true;
}
}
Измерения времени (на массиве 256 МБ):
LINQ All(b => b == 0) : 6350,4185 ms
Foreach over byte[] : 580,4394 ms
For with byte[].Length property : 809,7283 ms
For with Length in local variable : 407,2158 ms
For unrolled 16 times : 334,8038 ms
For fixed byte* : 272,386 ms
For fixed byte* unrolled 16 times : 141,2775 ms
For fixed long* : 52,0284 ms
For fixed long* unrolled 16 times : 25,9794 ms
SIMD Vector16b equals Vector16b.Zero : 56,9328 ms
SIMD Vector16b also unrolled 16 times : 32,6358 ms
Выводы:
- Mono.Simd имеет только ограниченный набор инструкций. Я не нашел инструкции для вычисления скалярной суммы (вектор) или макс (вектор). Однако есть оператор равенства векторов, возвращающий bool.
- Развертывание петли является мощной техникой. Даже самый быстрый код выигрывает от его использования.
- LINQ безбожно медленный, потому что он использует делегаты из лямбда-выражений. Если вам нужны самые современные характеристики, тогда, очевидно, это не тот путь.
- Все представленные методы используют оценку короткого замыкания, то есть они заканчиваются, как только они сталкиваются с ненулевым значением.
- SIMD код был в итоге взломан. Есть и другие вопросы по поводу того, что SIMD действительно ускоряет процесс.
Разместил этот код на Peer Review, так что 2 ошибки найдены и исправлены.
Скалярная реализация обрабатывает длинные, которые являются 64-битными (8-байтовыми) за раз, и получает большую часть ускорения за счет этого мощного параллелизма.
Приведенный выше код SIMD/SSE использует 128-битные инструкции SIMD/SSE (16 байт). При использовании новых 256-битных (32-байтовых) инструкций SSE реализация SIMD примерно на 10% быстрее. Благодаря 512-битным (64-байтовым) инструкциям AVX/AVX2 в последних процессорах реализация SIMD с их использованием должна быть еще быстрее.
private static bool ZeroDetectSseInner(this byte[] arrayToOr, int l, int r)
{
var zeroVector = new Vector<byte>(0);
int concurrentAmount = 4;
int sseIndexEnd = l + ((r - l + 1) / (Vector<byte>.Count * concurrentAmount)) * (Vector<byte>.Count * concurrentAmount);
int i;
int offset1 = Vector<byte>.Count;
int offset2 = Vector<byte>.Count * 2;
int offset3 = Vector<byte>.Count * 3;
int increment = Vector<byte>.Count * concurrentAmount;
for (i = l; i < sseIndexEnd; i += increment)
{
var inVector = new Vector<byte>(arrayToOr, i );
inVector |= new Vector<byte>(arrayToOr, i + offset1);
inVector |= new Vector<byte>(arrayToOr, i + offset2);
inVector |= new Vector<byte>(arrayToOr, i + offset3);
if (!Vector.EqualsAll(inVector, zeroVector))
return false;
}
byte overallOr = 0;
for (; i <= r; i++)
overallOr |= arrayToOr[i];
return overallOr == 0;
}
public static bool ZeroValueDetectSse(this byte[] arrayToDetect)
{
return arrayToDetect.ZeroDetectSseInner(0, arrayToDetect.Length - 1);
}
Улучшенная версия (благодаря предложению Питера) показана в приведенном выше коде, безопасна и была интегрирована в пакет Nuget HPCsharp для ускорения на 20% с помощью 256-битных инструкций SSE.