当前位置: 首页 > news >正文

使用 C++23 从零实现 RISC-V 模拟器(1):最简CPU

👉🏻 文章汇总「从零实现模拟器、操作系统、数据库、编译器…」:https://okaitserrj.feishu.cn/docx/R4tCdkEbsoFGnuxbho4cgW2Yntc

本节实现一个最简的 CPU ,最终能够解析 addaddi 两个指令。如果对计算机组成原理已经有所了解可以跳过下面的内容直接看代码实现。

完整代码在这个分支:lab1-cpu-add,本章节尾有运行的具体指令。

1. 冯诺依曼结构

冯·诺依曼结构是现代计算机体系结构的基础,由约翰·冯·诺依曼在 1945 年提出。这种结构也称为冯·诺依曼体系结构,其核心特点是将程序指令和数据存储在同一个读写存储器(内存)中,计算机的工作流程则是按顺序执行存储器中的指令。这一概念是区别于早期的计算机设计,如图灵机和哈佛架构,后者将数据存储和指令存储分开。

冯·诺依曼结构的五大组成部分:

  1. 中央处理单元(CPU):负责解释计算机程序中的指令和处理数据。
  2. 存储器:存储程序和数据。在冯·诺依曼结构中,程序指令和数据共享同一存储区。
  3. 输入设备:用于将数据和程序输入到计算机中。
  4. 输出设备:将处理结果展示给用户。
  5. 控制单元(CU):协调其他部件的操作。

工作流程:

  1. 指令和数据的存储:在开始执行程序之前,程序的指令和所需的数据被加载到存储器中。
  2. 指令的执行:CPU 从存储器中读取指令,解析指令,然后执行指令。这个过程包括从存储器读取数据、在算术逻辑单元(ALU)中进行计算,以及将结果写回存储器或通过输出设备展示出来。
+------------------+      +-------------+
|    输入设备       | ---> |             |
+------------------+      |             ||             |
+------------------+      |             |      +-----------------+
|    存储器         | <--> |     CPU     | ---> |    输出设备      |
+------------------+      |             |      +-----------------+|             |
+------------------+      |             |
|    控制单元       | <--> |             |
+------------------+      +-------------+

在这个结构中,控制单元指导操作的流程,确保指令正确执行。输入设备可以是键盘、鼠标等,它们把用户的输入转换成机器可以理解的数据。输出设备如显示器和打印机,用于向用户展示结果。存储器不仅保存了待处理的数据,还保存了计算机程序的所有指令。CPU 是核心部件,执行所有的计算和逻辑处理。

重要性:

冯·诺依曼结构的提出,极大推动了计算机科学的发展,使得计算机设计变得更加灵活,程序存储成为可能。在这种架构下,更改程序不再需要重新设计计算机硬件,仅需在存储器中替换或修改程序即可。这一概念至今仍是大多数计算机设计的基础。

2. 最简 CPU

用数组来模拟内存,其中存放待执行的指令。pc 是程序计数器(Program Counter)的简写,用来指向当前正在执行指令的下一条指令。此外 RISC-V 有 32 个寄存器,可以用数组来存放,寄存器用来存放临时产生数据。

// main.cpp
#include <vector>
#include <array>
#include <cstdint>// 定义DRAM_SIZE为128MB
const uint64_t DRAM_SIZE = 1024 * 1024 * 128;class Cpu {// RISC-V 有 32 个寄存器std::array<uint64_t, 32> regs;// PC 寄存器包含下一条指令的内存地址uint64_t pc;// 内存,一个字节数组。在真实的 CPU 中没有内存,这里仅作模拟。std::vector<uint8_t> dram;public:// 构造函数Cpu(const std::vector<uint8_t>& code) : pc(0), dram(code) {regs.fill(0); // 初始化寄存器为0regs[2] = DRAM_SIZE - 1; // 设置堆栈指针寄存器的初始值}// 可能需要的其他成员函数声明
};int main() {// 示例代码使用std::vector<uint8_t> code = { /* 初始化代码 */ };Cpu cpu(code);// 使用cpu对象进行操作
}

其中 pc 的值置为 0 表示表示程序从地址 0 处开始执行。

2. CPU 流水线

