型飽和:符号付・なし型

データ型を変更するとき、特に大きなデータ型から小さなデータ型へ変換するときにオーバーフローやアンダーフローが発生しないように飽和させて代入する必要があります。ここでは、そのような型飽和処理を紹介します。
符号付、あるいは符号なし64ビット整数型の値を、符号付、あるいは符号なし32ビット整数型へ飽和させて代入するプログラムを紹介します。

符号付・なし整数型(64ビット/32ビット)

符号付・呼び出し側

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

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

#define T  long long
#define T2 long
extern "C" void q2DSat(const T*, T2*, const 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];
    T2 c[ArrLen], v[ArrLen];

    init(a, ArrLen);
    a[ArrLen - 1] = LLONG_MAX;
    a[ArrLen - 2] = LLONG_MIN;

    c_Sat<T, T2>(a, c, LONG_MAX, LONG_MIN, ArrLen);
    q2DSat(a, v, ArrLen);
    verify(c, v, ArrLen);
    return 0;
}

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

符号付・呼び出し側

符号なし64ビット整数型配列呼び出し側を示します。型以外は符号付と同じです。

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

#define T  unsigned long long
#define T2 unsigned long
extern "C" void q2UDSat(const T*, T2*, const 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];
    T2 c[ArrLen], v[ArrLen];

    init(a, ArrLen);
    a[ArrLen - 1] = ULLONG_MAX;
    a[ArrLen - 2] = 0ull;

    c_Sat<T, T2>(a, c, ULONG_MAX, 0ull, ArrLen);
    q2UDSat(a, v, ArrLen);
    verify(c, v, ArrLen);
    return 0;
}

先のプログラムは符号付64ビット整数型から符号付32ビット整数型へ飽和させるものでした。本プログラムは、符号なし64ビット整数型から符号なし32ビット整数型へ飽和させます。
まず、符号なし64ビット整数型の配列と、符号なし32ビット整数型の配列を宣言し、符号なし64ビット整数型にランダムな値を設定します。確実に関数が動作しているか確認するため、入力配列の最後に符号なし64ビット整数型の最大値と最小値を設定します。符号なしの場合、最小値はデータ型のサイズが変わっても値は 0 です。


呼び出され側

アセンブラーのコードを示します。符号ごとに関数を記述するのは面倒なのでマクロを使って記述します。

; macro
mymacro macro       MNAME, MINST

        public      MNAME
        align       16
MNAME   proc
        xor         rax, rax                    ; clear index

loop_f:
        vmovdqu64   zmm0, zmmword ptr [rcx+rax*8] ; load  a[]
        MINST       ymmword ptr [rdx+rax*4], zmm0 ; stote v[]

        add         rax, 8
        cmp         rax, r8
        jb          short loop_f

        ret
MNAME   endp

        endm

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

mymacro q2DSat,  vpmovsqd
mymacro q2UDSat, vpmovusqd

_TEXT   ends
        end

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

レジスタ 内容
rcx 入力配列の先頭アドレス(符号付、あるいは符号なし64ビット整数型)
rdx 出力配列の先頭アドレス(符号付、あるいは符号なし32ビット整数型)
r8 配列の要素数

基本的なループの構造は、これまでのプログラムと同様です。vmovdqu64 命令で、符号付、あるいは符号なし64ビット整数型配列から8個の要素を読み込みます。そして、vpmovsqd命令あるいはvpmovusqd命令で、符号付、あるいは符号なし32ビット整数型配列へ飽和させて8個の要素を書き込みます。
raxレジスターを配列のインデックスに使っています。注意しなければならないのは、入力と出力配列のデータサイスが異なりますので、読み込むときは [rcx+rax*8] としますが、書き込むときは[rdx+rax*4]としなければなりません。raxレジスターへ乗ずる値が、読み込みは 8 、書き込みは 4 です。これは、読み込みは64ビット整数型、書き込みは32ビット整数型だからです。
MINST は、vpmovsqd命令あるいはvpmovusqd命令へ置き換えられます。これはmymacro マクロの引数です。符号付・符号なしの関数で異なるのは1命令のみですので、マクロで記述し命令を置き換えます。これによって冗長なコードを記述するのを避けます。


 実行結果(signed)

C:\>ml64 /c Q2DsatAsm.asm
Microsoft (R) Macro Assembler (x64) Version 14.37.32822.0
Copyright (C) Microsoft Corporation. All rights reserved.

Assembling: Q2DsatAsm.asm

C:\>cl /O2 /EHsc Q2Dsat.cpp Q2DsatAsm.obj
Microsoft(R) C/C++ Optimizing Compiler Version 19.37.32822 for x64
Copyright (C) Microsoft Corporation. All rights reserved.

Q2Dsat.cpp
Microsoft (R) Incremental Linker Version 14.37.32822.0
Copyright (C) Microsoft Corporation. All rights reserved.

/out:Q2Dsat.exe
Q2Dsat.obj
Q2DsatAsm.obj

C:\>Q2Dsat
Ok!


 実行結果(unsigned)

C:\>ml64 /c Q2DsatAsm.asm
C:\>cl /O2 /EHsc Q2UDsat.cpp Q2DsatAsm.obj
C:\>Q2UDsat
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, typename T2>
void c_Sat(const T* a, T2* v, const T max,
                            const T min, const size_t length)
{
    T r;
    for (size_t i = 0; i < length; i++)
    {
        r = a[i] > max ? max : a[i];
        r = r < min ? min : r;
        v[i] = (T2)r;
    }
}


template <typename T, typename T2>
void c_addSat(const T* a, const T* b, T* v, const T max,
                            const T min, const size_t length)
{
    for (size_t i = 0; i < length; i++)
    {
        T2 r = a[i] + b[i];
        r = r > max ? max : r;
        r = r < min ? min : r;
        v[i] = (T)r;
    }
}

template <typename T, typename T2>
void c_subSat(const T* a, const T* b, T* v, const T max,
                            const T min, const size_t length)
{
    for (size_t i = 0; i < length; i++)
    {
        T2 r = a[i] - b[i];
        r = r > max ? max : r;
        r = r < min ? min : r;
        v[i] = (T)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);
}

符号付・なし整数型(64ビット/16ビット)

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

呼び出され側

符号付、あるいは符号なし64ビット整数型の値を、符号付、あるいは符号なし16ビット整数型へ飽和させて代入するプログラムを紹介します。以降に、ソースコードを示します。

; macro
mymacro macro       MNAME, MINST

        public      MNAME
        align       16
MNAME   proc
        xor         rax, rax                    ; clear index

loop_f:
        vmovdqu64   zmm0, zmmword ptr [rcx+rax*8] ; load  a[]
        MINST       xmmword ptr [rdx+rax*2], zmm0 ; stote v[]

        add         rax, 8
        cmp         rax, r8
        jb          short loop_f

        ret
MNAME   endp

        endm

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

mymacro q2WSat,  vpmovsqw
mymacro q2UWSat, vpmovusqw

_TEXT   ends
        end

最初のプログラムと異なる行を示します。
MINST xmmword ptr [rdx+rax*2], zmm0 ; stote v[]
mymacro q2WSat, vpmovsqw
mymacro q2UWSat, vpmovusqw

この部分が異なるだけで、ほとんど最初のプログラムと同様です。vpmovsqw 命令あるいはvpmovusqw命令で、符号付あるいは符号なし64ビット整数型配列から、符号付あるいは符号なし16ビット整数型配列へ飽和させて16個の要素を書き込みます。raxレジスターを配列のインデックスに使っています。読み込みは最初のプログラムと同じですが( [rcx+rax*8] )、書き込むときは[rdx+rax*2]としなければなりません。raxレジスターへ乗ずる値が、読み込みは 8 、書き込みは 2 です。これは、読み込みは64ビット整数型で8バイト長であり、書き込みは16ビット整数型で2バイト長だからです。
MINST は、vpmovsqw命令あるいはvpmovusqw命令へ置き換えられます。これはmymacro マクロの引数です。


符号付・なし整数型(64ビット/8ビット)

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

呼び出され側

符号付、あるいは符号なし64ビット整数型の値を、符号付、あるいは符号なし8ビット整数型へ飽和させて代入するプログラムを紹介します。以降に、ソースコードの一部を示します。

; macro
mymacro macro       MNAME, MINST
        :
        vmovdqu64   zmm0, zmmword ptr [rcx+rax*8] ; load  a[]
        MINST       qword ptr [rdx+rax], zmm0     ; stote v[]

        :
_TEXT   segment

mymacro q2BSat,  vpmovsqb
mymacro q2UBSat, vpmovusqb
        :

最初のプログラムと同様なため、異なる部分のみを示します。raxレジスターを配列のインデックスに使っています。読み込みは最初のプログラムと同じですが( [rcx+rax*8] )、書き込むときは[rdx+rax]としなければなりません。これは、読み込みは64ビット整数型で8バイト長であり、書き込みは8ビット整数型で1バイト長だからです。
MINST は、vpmovsqb 命令あるいはvpmovusqb命令へ置き換えられます。これはmymacro マクロの引数です。符号付あるいは符号なし64ビット整数型配列から、符号付あるいは符号なし8ビット整数型配列へ飽和させて32個の要素を書き込みます。

符号付・なし整数型(32ビット/16ビット)

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

呼び出され側

符号付、あるいは符号なし32ビット整数型の値を、符号付、あるいは符号なし16ビット整数型へ飽和させて代入するプログラムを紹介します。以降に、ソースコードの一部を示します。

mymacro macro       MNAME, MINST
        :
loop_f:
        vmovdqu32   zmm0, zmmword ptr [rcx+rax*4] ; load  a[]
        MINST       ymmword ptr [rdx+rax*2], zmm0 ; stote v[]

        add         rax, 16
        cmp         rax, r8
        jb          short loop_f
        :
_TEXT   segment

mymacro d2WSat,  vpmovsdw
mymacro d2UWSat, vpmovusdw

_TEXT   ends
        end

最初のプログラムと同様なため、異なる部分のみを示します。入力のデータ型が32ビット整数型へ変わるため、読み込む際の命令がvmovdqu32 へ変わります。書き込むときは、データ型が16ビット整数型なため[rdx+rax*2]としなければなりません。これは、読み込みは32ビット整数型、書き込みは16ビット整数型だからです。
MINST は、vpmovsdw命令あるいはvpmovusdw命令へ置き換えられます。これはmymacro マクロの引数です。符号付あるいは符号なし32ビット整数型配列から、符号付あるいは符号なし16ビット整数型配列へ飽和させて16個の要素を書き込みます。これよりループごとに、インデックスに使用するraxレジスターへ16を加算します。

符号付・なし整数型(32ビット/8ビット)

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

呼び出され側

符号付、あるいは符号なし32ビット整数型の値を、符号付、あるいは符号なし8ビット整数型へ飽和させて代入するプログラムを紹介します。以降に、ソースコードの一部を示します。

mymacro macro       MNAME, MINST
        :
        vmovdqu32   zmm0, zmmword ptr [rcx+rax*4] ; load  a[]
        MINST       xmmword ptr [rdx+rax], zmm0   ; stote v[]
        :
_TEXT   segment

mymacro d2BSat,  vpmovsdb
mymacro d2UBSat, vpmovusdb
        :

前節のプログラムと同様なため、異なる部分のみを示します。raxレジスターを配列のインデックスに使っています。読み込みは前節のプログラムと同じですが( [rcx+rax*4] )、書き込むときは[rdx+rax]としなければなりません。これは、読み込みは32ビット整数型、書き込みは8ビット整数型だからです。
MINST は、vpmovsdb命令あるいはvpmovusdb命令へ置き換えられます。これはmymacro マクロの引数です。符号付あるいは符号なし32ビット整数型配列から、符号付あるいは符号なし8ビット整数型配列へ飽和させて16個の要素を書き込みます。

