4

我正在比较这个 F# 函数的性能:

let e28 N =                               
  seq {for i in 2L..2L..N do for j in 1..4 -> i} |> Seq.scan (+) 1L |> Seq.sum  

使用 Python 3.3 等效项:

def e28a(N = 100000):
    diagNumber = 1                             
    sum        = diagNumber                
    for width in range(2, N+1, 2):
        for j in range(4):          
            diagNumber += width             
            sum        += diagNumber            
    return sum

import itertools as it
def e28b(N = 100000):
    return sum(it.accumulate(it.chain([1], (i for i in range(2, N+1, 2) for j in range(4)))))    

import numpy as np
def e28c(N = 100000):
    return np.sum(np.cumsum(np.fromiter(chain([1], (i for i in range(2, N+1, 2) for j in range(4))), np.int64)))

我在 Windows 7 上获得的 64 位 CPython 3.3.1 性能比 C++ 慢大约 574 倍。以下是 N = 100000 的时间:

e28:23毫秒;e28a:48.4ms;e28b:49.7ms;e28c:40.2ms;C++版本:0.07ms

在不改变底层算法的情况下优化 Python 代码是否容易实现?

4

3 回答 3

4

e28aF# 版本可以通过切换到程序化的、可变的方法(如您的 python )来加速约 10 倍。当“有效负载操作”(在这种情况下,只是 +)如此微不足道时,组合器的使用最终会增加相对显着的开销。作为旁注,Seq.sum使用检查算术,这也增加了一些开销。

F# 的优点之一是,如果需要性能关键的热路径,您可以回退到过程/可变样式。

let e28_original N =
  seq {
    for i in 2UL..2UL..N do 
        for j in 1..4 do
            yield i
  }
  |> Seq.scan (+) 1UL
  |> Seq.sum

let e28_mutable N = 
  let mutable sum = 1UL
  let mutable total = sum                            
  for i in 2UL..2UL..N do 
      for j in 1..4 do
         sum <- sum + i
         total <- total + sum
  total

let time f =
    f () |> ignore // allow for warmup / JIT
    let sw = System.Diagnostics.Stopwatch.StartNew()
    let result = f ()
    sw.Stop()
    printfn "Result: %A Elapsed: %A" result sw.Elapsed

time (fun _ -> e28_original 100000UL)
time (fun _ -> e28_mutable 100000UL)

结果

Result: 666691667100001UL Elapsed: 00:00:00.0429414
Result: 666691667100001UL Elapsed: 00:00:00.0034971
于 2013-07-05T20:33:44.880 回答
3

使用你的 F# 版本,我得到:

> e28(100000L);;
Real: 00:00:00.061, CPU: 00:00:00.062, GC gen0: 2, gen1: 0, gen2: 0
val it : int64 = 666691667100001L

使用:

let e28d N =
    seq {2L..2L..N}
    |> Seq.collect(fun x->seq{yield x;yield x; yield x; yield x})
    |> Seq.scan (+) 1L
    |> Seq.sum

我有:

> e28d(100000L);;
Real: 00:00:00.040, CPU: 00:00:00.031, GC gen0: 2, gen1: 0, gen2: 0
val it : int64 = 666691667100001L

仅仅因为编译了 F# 并解释了 Python,您可能很难让 python 的性能与 F# 一样好。话虽如此,上述改进也适用于 python:

>>> def e28a(N = 100000):
    diagNumber = 1;                            
    sum        = diagNumber;                   
    for width in range(2, N+1, 2):
        for j in range(4):          
            diagNumber += width;                
            sum        += diagNumber;           
    return sum;

>>> if __name__ == '__main__':
    import timeit
    print(timeit.timeit("e28a()", setup="from __main__ import e28a", number=10))


0.5249497228663813
>>> def e28a(N = 100000):
    diagNumber = 1;
    sum        = diagNumber;
    for width in range(2, N+1, 2):
        diagNumber += width;
        sum        += diagNumber;
        diagNumber += width;
        sum        += diagNumber;
        diagNumber += width;
        sum        += diagNumber;
        diagNumber += width;
        sum        += diagNumber;
    return sum;

>>> if __name__ == '__main__':
    import timeit
    print(timeit.timeit("e28a()", setup="from __main__ import e28a", number=10))


0.2585966329330063
>>> 

这种改进的一部分来自于更少的函数调用,即:

>>> def e28a(N = 100000):
    diagNumber = 1;                            
    sum        = diagNumber;
    temp_range = range(4)             #Change here
    for width in range(2, N+1, 2):
        for j in temp_range:          #Change here
            diagNumber += width;                
            sum        += diagNumber;           
    return sum;

>>> if __name__ == '__main__':
    import timeit
    print(timeit.timeit("e28a()", setup="from __main__ import e28a", number=10))


0.40251470339956086
>>> 

我认为另一部分来自删除循环。在 Python 中,这两者都可能相当昂贵。

于 2013-07-05T18:26:40.480 回答
1

这几乎是我机器上的两倍。它使用记忆化,以及基本的算术演绎。

您必须定义一个全局变量。

summi=2

def e28d(N = 100000):
    def memo(width):
        global summi
        summi+=width*4+4
        return summi-width*2+2
    x= sum((memo(width*4)) for width in range (2, N+1, 2))+1
    return x 

结果:
e28a:

0.0591201782227 秒

e28d:

0.0349650382996 秒

希望它至少是建设性的。注意:您必须根据数字是否为奇数来调整它。

更新: 通过完全避免循环,这是一个在 python 中运行大约快一百倍的函数(N=100000 大约为 0.5 毫秒):

import math
def e28e(X = 100000):
    keyint, keybool=int(X/6), X%6
    if keybool/2==0: keyvar=(16*keyint+sum(range(keyint))*12)
    elif keybool/2==1: keyvar=(44*keyint+sum(range(keyint))*36+7) 
    else: keyvar=(28*(keyint+1)+sum(range(keyint+1))*60-2)
    X-=keybool%2
    diag= math.pow(X,2)+2*X+1
    newvar=keyint+int(X/2)+1
    summ= int(diag*newvar+keyvar)
    return summ
于 2013-07-05T13:52:02.337 回答