Open niikoo opened 7 months ago
This is definitely a problem, does anyone have information about potential fixes?
Bump!
I also noticed a major performance issue with .NET 8 after migrating from .NET Framework.
Do you have any advices on this @alex-kulakov @SergeiPavlov?
Hello everyone, @niikoo, @gkverneland, @ketiovv
As far as I remember 7.1 is declared and build for NET 5 and 6. So we didn't test it on NET 7 nor NET 8 environment.
Can you provide some exact scenarios where it is noticeable so we could track what possible changes may be the cause. Just regular queries, some of them or all of them? Would be good if you and me had the same benchmark query or something to check it.
Is this include only translation or entire roundtrip from executing Expression to getting results?
Chose some query or something else that bothers you and post some performance numbers for 1) 7.1.1 on .Net Framework 4.8 2) 7.1.1 on .Net 7
I will try to reproduce it all along on latest 6.0, 7.0 and 7.1 versions and post some numbers too. Let's figure it out.
Secondly, I need more info about DeadlockException to investigate it, just saying that deadlock appears gives me zero information about how to at least reproduce scenarios so we could fix it.
Hi @alex-kulakov
Thanks for you positive response.
We have noticed worse performance across the system without being able to identify exactly where the difference is.
You mentioned running 7.1.1 on .Net Framework 4.8, but it does not look like 7.1.1 targets that framework. Would it be possible to make version 6 run on .net 8? That would make it easier for us to do a fair comparison.
Thanks in advance.
Hi @gkverneland,
Pardon me, you are right about 7.1.1 on .Net Framework.
If you say it is all across the system then it might be also new framework contributing to the issue, because, for example, Domain building process wasn't changed drastically, there were some improvements, thanks to Sergei and the company he works for (their project is big and they really fight for performance of DataObjects), but nothing really big happened with domain building.
We can compare domain building for starter, I have a model of 200 entities and around 10 associations per entity, I believe that it would be good for performance measurements. To eliminate variable of network interactions and database actions performance I would choose building domain in Skip mode, another option is to stop domain building right before translated upgrade actions executed. this would also eliminate impact of database almost completely.
Speaking of query translation, there was serious change of last step of translation - when we visit SqlDom expressions to translate them to result string for DbCommand. I remember that I checked performance of those changes on a number of query cases and it was at least not worse than before, memory efficiency increased though.
I have everything I need to compare domain building performance. I will start with it for following cases:
1) latest 6.0 version on .Net Framework it is compatible with 2) latest 6.0 version on .Net Core 2.0 3) latest 7.0 version on .Net Core 3.1 (and on other compatible. NETs, if it is possible) 4) latest 7.1 version on .NET 5 5) latest 7.1 version on .NET 6 4) latest 7.1 version on .NET 7 (should be possible) 4) latest 7.1 version on .NET 8 (if possible, I believe it is)
Query translation is a different story, it really depends on complexity of queries because it affects size of expression tree and also methods that are executed throughout translation. I would like to have an example of queries you usually execute in your project as a benchmark. The provider (sqlserver, postgresql, mysql, etc.) you use is important because translation to result string is in assembly of provider. So I need this info from you.
Unfortunately, I couldn't run all the benchmarks for now only results for DO v6.0. Projects for different frameworks and DataObjects.Net versions are ready but I haven't got enough time to gather all results. I could publish the projects I use for benchmarking, but later.
Here is a portion of numbers of the same model set and two ways of building Domain 1) in Skip mode - full Domain build, in this mode DO does minimal interaction with database 2) in Recreate mode - I intentionally interrupt building before any upgrade actions executed (table/indexes/foreign key creations) to eliminate network and database performance impact, they are translated to strings though.
Domain model contains 200 of entities similar to this
[HierarchyRoot]
[Index("Int16Field")]
[Index("Int32Field")]
[Index("Int64Field")]
[Index("FloatField")]
[Index("DoubleField")]
public class TestEntity12 : Entity
{
[Key, Field]
public long Id{get;set;}
[Field]
public bool BooleanField {get;set;}
[Field]
public Int16 Int16Field {get;set;}
[Field]
public Int32 Int32Field {get;set;}
[Field]
public Int64 Int64Field {get;set;}
[Field]
public float FloatField {get;set;}
[Field]
public double DoubleField {get;set;}
[Field]
public DateTime DateTimeField {get;set;}
[Field]
public string StringField {get;set;}
[Field]
[FullText("English")]
public string Text {get;set;}
[Field]
public TestEntity11 TestEntity11{get;set;}
[Field]
public TestEntity10 TestEntity10{get;set;}
[Field]
public TestEntity9 TestEntity9{get;set;}
[Field]
public TestEntity8 TestEntity8{get;set;}
[Field]
public TestEntity7 TestEntity7{get;set;}
[Field]
public TestEntity6 TestEntity6{get;set;}
[Field]
public TestEntity5 TestEntity5{get;set;}
[Field]
public TestEntity4 TestEntity4{get;set;}
[Field]
public TestEntity3 TestEntity3{get;set;}
[Field]
public TestEntity2 TestEntity2{get;set;}
[Field]
public TestEntity1 TestEntity1{get;set;}
}
I used SQL Server 2016 as database (installed on the same PC). My PC specs: Windows 10 (10.0.19045.4046/22H2/2022Update) Intel Core i5-8400 CPU 2.80GHz (Coffee Lake), 1 CPU, 6 logical and 6 physical cores I used BenchmarkDotNet v0.13.12
Benchmarks use cold start and 1 run to eliminate possible cache influence. Every table below is a result of one run of executable file.
Number of projects targeting different framework used the same packages of DataObjects.Net (v 6.0.12).
So...
Skip Mode results
Method | Mean | Error | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|
Build | 1.545 s | NA | 25000.0000 | 19000.0000 | 4000.0000 | 134.71 MB |
Method | Mean | Error | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|
Build | 1.539 s | NA | 25000.0000 | 19000.0000 | 4000.0000 | 134.78 MB |
Method | Mean | Error | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|
Build | 1.549 s | NA | 25000.0000 | 19000.0000 | 4000.0000 | 134.71 MB |
Recreate mode results
Method | Mean | Error | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|
Build | 3.261 s | NA | 149000.0000 | 91000.0000 | 5000.0000 | 767.82 MB |
Method | Mean | Error | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|
Build | 3.247 s | NA | 150000.0000 | 90000.0000 | 5000.0000 | 768.02 MB |
Method | Mean | Error | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|
Build | 3.269 s | NA | 150000.0000 | 91000.0000 | 5000.0000 | 768.07 MB |
Skip Mode results
Method | Mean | Error | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|
Build | 1.828 s | NA | 37000.0000 | 12000.0000 | 2000.0000 | 216.93 MB |
Method | Mean | Error | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|
Build | 1.820 s | NA | 37000.0000 | 12000.0000 | 2000.0000 | 216.93 MB |
Method | Mean | Error | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|
Build | 1.797 s | NA | 37000.0000 | 12000.0000 | 2000.0000 | 216.93 MB |
Recreate mode results
Method | Mean | Error | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|
Build | 3.543 s | NA | 237000.0000 | 54000.0000 | 4000.0000 | 1.22 GB |
Method | Mean | Error | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|
Build | 3.561 s | NA | 237000.0000 | 51000.0000 | 4000.0000 | 1.22 GB |
Method | Mean | Error | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|
Build | 3.544 s | NA | 237000.0000 | 51000.0000 | 4000.0000 | 1.22 GB |
Skip Mode results
Method | Mean | Error | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|
Build | 1.821 s | NA | 35000.0000 | 11000.0000 | 1000.0000 | 206.97 MB |
Method | Mean | Error | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|
Build | 1.825 s | NA | 35000.0000 | 12000.0000 | 1000.0000 | 206.97 MB |
Method | Mean | Error | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|
Build | 1.822 s | NA | 35000.0000 | 12000.0000 | 1000.0000 | 206.98 MB |
Recreate mode results
Method | Mean | Error | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|
Build | 3.533 s | NA | 228000.0000 | 50000.0000 | 2000.0000 | 1.17 GB |
Method | Mean | Error | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|
Build | 3.507 s | NA | 228000.0000 | 50000.0000 | 2000.0000 | 1.17 GB |
Method | Mean | Error | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|
Build | 3.518 s | NA | 228000.0000 | 50000.0000 | 2000.0000 | 1.17 GB |
Skip Mode results
Method | Mean | Error | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|
Build | 1.890 s | NA | 35000.0000 | 12000.0000 | 1000.0000 | 204.41 MB |
Method | Mean | Error | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|
Build | 1.876 s | NA | 35000.0000 | 12000.0000 | 1000.0000 | 204.41 MB |
Method | Mean | Error | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|
Build | 1.873 s | NA | 35000.0000 | 12000.0000 | 1000.0000 | 204.38 MB |
Recreate mode results
Method | Mean | Error | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|
Build | 3.760 s | NA | 223000.0000 | 48000.0000 | 2000.0000 | 1.15 GB |
Method | Mean | Error | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|
Build | 3.685 s | NA | 222000.0000 | 50000.0000 | 2000.0000 | 1.15 GB |
Method | Mean | Error | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|
Build | 3.757 s | NA | 223000.0000 | 50000.0000 | 2000.0000 | 1.15 GB |
Skip Mode results
Method | Mean | Error | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|
Build | 1.787 s | NA | 37000.0000 | 14000.0000 | 3000.0000 | 203.83 MB |
Method | Mean | Error | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|
Build | 1.802 s | NA | 37000.0000 | 14000.0000 | 3000.0000 | 203.83 MB |
Method | Mean | Error | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|
Build | 1.796 s | NA | 37000.0000 | 14000.0000 | 3000.0000 | 203.85 MB |
Recreate mode results
Method | Mean | Error | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|
Build | 3.849 s | NA | 221000.0000 | 48000.0000 | 1000.0000 | 1.14 GB |
Method | Mean | Error | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|
Build | 3.814 s | NA | 221000.0000 | 41000.0000 | 1000.0000 | 1.14 GB |
Method | Mean | Error | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|
Build | 3.833 s | NA | 221000.0000 | 41000.0000 | 1000.0000 | 1.14 GB |
Skip Mode results
Method | Mean | Error | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|
Build | 1.806 s | NA | 35000.0000 | 12000.0000 | 1000.0000 | 203.36 MB |
Method | Mean | Error | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|
Build | 1.818 s | NA | 35000.0000 | 12000.0000 | 1000.0000 | 203.36 MB |
Method | Mean | Error | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|
Build | 1.807 s | NA | 35000.0000 | 12000.0000 | 1000.0000 | 203.36 MB |
Recreate mode results
Method | Mean | Error | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|
Build | 4.024 s | NA | 217000.0000 | 46000.0000 | 1000.0000 | 1.13 GB |
Method | Mean | Error | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|
Build | 4.010 s | NA | 217000.0000 | 46000.0000 | 1000.0000 | 1.13 GB |
Method | Mean | Error | Gen0 | Gen1 | Gen2 | Allocated |
---|---|---|---|---|---|---|
Build | 4.064 s | NA | 217000.0000 | 45000.0000 | 1000.0000 | 1.13 GB |
I have finished domain build benchmarking.
Probably I should've explained why I chose Skip
mode and Recreate
mode for benchmarking. I'll explain now.
Skip. The key feature of Skip upgrade mode is that it assumes that database has already been upgraded. This assumption allows us to skip extraction of database structure and comparison of current domain model state with the state of database. Because of low overhead, this is great mode to benchmark Domain model building process. This is important because Domain model lives in memory the whole life of Domain itself and Domain is assumed to live almost entire life of application.
Recreate. This mode drops everything it can in database and creates entire structure of database. If it is done on already empty database it benchmarks states comparison process (current state of domain and current state of database). It also benchmarks efficiency of CREATE query translation (tables, foreign keys, indexes, etc.), in other words it benchmarks translation to string (to a certain extend).
Why I didn't use Perform
/PerformSafely
mode? These modes require to have certain state of database which leads to database structure extraction but I would like to exclude this network and storage interactions from result values.
Disclamer.
Error
column and Gen0
-Gen2
columns, so we look at time and memory consumption, to me they are insignificant for current discussion.For better understanding of how different versions of code perform I will arrange tables of results by Framework where every table is one of versions of DataObjects.Net.
Our base line is .Net Framework 4.8 with DO 6.0.12, which looks like this
Method | Mean | Allocated | Method | Mean | Allocated |
---|---|---|---|---|---|
Build | 1.545 s | 134.71 MB | Build | 3.261 s | 767.82 MB |
Build | 1.539 s | 134.78 MB | Build | 3.247 s | 768.02 MB |
Build | 1.549 s | 134.71 MB | Build | 3.269 s | 768.07 MB |
Let's start with .NET 8
.NET 8 (.NET 8.0.0 (8.0.23.53103))
v6.0.12
Method | Mean | Allocated | Method | Mean | Allocated |
---|---|---|---|---|---|
Build | 1.806 s | 203.36 MB | Build | 4.024 s | 1.13 GB |
Build | 1.818 s | 203.36 MB | Build | 4.010 s | 1.13 GB |
Build | 1.807 s | 203.36 MB | Build | 4.064 s | 1.13 GB |
v7.0.0
Method | Mean | Allocated | Method | Mean | Allocated |
---|---|---|---|---|---|
Build | 1.850 s | 199.26 MB | Build | 3.909 s | 1.05 GB |
Build | 1.842 s | 199.27 MB | Build | 3.801 s | 1.05 GB |
Build | 1.817 s | 199.26 MB | Build | 3.771 s | 1.05 GB |
Build | 1.834 s | 199.26 MB | Build | 3.800 s | 1.05 GB |
Build | 1.824 s | 199.26 MB | Build | 3.808 s | 1.05 GB |
Build | 1.833 s | 199.27 MB | Build | 3.813 s | 1.05 GB |
v7.0.1
Method | Mean | Allocated | Method | Mean | Allocated |
---|---|---|---|---|---|
Build | 2.200 s | 187.88 MB | Build | 4.412 s | 1.03 GB |
Build | 2.151 s | 187.87 MB | Build | 4.345 s | 1.03 GB |
Build | 2.138 s | 187.87 MB | Build | 4.398 s | 1.03 GB |
Build | 2.190 s | 187.88 MB | Build | 4.454 s | 1.03 GB |
Build | 2.199 s | 187.88 MB | Build | 4.212 s | 1.03 GB |
Build | 2.171 s | 187.86 MB | Build | 4.345 s | 1.03 GB |
v7.0.3
Method | Mean | Allocated | Method | Mean | Allocated |
---|---|---|---|---|---|
Build | 2.258 s | 187.86 MB | Build | 4.491 s | 1.03 GB |
Build | 2.241 s | 187.88 MB | Build | 4.345 s | 1.03 GB |
Build | 2.229 s | 187.87 MB | Build | 4.501 s | 1.03 GB |
Build | 2.223 s | 187.86 MB | Build | 4.381 s | 1.03 GB |
Build | 2.253 s | 187.87 MB | Build | 4.262 s | 1.03 GB |
Build | 2.238 s | 187.87 MB | Build | 4.246 s | 1.03 GB |
v7.1.1
Method | Mean | Allocated | Method | Mean | Allocated |
---|---|---|---|---|---|
Build | 2.251 s | 170.8 MB | Build | 3.868 s | 671.71 MB |
Build | 2.200 s | 170.8 MB | Build | 3.932 s | 671.71 MB |
Build | 2.248 s | 170.81 MB | Build | 3.876 s | 671.7 MB |
Build | 2.220 s | 170.8 MB | Build | 3.874 s | 671.73 MB |
Build | 2.185 s | 170.81 MB | Build | 3.850 s | 671.71 MB |
Build | 2.230 s | 170.81 MB | Build | 3.979 s | 671.7 MB |
Build | 2.219 s | 170.81 MB | Build | 4.048 s | 671.73 MB |
Build | 2.174 s | 170.8 MB | Build | 3.788 s | 671.72 MB |
Build | 2.201 s | 170.8 MB | Build | 3.892 s | 671.71 MB |
Build | 2.227 s | 170.8 MB | Build | 3.879 s | 671.7 MB |
v6.0.12
Method | Mean | Allocated | Method | Mean | Allocated |
---|---|---|---|---|---|
Build | 1.787 s | 203.83 MB | Build | 3.849 s | 1.14 GB |
Build | 1.802 s | 203.83 MB | Build | 3.814 s | 1.14 GB |
Build | 1.796 s | 203.85 MB | Build | 3.833 s | 1.14 GB |
v7.0.0
Method | Mean | Allocated | Method | Mean | Allocated |
---|---|---|---|---|---|
Build | 1.900 s | 199.74 MB | Build | 3.784 s | 1.06 GB |
Build | 1.869 s | 199.73 MB | Build | 3.719 s | 1.06 GB |
Build | 1.847 s | 199.74 MB | Build | 3.622 s | 1.06 GB |
Build | 1.829 s | 199.74 MB | Build | 3.670 s | 1.06 GB |
Build | 1.842 s | 199.74 MB | Build | 3.747 s | 1.06 GB |
Build | 1.831 s | 199.74 MB | Build | 3.735 s | 1.06 GB |
Build | 1.835 s | 199.75 MB | Build | 3.698 s | 1.06 GB |
Build | 1.850 s | 199.74 MB | Build | 3.829 s | 1.06 GB |
Build | 1.829 s | 199.75 MB | Build | 3.664 s | 1.06 GB |
Build | 1.858 s | 199.74 MB | Build | 3.734 s | 1.06 GB |
v7.0.3
Method | Mean | Allocated | Method | Mean | Allocated |
---|---|---|---|---|---|
Build | 2.082 s | 188.44 MB | Build | 3.924 s | 1.05 GB |
Build | 2.091 s | 188.44 MB | Build | 3.931 s | 1.05 GB |
Build | 2.094 s | 188.44 MB | Build | 3.979 s | 1.05 GB |
Build | 2.105 s | 188.45 MB | Build | 4.001 s | 1.05 GB |
Build | 2.109 s | 188.45 MB | Build | 4.006 s | 1.05 GB |
Build | 2.112 s | 188.47 MB | Build | 4.023 s | 1.05 GB |
v7.1.1
Method | Mean | Allocated | Method | Mean | Allocated |
---|---|---|---|---|---|
Build | 2.144 s | 171.49 MB | Build | 3.532 s | 686.45 MB |
Build | 2.142 s | 171.49 MB | Build | 3.518 s | 686.24 MB |
Build | 2.157 s | 171.5 MB | Build | 3.541 s | 686.26 MB |
Build | 2.165 s | 171.5 MB | Build | 3.508 s | 686.52 MB |
Build | 2.114 s | 171.49 MB | Build | 3.541 s | 686.24 MB |
Build | 2.107 s | 171.5 MB | Build | 3.540 s | 686.49 MB |
Build | 2.128 s | 171.51 MB | Build | 3.548 s | 686.5 MB |
Build | 2.123 s | 171.5 MB | Build | 3.550 s | 686.51 MB |
Build | 2.174 s | 171.5 MB | Build | 3.536 s | 686.42 MB |
Build | 2.086 s | 171.5 MB | Build | 3.577 s | 686.47 MB |
v6.0.12
Method | Mean | Allocated | Method | Mean | Allocated |
---|---|---|---|---|---|
Build | 1.890 s | 204.41 MB | Build | 3.760 s | 1.15 GB |
Build | 1.876 s | 204.41 MB | Build | 3.685 s | 1.15 GB |
Build | 1.873 s | 204.38 MB | Build | 3.757 s | 1.15 GB |
v7.0.0
Method | Mean | Allocated | Method | Mean | Allocated |
---|---|---|---|---|---|
Build | 1.871 s | 200.62 MB | Build | 3.607 s | 1.06 GB |
Build | 1.878 s | 200.66 MB | Build | 3.617 s | 1.06 GB |
Build | 1.898 s | 200.66 MB | Build | 3.624 s | 1.06 GB |
v7.0.1
Method | Mean | Allocated | Method | Mean | Allocated |
---|---|---|---|---|---|
Build | 2.092 s | 188.83 MB | Build | 3.694 s | 1.05 GB |
Build | 2.058 s | 188.86 MB | Build | 3.839 s | 1.05 GB |
Build | 2.077 s | 188.83 MB | Build | 3.849 s | 1.05 GB |
v7.0.3
Method | Mean | Allocated | Method | Mean | Allocated |
---|---|---|---|---|---|
Build | 2.070 s | 188.86 MB | Build | 3.797 s | 1.05 GB |
Build | 2.057 s | 188.85 MB | Build | 3.806 s | 1.05 GB |
Build | 2.083 s | 188.85 MB | Build | 3.812 s | 1.05 GB |
v7.1.1
Method | Mean | Allocated | Method | Mean | Allocated |
---|---|---|---|---|---|
Build | 2.131 s | 171.3 MB | Build | 3.449 s | 686.64 MB |
Build | 2.131 s | 171.28 MB | Build | 3.472 s | 686.62 MB |
Build | 2.114 s | 171.25 MB | Build | 3.482 s | 686.61 MB |
Build | 2.137 s | 171.27 MB | Build | 3.479 s | 686.66 MB |
Build | 2.134 s | 171.27 MB | Build | 3.515 s | 686.65 MB |
Build | 2.136 s | 171.3 MB | Build | 3.519 s | 686.63 MB |
Build | 2.101 s | 171.27 MB | Build | 3.460 s | 686.63 MB |
Build | 2.133 s | 171.26 MB | Build | 3.508 s | 686.65 MB |
Build | 2.141 s | 171.27 MB | Build | 3.461 s | 686.69 MB |
Build | 2.148 s | 171.27 MB | Build | 3.433 s | 686.68 MB |
v6.0.12
Method | Mean | Allocated | Method | Mean | Allocated |
---|---|---|---|---|---|
Build | 1.821 s | 206.97 MB | Build | 3.533 s | 1.17 GB |
Build | 1.825 s | 206.97 MB | Build | 3.507 s | 1.17 GB |
Build | 1.822 s | 206.98 MB | Build | 3.518 s | 1.17 GB |
v7.0.0
Method | Mean | Allocated | Method | Mean | Allocated |
---|---|---|---|---|---|
Build | 1.854 s | 203.2 MB | Build | 3.504 s | 1.09 GB |
Build | 1.849 s | 203.22 MB | Build | 3.463 s | 1.09 GB |
Build | 1.838 s | 203.21 MB | Build | 3.523 s | 1.09 GB |
v7.0.1
Method | Mean | Allocated | Method | Mean | Allocated |
---|---|---|---|---|---|
Build | 1.985 s | 191.13 MB | Build | 3.627 s | 1.07 GB |
Build | 2.010 s | 191.14 MB | Build | 3.562 s | 1.07 GB |
Build | 2.026 s | 191.15 MB | Build | 3.549 s | 1.07 GB |
7.0.3
Method | Mean | Allocated | Method | Mean | Allocated |
---|---|---|---|---|---|
Build | 2.049 s | 191.12 MB | Build | 3.612 s | 1.07 GB |
Build | 2.021 s | 191.13 MB | Build | 3.649 s | 1.07 GB |
Build | 1.985 s | 191.12 MB | Build | 3.672 s | 1.07 GB |
v7.1.1
Method | Mean | Allocated | Method | Mean | Allocated |
---|---|---|---|---|---|
Build | 2.111 s | 172.21 MB | Build | 3.415 s | 688.68 MB |
Build | 2.115 s | 172.18 MB | Build | 3.369 s | 688.72 MB |
Build | 2.118 s | 172.18 MB | Build | 3.369 s | 688.72 MB |
Build | 2.126 s | 172.19 MB | Build | 3.371 s | 688.72 MB |
Build | 2.165 s | 172.2 MB | Build | 3.380 s | 688.72 MB |
Build | 2.130 s | 172.19 MB | Build | 3.382 s | 688.73 MB |
Build | 2.117 s | 172.2 MB | Build | 3.537 s | 688.7 MB |
Build | 2.129 s | 172.2 MB | Build | 3.404 s | 688.72 MB |
Build | 2.110 s | 172.18 MB | Build | 3.404 s | 688.72 MB |
Build | 2.147 s | 172.22 MB | Build | 3.369 s | 688.7 MB |
v6.0.12
Method | Mean | Allocated | Method | Mean | Allocated |
---|---|---|---|---|---|
Build | 1.856 s | 207.38 MB | Build | 3.615 s | 1.17 GB |
Build | 1.844 s | 207.38 MB | Build | 3.672 s | 1.17 GB |
Build | 1.867 s | 207.44 MB | Build | 3.638 s | 1.17 GB |
v7.0.0
Method | Mean | Allocated | Method | Mean | Allocated |
---|---|---|---|---|---|
Build | 1.891 s | 203.61 MB | Build | 3.556 s | 1.08 GB |
Build | 1.874 s | 203.61 MB | Build | 3.523 s | 1.08 GB |
Build | 1.881 s | 203.61 MB | Build | 3.504 s | 1.08 GB |
v7.0.1
Method | Mean | Allocated | Method | Mean | Allocated |
---|---|---|---|---|---|
Build | 2.122 s | 190.39 MB | Build | 3.884 s | 1.07 GB |
Build | 2.115 s | 190.48 MB | Build | 3.789 s | 1.07 GB |
Build | 2.116 s | 190.41 MB | Build | 3.816 s | 1.07 GB |
v7.0.3
Method | Mean | Allocated | Method | Mean | Allocated |
---|---|---|---|---|---|
Build | 2.128 s | 190.43 MB | Build | 3.776 s | 1.07 GB |
Build | 2.149 s | 190.39 MB | Build | 3.817 s | 1.07 GB |
Build | 2.122 s | 190.38 MB | Build | 3.818 s | 1.07 GB |
No result for 7.1.1, no support for this framework
We can see that: 1) changing of framework contributes to performance, newer frameworks are less speedy and far less memory efficient. 2) there is a performance drop somewhere around 7.0.1, but we gained some memory usage improvement. 3) 7.1.1 shows great memory efficiency improvements, which were lost thanks to .NET 8 itself. It is still slower than .Net Framework 4.8, I admit, but not by big value.
Assuming that domain lifetime almost equals application lifetime, we can understand that memory efficiency of domain is more important than build time and we can sacrifice some time to improve memory usage. I'm not saying that we should sacrifice, I'm saying that we've improved the most important side of Domain building. Thanks to Sergei. Basically, it was main target of Sergei, because they have not only great number of Entities, but they have big number of Storage nodes (a way of dynamically scale Domain horizontally) and memory consumption of application startup is always important for his team.
I will revisit changes that we made in 7.0.1 to and try to find out what was the cause of performance drop, we will try to gain it back.
@niikoo, @ketiovv, @gkverneland, Take a look at the results.
The projects I used for benchmarking I published here https://github.com/alex-kulakov/domain-building-perf-check
@alex-kulakov Thanks for your information and testing. It is sad to see that performance is worse with newer frameworks and we're looking forward to your further investigations on why version 7.x.x seems to be slower than 6.x.x.
Btw, our concern is not mainly the startup time, but the performance while the system is running. Have you done testing related to heavy workloads in a multi-threaded application with small and large transactions going on at the same time?
@gkverneland, I agree that startup is not very important, for startup memory efficiency is important and as you can see we managed to improve it significantly.
Secondly, speaking of performance during normal work, I asked you guys what type of queries (or something else) bothers you. I already told that I can create a query, but it may not represent your usual queries. This is important. Since no one gave me one, query benchmarks will be my fantasy, probably unrealistic. :)
Btw, our concern is not mainly the startup time, but the performance while the system is running. Have you done testing related to heavy workloads in a multi-threaded application with small and large transactions going on at the same time?
No, we haven't. The thing is that applications are different. Some of them use query caching heavily, some don't; some of applications use session tweaking to have better performance on row inserts/updates/deletions, and some don't; some of applications use query batching and some don't. And list of differences goes on and on. How should I test of all variations the user can have in his code? I would do only tests of all combinations instead of making actual changes in one particular part of ORM responsibility.
Probably the most important part is queries. If we speak about query execution there are several layers that contributes to overall performance: 1) hardware layer (cpu, memory, hdd/ssd); 2) database performance; 3) network; 4) performance of database client library (Microsoft.Data.SqlClient, Npgsql, etc.); 5) performance of our Expression to SQL string translation. 6) results materialization
We can't control first 4 layers, we should not be responsible for them. I can benchmark 5th and 6th. The 5th will show how fast and efficient expression is transformed to plain text of query. This is what I'm going to measure, for this purpose I'm not going to execute DbCommands and stop on getting SQL query string.
The 6th layer benchmarks are harder because on the 4th layer each client library implements receiving results differently - some use direct reading of results from DbDataReader, others load data to internal collection and then give results. This makes benchmark results of one storage not applicable to others. But no one answered to my question about provider you, guys, use. I will skip benchmarking this part.
Next part is persisting changes to database. There weren't many changes in this part of DataObjects.net so its performance is mostly dictated by .NET (its improvements and degradations :), we saw them in example of 6.0 in pair with different frameworks). This part depends on relations in model because it uses graphs to order insert/update/delete operations. For now, I skip benchmarking it unless you are interested in this.
Next part - session openings, transactions openings/commits/rollbacks. It is simple here, mostly it is dictated by client library and database. We do have layers of abstraction that wraps DbConnection
and DbTransaction
but the code is simple and can be represented as a chain of method calls. I would be less concerned about this.
You say about "transactions going at the same time". Correct me if I'm wrong but to me this a database responsibility, not ours. It is also partially your responsibility as developer because you decide how to manage transactions - when to open them and when to close, durations and isolation levels. We can't do much about it. Our job is to call DbConnection.Open()
, dbConnection.BeginTransaction()
, DbTransaction.Commit()
/.Rollback()
the most efficient way. Right? What may confuse you is long execution of TransactionScope.Dispose()
. I'm not sure whether you familiar with one DataObjects.Net particularity - if there are some unsaved changes and TransactionScope
is marked as Complete, then unsaved changes are persisted during Transaction scope disposal. Some people don't know this.
Next, multithreaded performance. First of all, we see Session as a unit of parallelism. One Session one thread, this concept allows asynchronous execution but not parallel execution. We declared it in documentation. We develop with this assumption in mind, I hope you do the same. Sergei's team discovered that our domain-level caches are slow on high number of threads because of locks, these collections were developed long time. We replaced our collections with counterparts from BitFaster library. This happened in 7.1. By doing so multi-threaded performance of domain-level LRU caches was improved significantly. Other global caches use collections from System.Collections.Concurrent namespace, the collections are lock-free so they should be fine.
I believe that if we (me and you guys) start searching for exact cases, we will be more productive and chances of improving overall performance by improving exact weaknesses are higher. This is why I asked about particular cases that stand out on your side and should be addressed.
Hello,
I'm back with additional results, this time for query translation measurements. I've created following LINQ query (if somebody wants to provide his own query, I don't mind, I would glad to benchmark close-to-life query):
public static IQueryable<VehicleDto> MakeQuery(Session session)
{
return session.Query.All<IVehicle>()
.Where(iv => iv.EnergySource == EnergySource.DieselFuel
|| iv.EnergySource == EnergySource.Gasoline
|| iv.EnergySource == EnergySource.Electricity
|| iv.ModelName.StartsWith("A") || iv.ModelName.StartsWith("B") || iv.ModelName.StartsWith("C")
|| iv.ModelName.Contains("C") || iv.ModelName.Contains("D") || iv.ModelName.Contains("E")
|| iv.Type.In(IncludeAlgorithm.ComplexCondition,
VehicleType.Car, VehicleType.Truck, VehicleType.Ship, VehicleType.PersonalMobilityDevice)
|| (iv.Id > 100 && iv.Id < 1_000_000)
|| (iv.Timestamp.Year - iv.Timestamp.Day) > 1500
|| (iv.Timestamp.AddDays(3) > DateTime.UtcNow)
)
.LeftJoin(
session.Query.All<Person>()
.Where(p => p.FirstName.Contains("A") || p.FirstName.Contains("B") || p.FirstName.Contains("C")
|| p.FirstName.StartsWith("D") || p.FirstName.StartsWith("Y") || p.FirstName.StartsWith("Z")
|| p.Immovables.Count > 0
|| (p.Id > 100 && p.Id < 1_000_000)
|| (p.Timestamp.Year - p.Timestamp.Day) > 1500
|| (p.Timestamp.AddDays(3) > DateTime.UtcNow)
)
,
iv => iv.Owner.Id,
p => p.Id,
(iv, p) => new { IVehicle = iv, VehicleOwner = p })
.LeftJoin(
session.Query.All<VehicleManufacturer>()
.Where(vm => vm.Country.StartsWith("A") || vm.Country.StartsWith("B") || vm.Country.StartsWith("C")
|| vm.Country.Contains("C") || vm.Name.Contains("D") || vm.Name.Contains("Z")
|| (vm.Id > 100 && vm.Id < 1_000_000)
|| (vm.Timestamp.Year - vm.Timestamp.Day) > 1500
|| (vm.Timestamp.AddDays(3) > DateTime.UtcNow))
,
a => a.IVehicle.Manufacturer.Id,
vm => vm.Id,
(a, vm) => new { IVehicle = a.IVehicle, VehicleOwner = a.VehicleOwner, VehicleManufacturer = vm })
.LeftJoin(
session.Query.All<VehicleRegistrationInfo>()
.Where(ri => ri.LicensePlate.StartsWith("B")
|| ri.LicensePlate.StartsWith("C")
|| ri.LicensePlate.EndsWith("Z")
|| ri.LicensePlate.EndsWith("Y")
|| (ri.Id > 100 && ri.Id < 1_000_000)
|| (ri.Timestamp.Year - ri.Timestamp.Day) > 1500
|| (ri.Timestamp.AddDays(3) > DateTime.UtcNow))
,
a => a.IVehicle.RegistrationInfo.Id,
ri => ri.Id,
(a, ri) => new {
IVehicle = a.IVehicle,
VehicleOwner = a.VehicleOwner,
VehicleManufacturer = a.VehicleManufacturer,
VehicleRegistrationInfo = ri })
.LeftJoin(
session.Query.All<AddressInfo>()
.Where(ai => ai.City.Length > 5 || ai.Address.Length > 10 || ai.ContactType == ContactType.Address),
a => a.VehicleRegistrationInfo.RegistrationAddress.Id,
ra => ra.Id,
(a, ra) => new {
IVehicle = a.IVehicle,
VehicleOwner = a.VehicleOwner,
VehicleManufacturer = a.VehicleManufacturer,
VehicleRegistrationInfo = a.VehicleRegistrationInfo,
VehicleRegistractionAddress = ra
})
.Select(a => new VehicleDto
{
VehicleId = a.IVehicle.Id,
VehicleRecordTimestamp = a.IVehicle.Timestamp,
VehicleModelName = a.IVehicle.ModelName,
VehicleVinNumber = a.IVehicle.VinNumber,
VehicleEngineSource = a.IVehicle.EnergySource,
VehicleEnginePower = a.IVehicle.EnginePower,
VehicleType = a.IVehicle.Type,
VehicleOwner = new VehicleOwnerDto
{
OwnerId = a.VehicleOwner.Id,
OwnerRecordTimeStamp = a.VehicleOwner.Timestamp,
OwnerFirstName = a.VehicleOwner.FirstName,
OwnerLastName = a.VehicleOwner.LastName,
NumberOfVehicles = a.VehicleOwner.Vehicles.Count,
NumberOfImmovables = a.VehicleOwner.Immovables.Count,
},
VehicleManufacturer = new VehicleManufacturerDto
{
ManufacturerId = a.VehicleManufacturer.Id,
ManufacturerRecordTimeStamp = a.VehicleManufacturer.Timestamp,
ManufacturerName = a.VehicleManufacturer.Name,
ManufacturerCountry = a.VehicleManufacturer.Country
},
VehicleRegistrationInfo = new VehicleRegistrationInfoDto
{
RegistrationId = a.VehicleRegistrationInfo.Id,
RegistrationRecordTimeStamp = a.VehicleRegistrationInfo.Timestamp,
RegistrationLicensePlate = a.VehicleRegistrationInfo.LicensePlate,
RegistrationAddress = new RegistrationAddressDto
{
AddressId = a.VehicleRegistractionAddress.Id,
AddressPostCode = a.VehicleRegistractionAddress.PostCode,
AddressCountry = a.VehicleRegistractionAddress.Country,
AddressCity = a.VehicleRegistractionAddress.City,
AddressAddress = a.VehicleRegistractionAddress.Address
}
}
});
}
The idea in the query was to have different statements of SQL query.
I didn't benchmark the whole round-trip (from linq to sql command than execute and materialize) because it would bring network and RDBMS performance to the equation and dilute results.
DataObjects.Net has a built-in service called QueryBuilder
that will help to benchmark entire process as four parts of translation. I used it to gather info about each part to discover performance per part for better localization of problem (if it exists) for further possible investigation.
So, QueryBuilder
service has number of methods. We will user four of them.
1) TranslateQuery()
- gets IQueryable<T>
and translates it to SQL DOM expressions;
2) CompileQuery()
- gets result of TranslateQuery()
and compiles them to pre-text state, containing Nodes
, most of them containing final text of different parts of result command text, but some of them serve the purpose of being able to make final adjustments to command text if needed;
3) CreateRequest()
- basically unites compilation results and bindings, provided by translation, into single structure for the last step;
4) CreateCommand()
- creates DbCommand
, fills in DbParameters
and makes final preparation of the Nodes I mentioned before. This is a final step; result command is ready to be executed.
To have final results more precise, I moved results of previous steps (if any exists) out of benchmark scope. For instance, benchmark of CreateCommand()
step looks like this
[MemoryDiagnoser]
[Orderer(BenchmarkDotNet.Order.SummaryOrderPolicy.Declared)]
[RankColumn]
public class QueryBuilderStepFourBenchmark : TranslationBenchmarkBase
{
private QueryBuilder queryBuilder;
private QueryTranslationResult translationResult;
private SqlCompilationResult sqlCompilationResult;
private QueryRequest request;
public override void BeforeEveryIteration()
{
base.BeforeEveryIteration();
queryBuilder = session.Services.GetService<QueryBuilder>();
translationResult = queryBuilder.TranslateQuery(cachedQuery); //Step #1
sqlCompilationResult = queryBuilder.CompileQuery(translationResult.Query); // Step #2
request = queryRunner.CreateRequest(queryBuilder, sqlCompilationResult, translationResult.ParameterBindings); // Step #3
}
public override void AfterEveryIteration()
{
queryBuilder = null;
request = null;
sqlCompilationResult = null;
translationResult = null;
base.AfterEveryIteration();
}
[Benchmark]
public void CreateQueryCommand()
{
_ = queryRunner.CreateQueryCommand(queryBuilder, request); // Step #4, which we actually benchmark
}
public QueryBuilderStepFourBenchmark() : base()
{
}
}
I believe such approach is right for getting correct picture of specific step.
I performed benchmarks for v6.0.12, v7.0.0, v7.0.1, v7.0.2, v7.0.3, v7.0.4 and v7.1.1 across different frameworks and grouped results by framework and ordered by DO version.
Translate() method results
DO version | .NET version | Method | Mean | Error | StdDev | Rank | Allocated |
---|---|---|---|---|---|---|---|
6.0.12 | NET 4.7.1 | Translate | 2.840 ms | 0.0546 ms | 0.0584 ms | 1 | 586.79 KB |
6.0.12 | NET 4.7.2 | Translate | 2.771 ms | 0.0251 ms | 0.0196 ms | 1 | 586.79 KB |
6.0.12 | NET 4.8 | Translate | 2.840 ms | 0.0544 ms | 0.0509 ms | 1 | 586.79 KB |
6.0.12 | NET Core 2.1 | Translate | 3.075 ms | 0.0607 ms | 0.0568 ms | 1 | 959.99 KB |
------------ | ------------- | ---------- | ---------- | ----------- | ----------- | ------ | ----------- |
6.0.12 | NET Core 3.1 | Translate | 3.836 ms | 0.2027 ms | 0.5815 ms | 1 | 938.87 KB |
7.0.0 | NET Core 3.1 | Translate | 3.748 ms | 0.1801 ms | 0.5079 ms | 1 | 927.49 KB |
7.0.1 | NET Core 3.1 | Translate | 3.552 ms | 0.2150 ms | 0.6204 ms | 1 | 912.91 KB |
7.0.2 | NET Core 3.1 | Translate | 3.663 ms | 0.1618 ms | 0.4642 ms | 1 | 920.1 KB |
7.0.3 | NET Core 3.1 | Translate | 3.685 ms | 0.1851 ms | 0.5311 ms | 1 | 921.91 KB |
7.0.4 | NET Core 3.1 | Translate | 3.697 ms | 0.1881 ms | 0.5396 ms | 1 | 922.77 KB |
------------ | ------------- | ---------- | ---------- | ----------- | ----------- | ------ | ----------- |
6.0.12 | NET5 | Translate | 2.986 ms | 0.1799 ms | 0.5276 ms | 1 | 925.95 KB |
7.0.0 | NET5 | Translate | 2.903 ms | 0.1614 ms | 0.4734 ms | 1 | 915.78 KB |
7.0.1 | NET5 | Translate | 2.942 ms | 0.1705 ms | 0.5001 ms | 1 | 901.47 KB |
7.0.2 | NET5 | Translate | 3.276 ms | 0.1313 ms | 0.3829 ms | 1 | 907.23 KB |
7.0.3 | NET5 | Translate | 2.962 ms | 0.1657 ms | 0.4885 ms | 1 | 907.34 KB |
7.0.4 | NET5 | Translate | 3.579 ms | 0.0612 ms | 0.0478 ms | 1 | 910.38 KB |
7.1.1 | NET5 | Translate | 2.913 ms | 0.1666 ms | 0.4887 ms | 1 | 851.67 KB |
------------ | ------------- | ---------- | ---------- | ----------- | ----------- | ------ | ----------- |
7.0.0 | NET6 | Translate | 3.285 ms | 0.1166 ms | 0.3420 ms | 1 | 916.5 KB |
7.0.1 | NET6 | Translate | 3.214 ms | 0.1332 ms | 0.3887 ms | 1 | 901.46 KB |
7.0.2 | NET6 | Translate | 3.343 ms | 0.1148 ms | 0.3367 ms | 1 | 909.5 KB |
7.0.3 | NET6 | Translate | 3.159 ms | 0.1215 ms | 0.3545 ms | 1 | 909.5 KB |
7.0.4 | NET6 | Translate | 3.223 ms | 0.1160 ms | 0.3401 ms | 1 | 911.81 KB |
7.1.1 | NET6 | Translate | 3.242 ms | 0.1111 ms | 0.3042 ms | 1 | 850.29 KB |
------------ | ------------- | ---------- | ---------- | ----------- | ----------- | ------ | ----------- |
7.0.0 | NET7 | Translate | 3.807 ms | 0.1889 ms | 0.5510 ms | 1 | 891.55 KB |
7.0.1 | NET7 | Translate | 3.896 ms | 0.1716 ms | 0.4840 ms | 1 | 876.38 KB |
7.0.2 | NET7 | Translate | 4.403 ms | 0.1470 ms | 0.4265 ms | 1 | 885.72 KB |
7.0.3 | NET7 | Translate | 4.082 ms | 0.1984 ms | 0.5788 ms | 1 | 886.33 KB |
7.0.4 | NET7 | Translate | 4.045 ms | 0.1650 ms | 0.4786 ms | 1 | 887.15 KB |
7.1.1 | NET7 | Translate | 4.362 ms | 0.1272 ms | 0.3691 ms | 1 | 830.15 KB |
------------ | ------------- | ---------- | ---------- | ----------- | ----------- | ------ | ----------- |
7.0.0 | NET8 | Translate | 5.493 ms | 0.1482 ms | 0.4181 ms | 1 | 890.65 KB |
7.0.1 | NET8 | Translate | 5.967 ms | 0.1640 ms | 0.4810 ms | 1 | 873.07 KB |
7.0.2 | NET8 | Translate | 5.937 ms | 0.1968 ms | 0.5772 ms | 1 | 881.73 KB |
7.0.3 | NET8 | Translate | 5.769 ms | 0.1615 ms | 0.4634 ms | 1 | 880.79 KB |
7.0.4 | NET8 | Translate | 5.891 ms | 0.1674 ms | 0.4937 ms | 1 | 882.16 KB |
7.1.1 | NET8 | Translate | 5.902 ms | 0.1843 ms | 0.5376 ms | 1 | 825.9 KB |
I can clearly see how newer versions of .NET kill performance. After performance drop in .NET Core 2.1 and 3.1, it gets closer to .Net Framework 4.x levels in NET5, NET6 is still OK and then it drops significantly.
CompileQuery() method results
DO version | .NET version | Method | Mean | Error | StdDev | Rank | Allocated |
---|---|---|---|---|---|---|---|
6.0.12 | NET 4.7.1 | CompileTranslated | 338.8 μs | 6.73 μs | 10.86 μs | 1 | 172.37 KB |
6.0.12 | NET 4.7.2 | CompileTranslated | 338.7 μs | 6.77 μs | 14.29 μs | 1 | 172.37 KB |
6.0.12 | NET 4.8 | CompileTranslated | 335.6 μs | 6.72 μs | 11.41 μs | 1 | 172.37 KB |
6.0.12 | NET Core 2.1 | CompileTranslated | 376.9 μs | 7.36 μs | 11.02 μs | 1 | 240.73 KB |
------------ | ------------- | ------------------ | --------- | --------- | --------- | ----- | ---------- |
6.0.12 | NET Core 3.1 | CompileTranslated | 528.5 μs | 10.55 μs | 25.68 μs | 1 | 235.86 KB |
7.0.0 | NET Core 3.1 | CompileTranslated | 477.6 μs | 23.53 μs | 65.97 μs | 1 | 235.32 KB |
7.0.1 | NET Core 3.1 | CompileTranslated | 461.4 μs | 13.93 μs | 39.97 μs | 1 | 235.32 KB |
7.0.2 | NET Core 3.1 | CompileTranslated | 444.8 μs | 13.28 μs | 37.67 μs | 1 | 235.23 KB |
7.0.3 | NET Core 3.1 | CompileTranslated | 380.9 μs | 26.29 μs | 75.85 μs | 1 | 235.23 KB |
7.0.4 | NET Core 3.1 | CompileTranslated | 376.0 μs | 23.00 μs | 65.25 μs | 1 | 235.23 KB |
------------ | ------------- | ------------------ | --------- | --------- | --------- | ----- | ---------- |
6.0.12 | NET5 | CompileTranslated | 415.1 μs | 8.02 μs | 13.17 μs | 1 | 235.89 KB |
7.0.0 | NET5 | CompileTranslated | 350.9 μs | 26.42 μs | 77.47 μs | 1 | 235.35 KB |
7.0.1 | NET5 | CompileTranslated | 374.3 μs | 27.07 μs | 79.38 μs | 1 | 235.35 KB |
7.0.2 | NET5 | CompileTranslated | 358.2 μs | 22.84 μs | 65.89 μs | 1 | 235.27 KB |
7.0.3 | NET5 | CompileTranslated | 303.6 μs | 12.42 μs | 34.00 μs | 1 | 235.27 KB |
7.0.4 | NET5 | CompileTranslated | 312.8 μs | 13.57 μs | 37.59 μs | 1 | 235.27 KB |
7.1.1 | NET5 | CompileTranslated | 343.5 μs | 35.87 μs | 105.2 μs | 1 | 82.55 KB |
------------ | ------------- | ------------------ | --------- | --------- | --------- | ----- | ---------- |
7.0.0 | NET6 | CompileTranslated | 398.1 μs | 7.91 μs | 17.36 μs | 1 | 237.02 KB |
7.0.1 | NET6 | CompileTranslated | 404.9 μs | 10.15 μs | 29.29 μs | 1 | 236.94 KB |
7.0.2 | NET6 | CompileTranslated | 402.5 μs | 8.01 μs | 17.92 μs | 1 | 236.85 KB |
7.0.3 | NET6 | CompileTranslated | 327.0 μs | 16.11 μs | 47.26 μs | 1 | 236.85 KB |
7.0.4 | NET6 | CompileTranslated | 332.1 μs | 16.55 μs | 47.76 μs | 1 | 236.85 KB |
7.1.1 | NET6 | CompileTranslated | 274.8 μs | 13.91 μs | 40.35 μs | 1 | 82.84 KB |
------------ | ------------- | ------------------ | --------- | --------- | --------- | ----- | ---------- |
7.0.0 | NET7 | CompileTranslated | 378.7 μs | 27.07 μs | 77.68 μs | 1 | 236.8 KB |
7.0.1 | NET7 | CompileTranslated | 369.2 μs | 24.72 μs | 71.34 μs | 1 | 236.7 KB |
7.0.2 | NET7 | CompileTranslated | 499.4 μs | 18.65 μs | 53.21 μs | 1 | 236.62 KB |
7.0.3 | NET7 | CompileTranslated | 488.8 μs | 20.41 μs | 58.23 μs | 1 | 236.62 KB |
7.0.4 | NET7 | CompileTranslated | 503.7 μs | 14.10 μs | 38.84 μs | 1 | 236.62 KB |
7.1.1 | NET7 | CompileTranslated | 430.3 μs | 11.20 μs | 31.97 μs | 1 | 82.55 KB |
------------ | ------------- | ------------------ | --------- | --------- | --------- | ----- | ---------- |
7.0.0 | NET8 | CompileTranslated | 818.8 μs | 16.26 μs | 38.34 μs | 1 | 236.34 KB |
7.0.1 | NET8 | CompileTranslated | 710.9 μs | 14.14 μs | 33.32 μs | 1 | 236.22 KB |
7.0.2 | NET8 | CompileTranslated | 800.9 μs | 21.88 μs | 62.08 μs | 1 | 236.14 KB |
7.0.3 | NET8 | CompileTranslated | 786.9 μs | 39.16 μs | 115.5 μs | 1 | 236.14 KB |
7.0.4 | NET8 | CompileTranslated | 649.7 μs | 12.85 μs | 30.04 μs | 1 | 236.14 KB |
7.1.1 | NET8 | CompileTranslated | 580.6 μs | 11.49 μs | 27.97 μs | 1 | 81.38 KB |
Same story. Performance recovers in NET 5 and NET 6 and then deeps down in NET 7 and NET 6.
CreateRequest() results
DO version | .NET version | Method | Mean | Error | StdDev | Rank | Allocated |
---|---|---|---|---|---|---|---|
6.0.12 | NET 4.7.1 | CreateRequest | 4.699 μs | 0.5321 μs | 1.518 μs | 1 | 16 KB |
6.0.12 | NET 4.7.2 | CreateRequest | 4.732 μs | 0.6092 μs | 1.777 μs | 1 | 16 KB |
6.0.12 | NET 4.8 | CreateRequest | 4.665 μs | 0.5419 μs | 1.572 μs | 1 | 16 KB |
6.0.12 | NET Core 2.1 | CreateRequest | 8.597 μs | 0.7603 μs | 2.194 μs | 1 | 560 B |
------------ | ------------- | -------------- | --------- | ---------- | --------- | ----- | ---------- |
6.0.12 | NET Core 3.1 | CreateRequest | 10.86 μs | 1.595 μs | 4.576 μs | 1 | 560 B |
7.0.0 | NET Core 3.1 | CreateRequest | 12.74 μs | 2.165 μs | 6.212 μs | 1 | 560 B |
7.0.1 | NET Core 3.1 | CreateRequest | 12.98 μs | 1.590 μs | 4.510 μs | 1 | 560 B |
7.0.2 | NET Core 3.1 | CreateRequest | 11.12 μs | 1.283 μs | 3.598 μs | 1 | 560 B |
7.0.3 | NET Core 3.1 | CreateRequest | 11.92 μs | 1.469 μs | 4.145 μs | 1 | 560 B |
7.0.4 | NET Core 3.1 | CreateRequest | 12.57 μs | 1.775 μs | 5.065 μs | 1 | 560 B |
------------ | ------------- | -------------- | --------- | ---------- | --------- | ----- | ---------- |
6.0.12 | NET5 | CreateRequest | 8.471 μs | 0.7881 μs | 2.286 μs | 1 | 568 B |
7.0.0 | NET5 | CreateRequest | 8.214 μs | 0.8363 μs | 2.386 μs | 1 | 568 B |
7.0.1 | NET5 | CreateRequest | 8.793 μs | 0.9189 μs | 2.651 μs | 1 | 568 B |
7.0.2 | NET5 | CreateRequest | 9.151 μs | 1.014 μs | 2.925 μs | 1 | 568 B |
7.0.3 | NET5 | CreateRequest | 9.264 μs | 1.017 μs | 2.935 μs | 1 | 568 B |
7.0.4 | NET5 | CreateRequest | 9.141 μs | 0.9475 μs | 2.734 μs | 1 | 568 B |
7.1.1 | NET5 | CreateRequest | 9.426 μs | 0.9184 μs | 2.650 μs | 1 | 1.75 KB |
------------ | ------------- | -------------- | --------- | ---------- | --------- | ----- | ---------- |
7.0.0 | NET6 | CreateRequest | 8.925 μs | 0.8865 μs | 2.500 μs | 1 | 2.22 KB |
7.0.1 | NET6 | CreateRequest | 9.330 μs | 0.8908 μs | 2.570 μs | 1 | 2.14 KB |
7.0.2 | NET6 | CreateRequest | 9.514 μs | 0.9771 μs | 2.835 μs | 1 | 2.14 KB |
7.0.3 | NET6 | CreateRequest | 9.285 μs | 0.8970 μs | 2.574 μs | 1 | 2.14 KB |
7.0.4 | NET6 | CreateRequest | 8.885 μs | 0.8147 μs | 2.311 μs | 1 | 2.14 KB |
7.1.1 | NET6 | CreateRequest | 10.61 μs | 0.847 μs | 2.459 μs | 1 | 3.34 KB |
------------ | ------------- | -------------- | --------- | ---------- | --------- | ----- | ---------- |
7.0.0 | NET7 | CreateRequest | 11.25 μs | 0.828 μs | 2.363 μs | 1 | 2.01 KB |
7.0.1 | NET7 | CreateRequest | 11.34 μs | 0.861 μs | 2.444 μs | 1 | 1.91 KB |
7.0.2 | NET7 | CreateRequest | 11.79 μs | 1.082 μs | 3.104 μs | 1 | 1.91 KB |
7.0.3 | NET7 | CreateRequest | 11.66 μs | 1.016 μs | 2.933 μs | 1 | 1.91 KB |
7.0.4 | NET7 | CreateRequest | 11.90 μs | 0.942 μs | 2.701 μs | 1 | 1.91 KB |
7.1.1 | NET7 | CreateRequest | 11.87 μs | 0.756 μs | 2.182 μs | 1 | 3.04 KB |
------------ | ------------- | -------------- | --------- | ---------- | --------- | ----- | ---------- |
7.0.0 | NET8 | CreateRequest | 10.57 μs | 1.004 μs | 2.912 μs | 1 | 2.03 KB |
7.0.1 | NET8 | CreateRequest | 11.73 μs | 0.998 μs | 2.912 μs | 1 | 1.91 KB |
7.0.2 | NET8 | CreateRequest | 12.51 μs | 1.071 μs | 3.106 μs | 1 | 1.91 KB |
7.0.3 | NET8 | CreateRequest | 12.15 μs | 0.896 μs | 2.542 μs | 1 | 1.91 KB |
7.0.4 | NET8 | CreateRequest | 11.91 μs | 1.061 μs | 3.043 μs | 1 | 1.91 KB |
7.1.1 | NET8 | CreateRequest | 12.41 μs | 0.957 μs | 2.761 μs | 1 | 3.04 KB |
Story repeats. I already know how to slightly improve this region but since this part is basically several objects' instantiations and copying items of collection, we can do almost nothing for this part of translation. I did some research, maybe 1-2 nanoseconds and some memory efficiency will be gained. I already committed the changes in my private copy of the repo.
CreateCommand() results
DO version | .NET version | Method | Mean | Error | StdDev | Rank | Allocated |
---|---|---|---|---|---|---|---|
6.0.12 | NET 4.7.1 | CreateQueryCommand | 35.98 μs | 1.814 μs | 5.205 μs | 1 | 109.1 KB |
6.0.12 | NET 4.7.2 | CreateQueryCommand | 34.36 μs | 1.468 μs | 4.187 μs | 1 | 109.1 KB |
6.0.12 | NET 4.8 | CreateQueryCommand | 34.78 μs | 1.564 μs | 4.412 μs | 1 | 109.1 KB |
6.0.12 | NET Core 2.1 | CreateQueryCommand | 33.18 μs | 1.890 μs | 5.542 μs | 1 | 90.62 KB |
------------ | ------------- | ------------------- | --------- | --------- | --------- | ----- | ---------- |
6.0.12 | NET Core 3.1 | CreateQueryCommand | 60.14 μs | 8.987 μs | 25.49 μs | 1 | 90.42 KB |
7.0.0 | NET Core 3.1 | CreateQueryCommand | 58.01 μs | 7.598 μs | 21.18 μs | 1 | 91.67 KB |
7.0.1 | NET Core 3.1 | CreateQueryCommand | 63.95 μs | 9.164 μs | 25.70 μs | 1 | 91.67 KB |
7.0.2 | NET Core 3.1 | CreateQueryCommand | 61.33 μs | 7.666 μs | 21.37 μs | 1 | 91.58 KB |
7.0.3 | NET Core 3.1 | CreateQueryCommand | 51.88 μs | 2.965 μs | 7.759 μs | 1 | 91.58 KB |
7.0.4 | NET Core 3.1 | CreateQueryCommand | 56.64 μs | 5.635 μs | 15.33 μs | 1 | 91.58 KB |
------------ | ------------- | ------------------- | --------- | --------- | --------- | ----- | ---------- |
6.0.12 | NET5 | CreateQueryCommand | 44.99 μs | 2.438 μs | 6.916 μs | 1 | 90.45 KB |
7.0.0 | NET5 | CreateQueryCommand | 49.75 μs | 3.703 μs | 10.56 μs | 1 | 91.7 KB |
7.0.1 | NET5 | CreateQueryCommand | 51.17 μs | 3.998 μs | 11.28 μs | 1 | 91.7 KB |
7.0.2 | NET5 | CreateQueryCommand | 53.17 μs | 4.103 μs | 11.77 μs | 1 | 91.61 KB |
7.0.3 | NET5 | CreateQueryCommand | 49.89 μs | 3.014 μs | 8.598 μs | 1 | 91.61 KB |
7.0.4 | NET5 | CreateQueryCommand | 52.43 μs | 3.292 μs | 9.392 μs | 1 | 91.61 KB |
7.1.1 | NET5 | CreateQueryCommand | 68.96 μs | 4.334 μs | 12.44 μs | 1 | 114.94 KB |
------------ | ------------- | ------------------- | --------- | --------- | --------- | ----- | ---------- |
7.0.0 | NET6 | CreateQueryCommand | 44.02 μs | 2.832 μs | 7.895 μs | 1 | 93.37 KB |
7.0.1 | NET6 | CreateQueryCommand | 50.77 μs | 3.196 μs | 9.170 μs | 1 | 93.29 KB |
7.0.2 | NET6 | CreateQueryCommand | 47.19 μs | 3.221 μs | 9.139 μs | 1 | 93.2 KB |
7.0.3 | NET6 | CreateQueryCommand | 46.43 μs | 2.506 μs | 6.987 μs | 1 | 93.2 KB |
7.0.4 | NET6 | CreateQueryCommand | 48.09 μs | 3.438 μs | 9.585 μs | 1 | 93.2 KB |
7.1.1 | NET6 | CreateQueryCommand | 70.61 μs | 4.321 μs | 12.26 μs | 1 | 116.52 KB |
------------ | ------------- | ------------------- | --------- | --------- | --------- | ----- | ---------- |
7.0.0 | NET7 | CreateQueryCommand | 56.09 μs | 3.093 μs | 8.723 μs | 1 | 93.16 KB |
7.0.1 | NET7 | CreateQueryCommand | 62.40 μs | 3.515 μs | 9.856 μs | 1 | 93.05 KB |
7.0.2 | NET7 | CreateQueryCommand | 61.46 μs | 3.923 μs | 11.13 μs | 1 | 92.96 KB |
7.0.3 | NET7 | CreateQueryCommand | 62.14 μs | 3.766 μs | 10.62 μs | 1 | 92.96 KB |
7.0.4 | NET7 | CreateQueryCommand | 63.50 μs | 3.667 μs | 10.16 μs | 1 | 92.96 KB |
7.1.1 | NET7 | CreateQueryCommand | 93.73 μs | 5.221 μs | 14.98 μs | 1 | 116.23 KB |
------------ | ------------- | ------------------- | --------- | --------- | --------- | ----- | ---------- |
7.0.0 | NET8 | CreateQueryCommand | 64.78 μs | 4.068 μs | 11.67 μs | 1 | 93.18 KB |
7.0.1 | NET8 | CreateQueryCommand | 66.94 μs | 3.396 μs | 9.467 μs | 1 | 93.05 KB |
7.0.2 | NET8 | CreateQueryCommand | 66.06 μs | 3.708 μs | 10.34 μs | 1 | 92.59 KB |
7.0.3 | NET8 | CreateQueryCommand | 66.74 μs | 3.193 μs | 9.314 μs | 1 | 92.96 KB |
7.0.4 | NET8 | CreateQueryCommand | 66.62 μs | 3.565 μs | 10.17 μs | 1 | 92.96 KB |
7.1.1 | NET8 | CreateQueryCommand | 102.9 μs | 4.44 μs | 12.68 μs | 1 | 116.23 KB |
Here, unfortunately I see significant drop in version 7.1.1. but framework also contribute to losses. In 7.1.1 we changed SQL DOM expression translation to Nodes, which might have shifted some compute time to final preparation, but I'm not sure. Currently I'm working with this part, trying to find points for improvement.
I left the big parts to be last, because they are much complicated and will take a lot of time to find ways to improve. I will try to compensate losses caused by .NET 7 and .NET 8, .NET5 and NET6 will also be improved.
@gkverneland, @niikoo, @ketiovv take a look. I'd like to know your thoughts.
@alex-kulakov Thanks a lot for your investigations so far. It is sad to see that for Xtensive.Orm, all tests show that performance is way worse with .Net 8 than before.
I see that version 6.x.x is not included in your tests for .Net 7 and .Net 8, have you tested that combination? It would be very interesting to have tests where the Xtensive.Orm code is as similar as possible. We have been able to run version 6.x.x with .Net 7 and 8, and can give you the fork if you are interested.
Btw, have you seen this issue about performance for expression compilation? Is it relevant for this project? https://github.com/dotnet/runtime/issues/75891 https://github.com/dotnet/runtime/issues/76058
@gkverneland, @niikoo, @ketiovv take a look. I'd like to know your thoughts.
Hello!
Thanks for taking the time to reply to us.
Do you have a link to the benchmark project that you've used here: https://github.com/DataObjects-NET/dataobjects-net/issues/358#issuecomment-2056662526
@gkverneland
I see that version 6.x.x is not included in your tests for .Net 7 and .Net 8, have you tested that combination?
Yes, It is not included because there are some breaking changes in .net that prevented usage of already built packages. The benchmark project and Domain build successfully but queries don't work. To be honest, I don't want to resolve all the issues connected with migration once again just for benchmarks, I just assumed that 7.0.0 is good enough to substitute 6.0.12. If you have a version of 6.0.12 with changes that can be used in .net 5, 6 and so on I would get such version in form of packages. I believe it will be fastest way. Put some suffix in Version.props file before build, for instance <DoVersionSuffix>net5-compatible</DoVersionSuffix>
. You can reach me directly via alexey.kulakov@dataobjects.net
I'll gladly include it to benchmarks and post results here.
Btw, have you seen this issue about performance for expression compilation? Is it relevant for this project?
Not yet, but I will check it out. Thank you for pointing me to these two issues. Something outside DataObjects.Net ruins the performance for sure, and since translation uses reflection a lot there must be degradation of performance somewhere there.
@niikoo,
Do you have a link to the benchmark project that you've used here:
Not yet, it is not a problem to post. I just use the projects to monitor how my changes affect performance, so it has some garbage right now. If you are interested, I'll clean it up and post as repo as before, it is not a problem, I have nothing to hide 😉
BTW guys, do you use "cached queries" feature of DataObjects.Net? I'm talking about session.Query.Execute() group of methods. Though, not all the queries can be transformed into cacheable form, many of them can be, which allows to skip the most time-consuming part of query translation after first run of the query, so you might be interested in such option.
@niikoo,
The repo is here https://github.com/alex-kulakov/query-translation-perf-check
Hello guys, @gkverneland, @niikoo, @ketiovv
I have revised code and found some points for improvement, they are posted as the PR - https://github.com/DataObjects-NET/dataobjects-net/pull/392,
They are small, no drastic difference but still, also not all of them are connected with the test query in the query-translation-perf-check repository.
During my investigation I captured this
The functions are sorted by CPU Self time, which shows the most inefficient methods on the top. As you can see most percentage of time spent in external or native code. The items that take 3.57% of time are very small, for instance ExtendedExpression constructor is a constructor that assigns few properties.
It doesn't mean that our code is the most efficient, I believe there are still points for improvement like in any app/framework/etc, but it shows that outside code impacts performance seriously.
Actually we can reduce the amount of time spent in External/Native code by improving callers of that code.
For example: I found O(n²) - complexity algorithms where O(n) can be used: https://github.com/servicetitan/dataobjects-net/pull/208
@SergeiPavlov Nice.
Actually we can reduce the amount of time spent in External/Native code by improving callers of that code.
For example: I found O(n²) - complexity algorithms where O(n) can be used: servicetitan#208
I did some testing, building the servicetitan version of DO.net, and setting COMPlus_EnableWriteXorExecute=0 in the environment variables. It resulted in significant performance improvements. Could you @alex-kulakov look into this and see if you can cherry-pick the changes made by @SergeiPavlov?
@niikoo,
I glanced at the changes Sergei did and proposed, the changes can appear only in master (which is 7.2 in develop). Currently released versions will not receive them.
Outside of this discussion Sergei proposed me covariant returns for cloning of certain classes (e.g SqlNode derived ones), that I successfully applied to 7.1 branch. The changes can't be applied to version older than 7.1 due to target-framework-to-language-version binding.
I will also read about the option you mentioned to better understand the outcome of it. Thank you for this information.
Hello!
We have a project running on Xtensive.Orm 6.1.1 and .net Framework 4.8 and we have migrated it to Xtensive.Orm 7.1.1 and net7.0. We experience that database tasks are slower than before, even without any changes to the models. We also get many DeadlockExceptions. Is there anything we need to do or any changes in Xtensive.Orm 7.1.1 that affects the performance?