LLVM Cpu0 新后端8 尾调用优化 Stack Overflow Exception异常

 想好好熟悉一下llvm开发一个新后端都要干什么,于是参考了老师的系列文章:

LLVM 后端实践笔记

代码在这里(还没来得及准备,先用网盘暂存一下):

链接: https://pan.baidu.com/s/1yLAtXs9XwtyEzYSlDCSlqw?pwd=vd6s 提取码: vd6s 

在这一章,我们会在 Cpu0 后端中增加对子过程/函数调用的翻译功能,会添加大量代码。这一章首先会介绍 Mips 的栈帧结构,我们的 Cpu0 也会借用 Mips 的栈帧设计,大多数 RISC 机器的栈帧设计都是类似的,如果你对这块的背景知识有困惑,需要先查阅其他书籍,比如《深入理解计算机系统》这类计算机体系结构的书。

目录

1. 尾调用优化

1.1 什么是尾调用?

1.2 尾调用能做什么优化呢?

1.3 尾递归

2. Stack Overflow Exception

3. 修改的文件

3.1 Cpu0ISelLowering.cpp

3.1.1 LowerFormalArgument()

3.1.2 LowerCall()

3.1.3 LowerCall()中的尾调用

3.2 Cpu0MachineFunctionInfo.cpp

3.3 Cpu0SEISelLowering.cpp

3.4 Cpu0FrameLowering.cpp/.h

3.5 Cpu0InstrInfo.td

3.6 Cpu0InstrInfo.cpp

3.7 Cpu0MCInstLower.cpp/.h

3.8 Cpu0MachineFunctionInfo.cpp/.h

3.9 Cpu0SEFrameLowering.cpp/.h

3.10 MCTargetDesc/Cpu0AsmBackend.cpp

3.11 MCTargetDesc/Cpu0ELFObjectWriter.cpp

3.12 MCTargetDesc/Cpu0Fixupkinds.h

3.13 MCTargetDesc/Cpu0MCCodeEmitter.cpp

4. 结果

4.1 普通参数

4.2 结构体参数

4.3 字符串参数

4.4 浮点运算

4.5 尾调用优化


1. 尾调用优化

在这一章我们会添加一个跟尾调用优化有关系的函数,虽然这不是本章的重点,但是尾调用优化是编译器后端较重要的一个优化点,这里简单展开介绍一下。

1.1 什么是尾调用?

int f(int x) {
  return g(x);
}

int m(int x) {
  if (x > 0)
    return m(x);
  return n(x);
}

上述的两个函数都属于尾调用。

int f(int x) {
  int a = g(x);
  return a;
}
int m(int x) {
  return g(x) + 1;
}

上述两个函数都不属于尾调用。

尾调用就是指某个函数的最后一步是调用另一个函数,注意到这里的最后一步,上两个函数在调用之后都有别的操作,因此不满足尾调用。而且要注意这里的最后一步指的是程序执行上的最后一步,并不是说一定要写在最后。

1.2 尾调用能做什么优化呢?

函数调用的时候会在内存形成一个调用记录:调用栈,保存调用位置和内部变量等信息。如果函数main调用了函数foo,那么在main的调用记录上方会形成一个foo的调用记录。等到程序foo运行结束将结果返回给main,foo的调用记录才会消失。如果函数foo的内部还调用了函数begin,那么函数foo的上部还有一个begin的调用记录,以此类推。所以的调用记录会形成一个调用栈,在gdb调试的时候,我们bt就可以打印出来当前状态下的调用栈。

尾调用特殊的地方是他是一个函数执行的最后一步,如果说begin函数是foo函数内的尾调用,那么在begin函数结束的时候foo函数也会直接结束,这里就有一点可以优化的地方了。是不是说明在尾调用的时候就不需要外层函数的栈桢了呢?begin函数结束之后可以直接返回到上上层函数也就是main函数中了,因为begin之后foo函数内不会有别的操作了,这就是尾调用的一个优化点,能够缓解栈桢的压力。

1.3 尾递归

int f(int x, int total) {
  if (n == 1)
    return total;
  return f(n - 1, n * total);
}

