首页 > 编程语言 >AVX图像算法优化系列二: 使用AVX2指令集加速查表算法。

AVX图像算法优化系列二: 使用AVX2指令集加速查表算法。

时间:2022-10-16 22:32:44浏览次数:56  
标签:mm256 速查表 int LinePS unsigned char 算法 AVX2 LinePD

查表算法,无疑也是一种非常常用、有效而且快捷的算法,我们在很多算法的加速过程中都能看到他的影子,在图像处理中,尤其常用,但是查表在SSE指令的基础上很难得到加速,但是,随着AVX2提供了新的gather指令,使得查表过程的加速成为可能。

  查表算法,无疑也是一种非常常用、有效而且快捷的算法,我们在很多算法的加速过程中都能看到他的影子,在图像处理中,尤其常用,比如我们常见的各种基于直方图的增强,可以说,在photoshop中的调整菜单里80%的算法都是用的查表,因为他最终就是用的曲线调整。

  普通的查表就是提前建立一个表,然后在执行过程中算法计算出一个索引值,从表中查询索引对应的表值,并赋值给目标地址,比如我们常用的曲线算法如下所示:

int IM_Curve_PureC(unsigned char *Src, unsigned char *Dest, int Width, int Height, int Stride, unsigned char *TableB, unsigned char *TableG, unsigned char *TableR)
{
int Channel = Stride / Width;if (Channel == 1)
{
for (int Y = 0; Y < Height; Y++)
{
unsigned char *LinePS = Src + Y * Stride;
unsigned char *LinePD = Dest + Y * Stride;
for (int X = 0; X < Width; X++)
{
LinePD[X] = TableB[LinePS[X]];
}
}
}
else if (Channel == 3)
{
for (int Y = 0; Y < Height; Y++)
{
unsigned char *LinePS = Src + Y * Stride;
unsigned char *LinePD = Dest + Y * Stride;
for (int X = 0; X < Width; X++)
{
LinePD[0] = TableB[LinePS[0]];
LinePD[1] = TableG[LinePS[1]];
LinePD[2] = TableR[LinePS[2]];
LinePS += 3;
LinePD += 3;
}
}
}return IM_STATUS_OK;
}

  通常我们认为这样的算法是很高效的,当然,我们其实还可以做一定的优化,比如使用下面的四路并行:

int IM_Curve_PureC(unsigned char *Src, unsigned char *Dest, int Width, int Height, int Stride, unsigned char *TableB, unsigned char *TableG, unsigned char *TableR)
{
int Channel = Stride / Width;
if ((Channel != 1) && (Channel != 3)) return IM_STATUS_INVALIDPARAMETER;
if ((Src == NULL) || (Dest == NULL)) return IM_STATUS_NULLREFRENCE;
if ((Width <= 0) || (Height <= 0)) return IM_STATUS_INVALIDPARAMETER;
int BlockSize = 4, Block = Width / BlockSize;
if (Channel == 1)
{
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)
{
LinePD[X + 0] = TableB[LinePS[X + 0]];
LinePD[X + 1] = TableB[LinePS[X + 1]];
LinePD[X + 2] = TableB[LinePS[X + 2]];
LinePD[X + 3] = TableB[LinePS[X + 3]];
}
for (int X = Block * BlockSize; X < Width; X++)
{
LinePD[X] = TableB[LinePS[X]];
}
}
}
else if (Channel == 3)
{
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)
{
LinePD[0] = TableB[LinePS[0]];
LinePD[1] = TableG[LinePS[1]];
LinePD[2] = TableR[LinePS[2]];
LinePD[3] = TableB[LinePS[3]];
LinePD[4] = TableG[LinePS[4]];
LinePD[5] = TableR[LinePS[5]];
LinePD[6] = TableB[LinePS[6]];
LinePD[7] = TableG[LinePS[7]];
LinePD[8] = TableR[LinePS[8]];
LinePD[9] = TableB[LinePS[9]];
LinePD[10] = TableG[LinePS[10]];
LinePD[11] = TableR[LinePS[11]];
LinePS += 12;
LinePD += 12;
}
for (int X = Block * BlockSize; X < Width; X++)
{
LinePD[0] = TableB[LinePS[0]];
LinePD[1] = TableG[LinePS[1]];
LinePD[2] = TableR[LinePS[2]];
LinePS += 3;
LinePD += 3;
}
}
}
return IM_STATUS_OK;
}

  这样效率能进一步的提高。

​【算法随记七】巧用SIMD指令实现急速的字节流按位反转算法。​​ 一文的描述。 

  在我们再次接触AVX指令集,正如上一篇关于AVX指令的文章所述,他增加了非常具有特色的gather系列指令,具体有哪些如下图所示:

      

