C#の速度を徹底改善|遅い原因と高速化テクニックを実例で解説
はじめに
C#の速度が遅いと感じたとき、最初に疑うべきなのは「C#という言語そのもの」ではなく、ビルド設定、実行環境、メモリ確保、データ構造、DBアクセス、I/O待ち、LINQの使い方、非同期処理の設計などです。
C#は.NETランタイム上で動作するため、JITコンパイル、GC、最適化コンパイラ、ランタイム設定の影響を受けます。一方で、これらの仕組みを理解して正しく使えば、業務アプリ、Web API、バッチ処理、デスクトップアプリ、Unity開発など幅広い場面で十分に高速な処理を実現できます。
重要なのは、いきなり細かいテクニックに飛びつかないことです。C#の速度改善では、まず「どこが遅いのか」を測定し、CPU処理、メモリ、GC、DB、ネットワーク、ファイルI/Oのどこに原因があるかを切り分ける必要があります。そのうえで、効果の大きい箇所から順番に最適化していくことが、最短で成果を出す方法です。
この記事では、C#の速度が遅くなる原因から、計測方法、すぐ使える高速化テクニック、LINQやコレクション処理、GC対策、非同期・並列処理、DB・API通信、.NETランタイム設定まで、実例コードを交えて解説します。
1. C#の速度が遅いと感じる主な原因
C#の速度低下には、いくつかの典型的な原因があります。原因を正しく見分けないまま最適化すると、効果が小さい箇所に時間を使ってしまったり、コードの可読性だけを悪化させたりすることがあります。
1-1. C#自体が遅いのか、書き方・環境が原因なのか
「C#は遅い」と言われることがありますが、多くの場合、問題は言語そのものではなく、コードの書き方や実行環境にあります。
たとえば、次のようなケースです。
C#var result = users.Where(u => u.IsActive)
.Select(u => u.Name)
.ToList();
このようなLINQは読みやすく便利ですが、大量データを高頻度で処理する場面では、手書きループよりオーバーヘッドが出る場合があります。ただし、すべてのLINQが悪いわけではありません。問題は「使う場所」と「データ量」です。
また、Debugビルドで実行していたり、古い.NET Frameworkを使っていたり、DBクエリが非効率だったりするだけで、C#の処理全体が遅く見えることもあります。まずはC#の計算処理が遅いのか、外部アクセスが遅いのかを分けて考えることが大切です。
1-2. DebugビルドとReleaseビルドの速度差
C#の速度を確認するときは、必ずReleaseビルドで測定します。Debugビルドはデバッグしやすさを優先するため、最適化が弱く、実行速度もReleaseビルドより遅くなりやすいです。
特にVisual StudioでF5実行している場合、デバッガが接続されているため、通常実行より遅く見えることがあります。速度を比較するときは、次の条件をそろえます。
Bashdotnet run -c Release
または、ビルド済みのReleaseバイナリを直接実行します。
Bashdotnet publish -c Release
小さな処理では差が見えにくい場合もありますが、ループ回数が多い処理、数値計算、大量データ処理ではDebugとReleaseの差が大きくなることがあります。
1-3. JITコンパイル・初回実行時の遅延
C#は通常、ILと呼ばれる中間言語にコンパイルされ、実行時にJITコンパイラがネイティブコードへ変換します。そのため、アプリ起動直後やメソッドの初回実行時に遅延が発生することがあります。
たとえば、初回のAPIリクエストだけ遅い、アプリの起動直後だけ処理が重い、といったケースではJITや初期化処理が関係している可能性があります。
.NETには、起動時間を改善するためのReadyToRunやNative AOTといった選択肢もあります。ReadyToRunは事前コンパイルによってJITの作業量を減らし、起動時のパフォーマンス改善に役立つ仕組みです。Native AOTはアプリをネイティブコードとして発行し、起動時間やメモリ使用量の改善に効果がある一方、リフレクションなど一部の機能に制約が出る場合があります。Microsoft Learn+1
1-4. メモリ確保とGCによるパフォーマンス低下
C#では不要になったオブジェクトをGCが自動的に回収します。これは開発効率を高める大きなメリットですが、短時間に大量のオブジェクトを生成するとGCが頻繁に発生し、速度低下につながります。
たとえば、ループ内で文字列を何度も結合したり、一時配列を大量に作ったり、LINQで中間コレクションを何度も生成したりすると、GC負荷が増えます。
C#for (int i = 0; i < 100000; i++)
{
var temp = new byte[1024];
// 一時処理
}
このような一時オブジェクトが大量に発生する処理では、ArrayPool<T>やSpan<T>、バッファの再利用を検討すると効果が出ることがあります。
1-5. LINQ・foreach・例外処理など書き方による遅さ
C#では、読みやすい書き方が必ずしも最速とは限りません。特に速度に影響しやすいのは次のような書き方です。
C#var names = users
.Where(u => u.IsActive)
.Select(u => u.Name)
.Where(name => name.StartsWith("A"))
.ToList();
このようなLINQチェーンは便利ですが、大量データを何度も処理する場合、ラムダ式、イテレータ、中間処理のオーバーヘッドが無視できなくなることがあります。
また、例外処理を通常の分岐として使うのも避けるべきです。
C#try
{
int value = int.Parse(text);
}
catch
{
// 数値でない場合
}
このような処理は、次のようにTryParseを使う方が適切です。
C#if (int.TryParse(text, out int value))
{
// 数値の場合
}
例外は本当に例外的な状況で使うものであり、通常発生する分岐に使うと速度面でも設計面でも不利になります。
1-6. I/O・DB・ネットワーク待ちをCPU処理の遅さと誤解するケース
C#アプリが遅い原因は、CPU処理ではなく外部アクセスにあることがよくあります。
たとえば、次のような処理です。
C#var user = await db.Users.FindAsync(id);
var response = await httpClient.GetAsync(url);
var text = await File.ReadAllTextAsync(path);
DB、API、ファイル、ネットワークは、CPU計算よりも待ち時間が大きくなりやすい領域です。この場合、C#の計算処理を細かく最適化しても、体感速度はほとんど変わりません。
APIレスポンスが遅いなら非同期化や並行実行、DBが遅いならSQLやインデックス、ファイルが遅いならバッファリングや非同期I/Oを見直す必要があります。
2. C#の速度改善で最初にやるべき計測と特定
C#の速度改善では、最初に計測することが最も重要です。推測で最適化すると、効果がないどころか、保守しにくいコードを増やすだけになることがあります。
2-1. 推測で最適化せずボトルネックを測定する重要性
パフォーマンス改善では、「遅そうなコード」と「本当に遅いコード」は一致しないことがあります。
たとえば、見た目には複雑な計算処理よりも、1回のDBクエリの方が圧倒的に遅いことがあります。また、細かいLINQの置き換えより、N+1問題を解消する方が大きな効果を出すこともあります。
最適化の基本は、次の順番です。
遅い処理を測定する
ボトルネックを特定する
変更する
改善後を再測定する
可読性と副作用を確認する
この流れを守るだけで、無駄な高速化作業を大きく減らせます。
2-2. Stopwatchを使った簡易的な処理時間の測定
簡単な処理時間の測定にはStopwatchが使えます。
C#using System.Diagnostics;
var sw = Stopwatch.StartNew();
DoSomething();
sw.Stop();
Console.WriteLine($"処理時間: {sw.ElapsedMilliseconds} ms");
複数回実行して平均を取る場合は、次のようにします。
C#var sw = Stopwatch.StartNew();
for (int i = 0; i < 1000; i++)
{
DoSomething();
}
sw.Stop();
Console.WriteLine($"合計: {sw.ElapsedMilliseconds} ms");
Console.WriteLine($"平均: {sw.Elapsed.TotalMilliseconds / 1000} ms");
ただし、Stopwatchは簡易的な測定には便利ですが、JIT、GC、ウォームアップ、CPU状態などの影響を受けます。正確な比較にはBenchmarkDotNetを使うのがおすすめです。
2-3. BenchmarkDotNetで正確にベンチマークする方法
BenchmarkDotNetは、C#や.NETのパフォーマンス測定でよく使われるベンチマークライブラリです。ウォームアップ、複数回実行、統計情報の出力などを自動化できるため、処理速度の比較に向いています。BenchmarkDotNet公式サイトでも、メソッドをベンチマーク化し、再現性のある測定結果を共有できるツールとして説明されています。BenchmarkDotNet
インストールします。
Bashdotnet add package BenchmarkDotNet
サンプルコードです。
C#using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
BenchmarkRunner.Run<StringBenchmarks>();
public class StringBenchmarks
{
[Benchmark]
public string Plus()
{
var result = "";
for (int i = 0; i < 1000; i++)
{
result += i.ToString();
}
return result;
}
[Benchmark]
public string Builder()
{
var sb = new System.Text.StringBuilder();
for (int i = 0; i < 1000; i++)
{
sb.Append(i);
}
return sb.ToString();
}
}
このように比較対象を並べることで、改善前後の速度差を客観的に確認できます。
2-4. Visual Studio ProfilerでCPU・メモリ・GCを確認する
Visual Studioにはプロファイラーがあり、CPU使用率、メモリ使用量、GC、ホットパスなどを確認できます。特定のメソッドに時間が集中しているのか、オブジェクト生成が多いのか、GCが頻繁に発生しているのかを視覚的に把握できます。
特に次のような問題の特定に役立ちます。
C#// CPU負荷が高い処理
CalculateLargeData();
// メモリ確保が多い処理
CreateManyObjects();
// GCが発生しやすい処理
ProcessLargeTemporaryArrays();
Stopwatchでは「遅い」ことは分かっても、「なぜ遅いか」までは分からないことがあります。プロファイラーを使うと、CPU、メモリ、I/Oのどこが問題かを深掘りできます。
2-5. ログ・APM・トレースで本番環境の遅い処理を特定する
ローカル環境では速いのに、本番環境では遅いというケースもあります。原因としては、DBのデータ量、ネットワーク遅延、サーバースペック、同時アクセス数、外部APIの応答時間などが考えられます。
本番環境では、ログ、APM、分散トレースを活用します。
C#logger.LogInformation("User search started. Keyword={Keyword}", keyword);
var sw = Stopwatch.StartNew();
var result = await SearchUsersAsync(keyword);
sw.Stop();
logger.LogInformation("User search finished. Elapsed={Elapsed}ms", sw.ElapsedMilliseconds);
Web APIであれば、リクエスト単位で処理時間を記録し、どのエンドポイントが遅いかを把握します。DBクエリ時間、外部API時間、アプリ内部処理時間を分けて記録すると、原因を切り分けやすくなります。
2-6. 高速化すべき処理と最適化不要な処理の見極め方
すべてのコードを高速化する必要はありません。高速化すべきなのは、実際にユーザー体験やシステム負荷に影響している処理です。
たとえば、1日に1回しか実行されない管理バッチの処理を100ms短縮するより、毎秒何百回も呼ばれるAPIを10ms短縮する方が効果的です。
優先度が高いのは、次のような処理です。
呼び出し回数が多い処理
ユーザーの待ち時間に直結する処理
大量データを扱う処理
課金やインフラコストに影響する処理
タイムアウトや障害につながる処理
逆に、滅多に実行されない処理や、すでに十分速い処理を無理に最適化する必要はありません。
3. すぐ効くC#高速化テクニック
ここからは、C#の速度改善で実際に効果が出やすいテクニックを紹介します。すべての場面で必要なわけではありませんが、ボトルネックになっている箇所に適用すると大きな改善につながります。
3-1. Releaseビルド・最適化オプションを有効にする
まず確認すべきなのは、Releaseビルドで実行しているかです。
Bashdotnet build -c Release
dotnet run -c Release
プロジェクトファイルでは、次のように最適化が有効になります。
XML<PropertyGroup Condition="'$(Configuration)'=='Release'">
<Optimize>true</Optimize>
</PropertyGroup>
また、アプリの配布時には次のように発行します。
Bashdotnet publish -c Release
Debugビルドで速度を評価してしまうと、実際より遅い結果になります。特にベンチマークや性能試験ではReleaseビルドを必ず使いましょう。
3-2. 不要なオブジェクト生成を減らす
C#ではオブジェクト生成が簡単ですが、ループ内で大量に生成するとGC負荷が増えます。
悪い例です。
C#for (int i = 0; i < items.Count; i++)
{
var options = new ProcessOptions();
Process(items[i], options);
}
毎回同じ設定でよいなら、ループ外で生成します。
C#var options = new ProcessOptions();
for (int i = 0; i < items.Count; i++)
{
Process(items[i], options);
}
また、一時的なListを何度も作る処理も注意が必要です。
C#foreach (var group in groups)
{
var activeItems = group.Items.Where(x => x.IsActive).ToList();
Process(activeItems);
}
必要がなければToListせず、列挙のまま処理する、またはループで直接処理する方が効率的です。
3-3. 文字列結合はStringBuilderやstring.Createを使う
文字列は不変です。そのため、ループ内で+=を繰り返すと、新しい文字列が何度も生成されます。
悪い例です。
C#var text = "";
for (int i = 0; i < 10000; i++)
{
text += i + ",";
}
改善例です。
C#var sb = new StringBuilder();
for (int i = 0; i < 10000; i++)
{
sb.Append(i);
sb.Append(',');
}
var text = sb.ToString();
短い文字列を数回結合する程度なら+でも問題ありません。しかし、ループ内や大量データではStringBuilderを使う方が安定して高速です。
固定長の文字列を効率よく作りたい場合は、string.Createも選択肢になります。
C#string result = string.Create(5, 123, (span, value) =>
{
value.TryFormat(span, out _);
});
ただし、string.Createはやや高度な書き方になるため、可読性を重視する場面ではStringBuilderの方が扱いやすいです。
3-4. ループ内の無駄な処理・重複計算を削減する
ループ内で同じ計算やプロパティアクセスを繰り返している場合は、外に出せることがあります。
悪い例です。
C#for (int i = 0; i < items.Count; i++)
{
if (DateTime.Now.DayOfWeek == DayOfWeek.Monday)
{
Process(items[i]);
}
}
改善例です。
C#bool isMonday = DateTime.Now.DayOfWeek == DayOfWeek.Monday;
for (int i = 0; i < items.Count; i++)
{
if (isMonday)
{
Process(items[i]);
}
}
小さな改善に見えますが、ループ回数が多い場合や、処理内で重い計算をしている場合は効果があります。
3-5. コレクションの初期容量を指定して再確保を防ぐ
ListやDictionaryは要素が増えると内部配列を拡張します。大量の要素を追加することが分かっている場合は、初期容量を指定すると再確保の回数を減らせます。
C#var list = new List<int>(100000);
for (int i = 0; i < 100000; i++)
{
list.Add(i);
}
Dictionaryも同様です。
C#var map = new Dictionary<int, User>(users.Count);
foreach (var user in users)
{
map[user.Id] = user;
}
大量データ処理では、初期容量の指定だけでメモリ確保とコピー回数を減らせることがあります。
3-6. 例外を通常の分岐処理として使わない
例外処理はコストが高いため、通常の分岐に使うべきではありません。
悪い例です。
C#public bool IsNumber(string text)
{
try
{
int.Parse(text);
return true;
}
catch
{
return false;
}
}
改善例です。
C#public bool IsNumber(string text)
{
return int.TryParse(text, out _);
}
ファイル存在チェックなども同じです。
C#if (File.Exists(path))
{
var text = File.ReadAllText(path);
}
もちろん、ファイルは存在チェック後に削除される可能性もあるため、最終的な例外処理は必要です。しかし、発生が予想できる通常ケースは、例外ではなく分岐で処理する方が適切です。
3-7. キャッシュを活用して同じ計算や取得処理を繰り返さない
同じデータを何度も計算・取得している場合、キャッシュが有効です。
C#private readonly Dictionary<int, User> _userCache = new();
public User? GetUser(int id)
{
if (_userCache.TryGetValue(id, out var user))
{
return user;
}
user = LoadUserFromDatabase(id);
if (user != null)
{
_userCache[id] = user;
}
return user;
}
ただし、キャッシュには注意点もあります。古いデータを返してしまう、メモリを消費する、スレッドセーフにする必要がある、といった問題です。業務アプリでは、キャッシュの有効期限や削除タイミングを明確にしておきましょう。
4. LINQ・コレクション処理を高速化する実例
C#の速度改善でよく話題になるのがLINQです。LINQは読みやすく強力ですが、使い方によっては速度やメモリ効率に影響します。
4-1. LINQが遅くなる典型パターン
LINQが遅くなりやすいのは、次のようなケースです。
C#var result = users
.Where(u => u.IsActive)
.Select(u => new UserDto { Id = u.Id, Name = u.Name })
.Where(dto => dto.Name.StartsWith("A"))
.ToList();
データ件数が少なければ問題ありません。しかし、数百万件のデータを何度も処理する場合、ラムダ式、列挙子、中間オブジェクト生成のコストが積み重なります。
また、次のようにループ内でLINQを繰り返すコードも注意が必要です。
C#foreach (var order in orders)
{
var user = users.FirstOrDefault(u => u.Id == order.UserId);
}
この場合、注文ごとにusersを先頭から検索するため、データ量が増えるほど遅くなります。
4-2. Where・Select・ToListの多用による中間オブジェクトを減らす
LINQでは遅延実行される処理も多いですが、ToListやToArrayを呼ぶとその時点でコレクションが作成されます。
悪い例です。
C#var activeUsers = users.Where(u => u.IsActive).ToList();
var names = activeUsers.Select(u => u.Name).ToList();
var filteredNames = names.Where(n => n.StartsWith("A")).ToList();
改善例です。
C#var filteredNames = users
.Where(u => u.IsActive)
.Select(u => u.Name)
.Where(n => n.StartsWith("A"))
.ToList();
さらに速度が必要な場合は、手書きループにします。
C#var filteredNames = new List<string>();
foreach (var user in users)
{
if (!user.IsActive)
{
continue;
}
var name = user.Name;
if (name.StartsWith("A"))
{
filteredNames.Add(name);
}
}
重要なのは、ToListを必要な場所だけで使うことです。途中結果を何度もリスト化すると、メモリ確保とコピーが増えます。
4-3. Any・Contains・FirstOrDefaultを適切に使い分ける
存在確認には、件数を数えるよりAnyを使う方が適切です。
悪い例です。
C#if (users.Count(u => u.IsActive) > 0)
{
// 存在する
}
改善例です。
C#if (users.Any(u => u.IsActive))
{
// 存在する
}
Anyは条件に合う要素が見つかった時点で終了できます。一方、Countは全体を数える必要があるため、存在確認には向きません。
Containsも、Listで使う場合とHashSetで使う場合では速度が大きく変わります。
C#var ids = users.Select(u => u.Id).ToList();
if (ids.Contains(targetId))
{
// Listでは線形検索
}
大量データで頻繁に存在確認するならHashSetを使います。
C#var idSet = users.Select(u => u.Id).ToHashSet();
if (idSet.Contains(targetId))
{
// 高速に検索しやすい
}
FirstOrDefaultは最初の1件が欲しい場合に便利ですが、何度も検索する場合はDictionary化を検討しましょう。
4-4. List・Array・Dictionary・HashSetの速度差と選び方
C#のコレクションは用途によって速度特性が異なります。
Arrayは固定長で、連続したデータを高速に扱いやすいです。Listは要素追加がしやすく、一般的な可変長コレクションとして便利です。Dictionaryはキーから値を高速に取得したい場合に向いています。HashSetは重複なしの集合や存在確認に向いています。
使い分けの目安は次の通りです。
C#// 順番に処理するだけ
int[] numbers = new int[1000];
// 追加・削除がある一般的なリスト
var users = new List<User>();
// IDからユーザーを取得する
var userMap = new Dictionary<int, User>();
// IDが存在するか確認する
var userIds = new HashSet<int>();
よくある失敗は、Listのまま何度も検索してしまうことです。
C#var user = users.FirstOrDefault(u => u.Id == id);
1回だけなら問題ありませんが、何万回も検索するならDictionaryを使う方が効率的です。
4-5. foreachとforの使い分け
foreachは読みやすく、多くの場面で十分高速です。
C#foreach (var item in items)
{
Process(item);
}
Listや配列を高頻度で処理する場合、forの方がわずかに有利になることがあります。
C#for (int i = 0; i < items.Count; i++)
{
Process(items[i]);
}
ただし、foreachをすべてforに置き換える必要はありません。通常の業務アプリでは可読性の方が重要なことも多いです。大量データ処理やホットパスで、計測して差が出た場合にforへ置き換えるのが現実的です。
4-6. 大量データ処理でDictionaryやHashSetを使う高速化例
次のようなコードは、データ量が増えると遅くなります。
C#foreach (var order in orders)
{
var user = users.FirstOrDefault(u => u.Id == order.UserId);
if (user != null)
{
order.UserName = user.Name;
}
}
ordersが10万件、usersが10万件ある場合、最悪で膨大な検索が発生します。Dictionaryを使うと改善できます。
C#var userMap = users.ToDictionary(u => u.Id);
foreach (var order in orders)
{
if (userMap.TryGetValue(order.UserId, out var user))
{
order.UserName = user.Name;
}
}
存在確認だけならHashSetを使います。
C#var activeUserIds = users
.Where(u => u.IsActive)
.Select(u => u.Id)
.ToHashSet();
foreach (var order in orders)
{
if (activeUserIds.Contains(order.UserId))
{
Process(order);
}
}
このようなデータ構造の変更は、C#の速度改善で非常に効果が出やすいポイントです。
4-7. LINQを手書きループに置き換えるべきケース
LINQを手書きループに置き換えるべきなのは、次のような場合です。
大量データを処理している
高頻度で呼ばれる
プロファイラーでボトルネックになっている
中間オブジェクト生成が多い
複雑なLINQで可読性が下がっている
たとえば、次のLINQは読みやすいですが、大量データでは手書きループの方が有利な場合があります。
C#var total = items
.Where(x => x.IsActive)
.Select(x => x.Price * x.Quantity)
.Sum();
手書きループにすると、1回の走査で処理できます。
C#decimal total = 0;
foreach (var item in items)
{
if (item.IsActive)
{
total += item.Price * item.Quantity;
}
}
ただし、LINQを完全に避ける必要はありません。通常の画面表示や小規模データではLINQの可読性が大きなメリットになります。
5. メモリ管理とGC負荷を減らす高速化
C#の速度改善では、CPU処理だけでなくメモリ管理も重要です。不要なアロケーションを減らすことで、GCの発生頻度を抑え、安定したパフォーマンスを得やすくなります。
5-1. GCがC#の速度に与える影響
GCは不要になったオブジェクトを自動的に回収します。しかし、短時間に大量のオブジェクトが生成されると、GCが頻繁に動作し、アプリケーションの処理を一時的に止めることがあります。
特に注意すべきなのは、次のような処理です。
C#for (int i = 0; i < 100000; i++)
{
var buffer = new byte[4096];
Process(buffer);
}
このように毎回バッファを作ると、大量の一時オブジェクトが発生します。再利用できるバッファであれば、ループ外で確保する方が効率的です。
C#var buffer = new byte[4096];
for (int i = 0; i < 100000; i++)
{
Process(buffer);
}
GC対策では、まずメモリ確保の多い箇所をプロファイラーで確認することが大切です。
5-2. classとstructの使い分け
C#ではclassは参照型、structは値型です。structを使うとヒープアロケーションを避けられる場合がありますが、安易にstruct化すれば速くなるわけではありません。
小さく、不変で、値として扱いたいデータにはstructが向いています。
C#public readonly struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y)
{
X = x;
Y = y;
}
}
一方、大きなデータをstructにするとコピーコストが増えることがあります。
C#public struct LargeData
{
public long A;
public long B;
public long C;
public long D;
public long E;
}
classとstructの選択では、アロケーションだけでなくコピーコスト、可読性、API設計も考える必要があります。
5-3. Span<T>・Memory<T>でコピーとアロケーションを減らす
Span<T>は、配列や文字列の一部をコピーせずに参照できる仕組みです。文字列処理やバイナリ処理でアロケーションを減らすのに役立ちます。
たとえば、文字列の一部を扱うとき、Substringは新しい文字列を生成します。
C#var text = "2026-06-18";
var year = text.Substring(0, 4);
Spanを使うと、コピーせずに範囲を扱えます。
C#ReadOnlySpan<char> span = "2026-06-18";
ReadOnlySpan<char> year = span.Slice(0, 4);
数値変換にも使えます。
C#ReadOnlySpan<char> span = "12345";
if (int.TryParse(span, out int value))
{
Console.WriteLine(value);
}
Span<T>は高性能ですが、使える範囲に制約があります。通常のアプリでは、ボトルネックになっている文字列処理や大量データ処理で検討するとよいでしょう。
5-4. ArrayPool<T>で一時配列の生成を抑える
一時的な大きな配列を何度も作る場合は、ArrayPool<T>を使って配列を借りることができます。
C#using System.Buffers;
var pool = ArrayPool<byte>.Shared;
byte[] buffer = pool.Rent(4096);
try
{
Process(buffer);
}
finally
{
pool.Return(buffer);
}
ArrayPool<T>を使うと、配列の再利用によってアロケーションを減らせます。ただし、返却し忘れや、返却後の配列を使ってしまうバグには注意が必要です。
また、借りた配列は要求サイズより大きい場合があります。実際に使う長さは別途管理しましょう。
C#int length = 4096;
byte[] buffer = pool.Rent(length);
try
{
Process(buffer.AsSpan(0, length));
}
finally
{
pool.Return(buffer);
}
5-5. boxing・unboxingを避ける
boxingは値型をobjectとして扱うときに発生します。大量に発生するとメモリ確保とGC負荷につながります。
C#int value = 123;
object boxed = value; // boxing
int unboxed = (int)boxed; // unboxing
特に古い非ジェネリックコレクションを使うとboxingが発生しやすくなります。
C#var list = new ArrayList();
list.Add(123); // boxing
改善例です。
C#var list = new List<int>();
list.Add(123);
ログ出力や文字列フォーマットでもboxingが発生する場合があります。高頻度の処理では、型安全なAPIや構造化ログを活用しましょう。
5-6. IDisposableとusingでリソース解放を適切に行う
ファイル、ストリーム、DB接続などの外部リソースは、GC任せにせず適切に解放する必要があります。
C#using var stream = File.OpenRead(path);
using var reader = new StreamReader(stream);
var text = reader.ReadToEnd();
非同期処理では、IAsyncDisposableを使う場合もあります。
C#await using var resource = new AsyncResource();
await resource.ProcessAsync();
リソース解放が不適切だと、ファイルハンドル不足、接続枯渇、メモリリークのような問題につながり、結果として速度低下を引き起こします。
5-7. 大量データ処理でメモリ使用量を削減する実例
大量データを一度にListへ読み込むと、メモリ使用量が大きくなります。
C#var lines = File.ReadAllLines(path);
foreach (var line in lines)
{
Process(line);
}
行ごとに処理できるなら、ストリーミング処理にします。
C#foreach (var line in File.ReadLines(path))
{
Process(line);
}
非同期で処理する場合は、次のように書けます。
C#await foreach (var line in ReadLinesAsync(path))
{
Process(line);
}
大量データ処理では、「すべてをメモリに載せる」のではなく、「必要な分だけ順番に処理する」設計が有効です。
6. 非同期・並列処理でC#アプリを高速化する
C#では、async/awaitやTask、Parallelを使って処理を効率化できます。ただし、非同期処理と並列処理は目的が異なります。I/O待ちにはasync/await、CPU負荷の高い処理には並列化を検討します。
6-1. async/awaitでI/O待ちを効率化する
async/awaitは、DB、ファイル、HTTP通信などのI/O待ちに有効です。待っている間にスレッドを占有しにくくなるため、WebアプリやAPIサーバーではスケーラビリティ向上につながります。
C#public async Task<string> LoadAsync(string path)
{
return await File.ReadAllTextAsync(path);
}
HTTP通信も非同期で書けます。
C#public async Task<string> GetContentAsync(HttpClient httpClient, string url)
{
return await httpClient.GetStringAsync(url);
}
async/awaitは、単体の処理時間を必ず短くするものではありません。待ち時間の多い処理で、スレッドを効率よく使うための仕組みです。
6-2. Task.WhenAllで複数処理を並行実行する
複数の独立したI/O処理は、順番に待つより並行実行した方が速くなることがあります。
悪い例です。
C#var user = await GetUserAsync(id);
var orders = await GetOrdersAsync(id);
var points = await GetPointsAsync(id);
改善例です。
C#var userTask = GetUserAsync(id);
var ordersTask = GetOrdersAsync(id);
var pointsTask = GetPointsAsync(id);
await Task.WhenAll(userTask, ordersTask, pointsTask);
var user = await userTask;
var orders = await ordersTask;
var points = await pointsTask;
3つの処理が互いに依存していない場合、合計時間を短縮できます。ただし、DBや外部APIに一気に負荷をかけすぎないように注意が必要です。
6-3. Parallel.ForEachでCPU負荷の高い処理を並列化する
CPU負荷が高い処理では、Parallel.ForEachが有効な場合があります。
C#Parallel.ForEach(items, item =>
{
item.Result = HeavyCalculation(item);
});
.NET 6以降では、非同期処理に対応したParallel.ForEachAsyncも使えます。
C#await Parallel.ForEachAsync(urls, async (url, cancellationToken) =>
{
var content = await httpClient.GetStringAsync(url, cancellationToken);
Process(content);
});
ただし、並列化は万能ではありません。CPUコア数、処理粒度、共有リソースの有無によって効果が変わります。
6-4. 並列化で逆に遅くなるケース
並列化すると、タスク作成、スレッド切り替え、同期処理、ロック競合などのオーバーヘッドが発生します。そのため、軽い処理を大量に並列化すると逆に遅くなることがあります。
悪い例です。
C#Parallel.ForEach(numbers, n =>
{
var x = n + 1;
});
この程度の軽い処理では、並列化のオーバーヘッドの方が大きくなりやすいです。
また、共有リストに同時追加するような処理も注意が必要です。
C#var results = new List<int>();
Parallel.ForEach(numbers, n =>
{
results.Add(n * 2); // スレッドセーフではない
});
この場合は、スレッドセーフなコレクションを使うか、ローカルに集約して最後にマージする設計が必要です。
6-5. lock・競合・スレッドセーフによる速度低下を防ぐ
並列処理では、共有リソースへのアクセスがボトルネックになることがあります。
C#private readonly object _lock = new();
private int _count;
public void Increment()
{
lock (_lock)
{
_count++;
}
}
lockは正しく使えば安全ですが、高頻度に競合すると速度低下の原因になります。単純なカウンタであればInterlockedを使えます。
C#Interlocked.Increment(ref _count);
並列処理では、共有状態を減らすことが重要です。各スレッドで独立して処理し、最後に結果をまとめる設計にすると、競合を減らせます。
6-6. ConfigureAwaitやValueTaskを使うべき場面
ライブラリコードでは、ConfigureAwait(false)を使うことで、元の同期コンテキストに戻る必要がないことを明示できます。
C#public async Task<string> LoadAsync(string path)
{
return await File.ReadAllTextAsync(path).ConfigureAwait(false);
}
ASP.NET Coreでは同期コンテキストの影響が小さいため、必ず必要というわけではありません。しかし、汎用ライブラリでは検討する価値があります。
ValueTaskは、結果が同期的に返ることが多い非同期処理でアロケーションを減らせる場合があります。
C#public ValueTask<int> GetCountAsync()
{
if (_cached)
{
return ValueTask.FromResult(_count);
}
return new ValueTask<int>(LoadCountAsync());
}
ただし、ValueTaskは扱いがやや複雑です。通常はTaskを使い、パフォーマンス上の理由が明確な場合にValueTaskを検討しましょう。
6-7. 非同期処理のよくある失敗例と改善例
よくある失敗は、非同期メソッドを同期的に待ってしまうことです。
C#var result = GetDataAsync().Result;
または次のような書き方です。
C#GetDataAsync().Wait();
これらはデッドロックやスレッドブロックの原因になることがあります。基本的にはawaitを使います。
C#var result = await GetDataAsync();
また、非同期処理を順番にawaitしてしまうケースもあります。
C#foreach (var url in urls)
{
var content = await httpClient.GetStringAsync(url);
Process(content);
}
独立した処理ならTask.WhenAllを使います。
C#var tasks = urls.Select(url => httpClient.GetStringAsync(url));
var contents = await Task.WhenAll(tasks);
foreach (var content in contents)
{
Process(content);
}
ただし、外部APIへの大量同時アクセスは相手側にも負荷をかけるため、SemaphoreSlimなどで同時実行数を制限することも重要です。
7. DB・ファイル・API通信の速度改善
C#アプリが遅い原因として非常に多いのが、DB、ファイル、API通信です。CPU処理の最適化より、外部アクセスの見直しの方が大きな効果を出すことがあります。
7-1. C#アプリで遅くなりやすい外部アクセス処理
外部アクセスで遅くなりやすいのは、次のような処理です。
C#var users = await db.Users.ToListAsync();
var text = await File.ReadAllTextAsync(path);
var response = await httpClient.GetAsync(url);
これらはC#の計算速度というより、DB、ディスク、ネットワーク、外部サービスの応答時間に左右されます。
特にWebアプリでは、1リクエスト内で複数のDBクエリやAPI呼び出しをしていると、合計待ち時間が大きくなります。まずは、どの外部アクセスに何msかかっているかをログで確認しましょう。
7-2. Entity Framework Coreのクエリを高速化する
Entity Framework Coreを使っている場合、クエリの書き方が速度に大きく影響します。MicrosoftのEF Coreパフォーマンスガイドでも、効率的なクエリ、必要なデータだけの取得、インデックス設計などが重要な観点として扱われています。Microsoft Learn+1
悪い例です。
C#var users = await db.Users.ToListAsync();
var activeUsers = users
.Where(u => u.IsActive)
.ToList();
このコードは全ユーザーをDBから取得してから、メモリ上で絞り込んでいます。改善例です。
C#var activeUsers = await db.Users
.Where(u => u.IsActive)
.ToListAsync();
DBで絞り込むことで、転送量とメモリ使用量を減らせます。
更新しない読み取り専用データでは、AsNoTrackingを使うと変更追跡のコストを減らせます。
C#var users = await db.Users
.AsNoTracking()
.Where(u => u.IsActive)
.ToListAsync();
7-3. 必要な列だけ取得してデータ転送量を減らす
すべての列を取得すると、DBからアプリへの転送量が増えます。画面表示に必要な列だけをSelectで指定しましょう。
悪い例です。
C#var users = await db.Users
.Where(u => u.IsActive)
.ToListAsync();
改善例です。
C#var users = await db.Users
.Where(u => u.IsActive)
.Select(u => new UserListDto
{
Id = u.Id,
Name = u.Name,
Email = u.Email
})
.ToListAsync();
特に大きなテキスト列、JSON列、バイナリ列を持つテーブルでは、不要な列を取得しないだけで速度が改善することがあります。
7-4. N+1問題を避ける
N+1問題は、最初に1回クエリを実行し、その後データ件数分の追加クエリを発行してしまう問題です。
悪い例です。
C#var users = await db.Users.ToListAsync();
foreach (var user in users)
{
var orders = await db.Orders
.Where(o => o.UserId == user.Id)
.ToListAsync();
user.OrderCount = orders.Count;
}
ユーザーが1000人いれば、注文取得クエリが1000回発行される可能性があります。改善例です。
C#var orderCounts = await db.Orders
.GroupBy(o => o.UserId)
.Select(g => new
{
UserId = g.Key,
Count = g.Count()
})
.ToDictionaryAsync(x => x.UserId, x => x.Count);
var users = await db.Users.ToListAsync();
foreach (var user in users)
{
user.OrderCount = orderCounts.GetValueOrDefault(user.Id);
}
Includeを使う場合もあります。
C#var users = await db.Users
.Include(u => u.Orders)
.ToListAsync();
ただし、Includeで大量の関連データを読み込むと逆に重くなることもあります。必要なデータ量に応じて、Select、Include、集約クエリを使い分けましょう。
7-5. インデックス・SQL・接続プールを見直す
C#側のコードが適切でも、DB側にインデックスがなければ遅くなります。
C#var user = await db.Users
.FirstOrDefaultAsync(u => u.Email == email);
このような検索が多いなら、Email列にインデックスが必要です。
また、EF Coreが生成するSQLを確認することも重要です。
C#var query = db.Users.Where(u => u.IsActive);
Console.WriteLine(query.ToQueryString());
接続プールも確認しましょう。DB接続を毎回新しく作るのではなく、通常は接続プールによって再利用されます。ただし、接続を閉じ忘れたり、長時間トランザクションを保持したりすると、接続枯渇が発生することがあります。
7-6. ファイル読み書きはバッファリングと非同期I/Oを活用する
ファイルを一度にすべて読み込むと、メモリ使用量が増えます。
C#var text = File.ReadAllText(path);
小さなファイルなら問題ありませんが、大きなファイルではストリーミング処理を検討します。
C#using var stream = File.OpenRead(path);
using var reader = new StreamReader(stream);
string? line;
while ((line = await reader.ReadLineAsync()) != null)
{
Process(line);
}
バイナリファイルではバッファを使って読み込みます。
C#var buffer = new byte[8192];
using var stream = File.OpenRead(path);
int read;
while ((read = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
Process(buffer, read);
}
ファイルI/Oはディスク性能の影響も大きいため、コードだけでなく保存先、ネットワークドライブ、クラウドストレージの状態も確認しましょう。
7-7. HttpClientの使い回しとタイムアウト設定でAPI通信を改善する
HttpClientをリクエストごとにnewするのは避けましょう。
悪い例です。
C#using var client = new HttpClient();
var response = await client.GetAsync(url);
改善例です。
C#private static readonly HttpClient _httpClient = new HttpClient
{
Timeout = TimeSpan.FromSeconds(10)
};
public async Task<string> GetAsync(string url)
{
return await _httpClient.GetStringAsync(url);
}
ASP.NET CoreではIHttpClientFactoryを使うのが一般的です。
C#builder.Services.AddHttpClient("ApiClient", client =>
{
client.Timeout = TimeSpan.FromSeconds(10);
});
利用側です。
C#public class ApiService
{
private readonly HttpClient _client;
public ApiService(IHttpClientFactory factory)
{
_client = factory.CreateClient("ApiClient");
}
public async Task<string> GetAsync(string url)
{
return await _client.GetStringAsync(url);
}
}
タイムアウト、リトライ、サーキットブレーカーなどを適切に設定すると、外部APIの遅延がアプリ全体に広がるのを防ぎやすくなります。
8. .NETランタイム・コンパイル設定による高速化
C#の速度は、コードだけでなく.NETランタイムやコンパイル設定にも影響されます。古い環境を使っている場合、.NETのバージョンを上げるだけで改善することもあります。
8-1. .NET Frameworkと.NETの速度差
従来の.NET Frameworkと、現在の.NETではランタイムやJIT、ライブラリ実装が異なります。新しい.NETでは、実行速度、メモリ効率、GC、JIT最適化、コンテナ対応、クロスプラットフォーム対応などが継続的に改善されています。
古い.NET Frameworkアプリを保守している場合、すぐに移行できないこともあります。しかし、新規開発や大規模改修では、現在サポートされている.NETへの移行を検討する価値があります。
8-2. 最新の.NETへ移行するメリット
.NETは継続的にパフォーマンス改善が行われています。Microsoftの公式サポートポリシーでは、LTSとSTSのサポート期間が定義されており、LTSは長期サポート、STSは標準期間のサポートとして提供されます。2026年時点では.NET 10がLTSとして提供されています。Microsoft+1
新しい.NETへ移行するメリットは次の通りです。
JIT最適化の改善
GC性能の改善
標準ライブラリの高速化
ASP.NET Coreの性能改善
Native AOTやPGOなどの利用
セキュリティ更新を受けられる
ただし、移行には互換性確認が必要です。古いライブラリ、Windows依存API、設定ファイル、認証方式などを事前に確認しましょう。
8-3. ReadyToRun・Native AOTで起動速度を改善する
ReadyToRunを有効にすると、起動時のJIT負荷を減らせます。
XML<PropertyGroup>
<PublishReadyToRun>true</PublishReadyToRun>
</PropertyGroup>
発行例です。
Bashdotnet publish -c Release -r win-x64 --self-contained true
Native AOTを使うと、アプリを事前にネイティブコードへコンパイルできます。
XML<PropertyGroup>
<PublishAot>true</PublishAot>
</PropertyGroup>
発行例です。
Bashdotnet publish -c Release -r linux-x64
Native AOTは起動速度やメモリ使用量の改善に効果がありますが、リフレクション、動的コード生成、シリアライザーなどで制約が出る場合があります。Web API、CLIツール、サーバーレス、短命プロセスでは特に検討する価値があります。
8-4. Tiered CompilationとPGOによる最適化
Tiered Compilationは、最初は素早くJITし、その後よく使われるメソッドをより最適化されたコードへ置き換える仕組みです。Microsoftのランタイム設定ドキュメントでは、最初の段階でQuick JITやReadyToRunコードを使い、次の段階でバックグラウンドの最適化JITを行う流れが説明されています。Microsoft Learn+1
PGOは、実行時のプロファイル情報を使って最適化する仕組みです。よく通る分岐や型情報をもとに、より実際の実行パターンに合った最適化が行われます。
アプリによっては、これらの設定を明示的に確認するとよいでしょう。
XML<PropertyGroup>
<TieredCompilation>true</TieredCompilation>
<TieredPGO>true</TieredPGO>
</PropertyGroup>
多くの通常アプリでは既定設定で十分ですが、高性能が求められるサーバーアプリやベンチマークでは、設定を把握しておくと役立ちます。
8-5. RyuJITによる実行時最適化
RyuJITは.NETで使われるJITコンパイラです。メソッドのインライン化、不要な境界チェックの削減、ループ最適化、SIMD活用など、さまざまな最適化が行われます。
そのため、C#では手動で細かく最適化しなくても、ランタイムが効率的なコードに変換してくれることがあります。逆に、過度に複雑な手書き最適化をすると、JITが最適化しにくくなったり、可読性が下がったりすることもあります。
まずは素直で読みやすいコードを書き、必要な箇所だけ計測して最適化するのが基本です。
8-6. サーバーGCとワークステーションGCの違い
.NETには、ワークステーションGCとサーバーGCがあります。
ワークステーションGCは、デスクトップアプリなどの応答性を重視する場面に向いています。サーバーGCは、マルチコア環境で高スループットを狙うサーバーアプリに向いています。
ASP.NET Coreなどのサーバーアプリでは、サーバーGCが有効になっていることが多いです。設定例です。
XML<PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
ただし、サーバーGCはメモリ使用量が増える場合があります。小規模アプリやメモリ制限の厳しい環境では、実測して判断しましょう。
8-7. アプリ種別ごとの推奨設定
Web APIでは、Releaseビルド、最新の.NET、サーバーGC、非同期I/O、DBクエリ最適化が重要です。
バッチ処理では、大量データを一括でメモリに載せないこと、DictionaryやHashSetの活用、並列化、GC負荷削減が効果的です。
デスクトップアプリでは、起動速度、UIスレッドのブロック回避、バックグラウンド処理、ReadyToRunが候補になります。
CLIツールやサーバーレスでは、起動速度が重要なため、ReadyToRunやNative AOTを検討できます。
Unityでは、GCアロケーション削減、Update内の処理削減、オブジェクトプール、LINQの使いどころ、物理演算や描画負荷の確認が重要です。
9. C#高速化の実例コード
ここでは、よくあるC#の速度改善パターンをBefore/After形式で紹介します。
9-1. 文字列結合を高速化するBefore/After
Beforeです。
C#public string CreateCsv(IEnumerable<int> numbers)
{
var csv = "";
foreach (var number in numbers)
{
csv += number + ",";
}
return csv;
}
Afterです。
C#public string CreateCsv(IEnumerable<int> numbers)
{
var sb = new StringBuilder();
foreach (var number in numbers)
{
sb.Append(number);
sb.Append(',');
}
return sb.ToString();
}
さらに末尾のカンマを避けたい場合です。
C#public string CreateCsv(IEnumerable<int> numbers)
{
return string.Join(",", numbers);
}
単純な区切り文字結合なら、string.Joinが最も読みやすく効率的です。
9-2. List検索をDictionary検索に変えて高速化する例
Beforeです。
C#foreach (var order in orders)
{
var user = users.FirstOrDefault(u => u.Id == order.UserId);
if (user != null)
{
order.UserName = user.Name;
}
}
Afterです。
C#var userMap = users.ToDictionary(u => u.Id);
foreach (var order in orders)
{
if (userMap.TryGetValue(order.UserId, out var user))
{
order.UserName = user.Name;
}
}
Listを毎回検索する処理は、大量データで遅くなりやすいです。キー検索が多い場合はDictionaryを使いましょう。
9-3. LINQをループに置き換えて高速化する例
Beforeです。
C#var total = items
.Where(x => x.IsActive)
.Select(x => x.Price * x.Quantity)
.Sum();
Afterです。
C#decimal total = 0;
foreach (var item in items)
{
if (!item.IsActive)
{
continue;
}
total += item.Price * item.Quantity;
}
LINQの方が読みやすい場合も多いですが、ホットパスでは手書きループの方が速くなることがあります。
9-4. async/awaitでAPI呼び出しを高速化する例
Beforeです。
C#var user = await GetUserAsync(id);
var orders = await GetOrdersAsync(id);
var points = await GetPointsAsync(id);
Afterです。
C#var userTask = GetUserAsync(id);
var ordersTask = GetOrdersAsync(id);
var pointsTask = GetPointsAsync(id);
await Task.WhenAll(userTask, ordersTask, pointsTask);
var user = await userTask;
var orders = await ordersTask;
var points = await pointsTask;
依存関係のないI/O処理は、並行実行することで待ち時間を短縮できます。
9-5. Span<T>で文字列・配列処理を高速化する例
Beforeです。
C#var text = "2026-06-18";
var year = text.Substring(0, 4);
var month = text.Substring(5, 2);
var day = text.Substring(8, 2);
Afterです。
C#ReadOnlySpan<char> span = "2026-06-18";
var year = span.Slice(0, 4);
var month = span.Slice(5, 2);
var day = span.Slice(8, 2);
数値として扱う場合です。
C#ReadOnlySpan<char> span = "2026-06-18";
int.TryParse(span.Slice(0, 4), out int year);
int.TryParse(span.Slice(5, 2), out int month);
int.TryParse(span.Slice(8, 2), out int day);
Substringによる文字列生成を避けられるため、大量の文字列処理で効果が出る場合があります。
9-6. BenchmarkDotNetで改善前後の速度を比較する
改善前後を比較するベンチマーク例です。
C#using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
BenchmarkRunner.Run<SearchBenchmark>();
public class SearchBenchmark
{
private List<User> _users = null!;
private Dictionary<int, User> _userMap = null!;
[GlobalSetup]
public void Setup()
{
_users = Enumerable.Range(1, 100000)
.Select(i => new User { Id = i, Name = $"User{i}" })
.ToList();
_userMap = _users.ToDictionary(u => u.Id);
}
[Benchmark]
public User? SearchByList()
{
return _users.FirstOrDefault(u => u.Id == 90000);
}
[Benchmark]
public User? SearchByDictionary()
{
return _userMap.TryGetValue(90000, out var user) ? user : null;
}
}
public class User
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
このように実際のデータ量に近い条件で測定すると、改善効果を判断しやすくなります。
9-7. 高速化による可読性低下を防ぐ書き方
高速化を意識しすぎると、コードが読みにくくなることがあります。たとえば、すべてのLINQを複雑なfor文に置き換えたり、Span<T>を必要以上に使ったりすると、保守性が下がります。
可読性を保つには、次のようにメソッドへ分離します。
C#public decimal CalculateTotal(IEnumerable<OrderItem> items)
{
decimal total = 0;
foreach (var item in items)
{
if (ShouldInclude(item))
{
total += CalculatePrice(item);
}
}
return total;
}
private bool ShouldInclude(OrderItem item)
{
return item.IsActive && item.Quantity > 0;
}
private decimal CalculatePrice(OrderItem item)
{
return item.Price * item.Quantity;
}
高速化したコードでも、意図が分かる名前を付け、責務を分けることで保守しやすくできます。
10. C#の速度改善で避けるべき失敗
C#の速度改善では、やってしまいがちな失敗があります。効果のない最適化や、保守性を損なう改善を避けることが重要です。
10-1. 計測せずに細かい最適化から始める
最も多い失敗は、計測せずに最適化を始めることです。
たとえば、foreachをforに変える、LINQをすべて消す、文字列結合を全部StringBuilderにする、といった対応をしても、ボトルネックがDBなら効果はほとんどありません。
まずはStopwatch、BenchmarkDotNet、Profiler、ログなどで本当に遅い箇所を特定しましょう。
10-2. 可読性を犠牲にしすぎる
速度を追求するあまり、誰も読めないコードになるのは危険です。保守性が下がると、バグ修正や機能追加に時間がかかります。
高速化は重要ですが、すべてのコードを最速にする必要はありません。特に業務アプリでは、速度と可読性のバランスが大切です。
10-3. LINQをすべて悪者にする
LINQは遅いから使わない、という考え方は極端です。LINQは可読性が高く、バグを減らしやすい便利な機能です。
問題になるのは、大量データや高頻度処理で無駄なToListや複雑なチェーンを多用するケースです。通常の画面表示、設定処理、小規模データではLINQを使って問題ないことが多いです。
10-4. 並列化すれば必ず速くなると考える
並列化は、CPU負荷の高い処理や独立したI/O処理では効果があります。しかし、軽い処理や共有リソースが多い処理では、逆に遅くなることがあります。
並列化では、次の点を確認しましょう。
処理は十分に重いか
データは独立しているか
lock競合はないか
DBやAPIに負荷をかけすぎないか
同時実行数を制御しているか
並列化も必ず計測して効果を確認する必要があります。
10-5. メモリ削減と速度改善を混同する
メモリ使用量を減らすことが、必ず速度改善につながるとは限りません。メモリを少し多く使ってキャッシュした方が速い場合もあります。
逆に、速度を上げるためにキャッシュを増やしすぎると、メモリ不足やGC負荷の増加につながることもあります。
速度、メモリ、可読性、安定性はトレードオフです。どれを優先するかはアプリの要件によって変わります。
10-6. 本番環境と異なる条件でベンチマークする
ローカルPCでは速いのに、本番環境では遅いことがあります。原因は、データ量、同時アクセス数、DBサーバー、ネットワーク、CPU、メモリ、コンテナ制限などです。
ベンチマークでは、できるだけ本番に近い条件を用意しましょう。
本番に近いデータ量
本番に近い同時実行数
Releaseビルド
デバッガなし
実際のDBやAPIに近い環境
ウォームアップ後の測定
小さなサンプルデータだけで速いと判断するのは危険です。
10-7. 保守性と速度のバランスを考えない
高速化したコードは、その後も保守されます。将来の開発者が読めないコードは、長期的にはコストになります。
速度が必要な箇所ではコメントを残しましょう。
C#// 大量データ処理のホットパスのため、LINQではなくループで処理する
foreach (var item in items)
{
...
}
なぜその書き方にしたのかを残しておくと、後から不要に書き換えられるリスクを減らせます。
11. C#の速度改善に関するよくある質問
ここでは、C#の速度に関してよくある疑問に答えます。
11-1. C#はC++やJavaより遅いのか
C#はGCやJITを使うため、C++のように低レベルまで制御できる言語と比べると、不利になる場面はあります。特にリアルタイム性が厳しい処理や、メモリ管理を極限まで制御したい処理ではC++が選ばれることがあります。
一方で、C#と.NETはJIT最適化、GC改善、Span<T>、SIMD、Native AOTなどにより、多くの業務アプリやWebアプリでは十分高速です。
Javaとの比較では、処理内容、JVM/.NETのバージョン、GC設定、フレームワーク、コードの書き方によって結果が変わります。単純に「C#が遅い」「Javaが速い」とは言えません。
11-2. C#で最も遅くなりやすい処理は何か
よく遅くなるのは、C#の計算処理そのものよりも、DB、API、ファイルI/O、ネットワーク、不要なメモリ確保です。
特に多いのは次のケースです。
DBのN+1問題
インデックス不足
大量データのToList
API呼び出しの逐次実行
ループ内の不要なオブジェクト生成
文字列結合の多用
Listの線形検索を大量に繰り返す処理
まずは外部アクセスと大量データ処理を疑うとよいでしょう。
11-3. LINQは使わないほうがよいのか
LINQは使って問題ありません。むしろ、多くの場面では読みやすく、バグを減らせるメリットがあります。
ただし、大量データ処理、高頻度で呼ばれる処理、リアルタイム性が求められる処理では、LINQのオーバーヘッドが問題になることがあります。その場合は、プロファイラーやBenchmarkDotNetで確認し、必要に応じて手書きループへ置き換えましょう。
11-4. async/awaitを使うと処理速度は上がるのか
async/awaitは、CPU処理を速くする機能ではありません。主にI/O待ちを効率化するための仕組みです。
たとえば、DBやAPIの応答待ち中にスレッドを解放できるため、Webアプリでは同時リクエストを処理しやすくなります。また、Task.WhenAllを使えば、独立した複数のI/O処理を並行実行できます。
一方、CPU負荷の高い計算処理をasyncにしても速くなるわけではありません。CPU処理には並列化やアルゴリズム改善を検討します。
11-5. .NETのバージョンを上げるだけで速くなるのか
速くなる可能性はあります。新しい.NETでは、JIT、GC、標準ライブラリ、ASP.NET Coreなどが改善されているため、同じコードでも性能が向上することがあります。
ただし、必ず速くなるとは限りません。アプリのボトルネックがDBや外部APIにある場合、.NETのバージョンを上げても体感速度はあまり変わらないことがあります。
移行時は、互換性確認とベンチマークを行いましょう。
11-6. UnityのC#を高速化するには何を見直すべきか
Unityでは、通常のC#アプリ以上にGCアロケーションに注意が必要です。特にUpdate内で毎フレームオブジェクトを生成すると、GCスパイクの原因になります。
見直すべきポイントは次の通りです。
Update内の処理を減らす
毎フレームnewしない
LINQを多用しない
文字列結合を減らす
オブジェクトプールを使う
GetComponentの呼び出し回数を減らす
物理演算と描画負荷を確認する
ProfilerでGC Allocを確認する
Unityでは、CPUだけでなく描画、物理、アニメーション、アセット読み込みも速度に影響します。
11-7. 業務アプリで優先すべき高速化ポイントはどこか
業務アプリでは、まずDBと外部APIを見直すのが効果的です。
優先順位の例です。
遅い画面・APIをログで特定する
DBクエリ時間を確認する
N+1問題を解消する
必要な列だけ取得する
インデックスを確認する
外部API呼び出しを並行化・キャッシュする
大量データ処理のコレクションを見直す
GC負荷を確認する
業務アプリでは、C#の細かい構文よりも、DB設計やデータ取得方法の方が速度に大きく影響することが多いです。
12. C#の速度改善は計測・原因特定・適切な最適化が重要
C#の速度改善では、テクニックを知っているだけでは不十分です。どのテクニックを、どの場面で使うかが重要です。
12-1. まずは遅い原因を数値で把握する
最初にやるべきことは計測です。
C#var sw = Stopwatch.StartNew();
await ExecuteAsync();
sw.Stop();
logger.LogInformation("Elapsed: {Elapsed}ms", sw.ElapsedMilliseconds);
処理時間を記録し、どの処理が遅いのかを数値で把握します。感覚ではなく、ログやプロファイラーで確認しましょう。
12-2. CPU・メモリ・I/O・DBのどこがボトルネックか切り分ける
速度低下の原因は、大きく分けると次のどれかです。
CPU処理が重い
メモリ確保が多い
GCが頻繁に発生している
DBクエリが遅い
API通信が遅い
ファイルI/Oが遅い
ロック競合が発生している
同時実行数が多すぎる
CPUが原因ならアルゴリズムや並列化、メモリが原因ならアロケーション削減、DBが原因ならクエリやインデックス、APIが原因なら非同期化やキャッシュを検討します。
12-3. 効果の大きい高速化から優先して実施する
高速化では、効果の大きい順に対応します。
たとえば、次のような優先順位です。
N+1問題の解消
↓
不要なデータ取得の削減
↓
DictionaryやHashSetによる検索高速化
↓
不要なオブジェクト生成の削減
↓
LINQの見直し
↓
Span<T>やArrayPool<T>など高度な最適化
最初から高度なテクニックに手を出す必要はありません。大きなボトルネックを解消する方が、短時間で大きな効果を得られます。
12-4. 可読性と保守性を保ちながら継続的に改善する
速度改善は一度きりではなく、継続的に行うものです。機能追加、データ量増加、アクセス増加によって、以前は問題なかった処理が遅くなることもあります。
そのため、次のような仕組みを整えると効果的です。
重要なAPIの処理時間をログに残す
定期的にプロファイリングする
ベンチマークを用意する
DBクエリを監視する
メモリ使用量とGCを確認する
高速化した理由をコメントに残す
高速化は、可読性や保守性を犠牲にしすぎないことが重要です。測定し、効果を確認し、必要な箇所だけ最適化する姿勢が、長期的に強いC#アプリを作ります。
まとめ
C#の速度を改善するには、まず「C#が遅い」と決めつけず、何がボトルネックになっているかを測定することが重要です。Debugビルド、JITの初回遅延、GC、LINQの使い方、コレクション選択、DBクエリ、API通信、ファイルI/O、並列処理の設計など、速度低下の原因はさまざまです。
すぐに効果が出やすい改善としては、Releaseビルドの利用、不要なオブジェクト生成の削減、StringBuilderの活用、List検索のDictionary化、ToListの削減、AnyやHashSetの適切な利用、DBのN+1問題解消、非同期I/Oの活用などがあります。
一方で、すべてのコードを過度に最適化する必要はありません。LINQは便利で読みやすく、多くの場面では十分高速です。Span<T>、ArrayPool<T>、Native AOT、PGOなどの高度な最適化も、必要な場面で使えば大きな効果がありますが、まずは計測と原因特定が先です。
C#の速度改善で最も大切なのは、推測ではなく数値で判断することです。Stopwatch、BenchmarkDotNet、Visual Studio Profiler、ログ、APMなどを使って遅い箇所を特定し、効果の大きい改善から順番に進めましょう。可読性と保守性を保ちながら継続的に改善することで、C#アプリは安定して高速に動作するようになります。

