memcpyと構造体直接代入

直接代入が使えるのは知ってたけど、保証されている動作なのか判って
なかったので今まで使ってなかった。どうやらANSI CではOKと云うことらしい。
折角なので、どれだけ差が出るのか調べてみた。


サンプルコードは以下

struct addrinfo a1,a2;
//memcpy経由
memcpy(&a1,&a2,sizeof(struct addrinfo));
//直接代入
a1 = a2;
  • memcpy版のdisassemble出力 (gcc3.4.4 -O0)
0x080483c1 <main+33>:   push   $0x20
0x080483c3 <main+35>:   lea    0xffffffb8(%ebp),%eax
0x080483c6 <main+38>:   push   %eax
0x080483c7 <main+39>:   lea    0xffffffd8(%ebp),%eax
0x080483ca <main+42>:   push   %eax
0x080483cb <main+43>:   call   0x80482e8 
0x080483d0 <main+48>:   add    $0x10,%esp
  • 構造体代入版のdisassemble出力 (gcc3.4.4 -O0)
0x080483da <main+58>:   mov    $0x8,%eax
0x080483df <main+63>:   mov    %eax,%ecx
0x080483e1 <main+65>:   repz movsl %ds:(%esi),%es:(%edi)

と、構造体代入だと反復命令に展開される。確かに速いのも納得。

  • memcpy版のdisassemble出力 (gcc3.4.4 -O2)
0x08048377 <main+15>:   mov    $0x8,%ecx
0x0804837c <main+20>:   repz movsl %ds:(%esi),%es:(%edi)
  • 構造体代入版のdisassemble出力 (gcc3.4.4 -O2)
0x08048380 <main+24>:   mov    $0x8,%ecx
0x08048385 <main+29>:   repz movsl %ds:(%esi),%es:(%edi)

オプティマイズ指定すると両方とも同じコード出力となった。
ここで、addrinfoのサイズが0x20(32)byteでmovsl*8と丁度合って
たからなのかと思い、合ってないサイズで試してみた。


#以降は-O2において直接代入とmemcpyの結果は大体同じなので片方は省略。

  • struct サイズ 33の場合 (gcc3.4.4 -O2)
0x08048388 <main+32>:   mov    $0x8,%cl
0x0804838a <main+34>:   sub    $0x18,%esp
0x0804838d <main+37>:   repz movsl %ds:(%esi),%es:(%edi)
0x0804838f <main+39>:   lea    0xffffffc8(%ebp),%eax
0x08048392 <main+42>:   movsb  %ds:(%esi),%es:(%edi)

repz movslとmovsbの組み合わせで表現されている。


更に大きいデータをコピーする場合はどうなるのかと思いこちらも試してみた。

  • struct サイズ 1024の場合 (gcc3.4.4 -O2)
0x080483cc <main+44>:   push   $0x400
0x080483d1 <main+49>:   push   %ebx
0x080483d2 <main+50>:   push   %esi
0x080483d3 <main+51>:   call   0x80482e8

memcpyを普通にcallするようになってしまった。
サイズを調整しながら何度か試したら64を越える場合はmemcpy処理になる模様。


で、問題のmemcpyの中は以下のような感じ。

0x0804dfa0 <memcpy+0>:  push   %ebp
0x0804dfa1 <memcpy+1>:  mov    %esp,%ebp
0x0804dfa3 <memcpy+3>:  mov    0x8(%ebp),%edx
0x0804dfa6 <memcpy+6>:  push   %edi
0x0804dfa7 <memcpy+7>:  cmpl   $0x7,0x10(%ebp)
0x0804dfab <memcpy+11>: push   %esi
0x0804dfac <memcpy+12>: mov    %edx,%edi
0x0804dfae <memcpy+14>: mov    0xc(%ebp),%esi
0x0804dfb1 <memcpy+17>: jbe    0x804dfd0 <memcpy+48>
0x0804dfb3 <memcpy+19>: mov    %edx,%ecx
0x0804dfb5 <memcpy+21>: neg    %ecx
0x0804dfb7 <memcpy+23>: and    $0x3,%ecx
0x0804dfba <memcpy+26>: mov    0x10(%ebp),%eax
0x0804dfbd <memcpy+29>: sub    %ecx,%eax
0x0804dfbf <memcpy+31>: cld
0x0804dfc0 <memcpy+32>: repz movsb %ds:(%esi),%es:(%edi)
0x0804dfc2 <memcpy+34>: mov    %eax,%ecx
0x0804dfc4 <memcpy+36>: shr    $0x2,%ecx
0x0804dfc7 <memcpy+39>: cld
0x0804dfc8 <memcpy+40>: repz movsl %ds:(%esi),%es:(%edi)
0x0804dfca <memcpy+42>: and    $0x3,%eax
0x0804dfcd <memcpy+45>: mov    %eax,0x10(%ebp)
0x0804dfd0 <memcpy+48>: mov    0x10(%ebp),%ecx
0x0804dfd3 <memcpy+51>: cld
0x0804dfd4 <memcpy+52>: repz movsb %ds:(%esi),%es:(%edi)
0x0804dfd6 <memcpy+54>: pop    %esi
0x0804dfd7 <memcpy+55>: mov    %edx,%eax
0x0804dfd9 <memcpy+57>: pop    %edi
0x0804dfda <memcpy+58>: leave
0x0804dfdb <memcpy+59>: ret
0x0804dfdc <memcpy+60>: nop
0x0804dfdd <memcpy+61>: nop
0x0804dfde <memcpy+62>: nop

まあ、やってることは同じでrepzプレフィックスによる反復実行でコピーしている。
小さいサイズの場合インライン化される様にgccが最適化してるんだろうなぁ。


結局の所どちらを使うか知り合いに聞いたらmemcpyの方が多かった。
そんな私もmemcpy派。