// Copyright (C) 2026 Kiyotsugu Arai // SPDX-License-Identifier: LGPL-3.0-or-later // ScratchArena.hpp // Fast arena allocator for temporary buffers // Used to avoid malloc inside Karatsuba/Toom-Cook recursion // // VirtualAlloc-based: reserve a large virtual address range up front and // commit physical memory on demand. Reallocation never occurs, so existing // pointers never get invalidated. #pragma once #include #include #include #ifdef _WIN32 #ifndef NOMINMAX #define NOMINMAX #endif #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #include #else #include #include #endif namespace sangi { class ScratchArena { uint64_t* base_ = nullptr; size_t offset_ = 0; // used offset in uint64_t units size_t committed_ = 0; // committed size in uint64_t units size_t reserved_ = 0; // reserved size in uint64_t units static constexpr size_t RESERVE_WORDS = size_t(1) << 29; // 4GB (512M words * 8 bytes) static constexpr size_t COMMIT_GRANULARITY = size_t(1) << 16; // commit in 512KB chunks public: ScratchArena() { reserved_ = RESERVE_WORDS; size_t bytes = reserved_ * sizeof(uint64_t); #ifdef _WIN32 base_ = static_cast( VirtualAlloc(nullptr, bytes, MEM_RESERVE, PAGE_NOACCESS)); #else base_ = static_cast( mmap(nullptr, bytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); if (base_ == MAP_FAILED) base_ = nullptr; #endif if (!base_) { // Fallback: if reserve fails, retry with a smaller size reserved_ = size_t(1) << 24; // 128MB bytes = reserved_ * sizeof(uint64_t); #ifdef _WIN32 base_ = static_cast( VirtualAlloc(nullptr, bytes, MEM_RESERVE, PAGE_NOACCESS)); #else base_ = static_cast( mmap(nullptr, bytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); if (base_ == MAP_FAILED) base_ = nullptr; #endif } } ~ScratchArena() { if (base_) { #ifdef _WIN32 VirtualFree(base_, 0, MEM_RELEASE); #else munmap(base_, reserved_ * sizeof(uint64_t)); #endif } } ScratchArena(const ScratchArena&) = delete; ScratchArena& operator=(const ScratchArena&) = delete; uint64_t* alloc_limbs(size_t n) { if (n == 0) return nullptr; // 8-word alignment (64 bytes, cache line) size_t aligned_n = (n + 7) & ~size_t(7); size_t new_end = offset_ + aligned_n; // Commit physical memory on demand (the address does not change) if (new_end > committed_) { size_t need = new_end - committed_; size_t commit = ((need + COMMIT_GRANULARITY - 1) / COMMIT_GRANULARITY) * COMMIT_GRANULARITY; if (committed_ + commit > reserved_) { commit = reserved_ - committed_; if (new_end > reserved_) { throw std::runtime_error("ScratchArena: exceeded reserved address space"); } } #ifdef _WIN32 void* p = VirtualAlloc(base_ + committed_, commit * sizeof(uint64_t), MEM_COMMIT, PAGE_READWRITE); if (!p) throw std::runtime_error("ScratchArena: VirtualAlloc commit failed"); #else int ret = mprotect(base_ + committed_, commit * sizeof(uint64_t), PROT_READ | PROT_WRITE); if (ret != 0) throw std::runtime_error("ScratchArena: mprotect failed"); #endif committed_ += commit; } uint64_t* p = base_ + offset_; offset_ = new_end; return p; } size_t mark() const { return offset_; } void rewind(size_t saved) { offset_ = saved; } }; // Access to the thread_local arena (a static local avoids ODR issues) inline ScratchArena& getThreadArena() { static thread_local ScratchArena arena; return arena; } class ScratchScope { size_t saved_; public: ScratchScope() : saved_(getThreadArena().mark()) {} ~ScratchScope() { getThreadArena().rewind(saved_); } ScratchScope(const ScratchScope&) = delete; ScratchScope& operator=(const ScratchScope&) = delete; }; } // namespace sangi