这里一个递归计算阶乘的操作,尾调用自己的话就属于尾递归。我们知道在,正常的递归操作是非常占内存的,因为递归深度可能有成百上千的,而且很容易发生Stack Overflow Exception(栈溢出)。但是对于上例中的尾调用来说,在进入调用函数的时候,我们就不需要上层函数的调用栈了,这样的话我们永远只需要维护当前执行的一个函数的调用栈就可以,大大减轻了栈桢的压力。

同时尾递归还可以优化成循环迭代,参考下边4.5章节。

2. Stack Overflow Exception

上边提到了递归过深容易发生Stack Overflow Exception,Stack Overflow Exception算是异常里边最特殊的一个了,这里简单介绍一下。

我们程序的栈桢的空间肯定是有限的,当我们使用的栈空间超过了我们栈的范围时,就应该正确抛出Stack Overflow Exception异常。这里我说正确抛出,有些人可能会有个疑问,不就抛个异常吗,有什么难的?

对于Stack Overflow Exception这个异常来说还真有一点儿难。因为这个时候我们的栈已经不够用了,然而抛异常我们还需要调用执行对应的异常函数,执行函数又需要栈,这就与我们当前的问题相茅盾了。我们的栈已经不够用了,还支持我们抛出异常吗?这就是我说Stack Overflow Exception是最特殊的异常的原因。

所以,我们想要正确抛出Stack Overflow Exception异常的话一定要做一些特殊的措施:

  • 我们可以提前预留一部分栈,用于抛出Stack Overflow Exception异常。
  • 我们也可以在发生Stack Overflow Exception异常的时候撤回一部分栈,能让我们抛出异常。

 我们能够看到正确抛出Stack Overflow Exception异常不是那么容易的。

3. 修改的文件

Cpu0 函数调用的第一件事是设计好如何使用栈帧,比如在调用函数时,如何保存参数。

具体如下表所示,保存函数参数有两种设计,第一种是将所有参数都保存在栈帧上,第二种是选出一部分寄存器,将部分参数先保存在这些寄存器中,如果参数过多,超出的那些再保存在栈帧上。比如 Mips 设计中,将前 4 个参数保存在寄存器 $a0, $a1, $a2, $a3 中,然后再把多余的其他参数保存在栈帧。

3.1 Cpu0ISelLowering.cpp

3.1.1 LowerFormalArgument()

ISelLowering 中实现了几个重要的函数。其中之一就是 LowerFormalArgument(),LowerFormalArgument()的作用是实现在被调用函数内部将参数传递到被调用函数。回顾一下之前全局变量的实现代码,当时实现了 LowerGlobalAddress() 函数,然后在 td 文件中实现指令选择模板,当代码中存在全局变量的访问时,LLVM 就会访问这个函数。LowerFormalArgument() 也是同样的道理,会在函数被调用时被访问。它从 CCInfo 对象中获取输入参数的信息,比如 ArgLocs.size() 就是传入参数的数量,而每个参数的内容就放在 ArgLocs[i] 中,当 VA.isRegLoc() 为 true 时,表示参数放到寄存器中传递,而 VA.isMemLoc() 为 true 时,就表示参数放到栈上传递,在访问参数时,根据这个值,就可以根据实际情况来做处理加载参数的过程。它内部有一个 for 循环,来依次处理每个参数的情况。

当访问寄存器时,它会先激活寄存器(Live-in),然后拷贝其中的值,当使用内存传递参数时,它会创建栈的偏移对象,然后使用 load 节点来加载值。编译参数 -cpu0-s32-calls=false 时,它会选择将前两个参数从寄存器中读取,否则,所有参数都从栈中 load 出来。在加载参数前,会先调用 analyzeFormalArguments() 函数,在内部使用 fixedArgFn 来返回函数指针 CC_Cpu0O32 或 CC_Cpu0S32,这两个函数分别是处理两种不同的参数加载方式,即前两个参数从寄存器读取还是全部都从栈上加载。

ArgFlags.isByVal 用于处理结构体指针的关键信息,在遇到结构体指针时,会返回 true。当 -cpu0-s32-calls=false 时,栈帧偏移从 8 开始,这就是为了保证前两个从寄存器传递的参数有可能 spill 的情况,当编译参数为 true 时,栈帧偏移就从 0 开始了。传递结构体参数比较特殊,在函数结尾前,有一个和前边一样的 for 循环,再一次遍历所有的参数,并判断如果这个参数存在一个 SRet 标记,就将对应参数的值拷贝到以一个 SRet 寄存器为 base 的栈的偏移中,通过调用 getSRetReturnReg() 获取 SRet 寄存器,通常为 $2。我们还需要一些辅助的函数,比如 loadRegFromStackSlot() 函数,用来将参数从栈帧 load 到寄存器中。

