'linked lists implementation in MIPS

I have a few questions about the code snippet below. It was part of assigment we had. I've already done it but my questions are regarding the provided code we were given to start with. Those parts are indicated by a "# DO NOT MODIFY ANYTHING...".

What I'm trying to understand is the last section that starts from "# DO NOT MODIFY ANYTHING BELOW THIS LINE". I was able to implement the functions because I know what they are suppsoed to give me but I want to understand how they work. We were told it wasn't necessary to know that much and to ignore the details.

So I know freePtr holds the address where the unused nodes list starts and also tracks how many nodes have been allocated so far, which helps alloc and free do their work. For freePtr, the first word is next alloc amount, second word is ptr to first free node. So in the init function why is the first word set to 1, and the second word to 0?

And why are all the registers initialized to 0? I think that's how you make the list of unused nodes but how does that work? Your just putting zeros in the registers and will probably change them before ever using them.

And how deos freePtr tell us how many nodes have been allocated?

    sll $t1, $t1, 1
    sw $t1, 0($t0)
    sll $t1, $t1, 2
    move $a0, $t1
    li $v0, 9

and what's the above section for in alloc? I checked and 9 in syscall is for dynamically allocating memory. But why do we do it here? And why are we shifting the bits here? I know that's how we multiply by 2^N but how is that useful here?

.data
    freePtr: .word 0 0 # first word is next alloc amount, second word is ptr to first free node
    listPtr: .word 0
    # listPtr: .word c1; c1: .word c3 2; c2: .word 0 4; c3: .word c2, -2
    opPrompt: .asciiz "Select an op (1: print, 2: insert, 3: delete, 4: sort, 5: search, other: quit): "
    insertPrompt: .asciiz "Enter a value to insert: "
    deletePrompt: .asciiz "Enter a value to delete: "
    searchPrompt: .asciiz "Enter a value to search for: "
    searchTrue: .asciiz "The value was in the list.\n"
    searchFalse: .asciiz "The value was not in the list.\n"

.text

# DO NOT MODIFY ANYTHING ABOVE THIS LINE

main:

    la $a0, freePtr
    jal init

    mainLoop:

    li $v0, 4
    la $a0, opPrompt
    syscall

    li $v0, 5
    syscall

    beq $v0, 1, doPrint
    beq $v0, 2, doInsert
    beq $v0, 3, doDelete
    beq $v0, 4, doSort
    beq $v0, 5, doSearch
    li $v0, 10
    syscall

    doPrint:
    la $a0, listPtr
    jal print
    b mainLoop

    doInsert:
    li $v0, 4
    la $a0, insertPrompt
    syscall
    li $v0, 5
    syscall
    move $a1, $v0
    la $a0, listPtr
    la $a2, freePtr
    jal insert
    b mainLoop

    doDelete:
    li $v0, 4
    la $a0, deletePrompt
    syscall
    li $v0, 5
    syscall
    move $a1, $v0
    la $a0, listPtr
    la $a2, freePtr
    jal delete
    b mainLoop

    doSort:
    la $a0, listPtr
    jal sort
    b mainLoop

    doSearch:
    li $v0, 4
    la $a0, searchPrompt
    syscall
    li $v0, 5
    syscall
    move $a1, $v0
    la $a0, listPtr
    jal search
    beq $v0, $0, searchOutputFalse
    li $v0, 4
    la $a0, searchTrue
    syscall
    b mainLoop
    searchOutputFalse:
    li $v0, 4
    la $a0, searchFalse
    syscall
    b mainLoop
    


# Print a list out to the console
# Arguments:
#   $a0 = address of a word holding the address the first node in the list
# Return values:
#   <none>
print:
    move $t0, $a0
    printLoop:
    lw $t0, 0($t0)
    beqz $t0, printRet          # loop until next node is null
    lw $a0, 4($t0)              # get current node's value
    li $v0, 1
    syscall                     # print current value
    li $v0, 11
    li, $a0, 32
    syscall
    b printLoop

    printRet:
    li $v0, 11
    li $a0, 10
    syscall
    jr $ra

# Insert into a list (at tail)
# Arguments:
#   $a0 = address of a word holding the address the first node in the list
#   $a1 = value to insert
#   $a2 = address of freePtr
# Return values:
#   <none>
insert:
    addi $sp, $sp, -12
    sw $ra, 0($sp)
    sw $a1, 4($sp)

    insertLoop:
    move $t0, $a0
    lw $a0, 0($t0)
    bnez $a0, insertLoop        # loop until *next* node is null
    sw $t0, 8($sp)              # save the existing last node onto stack

    move $a0, $a2               # set up parameter for alloc
    jal alloc

    lw $t0, 4($sp)              # load back new value from stack
    sw $t0, 4($v0)              # save new value into new node
    lw $t0, 8($sp)              # load the existing last node onto stack
    sw $v0, 0($t0)              # store the address of the new node into the existing last node

    lw $ra, 0($sp)
    addi $sp, $sp, 12
    jr $ra

# Delete from a list (first occurrence only)
# Arguments:
#   $a0 = address of a word holding the address the first node in the list
#   $a1 = value to delete
#   $a2 = address of freePtr
# Return values:
#   <none>
delete:
    addi $sp, $sp, -4
    sw $ra, 0($sp)
    
    deleteLoop:
    move $t0, $a0
    lw $a0, 0($t0)
    beqz $a0, deleteNotFound
    lw $t1, 4($a0)
    beq $t1, $a1, deleteFound
    b deleteLoop

    deleteFound:
    lw $t1, 0($a0)          # get the next node from the one to be deleted
    sw $t1, 0($t0)          # store the next node's address into the previous node
    move $a1, $a0           # move the current node's address into the correct register for free
    move $a0, $a2
    jal free

    deleteNotFound:
    lw $ra, 0($sp)
    addi $sp, $sp, 4
    jr $ra

# Sort a list in ascending order (recursively using merge sort)
# Arguments:
#   $a0 = address of a word holding the address the first node in the list
# Return values:
#   <none>
sort:
    addi $sp, $sp, -16
    sw $ra, 0($sp)
    sw $a0, 4($sp)
    sw $s0, 8($sp)
    sw $s1, 12($sp)

    jal count
    li $t0, 2
    beq $v0, $t0, sortBase      # if count == 2, perform a simple swap-check base-case
    bgt $v0, $t0, sortSplit     # if count > 2, perform recursive merge sort
    b sortRet                   # if count < 2, just return

    sortBase:                   # note: using a base-case for count == 2 is an optional optimization...
    lw $t0, 4($sp)              # ... (could just handle this using merge sort)
    lw $t0, 0($t0)
    lw $t1, 0($t0)

    lw $t2, 4($t0)              # base case has only two nodes, so load both values
    lw $t3, 4($t1)
    blt $t2, $t3, sortRet       # if nodes already in order, return
    sw $t3, 4($t0)              # if nodes not in order, swap values
    sw $t2, 4($t1)
    b sortRet

    sortSplit:                  # recursive case. Need to split, recurse, then merge
    srl $v0, $v0, 1             # find half of count
    lw $t0, 4($sp)              # restore starting address from stack

    splitLoop:
    lw $t0, 0($t0)              # loop until halfway through list
    addi $v0, $v0, -1
    bnez $v0, splitLoop

    move $s0, $t0               # Use an $s register to store "head" of the right half ...
                                # ... (aka last node of left half) **note: this is only allowed  ...
                                # ... because we saved $s0's existing value to the stack!
    
    move $a0, $t0               # sort right half of list (recursive call)
    jal sort
    lw $s1, 0($s0)              # store address of first node in right half

    sw $0, 0($s0)               # unlink right half of list
    lw $a0, 4($sp)              # restore the head of the left half from staci
    jal sort                    # sort left half of list (recursive call)
    lw $s0, 4($sp)              # restore the head of the first half from stack
    lw $s0, 0($s0)              # store address of first node in left half

    lw $t2, 4($sp)              # restore address of listPtr
    mergeLoop:                  # $s0 and $s1 have address of first node in each half; need to merge
    beqz $s0, mergeFromRight    # if left half is empty, just merge an item from right half
    beqz $s1, mergeFromLeft     # if right half is empty, just merge an item from left half

    lw $t0, 4($s0)              # load values from first node in each half
    lw $t1, 4($s1)
    bgt $t0, $t1, mergeFromRight # merge from the half with the lower value

    mergeFromLeft:
    sw $s0, 0($t2)              # link node from the left half into the final list
    move $t2, $s0
    lw $s0, 0($s0)
    b mergeLoop

    mergeFromRight:
    beqz $s1, doneMerge         # if both halves are empty, we're done merging
    sw $s1, 0($t2)              # link node from the right half into the final list
    move $t2, $s1
    lw $s1, 0($s1)
    b mergeLoop

    doneMerge:
    sw $0, 0($t2)               # make sure last node in final list has next pointer of null

    sortRet:
    lw $s1, 12($sp)
    lw $s0, 8($sp)
    lw $ra, 0($sp)
    addi $sp, $sp, 16
    jr $ra

# Count the number of items in a list
# Arguments:
#   $a0 = address of a word holding the address the first node in the list
# Return values:
#   $v0 = the number of items in the list
count:
    li $v0, -1
    countLoop:
    addi $v0, $v0, 1
    move $t0, $a0
    lw $a0, 0($t0)
    bnez $a0, countLoop
    jr $ra

# Search for a provided value in a list
# Arguments:
#   $a0 = address of a word holding the address the first node in the list
#   $a1 = value to search for
# Return values:
#   $v0 = 1 if the value was found, 0 otherwise
search:
    addi $sp, $sp, -4
    sw $ra, 0($sp)
    searchLoop:
    move $t0, $a0
    lw $a0, 0($t0)
    beqz $a0, searchNotFound
    lw $t1, 4($a0)
    beq $t1, $a1, searchFound
    b searchLoop

    searchFound:
    li $v0, 1
    b searchRet
    searchNotFound:
    li $v0, 0
    searchRet:
    lw $ra, 0($sp)
    addi $sp, $sp, 4
    jr $ra

# DO NOT MODIFY ANYTHING BELOW THIS LINE

# Initialize the free list (sets up a list of unused nodes you can take from as needed)
# Arguments:
#   $a0 = address of 2-word freePtr
# Return values:
#   <none>
init:
    li $t0, 1
    sw $t0, 0($a0)
    sw $0, 4($a0)
    li $t0, 0
    li $t1, 0
    li $t2, 0
    li $t3, 0
    li $t4, 0
    li $t5, 0
    li $t6, 0
    li $t7, 0
    li $t8, 0
    li $t9, 0
    li $a0, 0
    li $a1, 0
    li $a2, 0
    li $a3, 0
    jr $ra

# Allocate a new node (gets a node from the free list for your usage)
# Arguments:
#   $a0 = address of 2-word freePtr
# Return values:
#   $v0 = address of a newly allocated node that can be used in your list
alloc:
    move $t0, $a0
    lw $v0, 4($t0)
    bne $v0, $0, allocRet

    lw $t1, 0($t0)
    sll $t1, $t1, 1
    sw $t1, 0($t0)
    sll $t1, $t1, 2
    move $a0, $t1
    li $v0, 9
    syscall
    move $t2, $v0
    allocLoop:
    sw $0, 0($t2)
    addi $t2, $t2, 8
    sw $t2, -4($t2)
    addi $t1, $t1, -8
    bnez $t1, allocLoop
    sw $0, -4($t2)

    allocRet:
    lw $t1, 4($v0)
    sw $0, 0($v0)
    sw $t1, 4($t0)
    li $t0, 0
    li $t1, 0
    li $t2, 0
    li $t3, 0
    li $t4, 0
    li $t5, 0
    li $t6, 0
    li $t7, 0
    li $t8, 0
    li $t9, 0
    li $a0, 0
    li $a1, 0
    li $a2, 0
    li $a3, 0
    jr $ra

# Deallocates a node (places a node back into the free list)
# Arguments:
#   $a0 = address of 2-word freePtr
#   $a1 = address of node to free
# Return values:
#   <none>
free:
    lw $t0, 4($a0)
    sw $0, 0($a1)
    sw $t0, 4($a1)
    sw $a1, 4($a0)
    li $t0, 0
    li $t1, 0
    li $t2, 0
    li $t3, 0
    li $t4, 0
    li $t5, 0
    li $t6, 0
    li $t7, 0
    li $t8, 0
    li $t9, 0
    li $a0, 0
    li $a1, 0
    li $a2, 0
    li $a3, 0
    jr $ra


Solution 1:[1]

This allocation scheme uses syscall#9 to allocate chunks of memory.  Each time it allocates a chunk, it carves the whole chunk into nodes, and places all of those nodes onto the free list.  The free list always ends with a nullptr, so that's how it knows the free list is empty.

It also doubles the size of the chunk it allocates (from the "operating system", via syscall#9) each time it makes a request for a chunk of memory this way — this is a simplified version of a classic approach that tries to right-size the (operating system) allocated free memory for the workload of the application, while also amortizing the number of syscall#9's performed. 

An application that only uses a 3 nodes will have two allocations (of syscall#9), one for 1 node and 1 for 2 nodes.  Whereas an application that uses millions of nodes will start allocating chunks of larger and larger sizes (by powers of 2, so 1 node (8 bytes), then 2 nodes (16 bytes), then 4 nodes (32 bytes), but quickly will get to 128 node (1024 bytes).. then later 1024 nodes (8192 bytes).  And an application that uses millions of nodes will only incur maybe ~20 syscall#9s, rather than millions of them (which would be the case if they were allocated one at a time).  So, this is an exponential scheme that reduces operations system interaction for larger workloads, while allocating some approximation of the least amount of memory needed for the workload.

So in the init function why is the first word set to 1, and the second word to 0?

The first word is the initial number of elements to allocate using syscall#9, so the first time syscall#9 is invoked, it will allocate one single 8-byte chunk.

And why are all the registers initialized to 0?

This is to prevent callers (you) from relying on the call-clobbered registers, catching bugs in your usage of the calling convention.

I think that's how you make the list of unused nodes but how does that work?

Clearing registers is not about the free list.

The free list is constructed after syscall#9 allocations using the code in allocLoop, and, also in delete when a single node is returned to the free list.

And how does freePtr tell us how many nodes have been allocated?

If your question is about how many have been allocated from the operating system via syscall#9: it doesn't really know directly how many have been allocated, though that could be computed from the doubling value in freePtr, which holds the number of nodes to allocate next (so we know that all the lower values have been allocated from the o.s. via syscall#9).

If your question is about how many nodes have been allocated to the application vs. how many are on the free list, the answer is it also doesn't directly know that, it just knows that the next node on the free list is nullptr, then the free list is empty.

and what's the above section for in alloc?

That's the chunk-size doubler and allocation of a single chunk.

And why are we shifting the bits here?

The first shift is for the doubling.  In total it uses shifting to multiply by 8 because nodes are 8 bytes long.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Erik Eidt