⛳
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