[C#] 对24位图像进行水平翻转(FlipX)的跨平台SIMD硬件加速向量算法(使用YShuffleX3Kernel)

文章目录

  • 一、标量算法
    • 1.1 算法实现
    • 1.2 基准测试代码
  • 二、向量算法
    • 2.1 算法思路
      • 2.1.1 难点说明
      • 2.1.2 解决办法:每次处理3个向量
      • 2.1.3 用YShuffleX3Kernel对3个向量内的24位像素进行翻转
    • 2.2 算法实现
    • 2.3 基准测试代码
    • 2.4 使用 YShuffleX3Kernel_Args 来做进一步的优化
  • 三、基准测试结果
    • 3.1 X86 架构
      • 3.1.1 X86 架构上`.NET 6.0`程序的测试结果
      • 3.1.2 X86 架构上`.NET 7.0`程序的测试结果
      • 3.1.3 X86 架构上`.NET 8.0`程序的测试结果
    • 3.2 Arm 架构
      • 3.2.1 Arm 架构上`.NET 6.0`程序的测试结果
      • 3.2.2 Arm 架构上`.NET 7.0`程序的测试结果
      • 3.2.3 Arm 架构上`.NET 8.0`程序的测试结果
    • 3.3 .NET Framework
  • 四、结语
  • 附录

在 上一篇文章里,给大家讲解了32位图像水平翻转(FlipX)算法,于是本文来探讨更加复杂的24位图像水平翻转算法。
本文除了会给出标量算法外,还会给出向量算法。且这些算法是跨平台的,同一份源代码,能在 X86(Sse、Avx等指令集)及Arm(AdvSimd等指令集)等架构上运行,且均享有SIMD硬件加速。

一、标量算法

1.1 算法实现

标量算法对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;}
}

1.2 基准测试代码

使用 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);}
}

二、向量算法

2.1 算法思路

2.1.1 难点说明

24位像素的标量算法改的很简单,但是24位像素的向量算法要复杂的多。

这是因为向量大小一般是 16或32字节这样的2的整数幂,而24位像素是3个字节一组,无法整除。这就给地址计算、数据处理等方面,带来很大的难题。

2.1.2 解决办法:每次处理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 增加了一批“多向量换位”的硬件指令,于是在以下平台,能获得更好的硬件加速。

  • Arm: .NET 8.0 新增了对 AdvSimd指令集里的“2-4向量查表”指令的支持。例如 vqtbl3q_u8.
  • X86: .NET 8.0 新增了对 Avx512系列指令集的支持,而它提供了“2向量重排”的指令。例如 _mm_permutex2var_epi8.

详见 [C#] .NET8增加了Arm架构的多寄存器的查表函数(VectorTableLookup/VectorTableLookupExtension)。

YShuffleX3 在 .NET Framework 等平台上运行时是没有硬件加速的,这是因为这些平台不支持Sse等向量指令。可以通过 Vectors 的 YShuffleX3Kernel_AcceleratedTypes 属性来得知哪些元素类型有硬件加速。当发现不支持时,宜切换为标量算法。

另外,还可以通过 Vectors.Instance.UsedInstructionSets 来查看该向量所使用的指令集。

2.1.3 用YShuffleX3Kernel对3个向量内的24位像素进行翻转

为了便于跨平台,这里使用了自动大小向量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);

随后便可参考上一篇文章的思路,对整个图像进行水平翻转。

2.2 算法实现

根据上面的思路,编写代码。源代码如下。

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;}
}

2.3 基准测试代码

随后为该算法编写基准测试代码。

[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);}
}

2.4 使用 YShuffleX3Kernel_Args 来做进一步的优化

跟上篇文章所说的 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;}
}

三、基准测试结果

3.1 X86 架构

3.1.1 X86 架构上.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 AVX2DefaultJob : .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 |
  • Scalar: 标量算法。
  • UseVectors: 向量算法。
  • UseVectorsArgs: 使用Args将部分运算挪至循环前的向量算法。

以1024时的测试结果为例,来观察向量化算法比起标量算法的性能提升。

  • UseVectors:1,110.8/492.3 ≈ 2.26。即性能提升了 2.26 倍。
  • UseVectorsArgs:1,110.8/238.9 ≈4.65。即性能提升了 4.65 倍。

将程序的输出信息翻到最前面,注意看这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行信息放在基准测试结果前,一起展示。

3.1.2 X86 架构上.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, DoubleBenchmarkDotNet 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 AVX2DefaultJob : .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:1,120.3/236.7 ≈ 4.73。
  • UseVectorsArgs:1,120.3/209.5 ≈5.35。

此时可以注意到,UseVectors与UseVectorsArgs的性能差距不大了。这是因为从 .NET 7.0 开始,即时编译器(JIT)会做优化,自动将循环内的重复运算挪至循环。故造成了差距不大的现象。

3.1.3 X86 架构上.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, DoubleBenchmarkDotNet 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+VBMIDefaultJob : .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时的测试结果为例,来观察向量化算法比起标量算法的性能提升。

  • UseVectors:549.22/68.21 ≈ 8.05。
  • UseVectorsArgs:549.22/68.71 ≈7.99。

性能大幅度提升!这是因为 .NET 8.0 支持了Avx512系列指令集,且这个CPU支持。对比一下 Vectors.Instance右侧的信息,会发现现在多了 Avx512VL 指令集。在Avx512系列指令集中,Avx512VL就是负责处理128~256位数据的指令集。

其实,由于 .NET 8.0也优化了标量算法,这导致上面的的性能提升倍数看起来比较低。若拿 .NET 7.0的测试结果,与 .NET 8.0的UseVectors进行对比,就能看出差别了。

  • Scalar:1,120.3/68.21 ≈ 16.42。即 .NET 8.0向量算法的性能,是 .NET 7.0标量算法的 16.42 倍。
  • UseVectors:236.7/68.21 ≈ 3.47。即 .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)。所以此时Avx512的理论性是Avx2的4倍,上面的向量算法提升了 3.47 倍,与理论值匹配。

3.2 Arm 架构

同样的源代码可以在 Arm 架构上运行。

3.2.1 Arm 架构上.NET 6.0程序的测试结果

Arm架构上.NET 6.0程序的基准测试结果如下。

Vectors.Instance:	VectorTraits128AdvSimdB64	// AdvSimd
YShuffleX3Kernel_AcceleratedTypes:	SByte, Byte, Int16, UInt16, Int32, UInt32, Int64, UInt64, Single, DoubleBenchmarkDotNet 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 AdvSIMDDefaultJob : .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时的测试结果为例,来观察向量化算法比起标量算法的性能提升。

  • UseVectors:1,504.84/119.36 ≈ 12.61。
  • UseVectorsArgs:1,504.84/83.89 ≈17.94。

注意一下 Vectors.Instance右侧的信息,会发现它使用了 AdvSimd 指令集。

3.2.2 Arm 架构上.NET 7.0程序的测试结果

Arm架构上.NET 7.0程序的基准测试结果如下。

Vectors.Instance:	VectorTraits128AdvSimdB64	// AdvSimd
YShuffleX3Kernel_AcceleratedTypes:	SByte, Byte, Int16, UInt16, Int32, UInt32, Int64, UInt64, Single, DoubleBenchmarkDotNet 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 AdvSIMDDefaultJob : .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时的测试结果为例,来观察向量化算法比起标量算法的性能提升。

  • UseVectors:1,504.47/108.65 ≈ 13.85。
  • UseVectorsArgs:1,504.47/81.78 ≈18.40。

性能稍有提升。

3.2.3 Arm 架构上.NET 8.0程序的测试结果

Arm架构上.NET 8.0程序的基准测试结果如下。

Vectors.Instance:	VectorTraits128AdvSimdB64	// AdvSimd
YShuffleX3Kernel_AcceleratedTypes:	SByte, Byte, Int16, UInt16, Int32, UInt32, Int64, UInt64, Single, DoubleBenchmarkDotNet 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 AdvSIMDDefaultJob : .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时的测试结果为例,来观察向量化算法比起标量算法的性能提升。

  • UseVectors:478.43/61.18 ≈ 7.82。
  • UseVectorsArgs:478.43/61.93 ≈7.73。

由于 .NET 8.0也优化了标量算法,这导致上面的的性能提升倍数看起来比较低。若拿 .NET 7.0的测试结果,与 .NET 8.0的UseVectors进行对比,就能看出差别了。

  • Scalar:1,504.47/61.18 ≈ 24.59。即 .NET 8.0向量算法的性能,是 .NET 7.0标量算法的 24.59 倍。
  • UseVectors:108.65/61.18 ≈ 1.78。
  • UseVectorsArgs:81.78/61.93 ≈ 1.32。即 .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,同样的源代码在编译时会自动切换为最佳的硬件指令。

3.3 .NET Framework

同样的源代码可以在 .NET Framework 上运行。基准测试结果如下。

