Files
netease-modsdk-wiki/docs/mcguide/20-玩法开发/18-性能优化/代码优化.md
2025-03-18 14:46:12 +08:00

659 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
front: https://nie.res.netease.com/r/pic/20210728/2dc2a94f-71f6-4cc5-8700-3c3696f79a0c.jpg
hard: 进阶
time: 30分钟
---
# 代码优化
## 前言
本文介绍了基于Python 2的一些常用技巧能够优化代码提升程序运行效率。
## 使用缓存(内存换CPU)
对象的重复创建与销毁会有一定性能消耗,对于需要频繁使用的数据,建议保存起来,下次从内存取出来直接使用,是一种常用的空间换时间(内存换CPU)的优化手段,对于减少游戏卡顿有较好效果。
### 避免在tick函数内使用import
import模块的消耗并没有小到可以忽略的地步建议挪到文件的顶部进行import。如果这样会导致循环引用则可以将模块缓存为类的成员变量
- 错误写法:
```python
class DemoClientSystem(ClientSystem):
def Update(self):
# 在每帧执行的逻辑内import模块
import mod.client.extraClientApi as clientApi
clientApi.xxx
```
- 正确写法:
```python
# 在文件顶部import模块
import mod.client.extraClientApi as clientApi
class DemoClientSystem(ClientSystem):
def Update(self):
clientApi.xxx
```
如果两个模块需要相互引用那么同时在文件顶部import对方会导致循环引用报错则可以用下面的方法处理
```python
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的时候
```python
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__函数当中
```python
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)
```
### 缓存多次用到的中间数据
一些方法多次调用的返回值是一样,可以使用临时变量缓存,不需要重复调用
- 错误写法:
```python
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)
```
- 正确写法:
```python
# 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推荐把命中概率较高的判断放前面。
- 错误写法:
```python
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()
...
```
- 正确写法:
```python
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 即一百万个方块,必然会卡)
```python
# 修改某个区域 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个)
```python
# 修改某个区域 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
```
### 非重要逻辑降帧处理
不要每帧执行所有逻辑更新,不同的逻辑实际中根据实时性要求进行间隔更新
- 错误写法:
(每帧执行所有更新逻辑)
```python
def Update(self):
self.do_something1()
self.do_something2()
self.do_something3()
```
- 正确写法:
(分开每帧只处理5个)
```python
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循环。
**列表添加元素:**
```python
a = []
for i in xrange(1000):
if i % 2 == 0:
a.append(i*i)
```
**缓存append方法**
```python
a = []
l = a.append
for i in xrange(1000):
if i % 2 == 0:
l(i*i)
```
**列表推导式:**
```python
a = [i*i for i in xrange(1000) if i % 2 == 0]
```
**测试样例:**
```python
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)
```
**测试结果:**
```python
loop + append: 0.6161811
loop + append(cache): 0.5132234
list comprehenshion: 0.4063318
```
**结论:**
列表推导式,能获得明显的性能提升,元素越多差距越明显。
还有**字典推导式:**
```python
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}
```
**集合推导式:**
```python
g = (1,2,3,4,5,6)
s = {v for v in g if v % 2 == 0}
```
### 字符串拼接
我们有很多办法拼接字符串比如直接相加、使用format、使用%、使用join那么到底哪种办法最快呢
**常见写法:**
```python
s = s1 + s2 + s3
s = s1; s += s2; s += s3
s = '%s%s%s' % (s1,s2,s3)
s = ''.join((s1,s2,s3))
```
**测试样例1**
```python
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**
```python
0.7396258
0.8553558
1.5691264
3.8130296
1.0085892
```
**测试样例2**
```python
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**
```python
1.4091635
1.6201083
3.4721674
4.6679361
1.2252783
```
**结论:**
- 要拼接的子串数量较少时如不多于3个直接相加是最快的
- 当拼接的子串数量较多时,`join`方法是最快的
- 如果只是想纯粹拼接一下字符串,不要使用格式化方法
### 变量访问
局部变量访问速度最快其次是全局变量。如果要访问对象的属性比如self.client.aaa.bbb中出现了三个点而每一个点代表一次访问就会多消耗一次性能。建议在频繁使用时缓存为局部变量。
```python
# 缓存为全局变量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可有下列写法
```python
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
```
**测试样例:**
```python
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填进去反而更慢。
### 函数调用
函数调用是有额外开销的,效率敏感场合不容忽略。
**测试样例:**
```python
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))
```
**测试结果:**
```python
0.0104322
0.0051873
```
**结论:**
python里1万次的函数调用的消耗约1毫秒的量级。在效率敏感场合尽量省去不必要的几行代码的函数包装减少调用层级以及减少默认参数个数。
### 方法调用
类与实例方法的调用和函数调用类似,封装太多也会有明显的效率下降,而且情况可能更严重。
**测试样例:**
```python
# -*- 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)
```
**测试结果:**
```python
函数 run 耗时: 274 毫秒
函数 run2 耗时: 168 毫秒
```
可见减少一层方法调用后耗时274ms能降到168ms。
**结论:**
为了效率的话请尽量避免过多的类方法封装同一实例方法的频繁调用请先缓存下来如第一个例子中的l=a.append
### 模块导入
关于import写在什么地方我们都知道写在模块开头有这么一些弊端
- 首次加载卡顿
- 内存过多
- 带来冗余
- 循环引用
但写在函数内就一定是最好的办法吗?
**测试样例:**
```python
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)
```
**测试结果:**
```python
0.1006268
0.0177434
0.1125192
```
可见函数内import明显要慢很多尤其是在另外一个package里面import。
**结论:**
基础性/通用性模块的导入import写在模块头当然前提是这些基础模块要做好规划不要过于臃肿不要互相耦合严重。
对于频繁调用的函数函数开头不适宜有太多importpackage结构也不宜搞得过于复杂。