# 代码优化

# 前言

本文介绍了基于Python 2的一些常用技巧,能够优化代码,提升程序运行效率。

# 使用缓存(内存换CPU)

对象的重复创建与销毁会有一定性能消耗,对于需要频繁使用的数据,建议保存起来,下次从内存取出来直接使用,是一种常用的空间换时间(内存换CPU)的优化手段,对于减少游戏卡顿有较好效果。

# 避免在tick函数内使用import

import模块的消耗并没有小到可以忽略的地步,建议挪到文件的顶部进行import。如果这样会导致循环引用,则可以将模块缓存为类的成员变量

  • 错误写法:
class DemoClientSystem(ClientSystem):
    def Update(self):
        # 在每帧执行的逻辑内import模块
        import mod.client.extraClientApi as clientApi
        clientApi.xxx
  • 正确写法:
# 在文件顶部import模块
import mod.client.extraClientApi as clientApi
class DemoClientSystem(ClientSystem):
    def Update(self):
        clientApi.xxx

如果两个模块需要相互引用,那么同时在文件顶部import对方,会导致循环引用报错,则可以用下面的方法处理:

class DemoClientSystem(ClientSystem):
    def __init__(self, namespace, systemName):
        ClientSystem.__init__(self, namespace, systemName)
        # 假设当前模块与另一个otherModule模块需要相互引用
        import demoScripts.client.otherModule as otherModule
        self.otherModule = otherModule

    def Update(self):
        self.otherModule.xxx

# 避免多次初始化常量

  • 错误写法:

在频繁调用的函数中进行声明,例如每次Update的时候

class DemoClientSystem(ClientSystem):
    def Update(self):
        # 常量,每帧创建,实际中可能这里会是比较多的数据
        bigDict = {
            (-1, -1): 1,
            (-1, 0): 2,
            (-1, 1): 3,
            (0, -1): 4,
            (0, 0): 5,
            (0, 1): 6,
            (1, -1): 7,
            (1, 0): 8,
            (1, 1): 9,
        }
        # 读取常量做一些逻辑
        do_something(bigDict)
  • 正确写法:

包含数据比较多的一些常量,特别是List或者Dict类型的,可以放到类的__init__函数当中

class DemoClientSystem(ClientSystem):
    # 构造函数
    def __init__(self, namespace, systemName):
        ClientSystem.__init__(self, namespace, systemName)
        # 在初始化时创建
        self.bigDict = {
            (-1, -1): 1,
            (-1, 0): 2,
            (-1, 1): 3,
            (0, -1): 4,
            (0, 0): 5,
            (0, 1): 6,
            (1, -1): 7,
            (1, 0): 8,
            (1, 1): 9,
        }

    def Update(self):
        do_something(self.bigDict)

# 缓存多次用到的中间数据

一些方法多次调用的返回值是一样,可以使用临时变量缓存,不需要重复调用

  • 错误写法:
class DemoServerSystem(ServerSystem):
    # 监听的ServerItemUseOnEvent事件回调
    def ServerItemUseOnEvent(self, args):
        # 设置多个方块
        self.SetBlock(args['dimensionId'], (args['x']-1, args['y'], args['z']), 'minecraft:air')
        self.SetBlock(args['dimensionId'], (args['x']-1, args['y'], args['z']), 'minecraft:air')
        self.SetBlock(args['dimensionId'], (args['x'], args['y'], args['z']), 'minecraft:air')
        self.SetBlock(args['dimensionId'], (args['x'], args['y'], args['z']-1), 'minecraft:air')
        self.SetBlock(args['dimensionId'], (args['x'], args['y'], args['z']+1), 'minecraft:air')

    def SetBlock(self, dimensionId, pos, blockName):
        serverApi.GetEngineCompFactory().CreateBlockInfo(levelId).SetBlockNew(pos, {'name': blockName}, 0, dimensionId)
  • 正确写法:
# compFactory使用缓存
serverCompFactory = serverApi.GetEngineCompFactory()
class DemoServerSystem(ServerSystem):
    # 监听的ServerItemUseOnEvent事件回调
    def ServerItemUseOnEvent(self, args):
        # 对字典内的值做缓存
        dimensionId = args['dimensionId']
        x = args['x']
        y = args['y']
        z = args['z']
        self.SetBlock(dimensionId, (x-1, y, z), 'minecraft:air')
        self.SetBlock(dimensionId, (x-1, y, z), 'minecraft:air')
        self.SetBlock(dimensionId, (x, y, z), 'minecraft:air')
        self.SetBlock(dimensionId, (x, y, z-1), 'minecraft:air')
        self.SetBlock(dimensionId, (x, y, z+1), 'minecraft:air')

    def SetBlock(self, dimensionId, pos, blockName):
        serverCompFactory.CreateBlockInfo(levelId).SetBlockNew(pos, {'name': blockName}, 0, dimensionId)

# 使用dict代替多个else if

当条件判断的分支很多时,dict跳转的性能会比一连串的else高很多。如果一定要用if,推荐把命中概率较高的判断放前面。

  • 错误写法:
serverCompFactory = serverApi.GetEngineCompFactory()
class DemoServerSystem(ServerSystem):
    def HandleBlocks(self, pos, dimensionId):
        # 获取方块信息
        blockIdentifier = serverCompFactory.CreateBlockInfo(levelId).GetBlockNew(pos, dimensionId)[0]
        # 根据方块类型做出不同的处理
        if blockIdentifier == "minecraft:iron_ore":
            self.handleIronBlock()
        elif blockIdentifier == "minecraft:gold_ore":
            self.handleGoldBlock()
        elif blockIdentifier == "minecraft:diamond_ore":
            self.handleDiamondBlock()
        ...
  • 正确写法:
serverCompFactory = serverApi.GetEngineCompFactory()
class DemoServerSystem(ServerSystem):
    def __init__(self):
        # 注册处理函数
        self.blockHandlers = {
            "minecraft:iron_ore": self.handleIronBlock,
            "minecraft:gold_ore": self.handleGoldBlock,
            "minecraft:diamond_ore": self.handleDiamondBlock,
        }

    def HandleBlocks(self, data):
        blockIdentifier = serverCompFactory.CreateBlockInfo(levelId).GetBlockNew(pos, dimensionId)[0]
        # 从dict中选取处理函数
        handler = self.blockHandlers.get(blockIdentifier)
        if handler:
            handler()

# 使用分帧(实时性换CPU)

同一时刻内处理大量的逻辑,容易造成卡顿。这时候需要把逻辑执行的时间错开到多帧去执行,让每一帧的任务量不要太重。

# 大批量修改数据分多帧处理

这里以方块为例:

  • 错误写法: (同一时刻全部处理,需要处理 100 * 100 * 100 即一百万个方块,必然会卡)
# 修改某个区域 100 * 100 * 100范围内的方块为空气
def SetBlocksToAir(self, fromPos):
    blockcomp = serverApi.CreateComponent(id, "Minecraft", "blockInfo")
    for x in range(1, 100):
        for y in range(1, 100):
            for z in range(1, 100):
                blockcomp.SetBlockNew((fromPos[0] + x, fromPos[1] + y, fromPos[2] + z), {'name':'minecraft:air'})
  • 正确写法: (分开每帧只处理5个)
# 修改某个区域 100 * 100 * 100范围内的方块为空气
def SetBlocksToAir(self, fromPos):
    # 命令队列
    self.posList = []
    self.posIndex = 0

    for x in range(1, 100):
        for y in range(1, 100):
            for z in range(1, 100):
                self.posList.append((fromPos[0] + x, fromPos[1] + y, fromPos[2] + z))

# 被引擎直接执行的父类的重写函数,引擎会执行该Update回调,1秒钟30帧
def Update(self):
    if self.posList:
        posListLen = len(self.posList)
        blockcomp = serverApi.CreateComponent(id, "Minecraft", "blockInfo")
        #每帧处理5个
        handleNum = 5
        while(handleNum > 0 and self.posIndex < posListLen):
            blockcomp.SetBlockNew(self.posList[self.posIndex], {'name':'minecraft:air'})
            self.posIndex = self.posIndex + 1
            handleNum = handleNum - 1

        # 全部处理完成
        if self.posIndex >= posListLen:
            self.posList = None

