В настоящее время мы используем график потока 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