multithreading - Rust 的并行扩展性差

标签 multithreading performance rust parallel-processing

目前,正在学习并行化并正在调查为什么我编写的测试程序不能很好地扩展。我有一个简单的程序,通过 L 进行 CPU 密集型计算迭代并将这些迭代分布到测试中的线程数(从 1 到 8)。虽然我不期望完美的缩放(8 个线程比 1 个线程快 8 倍),但我看到的缩放似乎已经够糟糕了,我相信一定有我遗漏的东西。
我假设我的代码有问题,或者我缺少并行化的某些方面。
我觉得可以排除的事情:

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

  • 硬件:
    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 |
    
    这是一张图表,显示了运行测试的时间与计算线程数的变化:
    Time to Run Test vs Threads
    这是一个图表,显示了性能乘数与线程以及完美乘数:
    Speed Multiplier vs Threads
    更新了跨线程分布的 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
    
    enter image description here
    enter image description here

    最佳答案

    与 CPU 制造商希望您相信的相反,超线程与物理内核不同。特别是,超线程仅在线程在任何给定时间运行不同的操作时才有效(线程可能运行相同的算法,但只有当一个线程正在等待缓存而另一个线程正在运行时,超线程才有用)。
    在您的情况下,4 个物理内核上的 4 个线程的性能提高了 3.25 倍,这取决于工作和整体系统负载,这并非完全不合理。当运行超过 4 个线程时,你会得到运行在同一个核心上的线程,并且必须共享同一个 FPU,它一次只能执行一项操作,这解释了为什么你不能获得超过 4 倍的性能提升。

    关于multithreading - Rust 的并行扩展性差,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/67183632/

    相关文章:

    ruby-on-rails - Rails - 从 Controller 运行 rake 任务或新线程?

    javascript - _gaq.push( ['_trackPageLoadTime' ]) 是如何工作的?

    Rust 借用语义字符串

    c - 如何用 Rust 包装现有的 C 函数或如何从 Rust 调用 C 函数?

    rust - 我如何解构匹配语法::ptr::P?

    c++ - 重现/调试一些多线程 hell

    multithreading - 锁定和解锁互斥体的效率如何?互斥体的成本是多少?

    c++ - 在 C++ 中使用 set_jmp/longjmp 不起作用

    c# - 线程. sleep (0) : What is the normal behavior?

    java - 自己的 SeqLock 实现,避免自旋锁会更好吗?