boost :: flat_map 및 map 및 unordered_map과 비교 한 성능


103

메모리 지역성이 캐시 적중으로 인해 성능을 크게 향상 시킨다는 것은 프로그래밍에서 상식입니다. 최근 boost::flat_map에지도의 벡터 기반 구현이 무엇인지 알아 냈습니다 . 당신의 전형적인만큼 인기가하지 않는 것 map/ unordered_map내가 어떤 성능 비교를 찾을 수 없어 있도록. 어떻게 비교하고 최상의 사용 사례는 무엇입니까?

감사!


것이 중요 boost.org/doc/libs/1_70_0/doc/html/boost/container/...의 임의 삽입 청구 N N 로그 (O 소요 (n은 임의의 요소를 삽입하여) 부스트 :: flat_map 채우기 암시 대수 시간이 걸린다 ) 시각. 아래 @ v.oddou의 답변 그래프에서 알 수 있듯이 거짓말입니다. 무작위 삽입은 O (n)이고 그중 n은 O (n ^ 2) 시간이 걸립니다.
Don Hatch

@DonHatch github.com/boostorg/container/issues에서 이것을보고하는 것은 어떻 습니까? (그것은 비교의 수를 제공 할 수 있지만, 이동의 수를 카운트 동반하지 않을 경우 그것은 참으로 잘못된 것입니다)
마크 Glisse

답변:


188

최근에 회사에서 다양한 데이터 구조에 대한 벤치 마크를 실행했기 때문에 한 마디도해야한다고 생각합니다. 무언가를 올바르게 벤치마킹하는 것은 매우 복잡합니다.

벤치마킹

웹에서 우리는 잘 설계된 벤치 마크를 거의 찾을 수 없습니다. 오늘까지 저는 저널리스트 방식으로 수행 된 벤치 마크 만 찾았습니다 (매우 신속하고 카펫 아래에서 수십 개의 변수를 휩쓸고 있음).

1) 캐시 워밍에 대해 고려해야합니다.

벤치 마크를 실행하는 대부분의 사람들은 타이머 불일치를 두려워하므로 물건을 수천 번 실행하고 전체 시간을 소비합니다. 모든 작업에 대해 동일한 수천 번을 수행하도록주의 한 다음 비교 가능한 것으로 간주합니다.

사실은 캐시가 따뜻하지 않고 작업이 한 번만 호출 될 가능성이 있기 때문에 현실 세계에서는 거의 의미가 없습니다. 따라서 RDTSC를 사용하여 벤치마킹하고 한 번만 호출하는 시간을 측정해야합니다. 인텔은 RDTSC를 사용 하는 방법을 설명 하는 문서 만들었습니다 (cpuid 명령을 사용하여 파이프 라인을 플러시하고 프로그램 시작시이를 안정화하기 위해 적어도 3 번 호출).

2) RDTSC 정확도 측정

또한 다음을 수행하는 것이 좋습니다.

u64 g_correctionFactor;  // number of clocks to offset after each measurement to remove the overhead of the measurer itself.
u64 g_accuracy;

static u64 const errormeasure = ~((u64)0);

#ifdef _MSC_VER
#pragma intrinsic(__rdtsc)
inline u64 GetRDTSC()
{
    int a[4];
    __cpuid(a, 0x80000000);  // flush OOO instruction pipeline
    return __rdtsc();
}

inline void WarmupRDTSC()
{
    int a[4];
    __cpuid(a, 0x80000000);  // warmup cpuid.
    __cpuid(a, 0x80000000);
    __cpuid(a, 0x80000000);

    // measure the measurer overhead with the measurer (crazy he..)
    u64 minDiff = LLONG_MAX;
    u64 maxDiff = 0;   // this is going to help calculate our PRECISION ERROR MARGIN
    for (int i = 0; i < 80; ++i)
    {
        u64 tick1 = GetRDTSC();
        u64 tick2 = GetRDTSC();
        minDiff = std::min(minDiff, tick2 - tick1);   // make many takes, take the smallest that ever come.
        maxDiff = std::max(maxDiff, tick2 - tick1);
    }
    g_correctionFactor = minDiff;

    printf("Correction factor %llu clocks\n", g_correctionFactor);

    g_accuracy = maxDiff - minDiff;
    printf("Measurement Accuracy (in clocks) : %llu\n", g_accuracy);
}
#endif

이것은 불일치 측정기이며 때때로 -10 ** 18 (64 비트 첫 음수 값)을 얻는 것을 피하기 위해 모든 측정 값의 최소값을 취합니다.

인라인 어셈블리가 아닌 내장 함수를 사용합니다. 첫 번째 인라인 어셈블리는 오늘날 컴파일러에서 거의 지원되지 않지만 무엇보다도 컴파일러는 내부를 정적으로 분석 할 수 없기 때문에 인라인 어셈블리 주변에 완전한 순서 장벽을 생성합니다. 따라서 이것은 특히 물건을 호출 할 때 실제 세계를 벤치마킹하는 문제입니다. 한번. 따라서 intrinsic이 여기에 적합합니다. 컴파일러가 명령을 자유롭게 재정렬하는 것을 방해하지 않기 때문입니다.

3) 매개 변수

마지막 문제는 사람들이 일반적으로 너무 적은 시나리오 변형을 테스트한다는 것입니다. 컨테이너 성능은 다음에 의해 영향을받습니다.

  1. 할당 자
  2. 포함 된 유형의 크기
  3. 포함 된 유형의 복사 작업, 할당 작업, 이동 작업, 구성 작업의 구현 비용.
  4. 컨테이너의 요소 수 (문제의 크기)
  5. 유형에는 사소한 3- 작업이 있습니다.
  6. 유형은 POD입니다

포인트 1은 컨테이너가 수시로 할당되기 때문에 중요하며, CRT "new"또는 풀 할당이나 freelist 또는 기타와 같은 일부 사용자 정의 작업을 사용하여 할당하는 경우 매우 중요합니다.

( pt 1에 관심이있는 사람들 은 시스템 할당 자 성능 영향에 대한 gamedev의 미스터리 스레드에 참여하세요. )

포인트 2는 일부 컨테이너 (예 : A)가 물건을 복사하는 데 시간을 낭비하고 유형이 클수록 오버 헤드가 커지기 때문입니다. 문제는 다른 컨테이너 B와 비교할 때 A가 작은 유형의 경우 B를 이기고 큰 유형의 경우 잃을 수 있다는 것입니다.

포인트 3은 비용에 일부 가중치를 곱한다는 점을 제외하면 포인트 2와 동일합니다.

포인트 4는 캐시 문제와 혼합 된 빅 O의 문제입니다. 일부 복잡하지 않은 컨테이너는 적은 수의 유형에 대해 복잡도가 낮은 컨테이너를 크게 능가 할 수 있습니다 (예 map: vector캐시 지역 성은 좋지만 map메모리 조각화). 그런 다음 일부 교차 지점에서 포함 된 전체 크기가 주 메모리로 "누수"되기 시작하고 캐시 미스를 유발하고 점근 적 복잡성이 느껴지기 시작하기 때문에 손실됩니다.

요점 5는 컴파일러가 컴파일 타임에 비어 있거나 사소한 것을 제거 할 수 있다는 것입니다. 컨테이너가 템플릿 화되어 있기 때문에 일부 작업을 크게 최적화 할 수 있으므로 각 유형은 자체 성능 프로필을 갖습니다.

포인트 5와 동일한 포인트 6, POD는 복사 구성이 단지 memcpy라는 사실에서 이점을 얻을 수 있으며 일부 컨테이너는 부분 템플릿 전문화 또는 SFINAE를 사용하여 T의 특성에 따라 알고리즘을 선택하여 이러한 경우에 대한 특정 구현을 가질 수 있습니다.

평면지도 정보

분명히 플랫 맵은 Loki AssocVector와 같은 정렬 된 벡터 래퍼이지만 C ++ 11과 함께 제공되는 일부 보완 현대화를 통해 이동 시맨틱을 활용하여 단일 요소의 삽입 및 삭제를 가속화합니다.

이것은 여전히 ​​주문 된 컨테이너입니다. 대부분의 사람들은 일반적으로 주문 부분이 필요하지 않으므로unordered.. .

