People who have been coding for years will tell you that abstraction is always a good thing. It makes the system more robust and tolerant to future changes. Recently, I’ve been working on some Python libraries that wrap C extensions. While working on those libraries, a couple of interesting things came up.This blog post is about one of those things, and how it affects the speed. When we talk about abstraction, we tend to think about hierarchies, encapsulations, and robustness in general. But what about the ways in which they are implemented underneath? Different languages implement this in their own way, and they are not always optimal. This is what we are going to talk about. Let’s dive in, shall we?
What exactly are we discussing here?
The word “abstraction” in general refers to the process of dealing with things independently of their associations. In the world of programming, abstraction can come in many forms like data abstraction, code abstraction, control abstraction, etc. Different languages provide different types of abstraction depending on their primary focus. In this blog post, we will talk about code abstraction. In object oriented languages such as C++, you can create abstract classes using the “virtual” keyword. Once we have such a declaration, the person who inherits the class has to implement the class and the associated functionality. This is very useful when we have a common theme and multiple extensions that can be based on that theme. Ideally, we don’t want the abstraction to affect the speed of our code. I mean, what’s the point in making our code robust if it makes our code slow and inefficient! As it turns out, different languages treat this differently. We will discuss it further soon.
Why should I care about abstraction?
The reason we want this nice feature is because it helps in reducing duplication of information in our code. We should never repeat anything, ever! If two classes are implementing something similar, it means that there is a layer of abstraction we are not utilizing. We should have a base class defining that common thing and these two classes should derive from it. This way, we keep our code clean and crisp. Also, if we want to update that common function at a later point of time, we only need to do it at one place. If we don’t have this layer of abstraction, we will have to go through all the classes and update the functionality. This is very inefficient! There is a very famous principle called DRY in the programming world. It stands for “Don’t Repeat Yourself”. No prizes for guessing what it means!
Can we get to the main point now?
Alright, enough with the background! Let’s look at some code now. We were discussing code abstraction earlier and how it affects the speed. Let’s look at the following Python code:
sum_values = 0 for i in xrange(999999): sum_values += i print sum_values
Pretty straightforward, right? Okay, now let’s abstract this code a bit:
sum_values = 0 class Accumulator: def __init__(self, init_val=0): self.sum_values = init_val def addValue(self, input_val): self.sum_values += input_val acc = Accumulator() for i in xrange(999999): acc.addValue(i) print acc.sum_values
We are not doing anything different here. It’s the same thing wrapped up in a class. So technically, this should take the same amount of time as our previous code, right? As it so happens, the second example takes around three times longer to execute than the first one.
Does it happen in C++ too?
Let’s do something very similar in C++.
#include <iostream> int main() { long int sum_values = 0; for (long i = 0; i < 999999; i++) sum_values += i; cout << sum_values ; }
Now let’s abstract accumulator into a class:
#include <stdio.h> class Accumulator { public: Accumulator() {sum_values = 0;} void addValue(long i) { sum_values += i; } volatile long sum_values; }; int main() { Accumulator acc; for (long i = 0; i < 999999; i++) acc.addValue(i); cout << acc.sum_values ; }
The above two pieces of code take more or less the same amount of time. Interesting, right? Adding abstraction in C++ doesn’t affect the speed, where as in Python, it takes three times longer.
Why did this happen?
We did the exact same thing in two different languages, and results were very different. An interesting thing to note is that C and C++ compilers can inline your code, where as interpreters for Python generally do not. This means that building abstractions in a high-level language, such as Python, has a big cost. In languages like C or C++, simple abstractions built around function calls basically don’t cost us anything.
The C++ compiler can inline your code even if you don’t explicitly specify it. The inline keyword really just tells the linker (or tells the compiler to tell the linker) that multiple identical definitions of the same function are not an error. The compiler cannot inline a function call, unless it has the full definition. If the function is not defined in the header, the compiler only has the declaration and cannot inline the function even if it wanted to. Nowadays, it’s not only the compiler that optimizes the code, but the linker can do that as well. A linker could inline function calls even if the function wasn’t defined in the same compilation unit.
This tells us that we have to be really careful when abstracting in languages like Python. If you have something that’s performance-critical, you should probably go with C or C++ where you can layer abstractions much more freely without fear of additional overhead.
———————————————————————————————————