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才能把那些数据正确的调整为需要的格式 。

推荐阅读