【问题标题】:C++ objects memory consumptionC++ 对象内存消耗
【发布时间】:2016-08-30 12:31:39
【问题描述】:

首先:这个问题不是关于“如何使用删除运算符”,而是关于“为什么许多小尺寸的类对象会消耗大量内存”。假设我们有这段代码:

class Foo
{

};

void FooTest()
{
    int sizeOfFoo = sizeof(Foo);

    for (int i = 0; i < 10000000; i++)
        new Foo();
}

空类 Foo 的大小为 1 字节,但执行代码时会消耗大约 600Mb 内存。怎么样?

更新。我已经在 Visual Studio 2010 中的 Win10 x64 上对此进行了测试。来自操作系统任务管理器的内存使用情况。

【问题讨论】:

  • 您是否在调试模式下运行?
  • 内存分配有开销。多少取决于实施。
  • 好读:stackoverflow.com/a/15881440/1870760 关于内存开销
  • 我刚刚在 VS 2015 Win 7 x64 上对此进行了测试,而我的应用程序在发布时仅使用了 160 MB。 2010 年的 malloc 实现并没有我想象的那么好。
  • 我的印象是您仍在调试中运行,在 x64 系统上对我们来说都是 160。

标签: c++ memory memory-leaks new-operator


【解决方案1】:

Foo 类的大小可能为 1 个字节,但由于您单独分配了许多 Foos,它们可以(并且可能确实)在某些字节对齐的地址上分配,并且由于碎片消耗的内存比您预期的要多。

此外,还有内部内存管理系统使用的内存。

【讨论】:

  • 仍然是 16b*10,000,000 = 160Mb(分配 16b 对齐的块时),即使实际分配了 32b(16b 用于内存管理),我们仍然“仅”达到 320Mb。
  • @Walter - 这是答案的一部分,这有什么不正确的吗?
  • 好吧,它没有解释 600Mb,而只是(最多)320Mb——那么剩下的内存呢?
  • @Walter,说得好。我也对其他内存在哪里使用感兴趣。
  • @AlekDepler:您如何测量内存使用情况?也许在 main 甚至运行之前预先分配了 512MB,你排除了这个吗?运行时可能会从系统分配 600MB 以更快地进行进一步分配,但并非所有这些内存实际上都被您的程序对象消耗(即,您可以分配更多对象并且内存使用量不会增加)。您需要更具体地了解如何测量事物以及运行时能够实际找到该内存的去向。
【解决方案2】:

C++ 堆管理器有 4 种不同的“模式”,它在对象周围保留更少或更多的空间。这些是

  1. 释放模式,正常运行
  2. 发布模式,在调试器下运行
  3. 调试模式,正常运行
  4. 调试模式,在调试器下运行

额外的内存用于无人区 (0xFDFDFDFD)、对齐 16 字节边界 (0xBAADF00D)、堆管理等。

我建议阅读 this post 并在调试器中查看 4 个场景,打开原始内存视图。你会学到很多东西。对于情况 1 和 3,插入一个暂停,您可以将调试器附加到正在运行的进程,而在情况 2 和 4 中,您应该先运行调试器,然后从那里启动可执行文件。

我曾经在解释缓冲区溢出时演示 C++ 堆的工作原理。这是一个您可以使用的演示程序,并不完美,但可能有用:

#include "stdafx.h"
#include <Windows.h>
#include <iostream>
#include <stdio.h>
void SimpleBufferOverrunDemo( int argc, _TCHAR* argv[] ) ;

int _tmain(int argc, _TCHAR* argv[])
{
    SimpleBufferOverrunDemo(argc, argv);

    getchar();
    return 0;
}

void SimpleBufferOverrunDemo( int argc, _TCHAR* argv[] ) 
{
    if (argc != 2)
    {
        std::cout << "You have to provide an argument!\n";
        return;
    }

    // Allocate 5 bytes
    byte* overrunBuffer = new byte[5];

    // Demo 1: How does the memory look after delete? Uncomment the following to demonstrate
    //delete [] overrunBuffer; //0xfeeefeee in debug mode.
    //DebugBreak();

    // Demo 2: Comment Demo 1 again. 
    // Provide a 5 byte sequence as argument
    // Attach with WinDbg and examine the overrunBuffer.

    // 2.1. How many heaps do we have?
    // !heap -s

    // 2.2. How to find the heap block and how large is it?
    // !heap -x [searchAddress]
    // !heap -i [blockAddress] -> Wow 72 bytes block size for 5 allocated bytes!

    // 2.3. Show that _HEAP_ENTRY does not work any more.

    // Demo 3: Write behind the 5 allocated bytes.
    // Provide more than 5 bytes as argument, depending on how what you want to destroy
    // 3.1 Write into the no mans land.
    // 3.2 Write into the guard bytes.
    // 3.3 Write into the meta data section of the following heap block! -> When does it crash?

    std::wstring arg = argv[1];

    for (size_t i = 0; i < arg.size(); i++)
    {
        overrunBuffer[i] = (byte)arg[i];
    }

    // Crash happens not where it was caused(!) This is important!
    std::cout << "Now we do a plenty of other work ...";
    ::Sleep(5000);

    delete[] overrunBuffer;

    // Demo 4: Demonstrate page heap / application verifier!
}

【讨论】:

  • 我现在明白了。对于我的示例,不同模式的内存值确实不同:从没有调试器的发布模式下的 150mb 到调试器下的调试模式下的 600mb。奇怪的世界。
  • @AlekDepler:额外的内存为调试提供了额外的好处。请参阅缓冲区溢出演示程序的更新答案。
  • @AlekDepler:我希望你不小心翻转了这些值。释放模式需要更少的内存。
  • 是的,我翻转了它们)那么对于我的示例来说,将内存消耗减少到 150mb 以下是不可能的吗?
  • @AlekDepler:正如 Amit 所提到的,数组应该会有所帮助,因为这只会产生 1 个大分配 + 1 个开销,而不是许多小分配 + 很多开销。请注意,这不会无限扩展。您可能会遇到总共有足够内存的问题,但不是一个大的空闲块(数组需要)。这就是所谓的“内存碎片”。尝试 1800 次分配 1MB 字节[] 与单个 1800MB 字节[] 分配。但那是你希望不需要关心的疯狂的高级东西。
【解决方案3】:

您必须知道,从操作系统的角度来看,进程消耗的内存不仅与代码中分配的对象有关。

这是严重依赖于实现的东西,但一般来说,出于性能原因,内存分配很少一对一地传递给操作系统,而是pooled via the management 的免费存储。大体原理如下:

  • 在程序启动时,您的 C++ 实现将为空闲存储和堆分配一些初始空间。
  • 当此内存被消耗并需要新内存时,内存管理将向操作系统请求更大的块并在空闲存储中提供这些块。
  • 从操作系统请求的块大小可能会根据分配模式自行调整。

因此,从任务管理器中查看的 600MB 来看,可能只有一小部分有效分配给您的对象,而更大的部分实际上仍然是免费的,并且可以在免费存储中使用。

话虽如此,消耗的内存将大于大小 x 对象数量:对于每个分配的对象,内存管理函数必须管理一些附加信息(如分配对象的大小)。同样,如果空闲块不连续,空闲内存池也需要指针(通常是链表)来跟踪空闲块。

【讨论】:

    【解决方案4】:

    关于 Windows 的非常有趣的帖子。

    作为比较,在 Ubuntu 15.10(64) 上:

    int t407(void)
    {
       std::cout << "\nsizeof(Foo): " << sizeof(Foo) << std::endl;
    
       std::cout << "\nsizeof(Foo*): " << sizeof(Foo*) << std::endl;
    
       std::vector<Foo>  fooVec;
       fooVec.reserve(10000000);
       for (size_t i=0; i<10000000; ++i)
       {
          Foo t;
          fooVec.push_back(t);
       }
       std::cout << "\nfooVec.size(): " << fooVec.size() 
                 << " elements" << std::endl
                 << "fooVec.size() * sizeof(Foo): " 
                 << fooVec.size() * sizeof(Foo) << " bytes" << std::endl
                 << "sizeof(fooVec): " << sizeof(fooVec) 
                 << " bytes (on stack)" << std::endl;
    
       return(0);
    }
    

    有输出:

     sizeof(Foo): 1
    
     sizeof(Foo*): 8
    
     fooVec.size(): 10000000 elements 
     fooVec.size() * sizeof(Foo): 10000000 bytes
     sizeof(fooVec): 24 bytes (on stack)
    

    【讨论】:

    • 尝试不使用向量并注意进程内存,而不是变量。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-02-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-03-15
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多