1

Let's say I have an allocator which gives out entire cachelines per request. So each allocation will begin on a new cacheline.

constexpr auto L1 = std::hardware_destructive_interference_size;
std::byte* allocator_cachelines = static_cast<std::byte*>(
            ::operator new(TOTAL_CACHELINES*L1, std::align_val_t{L1}));

// ALL TYPES USING THE ALLOCATOR HAVE IMPLICIT LIFETIME
// (are trivially copyable and destructable)
struct ABC { int i=0; };
struct XYZ { float f=1.f; };

auto* abc = reinterpret_cast<ABC*>(allocator.allocate(ABC_COUNT * sizeof(ABC)));
// allocate returns enough cachelines from allocator_cachelines
// to fulfill the request,
// plus the bookkeeping to mark those cachelines as in use

std::cout << abc[0].i; // OK?                                                       
// since ABC is the first type stored within the memory,
// does it's implicit lifetime begin?

SEE: https://en.cppreference.com/w/cpp/language/object#Object_creation:~:text=call%20to%20following,including%20placement%20new)

Now what happens if that memory is deallocated and reused for another implicit lifetime type.

allocator.deallocate(abc);
// deallocate marks the returned abc cachelines as free for reuse

auto* xyz = reinterpret_cast<XYZ*>(allocator.allocate(XYZ_COUNT * sizeof(XYZ)));
// allocate returns the abc cachelines which were just freed

std::cout << xyz[0].f; // OK?

// or do I need to first do
xyz[0] = XYZ{};
std::cout << xyz[0].f; // OK NOW?

Also if access to the allocator is controlled by an std::mutex. Would this solve any strict aliasing violations? Since a mutex introduces memory fences, memory reordering can't cause violations of strict aliasing since the allocating and deallocating gives mutual exclusive types to the reused memory (assuming that pointers to the previous data type are not cached for later use).

Lastly if I have an array of floats allocated from the allocator and use it with SIMD instrinsics. Would I have to write to all elements to avoid strict aliasing violations? Since the SIMD instrinsic could access elements at the end of the array which were never written to.
For example float array : [0, 1, 2, 3]
If 0, 1, 2 were written to and 3 wasn't (and a another type had occupied those bytes previously). Any SIMD instrinsic load of the 4 floats would access 3 as a different type then what was last written to it.

2 Answers 2

1

If all allocate does is returning a cacheline from the array, then both

xyz[0].f

and

xyz[0] = XYZ{};

have undefined behavior, but not exactly for aliasing violations.

operator new is specified to implicitly create objects of implicit-lifetime types, but your function allocate, if it doesn't perform some specific operation specified to do the same, does not.

Therefore, it is impossible for there to be a XYZ object at the address ofxyz. Consequently the pointer xyz points to some object that is not of type XYZ. Therefore the pointer arithmetic in xyz[0] has undefined behavior because of [expr.add]/6. Similarly xyz.f without pointer arithmetic would have undefined behavior because of [expr.ref]/8.

You need to create the object explicitly with a placement-new, even if it is implicit-lifetime. You need to create a XYZ object at each slot of the returned allocation individually or use an array placement-new expression to create an array of XYZ objects in the storage. There was a problem with that in the past in that an implementation was allowed to require more storage for a array new expression than the array itself would require, but that has been fixed with CWG 2382 for non-allocating forms of array new expressions.

abc[0].i also has undefined behavior. While operator new can implicitly-create objects and can return a pointer to one of these objects, you are using the result presumably to do std::byte* pointer arithmetic in allocate. Therefore you require operator new to return a pointer to the std::byte object instead. To obtain a pointer to the nested ABC object from a pointer to a std::byte object you need to std::launder the pointer:

auto* abc = std::launder(reinterpret_cast<ABC*>(allocator.allocate(ABC_COUNT * sizeof(ABC))));

Note that this is a completely distinct matter from the object creation issue. If you take the result of a placement-new expression, no std::launder is required, but if you try to reinterpret_cast from the std::byte pointer after a placement-new expression, then you need it again.

Also, in any case, you need to make sure that the storage returned by allocate or used to place an object is properly aligned and has sufficient size for both ABC and XYZ. Presumably you have already assured that by knowing the size and alignment requirements on this implementation and how they relate to cachelines.

Sign up to request clarification or add additional context in comments.

5 Comments

So if I placement-new several objects contiguously, can I just launder a ptr to the first element and use regular ptr arithmetic to reach the other objects or is std::launder required each time I access the other elements? Or can I use the ptr returned from placement-new-ing the first object to reach the rest?
@rad Technically pointer arithmetic to move through the objects created with individual non-array placement-news has undefined behavior as well.
@rad I just noticed that you didn't tag language-lawyer yourself. My answers so far are strictly about what the C++ standard guarantees. In practice implementations are much more permissive.
Does the ptr returned from the array form of placement-new make it well-defined?
@rad Yes, inside that array. The array placement-new creates an array object, while the non-array form does not. And pointer arithmetic is valid only inside an array. I also just noticed that they fixed the overallocation issue for non-allocating array new expressions. So you can use that safely now.
0

I would split your question in three parts:

Does the lifetime of ABC implicitly begin?

Yes as long as ABC is a implicit-lifetime class (which it is). However, you must be aware that the implicit member initializer (i = 0) won't be used unless you run its constructor, and therefore, i will get an indeterminate value. That is not bad per se, but using indeterminate values produces undefined behavior.

If you want to initialize the ABC object, the correct syntax is:

new (abc[0]) ABC();

not

abc[0] = ABC();

The latter would suggest that there was an object in abc[0] previously (although for aggregates, it doesn't really matter).

Does deallocating an object and using the memory for another constitute a strict aliasing violation

No. That's not what the strict aliasing rule is about. It is only violated if you have two pointer of (basically) different types pointing to the same object and use them concurrently. If you at one point cease using one and start using another, that's OK. Eg. even this is OK:

T* t = new T;
... // use t
t->T::~T(); // destruct
U* u = new (t) U; // construct U in the place formerly occupied by T
                  // provided that U fits into memory occupied by T
... // use u, CAN'T use t
u->U::~U(); // now destruct U
::operator delete(u);

Does SIMD have anything to do with the strict aliasing rule?

No. You can use the SIMD instructions freely, even when accessing extraneous elements, as long as those extra elements don't overlap in memory with other data (which, from your description, doesn't happen). However, it might be better from the point of the processor to initialize them to 0 IIUC.

4 Comments

So if I have a container which allocates enough space for an array of floats. After placement new-ing all elements, could I then store the pointer returned from the first placement new (from placement new-ing element 0), and use that pointer to access the rest of the array? Or would I have to std::launder every time I accessed a float element?
@rad: The way I read it, no. You can placement new each of the floats and either use the original pointer and launder to access the new floats, or store all the pointers to the new floats and use them directly. Or you can do the easiest thing, and placement-new the whole array, store the resulting pointer which you can use and index freely.
@rad: also, I might be wrong about the first question. I've been reading the papers and it seems that you actually need to perform a placement new (or memcpy) to begin the lifetime of an object. Do you want to avoid that for efficiency?
So I have to placement-new the entire capacity after allocating? container_ptr = ::new(allocator_ptr) float[capacity]{};

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.