抽出

一次元配列の各要素を検査し、条件を満足していたら、その要素を抽出します。ここでは任意の値以上の要素を抽出する、任意の値以下の要素を抽出する、任意範囲の要素を抽出する例を示します。以降に処理概要を図で示します。

符号付64ビット整数型

呼び出し側・符号付

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

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

#define T long long
extern "C"
{
    int extQLow (const T*, T*, const T, size_t);
    int extQHigh(const T*, T*, const T, size_t);
    int extQ    (const T*, T*, const T, size_t, const T);
}

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 low = -200, high = 5000;

    cout << endl << "[ v < " << low << "]" << endl;
    init(a, ArrLen);
    int cLen = cextractLow(a, c, low, ArrLen);
    int aLen = extQLow    (a, v, low, ArrLen);
    verify(cLen, aLen, c, v, ArrLen);

    cout << endl << "[ v > " << high << "]" << endl;
    cLen = cextractHigh(a, c, high, ArrLen);
    aLen = extQHigh    (a, v, high, ArrLen);
    verify(cLen, aLen, c, v, ArrLen);

    cout << endl << "[ " << low << " < v < " << high << "]" << endl;
    cLen = cextract(a, c, low, ArrLen, high);
    aLen = extQ    (a, v, low, ArrLen, high);
    verify(cLen, aLen, c, v, ArrLen);
    return 0;
}

簡単な処理ですので説明は省きます。

呼び出され側

本関数へ渡される内容とレジスターの対応を表で示します。

レジスタ 内容
rcx 入力配列 a の先頭アドレス
rdx 出力配列 c の先頭アドレス
r8 任意の値、あるいは下限値(範囲指定の時)
r9 入力配列 a の要素数
[rsp+40] オプション、上限値(範囲指定の時)

引数が可変なため少し面倒です。このライブラリは、一次元配列の1)任意の値以上、あるいは2)任意の値以下の要素を抽出するものと、3)任意の範囲の要素を抽出します。任意の値以上、あるいは任意の値以下の要素を抽出するときは、任意の値は一つだけ必要です。しかし、任意の範囲の要素を抽出するには、二つの値が必要です。このため、上表を各パターンに分けると以下のようになります。

任意の値以上の要素を抽出、任意の値以下の要素を抽出

レジスタ 内容
rcx 入力配列 a の先頭アドレス
rdx 出力配列 c の先頭アドレス
r8 任意の値
r9 入力配列 a の要素数

任意範囲の要素を抽出

rcx 入力配列 a の先頭アドレス
rdx 出力配列 c の先頭アドレス
r8 下限値
r9 入力配列 a の要素数
[rsp+40] 上限値

64ビット整数型配列の要素を比較し、条件に合う値を取り出すライブラリを、マクロを使って開発したものを示します。本関数は、符号なし/符号付の両方に対応します。かつ、任意の値以上、任意の値以下、そして任意範囲の要素を抽出する6つのライブラリへ展開されます。

    :
_MM_CMPINT_LT   EQU 1   ; - より小さい    <
_MM_CMPINT_GT   EQU 6   ; - より大きい    >
    :
mymacro macro       MNAME, CINST, CONDITION, CONDITION2

        public      MNAME
        align       16
MNAME   proc
        vpbroadcastq zmm1, r8               ; zmm1 = low/high
IFDEF CONDITION2
        vpbroadcastq zmm2, qword ptr [rsp+40]; zmm2 = high
ENDIF
        xor         r8,  r8                 ; init retrurn value = 0
        xor         rax, rax                ; clear in  index

loop_f:
        vmovdqu64   zmm0, zmmword ptr [rcx+rax*8]; load a[]
        CINST       k1, zmm0, zmm1, CONDITION
IFDEF CONDITION2
        CINST       k2, zmm0, zmm2, CONDITION2
        kandb       k1, k2, k1
ENDIF
        vpcompressq zmmword ptr [rdx+r8*8]{k1}, zmm0 ; store zmm0 to &c[r8]

        kmovb       r10, k1                 ; move mask bit
        popcnt      r10, r10
        add         r8, r10                 ; update retrurn value

        add         rax, 8
        cmp         rax, r9
        jb          short loop_f

        mov         eax, r8d                ; return value

        ret
MNAME   endp

        endm

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

;QWORD
mymacro extQLow,   vpcmpq,  _MM_CMPINT_LT                ; <
mymacro extQHigh,  vpcmpq,  _MM_CMPINT_GT                ; >
mymacro extQ,      vpcmpq,  _MM_CMPINT_GT, _MM_CMPINT_LT ; <>

;QWORD Unsigned
mymacro extUQLow,  vpcmpuq, _MM_CMPINT_LT                ; <
mymacro extUQHigh, vpcmpuq, _MM_CMPINT_GT                ; >
mymacro extUQ,     vpcmpuq, _MM_CMPINT_GT, _MM_CMPINT_LT ; <>

_TEXT   ends
        end

mymacro マクロを定義し、これを使ってextQLow、extQHigh、extQ、extUQLow、extUQHigh、extUQ関数を作ります。各関数の機能を表で示します。

関数名 機能
extQLow 一次元符号付64ビット整数配列から、任意の値以下の要素を抽出する。
extQHigh 一次元符号付64ビット整数配列から、任意の値以上の要素を抽出する。
extQ 一次元符号付64ビット整数配列から、任意範囲の要素を抽出する。
extUQLow 一次元符号なし64ビット整数配列から、任意の値以下の要素を抽出する。
extUQHigh 一次元符号なし64ビット整数配列から、任意の値以上の要素を抽出する。
extUQ 一次元符号なし64ビット整数配列から、任意範囲の要素を抽出する。


分かり易くするため、extQLowが展開されたソースリストを示します。

        public      extQLow
        align       16
extQLow proc
        vpbroadcastq zmm1, r8               ; zmm1 = low/high
        xor         r8,  r8                 ; init retrurn value = 0
        xor         rax, rax                ; clear index

loop_f:
        vmovdqu64   zmm0, zmmword ptr [rcx+rax*8]; load a[]
        vpcmpq      k1, zmm0, zmm1, _MM_CMPINT_LT
        vpcompressq zmmword ptr [rdx+r8*8]{k1}, zmm0 ; store zmm0 to &c[r8]

        kmovb       r10, k1                 ; move mask bit
        popcnt      r10, r10
        add         r8, r10                 ; update retrurn value

        add         rax, 8
        cmp         rax, r9
        jb          short loop_f

        mov         eax, r8d                ; return value

        ret
extQLow endp

まず、vpbroadcastq命令でr8 レジスターの値をzmm1レジスターへブロードキャストします。r8 レジスターは下限値を示す任意の値が格納されています。これによって、zmm1レジスターの8要素がr8 レジスターで埋められます。次にr8 レジスターとrax レジスターをxor命令でゼロクリアします。r8 レジスターは抽出した要素の数を、raxレジスターは一次元配列を参照するためのインデックスを保持します。
前準備が完了したためループへ入ります。zmm1レジスターの値と配列の8要素を順次比較します。vpcmpq命令で条件に合致する要素のビットがオンになるマスクをk1マスクレジスターへ得ます。

このマスクレジスターをvpcompressq命令に指定し、条件を満足する要素を抽出します。

一次元配列は8要素単に処理しますが、抽出する要素がいくつになるか不明です。そこでk1レジスターをr10レジスターへ移動し、popcnt命令でオンになっているビット数を数えます。この値は抽出した要素数を示します。この値をr8レジスターへ加算し、出力配列用のインデックスを管理します。
次に、入力配列用インデックスを保持するraxレジスターへ8を加算し、これを配列の要素数を保持するr9レジスターと比較し、すべての要素を処理したか調べます。処理が終わっていなければ、次の要素の処理へ移ります。全要素の処理が完了したら、r8に格納されている抽出した要素数を、eaxレジスターへ移動し処理を終了します。eaxレジスターは関数の戻り値であるint型の値です。

common.h、共通関数

ヘッダーファイルの一部を示します。簡単な内容なので、説明は省きします。

    :
// C++
template <typename T>
int cextractLow(const T* a, T* v,
                        const T cmpValue, const size_t length)
{
    int rIndex = 0;
    for (size_t i = 0; i < length; i++)
    {
        if (a[i] < cmpValue)
            v[rIndex++] = a[i];
    }
    return rIndex;
}

// C++
template <typename T>
int cextractHigh(const T* a, T* v,
                        const T cmpValue, const size_t length)
{
    int rIndex = 0;
    for (size_t i = 0; i < length; i++)
    {
        if (a[i] > cmpValue)
            v[rIndex++] = a[i];
    }
    return rIndex;
}


// C++
template <typename T>
int cextract(const T* a, T* v, const T cmpLow,
                            const size_t length, const T cmpHigh)
{
    int rIndex = 0;
    for (size_t i = 0; i < length; i++)
    {
        if (a[i] > cmpLow && a[i] < cmpHigh)
            v[rIndex++] = a[i];
    }
    return rIndex;
}
    :

すべての型に対応させるため、テンプレート関数とします。cextractLow関数は、一次元配列から任意の値以上の要素を抽出します。cextractHigh関数は、一次元配列から任意の値以下の要素を抽出します。cextract関数は、一次元配列から任意の範囲の要素を抽出します。

このプログラムのビルドし、実行した様子を示します。

 実行結果

[ v < -200]
cpp = 1997
asm = 1997

[ v > 5000]
cpp = 1421
asm = 1421

[ -200 < v < 5000]
cpp = 678
asm = 678

C++で処理した結果と、アセンブリ言語で開発したプログラムが同じ結果です。正常に処理されたようです。

念のため、任意範囲の要素を抽出するextQへ展開された関数のソースリストを示します。

        public      extQ
        align       16
extQ    proc
        vpbroadcastq zmm1, r8               ; zmm1 = low/high
        vpbroadcastq zmm2, qword ptr [rsp+40]; zmm2 = high

        xor         r8,  r8                 ; init retrurn value = 0
        xor         rax, rax                ; clear index

loop_f:
        vmovdqu64   zmm0, zmmword ptr [rcx+rax*8]; load a[]
        vpcmpq      k1, zmm0, zmm1, _MM_CMPINT_GT
        vpcmpq      k2, zmm0, zmm2, _MM_CMPINT_LT
        kandb       k1, k2, k1
        vpcompressq zmmword ptr [rdx+r8*8]{k1}, zmm0 ; store zmm0 to &c[r8]

        kmovb       r10, k1                 ; move mask bit
        popcnt      r10, r10
        add         r8, r10                 ; update retrurn value

        add         rax, 8
        cmp         rax, r9
        jb          short loop_f

        mov         eax, r8d                ; return value

        ret
extQ    endp

zmm1レジスターの値と配列の8要素をvpcmpq命令で2回比較し、k1とk2マスクレジスターに結果を求めます。kandb命令で、両方を満足する条件をk1マスクレジスターへ得ます。以降は、先ほどと同様です。

ほかのデータ型やライブラリ一覧

符号付64ビット整数型に続き、符号なし64ビット整数型、符号付32ビット整数型、符号なし32ビット整数型、符号付16ビット整数型、符号なし16ビット整数型、符号付8ビット整数型、符号なし8ビット整数型、64ビット浮動小数点型、そして32ビット浮動小数点型を用意します。各データ型のソースコードは、これまでと同様な違いがあるだけですので、説明もソースコードも示ししません。