對於C++程式設計的老鳥來說,有時候他們喜歡把一些東西按照編譯器的工作原理進行改寫,以便提高程式碼的執行效率。這麼做確實高明,也能體現出程式設計師的水平,但是這麼做也是有風險的。因為有時候你可能會因為一些簡單的筆誤,而造成非常難以察覺的錯誤。本文就給出了類似的例子。
這個 Bug 出現在 MySQL 原始碼中。
錯誤程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
static int rr_cmp(uchar *a,uchar *b) { if (a[0] != b[0]) return (int) a[0] - (int) b[0]; if (a[1] != b[1]) return (int) a[1] - (int) b[1]; if (a[2] != b[2]) return (int) a[2] - (int) b[2]; if (a[3] != b[3]) return (int) a[3] - (int) b[3]; if (a[4] != b[4]) return (int) a[4] - (int) b[4]; if (a[5] != b[5]) return (int) a[1] - (int) b[5]; <<<<==== if (a[6] != b[6]) return (int) a[6] - (int) b[6]; return (int) a[7] - (int) b[7]; } |
說明:
這是一個在對程式碼段進行拷貝貼上時出現的典型錯誤。程式設計師很可能是把“if (a[1] != b[1]) (int) a[1] – (int) b[1];” 這段程式碼拷貝了好幾遍(然後手動改陣列下標),用來實現一個迴圈。不過程式設計師忘記把其中某一行的陣列下標 1 改成 5。結果就是函式有時候能返回正確的值(,有的時候則不行),這種錯誤是很難偵測的。事實上這個錯誤的確很難捕捉,在我們用 PVS-Studio 掃描 MySQL 原始碼之前,所有其他的測試都沒能發現這個錯誤。
正確的程式碼:
1 2 |
if (a[5] != b[5]) return (int) a[5] - (int) b[5]; |
儘管之前的程式碼看上去整潔易讀,但是程式設計師還是很有可能漏看這個錯誤。因為這個程式碼塊的內部結構很相似,所以你本能地會一掃而過,而不會特別集中注意力去閱讀程式碼。
之所以把程式碼寫成這樣,很可能是程式設計師想盡可能地優化程式碼。他(或她)想手動“展開迴圈”(來進行優化)。不過我想在這兒可不是個好主意。
首先,我很懷疑程式設計師是不是真的能通過這種方法達到效果。要知道,現代編譯器已經相當智慧了,如果真的能優化程式效能,(編譯器)自動就會完成展開迴圈的優化。
其次,由於嘗試進行優化卻造成了程式碼中出現 bug。如果程式設計師一開始能老老實實寫一個簡單迴圈,那麼犯錯誤的機率就會降低很多。
我建議把這個方法寫成這樣:
1 2 3 4 5 6 7 8 9 |
static int rr_cmp(uchar *a,uchar *b) { for (size_t i = 0; i < 7; ++i) { if (a[i] != b[i]) return a[i] - b[i]; } return a[7] - b[7]; } |
這種寫法有兩個優勢:
- 這個函式更容易閱讀和理解。
- 編寫程式碼時,降低犯錯機率。
至於效能方面,我敢說這個版本不會比之前寫得很長的那個版本慢。
這個推薦的方法實際上表達了下面的意思:程式碼要簡單易讀。簡單的程式碼通常即是正確的程式碼。不要去做編譯器的工作——例如,(手動)展開迴圈。編譯器很明確知道自己該做什麼,並不需要你的幫助。手動程式碼優化工作只針對某些特定的關鍵程式碼,而且只在分析器已經確認這些程式碼是瓶頸以後,才可能恰當。
這個錯誤由 PVS-Studio 分析工具捕獲。錯誤文字:V525程式碼包含一組相似的程式碼塊。
打賞支援我翻譯更多好文章,謝謝!
打賞譯者
打賞支援我翻譯更多好文章,謝謝!