在 OpenMP 并行代码中,memset 并行运行有啥好处吗?
Posted
技术标签:
【中文标题】在 OpenMP 并行代码中,memset 并行运行有啥好处吗?【英文标题】:In an OpenMP parallel code, would there be any benefit for memset to be run in parallel?在 OpenMP 并行代码中,memset 并行运行有什么好处吗? 【发布时间】:2012-07-19 13:43:45 【问题描述】:我的内存块可能非常大(大于 L2 缓存),有时我必须将它们设置为零。 memset 在串行代码中很好,但是并行代码呢? 如果从并发线程调用 memset 真的可以加快大型数组的速度,有人有经验吗? 或者甚至使用简单的 openmp 并行 for 循环?
【问题讨论】:
不太可能。memset
缓存中的数据可能会受到内存带宽的限制。
在 NUMA 机器上并行运行 memset
(以及所有 MP 后 Core2 英特尔系统以及所有 MP 甚至一些 UP AMD 系统都是 NUMA)可能是您最难以-理解为什么是性能杀手,除非稍后相同的线程将只访问他们亲自归零的数组部分。
尽管如此,还是有行业标准STREAM benchmark。抓住OpenMP version,用不同数量的线程编译和运行,你自己看看。另请注意,memset()
在大多数libc
实现中都启用了 SIMD,并且已经将内存带宽推到了顶峰。
【参考方案1】:
HPC 中的人们通常说一个线程通常不足以使单个内存链接饱和,网络链接通常也是如此。 Here 是我为您编写的一个快速而肮脏的启用 OpenMP 的 memsetter,它用 2 GiB 的内存填充了两次零。以下是使用 GCC 4.7 在不同架构上具有不同线程数的结果(报告了多次运行的最大值):
GCC 4.7,使用-O3 -mtune=native -fopenmp
编译的代码:
四路 Intel Xeon X7350 - 具有独立内存控制器和前端总线的前 Nehalem 四核 CPU
单插座
threads 1st touch rewrite
1 1452.223 MB/s 3279.745 MB/s
2 1541.130 MB/s 3227.216 MB/s
3 1502.889 MB/s 3215.992 MB/s
4 1468.931 MB/s 3201.481 MB/s
(第一次触摸很慢,因为线程组是从头开始创建的,并且操作系统正在将物理页面映射到malloc(3)
保留的虚拟地址空间)
一个线程已经饱和了单个 CPU NB 链接的内存带宽。 (NB = 北桥)
每个套接字 1 个线程
threads 1st touch rewrite
1 1455.603 MB/s 3273.959 MB/s
2 2824.883 MB/s 5346.416 MB/s
3 3979.515 MB/s 5301.140 MB/s
4 4128.784 MB/s 5296.082 MB/s
需要两个线程来饱和NB 内存链接的全部内存带宽。
Octo-socket Intel Xeon X7550 - 带有八核 CPU 的 8 路 NUMA 系统(已禁用 CMT)
单插座
threads 1st touch rewrite
1 1469.897 MB/s 3435.087 MB/s
2 2801.953 MB/s 6527.076 MB/s
3 3805.691 MB/s 9297.412 MB/s
4 4647.067 MB/s 10816.266 MB/s
5 5159.968 MB/s 11220.991 MB/s
6 5330.690 MB/s 11227.760 MB/s
至少需要 5 个线程才能使一个内存链接的带宽饱和。
每个套接字 1 个线程
threads 1st touch rewrite
1 1460.012 MB/s 3436.950 MB/s
2 2928.678 MB/s 6866.857 MB/s
3 4408.359 MB/s 10301.129 MB/s
4 5859.548 MB/s 13712.755 MB/s
5 7276.209 MB/s 16940.793 MB/s
6 8760.900 MB/s 20252.937 MB/s
带宽几乎与线程数成线性关系。根据对单插槽的观察,可以说至少需要 40 个线程,每个插槽分布为 5 个线程,才能使所有 8 个内存链接饱和。
NUMA 系统上的基本问题是首次接触内存策略 - 内存分配在 NUMA 节点上,在该节点上执行第一个接触特定页面中虚拟地址的线程。线程固定(绑定到特定的 CPU 内核)在这样的系统上是必不可少的,因为线程迁移导致远程访问速度较慢。大多数 OpenMP 运行时都支持 pinnig。带有libgomp
的GCC 有GOMP_CPU_AFFINITY
环境变量,英特尔有KMP_AFFINITY
环境变量等。此外,OpenMP 4.0 引入了供应商中立的places 概念。
编辑:为完整起见,以下是在配备 Intel Core i5-2557M(双核 Sandy Bridge)的 MacBook Air 上以 1 GiB 阵列运行代码的结果具有 HT 和 QPI 的 CPU)。编译器是 GCC 4.2.1 (Apple LLVM build)
threads 1st touch rewrite
1 2257.699 MB/s 7659.678 MB/s
2 3282.500 MB/s 8157.528 MB/s
3 4109.371 MB/s 8157.335 MB/s
4 4591.780 MB/s 8141.439 MB/s
为什么即使是单线程也有这么高的速度?对gdb
的一些探索表明memset(buf, 0, len)
被OS X 编译器翻译为bzero(buf, len)
,并且bzero$VARIANT$sse42
提供了一个名为bzero$VARIANT$sse42
的启用了SSE4.2 的矢量化版本,并在运行时使用-时间。它使用MOVDQA
指令一次将16 个字节的内存归零。这就是为什么即使只有一个线程,内存带宽也几乎饱和的原因。使用 VMOVDQA
的单线程 AVX 启用版本可以一次将 32 个字节归零,并且可能会使内存链接饱和。
这里的重要信息是,有时矢量化和多线程在提高操作速度方面并不正交。
【讨论】:
感谢您提供这些结果。你如何控制“1thread/socket”或“1 socket中的所有线程”? 使用taskset
和/或设置GOMP_CPU_AFFINITY
变量。如果您安装了hwloc
,它会提供漂亮的hwloc-ls
工具。只需像hwloc-ls --taskset
一样运行它,它就会向您显示taskset
的必要位掩码,例如在单个套接字上运行。
这是一个很好的答案。但是你能解释一下为什么第一次接触和重写之间有这样的区别吗?我不完全理解您所说的“第一次触摸很慢,因为线程团队是从头开始创建的,并且操作系统正在将物理页面映射到 malloc(3) 保留的虚拟地址空间”
@Zboson,在第一次调用malloc
时,使用匿名mmap
分配内存。这会导致在进程的虚拟地址空间中进行映射,但该映射仍然不受物理 RAM 帧的支持,而是一个全零的特殊内核页面在该区域内的任何地方进行写时复制映射。因此,从新的 mmap 内存中读取会返回零。在第一次写入该区域内的某个地址时,会发生页面错误,错误处理程序会找到一个空闲的 RAM 帧并将其映射到相应的页面。
可以通过请求使用大页面或指示mmap(2)
提供预置内存(在Linux 上由MAP_POPULATE
提供;OS X 不支持预置)来减少第一次触摸的开销.在第二种情况下,对mmap
的调用将非常缓慢,但第一次触摸和重写之间的内存访问不会有任何差异。【参考方案2】:
嗯,总有 L3 缓存...
但是,这很可能已经受到主内存带宽的限制;添加更多并行性不太可能改善情况。
【讨论】:
以上是关于在 OpenMP 并行代码中,memset 并行运行有啥好处吗?的主要内容,如果未能解决你的问题,请参考以下文章
为啥 OpenMP 不并行化 vtk IntersectWithLine 代码
OpenMP 中的 C++ 动态内存分配速度较慢,即使对于非并行代码段也是如此