Search code examples
assemblymipsmips32

How do I read a value from an array from memory and then increment the counter in MIPS?


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?


Solution

  • 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.