AVX图像算法优化系列二: 使用AVX2指令集加速查表算法。_AVX2

  有一大堆啊,其实看明白了,就只有2大类,每大类里有2个小系列,每个系列里又有4中数据类型,

  两大类为 :针对128位的类型的gather和针对256位的gather。

  两个系列为:带mask和不带mask系列。

  4中数据类型为: int32、int64、float、double。

_mm_i32gather_epi32 、_mm256_i32gather_epi32、_mm_i32gather_ps、_mm256_i32gather_ps,我们以_mm256_i32gather_epi32为例。

  注意,这里所以下,不要以为_mm_i32gather_ps这样的intrinsics指令以_mm开头,他就是属于SSE的指令,实际行他并不是,他是属于AVX2的,只是高级别的指令集对老指令的有效补充。

  _mm256_i32gather_epi32的相关说明如下:    

                    

AVX图像算法优化系列二: 使用AVX2指令集加速查表算法。_加速_02

   其作用,翻译过来就是从固定的基地址base_addr开始, 燃用偏移量由 vindex提供,注意这里的vindex是一个__m256i数据类型,里面的数据要把它看成8个int32类型,即保存了8个数据的地址偏移量,最后一个scale表示地址偏移量的放大系数,容许的值只有1、2、4、8,代表了字节,双字节,四字节和把字节的意思,通常_mm256_i32gather_epi32一般都是使用的4这个数据。

  那么注意看这些gather函数,最下的操作单位都是int32,因此,如果我们的查找表是byte或者short类型,这个就有点困难了,正如我们上面的Cure函数一样,是无法直接使用这个函数的。

  那么我我们来看看一个正常的int型表,使用两者之间大概有什么区别呢,以及是如何使用该函数的,为了测试公平,我把正常的查找表也做了展开。

int main()
{
const int Length = 4000 * 4000;
int *Src = (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));
// int32 A0 A1 A2 A3 A4 A5 A6 A7
__m256i ValueL = _mm256_i32gather_epi32(Table, _mm256_cvtepu8_epi32(SrcV), 4);
// int32 B0 B1 B2 B3 B4 B5 B6 B7
__m256i ValueH = _mm256_i32gather_epi32(Table, _mm256_cvtepu8_epi32(_mm_srli_si128(SrcV, 8)), 4);
// short A0 A1 A2 A3 B0 B1 B2 B3 A4 A5 A6 A7 B4 B5 B6 B7
__m256i Value = _mm256_packs_epi32(ValueL, ValueH);
// byte A0 A1 A2 A3 B0 B1 B2 B3 0 0 0 0 0 0 0 0 A4 A5 A6 A7 B4 B5 B6 B7 0 0 0 0 0 0 0 0
Value = _mm256_packus_epi16(Value, _mm256_setzero_si256());
// byte A0 A1 A2 A3 A4 A5 A6 A7 B0 B1 B2 B3 B4 B5 B6 B7 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Value = _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才能把那些数据正确的调整为需要的格式。

  对于彩色的图像,就要稍微复杂一些了,因为涉及到RGB格式的排布,同时考虑一些对齐问题,最友好的方式就是一次性处理8个像素,24个字节,这一部分留给有兴趣的读者自行研究。 

  在我本机的CPU中测试呢,灰度版本的查找表大概有20%的提速,彩色版本的要稍微多一些,大概有30%左右。 

_mm_extract_epi32(这个是内在的SSE指令,不是用其他伪指令拼合的)提取出每个坐标值,然后在使用_mm_set相关的函数把查找表的返回值拼接成一个行的SSE变量,以便进行后续的计算,比如下面的代码:

      

AVX图像算法优化系列二: 使用AVX2指令集加速查表算法。_加速_03

  这个时候使用AVX2的这个指令就方便了,如下所示:

AVX图像算法优化系列二: 使用AVX2指令集加速查表算法。_加速_04

  注意到上面的Texture其实是个字节类型的数组,也就是一副图像,对应的C代码如下所示:

