【跟我学RISC-V】(二)RISC-V的基础知识学习与汇编练习

写在前面:

这篇文章是跟我学RISC-V的第二期,是第一期的延续,第一期主要是带大家了解一下什么是RISC-V,是比较大体、宽泛的概念。这一期主要是讲一些基础知识,然后进行RISC-V汇编语言与c语言的编程。在第一期里我们搭建了好几个环境,你可以任意选一个你喜欢的RISC-V环境(能够执行RV机器码的平台),然后进行代码编写、编译、汇编、链接、运行、观察现象的这一过程。同样地,在这一篇里我也会拿x86的知识与RISC-V进行对比,这样也可以促进对两种指令集的学习。

一、RISC-V指令集的基础信息

1、RISC-V的通用寄存器

在第一期里我讲过,无论是RV32还是RV64,它的通用寄存器的数量都是32个。32真是一个好数字,刚好是2的5次方,实际上伯克利大学的研究员在设计RV的时候就非常讲究,这么做的好处是颇多的,也体现了RISC-V指令集的特色,这个我们在学习之后再讨论这个问题。

这32个寄存器分别是x0 x1 x2 ... x31这样去编号,但是就单纯的这样去写汇编的话,是非常不方便的,因此每一个寄存器又有自己的别名,这个别名就代表了这个寄存器的含义,以及函数调用时候的规则。也就是说,你在实际汇编编程的时候,既可以使用编号名,也可以使用别名,实际上使用别名更好,这样能够把寄存器的含义和在这里的作用绑定起来,别人看你的代码就知道你要做什么了。

寄存器名别名作用在函数调用过程中的维护
x0zero零寄存器,永远是0不需要维护
x1rareturn address在函数调用时存放返回地址caller
x2spstack pointer栈指针寄存器callee
x3gpglobal pointer全局寄存器(用于联接器松弛优化)经常使用基于gp的寻址模式来访问全局变量和静态数据,从而提高访问速度和效率caller/不需要保存
x4tpthread pointer线程寄存器(保存pcb的地址)与线程相关
x5t0temporaries临时寄存器,相当于c语言的临时用一下变量,callee可能会改变他们的值,caller根据实际情况看是否要保存caller
x6t1
x7t2
x8s0saved保存寄存器,在函数调用过程中必须保存的寄存器callee
x9s1
x10a0

argumeng参数寄存器,在函数调用过程中传递参数和返回值。同时,a0和a1又会在函数返回时的传递返回值。

caller
x11a1
x12a2
x13a3
x14a4
x15a5
x16a6
x17s7
x18

s2

saved保存寄存器,在函数调用过程中必须保存的寄存器callee
x19s3
x20s4
x21s5
x22s6
x23s7
x24s8
x25s9
x26s10
x27s11
x28t3临时寄存器,总共有7个临时寄存器caller
x29t4
x30t5
x31t6

上图描述了32个通用寄存器在编程中作用的约定,特别是c编程时候的默认调用约定。其实这些寄存器本身来说想咋用,但是如果这样的话,你写一套使用寄存器的风格,他也有一套自己的风格,这样的话我写的函数你就没法调用了,因为寄存器安排不同,这样就非常麻烦,根本不利于开发。于是RISC-V指令集在设计之处,就把这些寄存器的作用和安排都规定好了,别名也取好了。你不需要自己想一套函数调用的法则,你只需要遵守约定就好。这样,我写的函数,你也可以直接调用,而不需要考虑参数保存在哪个寄存器,因为这都已经规定好了。

比如说A函数调用的B函数,那么caller就是A函数,callee就是B函数。

这个寄存器的别名是很有用的,你不需要去记忆x寄存器名到别名的映射,你只需要记住别名中前缀的含义,你在汇编语言编程的时候就知道该使用哪一个寄存器来保存什么信息了。

如果你是第一次看见这个表格,你可能会感觉很抽象,不过只要编程练习一下,那么也就不抽象了。不过想进行RISC-V汇编语言的编程,光是知道通用寄存器还是不够的,你还得知道一些指令,所以我在这里先分析一下RV的寄存器和x86的不同之处。

