DLR & Performance
-
27-10-2019 - |
Вопрос
Я намерен создать веб-сервис, который выполняет большое количество расчетов вручную как можно быстрее, и изучал использование DLR.
Извините, если это долго, но не стесняйтесь пройтись и получить общую суть.
Я использовал библиотеку Ironpython, так как это делает расчеты очень простыми для указания. My Works Naptop дает показатели около 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 предварительно построены, во время выполнения массивы случайных чисел.
Таким образом, скорость отличная, но нелегко указать расчет. Я бы в основном смотрел на создание моего собственного Lexer & Parser, тогда как 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, а Args1-предварительно построенный массив случайных парных разрядов. Этот пример выполняется медленнее, чем запуск петли в коде Python и прохождение во всех массивах.
Решение
Комментарий Делнана приводит вас к некоторым проблемам здесь. Но я просто буду конкретно о том, каковы различия здесь. В версии C# вы вырезали значительное количество динамических вызовов, которые у вас есть в версии Python. Для начала ваш цикл напечатан Int, и это звучит так, будто AR1 и AR2 сильно напечатаны массивы. Таким образом, в версии C# единственными динамическими операциями, которые у вас есть, являются призыв к OBJ.Add (который является 1 операцией в C#) и потенциально назначение к результатам, если он не набран на объект, который кажется маловероятным. Также обратите внимание на весь этот код бесплатный.
В версии Python у вас сначала есть распределение списка - это также кажется во время вашего таймера, где, как в C#, он не выглядит так. Тогда у вас есть динамический вызов в диапазоне, к счастью, это происходит только один раз. Но это снова создает гигантский список в памяти - предложение Delnan о XRange - это улучшение здесь. Тогда у вас есть счетчик петли I, который попадает в штучку на объект для каждой итерации через цикл. Затем у вас есть призыв к B.GetValue (), который на самом деле является 2 Dynamic Invocatiosn - сначала член GET, чтобы получить метод «getValue», а затем вызов на этот связанный объект метода. Это снова создает один новый объект для каждой итерации петли. Тогда у вас есть результат b.getValue (), который может быть еще одним значением, которое заложено на каждой итерации. Затем вы добавляете 1 к этому результату, и у вас есть еще одна операция по боксу на каждой итерации. Наконец, вы храните это в своем списке, который является еще одной динамичной операцией - я думаю, что эта окончательная операция должна блокировать, чтобы гарантировать, что список остается последовательным (опять же, предложение Делнана об использовании понимания списка улучшает это).
Итак, в итоге во время петли у нас есть:
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);
}
Другие советы
Если вы обеспокоены скоростью вычисления, лучше ли взглянуть на низкий уровень спецификация вычислений? Python и C#-это языки высокого уровня, и его время выполнения реализации может потратить много времени на тайную работу.
Посмотрите на эту библиотеку обертки LLVM: http://www.llvmpy.org
- Установите его, используя:
pip install llvmpy ply
- или на Debian Linux:
apt install python-llvmpy python-ply
Вам все еще нужно написать крошечный компилятор (вы можете использовать Библиотека) и свяжите его с вызовами JIT LLVM (см. Engine выполнения LLVM), но этот подход может быть более эффективным (сгенерированный код намного ближе к реальному коду ЦП) и мультиплатформа По сравнению с тюрьмой .NET.
LLVM готов к использованию Оптимизация компилятора Инфраструктура, в том числе множество модулей на сцене оптимизатора, и крупное сообщество пользователей и разработчиков.
Также посмотрите здесь: http://gmarkall.github.io/tutorials/llvm-cauldron-2016
PS: Если вы заинтересованы в этом, я могу помочь вам с компилятором, внесли свой вклад в руководство моего проекта параллельно. Но это не будет Jumpstart, эта тема тоже новая для меня.