【问题标题】:Find algorithm to base 2 of number without math module在没有数学模块的情况下查找以 2 为基数的算法
【发布时间】:2021-08-29 07:33:12
【问题描述】:

在不使用math 模块的情况下,找到给定数字以 2 为底的对数的最有效方法是什么?

math.logmath.log2 是浮点运算,因此存在固有的准确性问题,可能会给出错误的结果,但它们非常快。

该数字保证为2的幂,该函数只有在以下函数返回True时才会执行:

def power_of_2(n):
    return (n & (n-1) == 0) and n != 0

我希望结果是 100% 准确的,并且我已经找到了实现此目的的方法:

def log2(n):
    i = 0
    while n > 1:
        n >>= 1
        i += 1
    return i

性能:

In [244]: def log2(n):
     ...:     i = 0
     ...:     while n > 1:
     ...:         n >>= 1
     ...:         i += 1
     ...:     return i

In [245]: log2(33554432)
Out[245]: 25

In [246]: import math

In [247]: math.log2(33554432)
Out[247]: 25.0

In [248]: %timeit log2(33554432)
2.37 µs ± 208 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

In [249]: %timeit math.log2(33554432)
125 ns ± 1.34 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

我发现位操作比浮点操作更适合此任务,如何在使用相同逻辑的同时提高函数的性能?

【问题讨论】:

  • 你可以在这里找到答案stackoverflow.com/questions/13211137/…
  • @I_Al-thamary 你确定有“最有效的方法”吗?
  • n >>= 1 等于n //= 2,所以你基本上是除数,直到它变为零。如果您确定该数字是 2 的幂,您可以使用下一个“hack”return n.bit_length() - 1。它有效,因为在二进制表示中,任何 2 的幂都是1,尾部有一定数量的02⁰ = 0b12¹ = 0b102² = 0b100, ... , 2¹⁰ = 0b10000000000, ...
  • 准确率优越; 2^25 = 33554432
  • 这是作业吗?

标签: python python-3.x logarithm


【解决方案1】:

我尝试从12 的不同答案运行大多数方法,我发现power_of_2easy 表现出良好的性能。

import math
import timeit

def myLog(x, b):
    if x < b:
        return 0
    return 1 + float(myLog(x / b, b))


def easy(x):
    return x.bit_length() - 1


def brute(x):
    # determine max p such that 2^p <= x
    p = 0
    while 2 ** p <= x:
        p += 1
    return p - 1


def log2_approx(val):
    from math import floor
    val = floor(val)
    approx = 0
    while val != 0:
        val &= ~ (1 << approx)
        approx += 1
    return approx


def log2(n):
    i = 0
    while n > 1:
        n >>= 1
        i += 1
    return i


def power_of_2(n):
    return (n & (n - 1) == 0) and n != 0

def log2_fast(n):
    return  math.frexp(33554436)[1] - 1
import time
from functools import partial
 
import time

start_time = time.time()
math.log2(33554432)
print("math.log2 is %s microseconds ---" % ((time.time() - start_time) * 1000000))
start_time = time.time()
myLog(33554432.0, 2.0)
print(" myLog is %s microseconds ---" % ((time.time() - start_time) * 1000000))

start_time = time.time()
brute(33554432)
print(" brute is --- %s microseconds ---" % ((time.time() - start_time) * 1000000))
start_time = time.time()

log2_approx(33554432) - 1
print("log2_approx is --- %s microseconds ---" % ((time.time() - start_time) * 1000000))
start_time = time.time()
log2_fast = math.frexp(33554436)[1] - 1
print(" log2_fast is --- %s microseconds ---" % ((time.time() - start_time) * 1000000))

start_time = time.time()
log2(33554432)
print("log2 is --- %s microseconds ---" % ((time.time() - start_time) * 1000000))

start_time = time.time()
power_of_2(33554432)
print("power_of_2 is --- %s microseconds ---" % ((time.time() - start_time) * 1000000))
start_time = time.time()
easy(33554432)
print("easy is --- %s microseconds ---" % ((time.time() - start_time) * 1000000))

运行时间

 math.log2 is 3.0994415283203125 microseconds ---
 myLog is 5.4836273193359375 microseconds ---
 brute is --- 6.4373016357421875 microseconds ---
 log2_approx is --- 6.4373016357421875 microseconds ---
 log2_fast is --- 1.6689300537109375 microseconds ---
 log2 is --- 2.1457672119140625 microseconds ---
 power_of_2 is --- 0.7152557373046875 microseconds ---
 easy is --- 0.476837158203125 microseconds ---

使用timeit 显示延迟

test_items=33554432
base=2
times = timeit.Timer(partial(math.log2, test_items))
print("math.log2 is %s microseconds ---", (times.timeit(1000000)))
times = timeit.Timer(partial(myLog, test_items,base))
print("myLog is %s microseconds ---", (times.timeit(1000000)))
times = timeit.Timer(partial(easy, test_items))
print("easy is %s microseconds ---", (times.timeit(1000000)))
times = timeit.Timer(partial(brute, test_items))
print("brute is %s microseconds ---", (times.timeit(1000000)))
times = timeit.Timer(partial(log2_approx, test_items))
print("log2_approx is %s microseconds ---", (times.timeit(1000000)))
times = timeit.Timer(partial(log2, test_items))
print("log2 is %s microseconds ---", (times.timeit(1000000)))
times = timeit.Timer(partial(power_of_2, test_items))
print("power_of_2 is %s microseconds ---", (times.timeit(1000000)))
times = timeit.Timer(partial(log2_fast, test_items))
print("log2_fast is %s microseconds ---", (times.timeit(1000000)))

运行时间

math.log2 is %s microseconds --- 0.05126429593656212
myLog is %s microseconds --- 4.137887543998659
easy is %s microseconds --- 0.10356121498625726
brute is %s microseconds --- 5.254867412033491
log2_approx is %s microseconds --- 3.81522585300263
log2 is %s microseconds --- 1.7966924259671941
power_of_2 is %s microseconds --- 0.1572157460032031
log2_fast is %s microseconds --- 0.21886748599354178

【讨论】:

  • easy() 显示更好的结果,因为它是唯一的“原生”方法 (look)
  • @don'ttalkjustcode 与调用方法的顺序相同,我已更新。
  • 单次迭代的基准不具有代表性。考虑使用timeit
  • @I_Al-thamary,基准测试仍然需要改进
【解决方案2】:

如果您的代码需要重复调​​用此函数,并且输入保证是 2 的整数次幂(与您的示例不同!)我想很难击败硬编码的字典查找。

def precalculate_pow2(ceiling):
    i = 1
    result = dict()
    for n in range(ceiling):
        result[i] = n
        i <<= 1
    return result

powersof2 = precalculate_pow2(500) # or whatever your upper limit is

def log2(n):
    return powersof2[n]

很明显,计算值的循环并不比您当前的代码更有效,但是记住它可以让您在所有对 log2 函数的调用中分摊该成本。

作为一种变体,您可以从只包含一个值的字典开始,然后使用递归函数仅在后续调用中填充缺失的部分。

powersof2 = {1: 0}

def log2(n):
    if n not in powersof2:
        powersof2[n] = 1 + log2(n >> 1)
    return powersof2[n]

这有一个明显的缺点,如果你用负值调用它会进入一个无限循环(尽管 Python 会在你用完最大递归限制后抛出一个回溯)。

【讨论】:

  • 你能以n.bit_length() - 1为基准吗?
  • @OlvinR​​oght 不,不是。不要忘记查找需要通过相等性检查来确认哈希匹配。
  • 另一种变体可能是使用functools.lru_cache(),这样就无需预先计算所有可能未使用的值 - 您只需在首次使用时支付一次计算费用。
  • @OlvinR​​oght 在n=2**499(tripleee 选择的限制),bit_length 解已经是more than twice as fast
  • 我能理解为什么它可以在更大的数量上更快。基本上,bit_length(num)sizeof(num) * 8 - __builtin_clz(num) 结尾,这比任何散列函数都快。
猜你喜欢
  • 2017-09-24
  • 1970-01-01
  • 2020-02-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多