执行摘要: 如何在他的代码中指定OpenMP应该只为REAL内核使用线程,即不计算超线程的线程?
详细分析:多年来,我在空闲时间编写了一个仅限SW的开源渲染器(光栅化器/光线跟踪器)。 GPL代码和Windows二进制文件可从此处获得: https://www.thanassis.space/renderer.html 它在Windows,Linux,OS / X和BSD下编译并运行良好。
上个月我引入了光线追踪模式 - 生成的图片的质量飙升。不幸的是,光线跟踪比光栅化要慢几个数量级。为了提高速度,就像我对光栅化器一样,我为光线跟踪器添加了OpenMP(和TBB)支持 - 以便轻松利用额外的CPU内核。光栅化和光线跟踪都很容易进行线程化(每个三角形工作 - 每像素工作)。
在家中,凭借我的Core2Duo,第二核心帮助了所有模式 - 光栅化和光线跟踪模式的加速都在1.85x和1.9x之间。
问题:当然,我很想看到CPU的最高性能(我也用GPU“玩”preliminary CUDA port),所以我想要一个坚实的比较基础。我把代码交给了我的一个好朋友,他可以使用16英寸1500美元英特尔超级处理器的“野兽”机器。
他在“最重”模式,光线跟踪模式......
中运行它......他的速度是我的Core2Duo的速度的五分之一(!)
喘气 - 恐怖。刚刚发生了什么?我们开始尝试不同的修改,补丁......最终我们想出来了。
通过使用OMP_NUM_THREADS环境变量,可以控制生成的OpenMP线程数。 随着线程数从1增加到8,速度增加(接近线性增加)。 在我们越过8的那一刻,速度开始减弱,直到我的Core2Duo速度的五分之一,当使用所有16个核心时!
为什么8?
因为8是真实核心的数量。其他8个是......超线程的!
理论:现在,这对我来说是新闻 - 我看到超线程在其他算法中帮助很多(高达25%),所以这是出乎意料的。显然,即使每个超线程核心都有自己的寄存器(和SSE单元?),光线跟踪器也无法利用额外的处理能力。这让我想到了......
可能不是处理能力过剩 - 它是内存带宽。
光线跟踪器使用边界体积层次结构数据结构来加速光线三角形交叉点。如果使用超线程核心,则一对中的每个“逻辑核心”试图从该数据结构中的不同位置(即在存储器中)读取 - 并且CPU高速缓存(每对本地)完全被打破。至少,这是我的理论 - 任何建议都是最受欢迎的。
所以,问题是: OpenMP检测到“核心”的数量,并产生与之匹配的线程 - 也就是说,它包含计算中的超线程“核心”。就我而言,这显然会导致灾难性的结果,速度方面。有没有人知道如何使用OpenMP API(如果可能,可移植)只为REAL内核生成线程,而不是超线程的线程?
P.S。代码是开放的(GPL),可在上面的链接中找到,随时可以在您自己的机器上重现 - 我猜这将在所有超线程CPU中发生。
P.P.S。请原谅这篇文章的篇幅,我认为这是一种教育经历,想分享。
答案 0 :(得分:6)
基本上,您需要一些相当便携的方式来查询环境以获得相当低级别的硬件细节 - 通常,您不能仅仅通过系统调用来执行此操作(操作系统通常不会意识到硬件线程和芯)。
支持多种平台的一个库是hwloc - 支持Linux& windows(和其他人),intel& amd芯片。 Hwloc将让您了解有关硬件拓扑的所有信息,并了解内核和硬件线程之间的区别(称为PU - 处理单元 - 以hwloc术语表示)。所以你在开始时调用这个库,找到实际内核的数量,并调用omp_set_num_threads()(或者只是在并行部分的开头添加该变量作为指令)。
答案 1 :(得分:3)
不幸的是,您对这种情况发生原因的假设很可能是正确的。可以肯定的是,你必须使用一个配置文件工具 - 但我之前已经看过光线跟踪,所以这并不奇怪。在任何情况下,目前都无法从OpenMP中确定某些处理器是“真实的”而某些处理器是超线程的。您可以编写一些代码来确定这一点,然后自己设置数字。但是,仍然存在OpenMP不在处理器本身上调度线程的问题 - 它允许操作系统执行此操作。
OpenMP ARB语言委员会一直致力于为用户确定标准方式,以确定其环境并说明如何运行。目前,这一讨论仍在继续。许多实现允许您通过使用实现定义的环境变量将线程“绑定”到处理器。但是,用户必须知道处理器编号以及哪些处理器是“真实的”与超线程的。
答案 2 :(得分:1)
问题是OMP如何使用HT。 这不是内存带宽! 我在2.6GHz HT PIV上尝试了简单的循环。 结果令人惊讶......
使用OMP:
$ time ./a.out
4500000000
real 0m28.360s
user 0m52.727s
sys 0m0.064s
没有OMP: $ time ./a.out 45亿
real0 m25.417s
user 0m25.398s
sys 0m0.000s
代码:
#include <stdio.h>
#define U64 unsigned long long
int main() {
U64 i;
U64 N = 1000000000ULL;
U64 k = 0;
#pragma omp parallel for reduction(+:k)
for (i = 0; i < N; i++)
{
k += i%10; // last digit
}
printf ("%llu\n", k);
return 0;
}