OpenCV 4.11.0
开源计算机视觉
|
上一教程: 如何使用 OpenCV parallel_for_ 并行化您的代码
兼容性 | OpenCV >= 3.0 |
本教程的目标是指导您如何使用 通用内联函数 功能来向量化您的 C++ 代码,以加快运行速度。我们将简要介绍SIMD 内联函数以及如何使用宽寄存器,然后讲解使用宽寄存器的基本操作教程。
在本节中,我们将简要介绍一些概念,以帮助您更好地理解该功能。
内联函数是由编译器单独处理的函数。这些函数通常经过优化,以尽可能高效的方式执行,因此比普通实现运行得更快。但是,由于这些函数依赖于编译器,因此编写可移植应用程序变得困难。
SIMD 代表单指令多数据。SIMD 内联函数允许处理器向量化计算。数据存储在所谓的寄存器中。寄存器的宽度可能是128 位、256 位或512 位。每个寄存器存储多个相同数据类型的值。寄存器的尺寸和每个值的尺寸决定了总共存储的值的数量。
根据您的 CPU 支持的指令集,您可以使用不同的寄存器。要了解更多信息,请查看 此处
OpenCV 的通用内联函数为 SIMD 向量化方法提供了一种抽象,允许用户使用内联函数而无需编写特定于系统的代码。
OpenCV 通用内联函数支持以下指令集
我们现在将介绍可用的结构和函数
通用内联函数集将每个寄存器实现为基于特定 SIMD 寄存器的结构。所有类型都包含nlanes
枚举,它给出该类型可以容纳的值的确切数量。这消除了在实现过程中硬编码值数量的需要。
cv
命名空间下。有两种类型的寄存器
可变大小寄存器:这些结构没有固定大小,它们的精确位长在编译时根据可用的 SIMD 功能推断得出。因此,nlanes
枚举的值是在编译时确定的。
每个结构都遵循以下约定
v_[type of value][size of each value in bits]
例如,v_uint8 存储 8 位无符号整数,而v_float32 存储 32 位浮点值。然后,我们像在 C++ 中声明任何对象一样声明寄存器
根据可用的 SIMD 指令集,特定寄存器将容纳不同数量的值。例如:如果您的计算机最多支持 256 位寄存器,
v_uint8 a; // a is a register supporting uint8(char) data int n = a.nlanes; // n holds 32
可用数据类型和大小
类型 | 大小(位) |
---|---|
uint | 8, 16, 32, 64 |
int | 8, 16, 32, 64 |
float | 32, 64 |
固定大小寄存器:这些结构具有固定的位大小,并容纳固定数量的值。我们需要知道系统支持什么 SIMD 指令集,并选择兼容的寄存器。只有在需要确切位长时才使用这些寄存器。
每个结构都遵循以下约定
v_[type of value][size of each value in bits]x[number of values]
假设我们想要存储
v_int32x8 reg1 // holds 8 32-bit signed integers.
v_float64x8 reg2 // reg2.nlanes = 8
现在我们知道了寄存器的工作原理,让我们看看用于将这些寄存器填充值的函数。
加载:加载函数允许您将值加载到寄存器中。
float ptr[32] = {1, 2, 3 ..., 32}; // ptr is a pointer to a contiguous memory block of 32 floats // Variable Sized Registers // int x = v_float32().nlanes; // set x as the number of values the register can hold v_float32 reg1(ptr); // reg1 stores first x values according to the maximum register size available. v_float32 reg2(ptr + x); // reg stores the next x values // Constant Sized Registers // v_float32x4 reg1(ptr); // reg1 stores the first 4 floats (1, 2, 3, 4) v_float32x4 reg2(ptr + 4); // reg2 stores the next 4 floats (5, 6, 7, 8) // Or we can explicitly write down the values. v_float32x4(1, 2, 3, 4);
加载函数 - 我们可以使用 load 方法并提供数据的内存地址
float ptr[32] = {1, 2, 3, ..., 32}; v_float32 reg_var; reg_var = vx_load(ptr); // loads values from ptr[0] upto ptr[reg_var.nlanes - 1] v_float32x4 reg_128; reg_128 = v_load(ptr); // loads values from ptr[0] upto ptr[3] v_float32x8 reg_256; reg_256 = v256_load(ptr); // loads values from ptr[0] upto ptr[7] v_float32x16 reg_512; reg_512 = v512_load(ptr); // loads values from ptr[0] upto ptr[15]
vx_load_aligned()
函数。float ptr[4]; v_store(ptr, reg); // store the first 128 bits(interpreted as 4x32-bit floats) of reg into ptr.
通用内联函数集提供逐元素二元和一元运算。
v_float32 a, b; // {a1, ..., an}, {b1, ..., bn} v_float32 c; c = a + b // {a1 + b1, ..., an + bn} c = a * b; // {a1 * b1, ..., an * bn}
v_int32 as; // {a1, ..., an} v_int32 al = as << 2; // {a1 << 2, ..., an << 2} v_int32 bl = as >> 2; // {a1 >> 2, ..., an >> 2} v_int32 a, b; v_int32 a_and_b = a & b; // {a1 & b1, ..., an & bn}
// let us consider the following code is run in a 128-bit register v_uint8 a; // a = {0, 1, 2, ..., 15} v_uint8 b; // b = {15, 14, 13, ..., 0} v_uint8 c = a < b; /* let us look at the first 4 values in binary a = |00000000|00000001|00000010|00000011| b = |00001111|00001110|00001101|00001100| c = |11111111|11111111|11111111|11111111| If we store the values of c and print them as integers, we will get 255 for true values and 0 for false values. */ --- // In a computer supporting 256-bit registers v_int32 a; // a = {1, 2, 3, 4, 5, 6, 7, 8} v_int32 b; // b = {8, 7, 6, 5, 4, 3, 2, 1} v_int32 c = (a < b); // c = {-1, -1, -1, -1, 0, 0, 0, 0} /* The true values are 0xffffffff, which in signed 32-bit integer representation is equal to -1. */
v_int32 a; // {a1, ..., an} v_int32 b; // {b1, ..., bn} v_int32 mn = v_min(a, b); // {min(a1, b1), ..., min(an, bn)} v_int32 mx = v_max(a, b); // {max(a1, b1), ..., max(an, bn)}
v_int32 a; // a = {a1, ..., a4} int mn = v_reduce_min(a); // mn = min(a1, ..., an) int sum = v_reduce_sum(a); // sum = a1 + ... + an
v_uint8 a; // {a1, .., an} v_uint8 b; // {b1, ..., bn} v_int32x4 mask: // {0xff, 0, 0, 0xff, ..., 0xff, 0} v_uint8 Res = v_select(mask, a, b) // {a1, b2, b3, a4, ..., an-1, bn} /* "Res" will contain the value from "a" if mask is true (all bits set to 1), and value from "b" if mask is false (all bits set to 0) We can use comparison operators to generate mask and v_select to obtain results based on conditionals. It is common to set all values of b to 0. Thus, v_select will give values of "a" or 0 based on the mask. */
在下一节中,我们将为单通道向量化一个简单的卷积函数,并将结果与标量实现进行比较。
您可以从之前的教程中了解更多关于卷积的知识。我们使用之前的教程中相同的朴素实现,并将其与向量化版本进行比较。
完整的教程代码在此处。
我们将首先实现一维卷积,然后将其向量化。二维向量化卷积将跨行执行一维卷积以产生正确的结果。
我们现在来看一维卷积的向量化版本。
step
的所有window向量的标量积。我们将这些值添加到已存储在ans中的值。Mat
对象中。For example: kernel: {k1, k2, k3} src: ...|a1|a2|a3|a4|... iter1: for each idx i in (0, len), 'step' idx at a time kernel_wide: |k1|k1|k1|k1| window: |a0|a1|a2|a3| ans: ...| 0| 0| 0| 0|... sum = ans + window * kernel_wide = |a0 * k1|a1 * k1|a2 * k1|a3 * k1| iter2: kernel_wide: |k2|k2|k2|k2| window: |a1|a2|a3|a4| ans: ...|a0 * k1|a1 * k1|a2 * k1|a3 * k1|... sum = ans + window * kernel_wide = |a0 * k1 + a1 * k2|a1 * k1 + a2 * k2|a2 * k1 + a3 * k2|a3 * k1 + a4 * k2| iter3: kernel_wide: |k3|k3|k3|k3| window: |a2|a3|a4|a5| ans: ...|a0 * k1 + a1 * k2|a1 * k1 + a2 * k2|a2 * k1 + a3 * k2|a3 * k1 + a4 * k2|... sum = sum + window * kernel_wide = |a0*k1 + a1*k2 + a2*k3|a1*k1 + a2*k2 + a3*k3|a2*k1 + a3*k2 + a4*k3|a3*k1 + a4*k2 + a5*k3|
假设我们的卷积核有ksize行。为了计算特定行的值,我们计算前ksize/2行和后ksize/2行与相应卷积核行的1D卷积。最终值只是各个1D卷积的总和。
在本教程中,我们使用了水平梯度核。两种方法得到相同的输出图像。
运行时间的改进差异很大,取决于你的CPU中可用的SIMD功能。