Compiled Function Operation

The Wolfram System compiler generates a CompiledFunction expression that contains a sequence of simple instructions for evaluating a Wolfram Language computation. The compiled function expression also contains other information such as argument and result specifications, flags, error handlers, and version information. Since all of this information is stored in the expression, many of the tools for working with compiled functions can be written in the Wolfram Language.

The compiled function instructions can be executed by a virtual machine that is included in the Wolfram Language. This is based on an idealized register machine that uses temporary locations (registers) to store intermediate results as they are calculated.

You can get a good insight into how the instructions work with the CompiledFunctionTools` package. To use the package you have to load it.

Now you can use CompilePrint to show details of a compiled function.

In this computation you can see that the compiled function takes one real argument and returns a real argument. The argument is placed into register R0 and the result is found in register R3. The square of the argument is stored in register R1, and this is used twice. In addition, other information about runtime settings of the compiled function is displayed.

This example demonstrates a key part of the compiler: it maintains a type system. The type of the input is specified and the type of each intermediate result is computed, leading to the type of the result.

The type of arguments can be specified in the input. For example, to specify that the input is a complex number you would use Complex, as in the following.

In this example, the operations such as squaring and sin will use complex versions. This leads to a speed advantage since an optimized function for complex numbers is used rather than having to decide what should be done each time the function is called based on the actual arguments.

The compiler can also support working with vectors, matrices, and tensors. A simple example is shown in the following.

The compiler supports many Wolfram Language programming constructs such as Table, Map, Apply, Nest, NestList, Fold, and FoldList. The following example shows how local variables can be introduced with Module, and then an iteration run with Do.

Compiled functions such as these often run much faster than the uncompiled equivalents.

Type System

The Wolfram System compiler uses a type system to generate its instructions. The type of each intermediate computation is determined from knowledge of the function and the type of its arguments. Types can be specified for input arguments to a function. You can also introduce a variable of a given type by assigning a local variable to a constant.

The types that can be used in the Wolfram System compiler are summarized in this section.

Boolean

Booleans can be passed in as arguments to compiled functions, and are also supported as the value of a local variable.

Compile[{{arg,True|False}},body]a Boolean argument
Module[ {var=True},body]a Boolean local variable

Boolean types.

The following demonstrates a function that takes two Boolean arguments.

Integer

Integers can be passed in as arguments to compiled functions, and are also supported as the value of a local variable.

Compile[{{arg,_Integer}},body]an integer argument
Module[ {var=10},body]an integer local variable

Integer types.

The following demonstrates a function that takes one integer argument.

Real

Reals can be passed in as arguments to compiled functions, and are also supported as the value of a local variable.

Compile[{{arg,_Real}},body]a real argument
Module[ {var=10.5},body]a real local variable

Real types.

The following demonstrates a function that takes one real argument.

If you call the function with an integer, this is converted to a real number.

Complex

Complex numbers can be passed in as arguments to compiled functions, and are also supported as the value of a local variable.

Compile[{{arg,_Complex}},body]a complex argument
Module[ {var=10.+5.I},body]a complex local variable

Complex types.

The following demonstrates a function that takes one complex argument.

If you call the function with a real, this is converted to a complex number.

Tensor

The compiler works with vectors, matrices, and tensors. These can be passed in as arguments to compiled functions, and are also supported as the value of a local variable.

Compile[{{arg,_type, num}},body]a tensor argument
Module[ {var={5,6,7,8}},body]a tensor local variable

Tensor types.

The following demonstrates a function that takes a real vector as an argument.

If you call the function with a vector of integers, this is converted to a vector of reals.

Summary

The type specification syntax for arguments is summarized in the following table.

Compile[{x1,x2,},expr]create a compiled function that evaluates expr for numerical values of the xi
Compile[{{x1,t1},{x2,t2},},expr]compile expr assuming that xi is a of type ti
Compile[{{x1,t1,n1},{x2,t2,n2},},expr]compile expr assuming that xi is a rank ni array of objects each of type ti
_Integermachinesize integer
_Realmachineprecision approximate real number
_Complexmachineprecision approximate complex number
True|Falselogical variable

Type specification in Compile.

Type Propagation

After the arguments of a compiled function have been specified, the types are propagated through the set of functions in the compiler. Typically, this is relatively straightforward. For example, if two real numbers are added, the result will be a real.

The CompiledFunctionTools` package is useful to demonstrate the operation of the compiler. First, it needs to be loaded.

Now, you can use CompilePrint to show details of a compiled function. This shows that the square root of a complex number is also a complex number.

However, if the input is a real number, the square root could be a complex number or it could be a real number. This is demonstrated in the following.

This shows that for this input, the compiler chooses the result of the square root to also be a real number. This is less general but faster.

Type Coercion

