Коротко о моей проблеме:
У меня есть компьютер с 2 разъемами AMD Opteron 6272 и 64 ГБ оперативной памяти.
Я запускаю одну многопоточную программу на всех 32 ядрах и получаю скорость на 15% меньше по сравнению со случаем, когда я запускаю 2 программы, каждая на одном 16-ядерном сокете.
Как сделать однопрограммную версию такой же быстрой, как две?
Больше деталей:
У меня большое количество задач и я хочу полностью загрузить все 32 ядра системы.
Поэтому я упаковываю задачи в группы по 1000 единиц. Для такой группы требуется около 120 МБ входных данных, и на одном ядре требуется около 10 секунд. Чтобы сделать тест идеальным, я копирую эти группы 32 раза и использую ITBB parallel_for
Цикл распределяет задачи между 32 ядрами.
я использую pthread_setaffinity_np
чтобы эта система не заставляла мои потоки прыгать между ядрами. И чтобы все ядра использовались последовательно.
я использую 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% больше, чем в одной программе.
Что мне не хватает?
Я считаю, что это может быть проблема в библиотеках, которые я загружаю в память только для проверки потока. Может ли это быть проблемой? Можно ли скопировать данные библиотек, чтобы они были доступны независимо на обоих сокетах?
Я предполагаю, что это распределение памяти STL / boost, которое распределяет память для ваших коллекций и т. Д. По узлам numa, потому что они не осведомлены о numa, и у вас есть потоки в программе, работающей на каждом узле.
Пользовательские распределители для всех вещей STL / boost, которые вы используете, могут помочь (но, вероятно, это огромная работа).
Возможно, вы страдаете из-за ложного совместного использования кэша: http://en.wikipedia.org/wiki/False_sharing
Ваши потоки, вероятно, совместно используют доступ к той же структуре данных через ссылку block_range. Если вам нужна скорость, вы можете передать копию каждому потоку. Если ваши данные слишком велики, чтобы поместиться в стек вызовов, вы можете динамически распределить копию каждого диапазона в разных сегментах кэша (т.е. просто убедитесь, что они достаточно далеко расположены).
Или, может быть, мне нужно увидеть остальную часть кода, чтобы понять, что вы делаете лучше.