Подтвердить что ты не робот

Масштабируемое распределение больших областей памяти (8 МБ) на архитектурах NUMA

В настоящее время мы используем график потока TBB, в котором: a) параллельный фильтр обрабатывает массив (параллельно с смещениями) и помещает обработанные результаты в промежуточный вектор (выделенный в куче, в основном вектор будет расти до 8 МБ), Затем эти векторы передаются узлам, которые затем обрабатывают эти результаты по их характеристикам (определенным в а)). Из-за синхронизированных ресурсов для каждого признака может быть только один node. Прототип, который мы написали, хорошо работает на архитектурах UMA (проверен на одном процессоре Ivy Bridge и Sandy Bridge). Однако приложение не масштабируется в нашей архитектуре NUMA (4 CPU Nehalem-EX). Мы привязали проблему к распределению памяти и создали минимальный пример, в котором у нас есть параллельный конвейер, который просто выделяет память из кучи (через malloc 8-мегабайтного куска, а затем memset в области 8 МБ, аналогично тому, что будет делать первоначальный прототип) вплоть до определенного объема памяти. Наши результаты:

  • В архитектуре UMA приложение масштабируется линейно с количеством потоков, используемых конвейером (устанавливается через task_scheduler_init)

  • В архитектуре NUMA, когда мы привязываем приложение к одному сокету (используя numactl), мы видим одно и то же линейное масштабирование

  • В NUMA architecutre, когда мы используем несколько сокетов, время запуска нашего приложения увеличивается с количеством сокетов (отрицательный линейный масштаб - "вверх" )

Для нас это пахнет кучей раздора. До сих пор мы пытались заменить масштабируемый распределитель Intel TBB для распределителя glibc. Однако первоначальная производительность в одном сокете хуже, чем при использовании glibc, производительность нескольких сокетов не ухудшается, а также не улучшается. получил тот же эффект, используя tcmalloc, распределитель кладов и выровненный распределитель кэша TBB.

Вопрос: если кто-то испытал подобные проблемы. Выделение стека не является для нас вариантом, поскольку мы хотим сохранить выделенные кучи векторы даже после того, как конвейер побежал. Как одна куча может эффективно распределять области памяти в размере МБ на архитектурах NUMA из нескольких потоков? Мы действительно хотели бы использовать подход динамического выделения вместо предварительного распределения памяти и управления ею в приложении.

Я приложил перфорированную статистику для различных исполнений с numactl. Interleaving/localalloc не имеет никакого эффекта (шина QPI не является узким местом, мы проверили, что с PCM загрузка канала QPI составляет 1%). Я также добавил диаграмму, изображающую результаты для glibc, tbbmalloc и tcmalloc.

perf stat bin/prototype 598,867

Статистика счетчиков производительности для "bin/prototype":

  12965,118733 task-clock                #    7,779 CPUs utilized          
        10.973 context-switches          #    0,846 K/sec                  
         1.045 CPU-migrations            #    0,081 K/sec                  
       284.210 page-faults               #    0,022 M/sec                  
17.266.521.878 cycles                    #    1,332 GHz                     [82,84%]
15.286.104.871 stalled-cycles-frontend   #   88,53% frontend cycles idle    [82,84%]
10.719.958.132 stalled-cycles-backend    #   62,09% backend  cycles idle    [67,65%]
 3.744.397.009 instructions              #    0,22  insns per cycle        
                                         #    4,08  stalled cycles per insn [84,40%]
   745.386.453 branches                  #   57,492 M/sec                   [83,50%]
    26.058.804 branch-misses             #    3,50% of all branches         [83,33%]

   1,666595682 seconds time elapsed

perf stat numactl --cpunodebind = 0 bin/prototype 272,614

Статистика счетчика производительности для 'numactl --cpunodebind = 0 bin/prototype':

   3887,450198 task-clock                #    3,345 CPUs utilized          
         2.360 context-switches          #    0,607 K/sec                  
           208 CPU-migrations            #    0,054 K/sec                  
       282.794 page-faults               #    0,073 M/sec                  
 8.472.475.622 cycles                    #    2,179 GHz                     [83,66%]
 7.405.805.964 stalled-cycles-frontend   #   87,41% frontend cycles idle    [83,80%]
 6.380.684.207 stalled-cycles-backend    #   75,31% backend  cycles idle    [66,90%]
 2.170.702.546 instructions              #    0,26  insns per cycle        
                                         #    3,41  stalled cycles per insn [85,07%]
   430.561.957 branches                  #  110,757 M/sec                   [82,72%]
    16.758.653 branch-misses             #    3,89% of all branches         [83,06%]

   1,162185180 seconds time elapsed

perf stat numactl --cpunodebind = 0-1 bin/prototype 356,726

Статистика счетчиков производительности для 'numactl --cpunodebind = 0-1 bin/prototype':

   6127,077466 task-clock                #    4,648 CPUs utilized          
         4.926 context-switches          #    0,804 K/sec                  
           469 CPU-migrations            #    0,077 K/sec                  
       283.291 page-faults               #    0,046 M/sec                  
10.217.787.787 cycles                    #    1,668 GHz                     [82,26%]
 8.944.310.671 stalled-cycles-frontend   #   87,54% frontend cycles idle    [82,54%]
 7.077.541.651 stalled-cycles-backend    #   69,27% backend  cycles idle    [68,59%]
 2.394.846.569 instructions              #    0,23  insns per cycle        
                                         #    3,73  stalled cycles per insn [84,96%]
   471.191.796 branches                  #   76,903 M/sec                   [83,73%]
    19.007.439 branch-misses             #    4,03% of all branches         [83,03%]

   1,318087487 seconds time elapsed

perf stat numactl --cpunodebind = 0-2 bin/protoype 472,794

Статистика счетчиков производительности для 'numactl --cpunodebind = 0-2 bin/prototype':

   9671,244269 task-clock                #    6,490 CPUs utilized          
         7.698 context-switches          #    0,796 K/sec                  
           716 CPU-migrations            #    0,074 K/sec                  
       283.933 page-faults               #    0,029 M/sec                  
14.050.655.421 cycles                    #    1,453 GHz                     [83,16%]
12.498.787.039 stalled-cycles-frontend   #   88,96% frontend cycles idle    [83,08%]
 9.386.588.858 stalled-cycles-backend    #   66,81% backend  cycles idle    [66,25%]
 2.834.408.038 instructions              #    0,20  insns per cycle        
                                         #    4,41  stalled cycles per insn [83,44%]
   570.440.458 branches                  #   58,983 M/sec                   [83,72%]
    22.158.938 branch-misses             #    3,88% of all branches         [83,92%]

   1,490160954 seconds time elapsed

Минимальный пример: скомпилирован с g++ - 4.7 std = С++ 11 -O3 -march = native; выполняется с numactl --cpunodebind = 0... numactl --cpunodebind = 0-3 - с привязкой к ЦПУ мы имеем следующий вывод: 1 CPU (скорость x), 2 процессора (скорость ~ x/2), 3 процессора (скорость ~ x/3) [скорость = чем выше, тем лучше]. Итак, мы видим, что производительность ухудшается с количеством процессоров. Связывание памяти, чередование (--interleave = all) и -localalloc не имеют никакого эффекта здесь (мы контролировали все QPI-ссылки, а загрузка ссылок была ниже 1% для каждой ссылки).

#include <tbb/pipeline.h>
#include <tbb/task_scheduler_init.h>
#include <chrono>
#include <stdint.h>
#include <iostream>
#include <fcntl.h>
#include <sstream>
#include <sys/mman.h>
#include <tbb/scalable_allocator.h>
#include <tuple>

namespace {
// 8 MB
size_t chunkSize = 8 * 1024 * 1024;
// Number of threads (0 = automatic)
uint64_t threads=0;
}

using namespace std;
typedef chrono::duration<double, milli> milliseconds;

int main(int /* argc */, char** /* argv */)
{
   chrono::time_point<chrono::high_resolution_clock> startLoadTime = chrono::high_resolution_clock::now();
   tbb::task_scheduler_init init(threads==0?tbb::task_scheduler_init::automatic:threads);
   const uint64_t chunks=128;
   uint64_t nextChunk=0;
   tbb::parallel_pipeline(128,tbb::make_filter<void,uint64_t>(
         tbb::filter::serial,[&](tbb::flow_control& fc)->uint64_t
   {
      uint64_t chunk=nextChunk++;
      if(chunk==chunks)
         fc.stop();

      return chunk;
   }) & tbb::make_filter<uint64_t,void>(
         tbb::filter::parallel,[&](uint64_t /* item */)->void
   {
        void* buffer=scalable_malloc(chunkSize);
        memset(buffer,0,chunkSize);
   }));

   chrono::time_point<chrono::high_resolution_clock> endLoadTime = chrono::high_resolution_clock::now();
   milliseconds loadTime = endLoadTime - startLoadTime;
   cout << loadTime.count()<<endl;
}

Обсуждение форумов Intel TBB: http://software.intel.com/en-us/forums/topic/346334

4b9b3361

Ответ 1

Второе обновление (закрытие вопроса):

Просто профилируйте приложение примера с ядром 3.10.

Результаты параллельного распределения и memsetting из 16 ГБ данных:

маленькие страницы:

  • 1 socket: 3112.29 ms
  • 2 socket: 2965.32 ms
  • 3 socket: 3000.72 ms
  • 4 socket: 3211.54 ms

огромные страницы:

  • 1 socket: 3086,77 мс
  • 2 socket: 1568,43 мс
  • 3 socket: 1084.45 ms
  • 4 socket: 852.697 мс

Теперь масштабируемая проблема распределения исправлена ​​- по крайней мере, для огромных страниц.

Ответ 2

Краткое описание и частичный ответ для описанной проблемы: Вызов malloc или scalable_malloc не является узким местом, узким местом являются скорее ошибки страницы, вызванные memset ting выделенной памятью. Нет никакой разницы между glibc malloc и другими масштабируемыми распределителями, такими как Intel TBB scalable_malloc: для распределений, превышающих определенный порог (обычно 1 МБ, если ничего нет free d; может быть определено madvise), память будет быть распределены с помощью аномального mmap. Первоначально все страницы карты указывают на внутреннюю страницу ядра, которая предварительно задана и доступна только для чтения. Когда мы помещаем память, это вызывает исключение (разумеется, что страница ядра доступна только для чтения) и ошибка страницы. В это время будет добавлена ​​новая страница. Маленькие страницы - 4 КБ, поэтому это произойдет 2048 раз для 8 МБ буфера, который мы выделяем и пишем. Я измерил, что эти ошибки страниц не так дорого стоят на однопроцессорных машинах, но становятся все более и более дорогими на машинах NUMA с несколькими процессорами.

Решения, которые я придумал до сих пор:

  • Использование огромных страниц: помогает, но только задерживает проблему.

  • Используйте предварительно выделенный и предварительно сгенерированный (или memset или mmap + MAP_POPULATE) регион памяти (пул памяти) и выделяемый оттуда: помогает, но не обязательно хочет это сделать

  • Задайте эту проблему с масштабируемостью в ядре Linux