Click here to Skip to main content
Click here to Skip to main content

muParserSSE

By , 4 Sep 2011
Rate this:
Please Sign up or sign in to vote.
muparsersse.jpg

Introduction

muParserSSE is an mathematical expression parser able to compile a mathematical expression into machine code for 32 bit Intel processors at runtime. It will take an expression as well as variable definitions as its input and return the pointer to a just in time compiled function made up of fast SSE instructions. You can extend it with custom callback functions and operators. muParserSSE is based on asmjit, a just in time compiler written by Petr Kobalicek and the original muParser project.  

Table of Contents

  1. Introduction
  2. Table of contest
  3. Motivation
  4. Features 
  5. Implementation details 
  6. The parser interface
  7. Benchmarks
  8. Credits
  9. History

Motivation

The original muParser works by translating a mathematical expression into an intermediate bytecode representing its reverse polish notation. Successive evaluations then interpret this bytecode instead of reparsing the whole expression. muParser does a pretty good job performance wise. According to my benchmarks, it's the fastest C++ math parser library currently available. However, this is just my benchmark and if you prefer an independent benchmark, you will see all math parsers bringing more or less the same performance. There isn't really much of a difference. Until recently, this was good enough for me. But then, I found an article on CodeProject describing the use of just in time code generation to create a Fast Polymorphic Math Parser. Together with the article, the author provides a very simplistic parser. Rather spartanic in terms of features but good enough to prove the point that a just in time compiled expression could significantly outperform muParser. 

So here is my problem: Eventually someone will create a parser library based on just in time compiled code with a similar functionality as muParser and claim it's outperforming it by a factor of 10 or above. I admit I wouldn't like that. muParser doesn't have to be the fastest parser available, it has to be stable, maintainable, reliable and portable. However muParser should not be significantly slower than any other math parser either (except it's a fork of muParser). So basically I had to look into using just in time code generation for muParser myself. The other thing  I should mention is that I was looking for a project where I could use assembly language and using the asmjit compiler requires assembly language by definition. So the idea of writing muParserSSE the lightweight ultrafast version of muParser was born. I really wish I could have told you that I have a cool application requiring such a fast parser but in fact I don't, I wrote it for educational purposes and for the purpose of learning assembly language. If you actually use it in a project, please let me now.

Features  

Since this is a fork of muParser, its interface and features are pretty similar. However I had to remove some of muParsers more "esotheric" features in order to get the work done. A complete implementation just would have taken too much time and muParser has some features that are rarely used anyway. On the other side, I added some operators in order to expose as much of the SSE instructions to the user as possible. The following table compares muParserSSE with my other derivatives of muParser. A detailed description of the differences is listed below:   

Parser   Data types Precision User defined
operators
User defined
functions
muParser fail_ok.png(1) ok.png fail_ok.png(2) fail.png double ok.png ok.png ok.png ok.png ok.png ok.png 10000000
muParserSSE fail.png ok.png fail.png fail.png float ok.png ok.png ok.png fail.png max. 5 ok.png 20000000 -
100000000
muParserX  ok.png ok.png ok.png ok.png double ok.png ok.png ok.png ok.png ok.png fail.png 57000
Table 1: Feature comparison with other derivatives of muParser. (* Average performance calculated using this set of expressions; (1) muParser comes with an implementation for complex numbers but this is rather limited and more of a hack; (2) muParser can define strings but only as constants.)

The current release of muParserSSE contains a DLL and Project files for 32 bit Windows. Since both asmjit and muParser are running on Linux compiling the library under Linux should be possible but hasn't been tested. The same applies to OSX. Natively compiling this library for a 64 bit system is not possible. The following features are supported by muParserSSE:  

  • Extensible with custom operators (binary, infix or postfix)
  • Extensible with custom functions with up to 5 Parameters
  • Support for an unlimited number of variables and constants
  • No limit on expression complexity
  • Reads binary, hexadecimal values from expressions and can be extended to read user defined values as well
  • Supports a large variety of predefined operators, functions and constants
  • No external dependencies (asmjit is included in the archive)
  • Evaluation is using fast SSE instructions for improved performance
The following features are present in the original muParser library but were removed in order to speed up the development:
  • Data type is float rather than double as used by the original muParser
  • No assignment operators
  • Neither string constants nor string parameters to functions are supported 
  • No callbacks for functions with unlimited number of parameters
  • Not platform independent (technically impossible when using a just in time compiler)

