別の投稿者は、バイト幅のルックアップを使用してルックアップ テーブルを提供しました。もう少しパフォーマンスを上げたい場合 (256 個のルックアップ エントリではなく 32K のメモリを犠牲にして) 、C# 7 for .NETで15 ビットのルックアップ テーブルを使用するソリューションを次に示します。
興味深い部分は、テーブルの初期化です。これはプロセスの存続期間に必要な比較的小さなブロックであるため、 を使用してアンマネージ メモリを割り当てますMarshal.AllocHGlobal
。ご覧のとおり、最大のパフォーマンスを得るために、例全体がネイティブとして記述されています。
readonly static byte[] msb_tab_15;
// Initialize a table of 32768 bytes with the bit position (counting from LSB=0)
// of the highest 'set' (non-zero) bit of its corresponding 16-bit index value.
// The table is compressed by half, so use (value >> 1) for indexing.
static MyStaticInit()
{
var p = new byte[0x8000];
for (byte n = 0; n < 16; n++)
for (int c = (1 << n) >> 1, i = 0; i < c; i++)
p[c + i] = n;
msb_tab_15 = p;
}
テーブルは、上記のコードを介して一度だけ初期化する必要があります。これは読み取り専用であるため、同時アクセスのために単一のグローバル コピーを共有できます。この表を使用すると、さまざまな整数幅 (8、16、32、および 64 ビット) について、ここで探している整数log 2をすばやく検索できます。
0
「最上位セット ビット」の概念が定義されていない唯一の整数であるのテーブル エントリには、値 が与えられていることに注意してください-1
。この区別は、以下のコードで値が 0 の上位ワードを適切に処理するために必要です。これ以上苦労することなく、さまざまな整数プリミティブのそれぞれのコードを次に示します。
ulong (64 ビット) バージョン
/// <summary> Index of the highest set bit in 'v', or -1 for value '0' </summary>
public static int HighestOne(this ulong v)
{
if ((long)v <= 0)
return (int)((v >> 57) & 0x40) - 1; // handles cases v==0 and MSB==63
int j = /**/ (int)((0xFFFFFFFFU - v /****/) >> 58) & 0x20;
j |= /*****/ (int)((0x0000FFFFU - (v >> j)) >> 59) & 0x10;
return j + msb_tab_15[v >> (j + 1)];
}
uint (32 ビット) バージョン
/// <summary> Index of the highest set bit in 'v', or -1 for value '0' </summary>
public static int HighestOne(uint v)
{
if ((int)v <= 0)
return (int)((v >> 26) & 0x20) - 1; // handles cases v==0 and MSB==31
int j = (int)((0x0000FFFFU - v) >> 27) & 0x10;
return j + msb_tab_15[v >> (j + 1)];
}
上記のさまざまなオーバーロード
public static int HighestOne(long v) => HighestOne((ulong)v);
public static int HighestOne(int v) => HighestOne((uint)v);
public static int HighestOne(ushort v) => msb_tab_15[v >> 1];
public static int HighestOne(short v) => msb_tab_15[(ushort)v >> 1];
public static int HighestOne(char ch) => msb_tab_15[ch >> 1];
public static int HighestOne(sbyte v) => msb_tab_15[(byte)v >> 1];
public static int HighestOne(byte v) => msb_tab_15[v >> 1];
これは完全で実用的なソリューションであり、.NET 4.7.2 での最高のパフォーマンスを表すさまざまな代替手段を、特殊なパフォーマンス テスト ハーネスと比較しました。これらのいくつかを以下に示します。テスト パラメータは、すべての 65 ビット位置の均一な密度、つまり0 ... 31/63プラス値0
(結果は -1) でした。ターゲット インデックス位置より下のビットはランダムに埋められました。テストはx64のみ、リリース モードで、JIT 最適化が有効になっています。
これで正式な回答は終わりです。以下は、上記のコードのパフォーマンスと正確性を検証するために実行したテストに関連する代替テスト候補のソース コードへの簡単なメモとリンクです。
上記で提供され、Tab16A としてコード化されたバージョンは、多くの実行で一貫して勝者でした。これらのさまざまな候補は、アクティブな作業/スクラッチ形式で、ここ、ここ、およびここにあります。
1候補.HighestOne_Tab16A 622,496
2候補。HighestOne_Tab16C 628,234
3候補。HighestOne_Tab8A 649,146
4候補。HighestOne_Tab8B 656,847
5候補。HighestOne_Tab16B 657,147
6候補。HighestOne_Tab16D 659,650
7 _highest_one_bit_UNMANAGED.HighestOne_U 702,900
8 de_Bruijn.IndexOfMSB 709,672
9 _old_2.HighestOne_Old2 715,810
10 _test_A.HighestOne8 757,188
11 _old_1.HighestOne_Old1 757,925
12 _test_A.HighestOne5 (安全でない) 760,387
13 _test_B.HighestOne8 (安全でない) 763,904
14 _test_A.HighestOne3 (安全でない) 766,433
15 _test_A.HighestOne1 (安全でない) 767,321
16 _test_A.HighestOne4 (危険) 771,702
17 _test_B.HighestOne2 (危険) 772,136
18 _test_B.HighestOne1 (危険) 772,527
19 _test_B.HighestOne3 (危険) 774,140
20 _test_A.HighestOne7 (危険) 774,581
21 _test_B.HighestOne7 (危険) 775,463
22 _test_A.HighestOne2 (危険) 776,865
23 候補。HighestOne_NoTab 777,698
24 _test_B.HighestOne6 (危険) 779,481
25 _test_A.HighestOne6 (危険) 781,553
26 _test_B.HighestOne4 (危険) 785,504
27 _test_B.HighestOne5 (危険) 789,797
28 _test_A.HighestOne0 (安全でない) 809,566
29 _test_B.HighestOne0 (安全でない) 814,990
30 _highest_one_bit.HighestOne 824,345
30 _bitarray_ext.RtlFindMostSignificantBit 894,069
31 人の候補。HighestOne_Naive 898,865
注目に値するのは、 ntdll.dll!RtlFindMostSignificantBit
P/Invoke 経由のパフォーマンスがひどいことです。
[DllImport("ntdll.dll"), SuppressUnmanagedCodeSecurity, SecuritySafeCritical]
public static extern int RtlFindMostSignificantBit(ulong ul);
実際の関数全体を次に示します。
RtlFindMostSignificantBit:
bsr rdx, rcx
mov eax,0FFFFFFFFh
movzx ecx, dl
cmovne eax,ecx
ret
これらの 5 つの行に起因するパフォーマンスの低下は想像できないので、マネージド/ネイティブ移行のペナルティが原因であるに違いありません。また、テストで 128 バイト (および 256 バイト) (8 ビット) のルックアップ テーブルshort
よりも 32KB (および 64KB) (16 ビット) の直接ルックアップ テーブルが実際に好まれたことにも驚きました。byte
以下は 16 ビット ルックアップよりも競争力があると思いましたが、後者は一貫してこれを上回りました。
public static int HighestOne_Tab8A(ulong v)
{
if ((long)v <= 0)
return (int)((v >> 57) & 64) - 1;
int j;
j = /**/ (int)((0xFFFFFFFFU - v) >> 58) & 32;
j += /**/ (int)((0x0000FFFFU - (v >> j)) >> 59) & 16;
j += /**/ (int)((0x000000FFU - (v >> j)) >> 60) & 8;
return j + msb_tab_8[v >> j];
}
最後に指摘しておきたいのは、私の deBruijn メソッドがうまくいかなかったことにかなりショックを受けたということです。これは、私が以前に広く使用していた方法です。
const ulong N_bsf64 = 0x07EDD5E59A4E28C2,
N_bsr64 = 0x03F79D71B4CB0A89;
readonly public static sbyte[]
bsf64 =
{
63, 0, 58, 1, 59, 47, 53, 2, 60, 39, 48, 27, 54, 33, 42, 3,
61, 51, 37, 40, 49, 18, 28, 20, 55, 30, 34, 11, 43, 14, 22, 4,
62, 57, 46, 52, 38, 26, 32, 41, 50, 36, 17, 19, 29, 10, 13, 21,
56, 45, 25, 31, 35, 16, 9, 12, 44, 24, 15, 8, 23, 7, 6, 5,
},
bsr64 =
{
0, 47, 1, 56, 48, 27, 2, 60, 57, 49, 41, 37, 28, 16, 3, 61,
54, 58, 35, 52, 50, 42, 21, 44, 38, 32, 29, 23, 17, 11, 4, 62,
46, 55, 26, 59, 40, 36, 15, 53, 34, 51, 20, 43, 31, 22, 10, 45,
25, 39, 14, 33, 19, 30, 9, 24, 13, 18, 8, 12, 7, 6, 5, 63,
};
public static int IndexOfLSB(ulong v) =>
v != 0 ? bsf64[((v & (ulong)-(long)v) * N_bsf64) >> 58] : -1;
public static int IndexOfMSB(ulong v)
{
if ((long)v <= 0)
return (int)((v >> 57) & 64) - 1;
v |= v >> 1; v |= v >> 2; v |= v >> 4; // does anybody know a better
v |= v >> 8; v |= v >> 16; v |= v >> 32; // way than these 12 ops?
return bsr64[(v * N_bsr64) >> 58];
}
この SO question でdeBruijn メソッドがいかに優れていて優れているかについて多くの議論があり、私は同意する傾向がありました。私の推測では、deBruijn とダイレクト ルックアップ テーブル メソッド (最速であることがわかりました) はどちらもテーブル ルックアップを実行する必要があり、どちらも最小限の分岐しかありませんが、deBruijn だけが 64 ビットの乗算演算を行います。IndexOfMSB
私はここで関数をテストしただけで、deBruijn はテストしませんでしたが、deBruijnIndexOfLSB
は操作が非常に少ないため (上記を参照)、はるかにうまくいく可能性が高く、LSB には引き続き使用する可能性があります。