Hamilton: how to translate C into compilation


This article is completely completed by individuals. If there are loopholes, please point out and correct them. Thank you!

There is no overall code in this article, only some important sections of code. What is provided is only some thinking traps, directions, expansion and personal experience. Please check it as needed.

Title Source: pre stage of computer composition of Beihang_ mips


1, C language implementation:

int edge[10][10];//store the information of edge
int visited[50];// whether the point has been visited
int output[10];//Path used during debugging
int length;
int hanmilton(int edge[][10],int start,int last,int v,int n,int flag); 
int main()
    int i;
    int n;
    int m;
    int a,b;
    for(i=0; i<n; i++) visited[i] = 0 ;
    int flag=0;
    }//(this process is optimized in mips, see later)
 return 0;
int hanmilton(int edge[][10],int start,int last,int v,int n,int flag)
    int j;
    return flag;

I use the adjacency matrix to store the edge information, and set the visited array to indicate whether it has been accessed.

The Hamilton function uses the idea of DFS (deep first search), that is, start from the starting point and continuously query the path in order. When the path length is n, judge whether the last point is connected with the starting point. If so, it returns 1. If not, continue to find the next path. The meanings of the function parameters are: start the starting point of the current path, a point on the last path, v the current inspection point, the number of n points, flag whether the Hamiltonian is found.

2, Translation process:

Firstly, the whole process is divided into four parts: input, initial, process and output. However, each part is not only a few lines in C language, but also includes many details of self maintenance or update.

1. input:

The key part of reading in is the use of two-dimensional arrays. As explained earlier in the course, mips further reflects the essence that two-dimensional arrays are actually one-dimensional arrays.

First, use. macro to write out the getindex part, so that the address in the array can be obtained through the row and column coordinates. Among them, when doing multiplication, i adopt shift operation to reduce the complexity. For example, 8 is used in this question × 8 array, doing i × 8, it is obviously easier to use i < < 3. If applicable, 50 × What about the array of 50? There are two ways. One is to drive it into 64 × 64. Forcibly scrape out i < < 6, but there is no doubt that this has caused a lot of redundant space and may even burst the memory; The second is to put i × 50 is simplified by shift, and the binary of 50 is expressed as (110010) 2, so i < < 1 + i < < 4 + i < < 5 can be used to replace multiplication. Of course, if there are a lot of 1 in binary representation, this method is not as simple as direct multiplication.

edge: .space 256        #edge[8][8]
.macro  getindex(%ans, %i, %j)
    sll %ans, %i, 3
    add %ans, %ans, %j          
    sll %ans, %ans, 2           
visited: .space 32      #visited[8]

The second is the mips writing method of loop, which is described in detail in the tutorial.

2. initial:

The initialization includes the zeroing of the array and the parameter setting of the first call function. In mips, I also classify the register allocation of function parameters as this part.

The function requires a total of 5 parameters, and the array parameter edge still needs 4. So I use the following seemingly useless steps to specify parameters and complete initialization at the same time.

#1 part: function parameter
li $t4, 0   # $t4 for flag in function
li $t5, 0   # $t5 for start in function 
li $t6, 0   # $t6 for last in function
li $t7, 0   # $t7 for v in function

At the same time, there are also the settings of length and loop variable j:

li $s3, 0   #$s3 length
li $t1, 0   #$t1 loop j

3. process:

In fact, it is the writing of function body.

The function body is composed of entry processing, loop (call) and exit processing. The difficult part is recursion. The middle part is mainly described below.

In recursion, there is only one line in C language

flag = hanmilton(edge,start,v,j,n,flag);

Become extremely complex. First of all, in general, we can write another separate block work to complete the calling process.

What does the work need to do?

One is to complete the stacking of the original function quantity and address: why not use the function parameters here, because the quantity we need to maintain manually is not just the function parameters. The code of this part is as follows:

sw $ra, 0($sp)
subi $sp, $sp, 4
sw $t4, 0($sp)
subi $sp, $sp, 4
sw $t5, 0($sp)
subi $sp, $sp, 4
sw $t6, 0($sp)
subi $sp, $sp, 4
sw $t7, 0($sp)
subi $sp, $sp, 4
sw $t1, 0($sp)
subi $sp, $sp, 4

