在Python中执行a + b需要多少行C代码
How many lines of C it takes to execute a + b in Python

原始链接: https://codeconfessions.substack.com/p/cpython-dynamic-dispatch-internals

要回答标题提出的问题,就行数而言没有直接答案,因为实现涉及 C 语言语法和 C 编译器使用的汇编语言语法。 然而,根据前面提供的文本材料中给出的解释,在 Python 解释器执行程序期间动态分派“+”运算符所涉及的步骤的简单分解如下: 1. 在编译过程中,Python 中的每种数据类型都会分配一块内存,称为 PyTypeObject 结构体,其中包含支持各种运算符所需的函数指针表。 2. 名为“Obj”的全局变量,表示与当前活动的 Python 对象关联的 C 数据结构,保存对其各自 PyTypeObject 结构的引用。 3. 使用内置数学表达式进行算术计算时,在 CPython 虚拟机上执行相应的字节码。 “BINARY_OP”指令导致在“Objs/abstract.c”中存储的表中查找函数指针。 抽象接口位于“用 C 实现的抽象函数”(也称为抽象对象接口)中,支持在运行时在数据类型的具体表示和抽象表示之间进行分派。 如前所述,抽象对象接口调用的每个函数最终都会返回到相应数值表达式中包含的原始具体实现方法。 4. 遍历 PyTypeObject 结构并找到正确的功能对应项后,调用所需的方法。 因此,获得计算结果,分配给返回变量,并传递到下一个中​​间计算阶段。 5. 最终,经过涉及分散在各个 CPython 库中的大量已编译源文件的多层翻译后,最终产品被发送回用户控制台,准备进一步分析或在其他地方使用。 同样,对这里给出的概括描述进行了许多变化和修改。 这些示例仅旨在提供一个介绍性指南,帮助您掌握动态调度的 CPython 实现模型的基本原理以及广泛流行的 Python 编程语言的其他核心方面,并激发渴望进一步研究该主题或探索该主题的爱好者的进一步好奇心。 编程理论和实践进步的潜在途径。

然而,它忽略了许多重要因素,如可维护性、可扩展性、模块化、可扩展性、可移植性、安全性、可靠性、健壮性、简单性、优雅性、效率和其他人类价值。 此外,它需要大量的领域专业知识以及对底层硬件架构和指令集架构的理解,这限制了其可访问性并增加了其成本。 此外,由于逻辑和语法的错误和不一致,直接操作机器代码通常会导致算法效率低下、计算不准确以及程序不正确。 最终,高级语言比机器代码提供了许多好处和优势,包括更好的抽象、更高的自动化水平、减少的开发成本和时间、提高的准确性、一致性和质量、更简单地与各种系统和组件集成以及更好地支持并发、并行性、分布性和网络化。 因此,在绝大多数情况下,直接在拨动开关上使用机器代码既不实际也不可取。
相关文章

原文

Welcome back to the CPython internals series. In the previous article we explored the PyObject structure, and its role as the header for all CPython runtime objects. This structure plays a crucial role in enabling inheritance and polymorphism in the CPython object system. But that was just the tip of the iceberg

In this article we will go one level deeper, and look at what exactly goes on behind the scenes in the Python runtime to execute something as simple as “a + b”. In other words, we will learn the implementation details behind types, operators, and dynamic dispatch in CPython.

Note that, even though we will be following the implementation of dynamic dispatch for a specific operator, the same ideas apply for all the operators supported by CPython. So effectively, with this knowledge you could implement your own new operator, or your own new type.

If you are a recent subscriber, I’ve written quite a few articles on CPython internals, that you might enjoy. You can find them all here.

When we write code such as a + b in Python, the types of a and b determine the exact behavior of the + operation. Every type in Python has its own implementation of the + operator (if that type supports +) and the Python interpreter figures out the right implementation to call based on the type of the operands. This whole process is called dynamic dispatch in programming languages. The following diagram gives a high level overview of how it works in CPython:

High level flow of operator execution in the CPython VM
High level flow of operator execution in the CPython VM

Let’s briefly discuss the various parts:

  • The Python code gets compiled to bytecode, which is executed by a stack based virtual machine (VM) in CPython. The BINARY_OP instruction is responsible for executing the + operation on the two operands, a and b.

  • The VM itself does not know how to perform + on two objects. Instead, it delegates this to an abstract object interface to deal with it.

  • The abstract object interface in CPython defines an interface supporting all the common object level operations in CPython. This gives the VM a single unified way of executing all the operators without knowing any implementation details of the object system. The abstract interface dispatches the execution to the concrete implementation within the types via the function pointer table lookup in the object header (more on this later).

In the previous article, we briefly explored a part of this flow by looking at the implementation of the BINARY_OP instruction in the CPython VM. This time our goal is to properly understand how the dynamic dispatch happens. For this reason, we are going to follow a bottom up approach.

We will start by looking at how the different types implement various operators, then we will look at the abstract object interface and see how it calls those concrete implementations, and finally, we will see how the CPython VM integrates with the abstract object interface.

The PyTypeObject struct is the second building block of the CPython object system (first being PyObject). It contains the runtime type information about an object. Before we look at dynamic dispatch in CPython, we should first understand what’s inside PyTypeObject.

But first, let’s just recap, and see the definition of PyObject, which is where PyTypeObject comes into picture:

Definition of PyObject struct
Definition of PyObject struct

