How would I use the sized operators delete/delete[] and why are they better?
Asked Answered
A

3

25

C++14 introduced "sized" versions of operator delete, i.e.

void operator delete( void* ptr, std::size_t sz );

and

void operator delete[]( void* ptr, std::size_t sz );

Reading through N3536, it seems that those operators were introduced to increase performance. I know that the typical allocator used by operator new "stores" the size of the bulk memory somewhere, and that's how typical operator delete "knows" how much memory to return to the free store.

I am not sure however why the "sized" versions of operator delete will help in terms of performance. The only thing that can speed things up is one less read operation regarding the size from the control block. Is this indeed the only advantage?

Second, how can I deal with the array version? AFAIK, the size of the allocated array is not simply sizeof(type)*number_elements, but there may be some additional bytes allocated as the implementation may use those bytes as control bytes. What "size" should I pass to operator delete[] in this case? Can you provide a brief example of usage?

Alderete answered 22/12, 2015 at 1:15 Comment(15)
Re the array version: that's the compiler's problem, not yours (you pass it whatever that was passed to operator new[] to allocate that memory).Ananias
Dosen't this phrase from the paper Modern memory allocators often allocate in size categories, and, for space efficiency reasons, do not store the size of the object near the object. Deallocation then requires searching for the size category store that contains the object. This search can be expensive, particularly as the search data structures are often not in memory caches. cover what you are asking?Seacock
@Ananias But how can I find out what was passed out to operator new[]? Is it simply sizeof arr?Alderete
@ShafikYaghmour Perhaps... I would have thought that the worst case scenario is a simple pointer indirection, not a crazy search over a linked list. Is this indeed the case?Alderete
@vsoftco: You have to store that value when you call operator new[]. There is no way to fetch it if you didn't save it.Balboa
@vsoftco: It's more than a pointer indirection, but a lot less than a linked list. It's usually a fast lookup, but still a cache miss.Balboa
I don't understand your problem. Why on earth are you calling (non-placement) operator delete[] manually in the first place?Ananias
@MooingDuck Ohh I see, so I basically store the size from the operator new in my variable, which I then make sure I pass to operator delete.Alderete
@Ananias I'm not calling them, those are replaceable functions (which may be called by a new/delete expression, or even manually), and I was wondering why were they defined with a sized version.Alderete
@ShafikYaghmour Thanks for the link, will read it. The only allocator I was familiar with was the one in good old K&R (pretty basic but made the point).Alderete
@Alderete Imagine the internal code in std::array. It saves the size when you create the array, and can easily pass it when it calls delete on its members in its destructor.Peggiepeggir
@Alderete So why do you care about what the "size" parameter of operator delete[] should be? The compiler emits the call; it's its problem, not yours. (The compiler, obviously, knows how much storage new T[N] would request.)Ananias
@Ananias Yes, I understand this now, I was a bit confused as from where to take that size, but forgot I can just read it from the argument of the corresponding operator new.Alderete
Also not N3664 also references TCMalloc so these are probably somewhat related changes.Seacock
There is one more problem for compilers to call the operator sanely. It is fixed in C++17.Blooded
G
15

Dealing with your second question first:

If present, the std::size_t size argument must equal the size argument passed to the allocation function that returned ptr.

So, any extra space that might be allocated is the responsibility of the runtime library, not the client code.

The first question is more difficult to answer well. The primary idea is (or at least seems to be) that the size of a block often isn't stored right next to the block itself. In most cases, the size of the block is written, and never written again until the block is deallocated. To avoid that data polluting the cache while the block is in use, it can be kept separately. Then when you go to deallocate the block, the size will frequently have been paged out to disk, so reading it back in is quite slow.

It's also fairly common to avoid explicitly storing the size of every block explicitly at all. An allocator will frequently have separate pools for different sizes of blocks (e.g., powers of 2 from 16 or so up to around a couple kilobytes or so). It'll allocate a (fairly) large block from the OS for each pool, then allocate pieces of that large block to the user. When you pass back an address, it basically searches for that address through the different sizes of pools to find which pool it came from. If you have a lot of pools and a lot of blocks in each pool, that can be relatively slow.

The idea here is to avoid both of those possibilities. In a typical case, your allocations/deallocations are more or less tied to the stack anyway, and when they are the size you're allocating will likely be in a local variable. When you deallocate, you'll typically be at (or at least close to) the same level of the stack as where you did the allocation, so that same local variable will be easily available, and probably won't be paged out to disk (or anything like that) because other variables stored nearby are in use as well. For the non-array form, the call to ::operator new will typically stem from a new expression, and the call to ::operator delete from the matching delete expression. In this case, the code generated to construct/destroy the object "knows" the size it's going to request (and destroy) based solely on the type of object being created/destroyed.

Goolsby answered 22/12, 2015 at 1:35 Comment(8)
A plain delete p knows the size of the allocation (if p's destructor isn't polymorphic, it knows it statically), anyway.Ananias
Reminds me of Andrei Alexandrescu's talk at CppCon'15Gravitation
@Gravitation Yes I've seen that talk, and how pissed he was about new/delete :)Alderete
@T.C.: Yes, probably worth mentioning.Goolsby
@Jerry Coffin If present, the std::size_t size argument must equal the size argument passed to the allocation function that returned ptr.. If I understand you correctly, auto ptr=new int[5]; delete[] (ptr, sizeof(int)); is valid. Am I right?Mayemayeda
@John: This is talking about using operator new, which is distinct from the new operator (confusing as all get out, I know). So it'd be more like auto ptr = operator new(sizeof(int) * 5); and operator delete(ptr, sizeof(int) * 5);Goolsby
@JerryCoffin Yes, I am totally confused now. Could you please suggest some document for me to go through(about operator new and the new operator)?Mayemayeda
@John: Not sure, but perhaps this is of some help: https://mcmap.net/q/21774/-difference-between-39-new-operator-39-and-39-operator-new-39Goolsby
E
10

For the size argument to C++14 operator delete you must pass the same size you gave to operator new, which is in bytes. But as you discovered it's more complicated for arrays. For why it's more complicated, see here: Array placement-new requires unspecified overhead in the buffer?

So if you do this:

std::string* arr = new std::string[100]

It may not be valid to do this:

operator delete[](arr, 100 * sizeof(std::string)); # BAD CODE?

Because the original new expression was not equivalent to:

std::string* arr = new (new char[100 * sizeof(std::string)]) std::string[100];

As for why the sized delete API is better, it seems that today it is actually not but the hope is that some standard libraries will improve performance of deallocation because they actually do not store the allocation size next to each allocated block (the classical/textbook model). For more on that, see here: Sized Deallocation Feature In Memory Management in C++1y

And of course the reason not to store the size next to every allocation is that it is a waste of space if you don't truly need it. For programs which make many small dynamic allocations (which are more popular than they ought to be!), this overhead can be significant. For example in the "plain vanilla" std::shared_ptr constructor (rather than make_shared), a reference count is dynamically allocated, so if your allocator stores the size next to it, it might naively require about 25% overhead: one "size" integer for the allocator plus the four-slot control block. Not to mention memory-pressure: if the size is not stored next to the allocated block, you avoid loading a line from memory on deallocation--the only information you need is given in the function call (well, you also need to look at the arena or free-list or whatever, but you needed that in any case, you still get to skip one load).

Epanorthosis answered 22/12, 2015 at 1:48 Comment(3)
shared_ptr's control block isn't just one integer.Ananias
The Array placement-new requires unspecified overhead in the buffer? discussion has some serious bullshit in it, in specific, regarding the non-allocating placement allocation functions (ignore allocation for a second). The new(ptr) T[n] never ever required some unspecified overhead, it doesn't allocate, it does nothing, it serves only as dummy operator for the in-place object construction. If there was a compiler adding that overhead, it certainly was VC++.Gmur
@Gmur The 2nd allocation represents for allocation management in this case. ISO C++17 does change the wording to specify such ones "non-allocating" form.Blooded
S
3

Some relevant info: Currently VS 17 implementation of sized delete[] seems broken. It always returns the generic pointer size (void*). The g++ 7.3.1 give the size of the full array plus 8 bytes overhead. Haven't tested it on other compilers, but as you see neither of them giving the expected result. WRT to usefulness of it, as is alluded to in the selected answer, the main usefulness come in play when you have your custom allocators, either to pass to stl containers or just use for local memory management. In these cases it could be very useful to have user size array size be returned to you, so you can free the proper size from your allocators. I can see it is possible to avoid having to use this though. Here's a code you can use for testing the "correctness" of sized delete[] implementation in your compiler:

#include <iostream>
#include <sstream>

#include <string>

std::string true_cxx =

#ifdef __clang__
"clang++";
#elif _MSC_VER
"MVC";
#else
"g++";
#endif

std::string ver_string(int a, int b, int c) {
    std::ostringstream ss;
    ss << a << '.' << b << '.' << c;
    return ss.str();
}


std::string true_cxx_ver =
#ifdef __clang__
ver_string(__clang_major__, __clang_minor__, __clang_patchlevel__);
#elif _MSC_VER
#ifdef _MSC_FULL_VER
#if   _MSC_FULL_VER == 170060315
"MSVS 2012; Platform Toolset v110";
#elif _MSC_FULL_VER == 170051025
"MSVS 2012; Platform Toolset v120_CTP_Nov2012";
#elif _MSC_FULL_VER == 180020617
"MSVS 2013; Platform Toolset v120";
#elif _MSC_FULL_VER == 191426431
"MSVS 2017; Platform Toolset v140";
#else
"Not recognized";
#endif
#endif // _MSC_FULL_VER
#else
ver_string(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__);
#endif


// sized class-specific deallocation functions
struct X {
    static void operator delete(void* ptr, std::size_t sz)
    {
        std::cout << "custom delete for size " << sz << '\n';
        ::operator delete(ptr);
    }
    static void operator delete[](void* ptr, std::size_t sz)
    {
        std::cout << "custom delete[] for size " << sz << '\n';
        ::operator delete(ptr);
    }
    char16_t c[2];
};
int main() {
    X* p1 = new X;
    delete p1;
    X* p2 = new X[10];
    for (int i = 0; i < 10; ++i)
        p2[i] = X{ (char16_t)i };
    std::cout << "Compiler: "<<true_cxx.c_str()<<": Version:" << true_cxx_ver.c_str() << std::endl;
    delete[] p2;
}
Serology answered 15/12, 2018 at 23:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.