정수의 1 비트가 연속 영역에 있는지 테스트하는 우아하고 빠른 방법이 있습니까?


85

비트 값이 1 인 위치 (32 비트 정수의 경우 0에서 31까지)가 연속 영역을 형성하는지 테스트해야합니다. 예를 들면 :

00111111000000000000000000000000      is contiguous
00111111000000000000000011000000      is not contiguous

이 테스트, 즉 일부 기능 has_contiguous_one_bits(int)이 이식 가능하기를 바랍니다.

한 가지 분명한 방법은 위치를 반복하여 첫 번째 설정 비트를 찾은 다음 첫 번째 설정되지 않은 비트를 찾고 더 많은 설정 비트를 확인하는 것입니다.

더 빠른 방법이 있는지 궁금합니다. 최고 및 최저 세트 비트를 찾는 빠른 방법이있는 경우 (하지만 이 질문 에서 휴대용 비트 가없는 것으로 보입니다) 가능한 구현은 다음과 같습니다.

bool has_contiguous_one_bits(int val)
{
    auto h = highest_set_bit(val);
    auto l = lowest_set_bit(val);
    return val == (((1 << (h-l+1))-1)<<l);
}

재미를 위해 연속 비트가있는 처음 100 개의 정수가 있습니다.

0 1 2 3 4 6 7 8 12 14 15 16 24 28 30 31 32 48 56 60 62 63 64 96 112 120 124 126 127 128 192 224 240 248 252 254 255 256 384 448 480 496 504 508 510 511 512 768 896 960 992 1008 1016 1020 1022 1023 1024 1536 1792 1920 1984 2016 2032 2040 2044 2046 2047 2048 3072 3584 3840 3968 4032 4064 4080 4088 4092 4094 4095 4096 6144 7168 7680 7936 8064 8128 8160 8176 8184 8188 8190 8191 8192 12288 14336 15360 15872 16128 16256 16320

그들은 (물론) (1<<m)*(1<<n-1)음수가 아닌 형식 m이며 n.


4
@aafulei 예, 0x0컴팩트합니다. 그 반대 (간단하지 않음)를 정의하는 것이 더 쉽습니다. 설정 비트가있는 경우 그 사이에 설정되지 않은 비트가 하나 이상 있습니다.
Walter

1
@KamilCuk h>=l의 (묵시적) 기능 highest_set_bit()lowest_set_bit()
Walter


6
이 OEIS 링크는이 숫자가 2 진수 일 때 숫자가 증가하지 않는다고 말합니다. 그것들을 참조하는 또 다른 방법은 그것들이 연속적 (또는 연결되어 있음)이라고 말하는 것입니다. 이 수학자에게 "콤팩트"는 매우 다른 것을 의미합니다.
Teepeemm

1
@Teepeemm 나는이 질문이 뜨거운 네트워크 질문으로 끝나는 이유 중 하나는 compact라는 단어의 오용 때문이라고 생각합니다. 확실히 클릭 한 이유입니다. 그런 식으로. 분명히 말이되지 않습니다.
Nobody

답변:


147
static _Bool IsCompact(unsigned x)
{
    return (x & x + (x & -x)) == 0;
}

간단히:

x & -x가장 낮은 비트 세트를 제공합니다 x(또는 0 인 경우 x0).

x + (x & -x) 연속 된 1의 가장 낮은 문자열을 단일 1로 변환합니다 (또는 0으로 래핑).

x & x + (x & -x) 그 1 비트를 지 웁니다.

(x & x + (x & -x)) == 0 다른 1 비트가 남아 있는지 테스트합니다.

더 길게:

-x~x+1우리가 가정하는 2의 보수를 사용하여 같음 . 비트가 뒤집힌 후 ~x1을 더하면 하위 1 비트 ~x와 첫 번째 0 비트가 뒤집힌 다음 중지됩니다. 따라서 -x처음 1 개까지 의 하위 비트는의 하위 비트 와 동일 x하지만 상위 비트는 모두 뒤집 힙니다. (예 : ~10011100제공 01100011하고 1을 더하면는 제공 01100100되므로 낮은 100값은 동일하지만 높은 값 10011은으로 반전됩니다 01100.) 그런 다음 x & -x둘 다에서 1 인 유일한 비트, 즉 가장 낮은 1 비트 ( 00000100)를 제공합니다. ( x0이면 x & -x0입니다.)

이 값을 추가 x하면 연속 된 1을 모두 통과하여 0으로 변경합니다. 다음으로 높은 0 비트에 1이 남게됩니다 (또는 하이 엔드를 통과하여 래핑 된 총 0이 남음) (10100000 .)

이것이와 AND 처리되면 x1이 0으로 변경된 위치 (및 캐리가 0에서 1로 변경된 위치)에 0이 있습니다. 따라서 결과는 1 비트가 더 높은 경우에만 0이 아닙니다.


23
적어도 누군가는 Hacker 's Delight라는 책을 알고 있습니다. 답변은 2-1 장을 참조하십시오. 그러나 이것은 이미 여기에서 여러 번 답변되었습니다. 어쨌든 : +1
Armin Montigny

33
프로덕션에서 이러한 코드를 작성
했다면

14
이것은 x86 BMI1 x & -x에서 단일 blsi명령 으로 수행 할 수 있는 이점이 있습니다 . Intel에서는 1 uop, AMD Zen에서는 2 uop입니다. godbolt.org/z/5zBx-A . 그러나 BMI1이 없으면 @KevinZ의 버전 이 훨씬 더 효율적입니다.
Peter Cordes

3
@TommyAndersen : _BoolC 2018 6.4.1 1에 따른 표준 키워드입니다.
Eric Postpischil

1
@Walter : 흠? 이 코드는 unsigned. 2의 보수 signed에 대한 테스트를 수행하려는 경우 int가장 쉬운 방법은이 답변의 루틴에 간단히 전달하여을 int로 변환하는 것 unsigned입니다. 원하는 결과를 얻을 수 있습니다. int오버플로 / 캐리 문제로 인해 작업 표시를 서명 된 직접 적용하는 것은 문제가 될 수 있습니다. (당신이 하나의 보수 또는 부호와 크기를 테스트하고 싶다면 int, 그것은 또 다른 문제이며, 요즘에는 이론적으로 만 관심이 있습니다.)
Eric Postpischil

29

실제로 내장 함수를 사용할 필요가 없습니다.

먼저 첫 번째 1 앞에있는 모든 0을 뒤집습니다. 그런 다음 새 값이 메르 센 숫자인지 테스트합니다. 이 알고리즘에서 0은 true로 매핑됩니다.

bool has_compact_bits( unsigned const x )
{
    // fill up the low order zeroes
    unsigned const y = x | ( x - 1 );
    // test if the 1's is one solid block
    return not ( y & ( y + 1 ) );
}

물론 내장 함수를 사용하려는 경우 다음은 popcount 메서드입니다.

bool has_compact_bits( unsigned const x )
{
    size_t const num_bits = CHAR_BIT * sizeof(unsigned);
    size_t const sum = __builtin_ctz(x) + __builtin_popcount(x) + __builtin_clz(z);
    return sum == num_bits;
}

2
첫 번째 버전 -mtbm , exploiting blsfill/ blcfillinstructions로 컴파일 된 경우 4 개의 명령어로만 축소됩니다 . 지금까지 제안 된 가장 짧은 버전이 될 것입니다. 안타깝게도 명령 세트 확장을 지원하는 프로세서는 거의 없습니다 .
Giovanni Cerretani

19

실제로 선행 0을 계산할 필요가 없습니다. 주석에서 pmg가 제안한대로 찾고있는 숫자가 시퀀스 OEIS A023758 의 숫자라는 사실을 이용하여 즉, i> = j2 ^ i-2 ^ j 형식의 숫자를 이용하면 후행 0을 계산할 수 있습니다 ( 즉, j-1 ), 원래 값에서 해당 비트를 토글 ( 2 ^ j-1 을 더하는 것과 동일 ) 한 다음 해당 값이 2 ^ i-1 형식인지 확인합니다 . GCC / clang 내장 함수를 사용하면

