背景
アライメントまわりのことを調べていたらC++17でaligned_alloc
とnew
の新しいオーバーロードが入っていたようで、少し規格書(N4659)と元になったP0035R4にあたってみることにした。
(最初C++2aのN4727を見て書いていたのでN4659を確認したが、内容に特に変化はない)
アライメントとは
ほとんどのアーキテクチャは、任意の位置からメモリの内容を読み込むことができない。これはアクセスできないメモリ領域があるということではなくて、メモリから取ってこれるデータの位置とサイズには限界があるということだ。例えば、一回のメモリアクセスでは16バイト分のデータを16の倍数のアドレスからしか読み込めない、という制限がある。それがユーザーのコードにどう影響するかはアーキテクチャによって異なるわけだが、一つの値を一回のメモリアクセスで取ってこれなかった場合、最善でもオーバーヘッドが生じる。
例えば、中身が{x, y, z, w}
になっている8バイトのデータがあり、それをメモリから取ってきたいとする。もしこれがアクセス境界に並んでいなかった場合、アクセスは複数回生じるだろう(アーキテクチャに依るが、割り込みが発生する場合もある)。
一回目のアクセス v v v v | | |x|y|z|w| | | ^ ^ ^ ^ 二回目のアクセス
だがこれをメモリアクセスの境界にちゃんと並べられたなら、アクセスは一回で済む。
一回目のアクセス v v v v |x|y|z|w| | | | |
構造体の中にパディングが入る理由は、コンパイラがメモリを多少無駄遣いしても、構造体のメンバ、または構造体そのものへのアクセスを最適化しようと頑張るからだ。逆にメモリを全く無駄遣いできないときは、パディングを入れないようにコンパイラに伝えるか、最小限で済むようにメンバの配置を考える必要がある。
通常より大きなアライメントを指定したい状況
よくある例を挙げよう。多くのCPUがSIMD命令を持っている。これは複数の値に対して同じ命令を同時に適用できるというものだ。例えばIntel AVXは256bitの演算幅を提供しており、32bitのfloat
なら8つ、double
なら4つまでを同時に計算できる。これはイントリンシック関数を使うと
#include <immintrin.h> __m256d v1 = _mm256_set_pd(3.0, 2.0, 1.0, 0.0); __m256d v2 = _mm256_set1_pd(0.5); __m256d v3 = _mm256_add_pd(v1, v2); std::array<double, 4> result; _mm256_storeu_pd(result.data(), v3); // {0.5, 1.5, 2.5, 3.5} std::cout << '{' << result[0] << ", " << result[1] << ", " << result[2] << ", " << result[3] << "}\n";
のように書ける。ここで_mm256_add_pd(v1, v2)
が、4つの浮動小数点数の足し算を1命令(vaddpd)で行う命令に変換される。
ただし、SIMDレジスタへのロード・ストアは、メモリ側のアドレスに制約を課す。ロード・ストアの対象は256bit (32byte)幅にアラインしている必要がある(アラインされていない場合は注意してそれなりの扱いをしなければならない)。多くのアーキテクチャは普通そんな幅のアライメントをデフォルトにしない。これはユーザーが保証しなければならないのだ。
実を言うと上の例で使用したstoreu_pd
はストアする先が32byteにアラインされていない(unalignedな)場合にもデータを問題なく書き込んでくれるが、オーバーヘッドがかかる。アライメントされている前提で動くstore_pd
が存在し、こちらの方が高速に動作する。速度が必要だからSIMDを書いているのであって、オーバーヘッドがかかっては本末転倒である。なのでstore_pd
を使える方が望ましい。
SIMD以外にも、キャッシュラインに対応したアライメントに調整することでキャッシュヒット率を最適化するなどの用途があるようだ。
C++11 alignasの問題点
C++11にはalignas
やalignof
、std::aligned_storage
、std::align
が導入された。alignas
はアライメントを(通常より大きな値に)指定できるものである。
struct alignas(32) d4 { double v[4]; };
このようにするとこの構造体は32byte境界にアラインされる――ただし、静的確保した場合のみ。
動的確保した領域は指定したとおりにアラインされている保証はないのだ。つまり、new d4
やstd::vector<d4> vec;
のそれぞれの要素が正しくアライメントされている保証はない。
new
やmalloc
はあらゆる型のデフォルトのアライメント要求を満たすような領域を確保するが、デフォルトより大きいアライメント要求には応える保証はない。
これは困る。これを回避するには、2つ方法があった。
1つめはメモリを必要量より大きめに確保しておいて、アライメントがあう部分を取り出してそこだけを使うという戦略で、std::align
はその用途にもってこいだ。だがこれだとメモリ管理が異様に複雑になってしまう。
2つめは_mm_malloc/_mm_free
やposix_memalign
、_aligned_malloc
などのアライメントを保証する動的確保関数を使用することだ。だが、これらは非標準であるので、切り替えが面倒だ(それを言うなら_mm_add_ps
などはアーキテクチャ依存もいいとこだが)。
C11にはstdlib.h
にaligned_alloc
が入ったが、C++11には入らなかった。よってBoost.alignなどはコンパイラと環境を見て使用可能な関数をマクロで切り替えている。
言語レベルのアライメントサポートとして、これは片手落ちだ。
C++17におけるアライメントサポート
さて、C++17ではめでたく動的確保時にアライメントを保証できる機能が追加されることになった。C11に追従して<cstdlib>
にaligned_alloc
が追加され、またnew
についてalign_val_t
を取るオーバーロードが追加された(P0035, N4659 § 21.6)。
そのために、align_val_t
がまず定義された。意図しない型変換を防ぐため、enum class
になっている。ただ、これはopaque typedef相当の利用方法なので特に値が定義されているわけではない。
namespace std { enum class align_val_t : size_t {}; }
そして、以下のようなオーバーロードが追加された。
void* operator new(std::size_t, std::align_val_t); void* operator new[](std::size_t, std::align_val_t); void operator delete(void*, std::align_val_t); void operator delete[](void*, std::align_val_t); void operator delete(void*, std::size_t, std::align_val_t); void operator delete[](void*, std::size_t, std::align_val_t);
align_val_t
として不適当な値を渡した場合、その動作は未定義である(§ 21.6.2)。
これらの呼ばれ方は少し変わっている。通常のnewの結果よりも大きなアライメントを要求する型についてnew T
を行った時、自動的にalign_val_t
版が呼ばれ、そうでない場合は普通のバージョンが呼ばれるのである(§ 21.6.2.1 Effects、§ 21.6.2.2 Effects)。
「通常のnewの結果よりも大きなアライメントを要求する型(new-extended alignment)」は、__STDCPP_DEFAULT_NEW_ALIGNMENT__
を超えるアライメント要求を持っているかどうかで判定される(§ 6.6.5)。このマクロはstd::size_t
型の整数リテラルに展開され(§ 19.8)、それより大きいアライメント指定を持つ型にはnew(align_val_t(alignof(T)))
が呼ばれる。定義済みマクロとして提供される理由は、標準ライブラリヘッダを何一つincludeしていなくてもこの値を知りたい可能性があるからだ。その値を知るためだけに<new>
などをincludeする必要はないだろう。
要は、ユーザーはnew
に関してあまり頭を悩ませる必要はなく、大きなアライメントを好きに選択すれば、あとは必要に応じて勝手に必要な方のnew
が選択して解決されるのだ。素晴らしい。
pitfall
ところで、new/delete
はクラスでオーバーロードすることができる。もし再利用しようとしているC++17以前のコードで、クラスが専用new
を持っていたとしたら、そしてそのクラスが通常のnewよりも大きなアライメントを要求するなら、どうなるべきだろうか。
ユーザー定義new
は中でメモリ確保以外の副作用を持てるので、もしそれが呼ばれず標準定義align_val_t
版が呼ばれた場合、知らぬ間にプログラムが壊れてしまう。これは恐怖である。このことを知らないプログラマは、自分のプログラムを完全に制御できていると思いながら、実際には呼ばれることのないnew
演算子を定義しているかもしれないのだ。このような状況はできる限り避けねばならない。今回の場合、クラス特異的なユーザー定義new
が提供されている場合はそれが呼ばれる必要がある。
呼び出し解決の順序は以下のようになる。
- クラス特異的でアライメント要求あり
new
- クラス特異的でアライメント要求無し
new
- グローバルでアライメント要求あり
new
- グローバルでアライメント要求なし
new
クラス特異的なnew
は、アライメント要求に関わらずグローバルなnew
より優先される。アライメント要求の有無に関しては、ある方がない方より優先される。