🐚

abs() の正体

に公開

Hacker's Delight という書籍があり、さまざまなフレームワークで参照文献として利用されていました。

https://amzn.asia/d/ecyASW0

OpenJDK の実装など見ているとコメントでよく見かけます。
(以下は OpenJDK の reverse_bits の実装で、コメントに Hacker's Delight という文字を見つけることができます。)

 public:
  inline T operator()(T v) const {
    // Based on Hacker's Delight Section 7-1
    U x = static_cast<U>(v);
    x = ((x & rep_5555) << 1) | ((x >> 1) & rep_5555);
    x = ((x & rep_3333) << 2) | ((x >> 2) & rep_3333);
    x = ((x & rep_0F0F) << 4) | ((x >> 4) & rep_0F0F);
    return byteswap(static_cast<T>(x));
  }
};

https://github.com/openjdk/jdk/blob/5f083abafc7abfaa46ddd053668cdfbfd2ad8a87/src/hotspot/share/utilities/reverse_bits.hpp#L65

出版が2012年なので、2025年の現状はどうなっているか気になったので、 abs を例に情報のアップデートを行いました。

目的は、abs() 関数を今のモダンアーキテクチャではどのように実装しているか調べたというものです。

対象関数

今回、簡単な abs の実装を対象にしたいと思います。
ご存知だと思いますが、cppreference.com を見てもらえれば以下のようになっています。

https://en.cppreference.com/w/cpp/numeric/math/abs

Computes the absolute value of the integer number num. The behavior is undefined if the result cannot be represented by the return type.

If std::abs is called with an unsigned integral argument that cannot be converted to int by integral promotion, the program is ill-formed. 

以下のように絶対値を返してくれます。

    std::cout << std::showpos << "abs(-123) = " << std::abs(-123) << '\n';
  • showpos は正の符号も表示します。

出力:

abs(-123) = +123

Hacker's Delight での定義例

Hacker's Delight では、マシンが絶対値の命令を持たない 場合は以下のようにビット操作が使われるように説明されています。

y \leftarrow x \gg^{\mathrm{s}} 31
(x \oplus y) - y

1つ目は y にxを32ビットずらしているので2の補数の最上位ビットを取れる。つまり

  • x≥0(正またはゼロ)の場合: y=0
  • x<0(負)の場合: y=−1(すべてのビットが 1)
    となる。

この y を利用して元の値との排他的論理和 (X-OR) を取れば絶対値が取れるというものです。

  • x≥0 (正)
    • y=0
    • (x⊕0)−0=x−0
    • x (abs(x))
  • x<0 (負)
    • y=−1
    • (x⊕(−1))−(−1)
    • (−x) (abs(x))

つまり2の補数の最上位ビットを使い絶対値を取るというものです。

if 文で実装した場合

if 文でも実装できます。

if (x < 0) {
  return -1 * x;
} else {
  return x;
}

マシン語で考えると、 cmp / jl / neg / mov / ret が最低限必要になり、5ステップ必要になります。
(上記 else のブロックになった場合)
しかし、先ほどのビット演算の場合は、 ASRS / EOR / ADDS の3ステップで必ず終わります。つまり if文より abs() 関数を使った方が 2 ステップの高速化を図ることができます。

実験サンプルコード

今回の検証では以下のサンプルコードを利用します。

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv[]) {
    int input_val = -222;
    if (argc > 1) {
        input_val = atoi(argv[1]);
    }

    int a = abs(input_val);

    printf("abs result: %d\n", a);
    return 0;
}

最後に printf で abs() 関数の実行結果 a を出力するようにしてください。コンパイラの最適化によっては計算結果が削除される場合があるので、最後までメモリ上に保存させるようにしましょう。

アーキテクチャごとの検証

今回、Apple M4 と x86_64、おまけとして WASM で検証をしてみます。

Apple M4

利用した mac mini(M4) だと ARM64 で clang を利用しました。

最適化ビルドとバイナリーコードの解析

$ clang -O3 abs.c -o a.out
$ objdump -D a.out

結果 (TEXTのみ):

