本系列文為C++ Weekly PMR Series的筆記文之三
我們在上篇文章中說明了自訂義memory_resource的方法,以及建構pmr容器的一些注意事項 接下來我們將繼續深入探討PMR的細節
還漏掉了一片拼圖
- 我們先前已經看過了pmr::vector跟pmr::string,還有pmr::vector<pmr::string>
- 我們使用了vector.emplace_back()來插入pmr::string,使得vector內部的所有string都依靠vector的allocator來建構
- 但問題來了,世界不是只有美國,C++也不是只有string
- 假設今天我們有一個自己寫的class
S,我們要如何才可以讓S跟pmr::string一樣可以被放進pmr容器中,並且與容器使用相同的allocator呢?
- 要解決這個問題,我們得先分解問題為兩個部分
- 如何建構一個支援Polymorphic allocator的class S
- 如何讓容器裡的instance S與容器共用allocator
- 要解決這個問題,我們得先分解問題為兩個部分
- 先來看一眼pmr這個namespace是何方神聖,我們寫了他很多次,但都沒仔細看過他
- vector的定義
- string的定義
- 我們可以看出無論是vector還是string,所謂的namespace
pmr就只是把原本的Allocator<T>換成了polymorphic_allocator<T>而已
- 咦!?原本就有Allocator了? 那幹嘛我們還要再弄一個進去?
Allocator<T> V.S. polymorphic_allocator<T>
- 會發生這種奇怪問題的原因是因為原本的allocator在設計上並不是考慮的非常周全,例如以下的情況
1
2
3
4std::vector<int> std_vec {1, 2, 3} ; // same as std::vector<int, std::allocator<int>>
std::vector<int, custom_allocator> custom_vec;
custom_vec = std_vec ; // Error! - 可以看到的是,在使用allocator的狀況下,不同allocator的vector他們具有不同的型別,一些vector與vector之間的操作就變得不好實作了
- 如同上面示範的assign,因為std_vec與custom_vec的型別不同了,因此編譯會失敗,你得自己徒手實作operator =才行
- 更麻煩的是,即便你寫了一個可以處理custom_vec的operator =,萬一我又寫了一個新的allocator(我們叫他bump allocator好了),同樣的事情又要再來一遍,豈不是很惱人?
- 聰明的小吉肯定會想到要用template來解,的確,template可以解決掉這個問題
- 但代價是代碼膨脹以及編譯時間增加,就看你覺得值不值得
- 所以C++17引入了polymorphic_allocator,顧名思義,原本依靠不同的allocator<T>來切換記憶體管理策略的方式,現在一率都變成用繼承的方式來處理
- 你可以看到無論是先前提過的
monotonic_buffer_resource
,還是等等我們會介紹的unsynchronized_pool_resource
,他們全部都繼承自std::pmr::memory_resource
- 反過來說,任何pmr容器內部都會有一個polymorphic_allocator<T>,透過其內部不同的memory_resource來實現不同的記憶體管理行為
- 但我們從外面看,容器型別都是一樣的(Allocator都是polymorphic_allocator<T>)
- 你可以看到無論是先前提過的
- 如此一來,Allcator<T>的問題就能夠被解決掉,但由於polymorphic_allocator背後是仰賴v_table機制運作,因此virtual
function call的overhead就必須要被考慮
- 假設你很在乎的話啦...
Allocator-Aware Types
- 在釐清完allocator的問題後,我們就要來實際來解決第一個問題:
建構一個支援Polymorphic allocator的class S
- 由於我們只打算讓class支援polymorphic allocator,所以可以直接一點,直接塞進constructor裏頭,就不用那麼麻煩搞一個namespace pmr了
- 我們沿用上一篇文章的範例,稍微修改一下(code在這)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class S {
public:
S(const char* str) :
_str(str)
{
}
S(const char* str, const std::pmr::polymorphic_allocator<>& allocator) :
_str(str, allocator), _alloc(allocator)
{
}
private:
std::pmr::string _str = "long long long long long long";
std::pmr::polymorphic_allocator<> _alloc;
}; - 我們試著弄了一個class S,然後給他兩個constructor
- 一個普通的
- 跟一個可以塞pmr allocator的
- 看起來也沒有很難嘛,但是要接著解決下面的另一個問題:
讓容器裡的instance S與容器共用allocator
- 我們試著做一次之後就發現事情沒有想像的那麼容易...
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
27template <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() {
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 vec<S>\n";
auto container = create_container<std::pmr::vector<S>> (
&mem_resource,
"short string",
"A really long string here",
"Another really long string here"
);
std::cout << "exiting main\n";
} // main() - 對應的輸出為
1
2
3
4
5
6initializing vec<S>
Allocating 26
Allocating 32
exiting main
Deallocating 26: 'A really long string here'
Deallocating 32: 'Another really long string here' - vector內的所有具有long string的S通通都被print_alloc抓到了
- 我們在上篇文章中建立std::pmr::vector<std::pmr::string>還好端端的阿?
- 很明顯地這次的問題並不是出在initilizer_list上頭
- 那肯定跟我們的S有關
- 我們試著做一次之後就發現事情沒有想像的那麼容易...
- 問題在於,我們的std::pmr::vector在初始化每一個內部的S物件時,並沒有順利的把mem_resource轉送給S的constructor
- 換言之,S沒有認到mem_resource,我們可以透過對make_container做一些修改來佐證我們的推論
1
2
3
4
5
6
7
8template <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), resource), ...);
// ^ here
return result;
} - 在修改後,我們就可以看到原本被print_alloc捕捉到的alloc & dealloc行為消失了,所以的確問題就是S沒有成功的認到vector內的mem_resource
- 那為何std::pmr::string就可以在原本的make_container下被認到?
- 換言之,S沒有認到mem_resource,我們可以透過對make_container做一些修改來佐證我們的推論
- 這...說來話長
- 簡單的來說,任何帶有
using allocator_type
的class,在allocation的過程中(uses_allocator_args.h::uses_allocator_construction_args()
這一步)uses_allocator_v
這個compile time check會是true - 後面還有一個
is_constructible_v
檢查在確保給定的args真的可以建構出class- 一定要通過這些檢查,我們給的allocator才會真的被拿起來用
- 否則就會被忽略,改用default_constructor
- 所以我們的目標就是要讓S上也有
using allocator_type
- 簡單的來說,任何帶有
- OK,事不宜遲,我們來看影片中改過的版本長怎樣
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
32
33struct S {
std::pmr::string str;
using allocator_type = std::pmr::polymorphic_allocator<>;
// default constructor, delegate to aa constructor
S(const char* sstr) : S(sstr, allocator_type{}) {}
explicit S(const char* sstr, allocator_type alloc)
: str(sstr, alloc)
{
}
S(const S &other, allocator_type alloc = {})
: str(other.str, alloc)
{
}
S(S &&) = default;
S(S &&other, allocator_type alloc)
: str(std::move(other.str), alloc)
{}
S &operator=(const S &rhs) = default;
S &operator=(S &&rhs) = default;
~S() = default;
allocator_type get_allocator() const {
return str.get_allocator();
}
}; - 我們總共做了以下幾點修改
- 補上
using allocator_type
- 根據rule of five,把該給的constructor跟destructor寫出來
- 補上
- Cool, 完整程式碼在此,來看看輸出
1
2
3
4initializing vec<S>
A really long string here
Another really long string here
exiting main - 現在所有的pmr::string都有乖乖地在local buffer上做allocation了
- 到這裡,我們就正式的完成了Allocator-Aware Types的建構
小結
- 我們在本篇文章中探討了Allocator-Aware Types的建構方式,並且實際利用一個class S來實驗結果是否符合預期
- 我們還探討了polymorphic_allocator與傳統allocator的不同,以及他們各自的優缺點
- 我們也簡單的研究了一下using allocator_type背後的機轉為何
- 下一篇文章我們就會正式開始探討不同的記憶體管理策略
有的時候我都在想,搞那麼複雜幹嘛呢...