任意範囲へ飽和:符号付・なし

任意の範囲へ飽和させる飽和処理を紹介します。

64ビット整数型

呼び出し側・符号付

以降に、呼び出し側のC++コードを示します。common.hをincludeしていますがSIMDで記述したアセンブリ言語の関数と等価な機能を持つC++言語で記述した共通関数のヘッダです。最下部にソースコードを示します。条件は関数名に含まれます。

#include "..\common.h"  // TEMPLATES

#define T  long long
extern "C" void satULQ(const T*, T*, T, T, size_t);

// main
int main(void)
{
    const size_t ArrLen = 4096;
    static_assert(ArrLen % 16 == 0,
        "number of elements must be an integral multiple of 16.");
    T  a[ArrLen], c[ArrLen], v[ArrLen];
    const T lower = -5000, upper = 3000;

    init(a, ArrLen);
    a[ArrLen - 1] = lower - 1;
    a[ArrLen - 2] = upper + 1;

    c_Sat (a, c, lower, upper, ArrLen);
    satULQ(a, v, lower, upper, ArrLen);
    verify(c, v, ArrLen);
    return 0;
}

符号付64ビット整数型の配列をいくつか宣言し、配列へランダムな値を設定します。確実に関数が動作しているか確認するため、入力配列の最後に下限値と上限値を超える値を設定します。そしてアセンブリ言語で記述した関数とC++言語で開発した関数を呼び出し、結果をverify関数で検証します。

呼び出し側・符号なし

符号なしへ対応した呼び出し側のソースリストを示します。型以外は符号付と同じです。

#include "..\common.h"  // TEMPLATES

#define T  unsigned long long
extern "C" void satULUQ(const T*, T*, T, T, size_t);

// main
int main(void)
{
    const size_t ArrLen = 4096;
    static_assert(ArrLen % 16 == 0,
        "number of elements must be an integral multiple of 16.");
    T  a[ArrLen], c[ArrLen], v[ArrLen];
    const T lower = 300, upper = 3000;

    init(a, ArrLen);
    a[ArrLen - 1] = lower - 1;
    a[ArrLen - 2] = upper + 1;

    c_Sat  (a, c, lower, upper, ArrLen);
    satULUQ(a, v, lower, upper, ArrLen);
    verify(c, v, ArrLen);
    return 0;
}

先のプログラムは符号付64ビット整数型へ対応させたものでした。本プログラムは、符号なし64ビット整数型へ対応させたものです。変更は僅かです。符号なしですので、下限も上限も正の値を指定します。

呼び出され側

64ビット整数型配列の要素を、渡された範囲内に飽和処理します。以降に、マクロを使って開発したライブラリを示します。アセンブラーのコードを示します。符号ごとに関数を記述するのは面倒なのでマクロを使って記述します。

; macro
mymacro macro       MNAME, MMAXINST, MMININST

        public      MNAME
        align       16
MNAME   proc
        vpbroadcastq zmm1, r8                   ; zmm1 = lower
        vpbroadcastq zmm2, r9                   ; zmm2 = upper

        mov         r8, qword ptr [rsp+40]      ; length
        xor         rax, rax                    ; clear index
loop_f:
        vmovdqu64   zmm0, zmmword ptr [rcx+rax*8] ; load a[]
        MMAXINST    zmm0, zmm0, zmm1
        MMININST    zmm0, zmm0, zmm2
        vmovdqu64   zmmword ptr [rdx+rax*8], zmm0

        add         rax, 8
        cmp         rax, r8
        jb          short loop_f

        ret
MNAME   endp

        endm

;-------------------------------------------------------------------
; code
_TEXT   segment

mymacro satULQ,  vpmaxsq, vpminsq
mymacro satULUQ, vpmaxuq, vpminuq

_TEXT   ends
        end

本関数へ渡されるレジスターとデータの関係を以降の表に示します。

レジスタ 内容
rcx 入力配列の先頭アドレス
rdx 出力配列の先頭アドレス
r8 飽和の上限値
r9 飽和の下限値
rsp+40 配列の要素数

vpbroadcastq命令で、zmm1に下限値、zmm2に上限値をブロードキャストします。次に、r8レジスターへ要素数を設定します。基本的なループの構造は、これまでのプログラムと同様です。vmovdqu64 命令で、符号付、あるいは符号なし64ビット整数型配列から8個の要素を読み込みます。そして、vpmaxsq命令あるいはvpmaxuq命令で下限値へ飽和させます。次に、その結果を使って、vpminsq命令あるいはvpminuq命令で上限値へ飽和させます。これによって入力の値を、下限値と上限値の範囲へ飽和させ、vmovdqu64 命令で8個の要素を書き込みます。
一回の処理で8要素を処理するため、raxレジスターに8を加算し要素数に達するまで上記処理を繰り返します。なお、先に示したvpmaxsq命令あるいはvpmaxuq命令は、マクロの引数MMAXINST で置き換えられ、vpminsq命令あるいはvpminuq命令はMMININST で置き換えられます。

 実行結果(signed)

