.NET 8 + ASP.NET Core + EFCoreでどれだけ起動が早くなったか確認する

## はじめに

以前この記事で.NET + EFCoreは遅いという記事を見てからずっとやろうと思っていたんですが、[2つ前の記事で.NET 8.0 RC1のDevContainerを作った](https://qiita.com/karuakun/items/4695cfbdfbf623600c98)ので、.NET 8.0 RC1でどうなったか確認してみました。

https://qiita.com/fits/items/b3483bc3c79e59dc4c28

## サンプルコード

今回は.NET 7.0, .NET 8.0 RC1, .NET 8.0 RC1 AOTで下記のパターンを5回計測しました。
- データアクセス無し（単に現在時刻を返すだけ）
- EFCoreで単一テーブルを主キーで検索
- Dapperで単一テーブルを主キーで検索
- ADO.NETで単一テーブルを主キーで検索

ソースは以下のようになっています。
ソースコードは[ここ](https://github.com/karuakun/SandBox/tree/master/DotNetSpeedTest)からで確認できます。
ただし、.NET 8.0 RC1 AOTではEFCoreやDapperは実行時に異常終了したり、そもそももとにしているHostBuilderも違うので[GitHubのソースコード](https://github.com/karuakun/SandBox/blob/master/DotNetSpeedTest/App8Aot/Program.cs)を確認してください。

https://github.com/karuakun/SandBox/tree/master/DotNetSpeedTest

```cs: Program.cs
using Dapper;
using Microsoft.EntityFrameworkCore;
using MySqlConnector;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data;

var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("db");
builder.Services.AddScoped<IDbConnection>(_ => new MySqlConnection(connectionString));
builder.Services.AddDbContext<ItemContext>(opt =>
{
    opt.UseMySql(connectionString, ServerVersion.Parse("8.0"))
       .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
});

var app = builder.Build();
app.Map("/", () => $"aspnet{System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription}-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}");
app.MapPost("/find", async (ItemContext db, FindItem input) =>
    await db.Items.Where(r => r.Price >= input.Price).OrderBy(r => r.Price).ToArrayAsync()
);
app.MapPost("/findQuery", async (IDbConnection db, FindItem input) =>
    (await db.QueryAsync<Item>("select id, price from items where price = @price order by price", new { price = input.Price })).ToArray()
);
app.MapPost("/findQueryRaw", (IDbConnection db, FindItem input) =>
{
    if (db.State != ConnectionState.Open)
        db.Open();
    using var command = db.CreateCommand();
    command.CommandText = $"select id, price from items where price = {input.Price} order by price";
    using var reader = command.ExecuteReader();
    var result = new List<Item>();
    while (reader.Read())
    {
        result.Add(new Item
        {
            Id = reader.GetString(0),
            Price = reader.GetInt32(1)
        });
    }
    return result.ToArray();
});

app.Run();

record FindItem(int Price);

[Table("items")]
public class Item
{
    [Column("id")]
    public required string Id { get; set; }

    [Column("price")]
    public int Price { get; set; }
}

public class ItemContext : DbContext
{
    public DbSet<Item> Items => Set<Item>();

    public ItemContext(DbContextOptions<ItemContext> opts) : base(opts) { }
}
```

## 計測結果

.NET 8.0 RC1は現時点では.NET 7.0よりも遅いですね。これは現時点ではチューニングの最終段階が完了していないのとEFCoreもミドルウェアの関係上7.0を利用しているためだと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/2ab11648-1d64-a358-fab0-a11c48e13f71.png)
ざっくり見ていくと、素のASP.NETではAOTがかなり効きますね。
初回アクセスが2回目以降と変わりがないです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/38106f3c-561c-446f-d49e-8b948613f1bf.png)
EFCore、Dapper、素のADO.NETで比べると、やっぱりEFCoreの遅さが目立ちますね。

初回アクセス時にEFCoreは
- Dapperに比べて2.5倍遅い
- 素のADO.NETに比べ3.5倍遅い

2回目以降のアクセス時にEFCoreは
- Dapperに比べて2.5倍遅い
- 素のADO.NETに比べ2.5倍遅い
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/dbaaeb98-e610-7180-5750-456678875b0d.png)
AOTするとリフレクションなどの動的なコード生成が失敗するので、現時点ではEFCoreやDapperは実行時に異常終了します。ADO.NETを素で使った場合だけ計測できたので載せています。こちらも初回アクセスが早くなっていますね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36535/f66632fb-7294-dc99-926e-0bb2be0e93a7.png)

## おわりに

ということで、タイトルの答えとしては.NET 8.0 RC1の段階では.NET 7.0からそう変わらないという状況になりました。ただし、RC1から製品になるのと、ミドルウェアがEFCore 8.0に対応すると変わってくるかもしれません。