The Wolfram Language numerical coercion system maintains results in their most accurate form. For example, evaluates to itself, since any other result would involve a loss of precision.

This should be contrasted with many other computation systems. For example, a C or Fortran program would convert the result to a floating-point number. This is because in C or Fortran there is no other way to represent the result of computing the square root of 2. The Wolfram Language does not do this because its symbolic nature allows it to represent and work with expressions such as , and avoiding conversion to a floating-point number avoids a loss of information.

However, if is combined with a floating-point number, the Wolfram Language will return a floating-point number.

Similar issues are found with any exact input, which can be integers, rationals, or constants such as Pi or E.

When the compiler is invoked directly from Compile, it gives a different type coercion. For example, it converts integers to reals when used inside functions like square root. In the following example the result is a vector of real numbers.

The compiler works in this different way because its focus is to optimize numerical computations rather than exact mathematics.

However, when the compiler is called automatically, for example, by top-level calls to functions like Map, it is run in a mode where its coercion matches that of regular Wolfram Language. If the compiler cannot return appropriate results it will not be used.

This allows the Wolfram Language to use the compiler as an internal optimization, but maintains the same results as if the compiler were not being used. In the following example, the result is a vector of exact integers and roots of integers (if the compiler had been used it would be a vector of real numbers).

Type Consistency

The compiler type system determines the type of intermediate results in the computation. These need to follow in a consistent way. For example, if there is a branch, both sides of the branch must return the same result. The following generates a compilation error because the different branches are incompatible.

Note that type coercion can be applied to move to a consistent type. This is demonstrated in the following.

Closely related to the issue of type consistency across a branch is type consistency in a return statement. The following shows how all ways to return from a function must be type consistent.

A different example of type consistency involves local variables. Once a local variable has been assigned a type, it cannot be assigned to a value of an incompatible type.

External Calls

The Wolfram System compiler can create instructions for a wide variety of Wolfram Language commands and functions covering the supported type system. However, sometimes the compiler reaches something for which it does not have any built-in support.

When this happens the compiler can still continue. It has to create an instruction to compute the result; this is done by adding an instruction that calls out to the Wolfram Language evaluator. However, in addition it also has to determine the type of the result. Unless this information has been added, this is not easy.

It is worth having a good understanding of this process because it is a common source of inefficiencies in the compiler.

The CompiledFunctionTools` package is useful to demonstrate the operation of the compiler. First, it needs to be loaded.

The following shows an external function called from inside a compiled function. The instructions show that the Wolfram Language evaluator is called and the result is expected to be a real number.

This shows the function works as expected.

Here a different definition of the external function is used. It shows that the compiler still expects the result to be a real, whereas a Boolean is returned.

Now when the function is used a runtime error results.

You can avoid these problems by declaring that the external function returns a Boolean.

Now the function works as expected.

Undefined External Calls

The compiler will make external calls for parts of the input for which it does not have information. This can sometimes lead to problems as shown above. These problems can definitely cause efficiency problems when the compiled function executes. Even if no problems are found, the compiled function computation can still be inefficient.

One way to track these problems is to use the Compile::noinfo message. This can be enabled to give more insight into the compilation process but is disabled by default.

You can enable the message with On.

Now the compilation generates a message. This can be a useful way to track down efficiency problems.

Note that if you give type information for the computation, no message is generated.

Packed Arrays

Packed arrays are a feature of the Wolfram Language that provide important speed and memory benefits. They exist so that the Wolfram Language can be general enough to support symbolic computation and also fast enough that numerical computation is efficient. They give a specialized representation for vectors, matrices, and tensors of machine numbers such as integers, reals, and complex reals.

This example shows a vector of real numbers.

The vector is a packed array.

Now insert a symbolic quantity into the vector.

When the byte count is measured, the vector is shown to use more memory.

In addition, a mathematical operation on the vector is much slower.

The vector is no longer a packed array.

Packed arrays are important for the compiler because internally the compiler works with packed arrays. This gives an efficiency gain since they are fast to pass in and return from the compiler.

This demonstrates a compiled function that returns a vector of integers.

The result is a packed array. Whenever the compiler returns a vector, matrix, or tensor it will always be packed.

Compilation Errors

The Wolfram System compiler generates the sequence of instructions for evaluating a Wolfram Language expression. As it visits parts of the expression, it might encounter a problem such as the following.

Runtime Errors

The Wolfram Language virtual machine executes a compiled function for specific input arguments; when it finishes, it returns the result. However, an error can occur as the machine is running. Some possible errors include the following.

Mathematical Function Error

Mathematical function errors can arise from domain problems or function exceptions.

Domain problems arise because of the compiler type system. For example, working with real numbers, certain inputs can lead to an error. In the following case a compiled function works for positive inputs.

However, if the input is negative a runtime error is generated. At this point an error handler is called that switches to the Wolfram Language interpreter.

These errors do not have any analog for the Wolfram Language interpreter, because this switches between domains at runtime. In other words, there is only one type, a Wolfram Language expression.

In the compiler, you could avoid a domain error such as this by working with complex numbers. However, this would have a consequence for speed.

A function exception takes place for certain inputs to mathematical functions. In the following example, the power is computed without any problems.

However, 00 is undefined and this generates a runtime error and calls the Wolfram Language interpreter. The interpreter then also has a problem with the computation and it returns Indeterminate.

Overflow

Overflow happens when a number grows above its dynamic range. In the Wolfram Language this is handled by automatically switching from numbers implemented with the machine hardware to numbers implemented in software. This allows the Wolfram Language to continue to return useful results, but it also has a consequence for efficiency, since software arithmetic (despite being very efficient in the Wolfram Language) is slower than hardware arithmetic.

The following shows a machine integer.

When the number is incremented by 1 the expected result is returned.

However, the result is not a machine integer (note that this is done using 32-bit signed integers).

If this computation were carried in a C program, the result would have been -2147483648. This is because integer arithmetic is defined to work on bit patterns that are used to implement the integer, which in many cases actually carry out the operation. The consequence is that arithmetic is fast but not always "mathematically" correct. Of course, since programmers are aware of these errors they usually take care to detect them if they think they are likely to cause problems.

For floating point computation, numbers can also overflow as demonstrated with the following machine real.

When the number is multiplied by 10 the expected result is returned.

However, the result is not a machine real (note that this is done using double-precision reals).

If computations that involve overflow are done with machine reals in C the results are slightly different from the case of machine integers. This is because they follow a standard (IEEE 754) which allows for arithmetic exceptions. For overflow the result will typically be a machine real infinity. This machine real infinity will propagate through subsequent computations in a way defined by the standard.

This quick summary of overflow issues shows in part why the Wolfram Language is useful. It completely takes care of all of these details. Of course, some C or Fortran programmers simply ignore these issues and expect that their computations will never be troubled by such problems.

Execution of Wolfram Language-compiled functions also faces these issues. Typically, compiled functions catch integer problems. This is demonstrated in the following.

However, it should be understood that checking for integer overflow requires extra work for an operation that is typically very fast, which can in cases lead to a speed degradation. It is possible to disable the checking by a setting for the RuntimeOptions. The following shows how integer overflow checking is disabled and the mathematically incorrect (but fast) result is returned.

The case for real computation is slightly different. The following is a real computation.

If the input is larger, a runtime error is generated.

However, the treatment of overflow errors for real computation is different than for integer computation. In fact, the error is detected at the end of the computation. So if a function that does not return the overflow result is used, no error is generated.

This is possible for real computations because the hardware arithmetic allows these errors to propagate through other computations. The benefit is that computations do not have to be checked as they take place, leading to a performance increase.

There is also a setting for the RuntimeOptions that allows checking for real errors as they occur.

External Call Error

An external call error takes place when execution of a compiled function calls the Wolfram Language interpreter but the result is not of the expected type. The following shows how an external error can be introduced.

When the function is used, an error results.

Here a compiled function is created that has the expected result for the external evaluation.

Despite the fact that no runtime error now takes place, external calls are still a source of inefficiency and it is good to avoid them if possible.

List Processing and Other Errors

There are a number of other types of errors that can occur while a compiled function is executing. The following compiled function returns an element of a vector.

If the part number is greater than the length of the vector, an error results.

Runtime Attributes

Attributes are a way to specify various properties of Wolfram Language functions. Listable is one useful attribute; a listable function automatically threads over any lists in its input.

The following declares a listable function.

This demonstrates its use.

You can also specify attributes for Function.

Many built-in functions have the Listable attribute.

You can give attributes to a compiled function by setting the RuntimeAttributes option of Compile. At present you can only set the Listable attribute. This is demonstrated in the following.

The listable compiled function works on a single number in the normal fashion.

However, when the arguments include a list with higher rank than the input specification, the function threads over that argument.

A listable compiled function is useful for creating a function that will work with large amounts of data but which has branches and tests in it. For example, the following function is listable.

Now the function can be applied to a large dataset.

Here a compiled version of the function is made. It runs much faster.

Compilation Options

CompilationOptions is an option of Compile that specifies settings for the compilation process. At present, the only setting is the expression optimization level.

You can make individual settings in the CompilationOptions.

"ExpressionOptimization"Automaticwhether to optimize the input expression
"InlineCompiledFunctions"Automaticwhether to expand the body of nested compiled functions
"InlineExternalDefinitions"Automaticwhether to use external definitions

ExpressionOptimization

The "ExpressionOptimization" option controls whether optimization should be applied to the input to Compile.

The default setting is that input settings should be optimized to avoid computing the same thing more than once. The following shows how x2 is only computed once and is saved with a local variable.

You can disable expression optimization with a setting of False.

The default is a setting of Automatic; this means that if the compiled function needs to make an external call, no optimization is carried out.

A setting of True will force optimization to take place even if there is an external call.

InlineCompiledFunctions

The "InlineCompiledFunctions" option controls whether nested compiled functions should be inlined or called at runtime.

This simple compiled function can be called from another function.

This compiled function calls the simple function. It uses With to place the simple compiled function in its body.

Here the CompiledFunctionTools` package is loaded.

Now CompilePrint shows how the simple function has been inlined into the function.

If you set "InlineCompiledFunctions" to False, the nested function will not be inlined. Instead, it is called with a special instruction that avoids using the Wolfram Language evaluator.

The default setting of "InlineCompiledFunctions" is Automatic, which uses a heuristic to inline small functions.

It is inconvenient to make a direct call to a compiled function. This is because Compile has the attribute HoldAll so that it does not evaluate its argument. The examples avoid this problem by using With to insert the called function into the body of the caller. This is somewhat awkward and will prevent any recursive operation of the function.

An alternative is to use a definition to hold the called compiled function. However, this only works if the "InlineExternalDefinitions" option has been set.

In the following, the default value of "InlineExternalDefinitions" is used. This does not insert external definitions and the compiled function is not inlined and it does not use the special instruction for calling the compiler.

When "InlineExternalDefinitions" is set to True, the definition of the variable cf is used. This inserts the compiled function. Then the "InlineCompiledFunctions" is used, and this causes the function to be inlined.

Here the external definition is used, but the compiled function is not inlined. Instead it uses an efficient instruction to allow one compiled function to call another. This type of call is important since it could allow a compiled function to call itself, and when parallel execution is carried out in the compiler the call can be done without any synchronization locking.

Here a recursive call is set up. The compiler will still use the efficient instruction to make the nested call.

InlineExternalDefinitions

The "InlineExternalDefinitions" option controls whether external definitions should be inlined into a compiled function.

This makes an external definition, held in the symbol val. This is used by Compile.

To understand what the generated compiled function does it is useful to use the CompiledFunctionTools` package. First, it needs to be loaded.

Now, you can see how the compiled function works. It makes an external call to the function definition. However, it has processed the value to get the type correct. This is a consequence of the default setting of "InlineExternalDefinitions" to Automatic.

If "InlineExternalDefinitions" is set to True, the external definition is inserted into the compiled function. This is not done by default because if the definition is changed, the compiled function will continue to use the old value.

If "InlineExternalDefinitions" is set to False, the external definition is not inserted into the compiled function and type information is not derived from the definition. Note how the result of the external evaluation is a real, whereas it should really be an integer.

The "InlineExternalDefinitions" option can be used with the "InlineCompiledFunctions" option. In this example, a call is made to another compiled function. Setting "InlineCompiledFunctions" to False avoids inlining the compiled function but causes an efficient instruction to be used to call from one compiled function to another.

Runtime Options

RuntimeOptions is an option of Compile that specifies runtime settings for the compiled function. It takes a number of settings to control how overflow, and runtime errors should be handled, as well as whether messages should be issued. You can give overall settings to optimize either speed or quality.

The following shows the default setting, which catches machine integer overflow. The computation is then repeated using the Wolfram Language interpreter.

If you set the runtime options to be "Speed", there is no checking for machine integer overflow. The computation gets an incorrect result. If you knew that there was no possibility for machine integer overflow, then you might want to use this option.

You can make individual settings in the RuntimeOptions.

"CatchMachineOverflow"Falsewhether real overflow should be caught as it happens
"CatchMachineIntegerOverflow"Truewhether integer overflow should be caught
"CompareWithTolerance"Truewhether comparisons should work similarly to SameQ
"RuntimeErrorHandler"Evaluatea function to apply if there is a fatal runtime error executing the function
"WarningMessages"Truewhether warning messages should be omitted

CatchMachineOverflow

By default, machine overflow is not caught during a computation; consequently this example does not generate a runtime error.

If you turn on overflow checking, a runtime error is generated.

Note that if the compiler returns a result to the Wolfram Language that has a machine overflow, this does create an error.

It does this because there is no other way to represent the error.

CatchMachineIntegerOverflow

By default, machine integer overflow is caught and this generates a runtime error.

If you turn off machine integer overflow checking, no runtime error is generated and the result might be incorrect.

RuntimeErrorHandler

The "RuntimeErrorHandler" setting is used when there is a runtime error. The following example throws an exception when a runtime error is encountered.

With no error, the compiled function works as normal.

If there is a runtime error, the special runtime error handler is called.

The default runtime error handler is just to repeat the computation using the Wolfram Language interpreter.