Làm cách nào để đếm số bit được đặt trong số nguyên 32 bit?


868

8 bit đại diện cho số 7 trông như thế này:

00000111

Ba bit được đặt.

Các thuật toán để xác định số lượng bit thiết lập trong một số nguyên 32 bit là gì?


101
Đây là trọng lượng Hamming BTW.
Purfideas

11
Ứng dụng trong thế giới thực này là gì? (Điều này không được coi là một lời chỉ trích - Tôi chỉ tò mò thôi.)
jonmorgan

8
Tính toán bit chẵn lẻ (tra cứu), được sử dụng như phát hiện lỗi đơn giản trong giao tiếp.
Dialecticus

8
@Dialecticus, tính toán một bit chẵn lẻ rẻ hơn so với tính toán trọng lượng Hamming
vây

15
@spookyjon Giả sử bạn có một biểu đồ được biểu diễn dưới dạng ma trận kề, về cơ bản là một tập hợp bit. Nếu bạn muốn tính số cạnh của một đỉnh, nó sẽ rút gọn để tính trọng số Hamming của một hàng trong tập bit.
fuz

Câu trả lời:


850

Điều này được gọi là ' Trọng lượng Hamming ', 'dân số' hoặc 'bổ sung sang một bên'.

Thuật toán 'tốt nhất' thực sự phụ thuộc vào CPU bạn đang sử dụng và kiểu sử dụng của bạn là gì.

Một số CPU có một lệnh dựng sẵn để thực hiện và một số khác có các lệnh song song hoạt động trên các vectơ bit. Các hướng dẫn song song (như x86 popcnt, trên CPU có hỗ trợ) gần như chắc chắn sẽ nhanh nhất. Một số kiến ​​trúc khác có thể có một lệnh chậm được triển khai với một vòng lặp được mã hóa để kiểm tra một bit trên mỗi chu kỳ ( cần dẫn nguồn ).

Phương pháp tra cứu bảng được điền trước có thể rất nhanh nếu CPU của bạn có bộ đệm lớn và / hoặc bạn đang thực hiện nhiều hướng dẫn này trong một vòng lặp chặt chẽ. Tuy nhiên, nó có thể bị ảnh hưởng do chi phí của 'lỗi bộ nhớ cache', trong đó CPU phải lấy một số bảng từ bộ nhớ chính. (Tra cứu từng byte riêng biệt để giữ cho bảng nhỏ.)

Nếu bạn biết rằng byte của bạn sẽ chủ yếu là 0 hoặc chủ yếu là 1 thì có các thuật toán rất hiệu quả cho các kịch bản này.

Tôi tin rằng một thuật toán có mục đích chung rất tốt là như sau, được gọi là 'thuật toán SWAR song song' hoặc 'độ chính xác biến'. Tôi đã diễn đạt điều này bằng ngôn ngữ giả giống như C, bạn có thể cần điều chỉnh nó để hoạt động với một ngôn ngữ cụ thể (ví dụ: sử dụng uint32_t cho C ++ và >>> trong Java):

int numberOfSetBits(uint32_t i)
{
     // Java: use int, and use >>> instead of >>
     // C or C++: use uint32_t
     i = i - ((i >> 1) & 0x55555555);
     i = (i & 0x33333333) + ((i >> 2) & 0x33333333);
     return (((i + (i >> 4)) & 0x0F0F0F0F) * 0x01010101) >> 24;
}

Đối với JavaScript: ép buộc thành số nguyên với |0hiệu suất: thay đổi dòng đầu tiên thànhi = (i|0) - ((i >> 1) & 0x55555555);

Điều này có hành vi trong trường hợp xấu nhất tốt nhất trong số các thuật toán được thảo luận, do đó sẽ xử lý hiệu quả với bất kỳ mô hình sử dụng hoặc giá trị nào bạn ném vào nó.


Cách thức hoạt động của bithack SWAR này:

i = i - ((i >> 1) & 0x55555555);

Bước đầu tiên là một phiên bản tối ưu hóa của mặt nạ để cô lập các bit lẻ / chẵn, chuyển sang sắp xếp chúng và thêm. Điều này thực sự có 16 bổ sung riêng biệt trong các bộ tích lũy 2 bit ( SWAR = SIMD trong một thanh ghi ). Thích (i & 0x55555555) + ((i>>1) & 0x55555555).

Bước tiếp theo lấy số lẻ / thậm chí tám trong số các bộ tích lũy 16x 2 bit đó và thêm một lần nữa, tạo ra các khoản tiền 4 bit 4 bit. Việc i - ...tối ưu hóa không thể thực hiện được trong lần này vì vậy nó chỉ che dấu trước / sau khi chuyển. Sử dụng cùng một 0x33...hằng số cả hai lần thay vì 0xccc...trước khi dịch chuyển là một điều tốt khi biên dịch cho các ISA cần xây dựng các hằng số 32 bit trong các thanh ghi riêng biệt.

Bước dịch chuyển và thêm cuối cùng của việc (i + (i >> 4)) & 0x0F0F0F0Fmở rộng tới các bộ tích lũy 8 bit 8 bit. Nó che dấu sau khi thêm thay vì trước đó, bởi vì giá trị tối đa trong bất kỳ bộ tích lũy 4 bit nào là 4, nếu tất cả 4 bit của các bit đầu vào tương ứng được đặt. 4 + 4 = 8 vẫn phù hợp với 4 bit, do đó, việc mang giữa các phần tử nibble là không thể i + (i >> 4).

Cho đến nay, đây chỉ là SIMD khá bình thường sử dụng các kỹ thuật SWAR với một vài tối ưu hóa thông minh. Tiếp tục với cùng một mẫu cho 2 bước nữa có thể mở rộng lên gấp 2 lần 16 bit sau đó tăng gấp đôi số lượng 32 bit. Nhưng có một cách hiệu quả hơn trên các máy có nhân phần cứng nhanh:

Khi chúng ta có đủ "yếu tố", một bội số với hằng số ma thuật có thể tổng hợp tất cả các yếu tố thành yếu tố hàng đầu . Trong trường hợp này các phần tử byte. Nhân lên được thực hiện bằng cách dịch chuyển trái và thêm, do đó, bội số x * 0x01010101kết quả trong x + (x<<8) + (x<<16) + (x<<24). Các phần tử 8 bit của chúng tôi đủ rộng (và giữ số lượng đủ nhỏ) mà phần tử này không tạo ra mang theo 8 bit hàng đầu đó.

Phiên bản 64 bit này có thể thực hiện các phần tử 8 bit 8 bit trong số nguyên 64 bit với số nhân 0x0101010101010101 và trích xuất byte cao bằng >>56. Vì vậy, nó không thực hiện bất kỳ bước bổ sung, chỉ là hằng số rộng hơn. Đây là những gì GCC sử dụng cho __builtin_popcountllcác hệ thống x86 khi popcnthướng dẫn phần cứng không được bật. Nếu bạn có thể sử dụng nội trang hoặc nội tại cho việc này, hãy làm như vậy để cung cấp cho trình biên dịch cơ hội thực hiện tối ưu hóa cụ thể theo mục tiêu.


Với SIMD đầy đủ cho các vectơ rộng hơn (ví dụ: đếm toàn bộ một mảng)

Thuật toán bitwise-SWAR này có thể song song được thực hiện trong nhiều phần tử vectơ cùng một lúc, thay vì trong một thanh ghi số nguyên duy nhất, để tăng tốc cho CPU với SIMD nhưng không có lệnh popcount có thể sử dụng được. (ví dụ mã x86-64 phải chạy trên bất kỳ CPU nào, không chỉ Nehalem trở lên.)

Tuy nhiên, cách tốt nhất để sử dụng các hướng dẫn vectơ cho popcount thường là sử dụng biến ngẫu nhiên để thực hiện tra cứu bảng cho 4 bit tại một thời điểm của mỗi byte song song. (4 bit lập chỉ mục một bảng nhập 16 được giữ trong một thanh ghi vector).

Trên CPU Intel, hướng dẫn popcnt 64 bit phần cứng có thể vượt trội so với triển khai song song bit SSSE3PSHUFB khoảng 2 lần, nhưng chỉ khi trình biên dịch của bạn thực hiện đúng . Nếu không SSE có thể đi ra đáng kể phía trước. Các phiên bản trình biên dịch mới hơn nhận thức được vấn đề phụ thuộc sai popcnt trên Intel .

Người giới thiệu:


87
ha! yêu thích hàm NumberOfSetBits (), nhưng chúc may mắn nhận được điều đó thông qua đánh giá mã. :-)
Jason S

