C# ArrayPoolとは?GC負荷を減らす使い方・注意点・new配列との違いをわかりやすく解説

はじめに

C#で大量のデータ処理やファイル読み込み、ネットワーク通信、シリアライズ処理などを書くとき、一時的な配列を何度も作成する場面があります。

たとえば、次のようなコードです。

C#
for (int i = 0; i < 10000; i++)
{
byte[] buffer = new byte[8192];

// bufferを使った処理
}

このように new byte[] で配列を何度も生成すると、そのたびにメモリ確保が発生します。短時間しか使わない配列でも、使い終わればGCの回収対象になります。

処理回数が少なければ大きな問題になりませんが、高頻度で実行される処理ではGC負荷が増え、パフォーマンス低下につながることがあります。

そこで役立つのが ArrayPool<T> です。

ArrayPool<T> を使うと、配列を毎回 new するのではなく、プールから借りて、使い終わったら返すという形で配列を再利用できます。

この記事では、C#の ArrayPool とは何か、基本的な使い方、new 配列との違い、注意点、実践的なサンプルまでわかりやすく解説します。

1. C# ArrayPoolとは?配列を再利用してGC負荷を減らす仕組み

1-1. ArrayPool<T>の基本概念

ArrayPool<T> は、配列を再利用するための仕組みです。

通常、C#で配列を使う場合は次のように書きます。

C#
byte[] buffer = new byte[1024];

この場合、毎回新しい配列がメモリ上に確保されます。

一方、ArrayPool<T> では次のように配列を借ります。

C#
byte[] buffer = ArrayPool<byte>.Shared.Rent(1024);

使い終わったら、次のように返却します。

C#
ArrayPool<byte>.Shared.Return(buffer);

つまり、ArrayPool<T> は「配列を借りる」「使う」「返す」という流れで使います。

返却された配列は内部のプールに戻り、次回以降の処理で再利用される可能性があります。これにより、毎回新しい配列を確保する回数を減らせます。

1-2. System.Buffers.ArrayPool<T>でできること

ArrayPool<T>System.Buffers 名前空間にあります。

使う場合は、通常次の using を追加します。

C#
using System.Buffers;

ArrayPool<T> で主に使うメソッドは次の2つです。

C#
Rent(int minimumLength)
Return(T[] array)

Rent は、指定した長さ以上の配列を借りるメソッドです。

C#
var array = ArrayPool<int>.Shared.Rent(100);

Return は、借りた配列をプールへ返すメソッドです。

C#
ArrayPool<int>.Shared.Return(array);

また、返却時に配列の中身をクリアすることもできます。

C#
ArrayPool<int>.Shared.Return(array, clearArray: true);

clearArray: true を指定すると、配列を返却する前に要素が初期化されます。パスワード、トークン、個人情報などの機密データを扱う場合に検討します。

1-3. なぜ配列の使い回しがGC対策になるのか

C#の配列は参照型です。

new byte[8192] のように配列を作ると、マネージドヒープ上にオブジェクトとして確保されます。

大量の一時配列を生成すると、短命なオブジェクトが増えます。短命なオブジェクトはGCによって回収されますが、生成数が多いほどGCの負担も増えます。

特に次のような処理では、配列生成が積み重なりやすくなります。

C#
while (true)
{
byte[] buffer = new byte[8192];

// 短時間だけbufferを使う
}

このような処理を ArrayPool に置き換えると、同じようなサイズの配列を再利用できるため、不要なメモリ確保を減らせます。

C#
byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);

try
{
// 短時間だけbufferを使う
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}

GC負荷を完全になくせるわけではありませんが、配列の大量生成が原因でパフォーマンスが悪化している場合には、有効な改善策になります。

1-4. ArrayPoolが向いている処理・向いていない処理

ArrayPool が向いているのは、短時間だけ必要な一時配列を何度も使う処理です。

たとえば、次のようなケースです。

C#
// ファイル読み込み用の一時バッファ
byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);

ファイル処理、ネットワーク処理、圧縮処理、画像処理、シリアライズ処理などでは、一時バッファを頻繁に使うことがあります。このような場面では ArrayPool が効果を発揮しやすいです。

一方で、次のような場合には ArrayPool を使わない方がよいこともあります。

C#
int[] values = new int[3];

小さな配列を少数だけ使う場合、ArrayPool を使ってもメリットはほとんどありません。むしろ、借りる・返す処理が増えてコードが複雑になります。

また、長期間保持する配列にも向いていません。

C#
// 長期間フィールドに保持する用途には不向き
private byte[] _buffer;

ArrayPool は「一時的に借りて、すぐ返す」用途に向いています。

2. C#でArrayPoolを使うメリット

2-1. new配列の大量生成によるメモリ確保を減らせる

ArrayPool の大きなメリットは、配列のメモリ確保を減らせることです。

通常の new 配列では、呼び出すたびに新しい配列が作られます。

C#
byte[] buffer = new byte[8192];

これを何千回、何万回と繰り返すと、その分だけ配列オブジェクトが生成されます。

ArrayPool を使うと、既にプール内にある配列を再利用できる可能性があります。

C#
byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);

必ず既存の配列が返ってくるとは限りませんが、再利用できる場合は新しい配列確保を避けられます。

そのため、短時間に大量の配列を生成する処理では、メモリ確保の回数を抑えやすくなります。

2-2. GCの発生頻度や一時停止時間を抑えやすい

大量の配列を new すると、不要になった配列はGCの回収対象になります。

GCは自動でメモリ管理をしてくれる便利な仕組みですが、実行中にアプリケーションの処理へ影響を与えることがあります。

特に、低レイテンシが求められるアプリケーションでは、GCによる一時停止が問題になることがあります。

ArrayPool によって一時配列の生成数を減らせれば、GC対象となるオブジェクトも減りやすくなります。

結果として、GCの発生頻度やGCに伴う負荷を抑えられる可能性があります。

ただし、ArrayPool を使えば必ず速くなるわけではありません。実際の効果は、配列サイズ、処理頻度、実行環境、GC状況によって変わります。

2-3. 大きな配列や一時バッファで効果が出やすい

ArrayPool は、小さな配列よりも大きめの一時配列で効果が出やすいです。

たとえば、次のようなバッファです。

C#
byte[] buffer = new byte[1024 * 1024];

1MBの配列を頻繁に作ると、メモリ確保やGCの負担が大きくなりやすいです。

このような一時バッファを ArrayPool から借りるようにすると、配列を再利用できる可能性があります。

C#
byte[] buffer = ArrayPool<byte>.Shared.Rent(1024 * 1024);

try
{
// 大きな一時バッファを使う処理
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}

大きな配列を何度も生成している処理では、ArrayPool の導入を検討する価値があります。

2-4. 高頻度処理・ループ処理・I/O処理で有効

ArrayPool は、高頻度で呼ばれる処理と相性がよいです。

たとえば、次のような処理です。

C#
for (int i = 0; i < 100000; i++)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(4096);

try
{
// 繰り返し実行される処理
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}

特に、I/O処理では一時バッファを使う場面が多くあります。

ファイルから読み込む、ネットワークから受信する、ストリームをコピーする、バイト列を変換する、といった処理では byte[] がよく使われます。

このような場面で ArrayPool<byte> を使うと、バッファ用配列の生成を抑えられます。

3. ArrayPoolの基本的な使い方

3-1. ArrayPool<T>.Sharedを使う基本形

通常は ArrayPool<T>.Shared を使います。

C#
using System;
using System.Buffers;

class Program
{
static void Main()
{
int[] array = ArrayPool<int>.Shared.Rent(10);

try
{
for (int i = 0; i < 10; i++)
{
array[i] = i;
}

for (int i = 0; i < 10; i++)
{
Console.WriteLine(array[i]);
}
}
finally
{
ArrayPool<int>.Shared.Return(array);
}
}
}

ArrayPool<T>.Shared は共有プールです。多くの場合、独自のプールを作らずにこれを使えば十分です。

3-2. Rentで配列を借りる

配列を借りるには Rent を使います。

C#
byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);

ここで指定する 8192 は「最低限必要な長さ」です。

重要なのは、返ってくる配列の長さが必ず 8192 とは限らない点です。

C#
byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);

Console.WriteLine(buffer.Length);

この結果は 8192 より大きくなる場合があります。

そのため、処理で使う範囲は自分で管理する必要があります。

C#
int requestedLength = 8192;
byte[] buffer = ArrayPool<byte>.Shared.Rent(requestedLength);

try
{
for (int i = 0; i < requestedLength; i++)
{
buffer[i] = 0;
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}

buffer.Length 全体を無条件に使うのではなく、必要な長さだけを使う意識が重要です。

3-3. Returnで配列を返す

借りた配列は、使い終わったら Return で返します。

C#
ArrayPool<byte>.Shared.Return(buffer);

ArrayPool は借りた配列を自動で返してくれません。

Rent した配列は、開発者が明示的に Return する必要があります。

返却し忘れると、その配列はプールで再利用されません。アプリケーションの実行自体がすぐに壊れるわけではありませんが、ArrayPool を使うメリットが小さくなります。

3-4. try-finallyで確実に返却する書き方

ArrayPool を使うときは、基本的に try-finally とセットで書くのがおすすめです。

C#
byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);

try
{
// bufferを使う処理
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}

処理中に例外が発生した場合でも、finally ブロックは実行されます。

そのため、配列を確実にプールへ返却できます。

悪い例は次のような書き方です。

C#
byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);

// ここで例外が起きるとReturnされない可能性がある
DoSomething(buffer);

ArrayPool<byte>.Shared.Return(buffer);

このコードでは、DoSomething の中で例外が発生すると Return が実行されません。

ArrayPool を安全に使うなら、次のように書きます。

C#
byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);

try
{
DoSomething(buffer);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}

3-5. byte配列を使ったサンプルコード

次は、ArrayPool<byte> を使って一時バッファを借りる基本サンプルです。

C#
using System;
using System.Buffers;
using System.Text;

class Program
{
static void Main()
{
string text = "Hello ArrayPool";
byte[] rented = ArrayPool<byte>.Shared.Rent(1024);

try
{
int byteCount = Encoding.UTF8.GetBytes(
text,
0,
text.Length,
rented,
0);

Console.WriteLine($"変換したバイト数: {byteCount}");

for (int i = 0; i < byteCount; i++)
{
Console.Write($"{rented[i]} ");
}
}
finally
{
ArrayPool<byte>.Shared.Return(rented);
}
}
}

このコードでは、文字列をUTF-8のバイト列に変換するために、一時的な byte[] を借りています。

ポイントは、実際に使った範囲を byteCount で管理していることです。

Rent(1024) で借りた配列の長さが1024より大きい可能性があるため、処理対象は 0 から byteCount - 1 までに限定しています。

4. ArrayPool使用時の重要な注意点

4-1. Rentで指定した長さより大きい配列が返ることがある

ArrayPoolRent に指定する値は、必要な最小長です。

C#
byte[] buffer = ArrayPool<byte>.Shared.Rent(100);

このコードで返ってくる配列の長さは、必ず100とは限りません。

C#
Console.WriteLine(buffer.Length);

100以上の長さを持つ配列が返ってきます。

そのため、次のように buffer.Length 全体を処理対象にすると、意図しない範囲まで処理してしまう可能性があります。

C#
byte[] buffer = ArrayPool<byte>.Shared.Rent(100);

