hpc-lab-code/lab3/prime/BOTTLENECK_ANALYSIS.md
2026-01-21 18:02:30 +08:00

7.4 KiB
Raw Blame History

Prime Number MPI Program - Bottleneck and Scalability Analysis

程序瓶颈分析

1. 算法瓶颈:低效的素数检测算法

问题: 程序使用最简单的试除法检测素数,时间复杂度为 O(n²)

for ( j = 2; j < i; j++ )  // 对每个数字i需要检查i-2次
{
    if ( i % j == 0 )
    {
        prime = 0;
        break;
    }
}

影响:

  • 检测数字 2需要 0 次除法
  • 检测数字 100,000需要 99,998 次除法
  • 检测数字 1,000,000需要 999,998 次除法

改进建议:

  • 只检查到 √i 而不是 i-1可将复杂度降至 O(n√n)
  • 使用埃拉托斯特尼筛法Sieve of Eratosthenes
  • 使用更高效的算法如米勒-拉宾素性测试

2. 负载均衡瓶颈:进程间计算成本不均

问题表现:

从性能测试结果可以看到:

N值 进程数 时间(秒) 加速比 效率
100K 1 1.23 1.00x 100%
100K 2 1.32 0.96x 48%
100K 4 0.67 1.88x 47%
100K 6 0.68 1.85x 30%
100K 8 0.37 3.38x 42%

关键问题:

  • 2个进程时加速比 < 1比单进程还慢
  • 4个进程时加速比仅 1.88x(理想应该是 4x
  • 6个进程时效率仅 30%(理想应该是 100%
  • 8个进程时效率仅 42%

根本原因:

虽然程序使用循环分配策略让各进程检查相近数量的数字:

P=4时
- 进程0: 2, 6, 10, 14, ..., 99998  (25000个数字)
- 进程1: 3, 7, 11, 15, ..., 99999  (25000个数字)
- 进程2: 4, 8, 12, 16, ..., 100000 (25000个数字)
- 进程3: 5, 9, 13, 17, ..., 99997  (24999个数字)

但是! 数字大小不同,检测成本差异巨大:

  • 进程0检测的数字2, 6, 10, 14, ... (小数字,检测快)
  • 进程3检测的数字5, 9, 13, 17, ... (大数字,检测慢)

计算成本分析:

虽然各进程检查的数字数量相近,但:

  • 检测小数字如2, 3, 4只需要很少的除法运算
  • 检测大数字如99997, 99998, 99999需要大量除法运算

这导致:

  • 进程0:检测的数字最小,总计算成本最低
  • 进程P-1:检测的数字最大,总计算成本最高

实际负载分布N=100000, P=4

进程0: 检测 [2, 6, 10, ..., 99998]  → 平均数字大小 ≈ 50000
进程1: 检测 [3, 7, 11, ..., 99999]  → 平均数字大小 ≈ 50001
进程2: 检测 [4, 8, 12, ..., 100000] → 平均数字大小 ≈ 50002
进程3: 检测 [5, 9, 13, ..., 99997]  → 平均数字大小 ≈ 50001

虽然平均数字大小相近,但大数字的检测成本远高于小数字!


3. 通信瓶颈MPI_Reduce的开销

问题: 每个进程计算完成后需要调用 MPI_Reduce 汇总结果

MPI_Reduce(&total_part, &total, 1, MPI_INT, MPI_SUM, 0, MPI_COMM_WORLD);

影响:

  • 当进程数增加时,通信延迟增加
  • 对于小规模问题如N=100000通信开销占比显著

4. 同步瓶颈:进程间相互等待

问题: 由于负载不均衡,快的进程需要等待慢的进程完成

表现:

  • 进程0检测小数字很快完成
  • 进程P-1检测大数字很慢才完成
  • 所有进程必须等待最慢的进程完成才能调用 MPI_Reduce

加速比问题分析

问题12个进程时加速比 < 1

现象: 使用2个进程比单进程还慢

原因:

  1. 通信开销 > 并行收益当N=100000时问题规模较小MPI通信和同步的开销超过了并行计算的收益
  2. 负载不均衡2个进程时进程0检测偶数位置数字进程1检测奇数位置数字但奇数位置的平均数字更大检测成本更高
  3. 缓存效应:单进程可能有更好的缓存局部性

问题2效率随进程数增加而下降

现象:

  • 4进程效率 47%
  • 6进程效率 30%
  • 8进程效率 42%

原因:

  1. Amdahl定律程序中存在串行部分MPI初始化、Reduce汇总、结果打印限制了最大加速比
  2. 通信开销增加:进程数越多,通信和同步开销越大
  3. 负载不均衡加剧:进程数越多,进程间的计算成本差异越明显

问题36进程效率异常低30%

可能原因:

  1. NUMA效应6个进程可能跨越不同的CPU socket导致跨socket通信开销增加
  2. 线程调度操作系统调度6个进程到不同核心可能产生额外的上下文切换开销
  3. 内存带宽竞争6个进程同时访问内存可能导致带宽饱和

改进建议

1. 改进素数检测算法

// 改进只检查到√i
int is_prime(int n) {
    if (n < 2) return 0;
    if (n == 2) return 1;
    if (n % 2 == 0) return 0;
    
    for (int j = 3; j * j <= n; j += 2) {
        if (n % j == 0) return 0;
    }
    return 1;
}

预期效果: 将算法复杂度从 O(n²) 降至 O(n√n),可提速约 √n 倍

2. 改进负载均衡策略

方案A块分配Block Distribution

// 将数字范围分成P个连续的块
int block_size = (n - 1) / p;
int start = 2 + id * block_size;
int end = (id == p - 1) ? n : 2 + (id + 1) * block_size - 1;

for (int i = start; i <= end; i++) {
    // 检测i是否为素数
}

优点: 每个进程处理连续的数字范围,减少缓存失效 缺点: 仍然存在负载不均衡(后面的进程处理更大的数字)

方案B动态负载均衡

// 使用任务队列,进程完成一个任务后领取下一个
int current = 2;
#pragma omp critical
{
    current = next_number++;
}
if (current <= n) {
    // 检测current是否为素数
}

优点: 自动实现负载均衡 缺点: 需要同步机制,可能增加开销

方案C反向分配

// 让进程0处理大数字进程P-1处理小数字
for (int i = n - id; i >= 2; i -= p) {
    // 检测i是否为素数
}

优点: 简单,部分缓解负载不均衡 缺点: 不能完全解决问题

3. 减少通信开销

// 使用非阻塞通信
MPI_Ireduce(&total_part, &total, 1, MPI_INT, MPI_SUM, 0, 
            MPI_COMM_WORLD, &request);
// 在等待通信完成的同时做其他工作
MPI_Wait(&request, MPI_STATUS_IGNORE);

4. 优化数据局部性

// 预分配缓存,避免频繁分配
int* primes = (int*)malloc((n - 1) * sizeof(int));
int prime_count = 0;

// 批量处理,提高缓存命中率
for (int i = start; i <= end; i++) {
    if (is_prime(i)) {
        primes[prime_count++] = i;
    }
}

总结

主要瓶颈:

  1. 算法瓶颈O(n²)的素数检测算法效率低下
  2. 负载均衡瓶颈:进程间计算成本严重不均
  3. 通信瓶颈MPI_Reduce的同步开销
  4. 同步瓶颈:快进程等待慢进程

加速比问题:

  1. 小规模问题:通信开销 > 并行收益
  2. 负载不均衡:导致效率随进程数增加而下降
  3. Amdahl定律:串行部分限制了最大加速比

优先改进项:

  1. 改进算法将试除法优化到√n最优先
  2. 改进负载分配:使用块分配或动态分配
  3. 减少通信:使用非阻塞通信或减少通信频率

通过这些改进,预期可以将加速比从当前的 3.38x8进程提升到接近理想的 6-7x。