`volatile`是否允许与工会打字?

Hol*_*Cat 2 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;
Run Code Online (Sandbox Code Playgroud)

是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;
Run Code Online (Sandbox Code Playgroud)

现在它变得很明确,因为允许这种类型的惩罚.此外,如您所见,调用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;
Run Code Online (Sandbox Code Playgroud)

逻辑保持不变,但我们只有第二个线程.因为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;
Run Code Online (Sandbox Code Playgroud)

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

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

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


如果volatile使用,编译器假定u可以在任何时候静默修改.在这种修改中,任何字段都可以变为活动

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

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


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

Eri*_*ert 9

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

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

假设你有这样的代码:

int* currentBuf = bufferStart;
while(currentBuf < bufferEnd)
{
    *currentBuf = foobar;    
    currentBuf++;
}
Run Code Online (Sandbox Code Playgroud)

如果foobar不是volatile,那么允许编译器如下推理:"我知道foobar永远不会被currentBuf别名,因此在循环内不会改变,因此我可以优化代码为"

int* currentBuf = bufferStart;
int temp = foobar;
while(currentBuf < bufferEnd)
{
    *currentBuf = temp;    
    currentBuf++;
}
Run Code Online (Sandbox Code Playgroud)

如果foobarvolatile这样,那么许多其他代码生成优化被禁用.注意我说代码生成.该CPU是完全有权利但移动读取和写入围绕其心脏的内容,提供的内存模型CPU的不受侵犯.

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

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

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

道德是:即使你的编译器是有用的,并且对C#或Java等易变变量强加了额外的语义,它仍然可能是在所有线程中没有一致观察到的读写序列 ; 许多内存模型都没有强加这个要求.这可能会导致奇怪的运行时行为.如果您想知道对您有何意义,再次参考您的编译器文档volatile.