MSVC 改用 LLVM-libc,數學函式達成位元級一致
Microsoft C++ Blog · 2026-09-16
同一段程式在不同編譯器、不同版本、甚至編譯期求值和執行期求值之間,數學函式算出來的最後一個位元從此不再是機率問題——MSVC 把 /Zc:cmath 底下的數學函式求值,換成了 LLVM-libc 的正確捨入(correctly rounded)實作。微軟 C++ 團隊的 Cody Miller、Tue Ly、Michael Jones 三人於 2026 年 9 月 16 日發表文章,說明這項改動背後的原因與具體做法。這不是單純換個函式庫,而是把數學函式「正確」的定義從『看起來夠準』改成『每個位元都可證明是唯一解』。
跨平台 libm 不一致:x87 FSIN 是教科書案例
傳統上,sin、cos、pow 這類數學函式在不同平台的最後一兩個位元本來就不保證一樣。glibc、MSVC 內建的執行期函式庫、以及編譯期常數折疊(constexpr)各自用不同的近似多項式,同一個輸入丟進去,結果可能因為作業系統、編譯器版本,甚至是不是在編譯期求值而有落差。對只在乎畫面顯示的應用來說這點誤差無關緊要,但對需要跨機器重現結果的科學運算、模擬與金融計算而言,這種各算各的狀況會讓測試與除錯變得不可靠。
Intel 最早的 x87 硬體超越函式指令,就是這種不一致的極端案例。FSIN 指令在做參數化簡(argument reduction)時,用的是只截斷到 66 位元的 π 近似值,一旦輸入角度夠大,化簡出來的餘角就完全跑偏,最終誤差可以達到 1.37×10¹⁹ ULP(ULP 是浮點數在該精度下最後一位元的最小間距,這個誤差量級代表結果已經和正確答案毫無關係)。AMD 為了相容性選擇原封不動複製這個行為,等於把這個瑕疵焊進了整個 x86 生態系。文章引用 Bruce Dawson 的分析並指出,微碼化的 FSIN 不但不準,執行速度也比 Payne-Hanek 這類軟體演算法慢。
| 比較項目 | 舊機制(x87 FSIN 硬體指令) | 新機制(LLVM-libc 正確捨入) |
|---|---|---|
| π 近似值 | 硬體截斷至 66 位元 | 依需求精度動態計算 |
| 最壞情況誤差 | 可達 1.37×10¹⁹ ULP | 正確捨入,結果具數學唯一性 |
| 跨平台/版本一致性 | 依硬體廠商實作而異 | 位元級一致 |
正確捨入怎麼做到:Ziv's test 與 128 位元上界
正確捨入的定義很嚴格:對於給定的輸入和 IEEE-754 捨入模式,正確答案必須是把數學上精確的結果,依該捨入模式對應到唯一一個浮點數位元樣式。換句話說,只要輸入與捨入模式相同,答案就不該再有第二種可能,這正是位元級一致的來源。LLVM-libc 把這個定義直接當成實作目標,而不是「盡量準」的近似值。
要保證每次都拿到這個唯一答案,靠的是先用一段夠快、精度較低的近似多項式算出結果和對應的誤差區間,再檢查這個誤差區間有沒有跨過捨入邊界,這個技巧稱為 Ziv's rounding test。只要誤差區間完全落在邊界同一側,代表捨入方向已經確定,函式就能立刻回傳結果,不必動用更貴的高精度運算。文章指出,靠這套快速路徑就能處理超過 99.99% 的輸入。只有極少數卡在邊界附近的難捨入案例,才需要進一步加碼精度重算。
那些卡在邊界附近的少數案例怎麼辦?LLVM-libc 的作法是把中間運算精度一路加到 128 位元,而不是像任意精度(bignum)方案那樣不設上限地持續加碼。文章確認,對絕大多數單變數的雙精度數學函式來說,128 位元的中間精度已經足夠分辨出正確的捨入方向。這代表最壞情況下的運算量是有界、可預期的,不會因為極端輸入讓程式陷入不可預測的變慢。
誰該注意:跨平台數值計算與可重現性
這項改動影響最大的,是需要同一份程式碼在不同機器上算出同一個答案的場景。科學運算、數值模擬、金融計算,以及需要固定隨機種子重現訓練結果的機器學習流程,過去都得自行繞開 libm 在不同平台的細微差異,例如自製數學函式或鎖定特定函式庫版本。現在只要編譯器與函式庫都換成正確捨入的實作,這類自保措施就可以拿掉。
對 MSVC 使用者來說,具體的進入點是啟用 /Zc:cmath 編譯選項,讓編譯期常數求值(constexpr)和執行期呼叫都走 LLVM-libc 的正確捨入實作。這也解決了一個容易被忽略的陷阱:同一段運算式若在編譯期被算成常數、和在執行期才呼叫函式庫,過去可能得到不同結果,現在兩者保證一致。需要跨編譯器比對測試結果、或在 CI 中比較不同平台輸出的專案,可以優先評估升級。