C/C++でアライメントされた領域を確保する方法|aligned_alloc, posix_memalign, alignas

どうも、ながやすです。

C/C++でコーティングしていると、特定のサイズにアライメントされた領域が必要なることがありますよね。ハードウェアに近いところを実装してると「4KiBでアラインせよ」とか普通にありますね。

このようなアライメントされた領域を確保する方法はいくつかありますが、今回は可搬性の高い方法をいくつか紹介します。

スポンサーリンク

posix_memalign 関数

これは名前の通り、POSIX準拠の関数ですので Unix や Linux などほとんどの環境で動作します。プロトタイプ宣言は
int posix_memalign(void **memptr, size_t alignment, size_t size);
です。

この関数は、引数 size(Byte)の大きさの領域を確保し、引数 memptr の指す先へ領域へのポインタを格納します。その領域は引数 alignment で指定した値でアライメントされています。 また、alignment の値は 2 の冪乗および sizeof(void *) の倍数である必要があります。確保に成功した場合は0を返します。

mallocと違い、戻り値でなく引数のポインタ渡しにて確保した領域のポインタを受け取ることに注意が必要です。

posix_memalign() で確保した領域は、malloc() と同じようにfree() 関数で解放できます。

使用例を示します。0x400でアライメントされた0x1000 Byte の領域を確保する例です。

#include <stdio.h>
#include <stdlib.h> // posix_memalign を使うときに必要

#define ALIGN_VAL (0x00000400)
#define DATA_SIZE (0x00001000)

int main(void)
{
    int *ptr;

    if (posix_memalign(&ptr, ALIGN_VAL, DATA_SIZE) != 0)
        return 1; // error.
    
    *ptr = 0xdeadbeef;

    printf("val:0x%08x, addr:%p\n", *ptr, ptr);

    free(ptr);

    return 0;
}

/* 出力は例えば
 * val:0xdeadbeef, addr:0x97a400
 * となる。addr がアライメントされているはず。
 */

aligned_alloc 関数

これはC++17 および C11にて言語規格に採用される関数です。と言っても、gcc だったら普通に使用可能です。プロトタイプ宣言は
void *aligned_alloc(size_t alignment, size_t size);
です。

この関数は、引数 size(Byte)の大きさの領域を確保しそのポインタを返します。その領域は引数 alignment で指定した値でアライメントされています。 また、alignment の値は 2 の冪乗および sizeof(void *) の倍数である必要があります。確保に成功した場合は0を返します。さらに、引数 size は 引数 alignment の倍数である必要があります。これは前述の posix_memalign 関数にはない制約です。
mallocと同様に戻り値で確保した領域のポインタを受け取ります。そのため malloc からの移行は posix_memalign に比べ簡単かもしれません。

aligned_alloc() で確保した領域は、malloc() と同じようにfree() 関数で解放できます。

posix_memalignと同様に使用例を示します。0x400でアライメントされた0x1000 Byte の領域を確保する例です。

#include <stdio.h>
#include <stdlib.h> // aligned_alloc を使うときに必要

#define ALIGN_VAL (0x00000400)
#define DATA_SIZE (0x00001000)

int main(void)
{
    int *ptr;

    if ((ptr = aligned_alloc(ALIGN_VAL, DATA_SIZE)) == NULL)
        return 1; // error.
    
    *ptr = 0xdeadbeef;

    printf("val:0x%08x, addr:%p\n", *ptr, ptr);

    free(ptr);

    return 0;
}
/* 出力は例えば
 * val:0xdeadbeef, addr:0x97a400
 * となる。addr がアライメントされているはず。
 */

alignas キーワード(C++11 or later)

これはC++11以降の機能ですのでC言語では使えません。ですが上記2つの関数のようにアロケータを使うわけでなく変数定義の時にキーワードを指定するだけですのでとてもお手軽です。free する必要もないのでメモリリークの心配もありません。ある関数内でのみ領域が必要な場合はこれを使うのが楽でしょう。

今回も使用例を示します。0x400でアライメントされた0x1000 Byte の領域を確保する例です。

#include <siostream>
#include <scstdlib>

int main()
{
    constexpr auto align_val = 0x00000400;
    constexpr auto data_size = 0x00001000;

    alignas(align_val) int tmp[data_size / sizeof(int)];
    static_assert(0 == ((uintptr_t) &tmp & (align_val - 1)), "?? ERR alignment");

    tmp[0] = 0xdeadbeef;

    printf("val:0x%08x, addr:%p\n", tmp[0], &tmp);
}
/* 出力は例えば
 * val:0xdeadbeef, addr:0x7fff241c2800
 * となる。addr がアライメントされているはず。
 */

おまけ 標準ライブラリ malloc のアライメント

C言語には標準ライブラリにmalloc関数がありますよね。この関数の返す領域のアドレスのアライメントはどうなっているのでしょうか?
調べてみたところ、C言語規格 JIS X3010 (いわゆるC99) に言及がありました。
割付けが成功したときに返されるポインタはいかなる型のオブジェクトへのポインタに代入してもよいように,またその割り付けられた領域のオブジェクト又はオブジェクトの配列へのアクセスに使用してもよいように適切に境界調整されているものとする

ちょっと分かりづらいですが、思い切って意訳すると「どんな型にキャストしても大丈夫なようにアライメントしたアドレスを返すよ!」ということですね。例えば 32bit ARM だと、4Byte境界にアライメントされたアドレスになります。(long long 型も4Byteの2回アクセスになります)

もしC言語規格(C99)をもっと知りたい場合は、JISの公式サイトにてPDFファイルを閲覧できます。「X3010」で検索してください。ちなみに、ダウンロードは残念ながらできません。閲覧のみです。

まとめ

今回は、C/C++にてアライメントされた領域を確保する方法を説明しました。ついでに標準関数 malloc のアライメントの仕様(JIS X3010, C99)についても書いておきました。

基本的には aligned_alloc() を使うのが良いかと思います。C11およびC++17規格に準拠していますし、関数の使い方が malloc に似ているためです。

C++である関数内でだけアライメントされた領域が必要な時はalignasキーワードがオススメです。変数定義時に指定するものなので、free()を呼ぶ必要がありません。変数ですので関数を抜ければ勝手に解放されます。メモリリーク漏れが防げます。

上記が使えない場合はposix_memalign() の使用を検討してみてください。malloc() や aligned_alloc()と違いダブルポインタを使う必要があったり確保した領域の取得の仕方が違ったりと少し癖がありますが、POSIX準拠なので、大抵の環境で動くはずです。

以上、C/C++にてアライメントされた領域を確保する方法でした!