Другой плакат предоставил справочную таблицу , использующую широкий байт справочник. Если вы хотите повысить производительность (за счет 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;
}
Таблица требует одноразовой инициализации с помощью кода выше. Он доступен только для чтения, поэтому для одновременного доступа может использоваться одна глобальная копия. С помощью этой таблицы вы можете быстро найти целое число log 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 candidates.HighestOne_Tab16A 622,496
2 candidates.HighestOne_Tab16C 628,234
3 candidates.HighestOne_Tab8A 649,146
4 candidates.HighestOne_Tab8B 656,847
5 candidates.HighestOne_Tab16B 657,147
6 candidates.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 (unsafe) 760,387
13 _test_B.HighestOne8 (unsafe) 763,904
14 _test_A.HighestOne3 (unsafe) 766,433
15 _test_A.HighestOne1 (unsafe) 767,321
16 _test_A.HighestOne4 (unsafe) 771,702
17 _test_B.HighestOne2 (unsafe) 772,136
18 _test_B.HighestOne1 (unsafe) 772,527
19 _test_B.HighestOne3 (unsafe) 774,140
20 _test_A.HighestOne7 (unsafe) 774,581
21 _test_B.HighestOne7 (unsafe) 775,463
22 _test_A.HighestOne2 (unsafe) 776,865
23 candidates.HighestOne_NoTab 777,698
24 _test_B.HighestOne6 (unsafe) 779,481
25 _test_A.HighestOne6 (unsafe) 781,553
26 _test_B.HighestOne4 (unsafe) 785,504
27 _test_B.HighestOne5 (unsafe) 789,797
28 _test_A.HighestOne0 (unsafe) 809,566
29 _test_B.HighestOne0 (unsafe) 814,990
30 _highest_one_bit.HighestOne 824,345
30 _bitarray_ext.RtlFindMostSignificantBit 894,069
31 candidates.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
Я не могу себе представить низкую производительность, вызванную этими пятью линиями, поэтому виноваты штрафы за управляемый / нативный переход. Я также был удивлен, что тестирование действительно отдало предпочтение 32-килобайтным (и 64-килобайтным) short
(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];
}
Последнее, на что я укажу, это то, что я был шокирован тем, что мой метод Дебрюйна не стал лучше. Это метод, который я ранее использовал повсеместно:
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 есть операция 64-битного умножения. Я протестировал здесь только функции IndexOfMSB
, а не deBruijn IndexOfLSB
- но я ожидаю, что последний получит гораздо больше шансов, поскольку у него намного меньше операций (см. Выше), и я, вероятно, буду продолжать его использовать для LSB.