int SampleXF = IM_ClampI(ClipXF >> 16, 0, Width - 1);            //    试着拆分VX和VY的符号情况分开写,减少ClampI的次数,结果似乎区别不是特别大,因此优化意义不大
int SampleXB = IM_ClampI(ClipXB >> 16, 0, Width - 1);
int SampleYF = IM_ClampI(ClipYF >> 16, 0, Height - 1);
int SampleYB = IM_ClampI(ClipYB >> 16, 0, Height - 1);
unsigned char *SampleF = Texture + (SampleYF * Stride + SampleXF);
unsigned char *SampleB = Texture + (SampleYB * Stride + SampleXB);
Sum += SampleF[0] + SampleB[0];

  可见这里实际上是对字节类型进行查表,所以这里最后的那个scale参数我们取的是1,即中间的偏移是以字节为单位的,但是这里其实隐含着一个问题,即如果我们取样的是图片最右下角的那个位置的像素,因为要从那个位置开始读取四个字节的内存,除非图像原始格式是BGRA的,否则,必然会读取到超出图像内存外的内存数据,这个在普通的C语言中,已改会弹出一个系统错误框,蹦的一下说访问非法内存,但是我看用这个指令似乎目前还没有遇到这个错误,哪怕认为的输入一个会犯错误的坐标。

  如果是这样的话,得到的一个好处就是对于那些图像扭曲滤镜、缩放图像中哪些重新计算坐标的函数来说,不用临时构建一副同样数据的int类型图了,而可以直接放心的使用这个函数了。

  最后说明一点,经过在其他一些机器上测试,似乎有些初代即使支持AVX2的CPU,使用这些函数后相应的算法的执行速度反而有下降的可能性,不知道为什么。 

  在我提供的SIMD指令优化的DEMO中,在 Adjust-->Exposure菜单下可以看到使用C语言和使用AVX进行查表优化的功能,有兴趣的作者可以自行比较下。

        

AVX图像算法优化系列二: 使用AVX2指令集加速查表算法。_AVX2_05

   很明显,在这里SSE优化选项是无法使用的。

    关注公众号:

                        

AVX图像算法优化系列二: 使用AVX2指令集加速查表算法。_查表_06



标签:mm256,速查表,int,LinePS,unsigned,char,算法,AVX2,LinePD
From: https://blog.51cto.com/u_15341345/5760750

相关文章

  • AVX图像算法优化系列一: 初步接触AVX。
    弄了SSE指令集,必然会在不同的场合不同的人群中了解到还有更为高级的AVX指令集的存在,早些年也确实有偶尔写点AVX的函数,但是一直没有深入的去了解,这个十一,......
  • 【算法】BP神经网络结构详解!
    BP神经网络结构与原理参数表示$n_l$:表示网络层数,此处为4$L_l$​:表示第l层,L1是输入层,$L_n$​是输出层,其他为隐含层$w^{(l)}_{ij}$​:表示第l+1层第i个单......
  • 双指针算法学习总结以及实现案例
    双指针算法简介:指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个相同方向(快慢指针)或者相反方向(左右指针)的指针进行扫描,从而达到相应的目的。换言之,双......
  • 04 队列 | 数据结构与算法
    1.队列1.队列的概念队列:操作受限的线性表,只允许在一端进行元素的插入,另一端进行元素的删除空队列:不含有任何元素的队列队头和队尾:进行删除的一端叫队头front,进行插......
  • 算法之二分法
    目录算法简介之二分法(需要写的出来)一、简介二、什么是算法三、二分法算法简介之二分法(需要写的出来)一、简介关于算法我们只需要稍微了解一下就可以了,对于算法,刚入行的......
  • 递归与分治法实现快速排序算法
    ​本人CSDN链接:http://t.csdn.cn/Wt0Nm提示:首先了解并明白递归与分治法的快速排序文章目录 前言递归与分治法实现快速排序算法,输入一串以英文字符逗号隔开的数......
  • 【算法训练营day4】LeetCode24. 两两交换链表中的结点 LeetCode19. 删除链表的倒数第N
    【算法训练营day4】LeetCode24.两两交换链表中的结点LeetCode19.删除链表的倒数第N个结点LeetCode面试题02.07.链表相交LeetCode142.环形链表IILeetCode24.两两......
  • 【TSP问题】基于改进蜜蜂算法解决旅行商问题(Matlab代码实现)
    目录​​1蜜蜂优化算法​​​​1.1蜜蜂觅食机制​​​​1.2蜜蜂算法​​​​1.3流程​​​​2TSP问题 ​​​​3运行结果 ​​​​4结论​​​​5 Matlab代码实现......
  • 第二季:6.GC垃圾回收算法和垃圾收集器的关系?分别是什么请你谈谈【Java面试题】
    第二季:6.GC垃圾回收算法和垃圾收集器的关系?分别是什么请你谈谈【Java面试题】​​前言​​​​推荐​​​​6.GC垃圾回收算法和垃圾收集器的关系?分别是什么请你谈谈​​​......
  • 第三课 算法
    1.什么事算法是解决一个问题采取的方法和步骤结论:同一个问题可能有多种不同的算法,不同的算法的工作量不一定相同2.算法的特性1.有穷性2.确定性3.有零个或多个输入4......