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