在Python中实现Horner方法的问题

时间:2015-01-31 10:05:27

标签: python function loops iteration polynomial-math

所以我用三种不同的方法写下了用于评估多项式的​​代码。霍纳的方法应该是最快的,而天真的方法应该是最慢的,对吗?但是,为什么计算它的时间不是我所期望的呢?对于itera和naive方法,计算时间有时会变得完全相同。怎么了?

import numpy.random as npr
import time

def Horner(c,x):
    p=0
    for i in c[-1::-1]:
        p = p*x+i
    return p

def naive(c,x):
    n = len(c)
    p = 0
    for i in range(len(c)):
        p += c[i]*x**i 
    return p

def itera(c,x):
    p = 0
    xi = 1
    for i in range(len(c)):
        p += c[i]*xi
        xi *= x 
    return p

c=npr.uniform(size=(500,1))
x=-1.34

start_time=time.time()
print Horner(c,x)
print time.time()-start_time

start_time=time.time()
print itera(c,x)
print time.time()-start_time

start_time=time.time()
print naive(c,x)
print time.time()-start_time

以下是一些结果:

[  2.58646959e+69]
0.00699996948242
[  2.58646959e+69]
0.00600004196167
[  2.58646959e+69]
0.00600004196167

[ -3.30717922e+69]
0.00899982452393
[ -3.30717922e+69]
0.00600004196167
[ -3.30717922e+69]
0.00600004196167

[ -2.83469309e+69]
0.00999999046326
[ -2.83469309e+69]
0.00999999046326
[ -2.83469309e+69]
0.0120000839233

3 个答案:

答案 0 :(得分:16)

您的分析可以大大改进。另外,我们可以让您的代码运行速度提高200-500倍。


(1)冲洗并重复

由于两个原因,您无法仅运行一次性能测试迭代。

  1. 您的时间分辨率可能不够好。这就是为什么你有时会为两个实现获得相同的时间:一次运行的时间接近你的计时机制的分辨率,所以你只记录了一个“tick”。
  2. 各种影响效果的因素。对于有意义的比较而言,最​​好的选择是进行大量的迭代。
  3. 你不需要大量的跑步(当然,这并不会造成伤害),但是你估计并调整迭代次数,直到方差在你的目的可以接受的水平之内。

    timeit是一个很好的小模块,用于分析Python代码。

    我将此添加到您的脚本底部。

    import timeit
    
    n = 1000
    
    print 'Horner', timeit.timeit(
        number = n,
        setup='from __main__ import Horner, c, x',
        stmt='Horner(c,x)'
    )
    print 'naive', timeit.timeit(
        number = n,
        setup='from __main__ import naive, c, x',
        stmt='naive(c,x)', 
    )
    print 'itera', timeit.timeit(
        number = n,
        setup='from __main__ import itera, c, x',
        stmt='itera(c,x)', 
    )
    

    哪个产生

    Horner 1.8656351566314697
    naive 2.2408010959625244
    itera 1.9751169681549072
    

    霍纳是最快的,但它并没有完全打破其他两个人。


    (2)看看发生了什么......非常仔细

    Python有运算符重载,所以很容易错过这个。

    npr.uniform(size=(500,1))给你一个500 x 1 numpy的随机数结构。

    那又怎样?

    嗯,c[i]不是数字。 这是一个带有一个元素的numpy数组。 Numpy会重载运算符,所以你可以做一些事情,比如用数组乘以数组。

    没关系,但是为每个元素使用一个数组很多的开销,所以很难看出算法之间的区别。

    相反,让我们尝试一个简单的Python列表:

    import random
    c = [random.random() for _ in range(500)]
    

    现在,

    Horner 0.034661054611206055
    naive 0.12771987915039062
    itera 0.07331395149230957
    

    哇! 所有时间加快(10-60x)。按比例,霍纳的实施比其他两个更快。我们删除了所有三个的开销,现在可以看到“裸骨”的差异。

    Horner比天真快4倍,比itera快2倍。


    (3)备用运行时

    你正在使用Python 2.我假设是2.7。

    让我们看看Python 3.4如何运作。 (语法调整:您需要在参数列表周围添加括号print。)

    Horner 0.03298933599944576
    naive 0.13706714100044337
    itera 0.06771054599812487
    

    大致相同。

    让我们试试PyPy,一个Python的JIT实现。 (“普通”Python实现称为CPython。)

    Horner 0.006507158279418945
    naive 0.07541298866271973
    itera 0.005059003829956055
    

    尼斯!现在每个实现的运行速度提高了2-5倍。霍纳现在的速度是天真的10倍,但比itera略慢。

    JIT运行时比解释器更难以分析。让我们将迭代次数增加到50000,并尝试确保。

    Horner 0.12749004364013672
    naive 3.2823100090026855
    itera 0.06546688079833984
    

    (请注意,我们有50倍的迭代次数,但只有20倍的时间...... JIT在前1000次运行中的许多次都没有完全生效。)相同的结论,但差异更加明显。

    当然,JIT的想法是在运行时分析,分析和重写程序,所以如果你的目标是比较算法,这将增加许多非显而易见的实现细节。

    尽管如此,比较运行时可能有助于提供更广阔的视角。


    还有一些事情。例如,您的天真实现计算它从未使用过的变量。您使用range代替xrange。您可以尝试使用索引而不是反向切片向后迭代。等

    这些都没有改变我的结果,但是值得考虑。

