俺的垃圾箱

架构分析 解释编译原理
posts - 36, comments - 233, trackbacks - 12, articles - 1
  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理
编译汇编语句: ADD EBX, 300,重复2017次,测得其运行时间为 60 个 QueryPerfomanceCount。在这里直接运行的机器码应该是最快实现了吧。但是让我们编译IL语句: ldc.i4 300, ADD 重复2017次,测得其运行时间居然只有区区 5个 QueryPerfomanceCount。跌破眼球吧,到底是怎么回事了,这就是JITter 优化的功劳了。

IL代码如下:
L_0006: ldc.i4 300
L_000b: stloc.3
L_000c: ldloca.s num2
L_000e: call Borland.Delphi.LongBool Borland.Vcl.Units.Windows::QueryPerformanceCounter(int64&) L_0013: pop
L_0014: ldloc.3
L_0015: ldc.i4 300
L_001a: add
L_001b: stloc.3
L_001c: ldloc.3
L_001d: ldc.i4 300
L_0022: add
....
L_3f0b: stloc.3
L_3f0c: ldloc.3
L_3f0d: ldc.i4 300
L_3f12: add
L_3f13: stloc.3
L_3f14: ldloca.s num3
L_3f16: call Borland.Delphi.LongBool Borland.Vcl.Units.Windows::QueryPerformanceCounter(int64&)

用高级语言表述其实就是(Pascal):
var
  i, count: Integer;
  tBegin, tEnd: Int64;
i := 300;
QueryPerformanceCounter(tBegin);
i := i + 300;
....
i := i + 300;
QueryPerformanceCounter(tEnd);
count := tEnd - tBegin;
writeln('ExecuteTime:',count, '; Result=', i);;
估计JITter将中间结果给省略了,直接变成了 i := 300 * 2017,这样一下子就来了,当然快的不得了。关闭JITter优化必须在调试器中运行,感谢 neoragex2002 的测试,而关闭Optimal参数只是将额外附加一个NOOP指令而已.


想个办法让它不能省略中间结果,弄个数组,将每一步的值装载起来,看看能不能骗过去。
var
   tBegin, tEnd: Int64;
   count,i: Integer;
   aArray: array [0..2016] of Integer;

i := 300;
count := 0;
aArray[count] := i;
QueryPerformanceCounter(tBegin);
i:= i+300;Inc(count);aArray[count] := i;
....
i:= i+300;Inc(count);aArray[count] := i;

现在,默认全速的情况下(debug off, optimal on),执行时间是24,唉,只骗过一点点,不过将debug on, optimal off 之后情况就正常了,执行时间在143-178之间徘徊。

JITter优化真是惹人爱啊,但是这里却让我无法测试它的JITter真实的编译开销,到底有多大,俗话说,优化越多,那么JITter在编译上花销的时间也就该越多。也许将debug on, optimal off 之后情况才是真实的JITter编译开销?

不知道那位大侠有这方面的资料,还忘不吝赐教啊。

Feedback

#1楼   回复  引用  查看    

2006-12-22 17:53 by neoragex2002      
呵呵,这些观察都没有触及jit的本质:运行时优化。第一个例子:简单的常数传播,这部分分析工作无需运行时做,在编译的时候就可以做了,第二个例子,变量传播+最简单的数据流分析,实际上编译时也可以做,流程在QueryPerformanceCounter(tEnd);之前将aArray依次赋值即可。

上面都是编译时能够解决的优化问题,可为什么M$要用运行时的优化技术?从计算理论角度,哪些问题是静态优化方式无法解决的?哪些优化所需的信息是编译时无法获取的?呵呵,楼主可以思考一下,然后再写篇文章:)

#2楼   回复  引用  查看    

2006-12-22 18:17 by neoragex2002      
有兴趣的话,找这篇文章看看,A survey of adaptive optimization in virtual machines,'04 IBM的技术报告。

#3楼[楼主]   回复  引用  查看    

2006-12-22 19:11 by Riceball LEE      
是的,这些的确是上层编译器就可以处理的,至于为啥在上面没有处理,偏偏要放到JITter种处理,嘿嘿,那就是这说明JITter做的优化非常的全面,所以编译器偷懒了。同时也让我对它的编译效率(性能)尤为担心,这也是我做测试的初衷。

对于第一个例子,正如你提到的那样,由于这些都是能在编译时刻被优化掉的,所以在加载时候就被处理了,而不是在执行JITter的时候优化的,所以就算关闭优化也没有作用。

但是对于第二个例子,似乎是JITter做的运行时刻优化,而不是在加载时候被优化的。

感谢。

#4楼   回复  引用  查看    

2006-12-22 23:40 by Wisdom-zh      
局部优化理论上比较好做, 所以这里的测试结果如此, 对于太长的段, 则难以优化, 所以JIT的运行速度仍然要大大慢于编译成机器码的程序(虽然现在的很多编译器优化做得并不是太好)

#5楼   回复  引用  查看    

2006-12-23 00:19 by neoragex2002      
@Wisdom-zh
在c#/vb这些非mission-critical的语言编译器中,interprocedural optimazation功能是没有提供的。但值得注意的是,vs2005中的cl编译器提供了较全面的全局优化功能,而且与/clr选项(即c++/cli)兼容。所以我觉得你可以实验一下这个配合jit的优化效果如何。

http://msdn.microsoft.com/vstudio/tour/vs2005_guided_tour/VS2005pro/Framework/CPlusAdvancedProgramOptimization.htm">http://msdn.microsoft.com/vstudio/tour/vs2005_guided_tour/VS2005pro/Framework/CPlusAdvancedProgramOptimization.htm


#6楼[楼主]   回复  引用  查看    

2006-12-23 11:10 by Riceball LEE      

@neoragex2002
嗯,同意@Wisdom-zh所说的,能在上层编译器上做的活,就不要放到运行时期做是做好,毕竟优化工作也是要花时间的,而CLR的JITTer的实现,是当你首次调用方法的时候才做的编译工作,所以优化编译所消耗的时间就会延长了整个执行的时间,降低了执行效率。估计这也是为什么加载器也会对IL进行优化的原因,减少JITter的工作量,提高JITter效率,也就是提高了执行效率。可惜是vc++才提供interprocedural optimazation。不管这些,毕竟俺也是想编写个脚本引擎玩玩,到时候自己想怎么弄就怎么弄,想添加啥就添加啥,嘿嘿。

#7楼   回复  引用  查看    

2006-12-23 15:20 by Wisdom-zh      
对于像 C++ 这样的静态语言来说, 优化的潜力巨大.
动态脚本比较难以优化, 我们的 Nuva 语言的运行速度相对于静态语言的编译代码来说, 还是慢了很多, 但是既是脚本, 我也就不在意速度的优化, 而是表达能力的优化了.
虽说现在还比较初级, 但是我们的大部分程序都是采用这种新脚本语言写的, 很方便呐.
楼主对脚本感兴趣, 呵呵, 有空多交流咯:)

#8楼   回复  引用  查看    

2006-12-23 16:18 by neoragex2002      
@Riceball LEE
呵呵,你的理解似乎有点twisted... clr优化对于"加载"和"运行时"是不区分的,assembly加载时,并不会发生任何“加载器IL优化行为”,目前没有任何官方言论、文章提到了clr中存在着上述行为,考虑到对元数据及加载器本身的负面影响,这种运行时修改IL的设计行为在正常使用环境中是不太可能采用的(当然,不排除加壳、混淆等其他目的,不过那是hack了)。唯有需要运行特定的method时,jit编译、优化过程才发生,即clr优化只与jit有关。

由于jit优化是商业版clr才具有的特性(sscli中没有),因此暂不能肯定其优化程度究竟能够达到何种地步。但可以肯定的是,VC++编译器在IL层面的编译时优化比C#要完善得多,二者不是一个数量级的。

#9楼[楼主]   回复  引用  查看    

2006-12-24 10:41 by Riceball LEE      
@neoragex2002
当然这只是我的揣测,我不觉得有什么不合理的,只要代码段能被静态简化,那就当然可以在加载时候处理,不然就无法解释第一个例子为什么即使关闭JIT优化,依然速度远高于本地机器码的结果了,如果你有更好的理解或理论,俺洗耳恭听。

@Wisdom-zh
我在对以前策划的脚本引擎架构的方案进行实施,已经写出了引擎核心的原型,,然后针对x86解释器做的速度优化理念的尝试,试验验证了俺心中的一些想法。

你可以象我这样用整数加法VM指令做个简单的测试,在将测试的结果(速度=1/time)于直接本地机器码运行的结果比较,以机器码直接运行的结果为100,那么解释器速度占到直接运行速度的百分之几,就可以看出你写的解释器的效率,以及有无提高的空间。对了,还有调用子过程的效率也是非常关键的,因为绝大多数功能都是以子过程的形式完成的。另外一个衡量一个解释器的好坏的因素就是占用空间。

俺做的核心解释器VM指令执行效率性能损失均不到1倍。
附下测试结果(2017条指令,重复执行300+, 也就是 2017* 300):
直接VM加法指令的速度是:69% (与直接执行机器码相比性能损失31%)
调用
VM子过程近调用效率测试(将加法指令放到过程中):75% (只损耗了25%).


#10楼   回复  引用  查看    

2006-12-24 19:50 by neoragex2002      
C#编译配置:
Debug: /debug+ /debug:full /optimize-
Release: /debug:pdbonly /optimize+

结果:
用vs2005启动,Debug: 42 Release: 40
直接启动,Debug: 6 Release: 4

结论:
无论编译开关如何,只要用vs2005启动的,jit优化便是关闭的;无论编译开关如何,只要不是用vs2005启动的,jit优化便是开启的。这是一种最简单的打开/关闭Jit优化的方法。

我同时还用了cordbg试了试(把里面的JitOptimizations模式关闭或打开,再比较反汇编代码和运行结果),结论与vs2005+C#是一样的,那就是不存在你所说的那种关闭了jit优化还非常快的“加载优化”情况。我不知道第一个例子中你用delphi.net是否能够真正确信地关闭/开启jit优化,就像上面说的,jit优化开关更多的是与程序启动方式(即加不加载调试器)相关,而不是编译选项。

附测试程序:
class Program
{
[DllImport("kernel32.dll")]
public static extern bool QueryPerformanceCounter(ref ulong beepType);

static void Main(string[] args)
{
ulong x = 0, y = 0;
int i = 0;

QueryPerformanceCounter(ref x);
i += 300;
i += 300;
i += 300;
i += 300;
i += 300;
i += 300;
i += 300;
i += 300;
i += 300;
i += 300;
i += 300;
QueryPerformanceCounter(ref y);

Console.WriteLine(y - x);
Console.ReadLine();
}
}

#11楼   回复  引用  查看    

2006-12-24 20:24 by Wisdom-zh      
@Riceball LEE
呵呵, 最慢的不是这些, 而是变量/函数/对象的动态方法(包括虚函数, 运算符重载, 各种类型自动转换和垃圾回收等).
当然, 我只考虑整体性能, 不对单个指令的执行进行优化.
我们的 Nuva 程序的整体执行速度还可以, 如果不认真观察, 你不会以为是脚本语言写的:)
我们网站上的很多程序都是 Nuva 语言写的, 如果有兴趣, 可以一试,
http://www.macrobject.com">http://www.macrobject.com

#12楼[楼主]   回复  引用  查看    

2006-12-25 11:57 by Riceball LEE      
@neoragex2002
万分感谢,我在Delphi.Net 的IDE环境下试了下,果如你所言,真是尽信书不如无书啊。
我也不能确信Delphi.Net的Optimal参数和Debug就是把
我估摸着Delphi.Net和C#应该是类似的实现
再次查了查文档了,据文档说关闭Optimal参数将额外附加NOOP指令,所以显得慢一点,我在IDE环境下重新运行第二个例子:
Debug-Optimal+ IDE: 大约 180
Debug+Optimal- IDE: 大约 190

也就是说要想关闭JITter优化只有在调试环境下运行了。

另在IDE环境下看了看JIT产生的本地机器码,发现实际上还是做了点指令合并优化的:
ldloc.3
ldc.i4 InlineVar
add
stloc.3
被直接优化成:add esi, 300

@Wisdom-zh
也许你没有明白我的意思,我所关注的是解释器的效率,对单个指令并不关心,而从一个简单指令着手和本地机器码的简单指令比较,是为了能看出这个解释器的工作效率,
本地机器码的简单指令不用解释直接运行的,可以视作速度为100%,而VM指令执行则是经过解释器,那么让解释器的空耗(无用功)越小,自然执行速度就越逼近本地100%.这个比值是反映的解释器的效率,而不是这个指令本身的速度。

#13楼   回复  引用  查看    

2006-12-25 12:17 by Wisdom-zh      
@Riceball LEE
got it!
我们优化的策略是, VM 指令大于机器指令, 从而将"解释"粒度放大, 频度减少, 从而提高性能, 所以对你所说的单条指令解释效率没有概念, 因为二者基本上没有可比较性.
假想一下, 如果采用 VM 指令和机器指令对应的解释方法, 那么你无论如何都将损失数倍的效率, 因为粒度太小啊...

#14楼[楼主]   回复  引用  查看    

2006-12-25 12:43 by Riceball LEE      
@Wisdom-zh
无论你的解释器或VM是什么样的实现,从宏观上说,执行一个整数加法运算总该有的,那么这个执行时间也是一定的,所以也就有比值,那么这个比值也就是能看出你的解释器效率,当然如果你非要说我的系统不是用来运算,而是优化在大的功能块上(以这种功能块作为VM指令),那我就无话可说了。那你只能这样测试你的解释器效率:把这个功能块在编译型语言中用汇编实现,测试其执行效率,然后再在解释器中比较。但是这样一来,不准的因素就多了一个:你在编译型语言中用汇编实现的功能块的速度是由你的编程水平决定的。

#15楼   回复  引用  查看    

2006-12-25 13:03 by neoragex2002      
NOOP是用于32位指令对准和提高指令级流水线或分支预测中指令预取效率的,不是用来降低执行效率的....

#16楼[楼主]   回复  引用  查看    

2006-12-25 13:36 by Riceball LEE      
@neoragex2002
请别误解我的意思,我的意思在VM中实现空指令得以测出解释器的空耗(空耗越小你的解释器的效率就越高),不是指x86的空指令。

#17楼   回复  引用  查看    

2006-12-25 15:53 by Wisdom-zh      
@Riceball LEE
非常好!
Nuva 设计之初确实不是用来计算的, 而我们测试性能的方法确如你所言, 写一段典型程序, 其中包含常规的加减乘除运算, 字符串连缀, 子函数调用, 递归处理, 分支判断, 循环, 内存释放等, 然后用各种语言完整实现(完全一样的写法, 不经优化), 从而对比 Nuva 语言与各种语言实现的性能效率差异, 其中对比的语言包括各种编译型语言, 解释型语言, JIT 型语言等(排除了汇编, 因为程序实在难以用汇编实现).
最后我们接受了测试结果, 觉得整体性能还是不错的.
单条指令解释的速度确实有优化的必要, 但是 VM 的复杂性增加之后, 这个效率将受到影响, 比如你说的加法运算, 在 Nuva 语言中, 变量没有类型, 从而在实现加法解释之前, 需要确定操作数的数据类型, 从而实现操作符重载, 甚至包括类型自动转换, 这样性能就进一步降低了.
对于你上文提到的性能指标, 我想, Nuva 语言是不可能达到的, 我估计(没有测试过), Nuva 语言解释一个加法运算的消耗, 恐怕至少要十倍于这个加法本身的运算时间.
但是我们针对语言的其他部分进行优化, 从而提高整体性能.

#18楼[楼主]   回复  引用  查看    

2006-12-25 19:33 by Riceball LEE      
@Wisdom-zh
建议你看看Lua也是一种无类型的脚本语言,不过它的数字无论整数还是小数全部被转换成float实现的,但是它的i+=300执行效率可以达到汇编浮点运算指令的大约64%(空耗只有36%)。

另:你可以在VM实现一个NOOP空指令(什么事情也不做),用来精确测试你的解释器的空耗时间。

#19楼   回复  引用  查看    

2006-12-26 09:49 by Wisdom-zh      
@Riceball LEE
没错, Lua 确实很快, 当初我们的测试对比里面也包括 Lua, Lua 是我们测试过的最快的解释型语言, 性能大约比 Nuva 语言高两三倍(一个我上面所说的典型程序, 不是每种单独运算或指令的对比).
Lua 语言主要用于游戏脚本, 所以其性能指标相当重要, 相比较, Nuva 语言则用于模板处理, 文本处理以及一些 GUI 程序, 性能要求较低.
如果以 Lua 语言的性能作为优化目标, 那么, 我们还需要很多的工作要做:)
我上面说过, Nuva 语言的运算需要类型自动转换, 以及运算符重载, 做了好多无用功, Lua 语言似乎不能自动转换类型, 比如
var a = '2';
a ++; // 此时 a = 3
或者
var b = 1 + '2' // 此时 b = 3
我们会自动转换类型, 所以消耗较大.



发表评论

昵称: [登录] [注册]

主页:

邮箱:(仅博主可见)

评论内容:

  登录  注册

[使用Ctrl+Enter键快速提交评论]

0 600653




相关文章:

相关链接: