arm: unsignedはどうなのか
2024/10/21
ふと、最近のコンピュータ言語(最近じゃないかも)にはunsigned
が扱えないものがあることが気になった。
Java にはなかった。C++ から置き換えるとき苦労したような(昔のCなどには64bit型とかなかったので)。
JavaScript にはそもそも整数型とかそういうのがなかった。bignumber
とか使ったんだったか。
C# にはあった。
Go にはあった。
そして Kotlin には・・・あった。
えー、あるんだ。プリミティブ型ではなく存在するということか。
符号付きと符号無しが演算に混ざると面倒だ。
C言語で符号付きの有無が混ざってビット幅が違ったりするとどうなるのか全然覚えられなかった(そして覚えてない)。
寄せるなら符号付きのみということになるか。
最近は 64bit 演算くらいまでは標準でサポートしているので、ライブラリを使わずに数億くらいの数が扱えるということもあろう。
そういう言語的な複雑さをなくすためだけなのか、あるいは CPU にもそういう傾向があるのか。
昔使っていた SH-4 という CPU も unsigned
にする命令があって「signedの方が効率が良いですよ」と営業さんに説明された気がする(当時はまだSHARPだったと思う)。
基本的に、コンパイラにお任せするのが一番よいはずだ。
ただ、どうとでも書けるし、どう書いてもよい場合の指針がほしい。
例えば、2倍した値を返すだけの関数があって、その引数は 8bit の範囲しかないことが分かっていたとする。
そのとき、8bit で書いた方がよいのか 32bit で書いた方が良いのか。
signed
がよいのか unsigned signed
がよいのか。
uint32_t twice_uint32(uint32_t a)
{
return a * 2;
}
uint8_t twice_uint8(uint8_t a)
{
return a * 2;
}
-mcpu=cortex-m33
でコンパイルするとこうだった。
2倍なので左 1bit シフトするところまでは同じ(お尻のs
はAPSRフラグを変更する意味)。
32bit の場合はそのまま終わるのだが 8bit の場合は AND している。
お尻の .w
は32bit 命令の意味だ。
0xff
ではなく 0xfe
なのはビットシフトで一番下が0
なことが決まっているからか。
00000010 <twice_uint32>:
10: 0040 lsls r0, r0, #1
12: 4770 bx lr
0000001c <twice_uint8>:
1c: 0040 lsls r0, r0, #1
1e: f000 00fe and.w r0, r0, #254 ; 0xfe
22: 4770 bx lr
ちなみに、uint32_t
と int32_t
の違いはなかった。
コードが小さいので最適化された結果なのかもしれないが、どうせアセンブラにして全部確認しようとは思わないだろうからいいや。
おまけ
エンディアン逆転
ARM-v7M の命令を眺めていたら REV
という 32bit でエンディアンを逆転する命令があった。
これはいい!と思ったが、C言語からどうやればよいのかわからん。
ARM独自のライブラリを使うとそれはそれで面倒なことになりそうだし。
回答の一番上にあったコードと、その次にあった built-in関数 を私も試してみる。
// unsigned.c
#include <stdint.h>
// https://stackoverflow.com/questions/75056099/how-can-i-elegantly-take-advantage-of-arm-instructions-like-rev-and-rbit-when-wr
uint32_t endianize(uint32_t input)
{
return ((input >> 24) & 0x000000FF) |
((input >> 8) & 0x0000FF00) |
((input << 8) & 0x00FF0000) |
((input << 24) & 0xFF000000) ;
}
// https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html
uint32_t endianize2(uint32_t input)
{
return __builtin_bswap32(input);
}
コンパイラは Nordic の ncs を使う。
Windows を使っているのでコマンドプロンプトではなく git インストールで一緒に入った MINGW の bash を使っている。
$ /c/ncs/toolchains/cf2149caf2/opt/zephyr-sdk/arm-zephyr-eabi/bin/arm-zephyr-eabi-gcc.exe -O3 -c -mcpu=cortex-m33 unsigned.c
$ /c/ncs/toolchains/cf2149caf2/opt/zephyr-sdk/arm-zephyr-eabi/bin/arm-zephyr-eabi-objdump.exe -S unsigned.o
unsigned.o: file format elf32-littlearm
Disassembly of section .text:
00000000 <endianize>:
0: ba00 rev r0, r0
2: 4770 bx lr
00000004 <endianize2>:
4: ba00 rev r0, r0
6: 4770 bx lr
うむ。
r0
レジスタは戻り値になるんだったっけな。
x86_64 の gcc も入っていたので同じことをやってみよう。
$ /c/ncs/toolchains/cf2149caf2/opt/zephyr-sdk/x86_64-zephyr-elf/bin/x86_64-zephyr-elf-gcc.exe -O3 -c unsigned.c -o unsigned-x86.o
$ /c/ncs/toolchains/cf2149caf2/opt/zephyr-sdk/x86_64-zephyr-elf/bin/x86_64-zephyr-elf-objdump.exe -S unsigned-x86.o
unsigned-x86.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <endianize>:
0: 89 f8 mov %edi,%eax
2: 0f c8 bswap %eax
4: c3 ret
5: 66 66 2e 0f 1f 84 00 data16 cs nopw 0x0(%rax,%rax,1)
c: 00 00 00 00
0000000000000010 <endianize2>:
10: 89 f8 mov %edi,%eax
12: 0f c8 bswap %eax
14: c3 ret
ret
はサブルーチンから戻る命令。
CISC なので戻るアドレスが入った専用のレジスタがあるんだろう。
どちらもほぼ同じなのだが、ビット演算で書いた方は ret
の後ろになんか入っている。
アラインメントのゴミとかコメントとかそういうのだろうか?
Arm でも x86_64 でもなにがしかの最適化がかかるのが確認できた。
賢いね、コンパイラ。
私が書くときは先に &
していたような気がする。
これでも rev
に置き換えてくれた。
uint32_t endianize3(uint32_t input)
{
return (input >> 24) |
((input & 0x00ff0000) >> 8) |
((input & 0x0000ff00) << 8) |
(input << 24) ;
}