-1

So I am writing a piece of code that prints out elements of an array within a for loop. The C version of this code looks like this:

   for(i=0; i<arraySize; i++)
    {
      printf("Array[%i]=%i\n", i, array[i]);
    }

The code I have in comparison in MIPS is this:

.data
    array:      .byte 2,3,5,7,11
    array_size:     .word 20
    array_print1:   .asciiz "Array["
    array_print2:   .asciiz "]="
    newline:    .asciiz "\n"
    sum_print:  .asciiz "Sum of the array is "
.text
    main:
        lw $s1, array_size                  #int arraySize = 5
        la $s0, array                       #int array[] = {2, 3, 5, 7, 11}
        
        add $t0, $zero, $zero                   #int i = 0
        
        j for                           #go to for                      
        for:
            bge $t0, $s1, outsidefor            #i < arraySize otherwise leave for
            
            li $v0, 4                   #printing 'Array['
            la $a0, array_print1
            syscall
            
            div $t2, $t0, 4
            mflo $t2
            li $v0, 1                   #printing index
            move $a0, $t2
            syscall
            
            la $v0, 4                   #printing ']='
            la $a0, array_print2
            syscall
            
            lw $t1, 0($s0)                  #getting array[i]
            
            li $v0, 1                   #printing array[i]
            move $a0, $t1
            syscall
            
            li $v0, 4
            la $a0, newline                 #printing new line
            syscall
            
            addi $t0, $t0, 4                #incrementing i
            addi $t1, $t1, 4                #incrementing $t1
            j for

Currently what is being printed out is the address of the value at array[i] (or so I believe). What am I doing wrong here?

  • You don't seem to update `$s0` so `lw $t1, 0($s0)` should always load the first number. Use your debugger/simulator to single step the code and see where it deviates from your intentions. Note that using `div` is quite an inefficient way to implement this. – Jester May 03 '21 at 22:20
  • On the line `lw $t1, 0($s0)` the code is not going as planned. I'm not sure how exactly I am supposed to increment `s0`. Do I just do `addi $s0, $s0, 4`? – Ishaan Aggarwal May 03 '21 at 22:33

3 Answers3

1

SO it turns out I had to increment $s0 by 4, by doing addi $s0, $s0, 4 and I also had to either switch lw $t1... to lb or switch the byte up top in the .data to a .word

1

Your C code uses array references, but you have converted your assembly code to using pointers.  I recommend that if you want to do this conversion, do it in C first, then you'll have matching C code to follow in your assembly code.

(Doing algorithmic conversions during translation to assembly is error prone, converting from array references to pointers is one such; another is changing while loop into do while)

The C code equivalent for what you're doing (perhaps now with your updates):

int array[] = ...
int arraySize = (sizeof array) * 4;

int main ()
{
    int *pi = array;
    for(i=0; i < arraySize; i += 4)
    {
        printf("Array[%i]=%i\n", i/4, *pi);
        pi++;  // in C, pointer increments automatically scale
    }
}

C knows the type of pi as pointer to int (word).  So, C automatically scales constants (here the 1 in pi += 1, which pi++ is short for) by the size of what is pointed to.

You can see that the C code does not need to increment i by 4 and divide it by 4 either — that should have been done with increment of just 1 and no division.

The C compiler knows data types and remembers them.  In assembly, it is mostly the programmer's job to understand types, and we need understand and use the same matching size when: declaring data, dereferencing, and pointer arithmetic — if any one of those is not in agreement, things won't work.


Array references for integer arrays in MIPS go like this:

la $s0, array       # this one can be moved out of the loop, but is ok inside the loop
sll $t6, $t0, 2     # scale i by 4
add $t6, $s0, $t6   # add to base of array
lw $t1, ($t6)       # access element of the array
Erik Eidt
  • 23,049
  • 2
  • 29
  • 53
  • Now you've de-optimized the pointer-increment code. The best way would just be two separate loop counters, one pointer (`addiu r,r,4`) one integer (`addi r,r,1`). Or since the code in the question had a `.byte` array, you *could* just run one loop counter, a pointer in `$s0`. To make a print_int syscall with the array index: `sub $a0, $t5, $s0`, where `$t5` is the array base address that you loaded once outside the loop. – Peter Cordes May 04 '21 at 04:49
  • (My first comment was just based on looking at the last code block, doing `array[i]` the hard way. Your C is fine, still using an integer and pointer-increment, and you do comment about keeping 2 separate counters). – Peter Cordes May 04 '21 at 05:37
