Hello, and welcome to C++ Weekly. I'm your host, Jason Turner. In this episode I'm
gonna do a little bit of a comparison between Python and C++ and I'm, you know, yeah, it's a
little bit of clickbait, but it's also a real comparison, so I hope you stick around for it.
[Music]
Now before I continue I do want to remind you to check out the description of this
video because a lot of stuff happens behind the scenes where I collaborate with you all
on GitHub to come up with ideas and prioritize the topics for this channel, although sometimes
I just do the things that I want to do, and that's what I do to keep myself sane while
running a YouTube channel. So I have been playing lately with Game of Life, Conway's Game of Life,
in fact, and if you're not familiar with it then there are infinitely many resources for you to
actually play with it and read on Wikipedia and all of those things, and we can play and watch
an animation here. This one's got a little bit of a time lag thing going on to show you where cells
used to be active, but the basic idea is that there's simple rules for whether or not a cell
becomes active, stays alive or dies depending on how many neighbors it has, and the algorithm
for this can be quite simple. So I have a version written in C++ that I have the cellular Automata
struct type. I have not gone out of my way to make this the most efficient
version for C++ possible. The concept is pretty straightforward here - I have a "next member"
that makes a copy of the current state, and then it loops through every single element counting
its neighbors and then checking to see if the element that we are currently updating survives
or is born or dies, updates the result and then returns that copy back out. Now if you're paying
close attention here you may notice that I am, in fact, using a vector of bool, and you know what?
It's actually working all right, so I stuck with it, but this is the C++ implementation. Now what I
did was I took this exact same implementation and I applied it to Python as well. I did the exact
same algorithm - I took the C++ code, I copied it into a Python file, and I updated it to be Python.
So it's the same exact algorithm, and I know that there's going to be better ways to implement this
in both Python and in C++, but I wanted to do an apples to apples comparison as much as possible,
and then when it is done it prints out the current state. So I am running for
10,000 iterations. I have a 40 x 20 grid, and I have a single glider in it, and it executes for
10,000 iterations and then prints the result; and yeah, I'm compiling with full
optimizations because, again, we're trying to compare what's actually possible here.
I'm doing very, very simple timing right here. That executes in 0.08 seconds. That's
pretty consistent - 0.08, 0.09 seconds, and I have the equivalent, as I already stated,
life.py and it has a Point type and Automata type, and it has the exact same way that neighbors are
counted and the same way that the rules are evaluated and the same idea of a next function
that doesn't mutate the self, it just updates the return value and returns that. Then I have the
same rules, the same size, 40 x 20, a glider at the same place, I execute for 10,000 iterations,
and then I print the results. So if I were to run this with just the default C Python,
let's go ahead and time it...
There we go - it took 67.83 seconds, so a little over a minute, which is, shall we say,
considerably slower than the C++ version. So we had 0.08 seconds versus 67.83 seconds.
That makes the C++ version 847 times faster.
Now I know that you're sitting here going, "but no one who cares about performance would have
written code in that way," or "they wouldn't have just used the regular C Python implementation,
they would have used something like PyPy," and you're correct. This is
a jitted implementation of Python, so let's execute that. Let's execute PyPy3.
Look at that - 3.94 seconds. So for this particular example
PyPy is 17 times faster than C Python
and only 50 times slower than C++. That's pretty good, but in my experimentation it's not as good
as we could get. Now hopefully you watched the episode of C++ weekly that I recently aired about
cppyy with trivial interaction between C++ and Python, and I have an example of that here as
well. The Python file is pretty simple. In fact it fits in one screen here with my incredibly large
fonts. I have "import cppyy." I import some... actually that array is no longer useful in this
code, that was in a previous version. Anyhow, I create the Automata object with the same rule
set of 40 x 20. I add the glider for 10,000 iterations, execute it and then I print out
the results, and it shares a version of the C++ implementation that is almost exactly the same.
I had to change a couple of things because I only had C++ 17 available to me, not C++20, and, well,
it turns out that passing, like, a std::array of bools to this thing was way more difficult than I
expect it to be. So I created a version that takes a vector of bools and I pass a vector,
but that doesn't matter very much, it only impacts the startup cost. Then we execute it for 10,000
times, and it does exactly the same thing as the C++ version and the Python version before it.
So let's go ahead and run that, and I'm going to go back to C Python here because I've had a
little bit of difficulty in getting PyPy working with cppyy, even though it's supposed to be a
natural match - just isn't working for me, so let's go ahead and show this version first.
All right. 2.15 seconds. So that's, uh, what?
Yeah, so 2 (2.2) seconds versus 4.07 seconds. That's about two times faster again. So now
we're getting to the realm of pretty close to the C++ version. I'm at twenty-seven and a half times
slower, but the key point here is that all of that cost that we're paying is startup cost. We can
modify this one so that instead of 10,000 times we're executing, let's execute 20,000 times.
We're not going to see the time double to 4.4 seconds or something like that.
3.28 seconds, so it's definitely doesn't scale linearly, and if we could get PyPy
working with this, then we would expect to see better performance again. I'm just going
to go ahead and show you the error message that I get, which tells me that I have an
unqualified reference to a constructor name rather than a type in this context,
which is wrong. It's giving me a C++ error that makes no sense here right now,
but we've got that jitted Python is so much faster than regular C Python, but again this cppyy
module for Python is proving itself to be quite powerful and with pretty darn good performance.
So hopefully if there's anyone here who's more of an expert on these tools than I am,
be sure to check out the repository and figure out what I am doing wrong with trying to use
PyPy3 with my cppyy integration here and why that isn't going right. (Hello, future Jason here.
Um, shortly after recording this episode of C++ Weekly I realized that I had done a bit of a
disservice to the Python code here - in my attempt to keep it the same as the C++ code I actually
missed something rather important, and it comes down to this right here. Every single time I make
a call to count neighbors I am re-instantiating a neighbor's object. Now since this is
a tuple, if I understand my Python correctly (I'm definitely still learning Python things),
then that is immutable and I thought that the Python runtime would have moved this
into some sort of a constant, or something or another, which, well,
it didn't. So if I move this to being a class object so that it only gets initialized once,
then we see a bit of a performance difference here, so I'm going to do that. So as you can see,
the change was pretty simple here - I just moved this up to something that gets initialized at
the class scope, which is not at the object scope, but to reference it I still have to do
self.neighbors, and then, uh, here we go, our count_neighbors gets considerably simpler. So
let's go ahead and just rerun both tests so that we can see the full difference here.
Okay that's 52-ish seconds for the pre version with Python 3. I think that's pretty close to the
result that we got earlier, and I'm going to do PyPy3, and that's 3.16 seconds on this run. This
is very not scientific, right, but let's go ahead and move this to here so that we're at the class
scope, then we'll rerun those two tests and see what timing we get. Okay, so 43 seconds versus,
what did we just have? 52 seconds. That's a pretty significant difference, and let's run
this against PyPy3 so that we can try to do apples to apples to Python's comparisons. All right,
that went from 3.16 to 2.6 seconds, and yeah, there's a fair bit of variability here,
particularly when we get to these smaller numbers, but I think we can go ahead and pop up a graph now
on the screen and show you all the different timing results that we got for C++ versus Python
versus Python versus Python with C++ inside it. So hopefully this was a helpful update. Now you may
have noticed that there's another version over here, and it is a codon version, but we can't
talk about that yet. I have one more episode of C++ Weekly that I need to record and air before
we can talk about this particular version. Makes for an interesting story. Thanks for watching.)
[Music]