我们都知道,x86是CISC,而RISC-V在RISC,这二者在寄存器的安排上就有非常大的不同。在x86架构中,有一种说法是“寄存器较弱的体系结构”,意思就是x86架构的通用寄存器的数量是非常少的。在实模式下,也就ax,bx,cx,dx,bp,sp,si,di, 就是搞来搞去就这么几个寄存器,并且比如bx,bp还要拿来作为offset偏移量寻址、cx还要拿来作为循环次数的保存、sp还是指向栈顶。总结来说就是能够程序员使用的通用寄存器的数量是在是太少太少了,我在大一的时候学习8086汇编就比较难受,寄存器满打满算就这么几个,一下子就用掉了,总感觉不太够用(你可以看看我之前的blog)。进入IA-32e的长模式感觉就好多了,通用寄存器又加上了r8 ~ r15 ,Intel终于是不挤牙膏了。而对于RISC-V而言,有整整32个通用寄存器,其中临时寄存器的数量就有7个,相比x86真是太爽了,随便拿一个就能临时保存一下我计算过程中的数据(你可以认为是打草稿)。对程序员来说,这太方便、舒服了。

还有一些区别是,在x86中(保护模式),函数调用时候参数非常依赖于内存。也就是参数都是保存在栈里的,保存在栈里问题倒是不大,就是读写内存的速度相比读写寄存器的数据差距太大了。在RISC-V有中专门的a系列的寄存器可以用户保存函数调用时候的参数,a0 ~ a7 整整8个寄存器呢!基本上来说,你一个函数的参数也很少会超过8个,当然如果超过了那还是要保存在栈里。总的来说,参数保存在寄存器里那速度是快了好几倍。(当然在Intel IA-32e中也是使用寄存器保存参数了,Intel在多年的迭代过程中算是学聪明了,而RISC-V是一开始就这么聪明,这就是后发的优势)

还有一点就是在RISC-V体系结构中,专门可以拿出一个寄存器tp, 来存放指向当前进程task_struct的指针,用于加快访问速度。而像x86这样的体系结构(通用寄存器数量不多)就只能在内存栈顶创建thread_info结构,通过计算偏移量间接的查找task_struct(也就是pcb)。也就是x86体系中每一次调用current去查找当前进程的pcb都需要访问多次内存,还要通过偏移量去找到task_struct的地址,这转来转去速度就会变慢。而RISC-V则直接通过tp寄存器直接就能找到pcb ,那访问寄存器的速度快很多,并且也不需要通过偏移量去寻址。这又是一大优势。

还有就是在x86中通用寄存器是可拆解的,比如IA-32e的rax寄存器是64位的,你可以拆解它。rax、eax、ax、ah、al, 从8位到16位到32位到64位,是可以拆分的。这都是Intel为了兼容性而设计的这么一套东西,因为早期的8088是8位的CPU,8086是16位的,老奔腾是32位的,酷睿又是64位的,它为了兼容就用这种方法,你即便进入了长模式,仍然可以使用al寄存器。但是在RISC-V中,RV32寄存器的大小就是32位的,你不可能说拆成hx和hl,没有这样的用法。所以说RISC-V指令集里面,你在使用load系列指令的时候,把一个不到32位的数值放置到32位的寄存器,会进行符号扩展或者零扩展,而不是直接把这个数直接放到寄存器里,这样会损失符号的。

在上面表格中有一个非常特殊的寄存器x0 zero寄存器,它类似于LInux里的/dev/null这个设备,你往里面写入任何数据都没用,再怎么写都是0,写入任何数都会被丢弃掉;如果你把这个寄存器的值给读出来,也还是0。你可能觉得这个寄存器好像没啥用啊,难道我就不能用立即数0去替代这个x0寄存器吗?实际上,这个x0 寄存器是非常有用的,有很多地方都会用到它。特别是伪指令在转换成汇编指令的时候,会经常用到zero寄存器,这个我会在后面讲到。


2、RISC-V的指令格式与特点

(一)RISC-V指令的特点

RISC-V的每条指令宽度都是32位(固定的4B),如果有c扩展使用指令压缩后会变成2B ,这个我们先不提。RV的指令格式如图所示分为6种。

  1. R-type:寄存器与寄存器算术指令,这里的R就是register寄存器的意思;
  2. I-type:寄存器与立即数算术指令或者加载指令;
  3. S-type:存储指令(和上面的加载指令刚好是反义词);
  4. B-type:条件跳转指令;
  5. U-type:长立即数操作指令;
  6. J-type:无条件跳转指令。