1

With your answer (addi $s0, $s0, 4 to increment the pointer), you now have a normal pointer-increment loop over the array. But for some reason, you were already incrementing two other loop counters by 4. You could addu $t3, $t0, $s0 to add that byte-offset to the array base and get &array[i], ready to use with lw.

Using div to get the array index is incredibly inefficient. It would be much more efficient to simply use another register for another loop counter that increments by 1, instead of 4.

Also, if your array elements are all small, then an array index is a byte offset, or you can just use one subu instruction to convert a pointer to an element back to an index. (In C, i == &arr[i] - arr.)

Another option, good if you had .word elements, would be to run two counters: a pointer with add r,r,4 and an int index to print with add r,r,1.

.data
    array:      .byte 2,3,5,7,11
    array_end:
 .eqv   array_size,  5       # assemble-time constant.  But can't get MARS to calculate it from the actual array size, and hard-coding sucks.
    array_print1:   .asciiz "Array["
    array_print2:   .asciiz "]="
  # newline with print_char not print_string
    sum_print:  .asciiz "Sum of the array is "

.text
  main:
# only use $t registers; we don't need them to survive across a function call
      la   $t4, array                      # int8_t *arr = array;
      la   $t5, array_end                  # endp
      move $t0, $t4                        # int8_t *p = array

# we assume size is non-zero so we only need to check the count at the bottom
   loop:                           # do{
        li $v0, 4                    #printing 'Array['
        la $a0, array_print1
        syscall
        
        li   $v0, 1                   #printing index
        subu $a0, $t0, $t4            # i = p - arr
         # srl  $a0, $a0, 2         # if you had word elements, C pointer subtraction would involve scaling by the element size
        syscall
        
        li $v0, 4                   #printing ']='.  # This was la which works but isn't idiomatic
        la $a0, array_print2
        syscall
        
        li $v0, 1                   #printing array[i]
        lb $a0, 0($t0)              # sign_extend_int8_to_word (*p)
        syscall
        
        li  $v0, 11
        li  $a0, '\n'
        syscall                     # print_char(newline)
        
        addiu $t0, $t0, 1
        bne   $t0, $t5, loop    # }while(++p != endp);

      li   $v0, 10
      syscall            # exit()

Notice that I load into $a0 where I want the value, and otherwise don't waste move instructions. $a0 is a normal register just like $t1; it's not "reserved" for use by syscall, you just need your value there when syscall asks "the OS" (or in this case the emulator) to do something with the value there.

With all the code to print strings, the program doesn't look much smaller, but if you look at what's left after that, my version is much simpler:

  main:
# only use $t registers; we don't need them to survive across a function call
      la   $t4, array                      # int8_t *arr = array;
      la   $t5, array_end                  # endp
      move $t0, $t4                        # int8_t *p = array

# we assume size is non-zero so we only need to check the count at the bottom
   loop:                           # do{
            subu  $a0, $t0, $t4            # i = p - arr
               # for print_int
               ...
            lb    $a0, 0($t0)              # sign_extend_int8_to_word (*p)
               # for print_int
               ...
            addiu $t0, $t0, 1
            bne   $t0, $t5, loop    # }while(++p != endp);

      li   $v0, 10
      syscall            # exit()

Just 4 instructions of real work inside the loop to have i and arr[i] where we want them at the right times. (bne as the loop branch instead of j means we avoid a bge at the top). And all 4 instructions are simple efficient ones, not including div.

The rest is just printing.

(bge between two registers other than $zero is a pseudo-instruction for slt/bne; that's why in MIPS it's usually a good idea to calculate your loop end condition or count something down towards zero so you can use bne or beq as the loop condition. Your loop could have used beq since the count will be reached exactly.)

Also note that in my .data section, I avoided a .word 5 because I put a label on the end of the array so I could use la to get an end-pointer. Or I could have used .eqv array_size, 5 so I could later use li $t5, array_size. (Note li not lw or la. It will assemble to li $t5, 5, or more specifically addiu $t5, $zero, 5.

Putting a small integer constant into data memory and loading it from there with lw is inefficient so I avoid it.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847