符号付・なし整数型(16ビット/8ビット)

呼び出され側

符号付、あるいは符号なし16ビット整数型の値を、符号付、あるいは符号なし8ビット整数型へ飽和させて代入するプログラムを紹介します。以降に、ソースコードの一部を示します。

mymacro macro       MNAME, MINST
        :
loop_f:
        vmovdqu8    zmm0, zmmword ptr [rcx+rax*2] ; load  a[]
        MINST       ymmword ptr [rdx+rax], zmm0   ; stote v[]

        add         rax, 32
        cmp         rax, r8
        jb          short loop_f
        :
_TEXT   segment

mymacro d2BSat,  vpmovswb
mymacro d2UBSat, vpmovuswb
        :

前のプログラムと同様なため、異なる部分のみを示します。raxレジスターを配列のインデックスに使っています。読み込みは前節のプログラムと代わり( [rcx+rax*4] )を指定します。vmovdqu8 命令に変わり 、書き込むときは[rcx+rax*2]としなければなりません。これは、読み込みは16ビット整数型、書き込みは8ビット整数型だからです。
MINST は、vpmovswb命令あるいはvpmovuswb命令へ置き換えられます。これはmymacro マクロの引数です。符号付あるいは符号なし16ビット整数型配列から、符号付あるいは符号なし8ビット整数型配列へ飽和させて32個の要素を書き込みます。

符号付・なし16ビット整数型加算・減算

呼び出され側

加算や減算を行い、その結果を飽和させるものを紹介します。最初は16ビット整数型を扱うものを紹介します。符号付・なし16ビット整数型を加算、あるいは減算します。その結果が符号付・なし16ビット整数型の範囲を超えている場合は、16ビット整数型に収まるように飽和させます(7FFFh~8000h、-32768~32767、あるいは0~65535の範囲外なら飽和させる)。以降に、ソースコードを示します。

mymacro macro       MNAME, MINST

        public      MNAME
        align       16
MNAME   proc
        xor         rax, rax                    ; clear index

loop_f:
        vmovdqu16   zmm0, zmmword ptr [rcx+rax*2]       ; load  a[]
        MINST       zmm0, zmm0, zmmword ptr [rdx+rax*2] ; zmm0 = a[] <+|-> b[]
        vmovdqu16   zmmword ptr [r8+rax*2], zmm0        ; store zmm0

        add         rax, 32
        cmp         rax, r9
        jb          short loop_f

        ret
MNAME   endp

        endm

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

mymacro addWsat,  vpaddsw
mymacro addUWsat, vpaddusw

mymacro subWsat,  vpsubsw
mymacro subUWsat, vpsubusw

_TEXT   ends
        end

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

レジスタ 内容
rcx 入力配列の先頭アドレス(符号付、あるいは符号なし16ビット整数型)
rdx 入力配列の先頭アドレス(符号付、あるいは符号なし16ビット整数型)
r8 出力配列の先頭アドレス(符号付、あるいは符号なし16ビット整数型)
r9 配列の要素数

基本的なループの構造は、これまでのプログラムと同様です。vmovdqu16 命令で、符号付、あるいは符号なし16ビット整数型配列から32個の要素を読み込みます。そして、vpaddsw命令、vpaddusw命令、vpsubsw命令、あるいはvpsubusw命令で、符号付、あるいは符号なし16ビット整数型配列の各要素を加算、あるいは減算します。結果が16ビット整数型の表現できる範囲を超えたときは飽和させます。最後に、vmovdqu16 命令で結果を書き込みます。raxレジスターを配列のインデックスに使っていまので、読み書きは [rcx+rax*2] とします。raxレジスターへ乗ずる値は一回で32要素を処理するため、32を加算します。
MINST は、先に説明したように、vpaddsw命令、vpaddusw命令、vpsubsw命令、あるいはvpsubusw命令へ置き換えられます。

