diff --git a/README.md b/README.md index 9f1c924c..c73ff524 100644 --- a/README.md +++ b/README.md @@ -141,21 +141,29 @@ public static class StudentMapper { ### Performance & Memory efficient -Mapster was designed to be efficient on both speed and memory. You could gain a 4x performance improvement whilst using only 1/3 of memory. -And you could gain up to 12x faster performance with: +Mapster was designed to be efficient on both speed and memory. The repository includes a benchmark project in [`src/Benchmark`](https://github.com/MapsterMapper/Mapster/tree/master/src/Benchmark) that compares the local Mapster build, its compiler variants, and other modern mapping libraries like AutoMapper, Mapperly, and Facet. - [Roslyn Compiler](https://mapstermapper.github.io/Mapster/articles/packages/ExpressionDebugging.html) - [FEC](https://mapstermapper.github.io/Mapster/articles/packages/FastExpressionCompiler.html) - Code generation - -| Method | Mean | StdDev | Error | Gen 0 | Gen 1 | Gen 2 | Allocated | -|-------------------------- |----------:|----------:|----------:|-----------:|------:|------:|----------:| -| 'Mapster 6.0.0' | 108.59 ms | 1.198 ms | 1.811 ms | 31000.0000 | - | - | 124.36 MB | -| 'Mapster 6.0.0 (Roslyn)' | 38.45 ms | 0.494 ms | 0.830 ms | 31142.8571 | - | - | 124.36 MB | -| 'Mapster 6.0.0 (FEC)' | 37.03 ms | 0.281 ms | 0.472 ms | 29642.8571 | - | - | 118.26 MB | -| 'Mapster 6.0.0 (Codegen)' | 34.16 ms | 0.209 ms | 0.316 ms | 31133.3333 | - | - | 124.36 MB | -| 'ExpressMapper 1.9.1' | 205.78 ms | 5.357 ms | 8.098 ms | 59000.0000 | - | - | 236.51 MB | -| 'AutoMapper 10.0.0' | 420.97 ms | 23.266 ms | 35.174 ms | 87000.0000 | - | - | 350.95 MB | +- Facet +- Mapperly + +The snapshot below shows the `FlatTypes` scenario (`Person -> PersonDTO`), a best-case DTO with simple property-to-property mapping and no nested objects or collections. + +> [!NOTE] +> More complex object shapes can change the relative results. See the [complete benchmark results](https://mapstermapper.github.io/Mapster/articles/benchmarks.html) for `ComplexTypes`, `RecursiveTypes`, and `TotalAllTypes`. + +| Method | MapOperations | Mean | StdDev | Error | Ns/Map | Ratio | Gen0 | Allocated | Alloc Ratio | Bytes/Map | +|------------------------------------ |-------------- |----------:|----------:|----------:|-------:|------:|-----:|----------:|------------:|----------:| +| `Mapster 10.0.7` | 1000000 | 6.849 ms | 0.6851 ms | 1.0358 ms | 6.849 | 1.01 | 4781 | 76.29 MB | 1.00 | 80 | +| `Mapster 10.0.7 (Roslyn)` | 1000000 | 6.579 ms | 0.2782 ms | 0.4206 ms | 6.579 | 0.97 | 4781 | 76.29 MB | 1.00 | 80 | +| `Mapster 10.0.7 (FEC)` | 1000000 | 6.549 ms | 0.9130 ms | 1.3803 ms | 6.549 | 0.97 | 4781 | 76.29 MB | 1.00 | 80 | +| `Mapster 10.0.7 (Codegen)` | 1000000 | 5.868 ms | 0.3266 ms | 0.5488 ms | 5.868 | 0.86 | 4781 | 76.29 MB | 1.00 | 80 | +| `AutoMapper 14.0.0` | 1000000 | 29.645 ms | 0.8963 ms | 1.5062 ms | 29.645 | 4.37 | 4750 | 76.29 MB | 1.00 | 80 | +| `Facet 6.5.5` | 1000000 | 7.801 ms | 1.0231 ms | 1.5467 ms | 7.801 | 1.15 | 8601 | 137.33 MB | 1.80 | 144 | +| `Facet 6.5.5 (Compiled Projection)` | 1000000 | 5.508 ms | 0.7064 ms | 1.0679 ms | 5.508 | 0.81 | 4781 | 76.29 MB | 1.00 | 80 | +| `Mapperly 4.3.1` | 1000000 | 6.521 ms | 0.8369 ms | 1.2652 ms | 6.521 | 0.96 | 4781 | 76.29 MB | 1.00 | 80 | ### Step into debugging diff --git a/docs/articles/_Sidebar.md b/docs/articles/_Sidebar.md index 22c58675..dec82b38 100644 --- a/docs/articles/_Sidebar.md +++ b/docs/articles/_Sidebar.md @@ -54,3 +54,8 @@ * [Fluent API](https://github.com/MapsterMapper/Mapster/wiki/Fluent-API-Code-generation) * [Attributes](https://github.com/MapsterMapper/Mapster/wiki/Attribute-base-Code-generation) * [Interfaces](https://github.com/MapsterMapper/Mapster/wiki/Interface-base-Code-generation) + +## Benchmarks + +* [Benchmark results](https://mapstermapper.github.io/Mapster/articles/benchmarks.html) +* \ No newline at end of file diff --git a/docs/articles/benchmarks.md b/docs/articles/benchmarks.md new file mode 100644 index 00000000..7d524544 --- /dev/null +++ b/docs/articles/benchmarks.md @@ -0,0 +1,103 @@ +--- +uid: Mapster.Benchmarks +title: "Benchmark results" +--- + +Mapster includes a benchmark project in [`src/Benchmark`](https://github.com/MapsterMapper/Mapster/tree/master/src/Benchmark) that compares the local Mapster build with AutoMapper, Facet, and Mapperly across several object shapes. + +This page is a May 2026 snapshot of those benchmarks. Treat the numbers as a comparison point for this environment and benchmark configuration rather than an absolute guarantee for every machine or application. + +## How to read the tables + +- `Mean` is the total time for one BenchmarkDotNet benchmark invocation. Each invocation runs a manual hot loop with `MapOperations = 1,000,000`. +- `Ns/Map` normalizes `Mean` to the approximate cost of one logical mapping call. +- `Allocated` is the memory allocated by one benchmark invocation. +- `Bytes/Map` normalizes `Allocated` to one logical mapping call. +- `Ratio` and `Alloc Ratio` are relative to the default Mapster benchmark in the same scenario. +- `TotalAllTypes` runs three mapping scenarios in one benchmark invocation, so its `Ns/Map` and `Bytes/Map` values are divided by `MapOperations * 3`. + +## Benchmark scenarios + +### FlatTypes + +`FlatTypes` maps `Person -> PersonDTO`. It is a flat DTO shape: simple property-to-property copy, no nested objects, and no collections. This scenario mostly highlights mapper call overhead, generated IL quality, and allocation rate for a best-case DTO. + +### ComplexTypes + +`ComplexTypes` maps `Customer -> CustomerDTO`. It includes nested address mapping, array/list shape changes, and a flattening rule (`AddressCity <- Address.City`). This scenario is useful for typical DTOs that combine nested objects, collections, and a small amount of custom member mapping. + +### RecursiveTypes + +`RecursiveTypes` maps `Foo -> FooDTO`. The type shape is self-recursive: a `Foo` can contain another `Foo`, an enumerable of `Foo`, and an array of `Foo`. The sample data does not intentionally create a back-reference cycle, but the mapping graph is deeper and allocates more nested DTOs than the flat or customer scenarios. + +### TotalAllTypes + +`TotalAllTypes` runs `FlatTypes`, `RecursiveTypes`, and `ComplexTypes` sequentially in one benchmark method. `Mean` is therefore the total batch time for all three scenarios; use `Ns/Map` and `Bytes/Map` for normalized per-map interpretation. + +## Compared methods + +- `Mapster` uses the default Mapster expression compiler. +- `Mapster (Roslyn)` uses the Roslyn/debug-info compiler path. +- `Mapster (FEC)` uses FastExpressionCompiler. +- `Mapster (Codegen)` uses generated mapping code. +- `AutoMapper`, `Facet`, and `Mapperly` are included as external comparison points. +- `Facet (Compiled Projection)` uses Facet's compiled projection path, which can behave very differently from the constructor path depending on the object shape. + +## Results + +### FlatTypes + +| Scenario | Method | MapOperations | Mean | StdDev | Error | Ns/Map | Ratio | Gen0 | Allocated | Alloc Ratio | Bytes/Map | +|---------- |------------------------------------ |-------------- |----------:|----------:|----------:|-------:|------:|-----:|----------:|------------:|----------:| +| FlatTypes | `Mapster 10.0.7` | 1000000 | 6.849 ms | 0.6851 ms | 1.0358 ms | 6.849 | 1.01 | 4781 | 76.29 MB | 1.00 | 80 | +| FlatTypes | `Mapster 10.0.7 (Roslyn)` | 1000000 | 6.579 ms | 0.2782 ms | 0.4206 ms | 6.579 | 0.97 | 4781 | 76.29 MB | 1.00 | 80 | +| FlatTypes | `Mapster 10.0.7 (FEC)` | 1000000 | 6.549 ms | 0.9130 ms | 1.3803 ms | 6.549 | 0.97 | 4781 | 76.29 MB | 1.00 | 80 | +| FlatTypes | `Mapster 10.0.7 (Codegen)` | 1000000 | 5.868 ms | 0.3266 ms | 0.5488 ms | 5.868 | 0.86 | 4781 | 76.29 MB | 1.00 | 80 | +| FlatTypes | `AutoMapper 14.0.0` | 1000000 | 29.645 ms | 0.8963 ms | 1.5062 ms | 29.645 | 4.37 | 4750 | 76.29 MB | 1.00 | 80 | +| FlatTypes | `Facet 6.5.5` | 1000000 | 7.801 ms | 1.0231 ms | 1.5467 ms | 7.801 | 1.15 | 8601 | 137.33 MB | 1.80 | 144 | +| FlatTypes | `Facet 6.5.5 (Compiled Projection)` | 1000000 | 5.508 ms | 0.7064 ms | 1.0679 ms | 5.508 | 0.81 | 4781 | 76.29 MB | 1.00 | 80 | +| FlatTypes | `Mapperly 4.3.1` | 1000000 | 6.521 ms | 0.8369 ms | 1.2652 ms | 6.521 | 0.96 | 4781 | 76.29 MB | 1.00 | 80 | + +### ComplexTypes + +| Scenario | Method | MapOperations | Mean | StdDev | Error | Ns/Map | Ratio | Gen0 | Gen1 | Allocated | Alloc Ratio | Bytes/Map | +|------------- |------------------------------------ |-------------- |----------:|----------:|----------:|--------:|------:|-------:|-----:|-----------:|------------:|----------:| +| ComplexTypes | `Mapster 10.0.7` | 1000000 | 78.59 ms | 1.519 ms | 2.553 ms | 78.586 | 1.00 | 28111 | - | 450.13 MB | 1.00 | 472 | +| ComplexTypes | `Mapster 10.0.7 (Roslyn)` | 1000000 | 53.48 ms | 1.760 ms | 2.958 ms | 53.475 | 0.68 | 25818 | - | 411.99 MB | 0.92 | 432 | +| ComplexTypes | `Mapster 10.0.7 (FEC)` | 1000000 | 69.29 ms | 1.539 ms | 2.326 ms | 69.288 | 0.88 | 28200 | - | 450.13 MB | 1.00 | 472 | +| ComplexTypes | `Mapster 10.0.7 (Codegen)` | 1000000 | 51.86 ms | 2.514 ms | 3.801 ms | 51.863 | 0.66 | 25750 | - | 411.99 MB | 0.92 | 432 | +| ComplexTypes | `AutoMapper 14.0.0` | 1000000 | 112.27 ms | 4.516 ms | 7.590 ms | 112.270 | 1.43 | 29142 | - | 465.39 MB | 1.03 | 488 | +| ComplexTypes | `Facet 6.5.5` | 1000000 | 446.72 ms | 51.706 ms | 78.172 ms | 446.725 | 5.69 | 175000 | 500 | 2792.36 MB | 6.20 | 2928 | +| ComplexTypes | `Facet 6.5.5 (Compiled Projection)` | 1000000 | 678.87 ms | 42.646 ms | 64.474 ms | 678.868 | 8.64 | 44000 | - | 709.53 MB | 1.58 | 744 | +| ComplexTypes | `Mapperly 4.3.1` | 1000000 | 52.81 ms | 1.134 ms | 1.714 ms | 52.812 | 0.67 | 25769 | - | 411.99 MB | 0.92 | 432 | + +### RecursiveTypes + +| Scenario | Method | MapOperations | Mean | StdDev | Error | Ns/Map | Ratio | Gen0 | Gen1 | Allocated | Alloc Ratio | Bytes/Map | +|--------------- |------------------------------------ |-------------- |----------:|----------:|----------:|--------:|------:|-------:|-----:|-----------:|------------:|----------:| +| RecursiveTypes | `Mapster 10.0.7` | 1000000 | 404.49 ms | 63.593 ms | 96.14 ms | 404.486 | 1.02 | 49000 | - | 793.46 MB | 1.00 | 832 | +| RecursiveTypes | `Mapster 10.0.7 (Roslyn)` | 1000000 | 383.00 ms | 34.563 ms | 52.25 ms | 382.999 | 0.97 | 49000 | - | 793.46 MB | 1.00 | 832 | +| RecursiveTypes | `Mapster 10.0.7 (FEC)` | 1000000 | 84.82 ms | 10.777 ms | 16.29 ms | 84.816 | 0.21 | 45875 | 125 | 732.42 MB | 0.92 | 768 | +| RecursiveTypes | `Mapster 10.0.7 (Codegen)` | 1000000 | 82.81 ms | 11.104 ms | 16.79 ms | 82.806 | 0.21 | 49625 | 125 | 793.46 MB | 1.00 | 832 | +| RecursiveTypes | `AutoMapper 14.0.0` | 1000000 | 585.42 ms | 88.660 ms | 134.04 ms | 585.425 | 1.48 | 168000 | 1000 | 2693.18 MB | 3.39 | 2824 | +| RecursiveTypes | `Facet 6.5.5` | 1000000 | 302.24 ms | 9.444 ms | 18.06 ms | 302.241 | 0.76 | 150000 | 666 | 2395.63 MB | 3.02 | 2512 | +| RecursiveTypes | `Facet 6.5.5 (Compiled Projection)` | 1000000 | 708.77 ms | 58.207 ms | 88.00 ms | 708.770 | 1.79 | 73000 | - | 1174.93 MB | 1.48 | 1232 | +| RecursiveTypes | `Mapperly 4.3.1` | 1000000 | 116.05 ms | 16.639 ms | 25.16 ms | 116.055 | 0.29 | 68833 | 166 | 1098.63 MB | 1.38 | 1152 | + +### TotalAllTypes + +| Scenario | Method | MapOperations | Mean | StdDev | Error | Ns/Map | Ratio | Gen0 | Gen1 | Allocated | Alloc Ratio | Bytes/Map | +|-------------- |------------------------------------ |-------------- |-----------:|---------:|----------:|-------:|------:|-------:|-----:|----------:|------------:|----------:| +| TotalAllTypes | `Mapster 10.0.7` | 1000000 | 413.9 ms | 16.88 ms | 25.52 ms | 137.97 | 1.00 | 82000 | - | 1.29 GB | 1.00 | 461 | +| TotalAllTypes | `Mapster 10.0.7 (Roslyn)` | 1000000 | 546.4 ms | 29.01 ms | 43.86 ms | 182.12 | 1.32 | 80000 | - | 1.25 GB | 0.97 | 448 | +| TotalAllTypes | `Mapster 10.0.7 (FEC)` | 1000000 | 155.7 ms | 24.48 ms | 37.01 ms | 51.91 | 0.38 | 78800 | - | 1.23 GB | 0.95 | 440 | +| TotalAllTypes | `Mapster 10.0.7 (Codegen)` | 1000000 | 117.4 ms | 8.56 ms | 12.94 ms | 39.14 | 0.28 | 80250 | - | 1.25 GB | 0.97 | 448 | +| TotalAllTypes | `AutoMapper 14.0.0` | 1000000 | 681.4 ms | 76.25 ms | 115.27 ms | 227.13 | 1.65 | 202000 | 1000 | 3.16 GB | 2.45 | 1130 | +| TotalAllTypes | `Facet 6.5.5` | 1000000 | 677.4 ms | 33.30 ms | 55.96 ms | 225.80 | 1.64 | 329000 | 1000 | 5.14 GB | 3.99 | 1840 | +| TotalAllTypes | `Facet 6.5.5 (Compiled Projection)` | 1000000 | 1,337.4 ms | 42.95 ms | 64.94 ms | 445.81 | 3.24 | 122000 | - | 1.91 GB | 1.49 | 685 | +| TotalAllTypes | `Mapperly 4.3.1` | 1000000 | 141.5 ms | 3.04 ms | 5.81 ms | 47.10 | 0.34 | 99250 | 250 | 1.55 GB | 1.20 | 554 | + +## Interpretation notes + +- The fastest method depends on the object shape. Flat DTOs mostly measure low-level call overhead; recursive graphs and collection-heavy DTOs shift the bottleneck toward nested object creation and collection mapping. +- `Facet` constructor and compiled-projection paths are shown separately because they exercise different APIs and can trade CPU time for allocation behavior differently across scenarios. diff --git a/docs/articles/packages/ExpressionDebugging.md b/docs/articles/packages/ExpressionDebugging.md index e32f41dd..189d3ce9 100644 --- a/docs/articles/packages/ExpressionDebugging.md +++ b/docs/articles/packages/ExpressionDebugging.md @@ -50,12 +50,8 @@ TypeAdapterConfig.GlobalSettings.Compiler = exp => exp.CompileWithDebugInfo(opt) var dto = poco.Adapt(); //<-- you can step-into this function!! ``` -### Do not worry about performance +### Performance notes -In `RELEASE` mode, Roslyn compiler is actually faster than default dynamic compilation by 2x. -Here is the result: +In modern .NET runtimes, the Roslyn compiler path is mostly useful for step-into debugging and inspecting generated mapping code. In the current benchmark snapshot it performs close to the default Mapster compiler in steady-state execution. -| Method | Mean | StdDev | Error | Gen 0 | Gen 1 | Gen 2 | Allocated | -|-------------------------- |---------------:|-------------:|-------------:|------------:|------:|------:|-----------:| -| 'Mapster 4.1.1' | 115.31 ms | 0.849 ms | 1.426 ms | 31000.0000 | - | - | 124.36 MB | -| 'Mapster 4.1.1 (Roslyn)' | 53.55 ms | 0.342 ms | 0.654 ms | 31100.0000 | - | - | 124.36 MB | +See the [benchmark snapshot in README](../../../README.md#performance--memory-efficient) for current numbers. diff --git a/docs/articles/packages/FastExpressionCompiler.md b/docs/articles/packages/FastExpressionCompiler.md index a20df828..037d16aa 100644 --- a/docs/articles/packages/FastExpressionCompiler.md +++ b/docs/articles/packages/FastExpressionCompiler.md @@ -19,9 +19,11 @@ Then add following code on start up TypeAdapterConfig.GlobalSettings.Compiler = exp => exp.CompileFast(); ``` -That's it. Now your code will enjoy performance boost. Here is result. +That's it. Now your code will enjoy performance boost. Here is a current benchmark snapshot: -| Method | Mean | StdDev | Error | Gen 0 | Gen 1 | Gen 2 | Allocated | -|-------------------------- |---------------:|-------------:|-------------:|------------:|------:|------:|-----------:| -| 'Mapster 4.1.1' | 115.31 ms | 0.849 ms | 1.426 ms | 31000.0000 | - | - | 124.36 MB | -| 'Mapster 4.1.1 (FEC)' | 54.70 ms | 1.023 ms | 1.546 ms | 29600.0000 | - | - | 118.26 MB | +| Method | MapOperations | Mean | StdDev | Error | Ratio | Gen0 | Gen1 | Allocated | Alloc Ratio | +| -------- | -------------- | -----: | -------: | ------: | ------: | -----: | -----: | ----------: | ----------: | +| `Mapster 10.0.7` | 1000000 | 412,534 us | 2,704 us | 4,543 us | 1.00 | 77000 | - | 1243.59 MB | 1.00 | +| `Mapster 10.0.7 (FEC)` | 1000000 | 124,374 us | 1,290 us | 2,466 us | 0.30 | 74000 | - | 1182.56 MB | 0.95 | + +See the [benchmark snapshot in README](../../../README.md#performance--memory-efficient) for the full comparison. diff --git a/docs/articles/toc.yml b/docs/articles/toc.yml index 245cfb26..d218f825 100644 --- a/docs/articles/toc.yml +++ b/docs/articles/toc.yml @@ -15,4 +15,6 @@ items: topicHref: xref:Mapster.Packages.Async - name: Tools href: tools/toc.yml - topicHref: xref:Mapster.Tools.MapsterTool.Overview \ No newline at end of file + topicHref: xref:Mapster.Tools.MapsterTool.Overview +- name: Benchmarks + href: benchmarks.md \ No newline at end of file diff --git a/src/.editorconfig b/src/.editorconfig index 6600eeea..707ccf5f 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -1,4 +1,8 @@ -[*.cs] +[*.{csproj,props}] +indent_style = space +indent_size = 2 + +[*.cs] # S3220: Method calls should not resolve ambiguously to overloads with "params" dotnet_diagnostic.S3220.severity = suggestion diff --git a/src/Benchmark.Development/Benchmark.Development.csproj b/src/Benchmark.Development/Benchmark.Development.csproj index 2213d7a4..5b945fcf 100644 --- a/src/Benchmark.Development/Benchmark.Development.csproj +++ b/src/Benchmark.Development/Benchmark.Development.csproj @@ -3,14 +3,18 @@ Exe net10.0 - true - enable + 12.0 + enable - True + enable + Mapster.Benchmark.Development + + true Benchmark.Development.snk + True False - 7.4.0 - 12.0 + + 7.4.0 @@ -21,17 +25,17 @@ - - - - - - - - - - - + + + + + + + + - + + + + diff --git a/src/Benchmark.Development/Benchmarks/Config.cs b/src/Benchmark.Development/Benchmarks/Config.cs index 87dad0fe..9dfcf38b 100644 --- a/src/Benchmark.Development/Benchmarks/Config.cs +++ b/src/Benchmark.Development/Benchmarks/Config.cs @@ -1,14 +1,12 @@ -using Benchmark.Development; -using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Columns; using BenchmarkDotNet.Configs; using BenchmarkDotNet.Diagnosers; using BenchmarkDotNet.Exporters; using BenchmarkDotNet.Exporters.Csv; using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Loggers; -using Perfolizer.Models; -namespace Benchmark.Benchmarks +namespace Mapster.Benchmark.Development.Benchmarks { public class Config : ManualConfig { @@ -32,7 +30,7 @@ public Config() AddColumn(BaselineRatioColumn.RatioMean); AddColumnProvider(DefaultColumnProviders.Metrics); - + foreach (var version in MapsterVersion.Get()) diff --git a/src/Benchmark.Development/Benchmarks/TestAll.cs b/src/Benchmark.Development/Benchmarks/TestAll.cs index b3e28a76..5d0bac95 100644 --- a/src/Benchmark.Development/Benchmarks/TestAll.cs +++ b/src/Benchmark.Development/Benchmarks/TestAll.cs @@ -1,7 +1,7 @@ -using Benchmark.Classes; -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; +using Mapster.Benchmark.Development.Classes; -namespace Benchmark.Benchmarks +namespace Mapster.Benchmark.Development.Benchmarks { public class TestAll { diff --git a/src/Benchmark.Development/Benchmarks/TestComplexTypes.cs b/src/Benchmark.Development/Benchmarks/TestComplexTypes.cs index 62d7a764..df4b3beb 100644 --- a/src/Benchmark.Development/Benchmarks/TestComplexTypes.cs +++ b/src/Benchmark.Development/Benchmarks/TestComplexTypes.cs @@ -1,7 +1,7 @@ -using Benchmark.Classes; -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; +using Mapster.Benchmark.Development.Classes; -namespace Benchmark.Benchmarks +namespace Mapster.Benchmark.Development.Benchmarks { public class TestComplexTypes { @@ -15,7 +15,7 @@ public void MapsterTest() { TestAdaptHelper.TestMapsterAdapter(_customerInstance, Iterations); } - + [GlobalSetup(Target = nameof(MapsterTest))] public void SetupMapster() { diff --git a/src/Benchmark.Development/Benchmarks/TestSimpleTypes.cs b/src/Benchmark.Development/Benchmarks/TestSimpleTypes.cs index 8678a8ec..55aa0967 100644 --- a/src/Benchmark.Development/Benchmarks/TestSimpleTypes.cs +++ b/src/Benchmark.Development/Benchmarks/TestSimpleTypes.cs @@ -1,7 +1,7 @@ -using Benchmark.Classes; -using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Attributes; +using Mapster.Benchmark.Development.Classes; -namespace Benchmark.Benchmarks +namespace Mapster.Benchmark.Development.Benchmarks { public class TestSimpleTypes { @@ -15,7 +15,7 @@ public void MapsterTest() { TestAdaptHelper.TestMapsterAdapter(_fooInstance, Iterations); } - + [GlobalSetup(Target = nameof(MapsterTest))] public void SetupMapster() { diff --git a/src/Benchmark.Development/Classes/Customer.cs b/src/Benchmark.Development/Classes/Customer.cs index 5fac9cef..694d0b03 100644 --- a/src/Benchmark.Development/Classes/Customer.cs +++ b/src/Benchmark.Development/Classes/Customer.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace Benchmark.Classes +namespace Mapster.Benchmark.Development.Classes { public class Address { diff --git a/src/Benchmark.Development/Classes/Foo.cs b/src/Benchmark.Development/Classes/Foo.cs index 063541b8..5e3fa21e 100644 --- a/src/Benchmark.Development/Classes/Foo.cs +++ b/src/Benchmark.Development/Classes/Foo.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; - -namespace Benchmark.Classes +namespace Mapster.Benchmark.Development.Classes { public class Foo { diff --git a/src/Benchmark.Development/MapsterVersion.cs b/src/Benchmark.Development/MapsterVersion.cs index bfca6a69..391f06d5 100644 --- a/src/Benchmark.Development/MapsterVersion.cs +++ b/src/Benchmark.Development/MapsterVersion.cs @@ -1,4 +1,4 @@ -namespace Benchmark.Development +namespace Mapster.Benchmark.Development { internal static class MapsterVersion { @@ -6,7 +6,7 @@ internal static class MapsterVersion internal static string[] Get() => [ "7.4.0", - "9.0.0-pre01" + "10.0.0" ]; } } diff --git a/src/Benchmark.Development/Program.cs b/src/Benchmark.Development/Program.cs index 32e641ed..4e560383 100644 --- a/src/Benchmark.Development/Program.cs +++ b/src/Benchmark.Development/Program.cs @@ -1,5 +1,5 @@ -using Benchmark.Benchmarks; -using BenchmarkDotNet.Running; +using BenchmarkDotNet.Running; +using Mapster.Benchmark.Development.Benchmarks; var switcher = new BenchmarkSwitcher(new[] { diff --git a/src/Benchmark.Development/TestAdaptHelper.cs b/src/Benchmark.Development/TestAdaptHelper.cs index c6e06739..9e92fa43 100644 --- a/src/Benchmark.Development/TestAdaptHelper.cs +++ b/src/Benchmark.Development/TestAdaptHelper.cs @@ -1,12 +1,11 @@ -using Benchmark.Classes; -using Mapster; +using Mapster.Benchmark.Development.Classes; using System.Linq.Expressions; -namespace Benchmark +namespace Mapster.Benchmark.Development { public static class TestAdaptHelper { - + public static Customer SetupCustomerInstance() { return new Customer @@ -64,8 +63,8 @@ private static void SetupCompiler(MapsterCompilerType type) TypeAdapterConfig.GlobalSettings.Compiler = type switch { MapsterCompilerType.Default => _defaultCompiler, - // MapsterCompilerType.Roslyn => exp => exp.CompileWithDebugInfo(), - // MapsterCompilerType.FEC => exp => exp.CompileFast(), + // MapsterCompilerType.Roslyn => exp => exp.CompileWithDebugInfo(), + // MapsterCompilerType.FEC => exp => exp.CompileFast(), _ => throw new ArgumentOutOfRangeException(nameof(type)), }; } @@ -75,14 +74,14 @@ public static void ConfigureMapster(Foo fooInstance, MapsterCompilerType type) TypeAdapterConfig.GlobalSettings.Compile(typeof(Foo), typeof(Foo)); //recompile fooInstance.Adapt(); //exercise } - + public static void ConfigureMapster(Customer customerInstance, MapsterCompilerType type) { SetupCompiler(type); TypeAdapterConfig.GlobalSettings.Compile(typeof(Customer), typeof(CustomerDTO)); //recompile customerInstance.Adapt(); //exercise } - + public static void TestMapsterAdapter(TSrc item, int iterations) where TSrc : class where TDest : class, new() diff --git a/src/Benchmark/Benchmark.csproj b/src/Benchmark/Benchmark.csproj index b6872ab8..400a510f 100644 --- a/src/Benchmark/Benchmark.csproj +++ b/src/Benchmark/Benchmark.csproj @@ -1,45 +1,65 @@ - - - - Exe - net9.0 - true - **/*.g.cs - - - - True - True - CustomerMapper.tt - - - True - True - FooMapper.tt - - - - - - - - - - - - - - - - TextTemplatingFileGenerator - CustomerMapper.g.cs - - - TextTemplatingFileGenerator - FooMapper.g.cs - - - - - + + + Exe + net10.0 + + Mapster.Benchmark + enable + + true + **/*.g.cs + + + + + + + + + + + + + + + + + + + + + True + True + CustomerMapper.tt + + + True + True + FooMapper.tt + + + True + True + PersonMapper.tt + + + + + + TextTemplatingFileGenerator + CustomerMapper.g.cs + + + TextTemplatingFileGenerator + FooMapper.g.cs + + + TextTemplatingFileGenerator + PersonMapper.g.cs + + + + + + \ No newline at end of file diff --git a/src/Benchmark/Benchmarks/Config.cs b/src/Benchmark/Benchmarks/Config.cs index 5779b353..82bff156 100644 --- a/src/Benchmark/Benchmarks/Config.cs +++ b/src/Benchmark/Benchmarks/Config.cs @@ -6,7 +6,7 @@ using BenchmarkDotNet.Jobs; using BenchmarkDotNet.Loggers; -namespace Benchmark.Benchmarks +namespace Mapster.Benchmark.Benchmarks { public class Config : ManualConfig { @@ -19,14 +19,19 @@ public Config() AddExporter(HtmlExporter.Default); AddDiagnoser(MemoryDiagnoser.Default); + AddColumn(ScenarioColumn.Default); AddColumn(TargetMethodColumn.Method); + AddColumnProvider(DefaultColumnProviders.Params); AddColumn(StatisticColumn.Mean); + AddColumn(PerMapColumn.Nanoseconds); AddColumn(StatisticColumn.StdDev); AddColumn(StatisticColumn.Error); AddColumn(BaselineRatioColumn.RatioMean); + AddColumn(BaselineAllocationRatioColumn.RatioMean); AddColumnProvider(DefaultColumnProviders.Metrics); + AddColumn(PerMapColumn.Bytes); AddJob(Job.ShortRun .WithLaunchCount(1) diff --git a/src/Benchmark/Benchmarks/MappingBenchmarkBase.cs b/src/Benchmark/Benchmarks/MappingBenchmarkBase.cs new file mode 100644 index 00000000..932dbafa --- /dev/null +++ b/src/Benchmark/Benchmarks/MappingBenchmarkBase.cs @@ -0,0 +1,12 @@ +using BenchmarkDotNet.Attributes; + +namespace Mapster.Benchmark.Benchmarks +{ + public abstract class MappingBenchmarkBase + { + public IEnumerable MapOperationValues => new[] { 10_000, 100_000, 1_000_000 }; + + [ParamsSource(nameof(MapOperationValues))] + public int MapOperations { get; set; } + } +} \ No newline at end of file diff --git a/src/Benchmark/Benchmarks/PerMapColumn.cs b/src/Benchmark/Benchmarks/PerMapColumn.cs new file mode 100644 index 00000000..2eebfef2 --- /dev/null +++ b/src/Benchmark/Benchmarks/PerMapColumn.cs @@ -0,0 +1,86 @@ +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Reports; +using BenchmarkDotNet.Running; + +namespace Mapster.Benchmark.Benchmarks +{ + public abstract class PerMapColumn : IColumn + { + public static readonly IColumn Nanoseconds = new NanosecondsPerMapColumn(); + public static readonly IColumn Bytes = new BytesPerMapColumn(); + + public abstract string Id { get; } + public abstract string ColumnName { get; } + public abstract string Legend { get; } + public abstract ColumnCategory Category { get; } + + public UnitType UnitType => UnitType.Dimensionless; + public int PriorityInCategory => 100; + public bool IsNumeric => true; + public bool AlwaysShow => true; + public bool IsAvailable(Summary summary) => true; + public bool IsDefault(Summary summary, BenchmarkCase benchmarkCase) => false; + + public abstract string GetValue(Summary summary, BenchmarkCase benchmarkCase, SummaryStyle style); + + public string GetValue(Summary summary, BenchmarkCase benchmarkCase) + => GetValue(summary, benchmarkCase, summary.Style); + + protected static long GetLogicalMapCount(BenchmarkCase benchmarkCase) + { + var mapOperations = 1; + var parameter = benchmarkCase.Parameters.Items + .FirstOrDefault(p => p.Name == nameof(MappingBenchmarkBase.MapOperations)); + + if (parameter?.Value is int value && value > 0) + mapOperations = value; + + //TestTotalAllTypes includes 3 separate mapping calls per benchmark call + var mappingsPerBenchmarkCall = benchmarkCase.Descriptor.Type == typeof(TestTotalAllTypes) ? 3 : 1; + return (long)mapOperations * mappingsPerBenchmarkCall; + } + + protected static string Format(double value, SummaryStyle style) + => value.ToString("0.###", style.CultureInfo); + + public override string ToString() => ColumnName; + + private sealed class NanosecondsPerMapColumn : PerMapColumn + { + public override string Id => nameof(NanosecondsPerMapColumn); + public override string ColumnName => "Ns/Map"; + public override string Legend => "Mean nanoseconds per single mapping call"; + public override ColumnCategory Category => ColumnCategory.Statistics; + + public override string GetValue(Summary summary, BenchmarkCase benchmarkCase, SummaryStyle style) + { + if (!summary.HasReport(benchmarkCase)) + return "?"; + + var meanNanoseconds = summary[benchmarkCase].ResultStatistics?.Mean; + return meanNanoseconds.HasValue + ? Format(meanNanoseconds.Value / GetLogicalMapCount(benchmarkCase), style) + : "?"; + } + } + + private sealed class BytesPerMapColumn : PerMapColumn + { + public override string Id => nameof(BytesPerMapColumn); + public override string ColumnName => "Bytes/Map"; + public override string Legend => "Allocated bytes per single mapping call"; + public override ColumnCategory Category => ColumnCategory.Metric; + + public override string GetValue(Summary summary, BenchmarkCase benchmarkCase, SummaryStyle style) + { + if (!summary.HasReport(benchmarkCase)) + return "?"; + + var allocatedBytesPerBenchmarkCall = summary[benchmarkCase].GcStats.GetBytesAllocatedPerOperation(benchmarkCase); + return allocatedBytesPerBenchmarkCall.HasValue + ? Format((double)allocatedBytesPerBenchmarkCall.Value / GetLogicalMapCount(benchmarkCase), style) + : "?"; + } + } + } +} \ No newline at end of file diff --git a/src/Benchmark/Benchmarks/ScenarioColumn.cs b/src/Benchmark/Benchmarks/ScenarioColumn.cs new file mode 100644 index 00000000..67362598 --- /dev/null +++ b/src/Benchmark/Benchmarks/ScenarioColumn.cs @@ -0,0 +1,37 @@ +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Reports; +using BenchmarkDotNet.Running; + +namespace Mapster.Benchmark.Benchmarks +{ + /// + /// Adds a "Scenario" column to the joined summary so each row clearly indicates which benchmark class + /// (TestFlatTypes / TestRecursiveTypes / TestComplexTypes / TestTotalAllTypes) produced it. + /// + public class ScenarioColumn : IColumn + { + public static readonly IColumn Default = new ScenarioColumn(); + + public string Id => nameof(ScenarioColumn); + public string ColumnName => "Scenario"; + public string Legend => "Benchmark class the row belongs to"; + public UnitType UnitType => UnitType.Dimensionless; + public ColumnCategory Category => ColumnCategory.Job; + public int PriorityInCategory => -10; + public bool IsNumeric => false; + public bool AlwaysShow => true; + public bool IsAvailable(Summary summary) => true; + public bool IsDefault(Summary summary, BenchmarkCase benchmarkCase) => false; + + public string GetValue(Summary summary, BenchmarkCase benchmarkCase) + { + var name = benchmarkCase.Descriptor.Type.Name; + return name.StartsWith("Test") ? name[4..] : name; + } + + public string GetValue(Summary summary, BenchmarkCase benchmarkCase, SummaryStyle style) + => GetValue(summary, benchmarkCase); + + public override string ToString() => ColumnName; + } +} diff --git a/src/Benchmark/Benchmarks/TestAll.cs b/src/Benchmark/Benchmarks/TestAll.cs deleted file mode 100644 index 17581ddf..00000000 --- a/src/Benchmark/Benchmarks/TestAll.cs +++ /dev/null @@ -1,111 +0,0 @@ -using Benchmark.Classes; -using BenchmarkDotNet.Attributes; - -namespace Benchmark.Benchmarks -{ - public class TestAll - { - private Foo _fooInstance; - private Customer _customerInstance; - - [Params(100_000)]//, 1_000_000)] - public int Iterations { get; set; } - - [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion}")] - public void MapsterTest() - { - TestAdaptHelper.TestMapsterAdapter(_fooInstance, Iterations); - TestAdaptHelper.TestMapsterAdapter(_customerInstance, Iterations); - } - - [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Roslyn)")] - public void RoslynTest() - { - TestAdaptHelper.TestMapsterAdapter(_fooInstance, Iterations); - TestAdaptHelper.TestMapsterAdapter(_customerInstance, Iterations); - } - - [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (FEC)")] - public void FecTest() - { - TestAdaptHelper.TestMapsterAdapter(_fooInstance, Iterations); - TestAdaptHelper.TestMapsterAdapter(_customerInstance, Iterations); - } - - [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Codegen)")] - public void CodegenTest() - { - TestAdaptHelper.TestCodeGen(_fooInstance, Iterations); - TestAdaptHelper.TestCodeGen(_customerInstance, Iterations); - } - - [Benchmark(Description = $"ExpressMapper {TestAdaptHelper.ExpressionMapperVersion}")] - public void ExpressMapperTest() - { - TestAdaptHelper.TestExpressMapper(_fooInstance, Iterations); - TestAdaptHelper.TestExpressMapper(_customerInstance, Iterations); - } - - //[Benchmark(Description = $"AutoMapper {TestAdaptHelper.AutoMapperVersion}")] - //public void AutoMapperTest() - //{ - // TestAdaptHelper.TestAutoMapper(_fooInstance, Iterations); - // TestAdaptHelper.TestAutoMapper(_customerInstance, Iterations); - //} - - [GlobalSetup(Target = nameof(MapsterTest))] - public void SetupMapster() - { - _fooInstance = TestAdaptHelper.SetupFooInstance(); - _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - TestAdaptHelper.ConfigureMapster(_fooInstance, MapsterCompilerType.Default); - TestAdaptHelper.ConfigureMapster(_customerInstance, MapsterCompilerType.Default); - } - - [GlobalSetup(Target = nameof(RoslynTest))] - public void SetupRoslyn() - { - _fooInstance = TestAdaptHelper.SetupFooInstance(); - _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - TestAdaptHelper.ConfigureMapster(_fooInstance, MapsterCompilerType.Roslyn); - TestAdaptHelper.ConfigureMapster(_customerInstance, MapsterCompilerType.Roslyn); - } - - [GlobalSetup(Target = nameof(FecTest))] - public void SetupFec() - { - _fooInstance = TestAdaptHelper.SetupFooInstance(); - _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - TestAdaptHelper.ConfigureMapster(_fooInstance, MapsterCompilerType.FEC); - TestAdaptHelper.ConfigureMapster(_customerInstance, MapsterCompilerType.FEC); - } - - [GlobalSetup(Target = nameof(CodegenTest))] - public void SetupCodegen() - { - //_fooInstance = TestAdaptHelper.SetupFooInstance(); - //_customerInstance = TestAdaptHelper.SetupCustomerInstance(); - //FooMapper.Map(_fooInstance); - //CustomerMapper.Map(_customerInstance); - } - - [GlobalSetup(Target = nameof(ExpressMapperTest))] - public void SetupExpressMapper() - { - _fooInstance = TestAdaptHelper.SetupFooInstance(); - _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - TestAdaptHelper.ConfigureExpressMapper(_fooInstance); - TestAdaptHelper.ConfigureExpressMapper(_customerInstance); - } - - //[GlobalSetup(Target = nameof(AutoMapperTest))] - //public void SetupAutoMapper() - //{ - // _fooInstance = TestAdaptHelper.SetupFooInstance(); - // _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - // TestAdaptHelper.ConfigureAutoMapper(_fooInstance); - // TestAdaptHelper.ConfigureAutoMapper(_customerInstance); - //} - - } -} \ No newline at end of file diff --git a/src/Benchmark/Benchmarks/TestComplexTypes.cs b/src/Benchmark/Benchmarks/TestComplexTypes.cs index a61ca9d3..4f27a54b 100644 --- a/src/Benchmark/Benchmarks/TestComplexTypes.cs +++ b/src/Benchmark/Benchmarks/TestComplexTypes.cs @@ -1,91 +1,87 @@ -using Benchmark.Classes; using BenchmarkDotNet.Attributes; +using Mapster.Benchmark.Classes; +using Mapster.Benchmark.Comparisons; -namespace Benchmark.Benchmarks +namespace Mapster.Benchmark.Benchmarks { - public class TestComplexTypes + // Customer/CustomerDTO: nested object of different type, two collection shape changes + // (Address[] -> AddressDTO[], ICollection
-> List) and a flattening rule (AddressCity <- Address.City). + public class TestComplexTypes : MappingBenchmarkBase { - private Customer _customerInstance; + private static readonly Func CustomerFacetCompiled = + CustomerFacetDto.Projection.Compile(); - [Params(1000, 10_000, 100_000, 1_000_000)] - public int Iterations { get; set; } + private Customer _customer; - [Benchmark] + [Benchmark(Baseline = true, Description = $"Mapster {TestAdaptHelper.MapsterVersion}")] public void MapsterTest() - { - TestAdaptHelper.TestMapsterAdapter(_customerInstance, Iterations); - } - + => TestAdaptHelper.Loop(_customer, src => src.Adapt(), MapOperations); + [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Roslyn)")] public void RoslynTest() - { - TestAdaptHelper.TestMapsterAdapter(_customerInstance, Iterations); - } + => TestAdaptHelper.Loop(_customer, src => src.Adapt(), MapOperations); [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (FEC)")] public void FecTest() - { - TestAdaptHelper.TestMapsterAdapter(_customerInstance, Iterations); - } + => TestAdaptHelper.Loop(_customer, src => src.Adapt(), MapOperations); - [Benchmark] + [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Codegen)")] public void CodegenTest() - { - TestAdaptHelper.TestCodeGen(_customerInstance, Iterations); - } + => TestAdaptHelper.Loop(_customer, CustomerMapper.Map, MapOperations); - [Benchmark] - public void ExpressMapperTest() - { - TestAdaptHelper.TestExpressMapper(_customerInstance, Iterations); - } + [Benchmark(Description = $"AutoMapper {TestAdaptHelper.AutoMapperVersion}")] + public void AutoMapperTest() + => TestAdaptHelper.Loop(_customer, src => TestAdaptHelper.AutoMapper.Map(src), MapOperations); + + [Benchmark(Description = $"Facet {TestAdaptHelper.FacetVersion}")] + public void FacetTest() + => TestAdaptHelper.Loop(_customer, src => new CustomerFacetDto(src), MapOperations); + + [Benchmark(Description = $"Facet {TestAdaptHelper.FacetVersion} (Compiled Projection)")] + public void FacetCompiledTest() + => TestAdaptHelper.Loop(_customer, CustomerFacetCompiled, MapOperations); - //[Benchmark] - //public void AutoMapperTest() - //{ - // TestAdaptHelper.TestAutoMapper(_customerInstance, Iterations); - //} + [Benchmark(Description = $"Mapperly {TestAdaptHelper.MapperlyVersion}")] + public void MapperlyTest() + => TestAdaptHelper.Loop(_customer, MapperlyMappings.MapCustomer, MapOperations); [GlobalSetup(Target = nameof(MapsterTest))] public void SetupMapster() { - _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - TestAdaptHelper.ConfigureMapster(_customerInstance, MapsterCompilerType.Default); + _customer = TestAdaptHelper.SetupCustomerInstance(); + TestAdaptHelper.UseMapsterCompiler(MapsterCompilerType.Default); + _ = _customer.Adapt(); } [GlobalSetup(Target = nameof(RoslynTest))] public void SetupRoslyn() { - _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - TestAdaptHelper.ConfigureMapster(_customerInstance, MapsterCompilerType.Roslyn); + _customer = TestAdaptHelper.SetupCustomerInstance(); + TestAdaptHelper.UseMapsterCompiler(MapsterCompilerType.Roslyn); + _ = _customer.Adapt(); } [GlobalSetup(Target = nameof(FecTest))] public void SetupFec() { - _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - TestAdaptHelper.ConfigureMapster(_customerInstance, MapsterCompilerType.FEC); + _customer = TestAdaptHelper.SetupCustomerInstance(); + TestAdaptHelper.UseMapsterCompiler(MapsterCompilerType.FEC); + _ = _customer.Adapt(); } [GlobalSetup(Target = nameof(CodegenTest))] - public void SetupCodegen() - { - //_customerInstance = TestAdaptHelper.SetupCustomerInstance(); - //CustomerMapper.Map(_customerInstance); - } + public void SetupCodegen() => _customer = TestAdaptHelper.SetupCustomerInstance(); - [GlobalSetup(Target = nameof(ExpressMapperTest))] - public void SetupExpressMapper() - { - _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - TestAdaptHelper.ConfigureExpressMapper(_customerInstance); - } + [GlobalSetup(Target = nameof(AutoMapperTest))] + public void SetupAutoMapper() => _customer = TestAdaptHelper.SetupCustomerInstance(); + + [GlobalSetup(Target = nameof(FacetTest))] + public void SetupFacet() => _customer = TestAdaptHelper.SetupCustomerInstance(); + + [GlobalSetup(Target = nameof(FacetCompiledTest))] + public void SetupFacetCompiled() => _customer = TestAdaptHelper.SetupCustomerInstance(); - //[GlobalSetup(Target = nameof(AutoMapperTest))] - //public void SetupAutoMapper() - //{ - // _customerInstance = TestAdaptHelper.SetupCustomerInstance(); - // TestAdaptHelper.ConfigureAutoMapper(_customerInstance); - //} + [GlobalSetup(Target = nameof(MapperlyTest))] + public void SetupMapperly() => _customer = TestAdaptHelper.SetupCustomerInstance(); } -} \ No newline at end of file +} diff --git a/src/Benchmark/Benchmarks/TestFlatTypes.cs b/src/Benchmark/Benchmarks/TestFlatTypes.cs new file mode 100644 index 00000000..f860894f --- /dev/null +++ b/src/Benchmark/Benchmarks/TestFlatTypes.cs @@ -0,0 +1,87 @@ +using BenchmarkDotNet.Attributes; +using Mapster.Benchmark.Classes; +using Mapster.Benchmark.Comparisons; + +namespace Mapster.Benchmark.Benchmarks +{ + // FlatType DTO: simple property-to-property copy, no nesting, no collections. + // Highlights pure per-call overhead (delegate dispatch, allocation rate, IL quality of property copy). + public class TestFlatTypes : MappingBenchmarkBase + { + private static readonly Func PersonFacetCompiled = + PersonFacetDto.Projection.Compile(); + + private Person _person; + + [Benchmark(Baseline = true, Description = $"Mapster {TestAdaptHelper.MapsterVersion}")] + public void MapsterTest() + => TestAdaptHelper.Loop(_person, src => src.Adapt(), MapOperations); + + [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Roslyn)")] + public void RoslynTest() + => TestAdaptHelper.Loop(_person, src => src.Adapt(), MapOperations); + + [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (FEC)")] + public void FecTest() + => TestAdaptHelper.Loop(_person, src => src.Adapt(), MapOperations); + + [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Codegen)")] + public void CodegenTest() + => TestAdaptHelper.Loop(_person, PersonMapper.Map, MapOperations); + + [Benchmark(Description = $"AutoMapper {TestAdaptHelper.AutoMapperVersion}")] + public void AutoMapperTest() + => TestAdaptHelper.Loop(_person, src => TestAdaptHelper.AutoMapper.Map(src), MapOperations); + + [Benchmark(Description = $"Facet {TestAdaptHelper.FacetVersion}")] + public void FacetTest() + => TestAdaptHelper.Loop(_person, src => new PersonFacetDto(src), MapOperations); + + [Benchmark(Description = $"Facet {TestAdaptHelper.FacetVersion} (Compiled Projection)")] + public void FacetCompiledTest() + => TestAdaptHelper.Loop(_person, PersonFacetCompiled, MapOperations); + + [Benchmark(Description = $"Mapperly {TestAdaptHelper.MapperlyVersion}")] + public void MapperlyTest() + => TestAdaptHelper.Loop(_person, MapperlyMappings.MapPerson, MapOperations); + + [GlobalSetup(Target = nameof(MapsterTest))] + public void SetupMapster() + { + _person = TestAdaptHelper.SetupPersonInstance(); + TestAdaptHelper.UseMapsterCompiler(MapsterCompilerType.Default); + _ = _person.Adapt(); + } + + [GlobalSetup(Target = nameof(RoslynTest))] + public void SetupRoslyn() + { + _person = TestAdaptHelper.SetupPersonInstance(); + TestAdaptHelper.UseMapsterCompiler(MapsterCompilerType.Roslyn); + _ = _person.Adapt(); + } + + [GlobalSetup(Target = nameof(FecTest))] + public void SetupFec() + { + _person = TestAdaptHelper.SetupPersonInstance(); + TestAdaptHelper.UseMapsterCompiler(MapsterCompilerType.FEC); + _ = _person.Adapt(); + } + + [GlobalSetup(Target = nameof(CodegenTest))] + public void SetupCodegen() => _person = TestAdaptHelper.SetupPersonInstance(); + + [GlobalSetup(Target = nameof(AutoMapperTest))] + public void SetupAutoMapper() => _person = TestAdaptHelper.SetupPersonInstance(); + + [GlobalSetup(Target = nameof(FacetTest))] + public void SetupFacet() => _person = TestAdaptHelper.SetupPersonInstance(); + + [GlobalSetup(Target = nameof(FacetCompiledTest))] + public void SetupFacetCompiled() => _person = TestAdaptHelper.SetupPersonInstance(); + + [GlobalSetup(Target = nameof(MapperlyTest))] + public void SetupMapperly() => _person = TestAdaptHelper.SetupPersonInstance(); + } +} \ No newline at end of file diff --git a/src/Benchmark/Benchmarks/TestRecursiveTypes.cs b/src/Benchmark/Benchmarks/TestRecursiveTypes.cs new file mode 100644 index 00000000..b6693bdc --- /dev/null +++ b/src/Benchmark/Benchmarks/TestRecursiveTypes.cs @@ -0,0 +1,86 @@ +using BenchmarkDotNet.Attributes; +using Mapster.Benchmark.Classes; +using Mapster.Benchmark.Comparisons; + +namespace Mapster.Benchmark.Benchmarks +{ + // Self-recursive graph with nested references and collections. + // Source: Foo, Destination: FooDTO. + public class TestRecursiveTypes : MappingBenchmarkBase + { + private static readonly Func FooFacetCompiled = FooFacetDto.Projection.Compile(); + + private Foo _foo; + + [Benchmark(Baseline = true, Description = $"Mapster {TestAdaptHelper.MapsterVersion}")] + public void MapsterTest() + => TestAdaptHelper.Loop(_foo, src => src.Adapt(), MapOperations); + + [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Roslyn)")] + public void RoslynTest() + => TestAdaptHelper.Loop(_foo, src => src.Adapt(), MapOperations); + + [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (FEC)")] + public void FecTest() + => TestAdaptHelper.Loop(_foo, src => src.Adapt(), MapOperations); + + [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Codegen)")] + public void CodegenTest() + => TestAdaptHelper.Loop(_foo, FooMapper.Map, MapOperations); + + [Benchmark(Description = $"AutoMapper {TestAdaptHelper.AutoMapperVersion}")] + public void AutoMapperTest() + => TestAdaptHelper.Loop(_foo, src => TestAdaptHelper.AutoMapper.Map(src), MapOperations); + + [Benchmark(Description = $"Facet {TestAdaptHelper.FacetVersion}")] + public void FacetTest() + => TestAdaptHelper.Loop(_foo, src => new FooFacetDto(src), MapOperations); + + [Benchmark(Description = $"Facet {TestAdaptHelper.FacetVersion} (Compiled Projection)")] + public void FacetCompiledTest() + => TestAdaptHelper.Loop(_foo, FooFacetCompiled, MapOperations); + + [Benchmark(Description = $"Mapperly {TestAdaptHelper.MapperlyVersion}")] + public void MapperlyTest() + => TestAdaptHelper.Loop(_foo, MapperlyMappings.MapFoo, MapOperations); + + [GlobalSetup(Target = nameof(MapsterTest))] + public void SetupMapster() + { + _foo = TestAdaptHelper.SetupFooInstance(); + TestAdaptHelper.UseMapsterCompiler(MapsterCompilerType.Default); + _ = _foo.Adapt(); + } + + [GlobalSetup(Target = nameof(RoslynTest))] + public void SetupRoslyn() + { + _foo = TestAdaptHelper.SetupFooInstance(); + TestAdaptHelper.UseMapsterCompiler(MapsterCompilerType.Roslyn); + _ = _foo.Adapt(); + } + + [GlobalSetup(Target = nameof(FecTest))] + public void SetupFec() + { + _foo = TestAdaptHelper.SetupFooInstance(); + TestAdaptHelper.UseMapsterCompiler(MapsterCompilerType.FEC); + _ = _foo.Adapt(); + } + + [GlobalSetup(Target = nameof(CodegenTest))] + public void SetupCodegen() => _foo = TestAdaptHelper.SetupFooInstance(); + + [GlobalSetup(Target = nameof(AutoMapperTest))] + public void SetupAutoMapper() => _foo = TestAdaptHelper.SetupFooInstance(); + + [GlobalSetup(Target = nameof(FacetTest))] + public void SetupFacet() => _foo = TestAdaptHelper.SetupFooInstance(); + + [GlobalSetup(Target = nameof(FacetCompiledTest))] + public void SetupFacetCompiled() => _foo = TestAdaptHelper.SetupFooInstance(); + + [GlobalSetup(Target = nameof(MapperlyTest))] + public void SetupMapperly() => _foo = TestAdaptHelper.SetupFooInstance(); + } +} \ No newline at end of file diff --git a/src/Benchmark/Benchmarks/TestSimpleTypes.cs b/src/Benchmark/Benchmarks/TestSimpleTypes.cs deleted file mode 100644 index 81f507e5..00000000 --- a/src/Benchmark/Benchmarks/TestSimpleTypes.cs +++ /dev/null @@ -1,92 +0,0 @@ -using Benchmark.Classes; -using BenchmarkDotNet.Attributes; - -namespace Benchmark.Benchmarks -{ - public class TestSimpleTypes - { - private Foo _fooInstance; - - [Params(1000, 10_000, 100_000, 1_000_000)] - public int Iterations { get; set; } - - [Benchmark] - public void MapsterTest() - { - TestAdaptHelper.TestMapsterAdapter(_fooInstance, Iterations); - } - - [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Roslyn)")] - public void RoslynTest() - { - TestAdaptHelper.TestMapsterAdapter(_fooInstance, Iterations); - } - - [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (FEC)")] - public void FecTest() - { - TestAdaptHelper.TestMapsterAdapter(_fooInstance, Iterations); - } - - [Benchmark] - public void CodegenTest() - { - TestAdaptHelper.TestCodeGen(_fooInstance, Iterations); - } - - [Benchmark] - public void ExpressMapperTest() - { - TestAdaptHelper.TestExpressMapper(_fooInstance, Iterations); - } - - //[Benchmark] - //public void AutoMapperTest() - //{ - // TestAdaptHelper.TestAutoMapper(_fooInstance, Iterations); - //} - - - [GlobalSetup(Target = nameof(MapsterTest))] - public void SetupMapster() - { - _fooInstance = TestAdaptHelper.SetupFooInstance(); - TestAdaptHelper.ConfigureMapster(_fooInstance, MapsterCompilerType.Default); - } - - [GlobalSetup(Target = nameof(RoslynTest))] - public void SetupRoslyn() - { - _fooInstance = TestAdaptHelper.SetupFooInstance(); - TestAdaptHelper.ConfigureMapster(_fooInstance, MapsterCompilerType.Roslyn); - } - - [GlobalSetup(Target = nameof(FecTest))] - public void SetupFec() - { - _fooInstance = TestAdaptHelper.SetupFooInstance(); - TestAdaptHelper.ConfigureMapster(_fooInstance, MapsterCompilerType.FEC); - } - - [GlobalSetup(Target = nameof(CodegenTest))] - public void SetupCodegen() - { - //_fooInstance = TestAdaptHelper.SetupFooInstance(); - //FooMapper.Map(_fooInstance); - } - - [GlobalSetup(Target = nameof(ExpressMapperTest))] - public void SetupExpressMapper() - { - _fooInstance = TestAdaptHelper.SetupFooInstance(); - TestAdaptHelper.ConfigureExpressMapper(_fooInstance); - } - - //[GlobalSetup(Target = nameof(AutoMapperTest))] - //public void SetupAutoMapper() - //{ - // _fooInstance = TestAdaptHelper.SetupFooInstance(); - // TestAdaptHelper.ConfigureAutoMapper(_fooInstance); - //} - } -} \ No newline at end of file diff --git a/src/Benchmark/Benchmarks/TestTotalAllTypes.cs b/src/Benchmark/Benchmarks/TestTotalAllTypes.cs new file mode 100644 index 00000000..22c1c295 --- /dev/null +++ b/src/Benchmark/Benchmarks/TestTotalAllTypes.cs @@ -0,0 +1,143 @@ +using BenchmarkDotNet.Attributes; +using Mapster.Benchmark.Classes; +using Mapster.Benchmark.Comparisons; + +namespace Mapster.Benchmark.Benchmarks +{ + /// + /// Total benchmark across all three sample shapes: + /// + /// -> (FlatType DTO) + /// -> (self-recursive graph) + /// -> (nested + collections + flattening) + /// + /// Each [Benchmark] iteration runs all three scenarios via + /// loops, so the reported Mean is their total time. + /// + public class TestTotalAllTypes : MappingBenchmarkBase + { + private static readonly Func FooFacetCompiled = FooFacetDto.Projection.Compile(); + private static readonly Func CustomerFacetCompiled = CustomerFacetDto.Projection.Compile(); + private static readonly Func PersonFacetCompiled = PersonFacetDto.Projection.Compile(); + + private Person _person; + private Foo _foo; + private Customer _customer; + + [Benchmark(Baseline = true, Description = $"Mapster {TestAdaptHelper.MapsterVersion}")] + public void MapsterTest() + { + TestAdaptHelper.Loop(_person, src => src.Adapt(), MapOperations); + TestAdaptHelper.Loop(_foo, src => src.Adapt(), MapOperations); + TestAdaptHelper.Loop(_customer, src => src.Adapt(), MapOperations); + } + + [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Roslyn)")] + public void RoslynTest() + { + TestAdaptHelper.Loop(_person, src => src.Adapt(), MapOperations); + TestAdaptHelper.Loop(_foo, src => src.Adapt(), MapOperations); + TestAdaptHelper.Loop(_customer, src => src.Adapt(), MapOperations); + } + + [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (FEC)")] + public void FecTest() + { + TestAdaptHelper.Loop(_person, src => src.Adapt(), MapOperations); + TestAdaptHelper.Loop(_foo, src => src.Adapt(), MapOperations); + TestAdaptHelper.Loop(_customer, src => src.Adapt(), MapOperations); + } + + [Benchmark(Description = $"Mapster {TestAdaptHelper.MapsterVersion} (Codegen)")] + public void CodegenTest() + { + TestAdaptHelper.Loop(_person, PersonMapper.Map, MapOperations); + TestAdaptHelper.Loop(_foo, FooMapper.Map, MapOperations); + TestAdaptHelper.Loop(_customer, CustomerMapper.Map, MapOperations); + } + + [Benchmark(Description = $"AutoMapper {TestAdaptHelper.AutoMapperVersion}")] + public void AutoMapperTest() + { + TestAdaptHelper.Loop(_person, src => TestAdaptHelper.AutoMapper.Map(src), MapOperations); + TestAdaptHelper.Loop(_foo, src => TestAdaptHelper.AutoMapper.Map(src), MapOperations); + TestAdaptHelper.Loop(_customer, src => TestAdaptHelper.AutoMapper.Map(src), MapOperations); + } + + [Benchmark(Description = $"Facet {TestAdaptHelper.FacetVersion}")] + public void FacetTest() + { + TestAdaptHelper.Loop(_person, src => new PersonFacetDto(src), MapOperations); + TestAdaptHelper.Loop(_foo, src => new FooFacetDto(src), MapOperations); + TestAdaptHelper.Loop(_customer, src => new CustomerFacetDto(src), MapOperations); + } + + [Benchmark(Description = $"Facet {TestAdaptHelper.FacetVersion} (Compiled Projection)")] + public void FacetCompiledTest() + { + TestAdaptHelper.Loop(_person, PersonFacetCompiled, MapOperations); + TestAdaptHelper.Loop(_foo, FooFacetCompiled, MapOperations); + TestAdaptHelper.Loop(_customer, CustomerFacetCompiled, MapOperations); + } + + [Benchmark(Description = $"Mapperly {TestAdaptHelper.MapperlyVersion}")] + public void MapperlyTest() + { + TestAdaptHelper.Loop(_person, MapperlyMappings.MapPerson, MapOperations); + TestAdaptHelper.Loop(_foo, MapperlyMappings.MapFoo, MapOperations); + TestAdaptHelper.Loop(_customer, MapperlyMappings.MapCustomer, MapOperations); + } + + [GlobalSetup(Target = nameof(MapsterTest))] + public void SetupMapster() + { + SetupInstances(); + TestAdaptHelper.UseMapsterCompiler(MapsterCompilerType.Default); + _ = _person.Adapt(); + _ = _foo.Adapt(); + _ = _customer.Adapt(); + } + + [GlobalSetup(Target = nameof(RoslynTest))] + public void SetupRoslyn() + { + SetupInstances(); + TestAdaptHelper.UseMapsterCompiler(MapsterCompilerType.Roslyn); + _ = _person.Adapt(); + _ = _foo.Adapt(); + _ = _customer.Adapt(); + } + + [GlobalSetup(Target = nameof(FecTest))] + public void SetupFec() + { + SetupInstances(); + TestAdaptHelper.UseMapsterCompiler(MapsterCompilerType.FEC); + _ = _person.Adapt(); + _ = _foo.Adapt(); + _ = _customer.Adapt(); + } + + [GlobalSetup(Target = nameof(CodegenTest))] + public void SetupCodegen() => SetupInstances(); + + [GlobalSetup(Target = nameof(AutoMapperTest))] + public void SetupAutoMapper() => SetupInstances(); + + [GlobalSetup(Target = nameof(FacetTest))] + public void SetupFacet() => SetupInstances(); + + [GlobalSetup(Target = nameof(FacetCompiledTest))] + public void SetupFacetCompiled() => SetupInstances(); + + [GlobalSetup(Target = nameof(MapperlyTest))] + public void SetupMapperly() => SetupInstances(); + + private void SetupInstances() + { + _person = TestAdaptHelper.SetupPersonInstance(); + _foo = TestAdaptHelper.SetupFooInstance(); + _customer = TestAdaptHelper.SetupCustomerInstance(); + } + } +} \ No newline at end of file diff --git a/src/Benchmark/Classes/Customer.cs b/src/Benchmark/Classes/Customer.cs index 5fac9cef..d78c6ca3 100644 --- a/src/Benchmark/Classes/Customer.cs +++ b/src/Benchmark/Classes/Customer.cs @@ -1,22 +1,6 @@ -using System.Collections.Generic; - -namespace Benchmark.Classes +namespace Mapster.Benchmark.Classes { - public class Address - { - public int Id { get; set; } - public string Street { get; set; } - public string City { get; set; } - public string Country { get; set; } - } - - public class AddressDTO - { - public int Id { get; set; } - public string City { get; set; } - public string Country { get; set; } - } - + // Nested object and collection mapping source. public class Customer { public int Id { get; set; } @@ -28,6 +12,7 @@ public class Customer public ICollection
WorkAddresses { get; set; } } + // DTO with flattening and collection shape changes. public class CustomerDTO { public int Id { get; set; } @@ -38,4 +23,19 @@ public class CustomerDTO public List WorkAddresses { get; set; } public string AddressCity { get; set; } } + + public class Address + { + public int Id { get; set; } + public string Street { get; set; } + public string City { get; set; } + public string Country { get; set; } + } + + public class AddressDTO + { + public int Id { get; set; } + public string City { get; set; } + public string Country { get; set; } + } } diff --git a/src/Benchmark/Classes/Foo.cs b/src/Benchmark/Classes/Foo.cs index 063541b8..87c7f685 100644 --- a/src/Benchmark/Classes/Foo.cs +++ b/src/Benchmark/Classes/Foo.cs @@ -1,8 +1,6 @@ -using System; -using System.Collections.Generic; - -namespace Benchmark.Classes +namespace Mapster.Benchmark.Classes { + // Deep recursive graph with collections. public class Foo { public string Name { get; set; } @@ -29,4 +27,32 @@ public class Foo public IEnumerable Ints { get; set; } } + + // DTO copy of Foo. + public class FooDTO + { + public string Name { get; set; } + + public int Int32 { get; set; } + + public long Int64 { get; set; } + + public int? NullInt { get; set; } + + public float Floatn { get; set; } + + public double Doublen { get; set; } + + public DateTime DateTime { get; set; } + + public FooDTO Foo1 { get; set; } + + public IEnumerable Foos { get; set; } + + public FooDTO[] FooArr { get; set; } + + public int[] IntArr { get; set; } + + public IEnumerable Ints { get; set; } + } } diff --git a/src/Benchmark/Classes/Person.cs b/src/Benchmark/Classes/Person.cs new file mode 100644 index 00000000..37173a91 --- /dev/null +++ b/src/Benchmark/Classes/Person.cs @@ -0,0 +1,28 @@ +namespace Mapster.Benchmark.Classes +{ + // FlatType POCO: no nested types, no collections. Used to exercise the "best case" + // mapping path - a simple property-to-property copy with primitive/string values. + public class Person + { + public int Id { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string Email { get; set; } + public int Age { get; set; } + public DateTime BirthDate { get; set; } + public decimal Salary { get; set; } + public bool IsActive { get; set; } + } + + public class PersonDTO + { + public int Id { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string Email { get; set; } + public int Age { get; set; } + public DateTime BirthDate { get; set; } + public decimal Salary { get; set; } + public bool IsActive { get; set; } + } +} diff --git a/src/Benchmark/Comparisons/FacetModels.cs b/src/Benchmark/Comparisons/FacetModels.cs new file mode 100644 index 00000000..d557e1e8 --- /dev/null +++ b/src/Benchmark/Comparisons/FacetModels.cs @@ -0,0 +1,27 @@ +using Facet; +using Mapster.Benchmark.Classes; + +namespace Mapster.Benchmark.Comparisons +{ + [Facet(typeof(Foo), NestedFacets = new[] { typeof(FooFacetDto) }, MaxDepth = 2)] + public partial class FooFacetDto + { + } + + [Facet(typeof(Address))] + public partial class AddressFacetDto + { + } + + [Facet(typeof(Customer), NestedFacets = new[] { typeof(AddressFacetDto) })] + public partial class CustomerFacetDto + { + [MapFrom("Address.City")] + public string AddressCity { get; set; } + } + + [Facet(typeof(Person))] + public partial class PersonFacetDto + { + } +} \ No newline at end of file diff --git a/src/Benchmark/Comparisons/MapperlyModels.cs b/src/Benchmark/Comparisons/MapperlyModels.cs new file mode 100644 index 00000000..3552a8d5 --- /dev/null +++ b/src/Benchmark/Comparisons/MapperlyModels.cs @@ -0,0 +1,76 @@ +using Mapster.Benchmark.Classes; +using Riok.Mapperly.Abstractions; + +namespace Mapster.Benchmark.Comparisons +{ + public class FooMapperlyDto + { + public string Name { get; set; } + public int Int32 { get; set; } + public long Int64 { get; set; } + public int? NullInt { get; set; } + public float Floatn { get; set; } + public double Doublen { get; set; } + public DateTime DateTime { get; set; } + public FooMapperlyDto Foo1 { get; set; } + public IEnumerable Foos { get; set; } + public FooMapperlyDto[] FooArr { get; set; } + public int[] IntArr { get; set; } + public IEnumerable Ints { get; set; } + } + + public class AddressMapperlyDto + { + public int Id { get; set; } + public string Street { get; set; } + public string City { get; set; } + public string Country { get; set; } + } + + public class AddressSummaryMapperlyDto + { + public int Id { get; set; } + public string City { get; set; } + public string Country { get; set; } + } + + public class CustomerMapperlyDto + { + public int Id { get; set; } + public string Name { get; set; } + public AddressMapperlyDto Address { get; set; } + public AddressSummaryMapperlyDto HomeAddress { get; set; } + public AddressSummaryMapperlyDto[] Addresses { get; set; } + public List WorkAddresses { get; set; } + public string AddressCity { get; set; } + } + + public class PersonMapperlyDto + { + public int Id { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public string Email { get; set; } + public int Age { get; set; } + public DateTime BirthDate { get; set; } + public decimal Salary { get; set; } + public bool IsActive { get; set; } + } + + [Riok.Mapperly.Abstractions.Mapper(UseDeepCloning = true)] + public static partial class MapperlyMappings + { + public static partial AddressMapperlyDto MapAddress(Address source); + + [MapperIgnoreSource(nameof(Address.Street))] + public static partial AddressSummaryMapperlyDto MapAddressSummary(Address source); + + public static partial FooMapperlyDto MapFoo(Foo source); + + [MapperIgnoreSource(nameof(Customer.Credit))] + [MapProperty("Address.City", nameof(CustomerMapperlyDto.AddressCity))] + public static partial CustomerMapperlyDto MapCustomer(Customer source); + + public static partial PersonMapperlyDto MapPerson(Person source); + } +} \ No newline at end of file diff --git a/src/Benchmark/CustomerMapper.g.cs b/src/Benchmark/CustomerMapper.g.cs index 947ff47d..7b4dbd27 100644 --- a/src/Benchmark/CustomerMapper.g.cs +++ b/src/Benchmark/CustomerMapper.g.cs @@ -1,89 +1,88 @@ - -using System.Collections.Generic; -using Benchmark.Classes; +using System.Collections.Generic; +using Mapster.Benchmark.Classes; -namespace Benchmark +namespace Mapster.Benchmark { - public static partial class CustomerMapper - { - public static CustomerDTO Map(Customer p1) - { - return p1 == null ? null : new CustomerDTO() - { - Id = p1.Id, - Name = p1.Name, - Address = p1.Address == null ? null : new Address() - { - Id = p1.Address.Id, - Street = p1.Address.Street, - City = p1.Address.City, - Country = p1.Address.Country - }, - HomeAddress = p1.HomeAddress == null ? null : new AddressDTO() - { - Id = p1.HomeAddress.Id, - City = p1.HomeAddress.City, - Country = p1.HomeAddress.Country - }, - Addresses = func1(p1.Addresses), - WorkAddresses = func2(p1.WorkAddresses), - AddressCity = p1.Address == null ? null : p1.Address.City - }; - } - - private static AddressDTO[] func1(Address[] p2) - { - if (p2 == null) - { - return null; - } - AddressDTO[] result = new AddressDTO[p2.Length]; - - int v = 0; - - int i = 0; - int len = p2.Length; - - while (i < len) - { - Address item = p2[i]; - result[v++] = item == null ? null : new AddressDTO() - { - Id = item.Id, - City = item.City, - Country = item.Country - }; - i++; - } - return result; - - } - - private static List func2(ICollection
p3) - { - if (p3 == null) - { - return null; - } - List result = new List(p3.Count); - - ICollection list = result; - - IEnumerator
enumerator = p3.GetEnumerator(); - - while (enumerator.MoveNext()) - { - Address item = enumerator.Current; - list.Add(item == null ? null : new AddressDTO() - { - Id = item.Id, - City = item.City, - Country = item.Country - }); - } - return result; - - } - } -} + public static partial class CustomerMapper + { + public static CustomerDTO Map(Customer p1) + { + return p1 == null ? null : new CustomerDTO() + { + Id = p1.Id, + Name = p1.Name, + Address = p1.Address == null ? null : new Address() + { + Id = p1.Address.Id, + Street = p1.Address.Street, + City = p1.Address.City, + Country = p1.Address.Country + }, + HomeAddress = p1.HomeAddress == null ? null : new AddressDTO() + { + Id = p1.HomeAddress.Id, + City = p1.HomeAddress.City, + Country = p1.HomeAddress.Country + }, + Addresses = func1(p1.Addresses), + WorkAddresses = func2(p1.WorkAddresses), + AddressCity = p1.Address == null ? null : p1.Address.City + }; + } + + private static AddressDTO[] func1(Address[] p2) + { + if (p2 == null) + { + return null; + } + AddressDTO[] result = new AddressDTO[p2.Length]; + + int v = 0; + + int i = 0; + int len = p2.Length; + + while (i < len) + { + Address item = p2[i]; + result[v++] = item == null ? null : new AddressDTO() + { + Id = item.Id, + City = item.City, + Country = item.Country + }; + i++; + } + return result; + + } + + private static List func2(ICollection
p3) + { + if (p3 == null) + { + return null; + } + List result = new List(p3.Count); + + ICollection list = result; + + IEnumerator
enumerator = p3.GetEnumerator(); + + while (enumerator.MoveNext()) + { + Address item = enumerator.Current; + list.Add(item == null ? null : new AddressDTO() + { + Id = item.Id, + City = item.City, + Country = item.Country + }); + } + return result; + + } + } +} \ No newline at end of file diff --git a/src/Benchmark/CustomerMapper.tt b/src/Benchmark/CustomerMapper.tt index 7b316abd..3fa01aa8 100644 --- a/src/Benchmark/CustomerMapper.tt +++ b/src/Benchmark/CustomerMapper.tt @@ -8,7 +8,7 @@ <#@ Assembly Name="$(TargetDir)/$(ProjectName).dll" #> <#@ Assembly Name="$(TargetDir)/Mapster.dll" #> <#@ Assembly Name="$(TargetDir)/ExpressionTranslator.dll" #> -<#@ import namespace="Benchmark.Classes" #> +<#@ import namespace="Mapster.Benchmark.Classes" #> <#@ import namespace="ExpressionDebugger" #> <#@ import namespace="Mapster" #> <# @@ -18,7 +18,7 @@ { IsStatic = true, MethodName = "Map", - Namespace = "Benchmark", + Namespace = "Mapster.Benchmark", TypeName = "CustomerMapper" }; var code = foo.BuildAdapter() diff --git a/src/Benchmark/FooMapper.g.cs b/src/Benchmark/FooMapper.g.cs index 21edb8f9..0eabd97f 100644 --- a/src/Benchmark/FooMapper.g.cs +++ b/src/Benchmark/FooMapper.g.cs @@ -1,72 +1,71 @@ - using System; using System.Linq; -using Benchmark.Classes; using Mapster; +using Mapster.Benchmark.Classes; using Mapster.Utils; -namespace Benchmark +namespace Mapster.Benchmark { - public static partial class FooMapper - { - public static Foo Map(Foo p1) - { - return p1 == null ? null : new Foo() - { - Name = p1.Name, - Int32 = p1.Int32, - Int64 = p1.Int64, - NullInt = p1.NullInt, - Floatn = p1.Floatn, - Doublen = p1.Doublen, - DateTime = p1.DateTime, - Foo1 = Map(p1.Foo1), - Foos = p1.Foos == null ? null : p1.Foos.Select(func1), - FooArr = func2(p1.FooArr), - IntArr = func3(p1.IntArr), - Ints = p1.Ints == null ? null : MapsterHelper.ToEnumerable(p1.Ints) - }; - } - - private static Foo func1(Foo p2) - { - return Map(p2); - } - - private static Foo[] func2(Foo[] p3) - { - if (p3 == null) - { - return null; - } - Foo[] result = new Foo[p3.Length]; - - int v = 0; - - int i = 0; - int len = p3.Length; - - while (i < len) - { - Foo item = p3[i]; - result[v++] = Map(item); - i++; - } - return result; - - } - - private static int[] func3(int[] p4) - { - if (p4 == null) - { - return null; - } - int[] result = new int[p4.Length]; - Array.Copy(p4, 0, result, 0, p4.Length); - return result; - - } - } + public static partial class FooMapper + { + public static FooDTO Map(Foo p1) + { + return p1 == null ? null : new FooDTO() + { + Name = p1.Name, + Int32 = p1.Int32, + Int64 = p1.Int64, + NullInt = p1.NullInt, + Floatn = p1.Floatn, + Doublen = p1.Doublen, + DateTime = p1.DateTime, + Foo1 = Map(p1.Foo1), + Foos = p1.Foos == null ? null : p1.Foos.Select(func1), + FooArr = func2(p1.FooArr), + IntArr = func3(p1.IntArr), + Ints = p1.Ints == null ? null : MapsterHelper.ToEnumerable(p1.Ints) + }; + } + + private static FooDTO func1(Foo p2) + { + return Map(p2); + } + + private static FooDTO[] func2(Foo[] p3) + { + if (p3 == null) + { + return null; + } + FooDTO[] result = new FooDTO[p3.Length]; + + int v = 0; + + int i = 0; + int len = p3.Length; + + while (i < len) + { + Foo item = p3[i]; + result[v++] = Map(item); + i++; + } + return result; + + } + + private static int[] func3(int[] p4) + { + if (p4 == null) + { + return null; + } + int[] result = new int[p4.Length]; + Array.Copy(p4, 0, result, 0, p4.Length); + return result; + + } + } } diff --git a/src/Benchmark/FooMapper.tt b/src/Benchmark/FooMapper.tt index 5279368d..bb1e41b3 100644 --- a/src/Benchmark/FooMapper.tt +++ b/src/Benchmark/FooMapper.tt @@ -8,7 +8,7 @@ <#@ Assembly Name="$(TargetDir)/Benchmark.dll" #> <#@ Assembly Name="$(TargetDir)/Mapster.dll" #> <#@ Assembly Name="$(TargetDir)/ExpressionTranslator.dll" #> -<#@ import namespace="Benchmark.Classes" #> +<#@ import namespace="Mapster.Benchmark.Classes" #> <#@ import namespace="ExpressionDebugger" #> <#@ import namespace="Mapster" #> <# @@ -18,12 +18,12 @@ { IsStatic = true, MethodName = "Map", - Namespace = "Benchmark", + Namespace = "Mapster.Benchmark", TypeName = "FooMapper" }; var code = foo.BuildAdapter() - .CreateMapExpression() + .CreateMapExpression() .ToScript(def); - code = code.Replace("TypeAdapter.Map.Invoke", "Map"); + code = code.Replace("TypeAdapter.Map.Invoke", "Map"); WriteLine(code); #> \ No newline at end of file diff --git a/src/Benchmark/PersonMapper.g.cs b/src/Benchmark/PersonMapper.g.cs new file mode 100644 index 00000000..5060ae02 --- /dev/null +++ b/src/Benchmark/PersonMapper.g.cs @@ -0,0 +1,23 @@ +using Mapster.Benchmark.Classes; + + +namespace Mapster.Benchmark +{ + public static partial class PersonMapper + { + public static PersonDTO Map(Person p1) + { + return p1 == null ? null : new PersonDTO() + { + Id = p1.Id, + FirstName = p1.FirstName, + LastName = p1.LastName, + Email = p1.Email, + Age = p1.Age, + BirthDate = p1.BirthDate, + Salary = p1.Salary, + IsActive = p1.IsActive + }; + } + } +} \ No newline at end of file diff --git a/src/Benchmark/PersonMapper.tt b/src/Benchmark/PersonMapper.tt new file mode 100644 index 00000000..58c609ef --- /dev/null +++ b/src/Benchmark/PersonMapper.tt @@ -0,0 +1,25 @@ +<#@ template debug="true" language="C#" #> +<#@ output extension=".g.cs" #> +using Mapster.Benchmark.Classes; + + +namespace Mapster.Benchmark +{ + public static partial class PersonMapper + { + public static PersonDTO Map(Person p1) + { + return p1 == null ? null : new PersonDTO() + { + Id = p1.Id, + FirstName = p1.FirstName, + LastName = p1.LastName, + Email = p1.Email, + Age = p1.Age, + BirthDate = p1.BirthDate, + Salary = p1.Salary, + IsActive = p1.IsActive + }; + } + } +} \ No newline at end of file diff --git a/src/Benchmark/Program.cs b/src/Benchmark/Program.cs index 9c11da46..7da13343 100644 --- a/src/Benchmark/Program.cs +++ b/src/Benchmark/Program.cs @@ -1,7 +1,7 @@ -using Benchmark.Benchmarks; -using BenchmarkDotNet.Running; +using BenchmarkDotNet.Running; +using Mapster.Benchmark.Benchmarks; -namespace Benchmark +namespace Mapster.Benchmark { class Program { @@ -9,9 +9,10 @@ static void Main(string[] args) { var switcher = new BenchmarkSwitcher(new[] { - typeof(TestSimpleTypes), + typeof(TestFlatTypes), + typeof(TestRecursiveTypes), typeof(TestComplexTypes), - typeof(TestAll), + typeof(TestTotalAllTypes), }); switcher.Run(args, new Config()); diff --git a/src/Benchmark/TestAdaptHelper.cs b/src/Benchmark/TestAdaptHelper.cs index cf8918e2..c1221113 100644 --- a/src/Benchmark/TestAdaptHelper.cs +++ b/src/Benchmark/TestAdaptHelper.cs @@ -1,160 +1,123 @@ -using Benchmark.Classes; +using AutoMapper; using FastExpressionCompiler; -using Mapster; -using System; -using System.Collections.Generic; +using Mapster.Benchmark.Classes; using System.Linq.Expressions; -namespace Benchmark +namespace Mapster.Benchmark { + public enum MapsterCompilerType + { + Default, + Roslyn, + FEC, + } + + /// + /// Minimal shared helper for the comparison benchmarks. Contains only setup data, + /// the AutoMapper instance, a Mapster compiler switch and a generic hot-loop driver. + /// public static class TestAdaptHelper { - //private static readonly IMapper _mapper = new Mapper(new MapperConfiguration(cfg => - //{ - // cfg.CreateMap(); - // cfg.CreateMap(); - // cfg.CreateMap(); - // cfg.CreateMap(); - //})); + public const string MapsterVersion = "10.0.7"; + public const string AutoMapperVersion = "14.0.0"; + public const string FacetVersion = "6.5.5"; + public const string MapperlyVersion = "4.3.1"; + + private static readonly Func DefaultMapsterCompiler = + TypeAdapterConfig.GlobalSettings.Compiler; - public const string MapsterVersion = "10.0.0"; - public const string AutoMapperVersion = "13.0.0"; - public const string ExpressionTranslatorVersion = "2.5.0"; - public const string ExpressionMapperVersion = "1.9.1"; + public static readonly IMapper AutoMapper = new MapperConfiguration(cfg => + { + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap() + .ForMember(d => d.AddressCity, + o => o.MapFrom(s => s.Address != null ? s.Address.City : null)); + cfg.CreateMap(); + }).CreateMapper(); - public static Customer SetupCustomerInstance() + public static Foo SetupFooInstance() => new Foo { - return new Customer + Name = "foo", + Int32 = 12, + Int64 = 123123, + NullInt = 16, + DateTime = DateTime.Now, + Doublen = 2312112, + Foo1 = new Foo { Name = "foo one" }, + Foos = new List { - Address = new Address { City = "istanbul", Country = "turkey", Id = 1, Street = "istiklal cad." }, - HomeAddress = new Address { City = "istanbul", Country = "turkey", Id = 2, Street = "istiklal cad." }, - Id = 1, - Name = "Eduardo Najera", - Credit = 234.7m, - WorkAddresses = new List
- { - new Address {City = "istanbul", Country = "turkey", Id = 5, Street = "istiklal cad."}, - new Address {City = "izmir", Country = "turkey", Id = 6, Street = "konak"} - }, - Addresses = new[] - { - new Address {City = "istanbul", Country = "turkey", Id = 3, Street = "istiklal cad."}, - new Address {City = "izmir", Country = "turkey", Id = 4, Street = "konak"} - } - }; - } + new Foo { Name = "j1", Int64 = 123, NullInt = 321 }, + new Foo { Name = "j2", Int32 = 12345, NullInt = 54321 }, + new Foo { Name = "j3", Int32 = 12345, NullInt = 54321 }, + }, + FooArr = new[] + { + new Foo { Name = "a1" }, + new Foo { Name = "a2" }, + new Foo { Name = "a3" }, + }, + IntArr = new[] { 1, 2, 3, 4, 5 }, + Ints = new[] { 7, 8, 9 }, + }; - public static Foo SetupFooInstance() + public static Customer SetupCustomerInstance() => new Customer { - return new Foo + Id = 1, + Name = "Eduardo Najera", + Credit = 234.7m, + Address = new Address { Id = 1, City = "istanbul", Country = "turkey", Street = "istiklal cad." }, + HomeAddress = new Address { Id = 2, City = "istanbul", Country = "turkey", Street = "istiklal cad." }, + Addresses = new[] { - Name = "foo", - Int32 = 12, - Int64 = 123123, - NullInt = 16, - DateTime = DateTime.Now, - Doublen = 2312112, - Foo1 = new Foo { Name = "foo one" }, - Foos = new List - { - new Foo {Name = "j1", Int64 = 123, NullInt = 321}, - new Foo {Name = "j2", Int32 = 12345, NullInt = 54321}, - new Foo {Name = "j3", Int32 = 12345, NullInt = 54321} - }, - FooArr = new[] - { - new Foo {Name = "a1"}, - new Foo {Name = "a2"}, - new Foo {Name = "a3"} - }, - IntArr = new[] { 1, 2, 3, 4, 5 }, - Ints = new[] { 7, 8, 9 } - }; - } + new Address { Id = 3, City = "istanbul", Country = "turkey", Street = "istiklal cad." }, + new Address { Id = 4, City = "izmir", Country = "turkey", Street = "konak" }, + }, + WorkAddresses = new List
+ { + new Address { Id = 5, City = "istanbul", Country = "turkey", Street = "istiklal cad." }, + new Address { Id = 6, City = "izmir", Country = "turkey", Street = "konak" }, + }, + }; - private static readonly Func _defaultCompiler = TypeAdapterConfig.GlobalSettings.Compiler; + public static Person SetupPersonInstance() => new Person + { + Id = 42, + FirstName = "Eduardo", + LastName = "Najera", + Email = "eduardo@example.com", + Age = 39, + BirthDate = new DateTime(1986, 7, 11), + Salary = 12345.67m, + IsActive = true, + }; - private static void SetupCompiler(MapsterCompilerType type) + /// + /// Switches Mapster's global expression compiler. Call this from a [GlobalSetup] before warming up the mapping. + /// + public static void UseMapsterCompiler(MapsterCompilerType type) { TypeAdapterConfig.GlobalSettings.Compiler = type switch { - MapsterCompilerType.Default => _defaultCompiler, - MapsterCompilerType.Roslyn => exp => exp.CompileWithDebugInfo(), - MapsterCompilerType.FEC => exp => exp.CompileFast(), + MapsterCompilerType.Default => DefaultMapsterCompiler, + MapsterCompilerType.Roslyn => e => e.CompileWithDebugInfo(), + MapsterCompilerType.FEC => e => e.CompileFast(), _ => throw new ArgumentOutOfRangeException(nameof(type)), }; } - public static void ConfigureMapster(Foo fooInstance, MapsterCompilerType type) - { - SetupCompiler(type); - TypeAdapterConfig.GlobalSettings.Compile(typeof(Foo), typeof(Foo)); //recompile - fooInstance.Adapt(); //exercise - } - public static void ConfigureExpressMapper(Foo fooInstance) - { - //ExpressMapper.Mapper.Map(fooInstance); //exercise - } - //public static void ConfigureAutoMapper(Foo fooInstance) - //{ - // _mapper.Map(fooInstance); //exercise - //} - - public static void ConfigureMapster(Customer customerInstance, MapsterCompilerType type) - { - SetupCompiler(type); - TypeAdapterConfig.GlobalSettings.Compile(typeof(Customer), typeof(CustomerDTO)); //recompile - customerInstance.Adapt(); //exercise - } - public static void ConfigureExpressMapper(Customer customerInstance) - { - //ExpressMapper.Mapper.Map(customerInstance); //exercise - } - //public static void ConfigureAutoMapper(Customer customerInstance) - //{ - // _mapper.Map(customerInstance); //exercise - //} - - public static void TestMapsterAdapter(TSrc item, int iterations) - where TSrc : class - where TDest : class, new() - { - Loop(item, get => get.Adapt(), iterations); - } - - public static void TestExpressMapper(TSrc item, int iterations) - where TSrc : class - where TDest : class, new() - { - //Loop(item, get => ExpressMapper.Mapper.Map(get), iterations); - } - - //public static void TestAutoMapper(TSrc item, int iterations) - // where TSrc : class - // where TDest : class, new() - //{ - // Loop(item, get => _mapper.Map(get), iterations); - //} - - public static void TestCodeGen(Foo item, int iterations) - { - //Loop(item, get => FooMapper.Map(get), iterations); - } - public static void TestCodeGen(Customer item, int iterations) + /// + /// Hot loop: invokes on times. + /// Keeps the last result alive so the JIT can't dead-code-eliminate the call. + /// + public static void Loop(TSrc src, Func map, int count) { - //Loop(item, get => CustomerMapper.Map(get), iterations); - } + TDest r = default!; + for (var i = 0; i < count; i++) + r = map(src); - private static void Loop(T item, Action action, int iterations) - { - for (var i = 0; i < iterations; i++) action(item); + GC.KeepAlive(r); } } - - public enum MapsterCompilerType - { - Default, - Roslyn, - FEC, - } -} \ No newline at end of file +}