Merge branch 'MCNeteaseDevs:main' into main
This commit is contained in:
BIN
mcguide/20-玩法开发/18-性能优化/images/after_optimization.png
Normal file
BIN
mcguide/20-玩法开发/18-性能优化/images/after_optimization.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
mcguide/20-玩法开发/18-性能优化/images/before_optimization.png
Normal file
BIN
mcguide/20-玩法开发/18-性能优化/images/before_optimization.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
659
mcguide/20-玩法开发/18-性能优化/代码优化.md
Normal file
659
mcguide/20-玩法开发/18-性能优化/代码优化.md
Normal file
@@ -0,0 +1,659 @@
|
||||
---
|
||||
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写在模块头,当然前提是这些基础模块要做好规划,不要过于臃肿,不要互相耦合严重。
|
||||
|
||||
对于频繁调用的函数,函数开头不适宜有太多import,package结构也不宜搞得过于复杂。
|
||||
222
mcguide/20-玩法开发/18-性能优化/着色器优化.md
Normal file
222
mcguide/20-玩法开发/18-性能优化/着色器优化.md
Normal file
@@ -0,0 +1,222 @@
|
||||
---
|
||||
front: https://nie.res.netease.com/r/pic/20210728/5a263f49-e1f3-4a9a-96b3-307b11848590.png
|
||||
hard: 进阶
|
||||
time: 30分钟
|
||||
---
|
||||
|
||||
# Shader优化
|
||||
|
||||
## 前言
|
||||
|
||||
为了做出更加炫酷的效果,我们往往会自定义材质,然后定义自己的Shader,或者直接重写替换原版Shader,特别是目前的很多光影MOD,基本都需要对Shader进行修改。Shader与GPU性能密切相关,Shader写得好能让配置不是很高的玩家也能流畅体验炫酷的效果。写得不好,则可能导致高端机也很卡。
|
||||
|
||||
## 尽量少用 if else 条件语句
|
||||
|
||||
GPU是并行处理逻辑的,采用SIMD(单指令多数据)结构, 同一段代码,同一时刻会被多个GPU的处理单元同时处理,这段代码的执行耗时取决于会被执行到的时间最长的代码。为了充分发挥GPU的并行性,我们尽量少用条件分支逻辑,让所有GPU处理单元都执行相同的代码。
|
||||
|
||||
if else的写法如果底下逻辑不多,大部分可以合并,则常常可以改用step()函数进行优化
|
||||
|
||||
step(a,b)的功能为:
|
||||
```glsl
|
||||
若b >= a则返回1,否则返回0
|
||||
```
|
||||
|
||||
所以如果有这么一个写法:
|
||||
```glsl
|
||||
if(r >= 0.5)
|
||||
{
|
||||
r = 0.6;
|
||||
}else{
|
||||
r = 0.4;
|
||||
}
|
||||
```
|
||||
|
||||
则应该写成:
|
||||
```glsl
|
||||
r = 0.4 + step(0.5, r) * (0.6 - 0.4);
|
||||
```
|
||||
|
||||
逻辑上相当于若r >= 0.5 ,则:
|
||||
```glsl
|
||||
r = 0.4 + 1 * 0.2 = 0.6;
|
||||
```
|
||||
否则r < 0.5,则:
|
||||
```glsl
|
||||
r = 0.4 + 0 * 0.2 = 0.4;
|
||||
```
|
||||
由此即可消去if else语句。
|
||||
|
||||
|
||||
- 错误写法:
|
||||
(if else大量使用)
|
||||
```python
|
||||
// 简单的卡通着色例子,把连续的颜色值映射到几个特殊的离散的值上面
|
||||
//根据传入的颜色值取得一个新的颜色值, 这里为了展示简单,我们用仅用一个通道进行举例
|
||||
void main()
|
||||
{
|
||||
color.r = getNewRedColor(color.r);
|
||||
...(省略无关代码)
|
||||
}
|
||||
float getNewRedColor(float r)
|
||||
{
|
||||
float newR;
|
||||
if(r >= 0.6)
|
||||
{
|
||||
newR = 0.8;
|
||||
}else if(r >= 0.3)
|
||||
{
|
||||
newR = 0.5;
|
||||
}else{
|
||||
newR = 0.1;
|
||||
}
|
||||
return newR;
|
||||
}
|
||||
```
|
||||
|
||||
- 正确写法:
|
||||
(分开每帧只处理5个)
|
||||
```python
|
||||
// 简单的卡通着色例子,把连续的颜色值映射到几个特殊的离散的值上面
|
||||
//根据传入的颜色值取得一个新的颜色值, 这里为了展示简单,我们用仅用一个通道进行举例
|
||||
void main()
|
||||
{
|
||||
color.r = getNewRedColor(color.r);
|
||||
...(省略无关代码)
|
||||
}
|
||||
float getNewRedColor(float r)
|
||||
{
|
||||
float newR = 0.0;
|
||||
newR = newR + step(0.6, r) * 0.8;
|
||||
newR = newR + step(0.3, r) * step(r, 0.6) * 0.5;
|
||||
newR = newR + step(r, 0.3) * 0.1;
|
||||
return newR;
|
||||
}
|
||||
```
|
||||
|
||||
## 循环语句
|
||||
|
||||
for, while这类的循环语句内部实现其实也会有条件判断if else语句,并行性比较低,所以如果可以不用尽量不用,但这并不是让大家去复制粘贴多少次代码,这没有意义,则是尽量从逻辑上避免循环逻辑的出现,如果实在需要使用,则建议循环体内不要做太多耗性能的操作。
|
||||
除此之外,循环变量一定要记得初始化!变量的初始值在不同设备上有时候是不一样的, 比如int的初始值并不一定在所有设备上都是0。
|
||||
|
||||
- 错误写法:
|
||||
(循环变量i没有初始化)
|
||||
```python
|
||||
for(int i; i < 5; i ++)
|
||||
{
|
||||
func();
|
||||
}
|
||||
```
|
||||
在一些设备上i的值会被初始化为0,循环5次没有问题。但在一些设备上,i的默认值可能是一个没有意义的数,甚至是负数!例如是−2147483648,上面循环则会循环超级多次,玩家会直接卡到动不了。
|
||||
|
||||
- 正确写法:
|
||||
(分开每帧只处理5个)
|
||||
```python
|
||||
//这里i一定要给一个默认值
|
||||
for(int i = 0; i < 5; i ++)
|
||||
{
|
||||
func();
|
||||
}
|
||||
```
|
||||
|
||||
## 精美贴图开关
|
||||
|
||||
开关在游戏中的位置:设置->视频->精美贴图
|
||||
开发者可根据玩家是否开启精美贴图执行不一样的shader逻辑。这里需要声明两个文件,materials/sad.json 和 materials/fancy.json。我们先看下原版中两个文件的内容:
|
||||
```python
|
||||
sad.json:
|
||||
[
|
||||
{"path":"materials/sad.material"},
|
||||
{"path":"materials/entity.material"},
|
||||
{"path":"materials/terrain.material"},
|
||||
{"path":"materials/portal.material"},
|
||||
{"path":"materials/barrier.material"},
|
||||
{"path":"materials/wireframe.material"}
|
||||
]
|
||||
|
||||
fancy.json:
|
||||
[
|
||||
{"path":"materials/fancy.material", "+defines":["FANCY"]},
|
||||
{"path":"materials/entity.material", "+defines":["FANCY"]},
|
||||
{"path":"materials/terrain.material", "+defines":["FANCY"]},
|
||||
{"path":"materials/hologram.material"},
|
||||
{"path":"materials/portal.material", "+defines":["FANCY"]},
|
||||
{"path":"materials/barrier.material"},
|
||||
{"path":"materials/wireframe.material"}
|
||||
]
|
||||
```
|
||||
|
||||
开启精美贴图的时候会加载下面的材质,不开启的话加载上面的材质。我们用一个材质进行举例,比如sad和fancy中都有的terrain.material材质,fancy中不同在于额外定义了FANCY字段,则在Shader中可以这样写:
|
||||
```python
|
||||
void main()
|
||||
{
|
||||
#ifdef FANCY
|
||||
//这里可以做更多的逻辑,渲染更好的效果
|
||||
renderBeautiful();
|
||||
#else
|
||||
// 这里是关闭了精美贴图,这里不应该执行过多逻辑,只需要提供简单的显示效果就可以了
|
||||
renderSimple();
|
||||
#endif
|
||||
}
|
||||
```
|
||||
|
||||
## 降低精度
|
||||
|
||||
通常来说,我们写的shader不需要过于关注精度,因为大部分情况下性能瓶颈不在这里,但一些过于复杂,大部分玩家都说卡的MOD,建议可以考虑在精度方面做一些优化。
|
||||
|
||||
shader中变量精度越低,GPU运算越快,精度分为3档,关键字分别为:
|
||||
```glsl
|
||||
低:lowp
|
||||
中:mediump
|
||||
高:highp
|
||||
```
|
||||
|
||||
int(整数):建议256内的整数使用lowp, 1024内的整数使用mediump,其它情况则使用highp
|
||||
float(浮点):建议256内的浮点数使用lowp, 16384内的浮点数使用mediump,其它情况则使用highp
|
||||
|
||||
默认精度:
|
||||
```glsl
|
||||
顶点着色器中float, int均为highp
|
||||
像素着色器中int为mediump,float根据设备不同无默认精度
|
||||
```
|
||||
声明方法例子(直接把精度关键字加在变量类型前面):
|
||||
```glsl
|
||||
lowp float color
|
||||
```
|
||||
|
||||
## 移除无用变量与逻辑
|
||||
|
||||
部分开发者编写Shader的时候可能会偷下懒,从自己以前写的代码里面复制粘贴过来,但这部分代码可能有很多逻辑或者变量都没有用上,导致大量的运算是无意义浪费性能的,需要去掉。
|
||||
|
||||
- 错误写法:
|
||||
(包含大量无用逻辑)
|
||||
```python
|
||||
void main()
|
||||
{
|
||||
//这里声明多个变量,又或许是从其它地方复制过来,具体值先省略
|
||||
A = ...;
|
||||
B = ...;
|
||||
C = ...;
|
||||
|
||||
//这里只用了变量A;B和C都没有用到
|
||||
DoSomeThingWithA(A);
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
- 正确写法:
|
||||
(删除无用逻辑)
|
||||
```python
|
||||
void main()
|
||||
{
|
||||
//只留下A,把其它没用上的都删除掉
|
||||
A = ...;
|
||||
|
||||
//这里只用了变量A;B和C都没有用到
|
||||
DoSomeThingWithA(A);
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
## 分级做多个MOD版本
|
||||
|
||||
开发者可根据Shader复杂度上架不同版本,举个例子,例如可以有“网易光影低配版”, “网易光影高配版”,玩家看名字就大概知道对性能有不同的要求了,让玩家下载时自行选择。
|
||||
177
mcguide/20-玩法开发/18-性能优化/配置文件优化.md
Normal file
177
mcguide/20-玩法开发/18-性能优化/配置文件优化.md
Normal file
@@ -0,0 +1,177 @@
|
||||
---
|
||||
front: https://nie.res.netease.com/r/pic/20210728/5507b669-4c6f-4958-b5d0-b8556ab4cfb5.png
|
||||
hard: 进阶
|
||||
time: 20分钟
|
||||
---
|
||||
|
||||
# 配置文件优化指南
|
||||
|
||||
本文介绍了如何优化Python配置文件的内存使用。
|
||||
|
||||
## 1. 使用结构优化建筑生成
|
||||
|
||||
在建筑生成和放置方面,我们推荐使用结构文件而不是Python配置。
|
||||
|
||||
### 使用feature\_rules控制建筑生成
|
||||
|
||||
参考教程:[自定义特征](../15-自定义游戏内容/4-自定义维度/4-自定义特征.md)
|
||||
|
||||
**优点:**
|
||||
|
||||
* 无需代码处理,无卡顿效果
|
||||
* 采用微软原生机制,性能优异
|
||||
|
||||
**缺点:**
|
||||
|
||||
* feature\_rules文件中的molang表达式不宜过于复杂
|
||||
* 复杂的表达式可能导致内存占用增加,影响游戏加载速度
|
||||
|
||||
### 通过API放置structure
|
||||
|
||||
|
||||
参考API:<a href="../../../mcdocs/1-ModAPI/接口/世界/地图.html#placestructure" rel="noopenner">PlaceStructure</a>
|
||||
|
||||
**优点:**
|
||||
|
||||
* 提供灵活的代码控制能力
|
||||
* 可实现复杂的生成规则
|
||||
|
||||
**缺点:**
|
||||
|
||||
* 放置时可能出现短暂卡顿
|
||||
|
||||
## 2. 使用方块调色板节约内存
|
||||
|
||||
方块调色板可用于保存建筑数据。使用<a href="../../../mcdocs/1-ModAPI/接口/世界/方块组合.html#getblockpalettebetweenpos" rel="noopenner">GetBlockPaletteBetweenPos</a>等接口获取方块调色板后,使用<a href="../../../mcdocs/1-ModAPI/接口/方块/方块调色板.html#serializeblockpalette" rel="noopenner">SerializeBlockPalette</a>将调色板转化成字典后,手动存入配置文件中。
|
||||
|
||||
**优点:**
|
||||
|
||||
* 方块调色板是高度压缩的字典结构,可最大程度节约内存
|
||||
* 对应的放置接口<a href="../../../mcdocs/1-ModAPI/接口/世界/方块组合.html#setblockbyblockpalette" rel="noopenner">SetBlockByBlockPalette</a>是原生c++批量放置,性能较好
|
||||
* 可实现复杂的生成规则
|
||||
|
||||
**缺点:**
|
||||
|
||||
* 放置时可能出现短暂卡顿
|
||||
|
||||
## 3. 使用引用避免重复定义
|
||||
|
||||
在配置文件中,应避免重复定义相同的配置项,而是采用引用方式:
|
||||
|
||||
* 错误的写法
|
||||
|
||||
```python
|
||||
# 存在大量重复的方块数据定义
|
||||
building = {
|
||||
'build_A': {
|
||||
'size': [0, 1, 2],
|
||||
'blocks': [
|
||||
{'aux': 0, 'name': 'minecraft:air'},
|
||||
{'aux': 0, 'name': 'minecraft:air'},
|
||||
{'aux': 0, 'name': 'minecraft:air'},
|
||||
{'aux': 0, 'name': 'minecraft:air'},
|
||||
{'aux': 0, 'name': 'minecraft:air'},
|
||||
{'aux': 0, 'name': 'minecraft:air'},
|
||||
{'aux': 0, 'name': 'minecraft:air'},
|
||||
{'aux': 0, 'name': 'minecraft:air'},
|
||||
{'aux': 0, 'name': 'minecraft:air'},
|
||||
{'aux': 0, 'name': 'minecraft:air'},
|
||||
{'aux': 0, 'name': 'minecraft:air'},
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
* 正确的写法
|
||||
|
||||
```python
|
||||
# 定义基础方块数据
|
||||
AirBlock = {'aux': 0, 'name': 'minecraft:air'}
|
||||
GrassBlock = {'aux': 0, 'name': 'minecraft:grass'}
|
||||
StoneBlock = {'aux': 0, 'name': 'minecraft:stone'}
|
||||
|
||||
# 通过引用复用方块数据
|
||||
building = {
|
||||
'build_A': {
|
||||
'size': (0, 1, 2),
|
||||
'blocks': [
|
||||
AirBlock, AirBlock, StoneBlock, AirBlock, AirBlock,
|
||||
GrassBlock, AirBlock, GrassBlock, AirBlock, AirBlock, StoneBlock,
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 使用元组替代字典
|
||||
|
||||
对于只读的配置数据,建议使用元组代替字典,以提高内存使用效率:
|
||||
|
||||
* 错误的写法
|
||||
|
||||
```python
|
||||
# 定义基础方块数据
|
||||
AirBlock = {'aux': 0, 'name': 'minecraft:air'}
|
||||
GrassBlock = {'aux': 0, 'name': 'minecraft:grass'}
|
||||
StoneBlock = {'aux': 0, 'name': 'minecraft:stone'}
|
||||
```
|
||||
|
||||
* 正确的写法
|
||||
|
||||
```python
|
||||
# 使用元组存储只读数据,提高内存效率
|
||||
AirBlock = ('minecraft:air', 0)
|
||||
GrassBlock = ('minecraft:grass', 0)
|
||||
StoneBlock = ('minecraft:stone', 0)
|
||||
```
|
||||
|
||||
## 5. 分割配置文件并动态加载
|
||||
|
||||
如果你的配置文件已经有几十M的大小,建议将配置文件按功能模块分割,采用动态加载方式:
|
||||
|
||||
* 错误的写法
|
||||
|
||||
```python
|
||||
# 一次性导入所有配置
|
||||
from build import build1, build2, build3
|
||||
```
|
||||
|
||||
* 正确的写法
|
||||
|
||||
```python
|
||||
def load_build_config(data):
|
||||
building = data['build']
|
||||
|
||||
if build == '1':
|
||||
# 按需导入配置
|
||||
import build1
|
||||
place_build(data, build1)
|
||||
elif build == '2':
|
||||
# 按需导入配置
|
||||
import build2
|
||||
place_build(data, build)
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
* 建议将一个模块切割得尽量细,并结合前面所述的方法减少单个模块的大小,否则动态加载模块也会带来卡顿。
|
||||
* 即便使用动态加载,如果玩家将各个类型的建筑都摆放一次,那么所有配置也会被加载进来,大量占用内存。
|
||||
* 如果想进一步优化,可以将配置存储为json文件,通过json方式使用和卸载,相关接口<a href="../../../mcdocs/1-ModAPI/接口/通用/工具.html#getmodconfigjson" rel="noopenner">GetModConfigJson</a>。
|
||||
* 可以使用`zlib`库压缩数据。
|
||||
|
||||
## 优化效果
|
||||
|
||||
**优化前内存占用715.32M、优化后占用下降到了224.46M**
|
||||
|
||||
优化前的内存占用(使用方块探针工具):
|
||||

|
||||
|
||||
优化后的内存占用(使用方块探针工具):
|
||||

|
||||
|
||||
## 总结
|
||||
|
||||
1. 优先使用结构文件/方块调色板存储大型数据
|
||||
2. 使用类组织相关配置
|
||||
3. 采用引用而不是重复定义
|
||||
4. 使用元组存储只读数据
|
||||
5. 按功能分割配置文件
|
||||
Reference in New Issue
Block a user