bakaq
bakaq

Reputation: 53

How to workaround strict aliasing in C?

My aim is to make an generic arena allocator with an buffer stored in the .bss section of the executable to avoid any allocations in the actual program, however this has problems in C with strict aliasing. If I define the buffer as static char buffer[BUFFER_SIZE];, then by strict aliasing I can't use pointers to it that are of another type without causing undefined behavior. As I want this allocator to be generic, so I need to be able to allocate any type in it.

I can't use memcpy() because the allocator needs to return a pointer that can be used in any way, not an copy of the value. I also cannot use union-based type punning because, if I'm not mistaken, it doesn't work through pointers (see the second code block here).

The only way that I know to work around this in C is to disable strict aliasing entirely with the -fno-strict-aliasing flag. In this blog post the author attempts to do the same thing, but the solution he arrives is to use inline assembly to "launder" the pointer so that the optimizer can't apply the strict aliasing optimization, but this seems very fragile.

Is there any better way to do something like this? Ideally there would be an builtin that allows to clearly mark a cast or region of memory to be ignored by strict aliasing.

Upvotes: 3

Views: 446

Answers (2)

supercat
supercat

Reputation: 81247

TLDR: While not officially acknowledged by the "standard", the -fno-strict-aliasing flag is more widely supported and reliable than any alternative constructs other than forcibly separating compilation units and blocking link-time optimization, and the performance costs of -fno-strict-aliasing will often be less than imposed by opaque function calls.

Use the -fno-strict-aliasing flag with clang or gcc, without apology. That's the most portable and reliable way to make such code work. According to the published Rationale, the authors of the C Standard intended that implementations would--on a "quality of implementation" basis--extend the semantics of the language by specifying the behavior of more constructs and corner cases than mandated by the Standard, in cases where doing so would be useful. They sought to give programmers a "fighting chance" [their words!] to write portable programs, but never intended that programmers jump through hoops to ensure portability except when their code had to be usable on a particular implementation that couldn't otherwise support it. Writing code to use -fno-strict-aliasing without any compiler-specific syntax for laundering will yield a program which is compatible with any compiler that has a mode equivalent to -fno-strict-aliasing, i.e. almost any general-purpose C compiler on the planet.

If one doesn't use -fno-strict-aliasing, some parts of the clang or gcc optimizers may transform code whose behavior the Standard was intended to define, into code which downstream optimizations fail to process meaningfully. If, for example, some code happens to read a value using a 64-bit long, reuses the storage as a 64-bit long long, and later uses a long long to write a value that is numerically the same as the long that was read, clang and gcc may treat the latter action as reverting the Effective Type of the storage to long. Although clang or gcc wouldn't recognize or do anything weird with the vast majority of situations where a value that's written to storage using one type happens to match a value that had been read using a different type, the Standard makes no distinction between cases where they wouldn't apply such transforms and cases where they demonstrably apply it in ways that break things.

While it's possible to write some constructs in "optimization-proof" fashion, doing so will often result in machine code that's less efficient than what could be achieved using -fno-strict-aliasing, and the range of situations where quirks in future versions of clang and gcc might break things would be reduced.

Upvotes: 1

Eric Postpischil
Eric Postpischil

Reputation: 223795

Put the allocation code in a separate module or modules from the code using it (the client code). Then, as long as you are building with traditional compilers and linkers that do not perform cross-module optimization, aliasing necessarily works due to the separate-module nature of the build tools rather than due to the requirements of the C standard. (The allocation code itself needs to observe the aliasing rules.)

The reason this works is that when compiling some client code of the allocation routines, the compiler sees only the client code and not the allocation code. It cannot know the declared type used by the allocation code. After you compile the client code, you could link it with your allocation routines or you could, hypothetically, link it with some other code in which all the addresses returned by the allocation code have declared types matching their uses or with some other code in which all the addresses returned by the allocation code are provided by the malloc defined by the C standard rather than your own code. Since the client code could be linked with this other code, the compiler must generate an object module that works with it as specified by the C standard. However, the behavior of your allocation code and the behavior of this other code are identical in the view of the interfaces between them: The client code calls an allocation routine and receives a pointer or it calls a release routine and provides a pointer, and so on. Therefore, when the compiler generates code that works with the other standard-conforming code, it must also work with your actual allocation code.

Upvotes: 3

Related Questions