xref: /aosp_15_r20/external/pigweed/seed/0110.rst (revision 61c4878ac05f98d0ceed94b57d316916de578985)
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