hanmilton:如何将C翻译成汇编之心得体会

前言:

本文完全由个人完成,如果有漏洞烦请指出改正,谢谢!

本文中并无整体代码,只有部分重要段的代码。所提供的仅仅是一些思考的陷阱、方向、拓展以及个人的心得体会,请按需查看。

题目来源:北航计算机组成pre阶段_mips

 

一、C语言实现:

#include<stdio.h>
#include<stdlib.h>
#include<math.h>
int edge[10][10];//store the information of edge
int visited[50];// whether the point has been visited
int output[10];//调试过程所用的路径
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;
    //input:
    scanf("%d%d",&n,&m);
    int a,b;
    for(i=1;i<=m;i++){
        scanf("%d%d",&a,&b);
        edge[a-1][b-1]=1;
        edge[b-1][a-1]=1;
    }
    //initial:
    for(i=0; i<n; i++) visited[i] = 0 ;
    int flag=0;
    //process:
    for(i=0;i<n;i++){
        if(hanmilton(edge,i,i,i,n,0)==1)
        flag=1;
    }//(此过程在mips中得到优化,详见后文)
    //output:
    printf("\n");
    printf("%d",flag);
 return 0;
}
int hanmilton(int edge[][10],int start,int last,int v,int n,int flag)
{
    visited[v]=1;
    int j;
    length++;
    output[length]=v;
    
    for(j=0;j<n;j++){
        if(edge[v][j]==1&&visited[j]!=1){
            flag=hanmilton(edge,start,v,j,n,flag);
        }
        if(edge[v][start]==1&&length==n){
            output[++length]=start;
            flag=1;
            break;
        }
    }
    
    length--;
    visited[v]=0;
    return flag;
}

我采用的是邻接矩阵来存储边的信息,并设置了visited数组来表示是否被访问过。

hanmilton函数使用DFS(deep first search)思想,即从出发点开始,按顺序不断查询路径,当路径长度是n时,判断最后一个点是否与出发点相连,如果是,则返回1,如果不是,继续查找下一路径。函数参数含义分别为:start当前路径出发点,last路径上一个点,v当前考察点,n点的数目,flag是否找到哈密顿还的标志。

二、 翻译过程:

首先将整个过程分为四大部分:读入(input)、初始化(initial)、处理(process)、输出(output)。但每个部分却并非只有C语言中短短的几行,还包括很多自己维护或者更新的细节。

1. input:

读入的重点部分在于二维数组的使用。在课程前面有讲解,在mips更加体现了二维数组其实是一维数组的本质。

首先利用.macro写出getindex部分,以便通过行列坐标即可得到在数组中的地址。其中,在做乘法时我所采取的是移位运算来减少繁琐度。比如本题中所用的是8×8的数组,在做 i × 8 的运算时,用 i << 3 显然更为简便。可如果应用 50×50 的数组呢?这里就有两种方式,其一是开成 64×64,强行凑出 i << 6,但这毫无疑问造成了大量的冗余空间,甚至可能爆内存;其二是将 i × 50 用移位来简化,50 的二进制表示为(110010)2,所以就可以用 i << 1 + i << 4 + i << 5 来替代乘法。当然,如果二进制表示中1很多那这种方式反而不如直接算乘法来的简便。

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

其次就是循环的mips写法,这一部分在教程中有详细的阐述。

2. initial:

初始化中有数组的置零 和 初次调用函数的参数设置。在mips中,我将函数参数的寄存器分配也归为这一部分。

函数一共需要5个参数,出去数组参数edge,仍需要4个。所以我用以下看似无用的步骤来名确参数同时完成初始化。

#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

同时还有length和循环变量j的设定:

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

3. process:

其实就是函数体的书写。

函数体就是由进入处理、循环(调用)、退出处理组成,困难部分在递归,下文着重阐述中间部分。

在递归时,C语言中短短一行

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

变得极为复杂。首先,一般调用时我们可以写另一个单独的块 work,来完成调用的过程。

那work都需要做哪些工作呢?

其一是完成原函数量地址进栈:这里为什么没有用函数参数呢,因为所需要我们手动维护的量并不仅仅是函数的参数。此部分代码如下:

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

首先是地址进栈,其次是处理函数参数,这里函数参数有start($t5)、last($t6)、v($t7)、flag($t4)。事实上,只有last和v需要及时更新,start一成不变,而在翻译过程中,flag可以偷懒等于1时直接到底。最后是函数体中的特殊变量,在这里也就是 j 。在C语言中,我们在函数体中声明变量 j ,随着递归过程的进行,新声明的 j 不断覆盖原来的 j ,在退出函数时又不断恢复原先的 j 。在汇编系统中可没有这样的便利,这也是为什么我们还要手动人为维护其它的特殊变量的原因。所以此处我们将 j 的值也进栈,以便后来使用。

其二是完成函数变量的更新:也就是确定新调用函数的函数参数。这里需要对每个变量含义的充分理解。代码如下:

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
nop

事实上此处的更新略显暴力,不过考虑到这里多的指令数较少,应该不会因为这几行而造成超时,故没有进行优化。

其三是完成调用后恢复原来的函数参量:与进栈一样,所有进栈的量都需要依次出栈完成恢复。顺序与进栈顺序相反。代码如下:

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)

其四是返回进入work的地址:利用 j 指令即可,要注意的是返回时并不是直接回到函数体末尾,而是回到进来work的地方。因为此时新函数已经执行完,原函数并没有执行完,应该回到进来的位置继续完成原函数的执行。换句话说,work部分并不是完全等于函数调用,而是包括函数调用前准备、整个函数调用过程、函数调用后恢复。也就说,回去后应该继续执行判断 if(edgev==1&&length==n)

注:不要写完递归就觉得万事大吉,不要忘记还有函数的退出处理。

4. output:

输出部分可以写两小块 end_1 and end_2:

一个处理flag = 1,另一个处理flag = 2。事实上,遇到flag = 1可以直接利用jump指令调到相应end,没有就顺序执行到相应end 然后结束程序。

三、优化:

以上过程完成后最后一个点时TLE,原因在于第一个循环是多余的。

首先我们不妨想如果存在哈密顿环,那无论从哪个点出发,应该都可以找到该该环,也就是说,在哈密顿环上的诸点,就哈密顿环来说,是轮换对称的。所以我们可以摒弃第一个大循环,只设定一个出发点(不妨为 0 ),然后进行寻找。

四、心得:

在编写汇编时,要求编写者对程序理解的更为明确、深刻,对过程及执行顺序有整体上的把握。

以下有几个个人总结的小tips:

  • 模块化:如果之前编写C语言没有模块化的习惯,在编写mips时一定要分好模块,所谓“分好”,并不仅仅意味着分出,同时还要求你所分的模块各部分功能明确,衔接简明,同时你还要明确的知道各个模块的变量及其之间的关系。

  • 模式化:在mips中条件分支、循环、递归等语句都有相应的模式。模式并不意味着让你完全调用,但你却需要借鉴或者熟悉。这样更有利于形成自己的风格并避免各种神奇的bug。

  • 模仿化:(这个名主要是为了凑前面的“模”)这个是个人的习惯,诸位可以根据自己的喜好来不同的执行。所谓模仿,其实就是自己在脑子中运行一遍程序,以明确程序的执行过程是怎么样的。我个人喜欢用纸笔写,因为纸笔更便于我形成整体布局以及修改。可以将各个寄存器所存放的变量对应关系写出来,便于查看。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Prjj_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值