避浮點(diǎn)消減帶來的精度損失)
開發(fā)工具靜態(tài)分析代碼質(zhì)量質(zhì)量保障【免費(fèi)下載鏈接】cppcheckstatic analysis of C/C code項(xiàng)目地址https://gitcode.com/gh_mirrors/cpp/cppcheck點(diǎn)擊查看免費(fèi)下載導(dǎo)讀本文圍繞 cppcheck 的unpreciseMathCall檢查器消息 IDunpreciseMathCall展開它是 cppcheck 內(nèi)置的數(shù)值計(jì)算風(fēng)格級(jí)Style檢查用于識(shí)別exp(x) - 1、log(1 x)、1 - erf(x)這類用普通函數(shù)組合代替專用數(shù)學(xué)函數(shù)的寫法——在參數(shù)接近臨界值時(shí)這些寫法會(huì)因浮點(diǎn)消減cancellation而丟失大部分有效數(shù)字。讀完本文你將掌握該檢查器識(shí)別的三類精確模式、其底層 AST 匹配原理、觸發(fā)所需的標(biāo)準(zhǔn)與命令行開關(guān)以及如何用expm1()/log1p()/erfc()寫出數(shù)值上更穩(wěn)定的代碼并能通過源碼與測(cè)試用例驗(yàn)證每一個(gè)結(jié)論。檢查器概覽unpreciseMathCall屬于 cppcheck 的 man/checkers/unpreciseMathCall.md 檢查器文檔族其標(biāo)準(zhǔn)屬性如下屬性取值消息MessageExpression exp(x) - 1 can be replaced by expm1(x) to avoid loss of precision.類別CategoryCode Quality代碼質(zhì)量嚴(yán)重級(jí)別SeverityStyle適用語言LanguageC/CCWECWE-758依賴未定義、未指定或?qū)崿F(xiàn)定義行為在 lib/checkfunctions.cpp 的mathfunctionCallWarning(const Token*, const std::string, const std::string)實(shí)現(xiàn)中該消息以Severity::style、CWE758、Certainty::normal三個(gè)參數(shù)上報(bào)說明它是一類確定性的代碼風(fēng)格提示而非誤報(bào)率較高的推斷型警告。問題本質(zhì)為什么exp(x) - 1會(huì)丟失精度文檔 unpreciseMathCall.md 的核心動(dòng)機(jī)只有一條但它是整個(gè)檢查器存在的全部理由對(duì)于很小的xexp(x)的結(jié)果非常接近1此時(shí)計(jì)算exp(x) - 1等價(jià)于兩個(gè)幾乎相等的浮點(diǎn)數(shù)相減這是經(jīng)典的精度損失場(chǎng)景——結(jié)果的絕大多數(shù)有效數(shù)字都會(huì)在這個(gè)過程中消失。以 IEEE 754 雙精度為例double只有約 1517 位十進(jìn)制有效數(shù)字。當(dāng)x 1e-10時(shí)exp(x) ≈ 1.000000000100000000...減去1之后結(jié)果約1e-10但原本蘊(yùn)含在exp(x)尾數(shù)中的關(guān)于x的信息大部分在相減時(shí)被約掉了剩余的有效數(shù)字寥寥無幾。這正是數(shù)值計(jì)算中著名的消減誤差catastrophic cancellation。標(biāo)準(zhǔn)庫為此專門提供了三個(gè)一步到位的函數(shù)它們直接計(jì)算目標(biāo)數(shù)學(xué)結(jié)果內(nèi)部不會(huì)經(jīng)過先算普通函數(shù)、再相減的中間步驟因此在x接近臨界值0或-1時(shí)不會(huì)發(fā)生消減易失精度寫法推薦替代適用場(chǎng)景exp(x) - 1expm1(x)計(jì)算e^x - 1x → 0時(shí)log(1 x)log1p(x)計(jì)算ln(1 x)x → 0時(shí)1 - erf(x)erfc(x)計(jì)算1 - erf(x)即互補(bǔ)誤差函數(shù)x → 0時(shí)這些替代函數(shù)本身就是為在臨界參數(shù)下保持精度而設(shè)計(jì)的從數(shù)學(xué)結(jié)果看兩者完全等價(jià)從浮點(diǎn)實(shí)現(xiàn)看后者數(shù)值穩(wěn)定得多。檢查器識(shí)別的三類精確模式源碼級(jí)unpreciseMathCall并不做模糊的看起來像匹配而是通過 token 模式與 AST 結(jié)構(gòu)雙重校驗(yàn)只認(rèn)定三類精確寫法。核心邏輯位于 lib/checkfunctions.cpp 的CheckFunctionsImpl::checkMathFunctions()if (Token::Match(tok, %num% - erf () Tokenizer::isOneNumber(tok-str()) tok-next()-astOperand2() tok-tokAt(3)) { mathfunctionCallWarning(tok, 1 - erf(x), erfc(x)); } else if (Token::simpleMatch(tok, exp () Token::Match(tok-linkAt(1), ) - %num%) Tokenizer::isOneNumber(tok-linkAt(1)-strAt(2)) tok-linkAt(1)-next()-astOperand1() tok-next()) { mathfunctionCallWarning(tok, exp(x) - 1, expm1(x)); } else if (Token::simpleMatch(tok, log () tok-next()-astOperand2()) { const Token* plus tok-next()-astOperand2(); if (plus-str() ((plus-astOperand1() Tokenizer::isOneNumber(plus-astOperand1()-str())) || (plus-astOperand2() Tokenizer::isOneNumber(plus-astOperand2()-str())))) mathfunctionCallWarning(tok, log(1 x), log1p(x)); }逐條拆解三類模式的識(shí)別條件1 - erf(x)→erfc(x)要求erf左側(cè)是字面量1且通過astOperand2()確認(rèn)減法的右操作數(shù)確實(shí)是erf(...)整個(gè)調(diào)用表達(dá)式而不是erf(x)/2.0之類的復(fù)合表達(dá)式。exp(x) - 1→expm1(x)要求exp(...)的右括號(hào))之后緊跟- 1且astOperand1()確認(rèn)減法的左操作數(shù)是exp(...)調(diào)用本身。log(1 x)→log1p(x)要求log(...)的參數(shù)在 AST 中是加法節(jié)點(diǎn)且的任一操作數(shù)是字面量1即1 x或x 1都算。需要特別強(qiáng)調(diào)的是上報(bào)消息中的exp(x) - 1等字符串是規(guī)范化后的模式文本而非源碼中的原始表達(dá)式。由測(cè)試用例 test/testfunctions.cpp 可見即使源碼寫作exp(3 x*f(a)) - 1報(bào)告消息仍然統(tǒng)一顯示為Expression exp(x) - 1 can be replaced by expm1(x)...這保證了同類問題擁有穩(wěn)定、可檢索的消息文本。觸發(fā)條件風(fēng)格開關(guān)與語言標(biāo)準(zhǔn)門檻unpreciseMathCall不是無條件啟用的lib/checkfunctions.cpp 中有一個(gè)明確的啟用門檻const bool styleC99 mSettings.severity.isEnabled(Severity::style) ((mTokenizer-isC() mSettings.standards.c ! Standards::C89) || (mTokenizer-isCPP() mSettings.standards.cpp ! Standards::CPP03)); if (!styleC99 !printWarnings !mSettings.isPremiumEnabled(wrongmathcall)) return;這意味著同時(shí)滿足兩個(gè)條件才會(huì)執(zhí)行精度類檢查啟用 style 嚴(yán)重級(jí)別需要在命令行傳入--enablestyle或--enableall僅默認(rèn)檢查不會(huì)報(bào)告該問題語言標(biāo)準(zhǔn)達(dá)標(biāo)由于expm1/log1p/erfc是 C99 與 C11 才引入的標(biāo)準(zhǔn)庫函數(shù)因此對(duì) C 代碼要求非 C89 標(biāo)準(zhǔn)對(duì) C 代碼要求非 C03 標(biāo)準(zhǔn)。默認(rèn)情況下 cppcheck 以較新標(biāo)準(zhǔn)分析通常無需額外配置若分析目標(biāo)是老標(biāo)準(zhǔn)項(xiàng)目請(qǐng)確認(rèn)--std設(shè)置沒有把標(biāo)準(zhǔn)壓到 C89/C03。從調(diào)用關(guān)系看checkMathFunctions()在 lib/checkfunctions.h 中聲明遍歷符號(hào)數(shù)據(jù)庫SymbolDatabase中的全部函數(shù)作用域逐 token 掃描屬于 cppcheck 對(duì)cmath系列函數(shù)的專項(xiàng)檢查與wrongmathcall錯(cuò)誤參數(shù)調(diào)用共用同一入口只是各自命中后上報(bào)不同的消息 ID。不誤報(bào)的邊界什么寫法不會(huì)被報(bào)告unpreciseMathCall對(duì)形似但神不似的表達(dá)式刻意保持沉默測(cè)試 test/testfunctions.cpp 給出了兩個(gè)反面用例void foo() { print(2*exp(x) - 1); // 不報(bào)告減法左操作數(shù)不是 exp(...) 調(diào)用本身 print(1 - erf(x)/2.0); // 不報(bào)告erf(...) 不是減法的直接右操作數(shù) }原因在于源碼中的 AST 校驗(yàn)2*exp(x) - 1的減法左操作數(shù)是整個(gè)乘法表達(dá)式2*exp(x)astOperand1()指向乘法節(jié)點(diǎn)而非exp調(diào)用因此不滿足exp(x) - 1的精確形態(tài)1 - erf(x)/2.0同理減法右操作數(shù)是除法表達(dá)式。這一設(shè)計(jì)避免了把碰巧包含- 1或erf的一般表達(dá)式誤報(bào)為精度問題——只有替換后語義完全等價(jià)且確實(shí)能提升精度的寫法才會(huì)收到提示。如何修復(fù)Before / After 實(shí)戰(zhàn)示例文檔 unpreciseMathCall.md 給出了標(biāo)準(zhǔn)的修復(fù)演示這里擴(kuò)充為完整可編譯的示例。有問題的寫法會(huì)被報(bào)告#include cmath void f() { print(exp(3.5) - 1); // - 小參數(shù)時(shí)丟失精度 print(log(1 3.5)); // - 小參數(shù)時(shí)丟失精度 print(1 - erf(3.5)); // - 小參數(shù)時(shí)丟失精度 }修復(fù)后的寫法#include cmath void f() { print(expm1(3.5)); // 等價(jià)于 exp(3.5) - 1數(shù)值穩(wěn)定 print(log1p(3.5)); // 等價(jià)于 log(1 3.5)數(shù)值穩(wěn)定 print(erfc(3.5)); // 等價(jià)于 1 - erf(3.5)數(shù)值穩(wěn)定 }從測(cè)試用例 test/testfunctions.cpp 可以看到檢查器對(duì)1.0這類浮點(diǎn)字面量同樣敏感exp(x) - 1.0、log(1.0 x)、1.0 - erf(x)都會(huì)命中說明匹配基于數(shù)值等于 1而不是恰好寫成字符 1。同時(shí)x 1、x*4 1這類操作數(shù)順序和復(fù)雜實(shí)參如exp(3 x*f(a)) - 1、1 - erf(34*x f(x) - c)也都能被正確識(shí)別覆蓋面比直覺想象的更廣。與其他檢查器的區(qū)分unpreciseMathCall在檢查器文檔中明確關(guān)聯(lián)了 wrongmathcall.md消息 IDwrongmathcall。兩者雖然都針對(duì)數(shù)學(xué)函數(shù)但問題性質(zhì)截然不同維度unpreciseMathCallwrongmathcall問題類型精度損失寫法低效傳入了函數(shù)定義域之外的非法字面量示例exp(x) - 1→expm1(x)log(-2)、fmod(x, 0)嚴(yán)重級(jí)別StyleWarning類別Code QualityCorrectness檢查對(duì)象表達(dá)式形態(tài)AST 結(jié)構(gòu)直接寫在調(diào)用里的數(shù)值字面量從 lib/checkfunctions.cpp 可進(jìn)一步看到wrongmathcall會(huì)核對(duì)log/log10/log2/atan2/fmod/pow等函數(shù)的實(shí)參是否為超界字面量如log的參數(shù) 0、log1p的參數(shù) -1、atan2(0, 0)等僅分析字面量而不做變量值推斷。兩個(gè)檢查器共享checkMathFunctions()的掃描框架但一個(gè)是換種寫法更穩(wěn)一個(gè)是這個(gè)參數(shù)本身就是錯(cuò)的在實(shí)踐中容易混淆需要注意區(qū)分。測(cè)試驗(yàn)證與可復(fù)現(xiàn)命令該檢查器的行為由 cppcheck 單元測(cè)試固化核心用例是test/testfunctions.cpp中的mathfunctionCall_precision()test/testfunctions.cpp。測(cè)試斷言精確到行列號(hào)例如[test.cpp:2:11]: (style) Expression exp(x) - 1 can be replaced by expm1(x) to avoid loss of precision. [unpreciseMathCall] [test.cpp:3:11]: (style) Expression log(1 x) can be replaced by log1p(x) to avoid loss of precision. [unpreciseMathCall] [test.cpp:4:11]: (style) Expression 1 - erf(x) can be replaced by erfc(x) to avoid loss of precision. [unpreciseMathCall]本地復(fù)現(xiàn)同樣簡單將上文有問題的寫法保存為demo.cpp然后運(yùn)行cppcheck --enablestyle demo.cpp只要滿足前文所述的標(biāo)準(zhǔn)門檻默認(rèn)即可即可看到三條以(style)標(biāo)注、攜帶[unpreciseMathCall]消息 ID 的輸出。若想驗(yàn)證修復(fù)后不再報(bào)告將代碼替換為expm1/log1p/erfc版本重新運(yùn)行即可。小結(jié)unpreciseMathCall是 cppcheck 內(nèi)置的、面向數(shù)值穩(wěn)定性的一類風(fēng)格檢查它以 AST 精確匹配exp(x) - 1、log(1 x)、1 - erf(x)三類模式建議改用expm1()、log1p()、erfc()從根源上規(guī)避小參數(shù)下的浮點(diǎn)消減誤差。理解它的啟用條件--enablestyle C99/C11 起與精確匹配邊界2*exp(x) - 1不會(huì)被誤報(bào)可以讓你在 C/C 數(shù)值代碼中系統(tǒng)性地消除這類隱蔽的精度隱患。相關(guān)源碼與測(cè)試均可直接在倉庫中查閱檢查器實(shí)現(xiàn)、上報(bào)邏輯、單元測(cè)試 以及 關(guān)聯(lián)檢查器文檔。贊分享開發(fā)工具靜態(tài)分析代碼質(zhì)量質(zhì)量保障【免費(fèi)下載鏈接】cppcheckstatic analysis of C/C code項(xiàng)目地址https://gitcode.com/gh_mirrors/cpp/cppcheck點(diǎn)擊查看免費(fèi)下載相關(guān)推薦cppcheck floatConversionOverflow 檢查器詳解浮點(diǎn)轉(zhuǎn)整數(shù)的未定義行為檢測(cè)cppcheck floatConversionOverflow 檢查器詳解浮點(diǎn)轉(zhuǎn)整數(shù)的未定義行為檢測(cè) 導(dǎo)讀 本文深入講解 cppcheck 的 floatC開發(fā)工具靜態(tài)分析代碼質(zhì)量質(zhì)量保障cppcheck 檢查器詳解memsetClassFloat —— 對(duì)含浮點(diǎn)成員結(jié)構(gòu)體使用 memset 的可移植性隱患cppcheck 檢查器詳解memsetClassFloat —— 對(duì)含浮點(diǎn)成員結(jié)構(gòu)體使用 memset 的可移植性隱患 memsetClassFloat 是開發(fā)工具靜態(tài)分析代碼質(zhì)量質(zhì)量保障cppcheck redundantCopyLocalConst 檢查器詳解消除 const 局部變量的無謂拷貝cppcheck redundantCopyLocalConst 檢查器詳解消除 const 局部變量的無謂拷貝 導(dǎo)讀 redundantCopyLocalC開發(fā)工具靜態(tài)分析代碼質(zhì)量質(zhì)量保障上一篇如何高效使用 MikuTools40實(shí)用工具的完整使用教程下一篇如何在Chrome與Firefox中安裝Browserpass Legacy5分鐘快速上手教程創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考