mirror of https://github.com/pvnis/srsRAN_4G.git
implementation of concurrent fixed size pool that leverages thread local caches to avoid mutexing
parent
e200a3359e
commit
e1523692c2
@ -0,0 +1,113 @@
|
|||||||
|
/**
|
||||||
|
*
|
||||||
|
* \section COPYRIGHT
|
||||||
|
*
|
||||||
|
* Copyright 2013-2021 Software Radio Systems Limited
|
||||||
|
*
|
||||||
|
* By using this file, you agree to the terms and conditions set
|
||||||
|
* forth in the LICENSE file which can be found at the top level of
|
||||||
|
* the distribution.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef SRSRAN_FIXED_SIZE_POOL_H
|
||||||
|
#define SRSRAN_FIXED_SIZE_POOL_H
|
||||||
|
|
||||||
|
#include "memblock_cache.h"
|
||||||
|
#include "srsran/adt/circular_buffer.h"
|
||||||
|
#include "srsran/adt/singleton.h"
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
namespace srsran {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Concurrent fixed size memory pool made of blocks of equal size
|
||||||
|
* Each worker keeps a separate thread-local memory block cache that uses for fast allocation/deallocation.
|
||||||
|
* When this cache gets depleted, the worker tries to obtain blocks from a shared memory block cache
|
||||||
|
* Note: This pool does not implement stealing of blocks between workers, so it is possible that a worker can't allocate
|
||||||
|
* while another worker still has blocks in its own cache. This situation is avoided by upper bounding the
|
||||||
|
* size of each worker cache
|
||||||
|
* Note2: Taking into account the usage of thread_local, this class is made a singleton
|
||||||
|
* @tparam NofObjects number of objects in the pool
|
||||||
|
* @tparam ObjSize object size
|
||||||
|
*/
|
||||||
|
template <size_t NofObjects, size_t ObjSize, size_t MaxWorkerCacheSize = NofObjects / 16>
|
||||||
|
class concurrent_fixed_memory_pool : public singleton_t<concurrent_fixed_memory_pool<NofObjects, ObjSize> >
|
||||||
|
{
|
||||||
|
static_assert(NofObjects > 256, "This pool is particularly designed for a high number of objects");
|
||||||
|
static_assert(ObjSize > 256, "This pool is particularly designed for large objects");
|
||||||
|
|
||||||
|
struct obj_storage_t {
|
||||||
|
typename std::aligned_storage<ObjSize, alignof(detail::max_alignment_t)>::type buffer;
|
||||||
|
std::thread::id worker_id;
|
||||||
|
explicit obj_storage_t(std::thread::id id_) : worker_id(id_) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const static size_t batch_steal_size = 10;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// ctor only accessible from singleton
|
||||||
|
concurrent_fixed_memory_pool()
|
||||||
|
{
|
||||||
|
allocated_blocks.resize(NofObjects);
|
||||||
|
for (std::unique_ptr<obj_storage_t>& b : allocated_blocks) {
|
||||||
|
b.reset(new obj_storage_t(std::this_thread::get_id()));
|
||||||
|
srsran_assert(b.get() != nullptr, "Failed to instantiate fixed memory pool");
|
||||||
|
shared_mem_cache.push(static_cast<void*>(b.get()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
static size_t size() { return NofObjects; }
|
||||||
|
|
||||||
|
void* allocate_node(size_t sz)
|
||||||
|
{
|
||||||
|
srsran_assert(sz <= ObjSize, "Allocated node size=%zd exceeds max object size=%zd", sz, ObjSize);
|
||||||
|
memblock_cache* worker_cache = get_worker_cache();
|
||||||
|
|
||||||
|
void* node = worker_cache->try_pop();
|
||||||
|
if (node == nullptr) {
|
||||||
|
// fill the thread local cache enough for this and next allocations
|
||||||
|
std::array<void*, batch_steal_size> popped_blocks;
|
||||||
|
size_t n = shared_mem_cache.try_pop(popped_blocks);
|
||||||
|
for (size_t i = 0; i < n; ++i) {
|
||||||
|
new (popped_blocks[i]) obj_storage_t(std::this_thread::get_id());
|
||||||
|
worker_cache->push(static_cast<uint8_t*>(popped_blocks[i]));
|
||||||
|
}
|
||||||
|
node = worker_cache->try_pop();
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
void deallocate_node(void* p)
|
||||||
|
{
|
||||||
|
srsran_assert(p != nullptr, "Deallocated nodes must have valid address");
|
||||||
|
memblock_cache* worker_cache = get_worker_cache();
|
||||||
|
obj_storage_t* block_ptr = static_cast<obj_storage_t*>(p);
|
||||||
|
|
||||||
|
if (block_ptr->worker_id != std::this_thread::get_id() or worker_cache->size() >= MaxWorkerCacheSize) {
|
||||||
|
// if block was allocated in a different thread or local cache reached max capacity, send block to shared
|
||||||
|
// container
|
||||||
|
shared_mem_cache.push(static_cast<void*>(block_ptr));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// push to local memory block cache
|
||||||
|
worker_cache->push(static_cast<uint8_t*>(p));
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
memblock_cache* get_worker_cache()
|
||||||
|
{
|
||||||
|
thread_local memblock_cache worker_cache;
|
||||||
|
return &worker_cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
mutexed_memblock_cache shared_mem_cache;
|
||||||
|
std::mutex mutex;
|
||||||
|
std::vector<std::unique_ptr<obj_storage_t> > allocated_blocks;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace srsran
|
||||||
|
|
||||||
|
#endif // SRSRAN_FIXED_SIZE_POOL_H
|
@ -0,0 +1,151 @@
|
|||||||
|
/**
|
||||||
|
*
|
||||||
|
* \section COPYRIGHT
|
||||||
|
*
|
||||||
|
* Copyright 2013-2021 Software Radio Systems Limited
|
||||||
|
*
|
||||||
|
* By using this file, you agree to the terms and conditions set
|
||||||
|
* forth in the LICENSE file which can be found at the top level of
|
||||||
|
* the distribution.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef SRSRAN_MEMBLOCK_CACHE_H
|
||||||
|
#define SRSRAN_MEMBLOCK_CACHE_H
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
namespace srsran {
|
||||||
|
|
||||||
|
/// Stores provided mem blocks in a stack in an non-owning manner. Not thread-safe
|
||||||
|
class memblock_cache
|
||||||
|
{
|
||||||
|
struct node {
|
||||||
|
node* prev;
|
||||||
|
explicit node(node* prev_) : prev(prev_) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
public:
|
||||||
|
constexpr static size_t min_memblock_size() { return sizeof(node); }
|
||||||
|
|
||||||
|
memblock_cache() = default;
|
||||||
|
|
||||||
|
memblock_cache(const memblock_cache&) = delete;
|
||||||
|
|
||||||
|
memblock_cache(memblock_cache&& other) noexcept : head(other.head) { other.head = nullptr; }
|
||||||
|
|
||||||
|
memblock_cache& operator=(const memblock_cache&) = delete;
|
||||||
|
|
||||||
|
memblock_cache& operator=(memblock_cache&& other) noexcept
|
||||||
|
{
|
||||||
|
head = other.head;
|
||||||
|
other.head = nullptr;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
void push(void* block) noexcept
|
||||||
|
{
|
||||||
|
// printf("head: %ld\n", (long)head);
|
||||||
|
node* next = ::new (block) node(head);
|
||||||
|
head = next;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t* try_pop() noexcept
|
||||||
|
{
|
||||||
|
if (is_empty()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
node* last_head = head;
|
||||||
|
head = head->prev;
|
||||||
|
count--;
|
||||||
|
return (uint8_t*)last_head;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_empty() const { return head == nullptr; }
|
||||||
|
|
||||||
|
size_t size() const { return count; }
|
||||||
|
|
||||||
|
void clear() { head = nullptr; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
node* head = nullptr;
|
||||||
|
size_t count = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// memblock stack that mutexes pushing/popping
|
||||||
|
class mutexed_memblock_cache
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
mutexed_memblock_cache() = default;
|
||||||
|
|
||||||
|
mutexed_memblock_cache(const mutexed_memblock_cache&) = delete;
|
||||||
|
|
||||||
|
mutexed_memblock_cache(mutexed_memblock_cache&& other) noexcept
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lk1(other.mutex, std::defer_lock);
|
||||||
|
std::unique_lock<std::mutex> lk2(mutex, std::defer_lock);
|
||||||
|
std::lock(lk1, lk2);
|
||||||
|
stack = std::move(other.stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
mutexed_memblock_cache& operator=(const mutexed_memblock_cache&) = delete;
|
||||||
|
|
||||||
|
mutexed_memblock_cache& operator=(mutexed_memblock_cache&& other) noexcept
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lk1(other.mutex, std::defer_lock);
|
||||||
|
std::unique_lock<std::mutex> lk2(mutex, std::defer_lock);
|
||||||
|
std::lock(lk1, lk2);
|
||||||
|
stack = std::move(other.stack);
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
void push(void* block) noexcept
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mutex);
|
||||||
|
stack.push(block);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t* try_pop() noexcept
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mutex);
|
||||||
|
uint8_t* block = stack.try_pop();
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <size_t N>
|
||||||
|
size_t try_pop(std::array<void*, N>& result) noexcept
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mutex);
|
||||||
|
size_t i = 0;
|
||||||
|
for (; i < N; ++i) {
|
||||||
|
result[i] = stack.try_pop();
|
||||||
|
if (result[i] == nullptr) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_empty() const noexcept { return stack.is_empty(); }
|
||||||
|
|
||||||
|
size_t size() const noexcept
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mutex);
|
||||||
|
return stack.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mutex);
|
||||||
|
stack.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
memblock_cache stack;
|
||||||
|
mutable std::mutex mutex;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace srsran
|
||||||
|
|
||||||
|
#endif // SRSRAN_MEMBLOCK_CACHE_H
|
Loading…
Reference in New Issue