C言語で乗算や除算を使用した際とシフト演算する際のアセンブリ言語の動き

に公開

サマリ

乗算や除算、すなわち掛け算や割り算を「*」や「/」で行う際に、アセンブリ言語レベルでは命令の実行回数が多くなることを可視化しました。
比較対象として、上記の演算子を使用せずにシフト演算を使用します。
通常は乗算・除算でもコンパイラが最適化をしてくれますが、今回は明示的に最適化を行わずに比較をしてみました。

マシンスペック

MacBook Air M2 arm64

乗算

最適化動作を制御してコンパイル

本事象を確認するために、明示的にコンパイラの最適化を抑止して、値を単純に2倍するプログラムを作成します。

#include<stdio.h>

int main(void){
    int a = 5;
    volatile int two = 2;
    int ret = a * two;
    printf("ret is %d", ret);
    return 0;
}

出力されたアセンブリコードです。

cat times_norm.s 
	.section	__TEXT,__text,regular,pure_instructions
	.build_version macos, 14, 0	sdk_version 14, 4
	.globl	_main                           ; -- Begin function main
	.p2align	2
_main:                                  ; @main
	.cfi_startproc
; %bb.0:
	sub	sp, sp, #48
	.cfi_def_cfa_offset 48
	stp	x29, x30, [sp, #32]             ; 16-byte Folded Spill
	add	x29, sp, #32
	.cfi_def_cfa w29, 16
	.cfi_offset w30, -8
	.cfi_offset w29, -16
	mov	w8, #0
	str	w8, [sp, #12]                   ; 4-byte Folded Spill
	stur	wzr, [x29, #-4]
	mov	w8, #5
	stur	w8, [x29, #-8]
	mov	w8, #2
	stur	w8, [x29, #-12]
	ldur	w8, [x29, #-8]
	ldur	w9, [x29, #-12]
	mul	w8, w8, w9
	str	w8, [sp, #16]
	ldr	w9, [sp, #16]
                                        ; implicit-def: $x8
	mov	x8, x9
	mov	x9, sp
	str	x8, [x9]
	adrp	x0, l_.str@PAGE
	add	x0, x0, l_.str@PAGEOFF
	bl	_printf
	ldr	w0, [sp, #12]                   ; 4-byte Folded Reload
	ldp	x29, x30, [sp, #32]             ; 16-byte Folded Reload
	add	sp, sp, #48
	ret
	.cfi_endproc
                                        ; -- End function
	.section	__TEXT,__cstring,cstring_literals
l_.str:                                 ; @.str
	.asciz	"ret is %d"

.subsections_via_symbols

乗算を「*」記号を使わずにシフト演算にしてコンパイル

2倍は対象の値を1ビット左にシフトすれば実現できますので、実装します。
5は2進数で0x0101であり、これを1ビット左にシフトすると0x1010となり10進数の10になります。

#include<stdio.h>

int main(void){
    int a = 5;
    int ret = a << 2;
    printf("ret is %d", ret);
    return 0;
}

出力されたアセンブリコードです。

cat times_norm2.s
	.section	__TEXT,__text,regular,pure_instructions
	.build_version macos, 14, 0	sdk_version 14, 4
	.globl	_main                           ; -- Begin function main
	.p2align	2
_main:                                  ; @main
	.cfi_startproc
; %bb.0:
	sub	sp, sp, #48
	.cfi_def_cfa_offset 48
	stp	x29, x30, [sp, #32]             ; 16-byte Folded Spill
	add	x29, sp, #32
	.cfi_def_cfa w29, 16
	.cfi_offset w30, -8
	.cfi_offset w29, -16
	mov	w8, #0
	str	w8, [sp, #16]                   ; 4-byte Folded Spill
	stur	wzr, [x29, #-4]
	mov	w8, #5
	stur	w8, [x29, #-8]
	ldur	w8, [x29, #-8]
	lsl	w8, w8, #2
	stur	w8, [x29, #-12]
	ldur	w9, [x29, #-12]
                                        ; implicit-def: $x8
	mov	x8, x9
	mov	x9, sp
	str	x8, [x9]
	adrp	x0, l_.str@PAGE
	add	x0, x0, l_.str@PAGEOFF
	bl	_printf
	ldr	w0, [sp, #16]                   ; 4-byte Folded Reload
	ldp	x29, x30, [sp, #32]             ; 16-byte Folded Reload
	add	sp, sp, #48
	ret
	.cfi_endproc
                                        ; -- End function
	.section	__TEXT,__cstring,cstring_literals
l_.str:                                 ; @.str
	.asciz	"ret is %d"

.subsections_via_symbols

差分を確認

diff times_norm.s times_norm2.s        
16c16
< 	str	w8, [sp, #12]                   ; 4-byte Folded Spill
---
> 	str	w8, [sp, #16]                   ; 4-byte Folded Spill
20,21d19
< 	mov	w8, #2
< 	stur	w8, [x29, #-12]
22a21,22
> 	lsl	w8, w8, #2
> 	stur	w8, [x29, #-12]
24,26d23
< 	mul	w8, w8, w9
< 	str	w8, [sp, #16]
< 	ldr	w9, [sp, #16]
34c31
< 	ldr	w0, [sp, #12]                   ; 4-byte Folded Reload
---
> 	ldr	w0, [sp, #16]                   ; 4-byte Folded Reload
; 乗算版だけに現れる
+   ldr   w9, [x29, #-12]   ; two をロード
+   mul   w8, w8, w9        ; ★高レイテンシ
+   str   w8, [sp, #16]

除算

最適化動作を制御してコンパイル

10を2で割るプログラムです。

#include<stdio.h>

int main(void){
    int a = 10;
    volatile int two = 2;
    int ret = a / two;
    printf("ret is %d", ret);
    return 0;
}
	.section	__TEXT,__text,regular,pure_instructions
	.build_version macos, 14, 0	sdk_version 14, 4
	.globl	_main                           ; -- Begin function main
	.p2align	2
_main:                                  ; @main
	.cfi_startproc
; %bb.0:
	sub	sp, sp, #48
	.cfi_def_cfa_offset 48
	stp	x29, x30, [sp, #32]             ; 16-byte Folded Spill
	add	x29, sp, #32
	.cfi_def_cfa w29, 16
	.cfi_offset w30, -8
	.cfi_offset w29, -16
	mov	w8, #0
	str	w8, [sp, #12]                   ; 4-byte Folded Spill
	stur	wzr, [x29, #-4]
	mov	w8, #10
	stur	w8, [x29, #-8]
	mov	w8, #2
	stur	w8, [x29, #-12]
	ldur	w8, [x29, #-8]
	ldur	w9, [x29, #-12]
	sdiv	w8, w8, w9
	str	w8, [sp, #16]
	ldr	w9, [sp, #16]
                                        ; implicit-def: $x8
	mov	x8, x9
	mov	x9, sp
	str	x8, [x9]
	adrp	x0, l_.str@PAGE
	add	x0, x0, l_.str@PAGEOFF
	bl	_printf
	ldr	w0, [sp, #12]                   ; 4-byte Folded Reload
	ldp	x29, x30, [sp, #32]             ; 16-byte Folded Reload
	add	sp, sp, #48
	ret
	.cfi_endproc
                                        ; -- End function
	.section	__TEXT,__cstring,cstring_literals
l_.str:                                 ; @.str
	.asciz	"ret is %d"

.subsections_via_symbols

除算を「/」記号を使わずにシフト演算にしてコンパイル

2で割ることは対象の値を1ビット右にシフトすれば実現できますので、実装します。
10は2進数で0x1010であり、これを1ビット右にシフトすると0x0101となり10進数の5になります。

#include<stdio.h>

int main(void){
    int a = 10;
    int ret = a >> 2;
    printf("ret is %d", ret);
    return 0;
}
	.section	__TEXT,__text,regular,pure_instructions
	.build_version macos, 14, 0	sdk_version 14, 4
	.globl	_main                           ; -- Begin function main
	.p2align	2
_main:                                  ; @main
	.cfi_startproc
; %bb.0:
	sub	sp, sp, #48
	.cfi_def_cfa_offset 48
	stp	x29, x30, [sp, #32]             ; 16-byte Folded Spill
	add	x29, sp, #32
	.cfi_def_cfa w29, 16
	.cfi_offset w30, -8
	.cfi_offset w29, -16
	mov	w8, #0
	str	w8, [sp, #16]                   ; 4-byte Folded Spill
	stur	wzr, [x29, #-4]
	mov	w8, #10
	stur	w8, [x29, #-8]
	ldur	w8, [x29, #-8]
	asr	w8, w8, #2
	stur	w8, [x29, #-12]
	ldur	w9, [x29, #-12]
                                        ; implicit-def: $x8
	mov	x8, x9
	mov	x9, sp
	str	x8, [x9]
	adrp	x0, l_.str@PAGE
	add	x0, x0, l_.str@PAGEOFF
	bl	_printf
	ldr	w0, [sp, #16]                   ; 4-byte Folded Reload
	ldp	x29, x30, [sp, #32]             ; 16-byte Folded Reload
	add	sp, sp, #48
	ret
	.cfi_endproc
                                        ; -- End function
	.section	__TEXT,__cstring,cstring_literals
l_.str:                                 ; @.str
	.asciz	"ret is %d"

.subsections_via_symbols

差分を確認

16c16
< 	str	w8, [sp, #12]                   ; 4-byte Folded Spill
---
> 	str	w8, [sp, #16]                   ; 4-byte Folded Spill
20,21d19
< 	mov	w8, #2
< 	stur	w8, [x29, #-12]
22a21,22
> 	asr	w8, w8, #2
> 	stur	w8, [x29, #-12]
24,26d23
< 	sdiv	w8, w8, w9
< 	str	w8, [sp, #16]
< 	ldr	w9, [sp, #16]
34c31
< 	ldr	w0, [sp, #12]                   ; 4-byte Folded Reload
---
> 	ldr	w0, [sp, #16]                   ; 4-byte Folded Reload
; 除算版だけに現れる
+    mov   w8, #2            ; two = 2 をレジスタに
+    stur  w8, [x29, #-12]   ; two をスタックへ保存
+    ldr   w9, [x29, #-12]   ; two をロード
+    sdiv  w8, w8, w9        ; ★高レイテンシ整数除算
+    str   w8, [sp, #16]     ; 結果をいったんメモリへ退避
+    ldr   w9, [sp, #16]     ; printf 用に再ロード

まとめ

今回はC言語で乗算・除算を使用した際、それらをシフト演算に変えた際のアセンブリ言語の内容の差分を確認しました。
皆様の学習の少しでも役に立てると幸いです。

Discussion