How Python notebooks run, and how fast
A Python notebook runs on an interpreter written in C#, not on CPython, and that makes loops in a cell much slower than the same loop in a C# notebook. This page explains how the notebook works, shows what we measured, and says when to use which language.
How it works
The interpreter is Computerwelt, a port of Monty to C#. Everything runs inside the server process:
- Parse and compile. A cell is parsed and compiled to bytecode, with scope and name resolution done at compile time. See the Python overview.
- Run. A virtual machine with its own object heap executes the bytecode, one instruction at a time. It is an interpreter: each Python operation costs a few dispatches in managed code, where a C# cell runs as machine code.
- One session per notebook. A
PythonSessionkeeps the namespace between cells, so a variable from one cell is in the next. Each cell runs on a thread of its own, under a budget of 20,000,000 bytecode instructions. A cell that exhausts the budget stops with an error, and the notebook stays usable. The limits page lists the budgets. - No ambient access. The notebook has a scratch virtual file system that lives and dies with it, and no route to the server's files, network or processes. See the sandbox model and the virtual file system.
- The language is a subset. Packages with native code are not available, and
numpyandpandasare managed emulations of the common parts. See the language subset and the supported modules.
The Shell scope reaches the interpreter through a reflection bridge:
- When the first Python notebook opens, the server generates the
shellmodule by reflection over the scope's global members, and imports it before the first cell. q(),graphand the other names are bound to the real scope object of the administrator who opened the notebook. Whatq()returns is a host object: a wrapper the interpreter holds around a C# object.- Reading an attribute of a host object reads a property. Calling one picks an overload by reflection, converts each Python argument to the parameter's .NET type, calls the method, and converts the result back. A method that returns a
Taskis awaited by blocking the cell's thread. - Only members of Studio types are reachable.
Internalsis hidden, and a value of any other type arrives as text.
Computerwelt documents the mechanism for hosts in Libraries. The Shell scope, a query and the shell module's naming rules are in the Python notebooks guide.
A C# notebook has none of this. Each cell is compiled by Roslyn to IL in the same process and run by the .NET runtime, with the scope as its globals, exactly as a script run from the Shell is.
What it costs
We ran the same work in a C# cell and a Python cell, in the same process against the same graph, and took the median of seven runs after a warm-up. The C# time is the work itself, measured inside the cell.
| Work | C# | Python | Python / C# |
|---|---|---|---|
| List the UIDs of a node type (a few thousand nodes) | 2.2 ms | 6.2 ms | 2.8x |
One graph.GetNodeType(uid) call per node |
2.3 ms | 33.6 ms | 15x |
| Query as rows, then group by a field | 19.3 ms | 30.8 ms | 1.6x |
| Count words in 20,000 strings with a dictionary | 1.1 ms | 32.5 ms | 28x |
| Sieve of primes below 200,000 | 0.8 ms | 242 ms | 290x |
| Sum of squares, 1,000,000 iterations | 0.5 ms | 613 ms | 1,200x |
Every answer matched between the two languages. Sending a cell that does nothing costs 14 ms in C# (the Roslyn compile) and 0.1 ms in Python, so for a small cell the C# notebook is the slower one.
How to read it:
- Work done by the server is about the same in both. A query runs the same engine from either language, so a cell that is one query and a small amount of code is within a small factor.
- Each call across the bridge costs more in Python. A host call is a reflective dispatch plus argument and result conversion, and a loop of them is 15 times slower than in C#. Prefer one query over a call per node, and
dataframe()orquery_rows()over fetching fields one at a time. - Computation in the cell is two to three orders of magnitude slower. A tight loop costs about 0.6 µs per iteration in Python, so a million iterations take over half a second, and a cell that needs more than a few million loop iterations will meet the instruction budget. Move that work into the query, or into a C# notebook.
The numbers come from one run on a 4-core container, with a debug build of the test host and Computerwelt as published on NuGet. Treat them as orders of magnitude, not as a benchmark of your hardware. The benchmark is NotebookBenchmarkCSharpAgainstPython in the Studio test project. Set MOSAIK_NOTEBOOK_BENCHMARK=1 to run it.
Choosing a language
| Use | When |
|---|---|
| Python notebook | You explore: queries, then dataframe(), pandas, and charts. The cells are short and the time goes into the query. |
| C# notebook | The cell loops over many nodes, calls the scope once per node, or computes something heavy. Or you need a type or method that Python cannot call (the list). |