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

C#における安全なゼロコピー操作

2025年9月30日原文(ssg.dev)

概要

  • C# は多用途な言語であり、低レベルから高レベルまで幅広い開発が可能
  • 配列アクセスの境界チェック がパフォーマンスに影響を与える場合がある
  • unsafeコード やポインタ演算で高速化できるが、安全性に課題
  • Span<T>やReadOnlySpan<T> の導入で、安全かつ高速なゼロコピー操作が実現
  • .NETランタイム でもSpanを活用した新しいAPIが増加

C#の多様性と低レベル最適化

  • C#モバイルアプリデスクトップアプリゲームWebサイトサービス/API の開発に対応
  • Java風 の抽象化豊富な設計も可能だが、 unsafeコードポインタ による低レベル制御もサポート
  • パフォーマンス向上Cライブラリとの連携 には GCを介さない生ポインタ 利用が有効

配列アクセスと境界チェック

  • C#の配列アクセス は安全性のため 境界チェック が自動で行われる
  • forループ でインデックスが明確な場合、 コンパイラが境界チェックを省略 できる最適化
  • 動的なインデックス や外部から渡された範囲の場合、 境界チェックが残りパフォーマンス低下

unsafeコードのメリット・デメリット

  • unsafe関数ポインタ演算 で境界チェックを完全排除し、 高速なコード生成 が可能
  • 不正なlength値 で例外ではなく クラッシュや誤動作バッファオーバーフロー脆弱性 のリスク
  • unsafe関数の呼び出し元もunsafe指定 が必要で、安全性確保が困難

Span<T>による安全かつ高速な配列操作

  • 配列の部分範囲と実体の整合性 を保つために Span<T> を導入
    • readonly struct Span<T>
      • T*ポインタ長さ情報 を保持するイミュータブル型
  • Span<T>ref struct として宣言され、 スタック上のみ存続 しヒープに逃げない
    • ガーベジコレクション による解放リスクを排除
    • 他のref struct 内でのみ保持可能、 ボックス化不可
  • Span<T> を使った関数は 境界チェック不要 で高速化、部分配列も安全に受け渡し可能

ReadOnlySpan<T>の利点

  • ReadOnlySpan<T> で受け取れば 不変保証、内容の変更をコンパイラが防止
  • readonly配列 では参照自体しか守れないが、 ReadOnlySpan<T> は内容自体も保護

ゼロコピーAPIと実用例

Quicksortの比較

  • 従来のQuicksort配列と範囲インデックス で部分配列を表現し、 境界チェックやオーバーフロー のリスク
  • Span<T>を使うと範囲演算子(..) で部分配列を安全かつ直感的に表現
    • (low + high) / 2 のようなオーバーフローも発生しにくい
    • 境界チェック不要 でunsafe並みのパフォーマンス
    • コードの可読性・表現力向上

.NETランタイムのゼロコピーAPI

  • .NET では Span<T>ReadOnlySpan<T> 対応のAPIが増加
  • String.Split のようなAPIでも 新たなバッファ生成を避けて メモリ効率向上
    • 例: ReadOnlySpan<char> でCSV行を分割・処理することでGC負荷減少

まとめ

  • C#高レベル抽象化 から 低レベル最適化 まで柔軟に対応
  • unsafeコード はパフォーマンスに優れるが、安全性に難
  • Span<T>ReadOnlySpan<T> の導入で、安全かつ高速な ゼロコピー操作 が容易に
  • .NETランタイム もSpanベースのAPIを拡充し、現代的なパフォーマンスと安全性を両立

Hackerたちの意見

スパンやスライスのような構造は、現代のプログラミング言語における安全なメモリ操作の未来だ。受け入れていこう。俺はスパンをめっちゃ使ってるけど、同じ物理メモリに対して論理的なビューが必要な場合に大きな違いが出る。可能な限り、俺のコードは配列の代わりにスパンを使うように書き直した。ToArray()や、それを使うことを暗示するコードを見落としがちだけど、大きなコードベースでは特に注意が必要だよ。小さな、たまにあるアロケーションでも、作業セットを快適な場所から移動させてGCを動かす原因になることがある。パフォーマンスの違いは、場合によっては理不尽なこともある。こんなこともできる: var arena = stackalloc byte[1024]; var segment0 = arena.Slice(10); var segment1 = arena.Slice(10, 200); ... 上記のコードは、GCの圧力やアクティビティを全く引き起こさない。すべてスタック上で処理される。

こういう記事は本当にありがたい。俺はUmbraco CMSを使っていて、全体のシステムを動かすために推奨要件よりも低いコードを書いている。まだスパンを使う必要性は感じていないけど、膨大なコンテンツを持つウェブサイトには役立つかもしれないと思ってる。今は、ビュー用に作成するモデルに「public readonly record struct」を使うことを検討中。もちろん、標準クラスのreadonlyプロパティと比べてパフォーマンスをプロファイリングする必要があるけど、俺のコードのほとんどはCMSからクラスを水分補給するために短命だから、どれくらいのメリットがあるかはわからない。幸運なことに、主要なプロジェクトの合間にパフォーマンスを最大限に引き出す作業ができる立場にいる。誰か、.NET CMSでスパンや「public readonly record struct」を使って真剣なパフォーマンス向上を見つけた人いる?ページが通常「ファイア&フォゲット」だから、パフォーマンスを引き出すのが難しい。2013年からずっと、コードからパフォーマンスを引き出すために努力してきたけど、いくつかの小さなビジネスと一緒に仕事をしていて、チームの他のメンバーもWixやSquarespaceを検討し始めている。サイトを立ち上げるのに「俺」が関与しなくてもいいからね。俺の知る限り、セキュリティ侵害には遭ったことがないし、ログを読んだりコードを常に見直したりしているのが好きなんだ(少なくともUmbraco CMSの範囲内では、他にも知識があるけど)。2013年以前はPHPとCodeIgniterで働いていて(その後、PHPから.NETに移行する際に少しKohanaも使った)。C#が好きで、かなりのパフォーマンスを引き出せると感じているけど、もっと価値を生み出す方法についてアイデアがあれば、めっちゃ興味ある。

.NET CMSでスパンや「public readonly record struct」を使って真剣なパフォーマンス向上を見つけた人いる? この返答は「.NET CMS」の部分には直接答えてないよ。最適化について考えるべきタイミングを伝えたかっただけ。こういうマイクロ最適化は、特定のパフォーマンス問題を解決しようとしているときに考えるのがベストだよ。特に、あまりアクセスがないサイトを扱っているときはね。俺は、各ページの読み込みに5秒かかる小規模ビジネスのeコマースサイトを使ったことがあって、何かを買うのを諦めたことがある。その場合、サイトをプロファイリングして問題を特定するのは非常に価値がある。アクセスが多いサイトでは、こういうパフォーマンス最適化がコストを節約するのに役立つことがある。もしサービスを運営するのに100台のサーバーが必要で、パフォーマンスを調整して75台に減らせるなら、それはエンジニアリングの努力に見合うかもしれない。俺のおすすめは、何らかのプロファイラーを使うこと。アプリケーション全体でホットスポットを特定して、特定のパフォーマンス問題の原因を探るのがいい。ホットスポットを特定したら、BenchmarkDotNetで問題のマイクロベンチマークを作成して、スパンのようなツールを使って問題を解決してみて。

CMSや似たような状況では、スパンよりも高レベルの変更から大きなパフォーマンス向上が得られるよ。HTTPキャッシュコントロールヘッダーを正しく使ってCDNと組み合わせることで、桁違いの改善が得られる。より効率的なレイアウトテンプレートを使ってHTML/CSS/JSを減らすだけでも、サイト全体に乗数効果をもたらすことができる。俺の経験では、最大の成果はブラウザのF12ツールのネットワークタブを使うことで得られた。次に大きかったのは、プロダクションで動作しているAzure Application Insightsプロファイラー。最もコストのかかるデータベースクエリのトップ10を見て、それを徹底的に調整するのが重要。スパンのようなものは、共有ライブラリの作者にとっては重要だけど、「エンドユーザー」がウェブアプリを書く場合にはそれほどでもない。そういえば、NuGetパッケージのバージョンや.NETフレームワークのバージョンを9や10に更新するだけで、その使用を増やせるよ。これで、ほんの少しの努力で何千ものマイクロ最適化が得られる!

CMSの場合、主要なボトルネックはDBクエリにあることが多いと思うよ。特にC#みたいにデフォルトで結構速い言語の場合はね。こういう低レベルの最適化に行く前に、ちゃんと測定する必要があるよ。多分、この場合はフレームワークやCMSにオーバーヘッドがあって、どう使うかを理解することで一番改善できると思う。スパンは、低レベルのライブラリコードを書くときに注意すべき最適化だね。

普通、CMSのパフォーマンス問題はデータベースやレンダリングコンポーネントの使い方、キャッシュの仕方に関連してることが多いよ。私が経験した2つの.NET CMS、SitecoreとOptimizelyでは、スパンみたいなものはほとんど改善には繋がらないと思う。むしろ、ORMの使い方を見直したり、直接SQLを使ったり、レンダリングを別の方法でキャッシュしたり、CMSのAPIが正しく使われているかを確認する方が重要だよ。

