写点C2中memory subgraph中涉及的反依赖问题。没有提纲,心意所至,笔向所指。
有这样一段java代码...
static class Foo {
public int a = 2;
public int b = 4;
}
Foo test(int x) {
Foo f1 = new Foo();
Foo f2 = new Foo();
f1.a = 42; // LINE9
f2.a = f1.a; // LINE10
f1.a = 24; // LILNE11
return f1;
}
它对应的ir局部如下,这里我们只关心line9-11:
C2稍微做了些优化,将line10变成了f2.a=42,三条赋值分配对应188#StoreI,192#StoreI和195#StoreI,它们依次执行,和代码顺序一致,这里有个问题?C2如何构造IR的时候如何知道192#StoreI的in(Memory)是188#StoreI,答案就是通过alias type。
简单来说,alias type用来描述memory slice信息,它可以不同粒度的描述memory slice。比如下面的例子,f.a和f2.a访问的memory slice的alias type一样(Reduced\(Foo+12),f.b和f2.b同理alias type一样(Reduced\)Foo+16)。
int test() {
// @ <2> +any in BotPTR *+bot
// @ <3> +0 in rawptr:BotPTR
// @ <4> +0 in java/lang/Object *
// @ <5> RO +8 in java/lang/Object+8 * [narrowklass]
// @ <6> +12 in Reduced$Foo+12 *
// @ <7> +16 in Reduced$Foo+16 *
// @ <8> RO +172 in klass java/lang/Object: 0x00007fff54006cb8+172 *
Foo f = new Foo();
Foo f2 = new Foo();
f.a =12;
f.b = 23;
f2.a = 6;
f2.b = 7;
return f.a+f2.b;
}
回到例子中,由于三个赋值访问的都是Foo#a字段,所以它们相关。甚至,我们可以把上面的赋值改成:
f1.a=42; f2.a=25;
生成的StoreI也会关联起来
上面的情况中,赋值语句的rhs都是常量,但是很多情况下rhs也可以是一个读内存值,这个时候我们需要一个LoadX节点,它的输入是一个内存地址,得到的是内存地址里面的值,行为和解引用一样。下面的java代码:
static int i=2,j=3,k=9;
int test(int x) {
j = i; // 30#StoreI
i = k; // 35#StoreI
return i ; // at this point, i==9,j==2
}
35#StoreI相当于i=k,30#StoreI相当于j=i,具体点27#LoadI是读i的值,33#LoadI是读k的值。执行流程如下:
- 执行27#LoadI,读取i的值,相当于tmp=i 。此时tmp==2
- 执行35#StoreI,读取k的值,然后赋值给i所在内存。此时i == 9
- 执行30#StoreI,将27#LoadI读取到的i的值赋值给j,相当于j=tmp。此时j==2
- 最终
i==9,j==2
。
上面的IR有个比较严重的问题:27#LoadI(读i值)和35#StoreI(给i赋值)访问的memory slice的alias type是相同的,但是两者没有依赖关系的。想象一下,假如先执行35#StoreI,后执行27#LoadI会发生什么?
- 执行35#StoreI,读取k的值,然后赋值给i所在内存。此时i == 9
- 执行27#LoadI,读取i的值,相当于tmp=i 。此时tmp==9
- 执行30#StoreI,将27#LoadI读取到的i的值赋值给j,相当于j=tmp。此时j==9
- 最终
i==9,j==9
35#StoreI和27#LoadI没有依赖关系,不同的执行顺序可能导致不同的结果。
要解决这个问题,我们需要插入反依赖(PhaseCFG::insert_anti_dependences),这一步是在GCM阶段完成,GCM之后的IR如下图
可以看到8#storeI多了一条12#loadI的输入。当执行8#storeI的时候强制要求读取k和i的值,保证了12#lloadI先于8#storeI执行。这条从12#loadI流出到8#storeI的表就叫做反依赖,注意反依赖说的是load依赖store,主体是load。C2在处理反依赖问题的时候,除了插入反依赖的边之外,还有一种解决方案,它也可能将load节点所在的LCA block强制上移,使得load节点可以放置的early block-LCA block均处于store节点之上,用这样的方式来保证store不会先于load执行。