Performance

Logtalk is implemented as a trans-compiler to Prolog. When compiling predicates, it preserves in the generated Prolog code all cases of first-argument indexing and tail-recursion. In practice, this mean that if you know how to write efficient Prolog predicates, you already know the basics of how to write efficient Logtalk predicates.

The Logtalk compiler appends a single argument to the compiled form of all entity predicate clauses. This hidden argument is used to pass the execution-context when proving a query. In the common case where a predicate makes no calls to the execution-context predicates and message-sending control constructs and is neither a meta-predicate nor a coinductive predicate, the execution-context is simply passed between goals. In this case, with most backend Prolog virtual machines, the cost of this extra argument is null or negligible. When the execution-context needs to be accessed (e.g. to fetch the value of self for a (::)/1 call) there may be a small inherent overhead due to the access to the individual arguments of the compound term used to represent the execution-context.

Source code compilation modes

Source code can be compiled in optimal, normal, or debug mode, depending on the optimize and debug compiler flags. Optimal mode is used when deploying an application while normal and debug modes are used when developing an application. Compiling code in optimal mode enables several optimizations, notably use of static binding whenever enough information is available at compile time. In debug mode, most optimizations are turned off and the code is instrumented to generate debug events that enable developer tools such as the command-line debugger and the ports profiler.

Local predicate calls

Local calls to object (or category) predicates have zero overhead in terms of number of inferences, as expected, compared with local Prolog calls.

Calls to imported or inherited predicates

Assuming the optimize flag is turned on and a static predicate, (^^)/1 calls have zero overhead in terms of number of inferences.

Calls to module predicates

Local calls from an object (or category) to a module predicate have zero overhead (assuming both the module and the predicate are bound at compile time).

Messages

Logtalk implements static binding and dynamic binding for message sending calls. For dynamic binding, a caching mechanism is used by the runtime. It’s useful to measure the performance overhead in number of logic inferences compared with plain Prolog and Prolog modules. Note that the number of logic inferences is a metric independent of the chosen backend Prolog compiler. The results for Logtalk 3.17.0 and later versions are:

  • Static binding: 0

  • Dynamic binding (object bound at compile time): +1

  • Dynamic binding (object bound at runtime): +2

Static binding is the common case with libraries and most application code; it requires compiling code with the optimize flag turned on. Dynamic binding numbers are after the first call (i.e. after the generalization of the query is cached). All numbers with the events flag set to deny (setting this flag to allow adds an overhead of +5 inferences to the results above; note that this flag can be defined in a per-object basis as needed instead of globally and thus minimizing the performance impact).

The dynamic binding caches assume the used backend Prolog compiler does indexing of dynamic predicates. This is a common feature of modern Prolog systems but the actual details vary from system to system and may have an impact on dynamic binding performance.

Note that messages to self ((::)/1 calls) and messages to an object ((::)/2 calls) from the top-level interpreter always use dynamic binding as the object that receives the message is only know at runtime.

Messages sent from Prolog modules may use static binding depending on the used backend Prolog compiler native support for goal-expansion. Consult the Prolog compiler documentation and adapter file notes for details.

Warning

Some Prolog systems provide a time/1 predicate that also reports the number of inferences. But the reported numbers are often misleading when the predicate is called from the top-level. Besides common top-level bookkeeping operations (e.g. keeping track of goal history or applying goal-expansion) that may influence the inference counting, the Logtalk runtime code for a (::)/2 top-level goal is necessarily different from the code generated for a (::)/2 goal from a compiled object as it requires runtime compilation of the goal into the same low-level message-sending primitive (assuming dynamic-binding is also required for the compiled object goal).

Automatic expansion of built-in meta-predicates

The compiler always expands calls to the forall/2, once/1, and ignore/1 meta-predicates into equivalent definitions using the negation and conditional control constructs. It also expands calls to the call/1-N, phrase/2, and phrase/3 meta-predicates when the first argument is bound. These expansions are performed independently of the optimize flag value.

Inlining

When the optimize flag is turned on, the Logtalk compiler performs inlining of predicate calls whenever possible. This includes calls to Prolog predicates that are either built-in, foreign, or defined in a module (including user). Inlining notably allows wrapping module or foreign predicates using an object without introducing any overhead. In the specific case of the execution-context predicates, calls are inlined independently of the optimize flag value.

Generated code simplification and optimizations

When the optimize flag is turned on, the Logtalk compiler simplifies and optimizes generated clauses (including those resulting from the compilation of grammar rules), by flattening conjunctions, folding left unifications (e.g. generated as a by-product of the compilation of grammar rules), and removing redundant calls to true/0.

Size of the generated code

The size of the intermediate Prolog code generated by the compiler is proportional to the size of the source code. Assuming that the term-expansion mechanism is not used, each predicate clause in the source code is compiled into a single predicate clause. But the Logtalk compiler also generates internal tables for the defined entities, for the entity relations, and for the declared and defined predicates. These tables enable support for fundamental features such as inheritance and reflection. The size of these tables is proportional to the number of entities, entity relations, and predicate declarations and definitions. When the source_data is turned on (the default when developing an application), the generated code also includes additional data about the source code such as entity and predicates positions in a source file. This data enables advanced developer tool functionality but it is usually not required when deploying an application. Thus, turning this flag off is a common setting for minimizing an application footprint.

Circular references

Circular references, i.e. two objects sending messages to each other, are relatively costly and should be avoided if possible as they prevent using static binding for the messages sent from the first loaded object to the second object. The logtalk_make(circular) goal (or its {@} top-level abbreviation) can be used to scan for circular entity dependencies. The linter also warns by default about non-ideal file loading order (controlled by the unknown_entities flag).

Debug mode overhead

Code compiled in debug mode runs slower, as expected, when compared with normal or optimized mode. The overhead depends on the number of debug events generated when running the application. A debug event is simply a pass on a call or unification port of the procedure box model. These debug events can be intercepted by defined clauses for the logtalk::trace_event/2 and logtalk::debug_handler/3 multifile predicates. With no application (such as a debugger or a port profiler) loaded defining clauses for these predicates, each goal have an overhead of four extra inferences due to the runtime checking for a definition of the hook predicates and a meta-call of the user goal. The clause head unification events results in one or more inferences per goal (depending on the number of clauses whose head unify with the goal and backtracking). In practice, this overhead translates to code compiled in debug mode running typically ~2x to ~7x slower than code compiled in normal or optimized mode depending on the application (the exact overhead is proportional to the number of passes on the call and unification ports; deterministic code often results in a relatively larger overhead when compared with code performing significant backtracking).

Other considerations

One aspect of performance, that affects both Logtalk and Prolog code, is the characteristics of the Prolog VM. The Logtalk distribution includes two examples, bench and benchmarks, to help evaluate performance with specific backend Prolog systems. A table with benchmark results for a subset of the supported systems is also available at the Logtalk website. But note that multiple factors affect the performance of an application and the benchmark examples and their results only provide a partial assessment.