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

В скором времени о моей проблеме:

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

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

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


Подробнее:

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

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

Я использую 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% больше, чем однопрограммная версия.

Что мне не хватает?

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

Ответ 1

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

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

Ответ 2

Возможно, у вас плохой случай ложного обмена кешем: http://en.wikipedia.org/wiki/False_sharing

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

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