一个同事在github上淘到一个基于SIMD的RGB转Y(彩色转灰度或者转明度)的代码,我抽了点时间看了下,顺便学习了一些SIMD指令,这里把学习过程中的一些理解和认识共享给大家。

github上相关代码见链接:https://github.com/komrad36/RGB2Y,这哥们还有其他一些SIMD的代码,也是相当不错的可以借鉴的。

我们首先说说普通的RGB2Y的代码:

void RGB2Y(unsigned char *Src, unsigned char *Dest, int Width, int Height, int Stride)
{
const int B_WT = int(0.114 * + 0.5);
const int G_WT = int(0.587 * + 0.5);
const int R_WT = - B_WT - G_WT; // int(0.299 * 256 + 0.5); for (int Y = ; Y < Height; Y++)
{
unsigned char *LinePS = Src + Y * Stride;
unsigned char *LinePD = Dest + Y * Width;
for (int X = ; X < Width; X++, LinePS += )
{
LinePD[X] = (B_WT * LinePS[] + G_WT * LinePS[] + R_WT * LinePS[]) >> ;
}
}
}

  很简单,就是R/G/B分量分别乘以各自的系数得到亮度值,注意这个系数是归一化的,为了速度考虑,我们采用了定点化措施,但是注意R_WT最后的量化方法,为了保证系数定点化后的归一性,最佳方式就是用他们的理论之和减去其他的系数。

  上述代码的速度已经非常快了,在测试机上1920*1280的图像单次执行也只需要3.95ms左右,如果还需要优化,可以像下面这样模拟并行操作:

void RGB2Y(unsigned char *Src, unsigned char *Dest, int Width, int Height, int Stride)
{
const int B_WT = int(0.114 * + 0.5);
const int G_WT = int(0.587 * + 0.5);
const int R_WT = - B_WT - G_WT; // int(0.299 * 256 + 0.5); for (int Y = ; Y < Height; Y++)
{
unsigned char *LinePS = Src + Y * Stride;
unsigned char *LinePD = Dest + Y * Width;
int X = ;
for (; X < Width - ; X += , LinePS += )
{
LinePD[X + ] = (B_WT * LinePS[] + G_WT * LinePS[] + R_WT * LinePS[]) >> ;
LinePD[X + ] = (B_WT * LinePS[] + G_WT * LinePS[] + R_WT * LinePS[]) >> ;
LinePD[X + ] = (B_WT * LinePS[] + G_WT * LinePS[] + R_WT * LinePS[]) >> ;
LinePD[X + ] = (B_WT * LinePS[] + G_WT * LinePS[] + R_WT * LinePS[]) >> ;
}
for (; X < Width; X++, LinePS += )
{
LinePD[X] = (B_WT * LinePS[] + G_WT * LinePS[] + R_WT * LinePS[]) >> ;
}
}
}

  即采用4路并行,这样同样的图大概需要3.40ms,稍有提高。

基本上这样的速度能够满足所有场合的需求,但是也有一些极端的条件,比如一些用于高清视频的美容方面等,每个过程能提高1ms都是很有必要的,因此,我一直在关注这方面的优化算法,正好komrad36提供了这方面的SIMD代码。

  我们来分析分析他提供的代码先,为了篇幅,这里仅仅贴出核心的我稍作修改的SIMD指令(SSE):

     __m128i p1aL = _mm_mullo_epi16(_mm_cvtepu8_epi16(_mm_loadu_si128((__m128i *)(LinePS + ))), _mm_setr_epi16(B_WT, G_WT, R_WT, B_WT, G_WT, R_WT, B_WT, G_WT));
__m128i p2aL = _mm_mullo_epi16(_mm_cvtepu8_epi16(_mm_loadu_si128((__m128i *)(LinePS + ))), _mm_setr_epi16(G_WT, R_WT, B_WT, G_WT, R_WT, B_WT, G_WT, R_WT));
__m128i p3aL = _mm_mullo_epi16(_mm_cvtepu8_epi16(_mm_loadu_si128((__m128i *)(LinePS + ))), _mm_setr_epi16(R_WT, B_WT, G_WT, R_WT, B_WT, G_WT, R_WT, B_WT)); __m128i p1aH = _mm_mullo_epi16(_mm_cvtepu8_epi16(_mm_loadu_si128((__m128i *)(LinePS + ))), _mm_setr_epi16(R_WT, B_WT, G_WT, R_WT, B_WT, G_WT, R_WT, B_WT));
__m128i p2aH = _mm_mullo_epi16(_mm_cvtepu8_epi16(_mm_loadu_si128((__m128i *)(LinePS + ))), _mm_setr_epi16(B_WT, G_WT, R_WT, B_WT, G_WT, R_WT, B_WT, G_WT));
__m128i p3aH = _mm_mullo_epi16(_mm_cvtepu8_epi16(_mm_loadu_si128((__m128i *)(LinePS + ))), _mm_setr_epi16(G_WT, R_WT, B_WT, G_WT, R_WT, B_WT, G_WT, R_WT)); __m128i p1bL = _mm_mullo_epi16(_mm_cvtepu8_epi16(_mm_loadu_si128((__m128i *)(LinePS + ))), _mm_setr_epi16(B_WT, G_WT, R_WT, B_WT, G_WT, R_WT, B_WT, G_WT));
__m128i p2bL = _mm_mullo_epi16(_mm_cvtepu8_epi16(_mm_loadu_si128((__m128i *)(LinePS + ))), _mm_setr_epi16(G_WT, R_WT, B_WT, G_WT, R_WT, B_WT, G_WT, R_WT));
__m128i p3bL = _mm_mullo_epi16(_mm_cvtepu8_epi16(_mm_loadu_si128((__m128i *)(LinePS + ))), _mm_setr_epi16(R_WT, B_WT, G_WT, R_WT, B_WT, G_WT, R_WT, B_WT)); __m128i p1bH = _mm_mullo_epi16(_mm_cvtepu8_epi16(_mm_loadu_si128((__m128i *)(LinePS + ))), _mm_setr_epi16(R_WT, B_WT, G_WT, R_WT, B_WT, G_WT, R_WT, B_WT));
__m128i p2bH = _mm_mullo_epi16(_mm_cvtepu8_epi16(_mm_loadu_si128((__m128i *)(LinePS + ))), _mm_setr_epi16(B_WT, G_WT, R_WT, B_WT, G_WT, R_WT, B_WT, G_WT));
__m128i p3bH = _mm_mullo_epi16(_mm_cvtepu8_epi16(_mm_loadu_si128((__m128i *)(LinePS + ))), _mm_setr_epi16(G_WT, R_WT, B_WT, G_WT, R_WT, B_WT, G_WT, R_WT)); __m128i sumaL = _mm_add_epi16(p3aL, _mm_add_epi16(p1aL, p2aL));
__m128i sumaH = _mm_add_epi16(p3aH, _mm_add_epi16(p1aH, p2aH));
__m128i sumbL = _mm_add_epi16(p3bL, _mm_add_epi16(p1bL, p2bL));
__m128i sumbH = _mm_add_epi16(p3bH, _mm_add_epi16(p1bH, p2bH));
__m128i sclaL = _mm_srli_epi16(sumaL, );
__m128i sclaH = _mm_srli_epi16(sumaH, );
__m128i sclbL = _mm_srli_epi16(sumbL, );
__m128i sclbH = _mm_srli_epi16(sumbH, );
__m128i shftaL = _mm_shuffle_epi8(sclaL, _mm_setr_epi8(, , , -, -, -, -, -, -, -, -, -, -, -, -, -));
__m128i shftaH = _mm_shuffle_epi8(sclaH, _mm_setr_epi8(-, -, -, , , , -, -, -, -, -, -, -, -, -, -));
__m128i shftbL = _mm_shuffle_epi8(sclbL, _mm_setr_epi8(-, -, -, -, -, -, , , , -, -, -, -, -, -, -));
__m128i shftbH = _mm_shuffle_epi8(sclbH, _mm_setr_epi8(-, -, -, -, -, -, -, -, -, , , , -, -, -, -));
__m128i accumL = _mm_or_si128(shftaL, shftbL);
__m128i accumH = _mm_or_si128(shftaH, shftbH);
__m128i h3 = _mm_or_si128(accumL, accumH);
//__m128i h3 = _mm_blendv_epi8(accumL, accumH, _mm_setr_epi8(0, 0, 0, -1, -1, -1, 0, 0, 0, -1, -1, -1, 1, 1, 1, 1));
_mm_storeu_si128((__m128i *)(LinePD + X), h3);

  看上去篇幅好大,真的怀疑是否能比普通的C代码快,现给大家吃个定心丸吧,同样的机器和图片,以上述代码片段为核心的算法执行时间约为1.66ms, 比优化后的普通C代码还要快1倍多,好,先吃饭去了,早餐还没吃(12:06),咱们回来再继续分享。

好,吃了两个青菜,感觉不错,继续。

首先,代码一次性处理12个像素,我们用BGR序列表达出来如下:

B1  G1  R1  B2  G2  R2  B3  G3  R3  B4  G4  R4  B5  G5  R5  B6  G6  R6  B7  G7  R7  B8  G8  R8  B9  G9  R9  B10  G10  R10  B11  G11  R11  B12  G12  R12

  SSE指令一次性能处理16个字节型数据,8个short类型的,或者4个int类型数据,考虑到计算过程中有乘法,综合效率和结果的准确性,我们选用short类型的作为主要的计算对象(这也就是说上述的权重定点化最大的放大范围为什么是256了,两个byte类型相乘不会超出short能表达的范围)。

  那是如何利用SSE的批处理能力的呢,由于SSE的序列性,我们很难直接把图像数据相乘相加然后得到结果,上述代码的最大特点就是巧妙的从图像数据中构建了几个SSE的数据,然后在利用SSE指令进行一次性的处理,比如第一到第三行以及第17行就是实现了如下的功能:

(B1  G1  R1  B2  G2  R2  B3  G3)  x (B_WT  G_WT  R_WT  B_WT  G_WT  R_WT  B_WT  G_WT) +

  (G1  R1  B2  G2  R2  B3  G3  R3)  x (G_WT  R_WT  B_WT  G_WT  R_WT  B_WT  G_WT  R_WT) +

  (R1  B2  G2  R2  B3  G3  R3  B4)  x (R_WT  B_WT  G_WT  R_WT  B_WT  G_WT  R_WT  B_WT) =

(B1 x B_WT + G1 x G_WT + R1 x R_WT)    (G1 x G_WT + R1 x R_WT + B2 x B_WT)    (R1 x R_WT + B2 x B_WT + G2 x G_WT) +

(B2 x B_WT + G2 x G_WT + R2 x R_WT)    (G2 x G_WT + R2 x R_WT + B3 x B_WT)    (R2 x R_WT + B3 x B_WT + G3 x G_WT) +

 (B3 x B_WT + G3 x G_WT + R3 x R_WT)    (G3 x G_WT + R3 x R_WT + B4 x B_WT)

  注意到了上述式子中我用黑体字加粗的部分没有,这部分的结果就是我们需要获得的嘛,这是本SIMD代码的核心,好,现在一个函数一个函数的解释了吧。

  第一到第15行都是一样的过程,其核心就是把字节数据读入并和相应的权重相乘。_mm_loadu_si128就是把之后16个字节的数据读入到一个SSE寄存器中,注意由于任意位置的图像数据内存地址肯定不可能都满足SIMD16字节对齐的规定,因此这里不是用的_mm_load_si128指令。而_mm_cvtepu8_epi16指令则把这16个字节的低64位的8个字节数扩展为8个16位数据,这样做主要是为了上述的乘法进行准备的。那么这个其实也有其他的方式实现,比如使用_mm_unpacklo_epi8和 _mm_unpackhi_epi8配合_mm_setzero_si128也可以实现这样的效果,而且可以节省后面的某一句_mm_loadu_si128指令,不过实测速度无区别。

_mm_setr_epi16这个实际上就是用已知数的8个16位数据来构造一个SSE整型数,仔细观察,这个代码的12个_mm_setr_epi16函数实际只有3个是不一样的,我曾经是这个把他们定义为一个全局的变量,在循环体内部直接使用,结果速度无区别。后面反汇编看看这些语句都便以为类似于这样的汇编码:

5D48116C  movdqa      xmm7,xmmword ptr ds:[5D482110h]  

ptr ds:[5D482110h] 这是个内存地址,也就是说他并不会在循环里每次都构造这个数据,而是直接从某个内存里读,这也就和我们在外部定义是一个意思了。

当然这主要是两个原因造成的,第一,我们这里的数据都是常数,因此每次循环内部不会变,编译器也是能够认识到这一点的,第二,由于这个SSE的变量比较多,已经大大的超过了SIMD内部的寄存器数量,因此,他需要用内存来缓存这些数据。

_mm_mullo_epi16 指令就是两个16位的乘法,注意不是用的_mm_mulhi_epi16,因为两个16位数相乘,一般要用32位数才能完整的保存结果,而_mm_mullo_epi16 是提取这个32位的低16位,我们这里前面已经明确了成绩的结果是不会超出short类型的,因此,所以只取低16位就已经完全保留了所有的信息。

