世界を動かす技術を、日本語で。

「ZLinq」.NET向けゼロアロケーションLINQライブラリ

概要

  • ZLinqは 構造体ジェネリクス に基づき、 ゼロアロケーション を実現した革新的なLINQライブラリです。
  • .NET 10の全メソッド を100%カバーし、 SIMD最適化 や複数プラットフォーム対応を提供します。
  • パフォーマンスアセンブリ肥大化回避 を両立し、既存のLINQ実装の課題を解決しました。
  • Drop-In Replacement Source Generatorテスト互換性 も確保し、既存コードの置き換えも容易です。
  • 詳細な最適化設計 やアーキテクチャの工夫についても解説します。

ZLinq v1リリースとその特徴

  • ZLinq v1 は、構造体・ジェネリクス活用により ゼロアロケーション を実現することに成功したLINQライブラリであることを発表
  • LINQ to Span, LINQ to SIMD, LINQ to Tree (FileSystem, JSON, GameObject等)などの拡張を提供すること
  • Source GeneratorによるDrop-In Replacement 機能や、.NET Standard 2.0, Unity, Godotなど 複数プラットフォーム への対応を実現
  • GitHubで 2000スター超 を獲得するなど、注目を集めていることを報告
  • 詳細は公式リポジトリ(https://github.com/Cysharp/ZLinq)で確認すること

過去のLINQ実装の課題とZLinqの優位性

  • 構造体ベースのLINQは過去にも存在したが、 実用的なものはなかった ことを指摘
    • アセンブリサイズの肥大化
    • オペレータカバレッジ不足
    • 最適化不足によるパフォーマンス問題
  • ZLinqは .NET 10の全メソッド・オーバーロードを100%カバー し、 99%の動作互換 を実現
  • SIMD最適化アロケーション削減以外の最適化 も実装し、多くのケースで標準LINQを上回る性能を達成すること
  • 開発者の豊富なLINQ実装経験(linq.js, UniRx, R3, SimdLinq等)や高速シリアライザ(MessagePack-CSharp, MemoryPack等)知見を融合していること

パフォーマンスとベンチマーク

  • 通常のLINQはメソッドチェーンが増えると アロケーションが増加 するが、ZLinqは 常にゼロアロケーション を維持
  • ベンチマークシナリオをGitHub Actionsで公開し、多くの実用ケースでZLinqが優位であることを検証
  • Selectの多重呼び出しDistinct/OrderBy等の中間バッファ必須操作 で特に大きな差を実証
  • .NET 9の演算子チェーン最適化 も全て実装し、 全世代の.NET で最新最適化を享受可能
  • AsValueEnumerable() を追加するだけで既存コードをZLinq化できる利便性
  • System.Linq.Tests を移植し、9000件のテストで動作互換性を保証

Drop-In Replacement Source Generator

  • Drop-In Replacement Source Generator により、 AsValueEnumerable()さえ不要 でZLinqへの置き換えが可能
  • 置き換え範囲の制御も柔軟に行えること
  • テストコードも変更なしでZLinqに切り替えられる運用性

ValueEnumerableアーキテクチャと最適化

  • ValueEnumerable<T> は、 ref struct によるチェーンを可能にし、 IValueEnumerator<T> インターフェースを基盤とする設計
  • 型推論の制約を回避するため、 型宣言で型を明示 し、アセンブリ肥大化を防止
  • TryGetNext(out T current) 方式を採用し、 MoveNext + Current の2メソッド呼び出しを 1回に集約 して高速化
  • 構造体サイズの増大 を最小化し、チェーン時のコピーコストを抑制
  • 共変・反変非対応 だが、Span<T>との互換性や安全性を優先

TryGetNextの詳細

  • IEnumerator<T> はMoveNext()とCurrentの2操作が必要だが、ZLinqは TryGetNext で1回の呼び出しに集約
  • Rustのイテレータ設計も参考にした設計思想
  • Select実装 比較で、ZLinqは「current」フィールド不要で構造体サイズを縮小
  • 構造体チェーン が深くなってもパフォーマンスを維持

共変・反変の非対応について

  • 共変・反変 はSpan<T>と非互換であり、現代.NETでは 安全性の観点から非推奨
  • 例:配列のSpan変換でランタイムエラー発生リスク
  • ZLinqは 安全性とパフォーマンス を優先し、これらの機能を割り切っていること

TryGetNonEnumeratedCount, TryGetSpan, TryCopyToの最適化

  • TryGetNonEnumeratedCount :元ソースが確定サイズかつフィルタなしの場合、ToArray等で 固定長配列確保 が可能
  • TryGetSpan :連続メモリアクセス可能な場合、 SIMDやSpanベース高速処理 が実現
  • TryCopyTo :内部イテレータによる パフォーマンス向上 を実現
  • 外部イテレータと内部イテレータ の違いを活かした最適化設計

このように、ZLinqは 構造体ベースLINQの集大成 として、 パフォーマンス・安全性・実用性・互換性 を高次元で融合した次世代LINQライブラリであることを強調することができます。

Hackerたちの意見

なんでこういう改善が.NET自体に戻せないのかな?

それができない理由はないと思うよ: https://github.com/dotnet/runtime/pulls API変更リクエストの公式プロセスがあるから: https://github.com/dotnet/runtime/blob/main/docs/project/api...

こういうものを作るような人は、.NETに統合するための官僚的な手続きにはあまり耐えられないだろうなって想像できる。

ZLinqはValueEnumerableという独自の列挙型に依存してるんだけど、これは構造体なんだ。これをドロップイン置き換えとして使って再コンパイルすればたぶん動くけど、大きなアプリケーションではもっと複雑になるかも。Linqメソッドの正確なシグネチャに依存しているコードがあるかもしれないし、リフレクションを使った場合には検出できないこともあって、静かに壊れちゃう可能性もある。別の列挙型を追加するのは、全体のAPIの面積を実質的に倍増させるような大きな変更になるから、時間がかかるかも。まだSpanをサポートしてないところもあるしね。それに、Linqに関してはオーバーロードの数が考慮されたデザインの決定もあった。これを.NETに追加するのは、ValueEnumerableに変換する拡張メソッドを使えばできるかもしれないけど、その列挙型のサポートがなければ、異なる列挙型の間で行き来しなきゃいけない壁のある庭みたいになっちゃう。あんまり良くないと思うけど、可能ではあるかな。

ブログ記事を見た感じ、ジェネリックインスタンスの爆発はコードサイズや起動時間にとって深刻な問題になりそうだけど、なんとか解決できるかもね。パフォーマンスは確かに印象的だし。今のLINQのデフォルトの動きは、実際にイテレートしている型を隠すためにIEnumerableみたいなインターフェースを積極的に使ってるから、パフォーマンスに影響が出るんだ(これがZLinqが勝てる理由の一部ね)。でも、利点もあって、例えばWhere(seq)の同じ実装をいろんなTに使えるから、イテレートするたびに独自のボディをJITやAOTコンパイルする必要がないんだ。ZLinqを見てると、クエリが複雑になるにつれてユニークなジェネリック構造体の型が爆発的に増える可能性があるみたいだけど、実際にはそれほど悪くないかもしれないね。

C#では参照型を使うのがよりイディオマティックだね。ある程度、バグが少ないとも言える(問題なく渡せるから)。ほとんどのコアライブラリは、値型とボクシングから始める代わりに参照型を使ってる。TaskライブラリはValueTaskを追加したけど、結構手間がかかった。逆にLINQは展開されたループやライブラリに置き換えやすいから、あまりプレッシャーがなかったんだ。将来的に何か起こるかもしれないけど、かなりの努力が必要だね。

いくつかの小さな破壊的変更があって、イテレーションの順序が公式のLINQ実装と必ずしも同じじゃなかったり、Sumがチェックありとなしで異なる値を返すことがある。ほとんどの人には問題ないだろうけど、微妙な破壊的変更ではあるね。

これは素晴らしいね。生産環境の.NETサービスで働いてたけど、アロケーションのせいでホットパスではLINQを避けなきゃいけなかったことが多かった。forループや他の構造を使って関数を再実装するのは、LINQのメソッドチェーンに比べて時間がかかるしエラーが起きやすかった。LINQメソッドをチェーンするのはめちゃくちゃ強力で、JSのfilter、map、reduceみたいな感じだけど、他にもいろんな演算子や最適化がある。もっと多くの言語にこういうのがあればいいのに。

これを使う利点は、高階関数を使うのと比べて何があるの?Rubyだとlist.map { }やselect { }ができるけど、そっちの方が自然に感じるし(特別な言語サポートがいらない)、関数のセットも豊富(group_byやchunk_whileとか)で、ユーザーが自分のメソッドを追加することもできるし(モンキーパッチが気にならなければね)。

いくつかの企業は、基本的にLINQを避けようとするみたいだけど、実際にはLINQを避けても大したメリットはないことが多いよね。もちろん、行列の掛け算みたいなホットパスの場合は全然意味があるけど、LINQを避けることで不快な副作用が出ることもある。コードの健全性や品質が失われるっていうね。

理論的には.NET 10がこれを時代遅れにするはずで、見出しの機能[1]は基本的にこれに関するものだよ。実際には、まあ、ヒューリスティックだから、今ちょうど特にパフォーマンスに敏感なプロジェクトにこれを追加してるところ :) 編集: それと、C#はLinqを契約として認識してるのがいいね。正しいメソッド名とシグネチャがあれば(ちゃんとあるよ)、Linqの構文が自動的に使えるようになる。自分の作ったものにもこのトリックを使えるよ(Select、Join、Whereなどのオーバーロードを追加する)もしLinqの構文が好きならね。[1]: https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotn...

もう少し詳しく説明してもらえる?イテレータのパフォーマンス改善については何も見当たらないんだけど。Zlinqはヒープ上でイテレータを割り当ててガベージコレクトされるペナルティを取り除くようだね。君が送ったリンクには改善について触れてるけど、どうやってLINQがヒープ割り当てを避けるのかは分からないな。

Hacker Newsで議論の続きを見る