Vectors.Instance:       VectorTraits256Base     //
YShuffleX3Kernel_AcceleratedTypes:      NoneBenchmarkDotNet 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=256DefaultJob : .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个向量进行换位。它们的名称如下。

  • 2个向量: YShuffleX2, YShuffleX2Insert, YShuffleX2Kernel。
  • 3个向量: YShuffleX3, YShuffleX3Insert, YShuffleX3Kernel。
  • 4个向量: YShuffleX4, YShuffleX4Insert, YShuffleX4Kernel。

使用这些方法,能帮您解决很多算法的向量化改造难题。

附录

  • 完整源代码: https://github.com/zyl910/VectorTraits.Sample.Benchmarks/blob/main/VectorTraits.Sample.Benchmarks.Inc/Image/ImageFlipXOn24bitBenchmark.cs
  • YShuffleX3Kernel 的文档: https://zyl910.github.io/VectorTraits_doc/api/Zyl.VectorTraits.Vectors.YShuffleX3Kernel.html
  • VectorTraits 的NuGet包: https://www.nuget.org/packages/VectorTraits
  • VectorTraits 的在线文档: https://zyl910.github.io/VectorTraits_doc/
  • VectorTraits 源代码: https://github.com/zyl910/VectorTraits
  • [C#] .NET8增加了Arm架构的多寄存器的查表函数(VectorTableLookup/VectorTableLookupExtension)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/63071.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python打包深度学习虚拟环境

今天师兄让我把环境打包发给他&#xff0c;我才知道可以直接打包深度学习虚拟环境&#xff0c;这样另一个人就不用辛辛苦苦的去装环境了&#xff0c;我们都知道有些论文他需要的环境很难装上。比如装Apex&#xff0c;装 DCN&#xff0c;mmcv-full 我现在把3090机子上的ppft虚拟…

基于MobileNet深度学习网络的MQAM调制类型识别matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 (完整程序运行后无水印) 2.算法运行软件版本 matlab2022a 3.部分核心程序 &#xff08;完整版代码包含详细中文注释和操作步骤视频&#xff09…

Ceph分布式存储集群搭建

一.Ceph分布式存储 (1)Ceph概念:Ceph是一种高性能、可扩展的分布式存储系统&#xff0c;它提供了对象存储、块存储和文件系统存储三种存储接口,Ceph的设计目标是提供无单点故障的、高性能的、可扩展的存储解决方案&#xff0c;同时能够降低存储成本。(2)常用分布式存储方案Lust…

docker及docker exec命令学习笔记

docker exec 是一个常用的 Docker 命令&#xff0c;允许你在已经运行的容器中执行命令或启动新的进程。以下是详细介绍和常见用法&#xff1a; 基本语法 docker exec [OPTIONS] CONTAINER COMMAND [ARG...]参数详解 1. CONTAINER指定目标容器的名字或容器 ID。可以通过以下命…

<工具 Claude Desktop> 配置 MCP server 连接本地 SQLite, 本机文件夹(目录) 网络驱动器 Windows 11 系统

也是在学习中... 起因&#xff1a; 抖音博客 艾克AI分享 他的视频 #143《Claude开源MCP彻底打破AI的信息孤岛》 提到: Claude开源的MCP太强了&#xff0c;视频后面是快速演示&#xff0c;反正看了好几遍也没弄明白。菜单都不一样&#xff0c;感觉用的不是同一家 Claude. 探…

(78)MPSK基带调制通信系统瑞利平坦衰落信道传输性能的MATLAB仿真

文章目录 前言一、MATLAB仿真1.仿真代码2.仿真结果 二、子函数与完整代码总结 前言 本文给出瑞利平坦衰落信道上的M-PSK通信系统性能仿真的MATLAB源代码与仿真结果。其中&#xff0c;调制方式M-PSK包括BPSK、QPSK、8-PSK、16-PSK、32-PSK等方式。 一、MATLAB仿真 1.仿真代码 …

go语言 Pool实现资源池管理数据库连接资源或其他常用需要共享的资源

go Pool Pool用于展示如何使用有缓冲的通道实现资源池&#xff0c;来管理可以在任意数量的goroutine之间共享及独立使用的资源。这种模式在需要共享一组静态资源的情况&#xff08;如共享数据库连接或者内存缓冲区&#xff09;下非 常有用。如果goroutine需要从池里得到这些资…

LeetCode70. 爬楼梯(2024冬季每日一题 24)

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢&#xff1f; 示例 1&#xff1a; 输入&#xff1a;n 2 输出&#xff1a;2 解释&#xff1a;有两种方法可以爬到楼顶。 1. 1 阶 1 阶 2. 2 阶 示例 2&#xf…

Android 系统之Init进程分析

1、Init进程流程 2、Init细节逻辑 2.1 Init触发shutdown init进程触发系统重启是一个很合理的逻辑&#xff0c;为什么合理&#xff1f; init进程是android世界的一切基石&#xff0c;如果android世界的某些服务或者进程出现异常&#xff0c;那么会导致整个系统无法正常使用…

用micropython 操作stm32f4单片机的定时器实现蜂鸣器驱动

import pyb import time # 初始化引脚和定时器通道作为PWM输出 # 注意&#xff1a;这里我们假设您使用的是支持PWM的引脚和定时器 # 在不同的MicroPython板上&#xff0c;支持的引脚和定时器可能不同 # 请查阅您的板的文档以确认正确的引脚和定时器 buzzer_pin pyb.Pin(PD15,…

长沙市的科技查新单位

1、中南大学图书馆科技查新站&#xff1a; 中南大学图书馆科技查新站成立于2003年12月&#xff0c;中南大学图书馆科技查新站作为教育部首批批准的科技查新工作站之一&#xff0c;具备了在全国范围内开展科技查新工作的专业资质。 2、湖南大学科技查新站&#xff1a; 湖南大学…

java基础语法光速入门

前言 欢迎来到我的博客 个人主页:北岭敲键盘的荒漠猫-CSDN博客 本文整理Java的基础语法部分 适合有编程基础的人快点掌握语法使用 没学过一两门语言的话。。还是不建议看了 极致的浓缩没有一点解释 注释 单行注释 // 多行注释 /**/ 数据类型 布尔型:true false 整型:int,lon…

【redis】集群详解

redis集群 一、集群的概念二、数据分片算法2.1哈希求余算法2.2一致性哈希算法2.3哈希槽分区算法 三、集群的搭建3.1配置docker-compose.yml文件3.2配置generate.sh脚本文件3.3构建redis集群3.4简单测试redis集群 四、故障处理流程4.1故障判定4.2故障转移 五、集群扩容 一、集群…

Linux | Linux的开机启动流程

对于linux系统的初学者来说&#xff0c;理解并掌握linux系统启动流程能够使你够深入的理解linux系统&#xff0c;还可以通过系统的启动过程来分析问题解决问题。 Linux开机启动的流程如下图 power on 开机 post自检&#xff08;检查一部分大的硬件&#xff09; BIOS&#xf…

Scala的模式匹配(5)

package hfd.test32 //匹配 变量的类型 object Test34_4 {def main(args: Array[String]): Unit {val i1val j:Double1.2val b:Booleanfalseval x:Anybx match {case _:Int>println(s"当前是Int")case _:Double>println(s"当前是Double")case a:Boo…

TiDB如何保证数据一致性

1. 分布式事务协议 TiDB 采用了类似 Google Percolator 的分布式事务协议来处理分布式事务。这个协议基于两阶段提交&#xff08;2PC&#xff09;的思想&#xff0c;但进行了优化和改进&#xff0c;以适应分布式环境的特殊需求。在 TiDB 中&#xff0c;当一个事务需要跨多个节…

【Maven系列】深入解析 Maven 常用命令

前言 在当今的软件开发过程中&#xff0c;项目管理是至关重要的一环。项目管理包括了项目构建、依赖管理以及发布部署等诸多方面。而在Java生态系统中&#xff0c;Maven已经成为了最受欢迎的项目管理工具之一。Maven 是一套用于构建、依赖管理和项目管理的工具&#xff0c;主要…

DBA面试题-1

面临失业&#xff0c;整理一下面试题&#xff0c;找下家继续搬砖 主要参考&#xff1a;https://www.csdn.net/?spm1001.2101.3001.4476 略有修改 一、mysql有哪些数据类型 1&#xff0c; 整形 tinyint,smallint,medumint,int,bigint&#xff1b;分别占用1字节、2字节、3字节…

【Rust WebAssembly 入门实操遇到的问题】

Rust WebAssembly 入门实操遇到的问题 什么是WebAssembly跟着教程走wasm-pack build error总结 什么是WebAssembly WebAssembly&#xff08;简称Wasm&#xff09;是一种基于堆栈的虚拟机的二进制指令 格式。Wasm 被设计为编程语言的可移植编译目标&#xff0c;支持在 Web 上部…

数据挖掘之数据预处理

​​​​​​​ 引言 数据挖掘是从大量数据中提取有用信息和知识的过程。在这个过程中&#xff0c;数据预处理是不可或缺的关键步骤。数据预处理旨在清理和转换数据&#xff0c;以提高数据质量&#xff0c;从而为后续的数据挖掘任务奠定坚实的基础。由于现实世界中的数据通常…