現在、TBB フロー グラフを使用しています。このグラフでは、a) 並列フィルターが配列を (オフセットと並行して) 処理し、処理された結果を中間ベクトル (ヒープに割り当てられます。ほとんどの場合、ベクトルは 8MB まで増加します) に入れます。次に、これらのベクトルはノードに渡され、ノードはその特性 (a) で決定) に基づいてこれらの結果を後処理します。リソースが同期されているため、各特性に対してこのようなノードは 1 つしか存在できません。私たちが作成したプロトタイプは、UMA アーキテクチャ (単一 CPU の Ivy Bridge および Sandy Bridge アーキテクチャでテスト済み) でうまく動作します。ただし、アプリケーションは NUMA アーキテクチャ (4 CPU Nehalem-EX) ではスケーリングしません。問題をメモリ割り当てに突き止め、ヒープからメモリを割り当てるだけの並列パイプラインを持つ最小限の例を作成しました (8MB チャンクの malloc を介して、次に、8MB 領域を memset します。最初のプロトタイプが行うことと同様) メモリの特定の量まで。私たちの調査結果は次のとおりです。
UMA アーキテクチャでは、アプリケーションは、パイプラインで使用されるスレッドの数 (task_scheduler_init で設定) に比例してスケールアップします。
NUMA アーキテクチャでは、(numactl を使用して) アプリケーションを 1 つのソケットに固定すると、同じ線形スケールアップが見られます。
複数のソケットを使用する NUMA アーキテクチャーでは、アプリケーションの実行時間はソケットの数に応じて増加します (負の線形スケール - 「アップ」)。
私たちにとって、これはヒープの競合のようなにおいがします。これまでに試したことは、glibc アロケーターを Intel の TBB スケーラブル アロケーターに置き換えることです。ただし、単一ソケットでの初期パフォーマンスは glibc を使用するよりも悪く、複数ソケットでのパフォーマンスは悪化していませんが、改善もされていません。 tcmalloc、買いだめアロケータ、および TBB のキャッシュ アライメント アロケータを使用して同じ効果を得ました。
問題は、誰かが同様の問題を経験したかどうかです。パイプラインが実行された後でもヒープに割り当てられたベクトルを保持したいので、スタック割り当てはオプションではありません。複数のスレッドから NUMA アーキテクチャで、1 つのヒープに MB 単位のサイズのメモリ領域を効率的に割り当てるにはどうすればよいですか? メモリを事前に割り当ててアプリケーション内で管理するのではなく、動的割り当てアプローチを維持したいと考えています。
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 ビン/プロトタイプ 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 ビン/プロトタイプ 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 ビン/プロトタイプ 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 で実行 - CPU バインドでは、次の結果が得られます: 1 CPU (速度 x)、2 CPU (速度 ~ x/2)、3 CPU (速度~ x/3) [速度=高いほど良い]. したがって、CPU の数が増えるとパフォーマンスが低下することがわかります。メモリ バインディング、インターリーブ (--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