Cnumber Patch

lua-users home
wiki

Introduction

The CNUMBER patch (available at LuaPowerPatches) provides a more efficient mechanism for accessing numeric C variables from Lua. For example, consider the following C variable:


  double foo = 10.0;

which you might want to access in Lua as a regular variable:

  foo = foo + 1

In Lua 5.1, the primary approach to use here is to attach your own metatable to the global environment table and then attach "__index" and "__newindex" functions to that metatable. These two functions handle the getting and setting events to the "foo" variable, and they must be implemented in C because that is where the data is stored. This works reasonably well, but if you're super-concerned about efficiency (as I am), this process is a bit inefficient. First, it uses global variables, which are less efficient than local variables because global variable names are resolved at run-time via a lookup into the environment table, unlike local variables whose locations are computed at compile-time. In Lua, table lookup is reasonably efficient even for string keys (such as variable names) since strings are "internalized" and have precomputed hashes, but it's still a penalty. Also, this requires Lua meta-method calls, which is a further penalty. An additional concern, more of aesthetics, is that it may seem somewhat contorted the need to modify the behavior of the entire environment in order to change the behavior of a single variable. Note that such variables cannot exist outside of their environment because the behavior is attached to the environment rather than to the value itself.

Another method is to expose getter and setter accessor functions (or methods) from C and have the Lua code directly call those functions:


  set_foo(get_foo() + 1)

This is ugly and it's not particularly efficient either. For example, the "set_foo" and "get_foo" are both global variables that must be resolved at run-time, and both involve Lua function calls. A similar approach is given in [PIL]. One positive point is that the behavior is attached to the value itself, so no change is need to the environment.

Another proposed approach might be to implement numeric C variables as heavy userdata and attach a metatable with appropriate events so that they behave like normal values. However, that doesn't provide an efficient or syntactically clean solution either. You can change the behavior of values with metatable events, but you can't make them behave exactly like normal values. For example, there is no "assignment" metatable event that would allow such values to act as lvalues (unlike in C++).

This patch addresses these concerns by providing what is called a "CNUMBER." A CNUMBER is a variable exposed in Lua that is implemented in C. It is in some ways analogous to a Lua CFUNCTION, which is a function exposed in Lua and implemented in C. Access to CNUMBERs is almost as fast as access to Lua local variables, which is not surprising since CNUMBERs are handled in a similar way to locals (ignoring closures and scope).

Usage

To register CNUMBERs with Lua, you must define three functions as follows:


double foo = 0.0;

double foo2 = 0.0;

double foo3 = 0.0;



    int

iscnumber(lua_State * L, const char * varname, int * id) {

    if(strcmp(varname, "foo") == 0) {

        *id = 1; return 1; /* yes */

    }

    else if(strcmp(varname, "foo2") == 0) {

        *id = 2; return 1; /* yes */

    }

    else if(strcmp(varname, "foo3") == 0) {

        *id = 3; return 1; /* yes */

    }

    else {

        return 0; /* no */

    }

}



    double

getcnumber(lua_State *L, int id) {

    switch(id) {

        case 1: { return foo; }

        case 2: { return foo2; }

        case 3: { return foo3; }

        default: assert(0);

    }

    return 0;  /* should not occur */

}



    void

setcnumber(lua_State *L, int id, double value) {

    switch(id) {

        case 1: { foo  = value;  break; }

        case 2: { foo2 = value;  break; }

        case 3: { foo3 = value;  break; }

        default: assert(0);

    }

}

The first function, iscnumber, is called by the Lua parser. It allows Lua to determine if a given identifier should be interpreted as a CNUMBER. If the function identifies the name as a CNUMBER, the function assigns the identifier an ID for later use. The ID is an 18-bit unsigned number (this size is limited by the opcodes of Lua 5.1) whose meaning is known only to the C code. When Lua identifies CNUMBERs, it generates new special GETCNUMBER and SETCNUMBER opcodes and stores that ID with it. Note that each CNUMBER is resolved once (at compile time), so its access is more efficient at run-time.

The functions getcnumber and setcnumber are called at run-time upon processing the two opcodes to respectively get or set the variable. Each function is passed the ID mentioned before. The functions can use a simple lookup on that ID as above, or it could be something more complex (e.g. index into arrays). It is not necessary that an double variable actually exist in the C code.

These three functions must be registered before parsing as follows:


  lua_setcnumberhandler(L, iscnumber, getcnumber, setcnumber);

You can unregister them by setting them all to NULL.

Benchmarking

Benchmarking is provided on 10 iteratons of this simple example:


  for n=1,10000000 do

    foo = foo + 1

  end

The results compare CNUMBER with a variety of approaches on two systems:

  Linux 2.4.20 (virtualized) / GCC / Intel(R) Xeon(TM) 3 GHz

  == Run Times (sec)

  LOOP           : 8.100000

  CNUMBER        : 23.490000 *

  LOCAL          : 15.200000

  GLOBAL         : 37.150000

  METATABLELOCAL : 99.980000

  METATABLETABLE : 198.790000

  CFUNCTION      : 72.010000



  WinXP / GCC Ming / Intel(R) P4 3 GHz

  == Run Times (sec)

  LOOP           : 3.361000

  CNUMBER        : 13.702000 *

  LOCAL          : 8.579000

  GLOBAL         : 25.640000

  METATABLELOCAL : 72.858000

  METATABLETABLE : 150.266000

  CFUNCTION      : 52.672000

Description of each approach:

As shown, CNUMBERs are almost as fast as Lua locals and faster than Lua globals. They are much faster than the METATABLETABLE and CFUNCTION methods, which otherwise would be required to expose C variables from Lua. METATABLELOCAL is a simpler version of METATABLETABLE (omitting the second table lookup) but not really useful and only provided for comparison. This benchmark program is included in the patch (cnumber.c).

In a certain "real-life" application, CFUNCTION reduced run-time by about 40%.

Design Notes

The CNUMBER implementation has some important properties:

Possible extensions of this patch include these:

--DavidManura


RecentChanges · preferences
edit · history
Last edited February 20, 2006 11:02 pm GMT (diff)