[英]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.