我们可能要问,什么时候应该用编译,什么时候应该用解释呢?最终的结果是几乎相同,因此,答案通常最终归结为生成代码的原始速度,虽然内存使用情况和启动时间也是主要的关注。如果需要代码执行得更快,那么编译通常是更好的结果,有一定的优势。
清单 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# 的语言,也可以处理定义好的主要文本格式。
编译还是解释?