为何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) }
问题根源
内存布局与分配开销
- Rust代码输出的是
Vec<Vec<Vec<f32>>>,三层嵌套的动态向量意味着每个像素都要单独分配一个小Vec<f32>,900万个像素就会产生900万次小内存分配,这会带来极大的内存管理开销,同时内存碎片化严重,缓存命中率极低。 - Go代码使用的是
[][4]float32,固定大小的数组组成的切片,内存连续分配,不需要为每个像素单独申请内存,缓存友好性远高于Rust的实现。
- Rust代码输出的是
额外的中间层内存拷贝
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
相关产品推荐
相关产品推荐

