数据结构
//#+startup: content
entity : Map/Set/List
bforest : 树
主要模块§
src : clif-util.rs 中有 main() 函数
reader : 读取clif文件(文本格式IR),用来做测试
frontend : 生成IR
interpreter : IR解释器
preopt : 对IR的早期优化.
module : 链接?
codegen : IR -> 可执行机器码
native : 检测host机器的架构.
object : 生成.o文件
不是必要的/暂时可不看的 模块:§
jit : 实验性
umbrella : 无实际代码
serde : IR转JSON
isle : 生成rust代码的DSL (用于后端)
fuzzgen : 生成随机的 Cranelift modules
wasm
frontend§
创建IR形式的函数, 并用指令将其填充.
要先创建 FunctionBuilderContext. 并将其作为一个参数传给 FunctionBuilder
可变变量和IR value
- 声明签名,并指定所使用的调用约定.
- 为sig声明参数和返回值类型.
- 创建一个FunctionBuilderContext 对象
- 使用函数名和签名创建一个Function对象.
- 用函数对象和函数builder上下文创建一个FunctionBuilder
- builder.createblock()创建入口块
EntryIndex§
EntryIndex 的作用是为每个基本块生成一个唯一的标识符,方便在 Cranelift 内部进行引用和操作。可以通过 FunctionBuilder::createblock() 方法创建一个新的基本块,并得到该基本块的 EntryIndex。在生成跳转指令时,可以通过 Switch::setentry() 方法为一个 Switch 表格添加新的条目,其中每个条目都是一个基本块的 EntryIndex 和一个对应的标签(通常是一个整数)组成的元组。
switch.rs§
测试emit()用: setup, 返回一个函数 接收两个参数: 第一个是default, 第二个是一个各个分支的值list
let func : String = setup!(0, [0,]);
macro_rules! setup {
($default:expr, [$($index:expr,)*]) => {{
let mut func = Function::new();
let mut func_ctx = FunctionBuilderContext::new();
{
let mut bx = FunctionBuilder::new(&mut func, &mut func_ctx);
let block = bx.create_block();
bx.switch_to_block(block);// 使得焦点移到block上, 从而能在block中填入指令/对基本块进行其他操作
let val = bx.ins().iconst(types::I8, 0);//创建一条指令, 声明一个i8类型的值,用该值来决定应该跳往switch的哪个分支
let mut switch = Switch::new();
$(
let block = bx.create_block(); //为每个输入list中的索引创建一个对应的空块
switch.set_entry($index, block);//
)*
switch.emit(&mut bx, val, Block::with_number($default).unwrap());
//为函数生成分支代码
}
func
.to_string()
.trim_start_matches("function u0:0() fast {\n")
.trim_end_matches("\n}\n")
.to_string()
// 将函数转为字符串形式, 并去掉头尾.
}};
}
分支:
#[derive(Debug, Default)]
pub struct Switch {
cases: HashMap<EntryIndex, Block>,
}
可见核心逻辑在 Switch.emit() 中. switch结构本身只是用来生成分支指令的脚手架.
先来看emit的签名:
bx代表函数. val代表跳转基于的值. otherwise表示default块(即各个分支都落空后,要前往的块)
pub fn emit(self, bx: &mut FunctionBuilder, val: Value, otherwise: Block)
我们用这个测试用例来研究emit的逻辑:
#[test]
fn switch_many() {
let func = setup!(0, [0, 1, 5, 7, 10, 11, 12,]);
assert_eq!(
func,
" jt0 = jump_table [block1, block2]
jt1 = jump_table [block5, block6, block7]
block0:
v0 = iconst.i8 0
v1 = icmp_imm uge v0, 7 ; v0 = 0
brnz v1, block9
jump block8
block9:
v2 = icmp_imm.i8 uge v0, 10 ; v0 = 0
brnz v2, block10
jump block11
block11:
v3 = icmp_imm.i8 eq v0, 7 ; v0 = 0
brnz v3, block4
jump block0
block8:
v4 = icmp_imm.i8 eq v0, 5 ; v0 = 0
brnz v4, block3
jump block12
block12:
v5 = uextend.i32 v0 ; v0 = 0
br_table v5, block0, jt0
block10:
v6 = iadd_imm.i8 v0, -10 ; v0 = 0
v7 = uextend.i32 v6
br_table v7, block0, jt1"
);
}
首先的几行是判断条件分支中的最大值是否超过了val的type所能表示的范围.若超过了则报错. 这段暂时不用深究.
/// emit()
let max = self.cases.keys().max().copied().unwrap_or(0);
let val_ty = bx.func.dfg.value_type(val);
let val_ty_max = val_ty.bounds(false).1;
if max > val_ty_max {
panic!(
"The index type {} does not fit the maximum switch entry of {}",
val_ty, max
);
}
// ...
然后一行是: 从switch中计算出 ContiguousCaseRange (连续case范围) 的list
gpt:
collectcontiguouscaseranges 方法用于收集所有连续的 case range,并将它们组合成一个 Vec,以便后续处理。
在 buildsearchtree 方法中,首先通过 collectcontiguouscaseranges 方法获取到所有连续的 case range,接着通过递归的方式,将这些连续的 case range 分解成更小的 case range。最终,每个 case range 都被组织成一个搜索树。这个搜索树是一个嵌套的结构,每个节点包含一个 case 和一个子搜索树。
在 buildjumptables 方法中,根据搜索树,生成一个跳转表。跳转表的长度是 case range 的数量,每个 entry 是一个基本块。跳转表可以用于实现跳转语义,比如在 switch 表达式中跳转到对应的基本块。
总体来说,collectcontiguouscaseranges 方法是用于帮助收集连续的 case range,方便后续的处理。通过递归的方式,将搜索树分解成一个个小的 case range,最终生成一个跳转表,实现了 switch 语义。
/// emit()
let contiguous_case_ranges = self.collect_contiguous_case_ranges();
我们抄一个测试来输出它:
macro_rules! gen_switch {
($default:expr, [$($index:expr,)*]) => {{
let mut func = Function::new();
let mut func_ctx = FunctionBuilderContext::new();
let mut bx = FunctionBuilder::new(&mut func, &mut func_ctx);
let block = bx.create_block();
bx.switch_to_block(block);
let val = bx.ins().iconst(types::I8, 0);
let mut switch = Switch::new();
$(
let block = bx.create_block();
switch.set_entry($index, block);
)*
switch
}};
}
#[test]
fn learn_collect_contiguous_case_ranges() {
let switch = gen_switch!(0, [0, 1, 5, 7, 10, 11, 12,]);
eprintln!("{:?}", switch.collect_contiguous_case_ranges());
}
输出如下, 可见此函数就是将连续的case值放在一组,并记录下这组中的最小case值.
[
ContiguousCaseRange {
first_index: 0,
blocks: [
block1, // 0
block2, // 1
],
},
ContiguousCaseRange {
first_index: 5,
blocks: [
block3, // 5
],
},
ContiguousCaseRange {
first_index: 7,
blocks: [
block4, // 7
],
},
ContiguousCaseRange {
first_index: 10,
blocks: [
block5, // 10
block6, // 11
block7, // 12
],
},
]
emit()的最后两行调用了switch的两个函数:
let cases_and_jt_blocks =
Self::build_search_tree(bx, val, otherwise, contiguous_case_ranges);
Self::build_jump_tables(bx, val, otherwise, cases_and_jt_blocks);
/// emit() 结束
build_search_tree ()§
gpt: 在 switch.emit 中,搜索树(search tree)是一个用于优化分支跳转的数据结构。其目的是在多个条件分支中,使用二分查找的方式快速定位到一个具体的分支。
在这里,搜索树被用来优化 switch 语句。switch 语句中的每个 case 都对应着一个基本块,但是如果 case 的数量过多,直接使用分支跳转的方式会带来较大的开销。因此,通过将 case 按照顺序分成多个连续的范围,并将每个范围中的最小值与最大值记录在搜索树的节点上,可以加速查找特定值的 case 所对应的基本块。通过这种方式,即使 switch 语句中的 case 数量很多,查找特定值对应的 case 所对应的基本块的开销也可以降到对数级别。
GPT:
build_search_tree 方法是 SwitchBuilder 中的一个私有方法,其主要作用是构建搜索树。这个搜索树是一个二叉树,其中每个叶子节点对应于一个连续的 case 范围。每个非叶子节点表示对子树中所有 case 范围的分割。
方法的第一个参数是 bx: &mut FunctionBuilder,代表当前函数的生成器,第二个参数是 val: Value,代表 switch 表达式的值,第三个参数是 otherwise: Block,代表 switch 中的 otherwise 分支。方法的最后一个参数是一个 Vec<ContiguousCaseRange>,代表 switch 中所有连续的 case 范围。
在方法的第一行,我们创建了一个空的向量 cases_and_jt_blocks,用于存储最终生成的代码块和 jump table。
如果 contiguous_case_ranges 中的 case 数量小于等于 3,我们直接调用 build_search_branches 方法生成代码。build_search_branches 方法会生成一组比较,以及对应的分支代码,来处理每个 case 范围。
如果 contiguous_case_ranges 中的 case 数量大于 3,我们将 case 范围分成两部分,分别处理左右子树。具体地,我们通过 contiguous_case_ranges.len() / 2 来确定中间点,将左边的 case 范围放到 left 中,将右边的 case 范围放到 right 中。然后我们创建两个新的代码块 left_block 和 right_block,用于表示左子树和右子树。
接着,我们将 right[0].first_index 存储到 first_index 中,该值是右子树中第一个 case 的起始值。然后我们创建一个比较指令 should_take_right_side,用于比较 val 和 first_index 的大小。如果 val 大于或等于 first_index,则跳转到右子树;否则,跳转到左子树。
最后,我们使用 bx.seal_block 将新建的代码块封闭,表示我们已经完成了对该块的所有代码生成操作。最后,我们将左右子树压入堆栈,进行下一轮处理。如果堆栈为空,则所有子树已经处理完毕,我们返回 cases_and_jt_blocks。
该方法主要的目的是将 switch 中的 case 范围拆分成更小的块,以便更高效地生成代码。这个方法通过构建一个二叉树,将 case 范围按照值的范围进行分割,使得在代码生成时可以更快地找到需要跳转到哪个代码块。
先看 build_search_tree, 它以分好组的块作为输入.返回类型为:
-> Vec<(EntryIndex, Block, Vec<Block>)>
这个结构将来用于生成 jttable
当连续case值形成的分组不超过3时,形成的块的数量较少,可以直接生成跳转代码, 用buildsearchbranches来实现此函数.
let mut cases_and_jt_blocks = Vec::new(); //将作为输出结果
// Avoid allocation in the common case
if contiguous_case_ranges.len() <= 3 {
Self::build_search_branches(
bx,
val,
otherwise,
contiguous_case_ranges,
&mut cases_and_jt_blocks,
);
return cases_and_jt_blocks;
}
-
build_search_branches()首先是 声明了flag表明当前是否处在分支位置.
rust let mut was_branch = false;然后是一个辅助函数, 当处于分支位置时就执行,否则什么都不做.
rust let ins_fallthrough_jump = |was_branch: bool, bx: &mut FunctionBuilder| { if was_branch { let block = bx.create_block(); //创建一个新的基本块 bx.ins().jump(block, &[]);//在目前所在分支块中插入一条jump, 用来跳转到新块 bx.seal_block(block); // 为新块 "封口" (已处理好该块所有直接前驱) bx.switch_to_block(block); // 切换到这个新的空块中. } };rust for ContiguousCaseRange { first_index, blocks, } in contiguous_case_ranges.into_iter().rev() //...反向遍历连续case值的分组, 举个例子就是:
rust let switch = gen_switch!(0, [0, 1, 5, 7, 8,]);rust [ ContiguousCaseRange { // 第三轮 first_index: 0, blocks: [ block1, // 0 block2, // 1 ], }, ContiguousCaseRange { // 第二轮 first_index: 5, blocks: [ block3, // 5 ], }, ContiguousCaseRange { // 第一轮 first_index: 7, blocks: [ block4, // 7 block5, // 8 ], }, ]循环内的具体逻辑是根据每个组中的长度以及最小值做判断, 特殊处理长度=1或最小值=0的情况:
rust match (blocks.len(), first_index) { (1, 0) => {/** ... */} (1, _) => {/** ... */} (_, 0) => {/** ... */} (_, _) => {/** ... */} } was_branch = true;当组中只有一个块且case值为0时:
rust (1, 0) => { ins_fallthrough_jump(was_branch, bx); // 若处在分支位置, 则将下面的代码放到新块 bx.ins().brz(val, blocks[0], &[]); // 根据val是否为0来判断是否跳转到这个分支 }当组中只有一个case分支,且其case值不等于零时
rust (1, _) => { ins_fallthrough_jump(was_branch, bx); // 若处在分支位置, 则将下面的代码放到新块 let is_good_val = icmp_imm_u128(bx, IntCC::Equal, val, first_index); // is_good_val = (val == 此分支的case值) bx.ins().brnz(is_good_val, blocks[0], &[]); // brnz为true时跳转 }当组中的首个case值==0,且组中不止一个分支时, 说明该组是switch的最后一部分,执行到这里时应该与之前的各组都不能匹配,而因为val>=0 , 因此最终只能跳入到这一部分.无需再生成jump otherwise.
rust (_, 0) => { // if `first_index` is 0, then `icmp_imm uge val, first_index` is trivially true let jt_block = bx.create_block();//创建一个空块,>= first_index时才跳入它 bx.ins().jump(jt_block, &[]); //在当前块中插入一条jump, 跳到这个空块中 bx.seal_block(jt_block); // 将这个空块封口 cases_and_jt_blocks.push((first_index, jt_block, blocks)); // `jump otherwise` below must not be hit, because the current block has been // filled above. This is the last iteration anyway, as 0 is the smallest // unsigned int, so just return here. return; }当连续case分支不止一个且最小case值不是零时:
rust (_, _) => { ins_fallthrough_jump(was_branch, bx);// 若为分支处, let jt_block = bx.create_block();// 创建空块, >=first_index时才跳入此块 let is_good_val = // val >= first_index ? icmp_imm_u128(bx, IntCC::UnsignedGreaterThanOrEqual, val, first_index); bx.ins().brnz(is_good_val, jt_block, &[]);//结果为true=1时发生跳转 bx.seal_block(jt_block); // 密封新块 cases_and_jt_blocks.push((first_index, jt_block, blocks)); }当for的首次迭代结束后,
was_branch被置为true. 也就是说contiguous_case_ranges剩下的每个组各自开辟一个新块. 并在每个组的块最后插入一个无条件跳转到下个组的jumpswitch (val) { case '0': case '1': break; case '5' : break; case '7': case '8': break; }for循环之后: 生成跳转到default块的指令(和各个case都不匹配)
rust bx.ins().jump(otherwise, &[]);
继续 build_search_tree ()§
当形成的分组>3时,说明最终要形成的块的数量较多,此时直接生成跳转代码的效率太低, 要将比较逻辑用二叉树搜索的方式来进行.
上面这段代码为什么要用到stack+ while循环?
GPT: 这段代码通过使用一个栈结构和 while 循环来构建一个树形结构,其中每个节点都是一个基本块(block),该块的值是在 switch 表达式中被测试的值。在每个节点,根据该节点的连续 case 索引范围,会有一些分支指令用于测试测试值是否在该范围内。如果连续 case 索引范围小于或等于 3,则直接发出这些测试指令。否则,将节点拆分为两个子节点,并在中间插入一个条件分支,该分支将 switch 表达式值与分裂点的索引进行比较,从而确定应选择哪个子节点进行下一步分裂。 这个 while 循环实际上是在模拟一个深度优先搜索过程。在每一次迭代中,从栈顶弹出当前节点的信息,如果连续 case 索引范围小于等于 3,则在当前块中生成对应的分支指令,否则根据范围拆分当前节点,并将生成的子节点信息压入栈顶,这样就可以迭代生成整个搜索树。最终生成的 casesandjtblocks 向量包含了每个块的入口编号,以及每个 case 索引范围所对应的基本块和跳转表,这些信息将用于后续的跳转表生成过程。
先创建一个待处理stack,将 contiguous_case_ranges 不断切分后放入.
let mut stack: Vec<(Option<Block>, Vec<ContiguousCaseRange>)> = Vec::new();
stack.push((None, contiguous_case_ranges));
下面是循环处理stack中的元素:
while let Some((block, contiguous_case_ranges)) = stack.pop() {
// ...
}
若取到元素的key不是空的, 则将焦点移到这个块上继续生成代码.
if let Some(block) = block {
bx.switch_to_block(block);
}
下面对取出来的 contiguous_case_ranges 按照元素个是分别进行处理.
不超过3时和上面的处理一样, 用 buildsearchbranches 生成代码
if contiguous_case_ranges.len() <= 3 {
Self::build_search_branches(
bx,
val,
otherwise,
contiguous_case_ranges,
&mut cases_and_jt_blocks,
);
}
当这个list太长时,顺序地为每个连续case值形成的组生成指令是低效的.(相当于线性查找) 因此要对这个list做切分,并且还能正确地跳转到这两部分.
// ...
else {
let split_point = contiguous_case_ranges.len() / 2;
let mut left = contiguous_case_ranges;
let right = left.split_off(split_point); //从split_point处截断.
/// 生成两个辅助块,作为左半部分,右半部分生成指令的起始块:
let left_block = bx.create_block();
let right_block = bx.create_block();
/// 选择跳往 左半部分/右半部分 ?
let first_index = right[0].first_index; /// 右半部分第一个连续case组的最小case值
let should_take_right_side = /// val是否 >= 这个值
icmp_imm_u128(bx, IntCC::UnsignedGreaterThanOrEqual, val, first_index);
bx.ins().brnz(should_take_right_side, right_block, &[]); /// 若是则跳往右半部分
bx.ins().jump(left_block, &[]); /// 否则直接跳往左半部分
/// 负责标记: 两个辅助块的前驱块(也就是当前块)的代码已经生成完毕
bx.seal_block(left_block);
bx.seal_block(right_block);
/// 将切分后的两个list加入到待生成指令的工作队列中,待之后的循环为其生成指令.
stack.push((Some(left_block), left));
stack.push((Some(right_block), right));
}
build_search_tree 的输出是用于产生jttable的信息.
所谓jttable, 在生成ir的开头处声明:
jt0 = jump_table [block2, block3]
用法为通过brtable指令:
br_table 值, 落空块, jt_table
值就是jttable的索引,取值为 0,1,2,3, … 例如值为1时,跳转到jt0的第二个目标 block3 中. 当值无法对应jttable中的元素时,则跳转到落空块中(default分支).
build_jump_tables()§
Self::build_jump_tables(bx, val, otherwise, cases_and_jt_blocks);
jt_table 用于生成 跳转到连续case值所对应的块
此函数的输入: 就是为了生成jttable准备的. 第二个Block是生成指令的起始块.
cases_and_jt_blocks: Vec<(EntryIndex, Block, Vec<Block>)>,
此函数的主体是一个for循环, 反向取出元素 (firstindex从小到大)
for (first_index, jt_block, blocks) in cases_and_jt_blocks.into_iter().rev() {
/// ...
}
首先创建并初始化jumptable. 并将焦点移到此连续case值所对应的块
let mut jt_data = JumpTableData::new();
for block in blocks {
jt_data.push_entry(block);
}
let jump_table = bx.create_jump_table(jt_data);
bx.switch_to_block(jt_block);
然后要将val和各个分支的连续case值比较转化为 jumptable的索引.
例如: val=12, 一个case值形成的组是 [10,11,12,13]
为了生成正确的brtable指令,要将 val-10 作为该指令的操作数,即:
br_table [u32值](val - first_index), otherwise块, jump_table
但这个 firstindex 的类型是 u128, 不能直接转为 Value所表示的值
-
TODO (val - firstindex)的类型转换???
最后, 生成 brtable 指令:
rust bx.ins().br_table(discr, otherwise, jump_table);brtable() 是一个控制流指令,用于实现基于索引的跳转表。 它有两个操作数:一个是作为索引的值,另一个是指向一系列块的表。指令会根据索引跳转到表中的相应块。
rust let selector = …; // a value to use as an index into the jump table let table = [block0, block1, block2, block3, block4]; let default_block = …; let jump_dest = builder.br_table(selector, &table, default_block);在上面的示例中,如果 selector 是 0,那么 jumpdest 将指向 block0。如果 selector 是 4,那么 jumpdest 将指向 block4。如果 selector 不在 0..=4 的范围内,则 jumpdest 将指向 defaultblock。
ssa.rs§
#[test]
fn program_with_loop()
block0:
x = 1;
y = 2;
z = x + y;
jump block1
block1:
z = z + y;
brnz y, block3;
jump block2;
block2:
z = z - x;
return y
block3:
y = y - x
jump block1
实际生成的ssa
function u0:0() fast {
block0:
v0 = iconst.i32 1
v6 -> v0
v1 = iconst.i32 2
v2 = iadd v0, v1 ; v0 = 1, v1 = 2
jump block1(v2, v1) ; v1 = 2
block1(v3: i32, v4: i32):
v5 = iadd v3, v4
brnz v4, block3
jump block2
block2:
v7 = isub.i32 v5, v6 ; v6 = 1
return v4
block3:
v8 = isub.i32 v4, v6 ; v6 = 1
jump block1(v5, v8)
}
如何构建一个Function呢? 首先不必多说, Function::new()
创建出一个对象func. 函数在IR中被表示为控制流图, 因此要有基本块.
这可以通过func.dfg数据流图来创建出一个块: func.dfg.make_block().
并使用FuncCursor将其插入到函数布局中:
cur.insert_block(block0)
块由一条条指令ins构成, 每条指令是一个表达式. 指令的添加也需要借助
FuncCursor 来完成,
添加完成一个表达式后,返回一个代表表达式结果的值value.
let value = {
let mut cur = FuncCursor::new(&mut func).at_bottom(block_curr);
cur.ins().iadd(x_value,y_value)
}
let x_ssa_value = cur.ins().iconst(I32,233);
而这种方式显然是没有包含变量定义的, 因此当使用变量时也无法从block中获得信息.
因此需要构建SSA, 一种记录了变量定义的IR. ssa的构建要借助SSABuilder. 使用ssabuilder首先要声明当前所操作的块:
ssa.declare_block(block0);
当需要记录变量定义时,
- 创建一个"变量Variable":
Variable::new(N) - 将插入函数基本块中的指令所返回的"值Value"记录下来, 并用
def_var()进行变量定义
// 表示在块0中有定义: x_var <- x_ssa_value
ssa.def_var(x_var, x_ssa_value, block0);
当遇到变量的使用时: 用 def_var() 从当前块出发向前寻找变量的值
let x_value = ssa.use_var(&mut func,x_var, I32, block0).0
当移动到下一个块时, 除了创建新块,并将它记录到函数布局中,以及在ssa中声明后,还需要在ssas中声明它的前驱块:
ssa.declare_block_predecessor(block1,pred_block,jump_ins);
其中的指令是前驱块中的跳转指令.
当一个块的所有前驱块全都被构建完成后,要将该块进行"密封":
ssa.seal_block(block1,&mut func);
或者也可以在最后, 等到所有的block都创建完成后, 将所有的块密封:
ssa.seal_all_blocks(&mut func)
struct Function§
pub struct Function {
pub name: UserFuncName, // 函数名, 主要是用于.clif文件
pub stencil: FunctionStencil, // 函数模板, 核心部分
pub params: FunctionParameters,
}
函数模板 : 数据和布局分离!
pub struct FunctionStencil {
/// ...
/// Data flow graph containing the primary definition of all instructions, blocks and values.
pub dfg: DataFlowGraph, //真正包含block的数据
/// Layout of blocks and instructions in the function body.
pub layout: Layout, // <<<==!!! 记录函数中块的顺序, 以及指令顺序.
/// ...
}
pub struct Layout {
/// Linked list nodes for the layout order of blocks Forms a doubly linked list, terminated in
/// both ends by `None`.
blocks: SecondaryMap<Block, BlockNode>,
/// Linked list nodes for the layout order of instructions. Forms a double linked list per block,
/// terminated in both ends by `None`.
insts: SecondaryMap<Inst, InstNode>,
/// First block in the layout order, or `None` when no blocks have been laid out.
first_block: Option<Block>,
/// Last block in the layout order, or `None` when no blocks have been laid out.
last_block: Option<Block>,
}
SSABuilder: 实施ssa构造算法所需要的中间信息§
pub struct SSABuilder {
// TODO: Consider a sparse representation rather than SecondaryMap-of-SecondaryMap.
/// Records for every variable and for every relevant block, the last definition of
/// the variable in the block.
variables: SecondaryMap<Variable, SecondaryMap<Block, PackedOption<Value>>>,
//记录了每个块中的变量定义
/// Records the position of the basic blocks and the list of values used but not defined in the
/// block.
ssa_blocks: SecondaryMap<Block, SSABlockData>, // ssa算法中所需的块的信息
/// Call stack for use in the `use_var`/`predecessors_lookup` state machine.
calls: Vec<Call>,
/// Result stack for use in the `use_var`/`predecessors_lookup` state machine.
results: Vec<Value>,
/// Side effects accumulated in the `use_var`/`predecessors_lookup` state machine.
side_effects: SideEffects,
/// Reused allocation for blocks we've already visited in the
/// `can_optimize_var_lookup` method.
visited: HashSet<Block>, // loop?
}
struct SSABlockData {
// The predecessors of the Block with the block and branch instruction. // 前驱
predecessors: PredBlockSmallVec,
// A block is sealed if all of its predecessors have been declared.如果一个块的所有前任都已声明,则该块将被密封
sealed: bool,
// List of current Block arguments for which an earlier def has not been found yet.
undef_variables: Vec<(Variable, Value)>,
}
继续 program_with_loop()§
let mut func = Function::new();
let mut ssa = SSABuilder::new();
let block0 = func.dfg.make_block();
let block1 = func.dfg.make_block();
let block2 = func.dfg.make_block();
let block3 = func.dfg.make_block();
真正创建构成了函数的4个block.
-
declare_block创建在ssa构建中所需的块信息的结构:
``` rust ssa.declare_block(blockN)
pub fn declare_block(&mut self, block: Block) { self.ssa_blocks[block] = SSABlockData { predecessors: PredBlockSmallVec::new(), /// 前驱 sealed: false, /// 是否seal undef_variables: Vec::new(), /// 尚未能找到定义的变量 }; }
``` -
block0:
block0:
rust // block0 ssa.declare_block(block0); ssa.seal_block(block0, &mut func); // 没有前驱, 立即seal let x_var = Variable::new(0); let x1 = { let mut cur = FuncCursor::new(&mut func).at_bottom(block0); cur.ins().iconst(I32, 1) //为函数在block0中生成指令 x = 1 }; ssa.def_var(x_var, x1, block0); // 在ssa中记录 x 的定义rust let y_var = Variable::new(1); let y1 = { let mut cur = FuncCursor::new(&mut func).at_bottom(block0); cur.ins().iconst(I32, 2) }; ssa.def_var(y_var, y1, block0);调用usevar从当前块开始寻找变量的定义
rust let z_var = Variable::new(2); let x2 = ssa.use_var(&mut func, x_var, I32, block0).0; let y2 = ssa.use_var(&mut func, y_var, I32, block0).0; let z1 = { let mut cur = FuncCursor::new(&mut func).at_bottom(block0); cur.ins().iadd(x2, y2) // 生成 z = x+y 指令 }; ssa.def_var(z_var, z1, block0); // 在ssa中记录z在此块中的定义对应于
jump block1:rust let jump_block0_block1 = { let mut cur = FuncCursor::new(&mut func).at_bottom(block0); cur.ins().jump(block1, &[]) }; -
block1:
asm block1: z = z + y; brnz y, block3; jump block2;首先为ssa构建创建关于block1的信息的结构, 然后声明从block0可以通过上面的jump指令跳转到block1.
rust ssa.declare_block(block1); ssa.declare_block_predecessor(block1, block0, jump_block0_block1);因为 block3也会跳到block1,因此它目前还不能标记为sealed.
然后处理块中指令: 先利用ssa找出使用的变量的值, 然后为函数生成代码,最后再用ssa记录变量的定义.
rust let z2 = ssa.use_var(&mut func, z_var, I32, block1).0; let y3 = ssa.use_var(&mut func, y_var, I32, block1).0; let z3 = { let mut cur = FuncCursor::new(&mut func).at_bottom(block1); cur.ins().iadd(z2, y3) }; ssa.def_var(z_var, z3, block1);接下来是brnz:
rust brnz y, block3;若y不为0 ,则跳到block3 :
``` rust let y4 = ssa.use_var(&mut func, y_var, I32, block1).0;
let brnz_block1_block3 = { let mut cur = FuncCursor::new(&mut func).at_bottom(block1); cur.ins().brnz(y4, block3, &[]) }; ```
然后是
jump block2:rust let jump_block1_block2 = { let mut cur = FuncCursor::new(&mut func).at_bottom(block1); cur.ins().jump(block2, &[]) }; -
block2
紧接着是
asm block2: z = z - x; return y仍然是 declareblock + declareblockpredecessor
rust ssa.declare_block(block2); ssa.declare_block_predecessor(block2, block1, jump_block1_block2);并且,它的唯一前驱就是block1,因此可以被sealed.
rust ssa.seal_block(block2, &mut func);翻译
z = z - x``` rust let z4 = ssa.use_var(&mut func, z_var, I32, block2).0; /// z
let x3 = ssa.use_var(&mut func, x_var, I32, block2).0; /// x let z5 = { let mut cur = FuncCursor::new(&mut func).at_bottom(block2); cur.ins().isub(z4, x3) /// z - x }; ssa.def_var(z_var, z5, block2); /// z = ... ```
生成
return y``` rust let y5 = ssa.use_var(&mut func, y_var, I32, block2).0;
{ let mut cur = FuncCursor::new(&mut func).at_bottom(block2); cur.ins().return_(&[y5]) }; ```
-
block3
下面处理block3, 因为在block1中有
brnz y block3因此block1是其前驱.并且是其唯一前驱.``` rust ssa.declare_block(block3); ssa.declare_block_predecessor(block3, block1, brnz_block1_block3);
ssa.seal_block(block3, &mut func); ```
asm block3: y = y - x jump block1``` rust /// y = y - x let y6 = ssa.use_var(&mut func, y_var, I32, block3).0;
let x4 = ssa.use_var(&mut func, x_var, I32, block3).0;
let y7 = { let mut cur = FuncCursor::new(&mut func).at_bottom(block3); cur.ins().isub(y6, x4) }; ssa.def_var(y_var, y7, block3); ```
rust /// jump block1 let jump_block3_block1 = { let mut cur = FuncCursor::new(&mut func).at_bottom(block3); cur.ins().jump(block1, &[]) }; -
seal block1
现在block1的所有前驱已经处理完了,
rust ssa.declare_block_predecessor(block1, block3, jump_block3_block1); ssa.seal_block(block1, &mut func);接下来我们研究最后一个
seal_block究竟做了什么?首先 对于block1, 它是编号为1的Block结构.
此函数返回一个副作用结构.
rust pub fn seal_block(&mut self, block: Block, func: &mut Function) -> SideEffects { self.seal_one_block(block, func); mem::replace(&mut self.side_effects, SideEffects::new()) }-
seal_one_block()-
对当前块中每个未定义变量执行
predecessors_lookup()进入到:
rust fn seal_one_block(&mut self, block: Block, func: &mut Function) { let block_data = &mut self.ssa_blocks[block]; /// ...block1的blockdata:
``` rust predecessors: PredBlockSmallVec,/// block0, block3
sealed: bool, /// 值为假
undef_variables: Vec<(Variable, Value)>, /// z(2)=3, y(1)=4, x(0)=6, ```
继续:
``` rust /// ... // Extract the undef_variables data from the block so that we // can iterate over it without borrowing the whole builder. let undef_vars = mem::replace(&mut block_data.undef_variables, Vec::new());
// For each undef var we look up values in the predecessors and create a block parameter // only if necessary. for (var, val) in undef_vars { let ty = func.dfg.value_type(val); self.predecessors_lookup(func, val, var, ty, block); } self.mark_block_sealed(block); } ```
z(2) = 3 -
predecessors_lookup()rust self.predecessors_lookup(func, val, var, ty, block);其实现为:
rust self.begin_predecessors_lookup(value=3, block1 ); self.run_state_machine(func, z(2) , ty)-
begin_predecessors_lookup()将块的前驱压入调用栈rust fn begin_predecessors_lookup(&mut self, sentinel: Value, dest_block: Block) { self.calls .push(Call::FinishPredecessorsLookup(sentinel=3, dest_block=block1)); // Iterate over the predecessors. let mut calls = mem::replace(&mut self.calls, Vec::new()); calls.extend( self.predecessors(dest_block) .iter() .rev() .map(|&PredBlock { block: pred, .. }| Call::UseVar(pred)), ); self.calls = calls; }self.calls:Call::UseVar(block0) Call::UseVar(block3) Call::FinishPredecessorsLookup(val=3,block1) -
run_state_machine()开始-
UseVar(): 在当前块中寻找,找不到则为前驱安排查找任务
准备好任务栈后, 正式由下面的方法来执行. 此时入参var 表示变量z(2)
rust fn run_state_machine(&mut self, func: &mut Function, var: Variable, ty: Type) -> Value核心逻辑是用while不断从calls栈中弹出任务:
rust while let Some(call) = self.calls.pop() { Call::UseVar(ssa_block) => { ///... }, Call::FinishSealedOnePredecessor(ssa_block) => { ///... }, Call::FinishPredecessorsLookup(sentinel, dest_block) => { ///... }, }按照之前画出的调用栈:
首先取出的元素是
Call::UseVar(block0), 先尝试从当前块block0中找到z的定义,找到终止此次循环.否则尝试用use_var_nonlocal从前驱中继续寻找.rust Call::UseVar(block0) => { if let Some(var_defs) = self.variables.get(变量z) { if let Some(val) = var_defs[block0].expand() { self.results.push(val); continue; } } self.use_var_nonlocal(func, var, ty, block0); }这里能在block0中找到z的定义, 因此提前跳出.
下一次从calls中取出的是
Call::UseVar(block3), 这次不能在block3本身中找到z的定义,因此要借助use_var_nonlocal向calls中压入新任务, 用来到block3的前驱中寻找z的定义. -
use_var_nonlocal()将前驱作为任务压入调用栈rust fn use_var_nonlocal(&mut self, func: &mut Function, var: Variable, ty: Type, block: Block) /// var == z /// block == block3此函数被特意的分成两段,主要是为了不触犯借用规则. 第一部分返回一个值作为flag来指导第二部分的走向.
-
Part1
case指示了当前块能否继续向回查找(seal?) 或是此块是否有多个前驱
``` rust let optimize_var_lookup = self.can_optimize_var_lookup(block); let data = &mut self.ssa_blocks[block];
let case = if data.sealed { /// 此节点的所有前驱是否已经构造完成? // Optimize the common case of one predecessor: no param needed. if optimize_var_lookup { /// 此节点只有一个前驱,且该前驱不处于环中 UseVarCases::SealedOnePredecessor(data.predecessors[0].block) } else { /// 多个前驱 // Break potential cycles by eagerly adding an operandless param. let val = func.dfg.append_block_param(block, ty); UseVarCases::SealedMultiplePredecessors(val, block) } } else { /// 仍有前驱尚未构造完成,此变量的定义不能查找 let val = func.dfg.append_block_param(block, ty); data.undef_variables.push((var, val)); // 将这个块中尚不能找到定义的 UseVarCases::Unsealed(val) }; ```
-
can_optimize_var_lookup(): 只有一个前驱,且前驱不在环中``` rust /// 返回true: block只能有一个前驱,且它不处在一个环中. /// 用visited: HashSet
实现有无环检测 fn can_optimize_var_lookup(&mut self, block: Block) -> bool { // Check that the initial block only has one predecessor. This is only a requirement // for the first block. if self.predecessors(block).len() != 1 { return false; }
self.visited.clear(); /// 置为空 let mut current = block;
loop { let predecessors = self.predecessors(current);
// We haven't found the original block and we have either reached the entry // block, or we found the end of this line of dead blocks, either way we are // safe to optimize this line of lookups. if predecessors.len() == 0 { return true; } // We can stop the search here, the algorithm can handle these cases, even if they are // in an undefined island. if predecessors.len() > 1 { return true; } let next_current = predecessors[0].block; if !self.visited.insert(current) { /// 将当前节点插入,若返回false,则代表重复插入. return false; } current = next_current;} } ```
-
append_block_param(block, ty) -
Part2
-
-
-
-
-