【问题标题】:Creation of std::thread slows down main program by 50%std::thread 的创建使主程序速度减慢 50%
【发布时间】:2015-04-21 04:59:41
【问题描述】:

仅仅创建一个线程并加入它会使主线程的执行速度减慢 50%。正如您在下面的示例中看到的那样,线程什么都不做,但仍然对性能有显着影响。我认为这可能是与功率/频率缩放相关的问题,所以我在创建线程后尝试休眠,但无济于事。下面的程序如果编译使用

g++ -std=c++11 -o out thread_test.cpp -pthread

显示结果

Before thread() trial 0 time: 312024526 ignore -1593025974
Before thread() trial 1 time: 243018707 ignore -494037597
Before thread() trial 2 time: 242929293 ignore 177714863
Before thread() trial 3 time: 242935290 ignore 129069571
Before thread() trial 4 time: 243113945 ignore 840242475
Before thread() trial 5 time: 242824224 ignore -1635749271
Before thread() trial 6 time: 242809490 ignore -1256215542
Before thread() trial 7 time: 242910180 ignore -555222712
Before thread() trial 8 time: 235645414 ignore 537501443
Before thread() trial 9 time: 235746347 ignore 118363977
After thread() trial 0 time: 567509646 ignore 223146324
After thread() trial 1 time: 476450035 ignore -393907838
After thread() trial 2 time: 476377789 ignore -1678874628
After thread() trial 3 time: 476377012 ignore -1015350122
After thread() trial 4 time: 476185152 ignore 2034280344
After thread() trial 5 time: 476420949 ignore -1647334529
After thread() trial 6 time: 476354679 ignore 441573900
After thread() trial 7 time: 476120322 ignore -1576726357
After thread() trial 8 time: 476464850 ignore -895798632
After thread() trial 9 time: 475996533 ignore -997590921

而所有的试验都应该是相同的速度。

编辑:使用 rdtsc() 进行时间测量,使用更大的持续时间,使用计算结果

thread_test.cpp:

#include <ctime>
#include <thread>
#include <iostream>

int dorands(){
  int a =0;
  for(int i=0; i<10000000; i++){
   a +=rand();
  }
  return a;
}

inline uint64_t rdtsc(){
  uint32_t lo, hi;
  __asm__ __volatile__ (
    "xorl %%eax, %%eax\n"
    "cpuid\n"
    "rdtscp\n"
    : "=a" (lo), "=d" (hi)
    :
    : "%ebx", "%ecx" );
  return (uint64_t)hi << 32 | lo;
}


int foo(){return 0;}

int main(){

  uint64_t begin;
  uint64_t end;

  for(int i = 0; i< 10; i++){
    begin= rdtsc();
    volatile int e = dorands();
    end = rdtsc();
    std::cout << "Before thread() trial "<<i<<" time: " << end-begin << " ignore " << e << std::endl;;
  }

  std::thread t1(foo);
  t1.join();

  for(int i = 0; i< 10; i++){
    begin= rdtsc();
    volatile int e = dorands();
    end = rdtsc();
    std::cout << "After thread() trial "<<i<<" time: " << end-begin << " ignore " << e << std::endl;;
  }

  return 1;
}

【问题讨论】:

标签: c++ multithreading performance


【解决方案1】:

std::rand() 是 C rand(),在 glibc 下,invokes __random()__random()invokes __libc_lock_lock() and __libc_lock_unlock(),我不认为如果我们深入研究该代码,我们会发现锁在创建线程之前基本上是无操作的。

【讨论】:

  • 我想获得一个证明,但是 glibc 是一堆宏和条件,以至于我达到了我的 SO 回答时间和精力阈值。 :)
  • 通过 perf 注释源代码显示创建线程后时间花费在:lock cmpxchg %esi,0x384b74(%rip)
  • tl;dr rand() 针对单线程操作进行了优化,一旦产生更多线程,它就会减慢一点,而是使用以 rand() 为种子的线程本地/本地状态随机生成器(或更好)。
【解决方案2】:

我认为您遇到了一个基本问题:至少在典型的多任务操作系统上,有一个范围从大约(比如说)几毫秒到一秒左右,在这个范围内很难获得有意义的时序测量。

对于极短的序列,您可以使用时钟计数器(例如 x86 上的 RDTSC)并运行几次。如果在运行期间发生任务切换,它会真的很糟糕,因为运行时间比其他运行时间长很多倍。

这指出了真正的问题:一旦你到达一个序列(比如你的序列),它需要足够长的时间,几乎可以肯定在它运行时至少会发生一个任务切换,那么你就会遇到一个问题:任务切换所浪费的时间会大大缩短时间。特别是,如果在一次运行期间发生了任务切换,但在另一次运行期间没有发生,则可能会使第二次看起来比第一次快得多。

最终,您会遇到耗时足够长的任务,以至于所有任务都包含多个任务切换,因此由于任务切换的数量而产生的差异几乎被忽略了。

注意:理论上,clock 应该只测量 CPU 时间,而不是挂钟时间。实际上,要完全排除所有任务切换时间几乎是不可能的。

您的测试展示(或可能展示,无论如何)另一个相当基本的问题:您的dorand() 计算了一些东西,但没有(例如)打印出结果。一个足够智能的编译器可能(很容易)能够推断出它基本上没有影响,并且基本上完全排除了它。

即使您从dorand 打印出结果,您也没有播种随机数生成器,因此每次运行都需要产生相同的结果。同样,一个足够智能的编译器可以计算出这一点,并在编译时计算出正确的结果,然后打印出三个正确的结果。为了防止我们可以(作为一种可能性)在每次运行时以不同的方式播种随机数——通常的方法是检索当前时间,并将其传递给srand

为了消除(或至少减少)这些问题,我们可以像这样重写代码:

#include <ctime>
#include <thread>
#include <iostream>

long long int dorands(){
  long long int a =0;
  for(int i=0; i<100000000; i++){
    a +=rand();
  }
  return a;
}

int foo(){return 0;}

int main(){
    srand(time(NULL));
  clock_t begin = clock();
  long long int e = dorands();
  clock_t end = clock();
  std::cout << "ignore: " << e << ", trial 1 time: " << end-begin << std::endl;;

  begin = clock();
  e = dorands();
  end = clock();
  std::cout << "ignore: " << e << ", trial 2 time: " << end - begin << std::endl;;

  std::thread t1(foo);
  t1.join();

  begin = clock();
  e = dorands();
  end = clock();
  std::cout << "ignore: " << e << ", trial 3 time: " << end - begin << std::endl;;

  begin = clock();
  e = dorands();
  end = clock();
  std::cout << "ignore: " << e << ", trial 4 time: " << end - begin << std::endl;;


  return 1;
}

这里我打印了从dorand 返回的值,所以编译器不能完全跳过对rand 的调用。我还增加了dorand 中的数字,因此每个试验至少运行一秒钟(在我的计算机上,它们无论如何都会运行)。

运行它,我得到这样的结果:

ignore: 1638407535924, trial 1 time: 1519
ignore: 1638386748597, trial 2 time: 1455
ignore: 1638433228933, trial 3 time: 1433
ignore: 1638288863328, trial 4 time: 1491

在这个特定的运行中,第一次试验(平均而言)比第二次试验慢,但有足够的变化和重叠,我们可能很安全地猜测这只是噪音——如果平均速度有任何真正的差异,它太小了,我们无法测量。

【讨论】:

  • 奇怪的是,如果 rand() 被具有 local 状态的 PRNG 替换,例如 std::minstd_rand,我无法重现该问题,即使时间安排在与rand() 相同的球场。 (我想知道 glibc 的rand() 实现中使用的锁是否与它有关。)
  • @dyp:很难猜——我不确定有什么可以开始的。当我第一次下载并运行他的代码时,我得到的结果与他的非常接近。然后我再次运行它,结果几乎完全相反。我并不完全相信他所看到的不仅仅是噪音。
  • 不确定你在哪台机器上运行你的代码,但在 Linux Xeon E3-1231 上我得到更高的数字(忽略)、更高的时间和与 OP 一致的结果。
  • 我已经编辑了上面的代码来解决您的问题,但仍然有无法解释的结果。
  • 顺便说一句:我已经在我的第一个版本中使用了 volatile e 以避免 g++ 优化对 rand 的调用
猜你喜欢
  • 2021-07-11
  • 2011-06-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-05-28
  • 2014-09-28
  • 1970-01-01
相关资源
最近更新 更多