Skip to content

Commit c5bfffd

Browse files
perf: add PooledHashSet and PooledStringBuilder
1 parent 9cf0eaf commit c5bfffd

File tree

5 files changed

+321
-2
lines changed

5 files changed

+321
-2
lines changed

Src/CSharpier/DocPrinter/DocPrinter.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ internal class DocPrinter
77
protected readonly Stack<PrintCommand> RemainingCommands = new();
88
protected readonly Dictionary<string, PrintMode> GroupModeMap = new();
99
protected int CurrentWidth;
10-
protected readonly StringBuilder Output = new();
10+
protected readonly PooledStringBuilder PooledOutput;
11+
protected readonly StringBuilder Output;
1112
protected bool ShouldRemeasure;
1213
protected bool NewLineNextStringValue;
1314
protected bool SkipNextNewLine;
@@ -28,6 +29,8 @@ protected DocPrinter(Doc doc, PrinterOptions printerOptions, string endOfLine)
2829
this.RemainingCommands.Push(
2930
new PrintCommand(Indenter.GenerateRoot(), PrintMode.Break, doc)
3031
);
32+
this.PooledOutput = PooledStringBuilder.GetInstance();
33+
this.Output = this.PooledOutput.Builder;
3134
}
3235

3336
public static string Print(Doc document, PrinterOptions printerOptions, string endOfLine)
@@ -52,6 +55,8 @@ public string Print()
5255
result = result.TrimStart('\n', '\r');
5356
}
5457

58+
this.PooledOutput.Free();
59+
5560
return result;
5661
}
5762

Src/CSharpier/DocPrinter/PropagateBreaks.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ private class MarkerDoc : Doc { }
88

99
public static void RunOn(Doc document)
1010
{
11-
var alreadyVisitedSet = new HashSet<Group>();
11+
var alreadyVisitedSet = PooledHashSet<Group>.GetInstance();
1212
var groupStack = new Stack<Group>();
1313
var forceFlat = 0;
1414
var canSkipBreak = false;
@@ -132,5 +132,7 @@ void OnExit(Doc doc)
132132
docsStack.Push(hasContents.Contents);
133133
}
134134
}
135+
136+
alreadyVisitedSet.Free();
135137
}
136138
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using System.Diagnostics;
2+
3+
namespace CSharpier.Utilities;
4+
5+
// From https://github.com/dotnet/roslyn/blob/38f239fb81b72bfd313cd18aeff0b0ed40f34c5c/src/Dependencies/PooledObjects/PooledHashSet.cs#L12
6+
internal sealed class PooledHashSet<T> : HashSet<T>
7+
{
8+
private readonly ObjectPool<PooledHashSet<T>> _pool;
9+
10+
private PooledHashSet(ObjectPool<PooledHashSet<T>> pool, IEqualityComparer<T> equalityComparer)
11+
: base(equalityComparer)
12+
{
13+
_pool = pool;
14+
}
15+
16+
public void Free()
17+
{
18+
if (this.Count <= 100_000)
19+
{
20+
this.Clear();
21+
_pool?.Free(this);
22+
}
23+
}
24+
25+
// global pool
26+
private static readonly ObjectPool<PooledHashSet<T>> s_poolInstance = CreatePool(
27+
EqualityComparer<T>.Default
28+
);
29+
30+
// if someone needs to create a pool;
31+
public static ObjectPool<PooledHashSet<T>> CreatePool(IEqualityComparer<T> equalityComparer)
32+
{
33+
ObjectPool<PooledHashSet<T>>? pool = null;
34+
pool = new ObjectPool<PooledHashSet<T>>(
35+
() => new PooledHashSet<T>(pool!, equalityComparer),
36+
16
37+
);
38+
return pool;
39+
}
40+
41+
public static PooledHashSet<T> GetInstance()
42+
{
43+
var instance = s_poolInstance.Allocate();
44+
Debug.Assert(instance.Count == 0);
45+
return instance;
46+
}
47+
}

Src/CSharpier/Utilities/ObjectPool.cs

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
using System.Diagnostics;
2+
3+
namespace CSharpier.Utilities;
4+
5+
// From https://github.com/dotnet/roslyn/blob/38f239fb81b72bfd313cd18aeff0b0ed40f34c5c/src/Dependencies/PooledObjects/ObjectPool%601.cs#L42
6+
7+
/// <summary>
8+
/// Generic implementation of object pooling pattern with predefined pool size limit. The main
9+
/// purpose is that limited number of frequently used objects can be kept in the pool for
10+
/// further recycling.
11+
///
12+
/// Notes:
13+
/// 1) it is not the goal to keep all returned objects. Pool is not meant for storage. If there
14+
/// is no space in the pool, extra returned objects will be dropped.
15+
///
16+
/// 2) it is implied that if object was obtained from a pool, the caller will return it back in
17+
/// a relatively short time. Keeping checked out objects for long durations is ok, but
18+
/// reduces usefulness of pooling. Just new up your own.
19+
///
20+
/// Not returning objects to the pool in not detrimental to the pool's work, but is a bad practice.
21+
/// Rationale:
22+
/// If there is no intent for reusing the object, do not use pool - just use "new".
23+
/// </summary>
24+
internal class ObjectPool<T>
25+
where T : class
26+
{
27+
[DebuggerDisplay("{Value,nq}")]
28+
private struct Element
29+
{
30+
internal T? Value;
31+
}
32+
33+
/// <remarks>
34+
/// Not using System.Func{T} because this file is linked into the (debugger) Formatter,
35+
/// which does not have that type (since it compiles against .NET 2.0).
36+
/// </remarks>
37+
internal delegate T Factory();
38+
39+
// Storage for the pool objects. The first item is stored in a dedicated field because we
40+
// expect to be able to satisfy most requests from it.
41+
private T? _firstItem;
42+
private readonly Element[] _items;
43+
44+
// factory is stored for the lifetime of the pool. We will call this only when pool needs to
45+
// expand. compared to "new T()", Func gives more flexibility to implementers and faster
46+
// than "new T()".
47+
private readonly Factory _factory;
48+
49+
public readonly bool TrimOnFree;
50+
51+
internal ObjectPool(Factory factory, bool trimOnFree = true)
52+
: this(factory, Environment.ProcessorCount * 2, trimOnFree) { }
53+
54+
internal ObjectPool(Factory factory, int size, bool trimOnFree = true)
55+
{
56+
Debug.Assert(size >= 1);
57+
_factory = factory;
58+
_items = new Element[size - 1];
59+
TrimOnFree = trimOnFree;
60+
}
61+
62+
internal ObjectPool(Func<ObjectPool<T>, T> factory, int size)
63+
{
64+
Debug.Assert(size >= 1);
65+
_factory = () => factory(this);
66+
_items = new Element[size - 1];
67+
}
68+
69+
private T CreateInstance()
70+
{
71+
var inst = _factory();
72+
return inst;
73+
}
74+
75+
/// <summary>
76+
/// Produces an instance.
77+
/// </summary>
78+
/// <remarks>
79+
/// Search strategy is a simple linear probing which is chosen for it cache-friendliness.
80+
/// Note that Free will try to store recycled objects close to the start thus statistically
81+
/// reducing how far we will typically search.
82+
/// </remarks>
83+
internal T Allocate()
84+
{
85+
// PERF: Examine the first element. If that fails, AllocateSlow will look at the remaining elements.
86+
// Note that the initial read is optimistically not synchronized. That is intentional.
87+
// We will interlock only when we have a candidate. in a worst case we may miss some
88+
// recently returned objects. Not a big deal.
89+
var inst = _firstItem;
90+
if (inst == null || inst != Interlocked.CompareExchange(ref _firstItem, null, inst))
91+
{
92+
inst = AllocateSlow();
93+
}
94+
95+
return inst;
96+
}
97+
98+
private T AllocateSlow()
99+
{
100+
var items = _items;
101+
102+
for (var i = 0; i < items.Length; i++)
103+
{
104+
// Note that the initial read is optimistically not synchronized. That is intentional.
105+
// We will interlock only when we have a candidate. in a worst case we may miss some
106+
// recently returned objects. Not a big deal.
107+
var inst = items[i].Value;
108+
if (inst != null)
109+
{
110+
if (inst == Interlocked.CompareExchange(ref items[i].Value, null, inst))
111+
{
112+
return inst;
113+
}
114+
}
115+
}
116+
117+
return CreateInstance();
118+
}
119+
120+
/// <summary>
121+
/// Returns objects to the pool.
122+
/// </summary>
123+
/// <remarks>
124+
/// Search strategy is a simple linear probing which is chosen for it cache-friendliness.
125+
/// Note that Free will try to store recycled objects close to the start thus statistically
126+
/// reducing how far we will typically search in Allocate.
127+
/// </remarks>
128+
internal void Free(T obj)
129+
{
130+
Validate(obj);
131+
132+
if (_firstItem == null)
133+
{
134+
// Intentionally not using interlocked here.
135+
// In a worst case scenario two objects may be stored into same slot.
136+
// It is very unlikely to happen and will only mean that one of the objects will get collected.
137+
_firstItem = obj;
138+
}
139+
else
140+
{
141+
FreeSlow(obj);
142+
}
143+
}
144+
145+
private void FreeSlow(T obj)
146+
{
147+
var items = _items;
148+
for (var i = 0; i < items.Length; i++)
149+
{
150+
if (items[i].Value == null)
151+
{
152+
// Intentionally not using interlocked here.
153+
// In a worst case scenario two objects may be stored into same slot.
154+
// It is very unlikely to happen and will only mean that one of the objects will get collected.
155+
items[i].Value = obj;
156+
break;
157+
}
158+
}
159+
}
160+
161+
private void Validate(object obj)
162+
{
163+
Debug.Assert(obj != null, "freeing null?");
164+
165+
Debug.Assert(_firstItem != obj, "freeing twice?");
166+
167+
var items = _items;
168+
for (var i = 0; i < items.Length; i++)
169+
{
170+
var value = items[i].Value;
171+
if (value == null)
172+
{
173+
return;
174+
}
175+
176+
Debug.Assert(value != obj, "freeing twice?");
177+
}
178+
}
179+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
using System.Diagnostics;
2+
using System.Text;
3+
4+
namespace CSharpier.Utilities;
5+
6+
// From https://github.com/dotnet/roslyn/blob/38f239fb81b72bfd313cd18aeff0b0ed40f34c5c/src/Dependencies/PooledObjects/PooledStringBuilder.cs#L18
7+
8+
/// <summary>
9+
/// The usage is:
10+
/// var inst = PooledStringBuilder.GetInstance();
11+
/// var sb = inst.builder;
12+
/// ... Do Stuff...
13+
/// ... sb.ToString() ...
14+
/// inst.Free();
15+
/// </summary>
16+
internal sealed class PooledStringBuilder
17+
{
18+
public readonly StringBuilder Builder = new();
19+
private readonly ObjectPool<PooledStringBuilder> _pool;
20+
21+
private PooledStringBuilder(ObjectPool<PooledStringBuilder> pool)
22+
{
23+
Debug.Assert(pool != null);
24+
_pool = pool!;
25+
}
26+
27+
public int Length
28+
{
29+
get { return this.Builder.Length; }
30+
}
31+
32+
public void Free()
33+
{
34+
var builder = this.Builder;
35+
36+
// do not store builders that are too large.
37+
if (builder.Capacity <= 2_000_000)
38+
{
39+
builder.Clear();
40+
_pool.Free(this);
41+
}
42+
}
43+
44+
public string ToStringAndFree()
45+
{
46+
var result = this.Builder.ToString();
47+
this.Free();
48+
49+
return result;
50+
}
51+
52+
public string ToStringAndFree(int startIndex, int length)
53+
{
54+
var result = this.Builder.ToString(startIndex, length);
55+
this.Free();
56+
57+
return result;
58+
}
59+
60+
// global pool
61+
private static readonly ObjectPool<PooledStringBuilder> s_poolInstance = CreatePool();
62+
63+
// if someone needs to create a private pool;
64+
/// <summary>
65+
/// If someone need to create a private pool
66+
/// </summary>
67+
/// <param name="size">The size of the pool.</param>
68+
public static ObjectPool<PooledStringBuilder> CreatePool(int size = 16)
69+
{
70+
ObjectPool<PooledStringBuilder>? pool = null;
71+
pool = new ObjectPool<PooledStringBuilder>(() => new PooledStringBuilder(pool!), size);
72+
return pool;
73+
}
74+
75+
public static PooledStringBuilder GetInstance()
76+
{
77+
var builder = s_poolInstance.Allocate();
78+
Debug.Assert(builder.Builder.Length == 0);
79+
return builder;
80+
}
81+
82+
public static implicit operator StringBuilder(PooledStringBuilder obj)
83+
{
84+
return obj.Builder;
85+
}
86+
}

0 commit comments

Comments
 (0)