這是JAVA,Go和Rust之間的比較。 這不是基準測試,而是更多輸出可執行文件大小,內存使用,CPU使用率,運行時要求之間的比較,當然還有一個小的基準測試,可以每秒獲取一些請求,并嘗試使 一些數字的感覺。
內存使用情況
空閑,無所事事
> Memory usage of each Application while running idle in memory.
什么? Go和Rust版本的條形圖在哪里顯示空閑時的內存占用量? 好了,它們在那里,只有當JVM啟動程序并處于空閑狀態時,Java才消耗160 MB以上的內存,什么也沒做。 對于Go,程序使用0.86 MB,對于Rust,則使用0.36 MB。 這是一個很大的差異! 在這里,Java使用的內存比Go和Rust對應的內存高兩個數量級,只是坐在內存中什么都不做。 那是巨大的資源浪費。
執行文件尺寸
有關如何構建二進制文件的一些信息。 在Java的情況下,我已經使用maven-shade-plugin將所有內容構建到一個大的jar中,并使用了mvn packagetarget。 對于Go,我使用了go build。 最后,對于Rust,我使用了build –release版本。
> Compiled size of each program in megabytes.
工件編譯后的大小還取決于所選的庫/依賴項,因此,如果它們腫,則編譯后的程序將最終相同。 在我的特定情況下,對于我選擇的庫,以上是程序的編譯大小。
在單獨的部分中,我將把這三個程序構建并打包為Docker映像,并列出它們的大小,以顯示每種語言所需的運行時開銷。 下面有更多詳細信息。
為了嘗試將蘋果與蘋果進行比較(也許是?),我在此比較中使用每種語言編寫了一個Web服務。 Web服務非常簡單,它為三個REST端點提供服務。
> The endpoints served by the web service, in Java, Go, and Rust.
這三個Web服務的存儲庫托管在github上。
服務REST請求
讓我們使用wrk使用請求訪問API,并觀察內存和CPU使用情況,以及在我的計算機上針對程序的三個版本的每個端點每秒實現的請求數。
wrk -t2 -c400 -d30s http://127.0.0.1:8080/hello wrk -t2 -c400 -d30s http://127.0.0.1:8080/greeting/Janewrk -t2 -c400 -d30s http://127.0.0.1:8080/fibonacci/35
上面的wrk命令說以下內容,使用兩個線程(用于wrk)并在池中保留400個打開的連接,并重復調用GET端點,持續30秒。 這里我僅使用兩個線程,因為wrk和被測程序都在同一臺計算機上運行,所以我不希望它們在可用資源(尤其是CPU)上相互競爭(太多)。
每個Web服務都經過單獨測試,并且在每次運行之間都重新啟動了Web服務。
以下是該程序的每個版本的三個運行中的最佳結果。
/hello
該端點返回Hello,World! 信息。 它分配字符串" Hello,World!" 并將其序列化并以JSON格式返回。
> CPU usage while hitting the /hello endpoint
> Memory usage while hitting the /hello endpoint
> Requests per second while hitting the /hello endpoint
運行時大小
為了模擬現實世界中的云本機應用程序,并消除"它可以在我的機器上運行!",我為這三個應用程序中的每一個創建了一個docker映像。
Docker文件的源包含在相應程序文件夾下的存儲庫中。
作為Java應用程序的基本運行時映像,我使用過openjdk:8-jre-alpine,該映像被稱為是最小的映像之一,但是,這帶有一些警告,這些警告可能適用于您的應用程序,也可能不適用于您的應用程序 ,主要是高山圖片在處理環境變量名稱方面不符合posix,因此您不能使用。 docker文件中ENV中的(點)字符(沒什么大不了的),另一個是阿爾卑斯linux映像是使用musl libc而不是glibc編譯的,這意味著如果您的應用程序依賴于需要glibc(或朋友)的東西 )顯示,它根本無法正常工作。 以我為例,高山行之有效。
至于應用程序的Go和Rust版本,我已經對其進行了靜態編譯,這意味著它們不希望在運行時映像中存在libc(glibc,musl…等),這也意味著它們不需要 運行OS的基本映像。因此,我使用了臨時docker映像,這是一個無操作映像,以零開銷托管托管已編譯的可執行文件。
我使用的Docker映像的命名約定為{lang} / webservice。 該應用程序的Java,Go和Rust版本的圖像大小分別為113、8.68和4.24 MB。
> Final Docker images size
/ fibonacci / {number}
該端點接受段路徑參數{number},并返回斐波納契數和序列化為JSON格式的輸入數。
對于此特定端點,我選擇以遞歸形式實現它。 我毫不懷疑,迭代實現會產生更好的性能結果,并且出于生產目的,應該選擇一種迭代形式,但是在生產代碼中有些情況下必須使用遞歸(不是專門用于計算第n個斐波那契數) )。 因此,為此,我希望該實現大量涉及CPU堆棧分配。
> CPU usage while hitting the /fibonacci endpoint
> Memory usage while hitting the /fibonacci endpoint
> Requests per second while hitting the /fibonacci endpoint
在Fibonacci端點測試中,Java實現是唯一一個對150個請求超時的實現,如下wrk的輸出所示。
> Timeouts
> Latency for the /fibonacci endpoint
/ greeting / {name}
該端點接受段路徑參數{name},然后格式化字符串" Hello,{name}!",進行序列化并將其返回為JSON格式的問候消息。
> CPU usage while hitting the /greeting endpoint
> Memory usage while hitting the /greeting endpoint
> Requests per second while hitting the /greeting endpoint
結論
> How the three languages compare
在得出任何結論之前,我想指出這三種語言之間的關系(或缺乏)。 Java和Go都是垃圾收集語言,但是Java會提前編譯為在JVM上運行的字節碼。 當啟動Java應用程序時,即時(JIT)編譯器將被調用,以通過隨時隨地將其編譯為本機代碼來優化字節碼,以提高應用程序的性能。
Go和Rust都提前編譯為本地代碼,并且在運行時不會進行進一步的優化。
Java和Go都是垃圾收集語言,具有世界末日的副作用。 這意味著,每當垃圾收集器運行時,它將停止應用程序,進行垃圾收集,并在完成后從停止的地方恢復應用程序。 大多數垃圾收集器需要停止運行,但是有些實現似乎不需要這樣做。
當Java語言在90年代創建時,其最大的賣點之一是一次編寫,可在任何地方運行。 當時,這很棒,因為市場上沒有很多虛擬化解決方案。 如今,大多數CPU支持虛擬化,這種虛擬化僅在代碼可以在任何地方(無論如何在任何受支持的平臺上運行)的前提下,才停止使用某種語言進行開發的誘惑。Docker和其他解決方案以便宜的價格提供虛擬化。
在整個測試中,應用程序的Java版本比Go或Rust對應版本消耗了更多的內存,在前兩個測試中,Java使用的內存大約增加了8000%。 這意味著對于實際應用程序,Java應用程序的運行成本會更高。
對于前兩個測試,Go應用程序使用的CPU比Java少20%,同時處理38%的請求。 另一方面,Rust版本使用的CPU比Go減少了57%,而處理的請求卻增加了13%。
第三次測試在設計上是占用大量CPU的資源,因此我想從中擠出CPU的每一分。 Go和Rust都比Java使用了1%的CPU。 而且我認為,如果wrk不在同一臺計算機上運行,則所有這三個版本都會使CPU上限為100%。 在內存方面,Java使用的內存比Go和Rust多2000%。 Java可以處理的請求比Go多出20%,而Rust可以處理的請求比Java多出15%。
在撰寫本文時,Java編程語言已經存在了將近30年,這使得在市場上尋找Java開發人員變得相對容易。 另一方面,Go和Rust都是相對較新的語言,因此與Java相比,自然而然的數量或更少的開發人員。 不過,Go和Rust都獲得了很大的吸引力,許多開發人員正在將它們用于新項目,并且有許多使用Go和Rust的生產中正在運行的項目,因為簡單地說,就資源而言,它們比Java更有效。 需要。 (也許是因為它們是街上的新酷語言!)
在編寫本文的程序時,我同時學習了Go和Rust。就我而言,Go的學習曲線很短,因為它是一種相對容易掌握的語言,并且與其他語言相比語法很小。我只用了幾天就用Go編寫了程序。關于Go需要注意的一件事是編譯速度,我不得不承認,與Java / C / C ++ / Rust等其他語言相比,它的速度非常快。該程序的Rust版本花了我大約一個星期的時間來完成,我不得不說,大部分時間都花在弄清借閱檢查器向我要什么上。 Rust具有嚴格的所有權規則,但是一旦掌握了Rust的所有權和借用概念,編譯器錯誤消息就會突然變得更加有意義。當違反借閱檢查規則時,Rust編譯器對您大吼大叫的原因是,因為編譯器要在編譯時證明已分配內存的壽命和所有權。這樣,它保證了程序的安全性(例如:除非使用了不安全的代碼轉義,否則就沒有懸掛指針),并且在編譯時確定了釋放位置,從而消除了垃圾收集器的需求和運行時成本。當然,這是以學習Rust的所有權系統為代價的。
在競爭方面,我認為Go是Java(通常是JVM語言)的直接競爭對手,但不是Rust的競爭對手。 另一方面,Rust是Java,Go,C和C ++的重要競爭對手。
由于他們的效率,我看到了自己。 并且將會在Go和Rust中編寫更多的程序,但是很可能在Rust中編寫更多的程序。 兩者都非常適合網絡服務,CLI,系統程序(..etc)開發。 但是,Rust比Go具有根本優勢。 它不是垃圾收集的語言,與C和C ++相比,它可以安全地編寫代碼。 例如,Go并不是特別適合用于編寫OS內核,而這里又是Rust的亮點,并與C / C ++競爭,因為它們是編寫OS的悠久且事實上的語言。Rust與C競爭的另一種方式 / C ++在嵌入式世界中,但我將繼續進行討論。
感謝您的閱讀!
(本文翻譯自Dexter Darwich的文章《Comparison between Java, Go, and Rust》,參考:https://medium.com/@dexterdarwich/comparison-between-java-go-and-rust-fdb21bd5fb7c)