1.. _seed-0110: 2 3================================== 40110: Memory Allocation Interfaces 5================================== 6.. seed:: 7 :number: 110 8 :name: Memory Allocation Interfaces 9 :status: Accepted 10 :proposal_date: 2023-09-06 11 :cl: 168772 12 :authors: Alexei Frolov 13 :facilitator: Taylor Cramer 14 15------- 16Summary 17------- 18With dynamic memory allocation becoming more common in embedded devices, Pigweed 19should provide standardized interfaces for working with different memory 20allocators, giving both upstream and downstream developers the flexibility to 21move beyond manually sizing their modules' resource pools. 22 23---------- 24Motivation 25---------- 26Traditionally, most embedded firmware have laid out their systems' memory usage 27statically, with every component's buffers and resources set at compile time. 28As systems grow larger and more complex, the ability to dynamically allocate 29memory has become more desirable by firmware authors. 30 31Pigweed has made basic explorations into dynamic allocation in the past: the 32``pw_allocator`` provides basic building blocks which projects can use to 33assemble their own allocators. ``pw_allocator`` also provides a proof-of-concept 34allocator (``FreeListHeap``) based off of these building blocks. 35 36Since then, Pigweed has become a part of more complex projects and has 37developed more advanced capabilities into its own modules. As the project has 38progressed, several developers --- both on the core and customer teams --- have 39noted areas where dynamic allocation would simplify code and improve memory 40usage by enabling sharing and eliminating large reservations. 41 42-------- 43Proposal 44-------- 45 46Allocator Interfaces 47==================== 48The core of the ``pw_allocator`` module will define abstract interfaces for 49memory allocation. Several interfaces are provided with different allocator 50properties, all of which inherit from a base generic ``Allocator``. 51 52Core Allocators 53--------------- 54 55Allocator 56^^^^^^^^^ 57 58.. code-block:: c++ 59 60 class Allocator { 61 public: 62 class Layout { 63 public: 64 constexpr Layout( 65 size_t size, std::align_val_t alignment = alignof(std::max_align_t)) 66 : size_(size), alignment_(alignment) {} 67 68 // Creates a Layout for the given type. 69 template <typename T> 70 static constexpr Layout Of() { 71 return Layout(sizeof(T), alignof(T)); 72 } 73 74 size_t size() const { return size_; } 75 size_t alignment() const { return alignment_; } 76 77 private: 78 size_t size_; 79 size_t alignment_; 80 }; 81 82 template <typename T, typename... Args> 83 T* New(Args&&... args); 84 85 template <typename T> 86 void Delete(T* obj); 87 88 template <typename T, typename... Args> 89 std::optional<UniquePtr<T>> MakeUnique(Args&&... args); 90 91 void* Allocate(Layout layout) { 92 return DoAllocate(layout); 93 } 94 95 void Deallocate(void* ptr, Layout layout) { 96 return DoDeallocate(layout); 97 } 98 99 bool Resize(void* ptr, Layout old_layout, size_t new_size) { 100 if (ptr == nullptr) { 101 return false; 102 } 103 return DoResize(ptr, old_layout, new_size); 104 } 105 106 void* Reallocate(void* ptr, Layout old_layout, size_t new_size) { 107 return DoReallocate(void* ptr, Layout old_layout, size_t new_size); 108 } 109 110 protected: 111 virtual void* DoAllocate(Layout layout) = 0; 112 virtual void DoDeallocate(void* ptr, Layout layout) = 0; 113 114 virtual bool DoResize(void* ptr, Layout old_layout, size_t new_size) { 115 return false; 116 } 117 118 virtual void* DoReallocate(void* ptr, Layout old_layout, size_t new_size) { 119 if (new_size == 0) { 120 DoDeallocate(ptr, old_layout); 121 return nullptr; 122 } 123 124 if (DoResize(ptr, old_layout, new_size)) { 125 return ptr; 126 } 127 128 void* new_ptr = DoAllocate(new_layout); 129 if (new_ptr == nullptr) { 130 return nullptr; 131 } 132 133 if (ptr != nullptr && old_layout.size() != 0) { 134 std::memcpy(new_ptr, ptr, std::min(old_layout.size(), new_size)); 135 DoDeallocate(ptr, old_layout); 136 } 137 138 return new_ptr; 139 } 140 }; 141 142``Allocator`` is the most generic and fundamental interface provided by the 143module, representing any object capable of dynamic memory allocation. 144 145The ``Allocator`` interface makes no guarantees about its implementation. 146Consumers of the generic interface must not make any assumptions around 147allocator behavior, thread safety, or performance. 148 149**Layout** 150 151Allocation parameters are passed to the allocator through a ``Layout`` object. 152This object ensures that the values provided to the allocator are valid, as well 153as providing some convenient helper functions for common allocation use cases, 154such as allocating space for a specific type of object. 155 156**Virtual functions** 157 158Implementers of the allocator interface are responsible for providing the 159following operations: 160 161* ``DoAllocate`` (required): Obtains a block of memory from the allocator with a 162 requested size and power-of-two alignment. Returns ``nullptr`` if the 163 allocation cannot be performed. 164 165 The size and alignment values in the provided layout are guaranteed to be 166 valid. 167 168 Memory returned from ``DoAllocate`` is uninitialized. 169 170* ``DoDeallocate`` (required): Releases a block of memory back to the allocator. 171 172 If ``ptr`` is ``nullptr``, does nothing. 173 174 If ``ptr`` was not previously obtained from this allocator the behavior is 175 undefined. 176 177* ``DoResize`` (optional): Extends or shrinks a previously-allocated block of 178 memory in place. If this operation cannot be performed, returns ``false``. 179 180 ``ptr`` is guaranteed to be non-null. If ``ptr`` was not previously obtained 181 from this allocator the behavior is undefined. 182 183 If the allocated block is grown, the memory in the extended region is 184 uninitialized. 185 186* ``DoReallocate`` (optional): Extends or shrinks a previously-allocated block 187 of memory, potentially copying its data to a different location. A default 188 implementation is provided, which first attempts to call ``Resize``, falling 189 back to allocating a new block and copying data if it fails. 190 191 If ``ptr`` is ``nullptr``, behaves identically to ``Allocate(new_layout)``. 192 193 If the new block cannot be allocated, returns ``nullptr``, leaving the 194 original allocation intact. 195 196 If ``new_layout.size == 0``, frees the old block and returns ``nullptr``. 197 198 If the allocated block is grown, the memory in the extended region is 199 uninitialized. 200 201**Provided functions** 202 203* ``New``: Allocates memory for an object from the allocator and constructs it. 204 205* ``Delete``: Destructs and releases memory for a previously-allocated object. 206 207* ``MakeUnique``: Allocates and constructs an object wrapped in a ``UniquePtr`` 208 which owns it and manages its release. 209 210Allocator Utilities 211=================== 212In addition to allocator interfaces, ``pw_allocator`` will provide utilities for 213working with allocators in a system. 214 215UniquePtr 216--------- 217``pw::allocator::UniquePtr`` is a "smart pointer" analogous to 218``std::unique_ptr``, designed to work with Pigweed allocators. It owns and 219manages an allocated object, automatically deallocating its memory when it goes 220out of scope. 221 222Unlike ``std::unique_ptr``, Pigweed's ``UniquePtr`` cannot be manually 223constructed from an existing non-null pointer; it must be done through the 224``Allocator::MakeUnique`` API. This is required as the allocator associated with 225the object allocation must be known in order to release it. 226 227Usage reporting 228--------------- 229``pw_allocator`` will not require any usage reporting as part of its core 230interfaces to keep them minimal and reduce implementation burden. 231 232However, ``pw_allocator`` encourages setting up reporting and will provide 233utilities for doing so. Initially, this consists of a layered proxy allocator 234which wraps another allocator implementation with basic usage reporting through 235``pw_metric``. 236 237.. code-block:: c++ 238 239 class AllocatorMetricProxy : public Allocator { 240 public: 241 constexpr explicit AllocatorMetricProxy(metric::Token token) 242 : memusage_(token) {} 243 244 // Sets the wrapped allocator. 245 void Initialize(Allocator& allocator); 246 247 // Exposed usage statistics. 248 metric::Group& memusage() { return memusage_; } 249 size_t used() const { return used_.value(); } 250 size_t peak() const { return peak_.value(); } 251 size_t count() const { return count_.value(); } 252 253 // Implements the Allocator interface by forwarding through to the 254 // sub-allocator provided to Initialize. 255 256 }; 257 258Integration with C++ polymorphic memory resources 259------------------------------------------------- 260The C++ standard library has similar allocator interfaces to those proposed 261defined as part of its PMR library. The reasons why Pigweed is not using these 262directly are :ref:`described below <seed-0110-why-not-pmr>`; however, Pigweed 263will provide a wrapper which exposes a Pigweed allocator through the PMR 264``memory_resource`` interface. An example of how this wrapper might look is 265presented here. 266 267.. code-block:: c++ 268 269 template <typename Allocator> 270 class MemoryResource : public std::pmr::memory_resource { 271 public: 272 template <typename... Args> 273 MemoryResource(Args&&... args) : allocator_(std::forward<Args>(args)...) {} 274 275 private: 276 void* do_allocate(size_t bytes, size_t alignment) override { 277 void* p = allocator_.Allocate(bytes, alignment); 278 PW_ASSERT(p != nullptr); // Cannot throw in Pigweed code. 279 return p; 280 } 281 282 void do_deallocate(void* p, size_t bytes, size_t alignment) override { 283 allocator_.Deallocate(p, bytes, alignment); 284 } 285 286 bool do_is_equal(const std::pmr::memory_resource&) override { 287 // Pigweed allocators do not yet support the concept of equality; this 288 // remains an open question for the future. 289 return false; 290 } 291 292 Allocator allocator_; 293 }; 294 295Future Considerations 296===================== 297 298Allocator traits 299---------------- 300It can be useful for users to know additional details about a specific 301implementation of an allocator to determine whether it is suitable for their 302use case. For example, some allocators may have internal synchronization, 303removing the need for external locking. Certain allocators may be suitable for 304uses in specialized contexts such as interrupts. 305 306To enable users to enforce these types of requirements, it would be useful to 307provide a way for allocator implementations to define certain traits. 308Originally, this proposal accommodated for this by defining derived allocator 309interfaces which semantically enforced additional implementation contracts. 310However, this approach could have led to an explosion of different allocator 311types throughout the codebase for each permutation of traits. As such, it was 312removed from the initial allocator plan for future reinvestigation. 313 314Dynamic collections 315------------------- 316The ``pw_containers`` module defines several collections such as ``pw::Vector``. 317These collections are modeled after STL equivalents, though being 318embedded-friendly, they reserve a fixed maximum size for their elements. 319 320With the addition of dynamic allocation to Pigweed, these containers will be 321expanded to support the use of allocators. Unless absolutely necessary, upstream 322containers should be designed to work on the base ``Allocator`` interface --- 323not any of its derived classes --- to offer maximum flexibility to projects 324using them. 325 326.. code-block:: c++ 327 328 template <typename T> 329 class DynamicVector { 330 DynamicVector(Allocator& allocator); 331 }; 332 333Per-allocation tagging 334---------------------- 335Another interface which was originally proposed but shelved for the time being 336allowed for the association of an integer tag with each specific call to 337``Allocate``. This can be incredibly useful for debugging, but requires 338allocator implementations to store additional information with each allocation. 339This added complexity to allocators, so it was temporarily removed to focus on 340refining the core allocator interface. 341 342The proposed 32-bit integer tags happen to be the same as the tokens generated 343from strings by the ``pw_tokenizer`` module. Combining the two could result in 344the ability to precisely track the source of allocations in a project. 345 346For example, ``pw_allocator`` could provide a macro which tokenizes a user 347string to an allocator tag, automatically inserting additional metadata such as 348the file and line number of the allocation. 349 350.. code-block:: c++ 351 352 void GenerateAndProcessData(TaggedAllocator& allocator) { 353 void* data = allocator->AllocatedTagged( 354 Layout::Sized(kDataSize), PW_ALLOCATOR_TAG("my data buffer")); 355 if (data == nullptr) { 356 return; 357 } 358 359 GenerateData(data); 360 ProcessData(data); 361 362 allocator->Deallocate(data); 363 } 364 365Allocator implementations 366------------------------- 367Over time, Pigweed expects to implement a handful of different allocators 368covering the interfaces proposed here. No specific new implementations are 369suggested as part of this proposal. Pigweed's existing ``FreeListHeap`` 370allocator will be refactored to implement the ``Allocator`` interface. 371 372--------------------- 373Problem Investigation 374--------------------- 375 376Use cases and requirements 377========================== 378 379* **General-purpose memory allocation.** The target of ``pw_allocator`` is 380 general-purpose dynamic memory usage by typical applications, rather than 381 specialized types of memory allocation that may be required by lower-level 382 code such as drivers. 383 384* **Generic interfaces with minimal policy.** Every project has different 385 resources and requirements, and particularly in constrained systems, memory 386 management is often optimized for their specific use cases. Pigweed's core 387 allocation interfaces should offer as broad of an implementation contract as 388 possible and not bake in assumptions about how they will be run. 389 390* **RTOS or bare metal usage.** While many systems make use of an RTOS which 391 provides utilities such as threads and synchronization primitives, Pigweed 392 also targets systems which run without one. As such, the core allocators 393 should not be tied to any RTOS requirements, and accommodations should be made 394 for different system contexts. 395 396Out of scope 397------------ 398 399* **Asynchronous allocation.** As this proposal is centered around simple 400 general-purpose allocation, it does not consider asynchronous allocations. 401 While these are important use cases, they are typically more specialized and 402 therefore outside the scope of this proposal. Pigweed is considering some 403 forms of asynchronous memory allocation, such as the proposal in the 404 :ref:`Communication Buffers SEED <seed-0109>`. 405 406* **Direct STL integration.** The C++ STL makes heavy use of dynamic memory and 407 offers several ways for projects to plug in their own allocators. This SEED 408 does not propose any direct Pigweed to STL-style allocator adapters, nor does 409 it offer utilities for replacing the global ``new`` and ``delete`` operators. 410 These are additions which may come in future changes. 411 412 It is still possible to use Pigweed allocators with the STL in an indirect way 413 by going through the PMR interface, which is discussed later. 414 415* **Global Pigweed allocators.** Pigweed modules will not assume a global 416 allocator instantiation. Any usage of allocators by modules should rely on 417 dependency injection, leaving consumers with control over how they choose to 418 manage their memory usage. 419 420Alternative solutions 421===================== 422 423.. _seed-0110-why-not-pmr: 424 425C++ polymorphic allocators 426-------------------------- 427C++17 introduced the ``<memory_resource>`` header with support for polymorphic 428memory resources (PMR), i.e. allocators. This library defines many allocator 429interfaces similar to those in this proposal. Naturally, this raises the 430question of whether Pigweed can use them directly, benefitting from the larger 431C++ ecosystem. 432 433The primary issue with PMR with regards to Pigweed is that the interfaces 434require the use of C++ language features prohibited by Pigweed. The allocator 435is expected to throw an exception in the case of failure, and equality 436comparisons require RTTI. The team is not prepared to change or make exceptions 437to this policy, prohibiting the direct usage of PMR. 438 439Despite this, Pigweed's allocator interfaces have taken inspiration from the 440design of PMR, incorporating many of its ideas. The core proposed ``Allocator`` 441interface is similar to ``std::pmr::memory_resource``, making it possible to 442wrap Pigweed allocators with a PMR adapter for use with the C++ STL, albeit at 443the cost of an extra layer of virtual indirection. 444 445-------------- 446Open Questions 447-------------- 448This SEED proposal is only a starting point for the improvement of the 449``pw_allocator`` module, and Pigweed's memory management story in general. 450 451There are several open questions around Pigweed allocators which the team 452expects to answer in future SEEDs: 453 454* Should generic interfaces for asynchronous allocations be provided, and how 455 would they look? 456 457* Reference counted allocations and "smart pointers": where do they fit in? 458 459* The concept of allocator equality is essential to enable certain use cases, 460 such as efficiently using dynamic containers with their own allocators. 461 This proposal excludes APIs paralleling PMR's ``is_equal`` due to RTTI 462 requirements. Could Pigweed allocators implement a watered-down version of an 463 RTTI / type ID system to support this? 464 465* How do allocators integrate with the monolithic ``pw_system`` as a starting 466 point for projects? 467