Linux システムでは、pthreads ライブラリーは、キャッシュ・アライメント用の関数 (posix_memalign) を提供して、誤った共有を防ぎます。また、アーキテクチャの特定の NUMA ノードを選択するには、libnuma ライブラリを使用できます。私が欲しいのは、両方を必要とするものです。特定のスレッドを特定のプロセッサにバインドし、スレッドのメモリ操作の遅延を減らすために、対応する NUMA ノードから各スレッドにローカル データ構造を割り当てたいと考えています。これどうやってするの?
2 に答える
libnuma の numa_alloc_*() 関数は、メモリのページ全体 (通常は 4096 バイト) を割り当てます。通常、キャッシュ ラインは 64 バイトです。4096 は 64 の倍数であるため、numa_alloc_*() から返されるものはすべて、キャッシュ レベルで既に memaligned されています。
ただし、numa_alloc_*() 関数には注意してください。マニュアルページには、対応する malloc() よりも遅いと書かれていますが、これは確かに本当ですが、私が見つけたより大きな問題は、多くのコアで同時に実行される numa_alloc_*() からの同時割り当てです。大規模な競合の問題に苦しむ。私の場合、malloc() を numa_alloc_onnode() に置き換えるのは面倒でした (ローカル メモリを使用して得たものはすべて、割り当て/空き時間の増加によって相殺されました)。tcmalloc はどちらよりも高速でした。一度に 32 のスレッド/コアで数千の 12 ~ 16kb の malloc を実行していました。タイミングの実験では、numa_alloc_onnode() のシングル スレッド速度がプロセスの割り当ての実行に多くの時間を費やした原因ではないことが示されました。私が解決策 numa_alloc_onnode() で大量のメモリ チャンクを一度に取得し、必要に応じて各ノードで実行されているスレッドに分配する方法を採用しました。gcc アトミック ビルトインを使用して、各スレッド (スレッドを CPU にピン留め) が各ノードに割り当てられたメモリから取得できるようにします。必要に応じて、ディストリビューションが作成されるときにキャッシュラインサイズを調整できます。そうします。このアプローチは、tcmalloc でさえパンツを打ち負かします (これはスレッドに対応していますが、NUMA には対応していません - 少なくとも Debain Squeeze バージョンはそうではないようです)。このアプローチの欠点は、個々のディストリビューションを解放できないことです (とにかく、もっと多くの作業が必要です)。基礎となるノード上の割り当て全体を解放することしかできません。ただし、これが関数呼び出しのためのノード上の一時的なスクラッチ スペースである場合、またはそのメモリがいつ不要になるかを正確に指定できる場合は、次に、このアプローチは非常にうまく機能します。もちろん、各ノードに割り当てる必要があるメモリの量も予測できると役立ちます。
@nandu : 完全なソースを投稿するつもりはありません。それは長く、私が行っている他のことに関連する場所にあるため、完全に透明ではありません。私が投稿するのは、私の新しい malloc() 関数を少し短縮したバージョンで、核となるアイデアを説明しています。
void *my_malloc(struct node_memory *nm,int node,long size)
{
long off,obytes;
// round up size to the nearest cache line size
// (optional, though some rounding is essential to avoid misalignment problems)
if ((obytes = (size % CACHE_LINE_SIZE)) > 0)
size += CACHE_LINE_SIZE - obytes;
// atomically increase the offset for the requested node by size
if (((off = __sync_fetch_and_add(&(nm->off[node]),size)) + size) > nm->bytes) {
fprintf(stderr,"Out of allocated memory on node %d\n",node);
return(NULL);
}
else
return((void *) (nm->ptr[node] + off));
}
構造体 node_memory はどこにありますか
struct node_memory {
long bytes; // the number of bytes of memory allocated on each node
char **ptr; // ptr array of ptrs to the base of the memory on each node
long *off; // array of offsets from those bases (in bytes)
int nptrs; // the size of the ptr[] and off[] arrays
};
nm->ptr[node] は libnuma 関数 numa_alloc_onnode() を使用して設定されます。
私は通常、許容可能なノード情報も構造体に保存するので、 my_malloc() は関数呼び出しを行わずにノード要求が適切であることを確認できます。また、 nm が存在し、そのサイズが適切であることを確認します。関数 __sync_fetch_and_add() は、gcc 組み込みアトミック関数です。gcc でコンパイルしていない場合は、別のものが必要になります。私の限られた経験では、アトミックを使用するのは、(4P NUMA マシンのように) スレッド/コア数が多い条件でミューテックスよりもはるかに高速であるためです。
NUMA アロケーターのアライメント機能を取得したいだけの場合は、独自のアロケーターを簡単に構築できます。
malloc()
アイデアは、少し多くのスペースで非整列を呼び出すことです。次に、アラインされた最初のアドレスを返します。解放できるようにするには、既知の場所にベース アドレスを保存する必要があります。
これが例です。名前を適切なものに置き換えるだけです。
pint // An unsigned integer that is large enough to store a pointer.
NUMA_malloc // The NUMA malloc function
NUMA_free // The NUMA free function
void* my_NUMA_malloc(size_t bytes,size_t align, /* NUMA parameters */ ){
// The NUMA malloc function
void *ptr = numa_malloc(
(size_t)(bytes + align + sizeof(pint)),
/* NUMA parameters */
);
if (ptr == NULL)
return NULL;
// Get aligned return address
pint *ret = (pint*)((((pint)ptr + sizeof(pint)) & ~(pint)(align - 1)) + align);
// Save the free pointer
ret[-1] = (pint)ptr;
return ret;
}
void my_NUMA_free(void *ptr){
if (ptr == NULL)
return;
// Get the free pointer
ptr = (void*)(((pint*)ptr)[-1]);
// The NUMA free function
numa_free(ptr);
}
これを使用する場合は、 でmy_NUMA_free
割り当てられたものを呼び出す必要がありますmy_NUMA_malloc
。