Pyston roadmap

We’ve spent some time recently thinking about the future of Pyston, our faster implementation of Python, and wanted to share what’s on our mind. For updates please check out our wiki.

Roadmap

Our primary goal at this point is to get more people using Pyston, and our initial approach to that will be removing some of the reasons that make it difficult to use Pyston. We are currently 30% faster on our benchmarks and 60% faster on commonly-used benchmarks, and while being even faster would entice more people to use Pyston, we believe we will have a bigger impact by reducing obstacles than by working on performance.

The most common issue that our users report is not being able to install the packages they want. This happens because some packages are difficult to compile; CPython users will typically download pre-compiled versions, but there aren’t pre-compiled packages for Pyston yet. So our current focus is building these packages for our users and providing them in a way that’s easy to install.

To do that we’ve decided to provide the packages through conda. There are a few benefits of this: first it lets us provide the packages ourselves instead of waiting for package maintainers to produce Pyston builds, second it makes it easier for users to get Pyston in the first place, and third it will hopefully make us more portable in the process and work on more Linux distros.

After that we have many things we’d like to do, with the exact order to be determined:

  • Set up a CI/CD system
  • Add support for 64-bit ARM
  • Continued performance improvements

And even longer term:

  • Add Mac and Windows support
  • Integrate with Numba
  • Improve multithreading
  • Explore “opt-in” features that allow us to break semantics
  • Continued performance improvements

Target Python versions

We’ve decided that we only have the resources to target a single version of Python at a time. We would love to be able to provide a version of Pyston regardless of the version of Python you want, but that’s not feasible given our small team.

We currently target Python 3.8.12, and plan to retarget to Python 3.10 some time in early 2022. We intend to backport parts of the “Faster CPython” effort going on in 3.11 depending on the level of compatibility.

We are open to backporting semantic changes made in later versions of Python if we think this means the Python community has decided a certain feature is implementation-dependent. We anticipate that these are largely performance improvements that have small technical semantic differences; we do this on a case-by-case basis, and make a note of it on our wiki.

Supported Pyston versions

While we will try to help with any version of Pyston you are running, because our project moves quickly we won’t be backporting fixes to older versions of Pyston.

If you have any questions/thoughts/suggestions please feel free to join our Discord or file an issue on our GitHub!

Pyston Team Joins Anaconda

We have some very exciting news to announce today: we (Marius and Kevin) are joining Anaconda! Anaconda is a well-known company that produces open-source Python software, and we think that by joining them we can significantly accelerate the trajectory of Pyston, our faster implementation of Python.

[See the corresponding announcement on the Anaconda Blog: https://www.anaconda.com/blog/pyston-team-joins-anaconda]

What will this look like

Things will largely look the same from the outside, except now we will have access to more resources and expertise to move faster. In particular:

  • Pyston remains an open-source project with the same license as CPython
  • Pyston won’t be tied to using conda
  • We still get to set our roadmap, with potentially less time devoted to monetization work. By joining a company with a mature and efficient monetization scheme, we’ll spend more time doing core feature work.
  • Once we need it, we’ll have a governance model that is separate from Anaconda
  • We may develop integrations with other Anaconda projects in ways that are beneficial to both products
  • We’ll continue to work with the community on the other Python performance projects that are underway

Why Anaconda

We talked to a couple of companies about a possible joint future for Pyston, and Anaconda stood out to us in terms of alignment. They’re already doing similar work with Numba and their other projects, and they have a demonstrated open source commitment that means a lot to us.

We also are excited about the possibility of having better integrations with some of their complementary products. We don’t have anything to announce right now, but we already had conda integration on our roadmap, and now that it’s easier, it’s more likely to happen sooner. Together, we are very excited about possibly integrating the features of Numba and Pyston: the two projects target different layers of the stack, and the hope is that by combining features, we will be able to explore more of the space of possible Python optimizations.

And finally, the medium-term roadmap for Pyston mainly involves work to get Pyston into more peoples’ hands. In this sense, we’re finding alternative Python implementations require much more work than simply making them faster, and joining a leading Python distributor will let us short-cut a number of these steps.

The Future

Now that we have Anaconda’s sponsorship, we are planning out a short-term roadmap for the project. We will announce more when it is ready, so stay tuned! In the meantime, give Pyston a try and let us know how it works for you on our Github issue tracker or our Discord channel.

Pyston v2.2: faster and open source

We are proud to announce Pyston v2.2, the latest version of our faster implementation of the Python programming language. This version is significantly faster than previous ones, and importantly is now open source.

We also merged in many changes from CPython and are now based on CPython 3.8.8.

Performance

Pyston v2.2 is 30% faster than stock Python on our web server benchmarks. This is a significant improvement over our previous performance, and if we were feeling cheeky, we would advertise it as “50% more speedup.”

The foundational technology powering Pyston v2.2 is the same as that found in earlier versions, but we have tuned and optimized more areas and found additional speedups, particularly in our JIT and attribute cache mechanisms.

One noteworthy change is that we decided to remove many of the rarely-used debugging features that Python supports because they are expensive even when not needed. Doing so collectively resulted in a 2% speedup, which was remarkable to us: of all the computers in the world running Python, 2% of them are executing debugging checks. We’ve disabled those checks and are positioning ourselves as an “optimized build” similar to binaries without debugging information. Those who still want debugging features can use the “debug build” of stock Python because they are interchangeable. For a full list of the features we removed in Pyston v2.2, please see our wiki.

Open source

As we’ve continued talking to potential customers we now feel convinced that Pyston can thrive on an open-source business model, primarily by starting with support services. This means that we’ve open sourced Pyston v2.2, which you can find at our GitHub here.

We’ve archived our old repository to reduce confusion, but you can still find that here.

We are looking into which of our newest changes can be upstreamed to CPython. Throughout this process, we welcome your contributions. Help with getting Pyston packaged for additional platforms would be especially useful.

Moving forward

We continue to try and make Pyston as compelling and easy to use as possible. Working Pyston into your projects should be as easy as replacing “python” with “pyston.” If that’s not the case, we’d love to hear about it on our GitHub issues tracker or on our Discord channel. We hope you’ll give Pyston a try and see that it really is the easiest way to speed up your Python code.

Pyston v2: 20% faster Python

We’re very excited to release Pyston v2, a faster and highly compatible implementation of the Python programming language. Version 2 is 20% faster than stock Python 3.8 on our macrobenchmarks. More importantly, it is likely to be faster on your code. Pyston v2 can reduce server costs, reduce user latencies, and improve developer productivity.

Pyston v2 is easy to deploy, so if you’re looking for better Python performance, we encourage you to take five minutes and try Pyston. Doing so is one of the easiest ways to speed up your project.

Performance

Pyston v2 provides a noticeable speedup on many workloads while having few drawbacks. Our focus has been on web serving workloads, but Pyston v2 is also faster on other workloads and popular benchmarks.

Our team put together a new public Python macrobenchmark suite that measures the performance of several commonly-used Python projects. The benchmarks in this suite are larger than those found in other Python suites, making them more likely to be representative of real-world applications. Even though this gives us a lower headline number than other projects, we believe it translates to better speedups for real use cases. Pyston v2 still shows sped-up performance on microbenchmarks, being twice as fast as standard Python on tests like chaos.py and nbody.py.

Here are our performance results:

CPython 3.8.5Pyston 2.0PyPy 7.3.2
flaskblogging warmup time [1]n/an/a85s
flaskblogging mean latency5.1ms4.1ms2.5ms
flaskblogging p99 latency6.3ms5.2ms5.8ms
flaskblogging memory usage47MB54MB228MB
djangocms warmup time [1]n/an/a105s
djangocms mean latency14.1ms11.8ms15.9ms
djangocms p99 latency15.0ms12.8ms179ms
djangocms memory usage84MB91MB279MB
Pylint speedup1x1.16x0.50x
mypy speedup1x1.07x [2]unsupported
PyTorch speedup1x1.00x [2]unsupported
PyPy benchmark suite [3]1x1.36x2.48x
Results were collected on an m5.large EC2 instance running Ubuntu 20.04

[1] Warmup time is defined as time until the benchmark reached 95% of peak performance; if it was not distinguishable from noise it is marked “n/a”. Only post-warmup behavior is considered for latency measurement.
[2] mypy and PyTorch don’t support automatically building their C extensions from source, so these Pyston numbers use our unsafe compatibility mode
[3] The PyPy benchmark suite was modified to only run the benchmarks that are compatible with Python 3.8

Results analysis

In our targeted benchmarks (djangocms + flaskblogging), Pyston v2 provides an average 1.22x speedup for mean latency and an 1.18x improvement for p99 latency while using a just few more megabytes per process. We have not yet invested time in optimizing the other benchmarks.

“p99 latency” is the upper 99th percentile of the response-time distribution, and is a common metric used in web serving contexts since it can provide insight into user experience that is lost by taking an average. PyPy’s high p99 latency on djangocms comes from periodic latency spikes, presumably from garbage collection pauses. CPython and Pyston both exhibit periodic spikes, presumably from their cycle collectors, but they are both less frequent and much smaller in magnitude.

The mypy and PyTorch benchmarks show a natural boundary of Pyston v2. These benchmarks both do the bulk of their work in C extensions which are unaffected by our Python speedups. We natively support the C API and do not have an emulation layer, so we are still able to provide a small boost to mypy performance and do not degrade pytorch or numpy performance. Your benefit will depend on your mix of Python and C extension work.

Technical approach

We’re planning on going into more detail in future blog posts, but some of the techniques we use in Pyston v2 include:

  • A very-low-overhead JIT using DynASM
  • Quickening
  • General CPython optimizations
  • Build process improvements

Compatibility

Since Pyston is a fork of CPython, we believe it is one of the most compatible alternative Python implementations available today. It supports all the same features and C API that CPython does.

While Pyston is identically functional in theory, in practice there are some temporary compatibility hurdles for any new Python implementation. Please see our wiki for details.

Availability

Pyston v2.0 is immediately available as a pre-built package. Currently, we have packages for Ubuntu 18.04 and 20.04 x86_64. If you would like support for a different OS, let us know by filing an issue in our issue tracker.

Trying out Pyston is as simple as installing our package, replacing python3 with pyston3, and reinstalling your dependencies with pip-pyston3 install (though see our wiki for a known issue about setuptools). If you already have an automated build set up, the change should be just a few lines.

Our plan is to open-source the code in the future, but since compiler projects are expensive and we no longer have benevolent corporate sponsorship, it is currently closed-source while we iron out our business model.

Reaching us

We are designing Pyston for developers and love to hear about your needs and experiences. So, we’ve set up a Discord server where you can chat with us. If you’d like a commercially-supported version of Pyston, please send us an email.

We’ve optimized Pyston for several use cases but are eager to hear about new ones so that we can make it even more beneficial. If you run into any problems or instances where Pyston does not help as much as expected, please let us know!

Background

We designed Pyston v1 at Dropbox to speed up Python for its web serving workloads. After the project ended, some of us from the team brainstormed how we would do it differently if we were to do it again. In early 2020, enough pieces were in place for us to start a company and work on Pyston full-time.

Pyston v2 is inspired by but is technically unrelated to the original Pyston v1 effort.

Moving forward

We’re on a mission to make Python faster and have plenty of ideas to do so. That means we’re actively looking for people to join the team. Let us know if you’d like to get involved. Otherwise stay tuned for future releases and reach out if you have any questions!

Pyston 0.6.1 released, and future plans

Hello all, we’re happy to release Pyston version 0.6.1, the latest version of our high-performance Python JIT.  v0.6.1 contains performance enhancements over 0.6, bringing Pyston to 95% faster than CPython on standard benchmarks.

On the other hand, this is the last release that Dropbox is sponsoring.  We wanted to take some time to talk about what that means, both about the space of Python performance, and about the Pyston project specifically.

What’s happened

It’s hard to break down the change in cost-benefit analysis, but here are some factors that went into our decision:

  • We spent much more time than we expected on compatibility
  • We similarly had to spend more time on memory usage due to it being a bigger concern than expected
  • Dropbox has increasingly been writing its performance-sensitive code in other languages, such as Go

Our personal take is that the increase on the “cost” side could potentially be considered typical, whereas the decrease on the “benefit” side was probably a larger driver.  It’s hard to say, though, since if we had managed to build things twice as fast the calculus would have been different.

Where we are

We are quite proud that, over the last three years, we’ve been able to achieve meaningful speedups while maintaining a higher level of compatibility than other solutions: we are the first alternative Python runtime to be able to run Dropbox faster.

As for numbers, on the just-released v0.6.1, we are 95% faster on standard Python benchmarks.  On web-workload benchmarks that we created, we are 48% faster.  On Dropbox’s server, we are 10% faster.

We think it’s worth mentioning that the 10% speedup on Dropbox code is just a small fraction of what we think is possible with our approach. We’ve spent most of our time working on compatibility and memory usage and have not had time to optimize this particular workload.

Where we go from here

Marius and I are no longer spending our time working on Pyston and are transitioning to other projects.  The project itself remains open source and available, and we are investigating ways that the project can continue, either in whole or in part.  We are also looking into upstreaming parts of our code back to CPython, since our code is now based on theirs.

We’re proud of what we’ve done and we are looking forward to going into more detail about the technical details in the near future.  We also take some small consolation in having helped map out what Python performance-versus-compatibility tradeoffs may be valuable.

In the end, we are happy that we attempted this, are excited about the many potential ways that our work on Pyston could still be useful, and are happy to refocus ourselves on domains with more immediate needs.

Pyston 0.6 released

We are excited to announce the v0.6 release of Pyston, our high performance Python JIT.

In this release our main goal was to reduce the overall memory footprint.  It also contains a lot of additional smaller changes for improving compatibility and fixing bugs.

Memory usage reductions

One of the big items which reduced memory usage was moving away from representing the interpreter instructions in a tree and instead storing them as an actual bytecode. Now, each instruction follows each other in memory and does not involve pointer-chasing.

We are also much more aggressive in freeing unused memory now. For example for very hot functions which we compiled using the LLVM JIT (our highest tier) we now free the code which the baseline JIT emitted earlier-on. Additional bigger improvements were accomplished by making our analysis passes more memory efficient and fixing a few leaks.

release_v06_mem
This is a chart compares the maximal resident set size of several 64bit linux python implementations (lower is better) on a machine with 32GB of RAM.

While max RSS is not a very accurate memory usage number for various reasons, like not taking into account that pages can be shared between processes and only measuring peak usage, we think it shows nevertheless a very useful insight about how much (up to 2x) Pyston 0.6 improved over 0.5.1.

While we are happy that we were able to reduce the memory usage quite significantly in a few weeks we are not yet satisfied with it and will spend more time on reducing the memory usage further and developing better tools to investigate it. We have several ideas for this – some of the bytecode related ones are summarized here.

Additional changes

This release contains a lot of fixes for compatibility issues and other bugs.  We also spent time on making it easier to replace CPython with Pyston, such as by more closely matching its directory structure and following its ‘dict’ ordering.  We can now, for example, run pip and virtualenv unmodified, without requiring any upstream patches like other implementations do.

Aside: NumPy performance

NumPy hasn’t been a priority for us, but from time to time we check on how well we can run it.  We’ve focused on compatibility in the past, but for this post we took a look into performance as well.  We don’t have any NumPy-specific optimizations, so we were happy to see this graph from PyPy’s numpy benchmark runner:

download

As you can see, we closely match CPython’s performance on NumPy microbenchmarks, and are able to beat it on a few of the smaller ones.  Our current understanding is that we are doing better on the left two benchmarks because they run much more quickly — in about 1000x less time than the right three.  These shorter benchmarks spend a significant amount of time transitioning into and out of NumPy, which Pyston can help with, whereas the right three benchmarks are completely dominated by time inside NumPy.

As a side note, we the Pyston team don’t want to be in the business of picking what NumPy workloads are important.  If you have a program that you think shows real-world NumPy usage, please let us know because we would love to start benchmarking real programs rather than simple matrix operations.

Final words

As always, you can check out our online speed center for more details on our performance and memory usage.

We would like to thank all our open source contributors who contributed to this release, and especially Nexedi for their employment of Boxiang Sun, one of our core contributors.

  • Dong-hee Na
  • Krish Munot
  • Long Ang
  • Lucien Chan
  • SangHee Lee

Pyston 0.5.1 released

We are excited to announce the v0.5.1 release of Pyston, our high performance Python JIT.
This minor release passes all SciPy tests and is on average about 15% faster than 0.5.0. In addition several bug fixes and compatibility improvements got merged.

Performance related changes:

We released recently a blog post about our baseline JIT and inline caches. This release brings a lot of improvements in this area, some of the changes are:

  • the number of ICs slots is now variable. Before we specified for every IC how many slots it has and how large they should be (all slots in a IC had the same size). This often led to higher memory usage than necessary. We changed it now to a fixed size of memory which will than get filled with variable size slots whenever a new slot is required and there is space left in the IC. In addition this makes our IC size estimates in the LLVM tier more accurate because they are now based on the number of bytes we required in the bjit tier.
  • the interpreter reuses the stack slots (internally called vregs) assigned to temporary values which are only live in a basic block. This reduces stack usage which saves memory and made Pyston faster.
  • better non null value tracking, stack spilling, duplicate guard removal and much more temporary values will get held in registers
  • the bjit and ICs can now use callee-save register which removes a lot of spilling around calls
  • added a script which allows to inspect jited code directly from `perf report`.
    • usage with `make perf_<testname>`
  • our codegen and analysis passes now work on the vreg numbers which allows us to use arrays as internal data structures instead of hash tables which makes the code easier to understand and faster
  • faster reference counting pass in the code generator of the LLVM tier

Performance comparison:

startup performance benchmarks:

startup

This benchmarks show that the startup time improved significantly. Part of this comes from the numerous bjit improvements mentioned above (the chart also contains a direct comparison between the bjit performance of the different releases).

steady state benchmarks:

steadystate

Conclusion:

There are still a lot of low hanging fruit and we still have a huge amount of ideas for (performance) improvements for future releases.
The next months we will use to make Pyston ready for usage at dropbox – this is going to be very exciting 🙂

Finally, we would like to thank all of our open source contributors who have contributed to this release, and especially Nexedi for their employment of Boxiang Sun, one of our core contributors who helped greatly with the SciPy support.

  • Cullen Rhodes
  • Long Ang
  • Lucien Chan

 

baseline jit and inline caches

Creating an implementation for a dynamic language using just in time compilation (JIT) techniques involves a lot of compromises mainly between complexity of the implementation, speed, warm-up time and memory usage.
Especially speed is a difficult trade-off because it’s very easy to end-up spending more time optimizing a piece of code and emitting the assembly than we will ever be able to save by executing faster than executing it in a less optimized way.
This causes most JIT language implementations to use an approach of different tiers – approaches to running the code and different amount of optimizations done depending on how often the specific piece of code gets executed. Thereby reducing the chance that more time will get spend transforming the code in to a more efficient representation than it would take to execute it in a less efficient representation.

baseline just in time compiler

We noticed that our interpreter is interpreting code quite slowly while the LLVM tier takes a lot of time to JIT (even with the object cache which made it much faster) so it was obvious that we either have to speed the interpreter up or introduce a new tier in between.
There are well-known problems with our interpreter, mainly it’s slow because it does not represent the code in a contiguous block of memory (bytecode) but instead it involves a lot of pointer chasing because we reuse our AST nodes. Fixing this would be comparable easy but we still thought that this will only improve the performance a little bit but will not give us the performance we want.

About a year ago we introduced a new execution tier instead, the baseline jit (bjit). It is used for python code which is executed a medium number of times and therefore lives between the interpreter and the LLVM JIT tier. In practice this means most code which executes more than 25 times will currently end-up in the bjit and if it gets executed more than about 2500 times we will recompile it using the LLVM tier.

The main goal of the bjit is to generate reasonable machine code very fast and making heavy use of inline caches to get good performance (more on this further down).
It involved a number of design decisions (some may change in the future) but what we currently ended up with:

  • reuse our inline cache mechanism
    • it transform the bjit from only being able to remove the interpretation overhead (which is quite low for python – it depends on the workload but probably not more than 20%) to a JIT which actually is able to improve the performance by a much larger factor
  • generate machine code for a basic block at a time
    • only generating code for blocks which actually get executed reduces the time to generate code and memory usage at the expense of not being able to do optimizations across blocks (at the moment)
  • highly coupled to the interpreter and using the same frame format
    • making it very easy and fast to switch between the interpreter and bjit at every basic block start
    • we can fallback to the interpreter for blocks which contain operations which we are unable to JIT or for blocks which are unreasonable to JIT because the may be very large and generating code for them would cost too much memory
    • makes it easy to tier up to the bjit when we interpret a function which contains a loop with a large amount of iterations
  • does not use type analysis and all code it generates makes no assumptions about types
    • this makes it always safe to execute code in the bjit
    • type specific code is only inside the ICs and always contains a call to a generic implementation in case the assumptions don’t hold
  • all types are boxed / real python objects
  • it collects type information which we will use in LLVM tier to generate more optimized code later on if the function turns out to be hot
    • if an assumption in the LLVM tier turns out to be wrong we will deoptimize to the interpreter/bjit

Inline Cache

the inline cache mechanism is used in the LLVM tier and in the baseline JIT and is currently responsible for most of the performance improvements over the cpython interpreter (which does not use this technique). It removes most of the dynamic dictionary lookups and additional branching which a “normal” python interpreter often has to do. For every operation where we can use ICs we will provide a block of memory and fill it with a lot of nops and a call to the generic implementation of the operation. Therefore the first time we execute the code we will call into the generic implementation but it will trace the execution of the operation using the arguments supplied. It then fills in the block of memory a more optimized type specific version of the operation which we can use the next time we will hit this IC slot if the assumptions the trace made still hold.

Here is a simple diagram of how a IC with two slots could look like:

ic_example

A simple example will make it easier to understand what we are doing.

For the python function:

def f(a, b):
    return a + b

The CFG will look like this:

Block 0 'entry'; Predecessors: Successors:
 #0 = a
 #1 = b
 #2 = #0+#1
 return #2

We will now look at the IC for #2 = #0+#1

For example if we call f(1, 1) for the first time the C++ function binop() will trace the execution and fill in the memory block with the code to do an addition between two python int objects (it uses a C++ helper function called intAddInt()):

intAddInt

Notice the guard comparisons inside the first IC slot, they make sure that we will only use the more optimized implementation of the operation if it’s safe to do so (in this case the arguments have the same types and the types did not get modified since the trace got created) and otherwise jump to the next IC slot. Or if there is no optimized version call the generic implementation which is always safe to execute.

Most code is not very dynamic which means filling in one or two slots with optimized versions of a operation is enough to catch all encountered cases.
For example if we later on call f("hello ", "world") we will add a new slot in the IC:

strAddStr

We use ICs for nearly all operations not only for binary ones like the example showed. We also use them for stuff like global scope variable lookup, retrieving and setting attributes and much more (we also support more than two slots). Not all traces call helper functions like we have seen in the example some are inlined in the slot.

Pyston will overwrite slots if they already generated slots turn out to be invalid or unused because they assumption of the trace don’t hold anymore. Some code (luckily this is uncommon) is highly dynamic in this cases we will try to fill in the slot with a less aggressive version if possible – one which makes less assumption. If not possible we will just always call the generic version (like cpython always does).

The code we emit inside the ICs has similar trade offs to the bjit code – mainly it needs to get emitted very fast. We prefer generating smaller code instead of faster one because of the fixed size of the inline cache. It’s better to generate a smaller version which allows us to embed more slots if necessary and trashes the instruction cache less.

lots of ideas for improvements

Both the inline cache mechanism and the bjit have a lot of room for improvements. Some of the ideas we have are:

  • directly emit the content of some of the IC slots of the bjit in the LLVM tier as LLVM IR which makes it accessible to a powerful optimization pipeline which emits much better code with sophisticated inlining and much more
  • generating better representation for highly polymorphic sides
  • smarter (less) guards
  • introducing a simple IR which allows us to do some optimizations
  • better register allocation
  • allow tracing of additional operations
  • removal of unnecessary reference counting operations
  • the whole trace generation requires writing manual c++ code (called ‘rewriter’ inside the code base) which makes them quite hard to write but with the benefit of giving us total control of how a slot looks like. In the future we could try find a better trade-off by automatically generating them from the c++ code or LLVM IR when possible

We’ve already made a lot of improvements in this area, stay tuned for a 0.5.1 blog post talking about them 🙂

Pyston 0.5 released

Today we are extremely excited to announce the v0.5 release of Pyston, our high performance Python JIT. We’ve been a bit quiet for the past few months, and that’s because we’ve been working on some behind-the-scenes technology that we are finally ready to unveil. It might be a bit less shiny than some other things we could have worked on, but this change makes Pyston much more ready to use.

Pyston is now using reference counting.

Refcounting

Reference counting (“refcounting”), is a form of automatic memory management. It’s usually viewed as slower and less sophisticated than using a tracing garbage collector (a “GC”), the predominant technique in modern languages. All past versions of Pyston contained tracing garbage collectors, and much of our work from 0.4 to 0.5 was tearing it out in favor of refcounting.

Why did we do this? In short, because CPython (the main Python implementation) uses refcounting. We used a GC initially to try to get more performance. But applying a tracing GC to a refcounting C API, such as the one that Python has, is risky and comes with many performance pitfalls. And most challengingly, Pyston wants to support the large amount of code that has been written that relies on the special properties that refcounting provides (predictable immediate destruction). We found that we had to go to greater and greater lengths to support these programs, and there were also cases where we wouldn’t be able to support the applications in their current form.

So we decided to bite the bullet and convert to refcounting, with the goal of getting better application compatibility.

How did we do?

NumPy

We are very happy to announce: we can run NumPy, unmodified.

Specifically: on their latest release (v1.11), we run their entire test suite with one test failure, for which they’ve accepted our patch. For their latest trunk, we have three test failures. We do need to use a modified version of part of their build chain (Cython), and we are currently slower on the test suite than CPython.

Regardless, we are very happy with this result, especially because we will continue to improve both the compatibility and performance.

Other goodies

There are quite a few non-refcounting features that made it into this release as well:

  • Signal handling
  • Frame introspection of exited frames
  • Generator cleanup
  • Support for more C API functions, such as custom tracebacks
  • and many more small fixes than we can list here

These are a large part of our progress on NumPy, and they also help us run other tricky libraries such as py.test, lxml, and cffi. We’ve also greatly reduced the number of modifications that we maintain to the Python standard libraries and C extensions. Overall, refcounting was a big investment, but it’s bought us compatibility wins that we would have had a very hard time getting otherwise.

Performance

Unfortunately, since performance wasn’t our goal for this release, we did slide backwards a bit. v0.5 is about 10% slower than v0.4 was, largely due to the change to refcounting. We are okay with the regression since we explicitly focused on compatibility for the last six months, and our refcounting implementation still has many available optimizations.

As a side note, the “conventional wisdom” is that refcounting should have been even slower compared to using a GC.  We attribute this mainly to the compatibility restrictions that hampered our GC implementation.

There is a lot of low-hanging performance fruit available to us right now which we have been explicitly avoiding while we finished refcounting. Now would be a great time to consider contributing since we have more ideas than we can implement ourselves. This is especially true when it comes to NumPy performance.

Currently, we take about twice as long to run the NumPy test suite as CPython does. We don’t know how this will translate to performance on real NumPy programs, but we do know that much of the slowdown falls into two categories: the first is NumPy hits code paths that are otherwise-rare in Pyston and are currently unoptimized. The second is a bit more subtle: NumPy frequently calls from C code back into the Python runtime, which is expensive for us because it doesn’t benefit from our JIT (in addition to being previously-rare). We have techniques inside Pyston to handle these situations and invoke our JIT from C code, and we’d like to start exposing that so that NumPy and other libraries can use it.

Looking forward

We apologize — again — for the lengthy release cycle. We didn’t expect refcounting to take this long, and we even knew that it would take longer than we expected. We’re planning on doing another blog post to talk about what the difficulties were with it and go into more of the technical details of our refcounting system.

Moving forward, our plan for 0.6 is to focus on performance. We would love help from the community on identifying what is important to make performant. We could work on making the NumPy test suite fast, but it may not end up translating to real NumPy workloads.

We’re at the point that trying out Pyston should be easy; it won’t benefit all workloads, but it should be easy to drop it in and see if it does. To test it out, try

docker run -it pyston/pyston

or check out our readme for other options for obtaining Pyston.  To try NumPy, use the “pyston/pyston-numpy” image instead.

We have quite a few optimization ideas lined up, and the pressure has been strong to delay the 0.5 release “just one more week” so that we have time to include some of them. Expect to see an 0.5.1 release that improves performance.

Final words

Refcounting brings Pyston one step closer to being a drop-in replacement for CPython. There is still much more work to do, but we feel like with refcounting we’ve reached a threshold where we’d like to start getting Pyston into peoples’ hands. It’s still very much beta software, so there are many rough edges and unoptimized casses. But we want your feedback on what’s working and what’s not.

Finally, we would like to thank all of our open source contributors who have contributed to this release, and especially Nexedi for their employment of Boxiang Sun, one of our core contributors who helped greatly with the NumPy support.

  • Boxiang Sun
  • Dong-hee Na
  • Rudi Chen
  • Long Ang
  • @LoyukiL
  • Tony Narlock
  • Felipe Volpone
  • Daniel Milde
  • Krish Monut
  • Jacek Wielemborek