首页 > 代码库 > 编译还是解释?

编译还是解释?

编译还是解释?

 

我们可能要问,什么时候应该用编译,什么时候应该用解释呢?最终的结果是几乎相同,因此,答案通常最终归结为生成代码的原始速度,虽然内存使用情况和启动时间也是主要的关注。如果需要代码执行得更快,那么编译通常是更好的结果,有一定的优势。

清单 12-8 的测试工具,能够重复执行 interpret 函数中的 createDynamicMethod 方法,计算出花费的时间;还测试在动态方法上的重要变化。这还生成一个新的 .NET 委托值,作为该句柄,调用生成的代码。可以看出,这是到目前为止最快的技术。记住,评估抽象语法树花费的时间,可以是直接地,也可以是以编译形式,没有测量出解析时间或编译时间。

 

清单 12-8 比较性能的测试工具

open System

open System.Diagnostics

openStrangelights.Expression

 

//expression to process

let e = Multi(Val 2.,Plus(Val 2., Val 2.))

 

//collect the inputs

printf"Interpret/Compile/CompileThrough Delegate [i/c/cd]: "

let interpertFlag = Console.ReadLine()

printf"reps: "

let reps = int(Console.ReadLine())

 

type Df0 = delegate ofunit-> float

type Df1 = delegate offloat-> float

type Df2 = delegate offloat *float -> float

type Df3 = delegate offloat *float * float -> float

type Df4 = delegate offloat *float * float * float -> float

 

//run the tests

match interpertFlag with

| "i" ->

  let args = Interpret.getVariableValues e

  let clock = new Stopwatch()

  clock.Start()

  for i = 1 to reps do

    Interpret.interpret e args |> ignore

  clock.Stop()

  printf "%i" clock.ElapsedTicks

| "c" ->

  let paramNames = Compile.getParamList e

  let dm = Compile.createDynamicMethod e paramNames

  let args = Compile.collectArgs paramNames

  let clock = new Stopwatch()

  clock.Start()

  for i = 1 to reps do

    dm.Invoke(null, args) |> ignore

  clock.Stop()

  printf "%i" clock.ElapsedTicks

| "cd" ->

  let paramNames = Compile.getParamList e

  let dm = Compile.createDynamicMethod e paramNames

  let args = Compile.collectArgs paramNames

  let args = args |> Array.map (fun f -> f :?> float)

  let d =

    match args.Length with

    | 0 ->dm.CreateDelegate(typeof<Df0>)

    | 1 ->dm.CreateDelegate(typeof<Df1>)

    | 2 ->dm.CreateDelegate(typeof<Df2>)

    | 3 ->dm.CreateDelegate(typeof<Df3>)

    | 4 ->dm.CreateDelegate(typeof<Df4>)

    | _ -> failwith "too manyparameters"

  let clock = new Stopwatch()

  clock.Start()

  for i = 1 to reps do

    match d with

    | :? Df0 as d -> d.Invoke() |>ignore

    | :? Df1 as d -> d.Invoke(args.[0])|> ignore

    | :? Df2 as d -> d.Invoke(args.[0],args.[1]) |> ignore

    | :? Df3 as d -> d.Invoke(args.[0],args.[1], args.[2]) |> ignore

    | :? Df4 as d -> d.Invoke(args.[0],args.[1], args.[2], args.[4]) |> ignore

    | _ -> failwith "too manyparameters"

  clock.Stop()

  printf "%i" clock.ElapsedTicks

| _ -> failwith "not anoption"

 

表 12-2 汇总了执行程序计算表达式 Multi(Val2.,Plus(Val 2., Val 2.)) 的结果。

 

表 12-2汇总处理表达式 Multi(Val 2.,Plus(Val 2., Val 2.)),重复不同的次数(每微秒次数)

重复次数

1

10

100

1,000

10,000

100,000

1,000,000

解释

6,890

6,979

6,932

7,608

14,835

84,823

799,788

通过委托编译

8,65

856

854

1,007

2,369

15,871

151,602

编译

1,112

1,409

2,463

16,895

151,135

1,500,437

14,869,692

 

从表12-2 和图12-2 中可以看出,编译和通过委托编译在重复次数少时更快。但是,注意,重复1、10 和 100 次,需要的时间增长不明显,这是因为重复数量小,每次重复花费的时间不明显;真正花费的时间是即时编译器(JIT)用来把中间语言代码编译成本地代码,这是相对显著的。这就是为什么编译和通过委托编译的时间很接近,它们即时编译的代码数量相似;而解释花费更长的时间,是因为必须即时编译更多的代码,特别是解释器。但是,即时编译是一次性成本,每个方法只需要即时编译一次,因此,随着重复次数的增加,一次性的成本已经支付,这才真正看到相对性能成本的图像。

图 12-2 评估表达式1 + 1 的时间花费随计算次数的变化

[

图与程序已经不配套了,图还是原来的版本,程序是新的

]

 

从图12-2 可以看得更清楚,随着重复数量的增加,编译的成本急剧上升。这是因为,访问编译过的 DynamicMethod 方法通过调用Invoke,这是很昂贵的,每重复一次就增加一次成本,因此,用于编译的方法的耗时与重复的次数成正比。然而,问题在于不使用编译,而在于如何调用编译的代码。事实证明,通过委托调用 DynamicMethod,而不是在动态委托上的Invoke 成员,能够一次性的绑定方法的成本,因此,在打算多次评估表达式时,执行DynamicMethod,这种方法更有效。从这些结果来看,通过委托编译请求在速度方面最好的。

这个分析还表明测量的重要性,不要认为编译一定能筛期望的性能收益,除非真的看到针对具体数据集的好处,使用所有可用的技术来确保代码中没有不必要的躲藏开销。然而,现实中还会有其他许多因素,例如,如果表达式经常改变,解释器会要求再次即时编译一次。每次编译表达式将需要即时编译,因此,如果想看到任何性能提升,需要多次运行编译的代码。鉴于解释代码通常更容易实现,编译码仅在某些特定情况下才提供了显著的性能提升,因此,解释通常是一种更好的选择。

当处理的情况是需要尽可能快地执行代码,通常最好的方法是尝试不同的几个方法,然后,分析应用程序,看哪一种方法获得更好的结果。有关性能分析的更多内容,请看第十二章。

[

最后这一句话可能已经远处着落了。

本章就是第十二章。

原来的第十二章是“工具套件、NET 编程工具”,在新版本中好像没有了。

]

 

第十二章小结

 

在这一章,我们学习了 F# 中面向语言编程的主要特点与技术。可以看到有不同的技术,有使用数据结构作为小语言,或者使用引用,都涉及到对现有 F# 语法的改变或扩展;其他的,比如实现解析器,可以处理任何基于文本的语言,不管这种语言是自己设计的,或者,更常见的是已有的语言。所有这些技术,如能正确使用,都能极大地提高生产力。

下一章,我们将看一下如何使用 F# 解析文本,可以用它来创建不需要嵌入在 F# 的语言,也可以处理定义好的主要文本格式。