3.1.2 LowerCall()

LowerCall()实现在函数调用时将实参传入栈,以及如何将函数执行结束后的返回值传递回调用函数。在 LowerCall() 函数中,前边先调用了 analyzeCallOperands() 函数,分析 call 操作的操作数,为之后分配地址空间做准备。然后会调用尾调用优化函数 isEligibleForTailCallOptimization(),这里做这样的优化,可以避免尾递归情况下函数频繁开栈空间的问题。通常支持递归的栈式处理器程序都需要对尾调用做额外处理。

之后,插入 CALLSEQ_START 节点,标记进入 call 的输出过程。内部使用一个大循环,对所有参数做遍历,将需要通过寄存器传递的参数 push_back 到 RegsToPass 中,调用了 passByValArg() 函数生成存入寄存器的行为节点链。并在参数大小不满足调用约定的参数做 promote。然后对于通过栈传递的参数,将其加入到 MemOpChains 中,调用了 passArgOnStack() 函数来生成存入栈的行为节点链。

可以展开来看 passByValArg() 函数和 passArgOnStack() 函数的内部实现。如果被调用函数是一个外部函数,包括全局函数(基本都满足这种情况),需要生成一个外部符号加载,这里需要创建一个 TargetGlobalAddress 或 TargetExternalSymbol 节点,从而避免合法化阶段去操作它。其他部分的代码会将这里的节点转换成 load 外部符号的指令并发射。

之后将所有 call 节点参数(所有节点参数,包括实参、返回值、chain 等)使用 getOpndList()汇总起来做处理,在这个函数中,针对不同的参数类型和属性,分别创建不同的操作方式,比如对于需要通过寄存器传递的实参,创建一系列的 copy to reg 操作。最后把所有操作都打包到 `Ops` 中返回。

如果是 PIC 模式,编译器会生成一条 load 重定位的 call 符号的地址 + 一条 jarl;如果不是,则会生成 jsub + 符号地址;PIC 模式会留给链接器之后再去重定位。

最后,生成一条跳转节点 Cpu0ISD::JmpLink,跳转到被调用函数的符号地址,Ops 作为 call 节点参数被引入。对于尾调用,需要额外生成 Cpu0ISD::TailCall 节点。插入 CALLSEQ_END 节点,标记结束 call 动作。

最后会调用 LowerCallResult() 函数处理调用结束返回时的动作。其中调用了 analyzeCallResult 分析返回 call 的参数,并处理所有返回参数,还原 caller saved register。如果是一个 struct 传值的返回值,Flags.isByVal 是为 true,就会将结构体的值依次存入栈中。

需要提及的是,我们在 Cpu0CallConv.td 中定义的 caller register 和 callee register 会在这里参与指导流程,我们通过调用 Cpu0CCInfo 对象来访问这些配置化的属性。

定义了一个统计参数 NumTailCalls,用来计数 case 中尾调用的数量。

3.1.3 LowerCall()中的尾调用

在 LowerCall() 函数中,检查是否可以进行尾调用优化。这个状态是 Clang 给出的,Clang 在前端就可以分析是否满足尾调用特征,并在开优化的情况下生成 tail call 的 IR。如果是尾调用优化,就不必再发射 callseq_start 到 callseq_end 的这段代码了。取而代之的是在指令选择时选择到伪代码 TAILCALL,并在指令发射时展开成 JMP 指令。

新增代码到 Cpu0AsmPrinter.cpp 中,在 EmitInstruction() 中的指令循环时,插入如果满足 emitPseudoExpansionLowering() 时做调用,后者是由 tablegen 自动生成的一个函数,在 Cpu0GenMCPseudoLowering.inc 文件中定义。

尾调用优化是 Clang 支持的一种优化,需要使能至少 O1 优化级别。递归调用层次太深,即使使用了尾调用优化,但依然需要频繁的访问栈。使用循环来替代递归是一种不错的解决问题的方式,Clang 支持这种优化,会分析在尾调用满足循环替代递归的特性时做变换。

