标量算法对24位图像的处理,与32位图像非常相似,仅 cbPixel 的值不同。
源代码如下。
public static unsafe void ScalarDoBatch(byte* pSrc, int strideSrc, int width, int height, byte* pDst, int strideDst) {
const int cbPixel = 3; // 24 bit: Bgr24, Rgb24.
byte* pRow = pSrc;
byte* qRow = pDst;
for (int i = 0; i < height; i++) {
byte* p = pRow + (width - 1) * cbPixel;
byte* q = qRow;
for (int j = 0; j < width; j++) {
for (int k = 0; k < cbPixel; k++) {
q[k] = p[k];
}
p -= cbPixel;
q += cbPixel;
}
pRow += strideSrc;
qRow += strideDst;
}
}
使用 BenchmarkDotNet 进行基准测试。
[Benchmark(Baseline = true)]
public void Scalar() {
ScalarDo(_sourceBitmapData, _destinationBitmapData, false);
}
//[Benchmark]
public void ScalarParallel() {
ScalarDo(_sourceBitmapData, _destinationBitmapData, true);
}
public static unsafe void ScalarDo(BitmapData src, BitmapData dst, bool useParallel = false) {
int width = src.Width;
int height = src.Height;
int strideSrc = src.Stride;
int strideDst = dst.Stride;
byte* pSrc = (byte*)src.Scan0.ToPointer();
byte* pDst = (byte*)dst.Scan0.ToPointer();
bool allowParallel = useParallel && (height > 16) && (Environment.ProcessorCount > 1);
if (allowParallel) {
Parallel.For(0, height, i => {
int start = i;
int len = 1;
byte* pSrc2 = pSrc + start * (long)strideSrc;
byte* pDst2 = pDst + start * (long)strideDst;
ScalarDoBatch(pSrc2, strideSrc, width, len, pDst2, strideDst);
});
} else {
ScalarDoBatch(pSrc, strideSrc, width, height, pDst, strideDst);
}
}
24位像素的标量算法改的很简单,但是24位像素的向量算法要复杂的多。
这是因为向量大小一般是 16或32字节这样的2的整数幂,而24位像素是3个字节一组,无法整除。这就给地址计算、数据处理等方面,带来很大的难题。
既然1个向量无法被3整除,那么我们干脆用3个向量。这样肯定能被3整除。
例如使用Sse指令集时,向量大小为128位,即16个字节。3个向量,就是 48字节,正好能放下16个 24位像素。
随后面临一个难点——怎样对3个向量内的24位像素进行翻转?
根据前一篇文章的经验,处理1个向量内翻转时,可以使用Shuffle方法,只要构造好索引就行。现在面对3个向量,若有适用于3个向量的换位方法就好了。
为了解决这一难题,VectorTraits库提供了YShuffleX3等方法。且由于能确保索引总是在有效范围内,故还可以使用性能更好的 YShuffleX3Kernel 方法。
在大多数时候,YShuffleX3Kernel 是利用单向量的shuffle指令组合而成。由于 .NET 8.0
增加了一批“多向量换位”的硬件指令,于是在以下平台,能获得更好的硬件加速。
.NET 8.0
新增了对 AdvSimd指令集里的“2-4向量查表”指令的支持。例如 vqtbl3q_u8
..NET 8.0
新增了对 Avx512系列指令集的支持,而它提供了“2向量重排”的指令。例如 _mm_permutex2var_epi8
.详见 [C#] .NET8增加了Arm架构的多寄存器的查表函数(VectorTableLookup/VectorTableLookupExtension)。
YShuffleX3 在 .NET Framework
等平台上运行时是没有硬件加速的,这是因为这些平台不支持Sse等向量指令。可以通过 Vectors 的 YShuffleX3Kernel_AcceleratedTypes 属性来得知哪些元素类型有硬件加速。当发现不支持时,宜切换为标量算法。
另外,还可以通过 Vectors.Instance.UsedInstructionSets
来查看该向量所使用的指令集。
为了便于跨平台,这里使用了自动大小向量Vector。且由于它的大小不固定,于是需要写个循环来计算索引。根据上一篇文章的经验,我们可以在类的静态构造方法里做这个计算。
private static readonly Vector<byte> _shuffleIndices0;
private static readonly Vector<byte> _shuffleIndices1;
private static readonly Vector<byte> _shuffleIndices2;
static ImageFlipXOn24bitBenchmark() {
const int cbPixel = 3; // 24 bit: Bgr24, Rgb24.
int vectorWidth = Vector<byte>.Count;
int blockSize = vectorWidth * cbPixel;
Span<byte> buf = stackalloc byte[blockSize];
for (int i = 0; i < blockSize; i++) {
int m = i / cbPixel;
int n = i % cbPixel;
buf[i] = (byte)((vectorWidth - 1 - m) * cbPixel + n);
}
_shuffleIndices0 = Vectors.Create(buf);
_shuffleIndices1 = Vectors.Create(buf.Slice(vectorWidth * 1));
_shuffleIndices2 = Vectors.Create(buf.Slice(vectorWidth * 2));
}
由于现在是需要对3个向量计算索引,故可以使用栈分配,创建一个3倍向量宽度的buf。计算好索引后,可以利用Span的Slice方法,分别加载这3个索引向量。
索引计算好后,便可以用 YShuffleX3Kernel 来对3个向量做换位了。
temp0 = Vectors.YShuffleX3Kernel(data0, data1, data2, _shuffleIndices0);
temp1 = Vectors.YShuffleX3Kernel(data0, data1, data2, _shuffleIndices1);
temp2 = Vectors.YShuffleX3Kernel(data0, data1, data2, _shuffleIndices2);
随后便可参考上一篇文章的思路,对整个图像进行水平翻转。
根据上面的思路,编写代码。源代码如下。
public static unsafe void UseVectorsDoBatch(byte* pSrc, int strideSrc, int width, int height, byte* pDst, int strideDst) {
const int cbPixel = 3; // 24 bit: Bgr24, Rgb24.
Vector<byte> indices0 = _shuffleIndices0;
Vector<byte> indices1 = _shuffleIndices1;
Vector<byte> indices2 = _shuffleIndices2;
int vectorWidth = Vector<byte>.Count;
if (width <= vectorWidth) {
ScalarDoBatch(pSrc, strideSrc, width, height, pDst, strideDst);
return;
}
int maxX = width - vectorWidth;
byte* pRow = pSrc;
byte* qRow = pDst;
for (int i = 0; i < height; i++) {
Vector<byte>* pLast = (Vector<byte>*)pRow;
Vector<byte>* qLast = (Vector<byte>*)(qRow + maxX * cbPixel);
Vector<byte>* p = (Vector<byte>*)(pRow + maxX * cbPixel);
Vector<byte>* q = (Vector<byte>*)qRow;
for (; ; ) {
Vector<byte> data0, data1, data2, temp0, temp1, temp2;
// Load.
data0 = p[0];
data1 = p[1];
data2 = p[2];
// FlipX.
temp0 = Vectors.YShuffleX3Kernel(data0, data1, data2, indices0);
temp1 = Vectors.YShuffleX3Kernel(data0, data1, data2, indices1);
temp2 = Vectors.YShuffleX3Kernel(data0, data1, data2, indices2);
// Store.
q[0] = temp0;
q[1] = temp1;
q[2] = temp2;
// Next.
if (p <= pLast) break;
p -= cbPixel;
q += cbPixel;
if (p < pLast) p = pLast; // The last block is also use vector.
if (q > qLast) q = qLast;
}
pRow += strideSrc;
qRow += strideDst;
}
}
随后为该算法编写基准测试代码。
[Benchmark]
public void UseVectors() {
UseVectorsDo(_sourceBitmapData, _destinationBitmapData, false);
}
//[Benchmark]
public void UseVectorsParallel() {
UseVectorsDo(_sourceBitmapData, _destinationBitmapData, true);
}
public static unsafe void UseVectorsDo(BitmapData src, BitmapData dst, bool useParallel = false) {
int vectorWidth = Vector<byte>.Count;
int width = src.Width;
int height = src.Height;
if (width <= vectorWidth) {
ScalarDo(src, dst, useParallel);
return;
}
int strideSrc = src.Stride;
int strideDst = dst.Stride;
byte* pSrc = (byte*)src.Scan0.ToPointer();
byte* pDst = (byte*)dst.Scan0.ToPointer();
bool allowParallel = useParallel && (height > 16) && (Environment.ProcessorCount > 1);
if (allowParallel) {
Parallel.For(0, height, i => {
int start = i;
int len = 1;
byte* pSrc2 = pSrc + start * (long)strideSrc;
byte* pDst2 = pDst + start * (long)strideDst;
UseVectorsDoBatch(pSrc2, strideSrc, width, len, pDst2, strideDst);
});
} else {
UseVectorsDoBatch(pSrc, strideSrc, width, height, pDst, strideDst);
}
}
跟上篇文章所说的 YShuffleKernel 一样,YShuffleX3Kernel 也提供了Args、Core后缀的方法。这用这些方法,可以将部分运算从循环内,挪至循环前,从而提高了性能。
源代码如下。
public static unsafe void UseVectorsArgsDoBatch(byte* pSrc, int strideSrc, int width, int height, byte* pDst, int strideDst) {
const int cbPixel = 3; // 24 bit: Bgr24, Rgb24.
Vectors.YShuffleX3Kernel_Args(_shuffleIndices0, out var indices0arg0, out var indices0arg1, out var indices0arg2, out var indices0arg3);
Vectors.YShuffleX3Kernel_Args(_shuffleIndices1, out var indices1arg0, out var indices1arg1, out var indices1arg2, out var indices1arg3);
Vectors.YShuffleX3Kernel_Args(_shuffleIndices2, out var indices2arg0, out var indices2arg1, out var indices2arg2, out var indices2arg3);
int vectorWidth = Vector<byte>.Count;
if (width <= vectorWidth) {
ScalarDoBatch(pSrc, strideSrc, width, height, pDst, strideDst);
return;
}
int maxX = width - vectorWidth;
byte* pRow = pSrc;
byte* qRow = pDst;
for (int i = 0; i < height; i++) {
Vector<byte>* pLast = (Vector<byte>*)pRow;
Vector<byte>* qLast = (Vector<byte>*)(qRow + maxX * cbPixel);
Vector<byte>* p = (Vector<byte>*)(pRow + maxX * cbPixel);
Vector<byte>* q = (Vector<byte>*)qRow;
for (; ; ) {
Vector<byte> data0, data1, data2, temp0, temp1, temp2;
// Load.
data0 = p[0];
data1 = p[1];
data2 = p[2];
// FlipX.
//temp0 = Vectors.YShuffleX3Kernel(data0, data1, data2, _shuffleIndices0);
//temp1 = Vectors.YShuffleX3Kernel(data0, data1, data2, _shuffleIndices1);
//temp2 = Vectors.YShuffleX3Kernel(data0, data1, data2, _shuffleIndices2);
temp0 = Vectors.YShuffleX3Kernel_Core(data0, data1, data2, indices0arg0, indices0arg1, indices0arg2, indices0arg3);
temp1 = Vectors.YShuffleX3Kernel_Core(data0, data1, data2, indices1arg0, indices1arg1, indices1arg2, indices1arg3);
temp2 = Vectors.YShuffleX3Kernel_Core(data0, data1, data2, indices2arg0, indices2arg1, indices2arg2, indices2arg3);
// Store.
q[0] = temp0;
q[1] = temp1;
q[2] = temp2;
// Next.
if (p <= pLast) break;
p -= cbPixel;
q += cbPixel;
if (p < pLast) p = pLast; // The last block is also use vector.
if (q > qLast) q = qLast;
}
pRow += strideSrc;
qRow += strideDst;
}
}
.NET 6.0
程序的测试结果X86架构上.NET 6.0
程序的基准测试结果如下。
BenchmarkDotNet v0.14.0, Windows 11 (10.0.22631.4541/23H2/2023Update/SunValley3)
AMD Ryzen 7 7840H w/ Radeon 780M Graphics, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.403
[Host] : .NET 6.0.35 (6.0.3524.45918), X64 RyuJIT AVX2
DefaultJob : .NET 6.0.35 (6.0.3524.45918), X64 RyuJIT AVX2
| Method | Width | Mean | Error | StdDev | Ratio | RatioSD | Code Size |
|--------------- |------ |------------:|----------:|----------:|------:|--------:|----------:|
| Scalar | 1024 | 1,110.8 us | 21.74 us | 22.33 us | 1.00 | 0.03 | 2,053 B |
| UseVectors | 1024 | 492.3 us | 9.74 us | 15.72 us | 0.44 | 0.02 | 4,505 B |
| UseVectorsArgs | 1024 | 238.9 us | 3.14 us | 2.94 us | 0.22 | 0.00 | 4,234 B |
| | | | | | | | |
| Scalar | 2048 | 4,430.0 us | 87.93 us | 94.08 us | 1.00 | 0.03 | 2,053 B |
| UseVectors | 2048 | 2,319.6 us | 18.62 us | 17.41 us | 0.52 | 0.01 | 4,505 B |
| UseVectorsArgs | 2048 | 1,793.2 us | 34.57 us | 33.95 us | 0.40 | 0.01 | 4,234 B |
| | | | | | | | |
| Scalar | 4096 | 16,536.4 us | 329.23 us | 618.37 us | 1.00 | 0.05 | 2,053 B |
| UseVectors | 4096 | 9,040.4 us | 104.73 us | 97.96 us | 0.55 | 0.02 | 4,490 B |
| UseVectorsArgs | 4096 | 6,728.0 us | 120.28 us | 133.69 us | 0.41 | 0.02 | 4,219 B |
以1024时的测试结果为例,来观察向量化算法比起标量算法的性能提升。
将程序的输出信息翻到最前面,注意看这2行信息。
Vectors.Instance: VectorTraits256Avx2 // Avx, Avx2, Sse, Sse2
YShuffleX3Kernel_AcceleratedTypes: SByte, Byte, Int16, UInt16, Int32, UInt32, Int64, UInt64, Single, Double
Vectors.Instance
: Vectors 用的是哪一套实现。“VectorTraits256Avx2”表示是256位Avx2指令集的实现。且它右侧的“//”后面,给出了已使用指令集的名称列表。例如现在是 Avx, Avx2, Sse, Sse2
. (由于在组装256位向量时,有时需使用128位向量,故也使用了 Sse、Sse2 指令集)。YShuffleX3Kernel_AcceleratedTypes
: YShuffleX3Kernel的哪些元素类型有硬件加速。上面的代码使用的是Byte类型,而该属性含有Byte类型,故上面的代码中的YShuffleX3Kernel是有硬件加速的。为了方便大家观察所使用的指令集、是否有硬件极速,后面会将这2行信息放在基准测试结果前,一起展示。
.NET 7.0
程序的测试结果X86架构上.NET 7.0
程序的基准测试结果如下。
Vectors.Instance: VectorTraits256Avx2 // Avx, Avx2, Sse, Sse2
YShuffleX3Kernel_AcceleratedTypes: SByte, Byte, Int16, UInt16, Int32, UInt32, Int64, UInt64, Single, Double
BenchmarkDotNet v0.14.0, Windows 11 (10.0.22631.4541/23H2/2023Update/SunValley3)
AMD Ryzen 7 7840H w/ Radeon 780M Graphics, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.403
[Host] : .NET 7.0.20 (7.0.2024.26716), X64 RyuJIT AVX2
DefaultJob : .NET 7.0.20 (7.0.2024.26716), X64 RyuJIT AVX2
| Method | Width | Mean | Error | StdDev | Ratio | RatioSD | Code Size |
|--------------- |------ |------------:|----------:|----------:|------:|--------:|----------:|
| Scalar | 1024 | 1,120.3 us | 22.39 us | 25.78 us | 1.00 | 0.03 | 1,673 B |
| UseVectors | 1024 | 236.7 us | 4.63 us | 5.69 us | 0.21 | 0.01 | 3,724 B |
| UseVectorsArgs | 1024 | 209.5 us | 4.00 us | 4.45 us | 0.19 | 0.01 | 4,031 B |
| | | | | | | | |
| Scalar | 2048 | 4,431.6 us | 65.38 us | 61.16 us | 1.00 | 0.02 | 1,673 B |
| UseVectors | 2048 | 1,866.8 us | 36.26 us | 48.41 us | 0.42 | 0.01 | 3,724 B |
| UseVectorsArgs | 2048 | 1,889.9 us | 37.54 us | 74.97 us | 0.43 | 0.02 | 4,031 B |
| | | | | | | | |
| Scalar | 4096 | 16,617.9 us | 329.75 us | 559.94 us | 1.00 | 0.05 | 1,673 B |
| UseVectors | 4096 | 6,337.2 us | 62.08 us | 55.03 us | 0.38 | 0.01 | 3,709 B |
| UseVectorsArgs | 4096 | 6,408.1 us | 126.27 us | 118.11 us | 0.39 | 0.01 | 4,016 B |
以1024时的测试结果为例,来观察向量化算法比起标量算法的性能提升。
此时可以注意到,UseVectors与UseVectorsArgs的性能差距不大了。这是因为从 .NET 7.0
开始,即时编译器(JIT)会做优化,自动将循环内的重复运算挪至循环。故造成了差距不大的现象。
.NET 8.0
程序的测试结果X86架构上.NET 8.0
程序的基准测试结果如下。
Vectors.Instance: VectorTraits256Avx2 // Avx, Avx2, Sse, Sse2, Avx512VL
YShuffleX3Kernel_AcceleratedTypes: SByte, Byte, Int16, UInt16, Int32, UInt32, Int64, UInt64, Single, Double
BenchmarkDotNet v0.14.0, Windows 11 (10.0.22631.4541/23H2/2023Update/SunValley3)
AMD Ryzen 7 7840H w/ Radeon 780M Graphics, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.403
[Host] : .NET 8.0.10 (8.0.1024.46610), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
DefaultJob : .NET 8.0.10 (8.0.1024.46610), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
| Method | Width | Mean | Error | StdDev | Ratio | RatioSD |
|--------------- |------ |------------:|-----------:|-----------:|------:|--------:|
| Scalar | 1024 | 549.22 us | 10.876 us | 11.637 us | 1.00 | 0.03 |
| UseVectors | 1024 | 68.21 us | 1.326 us | 2.142 us | 0.12 | 0.00 |
| UseVectorsArgs | 1024 | 68.71 us | 1.360 us | 2.453 us | 0.13 | 0.01 |
| | | | | | | |
| Scalar | 2048 | 2,704.83 us | 53.643 us | 92.531 us | 1.00 | 0.05 |
| UseVectors | 2048 | 1,014.52 us | 8.824 us | 7.822 us | 0.38 | 0.01 |
| UseVectorsArgs | 2048 | 1,020.66 us | 15.739 us | 14.723 us | 0.38 | 0.01 |
| | | | | | | |
| Scalar | 4096 | 9,778.60 us | 114.022 us | 106.656 us | 1.00 | 0.01 |
| UseVectors | 4096 | 4,360.43 us | 60.832 us | 56.903 us | 0.45 | 0.01 |
| UseVectorsArgs | 4096 | 4,341.89 us | 82.877 us | 101.780 us | 0.44 | 0.01 |
以1024时的测试结果为例,来观察向量化算法比起标量算法的性能提升。
性能大幅度提升!这是因为 .NET 8.0
支持了Avx512系列指令集,且这个CPU支持。对比一下 Vectors.Instance
右侧的信息,会发现现在多了 Avx512VL 指令集。在Avx512系列指令集中,Avx512VL就是负责处理128~256位数据的指令集。
其实,由于 .NET 8.0
也优化了标量算法,这导致上面的的性能提升倍数看起来比较低。若拿 .NET 7.0
的测试结果,与 .NET 8.0
的UseVectors进行对比,就能看出差别了。
.NET 8.0
向量算法的性能,是 .NET 7.0
标量算法的 16.42 倍。.NET 8.0
向量算法的性能,是 .NET 7.0
向量算法的 3.47 倍。也可看做,Avx512的性能是Avx2的3.47倍。同样是256位向量宽度,Avx512为什么能快这么多?这是因为Avx2没有提供“跨小道(lane)重排指令”,导致需要使用2条shuffle指令才能实现全256位的换位。而Avx512不仅提供了“跨小道重排指令”(_mm_permutexvar_epi8
),且提供了“2向量的跨小道重排指令”(_mm_permutex2var_epi8
)。再加上内部还可以利用512位寄存器进行进一步优化,于是性能提升了很多。(下一篇文章会详细讲解)
同样的源代码可以在 Arm 架构上运行。
.NET 6.0
程序的测试结果Arm架构上.NET 6.0
程序的基准测试结果如下。
Vectors.Instance: VectorTraits128AdvSimdB64 // AdvSimd
YShuffleX3Kernel_AcceleratedTypes: SByte, Byte, Int16, UInt16, Int32, UInt32, Int64, UInt64, Single, Double
BenchmarkDotNet v0.14.0, macOS Sequoia 15.1.1 (24B91) [Darwin 24.1.0]
Apple M2, 1 CPU, 8 logical and 8 physical cores
.NET SDK 8.0.204
[Host] : .NET 6.0.33 (6.0.3324.36610), Arm64 RyuJIT AdvSIMD
DefaultJob : .NET 6.0.33 (6.0.3324.36610), Arm64 RyuJIT AdvSIMD
| Method | Width | Mean | Error | StdDev | Ratio |
|--------------- |------ |-------------:|----------:|----------:|------:|
| Scalar | 1024 | 1,504.84 us | 0.449 us | 0.375 us | 1.00 |
| UseVectors | 1024 | 119.36 us | 0.042 us | 0.040 us | 0.08 |
| UseVectorsArgs | 1024 | 83.89 us | 0.160 us | 0.149 us | 0.06 |
| | | | | | |
| Scalar | 2048 | 6,011.17 us | 1.346 us | 1.193 us | 1.00 |
| UseVectors | 2048 | 476.02 us | 6.485 us | 6.066 us | 0.08 |
| UseVectorsArgs | 2048 | 328.52 us | 0.298 us | 0.264 us | 0.05 |
| | | | | | |
| Scalar | 4096 | 24,403.68 us | 6.763 us | 6.326 us | 1.00 |
| UseVectors | 4096 | 3,378.05 us | 1.674 us | 1.566 us | 0.14 |
| UseVectorsArgs | 4096 | 2,852.52 us | 22.086 us | 20.660 us | 0.12 |
以1024时的测试结果为例,来观察向量化算法比起标量算法的性能提升。
注意一下 Vectors.Instance
右侧的信息,会发现它使用了 AdvSimd 指令集。
.NET 7.0
程序的测试结果Arm架构上.NET 7.0
程序的基准测试结果如下。
Vectors.Instance: VectorTraits128AdvSimdB64 // AdvSimd
YShuffleX3Kernel_AcceleratedTypes: SByte, Byte, Int16, UInt16, Int32, UInt32, Int64, UInt64, Single, Double
BenchmarkDotNet v0.14.0, macOS Sequoia 15.1.1 (24B91) [Darwin 24.1.0]
Apple M2, 1 CPU, 8 logical and 8 physical cores
.NET SDK 8.0.204
[Host] : .NET 7.0.20 (7.0.2024.26716), Arm64 RyuJIT AdvSIMD
DefaultJob : .NET 7.0.20 (7.0.2024.26716), Arm64 RyuJIT AdvSIMD
| Method | Width | Mean | Error | StdDev | Ratio |
|--------------- |------ |-------------:|---------:|---------:|------:|
| Scalar | 1024 | 1,504.47 us | 0.639 us | 0.566 us | 1.00 |
| UseVectors | 1024 | 108.65 us | 0.139 us | 0.123 us | 0.07 |
| UseVectorsArgs | 1024 | 81.78 us | 0.142 us | 0.133 us | 0.05 |
| | | | | | |
| Scalar | 2048 | 6,014.20 us | 2.201 us | 1.718 us | 1.00 |
| UseVectors | 2048 | 427.18 us | 0.286 us | 0.267 us | 0.07 |
| UseVectorsArgs | 2048 | 318.35 us | 0.373 us | 0.330 us | 0.05 |
| | | | | | |
| Scalar | 4096 | 24,403.88 us | 6.181 us | 5.480 us | 1.00 |
| UseVectors | 4096 | 3,280.84 us | 4.771 us | 4.463 us | 0.13 |
| UseVectorsArgs | 4096 | 2,873.47 us | 4.675 us | 4.373 us | 0.12 |
以1024时的测试结果为例,来观察向量化算法比起标量算法的性能提升。
性能稍有提升。
.NET 8.0
程序的测试结果Arm架构上.NET 8.0
程序的基准测试结果如下。
Vectors.Instance: VectorTraits128AdvSimdB64 // AdvSimd
YShuffleX3Kernel_AcceleratedTypes: SByte, Byte, Int16, UInt16, Int32, UInt32, Int64, UInt64, Single, Double
BenchmarkDotNet v0.14.0, macOS Sequoia 15.1.1 (24B91) [Darwin 24.1.0]
Apple M2, 1 CPU, 8 logical and 8 physical cores
.NET SDK 8.0.204
[Host] : .NET 8.0.4 (8.0.424.16909), Arm64 RyuJIT AdvSIMD
DefaultJob : .NET 8.0.4 (8.0.424.16909), Arm64 RyuJIT AdvSIMD
| Method | Width | Mean | Error | StdDev | Ratio |
|--------------- |------ |------------:|----------:|----------:|------:|
| Scalar | 1024 | 478.43 us | 2.053 us | 1.921 us | 1.00 |
| UseVectors | 1024 | 61.18 us | 0.677 us | 0.633 us | 0.13 |
| UseVectorsArgs | 1024 | 61.93 us | 0.225 us | 0.199 us | 0.13 |
| | | | | | |
| Scalar | 2048 | 1,891.65 us | 5.621 us | 4.693 us | 1.00 |
| UseVectors | 2048 | 260.20 us | 0.201 us | 0.179 us | 0.14 |
| UseVectorsArgs | 2048 | 263.75 us | 0.851 us | 0.796 us | 0.14 |
| | | | | | |
| Scalar | 4096 | 7,900.34 us | 91.227 us | 85.333 us | 1.00 |
| UseVectors | 4096 | 2,310.99 us | 17.264 us | 14.416 us | 0.29 |
| UseVectorsArgs | 4096 | 2,310.74 us | 1.605 us | 1.423 us | 0.29 |
以1024时的测试结果为例,来观察向量化算法比起标量算法的性能提升。
由于 .NET 8.0
也优化了标量算法,这导致上面的的性能提升倍数看起来比较低。若拿 .NET 7.0
的测试结果,与 .NET 8.0
的UseVectors进行对比,就能看出差别了。
.NET 8.0
向量算法的性能,是 .NET 7.0
标量算法的 24.59 倍。.NET 8.0
向量算法的性能,是 .NET 7.0
向量算法的 1.32 倍。可看出,性能有较大提升。
同样是128位向量宽度, .NET 8.0
为什么能快这么多?这是因为 .NET 8.0
新增了对 AdvSimd指令集里的“2-4向量查表”指令的支持。其实Arm很早就有了这些指令,只是 .NET
直到.NET 8.0
时才将这些指令给集成进来。
使用VectorTraits库,您只需升级到 .NET 8.0
,同样的源代码在编译时会自动切换为最佳的硬件指令。
同样的源代码可以在 .NET Framework
上运行。基准测试结果如下。
Vectors.Instance: VectorTraits256Base //
YShuffleX3Kernel_AcceleratedTypes: None
BenchmarkDotNet v0.14.0, Windows 11 (10.0.22631.4541/23H2/2023Update/SunValley3)
AMD Ryzen 7 7840H w/ Radeon 780M Graphics, 1 CPU, 16 logical and 8 physical cores
[Host] : .NET Framework 4.8.1 (4.8.9282.0), X64 RyuJIT VectorSize=256
DefaultJob : .NET Framework 4.8.1 (4.8.9282.0), X64 RyuJIT VectorSize=256
| Method | Width | Mean | Error | StdDev | Ratio | RatioSD | Code Size |
|--------------- |------ |------------:|------------:|------------:|------:|--------:|----------:|
| Scalar | 1024 | 999.7 us | 14.16 us | 11.82 us | 1.00 | 0.02 | 2,717 B |
| UseVectors | 1024 | 6,040.0 us | 57.76 us | 54.03 us | 6.04 | 0.09 | NA |
| UseVectorsArgs | 1024 | 5,896.4 us | 105.77 us | 98.94 us | 5.90 | 0.12 | NA |
| | | | | | | | |
| Scalar | 2048 | 4,267.0 us | 74.72 us | 69.90 us | 1.00 | 0.02 | 2,717 B |
| UseVectors | 2048 | 23,070.7 us | 250.11 us | 221.72 us | 5.41 | 0.10 | NA |
| UseVectorsArgs | 2048 | 23,106.7 us | 241.23 us | 201.44 us | 5.42 | 0.10 | NA |
| | | | | | | | |
| Scalar | 4096 | 15,977.6 us | 308.91 us | 489.96 us | 1.00 | 0.04 | 2,717 B |
| UseVectors | 4096 | 91,944.4 us | 1,152.83 us | 1,078.36 us | 5.76 | 0.19 | NA |
| UseVectorsArgs | 4096 | 92,677.3 us | 1,555.69 us | 1,527.90 us | 5.81 | 0.20 | NA |
UseVectors 反而更慢了,这是因为 YShuffleX3Kernel 没有硬件加速。可以看到 “YShuffleX3Kernel_AcceleratedTypes”为“None”。
在实际使用时,应先检查YShuffleX3Kernel_AcceleratedTypes属性。当发现它没有硬件加速时,宜切换为标量算法。
VectorTraits库提供了完善的多向量换位的功能,能对 2~4个向量进行换位。它们的名称如下。
使用这些方法,能帮您解决很多算法的向量化改造难题。