海森堡蟲(heisenbug)
/ heisenbug -> HY-zen-bug /
有一種特別、令人抓狂的臭蟲,你一試著去看它,它就消失。你加一個印出想看看發生了什麼——當機就停了。你把它跑在除錯器下想抓住它——它表現得完美無瑕。你建一個除錯版——問題蒸發了,卻在你一出貨最佳化版的瞬間又回來。這就是海森堡蟲:一個行為會改變、往往消失的臭蟲,正因為你觀測了它。這個名字是對海森堡測不準原理的雙關,那是「測量一個系統會擾動它」的物理概念。
具體來說,這個臭蟲其實並不在乎你在看——改變的是你觀測這個動作作為副作用所更動到的某樣東西。常見的機制:加一個印出、或用 -g 編譯,會改變時序或記憶體佈局,所以兩個執行緒之間的資料競爭現在以不同方式交錯、這次執行不再發生破壞。除錯器暫停了一個執行緒、藏住一個競爭。一個未初始化的變數在最佳化版讀到垃圾,但除錯版碰巧把那個堆疊位置歸了零。最佳化器在 -O2 下利用的未定義行為,在 -O0 下被放過,所以關掉最佳化症狀就消失。每一種情況裡真正的臭蟲都還在——一個競爭、一次未初始化讀取、一個未定義行為——但它只在觀測所擾動的那個確切時序或記憶體條件下才現形。
為何重要:海森堡蟲是一個強烈的診斷訊號,不只是挫折。如果一個臭蟲在你加一個印出或接上除錯器時消失,它幾乎總是指向一份很短的元兇名單——一個資料競爭、對未初始化記憶體的使用、或最佳化器正在利用的未定義行為——所以這個消失本身就縮小了搜尋範圍。實務上的動作是:從會擾動的觀測,切換到能偵測底層類別、又不依賴時序的工具:執行緒消毒器抓競爭、記憶體消毒器或 Valgrind 抓未初始化讀取、UBSan 抓未定義行為。錯誤的做法是用那個正好藏住它的印出去「修」一個海森堡蟲、然後宣告勝利——你只是讓症狀安靜下來,而臭蟲會在不同條件下回來。
一個多執行緒的計數器算出錯誤的總和——但只在發行版。你加 printf("%d\n", count); 想調查,總和卻變正確了。那個印出沒修好任何東西;它把一個執行緒拖慢得恰好足以藏住一個資料競爭。接著 TSan 不論時序如何,都直接標出那個競爭。
一個印出靠擾動時序「修好」了症狀;資料競爭仍在,而 TSan 找得到它。
一個在觀測下消失的臭蟲並沒有被修好——那個印出或除錯器只是改變了讓它得以藏身的時序或記憶體佈局。把這個消失當成一個指向資料競爭、未初始化讀取、或被最佳化器利用之未定義行為的線索,動用一個消毒器、而不是宣告勝利。