[Программирование, .NET, Алгоритмы, C#, Математика] Компиляция математических выражений
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Привет. В этом очерке расскажу, как я реализовывал компиляцию математических (численных и логических) выражений в делегат при помощи Linq Expression.Навигация: Проблема · Правила компиляции · Компилятор · Дефолтные правила · Красивый API · Производительность · Примеры работы · Заключение · СсылкиЧто мы хотим?Мы хотим скомпилировать выражение в функцию от произвольного числа аргументов произвольного типа, причем не только численного, но и булевого. Например,
var func = "x + sin(y) + 2ch(0)".Compile<Complex, double, Complex>("x", "y");
Console.WriteLine(func(new(3, 4), 1.2d));
>>> (5.932039085967226, 4)
var func = "x > 3 and (a implies b)".Compile<int, bool, bool, bool>("x", "a", "b");
Console.WriteLine(func(4, false, true));
>>> True
Что у нас есть?Так как я делаю это в рамках существующей библиотеки символьной алгебры, то мы сразу перейдем к компиляции, уже имя парсер и дерево выражений.У нас есть базовый класс Entity и дерево наследников.
Entity
|
+--Operators
|
+--Sumf
|
+--Minusf
|
...
+--Trigonometry
|
+--Sinf
|
+--Cosf
|
...
+--Discrete
|
+--Andf
|
+--Lessf
|
Примерно так выглядит дерево типов. Дерево выражений - это просто граф, где дети ноды это операнды оператора/функции.Каждый тип либо абстрактный (служит лишь для обобщения типов), либо запечатанный. Последний - это как раз реальные операторы/функции/константы/другие сущности, которые встречаются в выражении (будь то плюс, синус, конъюкция, число, множество, и т. д.).Например, такопределен оператор суммы.Протокол компиляцииТак я назвал интерфейс/класс данных, который будет определять то, как именно преобразуются подтипы Entity в нативные структуры, операторы или функции. Так как мы не знаем, какие типы запросит пользователь в качестве входных и выходного, то пользователь должен будет предоставить эту информацию.Так он будет выглядеть:
public sealed record CompilationProtocol
{
public Func<Entity, Expression> ConstantConverter { get; init; }
public Func<Expression, Expression, Entity, Expression> BinaryNodeConverter { get; init; }
public Func<Expression, Entity, Expression> UnaryNodeConverter { get; init; }
public Func<IEnumerable<Expression>, Entity, Expression> AnyArgumentConverter { get; init; }
}
Важно: ConstantConverter, BinaryNodeConverter, и UnaryNodeConverter мы будем писать в конце статьи.Все эти лямбды наш компилятор будет вызывать внутри, когда будет преобразовывать унарные ноды, бинарные ноды, константные ноды, и множественные в Linq.Expression.То есть теперь мы знаем, по каким правилам мы будем преобразовывать каждую ноду. Настало время писать сам "компилятор", точнее алгоритм, который будет строить дерево.КомпиляторПрототип метода, который мы хотим написать, выглядит так:
internal static TDelegate Compile<TDelegate>(
Entity expr,
Type? returnType,
CompilationProtocol protocol,
IEnumerable<(Type type, Variable variable)> typesAndNames
) where TDelegate : Delegate
- Entity expr - это выражение с переменными, которое мы компилируем.
- Type? returnType - это возвращаемый тип. Мы не можем "вытащить" его из типа делегата, поэтому приходится передавать его отдельно.
- CompilationProtocol protocol - это тот самый протокол правил, по которым мы будем преобразовывать каждый узел выражения
- IEnumerable<(Type type, Variable variable)> typesAndNames - это набор пар тип-переменная, которые пользователь хочет передать в свой делегат. Например, если вместо x мы хотим подставлять целочисленное, а вместо y передавать комплексное, мы напишем new[] { (typeof(int), "x"), (typeof(Complex), "y") }
А вот и имплементация метода:
internal static TDelegate Compile<TDelegate>(Entity expr, Type? returnType, CompilationProtocol protocol, IEnumerable<(Type type, Variable variable)> typesAndNames) where TDelegate : Delegate
{
// для каждого поддерева есть локальная переменная, которую мы держим здесь
var subexpressionsCache = typesAndNames.ToDictionary(c => (Entity)c.variable, c => Expression.Parameter(c.type));
// это параметры функции: те переменные, которые будут передаваться в нее, а не создаваться локально
var functionArguments = subexpressionsCache.Select(c => c.Value).ToArray(); // copying
// а это локальные переменные, мы их сохраняем в этом списке
var localVars = new List<ParameterExpression>();
// это список инструкций-присваиваний (о нем позже)
var variableAssignments = new List<Expression>();
// по всем данным строим дерево
var tree = BuildTree(expr, subexpressionsCache, variableAssignments, localVars, protocol);
// построив, создаем блок, в котором объявляем локальные переменные, используемые в дереве
var treeWithLocals = Expression.Block(localVars, variableAssignments.Append(tree));
// если пользователь передал returnType, то попытаемся явно скастить в него
Expression entireExpresion = returnType is not null ? Expression.Convert(treeWithLocals, returnType) : treeWithLocals;
// создаем лямбду с аргументами функции
var finalLambda = Expression.Lambda<TDelegate>(entireExpresion, functionArguments);
// компилируем
return finalLambda.Compile();
}
Главная его задача - создать необходимые контейнеры для кеша поддеревьев, локальных переменных, и некоторых других вещей. Самая интересная тут функция это BuildTree. Она будет строить дерево linq-выражения по Entity. Вот так выглядит ее прототип:
internal static Expression BuildTree(
Entity expr,
Dictionary<Entity, ParameterExpression> cachedSubexpressions,
List<Expression> variableAssignments,
List<ParameterExpression> newLocalVars,
CompilationProtocol protocol)
Подробнее про аргументы BuildTree
- Entity expr - выражение или подвыражение, по которому строить дерево.
- Dictionary<Entity, ParameterExpression> cachedSubexpressions - словарь закешированных поддеревьев (то есть тех, которые уже записаны в существующие локальные переменные).
- List<Expression> variableAssignments - список присвоений уникальных поддеревьев локальным переменным.
- List<ParameterExpression> newLocalVars - созданные функции BuildTree локальные переменные (для хранения результатов поддеревьев).
- CompilationProtocol protocol - правила, по которым мы преобразовываем ноду Entity в ноду Linq.Expression. Он остается неизменным и просто передается во все вызовы BuildTree.
А вот и самая главная функция - BuildTree:
internal static Expression BuildTree(Entity expr, ...)
{
// если поддерево встречалось, возвращаем переменную, в которую оно присвоено
if (cachedSubexpressions.TryGetValue(expr, out var readyVar))
return readyVar;
Expression subTree = expr switch
{
...
// если константа - прогоняем через ConstantConverter
// нашего протокола
Entity.Boolean or Number => protocol.ConstantConverter(expr),
// аналогичная логика с унарной, бинарной, и n-арными нодами
IUnaryNode oneArg
=> protocol.UnaryNodeConverter(BuildTree(oneArg.NodeChild, ...), expr),
IBinaryNode twoArg
=> protocol.BinaryNodeConverter(
BuildTree(twoArg.NodeFirstChild, ...),
BuildTree(twoArg.NodeSecondChild, ...),
expr),
var other => protocol.AnyArgumentConverter(
other.DirectChildren.Select(c => BuildTree(c, ...)),
expr)
};
// для этого поддерева создадим локальную переменную
var newVar = Expression.Variable(subTree.Type);
// добавим запись вида var5 = subTree
variableAssignments.Add(Expression.Assign(newVar, subTree));
// запомним, что для этого поддерева используется эта переменная
cachedSubexpressions[expr] = newVar;
// запомним, что мы вообще такую создали
newLocalVars.Add(newVar);
return newVar;
}
Я опустил большие куски для читаемости. Итак, в ней для каждого поддерева мы либо сразу возвращаем локальную переменную, отвечающую за это выражение, либо строим новое дерево Linq.Expression, запоминаем его в новую локальную переменную, и возвращаем ее.Собственно, это все, компилятор готов. Но мы не реализовали ни одного правила преобразования наших выражений в Linq.Expression, потому что эти правила мы ждем от пользователя. Но почему бы не предоставить какую-то возможность по умолчанию для встроенных типов?Остаток статьи будет о создании дефолтного протокола.Предположения (assumptions)Сам метод Compile<TDelegate>(Entity, Type?, CompilationProtocol, IEnumerable<(Type, Variable)>) будет предоставлен пользователю, но становится очевидно, что это очень длинная и неуклюжая конструкция, придется писать огромное количество логики, описывающее преобразование каждой ноды и константы, да и само объявление метода достаточно длинное и неочевидное.Поэтому я предполагаю, что по умолчанию мы можем предложить протокол компиляции, который позволит работать с некоторыми встроенными примитивами (bool, int, long, float, double, Complex, BigInteger).ConstantConverter:Это правило преобразует константу из Entity в Linq.Constant и выглядит следующим образом:
public static Expression ConverterConstant(Entity e)
=> e switch
{
Number n => Expression.Constant(DownCast(n)),
Entity.Boolean b => Expression.Constant((bool)b),
_ => throw new AngouriBugException("Undefined constant type")
};
Число преобразуем в число в зависимости от его типа, булеву константу безусловно в примитивный bool.Подробнее о DownCastЭта функция преобразует Entity.Number в какой-то из встроенных типов и реализован следующим образом:
private static object DownCast(Number num)
{
if (num is Integer)
return (long)num;
if (num is Real)
return (double)num;
if (num is Number.Complex)
return (System.Numerics.Complex)num;
throw new InvalidProtocolProvided("Undefined type, provide valid compilation protocol");
}
Возвращает object потому, что именно такой аргумент принимает Expression.Constant. А нам это на руку: иначе под еще каким типом объединить эти три?UnaryNodeConverter:Это поле протокола - делегат, преобразующий ноду с одним аргументом в Linq.Expression.
public static Expression OneArgumentEntity(Expression e, Entity typeHolder)
=> typeHolder switch
{
Sinf => Expression.Call(GetDef("Sin", 1, e.Type), e),
...
Cosecantf => Expression.Call(GetDef("Csc", 1, e.Type), e),
Arcsinf => Expression.Call(GetDef("Asin", 1, e.Type), e),
...
Arccosecantf => Expression.Call(GetDef("Acsc", 1, e.Type), e),
Absf => Expression.Call(GetDef("Abs", 1, e.Type), e),
Signumf => Expression.Call(GetDef("Sgn", 1, e.Type), e),
Notf => Expression.Not(e),
_ => throw new AngouriBugException("A node seems to be not added")
};
Я опустил некоторые большие блоки (весь код здесь). Итак, здесь мы рассматриваем возможные типы нашей ноды, и на каждую выбираем нужный оверлоад нужной функции. GetDef находит нужную функцию по имени.О GetDefСначала я думал загружать все нужные функции из модулей Math и Complex. Приходилось везде if-ать, в каких случаях Math, в каких Complex, в каких вообще BigInteger. А потом еще и оказывалось, что у Math нет int Pow(int, int), и получается приходилось кастить в любой непонятной ситуации.Поэтому я создал класс MathAllMethods(на T4), в котором сделал все нужные ноды для всех примитивов, поддерживаемых остальными дефолтными правилами.GetDef ищет нужный метод с данным числом аргументов и типом именно в этом классе. Это позволило сильно очистить код от мусора и красиво и кратко записать все обращения к нужным функциям по данным типам.BinaryNodeConverter:Это правило преобразует ноду с двумя аргументами в Linq.Expression.
public static Expression TwoArgumentEntity(Expression left, Expression right, Entity typeHolder)
{
var typeToCastTo = MaxType(left.Type, right.Type);
if (left.Type != typeToCastTo)
left = Expression.Convert(left, typeToCastTo);
if (right.Type != typeToCastTo)
right = Expression.Convert(right, typeToCastTo);
return typeHolder switch
{
Sumf => Expression.Add(left, right),
...
Andf => Expression.And(left, right),
...
Lessf => Expression.LessThan(left, right),
...
_ => throw new AngouriBugException("A node seems to be not added")
};
}
Тут есть upcast. Так как у нас могут встретиться два выражения разного типа, мы хотим найти наиболее примитивный тип, к которому кастятся оба операнда. Для этого каждому типу присвоим уровень. Я распределил так:
Complex: 10
double: 9
float: 8
long: 8
BigInteger: 8
int: 7
Если типы одинаковые, MaxType вернет один из них. Например, MaxType(int, int) -> int.Если у типа операнда A уровень выше, чем у типа операнда B, то B кастится в A. Например, MaxType(long, double) -> double.Если уровни одинаковые, а типы - нет, то находится ближайшая общая вершина, то есть любой такой тип, у которого уровень на один выше. Например, MaxType(long, float) -> double.Операнды, при необходимости, кастятся к выбранному типу, и далее мы просто находим нужный оверлоад или оператор. Например, для Sumf мы выберем Expression.Add, а для конъюкции, Andf, будет Expression.And.Осознаем.Отлично, мы определили все необходимые правила нашего протокола. Теперь, при создании мы сможем передать эти правила в нужные свойства протокола.Красивое APIНачали мы с метода, который в конечном API выглядит так:
public TDelegate Compile<TDelegate>(CompilationProtocol protocol, Type returnType, IEnumerable<(Type type, Variable variable)> typesAndNames) where TDelegate : Delegate
Он неудобный и требует много усилий. Но мы можем передавать наш дефолтный протокол И перегрузить этот метод для делегатов от одного аргумента, двух, трех, и так далее. Так как я уже присваиваюнаши правила свойствам протокола по умолчанию, то передавая протокол, мы просто создаем его инстанс. Со вторым чуть сложнее - я решил это генерацией кода с помощью T4 Text Template. Вот пример сгенерированного кода:
// указываем типы аргументов и выходной а здесь указываем соответствующие переменные из самого выражения
public Func<TIn1, TIn2, TIn3, TOut> Compile<TIn1, TIn2, TIn3, TOut>(Variable var1, Variable var2, Variable var3)
// делегат генерируем сами выходной тип известен new() так как создаем правила по умолчанию
=> IntoLinqCompiler.Compile<Func<TIn1, TIn2, TIn3, TOut>>(this, typeof(TOut), new(),
new[] { (typeof(TIn1), var1), (typeof(TIn2), var2) , (typeof(TIn3), var3) });
Он же в исходниках.Текст T4-темплейта для генерации
<# for (var i = 1; i <= 8; i++) { #>
public Func<<# for(var t=1;t<=i;t++){ #>TIn<#= t #>, <# } #>TOut> Compile<<# for(var t=1;t<=i;t++){ #>TIn<#= t #>, <# } #>TOut>(Variable var1<# for(var t=2; t<=i; t++){ #>, Variable var<#= t #><# } #>)
=> IntoLinqCompiler.Compile<Func<<# for(var t=1;t<=i;t++){ #>TIn<#= t #>, <# } #>TOut>>(this, typeof(TOut), new(),
new[] { (typeof(TIn1), var1)<# for(var t=2;t<=i;t++){ #>, (typeof(TIn<#= t #>), var<#= t #>) <# } #> });
<# } #>
Аналогичным образом создаем методы расширения. Вот пример сгенерированного:
public static Func<TIn1, TIn2, TOut> Compile<TIn1, TIn2, TOut>(this string @this, Variable var1, Variable var2)
=> IntoLinqCompiler.Compile<Func<TIn1, TIn2, TOut>>(@this, typeof(TOut), new(),
new[] { (typeof(TIn1), var1), (typeof(TIn2), var2) });
Теперь надо померить производительность.ПроизводительностьBenchNormalSimple - простая лямбда, объявленная прямо в коде.BenchMySimple - та же лямбда, но скомпилированная мною.BenchNormalComplicated - большая толстая лямбда с кучей идентичных поддеревьев, объявленная прямо в коде.BenchmyComplicated - та же лямбда, но скомпилированная мною.
| Method | Mean | Error | StdDev |
|----------------------- |-----------:|---------:|---------:|
| BenchNormalSimple | 189.1 ns | 3.75 ns | 5.83 ns |
| BenchMySimple | 195.7 ns | 3.92 ns | 5.50 ns |
| BenchNormalComplicated | 1,383.0 ns | 26.82 ns | 35.80 ns |
| BenchMyComplicated | 293.6 ns | 5.74 ns | 8.77 ns |
Простые штуки работают одинаково быстро, а там, где есть идентичные поддеревья, моя компиляция выигрывает. В общем-то предсказуемо, и ничего гениального тут нет.Бенчмарк тут.Примеры работы
var func = "sin(x)".Compile<double, double>("x");
Console.WriteLine(func(Math.PI / 2));
>>> 1
var func1 = "a > b".Compile<float, int, bool>("a", "b");
Console.WriteLine(func1(5.4f, 4));
Console.WriteLine(func1(4f, 4));
>>> True
>>> False
var cr = new CompilationProtocol()
{
ConstantConverter = ent => Expression.Constant(ent.ToString()),
BinaryNodeConverter = (a, b, t) => t switch
{
Sumf => Expression.Call(typeof(string)
.GetMethod("Concat", new[] { typeof(string), typeof(string) }) ?? throw new Exception(), a, b),
_ => throw new Exception()
}
};
var func2 = "a + b + c + 1234"
.Compile<Func<string, string, string, string>>(
cr, typeof(string),
new[] {
(typeof(string), Var("a")),
(typeof(string), Var("b")),
(typeof(string), Var("c")) }
);
Console.WriteLine(func2("White", "Black", "Goose"));
>>> WhiteBlackGoose1234
(Последний пример - пример того, как пользователь сам объявляет протокол вместо того, чтобы использовать существующий. И получает лямбду, которая конкатенирует строки).Заключение
- Linq.Expression по истине гениальная вещь.
- В этой небольшой статье мы, не особо напрягаясь, скомпилировали математическое выражение.
- Чтобы обеспечить произвольность типов, мы придумали протокол, по которому происходит трансляция выражения.
- Чтобы избежать написания одного и того же кода, мы предложили целый ряд оверлоадов с дефолтным протоколом, отлично работающим с целым рядом встроенных типов. Этот протокол автоматически апкастит типы в бинарных операторах и функциях до ближайшего общего типа, если есть необходимость.
Такой функционал можно использовать там, где хотелось бы в рантайме получать быстро-работающую математическую функцию из строки. Может быть, сообщество придумает еще какое-нибудь полезное применение. Кстати, компиляцию в рантайме я уже использовал в другом проекте, в котором динамическая компиляция вложенных циклов позволила избежать рекурсии (и сэкономить драгоценные наносекунды).Спасибо за внимание! Следующая статья возможно будет про символьные пределы или парсинг.Ссылки
- GitHub проекта AngouriMath, в рамках которого я и делал компиляцию
- Компиляция здесь
- Тесты компиляции можно посмотреть здесь
===========
Источник:
habr.com
===========
Похожие новости:
- [Open source, Программирование, Геоинформационные сервисы, Визуализация данных, Научно-популярное] Вычислительная геология и визуализация: пример Python 3 Jupyter Notebook
- [JavaScript, Программирование, Совершенный код, ReactJS] Удвойте скорость написания кода на React с помощью этих простых трюков (перевод)
- [Математика, Карьера в IT-индустрии, Читальный зал] История одного собеседования
- [Высокая производительность, Программирование, Алгоритмы, WebAssembly] Разгоняем JS-парсер с помощью WebAssembly (часть 3: SIMD)
- [.NET, C#] Пошаговая инструкция по настройке и использованию Gitlab CI + Visual Studio для сборки приложения .NET Framework
- [C++, C, Программирование микроконтроллеров, Компьютерное железо] Размещение кучи FreeRTOS в разделе CCMRAM для STM32
- [Программирование, Управление разработкой] Пишите библиотеки вместо сервисов (если это возможно) (перевод)
- [Разработка веб-сайтов, JavaScript, Программирование, ReactJS] React: основные подходы к управлению состоянием
- [Python, JavaScript, C#, Логические игры] 10 лучших игр по программированию, которые улучшат ваши навыки (перевод)
- [Занимательные задачки, Математика, Читальный зал] Заключённый использовал одиночную камеру для изучения математики. Сегодня он решает самые трудные уравнения в мире (перевод)
Теги для поиска: #_programmirovanie (Программирование), #_.net, #_algoritmy (Алгоритмы), #_c#, #_matematika (Математика), #_csharp, #_matematika (математика), #_kompiljatsija (компиляция), #_expression, #_angourimath, #_linq, #_.net, #_programmirovanie (
Программирование
), #_.net, #_algoritmy (
Алгоритмы
), #_c#, #_matematika (
Математика
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 08:15
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Привет. В этом очерке расскажу, как я реализовывал компиляцию математических (численных и логических) выражений в делегат при помощи Linq Expression.Навигация: Проблема · Правила компиляции · Компилятор · Дефолтные правила · Красивый API · Производительность · Примеры работы · Заключение · СсылкиЧто мы хотим?Мы хотим скомпилировать выражение в функцию от произвольного числа аргументов произвольного типа, причем не только численного, но и булевого. Например, var func = "x + sin(y) + 2ch(0)".Compile<Complex, double, Complex>("x", "y");
Console.WriteLine(func(new(3, 4), 1.2d)); >>> (5.932039085967226, 4) var func = "x > 3 and (a implies b)".Compile<int, bool, bool, bool>("x", "a", "b"); Console.WriteLine(func(4, false, true)); >>> True Entity
| +--Operators | +--Sumf | +--Minusf | ... +--Trigonometry | +--Sinf | +--Cosf | ... +--Discrete | +--Andf | +--Lessf | public sealed record CompilationProtocol
{ public Func<Entity, Expression> ConstantConverter { get; init; } public Func<Expression, Expression, Entity, Expression> BinaryNodeConverter { get; init; } public Func<Expression, Entity, Expression> UnaryNodeConverter { get; init; } public Func<IEnumerable<Expression>, Entity, Expression> AnyArgumentConverter { get; init; } } internal static TDelegate Compile<TDelegate>(
Entity expr, Type? returnType, CompilationProtocol protocol, IEnumerable<(Type type, Variable variable)> typesAndNames ) where TDelegate : Delegate
internal static TDelegate Compile<TDelegate>(Entity expr, Type? returnType, CompilationProtocol protocol, IEnumerable<(Type type, Variable variable)> typesAndNames) where TDelegate : Delegate
{ // для каждого поддерева есть локальная переменная, которую мы держим здесь var subexpressionsCache = typesAndNames.ToDictionary(c => (Entity)c.variable, c => Expression.Parameter(c.type)); // это параметры функции: те переменные, которые будут передаваться в нее, а не создаваться локально var functionArguments = subexpressionsCache.Select(c => c.Value).ToArray(); // copying // а это локальные переменные, мы их сохраняем в этом списке var localVars = new List<ParameterExpression>(); // это список инструкций-присваиваний (о нем позже) var variableAssignments = new List<Expression>(); // по всем данным строим дерево var tree = BuildTree(expr, subexpressionsCache, variableAssignments, localVars, protocol); // построив, создаем блок, в котором объявляем локальные переменные, используемые в дереве var treeWithLocals = Expression.Block(localVars, variableAssignments.Append(tree)); // если пользователь передал returnType, то попытаемся явно скастить в него Expression entireExpresion = returnType is not null ? Expression.Convert(treeWithLocals, returnType) : treeWithLocals; // создаем лямбду с аргументами функции var finalLambda = Expression.Lambda<TDelegate>(entireExpresion, functionArguments); // компилируем return finalLambda.Compile(); } internal static Expression BuildTree(
Entity expr, Dictionary<Entity, ParameterExpression> cachedSubexpressions, List<Expression> variableAssignments, List<ParameterExpression> newLocalVars, CompilationProtocol protocol)
internal static Expression BuildTree(Entity expr, ...)
{ // если поддерево встречалось, возвращаем переменную, в которую оно присвоено if (cachedSubexpressions.TryGetValue(expr, out var readyVar)) return readyVar; Expression subTree = expr switch { ... // если константа - прогоняем через ConstantConverter // нашего протокола Entity.Boolean or Number => protocol.ConstantConverter(expr), // аналогичная логика с унарной, бинарной, и n-арными нодами IUnaryNode oneArg => protocol.UnaryNodeConverter(BuildTree(oneArg.NodeChild, ...), expr), IBinaryNode twoArg => protocol.BinaryNodeConverter( BuildTree(twoArg.NodeFirstChild, ...), BuildTree(twoArg.NodeSecondChild, ...), expr), var other => protocol.AnyArgumentConverter( other.DirectChildren.Select(c => BuildTree(c, ...)), expr) }; // для этого поддерева создадим локальную переменную var newVar = Expression.Variable(subTree.Type); // добавим запись вида var5 = subTree variableAssignments.Add(Expression.Assign(newVar, subTree)); // запомним, что для этого поддерева используется эта переменная cachedSubexpressions[expr] = newVar; // запомним, что мы вообще такую создали newLocalVars.Add(newVar); return newVar; } public static Expression ConverterConstant(Entity e)
=> e switch { Number n => Expression.Constant(DownCast(n)), Entity.Boolean b => Expression.Constant((bool)b), _ => throw new AngouriBugException("Undefined constant type") }; private static object DownCast(Number num)
{ if (num is Integer) return (long)num; if (num is Real) return (double)num; if (num is Number.Complex) return (System.Numerics.Complex)num; throw new InvalidProtocolProvided("Undefined type, provide valid compilation protocol"); } public static Expression OneArgumentEntity(Expression e, Entity typeHolder)
=> typeHolder switch { Sinf => Expression.Call(GetDef("Sin", 1, e.Type), e), ... Cosecantf => Expression.Call(GetDef("Csc", 1, e.Type), e), Arcsinf => Expression.Call(GetDef("Asin", 1, e.Type), e), ... Arccosecantf => Expression.Call(GetDef("Acsc", 1, e.Type), e), Absf => Expression.Call(GetDef("Abs", 1, e.Type), e), Signumf => Expression.Call(GetDef("Sgn", 1, e.Type), e), Notf => Expression.Not(e), _ => throw new AngouriBugException("A node seems to be not added") }; public static Expression TwoArgumentEntity(Expression left, Expression right, Entity typeHolder)
{ var typeToCastTo = MaxType(left.Type, right.Type); if (left.Type != typeToCastTo) left = Expression.Convert(left, typeToCastTo); if (right.Type != typeToCastTo) right = Expression.Convert(right, typeToCastTo); return typeHolder switch { Sumf => Expression.Add(left, right), ... Andf => Expression.And(left, right), ... Lessf => Expression.LessThan(left, right), ... _ => throw new AngouriBugException("A node seems to be not added") }; } Complex: 10
double: 9 float: 8 long: 8 BigInteger: 8 int: 7 public TDelegate Compile<TDelegate>(CompilationProtocol protocol, Type returnType, IEnumerable<(Type type, Variable variable)> typesAndNames) where TDelegate : Delegate
// указываем типы аргументов и выходной а здесь указываем соответствующие переменные из самого выражения
public Func<TIn1, TIn2, TIn3, TOut> Compile<TIn1, TIn2, TIn3, TOut>(Variable var1, Variable var2, Variable var3) // делегат генерируем сами выходной тип известен new() так как создаем правила по умолчанию => IntoLinqCompiler.Compile<Func<TIn1, TIn2, TIn3, TOut>>(this, typeof(TOut), new(), new[] { (typeof(TIn1), var1), (typeof(TIn2), var2) , (typeof(TIn3), var3) }); <# for (var i = 1; i <= 8; i++) { #>
public Func<<# for(var t=1;t<=i;t++){ #>TIn<#= t #>, <# } #>TOut> Compile<<# for(var t=1;t<=i;t++){ #>TIn<#= t #>, <# } #>TOut>(Variable var1<# for(var t=2; t<=i; t++){ #>, Variable var<#= t #><# } #>) => IntoLinqCompiler.Compile<Func<<# for(var t=1;t<=i;t++){ #>TIn<#= t #>, <# } #>TOut>>(this, typeof(TOut), new(), new[] { (typeof(TIn1), var1)<# for(var t=2;t<=i;t++){ #>, (typeof(TIn<#= t #>), var<#= t #>) <# } #> }); <# } #> public static Func<TIn1, TIn2, TOut> Compile<TIn1, TIn2, TOut>(this string @this, Variable var1, Variable var2)
=> IntoLinqCompiler.Compile<Func<TIn1, TIn2, TOut>>(@this, typeof(TOut), new(), new[] { (typeof(TIn1), var1), (typeof(TIn2), var2) }); | Method | Mean | Error | StdDev |
|----------------------- |-----------:|---------:|---------:| | BenchNormalSimple | 189.1 ns | 3.75 ns | 5.83 ns | | BenchMySimple | 195.7 ns | 3.92 ns | 5.50 ns | | BenchNormalComplicated | 1,383.0 ns | 26.82 ns | 35.80 ns | | BenchMyComplicated | 293.6 ns | 5.74 ns | 8.77 ns | var func = "sin(x)".Compile<double, double>("x");
Console.WriteLine(func(Math.PI / 2)); >>> 1 var func1 = "a > b".Compile<float, int, bool>("a", "b"); Console.WriteLine(func1(5.4f, 4)); Console.WriteLine(func1(4f, 4)); >>> True >>> False var cr = new CompilationProtocol() { ConstantConverter = ent => Expression.Constant(ent.ToString()), BinaryNodeConverter = (a, b, t) => t switch { Sumf => Expression.Call(typeof(string) .GetMethod("Concat", new[] { typeof(string), typeof(string) }) ?? throw new Exception(), a, b), _ => throw new Exception() } }; var func2 = "a + b + c + 1234" .Compile<Func<string, string, string, string>>( cr, typeof(string), new[] { (typeof(string), Var("a")), (typeof(string), Var("b")), (typeof(string), Var("c")) } ); Console.WriteLine(func2("White", "Black", "Goose")); >>> WhiteBlackGoose1234
=========== Источник: habr.com =========== Похожие новости:
Программирование ), #_.net, #_algoritmy ( Алгоритмы ), #_c#, #_matematika ( Математика ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 08:15
Часовой пояс: UTC + 5