【C99】可変長配列を試してみる

2019-12-31

C99だと可変長配列(Variable-length array, VLA)というものが使えるということをいまさらながら知ったので、調べてみた。

可変長配列

C言語で配列を宣言する際に従来は配列の要素数は定数である必要があり、実行時にサイズが決定する場合には malloc で領域を確保する必要があった。 別の選択肢として、malloc の代わりに alloca を使用すると関数を抜ける時点で自動的に解放されるので便利 (ただしANSI Cではない)。

void foo(int len) {
int *array = alloca(sizeof(int) * len);
...
}

これがC99では可変長配列が使えるとのこと:

void foo(int len) {
int array[len];
...
}

用例

「ああ、これは alloca みたいなもんでしょ」と安易に思っていたところ、cppreference.comにいろいろ難しい例が載っていた:

  • sizeof でサイズを取得できる
  • goto で戻って繰り返せる
  • 関数のプロトタイプ宣言ではサイズに * を指定できる
    • 関数の実体定義時には値に式を指定する
  • スコープ内の型宣言に使える
    • typedef int VLA[m][m];

動作・実装の確認

void use_in_loop(int n) {
for (int i = 1; i <= n; ++i) {
int buf[i];
buf[0] = i;
printf("buf=%p, size=%zu\n", buf, sizeof(buf));
}
}

void use_with_goto(int n) {
int i = 1;
label:;
int buf[i];
buf[0] = i;
printf("buf=%p, size=%zu\n", buf, sizeof(buf));
if (++i <= n)
goto label;
}

int main() {
use_in_loop(3);
/* 実行結果:すべて同じアドレス
buf=0x7ffdc95b5030, size=4
buf=0x7ffdc95b5030, size=8
buf=0x7ffdc95b5030, size=12
*/
use_with_goto(3);
/* goto版も同じ!
buf=0x7ffdc95b5030, size=4
buf=0x7ffdc95b5030, size=8
buf=0x7ffdc95b5030, size=12
*/
return 0;
}

forループのスコープ内で使用した場合にはバッファが同じアドレスになるのはまあわかるとしても、gotoでも同じ動作になるとは「え、どうなってるの?スコープを抜けるわけじゃないのになんで戻るの?」という感じだった。

objdumpでコンパイル結果を見てみた:

$ clang -O2 -c vla.c && objdump -S vla.o

vla.o: file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <use_in_loop>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 41 57 push %r15
6: 41 56 push %r14
8: 41 54 push %r12
a: 53 push %rbx
b: 41 89 fe mov %edi,%r14d
e: 45 85 f6 test %r14d,%r14d
11: 7e 5d jle 70 <use_in_loop+0x70>
13: 41 83 c6 01 add $0x1,%r14d
17: bb 01 00 00 00 mov $0x1,%ebx // %ebx = n
1c: 41 bf 04 00 00 00 mov $0x4,%r15d // %r15 = 確保する領域のサイズ n * 4
22: 66 66 66 66 66 2e 0f data16 data16 data16 data16 nopw %cs:0x0(%rax,%rax,1)
29: 1f 84 00 00 00 00 00
30: 49 89 e4 mov %rsp,%r12 // 元々のスタックポインタを%r12に退避
33: 48 89 e0 mov %rsp,%rax
36: 48 8d 0c 9d 0f 00 00 lea 0xf(,%rbx,4),%rcx // %rcx = n * 4 + 15
3d: 00
3e: 48 83 e1 f0 and $0xfffffffffffffff0,%rcx // 16バイト境界にアライメントする
42: 48 89 c6 mov %rax,%rsi
45: 48 29 ce sub %rcx,%rsi // %rsi = 元のスタックポインタ - (n * 4 + 15) & -16
48: 48 89 f4 mov %rsi,%rsp // スタックポインタをずらす
4b: 48 f7 d9 neg %rcx
4e: 89 1c 08 mov %ebx,(%rax,%rcx,1)) // buf[0] = n; %raxが元のスタックポインタ、%rcxが-((n * 4 + 15) & -16)
51: bf 00 00 00 00 mov $0x0,%edi // 第1引数 %edi: フォーマット文字列
56: 31 c0 xor %eax,%eax
58: 4c 89 fa mov %r15,%rdx // 第3引数 %rdx: %r15 = sizeof(buf)
5b: e8 00 00 00 00 callq 60 <use_in_loop+0x60> // printf呼び出し:第2引数%rsiはスタック操作結果(=buf)になっている
60: 4c 89 e4 mov %r12,%rsp // スタックをVLA操作前に戻す
63: 48 83 c3 01 add $0x1,%rbx
67: 49 83 c7 04 add $0x4,%r15
6b: 49 39 de cmp %rbx,%r14
6e: 75 c0 jne 30 <use_in_loop+0x30>
70: 48 8d 65 e0 lea -0x20(%rbp),%rsp
74: 5b pop %rbx
75: 41 5c pop %r12
77: 41 5e pop %r14
79: 41 5f pop %r15
7b: 5d pop %rbp
7c: c3 retq
7d: 0f 1f 00 nopl (%rax)

