本系列文為C++ Weekly PMR Series的筆記文之二
讓我們接續先前的進度,繼續深入探討PMR
Custom memory resource
- 我們在上篇文章的結尾,探討了當local
buffer耗盡時的種種問題,問題是我們似乎沒有一個很好的手段可以去監視記憶體配置的行為
- 也就是說,alloc與dealloc發生時並沒有一個很明確的feedback
- 我們有辦法...印個log?
- 來看一下影片中提到的memory_resource
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// thanks to Rahil Baber
// Prints if new/delete gets used.
class print_alloc : public std::pmr::memory_resource {
private:
void* do_allocate(std::size_t bytes, std::size_t alignment) override {
std::cout << "Allocating " << bytes << '\n';
return std::pmr::new_delete_resource()->allocate(bytes, alignment);
}
void do_deallocate(void* p, std::size_t bytes,
std::size_t alignment) override {
std::cout << "Deallocating " << bytes << ": '";
for (std::size_t i = 0; i < bytes; ++i) {
std::cout << *(static_cast<char*>(p) + i);
}
std::cout << "'\n";
return std::pmr::new_delete_resource()->deallocate(p, bytes, alignment);
}
bool do_is_equal(
const std::pmr::memory_resource& other) const noexcept override {
return std::pmr::new_delete_resource()->is_equal(other);
}
}; - 雖然叫做print_alloc,但它並不是allocator,他是一個memory_resource
- 萬事起頭難,先來看一看cppreference是如何描述它的
std::pmr::memory_resource
is an abstract interface to an unbounded set of classes encapsulating memory resources- memory_resource作為一個介面(interface)存在,有下面三的virtual
method需要我們去實作
- do_allocate
- do_deallocate
- do_is_equal
- memory_resource作為一個介面(interface)存在,有下面三的virtual
method需要我們去實作
- 接著再來仔細看看這三個method的描述
- void* do_allocate(std::size_t bytes, std::size_t alignment)
- 負責從被管理的buffer(stack或是heap)中嘗試著分配一塊長度為
bytes
,並且memory address有對齊alignment的空間出來- 好比說對齊8就是return address永遠會是8的倍數
- 負責從被管理的buffer(stack或是heap)中嘗試著分配一塊長度為
- void do_deallocate(void* p, std::size_t bytes, std::size_t
alignment);
- 負責回收先前do_allocate()配置的記憶體,唯一一點要注意的是
p
必須是先前do_allocate()配置出來的
- 負責回收先前do_allocate()配置的記憶體,唯一一點要注意的是
- bool do_is_equal(const std::pmr::memory_resource& other) const
noexcept
- 負責判斷
other
跟當前的這個(*this)是否管理的是同一塊buffer
- 負責判斷
- void* do_allocate(std::size_t bytes, std::size_t alignment)
Case study: vector of std::string
- 講是這樣講,但實際上還是要看實作怎麼做,所以我們仔細看一看我們的print_alloc
- print_alloc底下實作通通都是仰賴new_delete_resource,但至少在do_allocate()跟do_deallocate()裡頭多了std::cout來輸出一些訊息給我們
- 我們做了一個會在alloc跟dealloc時印log的memory_resource,然後呢?
- 我修改了一下影片中的範例,完整程式碼請看這裡,我們在這裡先將注意力放在main()上
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
26int main() {
// remember initializer lists are broken.
print_alloc mem;
std::pmr::set_default_resource(&mem);
std::array<std::uint8_t, 1024> buffer{};
std::pmr::monotonic_buffer_resource mem_resource(
buffer.data(),
buffer.size()
);
std::cout << "initializing vector\n";
std::pmr::vector<std::string> container({
"short string",
"A really long string here",
"Another really long string here"
}, &mem_resource) ;
int idx = 0 ;
for (const auto& elem : container) {
fmt::print("[{}] addr: 0x{:x},\n\t {}\n", idx++, (uint64_t)elem.data(), elem) ;
} // for
std::cout << "exiting main\n";
} - 我們將剛剛提到的print_alloc設置為default_resource,如此一來凡是沒有預先給定memory_resource的容器都會使用到我們的print_alloc
- 也就是說那些我們沒注意到的alloc跟dealloc行為現在都會印出Log來提醒我們了,好耶!
- 但從輸出看起來,並沒有那麼順利......
1
2
3
4
5
6
7
8initializing vector
[0] addr: 0x7ffe618514d0,
short string
[1] addr: 0x1874f20,
A really long string here
[2] addr: 0x1874f50,
Another really long string here
exiting main - 不僅所有long string的string
data都跑去heap上,我們的print_alloc還完全沒有發揮出他該有的用途
- 奈A安捏?
- 冷靜的觀察一下輸出,很快的我們就會發現事有蹊蹺
- 第一個std::string,比較短的那個好端端地被配置到我們的local buffer上了
- 但是後面兩個比較長的全部失手
- 讓我們再多印點東西,看看能不能調查出什麼,緊接著修改一下用來dump內容物的for
1
2
3
4for (const auto& elem : container) {
fmt::print("[{}] elem addr: 0x{:x}, data addr: 0x{:x},\n\t {}\n",
idx++, (uint64_t)&elem, (uint64_t)elem.data(), elem) ;
} // for - 對應的輸出為
1
2
3
4
5
6
7
8initializing vector
[0] elem addr: 0x7ffedbcc5650, data addr: 0x7ffedbcc5660,
short string
[1] elem addr: 0x7ffedbcc5670, data addr: 0x1276f20,
A really long string here
[2] elem addr: 0x7ffedbcc5690, data addr: 0x1276f50,
Another really long string here
exiting main - 喔齁,每個物件(str::string)本身確實都好端端地放在local
buffer上,但是長字串的data()卻被丟去了heap,怎麼會因為字串長了點就有這樣的差別阿
- 因為std::string具有所謂的Small Object Optimization機制
- 正如我們現在在做的,為了避免動不動就使用dynamic allocation,std::string對於短字串(至於到底以個字元以下才算短,不一定,要看實作)並不會真的乖乖配置heap
- 取而代之的是std::string內部會有一個std::array<char>,來存放短字串的內容
- 由於這個array也是std::string物件的一部分,因此不屬於dynamic allocation
- 仔細看,短字串的elem addr跟data addr不是正好都在local buffer上嗎?
- 另外每一個std::string的大小皆為0x20 bytes(你可以從elem
addr的差看出來)
- 有興趣的話不妨留意一下libstdc++的basic_string裏頭的_M_local_buf
- 所以現在我們知道問題了
- std::pmr::vector<std::string>裡頭的std::string的allocator沒有跟著一起被換成
mem_resource
- OK,所以我們現在的目標就是讓std::string能夠吃到跟vector一樣的allocator
- 我們來修改一下vector的宣告
1
2
3
4
5std::pmr::vector<std::pmr::string> container({
"short string",
"A really long string here",
"Another really long string here"
}, &mem_resource) ;
- std::pmr::vector<std::string>裡頭的std::string的allocator沒有跟著一起被換成
- 修改過後,其對應的輸出為
1
2
3
4
5
6
7
8
9
10
11
12initializing vector
Allocating 26
Allocating 32
Deallocating 32: 'Another really long string here'
Deallocating 26: 'A really long string here'
[0] elem addr: 0x7ffd2607d730, data addr: 0x7ffd2607d748,
short string
[1] elem addr: 0x7ffd2607d758, data addr: 0x7ffd2607d7a8,
A really long string here
[2] elem addr: 0x7ffd2607d780, data addr: 0x7ffd2607d7c2,
Another really long string here
exiting main - 好耶!現在三個std::"pmr"::string都在local
buffer上了,但是看起來好像還是怪怪?
- 我們的new_delete_resource居然被觸發了,為啥?
- 我修改了一下影片中的範例,完整程式碼請看這裡,我們在這裡先將注意力放在main()上
Initializer list的玄機
- 要解釋這個問題,我們得先來仔細端詳端詳我們對vector的初始化步驟,那個{}包起來的東東型別如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14// What is this?
// {
// "short string",
// "A really long string here",
// "Another really long string here"
// }
std::initializer_list<
std::basic_string<
char,
std::char_traits<char>,
std::pmr::polymorphic_allocator<char>
>
> - 型別有點長,但是我有先幫你format好喔
- 他是一個initializer_list,根據cppreference所述,其背後運作機制是這樣:
- 建構一個
const T[N]
(在我們的例子中T=std::basic_string<char, std::char_traits<char>, std::pmr::polymorphic_allocator<char>>
) - 接著vector的初始化會拿T的begin跟end(也可能會是begin跟length)去做真正的初始化(local buffer上的string是這一步做出來的)
- 建構一個
- 由於default_resource被我們換成print_alloc了,所以才抓的到
const T[N]
的allocation
- 他是一個initializer_list,根據cppreference所述,其背後運作機制是這樣:
- 如果我們今天沒有看log,還真的不會察覺到有這件事情發生,不過問題又來了,那怎樣初始化才是對的?
push_back() V.S. emplace_back()
- 既然用initializer_list會有非必要的allocation發生,那換種方式總可以了吧
- 試試push_back()?
1
2
3
4std::pmr::vector<std::pmr::string> container(&mem_resource) ;
container.push_back("short string") ;
container.push_back("A really long string here");
container.push_back("Another really long string here"); - 輸出為 雖然建構跟解構的順序不同了,但基本上還是沒有解決問題啊...
1
2
3
4Allocating 26
Deallocating 26: 'A really long string here'
Allocating 32
Deallocating 32: 'Another really long string here' - 隨便亂試解決不了問題,看來得仔細想想背後到底出了什麼問題
- 就如同剛剛initializer list發生的事情一樣,我們都先建構了一個temp value,然後才用copy initialized的方式在local buffer上建構出正確的物件
- push_back()也會做一樣的事情
- 有沒有辦法不要? 建構temp value並不是我們需要的步驟
- How about
emplace_back()
- How about
emplace_back()
做為跟push_back()
相似的method,雖然目的差不多,但運作機制不太一樣- push_back()是接受一個已經建構好的物件,複製進容器
- emplace_back()接受的是該物件的constructor所需的參數,內部會直接利用allocator建構出物件,最後放進容器
- 不正是我們要的?
- 手起刀落,馬上給他改下去
1
2
3
4std::pmr::vector<std::pmr::string> container(&mem_resource) ;
container.emplace_back("short string") ;
container.emplace_back("A really long string here");
container.emplace_back("Another really long string here"); - 這樣做的確能達到我們要的效果,但總覺得有點麻煩阿,emplace_back連續寫好幾次
- 影片中給出的解法是用一個帶有parameter pack的function,裡面再去用fold
expression去自動的生出一堆emplace_back()出來
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18template <typename Container, typename... Values>
auto create_container(auto *resource, Values&&... values) {
Container result{resource};
result.reserve(sizeof...(values));
(result.emplace_back(std::forward<Values>(values)), ...);
return result;
}
int main() {
// ...
auto container = create_container<std::pmr::vector<std::pmr::string>> (
&mem_resource,
"short string",
"A really long string here",
"Another really long string here"
) ;
// ...
} - 雖然現在講這個有點後知後覺,不過關於為何pmr container是傳入memory_resource的pointer而非reference,以及內部的一些細節可以參考std::pmr::vector的參數危機
- 試試push_back()?
小結
- 我們在本篇文章中介紹了如何自定義一個memory_resource,用於監視alloc以及dealloc的發生
- 並且提到了std::string以及std::pmr::string的差異
- 還介紹了正確建構pmr容器的方式
關於PMR的相關細節還有不少沒有提到,所以我們會在下篇文章繼續講下去