DLR及性能
我打算创建一个网络服务,能够尽可能快速地进行大量手动指定的计算,并且我在研究使用DLR(动态语言运行时)。
抱歉内容有点长,但你可以随意浏览一下,了解大概意思就行。
我一直在使用IronPython库,因为它让计算的指定变得非常简单。我的工作笔记本电脑每秒能进行大约400,000次计算,具体做法如下:
ScriptEngine py = Python.CreateEngine();
ScriptScope pys = py.CreateScope();
ScriptSource src = py.CreateScriptSourceFromString(@"
def result():
res = [None]*1000000
for i in range(0, 1000000):
res[i] = b.GetValue() + 1
return res
result()
");
CompiledCode compiled = src.Compile();
pys.SetVariable("b", new DynamicValue());
long start = DateTime.Now.Ticks;
var res = compiled.Execute(pys);
long end = DateTime.Now.Ticks;
Console.WriteLine("...Finished. Sample data:");
for (int i = 0; i < 10; i++)
{
Console.WriteLine(res[i]);
}
Console.WriteLine("Took " + (end - start) / 10000 + "ms to run 1000000 times.");
这里的DynamicValue是一个类,它从一个预先构建的数组中返回随机数(这个数组是在运行时生成的)。
当我创建一个DLR类来做同样的事情时,性能大幅提升,达到了每秒约10,000,000次计算。这个类的代码如下:
class DynamicCalc : IDynamicMetaObjectProvider
{
DynamicMetaObject IDynamicMetaObjectProvider.GetMetaObject(Expression parameter)
{
return new DynamicCalcMetaObject(parameter, this);
}
private class DynamicCalcMetaObject : DynamicMetaObject
{
internal DynamicCalcMetaObject(Expression parameter, DynamicCalc value) : base(parameter, BindingRestrictions.Empty, value) { }
public override DynamicMetaObject BindInvokeMember(InvokeMemberBinder binder, DynamicMetaObject[] args)
{
Expression Add = Expression.Convert(Expression.Add(args[0].Expression, args[1].Expression), typeof(System.Object));
DynamicMetaObject methodInfo = new DynamicMetaObject(Expression.Block(Add), BindingRestrictions.GetTypeRestriction(Expression, LimitType));
return methodInfo;
}
}
}
并且通过以下方式调用/测试:
dynamic obj = new DynamicCalc();
long t1 = DateTime.Now.Ticks;
for (int i = 0; i < 10000000; i++)
{
results[i] = obj.Add(ar1[i], ar2[i]);
}
long t2 = DateTime.Now.Ticks;
这里的ar1和ar2是预先构建的、在运行时生成的随机数数组。
这样速度很快,但指定计算的过程并不简单。我基本上需要自己创建一个词法分析器和解析器,而IronPython已经提供了我所需的一切。
我本以为IronPython的性能会更好,因为它是建立在DLR之上的,但我现在的性能还不够理想。
我的例子是否充分利用了IronPython引擎?有没有可能获得显著更好的性能呢?
(编辑)这是第一个例子的相同内容,但用C#中的循环来设置变量并调用Python函数:
ScriptSource src = py.CreateScriptSourceFromString(@"b + 1");
CompiledCode compiled = src.Compile();
double[] res = new double[1000000];
for(int i=0; i<1000000; i++)
{
pys.SetVariable("b", args1[i]);
res[i] = compiled.Execute(pys);
}
这里的pys是来自py的ScriptScope,args1是一个预先构建的随机双精度数组。这个例子的执行速度比在Python代码中运行循环并传入整个数组要慢。
2 个回答
如果你关心计算速度,看看底层计算的规范可能更好。Python和C#都是高级语言,它们在运行时可能会花很多时间在一些隐秘的工作上。
可以看看这个LLVM的封装库:http://www.llvmpy.org
- 你可以通过以下命令安装它:
pip install llvmpy ply
- 或者在Debian Linux上使用:
apt install python-llvmpy python-ply
你还需要写一个小编译器(可以使用PLY库),并将其与LLVM的即时编译调用绑定(可以查看LLVM执行引擎),但这种方法可能更有效(生成的代码更接近真实的CPU代码),而且与.NET的限制相比,它是多平台的。
LLVM有现成的优化编译器基础设施,包括很多优化阶段的模块,还有一个庞大的用户和开发者社区。
你也可以看看这里:http://gmarkall.github.io/tutorials/llvm-cauldron-2016
附言:如果你对此感兴趣,我可以帮你做一个编译器,同时为我的项目手册贡献一些内容。不过这不会很快入门,这个主题对我来说也是新的。
delnan的评论提到了这里的一些问题。不过我会具体说明一下这里的区别。在C#版本中,你省略了很多动态调用,而这些在Python版本中是有的。首先,你的循环是用int类型的,这听起来ar1和ar2是强类型数组。所以在C#版本中,唯一的动态操作就是调用obj.Add(在C#中这只是一个操作),还有可能是赋值给results,如果它不是类型为object的话,这种可能性似乎不大。另外,注意这些代码都是无锁的。
在Python版本中,首先你要分配一个列表——这似乎是在你的计时器中进行的,而在C#中看起来并不是。接着你有一个动态调用range,幸运的是这只发生一次。但这又在内存中创建了一个巨大的列表——delnan建议使用xrange会在这里有所改善。然后你有一个循环计数器i,在每次循环中都会被装箱成一个对象。接下来你调用b.GetValue(),这实际上是两个动态调用——首先是获取“GetValue”方法的成员,然后在这个绑定的方法对象上进行调用。这又是在每次循环中创建一个新对象。然后b.GetValue()的结果可能在每次循环中又是一个被装箱的值。接着你在这个结果上加1,这又是在每次循环中进行一次装箱操作。最后你把这个结果存入列表,这又是一个动态操作——我认为这个最终操作需要加锁,以确保列表的一致性(再次强调,delnan建议使用列表推导会改善这一点)。
所以总结一下,在循环中我们有:
C# IronPython
Dynamic Operations 1 4
Allocations 1 4
Locks Acquired 0 1
基本上,Python的动态特性确实比C#要付出代价。如果你想兼顾两者的优点,可以尝试平衡在C#和Python中的操作。例如,你可以在C#中写循环,然后调用一个委托,这个委托是一个Python函数(你可以使用scope.GetVariable>从作用域中获取一个函数作为委托)。如果你真的需要获得每一丝性能,你还可以考虑为结果分配一个.NET数组,这样可以减少工作集和GC复制,因为不需要保留一堆装箱的值。
为了使用委托,你可以让用户写:
def computeValue(value):
return value + 1
然后在C#代码中你可以这样做:
CompiledCode compiled = src.Compile();
compiled.Execute(pys);
var computer = pys.GetVariable<Func<object,object>>("computeValue");
现在你可以这样做:
for (int i = 0; i < 10000000; i++)
{
results[i] = computer(i);
}