查看: 1085|回复: 0
收起左侧

用一段C代码编译的指令代码,来阐明RISC-V架构的简洁

[复制链接]

  离线 

  • TA的每日心情
    拍拍
    2022-6-27 11:09
  • 签到天数: 25 天

    [LV.4]

    发表于 2020-9-24 10:05:50 | 显示全部楼层 |阅读模式

    有人预言,RISC-V或将是继Intel和Arm之后的第三大主流处理器体系。欢迎访问全球首家只专注于RISC-V单片机行业应用的中文网站

    您需要 登录 才可以下载或查看,没有帐号?立即注册

    x
    本帖最后由 sky 于 2020-9-24 10:05 编辑

    国内芯片技术交流-用一段C代码编译的指令代码,来阐明RISC-V架构的简洁risc-v单片机中文社区(1)

    前言
    笔者最初对计算机产生浓厚兴趣,是因为想要弄明白为什么计算机能够帮我们计算1+1=2的?就是这样朴素的心愿,让我进一步去探寻学习。
    这篇是我很早之前的一个回答,让我决心在工作之余写一些东西分享。

    对编程感兴趣的程序员是否都对电路、单片机也怀有浓厚的兴趣?
    现如今,我终于可以绕过ARM和X86黑色的面纱,从另一个开源的CPU架构RISC-V中去深入的认识计算机的工作原理。

    这篇文章想通过分析一段C代码,以及分析C代码最终产生的机器码指令,来分享一点对RSIC-V结构优劣势的思考。

    先从一段最简单不过的“插入排序算法”开始
    1. void insertion_sort(long a[] , size_t n){
    2. for(size_t i = 1,j;i < n; i++){
    3. long x = a[j];
    4. for(j=i; j>0 && a[j-1] > x;j--){
    5. a[j] = a[j-1];
    6.         }
    7. a[j] = x;
    8.     }
    9. }
    复制代码

    上面的一段代码是C语言实现的经典“插入排序”算法,主要用到了一些基本的原子操作,例如,“比较大小”,“交换内存位置”,等等。换句话说,只要一个智能设备,人或者是计算机,只要它具备这些原子操作的能力,然后只要我将做事的顺序和方法(算法)告诉他,他就可以帮我们完成插入排序这样的工作。

    不同的执行机构对原子操作的实现不同,具体的体现在指令集架构的不同,硬件设计的不同,这篇文章会分析两种种架构实现原子操作的指令方法,分别是STM32等流行嵌入式架构(ARM-32),以及开源轻量的后起之秀RISC-V架构,分析他们在同一段C代码下的执行过程和效率比较。
    我们把编译器的优化等级设置到最高,首先对比不同架构的极限代码大小。

    最终的编译结果如下图所示

    国内芯片技术交流-用一段C代码编译的指令代码,来阐明RISC-V架构的简洁risc-v单片机中文社区(2)


    可以看出,无论在代码大小还是在指令数量上RSIC-V的行那都不逊色于X86和ARM-32。



    首先简单看一下ARM-32架构编译出的指令



    国内芯片技术交流-用一段C代码编译的指令代码,来阐明RISC-V架构的简洁risc-v单片机中文社区(3)


    上述的指令代码,要先告知读者含义,最左边的0 ,4,8,c之类的数据代表十六进制的行号,可以看出代码指令是4字节也就是32BITS对齐的,这也是取名叫ARM32而不是ARM16的原因之一。紧跟着的是机器码,所谓的程序部分也就是这个,这一个32BITS的机器码,包含了这次指令所有的信息。再跟着的是汇编伪代码,为了进一步能让人类去理解机器码的含义。最后面的是注释。

    举例来说:

    e3a03001 mov r3,#1

    代表着将1这个数据移动到了r3这个寄存器中,等效于C语言的i=1,mov r3 #1这个操作的所有信息都包含在了e3a03001这一个长度为32bits的数里面,方便代码的储存。至于如何把汇编转变成机器码,那就是编译器去做的事情了。

    重点看RISC-V的编译结果,因为RISC-V的文档很简明方便学习


    国内芯片技术交流-用一段C代码编译的指令代码,来阐明RISC-V架构的简洁risc-v单片机中文社区(4)


    对比一下,能看出和ARM-32的结果相比,汇编的伪代码有所不同,而且由于框架不同,编译出来使用的指令也不同。
    我们假设编译器做如下分配,通用寄存器A1存放C代码中n的内容,A3存放数组a[0]的地址,A4存放i的内容,A5存放j的内容,a6存放中间变量a6的内容。

    我们逐一分析指令代码
    Addi a3,a0,4

    国内芯片技术交流-用一段C代码编译的指令代码,来阐明RISC-V架构的简洁risc-v单片机中文社区(5)


    假定数组a[]的存放地址是0x00,我们把其第一个元素a[1]的地址0x04传进去,因为很显然插入操作是从a[1]开始做的。把0x04加载到a0寄存器中,结果写入a3,相当于认为a3存放着C语言中a[1]的地址。这里做个假设a的首地址是0x00,很显然数据a[]的首地址是通过传参传进来的。
    Addi a4,x0,1

    将1加到x0寄存器中,最终结果写入a4寄存器。

    bltu a4,a1,10

    国内芯片技术交流-用一段C代码编译的指令代码,来阐明RISC-V架构的简洁risc-v单片机中文社区(6)


    如果 a4和a1相比,a4<a1,那么程序PC值增加10。从前面的代码看出a4存放i,a1存放的是n,也就是数组的最大长度,所以这一句在比较i和n的大小,如果i<n,程序PC+=10,否则继续运行。

    jalr x0,x1,0

    国内芯片技术交流-用一段C代码编译的指令代码,来阐明RISC-V架构的简洁risc-v单片机中文社区(7)


    相当于C语言里的return语句,函数结束。显然,如果bltu a4,a1,10语句没有跳转,也就是i<n的条件不满足,这一句会被执行。如果i<n条件满足,会进到0x10的指令位置

    lw a6,0(a3)

    国内芯片技术交流-用一段C代码编译的指令代码,来阐明RISC-V架构的简洁risc-v单片机中文社区(8)


    前面认为a3寄存器存放着输入数组a[]的首地址,所以lw a6,0(a3)意味着把a3目前指向的地址进行一次取值,相当于a6=*a。前面也假设了编译器用a6存放x的数据,所以也就是x=a的含义。

    Addi a2,a3,0

    假设a2存放a[j]的数据,显然a[j]要首先指向a[0]的位置,所以先把0加到a3(也就是a的地址)然后把结果方在a2的位置。这样a2的地址和a3的地址实际隔了0个地址。这个0在后面肯定会是其他数字,来完成数组遍历。

    Addi a5,a4,0

    a5存放j的数值,前面假定了a4存放i的数值,所以这句话的含义就是j=i

    程序运行到这,整理一下,目前a2=a3都存放着a[1]的地址,a5=4存放着i和j的取值也就是1。

    接着往下进行

    lw a7,-4(a2)

    又出现一个新的寄存器a7,从a2的地址拿数据,这里-4代表拿的不是a2这个地址的数据,而是往前4个Byte,很显然,C代码显然的说明了a[]数组是long型的,所以一个元素是4个字节,a2指向的是a[1],往前4个Byte,也就是说取到了a[0]的数据。这一句与C语言的a[j-1]相互对应。A7拿到了a[0]的值,暂且记录a7=a[0];

    国内芯片技术交流-用一段C代码编译的指令代码,来阐明RISC-V架构的简洁risc-v单片机中文社区(9)
    bge a6,a7,34

    如果a[j-1] <= a[j],跳出内部循环,去到0x34地址否则继续往下运行。

    国内芯片技术交流-用一段C代码编译的指令代码,来阐明RISC-V架构的简洁risc-v单片机中文社区(10)
    sw a7,0(a2)

    前面谈到,a[]数组的元素类型是long,所以长度是4个byte,也就是把a7的四个字节写到a2里面去了,翻译一下就是a[j]=a[j-1] ,此次循环的含义就是a[1]=a[0]。

    Addi a5,a5,-1

    a5存放j值,等价于j--

    Addi a2,a2,-4

    a2存放a[]数组的地址,a2地址跟着j减小一起减小4字节

    bne a5,x0,1c

    国内芯片技术交流-用一段C代码编译的指令代码,来阐明RISC-V架构的简洁risc-v单片机中文社区(11)

    可以看到x0是i的取值,所以这句话相当于,if(j!=i)跳转到0x1c。否则继续执行到0x34。

    Slli a5,a5,0x2

    国内芯片技术交流-用一段C代码编译的指令代码,来阐明RISC-V架构的简洁risc-v单片机中文社区(12)

    把j的值左移2位,也就是相当于把j乘上了4,j=j*4

    Add 15,a0,a5

    原来j是一个计数值,当j乘上4以后,4*j就是地址的偏移值了,所以认为此时a0存放的是&a[j]。

    Sw a6,0(a5) // a[j] = x

    Addi a4,a4,1 // i++

    Addi a3,a3,4 //随着i++,&a地址也跟着自增

    Jal x0,8 //回到地址0x08,继续循环判断


    对比一下
    同样完成插入排序的代码

    ARM-32用了如下11个操作指令

    mov cmp bxcs push ldr subs str bne add bcc pop

    而RISC-V仅用了9个操作指令

    addi bltu jalr lw bge sw bne slli jal

    显然,指令数量方面RISC-V比ARM-32要优秀的多。指令少,硬件设计的就少,重用率高。

    像我们这样逐句分析代码的人就会少受一点查阅资料的痛苦。

    官方的RISC-V文档给出了更多的比较,见下图

    国内芯片技术交流-用一段C代码编译的指令代码,来阐明RISC-V架构的简洁risc-v单片机中文社区(13)
    后面会在深入研究RSIC-V的过程中慢慢发现验证。

    最后
    这篇文章主要是分析了一段C代码在RISC-V框架下的编译后的指令流程。窥探了一小部分编译器开发者的智慧精华,因为如果你能仔细的看完最终的指令代码是如何完成C语言想要完成的功能的,你就会赞叹了,并且对C语言会有一个更加清晰的认识。

    比如说,熟练的C开发者都喜欢这样写加法操作,a+=10。对比汇编来看,这正是迎合了Addi a4,a4,1这样的编码风格,实际仅用了A4一个寄存器而已。还有就是jar指令,对应好像是C语言中的goto关键词。Lw指令类似C语言的*符,把指针指向的值取出来,如果能从最底层理解lw指令,那么对C指针一定就不害怕了。

    如果能熟悉最底层的实现原理,那么今后想将一个数乘上4,就不会这样写了。

    j *= 4;

    而是像一个底层开发者一样睿智的写出

    j <<= 2; // Slli a5,a5,0x2



    本人目前在RISC-V的独角兽半导体公司工作,我想让更多的人认识到RISC-V,感受到RISC-V的魅力所在!




    上一篇:图灵奖得主华人高徒发布首款终端AI芯片!64位RISC-V
    下一篇:集成电路史上著名的十个人,有几个中国人?
    RISCV作者优文
    全球首家只专注于RISC-V单片机行业应用的中文网站
    回复

    使用道具 举报

    高级模式
    B Color Image Link Quote Code Smilies

    本版积分规则

    关闭

    RISC-V单片机中文网上一条 /2 下一条



    版权及免责声明|RISC-V单片机中文网 |网站地图

    GMT+8, 2024-4-26 00:50 , Processed in 1.180034 second(s), 48 queries .

    快速回复 返回顶部 返回列表