以降に、アセンブリ言語で記述した関数と同等の機能を記述したC++言語で開発した関数を示します。ここでは、加算をC++言語で記述したものを示します。

    :
template <typename T, typename T2>
void c_addSat(const T* a, const T* b, T* v, const T max,
                            const T min, const size_t length)
{
    for (size_t i = 0; i < length; i++)
    {
        T2 r = a[i] + b[i];
        r = r > max ? max : r;
        r = r < min ? min : r;
        v[i] = (T)r;
    }
}
    :

符号付・呼び出し側

次に、符号付に対応した呼び出し側のソースリストを示します。

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

#define T  short
#define T2 long
extern "C"
{
    void addWsat(const T*, const T*, T*, const size_t);
    void subWsat(const T*, const T*, T*, const 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], b[ArrLen], c[ArrLen], v[ArrLen];

    init(a, b, ArrLen);

    addWsat(a, b, v, ArrLen);
    c_addSat<T, T2>(a, b, c, SHRT_MAX, SHRT_MIN, ArrLen);
    verify(c, v, ArrLen);

    c_subSat<T, T2>(a, b, c, SHRT_MAX, SHRT_MIN, ArrLen);
    subWsat(a, b, v, ArrLen);
    verify(c, v, ArrLen);
    return 0;
}

符号付16ビット整数型の配列をいくつか宣言し、入力使用する2つの配列にランダムな値を設定します。次にアセンブリ言語で記述した関数とC++言語で開発した関数を呼び出します。アセンブリ言語で記述した関数に渡すデータ型は符号付16ビット整数ですが、C++言語で開発した関数へは、中間値を保持するデータ型を指定します。アセンブリ言語で記述した関数は命令そのもので飽和させますが、C++言語で開発した関数は中間値を保持したのち飽和処理します。このため、中間値を保持するデータ型を指定する必要があります。また、C++言語で開発した関数へは、飽和させる範囲を渡します。飽和の範囲は、SHRT_MAX ~ SHRT_MINです。
最後に、両方の関数を呼び出し、得られた結果をverify関数で検証します。
このプログラムのビルドと実行の例を示します。


 実行結果

C:\>ml64 /c addsubWsatAsm.asm

C:\>cl /O2 /EHsc addsubWsat.cpp addsubWsatAsm.obj

C:\>addsubWsat
Ok!
Ok!

正常に処理されているのが分かります。

符号なし・呼び出し側

符号なしへ対応した呼び出し側のソースリストを示します。

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

#define T  unsigned short
#define T2 long
extern "C"
{
    void addUWsat(const T*, const T*, T*, const size_t);
    void subUWsat(const T*, const T*, T*, const 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], b[ArrLen], c[ArrLen], v[ArrLen];

    init(a, b, ArrLen);

    addUWsat(a, b, v, ArrLen);
    c_addSat<T, T2>(a, b, c, USHRT_MAX, 0u, ArrLen);
    verify(c, v, ArrLen);

    c_subSat<T, T2>(a, b, c, USHRT_MAX, 0u, ArrLen);
    subUWsat(a, b, v, ArrLen);
    verify(c, v, ArrLen);
    return 0;
}

先のプログラムは符号付16ビット整数型へ対応したものでした。本プログラムは、符号なし16ビット整数型へ対応させたものです。アセンブリ言語で記述した関数は命令そのもので飽和させますが、C++言語で開発した関数は中間値を保持したのち飽和処理します。このため、飽和させる範囲を指定します。符号付16ビット整数型の範囲は飽和の範囲は、SHRT_MAX ~ SHRT_MINでしたが、符号なし16ビット整数型は、USHRT_MAX ~ 0u です。

符号付・なし8ビット整数型加算・減算

呼び出され側

