From 6798a5b354dd7ea5a94289e550fe4f673a88b14d Mon Sep 17 00:00:00 2001 From: Jeff Hansen Date: Sat, 21 Feb 2026 16:52:22 -0500 Subject: [PATCH] Replace `Microsoft.Extensions.ObjectPool` with built-in Roslyn-based pool This may allocate more during heavy contention, but is ~50% faster overall --- .gitignore | 1 + .../Jeffijoe.MessageFormat.Benchmarks.csproj | 21 ++++ .../MessageFormat.snk | Bin 0 -> 596 bytes .../MessageFormatterBenchmarks.cs | 104 ++++++++++++++++++ .../PoolBenchmarks.cs | 45 ++++++++ .../Program.cs | 3 + .../ObjectPoolTests.cs | 79 +++++++++++++ .../Jeffijoe.MessageFormat.csproj | 5 +- src/Jeffijoe.MessageFormat/ObjectPool.cs | 96 ++++++++++++++++ .../Properties/AssemblyInfo.cs | 1 + .../StringBuilderPool.cs | 25 +++-- src/MessageFormat.sln | 42 +++++++ 12 files changed, 410 insertions(+), 12 deletions(-) create mode 100644 src/Jeffijoe.MessageFormat.Benchmarks/Jeffijoe.MessageFormat.Benchmarks.csproj create mode 100644 src/Jeffijoe.MessageFormat.Benchmarks/MessageFormat.snk create mode 100644 src/Jeffijoe.MessageFormat.Benchmarks/MessageFormatterBenchmarks.cs create mode 100644 src/Jeffijoe.MessageFormat.Benchmarks/PoolBenchmarks.cs create mode 100644 src/Jeffijoe.MessageFormat.Benchmarks/Program.cs create mode 100644 src/Jeffijoe.MessageFormat.Tests/ObjectPoolTests.cs create mode 100644 src/Jeffijoe.MessageFormat/ObjectPool.cs diff --git a/.gitignore b/.gitignore index 2e5faf7..45158ca 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ imagecache /src/.vs src/.idea/.idea.MessageFormat/.idea/workspace.xml .DS_Store +BenchmarkDotNet.Artifacts/ diff --git a/src/Jeffijoe.MessageFormat.Benchmarks/Jeffijoe.MessageFormat.Benchmarks.csproj b/src/Jeffijoe.MessageFormat.Benchmarks/Jeffijoe.MessageFormat.Benchmarks.csproj new file mode 100644 index 0000000..6315f90 --- /dev/null +++ b/src/Jeffijoe.MessageFormat.Benchmarks/Jeffijoe.MessageFormat.Benchmarks.csproj @@ -0,0 +1,21 @@ + + + + Exe + net10.0 + latest + enable + enable + True + MessageFormat.snk + + + + + + + + + + + diff --git a/src/Jeffijoe.MessageFormat.Benchmarks/MessageFormat.snk b/src/Jeffijoe.MessageFormat.Benchmarks/MessageFormat.snk new file mode 100644 index 0000000000000000000000000000000000000000..ba4de3e31dacaa7eb2ddabadc98d074accc7d7cc GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50096`4Z#Z7hc{9AhCyn5F*M|Jtu$1p6OJa& zmtgPnL2#eN4r$qiwBYJdD^@56kVB&!;p77J>g)ftcP{Oo^QWofpAsfB@s%l|>6m~8 zD4x)o9w|z`6BR1{yjg=>FYhRM*kl`4O{#xU5}52Siawh%C>h{fy6Ox3bN&zp6~D(R z2K8Qu)4ULXeB6@Eo;>OAD1!rwnPhq+3jN=#`>@szEkB3dbzg^ofOX&Xo=fOAMYIl4 ziKlJlbg@?#YV^prHihp9*0vbvf=Q?eXCnn zDN>{%hm_P|CUWYx*6$aj(9(vH?{)sU@Zl4s0PsVrF;>qfv&cvg_l#&(*Mw^0?C_5E zq@z$PvjME|HbsIZ^>ckxhax!~E62ZWF*7?C_II?0MM zgDq^8RmE0-na2lq)0PzU+N`X?;Qx}W$ISt+yQOgZV96rtbrikTlHQ$e^LbKY=vKR! zW>VmgM^K(0A!=b}p>UEj4=E@=-X390=@hCUhv;XS1&TukEwQ@RDCKMYOzGjNeBx30 zTh=>(-k7l2Dxw4{1x=FmyW$xq=;CmQ#=?^03!)&e^8W1uf~B1!_Hmdpyja>wq=okC z4XyvaUNWo@5{N_C>LU-12X$rBso_TXBVJU$0V)v=u123+H@_fnCAqj!Rgdet5urTd iS?#=Ksj{+Dm-ig3!o++c!PiDd%I>d>yHI|ZSd1ELG%EW5 literal 0 HcmV?d00001 diff --git a/src/Jeffijoe.MessageFormat.Benchmarks/MessageFormatterBenchmarks.cs b/src/Jeffijoe.MessageFormat.Benchmarks/MessageFormatterBenchmarks.cs new file mode 100644 index 0000000..e7cdf05 --- /dev/null +++ b/src/Jeffijoe.MessageFormat.Benchmarks/MessageFormatterBenchmarks.cs @@ -0,0 +1,104 @@ +using System.Collections.Generic; +using BenchmarkDotNet.Attributes; +using Jeffijoe.MessageFormat; + +namespace Jeffijoe.MessageFormat.Benchmarks; + +[MemoryDiagnoser] +public class MessageFormatterBenchmarks +{ + private MessageFormatter _formatter = null!; + + private readonly Dictionary _simpleArgs = new() { ["name"] = "World" }; + + private readonly Dictionary _pluralSimpleArgs = new() { ["count"] = 5 }; + + private readonly Dictionary _selectSimpleArgs = new() { ["gender"] = "male" }; + + private readonly Dictionary _pluralOffsetArgs = new() { ["count"] = 3 }; + + private readonly Dictionary _nested2Args = new() { ["gender"] = "female", ["count"] = 1 }; + + private readonly Dictionary _nested3Args = new() + { + ["gender"] = "male", + ["count"] = 2, + ["total"] = 10 + }; + + [GlobalSetup] + public void Setup() + { + _formatter = new MessageFormatter(); + } + + [Benchmark] + public string SimpleSubstitution() + { + return _formatter.FormatMessage("{name}", _simpleArgs); + } + + [Benchmark] + public string PluralSimple() + { + return _formatter.FormatMessage( + "{count, plural, one {1 thing} other {# things}}", + _pluralSimpleArgs); + } + + [Benchmark] + public string SelectSimple() + { + return _formatter.FormatMessage( + "{gender, select, male {He} female {She} other {They}}", + _selectSimpleArgs); + } + + [Benchmark] + public string PluralWithOffset() + { + return _formatter.FormatMessage( + "{count, plural, offset:1 =0 {Nobody} one {You and one other} other {You and # others}}", + _pluralOffsetArgs); + } + + [Benchmark] + public string Nested2Levels() + { + return _formatter.FormatMessage( + "{gender, select, male {{count, plural, one {He has 1 item} other {He has # items}}} female {{count, plural, one {She has 1 item} other {She has # items}}} other {{count, plural, one {They have 1 item} other {They have # items}}}}", + _nested2Args); + } + + [Benchmark] + public string Nested3Levels() + { + return _formatter.FormatMessage( + "{gender, select, male {{count, plural, one {He has 1 of {total} items} other {He has # of {total} items}}} female {{count, plural, one {She has 1 of {total} items} other {She has # of {total} items}}} other {{count, plural, one {They have 1 of {total} items} other {They have # of {total} items}}}}", + _nested3Args); + } + + [Params(1, 2, 4, 8)] + public int ThreadCount { get; set; } + + [Benchmark] + public void MultiThreadFormatMessage() + { + var args = new Dictionary { ["count"] = 5 }; + var pattern = "{count, plural, one {1 thing} other {# things}}"; + + var tasks = new Task[ThreadCount]; + for (var t = 0; t < ThreadCount; t++) + { + tasks[t] = Task.Run(() => + { + for (var i = 0; i < 1000; i++) + { + _formatter.FormatMessage(pattern, args); + } + }); + } + + Task.WaitAll(tasks); + } +} diff --git a/src/Jeffijoe.MessageFormat.Benchmarks/PoolBenchmarks.cs b/src/Jeffijoe.MessageFormat.Benchmarks/PoolBenchmarks.cs new file mode 100644 index 0000000..9948aa8 --- /dev/null +++ b/src/Jeffijoe.MessageFormat.Benchmarks/PoolBenchmarks.cs @@ -0,0 +1,45 @@ +using System.Text; +using BenchmarkDotNet.Attributes; +using Jeffijoe.MessageFormat; + +namespace Jeffijoe.MessageFormat.Benchmarks; + +[MemoryDiagnoser] +public class PoolBenchmarks +{ + private const int OperationsPerThread = 1000; + + [Benchmark] + public void SingleThreadGetReturn() + { + for (var i = 0; i < OperationsPerThread; i++) + { + var sb = StringBuilderPool.Get(); + sb.Append("test"); + StringBuilderPool.Return(sb); + } + } + + [Params(1, 2, 4, 8)] + public int ThreadCount { get; set; } + + [Benchmark] + public void MultiThreadGetReturn() + { + var tasks = new Task[ThreadCount]; + for (var t = 0; t < ThreadCount; t++) + { + tasks[t] = Task.Run(() => + { + for (var i = 0; i < OperationsPerThread; i++) + { + var sb = StringBuilderPool.Get(); + sb.Append("test"); + StringBuilderPool.Return(sb); + } + }); + } + + Task.WaitAll(tasks); + } +} diff --git a/src/Jeffijoe.MessageFormat.Benchmarks/Program.cs b/src/Jeffijoe.MessageFormat.Benchmarks/Program.cs new file mode 100644 index 0000000..c9a0467 --- /dev/null +++ b/src/Jeffijoe.MessageFormat.Benchmarks/Program.cs @@ -0,0 +1,3 @@ +using BenchmarkDotNet.Running; + +BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); diff --git a/src/Jeffijoe.MessageFormat.Tests/ObjectPoolTests.cs b/src/Jeffijoe.MessageFormat.Tests/ObjectPoolTests.cs new file mode 100644 index 0000000..c530fd3 --- /dev/null +++ b/src/Jeffijoe.MessageFormat.Tests/ObjectPoolTests.cs @@ -0,0 +1,79 @@ +using System.Text; +using System.Threading.Tasks; + +using Xunit; + +namespace Jeffijoe.MessageFormat.Tests; + +public class ObjectPoolTests +{ + [Fact] + public void Allocate_WhenPoolEmpty_ReturnsNewObject() + { + var pool = new ObjectPool(() => new StringBuilder()); + var sb = pool.Allocate(); + Assert.NotNull(sb); + } + + [Fact] + public void Free_ThenAllocate_ReturnsSameInstance() + { + var pool = new ObjectPool(() => new StringBuilder()); + var sb = pool.Allocate(); + pool.Free(sb); + var sb2 = pool.Allocate(); + Assert.Same(sb, sb2); + } + + [Fact] + public void Allocate_BeyondPoolSize_CreatesNewObjects() + { + var pool = new ObjectPool(() => new StringBuilder(), size: 2); + var a = pool.Allocate(); + var b = pool.Allocate(); + var c = pool.Allocate(); + Assert.NotSame(a, b); + Assert.NotSame(b, c); + Assert.NotSame(a, c); + } + + [Fact] + public void Free_BeyondPoolSize_DoesNotThrow() + { + var pool = new ObjectPool(() => new StringBuilder(), size: 2); + var a = pool.Allocate(); + var b = pool.Allocate(); + var c = pool.Allocate(); + pool.Free(a); + pool.Free(b); + pool.Free(c); // exceeds pool size, should silently discard + } + + [Fact] + public async Task ConcurrentAllocateAndFree_DoesNotThrow() + { + var pool = new ObjectPool(() => new StringBuilder()); + const int ThreadCount = 8; + const int Iterations = 1000; + + var tasks = new Task[ThreadCount]; + for (var t = 0; t < ThreadCount; t++) + { + tasks[t] = Task.Run(() => + { + for (var i = 0; i < Iterations; i++) + { + var sb = pool.Allocate(); + sb.Append("test"); + var output = sb.ToString(); + // Assert we didn't get a dirty builder with data still left in it. + Assert.Equal("test", output); + sb.Clear(); + pool.Free(sb); + } + }); + } + + await Task.WhenAll(tasks); + } +} diff --git a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj index e17ea51..793e4f7 100644 --- a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj +++ b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj @@ -20,7 +20,6 @@ - all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -35,5 +34,9 @@ + + + + diff --git a/src/Jeffijoe.MessageFormat/ObjectPool.cs b/src/Jeffijoe.MessageFormat/ObjectPool.cs new file mode 100644 index 0000000..6b26c26 --- /dev/null +++ b/src/Jeffijoe.MessageFormat/ObjectPool.cs @@ -0,0 +1,96 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// Ported from Roslyn, see: https://github.com/dotnet/roslyn/blob/main/src/Dependencies/PooledObjects/ObjectPool%601.cs. + +using System; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace Jeffijoe.MessageFormat; + +/// +/// Generic implementation of object pooling pattern with predefined pool size limit. The main purpose +/// is that limited number of frequently used objects can be kept in the pool for further recycling. +/// +/// The type of objects to pool. +internal sealed class ObjectPool(Func factory, int size) + where T : class +{ + private readonly Element[] _items = new Element[size - 1]; + private T? _firstItem; + + public ObjectPool(Func factory) + : this(factory, Environment.ProcessorCount * 2) + { + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T Allocate() + { + var item = _firstItem; + + if (item is null || item != Interlocked.CompareExchange(ref _firstItem, null, item)) + { + item = AllocateSlow(); + } + + return item; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Free(T obj) + { + if (_firstItem is null) + { + _firstItem = obj; + } + else + { + FreeSlow(obj); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private T AllocateSlow() + { + foreach (ref var element in _items.AsSpan()) + { + var instance = element.Value; + + if (instance is null) + { + continue; + } + + if (instance == Interlocked.CompareExchange(ref element.Value, null, instance)) + { + return instance; + } + } + + + return factory(); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void FreeSlow(T obj) + { + foreach (ref var element in _items.AsSpan()) + { + if (element.Value is not null) + { + continue; + } + + element.Value = obj; + break; + } + } + + private struct Element + { + internal T? Value; + } +} diff --git a/src/Jeffijoe.MessageFormat/Properties/AssemblyInfo.cs b/src/Jeffijoe.MessageFormat/Properties/AssemblyInfo.cs index f9ea5d5..8df6583 100644 --- a/src/Jeffijoe.MessageFormat/Properties/AssemblyInfo.cs +++ b/src/Jeffijoe.MessageFormat/Properties/AssemblyInfo.cs @@ -6,3 +6,4 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Jeffijoe.MessageFormat.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001004f0dc10ad8873751f986416a7d3134e473ad3454a7138e26cf9760eff341709fc50e69d985b4e0ea512b5628079043a31ce1e402f4eaebffb5772eed9ef3a7a9e39f122633f19529a1e9988005289ed09a1e294abe13152afebc59835c2fef2879d8641b564daa7f511298ec2f8a3e9b322819e05cbaea0bfc73fe100615bfc7")] +[assembly: InternalsVisibleTo("Jeffijoe.MessageFormat.Benchmarks, PublicKey=00240000048000009400000006020000002400005253413100040000010001004f0dc10ad8873751f986416a7d3134e473ad3454a7138e26cf9760eff341709fc50e69d985b4e0ea512b5628079043a31ce1e402f4eaebffb5772eed9ef3a7a9e39f122633f19529a1e9988005289ed09a1e294abe13152afebc59835c2fef2879d8641b564daa7f511298ec2f8a3e9b322819e05cbaea0bfc73fe100615bfc7")] diff --git a/src/Jeffijoe.MessageFormat/StringBuilderPool.cs b/src/Jeffijoe.MessageFormat/StringBuilderPool.cs index f91fc26..7d0a1cb 100644 --- a/src/Jeffijoe.MessageFormat/StringBuilderPool.cs +++ b/src/Jeffijoe.MessageFormat/StringBuilderPool.cs @@ -1,25 +1,28 @@ -using System.Text; -using Microsoft.Extensions.ObjectPool; +using System.Text; namespace Jeffijoe.MessageFormat; internal static class StringBuilderPool { - private static readonly ObjectPool SbPool; + private const int MaxBuilderCapacity = 4096; - static StringBuilderPool() - { - var shared = new DefaultObjectPoolProvider(); - SbPool = shared.CreateStringBuilderPool(); - } + private static readonly ObjectPool SbPool = new(static () => new StringBuilder()); public static StringBuilder Get() { - return SbPool.Get(); + return SbPool.Allocate(); } public static void Return(StringBuilder sb) { - SbPool.Return(sb); + // If the builder grew too large, just let it go + // rather than returning it so it can get garbage-collected. + if (sb.Capacity > MaxBuilderCapacity) + { + return; + } + + sb.Clear(); + SbPool.Free(sb); } -} \ No newline at end of file +} diff --git a/src/MessageFormat.sln b/src/MessageFormat.sln index 3a62c07..d682f2e 100644 --- a/src/MessageFormat.sln +++ b/src/MessageFormat.sln @@ -9,24 +9,66 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jeffijoe.MessageFormat.Test EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jeffijoe.MessageFormat.MetadataGenerator", "Jeffijoe.MessageFormat.MetadataGenerator\Jeffijoe.MessageFormat.MetadataGenerator.csproj", "{5431C848-23D1-4752-A9B0-5159E5B2F92E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jeffijoe.MessageFormat.Benchmarks", "Jeffijoe.MessageFormat.Benchmarks\Jeffijoe.MessageFormat.Benchmarks.csproj", "{D63A7E6E-D302-44E2-A355-F72DD005AB57}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {7D16B114-A482-4FC4-A055-8E96573BE2A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7D16B114-A482-4FC4-A055-8E96573BE2A3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7D16B114-A482-4FC4-A055-8E96573BE2A3}.Debug|x64.ActiveCfg = Debug|Any CPU + {7D16B114-A482-4FC4-A055-8E96573BE2A3}.Debug|x64.Build.0 = Debug|Any CPU + {7D16B114-A482-4FC4-A055-8E96573BE2A3}.Debug|x86.ActiveCfg = Debug|Any CPU + {7D16B114-A482-4FC4-A055-8E96573BE2A3}.Debug|x86.Build.0 = Debug|Any CPU {7D16B114-A482-4FC4-A055-8E96573BE2A3}.Release|Any CPU.ActiveCfg = Release|Any CPU {7D16B114-A482-4FC4-A055-8E96573BE2A3}.Release|Any CPU.Build.0 = Release|Any CPU + {7D16B114-A482-4FC4-A055-8E96573BE2A3}.Release|x64.ActiveCfg = Release|Any CPU + {7D16B114-A482-4FC4-A055-8E96573BE2A3}.Release|x64.Build.0 = Release|Any CPU + {7D16B114-A482-4FC4-A055-8E96573BE2A3}.Release|x86.ActiveCfg = Release|Any CPU + {7D16B114-A482-4FC4-A055-8E96573BE2A3}.Release|x86.Build.0 = Release|Any CPU {F1AC744E-9031-468E-A397-6F44AA19EBA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F1AC744E-9031-468E-A397-6F44AA19EBA1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F1AC744E-9031-468E-A397-6F44AA19EBA1}.Debug|x64.ActiveCfg = Debug|Any CPU + {F1AC744E-9031-468E-A397-6F44AA19EBA1}.Debug|x64.Build.0 = Debug|Any CPU + {F1AC744E-9031-468E-A397-6F44AA19EBA1}.Debug|x86.ActiveCfg = Debug|Any CPU + {F1AC744E-9031-468E-A397-6F44AA19EBA1}.Debug|x86.Build.0 = Debug|Any CPU {F1AC744E-9031-468E-A397-6F44AA19EBA1}.Release|Any CPU.ActiveCfg = Release|Any CPU {F1AC744E-9031-468E-A397-6F44AA19EBA1}.Release|Any CPU.Build.0 = Release|Any CPU + {F1AC744E-9031-468E-A397-6F44AA19EBA1}.Release|x64.ActiveCfg = Release|Any CPU + {F1AC744E-9031-468E-A397-6F44AA19EBA1}.Release|x64.Build.0 = Release|Any CPU + {F1AC744E-9031-468E-A397-6F44AA19EBA1}.Release|x86.ActiveCfg = Release|Any CPU + {F1AC744E-9031-468E-A397-6F44AA19EBA1}.Release|x86.Build.0 = Release|Any CPU {5431C848-23D1-4752-A9B0-5159E5B2F92E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5431C848-23D1-4752-A9B0-5159E5B2F92E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5431C848-23D1-4752-A9B0-5159E5B2F92E}.Debug|x64.ActiveCfg = Debug|Any CPU + {5431C848-23D1-4752-A9B0-5159E5B2F92E}.Debug|x64.Build.0 = Debug|Any CPU + {5431C848-23D1-4752-A9B0-5159E5B2F92E}.Debug|x86.ActiveCfg = Debug|Any CPU + {5431C848-23D1-4752-A9B0-5159E5B2F92E}.Debug|x86.Build.0 = Debug|Any CPU {5431C848-23D1-4752-A9B0-5159E5B2F92E}.Release|Any CPU.ActiveCfg = Release|Any CPU {5431C848-23D1-4752-A9B0-5159E5B2F92E}.Release|Any CPU.Build.0 = Release|Any CPU + {5431C848-23D1-4752-A9B0-5159E5B2F92E}.Release|x64.ActiveCfg = Release|Any CPU + {5431C848-23D1-4752-A9B0-5159E5B2F92E}.Release|x64.Build.0 = Release|Any CPU + {5431C848-23D1-4752-A9B0-5159E5B2F92E}.Release|x86.ActiveCfg = Release|Any CPU + {5431C848-23D1-4752-A9B0-5159E5B2F92E}.Release|x86.Build.0 = Release|Any CPU + {D63A7E6E-D302-44E2-A355-F72DD005AB57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D63A7E6E-D302-44E2-A355-F72DD005AB57}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D63A7E6E-D302-44E2-A355-F72DD005AB57}.Debug|x64.ActiveCfg = Debug|Any CPU + {D63A7E6E-D302-44E2-A355-F72DD005AB57}.Debug|x64.Build.0 = Debug|Any CPU + {D63A7E6E-D302-44E2-A355-F72DD005AB57}.Debug|x86.ActiveCfg = Debug|Any CPU + {D63A7E6E-D302-44E2-A355-F72DD005AB57}.Debug|x86.Build.0 = Debug|Any CPU + {D63A7E6E-D302-44E2-A355-F72DD005AB57}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D63A7E6E-D302-44E2-A355-F72DD005AB57}.Release|Any CPU.Build.0 = Release|Any CPU + {D63A7E6E-D302-44E2-A355-F72DD005AB57}.Release|x64.ActiveCfg = Release|Any CPU + {D63A7E6E-D302-44E2-A355-F72DD005AB57}.Release|x64.Build.0 = Release|Any CPU + {D63A7E6E-D302-44E2-A355-F72DD005AB57}.Release|x86.ActiveCfg = Release|Any CPU + {D63A7E6E-D302-44E2-A355-F72DD005AB57}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE