低レベル最適化とZig
(alloc.dev)- 低レベル最適化はZig言語で容易に実装できる
- コンパイラはほとんどの状況で最適化をうまく行うが、より良い性能を得るには、時としてプログラマの意図を明確に伝える必要がある
- Zigはコンパイル時実行(comptime) 機能により、高性能なコード生成と強力なメタプログラミングを支援する
- Rustと比較すると、Zigはアノテーションと明示的なコード構造によって、より精密な最適化が可能
- 文字列比較などの反復的な演算では、comptimeを活用することで通常の関数より優れたアセンブリコードを生成できる
最適化とZig
「すべては可能だが、興味深いものは簡単には手に入らない」という有名な警句のように、プログラムの最適化は常に開発者の主要な関心事である。クラウドインフラのコスト、レイテンシ改善、システムの単純化などのために、コード最適化は必須となる。この記事では、Zigにおける低レベル最適化の概念と、Zigの強みを中心に説明する。
コンパイラは信頼できるのか?
- 一般的には「コンパイラを信頼せよ」という助言が多いが、実際にはコンパイラが期待どおりに動作しない、あるいは言語仕様に違反する場合もある
- 高水準言語では意図(intent) を明確に伝えにくいため、性能上の制約が生じる
- 低水準言語では、コードの明示性によってコンパイラが最適化に必要な情報を把握できる。たとえばJavaScriptとZigの
maxArray関数を比較すると、Zigは明確な型、アラインメント、aliasの有無などをランタイムではなくコンパイル時に伝える - 同じ
maxArray演算をZigとRustで書くと、ほぼ同等の高性能なアセンブリコードが得られるが、意図をより適切に表現するほど最適化結果は向上する - ただし、常にコンパイラ性能を信頼できるわけではないため、ボトルネック区間ではコードとコンパイル結果を直接確認し、最適化方法を模索する必要がある
Zigの役割
- Zigは正確な明示性、豊富な組み込み関数、ポインタとアノテーション、comptime、明確に定義されたIllegal Behaviorなどの特性により、抽象的な情報なしでも最適化されたコードを作成できる
- Rustはメモリモデルのおかげで、引数にaliasがないことが基本的に保証されるが、Zigではnoaliasなどのアノテーションが必要になる
- もしLLVM IRだけを基準にするなら、Zigの最適化水準も高い
- 何よりもZigのcomptime(コンパイル時実行) は強力な最適化ツールである
comptimeとは何か?
- Zigのcomptimeは、コード生成、定数値の埋め込み、型ベースのジェネリック構造体生成などに活用され、ランタイム性能向上に重要な役割を果たす
- comptimeによってメタプログラミングを実装できる
- C/C++のマクロやRustのmacroシステムと異なり、comptimeは別個の構文ではなく通常のコードである
- comptimeコードはASTを直接変更せず、あらゆる型に対してコンパイル時に検査、反映、生成が可能である
- comptimeの柔軟性はRustなど他言語の改善にも影響を与えており、自然な形でZig言語に統合されている
comptimeの限界
- token-pastingのような一部のmacro機能はZigのcomptimeでは代替できない
- Zigはコードの可読性を重視するため、スコープ外での変数生成やマクロ定義などは許可しない
- その代わり、Zigのcomptimeには型リフレクション、DSL実装、文字列パース最適化など、幅広いメタプログラミング活用例が存在する
comptimeを活用した文字列比較最適化
- 一般的な文字列比較関数はどの言語でも実装できるが、Zigでは2つの文字列のうち1つがcomptimeで既知の定数である場合、より効率的なアセンブリコードを生成できる
- たとえば、一方の文字列が常に
"Hello!\n"であるなら、この値をバイト単位ではなくより大きなブロック単位で比較する最適化を利用できる - そのためにcomptimeを使えば、SIMDベクトル、ブロック処理、残りバイトの最適化など、高性能コードをコンパイル時に生成できる
- この方法により、反復的な文字列比較だけでなく、静的データに基づくさまざまなマッピング、完全ハッシュテーブル、ASTパーサなど、多様な性能重視コードを実装できる
結論
- Zigは低レベル最適化に非常に適しており、明示的なコード構造と強力なcomptime機能のおかげで最高の性能を直接実装できる
- Rustなど他言語と比べても、Zigのコンパイル時プログラミング能力と明示性は、高性能ソフトウェア開発に大きな利点として作用する
- Zigの最適化能力は今後さらに重要な競争力となるだろう
1件のコメント
Hacker Newsの意見
bunが本当に感嘆するほど好きだ。bunのおかげで生活がとても楽になった。Rust製のuvも似た体験を与えてくれるfor(;;);のような明白な無限ループは実際に無限ループでなければならず、Rustのloop {}も同様であるべきだ。ところがLLVM開発者はしばしば、自分たちがC++コンパイラしか作っていないかのように振る舞い、Rustで「無限ループをお願いします」と言ってもLLVMが「C++基準ではそんなことは起きないので最適化!」としてしまい問題が起きる。誤った言語に誤った最適化が適用されたわけだgenericなコードだ。このためJSでは、型情報をうまく与えればJITがはるかに速いループを生成できる(ベクトル化まではしなくても)。実際にはTypedArrayはあまり使われない。初期化コストが大きく、頻繁に再利用する場合にだけ見合うからだ。また記事ではJSコードが膨らんでいるとしていたが、JITが配列長チェックを信用できずガードを入れていることが大きく、実際には誰でもi < x.lengthのようなループを書くのでJIT最適化される。そういう意味ではやや揚げ足取りだが、細かい差ではあるpurchase.calculate_tax().await.map_err(|e| TaxCalculationError { source: e })?;のようなコードは意図に満ちているが、実際にどんなマシンコードになるかは予測できない-march=native、全体プログラム最適化など)を使っているためだ。実際、Cでもunreachableのような最適化ヒントは言語拡張で可能で、Clangも定数畳み込みに非常に積極的だ。つまり、ZigのcomptimeとCのコード生成の差は、コンパイラの最適化設定に由来する場合が多い。TL;DR: Cが遅いなら、まずコンパイラ設定を確認すべきだ。結局、最適化の中核はLLVMなのだから