37
Có lẽ nó nên sử dụng unsigned int, để dễ dàng cho thấy rằng nó không có bất kỳ biến chứng bit dấu hiệu nào. Cũng sẽ uint32_tan toàn hơn, như trong, bạn có nhận được những gì bạn mong đợi trên tất cả các nền tảng?
Craig McQueen

35
@nonnb: Trên thực tế, như đã viết, mã bị lỗi và cần bảo trì. >>được xác định thực hiện cho các giá trị âm. Đối số cần được thay đổi (hoặc truyền) thành unsignedvà vì mã này là 32 bit cụ thể, nên có lẽ nên sử dụng uint32_t.
R .. GitHub DỪNG GIÚP ICE

6
Nó không thực sự kỳ diệu. Đó là thêm các bộ bit nhưng làm như vậy với một số tối ưu hóa thông minh. Liên kết wikipedia được đưa ra trong câu trả lời thực hiện tốt công việc giải thích những gì đang diễn ra nhưng tôi sẽ đi từng dòng một. 1) Đếm số lượng bit trong mỗi cặp bit, đặt số đếm đó vào cặp bit đó (bạn sẽ có 00, 01 hoặc 10); bit "thông minh" ở đây là phép trừ tránh một mặt nạ. 2) Thêm các cặp bitpairs đó vào các ngòi tương ứng của chúng; Không có gì thông minh ở đây nhưng mỗi nibble bây giờ sẽ có giá trị 0-4. (tiếp)
dash-tom-bang

8
Một lưu ý khác, điều này mở rộng đến các thanh ghi 64 và 128 bit bằng cách mở rộng các hằng số một cách thích hợp. Thật thú vị (với tôi), các hằng số đó cũng là ~ 0/3, 5, 17 và 255; ba trước đây là 2 ^ n + 1. Tất cả điều này có ý nghĩa hơn khi bạn càng nhìn chằm chằm vào nó và suy nghĩ về nó trong khi tắm. :)
dash-tom-bang

214

Cũng xem xét các chức năng tích hợp trong trình biên dịch của bạn.

Trên trình biên dịch GNU chẳng hạn, bạn chỉ có thể sử dụng:

int __builtin_popcount (unsigned int x);
int __builtin_popcountll (unsigned long long x);

Trong trường hợp xấu nhất, trình biên dịch sẽ tạo một cuộc gọi đến một hàm. Trong trường hợp tốt nhất, trình biên dịch sẽ phát ra một lệnh cpu để thực hiện cùng một công việc nhanh hơn.

Nội tại GCC thậm chí hoạt động trên nhiều nền tảng. Popcount sẽ trở thành xu hướng chủ đạo trong kiến ​​trúc x86, vì vậy thật hợp lý khi bắt đầu sử dụng nội tại ngay bây giờ. Các kiến ​​trúc khác có số lượng nhiều năm.


Trên x86, bạn có thể nói với trình biên dịch rằng nó có thể đảm nhận hỗ trợ cho popcnthướng dẫn bằng -mpopcnthoặc -msse4.2cũng cho phép các hướng dẫn vectơ được thêm vào trong cùng một thế hệ. Xem các tùy chọn GCC x86 . -march=nehalem(hoặc -march=bất cứ CPU nào bạn muốn mã của mình giả định và điều chỉnh cho) có thể là một lựa chọn tốt. Chạy nhị phân kết quả trên CPU cũ hơn sẽ dẫn đến lỗi hướng dẫn bất hợp pháp.

Để tối ưu hóa nhị phân cho máy bạn xây dựng, hãy sử dụng -march=native (với gcc, clang hoặc ICC).

MSVC cung cấp một nội tại cho x86 popcnthướng dẫn , nhưng không giống như gcc, nó thực sự là một nội tại cho hướng dẫn phần cứng và yêu cầu hỗ trợ phần cứng.


Sử dụng std::bitset<>::count() thay vì tích hợp

Về lý thuyết, bất kỳ trình biên dịch nào biết cách tính toán hiệu quả cho CPU mục tiêu đều phải thể hiện chức năng đó thông qua ISO C ++ std::bitset<>. Trong thực tế, bạn có thể tốt hơn với bit-hack AND / shift / ADD trong một số trường hợp đối với một số CPU mục tiêu.

Đối với các kiến ​​trúc đích trong đó popcount phần cứng là một phần mở rộng tùy chọn (như x86), không phải tất cả các trình biên dịch đều có một std::bitsetlợi thế của nó khi có sẵn. Ví dụ: MSVC không có cách nào để kích hoạt popcnthỗ trợ tại thời gian biên dịch và luôn sử dụng tra cứu bảng , ngay cả với/Ox /arch:AVX (ngụ ý SSE4.2, mặc dù về mặt kỹ thuật có một bit tính năng riêng cho popcnt.)

Nhưng ít nhất bạn có được thứ gì đó di động hoạt động ở mọi nơi và với gcc / clang với các tùy chọn mục tiêu phù hợp, bạn sẽ có được số lượng phần cứng cho các kiến ​​trúc hỗ trợ nó.

#include <bitset>
#include <limits>
#include <type_traits>

template<typename T>
//static inline  // static if you want to compile with -mpopcnt in one compilation unit but not others
typename std::enable_if<std::is_integral<T>::value,  unsigned >::type 
popcount(T x)
{
    static_assert(std::numeric_limits<T>::radix == 2, "non-binary type");

    // sizeof(x)*CHAR_BIT
    constexpr int bitwidth = std::numeric_limits<T>::digits + std::numeric_limits<T>::is_signed;
    // std::bitset constructor was only unsigned long before C++11.  Beware if porting to C++03
    static_assert(bitwidth <= std::numeric_limits<unsigned long long>::digits, "arg too wide for std::bitset() constructor");

    typedef typename std::make_unsigned<T>::type UT;        // probably not needed, bitset width chops after sign-extension

    std::bitset<bitwidth> bs( static_cast<UT>(x) );
    return bs.count();
}

Xem asm từ gcc, clang, icc và MSVC trên trình thám hiểm trình biên dịch Godbolt.

x86-64 gcc -O3 -std=gnu++11 -mpopcntphát ra điều này:

unsigned test_short(short a) { return popcount(a); }
    movzx   eax, di      # note zero-extension, not sign-extension
    popcnt  rax, rax
    ret
unsigned test_int(int a) { return popcount(a); }
    mov     eax, edi
    popcnt  rax, rax
    ret
unsigned test_u64(unsigned long long a) { return popcount(a); }
    xor     eax, eax     # gcc avoids false dependencies for Intel CPUs
    popcnt  rax, rdi
    ret

PowerPC64 gcc -O3 -std=gnu++11phát ra (đối với intphiên bản arg):

    rldicl 3,3,0,32     # zero-extend from 32 to 64-bit
    popcntd 3,3         # popcount
    blr

Nguồn này hoàn toàn không dành riêng cho x86 hoặc GNU, nhưng chỉ biên dịch tốt cho x86 với gcc / clang / icc.

Cũng lưu ý rằng dự phòng của gcc cho các kiến ​​trúc không có số đếm đơn hướng dẫn là một tra cứu bảng theo thời gian. Điều này không phải là tuyệt vời cho ARM, ví dụ .


5
Tôi đồng ý rằng đây là cách thực hành tốt nói chung, nhưng trên XCode / OSX / Intel tôi thấy nó tạo mã chậm hơn hầu hết các đề xuất được đăng ở đây. Xem câu trả lời của tôi để biết chi tiết.

5
Intel i5 / i7 có POPCNT hướng dẫn SSE4, sử dụng các thanh ghi mục đích chung. GCC trên hệ thống của tôi không phát ra hướng dẫn sử dụng nội tại này, tôi đoán vì chưa có tùy chọn -march = nehalem.
matja

3
@matja, GCC 4.4.1 của tôi phát ra hướng dẫn popcnt nếu tôi biên dịch với -msse4.2
Nils Pipenbrinck

74
sử dụng c ++ std::bitset::count. sau khi nội tuyến này biên dịch thành một __builtin_popcountcuộc gọi duy nhất .
deft_code

1
@nlucaroni Vâng, vâng. Thời gian đang thay đổi. Tôi đã viết câu trả lời này vào năm 2008. Ngày nay chúng ta có số lượng người bản địa và nội tại sẽ biên dịch thành một câu lệnh trình biên dịch đơn nếu nền tảng cho phép điều đó.
Nils Pipenbrinck

184

Theo tôi, giải pháp "tốt nhất" là giải pháp có thể được đọc bởi một lập trình viên khác (hoặc lập trình viên gốc hai năm sau đó) mà không cần bình luận nhiều. Bạn cũng có thể muốn giải pháp nhanh nhất hoặc thông minh nhất mà một số người đã cung cấp nhưng tôi thích khả năng đọc hơn sự thông minh bất cứ lúc nào.

unsigned int bitCount (unsigned int value) {
    unsigned int count = 0;
    while (value > 0) {           // until all bits are zero
        if ((value & 1) == 1)     // check lower bit
            count++;
        value >>= 1;              // shift bits, removing lower bit
    }
    return count;
}

Nếu bạn muốn có thêm tốc độ (và giả sử bạn tài liệu tốt để giúp đỡ những người kế nhiệm), bạn có thể sử dụng tra cứu bảng:

// Lookup table for fast calculation of bits set in 8-bit unsigned char.

static unsigned char oneBitsInUChar[] = {
//  0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F (<- n)
//  =====================================================
    0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, // 0n
    1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, // 1n
    : : :
    4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8, // Fn
};

// Function for fast calculation of bits set in 16-bit unsigned short.

unsigned char oneBitsInUShort (unsigned short x) {
    return oneBitsInUChar [x >>    8]
         + oneBitsInUChar [x &  0xff];
}

// Function for fast calculation of bits set in 32-bit unsigned int.

unsigned char oneBitsInUInt (unsigned int x) {
    return oneBitsInUShort (x >>     16)
         + oneBitsInUShort (x &  0xffff);
}

Mặc dù chúng dựa trên kích thước loại dữ liệu cụ thể để chúng không di động. Nhưng, vì nhiều tối ưu hóa hiệu suất không có khả năng di động, điều đó có thể không phải là vấn đề. Nếu bạn muốn tính di động, tôi sẽ sử dụng giải pháp có thể đọc được.


21
Thay vì chia cho 2 và nhận xét nó là "bit shift ...", bạn chỉ nên sử dụng toán tử shift (>>) và bỏ qua nhận xét.
indiv

9
Nó sẽ có ý nghĩa hơn để thay thế if ((value & 1) == 1) { count++; }với count += value & 1?
Ponkadoodle

21
Không, giải pháp tốt nhất không phải là giải pháp dễ đọc nhất trong trường hợp này. Ở đây thuật toán tốt nhất là nhanh nhất.
NikiC

21
Đó hoàn toàn là ý kiến ​​của bạn, @nikic, mặc dù rõ ràng bạn đang tự do đánh giá thấp tôi. Không có đề cập nào trong câu hỏi làm thế nào để định lượng "tốt nhất", từ "hiệu suất" hay "nhanh" có thể được nhìn thấy ở đâu. Đó là lý do tại sao tôi chọn để đọc.
paxdiablo

3
Tôi đang đọc câu trả lời này 3 năm sau, và tôi thấy đó là câu trả lời tốt nhất vì nó dễ đọc và có nhiều bình luận hơn. giai đoạn = Stage.
waka-waka-waka

98

Từ Delight Delight, p. 66, Hình 5-2

int pop(unsigned x)
{
    x = x - ((x >> 1) & 0x55555555);
    x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
    x = (x + (x >> 4)) & 0x0F0F0F0F;
    x = x + (x >> 8);
    x = x + (x >> 16);
    return x & 0x0000003F;
}

Thực hiện trong ~ 20-ish hướng dẫn (phụ thuộc vòm), không phân nhánh.

Delight của hacker thú vị! Rất khuyến khích.


8
Phương thức Java Integer.bitCount(int)sử dụng chính việc thực hiện này.
Marco Bolis

Có một chút rắc rối sau việc này - nó sẽ thay đổi như thế nào nếu chúng ta chỉ quan tâm đến các giá trị 16 bit, thay vì 32 bit?
Jeremy Blum

Có thể tin tặc thích thú là thú vị, nhưng tôi sẽ đá tốt cho bất kỳ ai gọi điều này popthay vì population_count(hoặc pop_cntnếu bạn phải có một sự từ bỏ). @MarcoBolis Tôi cho rằng điều đó sẽ đúng với tất cả các phiên bản Java, nhưng chính thức sẽ phụ thuộc vào việc triển khai :)
Maarten Bodewes

Và, điều này không yêu cầu nhân, giống như mã trong câu trả lời được chấp nhận.
Alex

Lưu ý rằng trong việc khái quát hóa thành 64-bit, có một vấn đề. Kết quả không thể là 64, vì mặt nạ.
Albert van der Horst

76

Tôi nghĩ cách nhanh nhất mà không cần sử dụng bảng tra cứu và bảng xếp hạng popisount sau đây. Nó đếm các bit thiết lập chỉ với 12 thao tác.

int popcount(int v) {
    v = v - ((v >> 1) & 0x55555555);                // put count of each 2 bits into those 2 bits
    v = (v & 0x33333333) + ((v >> 2) & 0x33333333); // put count of each 4 bits into those 4 bits  
    return c = ((v + (v >> 4) & 0xF0F0F0F) * 0x1010101) >> 24;
}

Nó hoạt động vì bạn có thể đếm tổng số bit được đặt bằng cách chia thành hai nửa, đếm số bit được đặt trong cả hai nửa và sau đó thêm chúng lên. Cũng biết như Divide and Conquermô thức. Chúng ta hãy đi vào chi tiết ..

v = v - ((v >> 1) & 0x55555555); 

Số lượng bit trong hai bit có thể 0b00, 0b01hoặc 0b10. Hãy thử làm việc này trên 2 bit ..

 ---------------------------------------------
 |   v    |   (v >> 1) & 0b0101   |  v - x   |
 ---------------------------------------------
   0b00           0b00               0b00   
   0b01           0b00               0b01     
   0b10           0b01               0b01
   0b11           0b01               0b10

Đây là những gì được yêu cầu: cột cuối cùng hiển thị số bit được đặt trong mỗi cặp hai bit. Nếu số hai bit >= 2 (0b10)sau đó được andtạo ra 0b01, thì nó sẽ tạo ra 0b00.

v = (v & 0x33333333) + ((v >> 2) & 0x33333333); 

Câu nói này phải dễ hiểu. Sau thao tác đầu tiên, chúng ta có số bit được đặt trong mỗi hai bit, bây giờ chúng ta tổng hợp số đếm đó trong mỗi 4 bit.

v & 0b00110011         //masks out even two bits
(v >> 2) & 0b00110011  // masks out odd two bits

Sau đó, chúng tôi tổng hợp kết quả trên, cung cấp cho chúng tôi tổng số bit được đặt trong 4 bit. Tuyên bố cuối cùng là khó khăn nhất.

c = ((v + (v >> 4) & 0xF0F0F0F) * 0x1010101) >> 24;

Hãy phá vỡ nó thêm nữa ...

v + (v >> 4)

Nó tương tự như tuyên bố thứ hai; thay vào đó, chúng tôi đang đếm các bit thiết lập trong nhóm 4. Chúng tôi biết rất nhiều vì các hoạt động trước đây của chúng tôi, đó là mọi hoạt động đều có số bit được đặt trong đó. Hãy xem một ví dụ. Giả sử chúng ta có byte 0b01000010. Nó có nghĩa là nibble đầu tiên có bộ 4 bit và cái thứ hai có bộ 2 bit. Bây giờ chúng ta thêm những cái đó vào nhau.

0b01000010 + 0b01000000

Nó cung cấp cho chúng ta số bit thiết lập trong một byte, trong lần đầu tiên 0b01100010và do đó chúng ta che dấu bốn byte cuối cùng của tất cả các byte trong số (loại bỏ chúng).

0b01100010 & 0xF0 = 0b01100000

Bây giờ mỗi byte có số bit thiết lập trong đó. Chúng ta cần thêm tất cả chúng lại với nhau. Bí quyết là nhân kết quả 0b10101010có một thuộc tính thú vị. Nếu số của chúng tôi có bốn byte, A B C Dnó sẽ dẫn đến một số mới với các byte này A+B+C+D B+C+D C+D D. Một số 4 byte có thể có tối đa 32 bit được đặt, có thể được biểu diễn dưới dạng 0b00100000.

Tất cả những gì chúng ta cần bây giờ là byte đầu tiên có tổng của tất cả các bit được đặt trong tất cả các byte và chúng ta có được nó >> 24. Thuật toán này được thiết kế cho các 32 bittừ nhưng có thể dễ dàng sửa đổi cho các 64 bittừ.


Là gì c = vậy? Hình như là nên loại bỏ. Hơn nữa, đề xuất thêm một bộ paren A "(((v + (v >> 4)) & 0xF0F0F0F) * 0x1010101) >> 24" để tránh một số cảnh báo cổ điển.
chux - Phục hồi Monica

4
Một tính năng quan trọng là thói quen 32 bit này hoạt động cho cả popcount(int v)popcount(unsigned v). Đối với tính di động, hãy xem xét popcount(uint32_t v), vv Thực sự thích phần * 0x1010101.
chux - Phục hồi Monica

Nước xốt ? (sách, liên kết, tên của invetors, v.v.) sẽ được RẤT hoan nghênh. Bởi vì sau đó chúng ta có thể dán nó trong các cơ sở mã của mình với một nhận xét đến từ đâu.
v.oddou

1
Tôi nghĩ để rõ ràng hơn, dòng cuối cùng nên được viết là: return (((i + (i >> 4)) & 0x0F0F0F0F) * 0x01010101) >> 24;vì vậy chúng tôi không cần phải đếm các chữ cái để xem những gì bạn đang làm (vì bạn đã loại bỏ dòng đầu tiên 0, tôi vô tình nghĩ rằng bạn đã sử dụng mô hình bit sai (lật) làm mặt nạ - đó là cho đến khi tôi lưu ý chỉ có 7 chữ cái chứ không phải 8).
emem 6/2/2016

Phép nhân đó bằng 0x01010101 có thể chậm, tùy thuộc vào bộ xử lý. Ví dụ: trong PowerBook G4 cũ của tôi, 1 phép nhân có tốc độ chậm bằng 4 lần bổ sung (không tệ bằng phép chia, trong đó 1 phép chia chậm bằng 23 lần bổ sung).
George Koehler

54

Tôi đã chán, và hẹn giờ một tỷ lần lặp của ba cách tiếp cận. Trình biên dịch là gcc -O3. CPU là bất cứ thứ gì họ đặt trong Macbook Pro thế hệ 1.

Nhanh nhất là như sau, ở mức 3,7 giây:

static unsigned char wordbits[65536] = { bitcounts of ints between 0 and 65535 };
static int popcount( unsigned int i )
{
    return( wordbits[i&0xFFFF] + wordbits[i>>16] );
}

Vị trí thứ hai thuộc về cùng một mã nhưng tìm kiếm 4 byte thay vì 2 Halfwords. Điều đó mất khoảng 5,5 giây.

Vị trí thứ ba thuộc về cách tiếp cận 'bổ sung ngang' đôi chút, mất 8,6 giây.

Vị trí thứ tư thuộc về __builtin_popcount () của GCC, trong 11 giây đáng xấu hổ.

Cách tiếp cận đếm từng bit một lúc chậm hơn và tôi cảm thấy chán khi chờ đợi nó hoàn thành.

Vì vậy, nếu bạn quan tâm đến hiệu suất trên hết, hãy sử dụng phương pháp đầu tiên. Nếu bạn quan tâm, nhưng không đủ để chi 64Kb RAM cho nó, hãy sử dụng phương pháp thứ hai. Mặt khác, sử dụng phương pháp một bit có thể đọc được (nhưng chậm).

Thật khó để nghĩ về một tình huống mà bạn muốn sử dụng phương pháp xoay vòng một chút.

Chỉnh sửa: Kết quả tương tự ở đây .


49
@Mike, Cách tiếp cận dựa trên bảng là không thể đánh bại nếu bảng nằm trong bộ đệm. Điều này xảy ra trong các điểm chuẩn vi mô (ví dụ: thực hiện hàng triệu bài kiểm tra trong một vòng lặp chặt chẽ). Tuy nhiên, một lỗi bộ nhớ cache mất khoảng 200 chu kỳ và ngay cả những người có số lượng ngây thơ nhất cũng sẽ nhanh hơn ở đây. Nó luôn luôn phụ thuộc vào ứng dụng.
Nils Pipenbrinck

10
Nếu bạn không gọi thói quen này vài triệu lần trong một vòng lặp chặt chẽ thì bạn không có lý do gì để quan tâm đến hiệu suất của nó cả, và cũng có thể sử dụng phương pháp ngây thơ nhưng dễ đọc vì mất hiệu suất sẽ không đáng kể. Và FWIW, LUT 8 bit bị nóng bộ nhớ cache trong vòng 10-20 cuộc gọi.

6
Tôi không nghĩ rằng thật khó để tưởng tượng ra một tình huống trong đó đây là một cuộc gọi lá được thực hiện từ phương thức - thực sự là việc nâng vật nặng - trong ứng dụng của bạn. Tùy thuộc vào những gì khác đang diễn ra (và phân luồng), phiên bản nhỏ hơn có thể giành chiến thắng. Rất nhiều thuật toán đã được viết để đánh bại các đồng nghiệp của họ do địa phương tham khảo tốt hơn. Tại sao không phải điều này quá?
Jason

Hãy thử điều này với tiếng kêu, nó thông minh hơn đáng kể trong việc triển khai các nội trang.
Matt Joiner

3
GCC sẽ không phát ra hướng dẫn popcont trừ khi được gọi với -msse4.2, trường hợp nhanh hơn 'bổ sung ngang'.
lvella

54

Nếu bạn tình cờ sử dụng Java, phương thức dựng sẵn Integer.bitCountsẽ làm điều đó.


Khi sun cung cấp các API khác nhau, nó phải sử dụng một số logic trên nền, phải không?
Cuộc tuần tra của Vallabh

2
Là một lưu ý phụ, việc triển khai Java sử dụng cùng một thuật toán được chỉ ra bởi Kevin Little .
Marco Bolis

2
Thực hiện sang một bên, đây có lẽ là thông điệp rõ ràng nhất về ý định cho các nhà phát triển duy trì mã của bạn sau khi bạn (hoặc khi bạn quay lại 6 tháng sau)
divillysausages

31
unsigned int count_bit(unsigned int x)
{
  x = (x & 0x55555555) + ((x >> 1) & 0x55555555);
  x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
  x = (x & 0x0F0F0F0F) + ((x >> 4) & 0x0F0F0F0F);
  x = (x & 0x00FF00FF) + ((x >> 8) & 0x00FF00FF);
  x = (x & 0x0000FFFF) + ((x >> 16)& 0x0000FFFF);
  return x;
}

Hãy để tôi giải thích thuật toán này.

Thuật toán này dựa trên thuật toán phân chia và chinh phục. Giả sử có một số nguyên 8 bit 213 (11010101 ở dạng nhị phân), thuật toán hoạt động như thế này (mỗi lần hợp nhất hai khối lân cận):

+-------------------------------+
| 1 | 1 | 0 | 1 | 0 | 1 | 0 | 1 |  <- x
|  1 0  |  0 1  |  0 1  |  0 1  |  <- first time merge
|    0 0 1 1    |    0 0 1 0    |  <- second time merge
|        0 0 0 0 0 1 0 1        |  <- third time ( answer = 00000101 = 5)
+-------------------------------+

7
Thuật toán này là phiên bản Matt Howells được đăng, trước khi được tối ưu hóa đến mức nó không thể đọc được.
Lefteris E

29

Đây là một trong những câu hỏi giúp tìm hiểu kiến ​​trúc vi mô của bạn. Tôi chỉ định thời gian cho hai biến thể trong gcc 4.3.3 được biên dịch với -O3 bằng cách sử dụng nội tuyến C ++ để loại bỏ phí gọi hàm, một tỷ lần lặp, giữ tổng số chạy để đảm bảo trình biên dịch không xóa bất cứ điều gì quan trọng, sử dụng ndtsc cho thời gian ( chu kỳ đồng hồ chính xác).

nội tuyến int pop2 (không dấu x, không dấu y)
{
    x = x - ((x >> 1) & 0x55555555);
    y = y - ((y >> 1) & 0x55555555);
    x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
    y = (y & 0x33333333) + ((y >> 2) & 0x33333333);
    x = (x + (x >> 4)) & 0x0F0F0F0F;
    y = (y + (y >> 4)) & 0x0F0F0F0F;
    x = x + (x >> 8);
    y = y + (y >> 8);
    x = x + (x >> 16);
    y = y + (y >> 16);
    trả lại (x + y) & 0x000000FF;
}

Delight chưa được sửa đổi của Hacker mất 12,2 gigacyc. Phiên bản song song của tôi (đếm gấp đôi số bit) chạy trong 13.0 gigacyc. Tổng số 10,5 giây trôi qua cho cả hai cùng nhau trên Core Duo 2,4 GHz. 25 gigaciking = chỉ hơn 10 giây ở tần số đồng hồ này, vì vậy tôi tự tin rằng thời gian của mình là đúng.

Điều này có liên quan đến các chuỗi phụ thuộc lệnh, rất tệ cho thuật toán này. Tôi có thể tăng gần gấp đôi tốc độ một lần nữa bằng cách sử dụng một cặp thanh ghi 64 bit. Trên thực tế, nếu tôi khéo léo và thêm x + ya sớm hơn tôi có thể tắt một số ca. Phiên bản 64 bit với một số điều chỉnh nhỏ sẽ xuất hiện đồng đều, nhưng lại tính gấp đôi số bit.

Với các thanh ghi SIMD 128 bit, một yếu tố khác của hai và các bộ hướng dẫn SSE cũng thường có các phím tắt thông minh.

Không có lý do gì để mã đặc biệt minh bạch. Giao diện rất đơn giản, thuật toán có thể được tham chiếu trực tuyến ở nhiều nơi và có thể kiểm tra đơn vị toàn diện. Các lập trình viên tình cờ tìm thấy nó thậm chí có thể học được điều gì đó. Các hoạt động bit là cực kỳ tự nhiên ở cấp độ máy.

OK, tôi quyết định chuẩn bị phiên bản 64-bit được điều chỉnh. Đối với một sizeof này (dài không dấu) == 8

nội tuyến int pop2 (dài không dấu x, dài không dấu y)
{
    x = x - ((x >> 1) & 0x5555555555555555);
    y = y - ((y >> 1) & 0x5555555555555555);
    x = (x & 0x3333333333333333) + ((x >> 2) & 0x333333333333333333);
    y = (y & 0x3333333333333333) + ((y >> 2) & 0x333333333333333333);
    x = (x + (x >> 4)) & 0x0F0F0F0F0F0F0F0F;
    y = (y + (y >> 4)) & 0x0F0F0F0F0F0F0F0F;
    x = x + y; 
    x = x + (x >> 8);
    x = x + (x >> 16);
    x = x + (x >> 32); 
    trả lại x & 0xFF;
}

Điều đó có vẻ đúng (mặc dù tôi không kiểm tra cẩn thận). Bây giờ thời gian xuất hiện ở mức 10,70 gigacyc / 14,1 gigacyc. Con số sau đó đã tóm tắt 128 tỷ bit và tương ứng với 5,9 giây trôi qua trên máy này. Phiên bản không song song tăng tốc một chút vì tôi đang chạy ở chế độ 64 bit và nó thích các thanh ghi 64 bit tốt hơn một chút so với các thanh ghi 32 bit.

Chúng ta hãy xem liệu có thêm một chút đường ống nào ở đây không. Điều này có liên quan nhiều hơn một chút, vì vậy tôi thực sự đã thử nghiệm một chút. Mỗi thuật ngữ một mình tính đến 64, tất cả tổng cộng thành 256.

int int4 (không dấu dài x, dài không dấu y, 
                unsign dài u, unsign dài v)
{
  enum {m1 = 0x5555555555555555, 
         m2 = 0x3333333333333333, 
         m3 = 0x0F0F0F0F0F0F0F0F, 
         m4 = 0x000000FF000000FF};

    x = x - ((x >> 1) & m1);
    y = y - ((y >> 1) & m1);
    u = u - ((u >> 1) & m1);
    v = v - ((v >> 1) & m1);
    x = (x & m2) + ((x >> 2) & m2);
    y = (y & m2) + ((y >> 2) & m2);
    u = (u & m2) + ((u >> 2) & m2);
    v = (v & m2) + ((v >> 2) & m2);
    x = x + y; 
    u = u + v; 
    x = (x & m3) + ((x >> 4) & m3);
    u = (u & m3) + ((u >> 4) & m3);
    x = x + u; 
    x = x + (x >> 8);
    x = x + (x >> 16);
    x = x & m4; 
    x = x + (x >> 32);
    trả lại x & 0x000001FF;
}

Tôi đã rất phấn khích một lúc, nhưng hóa ra gcc đang chơi các thủ thuật nội tuyến với -O3 mặc dù tôi không sử dụng từ khóa nội tuyến trong một số bài kiểm tra. Khi tôi để gcc chơi các thủ thuật, một tỷ cuộc gọi đến pop4 () mất 12,56 gigacyc, nhưng tôi xác định đó là các đối số gấp như các biểu thức không đổi. Một con số thực tế hơn dường như là 19,6gc để tăng thêm 30%. Vòng lặp thử nghiệm của tôi bây giờ trông như thế này, đảm bảo mỗi đối số đủ khác nhau để ngăn gcc chơi các thủ thuật.

   hitime b4 = rdtsc (); 
   cho (dài không dấu i = 10L * 1000 * 1000 * 1000; i <11L * 1000 * 1000 * 1000; ++ i) 
      tổng + = pop4 (i, i ^ 1, ~ i, i | 1); 
   hitime e4 = rdtsc (); 

256 tỷ bit được tóm tắt trong 8,17 giây. Hoạt động tới 1,02 giây cho 32 triệu bit như được điểm chuẩn trong tra cứu bảng 16 bit. Không thể so sánh trực tiếp, bởi vì băng ghế khác không cho tốc độ xung nhịp, nhưng có vẻ như tôi đã tát cái snot ra khỏi phiên bản bảng 64KB, đây là cách sử dụng bi thảm của bộ đệm L1 ở nơi đầu tiên.

Cập nhật: quyết định làm rõ ràng và tạo pop6 () bằng cách thêm bốn dòng trùng lặp. Đã đạt tới 22,8gc, 384 tỷ bit được tóm tắt trong 9,5 giây. Vì vậy, có 20% khác bây giờ ở 800ms cho 32 tỷ bit.


2
Hình thức không hợp ngữ tốt nhất như thế này Tôi từng thấy 24 từ 32 bit không được kiểm soát tại một thời điểm. dalkescientific.com/writings/diary/popcnt.c , stackoverflow.com/questions/3693981/... , dalkescientific.com/writings/diary/archive/2008/07/05/...
Matt Joiner

28

Tại sao không lặp lại chia cho 2?

đếm = 0
trong khi n> 0
  if (n% 2) == 1
    đếm + = 1
  n / = 2  

Tôi đồng ý rằng đây không phải là nhanh nhất, nhưng "tốt nhất" là hơi mơ hồ. Tôi muốn tranh luận rằng "tốt nhất" nên có yếu tố rõ ràng


Điều đó sẽ hiệu quả và dễ hiểu, nhưng có những phương pháp nhanh hơn.
Matt Howells

2
Trừ khi bạn làm điều này RẤT NHIỀU , tác động hiệu suất sẽ không đáng kể. Vì vậy, tất cả mọi thứ đều bình đẳng, tôi đồng ý với daniel rằng 'tốt nhất' ngụ ý "không đọc giống như vô nghĩa".

2
Tôi cố tình không định nghĩa 'tốt nhất', để có được nhiều phương pháp. Hãy đối mặt với nó nếu chúng ta đã xuống đến mức độ như vậy, có lẽ chúng ta đang tìm kiếm thứ gì đó rất nhanh, trông giống như một con tinh tinh đã gõ nó.
Matt Howells

6
Mã xấu. Một trình biên dịch có thể làm cho nó tốt hơn, nhưng trong các thử nghiệm của tôi, GCC đã không làm được. Thay thế (n% 2) bằng (n & 1); VÀ nhanh hơn nhiều so với MODULO. Thay thế (n / = 2) bằng (n >> = 1); bẻ khóa nhanh hơn nhiều so với phân chia.
Mecki

6
@Mecki: Trong các thử nghiệm của tôi, gcc (4.0, -O3) đã thực hiện các tối ưu hóa rõ ràng.

26

Việc thay đổi bit Delight của Hacker trở nên rõ ràng hơn rất nhiều khi bạn viết ra các mẫu bit.

unsigned int bitCount(unsigned int x)
{
  x = ((x >> 1) & 0b01010101010101010101010101010101)
     + (x       & 0b01010101010101010101010101010101);
  x = ((x >> 2) & 0b00110011001100110011001100110011)
     + (x       & 0b00110011001100110011001100110011); 
  x = ((x >> 4) & 0b00001111000011110000111100001111)
     + (x       & 0b00001111000011110000111100001111); 
  x = ((x >> 8) & 0b00000000111111110000000011111111)
     + (x       & 0b00000000111111110000000011111111); 
  x = ((x >> 16)& 0b00000000000000001111111111111111)
     + (x       & 0b00000000000000001111111111111111); 
  return x;
}

