图像沿列方向降维的AVX2实现讨论
图像沿列方向降维的AVX2实现讨论
对于二维图像,取每一列像素序列的和或均值是一种基础算子。
本文主要介绍本人的优化思路,因此本文中要求图像高度不能超过256,这样就可以保证对于常见的8bit图像,每列求和的结果使用uint16_t
保存即可保证不溢出,同时要求图像宽度是32或64的倍数等,省略对每行尾部无法并行的部分的计算。
普通指针实现
void ReduceColSum(const uint8_t* src, uint16_t* dst, uint32_t height,uint32_t width) {for (size_t y = 0; y < height; ++y) {for (size_t x = 0; x < width; ++x) {dst[x] += (uint16_t)src[y * width + x];}}
}
当前OpenCV在4.6.0中的cv::Reduce
代码中使用的便是使用通用寄存器执行for循环,不过对不同的算术运算封装成了模板,并使用了×4的循环展开。
AVX指令集实现
两种基本实现
第一种AVX指令集的实现如下函数ReduceColSum1
所示。循环内部avx指令相关的操作主要包括load256->permute->unpacklo/unpackhi->adds->storeu。
void ReduceColSum1(const uint8_t* src, uint16_t* dst, uint32_t height,uint32_t width) {assert(width % 32 == 0);assert(height <= 256);__m256i mSrc, mSum, mSum2;int nCount = (width / 32);const uint8_t* psrc = src;const __m256i m0 = _mm256_setzero_si256();for (size_t y = 0; y < height; ++y) {uint16_t* pdst = dst;for (size_t x = 0; x < nCount; ++x) {mSrc = _mm256_loadu_si256((__m256i*)(psrc));mSrc = _mm256_permute4x64_epi64(mSrc, 0xD8);mSum = _mm256_loadu_si256((__m256i*)(pdst));mSum2 = _mm256_loadu_si256((__m256i*)(pdst + 16));mSum = _mm256_adds_epu16(mSum, _mm256_unpacklo_epi8(mSrc, m0));_mm256_store_si256((__m256i*)(pdst), mSum);mSum2 = _mm256_adds_epu16(mSum2, _mm256_unpackhi_epi8(mSrc, m0));_mm256_storeu_si256((__m256i*)(pdst + 16), mSum2);pdst += 32;psrc += 32;}}
}
第二种实现如下ReduceColSum2
函数所示,实现是将permute->unpacklo/unpackhi替换为AVX2指令集中的convert指令。可以代码更简洁易懂。但每个内层循环只能读取并处理16字节。经测试两种实现耗时大体相当。
void ReduceColSum2(const uint8_t* src, uint16_t* dst, uint32_t height,uint32_t width) {assert(width % 32 == 0);assert(height <= 256);__m128i mSrc;__m256i mSum;const uint8_t* psrc = src;const int nCount = (width / 16);for (size_t y = 0; y < height; ++y) {uint16_t* pdst = dst;for (size_t x = 0; x < nCount; ++x) {mSum = _mm256_loadu_si256((__m256i*)(pdst));mSrc = _mm_loadu_si128((const __m128i*)(psrc));mSum = _mm256_adds_epu16(mSum, _mm256_cvtepu8_epi16(mSrc));_mm256_storeu_si256((__m256i*)(pdst), mSum);pdst += 16;psrc += 16;}}
}
测试_mm256_cvtepu8_epi32
将ReduceColSum2
中_mm256_cvtepu8_epi16
替换为_mm256_cvtepu8_epi32
,取消图像高度不大于256的限制,经测试耗时增加了一倍左右。
void ReduceColSum3(const uint8_t* src, uint16_t* dst, uint32_t height,uint32_t width) {assert(width % 32 == 0);assert(height <= 256);__m128i mSrc;__m256i mSum;const uint8_t* psrc = src;const int nCount = (width / 8);for (size_t y = 0; y < height; ++y) {uint16_t* pdst = dst;for (size_t x = 0; x < nCount; ++x) {mSum = _mm256_loadu_si256((__m256i*)(pdst));mSrc = _mm_loadu_si128((const __m128i*)(psrc));mSum = _mm256_add_epi32(mSum, _mm256_cvtepu8_epi32(mSrc));_mm256_storeu_si256((__m256i*)(pdst), mSum);pdst += 8;psrc += 8;}}
}
使用循环展开优化
可以看到列求和操作计算部分包括cvt和add两步骤,两条指令的Latency是比较低的,反之读写操作loadu和storeu的Latency相对较高,因此可以使用循环展开进行优化。ReduceColSum3
和ReduceColSum4
分别是展开2倍和4倍的代码,经测试耗时约是不使用循环展开的70%和50%。
void ReduceColSum3(const uint8_t* src, uint16_t* dst, uint32_t height,uint32_t width) {assert(width % 32 == 0);assert(height <= 256);__m128i mSrc, mSrc1;__m256i mSum, mSum1;const uint8_t* psrc = src;const int nCount = (width / 32);for (size_t y = 0; y < height; ++y) {uint16_t* pdst = dst;for (size_t x = 0; x < nCount; ++x) {mSum = _mm256_loadu_si256((__m256i*)(pdst));mSum1 = _mm256_loadu_si256((__m256i*)(pdst+16));mSrc = _mm_loadu_si128((const __m128i*)(psrc));mSrc1 = _mm_loadu_si128((const __m128i*)(psrc + 16));mSum = _mm256_adds_epu16(mSum, _mm256_cvtepu8_epi16(mSrc));_mm256_storeu_si256((__m256i*)(pdst), mSum);mSum1 = _mm256_adds_epu16(mSum1, _mm256_cvtepu8_epi16(mSrc1));_mm256_storeu_si256((__m256i*)(pdst + 16), mSum1);pdst += 32;psrc += 32;}}
}
void ReduceColSum4(const uint8_t* src, uint16_t* dst, uint16_t height,uint16_t width) {assert(width % 64 == 0);assert(height <= 256);__m128i mSrc, mSrc1, mSrc2, mSrc3;__m256i mSum, mSum1, mSum2, mSum3;const uint8_t* psrc = src;const int nCount = (width / 64);for (size_t y = 0; y < height; ++y) {uint16_t* pdst = dst;for (size_t x = 0; x < nCount; ++x) {mSum = _mm256_loadu_si256((__m256i*)(pdst));mSum1 = _mm256_loadu_si256((__m256i*)(pdst+16));mSum2 = _mm256_loadu_si256((__m256i*)(pdst+32));mSum3 = _mm256_loadu_si256((__m256i*)(pdst+48));mSrc = _mm_loadu_si128((const __m128i*)(pdst));mSrc1 = _mm_loadu_si128((const __m128i*)(pdst+16));mSrc2 = _mm_loadu_si128((const __m128i*)(pdst+32));mSrc3 = _mm_loadu_si128((const __m128i*)(pdst+48));mSum = _mm256_adds_epu16(mSum, _mm256_cvtepu8_epi16(mSrc));_mm256_storeu_si256((__m256i*)(pdst), mSum);mSum1 = _mm256_adds_epu16(mSum1, _mm256_cvtepu8_epi16(mSrc1));_mm256_storeu_si256((__m256i*)(pdst + 16), mSum1);mSum2 = _mm256_adds_epu16(mSum2, _mm256_cvtepu8_epi16(mSrc2));_mm256_storeu_si256((__m256i*)(pdst + 32), mSum2);mSum3 = _mm256_adds_epu16(mSum3, _mm256_cvtepu8_epi16(mSrc3));_mm256_storeu_si256((__m256i*)(pdst + 48), mSum3);pdst += 64;psrc += 64;}}
}
总结
除了本文之外,我实际进行了更多测试,总体由于不同指令的测试效果并不稳定,在本文并未体现。总体上可以看到使用AVX指令集并行只不过是第一步,尤其是对于简单的图像计算也是最简单的一步,全部依赖编译器优化也是不合理,比如使用的MSVC并不会使用循环展开。只有深入理解计算机系统,理解汇编代码、清楚计算机工作原理,才能更好的优化代码执行。
图像沿列方向降维的AVX2实现讨论相关推荐
- 图像算法九:【图像特征提取】特征降维、PCA人脸特征抽取、局部二进制
PCA数学理论: 关于PCA的理论,资料很多,公式也一大把,本人功底有限,理论方面这里就不列出了.下面主要从应用的角度大概来讲讲具体怎么实现数据集的降维. 把原始数据中每个样本用一个向量表示,然后把所 ...
- ITK:计算图像在特定方向上的导数
ITK:计算图像在特定方向上的导数 内容提要 C++实现代码 内容提要 计算图像在特定方向上的导数. C++实现代码 #include "itkImage.h" #include ...
- MIT线性代数笔记一 行图像和列图像
文章目录 1. 曾经 2. 现在 3. 第一讲 行图像和列图像 3.1 行图像 3.2 列图像 1. 曾经 若干年前,有一个年轻的男老师(王清老师)给我们讲线性代数.他讲课的声音比较小,坐到后面接 ...
- opencv java水平投影_OpenCV实现图像在水平方向上投影
开始没有将数组赋值为零,不能正常显示.代码如下: Mat srcImage=imread("test.png"); imshow("C",srcImage); ...
- 图像特征:方向梯度直方图 HOG
文章目录 参考资料 简介 算法流程 灰度化和gamma校正 计算梯度 统计cell的梯度方向直方图 Block 块内归一化(重点) 组合为HOG特征 HOG特征与可视化 OpenCV 算法实现 参考资 ...
- PyQT:第一个Demo,画出鼠标单击位置出图像的列像素折线图
场景:有一系列图像,需要查看图像每列的像素值的大小,可以把图像读出来然后指定列,再查看,但比较麻烦,每看一列都要修改一下.后面又用回调函数滑动条,这样不用每次都修改列了,但假如换张图像的话还是要修改图 ...
- python treeview insert_pythonttkinter Treeview添加图像作为列值
您可以使用w.insert方法中的image参数显示图像.见下文.在from tkinter import PhotoImage. self._img = PhotoImage(file=" ...
- android 获取相机方向,android – 从相机捕捉图像,导致炸毁方向
我试图从相机捕捉图像,它可以在横向模式下正常工作,当我拍摄肖像时,它会旋转.以下是我使用的代码: BitmapFactory.Options bounds = new BitmapFactory.Op ...
- numpy.hstack(a,b) 按列方向扩展 与 numpy.vstack(a,b) 按行方向扩展 其中n*1 数组可以写成 举例 np.array([[1],[2],[3]])
https://www.jianshu.com/p/608140bec2f5 其中n*1 数组可以写成 举例 np.array([[1],[2],[3]])
最新文章
- 缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级
- 机器学习与气象数据_气象大数据与机器学习联合实验室 大数据和气象的“联姻”...
- Openldap命令详解
- 设计模式(二)设计模式的本质
- c调用其他类的方法_吊打面试官-类加载器
- oracle诊断日志,oracle日常诊断语句
- (66)FPGA面试题-为parallel encoder编写Verilog代码,实现MUX4_1
- 怎么看tomcat添加的项目名_Tomcat部署项目不加项目名访问,不加8080访问
- 名编辑电子杂志大师教程 | 添加搜索功能
- iris数据集——决策树
- 小甲鱼C语言单链表通讯录作业
- EC Final 2019 题解
- Lion Disk Maker让你一键制作Lion系统安装U盘
- 三种碎片化方法:RECAP, BRICS与eMolFrag
- C# 解决上传附件大小限制
- vue 动态生成下载二维码
- 三星手机html默认,关于三星手机恢复出厂设置的方法
- 【限流算法】java实现固定时间窗口算法
- java解惑之字符之谜(谜题17)
- 基恩士KV06N程序 基恩士KV06N,昆仑通态触摸屏 全自动LED划线点装机,PLC本体伺服轴控制