My Reaction to "Under Discussion: The Performance of Python"

Today I read an article Under Discussion: The Performance of Python, and discussed it with some friends. I got some ideas from the discussion, so I decided to write an article.

My overall reaction to this article is: it points straight to some of the key factors that prevent Python to gain major performance improvements, thus really worth reading.

In the article, the author interviewed Python core developer Victor Stinner (VS) and experienced Python developer Julien Danjou (JD). Their names should be familiar to those who's been in the Python community for a while. Unlike some other articles that talk about the technical reasons of why Python is slow, this article focused on the efforts made and difficulties met to improve Python's performance.

To start, let me quote the part that I find most interesting:

VS: Now, there are different reasons behind Python’s slowness. The first is the official statement, which is that there is no such performance issue! Saying this is preventing us from working on the issue.

It blew my mind when I saw this. You know it, I know it, but as a core dev, you still need a bit of courage to say that in public. I appreciate what Victor said, and I'm 1000% agree with it.

JD: I agree with Victor. I don’t see a lot of people working upstream on Python who are trying to improve its performance, which is a shame considering the number of people who use the language and want better performance from it in general.

(Julien has clarified that he was referring to big companies not investing enough in Python.)

I think it's too harsh to say that. I respect the work people (especially core devs) did, and they've made Python better than it ever has been. My friend @uranusjr reacted more strongly. He said:

I don’t see why it’s a shame. If you want it to improve, feel free to contribute. Core developers don’t owe you anything.

This brings to the main topic I want to talk about today.

The mindset of "If you want it to improve, feel free to contribute" does not help

Don't get me wrong. I agree with the word. What I'm saying is, it's not helpful. I'll explain it later.

As you might know, I hosted a Python podcast in my free time, and we once invited Xiang Zhang, the only core developer from China. One thing he said in the show still strikes me until today. He said, Python's evolvement pretty much relies on 个人英雄主义 (individualistic heroism), and that's why it's so hard to make big improvements.

The real problem behind the scenes is the mindset. We're so used to relying on individualistic heroism, relying on "feel free to contribute", and almost forgot that contributing to a project as complicated as Python, is hard. Python does have a comprehensive Developer’s Guide, which is nice, but it's not enough. We need to change the mindset from "If you want it to improve, feel free to contribute" to "If you want it to improve, we'll make it easy for you to contribute". This is important because, once we build this new mindset, it is obvious that a lot more can (or should) be done other than having a good developer guide. I'll list a few that came to my mind.

Documentation

Again, it's documentation. So far I've made 4 commits to improve, but there are a lot more problems out there. Reading documentation is often the first step to start contributing. If the documentation itself is wrong or outdated, it would confuse people and scare them away. The more problems the documentation has, the less people care about fixing it, since it's "already broken", and that's the so-called "broken windows theory".

Specification

Bugs in documentation sometimes reveal a bigger problem: lack of specification. Personally, I think this is THE MOST important issue that Python needs to solve. To give an example, there is no formal definition for "value stack", but the word "stack" is used everywhere in bytecode's documentation, which represents the "value stack". Sometimes I got the feeling that the boundary between (C)Python's behavior and its implementation is vague, which makes it hard for both people to use and improve the language. Sure CPython is the reference implementation, but perhaps it can do better?

This topic is a bit beyond the scope of this article and I'm not an expert either, so I'll link to a recent discussion on Python Language Summit 2020: A formal specification for the (C)Python virtual machine.

Tools

When I was talking to Xiang Zhang, he mentioned he wish Python has gdb's functionalities built-into the language. I didn't understand why this is important, until one day I tried to use gdb on Mac to trace code execution in Python's VM. It was painful, and I didn't get it to work. Later I found that gdb has quirks on Mac and does not work well in certain cases. If Python provides this functionality, then perhaps Mac and Windows users are more likely to contribute?

Xiang also mentioned that he wish Python has hooks that provide memory usage and thread information. Could this be the consequence of lacking a formal specification, or simply because the community lacks interest in them? I don't know. But I do know these tools (if they exist) will not only benefit application developers but Python contributors as well.

Roadmap

This is the hardest topic, and I honestly don't feel qualified to talk about it. But it has been on my mind for quite a long time, so I might as well give my two cents.

