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
Replace BufferCache
with ArrayPool<T>
#8841
base: main
Are you sure you want to change the base?
Conversation
@onkrot - Thank you for this PR. Looks nice. Were you able to benchmark the perf impact of this change at your end? |
For the benchmark, I have taken several considerations:
So, with all the above considerations, I have come up with this benchmark: using BenchmarkDotNet.Attributes;
using MS.Internal.FontCache;
using System.Buffers;
namespace Bench
{
public class BenchCache
{
[Params(100, 200)]
public int FontCount { get; set; }
readonly Random random = new(0);
[Benchmark]
public void BufferCacheUint()
{
for (int i = 0; i < FontCount; i++)
{
var array = BufferCache.GetUInts(random.Next(65536));
BufferCache.ReleaseUInts(array);
}
}
[Benchmark]
public void BufferCacheUshort()
{
for (int i = 0; i < FontCount; i++)
{
var array = BufferCache.GetUShorts(random.Next(65536));
BufferCache.ReleaseUShorts(array);
}
}
[Benchmark]
public void BufferCacheGlyph()
{
for (int i = 0; i < FontCount; i++)
{
var array = BufferCache.GetGlyphMetrics(random.Next(65536));
BufferCache.ReleaseGlyphMetrics(array);
}
}
[Benchmark]
public void ArrayPoolUint()
{
for (int i = 0; i < FontCount; i++)
{
var array = ArrayPool<uint>.Shared.Rent(random.Next(65536));
ArrayPool<uint>.Shared.Return(array);
}
}
[Benchmark]
public void ArrayPoolUshort()
{
for (int i = 0; i < FontCount; i++)
{
var array = ArrayPool<ushort>.Shared.Rent(random.Next(65536));
ArrayPool<ushort>.Shared.Return(array);
}
}
[Benchmark]
public void ArrayPoolGlyph()
{
for (int i = 0; i < FontCount; i++)
{
var array = ArrayPool<GlyphMetrics>.Shared.Rent(random.Next(65536));
ArrayPool<GlyphMetrics>.Shared.Return(array);
}
}
}
} which has yielded these results:
|
In an ideal scenario (with the number of elements < 1024) BufferCache is faster:
|
The results for increased max array size for
|
@@ -632,7 +642,7 @@ out idealWidth | |||
GlyphTypeface glyphTypeface = TryGetGlyphTypeface(); | |||
Invariant.Assert(glyphTypeface != null); | |||
|
|||
MS.Internal.Text.TextInterface.GlyphMetrics[] glyphMetrics = BufferCache.GetGlyphMetrics(charBufferRange.Length); | |||
MS.Internal.Text.TextInterface.GlyphMetrics[] glyphMetrics = ArrayPool<MS.Internal.Text.TextInterface.GlyphMetrics>.Shared.Rent(charBufferRange.Length); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for your contribution. Sorry, I don't believe that placing GlyphMetrics into the ArrayPool is a genuine point of optimization. The reason is that GlyphMetrics is an array type that is only used in this context, unlike the ushort
or byte
, and thus it cannot be reused with other modules through ArrayPool. In order to create a shared GlyphMetrics array, it would be necessary to create additional objects such as SharedArrayPool<GlyphMetrics>
, which seems to have a higher cost than benefit. More importantly, the GlyphMetrics array is an object that is frequently used and then released. It can be assumed that virtually all modern WPF applications have text, meaning they constantly require the acquisition and return of GlyphMetrics arrays. In this case, it might be worth considering using a local variable to cache it. Perhaps the cost of using a local variable for caching is the lowest, and the benefit is the highest.
Type | Count | Size | ReferenceSize |
---|---|---|---|
SharedArrayPool+Partitions<GlyphMetrics>[] |
1 | 240 | 240 |
GlyphMetrics[] |
1 | 152 | 152 |
ConditionalWeakTable+Entry<SharedArrayPool+ThreadLocalArray<GlyphMetrics>[], Object>[] |
1 | 152 | 152 |
ConditionalWeakTable+Container<SharedArrayPool+ThreadLocalArray<GlyphMetrics>[], Object> |
1 | 56 | 264 |
ConditionalWeakTable<SharedArrayPool+ThreadLocalArray<GlyphMetrics>[], Object> |
1 | 40 | 328 |
SharedArrayPool<GlyphMetrics> |
1 | 40 | 608 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, let me group proposed solutions:
GlyphMetrics[]
- approach similar toBufferCache
without automatic releaseConditionalWeakTable
- automatic release ofArrayPool
? SharedArrayPool already has a mechanism to detect unused arrays and free them on garbage collection eventSharedArrayPool
- proposed solution
Also, perhaps we could use stackalloc
array here if the number of elements is not too high (maybe 150 (for 4096 / sizeof(GlyphMetrics)
))
@onkrot Thank you for this PR. Unfortunately, your benchmark is not representative because Running your benchmark with a fixed buffer size of
|
After looking back at this PR it seems like I missed the fact that you already mention the 1024 items limit so you can disregard my comment. Though I think my suggestion of removing the use of For this PR, it looks like the benefit is improved performance for larger arrays at the cost of reduced performance for smaller arrays ? |
@ThomasGoulet73 As the benchmark shows - yes. But the |
@onkrot With the common business logic, It hard to get the bad input. |
Fixes #
Main PR
Description
Replace internal
BufferCache
with genericArrayPool<T>
Customer Impact
Small performance improvement
Regression
None
Testing
None
Risk
Low risk
Microsoft Reviewers: Open in CodeFlow