0000000100000460 <_main>:
100000460: d10083ff    	sub	sp, sp, #0x20
100000464: a9017bfd    	stp	x29, x30, [sp, #0x10]
100000468: 910043fd    	add	x29, sp, #0x10
10000046c: 7100081f    	cmp	w0, #0x2
100000470: 5400008b    	b.lt	0x100000480 <_main+0x20>
100000474: f9400420    	ldr	x0, [x1, #0x8]
100000478: 9400000d    	bl	0x1000004ac <_printf+0x1000004ac>
10000047c: 14000002    	b	0x100000484 <_main+0x24>
100000480: 12801ba0    	mov	w0, #-0xde              ; =-222
100000484: 7100001f    	cmp	w0, #0x0
100000488: 5a805408    	cneg	w8, w0, mi
10000048c: f90003e8    	str	x8, [sp]
100000490: 90000000    	adrp	x0, 0x100000000 <_printf+0x100000000>
100000494: 91131000    	add	x0, x0, #0x4c4
100000498: 94000008    	bl	0x1000004b8 <_printf+0x1000004b8>
10000049c: 52800000    	mov	w0, #0x0                ; =0
1000004a0: a9417bfd    	ldp	x29, x30, [sp, #0x10]
1000004a4: 910083ff    	add	sp, sp, #0x20
1000004a8: d65f03c0    	ret

100000488 で cneg w8, w0, mi となっている箇所が abs 関数のようです。
ARM の仕様書を見ると CNEG は条件が一致した場合はレジスターの値を負数を登録し、条件に一致しない場合はレジスターの値をそのまま登録するとなっている。
https://developer.arm.com/documentation/ddi0602/2025-06/Base-Instructions/CNEG--Conditional-negate--an-alias-of-CSNEG-

つまり、w0 レジストリーの値が負数(mi: minus)の場合、負数を w8 に格納し、整数であれば w0 の値をそのまま w8 に格納することにより絶対値を取得している

Hacker's Delight では「マシンが絶対値の命令を持たない場合」とあったので、mac の M4 チップセットでは CNEG という命令があるのでそれを利用したということになります。

調べてはいませんが、ARM のほとんどの CPU にはこの命令があるのではないでしょうか。

x86(Intel)

利用した Dell XPS(Intel) だと x86_64 で clang を利用しました。

最適化ビルドとバイナリーコードの解析

$ clang -O3 abs.c -o a.out
$ objdump -D a.out

結果 (TEXTのみ):

0000000000001150 <main>:
    1150:	50                   	push   %rax
    1151:	b8 22 ff ff ff       	mov    $0xffffff22,%eax
    1156:	83 ff 02             	cmp    $0x2,%edi
    1159:	7c 10                	jl     116b <main+0x1b>
    115b:	48 8b 7e 08          	mov    0x8(%rsi),%rdi
    115f:	31 f6                	xor    %esi,%esi
    1161:	ba 0a 00 00 00       	mov    $0xa,%edx
    1166:	e8 d5 fe ff ff       	call   1040 <strtol@plt>
    116b:	89 c6                	mov    %eax,%esi
    116d:	f7 de                	neg    %esi
    116f:	0f 48 f0             	cmovs  %eax,%esi
    1172:	48 8d 3d 8b 0e 00 00 	lea    0xe8b(%rip),%rdi        # 2004 <_IO_stdin_used+0x4>
    1179:	31 c0                	xor    %eax,%eax
    117b:	e8 b0 fe ff ff       	call   1030 <printf@plt>
    1180:	31 c0                	xor    %eax,%eax
    1182:	59                   	pop    %rcx
    1183:	c3                   	ret

116f で cmovs %eax,%esi となっている箇所が abs 関数のようです。
AMD の仕様書を見ると cmovs は条件が一致した場合はレジスターの値を負数を登録するとなっている。
https://developer.arm.com/documentation/ddi0602/2025-06/Base-Instructions/CNEG--Conditional-negate--an-alias-of-CSNEG-

つまり、 mov %eax,%esi で入力値を %eax に格納し、入力値の負数を %esi に入れておく。
cmovs%esi が負数だった場合はその負数を %eaxに入れる。
これにより絶対値が %eax に格納されるようになっている。

では Delight の実装はどこで使われてる?

主流のCPUでは絶対数を扱うための命令セットがあるので、Hacker's Delight で紹介されているビット操作を行うものは利用されていなかった。

ではビット操作で絶対値を使うケースはどこにあるのか?

WASM でビルドする

CPU 依存をさせないため、先ほどと同じサンプルコードを WASM でビルドしてみます。

em++ abs.c
wasm-objdump -d a.out.wasm

バイナリ:

 0002ad: 20 02                      | local.get 2    #入力値 x を取得
 0002af: 28 02 10                   | i32.load 2 16  #定数 31 (32ビット整数の最上位ビットのインデックス)
 0002b2: 21 03                      | local.set 3
 0002b4: 20 03                      | local.get 3
 0002b6: 41 1f                      | i32.const 31
 0002b8: 75                         | i32.shr_s      # 算術右シフト
 0002b9: 21 04                      | local.set 4    # マスク値を local 4 に格納
 0002bb: 20 02                      | local.get 2
 0002bd: 20 03                      | local.get 3    # 入力値 x を再度取得
 0002bf: 20 04                      | local.get 4    # マスク値 s を取得
 0002c1: 73                         | i32.xor        # x XOR s を計算
 0002c2: 20 04                      | local.get 4    # マスク値 s を再度取得
 0002c4: 6b                         | i32.sub        # (x XOR s) - s を計算

抜粋ですが、このコードを見ると入力値に対してシフト演算子を行い、Hacker's Delight の絶対値と同じ計算をしていることがわかりました。

まとめ

Hacker's Delight に記載のある絶対値計算は今でも利用されているみたいです。
ただ、ARM64 / x86_64 の例を見ての通り、専用の符号判定命令セットが用意されていてビルド時にはそれが利用されるようです。

Discussion