ねののお庭。

かりかりもふもふ。

【C#】DATAS という GC の機能について。

この記事は C# アドベントカレンダー 2025 24日目の記事です。

はじめに

DATAS は .NET 8 で導入され、.NET 9 からは既定で有効となっている GC の機能です。 .NET 9 は STS (Standard Term Support) でしたが、.NET 10 は LTS (Long Term Support) なので、.NET 10 で初めて DATAS を利用する事になる人々は多いでしょう。

そしてこの DATAS という代物、かなり性能特性に影響します。 DATAS は既定で有効になるため、.NET 8 から .NET 9, 10 に上げて負荷試験などを実施した場合、その性能特性の差を目の当たりにする事が予想されます (単純な負荷試験だと DATAS のメリットが可視化されにくいかもしれませんが...)。 というわけで、ちゃんと DATAS がどのような代物か理解しておく必要があるので、みていきましょう。

DATAS とはいったい何なのか

DATAS は Dynamic Adaptation To Application Sizes の略です。 この DATAS を有効化する事で、アプリケーションの稼働状況に応じて確保するメモリ量を動的に調整してくれるようになります。

...はい、この説明で理解できる人はなかなかいないでしょう。 それ今までの GC でやってきた事と何が違うん、という。

DATAS を理解するには .NET の GC について、ある程度知っておく必要があります。 というわけでここから .NET の GC の基礎知識編に突入します。

Workstation GC と Server GC

.NET の GC には Workstation GC と Server GC という 2 種類のフレーバーが存在します。 そしてそれぞれ異なるワークロードに最適化されています。

  • Workstation GC
    • クライアントアプリ向けに最適化された GC
    • 論理 CPU コアが 1 つなら必ず Workstation GC が用いられる
    • ヒープは必ず 1 つのみ
    • Background GC にタイムアウトあり
      • Foreground GC にはタイムアウトなし
    • 主な目的
      • メモリ消費量を抑える
        • Workstation では様々なアプリケーションが動いている
        • そのため 1 つのアプリケーションがメモリを占有する事は許されない
      • GUI を GC 起因でフリーズさせない
        • そのため 1回あたりの GC 時間を抑えたい
        • 1 発の長い GC ではなく、短い GC を複数回
        • 故に Background GC にタイムアウトが設定されている
  • Server GC
    • サーバアプリ向けに最適化された GC
    • ヒープは論理 CPU コアの数と同数だけ存在する (既定では)
    • Background GC にタイムアウトなし
      • Foreground GC もタイムアウトなし
    • 主な目的
      • スループットの最大化が最大のテーマ
        • メモリの消費量は二の次
        • 頻繁に GC を動かさない
          • これによりスループットの最大化につなげる
          • つまり 1 回あたりの GC 時間は長め
          • Background GC のタイムアウトもない

なるほどね、という感じでしょう。良さそうに見えます。

しかしながら課題が無いわけではありません。そしてそれこそが DATAS が解決したかった課題です。 その課題とはなにかというと、Server GC のメモリ消費に関する事です。

Server GC はスループットの最大化を主目的としています。 そこで Server GC ではヒープを論理 CPU コアの数と同数だけ確保する事を基本的な戦略とし、それにより以下のようなスループット向上に繋がる多くのメリットを得ています。

  • 巨大な1つのヒープに対して mark & sweep する必要がない
    • 各ヒープに対して GC を並列に実行する事ができるため高速
  • GC の発生頻度が少なくなる
    • 何故なら単一のヒープより gen0 の allocation budget が大きくなるため
      • ヒープの数分 gen0 の allocation budget は大きくなると考えれば分かりやすい
    • GC による停止頻度が少ない事はスループットの向上に寄与する
    • 不要な gen1 への昇格やそれに伴うコンパクションなどが発生しないため高速
      • GC 頻度が低ければ、GC が発生した際に GC で回収対象となる参照不能な死んだオブジェクトが多くなる
      • とくにサーバアプリの用途では gen0 から gen1 に昇格すべきオブジェクトは少ない
      • つまり大多数は gen0 のまま回収される事になるので、昇格とそれに伴うコンパクションの発生率が減り、単純にお掃除すればいいだけなので効率的
      • メモリの断片化も防げるため効率的

スループットが高まる要素が満載なわけです。素敵ですね。

しかし裏を返せば、ヒープを論理 CPU コアの数ど同数だけ確保しているという事は、それだけメモリを消費しているということですし、頻繁に GC を発生させていないという事は、すでに参照不可能な死んだオブジェクトをメモリに乗せっぱなしにしている時間が長くなるという事なので、やはりそれだけメモリを消費しているという事です。

要するに、スループットを最大化するために、メモリの消費量が増えているという事です。 そういうトレードオフです。

特にこれはバースト型のワークロードにおいて問題になる事が多いです。 特定の時間にバーストが発生するユースケースは非常に多いでしょう。 そのようなユースケースでは特定の時間に高負荷がかかり、ヒープが拡大し、アプリケーションが確保しているメモリ領域が増大しますが、 負荷がかからなくなったとしてもそのメモリは解放されず、高負荷時を基準としたメモリ利用状況が維持されます。 つまり、確保しているメモリ量がその時のアプリケーションの稼働状況に見合っていないという状況になります。

この課題を解決するのが DATAS です。 DATAS によって、その時のアプリケーションの稼働状況にあったメモリの使用量のみを確保するようになります。 上記のような高負荷から低負荷になった際には、高負荷時のメモリ使用量が維持されるのではなく、低負荷時に見合ったメモリ量を確保するようになります。

なお DATAS は Server GC 向けの機能であり、Workstation GC とは無関係な事に注意してください。

DATAS はどのようにメモリの使用量を調整するか

それでは DATAS はどのようにメモリの使用量を稼働状況に合わせているのか?についてみていきましょう。 ここが気になるポイントであり、面白いお話でもあります。

DATAS を理解するには、以下の 2 つの変数を理解しておくことが重要です。

  • BCD
    • Budget Computed via DATAS
    • DATAS によって計算される gen0 の allocation budget
  • TCP
    • Throughput Cost Percentage
    • GC の停止時間などのオーバヘッドが、GC が停止していな時間に対してどれだけかかっているかの割合

DATAS では TCP の目標値を定め、TCP を一定に保つよう BCD を調整する事によって、メモリの使用量をアプリケーションの稼働状況に合わせたサイズに調整します。 なお TCP の目標値は定数で、既定では 2 % という設定がなされています。

DATAS の挙動を大まかに理解するには具体例が手っ取り早いので、具体例をみていきましょう。

例えばとある WEB アプリケーションが以下の条件で存在していたとしましょう。

  • 初期 BCD
    • 1 GB
  • TCP の目標値
    • 2 %
  • ピーク時
    • 秒間 1 GB の allocation
  • 非ピーク時
    • 秒間 200 MB の allocation
  • GC の停止時間
    • 20 msec
    • 実際にはここは定数になりえないが、問題の単純化のため定数とした

このような条件下では、ピーク時には 1 秒ごとに GC が発火する事になります。 これは BCD として 1 GB が設定されているためです。 そして GC で 20 msec ほど停止してしまった場合、TCP は 2 % となります (= GC の停止時間 (20 msec) / GC が発生するまでの時間 (1000 msec))。 一方で非ピーク時には初期 BCD のままなら 5 秒毎に GC が発火する事になります。 この時 TCP は 0.4 (= GC の停止時間 (20 msec) / GC が発生するまでの時間 (5000 msec)) となり、TCP の目標値 2 % から大きく離れてしまう事になります。 このような場合に DATAS では BCD を 1 GB から 200 MB に調整します。このようにする事で、非ピーク時にも 1 秒毎に GC が走り、TCP が 2 % となります。 当然、BCD が 1 GB から 200 MB に調整されたという事は、大雑把には 800 MB のメモリを解放する事ができるようになるという事です。

このようにする事で、DATAS は「アプリケーションの稼働状況に応じたメモリ量のみ」を確保するようになるのです。 なおヒープ数は DATAS が調整する事になるので、論理 CPU コア数とは一致しなくなります。

DATAS のトレードオフ

何事にもトレードオフは付き物です。 そして DATAS も当然トレードオフが存在します。 Server GC において、DATAS を使った場合、使わなかった場合を比較してみると以下のようになっています。

  • Server GC w/o DATAS
    • pros: スループットの最大化
    • cons: メモリの使用量は大きくなりがち
  • Server GC w/ DATAS
    • pros: アプリケーションの稼働状況に合わせたメモリの使用量
      • バーストが発生するワークロード等においては、省メモリになる
    • cons: スループットは DATAS を利用しない場合に比べて落ちる

つまりスループットの最大化を指向するのであれば DATAS は無効化するべきです。 一方で、省メモリを指向したい場合は DATAS は有効化するべきです。

おわりに

基本的に GC は幅広いワークロード・シナリオに適した設計がなされます。 そのため GC に改善が入っても、明示的なオプションとして現れず、無意識に利用している事が多いです。 しかしながら、DATAS については明確に最適なワークロードとそうでないワークロードが存在します。 そのため、DATAS が自身のワークロードに沿っているかを考え、都度利用するかしないかを判断する必要があります。 個人的には GC のフレーバーが2つから3つになったと思っておくと良いと思います。

  • Workstation GC
  • Server GC
  • Server GC w/ DATAS
    • new !

今までは作成した WEB アプリケーションを省メモリで動かしたい場合、Server GC ではなく Workstation GC を用いるか、細々とした GC のパラメータ (e.g., ヒープ数) を手動でいじって調整する他なかったのですが、DATAS が登場した事によって、Server GC を特段面倒な調整をせずに省メモリに稼働させる事ができるようになりました。 この点については素直に喜んでよいでしょう。

おまけ

Server GC / DATAS を用いるか否かは csproj で <ServerGarbageCollection> / <GarbageCollectionAdaptationMode> を用いる事で設定可能です。 DATAS は既定で有効となっているので、.NET 8 までと同様に .NET 9, 10 でもスループットを優先したい場合は <GarbageCollectionAdaptationMode>0 に設定しましょう。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <ServerGarbageCollection>true</ServerGarbageCollection>
    <GarbageCollectionAdaptationMode>0</GarbageCollectionAdaptationMode>
  </PropertyGroup>

</Project>

References