这是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)