wip(assembler): round L4 -- lazy per-thread caches: 17.3 MiB -> 43 KiB per thread, view 7% faster

VoxelForgeCoreThreadLocal::FLazy<T> (new public header ThreadLocalStorage.h) makes every large
thread_local cache a pointer that heap-allocates on that thread's first use (still worker-private,
lock-free). Static TLS 17.33 MiB -> 43 KiB (initialised image 5.8 MiB -> 21 KiB); worker start
5.2 -> 0.12 ms; idle worker RSS 17.7 MiB -> 52 KiB. Linux workers back to 8 MiB stacks.
World unchanged (L1 hashes Clang off/GCC default, W1/W12; FloatingIslands seed 0).
Reviewer interleaved W12: 2.256 -> 2.091 s. MSVC/UBT not compiled here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ED33VnqepD4YY6gDAAA832
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-19 19:16:14 +02:00
1 parent 046f2af495
commit 7770074503
15 files changed
+361 -48

No files matched your search

@@ -0,0 +1,33 @@
#pragma once
// Keep large worker caches out of each thread's eager ELF TLS image. Each handle is itself
// thread-local; its payload is allocated on the first use by that thread and released when the
// thread exits. The payload is never shared and this type has no synchronization.
namespace VoxelForgeCoreThreadLocal
{
template <typename T>
class FLazy
{
public:
FLazy() noexcept = default;
FLazy(const FLazy&) = delete;
FLazy& operator=(const FLazy&) = delete;
~FLazy()
{
delete Value;
}
T& Get()
{
if (Value == nullptr)
{
Value = new T{};
}
return *Value;
}
private:
T* Value = nullptr;
};
}
@@ -54,6 +54,7 @@
// value tree shared lets the viewer resolve its reference world and Core snapshot from one parse.
#include "Json.h"
#include "VoxelForgeCore/VoxelForgeCore.h"
#include "VoxelForgeCore/ThreadLocalStorage.h"
// Keep Core's expression-level FMA behavior fixed regardless of the toolchain setting.
// Clang's "fast" mode ignores the pragma, so Core's runtime self-test rejects that build.
+10 -5
View File
@@ -1,5 +1,6 @@
#pragma once
#include "VoxelForgeCore/ThreadLocalStorage.h"
#include "Json.h"
#include "VoxelForgeCore/VoxelForgeCore.h"
@@ -441,7 +442,9 @@ namespace floating_island_detail
constexpr std::size_t Ways=4;
constexpr std::size_t SetCount=2048;
static_assert((SetCount&(SetCount-1))==0);
static thread_local std::array<FIslandColumnCacheEntry,SetCount*Ways> Cache{};
static thread_local VoxelForgeCoreThreadLocal::FLazy<
std::array<FIslandColumnCacheEntry,SetCount*Ways>> Cache;
auto& cache=Cache.Get();
const std::int64_t SampleX=static_cast<std::int64_t>(x);
const std::int64_t SampleY=static_cast<std::int64_t>(y);
std::uint64_t IndexKey=island.stableId
@@ -452,10 +455,10 @@ namespace floating_island_detail
IndexKey*=0xff51afd7ed558ccdull;
IndexKey^=IndexKey>>33;
const std::size_t Base=static_cast<std::size_t>(IndexKey&(SetCount-1))*Ways;
FIslandColumnCacheEntry* Replacement=&Cache[Base+((IndexKey>>11)&(Ways-1))];
FIslandColumnCacheEntry* Replacement=&cache[Base+((IndexKey>>11)&(Ways-1))];
for(std::size_t Way=0;Way<Ways;++Way)
{
FIslandColumnCacheEntry& Entry=Cache[Base+Way];
FIslandColumnCacheEntry& Entry=cache[Base+Way];
const bool bKeyMatches=Entry.bValid&&Entry.Seed==recipe.seed
&&Entry.StableId==island.stableId
&&Entry.SampleX==SampleX&&Entry.SampleY==SampleY
@@ -559,11 +562,13 @@ namespace floating_island_detail
// Mettre en cache les constantes par worker; la cle complete evite tout partage d'etat.
constexpr std::size_t CacheSize=1024;
static_assert((CacheSize&(CacheSize-1))==0);
static thread_local std::array<FRootLobeCacheEntry,CacheSize> Cache{};
static thread_local VoxelForgeCoreThreadLocal::FLazy<
std::array<FRootLobeCacheEntry,CacheSize>> Cache;
auto& cache=Cache.Get();
const std::uint64_t IndexKey=island.stableId^(island.stableId>>33)
^(static_cast<std::uint64_t>(recipe.seed)<<17)
^(static_cast<std::uint64_t>(static_cast<std::uint32_t>(rootIndex))*0x9e3779b97f4a7c15ull);
FRootLobeCacheEntry& Entry=Cache[static_cast<std::size_t>(IndexKey)&(CacheSize-1)];
FRootLobeCacheEntry& Entry=cache[static_cast<std::size_t>(IndexKey)&(CacheSize-1)];
const bool bKeyMatches=Entry.bValid&&Entry.Seed==recipe.seed
&&Entry.StableId==island.stableId&&Entry.RootIndex==rootIndex
&&Entry.GeometryVersion==recipe.geometryVersion
+14 -11
View File
@@ -1,5 +1,6 @@
#pragma once
#include "VoxelForgeCore/ThreadLocalStorage.h"
#include "Json.h"
#include "VoxelForgeCore/VoxelForgeCore.h"
@@ -1367,9 +1368,10 @@ namespace world_assembly_detail
// qui partagent les bits faibles du hash.
constexpr std::size_t HeightfieldColumnCacheWays=4;
constexpr std::size_t HeightfieldColumnCacheSetCount=65536/HeightfieldColumnCacheWays;
inline thread_local std::array<HeightfieldColumnCacheEntry, 65536> HeightfieldColumnCache{};
inline thread_local std::array<std::uint8_t, HeightfieldColumnCacheSetCount>
HeightfieldColumnCacheNextWay{};
inline thread_local VoxelForgeCoreThreadLocal::FLazy<
std::array<HeightfieldColumnCacheEntry, 65536>> HeightfieldColumnCache;
inline thread_local VoxelForgeCoreThreadLocal::FLazy<
std::array<std::uint8_t, HeightfieldColumnCacheSetCount>> HeightfieldColumnCacheNextWay;
inline thread_local HeightfieldColumnCacheStats HeightfieldColumnStats{};
inline thread_local bool HeightfieldColumnCacheLegacyDirectMapped=false;
@@ -1504,6 +1506,8 @@ namespace world_assembly_detail
inline HeightfieldPointSample CachedColumn(const WorldAssemblyRecipeV1& recipe,
const WorldAssemblyStratumPlanV1& plan,std::int64_t x,std::int64_t y)
{
auto& cache=HeightfieldColumnCache.Get();
auto& nextWay=HeightfieldColumnCacheNextWay.Get();
HeightfieldColumnCacheEntry* replacement=nullptr;
if(HeightfieldColumnCacheLegacyDirectMapped)
{
@@ -1513,8 +1517,7 @@ namespace world_assembly_detail
^std::rotl(static_cast<std::uint64_t>(y)*0xC2B2AE3D27D4EB4Full,23)
^static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(&recipe))
^std::rotl(static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(&plan)),41);
replacement=&HeightfieldColumnCache[static_cast<std::size_t>(mixed)
&(HeightfieldColumnCache.size()-1)];
replacement=&cache[static_cast<std::size_t>(mixed)&(cache.size()-1)];
if(replacement->recipe==&recipe&&replacement->plan==&plan
&&replacement->recipeHash==recipe.contentHash
&&replacement->planHash==plan.contentHash&&replacement->x==x&&replacement->y==y)
@@ -1528,7 +1531,7 @@ namespace world_assembly_detail
std::size_t replacementWay=HeightfieldColumnCacheWays;
for(std::size_t way=0;way<HeightfieldColumnCacheWays;++way)
{
auto& candidate=HeightfieldColumnCache[first+way];
auto& candidate=cache[first+way];
if(candidate.recipe==&recipe&&candidate.plan==&plan
&&candidate.recipeHash==recipe.contentHash&&candidate.planHash==plan.contentHash
&&candidate.x==x&&candidate.y==y)
@@ -1539,11 +1542,11 @@ namespace world_assembly_detail
if(replacementWay==HeightfieldColumnCacheWays)
{
++HeightfieldColumnStats.columnConflictMisses;
replacementWay=HeightfieldColumnCacheNextWay[set];
replacementWay=nextWay[set];
}
HeightfieldColumnCacheNextWay[set]=static_cast<std::uint8_t>(
nextWay[set]=static_cast<std::uint8_t>(
(replacementWay+1)%HeightfieldColumnCacheWays);
replacement=&HeightfieldColumnCache[first+replacementWay];
replacement=&cache[first+replacementWay];
}
auto& entry=*replacement;
const auto start=std::chrono::steady_clock::now();
@@ -2036,9 +2039,9 @@ void Heightfield2D::resetThreadColumnCacheStats(bool captureCoordinates, bool cl
// Les sondes de mesure peuvent vider les entrees d'une tuile avant de comparer deux grilles.
if(clearCache)
{
world_assembly_detail::HeightfieldColumnCache.fill(
world_assembly_detail::HeightfieldColumnCache.Get().fill(
world_assembly_detail::HeightfieldColumnCacheEntry{});
world_assembly_detail::HeightfieldColumnCacheNextWay.fill(0);
world_assembly_detail::HeightfieldColumnCacheNextWay.Get().fill(0);
}
auto& Stats=world_assembly_detail::HeightfieldColumnStats;
world_assembly_detail::HeightfieldColumnOctaveDrop=octaveDrop;