![]() |
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功能。