使用通用内部函数矢量化代码

目标

本教程的目标是提供使用通用内部函数功能矢量化 C++ 代码以提高运行时速度的指南。我们将简要介绍 SIMD 内部函数以及如何使用宽寄存器,然后介绍使用宽寄存器的基本操作。

理论

在本节中,我们将简要介绍一些概念,以帮助更好地理解该功能。

内部函数

内部函数是由编译器单独处理的函数。这些函数通常经过优化,以最有效的方式执行,因此运行速度比正常实现更快。但是,由于这些函数依赖于编译器,因此很难编写可移植的应用程序。

SIMD的

SIMD 代表 单指令,多数据。SIMD 内部函数允许处理器对计算进行矢量化。数据存储在所谓的寄存器中。寄存器可以是 128 位256 位或 512 位宽。每个寄存器存储相同数据类型多个值。寄存器的大小和每个值的大小决定了总共存储的值的数量。

根据 CPU 支持的指令集,您可以使用不同的寄存器。要了解更多信息,请看这里

通用内部函数

OpenCV 的通用内部函数提供了对 SIMD 矢量化方法的抽象,并允许用户使用内部函数,而无需编写特定于系统的代码。

OpenCV Universal Intrinsics 支持以下指令集:

  • 支持各种类型的 128 位寄存器,适用于各种架构,包括
    • x86(SSE/SSE2/SSE4.2),
    • 手臂(霓虹灯),
    • 电源PC(VSX),
    • MIPS(MSA)。
  • x86(AVX2) 支持 256 位寄存器,并且
  • x86(AVX512) 支持 512 位寄存器

现在,我们将介绍可用的结构和功能:

  • 寄存器结构
  • 加载和存储
  • 数学运算
  • 减少和遮罩

寄存器结构

通用内部函数集将每个寄存器实现为基于特定 SIMD 寄存器的结构。所有类型都包含枚举,该枚举给出类型可以容纳的值的确切数量。这样就无需在实现过程中对值的数量进行硬编码。nlanes

注意

每个寄存器结构都在命名空间下。cv

有两种类型的寄存器:

  • 可变大小的寄存器:这些结构没有固定的大小,它们的确切位长度是在编译过程中根据可用的 SIMD 功能推断出来的。因此,枚举的值是在编译时确定的。nlanes

    每个结构都遵循以下约定:

    <span style="background-color:#fbfcfd">v_[type of value][size of each value in bits]
    </span>

    例如,v_uint8 保存 8 位无符号整数v_float32保存 32 位浮点值。然后我们声明一个寄存器,就像我们在 C++ 中声明任何对象一样

    根据可用的 SIMD 指令集,特定寄存器将保存不同数量的值。例如:如果您的计算机支持最大 256 位寄存器,

    • v_uint8将保存 32 个 8 位无符号整数
    • v_float64将容纳 4 个 64 位浮点数(双精度)
      <span style="background-color:#fbfcfd">  v_uint8 a;                            // a is a register supporting uint8(char) data
        int n = a.nlanes;                     // n holds 32
      </span>

    可用的数据类型和大小:

    类型大小(以位为单位)
    uint8, 16, 32, 64
    int8, 16, 32, 64
    32, 64
  • 恒定大小的寄存器:这些结构具有固定的位大小并保存恒定数量的值。我们需要知道系统支持哪些 SIMD 指令集,并选择兼容的寄存器。仅当需要确切的位长度时才使用它们。

    每个结构都遵循约定:

    <span style="background-color:#fbfcfd">v_[type of value][size of each value in bits]x[number of values]
    </span>

    假设我们要存储

    • 128 位寄存器中的 32 位(以位为单位)有符号整数。由于寄存器大小是已知的,我们可以找出寄存器中的数据点数 (128/32 = 4):
      <span style="background-color:#fbfcfd">  v_int32x8 reg1                       // holds 8 32-bit signed integers.
      </span>
    • 512 位寄存器中的 64 位浮点数:
      <span style="background-color:#fbfcfd">  v_float64x8 reg2                     // reg2.nlanes = 8
      </span>

加载和存储操作

