简体   繁体   中英

C++ objects memory consumption

First of all: this question is not about "how to use delete operator", it is about "why many class objects of small size consumes lots of memory". Let's say we have this code:

class Foo
{

};

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

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

Size of empty class Foo is 1 byte, but when the code is executed it consumes about 600Mb memory. How is that?

UPDATE. I've tested this on Win10 x64 in Visual Studio 2010. Memory usage from OS task manager.

The C++ heap manager has 4 different "modes" in which it reserves less or more space around an object. These are

  1. Release mode, running normally
  2. Release mode, running under the debugger
  3. Debug mode, running normally
  4. Debug mode, running under the debugger

The additional memory is used for no mans land (0xFDFDFDFD), alignment to 16 byte boundaries (0xBAADF00D), heap management etc.

I suggest reading this post and looking at the 4 scenarios in a debugger, opening the raw memory view. You'll learn a lot. For case 1 and 3, insert a pause where you can attach the debugger to the running process, while in case 2 and 4 you should run the debugger first and then start the executable from there.

I used to demonstrate how the C++ heap works when explaining the buffer overrun. Here's a demo program you can use, not perfect, but maybe useful:

#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!
}

Class Foo might be 1 byte in size, but since you allocate many Foo s individually, they can (and probably do) get allocated on some byte aligned addresses and due to fragmentation consume more memory than you expected.

Additionally, there's the memory used by the internal memory management system.

You have to know that the memory consumed by a process from the OS point of view is not only related to the objects that are allocated in your code.

It's something that is heavily implementation dependent, but in general, for performance reasons, memory allocation is rarely passed one to one to the OS, but pooled via the management of the free store. The general principle is as follows:

  • At startup of the program, your C++ implementation will allocate some initial space for the free store and the heap.
  • When this memory is consumed, and new memory is needed, the memory management will request larger chunks to the OS and make these available in the free store.
  • It is possible that the size of chunks requested from OS could adapt themselves to the allocation patterns.

So from the 600MB viewed in the task manager, it is possible that only a small part is effectively allocated to your objects, and a bigger part is in fact still free and available in the free store.

This being said, the consumed memory will be larger than size x number of objects: for each allocated object, the memory management functions have to manage some additional information (like the size of the allocated object). SImilarly, the free memory pool also requires pointer (generally a linked list) to keep track of the free blocks if these are not contiguous.

Very interesting post about Windows.

For comparison, on 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);
}

With output:

 sizeof(Foo): 1

 sizeof(Foo*): 8

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM