动态分配许多小块内存
Posted
技术标签:
【中文标题】动态分配许多小块内存【英文标题】:Dynamically allocate many small pieces of memory 【发布时间】:2015-07-26 16:59:09 【问题描述】:我认为这是一个非常普遍的问题。我举个例子吧。
我有一个文件,其中包含很多行(例如一百万行),每一行的格式如下:首先是一个数字X
,然后是一个长度为X
的字符串。
现在我想读取文件并存储所有字符串(无论出于何种原因)。通常,我会做的是:对于每一行,我读取长度X
,并使用malloc
(C 语言)或new
(C++)分配X
字节,然后读取字符串。
我不喜欢这种方法的原因是:可能大多数字符串都很短,比如不到 8 个字节。那样的话,按照我的理解,分配会很浪费时间和空间。
(这里的第一个问题:我理解正确吗,分配小块内存很浪费?)
我考虑过以下优化:每次我分配一个大块,比如 1024 字节,每当需要一小块时,就从大块中切下它。这种方法的问题在于,释放几乎是不可能的......
这听起来像是我想自己做内存管理......但是,我还是想知道是否存在更好的方法?如果需要,我不介意使用一些数据结构来进行管理。
如果您有一些仅在条件下有效的好主意(例如,知道大多数片段都很小),我也很乐意知道。
【问题讨论】:
“我认为这是一个很常见的问题。” – 是的! 没错。一个常见的解决方案是使用内存池 - 与您通过预分配大块描述的大致相同。 为什么需要从文件中动态分配每个新字符串?就像我上面的那个人说的那样,你可以分配一个大的缓冲区来读取字符串,而不是许多单一的分配 问题是如果我使用内存池分配内存,我将无法释放内存。这可能是个大问题,对吧? 内存池当然是一种选择。然而,std::string
的大多数实现都有一个“小字符串优化”,其中小于一定长度的字符串根本不需要动态分配。鉴于您想从文件中读取“字符串”,我想不出任何不使用 std::string
的充分理由。
C 还是 C++?体面的内存管理在两者之间完全不同。
【参考方案1】:
进行内存分配的“自然”方式是确保每个内存块至少足够大以包含指针和大小,或一些足以维持空闲节点结构的类似簿记。细节各不相同,但您可以通过查看在进行少量分配时从分配器返回的实际地址来实验性地观察开销。
这就是小分配“浪费”的意义。实际上,对于大多数 C 或 C++ 实现,all 块会被舍入为 2 的某个幂的倍数(幂取决于分配器,有时取决于分配的数量级大小)。所以所有的分配都是浪费的,但从比例上讲,如果将大量 1 和 2 字节分配填充到 16 字节,比将大量 113 和 114 字节分配填充到 128 字节时浪费更多。
如果您愿意放弃仅释放和重用单个分配的能力(例如,如果您打算在担心此文件的内容后将所有分配一起释放,这很好) 那么可以肯定,您可以以更紧凑的方式分配许多小字符串。例如,将它们全部放在一个或几个大分配中,每个字符串都以 nul 结尾,并处理指向每个字符串的第一个字节的指针。开销是每个字符串 1 个或 0 个字节,具体取决于您如何考虑 nul。如果您只是用 nul 字节覆盖换行符,这在将文件拆分为行的情况下可以特别巧妙地工作。显然,您不必介意每行都删除了换行符!
如果您需要释放和重用,并且您知道 所有 分配的大小相同,那么您可以从簿记中删除大小,并编写自己的分配器 (或者,在实践中,找到一个您满意的现有池分配器)。最小分配大小可以是一个指针。但是,如果 所有 字符串都小于指针的大小,那么这只是一个轻松的胜利,“大多数”并不是那么简单。
【讨论】:
【参考方案2】:是的,静态分配一个大的缓冲区并读入它是读取数据的常用方法。
假设您选择 1KB 作为缓冲区大小,因为您希望大多数读取都适合该大小。
您能否将超过 1KB 的罕见读取分成多个读取?
那就这样做吧。还是不行?
然后您可以在必要时动态分配。一些简单的指针魔术就可以完成这项工作。static const unsigned int BUF_SIZE = 1024;
static char buf[BUF_SIZE];
while (something)
const unsigned int num_bytes_to_read = foo();
const char* data = 0;
if (num_bytes_to_read <= BUF_SIZE)
read_into(&buf[0]);
data = buf;
else
data = new char[num_bytes_to_read];
read_into(data);
// use data
if (num_bytes_to_read > BUF_SIZE)
delete[] data;
此代码是 C、C++ 和伪代码的令人愉快的混搭,因为您没有指定语言。
如果您实际使用的是 C++,请使用向量,看在上帝的份上;如果需要,让它增长,否则只需重新使用它的存储空间。
【讨论】:
【参考方案3】:你可以先计算文本的行数和总长度,然后分配一块内存来存储文本,然后分配一块内存来存储指针。通过第二次读取文件来填充这些块。请记住添加终止零。
【讨论】:
【参考方案4】:如果整个文件都适合内存,那么为什么不获取文件的大小,分配那么多内存和足够的指针,然后读入整个文件并创建一个指向文件中行的指针数组?
【讨论】:
【参考方案5】:我会使用最大的缓冲区来存储“x”。 您没有告诉我们 x 的最大大小为 sizeof(x)。我认为将它存储在缓冲区中以逃避每个单词的寻址并相对快速地访问它们是至关重要的。
类似:
char *buffer = "word1\0word2\0word3\0";
同时为“快速”访问存储地址或...等。
变成了这样:
char *buffer = "xx1word1xx2word2xx3word3\0\0\0\0";
正如您所见,使用固定大小的 x 可以非常有效地逐字跳转,而无需存储每个地址,只需要读取 x 并使用 x 将地址递增 ... x 不转换为 char,integer 注入并使用他的类型大小读取,这种方式不需要字符串的结尾 \0,只有完整的 buff 才能知道缓冲区的结尾(如果 x==0 那么结束了)。
由于我的英语,我不太擅长解释我给你一些代码作为更好的解释:
#include <stdio.h>
#include <stdint.h>
#include <string.h>
void printword(char *buff)
char *ptr;
int i;
union
uint16_t x;
char c[sizeof(uint16_t)];
u;
ptr=buff;
memcpy(u.c,ptr,sizeof(uint16_t));
while(u.x)
ptr+=sizeof(u.x);
for(i=0;i<u.x;i++)printf("%c",buff[i+(ptr-buff)]);/*jump in buff using x*/
printf("\n");
ptr+=u.x;
memcpy(u.c,ptr,sizeof(uint16_t));
void addword(char *buff,const char *word,uint16_t x)
char *ptr;
union
uint16_t x;
char c[sizeof(uint16_t)];
u;
ptr=buff;
/* reach end x==0 */
memcpy(u.c,ptr,sizeof(uint16_t));
while(u.x)ptr+=sizeof(u.x)+u.x;memcpy(u.c,ptr,sizeof(uint16_t));/*can jump easily! word2word*/
/* */
u.x=x;
memcpy(ptr,u.c,sizeof(uint16_t));
ptr+=sizeof(u.x);
memcpy(ptr,word,u.x);
ptr+=u.x;
memset(ptr,0,sizeof(uint16_t));/*end of buffer x=0*/
int main(void)
char buffer[1024];
memset(buffer,0,sizeof(uint16_t));/*first x=0 because its empty*/
addword(buffer,"test",4);
addword(buffer,"yay",3);
addword(buffer,"chinchin",8);
printword(buffer);
return 0;
【讨论】:
以上是关于动态分配许多小块内存的主要内容,如果未能解决你的问题,请参考以下文章