这是我的问题:我需要在远程连续缓冲区中管理内存,而我的程序无法读取或写入。它需要具有malloc()/ free()语义,并支持设置最小对齐和碎片避免(尽可能)。由于我无法直接读取或写入此缓冲区,因此我需要使用本地结构来管理所有分配。
我已经在使用提升,所以如果可以按摩内部的某些东西来做到这一点,那就太好了。但是,我并不反对使用C库或类似的东西。
作为一个例子,我需要一个非IPC版本:
boost::interprocess::basic_managed_external_buffer<
char,
boost::interprocess::rbtree_best_fit<
boost::interprocess::mutex_family,
boost::interprocess::offset_ptr<void>,
SOME_ALIGNMENT>,
boost::interprocess::iset_index>
最好使用malloc / free语义而不是new / delete 但没有实际读取或写入底层缓冲区(并将所有分配信息/数据结构保存在单独的缓冲区中)
有什么想法吗?
P.S。我不希望boost :: interprocess示例产生误导,我只是熟悉接口,所以以它为例。应用程序实际上并不是进程间的,分配器只能在我的应用程序中使用。
具体来说,我希望能够管理一个16GB的外部缓冲区,其分配大小从128字节一直到512MB。这是严格的64位代码,但即便如此我更喜欢指针类型作为模板参数,所以我可以明确地使用uint64_t。
答案 0 :(得分:1)
我不知道,可以使用任何可以使用的罐头实现。但是,使用C ++标准库中的花园种类容器,这似乎并不是特别难以实现。
我建议使用两个std::map
和一个std::multimap
的简单方法。让我们说bufaddr_t
是一个不透明的整数,表示外部缓冲区中的地址。由于我们正在讨论16 gig缓冲区,因此它必须是64位地址:
typedef uint64_t memblockaddr_t;
同样分配块的大小。
typedef uint64_t memblocksize_t;
我认为,只要不透明数据类型具有严格的弱排序,我就可以为memblockaddr_t
使用其他内容。
第一部分很简单。跟踪所有已分配的块:
std::map<memblockaddr_t, memblocksize_t> allocated;
因此,当您在外部缓冲区中成功分配一块内存时,请将其插入此处。当您希望释放一块内存时,在此处查找已分配块的大小,并删除映射条目。很简单。
但是,这当然不是整个故事。现在,我们需要跟踪可用的未分配内存块。让我们这样做:
typedef std::multimap<memblocksize_t, memblockaddr_t> unallocated_t;
unallocated_t unallocated;
std::map<memblockaddr_t, unallocated_t::iterator> unallocated_lookup;
unallocated
是外部缓冲区中所有未分配块的集合,由块大小键入。关键是块大小。因此,当您需要分配特定大小的内存块时,您可以简单地使用lower_bound()
方法(或upper_bound()
,如果您愿意)立即找到第一个大小最小的块尽可能多地分配。
当然,由于你可以有许多相同大小的块,unallocated
必须是std::multimap
。
此外,unallocated_lookup
是一个由每个未分配的块的地址键入的映射,它为unallocated
中的这个块条目提供了迭代器。为什么你需要它会在一瞬间变得清晰。
所以:
使用单个条目初始化一个新的,完全未分配的缓冲区:
memblockaddr_t beginning=0; // Or, whatever represents the start of the buffer.
auto p=unallocated.insert(std::make_pair(BUFFER_SIZE, beginning)).first;
unallocated_lookup.insert(std::make_pair(beginning, p));
然后:
要分配一个块,请使用我上面提到的lower_bound()/ upper_bound()方法来查找至少同样大的第一个未分配的块,并从unallocated
中删除它的条目和unallocated_lookup
。如果它超出了您的需要,请将多余的部分返还给游泳池,就像您不需要的额外金额被解除分配一样(下面的步骤3)。最后,将其插入allocated
数组,以便记住分配的块的大小。
要解除分配块,请在allocated
数组中查找,获取其大小,将其从allocated
数组中删除,然后:
将其插入unallocated
和unallocated_lookup
,类似于插入初始未分配的块的方式,请参见上文。
但你还没有完成。然后,您必须使用unallocated_lookup
在内存缓冲区中轻松查找前面未分配的块和以下未分配的块。如果它们中的任何一个或两个紧邻新释放的块,则必须将它们合并在一起。这应该是一个非常明显的过程。您可以简单地完成从unallocated
和unallocated_lookup
单独正式删除相邻块的动作,然后释放单个合并的块。
unallocated_lookup
的真正目的是能够轻松地合并连续的未分配块。
据我所知,上述所有操作都具有对数复杂性。它们完全基于具有对数复杂性的std::map
和std::multimap
方法,而不是其他任何方法。
最后:
根据您的应用程序的行为,您可以轻松地调整实现,以便将已分配块的大小内部向上舍入到您希望的任何倍数。或者调整分配策略 - 从足以满足分配请求的最小块分配,或者仅从大的未分配块(简单,使用end()
来查找它)等进行分配。
滚动自己的实现是一个优势 - 您可以更灵活地调整自己的实现,然后通常会使用一些固定的外部库。
答案 1 :(得分:0)
我正在发布我们实际上正在做的事情的更新。我最终实现了自己的远程内存分配器(下面的源代码)。它在精神上与answer Sam suggests类似,但在释放,加入等时使用boost侵入式RB树来避免一些log(N)查找。它是线程安全的并支持各种远程指针/偏移类型作为模板参数。它在很多方面可能并不理想,但它足以满足我们的需要。如果您发现错误,请告诉我。
/*
* Thread-safe remote memory allocator
*
* Author: Yuriy Romanenko
* Copyright (c) 2015 Lytro, Inc.
*
*/
#pragma once
#include <memory>
#include <mutex>
#include <cstdint>
#include <cstdio>
#include <functional>
#include <boost/intrusive/rbtree.hpp>
namespace bi = boost::intrusive;
template<typename remote_ptr_t = void*,
typename remote_size_t = size_t,
typename remote_uintptr_t = uintptr_t>
class RemoteAllocator
{
/* Internal structure used for keeping track of a contiguous block of
* remote memory. It can be on one or two of the following RB trees:
* Free Chunks (sorted by size)
* All Chunks (sorted by remote pointer)
*/
struct Chunk
{
bi::set_member_hook<> mRbFreeChunksHook;
bi::set_member_hook<> mRbAllChunksHook;
remote_uintptr_t mOffset;
remote_size_t mSize;
bool mFree;
Chunk(remote_uintptr_t off, remote_size_t sz, bool fr)
: mOffset(off), mSize(sz), mFree(fr)
{
}
bool contains(remote_uintptr_t off)
{
return (off >= mOffset) && (off < mOffset + mSize);
}
private:
Chunk(const Chunk&);
Chunk& operator=(const Chunk&);
};
struct ChunkCompareSize : public std::binary_function <Chunk,Chunk,bool>
{
bool operator() (const Chunk& x, const Chunk& y) const
{
return x.mSize < y.mSize;
}
};
struct ChunkCompareOffset : public std::binary_function <Chunk,Chunk,bool>
{
bool operator() (const Chunk& x, const Chunk& y) const
{
return x.mOffset < y.mOffset;
}
};
typedef bi::rbtree<Chunk,
bi::member_hook<Chunk,
bi::set_member_hook<>,
&Chunk::mRbFreeChunksHook>,
bi::compare< ChunkCompareSize > > FreeChunkTree;
typedef bi::rbtree<Chunk,
bi::member_hook<Chunk,
bi::set_member_hook<>,
&Chunk::mRbAllChunksHook>,
bi::compare< ChunkCompareOffset > > AllChunkTree;
// Thread safety lock
std::mutex mLock;
// Size of the entire pool
remote_size_t mSize;
// Start address of the pool
remote_ptr_t mStartAddr;
// Tree of free chunks
FreeChunkTree mFreeChunks;
// Tree of all chunks
AllChunkTree mAllChunks;
// This removes the chunk from both trees
Chunk *unlinkChunk(Chunk *c)
{
mAllChunks.erase(mAllChunks.iterator_to(*c));
if(c->mFree)
{
mFreeChunks.erase(mFreeChunks.iterator_to(*c));
}
return c;
}
// This reinserts the chunk into one or two trees, depending on mFree
Chunk *relinkChunk(Chunk *c)
{
mAllChunks.insert_equal(*c);
if(c->mFree)
{
mFreeChunks.insert_equal(*c);
}
return c;
}
/* This assumes c is 'free' and walks the mAllChunks tree to the left
* joining any contiguous free chunks into this one
*/
bool growFreeLeft(Chunk *c)
{
auto it = mAllChunks.iterator_to(*c);
if(it != mAllChunks.begin())
{
it--;
if(it->mFree)
{
Chunk *left = unlinkChunk(&(*it));
unlinkChunk(c);
c->mOffset = left->mOffset;
c->mSize = left->mSize + c->mSize;
delete left;
relinkChunk(c);
return true;
}
}
return false;
}
/* This assumes c is 'free' and walks the mAllChunks tree to the right
* joining any contiguous free chunks into this one
*/
bool growFreeRight(Chunk *c)
{
auto it = mAllChunks.iterator_to(*c);
it++;
if(it != mAllChunks.end())
{
if(it->mFree)
{
Chunk *right = unlinkChunk(&(*it));
unlinkChunk(c);
c->mSize = right->mSize + c->mSize;
delete right;
relinkChunk(c);
return true;
}
}
return false;
}
public:
RemoteAllocator(remote_size_t size, remote_ptr_t startAddr) :
mSize(size), mStartAddr(startAddr)
{
/* Initially we create one free chunk the size of the entire managed
* memory pool, and add it to both trees
*/
Chunk *all = new Chunk(reinterpret_cast<remote_uintptr_t>(mStartAddr),
mSize,
true);
mAllChunks.insert_equal(*all);
mFreeChunks.insert_equal(*all);
}
~RemoteAllocator()
{
auto it = mAllChunks.begin();
while(it != mAllChunks.end())
{
Chunk *pt = unlinkChunk(&(*it++));
delete pt;
}
}
remote_ptr_t malloc(remote_size_t bytes)
{
std::unique_lock<std::mutex> lock(mLock);
auto fit = mFreeChunks.lower_bound(
Chunk(reinterpret_cast<remote_uintptr_t>(mStartAddr),
bytes,
true));
/* Out of memory */
if(fit == mFreeChunks.end())
return remote_ptr_t{0};
Chunk *ret = &(*fit);
/* We need to split the chunk because it's not the exact size */
/* Let's remove the node */
mFreeChunks.erase(fit);
if(ret->mSize != bytes)
{
Chunk *right, *left = ret;
/* The following logic decides which way the heap grows
* based on allocation size. I am not 100% sure this actually
* helps with fragmentation with such a big threshold (50%)
*
* Check if we will occupy more than half of the chunk,
* in that case, use the left side. */
if(bytes > ret->mSize / 2)
{
right = new Chunk(left->mOffset + bytes,
left->mSize - bytes,
true);
relinkChunk(right);
left->mSize = bytes;
left->mFree = false;
ret = left;
}
/* We'll be using less than half, let's use the right side. */
else
{
right = new Chunk(left->mOffset + left->mSize - bytes,
bytes,
false);
relinkChunk(right);
left->mSize = left->mSize - bytes;
mFreeChunks.insert_equal(*left);
ret = right;
}
}
else
{
ret->mFree = false;
}
return reinterpret_cast<remote_ptr_t>(ret->mOffset);
}
remote_ptr_t malloc_aligned(remote_size_t bytes, remote_size_t alignment)
{
remote_size_t bufSize = bytes + alignment;
remote_ptr_t mem = this->malloc(bufSize);
remote_ptr_t ret = mem;
if(mem)
{
remote_uintptr_t offset = reinterpret_cast<remote_uintptr_t>(mem);
if(offset % alignment)
{
offset = offset + (alignment - (offset % alignment));
}
ret = reinterpret_cast<remote_ptr_t>(offset);
}
return ret;
}
void free(remote_ptr_t ptr)
{
std::unique_lock<std::mutex> lock(mLock);
Chunk ref(reinterpret_cast<remote_uintptr_t>(ptr), 0, false);
auto it = mAllChunks.find(ref);
if(it == mAllChunks.end())
{
it = mAllChunks.upper_bound(ref);
it--;
}
if(!(it->contains(ref.mOffset)) || it->mFree)
throw std::runtime_error("Could not find chunk to free");
Chunk *chnk = &(*it);
chnk->mFree = true;
mFreeChunks.insert_equal(*chnk);
/* Maximize space */
while(growFreeLeft(chnk));
while(growFreeRight(chnk));
}
void debugDump()
{
std::unique_lock<std::mutex> lock(mLock);
int i = 0;
printf("----------- All chunks -----------\n");
for(auto it = mAllChunks.begin(); it != mAllChunks.end(); it++)
{
printf(" [%d] %lu -> %lu (%lu) %s\n",
i++,
it->mOffset,
it->mOffset + it->mSize,
it->mSize,
it->mFree ? "(FREE)" : "(NOT FREE)");
}
i = 0;
printf("----------- Free chunks -----------\n");
for(auto it = mFreeChunks.begin(); it != mFreeChunks.end(); it++)
{
printf(" [%d] %lu -> %lu (%lu) %s\n",
i++,
it->mOffset,
it->mOffset + it->mSize,
it->mSize,
it->mFree ? "(FREE)" : "(NOT FREE)");
}
}
};