C:\>ml64 /c satULQAsm.asm
C:\>cl /O2 /EHsc satULQ.cpp satULQAsm.obj
C:\>satULQ
Ok!

 実行結果(unsigned)

C:\>ml64 /c satULQAsm.asm
C:\>cl /O2 /EHsc satULUQ.cpp satULQAsm.obj
C:\>satULUQ
Ok!

common.h、共通関数

#include <iostream>

using namespace std;

// initialize values
template <typename T>
void init(T* a, const size_t length)
{
    for (size_t i = 0; i < length; i++)
    {
        a[i] = (T)(rand() - (RAND_MAX / 2));
    }
}

// initialize values
template <typename T>
void init(T* a, T* b, const size_t length)
{
    for (size_t i = 0; i < length; i++)
    {
        a[i] = (T)(rand() - (RAND_MAX / 2));
        b[i] = (T)(rand() - (RAND_MAX / 2));
    }
}

// verify values
template <typename T>
void verify(T* c, T* v, const size_t length)
{
    bool errorFlag = false;
    for (size_t i = 0; i < length; i++)
    {
        if (c[i] != v[i])
        {
            cout << "Error, " << "i = " << i << ", c = "
                            << c[i] << ", v = " << v[i] << endl;
            errorFlag = true;
            break;
        }
    }
    if(!errorFlag)
        cout << "Ok!" << endl;
}

template <typename T>
void c_Sat(const T* a, T* v, const T lower,
                            const T upper, const size_t length)
{
    for (size_t i = 0; i < length; i++)
    {
        T r = a[i] < lower ? lower : a[i];
        r = r > upper ? upper : r;
        v[i] = r;
    }
}

// print elapsed time
void print_elTime(char* prompt, clock_t start, clock_t end)
{
    float elapsed = static_cast<double>(end - start) /
                                CLOCKS_PER_SEC * 1000.0;
    printf(prompt);
    printf("%10.3f [ms]\n", elapsed);
}

32ビット整数型

呼び出し側は示さず、呼び出され側のみを示します。

呼び出され側

32ビット整数型へ対応したものを紹介します。以降に、ソースコードの一部を示します。

        :
mymacro macro       MNAME, MMAXINST, MMININST
        :

MNAME   proc
        vpbroadcastd zmm1, r8                   ; zmm1 = lower
        vpbroadcastd zmm2, r9                   ; zmm2 = upper

        mov         r8, qword ptr [rsp+40]      ; length
        xor         rax, rax                    ; clear index
loop_f:
        vmovdqu32   zmm0, zmmword ptr [rcx+rax*4] ; load a[]
        MMAXINST    zmm0, zmm0, zmm1
        MMININST    zmm0, zmm0, zmm2
        vmovdqu32   zmmword ptr [rdx+rax*4], zmm0

        add         rax,16
        :

_TEXT   segment

mymacro satULD,  vpmaxsd, vpminsd
mymacro satULUD, vpmaxud, vpminud

_TEXT   ends
        end

vpbroadcastd命令で、zmm1に下限値、zmm2に上限値をブロードキャストします。次に、r8レジスターへ要素数を設定します。
vmovdqu32 命令で、符号付、あるいは符号なし32ビット整数型配列から16個の要素を読み込みます。そして、vpmaxsd命令あるいはvpmaxud命令で下限値へ飽和させます。次に、その結果を使って、vpminsd命令あるいはvpminud命令で上限値へ飽和させます。これによって入力の値を、下限値と上限値の範囲へ飽和させ、vmovdqu32命令で16個の要素を書き込みます。
モリーへのアクセスは [rcx+rax*4] で指定します。rcxに配列の先頭アドレスが格納されています。オフセットは、配列のインデックスを保持しているraxへ4乗じて求めます。この4は要素のバイト長、つまり32ビット整数をバイト長で表したときのバイト長です。
一回の処理で16要素を処理するため、raxレジスターに16を加算し要素数に達するまで上記処理を繰り返します。なお、先に示したvpmaxsd命令あるいはvpmaxud命令は、マクロの引数MMAXINST で置き換えられ、vpmaxsd命令あるいはvpmaxud命令はMMININST で置き換えられます。 符号なしへ対応した呼び出し側の変更は軽微ですので示しません。

64ビット浮動小数点型

呼び出し側

