(Owed by: 春夜喜雨 http://blog.csdn.net/chunyexiyu)
参考:样例代码使用豆包AI提示生成
在做数据处理过程中,经常面临处理字符数据量的不断增长,有时数据量可能达到GB以上,甚至几十GB的情况;
当需要把这些数据缓存到内存中时,如何合理的设定数据的存储结构,如何分配这么大的内存来存储数据,可能会是我们要面对的情况。
分块存储策略,是一个比较容易想到的策略,使用许多分块内存来满足数据的不断增长。
使用分块存储一块大内存,好处也比较明显,比如申请一个1GB的字符串,分块存储时,如果按照1MB来存储,申请1024个1MB的块即可。
而通常来说,申请1GB的困难程度,与申请1024个1MB块的困难程度,可能对于内存来说并不相同,通常我们认为申请1024个1MB块更容易一些,在内存碎块多时,也容易申请内存成功。
为了达成内存块的分配,会有多种分配块的方式来处理:
通常有不考虑记录顺序的-数据内存池结构;
还有需要考虑记录顺序的-连续数据块结构;
需要考虑数据顺序,也就是当作连续存储内容;
这个内存池分配使用有所不同,连续存储内容的分配,要求分配池本身提供数据顺序,这种情况下,需要在分配池内确定数据顺序了。
要保证数据的顺序,就需要分配内存时,有序分配,采用类似这样的分配池:
std::vector
分配池依次分配内存放入blocks分配池种,数据放置时,也依序放置,存在准则:
基于这两个准则,就可以明确了记录的数据顺序。
读取时,按照block之内从前往后读取,blocks之间,从前一个向后一个读取,就能保证数据按照放入顺序,依次被读取出来。
该种情况下,实现可以有两种:
这两种情况下,所形成的结果也不一样:
两种实现也各有优劣点。
对于固定大小block的情况:
如果采用固定大小block,那么无论block多大,都可能存在记录大小比它大的情况,就必然存在着数据的拆分。
另外采用固定大小block时,数据采用无间距形式,把前一个block的空间都填满后再用后一个block空间;
采用固定大小block时,一个读取位置可以直接推断出所在的blocks-index,所在的block的读取偏移位置offset;
readindex = pos / kFixedBlockSize;
readoffset = pos % kFixedBlockSize;
读取位置就可以直接推断出:blocks_[readindex] + readoffset
如果读取位置pos,读取长度len的数据时,
一方面计算出读取的位置,
另一方面还要考虑读取长度是不是全部在一个block中,如果不全部在一个block中,需要跨block读取内容;
整体代码实现形如:// 注:样例代码生成 by 豆包ai
#include
#include
#include
#include
class FixedBlockReader {
private:
static const size_t kBlockSize = 1024;
std::vector<char*> blocks_;
size_t totalSize_;
public:
FixedBlockReader() : totalSize_(0) {}
~FixedBlockReader() { for (char* block : blocks_) delete[] block; }
void addData(const char* data, size_t size) {
if (!data || size == 0) return;
size_t remaining = size;
const char* currentPos = data;
size_t blocksNeeded = (remaining + kBlockSize - 1) / kBlockSize;
for (size_t i = 0; i < blocksNeeded; ++i) {
char* newBlock = new char[kBlockSize];
if (!newBlock) throw std::runtime_error("内存分配失败");
size_t bytesToCopy = (remaining > kBlockSize) ? kBlockSize : remaining;
std::memcpy(newBlock, currentPos, bytesToCopy);
currentPos += bytesToCopy;
remaining -= bytesToCopy;
blocks_.push_back(newBlock);
}
totalSize_ += size;
}
size_t readData(char* buffer, size_t pos, size_t len) const {
if (pos >= totalSize_) return 0;
if (pos + len > totalSize_) len = totalSize_ - pos;
size_t bytesRead = 0;
size_t remaining = len;
size_t readIndex = pos / kBlockSize;
size_t readOffset = pos % kBlockSize;
for (size_t i = readIndex; i < blocks_.size() && remaining > 0; ++i) {
const char* currentBlock = blocks_[i];
size_t bytesInBlock = kBlockSize - readOffset;
size_t bytesToRead = (bytesInBlock < remaining) ? bytesInBlock : remaining;
std::memcpy(buffer + bytesRead, currentBlock + readOffset, bytesToRead);
bytesRead += bytesToRead;
remaining -= bytesToRead;
readOffset = 0;
}
return bytesRead;
}
size_t getTotalSize() const { return totalSize_; }
};
int main() {
try {
FixedBlockReader reader;
const char* data1 = "This is the first block of data. ";
const char* data2 = "This is the second block, which contains more information. ";
const char* data3 = "This is the third and final block.";
reader.addData(data1, strlen(data1));
reader.addData(data2, strlen(data2));
reader.addData(data3, strlen(data3));
char buffer[200];
size_t bytesRead1 = reader.readData(buffer, 0, 30);
buffer[bytesRead1] = '\0';
std::cout << "读取数据1 (" << bytesRead1 << " 字节): " << buffer << std::endl;
size_t bytesRead2 = reader.readData(buffer, 50, 40);
buffer[bytesRead2] = '\0';
std::cout << "读取数据2 (" << bytesRead2 << " 字节): " << buffer << std::endl;
size_t bytesRead3 = reader.readData(buffer, 100, 200);
buffer[bytesRead3] = '\0';
std::cout << "尝试读取超出范围: " << bytesRead3 << " 字节" << std::endl;
} catch (const std::exception& e) {
std::cerr << "错误: " << e.what() << std::endl;
return 1;
}
return 0;
}
非固定大小的块的情况和leveldb的Arena情况有一点像,不过也不一样,因为Arena不考虑数据写入的次序,分配前后的Arena记录之间不保证下面这一点:
这种情况,就需要每个block自己记录自身分配的量,使用的量,剩余的量;
另外放置数据时,发现最后一个block的空间够用就用,不够用就申请新的来用,申请时确保当前记录能放下;
实现的代码形如:// 注:样例代码生成 by 豆包ai
#include
#include
#include
class VariableBlockReader {
private:
static const size_t kDefaultBlockSize = 1024;
struct Block {
char* data;
size_t allocated;
size_t used;
Block() : data(nullptr), allocated(0), used(0) {}
~Block() { delete[] data; }
};
std::vector<Block> blocks_;
size_t totalSize_;
public:
VariableBlockReader() : totalSize_(0) {}
~VariableBlockReader() {}
void addData(const char* data, size_t size) {
if (!data || size == 0) return;
// 如果有块且数据可以完全放入最后一个块的剩余空间
if (!blocks_.empty()) {
Block& lastBlock = blocks_.back();
size_t freeSpace = lastBlock.allocated - lastBlock.used;
// 如果数据可以完全放入剩余空间且剩余空间足够大
if (size <= freeSpace && freeSpace >= kDefaultBlockSize / 4) {
std::memcpy(lastBlock.data + lastBlock.used, data, size);
lastBlock.used += size;
totalSize_ += size;
return; // 数据已完全添加,直接返回
}
}
// 创建新块,大小为数据大小或默认块大小中的较大值
size_t blockSize = (size > kDefaultBlockSize) ? size : kDefaultBlockSize;
Block newBlock;
newBlock.data = new char[blockSize];
if (!newBlock.data) throw std::runtime_error("内存分配失败");
newBlock.allocated = blockSize;
// 复制数据到新块
std::memcpy(newBlock.data, data, size);
newBlock.used = size;
// 添加新块并更新总大小
blocks_.push_back(std::move(newBlock));
totalSize_ += size;
}
size_t readData(char* buffer, size_t pos, size_t len) const {
if (pos >= totalSize_) return 0;
if (pos + len > totalSize_) len = totalSize_ - pos;
size_t bytesRead = 0;
size_t remaining = len;
size_t currentPos = pos;
size_t blockIndex = 0;
while (blockIndex < blocks_.size() && currentPos >= blocks_[blockIndex].used) {
currentPos -= blocks_[blockIndex].used;
++blockIndex;
}
for (size_t i = blockIndex; i < blocks_.size() && remaining > 0; ++i) {
const Block& block = blocks_[i];
size_t bytesInBlock = block.used - currentPos;
size_t bytesToRead = (bytesInBlock < remaining) ? bytesInBlock : remaining;
std::memcpy(buffer + bytesRead, block.data + currentPos, bytesToRead);
bytesRead += bytesToRead;
remaining -= bytesToRead;
currentPos = 0;
}
return bytesRead;
}
size_t getTotalSize() const { return totalSize_; }
void printBlockInfo() const {
std::cout << "块数量: " << blocks_.size() << ", 总大小: " << totalSize_ << " 字节" << std::endl;
for (size_t i = 0; i < blocks_.size(); ++i) {
const Block& block = blocks_[i];
std::cout << "块 " << i << ": 分配=" << block.allocated << " 字节, 使用=" << block.used
<< " 字节, 剩余=" << (block.allocated - block.used) << " 字节" << std::endl;
}
}
};
存储的数据放入次序不关注,只关注使用内存时,能申请到合适内存;
如果需要顺序,使用外部的其它结构来记录数据顺序,或特定数据的顺序。
这块的数据池,不同数据块之间,放入数据次序不固定,对外表现是一个内存池的特点,也就是一个内存分配池的作用。实现的方式有许多,基于大小采用多个不同的分配池,也或使用一个分配池,适配不同大小。
对于内存池这种类型的分配方式,这块不做细的介绍,因为公开的实现有许多,就举一个简答的例子。
比较简单的例子是Leveldb的Arena内存池分配机制,Arena本质是作为数据存储池作用。
Arena为MemTable的内容提供数据分配池,MemTable通过skiplist来确定数据的键值排序,并不通过Arena确定数据顺序。
Arena分配的机制时,通过分配块来满足外部使用内存的需求,关注内存分配的总量,但不关注分配的内存块的连续性,分配主要关注的:
Arena形成的分配策略是:
Arena分配策略的实现代码:// 注:样例代码取自leveldb源码
inline char* Arena::Allocate(size_t bytes) {
// The semantics of what to return are a bit messy if we allow
// 0-byte allocations, so we disallow them here (we don't need
// them for our internal use).
assert(bytes > 0);
if (bytes <= alloc_bytes_remaining_) {
char* result = alloc_ptr_;
alloc_ptr_ += bytes;
alloc_bytes_remaining_ -= bytes;
return result;
}
return AllocateFallback(bytes);
}
char* Arena::AllocateFallback(size_t bytes) {
if (bytes > kBlockSize / 4) {
// Object is more than a quarter of our block size. Allocate it separately
// to avoid wasting too much space in leftover bytes.
char* result = AllocateNewBlock(bytes);
return result;
}
// We waste the remaining space in the current block.
alloc_ptr_ = AllocateNewBlock(kBlockSize);
alloc_bytes_remaining_ = kBlockSize;
char* result = alloc_ptr_;
alloc_ptr_ += bytes;
alloc_bytes_remaining_ -= bytes;
return result;
}
综上,无论采用哪种方式,使用分配池的多块存储大块内容的方式,都可以算作对大块内存申请下的一种优化。这种把一整块内存申请化为多个blocks的形式:
总之,使用Arena这种分块分配内存方式,是对内存与CPU都友好型的一种大内存申请使用优化。
(Owed by: 春夜喜雨 http://blog.csdn.net/chunyexiyu)