I want to kick bad habits to the curb quick, and know if there's any optimizations I can make to this code
Sure thing. I'll look past the option to print all text at once without a loop though, that would be an optimization but it wouldn't be as instructive or interesting. The details that are left are all minor compared to the cost of printing anything, but so be it.
movb $0, %al
In general when possible, I recommend xorl %eax, %eax
(which you used later). Writing to 8-bit registers has complicated issues. Taking out the high bits of eax
as "collateral damage" isn't a problem here, so I'd go with the good old xorl %eax, %eax
, which doesn't have complicated issues and is in general the recommended way to zero a register.
loop patterns
The loop pattern has a lot of jumps and branches, all of them in the loop. Looping a known non-zero number of times can be done with just one branch. Even in general, you can do it with one branch and a jump outside of the loop (so it is executed less often).
For example, for the general case:
jmp Loop_Condition_Compare
Loop_Inside:
; stuff
Loop_Condition_Compare:
; some comparison
jcc Loop_Inside
Loop_Exit:
And if you know that the loop condition will be true the first time it would be evaluated in that general pattern, you can leave out the jump that goes to the loop condition, leaving only the jcc
. By the way jcc
is a short-hand for a conditional jump with whichever condition code that you need.
Iterating to zero can help save an instruction, but involves either counting down, or counting up through negative numbers, so this technique is not always easy or good to apply. I probably wouldn't use it in a case like this, but it's possible.
You can also align the loop, but when that helps and by how much is not easy to predict.
Prefer registers over stack
Putting i
on the stack is not necessary, it could be in a callee-save (aka non-volatile) register instead. For the x64 SysV ABI (used by x64 Mac and x64 Linux and generally almost anything that isn't Windows) that's rbx
, rbp
, r12
, r13
, r14
and r15
. Saving one of those (in the function prologue) and using it for i
will also mean that i
survives the function calls, and saves on stores and loads.
Alternatively, you could keep i
in a convenient register most of the time and only store it to the stack right before the call, and then load it back right after the call.
I don't know if maybe you're following a mental model of "local variables go on the stack" (which I absolutely wouldn't blame you for, that's a common oversimplification found in most textbooks and tutorial websites and so on), but that's a thing for compilers in -O0
mode. My general advice is: prefer to put values in registers. I intentionally write "values", because a variable doesn't need to be in the same place all the time, it could be in one place at one time and another place another time if that works out better in the code, so picking a location for a variable is not really the proper/complete way to look at it (it might lead you to miss optimization opportunities). Compilers also try to be a bit clever with this sort of thing, when not in -O0
mode.
addl $1, -20(%rbp)
\$\endgroup\$jne Loop_Inside
jmp Loop_Exit
Loop_Inside:
, you should inverse the condition and allow falling through in the code beneath:je Loop_Exit
Loop_Inside:
\$\endgroup\$