他の人も言ってるけど、CMSのようなプロジェクトではボトルネックはデータベースやキャッシュにあることが多いよ。スパンやスタックアロック、値構造体は、画像処理やゲーム、"AI"/ベクトルクエリ、データベースエンジンの実装みたいな重いデータや数値を扱うシナリオを書くときに重要になるね(昨日のC++を使うって発表した人たちの話も見てみて、RustやGo、Erlang、Java、C#との比較がされてたよ)。私はCMSのワークロードを思い出させるアプリケーションを書くことが多いけど、構造体を使うこともあるけど、過去6年間で低レベルの最適化スキルを本格的に使ったのは数回だけで、95%の時間はDBの使い方が悪いことが原因だね。

誰か、.NETのCMSでSpanや「public readonly record struct」を使って、真剣なパフォーマンス向上を見つけた人いる?ページは通常、ファイアアンドフォゲットだから。Spanの利点は、.NETのアップグレードに合わせて得られるものが多いよ。Spanは低レベルの最適化で、ASP.NETの内部にはユーザーコードよりもずっと効果がある。Spanが追加されて以来、各バージョンの.NETはその使い方を改善してきたし。さらにC#では、コンパイラは意味があるときにSpanのオーバーロードを優先するから、最新の.NETに再ビルドするだけでその利点を享受できる。これが「真剣な」利点かどうかは好みの問題でもあるし、あなたのコードが常に低レベルのことをしているわけではないってことを思い出させてくれる。例えば、データベースのクエリ時間は一般的にもっと大きな影響を与えるしね。 「public readonly record struct」については、注意が必要だよ。デフォルトでその冗長なバージョンを使い始めるコードベースをいくつか見たけど、期待以上にひどいパフォーマンスの落ち込みを招いてしまった。なぜ「record」がデフォルトでクラスになっているのか、構造体の動作を選択する必要があるのかには、たくさんの理由がある。クラスの方が理解しやすいし、パフォーマンスのトレードオフも把握しやすい。GCは友達であって敵じゃない、特に短命のデータに対してはね。(Gen0のコレクションはしばしば非常に速い。ナースリーはファイアアンドフォゲットのデータの回転のために設計されている。世代型ガベージコレクターの仕事は、回転と安定を分け、回転を素早く処理して安定を保つことだから。)構造体は値渡しだから、同じスタックに留まる「ラッキーパス」を出ると、どんどんコピーされちゃう。モデルに他の構造体がたくさん含まれていると、メモリをもっと頻繁にコピーすることになる。構造体が特定のスタックフレームの制限を超えると、結局GCヒープにボックス化されて、ヒープのアロケーションを節約できない。クラスは参照渡し。構造体を使って不変データモデルを作るために「readonly」を使っているなら、すべてのコピーが不変データの変更から積み重なって新しい構造体を作ることになる。一方で、「通常の」不変レコード(クラス)は、互いに参照を共有できるから、変更しない部分は同じ参照を使って同じメモリを共有できる。モデルが整数が数個以上で、何らかのネストや複雑な関係を持っているなら、「public readonly record struct」は早すぎる最適化で、実際にはパフォーマンスを損なうことになる。すべてのデータをスタックに移せるわけじゃないし、移すべきでもない。トレードオフがあることを忘れないで、健全なパフォーマンスの.NETアプリケーションは、スタックとGCの賢い組み合わせを使うことが一般的だから、どちらも重要なツールなんだ。言った通り、「public record」がデフォルトで「クラス」になっている理由や、「public readonly record struct」が冗長なオプトインである理由を覚えておくのは役に立つよ。

C#の開発者として、スパンが大好き。Rustとは同じ領域ではないのは理解してるけど、Rustが提供するパワーとどれくらい比較できるのかな?

これは好みの問題だね。C#はより一般的なパターンを許可するけど、Rustはもっと多くのことを許可する。例えば、RustではSpan(Rustではスライスと呼ばれる)をどこにでも保存することができる(ヒープ上でも、ただしこれは稀)。

主な違いはこれだと思う。C#はGCがあって、システムは使用中のメモリを保護するけど、スパンやメモリなどを許可する一方で、ライフタイムに関する考慮が雑なためにいくつかの制約がある。でも一般的には「簡単」に使えるから、ライフタイムを気にしなくていい。Rustはライフタイムのセマンティクスが言語のコアに組み込まれていて、コンパイラは安全なものについてずっとよく知っているけど、証明できないけど安全なものからは早めに禁止される(でも経験に基づいてチェックを改善している)。その知識のおかげで、アロケーションをより正確に扱えるようになるんだ。個人的にはアセンブリやC、C++などのバックグラウンドがあるから、Rustの魅力は理解できるし、パフォーマンスが必要な経験の浅い開発者がクリティカルなコンポーネントにRustを選ぶのはプラスだと思う。自分もRustでプロジェクトをやってみようと思ってるけど、今のところRustのわずかなパフォーマンス向上が、C#が許す「ちょっと雑な」接続ができることから得られる「生産性」の向上を上回るプロジェクトは見たことがないな。

これが役に立つかも: 「Rustの借用チェッカーとC#の比較」 https://em-tg.github.io/csborrow/

Hacker Newsで議論の続きを見る