0番目のインデックスが配列の最初のバイトのMSBであり、8番目のインデックスが2番目のバイトのMSBであるビット配列実装を持っています...
このビット配列に設定されている最初のビットをすばやく見つける方法は何ですか?私が調べた関連ソリューションはすべて、最初の最下位ビットを見つけますが、最初の最も重要なビットが必要です。したがって、0x00A1を指定すると、8が必要です(左から9番目のビットなので)。
GCCには __builtin_clz
これは、x86/x64ではBSR、ARMではCLZなどに変換され、ハードウェアが実装していない場合は命令をエミュレートします。
Visual C++ 2005以降では _BitScanReverse
。
tl:dr; 32ビットの場合、 de Bruijn乗算 。を使用します
"fastest" ポータブルアルゴリズムです。このスレッドの他のすべてのポータブル32ビットMSBアルゴリズムよりも大幅に高速で正確です。
De Bruijnアルゴリズムは、入力がゼロの場合にも正しい結果を返します。 __builtin_clzおよび_BitScanReverse命令 誤った結果を返す 入力がゼロの場合
Windows x86-64では、de Bruijn乗算は、同等の(欠陥のある)Windows関数に匹敵する速度で実行されます。パフォーマンスの差はわずか3%です。
コードは次のとおりです。
u32 msbDeBruijn32( u32 v )
{
static const int MultiplyDeBruijnBitPosition[32] =
{
0, 9, 1, 10, 13, 21, 2, 29, 11, 14, 16, 18, 22, 25, 3, 30,
8, 12, 20, 28, 15, 17, 24, 7, 19, 27, 23, 6, 26, 5, 4, 31
};
v |= v >> 1; // first round down to one less than a power of 2
v |= v >> 2;
v |= v >> 4;
v |= v >> 8;
v |= v >> 16;
return MultiplyDeBruijnBitPosition[( u32 )( v * 0x07C4ACDDU ) >> 27];
}
このスレッドの他のすべての回答は、著者が示唆するよりも実行がはるかに遅いか、結果を正しく計算しないか、またはその両方です。それらすべてのベンチマークを行い、彼らが主張することを実行することを確認しましょう。
これらすべての実装をテストするための簡単なC++ 11ハーネスを次に示します。 Visual Studioで正常にコンパイルされますが、すべての最新のコンパイラで動作するはずです。パフォーマンスモード(bVerifyResults = false)およびチェックモード(bVerifyResults = true)でベンチマークを実行できます。
検証モードでの結果は次のとおりです。
Verification failed for msbNative64: input was 0; output was 818af060; expected 0
Verification failed for msbFfs: input was 22df; output was 0; expected d
Verification failed for msbPerformanceJunkie32: input was 0; output was ffffffff; expected 0
Verification failed for msbNative32: input was 0; output was 9ab07060; expected 0
「パフォーマンスジャンキー」とMicrosoftネイティブ実装は、入力がゼロの場合に異なる処理を実行します。 msbPerformanceJunkie32は-1を生成し、Microsoftの_BitScanReverseは、基になるハードウェア命令と一致する乱数を生成します。また、msbPerformanceJunkie32実装は、他のすべての回答から1つ外れた結果を生成します。
リリースモードでコンパイルされたi7-4600ラップトップで実行されているパフォーマンスモードでの結果は次のとおりです。
msbLoop64 took 2.56751 seconds
msbNative64 took 0.222197 seconds
msbLoop32 took 1.43456 seconds
msbFfs took 0.525097 seconds
msbPerformanceJunkie32 took 1.07939 seconds
msbDeBruijn32 took 0.224947 seconds
msbNative32 took 0.218275 seconds
De Bruijnバージョンは、ブランチレスであるため、他の実装soundlyよりも優れているため、均等に分散された出力セットを生成する入力に対して適切に動作します。他のすべてのバージョンは、最新のCPUでの分岐の予測ミスのペナルティのために、任意の入力に対して低速です。 smbFfs関数は誤った結果を生成するため、無視できます。
実装の一部は32ビット入力で動作し、一部は64ビット入力で動作します。テンプレートは、入力サイズに関係なく、リンゴとリンゴを比較するのに役立ちます。
コードは次のとおりです。必要に応じて、ベンチマークをダウンロードして実行してください。
#include <iostream>
#include <chrono>
#include <random>
#include <cassert>
#include <string>
#include <limits>
#ifdef _MSC_VER
#define Microsoft_COMPILER 1
#include <intrin.h>
#endif // _MSC_VER
const int iterations = 100000000;
bool bVerifyResults = false;
std::random_device rd;
std::default_random_engine re(rd());
typedef unsigned int u32;
typedef unsigned long long u64;
class Timer
{
public:
Timer() : beg_(clock_::now()) {}
void reset() {
beg_ = clock_::now();
}
double elapsed() const {
return std::chrono::duration_cast<second_>
(clock_::now() - beg_).count();
}
private:
typedef std::chrono::high_resolution_clock clock_;
typedef std::chrono::duration<double, std::ratio<1> > second_;
std::chrono::time_point<clock_> beg_;
};
unsigned int msbPerformanceJunkie32(u32 x)
{
static const unsigned int bval[] =
{ 0,1,2,2,3,3,3,3,4,4,4,4,4,4,4,4 };
unsigned int r = 0;
if (x & 0xFFFF0000) {
r += 16 / 1;
x >>= 16 / 1;
}
if (x & 0x0000FF00) {
r += 16 / 2;
x >>= 16 / 2;
}
if (x & 0x000000F0) {
r += 16 / 4;
x >>= 16 / 4;
}
return r + bval[x];
}
#define FFS(t) \
{ \
register int n = 0; \
if (!(0xffff & t)) \
n += 16; \
if (!((0xff << n) & t)) \
n += 8; \
if (!((0xf << n) & t)) \
n += 4; \
if (!((0x3 << n) & t)) \
n += 2; \
if (!((0x1 << n) & t)) \
n += 1; \
return n; \
}
unsigned int msbFfs32(u32 x)
{
FFS(x);
}
unsigned int msbLoop32(u32 x)
{
int r = 0;
if (x < 1) return 0;
while (x >>= 1) r++;
return r;
}
unsigned int msbLoop64(u64 x)
{
int r = 0;
if (x < 1) return 0;
while (x >>= 1) r++;
return r;
}
u32 msbDeBruijn32(u32 v)
{
static const int MultiplyDeBruijnBitPosition[32] =
{
0, 9, 1, 10, 13, 21, 2, 29, 11, 14, 16, 18, 22, 25, 3, 30,
8, 12, 20, 28, 15, 17, 24, 7, 19, 27, 23, 6, 26, 5, 4, 31
};
v |= v >> 1; // first round down to one less than a power of 2
v |= v >> 2;
v |= v >> 4;
v |= v >> 8;
v |= v >> 16;
return MultiplyDeBruijnBitPosition[(u32)(v * 0x07C4ACDDU) >> 27];
}
#ifdef Microsoft_COMPILER
u32 msbNative32(u32 val)
{
unsigned long result;
_BitScanReverse(&result, val);
return result;
}
u32 msbNative64(u64 val)
{
unsigned long result;
_BitScanReverse64(&result, val);
return result;
}
#endif // Microsoft_COMPILER
template <typename InputType>
void test(unsigned int msbFunc(InputType),
const std::string &name,
const std::vector< InputType > &inputs,
std::vector< unsigned int > &results,
bool bIsReference = false
)
{
if (bIsReference)
{
int i = 0;
for (int i = 0; i < iterations; i++)
results[i] = msbFunc(inputs[i]);
}
InputType result;
if (bVerifyResults)
{
bool bNotified = false;
for (int i = 0; i < iterations; i++)
{
result = msbFunc(inputs[i]);
if ((result != results[i]) && !bNotified)
{
std::cout << "Verification failed for " << name << ": "
<< "input was " << std::hex << inputs[i]
<< "; output was " << result
<< "; expected " << results[i]
<< std::endl;
bNotified = true;
}
}
}
else
{
Timer t;
for (int i = 0; i < iterations; i++)
{
result = msbFunc(inputs[i]);
}
double elapsed = t.elapsed();
if ( !bIsReference )
std::cout << name << " took " << elapsed << " seconds" << std::endl;
if (result == -1.0f)
std::cout << "this comparison only exists to keep the compiler from " <<
"optimizing out the benchmark; this branch will never be called";
}
}
void main()
{
std::uniform_int_distribution <u64> dist64(0,
std::numeric_limits< u64 >::max());
std::uniform_int_distribution <u32> shift64(0, 63);
std::vector< u64 > inputs64;
for (int i = 0; i < iterations; i++)
{
inputs64.Push_back(dist64(re) >> shift64(re));
}
std::vector< u32 > results64;
results64.resize(iterations);
test< u64 >(msbLoop64, "msbLoop64", inputs64, results64, true);
test< u64 >(msbLoop64, "msbLoop64", inputs64, results64, false);
#ifdef Microsoft_COMPILER
test< u64 >(msbNative64, "msbNative64", inputs64, results64, false);
#endif // Microsoft_COMPILER
std::cout << std::endl;
std::uniform_int_distribution <u32> dist32(0,
std::numeric_limits< u32 >::max());
std::uniform_int_distribution <u32> shift32(0, 31);
std::vector< u32 > inputs32;
for (int i = 0; i < iterations; i++)
inputs32.Push_back(dist32(re) >> shift32(re));
std::vector< u32 > results32;
results32.resize(iterations);
test< u32 >(msbLoop32, "msbLoop32", inputs32, results32, true);
test< u32 >(msbLoop32, "msbLoop32", inputs32, results32, false);
test< u32 >(msbFfs32, "msbFfs", inputs32, results32, false);
test< u32 >(msbPerformanceJunkie32, "msbPerformanceJunkie32",
inputs32, results32, false);
test< u32 >(msbDeBruijn32, "msbDeBruijn32", inputs32, results32, false);
#ifdef Microsoft_COMPILER
test< u32 >(msbNative32, "msbNative32", inputs32, results32, false);
#endif // Microsoft_COMPILER
}
パフォーマンスジャンキーとして、MSBセットのバリエーションを多数試しましたが、次のものが私が遭遇した最速です。
unsigned int msb32(unsigned int x)
{
static const unsigned int bval[] =
{0,1,2,2,3,3,3,3,4,4,4,4,4,4,4,4};
unsigned int r = 0;
if (x & 0xFFFF0000) { r += 16/1; x >>= 16/1; }
if (x & 0x0000FF00) { r += 16/2; x >>= 16/2; }
if (x & 0x000000F0) { r += 16/4; x >>= 16/4; }
return r + bval[x];
}
これを行うには複数の方法があり、異なる実装の相対的なパフォーマンスはある程度マシンに依存します(これを同様の目的である程度ベンチマークしました)。一部のマシンでは、このための組み込みの指示さえあります(使用可能な場合は使用し、移植性に対処できる場合)。
いくつかの実装を確認してください here (「integer log base 2」の下)。 GCCを使用している場合は、関数__builtin_clz
および__builtin_clzl
(ゼロ以外の符号なし整数と符号なしlongに対してそれぞれこれを行います)。 「clz」は「count leading zeros」の略で、同じ問題を説明する別の方法です。
もちろん、ビット配列が適切なマシンWordに収まらない場合、配列内の単語を反復処理して最初の非ゼロのWordを見つけ、そのWordでのみこの計算を実行する必要があります。
これを行う最も速い方法については、BSR(ビットスキャンリバース)x86 asm命令を参照してください。 Intelのドキュメントから:Searches the source operand (second operand) for the most significant set bit (1 bit). If a most significant 1 bit is found, its bit index is stored in the destination operand (first operand).
X86を使用している場合、SSE2操作と(gccの世界では)「ffs」と発音されるfind-first-bit命令を組み合わせて、事実上、バイト単位またはワード単位のソリューションを破ることができます。 「最下位ビットには「fls」、最上位ビットには「fls」。回答に「C」コードをフォーマットするのに問題があります(!@#$%^)。チェックアウト: http://mischasan.wordpress.com/2011/11/03/sse2-bit-trick-ffsfls-for-xmm-registers/
最も重要なビットを取得するために多くの関数を使用しましたが、一般に32ビットと64ビットの数値間またはx86_64とx86ボックス間の問題が発生します。関数__builtin_clz
、__builtin_clzl
、および__builtin_clzll
は、32/64ビット数およびx86_64およびx86マシンで適切に機能します。ただし、3つの機能が必要です。正数のすべてのケースを処理する右シフトに依存する単純なMSBを見つけました。少なくとも私がそれを使用する場合、他の人が失敗した場合は成功しました:
int
getmsb (unsigned long long x)
{
int r = 0;
if (x < 1) return 0;
while (x >>= 1) r++;
return r;
}
入力をunsigned long long
として指定することにより、unsigned char
からunsigned long long
までのすべての数値クラスを処理でき、標準定義が与えられると、x86_64およびx86ビルド間で互換性があります。 0
のケースは0
を返すように定義されていますが、必要に応じて変更できます。簡単なテストと出力は次のとおりです。
int
main (int argc, char *argv[]) {
unsigned char c0 = 0;
unsigned char c = 216;
unsigned short s = 1021;
unsigned int ui = 32768;
unsigned long ul = 3297381253;
unsigned long long ull = 323543844043;
int i = 32767;
printf (" %16u MSB : %d\n", c0, getmsb (c0));
printf (" %16u MSB : %d\n", c, getmsb (c));
printf (" %16u MSB : %d\n", s, getmsb (s));
printf (" %16u MSB : %d\n", i, getmsb (i));
printf (" %16u MSB : %d\n", ui, getmsb (ui));
printf (" %16lu MSB : %d\n", ul, getmsb (ul));
printf (" %16llu MSB : %d\n", ull, getmsb (ull));
return 0;
}
出力:
0 MSB : 0
216 MSB : 7
1021 MSB : 9
32767 MSB : 14
32768 MSB : 15
3297381253 MSB : 31
323543844043 MSB : 38
注:速度を考慮して、単一の関数を使用して__builtin_clzll
を中心とする同じことを達成すると、約6倍速くなります。
追加します!
typedef unsigned long long u64;
typedef unsigned int u32;
typedef unsigned char u8;
u8 findMostSignificantBit (u64 u64Val)
{
u8 u8Shift;
u8 u8Bit = 0;
assert (u64Val != 0ULL);
for (u8Shift = 32 ; u8Shift != 0 ; u8Shift >>= 1)
{
u64 u64Temp = u64Val >> u8Shift;
if (u64Temp)
{
u8Bit |= u8Shift; // notice not using +=
u64Val = u64Temp;
}
}
return u8Bit;
}
もちろん、これは配列ではなく64ビットの数値(unsigned long long)で機能しています。また、私が知らなかった組み込みのg ++関数を多くの人が指摘しています。なんて面白い。
とにかく、これは6回の反復で最上位ビットを見つけ、関数に0を渡した場合にアサートを与えます。チップセットの命令にアクセスできる場合、使用するのに最適な機能ではありません。
また、+ =の代わりに| =を使用しています。これらは常に2のべき乗であり、ORは(古典的に)加算よりも高速です。ロールオーバーしたことはありません。
これはバイナリ検索であり、常に6回の反復で結果を見つけます。
繰り返しますが、これは優れています:
u8 findMostSignificantBit2 (u64 u64Val)
{
assert (u64Val != 0ULL);
return (u8) (__builtin_ctzll(u64Val));
}
最速ではありませんが、動作します...
//// C program
#include <math.h>
#define POS_OF_HIGHESTBIT(a) /* 0th position is the Least-Signif-Bit */ \
((unsigned) log2(a)) /* thus: do not use if a <= 0 */
#define NUM_OF_HIGHESTBIT(a) ((!(a)) \
? 0 /* no msb set*/ \
: (1 << POS_OF_HIGHESTBIT(a) ))
// could be changed and optimized, if it is known that the following NEVER holds: a <= 0
int main()
{
unsigned a = 5; // 0b101
unsigned b = NUM_OF_HIGHESTBIT(a); // 4 since 4 = 0b100
return 0;
}
x86には、ビットインデックス(先行ゼロのカウントaboveではなく)を返すBSR命令があります。
しかし、残念ながら、効率的にすべてのコンパイラーに公開する移植可能な組み込み関数はありません。 GNU Cは___builtin_clz
_を提供しますが、unsigned bitidx = 31 - __builtin_clz(x);
は現在のGCCおよびICCを使用してBSRに最適化することはしません。式は同等なので、could)。
以下は、x86でbsr
命令をjustに効率的にコンパイルするBSR32()
およびBSR64()
マクロまたは関数を定義しています。 (入力がゼロの場合、ガベージ結果が生成されます。組み込み関数では、asm命令の動作を利用して、input = 0の宛先を変更しないままにする方法はありません。)
非x86への移植性には、さらに_#ifdef
_が必要です。 _31-__builtin_clz
_にフォールバックします。ほとんどの非x86 ISAは、先行ゼロビットがある場合、ビットインデックスを提供する代わりに先行ゼロをカウントします。そのため、GNU Cは___builtin_clz
_をポータブルビルトインとして定義します。(ターゲットシステムにHWサポートがない場合、ビルトインは通常libgccヘルパー関数を呼び出してソフトウェアエミュレーションにコンパイルします。 )
_#include <stdint.h>
// define BSR32() and BSR64()
#if defined(_MSC_VER) || defined(__INTEL_COMPILER)
#ifdef __INTEL_COMPILER
typedef unsigned int bsr_idx_t;
#else
#include <intrin.h> // MSVC
typedef unsigned long bsr_idx_t;
#endif
static inline
unsigned BSR32(unsigned long x){
bsr_idx_t idx;
_BitScanReverse(&idx, x); // ignore bool retval
return idx;
}
static inline
unsigned BSR64(uint64_t x) {
bsr_idx_t idx;
_BitScanReverse64(&idx, x); // ignore bool retval
return idx;
}
#Elif defined(__GNUC__)
#ifdef __clang__
static inline unsigned BSR64(uint64_t x) {
return 63-__builtin_clzll(x);
// gcc/ICC can't optimize this back to just BSR, but clang can and doesn't provide alternate intrinsics
}
#else
#define BSR64 __builtin_ia32_bsrdi
#endif
#include <x86intrin.h>
#define BSR32(x) _bit_scan_reverse(x)
#endif
_
bsf
はおそらくコンパイラにとってそれほど助けを必要としません。なぜなら、ビルトインはLSBのビットインデックス、つまり後続ゼロのカウントを返すasm命令の動作と一致するからです。
テスト呼び出し元unsigned test32(unsigned x) { return BSR32(x); }
は、すべての主要なx86コンパイラーで1命令にインライン化します Godboltコンパイラーエクスプローラー 。 BSR64は同じ方法で、64ビットのオペランドサイズバージョンにインライン化します。 最上位ビット以下のすべてのビットをゼロにするx86/x86_64命令はありますか? ユースケースなども参照してください。
_;; x64 MSVC 19.16 -O2
unsigned int test32(unsigned int) PROC ; test32, COMDAT
bsr eax, ecx
ret 0
unsigned int test32(unsigned int) ENDP ; test32
_
_# clang -O3 -march=haswell is too "smart?" for its own good:
test32(unsigned int):
lzcnt eax, edi
xor eax, 31
ret
_
_# gcc8.2 -O3 -march=haswell
test32(unsigned int):
bsr eax, edi
ret
_
_# ICC19 -O3 -march=haswell
test32(unsigned int):
bsr eax, edi #15.9
ret #41.12
_
これのポイントは、ポータブル(非MSVC)バージョンからの遅いコードを避けることです:
_#ifdef __GNUC__
unsigned badgcc(uint64_t x) {
return 63 - __builtin_clzll(x);
}
#endif
_
_-march=haswell
_がなければ、clangからBSRを取得しますが、次のようになります。
_# gcc8.2 -O3
badgcc(unsigned long):
bsr rdi, rdi
mov eax, 63
xor rdi, 63
sub eax, edi
ret
_
_# ICC19.0.1 -O3
badgcc(unsigned long):
mov rax, -1 #46.17
bsr rdx, rdi #46.17
cmove rdx, rax #46.17
neg rdx #46.17
add rdx, 63 #46.17
neg edx #46.17
add edx, 63 #46.17
mov eax, edx #46.17
ret #46.17
_
それはただ厄介です。 (ICCがCMOVを実行して、入力がゼロの場合_-1
_を生成することを確認するのは興味深い。BSRは、inputに従ってZFを設定します。結果に)
_-march=haswell
_(またはBMI1命令の使用を有効にする)を使用すると、それほど悪くはありませんが、BSRほどではありません。モジュロ出力の依存関係。コンパイラは主にlzcntを回避するために動作しますが、奇妙なことにBSRを回避しません。 (input = 0の振る舞いのため、出力依存関係がtrue依存関係である場合) LZCNT問題の「出力依存関係」を破る理由
純粋なCでこれを行うための2つの最良の方法:
最初にバイト/ワード配列を線形検索してゼロでない最初のバイト/ワードを見つけてから、見つけたバイト/ワードの展開されたバイナリ検索を行います。
if (b>=0x10)
if (b>=0x40)
if (b>=0x80) return 0;
else return 1;
else
if (b>=0x20) return 2;
else return 3;
else
if (b>=0x4)
if (b>=0x8) return 4;
else return 5;
else
if (b>=0x2) return 6;
else return 7;
3(ちなみにlog2(8)です)答えを得るための条件付きジャンプ。最新のx86マシンでは、最後のマシンが条件付きmovに最適化されます。
または、ルックアップテーブルを使用して、設定されている最初のビットのインデックスにバイトをマッピングします。
関連するトピックは、整数log2関数です。思い出すと、ffmpegにはNice実装があります。
編集:実際には上記のバイナリ検索をブランチレスバイナリ検索にすることができますが、この場合の方が効率的かどうかはわかりません...
__builtin_clz()を説明するコードスニペットを次に示します。
////// go.c ////////
#include <stdio.h>
unsigned NUM_BITS_U = ((sizeof(unsigned) << 3) - 1);
#define POS_OF_HIGHESTBITclz(a) (NUM_BITS_U - __builtin_clz(a)) /* only works for a != 0 */
#define NUM_OF_HIGHESTBITclz(a) ((a) \
? (1U << POS_OF_HIGHESTBITclz(a)) \
: 0)
int main()
{
unsigned ui;
for (ui = 0U; ui < 18U; ++ui)
printf("%i \t %i\n", ui, NUM_OF_HIGHESTBITclz(ui));
return 0;
}
タグは32ビットを示していますが、使用している値は16ビットのようです。あなたが32ビットを意味していた場合、0x00a1の答えは8ではなく24でなければなりません。
左側からMSBビットインデックスを探していて、uint32_tのみを処理することがわかっていると仮定した場合、ここに明確で単純なアルゴリズムがあります。
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
int main()
{
uint32_t test_value = 0x00a1;
int i;
for (i=0; i<32; ++i)
{
if (test_value & (0x80000000 >> i))
{
printf("i = %d\n", i);
exit(0);
}
}
return 0;
}
Java私はこれを使用します:
static public final int msb(int n) {
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
n >>>= 1;
n += 1;
return n;
}
そして:
static public final int msb_index(int n) {
final int[] multiply_de_bruijn_bit_position = {
0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8,
31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9
};
return multiply_de_bruijn_bit_position[(msb(n) * 0x077CB531) >>> 27];
}
これは、任意のサイズのバイト配列に対する単純なブルートフォースアルゴリズムです。
_int msb( unsigned char x); // prototype for function that returns
// most significant bit set
unsigned char* p;
for (p = arr + num_elements; p != arr;) {
--p;
if (*p != 0) break;
}
// p is with pointing to the last byte that has a bit set, or
// it's pointing to the first byte in the array
if (*p) {
return ((p - arr) * 8) + msb( *p);
}
// what do you want to return if no bits are set?
return -1;
_
適切なmsb()
関数とint
または_long long
_サイズのデータのちらつきを処理するための最適化を考え出す読者のための演習として残しておきます。