try
{
// 注意:必要な100要素ではなく、配列全体を処理してしまう
for (int i = 0; i < buffer.Length; i++)
{
buffer[i] = 1;
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}

必要な長さを別変数で管理しましょう。

C#
int length = 100;
byte[] buffer = ArrayPool<byte>.Shared.Rent(length);

try
{
for (int i = 0; i < length; i++)
{
buffer[i] = 1;
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}

4-2. 使い終わった配列は必ずReturnする

ArrayPool で借りた配列は、必ず Return します。

C#
byte[] buffer = ArrayPool<byte>.Shared.Rent(1024);

try
{
// bufferを使う
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}

返却しないと、その配列はプールに戻りません。

ArrayPool の目的は配列の再利用です。返却を忘れると再利用できる配列が減り、結果として新しい配列確保が増える可能性があります。

特にループ処理では注意が必要です。

C#
for (int i = 0; i < 10000; i++)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(1024);

// Returnし忘れるとプールに戻らない
}

このようなコードは避け、必ず try-finally を使いましょう。

4-3. Return後の配列を参照・再利用してはいけない

Return した配列は、もう自分のものではありません。

C#
byte[] buffer = ArrayPool<byte>.Shared.Rent(1024);

ArrayPool<byte>.Shared.Return(buffer);

// NG: 返却後の配列を使ってはいけない
buffer[0] = 123;

返却後の配列は、別の処理で再利用される可能性があります。

そのため、返却後に参照したり、値を書き込んだりしてはいけません。

安全のため、返却後に変数へ null を代入することもあります。

C#
byte[]? buffer = ArrayPool<byte>.Shared.Rent(1024);

try
{
// bufferを使う
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
buffer = null;
}

ただし、null を代入しただけで安全になるわけではありません。大切なのは、Return 後にその配列を使わない設計にすることです。

4-4. 配列の中身は初期化されているとは限らない

new で作成した配列は、要素が初期値で埋められます。

C#
int[] array = new int[3];

Console.WriteLine(array[0]); // 0

しかし、ArrayPool から借りた配列は、以前使われた内容が残っている可能性があります。

C#
byte[] buffer = ArrayPool<byte>.Shared.Rent(1024);

try
{
// bufferの中身がすべて0とは限らない
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}

そのため、初期化済みであることを前提にしてはいけません。

必要であれば、使う範囲だけ明示的に初期化します。

C#
int length = 1024;
byte[] buffer = ArrayPool<byte>.Shared.Rent(length);

try
{
Array.Clear(buffer, 0, length);

// 初期化後に使う
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}

4-5. 機密情報を扱う場合はclearArrayを検討する

Return には clearArray 引数があります。

C#
ArrayPool<byte>.Shared.Return(buffer, clearArray: true);

clearArray: true を指定すると、返却時に配列の中身がクリアされます。

パスワード、アクセストークン、秘密鍵、個人情報などを扱う場合は、返却時にクリアすることを検討しましょう。

C#
byte[] buffer = ArrayPool<byte>.Shared.Rent(1024);

try
{
// 機密情報をbufferに格納する処理
}
finally
{
ArrayPool<byte>.Shared.Return(buffer, clearArray: true);
}

ただし、常に clearArray: true にすればよいわけではありません。

配列をクリアする処理にもコストがあります。特に大きな配列では、毎回クリアするとパフォーマンスに影響する可能性があります。

機密情報を扱う場合は安全性を優先し、そうでない一時バッファでは必要性を判断して使い分けます。

4-6. 二重返却・返却忘れ・例外時のリークに注意する

ArrayPool では、次のようなミスに注意が必要です。

まず、返却忘れです。

C#
byte[] buffer = ArrayPool<byte>.Shared.Rent(1024);

// NG: Returnしていない

次に、例外時に返却されないパターンです。

C#
byte[] buffer = ArrayPool<byte>.Shared.Rent(1024);

DoSomething(buffer);

// DoSomethingで例外が起きるとここに到達しない
ArrayPool<byte>.Shared.Return(buffer);

さらに、二重返却にも注意が必要です。

C#
byte[] buffer = ArrayPool<byte>.Shared.Rent(1024);

ArrayPool<byte>.Shared.Return(buffer);

// NG: 同じ配列を二重に返却している
ArrayPool<byte>.Shared.Return(buffer);

二重返却すると、同じ配列が複数回プールに戻ったような状態になり、予期しない不具合につながる可能性があります。

基本形は常に次のようにします。

C#
byte[] buffer = ArrayPool<byte>.Shared.Rent(1024);

try
{
DoSomething(buffer);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}

5. ArrayPoolとnew配列の違い

5-1. new配列は毎回新しい配列を確保する

new 配列は、呼び出すたびに新しい配列を作成します。

C#
byte[] buffer1 = new byte[1024];
byte[] buffer2 = new byte[1024];

buffer1buffer2 は別々の配列です。

new 配列のメリットは、動作がわかりやすいことです。

配列の長さは指定した通りになります。

C#
byte[] buffer = new byte[1024];

Console.WriteLine(buffer.Length); // 1024

また、要素は初期化されています。

C#
Console.WriteLine(buffer[0]); // 0

返却処理も不要です。使い終わった配列は、参照されなくなればGCの回収対象になります。

5-2. ArrayPoolは既存の配列を借りて再利用する

ArrayPool は、配列をプールから借りて使います。

C#
byte[] buffer = ArrayPool<byte>.Shared.Rent(1024);

使い終わったら返します。

C#
ArrayPool<byte>.Shared.Return(buffer);

返却された配列は、別の処理で再利用される可能性があります。

そのため、ArrayPool では配列のライフサイクル管理が重要です。

new 配列では「作って使う」だけで済みますが、ArrayPool では「借りて、使って、返す」必要があります。

5-3. 初期化・サイズ・ライフサイクルの違い

new 配列と ArrayPool の主な違いは、初期化、サイズ、ライフサイクルです。

new 配列は、指定した長さの配列が作られます。

C#
byte[] buffer = new byte[100];

// Lengthは100

一方、ArrayPool は指定した長さ以上の配列を返します。

C#
byte[] buffer = ArrayPool<byte>.Shared.Rent(100);

// Lengthは100以上

new 配列は初期化されていますが、ArrayPool から借りた配列は中身が初期化されているとは限りません。

また、new 配列は返却不要ですが、ArrayPool は必ず返却が必要です。

C#
ArrayPool<byte>.Shared.Return(buffer);

この違いを理解せずに使うと、意図しないデータ混入や返却忘れが発生しやすくなります。

5-4. 可読性と安全性ではnew配列が有利な場面もある

ArrayPool は便利ですが、すべての場面で使うべきではありません。

単純な処理では、new 配列の方が読みやすく安全です。

C#
int[] values = new int[3];

values[0] = 10;
values[1] = 20;
values[2] = 30;

このようなコードに ArrayPool を使うと、かえって複雑になります。

C#
int[] values = ArrayPool<int>.Shared.Rent(3);

try
{
values[0] = 10;
values[1] = 20;
values[2] = 30;
}
finally
{
ArrayPool<int>.Shared.Return(values);
}

短い処理や小さな配列では、new 配列の方が適していることが多いです。

可読性、保守性、安全性を優先するなら、無理に ArrayPool を使う必要はありません。

5-5. パフォーマンス重視ならArrayPoolが有利な場面

一方で、パフォーマンスを重視する処理では ArrayPool が有利になることがあります。

たとえば、次のような処理です。

C#
for (int i = 0; i < 100000; i++)
{
byte[] buffer = new byte[8192];

// 一時的に使う
}

このコードでは、大量の byte[] が生成されます。

ArrayPool を使うと、次のように配列を再利用できます。

C#
for (int i = 0; i < 100000; i++)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);

try
{
// 一時的に使う
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}

高頻度に呼ばれる処理、大きな一時配列を使う処理、GC負荷が問題になっている処理では、ArrayPool の導入を検討する価値があります。

6. ArrayPoolを使うべきケース・使わない方がよいケース

6-1. 使うべきケース:短時間だけ必要な一時バッファ

ArrayPool が最も向いているのは、短時間だけ必要な一時バッファです。

C#
byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);

try
{
// 短時間だけbufferを使う
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}

一時バッファは、処理の途中でだけ必要になり、処理が終われば不要になります。

このような配列を毎回 new するより、ArrayPool で借りて返す方がメモリ確保を減らしやすいです。

6-2. 使うべきケース:大量の配列生成が発生する処理

大量の配列生成が発生する処理も、ArrayPool が向いています。

C#
for (int i = 0; i < 100000; i++)
{
byte[] buffer = new byte[4096];

// 処理
}

