2015-05-18 15 views
20

Gần đây tôi đã hỏi một câu hỏi về Programmers liên quan đến lý do để sử dụng thao tác bit tay loại nguyên thủy trên std::bitset.Hiệu suất của std :: bitset là gì?

Từ thảo luận mà tôi đã kết luận rằng nguyên nhân chính là hiệu suất, mặc dù tôi không biết về bất kỳ cơ sở đo cho ý kiến ​​này. Vì vậy, câu hỏi tiếp theo của tôi là; đạt hiệu suất, nếu có, nhiều khả năng sẽ được phát sinh bằng cách sử dụng std::bitset trên một nguyên thủy?

Câu hỏi là cố ý rộng, bởi vì sau khi tìm kiếm trực tuyến, tôi không thể tìm thấy bất cứ điều gì, vì vậy tôi sẽ lấy những gì tôi có thể nhận được. Về cơ bản tôi là sau khi một nguồn tài nguyên cung cấp một số hồ sơ của std::bitset vs 'pre-bitet' lựa chọn thay thế cho các vấn đề tương tự trên một số kiến ​​trúc máy phổ biến bằng cách sử dụng GCC, Clang và/hoặc VC + +. Có một bài báo rất toàn diện mà attemtps để trả lời câu hỏi này cho vectơ bit:

http://www.cs.up.ac.za/cs/vpieterse/pub/PieterseEtAl_SAICSIT2010.pdf

Đáng tiếc là nó xảy ra trước một trong hai hoặc coi ra khỏi phạm vi std::bitset, vì vậy nó tập trung vào vectơ/triển khai mảng động để thay thế.

Tôi thực sự chỉ muốn biết liệu std::bitsettốt hơn so với các lựa chọn thay thế cho các trường hợp sử dụng mà nó có ý định giải quyết. Tôi đã biết rằng đó là dễ dàng hơnrõ ràng hơn hơn bit-fiddling trên một số nguyên, nhưng có phải là nhanh?

+0

http://stackoverflow.com/questions/11712479/which-bitset-implementation-should-i-use-for-maximum-performance –

+8

Không phải mất nhiều thời gian để chuẩn như nó đã viết câu hỏi của bạn...? –

+5

@TonyD Sẽ mất khoảng một ngày để đưa ra một bộ thử nghiệm toàn diện trên các kiến ​​trúc khác nhau mà có thể sử dụng trong một ý nghĩa chung, và thậm chí sau đó quá trình sẽ dễ bị lỗi vì tôi không phải là một chuyên gia. Tôi không nghĩ rằng nó không hợp lý để hỏi nếu nghiên cứu về điều này đã tồn tại ở nơi khác. – arman

Trả lời

19

Cập nhật

Đó là lứa tuổi kể từ khi tôi đăng một này, nhưng:

Tôi đã biết rằng đó là dễ dàng hơn và rõ ràng hơn so với bit không quan trọng trên một số nguyên , nhưng nó như là Nhanh?

Nếu bạn đang sử dụng bitset theo một cách mà không thực sự làm cho nó rõ ràng hơn và sạch hơn so với bit không quan trọng, như kiểm tra một chút tại một thời điểm thay vì sử dụng một mặt nạ bit, sau đó chắc chắn bạn sẽ mất tất cả những lợi ích mà các hoạt động bitwise cung cấp, như có thể kiểm tra xem 64 bit được đặt cùng một lúc với một mặt nạ hay sử dụng hướng dẫn FFS để nhanh chóng xác định bit nào được đặt trong 64 bit.

Tôi không chắc rằng bitset gánh chịu một hình phạt để sử dụng trong tất cả các cách có thể (ví dụ: sử dụng Bitwise nó operator&), nhưng nếu bạn sử dụng nó như một kích thước cố định mảng boolean đó là khá nhiều cách tôi luôn thấy mọi người sử dụng nó, sau đó bạn thường mất tất cả những lợi ích được mô tả ở trên. Thật không may, chúng tôi không thể có được mức độ biểu đạt của việc chỉ truy cập một bit tại một thời điểm với operator[] và có trình tối ưu hóa tất cả các thao tác bitwise và FFS và FFZ và tiếp tục cho chúng tôi, ít nhất là không kể từ lần cuối cùng tôi được kiểm tra (nếu không, bitset sẽ là một trong những cấu trúc yêu thích của tôi).

