什么是计算/创建十的幂的最快方法?

6

如果您提供(整数)幂作为输入,创建相应的十的幂的最快方法是什么? 我想到了四种选择,其中最快的方法似乎是使用 f-string:

from functools import partial
from time import time
import numpy as np

def fstring(power):
    return float(f'1e{power}')

def asterisk(power):
    return 10**power

methods = {
    'fstring': fstring,
    'asterisk': asterisk,
    'pow': partial(pow, 10),
    'np.pow': partial(np.power, 10, dtype=float)
}

# "dtype=float" is necessary because otherwise it will raise: 
# ValueError: Integers to negative integer powers are not allowed.
# see https://dev59.com/olgQ5IYBdhLWcg3wGgP0#43287598
powers = [int(i) for i in np.arange(-10000, 10000)]
for name, method in methods.items():
    start = time()
    for i in powers:
        method(i)
    print(f'{name}: {time() - start}')

结果:

fstring: 0.008975982666015625
asterisk: 0.5190775394439697
pow: 0.4863283634185791
np.pow: 0.046906232833862305

我猜f-string方法是最快的,因为实际上没有计算任何东西,尽管它仅适用于十的整数幂,而其他方法是更复杂的操作,也适用于任何实数作为底数和指数。那么f-string实际上是最好的方法吗?


2
在 f-string 中计算了 Something。关键是所有的计算都是在 C 中进行的。 - chepner
1
一个区别是你的 fstring 只计算单个 float,估算单个 double 类型的十次幂。其他所有的都是构建一个大整数。sys.getsizeof(10**10000) 给我 4456 - 这是一个大整数。我没有偷看算法,但增加 Python 整数的成本相对较高。如果浮点数对你的应用程序足够好,那么就使用它。但如果你真的想要整数,你就要付出代价。 - tdelaney
1
你试过 int('1' + power * '0') 吗? - Arne
2
np.pow方法看起来有些虚假,因为对于指数大于38/308/4932float32/float64/float128类型,它们将返回inf - ekhumoro
1
decimal.Decimal 很快。我在全局定义了 ten = decimal.Decimal('10'),然后函数中返回 ten ** power - tdelaney
显示剩余4条评论
3个回答

6
你这是在比较不相干的东西。当n非负时,10 ** n计算一个整数,而float(f'1e{n}')计算一个浮点数。它们所需的时间不同,但它们解决不同的问题,因此哪个更快并不重要。
更糟糕的是,还有调用函数的开销,这个时间包括在所有备选项的计时中,但只有某些选项实际涉及到调用函数。如果你写10 ** n,那么你没有调用函数,但如果你使用partial(pow, 10),则必须将其作为函数调用才能得到结果。因此,您实际上并没有公平地比较10 ** n的速度。
不要自己编写计时代码,而应该使用专门设计用于此的timeit库进行测试。结果以秒为单位表示100万次循环(默认情况下),或者等效地表示每次重复的平均时间(以微秒为单位)。
以下是计算10的整数次幂的比较:
>>> from timeit import timeit
>>> timeit('10 ** n', setup='n = 500')
1.09881673199925
>>> timeit('pow(10, n)', setup='n = 500')
1.1821871869997267
>>> timeit('f(n)', setup='n = 500; from functools import partial; f = partial(pow, 10)')
1.1401332350014854

计算浮点数的10次方,下面是一个比较:注意计算 10.0 ** 5001e500 是没有意义的,因为结果只是一个 OverflowErrorinf

>>> timeit('10.0 ** n', setup='n = 200')
0.12391662099980749
>>> timeit('pow(10.0, n)', setup='n = 200')
0.17336435099969094
>>> timeit('f(n)', setup='n = 200; from functools import partial; f = partial(pow, 10.0)')
0.18887039500077663
>>> timeit('float(f"1e{n}")', setup='n = 200')
0.44305286100097874
>>> timeit('np.power(10.0, n, dtype=float)', setup='n = 200; import numpy as np')
1.491982370000187
>>> timeit('f(n)', setup='n = 200; from functools import partial; import numpy as np; f = partial(np.power, 10.0, dtype=float)')
1.6273324920002779

因此,在这两种情况下最快的选项都很明显:10 ** n用于整数,10.0 ** n用于浮点数。

1
我认为他们对partial的使用相当于将他们的“表达式解决方案”放入函数中,这似乎相当公平。与您将表达式与额外的partial调用进行比较不同。最好放弃partial,或者尝试使用和不使用两种方式。 - Kelly Bundy
1
他们的真正意图很明显:找到给定能力的结果的最快方法。所以,最好尝试两种方法。 - Kelly Bundy
我不认为我在比较苹果和橙子,因为我所寻找的只是计算十的幂的最快方法,无论结果是浮点数还是整数。我不确定你所说的“10 ** n”计算一个整数是什么意思,因为例如“10 ** -8”是完全可能的。另一方面,对于f-string,我必须使用“float(f'1e{power}')”,因为“int(f'1e{power}')”显然对于负幂不起作用。 - mapf
关于整数和浮点数,我写了“(当n为非负数时)” - kaya3
1
@mapf 是的,它会检查指数是否为负数,并在这种情况下使用不同的浮点幂函数;源代码链接 - kaya3
显示剩余9条评论

