【问题标题】:Make Python group operations进行 Python 分组操作
【发布时间】:2021-02-16 11:00:48
【问题描述】:
def f1(x):
    for i in range(1, 100):
        x *= 2
        x /= 3.14159
        x *= i**.25
    return x

def f2(x):
    for i in range(1, 100):
        x *= 2 / 3.14159 * i**.25
    return x

两个函数的计算完全相同,但 f1 的计算时间要长 3 倍,即使是 @numba.njit。是否可以让 Python 识别编译中的等价性,就像它以其他方式优化 dis 一样,例如丢弃未使用的作业?

注意,我知道浮点运算关心顺序,因此这两个函数的输出可能略有不同,但如果有任何更多对数组值的单独编辑更少准确,所以这将是一个二合一优化。


x = np.random.randn(10000, 1000)
%timeit f1(x.copy())        # 2.68 s ± 50.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit f2(x.copy())        # 894 ms ± 36.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit njit(f1)(x.copy())  # 2.59 s ± 65.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit njit(f2)(x.copy())  # 901 ms ± 41.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

【问题讨论】:

  • 这两个函数本质上是相同的,但是一个涉及三个向量运算,一个涉及一个。
  • @PranavHosangadi 我知道原因。

标签: python python-3.x compiler-optimization numba


【解决方案1】:

使用numba.jit 可能是您目前对此类功能的最佳优化。您可能还想尝试pypy 并进行一些基准比较。

尽管如此,我想指出为什么这两个函数等效,所以你不应该期望f1 被简化为f2

f1的操作顺序如下:

x1 = (x * 2)            # First binary operation
x2 = (x1 / 3.14159      # Second binary operation
x3 = x2 * (i ** 0.25)   # Third and fourth binary operation

# Order: Multiplication, division, exponent, multiplication

这与f2不同:

x *= ((2 / 3.14159) * (i ** 0.25))
#  ^     ^          ^     ^
#  |     |          |     |
#  4     1          3     2

# Order: Division, exponent, multiplication, multiplication

由于floating-point arithmetic is not associative,这些可能不会产生相同的结果。出于这个原因,除非是为了优化浮点精度,否则编译器或解释器做你期望的优化是错误的。

我不知道有什么 Python 工具可以进行这种特定类型的优化。

【讨论】:

  • 我了解关联性,但如果对数组值的任何更多修改更少准确,那么它将是一个二进式-一次优化。
  • @OverLordGoldDragon 我不确定您所说的对数组值的修改不太准确是什么意思
  • 例如,舍入误差会累积在重复例如加法,而一次全部加法只涉及一次舍入。机器学习中的典型问题。
  • @OverLordGoldDragon 我明白了。然后回到你的问题:如果你将 f2 标为更快,为什么不直接使用它而不是 f1?这不是我认为你应该期望 Python 编译器在今天做的那种优化......希望将来我们会找到一些方法。
  • @OverLordGoldDragon 我明白了。在这种情况下,问题甚至更严重,因为 psihfn 可以动态分配另一个函数。这使得 Python 中的静态分析非常复杂。
【解决方案2】:

使用 jit 可能无法做到这一点。我已经尝试过 api 中指定的 fastmath 和 nogil kwarg:https://numba.pydata.org/numba-doc/latest/reference/jit-compilation.html

f0 在摆脱溢出或非正常数字后仍然比f1 稍慢。 plot

from timeit import default_timer as timer
import numpy as np
import matplotlib.pyplot as plt
import numba as nb


def f0(x):
    for i in range(1, 1000):
        x *= 3.000001
        x /= 3
    return x


def f1(x):
    for i in range(1, 1000):
        x *= 3.000001 / 3
    return x


def timing(f, **kwarg):
    x = np.ones(1000, dtype=np.float32)
    times = []
    n_iter = list(range(100, 1000, 100))
    f2 = nb.njit(f, **kwarg)
    for i in n_iter:
        print(i)
        s = timer()
        for j in range(i):
            f2(x)
        e = timer()
        times.append(e - s)
    print(x)
    m, b = np.polyfit(n_iter, times, 1)
    return times, m, b, n_iter


def main():
    results = []
    for fastmath in [True, False]:
        for i, f in enumerate([f0, f1]):
            kwarg = {
                "fastmath": fastmath,
                "nogil": True
            }
            r1, m, b, n_iter = timing(f, **kwarg)
            label = "f%d with %s" % (i, kwarg)
            plt.plot(n_iter, r1, label=label)
            results.append((m, b, label))
    for m, b, kwarg in results:
        print(m * 1e5, b, kwarg)
    plt.legend(loc="upper left")
    plt.xlabel("n iterations")
    plt.ylabel("timing")
    plt.show()
    plt.close()


if __name__ == '__main__':
    main()

【讨论】:

  • 你没有使用大数组。
猜你喜欢
  • 2012-02-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-10-27
  • 1970-01-01
相关资源
最近更新 更多