【问题标题】:C++ new allocates more space than expectedC++ new 分配的空间比预期的多
【发布时间】:2016-01-07 18:17:22
【问题描述】:

当内存要求很高时,我正在尝试测试一些 c++ 应用程序的行为,但似乎我无法使用所有可用的 ram。我有以下程序:

class Node {
    public:
        Node *next;
};


int main() {
    int i=0;

    Node *first = new Node();
    Node *last = first;

    //Should be   120000000 * 8 bytes each -> approx 1 GB
    for (i=0; i < 120000000; i++) {
        Node *node = new Node();
        node->next = 0;
        last->next = node;
        last = last->next;
    }


    for (i=0; i < 120000000; i++) {
        Node *oldfirst = first;
        first = first->next;
        delete oldfirst;
    }

    delete first;

    return 0;    
}

它应该分配大约 1 GB 的数据,因为 Node 类占用 8 个字节。我已经通过 sizeof、gdb 甚至 valgrind 验证了这一点。

然而,这个程序分配了大约 4 GB 的数据!如果我将这个大小加倍( 120000000 -> 2400000000 ),那么有 2 个选项(我的笔记本电脑安装了 8GB 的​​ RAM):

  • 如果我关闭了交换区,进程就会被内核杀死。
  • 如果没有,则会发生分页,操作系统变得非常慢。

关键是我无法测试分配 2 GB 数据的应用程序,因为它消耗 8 GB RAM!

我想可能是我请求新Node时分配的字节数超过8(即Node对象的大小),所以我尝试了以下:

class Node {
    public:
        Node *next;
        Node *second_next;
};


int main() {
    int i=0;

    Node *first = new Node();
    Node *last = first;

    //Should be   120000000 * 8 bytes each -> approx 1 GB
    for (i=0; i < 120000000; i++) {
        Node *node = new Node();
        node->next = 0;
        last->next = node;
        last = last->next;
    }


    for (i=0; i < 120000000; i++) {
        Node *oldfirst = first;
        first = first->next;
        delete oldfirst;
    }

    delete first;

    return 0;    
}

现在 Node 对象占用 16 个字节。应用程序的内存占用完全一样! 120000000 导致使用 4 GB 的 RAM,240000000 导致我的应用程序被 Linux 内核杀死。

所以我遇到了this post

C++ 中的每个 new 是否都至少分配 32 个字节?

【问题讨论】:

  • 底层分配器的确切行为将取决于操作系统和系统库。当然有可能它会分配一些最小块大小,但是不可能对 C++ 的行为做出一般性断言,就好像你深入挖掘一样,operator new 实际上只是向系统请求内存,可能是通过malloc().
  • 听起来您需要使用自定义分配器,或者更好的是,使用链表以外的其他东西。
  • @DavidSchwartz 这只是为了演示 - 它不是生产代码。我想知道一般来说动态分配的对象是如何放置在内存中的。
  • 可能是您的操作系统以固定大小的块分配内存吗?因此,如果它以2^n 的块分配,当您请求 120 个字节时,它将分配 128 个字节。如果这是正确的,它将在您请求分配略高于 1GB(2^30 字节)时分配 2^31 字节(2.15GB)。这只是一个猜测。我使用的操作系统会这样做,但使用斐波那契数列来计算块大小。
  • 完全可以预期内存分配的开销。我不知道你为什么感到惊讶。您应该意识到这一点并避免分配大量小对象。而是使用矢量。

标签: c++ memory-management malloc new-operator


【解决方案1】:

简短的回答 - 您忘记考虑内存分配开销。内存分配器本身需要跟踪分配的内存块,这本身会消耗内存,如果您分配了很多小块,那么与您请求的内存量相比,开销会变得不合理。然后还需要考虑块对齐,很多分配器都尝试智能并对齐内存块以获得最佳 CPU 访问速度,因此它们将与缓存行对齐。

最后但并非最不重要的一点是,为您提供 8 字节内存的成功请求很可能在幕后分配了更大的块。毕竟,向 malloc/new 询问特定数量的内存只能保证您将获得至少那个大小的块,而不是那个大小。

对于分配大量小块的用例,您需要研究诸如池分配器之类的东西,以最大限度地减少开销。

实际上,您可能应该考虑的是比具有许多小节点的非常大的链表更好的数据结构。

【讨论】:

  • 较小的对象并不总是意味着相应的较大开销。例如。 malloc 的优化实现可以通过为它们使用池以不同的方式处理小对象。
  • 当然,我永远不会编写这样的生产代码。我想实现内存耗尽,这就是我写这个简单实现的原因。但显然我对内存管理的看法是不正确的。无论如何,谢谢!
【解决方案2】:

如果您只是想了解您正在使用的分配,它有 8 字节开销,最少 32 字节,包括开销和 16 字节对齐。例如:

1 .. 24 字节:需要 32
25 .. 40 字节:需要 48
41 .. 56 字节:需要 64
等等。

如果你想有效地使用大量的小对象,你需要以其他方式分配它们(批量然后自己细分分配)。

【讨论】:

    【解决方案3】:

    这真的取决于malloc 的实现。我在我的机器(64位)上测试,如果我使用tcmalloc,大概需要1GB内存。在内部 tcmalloc 为不同的分配大小保留单独的池,因此对于每个池,不需要记录对象大小,这减少了小对象的开销。对于 8 字节的分配,完全没有开销。

    hidden $ cat c.cpp 
    #include <iostream>
    #include <string>
    using namespace std;
    
    struct Node { Node *next; };
    
    int main() {
        Node *first = new Node();
        Node *last = first;
        for (int i=0; i < 120000000; i++) {
            Node *node = new Node();
            node->next = 0;
            last = last->next = node;
        }
    
        cout << "Press <Enter> to continue...";
        string s;
        cin >> s;
        return 0;
    }
    hidden $ g++ -std=c++11 -O3 c.cpp /usr/lib/libtcmalloc_minimal.so.4
    hidden $ ./a.out & { sleep 5; ps -C a.out -o rss; killall a.out; }
    [1] 31500
    Press <Enter> to continue...
    [1]+  Stopped                 ./a.out
      RSS
    947064
    

    【讨论】:

      猜你喜欢
      • 2023-03-06
      • 2020-05-31
      • 1970-01-01
      • 1970-01-01
      • 2021-12-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多