目的
小数はx86のCPU上でどのように処理されるのかを確認します。
[追記]ARMでも確かめました。
ARM 32bit/64bit における C 言語の浮動小数点処理とアセンブリ命令の違い
C言語及びアセンブリへ変換
gcc -O0 -S -masm=intel test.c -o test.s
#include <stdio.h>
int main() {
float f1 = 1.5f, f2 = 2.25f;
double d1 = 3.125, d2 = 4.875;
float fsum = f1 + f2;
double dsum = d1 + d2;
return 0;
}
アセンブリ言語
.file "test.c"
.intel_syntax noprefix
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
endbr64
push rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
mov rbp, rsp
.cfi_def_cfa_register 6
movss xmm0, DWORD PTR .LC0[rip]
movss DWORD PTR -36[rbp], xmm0
movss xmm0, DWORD PTR .LC1[rip]
movss DWORD PTR -32[rbp], xmm0
movsd xmm0, QWORD PTR .LC2[rip]
movsd QWORD PTR -24[rbp], xmm0
movsd xmm0, QWORD PTR .LC3[rip]
movsd QWORD PTR -16[rbp], xmm0
movss xmm0, DWORD PTR -36[rbp]
addss xmm0, DWORD PTR -32[rbp]
movss DWORD PTR -28[rbp], xmm0
movsd xmm0, QWORD PTR -24[rbp]
addsd xmm0, QWORD PTR -16[rbp]
movsd QWORD PTR -8[rbp], xmm0
mov eax, 0
pop rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.section .rodata
.align 4
.LC0:
.long 1069547520
.align 4
.LC1:
.long 1074790400
.align 8
.LC2:
.long 0
.long 1074331648
.align 8
.LC3:
.long 0
.long 1075019776
.ident "GCC: (Ubuntu 11.4.0-1ubuntu1~22.04.2) 11.4.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5
0:
.string "GNU"
1:
.align 8
.long 0xc0000002
.long 3f - 2f
2:
.long 0x3
3:
.align 8
4:
浮動小数点形式
10進数の小数を2進数で表す仕組みの説明は、難しいので参考にした記事のURLを貼ります。
浮動小数点数とは?32ビットと64ビットのしくみを理解しよう
1.5 → 1069547520
.LC0:
.long 1069547520 ; 1.5f
.align 4
10進数1069547520
→ 16進数0x3FC00000
→ 2進数0 01111111 10000000000000000000000
0.5×2 = 1.0
よって小数部 = .1
1.1₂ = 1.1 × 2⁰と表せる
符号 0 正の数
指数部 01111111 127(2⁰の指数0
に127を足した値)
仮数部 10000000000000000000000 (1.1₂の小数部分)
2.25 → 1074790400
.LC1:
.long 1074790400 ; 2.25f
.align 8
10進数1074790400
→ 16進数0x40100000
→ 2進数0 10000000 00100000000000000000000
2.25₁₀ = 2 + 0.25
0.25 × 2 = 0.5 → 整数部分 0
0.5 × 2 = 1.0 → 整数部分 1
小数部は0.25₁₀ = 0.01₂
随って2.25₁₀ = 10.01₂
正規化すると
10.01₂ = 1.001₂ × 2¹
符号 0 正の数
指数部 10000000 128(2¹の指数1に127を足した値)
仮数部 00100000000000000000000(正規化後1.001₂の小数部分)
3.125 → 1074331648
.LC2:
.long 0
.long 1074331648 ; 3.125
.align 8
10進数 1074331648 → 16進数 0x40440000 → 2進数 0 10000000 10010000000000000000000
3.125₁₀ = 3 + 0.125
0.125 × 2 = 0.25 → 整数部分 0
0.25 × 2 = 0.5 → 整数部分 0
0.5 × 2 = 1.0 → 整数部分 1
小数部は0.125₁₀ = 0.001₂
随って3.125₁₀ = 11.001₂
正規化すると11.001₂ = 1.1001₂ × 2¹
符号 0 正の数
指数部 10000000 128(2¹の指数1に127を足した値)
仮数部 10010000000000000000000(正規化後1.1001₂の小数部分)
4.875 → 1075019776
.LC3:
.long 0
.long 1075019776 ; 4.875
10進数 1075019776 → 16進数 0x409E0000 → 2進数 0 10000001 00111000000000000000000
4.875₁₀ = 4 + 0.875
.875 × 2 = 1.75 → 整数部分 1
0.75 × 2 = 1.5 → 整数部分 1
0.5 × 2 = 1.0 → 整数部分 1
小数部 0.875₁₀ = 0.111₂
随って4.875₁₀ = 100.111₂
正規化すると100.111₂ = 1.00111₂ × 2²
符号 0 正の数
指数部 10000001 129(2²の指数2に127を足した値)
仮数部 00111000000000000000000(正規化後1.00111₂の小数部分)
浮動小数を扱うCPU命令
浮動小数の数値をxmm0
レジスタ経由でメモリへ格納しています。
movss
対象:単精度浮動小数点 (float, 32bit)
レジスタ:XMM レジスタの 下位 32bit
movss xmm0, DWORD PTR .LC0[rip]
movss DWORD PTR -36[rbp], xmm0 ; float f1 = 1.5f
movss xmm0, DWORD PTR .LC1[rip]
movss DWORD PTR -32[rbp], xmm0 ; float f2 = 2.25f
movsd
対象:倍精度浮動小数点 (double, 64bit)
レジスタ:XMM レジスタの 下位 64bit
movsd xmm0, QWORD PTR .LC2[rip]
movsd QWORD PTR -24[rbp], xmm0 ; double d1 = 3.125
movsd xmm0, QWORD PTR .LC3[rip]
movsd QWORD PTR -16[rbp], xmm0 ; double d2 = 4.875
addss
対象:float 型 (32bit)
操作:下位 32bit 同士の加算
movss xmm0, DWORD PTR -36[rbp]
addss xmm0, DWORD PTR -32[rbp] ; float fsum = f1 + f2;
movss DWORD PTR -28[rbp], xmm0
addsd
対象:double 型 (64bit)
操作:下位 64bit 同士の加算
movsd xmm0, QWORD PTR -24[rbp]
addsd xmm0, QWORD PTR -16[rbp] ; double dsum = d1 + d2;
movsd QWORD PTR -8[rbp], xmm0
最後に
浮動小数の計算をする場合はソフト側で泥臭い計算をしているものとばかり思っており、浮動小数専用のレジスタや命令があることは今まで全く知りませんでした。
動作環境(Linux / x86_64 版)
OS: Ubuntu 22.04.5 LTS (Jammy Jellyfish)
Kernel: Linux 6.8.0-84-generic
CPU: Intel Core i5-8250U, 4 cores, 8 threads, Little Endian
GCC: 12.3.0
Architecture: x86_64