在维基百科( wikipedia )中,给出了如下的别名集的一个很好的解释。
别名分析 是在编译器理论中的一个技术,用于确定一个存储位置是否可能以多个方式访问。两个指针通常被认为别名,如果它们指向同一个位置。 别名分析技术通常分为流 - 敏感( flow-sensitivity )及上下文 - 敏感( context-sensitivity )。它们可以确定可能别名( may-alias )或必须别名( must-alias )的信息。术语别名分析 通常与术语指向分析 互用,一个具体的案例。 别名分析做什么用? 大体上,别名分析确定不同的内存引用是否指向内存的同一个区域。这允许编译器确定在程序中,一个语句将影响哪些变量。例如,考虑下面访问结构体成员的代码: ...; p.foo = 1; q.foo = 2; i = p.foo + 3; ... 这里有 3 个可能的别名情形: 变量 p 及 q 不能是别名。 变量 p 及 q 必定是别名。 在编译时刻,不能确定地决定 p 及 q 是否是别名。 如果 p 及 q 不能是别名,那么 i = p.foo + 3; 可以被改为 i = 4 。如果 p 及 q 必须是别名,那么 i = p.foo + 3; 可以被改为 i = 5 。在这 2 个情形下,通过该别名的了解,我们可以执行优化。另一方面,如果不能知道 p 及 q 是否是别名,那么就不能执行优化,而必须执行完整的代码来得到结果。两个内存引用被称为具有一个可能别名( may-alias )的关系,如果它们的别名使用是未知的。 执行别名分析 在别名分析中,我们把程序的内存分成别名组( alias classes )。别名组是不相接的位置集合,它们彼此间不是别名。为了这里的讨论,假定这里执行的优化发生在程序的一个低级的中间表达方式中。这即是说该程序已经被编译成二元操作( binary operation ),跳转( jump ),寄存器间转移( moves between registers ),寄存器到内存转移( moves from registers to memory ),内存到寄存器转移( moves from memory to registers ),分支( branche ),及函数调用、返回( function calls/returns )。 基于类型的别名分析 如果被编译的语言是类型安全的,编译器的类型检查器是正确的,并且该语言缺少创建引用局部变量的指针的能力,(例如 ML , Haskell 或 Java )那么可以进行某些有用的优化。在有很多情况下,我们知道两个内存位置必须在不同的别名组中: 1. 两个不同类型的变量不能在同一个别名组中,因为强类型,没有内存引用(即,内存位置的引用不能直接改变)语言的一个属性是,不同类型的两个变量不能同时共享同一个内存位置。 2. 相对当前栈框的局部分配,不能与之前其它栈框中的分配,在同一个别名组中。这个情形是因为新的内存分配必须与其它内存分配不相邻。 3. 每个记录( record )类型的每个记录域( record field )有其自己的别名组,大体上,因为类型规则( typing discipline )通常仅允许别名相同类型的记录。因为一个类型的所有记录在内存中将被以相同格式存储,一个域不能是它自身的别名。 4. 类似地,一个给定类型的每个数组有其自己的别名组。 当对代码执行别名分析时,对内存的每次存、取,需要用其组别标记。那么我们具有这个有用的属性,给定具有 i , j 别名组的内存位置 Ai 及 Bj ,如果 i = j ,则 Ai 可能别名( may-alias )于 Bj ,而如果 ,则这些内存位置将不是别名。 基于流的别名分析 基于流的别名分析,不同于基于类型的别名分析,可以被应用于一个具有引用或类型转换( type-casting )的语言的程序中。具有流的分析可以被用于代替或补充基于类型的分析。在基于流的分析中,为每个内存位置,每个地址被使用的全局及局部变量,构建新的别名组。随着时间的推移,引用可能指向多于一个值,并因而属于多个别名组。这意味着每个内存位置具有一组别名组,而不是单个别名组。 |
毫无疑问,在 GNU C++ 中,基于流的分析及基于类型的分析都使用了。正如我们已经看到的,对于消耗内存的项(主要是声明及常量),它们具有关联的 MEM 节点。
在编译器中,赋给 MEM 的别名集帮助后端确定哪个 MEM 是其他 MEM 的别名。大体上,两个具有不同别名集的 MEM 不能互为别名,除了一个重要的例外。
考虑形如: struct S { int i; double d; };
对一个‘ S ’的存储可以是某些具有类型‘ int ’或类型‘ double ’的东西的别名(事实上,在 C++ 中,必须是指针或引用)。(不过,对一个‘ int ’的存储不能是一个‘ double ’的存储的别名,反之亦然)。我们通过一个如下的树结构来表示:
struct S
/ /
/ /
|/_ _/|
int double
(箭头是有方向的,并指向下)。在这个情形下,我们称对应‘ struct S ’的别名集为‘超集’,而那些对应‘ int ’及‘ double ’的别名集为‘子集’。
为了查看两个别名集是否可以指向同一块内存,我们必须检查别名集之一是否是其他别名集的子集。不过,我们不需要跟踪过去的直接后代,因为我们把所有孙辈传播到上一级。
别名集 0 隐含地是所有其他别名集的一个超集。不过,对于别名集 0 ,并没有真正的对应的项。尝试显式地构建 0 的一个子集是一个错误。
换句话说,别名集被用于描述在程序中所访问的内存块的层次及关系。出于这个目的,编译器为别名集定义了结构体 alias_set_entry 。
77 struct alias_set_entry GTY(()) in alias.c
78 {
79 /* The alias set number, as stored in MEM_ALIAS_SET. */
80 HOST_WIDE_INT alias_set;
81
82 /* The children of the alias set. These are not just the immediate
83 children, but, in fact, all descendants. So, if we have:
84
85 struct T { struct S s; float f; }
86
87 continuing our example above, the children here will be all of
88 `int', `double', `float', and `struct S'. */
89 splay_tree GTY((param1_is (int), param2_is (int))) children;
90
91 /* Nonzero if would have a child of zero: this effectively makes this
92 alias set the same as alias set zero. */
93 int has_zero_child;
94 };
95 typedef struct alias_set_entry *alias_set_entry;
其中,在 80 行的域 alias_set ,如果是 0 ,表示关联的 MEM 不在任一别名集中,并且可能是任何对象的别名(事实上,如果使用了别名分析, alias_set 不应该是 0 ,参见下面)。这个 alias_set 也作为别名集的唯一的 id 。在 89 行,域 children 是伸展树的一个指针。这个伸展树是一种二叉树,使用 alias_set 作为排序键值。在这个伸展树的节点中,其 value 域是不使用的。因此看到每个对应别名集的 alias_set_entry 都将保存一个伸展树,如果它别名于其他别名集。并且在树中节点的位置足以描述这个别名关系。
为别名集分配新的 alias_set_entry 由下面的 new_alias_set 完成。如果优化选项高于 -O2 ,设置标记 flag_strict_aliasing ,为之我们应该执行依赖于语言的别名分析。
614 HOST_WIDE_INT
615 new_alias_set (void) in alias.c
616 {
617 if (flag_strict_aliasing )
618 {
619 if (!alias_sets )
620 VARRAY_GENERIC_PTR_INIT (alias_sets , 10, "alias sets");
621 else
622 VARRAY_GROW (alias_sets , last_alias_set + 2);
623 return ++last_alias_set ;
624 }
625 else
626 return 0;
627 }
全局变量 last_alias_set 记录到此为止所分配的 alias_set_entry 的数目,它将被设置在这次分配的 alias_set_entry 的 alias_set 中。
而函数 record_alias_subset ,通过把 subset 插入由 superset 为根节点的树来构建别名树,使得 superset 成为 subset 的超集。
642 void
643 record_alias_subset (HOST_WIDE_INT superset, HOST_WIDE_INT subset) in alias.c
644 {
645 alias_set_entry superset_entry;
646 alias_set_entry subset_entry;
647
648 /* It is possible in complex type situations for both sets to be the same,
649 i n which case we can ignore this operation. */
650 if (superset == subset)
651 return ;
652
653 if (superset == 0)
654 abort ();
655
656 superset_entry = get_alias_set_entry (superset);
657 if (superset_entry == 0)
658 {
659 /* Create an entry for the SUPERSET, so that we have a place to
660 attach the SUBSET. */
661 superset_entry = ggc_alloc (sizeof (struct alias_set_entry));
662 superset_entry->alias_set = superset;
663 superset_entry->children
664 = splay_tree_new_ggc (splay_tree_compare_ints);
665 superset_entry->has_zero_child = 0;
666 VARRAY_GENERIC_PTR (alias_sets , superset) = superset_entry;
667 }
668
669 if (subset == 0)
670 superset_entry->has_zero_child = 1;
671 else
672 {
673 subset_entry = get_alias_set_entry (subset);
674 /* If there is an entry for the subset, enter all of its children
675 (if they are not already present) as children of the SUPERSET. */
676 if (subset_entry)
677 {
678 if (subset_entry->has_zero_child)
679 superset_entry->has_zero_child = 1;
680
681 splay_tree_foreach (subset_entry->children, insert_subset_children,
682 superset_entry->children);
683 }
684
685 /* Enter the SUBSET itself as a child of the SUPERSET. */
686 splay_tree_insert (superset_entry->children,
687 (splay_tree_key) subset, 0);
688 }
689 }
看到构建具有值 0 的超集是不允许的,因为别名集 0 必须被局限在某些别名集中,以表示其对其限制集的全面访问性。没有这个限制,就有可能构建一个以别名集 0 为根的树;这肯定会使编译器崩溃。注意到在 669 行,如果子集是 0 ,将不会构建真正的实体,而仅是提醒这个超集具有值 0 的后代。而 678 行表明这个标记将被传播入父辈中。
上面的标记 has_zero_child 主要用在由下面函数执行的别名集冲突检查。如果指定的两个别名集可能冲突,它返回 1 ,表明关联的对象可能彼此别名。
260 int
261 alias_sets_conflict_p (HOST_WIDE_INT set1, HOST_WIDE_INT set2) in alias.c
262 {
263 alias_set_entry ase;
264
265 /* If have no alias set information for one of the operands, we have
266 to assume it can alias anything. */
267 if (set1 == 0 || set2 == 0
268 /* If the two alias sets are the same, they may alias. */
269 || set1 == set2)
270 return 1;
271
272 /* See if the first alias set is a subset of the second. */
273 ase = get_alias_set_entry (set1);
274 if (ase != 0
275 && (ase->has_zero_child
276 || splay_tree_lookup (ase->children,
277 (splay_tree_key) set2)))
278 return 1;
279
280 /* Now do the same, but with the alias sets reversed. */
281 ase = get_alias_set_entry (set2);
282 if (ase != 0
283 && (ase->has_zero_child
284 || splay_tree_lookup (ase->children,
285 (splay_tree_key) set1)))
286 return 1;
287
288 /* The two alias sets are distinct and neither one is the
289 child of the other. Therefore, they cannot alias. */
290 return 0;
291 }
这个函数应该被仔细使用。参数 set1 及 set2 背后的树对象必须在同一个类型树上(而作为结果 set1 及 set2 ,如果非 0 ,来自别名集的同一棵树)。看到可能别名( may-alias )是一个保守的预测,如果两个对象具有相同的成员布局(因而导致 set1 等于 set2 ),它们被视为可能别名。