3.2 Cpu0MachineFunctionInfo.cpp

增加获取MachinePointerInfo的接口。

3.3 Cpu0SEISelLowering.cpp

继承一个isEligibleForTailCallOptimization函数,判断是否能做尾调用优化。

3.4 Cpu0FrameLowering.cpp/.h

这里实现了一个消除 call frame 伪指令 .cpload 和 .cprestore 的函数 eliminateCallFramePseudoInstr(),因为没有额外的事情要做,所以这里就直接 MBB.erase(I) 就可以了。

3.5 Cpu0InstrInfo.td

加入了一条链接并跳转指令 jalr 和 jsub,两者的差别是前者是将跳转地址保存到寄存器,后者是直接通过 label 传递。增加CALLSEQ_START和CALLSEQ_END。

CALLSEQ_START: 这个指令标志着函数调用序列的开始。它通常包含有关函数调用的信息,例如调用约定、参数传递方式等。CALLSEQ_START指令的存在可以帮助编译器在生成代码时正确地处理函数调用的相关信息。

CALLSEQ_END: 这个指令标志着函数调用序列的结束。它通常用于指示编译器在函数调用结束时应该执行的清理操作,例如恢复寄存器状态、处理返回值等。CALLSEQ_END指令的存在有助于编译器正确地结束函数调用序列并进行必要的清理工作。

3.6 Cpu0InstrInfo.cpp

将伪指令 ADJCALLSTACKDOWN, ADJCALLSTACKUP 注册到 Cpu0GenInstrInfo 对象中。

3.7 Cpu0MCInstLower.cpp/.h

这里定义了编译器输出的 call 的符号类型,新增了 Cpu0MCExpr::CEK_GOT_CALL。还增加了外部符号 MO_ExternalSymbol 的计算方式,全局符号 MO_GlobalSymbol 的代码已经在之前章节添加。

3.8 Cpu0MachineFunctionInfo.cpp/.h

新增了一些辅助函数和属性,这些函数和属性是继承自 TargetMachineFunction,我们希望在其他代码中调用到这些属性来辅助生成正确的代码。

3.9 Cpu0SEFrameLowering.cpp/.h

实现了一个函数 spillCalleeSavedRegisters(),用来定义 callee saved register 的 spill 动作,里边比较简单,就是遍历所有 callee saved register 并调用 storeRegToStackSlot() 函数将他们存入栈。需要注意 $lr 寄存器如果保存了返回地址,则不需要 spill。

3.10 MCTargetDesc/Cpu0AsmBackend.cpp

新建一个重定位类型 fixup_Cpu0_CALL16

3.11 MCTargetDesc/Cpu0ELFObjectWriter.cpp

新建重定位类型的 Type,ELF::R_CPU0_CALL16。

3.12 MCTargetDesc/Cpu0Fixupkinds.h

还是这个重定位类型的声明。

3.13 MCTargetDesc/Cpu0MCCodeEmitter.cpp

修改 getJumpTargetOpValue(),对于 JSUB 指令,也发射重定位信息。

4. 结果

4.1 普通参数

int gI = 100;

int sum_i(int x1, int x2, int x3, int x4, int x5, int x6)
{
  int sum = gI + x1 + x2 + x3 + x4 + x5 + x6;

  return sum;
}

int main()
{
  int a = sum_i(1, 2, 3, 4, 5, 6);

  return a;
}
sum_i:
...
	addiu	$sp, $sp, -8
	lui	$2, %got_hi(gI)
	addu	$2, $2, $gp
	ld	$2, %got_lo(gI)($2)  # 加载全局变量
	ld	$2, 0($2)
	ld	$3, 8($sp)           # 第一个参数
	addu	$2, $2, $3
	ld	$3, 12($sp)           # 第二个参数
	addu	$2, $2, $3
	ld	$3, 16($sp)           # 第三个参数
	addu	$2, $2, $3
	ld	$3, 20($sp)           # 第四个参数
	addu	$2, $2, $3
	ld	$3, 24($sp)           # 第五个参数
	addu	$2, $2, $3
	ld	$3, 28($sp)           # 最后一个参数
	addu	$2, $2, $3
	st	$2, 4($sp)
	ld	$2, 4($sp)            # 计算结果存入 $2
	addiu	$sp, $sp, 8
	ret	$lr
	nop
...
main:
...
	addiu	$sp, $sp, -40
	st	$lr, 36($sp)         # 调用 main 的返回地址
	addiu	$2, $zero, 0
	st	$2, 32($sp)
	addiu	$2, $zero, 6
	st	$2, 20($sp)
	addiu	$2, $zero, 5
	st	$2, 16($sp)
	addiu	$2, $zero, 4
	st	$2, 12($sp)
	addiu	$2, $zero, 3
	st	$2, 8($sp)
	addiu	$2, $zero, 2
	st	$2, 4($sp)
	addiu	$2, $zero, 1
	st	$2, 0($sp)          # 保存这 6 个实参到栈
	ld	$6, %call16(sum_i)($gp)
	jalr	$6             # 这条指令会更新 $lr 并跳转到 $6 地址处
	nop
	st	$2, 28($sp)        # 从 $2 中取出 sum_i 的计算结果,存入 main 的栈
	ld	$2, 28($sp)        # 再次取出计算结果存入 $2,因为 main 也是将计算结果直接返回
	ld	$lr, 36($sp)                    # 4-byte Folded Reload
	addiu	$sp, $sp, 40
	ret	$lr
	nop
llc -march=cpu0 -mcpu=cpu032I -cpu0-s32-calls=true -relocation-model=pic -filetype=asm ch8_1.ll -o ch8_1.s

第二个要测试的参数是 -relocation-model=static,因为我们对于 PIC 模式和静态模式处理全局符号/外部符号的方式是不一样的。如果我们以静态模式编译得到的汇编中使用jsub代替了ld+jalr,其他都是类似的。

4.2 结构体参数

struct Date {
  int year;
  int month;
  int day;
  int hour;
  int minute;
  int second;
};
static struct Date gDate = {2021, 6, 18, 1, 2, 3};

struct Time {
  int hour;
  int minute;
  int second;
};
static struct Time gTime = {2, 20, 30};

static struct Date getDate() {
  return gDate;
}

static struct Date copyDateByVal(struct Date date) {
  return date;
}

static struct Date* copyDateByAddr(struct Date* date) {
  return date;
}

static struct Time copyTimeByVal(struct Time time) {
  return time;
}

static struct Time* copyTimeByAddr(struct Time* time) {
  return time;
}

int test_func_arg_struct() {
  struct Time time1 = {1, 10, 12};
  struct Date date1 = getDate();
  struct Date date2 = copyDateByVal(date1);
  struct Date *date3 = copyDateByAddr(&date1);
  struct Time time2 = copyTimeByVal(time1);
  struct Time *time3 = copyTimeByAddr(&time1);

  return 0;
}
test_func_arg_struct:
...
	addiu	$9, $sp, 80    # 调用函数中先设定 SRet
	st	$9, 0($sp)
	ld	$2, %got(getDate)($gp)
	ori	$6, $2, %lo(getDate)
	jalr	$6
	nop
...              # @copyDateByVal
copyDateByVal:
...
	addiu	$sp, $sp, -24
	ld	$2, 24($sp)
	ld	$3, 48($sp)
	ld	$4, 44($sp)
	ld	$5, 40($sp)
	ld	$6, 36($sp)
	ld	$7, 32($sp)
	ld	$8, 28($sp)
	st	$8, 0($sp)
	st	$7, 4($sp)
	st	$6, 8($sp)
	st	$5, 12($sp)
	st	$4, 16($sp)
	st	$3, 20($sp)
	ld	$3, 20($sp)
	st	$3, 20($2)
	ld	$3, 16($sp)
	st	$3, 16($2)
	ld	$3, 12($sp)
	st	$3, 12($2)
	ld	$3, 8($sp)
	st	$3, 8($2)
	ld	$3, 4($sp)
	st	$3, 4($2)
	ld	$3, 0($sp)
	st	$3, 0($2)
	addiu	$sp, $sp, 24
	ret	$lr
	nop
...
copyDateByAddr:
...
	ld	$2, 0($sp)
	ret	$lr          # 传指针的方式就非常简单了
	nop

4.3 字符串参数

