インテル® C++ コンパイラーのオフロード機能を効率良く使用するには

同カテゴリーの次の記事

HPC パフォーマンスの測定

この記事は、インテル® デベロッパー・ゾーンに掲載されている「Effective Use of the Intel Compiler’s Offload Features」の日本語参考訳です。


はじめに

ここでは、インテル® メニー・インテグレーテッド・コア (インテル® MIC) アーキテクチャー向けのインテル® Composer XE 2013 によるヘテロジニアス・オフロード・プログラミング・モデルの最も一般的な手法を検証します。

トピック

オフロードするコード領域を選択する

並列性に基づいて選択する

コプロセッサーでは高度に並列化されたコード領域を実行します。シリアルコードをコプロセッサーにオフロードすると、ほとんどの場合 CPU 上で実行するよりも遅くなります。

データフローに基づいてオフロード領域のスコープを変更する

並列化レベルに基づいてオフロードするコード領域を選択すると、多数の小さな領域がオフロードされる可能性があります。その場合、CPU と MIC 間で必要なデータ転送とのバランスを考慮しなければいけません。データ転送には時間がかかる可能性があります (PCIe バスの速度に依存します)。また、マーシャリング (pragma offload)、あるいは _Cilk_shared キーワードの追加と _Offload_shared_malloc による動的割り当てが必要になるため、容易ではありません。2 つの並列領域の間にシリアル処理が行われている場合、1 番目の並列領域の出力データを CPU に転送し、CPU でシリアル処理を実行してから、2 番目の並列領域の入力データを CPU からコプロセッサーに転送するか、あるいは 1 番目の並列領域の出力データをコプロセッサーで保持し、コプロセッサーでシリアル処理を実行する (つまり、並列-シリアル-並列領域全体をオフロードする) ことができます。

データ転送メカニズムを選択する