# 非重要逻辑降帧处理

不要每帧执行所有逻辑更新,不同的逻辑实际中根据实时性要求进行间隔更新

  • 错误写法: (每帧执行所有更新逻辑)
def Update(self):
    self.do_something1()
    self.do_something2()
    self.do_something3()
  • 正确写法: (分开每帧只处理5个)
class DemoClientSystem(ClientSystem):
    # 构造函数
    def __init__(self, namespace, systemName):
        ClientSystem.__init__(self, namespace, systemName)
        self.tick = 0

    def Update(self):
        self.tick = self.tick + 1
        # 重要逻辑每帧执行
        self.do_something1()

        if self.tick % 5 == 0:
            # 次要逻辑降帧执行
            self.do_something2()

        if self.tick % 10 == 0:
            # 更次要的逻辑,使用更低的帧率执行
            self.do_something3()

# 少用轮询逻辑

使用事件或一些适用的接口来代替每帧尝试的操作。

假想有一个需求:我想删除一个实体,但是当前这个实体没有被加载

  • 错误写法:

    每帧尝试删除该实体,直到成功为止

  • 推荐写法:

    1. 监听AddEntityServerEvent,在该实体的回调中删除。
    2. 如果该实体是手动创建的,可以使用SetPersistence接口将其设置为不存盘,那就不再需要处理该实体被卸载而无法删除的情况。

# 优化字节码

Python 是解释型语言,代码在运行时会先编译为字节码(Bytecode),再由解释器逐行执行字节码,优化字节码可以直接提升执行效率。

# 使用推导式

如果要对容器进行操作,使用推导式是最快的办法。在可以使用列表/字典/集合推导式时,尽量使用推导式,而不是使用for循环。

列表添加元素:

a = []
for i in xrange(1000):
    if i % 2 == 0:
        a.append(i*i)

缓存append方法:

a = []
l = a.append
for i in xrange(1000):
    if i % 2 == 0:
        l(i*i)

列表推导式:

a = [i*i for i in xrange(1000) if i % 2 == 0]

测试样例:

from timeit import timeit
print "loop + append:", timeit("for i in xrange(1000):\n if i % 2 == 0:\n  a.append(i*i)", "a=[]", number=10000)
print "loop + append(cache):", timeit("for i in xrange(1000):\n if i % 2 == 0:\n  l(i*i)", "a=[];l=a.append", number=10000)
print "list comprehenshion:", timeit("a = [i*i for i in xrange(1000) if i % 2 == 0]", number=10000)

测试结果:

loop + append: 0.6161811
loop + append(cache): 0.5132234
list comprehenshion: 0.4063318

**结论:**列表推导式,能获得明显的性能提升,元素越多差距越明显。

还有字典推导式:

g =  (('a',1),('b',2),('c',3),('d',4),('e',5),('f',6))
d = {k:v for k, v in g if v % 2 == 0}

集合推导式:

g = (1,2,3,4,5,6)
s = {v for v in g if v % 2 == 0}

# 字符串拼接

我们有很多办法拼接字符串,比如直接相加、使用format、使用%、使用join,那么到底哪种办法最快呢?

常见写法:

s = s1 + s2 + s3

s = s1; s += s2; s += s3

s = '%s%s%s' % (s1,s2,s3)

s = ''.join((s1,s2,s3))

测试样例1:

from timeit import timeit
N = 10000000
setup = 's1="hello"*35; s2="world"*25; s3="!"*30; s4=s3*2; s5=s3*2'
print(timeit("s = s1 + s2 + s3", setup, number=N))
print(timeit("s = s1; s+=s2; s+=s3", setup, number=N))
print(timeit("s = '%s%s%s' % (s1,s2,s3)", setup, number=N))
print(timeit("s = '{}{}{}'.format(s1,s2,s3)", setup, number=N))
print(timeit("s = ''.join((s1,s2,s3))", setup, number=N))

测试结果1:

0.7396258
0.8553558
1.5691264
3.8130296
1.0085892

测试样例2:

from timeit import timeit
N = 10000000
setup = 's1="hello"*35; s2="world"*25; s3="!"*30; s4=s3*2; s5=s3*2'
print(timeit("s = s1 + s2 + s3 + s4 + s5", setup, number=N))
print(timeit("s = s1; s+=s2; s+= s3; s+= s4; s+= s5", setup, number=N))
print(timeit("s = '%s%s%s%s%s' % (s1,s2,s3,s4,s5)", setup, number=N))
print(timeit("s = '{}{}{}{}{}'.format(s1,s2,s3,s4,s5)", setup, number=N))
print(timeit("s = ''.join((s1,s2,s3,s4,s5))", setup, number=N))

测试结果2:

1.4091635
1.6201083
3.4721674
4.6679361
1.2252783

结论:

  • 要拼接的子串数量较少时(如不多于3个),直接相加是最快的
  • 当拼接的子串数量较多时,join方法是最快的
  • 如果只是想纯粹拼接一下字符串,不要使用格式化方法

# 变量访问

局部变量访问速度最快,其次是全局变量。如果要访问对象的属性,比如self.client.aaa.bbb中出现了三个点,而每一个点代表一次访问,就会多消耗一次性能。建议在频繁使用时缓存为局部变量。

# 缓存为全局变量CF,减少了一次访问
CF = serverApi.GetEngineCompFactory()
def OnCustomCommandTrigger(self, args):
    # 在循环前,将api方法缓存为局部变量
    createExplosion = CF.CreateExplosion(levelId).CreateExplosion
    for _ in xrange(1000):
        createExplosion(...)# 直接调用

    # 将自己的方法/属性缓存为局部变量
    func = self.xxxsystem.aaa.bbb
    for _ in xrange(1000):
        func(...)

# 字典查询

字典的查询属于属性访问中的一个特例。取字典中特定key的值,如取不到返回None,可有下列写法:

def get1(d, key):
    if key in d:
        return d[key]
    return None

def get2(d, key):
    if d.has_key(key):
        return d[key]
    return None

def get3(d, key):
    return d.get(key)

def get4(d, key):
    return d.get(key, None)

def get5(d, key):
    try:
        return d[key]
    except KeyError:
        pass

测试样例:

g_d = {"a": 23, "b": 11, "c": 88, "d": 2, "e": 3, "f": 4, "g": 11, "h": 25, "i": 46}
from timeit import timeit
print(timeit('get1(g_d, "b")', 'from __main__ import get1, g_d', number=100000))
print(timeit('get2(g_d, "b")', 'from __main__ import get2, g_d', number=100000))
print(timeit('get3(g_d, "b")', 'from __main__ import get3, g_d', number=100000))
print(timeit('get4(g_d, "b")', 'from __main__ import get4, g_d', number=100000))
print(timeit('get5(g_d, "b")', 'from __main__ import get5, g_d', number=100000))

print(timeit('get1(g_d, "z")', 'from __main__ import get1, g_d', number=100000))
print(timeit('get2(g_d, "z")', 'from __main__ import get2, g_d', number=100000))
print(timeit('get3(g_d, "z")', 'from __main__ import get3, g_d', number=100000))
print(timeit('get4(g_d, "z")', 'from __main__ import get4, g_d', number=100000))
print(timeit('get5(g_d, "z")', 'from __main__ import get5, g_d', number=100000))

结果分命中、不命中两种情况汇总:

单位:ms/1w次 命中 不命中
get1 1.17 1.05
get2 1.59 1.43
get3 1.62 1.59
get4 1.75 1.80
get5 1.04 9.01

从这个表可以看到,get1用in来判断,平均表现是最好的,是否命中,都是1ms多一点。而最后这个try except,命中的时候是最佳的,不命中的时候性能就大幅恶化。

