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

时间:2013-07-09 01:50:34

标签: python jython

当我学习Python(特别是Jython,如果差异在这里很重要)我正在编写一个简单的终端游戏,它使用基于这些技能水平的技能和骰子来确定尝试动作的成功/失败。我希望最终在一个更大的游戏项目中使用这段代码。

在压力测试下,代码使用.5GB的内存并且似乎需要相当长的时间才能获得结果(约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,但我忘了除以骰子的数量)。 mem用法现在看起来是〜.2GB。此前的实现也无法进行100mil测试,我已经在最高达到1bil的测试中运行了这个测试(花了8分钟,内存使用情况似乎没有太大差异)。

4 个答案:

答案 0 :(得分:4)

你正在做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 Commons数字或PColt这样的Java库,并找出Java与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或其他方面是不可用的。你也可以只borrow the source获取check_output从2.7的实现开始。)

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

在快速测试中,开销(主要是产生一个新的翻译过程,但也有编组参数和结果等)增加了10%以上的成本,这意味着我只有9倍的改进,而不是11倍。在Windows上,这会更糟糕。但还不足以否定任何需要运行一分钟的脚本的好处。


最后,如果你正在做更复杂的事情,你可以使用execnet,它包装了Jython&lt; - &gt; CPython&lt; - &gt; PyPy,让你在代码的每个部分使用最好的东西而不用必须做所有明确subprocess的事情。

答案 1 :(得分:2)

嗯,有一点,使用xrange代替rangerange为1000万个数字中的每一个分配一个带有元素的数组,而xrange创建一个生成器。这将有助于记忆,也可能是速度。

答案 2 :(得分:1)

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

其次,您忘记将range()中的RollTesting()更改为xrange()

第三,Python有通常的+=*=-=等运算符,因此fail = fail + 1变为fail += 1。但是,Python没有--++

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

答案 3 :(得分:0)

如果您担心效率,请使用分析器。这是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))

使用作业的内置功能。<​​/ p>

>>> 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}

这使时间缩短了一半。

如果您的大多数模具卷都是一种特定类型的卷筒,您应该只使用随机函数生成您将为该卷筒获得的特定分布。