Benchmarks
Avalanche.Localization is initialization heavy as resources are located, read and parsed on first use, albeit lazily. After initialization, texts are cached and are accessible with a dictionary lookup. In this article after-initialization performance is studied.
LocalizedText
ILocalizedText was ran with 0 and 1 parameters, with and without memory pool, and with and without pluralized parameter.
Method | Mean | Error | StdDev | Gen 0 | Allocated |
---|---|---|---|---|---|
LocalizedTextZeroParameters | 114.0 ns | 1.03 ns | 0.96 ns | - | - |
LocalizedTextZeroParametersMemoryPool | 262.0 ns | 1.52 ns | 1.43 ns | - | - |
LocalizedTextOneParameter | 319.5 ns | 2.23 ns | 2.09 ns | 0.0105 | 88 B |
LocalizedTextOneParameterMemoryPool | 458.0 ns | 3.21 ns | 3.00 ns | - | - |
LocalizedTextOnePluralizedParameter | 278.7 ns | 1.91 ns | 1.79 ns | 0.0076 | 64 B |
LocalizedTextOnePluralizedParameterMemoryPool | 454.8 ns | 2.73 ns | 2.56 ns | 0.0076 | 64 B |
Benchmark's localization initialization.
ILocalization localization =
Localization.CreateDefault()
.AddLine("fi", "Namespace.HelloWorld", "Brace", "Hei maailma")
.AddLine("fi", "Namespace.Welcome", "Brace", "Tervetuloa, {User}.")
.AddLine("fi", "Namespace.Apples", "Brace", "Sinulla on ei ole omenoita.", "Unicode.CLDR41", "0:cardinal:zero")
.AddLine("fi", "Namespace.Apples", "Brace", "Sinulla on yksi omena.", "Unicode.CLDR41", "0:cardinal:one")
.AddLine("fi", "Namespace.Apples", "Brace", "Sinulla on {0} omenaa.", "Unicode.CLDR41", "0:cardinal:other");
Zero parameter localization is a simple dictionary lookup and is zero heap operation.
public void LocalizedTextZeroParameters()
{
// Localize text from cache
ILocalizedText text = localization.LocalizedTextCached[("fi", "Namespace.HelloWorld")];
// Print without arguments
string print = text.Print(null, null);
}
One parameter localization allocates a string for result, and the caller allocates an object[] for input.
public void LocalizedTextOneParameter()
{
// Localize text from cache
ILocalizedText text = localization.LocalizedTextCached[("fi", "Namespace.Welcome")];
// Print with argument
string print = text.Print(null, new object?[] { "User" });
}
One parameter localization can be processed zero heap, if the caller allocates arguments and result characters from memory pool.
public void LocalizedTextOneParameterMemoryPool()
{
// Localize text from cache
ILocalizedText text = localization.LocalizedTextCached[("fi", "Namespace.Welcome")];
// Rent arguments
object[] arguments = ArrayPool<object>.Shared.Rent(1);
// Assign argument
arguments[0] = "User";
// Estimate length
int length = text.EstimatePrintLength(arguments);
// Rent chars
char[] buffer = ArrayPool<char>.Shared.Rent(length);
// Get span
Span<char> bufferSpan = buffer.AsSpan();
// Print to buffer
length = text.PrintTo(bufferSpan, arguments);
// Slice print
ReadOnlySpan<char> print = bufferSpan[..length];
// Return rentals
ArrayPool<object>.Shared.Return(arguments);
ArrayPool<char>.Shared.Return(buffer);
}
If a parameter is pluralized, the evaluation is about 3x-4x heavier. Features are extracted from the argument and features are evaluated against pluralization rules.
public void LocalizedTextOnePluralizedParameter()
{
// Localize text from cache
ILocalizedText text = localization.LocalizedTextCached[("fi", "Namespace.Apples")];
// Create arguments
object?[] arguments = new object?[] { "2" };
// Print with arguments
string print = text.Print(null, arguments);
}
Pluralization heap footprint is alleviated slightly (-64 bytes, -11%) if caller uses memory pool.
public void LocalizedTextOnePluralizedParameterMemoryPool()
{
// Localize text from cache
ILocalizedText text = localization.LocalizedTextCached[("fi", "Namespace.Apples")];
// Rent arguments
object[] arguments = ArrayPool<object>.Shared.Rent(1);
// Assign argument
arguments[0] = "2";
// Localize & Pluralize to culture & arguments
ITemplatePrintable printable = text.Pluralize(arguments);
// Estimate length
int length = printable.EstimatePrintLength(arguments);
// Rent chars
char[] buffer = ArrayPool<char>.Shared.Rent(length);
// Get span
Span<char> bufferSpan = buffer.AsSpan();
// Print to buffer
length = printable.PrintTo(bufferSpan, arguments);
// Slice print
ReadOnlySpan<char> print = bufferSpan[..length];
// Return rentals
ArrayPool<object>.Shared.Return(arguments);
ArrayPool<char>.Shared.Return(buffer);
}
LocalizedTextBenchmark.cs
using System.Buffers;
using System.Globalization;
using Avalanche.Localization;
using Avalanche.Template;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
[SimpleJob(RuntimeMoniker.Net70), MemoryDiagnoser]
public class LocalizedTextBenchmark
{
// <init>
ILocalization localization =
Localization.CreateDefault()
.AddLine("fi", "Namespace.HelloWorld", "Brace", "Hei maailma")
.AddLine("fi", "Namespace.Welcome", "Brace", "Tervetuloa, {User}.")
.AddLine("fi", "Namespace.Apples", "Brace", "Sinulla on ei ole omenoita.", "Unicode.CLDR41", "0:cardinal:zero")
.AddLine("fi", "Namespace.Apples", "Brace", "Sinulla on yksi omena.", "Unicode.CLDR41", "0:cardinal:one")
.AddLine("fi", "Namespace.Apples", "Brace", "Sinulla on {0} omenaa.", "Unicode.CLDR41", "0:cardinal:other");
// </init>
[Benchmark]
// <LocalizedTextZeroParameters>
public void LocalizedTextZeroParameters()
{
// Localize text from cache
ILocalizedText text = localization.LocalizedTextCached[("fi", "Namespace.HelloWorld")];
// Print without arguments
string print = text.Print(null, null);
}
// </LocalizedTextZeroParameters>
[Benchmark]
// <LocalizedTextZeroParametersMemoryPool>
public void LocalizedTextZeroParametersMemoryPool()
{
// Localize text from cache
ILocalizedText text = localization.LocalizedTextCached[("fi", "Namespace.HelloWorld")];
// Assign arguments
object?[]? arguments = null;
// Estimate length
int length = text.EstimatePrintLength(arguments);
// Rent chars
char[] buffer = ArrayPool<char>.Shared.Rent(length);
// Get span
Span<char> bufferSpan = buffer.AsSpan();
// Print to buffer
length = text.PrintTo(bufferSpan, arguments);
// Slice print
ReadOnlySpan<char> print = bufferSpan[..length];
// Return rentals
ArrayPool<char>.Shared.Return(buffer);
}
// </LocalizedTextZeroParametersMemoryPool>
[Benchmark]
// <LocalizedTextOneParameter>
public void LocalizedTextOneParameter()
{
// Localize text from cache
ILocalizedText text = localization.LocalizedTextCached[("fi", "Namespace.Welcome")];
// Print with argument
string print = text.Print(null, new object?[] { "User" });
}
// </LocalizedTextOneParameter>
[Benchmark]
// <LocalizedTextOneParameterMemoryPool>
public void LocalizedTextOneParameterMemoryPool()
{
// Localize text from cache
ILocalizedText text = localization.LocalizedTextCached[("fi", "Namespace.Welcome")];
// Rent arguments
object[] arguments = ArrayPool<object>.Shared.Rent(1);
// Assign argument
arguments[0] = "User";
// Estimate length
int length = text.EstimatePrintLength(arguments);
// Rent chars
char[] buffer = ArrayPool<char>.Shared.Rent(length);
// Get span
Span<char> bufferSpan = buffer.AsSpan();
// Print to buffer
length = text.PrintTo(bufferSpan, arguments);
// Slice print
ReadOnlySpan<char> print = bufferSpan[..length];
// Return rentals
ArrayPool<object>.Shared.Return(arguments);
ArrayPool<char>.Shared.Return(buffer);
}
// </LocalizedTextOneParameterMemoryPool>
[Benchmark]
// <LocalizedTextOnePluralizedParameter>
public void LocalizedTextOnePluralizedParameter()
{
// Localize text from cache
ILocalizedText text = localization.LocalizedTextCached[("fi", "Namespace.Apples")];
// Create arguments
object?[] arguments = new object?[] { "2" };
// Print with arguments
string print = text.Print(null, arguments);
}
// </LocalizedTextOnePluralizedParameter>
[Benchmark]
// <LocalizedTextOnePluralizedParameterMemoryPool>
public void LocalizedTextOnePluralizedParameterMemoryPool()
{
// Localize text from cache
ILocalizedText text = localization.LocalizedTextCached[("fi", "Namespace.Apples")];
// Rent arguments
object[] arguments = ArrayPool<object>.Shared.Rent(1);
// Assign argument
arguments[0] = "2";
// Localize & Pluralize to culture & arguments
ITemplatePrintable printable = text.Pluralize(arguments);
// Estimate length
int length = printable.EstimatePrintLength(arguments);
// Rent chars
char[] buffer = ArrayPool<char>.Shared.Rent(length);
// Get span
Span<char> bufferSpan = buffer.AsSpan();
// Print to buffer
length = printable.PrintTo(bufferSpan, arguments);
// Slice print
ReadOnlySpan<char> print = bufferSpan[..length];
// Return rentals
ArrayPool<object>.Shared.Return(arguments);
ArrayPool<char>.Shared.Return(buffer);
}
// </LocalizedTextOnePluralizedParameterMemoryPool>
}
LocalizableText
ILocalizableText performance is similiar to the performance of ILocalizedText.
Method | Mean | Error | StdDev | Gen 0 | Allocated |
---|---|---|---|---|---|
LocalizableTextZeroParameters | 220.5 ns | 1.62 ns | 1.51 ns | - | - |
LocalizableTextZeroParametersMemoryPool | 430.1 ns | 4.18 ns | 3.91 ns | - | - |
LocalizableTextOneParameter | 436.4 ns | 1.12 ns | 0.88 ns | 0.0105 | 88 B |
LocalizableTextOneParameterMemoryPool | 654.6 ns | 8.41 ns | 7.86 ns | - | - |
LocalizableTextOnePluralizedParameter | 418.4 ns | 3.01 ns | 2.82 ns | 0.0076 | 64 B |
LocalizableTextOnePluralizedParameterMemoryPool | 666.0 ns | 4.14 ns | 3.88 ns | 0.0076 | 64 B |
LocalizableTextBenchmark.cs
using System;
using System.Buffers;
using System.Globalization;
using Avalanche.Localization;
using Avalanche.Template;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
[SimpleJob(RuntimeMoniker.Net70), MemoryDiagnoser]
public class LocalizableTextBenchmark
{
ILocalization localization =
Localization.CreateDefault()
.AddLine("fi", "Namespace.HelloWorld", "Brace", "Hei maailma")
.AddLine("fi", "Namespace.Welcome", "Brace", "Tervetuloa, {User}.")
.AddLine("fi", "Namespace.Apples", "Brace", "Sinulla on ei ole omenoita.", "Unicode.CLDR41", "0:cardinal:zero")
.AddLine("fi", "Namespace.Apples", "Brace", "Sinulla on yksi omena.", "Unicode.CLDR41", "0:cardinal:one")
.AddLine("fi", "Namespace.Apples", "Brace", "Sinulla on {0} omenaa.", "Unicode.CLDR41", "0:cardinal:other");
CultureInfo _culture = CultureInfo.GetCultureInfo("fi");
[Benchmark]
// <LocalizableTextZeroParameters>
public void LocalizableTextZeroParameters()
{
// Localizable text from cache
ILocalizableText text = localization.LocalizableTextCached["Namespace.HelloWorld"];
// Print without arguments
string print = text.Print(_culture, null);
}
// </LocalizableTextZeroParameters>
[Benchmark]
// <LocalizableTextZeroParametersMemoryPool>
public void LocalizableTextZeroParametersMemoryPool()
{
// Localizable text from cache
ILocalizableText localizable = localization.LocalizableTextCached["Namespace.HelloWorld"];
// Localize text from cache
ILocalizedText text = localizable.Localize("fi")!.Value;
// Assign arguments
object?[]? arguments = null;
// Estimate length
int length = text.EstimatePrintLength(arguments);
// Rent chars
char[] buffer = ArrayPool<char>.Shared.Rent(length);
// Get span
Span<char> bufferSpan = buffer.AsSpan();
// Print to buffer
length = text.PrintTo(bufferSpan, arguments);
// Slice print
ReadOnlySpan<char> print = bufferSpan[..length];
// Return rentals
ArrayPool<char>.Shared.Return(buffer);
}
// </LocalizableTextZeroParametersMemoryPool>
[Benchmark]
// <LocalizableTextOneParameter>
public void LocalizableTextOneParameter()
{
// Localizable text from cache
ILocalizableText text = localization.LocalizableTextCached["Namespace.Welcome"];
// Print with argument
string print = text.Print(_culture, new object?[] { "User" });
}
// </LocalizableTextOneParameter>
[Benchmark]
// <LocalizableTextOneParameterMemoryPool>
public void LocalizableTextOneParameterMemoryPool()
{
// Localizable text from cache
ILocalizableText localizable = localization.LocalizableTextCached["Namespace.Welcome"];
// Localize text from cache
ILocalizedText text = localizable.Localize("fi")!.Value; // Rent arguments
object[] arguments = ArrayPool<object>.Shared.Rent(1);
// Assign argument
arguments[0] = "User";
// Estimate length
int length = text.EstimatePrintLength(arguments);
// Rent chars
char[] buffer = ArrayPool<char>.Shared.Rent(length);
// Get span
Span<char> bufferSpan = buffer.AsSpan();
// Print to buffer
length = text.PrintTo(bufferSpan, arguments);
// Slice print
ReadOnlySpan<char> print = bufferSpan[..length];
// Return rentals
ArrayPool<object>.Shared.Return(arguments);
ArrayPool<char>.Shared.Return(buffer);
}
// </LocalizableTextOneParameterMemoryPool>
[Benchmark]
// <LocalizableTextOnePluralizedParameter>
public void LocalizableTextOnePluralizedParameter()
{
// Localizable text from cache
ILocalizableText text = localization.LocalizableTextCached["Namespace.Apples"];
// Create arguments
object?[] arguments = new object?[] { "2" };
// Print with arguments
string print = text.Print(_culture, arguments);
}
// </LocalizableTextOnePluralizedParameter>
[Benchmark]
// <LocalizableTextOnePluralizedParameterMemoryPool>
public void LocalizableTextOnePluralizedParameterMemoryPool()
{
// Localizable text from cache
ILocalizableText localizable = localization.LocalizableTextCached["Namespace.Apples"];
// Localize text from cache
ILocalizedText text = localizable.Localize("fi")!.Value;
// Rent arguments
object[] arguments = ArrayPool<object>.Shared.Rent(1);
// Assign argument
arguments[0] = "2";
// Localize & Pluralize to culture & arguments
ITemplatePrintable printable = text.Pluralize(arguments);
// Estimate length
int length = printable.EstimatePrintLength(arguments);
// Rent chars
char[] buffer = ArrayPool<char>.Shared.Rent(length);
// Get span
Span<char> bufferSpan = buffer.AsSpan();
// Print to buffer
length = printable.PrintTo(bufferSpan, arguments);
// Slice print
ReadOnlySpan<char> print = bufferSpan[..length];
// Return rentals
ArrayPool<object>.Shared.Return(arguments);
ArrayPool<char>.Shared.Return(buffer);
}
// </LocalizableTextOnePluralizedParameterMemoryPool>
}
LocalizingText
ILocalizingText performance is similiar to the performance of ILocalizedText.
Method | Mean | Error | StdDev | Gen 0 | Allocated |
---|---|---|---|---|---|
LocalizingTextZeroParameters | 247.3 ns | 2.85 ns | 2.67 ns | - | - |
LocalizingTextZeroParametersMemoryPool | 418.7 ns | 3.71 ns | 3.47 ns | - | - |
LocalizingTextOneParameter | 461.3 ns | 4.01 ns | 3.55 ns | 0.0105 | 88 B |
LocalizingTextOneParameterMemoryPool | 603.2 ns | 8.13 ns | 7.60 ns | - | - |
LocalizingTextOnePluralizedParameter | 448.8 ns | 5.18 ns | 4.85 ns | 0.0076 | 64 B |
LocalizingTextOnePluralizedParameterMemoryPool | 650.3 ns | 5.41 ns | 5.06 ns | 0.0076 | 64 B |
LocalizingTextBenchmark.cs
using System.Buffers;
using System.Globalization;
using Avalanche.Localization;
using Avalanche.Template;
using Avalanche.Utilities;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
[SimpleJob(RuntimeMoniker.Net70), MemoryDiagnoser]
public class LocalizingTextBenchmark
{
ILocalization localization =
Localization.CreateDefault()
.AddLine("fi", "Namespace.HelloWorld", "Brace", "Hei maailma")
.AddLine("fi", "Namespace.Welcome", "Brace", "Tervetuloa, {User}.")
.AddLine("fi", "Namespace.Apples", "Brace", "Sinulla on ei ole omenoita.", "Unicode.CLDR41", "0:cardinal:zero")
.AddLine("fi", "Namespace.Apples", "Brace", "Sinulla on yksi omena.", "Unicode.CLDR41", "0:cardinal:one")
.AddLine("fi", "Namespace.Apples", "Brace", "Sinulla on {0} omenaa.", "Unicode.CLDR41", "0:cardinal:other");
CultureInfo _culture = CultureInfo.GetCultureInfo("fi");
ICultureProvider cultureProvider = new CultureProvider("fi").SetReadOnly();
[Benchmark]
// <LocalizingTextZeroParameters>
public void LocalizingTextZeroParameters()
{
// Localizing text from cache
ILocalizingText text = localization.LocalizingTextCached[(cultureProvider, "Namespace.HelloWorld")];
// Print without arguments
string print = text.Print(_culture, null);
}
// </LocalizingTextZeroParameters>
[Benchmark]
// <LocalizingTextZeroParametersMemoryPool>
public void LocalizingTextZeroParametersMemoryPool()
{
// Localizing text from cache
ILocalizingText localizing = localization.LocalizingTextCached[(cultureProvider, "Namespace.HelloWorld")];
// Localize text from cache
ILocalizedText text = localizing.Value!.Value;
// Assign arguments
object?[]? arguments = null;
// Estimate length
int length = text.EstimatePrintLength(arguments);
// Rent chars
char[] buffer = ArrayPool<char>.Shared.Rent(length);
// Get span
Span<char> bufferSpan = buffer.AsSpan();
// Print to buffer
length = text.PrintTo(bufferSpan, arguments);
// Slice print
ReadOnlySpan<char> print = bufferSpan[..length];
// Return rentals
ArrayPool<char>.Shared.Return(buffer);
}
// </LocalizingTextZeroParametersMemoryPool>
[Benchmark]
// <LocalizingTextOneParameter>
public void LocalizingTextOneParameter()
{
// Localizing text from cache
ILocalizingText text = localization.LocalizingTextCached[(cultureProvider, "Namespace.Welcome")];
// Print with argument
string print = text.Print(_culture, new object?[] { "User" });
}
// </LocalizingTextOneParameter>
[Benchmark]
// <LocalizingTextOneParameterMemoryPool>
public void LocalizingTextOneParameterMemoryPool()
{
// Localizing text from cache
ILocalizingText localizing = localization.LocalizingTextCached[(cultureProvider, "Namespace.Welcome")];
// Localize text from cache
ILocalizedText text = localizing.Value!.Value;
// Rent arguments
object[] arguments = ArrayPool<object>.Shared.Rent(1);
// Assign argument
arguments[0] = "User";
// Estimate length
int length = text.EstimatePrintLength(arguments);
// Rent chars
char[] buffer = ArrayPool<char>.Shared.Rent(length);
// Get span
Span<char> bufferSpan = buffer.AsSpan();
// Print to buffer
length = text.PrintTo(bufferSpan, arguments);
// Slice print
ReadOnlySpan<char> print = bufferSpan[..length];
// Return rentals
ArrayPool<object>.Shared.Return(arguments);
ArrayPool<char>.Shared.Return(buffer);
}
// </LocalizingTextOneParameterMemoryPool>
[Benchmark]
// <LocalizingTextOnePluralizedParameter>
public void LocalizingTextOnePluralizedParameter()
{
// Localizing text from cache
ILocalizingText text = localization.LocalizingTextCached[(cultureProvider, "Namespace.Apples")];
// Create arguments
object?[] arguments = new object?[] { "2" };
// Print with arguments
string print = text.Print(_culture, arguments);
}
// </LocalizingTextOnePluralizedParameter>
[Benchmark]
// <LocalizingTextOnePluralizedParameterMemoryPool>
public void LocalizingTextOnePluralizedParameterMemoryPool()
{
// Localizing text from cache
ILocalizingText localizing = localization.LocalizingTextCached[(cultureProvider, "Namespace.Apples")];
// Localize text from cache
ILocalizedText text = localizing.Value!.Value;
// Rent arguments
object[] arguments = ArrayPool<object>.Shared.Rent(1);
// Assign argument
arguments[0] = "2";
// Localize & Pluralize to culture & arguments
ITemplatePrintable printable = text.Pluralize(arguments);
// Estimate length
int length = printable.EstimatePrintLength(arguments);
// Rent chars
char[] buffer = ArrayPool<char>.Shared.Rent(length);
// Get span
Span<char> bufferSpan = buffer.AsSpan();
// Print to buffer
length = printable.PrintTo(bufferSpan, arguments);
// Slice print
ReadOnlySpan<char> print = bufferSpan[..length];
// Return rentals
ArrayPool<object>.Shared.Return(arguments);
ArrayPool<char>.Shared.Return(buffer);
}
// </LocalizingTextOnePluralizedParameterMemoryPool>
}
StringLocalizer
IStringLocalizer implementation allocates two heap objects, a string for the key, and another object for the LocalizedString result object. It's possible to drop heap allocation to zero if localizer is decorated with a Dictionary lookup.
Method | Mean | Error | StdDev | Gen 0 | Allocated |
---|---|---|---|---|---|
StringLocalizerZeroParameters | 115.1 ns | 1.00 ns | 0.93 ns | 0.0134 | 112 B |
StringLocalizerOneParameter | 363.4 ns | 1.69 ns | 1.58 ns | 0.0229 | 192 B |
StringLocalizerOnePluralizedParameter | 325.4 ns | 4.42 ns | 4.14 ns | 0.0200 | 168 B |
StringLocalizerBenchmark.cs
using Avalanche.Localization;
using Avalanche.Localization.Internal;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using Microsoft.Extensions.Localization;
[SimpleJob(RuntimeMoniker.Net70), MemoryDiagnoser]
public class StringLocalizerBenchmark
{
ILocalization localization =
Avalanche.Localization.Localization.CreateDefault()
.AddLine("fi", "Namespace.HelloWorld", "Brace", "Hei maailma")
.AddLine("fi", "Namespace.Welcome", "Brace", "Tervetuloa, {User}.")
.AddLine("fi", "Namespace.Apples", "Brace", "Sinulla on ei ole omenoita.", "Unicode.CLDR41", "0:cardinal:zero")
.AddLine("fi", "Namespace.Apples", "Brace", "Sinulla on yksi omena.", "Unicode.CLDR41", "0:cardinal:one")
.AddLine("fi", "Namespace.Apples", "Brace", "Sinulla on {0} omenaa.", "Unicode.CLDR41", "0:cardinal:other");
IStringLocalizer localizer;
public StringLocalizerBenchmark()
{
localizer = new DiStringLocalizer(localization, "Namespace", "fi");
}
[Benchmark]
// <StringLocalizerZeroParameters>
public void StringLocalizerZeroParameters()
{
// Localize text
LocalizedString text = localizer["HelloWorld"]!;
// Print
string print = text.Value;
}
// </StringLocalizerZeroParameters>
[Benchmark]
// <StringLocalizerOneParameter>
public void StringLocalizerOneParameter()
{
// Localize text
LocalizedString text = localizer["Welcome", "User"]!;
// Print
string print = text.Value;
}
// </StringLocalizerOneParameter>
[Benchmark]
// <StringLocalizerOnePluralizedParameter>
public void StringLocalizerOnePluralizedParameter()
{
// Localize text
LocalizedString text = localizer["Apples", "2"]!;
// Print
string print = text.Value;
}
// </StringLocalizerOnePluralizedParameter>
}
TextLocalizer
ITextLocalizer implementation allocates a string for the key. It's possible to drop heap allocation to zero if localizer is decorated with a Dictionary lookup.
Method | Mean | Error | StdDev | Gen 0 | Allocated |
---|---|---|---|---|---|
TextLocalizerZeroParameters | 212.5 ns | 1.01 ns | 0.79 ns | 0.0076 | 64 B |
TextLocalizerZeroParametersMemoryPool | 356.5 ns | 3.85 ns | 3.41 ns | 0.0076 | 64 B |
TextLocalizerOneParameter | 408.3 ns | 1.51 ns | 1.26 ns | 0.0172 | 144 B |
TextLocalizerOneParameterMemoryPool | 539.2 ns | 1.66 ns | 1.55 ns | 0.0067 | 56 B |
TextLocalizerOnePluralizedParameter | 394.3 ns | 2.72 ns | 2.54 ns | 0.0143 | 120 B |
TextLocalizerOnePluralizedParameterMemoryPool | 577.0 ns | 6.63 ns | 6.20 ns | 0.0143 | 120 B |
TextLocalizerBenchmark.cs
using System.Buffers;
using Avalanche.Localization;
using Avalanche.Template;
using Avalanche.Utilities;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
[SimpleJob(RuntimeMoniker.Net70), MemoryDiagnoser]
public class TextLocalizerBenchmark
{
ILocalization localization =
Localization.CreateDefault()
.AddLine("fi", "Namespace.HelloWorld", "Brace", "Hei maailma")
.AddLine("fi", "Namespace.Welcome", "Brace", "Tervetuloa, {User}.")
.AddLine("fi", "Namespace.Apples", "Brace", "Sinulla on ei ole omenoita.", "Unicode.CLDR41", "0:cardinal:zero")
.AddLine("fi", "Namespace.Apples", "Brace", "Sinulla on yksi omena.", "Unicode.CLDR41", "0:cardinal:one")
.AddLine("fi", "Namespace.Apples", "Brace", "Sinulla on {0} omenaa.", "Unicode.CLDR41", "0:cardinal:other");
ICultureProvider cultureProvider = new CultureProvider("fi").SetReadOnly();
ITextLocalizer localizer;
public TextLocalizerBenchmark()
{
localizer = new TextLocalizer(localization, cultureProvider, "Namespace");
}
[Benchmark]
// <TextLocalizerZeroParameters>
public void TextLocalizerZeroParameters()
{
// Localize text
ILocalizedText text = localizer["HelloWorld"]!;
// Print without arguments
string print = text.Print(null, null);
}
// </TextLocalizerZeroParameters>
[Benchmark]
// <TextLocalizerZeroParametersMemoryPool>
public void TextLocalizerZeroParametersMemoryPool()
{
// Localize text
ILocalizedText text = localizer["HelloWorld"]!;
// Assign arguments
object?[]? arguments = null;
// Estimate length
int length = text.EstimatePrintLength(arguments);
// Rent chars
char[] buffer = ArrayPool<char>.Shared.Rent(length);
// Get span
Span<char> bufferSpan = buffer.AsSpan();
// Print to buffer
length = text.PrintTo(bufferSpan, arguments);
// Slice print
ReadOnlySpan<char> print = bufferSpan[..length];
// Return rentals
ArrayPool<char>.Shared.Return(buffer);
}
// </TextLocalizerZeroParametersMemoryPool>
[Benchmark]
// <TextLocalizerOneParameter>
public void TextLocalizerOneParameter()
{
// Localize text
ILocalizedText text = localizer["Welcome"]!;
// Print with argument
string print = text.Print(null, new object?[] { "User" });
}
// </TextLocalizerOneParameter>
[Benchmark]
// <TextLocalizerOneParameterMemoryPool>
public void TextLocalizerOneParameterMemoryPool()
{
// Localize text
ILocalizedText text = localizer["Welcome"]!;
// Rent arguments
object[] arguments = ArrayPool<object>.Shared.Rent(1);
// Assign argument
arguments[0] = "User";
// Estimate length
int length = text.EstimatePrintLength(arguments);
// Rent chars
char[] buffer = ArrayPool<char>.Shared.Rent(length);
// Get span
Span<char> bufferSpan = buffer.AsSpan();
// Print to buffer
length = text.PrintTo(bufferSpan, arguments);
// Slice print
ReadOnlySpan<char> print = bufferSpan[..length];
// Return rentals
ArrayPool<object>.Shared.Return(arguments);
ArrayPool<char>.Shared.Return(buffer);
}
// </TextLocalizerOneParameterMemoryPool>
[Benchmark]
// <TextLocalizerOnePluralizedParameter>
public void TextLocalizerOnePluralizedParameter()
{
// Localize text
ILocalizedText text = localizer["Apples"]!;
// Create arguments
object?[] arguments = new object?[] { "2" };
// Print with arguments
string print = text.Print(null, arguments);
}
// </TextLocalizerOnePluralizedParameter>
[Benchmark]
// <TextLocalizerOnePluralizedParameterMemoryPool>
public void TextLocalizerOnePluralizedParameterMemoryPool()
{
// Localize text
ILocalizedText text = localizer["Apples"]!;
// Rent arguments
object[] arguments = ArrayPool<object>.Shared.Rent(1);
// Assign argument
arguments[0] = "2";
// Localize & Pluralize to culture & arguments
ITemplatePrintable printable = text.Pluralize(arguments);
// Estimate length
int length = printable.EstimatePrintLength(arguments);
// Rent chars
char[] buffer = ArrayPool<char>.Shared.Rent(length);
// Get span
Span<char> bufferSpan = buffer.AsSpan();
// Print to buffer
length = printable.PrintTo(bufferSpan, arguments);
// Slice print
ReadOnlySpan<char> print = bufferSpan[..length];
// Return rentals
ArrayPool<object>.Shared.Return(arguments);
ArrayPool<char>.Shared.Return(buffer);
}
// </TextLocalizerOnePluralizedParameterMemoryPool>
}
MemoryPool vs Heap
The usage of memory pool is seemingly slightly slower than allocating short lived heap objects.
However, benchmark does not count in the CPU cycles that are expended on garbage collection in parallel thread. So the numbers for the non-pooled cases are not entirely complete, atleast not comparable with the pooled cases.
In a server environment, where all threads are congested, I'd wager that array pool solution is marginally more efficient, but in typical use cases, array pooling is not worth the effort.
Other
CPU cycles on pluralization cases dropped by 70% when moving from .NET 6 to .NET 7. There are some significant improvements under the hood.
Benchmarks were ran on Ryzen 3950x 16c32t.