结论:

  • 对于key是否存在,直接用in来做判断即可,has_key接口比in慢。当然in方法不止可以对字典用,也可以对任何iterable的对象用,python是动态语言,要清楚你in的对象到底是什么。
  • get的default参数不必填None,因为它本来就是None,填进去反而更慢。

# 函数调用

函数调用是有额外开销的,效率敏感场合不容忽略。

测试样例:

log = lambda msg: None

def foo(msg):
    log(msg)

from timeit import timeit
print(timeit('foo("hello")', 'from __main__ import foo', number=100000))
print(timeit('log("hello")', 'from __main__ import log', number=100000))

测试结果:

0.0104322
0.0051873

**结论:**python里1万次的函数调用的消耗,约1毫秒的量级。在效率敏感场合,尽量省去不必要的几行代码的函数包装,减少调用层级,以及减少默认参数个数。

# 方法调用

类与实例方法的调用和函数调用类似,封装太多也会有明显的效率下降,而且情况可能更严重。

测试样例:

# -*- coding: gbk -*-
import time
# 定义时间测量装饰器
def time_it(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print "函数 {} 耗时: {:.0f} 毫秒".format(func.__name__, (end_time - start_time) * 1000)
        return result
    return wrapper
def show_warn(message):
    pass
HP_TH = 10
class Player(object):
    def __init__(self):
        self.hp = 0
        self.hp_th = HP_TH
    def tick(self):
        if self.hp < self.hp_th:
            self.perform_warn()
    def perform_warn(self):
        show_warn("warn")
class Player2(object):
    def __init__(self):
        self.hp = 0
        self.hp_th = HP_TH
    def tick(self):
        if self.hp < self.hp_th:
            show_warn("warn")
# 性能测试
if __name__ == "__main__":
    N = 10000
    # 测试 Player 类
    players = []
    for _ in xrange(N):
        players.append(Player())
    @time_it
    def run(n):
        for _ in xrange(n):
            for p in players:
                p.tick()
    run(100)
    # 测试 Player2 类
    players = []
    for _ in xrange(N):
        players.append(Player2())
    @time_it
    def run2(n):
        for _ in xrange(n):
            for p in players:
                p.tick()
    run2(100)

测试结果:

函数 run 耗时: 274 毫秒
函数 run2 耗时: 168 毫秒

可见,减少一层方法调用后,耗时274ms能降到168ms。

**结论:**为了效率的话,请尽量避免过多的类方法封装;同一实例方法的频繁调用,请先缓存下来(如第一个例子中的l=a.append)

# 模块导入

关于import写在什么地方,我们都知道,写在模块开头,有这么一些弊端:

  • 首次加载卡顿
  • 内存过多
  • 带来冗余
  • 循环引用

但写在函数内就一定是最好的办法吗?

测试样例:

def tick():
    from packageA.subpackageA import math
    math.fabs(100)

from packageA.subpackageA import math
def tick2():
    math.fabs(100)

from timeit import timeit
print timeit("tick()", "from __main__ import tick", number=100000)
print timeit("tick2()", "from __main__ import tick2", number=100000)

# 假设把tick函数移到另一个package下(packageB/test.py):
print timeit("tick()", "from packageB.test import tick", number=100000)

测试结果:

0.1006268
0.0177434
0.1125192

可见,函数内import明显要慢很多,尤其是在另外一个package里面import。

结论:

基础性/通用性模块的导入,import写在模块头,当然前提是这些基础模块要做好规划,不要过于臃肿,不要互相耦合严重。

对于频繁调用的函数,函数开头不适宜有太多import,package结构也不宜搞得过于复杂。

https://nie.res.netease.com/r/pic/20210728/2dc2a94f-71f6-4cc5-8700-3c3696f79a0c.jpg

进阶

30分钟

前言

使用缓存(内存换CPU)

避免在tick函数内使用import

避免多次初始化常量

缓存多次用到的中间数据

使用dict代替多个else if

使用分帧(实时性换CPU)

大批量修改数据分多帧处理

非重要逻辑降帧处理

少用轮询逻辑

优化字节码

使用推导式

字符串拼接

变量访问

字典查询

函数调用

方法调用

模块导入