Итак, исходная реализация: предварительно разбираем выражение и строим синтаксическое дерево (AST, но в своей объектной модели). Далее при выполнении кода просто просим дерево посчитать значение выражения для текущего контекста выполнения (контекст включает текущую строку данных и область видимости блоков агрегации данных, например в матричной группировке).
Варианты решения
Первая попытка решения - вместо собственного AST формируем функцию вычисления значения, аргументом которой является контекст. Например есть выражение:(GetRecordValue(ctx,2) * 1024 + GetRecordValue(ctx,3))/3600/(Max(1, GetRecordValue(ctx,4))
Код построения функции даже проще кода построения AST. К сожалению, на каждом этапе построения функции приходится формировать лямбда функцию с аргументом контекста, что приводит к следующей результирующей функции:
ctx5 => (ctx4 => (ctx3 => (ctx2 => (ctx => GetRecordValue(ctx,2))(ctx2) * (ctx => 1024)(ctx2))(ctx3) + (ctx2 => (ctx => GetRecordValue(ctx,2))(ctx2))(ctx3))(ctx4) / (ctx4 => 3600))(ctx5) / (ctx2 => Max(ctx2 => 1, (ctx => GetRecordValue(ctx,4)(ctx2)))(ctx5)
Кроме того, для каждой лямбда функции (точнее для ее замыкания) создается класс. Несмотря на это, результат - примерно двукратный рост скорости - хорош, но недостаточно. Хотелось чтобы скорость выполнения была такая же как и для кода написанного на C#.
Выражения, появившиеся в .NET 3.5 - это аналог исходного решения, но если метод Compile использует IL Emit, можно ожидать существенно лучший результат.
Для проверки решения на практике реализуем два парсера - первый возвращает лямбда-функцию вычисления, второй - Expression.
NParsec
Проще всего сделать парсер с помощью библиотеки NParsec (последовательный порт parsec => jparsec => nparsec).Обобщенный код парсера занимает примерно 30 строк наглядного кода:
private static Parser<T> CreateParser<T>(Func<string, T> getVar, IDictionary<char,Map<T,T,T>> binaryOps, Map<string,T> parseValue, Map<T,T> unaryMinus) { var patCharExt = Patterns.IsChar(Char.IsLetter).Or(Patterns.IsChar('_')); var patVar = patCharExt.Seq(Patterns.InRange('0', '9').Or(patCharExt).Many()); var scannerVar = Scanners.IsPattern(patVar, "var"); var lexerVal = Lexers.Lex(scannerVar, Tokenizers.ForString); var operators = Terms.GetOperatorsInstance("+", "-", "*", "/", "(", ")", "^"); var ignored = Scanners.IsWhitespaces().Many_(); var lexeme = Lexers.Lexeme(ignored, operators.Lexer | Lexers.LexDecimal() | lexerVal).FollowedBy(Parsers.Eof()); var pPlus = GetOperator(operators, "+", binaryOps['+']); var pMinus = GetOperator(operators, "-", binaryOps['-']); var pMul = GetOperator(operators, "*", binaryOps['*']); var pDiv = GetOperator(operators, "/", binaryOps['/']); var pPower = GetOperator(operators, "^", binaryOps['^']); var pNeg = GetOperator(operators, "-", unaryMinus); var pVar = Terms.OnString((@from, len, data) => getVar(data)); var pNumber = Terms.OnDecimal((_, __, s) => parseValue(s)); var lazyExpr = new Parser<T>[1]; var pLazyExpr = Parsers.Lazy(() => lazyExpr[0]); var pTerm = pLazyExpr.Between(operators.GetParser("("), operators.GetParser(")")) | pNumber | pVar; var optable = new OperatorTable<T>() .Infixl(pPlus, 10).Infixl(pMinus, 10) .Infixl(pMul, 20).Infixl(pDiv, 20) .Infixl(pPower, 25).Prefix(pNeg, 30); var pExpr = Expressions.BuildExpressionParser(pTerm, optable); lazyExpr[0] = pExpr; return Parsers.ParseTokens(lexeme, pExpr.FollowedBy(Parsers.Eof()), "calculator"); } private static Parser<T> GetOperator<T>(Terms ops, string op, T v) { return ops.GetParser(op).Seq(Parsers.Return(v)); }
Парсеры для лямбда-функций и Expressions на основе обобщенного парсера выглядят как:
private static Parser<Func<double>> CreateLambdaParser(Func<string, Func<double>> getVar) { var binaryOps = new Dictionary<char, Map<Func<double>, Func<double>, Func<double>>> { {'+', (a, b) => () => a() + b()}, {'/', (a, b) => () => a()/b()}, {'-', (a, b) => () => a() - b()}, {'*', (a, b) => () => a()*b()}, {'^', (a, b) => () => Math.Pow(a(), b())} }; Map<Func<double>, Func<double>> unaryMinus = v => () => -v(); Map<string, Func<double>> parseDouble = s => { var v = double.Parse(s); return () => v; }; return CreateParser(getVar, binaryOps, parseDouble, unaryMinus); } private static Parser<Expression> CreateExpressionParser(Func<string, Expression> getVar) { var binaryOps = new Dictionary<char, Map<Expression, Expression, Expression>> { {'+', Expression.Add}, {'/', Expression.Divide}, {'-', Expression.Subtract}, {'*', Expression.Multiply}, {'^', (a,b) => Expression.Call(typeof(Math).GetMethod("Pow"), a, b)} }; Map<string, Expression> parseDouble = s => Expression.Constant(double.Parse(s)); return CreateParser(getVar, binaryOps, parseDouble, Expression.Negate); }
Ну и собственно:
Результаты замера производительности
Для замера скорости подойдет полином 20-й степени вида ((a1+1)*a2+1)*a3 + 1... Результат - примерно 200 кратное превосходство Expression - 120 секунд против 600 мс. Результат подозрительный, но причина в том что переменные a1..a20 - это по сути константы, так как компилятор выражений, обнаружив что в правой части выражения стоит константа, вероятно подставил значения в AST и произвел оптимизацию.Для получения достоверного результата я заменил константу на вызов функции и получил вполне достоверный результат 5.58c для лямбда-функций против 1.17с для выражений. А при включенной оптимизации компилятора C# - 2.27с против 0.26с. В результате имеем 10-кратный выигрыш при соизмеримой сложности кода.
Исходный код и тесты доступны по в репозитарии на bitbucket
Комментариев нет:
Отправить комментарий