找回密码
立即注册
搜索
发回帖 发新帖

6738

积分

0

好友

853

主题
发表于 2026-1-25 01:03:30 | 查看: 395| 回复: 0

本文源于对知乎提问《Rust再一次比Go慢,问题出在哪里呢?》的深入探究与实测。我们将通过一个具体的端口扫描程序案例,对比分析Rust与Go的性能表现差异,并深入系统调用层面定位问题根源。

一、问题背景

一位开发者在测试时发现,其编写的端口扫描程序,Rust版本(使用Tokio)耗时显著高于Go版本。具体表现为:Go版本耗时约1秒,而Rust版本耗时超过2秒。他尝试咨询了多个AI模型,均未得到合理解释,因此在技术社区发起求助。

知乎提问截图:关于Rust与Go性能差异的讨论

原问题链接:https://www.zhihu.com/question/1995930596204099510/answer/1996522199473997490

该开发者主要困惑有两点:

  1. Rust程序执行更慢的具体原因。
  2. 尝试将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 服务器硬件与系统信息

服务器CPU与内存信息截图
服务器配置:Intel Xeon E5-2689 @ 2.40GHz (2核), 15GB内存, Linux 5.14内核。

3.2 语言环境

Rust环境:使用稳定版工具链 (stable-x86_64-unknown-linux-gnu)。
Rust工具链信息截图

Go环境:版本为 go1.24.2。
Go环境变量截图

四、编译阶段对比

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

Rust 发布构建:
Rust编译过程截图
执行 time cargo build --release,总耗时约29.4秒。

Go 构建:
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版本执行:
Rust程序执行结果截图
程序报告发现了3个开放端口(22, 56682, 57746),总执行时间约6.9秒。其中系统态(sys)时间高达5.243秒。

Go版本执行:
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

发现:

  1. 结果准确性:Rust版本出现了误报,多扫描出了两个处于TIME_WAIT状态的连接端口,这与其超时控制和连接状态处理有关。
  2. 性能差异:Rust版本总耗时是Go版本的2倍以上。
  3. 关键线索:Rust版本的系统调用时间(sys)比Go版本多了约1.3秒!这强烈暗示性能瓶颈可能出现在系统调用层面。

六、深入系统调用定位瓶颈

既然系统态时间差异显著,我们直接使用 perf 工具分析程序运行期间发生的系统调用,这是定位性能问题的利器。命令如下:

perf stat -e 'syscalls:sys_enter_*' ./program

6.1 Rust版本系统调用分析

我们截取了部分关键的系统调用统计:

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

Rust版本epoll相关系统调用
epoll_ctl被调用了131,072次,epoll_wait被调用了42,995次。

Rust版本调度与内存相关系统调用
Rust版本内存与调度系统调用
Rust版本futex系统调用
write调用2,743次,sched_yield(让出CPU)调用4,602次,futex(用户态锁)调用1,840次,brk(调整堆内存)调用166次。

6.2 Go版本系统调用分析

同样,我们查看Go版本的关键系统调用:

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

Go版本epoll相关系统调用
epoll_ctl调用131,075次,但epoll_wait为0次,取而代之的是epoll_pwait,仅调用1,915次。

Go版本其他系统调用
Go版本系统调用续
Go版本系统调用统计结尾
Go版本执行时间统计
sched_yield仅97次,futex 557次,brk仅3次,write仅4次。

6.3 关键差异综合对比

我们将核心差异整理如下表:

Rust与Go系统调用差异对比表格

差异点 相关程序 调用项 调用次数 差异解释与分析
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版本,主要根源在于两者异步网络库和运行时在系统调用层面的实现差异,而非语言本身的绝对性能优劣。

具体来说:

  1. I/O多路复用效率:Go标准库的net包与运行时调度器深度集成,使用了更高效的epoll_pwait模型,事件通知开销远低于本例中Tokio库的epoll_wait模型。
  2. 调度与同步开销:Tokio在此高并发设置下(buffer_unordered(25000))产生了显著的调度竞争(大量sched_yield和futex),而Go的GMP调度器在此场景下显得更加游刃有余。
  3. 内存管理策略:本例中Rust代码的默认内存分配行为导致了更多的系统调用(brk),而Go的GC和内存池预分配机制规避了这个问题。

给开发者的建议

  • 关于语言选择:Go以其简洁的语法、强大的标准库和高效的并发模型,非常适合快速开发高性能网络服务,学习曲线相对平缓。Rust则提供了无与伦比的控制力和内存安全性,但在编写极致高性能的网络程序时,需要对异步运行时、底层IO模型有更深入的理解。
  • 关于本次性能问题:这并不能说明Rust比Go慢。通过优化Rust代码,例如:选用更合适的异步运行时或网络库(如async-std、smol或直接使用mio)、调整并发策略、使用连接池、为异步任务配置合适的分配器(如jemalloc),完全有可能达到甚至超越Go版本的性能。
  • 关于“最佳实践”:提问者的Go代码本身也有优化空间(例如资源控制)。性能优化永远是一个具体问题具体分析的过程。盲目地进行1:1的代码迁移,往往无法发挥目标语言或库的最佳特性。

最终观点:编程语言是工具,各有其设计哲学与适用场景。性能差异往往源于库的实现、运行时特性以及程序员对它们的运用方式。优秀的开发者应深入理解所用工具的原理,而非简单地归咎于语言。欢迎在云栈社区交流更多关于系统编程和性能优化的实践经验。

Rust vs Go 概念对比图




上一篇:AI智能体需求重塑CPU市场,半导体供需失衡与涨价分析
下一篇:为什么我选择做小而美的SaaS?一份给未来自己的礼物
您需要登录后才可以回帖 登录 | 立即注册

关闭

站长推荐上一条 /1 下一条

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-11-6 05:54 , Processed in 0.077844 second(s), 43 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表