黃爸爸狗園

本園只有sanitizer,沒有狗籠

0%

C++ Polymorphic allocator,花式記憶體管理 (四)

本系列文為C++ Weekly PMR Series的筆記文之四

我們在上篇文章中說明了定義Allocator aware type所需的注意事項,以及其背後的機制

在本文中,我們將繼續介紹包含monotonic_buffer_resource在內的數種memory_resource的特性

memory_resource與記憶體分配策略

  • 我們在前文中有提過polymorphic allocator到底是使用那一種記憶體分配策略,是依靠其內部的memory_resource在控制
  • 我們也在前面的文章中幹過不少次把容器的allocator換成帶有monotonic_buffer_resource的allocator
  • 接著我們討論到當memory_resource耗盡了內部的記憶體資源時,該層級的memory_resource就會使用它的upstream_resource來獲取另一塊記憶體
    • 關鍵是,雖然預設upstream_resource會是new_delete_resource,但我們可以將其替換成不同的resource,藉此組合不同memory_resource的特性來達成目的
    • 所以接下來我們就要來探討standard library提供的memory_resource有那些,而我們又該如何組合他們

各種memory_resoure

  • null_memory_resource
    • 顧名思義,就是一個什麼都沒有的memory_resource,我們在先前的文章中也看過他幾次
      • 他的allocate()只要一被呼叫,就會立即拋出例外std::bad_alloc
      • deallocate()則啥也不幹
    • 所以我們會把null_memory_resource使用在那些常理來說不應該被耗盡的memory_resource上(透過指定upstream_resource的方式)
    • 亦或者是將其指定為default_resource,避免有不被預期的allocation沒有被我們發現
  • monotonic_buffer_resource
    • 其運作邏輯是,給定一塊buffer(不一定要是local buffer,我們也可以把他的upstream設為new_delete,讓他去管理heap上的buffer)
    • 在這塊buffer上,我們永遠只會做allocation,絕對不做deallocation
      • 藉此將memory allocation的overhead壓到最低
      • 但其代價就是面對頻繁新增刪除的場景就會導致非常糟糕的記憶體使用效率
    • 當它內部的buffer耗盡時,一樣也會透過upstream_resource去要一塊新的buffer
      • 具體而言是透過monotonic_memory_resource::_M_new_buffer()這個function
      • 但新的buffer會有多大,依照cppreference所述,看實作= =
  • (un)synchronized_pool_resource
    • 具體而言是synchronized_pool_resource跟unsynchronized_pool_resource
      • 基本上差不多,但synchronized的版本是thread-safe,另一個不是
    • 名字裡頭有個pool,所以資料都泡水嗎
      • 並沒有,這裡的pool應該要加上s,他的意思是這個memory_resource內有很多不同用chunk size區分的資源池,有點像是spa中心那樣有很多不同的池
        • 等到那天黃爸有工作了,一定要去spa中心轉轉
      • 舉例來說,pool_resource會建立4 byte池,8 byte池...等資源池,每一個池子都是一塊獨立的buffer,由於每一個池子內部的資源大小都是固定的,因此我們新增以及修改元素就比較不容易造成記憶體破碎化問題
      • 詳細有關pool的內部實作方式可以參考libstdc++
        • Pool的建構 -> __pool_resource::_M_alloc_pools()
        • 一個物件要進到pool,要先看他的size決定要去哪個pool -> unsynchronized_pool_resource::_M_find_pool(size_t block_siz)
        • 然後就是在do_allocate()裡頭呼叫剛剛找到的pool的allocate()
    • 如果其中某個pool資源耗盡,則pool_resource會使用他的upstream_resource重新配置一塊buffer出來作為該pool的資源,新配置的buffer大小會與舊buffer呈現等比級數關係
  • new_delete_resource
    • 我們的老朋友,沒啥好說的

Case study: unsynchronized_pool + monotonic

  • 我們現在來仔細看一看範例,影片中的範例是操作string,有點不易觀察,我換成了uint8_t
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int main() {
spdlog::set_level(spdlog::level::trace);

print_alloc default_alloc{"Rogue PMR Allocation!",
std::pmr::null_memory_resource()};
std::pmr::set_default_resource(&default_alloc);

print_alloc oom{"Out of Memory", std::pmr::null_memory_resource()};

std::array<std::uint8_t, 32768> buffer{};
spdlog::debug("Buffer area: 0x{:x} ~ 0x{:x}", (uint64_t)buffer.data(), (uint64_t)(buffer.data() + buffer.size()));
std::pmr::monotonic_buffer_resource underlying_bytes(buffer.data(),
buffer.size(), &oom);

print_alloc monotonic{"Monotonic Array", &underlying_bytes};

spdlog::debug("Starting pool_resource construction");
std::pmr::unsynchronized_pool_resource unsync_pool(&monotonic);

print_alloc pool("Pool", &unsync_pool);

spdlog::debug("Starting vector construction");
std::pmr::vector<uint8_t> vec(&pool);
vec.push_back(0x12);
vec.push_back(0x34);
vec.push_back(0x56);
vec.push_back(0x78);
spdlog::debug("Edit element");
vec[1] = 0xaa;
spdlog::debug("Exiting Main");
}
  • 先來仔細看一看main()
    • default_alloc是一個帶有null_memory_resource的allocator,設做default來避免不預期的allocation發生
    • oom是另外一個帶有null_memory_resource的allocator,作為underlying_bytes的upstream,表示帶有local buffer的monotonic resource不允許額外alloc(沒了就是沒了)
    • 再來就是underlying_bytes跟monotonic,再來就是underlying_bytes跟monotonic
      • underlying_bytes是一個monotonic_memory_resource,我們將它設為monotonic(他是print_alloc)的upstream
      • monotonic就變得有點像是監視各種memory_resource的alloc & dealloc的行為的中間人
      • 仔細看一下這個範例中的print_alloc就能發現,他的do_allocate()跟do_deallocate()基本上就是印log跟轉發給upstream
    • 最後則是unsync_pool,他是一個pool_resource
      • 但是非常神奇的,他的upstream被設為了monotonic
      • 這就代表unsync_pool裡頭所有的pool都會在一個連續的buffer上
    • 我們使用了這個unsync_pool當作vector的resource,預期所有的資料都會在buffer上
  • 對應的輸出為
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    [2022-08-25 16:58:23.809] [debug] Buffer area: 0x7ffea942a700 ~ 0x7ffea9432700
    [2022-08-25 16:58:23.809] [debug] Starting pool_resource construction
    [2022-08-25 16:58:23.809] [trace] [Monotonic Array (alloc)] Size: 528 Alignment: 8 ...
    [2022-08-25 16:58:23.809] [trace] [Monotonic Array (alloc)] ... Address: 0x7ffea942a700
    [2022-08-25 16:58:23.809] [debug] Starting vector construction
    [2022-08-25 16:58:23.809] [trace] [Pool (alloc)] Size: 1 Alignment: 1 ...
    [2022-08-25 16:58:23.809] [trace] [Monotonic Array (alloc)] Size: 1024 Alignment: 8 ...
    [2022-08-25 16:58:23.809] [trace] [Monotonic Array (alloc)] ... Address: 0x7ffea942a910
    [2022-08-25 16:58:23.809] [trace] [Monotonic Array (alloc)] Size: 192 Alignment: 8 ...
    [2022-08-25 16:58:23.809] [trace] [Monotonic Array (alloc)] ... Address: 0x7ffea942ad10
    [2022-08-25 16:58:23.809] [trace] [Pool (alloc)] ... Address: 0x7ffea942a910
    [2022-08-25 16:58:23.809] [trace] [Pool (alloc)] Size: 2 Alignment: 1 ...
    [2022-08-25 16:58:23.809] [trace] [Pool (alloc)] ... Address: 0x7ffea942a918
    [2022-08-25 16:58:23.809] [trace] [Pool (dealloc)] Address: 0x7ffea942a910 Dealloc Size: 1 Alignment: 1 Data: 12
    [2022-08-25 16:58:23.809] [trace] [Pool (alloc)] Size: 4 Alignment: 1 ...
    [2022-08-25 16:58:23.809] [trace] [Pool (alloc)] ... Address: 0x7ffea942a910
    [2022-08-25 16:58:23.809] [trace] [Pool (dealloc)] Address: 0x7ffea942a918 Dealloc Size: 2 Alignment: 1 Data: 12 34
    [2022-08-25 16:58:23.809] [debug] Edit element
    [2022-08-25 16:58:23.809] [debug] Exiting Main
    [2022-08-25 16:58:23.809] [trace] [Pool (dealloc)] Address: 0x7ffea942a910 Dealloc Size: 4 Alignment: 1 Data: 12 aa 56 78
    [2022-08-25 16:58:23.809] [trace] [Monotonic Array (dealloc)] Address: 0x7ffea942a910 Dealloc Size: 1024 Alignment: 8 Data: 12 aa 56 78 00 00 00 00 12 34 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 <truncated...>
    [2022-08-25 16:58:23.809] [trace] [Monotonic Array (dealloc)] Address: 0x7ffea942ad10 Dealloc Size: 192 Alignment: 8 Data: 00 ad 42 a9 fe 7f 00 00 7e 00 00 00 00 04 00 00 10 a9 42 a9 fe 7f 00 00 00 00 00 00 00 00 00 00 00 <truncated...>
    [2022-08-25 16:58:23.809] [trace] [Monotonic Array (dealloc)] Address: 0x7ffea942a700 Dealloc Size: 528 Alignment: 8 Data: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 08 00 00 00 fc 00 00 00 00 00 00 00 00 00 00 00 00 <truncated...>
  • 我們可以發現幾件事情
    • monotonic array的alloc總共發生了3次
      • 0x7ffea942a700這個是屬於pool_resource的,其餘兩次都是vector
      • 重點來了,怎麼換上了pool_resource之後,居然allocation被拆成兩次
        • 原因是因為pool的特性,放uint8_t(1 byte, alignment 1)的以及放vector內部pointer type(8 byte, alignment 8)的被分去兩個pool
    • 另外,pool的allocation情形也值得一看
      • 由於是vector的關係,所以不會每一次push_back()都觸發alloc(記得capacity嗎?),每次Pool(alloc)的size都會跟capacity相同
      • 我們可以看到,在push_back的過程中pool有發生deallocation,但是unsync_pool的upstream是monotonic阿,照理說不應該發生deallocation
      • 有趣的點在於,pool裏頭的memory雖然來自於monotonic,但是實際上歸unsync_pool管理(我們從log上看到的是Pool (dealloc)對吧),因此dealloc的邏輯是照著unsynchronized_pool_resource::do_deallocate()
      • unsynchronized_pool_resource::deallocate()並不是真的歸還記憶體,他是將此塊記憶體重新標示為可用,等到下次有人alloc時就會重新被給出去
      • 而且受惠於upstream是monotonic的關係,pool的memory可以是local buffer,很酷吧!

小結

  • 我們在本文介紹了以下數種memory_resource的特性
    • null_memory_resource
    • monotonic_buffer_resource
    • (un)synchronized_pool_resource
    • new_delete_resource
  • 並且實際操作了unsynchronized_pool_resource + monotonic_buffer_resource的組合
  • 下一篇,嗯..... 可以把最後一支影片的JSON範例實際走一遍