大家从图里可以清晰地看到:无论是什么类型的指令,确实都是4B的,并且共同点就是opcode操作码都在低7位。操作码这个概念相信学过计算机组成原理的都知道。

  • 足够的编码空间:使用7位操作码可以提供128种不同的可能值,这允许定义多种不同的基本操作和指令格式。对于一个旨在可扩展和支持多种扩展模块(如整数、浮点、原子操作等)的现代处理器架构来说,这一点非常重要。

  • 简化解码:RISC-V的指令长度固定为32位,这使得硬件能够更加简单和高效地解码指令。opcode位于指令的最低7位,硬件可以快速地读取这7位并确定如何进一步解析整个指令,这对提高指令解码速度和处理器整体性能至关重要。

  • 支持指令格式多样性:RISC-V使用不同的指令格式(如R、I、S、B、U、J格式)来支持不同类型的操作。这些格式有不同的字段组合和长度,opcode的7位设计帮助区分这些格式,并指导如何解析随后的字段。

  • 扩展性:RISC-V架构被设计为可扩展的,以支持新的功能和指令集扩展。7位opcode为未来可能的指令集扩展留出了空间,使得可以轻松加入新的操作码而不会干扰现有的指令解码逻辑。

在图中的rd就是目的寄存器,rs就是源寄存器。这个概念类似于x86中的rdi和rsi。大家可以总结出来,rd要么是1个要么是0个(有的指令是不用把值输出到目的寄存器的),rs最多支持两个,就是最多放两个源寄存器进来。无论是rd还是rs,它所占用的位数都是5位。这个事情我们之前提到过,因为寄存器总共就32个,2的5次方等于32,那么设置成5位这样,是非常巧妙的。

图片中还经常出现imme,这个就是立即数的意思,immediately.

还有就是占用3位的funct3与占用7位的funct7,就是说单纯的opcode还不足以确定这条指令究竟是哪一条指令。而是要opcode和funct功能码,这二者一起才能共同决定这条指令对应的具体的汇编指令手动反汇编的时候要用到。

实际上对照这张表格,你就很容易做到反汇编了。拿到一个4B的16进制数,你先把他转换成32位二进制数,然后对照opcode先确定是什么类型,确定好之后再根据具体的funct(如果存在)就能确定是哪一条指令了。确定指令之后,再通过rs,rd推出对应的寄存器号,有立即数的话把立即数也带进去。这样,一整条汇编指令就出现了。


(二)RISC-V每条指令详解

接下来,我要对每一条指令进行说明,大家耐心看一看吧。为了让现象更加明显,我使用c语言内联汇编的方法,把指令执行后的现象给展示出来,方便大家查看,那么大家如果能够跟着实践一遍这样更好。这里我还没有讲到c语言内联汇编的东西,不过有编程基础的应该能够看懂asm语句,我会在c代码后面讲述这么做的目的。

①加载指令

加载指令load就是把数据从内存加载到寄存器的这一过程。

指令格式数据位宽说明
lb rd,offset(rs)8把rs寄存器里的值指向的地址作为基地址,在偏移offset的地址处,加载1B的数据经过符号扩展之后放入到rd寄存器里面
lbu rd,offset(rs)8作为无符号加载,经过零扩展放入到寄存器rd
lh rd,offset(rs)16符号扩展加载2B
lhu rd,offset(rs)16零扩展加载2B
lw rd,offset(rs)32符号扩展加载4B
lwu rd,offset(rs)32零扩展加载4B
ld rd,offset(rs)64直接加载到rd寄存器里,不用扩展了
lui rd,imme64把立即数imme左移12位,然后符号扩展,再把结果写入到rd寄存器(这里的u是upper的意思,不是unsigned的意思)

RISC-V的指令都挺有规律的,l就是load加载的意思,代表数据从内存加载到寄存器;b是byte的意思,表示1个字节;h是halfword的意思,表示半字,2个字节;w表示word,一个字,4个字节;d表示double word表示双字,就是8个字节。跟在b/h/w/d后面的u是unsigned的意思,表示这是无符号数,不存在符号扩展;直接跟在l后面的是u ,表示这是upper,需要左移。记忆是比较容易的。

我们先进行一些区分:

#include <stdio.h>

int main(void)
{
    long rd = 0;

    char rs[3];
    rs[0] = 'a';
    rs[1] = 'b';
    rs[2] = 'c';

    asm volatile(
        "lb %0,1(%1)    \n\t"
        :"=r"(rd)
        :"r"(rs)
    );
    printf("%c\n",rd);
        return 0;
}

