第11章 メタプログラミング
11.1 この章で学ぶこと
この章で学ぶ内容は以下のとおりです。
学ぶこと |
ポイント |
|---|---|
コンパイル時評価の 3 つの形 |
|
アセンブリで見る「消えた枝」 |
|
|
コンパイル時 parameter vs 実行時 argument |
specialization の証拠 |
シンボル名に |
trait / generics / constraints |
型の約束・型の抽象化・誤用の早期防止 |
Mojo のメタプログラミングは「コンパイル時に決められることを先に決め、実行時コストを消す」ことに尽きます。 3 つの問いで整理できます。
どの枝が実行されるか →
comptime ifで決めるループを展開するか →
comptime forで決める型・幅・長さは何か →
[]parameter で決める
11.2 comptime if:選ばれた枝だけが残る
通常の if 文はコンパイル後も「どちらの枝が実行されるか」を実行時に判定します。
そのため、true 側と false 側の両方がバイナリに残ります。
comptime if は条件をコンパイル時に確定させ、選ばれなかった枝をバイナリから消去します。
結果として実行時には分岐命令すら存在しない、最小限のコードが残ります。
典型的な用途は次のとおりです。
デバッグビルドとリリースビルドの切り替え(
comptime if debug_mode: ...)プラットフォーム固有のコードの選択(
comptime if target.is_x86: ...)SIMD 幅など、コンパイル時に決まる定数による処理の分岐
def main():
comptime branch = 1
comptime if branch == 1:
print(10)
else:
print(0)
リスト11-1: meta_comptime_if.mojo
comptime branch = 1 は実行時変数ではありません。
comptime キーワードが付くため、コンパイル時に値が確定する定数です。
その結果、comptime if branch == 1: の条件もコンパイル時に評価され、else 側のコードはコンパイラがそもそも生成しません。
アセンブリで実際に何が起きているか確認します。
meta_comptime_if.o:
(__TEXT,__text) section
_meta_comptime_if::main():
; def main():
0: 48 83 ec 78 subq $0x78, %rsp
; print(10)
4: 48 c7 44 24 10 0a 00 00 00 movq $0xa, 0x10(%rsp)
d: 90 nop
e: 90 nop
f: 48 8d 05 00 00 00 00 leaq _static_string_a8d4ace0dc8d360e(%rip), %rax
16: 48 89 44 24 38 movq %rax, 0x38(%rsp)
1b: 48 c7 44 24 40 01 00 00 00 movq $0x1, 0x40(%rsp)
24: 48 c7 44 24 48 01 00 00 00 movq $0x1, 0x48(%rsp)
2d: 48 89 44 24 18 movq %rax, 0x18(%rsp)
32: 48 89 44 24 20 movq %rax, 0x20(%rsp)
37: 48 89 44 24 50 movq %rax, 0x50(%rsp)
3c: 48 89 44 24 58 movq %rax, 0x58(%rsp)
41: 48 c7 44 24 60 01 00 00 00 movq $0x1, 0x60(%rsp)
; print(10)
4a: 90 nop
4b: 90 nop
4c: 48 8d 05 00 00 00 00 leaq _static_string_bbe01a6a523daf15(%rip), %rax
53: 48 89 44 24 38 movq %rax, 0x38(%rsp)
58: 48 c7 44 24 40 01 00 00 00 movq $0x1, 0x40(%rsp)
61: 48 c7 44 24 48 01 00 00 00 movq $0x1, 0x48(%rsp)
6a: 48 8d 05 00 00 00 00 leaq _static_string_bbe01a6a523daf15(%rip), %rax
71: 48 89 44 24 28 movq %rax, 0x28(%rsp)
76: 48 8d 05 00 00 00 00 leaq _static_string_bbe01a6a523daf15(%rip), %rax
7d: 48 89 44 24 30 movq %rax, 0x30(%rsp)
82: 48 8d 05 00 00 00 00 leaq _static_string_bbe01a6a523daf15(%rip), %rax
89: 48 89 44 24 50 movq %rax, 0x50(%rsp)
8e: 48 8d 05 00 00 00 00 leaq _static_string_bbe01a6a523daf15(%rip), %rax
95: 48 89 44 24 68 movq %rax, 0x68(%rsp)
9a: 48 c7 44 24 70 01 00 00 00 movq $0x1, 0x70(%rsp)
; print(10)
a3: 48 89 e0 movq %rsp, %rax
a6: 48 c7 00 01 00 00 00 movq $0x1, "_meta_comptime_if::main()"(%rax)
ad: 48 8d 35 00 00 00 00 leaq _static_string_a8d4ace0dc8d360e(%rip), %rsi
b4: 48 8d 0d 00 00 00 00 leaq _static_string_bbe01a6a523daf15(%rip), %rcx
bb: bf 0a 00 00 00 movl $0xa, %edi
c0: 41 b8 01 00 00 00 movl $0x1, %r8d
c6: 45 31 c9 xorl %r9d, %r9d
c9: 4c 89 c2 movq %r8, %rdx
cc: e8 00 00 00 00 callq "_std::io::io::print[KGENParamList[::Writable & ::AnyType],*::Writable & ::AnyType,LITImmOrigin,::Origin[::Bool(False), $2],::Bool,LITOrigin[$4._mlir_value],::Origin[$4, $5],::Bool,LITOrigin[$7._mlir_value],::Origin[$7, $8]](*$0,sep:::StringSpan[$4, $5, $6],end:::StringSpan[$7, $8, $9],flush:::Bool,file:::FileDescriptor$),Ts.values`=[[typevalue<#kgen.instref<\"std::builtin::simd::SIMD,dtype=index,length=1\">>, scalar<index>]],sep.mut`4=false,end.mut`7=false"
; def main():
d1: 48 83 c4 78 addq $0x78, %rsp
d5: c3 retq
d6: 66 2e 0f 1f 84 00 00 00 00 00 nopw %cs:"_meta_comptime_if::main()"(%rax,%rax)
リスト11-2: meta_comptime_if.asm
注目ポイント |
内容 |
|---|---|
|
|
|
同じく |
|
まったく存在しない。 |
分岐命令( |
存在しない。コンパイル時に選択が終わっているため不要 |
通常の if では「実行時に branch == 1 かどうか比較し、どちらかにジャンプする」コードが生成されます。
comptime if では比較もジャンプも不要で、print(10) の呼び出し一本だけが残ります。
これが「コンパイル時に選ばれた枝だけが残る」の意味です。
図11-1: comptime if:通常の if との比較とアセンブリ出力
11.3 comptime for:ループの展開
通常の for ループはバイナリにそのまま残ります。
ループカウンタのインクリメント、境界値との比較、分岐命令が必要で、繰り返し回数が多ければその分 CPU が判定を繰り返します。
comptime for はコンパイル時に繰り返し回数が確定しているため、コンパイラがループを完全に展開し、個々の命令を直列に並べます。
実行時にはループの痕跡が一切残りません。
これが有効な典型的な場面は次のとおりです。
固定サイズのベクトル演算(
comptime for i in range(8): ...)struct のフィールドを一括で初期化・検査する汎用処理
comptime for T in (Int, Float64, String): ...のように型を列挙してコードを生成する場合
def main():
comptime for i in range(3):
print(i)
リスト11-3: comptime_for_unroll.mojo
comptime for i in range(3): の i は実行時の変数ではありません。
コンパイラが i = 0、i = 1、i = 2 のそれぞれについてコードを 3 回生成し、ループ本体をそのまま直列に並べます。
生成されるバイナリは print(0), print(1), print(2) を順番に呼ぶコードと等価です。
アセンブリで実際に何が起きているか確認します。
comptime_for_unroll.o:
(__TEXT,__text) section
_comptime_for_unroll::main():
; def main():
0: 48 81 ec e8 00 00 00 subq $0xe8, %rsp
; print(i)
7: 48 c7 44 24 30 00 00 00 00 movq $"_comptime_for_unroll::main()", 0x30(%rsp)
10: 90 nop
11: 90 nop
12: 48 8d 35 00 00 00 00 leaq _static_string_a8d4ace0dc8d360e(%rip), %rsi
19: 48 89 74 24 20 movq %rsi, 0x20(%rsp)
1e: 48 89 b4 24 a8 00 00 00 movq %rsi, 0xa8(%rsp)
26: 48 c7 84 24 b0 00 00 00 01 00 00 00 movq $0x1, 0xb0(%rsp)
32: 48 c7 84 24 b8 00 00 00 01 00 00 00 movq $0x1, 0xb8(%rsp)
3e: 48 89 74 24 38 movq %rsi, 0x38(%rsp)
43: 48 89 74 24 40 movq %rsi, 0x40(%rsp)
48: 48 89 b4 24 c0 00 00 00 movq %rsi, 0xc0(%rsp)
50: 48 89 b4 24 c8 00 00 00 movq %rsi, 0xc8(%rsp)
58: 48 c7 84 24 d0 00 00 00 01 00 00 00 movq $0x1, 0xd0(%rsp)
; print(i)
64: 90 nop
65: 90 nop
66: 48 8d 0d 00 00 00 00 leaq _static_string_bbe01a6a523daf15(%rip), %rcx
6d: 48 89 4c 24 28 movq %rcx, 0x28(%rsp)
72: 48 89 8c 24 a8 00 00 00 movq %rcx, 0xa8(%rsp)
7a: 48 c7 84 24 b0 00 00 00 01 00 00 00 movq $0x1, 0xb0(%rsp)
86: 48 c7 84 24 b8 00 00 00 01 00 00 00 movq $0x1, 0xb8(%rsp)
92: 48 89 4c 24 48 movq %rcx, 0x48(%rsp)
97: 48 89 4c 24 50 movq %rcx, 0x50(%rsp)
9c: 48 89 8c 24 c0 00 00 00 movq %rcx, 0xc0(%rsp)
a4: 48 89 8c 24 d8 00 00 00 movq %rcx, 0xd8(%rsp)
ac: 48 c7 84 24 e0 00 00 00 01 00 00 00 movq $0x1, 0xe0(%rsp)
; print(i)
b8: 48 89 e0 movq %rsp, %rax
bb: 48 c7 00 01 00 00 00 movq $0x1, "_comptime_for_unroll::main()"(%rax)
c2: 45 31 c9 xorl %r9d, %r9d
c5: 44 89 4c 24 1c movl %r9d, 0x1c(%rsp)
ca: 44 89 cf movl %r9d, %edi
cd: 41 b8 01 00 00 00 movl $0x1, %r8d
d3: 4c 89 44 24 10 movq %r8, 0x10(%rsp)
; print(i)
d8: 4c 89 c2 movq %r8, %rdx
db: e8 00 00 00 00 callq "_std::io::io::print[KGENParamList[::Writable & ::AnyType],*::Writable & ::AnyType,LITImmOrigin,::Origin[::Bool(False), $2],::Bool,LITOrigin[$4._mlir_value],::Origin[$4, $5],::Bool,LITOrigin[$7._mlir_value],::Origin[$7, $8]](*$0,sep:::StringSpan[$4, $5, $6],end:::StringSpan[$7, $8, $9],flush:::Bool,file:::FileDescriptor$),Ts.values`=[[typevalue<#kgen.instref<\"std::builtin::simd::SIMD,dtype=index,length=1\">>, scalar<index>]],sep.mut`4=false,end.mut`7=false"
e0: 4c 8b 44 24 10 movq 0x10(%rsp), %r8
e5: 44 8b 4c 24 1c movl 0x1c(%rsp), %r9d
ea: 48 8b 74 24 20 movq 0x20(%rsp), %rsi
ef: 48 8b 4c 24 28 movq 0x28(%rsp), %rcx
f4: 48 c7 44 24 58 01 00 00 00 movq $0x1, 0x58(%rsp)
fd: 90 nop
fe: 90 nop
ff: 48 89 b4 24 a8 00 00 00 movq %rsi, 0xa8(%rsp)
107: 48 c7 84 24 b0 00 00 00 01 00 00 00 movq $0x1, 0xb0(%rsp)
113: 48 c7 84 24 b8 00 00 00 01 00 00 00 movq $0x1, 0xb8(%rsp)
11f: 48 89 74 24 60 movq %rsi, 0x60(%rsp)
124: 48 89 74 24 68 movq %rsi, 0x68(%rsp)
129: 48 89 b4 24 c0 00 00 00 movq %rsi, 0xc0(%rsp)
131: 48 89 b4 24 c8 00 00 00 movq %rsi, 0xc8(%rsp)
139: 48 c7 84 24 d0 00 00 00 01 00 00 00 movq $0x1, 0xd0(%rsp)
; print(i)
145: 90 nop
146: 90 nop
147: 48 89 8c 24 a8 00 00 00 movq %rcx, 0xa8(%rsp)
14f: 48 c7 84 24 b0 00 00 00 01 00 00 00 movq $0x1, 0xb0(%rsp)
15b: 48 c7 84 24 b8 00 00 00 01 00 00 00 movq $0x1, 0xb8(%rsp)
167: 48 89 4c 24 70 movq %rcx, 0x70(%rsp)
16c: 48 89 4c 24 78 movq %rcx, 0x78(%rsp)
171: 48 89 8c 24 c0 00 00 00 movq %rcx, 0xc0(%rsp)
179: 48 89 8c 24 d8 00 00 00 movq %rcx, 0xd8(%rsp)
181: 48 c7 84 24 e0 00 00 00 01 00 00 00 movq $0x1, 0xe0(%rsp)
; print(i)
18d: 48 89 e0 movq %rsp, %rax
190: 48 c7 00 01 00 00 00 movq $0x1, "_comptime_for_unroll::main()"(%rax)
197: 4c 89 c7 movq %r8, %rdi
19a: 4c 89 c2 movq %r8, %rdx
19d: e8 00 00 00 00 callq "_std::io::io::print[KGENParamList[::Writable & ::AnyType],*::Writable & ::AnyType,LITImmOrigin,::Origin[::Bool(False), $2],::Bool,LITOrigin[$4._mlir_value],::Origin[$4, $5],::Bool,LITOrigin[$7._mlir_value],::Origin[$7, $8]](*$0,sep:::StringSpan[$4, $5, $6],end:::StringSpan[$7, $8, $9],flush:::Bool,file:::FileDescriptor$),Ts.values`=[[typevalue<#kgen.instref<\"std::builtin::simd::SIMD,dtype=index,length=1\">>, scalar<index>]],sep.mut`4=false,end.mut`7=false"
1a2: 48 8b 4c 24 20 movq 0x20(%rsp), %rcx
1a7: 48 8b 44 24 28 movq 0x28(%rsp), %rax
1ac: 48 c7 84 24 80 00 00 00 02 00 00 00 movq $0x2, 0x80(%rsp)
1b8: 90 nop
1b9: 90 nop
1ba: 48 89 8c 24 a8 00 00 00 movq %rcx, 0xa8(%rsp)
1c2: 48 c7 84 24 b0 00 00 00 01 00 00 00 movq $0x1, 0xb0(%rsp)
1ce: 48 c7 84 24 b8 00 00 00 01 00 00 00 movq $0x1, 0xb8(%rsp)
1da: 48 89 8c 24 88 00 00 00 movq %rcx, 0x88(%rsp)
1e2: 48 89 8c 24 90 00 00 00 movq %rcx, 0x90(%rsp)
1ea: 48 89 8c 24 c0 00 00 00 movq %rcx, 0xc0(%rsp)
1f2: 48 89 8c 24 c8 00 00 00 movq %rcx, 0xc8(%rsp)
1fa: 48 c7 84 24 d0 00 00 00 01 00 00 00 movq $0x1, 0xd0(%rsp)
; print(i)
206: 90 nop
207: 90 nop
208: 48 89 84 24 a8 00 00 00 movq %rax, 0xa8(%rsp)
210: 48 c7 84 24 b0 00 00 00 01 00 00 00 movq $0x1, 0xb0(%rsp)
21c: 48 c7 84 24 b8 00 00 00 01 00 00 00 movq $0x1, 0xb8(%rsp)
228: 48 8d 05 00 00 00 00 leaq _static_string_bbe01a6a523daf15(%rip), %rax
22f: 48 89 84 24 98 00 00 00 movq %rax, 0x98(%rsp)
237: 48 8d 05 00 00 00 00 leaq _static_string_bbe01a6a523daf15(%rip), %rax
23e: 48 89 84 24 a0 00 00 00 movq %rax, 0xa0(%rsp)
246: 48 8d 05 00 00 00 00 leaq _static_string_bbe01a6a523daf15(%rip), %rax
24d: 48 89 84 24 c0 00 00 00 movq %rax, 0xc0(%rsp)
255: 48 8d 05 00 00 00 00 leaq _static_string_bbe01a6a523daf15(%rip), %rax
25c: 48 89 84 24 d8 00 00 00 movq %rax, 0xd8(%rsp)
264: 48 c7 84 24 e0 00 00 00 01 00 00 00 movq $0x1, 0xe0(%rsp)
; print(i)
270: 48 89 e0 movq %rsp, %rax
273: 48 c7 00 01 00 00 00 movq $0x1, "_comptime_for_unroll::main()"(%rax)
27a: 48 8d 35 00 00 00 00 leaq _static_string_a8d4ace0dc8d360e(%rip), %rsi
281: 48 8d 0d 00 00 00 00 leaq _static_string_bbe01a6a523daf15(%rip), %rcx
288: bf 02 00 00 00 movl $0x2, %edi
28d: 41 b8 01 00 00 00 movl $0x1, %r8d
293: 45 31 c9 xorl %r9d, %r9d
296: 4c 89 c2 movq %r8, %rdx
299: e8 00 00 00 00 callq "_std::io::io::print[KGENParamList[::Writable & ::AnyType],*::Writable & ::AnyType,LITImmOrigin,::Origin[::Bool(False), $2],::Bool,LITOrigin[$4._mlir_value],::Origin[$4, $5],::Bool,LITOrigin[$7._mlir_value],::Origin[$7, $8]](*$0,sep:::StringSpan[$4, $5, $6],end:::StringSpan[$7, $8, $9],flush:::Bool,file:::FileDescriptor$),Ts.values`=[[typevalue<#kgen.instref<\"std::builtin::simd::SIMD,dtype=index,length=1\">>, scalar<index>]],sep.mut`4=false,end.mut`7=false"
; def main():
29e: 48 81 c4 e8 00 00 00 addq $0xe8, %rsp
2a5: c3 retq
2a6: 66 2e 0f 1f 84 00 00 00 00 00 nopw %cs:"_comptime_for_unroll::main()"(%rax,%rax)
リスト11-4: comptime_for_unroll.asm
注目ポイント |
内容 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
ループカウンタ・ |
まったく存在しない。3 つの |
通常の for では、i をインクリメントし 3 と比較して繰り返すか判定するコードが繰り返し実行されます。
comptime for ではその判定がコンパイル時に完了しており、実行時には純粋な処理だけが残ります。
繰り返し回数が増えるほど、この差は大きくなります。
図11-2: comptime for:通常の for との比較とアセンブリ出力
11.4 Parameters:[] と ()
Mojo の関数定義には [] と () という 2 種類の括弧が登場します。
どちらも引数を受け取りますが、確定するタイミングが異なります。
[]に書く parameter(パラメータ)はコンパイル時に確定します。型、SIMD 幅、バッファサイズなど、実行前から決まっている値を渡すために使います。()に書く argument(引数)は実行時に確定します。ユーザー入力、ループカウンタ、センサーの読み取り値など、実行してみないとわからない値を渡すために使います。
記法 |
確定タイミング |
使いどころ |
|---|---|---|
|
コンパイル時 |
型、サイズ、幅など「呼び出しごとに違うが実行前に確定する値」 |
|
実行時 |
ユーザー入力、ループカウンタなど動的な値 |
def lanes[width: Int](scale: Int) -> Int:
return width * scale
def main():
print(lanes[8](2))
リスト11-5: params_width_runtime_value.mojo
lanes[width: Int](scale: Int) では width が [] に、scale が () に書かれています。
lanes[8](2) と呼ぶと、コンパイラは「width = 8 は確定」として width 固定のコードを生成します。
一方 scale = 2 は実行時の引数として渡されます。
アセンブリで実際に何が起きているか確認します。
params_width_runtime_value.o:
(__TEXT,__text) section
_params_width_runtime_value::main():
; def main():
10: 48 83 ec 78 subq $0x78, %rsp
; print(lanes[8](2))
14: bf 02 00 00 00 movl $0x2, %edi
19: e8 00 00 00 00 callq "_params_width_runtime_value::lanes[::SIMD[::DType(int), ::SIMDLength(1)]](::SIMD[::DType(int), ::SIMDLength(1)]),width=8"
1e: 48 89 c7 movq %rax, %rdi
21: 48 89 44 24 10 movq %rax, 0x10(%rsp)
26: 90 nop
27: 90 nop
28: 48 8d 05 00 00 00 00 leaq _static_string_a8d4ace0dc8d360e(%rip), %rax
2f: 48 89 44 24 38 movq %rax, 0x38(%rsp)
34: 48 c7 44 24 40 01 00 00 00 movq $0x1, 0x40(%rsp)
3d: 48 c7 44 24 48 01 00 00 00 movq $0x1, 0x48(%rsp)
46: 48 89 44 24 18 movq %rax, 0x18(%rsp)
4b: 48 89 44 24 20 movq %rax, 0x20(%rsp)
50: 48 89 44 24 50 movq %rax, 0x50(%rsp)
55: 48 89 44 24 58 movq %rax, 0x58(%rsp)
5a: 48 c7 44 24 60 01 00 00 00 movq $0x1, 0x60(%rsp)
; print(lanes[8](2))
63: 90 nop
64: 90 nop
65: 48 8d 05 00 00 00 00 leaq _static_string_bbe01a6a523daf15(%rip), %rax
6c: 48 89 44 24 38 movq %rax, 0x38(%rsp)
71: 48 c7 44 24 40 01 00 00 00 movq $0x1, 0x40(%rsp)
7a: 48 c7 44 24 48 01 00 00 00 movq $0x1, 0x48(%rsp)
83: 48 8d 05 00 00 00 00 leaq _static_string_bbe01a6a523daf15(%rip), %rax
8a: 48 89 44 24 28 movq %rax, 0x28(%rsp)
8f: 48 8d 05 00 00 00 00 leaq _static_string_bbe01a6a523daf15(%rip), %rax
96: 48 89 44 24 30 movq %rax, 0x30(%rsp)
9b: 48 8d 05 00 00 00 00 leaq _static_string_bbe01a6a523daf15(%rip), %rax
a2: 48 89 44 24 50 movq %rax, 0x50(%rsp)
a7: 48 8d 05 00 00 00 00 leaq _static_string_bbe01a6a523daf15(%rip), %rax
ae: 48 89 44 24 68 movq %rax, 0x68(%rsp)
b3: 48 c7 44 24 70 01 00 00 00 movq $0x1, 0x70(%rsp)
; print(lanes[8](2))
bc: 48 89 e0 movq %rsp, %rax
bf: 48 c7 00 01 00 00 00 movq $0x1, "_params_width_runtime_value::lanes[::SIMD[::DType(int), ::SIMDLength(1)]](::SIMD[::DType(int), ::SIMDLength(1)]),width=8"(%rax)
c6: 48 8d 35 00 00 00 00 leaq _static_string_a8d4ace0dc8d360e(%rip), %rsi
cd: 48 8d 0d 00 00 00 00 leaq _static_string_bbe01a6a523daf15(%rip), %rcx
d4: 41 b8 01 00 00 00 movl $0x1, %r8d
da: 45 31 c9 xorl %r9d, %r9d
dd: 4c 89 c2 movq %r8, %rdx
e0: e8 00 00 00 00 callq "_std::io::io::print[KGENParamList[::Writable & ::AnyType],*::Writable & ::AnyType,LITImmOrigin,::Origin[::Bool(False), $2],::Bool,LITOrigin[$4._mlir_value],::Origin[$4, $5],::Bool,LITOrigin[$7._mlir_value],::Origin[$7, $8]](*$0,sep:::StringSpan[$4, $5, $6],end:::StringSpan[$7, $8, $9],flush:::Bool,file:::FileDescriptor$),Ts.values`=[[typevalue<#kgen.instref<\"std::builtin::simd::SIMD,dtype=index,length=1\">>, scalar<index>]],sep.mut`4=false,end.mut`7=false"
; def main():
e5: 48 83 c4 78 addq $0x78, %rsp
e9: c3 retq
ea: 66 0f 1f 44 00 00 nopw (%rax,%rax)
リスト11-6: params_width_runtime_value.asm
アセンブリのシンボル名に注目します。
callq "_params_width_runtime_value::lanes[::SIMD[::DType(int), ::SIMDLength(1)]](::SIMD[::DType(int), ::SIMDLength(1)]),width=8"
width=8 がシンボル名に刻まれています。
これが specialization(特化)です。
コンパイラは width = 8 に最適化されたコードを別途生成し、そのシンボルを直接呼び出します。
Int 型は 1.0.0 では内部的に SIMD[DType(int), SIMDLength(1)](要素数 1 の SIMD ベクトル)として表現されるため、シンボル名には ::Int ではなくこの内部表現がそのまま刻まれています。
一方、scale の値(2)はシンボル名には現れず、movl $0x2, %edi(offset 14)として実行時にレジスタ経由で渡されます。
width はコンパイル時に消え、scale だけが実行時の変数として残ります。
lanes[4](2) を呼んだ場合は width=4 の別シンボルが生成されます。
実行時に「width はいくつか」を判定する if 文はどこにも存在しません。
コラム: specialization のコスト対効果
lanes[8] と lanes[4] を両方呼ぶと、コンパイラは 2 つの別々の関数を生成します。
バイナリサイズはわずかに増えます。
見返りは「実行時のオーバーヘッドゼロ」です。
実行時に if width == 8 ... elif width == 4 ... を分岐する必要がなく、直接最適な SIMD 命令などが使われます。
GPU カーネルのように「幅はコンパイル時に決まっているが、スケールは実行時に変わる」という状況で効果が大きいパターンです。
図11-3: Parameters:[] と () の違いと specialization
11.5 Traits:振る舞いの約束
Trait は「この型はこの操作ができる」という約束をコンパイル時に表明する仕組みです。 Python の ABC(抽象基底クラス)や Rust の trait に近い概念ですが、Mojo の trait はすべてコンパイル時に検査されます。 実行時に「このメソッドがあるかどうか」を探しにいくコストはゼロです。
型に trait を宣言すると、コンパイラはその型が必要なメソッドを持つかどうかを検証します。 宣言しておくことで、その型を受け取る関数側が「この操作は必ず使える」と静的に知ることができます。
代表的な組み込み trait を次の表に示します。
trait |
保証する操作 |
代表的な型 |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
廃止方向 |
|
|
|
@fieldwise_init
struct Label(Copyable):
var text: String
def main():
var a = Label("x")
var b = a.copy()
print(a.text, b.text)
リスト11-7: traits_copyable_label.mojo
struct Label(Copyable): と書くことで、Label が Copyable を満たすことをコンパイラに宣言しています。
@fieldwise_init が __init__ を自動生成し、Copyable が .copy() を自動生成します。
var b = a.copy() は a のフィールドを新しいインスタンスにコピーし、a と b はそれぞれ独立した値を持ちます。
Copyable を宣言していない型に .copy() を呼ぶとコンパイルエラーになるため、「コピーできる型とできない型」をコード上で明確に区別できます。
図11-4: Traits:宣言、検証、静的保証の流れと代表的な組み込み trait
11.6 Generics:型を抽象化する
Generics(ジェネリクス)は「複数の型に対して同じロジックを書く」仕組みです。 Python では関数の引数に型を書かず、実行時に「このオブジェクトはこのメソッドを持つか」を探します(duck typing)。 Mojo の generics はこれをコンパイル時に行います。
def f[T: SomeTrait](x: T) と書くと、T は「SomeTrait を満たす任意の型」を受け取れます。
呼び出し時に T が確定し、コンパイラはその型専用のコードを生成します。
実行時に型を探す処理はありません。
def byte_len[T: Writable](x: T) -> Int:
return String(x).byte_length()
def main():
var s = String("abc")
print(byte_len(s))
リスト11-8: generics_writable_len.mojo
byte_len[T: Writable](x: T) は Writable を満たす任意の型を受け取り、String(x) で文字列化してバイト長を返します。
String を渡せば T = String に、Int を渡せば T = Int にそれぞれ確定し、型ごとに別のコードが生成されます。
String を渡した場合のシンボル名をアセンブリで確認します。
callq "_...byte_len[::Writable]($0),T=[typevalue<String>, struct<...>]"
T = String がシンボル名に刻まれています。
これが型パラメータの monomorphization(単相化)です。
Int を渡せば T=Int の別シンボルが生成されます。
どちらも「実行時に型を調べる」コードは存在しません。
Python との比較 |
説明 |
|---|---|
Python の |
実行時に |
Mojo の |
コンパイル時に |
速さの根拠 |
実行時の型検索なし。直接最適な命令列が実行される |
generics と trait を組み合わせることで「この操作ができる型なら何でも受け取れるが、できない型はコンパイル時に拒否する」という安全な汎用コードが書けます。
図11-5: Generics:Python の duck typing との比較と monomorphization
11.7 Constraints:誤用をコンパイル時に防ぐ
[] parameter には値だけでなく、その値が満たすべき条件(constraint)を where 句で付与できます。
条件を満たさない値でインスタンス化しようとすると、実行前にコンパイルエラーになります。
Python では「サイズが 0 以下のバッファは使えない」という制約を表現するには実行時チェック(if size <= 0: raise ValueError(...))が必要です。
Mojo の constraint はこれをコンパイル時に移動させ、そもそも不正な型をプログラムに存在させないことができます。
struct Buf[size: Int] where size > 0:
var data: Int
def __init__(out self):
self.data = Self.size
def main():
var b = Buf[4]()
print(b.data)
リスト11-9: constraints_buf_positive.mojo
struct Buf[size: Int] where size > 0: の where size > 0 が constraint です。
Buf[4]() はコンパイルが通り、b.data には 4 が入ります。
一方 Buf[0]() と書くと where size > 0 を満たさないためコンパイラが拒否します。
Self.size は parameter 値をそのまま参照する書き方です。
size はコンパイル時定数なので、self.data = Self.size は実行時に即値として埋め込まれます。
要素 |
説明 |
|---|---|
|
|
|
|
|
parameter 値をコンパイル時定数として参照する |
不変条件をコードで表現できる |
バグを実行前に防ぐ(実行時チェック不要) |
where 句は GPU レイアウトの制約(アライメント、タイル幅など)でも使われます。
「2 の冪乗でなければならない」「正の値でなければならない」といった条件を型の定義に直接埋め込むことで、誤った使い方をコードが存在する段階で検出できます。
図11-6: Constraints:where 句による制約と Python との比較
11.8 Reflection:コンパイル時の型情報取得
Reflection(リフレクション)は「プログラムが自分自身の構造を調べる」機能です。
Python では dir(obj)、type(obj)、__dict__ などを使って実行時に型情報を取得します。
Mojo の reflection はタイミングが異なります。
comptime ブロック内で型情報を取得するため、コンパイル時に結果が確定し、実行時コストはゼロです。
汎用のシリアライザや、フィールド数に応じてコードを生成するマクロ的な用途に向いています。
struct Point:
var x: Int
var y: Int
def field_count[T: AnyType]() -> Int:
comptime n = reflect[T].field_count()
return n
def main():
print(field_count[Point]())
リスト11-10: reflection_field_count.mojo
reflect[T].field_count() は T のフィールド数をコンパイル時に返します。
reflect[T] は関数呼び出しではなく、型 T をコンパイル時パラメータとして保持するコンパイル時のハンドルで、reflect 自体は prelude から自動的に使えるため import は不要です。
comptime n = reflect[T].field_count() とすることで n はコンパイル時定数になり、実行時には即値として埋め込まれます。
Point は x と y の 2 フィールドを持つため、field_count[Point]() は 2 を返します。
field_count 関数自体は generics で書かれており、T に別の struct を渡せばそのフィールド数が返ります。
型を切り替えるたびに「何フィールドあるか」を手書きする必要はありません。
Python の reflection |
Mojo の reflection |
|
|---|---|---|
タイミング |
実行時( |
コンパイル時( |
用途 |
デバッグ、シリアライズ、メタクラス |
型の制約確認、汎用コードのコンパイル時最適化 |
実行時コスト |
あり(辞書検索等) |
なし(コンパイル後は定数として埋め込まれる) |
Python の reflection は柔軟ですが、実行するまで「フィールドが何個あるか」すら確定しません。 Mojo の reflection はコンパイル時に確定するため、型の情報をもとにしたコード生成をゼロコストで行えます。
図11-7: Reflection:Python との比較とコンパイル時評価のゼロコスト
11.9 まとめ:コンパイル時と実行時の対応表
本章で扱ったコンパイル時と実行時の操作の対応をまとめます。
操作 |
コンパイル時 |
実行時 |
|---|---|---|
条件分岐 |
|
|
ループ |
|
|
型・サイズ |
|
|
型の検査 |
|
不可( |