以降に、呼び出し側のC++コードを示します。common.hをincludeしていますがSIMDで記述したアセンブリ言語の関数と等価な機能を持つC++言語で記述した共通関数のヘッダです。最下部にソースコードを示します。条件は関数名に含まれます。

#include "..\common.h"  // TEMPLATES

#define T  double
extern "C" void satULPD(const T*, T*, T, T, size_t);

// main
int main(void)
{
    const size_t ArrLen = 4096;
    static_assert(ArrLen % 16 == 0,
        "number of elements must be an integral multiple of 16.");
    T  a[ArrLen], c[ArrLen], v[ArrLen];
    const T lower = -5000, upper = 3000;

    init(a, ArrLen);
    a[ArrLen - 1] = lower - 1;
    a[ArrLen - 2] = upper + 1;

    c_Sat  (a, c, lower, upper, ArrLen);
    satULPD(a, v, lower, upper, ArrLen);
    verify(c, v, ArrLen);
    return 0;
}

呼び出され側

呼び出し側のソースリストを示します。

; code
_TEXT   segment

        public      satULPD
        align       16
satULPD proc
        vbroadcastsd zmm1, xmm2                 ; zmm1 = lower
        vbroadcastsd zmm2, xmm3                 ; zmm2 = upper

        mov         r8, qword ptr [rsp+40]      ; length
        xor         rax, rax                    ; clear index
loop_f:
        vmovupd     zmm0, zmmword ptr [rcx+rax*8] ; load a[]
        vmaxpd      zmm0, zmm0, zmm1
        vminpd      zmm0, zmm0, zmm2
        vmovupd     zmmword ptr [rdx+rax*8], zmm0

        add         rax, 8
        cmp         rax, r8
        jb          short loop_f

        ret
satULPD endp

_TEXT   ends
        end

本関数へ渡されるレジスターとデータの関係を以降の表に示します。

レジスタ 内容
rcx 入力配列の先頭アドレス
rdx 出力配列の先頭アドレス
r8 飽和の上限値
r9 飽和の下限値
rsp+40 配列の要素数

vbroadcastsd命令で、zmm1に下限値、zmm2に上限値をブロードキャストします。次に、r8レジスターへ要素数を設定します。
vmovupd命令で、64ビット浮動小数点型から8個の要素を読み込みます。そして、vmaxpd命令で下限値へ飽和させます。次に、その結果を使って、vminpd 命令で上限値へ飽和させます。これによって入力の値を、下限値と上限値の範囲へ飽和させ、vmovupd命令で8個の要素を書き込みます。インデックスやメモリーへのアクセスなどの処理は、64ビット整数型と同様です。

このプログラムのビルドと実行の例を示します。


 実行結果(signed)

C:\>ml64 /c satULPDAsm.asm

C:\>cl /O2 /EHsc satULPD.cpp satULPDAsm.obj

C:\>satULPD
Ok!

32ビット浮動小数点型

呼び出し側のソースコードは示しません。

呼び出され側

呼び出し側のソースリストを示します。

; code
_TEXT   segment

        public      satULPS
        align       16
satULPS proc
        vbroadcastss zmm1, xmm2                 ; zmm1 = lower
        vbroadcastss zmm2, xmm3                 ; zmm2 = upper

        mov         r8, qword ptr [rsp+40]      ; length
        xor         rax, rax                    ; clear index
loop_f:
        vmovups     zmm0, zmmword ptr [rcx+rax*4] ; load a[]
        vmaxps      zmm0, zmm0, zmm1
        vminps      zmm0, zmm0, zmm2
        vmovups     zmmword ptr [rdx+rax*4], zmm0

        add         rax,16
        cmp         rax, r8
        jb          short loop_f

        ret
satULPS endp

_TEXT   ends
        end

vpbroadcastd命令で、zmm1に下限値、zmm2に上限値をブロードキャストします。次に、r8レジスターへ要素数を設定します。
vmovdqu32 命令で、符号付、あるいは符号なし32ビット整数型配列から16個の要素を読み込みます。そして、vpmaxsd命令あるいはvpmaxud命令で下限値へ飽和させます。次に、その結果を使って、vpminsd命令あるいはvpminud命令で上限値へ飽和させます。これによって入力の値を、下限値と上限値の範囲へ飽和させ、vmovdqu32命令で16個の要素を書き込みます。インデックスやメモリーへのアクセスなどは、32ビット整数型と同様です。

このプログラムのビルドと実行の例を示します。

 実行結果(signed)

C:\>ml64 /c satULPSAsm.asm

C:\>cl /O2 /EHsc satULPS.cpp satULPSAsm.obj

C:\>satULPS
Ok!