軟件bug的規律

jopen 8年前發布 | 10K 次閱讀 程序員

英文原文:The Law Of Software Bugs

一般地,在程序代碼中犯錯比不犯錯更容易

不管看起來是否違反直覺,我覺得存在一種半正式的論據。在同樣心態下,也有一些有趣的推論。


首先,為什么改錯比犯錯更難,尤其在軟件工程中,更是如此?

讓我們盡量從熵、混沌注1和有序的視角來分析。

下圖是有序:

軟件bug的規律

下圖是混沌:

軟件bug的規律

我們大概都了解混沌和有序,通常不必費力思考原因的一個現象就是,我們更容易給系統創造混沌而非有序。一條狗很快就能把你的臥室搞亂,而整理好房間卻需要視角和某種精神上的努力。再舉個更為尖刻的例子:一家建筑公司傾其全力造好一棟建筑需要數年時間,而把該棟建筑破壞掉,只需要一個人,只要他足夠多的炸藥,瞬間就能完成。

創造比破壞難,原因在于系統內部所具有的混沌狀態,要遠遠大于有序狀態。當然,這取決于有序的定義和界定,但是,如果和無序狀態比起來,任何合理的定義都會讓你得到更少的、可能有序的狀態。考慮一下臥室里的襪子:為了創造混沌,你只需肆意甩出去,不必多想。狗狗就能做到。但是,為了將來需要時能快速找到,整理這一雙雙襪子、并按照某種方式擺放好,很明顯就需要更多努力,還有某種認知和思考。

因此,問題就變成了可能狀態(混沌)的數量,和相應使其有意義的一個子集(有序)。


那么,程序代碼中的錯誤是怎么回事?bug 究竟是怎樣產生的?

當你的代碼未能按照預期執行時,就說明存在 bug 了。(讓我們把可計算性的話題放到另一篇博文,因為它比較復雜。某個任務在合理時間內,可能有解決方案,也可能沒有,不過,我的意圖是,存在可做的先驗。我們簡化一下問題:程序未能按照意圖運行。)

比如,你需要一個函數,它返回兩個 int 值的平均數(整數)。首先,你可能這樣寫代碼:

int avg (int a, int b)
{ return (a + b) / 2;
}

正確嗎?

錯。它沒有考慮可能存在的整數溢出,而兩個整數的平均數應該總是介于 a 和 b 之間。換句話說,avg () 函數被期望總是返回一個(四舍五入)的結果,且不會溢出。

讓我們修復一下。一個新手程序員可能會按照如下方式「修復」:

int avg (int a, int b)
{ return a / 2 + b / 2;
}

當然,又錯了。

在某個互聯網論壇上,當被問到根據其最初形式修復 avg () 時,總是會給出建議,要求把參數的數據類型轉換為浮點,再把結果四舍五入為 int 型:再說一次,糟糕想法會產生奇怪的結果,即使有最好的結果,也一定沒有效率。兩個 int 值的平均數計算竟然用到了浮點,你是認真的嗎?

我們再考慮一種更好的方式。這一次,顯然修復成了:

int avg (int a, int b)
{ return a + (b - a) / 2;
}

正確嗎?

不正確!嘗試 avg (INT_MIN, INT_MAX) 的情況,你就明白錯在哪里了。事實上,如果 a 或者 b是負數,(b-a) 甚至在達到接近邊界值 INT_MIN 和 INT_MAX 之前就相對容易溢出了。

最后的解決方案呢?如果我沒有遺漏什么的話,我相信下面的代碼就是最好的了:

int avg (int a, int b)
{ if ((a < ) != (b < )) return (a + b) / 2; else return a + (b - a) / 2;
}

首先,上面代碼中有趣的地方較為明顯了,軟件 bug 常常是沒有考慮到、漠視或忘記了某些東西,也有可能是壓根兒不知道。

在每行源代碼里,存在著一定數量的對象,這些對象又包含了特定屬性。在 avg () 例子中,對象有:a, b, 2, +, /, result。在 bug 讓你損失數十億美元的云服務或太空船之前,需要定位并修復它,你應該查看表達式/語句所涉及到的所有對象,并思考你掌握它們的所有知識。比如,a 和 b 是 int 型,因此它們必定總是在某個限制內(現實往往比較殘酷);+ 操作符在大多數語言中因為沒有警告溢出而臭名昭著;除法容易搞砸 int 型,也禁止被零除。幸虧我們多多少少習慣了。

在現實生活中,它太簡單了,以致于我們忘記、漠視或對上面提到的知識點一無所知。對于既定的一項任務,只存在很少的、形式上正確的方案,通常,有一種方案最簡短,但是還有很大可能隱藏著 bug。如果你在寫代碼時,腦子里需要記住 10 個因素,只要你無視了其中一個,就會導致 bug 的產生。我們的大腦還不夠完美:它傾向于忘掉那些不應該忘掉的東西。


我們對找到 avg () 的最佳解決方案的探求,應該已經提醒你注意臥室里、那條淘氣的狗狗了。犯錯很容易,而找到正確的方案,相對難一些。

為了采取相對正式的方法,假定你被安排了一項先驗的計算任務,那么你需要決定為編碼投入多少努力。有兩個極端:零努力、無窮多的努力。

對于零努力的情形,如果你無論如何都要做點兒事情的話,那就是在機器上扔一個隨機的位元流注2,觀察運行情況。這種做法屬于十足的混沌:用隨機序列來解決某個特定問題的可能性微乎其微,就好像把一堆球扔到臺球桌、還期望它們能夠組成三角形。各種可能性太多,導致正確的可能性幾乎不會發生。

對于無窮多努力的情形,為了找到最便捷、最牛逼的解決方案而投入了足夠多的努力,當然你會找到的。這是有序,很難。

不是很難,而總是更難。

當你從零努力開始朝著積極方向前進時,隨著你考慮越來越多的因素,也就有了越來越少的可能性,其中,很多可能性將是錯誤的、有 bug 的,只有很少一部分是正確的。朝著極限前進,會增加你編寫正確解決方案的機會。

總結一下:

找到某項任務的正確解決方案,需要記住并應用一定數量的條件。漠視或不了解其中一種,將導致 bug 的產生。和掌握并應用所有相對知識相比,引入 bug 常常更容易,因為某些相關知識沒被應用的情形有很多,而考慮了所有因素的情形少之又少。

觀點被證明了嗎?我想說,證明過了。


更多的思考。首先,修復軟件 bug 不等同于找到了正確的解決方案:需要額外的時間,首先甄別出代碼中讓人不爽的地方,然后思考并找到更好的解決方案。

但是,找到正確的解決方案,要比修改代碼 bug(程序錯誤的規律)投入更多的努力,這個事實加劇了狀況的惡化。因此 bug 對項目造成的損失就等于:按照錯誤方案編寫代碼所消耗的時間,加上后續尋找和甄別錯誤的時間,再加上團隊/公司所遭受的道德或財務上的損失。這樣:

推論 1:修改 bug 比起一開始就不引入 bug,需要更多的努力(常常不成比例)。

bug 存在一個至關重要的、且常被忽略的副作用,那就是一段時間以后,新員工總體優勢的下降,并因此引起了 bug 產生率的進一步增加,等等。產品最初的原型階段,常常由一組技術底子好的優秀程序員完成,一旦過了這個階段,就進入了相對乏味和修復 bug 的工作階段。此時,團隊開始進入成本削減的螺旋上升和產品質量下降的階段。新員工競爭力相對低些(因為沒有人愿意做無聊的工作和修復 bug),因此帶來了更多的 bug 等等。過了一段時間,如果管理方面不做一些非常規測量(也從沒做過),軟件團隊就達到了某種均衡,50% 的時間花在了修復代碼上(正如這項研究所展示的,盡管研究沒有提到最初的原型階段,不過,有趣的地方在于,那時候產生的 bug 也不多),更多的早期優秀員工/創始人離職、或不再寫代碼了。所有這些因素一定導致了一種結果,催生了第一個 bug 和糟糕的最初設計。

推論 2:低競爭優勢的團隊成員給團隊工作造成的危害,遠遠大于團隊競爭優勢缺乏和薪水降低所帶來的危害。

最終,就像某個物理系統進入混沌狀態、且處于「無人值守」的境地一樣,軟件項目步入了雜亂無章,典型的軟件血汗工廠,充斥著得意忘形的團隊、不可維護且 bug 林立的代碼庫、財務損失、了然無趣。到達混沌比遠離混沌要容易得多。這就到了:

推論 3:在產品生命周期的各個階段,只有有意識的協作努力,才能使 bug 最少,也才能為享受編程和削減開發成本提供保障。因此,任何編程「方法論」,如果在最后沒有論證出減少的 bug 產生率,那么,不管它說得多么天花亂墜,都是扯淡。

推論 3.1:如果你獨自編程,那也挺好。只要確保別把 bug 和 「讓人厭煩」的工作悄悄帶入你的項目,就沒有雇傭更多人的必要。


譯文: 《軟件 bug 的規律 》 臘八粥

注釋

  1. 混沌理論(Chaos theory)是關于非線性系統在一定參數條件下展現分岔(bifurcation)、周期運動與非周期運動相互糾纏,以至于通向某種非周期有序運動的理論。https://zh.wikipedia.org/wiki/%E6%B7%B7%E6%B2%8C%E7%90%86%E8%AE%BA 
  2. 一個位元流(bitstream 或 bit stream)是一個位元的序列。一個字節流則是一個字節的序列,一般來說一個字節是 8 個位元。也可以被視為是一種特殊的位元流。https://zh.wikipedia.org/wiki/%E4%BD%8D%E5%85%83%E6%B5%81 

來自: www.labazhou.net

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!