Использование C# System..Numerics.Vector<T> для распаковки / упаковки битов
Я проверяю возможности класса.Net C# System.Numerics.Vector для упаковки и распаковки битов.
Я надеялся на функциональность векторного побитового сдвига влево / вправо, но в настоящее время она недоступна, поэтому я попытался смоделировать сдвиг, используя арифметические и логические методы, как показано ниже. Вот что я увидел:
Упаковка (имитация побитового сдвига влево и OR) с использованием Vector.Multiply() и Vector.BitwiseOr() немного хуже *, чем код массива / указателя.
*<10% снижение пропускной способности (МБ / с).
Но распаковка (имитация побитового сдвига вправо и AND) с использованием Vector.Divide() и Vector.BitwiseAnd() намного хуже **, чем код массива / указателя.
** 50% снижение пропускной способности
NB:
Вектор был протестирован с использованием модуля (это также было отмечено в комментариях).
Основой тестирования была упаковка и распаковка от 100Mn до 1Bn целых чисел в блоках по 65536 целых чисел. Я случайно сгенерировал int[] для каждого блока.
Я также проверял побитовые (& | >> <<) и арифметические (+ - * /) операции и не видел заметной разницы в стоимости. Даже разделение было не так уж и плохо, всего лишь 10% ухудшение по сравнению с множителем (вопрос разделения был поднят в комментариях)
Я изменил свой исходный тестовый код (для сравнения не-вектора) на небезопасную / указательную подпрограмму, чтобы создать более похожий тест в терминах упаковки (много целых в слово) по сравнению с распаковкой (слово ко многим целым числам).). Это привело к разнице во всем (между упаковкой и распаковкой) для не-векторного кода до дисперсии <5%. (что противоречит моему комментарию о компиляторе и оптимизации ниже)
Неоптимизированный вектор: упаковка в 2 раза быстрее, чем распаковка
Оптимизированный вектор: 4-кратное улучшение (по сравнению с неоптимизированным вектором) в упаковке и 2-кратное улучшение при распаковке
Неоптимизированный массив / указатель: распаковка происходит на ~5% быстрее, чем упаковка
Оптимизированный массив / указатель: получено 3-кратное улучшение (по сравнению с неоптимизированным указателем массива) для упаковки и 2,5-кратное улучшение для распаковки. В целом, Оптимизированная упаковка массивов / указателей была <5% быстрее, чем Оптимизированная распаковка массивов / указателей.
Оптимизированная упаковка массивов / указателей была на ~10% быстрее, чем пакет Optimized Vector
Вывод на данный момент:
Vector.Divide() выглядит сравнительно медленной реализацией по сравнению с обычным арифметическим делением
Кроме того, компилятор, по-видимому, не оптимизирует код Vector.Divide() примерно так же, как Vector.Multiply() (который поддерживает приведенные ниже комментарии относительно оптимизации деления).
Обработка массива / указателя в настоящее время немного быстрее, чем класс Vector для упаковки данных и значительно быстрее для распаковки
System.Numerics нужны методы Vector.ShiftLeft() и Vector.ShiftRight()
Вопрос (обновлено);
- мой вывод примерно на верном пути? или есть другие аспекты для проверки / рассмотрения?
Дальнейшая информация:
int numPages = 8192; // up to >15K
int testSize = 65536;
StopWatch swPack = new StopWatch();
StopWatch swUnpack = new StopWatch();
long byteCount = 0;
for (int p = 0; p < numpages; b++)
{
int[] data = GetRandomIntegers(testSize, 14600, 14800);
swPack.Start();
byte[] compressedBytes = pack(data);
swPack.Stop();
swUnpack.Start();
int[] unpackedInts = unpack(compressedBytes);
swUnpack.Stop();
byteCount += (data.Length*4);
}
Console.WriteLine("Packing Throughput (MB/sec): " + byteCount / 1000 / swPack.ElapsedMilliseconds);
Console.WriteLine("Unpacking Throughput (MB/sec): " + byteCount / 1000 / swUnpacking.ElapsedMilliseconds);
2 ответа
IL
/// non-SIMD fallback implementation for 128-bit right-shift (unsigned)
/// n: number of bit positions to right-shift a 16-byte memory image.
/// Vector(T) argument 'v' is passed by-ref and modified in-situ.
/// Layout order of the two 64-bit quads is little-endian.
.method public static void SHR(Vector_T<uint64>& v, int32 n) aggressiveinlining
{
ldarg v
dup
dup
ldc.i4.8
add
ldind.i8
ldc.i4.s 64
ldarg n
sub
shl
ldarg v
ldind.i8
ldarg n
shr.un
or
stind.i8
ldc.i4.8
add
dup
ldind.i8
ldarg n
shr.un
stind.i8
ret
}
псевдокод
As<Vector<ulong>,ulong>(ref v) = (As<Vector<ulong>,ulong>(in v) >> n) |
(ByteOffsAs<Vector<ulong>,ulong>(in v, 8) << (64 - n));
ByteOffsAs<Vector<ulong>,ulong>(ref v, 8) >>= n;
Объявление C# extern
static class vector_ext
{
[MethodImpl(MethodImplOptions.ForwardRef | MethodImplOptions.AggressiveInlining)]
extern public static void SHR(ref Vector<ulong> v, int n);
};
Вы можете связать промежуточные двоичные файлы .netmodule, созданные из IL (ildasm.exe
) и C# (csc.exe
) вместе в единую сборку с помощью /LTCG
(генерация кода во время компоновки) вариант в link.exe
.
результат выполнения x64 JIT (.NET Framework 4.7.2)
0x7FF878F5C7E0 48 89 4C 24 08 mov qword ptr [rsp+8],rcx
0x7FF878F5C7E5 8B C2 mov eax,edx
0x7FF878F5C7E7 F7 D8 neg eax
0x7FF878F5C7E9 8D 48 40 lea ecx,[rax+40h]
0x7FF878F5C7EC 48 8B 44 24 08 mov rax,qword ptr [rsp+8]
0x7FF878F5C7F1 4C 8B 40 08 mov r8,qword ptr [rax+8]
0x7FF878F5C7F5 49 D3 E0 shl r8,cl
0x7FF878F5C7F8 4C 8B 08 mov r9,qword ptr [rax]
0x7FF878F5C7FB 8B CA mov ecx,edx
0x7FF878F5C7FD 49 D3 E9 shr r9,cl
0x7FF878F5C800 4D 0B C1 or r8,r9
0x7FF878F5C803 4C 89 00 mov qword ptr [rax],r8
0x7FF878F5C806 48 83 C0 08 add rax,8
0x7FF878F5C80A 8B CA mov ecx,edx
0x7FF878F5C80C 48 D3 28 shr qword ptr [rax],cl
0x7FF878F5C80F C3 ret
Vector.Divide
не имеет аппаратного ускорения для целочисленных типов. Это очень медленно.
Это было до тех пор, пока.NET 7.0
этот Вектор добавил методы ShiftRightArithmetic, ShiftRightLogical.
Я разработал библиотеку VectorTraits. Это позволяет использовать более низкие версии.NET
программы (.NET Core 3.0
+,.NET 5.0
+) использовать аппаратно ускоренные методы ShiftRightArithmetic, ShiftRightLogical. https://www.nuget.org/packages/VectorTraits