2

另一个解决浮点数情况的方法是,预先计算所有可能的非零有限结果并查找它们:

0.0 if n < -323 else f[n] if n < 309 else inf

准备工作:
f = [10.0 ** i for i in [*range(309), *range(-323, 0)]]
inf = float('inf')

使用 kaya3 的指数 n = 200 以及 n = -200 作为负指数,同时得到非零结果,并使用 n = -5000 / n = 5000 作为原始范围内的中等大小的负/正指数进行基准测试:

n = 200
487 ns  487 ns  488 ns  float(f'1e{n}')
108 ns  108 ns  108 ns  10.0 ** n
128 ns  129 ns  130 ns  10.0 ** n if n < 309 else inf
 72 ns   73 ns   73 ns  0.0 if n < -323 else f[n] if n < 309 else inf

n = -200
542 ns  544 ns  545 ns  float(f'1e{n}')
109 ns  109 ns  110 ns  10.0 ** n
130 ns  130 ns  131 ns  10.0 ** n if n < 309 else inf
 76 ns   76 ns   76 ns  0.0 if n < -323 else f[n] if n < 309 else inf

n = -5000
291 ns  291 ns  291 ns  float(f'1e{n}')
 99 ns   99 ns  100 ns  10.0 ** n
119 ns  120 ns  120 ns  10.0 ** n if n < 309 else inf
 34 ns   34 ns   34 ns  0.0 if n < -323 else f[n] if n < 309 else inf

n = 5000
292 ns  293 ns  293 ns  float(f'1e{n}')
 error   error   error  10.0 ** n
 33 ns   33 ns   33 ns  10.0 ** n if n < 309 else inf
 53 ns   53 ns   53 ns  0.0 if n < -323 else f[n] if n < 309 else inf

Benchmark 代码 (在线尝试!):
from timeit import repeat

solutions = [
    "float(f'1e{n}')",
    '10.0 ** n',
    '10.0 ** n if n < 309 else inf',
    '0.0 if n < -323 else f[n] if n < 309 else inf',
]

for n in 200, -200, -5000, 5000:
    print(f'{n = }')
    setup = f'''
n = {n}
f = [10.0 ** i for i in [*range(309), *range(-323, 0)]]
inf = float('inf')
'''
    for solution in solutions:
        try:
            ts = sorted(repeat(solution, setup))[:3]
        except OverflowError:
            ts = [None] * 3
        print(*('%3d ns ' % (t * 1e3) if t else ' error ' for t in ts), solution)
    print()

太好了,非常感谢你写出你的建议!这是一个非常巧妙的想法。虽然它仍然基于10.0 ** i,但我认为在所有情况下接受kaya3的答案仍然是公平的。 - mapf
@mapf 嗯,我可以基于任何解决方案,因为我无论如何都不考虑准备 :-). 但是,接受您最喜欢的答案,kaya3的答案肯定很好,像往常一样。 - Kelly Bundy
嗨,我又想了一下这个问题,并意识到使用 dict 进行查找应该会更快,但是,事实并非如此。你知道为什么吗? - mapf
@mapf现在你知道为什么我使用了稍微棘手的两个范围的列表构建,而不是更简单的字典 :-)(我想我曾经考虑过字典,但因为它较慢而被忽略)。正如那里的答案所说,你把查找和搜索混淆了(即,混淆了访问和搜索)。 - Kelly Bundy
是的,谢谢!我没有意识到“lookup”这个术语是有歧义的,我觉得应该更好地澄清一下。无论如何,我的问题因某种原因被投票降低了,所以我可能会删除它... - mapf

0
你可以尝试使用 math.log 和 math.exp 的对数方法,但值的范围将受到限制(可以使用 try/except 处理)。
这种方法似乎与 fstring 一样快,甚至更快一些。
import math
ln10 = math.log(10)
def mPow(power):
    try:
        return math.exp(ln10*power)
    except:
        return 0 if power<0 else math.inf

[编辑] 鉴于我们受到浮点数能力的限制,我们最好准备一个包含617个可能的10的幂次方(可以保存在浮点数中)的列表,并通过索引获取答案:

import math
minP10,maxP10 = -308,308
powersOf10 = [10**i for i in range(maxP10+1)]+[10**i for i in range(minP10,0)]
def tenPower(power):
    if power < minP10: return 0
    if power > maxP10: return math.inf
    return powersOf10[power]    # negative indexes for powers -308...-1

这个肯定比 fstring 更快


有趣的想法!难道没有办法让它也适用于负幂吗? - mapf
它应该也能处理负数幂(至少在我的测试中是这样的) - Alain T.
啊,好的。我的错。我以为如果功率小于0,它总是会失败。太棒了!我运行了我的(不完美的)速度测试,看起来它确实非常快。从我的数字来判断,甚至是最快的。 - mapf
通过对数和指数进行计算会引入一些不精确性。请参见我的最后一个[EDIT],其中提供了一种更精确且更快的解决方案。 - Alain T.
嘿,我明白了!使用查找列表非常聪明。这也是凯利·邦迪提出的建议。 - mapf
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接