簡體   English   中英

VC ++仍然按順序 - 一致嗎?

[英]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中。 (感謝VoonosidChris Dodd的所有測試)。

有人建議,以紀念avolatile 這確實可以防止錯誤,但這只是因為它阻止優化器執行循環寄存器優化。

我找到了另一個解決方案:添加另一個局部變量b ,如果需要(並在鎖定下),請執行以下操作:

  1. a復制到b
  2. 循環中增加b
  3. 如果需要,復制回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 == 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)  

     ........

這樣可以保持優化並解決(或解決)問題。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM