在本主題的 上一篇文章里,給大家講解了24位圖像水平翻轉(zhuǎn)(FlipX)算法。但該文章主要是為了介紹 YShuffleX3Kernel 的使用,該算法性能并不是最優(yōu)的。于是本文將介紹如何使用 YShuffleX2Kernel 來優(yōu)化程序。而且Imageshop在留言區(qū)給了一份C語言的、基于Sse系列指令集實(shí)現(xiàn)的代碼,正好一起對比一下。
一、算法思路
1.1 瓶頸分析
當(dāng)硬件沒有多向量換位(shuffle)的指令時(shí),一般來說需要3條換位指令才能實(shí)現(xiàn) YShuffleX3Kernel 方法。
對于24位圖像水平翻轉(zhuǎn)來說,每次內(nèi)循環(huán)是處理3個(gè)向量長度的數(shù)據(jù),需調(diào)用3次 YShuffleX3Kernel 方法,故共調(diào)用了 3*3=9 條換位指令。
且在使用X86平臺的Avx2指令集時(shí),因它沒有提供“跨小道(lane)換位”指令,導(dǎo)致需要用2條換位指令來實(shí)現(xiàn)“向量內(nèi)全范圍的換位”。于是對于24位圖像水平翻轉(zhuǎn)來說,故共調(diào)用了 3*3*2=18 條換位指令。這個(gè)數(shù)量很大了。
于是,若能降低換位指令的數(shù)量,將能提升程序的性能。
1.2 優(yōu)化思路
X86架構(gòu)的Sse指令集,與Arm架構(gòu)的AdvSimd指令集里均使用128位的向量寄存器,即16個(gè)字節(jié)。
24位像素是3個(gè)字節(jié)一組。對于16個(gè)字節(jié)向量寄存器來說,僅能完整存放 5個(gè)像素((int)(16 / 3) = 5),占據(jù)了 3*5=15 個(gè)字節(jié)。剩余的1個(gè)字節(jié)超出向量大小的邊界了,位于另一個(gè)向量中。
換個(gè)角度來看,使用1條單向量的換位指令,能將16個(gè)字節(jié)中的15個(gè)字節(jié)做好水平翻轉(zhuǎn)。僅剩1個(gè)字節(jié)的數(shù)據(jù)不正確,此時(shí)可以另想辦法來修正。
一種思路是用標(biāo)量的內(nèi)存讀寫語句來修正這些不正確的字節(jié)。這就是 Imageshop的Sse版算法的思路,對于每次處理48個(gè)字節(jié)的內(nèi)循環(huán),使用標(biāo)量的內(nèi)存讀寫語句修正了6個(gè)字節(jié)的數(shù)據(jù)。這種辦法能將換位指令數(shù)量降到了最低,但代價(jià)是增加了標(biāo)量的內(nèi)存讀寫工作。需要進(jìn)行基準(zhǔn)測試,實(shí)際對比性能。
另一種思路是再用1條換位指令來計(jì)算剩余字節(jié)的數(shù)據(jù),然后使用條件掩碼將這2種結(jié)果(15個(gè)字節(jié)+剩余1字節(jié))合并。而且大多數(shù)架構(gòu)(X86、Arm)的換位指令帶有清零能力,小心的調(diào)整掩碼,能使無效字節(jié)為0,這樣僅需 or 指令就能將2種結(jié)果合并了。
上面這種計(jì)算,正好是“2向量的換位”操作。X86架構(gòu)的Avx512系列指令集,以及Arm架構(gòu)的AdvSimd指令集,均提供了這樣的指令。而且 .NET 8.0 支持了這些指令的內(nèi)在函數(shù)(Intrinsic Functions)。
手動(dòng)調(diào)用內(nèi)在函數(shù)是很繁瑣的,且難以跨平臺。于是 VectorTraits 提供了 YShuffleX2Kernel 方法。它會檢查硬件是否支持“2向量的換位”指令,如支持便會使用各平臺的這些指令;若不支持,便會自動(dòng)計(jì)算好掩碼,調(diào)用2條換位指令并組合。
也就是說,當(dāng)硬件沒有多向量換位的指令時(shí),YShuffleX2Kernel 比起 YShuffleX3Kernel,能少使用1個(gè)換位指令。從而降低了開銷,優(yōu)化了性能。
1.3 計(jì)算索引
對于24位圖像水平翻轉(zhuǎn)來說,每次內(nèi)循環(huán)是處理3個(gè)向量長度的數(shù)據(jù),此時(shí)需要3個(gè)索引向量。其中 索引0、索引2,僅需訪問2個(gè)數(shù)據(jù)向量,正好能使用 YShuffleX2Kernel。而中間的索引1,默認(rèn)情況會訪問3個(gè)數(shù)據(jù)向量。下面的表格詳細(xì)說明了這些情況。
| Name | offset | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Indice | 0 | 45 | 46 | 47 | 42 | 43 | 44 | 39 | 40 | 41 | 36 | 37 | 38 | 33 | 34 | 35 | 30 | 31 | 32 | 27 | 28 | 29 | 24 | 25 | 26 | 21 | 22 | 23 | 18 | 19 | 20 | 15 | 16 | 17 | 12 | 13 | 14 | 9 | 10 | 11 | 6 | 7 | 8 | 3 | 4 | 5 | 0 | 1 | 2 |
| Indice0 | 16 | 29 | 30 | 31 | 26 | 27 | 28 | 23 | 24 | 25 | 20 | 21 | 22 | 17 | 18 | 19 | 14 | ||||||||||||||||||||||||||||||||
| Indice1E | 16 | 15 | 16 | 11 | 12 | 13 | 8 | 9 | 10 | 5 | 6 | 7 | 2 | 3 | 4 | -1 | 0 | ||||||||||||||||||||||||||||||||
| Indice1A | 0 | 31 | 32 | 27 | 28 | 29 | 24 | 25 | 26 | 21 | 22 | 23 | 18 | 19 | 20 | 15 | 16 | ||||||||||||||||||||||||||||||||
| Indice1B | 15 | 16 | 17 | 12 | 13 | 14 | 9 | 10 | 11 | 6 | 7 | 8 | 3 | 4 | 5 | 0 | 1 | ||||||||||||||||||||||||||||||||
| Indice2 | 0 | 17 | 12 | 13 | 14 | 9 | 10 | 11 | 6 | 7 | 8 | 3 | 4 | 5 | 0 | 1 | 2 |
變量說明:
- Indice: YShuffleX3Kernel 的索引。3個(gè)索引均寫在同一行。
- Indice0: YShuffleX2Kernel 的索引0。
- Indice1E: 演示了錯(cuò)誤方案下的索引1。
- Indice1A: 索引1使用A方案。
- Indice1B: 索引1使用B方案。
- Indice2: YShuffleX2Kernel 的索引2。
索引2(Indice2)最簡單,直接使用 YShuffleX3Kernel的索引2就行。因它僅需訪問第0個(gè)與第1個(gè)輸入向量。
索引0(Indice2)稍微復(fù)雜一點(diǎn)。它僅需訪問第1個(gè)與第2個(gè)輸入向量,于是從數(shù)據(jù)地址的偏移量(offset)來說,偏移量為16。即向量寄存器的字節(jié)數(shù)(Vector<byte>.Count)。
索引1最麻煩,因?yàn)樗枰L問3個(gè)向量。于是上表給出了 Indice1E 這一行,將 offset 設(shè)為16,于是可以觀察到它不僅有小于下界的值(-1),還有超過上界的值(16)。有2種思路來解決這一難題:
- A. 干脆用 YShuffleX3Kernel 來計(jì)算它。于是可以直接使用 YShuffleX3Kernel 時(shí)的索引1,即 Indice1A。也就說,內(nèi)循環(huán)會調(diào)用2次 YShuffleX2Kernel,和1次 YShuffleX3Kernel,比起調(diào)用3次YShuffleX3Kernel的開銷低。
- B. 干脆為它提供的輸入向量。此時(shí)用“偏移量15”來加載2筆連續(xù)的向量數(shù)據(jù),便能用 YShuffleX2Kernel 來計(jì)算了。由于現(xiàn)在數(shù)據(jù)加載地址很近(偏移15與偏移16很近),且現(xiàn)在處理器的高速緩存(Cache)技術(shù)很成熟,使得這種加載的開銷很低。
隨后可以通過基準(zhǔn)測試,來看哪種思路的性能更好。
二、算法實(shí)現(xiàn)
2.1 程序里計(jì)算索引
首先需要計(jì)算索引。可以在先前YShuffleX3Kernel的索引計(jì)算代碼上修改,關(guān)鍵是處理好 offset。
// -- Indices of YShuffleX3Kernel
private static readonly Vector<byte> _shuffleIndices0;
private static readonly Vector<byte> _shuffleIndices1;
private static readonly Vector<byte> _shuffleIndices2;
// -- Indices of YShuffleX2Kernel
private static readonly byte _shuffleX2Offset0 = (byte)Vector<byte>.Count;
private static readonly byte _shuffleX2Offset1A = 0;
private static readonly byte _shuffleX2Offset1B = (byte)(Vector<byte>.Count / 3 * 3);
private static readonly byte _shuffleX2Offset2 = 0;
private static readonly Vector<byte> _shuffleX2Indices0;
private static readonly Vector<byte> _shuffleX2Indices1A; // Need YShuffleX3Kernel
private static readonly Vector<byte> _shuffleX2Indices1B;
private static readonly Vector<byte> _shuffleX2Indices2;
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));
// -- Indices of YShuffleX2Kernel
_shuffleX2Indices0 = Vector.Subtract(_shuffleIndices0, new Vector<byte>(_shuffleX2Offset0));
_shuffleX2Indices1A = _shuffleIndices1; // _shuffleX2Offset1A is 0
_shuffleX2Indices1B = Vector.Subtract(_shuffleIndices1, new Vector<byte>(_shuffleX2Offset1B));
_shuffleX2Indices2 = _shuffleIndices2; // _shuffleX2Offset2 is 0
}
可見,代碼改動(dòng)不多。僅需使用 Vector.Subtract 做向量減法,減去偏移量。
2.2 思路A的實(shí)現(xiàn)
根據(jù)上面的思路A,編寫代碼。源代碼如下。
public static unsafe void UseVectorsX2AArgsDoBatch(byte* pSrc, int strideSrc, int width, int height, byte* pDst, int strideDst) {
const int cbPixel = 3; // 24 bit: Bgr24, Rgb24.
Vectors.YShuffleX2Kernel_Args(_shuffleX2Indices0, out var indices0arg0, out var indices0arg1, out var indices0arg2, out var indices0arg3);
Vectors.YShuffleX3Kernel_Args(_shuffleX2Indices1A, out var indices1arg0, out var indices1arg1, out var indices1arg2, out var indices1arg3);
Vectors.YShuffleX2Kernel_Args(_shuffleX2Indices2, 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.YShuffleX2Kernel_Core(data1, data2, indices0arg0, indices0arg1, indices0arg2, indices0arg3);
temp1 = Vectors.YShuffleX3Kernel_Core(data0, data1, data2, indices1arg0, indices1arg1, indices1arg2, indices1arg3);
temp2 = Vectors.YShuffleX2Kernel_Core(data0, data1, 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;
}
}
前面文章有提到,使用 Args、Core后綴的方法可以將部分運(yùn)算從循環(huán)內(nèi),挪至循環(huán)前,從而提高了性能。于是本函數(shù)便使用了這個(gè)辦法。
2.3 思路B的實(shí)現(xiàn)
根據(jù)上面的思路B,編寫代碼。源代碼如下。
public static unsafe void UseVectorsX2BArgsDoBatch(byte* pSrc, int strideSrc, int width, int height, byte* pDst, int strideDst) {
const int cbPixel = 3; // 24 bit: Bgr24, Rgb24.
int offsetB0 = _shuffleX2Offset1B;
int offsetB1 = offsetB0 + Vector<byte>.Count;
Vectors.YShuffleX2Kernel_Args(_shuffleX2Indices0, out var indices0arg0, out var indices0arg1, out var indices0arg2, out var indices0arg3);
Vectors.YShuffleX2Kernel_Args(_shuffleX2Indices1B, out var indices1arg0, out var indices1arg1, out var indices1arg2, out var indices1arg3);
Vectors.YShuffleX2Kernel_Args(_shuffleX2Indices2, 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, dataB0, dataB1, temp0, temp1, temp2;
// Load.
data0 = p[0];
data1 = p[1];
data2 = p[2];
dataB0 = *(Vector<byte>*)((byte*)p + offsetB0);
dataB1 = *(Vector<byte>*)((byte*)p + offsetB1);
// FlipX.
temp0 = Vectors.YShuffleX2Kernel_Core(data1, data2, indices0arg0, indices0arg1, indices0arg2, indices0arg3);
temp1 = Vectors.YShuffleX2Kernel_Core(dataB0, dataB1, indices1arg0, indices1arg1, indices1arg2, indices1arg3);
temp2 = Vectors.YShuffleX2Kernel_Core(data0, data1, 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;
}
}
思路B的內(nèi)循環(huán),全部使用 YShuffleX2Kernel_Core 來計(jì)算。只是需要為索引1(_shuffleX2Indices1B),加載其特有偏移量的2個(gè)向量(dataB0、dataB1)。由于偏移量是基于字節(jié)的,于是在在計(jì)算地址時(shí),需將指針p轉(zhuǎn)為了 byte*類型后再加偏移量,最后再轉(zhuǎn)回 Vector<byte>* 類型來做向量加載。
三、基準(zhǔn)測試結(jié)果
3.1 X86 架構(gòu)
3.1.1 X86 架構(gòu)上.NET 6.0程序的測試結(jié)果
X86架構(gòu)上.NET 6.0程序的基準(zhǔn)測試結(jié)果如下。
BenchmarkDotNet v0.14.0, Windows 11 (10.0.26100.3476)
AMD Ryzen 7 7840H w/ Radeon 780M Graphics, 1 CPU, 16 logical and 8 physical cores
.NET SDK 9.0.200
[Host] : .NET 6.0.36 (6.0.3624.51421), X64 RyuJIT AVX2
DefaultJob : .NET 6.0.36 (6.0.3624.51421), X64 RyuJIT AVX2
| Method | Width | Mean | Error | StdDev | Ratio | Code Size |
|------------------ |------ |------------:|----------:|----------:|------:|----------:|
| Scalar | 1024 | 1,047.8 us | 10.47 us | 9.79 us | 1.00 | 2,053 B |
| UseVectors | 1024 | 375.6 us | 7.49 us | 7.69 us | 0.36 | 4,505 B |
| UseVectorsArgs | 1024 | 202.0 us | 4.02 us | 4.94 us | 0.19 | 4,234 B |
| UseVectorsX2AArgs | 1024 | 149.6 us | 2.97 us | 8.63 us | 0.14 | 4,275 B |
| UseVectorsX2BArgs | 1024 | 125.2 us | 2.39 us | 2.11 us | 0.12 | 3,835 B |
| ImageshopSse | 1024 | 145.0 us | 2.81 us | 4.30 us | 0.14 | 1,440 B |
| | | | | | | |
| Scalar | 2048 | 4,248.4 us | 41.26 us | 38.59 us | 1.00 | 2,053 B |
| UseVectors | 2048 | 2,578.7 us | 18.84 us | 17.63 us | 0.61 | 4,505 B |
| UseVectorsArgs | 2048 | 2,022.4 us | 22.92 us | 21.44 us | 0.48 | 4,234 B |
| UseVectorsX2AArgs | 2048 | 1,710.7 us | 16.22 us | 14.38 us | 0.40 | 4,275 B |
| UseVectorsX2BArgs | 2048 | 1,682.1 us | 18.11 us | 16.94 us | 0.40 | 3,835 B |
| ImageshopSse | 2048 | 1,854.0 us | 21.15 us | 19.78 us | 0.44 | 1,440 B |
| | | | | | | |
| Scalar | 4096 | 16,231.0 us | 133.81 us | 118.62 us | 1.00 | 2,053 B |
| UseVectors | 4096 | 8,418.7 us | 55.64 us | 52.04 us | 0.52 | 4,490 B |
| UseVectorsArgs | 4096 | 5,906.4 us | 49.55 us | 46.34 us | 0.36 | 4,219 B |
| UseVectorsX2AArgs | 4096 | 5,497.9 us | 46.65 us | 43.64 us | 0.34 | 4,260 B |
| UseVectorsX2BArgs | 4096 | 5,385.9 us | 79.28 us | 74.16 us | 0.33 | 3,820 B |
| ImageshopSse | 4096 | 5,784.4 us | 50.70 us | 44.94 us | 0.36 | 1,440 B |
- Scalar: 標(biāo)量算法。
- UseVectors: 向量算法。
- UseVectorsArgs: 使用Args將部分運(yùn)算挪至循環(huán)前的向量算法。
- UseVectorsX2AArgs: 思路A的算法(2次YShuffleX2Kernel + 1次YShuffleX3Kernel)。
- UseVectorsX2BArgs: 思路B的算法(3次YShuffleX2Kernel,并處理特殊偏移 )。
- ImageshopSse: Imageshop的Sse版算法,翻譯為 C# 語言。
以1024時(shí)的測試結(jié)果為例,來觀察向量化算法比起標(biāo)量算法的性能提升。
- UseVectors:1,047.8/492.3 ≈ 2.13。即性能提升了 2.13 倍。
- UseVectorsArgs:1,047.8/202.0 ≈5.19。即性能提升了5.19 倍。
- UseVectorsX2AArgs:1,047.8/149.6 ≈7.00。即性能提升了 7.00 倍。
- UseVectorsX2BArgs:1,047.8/125.2 ≈8.37。即性能提升了 8.37 倍。
- ImageshopSse:1,047.8/145.0 ≈7.23。即性能提升了 7.23 倍。
可以發(fā)現(xiàn),減少換位指令數(shù)量的辦法確實(shí)有效,UseVectorsX2AArgs 等函數(shù)的速度,均比 UseVectorsArgs 要快。
還可發(fā)現(xiàn),內(nèi)循環(huán)僅使用3次換位的ImageshopSse ,性能與 UseVectorsX2AArgs 差不多。由于AVX2沒有“跨小道(lane)換位”指令,導(dǎo)致需要用2條換位指令來實(shí)現(xiàn)“向量內(nèi)全范圍的換位”,于是UseVectorsX2AArgs的內(nèi)循環(huán)里有 (2+3+2) * 2 = 14 條換位指令。雖然換位指令的數(shù)量相差這么多,但性能差不多,說明標(biāo)量內(nèi)存讀寫的開銷頗大。
UseVectorsX2BArgs 的速度最快。這是因?yàn)?UseVectorsX2BArgs的內(nèi)循環(huán) 比 UseVectorsX2AArgs 少一次換位操作,代價(jià)僅是多了2次向量加載。
3.1.2 X86 架構(gòu)上.NET 7.0程序的測試結(jié)果
X86架構(gòu)上.NET 7.0程序的基準(zhǔn)測試結(jié)果如下。
BenchmarkDotNet v0.14.0, Windows 11 (10.0.26100.3476)
AMD Ryzen 7 7840H w/ Radeon 780M Graphics, 1 CPU, 16 logical and 8 physical cores
.NET SDK 9.0.200
[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 | Code Size |
|------------------ |------ |------------:|----------:|----------:|------:|----------:|
| Scalar | 1024 | 1,009.2 us | 10.62 us | 9.42 us | 1.00 | 1,673 B |
| UseVectors | 1024 | 214.5 us | 4.05 us | 3.98 us | 0.21 | 3,724 B |
| UseVectorsArgs | 1024 | 179.5 us | 3.47 us | 3.71 us | 0.18 | 4,031 B |
| UseVectorsX2AArgs | 1024 | 146.9 us | 2.89 us | 2.84 us | 0.15 | 3,912 B |
| UseVectorsX2BArgs | 1024 | 119.5 us | 2.39 us | 2.75 us | 0.12 | 3,673 B |
| ImageshopSse | 1024 | 149.3 us | 2.92 us | 5.42 us | 0.15 | 1,350 B |
| | | | | | | |
| Scalar | 2048 | 4,233.3 us | 48.45 us | 45.32 us | 1.00 | 1,673 B |
| UseVectors | 2048 | 1,707.1 us | 21.99 us | 20.57 us | 0.40 | 3,724 B |
| UseVectorsArgs | 2048 | 1,625.7 us | 14.62 us | 13.68 us | 0.38 | 4,031 B |
| UseVectorsX2AArgs | 2048 | 1,519.1 us | 19.57 us | 18.30 us | 0.36 | 3,912 B |
| UseVectorsX2BArgs | 2048 | 1,439.8 us | 16.77 us | 15.69 us | 0.34 | 3,673 B |
| ImageshopSse | 2048 | 1,425.7 us | 18.37 us | 16.28 us | 0.34 | 1,350 B |
| | | | | | | |
| Scalar | 4096 | 15,994.4 us | 134.29 us | 119.04 us | 1.00 | 1,673 B |
| UseVectors | 4096 | 5,962.0 us | 76.95 us | 68.22 us | 0.37 | 3,709 B |
| UseVectorsArgs | 4096 | 5,858.2 us | 74.10 us | 69.31 us | 0.37 | 4,016 B |
| UseVectorsX2AArgs | 4096 | 5,528.2 us | 34.26 us | 32.05 us | 0.35 | 3,897 B |
| UseVectorsX2BArgs | 4096 | 5,342.9 us | 51.69 us | 48.35 us | 0.33 | 3,658 B |
| ImageshopSse | 4096 | 5,603.8 us | 38.53 us | 34.15 us | 0.35 | 1,350 B |
以1024時(shí)的測試結(jié)果為例,來觀察向量化算法比起標(biāo)量算法的性能提升。
- UseVectors:1,009.2/214.5 ≈ 4.70。
- UseVectorsArgs:1,009.2/179.5 ≈5.62。
- UseVectorsX2AArgs:1,009.2/146.9 ≈6.87。
- UseVectorsX2BArgs:1,009.2/119.5 ≈8.44。
- ImageshopSse:1,009.2/149.3 ≈6.76。
與 .NET 6.0 時(shí)的測試結(jié)果差不多,UseVectorsX2BArgs 最快,UseVectorsX2AArgs、ImageshopSse 并列第2。
3.1.3 X86 架構(gòu)上.NET 8.0程序的測試結(jié)果
X86架構(gòu)上.NET 8.0程序的基準(zhǔn)測試結(jié)果如下。
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.26100.3476)
AMD Ryzen 7 7840H w/ Radeon 780M Graphics, 1 CPU, 16 logical and 8 physical cores
.NET SDK 9.0.200
[Host] : .NET 8.0.13 (8.0.1325.6609), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
DefaultJob : .NET 8.0.13 (8.0.1325.6609), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI
| Method | Width | Mean | Error | StdDev | Ratio | Code Size |
|------------------ |------ |-------------:|----------:|----------:|------:|----------:|
| Scalar | 1024 | 565.61 us | 6.062 us | 5.671 us | 1.00 | NA |
| UseVectors | 1024 | 70.15 us | 0.946 us | 0.839 us | 0.12 | NA |
| UseVectorsArgs | 1024 | 71.35 us | 1.395 us | 2.368 us | 0.13 | NA |
| UseVectorsX2AArgs | 1024 | 70.38 us | 1.389 us | 1.757 us | 0.12 | NA |
| UseVectorsX2BArgs | 1024 | 71.11 us | 1.417 us | 1.325 us | 0.13 | NA |
| ImageshopSse | 1024 | 147.10 us | 3.065 us | 5.286 us | 0.28 | 1,304 B |
| | | | | | | |
| Scalar | 2048 | 2,778.83 us | 31.741 us | 28.137 us | 1.00 | NA |
| UseVectors | 2048 | 1,021.40 us | 10.916 us | 10.211 us | 0.37 | NA |
| UseVectorsArgs | 2048 | 1,057.84 us | 20.079 us | 18.782 us | 0.38 | NA |
| UseVectorsX2AArgs | 2048 | 1,057.32 us | 16.454 us | 15.391 us | 0.38 | NA |
| UseVectorsX2BArgs | 2048 | 1,012.21 us | 13.793 us | 12.227 us | 0.36 | NA |
| ImageshopSse | 2048 | 1,742.22 us | 15.396 us | 14.401 us | 0.63 | 1,308 B |
| | | | | | | |
| Scalar | 4096 | 11,051.36 us | 86.964 us | 77.092 us | 1.00 | NA |
| UseVectors | 4096 | 4,408.84 us | 48.341 us | 45.218 us | 0.40 | NA |
| UseVectorsArgs | 4096 | 4,330.39 us | 39.934 us | 35.401 us | 0.39 | NA |
| UseVectorsX2AArgs | 4096 | 4,336.47 us | 48.908 us | 45.748 us | 0.39 | NA |
| UseVectorsX2BArgs | 4096 | 4,083.04 us | 72.525 us | 67.840 us | 0.37 | NA |
| ImageshopSse | 4096 | 5,692.53 us | 53.488 us | 50.032 us | 0.52 | 1,311 B |
以1024時(shí)的測試結(jié)果為例,來觀察向量化算法比起標(biāo)量算法的性能提升。
- UseVectors:565.61/70.15 ≈ 8.06。
- UseVectorsArgs:565.61/71.35 ≈7.93。
- UseVectorsX2AArgs:565.61/70.38 ≈8.04。
- UseVectorsX2BArgs:565.61/71.11 ≈8.44。
- ImageshopSse:565.61/147.10 ≈6.76。
其實(shí),由于 .NET 8.0也優(yōu)化了標(biāo)量算法,這導(dǎo)致上面的的性能提升倍數(shù)看起來比較低。若拿 .NET 7.0的測試結(jié)果,與 .NET 8.0的UseVectors進(jìn)行對比,就能看出差別了。
- Scalar:1,120.3/70.15 ≈ 16.42。即
.NET 8.0向量算法的性能,是.NET 7.0標(biāo)量算法的 16.42 倍。 - UseVectors:236.7/70.15 ≈ 3.47。即
.NET 8.0向量算法的性能,是.NET 7.0向量算法的 3.47 倍。也可看做,Avx512的性能是Avx2的3.47倍。 - UseVectorsX2AArgs:146.9/70.38 ≈2.09。
- UseVectorsX2BArgs:119.5/71.11 ≈1.68。
- ImageshopSse:149.3/147.10 ≈1.01。
此時(shí) UseVectors、UseVectorsArgs、UseVectorsX2AArgs、UseVectorsX2BArgs 的成績相差不大,考慮到測試誤差,可以將它們并列第一。這是因?yàn)槭褂肁vx512系列指令集之后,256位向量無論是“2向量換位”,還是“3向量換位”,都是僅需1條換位指令。
而 ImageshopSse 的性能保持不變,這是因?yàn)樗潭ㄊ褂昧?Sse系列指令集。
3.2 Arm 架構(gòu)
同樣的源代碼可以在 Arm 架構(gòu)上運(yùn)行。
3.2.1 Arm 架構(gòu)上.NET 6.0程序的測試結(jié)果
Arm架構(gòu)上.NET 6.0程序的基準(zhǔn)測試結(jié)果如下。
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 9.0.102
[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 | RatioSD |
|------------------ |------ |-------------:|-----------:|-----------:|------:|--------:|
| Scalar | 1024 | 1,504.09 us | 0.575 us | 0.480 us | 1.00 | 0.00 |
| UseVectors | 1024 | 120.26 us | 1.569 us | 1.468 us | 0.08 | 0.00 |
| UseVectorsArgs | 1024 | 83.77 us | 0.067 us | 0.056 us | 0.06 | 0.00 |
| UseVectorsX2AArgs | 1024 | 72.68 us | 0.034 us | 0.030 us | 0.05 | 0.00 |
| UseVectorsX2BArgs | 1024 | 82.61 us | 0.283 us | 0.265 us | 0.05 | 0.00 |
| ImageshopSse | 1024 | NA | NA | NA | ? | ? |
| | | | | | | |
| Scalar | 2048 | 6,015.27 us | 5.786 us | 4.831 us | 1.00 | 0.00 |
| UseVectors | 2048 | 479.44 us | 0.424 us | 0.397 us | 0.08 | 0.00 |
| UseVectorsArgs | 2048 | 320.78 us | 0.212 us | 0.165 us | 0.05 | 0.00 |
| UseVectorsX2AArgs | 2048 | 332.22 us | 0.314 us | 0.263 us | 0.06 | 0.00 |
| UseVectorsX2BArgs | 2048 | 319.60 us | 1.490 us | 1.394 us | 0.05 | 0.00 |
| ImageshopSse | 2048 | NA | NA | NA | ? | ? |
| | | | | | | |
| Scalar | 4096 | 24,709.98 us | 308.477 us | 288.549 us | 1.00 | 0.02 |
| UseVectors | 4096 | 3,362.91 us | 1.807 us | 1.509 us | 0.14 | 0.00 |
| UseVectorsArgs | 4096 | 2,840.79 us | 13.642 us | 12.760 us | 0.11 | 0.00 |
| UseVectorsX2AArgs | 4096 | 2,592.20 us | 25.326 us | 23.690 us | 0.10 | 0.00 |
| UseVectorsX2BArgs | 4096 | 2,843.72 us | 30.984 us | 28.982 us | 0.12 | 0.00 |
| ImageshopSse | 4096 | NA | NA | NA | ? | ? |
以1024時(shí)的測試結(jié)果為例,來觀察向量化算法比起標(biāo)量算法的性能提升。
- UseVectors:1,504.09/120.26 ≈ 12.51。
- UseVectorsArgs:1,504.09/ 83.77 ≈17.94。
- UseVectorsX2AArgs:1,504.09/72.68 ≈20.69。
- UseVectorsX2BArgs:1,504.09/82.61 ≈18.21。
注意一下 Vectors.Instance右側(cè)的信息,會發(fā)現(xiàn)它使用了 AdvSimd 指令集。
因?yàn)楝F(xiàn)在是 Arm架構(gòu)的處理器,沒有Sse系列指令集,于是 ImageshopSse 沒有測試結(jié)果。而 UseVectorsX2AArgs 等支持跨平臺的測試函數(shù),都有測試結(jié)果。
可以發(fā)現(xiàn) UseVectorsX2AArgs 最快,而 UseVectorsX2BArgs 與 UseVectorsArgs 的性能差不多。看來在Arm平臺上 UseVectorsX2BArgs 減少1次換位指令的優(yōu)化,被它額外的2次向量加載給抵消了。
3.2.2 Arm 架構(gòu)上.NET 7.0程序的測試結(jié)果
Arm架構(gòu)上.NET 7.0程序的基準(zhǔn)測試結(jié)果如下。
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 9.0.102
[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 | RatioSD |
|------------------ |------ |-------------:|----------:|----------:|------:|--------:|
| Scalar | 1024 | 1,506.38 us | 2.527 us | 2.240 us | 1.00 | 0.00 |
| UseVectors | 1024 | 108.38 us | 0.170 us | 0.159 us | 0.07 | 0.00 |
| UseVectorsArgs | 1024 | 81.57 us | 0.070 us | 0.058 us | 0.05 | 0.00 |
| UseVectorsX2AArgs | 1024 | 69.35 us | 0.111 us | 0.098 us | 0.05 | 0.00 |
| UseVectorsX2BArgs | 1024 | 80.66 us | 0.104 us | 0.081 us | 0.05 | 0.00 |
| ImageshopSse | 1024 | NA | NA | NA | ? | ? |
| | | | | | | |
| Scalar | 2048 | 6,014.79 us | 2.863 us | 2.235 us | 1.00 | 0.00 |
| UseVectors | 2048 | 425.96 us | 0.234 us | 0.207 us | 0.07 | 0.00 |
| UseVectorsArgs | 2048 | 317.95 us | 0.273 us | 0.228 us | 0.05 | 0.00 |
| UseVectorsX2AArgs | 2048 | 270.73 us | 0.238 us | 0.199 us | 0.05 | 0.00 |
| UseVectorsX2BArgs | 2048 | 308.50 us | 1.324 us | 1.239 us | 0.05 | 0.00 |
| ImageshopSse | 2048 | NA | NA | NA | ? | ? |
| | | | | | | |
| Scalar | 4096 | 24,451.53 us | 31.420 us | 27.853 us | 1.00 | 0.00 |
| UseVectors | 4096 | 3,263.99 us | 3.354 us | 2.801 us | 0.13 | 0.00 |
| UseVectorsArgs | 4096 | 2,868.68 us | 7.482 us | 6.999 us | 0.12 | 0.00 |
| UseVectorsX2AArgs | 4096 | 2,512.38 us | 11.036 us | 9.783 us | 0.10 | 0.00 |
| UseVectorsX2BArgs | 4096 | 2,787.01 us | 4.692 us | 3.918 us | 0.11 | 0.00 |
| ImageshopSse | 4096 | NA | NA | NA | ? | ? |
以1024時(shí)的測試結(jié)果為例,來觀察向量化算法比起標(biāo)量算法的性能提升。
- UseVectors:1,506.38/108.38 ≈ 13.90。
- UseVectorsArgs:1,506.38/81.57 ≈18.47。
- UseVectorsX2AArgs:1,506.38/69.35 ≈21.72。
- UseVectorsX2BArgs:1,506.38/80.66 ≈18.68。
與 .NET 6.0 時(shí)的測試結(jié)果差不多,UseVectorsX2AArgs 最快,UseVectorsX2BArgs、UseVectorsArgs 并列第2。
3.2.3 Arm 架構(gòu)上.NET 8.0程序的測試結(jié)果
Arm架構(gòu)上.NET 8.0程序的基準(zhǔn)測試結(jié)果如下。
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 9.0.102
[Host] : .NET 8.0.12 (8.0.1224.60305), Arm64 RyuJIT AdvSIMD
DefaultJob : .NET 8.0.12 (8.0.1224.60305), Arm64 RyuJIT AdvSIMD
| Method | Width | Mean | Error | StdDev | Ratio | RatioSD |
|------------------ |------ |------------:|----------:|----------:|------:|--------:|
| Scalar | 1024 | 489.45 us | 9.667 us | 8.570 us | 1.00 | 0.02 |
| UseVectors | 1024 | 60.78 us | 0.050 us | 0.045 us | 0.12 | 0.00 |
| UseVectorsArgs | 1024 | 60.20 us | 0.621 us | 0.551 us | 0.12 | 0.00 |
| UseVectorsX2AArgs | 1024 | 61.02 us | 0.054 us | 0.045 us | 0.12 | 0.00 |
| UseVectorsX2BArgs | 1024 | 73.73 us | 0.159 us | 0.141 us | 0.15 | 0.00 |
| ImageshopSse | 1024 | NA | NA | NA | ? | ? |
| | | | | | | |
| Scalar | 2048 | 1,904.18 us | 25.572 us | 23.920 us | 1.00 | 0.02 |
| UseVectors | 2048 | 262.79 us | 0.482 us | 0.428 us | 0.14 | 0.00 |
| UseVectorsArgs | 2048 | 266.08 us | 1.379 us | 1.290 us | 0.14 | 0.00 |
| UseVectorsX2AArgs | 2048 | 266.29 us | 0.949 us | 0.887 us | 0.14 | 0.00 |
| UseVectorsX2BArgs | 2048 | 297.26 us | 1.482 us | 1.237 us | 0.16 | 0.00 |
| ImageshopSse | 2048 | NA | NA | NA | ? | ? |
| | | | | | | |
| Scalar | 4096 | 8,042.44 us | 17.405 us | 16.281 us | 1.00 | 0.00 |
| UseVectors | 4096 | 2,307.59 us | 2.411 us | 1.882 us | 0.29 | 0.00 |
| UseVectorsArgs | 4096 | 2,309.09 us | 4.411 us | 3.910 us | 0.29 | 0.00 |
| UseVectorsX2AArgs | 4096 | 2,193.09 us | 7.278 us | 6.078 us | 0.27 | 0.00 |
| UseVectorsX2BArgs | 4096 | 2,478.22 us | 3.373 us | 2.816 us | 0.31 | 0.00 |
| ImageshopSse | 4096 | NA | NA | NA | ? | ? |
以1024時(shí)的測試結(jié)果為例,來觀察向量化算法比起標(biāo)量算法的性能提升。
- UseVectors:489.45/60.78 ≈ 8.05。
- UseVectorsArgs:489.45/60.20 ≈8.13。
- UseVectorsX2AArgs:489.45/61.02 ≈8.02。
- UseVectorsX2BArgs:489.45/73.73 ≈ 6.64。
由于 .NET 8.0也優(yōu)化了標(biāo)量算法,這導(dǎo)致上面的的性能提升倍數(shù)看起來比較低。若拿 .NET 7.0的測試結(jié)果,與 .NET 8.0的UseVectors進(jìn)行對比,就能看出差別了。
- Scalar:1,504.47/60.78 ≈ 24.75。即
.NET 8.0向量算法的性能,是.NET 7.0標(biāo)量算法的 24.75 倍。 - UseVectors:108.65/60.78 ≈1.79。
- UseVectorsArgs:81.78/60.20 ≈ 1.36。即
.NET 8.0向量算法的性能,是.NET 7.0向量算法的 1.36 倍。 - UseVectorsX2AArgs:69.35/61.02 ≈1.17。
- UseVectorsX2BArgs:80.66/73.73 ≈1.09。
此時(shí) UseVectors、UseVectorsArgs、UseVectorsX2AArgs 的成績相差不大,考慮到測試誤差,可以將它們并列第一。這是因?yàn)槭褂肁dvSimd的“多向量換位”指令之后,無論是“2向量換位”,還是“3向量換位”,都是僅需1條換位指令。
而 UseVectorsX2BArgs 的比 UseVectorsArgs 還慢了。看來對于 Arm架構(gòu),2次向量加載的開銷也是頗大的。
3.3 .NET Framework
同樣的源代碼可以在 .NET Framework 上運(yùn)行。基準(zhǔn)測試結(jié)果如下。
Vectors.Instance: VectorTraits256Base //
YShuffleX3Kernel_AcceleratedTypes: None
BenchmarkDotNet v0.14.0, Windows 11 (10.0.26100.3476)
AMD Ryzen 7 7840H w/ Radeon 780M Graphics, 1 CPU, 16 logical and 8 physical cores
[Host] : .NET Framework 4.8.1 (4.8.9290.0), X64 RyuJIT VectorSize=256
DefaultJob : .NET Framework 4.8.1 (4.8.9290.0), X64 RyuJIT VectorSize=256
| Method | Width | Mean | Error | StdDev | Ratio | RatioSD | Code Size |
|------------------ |------ |-----------:|----------:|----------:|------:|--------:|----------:|
| Scalar | 1024 | 1.033 ms | 0.0177 ms | 0.0165 ms | 1.00 | 0.02 | 2,717 B |
| UseVectors | 1024 | 6.161 ms | 0.0461 ms | 0.0409 ms | 5.96 | 0.10 | 4,883 B |
| UseVectorsArgs | 1024 | 6.089 ms | 0.1066 ms | 0.0998 ms | 5.89 | 0.13 | 4,928 B |
| UseVectorsX2AArgs | 1024 | 6.349 ms | 0.0531 ms | 0.0497 ms | 6.15 | 0.11 | 5,288 B |
| UseVectorsX2BArgs | 1024 | 6.512 ms | 0.1288 ms | 0.1205 ms | 6.30 | 0.15 | 4,794 B |
| | | | | | | | |
| Scalar | 2048 | 4.284 ms | 0.0539 ms | 0.0504 ms | 1.00 | 0.02 | 2,717 B |
| UseVectors | 2048 | 23.636 ms | 0.3372 ms | 0.3155 ms | 5.52 | 0.09 | 4,883 B |
| UseVectorsArgs | 2048 | 23.650 ms | 0.2341 ms | 0.2190 ms | 5.52 | 0.08 | 4,928 B |
| UseVectorsX2AArgs | 2048 | 25.062 ms | 0.3512 ms | 0.3113 ms | 5.85 | 0.10 | 5,288 B |
| UseVectorsX2BArgs | 2048 | 25.362 ms | 0.3052 ms | 0.2706 ms | 5.92 | 0.09 | 4,794 B |
| | | | | | | | |
| Scalar | 4096 | 16.291 ms | 0.2417 ms | 0.2261 ms | 1.00 | 0.02 | 2,717 B |
| UseVectors | 4096 | 94.486 ms | 1.5107 ms | 1.4131 ms | 5.80 | 0.11 | 4,883 B |
| UseVectorsArgs | 4096 | 93.715 ms | 0.8965 ms | 0.7486 ms | 5.75 | 0.09 | 4,928 B |
| UseVectorsX2AArgs | 4096 | 99.979 ms | 1.9541 ms | 1.9192 ms | 6.14 | 0.14 | 5,288 B |
| UseVectorsX2BArgs | 4096 | 101.354 ms | 1.6959 ms | 1.5864 ms | 6.22 | 0.13 | 4,794 B |
UseVectors 等向量函數(shù)反而更慢了,這是因?yàn)?YShuffleX3Kernel 沒有硬件加速。可以看到 “YShuffleX3Kernel_AcceleratedTypes”為“None”。
在實(shí)際使用時(shí),應(yīng)先檢查YShuffleX3Kernel_AcceleratedTypes屬性。當(dāng)發(fā)現(xiàn)它沒有硬件加速時(shí),宜切換為標(biāo)量算法。
四、結(jié)語
對于 Arm架構(gòu),很明顯思路A(UseVectorsX2AArgs)的性能最好。
而對于 X86架構(gòu),情況有一些復(fù)雜——
- 不支持Avx512時(shí):思路B(UseVectorsX2BArgs)最快。思路A(UseVectorsX2AArgs)與ImageshopSse并列第2,都比 UseVectorsArgs 快。
- 支持Avx512時(shí):思路A(UseVectorsX2AArgs)、思路B(UseVectorsX2BArgs)與UseVectorsArgs等辦法,均并列第1。
考慮到跨平臺時(shí)代碼的維護(hù)成本,選擇維護(hù)唯一一套代碼比較好。此時(shí)推薦 思路A(UseVectorsX2AArgs)。因?yàn)樗诮^大多數(shù)時(shí)候(Arm、X86支持Avx512時(shí))都是排第1,僅在不支持Avx512時(shí)才排到第2。而且它的代碼相對更簡單。
若追求極致性能,可考慮維護(hù)2套代碼(思路A、思路B),隨后根據(jù)是否支持Avx512來切換。
附錄
- 完整源代碼: https://github.com/zyl910/VectorTraits.Sample.Benchmarks/blob/main/VectorTraits.Sample.Benchmarks.Inc/Image/ImageFlipXOn24bitBenchmark.cs
- YShuffleX2Kernel 的文檔: https://zyl910.github.io/VectorTraits_doc/api/Zyl.VectorTraits.Vectors.YShuffleX2Kernel.html
- VectorTraits 的NuGet包: https://www.nuget.org/packages/VectorTraits
- VectorTraits 的在線文檔: https://zyl910.github.io/VectorTraits_doc/
- VectorTraits 源代碼: https://github.com/zyl910/VectorTraits
- [C#] 對24位圖像進(jìn)行水平翻轉(zhuǎn)(FlipX)的跨平臺SIMD硬件加速向量算法(使用YShuffleX3Kernel)

浙公網(wǎng)安備 33010602011771號