このようなコードでは、短時間に多くの配列が作られます。

ArrayPool を使うと、次のように改善できます。

C#
for (int i = 0; i < 100000; i++)
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(4096);

try
{
// 処理
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}

特に、Webアプリケーション、APIサーバー、バッチ処理、ゲーム、リアルタイム処理などでは、一時配列の生成が積み重なることがあります。

6-3. 使うべきケース:ファイル・ネットワーク・シリアライズ処理

ファイル処理やネットワーク処理では、byte[] の一時バッファをよく使います。

C#
byte[] buffer = new byte[8192];

int read = stream.Read(buffer, 0, buffer.Length);

このようなバッファを頻繁に作る場合、ArrayPool<byte> が役立ちます。

C#
byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);

try
{
int read = stream.Read(buffer, 0, 8192);

// 読み込んだデータを処理
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}

また、JSONやMessagePackなどのシリアライズ処理、圧縮・解凍処理、暗号化処理などでも一時バッファが必要になることがあります。

そのような場面では、ArrayPool を使うことでアロケーション削減につながる可能性があります。

6-4. 使わない方がよいケース:小さな配列を少数だけ使う処理

小さな配列を少数だけ使う場合、ArrayPool を使うメリットは小さいです。

C#
int[] values = new int[5];

この程度の配列であれば、new 配列の方がシンプルです。

ArrayPool にすると、次のようにコードが増えます。

C#
int[] values = ArrayPool<int>.Shared.Rent(5);

try
{
// valuesを使う
}
finally
{
ArrayPool<int>.Shared.Return(values);
}

わずかなメモリ確保を避けるために、コードの可読性や安全性を大きく下げるのは得策ではありません。

6-5. 使わない方がよいケース:長期間保持する配列

ArrayPool から借りた配列を長期間保持するのは避けましょう。

C#
public class BufferHolder
{
private byte[] _buffer;

public BufferHolder()
{
_buffer = ArrayPool<byte>.Shared.Rent(8192);
}
}

このように長く保持すると、その配列はプールに戻らず、再利用されません。

ArrayPool は短期間だけ借りて、すぐ返すことで効果を発揮します。

長期間保持する必要がある配列なら、通常の new 配列の方が適している場合があります。

C#
private byte[] _buffer = new byte[8192];

6-6. 使わない方がよいケース:コードの単純さを優先したい処理

パフォーマンスよりもコードの単純さを優先したい処理では、ArrayPool を使わない判断も重要です。

ArrayPool には、次のような注意点があります。

C#
// 必ず返却が必要
ArrayPool<byte>.Shared.Return(buffer);

// 返却後は使ってはいけない
// 中身が初期化されているとは限らない
// 指定より大きい配列が返ることがある

これらを正しく扱うには、コードに注意が必要です。

単純な業務ロジックや、性能上の問題が出ていない処理では、まず new 配列で書く方がよい場合も多いです。

ArrayPool は、必要になったときに導入するパフォーマンス最適化の選択肢として考えるとよいでしょう。

7. ArrayPoolの実装パターンと実践サンプル

7-1. 文字列・バイト列処理での利用例

文字列をバイト列へ変換する処理で、ArrayPool<byte> を使う例です。

C#
using System;
using System.Buffers;
using System.Text;

