C# + LINQ の速度比較（.NET Framework 4.7 ～ .NET 8）

# はじめに

:::note warn
当初、本記事は２件のフレームワークを比較していたため、タイトル後半部を `（.NET Framework 4.8 vs .NET 8）` としていましたが、その後にフレームワークを増やして６件にしたため、`（.NET Framework 4.7 ～ .NET 8）` と改めました
:::


C# を使った開発では LINQ を多用します。アプリケーションのパフォーマンスについて議論する際はこの LINQ の影響が大きいと考え、フレームワークによりどれほど違いがあるのかを確かめてみました。

## 測定したメソッド

下記の５件の実行時間を測定しました。

1. `Select`
1. `Where`
1. `OrderBy`
1. `Max`
1. `Sum`

## 比較したフレームワーク

下記６件のフレームワークを比較しました。

### .NET Framework

|                      | リリース | サポート終了 | 
| -------------------- | :----------: | :----------: | 
| 4.7.2 | 2018-04-30   | ???          | 
| 4.8   | 2019-04-18   | ???          | 
| 4.8.1 | 2022-08-09   | ???          | 

### .NET

|                      | リリース | サポート終了 | 
| -------------------- | :----------: | :----------: | 
| 6.0.33 (LTS)    | 2021-11-08   | 2024-11-12   | 
| 7.0.20          | 2022-11-08   | 2024-05-14   | 
| 8.0.8 (LTS)     | 2023-11-14   | 2026-11-10   | 

.NET Framework は 4.8.x が最後のリリースです。また、このサポート終了は Windows Server 2022 のサポート期限を考慮すると「2031-10-14 より後」と考えられているようです。

https://itskillmap.com/menu3_02/

対して .NET 8 のサポート終了は「2026-11-10」と .NET Framework 4.8 より早くサポート終了を迎えますが、後続として LTS 版である .NET 10 のリリースが控えています。
.NET 10 は 2025年11月にリリースされる予定であり、リリースから 3年間のサポートが予定されています。

## 検証環境

下記スペックの PC にて検証しました。

- CPU: Intel(R) Core(TM) i7-1068NG7 CPU @ 2.30GHz   2.30 GHz
- RAM: 32.0 GB
- OS: Windows 10 Pro (x64 / 19045.4780)

## 検証方法
Microsoft Visual Studio Professional 2022 (64 ビット) Version 17.10.6 にて検証ソースを Release ビルドして実行しました。

# 検証結果

.NET 8 が比較したメソッドの全てにおいて速いです。
メモリ使用量は概ね変わりません。`Where` のみ .NET 系が倍ほど利用しています

:::note info
下記の表は .NET Framework 4.8.1 での結果を `1.000` とした場合の値です
:::

## 所要時間

|         | 4.7.2 | 4.8   | 4.8.1 | 
| ------- | ----: | ----: | ----: | 
| Select  | 1.516 | 1.008 | 1.000 | 
| Where   | 1.021 | 1.002 | 1.000 | 
| OrderBy | 1.033 | 1.005 | 1.000 | 
| Max     | 1.032 | 1.031 | 1.000 | 
| Sum     | 1.018 | 1.002 | 1.000 | 

| 6.0.33 | 7.0.20 | 8.0.8 | 
| -----: | -----: | ----: | 
| 0.278  | 0.268  | 0.213 | 
| 0.586  | 0.595  | 0.515 | 
| 0.804  | 0.788  | 0.479 | 
| 0.961  | 1.022  | 0.503 | 
| 1.201  | 1.246  | 0.656 | 

## メモリ使用量

|         | 4.7.2 | 4.8   | 4.8.1 | 
| ------- | ----: | ----: | ----: | 
| Select  | 1.000 | 1.000 | 1.000 | 
| Where   | 1.000 | 1.000 | 1.000 | 
| OrderBy | 1.000 | 1.000 | 1.000 | 
| Max     | 1.000 | 1.000 | 1.000 | 
| Sum     | 1.000 | 1.000 | 1.000 | 

| 6.0.33 | 7.0.20 | 8.0.8 | 
| -----: | -----: | ----: | 
| 0.981  | 0.981  | 0.958 | 
| 1.997  | 1.997  | 1.997 | 
| 0.955  | 0.955  | 0.955 | 
| 0.816  | 0.816  | 0.816 | 
| 0.816  | 0.816  | 0.816 | 

# 検証ソース

下記のコードにて BenchmarkDotNet を利用し、処理の所要時間とメモリ使用量を出力しました。

https://www.nuget.org/packages/BenchmarkDotNet

