AVX圖像算法優化系列二: 使用AVX2指令集加速查表算法。( 二 )


那么注意看這些gather函數 , 最下的操作單位都是int32,因此 , 如果我們的查找表是byte或者short類型,這個就有點困難了 , 正如我們上面的Cure函數一樣,是無法直接使用這個函數的 。
那么我我們來看看一個正常的int型表,使用兩者之間大概有什么區別呢 , 以及是如何使用該函數的,為了測試公平,我把正常的查找表也做了展開 。
int main(){const int Length = 4000 * 4000;int *Src = https://www.huyubaike.com/biancheng/(int *)calloc(Length, sizeof(int));int *Dest = (int *)calloc(Length, sizeof(int));int *Table = (int *)calloc(65536, sizeof(int));for (int Y = 0; Y < Length; Y++)Src[Y] = rand();//產生的隨機數在0-65535之間,正好符號前面表的大小for (int Y = 0; Y < 65536; Y++){Table[Y] = 65535 - Y;//隨意的分配一些數據}LARGE_INTEGER nFreq;//LARGE_INTEGER在64位系統中是LONGLONG,在32位系統中是高低兩個32位的LONG,在windows.h中通過預編譯宏作定義LARGE_INTEGER nBeginTime;//記錄開始時的計數器的值LARGE_INTEGER nEndTime;//記錄停止時的計數器的值double time;QueryPerformanceFrequency(&nFreq);//獲取系統時鐘頻率QueryPerformanceCounter(&nBeginTime);//獲取開始時刻計數值for (int Y = 0; Y < Length; Y += 4){Dest[Y + 0] = Table[Src[Y + 0]];Dest[Y + 1] = Table[Src[Y + 1]];Dest[Y + 2] = Table[Src[Y + 2]];Dest[Y + 3] = Table[Src[Y + 3]];}QueryPerformanceCounter(&nEndTime);//獲取停止時刻計數值time = (double)(nEndTime.QuadPart - nBeginTime.QuadPart) * 1000 / (double)nFreq.QuadPart;//(開始-停止)/頻率即為秒數,精確到小數點后6位printf("%f\n", time);QueryPerformanceCounter(&nBeginTime);//獲取開始時刻計數值for (int Y = 0; Y < Length; Y += 16){__m256i Index0 = _mm256_loadu_si256((__m256i *)(Src + Y));__m256i Index1 = _mm256_loadu_si256((__m256i *)(Src + Y + 8));__m256i Value0 = _mm256_i32gather_epi32(Table, Index0, 4);__m256i Value1 = _mm256_i32gather_epi32(Table, Index1, 4);_mm256_storeu_si256((__m256i *)(Dest + Y), Value0);_mm256_storeu_si256((__m256i *)(Dest + Y + 8), Value1);}QueryPerformanceCounter(&nEndTime);//獲取停止時刻計數值time = (double)(nEndTime.QuadPart - nBeginTime.QuadPart) * 1000 / (double)nFreq.QuadPart;//(開始-停止)/頻率即為秒數,精確到小數點后6位printf("%f\n", time);free(Src);free(Dest);free(Table);getchar();return 0;}直接使用這句即可完成查表工作:__m256i Value0 = _mm256_i32gather_epi32(Table, Index0, 4);
這是一個比較簡單的應用場景,在我本機的測試中,普通C語言的耗時大概是27ms , AVX版本的算法那耗時大概是17ms , 速度有1/3的提升 ??紤]到加載內存和保存數據在本代碼中占用的比重明顯較大,因此,提速還是相當明顯的 。
我們回到剛才的關于Curve函數的應用,因為gather相關指令最小的收集粒度都是32位,因此,對于字節版本的表是無論為力的 , 但是為了能借用這個函數實現查表 , 我們可以稍微對輸入的參數做些手續,再次構造一個int類型的表格,即使用如下代碼(弧度版本,Channel == 1):
int Table[256];for (int Y = 0; Y < 256; Y++){Table[Y] = TableB[Y];}這樣這個表就可以用了,對于24位我們也可以用類似的方式構架一個256*3個int元素的表 。
但是我們又面臨著另外一個問題,即_mm256_i32gather_epi32這個返回的是8個int32類型的整形數,而我們需要的返回值確實字節數 , 所以這里就又涉及到8個int32數據轉換為8個字節數并保存的問題,當然為了更為高效的利用指令集,我們這里考慮同時把2個__m256i類型里的16個int32數據同時轉換為16個字節數,這個可以用如下的代碼高效的實現:
for (int Y = 0; Y < Height; Y++){unsigned char *LinePS = Src + Y * Stride;unsigned char *LinePD = Dest + Y * Stride;for (int X = 0; X < Block * BlockSize; X += BlockSize){__m128i SrcV = _mm_loadu_si128((__m128i *)(LinePS + X));//int32A0A1A2A3A4A5A6A7__m256i ValueL = _mm256_i32gather_epi32(Table, _mm256_cvtepu8_epi32(SrcV), 4);//int32B0B1B2B3B4B5B6B7__m256i ValueH = _mm256_i32gather_epi32(Table, _mm256_cvtepu8_epi32(_mm_srli_si128(SrcV, 8)), 4);//shortA0A1A2A3B0B1B2B3A4A5A6A7B4B5B6B7__m256i Value = https://www.huyubaike.com/biancheng/_mm256_packs_epi32(ValueL, ValueH);//byteA0A1A2A3B0B1B2B300000000A4A5A6A7B4B5B6B700000000Value = _mm256_packus_epi16(Value, _mm256_setzero_si256());//byteA0A1A2A3A4A5A6A7B0B1B2B3B4B5B6B70000000000000000Value = _mm256_permutevar8x32_epi32(Value, _mm256_setr_epi32(0, 4, 1, 5, 2, 3, 6, 7));_mm_storeu_si128((__m128i *)(LinePD + X), _mm256_castsi256_si128(Value));}for (int X = Block * BlockSize; X < Width; X++){LinePD[X] = TableB[LinePS[X]];}    上面的代碼里涉及到了沒有按常規方式出牌的_mm256_packs_epi32、_mm256_packus_epi16等等,最后我們也是需要借助于AVX2提供的_mm256_permutevar8x32_epi32才能把那些數據正確的調整為需要的格式 。

推薦閱讀