Bây giờ nếu bạn định sử dụng bitset<N> bits thay thế cho nhau, ví dụ như, uint64_t bits[N/64] như truy cập cả hai cách tương tự bằng cách sử dụng bitwise, nó có thể là ngang hàng (chưa kiểm tra từ bài đăng cũ này). Nhưng sau đó bạn mất nhiều lợi ích khi sử dụng bitset ngay từ đầu.

for_each phương pháp

Trong quá khứ tôi đã vào một số hiểu lầm, tôi nghĩ rằng, khi tôi đề xuất một phương pháp for_each để lặp qua những thứ như vector<bool>, deque, và bitset. Điểm của phương thức như vậy là sử dụng kiến ​​thức nội bộ của container để lặp qua các phần tử hiệu quả hơn khi gọi một hàm, giống như một số thùng chứa liên kết cung cấp phương thức của riêng mình thay vì sử dụng std::find để làm tốt hơn thời gian tuyến tính. Tìm kiếm. Ví dụ, bạn có thể lặp qua tất cả các bit thiết lập của vector<bool> hoặc bitset nếu bạn có kiến ​​thức nội bộ về các vùng chứa này bằng cách kiểm tra 64 phần tử cùng một lúc bằng cách sử dụng mặt nạ 64 bit khi 64 chỉ mục liền kề bị chiếm đóng và tương tự như vậy sử dụng hướng dẫn FFS khi không phải như vậy.

Nhưng một thiết kế vòng lặp phải làm loại logic vô hướng trong operator++ chắc chắn sẽ phải làm một cái gì đó đáng kể tốn kém hơn, chỉ bởi bản chất mà các vòng lặp được thiết kế trong những trường hợp đặc biệt này. bitset thiếu các trình vòng lặp hoàn toàn và thường làm cho những người muốn sử dụng nó để tránh xử lý bitwise logic để sử dụng operator[] để kiểm tra từng bit riêng lẻ trong một vòng tuần tự chỉ muốn tìm ra các bit nào được đặt. Điều đó cũng không hiệu quả như những gì thực hiện phương pháp for_each có thể thực hiện.

Double/Nested Vòng lặp

Một thay thế cho phương pháp for_each container cụ thể đề xuất trên sẽ được sử dụng lặp kép/lồng nhau: đó là, một iterator bên ngoài mà chỉ vào một tiểu phạm vi của một khác nhau loại trình lặp. Ví dụ về mã khách hàng:

for (auto outer_it = bitset.nbegin(); outer_it != bitset.nend(); ++outer_it) 
{ 
    for (auto inner_it = outer_it->first; inner_it != outer_it->last; ++inner_it) 
      // do something with *inner_it (bit index) 
} 

Mặc dù không phù hợp với loại thiết kế lặp có sẵn trong thùng chứa tiêu chuẩn, điều này có thể cho phép một số tối ưu hóa rất thú vị. Như một ví dụ, hãy tưởng tượng một trường hợp như thế này:

bitset<64> bits = 0x1fbf; // 0b1111110111111; 

Trong trường hợp đó, các iterator bên ngoài có thể, chỉ với một vài lần lặp Bitwise ((FFZ/hoặc/bổ sung), suy ra rằng phạm vi đầu tiên của các bit để xử lý sẽ là bit [0, 6), tại thời điểm đó chúng ta có thể lặp qua tiểu phạm vi đó rất rẻ thông qua trình vòng lặp bên trong/lồng nhau (nó sẽ tăng số nguyên, làm cho ++inner_it tương đương với chỉ ++int). Sau đó, khi chúng ta tăng vòng lặp bên ngoài, nó có thể sau đó rất nhanh chóng, và một lần nữa với một vài hướng dẫn bitwise, xác định rằng phạm vi tiếp theo sẽ là [7, 13]. Sau khi chúng tôi lặp qua tiểu phạm vi đó, chúng tôi đã hoàn tất. Thực hiện việc này như một ví dụ khác:

bitset<16> bits = 0xffff; 

Trong trường hợp này, các tiểu phạm vi đầu tiên và cuối cùng sẽ là [0, 16), và bitset có thể xác định rằng với một hướng dẫn Bitwise duy nhất tại thời điểm đó chúng tôi có thể lặp qua tất cả các bit bộ và sau đó chúng ta đã xong.

Loại thiết kế vòng lặp lồng nhau này sẽ ánh xạ đặc biệt tốt đến vector<bool>, dequebitset cũng như các cấu trúc dữ liệu khác mà mọi người có thể tạo như danh sách chưa được kiểm soát.

Tôi nói rằng theo cách vượt xa chỉ là suy đoán về ghế bành, vì tôi có một tập hợp các cấu trúc dữ liệu giống như số deque thực sự ngang bằng với lặp tuần tự vector (vẫn chậm hơn một cách ngẫu nhiên để truy cập ngẫu nhiên, đặc biệt là nếu chúng ta chỉ lưu trữ một loạt các nguyên thủy và xử lý tầm thường). Tuy nhiên, để đạt được thời gian tương đương với vector cho lặp tuần tự, tôi phải sử dụng các loại kỹ thuật này (phương pháp for_each và các trình vòng lặp đôi/lồng nhau) để giảm số lượng xử lý và phân nhánh đang diễn ra trong mỗi lần lặp. Tôi không thể cạnh tranh thời gian bằng cách khác chỉ sử dụng thiết kế lặp vòng lặp phẳng và/hoặc operator[]. Và tôi chắc chắn không thông minh hơn những người triển khai thư viện chuẩn nhưng đã đưa ra một vùng chứa deque giống như có thể được lặp tuần tự nhanh hơn nhiều và điều đó gợi ý cho tôi rằng đó là vấn đề với thiết kế giao diện chuẩn của trình lặp trong trường hợp này đi kèm với một số chi phí trong những trường hợp đặc biệt mà trình tối ưu hóa không thể tối ưu hóa.

Old trả lời

Tôi là một trong những người sẽ cung cấp cho bạn một câu trả lời thực hiện tương tự, nhưng tôi sẽ cố gắng cung cấp cho bạn một cái gì đó nhiều hơn một chút sâu hơn "just because". Đó là điều tôi đã trải qua qua việc định hình và định thời gian thực tế, không chỉ là sự ngờ vực và hoang tưởng.

Một trong những vấn đề lớn nhất với bitsetvector<bool> là thiết kế giao diện của họ "quá tiện lợi" nếu bạn muốn sử dụng chúng như một mảng các boolean. Tối ưu hóa tất cả cấu trúc mà bạn thiết lập để cung cấp an toàn, giảm chi phí bảo trì, thay đổi ít xâm nhập, v.v. Họ làm một công việc đặc biệt tốt với việc chọn hướng dẫn và phân bổ số lượng đăng ký tối thiểu để làm cho mã chạy nhanh như các lựa chọn thay thế không an toàn, không dễ bảo trì/thay đổi.

Phần làm giao diện bitet "quá tiện lợi" với chi phí hiệu quả là truy cập ngẫu nhiên operator[] cũng như thiết kế trình lặp cho vector<bool>. Khi bạn truy cập vào một trong các chỉ mục này tại chỉ mục n, trước tiên, mã phải tìm ra byte thứ n thuộc về, và sau đó chỉ mục phụ tới bit trong đó. Giai đoạn đầu tiên thường liên quan đến một bộ phận/rshifts chống lại một lvalue cùng với modulo/bitwise và đó là tốn kém hơn so với hoạt động bit thực tế bạn đang cố gắng thực hiện.

Thiết kế iterator cho vector<bool> phải đối mặt với tình trạng khó xử vụng về tương tự nơi nó hoặc có chi nhánh thành mã khác nhau mỗi 8 + lần bạn lặp qua nó hoặc trả tiền rằng loại chi phí lập chỉ mục mô tả ở trên. Nếu trước đây được thực hiện, nó làm cho logic không đối xứng trên lặp đi lặp lại, và thiết kế lặp có xu hướng để có một hit hiệu suất trong những trường hợp hiếm hoi. Để minh họa, nếu vector có phương thức for_each của riêng nó, bạn có thể lặp qua, bao gồm 64 phần tử cùng một lúc bằng cách chỉ che các bit dựa vào mặt nạ 64 bit cho vector<bool> nếu tất cả các bit được đặt mà không kiểm tra từng bit riêng. Nó thậm chí có thể sử dụng FFS để tìm ra phạm vi tất cả cùng một lúc. Một thiết kế iterator sẽ có xu hướng chắc chắn phải làm điều đó trong một thời trang vô hướng hoặc lưu trữ nhiều tiểu bang mà phải được redundantly kiểm tra mỗi iteration. Để truy cập ngẫu nhiên, trình tối ưu hóa dường như không thể tối ưu hóa chi phí lập chỉ mục này để tìm ra byte và bit tương đối nào cần truy cập (có lẽ hơi phụ thuộc vào thời gian chạy) khi không cần thiết và bạn có xu hướng thấy hiệu suất đáng kể đạt được với các bit xử lý mã thủ công hơn theo tuần tự với kiến ​​thức nâng cao về byte/từ/dword/qword mà nó đang làm việc.Đó là một sự so sánh không công bằng, nhưng khó khăn với std::bitset là không có cách nào để so sánh công bằng trong những trường hợp như vậy, nơi mã biết byte nào nó muốn truy cập trước, và thường xuyên hơn không, bạn có xu hướng có thông tin này trước. Đó là một quả táo để so sánh màu cam trong trường hợp truy cập ngẫu nhiên, nhưng bạn thường chỉ cần cam.

Có lẽ sẽ không xảy ra nếu thiết kế giao diện liên quan đến bitset trong đó operator[] trả về proxy, yêu cầu mẫu truy cập hai chỉ mục để sử dụng. Ví dụ, trong trường hợp này, bạn sẽ truy cập bit 8 bằng cách viết bitset[0][6] = true; bitset[0][7] = true; với tham số mẫu để biểu thị kích thước của proxy (64 bit, ví dụ). Một ưu tốt có thể mất một thiết kế như vậy và làm cho nó cạnh tranh với, loại trường cũ tay cách để thực hiện các thao tác bit bằng tay bằng cách dịch đó vào: bitset |= 0x60;

Một thiết kế có thể giúp là nếu bitsets cung cấp một for_each_bit loại phương thức, chuyển một proxy bit tới hàm functor mà bạn cung cấp. Điều đó thực sự có thể có khả năng cạnh tranh với phương pháp thủ công.

std::deque có vấn đề về giao diện tương tự. Hiệu suất của nó không được là chậm hơn nhiều so với std::vector để truy cập tuần tự. Tuy nhiên, thật không may, chúng tôi truy cập nó theo tuần tự sử dụng operator[] được thiết kế để truy cập ngẫu nhiên hoặc thông qua một trình lặp, và đại diện nội bộ của deques chỉ đơn giản là không ánh xạ rất hiệu quả cho một thiết kế dựa trên vòng lặp. Nếu deque cung cấp một phương thức riêng của mình là for_each thì có khả năng nó sẽ có khả năng bắt đầu gần hơn với hiệu suất truy cập tuần tự std::vector's. Đây là một số trường hợp hiếm hoi mà thiết kế giao diện trình tự đi kèm với một số chi phí hiệu quả mà các trình tối ưu hóa thường không thể xóa bỏ. Thường thì các trình tối ưu hóa tốt có thể làm cho tiện ích trở nên miễn phí với chi phí thời gian chạy trong một bản dựng sản xuất, nhưng tiếc là không phải trong mọi trường hợp.

Xin lỗi!

Cũng xin lỗi, khi nhìn lại tôi lang thang một chút với bài đăng này nói về vector<bool>deque ngoài bitset. Đó là bởi vì chúng tôi đã có một codebase, nơi sử dụng ba, và đặc biệt là lặp đi lặp lại thông qua họ hoặc sử dụng chúng với truy cập ngẫu nhiên, thường là các điểm nóng.

táo để cam

Như đã nhấn mạnh trong câu trả lời cũ, so sánh việc sử dụng đơn giản của bitset để loại nguyên thủy với logic bitwise ở mức độ thấp được so sánh táo để cam. Nó không giống như bitset được thực hiện rất không hiệu quả cho những gì nó làm. Nếu bạn thực sự cần truy cập một loạt các bit với một mẫu truy cập ngẫu nhiên, vì một lý do nào đó, cần kiểm tra và thiết lập một chút thời gian, thì nó có thể được thực hiện lý tưởng cho một mục đích như vậy. Nhưng quan điểm của tôi là hầu như tất cả các trường hợp sử dụng tôi gặp phải không yêu cầu điều đó, và khi nó không được yêu cầu, cách học cũ liên quan đến hoạt động bitwise có xu hướng hiệu quả hơn đáng kể.

+1

Trong bài kiểm tra của tôi (www.plflib.org/colony.htm) tốc độ lặp lại deque rất giống với vectơ miễn là bạn đang sử dụng một trình lặp và không phải toán tử []. Ngoài ra, rất tiếc các câu lệnh được tạo ra cho các bit không bao giờ có điểm chuẩn. Logic là âm thanh, nhưng so sánh duy nhất tôi đã thấy chống lại việc thực hiện bitet đi kèm với kết quả rất khác nhau: www.cs.up.ac.za/cs/vpieterse/pub/PieterseEtAl_SAICSIT2010.pdf – metamorphosis

+0

Một phần khó khăn là những điểm chuẩn quá có thể thay đổi rất nhiều: http://www.gotw.ca/gotw/054.htm (mặc dù cũ). Theo từng trường hợp, phụ thuộc vào yếu tố đầu vào, bộ nhớ, phần cứng, triển khai nhà cung cấp, v.v. Điều tôi đang cố gắng giải quyết nhiều hơn ở mức khái niệm. Một deque không cung cấp các yêu cầu tiếp giáp và có thể bao gồm nhiều khối - theo sau tự nhiên, thiết kế trình vòng lặp tuân thủ STL yêu cầu phân nhánh trong các toán tử tăng/giảm (giá rẻ/đắt tiền khác nhau, nhưng có thể nói nó là khái niệm hơn đắt hơn tăng/giảm con trỏ/chỉ mục). –

+0

Chi phí phân nhánh đó sau đó giảm đáng kể với kiểu thiết kế "for_each" được thực hiện trực tiếp với nội bộ của deque. Các bitet/vector so sánh không được nhiều so với những người khác như giấy trích dẫn như phiên bản Qt, nhưng chỉ đơn thuần là chống lại mã logic bit của loại thường gặp trong C. Mặc dù tôi thường khuyên bạn nên cách tiếp cận thực dụng của việc lựa chọn phiên bản đơn giản ưa thích chi phí bảo trì thấp nhất, sau đó lập hồ sơ và đo lường nhiều lần và tối ưu hóa khi cần thiết (và luôn luôn đo lường các tối ưu hóa đó để đảm bảo chúng thực sự tạo sự khác biệt). –

11

Đã làm một bài kiểm tra ngắn profiling std :: bitset vs mảng bool cho truy cập tuần tự và ngẫu nhiên - bạn có thể quá:

#include <iostream> 
#include <bitset> 
#include <cstdlib> // rand 
#include <ctime> // timer 

inline unsigned long get_time_in_ms() 
{ 
    return (unsigned long)((double(clock())/CLOCKS_PER_SEC) * 1000); 
} 


void one_sec_delay() 
{ 
    unsigned long end_time = get_time_in_ms() + 1000; 

    while(get_time_in_ms() < end_time) 
    { 
    } 
} 



int main(int argc, char **argv) 
{ 
    srand(get_time_in_ms()); 

    using namespace std; 

    bitset<5000000> bits; 
    bool *bools = new bool[5000000]; 

    unsigned long current_time, difference1, difference2; 
    double total; 

    one_sec_delay(); 

    total = 0; 
    current_time = get_time_in_ms(); 

    for (unsigned int num = 0; num != 200000000; ++num) 
    { 
     bools[rand() % 5000000] = rand() % 2; 
    } 

    difference1 = get_time_in_ms() - current_time; 
    current_time = get_time_in_ms(); 

    for (unsigned int num2 = 0; num2 != 100; ++num2) 
    { 
     for (unsigned int num = 0; num != 5000000; ++num) 
     { 
      total += bools[num]; 
     } 
    } 

    difference2 = get_time_in_ms() - current_time; 

    cout << "Bool:" << endl << "sum total = " << total << ", random access time = " << difference1 << ", sequential access time = " << difference2 << endl << endl; 


    one_sec_delay(); 

    total = 0; 
    current_time = get_time_in_ms(); 

    for (unsigned int num = 0; num != 200000000; ++num) 
    { 
     bits[rand() % 5000000] = rand() % 2; 
    } 

    difference1 = get_time_in_ms() - current_time; 
    current_time = get_time_in_ms(); 

    for (unsigned int num2 = 0; num2 != 100; ++num2) 
    { 
     for (unsigned int num = 0; num != 5000000; ++num) 
     { 
      total += bits[num]; 
     } 
    } 

    difference2 = get_time_in_ms() - current_time; 

    cout << "Bitset:" << endl << "sum total = " << total << ", random access time = " << difference1 << ", sequential access time = " << difference2 << endl << endl; 

    delete [] bools; 

    cin.get(); 

    return 0; 
} 

Xin lưu ý: xuất ra tổng số tiền là cần thiết để trình biên dịch không tối ưu hóa vòng lặp for - mà một số sẽ thực hiện nếu kết quả của vòng lặp không được sử dụng.

Dưới GCC x64 với các cờ sau: -O2; -Bán; -march = gốc; -fomit-frame-pointer; -std = C++ 11; tôi nhận được kết quả như sau:

Bool mảng: truy cập ngẫu nhiên time = 4695, thời gian truy cập tuần tự = 390

BitSet: thời gian truy cập ngẫu nhiên = 5382, thời gian truy cập tuần tự = 749

+0

một điểm dữ liệu không cho phép bạn đánh giá chi phí tiệm cận. nó có tuyến tính không? bậc hai? thứ gì khác? – sp2danny

2

Ngoài những gì các câu trả lời khác nói về hiệu suất truy cập, cũng có thể có một không gian đáng kể trên không: Các triển khai điển hình bitset<> chỉ đơn giản là sử dụng kiểu số nguyên dài nhất để trả về các bit của chúng. Do đó, đoạn code sau

#include <bitset> 
#include <stdio.h> 

struct Bitfield { 
    unsigned char a:1, b:1, c:1, d:1, e:1, f:1, g:1, h:1; 
}; 

struct Bitset { 
    std::bitset<8> bits; 
}; 

int main() { 
    printf("sizeof(Bitfield) = %zd\n", sizeof(Bitfield)); 
    printf("sizeof(Bitset) = %zd\n", sizeof(Bitset)); 
    printf("sizeof(std::bitset<1>) = %zd\n", sizeof(std::bitset<1>)); 
} 

xuất ra như sau trên máy tính của tôi:

sizeof(Bitfield) = 1 
sizeof(Bitset) = 8 
sizeof(std::bitset<1>) = 8 

Như bạn thấy, trình biên dịch của tôi phân bổ một con số khổng lồ 64 bit để lưu trữ một duy nhất, với cách tiếp cận bitfield, tôi chỉ cần làm tròn tới tám bit.

Yếu tố này tám trong sử dụng không gian có thể trở nên quan trọng nếu bạn có nhiều bit nhỏ.

1

Không phải là một câu trả lời tuyệt vời ở đây, mà là một giai thoại liên quan:

Một vài năm trước, tôi đã làm việc trên phần mềm thời gian thực và chúng tôi chạy vào vấn đề lập kế hoạch. Có một mô-đun đã vượt quá thời gian ngân sách, và điều này rất đáng ngạc nhiên vì mô-đun này chỉ chịu trách nhiệm cho một số ánh xạ và đóng gói/giải nén các bit vào/từ các từ 32 bit.

Hóa ra là mô-đun đã sử dụng std :: bitset. Chúng tôi đã thay thế điều này bằng các thao tác thủ công và thời gian thực hiện giảm từ 3 mili giây xuống 25 micro giây. Đó là một vấn đề hiệu suất đáng kể và một sự cải thiện đáng kể.

Vấn đề là các vấn đề hiệu suất do lớp này gây ra có thể rất thực tế.

Các vấn đề liên quan