偽共享(false sharing)
兩個室友安和班,從不碰彼此的東西——安永遠只用共用抽屜的左半邊,班只用右半邊。他們實際上毫無共享。但有一條規則:只要有人打開抽屜,其他每個人都得關上自己的再重開。現在每次安用她的東西,就逼班把他的甩上再重開,反之亦然,一整天。他們不斷地為一個抽屜爭鬥,儘管從沒想要同一樣東西。偽共享(false sharing)就是快取裡這種令人抓狂的處境:兩個核心不停地使對方那條快取列的副本失效,儘管它們碰的是邏輯上各自獨立、只是剛好住在同一條列裡的變數。
機制直接源自一致性的運作方式。快取以整條快取列為粒度維持一致性(典型是 64 位元組),而非以每位元組或每變數。若變數 X(只被核心 A 使用)和變數 Y(只被核心 B 使用)落在同一條 64 位元組的列裡,那麼當 A 寫 X 時,一致性協定就使 B 快取裡整條列失效——即使 B 根本不在乎 X。B 下次存取 Y 便未命中、重新提取那條列,而當 B 寫 Y 時又反過來使 A 的副本失效。那條列在兩個快取之間乒乓來回,伴隨不斷的未命中和一致性流量,儘管程式邏輯毫無共享。
偽共享是個惡毒的效能臭蟲,因為程式碼看起來完美平行、邏輯上也正確——答案是對的,只是慢得驚人,有時比預期慢上許多倍,而且在原始碼裡不留痕跡。它常咬住這類情形:陣列中每個執行緒寫自己的格子(格子被擠進同一條列),或彼此相鄰擺放的每執行緒計數器。解法是佈局而非邏輯:填補或對齊資料,讓每個執行緒的熱門變數各自坐在自己的快取列裡,使任兩個核心都不共享一條列。這是快取意識強的程式設計師會去想「列」而不只是「變數」的原因之一。
八個執行緒各自遞增一個 8 元素整數陣列裡的 results[i]。八個整數全擠在一條 64 位元組的快取列裡,所以任一執行緒的每次遞增都使另外七個快取裡那條列失效——那條列乒乓來回,使得這個「平行」迴圈跑得比單一執行緒還慢。修法:把每個元素填補到各自的 64 位元組列,執行緒就不再碰撞。
邏輯上獨立的寫入因共享一條快取列而碰撞;填補到各自分開的列,就能消除這種幽靈般的爭用。
偽共享從不導致錯誤的答案——只導致緩慢——這正是它如此容易被忽略的原因。它是資料在快取列中佈局的性質,而非程式邏輯的性質,並且在原始碼裡是隱形的。