Создание std:: thread замедляет основную программу на 50%

просто создавая поток, и объединение его замедляет выполнение основного потока на 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

тогда как все испытания должны иметь одинаковую скорость.

EDIT: используйте 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;
}

Ответы

Ответ 1

std::rand() - это C rand(), который под glibc вызывает __random(). __random() вызывает __libc_lock_lock() и __libc_lock_unlock(), и я не думаю, что это растягивает воображение, что если мы углубимся в этот код, мы найдем что блокировки по существу являются no-op, пока не будет создан поток.

Ответ 2

Я думаю, что вы столкнулись с основной проблемой: по крайней мере, в типичной многозадачной операционной системе существует диапазон от (скажем) от нескольких миллисекунд до секунды или около того, в пределах которого трудно получить значимые временные измерения.

Для чрезвычайно коротких последовательностей вы можете использовать счетчик часов (например, RDTSC на x86) и запускать его несколько раз. Если во время прогона происходит переключение задачи, оно будет очень сильно торчать, поскольку выполнение выполняется в много раз дольше, чем остальные.

Это указывает на реальную проблему: как только вы дойдете до последовательности (например, вашей), которая занимает достаточно много времени, и почти наверняка произойдет, что по крайней мере один переключатель задачи будет работать во время его запуска, тогда вы столкнетесь с проблемой: время, потерянное для переключения задачи, может значительно сократить время. В частности, если переключатель задач происходит во время одного прогона, но не во время другого, он может сделать второй, значительно быстрее, чем первый.

В конце концов вы попадаете на задачи, которые занимают достаточно много времени, и все они включают в себя несколько переключателей задач, поэтому разница между количеством переключателей задач в значительной степени теряется в шуме.

Примечание: теоретически clock должен измерять только время процессора, а не время настенных часов. В действительности практически невозможно полностью исключить все время переключения задачи.

Ваш тест демонстрирует (или может продемонстрировать, в любом случае) еще одну довольно основную проблему: ваш 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

В этом конкретном прогоне первые испытания медленнее (в среднем), чем вторые испытания, но есть достаточно вариаций и перекрытий, что мы, вероятно, довольно безопасно угадываем, что это просто шум - если есть какая-то реальная разница в средней скорости, это слишком мало для нас, чтобы измерить.