├── .gitignore ├── Arena.cs ├── ArenaDict.cs ├── ArenaID.cs ├── ArenaList.cs ├── ArenaListExtensions.cs ├── ArenaPool.cs ├── ArenaString.cs ├── ArenaStringExtensions.cs ├── Arenas.csproj ├── Arenas.sln ├── ArenasTest ├── App.config ├── ArenasTest.csproj ├── Person.cs ├── Program.cs └── Properties │ └── AssemblyInfo.cs ├── ArenasTestCore ├── ArenasTestCore.csproj └── Program.cs ├── BitpackedPtr.cs ├── CollectionExtensions.cs ├── IArenaContents.cs ├── IMemoryAllocator.cs ├── IUnmanagedRef.cs ├── ItemVersion.cs ├── LICENSE ├── ManagedObjectPool.cs ├── ManagedRef.cs ├── MemHelper.cs ├── README.md ├── RefVersion.cs ├── TypeHandle.cs ├── TypeInfo.cs ├── UnmanagedRef.cs └── UnmanagedRefDebugView.cs /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.tlog 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 298 | *.vbp 299 | 300 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 301 | *.dsw 302 | *.dsp 303 | 304 | # Visual Studio 6 technical files 305 | *.ncb 306 | *.aps 307 | 308 | # Visual Studio LightSwitch build output 309 | **/*.HTMLClient/GeneratedArtifacts 310 | **/*.DesktopClient/GeneratedArtifacts 311 | **/*.DesktopClient/ModelManifest.xml 312 | **/*.Server/GeneratedArtifacts 313 | **/*.Server/ModelManifest.xml 314 | _Pvt_Extensions 315 | 316 | # Paket dependency manager 317 | .paket/paket.exe 318 | paket-files/ 319 | 320 | # FAKE - F# Make 321 | .fake/ 322 | 323 | # CodeRush personal settings 324 | .cr/personal 325 | 326 | # Python Tools for Visual Studio (PTVS) 327 | __pycache__/ 328 | *.pyc 329 | 330 | # Cake - Uncomment if you are using it 331 | # tools/** 332 | # !tools/packages.config 333 | 334 | # Tabs Studio 335 | *.tss 336 | 337 | # Telerik's JustMock configuration file 338 | *.jmconfig 339 | 340 | # BizTalk build output 341 | *.btp.cs 342 | *.btm.cs 343 | *.odx.cs 344 | *.xsd.cs 345 | 346 | # OpenCover UI analysis results 347 | OpenCover/ 348 | 349 | # Azure Stream Analytics local run output 350 | ASALocalRun/ 351 | 352 | # MSBuild Binary and Structured Log 353 | *.binlog 354 | 355 | # NVidia Nsight GPU debugger configuration file 356 | *.nvuser 357 | 358 | # MFractors (Xamarin productivity tool) working folder 359 | .mfractor/ 360 | 361 | # Local History for Visual Studio 362 | .localhistory/ 363 | 364 | # Visual Studio History (VSHistory) files 365 | .vshistory/ 366 | 367 | # BeatPulse healthcheck temp database 368 | healthchecksdb 369 | 370 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 371 | MigrationBackup/ 372 | 373 | # Ionide (cross platform F# VS Code tools) working folder 374 | .ionide/ 375 | 376 | # Fody - auto-generated XML schema 377 | FodyWeavers.xsd 378 | 379 | # VS Code files for those working on multiple tools 380 | .vscode/* 381 | !.vscode/settings.json 382 | !.vscode/tasks.json 383 | !.vscode/launch.json 384 | !.vscode/extensions.json 385 | *.code-workspace 386 | 387 | # Local History for Visual Studio Code 388 | .history/ 389 | 390 | # Windows Installer files from build outputs 391 | *.cab 392 | *.msi 393 | *.msix 394 | *.msm 395 | *.msp 396 | 397 | # JetBrains Rider 398 | *.sln.iml 399 | -------------------------------------------------------------------------------- /Arena.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Concurrent; 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | using System.Drawing; 7 | using System.Linq; 8 | using System.Runtime.CompilerServices; 9 | using System.Runtime.InteropServices; 10 | using static Arenas.TypeHandle; 11 | using static Arenas.TypeInfo; 12 | 13 | namespace Arenas { 14 | // NOTE: all items allocated to the arena will be aligned to a 64-bit word boundary and their size will be a multiple of 64-bits 15 | public unsafe class Arena : IDisposable, IEnumerable, IArenaPoolable { 16 | public const int DefaultPageSize = 4096; 17 | public const int MinimumPageSize = 256; 18 | 19 | private Dictionary objToPtr; 20 | private bool disposedValue; 21 | private bool finalized; 22 | private List pages; 23 | private Dictionary freelists; 24 | private ArenaID id; 25 | private int enumVersion; 26 | private bool initialized; 27 | private int pageSize; 28 | 29 | public Arena(int pageSize = DefaultPageSize) 30 | : this(DefaultAllocator, pageSize) { 31 | } 32 | 33 | public Arena(IMemoryAllocator allocator, int pageSize = DefaultPageSize) { 34 | if (pageSize < MinimumPageSize) { 35 | throw new ArgumentOutOfRangeException(nameof(pageSize), $"Arena page size must be greater than or equal to {MinimumPageSize} bytes"); 36 | } 37 | if (!MemHelper.IsPowerOfTwo((uint)pageSize)) { 38 | throw new ArgumentOutOfRangeException(nameof(pageSize), $"Arena page size must be a power of two"); 39 | } 40 | 41 | this.pageSize = pageSize; 42 | Allocator = allocator ?? throw new ArgumentNullException(nameof(allocator)); 43 | Init(); 44 | 45 | // call clear to set up everything we need for use 46 | Clear(false); 47 | } 48 | 49 | private void Init() { 50 | initialized = false; 51 | 52 | objToPtr = objToPtr ?? new Dictionary(ObjectReferenceEqualityComparer.Instance); 53 | pages = pages ?? new List(); 54 | freelists = freelists ?? new Dictionary(); 55 | Allocator = Allocator ?? DefaultAllocator; 56 | } 57 | 58 | public UnmanagedRef UnmanagedRefFromPtr(T* ptr) where T : unmanaged { 59 | return UnmanagedRefFromPtr((IntPtr)ptr); 60 | } 61 | 62 | public UnmanagedRef UnmanagedRefFromPtr(IntPtr ptr) where T : unmanaged { 63 | if (ptr == IntPtr.Zero) { 64 | throw new ArgumentNullException(nameof(ptr)); 65 | } 66 | 67 | if (id == ArenaID.Empty) { 68 | throw new InvalidOperationException("Pointer in UnmanagedRefFromPtr(IntPtr) did not point to a valid item."); 69 | } 70 | 71 | var header = ItemHeader.GetHeader(ptr); 72 | 73 | Type type; 74 | if (!TryGetTypeFromHandle(header.TypeHandle, out type)) { 75 | throw new InvalidOperationException("Type mismatch in header for pointer in UnmanagedRefFromPtr(IntPtr), address may be invalid."); 76 | } 77 | if (type != typeof(T)) { 78 | throw new InvalidOperationException($"Type mismatch in header for pointer in UnmanagedRefFromPtr(IntPtr), types do not match: type {typeof(T)} expected, but item was of type {type}."); 79 | } 80 | 81 | if (header.Version.Arena != id || !header.Version.Item.Valid) { 82 | throw new InvalidOperationException("Pointer in UnmanagedRefFromPtr(IntPtr) did not point to a valid item."); 83 | } 84 | 85 | return new UnmanagedRef((T*)ptr, header.Version, header.Size / sizeof(T)); 86 | } 87 | 88 | public UnmanagedRef UnmanagedRefFromPtr(IntPtr ptr) { 89 | if (ptr == IntPtr.Zero) { 90 | throw new ArgumentNullException(nameof(ptr)); 91 | } 92 | 93 | if (id == ArenaID.Empty) { 94 | throw new InvalidOperationException("Pointer in UnmanagedRefFromPtr(IntPtr) did not point to a valid item."); 95 | } 96 | 97 | var header = ItemHeader.GetHeader(ptr); 98 | 99 | TypeInfo info; 100 | if (!TryGetTypeInfo(header.TypeHandle, out info)) { 101 | throw new InvalidOperationException("Invalid type in header for pointer in UnmanagedRefFromPtr(IntPtr), address may be invalid."); 102 | } 103 | 104 | if (header.Version.Arena != id || !header.Version.Item.Valid) { 105 | throw new InvalidOperationException("Pointer in UnmanagedRefFromPtr(IntPtr) did not point to a valid item."); 106 | } 107 | 108 | return new UnmanagedRef(ptr, header.Version, header.Size / info.Size); 109 | } 110 | 111 | private Page AllocPage(int size) { 112 | // add one 64-bit word in size to make sure this can always be 64-bit aligned 113 | // while keeping the original size (unused space is available if already aligned) 114 | size += sizeof(ulong); 115 | size = MemHelper.AlignCeil(size, pageSize); 116 | 117 | // allocate pointer, potentially unaligned to 64-bit word 118 | var alloc = Allocator.Allocate(size); 119 | 120 | var mem = alloc.Pointer; 121 | var actualSize = MemHelper.AlignFloor(alloc.SizeBytes, sizeof(ulong)); 122 | Debug.Assert(actualSize >= size); 123 | 124 | MemHelper.ZeroMemory(mem, (UIntPtr)alloc.SizeBytes); 125 | 126 | // store the original pointer for freeing, and find 64-bit word align position 127 | var freePtr = mem; 128 | var aligned = MemHelper.AlignCeil(mem, sizeof(ulong)); 129 | 130 | // page memory must be 64-bit word aligned to keep 64-bit word alignment for all items 131 | // allocated to the arena 132 | if (mem != aligned) { 133 | // memory is misaligned, realign and reduce size accordingly 134 | var sizeDifference = (int)((ulong)aligned - (ulong)mem); 135 | mem = aligned; 136 | actualSize -= sizeDifference; 137 | } 138 | 139 | // create page 140 | var page = new Page(freePtr, mem, actualSize); 141 | pages.Add(page); 142 | 143 | Debug.Assert(((ulong)page.Address % sizeof(ulong)) == 0); 144 | return page; 145 | } 146 | 147 | private IntPtr Allocate(Type type, int elementSize, ulong sizeBytes, out RefVersion version) { 148 | IntPtr ptr; 149 | 150 | Debug.Assert(sizeBytes >= sizeof(ulong)); 151 | Debug.Assert((sizeBytes % sizeof(ulong)) == 0); 152 | Debug.Assert(sizeBytes <= int.MaxValue); 153 | 154 | var iSizeBytes = (int)sizeBytes; 155 | var elementCount = iSizeBytes / elementSize; 156 | 157 | // check if there is a freelist for this type and attempt to get an item from it 158 | Freelist freelist; 159 | if (!freelists.TryGetValue(iSizeBytes, out freelist) || (ptr = freelist.Pop()) == IntPtr.Zero) { 160 | // failed to get an item from freelist so push a new item onto the arena 161 | ptr = Push(iSizeBytes + itemHeaderSize) + itemHeaderSize; 162 | 163 | // increment item version by 1 and set header 164 | var prevVersion = ItemHeader.GetVersion(ptr); 165 | version = prevVersion.IncrementItemVersion(true, elementCount).SetArenaID(id); 166 | ItemHeader.SetHeader(ptr, new ItemHeader(GetTypeHandle(type), iSizeBytes, IntPtr.Zero, version)); // set header 167 | } 168 | else { 169 | freelists[iSizeBytes] = freelist; 170 | 171 | // increment item version by 1 172 | var prevVersion = ItemHeader.GetVersion(ptr); 173 | version = prevVersion.IncrementItemVersion(true, elementCount).SetArenaID(id); 174 | ItemHeader.SetVersion(ptr, version); 175 | ItemHeader.SetTypeHandle(ptr, GetTypeHandle(type)); 176 | } 177 | 178 | return ptr; 179 | } 180 | 181 | public UnmanagedRef Allocate(T item) where T : unmanaged { 182 | var info = GenerateTypeInfo(); 183 | var items = AllocCount(1); 184 | items.Value[0] = item; 185 | info.TrySetArenaID((IntPtr)items.Value, id); 186 | return items; 187 | } 188 | 189 | public UnmanagedRef Allocate(ref T item) where T : unmanaged { 190 | var info = GenerateTypeInfo(); 191 | var items = AllocCount(1); 192 | items.Value[0] = item; 193 | info.TrySetArenaID((IntPtr)items.Value, id); 194 | return items; 195 | } 196 | 197 | public UnmanagedRef AllocCount(int count) where T : unmanaged { 198 | if (count <= 0) { 199 | throw new ArgumentOutOfRangeException(nameof(count)); 200 | } 201 | 202 | var info = GenerateTypeInfo(); 203 | var items = _AllocValues(count); 204 | 205 | if (info.IsArenaContents) { 206 | var cur = items.Value; 207 | var elementCount = items.ElementCount; 208 | 209 | for (int i = 0; i < elementCount; i++, cur++) { 210 | // set arena ID 211 | info.TrySetArenaID((IntPtr)cur, id); 212 | } 213 | } 214 | 215 | return items; 216 | } 217 | 218 | /// 219 | /// Allocate an approximate amount of bytes. The arena will allocate a number of bytes that may be 220 | /// greater or less than the amount requested, and which optimally uses the allocator's memory 221 | /// resources for sizes 384 and over. Use this when allocating buffers where you don't care what the 222 | /// exact length of the requested buffer is. 223 | /// 224 | /// Effectively this method call will round to the nearest power of two, give or take some bytes for 225 | /// overhead from allocation headers. 226 | /// 227 | /// The approximate amount of bytes you're requesting from the arena 228 | /// An UnmanagedRef<byte> instance which may contain fewer or more bytes than requested 229 | public UnmanagedRef AllocRoughly(int sizeBytes) { 230 | if (sizeBytes < 0) { 231 | throw new ArgumentOutOfRangeException(nameof(sizeBytes)); 232 | } 233 | 234 | if (sizeBytes < sizeof(ulong)) { 235 | sizeBytes = sizeof(ulong); 236 | } 237 | 238 | if (!MemHelper.IsPowerOfTwo((uint)sizeBytes)) { 239 | var nextPowerOfTwo = MemHelper.NextPowerOfTwo((uint)sizeBytes); 240 | var prevPowerOfTwo = nextPowerOfTwo >> 1; 241 | 242 | int powerOfTwo; 243 | if (nextPowerOfTwo - (uint)sizeBytes <= (uint)sizeBytes - prevPowerOfTwo && nextPowerOfTwo <= int.MaxValue) { 244 | powerOfTwo = (int)nextPowerOfTwo; 245 | } 246 | else { 247 | powerOfTwo = (int)prevPowerOfTwo; 248 | } 249 | 250 | var up = MemHelper.AlignCeil(sizeBytes, powerOfTwo); 251 | var down = MemHelper.AlignFloor(sizeBytes, powerOfTwo); 252 | 253 | if (Math.Abs(up - sizeBytes) <= Math.Abs(sizeBytes - down) || down == 0) { 254 | sizeBytes = up; 255 | } 256 | else { 257 | sizeBytes = down; 258 | } 259 | } 260 | 261 | if (sizeBytes >= 512) { 262 | sizeBytes -= sizeof(ulong) + itemHeaderSize; 263 | } 264 | 265 | var items = _AllocValues(sizeBytes); 266 | return items; 267 | } 268 | 269 | public UnmanagedRef _AllocValues(int count) where T : unmanaged { 270 | enumVersion++; 271 | 272 | Type type = typeof(T); 273 | int elementSize = sizeof(T); 274 | 275 | ulong sizeBytes = (ulong)elementSize * (ulong)count; 276 | 277 | // make sure size in bytes is at least one 64-bit word, is 64-bit word 278 | // aligned, is a power of 2 for multiple elements and doesn't overflow 279 | // item size must be 64-bit word aligned to keep 64-bit word alignment 280 | // for all items allocated to the arena 281 | if (sizeBytes < sizeof(ulong)) { 282 | sizeBytes = sizeof(ulong); 283 | } 284 | 285 | sizeBytes = MemHelper.AlignCeil(sizeBytes, sizeof(ulong)); 286 | var headerSize = (uint)itemHeaderSize; 287 | 288 | if (count > 1 && sizeBytes > MinimumPageSize) { 289 | // add sizeof(ulong) here because if sizeBytes + headerSize is exactly the 290 | // same as the arena's page size this will cause the page allocator to add 291 | // one additional increment of page size to the newly allocated page. this 292 | // is because it adds sizeof(ulong) itself before rounding up to the nearest 293 | // page increment, in order to guarantee 64-bit alignment on all systems. 294 | // 295 | // if the only allocations made to the arena were the exact size of a page 296 | // then a worst case scenario of 50% of memory being wasted would occur. 297 | // while this change does cause overhead on smaller allocations, due to the 298 | // minimum size of over 256 bytes for this block that overhead is at most 3%. 299 | var roundedSize = MemHelper.NextPowerOfTwo(sizeBytes + sizeof(ulong) + headerSize); 300 | roundedSize -= headerSize + sizeof(ulong); 301 | sizeBytes = roundedSize; 302 | } 303 | 304 | if (sizeBytes > int.MaxValue - sizeof(ulong)) { 305 | throw new InvalidOperationException("Arena can't allocate size that large"); 306 | } 307 | 308 | Debug.Assert((sizeBytes % sizeof(ulong)) == 0); 309 | 310 | // allocate items and zero memory 311 | RefVersion version; 312 | var ptr = Allocate(type, sizeof(T), sizeBytes, out version); 313 | MemHelper.ZeroMemory(ptr, (UIntPtr)sizeBytes); 314 | 315 | Debug.Assert(((ulong)ptr % sizeof(ulong)) == 0); 316 | 317 | // get actual allocated item count (can be bigger than requested) 318 | count = (int)sizeBytes / sizeof(T); 319 | 320 | // return pointer as an UnmanagedRef 321 | return new UnmanagedRef((T*)ptr, version, count); 322 | } 323 | 324 | private IntPtr Push(int size) { 325 | IntPtr ptr; 326 | var page = pages.Last(); 327 | 328 | Debug.Assert((size % sizeof(ulong)) == 0); 329 | 330 | // try to claim size bytes in current page, will return null if out of space 331 | if ((ptr = page.Push(size)) == IntPtr.Zero) { 332 | // out of space, allocate new page, rounding size up to nearest multiple of PageSize 333 | page = AllocPage(size); 334 | 335 | // claim size bytes in current page 336 | // this will always work because we just made sure the new page fits the requested size 337 | ptr = page.Push(size); 338 | Debug.Assert(ptr != IntPtr.Zero); 339 | } 340 | 341 | pages.SetLast(page); 342 | 343 | Debug.Assert(((ulong)ptr % sizeof(ulong)) == 0); 344 | return ptr; 345 | } 346 | 347 | public void Free(IntPtr ptr) { 348 | var uref = UnmanagedRefFromPtr(ptr); 349 | Free(uref); 350 | } 351 | 352 | public void Free(in T items) where T : IUnmanagedRef { 353 | Free(items.Reference); 354 | } 355 | 356 | public void Free(in UnmanagedRef items) { 357 | IntPtr cur; 358 | if (!items.TryGetValue(out cur)) { 359 | // can't free that ya silly bugger 360 | return; 361 | } 362 | 363 | enumVersion++; 364 | 365 | var type = items.Type; 366 | var info = GetTypeInfo(type); 367 | var elementSize = info.Size; 368 | 369 | if (info.IsArenaContents) { 370 | var elementCount = items.ElementCount; 371 | for (int i = 0; i < elementCount; i++) { 372 | // free contents 373 | info.TryFree(cur); 374 | cur += elementSize; 375 | } 376 | } 377 | 378 | _FreeValues(items.Value); 379 | } 380 | 381 | public void Free(in UnmanagedRef items) where T : unmanaged { 382 | T* cur; 383 | if (!items.TryGetValue(out cur)) { 384 | // can't free that ya silly bugger 385 | return; 386 | } 387 | 388 | enumVersion++; 389 | var info = GetTypeInfo(typeof(T)); 390 | 391 | if (info.IsArenaContents) { 392 | var elementCount = items.ElementCount; 393 | for (int i = 0; i < elementCount; i++, cur++) { 394 | // free contents 395 | info.TryFree((IntPtr)cur); 396 | } 397 | } 398 | 399 | _FreeValues((IntPtr)items.Value); 400 | } 401 | 402 | private void _FreeValues(IntPtr itemPtr) { 403 | var sizeBytes = ItemHeader.GetSize(itemPtr); 404 | 405 | // set version to indicate item is not valid 406 | ItemHeader.Invalidate(itemPtr); 407 | 408 | var page = pages.Last(); 409 | if (page.IsTop(itemPtr + sizeBytes)) { 410 | // if the item as at the top of the current page then simply pop it off 411 | page.Pop(sizeBytes + itemHeaderSize); 412 | pages.SetLast(page); 413 | } 414 | else { 415 | // otherwise ensure a freelist for the type exists and push the item's location onto it 416 | // for reuse 417 | Freelist freelist; 418 | if (!freelists.TryGetValue(sizeBytes, out freelist)) { 419 | freelist = new Freelist(); 420 | } 421 | 422 | freelist.Push(itemPtr); 423 | freelists[sizeBytes] = freelist; 424 | } 425 | } 426 | 427 | internal IntPtr SetOutsidePtr(T value, IntPtr currentHandlePtr) where T : class { 428 | if (!(value is object) && currentHandlePtr == IntPtr.Zero) { 429 | // both null, do nothing 430 | return IntPtr.Zero; 431 | } 432 | 433 | var managedEntry = default(ObjectEntry); 434 | 435 | if (value is object) { 436 | // value is not null. get object handle, or create one if none exist 437 | if (!objToPtr.TryGetValue(value, out managedEntry)) { 438 | // allocate object handle 439 | managedEntry.Handle = GCHandle.Alloc(value, GCHandleType.Weak); 440 | 441 | // add handle to lookup tables 442 | objToPtr[value] = managedEntry; 443 | } 444 | } 445 | 446 | if (managedEntry.Handle.IsAllocated) { 447 | var managedHandlePtr = GCHandle.ToIntPtr(managedEntry.Handle); 448 | if (managedHandlePtr == currentHandlePtr) { 449 | // same value, do nothing 450 | return managedHandlePtr; 451 | } 452 | } 453 | 454 | if (currentHandlePtr != IntPtr.Zero) { 455 | var currentHandle = GCHandle.FromIntPtr(currentHandlePtr); 456 | var currentTarget = currentHandle.Target; 457 | Debug.Assert(!(currentTarget is null)); 458 | 459 | ObjectEntry currentManagedEntry; 460 | if (!objToPtr.TryGetValue(currentTarget, out currentManagedEntry)) { 461 | throw new InvalidOperationException("Object handle is allocated but not in lookup tables"); 462 | } 463 | 464 | // current value of field being set isn't null so decrease refcount and clean up if needed 465 | currentManagedEntry.RefCount--; 466 | 467 | // can clean up here because we've already established the value isn't the same on both sides 468 | if (currentManagedEntry.RefCount <= 0) { 469 | // free object handle and remove from lookup tables so .NET's tracing GC can (theoretically) 470 | // collect the object being referenced now that no references to it from within this arena exist 471 | currentHandle.Free(); 472 | objToPtr.Remove(currentTarget); 473 | } 474 | else { 475 | objToPtr[currentTarget] = currentManagedEntry; // update entry in lookup tables 476 | } 477 | } 478 | 479 | if (managedEntry.Handle.IsAllocated) { 480 | // increase object handle reference count 481 | managedEntry.RefCount++; 482 | objToPtr[value] = managedEntry; // update entry in lookup tables 483 | 484 | // return new object handle 485 | return GCHandle.ToIntPtr(managedEntry.Handle); 486 | } 487 | 488 | return IntPtr.Zero; 489 | } 490 | 491 | public bool VersionsMatch(RefVersion version, IntPtr item) { 492 | return version.Arena == id && version == ItemHeader.GetVersion(item); 493 | } 494 | 495 | public void Clear() { 496 | Clear(false); 497 | } 498 | 499 | private void Clear(bool disposing, bool fromFinalizer = false) { 500 | enumVersion++; 501 | 502 | // free GCHandles 503 | foreach (var entry in objToPtr.Values) { 504 | var gcHandle = entry.Handle; 505 | if (gcHandle.IsAllocated) { 506 | gcHandle.Free(); 507 | } 508 | } 509 | 510 | // free page memory 511 | var allocator = Allocator; 512 | foreach (var page in pages) { 513 | page.Free(allocator); 514 | } 515 | 516 | pages.Clear(); 517 | freelists.Clear(); 518 | objToPtr.Clear(); 519 | 520 | if (disposing) { 521 | Remove(id, fromFinalizer); 522 | id = ArenaID.Empty; 523 | } 524 | else { 525 | if (!initialized) { 526 | // get an ID and ID->Arena mapping entry 527 | initialized = true; 528 | Add(this); 529 | } 530 | else { 531 | // get a new ID to invalidate any stale references 532 | ChangeID(this); 533 | } 534 | 535 | // allocate one page to start 536 | AllocPage(pageSize - sizeof(ulong)); 537 | } 538 | } 539 | 540 | bool IArenaPoolable.ResetForPool() { 541 | if (finalized) { 542 | return false; 543 | } 544 | 545 | if (disposedValue) { 546 | disposedValue = false; 547 | GC.ReRegisterForFinalize(this); 548 | Init(); 549 | } 550 | 551 | Clear(); 552 | return true; 553 | } 554 | 555 | #region IEnumerable 556 | public Enumerator GetEnumerator() { 557 | return new Enumerator(this); 558 | } 559 | 560 | IEnumerator IEnumerable.GetEnumerator() { 561 | return GetEnumerator(); 562 | } 563 | 564 | IEnumerator IEnumerable.GetEnumerator() { 565 | return GetEnumerator(); 566 | } 567 | #endregion 568 | 569 | #region IDisposable 570 | protected virtual void Dispose(bool disposing) { 571 | if (!disposedValue) { 572 | if (disposing) { 573 | // dispose managed state (managed objects) 574 | } 575 | 576 | // free unmanaged resources (unmanaged objects) and override finalizer 577 | // set large fields to null 578 | Clear(true, !disposing); 579 | 580 | disposedValue = true; 581 | } 582 | } 583 | 584 | ~Arena() { 585 | // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method 586 | Dispose(disposing: false); 587 | finalized = true; 588 | } 589 | 590 | public void Dispose() { 591 | // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method 592 | Dispose(disposing: true); 593 | GC.SuppressFinalize(this); 594 | } 595 | #endregion 596 | 597 | public bool IsDisposed { get { return disposedValue; } } 598 | public ArenaID ID { get { return id; } } 599 | public IMemoryAllocator Allocator { get; private set; } 600 | 601 | #region Static 602 | private const int MaxFinalizedRemovalsPerAdd = 8; 603 | 604 | private static ConcurrentQueue finalizedRemovals; 605 | private static Dictionary> arenas; 606 | private static object arenasLock; 607 | private static readonly int itemHeaderSize; 608 | 609 | public static IMemoryAllocator DefaultAllocator { get; set; } 610 | 611 | static Arena() { 612 | // item header size must be 64-bit word aligned to keep 64-bit word alignment for all items 613 | // allocated to the arena 614 | itemHeaderSize = MemHelper.AlignCeil(sizeof(ItemHeader), sizeof(ulong)); 615 | Debug.Assert((itemHeaderSize % sizeof(ulong)) == 0); 616 | 617 | finalizedRemovals = new ConcurrentQueue(); 618 | arenas = new Dictionary>(); 619 | arenasLock = new object(); 620 | 621 | DefaultAllocator = new MarshalHGlobalAllocator(); 622 | } 623 | 624 | private static void Add(Arena arena) { 625 | Add(arena, false); 626 | } 627 | 628 | private static void Add(Arena arena, bool hasPreviousEntry) { 629 | bool doRemovals = true; 630 | 631 | while (true) { 632 | var id = ArenaID.NewID(); 633 | Debug.Assert(id.Value != 0); 634 | 635 | lock (arenasLock) { 636 | if (doRemovals && !finalizedRemovals.IsEmpty) { 637 | doRemovals = false; 638 | 639 | // if there are pending removals via finalizer, remove them now 640 | // but only remove a limited number as to not block for too long 641 | for (int i = 0; i < MaxFinalizedRemovalsPerAdd; i++) { 642 | ArenaID removeID; 643 | if (finalizedRemovals.TryDequeue(out removeID)) { 644 | arenas.Remove(removeID); 645 | } 646 | else { 647 | break; 648 | } 649 | } 650 | } 651 | 652 | if (arenas.ContainsKey(id)) { 653 | continue; 654 | } 655 | 656 | if (hasPreviousEntry) { 657 | var removed = arenas.Remove(arena.id); 658 | Debug.Assert(removed); 659 | } 660 | else { 661 | Debug.Assert(!arenas.ContainsKey(arena.id)); 662 | } 663 | 664 | arenas[id] = new WeakReference(arena); 665 | arena.id = id; 666 | break; 667 | } 668 | } 669 | } 670 | 671 | private static void ChangeID(Arena arena) { 672 | Add(arena, true); 673 | } 674 | 675 | private static void Remove(ArenaID id, bool fromFinalizer) { 676 | if (fromFinalizer) { 677 | // don't lock in code called from finalizer, instead 678 | // add to removals queue which is emptied during Add 679 | finalizedRemovals.Enqueue(id); 680 | } 681 | else { 682 | // removal from Dispose (or Clear) method, remove now 683 | lock (arenasLock) { 684 | arenas.Remove(id); 685 | } 686 | } 687 | } 688 | 689 | public static Arena Get(ArenaID id) { 690 | if (id.Value == 0) { 691 | return null; 692 | } 693 | 694 | WeakReference aref; 695 | lock (arenasLock) { 696 | if (!arenas.TryGetValue(id, out aref)) { 697 | return null; 698 | } 699 | 700 | Arena arena; 701 | if (!aref.TryGetTarget(out arena)) { 702 | return null; 703 | } 704 | 705 | return arena; 706 | } 707 | } 708 | #endregion 709 | 710 | [StructLayout(LayoutKind.Sequential)] 711 | private struct ObjectEntry { 712 | public GCHandle Handle; 713 | public int RefCount; 714 | 715 | public override string ToString() { 716 | return $"ObjectEntry(Handle=0x{GCHandle.ToIntPtr(Handle).ToInt64():x}, RefCount={RefCount})"; 717 | } 718 | } 719 | 720 | [StructLayout(LayoutKind.Sequential)] 721 | private struct Page { 722 | public IntPtr Address; 723 | /// 724 | /// Size in bytes 725 | /// 726 | public int Size; 727 | /// 728 | /// Offset in bytes 729 | /// 730 | public int Offset; 731 | 732 | private IntPtr freePtr; 733 | 734 | public Page(IntPtr freePtr, IntPtr address, int size) { 735 | this.freePtr = freePtr; 736 | Address = address; 737 | Size = size; 738 | Offset = 0; 739 | } 740 | 741 | public void Free(IMemoryAllocator allocator) { 742 | if (freePtr == IntPtr.Zero) { 743 | return; 744 | } 745 | allocator.Free(freePtr); 746 | freePtr = IntPtr.Zero; 747 | } 748 | 749 | public IntPtr Push(int size) { 750 | var end = (long)Offset + size; 751 | if (end > Size) { 752 | return IntPtr.Zero; 753 | } 754 | var ret = Address + Offset; 755 | Offset += size; 756 | return ret; 757 | } 758 | 759 | public void Pop(int size) { 760 | Debug.Assert(Offset - size >= 0, "Bad pop size"); 761 | Offset -= size; 762 | } 763 | 764 | public bool IsTop(IntPtr ptr) { 765 | return ptr == Address + Offset; 766 | } 767 | 768 | public override string ToString() { 769 | return $"Page(Address=0x{Address.ToInt64():x}, Size={Size}, Offset={Offset})"; 770 | } 771 | } 772 | 773 | [StructLayout(LayoutKind.Sequential)] 774 | private struct Freelist { 775 | public IntPtr Head; 776 | 777 | public IntPtr Pop() { 778 | if (Head == IntPtr.Zero) { 779 | return IntPtr.Zero; 780 | } 781 | 782 | var item = Head; 783 | Head = ItemHeader.GetNextFree(Head); 784 | 785 | return item; 786 | } 787 | 788 | public void Push(IntPtr item) { 789 | var next = Head; 790 | Head = item; 791 | ItemHeader.SetNextFree(Head, next); 792 | } 793 | 794 | public override string ToString() { 795 | return $"Freelist(Head=0x{Head.ToInt64():x})"; 796 | } 797 | } 798 | 799 | [StructLayout(LayoutKind.Sequential)] 800 | internal struct ItemHeader { 801 | public TypeHandle TypeHandle; 802 | public int Size; 803 | public IntPtr NextFree; 804 | public RefVersion Version; 805 | 806 | public ItemHeader(TypeHandle typeHandle, int size, IntPtr next, RefVersion version) { 807 | TypeHandle = typeHandle; 808 | Size = size; 809 | NextFree = next; 810 | Version = version; 811 | } 812 | 813 | public override string ToString() { 814 | return $"ItemHeader(Type={GetTypeFromHandle(TypeHandle).FullName}, Size={Size}, Next=0x{NextFree.ToInt64():x}, Version=({Version}))"; 815 | } 816 | 817 | // helper functions for manipulating an item header, which is always located 818 | // in memory right before where the item is allocated 819 | public static void SetVersion(IntPtr item, RefVersion version) { 820 | var header = (ItemHeader*)(item - sizeof(ItemHeader)); 821 | header->Version = version; 822 | } 823 | 824 | public static RefVersion GetVersion(IntPtr item) { 825 | if (item == IntPtr.Zero) { 826 | return default(RefVersion).Invalidate(); 827 | } 828 | var header = (ItemHeader*)(item - sizeof(ItemHeader)); 829 | return header->Version; 830 | } 831 | 832 | public static void SetSize(IntPtr item, int size) { 833 | var header = (ItemHeader*)(item - sizeof(ItemHeader)); 834 | header->Size = size; 835 | } 836 | 837 | public static int GetSize(IntPtr item) { 838 | if (item == IntPtr.Zero) { 839 | return 0; 840 | } 841 | var header = (ItemHeader*)(item - sizeof(ItemHeader)); 842 | return header->Size; 843 | } 844 | 845 | public static void SetNextFree(IntPtr item, IntPtr next) { 846 | var header = (ItemHeader*)(item - sizeof(ItemHeader)); 847 | header->NextFree = next; 848 | } 849 | 850 | public static IntPtr GetNextFree(IntPtr item) { 851 | var header = (ItemHeader*)(item - sizeof(ItemHeader)); 852 | return header->NextFree; 853 | } 854 | 855 | public static void SetTypeHandle(IntPtr item, TypeHandle handle) { 856 | var header = (ItemHeader*)(item - sizeof(ItemHeader)); 857 | header->TypeHandle = handle; 858 | } 859 | 860 | public static TypeHandle GetTypeHandle(IntPtr item) { 861 | if (item == IntPtr.Zero) { 862 | return new TypeHandle(0); 863 | } 864 | var header = (ItemHeader*)(item - sizeof(ItemHeader)); 865 | return header->TypeHandle; 866 | } 867 | 868 | public static void SetHeader(IntPtr item, ItemHeader itemHeader) { 869 | var header = (ItemHeader*)(item - sizeof(ItemHeader)); 870 | *header = itemHeader; 871 | } 872 | 873 | public static ItemHeader GetHeader(IntPtr item) { 874 | var header = (ItemHeader*)(item - sizeof(ItemHeader)); 875 | return *header; 876 | } 877 | 878 | public static void Invalidate(IntPtr item) { 879 | var header = (ItemHeader*)(item - sizeof(ItemHeader)); 880 | header->Version = header->Version.Invalidate(); 881 | } 882 | 883 | public static ArenaID GetArenaID(IntPtr item) { 884 | if (item == IntPtr.Zero) { 885 | return ArenaID.Empty; 886 | } 887 | var header = (ItemHeader*)(item - sizeof(ItemHeader)); 888 | return header->Version.Arena; 889 | } 890 | } 891 | 892 | [Serializable] 893 | public struct Enumerator : IEnumerator, IEnumerator { 894 | private Arena arena; 895 | private int pageIndex; 896 | private int offset; 897 | private int version; 898 | private UnmanagedRef current; 899 | 900 | internal Enumerator(Arena arena) { 901 | this.arena = arena; 902 | pageIndex = 0; 903 | offset = 0; 904 | version = arena.enumVersion; 905 | current = default; 906 | } 907 | 908 | public void Dispose() { 909 | } 910 | 911 | public bool MoveNext() { 912 | Arena localArena = arena; 913 | 914 | if (version == localArena.enumVersion && pageIndex < localArena.pages.Count) { 915 | while (pageIndex < localArena.pages.Count) { 916 | Page curPage = localArena.pages[pageIndex]; 917 | 918 | if (offset >= curPage.Offset) { 919 | pageIndex++; 920 | offset = 0; 921 | continue; 922 | } 923 | 924 | var ptr = curPage.Address + offset + itemHeaderSize; 925 | var header = ItemHeader.GetHeader(ptr); 926 | offset += header.Size + itemHeaderSize; 927 | 928 | Type type; 929 | if (!TryGetTypeFromHandle(header.TypeHandle, out type) || header.Size < 0 || offset < 0 || offset > curPage.Offset) { 930 | throw new InvalidOperationException("Enumeration encountered an error; arena memory may be corrupted"); 931 | } 932 | 933 | if (header.Version.Valid) { 934 | current = localArena.UnmanagedRefFromPtr(ptr); 935 | return true; 936 | } 937 | } 938 | } 939 | 940 | return MoveNextRare(); 941 | } 942 | 943 | private bool MoveNextRare() { 944 | if (version != arena.enumVersion) { 945 | throw new InvalidOperationException("Collection was modified; enumeration operation may not execute."); 946 | } 947 | 948 | pageIndex = arena.pages.Count + 1; 949 | offset = 0; 950 | current = default; 951 | return false; 952 | } 953 | 954 | public UnmanagedRef Current { 955 | get { 956 | return current; 957 | } 958 | } 959 | 960 | object IEnumerator.Current { 961 | get { 962 | if ((pageIndex == 0 && offset == 0) || pageIndex == arena.pages.Count + 1) { 963 | throw new InvalidOperationException("Enumeration has either not started or has already finished."); 964 | } 965 | return Current; 966 | } 967 | } 968 | 969 | void IEnumerator.Reset() { 970 | if (version != arena.enumVersion) { 971 | throw new InvalidOperationException("Collection was modified; enumeration operation may not execute."); 972 | } 973 | 974 | pageIndex = 0; 975 | offset = 0; 976 | current = default; 977 | } 978 | } 979 | 980 | private class ObjectReferenceEqualityComparer : IEqualityComparer { 981 | public static readonly ObjectReferenceEqualityComparer Instance = new ObjectReferenceEqualityComparer(); 982 | 983 | public new bool Equals(object x, object y) { 984 | return ReferenceEquals(x, y); 985 | } 986 | 987 | public int GetHashCode(object obj) { 988 | return RuntimeHelpers.GetHashCode(obj); 989 | } 990 | } 991 | 992 | public sealed class MarshalHGlobalAllocator : IMemoryAllocator { 993 | public MemoryAllocation Allocate(int sizeBytes) { 994 | var ptr = Marshal.AllocHGlobal(sizeBytes); 995 | return new MemoryAllocation(ptr, sizeBytes); 996 | } 997 | 998 | public void Free(IntPtr ptr) { 999 | Marshal.FreeHGlobal(ptr); 1000 | } 1001 | } 1002 | } 1003 | } 1004 | -------------------------------------------------------------------------------- /ArenaDict.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Linq; 6 | using System.Runtime.InteropServices; 7 | 8 | namespace Arenas { 9 | using static Arenas.UnmanagedListTypes; 10 | using static UnmanagedDictTypes; 11 | 12 | // TODO: Custom IEqualityComparer 13 | [DebuggerTypeProxy(typeof(ArenaDictDebugView<,>))] 14 | public unsafe struct ArenaDict : IDictionary, IDisposable where TKey : unmanaged where TValue : unmanaged { 15 | private const int defaultCapacity = 4; 16 | private const uint fibHashMagic = 2654435769; 17 | private const int noneHashCode = 0; 18 | private const int nullOffset = 0; 19 | 20 | private static int RebalanceCount(int capacity) { 21 | return capacity * 3 / 4; 22 | } 23 | 24 | private UnmanagedRef info; 25 | 26 | private ArenaDict(UnmanagedRef dictData) { 27 | info = dictData; 28 | } 29 | 30 | public ArenaDict(Arena arena, int capacity = defaultCapacity) { 31 | if (arena is null) { 32 | throw new ArgumentNullException(nameof(arena)); 33 | } 34 | 35 | if (capacity < 0) { 36 | capacity = defaultCapacity; 37 | } 38 | 39 | var typeK = TypeInfo.GenerateTypeInfo(); 40 | var typeV = TypeInfo.GenerateTypeInfo(); 41 | 42 | if (typeK.IsArenaContents) { 43 | throw new NotSupportedException("ArenaList cannot store keys which implement IArenaContents. Please use UnmanagedRef instead."); 44 | } 45 | if (typeV.IsArenaContents) { 46 | throw new NotSupportedException("ArenaList cannot store values which implement IArenaContents. Please use UnmanagedRef instead."); 47 | } 48 | 49 | // first allocate our info object 50 | info = arena.Allocate(new UnmanagedDict()); 51 | 52 | // must have positive power of two capacity 53 | ulong powTwo = (uint)capacity; 54 | if (!MemHelper.IsPowerOfTwo(powTwo)) { 55 | if (capacity <= 0) { 56 | capacity = defaultCapacity; 57 | } 58 | 59 | powTwo = MemHelper.NextPowerOfTwo((ulong)capacity); 60 | if (powTwo > int.MaxValue) { 61 | throw new ArgumentOutOfRangeException(nameof(capacity)); 62 | } 63 | } 64 | else if (powTwo == 1) { 65 | // capacity 1 doesn't really work since the rebalance count is 0 66 | // so it'll rebalance to 2 items as soon as you use it at all 67 | powTwo = 2; 68 | } 69 | 70 | // get the shift amount from the capacity which is now a power of two 71 | // this is used to shift the hashcode into a backing array index after 72 | // multiplying it by our magic fibonacci number 73 | capacity = (int)powTwo; 74 | var shift = 32 - MemHelper.PowerOfTwoLeadingZeros[powTwo]; 75 | 76 | // because .NET standard can't have composed unmanaged types we cant allocate a memory area 77 | // that is a series of entry structs, and instead have to interleave entry headers with key 78 | // and value manually. we use our type info here in order to get all the offsets and sizes 79 | // and such that are needed 80 | var entryInfo = TypeInfo.GenerateTypeInfo(); 81 | var entrySize = MemHelper.AlignCeil(typeK.Size + typeV.Size + entryInfo.Size, sizeof(ulong)); 82 | 83 | // Old comment: 84 | // now we know entry size, calculate the actual amount of memory needed 85 | // this needs to be twice the amount in case of a worst case scenario of all items mapping 86 | // to the same hashcode, that way we know we always have enough capacity outside of the 87 | // backing array 88 | 89 | // New comment: 90 | // actually we only need as much overflow storage as 75% of the capacity in order to make 91 | // sure that there's always enough room for items before rebalancing, so just use the 92 | // rebalancing amount to determine the extra overflow storage space needed. this is still 93 | // 1 item more than we actually need, because even in the worst case the head is always 94 | // stored inside the backing array, but idk, off by one errors are scary and there's no 95 | // need to ditch the spare bonus item just in case 96 | // 97 | // by keeping the overflow size a little smaller than the power-of-two sized backing array 98 | // we usually wind up with a size a little under a power of 2 in size, which is useful 99 | // for memory efficiency because the arena will dole out chunks of power-of-two minus the 100 | // size of an item header in memory, so requesting a full power of two actually doubles the 101 | // amount allocated but requesting slightly less means we wind up with basically exactly 102 | // the amount we want so overhead becomes way lower 103 | var size = capacity * entrySize; 104 | //size *= 2; // extra space for linked lists 105 | size += RebalanceCount(capacity) * entrySize; 106 | var itemsRef = arena.AllocCount(size); // alloc memory buffer 107 | 108 | var backingArrayLength = capacity; 109 | var overflowLength = itemsRef.Size / entrySize - backingArrayLength; 110 | 111 | // set all our props 112 | var self = info.Value; 113 | 114 | self->Shift = shift; 115 | self->EntrySize = entrySize; 116 | self->KeyOffset = entryInfo.Size; // key comes after entry struct 117 | self->ValueOffset = self->KeyOffset + typeK.Size; // val comes after key 118 | 119 | self->ItemsBuffer = (UnmanagedRef)itemsRef; 120 | self->BackingArrayLength = capacity; 121 | self->OverflowLength = itemsRef.Size / entrySize - backingArrayLength; 122 | //Debug.Assert(self->OverflowLength >= self->BackingArrayLength); 123 | Debug.Assert(self->OverflowLength >= RebalanceCount(self->BackingArrayLength) - 1); 124 | 125 | // position our bump allocator to the end of the backing array 126 | // this is used to allocate new entries when the freelist is empty 127 | self->Bump = self->BackingArrayLength * self->EntrySize; 128 | } 129 | 130 | // https://probablydance.com/2018/06/16/fibonacci-hashing-the-optimization-that-the-world-forgot-or-a-better-alternative-to-integer-modulo/ 131 | private static uint HashToIndex(uint hash, int shift) { 132 | hash ^= hash >> shift; // this line improves distribution but can be commented out 133 | return (fibHashMagic * hash) >> shift; 134 | } 135 | 136 | public void Free() { 137 | if (Arena is null) { 138 | throw new InvalidOperationException("Cannot Free ArenaDict: dictionary has not been properly initialized with arena reference"); 139 | } 140 | 141 | var self = info.Value; 142 | if (self == null) { 143 | throw new InvalidOperationException("Cannot Free ArenaDict: dictionary memory has previously been freed"); 144 | } 145 | 146 | self->Version++; 147 | var items = self->ItemsBuffer; 148 | Arena.Free(items); 149 | Arena.Free(info); 150 | info = default; 151 | } 152 | 153 | public void Dispose() { 154 | if (!IsAllocated) { 155 | return; 156 | } 157 | Free(); 158 | } 159 | 160 | public void Clear() { 161 | if (Arena is null) { 162 | throw new InvalidOperationException("Cannot Clear ArenaDict: dictionary has not been properly initialized with arena reference"); 163 | } 164 | 165 | var self = info.Value; 166 | if (self == null) { 167 | throw new InvalidOperationException("Cannot Clear ArenaDict: dictionary memory has previously been freed"); 168 | } 169 | 170 | var items = self->ItemsBuffer.Value; 171 | if (items == IntPtr.Zero) { 172 | throw new InvalidOperationException("Cannot Clear ArenaDict: dictionary's backing array has previously been freed"); 173 | } 174 | 175 | self->Version++; 176 | self->Count = 0; 177 | self->Bump = self->BackingArrayLength * self->EntrySize; 178 | self->Head = nullOffset; 179 | 180 | // zero backing array memory (Bump has been set to the end of the backing array) 181 | // remaining storage space doesn't need to be zeroed because the bump allocator 182 | // zeroes out newly allocated entries 183 | MemHelper.ZeroMemory(items, self->Bump); 184 | } 185 | 186 | // TODO: implement 187 | //public void TrimExcess(int capacity = 0) { 188 | //} 189 | 190 | private void AddCapacity(UnmanagedDict* self, ref IntPtr items) { 191 | // copy into new dictionary 192 | var newDict = new ArenaDict(info.Arena, self->BackingArrayLength * 2); 193 | 194 | var entryEnumerator = new FastInternalEnumerator(this); 195 | while (entryEnumerator.GetNextEntry(out var entry)) { 196 | newDict.Add(*entry.Key, *entry.Value); 197 | } 198 | 199 | // free old items buffer 200 | Arena.Free(items); 201 | 202 | // copy new dictionary's info 203 | *self = *newDict.info.Value; 204 | 205 | // free new dictionary's info 206 | Arena.Free(newDict.info); 207 | 208 | items = self->ItemsBuffer.Value; 209 | } 210 | 211 | private int GetHashCode(TKey* key) { 212 | var hashCode = key->GetHashCode(); 213 | if (hashCode == noneHashCode) hashCode = 0x1234FEDC; // hashCode cannot be 0 214 | return hashCode; 215 | } 216 | 217 | private bool TryGetEntry(UnmanagedDict* self, IntPtr items, TKey* _key, int hashCode, out Entry entry, out Entry? previous) { 218 | var index = (int)HashToIndex((uint)hashCode, self->Shift); 219 | entry = GetIndex(self, items, index); 220 | 221 | var key = *_key; 222 | 223 | if (entry.HashCode == noneHashCode) { 224 | // no value at index, insert 225 | previous = null; 226 | return true; 227 | } 228 | else if (entry.HashCode == hashCode && EqualityComparer.Default.Equals(*entry.Key, key)) { 229 | // found identical key at index 230 | previous = null; 231 | return true; 232 | } 233 | else { 234 | // bucket exists but value wasn't at head, search nodes 235 | var next = entry.Next; 236 | while (next > nullOffset) { 237 | previous = entry; 238 | entry = GetOffset(self, items, next); 239 | if (entry.HashCode == hashCode && EqualityComparer.Default.Equals(*entry.Key, key)) { 240 | // found identical key in bucket 241 | return true; 242 | } 243 | next = entry.Next; 244 | } 245 | 246 | previous = null; 247 | return false; 248 | } 249 | } 250 | 251 | public void Add(TKey key, TValue value) { 252 | if (Arena is null) { 253 | throw new InvalidOperationException("Cannot Add item to ArenaDict: dictionary has not been properly initialized with arena reference"); 254 | } 255 | 256 | var self = info.Value; 257 | if (self == null) { 258 | throw new InvalidOperationException("Cannot Add item to ArenaDict: dictionary memory has previously been freed"); 259 | } 260 | 261 | var items = self->ItemsBuffer.Value; 262 | if (items == IntPtr.Zero) { 263 | throw new InvalidOperationException("Cannot Add item to ArenaDict: dictionary's backing array has previously been freed"); 264 | } 265 | 266 | Set(self, ref items, &key, &value, false); 267 | } 268 | 269 | void ICollection>.Add(KeyValuePair item) { 270 | Add(item.Key, item.Value); 271 | } 272 | 273 | public bool TryGetValue(TKey key, out TValue value) { 274 | if (Arena is null) { 275 | throw new InvalidOperationException("Cannot TryGetValue on ArenaDict: dictionary has not been properly initialized with arena reference"); 276 | } 277 | 278 | var self = info.Value; 279 | if (self == null) { 280 | throw new InvalidOperationException("Cannot TryGetValue on ArenaDict: dictionary memory has previously been freed"); 281 | } 282 | 283 | var items = self->ItemsBuffer.Value; 284 | if (items == IntPtr.Zero) { 285 | throw new InvalidOperationException("Cannot TryGetValue on ArenaDict: dictionary's backing array has previously been freed"); 286 | } 287 | 288 | var valRef = Get(self, items, &key, false); 289 | if (valRef == null) { 290 | value = default; 291 | return false; 292 | } 293 | value = *valRef; 294 | return true; 295 | } 296 | 297 | public bool ContainsKey(TKey key) { 298 | if (Arena is null) { 299 | throw new InvalidOperationException("Cannot ContainsKey on ArenaDict: dictionary has not been properly initialized with arena reference"); 300 | } 301 | 302 | var self = info.Value; 303 | if (self == null) { 304 | throw new InvalidOperationException("Cannot ContainsKey on ArenaDict: dictionary memory has previously been freed"); 305 | } 306 | 307 | var items = self->ItemsBuffer.Value; 308 | if (items == IntPtr.Zero) { 309 | throw new InvalidOperationException("Cannot ContainsKey on ArenaDict: dictionary's backing array has previously been freed"); 310 | } 311 | 312 | return Get(self, items, &key, false) != null; 313 | } 314 | 315 | public bool ContainsValue(TValue value) { 316 | if (Arena is null) { 317 | throw new InvalidOperationException("Cannot ContainsValue on ArenaDict: dictionary has not been properly initialized with arena reference"); 318 | } 319 | 320 | var self = info.Value; 321 | if (self == null) { 322 | throw new InvalidOperationException("Cannot ContainsValue on ArenaDict: dictionary memory has previously been freed"); 323 | } 324 | 325 | var items = self->ItemsBuffer.Value; 326 | if (items == IntPtr.Zero) { 327 | throw new InvalidOperationException("Cannot ContainsValue on ArenaDict: dictionary's backing array has previously been freed"); 328 | } 329 | 330 | var entryEnumerator = new FastInternalEnumerator(this); 331 | while (entryEnumerator.GetNextEntry(out var entry)) { 332 | if (EqualityComparer.Default.Equals(value, *entry.Value)) return true; 333 | } 334 | 335 | return false; 336 | } 337 | 338 | bool ICollection>.Contains(KeyValuePair item) { 339 | if (Arena is null) { 340 | throw new InvalidOperationException("Cannot Contains key value pair on ArenaDict: dictionary has not been properly initialized with arena reference"); 341 | } 342 | 343 | var self = info.Value; 344 | if (self == null) { 345 | throw new InvalidOperationException("Cannot Contains key value pair on ArenaDict: dictionary memory has previously been freed"); 346 | } 347 | 348 | var items = self->ItemsBuffer.Value; 349 | if (items == IntPtr.Zero) { 350 | throw new InvalidOperationException("Cannot Contains key value pair on ArenaDict: dictionary's backing array has previously been freed"); 351 | } 352 | 353 | var key = item.Key; 354 | var valRef = Get(self, items, &key, false); 355 | if (valRef == null) { 356 | return false; 357 | } 358 | 359 | return EqualityComparer.Default.Equals(*valRef, item.Value); 360 | } 361 | 362 | private TValue* Get(UnmanagedDict* self, IntPtr items, TKey* key, bool throwIfNotFound) { 363 | var hashCode = GetHashCode(key); 364 | 365 | if (!TryGetEntry(self, items, key, hashCode, out var entry, out _) || entry.HashCode == noneHashCode) { 366 | // no key found 367 | if (throwIfNotFound) { 368 | throw new KeyNotFoundException(); 369 | } 370 | return null; 371 | } 372 | 373 | return entry.Value; 374 | } 375 | 376 | private void Set(UnmanagedDict* self, ref IntPtr items, TKey* key, in TValue* value, bool allowDuplicates) { 377 | var hashCode = GetHashCode(key); 378 | 379 | if (!TryGetEntry(self, items, key, hashCode, out var entry, out _)) { 380 | // if TryGetEntry fails then `entry` is the last entry it checked 381 | // inside the bucket and never the head entry inside the backing 382 | // array, so we need to add a node after it 383 | 384 | // no key found, add to bucket 385 | var head = GetHead(self, items); // get head from freelist or bump allocator 386 | 387 | // remove head by advancing head to the next item 388 | self->Head = head.Next; 389 | 390 | // point popped head to nothing, point entry to popped head 391 | head.Next = nullOffset; 392 | entry.Next = head.Offset; 393 | entry = head; 394 | } 395 | 396 | // overwrite entry with correct values 397 | entry.HashCode = hashCode; 398 | *entry.Key = *key; 399 | *entry.Value = *value; 400 | 401 | // increment count and rebalance at 75% full 402 | self->Version++; 403 | self->Count++; 404 | if (self->Count >= RebalanceCount(self->BackingArrayLength)) { 405 | AddCapacity(self, ref items); 406 | } 407 | } 408 | 409 | private Entry GetIndex(UnmanagedDict* self, IntPtr items, int index) { 410 | return GetOffset(self, items, index * self->EntrySize); 411 | } 412 | 413 | private Entry GetOffset(UnmanagedDict* self, IntPtr items, int offset) { 414 | return new Entry(items + offset, self); 415 | } 416 | 417 | /// 418 | /// Gets but does not pop the head off the overflow area's freelist. This may 419 | /// instead allocate a new entry using the bump allocator if the freelist is 420 | /// empty 421 | /// 422 | /// Entry which point to the head entry 423 | private Entry GetHead(UnmanagedDict* self, IntPtr items) { 424 | var head = self->Head; 425 | 426 | if (head != nullOffset) { 427 | return GetOffset(self, items, head); 428 | } 429 | 430 | // no entry in freelist, allocate via bump allocator 431 | head = self->Bump; 432 | self->Bump += self->EntrySize; 433 | 434 | // make sure the newly allocated entry is zeroed, because 435 | // we only zero the backing array on clear, not the additional 436 | // memory areas 437 | var headEntry = GetOffset(self, items, head); 438 | 439 | // don't actually need to clear, just set Next to 0 440 | //headEntry.Clear(); 441 | headEntry.Next = nullOffset; 442 | 443 | Debug.Assert(head < self->ItemsBuffer.Size); 444 | Debug.Assert(headEntry.Next == nullOffset); 445 | 446 | return headEntry; 447 | } 448 | 449 | public bool Remove(TKey key) { 450 | if (Arena is null) { 451 | throw new InvalidOperationException("Cannot Remove item from ArenaDict: dictionary has not been properly initialized with arena reference"); 452 | } 453 | 454 | var self = info.Value; 455 | if (self == null) { 456 | throw new InvalidOperationException("Cannot Remove item from ArenaDict: dictionary memory has previously been freed"); 457 | } 458 | 459 | var items = self->ItemsBuffer.Value; 460 | if (items == IntPtr.Zero) { 461 | throw new InvalidOperationException("Cannot Remove item from ArenaDict: dictionary's backing array has previously been freed"); 462 | } 463 | 464 | var hashCode = GetHashCode(&key); 465 | if (!TryGetEntry(self, items, &key, hashCode, out var entry, out var _prev)) { 466 | return false; 467 | } 468 | 469 | if (_prev.HasValue) { 470 | // entry is a linked list node, so we just unlink it 471 | UnlinkEntry(self, entry, _prev.Value); 472 | } 473 | else { 474 | // entry is in main backing array 475 | if (entry.Next != nullOffset) { 476 | // entry links to another value, copy the next value into the 477 | // backing array and then unlink it 478 | var next = GetOffset(self, items, entry.Next); 479 | 480 | // set hashcode, key, and value but leave `next` property as 481 | // is so we can subsequently call UnlinkEntry 482 | entry.HashCode = next.HashCode; 483 | *entry.Key = *next.Key; 484 | *entry.Value = *next.Value; 485 | 486 | UnlinkEntry(self, next, entry); 487 | } 488 | else { 489 | // entry links to no values, clear 490 | entry.Clear(); 491 | } 492 | } 493 | 494 | self->Version++; 495 | self->Count--; 496 | return true; 497 | } 498 | 499 | private void UnlinkEntry(UnmanagedDict* self, Entry entry, Entry prev) { 500 | // unlink node 501 | prev.Next = entry.Next; 502 | 503 | // insert new head node into freelist 504 | var prevHead = self->Head; 505 | self->Head = entry.Offset; 506 | entry.Next = prevHead; 507 | } 508 | 509 | bool ICollection>.Remove(KeyValuePair item) { 510 | if (Arena is null) { 511 | throw new InvalidOperationException("Cannot Remove key value pair from ArenaDict: dictionary has not been properly initialized with arena reference"); 512 | } 513 | 514 | var self = info.Value; 515 | if (self == null) { 516 | throw new InvalidOperationException("Cannot Remove key value pair from ArenaDict: dictionary memory has previously been freed"); 517 | } 518 | 519 | var items = self->ItemsBuffer.Value; 520 | if (items == IntPtr.Zero) { 521 | throw new InvalidOperationException("Cannot Remove key value pair from ArenaDict: dictionary's backing array has previously been freed"); 522 | } 523 | 524 | var key = item.Key; 525 | var valRef = Get(self, items, &key, false); 526 | if (valRef != null && EqualityComparer.Default.Equals(*valRef, item.Value)) { 527 | Remove(key); 528 | return true; 529 | } 530 | return false; 531 | } 532 | 533 | void ICollection>.CopyTo(KeyValuePair[] array, int index) { 534 | if (Arena is null) { 535 | throw new InvalidOperationException("Cannot CopyTo key value pairs on ArenaDict: dictionary has not been properly initialized with arena reference"); 536 | } 537 | 538 | var self = info.Value; 539 | if (self == null) { 540 | throw new InvalidOperationException("Cannot CopyTo key value pairs on ArenaDict: dictionary memory has previously been freed"); 541 | } 542 | 543 | var items = self->ItemsBuffer.Value; 544 | if (items == IntPtr.Zero) { 545 | throw new InvalidOperationException("Cannot CopyTo key value pairs on ArenaDict: dictionary's backing array has previously been freed"); 546 | } 547 | 548 | if (array == null) { 549 | throw new ArgumentNullException(nameof(array)); 550 | } 551 | if (array.Rank != 1) { 552 | throw new ArgumentException("Only single dimensional arrays are supported for the requested action.", nameof(array)); 553 | } 554 | if (array.GetLowerBound(0) != 0) { 555 | throw new ArgumentException("The lower bound of target array must be zero.", nameof(array)); 556 | } 557 | if ((uint)index > (uint)array.Length) { 558 | throw new ArgumentOutOfRangeException(nameof(index), "Non-negative number required."); 559 | } 560 | if (array.Length - index < Count) { 561 | throw new ArgumentException("Destination array is not long enough to copy all the items in the collection. Check array index and length.", nameof(array)); 562 | } 563 | 564 | var entryEnumerator = new FastInternalEnumerator(this); 565 | while (entryEnumerator.GetNextEntry(out var entry)) { 566 | array[index++] = new KeyValuePair(*entry.Key, *entry.Value); 567 | } 568 | } 569 | 570 | public Enumerator GetEnumerator() { 571 | if (Arena is null) { 572 | throw new InvalidOperationException("Cannot GetEnumerator for ArenaDict: dictionary has not been properly initialized with arena reference"); 573 | } 574 | 575 | var self = info.Value; 576 | if (self == null) { 577 | throw new InvalidOperationException("Cannot GetEnumerator for ArenaDict: dictionary memory has previously been freed"); 578 | } 579 | 580 | var items = self->ItemsBuffer.Value; 581 | if (items == IntPtr.Zero) { 582 | throw new InvalidOperationException("Cannot GetEnumerator for ArenaDict: dictionary's backing array has previously been freed"); 583 | } 584 | 585 | return new Enumerator(this); 586 | } 587 | 588 | IEnumerator> IEnumerable>.GetEnumerator() { 589 | return GetEnumerator(); 590 | } 591 | 592 | IEnumerator IEnumerable.GetEnumerator() { 593 | return GetEnumerator(); 594 | } 595 | 596 | public UnmanagedRef GetUnderlyingReference() { 597 | return info; 598 | } 599 | 600 | public static explicit operator ArenaDict(UnmanagedRef dictData) { 601 | return new ArenaDict(dictData); 602 | } 603 | 604 | public TValue this[TKey key] { 605 | get { 606 | if (Arena is null) { 607 | throw new InvalidOperationException("Cannot get item at index in ArenaDict: dictionary has not been properly initialized with arena reference"); 608 | } 609 | 610 | var self = info.Value; 611 | if (self == null) { 612 | throw new InvalidOperationException("Cannot get item at index in ArenaDict: dictionary memory has previously been freed"); 613 | } 614 | 615 | var items = self->ItemsBuffer.Value; 616 | if (items == IntPtr.Zero) { 617 | throw new InvalidOperationException("Cannot get item at index in ArenaDict: dictionary's backing array has previously been freed"); 618 | } 619 | 620 | return *Get(self, items, &key, true); 621 | } 622 | set { 623 | if (Arena is null) { 624 | throw new InvalidOperationException("Cannot set item at index in ArenaDict: dictionary has not been properly initialized with arena reference"); 625 | } 626 | 627 | var self = info.Value; 628 | if (self == null) { 629 | throw new InvalidOperationException("Cannot set item at index in ArenaDict: dictionary memory has previously been freed"); 630 | } 631 | 632 | var items = self->ItemsBuffer.Value; 633 | if (items == IntPtr.Zero) { 634 | throw new InvalidOperationException("Cannot set item at index in ArenaDict: dictionary's backing array has previously been freed"); 635 | } 636 | 637 | Set(self, ref items, &key, &value, true); 638 | } 639 | } 640 | 641 | public int Count { 642 | get { 643 | var self = info.Value; 644 | if (self == null) { 645 | return 0; 646 | } 647 | return self->Count; 648 | } 649 | } 650 | 651 | public KeyCollection Keys { 652 | get { 653 | if (Arena is null) { 654 | throw new InvalidOperationException("Cannot get Keys for ArenaDict: dictionary has not been properly initialized with arena reference"); 655 | } 656 | 657 | var self = info.Value; 658 | if (self == null) { 659 | throw new InvalidOperationException("Cannot get Keys for ArenaDict: dictionary memory has previously been freed"); 660 | } 661 | 662 | var items = self->ItemsBuffer.Value; 663 | if (items == IntPtr.Zero) { 664 | throw new InvalidOperationException("Cannot get Keys for ArenaDict: dictionary's backing array has previously been freed"); 665 | } 666 | 667 | return new KeyCollection(this); 668 | } 669 | } 670 | public ValueCollection Values { 671 | get { 672 | if (Arena is null) { 673 | throw new InvalidOperationException("Cannot get Values for ArenaDict: dictionary has not been properly initialized with arena reference"); 674 | } 675 | 676 | var self = info.Value; 677 | if (self == null) { 678 | throw new InvalidOperationException("Cannot get Values for ArenaDict: dictionary memory has previously been freed"); 679 | } 680 | 681 | var items = self->ItemsBuffer.Value; 682 | if (items == IntPtr.Zero) { 683 | throw new InvalidOperationException("Cannot get Values for ArenaDict: dictionary's backing array has previously been freed"); 684 | } 685 | 686 | return new ValueCollection(this); 687 | } 688 | } 689 | 690 | public bool IsAllocated { get { return info.HasValue; } } 691 | public Arena Arena { get { return info.Arena; } } 692 | bool ICollection>.IsReadOnly { get { return false; } } 693 | 694 | ICollection IDictionary.Keys { get { return Keys; } } 695 | ICollection IDictionary.Values { get { return Values; } } 696 | 697 | /// 698 | /// Wrapper struct around a memory area representing an UnmanagedDictEntry, key, and value 699 | /// 700 | private unsafe readonly struct Entry { 701 | public readonly IntPtr Pointer; 702 | public readonly UnmanagedDict* Dict; 703 | 704 | public Entry(IntPtr pointer, UnmanagedDict* dict) { 705 | Pointer = pointer; 706 | Dict = dict; 707 | } 708 | 709 | public void Clear() { 710 | *(UnmanagedDictEntry*)Pointer = default; 711 | *Key = default; 712 | *Value = default; 713 | } 714 | 715 | public override string ToString() { 716 | return $"Entry({*Key}={*Value}, HashCode={HashCode}, Next={Next}, Offset={Offset})"; 717 | } 718 | 719 | /// 720 | /// Offset of this entry from the start of the memory storage area for the dictionary's items 721 | /// 722 | public int Offset { get { return (int)((ulong)Pointer - (ulong)Dict->ItemsBuffer.Value); } } 723 | public TKey* Key { get { return (TKey*)(Pointer + Dict->KeyOffset); } } 724 | public TValue* Value { get { return (TValue*)(Pointer + Dict->ValueOffset); } } 725 | public int HashCode { get { return ((UnmanagedDictEntry*)Pointer)->HashCode; } set { ((UnmanagedDictEntry*)Pointer)->HashCode = value; } } 726 | public int Next { get { return ((UnmanagedDictEntry*)Pointer)->Next; } set { ((UnmanagedDictEntry*)Pointer)->Next = value; } } 727 | } 728 | 729 | [Serializable] 730 | public struct Enumerator : IEnumerator>, System.Collections.IEnumerator { 731 | private ArenaDict dict; 732 | private int index; 733 | private int offset; 734 | private int headOffset; 735 | private int version; 736 | private int count; 737 | private KeyValuePair current; 738 | 739 | internal Enumerator(ArenaDict dict) { 740 | this.dict = dict; 741 | var dictPtr = dict.info.Value; 742 | index = 0; 743 | offset = 0; 744 | headOffset = 0; 745 | version = dictPtr->Version; 746 | count = dictPtr->BackingArrayLength; 747 | current = default; 748 | } 749 | 750 | public void Dispose() { 751 | } 752 | 753 | public bool MoveNext() { 754 | var dictPtr = dict.info.Value; 755 | if (dictPtr == null || version != dictPtr->Version) { 756 | throw new InvalidOperationException("Collection was modified; enumeration operation may not execute."); 757 | } 758 | 759 | var itemsPtr = dictPtr->ItemsBuffer.Value; 760 | if (itemsPtr == IntPtr.Zero) { 761 | throw new InvalidOperationException("Collection was modified; enumeration operation may not execute."); 762 | } 763 | 764 | while ((uint)index < (uint)count) { 765 | // get current entry 766 | var entry = dict.GetOffset(dictPtr, itemsPtr, offset); 767 | 768 | // move to next entry associated with the current index 769 | offset = entry.Next; 770 | if (offset == nullOffset) { 771 | // if the position of the next entry is zero then there are no further 772 | // entries at this index, increment the index and set the new offset to 773 | // the head of the list (the entry in the backing array) 774 | headOffset += dictPtr->EntrySize; 775 | offset = headOffset; 776 | index++; 777 | } 778 | 779 | // only entries with a hashcode which isn't zero are valid entries 780 | if (entry.HashCode != noneHashCode) { 781 | current = new KeyValuePair(*entry.Key, *entry.Value); 782 | return true; 783 | } 784 | } 785 | 786 | index = count + 1; 787 | current = default; 788 | return false; 789 | } 790 | 791 | public KeyValuePair Current { 792 | get { 793 | return current; 794 | } 795 | } 796 | 797 | object IEnumerator.Current { 798 | get { 799 | if (index == 0 || index == count + 1) { 800 | throw new InvalidOperationException("Enumeration has either not started or has already finished."); 801 | } 802 | return Current; 803 | } 804 | } 805 | 806 | void IEnumerator.Reset() { 807 | var dictPtr = dict.info.Value; 808 | if (dictPtr == null || version != dictPtr->Version || !dictPtr->ItemsBuffer.HasValue) { 809 | throw new InvalidOperationException("Collection was modified; enumeration operation may not execute."); 810 | } 811 | 812 | index = 0; 813 | offset = 0; 814 | headOffset = 0; 815 | current = default; 816 | } 817 | } 818 | 819 | /// 820 | /// Used in place of `foreach (var kvp in this)` internally since it's more efficient 821 | /// 822 | private struct FastInternalEnumerator { 823 | private ArenaDict dict; 824 | private int index; 825 | private int offset; 826 | private int headOffset; 827 | private int count; 828 | private UnmanagedDict* dictPtr; 829 | private IntPtr itemsPtr; 830 | 831 | internal FastInternalEnumerator(ArenaDict dict) { 832 | this.dict = dict; 833 | dictPtr = dict.info.Value; 834 | index = 0; 835 | offset = 0; 836 | headOffset = 0; 837 | count = dictPtr->BackingArrayLength; 838 | itemsPtr = dictPtr->ItemsBuffer.Value; 839 | } 840 | 841 | public bool GetNextEntry(out Entry result) { 842 | while ((uint)index < (uint)count) { 843 | // get current entry 844 | var entry = dict.GetOffset(dictPtr, itemsPtr, offset); 845 | 846 | // move to next entry associated with the current index 847 | offset = entry.Next; 848 | if (offset == nullOffset) { 849 | // if the position of the next entry is zero then there are no further 850 | // entries at this index, increment the index and set the new offset to 851 | // the head of the list (the entry in the backing array) 852 | headOffset += dictPtr->EntrySize; 853 | offset = headOffset; 854 | index++; 855 | } 856 | 857 | // only entries with a hashcode which isn't zero are valid entries 858 | if (entry.HashCode != noneHashCode) { 859 | result = entry; 860 | return true; 861 | } 862 | } 863 | 864 | index = count + 1; 865 | result = default; 866 | return false; 867 | } 868 | } 869 | 870 | #region Key and value collections 871 | public readonly struct KeyCollection : ICollection, IEnumerable, IReadOnlyCollection { 872 | private readonly ArenaDict dict; 873 | 874 | public KeyCollection(ArenaDict dict) { 875 | this.dict = dict; 876 | } 877 | 878 | public Enumerator GetEnumerator() { 879 | if (dict.Arena is null) { 880 | throw new InvalidOperationException("Cannot GetEnumerator for ArenaDict.KeyCollection: dictionary has not been properly initialized with arena reference"); 881 | } 882 | 883 | var self = dict.info.Value; 884 | if (self == null) { 885 | throw new InvalidOperationException("Cannot GetEnumerator for ArenaDict.KeyCollection: dictionary memory has previously been freed"); 886 | } 887 | 888 | var items = self->ItemsBuffer.Value; 889 | if (items == IntPtr.Zero) { 890 | throw new InvalidOperationException("Cannot GetEnumerator for ArenaDict.KeyCollection: dictionary's backing array has previously been freed"); 891 | } 892 | 893 | return new Enumerator(dict); 894 | } 895 | 896 | public void CopyTo(TKey[] array, int index) { 897 | if (dict.Arena is null) { 898 | throw new InvalidOperationException("Cannot CopyTo on ArenaDict.KeyCollection: dictionary has not been properly initialized with arena reference"); 899 | } 900 | 901 | var self = dict.info.Value; 902 | if (self == null) { 903 | throw new InvalidOperationException("Cannot CopyTo on ArenaDict.KeyCollection: dictionary memory has previously been freed"); 904 | } 905 | 906 | var items = self->ItemsBuffer.Value; 907 | if (items == IntPtr.Zero) { 908 | throw new InvalidOperationException("Cannot CopyTo on ArenaDict.KeyCollection: dictionary's backing array has previously been freed"); 909 | } 910 | 911 | if (array == null) { 912 | throw new ArgumentNullException(nameof(array)); 913 | } 914 | if (array.Rank != 1) { 915 | throw new ArgumentException("Only single dimensional arrays are supported for the requested action.", nameof(array)); 916 | } 917 | if (array.GetLowerBound(0) != 0) { 918 | throw new ArgumentException("The lower bound of target array must be zero.", nameof(array)); 919 | } 920 | if ((uint)index > (uint)array.Length) { 921 | throw new ArgumentOutOfRangeException(nameof(index), "Non-negative number required."); 922 | } 923 | if (array.Length - index < Count) { 924 | throw new ArgumentException("Destination array is not long enough to copy all the items in the collection. Check array index and length.", nameof(array)); 925 | } 926 | 927 | var entryEnumerator = new FastInternalEnumerator(dict); 928 | while (entryEnumerator.GetNextEntry(out var entry)) { 929 | array[index++] = *entry.Key; 930 | } 931 | } 932 | 933 | public int Count => dict.Count; 934 | 935 | bool ICollection.IsReadOnly => true; 936 | 937 | void ICollection.Add(TKey item) => 938 | throw new NotSupportedException("Mutating a value collection derived from a dictionary is not allowed."); 939 | 940 | bool ICollection.Remove(TKey item) => 941 | throw new NotSupportedException("Mutating a value collection derived from a dictionary is not allowed."); 942 | 943 | void ICollection.Clear() => 944 | throw new NotSupportedException("Mutating a value collection derived from a dictionary is not allowed."); 945 | 946 | bool ICollection.Contains(TKey item) => dict.ContainsKey(item); 947 | 948 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 949 | 950 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 951 | 952 | public struct Enumerator : IEnumerator, System.Collections.IEnumerator { 953 | private ArenaDict dict; 954 | private int index; 955 | private int offset; 956 | private int headOffset; 957 | private int version; 958 | private int count; 959 | private TKey currentKey; 960 | 961 | internal Enumerator(ArenaDict dict) { 962 | this.dict = dict; 963 | var dictPtr = dict.info.Value; 964 | index = 0; 965 | offset = 0; 966 | headOffset = 0; 967 | version = dictPtr->Version; 968 | count = dictPtr->BackingArrayLength; 969 | currentKey = default; 970 | } 971 | 972 | public void Dispose() { 973 | } 974 | 975 | public bool MoveNext() { 976 | var dictPtr = dict.info.Value; 977 | if (dictPtr == null || version != dictPtr->Version) { 978 | throw new InvalidOperationException("Collection was modified; enumeration operation may not execute."); 979 | } 980 | 981 | var itemsPtr = dictPtr->ItemsBuffer.Value; 982 | if (itemsPtr == IntPtr.Zero) { 983 | throw new InvalidOperationException("Collection was modified; enumeration operation may not execute."); 984 | } 985 | 986 | while ((uint)index < (uint)count) { 987 | // get current entry 988 | var entry = dict.GetOffset(dictPtr, itemsPtr, offset); 989 | 990 | // move to next entry associated with the current index 991 | offset = entry.Next; 992 | if (offset == nullOffset) { 993 | // if the position of the next entry is zero then there are no further 994 | // entries at this index, increment the index and set the new offset to 995 | // the head of the list (the entry in the backing array) 996 | headOffset += dictPtr->EntrySize; 997 | offset = headOffset; 998 | index++; 999 | } 1000 | 1001 | // only entries with a hashcode which isn't zero are valid entries 1002 | if (entry.HashCode != noneHashCode) { 1003 | currentKey = *entry.Key; 1004 | return true; 1005 | } 1006 | } 1007 | 1008 | index = count + 1; 1009 | currentKey = default; 1010 | return false; 1011 | } 1012 | 1013 | public TKey Current { 1014 | get { 1015 | return currentKey; 1016 | } 1017 | } 1018 | 1019 | object IEnumerator.Current { 1020 | get { 1021 | if (index == 0 || index == count + 1) { 1022 | throw new InvalidOperationException("Enumeration has either not started or has already finished."); 1023 | } 1024 | return Current; 1025 | } 1026 | } 1027 | 1028 | void IEnumerator.Reset() { 1029 | var dictPtr = dict.info.Value; 1030 | if (dictPtr == null || version != dictPtr->Version || !dictPtr->ItemsBuffer.HasValue) { 1031 | throw new InvalidOperationException("Collection was modified; enumeration operation may not execute."); 1032 | } 1033 | 1034 | index = 0; 1035 | offset = 0; 1036 | headOffset = 0; 1037 | currentKey = default; 1038 | } 1039 | } 1040 | } 1041 | 1042 | public readonly struct ValueCollection : ICollection, IEnumerable, IReadOnlyCollection { 1043 | private readonly ArenaDict dict; 1044 | 1045 | public ValueCollection(ArenaDict dict) { 1046 | this.dict = dict; 1047 | } 1048 | 1049 | public Enumerator GetEnumerator() { 1050 | if (dict.Arena is null) { 1051 | throw new InvalidOperationException("Cannot GetEnumerator for ArenaDict.ValueCollection: dictionary has not been properly initialized with arena reference"); 1052 | } 1053 | 1054 | var self = dict.info.Value; 1055 | if (self == null) { 1056 | throw new InvalidOperationException("Cannot GetEnumerator for ArenaDict.ValueCollection: dictionary memory has previously been freed"); 1057 | } 1058 | 1059 | var items = self->ItemsBuffer.Value; 1060 | if (items == IntPtr.Zero) { 1061 | throw new InvalidOperationException("Cannot GetEnumerator for ArenaDict.ValueCollection: dictionary's backing array has previously been freed"); 1062 | } 1063 | 1064 | return new Enumerator(dict); 1065 | } 1066 | 1067 | public void CopyTo(TValue[] array, int index) { 1068 | if (dict.Arena is null) { 1069 | throw new InvalidOperationException("Cannot CopyTo on ArenaDict.ValueCollection: dictionary has not been properly initialized with arena reference"); 1070 | } 1071 | 1072 | var self = dict.info.Value; 1073 | if (self == null) { 1074 | throw new InvalidOperationException("Cannot CopyTo on ArenaDict.ValueCollection: dictionary memory has previously been freed"); 1075 | } 1076 | 1077 | var items = self->ItemsBuffer.Value; 1078 | if (items == IntPtr.Zero) { 1079 | throw new InvalidOperationException("Cannot CopyTo on ArenaDict.ValueCollection: dictionary's backing array has previously been freed"); 1080 | } 1081 | 1082 | if (array == null) { 1083 | throw new ArgumentNullException(nameof(array)); 1084 | } 1085 | if (array.Rank != 1) { 1086 | throw new ArgumentException("Only single dimensional arrays are supported for the requested action.", nameof(array)); 1087 | } 1088 | if (array.GetLowerBound(0) != 0) { 1089 | throw new ArgumentException("The lower bound of target array must be zero.", nameof(array)); 1090 | } 1091 | if ((uint)index > (uint)array.Length) { 1092 | throw new ArgumentOutOfRangeException(nameof(index), "Non-negative number required."); 1093 | } 1094 | if (array.Length - index < Count) { 1095 | throw new ArgumentException("Destination array is not long enough to copy all the items in the collection. Check array index and length.", nameof(array)); 1096 | } 1097 | 1098 | var entryEnumerator = new FastInternalEnumerator(dict); 1099 | while (entryEnumerator.GetNextEntry(out var entry)) { 1100 | array[index++] = *entry.Value; 1101 | } 1102 | } 1103 | 1104 | public int Count => dict.Count; 1105 | 1106 | bool ICollection.IsReadOnly => true; 1107 | 1108 | void ICollection.Add(TValue item) => 1109 | throw new NotSupportedException("Mutating a value collection derived from a dictionary is not allowed."); 1110 | 1111 | bool ICollection.Remove(TValue item) => 1112 | throw new NotSupportedException("Mutating a value collection derived from a dictionary is not allowed."); 1113 | 1114 | void ICollection.Clear() => 1115 | throw new NotSupportedException("Mutating a value collection derived from a dictionary is not allowed."); 1116 | 1117 | bool ICollection.Contains(TValue item) => dict.ContainsValue(item); 1118 | 1119 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 1120 | 1121 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 1122 | 1123 | public struct Enumerator : IEnumerator, System.Collections.IEnumerator { 1124 | private ArenaDict dict; 1125 | private int index; 1126 | private int offset; 1127 | private int headOffset; 1128 | private int version; 1129 | private int count; 1130 | private TValue currentValue; 1131 | 1132 | internal Enumerator(ArenaDict dict) { 1133 | this.dict = dict; 1134 | var dictPtr = dict.info.Value; 1135 | index = 0; 1136 | offset = 0; 1137 | headOffset = 0; 1138 | version = dictPtr->Version; 1139 | count = dictPtr->BackingArrayLength; 1140 | currentValue = default; 1141 | } 1142 | 1143 | public void Dispose() { 1144 | } 1145 | 1146 | public bool MoveNext() { 1147 | var dictPtr = dict.info.Value; 1148 | if (dictPtr == null || version != dictPtr->Version) { 1149 | throw new InvalidOperationException("Collection was modified; enumeration operation may not execute."); 1150 | } 1151 | 1152 | var itemsPtr = dictPtr->ItemsBuffer.Value; 1153 | if (itemsPtr == IntPtr.Zero) { 1154 | throw new InvalidOperationException("Collection was modified; enumeration operation may not execute."); 1155 | } 1156 | 1157 | while ((uint)index < (uint)count) { 1158 | // get current entry 1159 | var entry = dict.GetOffset(dictPtr, itemsPtr, offset); 1160 | 1161 | // move to next entry associated with the current index 1162 | offset = entry.Next; 1163 | if (offset == nullOffset) { 1164 | // if the position of the next entry is zero then there are no further 1165 | // entries at this index, increment the index and set the new offset to 1166 | // the head of the list (the entry in the backing array) 1167 | headOffset += dictPtr->EntrySize; 1168 | offset = headOffset; 1169 | index++; 1170 | } 1171 | 1172 | // only entries with a hashcode which isn't zero are valid entries 1173 | if (entry.HashCode != noneHashCode) { 1174 | currentValue = *entry.Value; 1175 | return true; 1176 | } 1177 | } 1178 | 1179 | index = count + 1; 1180 | currentValue = default; 1181 | return false; 1182 | } 1183 | 1184 | public TValue Current { 1185 | get { 1186 | return currentValue; 1187 | } 1188 | } 1189 | 1190 | object IEnumerator.Current { 1191 | get { 1192 | if (index == 0 || index == count + 1) { 1193 | throw new InvalidOperationException("Enumeration has either not started or has already finished."); 1194 | } 1195 | return Current; 1196 | } 1197 | } 1198 | 1199 | void IEnumerator.Reset() { 1200 | var dictPtr = dict.info.Value; 1201 | if (dictPtr == null || version != dictPtr->Version || !dictPtr->ItemsBuffer.HasValue) { 1202 | throw new InvalidOperationException("Collection was modified; enumeration operation may not execute."); 1203 | } 1204 | 1205 | index = 0; 1206 | offset = 0; 1207 | headOffset = 0; 1208 | currentValue = default; 1209 | } 1210 | } 1211 | } 1212 | #endregion 1213 | } 1214 | 1215 | public static class UnmanagedDictTypes { 1216 | [StructLayout(LayoutKind.Sequential)] 1217 | public struct UnmanagedDict { 1218 | /// 1219 | /// Raw memory buffer that contains dictionary entries 1220 | /// 1221 | public UnmanagedRef ItemsBuffer; 1222 | /// 1223 | /// Length in number of items of the backing array that holds the linked list head entries, 1224 | /// which should always be a power of two 1225 | /// 1226 | public int BackingArrayLength; 1227 | /// 1228 | /// Length in number of items that the remaining raw memory buffer can store, 1229 | /// which should always be greater than or equal to the backing array length 1230 | /// 1231 | public int OverflowLength; 1232 | /// 1233 | /// Number of items in the dictionary 1234 | /// 1235 | public int Count; 1236 | /// 1237 | /// Number of bits to shift to the right to get a valid backing array index 1238 | /// after applying fibonacci hasing (see HashToIndex) 1239 | /// 1240 | public int Shift; 1241 | /// 1242 | /// Version number, increased with every mutation of the contained data 1243 | /// 1244 | public int Version; 1245 | /// 1246 | /// Size of each dictionary entry in bytes 1247 | /// 1248 | public int EntrySize; 1249 | /// 1250 | /// Byte offset from the start of an entry (which contains an UnmanagedDictEntry) 1251 | /// to the entry's key 1252 | /// 1253 | public int KeyOffset; 1254 | /// 1255 | /// Byte offset from the start of an entry (which contains an UnmanagedDictEntry) 1256 | /// to the entry's value 1257 | /// 1258 | public int ValueOffset; 1259 | /// 1260 | /// The head of the freelist of entries removed from the dictionary which resided 1261 | /// outside of the backing array, which is used to allocate entries if not zero. 1262 | /// This is the offset in bytes inside the raw memory buffer to get to the entry 1263 | /// 1264 | public int Head; 1265 | /// 1266 | /// Position of the bump allocator, used to allocate entries when the freelist is 1267 | /// empty. This is the offset in bytes inside the raw memory buffer to get to the entry 1268 | /// 1269 | public int Bump; 1270 | } 1271 | 1272 | [StructLayout(LayoutKind.Sequential)] 1273 | public struct UnmanagedDictEntry { 1274 | public int HashCode; 1275 | public int Next; 1276 | 1277 | public UnmanagedDictEntry(int hashCode, int next) { 1278 | HashCode = hashCode; 1279 | Next = next; 1280 | } 1281 | } 1282 | 1283 | internal unsafe readonly struct ArenaDictDebugView where TKey : unmanaged where TValue : unmanaged { 1284 | private readonly ArenaDict dict; 1285 | 1286 | public ArenaDictDebugView(ArenaDict dict) { 1287 | this.dict = dict; 1288 | } 1289 | 1290 | public KeyValuePair[] Items { 1291 | get { 1292 | var items = new KeyValuePair[dict.Count]; 1293 | ((ICollection>)dict).CopyTo(items, 0); 1294 | return items; 1295 | } 1296 | } 1297 | 1298 | public int Count { get { return dict.Count; } } 1299 | public Arena Arena { get { return dict.Arena; } } 1300 | public bool IsAllocated { get { return dict.IsAllocated; } } 1301 | } 1302 | } 1303 | } 1304 | -------------------------------------------------------------------------------- /ArenaID.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Runtime.InteropServices; 5 | using System.Text; 6 | 7 | namespace Arenas { 8 | [StructLayout(LayoutKind.Sequential)] 9 | public readonly struct ArenaID : IEquatable { 10 | public readonly int Value; 11 | 12 | public ArenaID(int value) { 13 | Value = value; 14 | } 15 | 16 | #region Equality 17 | public override bool Equals(object obj) { 18 | return obj is ArenaID iD && 19 | Value == iD.Value; 20 | } 21 | 22 | public bool Equals(ArenaID other) { 23 | return other.Value == Value; 24 | } 25 | 26 | public override int GetHashCode() { 27 | return 710438321 + Value.GetHashCode(); 28 | } 29 | 30 | public static bool operator ==(ArenaID left, ArenaID right) { 31 | return left.Equals(right); 32 | } 33 | 34 | public static bool operator !=(ArenaID left, ArenaID right) { 35 | return !(left == right); 36 | } 37 | #endregion 38 | 39 | public override string ToString() { 40 | return Value.ToString("x", CultureInfo.InvariantCulture); 41 | } 42 | 43 | public static ArenaID NewID() { 44 | return new ArenaID((int)LFSR.Shift()); 45 | } 46 | 47 | private static readonly ArenaID empty = new ArenaID(0); 48 | public static ArenaID Empty { get { return empty; } } 49 | 50 | /// 51 | /// A maximum length 32-bit linear feedback shift register which produces ID numbers 52 | /// 53 | private static class LFSR { 54 | /// 55 | /// 32-bit term that produces a maximum-length LFSR 56 | /// 57 | private const uint feedbackTerm = 0x80000EA6; 58 | 59 | private static uint value; 60 | private static object lfsrLock = new object(); 61 | 62 | static LFSR() { 63 | value = 0x0BADCAFE; 64 | } 65 | 66 | public static uint Shift() { 67 | uint ret; 68 | lock (lfsrLock) { 69 | if (value == feedbackTerm) { 70 | ret = value = 0; 71 | } 72 | else { 73 | if (value == 0) { 74 | ret = value = feedbackTerm; 75 | } 76 | 77 | if ((value & 1) == 1) { 78 | ret = value = (value >> 1) ^ feedbackTerm; 79 | } 80 | else { 81 | ret = value = value >> 1; 82 | } 83 | } 84 | } 85 | if (ret == 0) { 86 | return Shift(); 87 | } 88 | return ret; 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /ArenaList.cs: -------------------------------------------------------------------------------- 1 | using Arenas; 2 | using System; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | using System.Linq; 7 | using System.Runtime.InteropServices; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | namespace Arenas { 12 | using static UnmanagedListTypes; 13 | 14 | [DebuggerTypeProxy(typeof(ArenaListDebugView<>))] 15 | public unsafe struct ArenaList : IList, IDisposable where T : unmanaged { 16 | private const int defaultCapacity = 4; 17 | 18 | private UnmanagedRef info; 19 | 20 | private ArenaList(UnmanagedRef listData) { 21 | info = listData; 22 | } 23 | 24 | public ArenaList(Arena arena, int capacity = defaultCapacity) { 25 | if (arena is null) { 26 | throw new ArgumentNullException(nameof(arena)); 27 | } 28 | if (TypeInfo.GenerateTypeInfo().IsArenaContents) { 29 | throw new NotSupportedException("ArenaList cannot store items which implement IArenaContents. Please use UnmanagedRef instead."); 30 | } 31 | 32 | info = arena.Allocate(new UnmanagedList()); 33 | var self = info.Value; 34 | 35 | var minCapacity = Math.Max(capacity, defaultCapacity); 36 | var itemsRef = arena.AllocCount(minCapacity); 37 | 38 | self->Items = (UnmanagedRef)itemsRef; 39 | self->Capacity = itemsRef.ElementCount; // we might get more capacity than requested 40 | } 41 | 42 | public void Free() { 43 | if (Arena is null) { 44 | throw new InvalidOperationException("Cannot Free ArenaList: list has not been properly initialized with arena reference"); 45 | } 46 | 47 | var self = info.Value; 48 | if (self == null) { 49 | throw new InvalidOperationException("Cannot Free ArenaList: list memory has previously been freed"); 50 | } 51 | 52 | self->Version++; 53 | var items = self->Items; 54 | Arena.Free(items); 55 | Arena.Free(info); 56 | info = default; 57 | } 58 | 59 | public void Dispose() { 60 | if (!IsAllocated) { 61 | return; 62 | } 63 | Free(); 64 | } 65 | 66 | public void Clear() { 67 | if (Arena is null) { 68 | throw new InvalidOperationException("Cannot Clear ArenaList: list has not been properly initialized with arena reference"); 69 | } 70 | 71 | var self = info.Value; 72 | if (self == null) { 73 | throw new InvalidOperationException("Cannot Clear ArenaList: list memory has previously been freed"); 74 | } 75 | 76 | self->Version++; 77 | self->Count = 0; 78 | } 79 | 80 | private void Copy(UnmanagedList* self, T* items, int sourceIndex, int destIndex, int count) { 81 | Debug.Assert(destIndex + count <= self->Capacity, "Bad ArenaList copy"); 82 | var source = items + sourceIndex; 83 | var dest = items + destIndex; 84 | var destSize = (self->Capacity - destIndex) * sizeof(T); 85 | var bytesToCopy = (self->Count - sourceIndex) * sizeof(T); 86 | Buffer.MemoryCopy(source, dest, destSize, bytesToCopy); 87 | } 88 | 89 | private void AddCapacity(UnmanagedList* self, ref T* items) { 90 | if (self->Count < self->Capacity) { 91 | return; 92 | } 93 | 94 | var newMinCapacity = self->Capacity * 2; 95 | 96 | var newItems = Arena.AllocCount(newMinCapacity); 97 | self->Capacity = newItems.ElementCount; // we might get more capacity than requested 98 | 99 | var newSize = newItems.Size; 100 | var newItemsPtr = newItems.Value; 101 | 102 | Buffer.MemoryCopy(items, newItemsPtr, newSize, newSize); 103 | 104 | Arena.Free(self->Items); 105 | self->Items = (UnmanagedRef)newItems; 106 | 107 | items = newItemsPtr; 108 | } 109 | 110 | public void Add(T item) { 111 | if (Arena is null) { 112 | throw new InvalidOperationException("Cannot Add item to ArenaList: list has not been properly initialized with arena reference"); 113 | } 114 | 115 | var self = info.Value; 116 | if (self == null) { 117 | throw new InvalidOperationException("Cannot Add item to ArenaList: list memory has previously been freed"); 118 | } 119 | 120 | var items = (T*)self->Items.Value; 121 | if (items == null) { 122 | throw new InvalidOperationException("Cannot Add item to ArenaList: list's backing array has previously been freed"); 123 | } 124 | 125 | self->Version++; 126 | AddCapacity(self, ref items); 127 | items[self->Count] = item; 128 | self->Count++; 129 | } 130 | 131 | public void Insert(int index, T item) { 132 | if (Arena is null) { 133 | throw new InvalidOperationException("Cannot Insert item into ArenaList: list has not been properly initialized with arena reference"); 134 | } 135 | 136 | var self = info.Value; 137 | if (self == null) { 138 | throw new InvalidOperationException("Cannot Insert item into ArenaList: list memory has previously been freed"); 139 | } 140 | 141 | var items = (T*)self->Items.Value; 142 | if (items == null) { 143 | throw new InvalidOperationException("Cannot Insert item into ArenaList: list's backing array has previously been freed"); 144 | } 145 | 146 | if (index < 0 || index >= Count) { 147 | throw new ArgumentOutOfRangeException(nameof(index)); 148 | } 149 | 150 | self->Version++; 151 | AddCapacity(self, ref items); 152 | 153 | if (index == Count) { 154 | items[self->Count] = item; 155 | self->Count++; 156 | return; 157 | } 158 | 159 | Copy(self, items, index, index + 1, Count - index); 160 | items[index] = item; 161 | self->Count++; 162 | } 163 | 164 | public void RemoveAt(int index) { 165 | if (Arena is null) { 166 | throw new InvalidOperationException("Cannot RemoveAt in ArenaList: list has not been properly initialized with arena reference"); 167 | } 168 | 169 | var self = info.Value; 170 | if (self == null) { 171 | throw new InvalidOperationException("Cannot RemoveAt in ArenaList: list memory has previously been freed"); 172 | } 173 | 174 | var items = (T*)self->Items.Value; 175 | if (items == null) { 176 | throw new InvalidOperationException("Cannot RemoveAt in ArenaList: list's backing array has previously been freed"); 177 | } 178 | 179 | RemoveAt(self, items, index); 180 | } 181 | 182 | private void RemoveAt(UnmanagedList* self, T* items, int index) { 183 | if (index < 0 || index >= Count) { 184 | throw new ArgumentOutOfRangeException(nameof(index)); 185 | } 186 | 187 | self->Version++; 188 | 189 | if (index == Count - 1) { 190 | self->Count--; 191 | return; 192 | } 193 | 194 | self->Count--; 195 | Copy(self, items, index + 1, index, Count - index); 196 | } 197 | 198 | public bool Remove(T item) { 199 | if (Arena is null) { 200 | throw new InvalidOperationException("Cannot Remove item from ArenaList: list has not been properly initialized with arena reference"); 201 | } 202 | 203 | var self = info.Value; 204 | if (self == null) { 205 | throw new InvalidOperationException("Cannot Remove item from ArenaList: list memory has previously been freed"); 206 | } 207 | 208 | var items = (T*)self->Items.Value; 209 | if (items == null) { 210 | throw new InvalidOperationException("Cannot Remove item from ArenaList: list's backing array has previously been freed"); 211 | } 212 | 213 | var index = IndexOf(self, items, item); 214 | if (index < 0) { 215 | return false; 216 | } 217 | 218 | RemoveAt(self, items, index); 219 | return true; 220 | } 221 | 222 | public int IndexOf(T item) { 223 | if (Arena is null) { 224 | throw new InvalidOperationException("Cannot get IndexOf item in ArenaList: list has not been properly initialized with arena reference"); 225 | } 226 | 227 | var self = info.Value; 228 | if (self == null) { 229 | throw new InvalidOperationException("Cannot get IndexOf item in ArenaList: list memory has previously been freed"); 230 | } 231 | 232 | var items = (T*)self->Items.Value; 233 | if (items == null) { 234 | throw new InvalidOperationException("Cannot get IndexOf item in ArenaList: list's backing array has previously been freed"); 235 | } 236 | 237 | return IndexOf(self, items, item); 238 | } 239 | 240 | private int IndexOf(UnmanagedList* self, T* items, T item) { 241 | var count = Count; 242 | var cur = items; 243 | 244 | for (int i = 0; i < count; i++) { 245 | if (EqualityComparer.Default.Equals(*(cur++), item)) { 246 | return i; 247 | } 248 | } 249 | 250 | return -1; 251 | } 252 | 253 | public bool Contains(T item) { 254 | if (Arena is null) { 255 | throw new InvalidOperationException("Cannot check if ArenaList: list has not been properly initialized with arena reference"); 256 | } 257 | 258 | var self = info.Value; 259 | if (self == null) { 260 | throw new InvalidOperationException("Cannot check if ArenaList Contains item: list memory has previously been freed"); 261 | } 262 | 263 | var items = (T*)self->Items.Value; 264 | if (items == null) { 265 | throw new InvalidOperationException("Cannot check if ArenaList Contains item: list's backing array has previously been freed"); 266 | } 267 | 268 | return IndexOf(self, items, item) >= 0; 269 | } 270 | 271 | public void CopyTo(T[] dest) { 272 | CopyTo(0, dest, 0, Count); 273 | } 274 | 275 | public void CopyTo(T[] dest, int destIndex) { 276 | CopyTo(0, dest, destIndex, Count); 277 | } 278 | 279 | public void CopyTo(int sourceIndex, T[] dest, int destIndex, int count) { 280 | if (Arena is null) { 281 | throw new InvalidOperationException("Cannot CopyTo array from ArenaList: list has not been properly initialized with arena reference"); 282 | } 283 | 284 | var self = info.Value; 285 | if (self == null) { 286 | throw new InvalidOperationException("Cannot CopyTo array from ArenaList: list memory has previously been freed"); 287 | } 288 | 289 | var items = self->Items; 290 | if (!items.HasValue) { 291 | throw new InvalidOperationException("Cannot CopyTo array from ArenaList: list's backing array has previously been freed"); 292 | } 293 | 294 | if (count < 0) { 295 | throw new ArgumentOutOfRangeException(nameof(count)); 296 | } 297 | if (destIndex < 0 || destIndex + count > dest.Length) { 298 | throw new ArgumentOutOfRangeException(nameof(destIndex)); 299 | } 300 | if (sourceIndex < 0 || sourceIndex + count > Count) { 301 | throw new ArgumentOutOfRangeException(nameof(sourceIndex)); 302 | } 303 | 304 | items.CopyTo(dest, destIndex, sourceIndex, count); 305 | } 306 | 307 | public Enumerator GetEnumerator() { 308 | if (Arena is null) { 309 | throw new InvalidOperationException("Cannot GetEnumerator for ArenaList: list has not been properly initialized with arena reference"); 310 | } 311 | 312 | var self = info.Value; 313 | if (self == null) { 314 | throw new InvalidOperationException("Cannot GetEnumerator for ArenaList: list memory has previously been freed"); 315 | } 316 | 317 | var items = self->Items; 318 | if (!items.HasValue) { 319 | throw new InvalidOperationException("Cannot GetEnumerator for ArenaList: list's backing array has previously been freed"); 320 | } 321 | 322 | return new Enumerator(this); 323 | } 324 | 325 | IEnumerator IEnumerable.GetEnumerator() { 326 | return GetEnumerator(); 327 | } 328 | 329 | IEnumerator IEnumerable.GetEnumerator() { 330 | return GetEnumerator(); 331 | } 332 | 333 | public UnmanagedRef GetUnderlyingReference() { 334 | return info; 335 | } 336 | 337 | public static explicit operator ArenaList(UnmanagedRef listData) { 338 | return new ArenaList(listData); 339 | } 340 | 341 | public T this[int index] { 342 | get { 343 | if (Arena is null) { 344 | throw new InvalidOperationException("Cannot get item at index in ArenaList: list has not been properly initialized with arena reference"); 345 | } 346 | 347 | var self = info.Value; 348 | if (self == null) { 349 | throw new InvalidOperationException("Cannot get item at index in ArenaList: list memory has previously been freed"); 350 | } 351 | 352 | var items = (T*)self->Items.Value; 353 | if (items == null) { 354 | throw new InvalidOperationException("Cannot get item at index in ArenaList: list's backing array has previously been freed"); 355 | } 356 | 357 | if (index < 0 || index >= self->Count) { 358 | throw new IndexOutOfRangeException(); 359 | } 360 | 361 | return items[index]; 362 | } 363 | set { 364 | if (Arena is null) { 365 | throw new InvalidOperationException("Cannot set item at index in ArenaList: list has not been properly initialized with arena reference"); 366 | } 367 | 368 | var self = info.Value; 369 | if (self == null) { 370 | throw new InvalidOperationException("Cannot set item at index in ArenaList: list memory has previously been freed"); 371 | } 372 | 373 | var items = (T*)self->Items.Value; 374 | if (items == null) { 375 | throw new InvalidOperationException("Cannot get item at index in ArenaList: list's backing array has previously been freed"); 376 | } 377 | 378 | if (index < 0 || index >= self->Count) { 379 | throw new IndexOutOfRangeException(); 380 | } 381 | 382 | self->Version++; 383 | items[index] = value; 384 | } 385 | } 386 | 387 | public int Count { 388 | get { 389 | var self = info.Value; 390 | if (self == null) { 391 | return 0; 392 | } 393 | return self->Count; 394 | } 395 | } 396 | 397 | public bool IsAllocated { get { return info.HasValue; } } 398 | public Arena Arena { get { return info.Arena; } } 399 | bool ICollection.IsReadOnly { get { return false; } } 400 | 401 | [Serializable] 402 | public struct Enumerator : IEnumerator, System.Collections.IEnumerator { 403 | private ArenaList list; 404 | private int index; 405 | private int version; 406 | private int count; 407 | private T current; 408 | 409 | internal Enumerator(ArenaList list) { 410 | this.list = list; 411 | var listPtr = list.info.Value; 412 | index = 0; 413 | count = listPtr->Count; 414 | version = listPtr->Version; 415 | current = default; 416 | } 417 | 418 | public void Dispose() { 419 | } 420 | 421 | public bool MoveNext() { 422 | var listPtr = list.info.Value; 423 | 424 | if (listPtr != null && version == listPtr->Version && ((uint)index < (uint)count)) { 425 | var items = (T*)listPtr->Items.Value; 426 | if (items != null) { 427 | current = items[index]; 428 | index++; 429 | return true; 430 | } 431 | } 432 | return MoveNextRare(); 433 | } 434 | 435 | private bool MoveNextRare() { 436 | var listPtr = list.info.Value; 437 | if (listPtr == null || version != listPtr->Version || !listPtr->Items.HasValue) { 438 | throw new InvalidOperationException("Collection was modified; enumeration operation may not execute."); 439 | } 440 | 441 | index = count + 1; 442 | current = default; 443 | return false; 444 | } 445 | 446 | public T Current { 447 | get { 448 | return current; 449 | } 450 | } 451 | 452 | object IEnumerator.Current { 453 | get { 454 | if (index == 0 || index == count + 1) { 455 | throw new InvalidOperationException("Enumeration has either not started or has already finished."); 456 | } 457 | return Current; 458 | } 459 | } 460 | 461 | void IEnumerator.Reset() { 462 | var listPtr = list.info.Value; 463 | if (listPtr == null || version != listPtr->Version || !listPtr->Items.HasValue) { 464 | throw new InvalidOperationException("Collection was modified; enumeration operation may not execute."); 465 | } 466 | 467 | index = 0; 468 | current = default; 469 | } 470 | } 471 | } 472 | 473 | public static class UnmanagedListTypes { 474 | [StructLayout(LayoutKind.Sequential)] 475 | public struct UnmanagedList { 476 | public UnmanagedRef Items; 477 | public int Count; 478 | public int Capacity; 479 | public int Version; 480 | } 481 | 482 | internal unsafe readonly struct ArenaListDebugView where T : unmanaged { 483 | private readonly ArenaList list; 484 | 485 | public ArenaListDebugView(ArenaList list) { 486 | this.list = list; 487 | } 488 | 489 | public T[] Items { 490 | get { 491 | var items = new T[list.Count]; 492 | list.CopyTo(items, 0); 493 | return items; 494 | } 495 | } 496 | 497 | public int Count { get { return list.Count; } } 498 | public Arena Arena { get { return list.Arena; } } 499 | public bool IsAllocated { get { return list.IsAllocated; } } 500 | } 501 | } 502 | } 503 | -------------------------------------------------------------------------------- /ArenaListExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Arenas { 6 | public static class ArenaListExtensions { 7 | public static void Add(this ArenaList list, in T uref) where T : IUnmanagedRef { 8 | list.Add(uref.Reference); 9 | } 10 | 11 | public static void Add(this ArenaList list, in T uref) where T : IUnmanagedRef { 12 | var pointer = uref.Reference.Value; 13 | if (pointer == IntPtr.Zero) { 14 | throw new ArgumentNullException(nameof(uref)); 15 | } 16 | list.Add(pointer); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ArenaPool.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Arenas { 6 | public interface IArenaPoolable { 7 | bool ResetForPool(); 8 | } 9 | 10 | public class ArenaPool : ManagedObjectPool { 11 | private IMemoryAllocator allocator; 12 | private int pageSize; 13 | 14 | public ArenaPool(IMemoryAllocator allocator = null, int pageSize = Arena.DefaultPageSize) 15 | : base() { 16 | this.allocator = allocator; 17 | this.pageSize = pageSize; 18 | createInstance = CreateInstance; 19 | resetInstance = ResetInstance; 20 | } 21 | 22 | private Arena CreateInstance() { 23 | return new Arena(allocator ?? Arena.DefaultAllocator, pageSize); 24 | } 25 | 26 | private bool ResetInstance(Arena arena) { 27 | return ((IArenaPoolable)arena).ResetForPool(); 28 | } 29 | 30 | private static ArenaPool defaultPool = new ArenaPool(); 31 | public static ArenaPool Default { get { return defaultPool; } set { defaultPool = value; } } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ArenaStringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Arenas { 6 | public static class ArenaStringExtensions { 7 | public static bool IsNullOrEmpty(this ArenaString value) { 8 | return value.Length == 0; 9 | } 10 | 11 | public static bool IsNullOrWhiteSpace(this ArenaString value) { 12 | if (!value.IsAllocated) { 13 | return true; 14 | } 15 | var trimmed = value.Trim(); 16 | try { 17 | return trimmed.Length == 0; 18 | } 19 | finally { 20 | trimmed.Free(); 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Arenas.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | true 9 | 10 | 11 | 12 | true 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Arenas.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.8.34322.80 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Arenas", "Arenas.csproj", "{FC0F371D-10A4-4A3D-B3F8-A3710921F7A2}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArenasTest", "ArenasTest\ArenasTest.csproj", "{C3BA8364-E22D-428E-8D7F-558403371451}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ArenasTestCore", "ArenasTestCore\ArenasTestCore.csproj", "{8C7008F2-8A1A-419B-BD65-602D3B2B7991}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Debug|x64 = Debug|x64 16 | Debug|x86 = Debug|x86 17 | Release|Any CPU = Release|Any CPU 18 | Release|x64 = Release|x64 19 | Release|x86 = Release|x86 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {FC0F371D-10A4-4A3D-B3F8-A3710921F7A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {FC0F371D-10A4-4A3D-B3F8-A3710921F7A2}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {FC0F371D-10A4-4A3D-B3F8-A3710921F7A2}.Debug|x64.ActiveCfg = Debug|Any CPU 25 | {FC0F371D-10A4-4A3D-B3F8-A3710921F7A2}.Debug|x64.Build.0 = Debug|Any CPU 26 | {FC0F371D-10A4-4A3D-B3F8-A3710921F7A2}.Debug|x86.ActiveCfg = Debug|Any CPU 27 | {FC0F371D-10A4-4A3D-B3F8-A3710921F7A2}.Debug|x86.Build.0 = Debug|Any CPU 28 | {FC0F371D-10A4-4A3D-B3F8-A3710921F7A2}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {FC0F371D-10A4-4A3D-B3F8-A3710921F7A2}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {FC0F371D-10A4-4A3D-B3F8-A3710921F7A2}.Release|x64.ActiveCfg = Release|Any CPU 31 | {FC0F371D-10A4-4A3D-B3F8-A3710921F7A2}.Release|x64.Build.0 = Release|Any CPU 32 | {FC0F371D-10A4-4A3D-B3F8-A3710921F7A2}.Release|x86.ActiveCfg = Release|Any CPU 33 | {FC0F371D-10A4-4A3D-B3F8-A3710921F7A2}.Release|x86.Build.0 = Release|Any CPU 34 | {C3BA8364-E22D-428E-8D7F-558403371451}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {C3BA8364-E22D-428E-8D7F-558403371451}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {C3BA8364-E22D-428E-8D7F-558403371451}.Debug|x64.ActiveCfg = Debug|x64 37 | {C3BA8364-E22D-428E-8D7F-558403371451}.Debug|x64.Build.0 = Debug|x64 38 | {C3BA8364-E22D-428E-8D7F-558403371451}.Debug|x86.ActiveCfg = Debug|x86 39 | {C3BA8364-E22D-428E-8D7F-558403371451}.Debug|x86.Build.0 = Debug|x86 40 | {C3BA8364-E22D-428E-8D7F-558403371451}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {C3BA8364-E22D-428E-8D7F-558403371451}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {C3BA8364-E22D-428E-8D7F-558403371451}.Release|x64.ActiveCfg = Release|x64 43 | {C3BA8364-E22D-428E-8D7F-558403371451}.Release|x64.Build.0 = Release|x64 44 | {C3BA8364-E22D-428E-8D7F-558403371451}.Release|x86.ActiveCfg = Release|x86 45 | {C3BA8364-E22D-428E-8D7F-558403371451}.Release|x86.Build.0 = Release|x86 46 | {8C7008F2-8A1A-419B-BD65-602D3B2B7991}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {8C7008F2-8A1A-419B-BD65-602D3B2B7991}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {8C7008F2-8A1A-419B-BD65-602D3B2B7991}.Debug|x64.ActiveCfg = Debug|x64 49 | {8C7008F2-8A1A-419B-BD65-602D3B2B7991}.Debug|x64.Build.0 = Debug|x64 50 | {8C7008F2-8A1A-419B-BD65-602D3B2B7991}.Debug|x86.ActiveCfg = Debug|x86 51 | {8C7008F2-8A1A-419B-BD65-602D3B2B7991}.Debug|x86.Build.0 = Debug|x86 52 | {8C7008F2-8A1A-419B-BD65-602D3B2B7991}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {8C7008F2-8A1A-419B-BD65-602D3B2B7991}.Release|Any CPU.Build.0 = Release|Any CPU 54 | {8C7008F2-8A1A-419B-BD65-602D3B2B7991}.Release|x64.ActiveCfg = Release|x64 55 | {8C7008F2-8A1A-419B-BD65-602D3B2B7991}.Release|x64.Build.0 = Release|x64 56 | {8C7008F2-8A1A-419B-BD65-602D3B2B7991}.Release|x86.ActiveCfg = Release|x86 57 | {8C7008F2-8A1A-419B-BD65-602D3B2B7991}.Release|x86.Build.0 = Release|x86 58 | EndGlobalSection 59 | GlobalSection(SolutionProperties) = preSolution 60 | HideSolutionNode = FALSE 61 | EndGlobalSection 62 | GlobalSection(ExtensibilityGlobals) = postSolution 63 | SolutionGuid = {C6CBDCF3-5F2F-47FB-B111-20D04F80709D} 64 | EndGlobalSection 65 | EndGlobal 66 | -------------------------------------------------------------------------------- /ArenasTest/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ArenasTest/ArenasTest.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {C3BA8364-E22D-428E-8D7F-558403371451} 8 | Exe 9 | ArenasTest 10 | ArenasTest 11 | v4.8.1 12 | 512 13 | true 14 | true 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | true 26 | 27 | 28 | AnyCPU 29 | pdbonly 30 | true 31 | bin\Release\ 32 | TRACE 33 | prompt 34 | 4 35 | true 36 | 37 | 38 | true 39 | bin\x64\Debug\ 40 | DEBUG;TRACE 41 | true 42 | full 43 | x64 44 | 7.3 45 | prompt 46 | true 47 | 48 | 49 | bin\x64\Release\ 50 | TRACE 51 | true 52 | true 53 | pdbonly 54 | x64 55 | 7.3 56 | prompt 57 | true 58 | 59 | 60 | true 61 | bin\x86\Debug\ 62 | DEBUG;TRACE 63 | true 64 | full 65 | x86 66 | 7.3 67 | prompt 68 | true 69 | 70 | 71 | bin\x86\Release\ 72 | TRACE 73 | true 74 | true 75 | pdbonly 76 | x86 77 | 7.3 78 | prompt 79 | true 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | {FC0F371D-10A4-4A3D-B3F8-A3710921F7A2} 102 | Arenas 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /ArenasTest/Person.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace Arenas { 6 | [StructLayout(LayoutKind.Sequential)] 7 | unsafe public struct Person : IArenaContents { 8 | // these two lines are boilerplate for IArenaContents structs 9 | ArenaID IArenaContents.ArenaID { get; set; } 10 | IArenaMethods IArenaContents.ArenaMethods { get => ArenaMethods.Instance; } 11 | 12 | private ManagedRef firstName; 13 | private ManagedRef lastName; 14 | 15 | public override string ToString() { 16 | return $"{FirstName} {LastName}"; 17 | } 18 | 19 | public void Free() { 20 | // free managed references by setting to null 21 | FirstName = null; 22 | LastName = null; 23 | } 24 | 25 | public string FirstName { 26 | get { return firstName.Get(); } 27 | set { firstName = firstName.Set(ref this, value); } 28 | } 29 | public string LastName { 30 | get { return lastName.Get(); } 31 | set { lastName = lastName.Set(ref this, value); } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ArenasTest/Program.cs: -------------------------------------------------------------------------------- 1 | using Arenas; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Linq; 6 | using System.Runtime.InteropServices; 7 | using System.Security.Policy; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | namespace ArenasTest { 12 | class Program { 13 | static unsafe void Main(string[] args) { 14 | UnmanagedRef staleRefTest; 15 | 16 | using (var arena = new Arena()) { 17 | // allocate some people in the arena 18 | var john = arena.Allocate(new Person()); 19 | john.Value->FirstName = "John"; 20 | john.Value->LastName = "Doe"; 21 | 22 | var jack = arena.Allocate(new Person()); 23 | jack.Value->FirstName = "Jack"; 24 | jack.Value->LastName = "Black"; 25 | 26 | Console.WriteLine($"Size of UnmanagedRef: {sizeof(UnmanagedRef)}"); 27 | 28 | Console.WriteLine(john); 29 | Console.WriteLine(jack); 30 | 31 | // make a list of integers in the arena 32 | var list = new ArenaList(arena) { 1, 2, 3 }; 33 | for (int i = 10; i < 22; i++) { 34 | list.Add(i); 35 | } 36 | 37 | Console.WriteLine("Values in list:"); 38 | foreach (var i in list) { 39 | Console.WriteLine(i); 40 | } 41 | 42 | // make a dictionary of integers in the arena 43 | var dict = new ArenaDict(arena); 44 | var random = new Random(12345); 45 | for (int i = 0; i < 20; i++) { 46 | dict[random.Next(1000)] = random.Next(1000); 47 | } 48 | 49 | Console.WriteLine("Values in dictionary (sorted by key ascending):"); 50 | foreach (var kvp in from entry in dict orderby entry.Key ascending select entry) { 51 | Console.WriteLine(kvp); 52 | } 53 | 54 | Console.WriteLine("Items in arena:"); 55 | foreach (var item in arena) { 56 | Console.WriteLine(item); 57 | } 58 | 59 | // free an item 60 | arena.Free(jack); 61 | 62 | Console.WriteLine("Items in arena after freeing:"); 63 | foreach (var item in arena) { 64 | Console.WriteLine(item); 65 | } 66 | 67 | // free the rest and show that our references are stale 68 | arena.Clear(); 69 | Console.WriteLine($"Does stale reference have a value? {john.HasValue}"); 70 | 71 | // make some random bytes using a Guid 72 | var guid = Guid.NewGuid(); 73 | var guidBytes = guid.ToByteArray(); 74 | 75 | // allocate a buffer for the bytes in the arena and copy them 76 | var unmanagedBytes = arena.AllocCount(guidBytes.Length); 77 | Marshal.Copy(guidBytes, 0, (IntPtr)unmanagedBytes.Value, guidBytes.Length); 78 | 79 | // check if the bytes are the same 80 | var isSame = true; 81 | for (int i = 0; i < guidBytes.Length; i++) { 82 | if (guidBytes[i] != *unmanagedBytes[i]) { 83 | isSame = false; 84 | break; 85 | } 86 | } 87 | Console.WriteLine(isSame ? "ArenaID bytes match" : "ArenaID bytes don't match"); 88 | 89 | // split a string into a bunch of ArenaString instances 90 | using (var splitResults = ArenaString.Split(arena, "Lorem ipsum dolor sit amet", ' ')) { 91 | for (int i = 0; i < splitResults.Count; i++) { 92 | var str = splitResults[i]; 93 | Console.WriteLine(str); 94 | str.Free(); 95 | } 96 | } 97 | 98 | // final stale reference test for disposal 99 | staleRefTest = arena.Allocate(new Person()); 100 | staleRefTest.Value->FirstName = "Stale"; 101 | staleRefTest.Value->LastName = "Reference"; 102 | } 103 | 104 | Console.WriteLine($"Does stale reference have a value after disposal? {staleRefTest.HasValue}"); 105 | 106 | Console.WriteLine(); 107 | Console.WriteLine("Running unmanaged list of pointers code"); 108 | UnmanagedPtrList(); 109 | 110 | Console.WriteLine(); 111 | Console.WriteLine("Running arenas in arenas"); 112 | ArenaArenas(); 113 | } 114 | 115 | static unsafe void UnmanagedPtrList() { 116 | using (var arena = new Arena()) { 117 | // allocate a list 118 | var people = new ArenaList(arena); 119 | 120 | // allocate some people references 121 | var john = arena.Allocate(new Person()); 122 | john.Value->FirstName = "John"; 123 | john.Value->LastName = "Doe"; 124 | 125 | var jack = arena.Allocate(new Person()); 126 | jack.Value->FirstName = "Jack"; 127 | jack.Value->LastName = "Black"; 128 | 129 | // store references inside the list 130 | people.Add(john); 131 | people.Add(jack); 132 | 133 | // iterate over unmanaged list and write out all the people 134 | foreach (var item in people) { 135 | var person = item.As(); 136 | Console.WriteLine(*person); 137 | } 138 | 139 | people.Free(); 140 | } 141 | } 142 | 143 | private static Arena parentArena = new Arena(); 144 | 145 | private unsafe class ArenaAllocator : IMemoryAllocator { 146 | public MemoryAllocation Allocate(int sizeBytes) { 147 | var alloc = parentArena.AllocCount(sizeBytes); 148 | return new MemoryAllocation((IntPtr)alloc.Value, alloc.Size); 149 | } 150 | 151 | public void Free(IntPtr ptr) => parentArena.Free(ptr); 152 | } 153 | 154 | static unsafe void ArenaArenas() { 155 | // by using a page size of 2048 we're actually guaranteeing this allocator 156 | // will use pages of ~4k, because the size is rounded to the next power of 157 | // two after adding the item header size 158 | using (var childArena = new Arena(new ArenaAllocator(), 2048)) { 159 | var john = childArena.Allocate(new Person()); 160 | john.Value->FirstName = "John"; 161 | john.Value->LastName = "Doe"; 162 | 163 | var jack = childArena.Allocate(new Person()); 164 | jack.Value->FirstName = "Jack"; 165 | jack.Value->LastName = "Black"; 166 | 167 | Console.WriteLine("Child arena:"); 168 | foreach (var item in childArena) Console.WriteLine(item); 169 | Console.WriteLine("Parent arena:"); 170 | foreach (var item in parentArena) Console.WriteLine(item); 171 | } 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /ArenasTest/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("ArenasTest")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("ArenasTest")] 13 | [assembly: AssemblyCopyright("Copyright © 2023")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("c3ba8364-e22d-428e-8d7f-558403371451")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /ArenasTestCore/ArenasTestCore.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | AnyCPU;x64;x86 7 | true 8 | 9 | 10 | 11 | true 12 | 13 | 14 | 15 | true 16 | 17 | 18 | 19 | true 20 | 21 | 22 | 23 | true 24 | 25 | 26 | 27 | true 28 | 29 | 30 | 31 | true 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /ArenasTestCore/Program.cs: -------------------------------------------------------------------------------- 1 | using Arenas; 2 | using System; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace ArenasTestCore { 6 | class Program { 7 | unsafe static void Main(string[] args) { 8 | using (var arena = new Arena()) { 9 | Console.WriteLine($"Original string: {sourceText}"); 10 | 11 | // contrived example to split a string into words using an arena 12 | // in order to avoid allocations 13 | var words = new ArenaList(arena); 14 | 15 | var index = 0; 16 | var startIndex = 0; 17 | 18 | void addWord() { 19 | var length = index - startIndex; 20 | if (length > 0) { 21 | var chars = arena.AllocCount(length); 22 | var source = sourceText.AsSpan(startIndex, length); 23 | var dest = new Span(chars.Value, length); 24 | source.CopyTo(dest); 25 | words.Add(new Word(length, chars.Value)); 26 | } 27 | 28 | startIndex = index + 1; 29 | }; 30 | 31 | while (index < sourceText.Length) { 32 | var c = sourceText[index]; 33 | if (c == ' ') { 34 | addWord(); 35 | } 36 | index++; 37 | } 38 | 39 | addWord(); 40 | 41 | Console.Write("Split string: "); 42 | foreach (var word in words) { 43 | var s = new Span(word.Data, word.Length); 44 | foreach (var c in s) { 45 | Console.Write(c); 46 | } 47 | Console.Write(' '); 48 | } 49 | Console.WriteLine(); 50 | 51 | Console.WriteLine("Arena contents after splitting:"); 52 | foreach (var item in arena) { 53 | Console.WriteLine($"0x{item.Value:x16}: {item}"); 54 | } 55 | } 56 | } 57 | 58 | private static string sourceText = @"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam sodales elit rutrum iaculis dictum."; 59 | 60 | [StructLayout(LayoutKind.Sequential)] 61 | private unsafe struct Word { 62 | public int Length; 63 | public char* Data; 64 | 65 | public Word(int length, char* data) { 66 | Length = length; 67 | Data = data; 68 | } 69 | 70 | public override string ToString() => Data is null || Length <= 0 ? "" : new string(Data, 0, Length); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /BitpackedPtr.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Text; 5 | 6 | namespace Arenas { 7 | internal readonly struct BitpackedPtr : IEquatable { 8 | internal const int LowerMask = 0b111; 9 | internal const ulong PointerMask = ~0b111UL; 10 | 11 | private readonly ulong value; 12 | 13 | public BitpackedPtr(IntPtr ptr, int packedValue) { 14 | if (packedValue < 0 || packedValue > LowerMask) { 15 | throw new ArgumentOutOfRangeException(nameof(packedValue)); 16 | } 17 | Debug.Assert(((ulong)ptr & LowerMask) == 0); 18 | value = (ulong)ptr | (uint)packedValue; 19 | } 20 | 21 | #region Equality 22 | public override bool Equals(object obj) { 23 | return obj is BitpackedPtr ptr && 24 | value == ptr.value; 25 | } 26 | 27 | public bool Equals(BitpackedPtr other) { 28 | return value == other.value; 29 | } 30 | 31 | public override int GetHashCode() { 32 | return 731850958 + value.GetHashCode(); 33 | } 34 | 35 | public static bool operator ==(BitpackedPtr left, BitpackedPtr right) { 36 | return left.Equals(right); 37 | } 38 | 39 | public static bool operator !=(BitpackedPtr left, BitpackedPtr right) { 40 | return !(left == right); 41 | } 42 | #endregion 43 | 44 | public override string ToString() { 45 | return $"{Value}:{PackedValue}"; 46 | } 47 | 48 | public IntPtr Value { get { return (IntPtr)(value & PointerMask); } } 49 | public int PackedValue { get { return (int)value & LowerMask; } } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /CollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Arenas { 8 | internal static class CollectionExtensions { 9 | public static void SetLast(this IList list, T item) { 10 | if (list is null) { 11 | throw new ArgumentNullException(nameof(list)); 12 | } 13 | if (list.Count == 0) { 14 | throw new ArgumentOutOfRangeException("List in SetLast was empty"); 15 | } 16 | list[list.Count - 1] = item; 17 | } 18 | 19 | public static void SetLast(this IList list, ref T item) { 20 | if (list is null) { 21 | throw new ArgumentNullException(nameof(list)); 22 | } 23 | if (list.Count == 0) { 24 | throw new ArgumentOutOfRangeException("List in SetLast was empty"); 25 | } 26 | list[list.Count - 1] = item; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /IArenaContents.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | 6 | // This has become a bit of a mess thanks to NativeAOT not being great with reflection and because of a 7 | // desire to reduce excessive boilerplate. The gist of it is this: 8 | // 9 | // 1. Structs that can have ManagedRefs and/or be freed by an arena must implement IArenaContents. 10 | // 2. The Free method is used to free any resources (like Dispose) and the ArenaID is used to get the 11 | // arena the struct is allocated to. 12 | // 3. The ArenaMethods property returns an object implementing IArenaMethods. When a type is first 13 | // allocated on an arena this object is requested once and cached for the remaining runtime of the 14 | // program. It exposes Free and SetArenaID methods that are used on structs which implement the 15 | // IArenaContents interface and are expected to call the Free method and set the ArenaID property. 16 | // 4. The IArenaContents implementing struct of type T should return an instance of ArenaMethods, 17 | // which takes an IntPtr, casts it to a pointer to the struct, and calls Free or assigns ArenaID. 18 | // Because of this arenas can take an unmanaged struct T, detect if it implements IArenaContents, 19 | // request the cached version of ArenaMethods, and pass a pointer to the struct in order to 20 | // perform necessary operations on the struct without boxing and without reflection. 21 | // 5. User code should not be going around manipulating the ArenaID of arena-bound structs so this 22 | // property should be implemented explicitly to avoid being used by user code. 23 | // 6. But if the ArenaID property is explicit it's also hidden from the struct's code which means it 24 | // can't interact with the arena it's in. This is solved via IArenaContentsExtensions.GetArena 25 | // which indirectly allows the struct to retrieve its own arena. 26 | // 27 | // This leads to only two lines of boilerplate: 28 | // 29 | // ArenaID IArenaContents.ArenaID { get; set; } 30 | // IArenaMethods IArenaContents.ArenaMethods { get => ArenaMethods.Instance; } 31 | // 32 | // Where T should be altered to the type of the containing struct. 33 | namespace Arenas { 34 | public interface IArenaContents { 35 | void Free(); 36 | ArenaID ArenaID { get; set; } 37 | IArenaMethods ArenaMethods { get; } 38 | } 39 | 40 | public interface IArenaMethods { 41 | void Free(IntPtr ptr); 42 | void SetArenaID(IntPtr ptr, ArenaID id); 43 | } 44 | 45 | public unsafe sealed class ArenaMethods : IArenaMethods where T : unmanaged, IArenaContents { 46 | public void Free(IntPtr ptr) { 47 | ((T*)ptr)->Free(); 48 | } 49 | 50 | public void SetArenaID(IntPtr ptr, ArenaID id) { 51 | ((T*)ptr)->ArenaID = id; 52 | } 53 | 54 | private static readonly ArenaMethods inst = new ArenaMethods(); 55 | public static ArenaMethods Instance { get { return inst; } } 56 | } 57 | 58 | public static class IArenaContentsExtensions { 59 | public static Arena GetArena(ref this T inst) where T : unmanaged, IArenaContents { 60 | return Arena.Get(inst.ArenaID); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /IMemoryAllocator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Text; 5 | 6 | namespace Arenas { 7 | public interface IMemoryAllocator { 8 | MemoryAllocation Allocate(int sizeBytes); 9 | void Free(IntPtr ptr); 10 | } 11 | 12 | public readonly struct MemoryAllocation { 13 | public readonly IntPtr Pointer; 14 | public readonly int SizeBytes; 15 | 16 | public MemoryAllocation(IntPtr pointer, int sizeBytes) { 17 | Pointer = pointer; 18 | SizeBytes = sizeBytes; 19 | } 20 | 21 | public override string ToString() { 22 | return $"Allocation(0x{(ulong)Pointer:x}, {SizeBytes} bytes)"; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /IUnmanagedRef.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace Arenas { 5 | public interface IUnmanagedRef : IEquatable { 6 | UnmanagedRef Reference { get; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /ItemVersion.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Runtime.InteropServices; 5 | using System.Text; 6 | 7 | namespace Arenas { 8 | // Bit packing layout. For more information see RefVersion.cs 9 | // 10 | // Bit index: 1111111111111111 0000000000000000 11 | // FEDCBA9876543210 FEDCBA9876543210 12 | // 13 | // Bit layout: VEEEEEEEEEEEEEEE IIIIIIIIIIIIIIIS 14 | // VIIIIIIIIIIIIIII IIIIIIIIIIIIIIIL 15 | // 16 | // V = item version valid bit (valid if set) 17 | // E = element count (0 bits long, 15 bits short) 18 | // I = item version (30 bits long, 15 bits short) 19 | // S = short version (lowest bit unset) 20 | // L = long version (lowest bit set) 21 | [StructLayout(LayoutKind.Sequential)] 22 | public readonly struct ItemVersion : IEquatable { 23 | private const int validBit = unchecked((int)0x80000000); // -2147483648 24 | private const int versionBitIndex = 1; 25 | private const int maxShortVersion = 0x7FFF; 26 | private const int shortVersionMask = maxShortVersion << versionBitIndex; 27 | private const int maxVersion = 0x3FFFFFFF; 28 | private const int longVersionMask = maxVersion << versionBitIndex; 29 | private const int maxElementCount = 0x7FFF; 30 | private const int elementCountBitIndex = versionBitIndex + 15; 31 | private const int elementCountMask = maxElementCount << elementCountBitIndex; 32 | 33 | private readonly int rawValue; 34 | 35 | public ItemVersion(int rawValue) { 36 | this.rawValue = rawValue; 37 | } 38 | 39 | public ItemVersion(bool isShort, int version, int elementCount, bool valid) { 40 | int value; 41 | 42 | if (isShort) { 43 | Debug.Assert((version & (~maxShortVersion)) == 0); 44 | Debug.Assert((elementCount & (~maxElementCount)) == 0); 45 | value = (version << versionBitIndex) | (elementCount << elementCountBitIndex); 46 | } 47 | else { 48 | Debug.Assert((version & (~maxVersion)) == 0); 49 | value = (version << versionBitIndex) | 1; 50 | } 51 | 52 | if (valid) { 53 | value |= validBit; 54 | } 55 | 56 | rawValue = value; 57 | } 58 | 59 | public ItemVersion Invalidate() { 60 | return new ItemVersion(IsShortVersion, Version, ElementCount, false); 61 | } 62 | 63 | public ItemVersion Increment(bool valid, int elementCount) { 64 | int newVersion = Version + 1; 65 | bool isShort = IsShortVersion && newVersion <= maxShortVersion; 66 | 67 | newVersion &= maxVersion; 68 | if (newVersion == 0) { 69 | newVersion = 1; 70 | } 71 | 72 | if (!isShort || elementCount < 0 || elementCount > maxElementCount) { 73 | elementCount = 0; 74 | } 75 | 76 | return new ItemVersion(isShort, newVersion, elementCount, valid); 77 | } 78 | 79 | #region Equality 80 | public override bool Equals(object obj) { 81 | return obj is ItemVersion version && 82 | rawValue == version.rawValue; 83 | } 84 | 85 | public bool Equals(ItemVersion other) { 86 | throw new NotImplementedException(); 87 | } 88 | 89 | public override int GetHashCode() { 90 | return -8906994 + rawValue.GetHashCode(); 91 | } 92 | 93 | public static bool operator ==(ItemVersion left, ItemVersion right) { 94 | return left.Equals(right); 95 | } 96 | 97 | public static bool operator !=(ItemVersion left, ItemVersion right) { 98 | return !(left == right); 99 | } 100 | #endregion 101 | 102 | public override string ToString() { 103 | var v = Version; 104 | return !Valid ? $"{v} (invalid)" : $"{v}"; 105 | } 106 | 107 | public int ElementCount { get { return IsShortVersion ? (rawValue & elementCountMask) >> elementCountBitIndex : 0; } } 108 | public int Version { get { return (IsShortVersion ? rawValue & shortVersionMask : rawValue & longVersionMask) >> versionBitIndex; } } 109 | public bool Valid { get { return (rawValue & validBit) != 0; } } 110 | public bool HasElementCount { get { return IsShortVersion && (rawValue & elementCountMask) != 0; } } 111 | public bool IsShortVersion { get { return (rawValue & 1) == 0; } } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Emma Maassen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ManagedObjectPool.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace Arenas { 7 | public class ManagedObjectPool where T : class { 8 | public delegate T CreateInstanceDelegate(); 9 | public delegate bool ResetInstanceDelegate(T instance); 10 | 11 | private int maxStoredInstances; 12 | private ConcurrentStack freeList; 13 | protected CreateInstanceDelegate createInstance; 14 | protected ResetInstanceDelegate resetInstance; 15 | 16 | public ManagedObjectPool(CreateInstanceDelegate createInstance, ResetInstanceDelegate resetInstance) 17 | : this() { 18 | this.createInstance = createInstance; 19 | this.resetInstance = resetInstance; 20 | } 21 | 22 | protected ManagedObjectPool() { 23 | freeList = new ConcurrentStack(); 24 | } 25 | 26 | public void EnsureCount(int count) { 27 | while (freeList.Count < count) { 28 | freeList.Push(createInstance()); 29 | } 30 | } 31 | 32 | public void Return(T instance) { 33 | if (resetInstance(instance) && (maxStoredInstances == 0 || freeList.Count < MaxStoredInstances)) { 34 | freeList.Push(instance); 35 | } 36 | } 37 | 38 | public T Get() { 39 | if (!freeList.TryPop(out var instance)) { 40 | return createInstance(); 41 | } 42 | return instance; 43 | } 44 | 45 | public PooledAutoReturn Borrow(out T instance) { 46 | instance = Get(); 47 | return new PooledAutoReturn(this, instance); 48 | } 49 | 50 | public int MaxStoredInstances { 51 | get { return maxStoredInstances; } 52 | set { 53 | maxStoredInstances = Math.Max(0, value); 54 | 55 | if (maxStoredInstances > 0) { 56 | var popCount = freeList.Count - maxStoredInstances; 57 | T val; 58 | 59 | // alas ConcurrentStack does not give us a way to pop N objects off the stack 60 | // without first allocating an array for them, so this is the best allocation 61 | // free method that can be used here 62 | while (popCount > 0) { 63 | freeList.TryPop(out val); 64 | popCount--; 65 | } 66 | } 67 | } 68 | } 69 | } 70 | 71 | public readonly struct PooledAutoReturn : IDisposable where T : class { 72 | public readonly ManagedObjectPool Pool; 73 | public readonly T Value; 74 | 75 | public PooledAutoReturn(ManagedObjectPool pool, T value) { 76 | Pool = pool ?? throw new ArgumentNullException(nameof(pool)); 77 | Value = value ?? throw new ArgumentNullException(nameof(value)); 78 | } 79 | 80 | public void Dispose() { 81 | if (Value == null || Pool == null) { 82 | return; 83 | } 84 | Pool.Return(Value); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /ManagedRef.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace Arenas { 5 | [StructLayout(LayoutKind.Sequential)] 6 | public readonly struct ManagedRef { 7 | private readonly IntPtr handle; 8 | 9 | public ManagedRef(IntPtr handle) { 10 | this.handle = handle; 11 | } 12 | 13 | public ManagedRef Set(Arena arena, T value) where T : class { 14 | if (arena is null) { 15 | throw new NullReferenceException("Arena cannot be null in ManagedRef.Set"); 16 | } 17 | return new ManagedRef(arena.SetOutsidePtr(value, handle)); 18 | } 19 | 20 | public ManagedRef Set(ref TSource source, TVal value) where TSource : unmanaged, IArenaContents where TVal : class { 21 | var arena = Arena.Get(source.ArenaID); 22 | if (arena is null) { 23 | throw new NullReferenceException("Arena cannot be null in ManagedRef.Set"); 24 | } 25 | return new ManagedRef(arena.SetOutsidePtr(value, handle)); 26 | } 27 | 28 | public T Get() where T : class { 29 | if (handle == IntPtr.Zero) { 30 | return null; 31 | } 32 | GCHandle gcHandle = GCHandle.FromIntPtr(handle); 33 | return (T)gcHandle.Target; 34 | } 35 | 36 | public override string ToString() { 37 | return $"ManagedRef(0x{handle:x})"; 38 | } 39 | 40 | public static explicit operator IntPtr(ManagedRef ptr) { 41 | return ptr.handle; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /MemHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.InteropServices; 4 | using System.Text; 5 | 6 | namespace Arenas { 7 | public static class MemHelper { 8 | [DllImport("kernel32.dll")] 9 | private static extern void RtlZeroMemory(IntPtr dst, UIntPtr length); 10 | 11 | private delegate void ZeroMemoryDelegate(IntPtr dst, UIntPtr length); 12 | private static readonly ZeroMemoryDelegate zeroMemory; 13 | 14 | static MemHelper() { 15 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { 16 | zeroMemory = RtlZeroMemory; 17 | } 18 | else { 19 | zeroMemory = ZeroMemPlatformIndependent; 20 | } 21 | } 22 | 23 | public static void ZeroMemory(IntPtr dst, UIntPtr length) { 24 | zeroMemory(dst, length); 25 | } 26 | 27 | public static void ZeroMemory(IntPtr dst, int length) { 28 | if (length < 0) { 29 | throw new ArgumentOutOfRangeException(nameof(length)); 30 | } 31 | zeroMemory(dst, (UIntPtr)length); 32 | } 33 | 34 | private static readonly Dictionary powerOfTwoLeadingZeros = new Dictionary() { 35 | { 1 << 0, 0 }, { 1 << 1, 1 }, { 1 << 2, 2 }, { 1 << 3, 3 }, 36 | { 1 << 4, 4 }, { 1 << 5, 5 }, { 1 << 6, 6 }, { 1 << 7, 7 }, 37 | { 1 << 8, 8 }, { 1 << 9, 9 }, { 1 << 10, 10 }, { 1 << 11, 11 }, 38 | { 1 << 12, 12 }, { 1 << 13, 13 }, { 1 << 14, 14 }, { 1 << 15, 15 }, 39 | { 1 << 16, 16 }, { 1 << 17, 17 }, { 1 << 18, 18 }, { 1 << 19, 19 }, 40 | { 1 << 20, 20 }, { 1 << 21, 21 }, { 1 << 22, 22 }, { 1 << 23, 23 }, 41 | { 1 << 24, 24 }, { 1 << 25, 25 }, { 1 << 26, 26 }, { 1 << 27, 27 }, 42 | { 1 << 28, 28 }, { 1 << 29, 29 }, { 1 << 30, 30 }, { 1UL << 31, 31 }, 43 | { 1UL << 32, 32 }, { 1UL << 33, 33 }, { 1UL << 34, 34 }, { 1UL << 35, 35 }, 44 | { 1UL << 36, 36 }, { 1UL << 37, 37 }, { 1UL << 38, 38 }, { 1UL << 39, 39 }, 45 | { 1UL << 40, 40 }, { 1UL << 41, 41 }, { 1UL << 42, 42 }, { 1UL << 43, 43 }, 46 | { 1UL << 44, 44 }, { 1UL << 45, 45 }, { 1UL << 46, 46 }, { 1UL << 47, 47 }, 47 | { 1UL << 48, 48 }, { 1UL << 49, 49 }, { 1UL << 50, 50 }, { 1UL << 51, 51 }, 48 | { 1UL << 52, 52 }, { 1UL << 53, 53 }, { 1UL << 54, 54 }, { 1UL << 55, 55 }, 49 | { 1UL << 56, 56 }, { 1UL << 57, 57 }, { 1UL << 58, 58 }, { 1UL << 59, 59 }, 50 | { 1UL << 60, 60 }, { 1UL << 61, 61 }, { 1UL << 62, 62 }, { 1UL << 63, 63 }, 51 | }; 52 | public static IReadOnlyDictionary PowerOfTwoLeadingZeros { get { return powerOfTwoLeadingZeros; } } 53 | 54 | // http://graphics.stanford.edu/%7Eseander/bithacks.html#RoundUpPowerOf2 55 | public static ulong NextPowerOfTwo(ulong v) { 56 | v--; 57 | v |= v >> 1; 58 | v |= v >> 2; 59 | v |= v >> 4; 60 | v |= v >> 8; 61 | v |= v >> 16; 62 | v |= v >> 32; 63 | v++; 64 | return v; 65 | } 66 | 67 | public static bool IsPowerOfTwo(ulong v) { 68 | return powerOfTwoLeadingZeros.ContainsKey(v); 69 | } 70 | 71 | public static int AlignFloor(int addr, int size) { 72 | return addr & (~(size - 1)); 73 | } 74 | 75 | public static int AlignCeil(int addr, int size) { 76 | return (addr + (size - 1)) & (~(size - 1)); 77 | } 78 | 79 | public static IntPtr AlignFloor(IntPtr addr, int size) { 80 | return (IntPtr)AlignFloor((ulong)addr, size); 81 | } 82 | 83 | public static IntPtr AlignCeil(IntPtr addr, int size) { 84 | return (IntPtr)AlignCeil((ulong)addr, size); 85 | } 86 | 87 | public static ulong AlignFloor(ulong addr, int size) { 88 | var sizel = (ulong)size; 89 | return addr & (~(sizel - 1)); 90 | } 91 | 92 | public static ulong AlignCeil(ulong addr, int size) { 93 | var sizel = (ulong)size; 94 | return (addr + (sizel - 1)) & (~(sizel - 1)); 95 | } 96 | 97 | private unsafe static void ZeroMemPlatformIndependent(IntPtr ptr, UIntPtr length) { 98 | ulong size = (ulong)length; 99 | 100 | // clear to word alignment 101 | var byteptr = (byte*)ptr; 102 | var bytes = (int)((ulong)byteptr & 0b111); 103 | for (int i = 0; i < bytes; i++, byteptr++) { 104 | *byteptr = 0; 105 | } 106 | 107 | size -= (ulong)bytes; 108 | 109 | // clear words 110 | var count = size / sizeof(ulong); 111 | var longptr = (ulong*)byteptr; 112 | 113 | for (ulong i = 0; i < count; i++, longptr++) { 114 | *longptr = 0; 115 | } 116 | 117 | size -= count * sizeof(ulong); 118 | 119 | // clear remaining bytes 120 | byteptr = (byte*)longptr; 121 | bytes = (int)size; 122 | for (int i = 0; i < bytes; i++, byteptr++) { 123 | *byteptr = 0; 124 | } 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Arenas: Arena allocators for C#/CSharp 2 | 3 | This is a .NET Standard 2.0 library that provides access to arena allocators along with the ability to use unmanaged references with natural C# syntax and safe-guards, as well as the ability for arena-allocated items to reference managed C# objects. It is NativeAOT compatible. 4 | 5 | Use by creating a `new Arena()` and calling `Allocate` with blittable structs or `AllocCount` to allocate arrays of items. Types that implement `IArenaContents` will automatically have `Free` called and `ArenaID` set where appropriate (without boxing.) 6 | 7 | ## Features 8 | 9 | - Arena allocation for multiple unmanaged types 10 | - All memory is dumped when calling `Clear` or `Dispose` on an arena instance 11 | - Memory reuse via per-size freelists 12 | - Managed C# code can reference unmanaged structs via the `UnmanagedRef` and `UnmanagedRef` types 13 | - Unmanaged C# code can reference managed objects via the `ManagedRef` type 14 | - Managed references are kept alive by the arena via reference counting 15 | - `UnmanagedRef` types return `null`/`IntPtr.Zero` if the reference is stale 16 | - Can enumerate over all entries in the arena 17 | - Allocate any count of items 18 | - Allocate generic buffers of any size by allocating using `AllocCount(sizeInBytes)` 19 | - Arena object pooling via ArenaPool. Use `ArenaPool.Default.Get()` with `ArenaPool.Default.Return` or automatically return arenas at the end of a `using` block with `using (ArenaPool.Default.Borrow(out var arena))` 20 | - Optimal allocations for buffers where the size doesn't matter to the caller through the `AllocRoughly` method (size may be smaller than requested) 21 | - Debug view will show list of items for `UnmanagedRef` types (handy when inspecting multiple elements) 22 | - Copy `UnmanagedRef` types to arrays via `ToArray` and `CopyTo` 23 | - Generic collections `ArenaList` and `ArenaDict` for storing collections of unmanaged items inside an arena instance 24 | - `ArenaString` type for working with string data in arenas 25 | - Static methods on `ArenaString` for splitting standard C# strings and `char*` pointers into ArenaStrings 26 | - Ability to free items via `IntPtr` 27 | - `UnmanagedRef` is a lightweight struct (only 16 bytes in size) but will cache element counts (always for 7 or fewer elements, and for 32k or fewer elements until item versions exceed 32k) 28 | - `UnmanagedRef` is blittable and can itself be stored inside arenas (see samples) 29 | - Ability to use custom memory allocator for allocating page memory, as well as custom page size 30 | 31 | ## Samples 32 | 33 | Do some stuff with arenas: 34 | 35 | ```csharp 36 | UnmanagedRef staleRefTest; 37 | 38 | using (var arena = new Arena()) { 39 | // allocate some people in the arena 40 | var john = arena.Allocate(new Person()); 41 | john.Value->FirstName = "John"; 42 | john.Value->LastName = "Doe"; 43 | 44 | var jack = arena.Allocate(new Person()); 45 | jack.Value->FirstName = "Jack"; 46 | jack.Value->LastName = "Black"; 47 | 48 | Console.WriteLine($"Size of UnmanagedRef: {sizeof(UnmanagedRef)}"); 49 | 50 | Console.WriteLine(john); 51 | Console.WriteLine(jack); 52 | 53 | // make a list of integers in the arena 54 | var list = new ArenaList(arena) { 1, 2, 3 }; 55 | for (int i = 10; i < 22; i++) { 56 | list.Add(i); 57 | } 58 | 59 | Console.WriteLine("Values in list:"); 60 | foreach (var i in list) { 61 | Console.WriteLine(i); 62 | } 63 | 64 | // make a dictionary of integers in the arena 65 | var dict = new ArenaDict(arena); 66 | var random = new Random(12345); 67 | for (int i = 0; i < 20; i++) { 68 | dict[random.Next(1000)] = random.Next(1000); 69 | } 70 | 71 | Console.WriteLine("Values in dictionary (sorted by key ascending):"); 72 | foreach (var kvp in from entry in dict orderby entry.Key ascending select entry) { 73 | Console.WriteLine(kvp); 74 | } 75 | 76 | Console.WriteLine("Items in arena:"); 77 | foreach (var item in arena) { 78 | Console.WriteLine(item); 79 | } 80 | 81 | // free an item 82 | arena.Free(jack); 83 | 84 | Console.WriteLine("Items in arena after freeing:"); 85 | foreach (var item in arena) { 86 | Console.WriteLine(item); 87 | } 88 | 89 | // free the rest and show that our references are stale 90 | arena.Clear(); 91 | Console.WriteLine($"Does stale reference have a value? {john.HasValue}"); 92 | 93 | // make some random bytes using a Guid 94 | var guid = Guid.NewGuid(); 95 | var guidBytes = guid.ToByteArray(); 96 | 97 | // allocate a buffer for the bytes in the arena and copy them 98 | var unmanagedBytes = arena.AllocCount(guidBytes.Length); 99 | Marshal.Copy(guidBytes, 0, (IntPtr)unmanagedBytes.Value, guidBytes.Length); 100 | 101 | // check if the bytes are the same 102 | var isSame = true; 103 | for (int i = 0; i < guidBytes.Length; i++) { 104 | if (guidBytes[i] != *unmanagedBytes[i]) { 105 | isSame = false; 106 | break; 107 | } 108 | } 109 | Console.WriteLine(isSame ? "ArenaID bytes match" : "ArenaID bytes don't match"); 110 | 111 | // split a string into a bunch of ArenaString instances 112 | using (var splitResults = ArenaString.Split(arena, "Lorem ipsum dolor sit amet", ' ')) { 113 | for (int i = 0; i < splitResults.Count; i++) { 114 | var str = splitResults[i]; 115 | Console.WriteLine(str); 116 | str.Free(); 117 | } 118 | } 119 | 120 | // final stale reference test for disposal 121 | staleRefTest = arena.Allocate(new Person()); 122 | staleRefTest.Value->FirstName = "Stale"; 123 | staleRefTest.Value->LastName = "Reference"; 124 | } 125 | 126 | Console.WriteLine($"Does stale reference have a value after disposal? {staleRefTest.HasValue}"); 127 | ``` 128 | 129 | Create a blittable struct with managed references: 130 | 131 | ```csharp 132 | [StructLayout(LayoutKind.Sequential)] 133 | unsafe public struct Person : IArenaContents { 134 | // these two lines are boilerplate for IArenaContents structs 135 | ArenaID IArenaContents.ArenaID { get; set; } 136 | IArenaMethods IArenaContents.ArenaMethods { get => ArenaMethods.Instance; } 137 | 138 | private ManagedRef firstName; 139 | private ManagedRef lastName; 140 | 141 | public override string ToString() { 142 | return $"{FirstName} {LastName}"; 143 | } 144 | 145 | public void Free() { 146 | // free managed references by setting to null 147 | FirstName = null; 148 | LastName = null; 149 | } 150 | 151 | public string FirstName { 152 | get { return firstName.Get(); } 153 | set { firstName = firstName.Set(ref this, value); } 154 | } 155 | public string LastName { 156 | get { return lastName.Get(); } 157 | set { lastName = lastName.Set(ref this, value); } 158 | } 159 | } 160 | ``` 161 | 162 | Store references to items in arena in ArenaList: 163 | 164 | ```csharp 165 | using (var arena = new Arena()) { 166 | // allocate a list 167 | var people = new ArenaList(arena); 168 | 169 | // allocate some people references 170 | var john = arena.Allocate(new Person()); 171 | john.Value->FirstName = "John"; 172 | john.Value->LastName = "Doe"; 173 | 174 | var jack = arena.Allocate(new Person()); 175 | jack.Value->FirstName = "Jack"; 176 | jack.Value->LastName = "Black"; 177 | 178 | // store references inside the list 179 | people.Add(john); 180 | people.Add(jack); 181 | 182 | // iterate over unmanaged list and write out all the people 183 | foreach (var item in people) { 184 | var person = item.As(); 185 | Console.WriteLine(*person); 186 | } 187 | } 188 | ``` 189 | 190 | Zero-allocation string splitting via arenas (requires .NET Core): 191 | 192 | **This sample was made before the ArenaString type existed as an example of interaction between arenas and the .NET Core Span type. For zero-allocation string splitting please use ArenaString.Split** 193 | 194 | ```csharp 195 | class Program { 196 | unsafe static void Main(string[] args) { 197 | using (var arena = new Arena()) { 198 | Console.WriteLine($"Original string: {sourceText}"); 199 | 200 | // contrived example to split a string into words using an arena 201 | // in order to avoid allocations 202 | var words = new ArenaList(arena); 203 | 204 | var index = 0; 205 | var startIndex = 0; 206 | 207 | void addWord() { 208 | var length = index - startIndex; 209 | if (length > 0) { 210 | var chars = arena.AllocCount(length); 211 | var source = sourceText.AsSpan(startIndex, length); 212 | var dest = new Span(chars.Value, length); 213 | source.CopyTo(dest); 214 | words.Add(new Word(length, chars.Value)); 215 | } 216 | 217 | startIndex = index + 1; 218 | }; 219 | 220 | while (index < sourceText.Length) { 221 | var c = sourceText[index]; 222 | if (c == ' ') { 223 | addWord(); 224 | } 225 | index++; 226 | } 227 | 228 | addWord(); 229 | 230 | Console.Write("Split string: "); 231 | foreach (var word in words) { 232 | var s = new Span(word.Data, word.Length); 233 | foreach (var c in s) { 234 | Console.Write(c); 235 | } 236 | Console.Write(' '); 237 | } 238 | Console.WriteLine(); 239 | 240 | Console.WriteLine("Arena contents after splitting:"); 241 | foreach (var item in arena) { 242 | Console.WriteLine($"0x{item.Value:x16}: {item}"); 243 | } 244 | } 245 | } 246 | 247 | private static string sourceText = @"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam sodales elit rutrum iaculis dictum."; 248 | 249 | [StructLayout(LayoutKind.Sequential)] 250 | private unsafe struct Word { 251 | public int Length; 252 | public char* Data; 253 | 254 | public Word(int length, char* data) { 255 | Length = length; 256 | Data = data; 257 | } 258 | 259 | public override string ToString() => Data is null || Length <= 0 ? "" : new string(Data, 0, Length); 260 | } 261 | } 262 | ``` 263 | 264 | "yo dawg i herd u liek arena allocators so i put some arena allocators in ur arena allocators": 265 | 266 | ```csharp 267 | private static Arena parentArena = new Arena(); 268 | 269 | private unsafe class ArenaAllocator : IMemoryAllocator { 270 | public MemoryAllocation Allocate(int sizeBytes) { 271 | var alloc = parentArena.AllocCount(sizeBytes); 272 | return new MemoryAllocation((IntPtr)alloc.Value, alloc.Size); 273 | } 274 | 275 | public void Free(IntPtr ptr) => parentArena.Free(ptr); 276 | } 277 | 278 | static unsafe void ArenaArenas() { 279 | // by using a page size of 2048 we're actually guaranteeing this allocator 280 | // will use pages of ~4k, because the size is rounded to the next power of 281 | // two after adding the item header size 282 | using (var childArena = new Arena(new ArenaAllocator(), 2048)) { 283 | var john = childArena.Allocate(new Person()); 284 | john.Value->FirstName = "John"; 285 | john.Value->LastName = "Doe"; 286 | 287 | var jack = childArena.Allocate(new Person()); 288 | jack.Value->FirstName = "Jack"; 289 | jack.Value->LastName = "Black"; 290 | 291 | Console.WriteLine("Child arena:"); 292 | foreach (var item in childArena) Console.WriteLine(item); 293 | Console.WriteLine("Parent arena:"); 294 | foreach (var item in parentArena) Console.WriteLine(item); 295 | } 296 | } 297 | ``` 298 | 299 | ## Potential future work 300 | 301 | - More arena-specific generic collections like HashSet/Stack/Queue/LinkedList 302 | - More ArenaString refinements 303 | - ManagedObject struct which exists purely to store references to managed objects in arenas? 304 | - Custom per-arena tracing GC? 305 | 306 | ## Should I use this in production? 307 | 308 | Eh, maybe? I feel like the library is pretty mature, so for small projects I think it'd be okay. Probably not for big dang enterprise stuff though. Unless you really want to, I'm not your dad. I'd definitely use it myself at this point, but I'm a game developer, what do I know? 309 | -------------------------------------------------------------------------------- /RefVersion.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.InteropServices; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Arenas { 9 | // Versions contain information about the arena ID, item version number, and optional 10 | // element count. This allows UnmanagedRef to store the element count for items with low 11 | // version numbers (up to 32,767) to store element counts up to 32,767. The valid bit 12 | // indicates that an item is allocated in the arena if set, and indicates free space 13 | // if unset. 14 | // 15 | // Bit index: 3333333333333333 2222222222222222 1111111111111111 0000000000000000 16 | // FEDCBA9876543210 FEDCBA9876543210 FEDCBA9876543210 FEDCBA9876543210 17 | // 18 | // Bit layout: AAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAA VEEEEEEEEEEEEEEE IIIIIIIIIIIIIIIS 19 | // AAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAA VIIIIIIIIIIIIIII IIIIIIIIIIIIIIIL 20 | // 21 | // A = arena ID (32 bits) 22 | // V = item version valid bit (valid if set) 23 | // E = element count (0 bits long, 15 bits short) 24 | // I = item version (30 bits long, 15 bits short) 25 | // S = short version (lowest bit unset) 26 | // L = long version (lowest bit set) 27 | [StructLayout(LayoutKind.Explicit)] 28 | public readonly struct RefVersion : IEquatable { 29 | [FieldOffset(0)] 30 | public readonly ulong Value; 31 | 32 | [FieldOffset(0)] 33 | public readonly ItemVersion Item; 34 | [FieldOffset(sizeof(int))] 35 | public readonly ArenaID Arena; 36 | 37 | public RefVersion(ItemVersion item, ArenaID arena) { 38 | Value = 0; 39 | Item = item; 40 | Arena = arena; 41 | } 42 | 43 | public RefVersion IncrementItemVersion(bool valid, int elementCount) { 44 | return new RefVersion(Item.Increment(valid, elementCount), Arena); 45 | } 46 | 47 | public RefVersion SetArenaID(ArenaID id) { 48 | return new RefVersion(Item, id); 49 | } 50 | 51 | public RefVersion Invalidate() { 52 | return new RefVersion(Item.Invalidate(), ArenaID.Empty); 53 | } 54 | 55 | #region Equality 56 | public override bool Equals(object obj) { 57 | return obj is RefVersion version && Value == version.Value; 58 | } 59 | 60 | public bool Equals(RefVersion other) { 61 | return Value == other.Value; 62 | } 63 | 64 | public override int GetHashCode() { 65 | return 1688058797 + Value.GetHashCode(); 66 | } 67 | 68 | public static bool operator ==(RefVersion left, RefVersion right) { 69 | return left.Equals(right); 70 | } 71 | 72 | public static bool operator !=(RefVersion left, RefVersion right) { 73 | return !(left == right); 74 | } 75 | #endregion 76 | 77 | public override string ToString() { 78 | return $"RefVersion(Arena={Arena}, Item={Item})"; 79 | } 80 | 81 | public bool Valid { get { return Arena.Value != 0 && Item.Valid; } } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /TypeHandle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Runtime.InteropServices; 5 | 6 | namespace Arenas { 7 | [StructLayout(LayoutKind.Sequential)] 8 | public readonly struct TypeHandle : IEquatable { 9 | public readonly int Value; 10 | 11 | public TypeHandle(int value) { 12 | Value = value; 13 | } 14 | 15 | #region Equality 16 | public override bool Equals(object obj) { 17 | return obj is TypeHandle handle && 18 | Value == handle.Value; 19 | } 20 | 21 | public bool Equals(TypeHandle other) { 22 | return Value == other.Value; 23 | } 24 | 25 | public override int GetHashCode() { 26 | return 1909215196 + Value.GetHashCode(); 27 | } 28 | 29 | public static bool operator ==(TypeHandle left, TypeHandle right) { 30 | return left.Equals(right); 31 | } 32 | 33 | public static bool operator !=(TypeHandle left, TypeHandle right) { 34 | return !(left == right); 35 | } 36 | 37 | public override string ToString() { 38 | return GetTypeFromHandle(this).ToString(); 39 | } 40 | #endregion 41 | 42 | public bool HasValue { get { return Value != 0; } } 43 | public static TypeHandle None { get { return default; } } 44 | 45 | #region Static 46 | private static Dictionary typeToHandle; 47 | private static Dictionary handleToType; 48 | private static object typeHandleLock; 49 | 50 | static TypeHandle() { 51 | typeToHandle = new Dictionary(); 52 | handleToType = new Dictionary(); 53 | typeHandleLock = new object(); 54 | } 55 | 56 | public static TypeHandle GetTypeHandle(Type type) { 57 | if (type == typeof(Exception)) { 58 | throw new ArgumentException("Cannot pass typeof(Exception) to TypeHandle.GetTypeHandle", nameof(type)); 59 | } 60 | 61 | TypeHandle handle; 62 | lock (typeHandleLock) { 63 | if (!typeToHandle.TryGetValue(type, out handle)) { 64 | typeToHandle[type] = handle = new TypeHandle(typeToHandle.Count + 1); 65 | if (!handle.HasValue) { 66 | throw new OverflowException("Arena.TypeHandle value overflow: too many types"); 67 | } 68 | handleToType[handle] = type; 69 | } 70 | } 71 | return handle; 72 | } 73 | 74 | /// 75 | /// This returns the Type instance for a TypeHandle or typeof(Exception) if no matching Type is found 76 | /// 77 | /// Handle for a Type instance 78 | /// The Type instance for this TypeHandle or typeof(Exception) if no matching Type is found 79 | public static Type GetTypeFromHandle(TypeHandle handle) { 80 | if (!handle.HasValue) { 81 | return typeof(Exception); 82 | } 83 | 84 | Type type; 85 | lock (typeHandleLock) { 86 | if (!handleToType.TryGetValue(handle, out type)) { 87 | return typeof(Exception); 88 | } 89 | } 90 | return type; 91 | } 92 | 93 | public static bool TryGetTypeFromHandle(TypeHandle handle, out Type type) { 94 | type = null; 95 | if (!handle.HasValue) { 96 | return false; 97 | } 98 | 99 | lock (typeHandleLock) { 100 | if (!handleToType.TryGetValue(handle, out type)) { 101 | return false; 102 | } 103 | } 104 | return true; 105 | } 106 | #endregion 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /TypeInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | using System.Runtime.CompilerServices; 5 | using System.Text; 6 | using static Arenas.TypeHandle; 7 | 8 | namespace Arenas { 9 | public unsafe class TypeInfo { 10 | public Type Type { get; private set; } 11 | public TypeHandle Handle { get; private set; } 12 | public int Size { get; private set; } 13 | public IArenaMethods ArenaContentsMethods { get; private set; } 14 | public bool IsArenaContents { get { return ArenaContentsMethods != null; } } 15 | 16 | private Func toStringFunc; 17 | private Func getHashCodeFunc; 18 | private Func ptrToStructFunc; 19 | 20 | internal TypeInfo(Type type, TypeHandle handle, int size, Func toStringFunc, Func getHashCodeFunc, Func ptrToStructFunc) { 21 | Type = type; 22 | Handle = handle; 23 | Size = size; 24 | this.toStringFunc = toStringFunc; 25 | this.getHashCodeFunc = getHashCodeFunc; 26 | this.ptrToStructFunc = ptrToStructFunc; 27 | } 28 | 29 | public string ToString(IntPtr instance) { 30 | return toStringFunc(instance); 31 | } 32 | 33 | public int GetHashCode(IntPtr instance) { 34 | return getHashCodeFunc(instance); 35 | } 36 | 37 | public object PtrToStruct(IntPtr instance) { 38 | return ptrToStructFunc(instance); 39 | } 40 | 41 | public bool TryFree(IntPtr instance) { 42 | if (ArenaContentsMethods == null) return false; 43 | ArenaContentsMethods.Free(instance); 44 | return true; 45 | } 46 | 47 | public bool TrySetArenaID(IntPtr instance, ArenaID id) { 48 | if (ArenaContentsMethods == null) return false; 49 | ArenaContentsMethods.SetArenaID(instance, id); 50 | return true; 51 | } 52 | 53 | public override string ToString() { 54 | return $"TypeInfo({Type})"; 55 | } 56 | 57 | #region Static 58 | private static Dictionary handleToInfo; 59 | private static object handleLock; 60 | private static Dictionary typeToInfo; 61 | private static object typeLock; 62 | 63 | static TypeInfo() { 64 | handleToInfo = new Dictionary(); 65 | handleLock = new object(); 66 | typeToInfo = new Dictionary(); 67 | typeLock = new object(); 68 | } 69 | 70 | private static string ToStringFromPtr(IntPtr ptr) where T : unmanaged { 71 | var inst = (T*)ptr; 72 | return inst->ToString(); 73 | } 74 | 75 | private static int GetHashCodeFromPtr(IntPtr ptr) where T : unmanaged { 76 | var inst = (T*)ptr; 77 | return inst->GetHashCode(); 78 | } 79 | 80 | private static object CloneFromPtr(IntPtr ptr) where T : unmanaged { 81 | var inst = (T*)ptr; 82 | return *inst; 83 | } 84 | 85 | public static TypeInfo GenerateTypeInfo() where T : unmanaged { 86 | Type type = typeof(T); 87 | TypeInfo info; 88 | 89 | lock (typeLock) { 90 | if (!typeToInfo.TryGetValue(type, out info)) { 91 | info = new TypeInfo(type, GetTypeHandle(type), sizeof(T), 92 | ToStringFromPtr, GetHashCodeFromPtr, CloneFromPtr); 93 | 94 | if (typeof(IArenaContents).IsAssignableFrom(type)) { 95 | var inst = default(T); 96 | if (inst is IArenaContents) { 97 | info.ArenaContentsMethods = ((IArenaContents)inst).ArenaMethods; 98 | } 99 | } 100 | 101 | typeToInfo[type] = info; 102 | handleToInfo[GetTypeHandle(typeof(T))] = info; 103 | } 104 | } 105 | 106 | return info; 107 | } 108 | 109 | public static bool TryGetTypeInfo(TypeHandle handle, out TypeInfo info) { 110 | lock (handleLock) { 111 | if (!handleToInfo.TryGetValue(handle, out info)) { 112 | return false; 113 | } 114 | } 115 | return true; 116 | } 117 | 118 | public static TypeInfo GetTypeInfo(TypeHandle handle) { 119 | TypeInfo result; 120 | if (!TryGetTypeInfo(handle, out result)) { 121 | Type type; 122 | if (!TypeHandle.TryGetTypeFromHandle(handle, out type)) { 123 | throw new KeyNotFoundException("No TypeInfo for handle to unknown type"); 124 | } 125 | else { 126 | throw new KeyNotFoundException($"No TypeInfo for handle to type {type}"); 127 | } 128 | } 129 | return result; 130 | } 131 | 132 | public static bool TryGetTypeInfo(Type type, out TypeInfo info) { 133 | lock (typeLock) { 134 | if (!typeToInfo.TryGetValue(type, out info)) { 135 | return false; 136 | } 137 | } 138 | return true; 139 | } 140 | 141 | public static TypeInfo GetTypeInfo(Type type) { 142 | TypeInfo result; 143 | if (!TryGetTypeInfo(type, out result)) { 144 | throw new KeyNotFoundException($"No TypeInfo for type {type}"); 145 | } 146 | return result; 147 | } 148 | #endregion 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /UnmanagedRef.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Drawing; 5 | using System.Runtime.CompilerServices; 6 | using System.Runtime.InteropServices; 7 | using System.Text; 8 | using static Arenas.TypeHandle; 9 | using static Arenas.TypeInfo; 10 | 11 | namespace Arenas { 12 | [DebuggerTypeProxy(typeof(UnmanagedRefDebugView<>))] 13 | [DebuggerDisplay("{HasValue ? ToString() : null}")] 14 | [StructLayout(LayoutKind.Sequential)] 15 | unsafe readonly public struct UnmanagedRef : IUnmanagedRef, IEquatable> where T : unmanaged { 16 | public readonly UnmanagedRef Reference; 17 | 18 | public UnmanagedRef(UnmanagedRef reference) { 19 | Reference = reference; 20 | } 21 | 22 | public UnmanagedRef(T* pointer, RefVersion version, int elementCount) { 23 | Reference = new UnmanagedRef((IntPtr)pointer, version, elementCount); 24 | } 25 | 26 | public bool TryGetValue(out T* ptr) { 27 | ptr = Value; 28 | return ptr != null; 29 | } 30 | 31 | #region Copying 32 | public void CopyTo(T[] dest) { 33 | var elementCount = ElementCount; 34 | Reference.CopyTo(dest, 0, 0, elementCount, elementCount, typeof(T)); 35 | } 36 | 37 | public void CopyTo(T[] dest, int destIndex) { 38 | var elementCount = ElementCount; 39 | Reference.CopyTo(dest, destIndex, 0, elementCount, elementCount, typeof(T)); 40 | } 41 | 42 | public void CopyTo(T[] dest, int destIndex, int sourceIndex, int count) { 43 | var elementCount = ElementCount; 44 | Reference.CopyTo(dest, destIndex, sourceIndex, count, elementCount, typeof(T)); 45 | } 46 | 47 | public void CopyTo(UnmanagedRef dest) { 48 | var elementCount = ElementCount; 49 | Reference.CopyTo(dest.Value, dest.ElementCount, 0, 0, elementCount, elementCount); 50 | } 51 | 52 | public void CopyTo(UnmanagedRef dest, int destIndex) { 53 | var elementCount = ElementCount; 54 | Reference.CopyTo(dest.Value, dest.ElementCount, destIndex, 0, elementCount, elementCount); 55 | } 56 | 57 | public void CopyTo(UnmanagedRef dest, int destIndex, int sourceIndex, int count) { 58 | var elementCount = ElementCount; 59 | Reference.CopyTo(dest.Value, dest.ElementCount, destIndex, sourceIndex, count, -1); 60 | } 61 | 62 | public void CopyTo(T* dest, int destLength) { 63 | var elementCount = ElementCount; 64 | Reference.CopyTo(dest, destLength, 0, 0, elementCount, elementCount); 65 | } 66 | 67 | public void CopyTo(T* dest, int destLength, int destIndex) { 68 | var elementCount = ElementCount; 69 | Reference.CopyTo(dest, destLength, destIndex, 0, elementCount, elementCount); 70 | } 71 | 72 | public void CopyTo(T* dest, int destLength, int destIndex, int sourceIndex, int count) { 73 | Reference.CopyTo(dest, destLength, destIndex, sourceIndex, count, -1); 74 | } 75 | 76 | public T[] ToArray() { 77 | if (!HasValue) { 78 | throw new NullReferenceException($"Error in UnmanagedRef<{typeof(T)}>.ToArray: HasValue was false"); 79 | } 80 | 81 | var elementCount = ElementCount; 82 | var items = new T[elementCount]; 83 | Reference.CopyTo(items, 0, 0, elementCount, elementCount, typeof(T)); 84 | return items; 85 | } 86 | #endregion 87 | 88 | public override string ToString() { 89 | var elementCount = ElementCount; 90 | if (elementCount > 1) { 91 | return $"UnmanagedRef<{GetType().GenericTypeArguments[0].Name}>(ElementCount={elementCount})"; 92 | } 93 | var ptr = Value; 94 | return ptr == null ? string.Empty : (*ptr).ToString(); 95 | } 96 | 97 | #region Equality 98 | public override bool Equals(object obj) { 99 | return obj is UnmanagedRef @ref && 100 | Reference.Equals(@ref.Reference); 101 | } 102 | 103 | public bool Equals(UnmanagedRef other) { 104 | return Reference.Equals(other.Reference); 105 | } 106 | 107 | public override int GetHashCode() { 108 | return Reference.GetHashCode(); 109 | } 110 | 111 | public bool Equals(UnmanagedRef other) { 112 | return Reference.Equals(other); 113 | } 114 | 115 | public static bool operator ==(UnmanagedRef left, UnmanagedRef right) { 116 | return left.Equals(right); 117 | } 118 | 119 | public static bool operator !=(UnmanagedRef left, UnmanagedRef right) { 120 | return !(left == right); 121 | } 122 | #endregion 123 | 124 | public static explicit operator IntPtr(UnmanagedRef uref) { 125 | return (IntPtr)uref.Reference; 126 | } 127 | 128 | public static explicit operator UnmanagedRef(UnmanagedRef uref) { 129 | return uref.Reference; 130 | } 131 | 132 | public static explicit operator UnmanagedRef(UnmanagedRef uref) { 133 | return new UnmanagedRef(uref); 134 | } 135 | 136 | public T* this[int index] { 137 | get { 138 | if (index < 0 || index >= ElementCount) { 139 | throw new ArgumentOutOfRangeException(nameof(index)); 140 | } 141 | return Value + index; 142 | } 143 | } 144 | 145 | public int ElementCount { 146 | get { 147 | if (!HasValue) { 148 | return 0; 149 | } 150 | 151 | var packedValue = Reference.PointerPackedValue; 152 | if (packedValue == 0) { 153 | if (Reference.Version.Item.HasElementCount) { 154 | return Reference.Version.Item.ElementCount; 155 | } 156 | 157 | // this is the slow path 158 | return Size / sizeof(T); 159 | } 160 | return packedValue; 161 | } 162 | } 163 | 164 | public Arena Arena { get { return Reference.Arena; } } 165 | public T* Value { get { return (T*)Reference.Value; } } 166 | public bool HasValue { get { return Reference.HasValue; } } 167 | public RefVersion Version { get { return Reference.Version; } } 168 | public int Size { get { return Reference.Size; } } 169 | UnmanagedRef IUnmanagedRef.Reference { get { return Reference; } } 170 | } 171 | 172 | [DebuggerTypeProxy(typeof(UnmanagedRefDebugView))] 173 | [DebuggerDisplay("{HasValue ? ToString() : null}")] 174 | [StructLayout(LayoutKind.Sequential)] 175 | unsafe readonly public struct UnmanagedRef : IUnmanagedRef, IEquatable { 176 | private readonly BitpackedPtr pointer; 177 | private readonly RefVersion version; 178 | 179 | public UnmanagedRef(IntPtr pointer, RefVersion version, int elementCount) { 180 | if (elementCount > 0 && elementCount <= BitpackedPtr.LowerMask) { 181 | this.pointer = new BitpackedPtr(pointer, elementCount); 182 | } 183 | else { 184 | this.pointer = new BitpackedPtr(pointer, 0); 185 | } 186 | this.version = version; 187 | } 188 | 189 | public bool TryGetValue(out IntPtr ptr) { 190 | ptr = Value; 191 | return ptr != IntPtr.Zero; 192 | } 193 | 194 | #region Copying 195 | public void CopyTo(T[] dest) where T : unmanaged { 196 | var elementCount = ElementCount; 197 | CopyTo(dest, 0, 0, elementCount, elementCount); 198 | } 199 | 200 | public void CopyTo(T[] dest, int destIndex) where T : unmanaged { 201 | var elementCount = ElementCount; 202 | CopyTo(dest, destIndex, 0, elementCount, elementCount); 203 | } 204 | 205 | public void CopyTo(T[] dest, int destIndex, int sourceIndex, int count) where T : unmanaged { 206 | CopyTo(dest, destIndex, sourceIndex, count, -1); 207 | } 208 | 209 | public void CopyTo(UnmanagedRef dest) where T : unmanaged { 210 | var elementCount = ElementCount; 211 | CopyTo(dest.Value, dest.ElementCount, 0, 0, elementCount, elementCount); 212 | } 213 | 214 | public void CopyTo(UnmanagedRef dest, int destIndex) where T : unmanaged { 215 | var elementCount = ElementCount; 216 | CopyTo(dest.Value, dest.ElementCount, destIndex, 0, elementCount, elementCount); 217 | } 218 | 219 | public void CopyTo(UnmanagedRef dest, int destIndex, int sourceIndex, int count) where T : unmanaged { 220 | CopyTo(dest.Value, dest.ElementCount, destIndex, sourceIndex, count, -1); 221 | } 222 | 223 | public void CopyTo(T* dest, int destLength) where T : unmanaged { 224 | var elementCount = ElementCount; 225 | CopyTo(dest, destLength, 0, 0, elementCount, elementCount); 226 | } 227 | 228 | public void CopyTo(T* dest, int destLength, int destIndex) where T : unmanaged { 229 | var elementCount = ElementCount; 230 | CopyTo(dest, destLength, destIndex, 0, elementCount, elementCount); 231 | } 232 | 233 | public void CopyTo(T* dest, int destLength, int destIndex, int sourceIndex, int count) where T : unmanaged { 234 | CopyTo(dest, destLength, destIndex, sourceIndex, count, -1); 235 | } 236 | 237 | internal void CopyTo(T[] dest, int destIndex, int sourceIndex, int count, int elementCount, Type type = null) where T : unmanaged { 238 | if (dest is null) { 239 | throw new ArgumentNullException(nameof(dest)); 240 | } 241 | fixed (T* destPtr = dest) { 242 | CopyTo(destPtr, dest.Length, destIndex, sourceIndex, count, elementCount, type); 243 | } 244 | } 245 | 246 | internal void CopyTo(T* dest, int destLength, int destIndex, int sourceIndex, int count, int elementCount, Type type = null) where T : unmanaged { 247 | if (!HasValue) { 248 | throw new NullReferenceException($"Error in UnmanagedRef.CopyTo: HasValue was false"); 249 | } 250 | if (destLength < 0) { 251 | throw new ArgumentOutOfRangeException(nameof(destLength)); 252 | } 253 | if (count < 0) { 254 | throw new ArgumentOutOfRangeException(nameof(count)); 255 | } 256 | if (destIndex < 0 || destIndex + count > destLength) { 257 | throw new ArgumentOutOfRangeException(nameof(destIndex)); 258 | } 259 | 260 | if (elementCount < 0) { 261 | elementCount = ElementCount; 262 | } 263 | 264 | if (sourceIndex < 0 || sourceIndex + count > elementCount) { 265 | throw new ArgumentOutOfRangeException(nameof(sourceIndex)); 266 | } 267 | 268 | if (count == 0) { 269 | return; 270 | } 271 | 272 | if (dest == null) { 273 | throw new ArgumentNullException(nameof(dest)); 274 | } 275 | 276 | type = type ?? Type; 277 | 278 | var info = GetTypeInfo(type); 279 | var elementSize = info.Size; 280 | var cur = RawUnsafePointer + elementSize * sourceIndex; 281 | 282 | for (int i = 0; i < count; i++) { 283 | dest[destIndex + i] = *(T*)cur; 284 | cur += elementSize; 285 | } 286 | } 287 | 288 | public T[] ToArray() where T : unmanaged { 289 | if (!HasValue) { 290 | throw new NullReferenceException($"Error in UnmanagedRef.ToArray: HasValue was false"); 291 | } 292 | 293 | var elementCount = ElementCount; 294 | var items = new T[elementCount]; 295 | CopyTo(items, 0, 0, elementCount, elementCount); 296 | return items; 297 | } 298 | 299 | public object[] ToArray() { 300 | if (!HasValue) { 301 | throw new NullReferenceException($"Error in UnmanagedRef.ToArray: HasValue was false"); 302 | } 303 | 304 | var elementCount = ElementCount; 305 | var items = new object[elementCount]; 306 | 307 | var info = GetTypeInfo(TypeHandle); 308 | var elementSize = info.Size; 309 | var cur = RawUnsafePointer; 310 | 311 | for (int i = 0; i < elementCount; i++) { 312 | items[i] = info.PtrToStruct(cur); 313 | cur += elementSize; 314 | } 315 | 316 | return items; 317 | } 318 | #endregion 319 | 320 | public override string ToString() { 321 | var elementCount = ElementCount; 322 | if (elementCount > 1) { 323 | return $"UnmanagedRef(Type={Type}, ElementCount={elementCount})"; 324 | } 325 | 326 | var ptr = Value; 327 | if (ptr == IntPtr.Zero) { 328 | return string.Empty; 329 | } 330 | 331 | TypeInfo info; 332 | if (!TryGetTypeInfo(TypeHandle, out info)) { 333 | return $"UnmanagedRef(Type=, ElementCount={elementCount})"; 334 | } 335 | 336 | return info.ToString(ptr); 337 | } 338 | 339 | public static explicit operator IntPtr(UnmanagedRef uref) { 340 | return uref.Value; 341 | } 342 | 343 | public T* As() where T : unmanaged { 344 | if (Type != typeof(T)) { 345 | return null; 346 | } 347 | return (T*)Value; 348 | } 349 | 350 | #region Equality 351 | public override bool Equals(object obj) { 352 | return obj is UnmanagedRef @ref && 353 | EqualityComparer.Default.Equals(pointer, @ref.pointer) && 354 | version.Equals(@ref.version); 355 | } 356 | 357 | public bool Equals(UnmanagedRef other) { 358 | return 359 | EqualityComparer.Default.Equals(pointer, other.pointer) && 360 | version.Equals(other.version); 361 | } 362 | 363 | public override int GetHashCode() { 364 | int hashCode = 598475582; 365 | hashCode = hashCode * -1521134295 + pointer.GetHashCode(); 366 | hashCode = hashCode * -1521134295 + version.GetHashCode(); 367 | return hashCode; 368 | } 369 | 370 | public static bool operator ==(UnmanagedRef left, UnmanagedRef right) { 371 | return left.Equals(right); 372 | } 373 | 374 | public static bool operator !=(UnmanagedRef left, UnmanagedRef right) { 375 | return !(left == right); 376 | } 377 | #endregion 378 | 379 | //public IntPtr this[int index] { 380 | // get { 381 | // if (index < 0 || index >= ElementCount) { 382 | // throw new ArgumentOutOfRangeException(nameof(index)); 383 | // } 384 | // return Value + index; 385 | // } 386 | //} 387 | 388 | public Type Type { 389 | get { 390 | if (!HasValue) { 391 | return null; 392 | } 393 | Type type; 394 | if (!TryGetTypeFromHandle(Arena.ItemHeader.GetTypeHandle(pointer.Value), out type)) { 395 | return null; 396 | } 397 | return type; 398 | } 399 | } 400 | 401 | public TypeHandle TypeHandle { 402 | get { 403 | if (!HasValue) { 404 | return TypeHandle.None; 405 | } 406 | return Arena.ItemHeader.GetTypeHandle(pointer.Value); 407 | } 408 | } 409 | 410 | public Arena Arena { 411 | get { 412 | return Arena.Get(version.Arena); 413 | } 414 | } 415 | 416 | public IntPtr Value { 417 | get { 418 | var arena = Arena; 419 | if (arena is null) { 420 | return IntPtr.Zero; 421 | } 422 | var ptr = pointer.Value; 423 | return ptr != IntPtr.Zero && !arena.VersionsMatch(version, ptr) ? IntPtr.Zero : ptr; 424 | } 425 | } 426 | 427 | public bool HasValue { 428 | get { 429 | var arena = Arena; 430 | if (arena is null) { 431 | return false; 432 | } 433 | var ptr = pointer.Value; 434 | return ptr != IntPtr.Zero && arena.VersionsMatch(version, ptr); 435 | } 436 | } 437 | 438 | public int ElementCount { 439 | get { 440 | if (!HasValue) { 441 | return 0; 442 | } 443 | 444 | var packedValue = pointer.PackedValue; 445 | if (packedValue == 0) { 446 | if (version.Item.HasElementCount) { 447 | return version.Item.ElementCount; 448 | } 449 | 450 | // this is the slow path 451 | var handle = TypeHandle; 452 | TypeInfo info; 453 | 454 | if (!TryGetTypeInfo(handle, out info)) { 455 | return 0; 456 | } 457 | 458 | return Size / info.Size; 459 | } 460 | return packedValue; 461 | } 462 | } 463 | 464 | public int Size { 465 | get { 466 | var ptr = pointer.Value; 467 | var arena = Arena; 468 | if (arena is null || !arena.VersionsMatch(version, ptr)) { 469 | return 0; 470 | } 471 | return Arena.ItemHeader.GetSize(ptr); 472 | } 473 | } 474 | 475 | public RefVersion Version { get { return version; } } 476 | public IntPtr RawUnsafePointer { get { return pointer.Value; } } 477 | 478 | internal int PointerPackedValue { get { return pointer.PackedValue; } } 479 | UnmanagedRef IUnmanagedRef.Reference { get { return this; } } 480 | } 481 | } 482 | -------------------------------------------------------------------------------- /UnmanagedRefDebugView.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.InteropServices; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Arenas { 9 | internal unsafe readonly struct UnmanagedRefDebugView where T : unmanaged { 10 | private readonly UnmanagedRef uref; 11 | 12 | public UnmanagedRefDebugView(UnmanagedRef uref) { 13 | this.uref = uref; 14 | } 15 | 16 | public T[] Contents { 17 | get { 18 | return uref.ToArray(); 19 | } 20 | } 21 | 22 | public Arena Arena { get { return uref.Arena; } } 23 | public T* Value { get { return uref.Value; } } 24 | public bool HasValue { get { return uref.HasValue; } } 25 | public RefVersion Version { get { return uref.Version; } } 26 | public int ElementCount { get { return uref.ElementCount; } } 27 | public int Size { get { return uref.Size; } } 28 | } 29 | 30 | internal readonly struct UnmanagedRefDebugView { 31 | private readonly UnmanagedRef uref; 32 | 33 | public UnmanagedRefDebugView(UnmanagedRef uref) { 34 | this.uref = uref; 35 | } 36 | 37 | public object[] Contents { 38 | get { 39 | return uref.ToArray(); 40 | } 41 | } 42 | 43 | public Type Type { get { return uref.Type; } } 44 | public Arena Arena { get { return uref.Arena; } } 45 | public IntPtr Value { get { return uref.Value; } } 46 | public bool HasValue { get { return uref.HasValue; } } 47 | public RefVersion Version { get { return uref.Version; } } 48 | public int ElementCount { get { return uref.ElementCount; } } 49 | public int Size { get { return uref.Size; } } 50 | } 51 | } 52 | --------------------------------------------------------------------------------