直前のプログラムは16ビット整数型へ対応していました。ここでは、8ビット整数型へ対応した、加算や減算を行い、結果を飽和させるライブラリを紹介します。符号付・なし8ビット整数型を加算、あるいは減算します。その結果が符号付・なし8ビット整数型の範囲を超えている場合は、8ビット整数型に収まるように飽和させます(-128~127、あるいは0~255の範囲外なら飽和させる)。以降に、ソースコードの一部を示します。

        :
loop_f:
        vmovdqu8    zmm0, zmmword ptr [rcx+rax]      ; load  a[]
        MINST       zmm0, zmm0, zmmword ptr [rdx+rax]; zmm0 = a[] <+|-> b[]
        vmovdqu8    zmmword ptr [r8+rax], zmm0       ; store zmm0

        add         rax, 64
        cmp         rax, r9
        jb          short loop_f
        :
_TEXT   segment

mymacro addBsat,  vpaddsb
mymacro addUBsat, vpaddusb

mymacro subBsat,  vpsubsb
mymacro subUBsat, vpsubusb
        :

基本的なループの構造は、これまでのプログラムと同様です。vmovdqu8命令で、符号付、あるいは符号なし8ビット整数型を配列から64要素読み込みます。そして、vpaddsb命令、vpaddusb命令、vpsubsb命令、あるいはvpsubusb命令で、符号付、あるいは符号なし8ビット整数型配列の各要素を加算、あるいは減算します。結果が8ビット整数型の表現できる範囲を超えたときは飽和させます。最後に、vmovdqu8命令で結果を書き込みます。raxレジスターを配列のインデックスに使っていまので、読み書きは [rcx+rax] とします。raxレジスターへ乗ずる値は一回で64要素を処理するため、64を加算します。
MINST は、先に説明したように、vpaddsb命令、vpaddusb命令、vpsubsb命令、あるいはvpsubusb命令へ置き換えられます。

符号付・呼び出し側

次に、符号付に対応した呼び出し側のソースリストを示します。

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

#define T  char
#define T2 short
extern "C"
{
    void addBsat(const T*, const T*, T*, const size_t);
    void subBsat(const T*, const T*, T*, const 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], b[ArrLen], c[ArrLen], v[ArrLen];

    init(a, b, ArrLen);

    addBsat(a, b, v, ArrLen);
    c_addSat<T, T2>(a, b, c, CHAR_MAX, CHAR_MIN, ArrLen);
    verify(c, v, ArrLen);

    c_subSat<T, T2>(a, b, c, CHAR_MAX, CHAR_MIN, ArrLen);
    subBsat(a, b, v, ArrLen);
    verify(c, v, ArrLen);
    return 0;
}

符号付8ビット整数型の配列をいくつか宣言し、入力使用する2つの配列にランダムな値を設定します。次にアセンブリ言語で記述した関数とC++言語で開発した関数を呼び出します。アセンブリ言語で記述した関数に渡すデータ型は符号付8ビット整数ですが、C++言語で開発した関数へは、中間値を保持するデータ型を指定します。アセンブリ言語で記述した関数は命令そのもので飽和させますが、C++言語で開発した関数は中間値を保持したのち飽和処理します。このため、中間値を保持するデータ型を指定する必要があります。また、C++言語で開発した関数へは、飽和させる範囲を渡します。飽和の範囲は、CHAR_MAX ~ CHAR_MINです。
最後に、両方の関数を呼び出し、得られた結果をverify関数で検証します。

符号なし・呼び出し側

今度は、符号なしへ対応した呼び出し側のソースリストを示します。

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

#define T  unsigned char
#define T2 short
extern "C"
{
    void addUBsat(const T*, const T*, T*, const size_t);
    void subUBsat(const T*, const T*, T*, const 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], b[ArrLen], c[ArrLen], v[ArrLen];

    init(a, b, ArrLen);

    addUBsat(a, b, v, ArrLen);
    c_addSat<T, T2>(a, b, c, UCHAR_MAX, 0, ArrLen);
    verify(c, v, ArrLen);

    c_subSat<T, T2>(a, b, c, UCHAR_MAX, 0, ArrLen);
    subUBsat(a, b, v, ArrLen);
    verify(c, v, ArrLen);
    return 0;
}

