我尝试使用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
知道迭代时间中的4x因子来自何处?
我已经使用GNU编译器和带有O3优化标志的英特尔编译器进行了测试。
答案 0 :(得分:4)
操作速度
do j=1, 100000000
array(j) = j
end do
不受ALU速度的限制,而是受内存带宽的限制。通常,现在每个CPU插槽可用的主内存有几个通道,但仍然比内核数小。
分配和释放也是内存访问绑定。我不确定allocate
和deallocate
是否还需要一些同步。
出于同样的原因,STREAM基准http://www.cs.virginia.edu/stream/提供的速度与纯粹的算术密集型问题不同。
答案 1 :(得分:3)
我确定我之前已经介绍过这个话题,但由于我似乎无法找到我之前的帖子,所以我再来一次......
Linux上(以及可能在其他平台上)的大内存分配是通过匿名内存映射处理的。也就是说,通过使用标记mmap(2)
调用MAP_ANONYMOUS
,可以在进程的虚拟地址空间中保留一些区域。这些地图最初是空的 - 没有物理内存备份它们。相反,它们与所谓的零页相关联,这是一个填充零的物理内存中的只读帧。由于零页面不可写,因此尝试写入仍由其支持的内存位置会导致分段错误。内核通过在物理内存中查找空闲帧并将其与发生故障的虚拟内存页面相关联来处理故障。此过程称为故障内存。
故障记忆是一个相对缓慢的过程,因为它涉及对过程的修改。 PTE(页表条目)和TLB(Translation Lookaside Buffer)缓存的刷新。在多核和多插槽系统上,它甚至更慢,因为它涉及通过昂贵的处理器间中断使远程TLB(称为远程TLB击落)失效。释放分配导致移除存储器映射和重置PTE。因此,整个过程在下一次迭代中重复进行。
实际上,如果你看看你的串行情况下的有效内存带宽,它是(假设一个双精度浮点数组):
(100000000 * 8) / 0.435 = 1.71 GiB/s
如果您的array
属于REAL
或INTEGER
元素,则应将带宽减半。即使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内核有一个竞技场,以便于无锁多线程分配。
这就是为什么许多基准测试应用程序只是丢掉第一次测量的原因。
为了通过实际测量来证实上述情况,我的E5-2650需要0.183秒才能在已发生故障的内存上进行一次迭代,并且需要0.209秒来执行16个线程(在双插槽系统上)。 / p>
答案 2 :(得分:1)
他们不是独立的。 Allocate / deallocate将共享堆。
尝试在并行部分之外分配一个更大的数组,然后只对内存访问进行计时。
它也是一种非统一内存架构 - 如果所有分配都来自一个cpu的本地内存,那么来自其他cpu的访问在通过第一个cpu路由时会相对较慢。解决这个问题非常繁琐。