【问题标题】:Custom malloc for lots of small, fixed size blocks?为大量固定大小的小块定制 malloc?
【发布时间】:2011-04-07 01:30:57
【问题描述】:

我需要分配和释放大量固定大小的小(16 字节)内存块,没有固定的顺序。我可以只为每个调用 malloc 和 free,但这可能会非常低效。更好的解决方案可能是为更大的块调用 malloc 和 free,并自行处理这些块内的分配。

问题是,如何最好地做到这一点?

看起来这应该不是一个非常不寻常的问题或罕见的问题,应该已经“解决”了,但我似乎找不到任何东西。有什么指点吗?

为了澄清,我知道内存池库和不存在的东西,但这些也需要一个大小参数。如果大小是恒定的,那么可以使用不同的更高效算法的选项,是否有这些实现?

【问题讨论】:

  • 您是否需要这种内存“一次性”,例如某些算法,或者您是否会在程序生命周期内执行此操作(可能很长?)
  • 开发分配库的标准之一是它在这种情况下的表现如何,甚至在不友好的情况下。虽然实现自定义分配例程很有趣,但您可能不需要它。
  • @Skurmedel - 在整个程序执行过程中会请求和释放大量内存。
  • “如果大小不变,则可以使用不同的选项来获得更高效的算法” - 效率并不高,我看不出你会在哪里找到算法差异。明显的优势是添加/减去立即常量而不是存储变量,这是相当小的啤酒。如果你手头有一个内存池库,那么修改它以硬编码 16 的大小并与原始库进行基准测试应该很容易。使用 C++,您可以(和标准库一样)将其作为模板参数,或者对于 C,使用宏并针对每种大小进行编译。
  • 如果您担心速度,这不是问题。如果您担心内存消耗过多,malloc 的任何典型实际实现都会在您的负载下产生 50% 的开销(预期内存使用量的 1.5 倍)。

标签: c malloc


【解决方案1】:

你说得对,这是一个常见问题 [编辑:我的意思是,如何进行固定大小的分配。 “malloc 正在降低我的应用程序速度”并不像您想象的那么常见]。

如果您的代码太慢并且malloc 可能是罪魁祸首,那么简单的单元分配器(或“内存池”)可能会有所改善。您几乎可以肯定在某个地方找到一个,或者很容易编写:

分配一个大块,并在每个 16 字节单元的开头放置一个单链表节点。将它们连接在一起。要进行分配,请将头从列表中取出并归还。要释放,请将单元格添加到列表的头部。当然如果你尝试分配而列表为空,那么你必须分配一个新的大块,将其划分为单元格,并将它们全部添加到空闲列表中。

如果您愿意,您可以避免进行大量的前期工作。当你分配一个大块时,只需存储一个指向它末尾的指针。要进行分配,请将指针向后移动 16 个字节并返回新值。当然,除非它已经在块 [*] 的开头。如果发生这种情况,并且空闲列表也是空的,则需要一个新的大块。免费不会改变 - 只需将节点添加到空闲列表即可。

您可以选择是先处理出块,如果已用尽则检查空闲列表,或者先检查空闲列表,如果为空则处理块。我不知道哪个会更快——后进先出空闲列表的好处是它对缓存友好,因为您使用的是最近使用的内存,所以我可能会尝试首先。

请注意,分配单元时不需要列表节点,因此每个单元的开销基本上为零。除了速度之外,这可能比malloc 或其他通用分配器更有优势。

请注意,删除整个分配器几乎是将内存释放回系统的唯一方法,因此计划分配大量单元、使用它们并全部释放它们的用户应该创建自己的分配器,使用它,然后销毁它。既是为了性能(您不必释放所有单元格),也是为了防止碎片式效果,如果其中任何一个单元格正在使用,则必须保留整个块。如果你不能这样做,你的内存使用将是你的程序运行时间的高水位线。对于一些有问题的程序(例如,在内存受限的系统上,内存使用偶尔出现大幅峰值的长期运行程序)。对于其他人来说,这绝对没问题(例如,如果正在使用的单元格数量增加直到非常接近程序结束,或者在你真的不关心你使用的内存比你严格可以使用的更多内存的范围内波动)。对于某些积极的需求(如果您知道要使用多少内存,您可以预先分配所有内存,而不必担心失败)。就此而言,malloc 的某些实现难以将内存从进程释放回操作系统。

[*] 其中“块的开始”可能意味着“块的开始,加上用于维护所有块列表的某个节点的大小,因此当单元分配器被销毁时,它们都可以被释放”。

【讨论】:

  • malloc 可能是罪魁祸首,但急于寻求新的解决方案是错误的想法。在所有情况下,在测量之前进行优化都是错误的。如果不进行测量,您怎么知道要修复什么?
  • @JaredPar:如果你不与其他任何东西进行比较,你会衡量什么? malloc 在以下两种情况下可能是罪魁祸首:要么分析器显示在那里花费了大量时间,要么你过去每次都编写过这样的代码。一个简单的单元分配器需要花费半个小时来编写(或者如果您在获取最后一个写入之前已经到过那里)。我不急于任何事情。无论如何都要说“首先配置文件”,但是如果您不说如果配置文件显示 malloc 存在问题该怎么办,那么您实际上还没有回答这个问题。
  • @Steve,你连接了一个分析器,看看你的应用程序的时间都花在了哪里。如果是 malloc,则调查替换或调查您的使用情况。但它也可能是函数foo,你有一个拼写错误或错误的算法占用了时间。在没有测量的情况下替换 malloc 正在急于找到解决方案。如果你不衡量,你根本不知道你在修复什么。
  • 是的,这就是我刚才所说的,如果您阅读我的回答,您会发现这一切都取决于malloc 使用是一个合理的优化目标。没有充分理由编写分配器是急于寻求解决方案。 告诉某人如何编写分配器,而不要求他们首先向我提交一式三份的证明,证明他们的应用程序需要一个,而不是急于找到解决方案。它免费提供信息。
  • @JaredPar:除了速度问题,如果两段代码经常分配和释放大小不同的东西,可能会出现严重的内存碎片。因此,大量使用 malloc 和 free 的代码可能是“脆弱的”。并不总是可以预测坏事何时会发生,因此对于生命周期较短的小对象避免使用 malloc() 可能是明智的。
【解决方案2】:

在开始重写malloc 的繁重任务之前,请遵循标准建议。分析您的代码,并确保这确实是一个问题!

【讨论】:

    【解决方案3】:

    做到这一点的最好方法是不要假设它效率低下。而是尝试使用 malloc 的解决方案,测量性能并证明它是否有效。然后,一旦它提供低效(可能不会)是您应该转移到自定义分配器的唯一时间。如果没有证据,您永远不会知道您的解决方案是否真的更快。

    【讨论】:

      【解决方案4】:

      根据您的要求,您的自定义分配器将非常简单。只是 calloc 一个大的数组内存

      calloc(N * 16)
      

      然后你就可以分发数组条目了。为了跟踪正在使用的数组位置,您可以使用一个简单的位图,然后通过一些巧妙的位操作和指针减法,您的自定义 malloc/free 操作应该很容易。如果你的空间用完了,你可以再 realloc 多一些,但是有一个合适的固定默认值会更容易一些。

      虽然你真的应该先使用mallocmalloc 创建不同大小的空闲内存块池,我敢打赌,有一个用于 16 字节内存块的池(不同的实现可能会或可能不会这样做,但它是一个非常常见的优化),因为你所有的分配都是相同的大小碎片应该不是问题。 (加上调试你的分配器可能有点噩梦。)

      【讨论】:

        【解决方案5】:

        您要查找的内容称为内存池。有现成的实现,尽管自己制作并不难(也是很好的做法)。

        对于相同大小的数据池,最简单的实现只是一个包含 n*size 缓冲区和 n 个指针堆栈的包装器。池中的“malloc”将指针从顶部弹出。池中的“空闲”将指针放回堆栈。

        【讨论】:

        • 这不允许我一次释放大块,也不允许分配它们,除非我有办法判断整个块是否是从链表中的不同点引用的。这确实需要一些实施思考,我正在寻找是否有一个可以插入的合理解决方案。
        【解决方案6】:

        您可以尝试覆盖适合大量小分配的 malloc/free with an alternative implementation

        【讨论】:

        • 像这样天真地使用 dlmalloc 会产生 50% 到 100% 的内存开销。痛苦。
        • @mmmmalloc:如果您说的是 8 或 16 字节的每个分配标头,那么不,它不是,因为您的系统 malloc 已经有每个分配的等效开销。
        【解决方案7】:

        由于学术兴趣,几天前我正在研究解决该问题的方法。实现非常简单但完整,您提到您正在寻找一个替代品,所以我认为我的实现可以为您工作。

        基本上它的工作方式与描述的 patros 类似,只是如果不再有空闲块,它会自动请求更多内存。该代码使用一个大型链表(大约 6 百万个节点,每个节点大小为 16 个字节)针对一个幼稚的 malloc()/free() 方案进行了测试,并且比这快了大约 15%。所以据说它可用于您的意图。很容易将其调整为不同的块大小,因为在创建如此大的内存块时指定了块大小。

        代码在github上:challoc

        示例用法:

        int main(int argc, char** argv) {
            struct node {
                   int data;
               struct node *next, *prev;
            };
            // reserve memory for a large number of nodes
            // at the moment that's three calls to malloc()
            ChunkAllocator*  nodes = chcreate(1024 * 1024, sizeof(struct node));
        
            // get some nodes from the buffer
            struct node* head = challoc(nodes);
            head->data = 1;
            struct node* cur = NULL;
            int i;
            // this loop will be fast, since no additional
            // calls to malloc are necessary
            for (i = 1; i < 1024 * 1024; i++) {
                    cur = challoc(nodes);
                cur->data = i;
                cur = cur->next;
            }
        
            // the next call to challoc(nodes) will
            // create a new buffer to hold double
            // the amount of `nodes' currently holds
        
            // do something with a few nodes here
        
            // put a single node back into the buffer
            chfree(nodes,head);
        
            // mark the complete buffer as `empty'
            // this also affects any additional
            // buffers that have been created implicitly
            chclear(nodes);
        
            // give all memory back to the OS
            chdestroy(nodes);
        
            return 0;
        }
        

        【讨论】:

          【解决方案8】:

          Wilson、Johnstone、Neely 和 Boles 写信给 a nice paper surveying all sorts of different allocators

          根据我的经验,如果您在有限的地址空间(例如没有页面文件的系统)。在我目前正在开发的应用程序中,如果我将块分配器替换为对 malloc() 的简单调用(最终由于碎片而崩溃),我们的主循环将从 30ms 跳转到 >100ms。

          【讨论】:

            【解决方案9】:

            下面的代码很丑,但目的不是美观,而是要找出malloc分配的块有多大。
            我请求了 4 个字节,malloc 从 OS 请求并收到了 135160 个字节。

            #include <stdio.h>
            #include <malloc.h>
            
            
            int main()
            {
              int* mem = (int*) malloc( sizeof(int) ) ;
              if(mem == 0) return 1;
              long i=1L;
            
              while(i)
                {
                  mem[i-1] = i;
                  printf("block is %d bytes\n", sizeof(int) * i++);
                }//while
            
              free(mem);
              return 0 ;
            }
            

            $ g++ -o 文件 file.cpp
            $ ./文件
            ...
            块是 135144 字节
            块为 135148 字节
            块是 135152 字节
            块为 135156 字节
            块为 135160 字节
            分段错误

            这个 malloc 是一件严肃的事情。
            如果由于内部池化而请求的大小小于可用大小,realloc 不会执行任何系统调用。
            realloc 将内存复制到更大的区域后,它既不会破坏前一个块,也不会立即将其返回给系统。这仍然可以访问(当然完全不安全)。 有了所有这些,对我来说没有意义,有人需要一个额外的内存池。

            【讨论】:

            • 访问未分配给程序的内存是未定义的行为,这样做并不一定会导致分段错误。
            猜你喜欢
            • 2013-08-12
            • 1970-01-01
            • 2014-01-03
            • 2011-06-07
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多