[C#] 对32位图像进行水平翻转(FlipX)的跨平台SIMD硬件加速向量算法(使用Ve
在上一篇文章里,我们讲解了图像的垂直翻转(FlipY)算法,于是本文来探讨水平翻转(FlipX)。先讲解比较容易的32位图像水平翻转算法,便于后续文章来探讨复杂的24位图像水平翻转算法。本文除了会给出标量算法外,还会给出向量算法。且这些算法是跨平台的,同一份源代码,能在 X86(Sse、Avx等指令集)及Arm(AdvSimd等指令集)等架构上运行,且均享有SIMD硬件加速。
一、标量算法
1.1 算法思路
水平翻转又称左右翻转,是将图像沿着垂直中轴线进行翻转。
假设用 src 可以访问源图像中的像素,用 dst 可以访问目标图像中的像素,width是图像的像素宽度。那么水平翻转的公式为——
dst = src注意像素坐标是从0开始编号的。于是最右边像素的x坐标是 width - 1。
简单来说,就是将行内的每一个像素,按相反的方向复制一遍。
由于需要逐个逐个的处理每一个像素,所以得根据不同的像素大小来编写算法。
1.1.1 32位像素的说明
32位像素是容易处理的。因为32位就是4字节,这是2的整数次幂,处理起来很方便。所以32位图像的使用频率最高。
受到 RGB通道顺序、是否含有 Alpha通道 等细节的影响,32位的像素有很多种像素格式——
[*]Bgr32。又称 BGRX8888、B8G8R8X8。GDI+ 里称 “Format32bppRgb”。
[*]Bgra32。又称 BGRA8888、B8G8R8A8。GDI+ 里称“Format32bppArgb”。
[*]Pbgra32。又称 预乘的BGRA8888、B8G8R8A8。GDI+ 里称“Format32bppPArgb”。
[*]Rgb32。又称 RGBX8888、R8G8B8X8。
[*]Rgba32。又称 RGBA8888、R8G8B8A8。
[*]Prgba32。又称 预乘的RGBA8888、R8G8B8A8。
由于现在是做图像水平翻转,无需精确到颜色通道,而是可以将整个像素作为整体进行处理。所以,本文的算法对所有的32位像素格式都有效,不仅上面提到的Bgr32等格式,其实连 Cmyk32等其他32位像素也有效。
1.2 算法实现
知道像素的字节数(cbPixel)后,便可以根据它来复制像素了。32位,就是4个字节。
源代码如下。
public static unsafe void ScalarDoBatch(byte* pSrc, int strideSrc, int width, int height, byte* pDst, int strideDst) {
const int cbPixel = 4; // 32 bit: Bgr32, Bgra32, Rgb32, Rgba32.
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 = p;
}
p -= cbPixel;
q += cbPixel;
}
pRow += strideSrc;
qRow += strideDst;
}
}用指针来编写图像的水平翻转算法,最关键的是做好地址计算。现在是水平翻转,故重点是做好行内像素(内循环j)相关的地址计算。
内循环采用了“逆序读取、顺序写入”的策略。具体来说——
[*]读取是从最后像素开始的,每次循环后移动到前一个像素。于是在上面的源代码中,p的初值是 pRow + (width - 1) * cbPixel(目标位图最后一行的地址),每次循环后q会 减去 cbPixel。
[*]写入是从第0个像素开始的,每次循环后移动到下一个像素。于是在上面的源代码中,q的初值就是 qRow,每次循环后q会 加上 cbPixel。
1.3 基准测试代码
使用 BenchmarkDotNet 进行基准测试。
public void Scalar() {
ScalarDo(_sourceBitmapData, _destinationBitmapData, false);
}
//
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);
}
}由于现在是图像水平翻转,是对行内像素进行处理。而对外循环的每一行,可以简单的依次来处理。于是并行(allowParallel)计算时的地址计算比较简单。
二、向量算法
2.1 算法思路
2.1.1 一个向量内如何做翻转
上面的标量算法是每次复制1个字节,而向量算法可以每次复制1个向量。
此时会遇到第一个难点——向量的颗粒度太大了。先前的标量算法是逐个字节的复制,能精准定位到每一个字节,具有很高的灵活性。而现在使用向量类型后,是一次性操作至少16个字节,笨重了很多。
Vector 类型的最小长度是128位,既16个字节。此时对于32位像素来说,1个Vector内可以存储4个像素。
所以首先要解决一个向量内如何做翻转的难题。
2.1.1.1 .NET 7.0的解决办法
在 .NET 7.0 之前,是没有好的办法。
从.NET 7.0开始,Vector128等向量类型增加了 Shuffle 方法。用该方法,可以给向量内的元素进行换位。为了支持不同的元素类型,该方法具有这些重载:
public static Vector128<byte> Shuffle(Vector128<byte> vector, Vector128<byte> indices);
public static Vector128<int> Shuffle(Vector128<int> vector, Vector128<int> indices);
...参数说明如下。
[*]vector: 源向量。
[*]indices: 索引。
[*]返回值:一个新向量,其中包含在vector里根据 indices所选定的值。例如它的第i个元素,就是vector里的第 indices 个元素。即 vector]。若索引超过范围,对应的元素会设置为0。
首先想到的是使用byte版的Shuffle方法,来做向量内的翻转。因为这是先前标量算法的思路。
但它不是最佳选择。因为现在是对32位像素进行处理,可以将整个像素一起处理。int是32位整数,于是可以选择int版的Shuffle方法。(由于是整个像素进行处理,不必关心符号位等细节,故 int、uint都能处理。只是用int会更简洁一些)
Vector128里可以存放4个32位像素。于是可以使用下面的代码进行翻转。
// Vector128<int> src = …… // 加载源值.
Vector128<int> indices = Vector128.Create((int)3, 2, 1, 0);
Vector128<int> dst = Vector128.Shuffle(src, indices);上述代码能够正常工作。但是实际使用,你会发现它存在一个重大缺点——速度太慢。
对它进行反汇编分析,会发现直至 .NET 8.0,Shuffle都没有硬件加速。而是使用了标量回退代码。
除了没有硬件加速外,Shuffle还存在这些缺点:
[*]仅固定大小的向量类型(如 Vector128、Vector256 等)提供了Shuffle方法,而自动大小的向量类型(Vector)尚未提供。
[*].NET 7.0才开始提供Shuffle方法,而早期版本的 .NET 没有这个方法,导致很多算法难以实现。
2.1.1.2 使用VectorTraits来解决Shuffle的缺点
为了解决 Shuffle 方法没有硬件加速的问题,我开发了VectorTraits 库。它使用了各个架构的shuffle类别的指令,从而使 Shuffle 方法具有硬件加速。具体来说,它分别使用了以下指令。
[*]X86: 使用 _mm_shuffle_epi8等指令.
[*]Arm: 使用 vqvtbl1q_u8 指令.
[*]Wasm: 使用 i8x16.swizzle 指令.
VectorTraits 不仅为固定大小的向量类型(如 Vector128)提供了Shuffle方法,它还为自动大小的向量类型(Vector)也提供了Vector方法。
而且 VectorTraits 能支持早期版本的 .NET。目前 3.0 版的VectorTraits,支持以下.NET 版本。
[*].NET: 5.0 - 8.0。
[*].NET Core: 2.0 - 3.1。
[*].NET Framework: 4.5 - 4.8.1。
[*].NET Standard: 1.1 - 2.1。
借助VectorTraits,可以方便的编写跨平台的SIMD硬件加速向量算法。
VectorTraits给各种向量类型,都提供了对应的静态类,规则是 “原名+s”。例如对于Vector128,提供了Vector128s类。于是将上述代码中的 Vector128s.Shuffle,加上一个字母“s”,使其变为 Vector128.Shuffle,便能享有SIMD硬件加速。
using Zyl.VectorTraits;
// Vector128<int> src = …… // 加载源值.
Vector128<int> indices = Vector128.Create((int)3, 2, 1, 0);
Vector128<int> dst = Vector128s.Shuffle(src, indices);2.1.1.3 使用自动大小的向量类型Vector
从.NET Core 3.0开始,才提供Vector128等固定大小向量类型。所以上面代码需要 .NET Core 3.0 或更高的环境。
如果是更早版本的 .NET,该怎么办呢?
答案是——换成自动大小的向量类型Vector。
从 .NET Framework 4.5开始,使用 nuget 安装了 System.Numerics.Vectors 包后,就能使用自动大小的向量类型Vector。
Vector 类型的大小不是固定的。一般来说,它是本机CPU的最大向量大小。
[*]X86:当支持 Avx和Avx2 指令集时,为256位;否则(例如仅支持 Sse系列指令集时)为128位。(直至 .NET 8.0, Vector 类型还不支持512位。即使CPU支持Avx512指令集,Vector 类型还是最高256位)
[*]Arm:目前固定为 128位。
[*]Wasm:目前固定为 128位。
Vector 类型提供了 Count属性,用来获取向量内元素数量。
[*]若 Vector为128位时,Vector.Count 的结果为4。
[*]若 Vector为256位时,Vector.Count 的结果为8。
由于Vector 类型的大小不是固定的,这给我们使用Shuffle方法带来了一些麻烦。先前给Vector128类型的indices设置初值时,因为元素数量固定,故直接写好每一个值就行。而面对自动大小的向量类型Vector,不能直接给indices设置初值。
查看文档,会发现 Vector的构造函数支持数组参数。故可以事先创建好数组,随后写个循环,在数组内填充值,最后用 Vector的构造函数来创建向量。
从.NET Core 3.0开始,Vector的构造函数还支持Span参数。于是可以使用栈分配,来减少内存分配的开销。源代码如下。
Span<int> buf = stackalloc int;
for (int i = 0;i< Vector<int>.Count; i++) {
buf = Vector<int>.Count - 1 - i;
}
indices = Vectors.Create(buf);上面代码中的 Vector.Count - 1 - i,就是计算各个元素在逆序时的索引。
还可以注意到,上面的代码并未使用构造函数来创建 Vector,而是使用VectorTraits提供的 Vectors.Create方法。这是为了能支持 .NET Core 3.0 之前的版本,例如 .NET Framework 4.5。
由于在程序启动后,Vector的Count属性将会固定为实际的值。于是没必要每次重新计算 indices,可以将它的计算挪至类的静态构造方法。
private static readonly Vector<int> _shuffleIndices;
static ImageFlipXOn32bitBenchmark() {
bool AllowCreateByDoubleLoop = true;
if (AllowCreateByDoubleLoop) {
_shuffleIndices = Vectors.CreateByDoubleLoop<int>(Vector<int>.Count - 1, -1);
} else {
Span<int> buf = stackalloc int;
for (int i = 0;i< Vector<int>.Count; i++) {
buf = Vector<int>.Count - 1 - i;
}
_shuffleIndices = Vectors.Create(buf);
}
}从上面的源代码中可以发现,Vectors 还提供了 CreateByDoubleLoop 方法,可以简化 indices 这样的向量的初始化。它比使用for循环要方便了很多。
有了 indices值(实际是 _shuffleIndices)后,便可以使用 Vectors 的 Shuffle 方法,对 自动大小的向量类型Vector 进行换位了。源代码如下。
// Vector<int> src = …… // 加载源值.
Vector<int> indices =_shuffleIndices;
Vector<int> dst = Vectors.Shuffle(src, indices);与先前Vector128的用法相同。
2.1.1.4 使用YShuffleKernel方法来做进一步的优化
Shuffle 方法还具有清零功能。若索引超过范围,对应的元素会设置为0。
而现在是做翻转,索引总是在有效范围内。于是,可以将 Shuffle 更换成 YShuffleKernel 方法。它不判断索引是否在范围内,所以它的性能一般更好。
// Vector<int> src = …… // 加载源值.
Vector<int> indices =_shuffleIndices;
Vector<int> dst = Vectors.YShuffleKernel(src, indices);为了与BCL的方法名进行区分,VectorTraits库追加的方法,都统一以字母“Y”开头。
2.1.2 翻转一行
基于“一个向量内翻转”的办法,可实现对一行像素进行翻转。具体来说,依然可以按照“逆序读取、顺序写入”的策略来处理。
若一行像素的字节数,刚好是向量大小的整数倍时,此时处理起来最简单。算法步骤如下:
[*]对源指针p设置初值,将它指向源位图当前行的最后一笔数据的地址。即: Vector* p = (Vector*)(pRow + maxX * cbPixel)。
[*]对目标指针q设置初值,将它指向目标位图当前行的起始地址。即: Vector* q = (Vector*)qRow。
[*]根据源指针p,将内存中的数据加载到向量中。即: data = *p。
[*]对向量进行翻转。即: temp = Vectors.YShuffleKernel(data, indices)。
[*]根据目标指针q,将向量写入到内存中。即: *q = temp。
[*]判断数据是否都处理完了。若是,跳到第9步完成。
[*]移动指针,处理下一个向量。即: --p; ++q;。
[*]跳到第3步,继续循环。
[*]完成。
假设向量大小为128位,此时1个向量里可以存储4个像素。下面演示一下各种倍数时的处理情况:
[*]1倍:此时一行是 4*1=4个像素。水平翻转是将 {x0, x1, x2, x3},翻转为 {x3, x2, x1, x0}.
[*]2倍:此时一行是 4*2=8个像素。水平翻转是将 {x0, x1, x2, x3, x4, x5, x6, x7},翻转为 {x7, x6, x5, x4, x3, x2, x1, x0}.
[*]3倍:此时一行是 4*3=12个像素。水平翻转是将 {x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11},翻转为 {x11, x10, x9, x8, x7, x6, x5, x4, x3, x2, x1, x0}.
[*]……
从上面的数据中可以看出,按照“逆序读取、顺序写入”的策略来翻转数据,便能顺利的完成图像的水平翻转。
在实际使用中,一行像素的字节数在大多数时候,并不是向量大小的整数倍,此时处理起来会复杂一些。可以参考上一篇文章里提到的“末尾指针”办法,进行处理。
2.2 算法实现
根据上面的思路,编写代码。源代码如下。
public static unsafe void UseVectorsDoBatch(byte* pSrc, int strideSrc, int width, int height, byte* pDst, int strideDst) {
const int cbPixel = 4; // 32 bit: Bgr32, Bgra32, Rgb32, Rgba32.
Vector<int> indices = _shuffleIndices;
int vectorWidth = Vector<int>.Count;
int maxX = width - vectorWidth;
byte* pRow = pSrc;
byte* qRow = pDst;
for (int i = 0; i < height; i++) {
Vector<int>* pLast = (Vector<int>*)pRow;
Vector<int>* qLast = (Vector<int>*)(qRow + maxX * cbPixel);
Vector<int>* p = (Vector<int>*)(pRow + maxX * cbPixel);
Vector<int>* q = (Vector<int>*)qRow;
for (; ; ) {
Vector<int> data, temp;
// Load.
data = *p;
// FlipX.
//temp = Vectors.Shuffle(data, indices);
temp = Vectors.YShuffleKernel(data, indices);
// Store.
*q = temp;
// Next.
if (p <= pLast) break;
--p;
++q;
if (p < pLast) p = pLast; // The last block is also use vector.
if (q > qLast) q = qLast;
}
pRow += strideSrc;
qRow += strideDst;
}
}2.3 基准测试代码
随后为该算法编写基准测试代码。
public void UseVectors() {
UseVectorsDo(_sourceBitmapData, _destinationBitmapData, false);
}
//
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 使用 YShuffleKernel_Args 来做进一步的优化
可以进一步提高性能,就是使用 YShuffleKernel_Args与YShuffleKernel_Core。
若循环内存在一些重复计算的话,可以将这些计算挪至循环外,从而提高了性能。Args、Core 后缀的方法,就是这种情况下使用的。
[*]Args: 参数运算。例如用于检查及转换参数。用本方法转换参数后,随后可调用 Core 版方法。一般在循环前使用。
[*]Core: 核心运算。需先调用 Args 版函数,才可调用本方法。一般在循环内使用。
于是我们可以将YShuffleKernel,换为 YShuffleKernel_Args与YShuffleKernel_Core。源代码如下。
public static unsafe void UseVectorsArgsDoBatch(byte* pSrc, int strideSrc, int width, int height, byte* pDst, int strideDst) {
const int cbPixel = 4; // 32 bit: Bgr32, Bgra32, Rgb32, Rgba32.
Vector<int> indices = _shuffleIndices;
Vector<int> args0, args1;
Vectors.YShuffleKernel_Args(indices, out args0, out args1);
int vectorWidth = Vector<int>.Count;
int maxX = width - vectorWidth;
byte* pRow = pSrc;
byte* qRow = pDst;
for (int i = 0; i < height; i++) {
Vector<int>* pLast = (Vector<int>*)pRow;
Vector<int>* qLast = (Vector<int>*)(qRow + maxX * cbPixel);
Vector<int>* p = (Vector<int>*)(pRow + maxX * cbPixel);
Vector<int>* q = (Vector<int>*)qRow;
for (; ; ) {
Vector<int> data, temp;
// Load.
data = *p;
// FlipX.
//temp = Vectors.YShuffleKernel(data, indices);
temp = Vectors.YShuffleKernel_Core(data, args0, args1);
// Store.
*q = temp;
// Next.
if (p <= pLast) break;
--p;
++q;
if (p < pLast) p = pLast; // The last block is also use vector.
if (q > qLast) q = qLast;
}
pRow += strideSrc;
qRow += strideDst;
}
}三、基准测试结果
3.1 X86 架构
X86架构下的基准测试结果如下。
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
: .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| 784.7 us |14.56 us |14.30 us |1.00 | 0.03 |
| UseVectors | 1024| 106.4 us | 2.12 us | 4.96 us |0.14 | 0.01 |
| UseVectorsArgs | 1024| 101.4 us | 2.03 us | 3.85 us |0.13 | 0.01 |
| | | | | | | |
| Scalar | 2048|3,453.5 us |25.88 us |22.94 us |1.00 | 0.01 |
| UseVectors | 2048|1,520.8 us |15.11 us |14.13 us |0.44 | 0.00 |
| UseVectorsArgs | 2048|1,412.9 us |27.96 us |47.48 us |0.41 | 0.01 |
| | | | | | | |
| Scalar | 4096| 12,932.8 us | 177.40 us | 165.94 us |1.00 | 0.02 |
| UseVectors | 4096|6,113.0 us |43.35 us |40.55 us |0.47 | 0.01 |
| UseVectorsArgs | 4096|6,270.9 us |56.80 us |50.35 us |0.48 | 0.01 |
[*]Scalar: 标量算法。
[*]UseVectors: 向量算法。
[*]UseVectorsArgs: 使用Args将部分运算挪至循环前的向量算法。
以1024时的测试结果为例,UseVectorsArgs的处理性能,大约是Scalar的 7.74 倍。即向量化算法的性能,是标量算法的7.74 倍。
注:784.7 / 101.4 ≈ 7.74
3.2 Arm 架构
同样的源代码可以在 Arm 架构上运行。基准测试结果如下。
BenchmarkDotNet v0.14.0, macOS Sequoia 15.1.1 (24B91)
Apple M2, 1 CPU, 8 logical and 8 physical cores
.NET SDK 8.0.204
: .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| 625.8 us |0.81 us |0.68 us |1.00 |
| UseVectors | 1024| 151.9 us |0.32 us |0.27 us |0.24 |
| UseVectorsArgs | 1024| 151.2 us |0.13 us |0.12 us |0.24 |
| | | | | | |
| Scalar | 2048|2,522.4 us |1.28 us |1.14 us |1.00 |
| UseVectors | 2048| 666.9 us |0.55 us |0.51 us |0.26 |
| UseVectorsArgs | 2048| 663.8 us |0.80 us |0.67 us |0.26 |
| | | | | | |
| Scalar | 4096| 10,797.2 us | 11.21 us | 10.48 us |1.00 |
| UseVectors | 4096|3,349.0 us | 39.67 us | 37.11 us |0.31 |
| UseVectorsArgs | 4096|3,339.6 us | 20.76 us | 16.21 us |0.31 |以1024时的测试结果为例,UseVectorsArgs的处理性能,大约是Scalar的 4.14 倍。即向量化算法的性能,是标量算法的4.14 倍。
注:625.8 / 151.2 ≈ 4.14
此时很多人会注意到,UseVectors 与 UseVectorsArgs的性能差距不大。貌似Args版方法的作用不大啊。
这是因为从 .NET 7.0 开始,即时编译器(JIT)会自动将部分运算挪至循环前去处理,造成了差距不大的现象。若换成早期版本的 .NET,差距会比较明显。
3.2.1 Arm 架构的 .NET 6.0 测试结果
将程序编译为 .NET 6.0 的,拿到 Arm 架构上运行。基准测试结果如下。
BenchmarkDotNet v0.14.0, macOS Sequoia 15.1.1 (24B91)
Apple M2, 1 CPU, 8 logical and 8 physical cores
.NET SDK 8.0.204
: .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,805.2 us |0.72 us |0.60 us |1.00 |
| UseVectors | 1024| 454.5 us |5.45 us |5.10 us |0.25 |
| UseVectorsArgs | 1024| 158.4 us |0.05 us |0.04 us |0.09 |
| | | | | | |
| Scalar | 2048|7,229.0 us |2.88 us |2.69 us |1.00 |
| UseVectors | 2048|1,857.4 us |2.73 us |2.56 us |0.26 |
| UseVectorsArgs | 2048| 656.2 us |0.26 us |0.23 us |0.09 |
| | | | | | |
| Scalar | 4096| 29,574.1 us | 13.21 us | 11.03 us |1.00 |
| UseVectors | 4096|8,117.2 us | 28.06 us | 26.25 us |0.27 |
| UseVectorsArgs | 4096|4,671.7 us |2.50 us |2.21 us |0.16 |以1024时的测试结果为例,来观察向量化算法相对于标量算法的性能提升。
[*]UseVectors:1,805.2/454.5 ≈ 3.97。即性能提高了 3.97 倍。
[*]UseVectorsArgs:1,805.2/158.4 ≈ 11.40。即性能提高了 11.40 倍。
3.3 .NET Framework
同样的源代码可以在 .NET Framework 上运行。基准测试结果如下。
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 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|1,315.2 us |26.06 us |25.59 us |1.00 | 0.03 | 2,718 B |
| UseVectors | 1024| 968.2 us |17.55 us |16.42 us |0.74 | 0.02 | 3,507 B |
| UseVectorsArgs | 1024| 887.0 us | 9.91 us | 8.78 us |0.67 | 0.01 | 3,507 B |
| | | | | | | | |
| Scalar | 2048|5,259.4 us |85.87 us |80.32 us |1.00 | 0.02 | 2,718 B |
| UseVectors | 2048|3,696.0 us |29.64 us |27.72 us |0.70 | 0.01 | 3,507 B |
| UseVectorsArgs | 2048|3,722.9 us |39.36 us |34.90 us |0.71 | 0.01 | 3,507 B |
| | | | | | | | |
| Scalar | 4096| 19,763.1 us | 300.29 us | 266.20 us |1.00 | 0.02 | 2,718 B |
| UseVectors | 4096| 14,303.8 us |62.36 us |55.28 us |0.72 | 0.01 | 3,507 B |
| UseVectorsArgs | 4096| 14,988.7 us | 286.49 us | 281.37 us |0.76 | 0.02 | 3,507 B |以1024时的测试结果为例,UseVectorsArgs的处理性能,大约是Scalar的 1.48 倍。
注:1,315.2 / 887.0 ≈ 1.48
其实,因为 .NET Framework 不支持Sse等指令集,所以 Vectors用的是标量回退代码。只要由于它的标量算法也是高度优化的,且它是基于 int 来处理的,于是它的性能比基于byte的标量算法要好。
附录
[*]完整源代码: https://github.com/zyl910/VectorTraits.Sample.Benchmarks/blob/main/VectorTraits.Sample.Benchmarks.Inc/Image/ImageFlipXOn32bitBenchmark.cs
[*]YShuffleKernel 的文档: https://zyl910.github.io/VectorTraits_doc/api/Zyl.VectorTraits.Vectors.YShuffleKernel.html
[*]微软官方文档-Vector128.Shuffle 方法: https://learn.microsoft.com/zh-cn/dotnet/api/system.runtime.intrinsics.vector128.shuffle?view=net-9.0
[*]VectorTraits 的NuGet包: https://www.nuget.org/packages/VectorTraits
[*]VectorTraits 的在线文档: https://zyl910.github.io/VectorTraits_doc/
[*]VectorTraits 源代码: https://github.com/zyl910/VectorTraits
[*]C# 使用SIMD向量类型加速浮点数组求和运算(1):使用Vector4、Vector
出处:http://www.cnblogs.com/zyl910/ 版权声明:自由转载-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0.
来源:https://www.cnblogs.com/zyl910/p/18580435/VectorTraits_Sample_Image_ImageFlipXOn32bitBenchmark
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!
页:
[1]