Also, every type definition in CPython includes PyObject as its first field as a header. For instance, this is the definition of the float type:

This means that every such object can be typecast to PyObject (see the previous article on PyObject to understand how), and because PyObject includes a pointer to PyTypeObject, the CPython runtime has all the type related information about the object available to it at all the times.

Now, let’s look inside PyTypeObject. It’s a very large object with dozens of fields. The following figure shows its full definition:

The PyTypeObject struct stores the runtime type details about the object, such as the type name, type size, functions to allocate and deallocate an object of that type.

Apart from that, it also stores function pointer tables for supporting various type specific behaviors. For instance, the tp_as_number field is one such table. It is a pointer to an object of type PyNumberMethods that defines a function pointer table for numeric operations.

Since we are interested in understanding how CPython executes the binary add (+) operator, we will zoom in and look at what’s inside PyNumberMethods. The following figure shows its definition:

Every type implementation in CPython needs to create an instance of PyNumberMethods struct and populate it with pointers to the functions that it implements for supporting numeric operators. If a type does not support numeric operations, it can simply set the tp_as_number field in PyTypeObject to NULL, which would tell the CPython runtime that this object does not support any of these operations.

Next, as a concrete example, let’s see how the float type implements these functions and then instantiates the PyTypeObject when creating a new float object.

The following figure shows the code from Objects/floatobject.c which contains the implementation of the float type in CPython.

Let’s break it down:

And, a pointer to float_as_number is included in every float object’s header (i.e. as the value of the ob_type field in PyObject). The following figure shows the function PyFloat_FromDouble, which creates new float type objects, and uses float_as_number to initialize the object header.

The figure is pretty detailed and annotated, so I won’t spend anymore time on it. But this is the code which is executed when you write “a = 3.14” in your Python code.

Side note: CPython maintains a cache of unused free float type objects and reuses them when it can. This possibly saves some time spent in memory allocation. There are similar caches for other objects, such as lists, tuples, dicts.

At this point, we understand that every type implements various operators as functions and uses them to populate the function pointer table in PyTypeObject, which is included in the object header. We have seen how this scheme works in the implementation of the float type.

Next, we move one layer up and see the abstract object interface, which actually performs dynamic dispatch.

Share

CPython defines an abstract object interface for unifying the access to the concrete type implementations. This keeps the VM code clean because it simply delegates the execution of an operator to this interface.

This abstract interface is defined in the file Include/abstract.h. and the following figure shows the numeric functions declared in it:

Now, abstract.h is a header file, so it only declares the prototypes of these functions. The implementations of these functions are in the file Objects/abstract.c. We will focus just on the implementation of the PyNumber_Add function in it, which is called by the VM to handle the + operator execution. The following figure shows its code, and the annotations explain what’s happening:

The + operation is supported by two classes of data types in Python: numeric types (int, float, complex etc.) and sequence types (list, tuple, etc.).

The PyNumber_Add function first tries to call the binary add implementation on the arguments. If those types do not support binary addition, then it tries to check if these types are sequence types, and if so, then it tries to call the concat function on them.

Let us focus on the numeric types here. For the numeric types, the PyNumber_Add function invokes the macro BINARY_OP1, which simply calls the binary_op1 function. The following figure shows binary_op1:

The function is doing quite a lot of things, but the annotations explain everything. The key takeaway is that abstract.c simply does function pointer lookup in the methods table present in the object’s header, and calls that function.

So far we have seen how a type implements various operators, and how the abstract object interface facilitates dynamic dispatch to those implementations. Now, for the final part, we will get back to the CPython VM and see where it calls the abstract object interface to execute an operator.

This is the final act where the CPython VM integrates operator execution with the abstract object interface. We discussed this partially in the previous article when understanding how the PyObject structure helps simulate polymorphism. This time, we will see it fully. But let’s start from the beginning.

The following figure shows a simple Python function and its bytecode instructions:

The bytecode instruction we are going to focus on is BINARY_OP. The following image shows how it is handled by the VM:

I won’t spend time explaining any of this, as we covered this in the previous article. However, we glossed over the actual execution of the binary op, let’s see that code because that’s where the VM delegates to abstract.c.

In the above code, we see this line of code:

res = binary_ops[oparg](lhs, rhs);

This code is doing a function pointer lookup in a table called binary_ops, using the opcode of the binary operator as the index, and calling that function. Let’s take a look at this table which is defined in the file ceval.c (which is where most of the VM execution code is implemented).

Each function pointer in the binary_ops table points to a function which is implemented in Objects/abstract.c. In the previous section, we already saw the definition of PyNumber_Add in abstract.c, and understood how it does dynamic dispatch to the correct implementation of the operator based on the types of the operands.

So, this is how the VM delegates the execution of the binary operators to the abstract interface implementation, which ultimately performs the dynamic dispatch via function pointer lookup in the tables present in the object headers.

And, this is it! This is everything that goes on behind the scenes when you execute “a + b“ in your Python code. Let’s summarize quickly:

This was a short tour of ALL the CPython code which comes into action when you execute something as simple as “a + b” in your Python code. Although, this must be a lot to digest, it’s not too complex if you understand function pointers.

Equipped with this knowledge, you could implement your own operators, although for that you would also need to modify the tokenizer and parser which we have not talked about yet. Maybe we will cover those soon, if you have interest in learning more about CPython internals. Let me know via your comments, replies, likes and shares.

Share

联系我们 contact @ memedata.com