ORM性能测试Benchmark(最终版)
ORM性能测试Benchmark(最终版)
本测试聚焦 ORM 在查询过程中,对查询表达式解析、数据映射、流程处理的性能差异。
由于 SQL 的实际执行由数据库引擎负责,ORM 无法改变数据库层面的执行逻辑;不同 ORM 的差异主要体现在 SQL 拼接、表达式解析和数据映射等实现细节(例如插入操作可通过生成 SQL 或使用 BulkCopy 实现)。
因此,本测试不对实现方式完全不同的操作(如 BulkCopy)进行比较,而是重点衡量表达式解析与数据映射两方面的运行效率与内存占用。
测试声明
本测试不代表任何立场和原作者也没任何关系,仅是在研究、学习、优化、测试,对内部项目myTest整理过程中形成的测试,有其它测式可下载源码自行添加实现。
测试环境
| 项目 | 说明 |
|---|---|
| 测试框架 | BenchmarkDotNet |
| 测试数据库 | SQLite(单机性能优,波动较小) |
| .NET 版本 | .NET 6.0+ |
| 测试硬件 | Intel Core i5-8265U CPU 1.60GHz |
测试的ORM
下面列出了近年收集到的ORM,除Dapper外,未涉及到表达式解析的ORM没有加入此测试
<PackageReference Include="Chloe.SQLite" Version="5.55.0" />
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="FreeSql.Provider.Sqlite" Version="3.5.305" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0-preview.6.23329.4" />
<PackageReference Include="SqlSugarCore" Version="5.1.4.212-preview02" />
<PackageReference Include="linq2db" Version="6.2.0" />
<Reference Include="Fast.Framework">
具体测试项目如下
- 对表达式解析进行测试 testQueryCondition
- 对查询返回结果进行数据映射测试 testQueryResult
- 自定义数据映射测试 testQueryAnonymousResult
- 循环读取指定数据测试 testQueryLoop
测试代码明细
对表达式解析进行测试 testQueryCondition
指定了查询条件和返回结果筛选
这里可以反映表达式解析生成SQL效率,ORM技术核心最困难的部份,非常考验代码组织和逻辑
query.Where(b => b.F_String == "111" && b.F_Decimal > 0 && b.F_Bool == true && b.F_String.StartsWith("abc")).Select(b => new { b.F_Float, b.F_Bool, b.F_Double, b.F_Byte, b.F_String, b.F_Decimal, b.F_Int64 }).ToSqlString();
对查询返回结果进行数据映射测试 testQueryResult
返回指定的数量的数据进行数据映射,以测试映射效率和内存使用情况
query.Take(100).ToList();
自定义数据映射测试 testQueryAnonymousResult
指定数据映射的结构,以测试解析和映射效率、内存使用情况
这里使用了匿名对象,是一个比较特殊的处理
query.Take(100).Select(b => new
{
b.Id,
b.F_Float,
b.F_Bool,
b.F_DateTime,
b.F_Decimal,
b.F_Double,
b.F_Int64
}).ToList();
循环读取指定数据测试 testQueryLoop
循环多次查询,以测试查询和映射效率
这里验证数据连接效率和查询效率,虽然每次查询的数据量很小,但循环多次会放大差异,能更明显的看出差别
for (var i = 0; i < 20; i++)
{
var item = query.Where(b => b.Id == i).ToList();
}
Benchmark测试代码样例
[MemoryDiagnoser]
public class ConditionTest : TestBase
{
[Benchmark]
public void TestCondition()
{
Invoke(b => b.testQueryCondition());
}
}
以下是具体测试结果,仅供参考
表格字段说明:
- Mean: 所有测量值的算术平均值 ns纳秒 μs微秒 ms毫秒。
- Error: 99.9% 置信区间的一半。
- StdDev: 所有测量值的标准差。
- Gen0: 第 0 代 GC 每 1000 次操作收集一次。
- Gen1: 第 1 代 GC 每 1000 次操作收集一次。
- Gen2: 第 2 代 GC 每 1000 次操作收集一次。
- Allocated: 每次操作分配的内存(仅托管内存,包含所有内容,1KB = 1024B)。
TestCondition
Dapper由于没有表达式解析,空跑,最低和最高不管是效率和内存占用差了一数量级
提示:拼接SQL会导致注入风险,除了语法字符外还有字符编码问题
效率 : 最低最高差近18倍
内存占用 : 最低最高差近6倍
| Method | ProvideType | Mean | Error | StdDev | Rank | Gen0 | Gen1 | Allocated |
|---|---|---|---|---|---|---|---|---|
| TestCondition | ChloeTest | 83.68 μs | 0.624 μs | 0.584 μs | 2 | 5.3711 | - | 16.65 KB |
| TestCondition | EfSqlliteTest | 218.78 μs | 2.815 μs | 2.351 μs | 5 | 19.5313 | - | 61.31 KB |
| TestCondition | FastFrameworkTest | 91.21 μs | 0.987 μs | 0.825 μs | 3 | 6.5918 | - | 20.86 KB |
| TestCondition | FreeSqlTest | 736.85 μs | 11.122 μs | 9.859 μs | 7 | 9.7656 | 3.9063 | 62.71 KB |
| TestCondition | LinqToDbTest | 99.93 μs | 1.846 μs | 2.706 μs | 4 | 6.1035 | - | 18.94 KB |
| TestCondition | MyTest | 42.79 μs | 0.705 μs | 0.589 μs | 1 | 4.8828 | - | 15.15 KB |
| TestCondition | SqlSugarTest | 355.77 μs | 7.084 μs | 6.280 μs | 6 | 33.6914 | - | 103.98 KB |
TestResult
强类型直接转换,最低和最高差别两倍,EF效率比大部份好
效率 : 最低最高差近20倍
内存占用 : 最低最高差近6倍
| Method | ProvideType | Mean | Error | StdDev | Rank | Gen0 | Gen1 | Allocated |
|---|---|---|---|---|---|---|---|---|
| TestResult | ChloeTest | 552.8 μs | 4.98 μs | 3.89 μs | 2 | 22.4609 | - | 70.68 KB |
| TestResult | DapperTest | 494.8 μs | 3.81 μs | 3.18 μs | 1 | 17.0898 | - | 52.68 KB |
| TestResult | EfSqlliteTest | 1,031.9 μs | 3.50 μs | 2.92 μs | 3 | 64.4531 | - | 202.9 KB |
| TestResult | FastFrameworkTest | 11,530.5 μs | 224.13 μs | 380.59 μs | 5 | 46.8750 | - | 153.21 KB |
| TestResult | FreeSqlTest | 1,266.6 μs | 9.07 μs | 8.48 μs | 4 | 31.2500 | 11.7188 | 96.32 KB |
| TestResult | LinqToDbTest | 1,239.5 μs | 24.28 μs | 42.52 μs | 4 | 17.5781 | - | 54.6 KB |
| TestResult | MyTest | 544.1 μs | 5.84 μs | 4.87 μs | 2 | 10.7422 | - | 35.6 KB |
| TestResult | SqlSugarTest | 1,307.7 μs | 17.64 μs | 14.73 μs | 4 | 46.8750 | - | 147.57 KB |
TestAnonymousResult
Dapper由于没有结果筛选,同TestResult(SqlSugar内存溢出?)
效率 : 最低最高差近18倍
内存占用 : 最低最高差近94倍
| Method | ProvideType | Mean | Error | StdDev | Rank | Gen0 | Gen1 | Allocated |
|---|---|---|---|---|---|---|---|---|
| TestAnonymousResult | ChloeTest | 529.8 μs | 7.58 μs | 6.72 μs | 2 | 20.5078 | - | 65.54 KB |
| TestAnonymousResult | DapperTest | 477.9 μs | 9.07 μs | 8.04 μs | 1 | 17.5781 | - | 55.8 KB |
| TestAnonymousResult | EfSqlliteTest | 664.7 μs | 8.19 μs | 6.84 μs | 3 | 33.2031 | - | 102.76 KB |
| TestAnonymousResult | FastFrameworkTest | 7,864.6 μs | 113.19 μs | 105.88 μs | 6 | 31.2500 | - | 122.4 KB |
| TestAnonymousResult | FreeSqlTest | 1,939.4 μs | 26.29 μs | 24.59 μs | 5 | 70.3125 | 15.6250 | 225.84 KB |
| TestAnonymousResult | LinqToDbTest | 1,222.5 μs | 11.43 μs | 8.93 μs | 4 | 15.6250 | - | 52.91 KB |
| TestAnonymousResult | MyTest | 465.8 μs | 7.98 μs | 7.07 μs | 1 | 10.7422 | - | 34.24 KB |
| TestAnonymousResult | SqlSugarTest | 8,546.3 μs | 77.17 μs | 64.44 μs | 7 | 1046.8750 | - | 3210.65 KB |
TestQueryLoop
循环多次查询调用,由于调用sqlite驱动不同,时间差别较大,但从内存使用上能看出差别
效率 : 最低最高差近100倍
内存占用 : 最低最高差近43倍
| Method | ProvideType | Mean | Error | StdDev | Rank | Gen0 | Gen1 | Allocated |
|---|---|---|---|---|---|---|---|---|
| TestQueryLoop | ChloeTest | 2.733 ms | 0.0386 ms | 0.0323 ms | 3 | 70.3125 | - | 235.21 KB |
| TestQueryLoop | DapperTest | 1.478 ms | 0.0291 ms | 0.0347 ms | 1 | 17.5781 | - | 54.7 KB |
| TestQueryLoop | EfSqlliteTest | 6.118 ms | 0.0766 ms | 0.0679 ms | 5 | 375.0000 | - | 1159.75 KB |
| TestQueryLoop | FastFrameworkTest | 231.701 ms | 2.7356 ms | 2.2843 ms | 8 | 666.6667 | - | 2332.27 KB |
| TestQueryLoop | FreeSqlTest | 15.951 ms | 0.2320 ms | 0.2171 ms | 6 | 93.7500 | 31.2500 | 664.93 KB |
| TestQueryLoop | LinqToDbTest | 17.132 ms | 0.2095 ms | 0.1635 ms | 7 | 125.0000 | - | 413.22 KB |
| TestQueryLoop | MyTest | 2.077 ms | 0.0307 ms | 0.0256 ms | 2 | 42.9688 | - | 138.73 KB |
| TestQueryLoop | SqlSugarTest | 4.311 ms | 0.0360 ms | 0.0301 ms | 4 | 210.9375 | - | 651.19 KB |
引入不同数据库实现的方式
本次测试使用的sqlite数据库,各种引入的方式也不相司,大致分为三类:
- 引用相关数据库的项目扩展包(ORM主体+扩展+数据库驱动)
- 直接包含所有支持的数据库驱动(ORM主体+数据库驱动*N)
- 按需自动或手动配置引入的数据库驱动(ORM主体+按需数据库驱动)
对于第一种,多数项目都采用这种方式,缺点是需要引入多个包,增加了依赖关系,并且强绑定了数据库驱动
对于第二种,虽然可以支持多种数据库,但会增加项目的体积,并且可能引入不必要的依赖,也强绑定了数据库驱动
对于第三种,封装度最高,虽然可以按需引入数据库驱动,减少了项目的体积和依赖复杂度,但由于没有强依赖,部份实现可能由于驱动不一致而实现复杂,如 BulkCopy
以LinqToDb为例,直接引入LinqToDb包,手动配置连接串 new DataOptions().UseConnectionString(ProviderName.SQLite, ITest.sqlLiteDb) 它就能自别识别当前项目引入的sqlite驱动,并且对特殊方法也进行了封装(BulkCopy),不管是哪种数据库,引入驱动后所有方法行为一至,无其它依赖。
各种测试项目的引入方式如下表所示:
| ProvideType | 引入方式 |
|---|---|
| ChloeTest | 扩展包&手动配置 |
| DapperTest | 扩展包 |
| EfSqlliteTest | 扩展包 |
| FastFrameworkTest | 手动配置 |
| FreeSqlTest | 扩展包 |
| MyTest | 手动或自动配置 |
| SqlSugarTest | 全包含 |
| LinqToDbTest | 自动配置 |
如何使用此测试
- 使用Release发布此项目
- 运行dbTest.exe
- 输入序号选择需要运行测试的方法,示例如下
---------------------[ Program ]---------------------
[1] testQueryResult [2] testQueryAnonymousResult
[3] testQueryCondition [4] testQueryLoop
[5] testMethod
---------------------[ invokeAll ]---------------------
[6] invokeAll
invoke method:
- 输入序号后回车,等待测试完成,测试结果会输出到控制台,并在运行目录生成BenchmarkDotNet.Artifacts文件夹,里面有详细的测试结果和分析报告
- 运行效果截图
![dbTest.gif]()
测试项目代码
https://gitee.com/hubro/dbTest.git
参考
使用 BenchmarkDotNet 对 .NET 代码进行性能基准测试
https://cloud.tencent.com/developer/article/2483382
mysql注入-字符编码技巧
https://developer.aliyun.com/article/1658273


浙公网安备 33010602011771号