如题,联动一下自己去年发的帖吧。
Python 因为本身比较慢,我觉得应该大多数程序员,跟我一样,写东西时都有最优化实现的需求。典型场景比如生产环境遇到连接两个大的字节流,用那种实现比较快?随便一想就有很多种写法,比如
ret = b'1'+b'2'
# 或
ret = bytearray();ret += b'1';ret += b'2'
# 或
ret = ''.join([b'1', b'2'])
再比如生成一个静态元组时是使用列表更快还是元组更快。等等等等不一而足,总之生产环境写码时是避免不了各种测试的,大概也是 py 独一份的问题了。
大家都知道 python 自带有 timeit 模块用来计时足以应付上述场景。但是我一直觉得 timeit 很不好用。首先需要引入包,然后把需要测试的部分单独封装起来,最后还要重载入才能得到结果,尤其在面对小型测试时很麻烦,所以我往常通常在类似测试时更喜欢自己封装一个上下文管理器做替代。由于 python 的上下文管理器生成新的 block 但不生成独立 scope ,不需要进行剥离封装,也不需要担心对原功能产生任何影响,总之不用动用任何脑细胞,非常哈皮。
也就是大概变成这样:
>>> foo = [b'1'] * 100
>>> with timeit():
>>> for _ in range(1000000):
>>> bar = ''.join(foo)
[line 1] time cost: 0.013489246368408203
唯一一点不太爽的是,测试时需要自己写 for 循环,多打一行不短的代码,在深度思考场或者大量测试的场合下会让人非常烦躁。
所以去年发了个帖子,问问有没有大佬知道奇技淫巧,可以以 hook 的方式提前下个钩子把管理的内容提取出来,这样就不用写烦人的 for _ in range 了,可惜当时讨论了一下大家都没有思路。昨天周末摸鱼时间突然开窍,按照 hack 进字节码解释器在 python 里实现 goto 的思路摸索了一下,写了一会搞了个雏形。应该还是有不少坑,欢迎大家测试:
pip install git+https://github.com/GoodManWEN/pipeit.git@test_install
大体思路是通过 py 的高动态特性和反射功能,可以抽取当前 scope 的状态流然后动态生成一份字节码,再然后再反过来实例化,这样就达到了提取出上下文管理器中间部分的目的。实现上到也说不上是很优雅,但是因为依赖的都是久经测试的 bil ,所以同样也说不是上是很肮脏。
目前的 demo 可以做到以下的用法:
>>> # 前两年在 v2 很多人讨论的用魔术方法重载运算符,达到解放传统 py 函数式痛苦的反写体验的功能
>>> # 但由于涉及到对象和方法调用,理所当然会比原生慢一些,假设我们现在想知道具体慢多少
>>> from pipeit import *
>>>
>>> foo = list(range(50))
>>> with timeit(1e6): # 循环百万次,自动转换结果到单轮时间
>>> bar = foo | Filter(lambda x: x%3==0) | Map(lambda x: x*10) | Reduce(lambda x, y:x+y) | int
[line 6] time cost per loop: 8.272400856018066μs
>>> bar
4080
>>> with timeit(1e6):
>>> bar = reduce(lambda x,y:x+y, map(lambda x:x*10, filter(lambda x:x%3 == 0, foo)))
[line 10] time cost per loop: 5.8983259201049805μs
>>> bar
4080
也就是直接 with timeit(循环次数):再加 tab 键就可以对单独行进行测速了。 在我的机器上跑的结果来看原生是比封装版快了 30%左右,不过考虑到本身也是 1 微秒级的差距,我通常开发中还是喜欢使用修改后的写法。
使用场景:
- 对少量代码进行定性类的,非严肃执行时间比较。
- 由于中间块是直接由字节码生成,理论上只多出了函数栈调用的时间,存在一定误差,但也可以忽略不计,所以其实感觉上做严肃比较也没啥问题。。
目前的问题:
- 执行上有一个不优雅的问题是固定会多执行一次,比如以 timeit(100)创建 block ,实际上 block 会被执行 101 次,因为解释器本身按照 with 逻辑执行 block 内部内容没办法把它拦截掉。
- 由于将 block 行为转化成了 scope 行为(实际上定义了函数),代码实质功能有所变化。为了准备相同的上下文环境,会在触发上下文管理器时所有的 scope 里将原 scope 内容重新执行一遍,给人一种很肮脏的感觉。考虑过另一种实现方式是将 locals 全部转化为 globals 再实例化,不过想了想感觉似乎问题更多。有待论坛老哥提供更好的解决方案。
- 目前的实现方案中存在一个严重问题是依赖于 python 动态分析生成字节码的能力。但转化为字节码后显然就丢失了原代码信息,比如你很难定位字节码的 xx 位置对应代码里的第 yy 行,这导致在同一个 scope 内多次执行计数时他们的对应关系会消失,实际上只能执行最初遇到的 timeit 块。所以上文中 demo 里的内容实际上无法实现,我实际操作时每次只能执行一种情况。感觉挺硬伤的,不知有无老哥提建议改善。当然分置于不同 scope 的话是互相不会影响的,不过如果要定义 scope 的话也需要写代码,又绕回去了。
- 由于目前的实现方式并未对栈进行规范,(因使用上通常是以 from import * 的形式来解放双手,但同时希望模块尽可能的轻量,占用可以忽略不计的内存),所以只是单纯对字节码进行裁剪,不支持嵌套使用。虽然感觉计时这种东西嵌套并无意义。。也许有用,等待老哥补充。
- 字节码不是大佬,照着标准书实现了一圈,可能在复杂环境下会产生各种奇怪 bug ,欢迎各种测试。
- 开发时使用的是 win 平台,多平台字节码解释器实现有没有坑我不知道,版本上有没有坑我也不知道。。由于 py 部分 api 变动,比如 CodeType 接收参数从 py3.6 的 str 变成了 3.7 的 bytes 。理论上应该是有做支持,具体能不能跑我不知道
总之还是有很多问题,权当抛砖引玉吧。希望以后 v 友遇到测速都能少写几行代码