`volatile`是否允许对工会进行类型惩罚?

时间:2015-10-17 11:34:31

标签: c++ volatile unions type-punning

我们都知道像这样的打字

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来电后 memcpy()内存保持不变。

<小时/> 现在让我们添加线程和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;

没有其他线程。 但编译器不知道没有其他线程!

从编译器的角度来看,没有任何改变!如果第三个例子定义明确,那么最后一个例子也必须明确定义!

我们不需要第二个线程,因为它无论如何都不会改变u内存。

<小时/> 如果使用volatile,则编译器假定可以在任何时候以静默方式修改u。在这种修改中,任何字段都可以变为活动状态。

因此,编译器永远无法跟踪volatile活动的哪个字段处于活动状态。 它不能假设一个字段在被分配后仍保持活动状态(并且其他字段保持不活动状态),即使没有任何内容真正修改该联合。

因此,在最后两个例子中,编译器应该给出转换为1.0f的{​​{1}}的精确位表示。

<小时/> 问题是:我的推理是否正确?第3和第4个例子真的很好吗?标准说的是什么?

2 个答案:

答案 0 :(得分:9)

  

在实际代码中,第二个线程可以通过任何蹩脚的线程库实现,编译器可能不知道第二个线程。但由于volatile关键字,它仍然定义明确。

这种说法是错误的,因此你得出结论的其余逻辑是不合理的。

假设你有这样的代码:

int* currentBuf = bufferStart;
while(currentBuf < bufferEnd)
{
    *currentBuf = foobar;    
    currentBuf++;
}

如果foobar不是易失性的,那么允许编译器推理如下:“我知道foobar永远不会被currentBuf别名,因此在循环中不会改变,因此我可以将代码优化为”< / p>

int* currentBuf = bufferStart;
int temp = foobar;
while(currentBuf < bufferEnd)
{
    *currentBuf = temp;    
    currentBuf++;
}

如果foobarvolatile,则会禁用此许可和许多其他代码生成优化。注意我说代码生成 CPU 完全在其权利范围内,但是如果不违反CPU 的内存模型,则将读取和写入移动到其内容的内容。

特别是,在foobar的每次读写操作中,编译器都不需要强制CPU返回主存。 全部需要做的是避开某些优化。 (这不是严格意义上的;编译器也必须确保保留涉及长跳转的某些属性,以及与线程无关的一些其他小细节。)如果有两个线程,并且每个都在不同的线程上处理器,并且每个处理器具有不同的缓存,volatile如果它们都包含foobar的内存副本,则不要求缓存变得一致。

为方便起见,有些编译器可能会选择实现这些语义,但不要求它们这样做;请参阅编译器文档。

我注意到C#和Java 需要获取和释放volatile上的语义,但这些要求可能会非常弱。特别是,x86不会重新排序两个易失性写入或两个易失性读取,但允许在另一个变量的易失性写入之前重新排序一个变量的易失性读取,事实上x86处理器可以在极少数情况下这样做。 (有关用C#编写的谜题,请参阅http://blog.coverity.com/2014/03/26/reordering-optimizations/,该谜题说明了即使一切都是易失性且具有获取释放语义,低锁代码如何也是错误的。)

道德观点是:即使你的编译器是有用的,并且对C#或Java等易变变量强加了额外的语义,仍然可能是所有线程都没有始终观察到的读写序列< /强>;许多内存模型都没有强加这个要求。这可能会导致奇怪的运行时行为。如果您想了解volatile对您的意义,请再次查阅您的编译器文档

答案 1 :(得分:1)

不 - 你的推理是错误的。不稳定的部分是一种普遍的误解 - 挥发性并不像你所说的那样有效。

工会部分也错了。阅读此Accessing inactive union member and undefined behavior?

使用c ++(11),当最后一次写入对应于下一次读取时,您只能期望正确/良好定义的行为。