|
在上一篇文章里,我们讲解了图像的垂直翻转(FlipY)算法,于是本文来探讨水平翻转(FlipX)。先讲解比较容易的32位图像水平翻转算法,便于后续文章来探讨复杂的24位图像水平翻转算法。
本文除了会给出标量算法外,还会给出向量算法。且这些算法是跨平台的,同一份源代码,能在 X86(Sse、Avx等指令集)及Arm(AdvSimd等指令集)等架构上运行,且均享有SIMD硬件加速。
一、标量算法
1.1 算法思路
水平翻转又称左右翻转,是将图像沿着垂直中轴线进行翻转。
假设用 src[x, y] 可以访问源图像中的像素,用 dst[x, y] 可以访问目标图像中的像素,width是图像的像素宽度。那么水平翻转的公式为——- dst[x, y] = src[width - 1 - x, y]
复制代码 注意像素坐标是从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[k] = p[k];
- }
- p -= cbPixel;
- q += cbPixel;
- }
- pRow += strideSrc;
- qRow += strideDst;
- }
- }
复制代码 用指针来编写图像的水平翻转算法,最关键的是做好地址计算。现在是水平翻转,故重点是做好行内像素(内循环j)相关的地址计算。
内循环采用了“逆序读取、顺序写入”的策略。具体来说——
- 读取是从最后像素开始的,每次循环后移动到前一个像素。于是在上面的源代码中,p的初值是 pRow + (width - 1) * cbPixel(目标位图最后一行的地址),每次循环后q会 减去 cbPixel。
- 写入是从第0个像素开始的,每次循环后移动到下一个像素。于是在上面的源代码中,q的初值就是 qRow,每次循环后q会 加上 cbPixel。
1.3 基准测试代码
使用 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);
- }
- }
复制代码 由于现在是图像水平翻转,是对行内像素进行处理。而对外循环的每一行,可以简单的依次来处理。于是并行(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[indices]。若索引超过范围,对应的元素会设置为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[Vector<int>.Count];
- for (int i = 0;i< Vector<int>.Count; i++) {
- buf[i] = 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[Vector<int>.Count];
- for (int i = 0;i< Vector<int>.Count; i++) {
- buf[i] = 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 基准测试代码
随后为该算法编写基准测试代码。- [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 使用 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
- [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 | 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) [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 [AttachedDebugger]
- 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) [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 [AttachedDebugger]
- 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
- [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 | 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的标量算法要好。
附录
出处: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】 我们会及时删除侵权内容,谢谢合作! |
|