1

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

在压力测试下,代码使用了 0.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,但我忘了除以骰子的数量)。内存使用量现在看起来约为 .2GB。此外,之前的实现无法进行 1 亿次测试,我已经运行了多达 10 亿次测试(花了 8 分钟,内存使用似乎没有显着不同)。

4

4 回答 4

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 人们为此使用什么,但在 CPython with 中numpy,它看起来像这样:

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 中事情不如 with 好numpy,并且您需要导入 Java 库,如 Apache Commons numerics 或 PColt 并自己找出 Java-vs.-Python 问题……但最好搜索和/或要求而不是假设。


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

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

下标.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

主脚本.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 或其他版本中不可用。您也可以从 2.7 的实现中借用源代码。)check_output

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

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


最后,如果您正在做更复杂的事情,您可以使用execnet,它封装了 Jython<->CPython<->PyPy,让您可以在代码的每个部分中使用最好的东西,而不必做所有明确的subprocess事情。

于 2013-07-09T02:32:01.093 回答
2

好吧,有一件事,使用xrange而不是range. range为 1000 万位数字中的每一个分配一个元素的数组,同时xrange创建一个生成器。这将有助于提高记忆力,而且可能也会提高速度。

于 2013-07-09T01:53:52.010 回答
1

所有这些调用float()都可以通过在RollTesting()as中定义那些局部变量来减少0.00int常数,0.0float常数。如果float任何算术运算都涉及到一个,float则返回另一个。

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

第三,Python 有常用+=的 , *=,-=等运算符,所以fail = fail + 1变成fail += 1. 但是, Python 没有--or ++

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

于 2013-07-09T02:05:37.503 回答
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))

使用作业的内置功能。

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

这将时间减半。

如果您的大多数骰子都是一种特定类型的骰子,您可能应该只使用随机函数来生成您将为该骰子获得的特定分布。

于 2013-07-09T02:54:18.333 回答