Code Planting Tutorial Part 4A - GetPoplog/Seed GitHub Wiki

Alternative to code-planting

In this tutorial it helps to know:

  • That lvars procedure foo declares a variable that is constrained to procedural values only. It is a weak form of type-enforcement.
  • The time macro is available by loading lib time and is a simple tool for rough and ready timing tests.
  • The built-in function valof, given the name of a variable, returns the value of that variable. It is a simple form of reflection.

Introduction

We left hanging the question of whether it was possible to "compile" a query-tree into an efficient procedure without using code-planting. Well, it is possible to do quite a good job using ordinary functional programming techniques - we can certainly eliminate all the run-time lookups.

The key idea is that we write a 'compiling' function that translates each part of the tree into a function of type T -> bool, where T is the type of the_record. This function will decompose and decode the tree at 'compile' time and return closures with those decoded parts.

So our entry-function will be responsible for destructuring and dispatch:

define predicateForQueryTree( tree ); lvars tree;
    lvars ( op, L, R ) = tree.dest.dest.hd;
    if op == "and" then
        predicateForAnd( L, R )
    elseif op == "or" then
        predicateForOr( L, R )
    elseif member( op, [ = < > <= >= /= ] ) then
        predicateForComparison( op, L, R )
    else
        mishap( 'Unknown operator', [ ^op ] )
    endif
enddefine;

Translating the and and or is simply a matter of building closures:

define predicateForAnd( L, R ); lvars L, R;
    lvars procedure LQ = L.predicateForQueryTree;
    lvars procedure RQ = R.predicateForQueryTree;
    procedure( the_record ); lvars the_record;
        the_record.LQ and the_record.RQ
    endprocedure
enddefine;

define predicateForOr( L, R ); lvars L, R;
    lvars procedure LQ = L.predicateForQueryTree;
    lvars procedure RQ = R.predicateForQueryTree;
    procedure( the_record ); lvars the_record;
        the_record.LQ or the_record.RQ
    endprocedure
enddefine;

And all that remains is the comparison:

define predicateForComparison( op, P, V ); lvars op, V; lvars procedure P;
    lvars procedure cmp = valof( op );                ;;; decode at 'compile'-time
    procedure( the_record ); lvars the_record;
        cmp( the_record.P, V )
    endprocedure
enddefine;

A quick interactive session, using the previously established example shows that it does just what we expect.

: lvars qp = predicateForQueryTree( example_tree );
: lvars i; for i in trivial_data do if qp(i) then i => endif endfor;
** 3
** 5
** 7
: 

Efficiency Comparison

So how does our pure-procedural version stack up against the code-planting version? Let's look at the three implementations side by side on a particular example.

: vars cp = plantQueryTree( example_tree );
: vars qp = predicateForQueryTree( example_tree );
: 
: time * 10000000 evalQuery( 7, example_tree ).erase;  ;;; Naive implementation
CPU TIME: 4.82   GC TIME: 3.32
^C
Setpop
: time * 10000000 qp( 7 ).erase;                       ;;; Procedural
CPU TIME: 0.49   GC TIME: 0.0
^C
Setpop
: time * 10000000 cp( 7 ).erase;                       ;;; Code-planting
CPU TIME: 0.26   GC TIME: 0.0

As we expect, the naive implementation is about 10x slower than the procedural version on this one example. The procedural version is good but the code-planting version is about twice as fast, which will be due to avoiding the procedural-calling 'plumbing'. That's certainly a significant improvement if the query will be called on a many records.

It's important to draw an appropriately limited conclusion from this single example! What we can say is that the procedural version squeezes out the worst overheads and that code-planting squeezes out the much smaller overheads of small-closures calling other small-closures. And we can also say that there are situations where this is likely to be worthwhile. BUT it is easy to imagine another example where the query-tree was calling expensive properties and comparison operations that would dwarf the small overhead of procedural plumbing.

Next Steps

Next Step

⚠️ **GitHub.com Fallback** ⚠️