You can extend the parser with custom constants but the following set of constants is already predefined:

  • The eulerian number with:
    e = 2.718281828459045235360287
  • Pi, the mathematical constant equal to a circle's circumference divided by its diameter.
    pi = 3.141592653589793238462643

The following list contains functions predefined by muParserSSE. More functions or operators can be added by providing custom callbacks.  

  • Triginometric functions and binary operators:
    sin, cos, tan, asin, acos, atan, sinh, cosh, tanh, asinh, acosh, atanh
  • Exponential and logarithmic functions:
    log2, log10, ln, exp
  • Other functions:
    abs, rint, sign, ite
  • Standard operators:
    "+", "-", "*", "/", "^"
  • Boolean operators:
    "==", "!=", ">", "<", "<=", ">="
  • Min/max operators:
    "<?", ">?"

Finally the following list contains the unary operators defined by muParserSSE. Unary operators can be either postfix operators or infix operators. Postfix operators can be used to easily distinguish value quantities (unit postfixes), whilst the only infix operator that is defined by default is the sign operator:

  • Postfix operators:
    "{n}", "{mu}", "{m}", "{k}", "{G}", "{M}"
  • Infix operators:
    "-" 

Implementation Details

Before starting to implement the just in time compiler engine, I had to find out how it could be implemented best. Ok I admit it: I had to find out how if I could implement it at all in a reasonable amount of time without having any prior experience in assembly language and I had to find an existing just in time compiler engine that I could use for this purpose. So the first thing was learn a bit of assembly and the second was to find an existing jit compiler that is easy to use. Here is what I found out:

CPU's implementing SSE, the Streaming SIMD Extension provides a set of 8 registers useable for fast mathematical operations with floating point numbers. Their main purpose is to allow performing multiple operations at once by packing up to 4 numbers into the registers and operating simultaneously with all 4 values. However the SSE instruction set also contains instructions for working with single floating point numbers. These instructions are prefixed with "ss" since they operate with single scalar values. This is the instruction subset predominantly used by this library (i.e. movss, divss, mulss, addss, subss, ...).

Regarding the just in time compiler issue: Since writing my own was totally out of the question, I had to look for existing projects. I choose asmjit because it has no external dependencies, is working on 32 and 64 bit systems, and it is published under MIT-Licence and of course, it is really easy to use.

The Reverse Polish Notation

With basic assembly knowledge and the proper tools, it's time to dig a bit deeper into the problem. In order to understand how muParserSSE works, you first need to understand what a reverse polish notation is. Reverse Polish notation (or just RPN) is a mathematical notation wherein every operator follows all of its operands. Got that? Probably not so let's explain it with a bit more detail. The way a mathematical expression is normally presented is called the infix notation. You probably didn't know that but believe me if you have ever attended a math lesson you have seen an expression in infix notation. Operators are written between the operands and you can use parentheses in order to define an evaluation order.

A Simple Expression

In order to demonstrate how the evaluation works, let's look at sample expressions. Our first sample is a simple expression without functions and with few binary operators:

expr0.png

Nothing special about that. Infix notation is a "human friendly" way to write an expression. Unfortunately to calculate it using a computer, one needs a "computer friendly" way to write the expression and the reverse polish notation (or postfix notation) is just that. The RPN of the sample above looks like:

expr0_rpn.png

Explaining how to translate infix notation into postfix notation is beyond the scope of this article but if you are interested, have a look at the Shunting-yard algorithm. For now, just let's assume you already have the RPN of your expression. The expression is evaluated from left to right. Each value or variable is pushed to a stack. If an operator is found, it is executed taking the two uppermost values from the stack as its arguments and then the result is pushed back to the stack. Let's assume our calculation stack is represented by an array called s and we parse the RPN from left to right. The following scheme shows the operations needed to compute the final result of the expression given above. The reverse Polish notation is on the left side written in up down direction, the right side shows the associated operation in the calculation array:

a : s[0] = a
1 : s[1] = 1
2 : s[2] = 2
+ : s[1] = s[1] + s[2]
b : s[2] = b
* : s[1] = s[1] * s[2]
+ : s[0] = s[0] + s[1]
3 : s[1] = 3
- : s[0] = s[0] - s[1]

