Bir tamsayıdaki 1 bitlerin bitişik bir bölgede olmasını test etmenin zarif ve hızlı bir yolu var mı?


84

Bit değeri 1 olan konumların (32 bitlik bir tam sayı için 0'dan 31'e kadar) bitişik bir bölge oluşturup oluşturmadığını test etmem gerekiyor. Örneğin:

00111111000000000000000000000000      is contiguous
00111111000000000000000011000000      is not contiguous

Bu testin, yani bazı işlevlerin has_contiguous_one_bits(int)taşınabilir olmasını istiyorum.

Açık bir yol, ilk set biti, ardından ilk set olmayan biti bulmak ve daha fazla set biti olup olmadığını kontrol etmek için pozisyonlar üzerinden döngü yapmaktır.

Daha hızlı bir yolu var mı acaba? En yüksek ve en düşük ayarlı bitleri bulmanın hızlı yöntemleri varsa (ancak bu sorudan taşınabilir olanlar yok gibi görünüyor), o zaman olası bir uygulama

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

Sırf eğlence olsun diye, işte bitişik bitlere sahip ilk 100 tam sayı:

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

onlar (elbette) (1<<m)*(1<<n-1)negatif olmayan mve n.


4
@aafulei evet, 0x0kompakttır. Tersini tanımlamak daha kolaydır (kompakt değil): eğer wo set bitleri varsa, aralarında en az bir set edilmemiş bit vardır.
Walter

1
@KamilCuk h>=lait (zımni) işlevselliği ile highest_set_bit()velowest_set_bit()
Walter


6
Bu OEIS bağlantısı, bu sayıların ikili durumdayken basamaklarının artmadığını söylüyor. Onlara atıfta bulunmanın başka bir yolu da, bunların bitişik (veya belki bağlantılı) olduğunu söylemektir. Bu matematikçi için "kompakt" çok farklı bir anlama geliyor.
Teepeemm

1
@Teepeemm Sanırım bu sorunun sıcak ağ sorularında sona ermesinin bir nedeni, tam olarak kompakt kelimesinin bu şekilde kötüye kullanılmasıdır, kesinlikle bu yüzden tıkladım: Çok fazla düşünmüyordum ve kompaktlığı tanımlamanın nasıl mantıklı olabileceğini merak ettim bu şekilde. Açıkçası mantıklı değil.
Kimse

Yanıtlar:


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

Kısaca:

x & -xen düşük bit setini verir x(veya sıfırsa xsıfırdır).

x + (x & -x) ardışık 1'lerin en düşük dizesini tek bir 1'e dönüştürür (veya sıfıra kaydırır).

x & x + (x & -x) bu 1 biti temizler.

(x & x + (x & -x)) == 0 diğer 1 bitin kalıp kalmadığını test eder.

Uzun:

-xeşittir ~x+1, varsaydığımız ikinin tümleyicisini kullanarak. Bitler ters çevrildikten sonra ~x, 1 taşıma ekleyerek düşük 1 biti içeri ~xve ilk 0 biti geri çevirir ancak sonra durur. Bu nedenle, -xilk 1'e kadar olan ve onu içeren düşük bitler, düşük bitleri ile aynıdır x, ancak tüm yüksek bitler ters çevrilir. (Örnek: ~10011100verir 01100011ve 1 verir 01100100, yani düşükler 100aynıdır, ancak yüksekler 10011çevrilir 01100.) Sonra x & -xbize her ikisinde de 1 olan tek biti verir, bu da en düşük 1 bittir ( 00000100). ( xSıfır ise x & -x, sıfırdır.)

Bunu eklemek x, tüm ardışık 1'leri 0'lara değiştirerek bir taşımaya neden olur. Bir sonraki yüksek 0 bitinde 1 bırakacak (veya üst uçta taşınarak sarılmış toplam sıfır bırakacak) ( 10100000.)

