第11章 メタプログラミング

11.1 この章で学ぶこと

この章で学ぶ内容は以下のとおりです。

表11-1: この章で学ぶこと

学ぶこと

ポイント

コンパイル時評価の 3 つの形

comptime ifcomptime foralias(コンパイル時定数)

アセンブリで見る「消えた枝」

comptime if で選ばれなかった枝はバイナリに存在しない

[]() の区別

コンパイル時 parameter vs 実行時 argument

specialization の証拠

シンボル名に width=8T=String が刻まれる

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

表11-2: comptime if:選ばれた枝だけが残る

注目ポイント

内容

movq $0xa, 0x10(%rsp)

0xa = 10 の即値。print(10) の引数がスタックに直接書き込まれている

movl $0xa, %edi(offset bb)

同じく 10 を引数として print に渡している

print(0) への参照

まったく存在しないelse 側はバイナリから完全に消えている

分岐命令(jmpcmp

存在しない。コンパイル時に選択が終わっているため不要

通常の if では「実行時に branch == 1 かどうか比較し、どちらかにジャンプする」コードが生成されます。 comptime if では比較もジャンプも不要で、print(10) の呼び出し一本だけが残ります。 これが「コンパイル時に選ばれた枝だけが残る」の意味です。

comptime if:選ばれた枝だけが残る

図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 = 0i = 1i = 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

表11-3: comptime for:ループの展開

注目ポイント

内容

xorl %r9d, %r9dmovl %r9d, %edi(offset c2〜ca)

i = 0 を即値 0 として print の引数に渡している

callq print(offset db)

i = 0print(0) 呼び出し

movl $0x1, %r8dmovq %r8, %rdi(offset cd〜197)

i = 1 を即値 1 として print の引数に渡している

callq print(offset 19d)

i = 1print(1) 呼び出し

movl $0x2, %edi(offset 288)

i = 2 を即値 2 として print の引数に渡している

callq print(offset 299)

i = 2print(2) 呼び出し

ループカウンタ・cmpjmp 命令

まったく存在しない。3 つの print 呼び出しが直列に並んでいるだけ

通常の for では、i をインクリメントし 3 と比較して繰り返すか判定するコードが繰り返し実行されます。 comptime for ではその判定がコンパイル時に完了しており、実行時には純粋な処理だけが残ります。 繰り返し回数が増えるほど、この差は大きくなります。

comptime for:ループの展開

図11-2: comptime for:通常の for との比較とアセンブリ出力

11.4 Parameters:[]()

Mojo の関数定義には []() という 2 種類の括弧が登場します。 どちらも引数を受け取りますが、確定するタイミングが異なります。

  • [] に書く parameter(パラメータ)はコンパイル時に確定します。型、SIMD 幅、バッファサイズなど、実行前から決まっている値を渡すために使います。

  • () に書く argument(引数)は実行時に確定します。ユーザー入力、ループカウンタ、センサーの読み取り値など、実行してみないとわからない値を渡すために使います。

表11-4: [] はコンパイル時、() は実行時

記法

確定タイミング

使いどころ

def f[N: Int]()

コンパイル時

型、サイズ、幅など「呼び出しごとに違うが実行前に確定する値」

def f(x: Int)

実行時

ユーザー入力、ループカウンタなど動的な値

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 カーネルのように「幅はコンパイル時に決まっているが、スケールは実行時に変わる」という状況で効果が大きいパターンです。

Parameters:[] と ()

図11-3: Parameters:[]() の違いと specialization

11.5 Traits:振る舞いの約束

Trait は「この型はこの操作ができる」という約束をコンパイル時に表明する仕組みです。 Python の ABC(抽象基底クラス)や Rust の trait に近い概念ですが、Mojo の trait はすべてコンパイル時に検査されます。 実行時に「このメソッドがあるかどうか」を探しにいくコストはゼロです。

型に trait を宣言すると、コンパイラはその型が必要なメソッドを持つかどうかを検証します。 宣言しておくことで、その型を受け取る関数側が「この操作は必ず使える」と静的に知ることができます。

代表的な組み込み trait を次の表に示します。

表11-5: Traits:振る舞いの約束

trait

保証する操作

代表的な型

Copyable

.copy() でコピーできる

IntFloat64Label

Movable

^ で所有権転送できる

StringList[T]

Writable

String(x) で文字列化できる

IntFloat64String

Stringable

str(x) で文字列化できる(旧 API)

廃止方向

Comparable

<>== が使える

IntFloat64

@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): と書くことで、LabelCopyable を満たすことをコンパイラに宣言しています。 @fieldwise_init__init__ を自動生成し、Copyable.copy() を自動生成します。

var b = a.copy()a のフィールドを新しいインスタンスにコピーし、ab はそれぞれ独立した値を持ちます。 Copyable を宣言していない型に .copy() を呼ぶとコンパイルエラーになるため、「コピーできる型とできない型」をコード上で明確に区別できます。

Traits:振る舞いの約束

図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 の別シンボルが生成されます。 どちらも「実行時に型を調べる」コードは存在しません。

表11-6: Generics:型を抽象化する

Python との比較

説明

Python の def f(x): (duck typing)

実行時に x の型を見てメソッドを探す(動的ディスパッチ)

Mojo の def f[T: Writable](x: T):

コンパイル時に T を確定し、T 専用のコードを生成(静的特化)

速さの根拠

実行時の型検索なし。直接最適な命令列が実行される

generics と trait を組み合わせることで「この操作ができる型なら何でも受け取れるが、できない型はコンパイル時に拒否する」という安全な汎用コードが書けます。

Generics:型を抽象化する

図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 は実行時に即値として埋め込まれます。

表11-7: Constraints:誤用をコンパイル時に防ぐ

要素

説明

struct Buf[size: Int] where size > 0:

size が 0 以下のとき コンパイルエラー

Buf[0]() を書くと

where size > 0 を満たさないのでコンパイラが拒否

Self.size

parameter 値をコンパイル時定数として参照する

不変条件をコードで表現できる

バグを実行前に防ぐ(実行時チェック不要)

where 句は GPU レイアウトの制約(アライメント、タイル幅など)でも使われます。 「2 の冪乗でなければならない」「正の値でなければならない」といった条件を型の定義に直接埋め込むことで、誤った使い方をコードが存在する段階で検出できます。

Constraints:誤用をコンパイル時に防ぐ

図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 はコンパイル時定数になり、実行時には即値として埋め込まれます。 Pointxy の 2 フィールドを持つため、field_count[Point]()2 を返します。

field_count 関数自体は generics で書かれており、T に別の struct を渡せばそのフィールド数が返ります。 型を切り替えるたびに「何フィールドあるか」を手書きする必要はありません。

表11-8: Reflection:コンパイル時の型情報取得

Python の reflection

Mojo の reflection

タイミング

実行時(dir(obj)type(obj) 等)

コンパイル時(comptime n = ...

用途

デバッグ、シリアライズ、メタクラス

型の制約確認、汎用コードのコンパイル時最適化

実行時コスト

あり(辞書検索等)

なし(コンパイル後は定数として埋め込まれる)

Python の reflection は柔軟ですが、実行するまで「フィールドが何個あるか」すら確定しません。 Mojo の reflection はコンパイル時に確定するため、型の情報をもとにしたコード生成をゼロコストで行えます。

Reflection:コンパイル時の型情報取得

図11-7: Reflection:Python との比較とコンパイル時評価のゼロコスト

11.9 まとめ:コンパイル時と実行時の対応表

本章で扱ったコンパイル時と実行時の操作の対応をまとめます。

表11-9: まとめ:コンパイル時と実行時の対応表

操作

コンパイル時

実行時

条件分岐

comptime if → 選ばれた枝のみ残る

if → 実行時判定、両方の枝がバイナリに存在

ループ

comptime for → 展開済み(ループ命令なし)

for → ループカウンタ + 分岐命令

型・サイズ

def f[N: Int]()N がシンボル名に刻まれる

def f(n: Int) → 実行時に任意の値

型の検査

reflect[T].field_count() → 定数

不可(type(x) に相当するものは原則ない)