抽出・インデックス

一次元配列の各要素を検査し、条件を満足していたら、その要素を抽出します。当時に抽出した要素の位置を得ます。ここでは任意の値以上の要素を抽出、任意の値以下の要素を抽出、任意範囲の要素を抽出するとともに、そのインデックスを得る例を示します。前の関数は要素だけを抽出しましたが、本関数は要素に加え、その要素の位置(インデックス値)を抽出します。以降に処理概要を図で示します。

符号付64ビット整数型

呼び出し側・符号付

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

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

#define T long long
extern "C"
{
    int extWidxQLow (const T*, T*, int*, const T, size_t );
    int extWidxQHigh(const T*, T*, int*, const T, size_t );
    int extWidxQ    (const T*, T*, int*, 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];
    int cIdx[ArrLen], vIdx[ArrLen];
    const T low = -50, high = 50;

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

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

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

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

呼び出され側

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

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

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

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

rcx 入力配列 a の先頭アドレス
rdx 出力配列 c の先頭アドレス
r8 インデックスを格納する配列 vIdx の先頭アドレス
r9 任意値
[rsp+40] 入力配列 a の要素数


任意範囲の要素を抽出

rcx 入力配列 a の先頭アドレス
rdx 出力配列 v の先頭アドレス
r8 インデックスを格納する配列 vIdx の先頭アドレス
r9 下限値
[rsp+40] 入力配列 a の要素数
[rsp+48] 上限値

4ビット整数型配列の要素を比較し、条件に合う値を取り出すライブラリを、マクロを使って開発したものを示します。本関数は、符号なし/符号付の両方に対応します。かつ、任意の値以上、任意の値以下、そして任意範囲の要素を抽出する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, r9               ; zmm1 = low/high
        mov         r11, qword ptr [rsp+40] ; length
IFDEF CONDITION2
        vpbroadcastq zmm2, qword ptr [rsp+48]; zmm2 = high
ENDIF
        vmovdqu32   ymm3, ymmword ptr index ; ymm3 = index
        mov         r9, 8
        vpbroadcastd ymm4, r9               ; ymm4 = 8,...,8

        xor         r9,  r9                 ; 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+r9*8]{k1}, zmm0 ; store zmm0 to &v[r9]
        vpcompressd ymmword ptr [r8 +r9*4]{k1}, ymm3 ; store ymm3 to &vIdx[r9]
        vpaddd      ymm3, ymm3, ymm4        ; update index

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

        add         rax, 8
        cmp         rax, r11
        jb          short loop_f

        mov         eax, r9d                ; return value

        ret
MNAME   endp

        endm

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

;QWORD
mymacro extWidxQLow,   vpcmpq,  _MM_CMPINT_LT                ; <
mymacro extWidxQHigh,  vpcmpq,  _MM_CMPINT_GT                ; >
mymacro extWidxQ,      vpcmpq,  _MM_CMPINT_GT, _MM_CMPINT_LT ; <>

;QWORD Unsigned
mymacro extWidxUQLow,  vpcmpuq, _MM_CMPINT_LT                ; <
mymacro extWidxUQHigh, vpcmpuq, _MM_CMPINT_GT                ; >
mymacro extWidxUQ,     vpcmpuq, _MM_CMPINT_GT, _MM_CMPINT_LT ; <>

_TEXT   ends

;.data
__DATA  segment         ;.data
index   dd      0, 1, 2, 3, 4, 5, 6, 7
__DATA  ends
        end 

mymacro マクロを定義し、これを使ってextWidxQLow、extWidxQHigh、extWidxQ、extWidxUQLow、extWidxUQHigh、extWidxUQ関数を作ります。各関数の機能を表で示します。

関数名 機能
extWidxQLow 一次元符号付64ビット整数配列から、任意の値以下の要素と、そのインデックス値を抽出する。
extWidxQHigh 一次元符号付64ビット整数配列から、任意の値以上の要素と、そのインデックス値を抽出する。
extWidxQ 一次元符号付64ビット整数配列から、任意範囲の要素と、そのインデックス値を抽出する。
extWidxUQLow 一次元符号なし64ビット整数配列から、任意の値以下の要素と、そのインデックス値を抽出する。
extWidxUQHigh 一次元符号なし64ビット整数配列から、任意の値以上の要素と、そのインデックス値を抽出する。
extWidxUQ 一次元符号なし64ビット整数配列から、任意範囲の要素と、そのインデックス値を抽出する。


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

        public      extWidxQLow
        align       16
extWidxQLow proc
        vpbroadcastq zmm1, r9               ; zmm1 = low/high
        mov         r11, qword ptr [rsp+40] ; length
        vmovdqu32   ymm3, ymmword ptr index ; ymm3 = index
        mov         r9, 8
        vpbroadcastd ymm4, r9               ; ymm4 = 8,...,8

        xor         r9,  r9                 ; init retrurn value = 0
        xor         rax, rax                ; clear in  index

