-2
#include <iostream>

using namespace std;
//Reference: https://www.geeksforgeeks.org/print-sums-subsets-given-set/

//==============testcase1==============
int arr[] = { 5, 4, 3 };
int n = 3;
//output: 12 9 8 5 7 4 3 0
//=====================================

//==============testcase2==============
//int arr[] = {2, 3};
//int n = 2;
//output: 5 2 3 0
//=====================================

// Prints sums of all subsets of arr[l..r]
void subsetSums(int arr[], int l, int r, int sum)
{
    // Print current subset
    if (l > r) {
        cout << sum << " ";
        return;
    }

    // Subset including arr[l]
    subsetSums(arr, l + 1, r, sum + arr[l]);

    // Subset excluding arr[l]
    subsetSums(arr, l + 1, r, sum);
}

// Driver code
int main()
{
    cout << "output: ";
    subsetSums(arr, 0, n - 1,0);
    return 0;
}

I want to convert to R64I assembly code. Here is what I have done.

# Reference: https://www.geeksforgeeks.org/print-sums-subsets-given-set/
.data
.align 4
# =========testcase1===========
arr: .word 5, 4, 3
n: .word 3
str: .string "output: "
space: .string " "
# output: 12 9 8 5 7 4 3 0
# ==============================

# =========testcase2===========
#arr: .word 2, 3
#n: .word 2
#str: .string "output: "
#space: .string " "
# output: 5 2 3 0
# ==============================

.text
.global _start
# Start your coding below, don't change anything upper except testing different testcase

_start:
   #la t0, aa
   #auipc t0, 0x1
   #nop
   #nop
   #addi t0, t0, 148
   #nop
   #nop
   li a7, 4
   la a0, str
   ecall
   #load arr, 0, n, 0
   la a0, arr
   la a1, 0
   la a2, n
   la a3, 0
   jal subsetSums
   j end       # Jump to end of program
subsetSums:
   mv t0, a0
   mv t1, a1
   mv t2, a3
   # if l > r print sum
   blt a2, a1, sum
   add t3, a1, 0
   slli t3, t3, 2
   addi t3, a0, t3
   lw t4, 0(t3)
   add a3, t2, t3
   addi a1, t1, 1
   jal subsetSums
   addi a1, t1, 1
   add a3, t2, 0
   jal subsetSums

sum:
   mv t0, a3
   # Print sum
   li a7, 1
   addi a0, t0, 0
   ecall
   #Print 
   li a7, 4
   la a0, space
   ecall

   lw   ra, 0(sp) # Reload return address from stack
   addi sp, sp, 4 # Restore stack pointer
   jr x1

end:nop

I want to do 2 testcases. First one from arr: .word 5,4,3 and get the output: 12 9 8 5 7 4 3 0. However, I don't know how to print out the strings and numbers. I have just learned Hello World for two years. I have never learned Assembly code before and have no idea what I'm doing. EDIT2: Thank you for all your reply. I have fixed how to output string and number, however I get a bug in this code

    subsetSums:
   mv t0, a0
   mv t1, a1
   mv t2, a3
   # if l > r print sum
   blt a2, a1, sum
   #BUG
   add t3, a1, 0
   slli t3, t3, 2
   addi t3, a0, t3
   lw t4, 0(t3)
   add a3, t2, t3
   addi a1, t1, 1
   jal subsetSums
   addi a1, t1, 1
   add a3, t2, 0
   jal subsetSums
   #BUG

It seems I can't put the correct arguments in my function. As for why I use nop in end:, I have to do that to follow my professor.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
  • 4
    Probably the easiest way to convert C++ code into assembly code for a given architecture is to compile it with a C++ compiler targeting that architecture. – Igor Tandetnik May 27 '23 at 14:43
  • @IgorTandetnik Then what is the point of learning Assembly Code if all you get to do is just converting? Also, I can't find any sources that can convert C++ code to RISC-V R64I format. – Thitiwat Thongprasertsang May 27 '23 at 15:13
  • 1
    Don't mix x register names like `x1` with friendly register names like `ra`, very confusing — just stick with the friendly names. – Erik Eidt May 27 '23 at 16:05
  • 1
    Use the `exit()` system call instead of jumping to a `nop`. – Erik Eidt May 27 '23 at 16:46
  • C++ compilers can make good examples for you to see how they did it. e.g. https://godbolt.org/z/c78Tced9K shows `clang -Og` output for RV64g (not gc since I used `-march=rv64g`), with demangling for C++ symbol names. It should be equivalent to RV64I since the program doesn't use any FP math. Printing with `cout <<` compiles to a significant amount of asm and you'd have to rewrite that part for RARS's system calls, if you're using that or another simulator. See also [How to remove "noise" from GCC/clang assembly output?](https://stackoverflow.com/q/38552116) – Peter Cordes May 28 '23 at 05:32

1 Answers1

2

It is a red herring to consider recursion as somehow uniquely problematic.

While it requires knowledge, the simple, proper approach is to follow the calling convention.  As long as the environment has a runtime call stack (all modern CPU environments do) then recursion can be seen simply as a subset of function calling — a subset that has no special requirements!

The calling convention says how one function should call another function, and that, the two functions can be in separate compilation units (meaning that they cannot see each other's internal code sequence and their register choices) and that the callee may change/version without needing to change its callers, as long as the function signature doesn't change (parameter list of variables & types, and return value & type).

To reiterate, recursion adds no additional specifications or requirements to the standard calling convention.

(It is true, however, that a simple, directly recursive function is in the same compilation unit as itself, and if we know the internal register usages of some function we can apply custom calling optimizations that violate the standard calling convention (provided the function doesn't also participate in standard calling such as from another compilation unit).  However, if your purpose/goal is to learn the proper way of function calling e.g. by the standard convention, such optimizations are more advanced topics than your goal.)

In more detail, the calling convention says how to:

  • pass & receive parameters & return values
  • what registers must be preserved for the benefit of the function's callers
    • and also what registers do not require preservation
  • and symmetrically, for your function's benefit
    • what registers are preserved by a function call
    • what registers are clobbered by a function call
  • how to use stack space, for register preservation, value preservation, or local storage

From these specifications, and an analysis of the variables used within the function, we can determine which values need to be preserved for the functions own benefit, potentially including return address, parameter variables, and local variables (including temporary variables).

Given that, you can have a recursive fib as follows:

int fib(int n) {
    if (n <= 1) return 1;
    return fib(n+1) + fib(n+2);
}

And, following the calling convention, the assembly/machine code for that will be identical to the following non-recursive function (substituting recursive fib for non-recursive fib2).

int fib(int n) {                   // non-recursive fib function
    if (n <= 1) return 1;
    return fib2(n+1) + fib2(n+2);  // uses fib2 instead of itself
}

So, the advice is to simply follow the calling convention and not worry about recursion.

Erik Eidt
  • 23,049
  • 2
  • 29
  • 53