第17和20行直接就是把每个元素相乘后的结果在相加,21和24行明显就是归一化的过程,进行移位,明显移位后的8个16位数只有低8位具有有效数字,高八位必然为0。

第25行到第32行其实把结果重新拼接到一个完整的SSE变量的过程,我们以第25句为例, shftaL 变量就正好记录了我们上面举例的那个结果,我们看到黑体加粗的结果是我们需要的,并且他应该位于真正结果的前3个字节的位置,因此,我们用_mm_shuffle_epi8指令把他们提到前面去,注意这个指令的前三个数字是0, 6, 12。为-1的部分都会变为0.

h3变量原代码是用_mm_blendv_epi8指令实现的,我觉得考虑这里的特殊性,用_mm_or_si128实现也是无妨的。

_mm_storeu_si128把处理的结果写入到目标内存中,注意,这里会多写了4个字节的内存数据(128 - 12 * 8),但是我们后面又会把他们重新覆盖掉,但是有一点要注意,就是如果是最后一行数据,在某些情况下超出的这个几个字节就已经不属于你这个进程该管理的范围了, 这个时候就会出现OOM错误,因此一种简单的方式就是在宽度方向上的循环终止条件设置为: X < Width - 12;这样剩余的像素用普通的算法处理即可避免这种问题出现。

上述代码中,一条SSE指令能同时执行8个short类型的计算,那为什么最后的提速只有1倍多一点呢,这其实很好解释,我们看到前面的计算中,计算出的8个累加值里只有3个是有效的,而其他的结果对我们来说毫无意义,并且在计算完之后,还有其他的一些合并操作,因此,最终只能获得这样的收益。

从个人理解来看,他这里一次性只处理了12个像素,其考虑的主要因素是最后一个_mm_blendv_epi8指令的方便,实际上稍作修改同时处理15个像素是可以的(小于16的最大的3的倍数)。

代码中还有一些其他的技巧,有些数字可能读者自己去看看那个指令的意义后会更加清晰,这些不太是语言能够完美表达清楚的。当然,这段代码在书写艺术上还是有很大的改良空间的,有兴趣的读者可以自行研究下。

同时,komrad36还提供了基于AVX的指令算法,执行耗时大概是1.15ms,和普通的算法比提速比约为3:1,思路和SSE基本是一样的。

我把这些代码稍做整理提供给大家使用和测试,我也相信,上述指令肯定不是最佳的SIMD实现方式,比如改变指令顺序让指令级又可以并行等手段也许也是有效的提速方式之一。

最后一点就是我有个疑问,在我提供的代码执行后,如果先使用SSE测试,后使用AVX测试,SSE的速度和上述报告数据差不多,但是一旦点了AVX测试后,在点SSE测试,SSE的速度就骤然下降很多,甚至比普通C的都要慢,我水平很有限,实在是不知道这是什么道理,烦请有知道的告知。

源代码下载地址:http://files.cnblogs.com/files/Imageshop/FastRGB2Y.rar

本笔记创建于2016年1月8日即将离开南京之际,特此纪念。

SSE图像算法优化系列一:一段BGR2Y的SIMD代码解析。的更多相关文章

  1. SSE图像算法优化系列十:简单的一个肤色检测算法的SSE优化。

    在很多场合需要高效率的肤色检测代码,本人常用的一个C++版本的代码如下所示: void IM_GetRoughSkinRegion(unsigned char *Src, unsigned char ...

  2. SSE图像算法优化系列二十五:二值图像的Euclidean distance map(EDM)特征图计算及其优化。

    Euclidean distance map(EDM)这个概念可能听过的人也很少,其主要是用在二值图像中,作为一个很有效的中间处理手段存在.一般的处理都是将灰度图处理成二值图或者一个二值图处理成另外一 ...

  3. SSE图像算法优化系列二十三: 基于value-and-criterion structure 系列滤波器(如Kuwahara,MLV,MCV滤波器)的优化。

    基于value-and-criterion structure方式的实现的滤波器在原理上其实比较简单,感觉下面论文中得一段话已经描述的比较清晰了,直接贴英文吧,感觉翻译过来反而失去了原始的韵味了. T ...

  4. SSE图像算法优化系列二十二:优化龚元浩博士的曲率滤波算法,达到约1000 MPixels/Sec的单次迭代速度

      2015年龚博士的曲率滤波算法刚出来的时候,在图像处理界也曾引起不小的轰动,特别是其所说的算法的简洁性,以及算法的效果.执行效率等方面较其他算法均有一定的优势,我在该算法刚出来时也曾经有关注,不过 ...

  5. SSE图像算法优化系列二十:一种快速简单而又有效的低照度图像恢复算法。

    又有很久没有动笔了,主要是最近没研究什么东西,而且现在主流的趋势都是研究深度学习去了,但自己没这方面的需求,同时也就很少有动力再去看传统算法,今天一个人在家,还是抽空分享一个简单的算法吧. 前段日子在 ...

  6. SSE图像算法优化系列十二:多尺度的图像细节提升。

    无意中浏览一篇文章,中间提到了基于多尺度的图像的细节提升算法,尝试了一下,还是有一定的效果的,结合最近一直研究的SSE优化,把算法的步骤和优化过程分享给大家. 论文的全名是DARK IMAGE ENH ...

  7. SSE图像算法优化系列十三:超高速BoxBlur算法的实现和优化(Opencv的速度的五倍)

    在SSE图像算法优化系列五:超高速指数模糊算法的实现和优化(10000*10000在100ms左右实现) 一文中,我曾经说过优化后的ExpBlur比BoxBlur还要快,那个时候我比较的BoxBlur ...

  8. SSE图像算法优化系列十五:YUV/XYZ和RGB空间相互转化的极速实现(此后老板不用再担心算法转到其他空间通道的耗时了)。

    在颜色空间系列1: RGB和CIEXYZ颜色空间的转换及相关优化和颜色空间系列3: RGB和YUV颜色空间的转换及优化算法两篇文章中我们给出了两种不同的颜色空间的相互转换之间的快速算法的实现代码,但是 ...

  9. SSE图像算法优化系列十四:局部均方差及局部平方差算法的优化。

    关于局部均方差有着较为广泛的应用,在我博客的基于局部均方差相关信息的图像去噪及其在实时磨皮美容算法中的应用及使用局部标准差实现图像的局部对比度增强算法中都有谈及,即可以用于去噪也可以用来增强图像,但是 ...

随机推荐

  1. hdu6107 倍增法st表

    发现lca的倍增解法和st表差不多..原理都是一样的 /* 整篇文章分成两部分,中间没有图片的部分,中间有图片的部分 分别用ST表求f1,f2表示以第i个单词开始,连续1<<j行能写多少单 ...

  2. 2017-2018-2 20155309 南皓芯 Exp3 免杀原理与实践

    报告内容 2.1.基础问题回答 (1)杀软是如何检测出恶意代码的 ? 1:基于特征码 一段特征码就是一段或多段数据.(如果一个可执行文件(或其他运行的库.脚本等)包含这样的数据则被认为是恶意代码) 杀 ...

  3. bind函数详解(转)

    var name = "The Window"; var object = { name: "My Object", getNameFunc: function ...

  4. 【C++ Primer 第11章】2. 关联容器操作

    练习答案 一.访问元素 关联容器额外类型别名  key_type 此容器类型的关键字类型 mapped_type 每个关键字关联的类型,只 适用于map mapped_type 对于set,与key_ ...

  5. Codeforces 788C The Great Mixing

    The Great Mixing 化简一下公式后发现, 问题变成了, 取最少多少数能使其和为1, bitset优化一下背包就好啦. 题解中介绍了一种bfs的方法没, 感觉比较巧妙. #include& ...

  6. 练习|Django-单表

    结构目录 页面展示: 1创建Django,创建app01 在modules.py添加 class Book(models.Model): id=models.AutoField(primary_key ...

  7. muduo网络库架构总结

    目录 muduo网络库简介 muduo网络库模块组成 Recator反应器 EventLoop的两个组件 TimerQueue定时器 Eventfd Connector和Acceptor连接器和监听器 ...

  8. 【原创】ABP源码分析

    接口篇 IConventionalDependencyRegistra接口分析 待续.............. 模块篇 敬请期待...... 领域篇 敬请期待...... 消息篇 敬请期待..... ...

  9. 51Nod1577 异或凑数 线性基 构造

    国际惯例的题面:异或凑出一个数,显然是线性基了.显然我们能把区间[l,r]的数全都扔进一个线性基,然后试着插入w,如果能插入,则说明w不能被这些数线性表出,那么就要输出"NO"了. ...

  10. BZOJ.2834.回家的路(最短路Dijkstra 拆点)

    题目链接 对于相邻的.处在同在一行或一列的车站连边,然后用dis[x][0/1](或者拆点)分别表示之前是从横边还是竖边到x的,跑最短路. 我选择拆点.. //13028kb 604ms #inclu ...