Нахождение показателя N = 2* * x с помощью побитовых операций [логарифм в базе 2 из n]

есть ли простой способ извлечения экспоненты из степени 2, используя только побитовые операции?

EDIT: хотя вопрос изначально был о побитовых операциях, поток также хорошо читается, если вам интересно " каков самый быстрый способ найти X с учетом Y = 2X в Python?"* *

В настоящее время я пытаюсь оптимизировать процедуру (Rabin-Miller primality test), что снижает даже N в форм 2**s * d. Я могу достать 2**s участие:

two_power_s = N & -N

но я не могу найти способ, чтобы извлечь только "s" С побитовой операции. Обходные пути, которые я в настоящее время тестирую без особого удовлетворения (все они довольно медленные):

  • использование функции логарифма
  • управление двоичным представлением 2* * s (т. е. подсчет трейлинга нули)
  • петля на деление на 2, пока результат не будет 1

Я использую Python, но ответ на этот вопрос должен быть язык агностик, я полагаю.

6 ответов


"языковой агностик" и беспокойство о производительности в значительной степени несовместимы.

большинство современных процессоров имеют инструкцию CLZ, "подсчет ведущих нулей". В GCC вы можете добраться до него с __builtin_clz(x) (который также производит разумный, если не самый быстрый, код для целей, которым не хватает clz). Обратите внимание, что эта CLZ не определена для нуля, поэтому вам понадобится дополнительная ветвь, чтобы поймать этот случай, если это имеет значение в вашем приложении.

в КЕЛЬТЕ ( http://celt-codec.org) CLZ без ветвей, который мы используем для комплиеров, не имеющих CLZ, был написан Тимоти Б. Терриберри:


int ilog(uint32 _v){
  int ret;
  int m;
  ret=!!_v;
  m=!!(_v&0xFFFF0000)<<4;
  _v>>=m;
  ret|=m;
  m=!!(_v&0xFF00)<<3;
  _v>>=m;
  ret|=m;
  m=!!(_v&0xF0)<<2;
  _v>>=m;
  ret|=m;
  m=!!(_v&0xC)<<1;
  _v>>=m;
  ret|=m;
  ret+=!!(_v&0x2);
  return ret;
}

(комментарии указывают, что это было обнаружено быстрее, чем ветвящаяся версия и версия на основе таблицы поиска)

но если производительность настолько важна, вы, вероятно, не должны реализовывать эту часть своего кода в python.


короткий ответ:

что касается python:

  • на самый быстрый метод из всех найти показатель 2* * x, посмотрев в словаре, хэши которого являются степенями 2 (см."hashlookup" в коде)
  • на самый быстрый побитовый метод - это называется "unrolled_bitwise".
  • оба предыдущих метода имеют четко определенные (но расширяемые) верхние пределы. The самый быстрый метод без жестко закодированных верхних пределов (который масштабируется, насколько python может обрабатывать числа) является "log_e".

предварительные замечания

  1. все измерения скорости ниже были получены через timeit.Timer.repeat(testn, cycles) здесь testn было установлено 3 и cycles был автоматически настроен скриптом для получения времени в диапазоне секунд (Примечание: в этой автоматической настройке была ошибка механизм, который был установлен 18/02/2010).
  2. не все методы могут масштабироваться, вот почему я не тестировал все функции для различных степеней 2
  3. мне не удалось заставить некоторые из предложенных методов работать (функция возвращает неверный результат). У меня еще не было tiem для пошаговой отладки: я включил код (прокомментировал) на случай, если кто-то обнаружит ошибку при проверке (или захочет выполнить отладку сами)

результаты

func (25)**

hashlookup:          0.13s     100%
lookup:              0.15s     109%
stringcount:         0.29s     220%
unrolled_bitwise:    0.36s     272%
log_e:               0.60s     450%
bitcounter:          0.64s     479%
log_2:               0.69s     515%
ilog:                0.81s     609%
bitwise:             1.10s     821%
olgn:                1.42s    1065%

func (231)**

hashlookup:          0.11s     100%
unrolled_bitwise:    0.26s     229%
log_e:               0.30s     268%
stringcount:         0.30s     270%
log_2:               0.34s     301%
ilog:                0.41s     363%
bitwise:             0.87s     778%
olgn:                1.02s     912%
bitcounter:          1.42s    1264%

func (2128)**

hashlookup:     0.01s     100%
stringcount:    0.03s     264%
log_e:          0.04s     315%
log_2:          0.04s     383%
olgn:           0.18s    1585%
bitcounter:     1.41s   12393%

func (21024)**

log_e:          0.00s     100%
log_2:          0.01s     118%
stringcount:    0.02s     354%
olgn:           0.03s     707%
bitcounter:     1.73s   37695%

код

import math, sys

def stringcount(v):
    """mac"""    
    return len(bin(v)) - 3

def log_2(v):
    """mac"""    
    return int(round(math.log(v, 2), 0)) # 2**101 generates 100.999999999

def log_e(v):
    """bp on mac"""    
    return int(round(math.log(v)/0.69314718055994529, 0))  # 0.69 == log(2)

def bitcounter(v):
    """John Y on mac"""
    r = 0
    while v > 1 :
        v >>= 1
        r += 1
    return r

def olgn(n) :
    """outis"""
    if n < 1:
        return -1
    low = 0
    high = sys.getsizeof(n)*8 # not the best upper-bound guesstimate, but...
    while True:
        mid = (low+high)//2
        i = n >> mid
        if i == 1:
            return mid
        if i == 0:
            high = mid-1
        else:
            low = mid+1

def hashlookup(v):
    """mac on brone -- limit: v < 2**131"""
#    def prepareTable(max_log2=130) :
#        hash_table = {}
#        for p in range(1, max_log2) :
#            hash_table[2**p] = p
#        return hash_table

    global hash_table
    return hash_table[v] 

def lookup(v):
    """brone -- limit: v < 2**11"""
#    def prepareTable(max_log2=10) :
#        log2s_table=[0]*((1<<max_log2)+1)
#        for i in range(max_log2+1):
#            log2s_table[1<<i]=i
#        return tuple(log2s_table)

    global log2s_table
    return log2s_table[v]

def bitwise(v):
    """Mark Byers -- limit: v < 2**32"""
    b = (0x2, 0xC, 0xF0, 0xFF00, 0xFFFF0000)
    S = (1, 2, 4, 8, 16)
    r = 0
    for i in range(4, -1, -1) :
        if (v & b[i]) :
            v >>= S[i];
            r |= S[i];
    return r

def unrolled_bitwise(v):
    """x4u on Mark Byers -- limit:   v < 2**33"""
    r = 0;
    if v > 0xffff : 
        v >>= 16
        r = 16;
    if v > 0x00ff :
        v >>=  8
        r += 8;
    if v > 0x000f :
        v >>=  4
        r += 4;
    if v > 0x0003 : 
        v >>=  2
        r += 2;
    return r + (v >> 1)

def ilog(v):
    """Gregory Maxwell - (Original code: B. Terriberry) -- limit: v < 2**32"""
    ret = 1
    m = (not not v & 0xFFFF0000) << 4;
    v >>= m;
    ret |= m;
    m = (not not v & 0xFF00) << 3;
    v >>= m;
    ret |= m;
    m = (not not v & 0xF0) << 2;
    v >>= m;
    ret |= m;
    m = (not not v & 0xC) << 1;
    v >>= m;
    ret |= m;
    ret += (not not v & 0x2);
    return ret - 1;


# following table is equal to "return hashlookup.prepareTable()" 
hash_table = {...} # numbers have been cut out to avoid cluttering the post

# following table is equal to "return lookup.prepareTable()" - cached for speed
log2s_table = (...) # numbers have been cut out to avoid cluttering the post

есть страница с большим количеством этих типов трюков и хаков. Это написано для C, но многие из них также должны работать на Python (хотя производительность, очевидно, будет отличаться). Бит, который вы хотите, это здесь и далее.

можно попробовать этой например:

register unsigned int r = 0; // result of log2(v) will go here
for (i = 4; i >= 0; i--) // unroll for speed...
{
  if (v & b[i])
  {
    v >>= S[i];
    r |= S[i];
  } 
}

похоже, что он может быть преобразован в Python довольно легко.


вы можете сделать это за O (lg s) время для целых чисел произвольной длины, используя binsearch.

import sys
def floorlg(n):
    if n < 1:
        return -1
    low=0
    high=sys.getsizeof(n)*8 # not the best upper-bound guesstimate, but...
    while True:
        mid = (low+high)//2
        i = n >> mid
        if i == 1:
            return mid
        if i == 0:
            high = mid-1
        else:
            low = mid+1

для целых чисел фиксированного размера таблица поиска должна быть самым быстрым решением и, вероятно, лучшим в целом.


кажется, что диапазон известен. Предположим, что он поднимается до 1

max_log2=20

поэтому составьте список, который (по сути) отображает целое число в его логарифм базы 2. Следующее сделает трюк:

log2s_table=[0]*((1<<max_log2)+1)
for i in range(max_log2+1):
    log2s_table[1<<i]=i

(это не делает ничего полезного для чисел, которые не являются степенями двух; постановка проблемы предполагает, что их не нужно обрабатывать. Это было бы достаточно легко исправить.)

функция для того чтобы получить логарифм очень прост и может быть легко встроен:

def table(v):
    return log2s_table[v]

я не могу гарантировать, что тестовый код, который я написал, точно такой же, как тот, который используется для получения таймингов примера, но это гораздо быстрее, чем stringcount код:

stringcount: 0.43 s.
table: 0.16 s.

поскольку все значения в таблице меньше 256, я задавался вопросом, будет ли использование строки вместо списка быстрее или, возможно,array.array байтов, но не кости:

string: 0.25 s.
arr: 0.21 s.

С помощью dict сделать поиск является еще одной возможностью, воспользовавшись тем, как проверяются только полномочия двух:

log2s_map=dict([(1<<x,x) for x in range(max_log2+1)])

def map(v):
    return log2s_map[v]

результаты для этого были не так хороши, хотя:

map: 0.20 s.

и просто для удовольствия можно также использовать hex метод на float-объектах, чтобы получить строку, которая включает (в качестве последней части) базовый показатель 2 числа. Это немного медленное извлечение в целом, но если показатель будет только одной цифрой, это можно сделать прямолинейно достаточно:

def floathex(v):
    return ord(float(v).hex()[-1])-48

это чисто хоть для развлечения, как это было uncompetetive-хотя, удивительно, еще быстрее, чем подход побитовые.

таким образом, похоже, что использование списка-это путь.

(этот подход не будет масштабироваться бесконечно, из-за ограниченной памяти, но компенсировать это скорость выполнения не будет зависеть от max_log2 или входные значения, любым способом, который вы заметите при запуске кода python. Относительно памяти потребление, если я правильно помню свои внутренности python, таблица займет около (1<<max_log2)*4 байт, потому что содержимое-это все малые целые числа, которые интерпретатор будет интернировать автоматически. Итак, когда max_log2 это 20, это около 4 МБ.)


это фактически комментарий к тесту производительности, опубликованному mac. Я отправляю это как ответ, чтобы иметь правильное форматирование кода и отступов

mac, не могли бы вы попробовать развернутую реализацию bitseach, предложенную Марком Байерсом? Может быть, это просто доступ к массиву, который замедляет его. Теоретически этот подход должен быть быстрее других.

это будет выглядеть примерно так, хотя я не уверен, подходит ли форматирование для python, но я думаю, вы можете видеть, что он должен делать.

def bitwise(v):
    r = 0;
    if( v > 0xffff ) : v >>= 16; r = 16;
    if( v > 0x00ff ) : v >>=  8; r += 8;
    if( v > 0x000f ) : v >>=  4; r += 4;
    if( v > 0x0003 ) : v >>=  2; r += 2;
    return r + ( v >> 1 );

Если Python акций, отсутствие в Java из unsingned чисел он должен быть что-то вроде этого:

def bitwise(v):
    r = 0;
    if( v & 0xffff0000 ) : v >>>= 16; r = 16;
    if( v > 0x00ff ) : v >>=  8; r += 8;
    if( v > 0x000f ) : v >>=  4; r += 4;
    if( v > 0x0003 ) : v >>=  2; r += 2;
    return r + ( v >> 1 );