为什么完全CPU绑定的流程能够更好地处理超线程?

时间:2015-09-11 19:48:19

标签: c# .net multithreading performance hyperthreading

假设:

  • 完全CPU绑定非常大(即多个CPU周期)作业,
  • 具有4个物理核心和8个逻辑核心的CPU,

8,16和28个线程可能比4个线程表现更好吗?我的理解是 4个线程将执行较小的上下文切换,并且在任何意义上将具有较小的开销,而不是8,16或28个线程将具有4物理核心机。但是,时间是 -

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

以下原始问题部分中提到了用于测试获取时间的代码。 CPU规格也在底部给出。

在阅读了各个用户提供的答案以及评论中给出的信息后,我终于可以将问题归结为我上面写的内容。如果上述问题为您提供完整的上下文,则可以跳过下面的原始问题。

原始问题

当我们说

时意味着什么
  

超线程的工作原理是复制某些部分   处理器 - 存储架构状态但不复制的处理器   主要的执行资源。这允许超线程处理器   看起来像通常的"物理"处理器和额外的"逻辑"   处理器到主机操作系统

今天在SO上询问了{p> This question,它基本上测试了执行相同工作的多个线程的性能。它具有以下代码:

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

(我已经删除了一些大括号,将代码放在一个页面中以便快速阅读。)

我在我的机器上运行此代码以复制问题。我的机器有4个物理核心和8个逻辑核心。上面代码中的方法DoWork()完全受CPU限制。 我觉得超线程可能会导致30%的加速(因为这里我们拥有与物理内核一样多的CPU绑定线程(即4))。但它几乎可以获得64%的性能提升。当我为4个线程运行此代码时,花了大约82秒,当我运行8,16和28个线程的代码时,它在所有情况下运行50秒。

总结时间:

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

我可以看到4个线程的CPU使用率约为50%。 不应该是~100%?毕竟我的处理器只有4个物理内核。 8线程和16线程的CPU使用率约为100%。

如果某人可以在开始时解释引用的文本,我希望能更好地理解超线程,并希望得到的答案为什么完全CPU绑定的进程会更好地工作超线程?

为了完成,

  • 我有英特尔酷睿i7-4770 CPU @ 3.40 GHz,3401 MHz,4个核心,8个逻辑处理器。
  • 我在发布模式下运行了代码。
  • 我知道计时的方式很糟糕。这只会给出最慢线程的时间。我从其他问题中获取了代码。但是,在4物理核心机器上运行4个CPU绑定线程时,50%CPU使用率的理由是什么?

4 个答案:

答案 0 :(得分:8)

  

我可以看到4个线程的CPU使用率约为50%。不应该是~100%?

不,它不应该。

  

在4个物理核心机器上运行4个CPU绑定线程时,50%CPU使用率的理由是什么?

这就是Windows中报告CPU利用率的方式(顺便说一下,至少在某些其他操作系统上也是如此)。 HT CPU显示为操作系统的两个核心,并按此报告。

因此,当你有四个HT CPU时,Windows会看到一台八核机器。如果查看任务管理器中的“性能”选项卡,您将看到八个不同的CPU图,并且计算总CPU利用率,100%利用率是这八个核的完全利用率。

如果您只使用四个线程,则这些线程无法充分利用可用的CPU资源并解释了时间。它们最多可以使用八个可用核心中的四个,因此当然您的利用率最高可达50%。 一旦超过逻辑核心数(8),运行时间再次增加;您正在添加调度开销而不添加任何新的计算资源。


顺便说一句......

从远旧的共享缓存和其他限制开始,HyperThreading已经有了很大的改进,但它仍然永远不会提供与完整CPU相同的吞吐量优势,因为CPU内部仍存在一些争用。所以即使忽略操作系统开销,你的速度提高35%对我来说也是相当不错的。我经常看到不超过20%的速度将额外的HT内核添加到计算瓶颈的过程中。

答案 1 :(得分:6)

CPU管道

每条指令必须经过pipeline中的几个步骤才能完全执行。至少,它必须被解码,发送到执行单元,然后在那里实际执行。现代CPU上有几个执行单元,它们可以完全并行执行指令。顺便说一句,执行单元可互换:某些操作只能在单个执行单元上完成。例如,内存加载通常专用于一个或两个单元,内存存储专门发送到另一个单元,所有计算都由其他单元完成。

了解管道,我们可能想知道:如果我们编写纯粹的后续代码并且每条指令都要经历如此多的流水线阶段,CPU如何能够如此快速地工作?答案就是:处理器以out-of-order方式执行指令。它有一个大的重新排序缓冲区(例如200条指令),它并行地通过其管道推送许多指令。如果在任何时候由于任何原因无法执行某些指令(等待来自慢速存储器的数据,取决于尚未完成的其他指令,则无论如何),它会延迟一些周期。在此期间,处理器执行一些新指令,这些指令位于我们代码中的延迟指令之后,因为它们不以任何方式依赖于延迟指令。

现在我们可以看到latency的问题。即使指令被解码并且其所有输入都已经可用,也需要几个周期才能完全执行。此延迟称为指令延迟。但是,我们知道,此时处理器可以执行许多其他独立指令,如果有的话。

如果指令从L2缓存加载数据,则必须等待大约10个周期才能加载数据。如果数据仅位于RAM中,则需要数百个周期才能将其加载到处理器。在这种情况下,我们可以说该指令具有高延迟。此时必须执行一些其他独立操作,以获得最大性能。这有时称为延迟隐藏

最后,我们不得不承认大多数真正的代码本质上是后续的。它有一些独立的指令可以并行执行,但不能太多。没有执行指令会导致pipeline bubbles,导致处理器晶体管的低效使用。另一方面,几乎在所有情况下,两个不同线程的指令自动独立。这直接引导我们超线程的想法。

P.S。您可能需要阅读Agner Fog's manual以更好地了解现代CPU的内部结构。

超线程

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

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

上下文切换

上下文是CPU的内部状态,至​​少包括所有寄存器。当执行线程改变时,OS必须进行上下文切换(详细描述here)。根据{{​​3}},上下文切换大约需要10微秒,而调度程序的时间量是10毫秒或更长(参见this answer)。因此,上下文切换不会对总时间产生太大影响,因为它们很少完成。请注意,在某些情况下,线程之间CPU缓存的竞争会增加交换机的有效成本。

但是,在超线程的情况下,每个内核在内部都有两个状态:两组寄存器,共享缓存,一组执行单元。因此,当您在4个物理内核上运行8个线程时,操作系统无需执行任何上下文切换。当您在四核上运行16个线程时,将执行上下文切换,但它们占用总时间的一小部分,如上所述。

流程经理

说到您在流程管理器中看到的CPU利用率,它不会测量CPU管道的内部。 Windows只能注意到线程何时将执行返回到OS以便:休眠,等待互斥,等待硬盘,以及执行其他慢速操作。因此,它认为如果有一个线程正在处理它就会完全使用一个核心,它不会休眠或等待任何事情。例如,您可以检查运行无限循环while (true) {}是否可以充分利用CPU。

答案 2 :(得分:4)

我无法解释你所观察到的超速加速:100%似乎对超线程的改进太过分了。但我可以解释原则。

超线程的主要好处是处理器必须在线程之间切换。每当有多个线程而不是CPU核心(真正的99.9997%的时间)并且操作系统决定切换到另一个线程时,它必须执行(大部分)以下步骤:

  1. 保存当前线程的状态:这包括堆栈,寄存器的状态和程序计数器。他们得到保存的地方取决于体系结构,但一般来说,他们要么保存在缓存中,要么保存在内存中。无论哪种方式,此步骤需要时间
  2. 将线程放入&#34; Ready&#34;国家(而不是&#34;运行&#34;州)。
  3. 再次加载下一个线程的状态:包括堆栈,寄存器和程序计数器,这又是一个需要时间的步骤
  4. 将线程翻转为&#34;运行&#34;状态。
  5. 在普通(非HT)CPU中,它拥有的核心数是处理单元的数量。它们中的每一个都包含寄存器,程序计数器(寄存器),堆栈计数器(寄存器),(通常)单独的高速缓存和完整的处理单元。因此,如果普通CPU有4个内核,它可以同时运行4个线程。当一个线程完成(或者操作系统已经决定它花费太多时间并且需要等待轮到它再次启动)时,CPU需要按照这四个步骤来卸载线程并加载新线程在开始执行新的之前。

    另一方面,在HyperThreading CPU中,上述情况属实,但此外,每个核心都有一组重复的寄存器,程序计数器,堆栈计数器和(有时)缓存。这意味着4核CPU仍然只能同时运行4个线程,但 CPU可以预先加载&#34;重复寄存器上的线程。所以4个线程正在运行,但是8个线程被加载到CPU上,4个活动,4个非活动。然后,当CPU切换线程的时间,而不是在线程需要切换时必须执行加载/卸载时,它只需切换&#34;切换&#34;哪个线程是活动的,并在新的&#34;非活动&#34;后台执行卸载/加载。寄存器。记住我后缀为&#34的两个步骤;这些步骤需要时间&#34;?在超线程系统中,步骤2和4是唯一需要实时执行的步骤,而步骤1和3则在硬件的后台执行(与线程或进程或CPU内核的任何概念分开)。

    现在,这个过程并没有完全加速多线程软件,但是在线程通常具有非常小的工作负载且它们执行频繁的环境中,线程交换机的数量可能很昂贵。即使在不符合该范例的环境中,超线程也可以带来好处。

    如果您需要任何澄清,请与我们联系。自CS250以来已经过了几年,所以我可能会在这里或那里混淆术语;如果我使用了错误的术语,请告诉我。我99.9997%肯定我所描述的所有内容在其运作方式的逻辑方面都是准确的。

答案 3 :(得分:3)

超线程通过在处理器执行流水线中交错指令来工作。虽然处理器正在一个线程上执行读写操作&#39;它正在对另一个线程进行逻辑评估,将它们分开,让你感觉性能翻倍。

您获得如此大的加速的原因是因为DoWork方法中没有分支逻辑。它是一个具有非常可预测的执行顺序的大循环。

处理器执行流水线必须经过几个时钟周期才能执行单次计算。处理器尝试通过使用接下来的几条指令预加载执行缓冲区来优化性能。如果加载的指令实际上是条件跳转(例如if语句),这是个坏消息,因为处理器必须刷新整个管道并从内存的不同部分获取指令。

您可能会发现,如果您在if方法中添加DoWork语句,则不会获得100%的加速...