在现代计算机体系结构中,尤其是遵循冯·诺依曼架构的计算机系统,程序的执行可以分解为几个连续的阶段,这些阶段共同构成了 CPU 的指令周期。这些阶段包括取指(Instruction Fetch, IF)、解码(Instruction Decode, ID)、执行(Execute, EX)、访存(Memory Access, MEM)和写回(Write Back, WB)。这个过程是循环进行的,每个阶段完成特定的任务,确保计算机程序顺利执行。

+--------+   +--------+   +--------+   +--------+   +--------+
| 取指IF  |-->| 解码ID |-->| 执行EX  |-->| 访存MEM |-->| 写回WB |
+--------+   +--------+   +--------+   +--------+   +--------+

在这个示意图中,每行代表 CPU 中的一条指令随时间前进经过不同的处理阶段。每个方框代表流水线的一个阶段,箭头表示指令从一个阶段移动到下一个阶段的流程。这种设计使得在任何给定的时钟周期内,最多可以有五条指令处于不同的执行阶段,极大提高了 CPU 的效率和性能。

  • 取指 IF:从内存中读取指令。
    • CPU 使用程序计数器(PC)指向的地址从内存中读取指令。读取后,PC 会更新到下一条指令的地址,为下一个周期准备。
  • 解码 ID:解析指令并准备必要的操作数。
    • 指令解码器(ID)解析取出的指令,确定需要执行的操作和操作数。这可能涉及到从指令中提取立即数、计算地址、确定寄存器编号等。
  • 执行 EX:执行计算或其他操作。
    • 执行阶段根据解码阶段的结果,进行相应的算术逻辑运算(ALU 操作)、分支决策或其他操作。此阶段可能需要使用到 ALU(算术逻辑单元)。
  • 访存 MEM:进行内存访问,如数据加载或存储。
    • 如果指令需要读取内存(如加载操作)或向内存写入数据(如存储操作),这一阶段将完成该操作。这一步是可选的,因为不是所有指令都需要访问内存。
  • 写回 WB:将执行结果写回寄存器。
    • 将执行阶段的结果或访存阶段从内存读取的数据写回到指定的寄存器。这一步确保了指令的执行结果可以被后续指令使用。

这五个阶段共同构成了指令的完整执行周期,是现代 CPU 设计的基础。通过将指令执行分解为这些阶段,计算机能够以高效和有序的方式运行程序。每个阶段都由 CPU 的不同部件负责,使得计算机能够在任何给定时刻执行多条指令的不同阶段,这种设计是流水线处理的基础,极大提高了 CPU 的执行效率。

3. 取指

接下来实现Cpu类中的fetch函数。此函数的目的是从 CPU 内部的动态随机存取存储器(DRAM)中读取当前程序计数器(pc)指向的指令。

文本图形化解释:

假设我们有以下 DRAM 内容和一个pc值指向 DRAM 的起始位置:

DRAM 内容(示例):
+----+----+----+----+
| 01 | 02 | 03 | 04 | ...
+----+----+----+----+↑pc
  • 步骤 1: pc指向 DRAM 中的第一个字节。

  • 步骤 2: fetch函数读取pc指向的四个字节(01, 02, 03, 04)。

  • 步骤 3: 将这四个字节组合成一个 32 位的指令。

组合过程:01                   02                   03                   04
00000001 (字节1) | 00000010 (字节2) << 8 | 00000011 (字节3) << 16 | 00000100 (字节4) << 24
= 04030201 (十六进制)
          DRAM
+----+----+----+----+----+----+----+----+
| 01 | 02 | 03 | 04 | xx | xx | xx | xx | ...
+----+----+----+----+----+----+----+----+↑pc(假设pc=0)fetch操作:
1. 读取 [01] → index=pc
2. 读取 [02] → index=pc+1
3. 读取 [03] → index=pc+2
4. 读取 [04] → index=pc+3组合为32位指令:04030201

代码解释:

// main.cpp
class Cpu {// ...
public:// ...// Fetch函数用于读取当前pc指向的指令uint32_t fetch() {size_t index = static_cast<size_t>(pc); // 确保pc值在转换时不会丢失信息uint32_t inst = static_cast<uint32_t>(dram[index])| (static_cast<uint32_t>(dram[index + 1]) << 8)| (static_cast<uint32_t>(dram[index + 2]) << 16)| (static_cast<uint32_t>(dram[index + 3]) << 24);return inst;}
};
  • size_t index = static_cast<size_t>(pc);pc的值转换为适合作为索引的类型。
  • static_cast<uint32_t>(dram[index]) 读取第一个字节,并保持其原位。
  • (static_cast<uint32_t>(dram[index + 1]) << 8) 读取第二个字节,并左移 8 位。
  • (static_cast<uint32_t>(dram[index + 2]) << 16) 读取第三个字节,并左移 16 位。
  • (static_cast<uint32_t>(dram[index + 3]) << 24) 读取第四个字节,并左移 24 位。
  • 这四个部分通过位或操作(|)组合成一个完整的 32 位指令。

