Variables and cache pointers

<< Click to Display Table of Contents >>

Navigation:  Introduction > Parser and object structures >

Variables and cache pointers

Previous pageReturn to chapter overviewNext page

When the parser and C#-code emitter were designed, it was with performance in mind as an important requirement. Why design a nice parser, a nice AST tree, a nice C#-code emitter, and nice Gekko objects and datastructures to interface those, if Gekko command files run slowly? So when the parser and emitter were rewritten in 2013, a lot of testing was performed in order to make sure that the Gekko code could run as fast as practically possible.

 

In 2013, it was considered to use so-called cache pointers. It should be mentioned that the idea was dropped both for the final version of Gekko 2.0, and for Gekko 3.0. More on this later on, but the idea was to avoid searching for a variable like x, %x or #x in a databank more than one time -- subsequent calls to x, %x or #x would remember the location of those variables, a bit like how the CPU cache is a faster kind of RAM stored closer to the CPU than the normal RAM. Still, the cache pointer ideas could be revived at a later time, if Gekko programs need to run faster.

 

Gekko operates with 7 types of variables. These are listed below:

 

Gekko type

Class name

Sigil

Reference

Definition

SERIES

Series

[none]

x

= 150;

VAL

ScalarVal

%

%v

%= 1.23;

DATE

ScalarDate

%

%d

%= 2000q3;

STRING

ScalarString

%

%s

%= 'Hello';

LIST

List

#

#x

#= a, b, c;

#= ('a', 'b', 'c');

MAP

Map

#

#x

#= (= 150, %= 1.23);

MATRIX

Matrix

#

#x

#= [1, 2; 3, 4];

 

Note that the Gekko type is usually not indicated in Gekko 3.0. For instance, with %= 1.23;, Gekko can infer that %x must be of VAL type, but types can be indicated, like for instance val %= 1.23;.

 

Timeseries (the Series type/class) are without special symbol (sigil). Scalars use 'sigil' %, which is an identifier character intended to make it easier to read Gekko command files. Variables beginning with % can be thought of as single non-vector elements, and are hence called scalars. Collections begin with # symbol and at the moment there are lists, maps, or matrix. All variable types reside in Gekko databanks.

 

As mentioned in previous sections, Gekko variables reside in Gekko databanks, and therefore finding a variable in a Gekko databank is done all the time in running Gekko programs. Finding a variable in a databank is done with so-called lookup-code, that is, code like O.Lookup(...), where all the details of databank lookups is handled. In the current source code documentation, there is a special section on lookups, cf. here.

 

 

Cache pointers

 

Up to the release of Gekko 2.0, so-called cache were considered, for speed reasons. The point is that when searching for (looking up) a Gekko variable in a Gekko databank, this search is often unnecessary. Consider a Gekko program that first defines a timeseries x!a (!a for annual frequency), then uses it to calculate a timeseries y!a, and then alters x!a somehow. The variable x!a will be looked up three times, which entails querying the underlying C# Dictionaries inside the hiearachical list of open databanks, to find x!a in one of these. If databanks are not opened, closed, copied or similar, the x!a variable will be exactly the same object in all three cases. Therefore, there is some unnecessary overhead in such cases, where Gekko ought to be able to "remember" the object from the first lookup, and reuse the reference to that object in the two subsequent lookups. This would be akin to a cache, and would be able to speed up Gekko programs, especially programs with many lookups (for instance, if these are inside a loop).

 

There are two points to consider now:

 

First of all, such cache pointers are tricky, and complicates the code generation from the AST tree to executable C# code. They are also a bit dangerous, because if the logic is wrong, for instance if the programmer forgets that a certain command may change something in the list of open databanks, the lookup will use an erroneous x!a object (that perhaps even, in a C#.NET context, would have otherwise been garbage-collected).

Secondly, the importance of cache pointers is somewhat diminished in Gekko 3.0, relative to Gekko 2.0. This is because timeseries operations in Gekko 3.0 work much more like vector arithmetic, performing operations on all relevant observations in the relevant time period at once (looping over the raw arrays containing the data), rather than looping over GekkoTime periods and getting and setting data by means of these objects. Therefore, in Gekko 3.0, a statement like = x + y; will only involve three lookups (for x, y, and z), where Gekko 2.0 would have had three lookups per observation/period in the relevant time period.

 

It should be noted that Gekko is a dynamic language in the sense that variable types do not need to be stated at compile time. For instance, in a Gekko statement like %+ %y, it is not known what the types of %y and %x are before the statement is actually executed (they may be values, dates, or strings), and the type of variable the statement returns is not know either.

 

What is known is that both %x and %y are objects, and that all variable objects implement the IVariable interface (and therefore are guaranteed to implement an Add() method for addition with the + operator). Another thing is that the Gekko variables live in Gekko databanks, adding another layer of indirection. In contrast, in C#, in an expression like + y, the types of x and y are already known. Therefore, if x and y are double-precision values, C# already knows exactly what to do with the + y expression, even before the program is run. Such execution is fast and does not involve the creation of objects or other kinds of wrappers.

 

 

The cache pointer idea

 

To do cache pointer caching, when accessing a scalar variable like %x, Gekko could produce C# code like the following:

 

public static IVariable cache1 = null;
 
// below, the variable "%x" is used in an expression:
 
.....   LookupCache(ref cache1, "%x")   .....
 
// and later on, "%x" is used again:
 
.....   LookupCache(ref cache1, "%x")   .....

 

The first time the LookupCache() method is run (to fetch the variable %x), the global variable cache1 will be null. Because the variable is null, the LookupCache() method will look up %x as Gekko normally does, finding it in a databank. When the variable is found, LookupCache() returns it, but it also sets the global cache1 variable equal to it (cache1 points to %x, hence the name cache pointer).

 

We may imagine the %x is used again later on in the program. This time, however, cache1 is no longer null, and when LookupCache() is called, the method will just return the object corresponding to the global cache1 variable. This saves the work of looking through open databanks, and for each of these looking for %x (in a C# Dictionary).

 

The problem with this approach is that the programmer needs to be very careful about the databanks: if any databank is opened or closed, if databank search is switched off, if any %x variable in any of the databanks is created or deleted, the cache needs to be invalidated. Invalidating is quite simple, simply setting cache1 = null, but the real problem is making sure when the cache needs to be cleared, and when not. All this adds complexity to the Gekko source code, and the question is whether this complexity is worth it? In C#, Dictionary access is surprisingly fast, and object creation (and garbage collection) is often also surprisingly fast, so Gekko is by no means inherently slow, at least not when compared to other dynamic languages like R or Python.

 

By using cache pointers, Gekko would be able to mitigate some of the speed loss compared to raw C# speed. If speed becomes an issue regarding Gekko, the ideas stated above or in the source code documentation regarding Gekko 2.0 could perhaps be of use (cf. for instance this page).

 

Also, if there is a need to speed up scalar access (values, dates, and strings), it would perhaps be wise to look into how this is implemented in Julia, by using so-called dynamic multiple dispatch.