int main() {
  char str[81] = "Hello world";
  char s[6] = "Hello";

  return 0;
}

我们还可以测试一下字符串初始化代码,因为一般情况下,LLVM 会为字符串初始化生成一条 memcpy 动作,在执行时需要配合 C 库中的 memcpy 完成初始化。不过 LLVM 为我们提供了一条优化,可以在字符串比较短时,使用 ld+st 来替代 call 一个 memcpy。

main:
...
	addiu	$sp, $sp, -120
	st	$lr, 116($sp)                   # 4-byte Folded Spill
	st	$9, 112($sp)                    # 4-byte Folded Spill
	addiu	$9, $zero, 0
	st	$9, 108($sp)
	addiu	$2, $zero, 81
	st	$2, 8($sp)
	ld	$2, %got($__const.main.str)($gp)
	ori	$2, $2, %lo($__const.main.str)
	st	$2, 4($sp)
	addiu	$2, $sp, 24
	st	$2, 0($sp)
	ld	$6, %call16(memcpy)($gp)     # 调用memcpy进行字符串赋值
	jalr	$6
	nop
	ld	$2, %got($__const.main.s)($gp)
	ori	$2, $2, %lo($__const.main.s)   # s较短就可以将memcpy优化掉
	lbu	$3, 5($2)
	lbu	$4, 4($2)
	shl	$4, $4, 8
	or	$3, $4, $3
	sh	$3, 20($sp)
	lbu	$3, 3($2)
	lbu	$4, 2($2)
	shl	$4, $4, 8
	or	$3, $4, $3
	lbu	$4, 1($2)
	lbu	$2, 0($2)
	shl	$2, $2, 8
	or	$2, $2, $4
	shl	$2, $2, 16
	or	$2, $2, $3
	st	$2, 16($sp)
	addu	$2, $zero, $9
	ld	$9, 112($sp)                    # 4-byte Folded Reload
	ld	$lr, 116($sp)                   # 4-byte Folded Reload
	addiu	$sp, $sp, 120
	ret	$lr
	nop

4.4 浮点运算

int tryFloat() {
  float a, b, c;
  c = a + b;
  return (int)c;
}

int tryDouble() {
  double a, b, c;
  c = a + b;
  return (int)c;
}
tryFloat:
...
	addiu	$sp, $sp, -24
	st	$lr, 20($sp)                    # 4-byte Folded Spill
	ld	$2, 16($sp)
	ld	$3, 12($sp)
	st	$3, 4($sp)
	st	$2, 0($sp)
	jsub	__addsf3     # double 类型浮点加法
	nop
	st	$2, 8($sp)
	ld	$2, 8($sp)
	st	$2, 0($sp)
	jsub	__fixsfsi   # double 类型浮点转 int
	nop
	ld	$lr, 20($sp)                    # 4-byte Folded Reload
	addiu	$sp, $sp, 24
	ret	$lr
	nop
...
tryDouble:
...
	addiu	$sp, $sp, -48
	st	$lr, 44($sp)                    # 4-byte Folded Spill
	ld	$2, 32($sp)
	ld	$3, 36($sp)
	ld	$4, 24($sp)
	ld	$5, 28($sp)
	st	$5, 12($sp)
	st	$4, 8($sp)
	st	$3, 4($sp)
	st	$2, 0($sp)
	jsub	__adddf3      # float 类型浮点加法
	nop
	st	$3, 20($sp)
	st	$2, 16($sp)
	ld	$2, 16($sp)
	ld	$3, 20($sp)
	st	$3, 4($sp)
	st	$2, 0($sp)
	jsub	__fixdfsi     # float 类型浮点转 int
	nop
	ld	$lr, 44($sp)                    # 4-byte Folded Reload
	addiu	$sp, $sp, 48
	ret	$lr

这里是编的静态重定位模式,pic模式也是类似的。我们现在还没有支持函数库,所以这里没有办法进一步做链接。

4.5 尾调用优化

int factorial(int x, int total) {
  if (x == 1)
    return total;
  return factorial(x - 1, x * total);
}

int test_tailcall(int a) {
  return factorial(a, 1);
}
clang -target mips-unknown-linux-gnu -O1 -S -emit-llvm ch8_tailcall.c -o ch8_tailcall.ll
llc -march=cpu0 -mcpu=cpu032I -relocation-model=static -filetype=asm -enable-cpu0-tail-calls -stats ch8_tailcall.ll -o ch8_tailcall.s

触发尾调用优化后,会生成 jmp 指令来调用被调用函数,jmp 指令只会跳转,而不会生成 jsub 指令。jmp 跳转到被调用函数后,被调用函数采用循环展开替代递归调用,并在递归结束后,直接返回到调用函数的 $lr,也就是直接返回调用函数应该返回的地方。因为我们还设置了一个统计参数,所以还可以查看打印的统计数据。

当clang使用O2以上优化级别时,我们能够看到递归调用直接被优化成了循环迭代了,这也是尾调用的一个优化方案。

define dso_local i32 @factorial(i32 signext %0, i32 signext %1) local_unnamed_addr #0 {
  %3 = icmp eq i32 %0, 1
  br i1 %3, label %10, label %4

4:                                                ; preds = %2, %4
  %5 = phi i32 [ %8, %4 ], [ %1, %2 ]
  %6 = phi i32 [ %7, %4 ], [ %0, %2 ]
  %7 = add nsw i32 %6, -1
  %8 = mul nsw i32 %5, %6
  %9 = icmp eq i32 %7, 1
  br i1 %9, label %10, label %4

10:                                               ; preds = %4, %2
  %11 = phi i32 [ %1, %2 ], [ %8, %4 ]
  ret i32 %11
}

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

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

相关文章

推荐这两款AI工具,真的很好用

巨日禄 巨日禄是一款由杭州巨日禄科技有限公司开发的AI工具,主要功能是将文本内容转换为视频。该工具通过分析大量的剧本数据和影视作品,为用户提供各种类型的故事情节和角色设置,帮助用户快速找到灵感,减少构思剧本的困难和犹豫。…

服务器通的远程桌面连接不上,服务器通的远程桌面连接不上解决方法

当面临服务器远程桌面连接不上的问题时,专业的处理方式需要遵循一系列步骤来确保问题得到准确且高效的解决。以下是一些建议的解决方法: 一、初步排查与诊断 1. 检查网络连接: - 确保本地计算机与服务器之间的网络连接是稳定的。 - 尝…

鸿蒙开发:【线程模型】

线程模型 线程类型 Stage模型下的线程主要有如下三类: 主线程 执行UI绘制。管理主线程的ArkTS引擎实例,使多个UIAbility组件能够运行在其之上。管理其他线程的ArkTS引擎实例,例如使用TaskPool(任务池)创建任务或取消…

ESP RainMaker®为企业提供AIoT云解决方案,启明云端乐鑫代理商

在AIoT的浪潮中,企业面临着前所未有的机遇与挑战。如何快速响应市场变化,开发出具有竞争力的智能产品?如何确保数据安全,同时实现高效的设备管理?这些问题,ESP RainMaker给出了答案。 ESP RainMaker是一个…

数据价值管理-数据验收标准

前情提要:数据价值管理是指通过一系列管理策略和技术手段,帮助企业把庞大的、无序的、低价值的数据资源转变为高价值密度的数据资产的过程,即数据治理和价值变现。第一讲介绍了业务架构设计的基本逻辑和思路。前面我们讲完了数据资产建设标准…

0604 集成电路运算放大器

6.4.1 集成电路运算放大器CMOS MC14573 6.4.2 集成运算放大器741

Day 19:419. 甲板上的战舰

Leetcode 419. 甲板上的战舰 给你一个大小为 m x n 的矩阵 board 表示甲板,其中,每个单元格可以是一艘战舰 ‘X’ 或者是一个空位 ‘.’ ,返回在甲板 board 上放置的 战舰 的数量。 战舰 只能水平或者垂直放置在 board 上。换句话说&#xff…

UV胶开裂主要因素有哪些?如何避免?

UV胶开裂主要因素有哪些?如何避免? UV胶开裂的原因可能包括多个方面: 固化不足:UV胶的固化需要足够的紫外线照射。如果照射时间不够,或者紫外线光源的强度不足,胶水可能没有完全固化,从而导致开…

Xmind导入纯文本TXT方法

最近有很多同事咨询我如何在xmind直接导入纯文本txt笔记或者思维导图呢? 解决办法如下: 1.先打开xmind随便打开一个思维导图-文件-导出-marldown 2.选中导出的markdown文件。右键-打开方式-苹果系统选择文本编辑,Win系统选择记事本 3.按照图示…

【数据结构】二叉树:一场关于节点与遍历的艺术之旅

专栏引入 哈喽大家好,我是野生的编程萌新,首先感谢大家的观看。数据结构的学习者大多有这样的想法:数据结构很重要,一定要学好,但数据结构比较抽象,有些算法理解起来很困难,学的很累。我想让大家…

初级网络工程师之从入门到入狱(三)

本文是我在学习过程中记录学习的点点滴滴,目的是为了学完之后巩固一下顺便也和大家分享一下,日后忘记了也可以方便快速的复习。 中小型网络系统综合实战实验 前言一、详细拓扑图二、LSW2交换机三、LSW3交换机四、LSW1三层交换机4.1、4.2、4.3、4.4、4.5、…

鸿蒙开发文件管理:【@ohos.statfs (statfs)】

statfs 该模块提供文件系统相关存储信息的功能,向应用程序提供获取文件系统总字节数、空闲字节数的JS接口。 说明: 本模块首批接口从API version 8开始支持。后续版本的新增接口,采用上角标单独标记接口的起始版本。 导入模块 import stat…

2024 全球软件研发技术大会官宣,50+专家共话软件智能新范式!

2024年的全球软件研发技术大会(SDCon)由CSDN和高端IT咨询与教育平台Boolan联合主办,将于7月4日至5日在北京威斯汀酒店举行。本次大会的主题为“大模型驱动软件智能化新范式”,旨在探讨大模型和开源技术的发展如何引领全球软件研发…

GoogleDeepMind联合发布医学领域大语言模型论文技术讲解

Towards Expert-Level Medical Question Answering with Large Language Mod 这是一篇由Google Research和DeepMind合作发表的论文,题为"Towards Expert-Level Medical Question Answering with Large Language Models"。 我先整体介绍下这篇论文的主要内容&#x…

图的存储表示

目录 概述 图的定义 图的存储结构 1)邻接矩阵 2)邻接表 3)十字链表 4)邻接多重表 概述 数据结构分为两类,一类是具有递归结构的数据结构,也就是可以给出一个递归定义的数据结构,一类是非递归结构…

Offline : How to Leverage Diverse Demonstrations in Offline Imitation Learning

ICML 2024 paper code Intro 文章提出一种从混合质量数据中高效抽取有用状态动作数据用于模仿学习。算法基于一种假设,即使当前状态并非属于专家状态,但是若在该状态下采取动作导致下一状态是专家状态,那么该状态相较于随机状态更有价值。 …

AI赋能银行国际结算审单:合合信息抽取技术的实践与应用

官.网地址:合合TextIn - 合合信息旗下OCR云服务产品 时下,银行国际业务是金融体系的重要组成部分,涵盖了外汇交易、国际结算、贸易融资、跨境投资等领域,这些业务对于国际贸易和全球经济发展具有重要作用。国际业务部门单据、凭证…

ffmpeg实现视频播放 ----------- Javacv

什么是Javacv和FFmpeg? Javacv是一个专门为Java开发人员提供的计算机视觉库,它基于FFmpeg和Opencv库,提供了许多用于处理图 像、视频和音频的功能。FFmpeg是一个开源的音视频处理工具集,它提供了用于编码、解码、转换和播放音视频…

LabVIEW RT环境中因字符串拼接导致的系统崩溃问题

在LabVIEW实时操作系统(RT)环境中运行的应用程序出现字符串拼接后死机的问题,通常涉及内存管理、内存泄漏或其他资源管理问题。以下是一些指导和步骤,帮助解决这个问题: 1. 内存泄漏检测 字符串拼接会在内存中创建新…

Android Glide loading Bitmap from RESOURCE_DISK_CACHE slow,cost time≈2 seconds+

Android Glide loading Bitmap from RESOURCE_DISK_CACHE slow,cost time≈2 seconds 加载一张宽高约100px多些的小图,是一张相当小的正常图片,loading Bitmap from RESOURCE_DISK_CACHE竟然耗时达到惊人的3秒左右!(打开Glide调试…