Việc triển khai gcc std :: unardered_map có chậm không? Nếu vậy - tại sao?


100

Chúng tôi đang phát triển một phần mềm quan trọng hiệu suất cao trong C ++. Ở đó chúng ta cần một bản đồ băm đồng thời và thực hiện một bản đồ. Vì vậy, chúng tôi đã viết một điểm chuẩn để tìm hiểu xem so sánh với bản đồ băm đồng thời của chúng tôi chậm hơn bao nhiêu std::unordered_map.

Tuy nhiên, std::unordered_mapcó vẻ như là cực kỳ chậm ... Vì vậy, đây là điểm chuẩn vi mô của chúng tôi (đối với bản đồ đồng thời, chúng tôi tạo ra một chuỗi mới để đảm bảo rằng khóa không bị tối ưu hóa và lưu ý rằng tôi không bao giờ chèn 0 vì tôi cũng đánh giá điểm chuẩn với google::dense_hash_map, cần giá trị 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;

(CHỈNH SỬA: toàn bộ mã nguồn có thể được tìm thấy ở đây: http://pastebin.com/vPqf7eya )

Kết quả cho std::unordered_maplà:

inserts: 35126
get    : 2959

Đối với google::dense_map:

inserts: 3653
get    : 816

Đối với bản đồ đồng thời được hỗ trợ bởi tay của chúng tôi (có khóa, mặc dù điểm chuẩn là một luồng - nhưng trong một luồng sinh sản riêng biệt):

inserts: 5213
get    : 2594

Nếu tôi biên dịch chương trình điểm chuẩn mà không hỗ trợ pthread và chạy mọi thứ trong chuỗi chính, tôi nhận được kết quả sau cho bản đồ đồng thời được hỗ trợ bằng tay của chúng tôi:

inserts: 4441
get    : 1180

Tôi biên dịch bằng lệnh sau:

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

Vì vậy, đặc biệt là các lượt chèn trên std::unordered_mapdường như cực kỳ tốn kém - 35 giây so với 3-5 giây cho các bản đồ khác. Ngoài ra thời gian tra cứu có vẻ khá cao.

Câu hỏi của tôi: tại sao lại như vậy? Tôi đọc một câu hỏi khác trên stackoverflow nơi ai đó hỏi, tại sao std::tr1::unordered_maplại chậm hơn việc triển khai của chính anh ta. Ở đó, câu trả lời được xếp hạng cao nhất nói rằng std::tr1::unordered_mapcần phải triển khai một giao diện phức tạp hơn. Nhưng tôi không thể thấy đối số này: chúng tôi sử dụng phương pháp tiếp cận xô trong concurrent_map của chúng tôi, std::unordered_mapsử dụng cả phương pháp tiếp cận xô ( google::dense_hash_mapkhông, nhưng hơnstd::unordered_map ít nhất phải nhanh hơn phiên bản an toàn đồng thời được hỗ trợ bởi tay của chúng tôi?). Ngoài điều đó ra, tôi không thể nhìn thấy bất kỳ thứ gì trong giao diện buộc một tính năng khiến bản đồ băm hoạt động không tốt ...

Vì vậy, câu hỏi của tôi: có đúng là nó std::unordered_mapcó vẻ là rất chậm? Nếu không: điều gì là sai? Nếu có: lý do cho điều đó là gì.

Và câu hỏi chính của tôi: tại sao việc chèn một giá trị vào một giá trị std::unordered_mapquá đắt đỏ (ngay cả khi chúng ta đặt đủ dung lượng lúc đầu, nó không hoạt động tốt hơn nhiều - vì vậy việc tạo lại có vẻ không phải là vấn đề)?

BIÊN TẬP:

Trước hết: vâng, điểm chuẩn được trình bày không hoàn hảo - điều này là do chúng tôi đã chơi rất nhiều với nó và nó chỉ là một vụ hack (ví dụ: uint64phân phối để tạo int trong thực tế không phải là một ý tưởng hay, loại trừ 0 trong vòng lặp là loại ngu ngốc vv ...).

Hiện tại, hầu hết các ý kiến ​​đều giải thích rằng tôi có thể làm cho bản đồ chưa có thứ tự nhanh hơn bằng cách bố trí trước đủ không gian cho nó. Trong ứng dụng của chúng tôi, điều này là không thể: chúng tôi đang phát triển một hệ thống quản lý cơ sở dữ liệu và cần một bản đồ băm để lưu trữ một số dữ liệu trong một giao dịch (ví dụ: khóa thông tin). Vì vậy, bản đồ này có thể là tất cả mọi thứ từ 1 (người dùng chỉ thực hiện một lần chèn và cam kết) đến hàng tỷ mục nhập (nếu xảy ra quét toàn bộ bảng). Nó chỉ là không thể phân bổ trước đủ không gian ở đây (và chỉ cần phân bổ nhiều ngay từ đầu sẽ tiêu tốn quá nhiều bộ nhớ).

Hơn nữa, tôi xin lỗi, rằng tôi đã không trình bày câu hỏi của mình đủ rõ ràng: Tôi không thực sự quan tâm đến việc tạo nhanh bản đồ không có thứ tự (sử dụng bản đồ băm dày đặc của googles hoạt động tốt cho chúng tôi), tôi chỉ thực sự không hiểu sự khác biệt về hiệu suất lớn này đến từ đâu . Nó không thể chỉ là định vị trước (ngay cả khi có đủ bộ nhớ được định vị trước, bản đồ dày đặc có thứ tự độ lớn nhanh hơn so với bản đồ không có thứ tự, bản đồ đồng thời được hỗ trợ bằng tay của chúng ta bắt đầu bằng một mảng có kích thước 64 - vì vậy một bản đồ nhỏ hơn bản đồ không có thứ tự).

Vì vậy, lý do cho hiệu suất tồi tệ này là std::unordered_mapgì? Hoặc được hỏi theo cách khác: Người ta có thể viết một triển khai của std::unordered_mapgiao diện tuân theo tiêu chuẩn và (gần) nhanh như bản đồ băm dày đặc của googles không? Hay có điều gì đó trong tiêu chuẩn buộc người thực hiện phải chọn một cách không hiệu quả để thực hiện nó?

CHỈNH SỬA 2:

Bằng cách lập hồ sơ, tôi thấy rằng rất nhiều thời gian được sử dụng cho các divion số nguyên. std::unordered_mapsử dụng số nguyên tố cho kích thước mảng, trong khi các triển khai khác sử dụng lũy ​​thừa của hai. Tại sao std::unordered_mapsử dụng số nguyên tố? Để hoạt động tốt hơn nếu băm xấu? Đối với hàm băm tốt, imho không tạo ra sự khác biệt.

CHỈNH SỬA 3:

Đây là những con số cho std::map:

inserts: 16462
get    : 16978

Sooooooo: tại sao chèn vào một std::mapnhanh hơn chèn vào một std::unordered_map... Ý tôi là WAT? std::mapcó vị trí kém hơn (cây so với mảng), cần phân bổ nhiều hơn (mỗi lần chèn so với mỗi lần lặp lại + cộng ~ 1 cho mỗi lần va chạm) và quan trọng nhất: có độ phức tạp thuật toán khác (O (logn) so với O (1))!


1
Hầu hết các vùng chứa trong std RẤT thận trọng với các ước tính của chúng, tôi sẽ xem xét số lượng nhóm bạn đang sử dụng (được chỉ định trong hàm tạo) và tăng nó lên một ước tính tốt hơn cho bạn SIZE.
Ylisar

Bạn đã thử concurrent_hash_map từ Intel TBB chưa? threadingbuildingblocks.org/docs/help/reference/...
Bác Học Điên

1
@MadScientist Chúng tôi đã coi là TBB. Vấn đề là việc cấp phép: đó là một dự án nghiên cứu và chúng tôi vẫn chưa chắc mình sẽ xuất bản nó như thế nào (chắc chắn là nguồn mở - nhưng nếu chúng tôi muốn cho phép sử dụng trong một sản phẩm thương mại, thì GPLv2 quá hạn chế). Ngoài ra nó là một sự phụ thuộc khác. Nhưng có thể chúng tôi sẽ sử dụng nó trong một thời gian sau, cho đến nay chúng tôi có thể sống tốt mà không có nó.
Markus Pilman,

1
Chạy nó dưới một hồ sơ, ví dụ như valgrind, có thể rất sâu sắc.
Maxim Egorushkin

1
Vị trí trong bảng băm tốt nhất là tốt hơn một chút so với vị trí trong cây, ít nhất là nếu hàm băm là "ngẫu nhiên". Hàm băm đó đảm bảo bạn hiếm khi truy cập vào các mục lân cận vào những thời điểm gần đó. Lợi thế duy nhất bạn có là mảng bảng băm là một khối liền kề. Dù sao thì điều đó cũng có thể đúng với một cái cây, nếu đống không bị phân mảnh và bạn dựng cây cùng một lúc. Khi kích thước lớn hơn bộ nhớ cache, sự khác biệt về vị trí sẽ tạo ra rất ít nếu có sự khác biệt đối với hiệu suất.
Steve314,

Câu trả lời:


87

Tôi đã tìm ra lý do: đó là Vấn đề của gcc-4.7 !!

Với gcc-4.7

inserts: 37728
get    : 2985

Với gcc-4,6

inserts: 2531
get    : 1565

Vì vậy, std::unordered_maptrong gcc-4.7 bị hỏng (hoặc cài đặt của tôi, là cài đặt gcc-4.7.0 trên Ubuntu - và một cài đặt khác là gcc 4.7.1 trên thử nghiệm debian).

Tôi sẽ gửi báo cáo lỗi .. cho đến lúc đó: KHÔNG sử dụng std::unordered_mapvới gcc 4.7!


Có điều gì ở vùng đồng bằng từ 4,6 có thể gây ra điều đó không?
Mark Canlas

30
Đã có một báo cáo trong danh sách gửi thư. Cuộc thảo luận dường như đang hướng đến "sửa chữa" để max_load_factorxử lý, điều này đã dẫn đến sự khác biệt về hiệu suất.
jxh

Thời điểm không tốt cho lỗi này! Tôi đã nhận được hiệu suất rất kém với unardered_map nhưng tôi rất vui vì nó đã được báo cáo và "sửa".
Bo Lu

+1 - Thật là ngớ ngẩn BBBBBUG .. Tôi tự hỏi điều gì xảy ra với gcc-4.8.2
ikh

2
Bất kỳ cập nhật về lỗi này? Nó có còn tồn tại cho các phiên bản GCC (5+) sau này không?
rph

21

Tôi đoán rằng bạn đã không đúng kích thước của bạn unordered_map, như Ylisar đề xuất. Khi các chuỗi phát triển quá lâu unordered_map, việc triển khai g ++ sẽ tự động chuyển sang một bảng băm lớn hơn và đây sẽ là một lực cản lớn đối với hiệu suất. Nếu tôi nhớ không lầm, unordered_mapmặc định là (số nguyên tố nhỏ nhất lớn hơn) 100.

Tôi không có chronotrên hệ thống của mình, vì vậy tôi đã hẹn giờ 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;
}

Tôi đã sử dụng một SIZEsố 10000000, và phải thay đổi mọi thứ một chút cho phiên bản của tôi về boost. Cũng xin lưu ý, tôi đã định kích thước trước bảng băm để phù hợp SIZE/DEPTH, đây DEPTHlà ước tính chiều dài của chuỗi xô do va chạm băm.

Chỉnh sửa: Howard chỉ ra cho tôi trong các nhận xét rằng hệ số tải tối đa unordered_map1. Vì vậy, các DEPTHđiều khiển kiểm soát số lần mã sẽ đổ lỗi.

#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");
}

Biên tập:

Tôi đã sửa đổi mã để có thể thay đổi DEPTHdễ dàng hơn.

#ifndef DEPTH
#define DEPTH 10000000
#endif

Vì vậy, theo mặc định, kích thước xấu nhất cho bảng băm được chọn.

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

Kết luận của tôi là không có nhiều khác biệt về hiệu suất đáng kể đối với bất kỳ kích thước bảng băm ban đầu nào ngoài việc làm cho nó bằng với toàn bộ số lần chèn duy nhất dự kiến. Ngoài ra, tôi không thấy thứ tự chênh lệch hiệu suất độ lớn mà bạn đang quan sát.


6
std::unordered_mapcó hệ số tải tối đa mặc định là 1. Vì vậy, ngoại trừ số lượng nhóm ban đầu, DEPTH của bạn bị bỏ qua. Nếu muốn, bạn có thể map.max_load_factor(DEPTH).
Howard Hinnant

@HowardHinnant: Cảm ơn vì thông tin đó. Vì vậy DEPTH, nó bị bỏ qua, nhưng nó vẫn kiểm soát tần suất bản đồ sẽ được tạo lại thành một bản đồ lớn hơn. Câu trả lời đã được cập nhật, và cảm ơn một lần nữa
jxh 23/07/12

@ user315052 Có, tôi biết tôi có thể làm cho nó tốt hơn bằng cách đặt cho nó kích thước phù hợp ngay từ đầu - nhưng tôi không thể làm điều đó trong phần mềm của chúng tôi (đó là một dự án nghiên cứu - một DBMS - và ở đó tôi không thể biết mình sẽ chèn bao nhiêu - nó có thể thay đổi từ 0 đến 1 tỷ ...). Nhưng ngay cả với pre kim loại, nó vẫn chậm hơn so với bản đồ của chúng tôi và chậm hơn so với googles secure_map - Tôi vẫn đang tự hỏi điều gì tạo nên sự khác biệt lớn.
Markus Pilman

@MarkusPilman: Tôi không biết kết quả của tôi như thế nào so với kết quả của bạn, vì bạn chưa bao giờ cung cấp mức độ lớn SIZEmà bạn đã làm việc. Tôi có thể nói unordered_maplà nhanh hơn gấp đôi với DEPTHthiết lập 1và phân bổ trước đúng cách.
jxh

1
@MarkusPilman: Thời gian của tôi chỉ tính bằng giây. Tôi nghĩ thời gian của bạn tính bằng mili giây. Nếu các lần chèn DEPTHđược đặt thành 1mất ít hơn 3giây, thì mức độ này chậm hơn như thế nào?
jxh

3

Tôi đã chạy mã của bạn bằng máy tính 64 bit / AMD / 4 lõi (2.1GHz) và nó cho tôi kết quả sau:

MinGW-W64 4.9.2:

Sử dụng std :: unardered_map:

inserts: 9280 
get: 3302

Sử dụng std :: map:

inserts: 23946
get: 24824

VC 2015 với tất cả các cờ tối ưu hóa mà tôi biết:

Sử dụng std :: unardered_map:

inserts: 7289
get: 1908

Sử dụng std :: map:

inserts: 19222 
get: 19711

Tôi chưa kiểm tra mã bằng GCC nhưng tôi nghĩ nó có thể tương đương với hiệu suất của VC, vì vậy nếu điều đó là đúng, thì GCC 4.9 std :: unardered_map nó vẫn bị hỏng.

[BIÊN TẬP]

Vì vậy, có, như ai đó đã nói trong các bình luận, không có lý do gì để nghĩ rằng hiệu suất của GCC 4.9.x có thể so sánh với hiệu suất của VC. Khi tôi có thay đổi, tôi sẽ kiểm tra mã trên GCC.

Câu trả lời của tôi chỉ là thiết lập một số loại cơ sở kiến ​​thức cho các câu trả lời khác.


"Tôi chưa thử nghiệm mã bằng GCC nhưng tôi nghĩ rằng nó có thể tương đương với hiệu suất của VC." Tuyên bố hoàn toàn vô căn cứ, không có bất kỳ điểm chuẩn nào có thể so sánh với điểm chuẩn được tìm thấy trong bài đăng gốc. "Câu trả lời" này không trả lời câu hỏi theo bất kỳ nghĩa nào, chứ chưa nói đến việc trả lời câu hỏi "tại sao".
4ae1e1

2
"Tôi chưa kiểm tra mã bằng GCC" ... làm thế nào mà bạn quản lý để có được và sử dụng MinGW trong khi biết quá ít về nó? MinGW về cơ bản là một cổng theo dõi chặt chẽ của GCC.
underscore_d
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.