15

有没有一种直接的方法可以仅使用按位运算从 2 的幂中提取指数?

编辑:虽然这个问题最初是关于按位运算的,但如果您想知道“在 Python 中给定 Y = 2 X找到 X 的最快方法是什么?”**

我目前正在尝试优化一个减少表单中偶数N的例程( Rabin-Miller primality test ) 。我可以通过以下方式获得零件:2**s * d2**s

two_power_s = N & -N

但我找不到通过按位运算仅提取“ s ”的方法。我目前正在测试的解决方法不太满意(它们都非常慢)是:

  • 使用对数函数
  • 操作 2**s 的二进制表示(即计算尾随零)
  • 循环除以 2 直到结果为 1

我正在使用 python,但我想这个问题的答案应该与语言无关。

4

7 回答 7

7

“语言不可知论”和担心性能是几乎不相容的概念。

大多数现代处理器都有一条 CLZ 指令,“计算前导零”。在 GCC 中,您可以使用 __builtin_clz(x) (如果不是最快的话,它也会为缺少 clz 的目标生成合理的代码)。请注意,此 CLZ 未定义为零,因此如果它在您的应用程序中很重要,您将需要一个额外的分支来捕获该情况。

在 CELT ( http://celt-codec.org ) 中,我们用于缺乏 CLZ 的编译器的无分支 CLZ 是由 Timothy B. Terriberry 编写的:


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 中实现这部分代码。

于 2010-02-12T21:36:12.240 回答
5

简短的回答

就python而言:

  • 找到 2**x 的指数的最快方法是在字典中查找,其哈希值是 2 的幂(参见代码中的“ hashlookup ”)
  • 最快的按位方法是一种称为“ unrolled_bitwise ”的方法。
  • 以前的两种方法都有明确定义(但可扩展)的上限。没有硬编码上限(只要 python 可以处理数字)的最快方法是“ log_e ”。

初步说明

  1. 下面的所有速度测量值都是通过timeit.Timer.repeat(testn, cycles)wheretestn设置为 3cycles获得的,并由脚本自动调整以获得秒范围内的时间(注意:此自动调整机制中存在一个错误,已于 18/02/ 修复2010)。
  2. 并非所有方法都可以扩展,这就是为什么我没有针对 2 的各种幂测试所有函数的原因
  3. 我没有设法使某些建议的方法起作用(该函数返回错误的结果)。我还没有时间进行逐步调试会话:我包含了代码(已注释),以防有人通过检查发现错误(或想自己执行调试)

结果

函数(2 5)**

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%

函数(2 31)**

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%

函数(2 128)**

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%

函数(2 1024)**

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
于 2010-02-14T01:00:33.687 回答
4

有一个页面包含很多这些类型的技巧和黑客。它是为 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。

于 2010-02-12T21:20:30.673 回答
3

您可以使用 binsearch 在 O(lg s) 时间内完成任意长度的整数。

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

对于固定大小的整数,查找表应该是最快的解决方案,并且可能是整体上最好的。

于 2010-02-12T21:24:34.237 回答
2

Late to the party, but how about int.bit_length(n) - 1? You asked for straightforward, and that seems the simplest to me. The CPython implementation looks reasonably performant.

于 2019-06-10T07:55:56.240 回答
1

范围似乎是已知的。让我们假设它上升到 1<<20,只是为了让它更有趣:

max_log2=20

所以制作一个列表,(实际上)将一个整数映射到它的以 2 为底的对数。以下将解决问题:

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

(这对于不是 2 的幂的数字没有任何用处;问题陈述表明它们不需要处理。不过很容易解决这个问题。)

获取对数的函数非常简单,可以很容易地内联:

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.

使用 adict进行查找是另一种可能性,利用仅检查两个幂的方式:

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在浮点对象上使用该方法来获取一个字符串,该字符串包括(作为其最后一部分)该数字的以 2 为底的指数。一般来说,这有点慢,但如果指数只是一位数,它可以很简单地完成:

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

这纯粹是为了娱乐价值,因为它没有竞争力——但令人惊讶的是,它仍然比按位方法更快。

所以看起来使用列表是要走的路。

(由于内存有限,这种方法不会无限扩展,但为了弥补这一点,执行速度不会以max_log2运行 python 代码时会注意到的任何方式依赖于 或输入值。关于内存消费,如果我没记错我的python内部,表格将占用大约(1<<max_log2)*4字节,因为内容都是解释器会自动实习的小整数。所以,当max_log2是20时,大约是4MB。)

于 2010-02-14T23:07:30.260 回答
1

这实际上是对mac发布的性能测试的评论。我将其发布为具有正确代码格式和缩进的答案

mac,你能尝试一下 Mark Byers 建议的 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 );
于 2010-02-14T23:36:16.003 回答