휴대용 멀티 코어 / NUMA 메모리 할당 / 초기화 모범 사례


17

공유 메모리 환경 (예 : 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 ++ 프로그래머가 그렇게 할 것을 제안 할 때 더 많은 저항이있는 경향이 있습니다 상황이 다릅니다.)

답변:


7

내가 선호하는이 문제에 대한 한 가지 해결책은 효과적으로 메모리 컨트롤러 수준에서 스레드와 (MPI) 작업을 분리하는 것입니다. 즉, CPU 소켓 또는 메모리 컨트롤러 당 하나의 작업을 수행 한 다음 각 작업에서 스레드를 수행하여 코드에서 NUMA 측면을 제거하십시오. 그렇게하면 할당 또는 초기화 작업을 실제로 수행하는 스레드에 관계없이 첫 번째 터치 또는 사용 가능한 API 중 하나를 통해 모든 메모리를 해당 소켓 / 컨트롤러에 안전하게 바인딩 할 수 있어야합니다. 소켓 간 메시지 전달은 일반적으로 최소한 MPI에서 매우 잘 최적화됩니다. 이보다 더 많은 MPI 작업을 항상 가질 수는 있지만 제기 한 문제로 인해 사람들이 더 적은 것을 권장하지는 않습니다.


1
이것은 실용적인 솔루션이지만, 더 많은 코어를 빠르게 가져오고 있지만 NUMA 노드 당 코어 수는 약 4에서 상당히 정체되어 있습니다. 따라서 1000 개의 가상 노드 노드에서 250 개의 MPI 프로세스를 실행합니까? (이것은 훌륭하지만 회의적입니다.)
Jed Brown

NUMA 당 코어 수가 정체 된 것에 동의하지 않습니다. Sandy Bridge E5는 8 개입니다. Magny Cours는 12 개입니다. 10의 Westmere-EX 노드가 있습니다. Interlagos (ORNL Titan)는 20 개입니다. Knights Corner는 50 개가 넘습니다. NUMA 당 코어 수는 무어의 법칙에 따라
Bill Barth

Magny Cours와 Interlagos는 서로 다른 NUMA 지역에 2 개의 다이가 있으므로 NUMA 지역 당 6 개 및 8 개의 코어가 있습니다. 쿼드 코어 클로버 타운의 두 소켓이 동일한 인터페이스 (블랙 포드 칩셋)를 메모리에 공유하는 2006 년으로 되 감아 NUMA 지역 당 코어 수가 그렇게 빠르게 성장하는 것처럼 보이지 않습니다. Blue Gene / Q는 메모리의이 평평한 시야를 조금 더 확장하고 Knight 's Corner가 또 다른 단계를 밟을 수 있습니다 (다른 장치 임에도 불구하고 15 (Fermi) 또는 8 ( 플랫 메모리를 보는 SM).
Jed Brown

AMD 칩을 잘 활용하십시오. 나는 잊어 버렸었다. 아직도, 나는 당신이 잠시 동안이 분야에서 지속적인 성장을 볼 것이라고 생각합니다.
Bill Barth

6

이 답변은 질문에서 두 가지 C ++ 관련 오해에 대한 답변입니다.

  1. "새 할당 (POD 포함) 초기화를 요구하는 C ++ new 연산자에도 동일하게 적용됩니다."
  2. "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";
}

수정 해 주셔서 감사합니다. C ++과 같은 비 POD 배열을 제외하고 C를 기준으로 존재하지 추가적인 합병증 않는 것 같다 std::complex하는 하는 명시 적으로 초기화합니다.
Jed Brown

1
@JedBrown : 사용을 피하는 이유 번호 6 std::complex?
Jack Poulson

1

거래 II. 스레딩 빌딩 블록을 사용하여 각 셀의 어셈블리를 여러 코어로 병렬화 할 수있는 소프트웨어 인프라가 갖추어져 있습니다. 구현되었지만 일반적인 아이디어입니다). 문제는 로컬 통합의 경우 여러 개의 임시 (스크래치) 객체가 필요하고 병렬로 실행할 수있는 작업이 최소한 있어야한다는 것입니다. 작업이 프로세서에 배치 될 때 일반적으로 다른 코어의 캐시에있을 스크래치 개체 중 하나를 가져 오기 때문에 속도가 느려질 수 있습니다. 두 가지 질문이있었습니다.

(i) 이것이 진짜 이유입니까? cachegrind에서 프로그램을 실행할 때 기본적으로 단일 스레드에서 프로그램을 실행할 때와 동일한 수의 명령을 사용하고 있지만 모든 스레드에 누적 된 총 런타임은 단일 스레드보다 훨씬 큽니다. 캐시에 지속적으로 오류가 발생했기 때문입니까?

(ii) 현재 위치, 각 스크래치 개체가있는 위치 및 현재 코어의 캐시에서 핫한 개체에 액세스하기 위해 어떤 스크래치 개체를 사용해야하는지 어떻게 알 수 있습니까?

궁극적으로, 우리는 이러한 솔루션 중 하나에 대한 답변을 찾지 못했으며 몇 번의 연구 끝에 이러한 문제를 조사하고 해결할 도구가 부족하다고 결정했습니다. 적어도 원칙적으로 문제 (ii)를 해결하는 방법을 알고 있습니다 (즉, 스레드가 프로세서 코어에 고정되어 있다고 가정 할 때 스레드 로컬 객체 사용-테스트하기 쉽지 않은 또 다른 추측). (나는).

따라서 우리의 관점에서 NUMA를 다루는 것은 여전히 ​​해결되지 않은 질문입니다.


프로세서가 고정되어 있는지 궁금하지 않도록 스레드를 소켓에 바인딩해야합니다. 리눅스는 물건을 옮기는 것을 좋아합니다.
Bill Barth

또한 getcpu () 또는 sched_getcpu ()를 샘플링하면 (libc 및 커널 등에 따라) Linux에서 스레드가 실행중인 위치를 결정할 수 있습니다.
Bill Barth

예, 스레드 작업을 예약하는 데 사용하는 스레딩 빌딩 블록은 스레드를 프로세서에 고정시킵니다. 이것이 우리가 스레드 로컬 스토리지 작업을 시도한 이유입니다. 그러나 여전히 내 문제에 대한 해결책을 제시하기가 어렵습니다 (i).
Wolfgang Bangerth

1

hwloc 외에도 HPC 클러스터의 메모리 환경을보고하고 다양한 NUMA 구성을 설정하는 데 사용할 수있는 몇 가지 도구가 있습니다.

LIKWID를 사용하면 프로세스를 코어에 고정시킬 수있는 코드 기반 접근 방식을 피할 수 있으므로 LIKWID를 권장합니다. 머신 별 메모리 구성을 처리하기위한 툴링의 이러한 접근 방식은 클러스터에서 코드의 이식성을 보장하는 데 도움이됩니다.

ISC'13 " LIKWID-Lightweight Performance Tools " 에서 간략하게 소개 한 프레젠테이션을 찾을 수 있으며 저자는 Arxiv " 현대 멀티 코어 프로세서에서 HPM 지원 성능 엔지니어링을위한 모범 사례 "에 관한 논문을 발표했습니다 . 이 백서에서는 하드웨어 카운터의 데이터를 해석하여 시스템 아키텍처 및 메모리 토폴로지와 관련된 성능 코드를 개발하는 방법에 대해 설명합니다.


LIKWID는 유용하지만 문제는 다양한 실행 환경, 스레딩 구성표, MPI 리소스 관리 및 선호도 설정, 사용 범위에서 예상되는 위치를 안정적으로 획득하고 자체 감사 할 수있는 숫자 / 메모리에 민감한 라이브러리를 작성하는 방법에 대한 것입니다. 다른 도서관 등
Jed Brown
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.