这是一段非常简单的c语言内联汇编的代码,意思就是把rs作为地址传入到寄存器里,再通过lb指令把rs指向的地址作为基地址,偏移了1B的地址里面取出来1B,把这个数据经过符号扩展放入寄存器里,在输出到rd变量。我们打印rd变量,确实是字符b.由于字符b是一个正数,因此符号扩展之后值就是本身。


lb.c

#include <stdio.h>


int main(void)
{
    long rd = 0;
    char rs = -20;

    asm volatile(
        "lb %0,0(%1)    \n\t"
        :"=r"(rd)
        :"r"(&rs)
    );
    printf("%d\n",rd);
        return 0;
}

lbu.c

#include <stdio.h>

int main(void)
{
    long rd = 0;
    char rs = -20;

    asm volatile(
        "lbu %0,0(%1)    \n\t"
        :"=r"(rd)
        :"r"(&rs)
    );
    printf("%d\n",rd);
        return 0;
}

可以看见,即便rs变量的值是-20,如果你使用的是lbu指令,那么就会进行零扩展,符号位就无效了。

从以上这个例子我们不难看出:符号扩展是计算机系统中把小字节转换成大字节的规则之一,它会将符号扩展到所需要的位数。

比如一个1字节的数0x8A,它的最高位也就是第7位是1,那么就需要进行符号扩展,高字节使用1来填充。如果扩展到64位,那么它的值就是0xffff ffff ffff ff8a

而零扩展的就是当成无符号来处理,既然是无符号数,高字节部分使用0来填充。

还有一点要注意的是,符号扩展是小字节往大字节扩展的时候进行的,而ld这一条指令,它本身就是从内存加载一个64位的数到寄存器,没有从小字节到大字节的过程,因此是不需要符号扩展的。


我再测试一下lui指令:

lui.c

#include <stdio.h>

int main(void)
{
    long rd = 0;

    asm volatile(
        "lui %0,0xff    \n\t"
        :"+r"(rd)
    );

    printf("rd = %lx\n",rd);
    return 0;
}

确实是左移了12位,1个16进制的0代表2进制的4位。你也许会很困惑,为什么要左移12位?为什么不是左移13位?为什么不是干脆不左移?

  1. 寻址能力的扩展

lui 指令将 20 位立即数置于寄存器的高 20 位。这样做的目的是允许程序能够引用位于较高地址范围内的内存地址或数据。考虑到 RISC-V 的寄存器是 32 位的,这种设计使得使用 lui 加上一个后续的加法或其他指令(比如 addi),可以访问整个 32 位地址空间。

  1. 高效的常数加载

通过将立即数左移 12 位,lui 指令可以快速地设置寄存器中的高位,这对设置大的常数值非常有效。如果需要加载的立即数不仅仅是高位,可以通过随后的 addi(Add Immediate)等指令来设置剩余的低 12 位。

  1. 指令编码的简化

在 RISC-V 的指令格式中,立即数字段(imm字段)经常被复用以适应不同类型的指令。lui 指令的设计使得指令的立即数字段直接对应于寄存器的高 20 位,从而简化了指令的解码和执行过程。

  1. 支持编译器优化

这种左移 12 位的设计也有助于编译器生成更优化的代码,尤其是在进行全局地址或大范围数据定位的时候。编译器可以更容易地生成用于初始化大数组或访问静态变量的代码。

总之就是,在RISC-V中一条指令总共就4B,能够分配给立即数imme的部分是很有限的,为了能够寻址到“高地址的地方”,于是很多指令都是具有upper的性质,即把其中的立即数左移12位,然后低于12位的部分你可以使用add系列的指令加上来,这样你的寻址能力大大提升,不用再受限于4B指令有限的imme位数能够表示的最大值了。此时你可能会觉得这也太麻烦了,我寻址一下难道还要把一个完成的地址给拆分成高位和低12位,这样组合成地址吗?实际上,你可以手动这样去组合、去拼凑,因为精简指令集本身就是多条指令的组合才能完成一个功能的,而不像x86那样,一条MOV指令打天下。当然,RISC-V的设计者为了程序员方便,它提供了大量的“伪指令”,你使用伪指令之后,伪指令会再拆分成真正的RISC-V汇编指令。有了这些伪指令,编程是不会太麻烦了。

在这个例子你,你可以使用一条伪指令叫做li,这个li就可以把一个立即数放进寄存器里。