Bu AND ile xyapıldığında, 1'lerin 0'lara değiştirildiği (ve ayrıca taşınmanın 0'ı 1'e değiştirdiği) yerlerde 0'lar vardır. Yani sonuç, sadece 1 bit daha yüksekse sıfır değildir.


23
En azından birisi Hacker's Delight kitabını biliyor. Cevap için lütfen bölüm 2-1'e bakın. Ancak bu, burada SO'da da birkaç kez yanıtlandı. Her neyse: +1
Armin Montigny

33
Umarım üretimde böyle bir kod yazarsanız, açıklamalara açıklamayı dahil edersiniz;)
Polygnome

14
Bu, x86 BMI1'in Intel'de 1 uop, AMD Zen'de 2 uop olan x & -xtek bir blsitalimatta yapılmasından oldukça faydalanıyor. godbolt.org/z/5zBx-A . Ancak BMI1 olmadan, @ KevinZ'in sürümü daha da verimli.
Peter Cordes

3
@TommyAndersen: _Boolstandart bir anahtar kelimedir, C 2018 6.4.1 1'e göre.
Eric Postpischil

1
@Walter: Hmm? Bu kod kullanır unsigned. Eğer ikiye tamamlayıcı işaretli testi yapmak istiyorsanız int, en kolay yol bu cevabın rutine intdönüştürülmesine izin vermenizdir unsigned. Bu istenen sonucu verecektir. Operasyon gösterisinin intdoğrudan imzalı bir kişiye uygulanması, taşma / taşıma sorunları nedeniyle sorunlu olabilir. (Bir kişinin bir tamamlayıcı veya oturum ve-büyüklüğünü test etmek istiyorsanız int, o ölçüde sadece teorik ilgi bugünlerde başka bir mesele.)
Eric Postpischil

29

Aslında herhangi bir içsel kullanmaya gerek yoktur.

İlk önce ilk 1'den önceki tüm 0'ları çevirin. Ardından yeni değerin bir mersenne numarası olup olmadığını test edin. Bu algo'da sıfır, true olarak eşlenir.

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

Elbette, intrinsics kullanmak istiyorsanız, işte popcount yöntemi:

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
İlk sürüm , istismar / talimatlarla derlendiğinde yalnızca 4 talimata indirgenir . Şimdiye kadar önerilen en kısa versiyon olacaktı. Ne yazık ki, neredeyse hiçbir işlemci bu komut seti uzantısını desteklemiyor . -mtbmblsfillblcfill
Giovanni Cerretani

18

Aslında baştaki sıfırları saymanıza gerek yok. Yorumlarda pmg tarafından önerildiği gibi, aradığınız sayıların OEIS A023758 dizisine ait olması gerçeğinden yararlanarak , yani 2 ^ i - 2 ^ j ve i> = j biçimindeki sayılar, sondaki sıfırları sayabilirsiniz ( yani j - 1 ), bu bitleri orijinal değerde değiştirin ( 2 ^ j - 1 eklemeye eşdeğer ) ve sonra bu değerin 2 ^ i - 1 biçiminde olup olmadığını kontrol edin . GCC / clang içselleriyle,

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

Bu sürüm sizinkinden biraz daha hızlı ve KamilCuk ve Yuri Feldman tarafından yalnızca popcount tarafından önerilen sürüm.

Eğer C ++ 20 kullanıyorsanız, size değiştirerek taşınabilir fonksiyonunu alabilirsiniz __builtin_ctzile 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;
}

Oyuncular çirkin, ancak bitleri işlerken işaretsiz türlerle çalışmanın daha iyi olduğu konusunda sizi uyarıyor. Pre-C ++ 20 alternatifleri vardır boost::multiprecision::lsb.

Düzenle:

Üstü çizili bağlantıdaki kıyaslama, Yuri Feldman sürümü için hiçbir popcount talimatının yayınlanmaması nedeniyle sınırlıydı. Bunları bilgisayarımda derlemeye çalışırken -march=westmere, aşağıdaki süreyi 1 milyar yineleme için aşağıdakilerden aynı dizilerle ölçtüm std::mt19937:

  • sizin sürümünüz: 5,7 sn
  • KamilCuk'un ikinci versiyonu: 4,7 sn
  • benim versiyonum: 4,7 s
  • Eric Postpischil'in ilk sürümü: 4,3 sn.
  • Yuri Feldman'ın sürümü (açıkça kullanarak __builtin_popcount): 4,1 sn

Yani, en azından benim mimarimde, en hızlısı popcount ile görünüyor.

Düzenleme 2:

Benchmarkımı yeni Eric Postpischil'in sürümüyle güncelledim. Yorumlarda talep edildiği gibi, testimin kodu burada bulunabilir . PRNG'nin ihtiyaç duyduğu süreyi tahmin etmek için işlemsiz bir döngü ekledim. KevinZ tarafından hazırlanan iki versiyonu da ekledim. Kod -O3 -msse4 -mbmialmak popcntve blsiöğretmek için clang ile derlenmiştir (Peter Cordes sayesinde).

Sonuçlar: En azından benim mimarimde, Eric Postpischil'in versiyonu Yuri Feldman'ınki kadar ve şimdiye kadar önerilen diğer versiyonlardan en az iki kat daha hızlı.


Bir ameliyat kaldırıldı: return (x & x + (x & -x)) == 0;.
Eric Postpischil

3
Bu, @ Eric'in sürümünün eski bir sürümünü karşılaştırıyor, değil mi? Mevcut sürümle, Eric'in gcc -O3 -march=nehalem(popcnt'yi kullanılabilir kılmak için) az sayıda talimatı veya BMI1 blsiiçin mevcutsa daha azını derler x & -x: godbolt.org/z/zuyj_f . Ve talimatlar, popcntYuri'nin 3 döngü gecikmesi olan versiyonu dışında, hepsi basit tek-uop'tur . (Ama iş hacmini ölçtüğünüzü varsayıyorum.) Ayrıca and valYuri'ninkini kaldırmış olmanız gerektiğini, yoksa daha yavaş olacağını varsayıyorum .
Peter Cordes

2
Ayrıca, hangi donanımı karşılaştırdınız? Karşılaştırma kodunuzun tamamını Godbolt'a veya başka bir şeye bağlamak iyi bir fikir olacaktır, bu nedenle gelecekteki okuyucular C ++ uygulamalarını kolayca test edebilirler.
Peter Cordes

2
Ayrıca @ KevinZ'in sürümünü de test etmelisiniz; BMI1 olmadan daha da az talimatla derler (en azından clang ile; gcc'nin satır içi olmayan versiyonu a israf eder movve bundan yararlanamaz lea): godbolt.org/z/5jeQLQ . İle BMI1, Eric'in versiyonu Intel en azından nerede, hala daha iyi x86-64 üzerindedir blsitek uop, ama AMD 2 UOPs var.
Peter Cordes

15

Hızlı olduğundan emin değilim, ancak val^(val>>1)en fazla 2 bit olduğunu doğrulayarak tek satırlık bir iş yapabilir .

Bu yalnızca işaretsiz türlerde çalışır: 0üstte bir kaydırma (mantıksal kaydırma) gereklidir, işaret bitinin bir kopyasında kayan aritmetik bir sağa kaydırma gerekmez.

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

Reddetmek için 0(yani yalnızca tam olarak 1 bitişik bit grubuna sahip girişleri kabul edin), mantıksal-AND ile valsıfır olmayan. Bu soruya verilen diğer cevaplar özet 0olarak kabul edilir.

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

C ++ std::bitset::count(), popcount aracılığıyla veya C ++ 20'destd::popcount . C hala, bir popcnt veya benzer bir talimatın mevcut olduğu hedefler üzerine güvenilir bir şekilde derleyen taşınabilir bir yoluna sahip değildir.


2
Ayrıca şimdiye kadarki en hızlısı.
Giovanni Cerretani

