[algorithm] 32 비트 정수로 설정된 비트 수를 계산하는 방법은 무엇입니까?

숫자 7을 나타내는 8 비트는 다음과 같습니다.

00000111

3 비트가 설정됩니다.

32 비트 정수에서 설정 비트 수를 결정하는 알고리즘은 무엇입니까?



답변

이를 ‘ 해밍 가중치 ‘, ‘인수’또는 ‘옆으로 추가’라고합니다.

‘최상의’알고리즘은 실제로 사용중인 CPU와 사용 패턴에 따라 다릅니다.

일부 CPU에는 단일 내장 명령이 있으며 다른 CPU에는 비트 벡터에 작용하는 병렬 명령이 있습니다. 병렬 명령어 ( popcnt지원되는 CPU 의 x86와 같은 )는 거의 가장 빠를 것입니다. 일부 다른 아키텍처에는 사이클 당 비트를 테스트하는 마이크로 코드 루프로 구현 된 느린 명령이있을 수 있습니다 ( 인용 필요 ).

CPU에 큰 캐시가 있거나 타이트한 루프에서 이러한 명령을 많이 수행하는 경우 미리 채워진 테이블 조회 방법이 매우 빠릅니다. 그러나 CPU가 메인 메모리에서 일부 테이블을 가져와야하는 ‘캐시 미스 (cache miss)’의 비용으로 인해 문제가 발생할 수 있습니다. (테이블을 작게 유지하려면 각 바이트를 별도로 찾으십시오.)

바이트가 대부분 0 또는 대부분 1임을 알면 이러한 시나리오에 매우 효율적인 알고리즘이 있습니다.

나는 매우 좋은 범용 알고리즘이 ‘병렬’또는 ‘가변 정밀도 SWAR 알고리즘’으로 알려진 다음과 같다고 생각합니다. 나는 이것을 C와 유사한 의사 언어로 표현했다. 특정 언어 (예를 들어 C ++의 경우 uint32_t를 사용하고 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;
}

JavaScript의 경우 : 성능 을 위해 정수강제|0 변환하십시오. 첫 번째 행을i = (i|0) - ((i >> 1) & 0x55555555);

이것은 논의 된 알고리즘 중 최악의 최악의 동작을 가지므로 사용자가 사용하는 사용 패턴이나 값을 효율적으로 처리합니다.


이 SWAR 비트 핵 작동 방식 :

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

첫 번째 단계는 홀수 / 짝수 비트를 분리하고 정렬하여 추가하고 추가하기 위해 최적화 된 마스킹 버전입니다. 이렇게하면 2 비트 누산기 ( SWAR = SIMD 내의 레지스터 ) 에서 16 개의 개별 추가가 효과적으로 수행 됩니다. 처럼 (i & 0x55555555) + ((i>>1) & 0x55555555).

다음 단계는 16x 2 비트 누산기의 홀수 / 짝수 8을 취하고 다시 더하여 8x 4 비트 합을 생성합니다. 이번에는 i - ...최적화가 불가능하므로 시프트 전 / 후 마스크 만 수행합니다. 레지스터에서 32 비트 상수를 별도로 구성해야하는 ISA를 컴파일 할 때 시프트하기 전에 0x33...대신 동일한 상수를 두 번 사용 0xccc...하는 것이 좋습니다.

마지막 이동 및 추가 단계는 (i + (i >> 4)) & 0x0F0F0F0F4 배속 8 ​​비트 누산기 로 확대됩니다. 해당 입력 비트의 모든 4 비트가 설정된 경우 4 비트 누산기의 최대 값이이므로 이전 대신 추가 한 후 마스킹 4됩니다. 4 + 4 = 8은 여전히 ​​4 비트에 맞으므로 니블 요소 사이의 캐리는 불가능 i + (i >> 4)합니다.

지금까지 이것은 몇 가지 영리한 최적화로 SWAR 기술을 사용하는 꽤 일반적인 SIMD입니다. 2 단계 이상의 동일한 패턴으로 계속하면 2x 16 비트, 1x 32 비트 카운트로 확장 될 수 있습니다. 그러나 빠른 하드웨어 곱셈을 가진 머신에는 더 효율적인 방법이 있습니다.

“요소”가 충분하지 않으면 마법 상수를 곱하면 모든 요소를 ​​최상위 요소에 합할 수 있습니다. 이 경우 바이트 요소입니다. 곱하기는 왼쪽 이동과 더하기에 의해 수행되므로 결과 는에 곱해 x * 0x01010101집니다 x + (x<<8) + (x<<16) + (x<<24). 우리의 8 비트 요소는이 너비 가 상위 8 비트에 들어 가지 않을만큼 충분히 넓습니다 (그리고 적은 수를 보유) .

이 64 비트 버전은 0x0101010101010101 승수를 사용하여 64 비트 정수에서 8x 8 비트 요소를 수행하고로 높은 바이트를 추출 할 수 >>56있습니다. 따라서 추가 단계를 거치지 않고 더 넓은 상수를 사용합니다. __builtin_popcountll하드웨어 popcnt명령어를 사용할 수없는 경우 x86 시스템에서 GCC가 사용합니다 . 이를 위해 내장 또는 내장 함수를 사용할 수있는 경우 컴파일러에서 대상별 최적화를 수행 할 수있는 기회를 제공하십시오.


