Heap/dynamic vs. static memory allocation for C++

2019-02-08 08:46发布

问题:

My specific question is that when implementing a singleton class in C++, is there any substantial differences between the two below codes regarding performance, side issues or something:

class singleton
{
    // ...
    static singleton& getInstance()
    {
        // allocating on heap
        static singleton* pInstance = new singleton();
        return *pInstance;
    }
    // ...
};

and this:

class singleton
{
    // ...
    static singleton& getInstance()
    {
        // using static variable
        static singleton instance;
        return instance;
    }
    // ...
};


(Note that dereferencing in the heap-based implementation should not affect performance, as AFAIK there is no extra machine-code generated for dereferencing. It's seems only a matter of syntax to distinguish from pointers.)

UPDATE:

I've got interesting answers and comments which I try to summarize them here. (Reading detailed answers is recommended for those interested.)‎:

  • In the singleton using static local variable, the class destructor is automatically invoked at process termination, whereas in the dynamic allocation case, you have to manage object destruction someway at sometime, e.g. by using smart pointers:
    static singleton& getInstance() {
        static std::auto_ptr<singleton> instance (new singleton());
        return *instance.get(); 
    }
  • The singleton using dynamic allocation is "lazier" than the static singleton variable, as in the later case, the required memory for the singleton object is (always?) reserved at process start-up (as part of the whole memory required for loading program) and only calling of the singleton constructor is deferred to getInstance() call-time. This may matter when sizeof(singleton) is large.

  • Both are thread-safe in C++11. But with earlier versions of C++, it's implementation-specific.

  • The dynamic allocation case uses one level of indirection to access the singleton object, whereas in the static singleton object case, direct address of the object is determined and hard-coded at compile-time.


P.S.: I have corrected the terminology I'd used in the original posting according to the @TonyD's answer.

回答1:

  • the new version obviously needs to allocate memory at run-time, whereas the non-pointer version has the memory allocated at compile time (but both need to do the same construction)

  • the new version won't invoke the object's destructor at program termination, but the non-new version will: you could use a smart pointer to correct this

    • you need to be careful that some static/namespace-scope object's destructors don't invoke your singleton after its static local instance's destructor has run... if you're concerned about this, you should perhaps read a bit more about Singleton lifetimes and approaches to managing them. Andrei Alexandrescu's Modern C++ Design has a very readable treatment.
  • under C++03, it's implementation-defined whether either will be thread safe. (I believe GCC tends to be, whilst Visual Studio tends not -comments to confirm/correct appreciated.)

  • under C++11, it's safe: 6.7.4 "If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization." (sans recursion).

Discussion re compile-time versus run-time allocation & initialisation

From the way you've worded your summary and a few comments, I suspect you're not completely understanding a subtle aspect of the allocation and initialisation of static variables....

Say your program has 3 local static 32-bit ints - a, b and c - in different functions: the compiler's likely to compile a binary that tells the OS loader to leave 3x32-bits = 12 bytes of memory for those statics. The compiler decides what offsets each of those variables is at: it may put a at offset 1000 hex in the data segment, b at 1004, and c at 1008. When the program executes, the OS loader doesn't need to allocate memory for each separately - all it knows about is the total of 12 bytes, which it may or may not have been asked specifically to 0-initialise, but it may want to do anyway to ensure the process can't see left over memory content from other users' programs. The machine code instructions in the program will typically hard-code the offsets 1000, 1004, 1008 for accesses to a, b and c - so no allocation of those addresses is needed at run-time.

Dynamic memory allocation is different in that the pointers (say p_a, p_b, p_c) will be given addresses at compile time as just described, but additionally:

  • the pointed-to memory (each of a, b and c) has to be found at run-time (typically when the static function first executes but the compiler's allowed to do it earlier as per my comment on the other answer), and
    • if there's too little memory currently given to the process by the Operating System for the dynamic allocation to succeed, then the program library will ask the OS for more memory (e.g. using sbreak()) - which the OS will typically wipe out for security reasons
    • the dynamic addresses allocated for each of a, b and c have to be copied back into the pointers p_a, p_b and p_c.

This dynamic approach is clearly more convoluted.



回答2:

The main difference is that using a local static the object will be destroyed when closing the program, instead heap-allocated objects will just be abandoned without being destroyed.

Note that in C++ if you declare a static variable inside a function it will be initialized the first time you enter the scope, not at program start (like it happens instead for global static duration variables).

In general over the years I switched from using lazy initialization to explicit controlled initialization because program startup and shutdown are delicate phases and quite difficult to debug. If your class is not doing anything complex and just cannot fail (e.g. it's just a registry) then even lazy initialization is fine... otherwise being in control will save you quite a lot of problems.

A program that crashes before entering the first instruction of main or after executing last instruction of main is harder to debug.

Another problem of using lazy construction of singletons is that if your code is multithread you've to pay attention to the risk of having concurrent threads initializing the singleton at the same time. Doing initialization and shutdown in a single thread context is simpler.