现在我们知道寄存器是如何工作的,让我们看一下用于用值填充这些寄存器的函数。

  • 加载:加载函数允许您将值加载到寄存器中。

    • 构造函数 - 在声明寄存器结构时,我们可以提供一个内存地址,寄存器将从中拾取连续的值,或者将值显式地作为多个参数提供(显式多个参数仅适用于恒定大小的寄存器):
      <span style="background-color:#fbfcfd">  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);
      </span>

    • Load 函数 - 我们可以使用 load 方法并提供数据的内存地址:

      <span style="background-color:#fbfcfd">  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]
      </span>

      注意

      load 函数假定数据未对齐。如果您的数据是对齐的,则可以使用该函数。vx_load_aligned()

  • 存储:存储函数允许您将寄存器中的值存储到特定的内存位置。
    • 要将寄存器中的值存储到内存位置,可以使用 v_store() 函数:
      <span style="background-color:#fbfcfd">  float ptr[4];
        v_store(ptr, reg); // store the first 128 bits(interpreted as 4x32-bit floats) of reg into ptr.
      </span>

      注意

      确保 ptr 与寄存器具有相同的类型。您还可以在执行操作之前将寄存器转换为正确的类型。简单地对指向特定类型的指针进行类型转换将导致对数据的错误解释。

      二进制和一元运算符

通用内部函数集提供元素二元和一元运算。

  • 算术:我们可以按元素对两个寄存器进行加、减、乘和除。寄存器必须具有相同的宽度并保持相同的类型。将两个寄存器相乘,例如:
    <span style="background-color:#fbfcfd">  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}
    </span>

  • 按位逻辑和移位:我们可以对寄存器的每个元素的位进行左移或右移。我们还可以在两个寄存器之间按元素应用按位 &、|、^ 和 ~ 运算符:
    <span style="background-color:#fbfcfd">  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}
    </span>

  • 比较运算符:我们可以使用 <、>、<=、>=、== 和 != 运算符比较两个寄存器之间的值。由于每个寄存器都包含多个值,因此这些操作不会得到单个布尔值。相反,对于真值,所有位都转换为 1(8 位为 0xff,16 位为 0xffff,以此类推),而 false 值返回转换为零的位。
    <span style="background-color:#fbfcfd">  // 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.
      */
    </span>
  • 最小值/最大值运算:我们可以使用 v_min() 和 v_max() 函数返回包含两个寄存器的元素最小值或最大值的寄存器:
    <span style="background-color:#fbfcfd">  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)}
    </span>

注意

比较和最小/最大运算符不适用于 64 位整数。按位移位和逻辑运算符仅适用于整数值。按位移位仅适用于 16、32 和 64 位寄存器。

减少和遮罩

  • Reduce操作v_reduce_min()v_reduce_max()v_reduce_sum()返回一个值,表示整个寄存器的最小值、最大值或总和:
    <span style="background-color:#fbfcfd">  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
    </span>
  • 掩码操作:掩码操作允许我们在宽寄存器中复制条件。这些包括:
    • v_check_all() - 返回一个布尔值,如果寄存器中的所有值都小于零,则为 true。
    • v_check_any() - 返回布尔值,如果寄存器中的任何值小于零,则为 true。
    • v_select() - 返回一个寄存器,该寄存器基于掩码混合两个寄存器。
      <span style="background-color:#fbfcfd">  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.
        */
      </span>

示范

在下一节中,我们将矢量化单通道的简单卷积函数,并将结果与标量实现进行比较。

注意

并非所有算法都可以通过手动矢量化进行改进。事实上,在某些情况下,编译器可能会自动矢量化代码,从而为标量实现产生更快的结果。

您可以从上一教程中了解有关卷积的更多信息。我们使用与上一教程相同的朴素实现,并将其与矢量化版本进行比较。

完整的教程代码在这里。

矢量化卷积

我们将首先实现一维卷积,然后对其进行矢量化。2-D 矢量化卷积将在各行之间执行 1-D 卷积以产生正确的结果。

一维卷积:标量
void conv1d(Mat src, Mat &dst, Mat 内核)
{
int len = src.cols;
dst = Mat(1, len, CV_8UC1);
int sz = kernel.cols / 2;
copyMakeBorder(src, src, 0, 0, sz, sz, BORDER_REPLICATE);
forint i = 0; i < len; i++)
{
double 值 = 0;
forint k = -sz; k <= sz; k++)
值 += src.ptr<uchar>(0)[i + k + sz] * kernel.ptr<float>(0)[k + sz];
dst.ptr<uchar>(0)[i] = saturate_cast<uchar>(值);
}
}
  1. 我们首先设置变量并在 src 矩阵的两侧创建边框,以处理边缘情况。
    int len = src.cols;
    dst = Mat(1, len, CV_8UC1);
    int sz = kernel.cols / 2;
    copyMakeBorder(src, src, 0, 0, sz, sz, BORDER_REPLICATE);
  2. 对于主循环,我们选择一个索引 i,并使用 k 变量将其与内核一起偏移。我们将值存储在 value 中,并将其添加到 dst 矩阵中。
    forint i = 0; i < len; i++)
    {
    double 值 = 0;
    forint k = -sz; k <= sz; k++)
    值 += src.ptr<uchar>(0)[i + k + sz] * kernel.ptr<float>(0)[k + sz];
    dst.ptr<uchar>(0)[i] = saturate_cast<uchar>(值);
    }
    一维卷积:矢量

我们现在将看一维卷积的矢量化版本。

void conv1dsimd(Mat src, Mat 内核, float *ans, int row = 0, int rowk = 0, int len = -1)
{
如果 (len == -1)
len = src.cols;
Mat src_32,kernel_32;
const int alpha = 1;
src.convertTo(src_32, CV_32FC1, alpha);
int ksize = kernel.cols, sz = kernel.cols / 2;
copyMakeBorder(src_32, src_32, 0, 0, sz, sz, BORDER_REPLICATE);
int 步骤 = VTraits<v_float32x4>::vlanes();
浮点数 *sptr = src_32.ptr<float>(row), *kptr = kernel.ptr<float>(rowk);
forint k = 0; k < ksize; k++)
{
v_float32 kernel_wide = vx_setall_f32(kptr[k]);
int i;
for (i = 0; i + 步长 < len; i += 步长)
{
v_float32窗口 = vx_load(sptr + i + k);
v_float32和 = v_add(vx_load(ans + i), v_mul(kernel_wide, 窗口));
v_store(ans + i,总和);
}
对于 (;我<len;i++)
{
*(ans + i) += sptr[i + k]*kptr[k];
}
}
}
  1. 在我们的例子中,内核是一个浮点数。由于内核的数据类型最大,我们将 src 转换为 float32,形成 src_32。我们也像对待幼稚的案件一样划定边界。
    Mat src_32,kernel_32;
    const int alpha = 1;
    src.convertTo(src_32, CV_32FC1, alpha);
    int ksize = kernel.cols, sz = kernel.cols / 2;
    copyMakeBorder(src_32, src_32, 0, 0, sz, sz, BORDER_REPLICATE);
  2. 现在,对于内核中的每一列,我们计算该值与所有长度为的窗口向量的标量乘积。我们将这些值添加到已存储的值中step
    int 步骤 = VTraits<v_float32x4>::vlanes();
    浮点数 *sptr = src_32.ptr<float>(row), *kptr = kernel.ptr<float>(rowk);
    forint k = 0; k < ksize; k++)
    {
    v_float32 kernel_wide = vx_setall_f32(kptr[k]);
    int i;
    for (i = 0; i + 步长 < len; i += 步长)
    {
    v_float32窗口 = vx_load(sptr + i + k);
    v_float32和 = v_add(vx_load(ans + i), v_mul(kernel_wide, 窗口));
    v_store(ans + i,总和);
    }
    对于 (;我<len;i++)
    {
    *(ans + i) += sptr[i + k]*kptr[k];
    }
    }
    • 我们声明一个指向 src_32 和内核的指针,并为每个内核元素运行一个循环
      int 步骤 = VTraits<v_float32x4>::vlanes();
      浮点数 *sptr = src_32.ptr<float>(row), *kptr = kernel.ptr<float>(rowk);
      forint k = 0; k < ksize; k++)
      {
    • 我们加载一个带有当前内核元素的寄存器。窗口从 0 移动到 len - step,其与 kernel_wide 数组的乘积被添加到存储在 ans 中的值中。我们将这些值存储回
      v_float32 kernel_wide = vx_setall_f32(kptr[k]);
      int i;
      for (i = 0; i + 步长 < len; i += 步长)
      {
      v_float32窗口 = vx_load(sptr + i + k);
      v_float32和 = v_add(vx_load(ans + i), v_mul(kernel_wide, 窗口));
      v_store(ans + i,总和);
      }
    • 由于长度可能无法被阶梯整除,因此我们直接处理剩余的值。值的数量将始终小于步长,并且不会显着影响性能。我们将所有值存储到 ans,这是一个浮点指针。我们也可以直接将它们存储在对象中Mat
      对于 (;我<len;i++)
      {
      *(ans + i) += sptr[i + k]*kptr[k];
      }
    • 下面是一个迭代示例:
      <span style="background-color:#fbfcfd">  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|
      </span>

注意

函数参数还包括 rowrowk 和 len。当将函数用作 2-D 卷积的中间步骤时,会使用这些值

2-D 卷积

假设我们的内核有 ksize 行。为了计算特定行的值,我们计算前一行 ksize/2 和下一行 ksize/2 的一维卷积,以及相应的内核行。最终值只是单个一维卷积的总和

void convolute_simd(Mat src, Mat &dst, Mat 内核)
{
int rows = src.rows, cols = src.cols;
int ksize = kernel.rows, sz = ksize / 2;
dst = Mat(行、列、CV_32FC1);
copyMakeBorder(src, src, sz, sz, 0, 0, BORDER_REPLICATE);
int 步骤 = VTraits<v_float32x4>::vlanes();
forint i = 0; i <行; i++)
{
forint k = 0; k < ksize; k++)
{
浮点数 ans[N] = {0};
conv1dsimd(src, 内核, ans, i + k, k, cols);
国际J;
for (j = 0; j + 步长 < 列; j += 步长)
{
v_float32 总和 = v_add(vx_load(&dst.ptr<float>(i)[j]), vx_load(&ans[j]));
v_store(&dst.ptr<浮点数>(i)[j], 总和);
}
对于 (;J < cols;j++)
dst.ptr<浮点数>(i)[j] += ans[j];
}
}
const int alpha = 1;
dst.convertTo(dst, CV_8UC1, alpha);
}
  1. 我们首先初始化变量,并在 src 矩阵的上方和下方创建一个边框。左右两侧由一维卷积函数处理。
    int rows = src.rows, cols = src.cols;
    int ksize = kernel.rows, sz = ksize / 2;
    dst = Mat(行、列、CV_32FC1);
    copyMakeBorder(src, src, sz, sz, 0, 0, BORDER_REPLICATE);
    int 步骤 = VTraits<v_float32x4>::vlanes();
  2. 对于每一行,我们计算其上方和下方行的一维卷积。然后,我们将这些值添加到 DST 矩阵中。
    forint i = 0; i <行; i++)
    {
    forint k = 0; k < ksize; k++)
    {
    浮点数 ans[N] = {0};
    conv1dsimd(src, 内核, ans, i + k, k, cols);
    国际J;
    for (j = 0; j + 步长 < 列; j += 步长)
    {
    v_float32 总和 = v_add(vx_load(&dst.ptr<float>(i)[j]), vx_load(&ans[j]));
    v_store(&dst.ptr<浮点数>(i)[j], 总和);
    }
    对于 (;J < cols;j++)
    dst.ptr<浮点数>(i)[j] += ans[j];
    }
    }
  3. 我们最终将 dst 矩阵转换为 8 位矩阵unsigned char
    const int alpha = 1;
    dst.convertTo(dst, CV_8UC1, alpha);

       在线教程

    麻省理工学院人工智能视频教程 – 麻省理工人工智能课程 人工智能入门 – 人工智能基础学习。Peter Norvig举办的课程 EdX 人工智能 – 此课程讲授人工智能计算机系统设计的基本概念和技术。 人工智能中的计划 – 计划是人工智能系统的基础部分之一。在这个课程中,你将会学习到让机器人执行一系列动作所需要的基本算法。 机器人人工智能 – 这个课程将会教授你实现人工智能的基本方法,包括:概率推算,计划和搜索,本地化,跟踪和控制,全部都是围绕有关机器人设计。 机器学习 – 有指导和无指导情况下的基本机器学习算法 机器学习中的神经网络 – 智能神经网络上的算法和实践经验 斯坦福统计学习

    第一阶段:零基础入门(3-6个月)

    新手应首先通过少而精的学习,看到全景图,建立大局观。 通过完成小实验,建立信心,才能避免“从入门到放弃”的尴尬。因此,第一阶段只推荐4本最必要的书(而且这些书到了第二、三阶段也能继续用),入门以后,在后续学习中再“哪里不会补哪里”即可。

    第二阶段:基础进阶(3-6个月)

    熟读《机器学习算法的数学解析与Python实现》并动手实践后,你已经对机器学习有了基本的了解,不再是小白了。这时可以开始触类旁通,学习热门技术,加强实践水平。在深入学习的同时,也可以探索自己感兴趣的方向,为求职面试打好基础。

    第三阶段:工作应用

    这一阶段你已经不再需要引导,只需要一些推荐书目。如果你从入门时就确认了未来的工作方向,可以在第二阶段就提前阅读相关入门书籍(对应“商业落地五大方向”中的前两本),然后再“哪里不会补哪里”。

     有需要的小伙伴,可以点击下方链接免费领取或者V扫描下方二维码免费领取🆓

    在这里插入图片描述

    有需要的小伙伴,可以点击下方链接免费领取或者V扫描下方二维码免费领取🆓

    请添加图片描述

    人工智能书籍

    OpenCV(中文版).(布拉德斯基等) OpenCV+3计算机视觉++Python语言实现+第二版 OpenCV3编程入门 毛星云编著 数字图像处理_第三版 人工智能:一种现代的方法 深度学习面试宝典 深度学习之PyTorch物体检测实战 吴恩达DeepLearning.ai中文版笔记
  • 20
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值