超线程技术使我的渲染器变慢了10倍

18

总结:在代码中如何指定OpenMP仅使用真实核心的线程,即不包括超线程?

详细分析:多年来,我在业余时间编写了一个纯软件、开源的渲染器(光栅化/光线追踪器) 。该GPL代码和Windows二进制文件可以从以下链接获取:https://www.thanassis.space/renderer.html。它可以在Windows、Linux、OS/X和BSD下编译和运行。

最近一个月,我引入了一种光线追踪模式,生成图片的质量突飞猛进。不幸的是,射线跟踪比光栅化慢几个数量级。为了提高速度,就像对光栅化程序所做的那样,我将OpenMP(和TBB)支持添加到射线追踪器中,以便于利用额外的CPU核心。无论光栅化还是光线追踪都很容易实现线程化(三角形工作 - 每个像素的工作)。

在家里,我的Core2Duo,第二个核心帮助所有模式——光栅化和光线追踪模式都得到了1.85倍到1.9倍的加速。

问题:当然,我很想看到最高的CPU性能(我还“玩”GPU,CUDA端口),所以我想要一个可靠的基础进行比较。我把代码交给了一个好朋友,他有一个拥有16个核心、1500美元的英特尔超级处理器的“怪物”机器。

他在“最重”的模式下运行它,即射线追踪模式......

......但他得到的速度只有我的Core2Duo的五分之一(!)

哇——太可怕了。发生了什么?

我们开始尝试不同的修改、补丁,... 最终我们找到了解决方法。

通过使用OMP_NUM_THREADS环境变量,可以控制生成多少个OpenMP线程。

随着线程数量从1增加到8,速度也在增加(接近线性增长)。但是一旦超过了8,速度开始下降,当使用所有16个核心时,速度甚至只有我的Core2Duo的五分之一!为什么是8呢?因为8是实际的核心数,另外8个是超线程核心!这就是理论:我对此感到很意外——我曾经看到超线程在其他算法中有很大帮助(高达25%),所以这个结果出乎意料。显然,尽管每个超线程核心都配备了自己的寄存器(和SSE单元?),光线追踪器无法利用额外的处理能力。这让我想到...可能不是处理能力被限制了,而是内存带宽被限制了。光线追踪器使用边界体分层数据结构来加速射线-三角形相交。如果使用超线程核心,则每个成对的“逻辑核心”都试图从该数据结构的不同位置(即内存中)读取 - 并且CPU缓存(每对本地)完全被压垮了。至少,这是我的理论-欢迎任何建议。那么问题来了:OpenMP检测“核心”的数量并生成相应的线程-也就是说,它将超线程“核心”包含在计算中。在我的情况下,这显然导致了灾难性的结果,速度方面。有人知道如何使用OpenMP API(如果可能的话)仅为实际核心而不是超线程核心生成线程吗?P.S.代码是开放的(GPL),并可在上面的链接处获得,在自己的机器上自由复制——我猜这会在所有超线程CPU上发生。P.P.S.请原谅我发布的长度,我认为这是一次教育经历,并想分享。

这篇文章有一些有用的答案。"https://dev59.com/EXVC5IYBdhLWcg3w51lv" - Dan
不幸的是,它们并没有提供太多帮助 - 它们都报告了一个包括超线程“核心”的数字... - ttsiodras
1
我发现“超线程”对许多应用程序来说可能是垃圾。由于应用程序不再正常运行或性能大大降低,我在许多情况下已经关闭了它(在BIOS中)。这不仅仅是英特尔的问题(在Power上也看到过)。 - Marm0t
3个回答

6
基本上,您需要一种相对便捷的方式来查询环境中较低级别的硬件细节,通常情况下,您无法仅通过系统调用实现这一点(操作系统通常甚至不知道硬件线程和核心之间的差异)。
一个支持多个平台的库是hwloc - 支持Linux和Windows(等等),英特尔和AMD芯片。Hwloc将让您找到关于硬件拓扑结构的所有信息,并知道核心和硬件线程(在hwloc术语中称为PU - 处理单元 - )之间的区别。所以你可以在开始时调用这个库,找到实际核心数量,并调用omp_set_num_threads()(或者只需将该变量作为指令添加到并行部分的开头)。

3
很遗憾,你的假设很可能是正确的。要确定,你需要使用配置工具——但我以前也见过这种光线追踪的情况,所以并不奇怪。无论如何,目前没有办法从OpenMP中确定某些处理器是“真实的”,还是超线程的。你可以编写一些代码来确定这一点,然后自己设置数量。然而,仍然存在一个问题,那就是OpenMP不会自己调度处理器上的线程——它允许操作系统来完成这个任务。
在OpenMP ARB语言委员会中,已经有一些工作正在进行,试图定义一个标准方式,使用户能够确定他的环境并告诉如何运行。目前,这个讨论仍在继续。许多实现允许你通过使用实现定义的环境变量来“绑定”线程到处理器上。然而,用户必须知道处理器编号以及哪些处理器是“真实的”,哪些是超线程的。

谢谢 - 我想我必须回到pthread_create,CreateThread和Co....并且自己实现“#pragma parallel for schedule(dynamic,N)”在可移植性方面吗?这不会有趣... - ttsiodras

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 4500000000

    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;
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接