В настоящее время мы используем потоковый граф TBB, в котором а) параллельный фильтр обрабатывает массив (параллельно со смещениями) и помещает обработанные результаты в промежуточный вектор (размещается в куче; в основном вектор будет расти до 8 МБ). Эти векторы затем передаются узлам, которые затем обрабатывают эти результаты на основе их характеристик (определенных в а)). Из-за синхронизированных ресурсов для каждой характеристики может быть только один такой узел. Прототип, который мы написали, хорошо работает на архитектурах UMA (протестирован на архитектуре Ivy Bridge и Sandy Bridge с одним процессором). Тем не менее, приложение не масштабируется на нашей архитектуре NUMA (4 процессора Nehalem-EX). Мы свалили проблему с распределением памяти и создали минимальный пример, в котором у нас есть параллельный конвейер, который просто выделяет память из кучи (через malloc блока 8 МБ, затем memset область 8 МБ; аналогично тому, что будет делать первоначальный прототип) до определенного объема памяти. Наши выводы:
В архитектуре UMA приложение линейно масштабируется с количеством потоков, используемых конвейером (устанавливается через task_scheduler_init)
В архитектуре NUMA, когда мы прикрепляем приложение к одному сокету (используя numactl), мы видим то же линейное масштабирование
На архитектуре NUMA, когда мы используем более одного сокета, время выполнения нашего приложения увеличивается с увеличением количества сокетов (отрицательный линейный масштаб — «вверх»)
Для нас это пахнет кучей раздоров. До сих пор мы пытались заменить масштабируемый распределитель Intel TBB на распределитель glibc. Однако начальная производительность на одном сокете хуже, чем на glibc, на нескольких сокетах производительность не ухудшается, но и не улучшается. получил тот же эффект, используя tcmalloc, распределитель запаса и распределитель, выровненный по кэш-памяти TBB.
Вопрос в том, сталкивался ли кто-то с подобными проблемами. Распределение стека для нас не вариант, так как мы хотим сохранить векторы, выделенные в куче, даже после запуска конвейера. Каким образом можно эффективно распределить области памяти размером в МБ на архитектурах NUMA из нескольких потоков? Мы действительно хотели бы сохранить подход динамического распределения вместо предварительного выделения памяти и управления ею в приложении.
Я приложил отличную статистику для различных казней с помощью numactl. Чередование / localalloc не оказывает никакого влияния (шина QPI не является узким местом; мы убедились, что с PCM нагрузка на канал QPI составляет 1%). Я также добавил диаграмму, отображающую результаты для glibc, tbbmalloc и tcmalloc.
перф стат бин / прототип
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 = c ++ 11 -O3 -march = native; выполняется с numactl —cpunodebind = 0 … numactl —cpunodebind = 0-3 — при привязке к ЦП мы имеем следующий результат: 1 ЦП (скорость x), 2 ЦП (скорость ~ x / 2), 3 ЦП (скорость) ~ х / 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
Краткое обновление и частичный ответ по описанной проблеме:
Призыв к malloc
или же scalable_malloc
не являются узким местом, узким местом являются скорее ошибки страницы, вызванные memset
Ting выделенной памяти. Там нет никакой разницы между Glibc malloc
и другие масштабируемые распределители, такие как Intel TBB scalable_malloc
: для распределений, превышающих определенный порог (обычно 1 МБ, если ничего не free
д; может быть определен madvise
) память будет выделяться аномальным mmap. Первоначально все страницы карты указывают на внутреннюю страницу ядра, которая имеет предварительные значения 0 и доступна только для чтения. Когда мы запоминаем память, это вызывает исключение (учтите, что страница ядра доступна только для чтения) и сбой страницы. Новая страница будет 0ed в это время. Небольшие страницы занимают 4 КБ, поэтому это будет происходить 2048 раз для буфера 8 МБ, который мы выделяем и записываем. Я измерил то, что эти ошибки страниц не так дороги на машинах с одним сокетом, но становятся все дороже на машинах NUMA с несколькими ЦП.
Решения, которые я придумала до сих пор:
Используйте огромные страницы: помогает, но только задерживает проблему
Используйте предварительно выделенный и предварительно ошибочный (либо memset
или же mmap
+ MAP_POPULATE
) область памяти (пул памяти) и выделение оттуда: помогает, но не обязательно это делать
Решить эту проблему масштабируемости в ядре Linux
Второе обновление (закрытие вопроса):
Просто снова профилировал пример приложения с ядром 3.10.
Результаты для параллельного размещения и установки 16 ГБ данных:
маленькие страницы:
огромные страницы:
Кажется, проблема масштабируемого размещения теперь решена — по крайней мере, для огромных страниц.