I had some spare time and wanted to check out whether running miniRT with little to no dynamic allocations would improve or worsen its rendering time. Here, I share with you my conclusions.
During this experiment, I realized that allocating things locally makes it really easy to perform operations involving matrices and tuples. In fact, I believe that most of the speed increase we observe comes from the fact we need to do way less operations when not worrying about dynamic memory management. For example, let’s say we want to multiply three matrices to produce the transformation matrix of a shape. Allocating matrices dynamically would force us into doing something like this:
t_matrix *transformation(void)
{
t_matrix *a;
t_matrix *b;
t_matrix *c;
t_matrix *aux;
t_matrix *product;
a = matrix();
b = matrix();
c = matrix();
aux = multiply(a, b);
result = multiply(aux, c);
free(a);
free(b);
free(c);
free(aux);
return (product);
}
A lot of work, right? And we aren’t even accounting for possible allocation errors. Of course, in reality, you would probably receive some of these matrices as parameters to the function, but it doesn’t change the fact that you would need to free everything up in order to not have leaks.
Using the stack, this block would be much shorter:
t_matrix transformation(void)
{
t_matrix a;
t_matrix b;
t_matrix c;
t_matrix product;
a = matrix();
b = matrix();
c = matrix();
product = multiply(multiply(a, b), c));
return (product);
}
If you are feeling confident and just want things done as fast as possible, in exchange of some readability, you can even do something like this:
t_matrix transformation(void)
{
return (multiply(multiply(matrix(), matrix()), matrix()));
}
That’s not only faster, but takes less lines of code and is way less error prone than using the heap for everything. Neat!
Obviously, life is not only about unicorns and stack memory — allocating everything locally can cause its problems.
First and foremost, not using pointers to do things makes it a little harder to check if everything went right. Normally, if something goes wrong, we just return NULL and the previous function frame can know that it should abort the mission. Since we can’t return NULL in most cases when talking about the stack, we have to rely on functions that take whatever it needs to change as parameter, and make it return a boolean that will indicate something went wrong.
For example, let’s create our canvas so we can paint beautiful shapes in it:
t_canvas *canvas;
canvas = create_canvas(1920, 1080);
if (!canvas)
return (NULL);
That is pretty straightforward, right? But since our canvas is allocated on the stack, it can’t really be a pointer, nor receive a NULL value. So it would actually be something like this:
t_canvas canvas;
if (create_canvas(&canvas, 1920, 1080) != 0)
return (-1);
It may look like we did less work, so its probably better right? Well, it can be, depending on what you want to do, but in a big project, instantiating everything in the stack and passing it by reference can be a pain, especially if your structure is in the stack, but it’s members are stored in the heap.