在 GitHub 上對編程語言與軟體質量的一個大規模研究
![](https://linuxstory.org/wp-content/uploads/2024/06/180042t9ilo3tizks3qtpp-800x400.jpg)
編程語言對軟體質量的影響是什麼?這個問題在很長一段時間內成為一個引起了大量辯論的主題。在這項研究中,我們從 GitHub 上收集了大量的數據(728 個項目,6300 萬行源代碼,29000 位作者,150 萬個提交,17 種編程語言),嘗試在這個問題上提供一些實證。這個還算比較大的樣本數量允許我們去使用一個混合的方法,結合多種可視化的回歸模型和文本分析,去研究語言特性的影響,比如,在軟體質量上,靜態與動態類型和允許混淆與不允許混淆的類型。通過從不同的方法作三角測量研究(LCTT 譯註:一種測量研究的方法),並且去控制引起混淆的因素,比如,團隊大小、項目大小和項目歷史,我們的報告顯示,語言設計確實(對很多方面)有很大的影響,但是,在軟體質量方面,語言的影響是非常有限的。最明顯的似乎是,不允許混淆的類型比允許混淆的類型要稍微好一些,並且,在函數式語言中,靜態類型也比動態類型好一些。值得注意的是,這些由語言設計所引起的輕微影響,絕大多數是由過程因素所主導的,比如,項目大小、團隊大小和提交數量。但是,我們需要提示讀者,即便是這些不起眼的輕微影響,也是由其它的無形的過程因素所造成的,例如,對某些函數類型、以及不允許類型混淆的靜態語言的偏愛。
1 序言
在給定的編程語言是否是「適合這個工作的正確工具」的討論期間,緊接著又發生了多種辯論。雖然一些辯論出現了帶有宗教般狂熱的色彩,但是大部分人都一致認為,編程語言的選擇能夠對編碼過程和由此生成的結果都有影響。
主張強靜態類型的人,傾向於認為靜態方法能夠在早期捕獲到缺陷;他們認為,一點點的預防勝過大量的矯正。動態類型擁護者主張,保守的靜態類型檢查無論怎樣都是非常浪費開發者資源的,並且,最好是依賴強動態類型檢查來捕獲錯誤類型。然而,這些辯論,大多數都是「紙上談兵」,只靠「傳說中」的證據去支持。
這些「傳說」也許並不是沒有道理的;考慮到影響軟體工程結果的大量其它因素,獲取這種經驗性的證據支持是一項極具挑戰性的任務,比如,代碼質量、語言特徵,以及應用領域。比如軟體質量,考慮到它有大量的眾所周知的影響因素,比如,代碼數量, 6 團隊大小, 2 和年齡/熟練程度。 9
受控實驗是檢驗語言選擇在面對如此令人氣餒的混淆影響時的一種方法,然而,由於成本的原因,這種研究通常會引入一種它們自己的混淆,也就是說,限制了範圍。在這種研究中,完整的任務是必須要受限制的,並且不能去模擬 真實的世界 中的開發。這裡有幾個最近的這種大學本科生使用的研究,或者,通過一個實驗因素去比較靜態或動態類型的語言。 7 , 12 , 15
幸運的是,現在我們可以基於大量的真實世界中的軟體項目去研究這些問題。GitHub 包含了多種語言的大量的項目,並且在大小、年齡、和開發者數量上有很大的差別。每個項目的倉庫都提供一個詳細的記錄,包含貢獻歷史、項目大小、作者身份以及缺陷修復。然後,我們使用多種工具去研究語言特性對缺陷發生的影響。對我們的研究方法的最佳描述應該是「混合方法」,或者是三角測量法; 5 我們使用文本分析、聚簇和可視化去證實和支持量化回歸研究的結果。這個以經驗為根據的方法,幫助我們去了解編程語言對軟體質量的具體影響,因為,他們是被開發者非正式使用的。
2 方法
我們的方法是軟體工程中典型的大範圍觀察研究法。我們首先大量的使用自動化方法,從幾種數據源採集數據。然後使用預構建的統計分析模型對數據進行過濾和清洗。過濾器的選擇是由一系列的因素共同驅動的,這些因素包括我們研究的問題的本質、數據質量和認為最適合這項統計分析研究的數據。尤其是,GitHub 包含了由大量的編程語言所寫的非常多的項目。對於這項研究,我們花費大量的精力專註於收集那些用大多數的主流編程語言寫的流行項目的數據。我們選擇合適的方法來評估計數數據上的影響因素。
2.1 數據收集
我們選擇了 GitHub 上的排名前 19 的編程語言。剔除了 CSS、Shell 腳本、和 Vim 腳本,因為它們不是通用的編程語言。我們包含了 Typescript,它是 JavaScript 的超集。然後,對每個被研究的編程語言,我們檢索出以它為主要編程語言的前 50 個項目。我們總共分析了 17 種不同的語言,共計 850 個項目。
我們的編程語言和項目的數據是從 GitHub Archive 中提取的,這是一個記錄所有活躍的公共 GitHub 項目的資料庫。它記錄了 18 種不同的 GitHub 事件,包括新提交、fork 事件、PR(拉取請求)、開發者信息和以每小時為基礎的所有開源 GitHub 項目的問題跟蹤。打包後的數據上傳到 Google BigQuery 提供的互動式數據分析介面上。
識別編程語言排名榜單
我們基於它們的主要編程語言分類合計項目。然後,我們選擇大多數的項目進行進一步分析,如 表 1 所示。一個項目可能使用多種編程語言;將它確定成單一的編程語言是很困難的。Github Archive 保存的信息是從 GitHub Linguist 上採集的,它使用項目倉庫中源文件的擴展名來確定項目的發布語言是什麼。源文件中使用數量最多的編程語言被確定為這個項目的 主要編程語言。
表 1 每個編程語言排名前三的項目
檢索流行的項目
對於每個選定的編程語言,我們先根據項目所使用的主要編程語言來選出項目,然後根據每個項目的相關 星 的數量排出項目的流行度。 星 的數量表示了有多少人主動表達對這個項目感興趣,並且它是流行度的一個合適的代表指標。因此,在 C 語言中排名前三的項目是 linux、git、php-src;而對於 C++,它們則是 node-webkit、phantomjs、mongo ;對於 Java,它們則是 storm、elasticsearch、ActionBarSherlock 。每個編程語言,我們各選了 50 個項目。
為確保每個項目有足夠長的開發歷史,我們剔除了少於 28 個提交的項目(28 是候選項目的第一個四分位值數)。這樣我們還剩下 728 個項目。表 1 展示了每個編程語言的前三個項目。
檢索項目演進歷史
對於 728 個項目中的每一個項目,我們下載了它們的非合併提交、提交記錄、作者數據、作者使用 git 的名字。我們從每個文件的添加和刪除的行數中計算代碼改動和每個提交的修改文件數量。我們以每個提交中修改的文件的擴展名所代表的編程語言,來檢索出所使用的編程語言(一個提交可能有多個編程語言標籤)。對於每個提交,我們通過它的提交日期減去這個項目的第一個提交的日期,來計算它的 提交年齡 。我們也計算其它的項目相關的統計數據,包括項目的最大提交年齡和開發者總數,用於我們的回歸分析模型的控制變數,它在第三節中會討論到。我們通過在提交記錄中搜索與錯誤相關的關鍵字,比如,error
、bug
、fix
、issue
、mistake
、incorrect
、fault
、defect
、flaw
,來識別 bug 修復提交。這一點與以前的研究類似。 18
表 2 匯總了我們的數據集。因為一個項目可能使用多個編程語言,表的第二列展示了使用某種編程語言的項目的總數量。我們進一步排除了項目中該編程語言少於 20 個提交的那些編程語言。因為 20 是每個編程語言的每個項目的提交總數的第一個四分位值。例如,我們在 C 語言中共找到 220 項目的提交數量多於 20 個。這確保了每個「編程語言 – 項目」對有足夠的活躍度。
表 2 研究主題
總而言之,我們研究了最近 18 年以來,用了 17 種編程語言開發的,總共 728 個項目。總共包括了 29,000 個不同的開發者,157 萬個提交,和 564,625 個 bug 修復提交。
2.2 語言分類
我們基於影響語言質量的幾種編程語言特性定義了語言類別, 7 , 8 , 12 ,如 表 3 所示。
編程範式 表示項目是以命令方式、腳本方式、還是函數語言所寫的。在本文的下面部分,我們分別使用 命令 和 腳本 這兩個術語去代表命令方式和腳本方式。
表 3. 語言分類的不同類型
類型檢查 代表靜態或者動態類型。在靜態類型語言中,在編譯時進行類型檢查,並且變數名是綁定到一個值和一個類型的。另外,(包含變數的)表達式是根據運行時,它們可能產生的值所符合的類型來分類的。在動態類型語言中,類型檢查發生在運行時。因此,在動態類型語言中,它可能出現在同一個程序中,一個變數名可能會綁定到不同類型的對象上的情形。
隱式類型轉換 允許一個類型為 T1 的操作數,作為另一個不同的類型 T2 來訪問,而無需進行顯式的類型轉換。這樣的隱式類型轉換在一些情況下可能會帶來類型混淆,尤其是當它表示一個明確的 T1 類型的操作數時,把它再作為另一個不同的 T2 類型的情況下。因為,並不是所有的隱式類型轉換都會立即出現問題,通過我們識別出的允許進行隱式類型轉換的所有編程語言中,可能發生隱式類型轉換混淆的例子來展示我們的定義。例如,在像 Perl、 JavaScript、CoffeeScript 這樣的編程語言中,一個字元和一個數字相加是允許的(比如,"5" + 2
結果是 "52"
)。但是在 Php 中,相同的操作,結果是 7
。像這種操作在一些編程語言中是不允許的,比如 Java 和 Python,因為,它們不允許隱式轉換。在強數據類型的 C 和 C++ 中,這種操作的結果是不可預料的,例如,int x; float y; y=3.5; x=y
;是合法的 C 代碼,並且對於 x
和 y
其結果是不同的值,具體是哪一個值,取決於含義,這可能在後面會產生問題。 a 在 Objective-C
中,數據類型 id 是一個通用對象指針,它可以被用於任何數據類型的對象,而不管分類是什麼。 b 像這種通用數據類型提供了很好的靈活性,它可能導致隱式的類型轉換,並且也會出現不可預料的結果。 c 因此,我們根據它的編譯器是否 允許 或者 不允許 如上所述的隱式類型轉換,對編程語言進行分類;而不允許隱式類型轉換的編程語言,會顯式檢測類型混淆,並報告類型不匹配的錯誤。
不允許隱式類型轉換的編程語言,使用一個類型判斷演算法,比如,Hindley 10 和 Milner, 17 或者,在運行時上使用一個動態類型檢查器,可以在一個編譯器(比如,使用 Java)中判斷靜態類型的結果。相比之下,一個類型混淆可能會悄無聲息地發生,因為,它可能因為沒有檢測到,也可能是沒有報告出來。無論是哪種方式,允許隱式類型轉換在提供了靈活性的同時,最終也可能會出現很難確定原因的錯誤。為了簡單起見,我們將用 隱含 代表允許隱式類型轉換的編程語言,而不允許隱式類型轉換的語言,我們用 明確 代表。
內存分類 表示是否要求開發者去管理內存。儘管 Objective-C 遵循了一個混合模式,我們仍將它放在非管理的分類中來對待,因為,我們在它的代碼庫中觀察到很多的內存錯誤,在第 3 節的 RQ4 中會討論到。
請注意,我們之所以使用這種方式對編程語言來分類和研究,是因為,這種方式在一個「真實的世界」中被大量的開發人員非正式使用。例如,TypeScript 被有意地分到靜態編程語言的分類中,它不允許隱式類型轉換。然而,在實踐中,我們注意到,開發者經常(有 50% 的變數,並且跨 TypeScript —— 在我們的數據集中使用的項目)使用 any
類型,這是一個籠統的聯合類型,並且,因此在實踐中,TypeScript 允許動態地、隱式類型轉換。為減少混淆,我們從我們的編程語言分類和相關的模型中排除了 TypeScript(查看 表 3 和 7)。
2.3 識別項目領域
我們基於編程語言的特性和功能,使用一個自動加手動的混合技術,將研究的項目分類到不同的領域。在 GitHub 上,項目使用 project descriptions
和 README
文件來描述它們的特性。我們使用一種文檔主題生成模型(Latent Dirichlet Allocation,縮寫為:LDA) 3 去分析這些文本。提供一組文檔給它,LDA 將生成不同的關鍵字,然後來識別可能的主題。對於每個文檔,LDA 也估算每個主題分配的文檔的概率。
我們檢測到 30 個不同的領域(換句話說,就是主題),並且評估了每個項目從屬於每個領域的概率。因為,這些自動檢測的領域包含了幾個具體項目的關鍵字,例如,facebook,很難去界定它的底層的常用功能。為了給每個領域分配一個有意義的名字,我們手動檢查了 30 個與項目名字無關的用於識別領域的領域識別關鍵字。我們手動重命名了所有的 30 個自動檢測的領域,並且找出了以下六個領域的大多數的項目:應用程序、資料庫、代碼分析、中間件、庫,和框架。我們也找出了不符合以上任何一個領域的一些項目,因此,我們把這個領域籠統地標記為 其它 。隨後,我們研究組的另一名成員檢查和確認了這種項目領域分類的方式。表 4 匯總了這個過程識別到的領域結果。
表 4 領域特徵
2.4 bug 分類
儘管修復軟體 bug 時,開發者經常會在提交日誌中留下關於這個 bug 的原始的重要信息;例如,為什麼會產生 bug,以及怎麼去修復 bug。我們利用很多信息去分類 bug,與 Tan 的 et al 類似。 13 , 24
首先,我們基於 bug 的 原因 和 影響 進行分類。 原因 進一步分解為不相關的錯誤子類:演算法方面的、並發方面的、內存方面的、普通編程錯誤,和未知的。bug 的 影響 也分成四個不相關的子類:安全、性能、失敗、和其它的未知類。因此,每個 bug 修復提交也包含原因和影響的類型。表 5 展示了描述的每個 bug 分類。這個類別分別在兩個階段中被執行:
表 5 bug 分類和在整個數據集中的描述
(1) 關鍵字搜索 我們隨機選擇了 10% 的 bug 修複信息,並且使用一個基於關鍵字的搜索技術去對它們進行自動化分類,作為可能的 bug 類型。我們對這兩種類型(原因和影響)分別使用這個注釋。我們選擇了一個限定的關鍵字和習慣用語集,如 表 5 所展示的。像這種限定的關鍵字和習慣用語集可以幫我們降低誤報。
(2) 監督分類 我們使用前面步驟中的有注釋的 bug 修復日誌作為訓練數據,為監督學習分類技術,通過測試數據來矯正,去對剩餘的 bug 修複信息進行分類。我們首先轉換每個 bug 修複信息為一個詞袋(LCTT 譯註:bag-of-words,一種信息檢索模型)。然後,刪除在所有的 bug 修複信息中僅出現過一次的詞。這樣減少了具體項目的關鍵字。我們也使用標準的自然語言處理技術來解決這個問題。最終,我們使用支持向量機(LCTT 譯註:Support Vector Machine,縮寫為 SVM,在機器學習領域中,一種有監督的學習演算法)去對測試數據進行分類。
為精確評估 bug 分類器,我們手動注釋了 180 個隨機選擇的 bug 修復,平均分布在所有的分類中。然後,我們比較手動注釋的數據集在自動分類器中的結果。最終處理後的,表現出的精確度是可接受的,性能方面的精確度最低,是 70%,並發錯誤方面的精確度最高,是 100%,平均是 84%。再次運行,精確度從低到高是 69% 到 91%,平均精確度還是 84%。
我們的 bug 分類的結果展示在 表 5 中。大多數缺陷的原因都與普通編程錯誤相關。這個結果並不意外,因為,在這個分類中涉及了大量的編程錯誤,比如,類型錯誤、輸入錯誤、編寫錯誤、等等。我們的技術並不能將在任何(原因或影響)分類中佔比為 1.4% 的 bug 修複信息再次進行分類;我們將它歸類為未知。
2.5 統計方法
我們使用回歸模型對軟體項目相關的其它因素中的有缺陷的提交數量進行了建模。所有的模型使用 負二項回歸 (縮寫為 NBR)(LCTT 譯註:一種回歸分析模型) 去對項目屬性計數進行建模,比如,提交數量。NBR 是一個廣義的線性模型,用於對非負整數進行響應建模。 4
在我們的模型中,我們對每個項目的編程語言,控制幾個可能影響最終結果的因素。因此,在我們的回歸分析中,每個(語言/項目)對是一個行,並且可以視為來自流行的開源項目中的樣本。我們依據變數計數進行對象轉換,以使變數保持穩定,並且提升了模型的適用度。 4 我們通過使用 AIC 和 Vuong 對非嵌套模型的測試比較來驗證它們。
去檢查那些過度的多重共線性(LCTT 譯註:多重共線性是指,在線性回歸模型中解釋變數之間由於存在精確相關關係或高度相關關係而使模型估計失真或難以估計準確。)並不是一個問題,我們在所有的模型中使用一個保守的最大值 5,去計算每個依賴的變數的膨脹因子的方差。 4 我們通過對每個模型的殘差和槓桿圖進行視覺檢查來移除高槓桿點,找出庫克距離(LCTT 譯註:一個統計學術語,用於診斷回歸分析中是否存在異常數據)的分離值和最大值。
我們利用 效果 ,或者 差異 ,編碼到我們的研究中,以提高編程語言回歸係數的表現。 4 加權的效果代碼允許我們將每種編程語言與所有編程語言的效果進行比較,同時彌補了跨項目使用編程語言的不均勻性。 23 去測試兩種變數因素之間的聯繫,我們使用一個獨立的卡方檢驗(LCTT 譯註:Chi-square,一種統計學上的假設檢驗方法)測試。 14 在證實一個依賴之後,我們使用 Cramer 的 V,它是與一個 r × c
等價的正常數據的 phi(φ)
係數,去建立一個效果數據。
3 結果
我們從簡單明了的問題開始,它非常直接地解決了人們堅信的一些核心問題,即:
問題 1:一些編程語言相比其它語言來說更易於出現缺陷嗎?
我們使用了回歸分析模型,去比較每個編程語言對所有編程語言缺陷數量平均值的影響,以及對缺陷修復提交的影響(查看 表 6)。
表 6. 一些語言的缺陷要少於其它語言
我們包括了一些變數,作為對明確影響反應的控制因子。項目 年齡 也包括在內,因為,越老的項目生成的缺陷修複數量越大。 提交 數量也會對項目反應有輕微的影響。另外,從事該項目的 開發人員 的數量和項目的原始 大小 ,都會隨著項目的活躍而增長。
上述模型中估算係數的大小和符號(LCTT 譯註:指 「+」或者「-」)與結果的預測因子有關。初始的四種變數是控制變數,並且,我們對這些變數對最終結果的影響不感興趣,只是說它們都是積極的和有意義的。語言變數是指示變數,是每個項目的變化因子,該因子將每種編程語言與所有項目的編程語言的加權平均值進行比較。編程語言係數可以大體上分為三類。第一類是,那些在統計學上無關緊要的係數,並且在建模過程中這些係數不能從 0 中區分出來。這些編程語言的表現與平均值相似,或者它們也可能有更大的方差。剩餘的係數是非常明顯的,要麼是正的,要麼是負的。對於那些正的係數,我們猜測可能與這個編程語言有大量的缺陷修復相關。這些語言包括 C、C++、Objective-C、Php,以及 Python。所有的有一個負的係數的編程語言,比如 Clojure、Haskell、Ruby,和 Scala,暗示這些語言的缺陷修復提交可能小於平均值。
應該注意的是,雖然,從統計學的角度觀察到編程語言與缺陷之間有明顯的聯繫,但是,大家不要過高估計編程語言對於缺陷的影響,因為,這種影響效應是非常小的。異常分析的結果顯示,這種影響小於總異常的 1%。
我們可以這樣去理解模型的係數,它代表一個預測因子在所有其它預測因子保持不變的情況下,這個預測因子一個 單位 的變化,所反應出的預期的響應的對數變化;換句話說,對於一個係數 β i ,在 β i 中一個單位的變化,產生一個預期的 e β i 響應的變化。對於可變因子,這個預期的變化是與所有編程語言的平均值進行比較。因此,如果對於一定數量的提交,用一個處於平均值的編程語言開發的特定項目有四個缺陷提交,那麼,如果選擇使用 C++ 來開發,意味著我們預計應該有一個額外的(LCTT 譯註:相對於平均值 4,多 1 個)缺陷提交,因為 e 0.18 × 4 = 4.79。對於相同的項目,如果選擇使用 Haskell 來開發,意味著我們預計應該少一個(LCTT 譯註:同上,相對於平均值 4)缺陷提交。因為, e −0.26 × 4 = 3.08。預測的精確度取決於剩餘的其它因子都保持不變,除了那些微不足道的項目之外,所有的這些都是一個極具挑戰性的命題。所有觀察性研究都面臨類似的局限性;我們將在第 5 節中詳細解決這些事情。
結論 1:一些編程語言相比其它編程語言有更高的缺陷相關度,不過,影響非常小。
在這篇文章的剩餘部分,我們會在基本結論的基礎上詳細闡述,通過考慮不同種類的應用程序、缺陷、和編程語言,可以進一步深入了解編程語言和缺陷傾向之間的關係。
軟體 bug 通常落進兩種寬泛的分類中:
- 特定領域的 bug :特定於項目功能,並且不依賴於底層編程語言。
- 普通 bug :大多數的普通 bug 是天生的,並且與項目功能無關,比如,輸入錯誤,並發錯誤、等等。
因此,在一個項目中,應用程序領域和編程語言相互作用可能會影響缺陷的數量,這一結論被認為是合理的。因為一些編程語言被認為在一些任務上相比其它語言表現更突出,例如,C 對於低級別的(底層)工作,或者,Java 對於用戶應用程序,對於編程語言的一個不合適的選擇,可能會帶來更多的缺陷。為研究這種情況,我們將理想化地忽略領域特定的 bug,因為,普通 bug 更依賴於編程語言的特性。但是,因為一個領域特定的 bug 也可能出現在一個普通的編程錯誤中,這兩者是很難區分的。一個可能的變通辦法是在控制領域的同時去研究編程語言。從統計的角度來看,雖然,使用 17 種編程語言跨 7 個領域,在給定的樣本數量中,理解大量的術語將是一個極大的挑戰。
鑒於這種情況,我們首先考慮在一個項目中測試領域和編程語言使用之間的依賴關係,獨立使用一個 卡方檢驗 測試。在 119 個單元中,是 46 個,也就是說是 39%,它在我們設定的保守值 5 以上,它太高了。這個數字不能超過 20%,應該低於 5。 14 我們在這裡包含了完整有值; d 但是,通過 Cramer 的 V 測試的值是 0.191,是低相關度的,表明任何編程語言和領域之間的相關度是非常小的,並且,在回歸模型中包含領域並不會產生有意義的結果。
去解決這種情況的一個選擇是,去移除編程語言,或者混合領域,但是,我們現有的數據沒有進行完全挑選。或者,我們混合編程語言;這個選擇導致一個相關但略有不同的問題。
問題 2: 哪些編程語言特性與缺陷相關?
我們按編程語言類別聚合它們,而不是考慮單獨的編程語言,正如在第 2.2 節所描述的那樣,然後去分析與缺陷的關係。總體上說,這些屬性中的每一個都將編程語言按照在上下文中經常討論的錯誤、用戶辯論驅動、或者按以前工作主題來劃分的。因此,單獨的屬性是高度相關的,我們創建了六個模型因子,將所有的單獨因子綜合到我們的研究中。然後,我們對六個不同的因子對缺陷數量的影響進行建模,同時控制我們在 問題 1 節中使用的模型中的相同的基本協變數(LCTT 譯註:協變數是指在實驗中不能被人為操縱的獨立變數)。
關於使用的編程語言(在前面的 表 6中),我們使用跨所有語言類的平均反應來比較編程語言 類 。這個模型在 表 7 中表達了出來。很明顯,Script-Dynamic-Explicit-Managed
類有最小的量級係數。這個係數是微不足道的,換句話說,對這個係數的 Z 校驗 (LCTT 譯註:統計學上的一種平均值差異校驗的方法) 並不能把它從 0 中區分出來。鑒於標準錯誤的量級,我們可以假設,在這個類別中的編程語言的行為是非常接近於所有編程語言行為的平均值。我們可以通過使用 Proc-Static-Implicit-Unmanaged
作為基本級並用於處理,或者使用基本級來虛假編碼比較每個語言類,來證明這一點。在這種情況下,Script-Dynamic-Explicit-Managed
是明顯不同於 p = 0.00044 的。注意,雖然我們在這是選擇了不同的編碼方法,影響了係數和 Z 值,這個方法和所有其它的方面都是一樣的。當我們改變了編碼,我們調整係數去反應我們希望生成的對比。 4 將其它類的編程語言與總體平均數進行比較,Proc-Static-Implicit-Unmanaged
類編程語言更容易引起缺陷。這意味著與其它過程類編程語言相比,隱式類型轉換或者管理內存會導致更多的缺陷傾向。
表 7. 函數式語言與缺陷的關聯度和其它類語言相比要低,而過程類語言則大於或接近於平均值。
在腳本類編程語言中,我們觀察到類似於允許與不允許隱式類型轉換的編程語言之間的關係,它們提供的一些證據表明,隱式類型轉換(與顯式類型轉換相比)才是導致這種差異的原因,而不是內存管理。鑒於各種因素之間的相關性,我們並不能得出這個結論。但是,當它們與平均值進行比較時,作為一個組,那些不允許隱式類型轉換的編程語言出現錯誤的傾向更低一些,而那些出現錯誤傾向更高的編程語言,出現錯誤的機率則相對更高。在函數式編程語言中靜態和動態類型之間的差異也很明顯。
函數式語言作為一個組展示了與平均值的很明顯的差異。靜態類型語言的係數要小很多,但是函數式語言類都有同樣的標準錯誤。函數式靜態編程語言出現錯誤的傾向要小於函數式動態編程語言,這是一個強有力的證據,儘管如此,Z 校驗僅僅是檢驗係數是否能從 0 中區分出來。為了強化這個推論,我們使用處理編碼,重新編碼了上面的模型,並且觀察到,Functional-Static-Explicit-Managed
編程語言類的錯誤傾向是明顯小於 Functional-Dynamic-Explicit-Managed
編程語言類的 p = 0.034。
與編程語言和缺陷一樣,編程語言類與缺陷之間關係的影響是非常小的。解釋類編程語言的這種差異也是相似的,雖然很小,解釋類編程語言的這種差異小於 1%。
我們現在重新回到應用領域這個問題。應用領域是否與語言類相互影響?怎麼選擇?例如,一個函數化編程語言,對特定的領域有一定的優勢?與上面一樣,對於這些因素和項目領域之間的關係做一個卡方檢驗,它的值是 99.05, df = 30, p = 2.622e–09,我們拒絕無意義假設,Cramer 的 V 產生的值是 0.133,表示一個弱關聯。因此,雖然領域和編程語言之間有一些關聯,但在這裡應用領域和編程語言類之間僅僅是一個非常弱的關聯。
結論 2:在編程語言類與缺陷之間有一個很小但是很明顯的關係。函數式語言與過程式或者腳本式語言相比,缺陷要少。
這個結論有些不太令人滿意的地方,因為,我們並沒有一個強有力的證據去證明,在一個項目中編程語言或者語言類和應用領域之間的關聯性。一個替代方法是,基於全部的編程語言和應用領域,忽略項目和缺陷總數,而去查看相同的數據。因為,這樣不再產生不相關的樣本,我們沒有從統計學的角度去嘗試分析它,而是使用一個描述式的、基於可視化的方法。
我們定義了 缺陷傾向 作為 bug 修復提交與每語言每領域總提交的比率。圖 1 使用了一個熱力圖展示了應用領域與編程語言之間的相互作用,從亮到暗表示缺陷傾向在增加。我們研究了哪些編程語言因素影響了跨多種語言寫的項目的缺陷修復提交。它引出了下面的研究問題:
圖 1. 編程語言的缺陷傾向與應用領域之間的相互作用。對於一個給定的領域(列底部),熱力圖中的每個格子表示了一個編程語言的缺陷傾向(行頭部)。「整體」列表示一個編程語言基於所有領域的缺陷傾向。用白色十字線標記的格子代表一個 null 值,換句話說,就是在那個格子里沒有符合的提交。
問題 3: 編程語言的錯誤傾向是否取決於應用領域?
為了回答這個問題,我們首先在我們的回歸模型中,以高槓桿點過濾掉認為是異常的項目,這種方法在這裡是必要的,儘管這是一個非統計學的方法,一些關係可能影響可視化。例如,我們找到一個簡單的項目,Google 的 v8,一個 JavaScript 項目,負責中間件中的所有錯誤。這對我們來說是一個驚喜,因為 JavaScript 通常不用於中間件。這個模式一直在其它應用領域中不停地重複著,因此,我們過濾出的項目的缺陷度都低於 10% 和高於 90%。這個結果在 圖 1 中。
我們看到在這個熱力圖中僅有一個很小的差異,正如在問題 1 節中看到的那樣,這個結果僅表示編程語言固有的錯誤傾向。為驗證這個推論,我們測量了編程語言對每個應用領域和對全部應用領域的缺陷傾向。對於除了資料庫以外的全部領域,關聯性都是正向的,並且 p 值是有意義的(<0.01)。因此,關於缺陷傾向,在每個領域的語言排序與全部領域的語言排序是基本相同的。
結論 3: 應用領域和編程語言缺陷傾向之間總體上沒有聯繫。
我們證明了不同的語言產生了大量的缺陷,並且,這個關係不僅與特定的語言相關,也適用於一般的語言類;然而,我們發現,項目類型並不能在一定程度上協調這種關係。現在,我們轉變我們的注意力到反應分類上,我想去了解,編程語言與特定種類的缺陷之間有什麼聯繫,以及這種關係怎麼與我們觀察到的更普通的關係去比較。我們將缺陷分為不同的類別,如 表 5 所描述的那樣,然後提出以下的問題:
問題 4:編程語言與 bug 分類之間有什麼關係?
我們使用了一個類似於問題 3 中所用的方法,去了解編程語言與 bug 分類之間的關係。首先,我們研究了 bug 分類和編程語言類之間的關係。一個熱力圖(圖 2)展示了在編程語言類和 bug 類型之上的總缺陷數。去理解 bug 分類和語言之間的相互作用,我們對每個類別使用一個 NBR 回歸模型。對於每個模型,我們使用了與問題 1 中相同的控制因素,以及使用加權效應編碼後的語言,去預測缺陷修復提交。
圖 2. bug 類別與編程語言類之間的關係。每個格子表示每語言類(行頭部)每 bug 類別(列底部)的 bug 修復提交佔全部 bug 修復提交的百分比。這個值是按列規範化的。
結果和編程語言的方差分析值展示在 表 8 中。每個模型的整體異常是非常小的,並且對於特定的缺陷類型,通過語言所展示的比例在大多數類別中的量級是類似的。我們解釋這種關係為,編程語言對於特定的 bug 類別的影響要大於總體的影響。儘管我們結論概括了全部的類別,但是,在接下來的一節中,我們對 表 5 中反應出來的 bug 數較多的 bug 類別做進一步研究。
表 8. 雖然編程語言對缺陷的影響因缺陷類別而不同,但是,編程語言對特定的類別的影響要大於一般的類別。
編程錯誤 普通的編程錯誤占所有 bug 修復提交的 88.53% 左右,並且在所有的編程語言類中都有。因此,回歸分析給出了一個與問題 1 中類似的結論(查看 表 6)。所有的編程語言都會導致這種編程錯誤,比如,處理錯誤、定義錯誤、輸入錯誤、等等。
內存錯誤 內存錯誤占所有 bug 修復提交的 5.44%。熱力圖 圖 2 證明了在 Proc-Static-Implicit-Unmanaged
類和內存錯誤之間存在著非常緊密的聯繫。非管理內存的編程語言出現內存 bug,這是預料之中的。表 8 也證明了這一點,例如,C、C++、和 Objective-C 引發了很多的內存錯誤。在管理內存的語言中 Java 引發了更多的內存錯誤,儘管它少於非管理內存的編程語言。雖然 Java 自己做內存回收,但是,它出現內存泄露一點也不奇怪,因為對象引用經常阻止內存回收。 11 在我們的數據中,Java 的所有內存錯誤中,28.89% 是內存泄漏造成的。就數量而言,編程語言中內存缺陷相比其它類別的 原因 造成的影響要大很多。
並發錯誤 在總的 bug 修復提交中,並發錯誤相關的修復提交占 1.99%。熱力圖顯示,Proc-Static-Implicit-Unmanaged
是主要的錯誤類型。在這種錯誤中,C 和 C++ 分別占 19.15% 和 7.89%,並且它們分布在各個項目中。
屬於 Static-Strong-Managed
語言類的編程語言都被證實處於熱力圖中的暗區中,普通的靜態語言相比其它語言產生更多的並發錯誤。在動態語言中,僅僅有 Erlang 有更多的並發錯誤傾向,或許與使用這種語言開發的並發應用程序非常多有關係。同樣地,在 表 8 中的負的係數表明,用諸如 Ruby 和 `Php 這樣的動態編程語言寫的項目,並發錯誤要少一些。請注意,某些語言,如 JavaScript、CoffeeScript 和 TypeScript 是不支持並發的,在傳統的慣例中,雖然 Php 具有有限的並發支持(取決於它的實現)。這些語言在我們的數據中引入了虛假的 0,因此,在 表 8 中這些語言的並發模型的係數,不能像其它的語言那樣去解釋。因為存在這些虛假的 0,所以在這個模型中所有語言的平均數非常小,它可能影響係數的大小,因此,她們給 w.r.t. 一個平均數,但是,這並不影響他們之間的相對關係,因為我們只關注它們的相對關係。
基於 bug 修復消息中高頻詞的文本分析表明,大多數的並發錯誤發生在一個條件爭用、死鎖、或者不正確的同步上,正如上面表中所示。遍歷所有語言,條件爭用是並發錯誤出現最多的原因,例如,在 Go 中占 92%。在 Go 中條件爭用錯誤的改進,或許是因為使用了一個爭用檢測工具幫助開發者去定位爭用。同步錯誤主要與消息傳遞介面(MPI)或者共享內存操作(SHM)相關。Erlang 和 Go 對線程間通訊使用 MPI e ,這就是為什麼這兩種語言沒有發生任何 SHM 相關的錯誤的原因,比如共享鎖、互斥鎖等等。相比之下,為線程間通訊使用早期的 SHM 技術的語言寫的項目,就可能存在鎖相關的錯誤。
安全和其它衝突錯誤 在所有的 bug 修復提交中,與 衝突 錯誤相關的提交佔了 7.33% 左右。其中,Erlang、C++、Python 與安全相關的錯誤要高於平均值(表 8)。Clojure 項目相關的安全錯誤較少(圖 2)。從熱力圖上我們也可以看出來,靜態語言一般更易於發生失敗和性能錯誤,緊隨其後的是 Functional-Dynamic-Explicit-Managed
語言,比如 Erlang。對異常結果的分析表明,編程語言與衝突失敗密切相關。雖然安全錯誤在這個類別中是弱相關的,與殘差相比,解釋類語言的差異仍然比較大。
結論 4: 缺陷類型與編程語言強相關;一些缺陷類型比如內存錯誤和並發錯誤也取決於早期的語言(所使用的技術)。對於特定類別,編程語言所引起的錯誤比整體更多。
4. 相關工作
在編程語言比較之前做的工作分為以下三類:
(1) 受控實驗
對於一個給定的任務,開發者使用不同的語言進行編程時受到監視。研究然後比較結果,比如,開發成果和代碼質量。Hanenberg 7 通過開發一個解析程序,對 48 位程序員監視了 27 小時,去比較了靜態與動態類型。他發現這兩者在代碼質量方面沒有顯著的差異,但是,基於動態類型的語言花費了更短的開發時間。他們的研究是在一個實驗室中,使用本科學生,設置了定製的語言和 IDE 來進行的。我們的研究正好相反,是一個實際的流行軟體應用的研究。雖然我們只能夠通過使用回歸模型間接(和 事後 )控制混雜因素,我們的優勢是樣本數量大,並且更真實、使用更廣泛的軟體。我們發現在相同的條件下,靜態化類型的語言比動態化類型的語言更少出現錯誤傾向,並且不允許隱式類型轉換的語言要好於允許隱式類型轉換的語言,其對結果的影響是非常小的。這是合理的,因為樣本量非常大,所以這種非常小的影響在這個研究中可以看得到。
Harrison et al. 8 比較了 C++ 與 SML,一個是過程化編程語言,一個是函數化編程語言,在總的錯誤數量上沒有找到顯著的差異,不過 SML 相比 C++ 有更高的缺陷密集度。SML 在我們的數據中並沒有體現出來,不過,認為函數式編程語言相比過程化編程語言更少出現缺陷。另一個重點工作是比較跨不同語言的開發工作。 12 , 20 不過,他們並不分析編程語言的缺陷傾向。
(2) 調查
Meyerovich 和 Rabkin 16 調查了開發者對編程語言的觀點,去研究為什麼一些編程語言比其它的語言更流行。他們的報告指出,非編程語言的因素影響非常大:先前的編程語言技能、可用的開源工具、以及現有的老式系統。我們的研究也證明,可利用的外部工具也影響軟體質量;例如,在 Go 中的並發 bug(請查看問題 4 節內容)。
(3) 對軟體倉庫的挖掘
Bhattacharya 和 Neamtiu 1 研究了用 C 和 C++ 開發的四個項目,並且發現在 C++ 中開發的組件一般比在 C 中開發的組件更可靠。我們發現 C 和 C++ 的錯誤傾向要高於全部編程語言的平均值。但是,對於某些 bug 類型,像並發錯誤,C 的缺陷傾向要高於 C++(請查看第 3 節中的問題 4)。
5. 有效的風險
我們認為,我們的報告的結論幾乎沒有風險。首先,在識別 bug 修復提交方面,我們依賴的關鍵字是開發者經常用於表示 bug 修復的關鍵字。我們的選擇是經過認真考慮的。在一個持續的開發過程中,我們去捕獲那些開發者一直面對的問題,而不是他們報告的 bug。不過,這種選擇存在過高估計的風險。我們對領域分類是為了去解釋缺陷的傾向,而且,我們研究組中另外的成員驗證過分類。此外,我們花費精力去對 bug 修復提交進行分類,也可能有被最初選擇的關鍵字所污染的風險。每個項目在提交日誌的描述上也不相同。為了緩解這些風險,我們像 2.4 節中描述的那樣,利用手工注釋評估了我們的類別。
我們判斷文件所屬的編程語言是基於文件的擴展名。如果使用不同的編程語言寫的文件使用了我們研究的通用的編程語言文件的擴展名,這種情況下也容易出現錯誤傾向。為減少這種錯誤,我們使用一個隨機樣本文件集手工驗證了我們的語言分類。
根據我們的數據集所顯示的情形,2.2 節中的解釋類編程語言,我們依據編程語言屬性的主要用途作了一些假設。例如,我們將 Objective-C 分到非管理內存類型中,而不是混合類型。同樣,我們將 Scala 注釋為函數式編程語言,將 C# 作為過程化的編程語言,雖然,它們在設計的選擇上兩者都支持。 19 , 21 在這項研究工作中,我們沒有從過程化編程語言中分離出面向對象的編程語言(OOP),因為,它們沒有清晰的區別,主要差異在於編程類型。我們將 C++ 分到允許隱式類型轉換的類別中是因為,某些類型的內存區域可以通過使用指針操作被進行不同的處理, 22 並且我們注意到大多數 C++ 編譯器可以在編譯時檢測類型錯誤。
最後,我們將缺陷修復提交關聯到編程語言屬性上,它們可以反應出報告的風格或者其它開發者的屬性。可用的外部工具或者 庫 也可以影響一個相關的編程語言的 bug 數量。
6. 總結
我們對編程語言和使用進行了大規模的研究,因為它涉及到軟體質量。我們使用的 Github 上的數據,具有很高的複雜性和多個維度上的差異的特性。我們的樣本數量允許我們對編程語言效果以及在控制一些混雜因素的情況下,對編程語言、應用領域和缺陷類型之間的相互作用,進行一個混合方法的研究。研究數據顯示,函數式語言是好於過程化語言的;不允許隱式類型轉換的語言是好於允許隱式類型轉換的語言的;靜態類型的語言是好於動態類型的語言的;管理內存的語言是好於非管理的語言的。進一步講,編程語言的缺陷傾向與軟體應用領域並沒有關聯。另外,每個編程語言更多是與特定的 bug 類別有關聯,而不是與全部 bug。
另一方面,即便是很大規模的數據集,它們被多種方法同時進行分割後,也變得很小且不全面。因此,隨著依賴的變數越來越多,很難去回答某個變數所產生的影響有多大這種問題,尤其是在變數之間相互作用的情況下。因此,我們無法去量化編程語言在使用中的特定的效果。其它的方法,比如調查,可能對此有幫助。我們將在以後的工作中來解決這些挑戰。
致謝
這個材料是在美國國家科學基金會(NSF)以及美國空軍科學研究辦公室(AFOSR)的授權和支持下完成的。授權號 1445079, 1247280, 1414172,1446683,FA955-11-1-0246。
參考資料
- Bhattacharya, P., Neamtiu, I. Assessing programming language impact on development and maintenance: A study on C and C++. In Proceedings of the 33rd International Conference on Software Engineering, ICSE'11 (New York, NY USA, 2011). ACM, 171–180.
- Bird, C., Nagappan, N., Murphy, B., Gall, H., Devanbu, P. Don't touch my code! Examining the effects of ownership on software quality. In Proceedings of the 19th ACM SIGSOFT Symposium and the 13th European Conference on Foundations of Software Engineering (2011). ACM, 4–14.
- Blei, D.M. Probabilistic topic models. Commun. ACM 55 , 4 (2012), 77–84.
- Cohen, J. Applied Multiple Regression/Correlation Analysis for the Behavioral Sciences. Lawrence Erlbaum, 2003.
- Easterbrook, S., Singer, J., Storey, M.-A., Damian, D. Selecting empirical methods for software engineering research. In Guide to Advanced Empirical Software Engineering (2008). Springer, 285–311.
- El Emam, K., Benlarbi, S., Goel, N., Rai, S.N. The confounding effect of class size on the validity of object-oriented metrics. IEEE Trans. Softw. Eng. 27 , 7 (2001), 630–650.
- Hanenberg, S. An experiment about static and dynamic type systems: Doubts about the positive impact of static type systems on development time. In Proceedings of the ACM International Conference on Object Oriented Programming Systems Languages and Applications, OOPSLA'10 (New York, NY, USA, 2010). ACM, 22–35.
- Harrison, R., Smaraweera, L., Dobie, M., Lewis, P. Comparing programming paradigms: An evaluation of functional and object-oriented programs. Softw. Eng. J. 11 , 4 (1996), 247–254.
- Harter, D.E., Krishnan, M.S., Slaughter, S.A. Effects of process maturity on quality, cycle time, and effort in software product development. Manage. Sci. 46 4 (2000), 451–466.
- Hindley, R. The principal type-scheme of an object in combinatory logic. Trans. Am. Math. Soc. (1969), 29–60.
- Jump, M., McKinley, K.S. Cork: Dynamic memory leak detection for garbage-collected languages. In ACM SIGPLAN Notices , Volume 42 (2007). ACM, 31–38.
- Kleinschmager, S., Hanenberg, S., Robbes, R., Tanter, É., Stefik, A. Do static type systems improve the maintainability of software systems? An empirical study. In 2012 IEEE 20th International Conference on Program Comprehension (ICPC) (2012). IEEE, 153–162.
- Li, Z., Tan, L., Wang, X., Lu, S., Zhou, Y., Zhai, C. Have things changed now? An empirical study of bug characteristics in modern open source software. In ASID'06: Proceedings of the 1st Workshop on Architectural and System Support for Improving Software Dependability (October 2006).
- Marques De Sá, J.P. Applied Statistics Using SPSS, Statistica and Matlab , 2003.
- Mayer, C., Hanenberg, S., Robbes, R., Tanter, É., Stefik, A. An empirical study of the influence of static type systems on the usability of undocumented software. In ACM SIGPLAN Notices , Volume 47 (2012). ACM, 683–702.
- Meyerovich, L.A., Rabkin, A.S. Empirical analysis of programming language adoption. In Proceedings of the 2013 ACM SIGPLAN International Conference on Object Oriented Programming Systems Languages & Applications (2013). ACM, 1–18.
- Milner, R. A theory of type polymorphism in programming. J. Comput. Syst. Sci. 17 , 3 (1978), 348–375.
- Mockus, A., Votta, L.G. Identifying reasons for software changes using historic databases. In ICSM'00. Proceedings of the International Conference on Software Maintenance (2000). IEEE Computer Society, 120.
- Odersky, M., Spoon, L., Venners, B. Programming in Scala. Artima Inc, 2008.
- Pankratius, V., Schmidt, F., Garretón, G. Combining functional and imperative programming for multicore software: An empirical study evaluating scala and java. In Proceedings of the 2012 International Conference on Software Engineering (2012). IEEE Press, 123–133.
- Petricek, T., Skeet, J. Real World Functional Programming: With Examples in F# and C#. Manning Publications Co., 2009.
- Pierce, B.C. Types and Programming Languages. MIT Press, 2002.
- Posnett, D., Bird, C., Dévanbu, P. An empirical study on the influence of pattern roles on change-proneness. Emp. Softw. Eng. 16 , 3 (2011), 396–423.
- Tan, L., Liu, C., Li, Z., Wang, X., Zhou, Y., Zhai, C. Bug characteristics in open source software. Emp. Softw. Eng. (2013).
作者
Baishakhi Ray (rayb@virginia.edu), Department of Computer Science, University of Virginia, Charlottesville, VA.
Daryl Posnett (dpposnett@ucdavis.edu), Department of Computer Science, University of California, Davis, CA.
Premkumar Devanbu (devanbu@cs.ucdavis.edu), Department of Computer Science, University of California, Davis, CA.
Vladimir Filkov (filkov@cs.ucdavis.edu), Department of Computer Science, University of California, Davis, CA.
腳註
- a. Wikipedia's article on type conversion, https://en.wikipedia.org/wiki/Type_conversion, has more examples of unintended behavior in C.
- b. This Apple developer article describes the usage of "id" http://tinyurl.com/jkl7cby.
- c. Some examples can be found here http://dobegin.com/objc-id-type/ and here http://tinyurl.com/hxv8kvg.
- d. Chi-squared value of 243.6 with 96 df. and p = 8.394e–15
- e. MPI does not require locking of shared resources.
作者:Baishakhi Ray, Daryl Posnett, Premkumar Devanbu, Vladimir Filkov 譯者:qhwdw 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive