在 上一篇文章里,给大家讲解了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</