LCOV - code coverage report
Current view: top level - /jenkins/workspace/boost-root/libs/capy/src/ex - recycling_memory_resource.cpp (source / functions) Coverage Total Hit Missed
Test: coverage_remapped.info Lines: 100.0 % 101 101
Test Date: 2026-11-13 12:15:37 Functions: 93.3 % 15 14 1

           TLA  Line data    Source code
       1                 : //
       2                 : // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
       3                 : //
       4                 : // Distributed under the Boost Software License, Version 1.0. (See accompanying
       5                 : // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
       6                 : //
       7                 : // Official repository: https://github.com/cppalliance/capy
       8                 : //
       9                 : 
      10                 : #include <boost/capy/ex/recycling_memory_resource.hpp>
      11                 : 
      12                 : #include <new>
      13                 : #include <tuple>
      14                 : 
      15                 : namespace boost {
      16                 : namespace capy {
      17                 : 
      18                 : // Instance destruction does nothing: the resource is stateless (all pools
      19                 : // are static). Global-pool cleanup is owned by global()'s holder, exactly
      20                 : // as in the original where the global pool's own destructor drained it,
      21                 : // independent of any instance lifetime.
      22 HIT         327 : recycling_memory_resource::~recycling_memory_resource() = default;
      23                 : 
      24                 : #if (defined(BOOST_CAPY_DYN_LINK) || defined(BOOST_ALL_DYN_LINK)) && \
      25                 :     !defined(BOOST_CAPY_STATIC_LINK)
      26                 : 
      27                 : recycling_memory_resource::pool&
      28         2590777 : recycling_memory_resource::local() noexcept
      29                 : {
      30                 :     static thread_local pool p;
      31         2590777 :     return p;
      32                 : }
      33                 : 
      34                 : bool&
      35         1144341 : recycling_memory_resource::thread_armed() noexcept
      36                 : {
      37                 :     static thread_local bool armed = false;
      38         1144341 :     return armed;
      39                 : }
      40                 : 
      41                 : #endif
      42                 : 
      43                 : namespace {
      44                 : 
      45                 : // Set once this thread's pool was drained at exit. Another thread_local
      46                 : // destroyed later can still free frames; they must not be cached.
      47                 : thread_local bool pool_drained = false;
      48                 : 
      49                 : } // namespace
      50                 : 
      51                 : bool
      52          302819 : recycling_memory_resource::arm_thread_cleanup() noexcept
      53                 : {
      54                 :     struct janitor
      55                 :     {
      56             373 :         ~janitor()
      57                 :         {
      58                 :             // Return this thread's cached blocks to the OS so nothing
      59                 :             // leaks at thread exit. The pool has a trivial dtor, so its
      60                 :             // thread-local storage is still valid here.
      61             373 :             auto& lp = local();
      62            2611 :             for(auto& b : lp.buckets)
      63           52545 :                 while(b.count > 0)
      64           50307 :                     ::operator delete(b.pop());
      65             373 :             pool_drained = true;
      66             373 :             thread_armed() = false;
      67             373 :         }
      68                 :     };
      69          302819 :     if(pool_drained)
      70               5 :         return false;
      71          302814 :     static thread_local janitor j;
      72          302814 :     std::ignore = j;
      73          302814 :     return true;
      74                 : }
      75                 : 
      76                 : recycling_memory_resource::pool&
      77          569107 : recycling_memory_resource::global() noexcept
      78                 : {
      79                 :     // Holder gives the global pool a destructor (the trivial-dtor pool type
      80                 :     // itself must stay guard-free for local()). Runs unconditionally at
      81                 :     // process exit, mirroring the original global pool destructor, and is
      82                 :     // locked because a worker thread may still be in a slow path. This path
      83                 :     // is cold (slow paths only), so the holder's guard costs nothing hot.
      84                 :     struct holder
      85                 :     {
      86                 :         pool p;
      87                 : 
      88              33 :         ~holder()
      89                 :         {
      90              33 :             std::lock_guard<std::mutex> lock(global_mutex());
      91             231 :             for(auto& b : p.buckets)
      92            5320 :                 while(b.count > 0)
      93            5122 :                     ::operator delete(b.pop());
      94              33 :         }
      95                 :     };
      96          569107 :     static holder h;
      97          569107 :     return h.p;
      98                 : }
      99                 : 
     100                 : std::mutex&
     101          569140 : recycling_memory_resource::global_mutex() noexcept
     102                 : {
     103                 :     // Never destroyed: it is locked during process-exit teardown (in
     104                 :     // global()'s holder destructor), after a function-local `static
     105                 :     // std::mutex` could itself have been destroyed. Placement-new into
     106                 :     // static storage owns no heap allocation, so there is nothing to leak.
     107                 :     alignas(std::mutex) static unsigned char storage[sizeof(std::mutex)];
     108                 :     static std::mutex* const mtx =
     109          569140 :         ::new(static_cast<void*>(storage)) std::mutex();
     110          569140 :     return *mtx;
     111                 : }
     112                 : 
     113                 : void*
     114          302444 : recycling_memory_resource::allocate_slow(
     115                 :     std::size_t rounded, std::size_t idx)
     116                 : {
     117                 :     // Once this thread's pool was drained at exit, take one block and
     118                 :     // leave the pool empty: nothing would release a refill.
     119          302444 :     bool const cache = arm_thread_cleanup();
     120          302444 :     void* p = nullptr;
     121          302444 :     std::size_t refilled = 0;
     122                 :     {
     123          302444 :         std::lock_guard<std::mutex> lock(global_mutex());
     124          302444 :         auto& b = global().buckets[idx];
     125          302444 :         if(cache)
     126                 :         {
     127          302443 :             auto& lb = local().buckets[idx];
     128          302443 :             p = b.pop(lb);
     129          302443 :             refilled = lb.count;
     130                 :         }
     131                 :         else
     132                 :         {
     133               1 :             p = b.pop();
     134                 :         }
     135          302444 :     }
     136          302444 :     if(p == nullptr)
     137          302415 :         return ::operator new(rounded);
     138                 : 
     139                 :     // A thread that only allocates never frees into its pool, so its
     140                 :     // refills are what drive its decay passes.
     141              29 :     auto& lp = local();
     142              29 :     lp.arrivals += refilled;
     143              29 :     if(lp.arrivals >= decay_period)
     144               7 :         decay(lp);
     145              29 :     return p;
     146                 : }
     147                 : 
     148                 : void
     149          266384 : recycling_memory_resource::deallocate_slow(
     150                 :     void* p, std::size_t idx)
     151                 : {
     152                 :     {
     153          266384 :         std::lock_guard<std::mutex> lock(global_mutex());
     154          266384 :         if(global().buckets[idx].push(p, bucket_capacity(idx)))
     155           59059 :             return;
     156          266384 :     }
     157          207325 :     ::operator delete(p);
     158                 : }
     159                 : 
     160                 : void
     161             279 : recycling_memory_resource::decay(pool& lp) noexcept
     162                 : {
     163             279 :     lp.arrivals = 0;
     164                 : 
     165                 :     // Unlink each bucket's share before taking the lock, so the walk
     166                 :     // through the blocks happens outside it.
     167             279 :     node* heads[num_classes] = {};
     168             279 :     node* tails[num_classes] = {};
     169             279 :     std::size_t counts[num_classes] = {};
     170            1953 :     for(std::size_t i = 0; i < num_classes; ++i)
     171                 :     {
     172            1674 :         auto& b = lp.buckets[i];
     173            1674 :         std::size_t const share = (b.low + 1) / 2;
     174            1674 :         if(share > 0)
     175                 :         {
     176             122 :             heads[i] = b.head;
     177             122 :             node* t = b.head;
     178           48938 :             for(std::size_t k = 1; k < share; ++k)
     179           48816 :                 t = t->next;
     180             122 :             b.head = t->next;
     181             122 :             b.count -= share;
     182             122 :             tails[i] = t;
     183             122 :             counts[i] = share;
     184                 :         }
     185            1674 :         b.low = b.count;
     186                 :     }
     187                 : 
     188             279 :     node* spill = nullptr;
     189                 :     {
     190             279 :         std::lock_guard<std::mutex> lock(global_mutex());
     191             279 :         auto& gp = global();
     192            1953 :         for(std::size_t i = 0; i < num_classes; ++i)
     193                 :         {
     194            1674 :             if(counts[i] == 0)
     195            1552 :                 continue;
     196             122 :             auto& g = gp.buckets[i];
     197                 :             // A share that doesn't fit whole is released whole: topping
     198                 :             // off a bucket already at least half full would mean walking
     199                 :             // the chain under the lock.
     200             122 :             if(g.count + counts[i] <= bucket_capacity(i))
     201                 :             {
     202              43 :                 tails[i]->next = g.head;
     203              43 :                 g.head = heads[i];
     204              43 :                 g.count += counts[i];
     205                 :             }
     206                 :             else
     207                 :             {
     208              79 :                 tails[i]->next = spill;
     209              79 :                 spill = heads[i];
     210                 :             }
     211                 :         }
     212             279 :     }
     213           39940 :     while(spill != nullptr)
     214                 :     {
     215           39661 :         node* n = spill;
     216           39661 :         spill = n->next;
     217           39661 :         ::operator delete(n);
     218                 :     }
     219             279 : }
     220                 : 
     221                 : void*
     222          102458 : recycling_memory_resource::do_allocate(std::size_t bytes, std::size_t alignment)
     223                 : {
     224          102458 :     return allocate_fast(bytes, alignment);
     225                 : }
     226                 : 
     227                 : void
     228          102458 : recycling_memory_resource::do_deallocate(void* p, std::size_t bytes, std::size_t alignment)
     229                 : {
     230          102458 :     deallocate_fast(p, bytes, alignment);
     231          102458 : }
     232                 : 
     233                 : std::pmr::memory_resource*
     234            4524 : get_recycling_memory_resource() noexcept
     235                 : {
     236            4524 :     static recycling_memory_resource instance;
     237            4524 :     return &instance;
     238                 : }
     239                 : 
     240                 : } // namespace capy
     241                 : } // namespace boost
        

Generated by: LCOV version 2.3