loop_f:
        vmovdqu64   zmm0, zmmword ptr [rcx+rax*8]; load a[]
        vpcmpq      k1, zmm0, zmm1, _MM_CMPINT_LT
        vpcompressq zmmword ptr [rdx+r9*8]{k1}, zmm0 ; store zmm0 to &v[r9]
        vpcompressd ymmword ptr [r8 +r9*4]{k1}, ymm3 ; store ymm3 to &vIdx[r9]
        vpaddd      ymm3, ymm3, ymm4        ; update index

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

        add         rax, 8
        cmp         rax, r11
        jb          short loop_f

        mov         eax, r9d                ; return value

        ret
extWidxQLow endp

まず、vpbroadcastq命令でr9 レジスターの値をzmm1レジスターへブロードキャストし初期化します。r9 レジスターは下限値が格納されています。これによって、zmm1レジスターの8要素がr9 レジスターで埋められます。次にr11 へ入力配列の要素数を読み込みます。
vmovdqu32命令でymm3レジスターへインデックスの初期値を設定します。vpbroadcastd命令で、ymm4レジスターへymm3レジスターが保持するインデックスの増分値を設定します。本例では、64ビット整数を対象とするため一回で8要素を処理します。これから、インデックスの増分値は8 です。使用しているレジスターから分かるように、値はzmmレジスターで8要素を、インデックスはymmレジスターで8要素を処理します。これはインデックス値をint型で管理するためです。
次にr9 レジスターとrax レジスターをxor命令でゼロクリアします。r9 レジスターは抽出した要素の数を、raxレジスターは一次元配列を参照するためのインデックスを保持します。
前準備が完了したためループへ入ります。zmm0レジスターへ配列の処理対象要素を読み込みます。これを下限値が格納されているzmm1レジスターと比較します。vpcmpq命令で条件に合致する要素のビットがオンになるマスクをk1マスクレジスターへ得ます。このマスクレジスターを使って要素と、そのインデックス値を抽出します。ただ、要素の抽出はvpcompressq命令、インデックス値の抽出はvpcompressd命令を使用します。これは、それぞれのデータ型が異なるためです。
抽出した要素がいくつになるか不明です。そこでk1レジスターをr10レジスターへ移動し、popcnt命令でオンになっているビット数を数えます。この値は抽出した要素数を示します。この値をr9へ加算し、出力配列用のインデックスを管理します。
次に、一次元配列のインデックスを保持するraxレジスターへ8を加算し、これを配列の要素数を保持するr11レジスターと比較し、すべての要素を処理したか調べます。処理が終わっていなければ、次の要素の処理へ移ります。全要素の処理が完了したら、r9に格納されている抽出した要素数を、eaxレジスターへ移動し処理を終了します。eaxレジスターは関数の戻り値であるint型の値です。
一部の処理は
arques.hatenablog.com
と同様な部分もありますので、それの説明も参考にすると良いでしょう。

common.h、共通関数

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

    :
// C++
template <typename T>
int cextractLow(const T* a, T* v, int* vIdx,
                            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];
            vIdx[rIndex] = i;
            rIndex++;
        }
    }
    return rIndex;
}

// C++
template <typename T>
int cextractHigh(const T* a, T* v, int* vIdx,
                            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];
            vIdx[rIndex] = i;
            rIndex++;
        }
    }
    return rIndex;
}

// C++
template <typename T>
int cextract(const T* a, T* v, int* vIdx, 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];
            vIdx[rIndex] = i;
            rIndex++;
        }
    }
    return rIndex;
}
    :

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

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

 実行結果

[ v < -50]
asm Length = 2016
cpp Length = 2016

[ v > 50]
asm Length = 2064
cpp Length = 2064

[ -50 < v < 50]
asm Length = 16
cpp Length = 16

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

念のため、任意範囲の要素とそのインデックス値を抽出するextWidxQ へ展開された関数のソースリストを示します。

        public      extWidxQ
        align       16
extWidxQ proc
        vpbroadcastq zmm1, r9               ; zmm1 = low/high
        mov         r11, qword ptr [rsp+40] ; length
        vpbroadcastq zmm2, qword ptr [rsp+48]; zmm2 = high
        vmovdqu32   ymm3, ymmword ptr index ; ymm3 = index
        mov         r9, 8
        vpbroadcastd ymm4, r9               ; ymm4 = 8,...,8

        xor         r9,  r9                 ; init retrurn value = 0
        xor         rax, rax                ; clear in  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+r9*8]{k1}, zmm0 ; store zmm0 to &v[r9]
        vpcompressd ymmword ptr [r8 +r9*4]{k1}, ymm3 ; store ymm3 to &vIdx[r9]
        vpaddd      ymm3, ymm3, ymm4        ; update index

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

        add         rax, 8
        cmp         rax, r11
        jb          short loop_f

        mov         eax, r9d                ; return value

        ret
extWidxQ endp

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

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

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