Інший плакат запропонував таблицю пошуку, використовуючи широкомасштабний пошук. У випадку, якщо ви хочете отримати трохи більшу продуктивність (ціною 32 Кб пам'яті замість всього 256 записів пошуку), тут ви знайдете рішення, використовуючи 15-бітну таблицю пошуку , в C # 7 для .NET .
Цікава частина - ініціалізація таблиці. Оскільки це порівняно невеликий блок, який ми хочемо впродовж всього процесу, я виділяю для цього некеровану пам'ять, використовуючи 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;
}
Таблиця вимагає одноразової ініціалізації за допомогою наведеного вище коду. Він доступний лише для читання, тому для одночасного доступу можна ділитися єдиною глобальною копією. За допомогою цієї таблиці ви можете швидко знайти цілий журнал 2 , який ми шукаємо тут, для всіх цілих ширин (8, 16, 32 та 64 біт).
Зверніть увагу, що значення таблиці для 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 кандидат. НайвищийOne_Tab16A 622 496
2 кандидати. НайвищийOne_Tab16C 628,234
3 кандидати. НайвищийOne_Tab8A 649,146
4 кандидати. НайвищийOne_Tab8B 656 847
5 кандидатів. НайвищийOne_Tab16B 657,147
6 кандидатів. НайвищийOne_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 кандидати. НайвищийOne_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 кандидат. НайвищийOne_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
Я не можу уявити, що низька продуктивність походить із цих п'яти рядків, тому винні адміністративні / перехідні штрафи повинні бути винні. Я також був здивований, що тестування дійсно віддало перевагу short
таблицям прямого пошуку 32 КБ (і 64 КБ) (16-бітові) над 128-байтовими (і 256-байтовими) byte
(8-бітовими) таблицями пошуку. Я думав, що наступне буде більш конкурентоспроможним щодо 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];
}
Існує багато дискусій про те, наскільки вищі та чудові методи deBruijn в цьому питанні питання , і я схильний погодитися. Моя думка полягає в тому, що в той час як і методи deBruijn, і таблиця прямого пошуку (що я виявився найшвидшим) обидва повинні виконати пошук таблиці, і обидва мають дуже мінімальне розгалуження, тільки deBruijn має 64-бітну операцію множення. Я протестував IndexOfMSB
тут лише функції - не deBruijn IndexOfLSB
- але я очікую, що останній матиме набагато кращі шанси, оскільки у нього стільки менше операцій (див. Вище), і я, ймовірно, продовжуватиму використовувати її для LSB.