本文源于对知乎提问《Rust再一次比Go慢,问题出在哪里呢?》的深入探究与实测。我们将通过一个具体的端口扫描程序案例,对比分析Rust与Go的性能表现差异,并深入系统调用层面定位问题根源。
一、问题背景
一位开发者在测试时发现,其编写的端口扫描程序,Rust版本(使用Tokio)耗时显著高于Go版本。具体表现为:Go版本耗时约1秒,而Rust版本耗时超过2秒。他尝试咨询了多个AI模型,均未得到合理解释,因此在技术社区发起求助。

原问题链接:https://www.zhihu.com/question/1995930596204099510/answer/1996522199473997490
该开发者主要困惑有两点:
- Rust程序执行更慢的具体原因。
- 尝试将Go代码逻辑1:1迁移到Rust时,发现Tokio的通道(channel)类型(主要是
mpsc)与Go的channel在使用模式上存在差异,难以直接对应。
接下来,我们将通过复现其代码和环境,进行对比测试与分析。
二、对比代码
为了控制变量,我们直接使用提问者提供的核心代码逻辑。
2.1 Go版本代码
Go版本采用经典的“生产者-消费者”worker模式进行端口扫描。
package main
import (
"bufio"
"fmt"
"net"
"os"
"sort"
"sync"
"time"
)
func measureTime(fn func()) {
start := time.Now()
fn()
elapsed := time.Since(start)
fmt.Printf("Execution Time: %v\n", elapsed)
}
func worker(ports <-chan int, results chan<- int) {
var wg sync.WaitGroup
// 创建指定数量的worker
for range 100 {
wg.Go(func() {
for port := range ports {
addr := net.JoinHostPort("127.0.0.1", fmt.Sprintf("%d", port))
conn, err := net.DialTimeout("tcp", addr, time.Duration(5)*time.Second)
if err != nil {
continue
}
conn.Close()
results <- port
}
})
}
wg.Wait()
close(results)
}
func tcpScan() {
ports := make(chan int, 100)
results := make(chan int, 100)
// 发送端口到 ports channel
go func() {
for i := range 65535 {
ports <- i
}
close(ports)
}()
// 启动 worker
go worker(ports, results)
fmt.Printf("Scanning %s\n", "127.0.0.1")
// 收集结果
opened := []int{}
for port := range results {
opened = append(opened, port)
}
// 排序
sort.Ints(opened)
// 打印结果
fmt.Printf("Open ports (%d found):\n", len(opened))
for _, port := range opened {
fmt.Printf(" %d\n", port)
}
}
func main() {
measureTime(tcpScan)
}
2.2 Rust版本代码
Rust版本使用Tokio异步运行时,通过buffer_unordered来控制并发度。
use futures::StreamExt;
use std::time::{Duration, Instant};
use tokio::net::TcpStream;
#[tokio::main]
async fn main() {
let start = Instant::now();
// 使用缓冲流控制并发
let mut result: Vec<_> = futures::stream::iter(1..=65535)
.map(|port| {
let host_port = format!("127.0.0.1:{port}");
async move {
if let Ok(Ok(_)) = tokio::time::timeout(Duration::from_millis(200), TcpStream::connect(host_port)).await {
Some(port)
} else {
None
}
}
})
.buffer_unordered(25000) // 最大并发数
.filter_map(|port| async move { port })
.collect()
.await;
result.sort();
let count = result.len();
print!("Open ports ({count} found):\n");
result.into_iter().for_each(|port| print!(" {port}\n"));
println!("Execution Time: {:?}", start.elapsed());
}
三、测试环境配置
为保证测试的客观性,我们在一个干净的Linux服务器环境中进行。
3.1 服务器硬件与系统信息

服务器配置:Intel Xeon E5-2689 @ 2.40GHz (2核), 15GB内存, Linux 5.14内核。
3.2 语言环境
Rust环境:使用稳定版工具链 (stable-x86_64-unknown-linux-gnu)。

Go环境:版本为 go1.24.2。

四、编译阶段对比
在性能对比前,我们先看看两者在编译阶段的差异,这也是两种语言设计哲学的一个体现。
Rust 发布构建:

执行 time cargo build --release,总耗时约29.4秒。
Go 构建:

执行 time go build .,总耗时仅约0.6秒。
编译结果对比:

| 项目 |
编译命令 |
编译耗时 (real) |
生成文件大小 |
| Rust |
cargo build --release |
29.427s |
974,672 Bytes |
| Go |
go build . |
0.615s |
3,189,972 Bytes |
小结:Rust的编译时间远长于Go,但生成的二进制文件更加精简;Go则以其快速的编译速度著称,但二进制文件体积通常更大。这一点已是共识。
五、运行时性能对比
编译差异并非本次重点,我们更关心运行时的表现。在本测试机上,实际开放的TCP端口仅有SSH服务的22端口。
5.1 预期结果验证
通过 netstat 命令确认,仅22端口处于监听状态。
[root@localhost test0001]# netstat -tnl | grep 0.0.0.0:
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN
5.2 各版本执行结果
Rust版本执行:

程序报告发现了3个开放端口(22, 56682, 57746),总执行时间约6.9秒。其中系统态(sys)时间高达5.243秒。
Go版本执行:

程序正确报告发现1个开放端口(22),总执行时间约3.31秒。系统态时间为3.989秒。
5.3 初步结果分析

| 指标 |
Rust 版本 |
Go 版本 |
| 执行结果 |
错误(多报端口) |
正确 |
| 程序输出耗时 |
6.903s |
3.308s |
| 系统time输出 |
real 0m6.911s, user 0m1.087s, sys 0m5.243s |
real 0m3.311s, user 0m1.603s, sys 0m3.989s |
发现:
- 结果准确性:Rust版本出现了误报,多扫描出了两个处于
TIME_WAIT状态的连接端口,这与其超时控制和连接状态处理有关。
- 性能差异:Rust版本总耗时是Go版本的2倍以上。
- 关键线索:Rust版本的系统调用时间(sys)比Go版本多了约1.3秒!这强烈暗示性能瓶颈可能出现在系统调用层面。
六、深入系统调用定位瓶颈
既然系统态时间差异显著,我们直接使用 perf 工具分析程序运行期间发生的系统调用,这是定位性能问题的利器。命令如下:
perf stat -e 'syscalls:sys_enter_*' ./program
6.1 Rust版本系统调用分析
我们截取了部分关键的系统调用统计:

Rust版本进行了大量socket, connect, accept, getsockopt, setsockopt调用。

epoll_ctl被调用了131,072次,epoll_wait被调用了42,995次。



write调用2,743次,sched_yield(让出CPU)调用4,602次,futex(用户态锁)调用1,840次,brk(调整堆内存)调用166次。
6.2 Go版本系统调用分析
同样,我们查看Go版本的关键系统调用:

Go版本同样有大量socket, connect等调用,但accept调用为0(这与实现方式有关)。

epoll_ctl调用131,075次,但epoll_wait为0次,取而代之的是epoll_pwait,仅调用1,915次。




sched_yield仅97次,futex 557次,brk仅3次,write仅4次。
6.3 关键差异综合对比
我们将核心差异整理如下表:

| 差异点 |
相关程序 |
调用项 |
调用次数 |
差异解释与分析 |
| epoll相关 |
Rust |
epoll_wait |
42,995 |
1. 事件模型差异:Rust的Tokio库在此配置下可能更频繁地轮询事件就绪状态,导致大量epoll_wait系统调用。 |
|
|
epoll_ctl |
131,072 |
2. 效率对比:Go的网络库默认使用了epoll_pwait,并将事件等待与信号处理结合,在本例中调用次数(1,915)远低于Rust的epoll_wait(42,995),I/O多路复用的效率更高。 |
|
Go |
epoll_pwait |
1,915 |
3. 库实现成熟度:Go标准库的网络IO模型经过长期优化,与Go的调度器深度集成,在此类高并发短连接场景下表现更佳。 |
|
|
epoll_ctl |
0 |
|
| 系统调度 |
Rust |
sched_yield |
4,602 |
1. 调度策略:Rust版本频繁让出CPU (sched_yield),且futex调用也多,表明任务间存在大量竞争,调度开销大。 |
|
|
futex |
1,804 |
2. 调度器对比:Go的GMP调度器是协同式与抢占式结合,在此场景下调度效率很高,相关系统调用少。 |
|
Go |
sched_yield |
97 |
|
|
|
futex |
557 |
|
| 内存管理 |
Rust |
brk |
166 |
1. 内存分配模式:Rust版本(无全局分配器特殊优化时)在此测试中频繁通过brk向系统申请堆内存,每次连接可能涉及独立的内存分配。 |
|
|
write |
2,743 |
2. GC优势:Go程序启动时已预分配内存池,且其垃圾回收机制在此类短生命周期对象场景下,减少了直接系统调用的次数。 |
|
Go |
brk |
3 |
3. 输出缓冲:Rust版本write调用极多,可能缺少缓冲或每次输出都触发系统调用;Go的fmt包有缓冲。 |
|
|
write |
4 |
|
七、结论与建议
通过以上层层剖析,我们可以得出结论:在此特定的端口扫描案例中,Rust版本慢于Go版本,主要根源在于两者异步网络库和运行时在系统调用层面的实现差异,而非语言本身的绝对性能优劣。
具体来说:
- I/O多路复用效率:Go标准库的
net包与运行时调度器深度集成,使用了更高效的epoll_pwait模型,事件通知开销远低于本例中Tokio库的epoll_wait模型。
- 调度与同步开销:Tokio在此高并发设置下(
buffer_unordered(25000))产生了显著的调度竞争(大量sched_yield和futex),而Go的GMP调度器在此场景下显得更加游刃有余。
- 内存管理策略:本例中Rust代码的默认内存分配行为导致了更多的系统调用(
brk),而Go的GC和内存池预分配机制规避了这个问题。
给开发者的建议
- 关于语言选择:Go以其简洁的语法、强大的标准库和高效的并发模型,非常适合快速开发高性能网络服务,学习曲线相对平缓。Rust则提供了无与伦比的控制力和内存安全性,但在编写极致高性能的网络程序时,需要对异步运行时、底层IO模型有更深入的理解。
- 关于本次性能问题:这并不能说明Rust比Go慢。通过优化Rust代码,例如:选用更合适的异步运行时或网络库(如
async-std、smol或直接使用mio)、调整并发策略、使用连接池、为异步任务配置合适的分配器(如jemalloc),完全有可能达到甚至超越Go版本的性能。
- 关于“最佳实践”:提问者的Go代码本身也有优化空间(例如资源控制)。性能优化永远是一个具体问题具体分析的过程。盲目地进行1:1的代码迁移,往往无法发挥目标语言或库的最佳特性。
最终观点:编程语言是工具,各有其设计哲学与适用场景。性能差异往往源于库的实现、运行时特性以及程序员对它们的运用方式。优秀的开发者应深入理解所用工具的原理,而非简单地归咎于语言。欢迎在云栈社区交流更多关于系统编程和性能优化的实践经验。
