1

我打算创建一个尽可能快地执行大量手动指定计算的 Web 服务,并且一直在探索 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 代码中运行循环并传入整个数组要慢。

4

2 回答 2

2

delnan 的评论将您引向这里的一些问题。但我将具体说明这里的差异。在 C# 版本中,您已经删除了 Python 版本中的大量动态调用。对于初学者,您的循环类型为 int,听起来 ar1 和 ar2 是强类型数组。因此,在 C# 版本中,您拥有的唯一动态操作是对 obj.Add 的调用(这是 C# 中的 1 个操作),并且如果未键入对象,则可能会分配给结果,这似乎不太可能。另请注意,所有此代码都是无锁的。

在 Python 版本中,您首先拥有列表的分配 - 这似乎也出现在您的计时器期间,而在 C# 中它看起来不像。然后你有对范围的动态调用,幸运的是这只发生了一次。但这又在内存中创建了一个巨大的列表——delnan 对 xrange 的建议在这里是一种改进。然后你有循环计数器 i ,它在循环的每次迭代中都被装箱到一个对象中。然后你可以调用 b.GetValue() 这实际上是 2 个动态调用 - 首先是获取“GetValue”方法的 get 成员,然后是对该绑定方法对象的调用。这再次为循环的每次迭代创建一个新对象。然后你有 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);
}
于 2011-02-02T03:37:45.900 回答
0

如果您关心计算速度,那么查看低级计算规范会更好吗?Python 和 C# 是高级语言,其实现运行时可能会花费大量时间进行卧底工作。

查看这个 LLVM 包装库:http ://www.llvmpy.org

  • 安装它使用:pip install llvmpy ply
  • 或在 Debian Linux 上:apt install python-llvmpy python-ply

您仍然需要编写一些小型编译器(您可以使用PLY 库),并将其与 LLVM JIT 调用绑定(请参阅 LLVM 执行引擎),但这种方法可能更有效(生成的代码更接近真实的 CPU 代码)和多平台与 .NET 监狱相比。

LLVM 已经准备好使用优化编译器基础设施,包括许多优化器阶段模块,以及大用户和开发者社区。

也看这里: http: //gmarkall.github.io/tutorials/llvm-cauldron-2016

PS:如果您对此感兴趣,我可以帮助您编译器,同时为我的项目手册做出贡献。但这不会是快速启动,这个主题对我来说也是新的。

于 2017-11-26T11:17:25.383 回答