6
\$\begingroup\$

I'm just starting out in MIPS (as is evident by the fact that I'm writing a FizzBuzz program), and my program runs fine and gives the desired output, but I feel like there is a way to do it without as many branches.

.data

    fstring:    .asciiz     "Fizz\n"
    bstring:    .asciiz     "Buzz\n"
    hstring:    .asciiz     "FizzBuzz\n"

.text

.globl main

main:
    li  $t0, 0

loop:
    bgt     $t0, 99, exit
    add     $t0, $t0, 1
    rem     $t1, $t0, 15
    rem     $t2, $t0, 3
    rem     $t3, $t0, 5
    beq     $t1, 0, fizzbuzz
    beq     $t2, 0, fizz
    beq     $t3, 0, buzz
    li      $v0, 1
    move    $a0, $t0
    syscall
    move    $t0, $a0
    li      $a0, 10
    li      $v0, 11
    syscall
    b       loop

fizzbuzz:
    la      $a0, hstring
    li      $v0, 4
    syscall
    b       loop

fizz:
    la      $a0, fstring
    li      $v0, 4
    syscall
    b       loop

buzz:
    la      $a0, bstring
    li      $v0, 4
    syscall
    b       loop

exit:
    li      $v0, 10
    syscall

Is there a way to sure up this program or is it relatively concise for what it's supposed to do?

\$\endgroup\$
3
\$\begingroup\$

Your code looks clean. Fizzbuzz is a very simple program and the most frequent reasons people get it wrong is because they go for optimizations that do not exist, rather than focusing on solving a rather simple problem.

Anything you do to optimize away those 3 branch statements is just calling for potential bugs down the line. However, in the spirit of codereview, I will give a suggestion:

DRY (Don't Repeat Yourself)

Two repetitions I noticed.

  • Printing a string (all the li $v0, 4)
  • Printing a new line.

These can be done away with by coordinating between branching and jump instructions i.e. function calls.

.data

    fstring:    .asciiz     "Fizz\n"
    bstring:    .asciiz     "Buzz\n"
    hstring:    .asciiz     "FizzBuzz\n"
    nl:         .asciiz     "\n"

.text

.globl main

main:
    li  $s0, 0

loop:
    bgt     $s0, 99, exit
    addi    $s0, $s0, 1
    move    $a0, $s0       # the function takes input with registers $a0 to $a3
    jal     check_fizzbuzz # call the function
    move    $a0, $v0       # the function returns a string in register $v0, so move it to $a0 so that it can be printed
    li      $v0, 4
    syscall                # print the string
    b loop

check_fizzbuzz:
    la      $v0, hstring
    rem     $t1, $a0, 15
    beq     $t1, 0, return_fizzbuzzz

    la      $v0, fstring
    rem     $t1, $a0, 3
    beq     $t1, 0, return_fizzbuzzz

    la      $v0, bstring
    rem     $t1, $a0, 5
    beq     $t1, 0, return_fizzbuzz

    li      $v0, 1
    syscall
    la      $v0, nl

return_fizzbuzz:
    jr $ra

exit:
    li      $v0, 10
    syscall
\$\endgroup\$
0
\$\begingroup\$

I never did MIPS Assembly, so I decided to try on this simple one.

I will comment on your code mostly from performance point of view (as smac89 covered simplicity/readability variant well).

In your case I wouldn't be afraid so much of branching (and it's not trivial to lower amount of branches down), but about number of integer divisions (rem) and also syscall outputting integers (hidden divisions).

From the limited info I was able to found about real world MIPS architecture implementation it looks like for example PIC32 MIPS32 M4K Core does use about 1 cycle for 1 bit during divide operation, maybe with some early exit optimizations, so in worst case it's about ~32 cycles for 32b/32b divide.

I tried to use MARS and it works OK, but I don't see any serious performance information except simple instruction counters, plus syscall is not counted into the stats at all.

So outputting integer is "for free", while in real world it would hurt so much that it would be probably better to keep just string representation of t0 and increment it as string, avoiding binary integer (at least with my code it would work, as I don't do rem on it, so I don't need integer form of "number").

Anyway I didn't go that far, only reworked your loop to avoid divisions, and also to avoid pseudo instructions when possible, so the amount of real instructions generated is similar to the source code.

.data

fizzstring: .asciiz "Fizz"
buzzstring: .asciiz "Buzz"

.text
.globl main

main:
    move    $t0, $zero  # num = 0
    li  $t1,1       # really handy constant 1
    li  $t2,100     # max_num to loop
    li  $t3,3       # countdown to divisible by 3 (mod 3)
    li  $t5,5       # countdown to divisible by 5 (mod 5)
loop:
    add     $t0, $t0, $t1   # ++num
    move    $a0, $zero  # did print some text = false
    sub $t3, $t3, $t1   # --mod 3 counter
    sub $t5, $t5, $t1   # --mod 5 counter
    bnez    $t3, Mod3NotZero
    # when 0 == mod 3: reset counter and output "Fizz"
    li  $t3, 3
    la      $a0, fizzstring
    li      $v0, 4
    syscall
Mod3NotZero:
    bnez    $t5, Mod5NotZero
    # when 0 == mod 5: reset counter and output "Buzz"
    li  $t5, 5
    la      $a0, buzzstring
    li      $v0, 4
    syscall
Mod5NotZero:
    bnez    $a0, SkipNumberPrint    # don't print number when text was
    # print number
    move    $v0, $t1
    move    $a0, $t0
    syscall
SkipNumberPrint:
    # print new line char
    li  $a0, 10
    li  $v0, 11
    syscall
    bne $t0, $t2, loop  # loop until (num == max_num)
    # exit
    li  $v0, 10
    syscall

So... I just guess it would run faster on real HW. But at least in simulator it does use about ~60% of instructions to run (~1500 vs ~2500), to use some measurable (but almost meaningless) number.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.