If you observe the lifetime of a programming language, there are two typical trajectories. If the language failed to gain enough popularity in the first few years after it was created, it gradually fades away. If it did, often with a few killer apps or certain traits that other languages don't have at the time, it becomes "mainstream". Once it gets there, its lifespan suddenly grows from less than a few years to, say, at least 30 years, or even longer (remember the call for COBOL programmers?).

One problem arises with the long lifespan, that is, the language gradually becomes outdated. Why does this happen? Because human is progressing, errors are learned and new ideas are created. The creators of new languages learn from existing ones, avoid their mistakes, and adopt new ideas from academia and industry. Most will fail, but there are always a few who managed to create a better language. You'll probably argue that there's no language that's perfect in every way, and there are always tradeoffs, which is true. But hey, making better tradeoffs is also a technique, and people learn it as well. Meanwhile, we should not forget that old mainstream languages are also being improved constantly, but just as any other legacy system, having to keep backward compatibility slows this progress down, and even made certain improvements impossible.

Python is right at this stage. It has become one of the most popular programming languages, but some new languages are trying to take its place, little by little. It is unavoidable and we have to accept it, then take action. But how? C++ gives a great example. Take a look at this document:

Direction for ISO C++

And its abstract:

This is intended as a document updated as times go by and new facts and opinions emerge. It tries to articulate our aims for C++, the challenges faced, and some concrete suggestions for priorities.

This document differs from most papers by aiming for a global view and placing individual features in context, trying to avoid delving into technical details of individual proposals except where that “details” could affect the language as a whole or its tool environments.

To prevent this document from becoming toothless “pure philosophy,” we propose giving priority to specific areas of development and to specific proposals in those areas.

I know, people are complaining about C++'s evolvement being slow, but let's not forget C++ is probably the most complicated language with way more historical burden than languages like Python. Despite all that, I feel this kind of "long term road map" is what Python needs. Again, there are tradeoffs here. Python does not have a committee, and everybody pretty much works on its own, which allows it to move fast. However, I've seen many people say that, despite Python gaining more and more features, they don't feel like upgrading the version they've been using, because there isn't much big of a difference. Many say they'll stick with 3.6, cause that's a version with major improvements (f-string, compact dict). While I know this is not true, that recent Python versions do carry many improvements they should care, I still think it is dangerous that people start to think that way. Part of the reason, IMHO, is because Python does not have a real road map, therefore most contributors just tend to focus on low-hanging fruits. There's nothing wrong with making small improvements, but certain aspects, like performance improvements, is not small, and it can only happen if there's careful long-term planning and thinking, aka, a roadmap. A roadmap is not a magic scroll, it does not turn Python into a better language right away. In fact, it's a long process, and will probably take years before we can tell the difference. However, that's also the reason why Python needs it so urgently. Years can pass in the twinkling of an eye, if we don't do something now, in five years, the world can be totally different from what we see today.

Edit: Ruby is similar to Python in many ways, except it has a roadmap. See Matz - Ruby 3 and Beyond.

Summary

This article is way longer than I expected, so I'll do a quick summary. In our latest show, my friend who currently works in the Google New York office talked about what he thinks makes a good manager. He said a good manager should create an environment for you to work effectively. And I think that's also true for Python.

Python 的 block stack

网上关于 Python block stack 的文章少得可怜,中文文章更是基本搜不着。block stack 是 CPython 中的重要概念,但其实现又比较繁琐,因此我会先跳过细节,讲一讲为什么需要 block stack。之后会贴一些链接,里面有更细致的讲解。本文假定读者对 Python 的字节码有基本了解,且针对的是 Python 3.7 版本。

作为预备知识,我们知道 Python 的解释器是基于栈的。以 BINARY_ADD 举例,这个指令把栈顶元素和与栈顶相邻的元素出栈、相加,并把结果存到栈顶。这个栈的官方名称是 "value stack",但很多地方也将其称作 "evaluation stack" 或者 "data stack"。这里显示出 Python 在暴露实现细节时的混乱——文档中并正式定义这个术语,而它却又被在文档中广泛提及,并且是总是以 "stack" 这个含糊的名字出现(都想提个 bpo 去改进文档了 >_<)。

言归正传,为什么要强调 "value stack",是为了和解释器中的另外两个栈区分开:它们分别是 call stack 和 block stack。Call stack 大家比较熟悉这里就略过了。下面正式介绍 block stack,为了能更清晰地解释其来源,我们先看一个例子:

if a == 1:
  foo()
else:
  bar()

作为解释器,该如何实现 if 功能呢?很简单,只要知道分支所在的位置,根据 a == 1 的结果跳过去可以了,比如我们可以用行号表示位置:

if a == 1 is True, jump to line of "foo()"
if a == 1 is False, jump to line of "else:"

解释器有代码位置的信息,它知道 line of foo()line of else: 的值,因此完全可以把这两条规则直接写死。不论整个程序其它部分的代码有多复杂,这个规则都不会变。

再看这个例子:

for i in range(4):
  break
foo()

请问 break 应该如何实现?是否可以像 if 一样,直接跳到 line of foo() 呢?

答案是不行,因为 break 的跳转受到程序其它部分的影响。比如我们加一个 try...finally...

for i in range(4):
  try:
    break
  finally:
    bar()
foo()

这时程序的执行流程就变为了 break ⟶ bar() ⟶ foo()。可以看到,break 跳转是上下文相关的。如果解释器仍然想用 hard-code 的规则实现跳转,就需要对上下文进行全面分析,才能判断出跳转的位置。这会让解释器的实现变得复杂,性能开销也会很大。

为了解决这个问题,CPython 引入了 block stack。用一句话概括就是,CPython 使用 block stack 应对控制流和异常处理带来的跳转不确定性,它让解释器利用运行时信息和少量的固定规则即可实现正确跳转。我将解释 block stack 在这个例子中是如何发挥作用的,为了方便读者理解会将一些地方简化。

首先,解释器在遇到循环 for i in range(4) 会 push 一个 Block(LOOP, line of foo(), b_level) 的结构到 block stack。我们称这个结构为 block,因为它对应了一个代码块。block 包含的第一个重要信息是它的类型,此处为 LOOP,表示它是一个循环结构。第二个重要信息是下一个指令的位置,即“当代码退出 block 的时候,接下来要执行的代码在哪”,显然,这里是 line of foo()

对于 try...finally... 同理,Block(FINALLY, line of bar(), b_level) 会被 push 到 block stack。这时候的 block stack 如下图所示。

现在程序执行到了 break,解释器会根据当前执行的操作和栈顶 block 的类型判断它接下来要做什么。比如这里,当前执行的操作是 break,且 block 类型是 FINALLY,操作就是弹出栈顶元素 Block(FINALLY, line of bar(), b_level),并跳转到该 block 的下一个指令继续执行,也就是开始执行 bar

bar() 执行完之后来到 finally 的结尾,解释器发现当前 block 的类型是 LOOP,且当前执行的操作是 break(这里比较 tricky,break 会被 push 到 value stack 上,所以现在还是可以读取的。我们只要知道解释器仍然知道当前的操作是 break 即可),则同样弹出栈顶元素 Block(LOOP, line of foo(), b_level),并跳转到该 block 的下一个指令 line of foo() 继续执行。此时 block stack 已被清空。

这里面有非常多的细节,我们可以先不去深究。关键是理解一点,即有了 block stack 之后,解释器只需要根据运行时的信息(当前执行的指令)以及先前 push 到 block stack 上的元素的信息(类型、下一个指令的位置),加上一些规则,就能够实现正确跳转。除了上面提到的几个 statements,try...except, continue, with 的实现都涉及 block stack,原因也是显而易见的。

本文仅为介绍 block stack 的概念,有意忽略了很多细节。因此下面列出一些材料,希望做深入了解的读者可以前往阅读。

  • dis moduel
    Python 的 dis 模块。需要注意一下,Python 3.8 对 block 相关的 bytecode 做了较大改动,取消和新增了若干指令,但 block stack 的概念和作用没有变。

  • 《Inside The Python Virtual Machine》 Section 10. The Block Stack
    我学习 block stack 的主要阅读材料。顺带一提本书可能是被低估的 Python 书籍,没有之一。

  • byterun 项目
    用 < 1000 Python 行代码部分实现了一个 Python 解释器(主要是 evaluation loop 的部分)。建议首先阅读概述文章《A Python Interpreter Written in Python》,以便对字节码的工作方式和该项目有一个整体性的认识。之后可以阅读代码中和 block stack 相关的部分,基本上和 CPython 中对应的部分是等价的。

  • ceval.c
    CPython 的 evaluation loop 实现。请重点关注 fast_block_end,它包含了 stack unwinding 的代码。限于篇幅我们没有过多介绍 stack unwinding。这个概念来源于 C++,在 Python 里指的是跳出一个 block 时需要做的清理工作(包括从 block stack 出栈、跳转到下一个指令)。清理工作其实还有另一部分,就留作思考题吧:请说明 UNWIND_BLOCK 这个宏的含义。我个人花了好长时间才看懂,要怪就怪 b_level 这个变量名起得实在是太烂了。

相似的人物,各异(?)的故事

如果你读过一些漫画,可能会发现有些漫画家会执着于某种特定人设,比如安达充。这个现象在百合漫领域更为普遍。一个漫画家笔下的人物在性格、身高、发色、长相上总是呈现某种相似性,甚至连关系都经常直接照葫芦画瓢,导致看的时候常常串戏。因为觉得很有意思,所以打算聊一聊。

儿玉直子,黑发小恶魔角色

首先要说的是儿玉直子(コダマナオコ)老师。由于《捏造陷阱》的动画化,想必熟悉她的人更多了。捏造中的 水科萤 就是这类角色的代表——美丽、脆弱、捉摸不透、反复无常,并且是永远的黑长直。她们总是故事里激发冲突的一方,会闯入另一个浅发角色的世界,让另一个角色为之烦恼。比如《海猫庄days》的斋藤 ,《残光噪音》中的亚梨美 。这就是小玉老师写故事的基本架构,我甚至不需要重看就能概括,但她就是能用这种简单的架构让读者着迷。小玉老师并不是最会讲故事的,但她笔下的人物足以弥补故事的不足。

小玉老师并非每次都会安排这种角色,虽然死鱼眼是一定会出现的(笑)。

大沢やよい,土气短发 × 开朗型角色

大沢やよい 很钟情这种组合。大家最为熟知的应该是名作《2DK、G笔、闹钟。》中的 本渡枫 × 香月奈奈美,但其实远不止于此。《你好,忧郁少女!》 的 浅野湊 × 须川响生 《任性的Fuzz与闪亮小姐》 中的 花 × 望 《呛辣女孩》中的 店员小姐 × 一条 ,都是这个类型。其中开朗型角色往往是更为主动的一方,并且会或多或少地引导土气角色做出改变。

另一个大沢老师常画的题材是乐器少女,我猜测她多半有玩乐器类社团的经历。自从低音号火了之后,描绘吹奏部或是乐队的漫画明显多了起来。但我们不要忘了,百合和音乐自古就有很深的联系。什么你说没有?建议重看《NANA》。

不过,我最吃的还是《你好,忧郁少女!》中的 稻垣千华 × 有田咲子,这对从各种意义上都太棒了,而且独特到几乎很难在其它漫画中找到类似。现在评论区全是恳求老师多画她们的(笑)。

伊藤ハチ,身高差 + 年龄差 + 兽耳

伊藤ハチ 老师的画风最人畜无害,故事最清水,但某种意义上也是最糟糕的一位。伊藤老师和安达充很像,笔下的人物基本长的一样。问题在于,年下的这位不论从哪个角度看年龄都不会超过幼女的范围,这就让人不禁怀疑萌萌画风是不是为了掩盖什么。更糟糕的是,年龄差还不是一两岁的那种。给一张图你们自己体会吧。。。

森永みるく,黑色短发乖乖女 × 黄褐色长发伪现充 + 推倒好友

好了,按惯例我要讲到 森永みるく 老师。森永老师的漫画里,乖乖女角色的人设基本上不会变,但是 CP 的另一方就比较难概括。黄褐色长发没什么疑问,虽然其实这很大程度上是为了在黑白漫画中区分角色。“伪现充”是我能想到最接近的词,因为这一方从气质外形上很符合一般印象中的现充,但又不是真正的现充。至于故事,则是遵循“我喜欢上了我最好的朋友,我最好的朋友也喜欢我”这个公理展开。

森永老师对黑色短发乖乖女的执着超乎想象,我高度怀疑她在创作的时候是把自己带入这一方的。而且角色形象性格极为接近,比如 真理(《GIRL FRIENDS》)和 奈奈(《唇瓣 叹惜 樱色/奈奈与瞳》)基本就是同一个人把刘海换了个方向,然后把头发卷一下就是 花(《放学后的花和绯奈》)。

这是真理

这是奈奈

这是花

对了,我有提到老师还有个短篇吗?这是夏目

老师,够了啊!请多来点!


top