構造体を返す関数と構造体を受け取る関数 [PSoC]
twitter にて、 C の関数が構造体を返す事を教えてもらいました。 私が知ってる C と違う? GCC ARM と Cortex-M0 を題材に深めに調べてみました。
24ビットの構造体の場合
まず、みっつの8ビットフィールドが含まれた例を調べます。
struct point8 { int8 x, y, z; };
そして、この構造体を返す関数を作成しました。
struct point8 createPoint8(int8 x, int8 y, int8 z) { struct point8 p; p.x = x; p.y = y; p.z = z; return p; }
三つの引数を受け取って、構造体を作成して返す単純な関数です。 コンパイルの結果、以下のようなコードが作成されました。
23 createPoint8: 30 0000 FF23 mov r3, #255 31 0002 1940 and r1, r3 33 0004 1840 and r0, r3 35 0006 0902 lsl r1, r1, #8 36 0008 1340 and r3, r2 37 000a 1B04 lsl r3, r3, #16 38 000c 0843 orr r0, r1 40 000e 82B0 sub sp, sp, #8 43 0010 1843 orr r0, r3 45 0012 02B0 add sp, sp, #8 47 0014 7047 bx lr
三つの引数 x, y, z は、三つのレジスタ r0, r1, r2 にそれぞれ格納されます。 そして、8ビットマスクと左シフトを使いながら、24ビットの値を構成して、 r0 レジスタに格納して返します。 計算式は、以下のようになります。
r0 = ((x & 0xFF) | ((y & 0xFF) << 8) | ((z & 0xFF) << 16))
呼び出し側で関数からの返り値を変数に代入すると、以下のようなコードが生成されます。
270 000a 0B20 mov r0, #11 271 000c 1621 mov r1, #22 272 000e 2122 mov r2, #33 273 0010 FFF7FEFF bl createPoint8 275 0014 144C ldr r4, .L14 276 0016 2070 strb r0, [r4] 277 0018 030A lsr r3, r0, #8 278 001a 6370 strb r3, [r4, #1] 279 001c 000C lsr r0, r0, #16 280 001e A070 strb r0, [r4, #2]
構造体を8ビットごとに分解してからメモリ領域に格納しています。 コストが高そうです。
引数で構造体を渡す場合を以下の関数で確認します。
void printPoint8(struct point8 p) { printPoint(p.x, p.y, p.z); }
これは、三つの値を表示する関数ですが、実際に表示する部分は別の関数になっています。 この関数からは、以下のようなコードが生成されます。
103 printPoint8: 107 0000 011C mov r1, r0 108 0002 00B5 push {lr} 111 0004 020C lsr r2, r0, #16 112 0006 090A lsr r1, r1, #8 113 0008 83B0 sub sp, sp, #12 116 000a 40B2 sxtb r0, r0 117 000c 49B2 sxtb r1, r1 118 000e 52B2 sxtb r2, r2 119 0010 FFF7FEFF bl printPoint 122 0014 03B0 add sp, sp, #12 124 0016 00BD pop {pc}
受け取った構造体は32ビットの値として扱う事ができるので、実際の処理は、32ビットの値に対するものと等価になります。 レジスタ r0 の値を8ビットごとに分解してレジスタ r0, r1, r2 に格納し、関数 printPoint() を呼び出します。 何だか、これもコストが高そうです。
306 004c 2068 ldr r0, [r4] 307 004e FFF7FEFF bl printPoint8
呼び出し側では、レジスタ r0 に構造体を一括で読み出して関数 printPoint8() を呼び出します。 ここでは、構造体を構成するような事はしないようです。
48ビットの構造体の場合
次は、構造体のサイズを二倍にしてみました。
struct point16 { int16 x, y, z; };
全体で48ビットありますので、レジスタひとつには入りません。 これも構造体を返す関数を作成してコードを生成させてみました。
struct point16 createPoint16(int16 x, int16 y, int16 z) { struct point16 p; p.x = x; p.y = y; p.z = z; return p; }
134 createPoint16: 140 0000 0180 strh r1, [r0] 141 0002 4280 strh r2, [r0, #2] 142 0004 8380 strh r3, [r0, #4] 145 0006 7047 bx lr
なんだか、ずいぶん簡単になってしまいました。 16ビットの格納命令が三つ並んでいるだけです。 それぞれのレジスタに何が入っているかは、呼び出し側を確認するとわかります。
282 0020 04A8 add r0, sp, #16 283 0022 1249 ldr r1, .L14+4 284 0024 124A ldr r2, .L14+8 285 0026 134B ldr r3, .L14+12 286 0028 FFF7FEFF bl createPoint16 288 002c 201D add r0, r4, #4 289 002e 04A9 add r1, sp, #16 290 0030 0622 mov r2, #6 291 0032 FFF7FEFF bl memcpy
三つの引数が、レジスタ r1, r2, r3 で渡されているほか、レジスタ r0 には、スタック上に確保されたメモリ領域のアドレスが渡されます。 このメモリ領域は、関数が返した構造体を格納するための一時メモリ領域です。 その後、一時メモリ領域を memcpy() 関数を使って変数に格納します。 これも受け渡しのためのコストが高そうです。
引数で構造体を渡す関数を作成してコードを生成させました。
void printPoint16(struct point16 p) { printPoint(p.x, p.y, p.z); }
155 printPoint16: 159 0000 00B5 push {lr} 163 0002 03B2 sxth r3, r0 165 0004 0A1C add r2, r1, #0 166 0006 83B0 sub sp, sp, #12 169 0008 0114 asr r1, r0, #16 170 000a 12B2 sxth r2, r2 171 000c 181C mov r0, r3 172 000e FFF7FEFF bl printPoint 175 0012 03B0 add sp, sp, #12 177 0014 00BD pop {pc}
引数は、レジスタ r0, r1 で渡されて、三つの16ビットの値に再構成されて printPoint() 関数に渡されています。
310 0052 6068 ldr r0, [r4, #4] 311 0054 A168 ldr r1, [r4, #8] 312 0056 FFF7FEFF bl printPoint16
呼び出し側では、構造体変数の値をレジスタに r0, r1 に格納して関数 printPoint16() を呼び出しています。 呼び出し側のコストは抑制されているようです。
96ビット構造体の場合
構造体のサイズをさらに倍にしました。
struct point32 { int32 x, y, z; };
構造体を返す関数と生成コードは、以下のようになりました。
struct point32 createPoint32(int32 x, int32 y, int32 z) { struct point32 p; p.x = x; p.y = y; p.z = z; return p; }
202 createPoint32: 208 0000 0160 str r1, [r0] 210 0002 4260 str r2, [r0, #4] 212 0004 8360 str r3, [r0, #8] 215 0006 7047 bx lr
360 0036 04AD add r5, sp, #16 361 0038 281C mov r0, r5 362 003a 1F49 ldr r1, .L16+16 363 003c 1F4A ldr r2, .L16+20 364 003e 204B ldr r3, .L16+24 365 0040 FFF7FEFF bl createPoint32 367 0044 231C mov r3, r4 368 0046 0C33 add r3, r3, #12 369 0048 2A1C mov r2, r5 370 004a 43CA ldmia r2!, {r0, r1, r6} 371 004c 43C3 stmia r3!, {r0, r1, r6}
返された構造体を格納する場所をスタックに確保して、そのアドレスを r0 で渡しているのは、48ビットの構造体の場合と同じです。 違っているのは、返された値を変数に格納し直す部分です。
48ビット構造体では、 memcpy() 関数を使用して格納を行っていましたが、96ビットのばあいには、 ldmia, stmia 命令を使用しています。 この命令は、指定されたアドレスに対してレジスタ群を読み出し・書き込みを行います。 つまり、この1命令で96ビット分(12バイト)の読み書きができます。 ただし、32ビット単位でアクセスをするので、48ビットの構造体に対しては使用できませんでした。
引数で構造体を渡す関数と生成コードおよび呼び出し側のコードは、以下のようになりました。
void printPoint32(struct point32 p) { printPoint(p.x, p.y, p.z); }
225 printPoint32: 229 0000 00B5 push {lr} 232 0002 85B0 sub sp, sp, #20 235 0004 FFF7FEFF bl printPoint 240 000a 00BD pop {pc}
402 0082 E068 ldr r0, [r4, #12] 403 0084 2169 ldr r1, [r4, #16] 404 0086 6269 ldr r2, [r4, #20] 405 0088 FFF7FEFF bl printPoint32
構造体は、三つのレジスタ r0, r1, r2 で渡されます。 この時、レジスタにはそれぞれ x, y, z が入ってくるため、関数 printPoint() にはレジスタをそのまま渡せば良い事になります。 そのため、関数 printPoint32() では、一切のデータ操作を行っていません。
192ビット構造体の場合
さらに構造体のサイズを大きくします。
struct point64 { int64 x, y, z; };
ついに一時レジスタで値を渡せるデータ量を超えました。 構造体を返す関数とそのコードおよび呼び出し側のコードは、以下のようになりました。
struct point64 createPoint64(int64 x, int64 y, int64 z) { struct point64 p; p.x = x; p.y = y; p.z = z; return p; }
250 createPoint64: 256 0000 0260 str r2, [r0] 257 0002 4360 str r3, [r0, #4] 259 0004 009A ldr r2, [sp] 260 0006 019B ldr r3, [sp, #4] 262 0008 8260 str r2, [r0, #8] 263 000a C360 str r3, [r0, #12] 265 000c 029A ldr r2, [sp, #8] 266 000e 039B ldr r3, [sp, #12] 267 0010 0261 str r2, [r0, #16] 268 0012 4361 str r3, [r0, #20] 271 0014 7047 bx lr
373 004e 1D4A ldr r2, .L16+28 374 0050 1D4B ldr r3, .L16+32 375 0052 0092 str r2, [sp] 376 0054 0193 str r3, [sp, #4] 377 0056 1D4A ldr r2, .L16+36 378 0058 1D4B ldr r3, .L16+40 379 005a 0292 str r2, [sp, #8] 380 005c 0393 str r3, [sp, #12] 381 005e 281C mov r0, r5 382 0060 1C4A ldr r2, .L16+44 383 0062 1D4B ldr r3, .L16+48 384 0064 FFF7FEFF bl createPoint64 386 0068 201C mov r0, r4 387 006a 1830 add r0, r0, #24 388 006c 291C mov r1, r5 389 006e 1822 mov r2, #24 390 0070 FFF7FEFF bl memcpy
この場合でも、スタック上に確保された一時メモリ領域のアドレスがレジスタ r0 を介して渡されます。 引数のうち、 x はレジスタ r2, r3 で渡されますが、 y, z は、スタック上のメモリを使用します。 r1 が使用されませんが、これは64ビットの値を扱う場合には r2:r3 のペアを使うという規約によるものです。
関数の処理は、単純になりました。 スタックで渡された引数をスタックに確保された一時メモリ領域にコピーするだけの処理が行われます。
一時メモリ領域に返された値は、関数 memcpy() で変数に格納されます。
一方、構造体を引数で渡す関数は以下のようになりました。
void printPoint64(struct point64 p) { printPoint(p.x, p.y, p.z); }
281 printPoint64: 285 0000 84B0 sub sp, sp, #16 287 0002 10B5 push {r4, lr} 291 0004 0290 str r0, [sp, #8] 292 0006 0391 str r1, [sp, #12] 293 0008 111C mov r1, r2 294 000a 0492 str r2, [sp, #16] 295 000c 0593 str r3, [sp, #20] 297 000e 069A ldr r2, [sp, #24] 298 0010 FFF7FEFF bl printPoint 302 0014 10BC pop {r4} 303 0016 08BC pop {r3} 304 0018 04B0 add sp, sp, #16 305 001a 1847 bx r3
408 008c 211C mov r1, r4 409 008e 2831 add r1, r1, #40 410 0090 6846 mov r0, sp 411 0092 0822 mov r2, #8 412 0094 FFF7FEFF bl memcpy 414 0098 A069 ldr r0, [r4, #24] 415 009a E169 ldr r1, [r4, #28] 416 009c 226A ldr r2, [r4, #32] 417 009e 636A ldr r3, [r4, #36] 418 00a0 FFF7FEFF bl printPoint64
引数は、レジスタ r0, r1, r2, r3 の128ビットとスタックの64ビットに分割して渡されます。
まとめ
本日のまとめです。
- 構造体が32ビット以内で表現できる時には、パッキングされて通常の変数と同じようにやり取りされる。パッキング・アンパッキングのコストは安くない。
- 32ビットを超えるサイズの構造体を返す関数は、呼び出し前にスタックにメモリ領域を確保し、そのアドレスをレジスタ r0 に与えて呼び出す。残りの引数は、 r1, r2, r3 レジスタ、スタックの順に格納される。
- 関数の引数に構造体を与えた場合、通常の引数と同じルールでレジスタおよびスタックに展開される。
参考サイト
- Chapter 3. The Cortex-M0 Instruction Set
- ARM が提供するサイトで、 Cortex-M0 の命令が参照できます。
コメント 0