abc:可扩展的AspectJ编译器
1. 运行时工厂扩展
为了给合适的连接点创建实现新
CastSignature
接口的对象,我们对运行时工厂进行了子类化,添加了相应方法。
AbcExtension
类中有一个方法,用于指定
thisJoinPointStaticPart
对象的工厂运行时类,该方法被重写,以使用新工厂创建运行时对象:
public String runtimeSJPFactoryClass()
{
return “org.aspectbench.eaj.runtime.reflect.EajFactory”;
}
2. 代码测量
为了让读者评估实现每个新特性所需的工作量,我们总结了一些统计数据,如下表所示:
| 类型 | 文件数 | 代码行数 |
| — | — | — |
| 解析 | 1 | 74 |
| 私有切入点变量 - AST节点 | 2 | 130 |
| 私有切入点变量 - 传递 | 0 | 0 |
| 私有切入点变量 - 织入器 | 0 | 0 |
| 私有切入点变量 - 运行时 | 0 | 0 |
| 全局切入点声明 - AST节点 | 4 | 64 |
| 全局切入点声明 - 传递 | 1 | 77 |
| 全局切入点声明 - 织入器 | 0 | 0 |
| 全局切入点声明 - 运行时 | 0 | 0 |
| 强制类型转换切入点 - AST节点 | 2 | 46 |
| 强制类型转换切入点 - 传递 | 0 | 0 |
| 强制类型转换切入点 - 织入器 | 2 | 94 |
| 强制类型转换切入点 - 运行时 | 2 | 27 |
| 抛出切入点 - AST节点 | 2 | 46 |
| 抛出切入点 - 传递 | 0 | 0 |
| 抛出切入点 - 织入器 | 2 | 91 |
| 抛出切入点 - 运行时 | 2 | 16 |
| 扩展信息和共享类 | 7 | 205 |
| 总计 | 27 | 870 |
从这些数据可以看出,不同特性的实现工作量差异明显,这表明模块化目标已达成。而且,所有扩展都无需修改基础编译器的代码,它们是清晰分离的插件模块。
3. 与ajc的详细比较
3.1 组件分离
3.1.1 前端和后端代码行数对比
| 编译器 | 前端 | 后端 | 总计 |
|---|---|---|---|
| ajc | 10,197 | 23,938 | 34,135 |
| abc | 16,444 | 17,397 | 33,841 |
乍一看,ajc的前端比abc小很多,但这是以对其依赖的Java编译器源文件进行大量修改为代价的,而这些修改未在上述数据中体现。另外,abc使用Polyglot,它鼓励使用许多小类,并且需要大量的访问者和工厂样板代码。abc后端较小,这是因为使用了干净的中间表示Jimple和Soot框架中的丰富分析工具。
3.1.2 与基础编译器的分离
- ajc :ajc基于Eclipse Java编译器,该编译器为速度而设计,避免使用Java集合类,采用低级数据结构和整数常量调度。但ajc需要自己的Eclipse编译器源树副本,并进行了大量修改,包括44个Java文件和至少119个源位置的显式更改,还修改了语法。这些更改存在复杂依赖,导致将ajc与最新版本的Eclipse编译器合并很困难。
- abc :abc基于Polyglot,无需对其源文件进行任何更改。Polyglot设计为可扩展,abc只是它的一个扩展。通过引入新的环境类型和类型系统来处理作用域规则的更改,这些都是对Polyglot相应类的简单扩展。Polyglot允许通过子类化进行更改,因此升级到新版本时无需额外工作。此外,abc提供了干净的LALR(1)语法,实现了Java语法和AspectJ扩展语法的清晰分离。
3.1.3 与字节码操作的分离
- ajc :ajc使用BCEL库进行字节码操作和代码生成,但需要维护一个特殊版本的BCEL,该版本最初通过约300行的补丁文件与BCEL发行版同步,现在作为ajc的一部分独立开发,修改后的BCEL有23,259行代码。
- abc :abc与Soot转换和代码生成框架完全分离,无需对Soot进行任何更改。
综上所述,abc是第一个实现与构建组件清晰分离的AspectJ编译器,其思想有望应用于为其他编程语言添加面向方面特性。
3.2 编译时间
为了评估使用方面对编译时间的影响,我们比较了四种不同的AspectJ编译器:普通ajc、ajc加上Soot优化(ajc + soot)、关闭所有优化的abc(abc -O0)和使用默认过程内优化的abc(abc)。以下是六个基准测试的编译时间:
| 基准测试 | SLOC | APPS | ajc | ajc + Soot | abc - O0 | abc | javac |
| — | — | — | — | — | — | — | — |
| bean | 124 | 4 | 1.77 | 4.00 | 3.30 | 3.59 | - |
| bean - java | 104 | 0 | 1.43 | 3.21 | 3.05 | 3.03 | 0.54 |
| sim - nullptr | 1474 | 138 | 2.96 | 12.00 | 10.38 | 10.69 | - |
| sim - nullptr - java | 1547 | 0 | 1.75 | 6.52 | 7.45 | 8.64 | 0.76 |
| figure | 94 | 12 | 1.62 | 3.43 | 2.95 | 3.07 | - |
| figure - java | 98 | 0 | 1.25 | 2.83 | 2.63 | 2.65 | 0.51 |
| LoD - sim | 1586 | 1332 | 4.10 | 29.87 | 36.47 | 46.14 | - |
| dcm | 1668 | 359 | 3.37 | 17.07 | 14.74 | 17.43 | - |
| tetris | 1043 | 29 | 2.88 | 8.42 | 8.40 | 8.93 | - |
从这些数据可以看出,abc的编译时间明显比ajc长,这是因为abc的代码没有针对编译时间进行优化,而ajc的设计目标之一就是短编译时间。不过,abc的编译时间反映了其强大的优化框架的成本,与ajc + soot相比,abc的编译时间相当,这是令人鼓舞的。而且,像abc这样的研究型编译器能够处理大型示例,适合用于对日常使用ajc开发的程序进行优化构建。
3.3 织入Jimple(abc)与织入字节码(ajc)
我们通过一个简单的例子来说明abc织入三地址Jimple表示与ajc直接织入字节码的优势。在Java代码中,在调用
bar
方法之前织入一段通知,以下是织入前后的代码:
// (a) 基础Java代码
public int f(int x, int y, int z)
{
return bar(x, y, z);
}
// (b) 直接织入字节码(ajc)
public int f(int x, int y, int z)
0: aload_0
1: iload_1
2: iload_2
3: iload_3
4: istore %4
6: istore %5
8: istore %6
10: astore %7
12: invokestatic A.aspectOf ()LA;
15: aload %7
17: invokevirtual A.ajc$before$A$124 (LFoo;)V
20: aload %7
22: iload %6
24: iload %5
26: iload %4
28: invokevirtual Foo.bar (III)I
31: ireturn
// (c) 织入Jimple(abc)
public int f(int, int, int)
{
Foo this;
int x, y, z, $i0;
A theAspect;
this := @this;
x := @parameter0;
y := @parameter1;
z := @parameter2;
theAspect = A.aspectOf();
theAspect.before$0(this);
$i0 = this.bar(x, y, z);
return $i0;
}
// (d) 从Jimple生成的字节码(abc)
public int f(int x, int y, int z)
0: invokestatic A.aspectOf ()LA;
3: aload_0
4: invokevirtual A.before$0 (LFoo;)V
7: aload_0
8: iload_1
9: iload_2
10: iload_3
11: invokevirtual Foo.bar (III)I
14: ireturn
ajc织入字节码时,除了实现查找方面和调用通知体的字节码外,还需要生成大量的栈修复代码来修复隐式字节码计算栈。而abc织入Jimple时,Jimple不使用隐式计算栈,所有值都用显式变量表示。织入时,abc只需声明一个Jimple变量,插入查找方面和调用通知的两行代码,无需额外的栈修复代码。从Jimple生成的字节码比ajc生成的字节码更小,使用的局部变量也更少,对编织代码的性能有积极影响。
3.4 使用Soot优化进行织入
abc使用Soot作为后端,能够利用Soot现有的优化过程来改进生成的代码。这不仅简化了织入器的设计,还能实现一些在直接织入时难以或无法应用的面向方面的优化。例如,AspectJ在通知体中提供了特殊变量
thisJoinPoint
,它包含连接点的各种反射信息,构建成本较高。abc和ajc都实现了该变量的“懒”初始化,即只有在通知体真正需要时才构建,且在一个连接点应用多个通知时,该变量只构建一次。
-
ajc
:当连接点有环绕通知时,懒初始化实现可能不起作用,并且在只有一个通知时会进行特殊处理以避免不必要的懒加载。
-
abc
:在所有情况下都使用懒初始化,并通过后续的空值分析消除大多数情况下的懒加载开销。该分析是标准的Java分析,并结合了AspectJ运行时库方法构建
thisJoinPoint
对象永远不会返回空值的额外信息,因此实现比ajc更简单、更健壮。
3.5 目标代码性能
虽然本文未对ajc和abc生成代码的效率进行详细比较,但在之前的工作中,我们对面向方面程序的动态行为进行了详细研究。通过特制的测量工具,我们证实了在许多AspectJ程序中,方面引入的开销可以忽略不计,但也发现了一些开销较高的常见情况。因此,abc的一个明确目标是能够试验新的面向方面的优化。
3.5.1 环绕通知优化
abc对环绕通知进行了改进,在一些基准测试中实现了六倍的加速。在某些情况下,ajc为了实现
proceed
会生成闭包,这会占用大量堆空间,导致显著的开销。而abc在几乎所有情况下都能避免闭包的构建。当ajc不生成闭包时,会进行大量内联,可能导致代码膨胀。abc的编译策略在代码大小和速度之间取得了平衡,如下表所示:
| 基准测试 | abc - 时间(s) | ajc - 时间(s) | abc - 大小(指令) | ajc - 大小(指令) |
| — | — | — | — | — |
| sim - nullptr | 21.9 | 21.4 | 7893 | 10186 |
| sim - nullptr - rec | 23.6 | 124.0 | 8216 | 10724 |
| weka - nullptr | 19.0 | 16.0 | 103,018 | 134,290 |
| weka - nullptr - rec | 18.9 | 45.5 | 103,401 | 130,483 |
| ants - delayed | 17.5 | 18.2 | 3688 | 3785 |
| ants - profiler | 22.5 | 21.2 | 7202 | 13401 |
3.5.2 cflow优化
ajc 1.2中
cflow
的实现使用了昂贵的栈操作,而使用简单计数器就足够了。此外,它在单个过程体中多次检索相同的线程局部状态,并且在多个相同
cflow
切入点的出现之间不共享工作。abc消除了这些问题,与ajc 1.2相比,这些小优化在
LoD - sim
基准测试中实现了182倍的改进。一些简单的优化(计数器和共享)已被纳入ajc 1.2.1。abc还提供了重织入技术,利用现有的纯Java分析,通过构建动态调用图的静态近似,在编译时确定每个影子是否在给定切入点的
cflow
中,从而完全消除
cflow
的成本。
整体来看,abc在扩展性、优化和组件分离方面具有明显优势,虽然编译时间较长,但能为程序带来更好的性能优化,适合用于对程序进行优化构建。
3.5.2 cflow优化(续)
在之前的ajc 1.2版本中,
cflow
的实现存在诸多问题。下面是对这些问题及abc解决方案的详细分析:
ajc 1.2中
cflow
实现的问题
-
昂贵的栈操作
:ajc 1.2在处理
cflow时,采用了复杂的栈操作。实际上,很多情况下使用简单的计数器就可以满足需求,栈操作不仅增加了代码的复杂度,还带来了额外的性能开销。 - 多次检索线程局部状态 :在单个过程体中,ajc 1.2会多次检索相同的线程局部状态,这无疑增加了不必要的开销。
-
缺乏工作共享
:对于多个相同
cflow切入点的出现,ajc 1.2没有进行工作共享,导致重复的计算和资源浪费。
abc的解决方案
-
小优化改进
:abc通过一系列小优化,消除了上述ajc 1.2中存在的问题。例如,使用计数器替代栈操作,避免多次检索线程局部状态,并实现了多个切入点之间的工作共享。这些优化在
LoD - sim基准测试中取得了显著的效果,实现了182倍的性能提升。 -
重织入技术
:abc还提供了重织入技术,利用现有的纯Java分析能力。其核心思想是构建动态调用图的静态近似,这样在编译时就可以确定每个影子是否在给定切入点的
cflow中,从而完全消除cflow的成本。
以下是不同编译器在不同优化配置下的
cflow
性能对比表格:
| 编译器 | 无优化 | 计数器 | 计数器 + 共享 | 计数器 + 共享 + 重用 | 计数器 + 共享 + 重用 + 过程间优化 |
| — | — | — | — | — | — |
| abc | 1072.2 | 238.3 | 90.3 | 20.3 | 1.96 |
| ajc 1.2 | 450.5 | 167.7 | - | - | - |
| ajc 1.2.1 | - | - | - | - | - |
| 基准测试 - figure | | | | | |
| abc | 122.3 | 75.1 | 27.9 | 27.4 | 27.3 |
| ajc 1.2 | 123.5 | 28.9 | - | - | - |
| ajc 1.2.1 | - | - | - | - | - |
| 基准测试 - quicksort | | | | | |
| abc | 29.0 | 29.1 | 22.8 | 22.5 | 20.4 |
| ajc 1.2 | 29.7 | 24.2 | - | - | - |
| ajc 1.2.1 | - | - | - | - | - |
| 基准测试 - sablecc | | | | | |
| abc | 18.7 | 18.8 | 18.7 | 17.9 | 13.1 |
| ajc 1.2 | 33.0 | 32.9 | - | - | - |
| ajc 1.2.1 | - | - | - | - | - |
| 基准测试 - ants | | | | | |
| abc | 1723.9 | 46.6 | 32.8 | 26.2 | 23.7 |
| ajc 1.2 | 4776.2 | 35.3 | - | - | - |
| ajc 1.2.1 | - | - | - | - | - |
| 基准测试 - LoD - sim | | | | | |
| abc | 1348.7 | 142.5 | 91.9 | 75.2 | 66.3 |
| ajc 1.2 | 2349.2 | 113.5 | - | - | - |
| ajc 1.2.1 | - | - | - | - | - |
| 基准测试 - LoD - weka | | | | | |
| abc | 592.8 | 80.1 | 41.2 | 27.4 | 23.1 |
| ajc 1.2 | 1107.4 | 56.0 | - | - | - |
| ajc 1.2.1 | - | - | - | - | - |
| 基准测试 - Cona - stack | | | | | |
| abc | 75.8 | 75.3 | 73.8 | 72.0 | 73.6 |
| ajc 1.2 | 76.8 | 69.0 | - | - | - |
| ajc 1.2.1 | - | - | - | - | - |
| 基准测试 - Cona - sim | | | | | |
从这个表格中可以清晰地看到,abc在不同的优化配置下,性能都有显著的提升,尤其是在使用了更高级的优化策略后,与ajc相比优势更加明显。
4. 总结
4.1 abc的优势
- 组件分离 :abc是第一个实现与构建组件清晰分离的AspectJ编译器。与ajc相比,abc无需对基础编译器(Polyglot)和字节码操作框架(Soot)进行修改,避免了复杂的依赖和合并问题,具有更好的可扩展性和维护性。
-
优化能力
:abc拥有强大的优化框架,通过使用Soot的优化过程,能够实现面向方面的优化。例如,在环绕通知和
cflow方面的优化,显著提高了程序的性能。虽然编译时间相对较长,但与ajc + soot相比,编译时间相当,并且能够处理大型示例,适合用于对日常使用ajc开发的程序进行优化构建。 - 代码生成 :abc织入Jimple表示的方式比ajc直接织入字节码更具优势。Jimple不使用隐式计算栈,织入时无需额外的栈修复代码,生成的字节码更小,使用的局部变量更少,对编织代码的性能有积极影响。
4.2 性能对比
| 对比项 | ajc | abc |
|---|---|---|
| 编译时间 | 短,设计目标之一是快速编译 | 较长,但反映了强大的优化框架成本,与ajc + soot相当 |
| 组件分离 | 对基础编译器和字节码操作库有大量修改,合并困难 | 与基础编译器和字节码操作框架完全分离,易于升级和维护 |
| 代码生成 | 直接织入字节码,可能需要大量栈修复代码,生成的代码较大 | 织入Jimple表示,生成的字节码更小,使用局部变量更少 |
| 优化能力 |
缺乏abc的一些优化能力,如环绕通知和
cflow
优化
| 具有强大的优化能力,能显著提高程序性能 |
4.3 应用建议
对于日常开发,由于ajc编译时间短,适合快速迭代和开发。而对于需要进行优化构建的程序,abc是一个很好的选择。其强大的优化能力可以在不牺牲太多编译时间的情况下,显著提高程序的性能。
5. 流程图:abc编译流程
graph LR
A[源代码] --> B[Polyglot解析]
B --> C[生成Jimple表示]
C --> D[Soot优化]
D --> E[织入通知]
E --> F[生成目标代码]
这个流程图展示了abc的编译流程,从源代码开始,经过Polyglot解析生成Jimple表示,然后利用Soot进行优化,接着织入通知,最后生成目标代码。整个流程体现了abc的组件分离和优化能力。
6. 总结表格:abc与ajc对比
| 特性 | ajc | abc |
|---|---|---|
| 基础编译器依赖 | 基于Eclipse Java编译器,需大量修改 | 基于Polyglot,无需修改 |
| 字节码操作库 | 使用特殊版本的BCEL,需维护 | 与Soot完全分离,无需修改 |
| 编译时间 | 短 | 长,但与ajc + soot相当 |
| 代码生成 | 直接织入字节码,有栈修复代码 | 织入Jimple,生成字节码更小 |
| 优化能力 | 部分优化,存在不足 | 强大的面向方面优化 |
| 组件分离 | 依赖紧密,合并困难 | 清晰分离,易于升级 |
通过这个表格,可以更加直观地对比abc和ajc在各个方面的差异,从而根据具体需求选择合适的编译器。
综上所述,abc作为一个可扩展的AspectJ编译器,在组件分离、优化能力和代码生成等方面具有明显的优势,虽然编译时间较长,但在优化构建方面表现出色,为面向方面编程提供了一个优秀的选择。
超级会员免费看
45

被折叠的 条评论
为什么被折叠?



