Skip to content

Commit

Permalink
Implemented object pools und thread safety settings (axuno#229)
Browse files Browse the repository at this point in the history
  • Loading branch information
axunonb authored Dec 3, 2021
1 parent 62c0e7e commit 8df6c0d
Show file tree
Hide file tree
Showing 111 changed files with 4,225 additions and 740 deletions.
108 changes: 90 additions & 18 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,50 @@
Latest Changes
====

What's new in v3.0.0-alpha.4
What's new in v3.0.0-alpha.5
====

Changes to release v2.7.x

### 1. Significant boost in performance
After implementing a zero allocation `ValueStringBuilder` based on [ZString](https://github.com/Cysharp/ZString) with [#193](https://github.com/axuno/SmartFormat/pull/193) and [#228](https://github.com/axuno/SmartFormat/pull/228):
a) After implementing a **zero allocation `ValueStringBuilder`** based on [ZString](https://github.com/Cysharp/ZString) with [#193](https://github.com/axuno/SmartFormat/pull/193) and [#228](https://github.com/axuno/SmartFormat/pull/228):
* Parsing is 10% faster with 50-80% less GC and memory allocation
* Formatting is up to 40% faster with 50% less GC and memory allocation
* Since [#228](https://github.com/axuno/SmartFormat/pull/228) there no more `Cysharp.Text` classes used in the `SmartFormat` namespace
* Created `ZStringBuilder` as a wrapper around `Utf16ValueStringBuilder`.
* Replaced occurrences of `Utf16ValueStringBuilder` with `ZStringBuilder`.
b) After implementing **Object Pools** ([#229](https://github.com/axuno/SmartFormat/pull/229)) for all classes which are frequently instantiated, GC and memory allocation again went down significantly. See the test results below.

See also: <a href="#ThreadSafety">thread safety</a> and <a href="#ObjectPooling">object pooling</a>

More optimizations:
**More optimizations:**

a) `ReflectionSource`
c) `ReflectionSource`

* Added a type cache which increases speed by factor 4. Thanks to [@karljj1](https://github.com/karljj1). ([#155](https://github.com/axuno/SmartFormat/pull/155)).
* Type caching can be disabled ([#217](https://github.com/axuno/SmartFormat/pull/217))
* Dictionary for type cache changed to `ConcurrentDictionary` ([#217](https://github.com/axuno/SmartFormat/pull/217))
* `TypeCache` is accessible from a derived class ([#217](https://github.com/axuno/SmartFormat/pull/217))

b) `DictionarySource`
* Depending on `SmartSettings.IsThreadSafe` the type cache is `ConcurrentDictionary` or `Dictionary`.

d) `StringSource`

The `StringSource` takes over a part of the functionality, which has been implemented in `ReflectionSource` in v2. Compared to reflection **with** caching, speed is 20% better at 25% less memory allocation.

e) `DictionarySource`

* Speed increased by 10% with less GC pressure ([#189](https://github.com/axuno/SmartFormat/pull/189))
Speed increased by 10% with less GC pressure ([#189](https://github.com/axuno/SmartFormat/pull/189))

#### Performance Test Results

The test setup for `ObjectPoolPerformanceTests` is included in the repo. It's obvious, that test results depend a lot on the input format string and the type of data arguments. Still, the results give a good impression of the improvements in `v3.0` compared to `v2.7`.

Results under NetStandard2.1:
```
| Method | N | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|--------------- |------ |---------:|--------:|--------:|-----------:|----------:|------:|----------:|
SmartFormat v2.7.1
| Format | 10000 | 223.9 ms | 1.48 ms | 1.38 ms | 21333.3333 | - | - | 172 MB |
SmartFormat v3.0-alpha.5
| SingleThread | 10000 | 108.2 ms | 0.52 ms | 0.49 ms | 3200.0000 | - | - | 26 MB |
| ThreadSafe | 10000 | 128.0 ms | 1.29 ms | 1.21 ms | 6000.0000 | - | - | 48 MB |
| PoolingDisabled| 10000 | 157.3 ms | 1.74 ms | 1.63 ms | 11000.0000 | 5500.0000 | - | 88 MB |
```
Note: `PoolingDisabled` is just for showing the advantage of object pooling, which was added in `v3.0-alpha.5`.

### 2. Exact control of whitespace text output
This was an issue in v2 and was going back to combining `string.Format` compatibility with *Smart.Format* features. This is resolved by setting the desired mode with `SmartSettings.StringFormatCompatibility` (defaults to `false`). ([#172](https://github.com/axuno/SmartFormat/pull/172))
Expand Down Expand Up @@ -66,7 +87,7 @@ var result = Smart.Format("Email {0:ismatch("^\(\(\\w+\([-+.]\\w+\)*@\\w+\([-.]\
```Csharp
var temperatures = new[] {-20, -10, -15};
// parse once
var parsedFormat = new Parser().ParseFormat("Temperature is {Temp}°.");
using var parsedFormat = new Parser().ParseFormat("Temperature is {Temp}°.");
// one SmartFormatter instance
var formatter = Smart.CreateDefaultSmartFormat();
foreach (var current in temperatures)
Expand All @@ -91,7 +112,7 @@ In v2, Alignment of output values was limited to the `DefaultFormatter`. It's ab
* Modified `ListFormatter` so that items can be aligned (but the spacers stay untouched).

### 8. Added `StringSource` as another `ISource` ([#178](https://github.com/axuno/SmartFormat/pull/178), [#216](https://github.com/axuno/SmartFormat/pull/216))
The `StringSource` takes over functionality, which have been implemented in `ReflectionSource` in v2. Compared to reflection caching, speed is 20% better at 25% less memory allocation.
The `StringSource` takes over a part of the functionality, which has been implemented in `ReflectionSource` in v2. Compared to reflection **with** caching, speed is 20% better at 25% less memory allocation.

`StringSource` brings the following built-in methods (as selector names):
* Length
Expand Down Expand Up @@ -140,7 +161,7 @@ Smart.Format("{TheValue:isnull:The value is null|The value is {}}", new {TheValu
// Result: "The value is 1234"
```

### 11. Added `LocalizationFormatter` ([#176](https://github.com/axuno/SmartFormat/pull/207)
### 11. Added `LocalizationFormatter` ([#176](https://github.com/axuno/SmartFormat/pull/207))

#### Features
* Added `LocalizationFormatter` to localize literals and placeholders
Expand Down Expand Up @@ -273,6 +294,57 @@ SmartFormat is not a fully-fledged HTML parser. If this is required, use [AngleS
Smart.Format("{0:time(fr):hours minutes}", timeSpan);
// result: "25 heures 1 minute"
```
<a id="ThreadSafety"></a>
### 20. Thread Safety ([#229](https://github.com/axuno/SmartFormat/pull/229))
SmartFormat makes heavy use of caching and object pooling for expensive operations, which both require `static` containers.

a) Instantiating `SmartFormatter`s from different threads:

`SmartSettings.IsThreadSafeMode=true` **must** be set, so that thread safe containers are used. This brings an inherent performance penalty.

**Note:** The simplified `Smart.Format(...)` API overloads use a static `SmartFormatter` instance which is **not** thread safe. Call `Smart.CreateDefaultSmartFormat()` to create a default `Formatter`.

a) Instantiating `SmartFormatter`s from a single thread:

`SmartSettings.IsThreadSafeMode=false` **should** be set for avoiding the multithreading overhead and thus for best performance.

The simplified `Smart.Format(...)` API overloads are allowed here.

<a id="ObjectPooling"></a>
### 21. How to benefit from object pooling ([#229](https://github.com/axuno/SmartFormat/pull/229))
In order to return "smart" objects back to the object pool, its important to use one of the following patterns.

Examples:

**a) Single thread context** (no need to care about object pooling)
```CSharp
var resultString = Smart.Format("format string", args);
```

**b) Recommended: Auto-dispose `Format`** (e.g.: caching, multi treading context)
```CSharp
var smart = Smart.CreateDefaultSmartFormat();
// Note "using" for auto-disposing the parsedFormat
using var parsedFormat = new Parser().ParseFormat("format string", args);
var resultString = smart.Format(parsedFormat);
```

**c) Call `Format.Dispose()`** (e.g.: caching, multi treading context)
```CSharp
var smart = Smart.CreateDefaultSmartFormat();
var parsedFormat = new Parser().ParseFormat("format string", args);
var resultString = smart.Format(parsedFormat);
// Don't use (or reference) "parsedFormat" after disposing
parsedFormat.Dispose();
```

### 22. Miscellaneous
* Since [#228](https://github.com/axuno/SmartFormat/pull/228) there are no more `Cysharp.Text` classes used in the `SmartFormat` namespace
* Created class `ZStringBuilder` as a wrapper around `Utf16ValueStringBuilder`.
* Replaced occurrences of `Utf16ValueStringBuilder` with `ZStringBuilder`.


v2.7.1
===
Expand Down Expand Up @@ -314,9 +386,9 @@ Supported frameworks now are:
v2.5.1.0
===
* Added ```System.Text.Json.JsonElement``` to the JsonSource extension. ```Newtonsoft.Json``` is still included.
* Added a demo version as a netcoreapp3.1 WindowsDesktop App
* Added a demo version as a net5.0 WindowsDesktop App
* Supported framworks now are:
* .Net Framework 4.6.2, 4.7.2 and 4.8 (```System.Text.Json``` is not supported for .Net Framework 4.5.x and thus had to be dropped)
* .Net Framework 4.6.1, 4.7.2 and 4.8 (```System.Text.Json``` is not supported for .Net Framework 4.5.x and thus had to be dropped)
* .Net Standard 2.0 and 2.1
* Updated the [Wiki](https://github.com/axuno/SmartFormat/wiki)

Expand Down
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,18 @@

[![Paypal-Donations](https://img.shields.io/badge/Donate-PayPal-important.svg?style=flat-square)](https://www.paypal.com/donate?hosted_button_id=KSC3LRAR26AHN)

**SmartFormat** is a **string composition** library written in C# which is basically compatible with string.Format. More than that **SmartFormat** can format data with named placeholders, lists, pluralization and other smart extensions.
**SmartFormat** is a **string composition** library written in C# which can be a drop-in replacement for `string.Format`. More than that **Smart.Format** can format data with named placeholders, lists, localization, pluralization and other smart extensions.

* High performance with low memory footprint
* Exact control of whitespace text output
* `string.Format` compatibility mode and `Smart.Format` enhanced mode
* Minimal, intuitive syntax
* Many built-in extensions, custom extensions are easy to integrate

### Supported Frameworks
* .Net Framework 4.6.1 and later
* .Net Standard 2.0 and later (including .Net 5.0)
* .Net Standard 2.0
* .Net Standard 2.1 and later for best optimizations

### Get started
[![NuGet](https://img.shields.io/nuget/v/SmartFormat.Net.svg)](https://www.nuget.org/packages/SmartFormat.Net/)
Expand All @@ -29,6 +36,3 @@ See [changelog](CHANGES.md) for changes.

**See the [list of changes](https://github.com/axuno/SmartFormat/blob/version/v3.0/CHANGES.md) already merged into branch `version/v3`**

<hr>

We have started to work on a new version of ```SmartFormat.Net``` and **would like to collect your input using [GitHub Discussions](https://github.com/axuno/SmartFormat/discussions/139)**.
1 change: 1 addition & 0 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ for:
- git config --global core.autocrlf true
build_script:
- ps: cd $env:APPVEYOR_BUILD_FOLDER\src
- ps: dotnet --version
- ps: dotnet restore --verbosity quiet
- ps: dotnet add .\SmartFormat.Tests\SmartFormat.Tests.csproj package AltCover
- ps: dotnet build SmartFormat.sln /verbosity:minimal /t:rebuild /p:configuration=release /nowarn:CS1591,CS0618
Expand Down
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<Copyright>Copyright 2011-2021 axuno gGmbH, Scott Rippey, Bernhard Millauer and other contributors.</Copyright>
<RepositoryUrl>https://github.com/axuno/SmartFormat.git</RepositoryUrl>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<Version>3.0.0-alpha.4</Version>
<Version>3.0.0-alpha.5</Version>
<FileVersion>3.0.0</FileVersion>
<AssemblyVersion>3.0.0</AssemblyVersion> <!--only update AssemblyVersion with major releases -->
<LangVersion>latest</LangVersion>
Expand Down
12 changes: 6 additions & 6 deletions src/Performance/FormatTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ public void Placeholder()
for (var i = 0; i < N; i++)
{
using var output = GetOutput(_placeholderFormat);
_literalFormatter.FormatInto(output, _placeholderFormat, LoremIpsum);
_literalFormatter.FormatInto(output, null, _placeholderFormat, LoremIpsum);
_ = output.ToString();
}
}
Expand All @@ -138,7 +138,7 @@ public void Placeholder0005()
for (var i = 0; i < N; i++)
{
using var output = GetOutput(_placeholder0005Format);
_literalFormatter.FormatInto(output, _placeholder0005Format, LoremIpsum, LoremIpsum, LoremIpsum, LoremIpsum, LoremIpsum);
_literalFormatter.FormatInto(output, null, _placeholder0005Format, LoremIpsum, LoremIpsum, LoremIpsum, LoremIpsum, LoremIpsum);
_ = output.ToString();
}
}
Expand All @@ -149,7 +149,7 @@ public void Literal0010Char()
for (var i = 0; i < N; i++)
{
using var output = GetOutput(_literal0010CharFormat);
_literalFormatter.FormatInto(output, _literal0010CharFormat);
_literalFormatter.FormatInto(output, null, _literal0010CharFormat);
_ = output.ToString();
}
}
Expand All @@ -160,7 +160,7 @@ public void Literal3000Char()
for (var i = 0; i < N; i++)
{
using var output = GetOutput(_literal3000CharFormat);
_literalFormatter.FormatInto(output, _literal3000CharFormat);
_literalFormatter.FormatInto(output, null, _literal3000CharFormat);
_ = output.ToString();
}
}
Expand All @@ -171,7 +171,7 @@ public void Literal6000Placeholder()
for (var i = 0; i < N; i++)
{
using var output = GetOutput(_literal6000PlaceholderFormat);
_literalFormatter.FormatInto(output, _literal6000PlaceholderFormat, LoremIpsum);
_literalFormatter.FormatInto(output, null, _literal6000PlaceholderFormat, LoremIpsum);
_ = output.ToString();
}
}
Expand All @@ -182,7 +182,7 @@ public void Literal18kUniPlaceholder()
for (var i = 0; i < N; i++)
{
using var output = GetOutput(_literal18kEscPlaceholderFormat);
_literalFormatter.FormatInto(output, _literal18kEscPlaceholderFormat, LoremIpsum);
_literalFormatter.FormatInto(output, null, _literal18kEscPlaceholderFormat, LoremIpsum);
_ = output.ToString();
}
}
Expand Down
90 changes: 90 additions & 0 deletions src/Performance/ObjectPoolPerformanceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using SmartFormat.Core.Settings;
using SmartFormat.Extensions;

namespace SmartFormat.Performance
{
/*
// * Summary *
BenchmarkDotNet=v0.13.0, OS=Windows 10.0.19043.1348 (21H1/May2021Update)
AMD Ryzen 9 3900X, 1 CPU, 24 logical and 12 physical cores
.NET SDK=5.0.403
[Host] : .NET 5.0.12 (5.0.1221.52207), X64 RyuJIT
.NET 5.0 : .NET 5.0.12 (5.0.1221.52207), X64 RyuJIT
Job=.NET 5.0 Runtime=.NET 5.0
| Method | N | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated |
|--------------- |------ |---------:|--------:|--------:|-----------:|----------:|------:|----------:|
SmartFormat v2.7.1
| Format | 10000 | 223.9 ms | 1.48 ms | 1.38 ms | 21333.3333 | - | - | 172 MB |
SmartFormat v3.0-alpha.5 (first version with ObjectPool)
| SingleThread | 10000 | 108.2 ms | 0.52 ms | 0.49 ms | 3200.0000 | - | - | 26 MB |
| ThreadSafe | 10000 | 128.0 ms | 1.29 ms | 1.21 ms | 6000.0000 | - | - | 48 MB |
| PoolingDisabled| 10000 | 157.3 ms | 1.74 ms | 1.63 ms | 11000.0000 | 5500.0000 | - | 88 MB |
// * Hints *
Outliers
ObjectPoolPerformanceTests.ObjectPoolTest: .NET 5.0 -> 1 outlier was removed (1.10 ms)
// * Legends *
N : Value of the 'N' parameter
Mean : Arithmetic mean of all measurements
Error : Half of 99.9% confidence interval
StdDev : Standard deviation of all measurements
Ratio : Mean of the ratio distribution ([Current]/[Baseline])
Gen 0 : GC Generation 0 collects per 1000 operations
Gen 1 : GC Generation 1 collects per 1000 operations
Gen 2 : GC Generation 2 collects per 1000 operations
Allocated : Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)
1 ms : 1 Millisecond (0.001 sec)
*/


[SimpleJob(RuntimeMoniker.Net50)]
[MemoryDiagnoser]
// [RPlotExporter]
public class ObjectPoolPerformanceTests
{
private readonly SmartFormatter _formatter;
private readonly List<int> _list = new() { 1, 2, 3 };

public ObjectPoolPerformanceTests()
{
_formatter = new SmartFormatter();
var listSourceAndFormat = new ListFormatter();
_formatter.AddExtensions(listSourceAndFormat, new StringSource(), new ReflectionSource(), new DefaultSource());
_formatter.AddExtensions(listSourceAndFormat, new DefaultFormatter());
}

[Params(10000)]
public int N;

[GlobalSetup]
public void Setup()
{
SmartSettings.IsThreadSafeMode = false;
PoolSettings.CheckReturnedObjectsExistInPool = false;
PoolSettings.IsPoolingEnabled = false;
}

[Benchmark(Baseline = false)]
public void ObjectPoolTest()
{
const string indexPlaceholders = "All items: {0[0]}, {0[1]}, and {0[2]}";
const string listPlaceholders = "Total items: {0.Count}. All items: {0:list:{}|, |, and }";

for (var i = 0; i < N; i++)
{
_ = _formatter.Format(indexPlaceholders, _list);
_ = _formatter.Format(listPlaceholders, _list);
}
}
}
}
4 changes: 3 additions & 1 deletion src/Performance/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ public static void Main()
{
//BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, new DebugInProcessConfig());
//BenchmarkRunner.Run<SourcePerformanceTests>();
BenchmarkRunner.Run<FormatTests>();
BenchmarkRunner.Run<ObjectPoolPerformanceTests>();
//BenchmarkRunner.Run<FormatTests>();
//BenchmarkRunner.Run<ParserTests>();
//BenchmarkRunner.Run(StackPerformanceTests)

//BenchmarkRunner.Run<SimpleSpanParserTests>();
//BenchmarkRunner.Run<NullFormatterChooseFormatterTests>();
Expand Down
Loading

0 comments on commit 8df6c0d

Please sign in to comment.