Skip to content

Commit 3d8e9b8

Browse files
authored
Merge pull request #1107 from zeux/alloccache
allocator: Implement support for allocation cache
2 parents 4195af6 + 61b97a6 commit 3d8e9b8

5 files changed

Lines changed: 224 additions & 0 deletions

File tree

‎README.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -978,6 +978,16 @@ meshopt_setAllocator(malloc, free);
978978

979979
> Note that the library expects the allocation function to either throw in case of out-of-memory (in which case the exception will propagate to the caller) or abort, so technically the use of `malloc` above isn't safe. If you want to handle out-of-memory errors without using C++ exceptions, you can use `setjmp`/`longjmp` instead.
980980
981+
For applications that end up using many allocations, such as when processing many small meshes or individual clusters as part of `clusterlod.h`, it may be beneficial to set up the allocator cache:
982+
983+
```c++
984+
meshopt_setAllocatorCache(16, 32 << 20); // 16 threads x 32MB per thread
985+
```
986+
987+
This will allocate a pool of memory that will be used for subsequent temporary allocations; this is particularly helpful for allocators that don't reuse larger allocations, or use global locks to manage heap memory, such as the default Windows heap. After processing is finished, `meshopt_setAllocatorCache(0, 0)` frees the cache; while the cache is active, the allocation callbacks can't be overridden, but if the cache is configured after `meshopt_setAllocator`, the cache itself and any allocations that don't fit will use the callbacks too.
988+
989+
> Note that the combination of allocator cache and `setjmp`/`longjmp` allocation error handling is not supported.
990+
981991
When building meshoptimizer as a shared library, allocations from the templated index wrappers provided in the header (used when index data is not `unsigned int`) will only be redirected to these callbacks if the library is built with `MESHOPTIMIZER_ALLOC_EXPORT` defined.
982992
983993
When building meshoptimizer with MSVC, if a non-default calling convention is used such as `__vectorcall` (via `/Gv`), `MESHOPTIMIZER_ALLOC_CALLCONV` should be defined to match the calling convention of `operator new`/`delete`.
@@ -1001,6 +1011,7 @@ Currently, the following APIs are experimental:
10011011
- `meshopt_SimplifyPreserveFolds` and `meshopt_SimplifyErrorClamped` flags for `meshopt_simplify*` functions
10021012
- `meshopt_generateNormals` function
10031013
- `meshopt_remesh` function and `meshopt_Remesh*` flags
1014+
- `meshopt_setAllocatorCache` function
10041015
10051016
## License
10061017

‎demo/nanite.cpp‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,11 @@ void nanite(const std::vector<Vertex>& vertices, const std::vector<unsigned int>
140140
float znear = 1e-2f;
141141
float proj = 1.f / tanf(fovy * 3.1415926f / 180.f * 0.5f);
142142

143+
// this is optional, but it can noticeably reduce page faults on windows for large meshes
144+
// for multi-threaded applications, set the first parameter to the expected concurrency; here we just need one thread
145+
// in practice, you'd do this *once* before processing all meshes of course, not per mesh; the call is not thread-safe
146+
meshopt_setAllocatorCache(1, 32 << 20);
147+
143148
clodBuild(config, mesh, [&](clodGroup group, const clodCluster* clusters, size_t cluster_count) -> int { // clang-format!
144149
if (stats.size() <= size_t(group.depth))
145150
stats.push_back({});
@@ -185,6 +190,8 @@ void nanite(const std::vector<Vertex>& vertices, const std::vector<unsigned int>
185190
return int(groups.size() - 1);
186191
});
187192

193+
meshopt_setAllocatorCache(0, 0);
194+
188195
// for cluster connectivity analysis and boundary statistics, we need a position-only remap that maps vertices with the same position to the same index
189196
std::vector<unsigned int> remap(vertices.size());
190197
meshopt_generatePositionRemap(&remap[0], &vertices[0].px, vertices.size(), sizeof(Vertex));

‎demo/tests.cpp‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1761,6 +1761,29 @@ static void customAllocator()
17611761
allocCount = freeCount = 0;
17621762
}
17631763

1764+
static void customAllocatorCache()
1765+
{
1766+
meshopt_setAllocator(customAlloc, customFree);
1767+
1768+
// cache memory is allocated using the callbacks
1769+
meshopt_setAllocatorCache(2, 48);
1770+
assert(allocCount == 1 && freeCount == 0);
1771+
1772+
float vb[] = {1, 0, 0, 0, 1, 0, 0, 0, 1};
1773+
unsigned int ib[] = {0, 1, 2};
1774+
1775+
// in-place vertex copy (36 bytes) fits into the block, remap table (12 bytes) doesn't fit after it
1776+
meshopt_optimizeVertexFetch(vb, ib, 3, vb, 3, 12);
1777+
assert(allocCount == 2 && freeCount == 1);
1778+
1779+
meshopt_setAllocatorCache(0, 0);
1780+
assert(allocCount == 2 && freeCount == 2);
1781+
1782+
meshopt_setAllocator(operator new, operator delete);
1783+
1784+
allocCount = freeCount = 0;
1785+
}
1786+
17641787
static void emptyMesh()
17651788
{
17661789
meshopt_optimizeVertexCache(NULL, NULL, 0, 0);
@@ -3652,6 +3675,7 @@ void runTests()
36523675
remapCustom();
36533676

36543677
customAllocator();
3678+
customAllocatorCache();
36553679

36563680
emptyMesh();
36573681

‎src/allocator.cpp‎

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,122 @@
22
#include "meshoptimizer.h"
33

44
#include <assert.h>
5+
#include <stdint.h>
6+
7+
// allocator cache requires std::atomic and thread_local for thread safety; if these are not available, meshopt_setAllocatorCache is not supported
8+
#if !defined(MESHOPTIMIZER_ALLOC_NOCACHE) && __cplusplus < 201103L && !(defined(_MSC_VER) && _MSC_VER >= 1900)
9+
#define MESHOPTIMIZER_ALLOC_NOCACHE
10+
#endif
11+
12+
#ifndef MESHOPTIMIZER_ALLOC_NOCACHE
13+
#include <atomic>
14+
#endif
15+
16+
#ifndef MESHOPTIMIZER_ALLOC_NOCACHE
17+
namespace meshopt
18+
{
19+
20+
struct GlobalCache
21+
{
22+
std::atomic<uint64_t> blocks;
23+
char padding[120]; // avoid false sharing between threads
24+
25+
void* data;
26+
size_t data_size;
27+
size_t block_size;
28+
uint64_t all_blocks;
29+
meshopt_Allocator::Storage fallback;
30+
};
31+
32+
struct LocalCache
33+
{
34+
void* block;
35+
size_t offset;
36+
uint64_t block_mask;
37+
};
38+
39+
alignas(128) static GlobalCache gCache = {};
40+
thread_local static LocalCache gCacheLocal;
41+
42+
static int cacheIndex(uint64_t mask)
43+
{
44+
int index = -1;
45+
while (mask)
46+
mask >>= 1, index++;
47+
return index;
48+
}
49+
50+
static void* MESHOPTIMIZER_ALLOC_CALLCONV cacheAllocate(size_t size)
51+
{
52+
GlobalCache& global = gCache;
53+
LocalCache& local = gCacheLocal;
54+
55+
// try to grab an available local block
56+
if (local.block == NULL && size < global.block_size && global.blocks.load(std::memory_order_relaxed) != 0)
57+
{
58+
uint64_t blocks = global.blocks.load(std::memory_order_relaxed);
59+
uint64_t mask = 0;
60+
61+
do
62+
{
63+
// prefer last index for coherency, but settle for lowest bit otherwise
64+
mask = (blocks & local.block_mask) ? local.block_mask : blocks & (0 - blocks);
65+
// no available block, unlikely to get one soon
66+
if (blocks == 0)
67+
break;
68+
// reloads blocks on failure
69+
} while (!global.blocks.compare_exchange_weak(blocks, blocks & ~mask, std::memory_order_acquire, std::memory_order_relaxed));
70+
71+
if (mask)
72+
{
73+
// extract block index from mask (must only have one bit set)
74+
int index = cacheIndex(mask);
75+
assert(mask == (1ull << index));
76+
77+
local.block = static_cast<char*>(global.data) + index * global.block_size;
78+
local.block_mask = mask;
79+
}
80+
}
81+
82+
// allocate from local block if any
83+
if (local.block && size < global.block_size && local.offset < global.block_size - size)
84+
{
85+
void* ptr = static_cast<char*>(local.block) + local.offset;
86+
local.offset += size ? size : 1;
87+
local.offset = (local.offset + 15) & ~size_t(15); // align future allocations to 16b
88+
return ptr;
89+
}
90+
91+
return global.fallback.allocate(size);
92+
}
93+
94+
static void MESHOPTIMIZER_ALLOC_CALLCONV cacheDeallocate(void* ptr)
95+
{
96+
GlobalCache& global = gCache;
97+
LocalCache& local = gCacheLocal;
98+
99+
// has our allocation come from the cache?
100+
if (global.data && ptr >= global.data && ptr < static_cast<char*>(global.data) + global.data_size)
101+
{
102+
// meshopt allocations are guaranteed to be stack ordered and thread local
103+
assert(local.block && ptr >= local.block && ptr < static_cast<char*>(local.block) + local.offset);
104+
local.offset = static_cast<char*>(ptr) - static_cast<char*>(local.block);
105+
106+
// return local block to the pool
107+
if (local.offset == 0)
108+
{
109+
assert(local.block_mask);
110+
global.blocks.fetch_or(local.block_mask, std::memory_order_release);
111+
local.block = NULL;
112+
// keep block_mask as an affinity hint for the next allocation
113+
}
114+
}
115+
else
116+
global.fallback.deallocate(ptr);
117+
}
118+
119+
} // namespace meshopt
120+
#endif
5121

6122
#ifdef MESHOPTIMIZER_ALLOC_EXPORT
7123
meshopt_Allocator::Storage& meshopt_Allocator::storage()
@@ -15,7 +131,62 @@ void meshopt_setAllocator(void* (MESHOPTIMIZER_ALLOC_CALLCONV* allocate)(size_t)
15131
{
16132
assert(allocate && deallocate);
17133

134+
#ifndef MESHOPTIMIZER_ALLOC_NOCACHE
135+
assert(!meshopt::gCache.data); // changing allocation callbacks is prohibited if the cache is already set up
136+
#endif
137+
18138
meshopt_Allocator::Storage& s = meshopt_Allocator::storage();
19139
s.allocate = allocate;
20140
s.deallocate = deallocate;
21141
}
142+
143+
#ifndef MESHOPTIMIZER_ALLOC_NOCACHE
144+
void meshopt_setAllocatorCache(size_t block_count, size_t block_size)
145+
{
146+
using namespace meshopt;
147+
148+
assert(block_count <= 64);
149+
assert(block_count == 0 || block_size >= 16);
150+
151+
meshopt_Allocator::Storage& allocator = meshopt_Allocator::storage();
152+
153+
// reset prior global state
154+
// note: all previously allocated blocks must have been returned at this point; this is guaranteed by the absence of concurrent execution with meshopt_ functions
155+
if (gCache.data)
156+
{
157+
assert(gCache.blocks.load() == gCache.all_blocks);
158+
gCache.fallback.deallocate(gCache.data);
159+
gCache.data = NULL;
160+
gCache.data_size = 0;
161+
gCache.block_size = 0;
162+
gCache.all_blocks = 0;
163+
gCache.blocks.store(0);
164+
165+
// reset global allocator
166+
allocator = gCache.fallback;
167+
}
168+
169+
// individual allocations are aligned to 16 bytes, so we only use a 16-byte-aligned subset
170+
block_size &= ~size_t(15);
171+
172+
// setup the cache if requested
173+
if (block_count && block_size)
174+
{
175+
size_t data_size = block_count > size_t(-1) / block_size ? size_t(-1) : block_count * block_size;
176+
177+
// allocate a block for each thread and mark each block as available
178+
gCache.data = allocator.allocate(data_size);
179+
if (!gCache.data)
180+
return;
181+
182+
gCache.data_size = data_size;
183+
gCache.block_size = block_size;
184+
gCache.all_blocks = (block_count >= 64) ? ~0ull : (1ull << block_count) - 1;
185+
gCache.blocks.store(gCache.all_blocks);
186+
gCache.fallback = allocator;
187+
188+
allocator.allocate = cacheAllocate;
189+
allocator.deallocate = cacheDeallocate;
190+
}
191+
}
192+
#endif

‎src/meshoptimizer.h‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,6 +1043,17 @@ MESHOPTIMIZER_API int meshopt_computePositionExponent(const float* minv, const f
10431043
*/
10441044
MESHOPTIMIZER_API void meshopt_setAllocator(void* (MESHOPTIMIZER_ALLOC_CALLCONV* allocate)(size_t), void (MESHOPTIMIZER_ALLOC_CALLCONV* deallocate)(void*));
10451045

1046+
/**
1047+
* Experimental: Enable allocation cache
1048+
* Allocates a fixed number of memory blocks to be used for temporary allocations in the library.
1049+
* Each thread performing temporary allocations can only use one block at a time; block count should be set according to expected concurrency.
1050+
* Once cache is setup, using meshopt_setAllocator is prohibited; cache needs to be disabled (using block_count=0) before reconfiguring allocation callbacks.
1051+
* This function is not thread safe and must not be called concurrently with any other meshopt_ function.
1052+
*
1053+
* block_count can be at most 64; when set to 0, the allocation cache is disabled.
1054+
*/
1055+
MESHOPTIMIZER_EXPERIMENTAL void meshopt_setAllocatorCache(size_t block_count, size_t block_size);
1056+
10461057
#ifdef __cplusplus
10471058
} /* extern "C" */
10481059
#endif

0 commit comments

Comments
 (0)