Skip to content

Commit 71be00a

Browse files
committed
Add hash-set pool hysteresis
1 parent 6d35556 commit 71be00a

3 files changed

Lines changed: 172 additions & 4 deletions

File tree

source/core/slang-dictionary.h

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,18 @@ class Dictionary
143143
//
144144

145145
// Removes all values from the map
146-
void clear() { map.clear(); }
146+
void clear()
147+
{
148+
if (!map.empty())
149+
map.clear();
150+
}
151+
152+
// Removes all values and releases backing storage.
153+
void clearAndDeallocate()
154+
{
155+
InnerMap emptyMap(0, map.hash_function(), map.key_eq(), map.get_allocator());
156+
map.swap(emptyMap);
157+
}
147158

148159
// Erases the value at the specified key if it exists
149160
void remove(const TKey& key) { map.erase(key); }
@@ -178,6 +189,7 @@ class Dictionary
178189
//
179190

180191
std::size_t getCount() const { return map.size(); }
192+
std::size_t getBucketCount() const { return map.bucket_count(); }
181193

182194
//
183195
// Lookup
@@ -398,7 +410,9 @@ class HashSetBase
398410

399411
public:
400412
auto getCount() const { return dict.getCount(); }
413+
auto getBucketCount() const { return dict.getBucketCount(); }
401414
void clear() { dict.clear(); }
415+
void clearAndDeallocate() { dict.clearAndDeallocate(); }
402416
bool add(const T& obj) { return dict.addIfNotExists(obj, _DummyClass()); }
403417
bool add(T&& obj) { return dict.addIfNotExists(_Move(obj), _DummyClass()); }
404418
void remove(const T& obj) { dict.remove(obj); }

source/slang/slang-container-pool.h

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
namespace Slang
1212
{
1313
static const int kContainerPoolSize = 1024;
14+
static const size_t kContainerPoolHashSetMinRetireBucketCount = 4096;
15+
static const size_t kContainerPoolHashSetRetireUnderuseDivisor = 8;
16+
static const int kContainerPoolHashSetRetireUnderuseCount = 2;
1417

1518
template<typename T>
1619
struct ObjectPool
@@ -29,9 +32,16 @@ struct ObjectPool
2932
return &m_objects[id];
3033
}
3134

32-
void freeObject(T* object)
35+
int getObjectIndex(T* object)
3336
{
3437
auto id = (int)(object - m_objects.getBuffer());
38+
SLANG_ASSERT(id >= 0 && id < m_objects.getCount());
39+
return id;
40+
}
41+
42+
void freeObject(T* object)
43+
{
44+
auto id = getObjectIndex(object);
3545
m_pool.free(id, 1);
3646
}
3747

@@ -44,12 +54,16 @@ struct ContainerPool
4454
ObjectPool<List<void*>> m_listPool;
4555
ObjectPool<Dictionary<void*, void*>> m_dictionaryPool;
4656
ObjectPool<HashSet<void*>> m_hashSetPool;
57+
List<int> m_hashSetUnderuseStreaks;
4758

4859
ContainerPool()
4960
: m_listPool(kContainerPoolSize)
5061
, m_dictionaryPool(kContainerPoolSize)
5162
, m_hashSetPool(kContainerPoolSize)
5263
{
64+
m_hashSetUnderuseStreaks.setCount(kContainerPoolSize);
65+
for (Index i = 0; i < kContainerPoolSize; i++)
66+
m_hashSetUnderuseStreaks[i] = 0;
5367
}
5468

5569
template<typename T>
@@ -87,8 +101,42 @@ struct ContainerPool
87101
template<typename T>
88102
void free(HashSet<T*>* set)
89103
{
90-
set->clear();
91-
m_hashSetPool.freeObject((HashSet<void*>*)set);
104+
auto pooledSet = (HashSet<void*>*)set;
105+
auto objectIndex = m_hashSetPool.getObjectIndex(pooledSet);
106+
auto liveCount = set->getCount();
107+
auto bucketCount = set->getBucketCount();
108+
bool shouldRetire = false;
109+
110+
// Hash sets normally keep their buckets after `clear()` so the next large use can reuse
111+
// that storage. On long runs, though, a pool slot can become expensive if one large use is
112+
// followed by many tiny uses: every return clears the same large table even though the live
113+
// count stays small. Track that pattern per slot and only release the buckets after it
114+
// repeats, so an occasional small use after a large pass does not cause allocation churn.
115+
if (bucketCount >= kContainerPoolHashSetMinRetireBucketCount &&
116+
liveCount <= bucketCount / kContainerPoolHashSetRetireUnderuseDivisor)
117+
{
118+
if (m_hashSetUnderuseStreaks[objectIndex] < kContainerPoolHashSetRetireUnderuseCount)
119+
m_hashSetUnderuseStreaks[objectIndex]++;
120+
shouldRetire =
121+
m_hashSetUnderuseStreaks[objectIndex] >= kContainerPoolHashSetRetireUnderuseCount;
122+
}
123+
else
124+
{
125+
m_hashSetUnderuseStreaks[objectIndex] = 0;
126+
}
127+
128+
if (shouldRetire)
129+
{
130+
// Swap with an empty set instead of clearing first, because the oversized bucket array
131+
// is exactly the cost we are trying to stop paying on later small uses of this slot.
132+
set->clearAndDeallocate();
133+
m_hashSetUnderuseStreaks[objectIndex] = 0;
134+
}
135+
else
136+
{
137+
set->clear();
138+
}
139+
m_hashSetPool.freeObject(pooledSet);
92140
}
93141
};
94142
} // namespace Slang
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// unit-test-container-pool.cpp
2+
3+
#include "core/slang-list.h"
4+
#include "slang/slang-container-pool.h"
5+
#include "unit-test/slang-unit-test.h"
6+
7+
using namespace Slang;
8+
9+
static void _fillPointerSet(HashSet<int*>* set, List<int>& values, Index count)
10+
{
11+
values.setCount(count);
12+
for (Index i = 0; i < count; i++)
13+
{
14+
values[i] = int(i);
15+
set->add(&values[i]);
16+
}
17+
}
18+
19+
static size_t _growAndReturnLargeHashSet(ContainerPool& pool, List<int>& values)
20+
{
21+
auto set = pool.getHashSet<int>();
22+
_fillPointerSet(set, values, Index(kContainerPoolHashSetMinRetireBucketCount));
23+
24+
auto bucketCount = set->getBucketCount();
25+
SLANG_CHECK(bucketCount >= kContainerPoolHashSetMinRetireBucketCount);
26+
pool.free(set);
27+
return bucketCount;
28+
}
29+
30+
SLANG_UNIT_TEST(containerPoolHashSetClearAndDeallocate)
31+
{
32+
HashSet<int> set;
33+
for (Index i = 0; i < Index(kContainerPoolHashSetMinRetireBucketCount); i++)
34+
set.add(int(i));
35+
36+
auto largeBucketCount = set.getBucketCount();
37+
SLANG_CHECK(largeBucketCount >= kContainerPoolHashSetMinRetireBucketCount);
38+
39+
set.clear();
40+
SLANG_CHECK(set.getCount() == 0);
41+
SLANG_CHECK(set.getBucketCount() == largeBucketCount);
42+
43+
set.add(0);
44+
set.clearAndDeallocate();
45+
SLANG_CHECK(set.getCount() == 0);
46+
SLANG_CHECK(set.getBucketCount() < largeBucketCount);
47+
SLANG_CHECK(set.getBucketCount() < kContainerPoolHashSetMinRetireBucketCount);
48+
}
49+
50+
SLANG_UNIT_TEST(containerPoolHashSetHysteresisRetiresAfterSecondUnderuse)
51+
{
52+
ContainerPool pool;
53+
List<int> values;
54+
55+
auto largeBucketCount = _growAndReturnLargeHashSet(pool, values);
56+
57+
auto set = pool.getHashSet<int>();
58+
SLANG_CHECK(set->getBucketCount() == largeBucketCount);
59+
_fillPointerSet(set, values, 1);
60+
pool.free(set);
61+
62+
set = pool.getHashSet<int>();
63+
SLANG_CHECK(set->getBucketCount() == largeBucketCount);
64+
_fillPointerSet(set, values, 1);
65+
pool.free(set);
66+
67+
set = pool.getHashSet<int>();
68+
SLANG_CHECK(set->getBucketCount() < largeBucketCount);
69+
SLANG_CHECK(set->getBucketCount() < kContainerPoolHashSetMinRetireBucketCount);
70+
pool.free(set);
71+
}
72+
73+
SLANG_UNIT_TEST(containerPoolHashSetHysteresisResetsAfterSubstantialUse)
74+
{
75+
ContainerPool pool;
76+
List<int> values;
77+
78+
auto largeBucketCount = _growAndReturnLargeHashSet(pool, values);
79+
80+
auto set = pool.getHashSet<int>();
81+
SLANG_CHECK(set->getBucketCount() == largeBucketCount);
82+
_fillPointerSet(set, values, 1);
83+
pool.free(set);
84+
85+
set = pool.getHashSet<int>();
86+
SLANG_CHECK(set->getBucketCount() == largeBucketCount);
87+
auto substantialUseCount =
88+
Index(largeBucketCount / kContainerPoolHashSetRetireUnderuseDivisor + 1);
89+
_fillPointerSet(set, values, substantialUseCount);
90+
pool.free(set);
91+
92+
set = pool.getHashSet<int>();
93+
SLANG_CHECK(set->getBucketCount() == largeBucketCount);
94+
_fillPointerSet(set, values, 1);
95+
pool.free(set);
96+
97+
set = pool.getHashSet<int>();
98+
SLANG_CHECK(set->getBucketCount() == largeBucketCount);
99+
_fillPointerSet(set, values, 1);
100+
pool.free(set);
101+
102+
set = pool.getHashSet<int>();
103+
SLANG_CHECK(set->getBucketCount() < largeBucketCount);
104+
SLANG_CHECK(set->getBucketCount() < kContainerPoolHashSetMinRetireBucketCount);
105+
pool.free(set);
106+
}

0 commit comments

Comments
 (0)