Ant is an experiment to build an ultra fast and ultra small embedded scripting engine with a close-to-native performance, small size and embedding simplicity. Ant is implemented as a single C/C++ standard-compliant header file that works on any platform, starting from 8-bit AVRs to 64-bit servers.
The ant.h
header file implements 3 engines: an infix engine (ant), a
postfix engine (ant2) and a bytecode engine (ant3). All engines where
tested on the Arduino XIAO board (SAMD21 Cortex-M0+ processor) by calculating
the following routine (see Ant.ino):
static long exec_c(void) {
long res = 0;
for (long i = 0; i < 1000; i++) res += i + i / 3;
return res;
}
Here is the result for all 3 engines and native C implementation. Note that antx is the same as ant3 but using computed goto feature.
ant, result: 665667, microseconds: 115434
ant2, result: 665667, microseconds: 41908
ant3, result: 665667, microseconds: 14548
antx, result: 665667, microseconds: 14542
c, result: 665667, microseconds: 2020
This result shows that the 7x slowness of the bytecode implementation can be considered close-to-native, but it also suggests that the implementation should use compilation step to convert source code into bytecode.
- Infix notation
- Arithmetics:
+
,-
,*
,/
- Single-letter variables from
a
toz
- Assignments
a = 0; b = 2;
- Increments and decrements
a += 2; b += a * (c + d);
- Labels for jumps:
#
- Jumps:
@f expr
jumps forward,@b expr
jumps backward ifexpr
is true. Jumps are performed to the nearest#
label - Loops and conditionals are implemented using labels and jumps, e.g.
if (a > 0) i++; ...
->@tf a < 0 i += 1 # ...
Below are major places where a scripting engine slows down in comparison with the native code:
- Expression parsing. To alleviate this slowness, either
- a postfix notation should be used, e.g.
1 2 + 3 *
instead of(1 + 2) * 3
- an extremely fast infix expression parser
- a postfix notation should be used, e.g.
- Variable lookup:
a = 123
ora = b + c
. Compiled code assigns some memory locations for each variable and references that memory directly. A scripting engine performs a variable lookup every time a variable gets referenced. To speed up variable lookups, the following tactics can be used:- using only single-letter variable names, for example from
a
toz
. This way, ant reserves 26 variables in total, and a variable name gives a direct index:vars[*pc - 'a']
- using explicit variable indices, e.g.
17 v
which givesvars[17]
. For example, JavaScript'slet foo = 123
can become ant's123 0 v =
if a postfix notation is used, which in turn executesvars[0] = 123
- using only single-letter variable names, for example from
- Marshalling arguments to FFI calls. To alleviate this, a scripting engine
can push arguments to machine's stack directly and therefore avoid any
extra marshalling layer, e.g.
"world" "hello, %s\n" P
whereP
references stdlib'sprintf
, and strings"world"
and"hello, %s"
push respective pointers to the machine's stack. This is not portable however, since some architectures might not use stack for arguments - Floating-point versus integer math
- Conditionals. Consider JavaScript's
if (cond) { body }
, wherecond
is false. Then, we should jump over thebody
whilst not executing it. If there is no compilation step, we don't know wherebody
ends and therefore we should "execute" thebody
without evaluating it. The solution is to use labels and jump commands, just like in the assembly code.