在上一篇文章里,给大家讲解了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 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 |
- 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, 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: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, 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时的测试结果为例,来观察向量化算法比起标量算法的性能提升。
- 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
)。再加上内部还可以利用512位寄存器进行进一步优化,于是性能提升了很多。(下一篇文章会详细讲解)
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, 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时的测试结果为例,来观察向量化算法比起标量算法的性能提升。
- 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, 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时的测试结果为例,来观察向量化算法比起标量算法的性能提升。
- 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, 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时的测试结果为例,来观察向量化算法比起标量算法的性能提升。
- 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: 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个向量进行换位。它们的名称如下。
- 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)