我们都知道像这样打字
union U {float a; int b;};
U u;
std::memset(u, 0, sizeof u);
u.a = 1.0f;
std::cout << u.b;
是C中未定义的行为 .
它是未定义的,因为在 u.a = 1.0f;
赋值后 .a
变为活动字段且 .b
变为非活动字段,并且从非活动字段读取它是未定义的行为 . 我们都知道这一点 .
现在,请考虑以下代码
union U {float a; int b;};
U u;
std::memset(u, 0, sizeof u);
u.a = 1.0f;
char *ptr = new char[std::max(sizeof (int),sizeof (float))];
std::memcpy(ptr, &u.a, sizeof (float));
std::memcpy(&u.b, ptr, sizeof (int));
std::cout << u.b;
现在它变得很明确,因为允许这种类型的惩罚 . 另外,如你所见, u memory remains same after memcpy() calls.
现在让我们添加线程和 volatile
关键字 .
union U {float a; int b;};
volatile U u;
std::memset(u, 0, sizeof u);
u.a = 1.0f;
std::thread th([&]
{
char *ptr = new char[sizeof u];
std::memcpy(ptr, &u.a, sizeof u);
std::memcpy(&u.b, ptr, sizeof u);
});
th.join();
std::cout << u.b;
逻辑保持不变,但我们只有第二个线程 . 由于 volatile
关键字代码仍然定义明确 .
在实际代码中,第二个线程可以通过任何蹩脚的线程库实现,编译器可能不知道第二个线程 . 但由于 volatile
关键字,它仍然定义明确 .
但是如果没有其他线程怎么办?
union U {float a; int b;};
volatile U u;
std::memset(u, 0, sizeof u);
u.a = 1.0f;
std::cout << u.b;
没有其他线程 . But compiler does not know that there is no other threads!
从编译器的角度来看,没有任何改变!如果第三个例子定义明确,那么最后一个例子也必须明确定义!
而且我们不需要第二个线程,因为它无论如何都不会改变 u
内存 .
如果使用 volatile
,编译器会假定 u
可以在任何时候以静默方式修改 . 在这种修改中,任何字段都可以变为活动
因此,编译器永远无法跟踪volatile活动的哪个字段处于活动状态 . 它不能假设一个字段在分配给它后保持活动状态(并且其他字段保持不活动状态),即使没有真正修改该联合 .
因此,在最后两个示例中,编译器应该给出 1.0f
转换为 int
的精确位表示 .
问题是: Is my reasoning correct? Are 3rd and 4th examples really well-defiend? What the standard says about it?
2 回答
这种说法是错误的,因此你得出结论的其余逻辑是不合理的 .
假设你有这样的代码:
如果
foobar
不是volatile,则允许编译器推理如下:"I know that foobar is never aliased by currentBuf and therefore does not change within the loop, therefore I may optimize the code as"如果
foobar
是volatile
,则禁用此和许多其他代码生成优化 . 注意我说代码生成 . 如果不违反内存模型 of the CPU ,则CPU完全在其权限范围内,可以将读取和写入移动到其内容中 .特别是,编译器不需要强制CPU在每次读写
foobar
时返回主存储器 . All 需要做的是避开某些优化 . (这不是严格正确的;编译器也有义务确保保留涉及长跳转的某些属性,以及一些与线程无关的其他小细节 . )如果有两个线程,并且每个都在不同的线程上处理器,并且每个处理器具有不同的缓存,如果它们都包含foobar
的内存副本,则不要求缓存变得一致 .为方便起见,有些编译器可能会选择实现这些语义,但不要求它们这样做;请参阅编译器文档 .
我注意到C#和Java确实需要在volatile上获取和释放语义,但这些要求可能会非常弱 . 特别是,x86不会重新排序两个易失性写入或两个易失性读取,但允许在另一个变量的易失性写入之前重新排序一个变量的易失性读取,事实上x86处理器可以在极少数情况下这样做 . (有关用C#编写的谜题,请参阅http://blog.coverity.com/2014/03/26/reordering-optimizations/,该谜题说明了即使一切都是易失性且具有获取释放语义,低锁代码如何也是错误的 . )
道德是:即使你的编译器是有用的,并且对C#或Java等易变变量强加了额外的语义, it still may be the case that there is no consistently observed sequence of reads and writes across all threads ;许多内存模型都没有强加这个要求 . 这可能会导致奇怪的运行时行为 . 如果您想知道
volatile
对您来说意味着什么,请再次参考您的编译器文档 .不 - 你的推理是错误的 . 不稳定的部分是一个普遍的误解 - 挥发性不是你所说的 .
工会部分也错了 . 阅读本文Accessing inactive union member and undefined behavior?
使用c(11),当最后一次写入对应于下一次读取时,您只能期望正确/良好定义的行为 .