【问题标题】:OpenMP overhead with a very parallel loop具有非常并行循环的 OpenMP 开销
【发布时间】:2015-11-17 22:18:15
【问题描述】:

我正在尝试使用 OpenMP 并行化一个循环,其中每次迭代都是独立的(下面的代码示例)。

!$OMP PARALLEL DO DEFAULT(PRIVATE)
do i = 1, 16

  begin = omp_get_wtime()

  allocate(array(100000000))

  do j=1, 100000000
    array(j) = j
  end do

  deallocate(array)

  end = omp_get_wtime()

  write(*,*) "It", i, "Thread", omp_get_thread_num(), "time", end - begin

end do
!$END OMP PARALLEL DO

我会排除这段代码的线性加速,每次迭代都需要与顺序版本一样多的时间,因为没有可能的竞争条件或错误共享问题。但是,我在具有 2 个 Xeon E5-2670(每个 8 个内核)的机器上获得了以下结果:

只有一个线程:

It           1 Thread           0 time  0.435683965682983     
It           2 Thread           0 time  0.435048103332520     
It           3 Thread           0 time  0.435137987136841     
It           4 Thread           0 time  0.434695959091187     
It           5 Thread           0 time  0.434970140457153     
It           6 Thread           0 time  0.434894084930420     
It           7 Thread           0 time  0.433521986007690     
It           8 Thread           0 time  0.434685945510864     
It           9 Thread           0 time  0.433223009109497     
It          10 Thread           0 time  0.434834957122803     
It          11 Thread           0 time  0.435106039047241     
It          12 Thread           0 time  0.434649944305420     
It          13 Thread           0 time  0.434831142425537     
It          14 Thread           0 time  0.434768199920654     
It          15 Thread           0 time  0.435182094573975     
It          16 Thread           0 time  0.435090065002441     

并且有 16 个线程:

It           1 Thread           0 time   1.14882898330688     
It           3 Thread           2 time   1.19775915145874     
It           4 Thread           3 time   1.24406099319458     
It          14 Thread          13 time   1.28723978996277     
It           8 Thread           7 time   1.39885497093201     
It          10 Thread           9 time   1.46112895011902     
It           6 Thread           5 time   1.50975203514099     
It          11 Thread          10 time   1.63096308708191     
It          16 Thread          15 time   1.69229602813721     
It           7 Thread           6 time   1.74118590354919     
It           9 Thread           8 time   1.78044819831848     
It          15 Thread          14 time   1.82169485092163     
It          12 Thread          11 time   1.86312794685364     
It           2 Thread           1 time   1.90681600570679     
It           5 Thread           4 time   1.96404480934143     
It          13 Thread          12 time   2.00902700424194   

知道迭代时间中的 4 倍因子来自哪里吗?

我已经使用 GNU 编译器和带有 O3 优化标志的 Intel 编译器进行了测试。

【问题讨论】:

    标签: fortran openmp


    【解决方案1】:

    运算速度

      do j=1, 100000000
        array(j) = j
      end do
    

    不受ALU 速度限制,而是受内存带宽限制。通常,现在每个可用 CPU 插槽都有多个主内存通道,但数量仍然少于内核数量。

    分配和释放也是内存访问绑定的。我不确定allocatedeallocate 是否还需要一些同步。

    出于同样的原因,STREAM 基准测试http://www.cs.virginia.edu/stream/ 提供的速度提升与纯粹的算术密集型问题不同。

    【讨论】:

    • 正确答案 +1。只是想补充一点,我在自己的代码中遇到了类似的问题,在我使内存带宽饱和后,增加线程数量的加速因素减少了。通过利用整个代码的缓存,我能够进一步优化。对于 OP 的情况,这显然无法做到,但它可能对以后偶然发现这个问题/现象的其他人有用。
    • 实际上,在这种特殊情况下,内存故障的开销是限制因素,而不是内存带宽。我猜 OP 的内核没有实现透明的大页面,因为我较慢的 E5-2650 由于 THP 比 OP 的 E5-2670 快 4 倍/8 倍。
    • 是的,我很确定分配有问题,但我不知道细节,所以只小心一句。
    【解决方案2】:

    我确定我之前已经讨论过该主题,但由于我似乎无法找到我之前的帖子,所以我又来了...

    Linux(也可能在其他平台)上的大内存分配是通过匿名内存映射处理的。也就是说,通过使用标志MAP_ANONYMOUS 调用mmap(2),在进程的虚拟地址空间中保留了一些区域。地图最初是空的 - 没有物理内存支持它们。相反,它们与所谓的零页相关联,它是物理内存中的一个只读帧,其中填充了零。由于零页不可写,因此尝试写入仍由它支持的内存位置会导致分段错误。内核通过在物理内存中找到一个空闲帧并将其与发生故障的虚拟内存页面相关联来处理故障。这个过程被称为内存故障

    内存故障是一个相对较慢的过程,因为它涉及修改进程的 PTE(页表条目)和刷新 TLB(翻译后备缓冲区)缓存。在多核和多插槽系统上,它甚至更慢,因为它涉及通过昂贵的处理器间中断使远程 TLB 失效(称为远程 TLB 击落)。释放分配会导致内存映射的移除和 PTE 的重置。因此,整个过程在下一次迭代中重复。

    确实,如果您查看串行情况下的有效内存带宽,它是(假设是一个双精度浮点数组):

    (100000000 * 8) / 0.435 = 1.71 GiB/s
    

    如果您的array 属于REALINTEGER 元素,则带宽应减半。这甚至不是the very first generation of E5-2670 提供的内存带宽。

    对于并行情况,情况更糟,因为内核在错误页面的同时锁定了页表。这就是为什么单个线程的平均带宽从 664 MiB/s 到 380 MiB/s 不等,总共为 7.68 GiB/s,这几乎比单个 CPU(和您的系统)的内存带宽慢了一个数量级。有两个,因此是可用带宽的两倍!)。

    如果将分配移到循环之外,将会出现完全不同的情况:

    !$omp parallel default(private)
    allocate(array(100000000))
    !$omp do
    do i = 1, 16
    
      begin = omp_get_wtime()
    
      do j=1, 100000000
        array(j) = j
      end do
    
      end = omp_get_wtime()
    
      write(*,*) "It", i, "Thread", omp_get_thread_num(), "time", end - begin
    
    end do
    !$omp end do
    deallocate(array)
    !$omp end parallel
    

    现在第二次和以后的迭代将产生两倍的时间(至少在 E5-2650 上)。这是因为在第一次迭代之后,所有内存都已经出现故障。对于多线程情况,增益甚至更大(将循环计数增加到 32 以让每个线程进行两次迭代)。

    内存故障时间很大程度上取决于系统配置。在启用了 THP(透明大页面)的系统上,内核会自动使用大页面来实现大映射。这将故障数量减少了 512 倍(对于 2 MiB 的大页面)。上面提到的串行情况下的 2 倍速度增益和并行情况下的 2.5 倍速度增益来自启用了 THP 的系统。在您的情况下,仅使用大页面将 E5-2650 上的第一次迭代时间减少到 1/4(如果您的数组是整数或单精度浮点数,则为 1/8)时间。

    对于较小的数组,通常情况并非如此,这些数组是通过细分更大且可重复使用的持久内存分配(称为 arena)进行分配的。 glibc 中较新的内存分配器通常每个 CPU 内核有一个 arena,以便于无锁多线程分配。

    这就是为什么许多基准测试应用程序会直接放弃第一次测量的原因。


    只是为了用实际测量来证实上述内容,我的 E5-2650 需要 0.183 秒来在已经出现故障的内存上执行串行一次迭代,并且需要 0.209 秒来执行 16 个线程(在双插槽系统上)。

    【讨论】:

    • 谢谢你非常详细的回答。
    【解决方案3】:

    他们不是独立的。 Allocate/deallocate 将共享堆。

    尝试在并行部分之外分配一个更大的数组,然后只对内存访问进行计时。

    它也是一种非统一的内存架构——如果所有分配都来自一个 cpu 的本地内存,那么来自另一个 cpu 的访问将相对较慢,因为它们是通过第一个 cpu 路由的。解决这个问题很乏味。

    【讨论】:

    • 实际上,假设 OP 运行的是 Linux,那么堆是不可能共享的。很长一段时间以来,glibc 一直通过每核内存领域提供无锁内存管理。此外,更大的内存分配直接实现为匿名映射 - 没有共享(除了进程页表)。
    • 有趣 - 这将大大降低 tc_malloc 等人的动机。你有现成的参考资料吗?
    • ptmalloc2 自 2.3.x 版以来一直是 glibc 的一部分。 (大约 2006 年)。 per-core arena 功能在源代码中存在,但最初并未在所有发行版中启用,例如绝对不在 Debian 和 Ubuntu 中。 tcmalloc 背后的最初动机是 ptmalloc2 从不在 arena 之间移动内存,现在可能仍然如此。至于对大分配使用匿名映射,这似乎是一件很常见的事情。 AFAIK,FreeBSD 使用相同的策略。
    • 太棒了。谢谢你的背景
    猜你喜欢
    • 2019-01-15
    • 1970-01-01
    • 2022-01-19
    • 2013-04-18
    • 2013-12-08
    • 1970-01-01
    • 2012-11-13
    • 1970-01-01
    相关资源
    最近更新 更多