You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何Rust的--release构建版本性能比Go慢近6倍?

Rust并发处理像素数据性能落后Go的问题排查与优化

问题描述

我正在学习Rust的并发与并行计算,编写了一个遍历二维向量(模拟图像像素)的脚本,用于测试iter与par_iter的性能差异。结果发现Rust耗时极高,用Go编写了类似的并发代码后,Go的性能快了约585%!

注:Rust版本使用--release构建,已尝试原生线程池、调整线程数量,结果一致。可忽略生成随机值填充二维向量的低效写法。

原始Rust代码(耗时约140ms)

use rand::Rng;
use std::time::Instant;
use rayon::prelude::*;

fn normalise(value: u16, min: u16, max: u16) -> f32 {
    (value - min) as f32 / (max - min) as f32
}

fn main() {
    let pixel_size = 9_000_000;
    let fake_image: Vec<Vec<u16>> = (0..pixel_size).map(|_| {
        (0..4).map(|_| {
            rand::thread_rng().gen_range(0..=u16::MAX)
        }).collect()
    }).collect();

    // Time starts now.
    let now = Instant::now();

    let chunk_size = 300_000;

    let _normalised_image: Vec<Vec<Vec<f32>>> = fake_image.par_chunks(chunk_size).map(|chunk| {
        let normalised_chunk: Vec<Vec<f32>> = chunk.iter().map(|i| {
            let r = normalise(i[0], 0, u16::MAX);
            let g = normalise(i[1], 0, u16::MAX);
            let b = normalise(i[2], 0, u16::MAX);
            let a = normalise(i[3], 0, u16::MAX);
            
            vec![r, g, b, a]
        }).collect();

        normalised_chunk
    }).collect();

    // Timer ends.
    let elapsed = now.elapsed();
    println!("Time elapsed: {:.2?}", elapsed);
}

对比Go代码(耗时约24ms)

package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

func normalise(value uint16, min uint16, max uint16) float32 {
    return float32(value-min) / float32(max-min)
}

func main() {
    const pixelSize = 9000000
    var fakeImage [][]uint16

    // Create a new random number generator
    src := rand.NewSource(time.Now().UnixNano())
    rng := rand.New(src)

    for i := 0; i < pixelSize; i++ {
        var pixel []uint16
        for j := 0; j < 4; j++ {
            pixel = append(pixel, uint16(rng.Intn(1<<16)))
        }
        fakeImage = append(fakeImage, pixel)
    }

    normalised_image := make([][4]float32, pixelSize)
    var wg sync.WaitGroup

    // Time starts now
    now := time.Now()
    chunkSize := 300_000
    numChunks := pixelSize / chunkSize
    if pixelSize%chunkSize != 0 {
        numChunks++
    }

    for i := 0; i < numChunks; i++ {
        wg.Add(1)

        go func(i int) {
            // Loop through the pixels in the chunk
            for j := i * chunkSize; j < (i+1)*chunkSize && j < pixelSize; j++ {
                // Normalise the pixel values
                _r := normalise(fakeImage[j][0], 0, ^uint16(0))
                _g := normalise(fakeImage[j][1], 0, ^uint16(0))
                _b := normalise(fakeImage[j][2], 0, ^uint16(0))
                _a := normalise(fakeImage[j][3], 0, ^uint16(0))

                // Set the pixel values
                normalised_image[j][0] = _r
                normalised_image[j][1] = _g
                normalised_image[j][2] = _b
                normalised_image[j][3] = _a
            }

            wg.Done()
        }(i)
    }

    wg.Wait()

    elapsed := time.Since(now)
    fmt.Println("Time taken:", elapsed)
}

问题根源

  1. 内存布局与分配开销

    • Rust代码输出的是Vec<Vec<Vec<f32>>>,三层嵌套的动态向量意味着每个像素都要单独分配一个小Vec<f32>,900万个像素就会产生900万次小内存分配,这会带来极大的内存管理开销,同时内存碎片化严重,缓存命中率极低。
    • Go代码使用的是[][4]float32,固定大小的数组组成的切片,内存连续分配,不需要为每个像素单独申请内存,缓存友好性远高于Rust的实现。
  2. 额外的中间层内存拷贝
    Rust中使用par_chunks后,每个chunk还要单独收集成Vec<Vec<f32>>,最后再合并成外层Vec,多了两次内存拷贝和分配的步骤,进一步增加了耗时。

优化后的Rust代码

核心优化点:改用连续内存布局,消除小内存分配,简化处理流程。

use rand::Rng;
use std::time::Instant;
use rayon::prelude::*;

// 简化函数,固定min=0、max=u16::MAX,减少参数传递,利于编译器内联
fn normalise(value: u16) -> f32 {
    value as f32 / u16::MAX as f32
}

fn main() {
    let pixel_size = 9_000_000;
    // 可选:输入也改用固定大小数组,进一步提升缓存命中率
    let fake_image: Vec<[u16; 4]> = (0..pixel_size).map(|_| {
        let mut rng = rand::thread_rng();
        [
            rng.gen_range(0..=u16::MAX),
            rng.gen_range(0..=u16::MAX),
            rng.gen_range(0..=u16::MAX),
            rng.gen_range(0..=u16::MAX),
        ]
    }).collect();

    let now = Instant::now();

    // 直接用par_iter处理,输出固定大小数组的Vec,内存连续无额外分配
    let _normalised_image: Vec<[f32; 4]> = fake_image
        .par_iter()
        .map(|pixel| {
            [
                normalise(pixel[0]),
                normalise(pixel[1]),
                normalise(pixel[2]),
                normalise(pixel[3]),
            ]
        })
        .collect();

    let elapsed = now.elapsed();
    println!("Time elapsed: {:.2?}", elapsed);
}

优化后,Rust的性能会大幅提升,基本与Go持平甚至超过,主要原因是:

  • 消除了900万次小内存分配,内存连续,缓存命中率显著提升
  • 简化了函数逻辑,减少了不必要的参数传递,编译器更容易做内联优化
  • 去掉了中间层的chunk收集步骤,直接由Rayon自动调度并行任务,减少内存拷贝开销

内容的提问来源于stack exchange,提问作者l1901

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.14 19:16:00