How it works...

To better understand the extent to which code has to execute so as to allocate a variable on the heap, we will start with the following simple example:

int main(void)
{
auto ptr = std::make_unique<int>();
}

As shown in the preceding example, we allocate an integer using std::unique_ptr(). We use std::unique_ptr() as our starting point, as this is how most C++ Core Guideline code will allocate memory on the heap.

The std::make_unique() function allocates a std::unique_ptr using the following pseudo logic (this is a simplified example as this doesn't show how custom deleters are handled):

namespace std
{
template<typename T, typename... ARGS>
auto make_unique(ARGS... args)
{
return std::unique_ptr(new T(std::forward<ARGS>(args)...));
}
}

As shown in the preceding code, the std::make_unique() function creates a std::unique_ptr and gives it a pointer that it allocates with the new() operator. Once std::unique_ptr loses scope, it will delete the pointer using delete().

When the compiler sees the new operator, it replaces the code with a call to operator new(unsigned long). To see this, let's look at the following example:

int main(void)
{
auto ptr = new int;
}

In the preceding example, we allocate a simple pointer using new(). Now, we can look at the resulting compiled assembly, which can be seen in the following screenshot:

As shown in the following screenshot, a call is made to _Znwm, which is mangled C++ code for operator new(unsigned long), which is easy to demangle:

The new() operator itself looks like the following pseudocode (note that this doesn't take into account the ability to disable exception support or provide support for a new handler):

void* operator new(size_t size)
{
if (auto ptr = malloc(size)) {
return ptr;
}

throw std::bad_alloc();
}

Now, we can look at the new operator to see malloc() being called:

As shown in the preceding screenshot, malloc() is called. If the resulting pointer is not NULL, the operator returns; otherwise, it enters its error state, which involves calling a new handler and eventually throwing std::bad_alloc() (at least by default).

The call to malloc() itself is far more complicated. When an application itself is started, the first thing it does is reserve heap space. The operating system gives every application a contiguous block of virtual memory to operate from, and the heap on Linux is the last block of memory in the application (that is, the memory that new() returns comes from the end of the application's memory space). Placing the heap here provides the operating system with a way to add additional memory to the application as it is needed (as the operating simply extends the end of the application's virtual memory).

The application itself uses the sbrk() function to ask the operating system for more memory when it runs out. When this function is called, the operating system allocates pages of memory from its internal page pool and maps this memory into the application by moving the end of the application's memory space. The map process itself is slow as the operating system not only has to allocate pages from the pool, which requires some sort of search and reservation logic, but it must also walk the application's page tables to add this additional memory to its virtual address space.

Once sbrk() has provided the application with additional memory, the malloc() engine takes over. As we mentioned previously, the operating system simply maps pages of memory into the application. Each page can be as small as 4k bytes to anywhere from 2 MB to even 1 GB, depending on the request. In our example, however, we allocated a simple integer, which is only 4 bytes in size. To convert pages into small objects without wasting memory, malloc() itself has an algorithm that breaks the memory provided by the operating system up into small blocks. This engine must also handle when these blocks of memory are freed so that they can be used again. This requires complex data structures to manage all of the application's memory, and each call to malloc(), free(), new(), and delete() has to exercise this logic.

A simple call to create a std::unique_ptr using std::make_unique() has to create std::unique_ptr with memory allocated from new(), which actually calls malloc(), which must search through a complex data structure to find a free block of memory that can eventually be returned, that is, assuming malloc() has free memory and doesn't have to ask the operating system for more memory using sbrk().

In other words, dynamic (that is, heap) memory is slow and should only be used when needed, and, ideally, not in time critical code.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset