gcc std :: unordered_map 구현이 느립니까? 그렇다면-왜?


100

우리는 C ++로 고성능 핵심 소프트웨어를 개발하고 있습니다. 거기에 동시 해시 맵이 필요하고 구현되었습니다. 그래서 우리는 동시 해시 맵이 .NET과 비교하여 얼마나 느린 지 알아 내기 위해 벤치 마크를 작성했습니다 std::unordered_map.

그러나 std::unordered_map엄청나게 느린 것 같습니다 ... 그래서 이것은 우리의 마이크로 벤치 마크입니다 google::dense_hash_map. null 값이 필요함) :

boost::random::mt19937 rng;
boost::random::uniform_int_distribution<> dist(std::numeric_limits<uint64_t>::min(), std::numeric_limits<uint64_t>::max());
std::vector<uint64_t> vec(SIZE);
for (int i = 0; i < SIZE; ++i) {
    uint64_t val = 0;
    while (val == 0) {
        val = dist(rng);
    }
    vec[i] = val;
}
std::unordered_map<int, long double> map;
auto begin = std::chrono::high_resolution_clock::now();
for (int i = 0; i < SIZE; ++i) {
    map[vec[i]] = 0.0;
}
auto end = std::chrono::high_resolution_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end - begin);
std::cout << "inserts: " << elapsed.count() << std::endl;
std::random_shuffle(vec.begin(), vec.end());
begin = std::chrono::high_resolution_clock::now();
long double val;
for (int i = 0; i < SIZE; ++i) {
    val = map[vec[i]];
}
end = std::chrono::high_resolution_clock::now();
elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end - begin);
std::cout << "get: " << elapsed.count() << std::endl;

(편집 : 전체 소스 코드는 여기에서 찾을 수 있습니다 : http://pastebin.com/vPqf7eya )

결과 std::unordered_map는 다음과 같습니다.

inserts: 35126
get    : 2959

대상 google::dense_map:

inserts: 3653
get    : 816

수작업으로 지원되는 동시 맵의 경우 (잠금을 수행하지만 벤치 마크는 단일 스레드이지만 별도의 스폰 스레드에 있음) :

inserts: 5213
get    : 2594

pthread 지원없이 벤치 마크 프로그램을 컴파일하고 메인 스레드에서 모든 것을 실행하면 수동으로 지원되는 동시 맵에 대해 다음과 같은 결과가 나타납니다.

inserts: 4441
get    : 1180

다음 명령으로 컴파일합니다.

g++-4.7 -O3 -DNDEBUG -I/tmp/benchmap/sparsehash-2.0.2/src/ -std=c++11 -pthread main.cc

따라서 특히 삽입물은 std::unordered_map매우 비쌉니다. 다른지도의 경우 35 초 대 3-5 초입니다. 또한 조회 시간이 상당히 높은 것 같습니다.

내 질문 : 왜 그렇습니까? 누군가가 std::tr1::unordered_map자신의 구현보다 느린 이유를 묻는 stackoverflow에 대한 또 다른 질문을 읽었습니다 . 가장 높은 등급의 답변 std::tr1::unordered_map은 더 복잡한 인터페이스를 구현해야한다는 것입니다. 그러나 나는이 주장을 볼 수 없다 : 우리 std::unordered_map는 Concurrent_map에서 버킷 접근 방식을 사용하고, 버킷 접근 방식도 사용한다 ( google::dense_hash_map그렇지 않지만 std::unordered_map적어도 우리의 손으로 지원하는 동시성 안전 버전보다 빠르다?). 그 외에는 인터페이스에서 해시 맵의 성능을 저하시키는 기능을 강제하는 것을 볼 수 없습니다.

그래서 내 질문 : std::unordered_map매우 느리게 보이는 것이 사실 입니까? 아니오 인 경우 : 무엇이 잘못 되었습니까? 그렇다면 : 그 이유는 무엇입니까?

그리고 내 주요 질문 : 왜 값을 std::unordered_map끔찍한 비용 으로 삽입하는 것입니까 (처음에 충분한 공간을 예약하더라도 훨씬 더 잘 수행되지 않으므로 다시 해싱이 문제가 아닌 것 같습니다)?

편집하다:

우선 : 예, 제시된 벤치 마크는 완벽하지 않습니다. 이것은 우리가 그것을 많이 가지고 놀았고 단지 해킹이기 때문입니다 (예를 들어 uint64int를 생성 하는 배포는 실제로 좋은 생각이 아닙니다. 루프에서 0을 제외합니다. 어리석은 등 ...).

현재 대부분의 댓글은 충분한 공간을 미리 할당하여 unorder_map을 더 빠르게 만들 수 있다고 설명합니다. 우리 애플리케이션에서는 이것이 불가능합니다. 데이터베이스 관리 시스템을 개발 중이며 트랜잭션 중에 일부 데이터 (예 : 잠금 정보)를 저장할 해시 맵이 필요합니다. 따라서이 맵은 1 (사용자가 하나의 삽입 및 커밋 만 수행)에서 수십억 개의 항목 (전체 테이블 스캔이 발생하는 경우)에 이르기까지 모든 것이 될 수 있습니다. 여기에 충분한 공간을 미리 할당하는 것은 불가능합니다 (처음에 많이 할당하면 너무 많은 메모리가 소모됩니다).

또한, 나는 내 질문을 충분히 명확하게 말하지 않은 것에 대해 사과드립니다. 나는 unorder_map을 빠르게 만드는 데 정말로 관심이 없습니다 (구글의 고밀도 해시 맵을 사용하면 잘 작동합니다).이 거대한 성능 차이가 어디서 오는지 실제로 이해하지 못합니다. . 사전 할당 일 수는 없습니다 (사전 할당 된 메모리가 충분하더라도 조밀 한 맵은 unordered_map보다 훨씬 빠르며, 손으로 지원하는 동시 맵은 64 크기의 배열로 시작하므로 unorder_map보다 작은 것입니다).

그래서이 나쁜 성능의 이유는 무엇 std::unordered_map입니까? 또는 다르게 질문 : std::unordered_map표준 준수 및 (거의) Google의 고밀도 해시 맵만큼 빠른 인터페이스 구현을 작성할 수 있습니까? 아니면 표준에 구현자가이를 구현하기 위해 비효율적 인 방법을 선택하도록 강제하는 것이 있습니까?

편집 2 :

프로파일 링을 통해 정수 분할에 많은 시간이 사용된다는 것을 알 수 있습니다. std::unordered_map배열 크기에 소수를 사용하는 반면 다른 구현은 2의 거듭 제곱을 사용합니다. std::unordered_map소수를 사용하는 이유는 무엇 입니까? 해시가 나쁘면 더 잘 수행하려면? 좋은 해시의 경우 imho는 아무런 차이가 없습니다.

편집 3 :

다음에 대한 숫자는 std::map다음과 같습니다.

inserts: 16462
get    : 16978

Sooooooo : 왜 삽입이 a에 삽입하는 std::map것보다 빠른 이유는 std::unordered_map... WAT를 의미합니까? std::map지역성이 더 나쁘고 (트리 대 배열), 더 많은 할당 (삽입 대 리해 시당 + 각 충돌에 대해 ~ 1)을 만들어야하며 가장 중요한 것은 다른 알고리즘 복잡성 (O (logn) 대 O (1))이 있습니다!


1
std의 대부분의 컨테이너는 추정치에 매우 보수적이며 사용중인 버킷 수 (constructor에 지정됨)를 살펴보고 더 나은 추정치로 늘릴 것 SIZE입니다.
Ylisar

인텔 TBB에서 concurrent_hash_map을 사용해 보셨습니까? threadingbuildingblocks.org/docs/help/reference/...
매드 사이언티스트

1
@MadScientist TBB를 고려했습니다. 문제는 라이센싱입니다. 그것은 연구 프로젝트이고 우리가 그것을 어떻게 게시 할 것인지 아직 확실하지 않습니다 (가장 확실히 오픈 소스이지만 상용 제품에서 사용을 허용하려면 GPLv2가 너무 제한적입니다). 또한 또 다른 의존성입니다. 그러나 우리는 나중에 그것을 사용할 것입니다. 지금까지는 그것 없이도 잘 살 수 있습니다.
Markus Pilman 2012

1
프로파일 러 (예 : valgrind)에서 실행하면 통찰력이있을 수 있습니다.
Maxim Egorushkin 2012

1
해시 테이블의 지역 성은 최소한 해시 함수가 "무작위"인 경우 트리의 지역성보다 약간 더 좋습니다. 이 해시 기능은 가까운 시간에 근처 항목에 거의 액세스하지 못하도록합니다. 유일한 장점은 해시 테이블 배열이 하나의 연속 블록이라는 것입니다. 어쨌든 힙이 조각화되지 않고 한 번에 트리를 빌드하는 경우 트리에 대해 사실 일 수 있습니다. 크기가 캐시보다 크면 지역성 차이는 성능에 거의 차이가 없습니다.
Steve314

답변:


87

이유를 찾았습니다. gcc-4.7의 문제입니다 !!

GCC-4.7

inserts: 37728
get    : 2985

GCC-4.6

inserts: 2531
get    : 1565

따라서 std::unordered_mapgcc-4.7에서 손상되었습니다 (또는 Ubuntu에 gcc-4.7.0을 설치하고 데비안 테스트에서 gcc 4.7.1을 설치 한 다른 설치).

버그 보고서를 제출하겠습니다. 그때까지 : std::unordered_mapgcc 4.7과 함께 사용하지 마십시오 !


그 원인이 될 4.6의 델타에 어떤 것이 있습니까?
Mark Canlas 2012

30
메일 링리스트에 이미 보고서가 있습니다. 논의는 max_load_factor처리에 대한 "수정"을 가리키는 것으로 보이며 , 이로 인해 성능 차이가 발생했습니다.
jxh

이 버그에 대한 잘못된 타이밍! unorder_map에서 성능이 매우 나빠졌지만보고되고 "고정"되어 기쁩니다.
Bo Lu

+1-이 얼마나 짜증나는 BBBBBUG .. gcc-4.8.2에서 무슨 일이 일어나는지 궁금합니다
ikh

2
이 버그에 대한 업데이트가 있습니까? 이후 버전의 GCC (5+)에도 여전히 존재하나요?
rph

21

난 당신이 제대로 크기하지 않은 것으로 추측하고 unordered_mapYlisar가 제안. 에서 체인이 너무 길어 unordered_map지면 g ++ 구현이 자동으로 더 큰 해시 테이블로 다시 해시되며 이는 성능에 큰 영향을 미칩니다. 올바르게 기억하면 unordered_map기본값은 (보다 작은 소수보다 큼) 100입니다.

나는하지 않았다 chrono내가 함께 시간 초과, 그래서 내 시스템에 times().

template <typename TEST>
void time_test (TEST t, const char *m) {
    struct tms start;
    struct tms finish;
    long ticks_per_second;

    times(&start);
    t();
    times(&finish);
    ticks_per_second = sysconf(_SC_CLK_TCK);
    std::cout << "elapsed: "
              << ((finish.tms_utime - start.tms_utime
                   + finish.tms_stime - start.tms_stime)
                  / (1.0 * ticks_per_second))
              << " " << m << std::endl;
}

내가 사용 SIZE의를 10000000, 그리고 내 버전에 대한 것들을 약간 수정했다 boost. 또한 일치하도록 해시 테이블의 크기를 미리 조정했습니다 SIZE/DEPTH. 여기서는 DEPTH해시 충돌로 인한 버킷 체인 길이의 추정치입니다.

편집 : Howard는 최대 부하 계수 unordered_map1. 따라서 DEPTH코드가 재해시되는 횟수를 제어합니다.

#define SIZE 10000000
#define DEPTH 3
std::vector<uint64_t> vec(SIZE);
boost::mt19937 rng;
boost::uniform_int<uint64_t> dist(std::numeric_limits<uint64_t>::min(),
                                  std::numeric_limits<uint64_t>::max());
std::unordered_map<int, long double> map(SIZE/DEPTH);

void
test_insert () {
    for (int i = 0; i < SIZE; ++i) {
        map[vec[i]] = 0.0;
    }
}

void
test_get () {
    long double val;
    for (int i = 0; i < SIZE; ++i) {
        val = map[vec[i]];
    }
}

int main () {
    for (int i = 0; i < SIZE; ++i) {
        uint64_t val = 0;
        while (val == 0) {
            val = dist(rng);
        }
        vec[i] = val;
    }
    time_test(test_insert, "inserts");
    std::random_shuffle(vec.begin(), vec.end());
    time_test(test_insert, "get");
}

편집하다:

DEPTH좀 더 쉽게 변경할 수 있도록 코드를 수정했습니다 .

#ifndef DEPTH
#define DEPTH 10000000
#endif

따라서 기본적으로 해시 테이블에 대한 최악의 크기가 선택됩니다.

elapsed: 7.12 inserts, elapsed: 2.32 get, -DDEPTH=10000000
elapsed: 6.99 inserts, elapsed: 2.58 get, -DDEPTH=1000000
elapsed: 8.94 inserts, elapsed: 2.18 get, -DDEPTH=100000
elapsed: 5.23 inserts, elapsed: 2.41 get, -DDEPTH=10000
elapsed: 5.35 inserts, elapsed: 2.55 get, -DDEPTH=1000
elapsed: 6.29 inserts, elapsed: 2.05 get, -DDEPTH=100
elapsed: 6.76 inserts, elapsed: 2.03 get, -DDEPTH=10
elapsed: 2.86 inserts, elapsed: 2.29 get, -DDEPTH=1

내 결론은 예상되는 전체 고유 삽입 수와 동일하게 만드는 것 외에는 초기 해시 테이블 크기에 대해 큰 성능 차이가 없다는 것입니다. 또한, 나는 당신이 관찰하고있는 성능 차이를 볼 수 없습니다.


6
std::unordered_map기본 최대로드 계수는 1입니다. 따라서 초기 버킷 수를 제외하고 DEPTH는 무시됩니다. 원하는 경우 할 수 있습니다 map.max_load_factor(DEPTH).
하워드 Hinnant

@HowardHinnant : 정보 감사합니다. 따라서 DEPTH는 무시되지만지도가 더 큰지도로 다시 해시되는 빈도를 제어합니다. 대답은 업데이트 및되었습니다 다시 한번 감사
JXH

@ user315052 예, 처음에는 정상적인 크기를 제공하여 더 나아질 수 있다는 것을 압니다.하지만 우리 소프트웨어에서는 그렇게 할 수 없습니다 (연구 프로젝트-DBMS-거기에 얼마나 삽입할지 모르겠습니다- 0에서 10 억까지 다양합니다 ...). 그러나 사전 할당으로도 그것은 우리의지도보다 느리고 googles density_map보다 훨씬 느립니다. 나는 여전히 큰 차이를 만드는 것이 무엇인지 궁금합니다.
마르쿠스 Pilman

@MarkusPilman : SIZE당신이 얼마나 큰 작업을 했는지 알려주지 않았기 때문에 내 결과가 당신의 결과와 어떻게 비교되는지 모르겠습니다 . 설정 하고 적절하게 사전 할당 하면 unordered_map두 배 더 빠르다고 말할 수 있습니다 . DEPTH1
jxh

1
@MarkusPilman : 내 시간은 이미 초 단위입니다. 나는 당신의 시간이 밀리 초라고 생각했습니다. 로 DEPTH설정된 삽입 13몇 초 미만으로 걸리는 경우 , 이것이 얼마나 느린가요?
jxh

3

64 비트 / AMD / 4 코어 (2.1GHz) 컴퓨터를 사용하여 코드를 실행 했으며 결과는 다음과 같습니다.

MinGW-W64 4.9.2 :

사용 표준 :: unordered_map도를 :

inserts: 9280 
get: 3302

사용하여 표준 : :지도 :

inserts: 23946
get: 24824

내가 아는 모든 최적화 플래그가있는 VC 2015 :

사용 표준 :: unordered_map도를 :

inserts: 7289
get: 1908

사용하여 표준 : :지도 :

inserts: 19222 
get: 19711

GCC를 사용하여 코드를 테스트하지는 않았지만 VC의 성능과 비슷할 수 있다고 생각하므로 이것이 사실이면 GCC 4.9 std :: unordered_map 여전히 손상되었습니다.

[편집하다]

따라서 누군가 의견에서 말했듯이 GCC 4.9.x의 성능이 VC 성능과 비슷할 것이라고 생각할 이유가 없습니다. 변경 사항이 있으면 GCC에서 코드를 테스트 할 것입니다.

내 대답은 다른 답변에 대한 일종의 지식 기반을 구축하는 것입니다.


"GCC를 사용하여 코드를 테스트하지는 않았지만 VC의 성능과 비슷할 것 같습니다." 원본 게시물에서 발견 된 것과 비교할만한 벤치마킹없이 완전히 근거없는 주장. 이 "답변"은 "왜"질문에 대한 대답은 말할 것도없고 어떤 의미에서도 질문에 대답하지 않습니다.
4ae1e1 2015

2
"GCC를 사용하여 코드를 테스트하지 않았습니다."... MinGW에 대해 거의 알지 못하면서 어떻게 획득하고 사용할 수 있었습니까? MinGW는 근본적으로 GCC의 밀접한 추적 포트입니다.
underscore_d
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.