hiro99ma blog

何か技術的なこと

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 シフトするところまでは同じ(お尻のsAPSRフラグを変更する意味)。
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_tint32_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)                ;
}
< Top page