1 ポイント 投稿者 GN⁺ 2025-06-08 | 1件のコメント | WhatsAppで共有
  • 低レベル最適化は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件のコメント

 
GN⁺ 2025-06-08
Hacker Newsの意見
  • zigで最も興味深く感じるのは、ビルドシステムの簡便さ、クロスコンパイル、そして高い反復速度を目指している点。自分はゲーム開発者なので性能は重要だが、ほとんどの要件に対しては大半の言語が十分な性能を提供する。だから言語選択の最優先基準ではない。どの言語でも強力なコードは書けるが、数十年にわたって保守できる将来志向のフレームワークを目指している。C/C++はどこでもサポートされている点で基本の選択肢になってきたが、zigもその域まで来られる気がする
    • 面白半分でかなり古いKindle端末(Linux 4.1.15)でzigを動かしてみたが、zigの完成度には驚かされた。ほとんどがそのまま動き、古いGDBでも奇妙なバグをデバッグできた。自分もzigに魅了された。詳しい体験はこちらで確認できる
    • ほとんどの言語で強力なコードは書けると思っているが、数十年先を見据えられるモジュラーなコードを望んでいる。Zigは好きだが、長期保守性とモジュール性の面では欠点があると思う。Zigはカプセル化に敵対的な言語だ。構造体メンバーをprivateにできない。このIssueコメントが例だ。Zigの立場は、内部表現なるものは本来存在すべきではなく、すべての利用者が内部実装を理解できるよう文書化・公開すべきだというものだ。しかしAPI契約、つまりモジュラーソフトウェアの核心を守るには内部実装を隠せなければならず、それができない。いつかZigがprivateフィールドをサポートしてくれることを願う
    • Rustを少し触ってみたが気に入った。だが「ひどい」という話を聞いてしばらく止め、その後また使い始めている。やはり良い。なぜあそこまで嫌われるのかよくわからない。醜いジェネリクス構文ならC#やTypeScriptも同じだ。Borrow Checkerも低レベル言語の経験があれば理解しやすい
    • Zigはもっと単純なRust、そしてより良いGoのように感じる。一方でzig上に作られたツールの中では、bunが本当に感嘆するほど好きだ。bunのおかげで生活がとても楽になった。Rust製のuvも似た体験を与えてくれる
    • C/C++が基本だという点には同意する。Cより良いものを作ろうとしても、その多くは結局C++になってしまった。それでも挑戦は止めるべきではない。RustとZigは、今なおより良いものを期待させてくれる証拠だ。自分はこれからC++をもっと学ぶつもりだ
  • 最先端のコンパイラが言語仕様を破ることがあるとしても、Clangの無限ループ終了仮定はC11以降の標準では正しい。C11では次のように明記されている。「制御式が定数式ではなく、入出力/volatile/sync/atomic演算も行わないループについては、コンパイラは終了すると仮定できる」
    • C++では(将来のC++26までは)すべてのループにその規定が適用されるが、おっしゃる通りC言語では「制御式が定数式でないループ」にだけ適用される。つまり、for(;;);のような明白な無限ループは実際に無限ループでなければならず、Rustのloop {}も同様であるべきだ。ところがLLVM開発者はしばしば、自分たちがC++コンパイラしか作っていないかのように振る舞い、Rustで「無限ループをお願いします」と言ってもLLVMが「C++基準ではそんなことは起きないので最適化!」としてしまい問題が起きる。誤った言語に誤った最適化が適用されたわけだ
  • コンパイル時(comptime)機能がなくても、文字列比較をインライン化・アンロールすることはCでも十分可能だ。関連例
    • 指摘の通りだ! 最初の例はあまりに単純すぎた。より良い例としてはコンパイル時 suffix automatonがある。また、上でリンクしたgodboltのコードは、むしろやるべきでない二つの例の一つを示している
  • 例として挙げられていたJavaScriptコードについて、V8が生成したバイトコードが非効率だという話は、良い比較例ではないと思う。ZigやRustには非常に新しい環境を指定してコンパイルさせているのに、V8にはそうした最適化条件を強制していない。実際、現代のJITも条件が整えばベクトル化できる。そして大半の現代言語も文字列関連の最適化は似たような処理をする。参考までにC++の例もある
    • 実質的にJSとZigを比べるのは、リンゴとフルーツサラダを比べるようなものだ。Zigの例では型とサイズが固定された配列を使っているが、JSは実行時にさまざまな型が入り得るgenericなコードだ。このためJSでは、型情報をうまく与えればJITがはるかに速いループを生成できる(ベクトル化まではしなくても)。実際にはTypedArrayはあまり使われない。初期化コストが大きく、頻繁に再利用する場合にだけ見合うからだ。また記事ではJSコードが膨らんでいるとしていたが、JITが配列長チェックを信用できずガードを入れていることが大きく、実際には誰でもi < x.lengthのようなループを書くのでJIT最適化される。そういう意味ではやや揚げ足取りだが、細かい差ではある
    • RustとZigのgodbolt例は、より古いCPUをターゲットに変更することもできる。JS側のターゲット制約は考えていなかった。そしてC++の例は、clangがどれほど良いコードを出すかを示す事例だ。ただ現状でもassemblyにはあまり満足していない(zigが特定CPUターゲット向けにビルドされていることを考慮しても)。コンパイル時 Suffix AutomatonのC++移植例があれば本当に面白そうだ。これはC++コンパイラでは推測しにくいcomptimeの実利用例だ
  • 「高水準言語は低水準言語が持つ『intent』に欠ける」という言い方には疑問がある。むしろ多様な方法でより詳細に意図を表現できることこそ、高水準言語の長所だと思う
    • 自分も同意する。根本的に高水準言語と低水準言語の違いは、高水準言語では意図を表現し、低水準言語では実装メカニズムそのものを露出させなければならないという点だ
    • ここでいう「意図」とは、「この購入の税金計算」のような業務的意図ではなく、「このバイトを左に3ビットシフトする」といった、コンピュータに何をさせるかに近い意味だ。たとえばpurchase.calculate_tax().await.map_err(|e| TaxCalculationError { source: e })?;のようなコードは意図に満ちているが、実際にどんなマシンコードになるかは予測できない
  • Zigのallocatorモデルは本当に気に入っている。GoでもGCの代わりにリクエストごとのallocatorのようなものを使えたら良いのにと思う
    • Goでもカスタムallocatorやarenaが不可能ではないが、使い勝手が非常に悪く、適切に使うのが難しい。言語レベルで所有権(ownership)ルールを表現したり強制したりする方法もない。結局は文法が少し違うだけのCを書くようなもので、GCがなければC++以上に危険ですらある
  • 「Zigの冗長さ(verbosity)が好きだ」という意見には共感するが、正直少し言い回しが妙だ。Cはあちこちが緩い一方で、Zigは逆に過剰な「アノテーションノイズ」を要求する場面が多い(特に数式で明示的な整数キャストをする時)。関連文参照。性能面ではzigがcより速い場合の多くは、Zigがより攻撃的なLLVM最適化設定(-march=native、全体プログラム最適化など)を使っているためだ。実際、Cでもunreachableのような最適化ヒントは言語拡張で可能で、Clangも定数畳み込みに非常に積極的だ。つまり、ZigのcomptimeとCのコード生成の差は、コンパイラの最適化設定に由来する場合が多い。TL;DR: Cが遅いなら、まずコンパイラ設定を確認すべきだ。結局、最適化の中核はLLVMなのだから
    • キャスト部分の例で言えば、むしろ関数を一つ作ってキャストを包めば、コード再利用性と意図の明確さを高められる
      fn signExtendCast(comptime T: type, x: anytype) T {
        const ST = std.meta.Int(.signed, @bitSizeOf(T));
        const SX = std.meta.Int(.signed, @bitSizeOf(@TypeOf(x)));
        return @bitCast(@as(ST, @as(SX, @bitCast(x))));
      }
      export fn addi8(addr: u16, offset: u8) u16 {
        return addr +% signExtendCast(u16, offset);
      }
      
      この方法でも同じassemblyが出力され、汎用性が高く明快だ
    • Zigのアイデアは興味深く、元記事で期待していたよりもコンパイル時(comptime)と全体プログラムコンパイルに重心が置かれていた。これには共感する。ちなみにVirgilは2006年から、コンパイル時に言語全体を活用し、全体プログラムコンパイルをサポートしていた。VirgilはLLVMをターゲットにしていないので、速度比較は結局バックエンド比較になる。このアプローチのおかげでVirgilは、メソッド呼び出しを事前に静的束縛(devirtualize)し、使われないフィールドやオブジェクトを可能な限り除去し、フィールドやヒープオブジェクトにまで定数伝播を行い、完全特化まで実現できる非常に強力な最適化が可能だ
    • 今後のAI活用を考えると、ますます明示的で冗長な言語が主流になる気がする。AIでコーディングするか、それが正しいかは別として、多くの開発者がAIの助けを好むようになれば、言語もそれに合わせて変わっていくだろう
    • 新しいx86バックエンドが導入されれば、今後はCとZigの性能差がZigプロジェクトそのものに起因する例も見られるようになるかもしれない
    • 明示的なinteger castについては、近いうちにもう少しすっきりする改善が入る予定だ。関連議論参照
  • 「CがPythonより速い」のように言語そのものをベンチマークするのは適切ではないが、言語の一部機能が最適化の大きな障壁になることはある。適切な言語を使えば、開発者とコンパイラの双方が自然で高速な形で意図を表現できる
  • Zigのfor loop構文はあまりにも雑然として見える。リストを二つ並べて位置を合わせなければならないなんて、見るだけで目が痛い。近年の言語があまりにも多くの「マジック」構文や特殊記号を詰め込みすぎているのは失敗だと思う。何時間も見続けるのはつらそうだ
    • こうした二つの配列を走査するパターンは低レベルコードでは非常によくあり、並列走査も同様だ。だからZigがそれを明確かつ自然にサポートしているのは、むしろ妥当だと思う。なぜそれで目が痛くなるのか不思議だ
  • 最適化は非常に重要だ。その効果は時間が経つほどさらに大きくなる
    • ただし、そのソフトウェアが実際に使われることが前提の話ではある