0000000000000080 <use_with_goto>:
80: 55 push %rbp
81: 48 89 e5 mov %rsp,%rbp
84: 41 57 push %r15
86: 41 56 push %r14
88: 41 54 push %r12
8a: 53 push %rbx
8b: 4c 63 f7 movslq %edi,%r14
8e: bb 01 00 00 00 mov $0x1,%ebx
93: 41 bf 04 00 00 00 mov $0x4,%r15d
99: 0f 1f 80 00 00 00 00 nopl 0x0(%rax)
a0: 49 89 e4 mov %rsp,%r12
a3: 48 89 e0 mov %rsp,%rax
a6: 48 8d 0c 9d 0f 00 00 lea 0xf(,%rbx,4),%rcx
ad: 00
ae: 48 83 e1 f0 and $0xfffffffffffffff0,%rcx
b2: 48 89 c6 mov %rax,%rsi
b5: 48 29 ce sub %rcx,%rsi
b8: 48 89 f4 mov %rsi,%rsp
bb: 48 f7 d9 neg %rcx
be: 89 1c 08 mov %ebx,(%rax,%rcx,1)
c1: bf 00 00 00 00 mov $0x0,%edi
c6: 31 c0 xor %eax,%eax
c8: 4c 89 fa mov %r15,%rdx
cb: e8 00 00 00 00 callq d0 <use_with_goto+0x50>
d0: 4c 89 e4 mov %r12,%rsp // スタックをVLA操作前に戻す
d3: 49 83 c7 04 add $0x4,%r15
d7: 4c 39 f3 cmp %r14,%rbx
da: 48 8d 5b 01 lea 0x1(%rbx),%rbx
de: 7c c0 jl a0 <use_with_goto+0x20> // gotoで戻る
e0: 48 8d 65 e0 lea -0x20(%rbp),%rsp
e4: 5b pop %rbx
e5: 41 5c pop %r12
e7: 41 5e pop %r14
e9: 41 5f pop %r15
eb: 5d pop %rbp
ec: c3 retq
ed: 0f 1f 00 nopl (%rax)

どちらもコンパイル結果は大体同じで、

  • VLAでの操作前のスタックポインタを退避(%r12)しておき、戻る前に復帰させる
  • VLAのsizeofは実行時にどこかの領域に格納しているんではなく、確保時の式と同じ値を使用する

alloca のように関数から抜けるときに一括でスタックポインタが巻き戻されるんじゃなく、ちゃんとスコープやラベルに応じて増減される。

雑多

C99の後、C11で必須の仕様ではなくオプション扱いになったとのこと。 サポートされてない場合には __STDC_NO_VLA__ が定義されている。

またC++でもサポートされてない(VLAとは違う可変長配列の仕様となっているらしい)。

参考