First, the address is put into the stack, followed by the processing function parameters. Here, the function parameters include start ($t5), last ($t6), v ($t7) and flag ($t4). In fact, only last and v need to be updated in time, and start remains unchanged. In the process of translation, flag can be lazy and go to the end when it is equal to 1. The last is the special variable in the function body, which is j. In C language, we declare the variable j in the function body. With the progress of recursion, the newly declared j continues to cover the original j, and the original j continues to be restored when exiting the function. There is no such convenience in the assembly system, which is why we have to manually maintain other special variables. So here we put the value of j on the stack for later use.

The second is to update the function variables: that is, to determine the function parameters of the new calling function. Here, we need to fully understand the meaning of each variable. The code is as follows:

move $a0, $t1
move $s4, $t4
move $s5, $t5
move $s6, $t7
move $s7, $t0
move $t4, $s4
move $t5, $s5
move $t6, $s7
move $t7, $a0
jal hanmilton

In fact, the update here is slightly violent, but considering that there are many instructions here and there are few instructions, it should not cause timeout due to these lines, so it is not optimized.

The third is to restore the original function parameters after the call is completed: like entering the stack, all the quantities entering the stack need to be out of the stack in turn to complete the recovery. The order is opposite to the stacking order. The code is as follows:

addi $sp, $sp, 4
lw $t1, 0($sp)
addi $sp, $sp, 4
lw $t7, 0($sp)
addi $sp, $sp, 4
lw $t6, 0($sp)
addi $sp, $sp, 4
lw $t5, 0($sp)
addi $sp, $sp, 4
lw $t4, 0($sp)
addi $sp, $sp, 4
lw $ra, 0($sp)

The fourth is to return the address entering the work: just use the j instruction. Note that when returning, it does not directly return to the end of the function body, but to the place where the work comes in. Because the new function has been executed and the original function has not been executed, you should return to the incoming position to continue to complete the execution of the original function. In other words, the work part is not exactly equal to the function call, but includes the preparation before the function call, the whole function call process, and the recovery after the function call. In other words, you should continue to judge if(edge) after you go back v==1&&length==n)

Note: don't think everything is fine after writing recursion. Don't forget the exit processing of functions.

4. output:

The output part can write two small pieces end_1 and end_2:

One handles flag = 1 and the other handles flag = 2. In fact, when flag = 1 is encountered, you can directly use the jump instruction to adjust to the corresponding end, without executing to the corresponding end in sequence, and then ending the program.

3, Optimization:

The last point after the above process is completed is TLE, because the first cycle is redundant.

First of all, we might as well think that if there is a Hamiltonian ring, we should be able to find the ring from any point, that is, the points on the Hamiltonian ring are rotationally symmetrical as far as the Hamiltonian ring is concerned. Therefore, we can abandon the first large cycle, set only one starting point (may as well be 0), and then find it.

4, Experience:

When compiling the assembly, the writer is required to understand the program more clearly and deeply, and have an overall grasp of the process and execution order.

Here are some personal tips:

  • Modularization: if you haven't used to modularization before writing C language, you must divide the modules well when writing mips. The so-called "good division" does not only mean separation, but also requires that the functions of each part of the module you are divided into are clear and concise. At the same time, you should clearly understand the variables of each module and the relationship between them.

  • Patterning: in mips, conditional branch, loop, recursion and other statements have corresponding patterns. Patterns don't mean you need to call them completely, but you need to learn from them or be familiar with them. This is more conducive to form their own style and avoid all kinds of magical bug s.

  • Imitation: (this name is mainly to gather the "mold" in front.) this is a personal habit. You can perform it differently according to your preferences. The so-called imitation is actually running the program in your mind to clarify how the program is executed. I personally like to write with paper and pen, because paper and pen are more convenient for me to form the overall layout and modify. The corresponding relationship of variables stored in each register can be written out for easy viewing.

Tags: C MIPS

Posted on Sat, 09 Oct 2021 04:32:57 -0400 by jockey_jockey