総和

一次元配列の総和を求めるプログラムを紹介します。以降に処理の概要図を示します。

64ビット整数型

呼び出し側・符号付

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

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

#define T short
extern "C" T aaddw(const 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];

    init(a, ArrLen);
    T cr = cadd (a, ArrLen);
    T ar = aaddw(a, ArrLen);

    cout << fixed << setprecision(2);
    cout << "cpp = " << cr << endl;
    cout << "asm = " << ar << endl;
    return 0;
}

一次元配列にランダムな値を与え、先ほどの関数を呼び出し一次元配列の総和を求めます。

呼び出され側

本ライブラリへは、入力配列 a のアドレスが rcx レジスターへ、配列の要素数rdx レジスターへ渡されます。渡される内容とレジスターの対応を表で示します。

レジスタ 内容
rcx 入力配列の先頭アドレス
rdx 配列の要素数
_TEXT   segment

        public          aaddq
        align           16

aaddq   proc
        vpxorq          zmm0, zmm0, zmm0        ; zmm0 = 0
        xor             rax, rax                ; clear index
loop_f:
        vpaddq          zmm0, zmm0, zmmword ptr [rcx+rax*8]
        add             rax, 8
        cmp             rax, rdx
        jb              short loop_f            ;loop        loop_f


        vextracti64x4   ymm1, zmm0, 1           ; reduce zmm0
        vpaddq          ymm1, ymm0, ymm1
        vextracti64x2   xmm0, ymm1, 1
        vpaddq          xmm2, xmm0, xmm1
        vpsrldq         xmm0, xmm2, 8
        vpaddq          xmm1, xmm0, xmm2
        vmovq           rax, xmm1

        ret
aaddq   endp

_TEXT   ends
        end 

まず、8要素単位の総和を保持するzmm0レジスターをvpxorq命令で0へ初期化します。次に配列を参照するインデックスを保持するraxレジスターをxor命令でクリアします。vpaddq命令で、zmm0レジスターと対象となる配列の8要素を加算し、結果をzmm0レジスターへ格納します。処理対象要素は、先頭アドレスを保持するrcxレジスターへインデックスを保持するraxレジスターへ8を乗じたものを加算して求めます。
次に、インデックスを保持するraxレジスターへ8を加算し、これを配列の要素数を保持するrdxレジスターと比較し、すべての要素を処理したか調べます。処理が終わっていなければ、次の要素の処理へ移ります。

全要素の処理が完了するとzmm0レジスターへ8要素単位の総和が格納されています。この8要素の総和を求めるreduce処理を行い、配列全体の総和を求めます。言葉で説明すると分かりづらいため、zmm0レジスターに格納された8要素単位の最小値から、全体の最小値を求める方法を図で示します。

これまでにreduceの説明はいくつか出てきていますので、詳細な説明は紙面を節約するために省きます。図とソースコードを参照することで処理は理解できるでしょう。

common.h、共通関数

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

    :
// C++の関数名にはデータ型を含ませない
template <typename T>
T cadd(const T* a, const size_t length)
{
    T sum = 0;
    for (size_t i = 0; i < length; i++)
    {
        sum +=  a[i];
    }
    return sum;
}
    :

すべての型に対応させるため、テンプレート関数とします。cadd 関数は一次元配列の全要素を加算した総和を返します。

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

 実行結果(signed)

C:\>ml64 /c addAsm.asm

C:\>cl /O2 /EHsc add.cpp addAsm.obj

C:\>add
cpp = 3747339
asm = 3747339

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