本文转载自公众号《读芯》(ID:AI_Discovery)先看两段超级简单的代码。foriinrange(10**7):x=i%5代码1:简单代码defmain():foriinrange(10**7):x=i%5main()代码2:定义一个main函数来运行相同的简单代码.两个代码都执行一个虚拟任务。取一个0到1000万之间的数(通过for循环)并计算它的模(余数)5,到目前为止操作非常简单。那么,测量代码的运行时间是多少?importtimestart_time=time.time()foriinrange(10**7):x=i%5finish_time=time.time()print("Duration:{}msec".format((finish_time-start_time)*1000))添加一个简单的代码1中的计时器importtimedefmain():foriinrange(10**7):x=i%5start_time=time.time()main()finish_time=time.time()print("Duration:{}msec".format((finish_time-start_time)*1000))在代码中添加一个简单的定时器2在两个代码中添加一个简单的定时器来测量各自的运行时间。由于两个代码都执行相同的简单任务,因此估计的运行时间也相同。当然,如果运行时间真的一样,就没有这篇文章的必要了。事实上,代码1和代码2分别运行了739毫秒和434毫秒。惊喜!很多Python程序员都不知道这个难题,因为它需要深入了解Python的工作原理。本文将回答“当你运行python代码时会发生什么?”,重点关注非常流行的Python工具CPython。如果您不知道自己使用的是什么Python工具,那么90%的时间您都在使用CPython。下面是运行源代码时发生的情况:首先,源代码被“词法分析”程序分解为标记,例如,x=1将被分解为x、=和1。然后,通过“语法分析”的过程,将这些标记组织成抽象语法树(AST),之后“编译器”将所有内容转换为称为“字节码”的抽象代码。在Python中,与C、C++、Java等语言不同,编译器不会获取“源代码”并将其转换为“机器代码”,理解这一点很重要。相反,编译器获取“源代码”并将其转换为“字节码”。解释器的工作是获取字节码并以机器可以理解的方式运行它。在Python运行代码的四个步骤中,解释器做的工作最重。而其他三个步骤并没有处理太多的任务。因此,无论何时你想要调查Python程序的性能,你都应该查看解释步骤并寻找一些线索。解释器读取字节码并执行其指令。如果字节码就像一个菜谱,那么指令就是菜谱中的不同步骤。如果可以读取字节码,或许可以从中找到一些解开上述谜题的线索。使用dis包查看字节码指令。dis是一个Python包,用于分析和解码字节码并以人类可理解的方式显示它。dis.dis()的输出结构如下:本文不详细介绍dis包的细节,只关注OperationNamed这一栏。操作名称表示Python解释器的行为。如果你很好奇,一个叫ceval.c的文件可以回答。以上两个代码都运行dis.dis()。为了简化操作,本文着重介绍了重要的部分,即循环部分。下图显示了两种代码的字节码:如图所示,两种代码在给定的指令方面非常相似。然而,仔细观察会发现字节码中存在一些细微(但重要)的差异。在代码1中,您可以看到STORE_NAME和LOAD_NAME,但在代码2中,您可以看到STORE_FAST和LOAD_FAST。运行时的差异似乎是由于两种指令类型之间的差异。有关差异,请参阅ceval.c文件。简而言之,在代码1中,解释器处理变量i和x的方式与代码2中的不同(注意_NAME和_FAST后缀)。在代码1中,i和x是全局变量,CPython将这些变量存储在字典数据结构中,这使得加载过程比存储在固定大小数组中的局部变量花费的时间更长。从固定大小的数组中检索变量比字典快得多。为什么Python会这样做?简单,因为在主代码中,不知道会出现多少个变量,但是一个函数中的变量个数是固定的。如果是这个原因,让我们做一个测试:打乱解释器,在清单2(快速代码)中将x和i变量定义为全局变量,然后再次测量运行时间。下面是修改后的代码2:defmain():globali,xforiinrange(10**7):x=i%5main()代码3和代码2一样,只是定义了变量i和x,看是否全局变量是困难代码中性能低下的原因。运行代码3花费了805毫秒(代码2花费了434毫秒)。代码3非常接近代码1(即739毫秒)。正如预期的那样,全局变量比局部变量(固定大小的数组和字典)花费更多的时间。如您所见,只要了解一点Python解释器的工作原理,并从dis库中获得帮助,这个难题就可以轻松解决。
