The string being empty is indeed an edge case, but one that you can deal with in a simpler fashion. Instead of initializing your RAX index at 0, prime it with -1 and let the very first increment that happens in the loop already raise it to 0. That way, you'll have shaved off two instructions:
mov rax, -1
_strlen_loop:
inc rax
cmp byte [rdi + rax], 0
jne _strlen_loop
ret
Your string will never be that long that it would require a 64-bit length. Therefore we can safely use the shorter inc eax
instruction as well as the shorter mov eax, -1
that automatically zero extends into the full RAX register.
mov eax, -1
_strlen_loop:
inc eax
cmp byte [rdi + rax], 0
jne _strlen_loop
ret
If the length should turn out to be 0, then you should not run the incrementation loop that follows at all!
cmp bl, 1
jnz _no_carry_flag
Your BL 'carry flag' is limited to the values 0 and 1. Instead of the 3-byte cmp bl, 1
instruction, you could use the 2-byte test bl, bl
instruction and branch on the 'zero' condition:
test bl, bl
jz _no_carry_flag
cmp r11, 0
jg _asm_inc_for_1
The R11 register contains a length and that is to be considered an unsigned quantity, so better use the unsigned conditional jump instructions. Also, testing for zero is better done via the test <reg>, <reg>
instruction and because the length is surely very much smaller than 32-bit, we can use R11D:
test r11d, r11d
jnz _asm_inc_for_1
The code that actually adds 1 to the binary number jumps around too much, and jumping is expensive speed-wise!
Consider what happens when you have to add 1 to a binary character.
For "0" (00110000b), BL becomes 0 and the digit becomes "1"
For "1" (00110001b), BL stays 1 and the digit becomes "0"
The new BL is identical to the lowest bit of the ASCII code for the digit that is to be modified (i) , and the binary character just has to toggle which you can do with a single xor byte [rdi + r11], 1
instruction (ii).
_asm_inc_for_1:
dec r11d
test bl, bl
jz _no_carry_flag
mov al, [rdi + r11]
mov bl, al
and bl, 1
xor al, 1
mov [rdi + r11], al
_no_carry_flag:
test r11d, r11d
jnz _asm_inc_for_1
And then for the killer loop optimization. As soon as BL becomes 0, you can stop the loop because no more changes will be made to the digits on the left that remain in the number.
When the incrementation loop did not produce a final carry, you leave the result where is was stored originally, but when a final carry does exist, you copy the result to another buffer (because you need the extra space). An easier approach would be to always keep one extra byte free at the start of the string. Then adding the new "1" becomes real easy...
Next snippet implements most of the above. I also prefer to use the ECX register instead of the R11D register because that shaves off an additional REX prefix from the encoding.
call _strlen ; -> RAX
mov ecx, eax
dec ecx
js _DONE ; (iii) if taken then empty string
mov bl, 1 ; carry flag = true
_LOOP1:
mov al, [rdi + rcx]
mov bl, al ; future carry flag
xor al, 1 ; new digit
mov [rdi + rcx], al
and bl, 1 ; new carry flag [0,1]
jz _DONE ; (iii) if taken then carry clear
dec ecx
jns _LOOP1 ; (iii) if taken then more digits
dec rdi
mov byte [rdi], "1" ; filling the kept-free-byte
_DONE:
mov rax, rdi ; return value = new string
ret
The new BL is identical to the lowest bit of the ASCII code for the digit that is to be modified (i),
The code copies the current character in AL to the BL register and then performs a bitwise and
with the mask value 1. This will zero every bit except the lowest bit:
AL 00110000 ASCII code for "0" 00110001 ASCII code for "1"
BL 00110000 ASCII code for "0" 00110001 ASCII code for "1"
00000001 The AND mask 00000001 The AND mask
-------- --------
BL 00000000 New 'carry flag' 00000001 New 'carry flag'
and the binary character just has to toggle which you can do with a single xor byte [rdi + r11], 1
instruction (ii).
The code has the current character in the AL register and then performs a bitwise xor
with the mask value 1. This will keep every bit unmodified except the lowest bit that toggles state:
AL 00110000 ASCII code for "0" 00110001 ASCII code for "1"
00000001 The XOR mask 00000001 The XOR mask
-------- --------
AL 00110001 ASCII code for "1" 00110000 ASCII code for "0"
Very often we don't need to cmp
before deciding about a conditional branch (iii)
Many instructions define flags according to the result of their operation.
In dec ecx
js _DONE
, the dec
instruction defines the OF, SF, ZF, AF, and PF. (It doesn't touch the CF). The js
that immediately follows is based on the SF that got defined alright.
Here the loop gets completely bypassed if ECX became -1 (same as SF=1), because that would have meant that the binary string had a length of 0.
In and bl, 1
jz _DONE
, the and
instruction defines the SF, ZF, and PF. (It also resets the OF and CF. The effect on AF is undefined). The jz
that immediately follows is based on the ZF that got defined alright.
Here we leave the loop early as soon as BL=0 (same as ZF=1).
In dec ecx
jns _LOOP1
, the dec
instruction defines the OF, SF, ZF, AF, and PF. (It doesn't touch the CF). The jns
that immediately follows is based on the SF that got defined alright.
Here the loop stops iterating as soon as ECX becomes -1 (same as SF=1), because that signals that we have just finished processing the leftmost digit of the binary string.
The dec rdi
can't go 'out of bounds' since the premise was that you kept an extra byte at the ready at the far left of the number.
There's no risk of writing the special "1" every single time. It only gets added in case the loop has to iterate over all of the digits of the binary string. When the early out from finding BL=0 is taken, that part of the code gets bypassed (the label _DONE:
sits below it in the source).
ps. I write this in the answer because on the computer that I'm using right now, I can't post comments.