Performance 仅当分母非零时计算分数的有效方法

Performance 仅当分母非零时计算分数的有效方法,performance,python-3.6,micro-optimization,Performance,Python 3.6,Micro Optimization,对于机器学习应用程序,我需要根据更新但初始化为0的值对列表进行排序。问题是,最初更新的值0的一部分被用作排序键的公式使用,其中一个在分母中,因此在第一次迭代时会抛出DivisionByZero错误 由于将对每个元素多次检查0,因此我希望在保持合理可读性的同时,非常有效地编写公式的分母。到目前为止,我提出了4个版本: import random import datetime def generate_value_pair(): # in my case about 10% of th

对于机器学习应用程序,我需要根据更新但初始化为0的值对列表进行排序。问题是,最初更新的值0的一部分被用作排序键的公式使用,其中一个在分母中,因此在第一次迭代时会抛出DivisionByZero错误

由于将对每个元素多次检查0,因此我希望在保持合理可读性的同时,非常有效地编写公式的分母。到目前为止,我提出了4个版本:

import random
import datetime


def generate_value_pair():
    # in my case about 10% of the time b == 0
    return 10**6 * random.random(), \
        10**6 * random.random() * (random.random() > 0.1)


def f0(a, b):
    if b != 0:
        return a/b
    else:
        return 0


def f1(a, b):
    return (not b or (a/b)+1)-1


def f2(a, b):
    return b and a/b


def f3(a, b):
    try:
        return a/b
    except:
        return 0


def compare(func):
    n = datetime.datetime.now()
    for _ in range(10**8):
        func(*generate_value_pair())

    print(datetime.datetime.now() - n)


fs = [f0, f1, f2, f3]

# sanity check for new versions
# not using assert because of floating point precision at 2/7
for a, b in zip(list(range(10)), reversed(list(range(10)))):
    t = (a, b)
    for f in fs:
        print(float(f(*t)), end='\t')
    print()

for f in fs:
    compare(f)

本试验的结果如下:

0:00:36.163209
0:00:38.947623
0:00:35.436445
0:00:35.450830
毫无疑问,
f2
,这是操作量最少且分支最快的函数。令我大吃一惊的是,
f3
和try/except并列第二,naive if/else
f0
并列第三,而我最初尝试改进naive版本的
f1
则排在最后

try/except比我想象的要快得多,所以我做了另一个测试,其中50%的情况
b==0

0:00:36.998776
0:00:37.043782
0:00:35.061485
0:00:41.943822
所以慢的是抛出异常和随后发生的事情,但是如果您的案例很少发生,那么try/except可以非常接近“最优”,如上所示

我的问题是:有没有更快的方法

有没有更快的办法

浮点格式存在严重的精度损失问题;但是如果你想变得狡猾,你可以滥用精度损失问题

具体而言;如果在几乎所有的浮点数上加上“最小”的非零值,由于精度损失,它将不起任何作用;但是如果你将相同的值加在零上,那么结果将是“最小的”非零值(并且可以安全地使用除数,因为技术上它不是零)

换言之:

def f3(a, b):
    return a/(b + math.nextafter(0.0, math.inf)

但是,;只有当
b
永远不是一个极小的负数时(例如,如果
b
等于
-math.nextafter(0.0,math.inf)
,则将非零变为零)。如果这可能是一个问题(如果除数可能是负数),你可能会通过对分子和除数求反来作弊。

大概你会想在下一次之后手动将
扩展为科学记数法中的数字文字,否则像CPython这样的解释器会对每次调用都进行求值。(然后您可以显式地选择是选择最小的正常值(DBL_MIN),还是最小的次正常值(
std::numeric_limits::denorm_MIN
)。一些现代硬件在将次正常值添加到正常值时没有此功能,但较旧的CPU确实会为此采取微码异常。例如,Nehalem和更早版本。)@PeterCordes所说的证实了这一点,在600万次运行中,你的建议比我的f2慢0.095秒(我的机器现在正在进行数字运算,所以我运行的次数较少,但大约慢0.1秒可能就足够了。今天晚些时候将运行另一个测试)。有趣的解决方案,我甚至没有想到!math.nextafter也出现在Python3.9中,但我标记了Python3.6。@jaaq:您是否尝试手动使用类似于
2.225073858507201383090232717332404064421921598046318306E-308
?在Python中,解析后它应该是一个常量。(引用规范化和非规范化的双倍值,精度过高,这样可以告诉您是因为调用nextafter而变慢,还是因为非规范化而变慢,或者两者兼而有之。)