Python vs Mojo 🔥: First look and speed ​comparison

enter image description here

Update on 9th May 2024:
Mojo is developing so rapidly that a couple of things I wrote last year are already obsolete.

Among all the tech stack I have worked with in my life I enjoy working with Python the most. Even with its weaknesses, efficacy and speed. They may come to an end, though. Around two months ago I heard about Mojo, spreading rapidly with its huge flames to burn down the obstacles of having a “fast Python”. Mojo is said to be thousands of times faster than Python. I was really interested and checked its documentation right after. While Mojo is still a work in progress, developers are provided with a JupyterHub-based playground to try it out. That’s exactly what I did.

About Mojo

Mojo is intended to be a new programming language for AI developers. It was created by the intention of unifying the world’s ML/AI infrastructure. Later they realized that programming across the entire stack was too complicated, so they chose to embrace the Python ecosystem and the goal was to provide kind of a superset of Python. Meaning to make existing Python code compatible with Mojo. Python is widely used and really popular, so they got it right I think.

Some Basic Facts About Mojo

  • Mojo is not interpreted at runtime as Python, but compiled ahead-of-time to machine-native code, using the LLVM toolchain.
  • Existing Python code is fully compatible with Mojo. You can import your own Python libraries as well.
  • Migrating existing Python code to Mojo is not fully compatible yet, but it’s a work in progress.
  • You can define var or let variables, var is mutable while let is immutable. Use let when possible for better performance.
  • struct vs class: Mojo’s struct is similar to Python’s class, however the former is static, while the latter is dynamic. Mojo structs are bound at compile-time meaning you cannot add methods at runtime. In return you get performance while being safe and easy to use.
  • In Mojo you can define a def or an fn. Similar to structs, an fn is stricter but provides you with greater performance.
  • You can define both immutable arguments and mutable arguments. An immutable argument, borrowed object, is just an immutable reference to the object the function receives. This is the default behavior.
  • Using the __copyinit__ method we can implement copyable types such as reference semantic, immutable or deep value semantic. In Python it is reference semantic.
  • The @register_passable("trivial") struct decorator tells Mojo that the type should be copyable and movable but that it has no user-defined logic for doing so.
  • Mojo destroys values using an “As Soon As Possible” (ASAP) policy, behaving like a hyper-active garbage collector.
  • Mojo’s file extension can be .mojo or .🔥. Yes, you see it well. :)

You can and I recommend that you check Mojo’s programming manual and modules.
Also, here is the launch video from Mojo’s website.

Let’s Test It!

To start with, this is how Mojo’s playground looks like:

enter image description here

Having been provided with access to the playground, I created two examples to test Mojo’s performance against Python.

My first test was inspired by a HackerRank task I just solved a few days ago. The implementation of complex numbers. The result is breath-taking.

Python version:

import time  
  
  
class MojoComplex:

	def __init__(self, real, imaginary):
		self.real = real
		self.imaginary = imaginary

	def __add__(self, no):
		return MojoComplex(self.real + no.real, self.imaginary + no.imaginary)

	def __sub__(self, no):
		return MojoComplex(self.real - no.real, self.imaginary - no.imaginary)

	def __mul__(self, no):
		r = (self.real * no.real) + (self.imaginary * no.imaginary * -1)
		i = (self.real * no.imaginary) + (self.imaginary * no.real)
		return MojoComplex(r, i)


start = time.time_ns()
result = 0
for i in range(10000000):
	x = MojoComplex(i + 91, i)
	y = MojoComplex(i, i - 2)
	sum = x + y
	sub = x - y
	mul = x * y
	result += (sum.real + sub.real + mul.real)
end = time.time_ns()
print(result)
print(end - start)

Output:

4750001345000000  
22078695473

If you want to run pure Python code in the playground notebook, use %%python in the cell.

Mojo version:

let time = Python.import_module("time")  
  
  
struct MojoComplex:
	var real: Int
	var imaginary: Int
  
	fn __init__(inout self, real: Int, imaginary: Int):
		self.real = real
		self.imaginary = imaginary
  
	fn __add__(self, no: MojoComplex) -> MojoComplex:
		return MojoComplex(self.real + no.real, self.imaginary + no.imaginary)

	fn __sub__(self, no: MojoComplex) -> MojoComplex:
		return MojoComplex(self.real - no.real, self.imaginary - no.imaginary)

	fn __mul__(self, no: MojoComplex) -> MojoComplex:
		let r: Int
		let i: Int
		r = (self.real * no.real) + (self.imaginary * no.imaginary * -1)
		i = (self.real * no.imaginary) + (self.imaginary * no.real)
		return MojoComplex(r, i)
  
  
start = time.time_ns()
var result: Int = 0
for i in range(10000000):
	let x: MojoComplex
	let y: MojoComplex
	let sum: MojoComplex
	let sub: MojoComplex
	let mul: MojoComplex
	x = MojoComplex(i + 91, i)
	y = MojoComplex(i, i - 2)
	sum = x + y
	sub = x - y
	mul = x * y
	result += (sum.real + sub.real + mul.real)
end = time.time_ns()
print(result)
print(end - start)

Output:

4750001345000000  
3006

The result was so breath-taking for me that I decided to create and print the result variable to make sure it is right. Truly incredible, around 7 million times faster. In the Mojo version we take advantage of the performant static struct type, fn methods and let variables when possible. If you declare a variable as var that could be let, Mojo warns you anyway.

My second test was just a random function with some mathematical operations.

Python version:

import time


def do_math_operations(a, b):  
	sum = 0  
	sum += (a + b)  
	sum += (a - b)  
	sum += (a * b)  
	return sum  


start = time.time_ns()  
result = 0  
for i in range(1000000):  
	result += do_math_operations(i*2, i)  
end = time.time_ns()  
print(result)  
print(end - start)

Output:

666667666665000000  
504218147

Mojo version:

let time = Python.import_module("time")  


def do_math_operations(a: Int, b: Int) -> Int:  
	var sum: Int = 0  
	sum += (a + b)  
	sum += (a - b)  
	sum += (a * b)  
	return sum  
  
  
start = time.time_ns()  
var result: Int = 0  
for i in range(1000000):  
	result += do_math_operations(i*2, i)  
end = time.time_ns()  
print(result)  
print(end - start)

Output:

666667666665000000  
2638

Again, the result is just mind-blowing. Simply no words. Around 190,000 times faster.

To test whether Mojo is really fully compatible with Python, I added a library from my previous project to predict whether it’s going to rain tomorrow. I didn’t change anything in the code, I just imported the library in Mojo and it ran successfully. It’s really fantastic to have this compatibility.

from PythonInterface import Python  
Python.add_to_path(".")  
let mypython = Python.import_module("my_python.predict_rain")

Only by importing the library the code is executed, because of the way the library is written.

Check this YouTube video that walks you through the Mojo playground and some examples to show you Mojo’s incredible performance. Also, I recommend you to go through, read and execute all of the notebooks in the playground, there are great and stunning examples with explanations.

🔥 Burn Mojo, Burn 🔥

I am really excited to have Mojo out and I see no reason why it wouldn’t be an immediate success. AI is present more and more in our lives (let’s put aside the question whether it is good or not, personally I think there should be limits) and I’m sure Mojo will be a great part of it. As things stand at the moment and the tests having been done so far, we have every reason to believe that Mojo’s flames will keep on burning, not to destroy, but to build and create …

Comments