8

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

rsp1984
  • 1,877
  • 21
  • 23

3 Answers3

3

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.

Cort Ammon
  • 10,221
  • 31
  • 45
  • Thanks, this is very helpful. If I understand you correctly, writing to `scratchMem[i]` before reading `scratchMem[i]` will always be safe from an aliasing standpoint because I have ended the object lifetime at location `scratchMem + i` by simply writing to it. Is that correct? Also, I am confused about the term 'reusing the storage'. How is that defined? For instance: `int64_t a = 0; ((int16_t*)a)[1] = 1;`. Has `a`'s lifetime ended with the 2nd assignment? How does a proper 'reuse of storage' look like? – rsp1984 Sep 05 '13 at 15:05
  • As BenVoigt pointed out in another comment, a compiler may be free to parallelize / delay access to memory when pointers are of different type. This is since the compiler assumes that these pointers don't alias. Therefore I assume one can still run into strict-aliasing-related problems even after setting `smem[0]` with a float type. The compiler may just be free to issue instructions modifying the memory at `smem`, interpreted as int type, after that since it assumes memory doesn't overlap, thereby changing the value of `smem[0]` through the back door. Would appreciate clarification though! – rsp1984 Sep 05 '13 at 21:09
  • Compilers are only allowed to do out of order execution if they can prove that doing so does not change the results of a validly formed program. Because a program may end the lifetime of a trivially destructable object at any time by reusing storage, the compiler may not paralellize these tasks unless it can prove that the lifetimes of the objects have not ended. If it was not for this, then there is no way you could write `void* mem = malloc(max(sizeof(A), sizeof(B)); A* a = new (mem) A; a->~A(); B* b = new (mem) B;` without fear of the constructors being run out of order – Cort Ammon Sep 06 '13 at 20:38
  • In your first comment, you have a choice. Either a is still alive at the time you do the 16-bit assignment, or it isn't. If 'a' is still alive, then you have a clear aliasing issue because you accessed the memory within a as a int16_t, which is not the type of a. On the other hand, if you declare 'a' to be dead before the assignment, then you may reuse the memory as an int16_t without issue. – Cort Ammon Sep 06 '13 at 20:44
  • Wrt out-of-order: The example you gave does make sense, however you are using placement-new syntax to re-use the memory. Does the same hold true for simple assignment? (Btw. I started another thread to discuss that very issue: http://stackoverflow.com/questions/18659427/c-c-strict-aliasing-object-lifetime-and-modern-compilers/18664954?noredirect=1#comment27490213_18664954) – rsp1984 Sep 06 '13 at 20:55
  • Wrt `a` being still alive or not, I guess I don't fully understand what you mean. How can I "declare `a` to be dead" before the assignment? – rsp1984 Sep 06 '13 at 20:57
  • And to be pedantic, I would word it that "a's lifetime ended anytime before the 2nd assignment." The second assignment does not directly affect the lifetime of a, other than to provide a bound that "a better not be alive by the time of the assignment = 1, or else there is an aliasing violation." Looking at it more stringently, 'a' may be obliged to live, because it is a local with automatic storage. 6.7.2 states that objects are destroyed at the end of a scope. This is obviously meaningful for those with non-trivial destructors, but does a trivial destructor require a to live until then? – Cort Ammon Sep 06 '13 at 21:04
  • I had to reread the spec to be more precise: I spoke incorrectly, the assignment =1 indeed is the official point where 'a' has to die, because that is the place where the storage behind 'a' is being reused. I tend to prefer to say "ends before the assignment" because that includes what the optimizer can do: the optimizer can decrease the lifespan of any object if doing so doesn't affect the result. However you choose to word it, the assignment =1 assigns a value into uninitialized storage space that happened to be 'a' beforehand. – Cort Ammon Sep 06 '13 at 21:17
  • So what is your final opinion on the issue? Does `((int16_t*)a)[1] = 1;` constitute an aliasing violation? Would it be considered a proper 're-use' of storage? What about types that match in size and alignment requirements: `((double*)a)[0] = 1;`? – rsp1984 Sep 06 '13 at 21:28
  • I do not believe `((int16_t*)a)[1] = 1` causes an aliasing violation. However, size and alignment will matter, not for purposes of aliasing, but simply to make sure the object being assigned is properly valid. There is one thing I am uncertain on. In your initial example, I think the `delete [] scratchMem` line might have an aliasing issue. I opened a followup question: http://stackoverflow.com/questions/18667318/do-trivial-destructors-cause-aliasing – Cort Ammon Sep 06 '13 at 22:14
  • Thanks. Another follow-on question you might be interested in: http://stackoverflow.com/questions/18667056/c-bypassing-strict-aliasing-through-union-then-use-restrict-extension – rsp1984 Sep 06 '13 at 22:24
1

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).

Kerrek SB
  • 464,522
  • 92
  • 875
  • 1,084
  • "always valid to interpret an object as a sequence of bytes"... only for some operations. For example, the "Create an object of any type within" operation that applies to sequences of bytes does not apply to "another object interpreted as a sequence of bytes". – Ben Voigt Sep 04 '13 at 22:37
  • @BenVoigt: Only to the extent that it's UB to use a non-trivial object's storage for something else before calling the destructor... I think there's nothing inherently wrong with the statement. – Kerrek SB Sep 04 '13 at 22:40
  • 2
    If you use a `float`'s storage to create an `int` within, you open the door to strict aliasing violations. In fact, I think the compiler might even be permitted to delay a write to the `float` until after the memory is reused for an `int` (corrupting the value of the `int`), because strict aliasing permits the compiler to assume that objects of different type don't overlap. Since this question is specifically concerned with strict aliasing rules, I think that's important. Best is to use memory that started life as `char[]`. – Ben Voigt Sep 04 '13 at 22:45
  • @BenVoigt: Sure, but I'm not using a float's storage. I'm using an array of chars. Not every array of bytes can be used for every purpose, I suppose. You can always *read* bytes, but writing bytes must adhere to the appropriate object semantics. – Kerrek SB Sep 04 '13 at 22:46
  • That's why I'm suggesting you remove the first clause of the first sentence. It isn't needed for the rest of the answer, and it might lead to someone thinking they can treat any object as if it were untyped storage and put something new inside. – Ben Voigt Sep 04 '13 at 22:49
  • @BenVoigt: Hm, maybe... I've expanded on it a bit. But do feel free to edit the post if you think it's too misleading. I'm not thinking very straight at the moment. – Kerrek SB Sep 04 '13 at 22:51
  • @KerrekSB: Do you have an answer for my second question ("Even though...") as well? It would help me understand strict aliasing better. – rsp1984 Sep 04 '13 at 22:58
  • @BenVoigt: (Or post an answer!) In fact, I think the following is legal: `float x; *(int*)(char*)(&x) = 10;`, and now always access the variable through an `int` pointer. Since `float` is a POD, its lifetime ends when its memory is reused, and assuming that size and alignment match, a new `int` is created in its place. It's basically the "active member" union semantics. – Kerrek SB Sep 04 '13 at 23:00
  • @RafaelSpring: Compilers can detect undefined behaviour and aggressively optimize all your code away. Clang is doing such optimizations fairly thoroughly. – Kerrek SB Sep 04 '13 at 23:01
  • @KerrekSB: So the answer would be "yes, breaking strict aliasing the way it's done in the question could still get you into trouble"? This answer (http://stackoverflow.com/questions/1225741/performance-impact-of-fno-strict-aliasing) suggests LLVM does not have strict aliasing but I may be confusing things here (e.g. not sure how to interpret "have" in the sentence and if LLVM means something different than clang). – rsp1984 Sep 04 '13 at 23:07
0

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.

Testing
  • 76
  • 3
  • 1
    For this purpose, probably, but `union` does not solve the strict aliasing problems most people use it for (reading a different member than what was written). – Ben Voigt Sep 04 '13 at 22:35