1.Halide介绍
Halide是一种编程语言,主要在图片处理和矩阵计算时具有方便快捷高性能的特点。它不是一种独立语言,而是基于C++的DSL(Domain Specified Language),主要应用在算法的底层加速,并且此优化与算法本身设计无关。Halide思想在传统的图像处理(OpenvCV)和深度学习(TVM)优化加速方面具有较强的借鉴意义。支持的目标如下:
- CPU 架构: X86, ARM, MIPS, Hexagon, PowerPC, RISC-V
- OS: Linux, Windows, macOS, Android, iOS, Qualcomm QuRT
- GPU APIs: CUDA, OpenCL, OpenGL Compute Shaders, Apple Metal, Microsoft Direct X 12
作为一种嵌入在C++上的语言,用户可以使用C++API创建Halide的内存中间表示,共两种编译方式JIT(just-in-time)和AOT(ahead-of-time),JIT方式在代码运行阶段被映射为Halide内存对象,AOT方式可以生成编译文件,在使用时进行链接,主要应用于嵌入式和交叉编译环境。
下面代码是使用Halide定义的3*3的图片过滤函数示例:
Func blur_3x3(Func input) {
Func blur_x, blur_y;
Var x, y, xi, yi;
blur_x(x, y) = (input(x-1, y) + input(x, y) + input(x+1, y))/3;
blur_y(x, y) = (blur_x(x, y-1) + blur_x(x, y) + blur_x(x, y+1))/3;
blur_y.tile(x, y, xi, yi, 256, 32)
.vectorize(xi, 8).parallel(y);
blur_x.compute_at(blur_y, x).vectorize(x, 8);
return blur_y;
}
2.高性能代码的特征
高性能代码需要开发者在并行、局部性、额外开销(如下图)三个方面进行平衡。通过多核并行或SIMD同时对多路数据计算可以缩短计算时间,但是此过程中会存在多进程或线程开销。为了获取程序内存的局部性,可能需要对内存进行合并、划分、拷贝等操作,从而产生额外工作。因此获取高性能代码的过程需要对并行、局部性、额外开销三个方面进行平衡。
2.1 并行性(parallelizing)
实现并行的方式主要为mutlticore和SIMD,其中multicore的方式比较容易理解,使用多个物理核进行计算,这样就可以比单核计算的速度快。SIMD即Single Instruction Multiple Data,一条指令操作多个数据。是CPU基本指令集的扩展,主要用于小碎数据的并行操作。在图像处理过程中,一个像素点的一个分量总是用小于等于8bit的数据表示的。如果使用传统的处理器做计算,虽然处理器的寄存器是32位或是64位的,处理这些数据却只能用于他们的低8位,如果把64位寄存器拆成8个8位寄存器就能同时完成8个操作,计算效率提升了8倍。
2.2 局部性(loclaity)
在CPU访问存储设备时,无论是存取数据抑或存取指令,都趋于聚集在一片连续的区域中,这就被称为局部性原理。时间局部性(Temporal Locality),如果一个信息项正在被访问,那么在近期它很可能还会被再次访问。比如循环、递归、方法的反复调用等。空间局部性(Spatial Locality),如果一个存储器的位置被引用,那么将来他附近的位置也会被引用。比如顺序执行的代码、连续创建的两个对象、数组等。下面是Halide中的经典模糊化(blurred)图像的例子,首先在x轴上对每个像素点以及周围的两个点进行求和平均,然后再到y轴上进行同样的操作,这样相当于一个3×3平均卷积核对整个图像进行操作。
void box_filter_3x3(const Mat ∈, Mat &blury){
Mat blurx(in.size(), in.type());
for(int x = 1; x < in.cols-1; x ++)
for(int y = 0 ; y < in.rows; y ++)
blurx.at<uint8_t >(y, x) = static_cast<uint8_t&