繁体   English   中英

Python / Jython中效率低下的随机骰子滚动

[英]Inefficient Random Dice Roll in Python/Jython

当我学习Python时(特别是Jython,如果这里的区别很重要),我正在写一个简单的终端游戏,该游戏根据这些技能的水平使用技巧和掷骰子来确定尝试进行操作的成功/失败。 我希望最终在更大的游戏项目中使用此代码。

在压力测试下,该代码使用.5GB的ram,似乎需要相当长的时间才能得到结果(〜50秒)。 可能只是任务确实很繁琐,但是作为菜鸟,我敢打赌,我的工作效率低下。 任何人都可以在这两个方面给出一些提示:

  • 如何提高这段代码的效率

  • 以及如何以更pythonic的方式编写此代码?

     import random def DiceRoll(maxNum=100,dice=2,minNum=0): return sum(random.randint(minNum,maxNum) for i in xrange(dice)) def RollSuccess(max): x = DiceRoll() if(x <= (max/10)): return 2 elif(x <= max): return 1 elif(x >= 100-(100-max)/10): return -1 return 0 def RollTesting(skill=50,rolls=10000000): cfail = 0 fail = 0 success = 0 csuccess = 0 for i in range(rolls+1): roll = RollSuccess(skill) if(roll == -1): cfail = cfail + 1 elif(roll == 0): fail = fail + 1 elif(roll == 1): success = success + 1 else: csuccess = csuccess + 1 print "CFails: %.4f. Fails: %.4f. Successes: %.4f. CSuccesses: %.4f." % (float(cfail)/float(rolls), float(fail)/float(rolls), float(success)/float(rolls), float(csuccess)/float(rolls)) RollTesting() 

编辑-这是我的代码:

from random import random

def DiceRoll():
   return 50 * (random() + random())

def RollSuccess(suclim):
  x = DiceRoll()
  if(x <= (suclim/10)):
    return 2
  elif(x <= suclim):
    return 1
  elif(x >= 90-suclim/10):
    return -1
  return 0

def RollTesting(skill=50,rolls=10000000):
  from time import clock
  start = clock()
  cfail = fail = success = csuccess = 0.0
  for _ in xrange(rolls):
    roll = RollSuccess(skill)
    if(roll == -1):
      cfail += 1
    elif(roll == 0):
      fail += 1
    elif(roll == 1):
      success += 1
    else:
      csuccess += 1
  stop = clock()
  print "Last time this statement was manually updated, DiceRoll and RollSuccess totaled 12 LOC."
  print "It took %.3f seconds to do %d dice rolls and calculate their success." % (stop-start,rolls)
  print "At skill level %d, the distribution is as follows" % (skill)
  print "CFails: %.4f. Fails: %.4f. Successes: %.4f. CSuccesses: %.4f." % (cfail/rolls, fail/rolls, success/rolls, csuccess/rolls)

RollTesting(50)

并输出:

Last time this statement was manually updated, DiceRoll and RollSuccess totaled 12 LOC.
It took 6.558 seconds to do 10000000 dice rolls and calculate their success.
At skill level 50, the distribution is as follows
CFails: 0.0450. Fails: 0.4548. Successes: 0.4952. CSuccesses: 0.0050.

值得注意的是,这并不等效,因为我对随机计算进行了足够的更改,以使输出明显不同(本来应该是0-100,但是我忘了除以骰子的数量)。 内存使用量现在看起来约为.2GB。 同样,以前的实现不能进行1亿次测试,我已经在多达10亿次测试中运行了该测试(花了8分钟,内存使用似乎没有太大不同)。

您正在执行1000万次循环。 仅循环成本就可能占您总时间的10%。 然后,如果整个循环无法立即放入缓存,则可能使速度进一步降低。

有没有办法避免在Python中执行所有这些循环? 是的,您可以使用Java来完成。

这样做的明显方法是实际编写和调用Java代码。 但是您不必这样做。


列表推导或由本机内置驱动器生成的表达式也将在Java中进行循环。 因此,除了更紧凑和更简单之外,这还应该更快:

attempts = (RollSuccess(skill) for i in xrange(rolls))
counts = collections.Counter(attempts)
cfail, fail, success, csuccess = counts[-1], counts[0], counts[1], counts[2]

不幸的是,尽管在Jython 2.7b1中似乎确实更快,但在2.5.2中实际上要慢一些。


加快循环速度的另一种方法是使用向量化库。 不幸的是,我不知道Jython人们会使用什么,但是在带有numpy CPython中,它看起来像这样:

def DiceRolls(count, maxNum=100, dice=2, minNum=0):
    return sum(np.random.random_integers(minNum, maxNum, count) for die in range(dice))

def RollTesting(skill=50, rolls=10000000):
    dicerolls = DiceRolls(rolls)
    csuccess = np.count_nonzero(dicerolls <= skill/10)
    success = np.count_nonzero((dicerolls > skill/10) & (dicerolls <= skill))
    fail = np.count_nonzero((dicerolls > skill) & (dicerolls <= 100-(100-skill)/10))
    cfail = np.count_nonzero((dicerolls > 100-(100-skill)/10)

这样可以使速度加快约8倍。

我怀疑在Jython中,事情并不像numpy那样好,并且您应该导入Java库(例如Apache Commons数值或PColt)并找出Java-vs.-Python本身,但是最好搜索和查找。 /或要求胜过承担。


最后,您可能想使用其他解释器。 这里的CPython 2.5或2.7与Jython 2.5似乎没有太大区别,但这确实意味着您可以使用numpy进行8倍的改进。 同时,PyPy 2.0速度提高了11倍,并且没有任何变化。

即使您需要在Jython中执行主程序,如果您的运行速度足以使启动新进程的成本相形见,,您也可以将其移至通过subprocess运行的单独脚本中。 例如:

subscript.py:

# ... everything up to the RollTesting's last line
    return csuccess, success, fail, cfail

skill = int(sys.argv[1]) if len(sys.argv) > 1 else 50
rolls = int(sys.argv[2]) if len(sys.argv) > 2 else 10000000
csuccess, success, fail, cfail = RollTesting(skill, rolls)
print csuccess
print success
print fail
print cfail

mainscript.py:

def RollTesting(skill, rolls):
    results = subprocess32.check_output(['pypy', 'subscript.py', 
                                         str(skill), str(rolls)])
    csuccess, success, fail, cfail = (int(line.rstrip()) for line in results.splitlines())
    print "CFails: %.4f. Fails: %.4f. Successes: %.4f. CSuccesses: %.4f." % (float(cfail)/float(rolls), float(fail)/float(rolls), float(success)/float(rolls), float(csuccess)/float(rolls))

(我用subprocess32模块获得的反向移植check_output ,这是不是可以在Python 2.5,Jython或以其他方式,你也可以只借来源check_output 2.7的实现。)

请注意,Jython 2.5.2在subprocess进程中有一些严重的错误(将在2.5.3和2.7.0中修复,但是今天对您没有帮助)。 但幸运的是,它们不会影响此代码。

在快速测试中,开销(主要是产生新的解释器过程,但还编组了参数和结果等)使成本增加了10%以上,这意味着我只得到了9倍的改进,而不是11倍的改进。 在Windows上,情况会更糟。 但是,这不足以抵消运行一分钟左右的任何脚本的好处。


最后,如果您正在做更复杂的事情,则可以使用execnet ,它包装了Jython <-> CPython <-> PyPy,使您可以在代码的每个部分中使用最佳内容,而不必执行所有显式的subprocess

好吧,一件事,使用xrange而不是range range为1000万个数字中的每个数字分配一个带有元素的数组,而xrange创建一个生成器。 这将帮助您节省大量内存,并且可能还会提高速度。

通过将RollTesting()那些局部变量定义为0.0可以减少对float()所有调用。 0int常数, 0.0float常数。 如果任何算术运算中甚至包含一个float ,则返回另一个float

其次,你忘了更改range()RollTesting()xrange()

第三,Python具有常用的+=*=-=等运算符,因此fail = fail + 1变为fail += 1 Python没有--++

最后,在if语句中不需要括号。

如果您担心效率,请使用探查器。 这是100,000卷:

Python 3.3.2 (v3.3.2:d047928ae3f6, May 16 2013, 00:03:43) [MSC v.1600 32 bit (In
tel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import scratch
CFails: 0.5522. Fails: 0.3175. Successes: 0.1285. CSuccesses: 0.0019.
         1653219 function calls in 5.433 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    5.433    5.433 <string>:1(<module>)
        2    0.000    0.000    0.000    0.000 cp437.py:18(encode)
   200002    0.806    0.000    2.526    0.000 random.py:165(randrange)
   200002    0.613    0.000    3.139    0.000 random.py:210(randint)
   200002    1.034    0.000    1.720    0.000 random.py:216(_randbelow)
        1    0.181    0.181    5.433    5.433 scratch.py:17(RollTesting)
   100001    0.371    0.000    4.864    0.000 scratch.py:4(DiceRoll)
   300003    0.769    0.000    3.908    0.000 scratch.py:5(<genexpr>)
   100001    0.388    0.000    5.251    0.000 scratch.py:7(RollSuccess)
        2    0.000    0.000    0.000    0.000 {built-in method charmap_encode}
        1    0.000    0.000    5.433    5.433 {built-in method exec}
        1    0.000    0.000    0.000    0.000 {built-in method print}
   100001    0.585    0.000    4.493    0.000 {built-in method sum}
   200002    0.269    0.000    0.269    0.000 {method 'bit_length' of 'int' obje
cts}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Prof
iler' objects}
   253196    0.417    0.000    0.417    0.000 {method 'getrandbits' of '_random.
Random' objects}

显然,由于我在Python 3中运行,因此此代码稍有不同,但是您应该可以看到,大部分时间都花在了各种random.py函数,求和和生成器表达式上。 基本上,这是您实际上希望花费时间的地方,但是我们可以进一步优化它吗?

当前,DiceRoll生成两个随机数并将它们相加。 这是正态分布的近似值。 为什么还要打乱骰子呢? 2d100是正态分布,平均值为101,标准偏差为40.82。 (由于这些骰子实际上是从0到99,所以我们可以减去两点。)

def DiceRoll2():
  return int(random.normalvariate(99, 40.82))

使用内置功能进行作业。

>>> timeit.timeit('scratch.DiceRoll()', 'import scratch')
7.253364044871624

>>> timeit.timeit('scratch.DiceRoll2()', 'import scratch')
1.8604163378306566

这是使用DiceRoll2运行100,000卷的探查器:

Python 3.3.2 (v3.3.2:d047928ae3f6, May 16 2013, 00:03:43) [MSC v.1600 32 bit (In
tel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import scratch
CFails: 0.5408. Fails: 0.3404. Successes: 0.1079. CSuccesses: 0.0108.
         710724 function calls in 2.275 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    2.275    2.275 <string>:1(<module>)
        2    0.000    0.000    0.000    0.000 cp437.py:18(encode)
   100001    0.819    0.000    1.393    0.000 random.py:354(normalvariate)
   100001    0.361    0.000    2.094    0.000 scratch.py:10(RollSuccess)
        1    0.180    0.180    2.275    2.275 scratch.py:20(RollTesting)
   100001    0.340    0.000    1.733    0.000 scratch.py:7(DiceRoll2)
        2    0.000    0.000    0.000    0.000 {built-in method charmap_encode}
        1    0.000    0.000    2.275    2.275 {built-in method exec}
   136904    0.203    0.000    0.203    0.000 {built-in method log}
        1    0.000    0.000    0.000    0.000 {built-in method print}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
   273808    0.371    0.000    0.371    0.000 {method 'random' of '_random.Random' objects}

这节省了一半的时间。

如果您的大多数模具辊将是一种特定类型的辊,则您可能应该仅使用随机函数来生成将为该辊获得的特定分布。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM