.net 为什么一个完全CPU绑定的进程在超线程中工作得更好?

72qzrwbm  于 2023-06-25  发布在  .NET
关注(0)|答案(4)|浏览(140)

给出:

  • 一个完全由CPU限制的非常大的(即,超过几个CPU周期)作业,以及
  • 具有四个物理核心和总共8个逻辑核心的CPU,

8、16和28个线程的性能是否优于4个线程?我知道,四个线程将执行较少的上下文切换和较少的开销,比8、16或28个线程将在四物理核机器上。不过,时间是-

Threads    Time Taken (in seconds)
   4         78.82
   8         48.58
   16        51.35
   28        52.10

下面的Original Question部分提到了用于测试获取计时的代码。CPU规格也在底部给出。

原始问题

  • 当我们说 *

超线程的工作原理是复制处理器的某些部分(存储体系结构状态的部分),但不复制主要的执行资源。这允许超线程处理器作为通常的“物理”处理器和主机操作系统额外的“逻辑”处理器出现

  • ?*

This question今天在SO上被问及,它测试了多个线程做相同工作的性能。代码如下:

private static void Main(string[] args)
{
    int threadCount;
    if (args == null || args.Length < 1 || !int.TryParse(args[0], out threadCount))
        threadCount = Environment.ProcessorCount;

    int load;
    if (args == null || args.Length < 2 || !int.TryParse(args[1], out load))
        load = 1;

    Console.WriteLine("ThreadCount:{0} Load:{1}", threadCount, load);
    List<Thread> threads = new List<Thread>();
    for (int i = 0; i < threadCount; i++)
    {
        int i1 = i;
        threads.Add(new Thread(() => DoWork(i1, threadCount, load)));
    }

    var timer = Stopwatch.StartNew();
    foreach (var thread in threads) thread.Start();
    foreach (var thread in threads) thread.Join();
    timer.Stop();

    Console.WriteLine("Time:{0} seconds", timer.ElapsedMilliseconds/1000.0);
}

static void DoWork(int seed, int threadCount, int load)
{
    var mtx = new double[3,3];
    for (var i = 0; i < ((10000000 * load)/threadCount); i++)
    {
         mtx = new double[3,3];
         for (int k = 0; k < 3; k++)
            for (int l = 0; l < 3; l++)
              mtx[k, l] = Math.Sin(j + (k*3) + l + seed);
     }
}

(我已经删除了几个大括号,以便将代码放在一个页面上,以便快速阅读。)
我在我的机器上运行了这个代码来复制这个问题。我的机器有四个物理内核和八个逻辑内核。上面代码中的方法DoWork()完全是CPU绑定的。**我觉得超线程可能有助于30%的加速(因为这里我们有与物理核心一样多的CPU绑定线程(即4))。但它几乎实现了64%的性能提升。**当我在四个线程中运行此代码时,大约花了82秒,当我在8、16和28个线程中运行此代码时,所有情况下运行都在大约50秒内。
要总结计时:

Threads    Time Taken (in seconds)
   4         78.82
   8         48.58
   16        51.35
   28        52.10

我可以看到,使用四个线程时,CPU使用率约为50%。* 不应该是100%吗?* 毕竟,我的处理器只有四个物理核心。8和16线程的CPU使用率约为100%。
我试图理解 * 为什么一个完全CPU绑定的进程在超线程中工作得更好?*.
为了完成

  • 我有英特尔酷睿i7-4770 CPU@3.40 GHz,3401 MHz,4核心,8逻辑处理器.
  • 我在Release模式下运行代码。
  • 我知道计时的方式很糟糕。这只会给最慢的线程给予时间。我从另一个问题中得到了代码。
i7uaboj4

i7uaboj41#

CPU流水线

每一条指令都必须经过pipeline中的几个步骤才能完全执行。至少,它必须被解码,发送到执行单元,然后在那里实际执行。现代CPU上有几个执行单元,它们可以完全并行地执行指令。顺便说一下,执行单元是不可互换的:一些操作只能在单个执行单元上完成。例如,存储器加载通常专用于一个或两个单元,存储器存储专门发送到另一个单元,所有的计算由一些其他单元完成。
了解管道,我们可能会想:如果我们写纯顺序代码,每条指令都要经过这么多流水线阶段,CPU怎么能工作得这么快?答案如下:处理器以out-of-order方式执行指令。它有一个大的重排序缓冲区(例如对于200条指令),并且它通过其流水线并行推送许多指令。如果在任何时刻某个指令由于任何原因不能执行(等待来自慢速存储器的数据,取决于尚未完成的其他指令,无论如何),那么它将被延迟一些周期。在此期间,处理器执行一些新的指令,这些指令位于我们的代码中的延迟指令之后,假定它们不以任何方式依赖于延迟指令。
现在我们可以看到latency的问题。即使一条指令被解码并且它的所有输入都已经可用,它也需要几个周期才能完全执行。这种延迟称为指令延迟。然而,我们知道,此时处理器可以执行许多其他独立指令,如果有的话。
如果指令从L2高速缓存加载数据,则必须等待大约10个周期才能加载数据。如果数据仅位于RAM中,那么将其加载到处理器将需要数百个周期。在这种情况下,我们可以说指令具有高延迟。此时执行一些其他独立的操作对于最大性能是很重要的。这有时被称为“延迟隐藏”。
最后,我们不得不承认,大多数真实的代码本质上是顺序的。它有一些独立的指令并行执行,但不是太多。没有指令执行会导致pipeline bubbles,并且会导致处理器晶体管的低效使用。另一方面,两个不同线程的指令在几乎所有情况下都是自动独立的。这直接将我们引向了超线程的概念。

**P.S.**你可能需要阅读Agner Fog's manual来更好地理解现代CPU的内部结构。

超线程

当在单个核上以超线程模式执行两个线程时,处理器可以交错它们的指令,从而允许用第二线程的指令填充来自第一线程的气泡。这允许更好地利用处理器的资源,特别是在普通程序的情况下。请注意,HT不仅在您有大量内存访问时有帮助,而且在严重顺序代码中也有帮助。一个良好优化的计算代码可以充分利用CPU的所有资源,在这种情况下,你将看到HT的没有利润(例如dgemm例程来自良好优化的BLAS)。

**P.S.**您可能需要阅读英特尔的detailed explanation of hyper-threading,包括有关哪些资源被复制或共享的信息,以及有关性能的讨论。

上下文切换

上下文是CPU的内部状态,至少包括所有寄存器。当执行线程改变时,OS必须进行上下文切换(详细描述here)。根据this answer,上下文切换大约需要10微秒,而调度器的时间数量是10毫秒或更多(参见here)。所以上下文切换不会对总时间产生太大影响,因为它们很少发生。请注意,在某些情况下,线程之间对CPU缓存的竞争会增加交换机的有效成本。
然而,在超线程的情况下,每个核心内部具有两种状态:两组寄存器、共享高速缓存、一组执行单元。因此,当您在4个物理内核上运行8个线程时,操作系统不需要进行任何上下文切换。当您在四核上运行16个线程时,会执行上下文切换,但它们只占用总时间的一小部分,如上所述。

进程管理器

说到进程管理器中看到的CPU利用率,它并没有衡量CPU管道的内部结构。Windows只能注意到线程何时将执行返回到OS,以便:睡眠、等待互斥锁、等待HDD以及其他缓慢的事情。因此,它认为如果有线程在内核上工作,而线程不休眠或等待任何事情,它就充分使用了。例如,您可以检查运行无限循环while (true) {}是否导致CPU的充分利用。

bmp9r5qi

bmp9r5qi2#

我可以看到,CPU使用率约为50%,有4个线程。不是应该是100%吗?
不,不应该。
在4个物理核的机器上运行4个CPU绑定线程时,CPU使用率为50%的理由是什么?
这只是Windows中CPU利用率的报告方式(顺便说一下,至少在其他一些操作系统上也是如此)。HT CPU在操作系统中显示为两个核心,并报告为两个核心。
因此,当你有四个HT CPU时,Windows会看到一个八核机器。如果您查看任务管理器中的“性能”选项卡,您将看到八个不同的CPU图表,并且计算总CPU利用率时,使用100%的利用率是这八个核心的全部利用率。
如果你只使用四个线程,那么这些线程就不能充分利用可用的CPU资源 *,这就解释了时间 *。他们最多可以使用八个可用核心中的四个,因此当然,您的利用率将最大化为50%。一旦超过逻辑核心的数量(8),运行时间再次增加;在这种情况下,您将增加调度开销,而不会增加任何新的计算资源。
对了...
HyperThreading与过去的共享缓存和其他限制相比有了很大的改进,但它仍然无法提供与完整CPU相同的吞吐量优势,因为CPU内部仍然存在一些争用。因此,即使忽略操作系统开销,你35%的速度改进对我来说似乎相当不错。我经常看到,在计算瓶颈的过程中添加额外的HT内核,速度不超过20%。

zzzyeukh

zzzyeukh3#

我无法解释你所观察到的加速速度:100%似乎对超线程的改进太多了。但我可以解释一下这些原则。
超线程的主要好处是当处理器必须在线程之间切换时。每当线程数多于CPU内核数时(正确率为99.9997%),并且操作系统决定切换到不同的线程时,它必须执行(大部分)以下步骤:
1.保存当前线程的状态:这包括堆栈、寄存器的状态和程序计数器。它们保存在哪里取决于体系结构,但一般来说,它们要么保存在缓存中,要么保存在内存中。无论如何,这一步都需要时间。
1.将线程置于“就绪”状态(与“运行”状态相反)。
1.加载下一个线程的状态:再次,包括堆栈、寄存器和程序计数器,这再次是一个花费时间的步骤。
1.将线程翻转到“运行”状态。
在普通(非HT)CPU中,它的核心数量是处理单元的数量。每一个都包含寄存器,程序计数器(寄存器),堆栈计数器(寄存器),(通常)单独的缓存和完整的处理单元。因此,如果一个普通的CPU有4个核心,它可以同时运行4个线程。当一个线程完成时(或者操作系统已经决定它花费了太多的时间,需要等待轮到它再次启动),CPU需要遵循这四个步骤来卸载线程并加载到新线程中,然后才能开始执行新线程。
另一方面,在超线程CPU中,上述情况也成立,但除此之外,每个核心都有一组重复的寄存器、程序计数器、堆栈计数器和(有时)缓存。这意味着4核CPU仍然只能同时运行4个线程,但是
CPU可以在复制的寄存器上“预加载”线程**。因此,4个线程正在运行,但8个线程加载到CPU上,4个活动,4个非活动。然后,当CPU切换线程的时候,而不是必须在线程需要切换的时刻执行加载/卸载,它简单地“切换”哪个线程是活动的,并在后台对新的“非活动”寄存器执行卸载/加载。还记得我用“这些步骤需要时间”作为后缀的两个步骤吗?在超线程系统中,步骤2和4是唯一需要实时执行的步骤,而步骤1和3是在硬件中的后台执行的(与线程或进程或CPU内核的任何概念分离)。
现在,这个过程并不能完全加快多线程软件的速度,但是在线程通常具有非常小的工作负载的环境中,线程切换的数量可能是昂贵的。即使在不符合该范例的环境中,也可以从超线程中获益。
如果你需要任何澄清,请告诉我。CS250已经有几年了,所以我可能把术语弄混了。如果我用错词了请告诉我我有99.9997%的把握,我所描述的一切就其运作的逻辑而言都是准确的。

f45qwnt8

f45qwnt84#

超线程通过在处理器执行流水线中交错指令来工作。当处理器在一个“线程”上执行读写操作时,它会在另一个“线程”上执行逻辑计算,使它们保持分离,并使性能加倍。
您获得如此大的加速的原因是因为在您的DoWork方法中没有分支逻辑。这是一个很大的循环,具有非常可预测的执行序列。
处理器执行流水线必须经过若干时钟周期来执行单个计算。处理器尝试通过用接下来的几条指令预加载执行缓冲器来优化性能。如果加载的指令实际上是一个条件跳转(例如if语句),这是一个坏消息,因为处理器必须刷新整个流水线并从内存的不同部分获取指令。
你可能会发现,如果你把if语句放在你的DoWork方法中,你不会得到100%的加速。

相关问题