综述
现代的异步编程中有如下的几个概念
由于协程是非常轻量的,所以可以在一个进程中大量的创建,runtime
会实际创建系统线程(一般为恰好的物理CPU数),并将协程映射到实际的物理线程上执行,这个有时候称为 M:N模型
。好的 runtime 会使得系统整体的性能随着物理CPU的增加而线性增加。
Golang 是原生支持上述模型的语言,这也是 Golang
与众不同的主要特性,在 Golang
中,通过关键词 go
即可轻松开启一个协程,通过关键词 chan
则可以定义一个队列,Golang
内置了调度运行时来支撑异步编程。
Rust 在 2019年的 1.39
版本中,加入 async/.await
关键词,为异步编程提供了基础支撑,之后,随着 Rust
生态中的主要异步运行时框架之一 tokio 1 发布,Rust
编写异步系统也变得跟 Golang
一样方便。
Kotlin 是一个基于 JVM 的语言,它语言层面原生支持协程,但由于 JVM 现在还不支持协程,所以它是在 JVM 之上提供了的调度运行时和队列。顺便,阿里巴巴的 Dragonwell JDK 在 OpenJDK 的基础上可以选择开启 Wisp2 特性,来使得 JVM 中的 Thread 不再是系统线程,而是一个协程。JDK 19 开始增加了预览版的轻量级线程(协程),也许在下一个 JDK LTS 会有正式版。
下表对比了使用这两种语言对异步编程的特性支持
Golang | Rust | Kotlin | |
---|---|---|---|
协程 | 语言内置 | 由异步运行时框架提供 | 语言内置 |
队列 | 语言内置 | 由异步运行时框架提供 | 语言内置 |
调度运行时 | 语言内置,不可更改 | 多个实现, tokio/async_std/... | 语言内置 |
异步函数 | 无需区分 | 需显式的定义 | 需显式定义 |
队列类型 | 无需特指,只有一种 mpmc | 可特指,不同的场景提供不同实现 | 无需特指 |
垃圾回收 | 通过GC算法进行垃圾回收 | 无GC,资源超出作用域即释放 | 通过GC算法进行垃圾回收 |
根据场景的不同,选择不同的队列,不同的运行时,可以得到更好的性能,但 Golang
和 Kotlin
简化了这些选择,一般来说,简化会带来性能的损失,本文测评 Go/Rust(tokio)/Kotlin 的调度和队列性能。
测评的逻辑如下
这个场景类似服务器的实现,当客户端连接到服务器时,创建一个协程,接收客户端的请求,然后将请求投递给处理协程。
在这样的逻辑下,有如下的几个参数来控制测评的规模
含义 | 命令行参数 | 说明 | |
---|---|---|---|
workers | 协程的数目 | -w | |
events | 消息数目 | -e | |
queue | 队列可堆积的消息的数目 | -q | 队列满了之后协程会阻塞 |
etype | 消息的类型 | -t | 0 整数 1 字符串 2 字符串指针 3 字符串复制 |
esize | 消息的大小 | -s | 对于字符串类似,越大的消息内存分配压力越大 |
测评完成后,会输出如下的几个数据
含义 | 说明 | |
---|---|---|
total_events | 总共产生和接收的消息数目 | 即 workers * events |
time | 完成测试使用的需要的时间 | 越小越好 |
speed | 每秒处理的消息数目 | total_events/time 越大越好 |
以下是各语言实现时的一些额外说明
Event
接口的不同 struct, 如 IntEvent, StrEvent, CheapStrEvent 等Event
接口的不同 struct, 如 IntEvent, StrEvent, CheapStrEvent 等--cpuprofile 文件名
参数来生成程序运行的性能分析数据boc-go/target/boc-go -w 10000 -e 10000 -q 256 --cpuprofile boc-go.pprof
然后可以通过 go tool pprof -http=:8081 boc-go.pprof
来查看boc-rs/target/release/boc-rs -c -w 10000 -e 10000 -q 256 --cpuprofile boc-rs.svg
, 然后使用浏览器打开 boc-rs.svg
来查看在安装了 go、rust、JDK/maven 的机器上
git clone https://gitee.com/elsejj/bench-of-chain.gitcd bench-of-chainmake
run.sh
以相同的参数,同时运行各语言实现的程序,得到如下的输出$ ./run.sh -w 5000 -e 10000 -q 256 -t 2program,etype,worker,event,time,speed
golang,str_ptr,5000,10000,0.477,104845454
rust,str_ptr,5000,10000,0.652,76636797
kotlin,str_ptr,5000,10000,1.638,30526077
bench.sh
以不同的 worker 、etype 运行多次,输出结果列表,bench.sh
在不同的机器上,可能会运行数分钟, 其结果如$ ./run.sh -e 10000
program | etype | worker | event | time | speed |
---|---|---|---|---|---|
golang | int | 100 | 10000 | 0.010 | 98969725 |
rust | int | 100 | 10000 | 0.012 | 80789148 |
kotlin | int | 100 | 10000 | 0.145 | 6917313 |
golang | str | 100 | 10000 | 0.045 | 21989041 |
rust | str | 100 | 10000 | 0.019 | 53630230 |
kotlin | str | 100 | 10000 | 0.159 | 6304093 |
golang | str_ptr | 100 | 10000 | 0.011 | 88775257 |
rust | str_ptr | 100 | 10000 | 0.012 | 81436541 |
kotlin | str_ptr | 100 | 10000 | 0.136 | 7340791 |
... | |||||
kotlin | str_ptr | 50000 | 10000 | 12.434 | 40212992 |
golang | int | 50000 | 10000 | 5.594 | 89376773 |
rust | int | 50000 | 10000 | 9.131 | 54760465 |
kotlin | int | 50000 | 10000 | 9.629 | 51927597 |
golang | str | 50000 | 10000 | 17.794 | 28099233 |
rust | str | 50000 | 10000 | 12.437 | 40203692 |
kotlin | str | 50000 | 10000 | 16.774 | 29807544 |
golang | str_ptr | 50000 | 10000 | 4.911 | 101819179 |
rust | str_ptr | 50000 | 10000 | 8.795 | 56850205 |
kotlin | str_ptr | 50000 | 10000 | 11.662 | 4287558 |
值 | |
---|---|
OS | Ubuntu 22.04 WSL on windows 11 64bit |
CPU | Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz |
Mem | 32G |
Go | 1.18.1 |
Rust | 1.62.0 |
JDK | OpenJDK 17.0.3 |
Kotlin | 1.7.10 |
./run.sh -e 10000
每个测评项会执行5次,取其平均值
从上述的运行结果来看
bench.sh
来进行相关的测试。