Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >[快速阅读六] 统计内存数据中二进制1的个数(SSE指令集优化版).

[快速阅读六] 统计内存数据中二进制1的个数(SSE指令集优化版).

作者头像
用户1138785
发布于 2024-05-31 04:36:35
发布于 2024-05-31 04:36:35
16000
代码可运行
举报
运行总次数:0
代码可运行

  关于这个问题,网络上讨论的很多,可以找到大量的资料,我觉得就就是下面这一篇讲的最好,也非常的全面:

统计无符号整数二进制中 1 的个数(Hamming Weight)

  在指令集不参与的情况下,分治法速度最快,MIT HAKMEM 169 算法因为最后有一个mod取余操作,速度要稍微慢一点,256元素的查表算法速度要次之,当然,其实要建议那个256元素的表不要使用int类型,而是使用unsigned char类型的,尽量减少表的内存占用量,也意味着cache miss小一些。 16位的查表算法速度反而慢了不少,主要是因为他用while,即使我们把他展开,也需要8次数据组合,还是比16位的慢。其他的就不要说了,都比较慢。

  在SSE4指令集能得到CPU的支持时,可以有一个直接的指令_mm_popcnt_u32可以使用,这个就可以加速很多了,一个常用的过程如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    Amount = 0;
    for (int Y = 0; Y < Length; Y++)
    {
        Amount += _mm_popcnt_u32(Array[Y]);
    }

  一千万的随机数据,用这个指令大概只要3.2毫秒多就可以处理完成,如果稍微改下代码,让他能并行化一点,如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    Amount = 0;
    for (int Y = 0; Y < Length; Y+=4)
    {
        int T0 = _mm_popcnt_u32(Array[Y + 0]);
        int T1 = _mm_popcnt_u32(Array[Y + 1]);
        int T2 = _mm_popcnt_u32(Array[Y + 2]);
        int T3 = _mm_popcnt_u32(Array[Y + 3]);
        Amount += T0 + T1 + T2 + T3;
    }

  还可以提高到大约2.7ms就可以处理完。

  其实,现在在运行的新的CPU基本上没有那个不支持SSE4的了,但是也不排除还有一些老爷机。因为SSE4最早是2008年发布的,如果CPU不支持SSE4,但是支持SSE3(2004年发布的),那是否有合适的指令集能加速这个过程呢,实际上也是有的。

  我们这里喵上了统计无符号整数二进制中 1 的个数(Hamming Weight)一文中的16元素查表算法,原文中的代码为:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  Amount = 0;
    for (int Y = 0; Y < Length; Y++)
    {
        unsigned int Value = Array[Y];
        while (Value)
        {
            Amount += Table16[Value & 0xf];
            Value >>= 4;
        }
    }

  这个明显是不合适指令处理的,前面说了,这个可以展开,展开后形式如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  Amount = 0;
    for (int Y = 0; Y < Length; Y++)
    {
        unsigned int Value = Array[Y];
        Amount += Table16[Value & 0xf] + Table16[(Value >> 4) & 0xf] + Table16[(Value >> 8) & 0xf] + Table16[(Value >> 12) & 0xf] +
                Table16[(Value >> 16) & 0xf] + Table16[(Value >> 20) & 0xf] + Table16[(Value >> 24) & 0xf] + Table16[(Value >> 28) & 0xf];
    }

  仔细观察他的意思就是提取内存的4位,然后根据4位的值来查16个元素的表,我在之前的多个文章里都有提高,16个元素的表(表内的值不能超过255)是可以借用一个_mm_shuffle_epi8指令进行优化的,一次性得到16个值。

  _mm_shuffle_epi8是在SSE3就开始支持的,因此,这一改动可以将硬件的适应性提前4年。

  具体的来首,就是我们加载16个字节数据,然后和0xF进行and操作,得到每个字节的低4位,然后进行shuffle,得到每个字节低4位的二进制中1的个数,然后在把原始字节数右移4位,再和0xF进行and操作,得到每个字节的高4位,然后进行shuffle,两次shuffle的结果相加,就得到了这16个字节数据的二进制中1的个数。 具体代码如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    __m128i Table = _mm_loadu_si128((__m128i*)Table16);
    __m128i Mask = _mm_set1_epi8(0xf);
    __m128i UsedV = _mm_setzero_si128();
    for (int Y = 0; Y < Length; Y += 4)
    {
        __m128i Src = _mm_loadu_si128((__m128i*)(Array + Y));
        __m128i SrcL = _mm_and_si128(Src, Mask);
        __m128i SrcH = _mm_and_si128(_mm_srli_epi16(Src, 4), Mask);
        __m128i ValidL = _mm_shuffle_epi8(Table, SrcL);
        __m128i ValidH = _mm_shuffle_epi8(Table, SrcH);
        UsedV = _mm_add_epi32(UsedV, _mm_add_epi32(_mm_sad_epu8(ValidL, _mm_setzero_si128()), _mm_sad_epu8(ValidH, _mm_setzero_si128())));
    }
    //    提取出前面的使用SSE指令计算出的总的有效点数
    Amount = _mm_cvtsi128_si32(_mm_add_epi32(UsedV, _mm_unpackhi_epi64(UsedV, UsedV)));

  注意到这里的函数除了_mm_shuffle_epi8,其他的都是SSE2就已经能支持的了,其中_mm_sad_epu8可以快速的把16字节结果相加。

   使用这个代码,测试上述1千万数据,大概只需要2.1ms就能处理完,比优化后的_mm_popcnt_u32还要快。

   实际上,我还遇到一种情况,一个AMD的早期CPU,用CPUID看他支持的指令集,他是支持SSE4.2的,也支持SSE3,但是执行_mm_shuffle_epi8确提示不识别的指令,也是很奇怪,或者说如果遇到一个机器不支持SSE3,只支持SSE2,那是否还能用指令集优化这个算法呢(SSE2是2001年发布的)。

  其实也是可以的,我们观察上面的不使用指令集的版本的,特别是分冶法的代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  Amount = 0;
    for (int Y = 0; Y < Length; Y++)
    {
        unsigned int Value = Array[Y];
        Value = (Value & 0x55555555) + ((Value >> 1) & 0x55555555);
        Value = (Value & 0x33333333) + ((Value >> 2) & 0x33333333);
        Value = (Value & 0x0f0f0f0f) + ((Value >> 4) & 0x0f0f0f0f);
        Value = (Value & 0x00ff00ff) + ((Value >> 8) & 0x00ff00ff);
        Value = (Value & 0x0000ffff) + ((Value >> 16) & 0x0000ffff);
        Amount += Value;
    }

  这里就是简单的一些位运算和移位,他们对应的指令集在SSE2里都能得到支持的,而且这个改为指令集也是水到渠成的事情:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  UsedV = _mm_setzero_si128();
    for (int Y = 0; Y < Length; Y += 4)
    {
        __m128i Value = _mm_loadu_si128((__m128i*)(Array + Y));
        Value = _mm_add_epi32(_mm_and_si128(Value, _mm_set1_epi32(0x55555555)), _mm_and_si128(_mm_srli_epi32(Value, 1), _mm_set1_epi32(0x55555555)));
        Value = _mm_add_epi32(_mm_and_si128(Value, _mm_set1_epi32(0x33333333)), _mm_and_si128(_mm_srli_epi32(Value, 2), _mm_set1_epi32(0x33333333)));
        Value = _mm_add_epi32(_mm_and_si128(Value, _mm_set1_epi32(0x0f0f0f0f)), _mm_and_si128(_mm_srli_epi32(Value, 4), _mm_set1_epi32(0x0f0f0f0f)));
        Value = _mm_add_epi32(_mm_and_si128(Value, _mm_set1_epi32(0x00ff00ff)), _mm_and_si128(_mm_srli_epi32(Value, 8), _mm_set1_epi32(0x00ff00ff)));
        Value = _mm_add_epi32(_mm_and_si128(Value, _mm_set1_epi32(0x0000ffff)), _mm_and_si128(_mm_srli_epi32(Value, 16), _mm_set1_epi32(0x0000ffff)));
        UsedV = _mm_add_epi32(UsedV, Value);
    }
    //    提取出前面的使用SSE指令计算出的总的有效点数
    //Amount = _mm_cvtsi128_si32(_mm_add_epi32(UsedV, _mm_unpackhi_epi64(UsedV, UsedV)));
    Amount = UsedV.m128i_u32[0] + UsedV.m128i_u32[1] + UsedV.m128i_u32[2] + UsedV.m128i_u32[3];

  这里唯一要注意的就是最后从UsedV 变量得到Amount的过程不能用之前shuffle那一套代码了,因为这里的UsedV的最高位里含有了符号位,所以要换成下面哪一种搞法。

  我们注意到,编译运行这个代码后,我们得到的耗时大概是5.2ms,但是同样的数据,前面的分冶法对应的C代码也差不多是5.5ms左右,速度感觉毫无提高,这是怎么回事呢,我们尝试反汇编C的代码,结果发现如下片段:

  注意到pand psrld paddd等指令没有,那些就对应了_mm_and_si128 _mm_srli_epi32 _mm_add_epi32等等,原来是编译器已经帮我们向量化了,而且即使在设置里设置 无增强指令 (/arch:IA32) 选项,编译器也会进行向量化(VS2019)。

  所以我暂时还得不到这个和纯C比的真正的加速比。

  但是,在编译器没有这个向量化能力时,直接手工嵌入SSE2的指令,还是能有明显的加速作用的,不过也可以看到,SSE2的优化速度还是比SSE3的shuffle版本慢一倍的,而sse3的shuffle确可以比SSE4的popcount快30%。

  以前我一直在想,这个算法有什么实际的应用呢,有什么地方我会用到统计二进制中1的个数呢,最近确实遇到过了一次。

  具体的应用是,我有一堆数据,我要统计出数据中符合某个条件(有可能是多个条件)的目标有多少个,这个时候我们多次应用了_mm_cmpxx_ps等函数组合,最后得到一个Mask,这个时候我们使用_mm_movemask_ps来得到一个标记,我们看看_mm_movemask_ps 这个函数的具体意思:

  他返回的是一个0到15之间的整形数据,很明显我们可以把他保存到一个unsigned char类型的变量里,这样,在计算完一堆数据后,我们就得到了一个mask数组,这个时候我们统计下数组里有多少个二进制1就可以得到符合条件的目标数量了。 当然,这里和前面的还不太一样,这个usnigned char变量的高4位一直为0,还可以不用处理的,还能进一步加速。

  当然,如果系统支持AVX2,那还可以进一步做速度优化。这个就不多言了。

  最后,列一下各个算法的耗时比较数据吧:

  相关测试代码地址: 数据流二进制中1的个数统计

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-05-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
【算法随记七】巧用SIMD指令实现急速的字节流按位反转算法。
  字节按位反转算法,在有些算法加密或者一些特殊的场合有着较为重要的应用,其速度也是一个非常关键的应用,比如一个byte变量a = 3,其二进制表示为00000011,进行按位反转后的结果即为11000000,即十进制的192。还有一种常用的应用是int型变量按位反转,其基本的原理和字节反转类似,本文仅以字节反转为例来比较这个算法的实现。