Bước đầu tiên thêm các bit chẵn vào các bit lẻ, tạo ra một tổng số bit trong mỗi hai. Các bước khác thêm các khối thứ tự cao vào các khối thứ tự thấp, tăng gấp đôi kích thước khối cho đến khi chúng ta có số đếm cuối cùng chiếm toàn bộ int.


3
Giải pháp này dường như có vấn đề nhỏ, liên quan đến quyền ưu tiên của nhà điều hành. Đối với mỗi thuật ngữ, nên nói: x = (((x >> 1) & 0b01010101010101010101010101010101) + (x & 0b01010101010101010101010101010101)); (tức là thêm parens thêm).
Nopik

21

Đối với phương tiện hạnh phúc giữa bảng tra cứu 2 32 và lặp lại qua từng bit riêng lẻ:

int bitcount(unsigned int num){
    int count = 0;
    static int nibblebits[] =
        {0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4};
    for(; num != 0; num >>= 4)
        count += nibblebits[num & 0x0f];
    return count;
}

Từ http://ctips.pbwiki.com/CountBits


Không xách tay. Nếu CPU có 9 bit byte thì sao? Vâng, có những CPU thực sự như thế ngoài kia ...
Robert S. Barnes

15
@Robert S. Barnes, chức năng này vẫn sẽ hoạt động. Nó không giả định về kích thước từ gốc và không tham chiếu đến "byte" nào cả.
vây

19

Điều này có thể được thực hiện trong đó O(k), ksố lượng bit được đặt.

int NumberOfSetBits(int n)
{
    int count = 0;

    while (n){
        ++ count;
        n = (n - 1) & n;
    }

    return count;
}

Đây thực chất là thuật toán của Brian Kernighan (nhớ anh ấy?), Với sự thay đổi nhỏ mà anh ấy đã sử dụng n &= (n-1)hình thức cô đọng hơn .
Adrian Mole

17

Đó không phải là giải pháp nhanh nhất hay tốt nhất, nhưng tôi đã tìm thấy câu hỏi tương tự theo cách của mình, và tôi bắt đầu suy nghĩ và suy nghĩ. cuối cùng tôi nhận ra rằng nó có thể được thực hiện như thế này nếu bạn gặp vấn đề từ phía toán học và vẽ biểu đồ, sau đó bạn thấy rằng đó là một hàm có một số phần định kỳ, và sau đó bạn nhận ra sự khác biệt giữa các thời kỳ ... vì vậy bạn đi đây

unsigned int f(unsigned int x)
{
    switch (x) {
        case 0:
            return 0;
        case 1:
            return 1;
        case 2:
            return 1;
        case 3:
            return 2;
        default:
            return f(x/4) + f(x%4);
    }
}

4
oh tôi thích điều đó làm thế nào bout phiên bản python:def f(i, d={0:lambda:0, 1:lambda:1, 2:lambda:1, 3:lambda:2}): return d.get(i, lambda: f(i//4) + f(i%4))()
underrun

10

Hàm bạn đang tìm kiếm thường được gọi là "tổng số ngang" hoặc "tổng số dân" của một số nhị phân. Knuth thảo luận về nó trong Tiền phát hành 1A, tr11-12 (mặc dù có một tài liệu tham khảo ngắn trong Tập 2, 4.6.3- (7).)

Các classicus locus là bài viết Peter Wegner của "Một kỹ thuật cho Ones đếm trong một máy tính nhị phân", từ truyền thông của ACM , Tập 3 (1960) Số 5, trang 322 . Ông đưa ra hai thuật toán khác nhau ở đó, một thuật toán được tối ưu hóa cho các số dự kiến ​​là "thưa thớt" (nghĩa là có một số lượng nhỏ) và một thuật toán cho trường hợp ngược lại.


10
  private int get_bits_set(int v)
    {
      int c; // c accumulates the total bits set in v
        for (c = 0; v>0; c++)
        {
            v &= v - 1; // clear the least significant bit set
        }
        return c;
    }

9

Vài câu hỏi mở: -

  1. Nếu số âm thì sao?
  2. Nếu số là 1024, thì phương thức "lặp chia cho 2" sẽ lặp lại 10 lần.

chúng ta có thể sửa đổi thuật toán để hỗ trợ số âm như sau: -

count = 0
while n != 0
if ((n % 2) == 1 || (n % 2) == -1
    count += 1
  n /= 2  
return count

Bây giờ để khắc phục vấn đề thứ hai, chúng ta có thể viết thuật toán như: -

int bit_count(int num)
{
    int count=0;
    while(num)
    {
        num=(num)&(num-1);
        count++;
    }
    return count;
}

để tham khảo đầy đủ xem:

http://goursaha.freeoda.com/Misiverse/IntegerBitCount.html


9

Tôi nghĩ rằng phương pháp của Brian Kernighan cũng sẽ hữu ích ... Nó trải qua nhiều lần lặp như có các bit được đặt. Vì vậy, nếu chúng ta có một từ 32 bit chỉ với tập bit cao, thì nó sẽ chỉ đi một lần qua vòng lặp.

int countSetBits(unsigned int n) { 
    unsigned int n; // count the number of bits set in n
    unsigned int c; // c accumulates the total bits set in n
    for (c=0;n>0;n=n&(n-1)) c++; 
    return c; 
}

Xuất bản năm 1988, Ngôn ngữ lập trình C 2nd Ed. (của Brian W. Kernighan và Dennis M. Ritchie) đề cập đến điều này trong bài tập 2-9. Vào ngày 19 tháng 4 năm 2006, Don Knuth đã chỉ ra cho tôi rằng phương pháp này "được Peter Wegner xuất bản lần đầu tiên trong CACM 3 (1960), 322. (Cũng được Derrick Lehmer phát hiện độc lập và xuất bản năm 1964 trong một cuốn sách do Beckenbach biên tập.)"


8

Tôi sử dụng mã dưới đây là trực quan hơn.

int countSetBits(int n) {
    return !n ? 0 : 1 + countSetBits(n & (n-1));
}

Logic: n & (n-1) đặt lại bit thiết lập cuối cùng của n.

PS: Tôi biết đây không phải là giải pháp O (1), mặc dù là một giải pháp thú vị.


điều này tốt cho các số "thưa thớt" với số lượng bit thấp O(ONE-BITS). Nó thực sự là O (1) vì có nhiều nhất là 32 bit.
ealfonso

7

Bạn có ý nghĩa gì với "Thuật toán tốt nhất"? Mã rút ngắn hay mã nhịn? Mã của bạn trông rất thanh lịch và nó có thời gian thực hiện liên tục. Mã này cũng rất ngắn.

Nhưng nếu tốc độ là yếu tố chính chứ không phải kích thước mã thì tôi nghĩ việc theo dõi có thể nhanh hơn:

       static final int[] BIT_COUNT = { 0, 1, 1, ... 256 values with a bitsize of a byte ... };
        static int bitCountOfByte( int value ){
            return BIT_COUNT[ value & 0xFF ];
        }

        static int bitCountOfInt( int value ){
            return bitCountOfByte( value ) 
                 + bitCountOfByte( value >> 8 ) 
                 + bitCountOfByte( value >> 16 ) 
                 + bitCountOfByte( value >> 24 );
        }

Tôi nghĩ rằng điều này sẽ không nhanh hơn đối với giá trị 64 bit nhưng giá trị 32 bit có thể nhanh hơn.


Mã của tôi có 10 hoạt động. Mã của bạn có 12 hoạt động. Liên kết của bạn hoạt động với các mảng nhỏ hơn (5). Tôi sử dụng 256 yếu tố. Với bộ nhớ đệm có thể là một vấn đề. Nhưng nếu bạn sử dụng nó rất thường xuyên thì đây không phải là vấn đề.
Horcrux7

Cách tiếp cận này nhanh hơn một chút so với cách tiếp cận xoay vòng bit, khi nó bật ra. Đối với việc sử dụng nhiều bộ nhớ hơn, nó biên dịch thành ít mã hơn và mức tăng đó được lặp lại mỗi khi bạn thực hiện chức năng. Vì vậy, nó có thể dễ dàng trở thành một chiến thắng ròng.

7

Tôi đã viết một macro bitcount nhanh cho các máy RISC vào khoảng năm 1990. Nó không sử dụng số học nâng cao (nhân, chia,%), tìm nạp bộ nhớ (cách quá chậm), các nhánh (cách quá chậm), nhưng nó cho rằng CPU có một Bộ dịch chuyển thùng 32 bit (nói cách khác, >> 1 và >> 32 có cùng số lượng chu kỳ.) Giả sử rằng các hằng số nhỏ (như 6, 12, 24) không tốn gì để tải vào các thanh ghi hoặc được lưu trữ trong thời gian và tái sử dụng nhiều lần.

Với các giả định này, nó đếm 32 bit trong khoảng 16 chu kỳ / hướng dẫn trên hầu hết các máy RISC. Lưu ý rằng 15 hướng dẫn / chu trình gần với giới hạn dưới của số chu kỳ hoặc hướng dẫn, bởi vì dường như phải mất ít nhất 3 hướng dẫn (mặt nạ, ca, toán tử) để cắt giảm một nửa số phụ lục, vì vậy log_2 (32) = 5, 5 x 3 = 15 hướng dẫn là một mệnh lệnh thấp hơn.

#define BitCount(X,Y)           \
                Y = X - ((X >> 1) & 033333333333) - ((X >> 2) & 011111111111); \
                Y = ((Y + (Y >> 3)) & 030707070707); \
                Y =  (Y + (Y >> 6)); \
                Y = (Y + (Y >> 12) + (Y >> 24)) & 077;

Đây là một bí mật cho bước đầu tiên và phức tạp nhất:

input output
AB    CD             Note
00    00             = AB
01    01             = AB
10    01             = AB - (A >> 1) & 0x1
11    10             = AB - (A >> 1) & 0x1

vì vậy nếu tôi lấy cột thứ 1 (A) ở trên, dịch chuyển sang phải 1 bit và trừ nó khỏi AB, tôi nhận được đầu ra (CD). Phần mở rộng thành 3 bit là tương tự; bạn có thể kiểm tra nó với một bảng boolean 8 hàng như của tôi ở trên nếu bạn muốn.

  • Donies

7

nếu bạn đang sử dụng C ++, một tùy chọn khác là sử dụng siêu lập trình mẫu:

// recursive template to sum bits in an int
template <int BITS>
int countBits(int val) {
        // return the least significant bit plus the result of calling ourselves with
        // .. the shifted value
        return (val & 0x1) + countBits<BITS-1>(val >> 1);
}

// template specialisation to terminate the recursion when there's only one bit left
template<>
int countBits<1>(int val) {
        return val & 0x1;
}

sử dụng sẽ là:

// to count bits in a byte/char (this returns 8)
countBits<8>( 255 )

// another byte (this returns 7)
countBits<8>( 254 )

// counting bits in a word/short (this returns 1)
countBits<16>( 256 )

tất nhiên bạn có thể mở rộng thêm mẫu này để sử dụng các loại khác nhau (thậm chí tự động phát hiện kích thước bit) nhưng tôi đã giữ nó đơn giản cho rõ ràng.

chỉnh sửa: quên đề cập đến điều này là tốt vì nó sẽ hoạt động trong bất kỳ trình biên dịch C ++ nào và về cơ bản nó chỉ hủy bỏ vòng lặp của bạn nếu giá trị không đổi được sử dụng cho số bit (nói cách khác, tôi khá chắc chắn đó là phương pháp chung nhanh nhất bạn sẽ tìm thấy)


Thật không may, việc đếm bit không được thực hiện song song, vì vậy nó có thể chậm hơn. Có thể làm cho một tốt đẹp constexprmặc dù.
imallett

Đồng ý - đó là một bài tập thú vị trong đệ quy mẫu C ++, nhưng chắc chắn là một giải pháp khá ngây thơ.
pentaphobe

6

Tôi đặc biệt thích ví dụ này từ tệp tài sản:

#define BITCOUNT (x) (((BX_ (x) + (BX_ (x) >> 4)) & 0x0F0F0F0F)% 255)
#define BX_ (x) ((x) - (((x) >> 1) & 0x77777777)
                             - (((x) >> 2) & 0x33333333)
                             - (((x) >> 3) & 0x11111111))

Tôi thích nó nhất vì nó rất đẹp!


1
Làm thế nào để nó thực hiện so với các đề xuất khác?
asdf

6

Java JDK1.5

Số nguyên.bitCount (n);

Trong đó n là số có 1 số được tính.

cũng kiểm tra

Integer.highestOneBit(n);
Integer.lowestOneBit(n);
Integer.numberOfLeadingZeros(n);
Integer.numberOfTrailingZeros(n);

//Beginning with the value 1, rotate left 16 times
     n = 1;
         for (int i = 0; i < 16; i++) {
            n = Integer.rotateLeft(n, 1);
            System.out.println(n);
         }

Không thực sự là một thuật toán, đây chỉ là một cuộc gọi thư viện. Hữu ích cho Java, không nhiều cho những người khác.
benzado

2
@benzado đúng nhưng dù sao thì +1, vì một số nhà phát triển Java có thể không biết về phương thức này
finnw

@finnw, tôi là một trong những nhà phát triển đó. :)
neevek

6

Tôi đã tìm thấy một triển khai đếm bit trong một mảng bằng cách sử dụng lệnh SIMD (SSSE3 và AVX2). Nó có hiệu suất tốt hơn gấp 2-2,5 lần so với việc nó sẽ sử dụng hàm nội tại __popcnt64.

Phiên bản SSSE3:

#include <smmintrin.h>
#include <stdint.h>

const __m128i Z = _mm_set1_epi8(0x0);
const __m128i F = _mm_set1_epi8(0xF);
//Vector with pre-calculated bit count:
const __m128i T = _mm_setr_epi8(0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4);

uint64_t BitCount(const uint8_t * src, size_t size)
{
    __m128i _sum =  _mm128_setzero_si128();
    for (size_t i = 0; i < size; i += 16)
    {
        //load 16-byte vector
        __m128i _src = _mm_loadu_si128((__m128i*)(src + i));
        //get low 4 bit for every byte in vector
        __m128i lo = _mm_and_si128(_src, F);
        //sum precalculated value from T
        _sum = _mm_add_epi64(_sum, _mm_sad_epu8(Z, _mm_shuffle_epi8(T, lo)));
        //get high 4 bit for every byte in vector
        __m128i hi = _mm_and_si128(_mm_srli_epi16(_src, 4), F);
        //sum precalculated value from T
        _sum = _mm_add_epi64(_sum, _mm_sad_epu8(Z, _mm_shuffle_epi8(T, hi)));
    }
    uint64_t sum[2];
    _mm_storeu_si128((__m128i*)sum, _sum);
    return sum[0] + sum[1];
}

Phiên bản AVX2:

#include <immintrin.h>
#include <stdint.h>

const __m256i Z = _mm256_set1_epi8(0x0);
const __m256i F = _mm256_set1_epi8(0xF);
//Vector with pre-calculated bit count:
const __m256i T = _mm256_setr_epi8(0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 
                                   0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4);

uint64_t BitCount(const uint8_t * src, size_t size)
{
    __m256i _sum =  _mm256_setzero_si256();
    for (size_t i = 0; i < size; i += 32)
    {
        //load 32-byte vector
        __m256i _src = _mm256_loadu_si256((__m256i*)(src + i));
        //get low 4 bit for every byte in vector
        __m256i lo = _mm256_and_si256(_src, F);
        //sum precalculated value from T
        _sum = _mm256_add_epi64(_sum, _mm256_sad_epu8(Z, _mm256_shuffle_epi8(T, lo)));
        //get high 4 bit for every byte in vector
        __m256i hi = _mm256_and_si256(_mm256_srli_epi16(_src, 4), F);
        //sum precalculated value from T
        _sum = _mm256_add_epi64(_sum, _mm256_sad_epu8(Z, _mm256_shuffle_epi8(T, hi)));
    }
    uint64_t sum[4];
    _mm256_storeu_si256((__m256i*)sum, _sum);
    return sum[0] + sum[1] + sum[2] + sum[3];
}

6

Tôi luôn sử dụng điều này trong Lập trình cạnh tranh và nó dễ viết và hiệu quả:

#include <bits/stdc++.h>

using namespace std;

int countOnes(int n) {
    bitset<32> b(n);
    return b.count();
}

5

Có nhiều thuật toán để đếm các bit thiết lập; nhưng tôi nghĩ cái tốt nhất là cái nhanh hơn Bạn có thể xem chi tiết trên trang này:

Hacks Twiddling

Tôi đề nghị cái này:

Đếm các bit được đặt trong các từ 14, 24 hoặc 32 bit bằng hướng dẫn 64 bit

unsigned int v; // count the number of bits set in v
unsigned int c; // c accumulates the total bits set in v

// option 1, for at most 14-bit values in v:
c = (v * 0x200040008001ULL & 0x111111111111111ULL) % 0xf;

// option 2, for at most 24-bit values in v:
c =  ((v & 0xfff) * 0x1001001001001ULL & 0x84210842108421ULL) % 0x1f;
c += (((v & 0xfff000) >> 12) * 0x1001001001001ULL & 0x84210842108421ULL) 
     % 0x1f;

// option 3, for at most 32-bit values in v:
c =  ((v & 0xfff) * 0x1001001001001ULL & 0x84210842108421ULL) % 0x1f;
c += (((v & 0xfff000) >> 12) * 0x1001001001001ULL & 0x84210842108421ULL) % 
     0x1f;
c += ((v >> 24) * 0x1001001001001ULL & 0x84210842108421ULL) % 0x1f;

Phương pháp này đòi hỏi CPU 64 bit có phân chia mô đun nhanh để có hiệu quả. Tùy chọn đầu tiên chỉ mất 3 thao tác; tùy chọn thứ hai mất 10; và tùy chọn thứ ba mất 15.


5

Giải pháp C # nhanh bằng cách sử dụng bảng tính toán trước của bit Byte với phân nhánh trên kích thước đầu vào.

public static class BitCount
{
    public static uint GetSetBitsCount(uint n)
    {
        var counts = BYTE_BIT_COUNTS;
        return n <= 0xff ? counts[n]
             : n <= 0xffff ? counts[n & 0xff] + counts[n >> 8]
             : n <= 0xffffff ? counts[n & 0xff] + counts[(n >> 8) & 0xff] + counts[(n >> 16) & 0xff]
             : counts[n & 0xff] + counts[(n >> 8) & 0xff] + counts[(n >> 16) & 0xff] + counts[(n >> 24) & 0xff];
    }

    public static readonly uint[] BYTE_BIT_COUNTS = 
    {
        0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4,
        1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
        1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
        1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
        3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
        1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
        3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
        3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
        3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
        4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8
    };
}

Trớ trêu thay, bảng đó có thể đã được tạo bởi bất kỳ thuật toán nào được đăng trong chuỗi này! Tuy nhiên, sử dụng các bảng như thế này có nghĩa là hiệu suất thời gian không đổi. Do đó, tiến thêm một bước và tạo bảng dịch 64K sẽ giảm một nửa các hoạt động AND, SHIFT và ADD cần thiết. Một chủ đề thú vị cho các nhà chế tác bit!
dùng924272

Các bảng lớn hơn có thể chậm hơn (và không phải thời gian liên tục) do các vấn đề về bộ đệm. Bạn có thể 'tra cứu' 3 bit cùng một lúc (0xe994 >>(k*2))&3mà không cần truy cập bộ nhớ ...
greggo

5

Đây là một mô-đun di động (ANSI-C) có thể điểm chuẩn từng thuật toán của bạn trên bất kỳ kiến ​​trúc nào.

CPU của bạn có 9 bit byte? Không có vấn đề gì :-) Tại thời điểm này, nó thực hiện 2 thuật toán, thuật toán K & R và bảng tra cứu thông minh byte. Bảng tra cứu trung bình nhanh hơn 3 lần so với thuật toán K & R. Nếu ai đó có thể tìm ra cách để làm cho thuật toán "Hacker's Delight" có thể di chuyển, hãy thêm nó vào.

#ifndef _BITCOUNT_H_
#define _BITCOUNT_H_

/* Return the Hamming Wieght of val, i.e. the number of 'on' bits. */
int bitcount( unsigned int );

/* List of available bitcount algorithms.  
 * onTheFly:    Calculate the bitcount on demand.
 *
 * lookupTalbe: Uses a small lookup table to determine the bitcount.  This
 * method is on average 3 times as fast as onTheFly, but incurs a small
 * upfront cost to initialize the lookup table on the first call.
 *
 * strategyCount is just a placeholder. 
 */
enum strategy { onTheFly, lookupTable, strategyCount };

/* String represenations of the algorithm names */
extern const char *strategyNames[];

/* Choose which bitcount algorithm to use. */
void setStrategy( enum strategy );

#endif

.

#include <limits.h>

#include "bitcount.h"

/* The number of entries needed in the table is equal to the number of unique
 * values a char can represent which is always UCHAR_MAX + 1*/
static unsigned char _bitCountTable[UCHAR_MAX + 1];
static unsigned int _lookupTableInitialized = 0;

static int _defaultBitCount( unsigned int val ) {
    int count;

    /* Starting with:
     * 1100 - 1 == 1011,  1100 & 1011 == 1000
     * 1000 - 1 == 0111,  1000 & 0111 == 0000
     */
    for ( count = 0; val; ++count )
        val &= val - 1;

    return count;
}

/* Looks up each byte of the integer in a lookup table.
 *
 * The first time the function is called it initializes the lookup table.
 */
static int _tableBitCount( unsigned int val ) {
    int bCount = 0;

    if ( !_lookupTableInitialized ) {
        unsigned int i;
        for ( i = 0; i != UCHAR_MAX + 1; ++i )
            _bitCountTable[i] =
                ( unsigned char )_defaultBitCount( i );

        _lookupTableInitialized = 1;
    }

    for ( ; val; val >>= CHAR_BIT )
        bCount += _bitCountTable[val & UCHAR_MAX];

    return bCount;
}

static int ( *_bitcount ) ( unsigned int ) = _defaultBitCount;

const char *strategyNames[] = { "onTheFly", "lookupTable" };

void setStrategy( enum strategy s ) {
    switch ( s ) {
    case onTheFly:
        _bitcount = _defaultBitCount;
        break;
    case lookupTable:
        _bitcount = _tableBitCount;
        break;
    case strategyCount:
        break;
    }
}

/* Just a forwarding function which will call whichever version of the
 * algorithm has been selected by the client 
 */
int bitcount( unsigned int val ) {
    return _bitcount( val );
}

#ifdef _BITCOUNT_EXE_

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

/* Use the same sequence of pseudo random numbers to benmark each Hamming
 * Weight algorithm.
 */
void benchmark( int reps ) {
    clock_t start, stop;
    int i, j;
    static const int iterations = 1000000;

    for ( j = 0; j != strategyCount; ++j ) {
        setStrategy( j );

        srand( 257 );

        start = clock(  );

        for ( i = 0; i != reps * iterations; ++i )
            bitcount( rand(  ) );

        stop = clock(  );

        printf
            ( "\n\t%d psudoe-random integers using %s: %f seconds\n\n",
              reps * iterations, strategyNames[j],
              ( double )( stop - start ) / CLOCKS_PER_SEC );
    }
}

int main( void ) {
    int option;

    while ( 1 ) {
        printf( "Menu Options\n"
            "\t1.\tPrint the Hamming Weight of an Integer\n"
            "\t2.\tBenchmark Hamming Weight implementations\n"
            "\t3.\tExit ( or cntl-d )\n\n\t" );

        if ( scanf( "%d", &option ) == EOF )
            break;

        switch ( option ) {
        case 1:
            printf( "Please enter the integer: " );
            if ( scanf( "%d", &option ) != EOF )
                printf
                    ( "The Hamming Weight of %d ( 0x%X ) is %d\n\n",
                      option, option, bitcount( option ) );
            break;
        case 2:
            printf
                ( "Please select number of reps ( in millions ): " );
            if ( scanf( "%d", &option ) != EOF )
                benchmark( option );
            break;
        case 3:
            goto EXIT;
            break;
        default:
            printf( "Invalid option\n" );
        }

    }

 EXIT:
    printf( "\n" );

    return 0;
}

#endif

1
Tôi rất thích cách tiếp cận đa hình thức, bổ trợ của bạn, cũng như việc chuyển sang xây dựng như một thư viện có thể sử dụng lại hoặc độc lập, có thể thực hiện kiểm tra.

5

những gì bạn có thể làm là

while(n){
    n=n&(n-1);
    count++;
}

logic đằng sau này là các bit của n-1 được đảo ngược từ bit được đặt ngoài cùng bên phải của n. nếu n = 6 tức là 110 thì 5 là 101, các bit được đảo ngược từ bit đặt bên phải của n. Vì vậy, nếu chúng ta và hai cái này, chúng ta sẽ tạo ra bit 0 đúng nhất trong mỗi lần lặp và luôn đi đến bit được đặt ở bên phải tiếp theo. Tính, đếm bit được đặt. Độ phức tạp thời gian tồi tệ nhất sẽ là O (logn) khi mỗi bit được đặt.

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.