Производительность выражений массовой оценки в IronPython
В приложении на 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 ответа
Я также сравнил производительность 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
}
Хотя я не знаю всех конкретных деталей в вашем случае, замедление всего в 5 раз для выполнения чего-либо такого низкого уровня в IronPython на самом деле довольно хорошо. Большинство записей в тесте Computer Languages Game показывают замедление в 10-30 раз.
Основная причина в том, что IronPython должен учитывать возможность того, что вы сделали что-то подлое во время выполнения и, следовательно, не можете создавать код с той же эффективностью.