这个过程展示了如何从连续的字节中构建一个完整的指令,是执行流水线中取指阶段的关键步骤。

同时上面的实现是小端,如果反过来高位数据位于低地址部分就是大端。

4. 解码

上一部分已经读取到指令了,接下来就是解析指令然后执行。接下来实现如何解析 add 和 addi 指令,在解析之前要先弄清楚这两个指令的具体作用及其使用场景。

add 指令和 addi 指令是在汇编语言和计算机架构中常用的两种基本指令,尤其在 MIPS 架构中广泛应用。它们用于进行加法运算,但在操作方式和使用场景上有所不同。

add 指令

add 指令用于将两个寄存器中的数值相加,并将结果存储在另一个寄存器中。这是一种寄存器到寄存器的操作。

格式: add 目标寄存器, 源寄存器1, 源寄存器2

例子:

add $t0, $t1, $t2

这条指令的意思是将 $t1$t2 中的值相加,然后将结果存储在 $t0 中。

addi 指令

addi 指令是 “add immediate” 的缩写,它将一个寄存器中的值与一个立即数(即直接提供的数值)相加,并将结果存储在另一个寄存器中。这是一种寄存器到立即数的操作。

格式: addi 目标寄存器, 源寄存器, 立即数

例子:

addi $t0, $t1, 5

这条指令的意思是将 $t1 中的值与立即数 5 相加,然后将结果存储在 $t0 中。

使用场景

  • add 指令 通常用于需要将两个变量的值相加的情况,这两个变量的值在执行指令之前已经被加载到寄存器中。
  • addi 指令 常用于需要将某个变量的值与一个已知的常数相加的场景,例如数组索引计算、根据偏移量计算地址等。

为了以文本图形化的方式更直观地展示这两个指令的作用,我们可以用简化的图表来表示它们的操作流程:

  1. add 操作流程
   [寄存器1]     [寄存器2]|             ||             |+----加法----+|↓[目标寄存器]
  1. addi 操作流程
   [寄存器]     [立即数]|           ||           |+----加法----+|↓[目标寄存器]

通过上面的解释和图形化表示,可以看出 addaddi 指令在汇编语言编程中如何用于处理不同的加法运算场景。

指令内部组成

上部分已经讲解了 add 和 addi 指令的具体功能和使用场景,接下来讲解这两个指令是如何存放在内存中的。

add 指令格式

add 指令在 RISC-V 中是一种 R 型(寄存器-寄存器)指令,用于将两个寄存器的数值相加,并将结果存储在第三个寄存器中。

  • 操作码(opcode):标识这是一种什么操作的字段,对于 add 指令,操作码指定了这是一种算术操作。
  • 目标寄存器(rd):存放操作结果的寄存器。
  • 功能码(funct3):提供操作的进一步细化,对于 add 来说,这个字段有特定的值。
  • 源寄存器 1(rs1):第一个操作数的寄存器。
  • 源寄存器 2(rs2):第二个操作数的寄存器。
  • 功能码(funct7):与 funct3 合作,确定是哪种具体的算术操作,add 有其特定的值。
┌───────┬─────┬──────┬─────┬─────┬────────┐
│opcode │  rd │funct3│ rs1 │ rs2 │ funct7 │
└───────┴─────┴──────┴─────┴─────┴────────┘7 bits  5 bits 3 bits 5 bits 5 bits  7 bits

接下来结合具体的汇编代码来讲解:

  1. 对于 add x7, x5, x6 指令:将寄存器 x5 和寄存器 x6 的值相加,并将结果存储到寄存器 x7 中。

下面是对应在内存中的表示:

