找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2565

积分

0

好友

358

主题
发表于 17 小时前 | 查看: 0| 回复: 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_yieldfutex),而Go的GMP调度器在此场景下显得更加游刃有余。
  3. 内存管理策略:本例中Rust代码的默认内存分配行为导致了更多的系统调用(brk),而Go的GC和内存池预分配机制规避了这个问题。

给开发者的建议

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

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

Rust vs Go 概念对比图




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

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

GMT+8, 2026-1-25 18:21 , Processed in 0.312288 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

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