C + + Performance Optimization -- dynamic memory management

Posted by KRAK_JOE on Mon, 21 Feb 2022 03:42:24 +0100

1, new/delete operator

C + + implements global new/new [] and delete/delete [] to improve the access and management of dynamic memory. operator new can be a member function or global function of a class. Generally, the C + + runtime provides default global new/new [] and delete/delete [] implementations. Applications can also use custom implementations to replace the default implementations provided by the C + + runtime, but a program can only have one custom implementation at most.

The declarations of new/new [] and delete/delete [] in the C + + standard are defined as follows:

namespace std
{
class bad_alloc;
struct nothrow_t {};
extern const nothrow_t nothrow;
typedef void (*new_handler)();
new_handler set_new_handler (new_handler new_p) throw();
new_handler get_new_handler() noexcept;
}

void* operator new (std::size_t size) throw (std::bad_alloc);
void* operator new (std::size_t size, const std::nothrow_t& nothrow_value) throw();
void* operator new (std::size_t size, void* ptr) throw();

void* operator new[] (std::size_t size) throw (std::bad_alloc);
void* operator new[] (std::size_t size, const std::nothrow_t& nothrow_value) throw();
void* operator new[] (std::size_t size, void* ptr) throw();

void operator delete (void* ptr) throw();
void operator delete (void* ptr, const std::nothrow_t& nothrow_constant) throw();
void operator delete (void* ptr, void* voidptr2) throw();

void operator delete[] (void* ptr) throw();
void operator delete[] (void* ptr, const std::nothrow_t& nothrow_constant) throw();
void operator delete[] (void* ptr, void* voidptr2) throw();

The C + + standard does not specify whether to initialize the obtained memory, so the initial value in memory depends on the C + + compiler implementation and the explicit assignment of memory by the developer. There are three ways to define operator new:

void* operator new (std::size_t size) throw (std::bad_alloc);
void* operator new (std::size_t size, const std::nothrow_t& nothrow_value) throw();
void* operator new (std::size_t size, void* ptr) throw();

The first method throws bad when memory allocation fails_ Alloc exception is specified in the C + + standard.

The second method does not throw an exception when allocating memory fails and returns NULL, which is used for compatibility with early C + + code.

The third method returns the memory at the specified location as the allocated memory.

The C + + standard has clear provisions on memory allocation failure, and the system will call the currently installed new_handler function, new_handler error handling function through set_ new_ The handler function is installed into the system. C + + standard stipulates NEW_ The handler performs one of the following three operations:

(1) Make new have more memory available, and then return.

(2) Throw bad_alloc exception or bad_alloc derived exception.

(3) Call abort or exit to exit.

2, Customize global new/delete operators

When an application needs to use a unified mechanism to control the memory allocation of data, and does not want to use the memory management mechanism provided by the system, it can be realized by rewriting its own global operator new/delete. Generally, for applications with high memory requirements or debugging memory allocation (generating log s for debugging and troubleshooting), it may be necessary to override the global operator new/delete.

#include <stdio.h>
#include <new>
#include <stdlib.h>
using namespace std;

char* gPool = NULL;
const unsigned int N = 1024* 1024*1024;

void def_new_handler()
{
    if(gPool != NULL)
    {
        printf("try to malloc more memory...\n");
        delete [] gPool;
        gPool = NULL;
        return ;
    }
    else
    {
        printf("new_handler no memory\n");
        throw std::bad_alloc();
    }
}

void* operator new(std::size_t n)
{
    void* pReturn = NULL;
    printf("global operator new, size = %u\n", n);
    if(n == 0)
    {
        n = 1;
    }
    while(true){
        pReturn = malloc(n);
        if(pReturn != NULL){
            printf("malloc %u Bytes memory at %x\n", n, pReturn);
            return pReturn;
        }

        new_handler global_handler = set_new_handler(0);
        set_new_handler(global_handler);
        if(global_handler != NULL){
            printf("call def_new_handler\n");
            (*global_handler)();

        }
        else
        {
            printf("no memory, no new_handler\n");
            throw std::bad_alloc();
        }
    }
}

void operator delete(void* p)
{
    printf("operator delete at %x\n", p);
    return free(p);
}


int main()
{
    set_new_handler(def_new_handler);
    gPool = new char[3 * N];
    if(gPool != NULL)
    {
        printf("preserve memory at %x\n", gPool);
    }
    char* p[10] = {0};
    for (size_t i = 0; i < 10; i++)
    {
        p[i] = new char[N];
        printf("%d GB p = %x\n", i + 1, p[i]);
    }

    return 0;
}

3, Custom class new/delete operator

When customizing global operator new/delete, all memory allocation and release in the program use a unified method, but in some usage scenarios, different class objects are required to use different memory allocation methods. The memory allocation of user-defined class objects can be realized by overloading the class member function operator new/delete, while other programs use the system default operator new/delete to allocate and release memory.

The operator new/delete of the custom class needs to be declared as a static function.

#include <stdio.h>
#include <new>
using namespace std;

class Parent
{
public:
    static void* operator new(size_t n)
    {
        printf("Parent::operator new, size = %u\n", n);
        return ::operator new(n);
    }

    static void operator delete(void* p)
    {
        printf("Parent::operator delete, p = %x\n", p);
        return ::operator delete(p);
    }
public:
    int m_value;
};

class Child : public Parent
{
public:
    int m_childValue;
};

int main()
{
    Parent* p = new Parent();
    Child* c = new Child();

    delete p;
    delete c;
    return 0;
}

The derived class inherits and uses the custom operator new/delete method of the base class by default. If the custom operator new/delete method of the base class is only optimized for the memory allocation efficiency of the base class and does not want the derived class to use the custom operator new/delete method of the base class, it needs to be processed in the operator new/delete method of the base class.

You can also override the operator new/delete method in a derived class to directly call the global operator new/delete method.

#include <stdio.h>
#include <new>
using namespace std;

class Parent
{
public:
    static void* operator new(size_t n)
    {
        printf("Parent::operator new, size = %u\n", n);
        return ::operator new(n);
    }

    static void operator delete(void* p)
    {
        printf("Parent::operator delete, p = %x\n", p);
        return ::operator delete(p);
    }
public:
    int m_value;
};

class Child : public Parent
{
public:
    static void* operator new(size_t n)
    {
        return ::operator new(n);
    }

    static void operator delete(void* p)
    {
        return ::operator delete(p);
    }
    int m_childValue;
};

int main()
{
    Parent* p = new Parent();
    Child* c = new Child();

    delete p;
    delete c;
    return 0;
}

4, Avoid memory leaks

1. Introduction to memory leakage

C + + language has no automatic garbage collection mechanism, but it provides a powerful and flexible mechanism to avoid memory leakage. In C + + language, the memory allocated by new or the object created must be released by delete. Therefore, the allocation and release can be encapsulated in the class, the memory can be allocated in the constructor and the memory can be released in the destructor.

#include <stdio.h>
#include <string.h>
using namespace std;

class SimpleClass
{
public:
    SimpleClass(size_t n = 1)
    {
        m_buf = new char[n];
        m_nSize = n;
    }

    char* buffer()
    {
        return m_buf;
    }

    ~SimpleClass()
    {
        printf("delete at %x, size = %u\n", m_buf, m_nSize);
        delete [] m_buf;
    }
private:
    char* m_buf;
    size_t m_nSize;
};

void func()
{
    SimpleClass obj(10);
    char* buf = obj.buffer();
    strcpy(buf, "hello");
    printf("buffer: %s\n", buf);
}


int main()
{
    func();
    return 0;
}

When SimpleClass class declares an object, it allocates the required memory. The allocated memory space will be automatically released when the scope of SimpleClass class object exits. delete release does not need to be displayed.

2. Copy construction

SimpleClass class does not implement the copy constructor. The C + + compiler will construct a default copy constructor to perform bit copy operation (shallow copy) and copy the contents of SimpleClass object byte by byte. Therefore, m of different objects after copying_ BUF points to the same memory space, and m will be caused when the object is destructed_ BUF is released multiple times.

The SimpleClass class can declare the copy constructor as a private function and prohibit copy construction. However, when the SimpleClass class user attempts to assign a value or pass it to the function as a parameter, a compilation error will occur.

#include <stdio.h>
#include <string.h>
using namespace std;

class SimpleClass
{
public:
    SimpleClass(size_t n = 1)
    {
        m_buf = new char[n];
        m_nSize = n;
    }

    char* buffer()
    {
        return m_buf;
    }

    ~SimpleClass()
    {
        printf("delete at %x, size = %u\n", m_buf, m_nSize);
        delete [] m_buf;
    }
private:
    SimpleClass(const SimpleClass& other);
private:
    char* m_buf;
    size_t m_nSize;
};

void func()
{
    SimpleClass obj(10);
    SimpleClass objb = obj;
    char* buf = obj.buffer();
    strcpy(buf, "hello");
    printf("buffer: %s\n", buf);
}

int main()
{
    func();
    return 0;
}

3. Reference count

Reference counting is to maintain a counter for the memory to be used and record how many pointers point to the allocated memory space. When a pointer points to the memory space, the counter is incremented by 1; When the pointer to the memory space is destroyed, the counter decreases by 1; When the counter of the memory space is 0, there is no pointer to the memory space, and the memory space can be released.

Reference counting should not only consider the reference count of multiple objects pointing to memory space during class object initialization, but also consider the transfer of pointer pointing to memory space during assignment between different objects.

#include <stdio.h>
#include <string.h>
using namespace std;

class SimpleClass
{
public:
    SimpleClass(size_t n = 1)
    {
        m_buf = new char[n];
        m_nSize = n;
        m_count = new int;
        (*m_count) = 1;
        printf("count is %u\n", *m_count);
    }

    SimpleClass(const SimpleClass& other)
    {
        m_buf = other.m_buf;
        m_nSize = other.m_nSize;
        m_count = other.m_count;
        (*m_count)++;
        printf("count is %u\n", *m_count);
    }

    SimpleClass& operator=(const SimpleClass& other)
    {
        if(m_buf == other.m_buf)
        {
            return *this;
        }
        (*m_count)--;
        if((*m_count) == 0)
        {
            printf("delete at %x, size = %u\n", m_buf, m_nSize);
            delete [] m_buf;
            delete m_count;
        }

        m_buf = other.m_buf;
        m_nSize = other.m_nSize;
        (*m_count)++;
    }

    char* buffer()
    {
        return m_buf;
    }

    ~SimpleClass()
    {
        (*m_count)--;
        printf("count is %u\n", *m_count);
        if(*m_count == 0)
        {
            printf("delete at %x, size = %u\n", m_buf, m_nSize);
            delete [] m_buf;
            delete m_count;
        }
    }
private:
    char* m_buf;
    size_t m_nSize;
    int* m_count;
};

void func()
{
    SimpleClass obj(10);
    char* buf = obj.buffer();
    strcpy(buf, "hello");
    printf("buffer: %s\n", buf);
    SimpleClass obja = obj;
    SimpleClass objb(8);
    objb = obj;
}

int main()
{
    func();
    return 0;
}

4. Deep copy

The equal bit copy will only copy byte by byte, causing the pointers of multiple objects to point to the same memory space. Therefore, in the copy constructor and assignment operator functions, it is necessary to make a deep copy of memory space resources, that is, deep copy.

#include <stdio.h>
#include <string.h>
using namespace std;

class SimpleClass
{
public:
    SimpleClass(size_t n = 1)
    {
        m_buf = new char[n];
        m_nSize = n;
        printf("SimpleClass Constructor, size = %u\n", m_nSize);
    }

    SimpleClass(const SimpleClass& other)
    {
        m_nSize = other.m_nSize;
        m_buf = new char[m_nSize];
        strncpy(m_buf, other.m_buf,  m_nSize - 1);
        printf("SimpleClass Copy Constructor, size = %u\n", m_nSize);
    }

    SimpleClass& operator=(const SimpleClass& other)
    {
        if(m_buf == other.m_buf)
        {
            return *this;
        }
        delete [] m_buf;
        m_nSize = other.m_nSize;
        m_buf = new char[m_nSize];
        strncpy(m_buf, other.m_buf,  m_nSize - 1);
        printf("SimpleClass assign operator=, size = %u\n", m_nSize);
    }

    char* buffer()
    {
        return m_buf;
    }

    ~SimpleClass()
    {
        printf("delete at %x, size = %u\n", m_buf, m_nSize);
        delete [] m_buf;
    }
private:
    char* m_buf;
    size_t m_nSize;
};

void func()
{
    SimpleClass obj(10);
    char* buf = obj.buffer();
    strcpy(buf, "hello");
    printf("buffer: %s\n", buf);
    SimpleClass obja = obj;
    SimpleClass objb(8);
    objb = obj;
}

int main()
{
    func();
    return 0;
}

5, Smart pointer

1. Introduction to smart pointer

Smart pointer is a class that stores pointers to dynamic memory and objects. Its working mechanism is the same as that of C language pointer, but it will automatically delete the pointed memory or objects at an appropriate time.

2,auto_ptr

auto_ptr is a class template provided by C + + standard library, auto_ The PTR object points to the dynamic memory created by new through initialization. When auto_ At the end of the PTR object's life cycle, its destructor will auto_ The dynamic memory owned by PTR object is automatically released. Even if an exception occurs, the dynamic memory can be released through the abnormal stack expansion process.

auto_ Precautions for PTR use are as follows:

(1)auto_ptr cannot share ownership.
(2)auto_ptr cannot point to an array.
(3)auto_ptr cannot be a member of a container.
(4) Auto cannot be initialized by assignment_ ptr.

#include <memory>
#include <string>
#include <stdio.h>
using namespace std;

class Test
{
public:
    Test(const char* c)
    {
        m_buffer = c;
        printf("Constructor, %s\n", m_buffer.c_str());
    }

    ~Test()
    {
        printf("DeConstructor, %s\n", m_buffer.c_str());
    }

    void print()
    {
        printf("%s\n", m_buffer.c_str());
    }
private:
    string m_buffer;
};

int main()
{
    auto_ptr<string> p(new string("HelloWorld."));
    printf("%s\n", p->c_str());
    auto_ptr<Test> pTest(new Test("HelloTest."));
    // auto_ptr<Test> pTest = new Test("HelloTest.");// error
    pTest->print();
}

3,boost::scoped_ptr

boost::scoped_ptr is a simple smart pointer, which can ensure that the object is automatically released after leaving the scope.

boost::scoped_ The implementation of PTR uses an object on the stack to manage an object on the heap, so that the object on the heap is automatically deleted when the object on the stack is destroyed_ PTR cannot be copied, so its ownership cannot be converted.

scoped_ Precautions for PTR use are as follows:

(1) Cannot convert ownership
boost::scoped_ The object lifecycle managed by PTR is limited to one scope, so ownership cannot be transferred to other objects.

(2) Cannot share ownership

scoped_ptr defines the copy constructor and assignment operator as private, so it is scoped_ptr cannot be used in STL container because push in container_ The back operation needs to call scoped_ If the assignment operator of PTR overloads the function, the compilation will fail.

(3) Cannot be used to manage array objects
scoped_ptr deletes managed objects through delete, while array objects must be deleted through deletep [], so boost::scoped_ptr cannot manage array objects. If you want to manage array objects, you need to use boost::scoped_array class.

(4) Multiple boost::scoped_ptr objects cannot manage the same object.

scoped_ptr object will call its destructor when it leaves its scope, and release object will be called inside the destructor when multiple scoped objects_ When PTR manages the same object, after leaving the scope, it will call delete many times to release the managed object, resulting in program running error.

(5)boost::scoped_ptr objects can release managed objects in advance.

boost::scoped_ptr exchanges the new address with the old address through the newly generated nameless temporary variable. Finally, it leaves the scope, the object dies, calls the destructor, and releases the

The original space does not leak memory, and the new space is managed.

#include <stdio.h>
#include <boost/scoped_ptr.hpp>
#include <vector>
using namespace std;
using namespace boost;

class Test
{
public:
    Test(const char* c)
    {
        m_buffer = c;
        printf("Creating Test ..., %s\n", m_buffer.c_str());
    }

    void print()
    {
        printf("print Test ..., %s\n", m_buffer.c_str());
    }

    ~Test()
    {
        printf("Destroying Test ..., %s\n", m_buffer.c_str());
    }
private:
    string m_buffer;
};

int main()
{
    printf("=====Main Begin=====\n");
    {
        scoped_ptr<Test> pTest(new Test("HelloTest"));
        // scoped_ ptr<Test> pTest1(pTest);// Copy construction
        // scoped_ ptr<Test> pTest2 = pTest;// Assignment operator
        pTest->print();
        Test* test = new Test("HelloTest1");
        pTest.reset(test);
        pTest->print();
        pTest.reset();

        vector< scoped_ptr<Test> > vec;
        // vec.push_back(pTest); // error
    }
    printf("===== Main End =====\n");
    return 0;
}

4,boost::shared_ptr

boost::shared_ptr is a smart pointer that can share ownership. If there are multiple shared_ When PTR jointly manages the same object, there are only all shared objects_ After PTR is separated from the object, the managed object will be released.

boost::shared_ptr counts the references of the managed objects. When a boost:: shared is added_ When PTR manages an object, the reference count of the object is increased by 1; Reduce one boost:: shared_ When PTR manages objects, it will reduce the reference count of the object by 1. If the reference count of the object is 0, delete will release the memory occupied by it.

boost::shared_ Precautions for PTR use:

(1) Avoid using shared_ Direct memory management operation of objects managed by PTR to avoid multiple releases of objects

(2)shared_ptr cannot automatically manage the memory of circular reference objects.

(3) Do not construct a temporary shared_ptr as parameter of function

#include <stdio.h>
#include <boost/shared_ptr.hpp>
#include <vector>
using namespace std;
using namespace boost;

class Test
{
public:
    Test(const char* c)
    {
        m_buffer = c;
        printf("Creating Test ..., %s\n", m_buffer.c_str());
    }

    void print()
    {
        printf("print Test ..., %s\n", m_buffer.c_str());
    }

    ~Test()
    {
        printf("Destroying Test ..., %s\n", m_buffer.c_str());
    }
private:
    string m_buffer;
};

int main()
{
    shared_ptr<Test> pTest(new Test("HelloTest"));
    pTest->print();
    shared_ptr<Test> pTest2 = pTest;
    pTest2->print();

    Test* test = new Test("HelloTest1");
    pTest.reset(test);
    pTest->print();
    pTest.reset();

    return 0;
}

5,boost::weak_ptr

weak_ptr is to cooperate with shared_ptr and the introduction of smart pointers, used to make up for the shared_ptr is insufficient to solve the problem of circular reference

boost::weak_ptr is boost:: shared_ The observer object of PTR, when the observed boost:: shared_ After PTR fails, the corresponding boost:: weak_ptr also fails. boost::weak_ptr only applies to boost::shared_ptr references without changing its reference count_ The construction of PTR will not cause the pointer reference count to increase_ PTR destructions also do not cause the pointer reference count to decrease.

#include <stdio.h>
#include <boost/shared_ptr.hpp>
#include <boost/weak_ptr.hpp>

int main(int argc, char *argv[])
{
    boost::shared_ptr<int> sp(new int(10));
    boost::weak_ptr<int> wp(sp);

    //Test whether the counter is 0
    if (!wp.expired()) 
    {
        //Promote wp to shared_ptr---sp2
        boost::shared_ptr<int> sp2 = wp.lock(); 
        *sp2 = 100;
    }

    // shared_ptr is set to null, weak_ptr failure
    sp.reset();  
    printf("expired= %d\n", wp.expired());    

    wp.lock();      
}

Topics: C++ Optimize