为什么在动态编译的Python代码中,空函数调用速度慢15%?

7 投票
1 回答
749 浏览
提问于 2025-04-17 06:38

这其实是个很小的优化问题,但我只是好奇一下。在“真实”世界中,这通常没有太大区别。

我在用 compile() 编译一个什么都不做的函数,然后用 exec 来执行这段代码,并获取我编译的函数的引用。接着,我执行这个函数几百万次并测量时间。然后再用一个本地函数重复这个过程。为什么在 Python 2.7.2 中,动态编译的函数调用速度大约慢了 15% 呢?

import datetime
def getCompiledFunc():
  cc = compile("def aa():pass", '<string>', 'exec')
  dd = {}
  exec cc in dd
  return dd.get('aa')

compiledFunc = getCompiledFunc()  
def localFunc():pass


def testCall(f):
  st = datetime.datetime.now()
  for x in xrange(10000000): f()
  et = datetime.datetime.now()
  return (et-st).total_seconds()

for x in xrange(10):
  lt = testCall(localFunc)
  ct = testCall(compiledFunc)
  print "%s %s %s%% slower" % (lt, ct, int(100.0*(ct-lt)/lt))

我得到的输出大概是这样的:

1.139 1.319 15% slower

1 个回答

11

dis.dis() 这个函数可以显示每个版本的代码对象是一样的:

aa
  1           0 LOAD_CONST               0 (None)
              3 RETURN_VALUE        
localFunc
 10           0 LOAD_CONST               0 (None)
              3 RETURN_VALUE 

所以,区别在于函数对象。我对每个字段(比如 func_doc、func_closure 等)进行了比较,发现唯一不同的是 func_globals。换句话说,localFunc.func_globals != compiledFunc.func_globals

使用你自己的字典而不是内置的全局字典是有代价的(前者在每次调用时创建堆栈帧时需要查找,而后者可以直接被已经知道默认内置全局字典的 C 代码引用)。

这很容易通过将你的代码中的 exec 行改为以下内容来验证:

exec cc in globals(), dd

通过这个改变,时间差就消失了。

谜团解开了!

撰写回答