OpenCV 4.10.0
开源计算机视觉
|
前一个教程: 如何使用 OpenCV parallel_for_对代码进行并行处理
兼容性 | OpenCV >= 3.0 |
本教程的目标是提供一个指南,利用通用内在函数对 C++ 代码进行矢量化处理,以实现更快的运行时间。我们首先简要介绍SIMD 内在函数以及如何使用宽寄存器,然后完成一个关于使用宽寄存器的基本操作的教程。
在本部分,我们简要介绍几个概念,以便更好地理解该功能。
内在函数由编译器单独处理。这些函数通常经过优化,以尽可能高效地执行,因此运行速度比一般实现要快。然而,由于这些函数依赖于编译器,因此编写可移植应用程序变得困难。
SIMD 的全称是单指令,多数据。SIMD 内在函数允许处理器对计算进行矢量化。数据存储在称为寄存器的地方。寄存器可能为128 位、256 位或512 位宽。每个寄存器存储相同数据类型的多个值。寄存器的宽度和每个值的大小决定了总共存储的值的数量。
根据 CPU 支持的指令集,您可能可以使用不同的寄存器。如需了解更多信息,请查看 此处
OpenCV 的通用内在函数提供了一个对 SIMD 矢量化方法的抽象,允许用户使用内在函数,而不需要编写特定系统的代码。
OpenCV 通用内在函数支持以下指令集
我们现在将介绍可用结构和函数
Universal Intrinsics 集将每个寄存器实现为一个基于特定 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);
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. */
在下面一节中,我们将对单通道的简单卷积函数进行矢量化处理,并将结果与标量实现进行比较。
你可以在之前的教程中了解更多有关卷积的内容。我们使用之前的教程中的简单实现,并将其与矢量化版本进行比较。
完整教程代码 在此。
我们首先实现一维卷积,然后将其矢量化。2D 向量化卷积将执行行上的 1D 卷积以生成正确的结果。
现在我们将研究 1D 卷积的矢量化版本。
step
的所有 *window* 向量的标量积。我们将这些值添加到已存储在 ans 中的值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* 行与相应内核行的 1 维卷积。最终值是各个 1 维卷积的和。
unsigned char
矩阵在本教程中,我们使用了水平梯度核。两者的输出图像相同。
运行时优化程度因 CPU 可用 SIMD 能力而异。