【问题标题】:How to optimize multiple calls to library function (e.g., random.random)?如何优化对库函数的多次调用(例如,random.random)?
【发布时间】:2014-11-28 23:57:27
【问题描述】:

假设我想使用基于球体体积的蒙特卡罗模拟来估计 π(代码如下)。

有没有办法优化对random.random()的调用?

$ cat pi_estimate.py
#!/usr/bin/env python

"""
The task:
    Consider the part of the sphere x^2 + y^2 + z^2 <= 1 for which x,y,z > 0

The calculation:
    We generate n (e.g., 10,000) points (x,y,z) with 0 <= x,y,z <= 1 and using
    the formula for sphere volume: V = (4/3) * PI * r^3, we'll estimate PI.

    The cube in which the sphare resides has a volume: 8 * r^3

    Now, if we only consider the quardant where 0 <= x,y,z <= 1, then it's 
    only 1/8th of the total volumes, namely, Vs = (1/6) * PI * r^3, and 
    Vc = r^3
    As r == 1, Vs = (1/6) * PI, and Vc = 1
    Vs/Vc = (1/6) * PI
    Thus PI = 6 * Vs/Vc

    So, every point (x,y,z) with 0 <= x,y,z <= 1 which is x^2 + y^2 + z^2 <= 1 
    is added to Vs (and Vc), and if not than it is only in Vc.
"""

import random

N = 1000000
Vs_counter = 0
for i in xrange(N):
    x = random.random() 
    y = random.random()
    z = random.random()
    if (x**2 + y**2 + z**2) <= 1:
        Vs_counter += 1

pi = 6 * (1.0 * Vs_counter / N)
print "PI is estimated",pi

以下分析确实表明该脚本大部分时间都在random.random()

$ python -m cProfile pi_estimate.py
PI is estimated 3.142194
         3000049 function calls in 3.856 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.000    0.000 __future__.py:48(<module>)
        1    0.000    0.000    0.000    0.000 __future__.py:74(_Feature)
        7    0.000    0.000    0.000    0.000 __future__.py:75(__init__)
        1    0.007    0.007    0.007    0.007 hashlib.py:55(<module>)
        6    0.000    0.000    0.000    0.000 hashlib.py:94(__get_openssl_constructor)
        1    2.822    2.822    3.856    3.856 pi_estimate.py:22(<module>)
        1    0.000    0.000    0.003    0.003 random.py:100(seed)
        1    0.030    0.030    0.040    0.040 random.py:40(<module>)
        1    0.000    0.000    0.000    0.000 random.py:655(WichmannHill)
        1    0.000    0.000    0.000    0.000 random.py:72(Random)
        1    0.000    0.000    0.000    0.000 random.py:805(SystemRandom)
        1    0.000    0.000    0.003    0.003 random.py:91(__init__)
        1    0.000    0.000    0.000    0.000 {_hashlib.openssl_md5}
        1    0.000    0.000    0.000    0.000 {_hashlib.openssl_sha1}
        1    0.000    0.000    0.000    0.000 {_hashlib.openssl_sha224}
        1    0.000    0.000    0.000    0.000 {_hashlib.openssl_sha256}
        1    0.000    0.000    0.000    0.000 {_hashlib.openssl_sha384}
        1    0.000    0.000    0.000    0.000 {_hashlib.openssl_sha512}
        1    0.000    0.000    0.000    0.000 {binascii.hexlify}
        1    0.001    0.001    0.001    0.001 {function seed at 0xffe31e2c}
        6    0.000    0.000    0.000    0.000 {getattr}
        6    0.000    0.000    0.000    0.000 {globals}
        1    0.000    0.000    0.000    0.000 {math.exp}
        2    0.000    0.000    0.000    0.000 {math.log}
        1    0.000    0.000    0.000    0.000 {math.sqrt}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
  3000000    0.994    0.000    0.994    0.000 {method 'random' of '_random.Random' objects}
        1    0.002    0.002    0.002    0.002 {posix.urandom}

编辑(2014 年 11 月 27 日星期四 20:45:26)

由于 @ch3ka 检查了他的代码并在使用库函数的本地名称时展示了改进,我决定使用 my 代码进行检查。
在我的代码中,我无法确定任何改进:

$ for i in {1..9} ; do for script in  pi_estimate.py  pi_estimate_local.py  ; do echo $script; python -m cProfile $script | grep 3000000; done; done
pi_estimate.py
  3000000    0.360    0.000    0.360    0.000 {method 'random' of '_random.Random' objects}
pi_estimate_local.py
  3000000    0.341    0.000    0.341    0.000 {method 'random' of '_random.Random' objects}
pi_estimate.py
  3000000    0.326    0.000    0.326    0.000 {method 'random' of '_random.Random' objects}
pi_estimate_local.py
  3000000    0.337    0.000    0.337    0.000 {method 'random' of '_random.Random' objects}
pi_estimate.py
  3000000    0.331    0.000    0.331    0.000 {method 'random' of '_random.Random' objects}
pi_estimate_local.py
  3000000    0.317    0.000    0.317    0.000 {method 'random' of '_random.Random' objects}
pi_estimate.py
  3000000    0.327    0.000    0.327    0.000 {method 'random' of '_random.Random' objects}
pi_estimate_local.py
  3000000    0.316    0.000    0.316    0.000 {method 'random' of '_random.Random' objects}
pi_estimate.py
  3000000    0.354    0.000    0.354    0.000 {method 'random' of '_random.Random' objects}
pi_estimate_local.py
  3000000    0.325    0.000    0.325    0.000 {method 'random' of '_random.Random' objects}
pi_estimate.py
  3000000    0.326    0.000    0.326    0.000 {method 'random' of '_random.Random' objects}
pi_estimate_local.py
  3000000    0.341    0.000    0.341    0.000 {method 'random' of '_random.Random' objects}
pi_estimate.py
  3000000    0.349    0.000    0.349    0.000 {method 'random' of '_random.Random' objects}
pi_estimate_local.py
  3000000    0.324    0.000    0.324    0.000 {method 'random' of '_random.Random' objects}
pi_estimate.py
  3000000    0.326    0.000    0.326    0.000 {method 'random' of '_random.Random' objects}
pi_estimate_local.py
  3000000    0.315    0.000    0.315    0.000 {method 'random' of '_random.Random' objects}
pi_estimate.py
  3000000    0.358    0.000    0.358    0.000 {method 'random' of '_random.Random' objects}
pi_estimate_local.py
  3000000    0.324    0.000    0.324    0.000 {method 'random' of '_random.Random' objects}

这是两个脚本(我编辑了文档字符串):

$ cat pi_estimate.py | tail -14

import random

N = 1000000
Vs_counter = 0
for i in xrange(N):
    x = random.random() 
    y = random.random()
    z = random.random()
    if (x**2 + y**2 + z**2) <= 1:
        Vs_counter += 1

pi = 6 * (1.0 * Vs_counter / N)
print "PI is estimated",pi

$ cat pi_estimate_local.py | tail -14

from random import random as rnd

N = 1000000
Vs_counter = 0
for i in xrange(N):
    x = rnd()
    y = rnd()
    z = rnd()
    if (x**2 + y**2 + z**2) <= 1:
        Vs_counter += 1

pi = 6 * (1.0 * Vs_counter / N)
print "PI is estimated",pi

编辑(2014 年 11 月 28 日星期五 17:50:09)

在@ch3ka 最后三个 cmets 之后,我计算了对 random() 的 300k 次调用,正如@ch3ka 指出的那样,Python 分析器确实呈现出误导性的画面。使用本地引用调用 random 确实可以节省时间,即:

$ for i in {1..9}; do python pi_estimate.py ; done
PI is estimated 3.143076  -> execution time: 2.62900018692
PI is estimated 3.143346  -> execution time: 2.58100008965
PI is estimated 3.140286  -> execution time: 2.52200007439
PI is estimated 3.145734  -> execution time: 2.5110001564
PI is estimated 3.140898  -> execution time: 2.51300001144
PI is estimated 3.143058  -> execution time: 2.59200000763
PI is estimated 3.139332  -> execution time: 2.60400009155
PI is estimated 3.142956  -> execution time: 2.47399997711
PI is estimated 3.144552  -> execution time: 2.50100016594

$ for i in {1..9}; do python pi_estimate_local.py ; done
PI is estimated 3.146772  -> execution time: 2.22300004959
PI is estimated 3.142038  -> execution time: 2.18499994278
PI is estimated 3.139032  -> execution time: 2.14800000191
PI is estimated 3.14052  -> execution time: 2.20199990273
PI is estimated 3.141384  -> execution time: 2.25199985504
PI is estimated 3.142086  -> execution time: 2.25200009346
PI is estimated 3.137748  -> execution time: 2.18099999428
PI is estimated 3.141906  -> execution time: 2.40199995041
PI is estimated 3.138126  -> execution time: 2.16100001335

【问题讨论】:

    标签: python python-2.7 optimization random cygwin


    【解决方案1】:

    其实,有一种优化技术可以使用——局部别名。

    考虑

    import random
    import timeit
    
    try: xrange # py3 compatibility
    except NameError: xrange = range
    
    def f1():
        return sum((random.random() for _ in xrange(10**5)))
    
    def f2():
        rand = random.random # bind random.random to local var
        myrange = xrange # same for range gen (kinda pointless here, but to illustrate that you can do this with everything)
        return sum((rand() for _ in myrange(10**5)))
    
    print(timeit.timeit(f1, number=100))
    print(timeit.timeit(f2, number=100))
    

    f1f2 做同样的事情,对吧?除了f2 具有在本地命名空间中查找范围和rand 函数的优势,其中f1 必须在模块命名空间中查找 - 并且 必须在.random 上进行属性查找。

    所以理论上,如果 python 在内部没有针对这种情况进行优化,我们应该在这里看到一个优势。事实上,我们甚至可以在 py3 上:

    ch3ka@x200 /tmp % python2 aliastest.py
    1.88513803482
    1.4380030632
    ch3ka@x200 /tmp % python3 aliastest.py
    2.096395079046488
    1.6709147160872817
    

    因此,使用这种技术,您也许可以加快您的程序 - 但话又说回来,这取决于实现细节。

    请注意,您也可以将f2 写为:

    def f3(rand = random.random, myrange = xrange):
        return sum((rand() for _ in myrange(10**5)))
    

    这将在函数定义时绑定名称。 我想,加速的最大部分是由于避免重复的属性查找。

    【讨论】:

    • 我的代码不在函数内。因此,您确定您的建议适用吗?
    • 是的,当您将random.random 别名为您的模块的本地名称时,当您重复 调用它时同样适用。它将保存重复的属性查找。
    • ch3ka@x200 /tmp % python2 -m timeit -c 'import random; sum(random.random() for _ in range(10**4))' 100 loops, best of 3: 2.17 msec per loop ch3ka@x200 /tmp % python2 -m timeit -c 'from random import random as r; sum(r() for _ in range(10**4))' 1000 loops, best of 3: 1.5 msec per loop
    • ch3ka@x200 /tmp % python3 -m timeit -c 'import random; sum(random.random() for _ in range(10**4))' 1000 loops, best of 3: 1.93 msec per loop`ch3ka@x200 /tmp % python3 -m timeit -c 'from random import random as r; sum(r() for _ in range(10**4))' 1000 loops, best of 3: 1.55 msec per loop
    • 不幸的是@ch3ka,当我检查 my 代码时,我没有看到改进。请参阅上面的edit (Thu, Nov 27, 2014 20:45:26)。我错过了什么吗?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-04-04
    • 1970-01-01
    • 2015-11-08
    相关资源
    最近更新 更多