2
İşaret bitinin kopyalarını değil, sıfırları değiştirdiğinizden emin olmak için işaretsiz bir tür kullanmanız gerektiğini düşünüyorum. Düşünün 11011111. Aritmetik sağa kayar, olur 11101111ve XOR olur 00110000. Mantıksal sağa kaydırma ile ( 0üstte a'ya kaydırma ), 10110000birden çok bit grubunu elde eder ve doğru şekilde algılarsınız. Bunu düzeltmek için düzenleme.
Peter Cordes

3
Bu gerçekten zekice. Tarzdan hoşlanmadığım kadar (sadece IMO kullanın __builtin_popcount(), her derleyicinin bugünlerde böyle bir ilkel var), bu açık ara en hızlısı (modern bir işlemci üzerinde). Aslında, sunumun ciddi şekilde önemli olduğunu iddia edeceğim, çünkü tek bir talimat olarak POPCNT'ye sahip olmayan bir cpu'da, benim uygulamam bunu yenebilir. Bu nedenle, bu uygulamayı kullanacaksanız, sadece intrinsic'i kullanmalısınız. std::bitsetkorkunç bir arayüze sahip.
KevinZ

9

CPU'ların bunun için çok hızlı özel talimatları vardır. PC'de BSR / BSF (1985'te 80386'da tanıtıldı), ARM'de ise CLZ / CTZ

En az önemli ayar bitinin dizinini bulmak için birini kullanın, tamsayıyı bu miktarda sağa kaydırın. En anlamlı set bitinin bir dizinini bulmak için başka bir tane kullanın, tamsayınızı (1u << (bsr + 1)) - 1 ile karşılaştırın.

Ne yazık ki 35 yıl, donanımla eşleşmesi için C ++ dilini güncellemek için yeterli değildi. Bu talimatları C ++ 'dan kullanmak için içsel bilgilere ihtiyacınız olacak, bunlar taşınabilir değildir ve sonuçları biraz farklı biçimlerde döndürür. #ifdefDerleyiciyi algılamak için ön işlemci vb. Kullanın ve ardından uygun içsel bilgileri kullanın. Bunlar MSVC ise _BitScanForward, _BitScanForward64, _BitScanReverse, _BitScanReverse64. GCC ve clang'da bunlar__builtin_clz ve __builtin_ctz.


2
@ e2-e4 Visual studio, AMD64 için derleme yaparken satır içi derlemeyi desteklemez. Bu yüzden intrinsics'i tavsiye ediyorum.
Soonts

5
C ++ 20'den beri std::countr_zerove var std::countl_zero. Kuvvetlendirme kullandığınız durumda, adı portatif sarmalayıcılarını sahiptir boost::multiprecision::lsbve boost::multiprecision::msb.
Giovanni Cerretani

8
Bu hiç benim soruya cevap vermez - herhangi upvotes var neden acaba
Walter

3
@Walter Ne demek "cevap vermiyor"? Tam olarak ne yapmanız gerektiğini yanıtladım, ön işlemciyi ve ardından içselleri kullanın.
Soonts

2
Görünüşe göre C ++ 20, #include <bit> en.cppreference.com/w/cpp/header/bit'i bit tarama, popcount ve rotate ile ekliyor. Bit taramayı taşınabilir bir şekilde açığa çıkarmanın bu kadar uzun sürmesi acınası, ama şimdi hiç olmadığı kadar iyi. (Taşınabilir popcnt üzerinden erişilebilir std::bitset::count().) C ++ 20, Rust'un sağladığı bazı şeyleri ( doc.rust-lang.org/std/primitive.i32.html ), örneğin bazı CPU'ların verimli bir şekilde sağladığı bit-ters ve endian gibi bazı şeyleri hala eksik fakat hepsi değil. Kullanıcıların neyin hızlı olduğunu bilmesi gerekse de, herhangi bir CPU'nun sahip olduğu bir işlem için taşınabilir bir yerleşik.
Peter Cordes

7

Birler yerine sıfırlarla karşılaştırma yapmak bazı işlemleri kurtaracaktır:

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

Aşağıdakiler gcc10 -O3, x86_64'te yukarıdakinden daha az bir talimatla sonuçlanır ve oturum uzantısında kullanılır:

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 üzerinde test edildi .


ne yazık ki bu taşınabilir değil. Ben her zaman korkarım ki bu vardiya operatörlerinde operatör önceliğini yanlış anlıyorum - ~val<<h>>h>>l == 0düşündüğünüz şeyi yaptığından emin misiniz?
Walter

4
Evet, eminim, yine de diş telleri ekledim. Och, yani taşınabilir bir çözümle ilgileniyor musunuz? Çünkü baktım there exists a faster way?ve her şeyin yolunda gittiğini varsaydım.
KamilCuk

5

Gereksinimi yeniden ifade edebilirsiniz:

  • N'yi öncekinden farklı bit sayısını ayarlayın (bitler arasında yineleyerek)
  • N = 2 ise ve ilk veya son bit 0 ise cevap evettir
  • N = 1 ise cevap evettir (çünkü tüm 1'ler bir tarafta)
  • Eğer N = 0 ise ve herhangi bir bit 0 ise o zaman hiç 1'iniz yok, cevabın evet veya hayır olduğunu düşünüyorsanız size kalmış
  • başka bir şey: cevap hayır

Tüm bitlerin üzerinden geçmek şöyle görünebilir:

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

Ancak bu kesinlikle optimize edilebilir (örneğin for, valueulaşıldığında döngü iptal edilerek 0, bu da 1 değerine sahip daha fazla anlamlı bit olmadığı anlamına gelir).


3

Şu hesaplama dizisini yapabilirsiniz ( valgirdi olarak varsayarsak ):

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

Tümü sıfırların altında en anlamlı birlerle 1dolu bir sayı elde etmek için .

Ayrıca y = val & -val, en az önemli olan 1 bit val(örneğin, 7 & -7 == 1ve 12 & -12 == 4) hariç tümünü ayırmayı da hesaplayabilirsiniz .
Uyarı: Bu başarısız olur val == INT_MIN, bu nedenle bu vakayı ayrı olarak ele almanız gerekir, ancak bu hemen olur.

Ardından y, gerçek LSB değerinin biraz altına inmek için bir konum sağa kaydırın ve aşağıdaki valrutini uygulayın x:

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

Daha sonra x - yveya tüm uzunluğunu kapsayan 'kompakt' bit maskesini üretir x & ~yveya x ^ yüretir val. "Kompakt" valolup olmadığını görmek için sadece karşılaştır val.


2

Aşağıdakileri kontrol etmek için gcc yerleşik talimatlarını kullanabiliriz:

Set bitlerinin sayısı

int __builtin_popcount (unsigned int x) x'teki
1 bit sayısını döndürür.

eşittir (a - b):

a : En yüksek ayarlanmış bitin indeksi (32 - CTZ) (32 bit çünkü işaretsiz bir tamsayı).

int __builtin_clz (işaretsiz int x)
En anlamlı bit konumundan başlayarak x'in başındaki 0 bitlerin sayısını döndürür. X 0 ise, sonuç tanımsızdır.

b : En düşük ayarlı bitin indeksi (CLZ):

int __builtin_clz (işaretsiz int x)
En anlamlı bit konumundan başlayarak x'in başındaki 0 bitlerin sayısını döndürür. X 0 ise, sonuç tanımsızdır.

Örneğin n = 0b0001100110; popcount ile 4 elde edeceğiz ancak indeks farkı (a - b) 6 döndürecektir.

şu şekilde de yazılabilir:

Bunun şu andaki en olumlu cevaptan daha zarif veya verimli olduğunu düşünmüyorum:

aşağıdaki montaj ile:

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

ama anlaşılması muhtemelen daha kolaydır.


1

Tamam, işte bitlerin üzerinden geçen bir sürüm

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

İlk iki döngü, ilk kompakt bölgeyi buldu. Son döngü, o bölgenin ötesinde başka bir bit olup olmadığını kontrol eder.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.