C/C++にて特定のサイズでアライメントされた領域を確保する方法を説明します。


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 の領域を確保する例です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#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 の領域を確保する例です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#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 の領域を確保する例です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#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回アクセスになります)

Note

もし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++にてアライメントされた領域を確保する方法でした!