더 넓은 벡터를위한 완전한 SIMD (예 : 전체 배열 계산)

이 비트 -SWAR 알고리즘은 SIMD는 있지만 사용 가능한 팝 카운트 명령이없는 CPU의 속도를 높이기 위해 단일 정수 레지스터 대신 여러 벡터 요소에서 한 번에 병렬 처리되도록 병렬 처리 할 수 ​​있습니다. (예 : Nehalem 이상이 아닌 모든 CPU에서 실행되어야하는 x86-64 코드)

그러나 popcount에 벡터 명령어를 사용하는 가장 좋은 방법은 일반적으로 variable-shuffle을 사용하여 각 바이트의 시간에 4 비트 씩 테이블 조회를 병렬로 수행하는 것입니다. (4 비트는 벡터 레지스터에 보유 된 16 개의 엔트리 테이블을 인덱스합니다).

Intel CPU에서 하드웨어 64 비트 popcnt 명령은 SSSE3 PSHUFB비트 병렬 구현 을 2 배 정도 수행 할 수 있지만 컴파일러에서 올바르게 얻는 경우 에만 가능합니다 . 그렇지 않으면 SSE가 크게 앞설 수 있습니다. 최신 컴파일러 버전은 인텔popcnt 잘못된 종속성 문제를 알고 있습니다.

참고 문헌 :


답변

또한 컴파일러의 내장 함수를 고려하십시오.

예를 들어 GNU 컴파일러에서는 다음을 사용할 수 있습니다.

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

최악의 경우 컴파일러는 함수에 대한 호출을 생성합니다. 가장 좋은 경우 컴파일러는 동일한 작업을 더 빨리 수행하기 위해 CPU 명령을 내 보냅니다.

GCC 내장 함수는 여러 플랫폼에서 작동합니다. Popcount는 x86 아키텍처에서 주류가 될 것이므로 이제 내장 함수를 사용하는 것이 좋습니다. 다른 아키텍처에는 몇 년 동안 인구수가 있습니다.


x86에서는 컴파일러에게 동일한 세대에 추가 된 벡터 명령어를 사용 하거나 popcnt명령어를 지원한다고 가정 할 수 있습니다 . GCC x86 옵션을 참조하십시오 . (또는 코드에서 가정하고 조정하려는 CPU)가 좋은 선택이 될 수 있습니다. 이전 CPU에서 결과 바이너리를 실행하면 잘못된 명령 오류가 발생합니다.-mpopcnt-msse4.2-march=nehalem-march=

바이너리를 빌드 한 머신에 최적화 된 바이너리를 만들려면 -march=native gcc, clang 또는 ICC와 함께 사용하십시오 .

MSVC는 x86 popcnt명령어에 대한 내장 함수를 제공 하지만 gcc와 달리 실제로는 하드웨어 명령어에 고유하며 하드웨어 지원이 필요합니다.


std::bitset<>::count()내장 대신 사용

이론적으로 대상 CPU에 대해 효율적으로 popcount하는 방법을 알고있는 모든 컴파일러는 ISO C ++를 통해 해당 기능을 노출해야합니다 std::bitset<>. 실제로 일부 대상 CPU의 경우 비트 핵 AND / shift / ADD를 사용하는 것이 좋습니다.

하드웨어 popcount가 선택적인 확장 (x86과 같은) 인 대상 아키텍처의 경우 모든 컴파일러가 std::bitset사용 가능한 경우이를 활용 하는 것은 아닙니다 . 예를 들어, MSVC는 popcnt컴파일 타임에 지원 을 활성화 할 수있는 방법이 없으며 기술적으로 별도의 기능 비트가 있지만 SSE4.2를 의미하는 경우에도 항상 테이블 조회를 사용 합니다 ./Ox /arch:AVXpopcnt

그러나 적어도 어느 곳에서나 휴대 가능한 것을 얻을 수 있으며 올바른 대상 옵션이있는 gcc / clang을 사용하면이를 지원하는 아키텍처에 대한 하드웨어 팝 카운트를 얻을 수 있습니다.

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

Godbolt 컴파일러 탐색기 에서 gcc, clang, icc 및 MSVC의 asm을 참조하십시오 .

x86-64는 다음을 gcc -O3 -std=gnu++11 -mpopcnt방출합니다.

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++11방출합니다 ( intarg 버전의 경우).

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

이 소스는 x86 또는 GNU에 국한되지 않지만 gcc / clang / icc를 사용하여 x86에 대해서만 잘 컴파일됩니다.

또한 단일 명령 popcount가없는 아키텍처에 대한 gcc의 폴백은 한 번에 바이트 당 테이블 조회입니다. 예를 들어 이것은 ARM 에게는 좋지 않습니다 .


답변

제 생각에는 “최고의”솔루션은 다른 프로그래머 (또는 2 년 후 원래 프로그래머)가 많은 의견없이 읽을 수있는 솔루션입니다. 일부는 이미 제공 한 가장 빠르고 영리한 솔루션을 원할 수 있지만 언제든지 영리함보다 가독성을 선호합니다.

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;
}

