Benchmarks
Microbenchmarks are important part of DotNext library to prove than important features can speed up performance of your application or, at least, is not slowing down it.
The configuration of all benchmarks:
Parameter | Configuration |
---|---|
Host | .NET Core 3.1.10 (CoreCLR 4.700.20.51601, CoreFX 4.700.20.51901), X64 RyuJIT |
Job | .NET Core 3.1.10 (CoreCLR 4.700.20.51601, CoreFX 4.700.20.51901), X64 RyuJIT |
LaunchCount | 1 |
RunStrategy | Throughput |
OS | Ubuntu 20.04.1 |
CPU | Intel Core i7-6700HQ CPU 2.60GHz (Skylake) |
Number of CPUs | 1 |
Physical Cores | 4 |
Logical Cores | 8 |
RAM | 24 GB |
You can run benchmarks using Bench
build configuration as follows:
cd <dotnext-clone-path>/src/DotNext.Benchmarks
dotnet run -c Bench
Bitwise Equality
This benchmark compares performance of BitwiseComparer<T>.Equals with overloaded equality ==
operator. Testing data types: Guid and custom value type with multiple fields.
Method | Mean | Error | StdDev | Median |
---|---|---|---|---|
BitwiseComparer<Guid>.Equals |
4.0946 ns | 0.1103 ns | 0.1546 ns | 4.0572 ns |
Guid.Equals |
2.6408 ns | 0.0679 ns | 0.0567 ns | 2.6404 ns |
ReadOnlySpan.SequenceEqual for Guid |
4.7230 ns | 0.1173 ns | 0.1992 ns | 4.6229 ns |
BitwiseComparer<LargeStruct>.Equals |
20.8664 ns | 0.5215 ns | 1.4537 ns | 21.0593 ns |
LargeStruct.Equals |
44.2600 ns | 0.8155 ns | 0.7230 ns | 44.4745 ns |
ReadOnlySpan.SequenceEqual for LargeStruct |
24.2414 ns | 0.3574 ns | 0.2984 ns | 24.2423 ns |
Bitwise equality method has the better performance than field-by-field equality check especially for large value types because BitwiseEquals
utilizes low-level optimizations performed by .NET Core according with underlying hardware such as SIMD. Additionally, it uses aligned memory access in constrast to SequenceEqual method.
Equality of Arrays
This benchmark compares performance of ReadOnlySpan.SequenceEqual, OneDimensionalArray.BitwiseEquals and manual equality check between two arrays using for
loop. The benchmark is applied to the array of Guid elements.
Method | Mean | Error | StdDev | Median |
---|---|---|---|---|
Guid[].BitwiseEquals , small arrays (~10 elements) |
10.13 ns | 0.083 ns | 0.077 ns | 10.15 ns |
ReadOnlySpan<Guid>.SequenceEqual , small arrays (~10 elements) |
46.68 ns | 0.200 ns | 0.187 ns | 46.69 ns |
for loop, small arrays (~10 elements) |
61.23 ns | 0.144 ns | 0.127 ns | 61.27 ns |
Guid[].BitwiseEquals , large arrays (~100 elements) |
52.30 ns | 0.130 ns | 0.121 ns | 52.25 ns |
ReadOnlySpan<Guid>.SequenceEqual , large arrays (~100 elements) |
437.96 ns | 8.669 ns | 8.109 ns | 444.44 ns |
for loop, large arrays (~100 elements) |
607.54 ns | 2.572 ns | 2.406 ns | 607.48 ns |
Bitwise equality is an absolute winner for equality check between arrays of any size.
Bitwise Hash Code
This benchmark compares performance of BitwiseComparer<T>.GetHashCode and GetHashCode
instance method for the types Guid and custom value type with multiple fields.
Method | Mean | Error | StdDev |
---|---|---|---|
Guid.GetHashCode |
1.446 ns | 0.0137 ns | 0.0128 ns |
BitwiseComparer<Guid>.GetHashCode |
5.646 ns | 0.0256 ns | 0.0214 ns |
BitwiseComparer<LargeStructure>.GetHashCode |
40.257 ns | 0.1833 ns | 0.1714 ns |
LargeStructure.GetHashCode |
33.694 ns | 0.7107 ns | 1.5749 ns |
Bitwise hash code algorithm is slower than JIT optimizations introduced by .NET Core 3.1 but still convenient in complex cases.
Bytes to Hex
This benchmark demonstrates performance of DotNext.Span.ToHex
extension method that allows to convert arbitrary set of bytes into hexadecimal form. It is compatible withSpan<T>
data type in constrast to BitConverter.ToString method.
Method | Num of Bytes | Mean | Error | StdDev |
---|---|---|---|---|
BitConverter.ToString |
16 bytes | 66.22 ns | 0.481 ns | 0.427 ns |
Span.ToHex |
16 bytes | 78.10 ns | 0.583 ns | 0.487 ns |
BitConverter.ToString |
64 bytes | 219.96 ns | 1.742 ns | 1.454 ns |
Span.ToHex |
64 bytes | 158.60 ns | 0.966 ns | 0.904 ns |
BitConverter.ToString |
128 bytes | 447.40 ns | 3.989 ns | 3.331 ns |
Span.ToHex |
128 bytes | 258.58 ns | 1.359 ns | 1.205 ns |
BitConverter.ToString |
256 bytes | 838.54 ns | 12.750 ns | 9.955 ns |
Span.ToHex |
256 bytes | 496.05 ns | 3.077 ns | 2.402 ns |
Span.ToHex
demonstrates the best performance especially for large arrays.
Fast Reflection
The next series of benchmarks demonstrate performance of strongly typed reflection provided by DotNext Reflection library.
Property Getter
This benchmark demonstrates overhead of getting instance property value caused by different mechanisms:
- Using FastMember library
- Using strongly typed reflection from DotNext Reflection library:
Type<IndexOfCalculator>.Property<int>.RequireGetter
- Using strongly typed reflection from DotNext Reflection library using special delegate type
Function<object, ValueTuple, object>
. It is assumed that instance type and property type is not known at compile type (th) so the delegate performs type check on every call. - Classic .NET reflection
Method | Mean | Error | StdDev |
---|---|---|---|
Direct call | 12.48 ns | 0.352 ns | 1.025 ns |
Reflection with DotNext using delegate type MemberGetter<IndexOfCalculator, int> |
15.09 ns | 0.294 ns | 0.327 ns |
Reflection with DotNext using DynamicInvoker |
24.16 ns | 0.469 ns | 0.688 ns |
Reflection with DotNext using delegate type Function<object, ValueTuple, object> |
27.17 ns | 0.523 ns | 0.957 ns |
ObjectAccess class from FastMember library |
55.84 ns | 1.083 ns | 1.013 ns |
.NET reflection | 183.45 ns | 2.580 ns | 2.414 ns |
Strongly typed reflection provided by DotNext Reflection library has the same performance as direct call.
Instance Method Call
This benchmark demonstrates overhead of calling instance method IndexOf
of type string caused by different mechanisms:
- Using strongly typed reflection from DotNext Reflection library:
Type<string>.Method<char, int>.Require<int>(nameof(string.IndexOf))
- Using strongly typed reflection from DotNext Reflection library using special delegate type:
Type<string>.RequireMethod<(char, int), int>(nameof(string.IndexOf));
- Using strongly typed reflection from DotNext Reflection library using special delegate type:
Function<object, (object, object), object>
. It is assumed that types of all parameters are not known at compile time. - Classic .NET reflection
The benchmark uses series of different strings to run the same set of tests. Worst case means that character lookup is performed for a string that doesn't contain the given character. Best case means that character lookup is performed for a string that has the given character.
Method | Condition | Mean | Error | StdDev | Median |
---|---|---|---|---|---|
Direct call | Empty String | 5.027 ns | 0.0192 ns | 0.0179 ns | 5.029 ns |
Direct call | Best Case | 11.265 ns | 0.2595 ns | 0.5473 ns | 11.038 ns |
Direct call | Worst Case | 13.702 ns | 0.0381 ns | 0.0357 ns | 13.701 ns |
Reflection with DotNext using delegate type Func<string, char, int, int> |
Empty String | 8.292 ns | 0.1272 ns | 0.1062 ns | 8.292 ns |
Reflection with DotNext using delegate type Func<string, char, int, int> |
Best Case | 10.786 ns | 0.0263 ns | 0.0233 ns | 10.787 ns |
Reflection with DotNext using delegate type Func<string, char, int, int> |
Worst Case | 16.156 ns | 0.0532 ns | 0.0445 ns | 16.164 ns |
Reflection with DotNext using delegate type Function<string, (char, int), int> |
Empty String | 12.920 ns | 0.0802 ns | 0.0711 ns | 12.912 ns |
Reflection with DotNext using delegate type Function<string, (char, int), int> |
Best Case | 16.764 ns | 0.3341 ns | 0.6357 ns | 16.547 ns |
Reflection with DotNext using delegate type Function<string, (char, int), int> |
Worst Case | 19.723 ns | 0.0467 ns | 0.0437 ns | 19.718 ns |
Reflection with DotNext using delegate type Function<object, (object, object), object> |
Empty String | 30.837 ns | 0.9323 ns | 2.6749 ns | 29.444 ns |
Reflection with DotNext using delegate type Function<object, (object, object), object> |
Best Case | 34.313 ns | 0.6684 ns | 1.1168 ns | 34.725 ns |
Reflection with DotNext using delegate type Function<object, (object, object), object> |
Worst Case | 38.278 ns | 0.2308 ns | 0.2159 ns | 38.261 ns |
.NET reflection | Empty String | 330.086 ns | 6.4941 ns | 10.4867 ns | 325.026 ns |
.NET reflection | Best Case | 332.212 ns | 6.6117 ns | 11.5798 ns | 326.741 ns |
.NET reflection | Worst Case | 339.153 ns | 2.0429 ns | 1.7059 ns | 339.822 ns |
DotNext Reflection library offers the best result in case when delegate type exactly matches to the reflected method with small overhead measured in a few nanoseconds.
Static Method Call
This benchmark demonstrates overhead of calling static method TryParse
of type decimal caused by different mechanisms:
- Using strongly typed reflection from DotNext Reflection library:
Type<decimal>.Method.Get<TryParseDelegate>(nameof(decimal.TryParse), MethodLookup.Static)
. The delegate type exactly matches to the reflected method signature:delegate bool TryParseDelegate(string text, out decimal result)
- Using strongly typed reflection from DotNext Reflection library using special delegate type:
Function<(string text, decimal result), bool>
- Using strongly typed reflection from DotNext Reflection library using special delegate type:
Function<(object text, object result), object>
. It is assumed that types of all parameters are not known at compile time. - Classic .NET reflection
Method | Mean | Error | StdDev | Median |
---|---|---|---|---|
Direct call | 127.5 ns | 0.74 ns | 0.65 ns | 127.2 ns |
Reflection with DotNext using delegate type TryParseDelegate |
127.4 ns | 0.41 ns | 0.36 ns | 127.1 ns |
Reflection with DotNext using delegate type Function<(string text, decimal result), bool> |
142.3 ns | 0.50 ns | 0.42 ns | 142.9 ns |
Reflection with DotNext using delegate type Function<(object text, object result), object> |
154.8 ns | 2.70 ns | 2.40 ns | 155.1 ns |
.NET reflection | 516.0 ns | 4.61 ns | 4.09 ns | 514.13 ns |
Strongly typed reflection provided by DotNext Reflection library has the same performance as direct call.
Atomic Access to Arbitrary Value Type
This benchmark compares performance of Atomic<T> and Synchronized methods. The implementation of the benchmark contains concurrent read/write threads to ensure that lock contention is in place.
Method | Mean | Error | StdDev | Median |
---|---|---|---|---|
Atomic | 352.8 us | 10.00 us | 93.91 us | 341.1 us |
Synchronized | 993.8 us | 11.41 us | 104.88 us | 982.4 us |
SpinLock | 1,539.2 us | 38.05 us | 337.18 us | 1,603.6 us |
Value Delegate
This benchmark compares performance of indirect method call using classic delegates from .NET and value delegates.
Method | Mean | Error | StdDev |
---|---|---|---|
Instance method, regular delegate, has implicit this | 0.9273 ns | 0.0072 ns | 0.0060 ns |
Instance method, Value Delegate, has implicit this | 1.8824 ns | 0.0495 ns | 0.0463 ns |
Static method, regular delegate, large size of param type, no implicitly captured object | 14.5560 ns | 0.0440 ns | 0.0367 ns |
Static method, Value Delegate, large size of param type, no implicitly captured object | 15.7549 ns | 0.0731 ns | 0.0684 ns |
Static method, regular delegate, small size of param type, no implicitly captured object | 23.2037 ns | 0.3844 ns | 0.3408 ns |
Static method, Value Delegate, small size of param type, no implicitly captured object | 21.8213 ns | 0.1073 ns | 0.0896 ns |
Large size of param type means that the type of the parameter is larger than 64 bit.
Interpretation of benchmark results:
- Proxy mode of Value Delegate adds a small overhead in comparison with regular delegate
- If the type of the parameter is less than or equal to the size of CPU register then Value Delegate offers the best performance
- If the type of the parameter is greater than the size of CPU register then Value Delegate is slower than regular delegate
File-buffering Writer
This benchmark compares performance of FileBufferingWriteStream from ASP.NET Core and FileBufferingWriter from .NEXT library.
Both classes switching from in-memory buffer to file-based buffer during benchmark execution. Note that benchmark result highly depends on disk I/O performance. The following results were obtained using NVMe SSD.
Method | Mean | Error | StdDev |
---|---|---|---|
FileBufferingWriter in synchronous mode |
1.001 ms | 0.0111 ms | 0.0104 ms |
FileBufferingWriteStream in synchronous mode |
26.690 ms | 1.4974 ms | 4.4151 ms |
FileBufferingWriter in asynchronous mode |
8.947 ms | 0.2014 ms | 0.5412 ms |
FileBufferingWriteStream in asynchronous mode |
19.300 ms | 1.2528 ms | 3.6546 ms |
FileBufferingWriter
is a winner in synchronous scenario because it has native support for synchronous mode in contrast to FileBufferingWriteStream
.