The final result is located at s[0] ready for retrieval. A straightforward implementation would allocate an array for s and compute all the necessary steps pretty much like shown above. This is what muParser is doing. So how can this be translated into assembly language for usage with asmjit? First of all, it's gotta be fast so I intend to use the SSE instruction set. CPU's with SSE support provide 8 additional registers on 32 bit machines. These are: xmm0, xmm1, .., xmm7. The RPN interpreter could be implemented using an array for storing the temporary values much like above. The values could then be loaded into the SSE registers for applying the mathematical operations (i.e. addition) and the result could then be moved back to the stack array. It would require only 2 SSE registers and a lot of data movement. All in all, not very efficient! A better solution would be to use the SSE registers as much as possible. So why not use the registers directly as the calculation stack? There would be no memory allocations, no data movements. It would be very efficient. So let's look at pseudo assembly code using SSE assembly instructions for computing our sample expression:

a : movss xmm0, a
1 : movss xmm1, 1
2 : movss xmm2, 2
+ : addss xmm1, xmm2
b : movss xmm2, b
* : mulss xmm1, xmm2
+ : addss xmm0, xmm1
3 : movss xmm1, 3
- : subss xmm0, xmm1

This is pretty much a direct translation of the operations shown above just with different  syntax. Keep in mind that this is only pseudo assembly code and some details were omitted in order to make it easier to understand. You cannot feed this directly into a compiler (although it's close to what you could write using inline assembly)! To explain it a bit: movss is an instruction moving a floating point value into an SSE register. The instructions addss, mulss and subss perform addition, multiplication and subtraction using the values in the given registers as their input and storing the result in the register used as the first argument. Once the calculation is done, the final result would be located in the register xmm0 ready for retrieval. Let's have a look at how this would look like in memory. For the following animation, we assume a=1 and b=2:

expr1.gif
Image 1: Schematic memory and register usage during evaluation of the expression a+((1+2)*b)-3.

The entire calculation can be performed exclusively by using SSE registers (xmm0..xmm2). Creating this set of instructions on the fly from a given RPN using asmjit is no big deal. Should it really be that easy? There are only 8 SSE registers given the approach outlined above will this be enough to deal with any expression? Let's look at another sample.

A Slightly More Complex Example

The next expression is slightly more complex. This expression doesn't have functions either and still just uses basic binary operators only. It uses a lot of parenthesis though in order to enforce a certain evaluation order. First let's look at the expression:

expr1.png

and its RPN:

expr1_rpn.png

Translating this expression into pseudo assembly using the same approach as for expression 1 would yield the following code:

1 : movss xmm0, 1
2 : movss xmm1, 2
3 : movss xmm2, 3
4 : movss xmm3, 4
5 : movss xmm4, 5
6 : movss xmm5, 6
7 : movss xmm6, 7
8 : movss xmm7, 8
a : movss xmm8, a     ; we don't have a register xmm8!
* : mulss xmm7, xmm8  ; we don't have a register xmm8!
+ : addss xmm6, xmm7
+ : addss xmm5, xmm6
+ : addss xmm4, xmm5
+ : addss xmm3, xmm4
+ : addss xmm2, xmm3
+ : addss xmm1, xmm2
+ : addss xmm0, xmm1

Looks great, except it does not work that way! There are only 8 SSE registers available but we would need 9 in order to evaluate this expression. So it's obvious that given an arbitrarily complex expression there is no way to store all values in SSE registers. We have to find another solution!

How about this: The lowermost 6 registers (xmm0, xmm1, ..., xmm5) are used for storing values directly. They serve as the calculation stack. If a value needs to be stored at a higher position, it's stored on the CPU stack instead. For instance, if all 6 registers are occupied and another value needs to be pushed to the calculation stack 4 additional bytes are allocated on the CPU stack and the value is stored at this location instead. The two remaining SSE registers (xmm6 and xmm7) are used for performing operations with values stored on the CPU stack.

expr1.gif
Image 2:  Schematic memory and register usage during evaluation of the expression 1+(2+(3+(4+(5+(6+(7+(8*a))))))).

Image 2 shows how even complex expressions can be evaluated using a combination of SSE register commands and stack allocations. Extending this mechanism to support function calls is relatively easy. This library supports only callbacks with the cdecl calling convention. Arguments are pushed to the stack from right to left and the calling function has to clean up the stack afterwards.

The Parser Interface

The following section gives an overview over the DLL interface exposed by muParserSSE. The interface of this library is very similar to the interface of the original muParser DLL.

Adding the Library to your Projects

In order to use the library, you need to add the DLL named muParserSSE32.dll and the header file named "muParserSSE.h" to your project. Using the DLL is the only way to use this parser with MSVC6 or languages other than C++. The DLL has an interface that exports all of its functions as plain C style functions. The following files are required:

  • muParserSSE.h
  • muParserSSE.lib
  • muParserSSE.dll

Include the header file in your project and add the lib file to the project resources. For more details on using DLLs, consult the manual of your IDE.

Parser Initialization / Deinitialization

Before using the parser, it's necessary to create a new instance handle. You can create as many different instance handles as you like. Internally, each handle will reference a different parser object (a different expression). After using the parser, you should release any parser handle created by mecInit() with a call to mecRelease(handle).

#include "muParserSSE.h"
// ...

mecParserHandle_t hParser;
hParser = mecCreate();  // Create a new handle

// ...

mecRelease(hParser);    // Release an existing parser handle

Setting the Expression

Setting the expression when using the DLL requires a valid parser handle and a pointer to null terminated string containing the expression.

const char szExpr = "sin(3*pi)";
mecSetExpr(hParser, szLine);

Evaluating an Expression

Unlike muParser, muParserSSE can't directly evaluate the expression. You have to compile the expression first. In order to compile the expression, use the mecCompile function. It will return a pointer to the evaluation function. In order to evaluate the expression, you must call this function.

mecEvalFun_t pFunEval = NULL;

// Compile the expression and get the pointer to the
// just in time compiled eval function
pFunEval = mecCompile(hParser);

// calculate the expression
float fVal = 0;
if (pFunEval!=NULL)
  fVal = pFunEval();
warning It is crucial to know that the just in time compiled functions remains valid only as long as you do not change the expression or any variables. If you release the parser handle, it gets invalid too. Accessing an invalid evaluation function will inevitably lead to a crash in your software! Just don't mess with the parser handle after having compiled the function!

Defining Variables

Custom variables can be defined either explicitly in the code by using the DefineVar function or implicitly by the parser. Implicit declaration will call a variable factory function provided by the user. The parser is never the owner of its variables. So you must take care of their destruction in case of dynamic allocations. The general idea is to bind every parser variable to a C++ variable. For this reason, you have to make sure the C++ variable stays valid as long as you have a parser object depending on it. Only variables of type float can be used as parser variables.

Explicitly defining variables

Explicitly in this context means you have to do add the variables manually in your application code. So you must know in advance which variables you intend to use. If this is not the case, have a look at the section on Implicit creation of new variables. In order to define variables, use the mecDefineVar function. The first parameter is a valid parser handle, the second the variable name, and the third a pointer to the associated C++ variable.

float fVal=0;
mecDefineVar(hParser, "a", &fVal);
warning Defining a variable will invalidate any existing compiled function so you need to recompile the function after defining new variables! It's important to understand that you should never use mecDefineVar for changing the value of an existing variable! Change the variable via the pointer submitted as the last parameter of mecDefineVar. The compiled function will access variables directly using their address!

Implicit creation of new variables

Implicit declaration of new variables is only possible by setting a factory function. Implicit creation means every time the parser finds an unknown token at a position where a variable could be located, it creates a new variable with that name automatically. The necessary factory function must be of type:

typedef mecFloat_t* (*mecFacFun_t)(const mecChar_t*, void*);

The following code is an example of a factory function. The example does not use dynamic allocation for the new variables although this would be possible too. But when using dynamic allocation, you must keep track of the variables allocated implicitly in order to free them later on.

// Factory function for creating new parser variables
// This could as well be a function performing database queries.

mecFloat_t* AddVariable(const mecChar_t* a_szName, void *pUserData)
{
  static mecFloat_t afValBuf[PARSER_MAXVARS];  // I don't want dynamic allocation here
  static int iVal = 0;                         // so i used this buffer

  printf("Generating new variable \"%s\" (slots left: %d)\n", 
		a_szName, PARSER_MAXVARS-iVal);

  afValBuf[iVal] = 0;
  if (iVal>=PARSER_MAXVARS-1) 
  {
     printf("Variable buffer overflow.");
     return NULL;
  }

  return &afValBuf[iVal++];
}

In order to add a variable factory, use the mecSetVarFactory functions. Without a variable factory, each undefined variable will cause an undefined token error. Factory functions can be used to query the values of newly created variables directly from a database.

mecSetVarFactory(hParser, AddVariable);

Defining Constants

Like variables, constants have to be of type float. Originally muParser was using constants as a way to access their values faster in its intermediate bytecode. In muParserSSE, there is no performance gain from using constants but the function remains for practical purposes. The names of user defined constants may contain only the following characters: 0-9, a-z, A-Z, _, and they may not start with a number. Violating this rule will raise a parser error.

// Define value constants _pi
mecDefineConst(hParser, "_pi", (float)PARSER_CONST_PI);  

Defining Functions

The parser allows using custom callback functions with up to 5 parameters. In order to define a parser callback function, you need to specify its name, a pointer to your static callback function, and an optional flag indicating if the function is volatile. Volatile functions are functions that should not be optimized since they may return different values even when fed with the same input (such as the rnd function). The static callback functions must be either one of the following types:

// function types for calculation
typedef mecFloat_t (*mecFun0_t)(); 
typedef mecFloat_t (*mecFun1_t)(mecFloat_t); 
typedef mecFloat_t (*mecFun2_t)(mecFloat_t, mecFloat_t); 
typedef mecFloat_t (*mecFun3_t)(mecFloat_t, mecFloat_t, mecFloat_t); 
typedef mecFloat_t (*mecFun4_t)(mecFloat_t, mecFloat_t, mecFloat_t, mecFloat_t); 
typedef mecFloat_t (*mecFun5_t)
	(mecFloat_t, mecFloat_t, mecFloat_t, mecFloat_t, mecFloat_t); 

The callback functions must be bound to the parser by using either one of the following functions:

// Add an function with a fixed number of arguments
mecDefineFun1(hParser, "fun1", pCallback1, false);             
mecDefineFun2(hParser, "fun2", pCallback2, false);             
mecDefineFun3(hParser, "fun3", pCallback3, false);             
mecDefineFun4(hParser, "fun4", pCallback4, false);             
mecDefineFun5(hParser, "fun5", pCallback5, false);      

Defining Parser Operators

The parser is extensible with different kinds of operators: prefix operators, infix operators and binary operators.

  • Postfix operators are operators that succeed values. For instance, the factorial operator (a! = a*(a-1)...*2*1). Another application for postfix operators is their use as multipliers that can be used for implementing units.
  • Infix operators are operators like the unary minus (sign operator).
  • Binary operators can be defined in order to supplement built-in binary operators. When defining them, you need to specify two additional parameters. The operator priority and the operator associativity.

Unary operators

Both postfix and infix operators take callback functions of type mecFun1_t like the following:

float MyCallback(float fVal) 
{
  return fVal/1000.0; 
}

For defining postfix operators and infix operators, you need a valid parser instance handle, an identifier string, and an optional third parameter marking the operator as volatile (non optimizable). In order to bind your callbacks to the parser, use the mecDefineInfixOprt and mecDefinePostfixOprt functions:

// Define an infix operator
mecDefineInfixOprt(hParser, "!", MyCallback);

// Define a postfix operators
mecDefinePostfixOprt(hParser, "M", MyCallback);

Binary operators

muParserSSE has 13 built-in binary operators. Sometimes it might be necessary to have additional custom binary operators. Examples are shl or shr, the "shift left" and "shift right" operators for integer numbers. In order to add user defined operators, you need to assign a name, a callback function of type mecFun2_t, a priority for each new binary operator. You are not allowed to overload! Let's consider the following callback function which should be assigned to a binary operator:

float MyAddFun(float v1, float v2) 
{
  return v1+v2; 
}

For the definition of binary operators, you need at least six parameters:

  1. A valid parser handle
  2. A string used as the operator identifier
  3. A pointer to a callback function
  4. An integer value determining the operator priority
  5. The operator associativity which can be either one of the following constants:
    • mecOPRT_ASCT_LEFT
    • mecOPRT_ASCT_RIGHT
  6. An integer flag; If this flag is 1, the operator is assumed to be volatile.

Having defined a proper operator callback function, you can add the binary operator with the following code:

mecDefineOprt(hParser, "add", MyAddFun, 0, mecOPRT_ASCT_LEFT, 0);

The Priority value must be greater or equal than zero (lowest possible priority). It controls the operator precedence in the expression. For instance, if you want to calculate the expression 1+2*3^4 in a mathematically correct sense, you have to make sure that addition has a lower priority than multiplication which in turn has a lower priority than the power operator. When adding custom binary operators, the most likely cases are that you assign an operator with either a very low priority of 0 (like and, or, xor) or a high priority that is larger than 6 (the priority of the power operator ^). By assigning priority values already used by built-in operators, you might introduce unwanted side effects. To avoid this and make the order of calculation clear, you must use brackets in these cases. Otherwise, the order will be determined by the expression parsing direction which is from left to right.

Example A: Priority of shl equals priority of an addition; the order of the execution is from left to right.

1 + 2 shl 1 => (1 + 2) shl 1
2 shl 1 + 1 => (s shl 1) + 1

Example B: Priority of shl is higher than the that of addition; shl is executed first.

1 + 2 shl 1 => 1 + (2 shl 1)
2 shl 1 + 1 => (2 shl 1) + 1

Querying Parser Variables

Keeping track of all variables can be a difficult task. For simplification, the parser allows the user to query the variables defined in the parser. There are two different sets of variables that can be accessed:

  • Variables defined in the parser
  • Variables used in the current expression

Since the usage of the necessary commands is similar, the following example shows querying the parser variables only:

void ListExprVar(mecParserHandle_t a_hParser)
{
  mecInt_t iNumVar = mecGetVarNum(a_hParser),
           i = 0;

  if (iNumVar==0)
  {
    printf("Expression dos not contain variables\n");
    return;
  }

  printf("\nExpression variables:\n");
  printf("---------------------\n");
  printf("Expression: %s\n", mecGetExpr(a_hParser) );
  printf("Number: %d\n", iNumVar);
  
  for (i=0; i<iNumVar; ++i)
  {
    const mecChar_t* szName = 0;
    mecFloat_t* pVar = 0;

    mecGetVar(a_hParser, i, &szName, &pVar);
    printf("Name: %s   Address: [0x%x]\n", szName, (long long)pVar);
  }
}

For querying the variables used in the expression, exchange mecGetVarNum(...) with mecGetExprVarNum(...) and mecGetVar(...) with mecGetExprVar(...). Due to the use of a temporary internal static buffer for storing the variable name in the DLL version, this DLL-function is not thread safe.

Querying Parser Constants

Querying parser constants is similar to querying variables and expression variables. Due to the use of a temporary internal static buffer for storing the variable name in the DLL version, this DLL-function is not thread safe. The following sample shows how to query parser constants:

void ListConst(mecParserHandle_t a_hParser)
{
  mecInt_t iNumVar = mecGetConstNum(a_hParser),
          i = 0;

  if (iNumVar==0)
  {
    printf("No constants defined\n");
    return;
  }

  printf("\nParser constants:\n");
  printf("---------------------\n");
  printf("Number: %d", iNumVar);

  for (i=0; i<iNumVar; ++i)
  {
    const mecChar_t* szName = 0;
    mecFloat_t fVal = 0;

    mecGetConst(a_hParser, i, &szName, &fVal);
    printf("  %s = %f\n", szName, fVal);
  }
}

Removing Variables or Constants

Removing variables and constants can be done all at once using mecClearVar and mecClearConst. Additionally, variables can be removed by name using mecRemoveVar. Since the parser never owns the variables, you must take care of their release yourself if they were dynamically allocated. If you need to browse all the variables for that purpose, have a look at the chapter explaining how to query parser variables.

// Remove all constants
mecClearConst(hParser);

// remove all variables
mecClearVar(hParser);

// remove a single variable by name
mecRemoveVar(hParser, "a");

Error Handling

In order to detect errors, you can set an error handler as a callback function. The program will then automatically jump into the error handler in case of any problems. Once an error is detected, you can use the following functions in order to get detailed information:

  • mecGetErrorMsg() - returns the error message.
  • mecGetExpr() - returns the current expression (if an expression is set)
  • mecGetErrorToken() - returns the token associated with the error (if applicable)
  • mecGetErrorPos() - returns the current position in the expression (if applicable)
  • mecGetErrorCode() - returns the error code.

The following table lists the parser error codes. The first column contains the constant used for the error, the second column lists the numeric value assigned to this constant and the third column contains the error description.

Constant Value Description
mecERR_UNEXPECTED_OPERATOR 0 Unexpected binary operator found
mecERR_UNASSIGNABLE_TOKEN 1 Token can't be identified
mecERR_UNEXPECTED_EOF 2 Unexpected end of formula (example: "2+sin(")
mecERR_UNEXPECTED_COMMA 3 An unexpected comma has been found (example: "1,23")
mecERR_UNEXPECTED_ARG 4 An unexpected argument has been found
mecERR_UNEXPECTED_VAL 5 An unexpected value token has been found
mecERR_UNEXPECTED_VAR 6 An unexpected variable token has been found
mecERR_UNEXPECTED_PARENS 7 Unexpected parenthesis, opening or closing
unused 8 - 10 unused
mecERR_MISSING_PARENS 11 Missing parens. (example: "3*sin(3")
mecERR_UNEXPECTED_FUN 12 Unexpected function found (example: "sin(8)cos(9)")
unused 13 unused
mecERR_TOO_MANY_PARAMS 14 Too many function parameters
mecERR_TOO_FEW_PARAMS 15 Too few function parameters (example: "ite(1<2,2)")
unused 16 - 17 unused
mecERR_INVALID_NAME 18 Invalid function, variable or constant name.
mecERR_BUILTIN_OVERLOAD 19 Trying to overload built-in operator
mecERR_INVALID_FUN_PTR 20 Invalid callback function pointer
mecERR_INVALID_VAR_PTR 21 Invalid variable pointer
mecERR_NAME_CONFLICT 22 Name conflict
mecERR_OPT_PRI 23 Invalid operator priority
mecERR_DOMAIN_ERROR 24 Catch division by zero, sqrt(-1), log(0) (currently unused)
mecERR_DIV_BY_ZERO 25 Division by zero (currently unused)
mecERR_GENERIC 26 Generic error
mecERR_INTERNAL_ERROR 27 Internal error of any kind.

Since dynamic libraries with functions exported in C-style can't throw exceptions, the DLL version provides the user with a callback mechanism to raise errors. Simply add a callback function that does the handling of errors. Additionally, you can query the error flag with mupError(). By calling this function, you will automatically reset the error flag!

// Callback function for errors
void OnError(mecParserHandle_t hParser)
{
  printf("\nError:\n");
  printf("------\n");
  printf("Message:  \"%s\"\n", mecGetErrorMsg(hParser));
  printf("Token:    \"%s\"\n", mecGetErrorToken(hParser));
  printf("Position: %d\n", mecGetErrorPos(hParser));
  printf("Errc:     %d\n", mecGetErrorCode(hParser));
}

// ...

// Set a callback for error handling
mecSetErrorHandler(OnError);


// If the next function raises an error the
// error handler is automatically called.
mecCompile(hParser);

// Before continuing you should test the error flag.
if (!mecError(hParser))
  printf("%f\n", fVal);

Example Code

The following snippet shows the minimal code necessary to set up muParserSSE. The application defines a parser variable named "x" and then calculates the expression "1+sin(x)".

#include "muParserSSE.h"

void OnError(mecParserHandle_t hParser)
{
  printf("\nError:\n");
  printf("------\n");
  printf("Message:  \"%s\"\n", mecGetErrorMsg(hParser));
  printf("Token:    \"%s\"\n", mecGetErrorToken(hParser));
  printf("Position: %d\n", mecGetErrorPos(hParser));
  printf("Errc:     %d\n", mecGetErrorCode(hParser));
}

int main(int argc, char* argv[])
{
  mecParserHandle_t hParser = mecCreate();
  mecEvalFun_t pFunEval = NULL;

  mecSetErrorHandler(hParser, OnError);
  
  // Define parser variables and bind them to C++ variables
  float x = 1;
  mecDefineVar(hParser, "x", &x);

  // Set the expression
  mecSetExpr(hParser, "1+sin(x)");

  // Compile the expression and get the pointer to the
  // just in time compiled eval function
  pFunEval = mecCompile(hParser);
  if (pFunEval==NULL)
    return -1;

  // Finally calculate the expression
  fVal = pFunEval();
  printf("Result: %2.2f\n", fVal);
  return 0;
}

Benchmarks

The whole point of creating muParserSSE was to improve evaluation performance. But making precise estimates over the gain in performance from muParserSSE is not easy. The performance can be an order of a magnitude better when the expression doesn't contain functions. It can be faster by factor 2 to 5 when functions are used and in some cases there is no benefit at all. There is still room for improvement in muParserSSE so I won't claim it's generating the fastest possible machine code. However it's significantly faster than math parsers based on interpretation rather than compilation and it's not slower than MathPresso the only other free math parser that uses a just in time compiler (to my knowledge) but since MathPresso currently does not support functions, it was omitted in the second diagram. The following parsers were used in the benchmark:

Based on expression interpretation:

  • fparser - Function Parser for C++ v4.2
  • MTParser - An extensible math expression parser with plug-ins
  • muParser - a fast math parser library

Based on compiling the expression

  • MathPresso - A Math expression parser from the author of asmjit
  • muParserSSE - A math expression compiler

In order to conduct the benchmarks, I set up a small application containing all of the math parsers with their source code. This was done in order to guarantee all use the same optimized compiler settings such as:

  • use of intrinsic functions
  • creation of SSE instructions
  • fast floating point model
  • omit frame pointer
  • inlining of all suitable functions
  • highest optimization level
  • no buffer safety checks

The results are shown in the following two diagrams, please note that the logarithmic scaling is used on the y-axis:


Image 3: Performance of different open source math parser for a set of random expressions without functions.


Image 4: Performance for a set of random expressions with functions.

Credits

Special thanks to Petr Kobalicek for writing the asmjit just in time compiler and making it available as open source. Writing muParserSSE wouldn't have been possible without asmjit!

History

V1.0 Initial Release

  • Initial release for 32 bit Windows based on asmjit 0.86

License

This article, along with any associated source code and files, is licensed under The MIT License

About the Author

iberg
Software Developer
Germany Germany
No Biography provided

Comments and Discussions

 
GeneralMy vote of 5 PinmemberTapirro8-Jan-13 9:14 
QuestionImpressive - a definite 5 PinmemberEspen Harlinn4-Sep-11 12:57 
AnswerRe: Impressive - a definite 5 PinmemberMauro Leggieri6-Sep-11 1:25 
AnswerRe: Impressive - a definite 5 Pinmemberiberg6-Sep-11 10:50 
GeneralRe: Impressive - a definite 5 PinmemberEspen Harlinn6-Sep-11 11:05 
GeneralVery nice. But there is a small problem with the precision of the result. [modified] Pinmemberwenzhuo8889-Sep-10 0:29 
GeneralRe: Very nice. But there is a small problem with the precision of the result. Pinmemberiberg9-Sep-10 1:01 
GeneralRe: Very nice. But there is a small problem with the precision of the result. Pinmemberwenzhuo88813-Sep-10 2:00 
GeneralRe: Very nice. But there is a small problem with the precision of the result. PinmemberRick York13-Oct-11 16:07 
GeneralMy vote of 5 PinmemberMarceloFabiano7-Aug-10 10:54 
GeneralOne Very Small Thing PinmemberRick York23-Jul-10 6:48 
GeneralRe: One Very Small Thing Pinmemberiberg23-Jul-10 9:53 
GeneralSuggested future application: evaluation of Content MathML PinmemberMJessick21-Jul-10 17:20 
GeneralRe: Suggested future application: evaluation of Content MathML [modified] Pinmemberiberg21-Jul-10 18:11 
GeneralAbsolutely Brilliant Pinmember Randor 21-Jul-10 13:24 
GeneralRe: Absolutely Brilliant Pinmemberiberg21-Jul-10 18:04 
GeneralMy vote of 5 PinmemberJF201520-Jul-10 21:54 
GeneralRe: My vote of 5 Pinmemberiberg21-Jul-10 17:53 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140415.2 | Last Updated 4 Sep 2011
Article Copyright 2010 by iberg
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid