JVM C1 编译优化:合并相同的表达式-Global Value Numbering 之实现

1. 原因

为了合并相同的运算,避免重复计算,通常在编译过程中,编译器会尝试合并相同的计算。

C1在初始的时候内部会构建图结构的HIR,它由基本块BB构成一个控制流图,每个基本块里面是SSA形式的指令。

单个BB块中通过ValueNumbering 来实现,多个BB块里实现的合并叫做Global Value Numbering ,但其算法的本质是一致的。

2. 值编号 Value Numbering

值编号(Value numbering):是指为每个SSA的instruction计算一个Hash值,然后遍历instruction寻找相同的instruction进行合并优化。而Global Value Numbering就是查找多个BB块中合并相同的值编号的instruction,基于C1如何生成的HIR的BB块在这篇博客里就不介绍了。C1 优化器会做一次BB块内部的Value Numbering的优化,然后在做一次跨BB块的Global Value Numbering的优化。

2.1 SSA的Instruction

首先我们先给一下BB块以及SSA的表达式Instruction的例子,能更直观的理解:

B0 (SV) [0, 14] -> B1 sux: B1 pred: B5
empty stack
inlining depth 0
__bci__use__tid____instr____________________________________
. 1    0    i8     a5._12 (I) t
  4    0    i9     8
  6    0    i10    i8 + i9
  9    0    i11    18
  11   0    i12    i6 + i11
. 14   0     13    goto B1

我们可以看到BB块的编号,以及前驱以及后继BB块,同时执行一个如下语句:

int adder = c.t+8;

需要3句Instructions,三元地址表达式

__bci__use__tid____instr____________________________________
. 1    0    i8     a5._12 (I) t
  4    0    i9     8
  6    0    i10    i8 + i9

2.2 计算Hash值

如何计算i10的Hash呢?i10是一个ArithmeticOp

LEAF(ArithmeticOp, Op2)
 public:
  // creation
  ArithmeticOp(Bytecodes::Code op, Value x, Value y, bool is_strictfp, ValueStack* state_before)
  : Op2(x->type()->meet(y->type()), op, x, y, state_before)
  {
    set_flag(IsStrictfpFlag, is_strictfp);
    if (can_trap()) pin();
  }
 
  // accessors
  bool        is_strictfp() const                { return check_flag(IsStrictfpFlag); }
 
  // generic
  virtual bool is_commutative() const;
  virtual bool can_trap() const;
  HASHING3(Op2, true, op(), x()->subst(), y()->subst())
};

可以看到主要是计算value x的subst和 value y 的subst, 也就是这里给的i8, i9, 什么是subst ?就是指向的值,比如i8里的a5.12 i9里的8

i10的hash的值就是计算i8指向的a5.12以及i9的常量8

#define HASH1(x1            )                    ((intx)(x1))
#define HASH2(x1, x2        )                    ((HASH1(x1        ) << 7) ^ HASH1(x2))
#define HASH3(x1, x2, x3    )                    ((HASH2(x1, x2    ) << 7) ^ HASH1(x3))
#define HASH4(x1, x2, x3, x4)                    ((HASH3(x1, x2, x3) << 7) ^ HASH1(x4))

2.3 ValueMapArray

image

Hash值的计算并不具备唯一性,Hash相同无法保证表达式的一只,Hash的算法只是为了更快速的查找到相同的Hash值的Instr,在JVM里构建了ValueMapEntry 来保存一个Instr的 Hash ,以及value,同时也保存了指向下一个entry的指针。

JVM同时构建了ValueMapArray的数组,通过Hash%size找到数组里对应的首个ValueMapEntry,通过遍历ValueMapEntry的链表结构,匹配相同的Hash值,然后在进行Value的比较,我们以前面的ArithmeticOp例子为例:

  virtual bool is_equal(Value v) const {              \
    if (!(enabled)  ) return false;                   \
    class_name* _v = v->as_##class_name();            \
    if (_v == NULL  ) return false;                   \
    if (f1 != _v->f1) return false;                   \
    if (f2 != _v->f2) return false;                   \
    if (f3 != _v->f3) return false;                   \
    return true;                                      \
  }  

比较都是ArithmeticOp里的value x的subst和 value y 的subst是否完全相同。