Copyin/Copyout モデル (#pragma offload)

このモデルは、インテル® C/C++ コンパイラーとインテル® Fortran コンパイラーの両方でサポートされています。
CPU とコプロセッサー間のデータ転送が、スカラーまたはビット単位でコピーできる要素の配列に限定されている場合、#pragma offload モデルを選択します。このモデルを使用するには、関数宣言のマークアップとコードのオフロード位置でローカルな変更が必要です。Fortran プログラムはこのモデルのみ利用できます (Fortran は以下の共有メモリーモデルをサポートしていません)。

共有メモリーモデル (_Cilk_shared/_Cilk_offload)
このモデルは、インテル® C/C++ コンパイラーでのみ利用できます (Fortran ではサポートされていません)。
CPU とコプロセッサー間のデータ転送が単純なスカラーやビット単位でコピーできる配列よりも複雑な場合は、_Cilk_shared/_Cilk_offload 構造の利用を検討してください。これらのプラグマは、共有メモリーを使用するオフロード・プログラミング・モデルの実装に役立ちます。このモデルを利用するには、関数と静的に割り当てられたデータが _Cilk_shared 属性でマークされており、動的に割り当てられたデータが共有メモリーに割り当てられていなければいけません。共有メモリー・プログラミング・モデルで _Cilk_Shared/_Cilk_Offload を実装するには、より多くの作業が必要になりますが、ほぼすべての C/C++ プログラムを処理することができるため、多くのプログラムでインテル® MIC アーキテクチャーを利用することができます。

#pragma offload を使用してオフロードする

オフロード・パフォーマンスの測定

初期化のオーバーヘッド
デフォルトでは、プログラムが最初に #pragma offload を実行するときに、プログラムに割り当てられているすべての MIC デバイスが初期化されます。初期化では、各デバイスへの MIC プログラムのロード、CPU とデバイス間のデータ転送パイプラインのセットアップ、CPU スレッドからのオフロード要求を処理する MIC スレッドの生成を行います。これらの処理には時間がかかります。そのため、最初のオフロードはタイミングの測定に含めないでください。デバイスへのダミーオフロードを実行することで、初回のみ発生するこのオーバーヘッドを除外できます。

// 初期化用の空のオフロードの例 
int main() 
{ 
    #pragma offload_transfer target(mic) 
    ... 
}

また別の方法として、環境変数を OFFLOAD_INIT=on_start に設定して、メインプログラムを開始する前に、利用可能なすべての MIC デバイスを事前に初期化することもできます。

オフロードデータ転送

入力データを最小限にする
可能な場合は、ローカルで計算するようにします。

オフロード間でデータを保持
オフロードの最終データを後続のオフロードが必要とする場合は、コプロセッサーでそのデータを保持します。

オフロード間でデータを再利用する場合は、同じコプロセッサーへオフロードする必要があります。target 節でコプロセッサーの番号を明示的に指定することで、同じプロセッサーへのオフロードが保証されます。

データ保持: 静的に割り当てられるデータ
C/C++ では、ファイルスコープで宣言された変数と “static” ストレージクラスを持つ関数ローカルの変数が静的に割り当てられます。Fortran では、共通ブロック、PROGRAM ブロックで宣言されているデータ、“save” 属性を持つデータが静的に割り当てられます。静的データは、新しい値で上書きされない限り、オフロード間で値を保持します。nocopy 節を用いて、以前の値を再利用できます。

// ファイルスコープ 
int x, y[100]; 
void f() 
{ 
    x = 55; 
    // CPU から x を転送し、コプロセッサーで y を計算します。
    ... 
#pragma offload target(mic:0) in(x) nocopy(y) 
{ y[50] = 66; } 
… 
#pragma offload target(mic:0) nocopy(x,y) 
{ // x と y は以前の値を保持します。} 
}

データ保持: スタックに割り当てられるデータ

C/C++ および Fortran では、関数とサブルーチン内で宣言された変数は、デフォルトで “automatic” に設定されるか、またはスタックストレージが割り当てられます。オフロード間で保持しなければならない関数ローカルな値は最小限に抑えてください。

オフロード環境で、各オフロード領域はコプロセッサー上で個別の関数として実行されます。スタックに割り当てられる変数は、通常、オフロード間で保持されません。オフロード間のデータ保持を模倣するには、“nocopy” が要求されている場合、オフロードの最後にスカラー値を CPU へコピーし、次のオフロードでコプロセッサーへ再度コピーします。効率の観点から、この方法はスカラー以外 (つまり、大きな関数ローカルの配列や構造体オブジェクト) では推奨しません。インテル® コンパイラー 13.0.0. 079 以降 (ベータ版は除く) では、関数ローカルな配列のオフロード機能をサポートしています。ただし、この機能をパフォーマンスが求められる部分に使うことは推奨しません。

void f() 
{
     int x = 55;
     int y[10] = { 0,1,2,3,4,5,6,7,8,9};

  // CPU から x、y を転送します。
  // このオフロードで計算された値 y を次のオフロードで使用するため、
  // y を CPU に転送します。
  #pragma offload target(mic:0) in(x) inout(y) 
  { y[5] = 66; } 

  // CPU で行われる x への代入は、
  // コプロセッサーの x の値とは関係ありません。
  x = 30;
  …
  // nocopy を使用して以前のオフロードの x を再利用できます。
  // ただし、配列 y を再度 CPU から転送する必要があります。
  #pragma offload target(mic:0) nocopy(x) in(y) 
  { = y[5]; // 値は 66 です。
    = x; // x は 55 (最初のオフロードからの値) です。
  }
}    

データ保持: ヒープに割り当てられるデータ

コプロセッサーのヒープは、オフロード間で保持されます。MIC 上でヒープメモリーを使用する方法は 2 つあります。

  1. #pragma offload を使う
  2. コプロセッサーで明示的に malloc を呼び出す

動的メモリーの管理は、#pragma を用いてコンパイラーに任せるか、または malloc/free を使って行うことができます。コンパイラーにより管理される動的メモリーは、alloc_if と free_if によって割り当て/割り当て解除が行われます。

コンパイラーにより管理されるヒープ割り当てデータ

メモリー割り当ては alloc_if と free_if によって制御され、データ転送は in/out/inout/nocopy によって制御されます。メモリー割り当てとデータ転送は独立していますが、データは割り当てられたメモリーにのみ転送できます。

// 次のマクロは、すべてのサンプルの alloc_if/free_if 節で使用されています。
#define ALLOC alloc_if(1) free_if(0)
#define FREE alloc_if(0) free_if(1)
#define REUSE alloc_if(0) free_if(0)

void f()
{
  int *p = (int *)malloc(100*sizeof(int));
  // p にメモリーを割り当て、CPU からデータを転送して保持します。
  #pragma offload target(mic:0) in(p[0:100] : ALLOC)
  { p[6]] = 66; }
  …

  // 以前のオフロードで使用した p のメモリーを再利用し、再度保持します。
  // 新しいデータをメモリーに格納します。
  #pragma offload target(mic:0) in(p[0:100] : REUSE)
  { p[6]] = 66; }
  …

  // 以前のオフロードで使用した p のメモリーを再利用し、
  // このオフロードの後に解放します。
  // 最終データをコプロセッサーから CPU へ転送します。
  #pragma offload target(mic:0) out(p[0:100] : FREE)
  { p[7]] = 77; }
  …
}

明示的に管理されるヒープ割り当てデータ

コプロセッサー上で実行するコードは、malloc/free を呼び出して明示的に動的メモリーの割り当て/割り当て解除を行えます。この方法で割り当てられる動的メモリーへのポインター変数はスカラーで、定義の有効範囲 (静的か、関数ローカル) に応じて、前述のデータ保持規則に従っていなければいけません。
コンパイラーにより管理される動的割り当てと明示的な動的割り当てが衝突しないように、明示的に管理されるオフロード領域内で参照されるポインター変数に nocopy 節を指定します。

void f()
{ int *p;
  … 
  // nocopy 節は、p の参照先の CPU の値が
  // コプロセッサーに転送されないことを保証します。
  #pragma offload target(mic:0) nocopy(p)
  {
    // p の動的メモリーをコプロセッサー上に割り当てます。
    p = (int *)malloc(100);
    p[0] = 77;
    …
  }
  ..
  // nocopy 節は、オフロードプロセスによって p が変更されないことを保証します。
  #pragma offload target(mic:0) nocopy(p)
  {
    // p の参照先の動的メモリーを再利用します。
    … = p[0]; // 77 になります。
  }
}


ローカルポインターとオフロード間で使用されるポインター

オフロード領域で使用されるポインターは、デフォルトでは inout です (つまり、関連付けられたデータは IN 転送と OUT 転送を行えます)。場合によっては、ポインターはローカルでのみ利用することができます (コプロセッサー上でのみ割り当てられ、使用されます)。この場合、nocopy 節を用いて offload 節でポインターが変更されないようにすると、プログラマーがポインターの値を明示的に管理できます。このほかにも、CPU からポインターにデータが転送され、以降のオフロードで a) 同じ割り当てメモリーに新しいデータを転送して使用するか、あるいは b) 同じ割り当てメモリーと同じデータを再利用する場合が考えられます。a) の場合は、要素数に等しい長さの in 節を使用します。b) の場合は、長さ 0 の in 節を使うことで、ポインターの “更新” のみを行い、データ転送は行わないようにすることができます。

in/out/nocopy 節と長さに関する詳細:

長さまたは要素数

< 0

長さまたは要素数

== 0

長さまたは要素数 > 0

nocopy :

alloc_if(0) free_if(0)

OK。ローカル MIC ポインターに利用できます。

OK。ローカル MIC ポインターに利用できます。

OK。ローカル MIC ポインターに利用できます。

nocopy :

alloc_if(0) free_if(1)

OK。ポインターを更新し、メモリーを解放します (長さは無視します)。

OK。ポインターを更新し、メモリーを解放します (長さは無視します)。

OK。ポインターを更新し、メモリーを解放します (長さは無視します)。

nocopy :

alloc_if(1) free_if(0)

エラー。0 未満を割り当てることはできません。

エラー。0 を割り当てることはできません。

OK。割り当てを行い、ポインターを更新します。

nocopy :

alloc_if(1) free_if(1)

エラー。0 未満を割り当てることはできません。

エラー。0 を割り当てることはできません。

OK。割り当てを行い、ポインターを更新し、メモリーを解放します。

in / out / inout :

alloc_if(0) free_if(0)

OK。ポインターの更新のみ行います。

OK。ポインターの更新のみ行います。

OK。ポインターを更新し、転送します。

in / out / inout :

alloc_if(0) free_if(1)

OK。ポインターを更新し、転送は行わず、解放します。

OK。ポインターを更新し、転送は行わず、解放します。

OK。ポインターを更新し、転送して、解放します。

in / out / inout :

alloc_if(1) free_if(0)

エラー。0 未満の割り当て/転送を行うことはできません。

エラー。0 を割り当てることはできません。

OK。割り当てを行い、ポインターを更新し、転送します。

in / out / inout :

alloc_if(1) free_if(1)

エラー。0 未満の割り当て/転送を行うことはできません。

エラー。0 を割り当てることはできません。

OK。割り当てを行い、ポインターを更新し、転送して、解放します。

in/out/nocopy 節と長さの使用例を以下に示します。

ローカルポインターの例

int *p;
int *temp;

p = malloc(SIZE);
temp = p;

// p にはデータを転送しますが、temp には何もしません。
#pragma offload target(mic:0) in(p : ALLOC) nocopy(temp)
{
// temp はローカルに割り当てます。
temp = malloc(…);
memcpy(temp, p, …);
}

// 以前のオフロードから temp の値を再利用します。
#pragma offload target(mic:0) out(p : FREE) nocopy(temp)
{
// temp の値は以前のオフロードから保持されます。
memcpy(p, temp, …);
free(temp);
}


データを保持する MIC ポインターとデータ転送の例

#include <stdlib.h>
#include <stdio.h>

#define SIZE 10

void func1(int *p)
{
int i;

// 転送カウントが 0 なので、MIC に転送されるデータはありません。
// ただし、in が使用されているため、
// MIC でポインターの値が初期化されます。
#pragma offload target(mic:0) \
in(p : length(0) REUSE)
{ for(i=0; i<SIZE; i++) printf("%3d", p[i]); printf("\n"); }
}

int main()
{
int i;
int *a;

a = (int *)malloc(SIZE*sizeof(int));
for(i=0; i<SIZE; i++) a[i] = i;

// a は MIC 上にのみ割り当てます。
#pragma offload_transfer target(mic:0) \
nocopy(a : length(SIZE) ALLOC)

// a に割り当てられたメモリーにデータを転送します。
// 転送カウントは SIZE です。
// a の各要素を出力し、インクリメントします。
#pragma offload target(mic) \
in(a : length(SIZE) REUSE)
{ for(i=0; i<SIZE; i++) printf("%3d", a[i]++); printf("\n"); }

func1(a);

// CPU にデータを転送し、MIC で解放します。
#pragma offload_transfer target(mic:0) \
out(a : length(SIZE) FREE)

return 0;
}


オフロードで使用した長さが不明なメモリーを解放する

void freeOnCoprocessor(void* mem_ptr) 
{
    char *c_ptr = (char *)(mem_ptr); 

    // データ転送なし。コプロセッサーで以前に割り当てられた
    // メモリーを解放します。ここでは長さが不明なため 0 を使用します。
    #pragma offload_transfer target(mic:0)
               nocopy(c_ptr:length(0) FREE)

    free(mem_ptr);
}

