2013-11-13 4 views
12

Коротко о моей проблеме:Многопоточность: Почему две программы лучше, чем одна?

У меня есть компьютер с 2 гнездами AMD Opteron 6272 и 64 ГБ оперативной памяти.

Я запускаю одну многопоточную программу на всех 32 ядрах и получаю скорость на 15% меньше по сравнению со случаем, когда я запускаю 2 программы, каждая на одной 16-ядерной розетке.

Как сделать однопрограммную версию так же быстро, как две программы?


Подробнее:

У меня есть большое количество задач, и вы хотите, чтобы полностью загрузить все 32 ядра системы. Таким образом, я собираю задачи группами по 1000. Такая группа требует около 120 МБ входных данных и занимает около 10 секунд для завершения одного ядра. Чтобы сделать идеальный тест, я копирую эти группы 32 раза, а цикл ITBB parallel_for распределяет задачи между 32 ядрами.

Я использую pthread_setaffinity_np, чтобы обеспечить, чтобы мои потоки не перескакивали между ядрами. И чтобы гарантировать, что все ядра используются consequtively.

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

Так что код выглядит следующим образом:

void operator()(const blocked_range<size_t> &range) const 
    { 
    for(unsigned int i = range.begin(); i != range.end(); ++i){ 

     pthread_t I = pthread_self(); 
     int s; 
     cpu_set_t cpuset; 
     pthread_t thread = I; 
     CPU_ZERO(&cpuset); 
     CPU_SET(threadNumberToCpuMap[i], &cpuset); 
     s = pthread_setaffinity_np(thread, sizeof(cpu_set_t), &cpuset); 

     mlockall(MCL_FUTURE); // lock virtual memory to stay at physical address where it was allocated 

     TaskManager manager; 
     for (int j = 0; j < fNTasksPerThr; j++){ 
     manager.SetData(&(InpData->fInput[j])); 
     manager.Run(); 
     } 
    } 
    } 

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

void operator()(const blocked_range<size_t> &range) const 
    { 
    for(unsigned int i = range.begin(); i != range.end(); ++i){ 

     pthread_t I = pthread_self(); 
     int s; 
     cpu_set_t cpuset; 
     pthread_t thread = I; 
     CPU_ZERO(&cpuset); 
     CPU_SET(threadNumberToCpuMap[i], &cpuset); 
     s = pthread_setaffinity_np(thread, sizeof(cpu_set_t), &cpuset); 

     mlockall(MCL_FUTURE); // lock virtual memory to stay at physical address where it was allocated 
     InpData[i].fInput = new ProgramInputData[fNTasksPerThr]; 

     for(int j=0; j<fNTasksPerThr; j++){ 
     InpData[i].fInput[j] = InpDataPerThread.fInput[j]; 
     } 
    } 
    } 

Теперь я бегу все это на 32 ядер и увидеть скорость ~ 1600 задач в секунду.

Затем я создаю две версии программы, а также с taskset и pthread гарантировать, что первый запуск на 16 ядрах первого разъема и второй - на втором гнезде. Я бегу их один рядом друг с другом, используя просто & команду в оболочке:

program1 & program2 & 

Каждая из этих программ достигает скорости ~ 900 задач/с. В общей сложности это> 1800 задач/с, что на 15% больше, чем однопрограммная версия.

Что я пропущу?

Я считаю, что это может быть проблемой в библиотеках, которые я загружаю только в память только для мелочей. Это может быть проблема? Могу ли я копировать данные библиотек, чтобы они были доступны независимо в обоих сокетах?

+0

Вы пробовали 32 однопоточных программы? –

+0

32 однопоточных программ не будут иметь дело с проблемой, которая, скорее всего, будет распределяться по памяти в неправильном узле numa. У него только 2 узла, поэтому ему нужны только 2 программы, каждая из которых привязана к одному узлу. –

+2

Numa node ?? Я понятия не имею, что это такое, но это звучит так хорошо, что я собираюсь это выяснить. – Dennis

ответ

3

Я бы предположил, что это распределение памяти STL/boost, распространяющее память для ваших коллекций и т. Д. Через узлы numa из-за того, что они не знают numa, и у вас есть потоки в программе, запущенной на каждом узле.

Пользовательские распределители для всех функций STL/boost, которые вы используете, могут помочь (но, скорее всего, это огромная работа).

+0

Не нужно ли использовать mlockall (MCL_FUTURE)? Этот http://linux.die.net/man/2/mlock говорит, что он должен помочь для всех будущих распределений памяти. – klm123

+1

Я ожидаю, что libs выделит память до этого, а также, поскольку они не имеют ничего общего с numa, они, вероятно, повторно используют память между коллекциями или внутренне. Использование пользовательских контейнеров - это, вероятно, путь IMHO. –

+0

Вы, кажется, правы. Минимизация использования std :: vector :: reserve Мне удалось уменьшить разницу во времени до 2%. – klm123

1

Вы могли бы страдать плохой случай ложного разделения кэша: http://en.wikipedia.org/wiki/False_sharing

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

Или, может быть, мне нужно увидеть остальную часть кода, чтобы понять, что вы делаете лучше.

+0

Я не уверен, что понимаю вас. Какие данные вы говорите? block_range - очень маленькая структура, и она не используется внутри программы (TaskManger). Все данные, которые используются, я уже скопировал и динамически присвоил. – klm123

+0

Хорошо. Ты прав. Я неправильно понял цель и характер block_range. Я думал, что это обычные данные, которыми вы манипулировали. Теперь я понимаю, что это шаблон TBB для целых интервалов. Виноват. Как определяется InpData? –

Смежные вопросы