如果完全相同,将会把当前的Instr里的subst替换成相同的Instr。在不同的情况下,插入当前的Instr在ValueMapArray中。

当然并不是所有的Instr都需要进行Value Number编号,比如:if, goto, return 这些的Hash值都会被计算为0,0的Hash值是不会被保存到ValueMapArray中。

2.4 CFG 控制流分析

Global Value Numbering 作为一个全局(函数)分析,和Value Numbering的区别主要在跨BB块的分析,基于控制流分析,在不同的BB块的流转。先来看一段代码:

        int adder = c.t+8;
        while (num<100){
            num = c.t+8;
            if(num>10)
            c.t=10;
        }
       
        return num+c.t+8;

把这段代码转为CFG

image

我们需要对每一个参数进行数据流分析,从而找到相同的表达式。在我们的例子里:我们不能把第一行的c.t+8 和第3行的num=c.t+8进行合并成 num=adder,主要原因是因为在5行里c.t 从新赋值了,这是我们对语意的理解。

编译优化对静态分析是要求准确,需要进行May Analyze,遍历所有存在的路径,但我们不会对一条分支进行判断以确定是否会真实的存在该条路径,例如第5行if(num<10)这条分支的num进行值域求解分而获取判断结果是否有可能走到该分支,而是直接使用在CFG存在这条路径就可以了,在某些优化后可能存在块合并的情况下,这也是合并后在进行GVN分析。

2.5 数据流分析的transfor function

在数据流分析中,通常会提到transfor function,常见的Out[B]= gen U(In - Kill)

2.5.1 Kill 场景

什么是Kill?在做了某些操作后,比如改变某个变量的值,我们需要让关于这个变量的前后表达式无法合并,这样就需要Kill这个变量。

我们以2.4的代码进行举例,因为num= c.t + 8 ,也就是c.t+8 和int addr=c.t+8 是一样的表达式,我们应该考虑合并。但是在第5行里c.t=10,进行了c.t 的赋值,这是很明显不能合并表达式的。

这里代表了一种场景,当一个字段被storeField过,就需要Kill,而 Kill的逻辑如下

a. 将该字段c.t标示成被kill状态

b. 尝试去移除和c.t相关的ValueMapEntry

如何表示该字段被kill,这里和常见的数据流分析一样,使用bitMap的每一个bit位表示每一个参数:

image

我们会发现SSA IR有个天然的好处,直接使用参数的下标id就可以了。如果没有用SSA IR表示,需要对每一个表达式进行行编号。

为何要从ValueMapEntryArray里移除和c.t相关的ValueMapEntry?毕竟已经在BitMap里表示了这个值被kill, 这个应该是从效率来考虑。要减少ValueMapEntryArray里的值,毕竟当Hash值相同的情况下,还是有需要链表访问里面的值,在这里还加了一个额外的逻辑,只会删除在同一Nest的ValueMapEntry, 可以简单的认为同一个BB块在同一层Nest。

有的人可能会很奇怪既然删除了ValueMapEntry,何必还要在BitMap里依然要标示,其原因就是因为在进行数据流分析的时候流的走向很重要,有可能存在删除了ValueMapEntry后有可能后面的BB块又访问了这个Field,为了避免复杂的分析,最安全的就是这时候是不在添加。

我们可以罗列一下哪些场景是需要被kill的:

1. Store field 2. Store Array 3. Monitor, 4. 操作unsafe 5.Intrinsic 6.Load field

大家可能会有点奇怪为何LoadField 是需要kill的呢?其实场景主要涉及volatile的语意,以及还有获取静态的字段的时候类有可能没有初始化成功

2.5.2 流分析

JVM C1 的HIR是双向链表,如下图:

image

在B2块里前向块是B6,B4, 后向块B1 ,在数据流分析中通常采用前向分析或者后向分析,在GVN的完整分析过程中,分析BB块中需要边分析边进行相同表达式合并。我们如果简单的考虑前向分析,后续的BB块中可能存在修改的场景并影响前面的BB块,这样的合并会存在问题,这种场景比较常见于循环。Natural loops 自然循环

  1. 在正常情况我们正向分析,依次对当前的block进行优化
  2. 当碰到循环头的时候,我们需要使用后向分析分析完完整的循环后,然后在进行当前的block优化

为何要分析完整的循环,也就是前面提到的后续的BB块可能存在修改的场景,我们需要遍历完所有的循环去执行Kill 的transfor function。

为何又要后向分析?我们来看这种场景:

image

红色的这个点,如果前向分析你会发现红色的这个点如果前向分析, 会把非循环的绿色的点加入到分析的点中。如何避免这样的问题发生,我们需要把绿色的点继续前向分析,以确定能回到蓝色的点。如果绿色的点链路很长,这会大大导致时间变长。同时我需要遍历所有的节点的每条路径以确保是在环里的。

使用后向分析:

image

我们会发现绿色的点是不会在加到循环的分析过程中。在循环过程中使用后向分析,能很轻易的把非环的节点排除掉,同时会在碰到分析的环的节点超过ValueMapMaxLoopSize默认是8的时候不在分析循环,以避免分析较大的循环而耗费过多的时间。

分析循环只是为了提高精度,主要是对field 以及数组的修改, 只是kill所对应的值

void do_StoreField     (StoreField*      x) {
    if (x->is_init_point() ||  // putstatic is an initialization point so treat it as a wide kill
        // This is actually too strict and the JMM doesn't require
        // this in all cases (e.g. load a; volatile store b; load a)
        // but possible future optimizations might require this.
        x->field()->is_volatile()) {
      kill_memory();
    } else {
      kill_field(x->field(), x->needs_patching());
    }
  }
  void do_StoreIndexed   (StoreIndexed*    x) { kill_array(x->type()); 

其它的部分场景下(如下)及避免复杂循环分析下,会进行最大颗粒度的kill_memory

  void do_MonitorEnter   (MonitorEnter*    x) { kill_memory(); }
  void do_MonitorExit    (MonitorExit*     x) { kill_memory(); }
  void do_Invoke         (Invoke*          x) { kill_memory(); }
  void do_UnsafePutRaw   (UnsafePutRaw*    x) { kill_memory(); }
  void do_UnsafePutObject(UnsafePutObject* x) { kill_memory(); }
  void do_UnsafeGetAndSetObject(UnsafeGetAndSetObject* x) { kill_memory(); }
  void do_Intrinsic      (Intrinsic*       x) { if (!x->preserves_state()) kill_memory(); }

大颗粒度的Kill_Memory ,只要是load field, load array 就直接kill,也就是前面的逻辑:删除ValueMapEntry,设置BitMap的bit位

#define MUST_KILL_MEMORY(must_kill, entry, value)                                        \
  bool must_kill = value->as_LoadField() != NULL || value->as_LoadIndexed() != NULL;

2.5.3 替换相同的表达式

在遍历BB块的时候,如果发现该表达式的值与前面的值相同的时候,就使用前序BB的值来替代现在的值。

. 27   0    i18    a4._12 (I) t
  30   0    i19    i11 + i18
  31   0    i20    8
  33   0    i21    i19 + i20
. 34   0    i22    ireturn i21

将被替换成

  30   1    i19    i11 + i7
  33   1    i21    i19 + i8
. 34   0    i22    ireturn i21

用i7替换成i18, i8代替i20

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容

  • 编译过程 不论是物理机还是虚拟机,大部分的程序代码从开始编译到最终转化成物理机的目标代码或虚拟机能执行的指令集之前...
    三也视界阅读 934评论 0 5
  • 常量折叠 常量折叠是Java在编译期做的一个优化,简单的来说,在编译期就把一些表达式计算好,不需要在运行时进行计算...
    三也视界阅读 1,364评论 0 1
  • 语法分析树[https://www.dazhuanlan.com/2019/12/27/5e058c86c53ff...
    xsser阅读 701评论 0 2
  • 以Android7.0 源码树为例,边翻源码边看ART在把dex编译为机器码的时候,做了哪些IR优化。 除去对特定...
    HAPPYers阅读 870评论 0 0
  • 今天感恩节哎,感谢一直在我身边的亲朋好友。感恩相遇!感恩不离不弃。 中午开了第一次的党会,身份的转变要...
    迷月闪星情阅读 10,551评论 0 11