インテル® C++ コンパイラー 17.0 デベロッパー・ガイドおよびリファレンス

自動的にアライメントされる動的割り当て

概要

個々の要素のアライメントよりもデータ構造全体のアライメントのほうが厳密なアライメントを必要とすることをコンパイラーに伝えます。次に例を示します。

C++ 標準構文

class alignas(64) X {
	double elem[8];
};
GNU* 互換構文
class __attribute__((aligned(64))) X {
	double elem[8];
};
Microsoft® 互換構文
class __declspec(align(64)) X {
	double elem[8];
};

これは、個々のデータ要素よりも厳密なアライメントを必要とする、SIMD 命令で使用される構造では重要です。コンパイラーは、そのような型の変数が静的に、またはスタック上で宣言された場合、適切なアライメントで割り当てられることを保証します。

しかし、そのような型のオブジェクトが new 構文によって動的に割り当てられる場合、コンパイラーはこれまで適切なアライメントを保証できませんでした。これは、C++ 言語では (プログラマーが必要に応じて制御できる) 限定された割り当て方法が必要なためで、これらの方法は特定のアライメントをサポートしていません。これらの割り当て方法はすべて、いくつかのアライメント値ですべてに対応できると想定しており、決まったアライメント値のみを保証しています。

これまで、特定の型に対してより厳密なアライメントを保証する場合、プログラマーが割り当てを制御しなければなりませんでした。その 1 つの方法は、適切なアライメントごとにメモリーを割り当て、割り当てを行わない new 配置構文を使用します。次に例を示します。

正しくないアライメント
new X
正しいアライメント
new (_mm_malloc(sizeof(X), alignof(X))) X

これは面倒でミスを引き起こしやすい方法です。

別の方法は、クラス固有の割り当て関数 operator new と解放関数 operator delete を使用します。次に例を示します。

class alignas(64) X {
  double elem[8];

public:
  void *operator new(size_t size){
    return _mm_malloc(size, alignof(X));
  }

  void operator delete(void *p){
    return _mm_free(p);
  }
};

この方法は、クラスが使用されている個所ではなく、クラスを変更するため、より簡単です。ただし、クラスの配列が動的に割り当てられたり、nothrow 割り当てが使用されるケースに対応できるように、いくつかの関数を追加する必要があるため、ある程度の作業が発生します。

自動的にアライメントされる動的割り当て

本バージョンのコンパイラーでは、新しいヘッダーをインクルードするだけで、アライメントされたデータを正しく動的に割り当てることができます。

#include <aligned_new>

このヘッダーをインクルードすると、アライメントされた型の new 構文は自動的にその型のアライメントでメモリーを割り当てます。

Windows® では、ソースを変更しなくても、/FI コマンドライン・オプションを指定してプライマリー・ソース・ファイルの最初にファイルを追加するように指示できます。

実装

このセクションでは、新機能の言語規則を説明します。(アライメント以外の理由で必要な場合) アライメントされたデータの動的割り当て/解放を制御する方法を次に示します。

ヘッダー <aligned_new> は、アライメントを意識した新しい割り当て/解放関数を定義します。各関数では、アライメント引数を指定します。

void *operator new    (size_t, align_val_t); 
void *operator new    (size_t, align_val_t, nothrow_t const &); 
void operator delete  (void *, align_val_t); 
void operator delete  (void *, align_val_t, nothrow_t const &); 
void *operator new[]  (size_t, align_val_t);
void *operator new[]  (size_t, align_val_t, nothrow_t const &);
void operator delete[](void *, align_val_t); 
void operator delete[](void *, align_val_t, nothrow_t const &);

align_val_t は、次のように、コンパイラーによって内部で宣言されます。

namespace std { 
  enum class align_val_t: size_t; 
}; 

つまり、std::align_val_t はスコープ付き列挙型であり、暗黙に整数型へ変換することはできませんが、std::size_t と同じ範囲で、同じように表現されます。

アライメントが (2 * sizeof(void *)) よりも大きい型の new 構文を処理する場合、コンパイラーは通常の C++ 規則に従って引数リストを作成します。引数リストでは、サイズ引数の後に追加で型 align_val_t のアライメント引数が指定されます (new 構文の配置引数がある場合はこの後に続きます)。そして、多重定義の解決により、それらの引数で呼び出すことができるアライメントを意識する operator new 関数または operator new[] 関数を検索します。アライメントを意識する関数が見つからない場合は、引数リストからアライメント引数を削除して、再度多重定義の解決を試みます。2 回目も失敗するとエラーになります。

クラス固有の割り当て/解放関数

アライメントされたクラスに対するクラス固有の割り当て/解放関数がすでに定義されている場合、<aligned_new> をインクルードしてもプログラムの動作は変わりません。これは、グローバル関数よりもクラス固有の関数が優先され、<aligned_new> はグローバル関数のみ定義するためです。

アライメントの異なるクラスを含むクラス階層の基本クラス用にクラス固有の割り当て/解放関数を記述しない限り、アライメントを意識する割り当て/解放関数でアライメント引数は不要でしょう。代わりに、クラス固有の割り当て/解放関数で適切なアライメントが得られます。

グローバルな割り当て/解放関数の置換

プログラムでグローバルな割り当て/解放関数を定義して標準ライブラリー関数を置換し、非配置 new 構文によりアライメントされたデータの割り当てを行い、<aligned_new> がそのような new 構文の前にインクルードされている場合、プログラムの動作は変わります。プログラムの割り当て関数を使用せず、代わりにインテルにより提供されるアライメントを意識する割り当て関数を使用して割り当てが行われます。グローバルな割り当て/解放関数を置換するプログラムでは、<aligned_new> をインクルードするかどうかを慎重に判断する必要があります。

プログラムでグローバルな割り当て/解放関数を置換し、それらの関数でアライメント引数を指定するコンパイラーの機能を利用する場合は、<aligned_new> をインクルードすべきではありません。このファイルには、アライメントを意識する関数のインライン展開の定義が含まれるため、プログラムの定義と競合したり、プログラムの定義よりも優先されます。代わりに <aligned_new> は、置換する、アライメントを意識する関数のプログラム固有の宣言や定義を記述する際の参考にしてください。