Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented object pools und thread safety settings #229

Merged
merged 10 commits into from
Dec 3, 2021
Prev Previous commit
Next Next commit
Final adjustments
* Performance tests
* Documentation in CHANGES.md
* Tests for PoolSettings.IsPoolingEnabled = false
* Include netstandard2.0 in unit tests
  • Loading branch information
axunonb committed Dec 3, 2021
commit 79395f4a56b03d546d4f26e1498cf93bae2a9734
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** 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

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

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)**.
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
90 changes: 19 additions & 71 deletions src/Performance/ObjectPoolPerformanceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ namespace SmartFormat.Performance

Job=.NET 5.0 Runtime=.NET 5.0

| Method | N | Mean | Error | StdDev | Ratio | Gen 0 | Gen 1 | Gen 2 | Allocated |
|----------------------- |------ |---------:|---------:|---------:|------:|----------:|--------:|------:|----------:|
Before including object pool - v3.0-alpha.4
| No-Object-Pool | 10000 | 15.13 ms | 0.082 ms | 0.073 ms | 1.00 | 2687.5000 | 15.6250 | - | 22 MB |
After including object pool into v3.0-alpha.4
| ObjectPoolTestDisabled | 10000 | 18.15 ms | 0.088 ms | 0.078 ms | | 3000.0000 | - | - | 24 MB |
| ObjectPoolSingleThread | 10000 | 21.60 ms | 0.087 ms | 0.081 ms | | 656.2500 | - | - | 5 MB |
| ObjectPoolThreadSafe | 10000 | 23.50 ms | 0.042 ms | 0.035 ms | | 1250.0000 | - | - | 10 MB |
| 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
Expand All @@ -52,24 +52,15 @@ After including object pool into v3.0-alpha.4
// [RPlotExporter]
public class ObjectPoolPerformanceTests
{
private const string LoremIpsum = "Us creeping good grass multiply seas under hath sixth fowl heaven days. Third. Deep abundantly all after also meat day. Likeness. Lesser saying meat sea in over likeness land. Meat own, made given stars. Form the. And his. So gathered fish god firmament, great seasons. Give sixth doesn't beast our fourth creature years isn't you you years moving, earth you every a male night replenish fruit. Set. Deep so, let void midst won't first. Second very after all god from night itself shall air had gathered firmament was cattle itself every great first. Let dry it unto. Creepeth don't rule fruit there creature second their whose seas without every man it darkness replenish made gathered you saying over set created. Midst meat light without bearing. Our him given his thing fowl blessed rule that evening let man beginning light forth tree she'd won't light. Moving evening shall have may beginning kind appear, also the kind living whose female hath void fifth saw isn't. Green you'll from. Grass fowl saying yielding heaven. I. Above which. Isn't i. They're moving. Can't cattle i. Gathering shall set darkness multiply second whales meat she'd form, multiply be meat deep bring forth land can't own she'd upon hath appear years let above, for days divided greater first was.\r\n\r\nPlace living all it Air you evening us don't fourth second them own which fish made. Subdue don't you'll the, the bearing said dominion in man have deep abundantly night she'd and place sixth the gathered lesser creeping subdue second fish multiply was created. Cattle wherein meat female fruitful set, earth them subdue seasons second, man forth over, be greater grass. Light unto, over bearing hath thing yielding be, spirit you'll given was set had let their abundantly you're beginning beginning divided replenish moved. Evening own heaven waters, their it of them cattle fruitful is, light after don't air fish multiply which moveth face the dominion fifth open, hath i evening from. Give from every waters two. That forth, bearing dry fly in may fish. Multiply Tree cattle.\r\n\r\nThing. Great saying good face gathered Forth over fowl moved Fourth upon form seasons over lights greater saw can't over saying beginning. Can't in moveth fly created subdue fourth. Them creature one moving living living thing Itself one after one darkness forth divided thing gathered earth there days seas fourth, stars herb. All from third dry have forth. Our third sea all. Male years you. Over fruitful they're. Have she'd their our image dry sixth void meat subdue face moved. Herb moved multiply tree, there likeness first won't there one dry it hath kind won't you seas of make day moving second thing were. Hath, had winged hath creature second had you. Upon. Appear image great place fourth the in, waters abundantly, deep hath void Him heaven divided heaven greater let so. Open replenish Wherein. Be created. The and was of. Signs cattle midst. Is she'd every saying bring there doesn't and. Rule. Stars green divided upon lesser a.";
private const string _format = "Address: {City.ZipCode} {City.Name}, {City.AreaCode}\n" +
"Name: {Person.FirstName} {Person.LastName}";

private const string _formatForLiteral = "Address: {0} {1}, {2}\nName: {3} {4}";
private Address _someAddress = new Address();
private SmartFormatter _formatter;

private readonly SmartFormatter _formatter;
private readonly List<int> _list = new() { 1, 2, 3 };

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

[Params(10000)]
Expand All @@ -80,62 +71,19 @@ public void Setup()
{
SmartSettings.IsThreadSafeMode = false;
PoolSettings.CheckReturnedObjectsExistInPool = false;
PoolSettings.IsPoolingEnabled = true;
PoolSettings.IsPoolingEnabled = false;
}

[Benchmark(Baseline = false)]
public void ObjectPoolTest()
{
for (var i = 0; i < N; i++)
{
_ = _formatter.Format("First {0}, Second {1}, Third {2}", i, i + 1, i + 2);
}
}

public class Address
{
public CityDetails City { get; set; } = new CityDetails();
public PersonDetails Person { get; set; } = new PersonDetails();
const string indexPlaceholders = "All items: {0[0]}, {0[1]}, and {0[2]}";
const string listPlaceholders = "Total items: {0.Count}. All items: {0:list:{}|, |, and }";

public Dictionary<string, object> ToDictionary()
{
var d = new Dictionary<string, object>
{
{ nameof(City), City.ToDictionary() },
{ nameof(Person), Person.ToDictionary() }
};
return d;
}

public class CityDetails
{
public string Name { get; set; } = "New York";
public string ZipCode { get; set; } = "00501";
public string AreaCode { get; set; } = "631";

public Dictionary<string, string> ToDictionary()
{
return new()
{
{nameof(Name), Name},
{nameof(ZipCode), ZipCode},
{nameof(AreaCode), AreaCode}
};
}
}

public class PersonDetails
for (var i = 0; i < N; i++)
{
public string FirstName { get; set; } = "John";
public string LastName { get; set; } = "Doe";
public Dictionary<string, string> ToDictionary()
{
return new()
{
{nameof(FirstName), FirstName},
{nameof(LastName), LastName}
};
}
_ = _formatter.Format(indexPlaceholders, _list);
_ = _formatter.Format(listPlaceholders, _list);
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/Performance/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ public static void Main()
{
//BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, new DebugInProcessConfig());
//BenchmarkRunner.Run<SourcePerformanceTests>();
//BenchmarkRunner.Run<ObjectPoolPerformanceTests>();
BenchmarkRunner.Run<FormatTests>();
BenchmarkRunner.Run<ObjectPoolPerformanceTests>();
//BenchmarkRunner.Run<FormatTests>();
//BenchmarkRunner.Run<ParserTests>();
//BenchmarkRunner.Run(StackPerformanceTests)

Expand Down
Loading