`
RednaxelaFX
  • 浏览: 3009368 次
  • 性别: Icon_minigender_1
  • 来自: 海外
社区版块
存档分类
最新评论

Adobe的ActionScript 3编译器对strict mode中的类型标注的诠释

阅读更多
(那个……我没读过中文版的ActionScript 3文档,不知道标准文档里翻译用了哪些词。凑合吧)

使用过ActionScript 3(AS3)的人应该会注意到其中的可选类型标注。在声明变量或常量时,可以有选择的在变量名之后加上冒号和类型名,像这样:
var myvar : Number;

在编译的时候,可以选择使用标准模式(standard mode)和严格模式(strict mode)来编译,默认为标准模式。
ECMAScript 4(ES4)接受了AS3的这个设计,也提出相同的类型标注和两种编译模式。
在ES4的文档wiki,Strict and Standard compilation modes中特别提到,严格模式并不改变程序的语义,而只是对变量进行静态的类型检查,以杜绝一些常见的类型相关错误,例如对未声明的变量赋值,或者调用函数是给的参数的个数不正确等。
引用
In order to specify the two modes’ runtime behavior consistently, we specify the dynamic semantics of ECMAScript in a single, unified, dynamically typed language. In other words, the choice of strict or standard mode does not affect the meaning of a legal program; it only affects the definition of “legality”.
...

...Strict mode is only meant as a way for programmers to indicate their desire for more extensive and conservative error analysis. All programs, regardless of mode, must execute as though they were performing all dynamic checks.

换句话说,AS3/ES4中的类型标注并不意味着更高的执行效率;在纯动态的解释器实现中,严格模式可能反而更慢(因为要做更多的动态检查来保证类型的正确性)。

Adobe在许多产品里都附带有AS3的编译器。我使用的是与Flex 3 SDK一起发布的,asc.jar,在Flex3SDK/lib目录下能找到。
直接用asc.jar来编译*.as源文件时,默认得到的是*.abc(Adobe ByteCode)文件,也就是对应AVM2(Adobe Virtual Machine 2,Flash 9/Flex 2/Flex 3/AIR所使用的虚拟机)的字节码。

在AVM2的指令集中,算术运算相关的指令都有专门针对整型的版本。例如add指令有相应的add_i版本。不过单是add指令本身的语义就很复杂了:
ActionScript Virtual Machine 2 (AVM2) Overview 写道
add
Operation
Add two values.
Format
add
Forms
add = 160 (0xa0)
Stack
…, value1, value2 => …, value3
Description
Pop value1 and value2 off of the stack and add them together as specified in ECMA-262 section 11.6 and as extended in ECMA-357 section 11.4. The algorithm is briefly described below.
1. If value1 and value2 are both Numbers, then set value3 to the result of adding the two number values. See ECMA-262 section 11.6.3 for a description of adding number values.
2. If value1 or value2 is a String or a Date, convert both values to String using the ToString algorithm described in ECMA-262 section 9.8. Concatenate the string value of value2 to the string value of value1 and set value3 to the new concatenated String.
3. If value1 and value2 are both of type XML or XMLList, construct a new XMLList object, then call [[Append]](value1), and then [[Append]](value2). Set value3 to the new XMLList.
See ECMA-357 section 9.2.1.6 for a description of the [[Append]] method.
4. If none of the above apply, convert value1 and value2 to primitives. This is done by calling ToPrimitive with no hint. This results in value1_primitive and value2_primitive. If value1_primitive or value2_primitive is a String then convert both to Strings using the ToString algorithm (ECMA-262 section 9.8), concatenate the results, and set value3 to the concatenated String. Otherwise convert both to Numbers using the ToNumber algorithm (ECMA-262 section 9.3), add the results, and set value3 to the result of the addition.
Push value3 onto the stack.
Notes
For more information, see ECMA-262 section 11.6 (“Additive Operators”) and ECMA-357 section 11.4.

而add_i指令的语义相对简单些:
ActionScript Virtual Machine 2 (AVM2) Overview 写道
add_i
Operation
Add two integer values.
Format
add_i
Forms
add_i = 197 (0xc5)
Stack
…, value1, value2 => …, value3
Description
Pop value1 and value2 off of the stack and convert them to int values using the ToInt32 algorithm (ECMA-262 section 9.5). Add the two int values and push the result onto the stack.

注意到,add_i指令并没有要求其操作数(栈顶的两个对象)是int型,而特别提到该指令的语义是用ToInt32算法将这两个对象转换到int。虽然如此,在将两个int型变量相加时,add_i执行的效率也应该比add指令好一些吧。

但实际上Adobe提供的AS3编译器会为我们编译出怎样的代码呢?让我们来看看下面的AS3代码,以严格模式编译会怎样:
(在命令行用java -jar asc.jar -import builtin.abc -import toplevel.abc -m -strict test.as编译)
test.as:
package {
    
    class TestClass {
        function foo(x : int, y : int) : int {
            return x + y;
        }
        
        function goo(x, y) {
            return foo(x, y);
        }
        
        function hoo(x, y : String) {
            return x + y;
        }
        
        function ioo(x : int, y : int) {
            var i = x + y;
            return i;
        }
        
        function joo(x : int, y : int) {
            var i : int = x + y;
            return i;
        }
    }
    
    var c = new TestClass();
    var i = c.goo(1, "2");
    print(i); // prints: 3
}

这里,我定义了几个方法,内容本质上都一样,都是使用+运算符把两个操作数“加”起来。但是其中foo()有完整的类型标注,包括参数和返回类型都标注上了;goo()完全没有类型标注;hoo()只有第二个参数做了类型标注;ioo()没有标注返回类型,局部变量也没有标注类型;joo()在ioo()的基础上标注了局部变量的类型。

编译时使用了-m参数,得到编译结果的文字表示test.il。其内容太长,就不完整贴出来了。下面的中间代码都出自test.il。

于是让我们把这几个方法分析一下。
首先是foo()。编译器为其生成的方法信息是:
MethodInfo  param_count=2 return_type=1 param_types={ 1 1 } debug_name_index=2 needs_arguments=false need_rest=false needs_activation=false has_optional=false ignore_rest=false native=false has_param_names =false -> 1

稍微解释一下。根据ABC(Adobe ByteCode)文件的格式定义,用于描述方法的signature的method_info数据结构如下:
method_info
{
    u30 param_count
    u30 return_type
    u30 param_type[param_count]
    u30 name
    u8 flags
    option_info options
    param_info param_names
}

其中return_type与param_type里都是对multiname数组的索引。如果为0则意味着类型是“*”,也就是AS3的严格模式中的任意类型。
也就是说,编译器正确的识别出了foo()的两个参数类型都是int,返回类型也是int。
foo()的方法体部分编译结果如下:
MethodBody max_stack=2 max_locals=3 scope_depth=4 max_scope=5 code_length=6 traits_count=0 -> 1

// ++StartMethod foo$0

LoadThis
      0:Getlocal0 [1]

PushScope
      1:Pushscope [0]

LoadRegister 1, int
      2:Getlocal1 [1]

LoadRegister 2, int
      3:Getlocal2 [2]

InvokeBinary BinaryPlusOp
      4:Add [1]

Return
      5:Returnvalue [0]

// --FinishMethod foo$0 TestClass/foo

可以看到,编译器在知道两个参数都是int型的前提下,仍然为运算符“+”选择了add而不是add_i指令。

另外几个方法编译出来的到的method_info分别为:
goo():
MethodInfo  param_count=2 return_type=0 param_types={ 0 0 } debug_name_index=2 needs_arguments=false need_rest=false needs_activation=false has_optional=false ignore_rest=false native=false has_param_names =false -> 2

hoo():
MethodInfo  param_count=2 return_type=0 param_types={ 0 3 } debug_name_index=2 needs_arguments=false need_rest=false needs_activation=false has_optional=false ignore_rest=false native=false has_param_names =false -> 3

ioo():
MethodInfo  param_count=2 return_type=0 param_types={ 1 1 } debug_name_index=2 needs_arguments=false need_rest=false needs_activation=false has_optional=false ignore_rest=false native=false has_param_names =false -> 4

joo():
MethodInfo  param_count=2 return_type=0 param_types={ 1 1 } debug_name_index=2 needs_arguments=false need_rest=false needs_activation=false has_optional=false ignore_rest=false native=false has_param_names =false -> 5

可以看到,编译器生成的method_info中关于类型的描述完全取决于代码中的标注;没有被标注的方法和变量一律被认为是“*”也就是任意类型;即便编译器有足够的信息区推导出其中的一些类型。

特别看一下ioo()中的状况:
// ++StartMethod ioo$0

LoadThis
      0:Getlocal0 [1]

PushScope
      1:Pushscope [0]

LoadRegister 1, int
      2:Getlocal1 [1]

LoadRegister 2, int
      3:Getlocal2 [2]

InvokeBinary BinaryPlusOp
      4:Add [1]

CheckType *
      5:Coerce.o [1]

StoreRegister 3, *
      6:Setlocal3 [0]

LoadRegister 3, *
      7:Getlocal3 [1]

Return
      8:Returnvalue [0]

// --FinishMethod ioo$0 TestClass/ioo

它与foo()最大的不同是,在两个参数被add之后,还对结果做了一次隐式类型转换到Object类型(coerce_o)。虽然编译器可以通过类型标注合理的推断出局部变量i也应该是int类型的,但却并没有这么做,而是继续将i看作“*”类型。

总的说来,AS3的可选类型标注的意义仅仅在于:
1、为编辑器提供更好的支持,使其容易实现高质量的智能感应(打个点能出现成员列表之类);
2、为编译器提供静态的类型检查的依据,尽量早的将类型错误报告给用户;有标注的就检查,没标注的就不检查;不能对未声明的变量赋值(而不像ES3或之前的版本,可以直接对未声明的变量赋值,赋值时自动创建出新的变量)。
至于运行效率,严格模式与标准模式编译出来的东西似乎没多少区别……并没有生成出更快的代码,不过至少也没有变得比标准模式更慢。

我的结论是否与现实状况有出入,这个有待进一步探究。Flex 3的asc.jar没有进行类型推导这点应该是没错的。

===================================================================================

但是值得一提的是,新版本的AVM2(Tamarin)正在试验所谓的Tracing-JIT,可以有效的减少动态类型检查带来的额外开销。无论是以标准模式还是以严格模式编译的AS3代码都能从中获益。或许的Adobe的策略就是:“反正后面有Tracing-JIT,即便编译器前端不做类型推导也没关系”,吧?
  • avmplus_test.zip (925.6 KB)
  • 描述: 文章中所做的测试所需要的东西,包括Tamarin、asc.jar、builtin.abc、toplevel.abc等
  • 下载次数: 37
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics