Shared memory buffers in C++ without violating strict aliasing rules

3.2k views Asked by At

I am struggling with implementing a shared memory buffer without breaking C99's strict aliasing rules.

Suppose I have some code that processes some data and needs to have some 'scratch' memory to operate. I could write it as something like:

void foo(... some arguments here ...) {
  int* scratchMem = new int[1000];   // Allocate.
  // Do stuff...
  delete[] scratchMem;  // Free.
}

Then I have another function that does some other stuff that also needs a scratch buffer:

void bar(...arguments...) {
  float* scratchMem = new float[1000];   // Allocate.
  // Do other stuff...
  delete[] scratchMem;  // Free.
}

The problem is that foo() and bar() may be called many times during operation and having heap allocations all over the place may be quite bad in terms of performance and memory fragmentation. An obvious solution would be to allocate a common, shared memory buffer of proper size once and then pass it into foo() and bar() as an argument, BYOB-style:

void foo(void* scratchMem);
void bar(void* scratchMem);

int main() {
  const int iAmBigEnough = 5000;
  int* scratchMem = new int[iAmBigEnough];

  foo(scratchMem);
  bar(scratchMem);

  delete[] scratchMem;
  return 0;
}

void foo(void* scratchMem) {
  int* smem = (int*)scratchMem;
  // Dereferencing smem will break strict-aliasing rules!
  // ...
}

void bar(void* scratchMem) {
  float* smem = (float*)scratchMem;
  // Dereferencing smem will break strict-aliasing rules!
  // ...
}


I guess I have two questions now:
- How can I implement a shared common scratch memory buffer that is not in violation of aliasing rules?
- Even though the above code does violate strict aliasing rules, there is no 'harm' being done with the alias. Therefore could any sane compiler generate (optimized) code that still gets me into trouble?

Thanks

3

There are 3 answers

10
Kerrek SB On

It is always valid to interpret an object as a sequence of bytes (i.e. it is not an aliasing violation to treat any object pointer as the pointer to the first element of an array of chars), and you can construct an object in any piece of memory that's large enough and suitably aligned.

So, you can allocate a large array of chars (any signedness), and locate an offset that's aliged at alignof(maxalign_t); now you can interpret that pointer as an object pointer once you've constructed the appropriate object there (e.g. using placement-new in C++).

You do of course have to make sure not to write into an existing object's memory; in fact, object lifetime is intimately tied to what happens to the memory which represents the object.

Example:

char buf[50000];

int main()
{
    uintptr_t n = reinterpret_cast<uintptr_t>(buf);
    uintptr_t e = reinterpret_cast<uintptr_t>(buf + sizeof buf);

    while (n % alignof(maxalign_t) != 0) { ++n; }

    assert(e > n + sizeof(T));

    T * p = :: new (reinterpret_cast<void*>(n)) T(1, false, 'x');

    // ...

    p->~T();
}

Note that memory obtained by malloc or new char[N] is always aligned for maximal alignment (but not more, and you may wish to use over-aligned addresses).

1
Testing On

If a union is used to hold the int and float variables, then you can by pass the strict aliasing. More about this is given in http://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html

Also see the following article.

http://blog.regehr.org/archives/959

He gives a way to use unions to do this.

11
Cort Ammon On

Actually, what you have written is not a strict aliasing violation.

C++11 spec 3.10.10 says:

If a program attempts to access the stored value of an object through a glvalue of other than one of the following types the behavior is undefined

So the thing that causes the undefined behavior is accessing the stored value, not just creating a pointer to it. Your example does not violate anything. It would need to do the next step: float badValue = smem[0]. smem[0] gets the stored value from the shared buffer, creating an aliasing violation.

Of course, you aren't about to just grab smem[0] before setting it. You are going to write to it first. Assigning to the same memory does not access the stored value, so no ailiasing However, it IS illegal to write over the top of an object while it is still alive. To prove that we are safe, we need object lifespans from 3.8.4:

A program may end the lifetime of any object by reusing the storage which the object occupies or by explicitly calling the destructor for an object of a class type with a non-trivial destructor. For an object of a class type with a non-trivial destructor, the program is not required to call the destructor explicitly before the storage which the object occupies is reused or released; ... [continues on regarding consequences of not calling destructors]

You have a POD type, so trivial destructor, so you can simply declare verbally "the int objects are all at the end of their lifespan, I'm using the space for floats." You then reuse the space for floats, and no aliasing violation occurs.