wip(standalone): paused by the owner -- hashed maps + inline allocator (no speedup yet), streaming in progress

Hashed TMap/TSet and real TInlineAllocator storage did not move level 0 (72.1 -> 74.1 s, within
noise): the slowness is elsewhere. StreamingWorld.h started (first build hit an int64/uint64
mismatch), library plan drafted (SHARED_CPP_LIBRARY_PLAN.md), before-normal captures saved.
Uncompiled/unmeasured parts: streaming. Resume by forking session 01a0fd2e-3c3b-7050-b4e9-628fdcc4d553.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-02 18:01:49 +02:00
1 parent 05f62cff36
commit 3f300b912a
8 files changed
+997 -100

No files matched your search

+296 -19
View File
@@ -19,7 +19,9 @@
#include <initializer_list>
#include <limits>
#include <memory>
#include <memory_resource>
#include <mutex>
#include <new>
#include <optional>
#include <shared_mutex>
#include <string>
@@ -88,13 +90,192 @@ template<class Signature> using TFunctionRef = std::function<Signature>;
template<class T> constexpr T&& Forward(std::remove_reference_t<T>& Value){return std::forward<T>(Value);}
template<class T> constexpr T&& Forward(std::remove_reference_t<T>&& Value){return std::forward<T>(Value);}
struct FCompatAllocationHeader
{
std::pmr::memory_resource* Pool=nullptr;
void* Base=nullptr;
std::size_t Bytes=0;
std::size_t Alignment=0;
};
inline std::pmr::memory_resource* FCompatThreadPool()
{
// A synchronized pool can return a worker's allocations from the result-drain thread. Keep
// one process-lifetime pool per allocating thread, matching Unreal's per-thread small bins.
thread_local auto* Pool=new std::pmr::synchronized_pool_resource();
return Pool;
}
template<class T>
class FCompatAllocator
{
public:
using value_type=T;
using is_always_equal=std::true_type;
using propagate_on_container_move_assignment=std::true_type;
FCompatAllocator() noexcept=default;
template<class U> FCompatAllocator(const FCompatAllocator<U>&) noexcept {}
[[nodiscard]] T* allocate(std::size_t Count)
{
if(Count>std::numeric_limits<std::size_t>::max()/sizeof(T))throw std::bad_array_new_length();
const std::size_t Bytes=Count*sizeof(T);
const std::size_t Alignment=std::max(alignof(T),alignof(FCompatAllocationHeader));
const std::size_t Total=Bytes+sizeof(FCompatAllocationHeader)+Alignment;
std::pmr::memory_resource* Pool=Bytes<=65536?FCompatThreadPool():nullptr;
void* Base=Pool?Pool->allocate(Total,Alignment): ::operator new(Total,std::align_val_t(Alignment));
void* Candidate=static_cast<std::byte*>(Base)+sizeof(FCompatAllocationHeader);
std::size_t Space=Total-sizeof(FCompatAllocationHeader);
void* Data=std::align(alignof(T),Bytes,Candidate,Space);
if(!Data)
{
if(Pool)Pool->deallocate(Base,Total,Alignment);
else ::operator delete(Base,std::align_val_t(Alignment));
throw std::bad_alloc();
}
auto* Header=reinterpret_cast<FCompatAllocationHeader*>(static_cast<std::byte*>(Data)-sizeof(FCompatAllocationHeader));
::new (Header) FCompatAllocationHeader{Pool,Base,Total,Alignment};
return static_cast<T*>(Data);
}
void deallocate(T* Data,std::size_t) noexcept
{
if(!Data)return;
auto* Header=reinterpret_cast<FCompatAllocationHeader*>(reinterpret_cast<std::byte*>(Data)-sizeof(FCompatAllocationHeader));
const FCompatAllocationHeader Allocation=*Header;
Header->~FCompatAllocationHeader();
if(Allocation.Pool)Allocation.Pool->deallocate(Allocation.Base,Allocation.Bytes,Allocation.Alignment);
else ::operator delete(Allocation.Base,std::align_val_t(Allocation.Alignment));
}
template<class U> bool operator==(const FCompatAllocator<U>&)const noexcept{return true;}
template<class U> bool operator!=(const FCompatAllocator<U>&)const noexcept{return false;}
};
template<class Allocator> struct FCompatInlineCapacity { static constexpr std::size_t Value=0; };
template<int32 N> struct FCompatInlineCapacity<TInlineAllocator<N>>
{ static constexpr std::size_t Value=static_cast<std::size_t>(N); };
template<class T,std::size_t InlineCount,class Allocator=FCompatAllocator<T>>
class FCompatSmallVector
{
public:
using value_type=T;
using iterator=T*;
using const_iterator=const T*;
using reference=T&;
using const_reference=const T&;
FCompatSmallVector()=default;
FCompatSmallVector(const FCompatSmallVector& Other){assign(Other.begin(),Other.end());}
FCompatSmallVector(FCompatSmallVector&& Other) noexcept
{
if(Other.bHeap){Heap=std::move(Other.Heap);bHeap=true;Other.bHeap=false;Other.InlineSize=0;}
else for(T& Value:Other)emplace_back(std::move(Value));
Other.clear();
}
FCompatSmallVector& operator=(const FCompatSmallVector& Other)
{if(this!=&Other)assign(Other.begin(),Other.end());return *this;}
FCompatSmallVector& operator=(FCompatSmallVector&& Other) noexcept
{
if(this==&Other)return *this;
clear();
if(Other.bHeap){Heap=std::move(Other.Heap);bHeap=true;Other.bHeap=false;Other.InlineSize=0;}
else for(T& Value:Other)emplace_back(std::move(Value));
Other.clear();return *this;
}
~FCompatSmallVector(){clear();}
T* data(){return bHeap?Heap.data():InlineData();}
const T* data()const{return bHeap?Heap.data():InlineData();}
std::size_t size()const{return bHeap?Heap.size():InlineSize;}
bool empty()const{return size()==0;}
std::size_t capacity()const{return bHeap?Heap.capacity():InlineCount;}
std::size_t heap_capacity()const{return bHeap?Heap.capacity():0;}
iterator begin(){return data();} iterator end(){return data()+size();}
const_iterator begin()const{return data();} const_iterator end()const{return data()+size();}
reference operator[](std::size_t I){return data()[I];}
const_reference operator[](std::size_t I)const{return data()[I];}
reference back(){return data()[size()-1];} const_reference back()const{return data()[size()-1];}
void reserve(std::size_t Count)
{
if(Count<=capacity())return;
if(bHeap){Heap.reserve(Count);return;}
std::vector<T,Allocator> NewHeap;NewHeap.reserve(Count);
for(std::size_t I=0;I<InlineSize;++I)NewHeap.emplace_back(std::move(InlineData()[I]));
DestroyInline();Heap=std::move(NewHeap);InlineSize=0;bHeap=true;
}
void clear()noexcept
{if(bHeap)Heap.clear();else DestroyInline();}
void resize(std::size_t Count)
{
const std::size_t Old=size();
if(Count<Old)
{
if(bHeap)Heap.resize(Count);
else{while(InlineSize>Count)InlineData()[--InlineSize].~T();}
return;
}
reserve(Count);
while(size()<Count)emplace_back();
}
void assign(std::size_t Count,const T& Value)
{clear();reserve(Count);for(std::size_t I=0;I<Count;++I)emplace_back(Value);}
template<class It> void assign(It First,It Last)
{clear();for(;First!=Last;++First)emplace_back(*First);}
void push_back(const T& Value){emplace_back(Value);}
void push_back(T&& Value){emplace_back(std::move(Value));}
template<class... Args> reference emplace_back(Args&&... ArgsIn)
{
if(bHeap){Heap.emplace_back(std::forward<Args>(ArgsIn)...);return Heap.back();}
if(InlineSize<InlineCount)
{
T* Result=::new (static_cast<void*>(InlineData()+InlineSize)) T(std::forward<Args>(ArgsIn)...);
++InlineSize;return *Result;
}
reserve(std::max<std::size_t>(InlineCount?InlineCount*2:1,InlineSize+1));
Heap.emplace_back(std::forward<Args>(ArgsIn)...);return Heap.back();
}
void pop_back(){if(bHeap)Heap.pop_back();else InlineData()[--InlineSize].~T();}
template<class It> iterator insert(iterator Position,It First,It Last)
{
const std::size_t Offset=static_cast<std::size_t>(Position-begin());
assert(Offset==size());
for(;First!=Last;++First)emplace_back(*First);
return begin()+Offset;
}
iterator erase(iterator First,iterator Last)
{
const std::size_t Start=static_cast<std::size_t>(First-begin());
const std::size_t Finish=static_cast<std::size_t>(Last-begin());
const std::size_t Old=size();
if(Finish<=Start)return begin()+Start;
const std::size_t Removed=Finish-Start;
for(std::size_t I=Finish;I<Old;++I)data()[I-Removed]=std::move(data()[I]);
if(bHeap)Heap.resize(Old-Removed);
else while(InlineSize>Old-Removed)InlineData()[--InlineSize].~T();
return begin()+Start;
}
private:
T* InlineData(){return reinterpret_cast<T*>(InlineBytes);}
const T* InlineData()const{return reinterpret_cast<const T*>(InlineBytes);}
void DestroyInline()noexcept{while(InlineSize>0)InlineData()[--InlineSize].~T();}
alignas(T) std::byte InlineBytes[(InlineCount?InlineCount:1)*sizeof(T)]{};
std::vector<T,Allocator> Heap;
std::size_t InlineSize=0;
bool bHeap=false;
};
template<class T, class Allocator = FDefaultAllocator>
class TArray
{
public:
using value_type = T;
using FStored = std::conditional_t<std::is_same_v<T,bool>,uint8,T>;
using FStorage = std::vector<FStored>;
using FStorage = FCompatSmallVector<FStored,FCompatInlineCapacity<Allocator>::Value,FCompatAllocator<FStored>>;
using iterator = typename FStorage::iterator;
using const_iterator = typename FStorage::const_iterator;
using reference = typename FStorage::reference;
@@ -133,7 +314,7 @@ public:
const T* Find(const T& Value) const { const int32 I=IndexOfByKey(Value); return I==INDEX_NONE?nullptr:&Values[I]; }
T* GetData() { return reinterpret_cast<T*>(Values.data()); }
const T* GetData() const { return reinterpret_cast<const T*>(Values.data()); }
std::size_t GetAllocatedSize() const { return Values.capacity() * sizeof(T); }
std::size_t GetAllocatedSize() const { return Values.heap_capacity() * sizeof(T); }
T& Last() { return Values.back(); }
const T& Last() const { return Values.back(); }
T Pop(bool = false) { T Out=std::move(Values.back()); Values.pop_back(); return Out; }
@@ -151,42 +332,133 @@ private:
template<class K, class V> struct TPair { K Key{}; V Value{}; TPair()=default; TPair(const K& K0,const V& V0):Key(K0),Value(V0){}; };
template<class K>
struct FCompatKeyHash
{
std::size_t operator()(const K& Key) const noexcept
{
if constexpr (std::is_integral_v<K> || std::is_enum_v<K>)
return std::hash<K>{}(Key);
else if constexpr (requires { GetTypeHash(Key); })
return static_cast<std::size_t>(GetTypeHash(Key));
else
static_assert(sizeof(K) == 0, "Standalone TMap/TSet key type needs GetTypeHash");
}
};
template<class K, class V, class Allocator = FDefaultAllocator>
class TMap
{
public:
int32 Num() const { return static_cast<int32>(Pairs.size()); }
bool IsEmpty() const { return Pairs.empty(); }
void Reserve(int32 N) { Pairs.reserve(static_cast<std::size_t>(std::max(0,N))); }
void Reset() { Pairs.clear(); }
void Reserve(int32 N)
{
const size_t Count=static_cast<size_t>(std::max(0,N));
Pairs.reserve(Count);
const size_t Needed=std::max<size_t>(8,std::bit_ceil(std::max<size_t>(1,(Count*10+6)/7)));
if(Buckets.size()<Needed)RebuildIndex(Needed);
}
void Reset()
{
Pairs.clear();Tombstones=0;
if(++Generation==0){for(FBucket& B:Buckets)B.Generation=0;Generation=1;}
}
void Empty(int32 = 0) { Reset(); }
bool Contains(const K& Key) const { return FindPair(Key) != Pairs.end(); }
V* Find(const K& Key) { auto I=FindPair(Key); return I==Pairs.end()?nullptr:&I->Value; }
const V* Find(const K& Key) const { auto I=FindPair(Key); return I==Pairs.end()?nullptr:&I->Value; }
V& FindOrAdd(const K& Key) { if (V* P=Find(Key)) return *P; Pairs.emplace_back(Key,V{}); return Pairs.back().Value; }
V& Add(const K& Key,const V& Value) { if (V* P=Find(Key)) { *P=Value; return *P; } Pairs.emplace_back(Key,Value); return Pairs.back().Value; }
V& Add(const K& Key,V&& Value) { if (V* P=Find(Key)) { *P=std::move(Value); return *P; } Pairs.emplace_back(Key,std::move(Value)); return Pairs.back().Value; }
int32 Remove(const K& Key) { auto I=FindPair(Key); if(I==Pairs.end())return 0; Pairs.erase(I);return 1; }
bool Contains(const K& Key) const { return FindIndex(Key)!=INDEX_NONE; }
V* Find(const K& Key) { const int32 I=FindIndex(Key);return I==INDEX_NONE?nullptr:&Pairs[static_cast<size_t>(I)].Value; }
const V* Find(const K& Key) const { const int32 I=FindIndex(Key);return I==INDEX_NONE?nullptr:&Pairs[static_cast<size_t>(I)].Value; }
V& FindOrAdd(const K& Key) { if (V* P=Find(Key)) return *P; return Add(Key,V{}); }
V& Add(const K& Key,const V& Value) { if (V* P=Find(Key)) { *P=Value; return *P; } PrepareInsert();const int32 I=static_cast<int32>(Pairs.size());Pairs.emplace_back(Key,Value);InsertIndex(Key,I);return Pairs.back().Value; }
V& Add(const K& Key,V&& Value) { if (V* P=Find(Key)) { *P=std::move(Value); return *P; } PrepareInsert();const int32 I=static_cast<int32>(Pairs.size());Pairs.emplace_back(Key,std::move(Value));InsertIndex(Key,I);return Pairs.back().Value; }
int32 Remove(const K& Key)
{
const int32 I=FindIndex(Key);if(I==INDEX_NONE)return 0;
MarkRemoved(Key);const int32 Last=static_cast<int32>(Pairs.size()-1);
if(I!=Last){Pairs[static_cast<size_t>(I)]=std::move(Pairs.back());UpdateIndex(Pairs[static_cast<size_t>(I)].Key,I);}
Pairs.pop_back();
if(Tombstones>Pairs.size()&&Tombstones>Buckets.size()/8)RebuildIndex(Buckets.size());
return 1;
}
V& operator[](const K& Key) { return FindOrAdd(Key); }
const V& operator[](const K& Key) const { const V* P=Find(Key); assert(P); return *P; }
auto begin(){return Pairs.begin();} auto end(){return Pairs.end();}
auto begin()const{return Pairs.begin();} auto end()const{return Pairs.end();}
private:
using It=typename std::vector<TPair<K,V>>::iterator;
using CIt=typename std::vector<TPair<K,V>>::const_iterator;
It FindPair(const K& Key){return std::find_if(Pairs.begin(),Pairs.end(),[&](auto& P){return P.Key==Key;});}
CIt FindPair(const K& Key)const{return std::find_if(Pairs.begin(),Pairs.end(),[&](auto& P){return P.Key==Key;});}
std::vector<TPair<K,V>> Pairs;
struct FBucket { uint32 Generation=0;int32 Index=INDEX_NONE; };
void PrepareInsert()
{
const size_t Count=Pairs.size()+1;
const size_t Needed=std::max<size_t>(8,std::bit_ceil(std::max<size_t>(1,(Count*10+6)/7)));
if(Buckets.size()<Needed||Tombstones>Buckets.size()/8)RebuildIndex(std::max(Needed,Buckets.size()));
}
int32 FindIndex(const K& Key)const
{
if(Buckets.empty())return INDEX_NONE;
size_t Slot=FCompatKeyHash<K>{}(Key)&(Buckets.size()-1);
for(size_t Probe=0;Probe<Buckets.size();++Probe)
{
const FBucket& B=Buckets[Slot];
if(B.Generation!=Generation)return INDEX_NONE;
if(B.Index>=0&&Pairs[static_cast<size_t>(B.Index)].Key==Key)return B.Index;
Slot=(Slot+1)&(Buckets.size()-1);
}
return INDEX_NONE;
}
void InsertIndex(const K& Key,int32 PairIndex)
{
size_t Slot=FCompatKeyHash<K>{}(Key)&(Buckets.size()-1),FirstTombstone=Buckets.size();
for(size_t Probe=0;Probe<Buckets.size();++Probe)
{
FBucket& B=Buckets[Slot];
if(B.Generation!=Generation){if(FirstTombstone!=Buckets.size())Slot=FirstTombstone;Buckets[Slot]={Generation,PairIndex};return;}
if(B.Index==INDEX_NONE&&FirstTombstone==Buckets.size())FirstTombstone=Slot;
Slot=(Slot+1)&(Buckets.size()-1);
}
assert(FirstTombstone!=Buckets.size());Buckets[FirstTombstone]={Generation,PairIndex};
}
void MarkRemoved(const K& Key)
{
size_t Slot=FCompatKeyHash<K>{}(Key)&(Buckets.size()-1);
for(size_t Probe=0;Probe<Buckets.size();++Probe)
{
FBucket& B=Buckets[Slot];if(B.Generation!=Generation)return;
if(B.Index>=0&&Pairs[static_cast<size_t>(B.Index)].Key==Key){B.Index=INDEX_NONE;++Tombstones;return;}
Slot=(Slot+1)&(Buckets.size()-1);
}
}
void UpdateIndex(const K& Key,int32 PairIndex)
{
size_t Slot=FCompatKeyHash<K>{}(Key)&(Buckets.size()-1);
for(size_t Probe=0;Probe<Buckets.size();++Probe)
{
FBucket& B=Buckets[Slot];if(B.Generation!=Generation)return;
if(B.Index>=0&&Pairs[static_cast<size_t>(B.Index)].Key==Key){B.Index=PairIndex;return;}
Slot=(Slot+1)&(Buckets.size()-1);
}
}
void RebuildIndex(size_t Capacity)
{
Capacity=std::max<size_t>(8,std::bit_ceil(Capacity));Buckets.assign(Capacity,FBucket{});Generation=1;Tombstones=0;
for(int32 I=0;I<static_cast<int32>(Pairs.size());++I)InsertIndex(Pairs[static_cast<size_t>(I)].Key,I);
}
std::vector<TPair<K,V>,FCompatAllocator<TPair<K,V>>> Pairs;
std::vector<FBucket,FCompatAllocator<FBucket>> Buckets;
uint32 Generation=1;
size_t Tombstones=0;
};
template<class T, class Allocator = FDefaultAllocator>
class TSet
{
public:
int32 Num()const{return Values.Num();} void Reset(){Values.Reset();} void Empty(){Reset();}
bool Contains(const T& V)const{return Values.Contains(V);} void Add(const T& V){Values.AddUnique(V);} void Add(T&& V){if(!Contains(V))Values.Add(std::move(V));}
int32 Num()const{return Values.Num();} void Reset(){Values.Reset();Indices.Reset();} void Empty(){Reset();}
void Reserve(int32 N){Values.Reserve(N);Indices.Reserve(N);}
bool Contains(const T& V)const{return Indices.Contains(V);}
void Add(const T& V){if(!Indices.Contains(V)){const int32 I=Values.Add(V);Indices.Add(V,I);}}
void Add(T&& V){if(!Indices.Contains(V)){const int32 I=Values.Add(std::move(V));Indices.Add(Values[I],I);}}
auto begin(){return Values.begin();}auto end(){return Values.end();}auto begin()const{return Values.begin();}auto end()const{return Values.end();}
private:TArray<T,Allocator> Values;
private:TArray<T,Allocator> Values;TMap<T,int32,Allocator> Indices;
};
template<class T> class TUniquePtr
@@ -225,6 +497,11 @@ public:
static FString Join(const TArray<FString>& Parts, std::string_view Delimiter){FString Result;for(int32 I=0;I<Parts.Num();++I){if(I)Result+=Delimiter;Result+=Parts[I];}return Result;}
static FString Printf(const char* Format,...){char Buf[4096];va_list A;va_start(A,Format);int N=std::vsnprintf(Buf,sizeof(Buf),Format,A);va_end(A);return N<0?FString{}:FString(Buf,static_cast<std::size_t>(std::min<int>(N,sizeof(Buf)-1)));}
};
inline uint32 GetTypeHash(const FString& Value)
{
const std::string_view View(Value.data(),Value.size());
return static_cast<uint32>(std::hash<std::string_view>{}(View));
}
inline const char* operator*(const FString& S){return S.c_str();}
using FName = FString;
constexpr const char* NAME_None = "";