基于 python 的 opcode 优化和模块按需加载机制研究 (学习与思考)
姓名: XXX
学校信息: XXX
主用编程语言: python3.5
个人技术博客: http://www.cnblogs.com/Mufasa/
文档转换为 PDF 有些图片无法完全显示, 请移步我的博客查看
完成时间: 2019.03.06
本项目希望您能完成以下任务:
- 优化 python 字节码解析代码, 从底层提升 python 脚本运行效率;(底层, 编译器, 虚拟机)
- 基本思路可以统计游戏常用 opcode 指令, 进行类似 opcode 合并, opcode 排序;
- 另外, 可以研究下指令预测相关资料, 比如 indirect threading, 寻找更优的机制;(自然语言处理里面的东西好像可以用, 类似语言翻译)
- 为了缩短应用的启动时间, 需要在应用启动时, 把模块进行按需加载 (或者延迟加载, lazy import);(优化启动项)
- 目前的不同实现主要是针对 Python 标准库进行处理, 对第三方扩展库, 尤其是游戏引擎相关的扩展支持不好, 甚至无法支持;(软件适配 & 通用化)
- 此课题不仅有一定的学术研究意义, 更在手游等 App 中有很好的实用价值;(意义价值)
- 希望在自适应学习的基础上, 能够做到按需加载.(自适应)
一, Python 字节码
Python 源代码文件以. py 结尾, 字节码文件以. pyc 结尾; 其中字节码文件在一个叫__pycache__的子目录中, 它可以避免每次运行 Python 时去重新解析源代码.
图 1 python 代码运行过程
Python 执行的四步操作:
1,lexing: 词法分析, 就是把一个句子分解成 token. 大致来说, 就是用 str.split() 可以实现的功能.
2,parsing: 解析, 就是把这些 token 组装成一个逻辑结构.
3,compiling: 编译, 把这个逻辑结构转化成一个或者多个 code object(代码对象)
4,interpreting: 解释, 执行每个 code object 代表的代码.
其中前三步可以归类为 "代码编译", 最后一步单独成类. Python 程序的执行过程就是, 它先把代码编译成 bytecode(字节码) 指令, 交给虚拟机, 逐条执行 bytecode 指令.
分清 function object,code object, 以及 bytecode
1function object: 定义一个函数之后, 它就成了一个 function object(函数对象). 只要不使用函数调用符号 -- 也就是小括号 -- 这个函数就不会执行. 但是它已经被编译了, 可以通过这个 function object 的__code__属性找到它的 code object
2code object:code object 的类型是'code'
3bytecode:bytecode 是 code object 的一个属性的值. 这个属性名为 co_code, 它的类型是'bytes', 长度是 8. 例: b'|\x00\x00d\x01\x00\x14S'
实例 1:
- >>> def double(a):
- return a*2 # 并不知道为什么贴在这里缩进会是这样
- >>> import dis
- >>> dis.dis(double)
- 2 0 LOAD_FAST 0 (a)
- 2 LOAD_CONST 1 (2)
- 4 BINARY_MULTIPLY
- 6 RETURN_VALUE
第 1 列的 2 源代码中的行号; 第 2 列的数字 0 3 6 7 是 bytecode 的偏移量; 第 3 列很好理解, 都是 opcode.
因为可以节省编译时间, 这里有一篇非常详细的文章, 作者在遗传编程领域工作, 发现他们 Python 程序的总运算时间中, 有 50% 都被编译过程吃掉. 于是作者深入到 bytecode 层次进行了小小改动, 大幅削减了编译时间, 把总的运算时间降至不足原先的一半.(有改进的潜力)
猜想的优化方向
1, 从字节码 bytecode 上下手
图 2 python 字节码优化猜想 1 - 代码级优化
优点: 有一些固定的套路可以使用并且实施起来比较简单, 例子: 累加可以直接将很多分步直接在同一次处理中进行, 节省步数
缺点: 优化后的效率, 不能达到量级变化
2, 从串行转并行入手 (多线程, 多进程, 多核心)
图 3 python 字节码优化猜想 2 - 处理方式优化
优点: 需要从 python 解释器底层进行重新布局
缺点: 优化的效率可以成倍数提升, 并且效率与相关硬件有一定关系, 参考 nvidia 的 pascal 架构的并行计算卡
前景: 现在的手机芯片, 电脑芯片大都是多核心, 多进程的, 这个可以一试.
我自己之前去实习的公司中船重工 709 所凌久电子, 设计过一台拥有 256 颗 C66X 核心的 DSP 处理机, 这台机器这个就是实时, 并发计算的, 耗电快赶上空调, 但是性能真的很强很强!!!(我在简历里面写过)
(注: 我只是最近两天看了一下相关的文档资料, 我现在不确定 python 解释器是否已经在内部集成并行处理的功能)
参考:
用 Python 实现多核心并行计算 https://www.cnblogs.com/pdev/p/5267720.html
浅谈多核 CPU, 多线程与并行计算
3, 从 python 解释器层级进行优化
图 4 python 字节码优化猜想 3 - 解释器层级
之前的两个都是不触及 python 最底层的东西, 这里是从最底层进行优化的思考.
如上图 4 可知, 我们现在常使用的 CPython 解释器是通过 C 语言进行二级运行的, 这就和 Android 虚拟机一样, 一台机器上运行另一个环境, 当我们想要改变什么的时候还需要通过中介来通知做出改变, 这个就和两个人隔着墙通过手机来通话, 但是这样不如我们面对面沟通的明了!!!
优点: 可以省去中间的很多步骤, 直接对计算机硬件进行操作, 效率提升至 C 语言那般畅快
缺点: 开发难度大, 计算机越接近底层开发难度越大, 这需要一个团队来进行.
参考: Python 解释器
总体参考链接:
理解 Python 的执行方式, 与字节码 bytecode 玩耍 (上) https://www.cnblogs.com/hello2764/p/5459758.html
理解 Python 的执行方式, 与字节码 bytecode 玩耍 (下) http://www.cnblogs.com/hello2764/p/5466945.html
Fun with Python bytecode
二, opcode 指令
根据上文中的 bytecode 以及其附属的给人类理解查看 opcode.opcode 又称为操作码, 是将 python 源代码进行编译之后的结果, python 虚拟机无法直接执行 human-readable 的源代码, 因此 python 编译器第一步先将源代码进行编译, 以此得到 opcode. 例如在执行 python 程序时一般会先生成一个 pyc 文件, pyc 文件就是编译后的结果, 其中含有 opcode 序列. Opcode 和 bytecode 是有一定相关性的两种不同表述.(这里不做累赘表述)
python 的目标不是一个性能高效的语言, 出于脚本动态类型的原因虚拟机做了大量计算来判断一个变量的当前类型, 并且整个 python 虚拟机是基于栈逻辑的, 频繁的压栈出栈操作也需要大量计算.
缺点即为可能的改进方向!
参考链接:
深入理解 python 之 Opcode 备忘录 https://www.jianshu.com/p/557cfe36f0f0
理解 Python 之 opcode 及优化 https://www.jianshu.com/p/f45e443cdfd7
操作码定义 opcode.h https://www.jianshu.com/p/f540e540f940
Peephole optimization
三, 指令预测
这个可以参考自然语言处理 (NLP), 指令是计算机的语言, 自然语言是人类的语言, 这两种语言都有自己需要表达的意思. 如果未来机器有了自我意识那么指令预测和我们自然人的语言词句预测又有何分别?!!
自然语言处理中的词句预测可以迁移到代码的指令预测.
参考链接:
[PaddlePaddle] 自然语言处理: 句词预测 https://www.cnblogs.com/dzqiu/p/9599062.html
Wikipedia-Threaded code https://en.wikipedia.org/wiki/Threaded_code
高性能虚拟机解释器: DTC vs ITC(Indirect-Threaded Code)
- Dynamically Disabling Way-prediction to Reduce Instruction Replay https://ieeexplore.ieee.org/document/8615679
- The research of indirect transfer prediction technology based on information feedback https://ieeexplore.ieee.org/document/5953289
四, 模块按需加载
Python import 原理:
图 5 import 运行大致原理
使用 import module_name 语句就可以将这个文件作为模块导入. 系统在导入模块时, 要做以下三件事:
1. 为源代码文件中定义的对象创建一个名字空间, 通过这个名字空间可以访问到模块中定义的函数及变量.
2. 在新创建的名字空间里执行源代码文件.
3. 创建一个名为源代码文件的对象, 该对象引用模块的名字空间, 这样就可以通过这个对象访问模块中的函数及变量.
普通加载方式:
文件抬头就对所有所需的库进行加载, 这样的缺点是耗时 (尤其是对快应用, 启动速度有要求的程序很敏感)
以前的两种惰性 / 延迟加载方法:
1本地子功能区加载而非程序启动时的全局加载. 直到你的程序运行需要这个库的时候才进行加载; 缺点: 易重复载入库文件, 容易遗忘库载入的范围.
2惰性加载. 需要模块的时候触发 ModuleNotFoundError 提前发现这个模块, 而延迟的只是后续补加载过程; 缺点: 显式优于隐式, 如果一个模块希望立即加载, 那么在延迟加载时, 它可能会严重崩溃.(Mercurial 实际上开发了一个模块黑名单, 以避免延迟加载来解决这个问题, 但是他们必须确保对其进行更新, 因此这也不是一个完美的解决方案.)
最新 py3.7 中的方法:
在 Python 3.7 中, 模块现在可以在其上定义__getattr__(), 允许编写一个函数, 在模块上的属性不可用时导入模块. 这样做的缺点是使它成为一个惰性导入而不是一个加载, 因此很晚才发现是否会引发 ModuleNotFoundError. 但是它是显式的, 并且仍然是为您的模块全局定义的, 因此更容易控制.
改进方向: 发现导入错误被推迟, 如何提前获知这个可能出现的导入错误防止程序抛出异常并终止.
缺点很明显啊! 当你用的时候才开始加载, 这个会锁住主线程进行库加载动作, 如果是带有画面的操作, 那么就会有视觉延迟 (假设这个加载是第一次运行, 且很耗时)
改进: 能不能在主线程旁边开一条线程提前进行预加载!!!
- import importlib
- # 这个是实现 lazy_import 的功能函数
- def lazy_import(importer_name, to_import):
- module = importlib.import_module(importer_name) # 直接加载调用的后一级函数
- import_mapping = {} # 字典 键名: 有可能为缩写名 值名: 为原始可查找库名, 例如: import_mapping['np'] = 'numpy'
- for name in to_import:
- importing, _, binding = name.partition('as')
- if not binding:
- _, _, binding = importing.rpartition('.')
- import_mapping[binding] = importing
- def __getattr__(name):
- if name not in import_mapping: # 如果这个库没在 import_mapping 中, 就抛出异常错误, 并且中断
- message = f'module {importer_name!r} has no attribute {name!r}'
- raise AttributeError(message)
- importing = import_mapping[name]
- imported = importlib.import_module(importing,module.__spec__.parent)
- # print('name=',name,'module=',module,'module.__spec__=',module.__spec__,'module.__spec__.parent=',module.__spec__.parent)
- setattr(module, name, imported) # sub, np, numpy
- return imported
- return module, __getattr__ #返回一个库和一个方法
详情见网址: lazy_import 源码解析 (原创) https://www.cnblogs.com/Mufasa/p/10482923.html 我自己的博客
现在的思路:
图 6 按需预加载
图 7 按需预加载运行逻辑
粗糙的实现代码 1:preload.py
优点:
将 preload 在需要的函数之前运行 (这个是多线程的加载方式, 不会锁定主线程的相关计算, 同时在计算机 IO 空闲的时候加载, 见缝插针进行. 提高程序运行效率), 在后面需要相关函数时就直接调用这个功能即可
缺点:
1预加载的代码提前多少, 这个我现在还没有办法说清, 要具体看机器的计算时间;2不太人性化, 需要人为或者程序转换原始. py 程序.
总地来说也是一种尝试! 后面还可以试试其他的多种方法解决.
- import threading
- from importlib import import_module
- # 可以返回值的功能函数
- class MyThread(threading.Thread):
- def __init__(self, func, args, name=''):
- threading.Thread.__init__(self)
- self.name = name
- self.func = func
- self.args = args
- self.result = self.func(*self.args)
- # 返回一个函数
- def get_result(self):
- try:
- return self.result
- except Exception:
- return None
- def module_before(module_name):
- t = MyThread(import_module, [module_name], import_module.__name__)
- t.start()
- return t
- if __name__ == '__main__':
- numpy_mid = module_before('numpy')
- numpy = numpy_mid.get_result()
- print(numpy.array([1, 2, 3, 4]))
代码段 2: 测试_preload.py
- import preload as pld
- # 这里面的预加载是放在需要这个函数的前面的相关代码块前,
- modules = ['numpy', 'sys', 'os']
- module = {}
- for i in modules:
- module[i] = pld.module_before(i)
- np = module['numpy'].get_result()
- print(np.array([1, 2, 3, 4]))
参考链接:
__getattr__使用方法 https://www.cnblogs.com/whigym/p/9858165.html
setattr() 函数
An approach to lazy importing in Python 3.7 https://snarky.ca/lazy-importing-in-python-3-7/
动态加载 lazy_import(利用__import__)
python 之 import 机制 https://www.cnblogs.com/kungfupanda/p/5257180.html
动态导入对象, importlib.import_module() 使用
关于 Python 的 import 机制原理 https://www.cnblogs.com/jayliu/p/9011817.html
备注: 这个里面的思路应该是有些问题的, 毕竟自己也不是专业的, 欢迎大家讨论指教.
来源: http://www.bubuko.com/infodetail-2978808.html