基本的に直前のプログラムと同様です。異なるのは飽和させる範囲がUCHAR_MAX ~ 0 へ変わることくらいです。

性能評価

「符号付・なし整数型(64ビット/16ビット)」で開発したアセンブリ言語で記述した関数を使用し、性能を観察します。以降に、性能評価に使用した呼び出し側のソースリストを示します。

呼び出し側・符号付整数型(64ビット/16ビット)

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

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

// main
int main(void)
{
    const size_t ArrLen = 32768;
    static_assert(ArrLen % 16 == 0,
        "number of elements must be an integral multiple of 16.");

    try
    {
        T* a = (T*)_mm_malloc(sizeof(T) * ArrLen, 64);
        T2* c = (T2*)_mm_malloc(sizeof(T2) * ArrLen, 64);
        T2* v = (T2*)_mm_malloc(sizeof(T2) * ArrLen, 64);

        clock_t start;

        #define LOOPS   10000
        init(a, ArrLen);
        a[ArrLen - 1] = LLONG_MAX;
        a[ArrLen - 2] = LLONG_MIN;

        start = clock();
        for (int j = 0; j < LOOPS;j++)
            c_Sat<T, T2>(a, c, LONG_MAX, LONG_MIN, ArrLen);
        print_elTime("cpp: ", start, clock());

        start = clock();
        for (int j = 0; j < LOOPS;j++)
            q2DSat(a, v, ArrLen);
        print_elTime("asm: ", start, clock());

        verify(c, v, ArrLen);

        _mm_free(a);
        _mm_free(c);
        _mm_free(v);
    }
    catch (char* str)
    {
        cerr << str << endl;
    }
    return 0;
}

符号付の64ビット整数型配列を、符号付の32ビット整数型配列を飽和代入します。ひとつの符号付の64ビット整数型配列へランダムな値を設定し、それをアセンブリ言語で記述した関数とC++言語で開発した関数で符号付の32ビット整数型配列を飽和代入、両者の結果に違いがないか検査します。同時に、clock関数を用いて、両者の処理に要した時間を計測します。

上記プログラムをビルド&実行したときの性能を示します。プログラムを起動すると、それぞれに要した時間が表示されます。横軸は比較の種類、縦軸は処理に要した時間(ミリ秒)です。

上記プログラムをビルド&実行したときの性能を示します。プログラムを起動すると、それぞれに要した時間が表示されます。横軸は比較の種類、縦軸は処理に要した時間(ミリ秒)です。

同じような観察を符号なしで行ってみます。符号なしの64ビット整数型配列を、符号付の32ビット整数型配列を飽和代入します。以降に、性能評価に称した呼び出し側のソースリストの一部を示します。以降に、性能評価に使用した呼び出し側のソースリストを示します。

呼び出し側・符号なし整数型(64ビット/16ビット)

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

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

        :
        init(a, ArrLen);
        a[ArrLen - 1] = ULLONG_MAX;
        a[ArrLen - 2] = 0ull;

        start = clock();
        for (int j = 0; j < LOOPS;j++)
            c_Sat<T, T2>(a, c, ULONG_MAX, 0ull, ArrLen);
        print_elTime("cpp: ", start, clock());

        start = clock();
        for (int j = 0; j < LOOPS;j++)
            q2UDSat(a, v, ArrLen);
        print_elTime("asm: ", start, clock());
        :

先のプログラムに近いため異なる部分のみ示します。先ほどと同様に、clock関数を用いて、両者の処理に要した時間を計測します。

プログラムをビルド&実行したときの性能を示します。プログラムを起動すると、それぞれに要した時間が表示されます。

アセンブリ言語で開発した関は、C++言語で開発した関数より4.6倍強高速です。