長さが分からなくても解放することができます (その場合は、ダミー値 0 を渡します)。alloc_if と free_if は式なので、実行時まで割り当てか、解放かは分かりません。そのため、ポインターでは字句的に length 修飾子を指定する必要があります。解放の場合、長さは無視されます。

ビット単位でコピーできないデータを CPU と MIC 間で転送する

場合によっては、ビット単位でコピーできる要素 (スカラーや配列など) とそうでない要素 (ほかのデータへのポインターなど) が混在するデータ・オブジェクトを CPU と MIC 間で転送する必要があります。デフォルトでは、コンパイラーは in/out 節でそのようなオブジェクトを許可していません。プログラムでそのようなオブジェクトのビット単位でコピーできる要素だけを転送する場合、-wd<number> コンパイラー・オプションを指定してエラーを無効にしたり、-ww<number> コンパイラー・オプションでエラーを警告に変換することができます。

注:

  1. ビット単位でコピーできない要素の値は不定であり、それらに有効な値を代入する前にアクセスしないようにすることは開発者の責任です。
  2. このほかの場合にも、コンパイラーは “ビット単位でコピーできない” と診断を下すことがあります。エラーを無効にできる可能性がある場合は、エラーコードが出力されるので、-wd または -ww コンパイラー・オプションでそのエラーコードを指定します。

    // ビット単位でないオブジェクト転送の例 - ビット単位でコピーできるデータのみ転送
    --- ファイル wd2568.cpp --- 
    #include <complex> 
    typedef std::complex<float> Value; 

    void f() 
    {
         const Value* C; 
         #pragma offload_transfer target(mic) in(C:length(2)) 
    }

    > icc -c wd2568.cpp
    wd2568.cpp(8): エラー #2568: このオフロード領域で使用される変数 "C" はビット単位でコピーできません。
    #pragma offload target(mic) in(C:length(2))
    ^
    コンパイルは wd2563.cpp で異常終了しました (コード 2)。
    // 次のように、-wd2568 を指定してコンパイルします。

    > icc -c wd2568.cpp -wd2568
    > 

ビット単位でないオブジェクトのすべての要素を転送する場合、オブジェクトを構成要素に “分解” し、各要素を個別に転送して、MIC 側でオブジェクトを再構築しなければいけません。

    // ビット単位でないオブジェクト転送の例 - すべてのデータ要素を転送
    typedef struct {
        int m1;
        char *m2;
    } nbwcs; 

    void sample11()
    {
        nbwcs struct1;
        struct1.m1 = 10;
        struct1.m2 = malloc(11);
        int m1;
        char *m2;

        // ターゲットに転送するため、struct を分解します。
        m1 = struct1.m1;
        m2 = struct1.m2;

        #pragma offload target(mic) inout(m1) inout(m2 : length(11)
       {
           nbwcs struct2;
           // ターゲットで struct を再構築します。
           struct2.m1 = m1;
           struct2.m2 = m2;
           ...
       }
       ...
    }
        
 

コプロセッサーのメモリー割り当てオーバーヘッドを最小限に抑える


コプロセッサー上では動的メモリー割り当てに時間がかかります。割り当てと解放を少なくすることで、割り当て/割り当て解除のオーバーヘッドを最小限に抑えられます。CPU とコプロセッサー間で配列の転送を複数回行う場合は、最初に使用するときに割り当て、最後に使用し終わったら解放します。「データ保持: ヒープに割り当てられるデータ」にある例を参照してください。オフロードコードで同じ配列を繰り返し再利用しない場合であっても、MIC 上に割り当てられた同じメモリーブロックを再利用することができます。

// コプロセッサーで “count” 要素からなるバッファーを割り当てます。
// このバッファーは、CPU 上の p を用いてアクセスできます。
#pragma offload_transfer target(mic:0) nocopy(p[0:count] : ALLOC)
…

// CPU 上の x のデータをコプロセッサー上のバッファー p に転送します。
// elements1 <= count
#pragma offload target(mic:0) \
        in(x[0:elements1] : REUSE into(p[0:elements1])
{ … = p[10]; }
…

// CPU 上の y のデータをコプロセッサー上のバッファー p に転送します。
// elements2 <= count
#pragma offload target(mic:0) \
        in(y[0:elements2] : REUSE into(p[0:elements2])
{ … = p[10]; }
…

// バッファーを解放します。
#pragma offload_transfer target(mic:0) nocopy(p[0:count] : FREE)

メモリーバッファーは必要ない場合であっても割り当てられ、コプロセッサーのメモリーを消費するため、メモリー使用量と割り当て/割り当て解除オーバーヘッドのバランスを考慮する必要があります。

オフロード・データ・アライメント

コプロセッサー上でコードのベクトル化を有効にするには、データを 64 バイト以上の境界でアライメントします。静的に割り当てられた変数の場合は、__declspec(align(64)) によりデータをアライメントすることができます。コプロセッサーへポインターデータを転送する場合は、#pragma offload の align 修飾子を利用できます。
#pragma offload target(mic) in(p[0:2048] :align(64))

オフロード・ライブラリーは、通常コプロセッサー・データを 64 バイト境界内のオフセットに割り当てます。このオフセットは、CPU データの 64 バイト境界内のオフセットと同じになります。このオフセットの一致により、CPU と コプロセッサー間で高速 DMA 転送が保証されます。align 修飾子は、このオフセットの一致を無効にする可能性があります。高速 DMA データ転送とコプロセッサー・データの適切なアライメントの利点を得るには、代わりに CPU データをアライメントし、明示的に align 宣言子を使用しないようにします。

データ転送レートを最大にする

CPU とコプロセッサー間のデータ転送レートは、スタックデータでは最も遅く、静的/動的に割り当てられたデータでは最も速くなります。64 バイト以上の境界で CPU データをアライメントすることで、データ転送レートを向上できます。2MB でアライメントすると最高の転送レートが得られます。
データ転送レートを向上するにはデータ転送サイズを 64 バイトの倍数にし、最高の転送レートを得るには 2MB の倍数にします。一般に、データ転送サイズが大きくなるにつれ、バンド幅も大きくなります。データ転送レートを向上するため、コプロセッサーのメモリーは大きなページ (2MB) に割り当てるようにします。環境変数 MIC_USE_2MB_BUFFERS に関する注意事項を参照してください。

データ転送とオフロード計算をオーバーラップさせる

オフロード計算に必要な入力データは、オフロードの前に送ることができます。データ転送中、CPU は処理を続行できます。次の例で、f1 と f2 はオフロードに先立ってコプロセッサーに送られます。

01 const int N = 4086;
02 float *f1, *f2;
03 float result;
04 f1 = (float *)memalign(64, N*sizeof(float)); 
05 f2 = (float *)memalign(64, N*sizeof(float));
...
10 // CPU はデータを送信して続行します。
11 #pragma offload_transfer in( f1[0:N] ) signal(f1)
12   
...

20 // CPU はデータを送信して続行します。
21 #pragma offload_transfer in( f2[0:N] ) signal(f2)
22   
...
30 // CPU は f1 と f2 を使用して計算を行うようにリクエストします。
31 // コプロセッサーはあらかじめ送信されたデータを受信した後にのみ実行を開始します。
32 #pragma offload wait(f1, f2) out( result )
33 {
34      result = foo(N, f1, f2);
35 }

MIC から CPU にデータを非同期で転送するには、2 つの異なるプラグマで signal 節と wait 節を使用します。最初のプラグマはデータ転送を開始し、次のプラグマはデータ転送が完了するのを待機します。

01 const int N = 4086;
02 float *f1, *f2;
03 f1 = (float *)memalign(64, N*sizeof(float)); 
04 f2 = (float *)memalign(64, N*sizeof(float));
...

10 // CPU は入力同期として f1 を送信します。
11 // 出力は f2 にありますが直ちに必要ではありません。
12 #pragma offload in(f1[0:N]) nocopy(f2[0:N]) signal(f2)
14 {
15       foo(N, f1, f2);
16 }
..
20 #pragma offload_transfer wait(f2) out(f2[0:N])
21   
22 // CPU が f2 の結果を利用できるようになりました。
23 ...

ラムダ関数をオフロードする

ラムダはインライン関数です。ラムダ関数をオフロードする場合、オフロード属性の適用には注意が必要です。次に例を示します。

#pragma offload_attribute(push,target(mic))
#include <stdio.h>

template<typename F>
void Run( F f ) {
   f();
}
#pragma offload_attribute(pop)

int main() {
#pragma offload target(mic)
    Run( [&] () __attribute__((target(mic)))
{
#ifdef __MIC__
        printf("MIC says Hello, world\n");
#else
        printf("CPU says Hello, world\n");
#endif
} ); }

_Cilk_shared/_Cilk_offload を使用してオフロードする

データとクラスを _Cilk_shared でマーク

共有ポインターの宣言

共有ポインターは次のように宣言します。

_Cilk_shared int *p;

共有データへのポインターの宣言

共有データへのポインターは次のように宣言します。

int * _Cilk_shared q;

共有データへの共有ポインターの宣言は、この 2 つを組み合わせます。

_Cilk_shared int * _Cilk_shared r;

クラス型を共有として宣言する

クラス型を共有として宣言する場合、宣言文で “class” の直後にキーワードを追加します。先頭に _Cilk_shared を追加すると、型ではなく、データが共有として宣言されます。

Class _Cilk_shared C {
  // クラスメンバー 
};

共有データに動的メモリーを割り当てる

動的に割り当てられる共有データは、共有メモリーのプールから割り当てられなければいけません。これは API を使用して行います。

_Offload_shared_malloc 
_Offload_shared_free 
_Offload_shared_aligned_malloc 
_Offload_shared_aligned_free

new を使用する

場合によっては、メモリー割り当てをユーザーが直接制御できないことがあります (例えば、STL オブジェクトなど)。offload.h ヘッダーは、STL メモリー割り当てを共有メモリーに転換する new メカニズムを提供します。次に例を示します。

#include <vector>
#include <stdio.h>
using namespace std;

#include "offload.h"

_Cilk_shared vector<int, __offload::shared_allocator<int> > * _Cilk_shared v;

_Cilk_shared void foo() {
   for (int i = 0; i< 5; i++) {
     printf("%d\n", (*v)[i]); // そうでない場合はここでフォルトします。
   }
}

int main() {
 // v は共有メモリーに割り当てられます。
 // v の要素も共有メモリーにあります。
  v = new (_Offload_shared_malloc(sizeof(vector<int>)))
          _Cilk_shared
          vector<int, __offload::shared_allocator<int> >(5);  
                                
  for (int i = 0; i< 5; i++) {
    (*v)[i] = i;
  }
  _Cilk_offload foo();
  return 0;
}

_Cilk_offload/_Cilk_shared のパフォーマンスを向上する

共有データ向けのデフォルトのメモリーモデルは、CPU とコプロセッサーの両方が共有データを変更することを仮定します。オフロードの入力データを CPU からコプロセッサーへ転送し、オフロード処理後に変更されたデータをすべて CPU へ転送して、CPU 上で同時に変更される可能性がある共有データとマージする必要のないアプリケーション・データ・モデルの場合、より単純で効率良い同期モデルを指定できます。このモデルは、環境変数 MYO_CONSISTENCE_PROTOCOL を使って有効にします。

setenv MYO_CONSISTENCE_PROTOCOL HYBRID_UPDATE_NOT_SHARED

オフロードコードとコプロセッサーのライブラリーをリンクする

コンパイラーは共有ポインターと非共有ポインターの混在を厳しくチェックしているため、そのいくつかを回避する必要があります。一般に、共有かどうか分からなくても、キャストされていれば、共有データは常にルーチンで処理されます。
次の手順に従って、オフロードコードと MIC 向けのサードパーティのライブラリーをリンクすることができます。
1.サードパーティのライブラリーはそのまま利用します。つまり、ライブラリーは –mmic オプションを指定してビルドされており、共有かどうかは分かりません。
2.CPU プログラムからデータ交換関数として機能する MIC 上の関数へオフロードします。これらの関数は _Cilk_shared としてマークされ、_Cilk_shared とマークされているデータを処理します。
3.MIC 上で実行するこれらのデータ交換関数から、MIC ライブラリーを呼び出します。_Cilk_shared としてマークされている関数で参照されるデータと関数は _Cilk_shared としてマークされていなければならないため、(共有キーワードを指定してビルドされていない) 外部ライブラリーで定義されているデータと関数はキャストする必要があります。
つまり、CPU の SHARED コードが –mmic を指定してビルドされた MIC (キャスト) コードで実行されます。

 --- Makefile ---
 main.out:         main.cpp libbar.so
             icc -o main.out main.cpp -offload-option,mic,compiler,"-L. -lbar"

 libbar.so:        libbar.cpp
            icc -mmic -fPIC -shared -o libbar.so libbar.cpp

 clean:
           rm *.o *.so main.out

run:
          export MIC_LD_LIBRARY_PATH=".:$$MIC_LD_LIBRARY_PATH"
          main.out
--- file main.cpp ---
#include <vector>
#include <stdio.h>
using namespace std;

#include "offload.h"
typedef vector<float, __offload::shared_allocator<float> > _Cilk_shared * SHARED_VECTOR_PTR;
typedef vector<float> * VECTOR_PTR;
SHARED_VECTOR_PTR v;
typedef _Cilk_shared void (*SHARED_FUNC)(VECTOR_PTR v);

extern void libbar(VECTOR_PTR v);

_Cilk_shared void bar(VECTOR_PTR v)
{
        if (_Offload_get_device_number() == 0)
        {
                printf("MIC bar\n");
                for (int i = 0; i<  5; i++) {
                        printf("\t%f\n", (*v)[i]);
                }
        }
}

_Cilk_shared void foo(SHARED_VECTOR_PTR v)
{
        if (_Offload_get_device_number() == 0)
        {
                printf("MIC foo\n");
                for (int i = 0; i<  5; i++) {
                        printf("\t%f\n", (*v)[i]);
                }
        }
        bar((VECTOR_PTR)v);
#ifdef __MIC__
   (*(SHARED_FUNC)&libbar)((VECTOR_PTR)v);
#else
#endif
}

int main()
{
        // v の要素は共有メモリーにあります。
        v = new (_Offload_shared_malloc(sizeof(vector<float>)))
                _Cilk_shared vector<float, __offload::shared_allocator<float> > (5);


        for (int i = 0; i<  5; i++) {
                (*v)[i] = i;
        }
        _Cilk_offload foo(v);

        return 0;
}


 ---- file libbar.cpp ---
#include <vector>
#include <stdio.h>
using namespace std;

typedef vector<float> * VECTOR_PTR;

void libbar(VECTOR_PTR v)
{
        if (_Offload_get_device_number() == 0)
        {
                printf("MIC libbar\n");
                for (int i = 0; i<  5; i++) {
                        printf("\t%f\n", (*v)[i]);
                }
        }
}

CPU と MIC 向けにコードをカスタマイズする

#ifdef __MIC__ は使用しないでください。
場合によっては、コプロセッサー向けにオフロードコードをカスタマイズする必要があります。可能な限り、動的チェックを行ってコプロセッサー上で実行するコードを選択します。

    if (_Offload_get_device_number() >= 0)
    { 
       // MIC バージョン
    } else {
       // CPU バージョン
    }

MIC バージョンまたは CPU バージョンで、プロセッサーで利用できない組込み関数が使われている場合、この手法は導入できません。その場合は、#ifdef を使用します。ただし、#ifdef __MIC__ を #pragma offload 構造内で直接使用しないでください。コプロセッサーと CPU 間で転送される変数の不一致を引き起こす可能性があります。変数のデフォルトは inout なので、変数の参照が 2 つのバージョンで異なる場合、不一致が発生します。

    #ifdef __MIC__
        // MIC バージョン
    #else
        // CPU バージョン
    #endif

CPU コンパイラーと MIC コンパイラーに渡すオプションを制御する

ユーザーが利用可能なオプション

// 次のコンパイラーの呼び出しは、以下に示す出力を生成します。
  ifort -openmp program.f90 -g -o g.out -watch=mic_cmd 

MIC コマンドライン:
  ifort -openmp program.f90 -g -o g.out

内部コンパイラー・オプション

内部コンパイラー・オプションは、通常 –m から始まり、CPU コンパイルから MIC コンパイルへ自動で渡されません。これらのオプションは、(–offload-option,mic,compiler CPU オプションを指定して) CPU コンパイルまたは MIC コンパイルで明示的に指定する必要があります。

// 次のコマンドは、内部オプションを CPU コンパイラーに渡します。
// MIC コマンドラインを出力するように指定しています。
icc -c test.c -watch=mic_cmd -mP2OPT_il0_list=1

MIC コマンドライン:
icc -c test.c

// 次のコマンドは、内部オプションを CPU コンパイラーに渡します。
// MIC コマンドラインを出力するように指定しています。
icc -c test.c -watch=mic_cmd -offload-option,mic,compiler,-mP2OPT_il0_list=1

MIC コマンドライン:
icc -c test.c -mP2OPT_il0_list=1

オフロードを制御する環境変数

環境変数には、次の 2 つのカテゴリーがあります。

  1. オフロード・ランタイム・ライブラリーの動作に影響を与える環境変数
  2. オフロード・ライブラリーによってコプロセッサーの実行環境に渡される環境変数

最初に 1 番目の環境変数について説明します。これらの環境変数には、”MIC_” または “OFFLOAD_” というプリフィックスが付けられています。このプリフィックスは、環境変数の名前のように固定です。

2 番目の環境変数には、最初の環境変数と区別するため、特殊な環境変数 MIC_ENV_PREFIX が使用されます。詳細は、このセクションの最後で述べます。

MIC_USE_2MB_BUFFERS

大きなページのバッファーを作成する際のしきい値を設定します。バッファーのサイズがしきい値を超える場合、大きなページのバッファーが作成されます。

// MIC 上に割り当てられる 100KB 以上の変数は、
// 大きなページに割り当てられます。
setenv MIC_USE_2MB_BUFFERS 100k

MIC_STACKSIZE

MIC 上のオフロード・プロセス・スタックのサイズを設定します。これはスタック全体のサイズです。各 OpenMP* スレッドのサイズを変更するには、MIC_OMP_STACKSIZE を使用します。

setenv MIC_STACKSIZE 100M // MIC スタックを 100MB に設定します。

MIC_LD_LIBRARY_PATH

MIC オフロードコードで必要な共有ライブラリーのパスを設定します。

「オフロードコードとコプロセッサーのライブラリーをリンクする」セクションの例を参照してください。

OFFLOAD_REPORT

__Offload_report(int on_or_off); // CPU でのみ呼び出されます。

API で実行時にレポートをオン/オフに設定できます。一方、環境変数 OFFLOAD_REPORT は 1 または 2 に設定できます。

API は実行時にフラグの値を変更することがあります。これは、環境変数の設定には影響しません。どのオフロードがレポートを生成するかに影響します。

#include <stdio.h> 
__declspec(target(mic)) volatile int x;
int main() 
{
    __Offload_report(0);
    #pragma offload target(mic)
    {
         x = 1;
    }
    __Offload_report(1);
    #pragma offload target(mic)
    {
         x = 2;
    }
    return 0;
}

上記のプログラムは、OFFLOAD_REPORT=1 の場合、次のようなレポートを生成します。

[Offload] [MIC 0] [File] test_ofld0.c

[Offload] [MIC 0] [Line] 15

[Offload] [MIC 0] [CPU Time] 0.000268 (seconds)

[Offload] [MIC 0] [MIC Time] 0.000022 (seconds)

OFFLOAD_REPORT=2 の場合、次のようなレポートを生成します。

[Offload] [MIC 0] [File] test_ofld0.c

[Offload] [MIC 0] [Line] 15

[Offload] [MIC 0] [CPU Time] 0.000263 (seconds)

[Offload] [MIC 0] [CPU->MIC Data] 0 (bytes)

[Offload] [MIC 0] [MIC Time] 0.000023 (seconds)

[Offload] [MIC 0] [MIC->CPU Data] 4 (bytes)

[CPU->MIC Data] と [MIC->CPU Data] は、転送されたデータの合計サイズ (バイト) です。

OFFLOAD_DEVICES

環境変数 OFFLOAD_DEVICES は、変数の値で指定された MIC カードのみ使用するようにプロセスを制限します。値は、物理デバイス番号をカンマ区切りで指定します。デバイス番号の範囲は、0 ~ (number_of_devices_in_the_system-1) です。

オフロードに利用可能なデバイスは、論理的に番号が付けられています。つまり、_Offload_number_of_devices() は利用可能なデバイスの数を返し、オフロードプラグマの target 指定子で指定されているデバイスのインデックスの範囲は 0 ~ (number_of_allowed_devices-1) です。

setenv OFFLOAD_DEVICES “1,2”

この場合、プログラムは (例えば、4 つのカードが取り付けられたシステムで) 物理 MIC カード 1 と 2 のみ使用できます。デバイス番号 0 または 1 へオフロードされた処理は、物理デバイス 1 と 2 で実行されます。1 よりも大きなデバイス番号が指定された場合は、すべてのオフロードがコプロセッサー 0 と 1 (物理カード 1 と 2) で行われます。物理デバイス 1 または 2 でオフロードが実行されている場合、MIC デバイス上で _Offload_get_device_number() は 0 または 1 を返します。

OFFLOAD_INIT

オフロードランタイムに MIC デバイスを初期化するタイミングのヒントを与えます。

利用可能な値は次のとおりです。

on_start

main に入る前に利用可能なすべてのデバイスが初期化されます。

on_offload

各デバイスの初期化は、そのデバイスへの最初のオフロードの直前に行われます。そのオフロードを処理する MIC デバイスのみ初期化されます。

on_offload_all

アプリケーションの最初のオフロードの直前に利用可能なすべての MIC デバイスが初期化されます。

(下位互換性のために) デフォルトは on_offload_all です。

MIC_ENV_PREFIX

MIC カードで実行するプロセスに環境変数の値を渡す一般的な手法です。

注: この環境変数の設定は、前のセクションで述べた固定の MIC_* 環境変数 (MIC_USE_2MB_BUFFERS、MIC_STACKSIZE、および MIC_LD_LIBRARY_PATH) には影響しません。これらの名前は固定です。

デフォルトでは、オフロードが発生すると、CPU プログラムを実行する環境で定義されているすべての環境変数がコプロセッサーの実行環境に複製されます。この動作は、環境変数 MIC_ENV_PREFIX を定義することで変更できます。MIC_ENV_PREFIX を設定すると、すべての CPU 環境変数ではなく、MIC_ENV_PREFIX 環境変数のプリフィックス値で始まる環境変数のみ複製されます。コプロセッサーに設定される環境変数のプリフィックス値は削除されます。この処理により、共通の環境変数名を使用する OpenMP*、インテル® Cilk™ Plus、その他の実行環境を個別に制御できます。

MIC_ENV_PREFIX が設定されていない場合、オフロードランタイムは単純にコプロセッサーにホスト環境を複製します。MIC_ENV_PREFIX が設定されている場合、MIC_ENV_PREFIX で定義されている値で始まる環境変数名のみ (プリフィックスを削除した状態で) コプロセッサーに渡されます。

つまり、MIC_ENV_PREFIX の値は、MIC デバイスで実行するプログラム用の環境変数を認識するプリフィックスを設定します。例えば、「setenv MIC_ENV_PREFIX MYCARDS」では、MIC プロセス用の環境変数を示す文字列として “MYCARDS” を使用します。

<mic-prefix>_<var>=<value> 形式の環境変数は、各カードに <var>=<value> を送ります。

<mic-prefix>_<card-number>_<var>=<value> 形式の環境変数は、MIC カード番号 <card-number> に <var>=<value> を送ります。

<mic-prefix>_ENV=<variable1=value1|variable2=value2> 形式の環境変数は、各カードに <variable1>=<value1> と <variable2>=<value2> を送ります。

<mic-prefix>_<card-number>_ENV=<variable1=value1|variable2=value2> 形式の環境変数は、MIC カード番号 <card-number> に <variable1>=<value1> と <variable2>=<value2> を送ります。

setenv MIC_ENV_PREFIX PHI// 使用されるプリフィックスを定義します。
setenv PHI_ABCD abc  // すべてのカードで ABCD=abcd に設定します。
setenv PHI_2_EFGH efgh // 論理 MIC 2 で EFGH=efgh に設定します。
setenv PHI_VAR X=x|Y=y // すべてのカードで X=x と Y=y に設定します。
setenv PHI_4_VAR P=p|Q=q // MIC 4 で P=p と Q=q に設定します。

次のステップ

この記事は、「Programming and Compiling for Intel® Many Integrated Core Architecture」(英語) の一部「Effective Use of the Intel Compiler’s Offload Features」の翻訳です。インテル® Xeon Phi™ コプロセッサー上にアプリケーションを移植し、チューニングを行うには、本ガイドの各リンクのトピックを参照してください。アプリケーションのパフォーマンスを最大限に引き出すために必要なステップを紹介しています。

ネイティブおよびオフロードのプログラミング・モデル」に戻る

コンパイラーの最適化に関する詳細は、最適化に関する注意事項を参照してください。

関連記事