発端はこれ。
http://stackoverflow.com/questions/29538935/why-does-unsigned-char0x80-24-get-sign-extended-to-0xffffffff80000000-64
C言語では(unsigned char)0x80<<24
は((int)(unsigned char)0x80)<<24
として解釈されるという話。そしてこの結果は未定義らしい。大抵はINT_MINになり、これをuint64_tにキャストすると0xffffffff80000000
になってしまう。
では、8バイトの配列をunsigned long longとして読み込むにはどう書くのが良いのだろうか。-Sオプションでアセンブリを出力し、それを読んだりdiffを取ったりして検証してみた。
read32()
旧コード
unsigned int read32(const void *p){
const unsigned char *x=(const unsigned char*)p;
return x[0]|(x[1]<<8)|(x[2]<<16)|(x[3]<<24);
}
訂正コード
unsigned int read32(const void *p){
const unsigned char *x=(const unsigned char*)p;
return x[0]|(x[1]<<8)|(x[2]<<16)|((unsigned int)x[3]<<24);
}
最適化がない状態のgccだとなぜか命令が1個増えたが、それ以外では、gcc/clangおよび-m32/-m64ともに差はなかった。移植性を考えて後者を採用する。
(unsigned int)INT_MINは0x80000000になるので問題なく動いていたのだろう。
read64()
下位ビット
まず、read32()の返値型をunsigned long longに変えただけのものを考える。gccでアセンブリを出力させてみた。
旧コード
unsigned long long read32(const void *p){
const unsigned char *x=(const unsigned char*)p;
return x[0]|(x[1]<<8)|(x[2]<<16)|(x[3]<<24);
}
movl 4(%esp), %eax
movl (%eax), %eax
cltd
ret
訂正コード
unsigned long long read32(const void *p){
const unsigned char *x=(const unsigned char*)p;
return x[0]|(x[1]<<8)|(x[2]<<16)|((unsigned int)x[3]<<24);
}
movl 4(%esp), %eax
xorl %edx, %edx
movl (%eax), %eax
ret
32bit
-
32bitモードでは64bit整数はedx:eaxに格納される(らしい)。
-
cltd(cdq)は、eaxを符号拡張してedx:eaxに代入する命令である。つまり、edxが、eaxが0x7fffffff以下なら0、そうでなければ0xffffffffで埋められるという命令である。
-
clangでは
movl %eax, %edx; sarl $31, %edx
となっているが、sarが符号付きであることを考えると同じことである。 -
今回、どのような値であっても0xffffffffで埋めてほしくないので、これは望ましくない。
-
一方、後者のコードではedxが0クリアされており、これは正しい状況である。
-
gccの最適化なし状態も似たような状況であった。
64bit
- 前者では最適化なしだとgcc/clangともにcltq命令を使う。eaxを符号拡張してraxに代入する命令。
- 後者は
movl %eax, %eax
であった。64bitではこの命令はnopではなくraxの上位32bitをクリアする命令になるらしい。知らなかった。 - 最適化ありのgccだと、
movslq (%rdi), %rax
vsmovl (%rdi), %eax
であった。movslqは符号を拡張して代入する命令。 - いずれにせよ、後者のコードだけが正解という結論となる。
- なお、キャストをunsigned longに変更すると、gcc/clangとも、 64bit版でのみunsigned int版と異なったコードが生成される。 unsigned intにしておくのが無難であろう。
上位ビット
こちらは大きく分けて2通りの解法がある。
//(A)
unsigned long long read64(const void *p){
const unsigned char *x=(const unsigned char*)p;
return x[0]|(x[1]<<8)|(x[2]<<16)|((unsigned int)x[3]<<24)|( (unsigned long long)(x[4]|(x[5]<<8)|(x[6]<<16)|((unsigned int)x[7]<<24)) <<32);
}
//(B)
unsigned long long read64(const void *p){
const unsigned char *x=(const unsigned char*)p;
return x[0]|(x[1]<<8)|(x[2]<<16)|((unsigned int)x[3]<<24)|((unsigned long long)x[4]<<32)|((unsigned long long)x[5]<<40)|((unsigned long long)x[6]<<48)|((unsigned long long)x[7]<<56);
}
コンパイラ | -m32 | -m32 -O2 | -m64 | -m64 -O2 |
---|---|---|---|---|
gcc | 差異あり | 差異あり | ||
clang | 差異あり | 差異あり |
-
gccの場合、最適化すると、32bitの場合
movl 4(%esp), %eax; movl 4(%eax), %edx; movl (%eax), %eax; ret
となり、64bitの場合movq (%rdi), %rax;ret
となる。一切の算術演算がない完璧な最適化である。 -
clangの場合、-m32 -O2で差異がないのは、上位ビットの演算をedxに対して行うように判断されるからである。
-
Android NDKでの調査の結果は、最適化なしの場合はmips/armともコード(A)の方が命令数が少なく、最適化ありでもarm -marm(※not thumb)の場合はコード(A)の方が命令数が少なかった、というものだった。
結論
処理系間でのパフォーマンスバランスを考えると、コード(A)を使うのが最良と思われます。
以前のコードはx[3]とx[7]をunsigned intにキャストしていなかったのが敗因でした。
実はキャストだけで命令数が増えると思っていましたが、算術演算はオペランド幅にかかわらずレジスタにロードしてから行うはずということを考えるとそんなことはなかったようです。