multithreading - AMD SMT或Intel HT性能

我真的不明白为什么逻辑处理器加倍的处理器比单个逻辑处理器要贵得多。 据我所知,对于6核/ 12线程CPU,在6或12个线程上运行代码没有区别。

正如猴子所问的那样,这是C#示例,它在每个线程上模拟繁重的负载:

static void Main(string[] args)
    {
        if (IntPtr.Size != 8)
            throw new Exception("use only x64 code, 2020 is coming...");

        //6 for physical cores, 12 for logical cores
        const int limit_threads = 12; 
        const int limit_actions = 256;
        const int limit_loop = 1000 * 1000 * 10;
        const double power = 1.0 / 17.0;

        long result = 0;
        var action = new Action(() =>
        {
            long value = 0;
            for (int i = 0; i < limit_loop; i++)
                value += (long)Math.Pow(i, power);

            Interlocked.Add(ref result, value);
        });

        var actions = Enumerable.Range(0, limit_actions).Select(x => action).ToArray();
        var sw = Stopwatch.StartNew();

        Parallel.Invoke(new ParallelOptions()
        {
            MaxDegreeOfParallelism = limit_threads
        }, actions);

        Console.WriteLine($"done in {sw.Elapsed.TotalSeconds}s\nresult={result}\nlimit_threads={limit_threads}\nlimit_actions={limit_actions}\nlimit_loop={limit_loop}");
    }

6个线程(AMD Ryzen 2600)的结果:

done in 13,7074543s
result=5086445312
limit_threads=6
limit_actions=256
limit_loop=10000000

12个线程的结果(AMD Ryzen 2600):

done in 11,3992756s
result=5086445312
limit_threads=12
limit_actions=256
limit_loop=10000000

使用所有逻辑内核而不是仅使用物理内核,性能几乎提高了10%,几乎为空。 您现在能说什么?

有人可以提供示例代码,与仅使用物理内核相比,这些示例代码在使用处理器多线程(AMD SMT或英特尔HT)时将更有价值?

我认为根据SMT / HT技术的可用性来改变处理器的价格只是营销策略的问题。
硬件可能在每种情况下都是相同的,但是制造商已禁用某些功能,以提供便宜的型号。

该技术依赖于以下事实:单个指令中的某些微操作必须等待执行某些操作。 因此,同一个内核不只是等待,而是使用其电路在另一个线程的微操作上取得了一些进展。
从粗略的角度来看,我们可以从在单个硬件上执行的两个不同线程(除了某些冗余部分,例如寄存器...)中,可以感知到两个(或在某些模型中,更多)微操作序列的执行。

这项技术的效率取决于问题。
经过各种测试后,我注意到如果问题是计算界的 ,即限制因素是计算(加,乘...)所需的时间,但不是内存界的 (数据已经可用,不需要等待内存) ),则此技术不会提供任何好处。
这是由于以下事实:在两个微操作序列之间没有间隙可以填充,因此两个线程的交错执行并不比两个独立的串行执行更好。
在完全相反的情况下,当问题是内存限制而不是计算限制时 ,就没有更多好处了,因为两个线程都必须等待来自内存的数据。
当问题混合在数据访问和计算之间时,我才注意到性能有所提高。 在这种情况下,当一个线程正在等待数据时,同一内核可以在另一线程的计算中取得一些进展,反之亦然。

编辑
下面给出了一个示例来说明这些情况,我获得了以下结果(多次运行时相当稳定,双Xeon E5-2697 v2,Linux 5.3.13)。

在这种内存受限的情况下,HT无法提供帮助。

$ ./prog_ht mem
24 threads running memory_task()
result: 1e+17
duration: 13.0383 seconds
$ ./prog_ht mem ht
48 threads (ht) running memory_task()
result: 1e+17
duration: 13.1096 seconds

在这种计算受限的情况下,HT可以帮助(将近30%的增益)
(我不确切知道计算cos时硬件中隐含的内容的细节,但是必须有一些不是由于内存访问而引起的延迟)

