boost :: flat_map và hiệu suất của nó so với map và không có thứ tự_map


103

Kiến thức phổ biến trong lập trình là vị trí bộ nhớ cải thiện hiệu suất rất nhiều do các lần truy cập bộ nhớ cache. Gần đây tôi đã tìm ra cách boost::flat_maptriển khai bản đồ dựa trên vectơ. Nó dường như không phổ biến như thông thường của bạn map/ unordered_mapvì vậy tôi không thể tìm thấy bất kỳ so sánh hiệu suất nào. Nó so sánh như thế nào và các trường hợp sử dụng tốt nhất cho nó là gì?

Cảm ơn!


Điều quan trọng cần lưu ý là boost.org/doc/libs/1_70_0/doc/html/boost/container/… tuyên bố rằng việc chèn ngẫu nhiên mất thời gian logarit, ngụ ý việc điền một boost :: flat_map (bằng cách chèn n phần tử ngẫu nhiên) sẽ lấy O (n log n ) thời gian. Nó đang nói dối, như được thể hiện rõ ràng từ đồ thị trong câu trả lời của @ v.oddou bên dưới: phần chèn ngẫu nhiên là O (n), và n trong số chúng mất O (n ^ 2) thời gian.
Don Hatch

@DonHatch Làm thế nào về báo cáo điều này tại đây: github.com/boostorg/container/issues ? (nó có thể được đưa ra một đếm số so sánh, nhưng đó thực sự là sai lầm nếu không kèm theo một đếm số lần di chuyển)
Marc Glisse

Câu trả lời:


188

Tôi đã chạy một điểm chuẩn cho các cấu trúc dữ liệu khác nhau rất gần đây tại công ty của tôi, vì vậy tôi cảm thấy mình cần phải bỏ qua một từ. Rất phức tạp để đánh giá một cái gì đó một cách chính xác.

Đo điểm chuẩn

Trên web, chúng tôi hiếm khi tìm thấy (nếu có) một điểm chuẩn được thiết kế tốt. Cho đến hôm nay, tôi chỉ tìm thấy các điểm chuẩn được thực hiện theo cách của nhà báo (khá nhanh chóng và quét hàng tá biến dưới thảm).

1) Bạn cần xem xét về sự nóng lên của bộ nhớ cache

Hầu hết mọi người chạy điểm chuẩn đều sợ sự khác biệt về bộ đếm thời gian, do đó họ chạy nội dung của họ hàng nghìn lần và mất toàn bộ thời gian, họ chỉ cẩn thận lấy cùng hàng nghìn lần cho mỗi thao tác và sau đó xem xét điều đó có thể so sánh được.

Sự thật là, trong thế giới thực, điều đó không có ý nghĩa gì, vì bộ nhớ cache của bạn sẽ không ấm và hoạt động của bạn có thể chỉ được gọi một lần. Do đó, bạn cần phải chuẩn bằng RDTSC và thời gian gọi chúng một lần duy nhất. Intel đã làm một bài báo mô tả cách sử dụng RDTSC (sử dụng lệnh cpuid để làm sạch đường ống và gọi nó ít nhất 3 lần vào đầu chương trình để ổn định nó).

2) Đo độ chính xác RDTSC

Tôi cũng khuyên bạn nên làm điều này:

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

Đây là một công cụ đo lường sự khác biệt và nó sẽ lấy giá trị tối thiểu của tất cả các giá trị đo được, để tránh nhận được -10 ** 18 (giá trị âm bản đầu tiên 64 bit) theo thời gian.

Lưu ý việc sử dụng nội tuyến chứ không phải lắp ráp nội tuyến. Hợp ngữ nội tuyến đầu tiên hiếm khi được hỗ trợ bởi trình biên dịch, nhưng tệ hơn nhiều, trình biên dịch tạo ra một rào cản sắp xếp thứ tự đầy đủ xung quanh lắp ráp nội tuyến vì nó không thể phân tích tĩnh bên trong, vì vậy đây là một vấn đề để đánh giá nội dung trong thế giới thực, đặc biệt là khi chỉ gọi nội dung Một lần. Vì vậy, một nội tại phù hợp ở đây, bởi vì nó không phá vỡ việc sắp xếp lại các hướng dẫn tự do của trình biên dịch.

3) tham số

Vấn đề cuối cùng là mọi người thường kiểm tra quá ít biến thể của kịch bản. Hiệu suất vùng chứa bị ảnh hưởng bởi:

  1. Người phân bổ
  2. kích thước của loại chứa
  3. chi phí thực hiện hoạt động sao chép, hoạt động chuyển nhượng, vận hành di chuyển, vận hành xây dựng, thuộc loại hình chứa.
  4. số phần tử trong vùng chứa (kích thước của vấn đề)
  5. loại có 3-phép toán tầm thường
  6. loại là POD

Điểm 1 rất quan trọng vì các vùng chứa luôn phân bổ theo thời gian và điều này rất quan trọng nếu chúng phân bổ bằng cách sử dụng CRT "mới" hoặc một số hoạt động do người dùng xác định, chẳng hạn như phân bổ nhóm hoặc người làm tự do hoặc ...

( đối với những người quan tâm đến pt 1, hãy tham gia chuỗi bí ẩn trên gamedev về tác động của trình phân bổ hệ thống )

Điểm 2 là vì một số vùng chứa (ví dụ A) sẽ mất thời gian sao chép nội dung xung quanh và loại càng lớn thì chi phí càng lớn. Vấn đề là khi so sánh với một thùng chứa B khác, A có thể thắng B đối với loại nhỏ và thua đối với loại lớn hơn.

Điểm 3 giống như điểm 2, ngoại trừ nó nhân chi phí với một số hệ số trọng số.

Điểm 4 là một câu hỏi về chữ O lớn lẫn với các vấn đề về bộ nhớ cache. Một số vùng chứa có độ phức tạp kém phần lớn có thể hoạt động tốt hơn các vùng chứa có độ phức tạp thấp đối với một số loại nhỏ (như mapso với vector, vì vị trí bộ nhớ cache của chúng tốt, nhưngmap phân mảnh bộ nhớ). Và sau đó tại một số điểm giao nhau, chúng sẽ mất đi, bởi vì kích thước tổng thể được chứa bắt đầu "rò rỉ" vào bộ nhớ chính và gây ra lỗi bộ nhớ cache, cộng với thực tế là độ phức tạp tiệm cận có thể bắt đầu được cảm nhận.

Điểm 5 là về việc trình biên dịch có thể giải quyết những thứ trống rỗng hoặc tầm thường tại thời điểm biên dịch. Điều này có thể tối ưu hóa rất nhiều hoạt động, bởi vì các vùng chứa được tạo khuôn mẫu, do đó mỗi loại sẽ có cấu hình hiệu suất riêng.

Điểm 6 cũng giống như điểm 5, POD có thể được hưởng lợi từ thực tế rằng việc xây dựng bản sao chỉ là một bản ghi nhớ và một số vùng chứa có thể có một triển khai cụ thể cho những trường hợp này, bằng cách sử dụng các chuyên môn mẫu từng phần hoặc SFINAE để chọn các thuật toán theo các đặc điểm của T.

Về bản đồ phẳng

Rõ ràng bản đồ phẳng là một trình bao bọc vectơ được sắp xếp, giống như Loki PGSVector, nhưng với một số hiện đại hóa bổ sung đi kèm với C ++ 11, khai thác ngữ nghĩa chuyển động để tăng tốc chèn và xóa các phần tử đơn lẻ.

Đây vẫn là một container được đặt hàng. Hầu hết mọi người thường không cần phần đặt hàng, do đó, sự tồn tại của unordered...

Bạn đã xem xét rằng có thể bạn cần một flat_unorderedmap? đó sẽ là một cái gì đó tương tự google::sparse_maphoặc một cái gì đó tương tự — một bản đồ băm địa chỉ mở.

Vấn đề của bản đồ băm địa chỉ mở là tại thời điểm rehash chúng phải sao chép mọi thứ xung quanh sang vùng đất bằng phẳng mở rộng mới, trong khi một bản đồ không có thứ tự tiêu chuẩn chỉ phải tạo lại chỉ mục băm, trong khi dữ liệu được phân bổ vẫn ở nguyên vị trí của nó. Tất nhiên, nhược điểm là bộ nhớ bị phân mảnh như địa ngục.

Tiêu chí của một rehash trong một bản đồ băm địa chỉ mở là khi dung lượng vượt quá kích thước của vectơ nhóm nhân với hệ số tải.

Một hệ số tải điển hình là 0.8; do đó, bạn cần phải quan tâm đến điều đó, nếu bạn có thể định kích thước trước bản đồ băm của mình trước khi điền nó, hãy luôn định kích thước trước thành: intended_filling * (1/0.8) + epsilonđiều này sẽ đảm bảo cho bạn không bao giờ phải quay lại và sao chép lại mọi thứ trong quá trình điền.

Ưu điểm của bản đồ địa chỉ đóng ( std::unordered..) là bạn không phải quan tâm đến các tham số đó.

Nhưng boost::flat_maplà một vector có thứ tự; do đó, nó sẽ luôn có độ phức tạp tiệm cận log (N), kém hơn so với bản đồ băm địa chỉ mở (thời gian không đổi được phân bổ). Bạn cũng nên xem xét điều đó.

Kết quả điểm chuẩn

Đây là một bài kiểm tra liên quan đến các bản đồ khác nhau (với intkhóa và __int64/ somestructdưới dạng giá trị) và std::vector.

thông tin các loại đã thử nghiệm:

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

Chèn

BIÊN TẬP:

Các kết quả trước đây của tôi có một lỗi: họ thực sự đã thử nghiệm tính năng chèn theo thứ tự, cho thấy hoạt động rất nhanh đối với các bản đồ phẳng.
Tôi đã để những kết quả đó sau đó xuống trang này vì chúng rất thú vị.
Đây là bài kiểm tra chính xác: chèn ngẫu nhiên 100

chèn ngẫu nhiên 10000

Tôi đã kiểm tra việc triển khai, không có thứ gọi là loại hoãn lại được thực hiện trong các bản đồ phẳng ở đây. Mỗi phần chèn sẽ được sắp xếp nhanh chóng, do đó điểm chuẩn này thể hiện các khuynh hướng tiệm cận:

map: O (N * log (N))
hashmaps: O (N)
vector và flatmaps: O (N * N)

Cảnh báo : sau đây, 2 bài kiểm tra cho std::mapvà cả hai đều flat_maplỗi và thực sự kiểm tra việc chèn theo thứ tự (so với chèn ngẫu nhiên cho các vùng chứa khác. Vâng, thật khó hiểu, xin lỗi):
chèn hỗn hợp 100 phần tử mà không cần đặt trước

Chúng ta có thể thấy rằng việc chèn theo thứ tự, dẫn đến đẩy lùi và cực kỳ nhanh chóng. Tuy nhiên, từ các kết quả không có biểu đồ của điểm chuẩn của tôi, tôi cũng có thể nói rằng điều này không gần mức tối ưu tuyệt đối cho việc chèn ngược. Tại 10k phần tử, tính tối ưu chèn ngược hoàn hảo thu được trên một vectơ được đặt trước. Điều này mang lại cho chúng ta 3 triệu chu kỳ; chúng tôi quan sát 4,8 triệu ở đây cho việc chèn có thứ tự vào flat_map(do đó 160% của mức tối ưu).

chèn hỗn hợp 10000 phần tử mà không cần đặt trước Phân tích: hãy nhớ đây là 'chèn ngẫu nhiên' cho vectơ, vì vậy 1 tỷ chu kỳ khổng lồ đến từ việc phải dịch chuyển một nửa (trung bình) dữ liệu lên trên (từng phần tử một) tại mỗi lần chèn.

Tìm kiếm ngẫu nhiên 3 yếu tố (đồng hồ được chuẩn hóa lại thành 1)

kích thước = 100

tìm kiếm rand trong vùng chứa 100 phần tử

kích thước = 10000

tìm kiếm rand trong vùng chứa 10000 phần tử

Lặp lại

trên kích thước 100 (chỉ loại MediumPod)

Lặp lại hơn 100 nhóm trung bình

trên kích thước 10000 (chỉ loại MediumPod)

Lặp lại hơn 10000 nhóm trung bình

Hạt muối cuối cùng

Cuối cùng, tôi muốn quay lại "Đo điểm chuẩn §3 Pt1" (công cụ phân bổ hệ thống). Trong một thử nghiệm gần đây tôi đang thực hiện xung quanh hiệu suất của bản đồ băm địa chỉ mở mà tôi đã phát triển , tôi đã đo được khoảng cách hiệu suất hơn 3000% giữa Windows 7 và Windows 8 trên một số std::unordered_maptrường hợp sử dụng ( thảo luận ở đây ).
Điều này khiến tôi muốn cảnh báo người đọc về các kết quả trên (chúng được thực hiện trên Win7): quãng đường của bạn có thể thay đổi.

trân trọng


1
ồ, trong trường hợp đó, nó có ý nghĩa. Đảm bảo thời gian phân bổ không đổi của Vector chỉ áp dụng khi chèn vào cuối. Việc chèn ở các vị trí ngẫu nhiên nên trung bình O (n) trên mỗi lần chèn vì mọi thứ sau điểm chèn phải được di chuyển về phía trước. Vì vậy, chúng tôi mong đợi hành vi bậc hai trong điểm chuẩn của bạn sẽ tăng lên khá nhanh, ngay cả đối với N. Các triển khai kiểu PGSVector có thể hoãn lại sắp xếp cho đến khi cần tra cứu, chẳng hạn, thay vì sắp xếp sau mỗi lần chèn. Khó nói nếu không thấy điểm chuẩn của bạn.
Billy ONeal

1
@BillyONeal: Ah, chúng tôi đã kiểm tra mã với một đồng nghiệp và tìm ra thủ phạm, việc chèn "ngẫu nhiên" của tôi đã được đặt hàng vì tôi đã sử dụng std :: set để đảm bảo các khóa được chèn là duy nhất. Đây là một sai lầm rõ ràng, nhưng tôi đã sửa điều đó với random_shuffle, tôi đang xây dựng lại ngay bây giờ và một số kết quả mới sẽ sớm xuất hiện sau khi chỉnh sửa. Vì vậy, bài kiểm tra ở trạng thái hiện tại cho thấy rằng "chèn theo thứ tự" là quá nhanh.
v.oddou,

3
"Intel có một bài báo" ← và ở đây nó được
isomorphismes

5
Có lẽ tôi đang thiếu một cái gì đó rõ ràng, nhưng tôi không hiểu tại sao tìm kiếm ngẫu nhiên lại chậm hơn flat_mapso với std::map- có ai có thể giải thích kết quả này không?
boycy

1
Tôi sẽ giải thích nó như một chi phí cụ thể của việc triển khai boost vào thời điểm này, chứ không phải là một đặc điểm nội tại của flat_mapnhư một vùng chứa. Vì Aska::phiên bản nhanh hơn so với việc std::maptra cứu. Chứng minh rằng có chỗ để tối ưu hóa. Hiệu suất dự kiến ​​là tiệm cận như nhau, nhưng có thể tốt hơn một chút nhờ vào vị trí bộ nhớ cache. Với bộ kích thước cao, chúng nên hội tụ.
v.oddou

6

Từ các tài liệu, có vẻ như điều này tương tự với Loki::AssocVectorthứ mà tôi là một người dùng khá nặng. Vì nó dựa trên một vectơ nên nó có các đặc điểm của một vectơ, nghĩa là:

  • Các trình lặp bị vô hiệu hóa bất cứ khi nào sizephát triển vượt quá capacity.
  • Khi nó phát triển vượt quá capacitynó cần phải phân bổ lại và di chuyển các đối tượng, tức là việc chèn không được đảm bảo thời gian liên tục ngoại trừ trường hợp đặc biệt là chèn vào endkhicapacity > size
  • Tra cứu nhanh hơn là std::mapdo cục bộ bộ nhớ cache, một tìm kiếm nhị phân có các đặc tính hiệu suất giống như std::mapcách khác
  • Sử dụng ít bộ nhớ hơn vì nó không phải là cây nhị phân được liên kết
  • Nó không bao giờ co lại trừ khi bạn buộc phải nói với nó (vì điều đó kích hoạt phân bổ lại)

Cách sử dụng tốt nhất là khi bạn biết trước số lượng phần tử (vì vậy bạn có thể reservetrả trước), hoặc khi việc chèn / loại bỏ hiếm khi xảy ra nhưng việc tra cứu thường xuyên. Sự vô hiệu hóa của trình lặp làm cho nó hơi cồng kềnh trong một số trường hợp sử dụng, do đó chúng không thể hoán đổi cho nhau về tính đúng đắn của chương trình.


1
false :) các phép đo ở trên cho thấy bản đồ nhanh hơn flat_map cho các hoạt động tìm kiếm, tôi đoán boost ppl cần phải sửa việc triển khai, nhưng về lý thuyết thì bạn đúng.
NoSenseEtAl
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.