はじめに
以前この記事で.NET + EFCoreは遅いという記事を見てからずっとやろうと思っていたんですが、2つ前の記事で.NET 8.0 RC1のDevContainerを作ったので、.NET 8.0 RC1でどうなったか確認してみました。
サンプルコード
今回は.NET 7.0, .NET 8.0 RC1, .NET 8.0 RC1 AOTで下記のパターンを5回計測しました。
- データアクセス無し(単に現在時刻を返すだけ)
- EFCoreで単一テーブルを主キーで検索
- Dapperで単一テーブルを主キーで検索
- ADO.NETで単一テーブルを主キーで検索
ソースは以下のようになっています。
ソースコードはここからで確認できます。
ただし、.NET 8.0 RC1 AOTではEFCoreやDapperは実行時に異常終了したり、そもそももとにしているHostBuilderも違うのでGitHubのソースコードを確認してください。
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を利用しているためだと思います。
ざっくり見ていくと、素のASP.NETではAOTがかなり効きますね。
初回アクセスが2回目以降と変わりがないです。
EFCore、Dapper、素のADO.NETで比べると、やっぱりEFCoreの遅さが目立ちますね。
初回アクセス時にEFCoreは
- Dapperに比べて2.5倍遅い
- 素のADO.NETに比べ3.5倍遅い
2回目以降のアクセス時にEFCoreは
- Dapperに比べて2.5倍遅い
- 素のADO.NETに比べ2.5倍遅い
AOTするとリフレクションなどの動的なコード生成が失敗するので、現時点ではEFCoreやDapperは実行時に異常終了します。ADO.NETを素で使った場合だけ計測できたので載せています。こちらも初回アクセスが早くなっていますね。
おわりに
ということで、タイトルの答えとしては.NET 8.0 RC1の段階では.NET 7.0からそう変わらないという状況になりました。ただし、RC1から製品になるのと、ミドルウェアがEFCore 8.0に対応すると変わってくるかもしれません。