(Я попытался упростить это, насколько мог, чтобы узнать, где я делаю что-то не так.)
Идея кода заключается в том, что у меня есть глобальный массив * v (надеюсь, что использование этого массива не замедляет работу, потоки никогда не должны иметь одинаковое значение, потому что все они работают на разных диапазонах), и я пытаюсь создать 2 потока каждый из которых сортирует первую половину, соответственно вторую половину, вызывая функцию merge_sort() с соответствующими параметрами.
В потоковом прогоне я вижу, что процесс будет 80-100% использования процессора (при двухъядерном процессоре), тогда как ни один из них не работает, он остается на уровне 50%, но время работы очень близко.
Это (релевантный) код:
//Это 2 функции сортировки, каждый поток вызывает merge_sort (..). Это проблема? оба потока, вызывающие одну и ту же (нормальную) функцию?
void merge (int *v, int start, int middle, int end) {
//dynamically creates 2 new arrays for the v[start..middle] and v[middle+1..end]
//copies the original values into the 2 halves
//then sorts them back into the v array
}
void merge_sort (int *v, int start, int end) {
//recursively calls merge_sort(start, (start+end)/2) and merge_sort((start+end)/2+1, end) to sort them
//calls merge(start, middle, end)
}
//здесь я ожидаю, что каждый поток будет создан и вызовет merge_sort в своем конкретном диапазоне (это упрощенная версия исходного кода, чтобы найти ошибку проще)
void* mergesort_t2(void * arg) {
t_data* th_info = (t_data*)arg;
merge_sort(v, th_info->a, th_info->b);
return (void*)0;
}
//в основном я просто создаю 2 потока, вызывающих указанную выше функцию
int main (int argc, char* argv[])
{
//some stuff
//getting the clock to calculate run time
clock_t t_inceput, t_sfarsit;
t_inceput = clock();
//ignore crt_depth for this example (in the full code i'm recursively creating new threads and i need this to know when to stop)
//the a and b are the range of values the created thread will have to sort
pthread_t thread[2];
t_data next_info[2];
next_info[0].crt_depth = 1;
next_info[0].a = 0;
next_info[0].b = n/2;
next_info[1].crt_depth = 1;
next_info[1].a = n/2+1;
next_info[1].b = n-1;
for (int i=0; i<2; i++) {
if (pthread_create (&thread[i], NULL, &mergesort_t2, &next_info[i]) != 0) {
cerr<<"error\n;";
return err;
}
}
for (int i=0; i<2; i++) {
if (pthread_join(thread[i], &status) != 0) {
cerr<<"error\n;";
return err;
}
}
//now i merge the 2 sorted halves
merge(v, 0, n/2, n-1);
//calculate end time
t_sfarsit = clock();
cout<<"Sort time (s): "<<double(t_sfarsit - t_inceput)/CLOCKS_PER_SEC<<endl;
delete [] v;
}
Выход (на 1 миллион значений):
Sort time (s): 1.294
Вывод с прямым вызовом merge_sort, без потоков:
Sort time (s): 1.388
Выход (на 10 миллионов значений):
Sort time (s): 12.75
Вывод с прямым вызовом merge_sort, без потоков:
Sort time (s): 13.838
Решение:
Я хотел бы поблагодарить WhozCraig и Адама, поскольку они намекали на это с самого начала.
Я использовал inplace_merge(..)
вместо своей собственной, и время выполнения программы такое же, как сейчас.
Здесь моя начальная функция слияния (не совсем уверен, что начальная, я, вероятно, несколько раз ее модифицировал, также индексы массивов могут быть ошибочными прямо сейчас, я пошел туда и обратно между [a, b] и [a, b), это была только последняя прокомментированная версия):
void merge (int *v, int a, int m, int c) { //sorts v[a,m] - v[m+1,c] in v[a,c]
//create the 2 new arrays
int *st = new int[m-a+1];
int *dr = new int[c-m+1];
//copy the values
for (int i1 = 0; i1 <= m-a; i1++)
st[i1] = v[a+i1];
for (int i2 = 0; i2 <= c-(m+1); i2++)
dr[i2] = v[m+1+i2];
//merge them back together in sorted order
int is=0, id=0;
for (int i=0; i<=c-a; i++) {
if (id+m+1 > c || (a+is <= m && st[is] <= dr[id])) {
v[a+i] = st[is];
is++;
}
else {
v[a+i] = dr[id];
id++;
}
}
delete st, dr;
}
все это было заменено на:
inplace_merge(v+a, v+m, v+c);
Редактируйте несколько раз на моем трехъядерном процессоре 3ghz:
1 миллион значений: 1 поток: 7.236 s 2 потока: 4.622 s 4 потока: 4.692 s
10 миллионов значений: 1 поток: 82.034 s 2 темы: 46.189 s 4 темы: 47.36 s
Одна вещь, которая поразила меня: "динамически создает 2 новых массива [...]". Поскольку для обоих потоков потребуется память из системы, им необходимо приобрести замок для этого, что вполне может быть вашим узким местом. В частности, идея выделения микроскопических массивов звучит ужасно неэффективно. Кто-то предложил сортировку на месте, которая не требует дополнительного хранилища, что намного лучше для производительности.
Другое дело, часто забытое стартовое полупредложение для любых измерений сложности большой O: "Существует n0, так что для всех n> n0...". Другими словами, возможно, вы еще не достигли n0? Недавно я увидел видео (надеюсь, кто-то еще его запомнит), где некоторые люди пытались определить этот предел для некоторых алгоритмов, и их результаты заключались в том, что эти ограничения на удивление высоки.
Примечание: поскольку OP использует Windows, мой ответ ниже (который неправильно принял Linux) может не применяться. Я оставил его ради тех, кто мог бы найти полезную информацию.
clock()
- неправильный интерфейс для измерения времени в Linux: он измеряет время процессора, используемое программой (см. http://linux.die.net/man/3/clock), которое в случае нескольких потоков является суммой Время процессора для всех потоков. Вам нужно измерить прошедшее время или время разгона. См. Более подробную информацию в этом вопросе SO: C: использование clock() для измерения времени в многопоточных программах, в котором также указано, какой API можно использовать вместо clock()
.
В реализации на основе MPI, с которой вы пытаетесь сравнить, используются два разных процесса (то, что MPI обычно включает параллелизм), а время процессора второго процесса не включено, поэтому время процессора близко к времени разгона. Тем не менее, все еще неправильно использовать время процессора (и, следовательно, clock()
) для измерения производительности даже в последовательных программах; по какой-то причине, если программа ждет, например, сетевого события или сообщения из другого процесса MPI, он по-прежнему проводит время, но не процессорное время.
Обновление. В Microsoft, использующем библиотеку времени выполнения C, clock()
возвращает время настенных часов, так что это нормально для использования в ваших целях. Это неясно, если вы используете Microsoft toolchain или что-то еще, например Cygwin или MinGW.
merge
по-прежнему последовательно. Какая доля времени расходуется на этапахmerge_sort
иmerge
?