bool has_compact_bits(int val) {
    if (val == 0) return true; // __builtin_ctz undefined if argument is zero
    int j = __builtin_ctz(val) + 1;
    val |= (1 << j) - 1; // add 2^j - 1
    val &= (val + 1); // val set to zero if of the form (2^i - 1)
    return val == 0;
}

이 버전 KamilCuk이 제안한 버전과 팝 카운트 만있는 Yuri Feldman이 제안한 버전 보다 약간 빠릅니다 .

당신이 C ++ (20)를 사용하는 경우, 당신은 대체하여 휴대용 기능을 얻을 수 있습니다 __builtin_ctz으로 std::countr_zero:

#include <bit>

bool has_compact_bits(int val) {
    int j = std::countr_zero(static_cast<unsigned>(val)) + 1; // ugly cast
    val |= (1 << j) - 1; // add 2^j - 1
    val &= (val + 1); // val set to zero if of the form (2^i - 1)
    return val == 0;
}

캐스트는 추악하지만 비트를 조작 할 때 부호없는 유형으로 작업하는 것이 더 낫다는 경고입니다. Pre-C ++ 20 대안은 다음과 같습니다.boost::multiprecision::lsb .

편집하다:

취소 선 링크에 대한 벤치 마크는 Yuri Feldman 버전에 대해 popcount 명령이 생성되지 않았기 때문에 제한되었습니다. 를 사용하여 내 PC에서 컴파일하려고 시도 -march=westmere하면서 동일한 시퀀스로 10 억 번의 반복에 대해 다음 시간을 측정했습니다 std::mt19937.

  • 현재 버전 : 5.7 초
  • KamilCuk의 두 번째 버전 : 4.7 초
  • 내 버전 : 4.7 초
  • Eric Postpischil의 첫 번째 버전 : 4.3 초
  • Yuri Feldman의 버전 (명시 적으로 사용 __builtin_popcount) : 4.1 초

따라서 적어도 내 아키텍처에서 가장 빠른 것은 popcount가있는 것 같습니다.

편집 2 :

새로운 Eric Postpischil 버전으로 벤치 마크를 업데이트했습니다. 의견에서 요청했듯이 내 테스트 코드는 여기 에서 찾을 수 있습니다 . PRNG에 필요한 시간을 추정하기 위해 no-op 루프를 추가했습니다. KevinZ의 두 가지 버전도 추가했습니다. 코드가 clang에서 컴파일 -O3 -msse4 -mbmi되어 가져 오기 popcntblsi지시가 가능합니다 (Peter Cordes 덕분에).

결과 : 적어도 내 아키텍처에서 Eric Postpischil의 버전은 Yuri Feldman의 버전만큼 빠르며 지금까지 제안 된 다른 버전보다 두 배 이상 빠릅니다.


작업을 제거했습니다 : return (x & x + (x & -x)) == 0;.
Eric Postpischil

3
이것은 @Eric 버전의 이전 버전을 벤치마킹하는 것입니다. 현재 버전에서 Eric 's는 gcc -O3 -march=nehalem(popcnt를 사용 가능하게 만들기 위해) 몇 가지 지침으로 컴파일 하거나 BMI1 blsi을 사용할 수있는 경우 x & -x: godbolt.org/z/zuyj_f 보다 적게 컴파일 합니다. 그리고 지침은 popcnt3주기 지연이있는 Yuri 버전을 제외하고는 모두 간단한 단일 uop 입니다. (하지만 처리량을 벤치마킹했다고 가정합니다.) 또한 and valYuri에서를 제거 했거나 느려질 것이라고 가정합니다 .
Peter Cordes

2
또한 어떤 하드웨어를 벤치마킹 했습니까? 전체 벤치 마크 코드를 Godbolt 등에 연결하는 것이 좋은 생각이므로 미래의 독자는 C ++ 구현을 쉽게 테스트 할 수 있습니다.
Peter Cordes

2
@KevinZ의 버전도 테스트해야합니다. 그것은 BMI1없이 더 적은 명령어로 컴파일됩니다 (적어도 clang; gcc의 비 인라인 버전은 a를 낭비하고 mov활용하지 못합니다 lea) : godbolt.org/z/5jeQLQ . BMI1을 사용하면 Eric의 버전은 x86-64에서, 적어도 blsi단일 uop 인 Intel에서는 더 좋지만 AMD에서는 2 uop입니다.
Peter Cordes

15

빠름에 대해서는 잘 모르겠지만 확인하여 한 줄로 할 수 있습니다. val^(val>>1) 최대 2 비트가 켜져 .

이것은 부호없는 유형에서만 작동합니다 0. 부호 비트의 복사본에서 이동하는 산술 오른쪽 이동이 아니라 맨 위에서 이동 (논리 이동)이 필요합니다.

#include <bitset>
bool has_compact_bits(unsigned val)
{
    return std::bitset<8*sizeof(val)>((val ^ (val>>1))).count() <= 2;
}

거부하려면 0(즉, 정확히 1 개의 연속 비트 그룹이있는 입력 만 허용), 논리적 AND는 val0이 아닙니다. 이 질문에 대한 다른 답변 0은 간결한 것으로 받아들 입니다.

bool has_compact_bits(unsigned val)
{
    return std::bitset<8*sizeof(val)>((val ^ (val>>1))).count() <= 2 and val;
}

C ++ 이식 노출하는 popcount를 통해 std::bitset::count(), 또는 를 통해 20 C ++로std::popcount . C는 여전히 사용 가능한 대상에서 popcnt 또는 유사한 명령어로 안정적으로 컴파일하는 이식 가능한 방법이 없습니다.


2
또한 지금까지 가장 빠릅니다.
Giovanni Cerretani

2
부호 비트의 사본이 아닌 0으로 이동하려면 부호없는 유형을 사용해야한다고 생각합니다. 고려하십시오 11011111. 오른쪽으로 시프트 된 산술은 11101111이고 XOR은 00110000입니다. 논리적 오른쪽 시프트 ( 0상단에서 이동 )를 사용하면 10110000여러 비트 그룹 을 얻고 올바르게 감지 할 수 있습니다. 수정하기 위해 수정합니다.
Peter Cordes

3
이것은 정말 영리합니다. 내가 스타일을 싫어하는만큼 (IMO는을 사용 __builtin_popcount()하고 모든 컴파일러에는 요즘과 같은 기본 요소가 있음) 이것은 훨씬 빠릅니다 (현대 CPU에서). 사실, 단일 명령어로 POPCNT를 사용하지 않는 CPU에서는 내 구현이 이보다 더 좋을 수 있기 때문에 프레젠테이션이 매우 중요하다고 주장 할 것입니다. 따라서이 구현을 사용하려면 내장 함수 만 사용해야합니다. std::bitset끔찍한 인터페이스가 있습니다.
KevinZ

9

CPU에는 매우 빠른 전용 명령이 있습니다. PC에서는 BSR / BSF (1985 년 80386에 도입 됨)이고 ARM에서는 CLZ / CTZ입니다.

하나를 사용하여 최하위 세트 비트의 인덱스를 찾고 그 양만큼 정수를 오른쪽으로 이동하십시오. 다른 하나를 사용하여 최상위 세트 비트의 인덱스를 찾고 정수를 (1u << (bsr + 1))-1과 비교하십시오.

불행히도 35 년은 하드웨어에 맞도록 C ++ 언어를 업데이트하기에 충분하지 않았습니다. C ++에서 이러한 명령어를 사용하려면 내장 함수가 필요하며 이식 가능하지 않으며 약간 다른 형식으로 결과를 반환합니다. 전 처리기 #ifdef등을 사용하여 컴파일러를 감지 한 다음 적절한 내장 함수를 사용합니다. 그들이 MSVC에서 _BitScanForward, _BitScanForward64, _BitScanReverse, _BitScanReverse64. GCC 및 clang에서는 __builtin_clz__builtin_ctz.


2
@ e2-e4 Visual Studio는 AMD64 용으로 컴파일 할 때 인라인 어셈블리를 지원하지 않습니다. 이것이 내가 내장 함수를 권장하는 이유입니다.

5
C ++ 20부터 std::countr_zerostd::countl_zero. Boost를 사용하는 경우 boost::multiprecision::lsb및 이라는 휴대용 래퍼가 boost::multiprecision::msb있습니다.
Giovanni Cerretani

8
이것은 내 질문에 전혀 대답하지 않습니다.-왜 어떤 찬성 투표를 받았는지 궁금합니다
Walter

3
@Walter "응답하지 않는다"는 말은 무엇입니까? 나는 당신이해야 할 일, 전처리기를 사용한 다음 내장 함수를 정확하게 대답했습니다.

2
분명히 C ++ 20은 마침내 비트 스캔, #include <bit> 팝 카운트 및 회전을 사용 하여 en.cppreference.com/w/cpp/header/bit 를 추가 하고 있습니다. 비트 스캔을 이식 가능하게 노출하는 데 이렇게 오래 걸렸다는 것은 한심한 일이지만 지금은 절대로하지 않는 것보다 낫습니다. (Portable popcnt는를 통해 사용 가능했습니다 std::bitset::count().) C ++ 20에는 Rust가 제공하는 것 ( doc.rust-lang.org/std/primitive.i32.html ), 예를 들어 일부 CPU가 효율적으로 제공 하는 비트 리버스 및 엔디안 이 여전히 누락 되어 있습니다 . 하지만 전부는 아닙니다. 사용자가 빠른 것이 무엇인지 알아야하지만 모든 CPU가 가지고있는 작업을위한 휴대용 내장.
Peter Cordes

7

1 대신 0과 비교하면 일부 작업이 절약됩니다.

bool has_compact_bits2(int val) {
    if (val == 0) return true;
    int h = __builtin_clz(val);
    // Clear bits to the left
    val = (unsigned)val << h;
    int l = __builtin_ctz(val);
    // Invert
    // >>l - Clear bits to the right
    return (~(unsigned)val)>>l == 0;
}

다음 gcc10 -O3은 x86_64 에서 위의 것보다 적은 하나의 명령어를 생성하고 on sign 확장을 사용합니다.

bool has_compact_bits3(int val) {
    if (val == 0) return true;
    int h = __builtin_clz(val);
    val <<= h;
    int l = __builtin_ctz(val);
    return ~(val>>l) == 0;
}

godbolt에서 테스트되었습니다 .


불행히도 이것은 휴대용이 아닙니다. 저는 항상 그 시프트 오퍼레이터들에게 오퍼레이터 우선 순위가 잘못 될까봐 두려워 ~val<<h>>h>>l == 0합니다. 당신이 생각하는 바를 확실히 하고 있습니까?
Walter

4
예, 어쨌든 중괄호를 편집하고 추가했습니다. Och, 휴대용 솔루션에 관심이 있습니까? 나는 there exists a faster way?무엇이든 보고 있다고 생각 했기 때문 이다.
KamilCuk

5

요구 사항을 바꿀 수 있습니다.

  • N을 이전 비트 수와 다른 비트 수로 설정 (비트를 반복하여)
  • N = 2이고 첫 번째 또는 마지막 비트가 0이면 대답은 예입니다.
  • N = 1이면 대답은 예입니다 (모든 1이 한쪽에 있기 때문에)
  • N = 0이고 어떤 비트가 0이면 1이없는 것입니다. 답을 예 또는 아니오라고 생각하면 귀하에게 달렸습니다.
  • 기타 : 대답은 '아니오'입니다.

모든 비트를 살펴보면 다음과 같이 보일 수 있습니다.

unsigned int count_bit_changes (uint32_t value) {
  unsigned int bit;
  unsigned int changes = 0;
  uint32_t last_bit = value & 1;
  for (bit = 1; bit < 32; bit++) {
    value = value >> 1;
    if (value & 1 != last_bit  {
      changes++;
      last_bit = value & 1;
    }
  }
  return changes;
}

그러나 이것은 확실히 최적화 될 수 있습니다 (예를 들어 , 값이 1 인 더 이상 중요한 비트가 존재하지 않음을 의미하는 도달 하면 for루프 를 중단함으로써 ).value0


3

다음과 같은 계산 순서를 수행 할 수 있습니다 ( val입력으로 가정 ).

uint32_t x = val;
x |= x >>  1;
x |= x >>  2;
x |= x >>  4;
x |= x >>  8;
x |= x >> 16;

1로 1채워진 최상위 아래에 모두 0이있는 숫자를 구합니다 .

당신은 또한 계산할 수 있습니다 y = val & -valval(예 : 7 & -7 == 112 & -12 == 4) 에서 최하위 1 비트를 제외한 모든 것을 제거하도록 .
경고 :에 대해 실패하므로이 val == INT_MIN케이스를 별도로 처리해야하지만 즉시 처리됩니다.

그런 다음 y한 위치 만큼 오른쪽으로 이동 하여의 실제 LSB보다 약간 아래 val로 이동하고 for와 동일한 루틴을 수행합니다 x.

uint32_t y = (val & -val) >> 1;
y |= y >>  1;
y |= y >>  2;
y |= y >>  4;
y |= y >>  8;
y |= y >> 16;

그런 다음 x - y또는 x & ~y또는 x ^ y의 전체 길이에 걸쳐 '압축 된'비트 마스크를 생성합니다 val. '콤팩트' val인지 비교해 보십시오 val.


2

gcc 내장 지침 을 사용하여 다음 사항 을 확인할 수 있습니다.

세트 비트 수

int __builtin_popcount (unsigned int x) x
의 1 비트 수를 반환합니다.

(a-b)와 같습니다.

a : 가장 높은 세트 비트의 인덱스 (32-CTZ) (부호없는 정수에서 32 비트이므로 32).

int __builtin_clz (unsigned int x)
최상위 비트 위치에서 시작하여 x의 선행 0 비트 수를 반환합니다. x가 0이면 결과는 정의되지 않습니다.

b : 최저 세트 비트 (CLZ)의 인덱스 :

int __builtin_clz (unsigned int x)
최상위 비트 위치에서 시작하여 x의 선행 0 비트 수를 반환합니다. x가 0이면 결과는 정의되지 않습니다.

예를 들어 n = 0b0001100110; popcount로 4를 얻지 만 인덱스 차이 (a-b)는 6을 반환합니다.

bool has_contiguous_one_bits(unsigned n) {
    return (32 - __builtin_clz(n) - __builtin_ctz(n)) == __builtin_popcount(n);
}

다음과 같이 작성할 수도 있습니다.

bool has_contiguous_one_bits(unsigned n) {
    return (__builtin_popcount(n) + __builtin_clz(n) + __builtin_ctz(n)) == 32;
}

현재 가장 많이 찬성 된 답변보다 더 우아하거나 효율적이라고 생각하지 않습니다.

return (x & x + (x & -x)) == 0;

다음 어셈블리로 :

mov     eax, edi
neg     eax
and     eax, edi
add     eax, edi
test    eax, edi
sete    al

하지만 이해하기 더 쉬울 것입니다.


1

좋아, 여기에 비트를 반복하는 버전이 있습니다.

template<typename Integer>
inline constexpr bool has_compact_bits(Integer val) noexcept
{
    Integer test = 1;
    while(!(test & val) && test) test<<=1; // skip unset bits to find first set bit
    while( (test & val) && test) test<<=1; // skip set bits to find next unset bit
    while(!(test & val) && test) test<<=1; // skip unset bits to find an offending set bit
    return !test;
}

처음 두 루프는 첫 번째 압축 영역을 찾았습니다. 최종 루프는 해당 영역 이외의 다른 설정 비트가 있는지 확인합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.