Следуя аналогичному вопросу несколько месяцев назад <[комбинация монотонного буфера и несинхронизированного пула памяти]https://stackoverflow.com/questions/77271609/c17-combination-of-monotonic-buffer-and-unsynchronized-memory-pool >, я пытаюсь объединить ресурс монотонного буфера и несинхронизированного пула, чтобы эффективно распределять память без необходимости использовать кучу для выделения памяти.
#include <iostream>
#include <memory_resource>
#include <vector>
class A{
char array[64];
};
typename std::aligned_storage<32000>::type storage;
thread_local std::pmr::monotonic_buffer_resource bufferPool{&storage,sizeof(storage),std::pmr::null_memory_resource()};
std::pmr::unsynchronized_pool_resource pool{{},&bufferPool};
int main()
{
for(int i =0; i < 10000;++i){
std::pmr::vector<A> vec (&pool);
vec.reserve(1000);
for(int j =0; j < 1000;++j){
vec.emplace_back();
}
}
}
На основании предыдущего ответа ожидается, что блок памяти вернется в пул и будет повторно использован для следующего распределения. Но этого не происходит. В следующих выделениях снова перейдите к монотонному буферу, который ведет себя монотонно, и после нескольких итераций заканчивается память. Что мне не хватает? И почему приведенный ниже пример работает и повторно использует одну и ту же память на каждой итерации?
#include <iostream>
#include <memory_resource>
int main() {
char buffer[1024];
std::pmr::monotonic_buffer_resource monotonicResource(buffer, sizeof(buffer));
// Use an unsynchronized_pool_resource on top of the monotonic_buffer_resource
std::pmr::unsynchronized_pool_resource poolResource(&monotonicResource);
// Allocate and deallocate memory using the unsynchronized_pool_resource
for (int i = 0; i < 5; ++i) {
std::cout << "Iteration " << i << ":\n";
// Allocate memory
void* ptr = poolResource.allocate(200); // request 200 bytes from the pool
std::cout << "Allocated at: " << ptr << "\n";
// Deallocate memory (returning it to the pool! Not the monotonic resource)
poolResource.deallocate(ptr, 200);
std::cout << "Deallocated.\n\n";
}
return 0;
}
В чем разница между ними?
Реализации распределителя пула обычно имеют две оптимизации, которые делают его оптимизированным для многих небольших выделений, но очень плохим для больших выделений.
Распределитель работает, но для выделения 64 КБ памяти на MSVC он запрашивает около 720 КБ памяти, чего не может удовлетворить небольшой буфер размером 32 КБ.
Решение действительно простое: не используйте его для больших векторов, вы можете повторно использовать векторную память без использования pmr::allocator
, просто поместив ее за пределы цикла for или как часть класса.
std::vector<A> vec;
vec.reserve(1000);
for (int i = 0; i < 10000; ++i) {
vec.clear();
for (int j = 0; j < 1000; ++j) {
vec.emplace_back();
}
}
Эти pmr
распределители более полезны для небольших распределений, таких как map
или unordered_map
или list
, где каждый раз, когда вы выделяете небольшие узлы постоянного размера, это также полезно для небольших векторов, около 10 элементов.
Если вы все еще хотите использовать его для большого вектора, вам нужно установить max_blocks_per_chunk
в pool_options
на 1 и позволить распределителю извлекать память из неограниченной глобальной кучи, потому что вы не можете заранее знать, сколько памяти он будет использовать. нуждаться.
Кроме того, gcc/clang по умолчанию имеет 4 МБ / 1 МБ largest_required_pool_block
, поэтому вам нужно указать, что вам нужно больше выделений в pool_options
, если вам нужны большие выделения.
std::pmr::monotonic_buffer_resource bufferPool{};
std::pmr::unsynchronized_pool_resource pool{std::pmr::pool_options{1,1024*1024*1024},&bufferPool};
Даже после того, как он настроен на выделение только 1 блока на фрагмент, он выделяет 140 КБ, чтобы удовлетворить выделение 64 КБ, не пытайтесь заранее оценить объем памяти, который вам понадобится, вы можете выделить небольшой буфер в стеке для обработки небольших выделения для небольших векторов (например, 10 КБ), но вы должны разрешить ему вернуться к глобальной куче для больших векторов.
@getsoubl вам нужно увеличить largest_required_pool_block
, так как по умолчанию в gcc установлено 4 МБ, и вам нужен гораздо больший буфер для больших выделений, как я сказал в ответе, это расточительно, потому что оно оптимизировано для небольших выделений, а не для больших выделений ️ 🔁 godbolt.org/z/rsr85v88n
@getsoubl largest_required_pool_block
должен быть больше, чем весь векторный блок, т.е. вектору из 1000 элементов потребуется блок размером 8 КБ, поэтому largest_required_pool_block
должен быть больше 8 КБ... но емкость вектора не будет равна точно 1000, потому что его емкость удваивается каждый раз, когда он растет, поэтому вам, вероятно, потребуется largest_required_pool_block
, чтобы он был не менее 16 КБ.
@Ahmend AEK, почему самый большой_требуемый_пул_блок должен быть больше, чем весь вектор? Разве каждый блок на фрагмент не содержит один элемент вектора? Я ожидаю, что 64 байта на блок будет в порядке. godbolt.org/z/e1r9Tdc4d. Что мне не хватает? Я уменьшаю выделение до 10. В последнем примере адрес монотонного буфера повторно не используется.
@getsoubl распределитель видит весь вектор как один блок (поскольку вектор ДОЛЖЕН быть смежным, в отличие от list
или map
, которые могут быть фрагментированы), вектор запрашивает у распределителя один блок size(A) * number_of_elements
, распределитель думает, что вы выделяете один большой объект такого размера.
Модифицированный код godbolt.org/z/83We9zd6Y. В каждом цикле используются одни и те же адреса, за исключением двух последних элементов. Почему это происходит. Кажется, это работает только для 10 элементов, а размер монотонного буфера составляет 4 КБ. Огромная трата памяти. @Ахменд АЕК