答案 1 :(得分:6)

通过测量这样的事情,你无法获得准确的结果:

start_time=time.time()
print Horner(c,x)
print time.time()-start_time

据推测,大部分时间都花费在print函数所涉及的IO函数上。此外,为了获得重要的东西,您应该在大量迭代中执行度量以平滑错误。在一般情况下,您可能还希望对各种输入数据执行测试 - 根据您的算法,某些情况可能会巧合地解决,而不是其他情况。

您应该明确地查看timeit模块。这样的事情,也许:

import timeit
print 'Horner',timeit.timeit(stmt='Horner(c,x)', 
                  setup='from __main__ import Horner, c, x',
                  number = 10000)
#                          ^^^^^
#                probably not enough. Increase that once you will
#                be confident

print 'naive',timeit.timeit(stmt='naive(c,x)', 
                  setup='from __main__ import naive, c, x',
                  number = 10000)
print 'itera',timeit.timeit(stmt='itera(c,x)', 
                  setup='from __main__ import itera, c, x',
                  number = 10000)

在我的系统上制作:

Horner 23.3317809105
naive 28.305519104
itera 24.385917902

但是仍然有来自另一个的可变结果:

Horner 21.1151690483
naive 23.4374330044
itera 21.305426836

正如我之前所说,要获得更有意义的结果,您应该明确增加测试次数,并在几个测试用例中运行,以便平滑结果。

答案 2 :(得分:3)

如果您正在进行大量的基准测试,科学计算,与numpy相关的工作以及使用ipython的更多内容将是一个非常有用的工具。

要进行基准测试,您可以使用ipython magic计算代码的时间,每次运行时您将获得更一致的结果,这只是使用timeit然后使用函数或代码来计算时间:

In [28]: timeit Horner(c,x)
1000 loops, best of 3: 670 µs per loop

In [29]: timeit naive(c,x)
1000 loops, best of 3: 983 µs per loop

In [30]: timeit itera(c,x)
1000 loops, best of 3: 804 µs per loop

要跨越多行的时间码,您只需使用%%timeit

In [35]: %%timeit
   ....: for i in range(100):
   ....:     i ** i
   ....: 
10000 loops, best of 3: 110 µs per loop

ipython可以compile cython codef2py代码,并使用不同的插件和ipython魔术命令执行许多其他非常有用的任务。

builtin magic commands

使用cython和一些非常基本的改进,我们可以将Horner的效率提高约25%:

In [166]: %%cython
import numpy as np
cimport numpy as np
cimport cython
ctypedef np.float_t DTYPE_t
def C_Horner(c, DTYPE_t x):
    cdef DTYPE_t p
    for i in reversed(c):
        p = p * x + i
    return p   

In [28]: c=npr.uniform(size=(2000,1))

In [29]: timeit Horner(c,-1.34)
100 loops, best of 3: 3.93 ms per loop
In [30]: timeit C_Horner(c,-1.34)
100 loops, best of 3: 2.21 ms per loop

In [31]: timeit itera(c,x)
100 loops, best of 3: 4.10 ms per loop
In [32]: timeit naive(c,x)
100 loops, best of 3: 4.95 ms per loop

使用@Paul drapers中的列表回答我们的cythonised版本运行速度是原始函数的两倍,比ietra和naive快得多:

In [214]: import random

In [215]: c = [random.random() for _ in range(500)]

In [44]: timeit C_Horner(c, -1.34)
10000 loops, best of 3: 18.9 µs per loop    
In [45]: timeit Horner(c, -1.34)
10000 loops, best of 3: 44.6 µs per loop
In [46]: timeit naive(c, -1.34)
10000 loops, best of 3: 167 µs per loop
In [47]: timeit itera(c,-1.34)
10000 loops, best of 3: 75.8 µs per loop