用户1138785
2020/01/02
1.3K0
H264/H265 NALU 起始码搜索性能优化(1)
在ffmpeg中,在进行h264 rbsp流demux的时候,需要进行starting code的搜索,其采用的方法比较简单,就是不断比较字节流中连续的三个字节,是不是 0x00, 0x00, 0x01,ffmpeg采用如下代码用来找到各个NALU的分界点:
码农心语
2024/04/09
1260
H264/H265 NALU 起始码搜索性能优化(1)
【AI PC端算法优化】三,深入优化RGB转灰度图算法
前几天发了一篇一步步优化RGB转灰度图算法,但实验做的并不完善,在上次的基础上我又补充了一些优化技巧,相对于传统实现将RGB转灰度图算法可以加速到近5倍左右。所以,这篇文章再次将所有涉及到的优化方法进行汇总,SSE优化相关的原理上一节已经讲得很清楚了,这里就不会再展开了,感兴趣可以查看上篇文章。【AI PC端算法优化】一,一步步优化RGB转灰度图算法 这一节的速度测试环境为:
BBuf
2020/04/15
1.2K0
SSE图像算法优化系列一:一段BGR2Y的SIMD代码解析。
该文章是一篇关于Linux、Windows和macOS操作系统之间区别的文章。文章主要介绍了Linux、Windows和macOS这三种操作系统在桌面环境、图形界面、文件系统、系统管理、软件安装、系统性能、安全性、适用范围等方面的区别。文章还探讨了每种操作系统的优缺点,以及适用场景。最后,作者提供了一些建议,帮助读者选择适合自己的操作系统。
用户1138785
2018/01/03
1.3K0
SSE图像算法优化系列三十一:Base64编码和解码算法的指令集优化(C#自带函数的3到4倍速度)。
Base64是一种用64个Ascii字符来表示任意二进制数据的方法。主要用于将不可打印的字符转换成可打印字符,或者简单的说是将二进制数据编码成Ascii字符。Base64也是网络上最常用的传输8bit字节数据的编码方式之一。
用户1138785
2021/09/08
1.1K0
AVX图像算法优化系列二: 使用AVX2指令集加速查表算法。
  查表算法,无疑也是一种非常常用、有效而且快捷的算法,我们在很多算法的加速过程中都能看到他的影子,在图像处理中,尤其常用,比如我们常见的各种基于直方图的增强,可以说,在photoshop中的调整菜单里80%的算法都是用的查表,因为他最终就是用的曲线调整。
用户1138785
2022/10/28
1.6K0
AVX图像算法优化系列二: 使用AVX2指令集加速查表算法。
【工程应用五】 opencv中linemod模板匹配算法诸多疑惑和自我解读。
        研究这个前前后后也有快两三个月了,因为之前也一直在弄模板匹配方面的东西,所以偶尔还是有不少朋友咨询或者问你有没有研究过linemod这个算法啊,那个效率啥的还不错啊,有段时间一直不以为然,觉得我现在用的那个匹配因该很不错的,没必要深究了。后来呢,还是忍不住手痒,把论文打出来看了看,又找了点资料研究了下,结果没想到一弄又是两个月过去了,中间也折腾了很久,浪费了不少时间。总算还是有点收获,稍微整理下做个交流。
用户1138785
2022/05/09
1.6K0
【工程应用五】 opencv中linemod模板匹配算法诸多疑惑和自我解读。
【AI PC端算法优化】一,一步步优化RGB转灰度图算法
公众号输入 「高性能计算」 关键词获取刘文志大佬的《并行编程方法与优化实践》电子书以及我整理的SSE指令集PDF。
BBuf
2020/04/15
1.7K0
【AI PC端算法优化】一,一步步优化RGB转灰度图算法
[强基固本-视频压缩] 第十三章 向量指令 第二部分:矢量化
本章节所有示例都将使用某个图像的像素块作为输入数据。为简单起见,考虑一个像素值范围为
用户1324186
2024/03/20
2190
[强基固本-视频压缩] 第十三章 向量指令 第二部分:矢量化
SSE图像算法优化系列八:自然饱和度(Vibrance)算法的模拟实现及其SSE优化(附源码,可作为SSE图像入门,Vibrance算法也可用于简单的肤色调整)。
本文介绍了自然饱和度算法及其SSE实现,该算法通过计算像素点与目标值的差值,并利用SSE指令加速处理,最后将处理后的像素点存储到目标颜色空间中。
用户1138785
2018/01/03
2.4K0
SSE图像算法优化系列八:自然饱和度(Vibrance)算法的模拟实现及其SSE优化(附源码,可作为SSE图像入门,Vibrance算法也可用于简单的肤色调整)。
SSE图像算法优化系列四:图像转置的SSE优化(支持8位、24位、32位),提速4-6倍
本文介绍了如何利用SSE/AVX指令集进行CPU并行加速,以解决图像转置中存在的内存访问瓶颈问题。首先介绍了图像转置的算法和实现过程,然后通过具体示例展示了如何使用SSE/AVX指令集进行CPU并行加速,最后给出了针对不同CPU架构的优化策略。
用户1138785
2018/01/03
2.1K0
C++ 中文周刊 第123期
RSS https://github.com/wanghenshui/cppweeklynews/releases.atom
王很水
2024/07/30
910
C++ 中文周刊 第123期
SSE图像算法优化
  图像金字塔技术在很多层面上都有着广泛的应用,很多开源的工具也都有对他们的建立写了专门的函数,比如IPP,比如OpenCV等等,这方面的理论文章特别多,我不需要赘述,但是我发现大部多分开源的代码的实现都不是严格意义上的金字塔,而是做了一定的变通,这种变通常常为了快捷的实现类似的效果,虽然这种变通不太会影响金字塔的效果,但是我这里希望从严格意义上对该算法进行优化,比如简要贴一下下面的某个高斯金字塔的代码:
用户1138785
2019/05/24
1.1K0
SSE图像算法优化系列十二:多尺度的图像细节提升。
本文讲述如何使用SSE指令集进行高性能图像处理,通过将图像划分为多个Block进行处理,并利用SSE指令集实现高效的滤波处理。同时,本文还根据处理结果比较了两种滤波方法的优缺点,并给出了一种基于SSE指令集的优化方法。
用户1138785
2018/01/03
1.2K0
SSE图像算法优化系列十二:多尺度的图像细节提升。
SSE图像算法优化系列九:灵活运用SIMD指令16倍提升Sobel边缘检测的速度(4000*3000的24位图像时间由480ms降低到30ms)。
本文通过分析技术社区中的内容编辑人员如何审查文章的摘要和总结,提出了一些建议。首先,编辑人员需要关注文章的核心内容,即文章的主题和论述,以确保文章的摘要和总结与文章的主题紧密相关。其次,编辑人员需要确保文章的摘要和总结准确、简洁,并且能够吸引读者的注意力。最后,编辑人员需要考虑文章的排版和格式,以确保文章的摘要和总结在视觉上清晰易读。
用户1138785
2018/01/03
2.4K0
SSE图像算法优化系列九:灵活运用SIMD指令16倍提升Sobel边缘检测的速度(4000*3000的24位图像时间由480ms降低到30ms)。
聊聊ClickHouse向量化执行引擎-过滤操作
俄罗斯Yandex开发的ClickHouse是一款性能黑马的OLAP数据库,其对SIMD的灵活运用给其带来了难以置信的性能。本文我们聊聊它如何对过滤操作进行SIMD优化。
yzsDBA
2023/05/25
1.1K0
聊聊ClickHouse向量化执行引擎-过滤操作
实现目前最快的半径相关类算法(附核心源码)
  我在两年前的博客里曾经写过 SSE图像算法优化系列七:基于SSE实现的极速的矩形核腐蚀和膨胀(最大值和最小值)算法 一文,通过SSE的优化把矩形核心的腐蚀和膨胀做到了不仅和半径无关,而且速度也相当的快,当时在被博文的评论里有博友提出了如下的问题:
用户1138785
2019/05/24
1.1K0
SSE图像算法优化系列二:高斯模糊算法的全面优化过程分享(一)。
根据文章内容总结的摘要
用户1138785
2018/01/03
2.2K0
SSE图像算法优化系列二:高斯模糊算法的全面优化过程分享(一)。
图像纹理合成及纹理传输算法学习(附源码)。
本文总结了基于深度学习的图像修复技术的原理和应用。首先介绍了图像修复的背景和意义,然后详细阐述了基于生成对抗网络(GAN)的图像修复方法和基于变分自编码器(VAE)的图像修复方法的技术原理和实现。最后,文章对图像修复技术的应用和前景进行了展望。
用户1138785
2018/01/03
1.7K0
图像纹理合成及纹理传输算法学习(附源码)。
SSE图像算法优化系列六:OpenCv关于灰度积分图的SSE代码学习和改进。
  最近一直沉迷于SSE方面的优化,实在找不到想学习的参考资料了,就拿个笔记本放在腿上翻翻OpenCv的源代码,无意中看到了OpenCv中关于积分图的代码,仔细研习了一番,觉得OpenCv对SSE的灵
用户1138785
2018/01/03
1.6K0
推荐阅读
【算法随记七】巧用SIMD指令实现急速的字节流按位反转算法。
1.3K0
H264/H265 NALU 起始码搜索性能优化(1)
1260
【AI PC端算法优化】三,深入优化RGB转灰度图算法
1.2K0
SSE图像算法优化系列一:一段BGR2Y的SIMD代码解析。
1.3K0
SSE图像算法优化系列三十一:Base64编码和解码算法的指令集优化(C#自带函数的3到4倍速度)。
1.1K0
AVX图像算法优化系列二: 使用AVX2指令集加速查表算法。
1.6K0
【工程应用五】 opencv中linemod模板匹配算法诸多疑惑和自我解读。
1.6K0
【AI PC端算法优化】一,一步步优化RGB转灰度图算法
1.7K0
[强基固本-视频压缩] 第十三章 向量指令 第二部分:矢量化
2190
SSE图像算法优化系列八:自然饱和度(Vibrance)算法的模拟实现及其SSE优化(附源码,可作为SSE图像入门,Vibrance算法也可用于简单的肤色调整)。
2.4K0
SSE图像算法优化系列四:图像转置的SSE优化(支持8位、24位、32位),提速4-6倍
2.1K0
C++ 中文周刊 第123期
910
SSE图像算法优化
1.1K0
SSE图像算法优化系列十二:多尺度的图像细节提升。
1.2K0
SSE图像算法优化系列九:灵活运用SIMD指令16倍提升Sobel边缘检测的速度(4000*3000的24位图像时间由480ms降低到30ms)。
2.4K0
聊聊ClickHouse向量化执行引擎-过滤操作
1.1K0
实现目前最快的半径相关类算法(附核心源码)
1.1K0
SSE图像算法优化系列二:高斯模糊算法的全面优化过程分享(一)。
2.2K0
图像纹理合成及纹理传输算法学习(附源码)。
1.7K0
SSE图像算法优化系列六:OpenCv关于灰度积分图的SSE代码学习和改进。
1.6K0
相关推荐
【算法随记七】巧用SIMD指令实现急速的字节流按位反转算法。
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档