kill tmp .xo-arena
This commit is contained in:
parent
6e691eaeef
commit
6fc8303602
62 changed files with 0 additions and 8604 deletions
|
|
@ -1,36 +0,0 @@
|
|||
# xo-arena/CMakeLists.txt
|
||||
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
project(xo_arena VERSION 1.0)
|
||||
enable_language(CXX)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
include(cmake/xo-bootstrap-macros.cmake)
|
||||
|
||||
xo_cxx_toplevel_options3()
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
# c++ settings
|
||||
|
||||
# one-time project-specific c++ flags. usually empty
|
||||
set(PROJECT_CXX_FLAGS "")
|
||||
add_definitions(${PROJECT_CXX_FLAGS})
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
# output targets
|
||||
|
||||
add_subdirectory(src/arena)
|
||||
add_subdirectory(utest)
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
# cmake export
|
||||
|
||||
xo_export_cmake_config(${PROJECT_NAME} ${PROJECT_VERSION} ${PROJECT_NAME}Targets)
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
# docs targets depend on all the other library/utest targets
|
||||
#
|
||||
add_subdirectory(docs)
|
||||
|
||||
# end CMakeLists.txt
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
# ----------------------------------------------------------------
|
||||
# for example:
|
||||
# $ PREFIX=/usr/local # for example
|
||||
# $ cmake -DCMAKE_MODULE_PATH=prefix -DCMAKE_INSTALL_PREFIX=$PREFIX -B .build
|
||||
#
|
||||
# will get
|
||||
# CMAKE_MODULE_PATH
|
||||
# from xo-cmake-config --cmake-module-path
|
||||
#
|
||||
# and expect .cmake macros in
|
||||
# CMAKE_MODULE_PATH/xo_macros/xo_cxx.cmake
|
||||
# ----------------------------------------------------------------
|
||||
|
||||
find_program(XO_CMAKE_CONFIG_EXECUTABLE NAMES xo-cmake-config REQUIRED)
|
||||
|
||||
if ("${XO_CMAKE_CONFIG_EXECUTABLE}" STREQUAL "XO_CMAKE_CONFIG_EXECUTABLE-NOT_FOUND")
|
||||
message(FATAL "could not find xo-cmake-config executable")
|
||||
endif()
|
||||
|
||||
message(STATUS "XO_CMAKE_CONFIG_EXECUTABLE=${XO_CMAKE_CONFIG_EXECUTABLE}")
|
||||
|
||||
if (XO_SUBMODULE_BUILD)
|
||||
if (("${CMAKE_MODULE_PATH}" STREQUAL "") OR ("${CMAKE_MODULE_PATH}" STREQUAL prefix))
|
||||
# local version of xo-cmake macros
|
||||
set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/xo-cmake/cmake")
|
||||
message(STATUS "CMAKE_MODULE_PATH=${CMAKE_MODULE_PATH}")
|
||||
endif()
|
||||
else()
|
||||
if (("${CMAKE_MODULE_PATH}" STREQUAL "") OR ("${CMAKE_MODULE_PATH}" STREQUAL prefix))
|
||||
# default to typical install location for xo-project-macros
|
||||
execute_process(COMMAND ${XO_CMAKE_CONFIG_EXECUTABLE} --cmake-module-path OUTPUT_VARIABLE CMAKE_MODULE_PATH)
|
||||
message(STATUS "CMAKE_MODULE_PATH=${CMAKE_MODULE_PATH}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# needs to have been installed somewhere on CMAKE_MODULE_PATH,
|
||||
# (e.g. from xo-cmake with the same value for CMAKE_INSTALL_PREFIX)
|
||||
#
|
||||
include(xo_macros/xo_cxx)
|
||||
|
||||
xo_cxx_bootstrap_message()
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
@PACKAGE_INIT@
|
||||
|
||||
include(CMakeFindDependencyMacro)
|
||||
|
||||
# note: changes to find_dependency() calls here
|
||||
# must coordinate with xo_dependency() calls
|
||||
# in CMakeLists.txt
|
||||
#
|
||||
find_dependency(xo_reflectutil)
|
||||
find_dependency(indentlog)
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Targets.cmake")
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@Share.cmake")
|
||||
check_required_components("@PROJECT_NAME@")
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
.. _AllocInfo-reference:
|
||||
|
||||
AllocInfo Reference
|
||||
===================
|
||||
|
||||
Describes a single allocation.
|
||||
Requires allocator configured to store per-allocation headers
|
||||
|
||||
Context
|
||||
-------
|
||||
|
||||
.. ditaa::
|
||||
:--scale: 0.99
|
||||
|
||||
+-----------------------------------------------------+
|
||||
| DArena |
|
||||
| DArenaIterator |
|
||||
+-----------------------------------------------------+
|
||||
| ArenaConfig |
|
||||
+--------------+------------------------+-------------+
|
||||
| | AllocInfo cBLU| |
|
||||
| +------------------------+ |
|
||||
| AllocError | AllocHeaderConfig | cmpresult |
|
||||
| +------------------------+ |
|
||||
| | AllocHeader | |
|
||||
+--------------+------------------------+-------------+
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
.. _ArenaConfig-reference:
|
||||
|
||||
ArenaConfig Reference
|
||||
=====================
|
||||
|
||||
Configuration for an arena allocator
|
||||
|
||||
Context
|
||||
-------
|
||||
|
||||
.. ditaa::
|
||||
:--scale: 0.99
|
||||
|
||||
+-----------------------------------------------------+
|
||||
| DArena |
|
||||
| DArenaIterator |
|
||||
+-----------------------------------------------------+
|
||||
| ArenaConfig cBLU|
|
||||
+--------------+------------------------+-------------+
|
||||
| | AllocInfo | |
|
||||
| +------------------------+ |
|
||||
| AllocError | AllocHeaderConfig | cmpresult |
|
||||
| +------------------------+ |
|
||||
| | AllocHeader | |
|
||||
+--------------+------------------------+-------------+
|
||||
|
||||
|
||||
.. uml::
|
||||
:caption: example arena config
|
||||
:scale: 99%
|
||||
:align: center
|
||||
|
||||
object cfg<<AreanConfig>>
|
||||
cfg : name = "tmp"
|
||||
cfg : size = 128MB
|
||||
cfg : hugepage_z = 2MB
|
||||
cfg : guard_z = 8
|
||||
cfg : guard_byte = 0xfd
|
||||
cfg : store_header_flag = true
|
||||
cfg : header_size_mask = 0xffffffff
|
||||
cfg : debug_flag = false
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
#include <xo/arena/ArenaConfig.hpp>
|
||||
|
||||
Class
|
||||
-----
|
||||
|
||||
.. doxygenclass:: xo::mm::ArenaConfig
|
||||
|
||||
Instance Variables
|
||||
------------------
|
||||
|
||||
.. doxygengroup:: mm-arenaconfig-instance-vars
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
# xo-arena/docs/CMakeLists.txt
|
||||
|
||||
xo_doxygen_collect_deps()
|
||||
xo_docdir_doxygen_config()
|
||||
xo_docdir_sphinx_config(
|
||||
index.rst
|
||||
glossary.rst
|
||||
examples.rst
|
||||
implementation.rst
|
||||
#AAllocator-reference.rst
|
||||
#IAllocator_Xfer-reference.rst
|
||||
#AAllocIterator-reference.rst
|
||||
ArenaConfig-reference.rst
|
||||
DArena-reference.rst
|
||||
AllocInfo-reference.rst
|
||||
cmpresult-reference.rst
|
||||
#install.rst
|
||||
#introduction.rst
|
||||
)
|
||||
|
||||
# see xo-reader/doc or xo-unit/doc for working examples
|
||||
# example.rst install.rst implementation.rst
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
.. _DArena-reference:
|
||||
|
||||
DArena
|
||||
======
|
||||
|
||||
Native arena allocator
|
||||
|
||||
Context
|
||||
-------
|
||||
|
||||
.. ditaa::
|
||||
:--scale: 0.99
|
||||
|
||||
+-----------------------------------------------------+
|
||||
| DArena cBLU|
|
||||
| DArenaIterator |
|
||||
+-----------------------------------------------------+
|
||||
| ArenaConfig |
|
||||
+--------------+------------------------+-------------+
|
||||
| | AllocInfo | |
|
||||
| +------------------------+ |
|
||||
| AllocError | AllocHeaderConfig | cmpresult |
|
||||
| +------------------------+ |
|
||||
| | AllocHeader | |
|
||||
+--------------+------------------------+-------------+
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
#include <xo/arena/DArena.hpp>
|
||||
|
||||
Arena memory layout
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
<------------------------reserved-------------------------->
|
||||
<------------committed-----------><-------uncommitted------>
|
||||
<--allocated--><----available---->
|
||||
|
||||
XXXXXXXXXXXXXXX___________________..........................
|
||||
^ ^ ^ ^
|
||||
lo free limit hi
|
||||
|
||||
[X] allocated: in use
|
||||
[_] committed: physical memory obtained
|
||||
[.] uncommitted: mapped in virtual memory, not backed by memory
|
||||
|
||||
|
||||
Representation for a single allocation
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
free_(pre)
|
||||
v
|
||||
|
||||
<-------------z1--------------->
|
||||
< guard >< hz >< req_z >< dz >< guard >
|
||||
|
||||
used <== +++++++++0000zzzz@@@@@@@@@@@@@@@@@ppppppp+++++++++ ==> avail
|
||||
|
||||
^ ^ ^
|
||||
header mem |
|
||||
^ |
|
||||
last_header_ free_(post)
|
||||
|
||||
[+] guard surrounding each allocation, for simple sanitize checks
|
||||
[0] unused header bits (available for application metadata)
|
||||
[z] record allocation size
|
||||
[@] new allocated memory
|
||||
[p] padding (to uintptr_t alignment)
|
||||
|
||||
Class
|
||||
-----
|
||||
|
||||
.. doxygenclass:: xo::mm::DArena
|
||||
|
||||
Member Variables
|
||||
----------------
|
||||
|
||||
.. doxygengroup:: mm-arena-instance-vars
|
||||
|
||||
Type Traits
|
||||
-----------
|
||||
|
||||
.. doxygengroup:: mm-arena-traits
|
||||
|
||||
Constructors
|
||||
------------
|
||||
|
||||
.. doxygengroup:: mm-arena-ctors
|
||||
|
||||
Methods
|
||||
-------
|
||||
|
||||
.. doxygengroup:: mm-arena-methods
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
.. _DArenaIterator-reference:
|
||||
|
||||
DArenaIterator
|
||||
==============
|
||||
|
||||
Iterator for allocs obtained from a :cpp:class:`xo::mm::DArena`.
|
||||
|
||||
Context
|
||||
-------
|
||||
|
||||
.. ditaa::
|
||||
:--scale: 0.99
|
||||
|
||||
+-----------------------------------------------------+
|
||||
| DArena |
|
||||
| DArenaIterator cBLU|
|
||||
+-----------------------------------------------------+
|
||||
| ArenaConfig |
|
||||
+--------------+------------------------+-------------+
|
||||
| | AllocInfo | |
|
||||
| +------------------------+ |
|
||||
| AllocError | AllocHeaderConfig | cmpresult |
|
||||
| +------------------------+ |
|
||||
| | AllocHeader | |
|
||||
+--------------+------------------------+-------------+
|
||||
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
#include <xo/arena/DArenaIterator.hpp>
|
||||
|
||||
Class
|
||||
-----
|
||||
|
||||
.. doxygenclass:: xo::mm::DArenaIterator
|
||||
|
||||
Member Variables
|
||||
----------------
|
||||
|
||||
.. doxygengroup:: mm-arenaiterator-instance-vars
|
||||
|
||||
Constructors
|
||||
------------
|
||||
|
||||
.. doxygengroup:: mm-arenaiterator-ctors
|
||||
|
||||
Methods
|
||||
-------
|
||||
|
||||
.. doxygengroup:: mm-arenaiterator-methods
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
build
|
||||
|
||||
+-----------------------------------------------+
|
||||
| cmake |
|
||||
| CMakeLists.txt |
|
||||
| $PREFIX/share/cmake/xo_macros/xo_cxx.cmake |
|
||||
+-----------------------------------------------+
|
||||
|
|
||||
| +----------------------+
|
||||
+------------------------------------------------->| .build/docs/Doxyfile |
|
||||
| +----------------------+
|
||||
| |
|
||||
| /------------/
|
||||
| |
|
||||
| v
|
||||
| +---------------------------------------+ +-----------------+
|
||||
+---->| doxygen |--->| .build/docs/dox |
|
||||
| | $PREFIX/share/xo-macros/Doxyfile.in | | +- html/ |
|
||||
| +---------------------------------------+ | +- xml/ |
|
||||
| +-----------------+
|
||||
| |
|
||||
| /------------/
|
||||
| |
|
||||
| v
|
||||
| +---------------------------------------+ +--------------------+
|
||||
\---->| sphinx |--->| .build/docs/sphinx |
|
||||
| +- conf.py | | +- html/ |
|
||||
| +- _static/ | +--------------------+
|
||||
| +- *.rst |
|
||||
+---------------------------------------+
|
||||
|
||||
files
|
||||
|
||||
README this file
|
||||
CMakeLists.txt build entry point
|
||||
conf.py sphinx config
|
||||
_static static files for sphinx
|
||||
|
||||
map
|
||||
|
||||
index.rst
|
||||
+- examples.rst
|
||||
+- ArenaConfig-reference.rst
|
||||
+- DArena-reference.rst
|
||||
+- DArenaIterator-reference.rst
|
||||
+- AllocInfo-reference.rst
|
||||
+- cmpresult-reference.rst
|
||||
+- glossary.rst
|
||||
...
|
||||
|
||||
examples
|
||||
|
||||
.. doxygenclass:: ${c++ class name}
|
||||
:project:
|
||||
:path:
|
||||
:members:
|
||||
:protected-members:
|
||||
:private-members:
|
||||
:undoc-members:
|
||||
:member-groups:
|
||||
:members-only:
|
||||
:outline:
|
||||
:no-link:
|
||||
:allow-dot-graphs:
|
||||
|
||||
.. doxygendefine:: ${c preprocessor define}
|
||||
|
||||
.. doxygenconcept:: ${c++ concept definition}
|
||||
|
||||
.. doxygenenum:: ${c++ enum definition}
|
||||
|
||||
.. doxygenfunction:: ${c++ function name}
|
||||
1
.xo-arena/docs/_static/README
vendored
1
.xo-arena/docs/_static/README
vendored
|
|
@ -1 +0,0 @@
|
|||
add any static {.html, .js, ..} files for sphinx to pickup here
|
||||
BIN
.xo-arena/docs/_static/img/favicon.ico
vendored
BIN
.xo-arena/docs/_static/img/favicon.ico
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 303 KiB |
|
|
@ -1,50 +0,0 @@
|
|||
.. _cmpresult-reference:
|
||||
|
||||
cmpresult
|
||||
=========
|
||||
|
||||
Represent the result of a partially ordered comparison
|
||||
|
||||
Context
|
||||
-------
|
||||
|
||||
.. ditaa::
|
||||
:--scale: 0.99
|
||||
|
||||
+-----------------------------------------------------+
|
||||
| DArena |
|
||||
| DArenaIterator |
|
||||
+-----------------------------------------------------+
|
||||
| ArenaConfig |
|
||||
+--------------+------------------------+-------------+
|
||||
| | AllocInfo | cBLU|
|
||||
| +------------------------+ |
|
||||
| AllocError | AllocHeaderConfig | cmpresult |
|
||||
| +------------------------+ |
|
||||
| | AllocHeader | |
|
||||
+--------------+------------------------+-------------+
|
||||
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
#include <xo/arena/cmpresult.hpp>
|
||||
|
||||
Class
|
||||
-----
|
||||
|
||||
.. doxygenclass:: xo::mm::cmpresult
|
||||
|
||||
Constructors
|
||||
------------
|
||||
|
||||
.. doxygengroup:: mm-cmpresult-ctors
|
||||
|
||||
Methods
|
||||
-------
|
||||
|
||||
.. doxygengroup:: mm-cmpresult-methods
|
||||
|
||||
Member Variables
|
||||
----------------
|
||||
|
||||
.. doxygengroup:: mm-cmpresult-instance-vars
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
# For the full list of built-in configuration values, see the documentation:
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||
|
||||
project = 'xo arena documentation'
|
||||
copyright = '2025, Roland Conybeare'
|
||||
author = 'Roland Conybeare'
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||
|
||||
#extensions = []
|
||||
extensions = [ "breathe",
|
||||
"sphinx.ext.mathjax", # inline math
|
||||
"sphinx.ext.autodoc", # generate info from docstrings
|
||||
"sphinxcontrib.ditaa", # diagrams-through-ascii-art
|
||||
"sphinxcontrib.plantuml" # text -> uml diagrams
|
||||
]
|
||||
|
||||
# note: breathe requires doxygen xml output -> must have GENERATE_XML = YES in Doxyfile.in
|
||||
# match project name in Doxyfile.in
|
||||
breathe_default_project = "xodoxxml"
|
||||
|
||||
templates_path = ['_templates']
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
||||
|
||||
#html_theme = 'alabaster'
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
html_static_path = ['_static']
|
||||
html_favicon = '_static/img/favicon.ico'
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
.. _examples:
|
||||
|
||||
.. toctree
|
||||
:maxdepth: 2
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
Arena allocation
|
||||
-----------------
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
#include <xo/alloc2/arena/DArena.hpp>
|
||||
|
||||
using namespace xo::mm;
|
||||
using namespace std;
|
||||
|
||||
|
||||
Create an arena:
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
// create arena, size 64k
|
||||
DArena arena = DArena::map(ArenaConfig { .size_ = 64*1024; });
|
||||
|
||||
cout << arena.lo() << ".." << arena.hi();
|
||||
|
||||
This determines a VM memory address range.
|
||||
Actually address range is rounded up to a whole number of VM pages.
|
||||
Size here is a hard maximum. It cannot be changed for this arena instance.
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
arena.reserved(); // 64k
|
||||
arena.committed(); // 0k
|
||||
arena.allocated(); // ok
|
||||
arena.available(); // 0k
|
||||
|
||||
Although we know the address range for arena, it doesn't own any physical
|
||||
memory yet. Two ways to commit memory:
|
||||
|
||||
1. Attempt allocation:
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
std::byte * mem = arena.alloc(5*1024);
|
||||
if (!mem)
|
||||
throw std::runtime_error("alloc failed");
|
||||
|
||||
arena.reserved(); // 64k
|
||||
arena.committed(); // 8k - 2 pages
|
||||
arena.allocateed(); // 5k
|
||||
arena.available(); // 3k
|
||||
|
||||
2. Expand committed memory explicitly:
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
bool ok = arena.expand(5*1024);
|
||||
assert(ok);
|
||||
|
||||
arena.reserved(); // 64k
|
||||
arena.committed(); // 8k - 2 pages
|
||||
arena.allocated(); // 0k
|
||||
arena.available(); // 8k
|
||||
|
||||
Examining alloc metadata
|
||||
------------------------
|
||||
|
||||
Given a successful allocation:
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
std::size_t req_z = 5*1024;
|
||||
std::byte * mem = arena.alloc(req_z);
|
||||
if (!mem)
|
||||
throw std::runtime_error("alloc failed");
|
||||
|
||||
AllocInfo info = arena.alloc_info(mem);
|
||||
|
||||
info.payload(); // [mem, mem + req_z (+ up to 7 bytes padding)]
|
||||
info.is_valid(); // true
|
||||
info.guard_lo(); // guard bytes preceding alloc
|
||||
info.guard_hi(); // guard bytes following alloc
|
||||
|
||||
Can alternatively scan all live allocs in arena:
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
for (AllocInfo info : arena) {
|
||||
info.payload(); // allocated memory range
|
||||
info.is_valid(); // true
|
||||
info.guard_lo(); // guard bytes preceding alloc
|
||||
info.guard_hi(); // guard bytes following alloc
|
||||
}
|
||||
|
||||
Recycling memory
|
||||
----------------
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
// arena in non-empty state
|
||||
arena.reserved(); // 64k
|
||||
arena.committed(); // 8k - 2 pages
|
||||
arena.allocateed(); // 5k
|
||||
arena.available(); // 3k
|
||||
|
||||
arena.clear();
|
||||
|
||||
arena.reserved() // 64k
|
||||
arena.committed(); // 8k - 2 pages
|
||||
arena.allocated(); // 0k
|
||||
arena.available(); // 8k
|
||||
|
||||
Memory recycled by :cpp:func:`DArena::clear()`
|
||||
is available for reuse by application; it's still owned by arena.
|
||||
We're just resetting the free pointer back to the beginning of arena
|
||||
memory.
|
||||
|
||||
To release memory to the operating system, destroy arena:
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
arena.~DArena(); // or just let arena go out of scope
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
.. _glossary:
|
||||
|
||||
Glossary
|
||||
--------
|
||||
|
||||
.. glossary::
|
||||
FOMO
|
||||
| faceted object model
|
||||
|
||||
page
|
||||
| a (4k) page of virtual memory.
|
||||
| O/S manages virtual memory in chunks of this size.
|
||||
|
||||
hugepage
|
||||
| large (2MB) VM page; use to reduce page fault expense and TLB pressure.
|
||||
|
||||
THP
|
||||
| transparent huge pages
|
||||
|
||||
TLB
|
||||
| translation lookaside buffer
|
||||
|
||||
VM
|
||||
| virtual memory
|
||||
|
|
@ -1,153 +0,0 @@
|
|||
.. _implementation:
|
||||
|
||||
Implementation
|
||||
==============
|
||||
|
||||
Library dependency tower for *xo-arena*
|
||||
|
||||
.. ditaa::
|
||||
|
||||
+------------------------------------+
|
||||
| xo_arena |
|
||||
+-----------------+------------------+
|
||||
| xo_indentlog | xo_reflectutil |
|
||||
+-----------------+------------------+
|
||||
| xo_cmake |
|
||||
+------------------------------------+
|
||||
|
||||
Abstraction tower for *xo-arena* components (simplified)
|
||||
|
||||
.. ditaa::
|
||||
:--scale: 0.99
|
||||
|
||||
+-------------------+
|
||||
| DArena |
|
||||
| DArenaIterator |
|
||||
+-------------------+
|
||||
| ArenaConfig |
|
||||
+-------------------+
|
||||
| auxiliary types |
|
||||
+-------------------+
|
||||
|
||||
|
||||
Abstraction tower for *xo-arena* components (detailed)
|
||||
|
||||
.. ditaa::
|
||||
:--scale: 0.99
|
||||
|
||||
+-----------------------------------------------------+
|
||||
| DArena |
|
||||
| DArenaIterator |
|
||||
+-----------------------------------------------------+
|
||||
| ArenaConfig |
|
||||
+--------------+------------------------+-------------+
|
||||
| | AllocInfo | |
|
||||
| +------------------------+ |
|
||||
| AllocError | AllocHeaderConfig | cmpresult |
|
||||
| +------------------------+ |
|
||||
| | AllocHeader | |
|
||||
+--------------+------------------------+-------------+
|
||||
|
||||
.. list-table:: Native Arena Allocator
|
||||
:header-rows: 1
|
||||
:widths: 20 90
|
||||
|
||||
* - Class
|
||||
- Description
|
||||
* - ``ArenaConfig``
|
||||
- Configuration for a ``DArena`` instance
|
||||
* - ``DArena``
|
||||
- VM-aware arena allocator
|
||||
* - ``DArenaIterator``
|
||||
- Iterator over ``DArena`` allocations
|
||||
|
||||
.. list-table:: Auxiliary/Support Types
|
||||
:header-rows: 1
|
||||
:widths: 20 90
|
||||
|
||||
* - Class
|
||||
- Description
|
||||
* - ``AllocError``
|
||||
- Return type for an alloc request, with error details.
|
||||
* - ``AllocInfo``
|
||||
- An opaque allocation. Value of an alloc-iterator.
|
||||
* - ``AllocHeaderConfig``
|
||||
- Per-allocator configuration of alloc headers
|
||||
* - ``AllocHeader``
|
||||
- Per-allocation header (8 bytes)
|
||||
* - ``cmpresult``
|
||||
- Result of alloc-iterator comparison
|
||||
|
||||
Example Object Diagram
|
||||
|
||||
.. uml::
|
||||
:caption: representation for an arena allocator
|
||||
:scale: 99%
|
||||
:align: center
|
||||
|
||||
object darena1<<DArena>>
|
||||
darena1 : config
|
||||
darena1 : lo
|
||||
darena1 : hi
|
||||
darena1 : free
|
||||
darena1 : limit
|
||||
darena1 : last_error
|
||||
|
||||
object header1<<ArenaConfig>>
|
||||
header1 : size
|
||||
header1 : header
|
||||
|
||||
object hconfig1<<AllocHeaderConfig>>
|
||||
hconfig1 : guard_z
|
||||
hconfig1 : guard_byte
|
||||
hconfig1 : tseq_bits
|
||||
hconfig1 : age_bits
|
||||
hconfig1 : size_bits
|
||||
|
||||
darena1 o-- header1
|
||||
header1 o-- hconfig1
|
||||
|
||||
.. uml::
|
||||
:caption: memory layout
|
||||
:scale: 99%
|
||||
:align: center
|
||||
|
||||
object darena1<<DArena>>
|
||||
darena1 : config
|
||||
darena1 : lo
|
||||
darena1 : hi
|
||||
darena1 : free
|
||||
darena1 : limit
|
||||
darena1 : last_error
|
||||
|
||||
rectangle "allocated" #90EE90 {
|
||||
note as n1
|
||||
lo -> free
|
||||
objects here
|
||||
end note
|
||||
}
|
||||
|
||||
rectangle "available" #FFFFE0 {
|
||||
note as n2
|
||||
free -> limit
|
||||
alloc from here
|
||||
end note
|
||||
}
|
||||
|
||||
rectangle "uncommitted" #D3D3D3 {
|
||||
note as n3
|
||||
limit -> hi
|
||||
not mapped yet
|
||||
end note
|
||||
}
|
||||
|
||||
darena1 -[hidden]down- n1
|
||||
n1 -[hidden]down- n2
|
||||
n2 -[hidden]down- n3
|
||||
|
||||
Remarks:
|
||||
|
||||
* See xo-alloc2 for abstract allocator trait *AAllocator*
|
||||
along with its application to *DArena*.
|
||||
* We split these because in *xo-facet* we rely on *DArena* to implement
|
||||
double-dispatch (two-dimensional vtables, as seen for example in CLOS, Julia, Mathematica).
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
# xo-arena documentation master file
|
||||
|
||||
xo-arena documentation
|
||||
======================
|
||||
|
||||
xo-arena provides:
|
||||
|
||||
* Fast vm-aware arena allocation.
|
||||
* Allocates uncommitted virtual memory, and commits on demand.
|
||||
* When available, uses THP (Transparent Huge Pages) to mitigate pagetable pressure.
|
||||
* Optional GC support, with per-alloc header.
|
||||
|
||||
Diagnostic features:
|
||||
|
||||
* with alloc headers: forward iterators over individual allocations
|
||||
* configurable guard memory between allocations.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: xo-arena contents
|
||||
|
||||
examples
|
||||
implementation
|
||||
ArenaConfig-reference
|
||||
DArena-reference
|
||||
DArenaIterator-reference
|
||||
AllocInfo-reference
|
||||
cmpresult-reference
|
||||
glossary
|
||||
genindex
|
||||
search
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
/** @file AllocError.hpp
|
||||
*
|
||||
* @author Roland Conybeare, Dec 2025
|
||||
**/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
|
||||
namespace xo {
|
||||
namespace mm {
|
||||
enum class error : int32_t {
|
||||
/** sentinel **/
|
||||
invalid = -1,
|
||||
/** not an error **/
|
||||
ok,
|
||||
/** reserved size exhauged **/
|
||||
reserve_exhausted,
|
||||
/** unable to commit (i.e. mprotect failure) **/
|
||||
commit_failed,
|
||||
/** allocation size too big (See @ref ArenaConfig::header_size_mask_) **/
|
||||
header_size_mask,
|
||||
/** sub_alloc not preceded by super alloc (or another sub_alloc) **/
|
||||
orphan_sub_alloc,
|
||||
/** attempt to call alloc_info for allocator with alloc header feature disabled
|
||||
* (e.g. @ref see ArenaConfig::store_header_flag_)
|
||||
**/
|
||||
alloc_info_disabled,
|
||||
/** attempt to call alloc_info for address not owned by allocator **/
|
||||
alloc_info_address,
|
||||
/** for example: alloc iteration not supported in arenas with
|
||||
* AllocConfig.store_header_flag_ = false
|
||||
**/
|
||||
alloc_iterator_not_supported,
|
||||
/** attempt to deref an iterator that does not refer to an alloc **/
|
||||
alloc_iterator_deref,
|
||||
/** attempt to advance an iterator that does not refer to an alloc **/
|
||||
alloc_iterator_next,
|
||||
};
|
||||
|
||||
struct AllocError {
|
||||
using size_type = std::size_t;
|
||||
using value_type = std::byte*;
|
||||
|
||||
AllocError() = default;
|
||||
explicit AllocError(error err,
|
||||
uint32_t seq) : error_{err},
|
||||
error_seq_{seq} {}
|
||||
AllocError(error err,
|
||||
const char * src_fn,
|
||||
uint32_t seq,
|
||||
size_type req_z,
|
||||
size_type com_z,
|
||||
size_type rsv_z) : error_{err},
|
||||
src_fn_{src_fn},
|
||||
error_seq_{seq},
|
||||
request_z_{req_z},
|
||||
committed_z_{com_z},
|
||||
reserved_z_{rsv_z} {}
|
||||
|
||||
static const char * error_description(error x);
|
||||
|
||||
/** error code **/
|
||||
error error_ = error::ok;
|
||||
/** source function. Typically injected with __PRETTY_FUNCTION__
|
||||
* somewhere suitable on stack
|
||||
**/
|
||||
const char * src_fn_ = nullptr;
|
||||
/** sequence# of this error.
|
||||
* Each error event within an allocator gets next sequence number
|
||||
**/
|
||||
uint32_t error_seq_ = 0;
|
||||
/** reqeust size assoc'd with errror **/
|
||||
size_type request_z_ = 0;
|
||||
/** committed allocator memory at time of error **/
|
||||
size_type committed_z_ = 0;
|
||||
/** reserved allocator memory at time of error **/
|
||||
size_type reserved_z_ = 0;
|
||||
};
|
||||
} /*namespace mm*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end AllocError.hpp */
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
/** @file AllocHeader.hpp
|
||||
*
|
||||
* @author Roland Conybeare, Dec 2025
|
||||
**/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <type_traits>
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
|
||||
namespace xo {
|
||||
namespace mm {
|
||||
/** @brief per-alloc header
|
||||
*
|
||||
* Appears immediately before each allocation when
|
||||
* ArenaConfig.store_header_flag_ is set.
|
||||
*
|
||||
* See AllocInfo.hpp for encoding of @ref repr_
|
||||
**/
|
||||
struct AllocHeader {
|
||||
using repr_type = std::uintptr_t;
|
||||
using size_type = std::size_t;
|
||||
|
||||
explicit AllocHeader(repr_type x) : repr_{x} {}
|
||||
|
||||
repr_type repr_;
|
||||
};
|
||||
|
||||
static_assert(sizeof(AllocHeader) == sizeof(AllocHeader::repr_type));
|
||||
static_assert(std::is_standard_layout_v<AllocHeader>);
|
||||
}
|
||||
}
|
||||
|
||||
/* end AllocHeader.hpp */
|
||||
|
|
@ -1,180 +0,0 @@
|
|||
/** @file AllocHeaderConfig.hpp
|
||||
*
|
||||
* @author Roland Conybeare, Dec 2025
|
||||
**/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "AllocHeader.hpp"
|
||||
#include "padding.hpp"
|
||||
#include <utility>
|
||||
|
||||
namespace xo {
|
||||
namespace mm {
|
||||
/**
|
||||
* @brief specifies alloc header layout
|
||||
*
|
||||
* Each allocation is preceded by a 64-bit header.
|
||||
* Header is split into 3 configurable-width bit fields,
|
||||
* labelled (from hi to lo bit order) {tseq, age, size}.
|
||||
*
|
||||
* 1. tseq. seq# identifying object types; needed for gc.
|
||||
* 2. gen. age cohort; increases when alloc survives gc.
|
||||
* 3. size. alloc size.
|
||||
*
|
||||
* Arena allocator only uses size.
|
||||
* X1 collector uses {tseq, gen, size}
|
||||
*
|
||||
* alloc header
|
||||
*
|
||||
* TTTTTTTTTTTTGGGGGZZZZZZZZZZZZ
|
||||
* < tseq ><gen>< size >
|
||||
*
|
||||
* masking
|
||||
*
|
||||
* ..432107654321076543210 bit
|
||||
*
|
||||
* > < .gen_bits
|
||||
* 0..............01111111 gen_mask_unshifted
|
||||
* 0..011111110..........0 gen_mask_shifted
|
||||
* > < gen_shift
|
||||
**/
|
||||
struct AllocHeaderConfig {
|
||||
using repr_type = AllocHeader;
|
||||
using span_type = std::pair<const std::byte *, const std::byte *>;
|
||||
|
||||
AllocHeaderConfig() = default;
|
||||
AllocHeaderConfig(std::uint32_t gz,
|
||||
std::uint8_t guard_byte,
|
||||
std::uint8_t t,
|
||||
std::uint8_t a,
|
||||
std::uint8_t z) noexcept : guard_z_{gz},
|
||||
guard_byte_{guard_byte},
|
||||
tseq_bits_{t},
|
||||
age_bits_{a},
|
||||
size_bits_{z} {}
|
||||
|
||||
/** create header tuple (@p t, @p a, @p z)
|
||||
* with typeseq @p t, age @p a, size @p z
|
||||
**/
|
||||
std::uint64_t mkheader(std::uint64_t t,
|
||||
std::uint64_t a,
|
||||
std::uint64_t z) const noexcept {
|
||||
|
||||
// don't let age wrap around.
|
||||
// Expect std::min() to compile to cmov (no branch)
|
||||
//
|
||||
a = std::min(a, this->max_age());
|
||||
|
||||
uint64_t tseq_bits = (t << (age_bits_ + size_bits_)) & tseq_mask();
|
||||
uint64_t age_bits = (a << size_bits_) & age_mask();
|
||||
uint64_t size_bits = z & size_mask();;
|
||||
|
||||
return (tseq_bits | age_bits | size_bits);
|
||||
}
|
||||
|
||||
std::uint64_t tseq_mask() const noexcept {
|
||||
// e.g.
|
||||
// FF FF FF 00 00 00 00 00
|
||||
// with tseq_bits=24, age_bits=8, size_bits=32
|
||||
//
|
||||
return ((1ul << tseq_bits_) - 1) << (age_bits_ + size_bits_);
|
||||
}
|
||||
|
||||
std::uint64_t max_age() const noexcept {
|
||||
return ((1ul << age_bits_) - 1);
|
||||
}
|
||||
|
||||
std::uint64_t age_mask() const noexcept {
|
||||
// e.g.
|
||||
// 00 00 00 FF 00 00 00 00
|
||||
// with age_bits=8, size_bits=32
|
||||
//
|
||||
return this->max_age() << size_bits_;
|
||||
}
|
||||
|
||||
std::uint64_t size_mask() const noexcept {
|
||||
// e.g.
|
||||
// 00 00 00 00 FF FF FF FF
|
||||
// with size_bits=32
|
||||
//
|
||||
return ((1ul << size_bits_) - 1);
|
||||
}
|
||||
|
||||
/** extract type id from alloc header @p hdr **/
|
||||
std::uint32_t tseq(repr_type hdr) const noexcept {
|
||||
// e.g.
|
||||
// 0x302010
|
||||
// for header
|
||||
// 30 20 10 -- -- -- -- --
|
||||
// with tseq_bits_ = 24, age_bits_ + size_bits_ = 40
|
||||
//
|
||||
return (hdr.repr_ & tseq_mask()) >> (age_bits_ + size_bits_);
|
||||
}
|
||||
|
||||
/** extract age from alloc header @p hdr **/
|
||||
std::uint32_t age(repr_type hdr) const noexcept {
|
||||
// e.g.
|
||||
// 0xa0
|
||||
// for header
|
||||
// -- -- -- a0 -- -- -- --
|
||||
// with age_bits_ = 8, size_bits_ = 32
|
||||
//
|
||||
return (hdr.repr_ & age_mask()) >> size_bits_;
|
||||
}
|
||||
|
||||
/** extract size from alloc header @p hdr **/
|
||||
std::size_t size(repr_type hdr) const noexcept {
|
||||
// e.g.
|
||||
// 0x01020300
|
||||
// for header
|
||||
// -- -- -- -- 01 02 03 00
|
||||
// with size_bits_ = 32
|
||||
//
|
||||
return (hdr.repr_ & size_mask());
|
||||
}
|
||||
|
||||
/** extract padded size from alloc header @p hdr **/
|
||||
std::size_t size_with_padding(repr_type hdr) const noexcept {
|
||||
return padding::with_padding(this->size(hdr));
|
||||
}
|
||||
|
||||
/** true iff sentinel tseq, flagging a forwarding pointer **/
|
||||
bool is_forwarding_tseq(repr_type hdr) const noexcept {
|
||||
// e.g.
|
||||
// 0xFFFFFF
|
||||
// i.e. header
|
||||
// FF FF FF -- -- -- -- --
|
||||
// with tseq_bits_ = 24, age_bits + size_bits_ = 40
|
||||
//
|
||||
return (hdr.repr_ & tseq_mask()) == tseq_mask();
|
||||
}
|
||||
|
||||
bool is_size_enabled() const noexcept { return size_bits_ > 0; }
|
||||
|
||||
/** construct alloc header for a forwarding object **/
|
||||
AllocHeader mark_forwarding_tseq(AllocHeader hdr) const noexcept {
|
||||
return AllocHeader((hdr.repr_ & ~tseq_mask()) | tseq_mask());
|
||||
}
|
||||
|
||||
/** if non-zero, allocate extra space between allocs, and fill
|
||||
* with fixed test-pattern contents. Allows for simple
|
||||
* runtime arena sanitizing checks.
|
||||
* Will be rounded up to multiple of @ref padding::c_alloc_alignment
|
||||
**/
|
||||
std::uint32_t guard_z_ = 0;
|
||||
/** if guard_z_ > 0, write at least that many copies
|
||||
* of this guard byte following each complete allocation
|
||||
**/
|
||||
std::uint8_t guard_byte_ = 0xfd;
|
||||
/** number of bits for tseq **/
|
||||
std::uint8_t tseq_bits_ = 24;
|
||||
/** number of bits for age **/
|
||||
std::uint8_t age_bits_ = 8;
|
||||
/** number of bits for size **/
|
||||
std::uint8_t size_bits_ = 32;
|
||||
};
|
||||
} /*namespace mm*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end AllocHeaderConfig.hpp */
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
/** @file AllocInfo.hpp
|
||||
*
|
||||
* @author Roland Conybeare, Dec 2025
|
||||
**/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "AllocHeaderConfig.hpp"
|
||||
#include <utility>
|
||||
|
||||
namespace xo {
|
||||
namespace mm {
|
||||
/** @class AllocInfo
|
||||
* @brief bookkeeping information for an allocation
|
||||
*
|
||||
* AllocInfo instances are 1:1 with sum of calls to
|
||||
* {@ref AAllocator::alloc, @ref AAllocator::alloc_super}
|
||||
*
|
||||
**/
|
||||
class AllocInfo {
|
||||
public:
|
||||
/** @defgroup mm-allocinfo-traits **/
|
||||
///@{
|
||||
|
||||
using size_type = AllocHeader::size_type;
|
||||
using byte = std::byte;
|
||||
using span_type = std::pair<const byte *, const byte *>;
|
||||
|
||||
///@}
|
||||
|
||||
/** @defgroup mm-allocinfo-ctors **/
|
||||
///@{
|
||||
|
||||
AllocInfo(const AllocHeaderConfig * p_cfg,
|
||||
const byte * p_guard_lo,
|
||||
const AllocHeader * p_hdr,
|
||||
const byte * p_guard_hi) : p_config_{p_cfg},
|
||||
p_guard_lo_{p_guard_lo},
|
||||
p_header_{p_hdr},
|
||||
p_guard_hi_{p_guard_hi} {}
|
||||
|
||||
/** error when alloc-header not configured **/
|
||||
static AllocInfo error_not_configured(const AllocHeaderConfig * p_cfg) {
|
||||
return AllocInfo(p_cfg, nullptr, nullptr, nullptr);
|
||||
}
|
||||
/** error on deref empty iterator **/
|
||||
static AllocInfo error_invalid_iterator(const AllocHeaderConfig * p_cfg) {
|
||||
return AllocInfo(p_cfg, nullptr, nullptr, nullptr);
|
||||
}
|
||||
|
||||
///@}
|
||||
|
||||
/** @defgroup mm-allocinfo-methods **/
|
||||
///@{
|
||||
|
||||
AllocHeader header() const noexcept { return *p_header_; }
|
||||
|
||||
/** true for non-sentinel AllocInfo instance **/
|
||||
bool is_valid() const noexcept { return ((p_config_ != nullptr)
|
||||
&& (p_header_ != nullptr)); }
|
||||
/** true iff sentinel tseq, flagging a forwarding pointer **/
|
||||
bool is_forwarding_tseq() const noexcept {
|
||||
return p_config_->is_forwarding_tseq(*p_header_);
|
||||
}
|
||||
|
||||
/** Guard bytes preceding allocation-header **/
|
||||
span_type guard_lo() const noexcept;
|
||||
/** Type sequence number in garbage collector **/
|
||||
std::uint32_t tseq() const noexcept { return p_config_->tseq(*p_header_); }
|
||||
/** Allocation age in garbage collector **/
|
||||
std::uint32_t age() const noexcept { return p_config_->age (*p_header_); }
|
||||
/** Allocation size (including allocator-supplied padding, excluding alloc header) **/
|
||||
size_type size() const noexcept { return p_config_->size(*p_header_); }
|
||||
/** Payload for this allocation. This is the memory available to application **/
|
||||
span_type payload() const noexcept;
|
||||
/** Guard bytes immediately following allocation **/
|
||||
span_type guard_hi() const noexcept;
|
||||
/** Number of guard bytes **/
|
||||
size_type guard_z() const noexcept { return p_config_->guard_z_; }
|
||||
/** Value (fixed test pattern) of guard byte **/
|
||||
uint8_t guard_byte() const noexcept { return p_config_->guard_byte_; }
|
||||
|
||||
///@}
|
||||
|
||||
/** @defgroup mm-allocinfo-instance-vars **/
|
||||
///@{
|
||||
|
||||
const AllocHeaderConfig * p_config_ = nullptr;
|
||||
const byte * p_guard_lo_ = nullptr;
|
||||
const AllocHeader * p_header_ = nullptr;
|
||||
const byte * p_guard_hi_ = nullptr;
|
||||
|
||||
///@}
|
||||
};
|
||||
} /*namespace mm*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end AllocInfo.hpp */
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
/** @file ArenaConfig.hpp
|
||||
*
|
||||
* @author Roland Conybeare, Dec 2025
|
||||
**/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "AllocHeaderConfig.hpp"
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
|
||||
namespace xo {
|
||||
namespace mm {
|
||||
|
||||
/** @class ArenaConfig
|
||||
*
|
||||
* @brief configuration for a @ref DArena instance
|
||||
**/
|
||||
struct ArenaConfig {
|
||||
/** @defgroup mm-arenaconfig-ctors **/
|
||||
///@{
|
||||
|
||||
/** NOTE: not providing explicit ctors so we can use designated initializers **/
|
||||
|
||||
ArenaConfig with_name(std::string name) const {
|
||||
ArenaConfig copy(*this);
|
||||
copy.name_ = name;
|
||||
return copy;
|
||||
}
|
||||
|
||||
ArenaConfig with_size(std::size_t z) const {
|
||||
ArenaConfig copy(*this);
|
||||
copy.size_ = z;
|
||||
return copy;
|
||||
}
|
||||
|
||||
ArenaConfig with_store_header_flag(bool x) const {
|
||||
ArenaConfig copy(*this);
|
||||
copy.store_header_flag_ = x;
|
||||
return copy;
|
||||
}
|
||||
|
||||
///@}
|
||||
/** @defgroup mm-arenaconfig-instance-vars ArenaConfig members **/
|
||||
///@{
|
||||
|
||||
/** optional name, for diagnostics **/
|
||||
std::string name_;
|
||||
/** desired arena size -- hard max = reserved virtual memory **/
|
||||
std::size_t size_ = 0;
|
||||
/** hugepage size -- using huge pages relieves some TLB pressure
|
||||
* (provided you use their full extent :)
|
||||
**/
|
||||
std::size_t hugepage_z_ = 2 * 1024 * 1024;
|
||||
/** true to store header (8 bytes) at the beginning of each allocation.
|
||||
* necessary and sufficient to allows iterating over allocs
|
||||
* present in arena.
|
||||
**/
|
||||
bool store_header_flag_ = false;
|
||||
/** configuration for per-alloc header **/
|
||||
AllocHeaderConfig header_{};
|
||||
/** true to enable debug logging **/
|
||||
bool debug_flag_ = false;
|
||||
|
||||
///@}
|
||||
};
|
||||
|
||||
} /*namespace mm*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end ArenaConfig.hpp */
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
/** @file ArenaHashMapConfig.hpp
|
||||
*
|
||||
* @author Roland Conybeare, Feb 2026
|
||||
**/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
|
||||
namespace xo {
|
||||
namespace map {
|
||||
/** @class ArenaHashMapConfig
|
||||
*
|
||||
* @brief configuration for a @ref DArenaHashMap instance
|
||||
**/
|
||||
struct ArenaHashMapConfig {
|
||||
/** @defgroup map-arenahashmapconfig-ctors **/
|
||||
///@{
|
||||
|
||||
ArenaHashMapConfig with_name(std::string name) const {
|
||||
ArenaHashMapConfig copy(*this);
|
||||
copy.name_ = name;
|
||||
return copy;
|
||||
}
|
||||
|
||||
ArenaHashMapConfig with_hint_max_capacity(std::size_t z) const {
|
||||
ArenaHashMapConfig copy(*this);
|
||||
copy.hint_max_capacity_ = z;
|
||||
return copy;
|
||||
}
|
||||
|
||||
ArenaHashMapConfig with_debug_flag(bool x) const {
|
||||
ArenaHashMapConfig copy(*this);
|
||||
copy.debug_flag_ = x;
|
||||
return copy;
|
||||
}
|
||||
|
||||
///@}
|
||||
/** @defgroup mm-arenahashmapconfig-instance-vars ArenaHashMapConfig members **/
|
||||
///@{
|
||||
|
||||
/** optional name, for diagnostics **/
|
||||
std::string name_;
|
||||
/** desired hard max hashmap size -> reserved virtual memory
|
||||
* hint: actual max may be larger, because of power-of-2 considerations.
|
||||
**/
|
||||
std::size_t hint_max_capacity_ = 0;
|
||||
/** true to enable debug logging **/
|
||||
bool debug_flag_ = false;
|
||||
|
||||
///@}
|
||||
};
|
||||
|
||||
} /*namespace map*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end ArenaHashMapConfig.hpp */
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
/** @file CircularBufferConfig.hpp
|
||||
*
|
||||
* @author Roland Conybeare, Jan 2026
|
||||
**/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace xo {
|
||||
namespace mm {
|
||||
/** @class CircularBufferConfig
|
||||
*
|
||||
* @brief configuration for a @ref DCircularBuffer instance
|
||||
**/
|
||||
struct CircularBufferConfig {
|
||||
/** @defgroup mm-circularbufferconfig-instance-vars CircularBufferConfig members **/
|
||||
///@{
|
||||
|
||||
/** optional name, for diagnostics **/
|
||||
std::string name_;
|
||||
/** hard maximum buffer size = reserved virtual memory.
|
||||
* However actual max will be this value rounded up to at least page size.
|
||||
* Buffer will generally map much less than this amount of memory
|
||||
**/
|
||||
std::size_t max_capacity_ = 0;
|
||||
/** hugepage size -- using huge pages relieves some TLB pressure,
|
||||
* at expense of inefficient memory consumption for (up to two)
|
||||
* partially used superpages.
|
||||
**/
|
||||
std::size_t hugepage_z_ = 2 * 1024 * 1024;
|
||||
/** Threshold 'move efficeincy' = (move_distance / move_qty)
|
||||
* applies to moving unread input to the beginning of mapped range,
|
||||
* when not prevented by pinned ranges.
|
||||
*
|
||||
* Higher numbers reduce cpu consumption but increase memory consumption
|
||||
* Reciprocal loose ceiling on relative effort that may be spent on
|
||||
* moving fractional input
|
||||
**/
|
||||
float threshold_move_efficiency_ = 50.0;
|
||||
/** lower bound for hard maximum number of capture spans.
|
||||
*
|
||||
* Expected use case is to track spans that are currently referenced
|
||||
* (rather than copied) from outside a DCircularBuffer instance.
|
||||
* Circular buffer will not unmap or overwrite memory for such spans.
|
||||
*
|
||||
* Expect to generally release captured spans in the same order they
|
||||
* were captured. Out of order release is supported, but cost
|
||||
* of out-of-order release grows
|
||||
* like O(r) for r remembered spans.
|
||||
*
|
||||
* A typical parser will need spans to remember one line of input
|
||||
**/
|
||||
std::size_t max_captured_span_ = 0;
|
||||
/** true to enable debug logging **/
|
||||
bool debug_flag_ = false;
|
||||
|
||||
///@}
|
||||
};
|
||||
} /*namespace mm*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end CircularBufferConfig.hpp */
|
||||
|
|
@ -1,338 +0,0 @@
|
|||
/** @file DArena.hpp
|
||||
*
|
||||
* @author Roland Conybeare, Dec 2025
|
||||
**/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ArenaConfig.hpp"
|
||||
#include "AllocError.hpp"
|
||||
#include "MemorySizeInfo.hpp"
|
||||
#include "AllocInfo.hpp"
|
||||
#include <xo/reflectutil/typeseq.hpp>
|
||||
|
||||
namespace xo {
|
||||
namespace mm {
|
||||
struct DArenaIterator; // see DArenaIterator.hpp
|
||||
|
||||
/** @class DArena
|
||||
*
|
||||
* @brief represent arena allocator state
|
||||
*
|
||||
* Provides minimal RAII functionality around memory mapping.
|
||||
* For allocation implementation see @ref IAllocator_DArena
|
||||
**/
|
||||
struct DArena {
|
||||
/*
|
||||
* <----------------------------size-------------------------->
|
||||
* <------------committed-----------><-------uncommitted------>
|
||||
* <--allocated-->
|
||||
*
|
||||
* XXXXXXXXXXXXXXX___________________..........................
|
||||
*
|
||||
* [X] allocated: in use
|
||||
* [_] committed: physical memory obtained
|
||||
* [.] uncommitted: mapped in virtual memory, not backed by memory
|
||||
*/
|
||||
|
||||
/** @defgroup mm-arena-traits arena type traits **/
|
||||
///@{
|
||||
|
||||
/** @brief an amount of memory **/
|
||||
using size_type = std::size_t;
|
||||
/** @brief allocation pointer; use for allocation results **/
|
||||
using value_type = std::byte*;
|
||||
/** @brief a contiguous memory range **/
|
||||
using range_type = std::pair<value_type, value_type>;
|
||||
/** @brief type for allocation header (if enabled) **/
|
||||
using header_type = AllocHeader;
|
||||
/** integer identifying a type (see xo::facet::typeid<T>()) **/
|
||||
using typeseq = xo::reflect::typeseq;
|
||||
|
||||
/** @brief mode argument for @ref _alloc **/
|
||||
enum class alloc_mode : uint8_t {
|
||||
/** ordinary alloc. Most common mode **/
|
||||
standard,
|
||||
/** begin a sequence of suballocs that share a single alloc header **/
|
||||
super,
|
||||
/** make a subsidiary allocation on behalf of a preceding super alloc.
|
||||
* Will be followed by at least one more suballoc call.
|
||||
**/
|
||||
sub_incomplete,
|
||||
/** make a subsidiary allocation that completes preceding super alloc. **/
|
||||
sub_complete,
|
||||
};
|
||||
|
||||
/** @brief Checkpoint for unwinding arena state **/
|
||||
struct Checkpoint {
|
||||
Checkpoint() = default;
|
||||
explicit Checkpoint(std::byte * x) : free_{x} {}
|
||||
std::byte * free_ = nullptr;
|
||||
};
|
||||
|
||||
///@}
|
||||
|
||||
/** @defgroup mm-arena-ctors arena constructors and destructors **/
|
||||
///@{
|
||||
|
||||
/** create arena per configuration @p cfg. **/
|
||||
static DArena map(const ArenaConfig & cfg);
|
||||
|
||||
/** null ctor **/
|
||||
DArena() = default;
|
||||
/** create arena from @p cfg. Will reserve memory for allocation **/
|
||||
DArena(const ArenaConfig & cfg);
|
||||
/** ctor from already-mapped (but not committed) address range [lo,hi] **/
|
||||
DArena(const ArenaConfig & cfg,
|
||||
size_type page_z,
|
||||
size_type arena_align_z,
|
||||
value_type lo,
|
||||
value_type hi);
|
||||
/** DArena is not copyable **/
|
||||
DArena(const DArena & other) = delete;
|
||||
/** move ctor **/
|
||||
DArena(DArena && other);
|
||||
/** dtor releases mapped memory **/
|
||||
~DArena();
|
||||
|
||||
/** move-assignment **/
|
||||
DArena & operator=(DArena && other);
|
||||
|
||||
///@}
|
||||
|
||||
/** @defgroup mm-arena-methods **/
|
||||
///@{
|
||||
|
||||
/** false -> not eligible for GC (allocates own memory + not moveable) **/
|
||||
static constexpr bool is_gc_eligible() { return false; }
|
||||
|
||||
/** Reserved memory, in bytes. This is the maximum size of this arena. **/
|
||||
size_type reserved() const noexcept { return hi_ - lo_; }
|
||||
/** Allocated memory in bytes: memory consumed by allocs from this arena,
|
||||
* including administrative overhead (alloc headers + guard bytes)
|
||||
**/
|
||||
size_type allocated() const noexcept { return free_ - lo_; }
|
||||
/** Committed memory in bytes: amount of memory actually backed by physical memory **/
|
||||
size_type committed() const noexcept { return committed_z_; }
|
||||
/** Available committed memory.
|
||||
* This is the amount of memory guaranteed to be usable for future allocs from this arena.
|
||||
**/
|
||||
size_type available() const noexcept { return limit_ - free_; }
|
||||
/** VM page size for this arena (likely 4KB) **/
|
||||
size_type page_z() const noexcept { return page_z_; }
|
||||
/** Last error encountered by this arena **/
|
||||
const AllocError & last_error() const noexcept { return last_error_; }
|
||||
|
||||
/** True iff address @p addr is owned by this arena,
|
||||
* i.e. falls within [@ref lo_, @ref hi_)
|
||||
**/
|
||||
bool contains(const void * addr) const noexcept { return (lo_ <= addr) && (addr < hi_); }
|
||||
|
||||
/** True iff address @p addr is owned by this arena and in allocated regions **/
|
||||
bool contains_allocated(const void * addr) const noexcept { return (lo_ <= addr) && (addr < free_); }
|
||||
|
||||
/** true if arena is mapped i.e. has a reserved address range **/
|
||||
bool is_mapped() const noexcept { return (lo_ != nullptr) && (hi_ != nullptr); }
|
||||
|
||||
/** @ret iterator pointing to the first allocation in this arena **/
|
||||
DArenaIterator begin() const noexcept;
|
||||
/** @ret iterator pointing to just after the last allocation in this arena **/
|
||||
DArenaIterator end() const noexcept;
|
||||
|
||||
/** @ret header for first allocation in this arena **/
|
||||
AllocHeader * begin_header() const noexcept;
|
||||
/** @ret location of header for next (not yet performed!)
|
||||
* allocation in this arena
|
||||
**/
|
||||
AllocHeader * end_header() const noexcept;
|
||||
|
||||
/** report memory use for this arena to @p fn.
|
||||
* For DArena reporting just one pool = arena's memory range
|
||||
**/
|
||||
void visit_pools(const MemorySizeVisitor & fn) const;
|
||||
|
||||
/** get header from allocated object address **/
|
||||
header_type * obj2hdr(void * obj) noexcept;
|
||||
/** get header from allocated object address (const version) **/
|
||||
const header_type * obj2hdr(void * obj) const noexcept;
|
||||
|
||||
/** report alloc book-keeping info for allocation at @p mem
|
||||
*
|
||||
* Require:
|
||||
* 1. @p mem is address returned by allocation on this arena
|
||||
* i.e. by @ref IAllocator_DArena::alloc() or
|
||||
* @ref IAllocator_DArena::alloc_super()
|
||||
* 2. @p mem has not been invalidated since it was allocated
|
||||
* i.e. by call to @ref DArena::clear
|
||||
*
|
||||
* Note: non-const, may stash error details
|
||||
**/
|
||||
AllocInfo alloc_info(value_type mem) const noexcept;
|
||||
|
||||
/** convenience template for allocating for a T-instance **/
|
||||
template <typename T>
|
||||
void * alloc_for(size_type n = sizeof(T)) {
|
||||
return this->alloc(typeseq::id<T>(), n);
|
||||
}
|
||||
|
||||
/** allocate at least @p z bytes of memory.
|
||||
* Return nullptr and capture error if unable to satisfy request.
|
||||
* May expand committed memory, as long as resulting committed size
|
||||
* is no larger than reserved size
|
||||
**/
|
||||
value_type alloc(typeseq t, size_type z);
|
||||
|
||||
/** when store_header_flag enabled:
|
||||
* like alloc(), but combine memory consumed by this alloc
|
||||
* plus following consecutive sub_alloc()'s into a single header.
|
||||
* otherwise equivalent to alloc()
|
||||
**/
|
||||
value_type super_alloc(typeseq t, size_type z);
|
||||
|
||||
/** when store_header_flag enabled:
|
||||
* follow preceding super_alloc() by one or more sub_allocs().
|
||||
* accumulate total allocated size (including padding) into
|
||||
* single header. All sub_allocs() except the last must set
|
||||
* @p complete_flag to false. The last sub_alloc() must set
|
||||
* @p complete_flag to true.
|
||||
**/
|
||||
value_type sub_alloc(size_type z, bool complete_flag);
|
||||
|
||||
/** alloc copy of @p src **/
|
||||
value_type alloc_copy(value_type src);
|
||||
|
||||
/** capture error information: advance error count + set last_error **/
|
||||
void capture_error(error err,
|
||||
const char * src_fn,
|
||||
size_type target_z = 0) const;
|
||||
|
||||
/** alloc driver. shared by alloc(), super_alloc(), sub_alloc() **/
|
||||
value_type _alloc(std::size_t req_z,
|
||||
alloc_mode mode,
|
||||
typeseq tseq,
|
||||
uint32_t age,
|
||||
const char * src_fn);
|
||||
|
||||
/** expand committed space in arena @p d
|
||||
* to size at least @p z, on behalf of @p src_fn
|
||||
* In practice will round up to a multiple of @ref page_z_.
|
||||
**/
|
||||
bool expand(size_type z, const char * src_fn) noexcept;
|
||||
|
||||
/** create initial guard **/
|
||||
void establish_initial_guard() noexcept;
|
||||
|
||||
/** checkpoint arena state. Revert to the same state with
|
||||
* @ref restore
|
||||
**/
|
||||
Checkpoint checkpoint() noexcept { return Checkpoint(free_); }
|
||||
|
||||
/** restore arena state to previously-established checkpoint **/
|
||||
void restore(Checkpoint ckp) noexcept { free_ = ckp.free_; }
|
||||
|
||||
|
||||
/** zero out all allocated memory. Likely use case is diagnostics **/
|
||||
void scrub() noexcept;
|
||||
|
||||
/** discard all allocated memory, return to empty state
|
||||
* Promise:
|
||||
* - committed memory unchanged
|
||||
* - available memory = committed memory
|
||||
**/
|
||||
void clear() noexcept;
|
||||
|
||||
/** release backing memory and reset bookkeeping to the empty state.
|
||||
*
|
||||
* Unmaps [@ref lo_, @ref hi_) (if mapped) and zeroes the bookkeeping
|
||||
* fields {lo_, committed_z_, last_header_, free_, limit_, hi_,
|
||||
* error_count_, last_error_}. @ref config_ (and page_z_/arena_align_z_)
|
||||
* are left intact.
|
||||
*
|
||||
* Idempotent: a second call is a no-op (lo_ has been cleared).
|
||||
* Invoked by ~DArena(); also safe to call on a live arena.
|
||||
*
|
||||
* Note: application code must not rely on observing the zeroed state
|
||||
* after destruction (that would be UB) -- the zeroing is defensive,
|
||||
* to avoid leaving dangling pointers behind.
|
||||
**/
|
||||
void unmap() noexcept;
|
||||
|
||||
/** swap contents (including configuration) with another arena **/
|
||||
void swap(DArena & other) noexcept;
|
||||
|
||||
///@}
|
||||
|
||||
/** @defgroup mm-arena-instance-vars **/
|
||||
///@{
|
||||
|
||||
/** arena configuration **/
|
||||
ArenaConfig config_;
|
||||
|
||||
/** size of a VM page (obtained automatically via getpagesize()). Likely 4k **/
|
||||
size_type page_z_ = 0;
|
||||
|
||||
/** alignment for this arena. In practice will be either page_z_ or cfg.hugepage_z_ **/
|
||||
size_type arena_align_z_ = 0;
|
||||
|
||||
/** arena owns memory in range [@ref lo_, @ref hi_)
|
||||
**/
|
||||
std::byte * lo_ = nullptr;
|
||||
|
||||
/** prefix of this size is committed.
|
||||
* Remainder mapped but uncommitted.
|
||||
**/
|
||||
size_type committed_z_ = 0;
|
||||
|
||||
/** if config_.store_header_flag_:
|
||||
* Pointer to header for last allocation.
|
||||
**/
|
||||
header_type * last_header_ = nullptr;
|
||||
|
||||
/** free pointer.
|
||||
* Memory in range [@ref lo_, @ref free_) current in use
|
||||
**/
|
||||
std::byte * free_ = nullptr;
|
||||
|
||||
/** soft limit; end of committed virtual memory
|
||||
* Memory in range [@ref lo_, @ref limit_) is committed
|
||||
* (backed by physical memory)
|
||||
**/
|
||||
std::byte * limit_ = nullptr;
|
||||
|
||||
/** hard limit; end of reserved virtual memory
|
||||
* Memory in range [@ref limit_, @ref hi_) is uncommitted
|
||||
**/
|
||||
std::byte * hi_ = nullptr;
|
||||
|
||||
/** count runtime errors. Each error updates @ref last_error_ **/
|
||||
uint32_t error_count_ = 0;
|
||||
|
||||
/** capture some error details if/when error **/
|
||||
AllocError last_error_;
|
||||
|
||||
///@}
|
||||
};
|
||||
|
||||
/** construct a @tparam T instance from arguments @p args
|
||||
* using memory obtained from arena @p ialloc
|
||||
**/
|
||||
template <typename T,
|
||||
typename... Args>
|
||||
static T *
|
||||
construct_with(DArena & ialloc, Args&&... args)
|
||||
{
|
||||
using xo::reflect::typeseq;
|
||||
|
||||
typeseq t = typeseq::id<T>();
|
||||
std::byte * mem = ialloc.alloc(t, sizeof(T));
|
||||
|
||||
if (mem)
|
||||
return new (mem) T(std::forward<Args>(args)...);
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} /*namespace mm*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end DArena.hpp */
|
||||
|
|
@ -1,768 +0,0 @@
|
|||
/** @file DArenaHashMap.hpp
|
||||
*
|
||||
* @author Roland Conybeare, Jan 2026
|
||||
**/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ArenaHashMapConfig.hpp"
|
||||
#include "DArenaVector.hpp"
|
||||
#include "hashmap/verify_policy.hpp"
|
||||
#include "hashmap/HashMapStore.hpp"
|
||||
#include "hashmap/DArenaHashMapIterator.hpp"
|
||||
#include <xo/indentlog/scope.hpp>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <utility>
|
||||
#include <cstring>
|
||||
|
||||
namespace xo {
|
||||
namespace map {
|
||||
#ifdef NOT_YET
|
||||
enum class insert_error : int32_t {
|
||||
/** sentinel **/
|
||||
invalid = -1,
|
||||
/** not an error **/
|
||||
ok,
|
||||
};
|
||||
#endif
|
||||
|
||||
/** @brief flat hash map of key-value pairs using dedicated DArenas for storage
|
||||
*
|
||||
* Replicates (to the extent feasible) std::unordered_map<K,V>
|
||||
*
|
||||
* @tparam Key key type.
|
||||
* @tparam Value value type.
|
||||
* @tparam Hash hash function for keys
|
||||
* @tparam Equal equality function for keys
|
||||
**/
|
||||
template <typename Key,
|
||||
typename Value,
|
||||
typename Hash = std::hash<Key>,
|
||||
typename Equal = std::equal_to<void>>
|
||||
struct DArenaHashMap : DArenaHashMapUtil {
|
||||
public:
|
||||
using size_type = DArenaHashMapUtil::size_type;
|
||||
using key_type = Key;
|
||||
using mapped_type = Value;
|
||||
using value_type = std::pair<const Key, Value>;
|
||||
using key_hash = Hash;
|
||||
using key_equal = Equal;
|
||||
using MemorySizeVisitor = xo::mm::MemorySizeVisitor;
|
||||
using byte = std::byte;
|
||||
using group_type = detail::ControlGroup;
|
||||
using store_type = detail::HashMapStore<Key, Value>;
|
||||
using insert_value_type = std::pair<value_type *, bool>;
|
||||
using iterator = detail::DArenaHashMapIterator<Key, Value>;
|
||||
using const_iterator = detail::DArenaHashMapConstIterator<Key, Value>;
|
||||
|
||||
public:
|
||||
/** create hash map **/
|
||||
DArenaHashMap(const ArenaHashMapConfig & cfg);
|
||||
DArenaHashMap(const std::string & name,
|
||||
size_type hint_max_capacity,
|
||||
bool debug_flag = false);
|
||||
DArenaHashMap(const std::string & name,
|
||||
Hash && hash = Hash(),
|
||||
Equal && eq = Equal(),
|
||||
size_type hint_max_capacity = 0,
|
||||
bool debug_flag = false);
|
||||
|
||||
/** true for types that support the AGCObject facet; DArenaHashMap gets its own memory! **/
|
||||
static constexpr bool is_gc_eligible() { return false; }
|
||||
|
||||
size_type empty() const noexcept { return store_.empty(); }
|
||||
size_type groups() const noexcept { return store_.n_group_; }
|
||||
size_type size() const noexcept { return store_.size_; }
|
||||
size_type capacity() const noexcept { return store_.capacity(); }
|
||||
float load_factor() const noexcept { return store_.load_factor(); }
|
||||
|
||||
const_iterator cbegin() const { return this->_begin_aux(); }
|
||||
const_iterator cend() const { return this->_end_aux(); }
|
||||
|
||||
const_iterator begin() const { return this->_begin_aux(); }
|
||||
const_iterator end() const { return this->_end_aux(); }
|
||||
|
||||
iterator begin() { return _promote_iterator(_begin_aux()); }
|
||||
iterator end() { return _promote_iterator(_end_aux()); }
|
||||
|
||||
void visit_pools(const MemorySizeVisitor & visitor) const {
|
||||
return store_.visit_pools(visitor);
|
||||
}
|
||||
|
||||
/** insert @p kv_pair into hash map.
|
||||
* Replaces any previous value stored under the same key.
|
||||
*
|
||||
* Return pair retval with:
|
||||
* retval.first: address of slots_[p] at which pair inserted/updated
|
||||
* retval.second: true if size incremented;
|
||||
*
|
||||
* When table is full retval.second will be nullptr,
|
||||
* with error captured in last_error_
|
||||
**/
|
||||
insert_value_type try_insert(const value_type & kv_pair);
|
||||
|
||||
/** insert @p kv_pair into hash map.
|
||||
* Increase table size if necessary
|
||||
**/
|
||||
bool insert(const value_type & kv_pair);
|
||||
|
||||
/** reset to empty state **/
|
||||
void clear();
|
||||
|
||||
/** find element with key @p key.
|
||||
* @return iterator to element if found, end() otherwise
|
||||
**/
|
||||
const_iterator find(const key_type & key) const { return _find(key); }
|
||||
iterator find(const key_type & key) { return _promote_iterator(_find(key)); }
|
||||
|
||||
/** establish kv pair for @p key in this table; return address of value part **/
|
||||
mapped_type & operator[](const key_type & key);
|
||||
|
||||
/** verify DArenaHashMap invariants
|
||||
* Act on failure according to policy @p
|
||||
* (combination of throw|log bits)
|
||||
**/
|
||||
bool verify_ok(verify_policy p = verify_policy::throw_only()) const;
|
||||
|
||||
store_type * _store() noexcept { return &store_; }
|
||||
|
||||
auto _hash(const key_type & key) const {
|
||||
size_type h = hash_(key);
|
||||
size_type h1 = h >> 7; // slot#
|
||||
size_type h2 = h % 0x7f; // fingerprint
|
||||
|
||||
size_type N = store_.capacity();
|
||||
|
||||
size_type slot_ix = h1 % (N - 1);
|
||||
|
||||
return std::make_pair(slot_ix, h2);
|
||||
}
|
||||
|
||||
private:
|
||||
iterator _promote_iterator(const_iterator ix) {
|
||||
return iterator(const_cast<uint8_t *>(ix._ctrl()),
|
||||
const_cast<value_type *>(ix._pos()));
|
||||
}
|
||||
|
||||
const_iterator _begin_aux() const {
|
||||
if (this->empty()) [[unlikely]] {
|
||||
return this->end();
|
||||
}
|
||||
|
||||
const_iterator ix(&(store_.control_[c_control_stub]),
|
||||
&(store_.slots_[0]));
|
||||
|
||||
if (ix._at_slot_sentinel()) {
|
||||
/* advance to first occupied position in table */
|
||||
++ix;
|
||||
}
|
||||
|
||||
return ix;
|
||||
}
|
||||
|
||||
const_iterator _end_aux() const {
|
||||
const_iterator ix(&(store_.control_[c_control_stub + store_.capacity()]),
|
||||
&(store_.slots_[store_.capacity()]));
|
||||
|
||||
return ix;
|
||||
}
|
||||
|
||||
/** search hash map on key @p key, return iterator to table member.
|
||||
* return end-iterator if @p key not found
|
||||
**/
|
||||
const_iterator _find(const key_type & key) const;
|
||||
|
||||
/** insert @p kv_pair,
|
||||
* where key hashes to @p hash_value, into @p *store
|
||||
**/
|
||||
insert_value_type _try_insert_aux(size_type hash_value,
|
||||
const value_type & kv_pair,
|
||||
store_type * p_store);
|
||||
|
||||
/** increase hash table size (invoke when max load factor reached) **/
|
||||
bool _try_grow();
|
||||
|
||||
/** load group abstraction from control bytes starting at @p ix **/
|
||||
group_type _load_group(size_type ix) { return store_._load_group(ix); }
|
||||
|
||||
/** like ctrl_[ix] = h2, but maintain overflow copy
|
||||
* at end of ctrl_[] array
|
||||
**/
|
||||
void _update_control(size_type ix, uint8_t h2) {
|
||||
return store_._update_control(ix, h2);
|
||||
}
|
||||
|
||||
private:
|
||||
/** hash function **/
|
||||
key_hash hash_;
|
||||
/** key equal **/
|
||||
key_equal equal_;
|
||||
/** hash table state contents + size-related attributes **/
|
||||
store_type store_;
|
||||
/** true to enable debug logging **/
|
||||
bool debug_flag_ = false;
|
||||
};
|
||||
|
||||
template <typename Key, typename Value, typename Hash, typename Equal>
|
||||
DArenaHashMap<Key, Value, Hash, Equal>::DArenaHashMap(const ArenaHashMapConfig & cfg)
|
||||
: DArenaHashMap(cfg.name_, Hash(), Equal(), cfg.hint_max_capacity_, cfg.debug_flag_)
|
||||
{
|
||||
}
|
||||
|
||||
template <typename Key, typename Value, typename Hash, typename Equal>
|
||||
DArenaHashMap<Key, Value, Hash, Equal>::DArenaHashMap(const std::string & name,
|
||||
size_type hint_max_capacity,
|
||||
bool debug_flag)
|
||||
: DArenaHashMap(name, Hash(), Equal(), hint_max_capacity, debug_flag)
|
||||
{
|
||||
}
|
||||
|
||||
/* remarks:
|
||||
* - control: extra 16 slots for safe wraparound.
|
||||
* last 16 bytes will be copy of first 16 bytes
|
||||
*/
|
||||
template <typename Key, typename Value, typename Hash, typename Equal>
|
||||
DArenaHashMap<Key, Value, Hash, Equal>::DArenaHashMap(const std::string & name,
|
||||
Hash && hash,
|
||||
Equal && eq,
|
||||
size_type hint_max_capacity,
|
||||
bool debug_flag)
|
||||
: hash_{std::move(hash)},
|
||||
equal_{std::move(eq)},
|
||||
store_{name, lub_exp2(lub_group_mult(hint_max_capacity))},
|
||||
debug_flag_{debug_flag}
|
||||
{
|
||||
}
|
||||
|
||||
template <typename Key, typename Value, typename Hash, typename Equal>
|
||||
auto
|
||||
DArenaHashMap<Key,
|
||||
Value,
|
||||
Hash,
|
||||
Equal>::try_insert(const value_type & kv_pair) -> insert_value_type
|
||||
{
|
||||
size_type h = hash_(kv_pair.first);
|
||||
|
||||
return _try_insert_aux(h, kv_pair, &store_);
|
||||
}
|
||||
|
||||
template <typename Key,
|
||||
typename Value,
|
||||
typename Hash,
|
||||
typename Equal>
|
||||
auto
|
||||
DArenaHashMap<Key,
|
||||
Value,
|
||||
Hash,
|
||||
Equal>::_try_insert_aux(size_type hash_value,
|
||||
const std::pair<const Key, Value> & kv_pair,
|
||||
store_type * p_store)
|
||||
-> std::pair<value_type *, bool>
|
||||
|
||||
{
|
||||
scope log(XO_DEBUG(false));
|
||||
|
||||
size_type h = hash_value;
|
||||
// h1: hi bits: probe sequence
|
||||
size_type h1 = h >> 7;
|
||||
// h2: lo bits: store in control byte
|
||||
uint8_t h2 = h & 0x7f;
|
||||
|
||||
size_type N = p_store->capacity();
|
||||
|
||||
if (N == 0) [[unlikely]] {
|
||||
return std::make_pair(nullptr, false);
|
||||
}
|
||||
|
||||
// same as:
|
||||
// ix = h1 % N
|
||||
// since N is power of 2
|
||||
size_type ix = h1 & (N - 1);
|
||||
|
||||
// will make series of probes
|
||||
for (;;) {
|
||||
auto grp = p_store->_load_group(ix);
|
||||
|
||||
{
|
||||
// look for matching slot to update
|
||||
uint16_t m = grp.all_matches(h2);
|
||||
|
||||
// process each match.
|
||||
// matches are encountered in the same order they
|
||||
// appear in ctrl_[]
|
||||
while (m) {
|
||||
// zeroes: #of 0 before least-significant 1 bit
|
||||
int skip = __builtin_ctz(m);
|
||||
size_type slot_ix = (ix + skip) & (N - 1);
|
||||
|
||||
// invariant: slot_ix in [0 .. N)
|
||||
|
||||
auto & slot = p_store->slots_[slot_ix];
|
||||
|
||||
if (equal_(slot.first, kv_pair.first)) {
|
||||
// we have match on existing key;
|
||||
// replace associated value
|
||||
slot.second = kv_pair.second;
|
||||
|
||||
// false: did not change table size
|
||||
return std::make_pair(&slot, false);
|
||||
}
|
||||
|
||||
// e.g:
|
||||
// /-- lowest 1 bit gets cleared
|
||||
// v
|
||||
// m = b01101000
|
||||
// m-1 = b01100111
|
||||
// & = b01100000
|
||||
|
||||
m &= (m - 1);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// look for empty slot to insert
|
||||
uint16_t e = grp.empty_matches();
|
||||
|
||||
// process each empty slot
|
||||
if (e) {
|
||||
// check that table is below max load factor (0.875).
|
||||
// Check here so that table can stay at max load factor
|
||||
// indefinitely as long as updates only
|
||||
//
|
||||
if (p_store->load_factor() >= c_max_load_factor) {
|
||||
return std::make_pair(nullptr, false);
|
||||
}
|
||||
|
||||
// zeroes: #of 0 before least significant 1 bit
|
||||
int skip = __builtin_ctz(e);
|
||||
size_type slot_ix = (ix + skip) & (N - 1);
|
||||
|
||||
// invariant: slot_ix in [0 .. N)
|
||||
|
||||
auto & slot = p_store->slots_[slot_ix];
|
||||
|
||||
// mark slot occupied in control space;
|
||||
// maintain copy-at-end for overflow
|
||||
p_store->_update_control(slot_ix, h2);
|
||||
new (&slot) value_type(kv_pair);
|
||||
|
||||
++(p_store->size_);
|
||||
|
||||
// true: increased table size
|
||||
return std::make_pair(&slot, true);
|
||||
}
|
||||
}
|
||||
|
||||
// slot range associated with grp
|
||||
// has no room, and does not contain target key
|
||||
// -> move on to next group.
|
||||
//
|
||||
// note: relying on c_group_size overflow bytes here
|
||||
// when ix is close to N
|
||||
|
||||
ix = (ix + c_group_size) & (N - 1);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Key, typename Value, typename Hash, typename Equal>
|
||||
bool
|
||||
DArenaHashMap<Key, Value, Hash, Equal>::_try_grow()
|
||||
{
|
||||
scope log(XO_DEBUG(false));
|
||||
|
||||
size_type n_group_exponent_2x = 0;
|
||||
size_type n_group_2x = 0;
|
||||
|
||||
if (store_.n_group_ == 0) [[unlikely]] {
|
||||
// special case: grow from hard empty state
|
||||
n_group_exponent_2x = 0;
|
||||
n_group_2x = 1;
|
||||
} else {
|
||||
n_group_exponent_2x = store_.n_group_exponent_ + 1;
|
||||
n_group_2x = 2 * n_group_exponent_2x;
|
||||
}
|
||||
|
||||
// optimization when table is empty. in that case can resize
|
||||
// arenas in place
|
||||
|
||||
if (this->empty()) {
|
||||
log && log("resize-from-empty branch");
|
||||
|
||||
this->store_.resize_from_empty(std::make_pair(n_group_exponent_2x, n_group_2x));
|
||||
} else {
|
||||
log && log("duplicate-and-replace branch");
|
||||
|
||||
detail::HashMapStore<Key, Value> store_2x("arenahashmap",
|
||||
std::make_pair(n_group_exponent_2x,
|
||||
n_group_2x));
|
||||
/* rehash everything in store_,
|
||||
* into store_2x
|
||||
*/
|
||||
|
||||
for (size_type i = 0, n = store_.capacity(); i < n; ++i) {
|
||||
uint8_t ctrl = store_.control_[c_control_stub + i];
|
||||
value_type & kv_pair = store_.slots_[i];
|
||||
|
||||
if (DArenaHashMapUtil::is_data(ctrl)) {
|
||||
size_type h = hash_(kv_pair.first);
|
||||
auto chk = this->_try_insert_aux(h, kv_pair, &store_2x);
|
||||
|
||||
if (!chk.second) {
|
||||
// shenanigans - something isn't right.
|
||||
// - may have run out of memory
|
||||
assert(false);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this->store_ = std::move(store_2x);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename Key,
|
||||
typename Value,
|
||||
typename Hash,
|
||||
typename Equal>
|
||||
bool
|
||||
DArenaHashMap<Key,
|
||||
Value,
|
||||
Hash,
|
||||
Equal>::insert(const std::pair<const Key, Value> & kv_pair)
|
||||
{
|
||||
scope log(XO_DEBUG(false));
|
||||
|
||||
auto [slot_addr, ins_flag] = this->try_insert(kv_pair);
|
||||
|
||||
if (slot_addr) {
|
||||
log && log("fast", xtag("slot_addr", (void*)slot_addr), xtag("ins_flag", ins_flag));
|
||||
|
||||
return ins_flag;
|
||||
}
|
||||
|
||||
assert((store_.size_ + 1) / static_cast<float>(store_.n_slot_) >= c_max_load_factor);
|
||||
|
||||
if (this->_try_grow()) {
|
||||
/* retry insert, with bigger table */
|
||||
auto [slot_addr, ins_flag] = this->try_insert(kv_pair);
|
||||
|
||||
return ins_flag;
|
||||
} else {
|
||||
assert(false);
|
||||
|
||||
// TODO: set last error. Presumeably reached max size
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Key,
|
||||
typename Value,
|
||||
typename Hash,
|
||||
typename Equal>
|
||||
void
|
||||
DArenaHashMap<Key, Value, Hash, Equal>::clear()
|
||||
{
|
||||
this->store_.clear();
|
||||
}
|
||||
|
||||
template <typename Key,
|
||||
typename Value,
|
||||
typename Hash,
|
||||
typename Equal>
|
||||
auto
|
||||
DArenaHashMap<Key, Value, Hash, Equal>::_find(const key_type & key) const -> const_iterator
|
||||
{
|
||||
size_type N = store_.capacity();
|
||||
|
||||
if (N == 0) [[unlikely]] {
|
||||
return this->cend();
|
||||
}
|
||||
|
||||
size_type h = hash_(key);
|
||||
size_type h1 = h >> 7;
|
||||
uint8_t h2 = h & 0x7f;
|
||||
|
||||
size_type ix = h1 & (N - 1);
|
||||
|
||||
for (;;) {
|
||||
auto grp = store_._load_group(ix);
|
||||
|
||||
{
|
||||
uint16_t m = grp.all_matches(h2);
|
||||
|
||||
while (m) {
|
||||
int skip = __builtin_ctz(m);
|
||||
size_type slot_ix = (ix + skip) & (N - 1);
|
||||
|
||||
auto & slot = store_.slots_[slot_ix];
|
||||
|
||||
if (equal_(slot.first, key)) {
|
||||
return const_iterator(&(store_.control_[c_control_stub + slot_ix]),
|
||||
&slot);
|
||||
}
|
||||
|
||||
m &= (m - 1);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
uint16_t e = grp.empty_matches();
|
||||
|
||||
if (e) {
|
||||
return this->end();
|
||||
}
|
||||
}
|
||||
|
||||
ix = (ix + c_group_size) & (N - 1);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Key,
|
||||
typename Value,
|
||||
typename Hash,
|
||||
typename Equal>
|
||||
auto
|
||||
DArenaHashMap<Key, Value, Hash, Equal>::operator[](const key_type & key) -> mapped_type &
|
||||
{
|
||||
{
|
||||
auto ix = this->find(key);
|
||||
|
||||
if (ix != this->end())
|
||||
return ix->second;
|
||||
}
|
||||
|
||||
// key-value pair
|
||||
value_type kv_pair = std::make_pair(key, mapped_type{});
|
||||
|
||||
auto [slot_addr, ins_flag] = this->try_insert(kv_pair);
|
||||
|
||||
if (slot_addr)
|
||||
return slot_addr->second;
|
||||
|
||||
if (!this->_try_grow()) {
|
||||
// we are out of room
|
||||
|
||||
throw std::runtime_error("DArenaHashMap::operator[]: table capacity exhausted");
|
||||
}
|
||||
|
||||
/* retry insert, now with bigger capacity */
|
||||
std::tie(slot_addr, ins_flag) = this->try_insert(kv_pair);
|
||||
|
||||
assert(slot_addr);
|
||||
|
||||
return slot_addr->second;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify DArenaHashMap class invariants.
|
||||
*
|
||||
* SM1. size consistency
|
||||
* - SM1.1 size_ <= n_slot_
|
||||
* - SM1.2 control_[] size consistent with slots_[] size
|
||||
* - SM1.3 n_group_ consistent with n_group_exponent_
|
||||
* - SM1.4 n_slot_ consistent with n_group_
|
||||
* - SM1.5 n_slot_ a power of 2
|
||||
* SM2. load factor
|
||||
* - SM2.1 load_factor() <= c_max_load_factor
|
||||
* SM3. control_
|
||||
* - SM3.1 control_[i] = c_iterator_bookend for i in [0, c_control_stub)
|
||||
* - SM3.2 control_[stub+i] = control_[stub+N+i] for i in [0, c_group_size)
|
||||
* - SM3.3 {number of control_[i] spots with non-sentinel values} = size_
|
||||
* - SM3.4 control_[stub+N+c_group_size+i] = c_iterator_bookend for i in [0, c_control_stub)
|
||||
* SM4. slots_
|
||||
* - SM4.1 if control_[i] is non-sentinel:
|
||||
* - SM4.1.1 control_[i] = hash_(slots_[i].first) & 0x7f
|
||||
* - SM4.1.2 all slots in range [h .. i] are non-empty,
|
||||
* where h is hash_(slots_[i].first >> 7
|
||||
* - SM4.2 if control_[i] is empty or tombstone:
|
||||
* - slots_[i].first = key_type()
|
||||
*
|
||||
**/
|
||||
template <typename Key, typename Value, typename Hash, typename Equal>
|
||||
bool
|
||||
DArenaHashMap<Key, Value, Hash, Equal>::verify_ok(verify_policy policy) const
|
||||
{
|
||||
using xo::scope;
|
||||
using xo::tostr;
|
||||
using xo::xtag;
|
||||
|
||||
constexpr const char * c_self = "DArenaHashMap::verify_ok";
|
||||
scope log(XO_DEBUG(debug_flag_),
|
||||
xtag("size", store_.size_));
|
||||
|
||||
/* SM1.1: size_ <= n_slot_ */
|
||||
if (store_.size_ > store_.n_slot_) {
|
||||
return policy.report_error(log,
|
||||
c_self, ": expect .size <= .n_slot",
|
||||
xtag("size", store_.size_),
|
||||
xtag("n_slot", store_.n_slot_));
|
||||
}
|
||||
|
||||
/* SM1.2: control_[] size consistent with slots_[] size */
|
||||
if (store_.control_.size() != control_size(store_.n_slot_)) {
|
||||
return policy.report_error
|
||||
(log,
|
||||
c_self, ": expect .control_.size = .n_slot + c_group_size + 2 * c_control_stub",
|
||||
xtag("control_.size", store_.control_.size()),
|
||||
xtag("n_slot", store_.n_slot_),
|
||||
xtag("c_group_size", c_group_size),
|
||||
xtag("c_control_stub", c_control_stub));
|
||||
}
|
||||
if (store_.slots_.size() != store_.n_slot_) {
|
||||
return policy.report_error(log,
|
||||
c_self, ": expect .slots_.size = .n_slot",
|
||||
xtag("slots_.size", store_.slots_.size()),
|
||||
xtag("n_slot", store_.n_slot_));
|
||||
}
|
||||
|
||||
/* SM1.3: n_group_ consistent with n_group_exponent_ */
|
||||
if ((store_.n_group_ > 0)
|
||||
&& (store_.n_group_ != (size_type{1} << store_.n_group_exponent_)))
|
||||
{
|
||||
return policy.report_error(log,
|
||||
c_self, ": expect .n_group = 2^.n_group_exponent",
|
||||
xtag("n_group", store_.n_group_),
|
||||
xtag("n_group_exponent", store_.n_group_exponent_));
|
||||
}
|
||||
|
||||
/* SM1.4: n_slot_ consistent with n_group_ */
|
||||
if (store_.n_slot_ != store_.n_group_ * c_group_size) {
|
||||
return policy.report_error(log,
|
||||
c_self, ": expect .n_slot = .n_group * c_group_size",
|
||||
xtag("n_slot", store_.n_slot_),
|
||||
xtag("n_group", store_.n_group_),
|
||||
xtag("c_group_size", c_group_size));
|
||||
}
|
||||
|
||||
/* SM1.5: n_slot_ a power of 2 */
|
||||
if ((store_.n_slot_ & (store_.n_slot_ - 1)) != 0) {
|
||||
return policy.report_error(log,
|
||||
c_self, ": expect .n_slot is power of 2",
|
||||
xtag("n_slot", store_.n_slot_));
|
||||
}
|
||||
|
||||
/* SM2.1: load_factor() <= c_max_load_factor */
|
||||
if (load_factor() > c_max_load_factor) {
|
||||
return policy.report_error(log,
|
||||
c_self, ": expect .load_factor <= c_max_load_factor",
|
||||
xtag("load_factor", load_factor()),
|
||||
xtag("c_max_load_factor", c_max_load_factor));
|
||||
}
|
||||
|
||||
/* SM3.1: control_[i] = c_iterator_bookend for i in [0, c_control_stub) */
|
||||
for (size_type i = 0; i < c_control_stub; ++i) {
|
||||
if (store_.control_[i] != c_iterator_bookend) {
|
||||
return policy.report_error(log,
|
||||
c_self, ": expect control_[i] = c_iterator_bookend for front stub",
|
||||
xtag("i", i),
|
||||
xtag("control_[i]", (int)(store_.control_[i])),
|
||||
xtag("c_iterator_bookend", (int)c_iterator_bookend));
|
||||
}
|
||||
}
|
||||
|
||||
/* SM3.2: control_[N+i] = control_[i] for i in [0, c_group_size) */
|
||||
for (size_type i = 0; i < c_group_size; ++i) {
|
||||
if (store_.control_[store_.n_slot_ + i + c_control_stub] != store_.control_[i + c_control_stub]) {
|
||||
return policy.report_error(log,
|
||||
c_self, ": expect control_[N+i] = control_[i]",
|
||||
xtag("i", i),
|
||||
xtag("control_[i]", (int)(store_.control_[i + c_control_stub])),
|
||||
xtag("control_[N+i]", (int)(store_.control_[store_.n_slot_ + i + c_control_stub])));
|
||||
}
|
||||
}
|
||||
|
||||
/* SM3.3: {number of control_[i] spots with non-sentinel values} = size_ */
|
||||
{
|
||||
size_type occupied_count = 0;
|
||||
for (size_type i = 0; i < store_.n_slot_; ++i) {
|
||||
uint8_t c = store_.control_[i + c_control_stub];
|
||||
if (DArenaHashMapUtil::is_data(c)) {
|
||||
++occupied_count;
|
||||
}
|
||||
}
|
||||
if (occupied_count != store_.size_) {
|
||||
return policy.report_error(log,
|
||||
c_self, ": expect occupied control count = size",
|
||||
xtag("occupied_count", occupied_count),
|
||||
xtag("size", store_.size_));
|
||||
}
|
||||
}
|
||||
|
||||
/* SM3.4: control_[stub+N+c_group_size+i] = c_iterator_bookend for i in [0, c_control_stub) */
|
||||
if (store_.n_slot_ > 0) {
|
||||
for (size_type i = 0; i < c_control_stub; ++i) {
|
||||
size_type ix = c_control_stub + store_.n_slot_ + c_group_size + i;
|
||||
if (store_.control_[ix] != c_iterator_bookend) {
|
||||
return policy.report_error
|
||||
(log,
|
||||
c_self, ": expect control_[stub+N+group+i] = c_iterator_bookend for end stub",
|
||||
xtag("i", i),
|
||||
xtag("N", store_.n_slot_),
|
||||
xtag("ix", ix),
|
||||
xtag("control_[ix]", (int)(store_.control_[ix])),
|
||||
xtag("c_iterator_bookend", (int)c_iterator_bookend));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* SM4.1.1: if control_[i] is non-sentinel, control_[i] = hash_(slots_[i].first) & 0x7f */
|
||||
for (size_type i = 0; i < store_.n_slot_; ++i) {
|
||||
uint8_t c = store_.control_[i + c_control_stub];
|
||||
if (DArenaHashMapUtil::is_data(c)) {
|
||||
uint8_t expected_h2 = hash_(store_.slots_[i].first) & 0x7f;
|
||||
if (c != expected_h2) {
|
||||
return policy.report_error(log,
|
||||
c_self, ": expect control[i] = hash(key) & 0x7f",
|
||||
xtag("i", i),
|
||||
xtag("control[i+stub]", c),
|
||||
xtag("expected_h2", expected_h2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* SM4.1.2: if control_[i] is non-sentinel, all slots in range [h .. i] are non-empty,
|
||||
* where h = (hash_(slots_[i].first) >> 7) & (n_slot_ - 1)
|
||||
*/
|
||||
for (size_type i = 0; i < store_.n_slot_; ++i) {
|
||||
uint8_t c = store_.control_[i + c_control_stub];
|
||||
if (DArenaHashMapUtil::is_data(c)) {
|
||||
size_type h = (hash_(store_.slots_[i].first) >> 7) & (store_.n_slot_ - 1);
|
||||
size_type j = h;
|
||||
while (j != i) {
|
||||
uint8_t cj = store_.control_[j + c_control_stub];
|
||||
if (DArenaHashMapUtil::is_sentinel(cj)) {
|
||||
return policy.report_error(log,
|
||||
c_self, ": expect non-empty slot in probe range [h..i]",
|
||||
xtag("i", i),
|
||||
xtag("h", h),
|
||||
xtag("j", j),
|
||||
xtag("control[j+stub]", cj));
|
||||
}
|
||||
j = (j + 1) & (store_.n_slot_ - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* SM4.2: if control_[i] is empty or tombstone, slots_[i].first = key_type() */
|
||||
for (size_type i = 0; i < store_.n_slot_; ++i) {
|
||||
uint8_t c = store_.control_[i + c_control_stub];
|
||||
if (DArenaHashMapUtil::is_sentinel(c)) {
|
||||
if (!(store_.slots_[i].first == key_type())) {
|
||||
return policy.report_error(log,
|
||||
c_self, ": expect empty/tombstone slot has default key",
|
||||
xtag("i", i),
|
||||
xtag("control[i+stub]", c));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
} /*namespace map*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end DArenaHashMap.hpp */
|
||||
|
|
@ -1,127 +0,0 @@
|
|||
/** @file DArenaIterator.hpp
|
||||
*
|
||||
* @author Roland Conybeare, Dec 2025
|
||||
**/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "AllocInfo.hpp"
|
||||
#include "AllocHeader.hpp"
|
||||
#include "cmpresult.hpp"
|
||||
|
||||
namespace xo {
|
||||
namespace mm {
|
||||
struct DArena;
|
||||
|
||||
/** @class DArenaIterator
|
||||
* @brief Representation for alloc iterator over arena
|
||||
*
|
||||
* Map showing an arena allocation:
|
||||
*
|
||||
* @verbatim
|
||||
*
|
||||
* <-------------z1--------------->
|
||||
* < guard >< hz >< req_z >< dz >< guard >
|
||||
*
|
||||
* +++++++++0000zzzz@@@@@@@@@@@@@@@@@ppppppp+++++++++
|
||||
*
|
||||
* ^ ^ ^
|
||||
* header mem header
|
||||
* ^ (next alloc)
|
||||
* DArenaIterator::pos_
|
||||
*
|
||||
* guard [+] guard before+after each allocation, for simple sanitize checks
|
||||
* header [0] alloc header (non-size bits)
|
||||
* [z] alloc header (size bits)
|
||||
* mem [@] app-requested memory, including padding [p]
|
||||
* dz [p] padding (to uintptr_t alignment. req_z+dz recorded in header)
|
||||
* free_ DArena::free_ just after guard bytes for last allocation
|
||||
*
|
||||
* @endverbatim
|
||||
**/
|
||||
struct DArenaIterator {
|
||||
/** @defgroup mm-arenaiterator-ctors DArenaIterator instance vars **/
|
||||
///@{
|
||||
DArenaIterator() = default;
|
||||
DArenaIterator(const DArena * arena,
|
||||
AllocHeader * pos) : arena_{arena},
|
||||
pos_{pos} {}
|
||||
|
||||
/** Create iterator in invalid state **/
|
||||
static DArenaIterator invalid() { return DArenaIterator(); }
|
||||
|
||||
/** Create iterator pointing to the beginning of @p arena
|
||||
* Iterator cannot modify memory, but can capture
|
||||
* an iterator error in @p *arena
|
||||
**/
|
||||
static DArenaIterator begin(const DArena * arena);
|
||||
/** Create iterator pointing to the end of @p arena
|
||||
* Iterator cannot modify memory, but can capture
|
||||
* an iterator error in @p *arena
|
||||
**/
|
||||
static DArenaIterator end(const DArena * arena);
|
||||
///@}
|
||||
|
||||
/** @defgroup mm-arenaiterator-methods DArenaIterator methods **/
|
||||
///@{
|
||||
/** Address of allocation header for beginning of alloc range in @p arena **/
|
||||
static AllocHeader * begin_header(const DArena * arena);
|
||||
/** Address of allocation header for end of alloc range.
|
||||
* This is the address of header for _next_ allocation in @p arena
|
||||
* i.e. free pointer
|
||||
**/
|
||||
static AllocHeader * end_header(const DArena * arena);
|
||||
|
||||
/** A valid iterator can be compared, at least with itself
|
||||
* It can be dereferenced if is also non-empty
|
||||
**/
|
||||
bool is_valid() const noexcept { return (arena_ != nullptr) && (pos_ != nullptr); }
|
||||
/** An invalid (or sentinel) iterator is incomparable with all
|
||||
* iterators including itself
|
||||
**/
|
||||
bool is_invalid() const noexcept { return !is_valid(); }
|
||||
|
||||
/** fetch contents at current iterator position **/
|
||||
AllocInfo deref() const noexcept;
|
||||
/** compare two iterators. To be comparable,
|
||||
* iterators must refer to the same arena
|
||||
**/
|
||||
cmpresult compare(const DArenaIterator & other) const noexcept;
|
||||
/** advance iterator to next allocation **/
|
||||
void next() noexcept;
|
||||
|
||||
/** cast iterator position to byte* */
|
||||
std::byte * pos_as_byte() const { return (std::byte *)pos_; }
|
||||
|
||||
/** *ix synonym for ix.deref() **/
|
||||
AllocInfo operator*() const noexcept { return this->deref(); }
|
||||
/** ++ix synonym for ix.next() **/
|
||||
DArenaIterator & operator++() noexcept { this->next(); return *this; }
|
||||
///@}
|
||||
|
||||
/** @defgroup mm-arenaiterator-instance-vars **/
|
||||
///@{
|
||||
/** iterator visits allocations from this arena **/
|
||||
const DArena * arena_ = nullptr;
|
||||
/** current iterator position **/
|
||||
AllocHeader * pos_ = nullptr;
|
||||
///@}
|
||||
};
|
||||
|
||||
inline bool
|
||||
operator==(const DArenaIterator & x,
|
||||
const DArenaIterator & y)
|
||||
{
|
||||
return x.compare(y).is_equal();
|
||||
}
|
||||
|
||||
inline bool
|
||||
operator!=(const DArenaIterator & x,
|
||||
const DArenaIterator & y)
|
||||
{
|
||||
return !x.compare(y).is_equal();
|
||||
}
|
||||
} /*namespace mm*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end DArenaIterator.hpp */
|
||||
|
|
@ -1,358 +0,0 @@
|
|||
/** @file DArenaVector.hpp
|
||||
*
|
||||
* @author Roland Conybeare, Jan 2026
|
||||
**/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "DArena.hpp"
|
||||
#include <stdexcept>
|
||||
#include <cstring> // for ::memset()
|
||||
|
||||
namespace xo {
|
||||
namespace mm {
|
||||
/** @brief vector of T using dedicated DArena for storage
|
||||
*
|
||||
* Replicate (to the extent feasible) std::vector<T>
|
||||
* behavior, but using a dedicated DArena to provide storage
|
||||
*
|
||||
* Unlike std::vector:
|
||||
* 1. does not support copying
|
||||
* 2. capacity fixed at construction time
|
||||
*
|
||||
* @tparam T element type. Must be Erasable
|
||||
**/
|
||||
template <typename T>
|
||||
struct DArenaVector {
|
||||
public:
|
||||
using value_type = T;
|
||||
using size_type = std::size_t;
|
||||
using difference_type = std::ptrdiff_t;
|
||||
using reference = value_type &;
|
||||
using const_reference = const value_type &;
|
||||
using iterator = value_type *;
|
||||
using const_iterator = const value_type *;
|
||||
|
||||
/** null ctor **/
|
||||
DArenaVector() = default;
|
||||
/** ctor from already-mapped (but not committed) address range_type
|
||||
* vector has size zero
|
||||
**/
|
||||
DArenaVector(const ArenaConfig & cfg,
|
||||
size_type page_z,
|
||||
size_type arena_align_z,
|
||||
DArena::value_type lo,
|
||||
DArena::value_type hi);
|
||||
/** not intended to be copyable **/
|
||||
DArenaVector(const DArenaVector &) = delete;
|
||||
/** move ctor **/
|
||||
DArenaVector(DArenaVector && other);
|
||||
/** releases mapped memory **/
|
||||
~DArenaVector();
|
||||
|
||||
/** create empty vector using @p cfg to configure backing store **/
|
||||
static DArenaVector map(const ArenaConfig & cfg);
|
||||
|
||||
/** true iff vector is emtpy **/
|
||||
bool empty() const { return size_ == 0; }
|
||||
size_type size() const { return size_; }
|
||||
size_type max_size() const { return capacity(); }
|
||||
size_type capacity() const { return store_.reserved() / sizeof(T); }
|
||||
|
||||
/** get reference to element at zero-based index @p i. Do not check bounds **/
|
||||
T & operator[](size_t i) noexcept { return *(this->_address_of(i)); }
|
||||
const T & operator[](size_t i) const noexcept { return *(this->_address_of(i)); }
|
||||
|
||||
/** get reference to element at zero-based index @p i. Do check bounds **/
|
||||
T & at(size_type i) { _check_valid_index(i); return *(this->_address_of(i)); }
|
||||
const T & at(size_type i) const { _check_valid_index(i); return *(this->_address_of(i)); }
|
||||
|
||||
/** get to at first element of vector. Same as @p end if vector is empty **/
|
||||
iterator begin() noexcept { return this->_address_of(0); }
|
||||
/** get iterator to end of vector - "one past the last element" **/
|
||||
iterator end() noexcept { return this->_address_of(size_); }
|
||||
const_iterator cbegin() const noexcept { return this->_address_of(0); }
|
||||
const_iterator begin() const noexcept { return this->cbegin(); }
|
||||
const_iterator cend() const noexcept { return this->_address_of(size_); }
|
||||
const_iterator end() const noexcept { return this->cend(); }
|
||||
|
||||
constexpr const DArena * store() const { return &store_; }
|
||||
constexpr T * data() { return reinterpret_cast<T*>(store_.lo_); }
|
||||
constexpr const T * data() const { return reinterpret_cast<const T*>(store_.lo_); }
|
||||
|
||||
/** arena used for element storage
|
||||
* (Might prefer obj<AResourceVisitor> here; refrain to avoid leveling violation)
|
||||
**/
|
||||
void visit_pools(const MemorySizeVisitor & fn) const { store_.visit_pools(fn); }
|
||||
|
||||
/** reserve space, if possible, for at least @p z elements.
|
||||
* Always limited by ArenaConfig.size_
|
||||
**/
|
||||
void reserve(size_type z);
|
||||
/** resize to size @p z. Return true on success. May fail iff oom. **/
|
||||
bool resize(size_type z);
|
||||
void shrink_to_fit();
|
||||
/** reset vector to empty state **/
|
||||
void clear();
|
||||
|
||||
T & insert(size_type pos, T && x);
|
||||
T & insert(size_type pos, const T & x);
|
||||
|
||||
void erase(size_type pos);
|
||||
|
||||
void push_back(T && x);
|
||||
void push_back(const T & x);
|
||||
|
||||
void swap(DArenaVector & other) noexcept;
|
||||
|
||||
DArenaVector & operator=(DArenaVector && x) noexcept;
|
||||
|
||||
private:
|
||||
T * _address_of(size_type i) { return ((T *)store_.lo_) + i; }
|
||||
const T * _address_of(size_type i) const { return ((const T *)store_.lo_) + i; }
|
||||
|
||||
void _check_valid_index(size_type i) const;
|
||||
|
||||
private:
|
||||
size_type size_ = 0;
|
||||
DArena store_;
|
||||
DArena::Checkpoint zero_ckp_;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
DArenaVector<T>::DArenaVector(const ArenaConfig & cfg,
|
||||
size_type page_z,
|
||||
size_type arena_align_z,
|
||||
DArena::value_type lo,
|
||||
DArena::value_type hi)
|
||||
: store_{cfg, page_z, arena_align_z, lo, hi},
|
||||
zero_ckp_{store_.checkpoint()}
|
||||
{}
|
||||
|
||||
template <typename T>
|
||||
DArenaVector<T>::DArenaVector(DArenaVector && other)
|
||||
: size_{other.size_}, store_{std::move(other.store_)}, zero_ckp_{std::move(other.zero_ckp_)}
|
||||
{
|
||||
other.size_ = 0;
|
||||
other.zero_ckp_ = DArena::Checkpoint();
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
DArenaVector<T>::~DArenaVector()
|
||||
{
|
||||
if constexpr (std::is_trivially_destructible_v<T>) {
|
||||
// nothing to do
|
||||
} else {
|
||||
// invoke destructor for each element
|
||||
for (size_type i = 0, n = size(); i < n; ++i) {
|
||||
T & x = (*this)[i];
|
||||
|
||||
x.~T();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
DArenaVector<T> &
|
||||
DArenaVector<T>::operator=(DArenaVector && other) noexcept
|
||||
{
|
||||
this->size_ = other.size_;
|
||||
this->store_ = std::move(other.store_);
|
||||
this->zero_ckp_ = std::move(other.zero_ckp_);
|
||||
|
||||
other.size_ = 0;
|
||||
other.zero_ckp_ = DArena::Checkpoint();
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
DArenaVector<T>
|
||||
DArenaVector<T>::map(const ArenaConfig & cfg)
|
||||
{
|
||||
DArenaVector<T> retval;
|
||||
|
||||
retval.store_ = std::move(DArena::map(cfg));
|
||||
retval.zero_ckp_ = retval.store_.checkpoint();
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void
|
||||
DArenaVector<T>::reserve(size_type z) {
|
||||
store_.expand(z * sizeof(T), __PRETTY_FUNCTION__);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool
|
||||
DArenaVector<T>::resize(size_type z) {
|
||||
// new arena size in bytes
|
||||
size_t req_z = z * sizeof(T);
|
||||
|
||||
if (z > size_) {
|
||||
// expand arena to accomodate
|
||||
|
||||
if (!store_.expand(req_z, __PRETTY_FUNCTION__))
|
||||
return false;
|
||||
|
||||
// run ctors
|
||||
if constexpr (std::is_trivially_constructible_v<T>) {
|
||||
::memset(this->_address_of(size_), 0, req_z - (size_ * sizeof(T)));
|
||||
} else {
|
||||
for (size_type i = size_; i < z; ++i) {
|
||||
void * addr = &(*this)[i];
|
||||
|
||||
new (addr) T();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if constexpr (std::is_trivially_destructible_v<T>) {
|
||||
// nothing to do
|
||||
} else {
|
||||
// invoke destructor for each discarded element
|
||||
for (size_type i = z; i < size_; ++i) {
|
||||
T & x = (*this)[i];
|
||||
|
||||
x.~T();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// rewind to checkpoint, then reallocate.
|
||||
// This is for form's sake, so that DArena considers memory
|
||||
// to be 'allocated'. DArenaVector<T> doesn't care for itself,
|
||||
// but this preserves expected behavior of visit_pools().
|
||||
//
|
||||
store_.restore(zero_ckp_);
|
||||
store_.alloc(xo::reflect::typeseq::id<std::byte>(), req_z);
|
||||
|
||||
this->size_ = z;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void
|
||||
DArenaVector<T>::shrink_to_fit() {
|
||||
// could in principle release unused mapped pages here
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void
|
||||
DArenaVector<T>::clear() {
|
||||
this->resize(0);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void
|
||||
DArenaVector<T>::_check_valid_index(size_type i) const {
|
||||
if (size_ <= i)
|
||||
throw std::out_of_range("DArenaVector index out of bounds");
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T &
|
||||
DArenaVector<T>::insert(size_type pos, T && x) {
|
||||
{
|
||||
size_type new_z = size_ + 1;
|
||||
size_type req_z = new_z * sizeof(T);
|
||||
|
||||
store_.expand(req_z, __PRETTY_FUNCTION__);
|
||||
}
|
||||
|
||||
// move elements [i .. z-1] right by one position.
|
||||
// must proceed in reverse order!
|
||||
for (size_type ip1 = size_; ip1 > pos; --ip1) {
|
||||
(*this)[ip1] = std::move((*this)[ip1-1]);
|
||||
}
|
||||
|
||||
T * addr = this->_address_of(pos);
|
||||
|
||||
new (addr) T{std::move(x)};
|
||||
|
||||
this->size_ = size_ + 1;
|
||||
|
||||
return *addr;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T &
|
||||
DArenaVector<T>::insert(size_type pos, const T & x) {
|
||||
{
|
||||
size_type new_z = size_ + 1;
|
||||
size_type req_z = new_z * sizeof(T);
|
||||
|
||||
store_.expand(req_z, __PRETTY_FUNCTION__);
|
||||
}
|
||||
|
||||
// move elements [i .. z-1] right by one position.
|
||||
// must proceed in reverse order!
|
||||
for (size_type ip1 = size_; ip1 > pos; --ip1) {
|
||||
(*this)[ip1] = std::move((*this)[ip1-1]);
|
||||
}
|
||||
|
||||
T * addr = this->_address_of(pos);
|
||||
|
||||
new (addr) T{x};
|
||||
|
||||
this->size_ = size_ + 1;
|
||||
|
||||
return *addr;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void
|
||||
DArenaVector<T>::erase(size_type pos) {
|
||||
// move elements [pos+1 .. z-1] left by one position.
|
||||
|
||||
if (pos >= size_) [[unlikely]]
|
||||
return;
|
||||
|
||||
for (size_type i = pos; i+1 < size_; ++i) {
|
||||
(*this)[i] = std::move((*this)[i+1]);
|
||||
}
|
||||
|
||||
--(this->size_);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void
|
||||
DArenaVector<T>::push_back(T && x) {
|
||||
size_type z = size_ + 1;
|
||||
size_type req_z = z * sizeof(T);
|
||||
|
||||
if (this->store_.expand(req_z, __PRETTY_FUNCTION__)) {
|
||||
T * addr = this->_address_of(size_);
|
||||
|
||||
new (addr) T{std::move(x)};
|
||||
|
||||
this->size_ = z;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void
|
||||
DArenaVector<T>::push_back(const T & x) {
|
||||
size_type z = size_ + 1;
|
||||
|
||||
if (this->store_.expand(z * sizeof(T), __PRETTY_FUNCTION__)) {
|
||||
T * addr = this->_address_of(size_);
|
||||
|
||||
new (addr) T{x};
|
||||
|
||||
this->size_ = z;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void
|
||||
DArenaVector<T>::swap(DArenaVector & other) noexcept {
|
||||
std::swap(size_, other.size_);
|
||||
std::swap(store_, other.store_);
|
||||
}
|
||||
|
||||
} /*namespace mm*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end DArenaVector.hpp */
|
||||
|
|
@ -1,262 +0,0 @@
|
|||
/** @file DCircularBuffer.hpp
|
||||
*
|
||||
* @author Roland Conybeare, Jan 2026
|
||||
**/
|
||||
|
||||
#include "CircularBufferConfig.hpp"
|
||||
#include "DArenaVector.hpp"
|
||||
#include "hashmap/verify_policy.hpp"
|
||||
#include "span.hpp"
|
||||
#include <cstdint>
|
||||
|
||||
namespace xo {
|
||||
namespace mm {
|
||||
/** @class DCircularBuffer
|
||||
*
|
||||
* @brief high performance vm-aware circular buffer
|
||||
*
|
||||
* Circular buffer implementation with parsing-friendly performance features.
|
||||
* - generalization of DArena.
|
||||
* Like DArena, maps superpages as needed.
|
||||
* Unlike DArena memory at the beginning of reserved range can be unmapped.
|
||||
* - allows address range >> physical range
|
||||
* - admits "Cheney on the MTA" strategy.
|
||||
* May be feasible to reserve a lifetime address range (say 1TB)
|
||||
* as long as buffer only every maps a subrange that fits in physical memory.
|
||||
* - zero copy support for parsing / protocol trnaslation:
|
||||
* provides capture/release semantics for a fixed number
|
||||
* of remembered spans. Will never unmap memory for a remembered span,
|
||||
* until that span is released.
|
||||
* - automatically resets to beginning of reserved range
|
||||
* whenever occupied range is empty
|
||||
**/
|
||||
struct DCircularBuffer {
|
||||
public:
|
||||
/** @defgroup mm-circularbuffer-types CircularBuffer type traits **/
|
||||
///@{
|
||||
|
||||
/** an amount of memory **/
|
||||
using size_type = std::size_t;
|
||||
using byte = std::byte;
|
||||
/** a contiguous addres range **/
|
||||
using span_type = span<char>;
|
||||
using const_span_type = span<const char>;
|
||||
|
||||
///@}
|
||||
|
||||
public:
|
||||
/** @defgroup mm-cicrularbuffer-ctors CircularBuffer constructors **/
|
||||
///@{
|
||||
|
||||
/** contruct instance
|
||||
* @p config circular buffer configuration
|
||||
* @p page_z o/s page size (via getpagesize())
|
||||
* @p buffer_align_z alignment for buffer memory
|
||||
* @p reserved_range reserved virtual address range
|
||||
**/
|
||||
DCircularBuffer(const CircularBufferConfig & config,
|
||||
size_type page_z,
|
||||
size_type buffer_align_z,
|
||||
span_type reserved_range);
|
||||
#ifdef NOT_YET
|
||||
/** constructor */
|
||||
DCircularBuffer(const CircularBufferConfig & config);
|
||||
#endif
|
||||
/** non-copyable **/
|
||||
DCircularBuffer(const DCircularBuffer & other) = delete;
|
||||
/** move ctor **/
|
||||
DCircularBuffer(DCircularBuffer && other);
|
||||
|
||||
/**
|
||||
* allocate virtual memory address (uncommitted!) for circular buffer
|
||||
* with configuration @p config.
|
||||
**/
|
||||
static DCircularBuffer map(const CircularBufferConfig & config);
|
||||
|
||||
///@}
|
||||
|
||||
/** @defgroup mm-circularbuffer-const-methods CircularBuffer const methods **/
|
||||
///@{
|
||||
|
||||
const_span_type reserved_range() const noexcept { return reserved_range_; }
|
||||
const_span_type mapped_range() const noexcept { return mapped_range_; }
|
||||
const_span_type occupied_range() const noexcept { return occupied_range_; }
|
||||
const_span_type input_range() const noexcept { return input_range_; }
|
||||
|
||||
/** report memory-size info for this buffer to @p fn **/
|
||||
void visit_pools(const MemorySizeVisitor & fn) const;
|
||||
|
||||
/** verify DCircularBuffer invariants.
|
||||
* Act on failure according to policy @p p
|
||||
* (combination of throw|log bits)
|
||||
*
|
||||
* verify invariants:
|
||||
* CB1: mapped_range_ is subrange of reserved_range_
|
||||
* CB2: occupied_range_ is subrange of mapped_range_
|
||||
* CB3: each remembered_spans_[i] is subrange of occupied_range_
|
||||
* CB4: buffer_align_z_ > 0 when buffer is mapped
|
||||
* CB5: reserved_range_.lo() aligned on buffer_align_z_ boundary
|
||||
**/
|
||||
bool verify_ok(verify_policy p = verify_policy::throw_only()) const;
|
||||
|
||||
///@}
|
||||
|
||||
/** @defgroup mm-circularbuffer-nonconst-methods CircularBuffer non-const methods **/
|
||||
///@{
|
||||
|
||||
span_type input_range() noexcept { return input_range_; }
|
||||
|
||||
/** copy memory in span @p r into buffer starting at the end of
|
||||
* @ref occupied_range_. Map new physical memory as needed.
|
||||
* On success returns empty suffix of @p r.
|
||||
* If buffer memory exhausted, may copy a prefix of @p r.
|
||||
* In that case returns the remaining suffix of @p r.
|
||||
**/
|
||||
const_span_type append(const_span_type r);
|
||||
|
||||
/** DMA version of @ref append_span : get mapped span A at which
|
||||
* buffer will receive new content. Upstream may write into
|
||||
* A. It must then coordinate with buffer by calling
|
||||
* @ref report_append(P) for some prefix P of A
|
||||
*
|
||||
* Example:
|
||||
* @code
|
||||
* CircularBuffer buf = ...;
|
||||
* constexpr size_type z = 64*1024;
|
||||
* auto span = buf.get_append_span(z);
|
||||
* ssize_t nr = read(FD, span.lo(), span.size());
|
||||
* if (nr > 0)
|
||||
* buf.report_append(span.prefix(nr));
|
||||
* @endcode
|
||||
**/
|
||||
span_type get_append_span(size_type desired_z);
|
||||
|
||||
/** update bookkeeping as if caller had invoked append(r);
|
||||
* however caller has already written to mapped memory
|
||||
* after using get_append_span(); so omit copy
|
||||
**/
|
||||
void report_append(span_type r);
|
||||
|
||||
/** consume span (or prefix thereof) previously obtained from @ref occupied_range()
|
||||
* Caller represents that it won't need to read this memory again
|
||||
* unless overlaps with a pinned span.
|
||||
**/
|
||||
void consume(const_span_type input);
|
||||
|
||||
/** pin memory range @p r. circular buffer will not touch
|
||||
* addresses that appear in any pinned range.
|
||||
* use to
|
||||
**/
|
||||
void pin_range(span_type r);
|
||||
|
||||
/** unwind a previous pin_range call on range @p r.
|
||||
* both start and end or @p r should exactly match a pinned range.
|
||||
**/
|
||||
void unpin_range(span_type r);
|
||||
|
||||
///@}
|
||||
|
||||
private:
|
||||
|
||||
/** @defgroup mm-circularbuffer-private-methods CircularBuffer non-const methods **/
|
||||
///@{
|
||||
|
||||
/** expand hi end of mapped memory range to at least @p hi.
|
||||
*
|
||||
* Require: @p hi < @ref reserved_range_.hi
|
||||
**/
|
||||
bool _expand_to(char * hi);
|
||||
|
||||
/** shrink occupied rnage to the smallest contiguous range that contains both:
|
||||
* all of .input_range_, and all pinned ranges in .pinned_spans_
|
||||
**/
|
||||
void _shrink_occupied_to_fit();
|
||||
|
||||
/** check for edge condition in which there are no pinned ranges. **/
|
||||
void _check_reset_map_start();
|
||||
|
||||
///@}
|
||||
|
||||
private:
|
||||
/** @defgroup mm-circularbuffer-instance-vars CircularBuffer member variables **/
|
||||
///@{
|
||||
|
||||
/* memory layout
|
||||
*
|
||||
* reserved_range_ : entire address range owned by buffer (may be huge, e.g., 1TB)
|
||||
* mapped_range_ : subrange backed by physical memory (fits in RAM)
|
||||
* occupied_range_ : subrange currently containing data
|
||||
* input_range_ : subrange containing unread input
|
||||
* pinned_spans_ : pinned subranges within occupied (prevents alteration or unmap)
|
||||
*
|
||||
* <------------------- .reserved_range --------------------->
|
||||
* . <------------- .mapped_range -------------> .
|
||||
* . . <----- .occupied_range -----> . .
|
||||
* . . . <- .input_range -----> . .
|
||||
* . . . . . . .
|
||||
* ........------XXXXXXXIIIIIIIIIIIIIIIIIIIIII--------........
|
||||
* pp ppp pp
|
||||
* Legend:
|
||||
* [.] reserved : uncommitted memory. may be huge (e.g. 1TB)
|
||||
* [-] mapped : range backed by physical memory
|
||||
* [X] consumed : preserved until last overlapping pin removed
|
||||
* [I] input : unread content, waiting to be read
|
||||
* [p] pinned : pinned memory will not be altered (let alone unmapped)
|
||||
*
|
||||
* Invariants:
|
||||
* - .input_range <= .occupied_range <= .mapped_range <= .reserved_range
|
||||
* - mapped_range_ cannot shrink to exclude any portion of a pinned span
|
||||
*/
|
||||
|
||||
/** buffer configuration **/
|
||||
CircularBufferConfig config_;
|
||||
|
||||
/** size of a VM page (obtained automatically via getpagesize()). 4k on ubuntu. 16k on osx **/
|
||||
size_type page_z_;
|
||||
|
||||
/** alignment for buffer address range.
|
||||
* In practice will be either page_z_ or config_.hugepage_z_
|
||||
**/
|
||||
size_type buffer_align_z_;
|
||||
|
||||
/** Circular buffer owns address range defined by this span.
|
||||
* Aligned on @ref buffer_align_z_.
|
||||
* Always a whole number of @ref page_z_ or @ref config_.hugepage_z_
|
||||
**/
|
||||
span_type reserved_range_;
|
||||
|
||||
/** buffer owns memory defined by this span.
|
||||
* Always a subrange of reserved_range
|
||||
* These addresses backed by physical memory.
|
||||
* Always a whole number of @ref page_z_ or @ref config_.hugepage_z_
|
||||
**/
|
||||
span_type mapped_range_;
|
||||
|
||||
/** currently occupied buffer memory.
|
||||
* Always a subrange of @ref mapped_range_
|
||||
**/
|
||||
span_type occupied_range_;
|
||||
|
||||
/** portion of occupied buffer memory waiting to be read.
|
||||
* Always represents a subspan of @ref occupied_range_, with the same
|
||||
* hi endpoint.
|
||||
* conversely @ref consume shrinks @ref input_range_ by increasing its lo endpoint.
|
||||
**/
|
||||
span_type input_range_;
|
||||
|
||||
/** remembered spans. For anticipated use cases expect one vm page sufficient.
|
||||
* Spans in this vector always represent subranges of @ref occupied_range_
|
||||
*
|
||||
* @ref pinned_spans_ is confined to @ref occupied_range_.
|
||||
* (In particular it's *not* confined to @ref input_range_)
|
||||
*
|
||||
* sorted on increasing span.lo()
|
||||
**/
|
||||
DArenaVector<span_type> pinned_spans_;
|
||||
|
||||
///@}
|
||||
};
|
||||
}
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end DCircularBuffer.hpp */
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
/** @file ErrorArena.hpp
|
||||
*
|
||||
* @author Roland Conybeare, Feb 2026
|
||||
**/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "DArena.hpp"
|
||||
|
||||
namespace xo {
|
||||
namespace mm {
|
||||
|
||||
/** @brief Dedicated arena for error reporting
|
||||
*
|
||||
* Reserving memory for error messaages.
|
||||
* Motivation
|
||||
* 1. so we have room to report an out-of-memory condition
|
||||
* 2. so we have place to allocate for an error that
|
||||
* doesn't interfere with other allocator state
|
||||
*
|
||||
* Expect to reset arena between errors, so only need
|
||||
* enough room to report one error.
|
||||
*
|
||||
* To initialize explicitly:
|
||||
* @code
|
||||
* // before any other ErrorArena method calls:
|
||||
* ErrorArena::init_once(cfg...);
|
||||
*
|
||||
* // do stuff with ErrorArena..
|
||||
* ErrorArena::instance()
|
||||
* @endcode
|
||||
*
|
||||
* Reminder: can't use obj<AAllocator> here,
|
||||
* would be leveling violation.
|
||||
**/
|
||||
class ErrorArena {
|
||||
public:
|
||||
/** default configuration for error arena **/
|
||||
static ArenaConfig default_config();
|
||||
|
||||
/** idempotent initialization **/
|
||||
static void init_once(const ArenaConfig & cfg = default_config());
|
||||
|
||||
/** get initialized instnace **/
|
||||
static DArena * instance();
|
||||
|
||||
private:
|
||||
static DArena s_instance;
|
||||
};
|
||||
|
||||
} /*namespace mm*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end ErrorArena.hpp */
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
/** @file MemorySizeInfo.hpp
|
||||
*
|
||||
* @author Roland Conybeare, Feb 2026
|
||||
**/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <xo/reflectutil/typeseq.hpp>
|
||||
#include <functional>
|
||||
#include <string_view>
|
||||
#include <cstddef>
|
||||
|
||||
namespace xo {
|
||||
namespace mm {
|
||||
|
||||
struct MemorySizeDetail {
|
||||
using typeseq = xo::reflect::typeseq;
|
||||
|
||||
/** identifies a c++ type T. See xo/facet/TypeRegistry **/
|
||||
typeseq tseq_;
|
||||
/** number of T-instances **/
|
||||
uint32_t n_alloc_ = 0;
|
||||
/** bytes used by T-instances **/
|
||||
uint32_t z_alloc_ = 0;
|
||||
};
|
||||
|
||||
struct MemorySizeInfo {
|
||||
using size_type = std::size_t;
|
||||
using DetailArrayType = std::array<MemorySizeDetail, 32>;
|
||||
|
||||
MemorySizeInfo() = default;
|
||||
MemorySizeInfo(std::string_view name,
|
||||
std::size_t u, std::size_t a, std::size_t c, std::size_t r,
|
||||
const void * lo, const void * hi,
|
||||
DetailArrayType * detail)
|
||||
: resource_name_{name},
|
||||
used_{u}, allocated_{a}, committed_{c}, reserved_{r}, lo_{lo}, hi_{hi}, detail_{detail}
|
||||
{}
|
||||
|
||||
static MemorySizeInfo sentinel() { return MemorySizeInfo(); }
|
||||
|
||||
/** resource name **/
|
||||
std::string_view resource_name_;
|
||||
/** memory used (excluding wasted space) **/
|
||||
std::size_t used_ = 0;
|
||||
/** memory allocated (including wasted space e.g. empty slots in hash tables **/
|
||||
std::size_t allocated_ = 0;
|
||||
/** memory committed (backed by physical memory) **/
|
||||
std::size_t committed_ = 0;
|
||||
/** memory reserved:
|
||||
* virtual memory addresses range obtained, whether or not committed
|
||||
**/
|
||||
std::size_t reserved_ = 0;
|
||||
/** start address (optional) **/
|
||||
const void * lo_ = 0;
|
||||
/** end address (optional) **/
|
||||
const void * hi_ = 0;
|
||||
|
||||
/** optional histogram with per-data-type counts **/
|
||||
DetailArrayType * detail_ = nullptr;
|
||||
};
|
||||
|
||||
/** function that visits MemorySizeInfo for a collection of @p n memory pools.
|
||||
* Each pool reported with index @p i in [0, n), with associated
|
||||
* size record @p info.
|
||||
**/
|
||||
using MemorySizeVisitor = std::function<void (const MemorySizeInfo & info)>;
|
||||
}
|
||||
}
|
||||
|
||||
/* end MemorySizeInfo.hpp */
|
||||
|
|
@ -1,236 +0,0 @@
|
|||
/** @file arena_streambuf.hpp
|
||||
*
|
||||
* @author Roland Conybeare, Feb 2026
|
||||
**/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "DArena.hpp"
|
||||
//#include "print/quoted_char.hpp"
|
||||
#include <iostream>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
#include <cstring> // e.g. for std::memcpy()
|
||||
#include <cstdint>
|
||||
#include <cassert>
|
||||
|
||||
namespace xo {
|
||||
namespace mm {
|
||||
/** @brief Arena-based buffer for logging and pretty-printing
|
||||
*
|
||||
* Arena-based using mmap
|
||||
* Write to self-extending storage array
|
||||
* Track position relative to start of line
|
||||
**/
|
||||
class arena_streambuf : public std::streambuf {
|
||||
public:
|
||||
struct rewind_state {
|
||||
explicit rewind_state(std::size_t solpos, std::size_t color_esc, std::uint32_t p)
|
||||
: solpos{solpos}, color_escape_chars{color_esc}, pos{p} {}
|
||||
|
||||
std::size_t solpos = 0;
|
||||
std::size_t color_escape_chars = 0;
|
||||
std::uint32_t pos = 0;
|
||||
};
|
||||
|
||||
public:
|
||||
/** arena should be ready-to-allocate i.e. have committed > 0 **/
|
||||
arena_streambuf(DArena * arena, bool debug_flag = false) : arena_{arena}, debug_flag_{debug_flag} {
|
||||
this->reset_stream();
|
||||
} /*ctor*/
|
||||
|
||||
std::streamsize capacity() const { return arena_->committed(); }
|
||||
const char * lo() const { return this->pbase(); }
|
||||
const char * hi() const { return this->lo() + this->capacity(); }
|
||||
std::uint32_t pos() const { return this->pptr() - this->pbase(); }
|
||||
|
||||
/** output position (relative to pbase) when local state last computed. Exposed here for unit tests **/
|
||||
std::size_t _local_ppos() const { return local_ppos_; }
|
||||
/** position (relative to pbase) one character after last \n or \r. For unit tests **/
|
||||
std::uint32_t _solpos() const { return solpos_; }
|
||||
/** start of incomplete color-escape sequence **/
|
||||
const char * _color_escape_start() const { return color_escape_start_; }
|
||||
/** number of non-printing chars after @ref solpos_ from completed color-escape sequences **/
|
||||
std::uint32_t _color_escape_chars() const { return color_escape_chars_; }
|
||||
|
||||
/** number of visible characters since start of line (last \n or \r) **/
|
||||
std::uint32_t lpos() const;
|
||||
|
||||
rewind_state checkpoint() const;
|
||||
|
||||
bool debug_flag() const { return debug_flag_; }
|
||||
|
||||
operator std::string_view () const { return std::string_view(this->pbase(), this->pptr()); }
|
||||
|
||||
void reset_stream();
|
||||
|
||||
void rewind_to(rewind_state s);
|
||||
|
||||
protected:
|
||||
/** expand buffer storage (by 2x), preserve current contents **/
|
||||
void expand_to(std::size_t new_z);
|
||||
|
||||
virtual std::streamsize xsputn(const char * s, std::streamsize n) override;
|
||||
|
||||
virtual int_type overflow(int_type new_ch) override;
|
||||
|
||||
/* off. offset, relative to starting point dir.
|
||||
* dir.
|
||||
* which. in|out|both
|
||||
*
|
||||
* Note that off=0,dir=cur,which=out reads offset
|
||||
*/
|
||||
virtual pos_type seekoff(off_type off,
|
||||
std::ios_base::seekdir dir,
|
||||
std::ios_base::openmode which) override;
|
||||
|
||||
private:
|
||||
void _update_local_state_char(const char * p_lo, const char * p)
|
||||
{
|
||||
if ((*p == '\n') || (*p == '\r')) {
|
||||
this->solpos_ = (p+1 - this->pbase());
|
||||
/* reset, since these chars relevant as correction to solpos */
|
||||
this->color_escape_chars_ = 0;
|
||||
/* -> incomplete color escape, broken by newline */
|
||||
this->color_escape_start_ = nullptr;
|
||||
} else if (*p == '\033') {
|
||||
if (debug_flag_) [[unlikely]] {
|
||||
std::cout << "xsputn: \\033 at p-p_lo=" << (p - p_lo) << std::endl;
|
||||
}
|
||||
this->color_escape_start_ = p;
|
||||
} else if (this->color_escape_start_ != nullptr) {
|
||||
if (*p == 'm') {
|
||||
/* escape seq non-printing including both endpoints */
|
||||
std::int64_t esc_chars = (p+1 - color_escape_start_);
|
||||
|
||||
this->color_escape_chars_ += esc_chars;
|
||||
|
||||
if (debug_flag_) [[unlikely]] {
|
||||
std::cout << "xsputn: m at p-p_lo" << (p - p_lo) << " +" << esc_chars
|
||||
<< " -> color_escape_chars=" << color_escape_chars_ << std::endl;
|
||||
}
|
||||
this->color_escape_start_ = nullptr;
|
||||
} else if (!isdigit(*p) && (*p != '[') && (*p != ';')) {
|
||||
/* not color escape after all */
|
||||
this->color_escape_start_ = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** recognize stale local state vars:
|
||||
* @ref solpos_, @ref color_escape_chars_, @ref color_escape_start_.
|
||||
*
|
||||
* Require:
|
||||
* - {pbase, pptr} in consistent state
|
||||
* Promise:
|
||||
* - @c local_ppos_ + @c pbase = @c pptr
|
||||
* - @c solpos_, @c color_escape_chars_, @c color_escape_start_ all up-to-date
|
||||
**/
|
||||
void _check_update_local_state() {
|
||||
const char * p0 = this->pbase();
|
||||
const char * pn = this->pptr();
|
||||
|
||||
if (debug_flag_) {
|
||||
std::cerr << "_check_update_local_state:" << std::endl;
|
||||
std::cerr << " buf: (p0=" << (void*)p0 << ", pn=" << (void*)pn << ")" << std::endl;
|
||||
std::cerr << " solpos_=" << solpos_ << ", color_escape_chars_=" << color_escape_chars_ << std::endl;
|
||||
}
|
||||
|
||||
if (p0 + local_ppos_ == pn) [[likely]] {
|
||||
// solpos_, color_escape_chars_, color_escape_start_ all up-to-date
|
||||
} else {
|
||||
// [pnew, pn): input that hasn't been incorporated into
|
||||
// {solpos_, color_escape_chars_, color_escape_start_)
|
||||
|
||||
const char * pnew = this->pbase() + this->local_ppos_;
|
||||
|
||||
if (debug_flag_) {
|
||||
std::cerr << "_check_update_local_state: range: (pnew=" << (void*)pnew << ", pn=" << (void*)pn << ")" << std::endl;
|
||||
}
|
||||
|
||||
for(const char * p = pnew; p < pn; ++p) {
|
||||
this->_update_local_state_char(p0, p);
|
||||
}
|
||||
}
|
||||
|
||||
// solpos_, color_escape_chars_, color_escape_start_ all up-to-date
|
||||
// for current buffered contents
|
||||
|
||||
this->local_ppos_ = pn - p0;
|
||||
|
||||
if (debug_flag_) {
|
||||
std::cerr << "_check_update_local_state: pos=" << pos();
|
||||
std::cerr << ", solpos=" << solpos_;
|
||||
std::cerr << ", color_escape_chars=" << color_escape_chars_ << std::endl;
|
||||
}
|
||||
|
||||
assert(pos() >= solpos_ + color_escape_chars_);
|
||||
}
|
||||
|
||||
private:
|
||||
/*
|
||||
* pbase: start of buffered text. Thils will be arena_->lo_
|
||||
*
|
||||
*
|
||||
* pbase pptr epptr
|
||||
* v >e1< >e2< v v
|
||||
* |xx\xxEEExxx\xxxxxxxEExxxxEExxxxxxxEExxx\xEExxxxxx..................|
|
||||
* ^ ^<------new------->
|
||||
* solpos local_ppos
|
||||
*
|
||||
* solpos : first character after newline (stale)
|
||||
* color_escape_pos : e1+e2+.. (stale)
|
||||
* new : new characters not reflected
|
||||
* in local_ppos_, color_escape_chars_ etc.
|
||||
*
|
||||
* Legend:
|
||||
* [\] newline
|
||||
* [x] visible character
|
||||
* [E] color escape chars
|
||||
*
|
||||
*
|
||||
* after _check_update_local_state():
|
||||
*
|
||||
*
|
||||
* pbase pptr epptr
|
||||
* v >e1< v v
|
||||
* |xx\xxEEExxx\xxxxxxxEExxxxEExxxxxxxEExxx\xEExxxxxx..................|
|
||||
* ^ ^
|
||||
* solpos local_ppos
|
||||
*
|
||||
*/
|
||||
|
||||
/** @defgroup logstreambuf-instance-vars **/
|
||||
///@{
|
||||
|
||||
/** value of pptr (relative to pbase) when _check_update_local_state() last ran **/
|
||||
std::size_t local_ppos_ = 0;
|
||||
/** position (relative to pbase) one character after last \n or \r.
|
||||
* Use to drive @ref lpos. This _has_ to be lazy, since
|
||||
* xsputn() isn'g guaranteed to be called when there's room in
|
||||
* in buffer.
|
||||
**/
|
||||
std::size_t solpos_ = 0;
|
||||
/** number of non-printing chars after @ref solpos_, from
|
||||
* completed color escape sequences.
|
||||
* (ansi color escapes = text between '\033' and 'm')
|
||||
**/
|
||||
std::size_t color_escape_chars_ = 0;
|
||||
/** non-null: start of incomplete color escape sequence **/
|
||||
const char * color_escape_start_ = nullptr;
|
||||
|
||||
/** buffered output stored here.
|
||||
* We don't use arena's allocation api, just treat as a block of available memory
|
||||
**/
|
||||
DArena * arena_ = nullptr;;
|
||||
/** true to debug log_streambuf itself **/
|
||||
bool debug_flag_ = false;
|
||||
|
||||
///@}
|
||||
}; /*log_streambuf*/
|
||||
|
||||
} /*namespace mm*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end arena_streambuf.hpp */
|
||||
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
/** @file backtrace.hpp
|
||||
*
|
||||
* @author Roland Conybeare, Apr 2026
|
||||
**/
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace xo {
|
||||
void print_backtrace(bool demangle_flag);
|
||||
void print_backtrace_dwarf(bool demangle_flag);
|
||||
}
|
||||
|
||||
/* end backtrace.hpp */
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
/** @file cmpresult.hpp
|
||||
*
|
||||
* @author Roland Conybeare, Dec 2025
|
||||
**/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <iostream>
|
||||
#include <cstdint>
|
||||
|
||||
namespace xo {
|
||||
namespace mm {
|
||||
enum class comparison : int32_t {
|
||||
invalid = -1,
|
||||
comparable = 0,
|
||||
incomparable = +1,
|
||||
};
|
||||
|
||||
extern const char * comparison2str(comparison x);
|
||||
|
||||
inline std::ostream &
|
||||
operator<<(std::ostream & os, comparison x) {
|
||||
os << comparison2str(x);
|
||||
return os;
|
||||
}
|
||||
|
||||
/** @brief result of a generic comparison operation
|
||||
**/
|
||||
struct cmpresult {
|
||||
/** @defgroup mm-cmpresult-ctors cmpresult ctors **/
|
||||
///@{
|
||||
cmpresult() : err_{comparison::invalid}, cmp_{0} {}
|
||||
cmpresult(comparison err, std::int16_t cmp) : err_{err}, cmp_{cmp} {}
|
||||
|
||||
static cmpresult incomparable() { return cmpresult(comparison::incomparable, 0); }
|
||||
static cmpresult lesser() { return cmpresult(comparison::comparable, -1); }
|
||||
static cmpresult equal() { return cmpresult(comparison::comparable, 0); }
|
||||
static cmpresult greater() { return cmpresult(comparison::comparable, +1); }
|
||||
template<typename T>
|
||||
static cmpresult from_cmp(T && x, T && y) {
|
||||
if (x < y)
|
||||
return cmpresult::lesser();
|
||||
else if (x == y)
|
||||
return cmpresult::equal();
|
||||
else
|
||||
return cmpresult::greater();
|
||||
}
|
||||
|
||||
///@}
|
||||
|
||||
/** @defgroup mm-cmpresult-methods cmpresult methods **/
|
||||
///@{
|
||||
|
||||
/** print to stream **/
|
||||
void display(std::ostream & os) const;
|
||||
|
||||
bool is_lesser() const {
|
||||
return (err_ == comparison::comparable) && (cmp_ < 0);
|
||||
}
|
||||
bool is_equal() const {
|
||||
return (err_ == comparison::comparable) && (cmp_ == 0);
|
||||
}
|
||||
///@}
|
||||
|
||||
/** @defgroup mm-cmpresult-instance-vars cmpresult instance vars **/
|
||||
///@{
|
||||
/** -1 -> invalid (sentinel)
|
||||
* 0 -> comparable
|
||||
* +1 -> incomparable (e.g. iterators from different arenas)
|
||||
**/
|
||||
comparison err_ = comparison::invalid;
|
||||
/** <0 -> lesser; 0 -> equal, >0 -> greater **/
|
||||
std::int16_t cmp_ = 0;
|
||||
///@}
|
||||
};
|
||||
|
||||
inline std::ostream & operator<<(std::ostream & os,
|
||||
const cmpresult & x)
|
||||
{
|
||||
x.display(os);
|
||||
return os;
|
||||
}
|
||||
|
||||
} /*namespace mm*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end cmpresult.hpp */
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
/** @file ControlGroupo
|
||||
*
|
||||
* @author Roland Conybeare, Jan 2026
|
||||
**/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "DArenaHashMapUtil.hpp"
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace xo {
|
||||
namespace map {
|
||||
namespace detail {
|
||||
/** @brief 16x 8-bit control bytes.
|
||||
*
|
||||
* Support optimization using SIMD operations
|
||||
**/
|
||||
struct ControlGroup {
|
||||
std::array<uint8_t, DArenaHashMapUtil::c_group_size> ctrl_;
|
||||
|
||||
/** Require: lo is aligned on c_group_size (probably 16 bytes) **/
|
||||
explicit ControlGroup(const uint8_t * lo) {
|
||||
::memcpy(ctrl_.data(), lo, DArenaHashMapUtil::c_group_size);
|
||||
}
|
||||
|
||||
/** find all exact matches in ctrl_[0..15] for @p h2.
|
||||
* for each match set corresponding bit in return value.
|
||||
* Bits {0x1, 0x2, 0x4, ...} set iff exact match on
|
||||
* {ctrl_[0], ctrl_[1], ctrl_2[], ...} respectively
|
||||
**/
|
||||
uint16_t all_matches(uint8_t h2) const {
|
||||
uint16_t retval = 0;
|
||||
uint16_t bit = 1;
|
||||
for (auto xi : ctrl_) {
|
||||
if (xi == h2)
|
||||
retval |= bit;
|
||||
bit = bit << 1;
|
||||
}
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
/** find all empty sentinels in ctrl_[0..15].
|
||||
* for each empty, set corresponding bit in return value.
|
||||
* Bits {0x1, 0x2, 0x4, ...} set iff empty spot
|
||||
* {ctrl_[0], ctrl_[1], ctrl_[2], ...} respectively
|
||||
**/
|
||||
uint16_t empty_matches() const {
|
||||
uint16_t retval = 0;
|
||||
uint16_t bit = 1;
|
||||
|
||||
for (auto xi : ctrl_) {
|
||||
if (xi == DArenaHashMapUtil::c_empty_slot)
|
||||
retval |= bit;
|
||||
bit = bit << 1;
|
||||
}
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
#ifdef NOT_YET
|
||||
__m128i ctrl; // 16 bytes loaded via SSE2
|
||||
|
||||
// Find all slots matching h2
|
||||
uint16_t Match(uint8_t h2) const {
|
||||
__m128i pattern = _mm_set1_epi8(h2);
|
||||
__m128i result = _mm_cmpeq_epi8(ctrl, pattern);
|
||||
return _mm_movemask_epi8(result); // 16-bit mask
|
||||
}
|
||||
|
||||
// Find all empty slots (0xFF)
|
||||
uint16_t MatchEmpty() const {
|
||||
return _mm_movemask_epi8(_mm_cmpeq_epi8(ctrl, _mm_set1_epi8(0xFF)));
|
||||
}
|
||||
#endif
|
||||
};
|
||||
}
|
||||
} /*namespace map*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end ControlGroup.hpp */
|
||||
|
|
@ -1,153 +0,0 @@
|
|||
/** @file DArenaHashMapIterator.hpp
|
||||
*
|
||||
* @author Roland Conybeare, Jan 2026
|
||||
**/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <xo/arena/hashmap/DArenaHashMapUtil.hpp>
|
||||
|
||||
namespace xo {
|
||||
namespace map {
|
||||
namespace detail {
|
||||
|
||||
template <typename Key,
|
||||
typename Value>
|
||||
struct DArenaHashMapIterator : public DArenaHashMapUtil {
|
||||
using value_type = std::pair<const Key, Value>;
|
||||
|
||||
public:
|
||||
DArenaHashMapIterator(uint8_t * c, value_type * p)
|
||||
: ctrl_{c}, pos_{p} {}
|
||||
|
||||
value_type & operator*() const { return *pos_; }
|
||||
value_type * operator->() const { return pos_; }
|
||||
|
||||
uint8_t * _ctrl() const { return ctrl_; }
|
||||
value_type * _pos() const { return pos_; }
|
||||
|
||||
/** true iff iterator at sentinel position (not dereferencable state !) **/
|
||||
bool _at_slot_sentinel() const { return is_sentinel(*ctrl_) && (*ctrl_ != c_iterator_bookend); }
|
||||
|
||||
bool operator==(const DArenaHashMapIterator & x) const {
|
||||
return this->pos_ == x.pos_;
|
||||
}
|
||||
|
||||
bool operator!=(const DArenaHashMapIterator & x) const {
|
||||
return this->pos_ != x.pos_;
|
||||
}
|
||||
|
||||
DArenaHashMapIterator & operator++() {
|
||||
do {
|
||||
++(this->ctrl_);
|
||||
++(this->pos_);
|
||||
|
||||
/** end condition: iterator ends at last non-wrapped position.
|
||||
* relyin on bookend sentinel values at known offset from 'wrap' section
|
||||
*
|
||||
* ctrl_ ctrl_ + c_group_size
|
||||
* | |
|
||||
* v v
|
||||
* <----------------- control_size(n_slot) ---------------->
|
||||
* <-stub-> <----------- n_slot ----------> <group> <-stub->
|
||||
* +--------+-------------------------------+-------+--------+
|
||||
* | 0xF0 | empty / data / tombstone | wrap | 0xF0 |
|
||||
* +--------+-------------------------------+-------+--------+
|
||||
**/
|
||||
} while (is_sentinel(*ctrl_)
|
||||
&& (*(ctrl_ + c_group_size) != c_iterator_bookend));
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
DArenaHashMapIterator & operator--() {
|
||||
/* simpler than forward iteration, since bookend immediately
|
||||
* precedes control byte for first slot
|
||||
*/
|
||||
do {
|
||||
--(this->ctrl_);
|
||||
--(this->pos_);
|
||||
} while (is_sentinel(*ctrl_)
|
||||
&& (*ctrl_ != c_iterator_bookend));
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
private:
|
||||
uint8_t * ctrl_ = nullptr;
|
||||
value_type * pos_ = nullptr;
|
||||
};
|
||||
|
||||
template <typename Key,
|
||||
typename Value>
|
||||
struct DArenaHashMapConstIterator : public DArenaHashMapUtil {
|
||||
using value_type = std::pair<const Key, Value>;
|
||||
|
||||
public:
|
||||
DArenaHashMapConstIterator(const uint8_t * c, const value_type * p)
|
||||
: ctrl_{c}, pos_{p} {}
|
||||
|
||||
const value_type & operator*() const { return *pos_; }
|
||||
const value_type * operator->() const { return pos_; }
|
||||
|
||||
const uint8_t * _ctrl() const { return ctrl_; }
|
||||
const value_type * _pos() const { return pos_; }
|
||||
|
||||
/** true iff iterator at sentinel position (not dereferencable state !) **/
|
||||
bool _at_slot_sentinel() const {
|
||||
return is_sentinel(*ctrl_) && (*ctrl_ != c_iterator_bookend);
|
||||
}
|
||||
|
||||
bool operator==(const DArenaHashMapConstIterator & x) const {
|
||||
return this->pos_ == x.pos_;
|
||||
}
|
||||
|
||||
bool operator!=(const DArenaHashMapConstIterator & x) const {
|
||||
return this->pos_ != x.pos_;
|
||||
}
|
||||
|
||||
DArenaHashMapConstIterator & operator++() {
|
||||
do {
|
||||
++(this->ctrl_);
|
||||
++(this->pos_);
|
||||
|
||||
/** end condition: iterator ends at last non-wrapped position.
|
||||
* relyin on bookend sentinel values at known offset from 'wrap' section
|
||||
*
|
||||
* ctrl_ ctrl_ + c_group_size
|
||||
* | |
|
||||
* v v
|
||||
* <----------------- control_size(n_slot) ---------------->
|
||||
* <-stub-> <----------- n_slot ----------> <group> <-stub->
|
||||
* +--------+-------------------------------+-------+--------+
|
||||
* | 0xF0 | empty / data / tombstone | wrap | 0xF0 |
|
||||
* +--------+-------------------------------+-------+--------+
|
||||
**/
|
||||
} while (is_sentinel(*ctrl_)
|
||||
&& (*(ctrl_ + c_group_size) != c_iterator_bookend));
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
DArenaHashMapConstIterator & operator--() {
|
||||
/* simpler than forward iteration, since bookend immediately
|
||||
* precedes control byte for first slot
|
||||
*/
|
||||
do {
|
||||
--(this->ctrl_);
|
||||
--(this->pos_);
|
||||
} while (is_sentinel(*ctrl_)
|
||||
&& (*ctrl_ != c_iterator_bookend));
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
private:
|
||||
const uint8_t * ctrl_ = nullptr;
|
||||
const value_type * pos_ = nullptr;
|
||||
};
|
||||
}
|
||||
} /*namespace map*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end DArenaHashMapIterator.hpp */
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
/** @file DArenaHashMapUtil.hpp
|
||||
*
|
||||
* @author Roland Conybeare, Jan 2026
|
||||
**/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace xo {
|
||||
namespace map {
|
||||
/** @class DArenaHashMapUtil
|
||||
*
|
||||
* @pre
|
||||
*
|
||||
* control
|
||||
*
|
||||
* <----------------- control_size(n_slot) ---------------->
|
||||
* <-stub-> <----------- n_slot ----------> <group> <-stub->
|
||||
* +--------+-------------------------------+-------+--------+
|
||||
* | 0xF0 | empty / data / tombstone | wrap | 0xF0 |
|
||||
* +--------+-------------------------------+-------+--------+
|
||||
* ^ ^
|
||||
* | ... | control_[stub+i] <--> slots_[i]
|
||||
* slots v v
|
||||
* +-------------------------------+
|
||||
* | {k,v} pairs |
|
||||
* +-------------------------------+
|
||||
* <--- n_slot key-value pairs -->
|
||||
*
|
||||
* sizes:
|
||||
* - stub before+after bookends. c_control_stub bytes (16)
|
||||
* - group c_group_size. power of 2 (16 bytes)
|
||||
* - n_slot hash table slots. power of 2 multiple of c_group_size.
|
||||
*
|
||||
* control bytes:
|
||||
* - 0b1xxxxxxx sentinel bitmask
|
||||
* - 0xf0 sentinel for before/after stubs (iterator bookends)
|
||||
* - 0xff sentinel for empty slot.
|
||||
* - 0xfe sentinel for tombstone
|
||||
* - 0b0xxxxxxx high bit clear; remainder hold low 7 bits of hash
|
||||
* - wrap duplicate first c_group_size bytes (after front stub)
|
||||
* for SIMD convenience
|
||||
*
|
||||
* @endpre
|
||||
**/
|
||||
struct DArenaHashMapUtil {
|
||||
using size_type = std::size_t;
|
||||
using control_type = std::uint8_t;
|
||||
|
||||
/** control: mask for sentinel states **/
|
||||
static constexpr uint8_t c_sentinel_mask = 0x80;
|
||||
/** control: sentinel for empty slot **/
|
||||
static constexpr uint8_t c_empty_slot = 0xFF;
|
||||
/** control: tombstone for deleted slot **/
|
||||
static constexpr uint8_t c_tombstone = 0xFE;
|
||||
/** control: bookends around control array,
|
||||
* for iterator edge support
|
||||
**/
|
||||
static constexpr uint8_t c_iterator_bookend = 0xF0;
|
||||
|
||||
/** group size **/
|
||||
static constexpr size_type c_group_size = 16;
|
||||
|
||||
/** max load factor **/
|
||||
static constexpr float c_max_load_factor = 0.875;
|
||||
|
||||
/** Iterator sentinel at begin/end of control array.
|
||||
* Load-bearing for bidirectional iterator implementation
|
||||
**/
|
||||
static constexpr size_type c_control_stub = c_group_size; //c_group_size;
|
||||
|
||||
/** control: true for sentinel values **/
|
||||
static constexpr bool is_sentinel(control_type ctrl) {
|
||||
return c_sentinel_mask == (ctrl & c_sentinel_mask);
|
||||
}
|
||||
|
||||
/** control; true for non-sentinel values **/
|
||||
static constexpr bool is_data(control_type ctrl) {
|
||||
return 0 == (ctrl & c_sentinel_mask);
|
||||
}
|
||||
|
||||
/** control: compute size of control array for swiss hash map with @p n_slot cells **/
|
||||
static constexpr size_type control_size(size_type n_slot) {
|
||||
if (n_slot == 0)
|
||||
return 0;
|
||||
|
||||
/* control:
|
||||
* - c_group_size overflow slots
|
||||
* - 2x c_control_stub begin/end sentinels
|
||||
*/
|
||||
|
||||
return n_slot + c_group_size + (2 * c_control_stub);
|
||||
}
|
||||
|
||||
/** find smallest multiple k : k * c_group_size >= n **/
|
||||
static size_type lub_group_mult(size_t n) {
|
||||
return (n + c_group_size - 1) / c_group_size;
|
||||
}
|
||||
|
||||
/** find smallest x such that 2^x >= n. Return {x, 2^x} **/
|
||||
static std::pair<size_type, size_type> lub_exp2(size_t n) {
|
||||
size_type ngx = 0;
|
||||
size_type ng = 1;
|
||||
|
||||
while (ng < n) {
|
||||
++ngx;
|
||||
ng *= 2;
|
||||
}
|
||||
|
||||
return std::make_pair(ngx, ng);;
|
||||
}
|
||||
};
|
||||
} /*namespace map*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end DArenaHashMapUtil.hpp */
|
||||
|
|
@ -1,163 +0,0 @@
|
|||
/** @file HashMapStore.hpp
|
||||
*
|
||||
* @author Roland Conybeare, Jan 2026
|
||||
**/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <xo/arena/hashmap/DArenaHashMapUtil.hpp>
|
||||
#include <xo/arena/hashmap/ControlGroup.hpp>
|
||||
|
||||
namespace xo {
|
||||
namespace map {
|
||||
namespace detail {
|
||||
template <typename Key,
|
||||
typename Value>
|
||||
struct HashMapStore : DArenaHashMapUtil {
|
||||
public:
|
||||
using value_type = std::pair<const Key, Value>;
|
||||
using group_type = detail::ControlGroup;
|
||||
using control_vector_type = xo::mm::DArenaVector<uint8_t>;
|
||||
using slot_vector_type = xo::mm::DArenaVector<value_type>;
|
||||
using MemorySizeVisitor = xo::mm::MemorySizeVisitor;
|
||||
using MemorySizeInfo = xo::mm::MemorySizeInfo;
|
||||
|
||||
public:
|
||||
/** group_exp2: number of groups {x, 2^x} **/
|
||||
explicit HashMapStore(const std::string & name,
|
||||
const std::pair<size_type,
|
||||
size_type> & group_exp2)
|
||||
: size_{0},
|
||||
n_group_exponent_{group_exp2.first},
|
||||
n_group_{group_exp2.second},
|
||||
n_slot_{group_exp2.second * c_group_size},
|
||||
control_{control_vector_type::map
|
||||
(xo::mm::ArenaConfig{
|
||||
.name_ = name + "-ctl",
|
||||
.size_ = control_size(n_slot_),
|
||||
.store_header_flag_ = false})},
|
||||
slots_{slot_vector_type::map
|
||||
(xo::mm::ArenaConfig{
|
||||
.name_ = name + "-slots",
|
||||
.size_ = n_slot_ * sizeof(value_type),
|
||||
.store_header_flag_ = false})}
|
||||
{
|
||||
/* here: arenas have allocated address range, but no committed memory yet */
|
||||
|
||||
this->_init();
|
||||
}
|
||||
|
||||
size_type empty() const noexcept { return size_ == 0; }
|
||||
size_type capacity() const noexcept { return n_group_ * c_group_size; }
|
||||
float load_factor() const noexcept { return size_ / static_cast<float>(n_slot_); }
|
||||
|
||||
void visit_pools(const MemorySizeVisitor & visitor) const {
|
||||
// complexity here in service of HashMapStore-specific value for MemorySizeInfo.used
|
||||
|
||||
MemorySizeInfo ctl_info;
|
||||
MemorySizeInfo slot_info;
|
||||
|
||||
control_.visit_pools([&ctl_info](const auto & x) { ctl_info = x; });
|
||||
slots_.visit_pools([&slot_info](const auto & x) { slot_info = x; });
|
||||
|
||||
// control: 1 byte per (key,value) pair
|
||||
ctl_info.used_ = size_;
|
||||
slot_info.used_ = size_ * sizeof(value_type);
|
||||
|
||||
visitor(ctl_info);
|
||||
visitor(slot_info);
|
||||
}
|
||||
|
||||
void resize_from_empty(const std::pair<size_type,
|
||||
size_type> & group_exp2)
|
||||
{
|
||||
assert(size_ == 0);
|
||||
|
||||
this->n_group_exponent_ = group_exp2.first;
|
||||
this->n_group_ = group_exp2.second;
|
||||
this->n_slot_ = group_exp2.second * c_group_size;
|
||||
|
||||
this->_init();
|
||||
}
|
||||
|
||||
void clear() {
|
||||
/* remark: discontinuity in the sense that we lose n_group_ = 2 ^ n_group_epxonent_
|
||||
*
|
||||
* juice may not be worth the squeeze here,
|
||||
* since DArena doesn't yet (Jan 2026) unmap on clear
|
||||
*/
|
||||
|
||||
this->size_ = 0;
|
||||
this->n_group_exponent_ = 0;
|
||||
this->n_group_ = 0;
|
||||
this->n_slot_ = 0;
|
||||
|
||||
this->control_.resize(0);
|
||||
this->slots_.resize(0);
|
||||
}
|
||||
|
||||
public:
|
||||
void _init() {
|
||||
this->control_.resize(control_size(n_slot_));
|
||||
|
||||
/* front stub: iterator bookend */
|
||||
std::fill(this->control_.begin(),
|
||||
this->control_.begin() + c_control_stub,
|
||||
c_iterator_bookend);
|
||||
|
||||
/* all slots marked empty initially */
|
||||
std::fill(this->control_.begin() + c_control_stub,
|
||||
this->control_.end() - c_control_stub,
|
||||
c_empty_slot);
|
||||
|
||||
/* end stub: iterator bookend */
|
||||
std::fill(this->control_.end() - c_control_stub,
|
||||
this->control_.end(),
|
||||
c_iterator_bookend);
|
||||
|
||||
this->slots_.resize(n_slot_);
|
||||
}
|
||||
|
||||
/** load control group for slot range [ix .. ix+c_group_size) **/
|
||||
group_type _load_group(size_type ix) const {
|
||||
return group_type(&(control_[ix + c_control_stub]));
|
||||
}
|
||||
|
||||
/** update control group for slot number @p ix, replace with @p h2 **/
|
||||
void _update_control(size_type ix, uint8_t h2) {
|
||||
this->control_[ix + c_control_stub] = h2;
|
||||
|
||||
if (ix < c_group_size) {
|
||||
size_type N = this->capacity();
|
||||
|
||||
// refresh end-of-array copy
|
||||
std::memcpy(&(control_[N + c_control_stub]),
|
||||
&(control_[c_control_stub]),
|
||||
c_group_size);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
/** number of pairs in this table **/
|
||||
size_type size_ = 0;
|
||||
/** base-2 logarithm of n_group_ **/
|
||||
size_type n_group_exponent_ = 0;
|
||||
/** table has capacity for this number of groups.
|
||||
* always an exact power of two.
|
||||
* number of slots is n_group_ * c_group_size
|
||||
**/
|
||||
size_type n_group_ = (1 << n_group_exponent_);
|
||||
/** table has capacity for this number of {key,value} pairs **/
|
||||
size_type n_slot_ = n_group_ * c_group_size;
|
||||
/** control_[] partitioned into groups of
|
||||
* c_group_size (16) consecutive elements
|
||||
**/
|
||||
control_vector_type control_;
|
||||
/** slots_[] holds {key,value} pairs **/
|
||||
slot_vector_type slots_;
|
||||
};
|
||||
}
|
||||
} /*namespace map*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end HashMapStore.hpp */
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
/** @file verify_policy.hpp
|
||||
*
|
||||
* @author Roland Conybeare, Jan 2026
|
||||
**/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <xo/indentlog/scope.hpp>
|
||||
#include <string>
|
||||
|
||||
namespace xo {
|
||||
// TODO: move xo/indentlog
|
||||
|
||||
/** @brief policy for verify_ok behavior.
|
||||
*
|
||||
* Remarke: wrote this for DArenaHashMap,
|
||||
* want to incorporate into other subsystems
|
||||
* that provide a verify_ok() method.
|
||||
* e.g. RedBlackTree
|
||||
**/
|
||||
struct verify_policy {
|
||||
static verify_policy log_only() {
|
||||
return verify_policy{.flags_ = 0x01};
|
||||
}
|
||||
static verify_policy throw_only() {
|
||||
return verify_policy{.flags_ = 0x02};
|
||||
}
|
||||
static verify_policy chatty() {
|
||||
return verify_policy{.flags_ = 0x03};
|
||||
}
|
||||
|
||||
bool is_silent() const noexcept { return flags_ == 0; }
|
||||
bool log_flag() const noexcept { return flags_ & 0x01; }
|
||||
bool throw_flag() const noexcept { return flags_ & 0x02; }
|
||||
|
||||
template<typename... Tn>
|
||||
bool report_error(scope & log, Tn&&... args)
|
||||
{
|
||||
if (!this->is_silent()) {
|
||||
// TODO: consider global arena here for string
|
||||
std::string msg = tostr(std::forward<Tn>(args)...);
|
||||
|
||||
if (this->log_flag()) {
|
||||
log.retroactively_enable();
|
||||
log(msg);
|
||||
}
|
||||
if (this->throw_flag()) {
|
||||
throw std::runtime_error(msg);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const char * c_self_ = "anonymous";
|
||||
uint8_t flags_;
|
||||
};
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end verify_policy.hpp */
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
/** @file mmap_util.hpp
|
||||
*
|
||||
* @author Roland Conybeare, Jan 2026
|
||||
**/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "span.hpp"
|
||||
|
||||
namespace xo {
|
||||
namespace mm {
|
||||
struct mmap_util {
|
||||
using byte = std::byte;
|
||||
using span_type = span<byte>;
|
||||
using size_type = std::size_t;
|
||||
|
||||
/** obtain uncommitted contiguous memory range comprising
|
||||
* a whole multiple of @p align_z bytes, of at least size @p req_z,
|
||||
* aligned on a @p align_z boundary. Uncommitted memory is not (yet)
|
||||
* backed by physical memory.
|
||||
*
|
||||
* If @p enable_hugepage_flag is true and THP
|
||||
* (transparent huge pages) are available, use THP for arena memory.
|
||||
* This relieves TLB and page table memory when @p req_z is a lot larger than
|
||||
* page size (likely 4KB). Cost is that arena will consum physical memory in unit
|
||||
* of @p align_z. Arena may waste up to @p align_z bytes of memory as a result.
|
||||
*
|
||||
* If @p enable_hugepage_flag is true, @p align_z should be huge page size
|
||||
* (probably 2MB) for optimal performance.
|
||||
*
|
||||
* At present the THP feature is not supported on OSX.
|
||||
* May be supportable through mach_vm_allocate().
|
||||
*
|
||||
* Note that we reject MAP_HUGETLB|MAP_HUGE_2MB flags to mmap here,
|
||||
* since requires previously-reserved memory in /proc/sys/vm/nr_hugepages.
|
||||
*
|
||||
* Write log messages iff @p debug_flag is true.
|
||||
*
|
||||
* @return spqn giving reserved memory address range [lo,hi)
|
||||
**/
|
||||
static span_type map_aligned_range(size_type req_z,
|
||||
size_type align_z,
|
||||
bool enable_hugepage_flag,
|
||||
bool debug_flag);
|
||||
};
|
||||
} /*namespace mm*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end mmap_util.hpp */
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
/** @file padding.hpp
|
||||
*
|
||||
* @author Roland Conybeare, Dec 2025
|
||||
**/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <cstdint>
|
||||
|
||||
namespace xo {
|
||||
namespace mm {
|
||||
|
||||
struct padding {
|
||||
/** word size for alignment**/
|
||||
static constexpr std::size_t c_alloc_alignment = sizeof(std::uintptr_t);
|
||||
|
||||
static inline std::size_t is_aligned(std::size_t n,
|
||||
std::size_t align = c_alloc_alignment) {
|
||||
return n % align == 0;
|
||||
}
|
||||
|
||||
/** how much to add to @p z to get a multiple of
|
||||
* @ref c_alloc_alignment
|
||||
**/
|
||||
static inline std::size_t alloc_padding(std::size_t z,
|
||||
std::size_t align = c_alloc_alignment)
|
||||
{
|
||||
|
||||
/* round up to multiple of c_bpw, but map 0 -> 0
|
||||
* (table assuming c_bpw==8)
|
||||
*
|
||||
* z%c_bpw dz
|
||||
* ------------
|
||||
* 0 0
|
||||
* 1 7
|
||||
* 2 6
|
||||
* .. ..
|
||||
* 7 1
|
||||
*/
|
||||
std::size_t dz = (align - (z % align)) % align;
|
||||
|
||||
return dz;
|
||||
}
|
||||
|
||||
/** @p z rounded up to an exact multiple
|
||||
* of @ref c_alloc_alignment
|
||||
**/
|
||||
static inline
|
||||
std::size_t with_padding(std::size_t z,
|
||||
std::size_t align = c_alloc_alignment)
|
||||
{
|
||||
return z + alloc_padding(z, align);
|
||||
}
|
||||
};
|
||||
|
||||
} /*namespace mm*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end padding.hpp */
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
/** @file print.hpp
|
||||
*
|
||||
* @author Roland Conybeare, Dec 2025
|
||||
**/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "AllocError.hpp"
|
||||
#include <xo/indentlog/print/tag.hpp>
|
||||
#include <iostream>
|
||||
|
||||
namespace xo {
|
||||
namespace mm {
|
||||
inline std::ostream &
|
||||
operator<<(std::ostream & os, const error & x) {
|
||||
os << AllocError::error_description(x);
|
||||
return os;
|
||||
}
|
||||
|
||||
inline std::ostream &
|
||||
operator<<(std::ostream & os, const AllocError & x) {
|
||||
os << "<AllocError"
|
||||
<< xtag("error", x.error_);
|
||||
|
||||
if (x.src_fn_)
|
||||
os << xtag("src_fn", x.src_fn_);
|
||||
|
||||
os << xtag("seq", x.error_seq_)
|
||||
<< xtag("req_z", x.request_z_)
|
||||
<< xtag("commit_z", x.committed_z_)
|
||||
<< xtag("resv_z", x.reserved_z_)
|
||||
<< ">";
|
||||
|
||||
return os;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* end print.hpp */
|
||||
|
|
@ -1,329 +0,0 @@
|
|||
/** @file span.hpp
|
||||
*
|
||||
* @author Roland Conybeare, Jul 2024
|
||||
**/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "xo/indentlog/scope.hpp"
|
||||
#include "xo/indentlog/print/ppdetail_atomic.hpp"
|
||||
#include <ostream>
|
||||
#include <cstdint>
|
||||
#include <cassert>
|
||||
|
||||
namespace xo {
|
||||
namespace mm {
|
||||
/** @class span compression/span.hpp
|
||||
*
|
||||
* @brief A contiguous range of characters, without ownership.
|
||||
*
|
||||
* @tparam CharT type for elements referred to by this span.
|
||||
**/
|
||||
template <typename CharT>
|
||||
class span {
|
||||
public:
|
||||
/** @defgroup span-type-traits span type traits **/
|
||||
///@{
|
||||
|
||||
/** typealias for span size (in units of CharT) **/
|
||||
using size_type = std::uint64_t;
|
||||
|
||||
/** typealias for span elements **/
|
||||
using value_type = CharT;
|
||||
|
||||
///@}
|
||||
|
||||
public:
|
||||
/** @defgroup span-ctors span constructors **/
|
||||
///@{
|
||||
|
||||
/** null span **/
|
||||
span() : lo_{nullptr}, hi_{nullptr} {}
|
||||
|
||||
/** Create span for the contiguous memory range [@p lo, @p hi) **/
|
||||
span(CharT * lo, CharT * hi) : lo_{lo}, hi_{hi} {}
|
||||
|
||||
/** Create span for the contiguous memory range [@p lo, @p lo + z) **/
|
||||
span(CharT * lo, size_t z) : lo_{lo}, hi_{lo + z} {}
|
||||
|
||||
/** explicit conversion from span<U> **/
|
||||
template<typename CharU>
|
||||
span(const span<CharU> & other,
|
||||
std::enable_if_t<std::is_convertible_v<CharU*, CharT*>
|
||||
&& !std::is_same_v<CharU, CharT>> * = nullptr)
|
||||
: lo_{other.lo()}, hi_{other.hi()} {}
|
||||
|
||||
/** copy ctor (explicit to avoid ambiguity with template ctor) **/
|
||||
span(const span & other) = default;
|
||||
span & operator=(const span & other) = default;
|
||||
|
||||
/** Create a null span (i.e. with null @p lo, @p hi pointers)
|
||||
* A null span can be concatenated with any other span
|
||||
* without triggering matching-endpoint asserts.
|
||||
**/
|
||||
static span make_null() { return span(static_cast<CharT*>(nullptr),
|
||||
static_cast<CharT*>(nullptr)); }
|
||||
|
||||
/** @brief create span for C-style string @p cstr **/
|
||||
static span from_cstr(const CharT * cstr) {
|
||||
CharT * lo = cstr;
|
||||
CharT * hi = cstr ? cstr + strlen(cstr) : nullptr;
|
||||
|
||||
return span(lo, hi);
|
||||
}
|
||||
|
||||
/** @brief create span from std::string @p str **/
|
||||
static span from_string(const std::string & str) {
|
||||
CharT * lo = &(*str.begin());
|
||||
CharT * hi = &(*str.end());
|
||||
|
||||
return span(lo, hi);
|
||||
}
|
||||
|
||||
/** @brief create span from std::string @p str **/
|
||||
static span from_string_view(const std::string_view & sv) {
|
||||
CharT * lo = &(*sv.begin());
|
||||
CharT * hi = &(*sv.end());
|
||||
|
||||
return span(lo, hi);
|
||||
}
|
||||
|
||||
/** @brief create span from raw memory **/
|
||||
static span from_memory(span<std::byte> span_memory) {
|
||||
CharT * lo = (CharT *)span_memory.lo();
|
||||
CharT * hi = (CharT *)span_memory.hi();
|
||||
|
||||
return span(lo, hi);
|
||||
}
|
||||
|
||||
/** @brief concatenate two contiguous spans */
|
||||
static span concat(const span & span1, const span & span2) {
|
||||
if (span1.is_null())
|
||||
return span2;
|
||||
if (span2.is_null())
|
||||
return span1;
|
||||
|
||||
if (span1.hi() != span2.lo()) {
|
||||
scope log(XO_DEBUG(true));
|
||||
|
||||
log && log(xtag("span1.hi", (void*)span1.hi()), xtag("span2.lo", (void*)span2.lo()));
|
||||
}
|
||||
|
||||
assert(span1.hi() == span2.lo());
|
||||
|
||||
CharT * lo = span1.lo();
|
||||
CharT * hi = span2.hi();
|
||||
|
||||
return span(lo, hi);
|
||||
}
|
||||
|
||||
///@}
|
||||
|
||||
/** @defgroup span-access-methods **/
|
||||
///@{
|
||||
|
||||
CharT * lo() const { return lo_; } /* get member span::lo_ */
|
||||
CharT * hi() const { return hi_; } /* get member span::hi_ */
|
||||
|
||||
/** true iff this span is null. distinct from empty. **/
|
||||
bool is_null() const { return lo_ == nullptr && hi_ == nullptr; }
|
||||
/** true iff this span is empty (comprises 0 elements). **/
|
||||
bool empty() const { return lo_ == hi_; }
|
||||
/** report the number of elements (of type CharT) in this span. **/
|
||||
size_type size() const { return hi_ - lo_; }
|
||||
|
||||
/** true iff this span is a subspan of @p other.
|
||||
* i.e. other.lo() <= this->lo() && this->hi() <= other.hi()
|
||||
**/
|
||||
bool is_subspan_of(const span & other) const noexcept {
|
||||
return (other.lo() <= lo_) && (hi_ <= other.hi());
|
||||
}
|
||||
|
||||
/** convert to string view **/
|
||||
std::string_view to_string_view() const {
|
||||
return std::string_view((const char *)lo_, (const char *)hi_);
|
||||
}
|
||||
|
||||
///@}
|
||||
|
||||
/** @defgroup span-general-methods **/
|
||||
///@{
|
||||
|
||||
/** @brief strip prefix until first occurence of '\n', including the newline **/
|
||||
void discard_until_newline() {
|
||||
for (const CharT * p = lo_; p < hi_; ++p) {
|
||||
if (*p == '\n') {
|
||||
lo_ = p + 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
lo_ = hi_;
|
||||
}
|
||||
|
||||
/** Create new span over supplied type,
|
||||
* with identical (possibly misaligned) endpoints.
|
||||
*
|
||||
* @warning
|
||||
* 1. New span uses exactly the same memory addresses.
|
||||
* Endpoint pointers may not be aligned.
|
||||
* 2. Implementation assumes code compiled with
|
||||
* @code -fno-strict-aliasing @endcode enabled.
|
||||
*
|
||||
* @tparam OtherT element type for new span
|
||||
**/
|
||||
template <typename OtherT>
|
||||
span<OtherT>
|
||||
cast() const { return span<OtherT>(reinterpret_cast<OtherT *>(lo_),
|
||||
reinterpret_cast<OtherT *>(hi_)); }
|
||||
|
||||
/** @brief create span including the first @p z members of this span. **/
|
||||
span prefix(size_type z) const { return span(lo_, lo_ + z); }
|
||||
|
||||
/** @brief create span representing prefix up to (but not including) @p *p
|
||||
**/
|
||||
span prefix_upto(CharT * p) const {
|
||||
if (p <= hi_)
|
||||
return span(lo_, p);
|
||||
else
|
||||
return span(lo_, hi_);
|
||||
}
|
||||
|
||||
/** @brief create span with first @p z members of this span removed **/
|
||||
span after_prefix(size_type z) const {
|
||||
if (lo_ + z > hi_)
|
||||
z = hi_ - lo_;
|
||||
|
||||
return span(lo_ + z, hi_);
|
||||
}
|
||||
|
||||
/** @brief create span with @p prefix of this span removed **/
|
||||
span after_prefix(const span & prefix) const {
|
||||
if (!prefix.is_null() && (prefix.lo() != lo_)) {
|
||||
throw std::runtime_error
|
||||
("after_prefix: expected prefix of this span");
|
||||
}
|
||||
|
||||
return after_prefix(prefix.size());
|
||||
}
|
||||
|
||||
/** Create span starting with position @p p.
|
||||
* Does boundary checking; will return empty span if @p p is outside @c [lo_,hi)
|
||||
**/
|
||||
span suffix_from(CharT * p) const {
|
||||
if ((lo_ <= p) && (p <= hi_))
|
||||
return span(p, hi_);
|
||||
else
|
||||
return span(hi_, hi_);
|
||||
}
|
||||
|
||||
/** increase extent of this spans to include @p x.
|
||||
* Requires @c hi() == @c x.lo()
|
||||
**/
|
||||
span & operator+=(const span & x) {
|
||||
if (hi_ == x.lo_) {
|
||||
hi_ = x.hi_;
|
||||
} else if (!x.is_null()) {
|
||||
assert(false);
|
||||
}
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
/** print representation for this span on stream @p os **/
|
||||
void print(std::ostream & os) const {
|
||||
os << "<span"
|
||||
<< xtag("addr", (void*)lo_)
|
||||
<< xtag("size", size())
|
||||
<< " :text " << xo::print::quot(std::string_view(lo_, hi_))
|
||||
<< ">";
|
||||
}
|
||||
///@}
|
||||
|
||||
private:
|
||||
/** @defgroup span-instance-vars **/
|
||||
///@{
|
||||
|
||||
/** start of span.
|
||||
Span comprises memory address between @p lo (inclusive) and @p hi (exclusive)
|
||||
**/
|
||||
CharT * lo_ = nullptr;
|
||||
|
||||
/** @brief end of span.
|
||||
Span comprises memory address between @p lo (inclusive) and @p hi (exclusive)
|
||||
**/
|
||||
CharT * hi_ = nullptr;
|
||||
|
||||
///@}
|
||||
}; /*span*/
|
||||
|
||||
/** @defgroup span-operators **/
|
||||
///@{
|
||||
|
||||
/** compare spans for equality.
|
||||
* Two spans are equal iff both endpoints match exactly.
|
||||
**/
|
||||
template <typename CharT>
|
||||
inline bool
|
||||
operator==(const span<CharT> & lhs, const span<CharT> & rhs) {
|
||||
return ((lhs.lo() == rhs.lo())
|
||||
&& (lhs.hi() == rhs.hi()));
|
||||
}
|
||||
|
||||
/** compare spans for inequality.
|
||||
* Two spans are unequal if either paired endpoint differs.
|
||||
**/
|
||||
template <typename CharT>
|
||||
inline bool
|
||||
operator!=(const span<CharT> & lhs, const span<CharT> & rhs) {
|
||||
return ((lhs.lo() != rhs.lo())
|
||||
|| (lhs.hi() != rhs.hi()));
|
||||
}
|
||||
|
||||
/** print a summary of @p x on stream @p os. Intended for diagnostics **/
|
||||
template <typename CharT>
|
||||
inline std::ostream &
|
||||
operator<<(std::ostream & os,
|
||||
const span<CharT> & x) {
|
||||
x.print(os);
|
||||
return os;
|
||||
}
|
||||
|
||||
///@}
|
||||
} /*namespace scm*/
|
||||
|
||||
namespace print {
|
||||
template <typename CharT>
|
||||
class printspan_impl {
|
||||
public:
|
||||
printspan_impl(xo::mm::span<CharT> x) : span_{x} {}
|
||||
|
||||
xo::mm::span<CharT> span_;
|
||||
};
|
||||
|
||||
template <typename CharT>
|
||||
printspan_impl<CharT> printspan(const xo::mm::span<CharT>& span) {
|
||||
return printspan_impl<CharT>(span);
|
||||
}
|
||||
|
||||
template <typename CharT>
|
||||
inline std::ostream &
|
||||
operator<< (std::ostream & os,
|
||||
const printspan_impl<CharT> & x)
|
||||
{
|
||||
for (const CharT * p = x.span_.lo(); p < x.span_.hi(); ++p)
|
||||
os << *p;
|
||||
|
||||
return os;
|
||||
}
|
||||
|
||||
#ifndef ppdetail_atomic
|
||||
template <typename CharT> \
|
||||
PPDETAIL_ATOMIC_BODY(printspan_impl<CharT>);
|
||||
|
||||
template <typename CharT> \
|
||||
PPDETAIL_ATOMIC_BODY(xo::scm::span<CharT>);
|
||||
#endif
|
||||
|
||||
} /*namespace mm*/
|
||||
} /*namespace xo*/
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
/** @file AllocError.cpp
|
||||
*
|
||||
* @author Roland Conybeare, Dec 2025
|
||||
**/
|
||||
|
||||
#include "AllocError.hpp"
|
||||
|
||||
namespace xo {
|
||||
namespace mm {
|
||||
|
||||
const char *
|
||||
AllocError::error_description(error x)
|
||||
{
|
||||
switch (x) {
|
||||
case error::invalid:
|
||||
break;
|
||||
case error::ok:
|
||||
return "ok";
|
||||
case error::reserve_exhausted:
|
||||
return "reserve-exhausted";
|
||||
case error::commit_failed:
|
||||
return "commit-failed";
|
||||
case error::header_size_mask:
|
||||
return "header-size-mask";
|
||||
case error::orphan_sub_alloc:
|
||||
return "orphan-sub-alloc";
|
||||
case error::alloc_info_disabled:
|
||||
return "alloc-info-disabled";
|
||||
case error::alloc_info_address:
|
||||
return "alloc-info-address";
|
||||
case error::alloc_iterator_not_supported:
|
||||
return "alloc-iterator-not-supported";
|
||||
case error::alloc_iterator_deref:
|
||||
return "alloc-iterator-deref";
|
||||
case error::alloc_iterator_next:
|
||||
return "alloc-iterator-next";
|
||||
}
|
||||
|
||||
return "?error";
|
||||
}
|
||||
|
||||
} /*namespace mm*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end AllocError.cpp */
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
/** @file AllocInfo.cpp
|
||||
*
|
||||
* @author Roland Conybeare, Dec 2025
|
||||
**/
|
||||
|
||||
#include "AllocInfo.hpp"
|
||||
|
||||
namespace xo {
|
||||
namespace mm {
|
||||
auto
|
||||
AllocInfo::guard_lo() const noexcept -> span_type
|
||||
{
|
||||
if (!p_guard_lo_)
|
||||
return span_type(nullptr, nullptr);
|
||||
|
||||
return span_type(p_guard_lo_,
|
||||
p_guard_lo_ + p_config_->guard_z_);
|
||||
}
|
||||
|
||||
auto
|
||||
AllocInfo::payload() const noexcept -> span_type
|
||||
{
|
||||
if (!p_header_)
|
||||
return span_type(nullptr, nullptr);
|
||||
|
||||
byte * lo = (byte *)(p_header_ + 1);
|
||||
size_type z = this->size();
|
||||
|
||||
return span_type(lo, lo+z);
|
||||
}
|
||||
|
||||
auto
|
||||
AllocInfo::guard_hi() const noexcept -> span_type
|
||||
{
|
||||
if (!p_guard_hi_)
|
||||
return span_type(nullptr, nullptr);
|
||||
|
||||
return span_type(p_guard_hi_,
|
||||
p_guard_hi_ + p_config_->guard_z_);
|
||||
}
|
||||
|
||||
} /*namespace mm*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end AllocInfo.cpp */
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
# xo-arena/src/CMakeLists.txt
|
||||
|
||||
set(SELF_LIB xo_arena)
|
||||
set(SELF_SRCS
|
||||
arena_streambuf.cpp
|
||||
ErrorArena.cpp
|
||||
cmpresult.cpp
|
||||
mmap_util.cpp
|
||||
AllocError.cpp
|
||||
AllocInfo.cpp
|
||||
DArena.cpp
|
||||
DArenaIterator.cpp
|
||||
DCircularBuffer.cpp
|
||||
backtrace.cpp
|
||||
)
|
||||
|
||||
xo_add_shared_library4(${SELF_LIB} ${PROJECT_NAME}Targets ${PROJECT_VERSION} 1 ${SELF_SRCS})
|
||||
xo_install_include_tree3(include/xo/arena)
|
||||
|
||||
# ----------------------------------------------------------------
|
||||
# input dependencies
|
||||
#
|
||||
# NOTE: dependency set here must be kept consistent with
|
||||
# xo-arena/cmake/xo_arenaConfig.cmake.in
|
||||
|
||||
xo_dependency(${SELF_LIB} xo_reflectutil)
|
||||
xo_dependency(${SELF_LIB} indentlog)
|
||||
|
||||
if (NOT APPLE)
|
||||
xo_external_pkgconfig_dependency(${SELF_LIB} LIBUNWIND libunwind-generic)
|
||||
xo_external_pkgconfig_dependency(${SELF_LIB} LIBDW libdw)
|
||||
else()
|
||||
endif()
|
||||
|
||||
# end src/CMakeLists.txt
|
||||
|
|
@ -1,655 +0,0 @@
|
|||
/** @file DArena.cpp
|
||||
*
|
||||
* @author Roland Conybeare, Dec 2025
|
||||
**/
|
||||
|
||||
//#include "alloc/AAllocator.hpp"
|
||||
#include "DArena.hpp"
|
||||
#include "DArenaIterator.hpp"
|
||||
#include "mmap_util.hpp"
|
||||
#include "backtrace.hpp"
|
||||
#include <xo/arena/padding.hpp>
|
||||
#include <xo/indentlog/scope.hpp>
|
||||
#include <xo/indentlog/print/tag.hpp>
|
||||
#include <cassert>
|
||||
#include <exception>
|
||||
#include <new> // for std::launder()
|
||||
#include <sys/mman.h> // for ::munmap()
|
||||
#include <unistd.h> // for ::getpagesize()
|
||||
#include <string.h> // for ::memset()
|
||||
|
||||
namespace xo {
|
||||
using xo::reflect::typeseq;
|
||||
using std::byte;
|
||||
using std::cerr;
|
||||
using std::endl;
|
||||
using std::size_t;
|
||||
|
||||
namespace mm {
|
||||
|
||||
DArena
|
||||
DArena::map(const ArenaConfig & cfg)
|
||||
{
|
||||
scope log(XO_DEBUG(cfg.debug_flag_));
|
||||
|
||||
/* vm page size. 4KB, probably */
|
||||
size_t page_z = getpagesize();
|
||||
|
||||
bool enable_hugepage_flag = (cfg.size_ >= cfg.hugepage_z_);
|
||||
|
||||
/* Align start of arena memory on this boundary.
|
||||
* Will use THP (transparent huge pages) if available
|
||||
* and arena size is at least as large as hugepage size (2MB, probably)
|
||||
*/
|
||||
size_t align_z = (enable_hugepage_flag ? cfg.hugepage_z_ : page_z);
|
||||
|
||||
log && log(xtag("page_z", page_z),
|
||||
xtag("align_z", align_z));
|
||||
|
||||
auto span = mmap_util::map_aligned_range(cfg.size_,
|
||||
align_z,
|
||||
enable_hugepage_flag,
|
||||
cfg.debug_flag_);
|
||||
|
||||
if (!span.lo()) {
|
||||
// control here implies mmap() failed silently
|
||||
|
||||
throw std::runtime_error(tostr("ArenaAlloc: reserve address range failed",
|
||||
xtag("size", cfg.size_)));
|
||||
}
|
||||
|
||||
|
||||
#ifdef NOPE
|
||||
log && log(xtag("lo", (void*)lo_),
|
||||
xtag("page_z", page_z_),
|
||||
xtag("hugepage_z", hugepage_z_));
|
||||
#endif
|
||||
|
||||
return DArena(cfg, page_z, align_z, span.lo(), span.hi());
|
||||
} /*map*/
|
||||
|
||||
DArena::DArena(const ArenaConfig & cfg)
|
||||
{
|
||||
*this = map(cfg);
|
||||
}
|
||||
|
||||
DArena::DArena(const ArenaConfig & cfg,
|
||||
size_type page_z,
|
||||
size_type arena_align_z,
|
||||
byte * lo,
|
||||
byte * hi) : config_{cfg},
|
||||
page_z_{page_z},
|
||||
arena_align_z_{arena_align_z},
|
||||
lo_{lo},
|
||||
committed_z_{0},
|
||||
free_{lo},
|
||||
limit_{lo},
|
||||
hi_{hi},
|
||||
error_count_{0},
|
||||
last_error_{}
|
||||
{
|
||||
//retval.checkpoint_ = lo_;
|
||||
|
||||
/** make sure guard size is aligned **/
|
||||
config_.header_.guard_z_
|
||||
= padding::with_padding(config_.header_.guard_z_);
|
||||
}
|
||||
|
||||
DArena::DArena(DArena && other) {
|
||||
config_ = other.config_;
|
||||
page_z_ = other.page_z_;
|
||||
arena_align_z_ = other.arena_align_z_;
|
||||
lo_ = other.lo_;
|
||||
committed_z_ = other.committed_z_;
|
||||
free_ = other.free_;
|
||||
limit_ = other.limit_;
|
||||
hi_ = other.hi_;
|
||||
error_count_ = other.error_count_;
|
||||
last_error_ = other.last_error_;
|
||||
|
||||
other.config_ = ArenaConfig();
|
||||
other.lo_ = nullptr;
|
||||
other.committed_z_ = 0;
|
||||
other.free_ = nullptr;
|
||||
other.limit_ = nullptr;
|
||||
other.hi_ = nullptr;
|
||||
other.error_count_ = 0;
|
||||
other.last_error_ = AllocError();
|
||||
}
|
||||
|
||||
DArena &
|
||||
DArena::operator=(DArena && other)
|
||||
{
|
||||
config_ = other.config_;
|
||||
page_z_ = other.page_z_;
|
||||
arena_align_z_ = other.arena_align_z_;
|
||||
lo_ = other.lo_;
|
||||
committed_z_ = other.committed_z_;
|
||||
free_ = other.free_;
|
||||
limit_ = other.limit_;
|
||||
hi_ = other.hi_;
|
||||
error_count_ = other.error_count_;
|
||||
last_error_ = other.last_error_;
|
||||
|
||||
other.config_ = ArenaConfig();
|
||||
other.lo_ = nullptr;
|
||||
other.committed_z_ = 0;
|
||||
other.free_ = nullptr;
|
||||
other.limit_ = nullptr;
|
||||
other.hi_ = nullptr;
|
||||
other.error_count_ = 0;
|
||||
other.last_error_ = AllocError();
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
void
|
||||
DArena::unmap() noexcept
|
||||
{
|
||||
if (lo_) {
|
||||
//log && log("unmap [lo,hi)",
|
||||
// xtag("lo", lo_),
|
||||
// xtag("z", hi_ - lo_),
|
||||
// xtag("hi", hi_));
|
||||
|
||||
::munmap(lo_, hi_ - lo_);
|
||||
}
|
||||
|
||||
/* Mandatory hygiene: zero the bookkeeping tail so no dangling
|
||||
* pointers survive (e.g. after ~DArena() runs the dtor of a
|
||||
* stack-owned arena). config_, page_z_, arena_align_z_ are
|
||||
* preserved -- only {lo_ .. last_error_} get cleared.
|
||||
*
|
||||
* The memset goes through a std::launder'd self pointer so the
|
||||
* compiler cannot prove it writes to the dying object and elide
|
||||
* the stores as dead (gcc>=15 does that with a plain member
|
||||
* assignment / un-laundered memset at -O1). All cleared fields
|
||||
* are trivially-copyable, so byte-zeroing them is well-defined.
|
||||
*/
|
||||
DArena * self = std::launder(this);
|
||||
byte * tail = reinterpret_cast<byte *>(&self->lo_);
|
||||
byte * end = reinterpret_cast<byte *>(self) + sizeof(DArena);
|
||||
|
||||
::memset(tail, 0, end - tail);
|
||||
}
|
||||
|
||||
DArena::~DArena()
|
||||
{
|
||||
this->unmap();
|
||||
}
|
||||
|
||||
auto
|
||||
DArena::obj2hdr(void * obj) noexcept -> header_type *
|
||||
{
|
||||
assert(config_.store_header_flag_);
|
||||
|
||||
return (header_type *)((byte *)obj - sizeof(header_type));
|
||||
}
|
||||
|
||||
auto
|
||||
DArena::obj2hdr(void * obj) const noexcept -> const header_type *
|
||||
{
|
||||
assert(config_.store_header_flag_);
|
||||
|
||||
return (const header_type *)((byte *)obj - sizeof(header_type));
|
||||
}
|
||||
|
||||
void
|
||||
DArena::visit_pools(const MemorySizeVisitor & fn) const
|
||||
{
|
||||
/** arena can't tell purpose of allocated memory;
|
||||
* must assume it's all used
|
||||
**/
|
||||
|
||||
// assemble histogram
|
||||
MemorySizeInfo::DetailArrayType detail_v;
|
||||
MemorySizeInfo::DetailArrayType * p_detail = nullptr;
|
||||
|
||||
if (config_.store_header_flag_) {
|
||||
p_detail = &detail_v;
|
||||
|
||||
for (const auto & ix : *this) {
|
||||
typeseq ix_tseq(ix.tseq());
|
||||
|
||||
// totals in detail_v[0]
|
||||
MemorySizeDetail & d = detail_v[0];
|
||||
++d.n_alloc_;
|
||||
d.z_alloc_ += ix.size();
|
||||
|
||||
// O(n) insertion here
|
||||
for (size_t i = 1; i < detail_v.size(); ++i) {
|
||||
if (detail_v[i].tseq_.is_sentinel()
|
||||
|| (detail_v[i].tseq_ == ix_tseq))
|
||||
{
|
||||
MemorySizeDetail & d = detail_v[i];
|
||||
|
||||
d.tseq_ = ix_tseq;
|
||||
++d.n_alloc_;
|
||||
d.z_alloc_ += ix.size();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn(MemorySizeInfo(config_.name_,
|
||||
this->allocated() /*used*/,
|
||||
this->allocated(),
|
||||
this->committed(),
|
||||
this->reserved(),
|
||||
lo_,
|
||||
hi_,
|
||||
p_detail));
|
||||
}
|
||||
|
||||
AllocInfo
|
||||
DArena::alloc_info(value_type mem) const noexcept
|
||||
{
|
||||
if (!config_.store_header_flag_) [[unlikely]] {
|
||||
this->capture_error(error::alloc_info_disabled, __PRETTY_FUNCTION__);
|
||||
|
||||
return AllocInfo::error_not_configured(&config_.header_);
|
||||
}
|
||||
|
||||
byte * header_mem = mem - sizeof(AllocHeader);
|
||||
|
||||
#ifdef OBSOLETE // relying on cross-alloc header shenanigans in DX1Collector
|
||||
if (!this->contains(header_mem)) {
|
||||
this->capture_error(error::alloc_info_address);
|
||||
}
|
||||
#endif
|
||||
|
||||
AllocHeader * header = (AllocHeader *)header_mem;
|
||||
|
||||
const byte * guard_lo
|
||||
= header_mem - config_.header_.guard_z_;
|
||||
const byte * guard_hi
|
||||
= mem + config_.header_.size(*header);
|
||||
|
||||
return AllocInfo(&config_.header_,
|
||||
guard_lo,
|
||||
(AllocHeader *)header_mem,
|
||||
guard_hi);
|
||||
}
|
||||
|
||||
DArenaIterator
|
||||
DArena::begin() const noexcept
|
||||
{
|
||||
return DArenaIterator::begin(this);
|
||||
}
|
||||
|
||||
DArenaIterator
|
||||
DArena::end() const noexcept
|
||||
{
|
||||
return DArenaIterator::end(this);
|
||||
}
|
||||
|
||||
AllocHeader *
|
||||
DArena::begin_header() const noexcept
|
||||
{
|
||||
if (config_.store_header_flag_ == false) {
|
||||
this->capture_error(error::alloc_iterator_not_supported, __PRETTY_FUNCTION__);
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return (AllocHeader *)(lo_ + config_.header_.guard_z_);
|
||||
}
|
||||
|
||||
AllocHeader *
|
||||
DArena::end_header() const noexcept
|
||||
{
|
||||
if (config_.store_header_flag_ == false) {
|
||||
this->capture_error(error::alloc_iterator_not_supported, __PRETTY_FUNCTION__);
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return (AllocHeader *)free_;
|
||||
}
|
||||
|
||||
std::byte *
|
||||
DArena::alloc(typeseq t, std::size_t req_z)
|
||||
{
|
||||
/* - primary allocation path:
|
||||
* exactly 1 header per alloc() call.
|
||||
* - store_header_flag follows configuration
|
||||
*/
|
||||
return _alloc(req_z,
|
||||
alloc_mode::standard,
|
||||
t,
|
||||
0 /*age*/,
|
||||
__PRETTY_FUNCTION__);
|
||||
}
|
||||
|
||||
std::byte *
|
||||
DArena::super_alloc(typeseq t, std::size_t req_z)
|
||||
{
|
||||
/* - (uncommon) pattern for parent alloc immediately followed by
|
||||
* zero-or-more susidiary allocs, all sharing a single header.
|
||||
* - collapses into alloc() behavior when
|
||||
* ArenaConfig.store_header_flag_ disabled
|
||||
*/
|
||||
|
||||
(void)t;
|
||||
|
||||
return _alloc(req_z,
|
||||
alloc_mode::super,
|
||||
t,
|
||||
0 /*age*/,
|
||||
__PRETTY_FUNCTION__);
|
||||
}
|
||||
|
||||
std::byte *
|
||||
DArena::sub_alloc(std::size_t req_z,
|
||||
bool complete_flag)
|
||||
{
|
||||
/* - (uncommon) pattern for subsidiary allocs:
|
||||
* that piggyback onto preceding super_alloc()
|
||||
* - collapses into alloc() behavior when
|
||||
* ArenaConfig.store_header_flag_ disabled
|
||||
*/
|
||||
|
||||
return _alloc(req_z,
|
||||
(complete_flag
|
||||
? alloc_mode::sub_complete
|
||||
: alloc_mode::sub_incomplete),
|
||||
typeseq::sentinel() /*typeseq: ignored*/,
|
||||
0 /*age - ignored */,
|
||||
__PRETTY_FUNCTION__);
|
||||
}
|
||||
|
||||
std::byte *
|
||||
DArena::alloc_copy(std::byte * src)
|
||||
{
|
||||
/* NOTE: allocator that owns src must have the same header configuration */
|
||||
|
||||
assert(config_.store_header_flag_);
|
||||
|
||||
/* src will come from an allocator other than this one;
|
||||
* we rely on header layout from destination
|
||||
* allocator -> assumes compatible header config
|
||||
*/
|
||||
AllocInfo src_info = alloc_info(src);
|
||||
|
||||
size_t req_z = src_info.size();
|
||||
typeseq tseq = typeseq(src_info.tseq());
|
||||
uint32_t age = src_info.age();
|
||||
|
||||
return _alloc(req_z, alloc_mode::standard, tseq, age + 1,
|
||||
__PRETTY_FUNCTION__);
|
||||
}
|
||||
|
||||
void
|
||||
DArena::capture_error(error err,
|
||||
const char * src_fn,
|
||||
size_type target_z) const
|
||||
{
|
||||
DArena * self = const_cast<DArena *>(this);
|
||||
|
||||
++(self->error_count_);
|
||||
self->last_error_ = AllocError(err,
|
||||
src_fn,
|
||||
error_count_,
|
||||
target_z,
|
||||
committed_z_,
|
||||
reserved());
|
||||
}
|
||||
|
||||
byte *
|
||||
DArena::_alloc(std::size_t req_z,
|
||||
alloc_mode mode,
|
||||
typeseq tseq,
|
||||
uint32_t age,
|
||||
const char * src_fn)
|
||||
{
|
||||
scope log(XO_DEBUG(config_.debug_flag_));
|
||||
|
||||
/*
|
||||
* sub_complete
|
||||
* sub_incomplete |
|
||||
* standard super | |
|
||||
* v v v v
|
||||
*/
|
||||
std::array<bool, 4> store_header_v = {{ true, true, false, false }};
|
||||
std::array<bool, 4> retain_header_v = {{ false, true, false, false }};
|
||||
std::array<bool, 4> store_guard_v = {{ true, false, false, true }};
|
||||
|
||||
/* -> write header at free_ */
|
||||
bool store_header_flag = false;
|
||||
/* -> stash last_header_*/
|
||||
bool retain_header_flag = false;
|
||||
/* -> write guard bytes */
|
||||
bool store_guard = false;
|
||||
|
||||
if (config_.store_header_flag_) {
|
||||
store_header_flag = store_header_v[(int)mode];
|
||||
retain_header_flag = retain_header_v[(int)mode];
|
||||
store_guard = store_guard_v[(int)mode];
|
||||
}
|
||||
|
||||
assert(padding::is_aligned((size_t)free_));
|
||||
|
||||
/*
|
||||
* free_(pre)
|
||||
* v
|
||||
*
|
||||
* <-------------z1--------------->
|
||||
* < guard >< hz >< req_z >< dz >< guard >
|
||||
*
|
||||
* used <== +++++++++0000zzzz@@@@@@@@@@@@@@@@@ppppppp+++++++++ ==> avail
|
||||
*
|
||||
* ^ ^ ^
|
||||
* header mem |
|
||||
* ^ |
|
||||
* last_header_ free_(post)
|
||||
*
|
||||
* [+] guard after each allocation, for simple sanitize checks
|
||||
* [0] unused header bits (avail to application)
|
||||
* [z] record allocation size
|
||||
* [@] new allocated memory
|
||||
* [p] padding (to uintptr_t alignment)
|
||||
*/
|
||||
|
||||
/* non-zero if header feature enabled */
|
||||
size_t hz = 0;
|
||||
/* dz: pad req_z to alignment size (multiple of 8 bytes, probably) */
|
||||
size_t dz = padding::alloc_padding(req_z);
|
||||
size_t z0 = req_z + dz;
|
||||
/* if non-zero:
|
||||
* will store padded alloc size at the beginning of each allocation
|
||||
* reminder:
|
||||
* important to store padded size for correct arena iteration
|
||||
*/
|
||||
uint64_t header = (req_z + dz);
|
||||
|
||||
if (store_header_flag)
|
||||
{
|
||||
if (config_.header_.is_size_enabled()) [[likely]] {
|
||||
header = this->config_.header_.mkheader(tseq.seqno(), age, req_z + dz);
|
||||
hz = sizeof(header);
|
||||
} else {
|
||||
/* req_z doesn't fit in configured header_size_mask bits */
|
||||
capture_error(error::header_size_mask, src_fn);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
size_t z1 = hz + z0;
|
||||
|
||||
assert(padding::is_aligned(z1));
|
||||
|
||||
if (!this->expand(this->allocated() + z1, src_fn)) [[unlikely]] {
|
||||
/* (error state already captured) */
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (store_header_flag) {
|
||||
/* capturing header */
|
||||
*(uint64_t *)free_ = header;
|
||||
|
||||
if (retain_header_flag) {
|
||||
/* and rembering for subsequent
|
||||
* sub_alloc()
|
||||
*/
|
||||
last_header_ = (AllocHeader *)free_;
|
||||
}
|
||||
}
|
||||
|
||||
byte * mem = free_ + hz;
|
||||
|
||||
this->free_ += z1;
|
||||
|
||||
if (store_guard) {
|
||||
/* write guard bytes for overrun detection */
|
||||
::memset(free_,
|
||||
config_.header_.guard_byte_,
|
||||
config_.header_.guard_z_);
|
||||
|
||||
this->free_ += config_.header_.guard_z_;
|
||||
}
|
||||
|
||||
log && log(xtag("self", config_.name_),
|
||||
xtag("hz", hz),
|
||||
xtag("z0", req_z),
|
||||
xtag("+pad", dz),
|
||||
xtag("z1", z1),
|
||||
xtag("size", this->committed()),
|
||||
xtag("avail", this->available()));
|
||||
log && log(xtag("mem", mem),
|
||||
xtag("free", free_));
|
||||
|
||||
return mem;
|
||||
}
|
||||
|
||||
void
|
||||
DArena::establish_initial_guard() noexcept
|
||||
{
|
||||
assert(free_ == lo_);
|
||||
|
||||
::memset(this->free_,
|
||||
config_.header_.guard_byte_,
|
||||
config_.header_.guard_z_);
|
||||
|
||||
this->free_ += config_.header_.guard_z_;
|
||||
}
|
||||
|
||||
bool
|
||||
DArena::expand(size_t target_z, const char * src_fn) noexcept
|
||||
{
|
||||
scope log(XO_DEBUG(config_.debug_flag_),
|
||||
xtag("target_z", target_z),
|
||||
xtag("committed_z", committed_z_),
|
||||
xtag("src_fn", src_fn));
|
||||
|
||||
if (target_z <= committed_z_) [[likely]] {
|
||||
log && log("trivial success, offset within committed range",
|
||||
xtag("target_z", target_z),
|
||||
xtag("committed_z", committed_z_));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (lo_ + target_z > hi_) [[unlikely]] {
|
||||
this->capture_error(error::reserve_exhausted, src_fn, target_z);
|
||||
|
||||
//fprintf(stderr, "DArena::expand: reserve exhausted");
|
||||
//print_backtrace_dwarf(true /*demangle_flag*/);
|
||||
//std::terminate();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* pre:
|
||||
*
|
||||
* _______________...................................
|
||||
* ^ ^ ^
|
||||
* lo limit hi
|
||||
*
|
||||
* < committed_z >
|
||||
* <----------target_z----------->
|
||||
* > <- z: 0 <= z < hugepage_z
|
||||
* <---------aligned_target_z--------->
|
||||
* <--- add_commit_z -->
|
||||
*
|
||||
* post:
|
||||
* ____________________________________..............
|
||||
* ^ ^ ^
|
||||
* lo limit hi
|
||||
*
|
||||
*/
|
||||
|
||||
std::size_t aligned_target_z = padding::with_padding(target_z, arena_align_z_);
|
||||
std::byte * commit_start = limit_; // = lo_ + committed_z_;
|
||||
std::size_t add_commit_z = aligned_target_z - committed_z_;
|
||||
|
||||
assert(limit_ == lo_ + committed_z_);
|
||||
|
||||
if (::mprotect(commit_start,
|
||||
add_commit_z,
|
||||
PROT_READ | PROT_WRITE) != 0) [[unlikely]]
|
||||
{
|
||||
if (log) {
|
||||
log("commit failed!");
|
||||
log(xtag("aligned_target_z", aligned_target_z),
|
||||
xtag("commit_start", commit_start),
|
||||
xtag("add_commit_z", add_commit_z),
|
||||
xtag("commit_end", commit_start + add_commit_z)
|
||||
);
|
||||
}
|
||||
|
||||
this->capture_error(error::commit_failed, src_fn, add_commit_z);
|
||||
|
||||
fprintf(stderr, "DArena::expand: mprotect failed (system oom?)");
|
||||
print_backtrace_dwarf(false /*!demangle_flag*/);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
committed_z_ = aligned_target_z;
|
||||
limit_ = lo_ + committed_z_;
|
||||
|
||||
if (commit_start == lo_) [[unlikely]] {
|
||||
/* first expand() for this allocator - start with guard_z_ bytes */
|
||||
|
||||
this->establish_initial_guard();
|
||||
}
|
||||
|
||||
assert(committed_z_ % arena_align_z_ == 0);
|
||||
assert(reinterpret_cast<size_t>(limit_) % arena_align_z_ == 0);
|
||||
|
||||
return true;
|
||||
} /*expand*/
|
||||
|
||||
void
|
||||
DArena::scrub() noexcept
|
||||
{
|
||||
::memset(this->lo_, 0, this->free_ - this->lo_);
|
||||
}
|
||||
|
||||
void
|
||||
DArena::clear() noexcept
|
||||
{
|
||||
this->free_ = lo_;
|
||||
this->establish_initial_guard();
|
||||
}
|
||||
|
||||
void
|
||||
DArena::swap(DArena & other) noexcept
|
||||
{
|
||||
std::swap(config_, other.config_);
|
||||
std::swap(page_z_, other.page_z_);
|
||||
std::swap(arena_align_z_, other.arena_align_z_);
|
||||
std::swap(lo_, other.lo_);
|
||||
std::swap(committed_z_, other.committed_z_);
|
||||
std::swap(last_header_, other.last_header_);
|
||||
std::swap(free_, other.free_);
|
||||
std::swap(limit_, other.limit_);
|
||||
std::swap(hi_, other.hi_);
|
||||
std::swap(error_count_, other.error_count_);
|
||||
std::swap(last_error_, other.last_error_);
|
||||
}
|
||||
}
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end DArena.cpp */
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
/** @file DArenaIterator.cpp
|
||||
*
|
||||
* @author Roland Conybeare, Dec 2025
|
||||
**/
|
||||
|
||||
#include "DArenaIterator.hpp"
|
||||
#include "DArena.hpp"
|
||||
#include <xo/indentlog/scope.hpp>
|
||||
#include <xo/indentlog/print/tag.hpp>
|
||||
#include <cassert>
|
||||
|
||||
namespace xo {
|
||||
using std::byte;
|
||||
|
||||
namespace mm {
|
||||
DArenaIterator
|
||||
DArenaIterator::begin(const DArena * arena)
|
||||
{
|
||||
constexpr bool c_debug_flag = false;
|
||||
scope log(XO_DEBUG(c_debug_flag));
|
||||
|
||||
AllocHeader * begin_hdr = begin_header(arena);
|
||||
|
||||
if (!begin_hdr)
|
||||
return DArenaIterator::invalid();
|
||||
|
||||
log && log(xtag("begin_hdr", begin_hdr));
|
||||
|
||||
return DArenaIterator(arena, begin_hdr);
|
||||
}
|
||||
|
||||
DArenaIterator
|
||||
DArenaIterator::end(const DArena * arena)
|
||||
{
|
||||
constexpr bool c_debug_flag = false;
|
||||
scope log(XO_DEBUG(c_debug_flag));
|
||||
|
||||
AllocHeader * end_hdr = end_header(arena);
|
||||
|
||||
if (!end_hdr)
|
||||
return DArenaIterator::invalid();
|
||||
|
||||
log && log(xtag("end_hdr", end_hdr));
|
||||
|
||||
return DArenaIterator(arena, end_hdr);
|
||||
}
|
||||
|
||||
AllocHeader *
|
||||
DArenaIterator::begin_header(const DArena * arena)
|
||||
{
|
||||
assert(arena);
|
||||
|
||||
return arena->begin_header();
|
||||
}
|
||||
|
||||
AllocHeader *
|
||||
DArenaIterator::end_header(const DArena * arena)
|
||||
{
|
||||
assert(arena);
|
||||
|
||||
return arena->end_header();
|
||||
}
|
||||
|
||||
AllocInfo
|
||||
DArenaIterator::deref() const noexcept
|
||||
{
|
||||
constexpr bool c_debug_flag = false;
|
||||
scope log(XO_DEBUG(c_debug_flag));
|
||||
|
||||
bool contains_flag = arena_->contains(this->pos_as_byte());
|
||||
bool bounds_flag = (this->pos_as_byte() < arena_->free_);
|
||||
|
||||
log && log(xtag("contains_flag", contains_flag),
|
||||
xtag("bounds_flag", bounds_flag));
|
||||
|
||||
if (!contains_flag || !bounds_flag) {
|
||||
arena_->capture_error(error::alloc_iterator_deref, __PRETTY_FUNCTION__);
|
||||
|
||||
return AllocInfo::error_invalid_iterator(&(arena_->config_.header_));
|
||||
}
|
||||
|
||||
/* iterator points to beginning of header.
|
||||
* memory given to application start immediately followed header
|
||||
*/
|
||||
|
||||
byte * mem = (byte *)(pos_ + 1);
|
||||
|
||||
return arena_->alloc_info(mem);
|
||||
}
|
||||
|
||||
cmpresult
|
||||
DArenaIterator::compare(const DArenaIterator & other_ix) const noexcept
|
||||
{
|
||||
scope log(XO_DEBUG(false),
|
||||
xtag("arena", arena_),
|
||||
xtag("pos", pos_),
|
||||
xtag("other.arena", other_ix.arena_),
|
||||
xtag("other.pos", other_ix.pos_));
|
||||
|
||||
if (is_invalid() || (arena_ != other_ix.arena_))
|
||||
return cmpresult::incomparable();
|
||||
|
||||
if (pos_ < other_ix.pos_) {
|
||||
return cmpresult::lesser();
|
||||
} else if(pos_ == other_ix.pos_) {
|
||||
return cmpresult::equal();
|
||||
} else {
|
||||
return cmpresult::greater();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
DArenaIterator::next() noexcept
|
||||
{
|
||||
constexpr bool c_debug_flag = false;
|
||||
scope log(XO_DEBUG(c_debug_flag));
|
||||
|
||||
bool contains_flag = arena_->contains(this->pos_as_byte());
|
||||
bool bounds_flag = (this->pos_as_byte() < arena_->free_);
|
||||
|
||||
log && log(xtag("contains_flag", contains_flag),
|
||||
xtag("bounds_flag", bounds_flag));
|
||||
|
||||
if (!contains_flag || !bounds_flag) {
|
||||
arena_->capture_error(error::alloc_iterator_next, __PRETTY_FUNCTION__);
|
||||
return;
|
||||
}
|
||||
|
||||
size_t mem_z = arena_->config_.header_.size(*pos_);
|
||||
size_t guard_z = arena_->config_.header_.guard_z_;
|
||||
|
||||
byte * next_as_byte = ((byte *)pos_ + sizeof(AllocHeader) + mem_z + guard_z);
|
||||
/* next == ix.arena_free_ --> iterator is at end of allocator */
|
||||
assert(next_as_byte <= arena_->free_);
|
||||
|
||||
AllocHeader * next = (AllocHeader *)next_as_byte;
|
||||
|
||||
this->pos_ = next;
|
||||
}
|
||||
|
||||
} /*namespace mm*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end DArenaIterator.cpp */
|
||||
|
|
@ -1,394 +0,0 @@
|
|||
/** @file DCircularBuffer.cpp
|
||||
*
|
||||
* @author Roland Conybeare, Jan 2026
|
||||
**/
|
||||
|
||||
#include "DCircularBuffer.hpp"
|
||||
#include "mmap_util.hpp"
|
||||
#include <xo/indentlog/scope.hpp>
|
||||
#include <xo/indentlog/print/tostr.hpp>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h> // for ::getpagesize() on osx
|
||||
|
||||
namespace xo {
|
||||
using xo::print::operator<<;
|
||||
using xo::print::printspan;
|
||||
|
||||
namespace mm {
|
||||
|
||||
DCircularBuffer::DCircularBuffer(DCircularBuffer && other)
|
||||
: config_{other.config_},
|
||||
page_z_{other.page_z_},
|
||||
buffer_align_z_{other.buffer_align_z_},
|
||||
reserved_range_{other.reserved_range_},
|
||||
mapped_range_{other.mapped_range_},
|
||||
occupied_range_{other.occupied_range_},
|
||||
pinned_spans_{std::move(other.pinned_spans_)}
|
||||
{
|
||||
other.reserved_range_ = span_type();
|
||||
other.mapped_range_ = span_type();
|
||||
other.occupied_range_ = span_type();
|
||||
}
|
||||
|
||||
DCircularBuffer
|
||||
DCircularBuffer::map(const CircularBufferConfig & config)
|
||||
{
|
||||
scope log(XO_DEBUG(config.debug_flag_));
|
||||
|
||||
/* vm page size. 4KB (probably if linux) or 16KB (probably if osx) */
|
||||
size_t page_z = getpagesize();
|
||||
|
||||
bool enable_hugepage_flag = (config.max_capacity_ >= config.hugepage_z_);
|
||||
|
||||
/* Align start of arena memory on this boundary.
|
||||
* Will use THP (transparent huge pages) if available
|
||||
* and arena size is at least as large as hugepage size (2MB, probably)
|
||||
*/
|
||||
size_t align_z = (enable_hugepage_flag ? config.hugepage_z_ : page_z);
|
||||
|
||||
log && log(xtag("page_z", page_z),
|
||||
xtag("align_z", align_z));
|
||||
|
||||
auto mapped_span
|
||||
= span<char>::from_memory(mmap_util::map_aligned_range
|
||||
(config.max_capacity_,
|
||||
align_z,
|
||||
enable_hugepage_flag,
|
||||
config.debug_flag_));
|
||||
|
||||
if (!mapped_span.lo()) {
|
||||
throw std::runtime_error(tostr("DCircularBuffer: reserve address range failed",
|
||||
xtag("size", config.max_capacity_)));
|
||||
}
|
||||
|
||||
return DCircularBuffer(config, page_z, align_z, mapped_span);
|
||||
}
|
||||
|
||||
DCircularBuffer::DCircularBuffer(const CircularBufferConfig & config,
|
||||
size_type page_z,
|
||||
size_type buffer_align_z,
|
||||
span_type reserved_range)
|
||||
: config_{config},
|
||||
page_z_{page_z},
|
||||
buffer_align_z_{buffer_align_z},
|
||||
reserved_range_{reserved_range},
|
||||
mapped_range_{reserved_range_.prefix(0)},
|
||||
occupied_range_{mapped_range_.prefix(0)},
|
||||
input_range_{occupied_range_.prefix(0)},
|
||||
pinned_spans_{DArenaVector<span_type>::map(ArenaConfig().with_name(config.name_ + "-pins"))}
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
DCircularBuffer::visit_pools(const MemorySizeVisitor & visitor) const
|
||||
{
|
||||
visitor(MemorySizeInfo(config_.name_,
|
||||
occupied_range_.size() /*used*/,
|
||||
occupied_range_.size(),
|
||||
mapped_range_.size(),
|
||||
reserved_range_.size(),
|
||||
reserved_range_.lo(),
|
||||
reserved_range_.hi(),
|
||||
nullptr /*detail*/));
|
||||
|
||||
pinned_spans_.visit_pools(visitor);
|
||||
}
|
||||
|
||||
bool
|
||||
DCircularBuffer::verify_ok(verify_policy policy) const
|
||||
{
|
||||
using xo::xtag;
|
||||
|
||||
constexpr const char * c_self = "DCircularBuffer::verify_ok";
|
||||
scope log(XO_DEBUG(false));
|
||||
|
||||
/* CB1: mapped_range_ is subrange of reserved_range_ */
|
||||
if ((mapped_range_.lo() < reserved_range_.lo())
|
||||
|| (mapped_range_.hi() > reserved_range_.hi()))
|
||||
{
|
||||
return policy.report_error(log,
|
||||
c_self, ": expect mapped_range subset of reserved_range",
|
||||
xtag("mapped.lo", (void*)mapped_range_.lo()),
|
||||
xtag("mapped.hi", (void*)mapped_range_.hi()),
|
||||
xtag("reserved.lo", (void*)reserved_range_.lo()),
|
||||
xtag("reserved.hi", (void*)reserved_range_.hi()));
|
||||
}
|
||||
|
||||
/* CB2: occupied_range_ is subrange of mapped_range_ */
|
||||
if ((occupied_range_.lo() < mapped_range_.lo())
|
||||
|| (occupied_range_.hi() > mapped_range_.hi()))
|
||||
{
|
||||
return policy.report_error(log,
|
||||
c_self, ": expect occupied_range subset of mapped_range",
|
||||
xtag("occupied.lo", (void*)occupied_range_.lo()),
|
||||
xtag("occupied.hi", (void*)occupied_range_.hi()),
|
||||
xtag("mapped.lo", (void*)mapped_range_.lo()),
|
||||
xtag("mapped.hi", (void*)mapped_range_.hi()));
|
||||
}
|
||||
|
||||
/* CB3: each remembered span is subrange of occupied_range_ */
|
||||
for (size_type i = 0, n = pinned_spans_.size(); i < n; ++i) {
|
||||
const const_span_type & pin = pinned_spans_[i];
|
||||
|
||||
if ((pin.lo() < occupied_range_.lo())
|
||||
|| (pin.hi() > occupied_range_.hi()))
|
||||
{
|
||||
return policy.report_error(log,
|
||||
c_self, ": expect remembered_span subset of occupied_range",
|
||||
xtag("i", i),
|
||||
xtag("pin.lo", (void*)pin.lo()),
|
||||
xtag("pin.hi", (void*)pin.hi()),
|
||||
xtag("occupied.lo", (void*)occupied_range_.lo()),
|
||||
xtag("occupied.hi", (void*)occupied_range_.hi()));
|
||||
}
|
||||
}
|
||||
|
||||
/* CB4: buffer_align_z_ is non-zero (when buffer is mapped) */
|
||||
if (!reserved_range_.is_null() && (buffer_align_z_ == 0)) {
|
||||
return policy.report_error(log,
|
||||
c_self, ": expect buffer_align_z > 0 when buffer is mapped",
|
||||
xtag("buffer_align_z", buffer_align_z_));
|
||||
}
|
||||
|
||||
/* CB5: reserved_range_ aligned on buffer_align_z_ boundary */
|
||||
if (!reserved_range_.is_null() && (buffer_align_z_ > 0)) {
|
||||
if (((size_type)(reserved_range_.lo()) % buffer_align_z_) != 0) {
|
||||
return policy.report_error(log,
|
||||
c_self, ": expect reserved_range.lo aligned on buffer_align_z",
|
||||
xtag("reserved.lo", (void*)reserved_range_.lo()),
|
||||
xtag("buffer_align_z", buffer_align_z_));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
auto
|
||||
DCircularBuffer::append(const_span_type src) -> const_span_type
|
||||
{
|
||||
span_type dest = get_append_span(src.size());
|
||||
|
||||
size_t copy_z = std::min(src.size(), dest.size());
|
||||
|
||||
::memcpy(occupied_range_.hi(), src.lo(), copy_z);
|
||||
|
||||
this->occupied_range_ += span_type(dest.lo(), copy_z);
|
||||
this->input_range_ += span_type(dest.lo(), copy_z);
|
||||
|
||||
return src.after_prefix(copy_z);
|
||||
}
|
||||
|
||||
auto
|
||||
DCircularBuffer::get_append_span(size_type desired_z) -> span_type
|
||||
{
|
||||
span_type dest = span_type(occupied_range_.hi(), desired_z);
|
||||
|
||||
if (dest.hi() > reserved_range_.hi()) {
|
||||
/* under no circumstances go past the end of reserved range */
|
||||
dest = span_type(dest.lo(), reserved_range_.hi());
|
||||
}
|
||||
|
||||
/* establish mapped range at least to dest.hi */
|
||||
this->_expand_to(dest.hi());
|
||||
|
||||
/* report available memory */
|
||||
return span_type(occupied_range_.hi(), mapped_range_.hi());
|
||||
}
|
||||
|
||||
void
|
||||
DCircularBuffer::report_append(span_type r)
|
||||
{
|
||||
if (r.lo() != occupied_range_.hi()) {
|
||||
// error!
|
||||
|
||||
// this->capture_error(error::bad_append_report, r.size())
|
||||
assert(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (r.hi() > mapped_range_.hi()) {
|
||||
// error!
|
||||
|
||||
// this->capture_error(error::bad_append_report, r.size())
|
||||
assert(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this->occupied_range_ += r;
|
||||
}
|
||||
|
||||
void
|
||||
DCircularBuffer::consume(const_span_type input)
|
||||
{
|
||||
scope log(XO_DEBUG(false), xtag("input", input.to_string_view()));
|
||||
|
||||
if (input.lo() != input_range_.lo()) {
|
||||
assert(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (input.hi() > occupied_range_.hi()) {
|
||||
assert(false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (occupied_range_.lo() < input_range_.lo()) {
|
||||
log && log("pinned range prevents shrinking occupied range");
|
||||
|
||||
/* here: a pinned range prevents shrinking occupied_range */
|
||||
|
||||
this->input_range_
|
||||
= input_range_.suffix_from((span_type::value_type *)input.hi());
|
||||
} else {
|
||||
log && log(xtag("msg", "will shrink occupied range"),
|
||||
xtag("input.lo", (void*)input.lo()),
|
||||
xtag("input.hi", (void*)input.hi()),
|
||||
xtag("stored.lo", (void*)input_range_.lo()),
|
||||
xtag("stored.hi", (void*)input_range_.hi())
|
||||
);
|
||||
|
||||
/* here: input; recompute occupied boundary */
|
||||
|
||||
this->input_range_
|
||||
= input_range_.suffix_from((span_type::value_type *)input.hi());
|
||||
|
||||
log && log(xtag("occupied", occupied_range_.size()),
|
||||
xtag("input", input_range_.size()));
|
||||
|
||||
this->_shrink_occupied_to_fit();
|
||||
|
||||
log && log(xtag("occupied", occupied_range_.size()),
|
||||
xtag("input", input_range_.size()));
|
||||
}
|
||||
|
||||
this->_check_reset_map_start();
|
||||
}
|
||||
|
||||
void
|
||||
DCircularBuffer::pin_range(span_type r)
|
||||
{
|
||||
// loop optimized for case where r falls
|
||||
// _after_ any existing pinned ranges
|
||||
|
||||
size_type z = pinned_spans_.size();
|
||||
size_type ip1 = z; // ip1 = i + 1
|
||||
|
||||
for (; ip1 > 0; --ip1) {
|
||||
if (r.lo() > pinned_spans_[ip1 - 1].lo())
|
||||
break;
|
||||
|
||||
// insert at i to maintain sorted order
|
||||
pinned_spans_.insert(ip1 - 1, r);
|
||||
return;
|
||||
}
|
||||
|
||||
pinned_spans_.push_back(r);
|
||||
}
|
||||
|
||||
void
|
||||
DCircularBuffer::unpin_range(span_type r)
|
||||
{
|
||||
// loop optimized for case where r
|
||||
// is the first pinned range
|
||||
|
||||
assert(pinned_spans_.size() > 0);
|
||||
|
||||
if (r == pinned_spans_[0]) {
|
||||
this->pinned_spans_.erase(0);
|
||||
|
||||
/* removing pinned span means can perhaps shrink
|
||||
* occupied range
|
||||
*/
|
||||
this->_shrink_occupied_to_fit();
|
||||
this->_check_reset_map_start();
|
||||
} else {
|
||||
for (size_type i = 1; i < pinned_spans_.size(); ++i) {
|
||||
if (r == pinned_spans_[i]) {
|
||||
this->pinned_spans_.erase(i);
|
||||
|
||||
/* since this isn't the first pinned span,
|
||||
* won't be able to shrink occupied range.
|
||||
*/
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
DCircularBuffer::_expand_to(char * hi)
|
||||
{
|
||||
scope log(XO_DEBUG(config_.debug_flag_));
|
||||
|
||||
if (hi < mapped_range_.hi()) {
|
||||
/* nothing todo */
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t add_z = hi - mapped_range_.hi();
|
||||
size_t add_commit_z = padding::with_padding(add_z, buffer_align_z_);
|
||||
char * commit_start = mapped_range_.hi();
|
||||
|
||||
if (::mprotect(commit_start,
|
||||
add_commit_z,
|
||||
PROT_READ | PROT_WRITE) != 0)
|
||||
{
|
||||
if (log) {
|
||||
log("commit failed");
|
||||
log(xtag("commit_start", commit_start),
|
||||
xtag("add_z", add_z),
|
||||
xtag("add_commit_z", add_commit_z));
|
||||
}
|
||||
|
||||
// this->capture_error(error::commit_failed, add_commit_z);
|
||||
return false;
|
||||
}
|
||||
|
||||
this->mapped_range_ += span(commit_start, add_commit_z);
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
DCircularBuffer::_shrink_occupied_to_fit()
|
||||
{
|
||||
if (pinned_spans_.empty()) {
|
||||
this->occupied_range_ = input_range_;
|
||||
} else if (occupied_range_.lo() < pinned_spans_[0].lo()) {
|
||||
this->occupied_range_ = occupied_range_.suffix_from(pinned_spans_[0].lo());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
DCircularBuffer::_check_reset_map_start()
|
||||
{
|
||||
if (pinned_spans_.empty()
|
||||
&& (input_range_ == occupied_range_)) {
|
||||
|
||||
// here: permissible to move input range to the beginning of mapped range.
|
||||
// decide (heuristically) whether we think this is optimal
|
||||
|
||||
std::size_t input_z = input_range_.size();
|
||||
|
||||
// 1st clause checks efficiency.
|
||||
// 2nd clause (probably redundant) check non-overlapping
|
||||
if ((input_range_.lo() > (mapped_range_.lo()
|
||||
+ std::max(page_z_,
|
||||
static_cast<size_type>(config_.threshold_move_efficiency_ * input_z))))
|
||||
&& (mapped_range_.lo() + input_z < input_range_.lo())) {
|
||||
|
||||
::memmove(mapped_range_.lo(), input_range_.lo(), input_z);
|
||||
|
||||
this->occupied_range_ = mapped_range_.prefix(input_z);
|
||||
this->input_range_ = mapped_range_.prefix(input_z);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} /*namespace mm*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end DCircularBuffer.cpp */
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
/** @file ErrorArena.cpp
|
||||
*
|
||||
* @author Roland Conybeare, Feb 2026
|
||||
**/
|
||||
|
||||
#include "ErrorArena.hpp"
|
||||
|
||||
namespace xo {
|
||||
namespace mm {
|
||||
DArena
|
||||
ErrorArena::s_instance;
|
||||
|
||||
ArenaConfig
|
||||
ErrorArena::default_config()
|
||||
{
|
||||
return ArenaConfig().with_name("error-arena").with_size(16 * 1024);
|
||||
}
|
||||
|
||||
namespace {
|
||||
bool s_init_done = false;
|
||||
}
|
||||
|
||||
void
|
||||
ErrorArena::init_once(const ArenaConfig & cfg)
|
||||
{
|
||||
if (!s_init_done) {
|
||||
s_init_done = true;
|
||||
s_instance = DArena::map(cfg);
|
||||
}
|
||||
}
|
||||
|
||||
DArena *
|
||||
ErrorArena::instance()
|
||||
{
|
||||
init_once(default_config());
|
||||
|
||||
return &s_instance;
|
||||
}
|
||||
|
||||
} /*namespace mm*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end ErrorArena.cpp */
|
||||
|
|
@ -1,214 +0,0 @@
|
|||
/** @file arena_streambuf.cpp
|
||||
*
|
||||
* @author Roland Conybeare, Feb 2026
|
||||
**/
|
||||
|
||||
#include "arena_streambuf.hpp"
|
||||
|
||||
namespace xo {
|
||||
namespace mm {
|
||||
|
||||
std::uint32_t
|
||||
arena_streambuf::lpos() const
|
||||
{
|
||||
if (debug_flag_) {
|
||||
std::cerr << "log_streambuf::lpos: enter" << std::endl;
|
||||
}
|
||||
|
||||
// logically-const. lazy implementation
|
||||
arena_streambuf * self = const_cast<arena_streambuf *>(this);
|
||||
|
||||
self->_check_update_local_state();
|
||||
|
||||
return pos() - solpos_ - color_escape_chars_;
|
||||
}
|
||||
|
||||
auto
|
||||
arena_streambuf::checkpoint() const -> rewind_state
|
||||
{
|
||||
// logically-const. lazy implementation
|
||||
arena_streambuf * self = const_cast<arena_streambuf *>(this);
|
||||
|
||||
self->_check_update_local_state();
|
||||
|
||||
return rewind_state(solpos_, color_escape_chars_, pos());
|
||||
}
|
||||
|
||||
void
|
||||
arena_streambuf::reset_stream()
|
||||
{
|
||||
assert(arena_);
|
||||
assert(arena_->committed() > 0);
|
||||
|
||||
char * p_lo = (char *)(arena_->lo_);
|
||||
char * p_hi = (char *)(arena_->limit_);
|
||||
|
||||
/* tells parent our buffer extent */
|
||||
this->setp(p_lo, p_hi);
|
||||
|
||||
this->local_ppos_ = 0;
|
||||
this->solpos_ = 0;
|
||||
this->color_escape_chars_ = 0;
|
||||
this->color_escape_start_ = nullptr;
|
||||
}
|
||||
|
||||
void
|
||||
arena_streambuf::rewind_to(rewind_state s)
|
||||
{
|
||||
if (debug_flag_) {
|
||||
std::cout << "rewind_to: pos " << pos() << "->" << s.pos
|
||||
<< " solpos " << solpos_ << "->" << s.solpos
|
||||
<< " color_esc " << color_escape_chars_ << "->" << s.color_escape_chars
|
||||
<< std::endl;
|
||||
}
|
||||
|
||||
/* .setp(): using just for side effect: sets .pptr to .pbase */
|
||||
this->setp(this->pbase(), this->epptr());
|
||||
/* advance pptr to saved position */
|
||||
this->pbump(s.pos);
|
||||
|
||||
this->local_ppos_ = this->pptr() - this->pbase();
|
||||
this->solpos_ = s.solpos;
|
||||
this->color_escape_chars_ = s.color_escape_chars;
|
||||
/* assuming we never try to capture rewind state with incomplete color escape */
|
||||
this->color_escape_start_ = nullptr;
|
||||
}
|
||||
|
||||
void
|
||||
arena_streambuf::expand_to(std::size_t new_z)
|
||||
{
|
||||
char * old_pptr = pptr();
|
||||
std::streamsize old_n = old_pptr - pbase();
|
||||
|
||||
assert(old_n <= static_cast<std::streamsize>(arena_->allocated()));
|
||||
assert(new_z > arena_->committed());
|
||||
|
||||
/* note: local_ppos_ invariant across expand_to() */
|
||||
|
||||
arena_->expand(new_z, __PRETTY_FUNCTION__);
|
||||
|
||||
char * p_base = (char *)(arena_->lo_);
|
||||
char * p_hi = (char *)(arena_->limit_);
|
||||
|
||||
this->setp(p_base, p_hi);
|
||||
this->pbump(old_n);
|
||||
}
|
||||
|
||||
std::streamsize
|
||||
arena_streambuf::xsputn(const char * s, std::streamsize n)
|
||||
{
|
||||
/* s must be an address in [this->lo() .. this->lo() + capacity()] */
|
||||
|
||||
assert(hi() >= pptr());
|
||||
|
||||
if (pptr() + n > hi()) {
|
||||
std::size_t new_z = std::max(2 * arena_->committed(), std::size_t(this->pos() + n + 1));
|
||||
|
||||
if (new_z > arena_->reserved())
|
||||
new_z = arena_->reserved();
|
||||
|
||||
this->expand_to(new_z);
|
||||
}
|
||||
|
||||
if (debug_flag_) {
|
||||
std::cout << "xsputn: pbase=" << (void *)(this->pbase())
|
||||
<< ", pptr=" << (void*)(this->pptr())
|
||||
<< "(+" << (this->pptr() - this->lo()) << ")"
|
||||
<< ", n=" << n << " -> (+" << (this->pptr() + n - this->lo()) << ")"
|
||||
<< ", arena.size=" << this->arena_->committed()
|
||||
<< std::endl;
|
||||
}
|
||||
|
||||
std::streamsize ncopied = 0;
|
||||
|
||||
if (this->pptr() + n > this->hi()) {
|
||||
ncopied = this->hi() - this->pptr();
|
||||
} else {
|
||||
ncopied = n;
|
||||
}
|
||||
|
||||
if (false /*debug_flag_*/) {
|
||||
std::cout << "xsputn: copying ncopied=" << ncopied << " (/n=" << n << ") bytes into range [lo,hi)"
|
||||
<< ", lo=" << (void*)this->pptr()
|
||||
<< ", hi=" << (void*)(this->pptr() + n)
|
||||
<< std::endl;
|
||||
}
|
||||
|
||||
std::memcpy(this->pptr(), s, ncopied);
|
||||
|
||||
this->pbump(ncopied);
|
||||
|
||||
/* now {pbase, pptr} consistent with new input */
|
||||
|
||||
this->_check_update_local_state();
|
||||
|
||||
return ncopied;
|
||||
}
|
||||
|
||||
auto
|
||||
arena_streambuf::overflow(int_type new_ch) -> int_type
|
||||
{
|
||||
char * old_base = this->pbase();
|
||||
char * old_pptr = this->pptr();
|
||||
/* #of chars buffered */
|
||||
std::streamsize old_n = old_pptr - old_base;
|
||||
|
||||
assert(old_n <= static_cast<std::streamsize>(arena_->committed()));
|
||||
|
||||
// if (debug_flag_) {
|
||||
// std::cout << "overflow: new_ch=" << quoted_char(new_ch) << std::endl;
|
||||
// }
|
||||
|
||||
/* increase buffer size */
|
||||
this->expand_to(2 * arena_->committed());
|
||||
|
||||
arena_->lo_[old_n] = static_cast<std::byte>(new_ch);
|
||||
this->pbump(1);
|
||||
|
||||
if ((new_ch == static_cast<int_type>('\n')) || (new_ch == static_cast<int_type>('\r'))) {
|
||||
this->solpos_ = this->pos();
|
||||
|
||||
// what if new_ch starts color escape ?
|
||||
}
|
||||
|
||||
if (new_ch == std::char_traits<char>::eof()) {
|
||||
/* reminder: returning eof sets badbit on ostream */
|
||||
return std::char_traits<char>::not_eof(new_ch);
|
||||
} else {
|
||||
return new_ch;
|
||||
}
|
||||
}
|
||||
|
||||
auto
|
||||
arena_streambuf::seekoff(off_type off,
|
||||
std::ios_base::seekdir dir,
|
||||
std::ios_base::openmode which) -> pos_type
|
||||
{
|
||||
//std::cout << "seekoff: off=" << off << ", dir=" << dir << ", which=" << which << std::endl;
|
||||
if (debug_flag_) {
|
||||
std::cout << "seekoff(off,dir,which)" << std::endl;
|
||||
}
|
||||
|
||||
// Only output stream is supported
|
||||
if (which != std::ios_base::out)
|
||||
throw std::runtime_error("log_streambuf: only output mode supported");
|
||||
|
||||
if (dir == std::ios_base::cur) {
|
||||
this->pbump(off);
|
||||
} else if (dir == std::ios_base::end) {
|
||||
/* .setp(): using for side effect: sets .pptr to .pbase */
|
||||
this->setp(this->pbase(), this->epptr());
|
||||
this->pbump(off);
|
||||
} else if (dir == std::ios_base::beg) {
|
||||
/* .setp(): using for side effect: sets .pptr to .pbase */
|
||||
this->setp(this->pbase(), this->epptr());
|
||||
this->pbump(this->capacity() + off);
|
||||
}
|
||||
|
||||
return this->pptr() - this->pbase();
|
||||
} /*seekoff*/
|
||||
|
||||
} /*namespace mm*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end arena_streambuf.cpp */
|
||||
|
|
@ -1,172 +0,0 @@
|
|||
/** @file backtrace.cpp
|
||||
*
|
||||
* @author Roland Conybeare, Apr 2026
|
||||
**/
|
||||
|
||||
#include "backtrace.hpp"
|
||||
#include <iostream>
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <libunwind.h>
|
||||
#include <cxxabi.h>
|
||||
#include <unistd.h>
|
||||
#ifndef __APPLE__
|
||||
# include <elfutils/libdwfl.h>
|
||||
#endif
|
||||
|
||||
namespace xo {
|
||||
void
|
||||
print_backtrace(bool demangle_flag) {
|
||||
unw_cursor_t cursor;
|
||||
unw_context_t cx;
|
||||
|
||||
// capture cpu register state at this call site
|
||||
unw_getcontext(&cx);
|
||||
|
||||
// stack frame iterator for current thread.
|
||||
// local -> this process
|
||||
//
|
||||
unw_init_local(&cursor, &cx);
|
||||
|
||||
// depth relative to top of call stack
|
||||
int depth = 0;
|
||||
while (unw_step(&cursor) > 0) {
|
||||
unw_word_t pc = 0;
|
||||
|
||||
// read return address of current frame into pc.
|
||||
// This determines the function that is executing
|
||||
// when print_backtrace() invoked
|
||||
//
|
||||
unw_get_reg(&cursor, UNW_REG_IP, &pc);
|
||||
|
||||
std::array<char, 256> name;
|
||||
unw_word_t offset = 0;
|
||||
|
||||
// mangled function name for current frame's pc.
|
||||
//
|
||||
if (unw_get_proc_name(&cursor, name.data(), sizeof(name), &offset) == 0) {
|
||||
int status = 0;
|
||||
|
||||
// we are resaponsible for calling ::free() on non-null demangled value
|
||||
char * demangled = nullptr;
|
||||
|
||||
if (demangle_flag)
|
||||
demangled = abi::__cxa_demangle(name.data(), nullptr, nullptr, &status);
|
||||
|
||||
if ((status == 0) && demangled) {
|
||||
fprintf(stderr, "#%d 0x%lx %s+0x%lx\n",
|
||||
depth, (long)pc, demangled, (long)offset);
|
||||
free(demangled);
|
||||
} else {
|
||||
// demangle failed (or disabled)
|
||||
fprintf(stderr, "#%d 0x%lx %s+0x%lx\n",
|
||||
depth, (long)pc, name.data(), (long)offset);
|
||||
}
|
||||
} else {
|
||||
// unable to get function name
|
||||
fprintf(stderr, "#%d 0x%lx ???\n", depth, (long)pc);
|
||||
}
|
||||
}
|
||||
}
|
||||
namespace {
|
||||
#ifndef __APPLE__
|
||||
// libdwfl requires callbacks for find_elf and find_debuginfo.
|
||||
// The offline defaults work for the current process.
|
||||
//
|
||||
static const Dwfl_Callbacks dwfl_callbacks = {
|
||||
.find_elf = dwfl_linux_proc_find_elf,
|
||||
.find_debuginfo = dwfl_standard_find_debuginfo,
|
||||
.section_address = nullptr,
|
||||
.debuginfo_path = nullptr,
|
||||
};
|
||||
#endif
|
||||
}
|
||||
|
||||
void
|
||||
print_backtrace_dwarf(bool demangle_flag)
|
||||
{
|
||||
|
||||
#ifdef __APPLE__
|
||||
(void)demangle_flag;
|
||||
|
||||
std::cerr << "backtrace with dwarf symbols not supported on osx" << std::endl;
|
||||
#else
|
||||
unw_cursor_t cursor;
|
||||
unw_context_t cx;
|
||||
|
||||
unw_getcontext(&cx);
|
||||
unw_init_local(&cursor, &cx);
|
||||
|
||||
// set up dwfl for resolving addresses to source locations.
|
||||
//
|
||||
Dwfl * dwfl = dwfl_begin(&dwfl_callbacks);
|
||||
|
||||
if (dwfl) {
|
||||
// populate module list from /proc/self/maps
|
||||
dwfl_linux_proc_report(dwfl, getpid());
|
||||
dwfl_report_end(dwfl, nullptr, nullptr);
|
||||
}
|
||||
|
||||
int depth = 0;
|
||||
while (unw_step(&cursor) > 0) {
|
||||
unw_word_t pc = 0;
|
||||
unw_get_reg(&cursor, UNW_REG_IP, &pc);
|
||||
|
||||
std::array<char, 256> name;
|
||||
unw_word_t offset = 0;
|
||||
|
||||
// resolve function name via libunwind
|
||||
//
|
||||
const char * func_name = "???";
|
||||
char * demangled = nullptr;
|
||||
int status = -1;
|
||||
|
||||
if (unw_get_proc_name(&cursor, name.data(), name.size(), &offset) == 0) {
|
||||
if (demangle_flag)
|
||||
demangled = abi::__cxa_demangle(name.data(), nullptr, nullptr, &status);
|
||||
|
||||
func_name = ((status == 0) && demangled) ? demangled : name.data();
|
||||
}
|
||||
|
||||
// resolve source file + line via DWARF debug info
|
||||
//
|
||||
const char * source_file = nullptr;
|
||||
int line = 0;
|
||||
|
||||
if (dwfl) {
|
||||
Dwfl_Module * module = dwfl_addrmodule(dwfl, pc);
|
||||
|
||||
if (module) {
|
||||
Dwfl_Line * dwfl_line = dwfl_module_getsrc(module, pc);
|
||||
|
||||
if (dwfl_line) {
|
||||
source_file = dwfl_lineinfo(dwfl_line, nullptr, &line,
|
||||
nullptr, nullptr, nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (source_file) {
|
||||
fprintf(stderr, "#%d 0x%lx %s+0x%lx at %s:%d\n",
|
||||
depth, (long)pc, func_name, (long)offset,
|
||||
source_file, line);
|
||||
} else {
|
||||
fprintf(stderr, "#%d 0x%lx %s+0x%lx\n",
|
||||
depth, (long)pc, func_name, (long)offset);
|
||||
}
|
||||
|
||||
if (demangled)
|
||||
free(demangled);
|
||||
|
||||
++depth;
|
||||
}
|
||||
|
||||
if (dwfl)
|
||||
dwfl_end(dwfl);
|
||||
#endif
|
||||
|
||||
}
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end backtrace.cpp */
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
/** @file cmpresult.cpp
|
||||
*
|
||||
* @author Roland Conybeare, Dec 2025
|
||||
**/
|
||||
|
||||
#include "cmpresult.hpp"
|
||||
#include <xo/indentlog/print/tag.hpp>
|
||||
#include <iostream>
|
||||
|
||||
namespace xo {
|
||||
namespace mm {
|
||||
const char *
|
||||
comparison2str(comparison x)
|
||||
{
|
||||
switch (x) {
|
||||
case comparison::invalid:
|
||||
break;
|
||||
case comparison::comparable:
|
||||
return "cmp";
|
||||
case comparison::incomparable:
|
||||
return "!cmp";
|
||||
}
|
||||
|
||||
return "?comparison";
|
||||
}
|
||||
|
||||
void
|
||||
cmpresult::display(std::ostream & os) const
|
||||
{
|
||||
os << "<cmpresult "
|
||||
<< xtag("err", err_)
|
||||
<< xtag("cmp", cmp_)
|
||||
<< ">";
|
||||
}
|
||||
} /*namespace mm*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end cmpresult.cpp */
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
/** @file mmap_util.cpp
|
||||
*
|
||||
* @author Roland Conybeare, Jan 2026
|
||||
**/
|
||||
|
||||
#include "mmap_util.hpp"
|
||||
#include "padding.hpp"
|
||||
#include <sys/mman.h> // for mmap
|
||||
|
||||
namespace xo {
|
||||
namespace mm {
|
||||
auto
|
||||
mmap_util::map_aligned_range(size_t req_z,
|
||||
size_t align_z,
|
||||
bool enable_hugepage_flag,
|
||||
bool debug_flag) -> span_type
|
||||
{
|
||||
scope log(XO_DEBUG(debug_flag),
|
||||
xtag("req_z", req_z),
|
||||
xtag("align_z", align_z));
|
||||
|
||||
// 1. round up to multiple of align_z
|
||||
size_t target_z = padding::with_padding(req_z, align_z); // 4.
|
||||
|
||||
// 2. mmap() will give us page-aligned memory,
|
||||
// but not hugepage-aligned.
|
||||
//
|
||||
// Over-request by align_z to ensure
|
||||
// aligned subrange of size target_z
|
||||
//
|
||||
byte * base = (byte *)(::mmap(nullptr,
|
||||
target_z + align_z,
|
||||
PROT_NONE,
|
||||
MAP_PRIVATE | MAP_ANONYMOUS,
|
||||
-1, 0));
|
||||
|
||||
// on mmap success: upper limit of mapped address range
|
||||
byte * hi = base + (target_z + align_z);
|
||||
// lowest hugepage-aligned address in [base, hi)
|
||||
byte * aligned_base = (byte *)(padding::with_padding((size_t)base, align_z));
|
||||
// end of hugeppage-aligned range starting at aligned_base
|
||||
byte * aligned_hi = aligned_base + target_z;
|
||||
|
||||
log && log("acquired memory [lo,hi) using mmap",
|
||||
xtag("lo", base),
|
||||
xtag("aligned_lo", aligned_base),
|
||||
xtag("req_z", req_z),
|
||||
xtag("target_z", target_z),
|
||||
xtag("aligned_hi", aligned_hi),
|
||||
xtag("hi", hi));
|
||||
|
||||
// 3. assess mmap success
|
||||
{
|
||||
if (base == MAP_FAILED) {
|
||||
throw std::runtime_error(tostr("ArenaAlloc: uncommitted allocation failed",
|
||||
xtag("size", req_z)));
|
||||
}
|
||||
|
||||
assert((size_t)aligned_base % align_z == 0);
|
||||
assert(aligned_base >= base);
|
||||
assert(aligned_base < base + align_z);
|
||||
}
|
||||
|
||||
// 4. release unaligned prefix
|
||||
if (base < aligned_base) {
|
||||
size_t ua_prefix = aligned_base - base;
|
||||
|
||||
::munmap(base, ua_prefix);
|
||||
}
|
||||
|
||||
// 5. release unaligned suffix
|
||||
if (aligned_hi < hi) {
|
||||
size_t suffix = hi - aligned_hi;
|
||||
|
||||
::munmap(aligned_hi, suffix);
|
||||
}
|
||||
|
||||
if (enable_hugepage_flag) {
|
||||
#ifdef __linux__
|
||||
/** linux:
|
||||
* opt-in to transparent huge pages (THP)
|
||||
* provided OS configured to support them.
|
||||
* otherwise fallback gracefully.
|
||||
*
|
||||
* Huge pages -> use fewer TLB entries + faster
|
||||
* shorter path through page table.
|
||||
*
|
||||
* When we commit (i.e. obtain physical memory on page fault),
|
||||
* typically expect to pay ~1us per superpage.
|
||||
* Much better than ~500us to commit 512 4k VM pages.
|
||||
*
|
||||
* But wasted if we don't use the memory.
|
||||
*
|
||||
* Page table has a handful of levels
|
||||
**/
|
||||
::madvise(aligned_base, target_z, MADV_HUGEPAGE); // 8.
|
||||
#endif
|
||||
}
|
||||
|
||||
return span_type(aligned_base, aligned_hi);
|
||||
}
|
||||
} /*namespace mm*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end mmap_util.cpp */
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# xo-alloc2/utest/CMakeLists.txt
|
||||
#
|
||||
|
||||
set(UTEST_EXE utest.arena)
|
||||
set(UTEST_SRCS
|
||||
arena_utest_main.cpp
|
||||
# objectmodel.test.cpp
|
||||
DArena.test.cpp
|
||||
DArenaVector.test.cpp
|
||||
DArenaHashMap.test.cpp
|
||||
DCircularBuffer.test.cpp
|
||||
# DArenaIterator.test.cpp
|
||||
# random_allocs.cpp
|
||||
)
|
||||
|
||||
if (ENABLE_TESTING)
|
||||
xo_add_utest_executable(${UTEST_EXE} ${UTEST_SRCS})
|
||||
xo_self_dependency(${UTEST_EXE} xo_arena)
|
||||
xo_headeronly_dependency(${UTEST_EXE} randomgen)
|
||||
# xo_headeronly_dependency(${UTEST_EXE} indentlog)
|
||||
xo_external_target_dependency(${UTEST_EXE} Catch2 Catch2::Catch2)
|
||||
endif()
|
||||
|
||||
# end CMakeLists.txt
|
||||
|
|
@ -1,219 +0,0 @@
|
|||
/** @file DArena.test.cpp
|
||||
*
|
||||
* @author Roland Conybeare, Jan 2026
|
||||
**/
|
||||
|
||||
#include "DArena.hpp"
|
||||
#include "print.hpp"
|
||||
#include <xo/indentlog/print/tag.hpp>
|
||||
#include <catch2/catch.hpp>
|
||||
|
||||
namespace xo {
|
||||
using xo::mm::DArena;
|
||||
using xo::mm::AllocHeader;
|
||||
using xo::mm::AllocHeaderConfig;
|
||||
using xo::mm::ArenaConfig;
|
||||
using xo::mm::padding;
|
||||
using xo::mm::error;
|
||||
using xo::reflect::typeseq;
|
||||
using xo::xtag;
|
||||
using std::byte;
|
||||
|
||||
namespace ut {
|
||||
TEST_CASE("DArena-tiny", "[arena][DArena]")
|
||||
{
|
||||
ArenaConfig cfg { .name_ = "testarena",
|
||||
.size_ = 1 };
|
||||
DArena arena = DArena::map(cfg);
|
||||
|
||||
REQUIRE(arena.config_.name_ == cfg.name_);
|
||||
REQUIRE(arena.lo_ != nullptr);
|
||||
REQUIRE(arena.free_ == arena.lo_);
|
||||
REQUIRE(arena.limit_ == arena.lo_);
|
||||
REQUIRE(arena.hi_ != nullptr);
|
||||
REQUIRE(arena.hi_ > arena.lo_);
|
||||
REQUIRE(((size_t)arena.hi_ - (size_t)arena.lo_) % arena.page_z_ == 0);
|
||||
REQUIRE(arena.lo_ + cfg.size_ <= arena.hi_);
|
||||
|
||||
/* verify arena.lo_ is aligned on a page boundary */
|
||||
REQUIRE(((size_t)(arena.lo_) & (arena.page_z_ - 1)) == 0);
|
||||
|
||||
/* verify arena.hi_ is aligned on a hugepage boundary */
|
||||
REQUIRE(((size_t)(arena.hi_) & (arena.page_z_ - 1)) == 0);
|
||||
|
||||
byte * lo = arena.lo_;
|
||||
byte * free = arena.free_;
|
||||
byte * limit = arena.limit_;
|
||||
byte * hi = arena.hi_;
|
||||
size_t committed_z = arena.committed_z_;
|
||||
|
||||
DArena arena2 = std::move(arena);
|
||||
|
||||
REQUIRE(arena.lo_ == nullptr);
|
||||
REQUIRE(arena.free_ == nullptr);
|
||||
REQUIRE(arena.limit_ == nullptr);
|
||||
REQUIRE(arena.hi_ == nullptr);
|
||||
REQUIRE(arena.committed_z_ == 0);
|
||||
|
||||
REQUIRE(arena.lo_ == nullptr);
|
||||
REQUIRE(arena2.lo_ == lo);
|
||||
REQUIRE(arena2.free_ == free);
|
||||
REQUIRE(arena2.limit_ == limit);
|
||||
REQUIRE(arena2.hi_ == hi);
|
||||
REQUIRE(arena2.committed_z_ == committed_z);
|
||||
}
|
||||
|
||||
TEST_CASE("DArena-medium", "[arena][DArena]")
|
||||
{
|
||||
ArenaConfig cfg { .name_ = "testarena",
|
||||
.size_ = 10*1024*1024 };
|
||||
DArena arena = DArena::map(cfg);
|
||||
|
||||
REQUIRE(arena.config_.name_ == cfg.name_);
|
||||
REQUIRE(arena.lo_ != nullptr);
|
||||
REQUIRE(arena.free_ == arena.lo_);
|
||||
REQUIRE(arena.limit_ == arena.lo_);
|
||||
REQUIRE(arena.hi_ != nullptr);
|
||||
REQUIRE(arena.hi_ > arena.lo_);
|
||||
REQUIRE(((size_t)arena.hi_ - (size_t)arena.lo_) % cfg.hugepage_z_ == 0);
|
||||
REQUIRE(arena.lo_ + cfg.size_ <= arena.hi_);
|
||||
|
||||
/* verify arena.lo_ is aligned on a page boundary */
|
||||
REQUIRE(((size_t)(arena.lo_) & (cfg.hugepage_z_ - 1)) == 0);
|
||||
|
||||
/* verify arena.hi_ is aligned on a hugepage boundary */
|
||||
REQUIRE(((size_t)(arena.hi_) & (cfg.hugepage_z_ - 1)) == 0);
|
||||
|
||||
byte * lo = arena.lo_;
|
||||
byte * free = arena.free_;
|
||||
byte * limit = arena.limit_;
|
||||
byte * hi = arena.hi_;
|
||||
size_t committed_z = arena.committed_z_;
|
||||
|
||||
DArena arena2 = std::move(arena);
|
||||
|
||||
REQUIRE(arena.lo_ == nullptr);
|
||||
REQUIRE(arena.free_ == nullptr);
|
||||
REQUIRE(arena.limit_ == nullptr);
|
||||
REQUIRE(arena.hi_ == nullptr);
|
||||
REQUIRE(arena.committed_z_ == 0);
|
||||
|
||||
REQUIRE(arena.lo_ == nullptr);
|
||||
REQUIRE(arena2.lo_ == lo);
|
||||
REQUIRE(arena2.free_ == free);
|
||||
REQUIRE(arena2.limit_ == limit);
|
||||
REQUIRE(arena2.hi_ == hi);
|
||||
REQUIRE(arena2.committed_z_ == committed_z);
|
||||
}
|
||||
|
||||
TEST_CASE("DArena-expand-1", "[arena][DArena]")
|
||||
{
|
||||
/* typed allocator a1o */
|
||||
ArenaConfig cfg { .name_ = "testarena",
|
||||
.size_ = 1,
|
||||
.debug_flag_ = false };
|
||||
DArena arena = DArena::map(cfg);
|
||||
|
||||
REQUIRE(arena.available() == 0);
|
||||
REQUIRE(arena.allocated() == 0);
|
||||
|
||||
size_t z2 = 512;
|
||||
bool ok = arena.expand(z2, __PRETTY_FUNCTION__);
|
||||
|
||||
INFO(xtag("last_error", arena.last_error()));
|
||||
|
||||
REQUIRE(ok);
|
||||
|
||||
REQUIRE(arena.reserved() % arena.page_z() == 0);
|
||||
REQUIRE(arena.committed() >= z2);
|
||||
REQUIRE(arena.committed() % arena.page_z() == 0);
|
||||
REQUIRE(arena.available() >= z2);
|
||||
REQUIRE(arena.available() == arena.committed());
|
||||
REQUIRE(arena.allocated() == 0);
|
||||
|
||||
}
|
||||
|
||||
TEST_CASE("arena-alloc-1", "[arena][DArena]")
|
||||
{
|
||||
/* typed allocator a1o */
|
||||
ArenaConfig cfg { .name_ = "testarena",
|
||||
.size_ = 64*1024,
|
||||
.debug_flag_ = false };
|
||||
DArena arena = DArena::map(cfg);
|
||||
|
||||
REQUIRE(arena.reserved() >= cfg.size_);
|
||||
REQUIRE(arena.committed() == 0);
|
||||
REQUIRE(arena.available() == 0);
|
||||
REQUIRE(arena.allocated() == 0);
|
||||
|
||||
size_t z0 = 1;
|
||||
byte * m0 = arena.alloc(typeseq::sentinel(), 1);
|
||||
|
||||
REQUIRE(m0);
|
||||
REQUIRE(arena.last_error().error_ == error::ok);
|
||||
REQUIRE(arena.last_error().error_seq_ == 0);
|
||||
REQUIRE(arena.allocated() >= z0);
|
||||
REQUIRE(arena.allocated() < z0 + padding::c_alloc_alignment );
|
||||
REQUIRE(arena.allocated() <= arena.committed());
|
||||
REQUIRE(arena.allocated() + arena.available() == arena.committed());
|
||||
REQUIRE(arena.committed() <= arena.reserved());
|
||||
|
||||
size_t z1 = 16;
|
||||
byte * m1 = arena.alloc(typeseq::sentinel(), z1);
|
||||
|
||||
REQUIRE(m1);
|
||||
REQUIRE(arena.last_error().error_ == error::ok);
|
||||
REQUIRE(arena.last_error().error_seq_ == 0);
|
||||
REQUIRE(arena.allocated() >= z0 + z1);
|
||||
REQUIRE(arena.allocated() < z0 + z1 + 2 * padding::c_alloc_alignment );
|
||||
REQUIRE(arena.allocated() <= arena.committed());
|
||||
REQUIRE(arena.allocated() + arena.available() == arena.committed());
|
||||
REQUIRE(arena.committed() <= arena.reserved());
|
||||
}
|
||||
|
||||
TEST_CASE("arena-alloc-2", "[arena][DArena]")
|
||||
{
|
||||
using header_type = AllocHeader;
|
||||
|
||||
/* typed allocator a1o, with object header */
|
||||
ArenaConfig cfg { .name_ = "testarena",
|
||||
.size_ = 64*1024,
|
||||
.store_header_flag_ = true,
|
||||
/* up to 4GB */
|
||||
.header_ = AllocHeaderConfig(0 /*guard_z*/,
|
||||
0xfd /*guard_byte*/,
|
||||
0 /*tseq-bits*/,
|
||||
0 /*age-bits*/,
|
||||
32 /*size-bits*/),
|
||||
.debug_flag_ = false,
|
||||
};
|
||||
DArena arena = DArena::map(cfg);
|
||||
|
||||
REQUIRE(arena.reserved() >= cfg.size_);
|
||||
REQUIRE(arena.committed() == 0);
|
||||
REQUIRE(arena.available() == 0);
|
||||
REQUIRE(arena.allocated() == 0);
|
||||
|
||||
size_t z0 = 1;
|
||||
byte * m0 = arena.alloc(typeseq::sentinel(), 1);
|
||||
|
||||
REQUIRE(m0);
|
||||
|
||||
header_type* header = (header_type*)(m0 - sizeof(header_type));
|
||||
|
||||
REQUIRE(arena.contains(header));
|
||||
REQUIRE(cfg.header_.size(*header) == padding::with_padding(z0));
|
||||
//REQUIRE(((*header) & cfg.header_size_mask_) == padding::with_padding(z0));
|
||||
REQUIRE(arena.last_error().error_ == error::ok);
|
||||
REQUIRE(arena.last_error().error_seq_ == 0);
|
||||
REQUIRE(arena.allocated() >= z0);
|
||||
REQUIRE(arena.allocated() < sizeof(DArena::header_type) + z0 + padding::c_alloc_alignment );
|
||||
REQUIRE(arena.allocated() <= arena.committed());
|
||||
REQUIRE(arena.allocated() + arena.available() == arena.committed());
|
||||
REQUIRE(arena.committed() <= arena.reserved());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/* end DArena.test.cpp */
|
||||
|
|
@ -1,360 +0,0 @@
|
|||
/** @file DArenaHashMap.test.cpp
|
||||
*
|
||||
* @author Roland Conybeare, Jan 2026
|
||||
**/
|
||||
|
||||
#include "DArenaHashMap.hpp"
|
||||
#include "random_hash_ops.hpp"
|
||||
#include <xo/randomgen/random_seed.hpp>
|
||||
#include <xo/indentlog/scope.hpp>
|
||||
#include <xo/indentlog/print/tag.hpp>
|
||||
#include <xo/indentlog/print/hex.hpp>
|
||||
#include <catch2/catch.hpp>
|
||||
|
||||
namespace xo {
|
||||
using xo::map::DArenaHashMapUtil;
|
||||
using xo::map::DArenaHashMap;
|
||||
using xo::rng::random_seed;
|
||||
using xo::rng::xoshiro256ss;
|
||||
using utest::UtestTools;
|
||||
using utest::HashMapUtil;
|
||||
|
||||
namespace ut {
|
||||
TEST_CASE("DArenaHashMap-ctor", "[arena][DArenaHashMap]")
|
||||
{
|
||||
using HashMap = DArenaHashMap<int, int>;
|
||||
|
||||
HashMap map("utest");
|
||||
|
||||
REQUIRE(map.empty());
|
||||
REQUIRE(map.size() == 0);
|
||||
REQUIRE(map.groups() == 1);
|
||||
REQUIRE(map.capacity() == DArenaHashMapUtil::c_group_size);
|
||||
}
|
||||
|
||||
TEST_CASE("DArenaHashMap-ctor2", "[arena][DArenaHashMap]")
|
||||
{
|
||||
using HashMap = DArenaHashMap<int, int>;
|
||||
|
||||
HashMap map("utest", 257);
|
||||
|
||||
REQUIRE(map.empty());
|
||||
REQUIRE(map.size() == 0);
|
||||
REQUIRE(map.capacity() == map.groups() * DArenaHashMapUtil::c_group_size);
|
||||
REQUIRE(map.capacity() == std::max(512ul,
|
||||
DArenaHashMapUtil::c_group_size));
|
||||
}
|
||||
|
||||
TEST_CASE("DArenaHashMap-try-insert", "[arena][DArenaHashMap]")
|
||||
{
|
||||
using HashMap = DArenaHashMap<int, int>;
|
||||
|
||||
HashMap map("utest");
|
||||
|
||||
REQUIRE(map.empty());
|
||||
REQUIRE(map.size() == 0);
|
||||
REQUIRE(map.groups() == 1);
|
||||
REQUIRE(map.capacity() == DArenaHashMapUtil::c_group_size);
|
||||
|
||||
{
|
||||
auto x = map.try_insert(std::make_pair(1, 11));
|
||||
|
||||
REQUIRE(x.first);
|
||||
REQUIRE(x.second);
|
||||
REQUIRE(!map.empty());
|
||||
REQUIRE(map.size() == 1);
|
||||
REQUIRE(map.groups() == 1);
|
||||
REQUIRE(map.capacity() == DArenaHashMapUtil::c_group_size);
|
||||
REQUIRE(map.load_factor() == 1/16.0);
|
||||
|
||||
/* verify iteration */
|
||||
{
|
||||
size_t n = 0;
|
||||
for (auto & ix : map) {
|
||||
REQUIRE(ix.first == 1);
|
||||
REQUIRE(ix.second == 11);
|
||||
++n;
|
||||
}
|
||||
REQUIRE(n == map.size());
|
||||
}
|
||||
|
||||
REQUIRE(map.verify_ok(verify_policy::chatty()));
|
||||
}
|
||||
|
||||
{
|
||||
auto x = map.try_insert(std::make_pair(2, 9));
|
||||
|
||||
REQUIRE(x.first);
|
||||
REQUIRE(x.second);
|
||||
REQUIRE(!map.empty());
|
||||
REQUIRE(map.size() == 2);
|
||||
REQUIRE(map.groups() == 1);
|
||||
REQUIRE(map.capacity() == DArenaHashMapUtil::c_group_size);
|
||||
REQUIRE(map.load_factor() == 2/16.0);
|
||||
|
||||
/* verify iteration */
|
||||
{
|
||||
size_t n = 0;
|
||||
for (auto & ix : map) {
|
||||
++n;
|
||||
}
|
||||
REQUIRE(n == map.size());
|
||||
}
|
||||
|
||||
REQUIRE(map.verify_ok(verify_policy::chatty()));
|
||||
}
|
||||
|
||||
{
|
||||
auto x = map.try_insert(std::make_pair(259, 12));
|
||||
|
||||
REQUIRE(x.first);
|
||||
REQUIRE(x.second);
|
||||
REQUIRE(!map.empty());
|
||||
REQUIRE(map.size() == 3);
|
||||
REQUIRE(map.groups() == 1);
|
||||
REQUIRE(map.capacity() == DArenaHashMapUtil::c_group_size);
|
||||
REQUIRE(map.load_factor() == 3/16.0);
|
||||
|
||||
/* verify iteration */
|
||||
{
|
||||
size_t n = 0;
|
||||
for (auto & ix : map) {
|
||||
switch (ix.first) {
|
||||
case 1:
|
||||
REQUIRE(ix.second == 11);
|
||||
break;
|
||||
case 2:
|
||||
REQUIRE(ix.second == 9);
|
||||
break;
|
||||
case 259:
|
||||
REQUIRE(ix.second == 12);
|
||||
break;
|
||||
default:
|
||||
REQUIRE(false);
|
||||
}
|
||||
++n;
|
||||
}
|
||||
REQUIRE(n == map.size());
|
||||
}
|
||||
|
||||
REQUIRE(map.verify_ok(verify_policy::chatty()));
|
||||
}
|
||||
|
||||
{
|
||||
map.clear();
|
||||
|
||||
REQUIRE(map.empty());
|
||||
REQUIRE(map.size() == 0);
|
||||
REQUIRE(map.groups() == 0);
|
||||
REQUIRE(map.capacity() == 0);
|
||||
|
||||
REQUIRE(map.verify_ok(verify_policy::chatty()));
|
||||
}
|
||||
|
||||
/* slightly different starting point, 0 capacity! */
|
||||
{
|
||||
auto x = map.try_insert(std::make_pair(1, 11));
|
||||
|
||||
/* try_insert should fail - no capacity */
|
||||
REQUIRE(!x.first);
|
||||
REQUIRE(!x.second);
|
||||
|
||||
REQUIRE(map.verify_ok(verify_policy::chatty()));
|
||||
}
|
||||
|
||||
{
|
||||
/* insert will grow hash table */
|
||||
auto x = map.insert(std::make_pair(1, 11));
|
||||
|
||||
CHECK(x);
|
||||
REQUIRE(!map.empty());
|
||||
REQUIRE(map.size() == 1);
|
||||
REQUIRE(map.groups() == 1);
|
||||
REQUIRE(map.capacity() == DArenaHashMapUtil::c_group_size);
|
||||
REQUIRE(map.load_factor() == 1/16.0);
|
||||
|
||||
/* verify iteration */
|
||||
{
|
||||
size_t n = 0;
|
||||
for (auto & ix : map) {
|
||||
REQUIRE(ix.first == 1);
|
||||
REQUIRE(ix.second == 11);
|
||||
++n;
|
||||
}
|
||||
REQUIRE(n == map.size());
|
||||
}
|
||||
|
||||
REQUIRE(map.verify_ok(verify_policy::chatty()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
TEST_CASE("DArenaHashMap-try-insert2", "[arena][DArenaHashMap]")
|
||||
{
|
||||
using HashMap = DArenaHashMap<int, int>;
|
||||
|
||||
std::uint64_t seed = 17747889312058974961ul;
|
||||
//random_seed(&seed); // to get new random seed
|
||||
//log && log(xtag("seed", seed));
|
||||
|
||||
auto rgen = xoshiro256ss(seed);
|
||||
|
||||
/* 1. Perform series of tests with increasing scale
|
||||
* 2. Each test may run in two modes:
|
||||
* a. silent fast fail. just report success.
|
||||
* In this mode avoid catch2 REQUIRE
|
||||
* b. noisy. run with logging enabled
|
||||
* This mode automatically invoked when silent mode
|
||||
* observes test failure
|
||||
*/
|
||||
|
||||
for (std::uint32_t n = 0; n <= 8; ) {
|
||||
HashMap hash_map("utest");
|
||||
|
||||
auto test_fn = [&rgen, &hash_map](bool dbg_flag,
|
||||
std::uint32_t n)
|
||||
{
|
||||
bool ok_flag = true;
|
||||
|
||||
ok_flag &= HashMapUtil<HashMap>::random_inserts(n, dbg_flag, &rgen, &hash_map);
|
||||
|
||||
ok_flag &= HashMapUtil<HashMap>::check_forward_iterator(0.0 /*dvalue*/,
|
||||
dbg_flag, hash_map);
|
||||
/* regular forward iterator, but start at hash_map.end() and use operator-- */
|
||||
ok_flag &= HashMapUtil<HashMap>::check_backward_iterator(0.0 /*dvalue*/,
|
||||
dbg_flag, hash_map);
|
||||
|
||||
ok_flag &= HashMapUtil<HashMap>::random_lookups(0.0 /*dvalue*/,
|
||||
dbg_flag, &rgen, hash_map);
|
||||
|
||||
return ok_flag;
|
||||
};
|
||||
|
||||
bool ok_flag = UtestTools::bimodal_test("DArenaHashMap-try-insert2", test_fn, n);
|
||||
|
||||
if (n == 0)
|
||||
n = 1;
|
||||
else
|
||||
n = 2*n;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("DArenaHashMap-operator-bracket", "[arena][DArenaHashMap]")
|
||||
{
|
||||
scope log(XO_DEBUG(false));
|
||||
|
||||
using HashMap = DArenaHashMap<int, int>;
|
||||
|
||||
HashMap map("utest");
|
||||
|
||||
// copy keys here so we can print stuff
|
||||
std::vector<int> key_v;
|
||||
|
||||
// insert via operator[]
|
||||
map[1] = 100;
|
||||
key_v.push_back(1);
|
||||
REQUIRE(map.verify_ok(verify_policy::chatty()));
|
||||
|
||||
map[2] = 200;
|
||||
key_v.push_back(2);
|
||||
REQUIRE(map.verify_ok(verify_policy::chatty()));
|
||||
|
||||
map[3] = 300;
|
||||
key_v.push_back(3);
|
||||
REQUIRE(map.verify_ok(verify_policy::chatty()));
|
||||
|
||||
REQUIRE(map.size() == 3);
|
||||
|
||||
// read back via operator[]
|
||||
REQUIRE(map[1] == 100);
|
||||
REQUIRE(map[2] == 200);
|
||||
REQUIRE(map[3] == 300);
|
||||
|
||||
// update via operator[]
|
||||
map[2] = 250;
|
||||
REQUIRE(map[2] == 250);
|
||||
REQUIRE(map.size() == 3); // size unchanged
|
||||
REQUIRE(map.verify_ok(verify_policy::chatty()));
|
||||
|
||||
// verify via find
|
||||
{
|
||||
auto it = map.find(1);
|
||||
REQUIRE(it != map.end());
|
||||
REQUIRE(it->second == 100);
|
||||
}
|
||||
{
|
||||
auto it = map.find(2);
|
||||
REQUIRE(it != map.end());
|
||||
REQUIRE(it->second == 250);
|
||||
}
|
||||
{
|
||||
auto it = map.find(3);
|
||||
REQUIRE(it != map.end());
|
||||
REQUIRE(it->second == 300);
|
||||
}
|
||||
{
|
||||
auto it = map.find(4);
|
||||
REQUIRE(it == map.end());
|
||||
}
|
||||
|
||||
REQUIRE(map.verify_ok(verify_policy::chatty()));
|
||||
|
||||
// operator[] on non-existent key creates default entry
|
||||
int & val = map[999];
|
||||
key_v.push_back(999);
|
||||
|
||||
for (uint64_t i_slot = 0, N = map._store()->n_slot_; i_slot < N; ++i_slot) {
|
||||
auto key = map._store()->slots_[i_slot].first;
|
||||
auto ctrl = map._store()->control_
|
||||
[i_slot + DArenaHashMapUtil::c_control_stub];
|
||||
auto isdata = DArenaHashMapUtil::is_data(ctrl);
|
||||
auto [h1,h2] = map._hash(key);
|
||||
|
||||
if ((key != 0)
|
||||
|| (h1 != 0)
|
||||
|| (h2 != 0)
|
||||
|| (ctrl != DArenaHashMapUtil::c_empty_slot)
|
||||
|| isdata
|
||||
) {
|
||||
log && log(xtag("i", i_slot),
|
||||
xtag("key[i]", key),
|
||||
xtag("h1", h1), xtag("h2", h2),
|
||||
xtag("ctrl[i]", (int)ctrl),
|
||||
xtag("isdata", isdata));
|
||||
}
|
||||
}
|
||||
|
||||
REQUIRE(map.verify_ok(verify_policy::chatty()));
|
||||
|
||||
REQUIRE(map.size() == 4);
|
||||
REQUIRE(val == 0); // default-initialized
|
||||
val = 999;
|
||||
REQUIRE(map[999] == 999);
|
||||
}
|
||||
|
||||
TEST_CASE("DArenaHashMap-string_view-key", "[arena][DArenaHashMap]")
|
||||
{
|
||||
using HashMap = DArenaHashMap<std::string_view, int>;
|
||||
|
||||
HashMap map("utest", 1024);
|
||||
|
||||
REQUIRE(map.verify_ok());
|
||||
|
||||
map["hello"] = 42;
|
||||
REQUIRE(map.size() == 1);
|
||||
REQUIRE(map.verify_ok());
|
||||
|
||||
map["world"] = 100;
|
||||
REQUIRE(map.size() == 2);
|
||||
REQUIRE(map.verify_ok());
|
||||
|
||||
REQUIRE(map["hello"] == 42);
|
||||
REQUIRE(map["world"] == 100);
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// - let's try getting lcov to work in xo-umbrella2
|
||||
}
|
||||
}
|
||||
|
||||
/* end DArenaHashMap.test.cpp */
|
||||
|
|
@ -1,539 +0,0 @@
|
|||
/** @file DArenaVector.test.cpp
|
||||
*
|
||||
* @author Roland Conybeare, Jan 2026
|
||||
**/
|
||||
|
||||
#include "DArenaVector.hpp"
|
||||
#include <catch2/catch.hpp>
|
||||
|
||||
namespace xo {
|
||||
using xo::mm::DArenaVector;
|
||||
using xo::mm::ArenaConfig;
|
||||
using std::byte;
|
||||
|
||||
namespace ut {
|
||||
TEST_CASE("DArenaVector-tiny", "[arena][DArenaVector]")
|
||||
{
|
||||
ArenaConfig cfg { .name_ = "testarena",
|
||||
.size_ = 1 };
|
||||
DArenaVector<double> arenavec = DArenaVector<double>::map(cfg);
|
||||
|
||||
REQUIRE(arenavec.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("DArenaVector-push_back-rvalue", "[arena][DArenaVector]")
|
||||
{
|
||||
ArenaConfig cfg { .name_ = "testarena",
|
||||
.size_ = 4096 };
|
||||
DArenaVector<double> vec = DArenaVector<double>::map(cfg);
|
||||
|
||||
REQUIRE(vec.empty());
|
||||
REQUIRE(vec.size() == 0);
|
||||
|
||||
vec.push_back(1.5);
|
||||
|
||||
REQUIRE(!vec.empty());
|
||||
REQUIRE(vec.size() == 1);
|
||||
REQUIRE(vec[0] == 1.5);
|
||||
|
||||
vec.push_back(2.5);
|
||||
vec.push_back(3.5);
|
||||
|
||||
REQUIRE(vec.size() == 3);
|
||||
REQUIRE(vec[0] == 1.5);
|
||||
REQUIRE(vec[1] == 2.5);
|
||||
REQUIRE(vec[2] == 3.5);
|
||||
}
|
||||
|
||||
TEST_CASE("DArenaVector-push_back-lvalue", "[arena][DArenaVector]")
|
||||
{
|
||||
ArenaConfig cfg { .name_ = "testarena",
|
||||
.size_ = 4096 };
|
||||
DArenaVector<double> vec = DArenaVector<double>::map(cfg);
|
||||
|
||||
double a = 10.0;
|
||||
double b = 20.0;
|
||||
double c = 30.0;
|
||||
|
||||
vec.push_back(a);
|
||||
|
||||
REQUIRE(vec.size() == 1);
|
||||
REQUIRE(vec[0] == 10.0);
|
||||
|
||||
vec.push_back(b);
|
||||
vec.push_back(c);
|
||||
|
||||
REQUIRE(vec.size() == 3);
|
||||
REQUIRE(vec[0] == 10.0);
|
||||
REQUIRE(vec[1] == 20.0);
|
||||
REQUIRE(vec[2] == 30.0);
|
||||
}
|
||||
|
||||
TEST_CASE("DArenaVector-at-valid", "[arena][DArenaVector]")
|
||||
{
|
||||
ArenaConfig cfg { .name_ = "testarena",
|
||||
.size_ = 4096 };
|
||||
DArenaVector<double> vec = DArenaVector<double>::map(cfg);
|
||||
|
||||
vec.push_back(100.0);
|
||||
vec.push_back(200.0);
|
||||
vec.push_back(300.0);
|
||||
|
||||
REQUIRE(vec.at(0) == 100.0);
|
||||
REQUIRE(vec.at(1) == 200.0);
|
||||
REQUIRE(vec.at(2) == 300.0);
|
||||
|
||||
// test mutability via at()
|
||||
vec.at(1) = 250.0;
|
||||
REQUIRE(vec.at(1) == 250.0);
|
||||
}
|
||||
|
||||
TEST_CASE("DArenaVector-at-throws", "[arena][DArenaVector]")
|
||||
{
|
||||
ArenaConfig cfg { .name_ = "testarena",
|
||||
.size_ = 4096 };
|
||||
DArenaVector<double> vec = DArenaVector<double>::map(cfg);
|
||||
|
||||
// empty vector - any index is invalid
|
||||
REQUIRE_THROWS_AS(vec.at(0), std::out_of_range);
|
||||
|
||||
vec.push_back(1.0);
|
||||
vec.push_back(2.0);
|
||||
|
||||
// valid indices work
|
||||
REQUIRE_NOTHROW(vec.at(0));
|
||||
REQUIRE_NOTHROW(vec.at(1));
|
||||
|
||||
// index == size is invalid
|
||||
REQUIRE_THROWS_AS(vec.at(2), std::out_of_range);
|
||||
|
||||
// index > size is invalid
|
||||
REQUIRE_THROWS_AS(vec.at(100), std::out_of_range);
|
||||
}
|
||||
|
||||
TEST_CASE("DArenaVector-resize-expand", "[arena][DArenaVector]")
|
||||
{
|
||||
ArenaConfig cfg { .name_ = "testarena",
|
||||
.size_ = 4096 };
|
||||
DArenaVector<double> vec = DArenaVector<double>::map(cfg);
|
||||
|
||||
REQUIRE(vec.size() == 0);
|
||||
|
||||
// resize from 0 to 5
|
||||
vec.resize(5);
|
||||
REQUIRE(vec.size() == 5);
|
||||
|
||||
// can write to all indices
|
||||
for (size_t i = 0; i < 5; ++i) {
|
||||
vec[i] = static_cast<double>(i * 10);
|
||||
}
|
||||
|
||||
REQUIRE(vec[0] == 0.0);
|
||||
REQUIRE(vec[1] == 10.0);
|
||||
REQUIRE(vec[2] == 20.0);
|
||||
REQUIRE(vec[3] == 30.0);
|
||||
REQUIRE(vec[4] == 40.0);
|
||||
|
||||
// resize to larger
|
||||
vec.resize(8);
|
||||
REQUIRE(vec.size() == 8);
|
||||
|
||||
// original values preserved
|
||||
REQUIRE(vec[0] == 0.0);
|
||||
REQUIRE(vec[1] == 10.0);
|
||||
REQUIRE(vec[4] == 40.0);
|
||||
}
|
||||
|
||||
TEST_CASE("DArenaVector-resize-shrink", "[arena][DArenaVector]")
|
||||
{
|
||||
ArenaConfig cfg { .name_ = "testarena",
|
||||
.size_ = 4096 };
|
||||
DArenaVector<double> vec = DArenaVector<double>::map(cfg);
|
||||
|
||||
vec.push_back(1.0);
|
||||
vec.push_back(2.0);
|
||||
vec.push_back(3.0);
|
||||
vec.push_back(4.0);
|
||||
vec.push_back(5.0);
|
||||
|
||||
REQUIRE(vec.size() == 5);
|
||||
|
||||
// shrink to 3
|
||||
vec.resize(3);
|
||||
REQUIRE(vec.size() == 3);
|
||||
|
||||
// first 3 elements preserved
|
||||
REQUIRE(vec[0] == 1.0);
|
||||
REQUIRE(vec[1] == 2.0);
|
||||
REQUIRE(vec[2] == 3.0);
|
||||
|
||||
// index 3 now out of bounds
|
||||
REQUIRE_THROWS_AS(vec.at(3), std::out_of_range);
|
||||
|
||||
// shrink to 0
|
||||
vec.resize(0);
|
||||
REQUIRE(vec.size() == 0);
|
||||
REQUIRE(vec.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("DArenaVector-resize-same", "[arena][DArenaVector]")
|
||||
{
|
||||
ArenaConfig cfg { .name_ = "testarena",
|
||||
.size_ = 4096 };
|
||||
DArenaVector<double> vec = DArenaVector<double>::map(cfg);
|
||||
|
||||
vec.push_back(10.0);
|
||||
vec.push_back(20.0);
|
||||
vec.push_back(30.0);
|
||||
|
||||
REQUIRE(vec.size() == 3);
|
||||
|
||||
// resize to same size
|
||||
vec.resize(3);
|
||||
REQUIRE(vec.size() == 3);
|
||||
|
||||
// values unchanged
|
||||
REQUIRE(vec[0] == 10.0);
|
||||
REQUIRE(vec[1] == 20.0);
|
||||
REQUIRE(vec[2] == 30.0);
|
||||
}
|
||||
|
||||
TEST_CASE("DArenaVector-clear", "[arena][DArenaVector]")
|
||||
{
|
||||
ArenaConfig cfg { .name_ = "testarena",
|
||||
.size_ = 4096 };
|
||||
DArenaVector<double> vec = DArenaVector<double>::map(cfg);
|
||||
|
||||
vec.push_back(1.0);
|
||||
vec.push_back(2.0);
|
||||
vec.push_back(3.0);
|
||||
|
||||
REQUIRE(vec.size() == 3);
|
||||
REQUIRE(!vec.empty());
|
||||
|
||||
vec.clear();
|
||||
|
||||
REQUIRE(vec.size() == 0);
|
||||
REQUIRE(vec.empty());
|
||||
|
||||
// can still push after clear
|
||||
vec.push_back(99.0);
|
||||
REQUIRE(vec.size() == 1);
|
||||
REQUIRE(vec[0] == 99.0);
|
||||
}
|
||||
|
||||
TEST_CASE("DArenaVector-iterators", "[arena][DArenaVector]")
|
||||
{
|
||||
ArenaConfig cfg { .name_ = "testarena",
|
||||
.size_ = 4096 };
|
||||
DArenaVector<double> vec = DArenaVector<double>::map(cfg);
|
||||
|
||||
vec.push_back(10.0);
|
||||
vec.push_back(20.0);
|
||||
vec.push_back(30.0);
|
||||
|
||||
// begin/end
|
||||
REQUIRE(vec.begin() != vec.end());
|
||||
REQUIRE(vec.end() - vec.begin() == 3);
|
||||
|
||||
// iterate with pointer arithmetic
|
||||
auto it = vec.begin();
|
||||
REQUIRE(*it == 10.0);
|
||||
++it;
|
||||
REQUIRE(*it == 20.0);
|
||||
++it;
|
||||
REQUIRE(*it == 30.0);
|
||||
++it;
|
||||
REQUIRE(it == vec.end());
|
||||
|
||||
// modify through iterator
|
||||
*vec.begin() = 15.0;
|
||||
REQUIRE(vec[0] == 15.0);
|
||||
}
|
||||
|
||||
TEST_CASE("DArenaVector-const-iterators", "[arena][DArenaVector]")
|
||||
{
|
||||
ArenaConfig cfg { .name_ = "testarena",
|
||||
.size_ = 4096 };
|
||||
DArenaVector<double> vec = DArenaVector<double>::map(cfg);
|
||||
|
||||
vec.push_back(1.0);
|
||||
vec.push_back(2.0);
|
||||
vec.push_back(3.0);
|
||||
|
||||
const DArenaVector<double> & cvec = vec;
|
||||
|
||||
REQUIRE(cvec.cbegin() != cvec.cend());
|
||||
REQUIRE(cvec.begin() == cvec.cbegin());
|
||||
REQUIRE(cvec.end() == cvec.cend());
|
||||
|
||||
auto it = cvec.cbegin();
|
||||
REQUIRE(*it == 1.0);
|
||||
++it;
|
||||
REQUIRE(*it == 2.0);
|
||||
}
|
||||
|
||||
TEST_CASE("DArenaVector-range-for", "[arena][DArenaVector]")
|
||||
{
|
||||
ArenaConfig cfg { .name_ = "testarena",
|
||||
.size_ = 4096 };
|
||||
DArenaVector<double> vec = DArenaVector<double>::map(cfg);
|
||||
|
||||
vec.push_back(1.0);
|
||||
vec.push_back(2.0);
|
||||
vec.push_back(3.0);
|
||||
|
||||
// read via range-for
|
||||
double sum = 0.0;
|
||||
for (double x : vec) {
|
||||
sum += x;
|
||||
}
|
||||
REQUIRE(sum == 6.0);
|
||||
|
||||
// modify via range-for
|
||||
for (double & x : vec) {
|
||||
x *= 2.0;
|
||||
}
|
||||
REQUIRE(vec[0] == 2.0);
|
||||
REQUIRE(vec[1] == 4.0);
|
||||
REQUIRE(vec[2] == 6.0);
|
||||
}
|
||||
|
||||
TEST_CASE("DArenaVector-reserve", "[arena][DArenaVector]")
|
||||
{
|
||||
ArenaConfig cfg { .name_ = "testarena",
|
||||
.size_ = 4096 };
|
||||
DArenaVector<double> vec = DArenaVector<double>::map(cfg);
|
||||
|
||||
REQUIRE(vec.size() == 0);
|
||||
REQUIRE(vec.capacity() > 0);
|
||||
|
||||
size_t initial_capacity = vec.capacity();
|
||||
|
||||
// reserve doesn't change size
|
||||
vec.reserve(100);
|
||||
REQUIRE(vec.size() == 0);
|
||||
REQUIRE(vec.capacity() >= 100);
|
||||
|
||||
// add some elements
|
||||
vec.push_back(1.0);
|
||||
vec.push_back(2.0);
|
||||
vec.push_back(3.0);
|
||||
|
||||
REQUIRE(vec.size() == 3);
|
||||
size_t cap_after_push = vec.capacity();
|
||||
|
||||
// reserve more space
|
||||
vec.reserve(200);
|
||||
REQUIRE(vec.size() == 3);
|
||||
REQUIRE(vec.capacity() >= 200);
|
||||
|
||||
// values still intact
|
||||
REQUIRE(vec[0] == 1.0);
|
||||
REQUIRE(vec[1] == 2.0);
|
||||
REQUIRE(vec[2] == 3.0);
|
||||
}
|
||||
|
||||
TEST_CASE("DArenaVector-swap", "[arena][DArenaVector]")
|
||||
{
|
||||
ArenaConfig cfg1 { .name_ = "testarena1",
|
||||
.size_ = 4096 };
|
||||
ArenaConfig cfg2 { .name_ = "testarena2",
|
||||
.size_ = 4096 };
|
||||
|
||||
DArenaVector<double> vec1 = DArenaVector<double>::map(cfg1);
|
||||
DArenaVector<double> vec2 = DArenaVector<double>::map(cfg2);
|
||||
|
||||
vec1.push_back(1.0);
|
||||
vec1.push_back(2.0);
|
||||
|
||||
vec2.push_back(10.0);
|
||||
vec2.push_back(20.0);
|
||||
vec2.push_back(30.0);
|
||||
|
||||
REQUIRE(vec1.size() == 2);
|
||||
REQUIRE(vec2.size() == 3);
|
||||
|
||||
vec1.swap(vec2);
|
||||
|
||||
// sizes swapped
|
||||
REQUIRE(vec1.size() == 3);
|
||||
REQUIRE(vec2.size() == 2);
|
||||
|
||||
// contents swapped
|
||||
REQUIRE(vec1[0] == 10.0);
|
||||
REQUIRE(vec1[1] == 20.0);
|
||||
REQUIRE(vec1[2] == 30.0);
|
||||
|
||||
REQUIRE(vec2[0] == 1.0);
|
||||
REQUIRE(vec2[1] == 2.0);
|
||||
}
|
||||
|
||||
TEST_CASE("DArenaVector-data", "[arena][DArenaVector]")
|
||||
{
|
||||
ArenaConfig cfg { .name_ = "testarena",
|
||||
.size_ = 4096 };
|
||||
DArenaVector<double> vec = DArenaVector<double>::map(cfg);
|
||||
|
||||
vec.push_back(1.0);
|
||||
vec.push_back(2.0);
|
||||
vec.push_back(3.0);
|
||||
|
||||
double * ptr = vec.data();
|
||||
|
||||
// data() points to first element
|
||||
REQUIRE(ptr == &vec[0]);
|
||||
|
||||
// can read via pointer
|
||||
REQUIRE(ptr[0] == 1.0);
|
||||
REQUIRE(ptr[1] == 2.0);
|
||||
REQUIRE(ptr[2] == 3.0);
|
||||
|
||||
// can write via pointer
|
||||
ptr[1] = 99.0;
|
||||
REQUIRE(vec[1] == 99.0);
|
||||
|
||||
// const version
|
||||
const DArenaVector<double> & cvec = vec;
|
||||
const double * cptr = cvec.data();
|
||||
REQUIRE(cptr[0] == 1.0);
|
||||
REQUIRE(cptr[1] == 99.0);
|
||||
REQUIRE(cptr[2] == 3.0);
|
||||
}
|
||||
|
||||
TEST_CASE("DArenaVector-move-ctor", "[arena][DArenaVector]")
|
||||
{
|
||||
ArenaConfig cfg { .name_ = "testarena",
|
||||
.size_ = 4096 };
|
||||
DArenaVector<double> vec1 = DArenaVector<double>::map(cfg);
|
||||
|
||||
vec1.push_back(10.0);
|
||||
vec1.push_back(20.0);
|
||||
vec1.push_back(30.0);
|
||||
|
||||
double * original_data = vec1.data();
|
||||
size_t original_size = vec1.size();
|
||||
|
||||
// move construct vec2 from vec1
|
||||
DArenaVector<double> vec2(std::move(vec1));
|
||||
|
||||
// vec2 has the data
|
||||
REQUIRE(vec2.size() == original_size);
|
||||
REQUIRE(vec2.data() == original_data);
|
||||
REQUIRE(vec2[0] == 10.0);
|
||||
REQUIRE(vec2[1] == 20.0);
|
||||
REQUIRE(vec2[2] == 30.0);
|
||||
|
||||
// vec1 is in valid but moved-from state
|
||||
REQUIRE(vec1.size() == 0);
|
||||
REQUIRE(vec1.empty());
|
||||
}
|
||||
|
||||
// Helper class to track ctor/dtor calls
|
||||
struct LifetimeTracker {
|
||||
static int ctor_count;
|
||||
static int dtor_count;
|
||||
|
||||
static void reset() {
|
||||
ctor_count = 0;
|
||||
dtor_count = 0;
|
||||
}
|
||||
|
||||
int value;
|
||||
|
||||
LifetimeTracker() : value{0} { ++ctor_count; }
|
||||
LifetimeTracker(int v) : value{v} { ++ctor_count; }
|
||||
LifetimeTracker(const LifetimeTracker & other) : value{other.value} { ++ctor_count; }
|
||||
LifetimeTracker(LifetimeTracker && other) : value{other.value} { ++ctor_count; other.value = 0; }
|
||||
~LifetimeTracker() { ++dtor_count; }
|
||||
|
||||
LifetimeTracker & operator=(const LifetimeTracker &) = default;
|
||||
LifetimeTracker & operator=(LifetimeTracker &&) = default;
|
||||
};
|
||||
|
||||
int LifetimeTracker::ctor_count = 0;
|
||||
int LifetimeTracker::dtor_count = 0;
|
||||
|
||||
TEST_CASE("DArenaVector-nontrivial-dtor", "[arena][DArenaVector]")
|
||||
{
|
||||
LifetimeTracker::reset();
|
||||
|
||||
{
|
||||
ArenaConfig cfg { .name_ = "testarena",
|
||||
.size_ = 4096 };
|
||||
DArenaVector<LifetimeTracker> vec = DArenaVector<LifetimeTracker>::map(cfg);
|
||||
|
||||
vec.push_back(LifetimeTracker{1});
|
||||
vec.push_back(LifetimeTracker{2});
|
||||
vec.push_back(LifetimeTracker{3});
|
||||
|
||||
// 3 temp objects created, 3 moved into vector
|
||||
// temps destroyed after push_back
|
||||
REQUIRE(vec.size() == 3);
|
||||
REQUIRE(LifetimeTracker::ctor_count == 6); // 3 temps + 3 moves
|
||||
REQUIRE(LifetimeTracker::dtor_count == 3); // 3 temps destroyed
|
||||
|
||||
// verify values
|
||||
REQUIRE(vec[0].value == 1);
|
||||
REQUIRE(vec[1].value == 2);
|
||||
REQUIRE(vec[2].value == 3);
|
||||
}
|
||||
// vec destroyed, should call dtor for all 3 elements
|
||||
REQUIRE(LifetimeTracker::dtor_count == 6);
|
||||
}
|
||||
|
||||
TEST_CASE("DArenaVector-nontrivial-resize-shrink", "[arena][DArenaVector]")
|
||||
{
|
||||
LifetimeTracker::reset();
|
||||
|
||||
ArenaConfig cfg { .name_ = "testarena",
|
||||
.size_ = 4096 };
|
||||
DArenaVector<LifetimeTracker> vec = DArenaVector<LifetimeTracker>::map(cfg);
|
||||
|
||||
vec.push_back(LifetimeTracker{1});
|
||||
vec.push_back(LifetimeTracker{2});
|
||||
vec.push_back(LifetimeTracker{3});
|
||||
vec.push_back(LifetimeTracker{4});
|
||||
vec.push_back(LifetimeTracker{5});
|
||||
|
||||
REQUIRE(vec.size() == 5);
|
||||
int dtors_before_shrink = LifetimeTracker::dtor_count;
|
||||
|
||||
// shrink from 5 to 2
|
||||
vec.resize(2);
|
||||
|
||||
REQUIRE(vec.size() == 2);
|
||||
// should have called dtor for 3 elements (indices 2,3,4)
|
||||
REQUIRE(LifetimeTracker::dtor_count == dtors_before_shrink + 3);
|
||||
|
||||
// remaining elements intact
|
||||
REQUIRE(vec[0].value == 1);
|
||||
REQUIRE(vec[1].value == 2);
|
||||
}
|
||||
|
||||
TEST_CASE("DArenaVector-nontrivial-clear", "[arena][DArenaVector]")
|
||||
{
|
||||
LifetimeTracker::reset();
|
||||
|
||||
ArenaConfig cfg { .name_ = "testarena",
|
||||
.size_ = 4096 };
|
||||
DArenaVector<LifetimeTracker> vec = DArenaVector<LifetimeTracker>::map(cfg);
|
||||
|
||||
vec.push_back(LifetimeTracker{1});
|
||||
vec.push_back(LifetimeTracker{2});
|
||||
vec.push_back(LifetimeTracker{3});
|
||||
|
||||
REQUIRE(vec.size() == 3);
|
||||
int dtors_before_clear = LifetimeTracker::dtor_count;
|
||||
|
||||
vec.clear();
|
||||
|
||||
REQUIRE(vec.size() == 0);
|
||||
REQUIRE(vec.empty());
|
||||
// should have called dtor for all 3 elements
|
||||
REQUIRE(LifetimeTracker::dtor_count == dtors_before_clear + 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* end DArenaVector.test.cpp */
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
/** @file DCircularBuffer.test.cpp
|
||||
*
|
||||
* @author Roland Conybeare, Jan 2026
|
||||
**/
|
||||
|
||||
#include "DCircularBuffer.hpp"
|
||||
#include "print.hpp"
|
||||
#include <xo/indentlog/print/tag.hpp>
|
||||
#include <catch2/catch.hpp>
|
||||
#include <unistd.h> // for getpagesize() on osx
|
||||
|
||||
namespace xo {
|
||||
using xo::mm::DCircularBuffer;
|
||||
using xo::mm::CircularBufferConfig;
|
||||
using xo::mm::span;
|
||||
using std::byte;
|
||||
|
||||
namespace ut {
|
||||
TEST_CASE("DCircularBuffer-tiny", "[arena][DCircularBuffer]")
|
||||
{
|
||||
// buffer works with bytes, not chars
|
||||
|
||||
CircularBufferConfig cfg { .name_ = "testcbuf",
|
||||
.max_capacity_ = 1 };
|
||||
DCircularBuffer buf = DCircularBuffer::map(cfg);
|
||||
|
||||
REQUIRE(buf.reserved_range().size() == getpagesize());
|
||||
REQUIRE(buf.mapped_range().size() == 0);
|
||||
REQUIRE(buf.occupied_range().size() == 0);
|
||||
REQUIRE(buf.input_range().size() == 0);
|
||||
|
||||
REQUIRE(buf.verify_ok(verify_policy::log_only()));
|
||||
REQUIRE(buf.get_append_span(1).size() == getpagesize());
|
||||
REQUIRE(buf.mapped_range().size() == getpagesize());
|
||||
REQUIRE(buf.occupied_range().size() == 0);
|
||||
REQUIRE(buf.input_range().size() == 0);
|
||||
|
||||
auto s0 = DCircularBuffer::const_span_type::from_cstr("abcdefghijk");
|
||||
/* return value is unaccepted suffix of input */
|
||||
REQUIRE(buf.append(s0).empty());
|
||||
REQUIRE(buf.verify_ok(verify_policy::log_only()));
|
||||
REQUIRE(buf.mapped_range().size() == getpagesize());
|
||||
REQUIRE(buf.occupied_range().size() == s0.size());
|
||||
REQUIRE(buf.input_range().size() == s0.size());
|
||||
|
||||
auto s1 = DCircularBuffer::const_span_type::from_cstr("lmnopq");
|
||||
REQUIRE(buf.append(s1).empty());
|
||||
REQUIRE(buf.mapped_range().size() == getpagesize());
|
||||
REQUIRE(buf.occupied_range().size() == s0.size() + s1.size());
|
||||
|
||||
REQUIRE(buf.occupied_range().to_string_view() == std::string_view("abcdefghijklmnopq"));
|
||||
|
||||
buf.consume(buf.occupied_range().prefix(3));
|
||||
|
||||
REQUIRE(buf.occupied_range().to_string_view() == std::string_view("defghijklmnopq"));
|
||||
}
|
||||
|
||||
// TODO: test pin_range() / unpin_range()
|
||||
} /*namespace ut*/
|
||||
} /*namespace xo*/
|
||||
|
||||
/* end DCircularBuffer.test.cpp */
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
/* file arena_utest_main.cpp */
|
||||
|
||||
#define CATCH_CONFIG_MAIN
|
||||
#include "catch2/catch.hpp"
|
||||
|
||||
/* end arena_utest_main.cpp */
|
||||
|
|
@ -1,714 +0,0 @@
|
|||
/* @file random_hash_ops.hpp **/
|
||||
|
||||
#include <xo/randomgen/xoshiro256.hpp>
|
||||
#include <xo/indentlog/scope.hpp>
|
||||
#include <xo/indentlog/print/tag.hpp>
|
||||
#include <xo/indentlog/print/vector.hpp>
|
||||
#include <catch2/catch.hpp>
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace utest {
|
||||
struct Util {
|
||||
/* generate vector with integers [0.. n-1] */
|
||||
static std::vector<std::uint32_t> vector_upto(std::uint32_t n) {
|
||||
std::vector<std::uint32_t> u(n);
|
||||
for (std::uint32_t i = 0; i < n; ++i)
|
||||
u[i] = i;
|
||||
|
||||
return u;
|
||||
} /*vector_upto*/
|
||||
|
||||
static std::map<std::uint32_t, std::uint32_t>
|
||||
map_upto(std::uint32_t n)
|
||||
{
|
||||
std::map<std::uint32_t, std::uint32_t> m;
|
||||
for(std::uint32_t i=0; i<n; ++i) {
|
||||
m[i] = i;
|
||||
}
|
||||
|
||||
return m;
|
||||
} /*map_upto*/
|
||||
|
||||
/* generate random permutation of integers [0.. n-1] */
|
||||
static std::vector<uint32_t>
|
||||
random_permutation(uint32_t n, xo::rng::xoshiro256ss *p_rgen) {
|
||||
/* vector [0 .. n-1] */
|
||||
std::vector<uint32_t> u = vector_upto(n);
|
||||
|
||||
/* shuffle to get unpredictable permutation */
|
||||
std::shuffle(u.begin(), u.end(), *p_rgen);
|
||||
|
||||
return u;
|
||||
} /*random_permutation*/
|
||||
}; /*Util*/
|
||||
|
||||
// TODO: move REQUIRE_OR_CAPTURE(), REQUIRE_ORFAIL() to new subsystem xo-utestutil
|
||||
|
||||
/* note: trivial REQUIRE() call in else branch bc we still want
|
||||
* catch2 to count assertions when verification succeeds
|
||||
*/
|
||||
# define REQUIRE_ORCAPTURE(ok_flag, catch_flag, expr) \
|
||||
if (catch_flag) { \
|
||||
REQUIRE((expr)); \
|
||||
} else { \
|
||||
REQUIRE(true); \
|
||||
ok_flag &= (expr); \
|
||||
}
|
||||
|
||||
# define REQUIRE_ORFAIL(ok_flag, catch_flag, expr) \
|
||||
REQUIRE_ORCAPTURE(ok_flag, catch_flag, expr); \
|
||||
if (!ok_flag) \
|
||||
return ok_flag
|
||||
|
||||
/** UtestTools
|
||||
**/
|
||||
struct UtestTools {
|
||||
/** bimodal may run twice:
|
||||
* - first mode is silent, only determines success or failure.
|
||||
* - second mode skipped when first mode succeeds.
|
||||
* when first mode fails, second mode runs noisily with debug logging enabled
|
||||
*
|
||||
* goal is to get detailed information from failing test;
|
||||
* more detailed than feasible from catch2 INFO()
|
||||
*
|
||||
* test function should use REQUIRE_ORCAPTURE() / REQUIRE_ORFAIL().
|
||||
* It should *not* use REQUIRE() or CHECK().
|
||||
*
|
||||
* @p test_name banner for initial log message (only printed on 2nd pass)
|
||||
* @p test_fn function to invoke test pass.
|
||||
* @p n test size/id (cosmetic - printed in log messages)
|
||||
**/
|
||||
static inline bool bimodal_test(std::string test_name,
|
||||
std::function<bool (bool dbg_flag, std::uint32_t n)> test_fn,
|
||||
std::uint32_t n)
|
||||
{
|
||||
bool ok_flag = false;
|
||||
|
||||
for (std::uint32_t attention = 0; !ok_flag && (attention < 2); ++attention) {
|
||||
bool debug_flag = (attention == 1);
|
||||
|
||||
xo::scope log(XO_DEBUG2(debug_flag, test_name));
|
||||
|
||||
ok_flag = test_fn(debug_flag, n);
|
||||
}
|
||||
|
||||
return ok_flag;
|
||||
}
|
||||
};
|
||||
|
||||
/* compare xo-ordinaltree/utest/random_tree_ops.hpp */
|
||||
template <typename HashMap>
|
||||
struct HashMapUtil : public Util {
|
||||
#ifdef NOT_YET
|
||||
static bool
|
||||
test_clear(bool catch_flag,
|
||||
Tree * p_tree)
|
||||
{
|
||||
bool ok_flag = true;
|
||||
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, p_tree->verify_ok(catch_flag));
|
||||
|
||||
p_tree->clear();
|
||||
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, p_tree->verify_ok(catch_flag));
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, p_tree->empty());
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, p_tree->size() == 0);
|
||||
|
||||
return ok_flag;
|
||||
} /*test_clear*/
|
||||
#endif
|
||||
|
||||
static bool
|
||||
random_inserts(const std::vector<typename HashMap::key_type> & keys,
|
||||
bool catch_flag,
|
||||
xo::rng::xoshiro256ss * p_rgen,
|
||||
HashMap * p_map)
|
||||
{
|
||||
using xo::xtag;
|
||||
|
||||
bool ok_flag = true;
|
||||
|
||||
xo::scope log(XO_DEBUG(catch_flag), xtag("n-keys", keys.size()));
|
||||
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, p_map->verify_ok(catch_flag));
|
||||
|
||||
/* n keys */
|
||||
std::size_t n = keys.size();
|
||||
/* permute keys, remembering original position */
|
||||
std::vector<std::pair<std::size_t, typename HashMap::key_type>> permuted_keys(n);
|
||||
{
|
||||
uint32_t i = 0;
|
||||
for (const auto & x : keys) {
|
||||
permuted_keys[i] = std::make_pair(i, x);
|
||||
}
|
||||
}
|
||||
/* shuffle to get unpredictable insert order */
|
||||
std::shuffle(keys.begin(), keys.end(), *p_rgen);
|
||||
|
||||
size_t tree_z0 = p_map->size();
|
||||
|
||||
/* insert keys in permuted order */
|
||||
{
|
||||
uint32_t i = 1;
|
||||
for(const auto & pr_i : permuted_keys) {
|
||||
log && log(xtag("i", i), xtag("ord", pr_i.first), xtag("n", n), xtag("key", pr_i.second));
|
||||
|
||||
/* .first: iterator @ insert position
|
||||
* .second: true if insert occurred (ịẹ tree size incremented)
|
||||
*/
|
||||
auto insert_result = p_map->insert(typename HashMap::value_type(pr_i.second, 10.0 * i));
|
||||
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, p_map->verify_ok(catch_flag));
|
||||
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, insert_result.second);
|
||||
|
||||
/* verify: iterator returned by Treẹinsert(), refers to inserted key,value pair */
|
||||
log && log(xtag("iter.node", insert_result.first.node()));
|
||||
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, insert_result.first->first == pr_i.second);
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, insert_result.first->second == 10.0 * i);
|
||||
|
||||
++i;
|
||||
}
|
||||
}
|
||||
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, p_map->size() == tree_z0 + n);
|
||||
|
||||
return ok_flag;
|
||||
}
|
||||
|
||||
/* do
|
||||
* n = (hi - lo) / k
|
||||
* random inserts (taken from *p_rgen) into *p_rbtreẹ
|
||||
* inserted keys will comprise the distinct values
|
||||
* {lo, lo+k, lo+2k, ..., lo+n.k}
|
||||
*/
|
||||
static bool
|
||||
random_inserts(std::uint32_t lo,
|
||||
std::uint32_t hi,
|
||||
std::uint32_t k,
|
||||
bool catch_flag,
|
||||
xo::rng::xoshiro256ss * p_rgen,
|
||||
HashMap * p_map)
|
||||
{
|
||||
// TODO: rewrite in terms of 'random_inserts with explicit vector'.
|
||||
|
||||
using xo::xtag;
|
||||
|
||||
bool ok_flag = true;
|
||||
|
||||
xo::scope log(XO_DEBUG(catch_flag), xtag("lo", lo), xtag("hi", hi), xtag("k", k));
|
||||
|
||||
auto policy = xo::verify_policy::chatty();
|
||||
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, p_map->verify_ok(policy));
|
||||
|
||||
if ((hi <= lo) || (k == 0))
|
||||
return true;
|
||||
|
||||
uint32_t n = (hi - lo) / k;
|
||||
|
||||
/* n keys 0..n-1 */
|
||||
std::vector<std::uint32_t> u(n);
|
||||
for(std::uint32_t i=0; i<n; ++i)
|
||||
u[i] = lo + i*k;
|
||||
|
||||
/* shuffle to get unpredictable insert order */
|
||||
std::shuffle(u.begin(), u.end(), *p_rgen);
|
||||
|
||||
size_t tree_z0 = p_map->size();
|
||||
|
||||
/* insert keys according to permutation u */
|
||||
uint32_t i = 1;
|
||||
for(uint32_t x : u) {
|
||||
log && log(xtag("i", i), xtag("n", n), xtag("key", x));
|
||||
/* .first: iterator @ insert position
|
||||
* .second: true if insert occurred (ịẹ tree size incremented)
|
||||
*/
|
||||
auto insert_result = p_map->try_insert(typename HashMap::value_type(x, 10 * x));
|
||||
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, p_map->verify_ok(policy));
|
||||
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, insert_result.second);
|
||||
|
||||
/* verify: iterator returned by Treẹinsert(), refers to inserted key,value pair */
|
||||
//log && log(xtag("iter.node", insert_result.first.node()));
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, insert_result.first->first == x);
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, insert_result.first->second == 10 * x);
|
||||
|
||||
++i;
|
||||
}
|
||||
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, p_map->size() == tree_z0 + n);
|
||||
|
||||
return ok_flag;
|
||||
} /*random_inserts*/
|
||||
|
||||
static bool
|
||||
random_inserts(std::uint32_t n,
|
||||
bool catch_flag,
|
||||
xo::rng::xoshiro256ss * p_rgen,
|
||||
HashMap * p_map)
|
||||
{
|
||||
return random_inserts(0, n, 1, catch_flag, p_rgen, p_map);
|
||||
}
|
||||
|
||||
#ifdef NOT_YET
|
||||
/* do n random removes (taken from *p_rgen) from *p_rbtree;
|
||||
* assumes *p_rbtree has keys [0 .. n-1] where n=p_rbtreẹsize
|
||||
*/
|
||||
static bool
|
||||
random_removes(bool catch_flag, // dbg_flag
|
||||
xo::rng::xoshiro256ss * p_rgen,
|
||||
Tree * p_map)
|
||||
{
|
||||
using xo::scope;
|
||||
using xo::xtag;
|
||||
|
||||
bool ok_flag = true;
|
||||
|
||||
xo::scope log(XO_DEBUG(catch_flag));
|
||||
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, p_map->verify_ok(catch_flag));
|
||||
|
||||
uint32_t n = p_map->size();
|
||||
|
||||
/* random permutation of keys in *p_map */
|
||||
std::vector<std::uint32_t> u
|
||||
= random_permutation(n, p_rgen);
|
||||
|
||||
log && log(xtag("remove-order", u));
|
||||
|
||||
/* will keep track of which keys remain as we move them */
|
||||
std::map<std::uint32_t, std::uint32_t> m = Util::map_upto(n);
|
||||
|
||||
/* remove keys in permutation order */
|
||||
std::uint32_t i = 1;
|
||||
for (std::uint32_t x : u) {
|
||||
log && log("iter i: removing key from n-node tree",
|
||||
xtag("i", i), xtag("key", x), xtag("n", n));
|
||||
|
||||
/* remove x from tracking map m also */
|
||||
m.erase(x);
|
||||
|
||||
log && log("remove key :iter ", i, "/", n, xtag("key", x));
|
||||
|
||||
p_map->erase(x);
|
||||
// rbtreẹdisplay();
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, p_map->size() == n-i);
|
||||
/* amongst other things, this guarantees that keys in *p_map
|
||||
* appear in increasing order
|
||||
*/
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, p_map->verify_ok(catch_flag));
|
||||
|
||||
#ifdef NOT_YET
|
||||
/* 1. rbtree should now contain all the keys in [0..n-1],
|
||||
* with u[0]..u[i-1] excluded; this is the same as the
|
||||
* contents of m.
|
||||
*/
|
||||
auto m_ix = m.begin();
|
||||
auto m_end_ix = m.end();
|
||||
auto visitor_fn =
|
||||
([&m_ix, m_end_ix]
|
||||
(std::pair<int, double> const & contents)
|
||||
{
|
||||
REQUIRE(m_ix != m_end_ix);
|
||||
REQUIRE(contents.first == m_ix->second);
|
||||
++m_ix;
|
||||
});
|
||||
p_map->visit_inorder(visitor_fn);
|
||||
#endif
|
||||
++i;
|
||||
}
|
||||
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, m.empty());
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, p_map->size() == 0);
|
||||
|
||||
log.end_scope();
|
||||
|
||||
return ok_flag;
|
||||
} /*random_removes*/
|
||||
#endif
|
||||
|
||||
/* Require:
|
||||
* - map has keys [0..n-1], where n=map.size()
|
||||
* - for each key k, associated value is dvalue+10*k
|
||||
*/
|
||||
static bool
|
||||
random_lookups(uint32_t dvalue,
|
||||
bool catch_flag,
|
||||
xo::rng::xoshiro256ss * p_rgen,
|
||||
HashMap & map)
|
||||
{
|
||||
using xo::scope;
|
||||
using xo::xtag;
|
||||
|
||||
xo::scope log(XO_DEBUG(catch_flag));
|
||||
|
||||
/* -> false if/when verification fails */
|
||||
bool ok_flag = true;
|
||||
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, map.verify_ok());
|
||||
|
||||
size_t n = map.size();
|
||||
std::vector<std::uint32_t> u
|
||||
= random_permutation(n, p_rgen);
|
||||
|
||||
/* lookup keys in permutation order */
|
||||
std::uint32_t i = 1;
|
||||
for (std::uint32_t x : u) {
|
||||
INFO(tostr(xtag("i", i), xtag("n", n), xtag("x", x)));
|
||||
|
||||
auto find_ix = map.find(x);
|
||||
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, find_ix != map.end());
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, find_ix->first == x);
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, find_ix->second == dvalue + x*10);
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, map.verify_ok());
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, map.size() == n);
|
||||
|
||||
++i;
|
||||
}
|
||||
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, map.size() == n);
|
||||
|
||||
log.end_scope();
|
||||
|
||||
return ok_flag;
|
||||
} /*random_lookups*/
|
||||
|
||||
/* Require:
|
||||
* - hash has keys [0..n-1] where n=map size
|
||||
* - hash value at key k is dvalue+10*k
|
||||
*/
|
||||
static bool
|
||||
check_forward_iterator(uint32_t dvalue,
|
||||
bool catch_flag,
|
||||
HashMap & map)
|
||||
{
|
||||
using xo::scope;
|
||||
using xo::xtag;
|
||||
|
||||
/* -> flase if/when verification fails */
|
||||
bool ok_flag = true;
|
||||
|
||||
std::size_t const n = map.size();
|
||||
|
||||
scope log(XO_DEBUG(catch_flag));
|
||||
|
||||
log && log("map with size n", xtag("n", n));
|
||||
|
||||
std::unordered_set<std::size_t> keys;
|
||||
|
||||
{
|
||||
auto end_ix = map.end();
|
||||
|
||||
//log && log(xtag("end_ix", end_ix));
|
||||
|
||||
auto begin_ix = map.begin();
|
||||
auto ix = begin_ix;
|
||||
|
||||
int last_key = -1;
|
||||
|
||||
while (ix != end_ix) {
|
||||
log && log("forward loop top"
|
||||
//xtag("ix", ix)
|
||||
);
|
||||
|
||||
/* verify: keys in map are in [0 .. n) */
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, 0 <= ix->first);
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, ix->first < n);
|
||||
|
||||
/* verify: keys in map are unique */
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, !keys.contains(ix->first));
|
||||
keys.insert(ix->first);
|
||||
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, ix->second == dvalue + 10 * ix->first);
|
||||
|
||||
last_key = ix->first;
|
||||
++ix;
|
||||
|
||||
log && log("forward loop bottom",
|
||||
xtag("last_key", last_key)
|
||||
//xtag("next ix", ix)
|
||||
);
|
||||
}
|
||||
|
||||
/* should have visited exactly n locations */
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, map.size() == keys.size());
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, ix == end_ix);
|
||||
|
||||
//log && log(xtag("ix", ix), xtag("begin_ix", begin_ix));
|
||||
}
|
||||
|
||||
return ok_flag;
|
||||
}
|
||||
|
||||
/* Require:
|
||||
* - hash has keys [0..n-1] where n=map size
|
||||
* - hash value at key k is dvalue+10*k
|
||||
*/
|
||||
static bool
|
||||
check_backward_iterator(uint32_t dvalue,
|
||||
bool catch_flag,
|
||||
HashMap & map)
|
||||
{
|
||||
using xo::scope;
|
||||
using xo::xtag;
|
||||
|
||||
/* -> flase if/when verification fails */
|
||||
bool ok_flag = true;
|
||||
|
||||
std::size_t const n = map.size();
|
||||
|
||||
scope log(XO_DEBUG(catch_flag));
|
||||
|
||||
log && log("map with size n", xtag("n", n));
|
||||
|
||||
std::unordered_set<std::size_t> keys;
|
||||
|
||||
{
|
||||
auto end_ix = map.end();
|
||||
|
||||
//log && log(xtag("end_ix", end_ix));
|
||||
|
||||
auto begin_ix = map.begin();
|
||||
auto ix = end_ix;
|
||||
|
||||
if (ix == begin_ix) [[unlikely]] {
|
||||
return ok_flag;
|
||||
}
|
||||
|
||||
while (ix != begin_ix) {
|
||||
log && log("backward loop top",
|
||||
xtag("n", n)
|
||||
);
|
||||
|
||||
--ix;
|
||||
|
||||
/* verify: keys in map are in [0 .. n) */
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, 0 <= ix->first);
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, ix->first < n);
|
||||
|
||||
log && log(xtag("ix->first", ix->first));
|
||||
|
||||
/* verify: keys in map are unique */
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, !keys.contains(ix->first));
|
||||
keys.insert(ix->first);
|
||||
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, ix->second == dvalue + 10 * ix->first);
|
||||
}
|
||||
|
||||
/* should have visited exactly n locations */
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, map.size() == keys.size());
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, ix == begin_ix);
|
||||
|
||||
//log && log(xtag("ix", ix), xtag("begin_ix", begin_ix));
|
||||
}
|
||||
|
||||
return ok_flag;
|
||||
}
|
||||
|
||||
#ifdef NOT_YET
|
||||
/* Require:
|
||||
* - tree has keys [0..n-1], where n=treẹsize()
|
||||
* - tree values at key k is dvalue+10*k
|
||||
*
|
||||
* catch_flag. true -> log to console + interact with catch2
|
||||
* false -> verify iteration behavior for return code
|
||||
*/
|
||||
static bool
|
||||
check_bidirectional_iterator(uint32_t dvalue,
|
||||
bool catch_flag,
|
||||
Tree const & tree)
|
||||
{
|
||||
using xo::scope;
|
||||
using xo::xtag;
|
||||
|
||||
/* -> false if/when verification fails */
|
||||
bool ok_flag = true;
|
||||
|
||||
std::size_t const n = tree.size();
|
||||
|
||||
xo::scope log(XO_DEBUG(catch_flag));
|
||||
|
||||
log && log("tree with size n", xtag("n", n));
|
||||
|
||||
{
|
||||
std::size_t i = 0;
|
||||
|
||||
auto end_ix = tree.end();
|
||||
|
||||
log && log(xtag("end_ix", end_ix));
|
||||
|
||||
auto begin_ix = tree.begin();
|
||||
auto ix = begin_ix;
|
||||
|
||||
int last_key = -1;
|
||||
|
||||
while (ix != end_ix) {
|
||||
log && log("forward loop top",
|
||||
xtag("i", i),
|
||||
xtag("ix", ix));
|
||||
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, ix->first == i);
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, ix->second == dvalue + 10*i);
|
||||
if(i > 0) {
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, ix->first > last_key);
|
||||
}
|
||||
last_key = ix->first;
|
||||
++i;
|
||||
++ix;
|
||||
|
||||
log && log("forward loop bottom",
|
||||
xtag("last_key", last_key),
|
||||
xtag("next ix", ix));
|
||||
}
|
||||
|
||||
/* should have visited exactly n locations */
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, i == n);
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, ix == end_ix);
|
||||
|
||||
log && log(xtag("ix", ix), xtag("begin_ix", begin_ix));
|
||||
|
||||
/* now run iterator backwards,
|
||||
* starting from "one past the end"
|
||||
*/
|
||||
if(ix != begin_ix) {
|
||||
do {
|
||||
--i;
|
||||
--ix;
|
||||
|
||||
log && log("forward backup",
|
||||
xtag("i", i),
|
||||
xtag("ix", ix));
|
||||
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, ix.is_dereferenceable());
|
||||
|
||||
log && log(xtag("ix.first", (*ix).first));
|
||||
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, (*ix).first == i);
|
||||
} while (ix != begin_ix);
|
||||
}
|
||||
|
||||
/* should have visited exactly n locations in reverse */
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, i == 0);
|
||||
}
|
||||
|
||||
/* ----- reverse iterators ----- */
|
||||
|
||||
{
|
||||
std::int64_t i = n - 1;
|
||||
|
||||
auto rbegin_ix = tree.rbegin();
|
||||
auto rend_ix = tree.rend();
|
||||
|
||||
auto rix = rbegin_ix;
|
||||
|
||||
int last_key = -1;
|
||||
|
||||
while (rix != rend_ix) {
|
||||
log && log("reverse loop top",
|
||||
xtag("i", i),
|
||||
xtag("rix", rix));
|
||||
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, rix->first == i);
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, rix->second == dvalue + 10*i);
|
||||
if (i < n-1) {
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, rix->first < last_key);
|
||||
}
|
||||
last_key = rix->first;
|
||||
--i;
|
||||
++rix;
|
||||
|
||||
log && log("reverse loop bottom",
|
||||
xtag("last_key", last_key),
|
||||
xtag("next ix", rix));
|
||||
}
|
||||
|
||||
/* should have visited exactly n locations */
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, i == -1);
|
||||
|
||||
log && log(xtag("rbegin_ix", rbegin_ix));
|
||||
|
||||
/* now run reverse iterator backwrds,
|
||||
* starting from "one before the beginning"
|
||||
*/
|
||||
if (rix != rbegin_ix) {
|
||||
do {
|
||||
++i;
|
||||
--rix;
|
||||
|
||||
log && log("reverse backup",
|
||||
xtag("i", i),
|
||||
xtag("rix", rix),
|
||||
xtag("rix.first", rix->first));
|
||||
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, (*rix).first == i);
|
||||
} while (rix != rbegin_ix);
|
||||
}
|
||||
|
||||
/* should have visited exactly n locations in reversê2 */
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, i == n - 1);
|
||||
}
|
||||
|
||||
log.end_scope();
|
||||
|
||||
return ok_flag;
|
||||
} /*check_bidirectional_iterator*/
|
||||
#endif
|
||||
|
||||
#ifdef NOT_YET
|
||||
/* Require:
|
||||
* - *p_rbtree has keys [0..n-1], where n=rbtree.size()
|
||||
* - for each key k, associated value is 10*k
|
||||
*
|
||||
* Promise:
|
||||
* - for each key k, associated value is dvalue + 10*k
|
||||
*/
|
||||
static bool
|
||||
random_updates(uint32_t dvalue,
|
||||
bool catch_flag,
|
||||
Tree * p_rbtree,
|
||||
xo::rng::xoshiro256ss * p_rgen)
|
||||
{
|
||||
using xo::scope;
|
||||
using xo::xtag;
|
||||
|
||||
scope log(XO_DEBUG(catch_flag));
|
||||
|
||||
/* -> false if/when check fails */
|
||||
bool ok_flag = true;
|
||||
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, p_rbtree->verify_ok());
|
||||
|
||||
std::size_t n = p_rbtree->size();
|
||||
std::vector<uint32_t> u
|
||||
= Util::random_permutation(n, p_rgen);
|
||||
|
||||
/* update key/value pairs in permutation order */
|
||||
uint32_t i = 1;
|
||||
for (uint32_t x : u) {
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, (*p_rbtree)[x] == x*10);
|
||||
|
||||
(*p_rbtree)[x] = dvalue + 10*x;
|
||||
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, (*p_rbtree)[x] == dvalue + 10*x);
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, p_rbtree->verify_ok());
|
||||
/* assignment to existing key does not change tree size */
|
||||
REQUIRE_ORFAIL(ok_flag, catch_flag, p_rbtree->size() == n);
|
||||
++i;
|
||||
}
|
||||
|
||||
REQUIRE(p_rbtree->size() == n);
|
||||
|
||||
return ok_flag;
|
||||
} /*random_updates*/
|
||||
#endif
|
||||
}; /*TreeUtil*/
|
||||
} /*namespace utest*/
|
||||
|
||||
/* end random_tree_ops.hpp */
|
||||
Loading…
Add table
Add a link
Reference in a new issue