```C#
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System;
using System.Collections.Generic;
using System.Linq;

namespace LinqBenchmark
{
	internal class Program
	{
		/// <summary>
		/// エントリポイント
		/// </summary>
		private static void Main()
		{
			// ベンチマークスタート
			BenchmarkRunner.Run<LinqOperation>();
			Console.ReadLine();
		}
	}

	[MemoryDiagnoser]
	public class LinqOperation
	{
		/// <summary>リストの要素数</summary>
		private const int LEN = 10000;

		/// <summary>リスト</summary>
		private readonly List<Entity> _entities = new List<Entity>();

		/// <summary>
		/// コンストラクタ
		/// </summary>
		public LinqOperation()
		{
			// リストを初期化
			for (var i = 0; i < LEN; i++) _entities.Add(new Entity(i));

			// シャッフル
			_entities = _entities.OrderBy(n => n.Guid).ToList();
		}

		[Benchmark]
		public void Select()
		{
			var array = _entities.Select(n => n.No.ToString()).ToArray();
			if (array.Length != LEN) throw new Exception();
		}

		[Benchmark]
		public void Where()
		{
			var list = _entities.Where(n => n.No % 2 == 0).ToList();
			if (list.Count != LEN / 2) throw new Exception();
		}

		[Benchmark]
		public void OrderBy()
		{
			var list = _entities.OrderBy(n => n.No).ToList();
			if (list[0].No != 1 || list[LEN - 1].No != LEN) throw new Exception();
		}

		[Benchmark]
		public void Max()
		{
			var max = _entities.Max(n => n.No);
			if (max != LEN) throw new Exception();
		}

		[Benchmark]
		public void Sum()
		{
			long sum = _entities.Sum(n => n.No);
			if (sum != 50005000) throw new Exception();
		}
	}

	internal class Entity
	{
		public int No { get; set; }
		public string Guid { get; set; }

		public Entity(int index)
		{
			No = index + 1;
			Guid = System.Guid.NewGuid().ToString();
		}
	}
}
```

# 測定データ

## .NET Framework 4.7.2

| Method  | Mean        | Allocated |
|-------- |------------:|----------:|
| Select  | 1,296.70 us |  407,668 B |
| Where   |   203.97 us |   65,800 B |
| OrderBy | 1,834.87 us |  251,608 B |
| Max     |   107.30 us |       49 B |
| Sum     |    86.47 us |       49 B |

## .NET Framework 4.8

| Method  | Mean        | Allocated |
|-------- |------------:|----------:|
| Select  |   862.35 us |  407,666 B |
| Where   |   200.16 us |   65,800 B |
| OrderBy | 1,784.52 us |  251,608 B |
| Max     |   107.19 us |       49 B |
| Sum     |    85.12 us |       49 B |

## .NET Framework 4.8.1

| Method  | Mean        | Allocated |
|-------- |------------:|----------:|
| Select  |   855.14 us |  407,662 B |
| Where   |   199.83 us |   65,800 B |
| OrderBy | 1,776.46 us |  251,608 B |
| Max     |   103.94 us |       49 B |
| Sum     |    84.93 us |       49 B |

## .NET 6.0.33

| Method  | Mean        | Allocated |
|-------- |------------:|----------:|
| Select  |   238.06 us |  399,808 B |
| Where   |   117.12 us |  131,432 B |
| OrderBy | 1,428.17 us |  240,309 B |
| Max     |    99.89 us |       40 B |
| Sum     |   101.97 us |       40 B |

## .NET 7.0.20

| Method  | Mean       | Allocated |
|-------- |-----------:|----------:|
| Select  |   229.5 us |  399,808 B |
| Where   |   118.8 us |  131,432 B |
| OrderBy | 1,400.1 us |  240,305 B |
| Max     |   106.2 us |       40 B |
| Sum     |   105.8 us |       40 B |

## .NET 8.0.8

| Method  | Mean      | Allocated |
|-------- |----------:|----------:|
| Select  | 182.57 us |  390,528 B |
| Where   | 102.87 us |  131,432 B |
| OrderBy | 850.84 us |  240,304 B |
| Max     |  52.27 us |       40 B |
| Sum     |  55.74 us |       40 B |

### 凡例

- `Mean`: Arithmetic mean of all measurements
- `Error`: Half of 99.9% confidence interval
- `StdDev`: Standard deviation of all measurements
- `Allocated`: Allocated memory per single operation (managed only, inclusive, 1KB = 1,024B)
- `1 us`: 1 Microsecond (0.000001 sec)

# おわりに

検証した LINQ メソッドの全てにおいて速い結果となった .NET 8 は、後続の .NET 9、.NET 10 にてさらなる速度改善が期待されています。パフォーマンス面では、今後の開発において .NET Framework 系を選択する理由はないように思います。