┌────────┬──────┬──────┬──────┬──────┬────────┐
│opcode  │  rd  │funct3│ rs1  │ rs2  │ funct7 │
│ 0110011│ 00111│ 000  │ 00101│ 00110│ 0000000│
└────────┴──────┴──────┴──────┴──────┴────────┘7 bits  5 bits 3 bits 5 bits 5 bits  7 bits
  • 操作码(opcode)0110011,表示这是一个 R-type 指令。
  • 目标寄存器(rd)00111,即寄存器 x7
  • 功能码(funct3)000,与 funct7 一起确定这是一个加法操作。
  • 源寄存器 1(rs1)00101,即寄存器 x5
  • 源寄存器 2(rs2)00110,即寄存器 x6
  • 功能码(funct7)0000000,与 funct3 一起指定了这是一个 add 操作。
RISC-V addi 指令格式

addi 指令在 RISC-V 中是一种 I 型(立即数)指令,它将一个寄存器中的数值与一个立即数相加,并将结果存储在另一个寄存器中。

  • 操作码(opcode):标识操作类型的字段,对于 addi,这指定了是一种立即数加法操作。
  • 目标寄存器(rd):存放操作结果的寄存器。
  • 功能码(funct3):进一步指定了操作的类型,addi 有其特定的值。
  • 源寄存器(rs1):操作数的寄存器。
  • 立即数(imm):与源寄存器中的值相加的立即数。
┌───────┬─────┬─────┬─────┬─────────────────┐
│opcode │  rd │funct3│ rs1 │       imm       │
└───────┴─────┴─────┴─────┴─────────────────┘7 bits  5 bits 3 bits 5 bits      12 bits

接下来结合具体的汇编代码来讲解:

对于 addi x7, x5, 10 指令: 将寄存器 x5 的值与立即数 10 相加,并将结果存储到寄存器 x7 中。

下面是对应在内存中的表示:

┌────────┬──────┬──────┬──────┬─────────────────┐
│opcode  │  rd  │funct3│ rs1  │       imm       │
│ 0010011│ 00111│ 000  │ 00101│ 0000000000101010│
└────────┴──────┴──────┴──────┴─────────────────┘7 bits  5 bits 3 bits 5 bits      12 bits
  • 操作码(opcode)0010011,表示这是一个 I-type 指令。
  • 目标寄存器(rd)00111,即寄存器 x7
  • 功能码(funct3)000,确定这是一个立即数加法操作。
  • 源寄存器(rs1)00101,即寄存器 x5
  • 立即数(imm)0000000000101010,表示十进制数 10。这里立即数字段实际上是 12 位,为简化表示,应解释为补码形式,代表正数 10

通过上面的例子,我们可以清楚地看到 RISC-V 架构下 addaddi 指令的内部组成及其编码方式。这种表示不仅有助于理解指令的结构,也方便在设计汇编语言程序时进行指令选择和使用。

使用场景和解析

  • add 指令 用于两个寄存器值的加法运算,常用于各种数值计算和数据处理任务。
  • addi 指令 用于将寄存器值与立即数相加,常见于地址计算、数值调整等场景。

这些指令的设计反映了 RISC-V 指令集的目标,即提供简单、高效且足够灵活的指令集,以支持现代编译器技术和硬件实现的需求。

代码

// main.cpp
class Cpu {
public:// 其他成员和方法...// 执行指令的函数void execute(uint32_t inst) {// 解析指令中的操作码(opcode),占用最低的7位uint32_t opcode = inst & 0x7f;// 解析目标寄存器(rd),位于指令的第7到11位uint32_t rd = (inst >> 7) & 0x1f;// 解析第一个源寄存器(rs1),位于指令的第15到19位uint32_t rs1 = (inst >> 15) & 0x1f;// 解析第二个源寄存器(rs2),位于指令的第20到24位uint32_t rs2 = (inst >> 20) & 0x1f;// 解析功能码(funct3),位于指令的第12到14位uint32_t funct3 = (inst >> 12) & 0x7;// 解析功能码(funct7),位于指令的第25到31位uint32_t funct7 = (inst >> 25) & 0x7f;// 寄存器x0永远为0regs[0] = 0;// 执行阶段switch (opcode) {case 0x13: { // 处理addi指令// 解析立即数,将指令的最高20位视为符号扩展的立即数int64_t imm = static_cast<int32_t>(inst & 0xfff00000) >> 20;// 执行加法操作,将rs1寄存器的值与立即数相加,并将结果存入rd寄存器regs[rd] = regs[rs1] + imm;break;}case 0x33: { // 处理add指令// 执行加法操作,将rs1和rs2寄存器的值相加,并将结果存入rd寄存器regs[rd] = regs[rs1] + regs[rs2];break;}default:// 如果操作码不是预期中的值,则输出错误信息std::cerr << "Invalid opcode: " << std::hex << opcode << std::endl;break;}}// 其他成员变量和方法...
};

