Assessing PyPy Performance with π and Prime Numbers

October 28, 2023 · 6 mins read

There is more than one implementation of the Python programming language, and CPython is by far the most widely used which, as the name implies, is written in the C programming language. Further details are reported in the Alternate Implementations section of the Python Reference Manual.

Today’s focus is PyPy an implementation of Python written in…Python! As stated in the PyPy’s Mission statement:

We aim to provide a compliant, flexible and fast implementation of the Python Language which uses the RPython toolchain to enable new advanced high-level features without having to encode the low-level details. We call this PyPy.

The main reason for adopting PyPy is to have Python code run faster and, as reported here, PyPy is 4.3 times as fast as CPython on geometric average. However, there are cases where the use of PyPy will be pointless:

  • Short-running processes: if it doesn’t run for at least a few seconds, then the JIT compiler won’t have enough time to warm up.
  • If all the time is spent in run-time libraries (i.e. in C functions), and not actually running Python code, the JIT compiler will not help.

The JIT (Just-In-Time) compiler is the key ingredient to make Python programs run faster. In contrast to the CPython’s AOT (Ahead-Of-Time) compilation which translates the code to bytecode before code execution, JIT allows to translate the code at runtime.

The installation of PyPy is pretty straightforward and the Installation section provides a lot of details. For this post I am using conda, so I have created an environment which supports PyPy by running the following command in the Anaconda Prompt.

conda create -c conda-forge -n my_pyp_env pypy python=3.9

The purpose of this post is to assess PyPy performance by running two famous algorithms:

  • Estimation of π using the Monte Carlo method (10⁷ iterations).
    def estimate_pi(iterations: int = 10_000_000) -> float:
      """
      Estimate pi using the Monte Carlo method.
    
      Parameters
      ----------
      iterations
    
      Returns
      -------
      float
    
      """
      circle_point_count = 0
      square_point_count = 0
      for _ in range(iterations):
          x = random.uniform(-1.0, 1.0)
          y = random.uniform(-1.0, 1.0)
          distance = x**2 + y**2
    
          if distance <= 1:
              circle_point_count += 1
          square_point_count += 1
    
      return 4 * circle_point_count / square_point_count
    
  • Computation of all the prime numbers less or equal to a given integer n (10⁷ for this example) using the ancient Sieve of Eratosthenes. The implementation here reported is a very elegant one which leverages a generator and set operations and is taken from the awesome Rosetta Code website.
    def compute_primes(integer: int) -> Iterator[int]:
      """
      Compute all the prime numbers smaller or equal than the integer.
    
      Sieve of Eratosthenes.
    
      Parameters
      ----------
      integer : int
    
      Returns
      -------
      Iterator[int]
    
      """
      primes_ = set()
      for i in range(2, integer + 1):
          if i not in primes_:
              yield i
              primes_.update(range(i * i, integer + 1, i))
    

Both algorithms were run with CPython and PyPy ten times and the average execution times are reported below. The perf_counter function from the time standard library was used for the computation of the elapsed time. The code is available here.

Regarding the π estimation algorithm, CPython took on average 9.31 seconds, while PyPy only 0.38 seconds, a whooping 24.5 times faster than CPython!

The Sieve of Eratosthenes ran in 3.17 seconds using CPython and took 1.90 seconds with PyPy on average, definitely a more modest result but still a valuable 40% reduction. As explained in the Rosetta Code website, the set.update method avoids explicit iterations in the interpreter thus reducing the impact of the PyPy speed up.

At the time of writing, the PyPy’s official website reports that:

PyPy is highly compatible with existing python code. It supports cffi, cppyy, and can run popular python libraries like twisted, and django. It can also run NumPy, Scikit-learn and more via a c-extension compatibility layer.

thus, PyPy may be beneficial for many projects. Unfortunately, it is currently not a viable option for machine learning applications. TensorFlow is one of the most popular libraries for machine learning but it does not support PyPy. An issue was open in 2015 but it is currently stale. Speaking of machine learning and improve performance, Mojo is a new programming language which is designed to be a superset of Python but with the performance of C. Thus, Mojo could be an appealing alternative to Python for machine learning applications, but this will be the topic of the next post.