더 빠른 속도를 원하고 (후임자를 돕기 위해 잘 문서화한다고 가정하면) 테이블 조회를 사용할 수 있습니다.

// 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);
}

이들은 특정 데이터 유형 크기에 의존하기 때문에 이식성이 떨어집니다. 그러나 많은 성능 최적화가 어쨌든 이식 가능하지 않기 때문에 문제가되지 않을 수 있습니다. 이식성을 원한다면 읽기 쉬운 솔루션을 고수하겠습니다.


답변

Hacker ‘s Delight, p. 66, 그림 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;
}

~ 20-ish 명령어로 실행 (아치에 따라 다름), 분기 없음.

해커의 기쁨 유쾌합니다! 추천.


답변

조회 테이블과 popcount 를 사용하지 않고 가장 빠른 방법 은 다음과 같습니다. 12 개의 연산으로 설정된 비트 수를 계산합니다.

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;
}

두 세트로 나누어 총 세트 비트 수를 세고, 세트 비트 수를 반으로 세고 합산하면 효과가 있습니다. Divide and Conquer패러다임 이라고도합니다 . 자세히 알아 보자 ..

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

두 비트의 비트 수 0b000b01또는 0b10입니다. 이것을 2 비트로 해결해 봅시다.

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

마지막 열은 두 비트 쌍마다 설정된 비트 수를 나타냅니다. 두 개의 비트 번호 인 경우 >= 2 (0b10)다음 and생산하고 0b01, 다른 사람이 생산 0b00.

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

이 문장은 이해하기 쉬워야합니다. 첫 번째 연산 후에 우리는 매 2 비트마다 세트 비트 수를 갖습니다. 이제 우리는 매 4 비트마다 그 수를 요약합니다.

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

그런 다음 위의 결과를 요약하여 총 4 비트 세트 비트 수를 제공합니다. 마지막 진술이 가장 까다 롭습니다.

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

더 자세히 살펴 보자 …

v + (v >> 4)

두 번째 진술과 비슷합니다. 대신 세트 그룹을 4 그룹으로 세고 있습니다. 우리는 이전 작업으로 인해 모든 니블에 설정된 비트 수가 있음을 알고 있습니다. 예를 봅시다. 바이트가 있다고 가정하자 0b01000010. 첫 번째 니블에 4 비트가 설정되어 있고 두 번째 니블에 2 비트가 설정되어 있음을 의미합니다. 이제 우리는 그 니블을 함께 추가합니다.

0b01000010 + 0b01000000

첫 번째 니블에서 바이트 단위의 세트 비트 수를 제공 0b01100010하므로 숫자에있는 모든 바이트의 마지막 4 바이트를 마스킹합니다 (삭제).

0b01100010 & 0xF0 = 0b01100000

이제 모든 바이트에는 설정된 비트 수가 있습니다. 우리는 그것들을 모두 합쳐야합니다. 트릭은 0b10101010흥미로운 속성을 가진 결과를 곱하는 것 입니다. 숫자에 4 바이트가 있으면 A B C D이 바이트로 새로운 숫자가됩니다 A+B+C+D B+C+D C+D D. 4 바이트 숫자는 최대 32 비트 세트를 가질 수 있으며 이는로 표시 될 수 있습니다 0b00100000.

우리가 지금 필요한 것은 모든 바이트의 모든 세트 비트의 합을 가진 첫 번째 바이트입니다 >> 24. 이 알고리즘은 32 bit단어 용으로 설계 되었지만 단어 용 으로 쉽게 수정할 수 있습니다 64 bit.


답변

지루 해졌고 세 가지 접근 방식을 10 억 회 반복했습니다. 컴파일러는 gcc -O3입니다. CPU는 1 세대 Macbook Pro에 넣은 모든 것입니다.

3.7 초에서 다음이 가장 빠릅니다.

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] );
}

두 번째 장소는 동일한 코드로 이동하지만 2 하프 워드 대신 4 바이트를 찾습니다. 약 5.5 초가 걸렸습니다.

3 위는 약간 꼬인 ‘옆으로 추가’접근 방식으로 8.6 초가 걸렸습니다.

4 위는 부끄러운 11 초에 GCC의 __builtin_popcount ()로갑니다.

한 번에 한 비트 씩 계산하는 것이 느리게 진행되었으며, 완료 될 때까지 기다리는 것이 지루했습니다.

따라서 무엇보다도 성능에 관심이 있다면 첫 번째 방법을 사용하십시오. 걱정하지만 64Kb의 RAM을 소비하기에 충분하지 않은 경우 두 번째 방법을 사용하십시오. 그렇지 않으면 한 번에 한 비트 씩 읽기 가능하지만 느리게 접근하십시오.

비트 트위들 링 접근법을 사용하려는 상황을 생각하기는 어렵습니다.

편집 : 비슷한 결과는 여기 .


답변

Java를 사용하는 경우 내장 메소드 Integer.bitCount가이를 수행합니다.