To practice learning x64 AT&T Assembly (GAS), I implemented three solutions for Project Euler Problem 1 (find the sum of all the multiples of 3 or 5 below 1000).
The codes and a rough pseudocode sketch of each are shown below. I'd like some open-ended feedback (some more specific questions are at the bottom). To clarify, I am not asking to compare the following solutions (I'm aware of their algorithmic complexities), but they are all working pieces of Assembly that I would like feedback on.
Sorry for the poor code formatting, looks like 8-width tabs didn't carry over so well.
For all versions, compilation is simply gcc pe001.S -no-pie.
Version 1:
.global main
.text
main:
xor %rbx, %rbx # sum = 0
xor %rax, %rax # i = 0
sum3:
add %rax, %rbx # sum += i
add $3, %rax # i += 3
cmp max, %rax # if(rax < max)
jl sum3 # goto sum3
xor %rax, %rax
sum5:
add %rax, %rbx
add $5, %rax
cmp max, %rax
jl sum5
xor %rax, %rax
sub15:
sub %rax, %rbx
add $15, %rax
cmp max, %rax
jl sub15
mov $fmt, %rdi # printf(fmt, sum)
mov %rbx, %rsi
xor %rax, %rax # clear this (for printf to work properly)
call printf
xor %rax, %rax # return(0)
ret
fmt: .asciz "%d\n"
max: .quad 1000
Version 1 Algorithm:
int sum = 0;
for(int i=0; i<1000; i+=3)
if(!(i%3))
sum += i;
for(int i=0; i<1000; i+=5)
if(!(i%5))
sum += i;
for(int i=0; i<1000; i+=15)
if(!(i%15))
sum -= i;
Version 2:
.global main
.text
main:
mov $999, %rax # i = 999
xor %rbx, %rbx # sum = 0
mov $3, %rcx # dividends = 3, 5
mov $5, %r8
iter:
push %rax # save divisor (i)
xor %rdx, %rdx # set rdx to 0
div %rcx # i/3 => rax remainder rdx
pop %rax # restore divisor (i)
test %rdx, %rdx # check if remainder == 0
jz addts # if divides evenly, add to sum
push %rax
xor %rdx, %rdx
div %r8
pop %rax
test %rdx, %rdx
jz addts
deci: # decrement i
dec %rax
jnz iter
mov $fmt, %rdi # printf("%d\n", rbx)
mov %rbx, %rsi
xor %rax, %rax
call printf
xor %rax, %rax
ret
addts: # add to sum
add %rax, %rbx
jmp deci
fmt: .asciz "%d\n"
Version 2 Algorithm:
int sum;
for(int i=0; i<1000; i++)
if(!(i%3) || !(i%5))
sum += i;
Version 3:
.global main
.text
sumtm: # arithmetic SUM up To Max: int sum(int n)
mov max, %rax # i = floor(max/n) (result in rax)
xor %rdx, %rdx
div %rdi
mov %rax, %rcx # j = i+1
inc %rcx
imul %rcx, %rax # j *= i (= i*(i+1))
shr $1, %rax # j >>= 1 (= i*(i+1)/2)
imul %rdi, %rax # j *= n (= n*i*(i+1)/2)
ret # return j
main:
xor %rsi, %rsi # sum = 0
mov $3, %rdi
call sumtm
add %rax, %rsi # sum += sumtm(3)
mov $5, %rdi
call sumtm
add %rax, %rsi # sum += sumtm(5)
mov $15, %rdi
call sumtm
sub %rax, %rsi # sum -= sumtm(15)
mov $fmt, %rdi # printf("%d\n", sum)
xor %rax, %rax # needed for printf to work correctly
call printf
xor %rax, %rax # return 0
ret
fmt: .asciz "%d\n"
max: .quad 999
Version 3 Algorithm:
int sumtm(int n) {
int i = floor(999/n);
return n*i*(i+1)/2;
}
int sum = sumtm(3) + sumtm(5) - sumtm(15);
Questions:
- Best practices for naming? Is there a common length limit for labels? (From the examples I've seen, it seems like variable names are often very terse and somewhat cryptic.) Common casing convention?
- Choosing registers? This is the biggest trouble for me. The names are not very intuitive to me, and I'm not sure if there's a commonly-accepted set of guidelines on when to choose what. I was slightly influenced by caller-saved/callee-saved (e.g., use caller-saved registers in a function so as to not worry about pushing/popping it) and the use of explicit registers in certain operations (e.g., reuse
%raxas divisor, reuse%rsias second parameter forprintf). - Is it common/good practice to follow the ABI's callee/caller-saved registers even in small code snippets like this, and when you have complete control over the code? I'd assume that this is much more important when writing a library, but how important is it for completely self-contained code?
- Verbosity/comment density? Is this abnormal?
- Overall efficiency/operator choice?
I'm very new to Assembly, so any other open-ended feedback is welcome.
comparative-reviewto your question. \$\endgroup\$