공유 메모리 환경 (예 : OpenMP, Pthreads 또는 TBB를 통해 스레드)에서 메모리 대역폭 제한 계산을 수행 할 때 각 스레드가 대부분의 메모리에 액세스 하도록 메모리가 실제 메모리에 올바르게 분산되도록하는 방법에는 딜레마가 있습니다. “로컬”메모리 버스. 인터페이스는 이식성이 없지만 대부분의 운영 체제에는 스레드 선호도를 설정하는 방법이 있습니다 (예 : pthread_setaffinity_np()
많은 POSIX 시스템, sched_setaffinity()
Linux, SetThreadAffinityMask()
Windows). 메모리 계층을 결정하기위한 hwloc 과 같은 라이브러리도 있지만 불행히도 대부분의 운영 체제는 아직 NUMA 메모리 정책을 설정하는 방법을 제공하지 않습니다. libnuma 와 함께 Linux는 주목할만한 예외입니다 .응용 프로그램이 페이지 단위로 메모리 정책 및 페이지 마이그레이션을 조작 할 수 있도록합니다 (2004 년 이후의 주요 내용). 다른 운영 체제에서는 사용자가 암시적인 “첫 번째 터치”정책을 준수해야합니다.
“first touch”정책으로 작업한다는 것은 호출자가 새로 할당 된 메모리에 처음 쓸 때 나중에 사용하려는 선호도를 가진 스레드를 작성하고 분배해야 함을 의미합니다. ( malloc()
실제로 페이지를 찾도록 구성되는 시스템은 거의 없으며 , 실제로는 다른 스레드에 의해 오류가 발생했을 때 해당 페이지를 찾도록 약속합니다.) 이는 사용 calloc()
후 할당을 사용 하거나 즉시 메모리를 초기화 하는 할당 memset()
은 결함이 있기 때문에 해롭다는 것을 의미합니다 할당 스레드를 실행하는 코어의 메모리 버스에있는 모든 메모리로 인해 여러 스레드에서 메모리에 액세스 할 때 최악의 메모리 대역폭이 발생합니다. new
많은 새로운 할당을 초기화 해야하는 C ++ 연산자 에도 동일하게 적용됩니다 (예 :std::complex
). 이 환경에 대한 몇 가지 관찰 :
- 할당은 “스레드 집합”으로 만들 수 있지만, 이제는 스레딩 모델에 할당이 혼합되어 다른 스레딩 모델을 사용하여 클라이언트와 상호 작용해야하는 라이브러리에는 바람직하지 않습니다 (각각 자체 스레드 풀이 있음).
- RAII는 관용적 C ++의 중요한 부분으로 여겨지지만 NUMA 환경에서 메모리 성능에 적극적으로 유해한 것 같습니다. 배치
new
는를 통해 할당 된 메모리malloc()
또는에서 루틴 과 함께 사용할 수libnuma
있지만 할당 프로세스가 변경됩니다 (필요하다고 생각합니다). - 편집 : 연산자
new
에 대한 내 이전 진술 이 잘못되었습니다. 여러 인수를 지원할 수 있습니다. Chetan의 답변을 참조하십시오. 라이브러리 또는 STL 컨테이너가 지정된 선호도를 사용하도록하는 데 여전히 우려가 있다고 생각합니다. 여러 필드가 압축 될 수 있으며, 예를 들어std::vector
올바른 컨텍스트 관리자가 활성화 된 상태 에서 재 할당 되는 것이 불편할 수 있습니다 . - 각 스레드는 자체 개인 메모리를 할당하고 오류를 일으킬 수 있지만 인접 영역으로 인덱싱하는 것이 더 복잡합니다. (희소 행렬 – 벡터 제품 고려 매트릭스 및 벡터의 로우 격벽과;의 소유되지 않은 부분의 색인 더 복잡한 데이터 구조를 필요로하는 경우 가상 메모리에서 연속이 아니다.)
NUMA 할당 / 초기화에 대한 솔루션이 관용어로 간주됩니까? 다른 중요한 문제를 배제 했습니까?
(저는 C ++ 예제가 그 언어에 중점을 두는 것을 의미하지는 않지만 C ++ 언어 는 C와 같은 언어가 아닌 메모리 관리에 대한 일부 결정을 인코딩하므로 C ++ 프로그래머가 그렇게 할 것을 제안 할 때 더 많은 저항이있는 경향이 있습니다 상황이 다릅니다.)
답변
내가 선호하는이 문제에 대한 한 가지 해결책은 효과적으로 메모리 컨트롤러 수준에서 스레드와 (MPI) 작업을 분리하는 것입니다. 즉, CPU 소켓 또는 메모리 컨트롤러 당 하나의 작업을 수행 한 다음 각 작업에서 스레드를 수행하여 코드에서 NUMA 측면을 제거하십시오. 그렇게하면 할당 또는 초기화 작업을 실제로 수행하는 스레드에 관계없이 첫 번째 터치 또는 사용 가능한 API 중 하나를 통해 모든 메모리를 해당 소켓 / 컨트롤러에 안전하게 바인딩 할 수 있어야합니다. 소켓 간 메시지 전달은 일반적으로 최소한 MPI에서 매우 잘 최적화됩니다. 이보다 더 많은 MPI 작업을 항상 가질 수는 있지만 제기 한 문제로 인해 사람들이 더 적은 것을 권장하지는 않습니다.
답변
이 답변은 질문에서 두 가지 C ++ 관련 오해에 대한 답변입니다.
- “새 할당 (POD 포함) 초기화를 요구하는 C ++ new 연산자에도 동일하게 적용됩니다.”
- “C ++ 연산자 new는 하나의 매개 변수 만 사용합니다”
언급 한 멀티 코어 문제에 대한 직접적인 답변은 아닙니다. 평판이 유지되도록 C ++ 프로그래머를 C ++ 열성 자로 분류하는 의견에 응답하십시오.
요점 1. C ++ “새”또는 스택 할당은 POD 여부에 관계없이 새 개체를 초기화하지 않습니다. 사용자가 정의한 클래스의 기본 생성자에게는 그 책임이 있습니다. 아래의 첫 번째 코드는 클래스가 POD인지 여부에 따라 정크 인쇄를 보여줍니다.
포인트 2. C ++에서는 여러 인수로 “새”를 오버로드 할 수 있습니다. 아래 두 번째 코드는 단일 객체를 할당하는 경우를 보여줍니다. 아이디어를 제공하고 현재 상황에 유용 할 것입니다. 연산자 new []도 적절히 수정할 수 있습니다.
// 포인트 1의 코드.
#include <iostream>
struct A
{
// int/double/char/etc not inited with 0
// with or without this constructor
// If present, the class is not POD, else it is.
A() { }
int i;
double d;
char c[20];
};
int main()
{
A* a = new A;
std::cout << a->i << ' ' << a->d << '\n';
for(int i = 0; i < 20; ++i)
std::cout << (int) a->c[i] << '\n';
}
인텔의 11.1 컴파일러는이 출력을 보여줍니다 (물론 초기화되지 않은 메모리는 “a”로 표시됨).
993001483 6.50751e+029
105
108
... // skipped
97
108
// 포인트 2의 코드.
#include <cstddef>
#include <iostream>
#include <new>
// Just to use two different classes.
class arena { };
class policy { };
struct A
{
void* operator new(std::size_t, arena& arena_obj, policy& policy_obj)
{
std::cout << "special operator new\n";
return (void*)0x1234; //Just to test
}
};
void* operator new(std::size_t, arena& arena_obj, policy& policy_obj)
{
std::cout << "special operator new (global)\n";
return (void*)0x5678; //Just to test
}
int main ()
{
arena arena_obj;
policy policy_obj;
A* ptr = new(arena_obj, policy_obj) A;
int* iptr = new(arena_obj, policy_obj) int;
std::cout << ptr << "\n";
std::cout << iptr << "\n";
}
답변
거래 II. 스레딩 빌딩 블록을 사용하여 각 셀의 어셈블리를 여러 코어로 병렬화 할 수있는 소프트웨어 인프라가 갖추어져 있습니다. 구현되었지만 일반적인 아이디어입니다). 문제는 로컬 통합의 경우 여러 개의 임시 (스크래치) 객체가 필요하고 병렬로 실행할 수있는 작업이 최소한 있어야한다는 것입니다. 작업이 프로세서에 배치 될 때 일반적으로 다른 코어의 캐시에있을 스크래치 개체 중 하나를 가져 오기 때문에 속도가 느려질 수 있습니다. 두 가지 질문이있었습니다.
(i) 이것이 진짜 이유입니까? cachegrind에서 프로그램을 실행할 때 기본적으로 단일 스레드에서 프로그램을 실행할 때와 동일한 수의 명령을 사용하고 있지만 모든 스레드에 누적 된 총 런타임은 단일 스레드보다 훨씬 큽니다. 캐시에 지속적으로 오류가 발생했기 때문입니까?
(ii) 현재 위치, 각 스크래치 개체가있는 위치 및 현재 코어의 캐시에서 핫한 개체에 액세스하기 위해 어떤 스크래치 개체를 사용해야하는지 어떻게 알 수 있습니까?
궁극적으로, 우리는 이러한 솔루션 중 하나에 대한 답변을 찾지 못했으며 몇 번의 연구 끝에 이러한 문제를 조사하고 해결할 도구가 부족하다고 결정했습니다. 적어도 원칙적으로 문제 (ii)를 해결하는 방법을 알고 있습니다 (즉, 스레드가 프로세서 코어에 고정되어 있다고 가정 할 때 스레드 로컬 객체 사용-테스트하기 쉽지 않은 또 다른 추측). (나는).
따라서 우리의 관점에서 NUMA를 다루는 것은 여전히 해결되지 않은 질문입니다.
답변
hwloc 외에도 HPC 클러스터의 메모리 환경을보고하고 다양한 NUMA 구성을 설정하는 데 사용할 수있는 몇 가지 도구가 있습니다.
LIKWID를 사용하면 프로세스를 코어에 고정시킬 수있는 코드 기반 접근 방식을 피할 수 있으므로 LIKWID를 권장합니다. 머신 별 메모리 구성을 처리하기위한 툴링의 이러한 접근 방식은 클러스터에서 코드의 이식성을 보장하는 데 도움이됩니다.
ISC’13 ” LIKWID-Lightweight Performance Tools ” 에서 간략하게 소개 한 프레젠테이션을 찾을 수 있으며 저자는 Arxiv ” 현대 멀티 코어 프로세서에서 HPM 지원 성능 엔지니어링을위한 모범 사례 “에 관한 논문을 발표했습니다 . 이 백서에서는 하드웨어 카운터의 데이터를 해석하여 시스템 아키텍처 및 메모리 토폴로지와 관련된 성능 코드를 개발하는 방법에 대해 설명합니다.