Производительность выражений массовой оценки в IronPython - PullRequest
4 голосов
/ 21 марта 2011

В приложении 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 это работает на моем ноутбуке за 0,270 с (win7 x64). Замена выражения делегатом с двумя аргументами типа 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 с (и 5,1 с при Func<object, object, object>) - коэффициент от 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]; } }
}

ScriptScope может быть создан из IDynamicMetaObjectProvider, который, как я ожидал, будет реализован динамическим 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 будет вызываться только для неопределенных свойств.

Вероятно, для ощутимой производительности мне нужно было бы реализовать IDynamicMetaObjectProvider для моего RowIterator, чтобы использовать динамические CallSites, но я не смог найти подходящий пример для начала. В своих экспериментах я не знал, как обрабатывать __builtins__ в BindGetMember:

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 столбцов. Буду благодарен за любые советы по улучшению дизайна и / или производительности.

Ответы [ 2 ]

1 голос
/ 25 ноября 2011

Я также сравнил производительность IronPython с реализацией C #. Выражение простое, просто добавление значений двух массивов по указанному индексу. Доступ к массивам напрямую обеспечивает базовую линию и теоретический оптимум. Доступ к значениям через словарь символов по-прежнему приемлемо.

Третий тест создает делегат из наивного (и плохого по намерению) дерева выражений без каких-либо причудливых вещей, таких как кэширование на стороне вызова, но он все же быстрее, чем IronPython.

Написание сценария выражения с помощью IronPython занимает больше всего времени. Мой профилировщик показывает мне, что большую часть времени проводят в PythonOps.GetVariable, PythonDictionary.TryGetValue и PythonOps.TryGetBoundAttr. Я думаю, что есть возможности для улучшения.

Тайминги:

  • Прямой: 00: 00: 00.002626
  • через словарь: 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
    }
0 голосов
/ 21 марта 2011

Хотя я не знаю всех конкретных деталей в вашем случае, замедление всего в 5 раз для выполнения чего-либо такого низкого уровня в IronPython на самом деле довольно хорошее. Большинство записей в тесте Computer Languages ​​ показывают замедление в 10-30 раз.

Основная причина в том, что IronPython должен допускать возможность того, что вы сделали что-то подлое во время выполнения и, следовательно, не можете создавать код с такой же эффективностью.

...