[英]Is VC++ still broken Sequentially-Consistent-wise?
我看了(大部分) Herb Sutter的atmoic <>武器视频 ,我想用样本中的循环来测试“条件锁定”。 显然,虽然(如果我理解正确的话)C ++ 11标准说下面的例子应该正常工作并且顺序一致,但事实并非如此。
在您继续阅读之前,我的问题是:这是正确的吗? 编译器坏了吗? 我的代码是否被破坏 - 我在这里遇到了一个我错过的竞争条件吗? 我该如何绕过这个?
我尝试了3种不同版本的Visual C ++:VC10专业版,VC11专业版和VC12 Express版(== Visual Studio 2013 Desktop Express)。
下面是我用于Visual Studio 2013的代码。对于其他版本,我使用boost而不是std,但想法是一样的。
#include <iostream>
#include <thread>
#include <mutex>
int a = 0;
std::mutex m;
void other()
{
std::lock_guard<std::mutex> l(m);
std::this_thread::sleep_for(std::chrono::milliseconds(2));
a = 999999;
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << a << "\n";
}
int main(int argc, char* argv[])
{
bool work = (argc > 1);
if (work)
{
m.lock();
}
std::thread th(other);
for (int i = 0; i < 100000000; ++i)
{
if (i % 7 == 3)
{
if (work)
{
++a;
}
}
}
if (work)
{
std::cout << a << "\n";
m.unlock();
}
th.join();
}
总结代码的概念:全局变量a
受全局互斥锁m
保护。 假设没有命令行参数( argc==1
),运行other()
的线程是唯一一个应该访问全局变量a
的线程。
程序的正确输出是打印999999。
但是,由于编译器循环优化(使用寄存器进行循环增量,并在循环结束时将值复制回a
),即使它不应该由程序集修改a
。
这发生在所有3个VC版本中,虽然在VC12的这个代码示例中,我不得不调用sleep()
来使其中断。
这是一些汇编代码(此运行中的a
的地址是0x00f65498
):
循环初始化 - 来自a
值被复制到edi
27: for (int i = 0; i < 100000000; ++i)
00F61543 xor esi,esi
00F61545 mov edi,dword ptr ds:[0F65498h]
00F6154B jmp main+0C0h (0F61550h)
00F6154D lea ecx,[ecx]
28: {
29: if (i % 7 == 3)
的条件内递增,并且在循环之后复制回的位置a
无条件
30: {
31: if (work)
00F61572 mov al,byte ptr [esp+1Bh]
00F61576 jne main+0EDh (0F6157Dh)
00F61578 test al,al
00F6157A je main+0EDh (0F6157Dh)
32: {
33: ++a;
00F6157C inc edi
27: for (int i = 0; i < 100000000; ++i)
00F6157D inc esi
00F6157E cmp esi,5F5E100h
00F61584 jl main+0C0h (0F61550h)
32: {
33: ++a;
00F61586 mov dword ptr ds:[0F65498h],edi
34: }
并且程序的输出为0
。
'volatile'关键字将阻止这种优化。 这正是它的用途:'a'的每次使用都将完全按照所示的方式读取或写入,并且不会以不同的顺序移动到其他volatile变量。
互斥锁的实现应该包括特定于编译器的指令,以便在该点引起“围栏”,告诉优化器不要跨越该边界重新排序指令。 由于实现不是来自编译器供应商,可能是遗漏了? 我从来没有检查过。
由于'a'是全局的,我通常会认为编译器会更加小心。 但是,VS10不了解线程,所以不会考虑其他线程会使用它。 由于优化器掌握了整个循环执行,它知道从循环内调用的函数不会触及'a',这就足够了。
我不确定新标准对于除volatile之外的全局变量的线程可见性的说法。 也就是说,是否存在一个可以阻止优化的规则(即使该函数可以一直向下掌握,因此它知道其他函数不使用全局,它是否必须假设其他线程可以)?
我建议使用编译器提供的std :: mutex来尝试更新的编译器,并检查C ++标准和当前草案的内容。 我认为以上内容可以帮助您了解要寻找什么。
-约翰
差不多一个月后,微软仍未对MSDN Connect中的错误做出回应。
总结一下上面的评论(以及一些进一步的测试),显然它也发生在VS2013专业版中,但是这个bug只发生在为Win32而不是x64构建时。 x64中生成的汇编代码没有此问题。 所以它似乎是优化器中的一个错误,并且此代码中没有竞争条件。
显然这个错误也发生在GCC 4.8.1中,但不是在GCC 4.9中。 (感谢Voo , nosid和Chris Dodd的所有测试)。
有人建议,以纪念a
为volatile
。 这确实可以防止错误,但这只是因为它阻止优化器执行循环寄存器优化。
我找到了另一个解决方案:添加另一个局部变量b
,如果需要(并在锁定下),请执行以下操作:
a
复制到b
b
a
优化取代了局部变量与寄存器,所以代码仍优化,但往返于拷贝a
,如果需要的只是完成,下锁。
这是新的main()
代码,箭头标记更改的行。
int main(int argc, char* argv[])
{
bool work = (argc == 1);
int b = 0; // <----
if (work)
{
m.lock();
b = a; // <----
}
std::thread th(other);
for (int i = 0; i < 100000000; ++i)
{
if (i % 7 == 3)
{
if (work)
{
++b; // <----
}
}
}
if (work)
{
a = b; // <----
std::cout << a << "\n";
m.unlock();
}
th.join();
}
这就是汇编代码的样子( &a == 0x000744b0
, b
替换为edi
):
21: int b = 0;
00071473 xor edi,edi
22:
23: if (work)
00071475 test bl,bl
00071477 je main+5Bh (07149Bh)
24: {
25: m.lock();
........
00071492 add esp,4
26: b = a;
00071495 mov edi,dword ptr ds:[744B0h]
27: }
28:
........
33: {
34: if (work)
00071504 test bl,bl
00071506 je main+0C9h (071509h)
35: {
36: ++b;
00071508 inc edi
30: for (int i = 0; i < 100000000; ++i)
00071509 inc esi
0007150A cmp esi,5F5E100h
00071510 jl main+0A0h (0714E0h)
37: }
38: }
39: }
40:
41: if (work)
00071512 test bl,bl
00071514 je main+10Ch (07154Ch)
42: {
43: a = b;
44: std::cout << a << "\n";
00071516 mov ecx,dword ptr ds:[73084h]
0007151C push edi
0007151D mov dword ptr ds:[744B0h],edi
00071523 call dword ptr ds:[73070h]
00071529 mov ecx,eax
0007152B call std::operator<<<std::char_traits<char> > (071A80h)
........
这样可以保持优化并解决(或解决)问题。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.