A typical situation you encounter strict aliasing problems is when overlaying a struct (like a device/network msg) onto a buffer of the word size of your system (like a pointer to uint32_ts or uint16_ts). When you overlay a struct onto such a buffer, or a buffer onto such a struct through pointer casting you can easily violate strict aliasing rules.
So in this kind of setup, if I want to send a message to something I'd have to have two incompatible pointers pointing to the same chunk of memory. I might then naively code something like this:
struct Msg
{
unsigned int a;
unsigned int b;
};
int main()
{
// Get a 32-bit buffer from the system
uint32_t* buff = malloc(sizeof(Msg));
// Alias that buffer through message
Msg* msg = (Msg*)(buff);
// Send a bunch of messages
for (int i =0; i < 10; ++i)
{
msg->a = i;
msg->b = i+1;
SendWord(buff[0] );
SendWord(buff[1] );
}
}
The strict aliasing rule makes this setup illegal, dereferencing a pointer that aliases another of anincompatible type is undefined behavior. Unfortunately you can still code this way, maybe* get some warnings, have it compile fine, only to have weird unexpected behavior when you run the code.
*(gcc appears pretty inconsistent in its ability to give aliasing warnings, giving us a friendly warning herebut not here)
To see why this behavior is undefined, we have to think about what the strict aliasing rule buys the compiler. Basically, with this rule, it doesn't have to think about inserting instructions to refresh the contents of buff every run of the loop. Instead when optimizing, with some annoyingly unenforced assumptions about aliasing, it can omit those instructions, load
buff[0]
and buff[1
] once before the loop is run, and speed up the body of the loop. Before strict aliasing was introduced, the compiler had to live in a state of paranoia that the contents of buff could change at anytime from anywhere by anybody. So to get an extra performance edge, and assuming most people don't type pun pointers, the strict aliasing rule was introduced.Keep in mind, if you think the example is contrived, this might even happen if you're passing a buffer to another function doing the sending for you, if instead you have.
void SendMessage(uint32_t* buff, size_t size32)
{
for (int i = 0; i < size32; ++i)
{
SendWord(buff[i]);
}
}
And rewrote our earlier loop to take advantage of this convenient function
for (int i =0; i < 10; ++i)
{
msg->a = i;
msg->b = i+1;
SendMessage(buff, 2);
}
The compiler may or may not be able to or smart enough to try to inline SendMessage and it may or may not decide to load or not load buff again. If SendMessage is part of another API that's compiled separately, it probably has instructions to load buff's contents. Then again, maybe you're in C++ and this is some templated header only implementation that the compiler thinks it can inline. Or maybe its just something you wrote in your .c file for your own convenience. Anyway undefined behavior might still ensue. Even when we know some of whats happening under the hood, its still a violation of the rule so no well defined behavior is guaranteed. So just by wrapping in a function that takes our word delimited buffer doesn't necessarily help.
So how do I get around this?
- Use a union. Most compilers support this without complaining about strict aliasing. Its usage, however, (write to the struct then read from the buffer) is technically in violation of the spec.
union {
Msg msg;
unsigned int asBuffer[sizeof(Msg)];
};
- You can disable strict aliasing in your compiler (f[no-]strict-aliasing in gcc))
- You can use
char*
for aliasing instead of your system's word. The rules allow an exception forchar*
. Its always assumed thatchar*
aliases other types. However this won't work the other way, there's no assumption that your struct aliases a buffer of chars.
Beginner beware
This is only one potential minefield when overlaying two types onto each other. You should also learn about endianess, word alignment, and how to deal with alignment issues through packing structscorrectly.
0 comments:
Post a Comment