【问题标题】:Performance of Mass-Evaluating Expressions in IronPythonIronPython 中质量评估表达式的性能
【发布时间】:2011-07-19 17:45:12
【问题描述】:

在 C#-4.0 应用程序中,我有一个长度相同的强类型 IList 字典 - 一个基于动态强类型列的表。 我希望用户根据将在所有行上聚合的可用列提供一个或多个(python-)表达式。在静态上下文中,它将是:

IDictionary<string, IList> table;
// ...
IList<int> a = table["a"] as IList<int>;
IList<int> b = table["b"] as IList<int>;
double sum = 0;
for (int i = 0; i < n; i++)
    sum += (double)a[i] / b[i]; // Expression to sum up

对于 n = 10^7,这在我的笔记本电脑 (win7 x64) 上运行时间为 0.270 秒。用两个 int 参数替换表达式需要 0.580 秒,非类型化委托需要 1.19 秒。 使用 IronPython 创建委托

IDictionary<string, IList> table;
// ...
var options = new Dictionary<string, object>();
options["DivisionOptions"] = PythonDivisionOptions.New;
var engine = Python.CreateEngine(options);
string expr = "a / b";
Func<int, int, double> f = engine.Execute("lambda a, b : " + expr);

IList<int> a = table["a"] as IList<int>;
IList<int> b = table["b"] as IList<int>;
double sum = 0;
for (int i = 0; i < n; i++)
    sum += f(a[i], b[i]);

需要 3.2 秒(Func&lt;object, object, object&gt; 需要 5.1 秒)- 4 到 5.5 倍。这是我正在做的事情的预期开销吗?有什么可以改进的?

如果我有很多列,上面选择的方法将不再足够。一种解决方案可能是确定每个表达式所需的列并仅使用这些列作为参数。我尝试过的另一个解决方案是使用 ScriptScope 并动态解析列。为此,我定义了一个 RowIterator,它有一个用于活动行的 RowIndex 和一个用于每一列的属性。

class RowIterator
{
    IList<int> la;
    IList<int> lb;

    public RowIterator(IList<int> a, IList<int> b)
    {
        this.la = a;
        this.lb = b;
    }
    public int RowIndex { get; set; }

    public int a { get { return la[RowIndex]; } }
    public int b { get { return lb[RowIndex]; } }
}

可以从 IDynamicMetaObjectProvider 创建 ScriptScope,我希望它由 C# 的动态实现 - 但在运行时 engine.CreateScope(IDictionary) 试图被调用,但失败了。

dynamic iterator = new RowIterator(a, b) as dynamic;
var scope = engine.CreateScope(iterator);
var expr = engine.CreateScriptSourceFromString("a / b").Compile();

double sum = 0;
for (int i = 0; i < n; i++)
{
    iterator.Index = i;
    sum += expr.Execute<double>(scope);
}

接下来我尝试让 RowIterator 从 DynamicObject 继承并使其成为一个运行示例 - 性能糟糕:158 秒。

class DynamicRowIterator : DynamicObject
{
    Dictionary<string, object> members = new Dictionary<string, object>();
    IList<int> la;
    IList<int> lb;

    public DynamicRowIterator(IList<int> a, IList<int> b)
    {
        this.la = a;
        this.lb = b;
    }

    public int RowIndex { get; set; }
    public int a { get { return la[RowIndex]; } }
    public int b { get { return lb[RowIndex]; } }

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        if (binder.Name == "a") // Why does this happen?
        {
            result = this.a;
            return true;
        }
        if (binder.Name == "b")
        {
            result = this.b;
            return true;
        }
        if (base.TryGetMember(binder, out result))
            return true;
        if (members.TryGetValue(binder.Name, out result))
            return true;
        return false;
    }

    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
        if (base.TrySetMember(binder, value))
            return true;
        members[binder.Name] = value;
        return true;
    }
}

我很惊讶 TryGetMember 是用属性的名称来调用的。从文档中,我预计 TryGetMember 只会为未定义的属性调用。

可能为了获得合理的性能,我需要为我的 RowIterator 实现 IDynamicMetaObjectProvider 以使用动态 CallSites,但找不到适合我的示例。在我的实验中,我不知道如何在 BindGetMember 中处理 __builtins__

class Iterator : IDynamicMetaObjectProvider
{
    IList<int> la;
    IList<int> lb;

    public Iterator(IList<int> a, IList<int> b)
    {
        this.la = a;
        this.lb = b;
    }
    public int RowIndex { get; set; }
    public int a { get { return la[RowIndex]; } }
    public int b { get { return lb[RowIndex]; } }

    public DynamicMetaObject GetMetaObject(Expression parameter)
    {
        return new MetaObject(parameter, this);
    }

    private class MetaObject : DynamicMetaObject
    {
        internal MetaObject(Expression parameter, Iterator self)
             : base(parameter, BindingRestrictions.Empty, self) { }

        public override DynamicMetaObject BindGetMember(GetMemberBinder binder)
        {
            switch (binder.Name)
            {
                case "a":
                case "b":
                    Type type = typeof(Iterator);
                    string methodName = binder.Name;
                    Expression[] parameters = new Expression[]
                    {
                        Expression.Constant(binder.Name)
                    };
                    return new DynamicMetaObject(
                        Expression.Call(
                            Expression.Convert(Expression, LimitType),
                            type.GetMethod(methodName),
                            parameters),
                        BindingRestrictions.GetTypeRestriction(Expression, LimitType));
                default:
                    return base.BindGetMember(binder);
            }
        }
    }
}

我确定我上面的代码不是最理想的,至少它还没有处理列的 IDictionary。如有任何关于如何改进设计和/或性能的建议,我将不胜感激。

【问题讨论】:

  • 我不会将 IDMOP 用作 ScriptScope 的成员,而是将 RowIterator 注入到 ScriptScope 中,或者甚至作为您要离开范围的委托的参数。
  • 我没有将 IDMOP 用作 ScriptScope 的 成员,而是用作“上下文”本身,即在我的表达式中我想输入“a / b”而不是“row.a / row.b”。这怎么能用你所谓的注入来完成?
  • @Dino:我认为使用 RowIterator 作为委托的参数是一个关于性能的好主意。如何将用户表达式“a / b”(或其他)替换为“row.a / row.b”,而我将实际编译它?

标签: performance c#-4.0 ironpython expression-trees dynamic-language-runtime


【解决方案1】:

我还将 IronPython 的性能与 C# 实现进行了比较。表达式很简单,只需在指定索引处添加两个数组的值。直接访问阵列提供了基线和理论最优值。通过符号字典访问值的性能仍然可以接受。

第三个测试从一个幼稚的(并且有意为之的)表达式树创建一个委托,没有任何花哨的东西,如调用端缓存,但它仍然比 IronPython 快。

通过 IronPython 编写表达式脚本花费的时间最多。我的分析器告诉我,大部分时间都花在了 PythonOps.GetVariable、PythonDictionary.TryGetValue 和 PythonOps.TryGetBoundAttr。我认为还有改进的余地。

时间安排:

  • 直接:00:00:00.0052680
  • 通过字典:00:00:00.5577922
  • 已编译的委托:00:00:03.2733377
  • 脚本:00:00:09.0485515

代码如下:

   public static void PythonBenchmark()
    {
        var engine = Python.CreateEngine();

        int iterations = 1000;
        int count = 10000;

        int[] a = Enumerable.Range(0, count).ToArray();
        int[] b = Enumerable.Range(0, count).ToArray();

        Dictionary<string, object> symbols = new Dictionary<string, object> { { "a", a }, { "b", b } };

        Func<int, object> calculate = engine.Execute("lambda i: a[i] + b[i]", engine.CreateScope(symbols));

        var sw = Stopwatch.StartNew();

        int sum = 0;

        for (int iteration = 0; iteration < iterations; iteration++)
        {
            for (int i = 0; i < count; i++)
            {
                sum += a[i] + b[i];
            }
        }

        Console.WriteLine("Direct: " + sw.Elapsed);



        sw.Restart();
        for (int iteration = 0; iteration < iterations; iteration++)
        {
            for (int i = 0; i < count; i++)
            {
                sum += ((int[])symbols["a"])[i] + ((int[])symbols["b"])[i];
            }
        }

        Console.WriteLine("via Dictionary: " + sw.Elapsed);



        var indexExpression = Expression.Parameter(typeof(int), "index");
        var indexerMethod = typeof(IList<int>).GetMethod("get_Item");
        var lookupMethod = typeof(IDictionary<string, object>).GetMethod("get_Item");
        Func<string, Expression> getSymbolExpression = symbol => Expression.Call(Expression.Constant(symbols), lookupMethod, Expression.Constant(symbol));
        var addExpression = Expression.Add(
                                Expression.Call(Expression.Convert(getSymbolExpression("a"), typeof(IList<int>)), indexerMethod, indexExpression),
                                Expression.Call(Expression.Convert(getSymbolExpression("b"), typeof(IList<int>)), indexerMethod, indexExpression));
        var compiledFunc = Expression.Lambda<Func<int, object>>(Expression.Convert(addExpression, typeof(object)), indexExpression).Compile();

        sw.Restart();
        for (int iteration = 0; iteration < iterations; iteration++)
        {
            for (int i = 0; i < count; i++)
            {
                sum += (int)compiledFunc(i);
            }
        }

        Console.WriteLine("Compiled Delegate: " + sw.Elapsed);



        sw.Restart();
        for (int iteration = 0; iteration < iterations; iteration++)
        {
            for (int i = 0; i < count; i++)
            {
                sum += (int)calculate(i);
            }
        }

        Console.WriteLine("Scripted: " + sw.Elapsed);
        Console.WriteLine(sum); // make sure cannot be optimized away
    }

【讨论】:

    【解决方案2】:

    虽然我不知道你的情况的所有具体细节,但在 IronPython 中执行如此低级别的任何事情,仅减速 5 倍实际上是相当不错的。 Computer Languages Benchmark Game 中的大多数条目显示 10-30 倍的减速。

    一个主要的原因是 IronPython 必须考虑到您在运行时做了一些鬼鬼祟祟的事情的可能性,因此无法生成同样效率的代码。

    【讨论】:

    • 只是为了澄清:“动态强类型”是指我的表实现。使用类型化 IList 的字典,我可以在运行时添加 typed 列。我认为这在创建 CallSites 时会有所帮助。
    • @Christian - 啊,好吧。相应地编辑了答案。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-12-19
    • 2012-12-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多