首页 > 解决方案 > Rust 的并行扩展性差

问题描述

目前,正在学习并行化并正在调查为什么我编写的测试程序不能很好地扩展。我有一个简单的程序,它通过L迭代进行 CPU 绑定计算,并将这些迭代分布到测试中的线程数(从 1 到 8)。虽然我不期望完美的缩放(8 个线程比 1 个线程快 8 倍),但我看到的缩放似乎已经够糟糕了,我相信一定有我遗漏的东西。

我假设我的代码有问题,或者我缺少并行化的某些方面。

我觉得可以排除的事情:

  1. 正在完成的工作仅使用局部变量,因此我认为内存带宽或缓存问题不是问题。
  2. 我已经尝试过这个测试,每个线程都固定在不同的核心上,但没有看到任何改进的性能。

硬件:

Lenovo T495
Operating System: Fedora 32
KDE Plasma Version: 5.18.5
KDE Frameworks Version: 5.75.0
Qt Version: 5.14.2
Kernel Version: 5.11.13-100.fc32.x86_64
OS Type: 64-bit
Processors: 8 × AMD Ryzen 5 PRO 3500U w/ Radeon Vega Mobile Gfx
Memory: 21.5 GiB of RAM

这是我写的代码:

use std::thread;
use std::time::Instant;

fn main() {
    let loops = 10_000_000_000;

    for threads in 1..=8 {
        // As threads are added to the test, evenly split the total number of iterations
        // across all threads, so that 1 thread test can be compared to 4 thread test.
        // For `threads` that are not divisors of `loops` some threads may have one more
        // iteration than the others but that will be 1 out of 10,000,000 and should have
        // negligible effect on the run time.
        n_threads(threads, loops / threads);
    }
}

/// Have `num_threads` threads each run a function that will
/// iterate a computation `loops` times.
fn n_threads(num_threads: usize, loops: usize) {
    let sw = Instant::now();

    let mut threads = Vec::new();
    for _ in 0..num_threads {
        let t = thread::spawn(move || {
            let sw = Instant::now();
            let v = work(loops);
            (v, sw.elapsed().as_millis())
        });
        threads.push(t);
    }

    let mut durations = vec![0; num_threads];
    let mut idx = 0;
    for t in threads.into_iter() {
        let (_, dur) = t.join().unwrap();
        durations[idx] = dur;
        idx += 1;
    }
    let time = sw.elapsed();
    let avg = durations.iter().sum::<u128>() as f64 / num_threads as f64;

    println!("{}, {}, {}", num_threads, time.as_millis(), avg);
}

fn work(loops: usize) -> f64 {
    let mut x = 0.5;

    for i in 0..loops {
        x += (i as f64 / 10000.).sin();
    }

    x
}

当我运行测试时,我得到以下结果:

| Threads | Time (ms) | Scale Factor |
| -------:| ---------:| ------------:|
| 1       | 1702      |           1  |
| 2 | 993 | 1.713997986 |
| 3 | 757 | 2.248348745 |
| 4 | 650 | 2.618461538 |
| 5 | 582 | 2.924398625 |
| 6 | 495 | 3.438383838 |
| 7 | 475 | 3.583157895 |
| 8 | 455 | 3.740659341 |

这是一张图表,显示了运行测试的时间与计算线程数的变化: 运行测试与线程的时间

这是一个图表,显示了性能乘数与线程以及完美乘数: 速度乘数与线程

更新了跨线程分布的 10,000,000,000 次总迭代的测试

根据需要更长时间的测试请求,我将迭代次数增加了 100 倍。我还将时间移到线程内(并更新了上面的代码):

Thread | Avg In Thread Time | Times Faster
1 | 155564 | 1
2 | 79400.5 | 1.959232
3 | 57965 | 2.683757
4 | 47753.25 | 3.257663
5 | 42054.6 | 3.699096
6 | 40028.66667 | 3.886315
7 | 39479.28571 | 3.940396
8 | 37376.625 | 4.162067

在此处输入图像描述 在此处输入图像描述

标签: multithreadingperformancerustparallel-processing

解决方案


与 CPU 制造商希望您相信的相反,超线程物理内核不同。特别是,超线程仅在线程在任何给定时间运行不同的操作时才有效(线程可能运行相同的算法,但只有当一个线程正在等待缓存而另一个线程正在运行时,超线程才有用)。

在您的情况下,4 个物理内核上的 4 个线程的性能提高了 3.25 倍,这取决于工作和整体系统负载,这并非完全不合理。当运行超过 4 个线程时,你会得到运行在同一个核心上的线程,并且必须共享同一个 FPU,它一次只能执行一项操作,这解释了为什么你不能获得超过 4 倍的性能提升。


推荐阅读