커스텀 C ++ 할당 자의 매력적인 예?


176

std::allocator커스텀 솔루션을 선호하는 좋은 이유는 무엇입니까 ? 정확성, 성능, 확장 성 등에 절대적으로 필요한 모든 상황에서 실행 했습니까? 정말 영리한 예가 있습니까?

사용자 지정 할당자는 항상 필요하지 않은 표준 라이브러리의 기능이었습니다. 나는 여기에있는 누군가가 그들의 존재를 정당화하기위한 매력적인 예를 제공 할 수 있는지 궁금합니다.

답변:


121

여기서 언급했듯이 Intel TBB의 사용자 정의 STL 할당자는 단일 스레드를 변경하여 멀티 스레드 응용 프로그램의 성능을 크게 향상시키는 것을 보았습니다.

std::vector<T>

std::vector<T,tbb::scalable_allocator<T> >

(이것은 TBB의 멋진 스레드 전용 힙을 사용하도록 할당자를 전환하는 빠르고 편리한 방법입니다. 이 문서의 7 페이지 참조 )


3
두 번째 링크 주셔서 감사합니다. 스레드 전용 힙을 구현하기 위해 할당자를 사용하는 것은 영리합니다. 리소스 제한이없는 시나리오 (내장 또는 콘솔)에서 사용자 지정 할당자가 분명한 이점을 갖는 좋은 예입니다.
Naaff

7
원래 링크는 지금 소멸하지만, CiteSeer는 PDF가 있습니다 citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.71.8289
아르토 Bendiken

1
그런 질문을해야합니다. 그런 벡터를 다른 스레드로 안정적으로 이동할 수 있습니까? (아니요 추측)
sellibitze

@ sellibitze : TBB 작업 내에서 벡터를 조작하고 여러 병렬 작업에서 재사용하고 TBB 작업자 스레드가 작업을 선택한다는 보장이 없으므로 제대로 작동한다고 결론을 내립니다. TBB가 다른 스레드의 한 스레드에서 생성 된 항목을 해제하는 데 역사적인 문제가 있었음에도 불구하고 (스레드 개인 힙 및 생산자-소비자 할당 및 할당 해제 패턴에 대한 고전적인 문제입니다. 아마도 최신 버전에서 수정되었을 수도 있습니다.)
timday

@ArtoBendiken : 링크의 다운로드 링크가 유효하지 않은 것 같습니다.
einpoklum

81

사용자 지정 할당자가 유용 할 수있는 영역 중 하나는 게임 개발, 특히 게임 콘솔에서 메모리가 적고 스왑이 없기 때문에 게임 개발입니다. 이러한 시스템에서는 각 하위 시스템을 엄격하게 제어하여 중요하지 않은 시스템 하나가 중요한 시스템에서 메모리를 훔칠 수 없도록해야합니다. 풀 할당 자 같은 다른 것들은 메모리 조각화를 줄이는 데 도움이 될 수 있습니다. 다음 주제에서 길고 자세한 논문을 찾을 수 있습니다.

EASTL-전자 예술 표준 템플릿 라이브러리


14
EASTL 링크의 경우 +1 : "STL의 가장 근본적인 약점은 std 할당 자 디자인이며,이 약점은 EASTL의 생성에 가장 큰 기여 요인이었습니다."
Naaff

65

벡터가 메모리 매핑 파일의 메모리를 사용할 수 있도록 mmap 할당 기에서 작업하고 있습니다. 목표는 mmap에 의해 매핑 된 가상 메모리에 직접있는 스토리지를 사용하는 벡터를 갖는 것입니다. 우리의 문제는 복사 오버 헤드없이 실제로 큰 파일 (> 10GB)을 메모리로 읽는 것을 향상시키는 것이므로이 사용자 지정 할당자가 필요합니다.

지금까지 std :: allocator에서 파생 된 사용자 지정 할당 자의 골격을 가지고 있으므로 자체 할당자를 작성하는 것이 좋은 출발점이라고 생각합니다. 이 코드를 원하는 방식으로 자유롭게 사용하십시오.

#include <memory>
#include <stdio.h>

namespace mmap_allocator_namespace
{
        // See StackOverflow replies to this answer for important commentary about inheriting from std::allocator before replicating this code.
        template <typename T>
        class mmap_allocator: public std::allocator<T>
        {
public:
                typedef size_t size_type;
                typedef T* pointer;
                typedef const T* const_pointer;

                template<typename _Tp1>
                struct rebind
                {
                        typedef mmap_allocator<_Tp1> other;
                };

                pointer allocate(size_type n, const void *hint=0)
                {
                        fprintf(stderr, "Alloc %d bytes.\n", n*sizeof(T));
                        return std::allocator<T>::allocate(n, hint);
                }

                void deallocate(pointer p, size_type n)
                {
                        fprintf(stderr, "Dealloc %d bytes (%p).\n", n*sizeof(T), p);
                        return std::allocator<T>::deallocate(p, n);
                }

                mmap_allocator() throw(): std::allocator<T>() { fprintf(stderr, "Hello allocator!\n"); }
                mmap_allocator(const mmap_allocator &a) throw(): std::allocator<T>(a) { }
                template <class U>                    
                mmap_allocator(const mmap_allocator<U> &a) throw(): std::allocator<T>(a) { }
                ~mmap_allocator() throw() { }
        };
}

이를 사용하려면 다음과 같이 STL 컨테이너를 선언하십시오.

using namespace std;
using namespace mmap_allocator_namespace;

vector<int, mmap_allocator<int> > int_vec(1024, 0, mmap_allocator<int>());

예를 들어 메모리가 할당 될 때마다 기록하는 데 사용할 수 있습니다. 필요한 것은 리 바인드 구조체입니다. 그렇지 않으면 벡터 컨테이너가 수퍼 클래스 할당 / 할당 해제 메소드를 사용합니다.

업데이트 : 메모리 매핑 할당자는 이제 https://github.com/johannesthoma/mmap_allocator 에서 사용할 수 있으며 LGPL입니다. 프로젝트에 자유롭게 사용하십시오.


17
std :: allocator에서 파생 된 머리는 실제로 할당자를 작성하는 관용적 방법이 아닙니다. 대신 최소한의 기능 만 제공 할 수있는 allocator_traits를 살펴보고 특성 클래스가 나머지를 제공합니다. STL은 항상 직접이 아닌 allocator_traits를 통해 할당자를 사용하므로 직접 allocator_traits를 참조 할 필요는 없습니다.
Nir Friedman

25

코드에 c ++을 사용하는 MySQL 스토리지 엔진을 사용하고 있습니다. 우리는 메모리를 위해 MySQL과 경쟁하는 대신 MySQL 메모리 시스템을 사용하기 위해 커스텀 할당자를 사용하고 있습니다. 이를 통해 사용자가 "추가"가 아닌 MySQL을 사용하도록 구성한대로 메모리를 사용하고 있는지 확인할 수 있습니다.


21

사용자 지정 할당자를 사용하여 힙 대신 메모리 풀을 사용하는 것이 유용 할 수 있습니다. 그것은 많은 다른 것들 중 하나의 예입니다.

대부분의 경우 이것은 확실히 조기 최적화입니다. 그러나 특정 상황 (내장 장치, 게임 등)에서 매우 유용 할 수 있습니다.


3
또는 해당 메모리 풀이 공유 될 때.
Anthony

9

사용자 지정 STL 할당 자로 C ++ 코드를 작성하지는 않았지만 HTTP 요청에 응답하는 데 필요한 임시 데이터를 자동으로 삭제하기 위해 사용자 지정 할당자를 사용하는 C ++로 작성된 웹 서버를 상상할 수 있습니다. 사용자 지정 할당자는 응답이 생성되면 모든 임시 데이터를 한 번에 해제 할 수 있습니다.

사용자 지정 할당 자 (사용했던)의 또 다른 유스 케이스는 함수의 동작이 입력의 일부에 의존하지 않음을 입증하기 위해 단위 테스트를 작성하는 것입니다. 사용자 지정 할당자는 임의의 패턴으로 메모리 영역을 채울 수 있습니다.


5
첫 번째 예는 할당자가 아닌 소멸자의 작업 인 것 같습니다.
Michael Dorst

2
힙에서 메모리의 초기 내용에 따라 프로그램이 걱정된다면 valgrind에서 빠른 (즉, 하룻밤!) 실행을 통해 다른 방법으로 알 수 있습니다.
cdyson37

3
@anthropomorphic : 소멸자와 커스텀 할당자가 함께 작동하고 소멸자가 먼저 실행 된 다음 커스텀 할당자를 삭제하면 free (...)를 호출하지 않지만 free (...)가 호출됩니다. 나중에 요청 제공이 완료되었습니다. 이것은 기본 할당 자보다 빠를 수 있으며 주소 공간 조각화를 줄입니다.
pts

8

GPU 또는 다른 보조 프로세서로 작업 할 때 특별한 방식으로 주 메모리에 데이터 구조를 할당하는 것이 때때로 유리합니다 . 메모리를 할당하는 이 특별한 방법 은 편리한 방식으로 커스텀 할당 자에서 구현 될 수 있습니다.

액셀러레이터를 사용할 때 액셀러레이터 런타임을 통한 사용자 지정 할당이 유리한 이유는 다음과 같습니다.

  1. 사용자 지정 할당을 통해 가속기 런타임 또는 드라이버에 메모리 블록이 통지됩니다.
  2. 또한 운영 체제는 할당 된 메모리 블록이 페이지 잠금 (일부 핀 고정 메모리라고 함 ), 즉 운영 체제의 가상 메모리 서브 시스템이 페이지를 메모리 내에서 또는 메모리 내에서 이동 또는 제거하지 못하도록 할 수 있습니다.
  3. 1. 및 2. hold 및 페이지 잠금 메모리 블록과 가속기 간의 데이터 전송이 요청 된 경우 런타임은 위치를 알고 운영 체제가이를 알지 못하므로 주 메모리의 데이터에 직접 액세스 할 수 있습니다. 이동 / 제거
  4. 이렇게하면 비 페이지 잠금 방식으로 할당 된 메모리에서 발생하는 하나의 메모리 사본이 저장됩니다. 가속기를 사용하여 데이터를 기본 메모리에서 페이지 잠금 스테이징 영역으로 복사해야합니다 (DMA를 통해 데이터 전송을 초기화 할 수 있음). )

1
... 페이지 정렬 메모리 블록을 잊지 마십시오. 드라이버와 대화 할 때 (예 : DMA를 통해 FPGA로) DMA 스 캐터리 스트에 대한 인 페이지 오프셋 계산의 번거 로움과 오버 헤드를 원하지 않는 경우에 특히 유용합니다.
Jan

7

여기서는 사용자 지정 할당자를 사용하고 있습니다. 다른 사용자 지정 동적 메모리 관리 문제를 해결 해야한다고 말할 수도 있습니다 .

배경 : malloc, calloc, free 및 다양한 연산자의 new 및 delete에 대한 과부하가 있으며 링커는 STL을 사용하여 우리를 위해 이것을 행복하게 만듭니다. 이를 통해 자동 작은 객체 풀링, 누수 감지, 할당량 채우기, 무료 채우기, 센트리를 사용한 패딩 할당, 특정 할당량에 대한 캐시 라인 정렬 및 지연되지 않은 할당과 같은 작업을 수행 할 수 있습니다.

문제는 우리가 임베디드 환경에서 실행하고 있다는 것입니다. 실제로 오랜 시간 동안 누출 감지 계정을 제대로 수행하기에 충분한 메모리가 부족합니다. 최소한 표준 RAM이 아닌 사용자 지정 할당 기능을 통해 다른 곳에서 사용할 수있는 RAM이 있습니다.

해결 방법 : 확장 힙을 사용하는 사용자 지정 할당자를 작성 하고 메모리 누수 추적 아키텍처의 내부 에서만 사용하십시오. 그 밖의 모든 것은 누수 추적을 수행하는 일반적인 새 / 삭제 과부하로 기본 설정됩니다. 이렇게하면 추적기 자체 추적을 피할 수 있습니다 (추적 기능도 제공합니다. 추적기 노드의 크기를 알고 있습니다).

우리는 또한 같은 이유로 함수 비용 프로파일 링 데이터를 유지하기 위해 이것을 사용합니다. 스레드 스위치뿐만 아니라 각 함수 호출 및 리턴에 대한 항목을 작성하면 비용이 많이들 수 있습니다. 사용자 지정 할당 기는 다시 큰 디버그 메모리 영역에서 더 작은 할당량을 제공합니다.


5

내 프로그램의 한 부분에서 할당 / 할당 취소 횟수를 계산하고 시간이 얼마나 걸리는지를 측정하기 위해 사용자 지정 할당자를 사용하고 있습니다. 이것이 달성 될 수있는 다른 방법이 있지만이 방법은 나에게 매우 편리합니다. 컨테이너의 하위 집합에만 사용자 지정 할당자를 사용할 수있는 것이 특히 유용합니다.


4

한 가지 필수 상황 : 모듈 (EXE / DLL) 경계에서 작동해야하는 코드를 작성할 때 하나의 모듈에서만 할당 및 삭제를 유지하는 것이 중요합니다.

내가 본 곳은 Windows의 플러그인 아키텍처였습니다. 예를 들어, DLL 경계를 가로 질러 std :: string을 전달하는 경우, 문자열의 재 할당은 DLL의 힙이 아닌 *에서 시작된 힙에서 발생해야합니다 *.

* CRT에 동적으로 연결하는 것처럼 어쨌든 효과가있을 수 있으므로 실제로 이보다 더 복잡합니다. 그러나 각 DLL에 CRT에 대한 정적 링크가 있으면 팬텀 할당 오류가 계속 발생하는 고통의 세계로 향하고 있습니다.


DLL 경계를 넘어 개체를 전달하는 경우 양쪽에 대해 다중 스레드 (디버그) DLL (/ MD (d)) 설정을 사용해야합니다. C ++는 모듈 지원을 염두에두고 설계되지 않았습니다. 또는 COM 인터페이스 뒤의 모든 것을 보호하고 CoTaskMemAlloc을 사용할 수 있습니다. 이는 특정 컴파일러, STL 또는 공급 업체에 바인딩되지 않은 플러그인 인터페이스를 사용하는 가장 좋은 방법입니다.
gast128

그 노인의 규칙은 :하지 마십시오. DLL API에서 STL 유형을 사용하지 마십시오. 그리고 DLL API 경계를 넘어 동적 메모리가없는 책임을 전달하지 마십시오. C ++ ABI는 없으므로 모든 DLL을 C API로 취급하면 모든 종류의 잠재적 인 문제를 피할 수 있습니다. 물론 "c ++ beauty"를 희생하여. 또는 다른 의견에서 알 수 있듯이 COM을 사용하십시오. 평범한 C ++은 나쁜 생각입니다.
BitTickler

3

내가 이것을 사용했던 시간의 한 예는 리소스가 제한된 임베디드 시스템으로 작업하는 것이 었습니다. 2k의 램이없고 프로그램에서 해당 메모리의 일부를 사용해야한다고 가정 해 봅시다. 스택에 있지 않은 곳에 4-5 개의 시퀀스를 저장해야하며 추가로 이러한 것들이 저장되는 위치에 대해 매우 정확하게 액세스해야합니다. 이는 자신의 할당자를 작성하려는 상황입니다. 기본 구현은 메모리를 조각화 할 수 있습니다. 메모리가 충분하지 않고 프로그램을 다시 시작할 수없는 경우 허용되지 않을 수 있습니다.

내가 작업 한 프로젝트 중 일부는 저전력 칩에서 AVR-GCC를 사용하는 것이 었습니다. 가변 길이의 시퀀스 8 개를 저장해야하지만 최대 값은 알려져 있습니다. 메모리 관리표준 라이브러리 구현malloc / free 주위의 얇은 래퍼로 할당 된 모든 메모리 블록 앞에 할당 된 메모리 조각의 끝을지나 포인터를 붙여 항목을 배치 할 위치를 추적합니다. 새로운 메모리 조각을 할당 할 때 표준 할당자는 각 메모리 조각을 걸어서 요청 된 메모리 크기에 맞는 다음 블록을 찾아야합니다. 데스크탑 플랫폼에서는이 항목이 매우 빠르지 만 이러한 마이크로 컨트롤러 중 일부는 매우 느리고 원시적이라는 점을 명심해야합니다. 또한 메모리 조각화 문제는 우리가 실제로 다른 접근 방식을 택할 수밖에 없었던 거대한 문제였습니다.

우리가 한 일은 우리 자신의 메모리 풀 을 구현하는 것이 었습니다 . 각 메모리 블록은 우리가 필요로하는 가장 큰 순서에 맞을만큼 충분히 컸습니다. 이로 인해 고정 크기의 메모리 블록이 미리 할당되어 현재 사용중인 메모리 블록이 표시되었습니다. 특정 블록이 사용 된 경우 각 비트가 표시되는 8 비트 정수를 유지하여이 작업을 수행했습니다. 우리는 전체 프로세스를 더 빠르게 만들기 위해 메모리 사용을 교환했습니다.

임베디드 시스템의 맥락에서 사용자 지정 할당자를 작성하는 것을 볼 수있는 다른 경우가 있습니다. 예를 들어 시퀀스의 메모리가 이러한 플랫폼 에서 자주 볼 수있는 것처럼 메인 램에 있지 않은 경우 .



2

공유 메모리의 경우 컨테이너 헤드뿐만 아니라 포함 된 데이터도 공유 메모리에 저장해야합니다.

Boost :: Interprocess 의 할당 자가 좋은 예입니다. 그러나 여기에서 읽을 수 있듯이 모든 STL 컨테이너 공유 메모리를 호환 가능하게 만들기 위해이 모든 것만으로는 충분하지 않습니다 (다른 프로세스에서 서로 다른 매핑 오프셋으로 인해 포인터가 "깨질 수 있습니다").


2

언젠가 나는이 솔루션이 STL 컨테이너 용 Fast C ++ 11 할당 자 에게 매우 유용하다는 것을 알았습니다 . VS2017 (~ 5x) 및 GCC (~ 7x)에서 STL 컨테이너 속도를 약간 높입니다. 메모리 풀에 기반한 특수 목적의 할당 자입니다. 요구하는 메커니즘 덕분에 STL 컨테이너와 함께 사용할 수 있습니다.


1

저는 개인적으로 Loki :: Allocator / SmallObject를 사용하여 작은 개체의 메모리 사용을 최적화합니다. 중간 크기의 작은 개체 (1-256 바이트)로 작업해야하는 경우 좋은 효율성과 만족스러운 성능을 보여줍니다. 다양한 크기의 작은 객체를 적당량 할당하는 것에 대해 이야기하면 표준 C ++ 새로운 / 삭제 할당보다 최대 30 배 더 효율적일 수 있습니다. 또한 "QuickHeap"이라는 VC 전용 솔루션이 있으며 최상의 성능을 제공합니다 (할당 및 할당 해제 작업은 각각 최대 99 개의 할당 / 반환되는 블록의 주소를 읽고 쓰기 만 함). — 설정 및 초기화에 따라 다름) 그러나 상당한 오버 헤드 비용이 발생합니다. 익스텐트 당 두 개의 포인터와 각각의 새 메모리 블록에 대해 하나의 추가 포인터가 필요합니다. 그것'

표준 C ++ new / delete 구현의 문제점은 일반적으로 C malloc / free 할당을위한 랩퍼이며 1024+ 바이트와 같은 더 큰 메모리 블록에 적합하다는 것입니다. 성능면에서 눈에 띄는 오버 헤드가 있으며 때로는 매핑에 사용되는 추가 메모리가 있습니다. 따라서 대부분의 경우 사용자 지정 할당자는 작은 (≤1024 바이트) 객체를 할당하는 데 필요한 추가 메모리의 양을 최소화하고 성능을 최대화하는 방식으로 구현됩니다.


1

그래픽 시뮬레이션에서 커스텀 할당자가

  1. std::allocator직접 지원하지 않는 정렬 구속 조건 .
  2. 단기 (이 프레임 만) 및 장기 할당에 별도의 풀을 사용하여 조각화를 최소화합니다.
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.