在這篇文章中,我將討論一個很少有人想到的問題。各種過程的計算機模擬正變得越來越普遍。這項技術非常棒,因為它使我們能夠節省時間和材料,而這些時間和材料本會用於無謂的化學、生物、物理和其他型別的實驗。機翼剖面流動的計算機模擬模型可以顯著減少在真實風洞中測試的原型數量。如今,人們越來越信任數值實驗。然而,在計算機模擬的勝利光環下,沒有人注意到軟體複雜性不斷增長的問題。人們將計算機和計算機程式僅僅視為獲取必要結果的手段。我擔心很少有人知道並關心軟體規模的增長會導致軟體錯誤數量的非線性增長。將計算機僅僅視為一個大計算器來使用是危險的。所以,這就是我的想法——我需要與他人分享這個想法。
起初,我打算將這篇文章命名為“如果程式設計師不能開發藥物,為什麼醫生可以編寫程式?”。設想一位程式設計師——他不被允許開發和製備藥物。原因顯而易見:他沒有必要的教育。然而,在程式設計方面並非如此簡單。看起來,一位學會程式設計的醫生,將理所當然地成為一名成功的、有用的程式設計師——尤其考慮到掌握或多或少可接受的程式設計技能比有機化學和藥物製備原理容易得多。
這裡藏著一個陷阱。計算機實驗需要像真實的實驗一樣細緻。實驗室工作人員被教導在實驗後清洗試管並確保它們是無菌的。但很少有人真正關心某個陣列意外地未初始化的問題。
程式設計師們都很清楚,軟體越複雜,出現的錯誤就越複雜和難以捉摸。換句話說,我指的是程式碼規模增長伴隨的錯誤數量的非線性增長。執行化學或任何其他科學計算的程式遠非簡單,不是嗎?這就是危險所在。醫生兼程式設計師犯錯是沒關係的。任何程式設計師,無論多麼熟練,都會時不時犯錯。不 OK 的是,人們越來越信任這些結果。你計算完就繼續做別的事情。
那些從事程式設計作為職業活動的人知道這種方法的危險性。他們知道什麼是未定義行為,以及程式如何僅僅“看起來”就能正常工作。有無數的文章和書籍解釋瞭如何正確開發單元測試並確保計算的正確性。
這就是程式設計師的世界。但我擔心,化學家/物理學家/醫生的世界並非如此。他們從不編寫複雜的程式——他們根本不是那樣思考的。他們使用計算機,就像它只是一個大計算器一樣。這個比喻是一位讀者提出的。讓我在這裡完整地引用他的評論,以便講英語的讀者也能在文章翻譯後瞭解它。
我可以根據自己的經驗告訴你一些關於這個主題的事情。雖然我是一名專業程式設計師,但我實際上來自一個物理學家家庭,並且接受過物理學教育。在我必須選擇進入哪所大學的時候,血統的呼喚比我對 IT 光明未來的信念更強大。所以,我進入了一所物理大學,該大學在我家鄉下諾夫哥羅德的一個大型研究所的監督下,實際上是一個“幼兒園”。瞭解該主題的人會立即猜到我指的是哪個研究所和哪所大學。
在那裡學習期間,我自然而然地成為程式設計(尤其是物理建模的數學方法)方面最好的學生之一。與此同時,我也發現了以下幾點:
1. 物理學家傾向於將計算機視為一個大型多功能計算器,允許你繪製 Eta 與 Theta 的圖,而 Gamma 趨於無窮大。正如人們可以自然預期的那樣,他們主要對圖本身感興趣,而不是程式。
2. 作為第一個事實的後果,程式設計師不被視為一種職業。程式設計師只是那個知道如何使用大計算器繪製所需圖表的人。他們根本不在乎它是怎麼完成的。抱歉,你說什麼?靜態分析?版本控制?哦,拜託,各位!C++ 是程式設計師的語言;物理學家用 FORTRAN 編寫!
3. 作為前一個事實的後果,任何打算致力於編寫物理建模程式的人,即使是通用的,即使是極其複雜的,也只是大計算器的一個附屬物。他甚至不是一個人——只是一種……順便說一句,不僅我被物理學家這樣對待(畢竟我只是一個普通學生)——甚至研究所裡最好的計算機建模專家,也是在我們大學教授計算方法課程的人,當我為寫學期論文而轉向他作為我的論文導師時,他幾乎直截了當地對我說:“他們會鄙視你,所以要準備好忍受。”
我不想忍受,畢業後離開了計算機建模領域,轉向程式設計師不被視為“次等人”的領域。我希望這個例子能幫助你理解為什麼像在相對大型(約 20-30 名開發人員)計算機建模專案上引入靜態分析這樣的倡議是徒勞的。可能根本就沒有人知道這是什麼。即使團隊中碰巧有這樣一個人,他們也很可能會把他排擠掉,因為他們不需要你那些時髦的程式設計師花哨的東西。“我們沒有它們已經有一百年了——將來還會繼續。”
對於那些還沒感到厭煩的人,再講一個故事。我的父親,儘管已經退休,仍然在我所在的下諾夫哥羅德一家非常大的國防工程企業工作(這是我們城市最大的企業之一,也是全國最大的企業之一;同樣,那些瞭解情況的人會猜到 ;))。他一生都在用 FORTRAN 程式設計。他開始程式設計時還在使用穿孔卡片。我不怪他沒有學習 C++。對他來說,十年前就已經太晚了——但他現在仍然做得很好。然而,這家企業有某些安全措施,其中 2/3 的員工或多或少都在從事程式設計工作。
1. 完全沒有網際網路。如果你需要文獻——你去圖書館。Stack Overflow?那是什麼?如果你需要傳送電子郵件,你必須向老闆提交書面請求,解釋你想傳送給誰以及為何傳送。只有少數選定的人才能“憑收據”使用網際網路。謝天謝地,他們至少有一個內部網路。
2. 你的電腦上沒有管理員許可權。也許這個限制對普通白領來說有意義,但我無法想象一個程式設計師在這種情況下感到滿意。
3.(與主題無關;只是說明。)你甚至不能帶一部帶有整合攝像頭的手機(如今還有沒有攝像頭的嗎?)。
結果是,即使是年輕的員工也用 FORTRAN 編寫程式碼,而真正有技能的程式設計師卻很少。我對此很確定,因為我指導了一個 25 歲的年輕人,我父親曾推薦他作為一名有前途的程式設計師。
我的結論是:他們還停留在 80 年代。即使有相當不錯的薪水,我也不想去那裡。
這只是來自知識精英階層的兩個例子。我無意貶低任何人——他們做得足夠好,但看著我父親有時不得不與之搏鬥的“風車”,我的心在流血。(謝天謝地,我最近說服他開始使用 git 了。)在一個擁有百萬行程式碼的專案中沒有 OOP,沒有靜態分析——什麼都沒有。
這僅僅是因為人類在自己不擅長的領域往往非常保守。
Ilja Mayzus. 原始評論。
這個故事的核心是將計算機視為大計算器的理念。在這種情況下,你不需要比它的弟弟——袖珍計算器——瞭解更多。而且,它在許多領域實際上就是這樣使用的。我們暫時岔開話題,看看物理學界。讓我們看看另一個理論是如何被證實的。為此,我將再次引用布萊恩·格林(Bryan Greene)的書《優雅的宇宙:弦論、隱藏的維度以及對終極理論的追求》[1] 的大段摘錄。
我們都擠在莫里森(Morrison)的辦公室裡,我和他一起使用他的電腦。阿斯平沃爾(Aspinwall)告訴莫里森如何將他的程式顯示在螢幕上,並向我們展示了所需的輸入的精確格式。莫里森妥善格式化了我們前一晚生成的計算結果,一切準備就緒。
我們進行的具體計算,粗略地說,是確定某個粒子種類——弦的特定振動模式——在穿越我們整個秋天都在識別的某個 Calabi-Yau 分量宇宙時產生的質量。我們希望,遵循前面討論的策略,這個質量將與在空間撕裂的翻轉過渡(flop transition)產生的 Calabi-Yau 形狀上進行的類似計算完全一致。後者是相對容易的計算,我們幾周前已經完成了;結果是 3,在我們使用的特定單位下。由於我們現在正在計算機上進行所謂的映象計算,我們期望得到一個非常接近 3 但不完全是 3 的結果,例如 3.000001 或 2.999999,微小的差異來自於舍入誤差。
莫里森坐在電腦前,手指懸停在回車鍵上。隨著緊張氣氛的加劇,他說:“開始了”,並啟動了計算。幾秒鐘後,計算機返回了結果:8.999999。我的心沉了下去。難道空間撕裂的翻轉過渡會破壞映象關係,這可能表明它們實際上不會發生?然而,幾乎立刻,我們都意識到肯定有什麼不對勁。如果由兩種形狀產生的物理學之間存在真正的差異,那麼計算機計算得到一個如此接近整數的結果的可能性極小。如果我們的想法錯了,那麼沒有任何理由期望得到比隨機數字集合更好的結果。我們得到了一個錯誤的結果,但這個結果可能表明我們只是犯了一個簡單的算術錯誤。阿斯平沃爾和我走到黑板前,片刻之後就找到了我們的錯誤:我們在幾周前進行的“更簡單的”計算中漏掉了一個因子 3;真實結果是 9。因此,計算機結果正是我們想要的。
當然,事後的吻合只具有微弱的說服力。當你知道你想要的結果時,很容易找出一種方法來得到它。我們需要做另一個例子。由於已經編寫了所有必要的計算機程式碼,這並不難。我們小心翼翼地在上面的 Calabi-Yau 形狀上計算了另一個粒子質量,這次確保沒有錯誤。我們發現結果是:12。我們又一次擠在電腦旁,啟動了計算。幾秒鐘後,它返回了 11.999999。吻合。我們已經證明了所謂的映象就是映象,因此空間撕裂的翻轉過渡是弦理論物理學的一部分。
聽到這個訊息,我從椅子上跳起來,在辦公室裡狂奔以示勝利。莫里森在電腦後面得意洋洋。然而,阿斯平沃爾的反應卻截然不同。“這很好,但我早就知道會這樣,”他平靜地說。“我的啤酒呢?”
我真心相信他們是天才。但讓我們暫時設想一下,如果是一些普通學生用這種方法來計算積分呢?我想程式設計師不會認真對待。如果程式直接生成了 3 呢?這個錯誤會被當作最終證據嗎?我認為在他們自己或他們的科學家同事重新檢查後會澄清。儘管如此,“理想化真空中的球形程式設計師”對此事實感到極度恐懼。
現實就是這樣。不僅僅是個人電腦這樣使用——集群系統也被用於科學計算。最可怕的是,人們信任程式產生的結果。未來我們將處理更多此類計算,軟體錯誤的代價也將越來越高。
難道是時候改變一些東西了?
是的,沒有人能阻止我給自己貼創可貼;我想我可以在感冒時推薦一些藥物。但僅此而已。我不能鑽牙或開處方。
難道你認為建立軟體系統的開發人員,其責任超出特定範圍,也應該確認他們的技能,這不合理嗎?
我知道存在各種認證。但現在我說的是另一件事。認證旨在確保程式程式碼符合某些標準。它以間接的方式部分地防止了粗製濫造。但是,認證是嚴格要求的地方範圍相當狹窄。它顯然不能涵蓋所有以及任何地方,在這些地方粗心使用大計算器可能會造成很大危害。
我想你們中的許多人會覺得我的擔憂過於抽象。因此,我建議研究一些現實生活中的例子。有一個開源軟體包 Trans-Proteomic Pipeline (TPP),旨在解決生物學中的各種任務。毫無疑問,它被使用著——由其開發人員和一些第三方組織使用。我相信其中的任何錯誤都已經是潛在的問題。它有錯誤嗎?是的,它有;而且還在不斷出現。我們在一年前檢查了這個專案,並在部落格文章“Trans-Proteomic Pipeline (TPP) 專案分析”中進行了報告。
自從那時以來有什麼變化嗎?沒有。專案正在繼續開發並積累新的錯誤。大計算器理念佔了上風。開發人員沒有編寫一個擁有最少錯誤數量的高質量專案。他們只是在解決自己的任務;否則,他們就會對去年的文章做出某種反應,並考慮引入一些靜態分析工具。我不是說他們必須選擇 PVS-Studio;還有許多其他靜態程式碼分析器。重點是,他們負責任的應用正在繼續累積最瑣碎的錯誤。讓我們看看他們又發現了什麼新鮮的錯誤。
在上一篇文章中,我提到了錯誤的迴圈條件。新軟體包版本中也有。
|
|
PVS-Studio 的診斷訊息:V521 使用 ',' 運算子的此類表示式很危險。請確保表示式正確。spectrastpeaklist.cpp 504
在檢查“i != this->m_bins->end(), j != other->m_bins->end()”時,逗號前的表示式沒有任何檢查。',' 運算子用於按從左到右的順序執行其左右兩邊的表示式,並**返回右邊表示式的值**。正確的檢查應該是這樣的:
i != this->m_bins->end() && j != other->m_bins->end()
相同的缺陷也可以在以下片段中找到:
這個錯誤不會導致輸出錯誤的計算結果——它會導致崩潰,這反而更好。但是,不提及這些錯誤會很奇怪。
|
|
PVS-Studio 的診斷訊息:V522 可能會發生空指標“pepIndx”的解引用。asapcgidisplay2main.cxx 534
相同的缺陷也可以在以下片段中找到:
|
|
在此程式碼中,分析器一次捕獲了兩個未清除的陣列。
V530 函式 'empty' 的返回值需要被利用。tag.cxx 72
V530 函式 'empty' 的返回值需要被利用。tag.cxx 73
您應該呼叫 clear() 函式而不是 empty()。
|
|
PVS-Studio 的診斷訊息:V603 物件已被建立但未使用。如果要呼叫建構函式,應使用“this->ExperimentCycleRecord::ExperimentCycleRecord(....)”。mascotconverter.cxx 101
ExperimentCycleRecord() 建構函式沒有達到預期目的,它沒有初始化任何東西。開發者可能是個優秀的化學家,但如果他不知道如何正確使用 C++ 語言,他使用未初始化記憶體進行的計算就毫無價值。這就像使用髒試管一樣。
該行“ExperimentCycleRecord(0,0,0,True,False);”建立了一個臨時物件,該物件將在之後被銷燬,而不是呼叫另一個建構函式。這種錯誤模式在文章“不要在未知的水域中徘徊。第一部分”中有詳細討論。
相同的缺陷也可以在以下片段中找到:
|
|
PVS-Studio 的診斷訊息:V628 可能該行被不正確地註釋掉了,從而改變了程式的執行邏輯。interprophetmain.cxx 175
在 'if' 運算子之後,幾行執行一些操作的程式碼被註釋掉了。結果,程式邏輯與預期大相徑庭。程式設計師不希望在執行條件後進行任何操作。相反,'if' 運算子會影響下面的程式碼。因此,測試的輸出不僅取決於“testType!=NO_TEST”條件,還取決於“getIsInteractiveMode()”條件。也就是說,測試可能什麼也不測試。因此,我強烈建議不要完全依賴一種測試方法(例如 TDD)。
印刷錯誤無處不在。如果因為這樣的錯誤,你在遊戲中獲得的生命點比應得的少,那還不是最糟糕的。但是,在計算化學反應時,不正確的資料意味著什麼?
|
|
PVS-Studio 的診斷訊息:V519 變數 'data->ratio[0]' 被連續兩次賦值。可能這是一個錯誤。請檢查行:130, 131。asapcgidisplay2main.cxx 131
同一個變數被錯誤地賦予了兩個不同的值。正確的程式碼是這樣的:
|
|
這段程式碼隨後被複制並貼上到程式的其他部分。
正確比較有符號和無符號值需要一些技巧。普通計算器不處理無符號值,但 C++ 語言卻可以。
|
|
PVS-Studio 的診斷訊息:V555 表示式“ppw_ref.size() - have_cluster > 0”將按“ppw_ref.size() != have_cluster”工作。proteinprophet.cpp 6767
程式設計師想要執行“ppw_ref.size() > have_cluster”檢查。但他卻得到了完全不同的結果。
為了更清楚地說明,讓我們假設我們有一個 32 位的 'size_t' 型別。假設函式“ppw_ref.size()”返回 10,而變數 have_cluster 等於 15。函式 ppw_ref.size() 返回無符號型別 'size_t'。根據 C++ 規則,在執行減法之前,減法運算子的右側運算元也必須是 'size_t' 型別。目前沒問題:左邊是 10u,右邊是 15u。
這是減法
10u - 15u
這就是問題所在。那些 C++ 規則告訴我們,兩個無符號變數相減的結果也必須是無符號的。
這意味著 10u - 15u = FFFFFFFBu。正如你所知,4294967291 大於 0。
大計算器暴動成功了。編寫一個正確的理論演算法只是工作的一半。你還需要編寫正確的程式碼。
以下是類似的錯誤:
|
|
PVS-Studio 的診斷訊息:V547 表示式“b + tau >= 0”始終為真。無符號型別值始終 >= 0。spectrastpeaklist.cpp 2058
正如你所看到的,變數 'tau' 的取值範圍是 [-75, 75]。為了避免陣列越界,使用了檢查 b + tau >= 0。我猜你已經明白了,這個檢查是無效的。變數 'b' 具有 'unsigned' 修飾符。這意味著“b + tau”表示式的結果也是無符號的。而無符號值總是大於或等於 0。
|
|
PVS-Studio 的診斷訊息:V612 迴圈內無條件 'return'。residuemass.cxx 1442
迴圈內有一個 'return' 運算子,它在任何情況下都會被呼叫。迴圈最多隻能執行一次,然後函式終止。這裡要麼是印刷錯誤,要麼是 'return' 運算子前缺少了某個條件。
|
|
PVS-Studio 的診斷訊息:V636 表示式“used_count_ / rts_.size()”已從“int”型別隱式轉換為“double”型別。考慮使用顯式型別轉換以避免丟失小數部分。例如:double A = (double)(X) / Y;。rtcalculator.cxx 6406
由於函式返回 double 型別的值,我傾向於認為以下情況是合理的。
當變數 'used_count_' 被賦值為 5,而函式 rts_.size() 返回 7 時,近似結果為 0.714。然而,在這種情況下,函式 getUsedForGradientRate() 將返回 0。
變數 'used_count_' 是 'int' 型別。rts_.size() 函式也返回一個 'int' 值。發生整數除法,結果顯而易見:為零。然後零被隱式轉換為 double,但這無關緊要。
為了修復這個缺陷,程式碼應該重寫如下:
return static_cast<double>(used_count_) / rts_.size();
其他類似缺陷:
函式 setPepMaxProb() 包含幾個大小相似的塊。在這個片段中,你可以感受到複製貼上技術的特殊味道。使用它自然會導致錯誤。我不得不“大幅”刪減示例文字。錯誤在刪減後的程式碼中非常明顯,但在原始程式碼中幾乎看不見。是的,這是對靜態分析工具的推廣,尤其是對 PVS-Studio 的推廣。
|
|
V525 包含相似程式碼塊的程式碼。請檢查行 4664、4690、4716、4743、4770 中的項 'max3'、'max4'、'max5'、'max6'、'max6'。proteinprophet.cpp 4664
PVS-Studio 的診斷訊息:V525 包含相似程式碼塊的程式碼。請檢查行 4664、4690、4716、4743、4770 中的項 'max3'、'max4'、'max5'、'max6'、'max6'。proteinprophet.cpp 4664
不幸的是,V525 診斷會產生許多誤報,因此被歸類為三級警告。但如果克服了懶惰,研究這類警告,你可能會發現許多類似的優秀錯誤。
|
|
PVS-Studio 的診斷訊息:V614 可能未初始化指標 'pScanIndex'。sqt2xml.cxx 476
如果函式 rampOpenFile() 返回 NULL,此程式可能會在最後崩潰。這還不算關鍵,但很令人不快。
這是另一個可能未初始化的變數:
|
|
PVS-Studio 的診斷訊息:V599 即使“DiscriminantFunction”類包含虛擬函式,也沒有出現虛解構函式。discrimvalmixturedistr.cxx 206
許多類繼承自 DiscriminantFunction 類。例如,DiscrimValMixtureDistr 類就是如此。它的解構函式會釋放記憶體,因此,非常希望呼叫它。不幸的是,DiscriminantFunction 類的解構函式沒有宣告為虛擬函式——並由此產生所有後果。
有許多小缺陷,它們不會造成嚴重後果,但仍然很不 pleasant 存在於你的程式碼中。還有一些奇怪的片段,但我無法確定它們是否不正確。這裡有一個:
|
|
PVS-Studio 的診斷訊息:V607 無屬主表示式“done_[charge]”。mixturemodel.cxx 1558
這是什麼?不完整的程式碼?或者程式設計師只是想指出,如果“done_[charge] < 0”條件為真,則不應執行任何操作?
這裡有一個不正確的記憶體釋放方式。不太可能造成嚴重後果,但程式碼確實有問題。
|
|
PVS-Studio 的診斷訊息:V611 使用“new T[]”運算子分配的記憶體使用“delete”運算子釋放。請檢查此程式碼。最好使用“delete [] pepString;”。pepxfield.cxx 1023
正確的做法是編寫“delete [] pepString”。還有許多其他類似的缺陷:
還有一個不正確的“--”運算子實現。它似乎沒有任何地方使用,否則這個錯誤會很快暴露出來。
|
|
PVS-Studio 的診斷訊息:V524 奇怪的是“--”函式的體與“++”函式的體完全等價。charindexedvector.hpp 81
運算子“--”和“++”的實現方式相同。它們一定是複製貼上的。
我們就此打住吧。這一切都不是很有趣,而且文章已經夠大了。一如既往,我敦促開發人員不要僅限於修復提到的缺陷。請下載並自行使用 PVS-Studio 檢查該專案。我可能遺漏了很多錯誤。我們甚至可以為您提供免費的註冊金鑰。
不幸的是,這篇文章顯得有些混亂。作者到底想說什麼?我將嘗試以非常簡練的形式重複我想與您分享的想法。
那麼,我們該怎麼辦?
首先,我希望您能意識到這個問題,並告知您相關領域的同事。程式設計師很早就知道,軟體複雜性的增長以及大型專案中低階錯誤可能輕易地導致巨大危害。另一方面,那些將程式設計和計算機僅僅視為工具的人不知道這一點,也不去想它。因此,我們需要引起他們對這個問題的關注。
這裡有一個類比。想象一個人拿到一根棍子,開始獵殺一些動物。棍子在他的手中逐漸變成石斧,然後是劍,最後是槍。但他仍然只是用它來擊暈野兔。不僅這種使用武器的方式效率低下;現在也危險得多(他可能會意外地射傷自己或他的同伴)。“程式設計師”部落的獵人很快就適應了這些變化。其餘的人沒有時間這樣做——他們忙於獵殺野兔。畢竟,都是關於野兔的。我們需要告訴這些人,他們必須學習,無論他們是否喜歡。這將改善每個人的生活。四處揮舞你的槍是沒有用的。