After examining the C++ optimizer and specifically reviewing loop unrolling, in this video, Amir Kirsh will dig deeper into loop unrolling scenarios, implications of undefined behavior in your code, and the effect of the optimization flag on the compiled code. Let’s get started.
Transcript: The C++ Optimizer and Loop Unrolling
My name is Amir Kirsh and I’m a dev advocate at Incredibuild.
Today I want to continue from my previous talk and cover a bit more simple C++ compiler, optimizer, and loop unrolling.
So, let’s just jump into Compiler Explorer, which is the online tool that I’m using to see the C++ code being compiled to assembly I hope you can see my screen and I’m going to write a very simple function. Let’s call it foo. Foo is getting an integer and returning an integer and inside foo, I want to have a simple loop. I want to loop on I and stop when i get to 10 and increment i, and at the end, I want to return i. So, if you take a look at this function, well, there are two possible results here, either we get i that is smaller than 10, in which case the return value would be 10, or we get i, which is bigger or equal to 10. And then we return just i, so either foo would return i, or foo would return 10, and the question is whether the loop would appear in the actual assembly and the answer well, it depends on the level of optimization.
If our optimization level is -O2, then indeed there is no need for a loop and there would not be a loop. If I change it here to -O0, you would just see a loop inside, here we can see that because we asked the compiler not to do any optimization. There’s actually a loop here, somebody is comparing the value to nine. And then decide whether to get back to the loop or end the loop.
So let’s go back to -O2. And the function becomes a simple if, a simple comparison, we take the 10 and put it in the return value register for integers. And then we compare the value in the return value register to the value that we got to the parameter, that seats in EDI register. And then we have a conditional move. If the previous comparison is greater or equal, then we copy the EDI register to the return value. Otherwise we do not do that. And we keep the 10 inside. So eventually what it will return is either 10 or i, depending on whether i was bigger than 10 or not, which is a simple if. So we saw here that the compiler can take a loop and turn it into something which does not need the loop. And this is called loop unrolling. And this is quite a simple example, much more simple than the one we saw in the previous video, even though the previous one was a bit more realistic, this one is a toy example.
Let’s add a main to our example, and let’s call foo in the main. And we can see again that we have here something in main and the same function in foo, let’s return something, let’s return the return of value of foo. So we return foo with, and we can see that there is no need to actually to call foo. The compiler decides, or the optimizer decides that, okay, we are going to return 120 because in some way, the foo is being in-lined. And the result is just 120. If we change it to five, then we can see that the results gives us just 10. And there’s no need to actually call foo, because in a way foo is being in-lined into main. And then the optimizer can just do the if, at compile time and decide whether to return from main 10, or some other value, depending on the initial value that they send to foo.
Let’s do something more in main, let’s for example, do some kind of a loop. Let’s have int i in the main. And we look and let’s decide to return i, return i, so we have something a bit similar to what we had in foo, but here i is not a variable that we get as an argument, we just define i, we have an i variable, local variable. We run in the loop and then we return i. And if we take a look at the assembly code, we see that the main just returns 10, why does the main return 10? Does it mean that the local variable is being initialized with zero? No, it doesn’t mean that, but it means that the optimizer assumes something, we have here undefined behavior, the undefined behavior is that we are using an uninitialized variable. And once you are using an uninitialized variable, the optimizer can assume anything. And here the optimizer decided to assume that i is less than 10 or less equal than 10, because if i was bigger than 10, then the results should be i, and this was not the case. We just return 10. So the optimizer assumes here something. And it is okay. It is legitimate because once we are having an undefined behavior, like for example, we are condition on something which is uninitialized, the optimizer can assume anything that it needs or wants to assume in order to optimize our code. And in this example, there is no need to actually check i, because, well, i is uninitialized, let’s assume it’s smaller or equal to 10.
Can we see an actual loop in the main? Well, if we go back to -O0, then we may find the loop, even in the main, because we ask the optimizer not to, or the compile not to optimize our code. I’m not telling you to use undefined behavior. No, do initialize your variables, but you have to know that once you do not initialize a variable or once your code is being dependent on undefined behavior, it means that the optimizer and the compiler can assume things. And then when you move your code to another compiler or use some other optimization flag, your code may behave differently, which is the result of an undefined behavior.
So we talked today about loop unrolling, C++ optimizer, and a bit about undefined behavior, see you in our next video. Thank you very much for being with us. Bye bye.