5. 测试

上面已经上一个最简 CPU 了,接下来需要增加一些辅助功能来使得 CPU 跑起来。

首先是需要能够查看寄存器中的数据,根据数据变化来验证指令执行正确。

class Cpu {
public:// 其他成员和方法...// RISC-V 寄存器名称const std::array<std::string, 32> RVABI = {"zero", "ra", "sp", "gp", "tp", "t0", "t1", "t2","s0", "s1", "a0", "a1", "a2", "a3", "a4", "a5","a6", "a7", "s2", "s3", "s4", "s5", "s6", "s7","s8", "s9", "s10", "s11", "t3", "t4", "t5", "t6",};void dump_registers() {std::cout << std::setw(80) << std::setfill('-') << "" << std::endl; // 打印分隔线std::cout << std::setfill(' '); // 重置填充字符for (size_t i = 0; i < 32; i += 4) {std::cout << std::setw(4) << "x" << i << "(" << RVABI[i] << ") = " << std::hex << std::setw(16) << std::setfill('0') << regs[i] << " "<< std::setw(4) << "x" << i + 1 << "(" << RVABI[i + 1] << ") = " << std::setw(16) << regs[i + 1] << " "<< std::setw(4) << "x" << i + 2 << "(" << RVABI[i + 2] << ") = " << std::setw(16) << regs[i + 2] << " "<< std::setw(4) << "x" << i + 3 << "(" << RVABI[i + 3] << ") = " << std::setw(16) << regs[i + 3] << std::endl;}}
};

总的来说上面的代码就是为 32 个寄存器增加了对应的名称以及提供一个一个能够打印其中数值的方法。

创建 add-addi.s 并写入下面的内容

.global _start
_start:addi x29, x0, 5addi x30, x0, 37add x31, x30, x29

在汇编语言中,.global _start_start: 的语句定义了程序的入口点。

  • .global _start:这条指令告诉链接器(linker),_start 标签是一个全局符号,可以被程序的其他部分或其他链接的文件访问。更重要的是,它标示 _start 为程序的入口点,即程序执行的起始位置。这对于操作系统(OS)来说非常关键,因为在程序被加载到内存并执行时,操作系统需要知道从哪里开始执行程序。

  • _start::这是一个标签,紧跟在它后面的是程序的入口点。在这个位置上编写的指令将会是程序执行的第一批指令。在一个裸机(bare-metal)环境或操作系统内核开发中,_start 是执行流程的起点,没有标准库或运行时环境的初始化过程。

综上所述,.global _start_start: 一起定义了程序开始执行的地方,为操作系统提供了一个明确的起点来运行程序。如果不写的话会报错。

这段代码是使用 RISC-V 汇编语言编写的,它执行了一个非常简单的任务:计算两个数值的和,并将结果存储。具体来说,代码做了以下几件事情:

  1. 将数字 5 存储到寄存器 x29。
  2. 将数字 37 存储到寄存器 x30。
  3. 将寄存器 x29 和 x30 中的数值相加,将结果存储到寄存器 x31。

总结来说,这段代码简单地计算了 5 和 37 的和,然后将结果 42 存储到寄存器 x31 中。

将汇编转为二进制文件:

$ riscv64-unknown-elf-gcc -Wl,-Ttext=0x0 -nostdlib -o add-addi add-addi.s
$ riscv64-unknown-elf-objcopy -O binary add-addi add-addi.bin

运行并测试是否正确:

mkdir -p build && cd build && cmake .. && make && ./crvemu ../add-addi.bin
~/crvemu/build$ ./crvemu ../add-addi.bin
--------------------------------------------------------------------------------
x0(zero) = 0000000000000000 000x1(ra) = 0000000000000000 000x2(sp) = 0000000007ffffff 000x3(gp) = 0000000000000000
000x4(tp) = 0000000000000000 000x5(t0) = 0000000000000000 000x6(t1) = 0000000000000000 000x7(t2) = 0000000000000000
000x8(s0) = 0000000000000000 000x9(s1) = 0000000000000000 000xa(a0) = 0000000000000000 000xb(a1) = 0000000000000000
000xc(a2) = 0000000000000000 000xd(a3) = 0000000000000000 000xe(a4) = 0000000000000000 000xf(a5) = 0000000000000000
000x10(a6) = 0000000000000000 000x11(a7) = 0000000000000000 000x12(s2) = 0000000000000000 000x13(s3) = 0000000000000000
000x14(s4) = 0000000000000000 000x15(s5) = 0000000000000000 000x16(s6) = 0000000000000000 000x17(s7) = 0000000000000000
000x18(s8) = 0000000000000000 000x19(s9) = 0000000000000000 000x1a(s10) = 0000000000000000 000x1b(s11) = 0000000000000000
000x1c(t3) = 0000000000000000 000x1d(t4) = 0000000000000005 000x1e(t5) = 0000000000000025 000x1f(t6) = 000000000000002a

注意最后一行最后三个,因为是二进制,所以数据是正确的,例如 25 对应的十进制就是 37 。

至此本节内容已经完成,目前已经能够实现解析 add 和 addi 两个指令。

👉🏻 文章汇总「从零实现模拟器、操作系统、数据库、编译器…」:https://okaitserrj.feishu.cn/docx/R4tCdkEbsoFGnuxbho4cgW2Yntc

相关文章:

使用 C++23 从零实现 RISC-V 模拟器(1):最简CPU

&#x1f449;&#x1f3fb; 文章汇总「从零实现模拟器、操作系统、数据库、编译器…」&#xff1a;https://okaitserrj.feishu.cn/docx/R4tCdkEbsoFGnuxbho4cgW2Yntc 本节实现一个最简的 CPU &#xff0c;最终能够解析 add 和 addi 两个指令。如果对计算机组成原理已经有所了…...

顺序表、链表(ArrayList、LinkedList)

目录 前言&#xff1a; 顺序表&#xff08;ArrayList&#xff09;&#xff1a; 顺序表的原理&#xff1a; ArrayList源码&#xff1a; 的含义&#xff1a;​编辑 ArrayList的相关方法&#xff1a;​编辑 向上转型List&#xff1a; 练习题&#xff08;杨辉三角&#x…...

第11讲投票创建后端实现

投票创建页面实现 文件选择上传组件 uni-file-picker 扩展组件 安装 https://ext.dcloud.net.cn/plugin?nameuni-file-picker 日期选择器uni-datetime-picker组件 安装 https://ext.dcloud.net.cn/plugin?nameuni-datetime-picker iconfont小图标 https://www.iconfont…...

SNMP 简单网络管理协议、网络管理

目录 1 网络管理 1.1 网络管理的五大功能 1.2 网络管理的一般模型 1.3 网络管理模型中的主要构件 1.4 被管对象 (Managed Object) 1.5 代理 (agent) 1.6 网络管理协议 1.6.1 简单网络管理协议 SNMP 1.6.2 SNMP 的指导思想 1.6.3 SNMP 的管理站和委托代理 1.6.4 SNMP…...

计算机设计大赛 深度学习YOLOv5车辆颜色识别检测 - python opencv

文章目录 1 前言2 实现效果3 CNN卷积神经网络4 Yolov56 数据集处理及模型训练5 最后 1 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; **基于深度学习YOLOv5车辆颜色识别检测 ** 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0…...

OpenCV-36 多边形逼近与凸包

目录 一、多边形的逼近 二、凸包 一、多边形的逼近 findContours后的轮廓信息countours可能过于复杂不平滑&#xff0c;可以用approxPolyDP函数对该多边形曲线做适当近似&#xff0c;这就是轮廓的多边形逼近。 apporxPolyDP就是以多边形去逼近轮廓&#xff0c;采用的是Doug…...

transformer中的QKV是如何得到的?

多头自注意力机制&#xff1a;...

console.log导致内存泄露 打包时自动去掉console.log方法

webpack通过工具&#xff1a;terser 使用前需要先安装一下 vue.config.js const { defineConfig } require(vue/cli-servise); module.exports defineConfig({transpileDependencies:true,terser:{terserOptions:{compress:{drop_console:true,drop_debugger:true,},},},}…...

《合成孔径雷达成像算法与实现》FIgure6.20

% rho_r c/(2*Fr)而不是rho_r c/(2*Bw) % Hsrcf exp函数里忘记乘pi了 clc clear close all参数设置 距离向参数设置 R_eta_c 20e3; % 景中心斜距 Tr 2.5e-6; % 发射脉冲时宽 Kr 20e12; % 距离向调频率 alpha_os_r 1.2; …...

Spring Boot 笔记 015 创建接口_更新文章分类

1.1.1 实体类id增加NotNull注释&#xff0c;并做分组校验 1.1.1.1 定义分组 1.1.1.2 实体类中指定校验项属于哪个分组 如果说某个校验项没有指定分组,默认属于Default分组 分组之间可以继承, A extends B 那么A中拥有B中所有的校验项package com.geji.pojo;import com.faste…...

【Java基础题型】判断是否是回文数

需求&#xff1a;如果给你一个正数x。 如果x是一个回文整数&#xff0c;打印true&#xff0c;否则&#xff0c;返回false 解释&#xff1a; 回文数是指正序(从左到右)和从倒序(从右到左)都是一样的整数数字。 eg.121是回文数&#xff0c;123不是&#xff0c;2112是回文数&…...

Linux paste命令教程:并行合并文件的利器(附案例详解和注意事项)

Linux paste命令介绍 paste 是一个在 Unix 或 Linux 操作系统中非常有用的命令。它用于通过在标准输出中输出由每个指定文件的行组成的行&#xff0c;以制表符为分隔符&#xff0c;来水平&#xff08;并行&#xff09;合并文件。 Linux paste命令适用的Linux版本 paste 命令…...

用163邮箱或者outlook接收国科大邮箱的邮件

使用如图下路径&#xff0c;创建一个新的密码&#xff0c;用于在163大师邮箱或者outlook登录即可 如果不行&#xff0c;则需要手动配置邮箱服务器 参考网址&#xff1a;中国科学院邮件系统帮助中心...

VitePress-15- 配置- description 的作用详解

作用描述 1、descriptioin 是站点的描述&#xff0c; 会被解析为 html 页面的 <meta name"description" content "xxx"> 标签 。2、description 本身就是 <meta> 标签的一种&#xff0c;不会在页面上展示出来&#xff0c; 仅仅是作为页面的一…...

寒假学习记录17:包管理器(包管理工具)

概念 包&#xff08;package&#xff09; 包含元数据的库&#xff0c;这些元数据包括&#xff1a;名称&#xff0c;描述&#xff0c;git主页&#xff0c;许可证协议&#xff0c;作者&#xff0c;依赖..... 库&#xff08;library&#xff0c;简称lib&#xff09; 以一个或多个模…...

【AIGC】Stable Diffusion的常见错误

Stable Diffusion 在使用过程中可能会遇到各种各样的错误。以下是一些常见的错误以及可能的解决方案&#xff1a; 模型加载错误&#xff1a;可能出现模型文件损坏或缺失的情况。解决方案包括重新下载模型文件&#xff0c;确保文件完整并放置在正确的位置。 依赖项错误&#x…...

线段树解决-----P1161 开灯 P1047 [NOIP2005 普及组] 校门外的树 python解法

# [NOIP2005 普及组] 校门外的树 ## 题目描述 某校大门外长度为 l 的马路上有一排树&#xff0c;每两棵相邻的树之间的间隔都是 1 米。我们可以把马路看成一个数轴&#xff0c;马路的一端在数轴 0 的位置&#xff0c;另一端在 l的位置&#xff1b;数轴上的每个整数点&#xf…...

学习总结16

# 【模板】最小生成树 ## 题目描述 如题&#xff0c;给出一个无向图&#xff0c;求出最小生成树&#xff0c;如果该图不连通&#xff0c;则输出 orz。 ## 输入格式 第一行包含两个整数 N,M&#xff0c;表示该图共有 N 个结点和 M 条无向边。 接下来 M 行每行包含三个整数 …...

问题:从完整的问题解决过程来看,( )是首要环节。A.理解问题 B.提出假设C.发现问题 D.检验假设 #学习方法#学习方法

问题&#xff1a;从完整的问题解决过程来看&#xff0c;&#xff08; &#xff09;是首要环节。A&#xff0e;理解问题 B&#xff0e;提出假设C&#xff0e;发现问题 D&#xff0e;检验假设 A.理解问题 B.提出假设 C&#xff0e;发现问题 参考答案如图所示...

服务器感染了.mallox勒索病毒,如何确保数据文件完整恢复?

导言&#xff1a; 在当今数字化的世界中&#xff0c;恶意软件已成为企业和个人数据安全的一大威胁&#xff0c;其中.mallox勒索病毒是最为恶劣的之一。本文91数据恢复将介绍.mallox勒索病毒的特点&#xff0c;以及如何恢复被其加密的数据文件以及预防措施。 如果您正在经历勒索…...

vscode里如何用git

打开vs终端执行如下&#xff1a; 1 初始化 Git 仓库&#xff08;如果尚未初始化&#xff09; git init 2 添加文件到 Git 仓库 git add . 3 使用 git commit 命令来提交你的更改。确保在提交时加上一个有用的消息。 git commit -m "备注信息" 4 …...

linux之kylin系统nginx的安装

一、nginx的作用 1.可做高性能的web服务器 直接处理静态资源&#xff08;HTML/CSS/图片等&#xff09;&#xff0c;响应速度远超传统服务器类似apache支持高并发连接 2.反向代理服务器 隐藏后端服务器IP地址&#xff0c;提高安全性 3.负载均衡服务器 支持多种策略分发流量…...

.Net框架,除了EF还有很多很多......

文章目录 1. 引言2. Dapper2.1 概述与设计原理2.2 核心功能与代码示例基本查询多映射查询存储过程调用 2.3 性能优化原理2.4 适用场景 3. NHibernate3.1 概述与架构设计3.2 映射配置示例Fluent映射XML映射 3.3 查询示例HQL查询Criteria APILINQ提供程序 3.4 高级特性3.5 适用场…...

云启出海,智联未来|阿里云网络「企业出海」系列客户沙龙上海站圆满落地

借阿里云中企出海大会的东风&#xff0c;以**「云启出海&#xff0c;智联未来&#xff5c;打造安全可靠的出海云网络引擎」为主题的阿里云企业出海客户沙龙云网络&安全专场于5.28日下午在上海顺利举办&#xff0c;现场吸引了来自携程、小红书、米哈游、哔哩哔哩、波克城市、…...

多场景 OkHttpClient 管理器 - Android 网络通信解决方案

下面是一个完整的 Android 实现&#xff0c;展示如何创建和管理多个 OkHttpClient 实例&#xff0c;分别用于长连接、普通 HTTP 请求和文件下载场景。 <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas…...

FastAPI 教程:从入门到实践

FastAPI 是一个现代、快速&#xff08;高性能&#xff09;的 Web 框架&#xff0c;用于构建 API&#xff0c;支持 Python 3.6。它基于标准 Python 类型提示&#xff0c;易于学习且功能强大。以下是一个完整的 FastAPI 入门教程&#xff0c;涵盖从环境搭建到创建并运行一个简单的…...

【论文笔记】若干矿井粉尘检测算法概述

总的来说&#xff0c;传统机器学习、传统机器学习与深度学习的结合、LSTM等算法所需要的数据集来源于矿井传感器测量的粉尘浓度&#xff0c;通过建立回归模型来预测未来矿井的粉尘浓度。传统机器学习算法性能易受数据中极端值的影响。YOLO等计算机视觉算法所需要的数据集来源于…...

Java毕业设计:WML信息查询与后端信息发布系统开发

JAVAWML信息查询与后端信息发布系统实现 一、系统概述 本系统基于Java和WML(无线标记语言)技术开发&#xff0c;实现了移动设备上的信息查询与后端信息发布功能。系统采用B/S架构&#xff0c;服务器端使用Java Servlet处理请求&#xff0c;数据库采用MySQL存储信息&#xff0…...

【无标题】路径问题的革命性重构:基于二维拓扑收缩色动力学模型的零点隧穿理论

路径问题的革命性重构&#xff1a;基于二维拓扑收缩色动力学模型的零点隧穿理论 一、传统路径模型的根本缺陷 在经典正方形路径问题中&#xff08;图1&#xff09;&#xff1a; mermaid graph LR A((A)) --- B((B)) B --- C((C)) C --- D((D)) D --- A A -.- C[无直接路径] B -…...

C++.OpenGL (20/64)混合(Blending)

混合(Blending) 透明效果核心原理 #mermaid-svg-SWG0UzVfJms7Sm3e {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-SWG0UzVfJms7Sm3e .error-icon{fill:#552222;}#mermaid-svg-SWG0UzVfJms7Sm3e .error-text{fill…...