SSブログ

構造体を返す関数と構造体を受け取る関数 [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ビットに分割して渡されます。

まとめ

本日のまとめです。

  1. 構造体が32ビット以内で表現できる時には、パッキングされて通常の変数と同じようにやり取りされる。パッキング・アンパッキングのコストは安くない。
  2. 32ビットを超えるサイズの構造体を返す関数は、呼び出し前にスタックにメモリ領域を確保し、そのアドレスをレジスタ r0 に与えて呼び出す。残りの引数は、 r1, r2, r3 レジスタ、スタックの順に格納される。
  3. 関数の引数に構造体を与えた場合、通常の引数と同じルールでレジスタおよびスタックに展開される。
こんなところでしょうか。

参考サイト

Chapter 3. The Cortex-M0 Instruction Set
ARM が提供するサイトで、 Cortex-M0 の命令が参照できます。

参考文献

プログラミング言語C 第2版 ANSI規格準拠

プログラミング言語C 第2版 ANSI規格準拠

  • 作者: B.W. カーニハン
  • 出版社/メーカー: 共立出版
  • 発売日: 1989/06/15
  • メディア: 単行本
苦しんで覚えるC言語

苦しんで覚えるC言語

  • 作者: MMGames
  • 出版社/メーカー: 秀和システム
  • 発売日: 2011/06/24
  • メディア: 単行本

nice!(0)  コメント(0)  トラックバック(0)  このエントリーを含むはてなブックマーク#

nice! 0

コメント 0

コメントを書く

お名前:
URL:
コメント:
画像認証:
下の画像に表示されている文字を入力してください。

トラックバック 0

トラックバックの受付は締め切りました