아마도 당신이 필요하다고 생각 했습니까 flat_unorderedmap? 그것은 다음과 같을 것입니다google::sparse_map 그-오픈 주소 해시 맵과 같은 또는 뭔가.

열린 주소 해시 맵의 문제는 rehash 모든 것을 새로운 확장 평지에 복사해야하는 반면, 표준 무순 맵은 해시 인덱스를 다시 생성해야하는 반면 할당 된 데이터는 그대로 유지된다는 것입니다. 물론 단점은 메모리가 지옥처럼 조각화되어 있다는 것입니다.

열린 주소 해시 맵에서 재해시의 기준은 용량이 버킷 벡터의 크기에 부하 계수를 곱한 값을 초과하는 경우입니다.

일반적인 부하율은 다음과 같습니다 0.8. 따라서 채우기 전에 해시 맵의 크기를 미리 조정할 수 있다면 항상 크기를 미리 조정해야합니다. intended_filling * (1/0.8) + epsilon이렇게하면 채우기 중에 모든 것을 허위로 다시 해시하고 다시 복사 할 필요가 전혀 없습니다.

닫힌 주소 맵 ( std::unordered..) 의 장점 은 이러한 매개 변수에 대해 신경 쓸 필요가 없다는 것입니다.

그러나 boost::flat_map는 정렬 된 벡터입니다. 따라서 항상 log (N) 점근 복잡도를 가지게되며 이는 개방 주소 해시 맵 (상각 된 일정 시간)보다 좋지 않습니다. 이것도 고려해야합니다.

벤치 마크 결과

이것은 ( int키 및 __int64/ somestruct를 값으로) 다른 맵과 관련된 테스트 std::vector입니다.

테스트 유형 정보 :

typeid=__int64 .  sizeof=8 . ispod=yes
typeid=struct MediumTypePod .  sizeof=184 . ispod=yes

삽입

편집하다:

내 이전 결과에는 버그가 포함되어있었습니다. 그들은 실제로 주문 된 삽입을 테스트했는데, 이는 평면지도에서 매우 빠른 동작을 나타 냈습니다.
이 결과는 흥미 롭기 때문에 나중에이 페이지에 남겨 두었습니다.
이것은 올바른 테스트입니다. 랜덤 삽입 100

무작위 삽입 10000

구현을 확인했지만 여기에 플랫 맵에 구현 된 지연된 정렬과 같은 것은 없습니다. 각 삽입은 즉시 정렬되므로이 벤치 마크는 점근 적 경향을 보여줍니다.

맵 : O (N * log (N))
해시 맵 : O (N)
벡터 및 플랫 맵 : O (N * N)

경고 : 이하의 두 테스트 std::map모두 flat_map의가되는 버그가 실제로 테스트하는 것은 삽입 명령 (다른 컨테이너에 대한 임의 삽입 대 네, 죄송 혼란.)
예약없이 100 개 요소의 혼합 삽입

순서대로 삽입하면 뒤로 밀고 매우 빠르다는 것을 알 수 있습니다. 그러나 내 벤치 마크의 차트에 나와 있지 않은 결과를 보면 이것이 역 삽입에 대한 절대적 최적성에 가깝지 않다고 말할 수도 있습니다. 10k 요소에서 사전 예약 된 벡터에서 완벽한 역 삽입 최적 성이 얻어집니다. 이것은 우리에게 3 백만 사이클을줍니다. 여기에서 주문 된 삽입에 대해 4.8M을 관찰합니다 flat_map(따라서 최적의 160 %).

예약없이 10000 개 요소의 혼합 삽입 분석 : 이것이 벡터에 대한 '무작위 삽입'이라는 것을 기억하십시오. 따라서 각 삽입에서 데이터의 절반 (평균)을 위로 (1 요소 1 요소) 이동해야하는 엄청난 10 억 사이클이 발생합니다.

3 개 요소 무작위 검색 (클럭이 1로 재 정규화 됨)

크기 = 100

100 개 요소의 컨테이너 내에서 랜드 검색

크기 = 10000

10000 개 요소의 컨테이너 내에서 rand 검색

되풀이

100 개 이상 (MediumPod 유형 만 해당)

100 개의 중간 포드 반복

10000 이상 (MediumPod 유형 만 해당)

10,000 개의 중간 포드 반복

최종 소금

결국 나는 "Benchmarking §3 Pt1"(시스템 할당 자)로 돌아가고 싶었습니다. 최근에 개발 한 오픈 주소 해시 맵 의 성능에 대해 수행하고있는 실험 에서 일부 std::unordered_map사용 사례 에서 Windows 7과 Windows 8 간의 성능 차이를 3000 % 이상 측정했습니다 ( 여기에서 설명 ).
위의 결과 (Win7에서 생성됨)에 대해 독자에게 경고하고 싶습니다. 마일리지가 다를 수 있습니다.

친애하는


1
오, 그렇다면 말이됩니다. 벡터의 일정 상각 시간 보장은 끝에 삽입 할 때만 적용됩니다. 임의의 위치에 삽입하는 것은 삽입 지점 이후의 모든 항목이 앞으로 이동해야하므로 삽입 당 평균 O (n)이어야합니다. 따라서 우리는 벤치 마크에서 작은 N의 경우에도 매우 빠르게 폭발하는 2 차 동작을 기대할 것입니다. AssocVector 스타일 구현은 아마도 모든 삽입 후에 정렬하는 것이 아니라 조회가 필요할 때까지 정렬을 연기 할 것입니다. 벤치 마크를 보지 않고는 말하기 어렵습니다.
Billy ONeal 2014

1
@BillyONeal : 아, 우리는 동료와 함께 코드를 검사 한 결과 범인을 찾았습니다. 삽입 된 키가 고유한지 확인하기 위해 std :: set을 사용했기 때문에 "무작위"삽입이 주문되었습니다. 이것은 평범한 불완전 함이지만 random_shuffle을 사용하여 지금 재구성 중이며 일부 새로운 결과가 편집 즉시 나타납니다. 따라서 현재 상태의 테스트는 "순서 된 삽입"이 엄청나게 빠르다는 것을 증명합니다.
v.oddou

3
"인텔은 논문을 가지고 있습니다"← 그리고 여기 있습니다
동 형사상

5
아마도 내가 뭔가 분명 실종 해요,하지만 무작위 검색이에 느린 왜 이해가 안 flat_map비교 std::map- 사람이 결과를 설명 할 수있다?
boycy

1
나는 flat_map컨테이너로서의 본질적인 특성이 아니라 이번 부스트 구현의 특정 오버 헤드로 설명 할 것이다 . 때문에 Aska::버전이 빠르게보다 std::map조회. 최적화의 여지가 있음을 증명합니다. 예상 성능은 점근 적으로 동일하지만 캐시 지역성 덕분에 약간 더 좋을 수 있습니다. 크기가 큰 세트에서는 수렴해야합니다.
v.oddou

6

문서에서 Loki::AssocVector이것은 내가 상당히 무거운 사용자 인 것과 유사한 것 같습니다 . 벡터를 기반으로하기 때문에 벡터의 특성이 있습니다.

  • 때마다 반복자는 무효가됩니다 size넘어 성장 capacity.
  • capacity재 할당하고 객체를 옮겨야 할 필요 이상으로 커지면 , 즉 end언제 삽입하는 특별한 경우를 제외하고는 삽입이 일정한 시간을 보장하지 않습니다.capacity > size
  • 조회는 std::map캐시 지역성, std::map다른 것과 동일한 성능 특성을 갖는 이진 검색으로 인해 더 빠릅니다.
  • 연결된 이진 트리가 아니기 때문에 메모리를 적게 사용합니다.
  • 강제로 지시하지 않는 한 절대 축소되지 않습니다 (재 할당을 트리거하기 때문에)

가장 좋은 용도는 요소의 수를 미리 알고 reserve있거나 (따라서 미리 할 수 ​​있음 ) 삽입 / 제거가 드물지만 조회가 빈번 할 때입니다. 반복자 무효화는 일부 사용 사례에서 약간 번거롭기 때문에 프로그램 정확성 측면에서 상호 교환 할 수 없습니다.


1
false :) 위의 측정은 찾기 작업을 위해 flat_map보다 빠르다는 것을 보여줍니다. boost ppl이 구현을 수정해야한다고 생각하지만 이론적으로는 맞습니다.
NoSenseEtAl
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.