![]() |
OpenCV 4.12.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
可用数据类型和大小
| Type | 位大小 |
|---|---|
| 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.
*/
在以下部分,我们将向量化一个简单的单通道卷积函数,并将结果与标量实现进行比较。
您可以从之前的教程中了解更多关于卷积的信息。我们使用与之前教程中相同的朴素实现,并将其与向量化版本进行比较。
完整的教程代码在此处。
我们将首先实现一维卷积,然后对其进行向量化。二维向量化卷积将对行执行一维卷积以生成正确结果。
现在我们来看看一维卷积的向量化版本。
step的窗口向量的标量积。我们将这些值添加到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行与相应核行的1-D卷积。最终值就是各个1-D卷积的和。
unsigned char矩阵在本教程中,我们使用了水平梯度核。两种方法都获得了相同的输出图像。
运行时间的改进效果各不相同,将取决于您的CPU中可用的SIMD功能。