public static class EncodingSample
{
public static void Run()
{
string text = "C# ArrayPool sample";
Encoding encoding = Encoding.UTF8;

int maxByteCount = encoding.GetMaxByteCount(text.Length);
byte[] buffer = ArrayPool<byte>.Shared.Rent(maxByteCount);

try
{
int byteCount = encoding.GetBytes(
text,
0,
text.Length,
buffer,
0);

ReadOnlySpan<byte> usedBytes = buffer.AsSpan(0, byteCount);

Console.WriteLine($"byte count: {byteCount}");

foreach (byte b in usedBytes)
{
Console.Write($"{b} ");
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
}

この例では、GetMaxByteCount で必要になり得る最大バイト数を計算し、そのサイズ以上の配列を借りています。

実際に使った範囲は byteCount で管理しています。

C#
ReadOnlySpan<byte> usedBytes = buffer.AsSpan(0, byteCount);

このように、ArrayPool を使うときは「借りた配列全体」ではなく「実際に使った範囲」を明確にすることが大切です。

7-2. Stream読み込みでの利用例

Stream からデータを読み込むときの例です。

C#
using System;
using System.Buffers;
using System.IO;

public static class StreamSample
{
public static void Copy(Stream source, Stream destination)
{
const int BufferSize = 8192;

byte[] buffer = ArrayPool<byte>.Shared.Rent(BufferSize);

try
{
int bytesRead;

while ((bytesRead = source.Read(buffer, 0, BufferSize)) > 0)
{
destination.Write(buffer, 0, bytesRead);
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
}

このコードでは、ファイルやネットワークストリームなどをコピーするために、一時バッファを ArrayPool<byte> から借りています。

ポイントは、Read に渡すサイズを BufferSize にしていることです。

C#
source.Read(buffer, 0, BufferSize)

buffer.LengthBufferSize より大きい可能性があります。必要な範囲だけを使うようにします。

非同期処理の場合は、次のように書けます。

C#
using System.Buffers;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

public static class AsyncStreamSample
{
public static async Task CopyAsync(
Stream source,
Stream destination,
CancellationToken cancellationToken = default)
{
const int BufferSize = 8192;

byte[] buffer = ArrayPool<byte>.Shared.Rent(BufferSize);

try
{
int bytesRead;

while ((bytesRead = await source.ReadAsync(
buffer.AsMemory(0, BufferSize),
cancellationToken)) > 0)
{
await destination.WriteAsync(
buffer.AsMemory(0, bytesRead),
cancellationToken);
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
}

ReadAsyncWriteAsync では、Memory<byte> と組み合わせると扱いやすくなります。

7-3. Span<T>やMemory<T>と組み合わせる例

ArrayPool で借りた配列は、Span<T>Memory<T> と組み合わせて使えます。

C#
using System;
using System.Buffers;

public static class SpanSample
{
public static void Run()
{
const int Length = 100;

int[] array = ArrayPool<int>.Shared.Rent(Length);

try
{
Span<int> span = array.AsSpan(0, Length);

for (int i = 0; i < span.Length; i++)
{
span[i] = i * 2;
}

int sum = 0;

foreach (int value in span)
{
sum += value;
}

Console.WriteLine(sum);
}
finally
{
ArrayPool<int>.Shared.Return(array);
}
}
}

Span<T> を使うことで、実際に使う範囲を明確にできます。

C#
Span<int> span = array.AsSpan(0, Length);

これにより、Rent で大きめの配列が返ってきても、必要な範囲だけを安全に扱いやすくなります。

Memory<T> は非同期処理と相性がよいです。

C#
Memory<byte> memory = buffer.AsMemory(0, length);

ただし、Return 後に Span<T>Memory<T> を使ってはいけません。元の配列はすでにプールへ返却されているためです。

7-4. 例外が発生してもReturnする安全な実装例

ArrayPool を使う処理は、例外が発生しても必ず返却されるように書く必要があります。

安全な基本形は次の通りです。

C#
public static void Process()
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(4096);

try
{
// 例外が発生する可能性がある処理
DoWork(buffer, 4096);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}

private static void DoWork(byte[] buffer, int length)
{
for (int i = 0; i < length; i++)
{
buffer[i] = (byte)(i % 256);
}
}

機密情報を含む可能性がある場合は、次のようにします。

C#
public static void ProcessSecret()
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(4096);

try
{
// 機密情報を扱う処理
DoWork(buffer, 4096);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer, clearArray: true);
}
}

try-finallyArrayPool 使用時の基本パターンです。

7-5. ArrayPoolを使った処理のアンチパターン

ArrayPool のアンチパターンとして、まず返却忘れがあります。

C#
public static void Bad()
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(1024);

// Returnしていない
}

次に、返却後に配列を使うパターンです。

C#
public static void Bad()
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(1024);

ArrayPool<byte>.Shared.Return(buffer);

// NG
buffer[0] = 1;
}

また、借りた配列の全体を無条件に処理するのも危険です。

C#
public static void Bad()
{
int requestedLength = 100;
byte[] buffer = ArrayPool<byte>.Shared.Rent(requestedLength);

try
{
// NG: buffer.Lengthは100より大きい可能性がある
for (int i = 0; i < buffer.Length; i++)
{
buffer[i] = 0;
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}

必要な範囲だけを処理しましょう。

C#
public static void Good()
{
int requestedLength = 100;
byte[] buffer = ArrayPool<byte>.Shared.Rent(requestedLength);

try
{
for (int i = 0; i < requestedLength; i++)
{
buffer[i] = 0;
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}

8. ArrayPool.SharedとArrayPool.Createの違い

8-1. 通常はArrayPool<T>.Sharedを使えばよい

ArrayPool を使う場合、基本的には ArrayPool<T>.Shared を使います。

C#
byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);

try
{
// bufferを使う
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}

Shared は共有インスタンスで、多くの一般的な用途に適しています。

独自にプールを作る必要がない場合は、Shared を使うのがシンプルです。

8-2. ArrayPool.Createで独自プールを作る場面

ArrayPool.Create を使うと、独自のプールを作成できます。

C#
ArrayPool<byte> pool = ArrayPool<byte>.Create(
maxArrayLength: 1024 * 1024,
maxArraysPerBucket: 50);

独自プールを作る場面は限られています。

たとえば、特定のコンポーネントだけでプールを分離したい場合や、配列サイズや保持数を制御したい場合です。

C#
public class MyBufferProcessor
{
private readonly ArrayPool<byte> _pool =
ArrayPool<byte>.Create(
maxArrayLength: 1024 * 1024,
maxArraysPerBucket: 20);

public void Process()
{
byte[] buffer = _pool.Rent(8192);

try
{
// 処理
}
finally
{
_pool.Return(buffer);
}
}
}

通常のアプリケーションでは、まず ArrayPool<T>.Shared を検討し、特別な理由がある場合だけ Create を使うとよいでしょう。

8-3. maxArrayLengthとmaxArraysPerBucketの考え方

ArrayPool.Create では、主に次の2つを指定できます。

C#
ArrayPool<T>.Create(int maxArrayLength, int maxArraysPerBucket)

maxArrayLength は、プールが扱う配列の最大長に関する設定です。

C#
ArrayPool<byte> pool = ArrayPool<byte>.Create(
maxArrayLength: 1024 * 1024,
maxArraysPerBucket: 10);

maxArraysPerBucket は、サイズ区分ごとに保持する配列数に関する設定です。

大きくしすぎると、プールが多くの配列を保持し、メモリ使用量が増える可能性があります。

小さくしすぎると、再利用できる配列が少なくなり、ArrayPool の効果が出にくくなる可能性があります。

つまり、独自プールの設定は「メモリ使用量」と「再利用効率」のバランスです。

8-4. 独自プールを使う際の注意点

独自プールを使う場合も、基本的な注意点は Shared と同じです。

借りた配列は必ず返します。

C#
byte[] buffer = pool.Rent(4096);

try
{
// bufferを使う
}
finally
{
pool.Return(buffer);
}

返却後の配列は使いません。

C#
pool.Return(buffer);

// NG
buffer[0] = 1;

また、プールを細かく作りすぎると、かえって管理が複雑になります。

C#
// 必要以上に大量の独自プールを作るのは避ける
ArrayPool<byte> pool1 = ArrayPool<byte>.Create();
ArrayPool<byte> pool2 = ArrayPool<byte>.Create();
ArrayPool<byte> pool3 = ArrayPool<byte>.Create();

独自プールを使う場合は、なぜ Shared ではなく Create が必要なのかを明確にしておきましょう。

9. ArrayPoolで本当に速くなる?パフォーマンス比較の考え方

9-1. new配列とArrayPoolの比較ポイント

ArrayPool はパフォーマンス改善に役立つことがありますが、必ず速くなるわけではありません。

比較するときは、単純な実行時間だけでなく、メモリ確保やGCの状況も見る必要があります。

比較対象は、たとえば次のようになります。

C#
public void UseNewArray()
{
byte[] buffer = new byte[8192];

// 処理
}
C#
public void UseArrayPool()
{
byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);

try
{
// 処理
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}

ArrayPool では RentReturn のコストが増えます。

そのため、配列生成のコスト削減が Rent / Return のコストを上回る場合に効果が出やすいです。

9-2. ベンチマークで見るべき指標

パフォーマンスを比較する場合は、次のような指標を確認します。

C#
// 実行時間
// メモリ割り当て量
// GC回数
// Gen0 / Gen1 / Gen2 の発生状況

単に「実行時間が少し短い」だけで判断するのではなく、メモリ割り当て量がどれだけ減ったかを見ることが重要です。

ArrayPool の主な目的は、配列のアロケーション削減です。

そのため、Allocated が大きく減っているか、GC回数が減っているかを確認します。

9-3. GC Allocated・Gen0/Gen1/Gen2の見方

ベンチマーク結果では、AllocatedGen0Gen1Gen2 といった項目を見ることがあります。

Allocated は、処理中に割り当てられたメモリ量です。

new 配列を大量に使うと、この値が大きくなりやすいです。

C#
byte[] buffer = new byte[8192];

ArrayPool を使うと、既存の配列を再利用できる場合、割り当て量を減らせます。

C#
byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);

Gen0 は短命なオブジェクトの回収に関係します。大量の一時配列を作ると、Gen0 GCが増えることがあります。

Gen1Gen2 は、より長く生き残ったオブジェクトの回収に関係します。

ArrayPool の効果を見るときは、特に AllocatedGen0 の変化が参考になります。

9-4. ArrayPoolで逆に遅くなる可能性があるケース

ArrayPool を使うことで、逆に遅くなるケースもあります。

たとえば、小さな配列を少数だけ使う場合です。

C#
int[] values = new int[4];

このような処理では、new 配列の方が単純で速いことがあります。

また、毎回 clearArray: true を指定して大きな配列を返している場合も注意が必要です。

C#
ArrayPool<byte>.Shared.Return(buffer, clearArray: true);

クリア処理にコストがかかるため、用途によってはパフォーマンス上のメリットが小さくなります。

さらに、ArrayPool を使うことでコードが複雑になり、バグが入りやすくなる可能性もあります。

パフォーマンスが問題になっていない場所に無理に導入する必要はありません。

9-5. 実務では計測して判断することが重要

ArrayPool を使うべきかどうかは、実務では計測して判断するのが基本です。

次のような観点で確認します。

C#
// 配列生成が本当にボトルネックになっているか
// Allocatedが減っているか
// GC回数が減っているか
// 実行時間が改善しているか
// コードの複雑さに見合う効果があるか

ArrayPool は強力な選択肢ですが、最初からすべての配列に使うものではありません。

まずはわかりやすい new 配列で実装し、メモリ確保やGCが問題になったタイミングで ArrayPool を検討するのが現実的です。

10. ArrayPoolに関するよくある質問

10-1. ArrayPoolで借りた配列は自動で返却される?

自動では返却されません。

ArrayPool で借りた配列は、開発者が明示的に Return する必要があります。

C#
byte[] buffer = ArrayPool<byte>.Shared.Rent(1024);

try
{
// bufferを使う
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}

using のように自動で返却される仕組みではありません。

そのため、try-finally を使って確実に返却するのが基本です。

10-2. Returnし忘れるとどうなる?

Return し忘れると、その配列はプールへ戻りません。

すぐにアプリケーションがクラッシュするわけではありませんが、配列が再利用されないため、ArrayPool を使うメリットが失われます。

C#
byte[] buffer = ArrayPool<byte>.Shared.Rent(1024);

// Returnし忘れ

返却忘れが大量に発生すると、結果的に新しい配列確保が増え、メモリ使用量やGC負荷が増える可能性があります。

10-3. Rentした配列の中身は0で初期化される?

初期化されているとは限りません。

new 配列の場合は初期化されます。

C#
byte[] buffer = new byte[1024];
// 中身は0

しかし、ArrayPool から借りた配列は、以前使われた内容が残っている可能性があります。

C#
byte[] buffer = ArrayPool<byte>.Shared.Rent(1024);
// 中身が0とは限らない

必要であれば、自分で初期化します。

C#
Array.Clear(buffer, 0, 1024);

10-4. clearArrayは常にtrueにすべき?

常に true にする必要はありません。

C#
ArrayPool<byte>.Shared.Return(buffer, clearArray: true);

clearArray: true は、配列の中身をクリアしてから返却したい場合に使います。

機密情報を扱う場合は有効です。

C#
ArrayPool<byte>.Shared.Return(buffer, clearArray: true);

しかし、毎回クリアすると処理コストが増えます。

特に大きな配列では、クリア処理がパフォーマンスに影響することがあります。

機密情報や残留データが問題になる場合は true、そうでない一時バッファでは必要に応じて判断するのがよいでしょう。

10-5. List<T>やMemoryPool<T>とは何が違う?

ArrayPool<T> は配列を再利用するための仕組みです。

C#
T[] array = ArrayPool<T>.Shared.Rent(length);

List<T> は、可変長のコレクションです。

C#
List<int> list = new List<int>();
list.Add(1);
list.Add(2);

List<T> は要素を追加・削除しやすい一方で、内部的には配列を持っています。配列そのものを一時バッファとして使いたい場合は、ArrayPool<T> の方が直接的です。

MemoryPool<T> は、Memory<T> をベースにしたメモリプールです。

C#
using MemoryPool<byte> pool = MemoryPool<byte>.Shared;

ArrayPool<T>T[] を借りる仕組み、MemoryPool<T>IMemoryOwner<T> を通じてメモリを借りる仕組み、と考えるとわかりやすいです。

単純に配列を再利用したい場合は ArrayPool<T>Memory<T> ベースの所有権管理を使いたい場合は MemoryPool<T> を検討します。

10-6. マルチスレッドで使っても安全?

ArrayPool<T>.Shared は、複数スレッドから利用できます。

そのため、複数の処理が同時に RentReturn を呼び出すことは可能です。

C#
byte[] buffer = ArrayPool<byte>.Shared.Rent(1024);

try
{
// スレッドごとに借りたbufferを使う
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}

ただし、借りた同じ配列を複数スレッドで同時に操作する場合は別問題です。

C#
// 同じbufferを複数スレッドで同時に書き換える場合は注意

ArrayPool 自体がスレッドセーフでも、借りた配列の中身に対する操作は自分で安全に管理する必要があります。

複数スレッドで使う場合は、スレッドごとに別々の配列を借りる、共有する場合は同期制御を行う、といった設計が必要です。

まとめ

C#の ArrayPool は、配列をプールから借りて再利用するための仕組みです。

通常の new 配列は毎回新しい配列を確保しますが、ArrayPool<T> を使うと既存の配列を再利用できる可能性があります。

基本的な使い方は、次の流れです。

C#
byte[] buffer = ArrayPool<byte>.Shared.Rent(8192);

try
{
// bufferを使う処理
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}

ArrayPool を使うメリットは、配列の大量生成を抑え、メモリ確保やGC負荷を減らしやすいことです。

特に、ファイル処理、ネットワーク処理、シリアライズ処理、高頻度のループ処理、大きな一時バッファを使う処理では効果が出やすいです。

一方で、注意点もあります。

C#
// 指定した長さより大きい配列が返ることがある
// 中身が初期化されているとは限らない
// 必ずReturnする必要がある
// Return後に使ってはいけない
// 機密情報を扱う場合はclearArrayを検討する

小さな配列を少数だけ使う処理や、コードの単純さを優先したい処理では、new 配列の方が適している場合もあります。

ArrayPool は、すべての配列に使うものではなく、パフォーマンス改善が必要な場面で効果を発揮する選択肢です。

C#でGC負荷やメモリ割り当てを減らしたい場合は、まずボトルネックを計測し、配列生成が問題になっている箇所に ArrayPool<T> を導入するとよいでしょう。