using System; using System.Collections.Generic; using System.Runtime.CompilerServices; namespace ARMeilleure.Common { unsafe sealed class ArenaAllocator : Allocator { private class PageInfo { public byte* Pointer; public byte Unused; public int UnusedCounter; } private int _lastReset; private ulong _index; private int _pageIndex; private PageInfo _page; private List<PageInfo> _pages; private readonly ulong _pageSize; private readonly uint _pageCount; private readonly List<IntPtr> _extras; public ArenaAllocator(uint pageSize, uint pageCount) { _lastReset = Environment.TickCount; // Set _index to pageSize so that the first allocation goes through the slow path. _index = pageSize; _pageIndex = -1; _page = null; _pages = new List<PageInfo>(); _pageSize = pageSize; _pageCount = pageCount; _extras = new List<IntPtr>(); } public Span<T> AllocateSpan<T>(ulong count) where T : unmanaged { return new Span<T>(Allocate<T>(count), (int)count); } public override void* Allocate(ulong size) { if (_index + size <= _pageSize) { byte* result = _page.Pointer + _index; _index += size; return result; } return AllocateSlow(size); } [MethodImpl(MethodImplOptions.NoInlining)] private void* AllocateSlow(ulong size) { if (size > _pageSize) { void* extra = NativeAllocator.Instance.Allocate(size); _extras.Add((IntPtr)extra); return extra; } if (_index + size > _pageSize) { _index = 0; _pageIndex++; } if (_pageIndex < _pages.Count) { _page = _pages[_pageIndex]; _page.Unused = 0; } else { _page = new PageInfo(); _page.Pointer = (byte*)NativeAllocator.Instance.Allocate(_pageSize); _pages.Add(_page); } byte* result = _page.Pointer + _index; _index += size; return result; } public override void Free(void* block) { } public void Reset() { _index = _pageSize; _pageIndex = -1; _page = null; // Free excess pages that was allocated. while (_pages.Count > _pageCount) { NativeAllocator.Instance.Free(_pages[_pages.Count - 1].Pointer); _pages.RemoveAt(_pages.Count - 1); } // Free extra blocks that are not page-sized foreach (IntPtr ptr in _extras) { NativeAllocator.Instance.Free((void*)ptr); } _extras.Clear(); // Free pooled pages that has not been used in a while. Remove pages at the back first, because we try to // keep the pages at the front alive, since they're more likely to be hot and in the d-cache. bool removing = true; // If arena is used frequently, keep pages for longer. Otherwise keep pages for a shorter amount of time. int now = Environment.TickCount; int count = (now - _lastReset) switch { >= 5000 => 0, >= 2500 => 50, >= 1000 => 100, >= 10 => 1500, _ => 5000 }; for (int i = _pages.Count - 1; i >= 0; i--) { PageInfo page = _pages[i]; if (page.Unused == 0) { page.UnusedCounter = 0; } page.UnusedCounter += page.Unused; page.Unused = 1; // If page not used after `count` resets, remove it. if (removing && page.UnusedCounter >= count) { NativeAllocator.Instance.Free(page.Pointer); _pages.RemoveAt(i); } else { removing = false; } } _lastReset = now; } protected override void Dispose(bool disposing) { if (_pages != null) { foreach (PageInfo info in _pages) { NativeAllocator.Instance.Free(info.Pointer); } foreach (IntPtr ptr in _extras) { NativeAllocator.Instance.Free((void*)ptr); } _pages = null; } } ~ArenaAllocator() { Dispose(false); } } }