特定値のインデックス抽出

一次元配列の各要素を検査し、与えられた値と一致する要素のインデックスを抽出します。戻り値は一次元配列に存在する特定値の数です。

符号付64ビット整数型

呼び出し側・符号付

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

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

#define T long long
extern "C"
{
    int aextIdxSQ(const T*, int*, const T, const size_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];
    int cIdx[ArrLen], aIdx[ArrLen];
    const T cmpValue = -10;

    cout << "cmpValue = " << cmpValue << endl;
    init(a, ArrLen);
    int cLen = cextIdx  (a, cIdx, cmpValue, ArrLen);
    int aLen = aextIdxSQ(a, aIdx, cmpValue, ArrLen);
    verify(cLen, aLen, cIdx, aIdx, ArrLen);

    return 0;
}

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

呼び出され側

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

レジスタ 内容
rcx 入力配列 a の先頭アドレス
rdx インデックスを格納する配列 vIdx の先頭アドレス
r8 特定値
r9 入力配列 a の要素数

64ビット整数型配列の要素が指定された値と一致するか調べ、そのインデックスを抽出します。また、抽出したインデックス値の数を返します。このような関数をマクロで記述したものを示します。

    :
mymacro macro       MNAME, CINST
        public      MNAME
        align       16
MNAME   proc
        vpbroadcastq zmm1, r8               ; zmm1 = cmpValue
        vmovdqu32   ymm3, ymmword ptr index ; ymm3 = index
        mov         r8, 8
        vpbroadcastd ymm4, r8               ; ymm4 = 8,...,8

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

loop_f:
        CINST       k1, zmm1, zmmword ptr [rcx+rax*8], _MM_CMPINT_EQ
        vpcompressd ymmword ptr [rdx+r8*4]{k1}, ymm3 ; store ymm3 to &aIdx[r8]
        vpaddd      ymm3, ymm3, ymm4        ; update index

        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            ; loop

        mov         eax, r8d                ; return value

        ret
MNAME   endp

        endm

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

mymacro aextIdxSQ, vpcmpq       ; QWORD
mymacro aextIdxUQ, vpcmpuq      ; QWORD Unsigned

_TEXT   ends

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

mymacro マクロを定義し、これを使ってaextIdxSQとaextIdxUQ関数を作ります。aextIdxSQ関数は符号付64ビット整数用、aextIdxUQ関数は符号付64ビット整数用の関数です。

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

        public      aextIdxSQ
        align       16
aextIdxSQ   proc
        vpbroadcastq zmm1, r8               ; zmm1 = cmpValue
        vmovdqu32   ymm3, ymmword ptr index ; ymm3 = index
        mov         r8, 8
        vpbroadcastd ymm4, r8               ; ymm4 = 8,...,8

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

loop_f:
        vpcmpq      k1, zmm1, zmmword ptr [rcx+rax*8], _MM_CMPINT_EQ
        vpcompressd ymmword ptr [rdx+r8*4]{k1}, ymm3 ; store ymm3 to &aIdx[r8]
        vpaddd      ymm3, ymm3, ymm4        ; update index

        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            ; loop

        mov         eax, r8d                ; return value

        ret
aextIdxSQ   endp

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

common.h、共通関数

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

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

すべての型に対応させるため、テンプレート関数とします。cextIdx関数は、一次元配列から指定値と一致する要素のインデックス値を抽出し、その数を戻します。

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

 実行結果

cmpValue = -10
asm Length = 68
cpp Length = 68


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