li.c

#include <stdio.h>

int main(void)
{
    long rd = 0;

    asm volatile(
        "li %0,0xff    \n\t"
        :"+r"(rd)
    );

    printf("rd = %lx\n",rd);
    return 0;
}

不过你的记得,这是一条伪指令,它不是真正的RISC-V汇编指令,它是多条指令的组合。


②存储指令

存储指令就是加载指令的反义词 --把数据从寄存器移动到内存里。只是它更加简单了,没有符号扩展,直接移动数据即可。

指令位宽说明
sb rs2,offset(rs1)8把rs2寄存器的低8位的值存储到以rs1寄存器的值为基地址,offset为偏移量的地址处。
sh rs2,offset(rs1)16低16位
sw rs2,offset(rs1)32低32位
sd rs2,offset(rs1)64整个rs2寄存器的值

这个存储指令就是store,把寄存器的值往内存里存,对应的指令类型是S-type.

大家其实也发现了,这个store指令系列对于load来说,简单太多了,没有什么又是u啊又是i的,就是非常单纯的把寄存器值的一部分或者整个寄存器的值,放置到指定的内存地址里面去。这里不需要什么符号扩展、零扩展的。

#include <stdio.h>

int main(void)
{
    char rs[3] = {0};

    asm volatile(
        "li t0,'b'  \n\t"
        "sb t0,1(%0)    \n\t"
        :
        :"r"(rs)
        :"t0","memory"
    );
    printf("rs[1] = %c\n",rs[1]);
    return 0;
}

注意这里我们在扩展内联汇编里直接使用到了寄存器t0,因此在损坏部分要把它写进去,这样在asm嵌入的代码块执行结束的时候会把t0原先的值给恢复回去。


③算术指令

算术指令相对来说是比较重要、用到的场景也是比较多的。

指令指令格式说明
addadd rd,rs1,rs2把rs1寄存器的值和rs2寄存器的值相加,并把加法的结果放到rd寄存器里
addiadd rd,rs,imme把rs寄存器的值和立即数imme相加,把结果放到rd寄存器里
addwaddw rd,rs1,rs2截取rs1和rs2寄存器的低32位,相加后把结果进行符号扩展并放到rd寄存器里
addiwaddiw rd,rs,imme截取rs寄存器的低23位并与imme立即数相加,把结果进行符号扩展并放到rd寄存器里
subsub rd,rs1,rs2把rs1寄存器里的值减去rs2寄存器里的值,把结果放到rd寄存器里
subwsubw rd,rs1,rs2把rs1寄存器的低32位减去rs2寄存器的低32位,把结果放到rd寄存器里

这个看起来比较简单,实践起来也不复杂。

add.c

#include <stdio.h>

int main(void)
{
    long rs1 = 20;
    long rs2 = 30;
    long rd = 0;

    asm volatile(
        "add %0,%1,%2   \n\t"
        :"=r"(rd)
        :"r"(rs1),"r"(rs2)
    );

    printf("rd = %d\n",rd);
    return 0;
}

sub.c

#include <stdio.h>

int main(void)
{
    long rs1 = 20;
    long rs2 = 30;
    long rd = 0;

    asm volatile(
        "sub %0,%1,%2   \n\t"
        :"=r"(rd)
        :"r"(rs1),"r"(rs2)
    );

    printf("rd = %d\n",rd);
    return 0;
}

怎么样,这样的汇编风格写起来,相比x86来说是不是简单太多了。


到目前为止,我们已经学习了load加载指令和store存储指令,这些都是真汇编指令,但有的时候,这些指令用起来会不太方便,毕竟不像x86那样一个MOV就能够达到目的。因此,对伪指令的学习也是非常重要的。

程序计数器(Program Counter,PC)是用来指示下一条指令的地址。为了保证CPU能够正确地执行程序的指令代码。就会使用一套PC寄存器来存储这个地址,那么硬件上就只需要把PC指针指向的地址里面的数据当作是代码,然后由指令领取单元IFU把指令送入预译码器并进行预译码。在这里面我们可以看到这个PC寄存器的重要作用,不同指令集给出的PC实现方式也不太一样。比如在x86架构中是使用CS:IP这一对寄存器来指定代码段的位置。而在RISC-V中简化了这一过程,它单纯使用PC寄存器来指定下一条指令的地址。这个PC寄存器,我们不能去读它的位置,但是可以用别的指令去相对PC寄存器进行寻址。

auipc rd,imme

auipc指令就是这么一条,通过PC寄存器进行相对寻址的指令。它的英文名是Add upper immediate to PC.

其中有upper,也就是说这里面的imme立即数也是要左移12位的,这里和上面是一样的。因此它只能寻址到与4KB对齐的地址,如果一个地址是在4KB内存块的内部,则auipc寻址不到它。不过我们也有相应的伪指令可以很方便地去寻址。这个auipc指令,我们用到的其实不太多,程序员用到的更多的是基于它的伪指令,当然这些基于它的伪指令展开还是auipc.

伪指令指令组合说明


二、RISC-V指令集的编程理论


三、RISC-V指令集的编程实践

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/592557.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

WPF之绑定验证(错误模板使用)

1&#xff0c;前言&#xff1a; 默认情况下&#xff0c;WPF XAML 中使用的绑定并未开启绑定验证&#xff0c;这样导致用户在UI上对绑定的属性进行赋值时即使因不符合规范内部已抛出异常&#xff08;此情况仅限WPF中的数据绑定操作&#xff09;&#xff0c;也被程序默认忽略&…

4.【Orangepi Zero2】Linux定时器(signal、setitimer),软件PWM驱动舵机(SG90)

Linux定时器&#xff08;signal、setitimer&#xff09;&#xff0c;软件PWM驱动舵机&#xff08;SG90&#xff09; signalsetitimer示例 软件PWM驱动舵机&#xff08;SG90&#xff09; signal 详情请看Linux 3.进程间通信&#xff08;shmget shmat shmdt shmctl 共享内存、si…

【计算机网络】循环冗余校验:Cyclic Redundancy Check

1. 任务目标 利用循环冗余校验&#xff08;CRC&#xff09;检测错误。 循环冗余校验&#xff08;英语&#xff1a;Cyclic redundancy check&#xff0c;通称 CRC&#xff09;是一种根据网上数据包或计算机文件等数据产生简短固定位数校验码的一种散列函数&#xff0c;主要用来…

智慧文旅展现文化新风貌,科技助力旅行品质升级:借助智慧技术,文旅产业焕发新生机,为旅行者带来更高品质的文化体验之旅

一、引言 在数字化、智能化的浪潮下&#xff0c;文旅产业正迎来前所未有的发展机遇。智慧文旅作为文旅产业与信息技术深度融合的产物&#xff0c;不仅为旅行者带来了全新的文化体验&#xff0c;也为文旅产业注入了新的活力。本文旨在探讨智慧文旅如何借助智慧技术展现文化新风…

C++:二叉搜索树的底层模拟实现

概念&#xff1a; 二叉搜索树又称二叉排序树&#xff0c;它或者是一棵空树&#xff0c;或者是具有以下性质的二叉树&#xff1a; 搜索二叉树的操作&#xff1a; int a[] {8, 3, 1, 10, 6, 4, 7, 14, 13};二叉搜索树需要满足左子树比根小&#xff0c;右子树比根大&#xff0c;…

Leetcode—1235. 规划兼职工作【困难】(upper_bound、自定义排序规则)

2024每日刷题&#xff08;125&#xff09; Leetcode—1235. 规划兼职工作 算法思想 实现代码 class Solution { public:int jobScheduling(vector<int>& startTime, vector<int>& endTime, vector<int>& profit) {int n startTime.size();vec…

循环神经网络完整实现(Pytorch 13)

一 循环神经网络的从零开始实现 从头开始基于循环神经网络实现字符级语言模型。 %matplotlib inline import math import torch from torch import nn from torch.nn import functional as F from d2l import torch as d2lbatch_size, num_steps 32, 35 train_iter, vocab …

(六)SQL系列练习题(下)#CDA学习打卡

目录 三. 查询信息 16&#xff09;检索"1"课程分数小于60&#xff0c;按分数降序排列的学生信息​ 17&#xff09;*按平均成绩从高到低显示所有学生的所有课程的成绩以及平均成绩 18&#xff09;*查询各科成绩最高分、最低分和平均分 19&#xff09;*按各科成绩…

PHP 反序列化

一、PHP 序列化 1、对象的序列化 <?php class people{public $nameGaming;private $NationLiyue;protected $Birthday12/22;public function say(){echo "老板你好呀&#xff0c;我是和记厅的镖师&#xff0c;叫我嘉明就行&#xff0c;要运货吗你&#xff1f;"…

手机恢复出厂设置ip地址会变吗

当我们对手机进行恢复出厂设置时&#xff0c;很多人会担心手机的IP地址是否会发生变化。IP地址对于手机的网络连接至关重要&#xff0c;它决定了手机在网络中的身份和位置。那么&#xff0c;手机恢复出厂设置后&#xff0c;IP地址到底会不会发生变化呢&#xff1f;虎观代理小二…

Jenkins docker部署springboot项目

1、创建jenkins容器 1&#xff0c;首先&#xff0c;我们需要创建一个 Jenkins 数据卷&#xff0c;用于存储 Jenkins 的配置信息。可以通过以下命令创建一个数据卷&#xff1a; docker volume create jenkins_data启动 Jenkins 容器并挂载数据卷&#xff1a; docker run -dit…

Python量化择时的技术指标函数

Python量化择时的技术指标函数 技术指标通过对原始数据&#xff08;开盘价、收盘价、最低价、最高价、成交量、成交金额、成交笔数&#xff09;的处理&#xff0c;来反映出市场的某一方面深层的内涵&#xff0c;这些内涵是很难通过原始数据直接看出来的。技术指标能客观地反映…

EXCEL怎样把筛选后含有公式的数据,复制粘贴到同一行的其它列?

自excel2003版之后&#xff0c;常规情况下&#xff0c;复制筛选后的数据&#xff0c;会忽略隐藏行&#xff0c;仅复制其筛选后的数据&#xff0c;粘贴则是粘贴到连续单元格区域&#xff0c;不管行是在显示状态还是隐藏状态。 一、初始数据&#xff1a; 二、题主的复制粘贴问题…

Codigger数据篇(下):数据安全的全方位保障

在数字化浪潮中&#xff0c;数据已成为现代企业的核心财富。Codigger作为领先的数据服务平台&#xff0c;深知数据安全对于用户的重要性&#xff0c;因此在深挖数据价值的同时&#xff0c;我们始终坚守数据安全防线。 一、双重加密技术保障 Codigger平台运用先进的加密通信和…

C语言学习【最基本】

C语言学习 简单的 C 程序示例 #include "stdio.h" /* 提供键盘输入与屏幕输出支持 */ /* 相当于把stdio.h文件中的所有内容都输入到该行所在位置 拷贝-粘贴 *//* void 表示不带任何参数 */ int main(void) /* 函数名 */ { …

UE—动画

1.动画蓝图 创建动画蓝图 在蓝图中添加状态机 状态机中状态的转换 转换条件设定 播放的动画 使用动画资源 使用混合空间 2.混合空间 混合空间1D 阿赵UE学习笔记——26、动画混合空间_ue 一维动画混合空间-CSDN博客 蓝图创建 混合空间内 按Ctrl到动画节点上即可预览 修改…

1. 傅里叶变换原理

1. 频率域的引入 1.1 时域角度 1.2. 频域角度 不同的角度表达的是同一件事情&#xff0c;从时间域和空间域来进行表达同一间事情 。时间域是都动态的&#xff0c;频率域是静止的 1.3. 时域角度和频域角度 1.4 相位 2 函数的时域角度 2.1 时间域 2.2 频率域 2.3 例子 2.3…

Spring扩展点(一)Bean生命周期扩展点

Bean生命周期扩展点 影响多个Bean的实例化InstantiationAwareBeanPostProcessorBeanPostProcessor 影响单个Bean的实例化纯粹的生命周期回调函数InitializingBean&#xff08;BeanPostProcessor 的before和after之间调用&#xff09;DisposableBean Aware接口在生命周期实例化过…

eSIM Network搭建指南

有任何关于GSMA\IOT\eSIM\RSP\业务应用场景相关的问题&#xff0c;欢迎W: xiangcunge59 一起讨论, 共同进步 (加的时候请注明: 来自CSDN-iot).

redis 缓存一致性,缓存穿透,缓存雪崩,缓存击穿

1.缓存一致性&#xff1a; 缓存一致性就是通过各种方法保证缓存与数据库信息一种&#xff0c;其中最多的办法就是想尽一切办法对过期key进行清除&#xff0c;以保证redis和数据库信息一只&#xff0c;其中就包括了这篇文章中提到的内存淘汰策略&#xff0c;过期key的清除等等&…