$ ./prog_ht
24 threads running compute_task()
result: -260.782
duration: 9.76226 seconds
$ ./prog_ht ht
48 threads (ht) running compute_task()
result: -260.782
duration: 7.58181 seconds

在这种混合情况下,HT可以提供更多帮助(增益约70%)

$ ./prog_ht mix
24 threads running mixed_task()
result: -260.782
duration: 60.1602 seconds
$ ./prog_ht mix ht
48 threads (ht) running mixed_task()
result: -260.782
duration: 35.121 seconds

这是源代码(在C ++中,我对C#不满意)

/*
  g++ -std=c++17 -o prog_ht prog_ht.cpp \
      -pedantic -Wall -Wextra -Wconversion \
      -Wno-missing-braces -Wno-sign-conversion \
      -O3 -ffast-math -march=native -fomit-frame-pointer -DNDEBUG \
      -pthread
*/

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <thread>
#include <chrono>
#include <cstdint>
#include <random>
#include <cmath>

#include <pthread.h>

bool // success
bind_current_thread_to_cpu(int cpu_id)
{
  /* !!!!!!!!!!!!!! WARNING !!!!!!!!!!!!!
  I checked the numbering of the CPUs according to the packages and cores
  on my computer/system (dual Xeon E5-2697 v2, Linux 5.3.13)
     0 to 11 --> different cores of package 1
    12 to 23 --> different cores of package 2
    24 to 35 --> different cores of package 1
    36 to 47 --> different cores of package 2
  Thus using cpu_id from 0 to 23 does not bind more than one thread
  to each single core (no HT).
  Of course using cpu_id from 0 to 47 binds two threads to each single
  core (HT is used).
  This numbering is absolutely NOT guaranteed on any other computer/system,
  thus the relation between thread numbers and cpu_id should be adapted
  accordingly.
  */
  cpu_set_t cpu_set;
  CPU_ZERO(&cpu_set);
  CPU_SET(cpu_id, &cpu_set);
  return !pthread_setaffinity_np(pthread_self(), sizeof(cpu_set), &cpu_set);
}

inline
double // seconds since 1970/01/01 00:00:00 UTC
system_time()
{
  const auto now=std::chrono::system_clock::now().time_since_epoch();
  return 1e-6*double(std::chrono::duration_cast
                     <std::chrono::microseconds>(now).count());
}

constexpr auto count=std::int64_t{20'000'000};
constexpr auto repeat=500;

void
compute_task(int thread_id,
             int thread_count,
             const int *indices,
             const double *inputs,
             double *results)
{
  (void)indices; // not used here
  (void)inputs; // not used here
  bind_current_thread_to_cpu(thread_id);
  const auto work_begin=count*thread_id/thread_count;
  const auto work_end=std::min(count, count*(thread_id+1)/thread_count);
  auto result=0.0;
  for(auto r=0; r<repeat; ++r)
  {
    for(auto i=work_begin; i<work_end; ++i)
    {
      result+=std::cos(double(i));
    }
  }
  results[thread_id]+=result;
}

void
mixed_task(int thread_id,
           int thread_count,
           const int *indices,
           const double *inputs,
           double *results)
{
  bind_current_thread_to_cpu(thread_id);
  const auto work_begin=count*thread_id/thread_count;
  const auto work_end=std::min(count, count*(thread_id+1)/thread_count);
  auto result=0.0;
  for(auto r=0; r<repeat; ++r)
  {
    for(auto i=work_begin; i<work_end; ++i)
    {
      const auto index=indices[i];
      result+=std::cos(inputs[index]);
    }
  }
  results[thread_id]+=result;
}

void
memory_task(int thread_id,
            int thread_count,
            const int *indices,
            const double *inputs,
            double *results)
{
  bind_current_thread_to_cpu(thread_id);
  const auto work_begin=count*thread_id/thread_count;
  const auto work_end=std::min(count, count*(thread_id+1)/thread_count);
  auto result=0.0;
  for(auto r=0; r<repeat; ++r)
  {
    for(auto i=work_begin; i<work_end; ++i)
    {
      const auto index=indices[i];
      result+=inputs[index];
    }
  }
  results[thread_id]+=result;
}

int
main(int argc,
     char **argv)
{
  //~~~~ analyse command line arguments ~~~~
  const auto args=std::vector<std::string>{argv, argv+argc};
  const auto has_arg=
    [&](const auto &a)
    {
      return std::find(cbegin(args)+1, cend(args), a)!=cend(args);
    };
  const auto use_ht=has_arg("ht");
  const auto thread_count=int(std::thread::hardware_concurrency())
                          /(use_ht ? 1 : 2);
  const auto use_mix=has_arg("mix");
  const auto use_mem=has_arg("mem");
  const auto task=use_mem ? memory_task
                          : use_mix ? mixed_task
                                    : compute_task;
  const auto task_name=use_mem ? "memory_task"
                               : use_mix ? "mixed_task"
                                         : "compute_task";

  //~~~~ prepare input/output data ~~~~
  auto results=std::vector<double>(thread_count);
  auto indices=std::vector<int>(count);
  auto inputs=std::vector<double>(count);
  std::generate(begin(indices), end(indices),
    [i=0]() mutable { return i++; });
  std::copy(cbegin(indices), cend(indices), begin(inputs));
  std::shuffle(begin(indices), end(indices), // fight the prefetcher!
               std::default_random_engine{std::random_device{}()});

  //~~~~ launch threads ~~~~
  std::cout << thread_count << " threads"<< (use_ht ? " (ht)" : "")
            << " running " << task_name << "()\n";
  auto threads=std::vector<std::thread>(thread_count);
  const auto t0=system_time();
  for(auto i=0; i<thread_count; ++i)
  {
    threads[i]=std::thread{task, i, thread_count,
                           data(indices), data(inputs), data(results)};
  }

  //~~~~ wait for threads ~~~~
  auto result=0.0;
  for(auto i=0; i<thread_count; ++i)
  {
    threads[i].join();
    result+=results[i];
  }
  const auto duration=system_time()-t0;
  std::cout << "result: " << result << '\n';
  std::cout << "duration: " << duration << " seconds\n";
  return 0;
}

TLDR:SMT / HT是一项可以抵消大规模多线程成本的技术,而不是通过更多内核来加快计算速度。

您误解了SMT / HT的功能。

“据我所知,对于6cores-12threads CPU,在6或12个线程上运行代码没有区别”。

如果是这样,则SMT / HT正常工作。

要了解原因,您需要了解现代OS内核和内核线程。 当今的操作系统使用的是抢先线程。

OS内核将每个内核分为称为“ Quantum”的时间片,并使用中断以复杂的循环方式调度各种进程。

我们要看的部分是中断。 当安排一个CPU内核切换运行另一个线程时,我们将此过程称为“上下文切换”。 上下文切换是昂贵,缓慢的过程,因为必须停止,保存高度流水线化的CPU的整个状态和流,并将其交换出另一种状态(以及其他缓存,寄存器,查找表等)。 根据此答案 ,上下文切换时间以微秒(数千个时钟周期)为单位; 而且只会随着CPU变得越来越复杂而变得更糟。

SMT / HT的目的是作弊,因为每个CPU内核都可以同时存储两个状态(想象一下,用两个监视器而不是一个监视器,您一次只能使用一个监视器,但是由于您不使用它,因此生产率更高无需在每次切换任务时重新排列窗口)。 因此,SMT / HT处理器可以使上下文切换必须比非SMT / HT处理器更快。

回到您的示例。 如果您在Ryzen 2600上关闭了SMT,然后以12个线程运行相同的工作负载,则会发现它的执行速度明显慢于6个线程。

另外,请注意,更多线程并不能使事情变得更快。

转载请注明来自askonline.tech,本文标题:multithreading - AMD SMT或Intel HT性能


 Top