VC ++仍然按顺序 - 一致吗?

时间:2014-06-19 17:49:09

标签: c++ multithreading visual-c++ concurrency compiler-optimization

我观看了(大部分)Herb Sutter's the atmoic<> weapons video,我想用样本中的循环来测试“条件锁定”。显然,虽然(如果我理解正确的话)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

2 个答案:

答案 0 :(得分:0)

&#39; volatile&#39;关键字将阻止这种优化。这正是它的用途:每次使用&#39; a&#39;将完全按照所示进行读取或写入,并且不会以不同的顺序移动到其他易变变量。

互斥锁的实现应包括特定于编译器的指令,以引起&#34; fence&#34;此时,告诉优化器不要跨越该边界重新排序指令。由于实现不是来自编译器供应商,可能是因为它被遗漏了?我从来没有检查过。

因为&#39; a&#39;是全局的,我一般认为编译器会更加小心。但是,VS10并不了解线程,所以它不会考虑其他线程会使用它。由于优化器掌握了整个循环执行,因此它知道从循环内调用的函数不会触及&#39; a&#39;这足够了。

我不确定新标准对除volatile之外的全局变量的线程可见性有何看法。也就是说,是否存在一个可以阻止优化的规则(即使函数可以一直掌握,因此它知道其他函数不能使用全局,它是否应该假设其他线程可以)?

我建议使用编译器提供的std :: mutex尝试更新的编译器,并检查C ++标准和当前草案的内容。我认为以上内容可以帮助您了解要寻找的内容。

-John

答案 1 :(得分:0)

差不多一个月后,微软仍然没有对bug in MSDN Connect作出回应。

总结一下上面的评论(以及一些进一步的测试),显然它也发生在VS2013专业版中,但只有在为Win32而不是x64构建时才会出现这个bug。 x64中生成的汇编代码没有这个问题。 所以它似乎是优化器中的一个错误,并且此代码中没有竞争条件。

显然,这个错误也发生在GCC 4.8.1中,但不会发生在GCC 4.9中。 (感谢VoonosidChris Dodd进行所有测试。

有人建议将a标记为volatile。这确实可以防止错误,但仅仅是因为它阻止优化器执行循环寄存器优化。

我找到了另一种解决方案:添加另一个本地变量b,如果需要(并在锁定下),请执行以下操作:

  1. a复制到b
  2. 循环中增加b
  3. 如果需要,请复制回a
  4. 优化器用寄存器替换局部变量,因此代码仍然是优化的,但是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 == 0x000744b0b替换为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)  
    
         ........
    

    这可以保持优化并解决(或解决)问题。