The Probably-False Economy of EVAL Consuming FRAME!

The earliest design usages for FRAME! were for things like ENCLOSE.

ENCLOSE builds the frame for the function you're enclosing, then passes the frame to a function that can manipulate the frame, and invoke it with EVAL (or not invoke it at all, if it wishes).

Here's a sort of simple historical example:

/foo: func [a b] [
   let result: a + b
   a: b: ~<whatever>~  ; functions can modify args/locals for any reason
   return result
]

/bar: enclose foo/ (lambda [f [frame!]] [
    let b: f.b
    f.a: f.a * 10
    (eval f) + b
])

>> bar 100 10
== 1020  ; ((a * 10) + b) + b

You'll notice above that I didn't write:

/bar: enclose foo/ (lambda [f [frame!]] [
    f.a: f.a * 10
    (eval f) + f.b
])

This is because EVAL of a FRAME! would "consume" the frame, and trying to use f.b after the EVAL would raise an error. In other words, memory was not allocated as it usually would be for a new frame to do the call...but the fields of the frame were the actual values being used.

I overwrote a and b to make the point that once that EVAL is called, you can't rely on any particular state of a frame's variables. In the general case, they can be changed to anything.

(eval copy f) is Easy, and So Is Saving Variables, Right?

The premise I was going on was that it seemed like it would be wasteful...especially in scenarios like an ENCLOSE, to make another copy of the frame's data.

And I figured usually you wouldn't need to refer to anything from the frame's input state after you called it.

So you had two choices: either evaluate a copy of the frame, or save any variables you were interested in as locals.

It Turns Out To Be Incredibly Common To Save Variables

I didn't know when originally trying to optimize the feature how often an ENCLOSE would need to talk about the input fields after an EVAL call.

But empirically I'd say you need the fields at least half the time. You actually want it more often than that when you consider debugging--you often want to print some information about the input parameters after you've done the EVAL.

(let b: f.b) Costs Much More Than malloc()+memcpy()

When you come down to it, relatively speaking: Evaluator cycles are expensive. Tuple lookup is expensive. Assignment is expensive. LET statements are expensive.

That's because this is an interpreted language, and running code in the interpreter involves pushing and popping entities that represent interpreter stack levels. There's all kinds of C data structures and layers of C function calls as the gears of the machinery turn...whether your operation be simple or complex. That's just the name of the game... a + b in a generalized evaluator is going to be at least 100x more costly than adding two integers in C, which is basically just a single CPU instruction.

So if you have to do any mitigation of losing the frame data by adding interpreted code, not only are you having to junk up what you're writing...but you're also paying much more than you would have if the system had just gone ahead and made a copy.

Explaining Why You Can't Is Lamer Than "It Just Works"

People understand that if they have a FRAME! for APPEND and they EVAL it, that the series is going to be mutated.

But they're going to understand less that the series field of the frame is not available at all to them after the call.

It's kind of a no-brainer to say that if the two approaches were at all comparable in speed or overall performance, that the more useful behavior should be the default.

And I actually believe the more useful behavior is faster in the general case...by avoiding additional intepreter cycles to save frame fields in variables.

(free f) Could Use FREE As An Intrinsic If You Want

FREE could be trivially made Intrinsic.

If profiling suggested that something like an ENCLOSE on a function with a large frame was affecting your bottom line by not freeing the frame, you could just free it after the EVAL. That would leave behind nothing but a tiny useless stub (to avoid latent references in other cells from crashing the GC), so you'd get the same end result as the historical EVAL.

I'm betting that having EVAL be able to be intrinsic when it takes one argument, and making FREE intrinsic would be faster than trying to do some weird refinement like eval:free to fold both into one operation...because refinement processing has its own cost, which I think would be greater.

But an optimized eval-free might be worth making, I don't know. However its mechanic would simply be to natively fold the free in after the EVAL, instead of trying to make EVAL take over the frame and use its memory.

My guess is that using EVAL-FREE won't be a benefit most of the time if you add any evaluator cycles to save a variable because of it.

Hence, EVAL Will No Longer Consume FRAME!

This makes the "action-is-frame" duality even more solid, because as frames are passed around in the system there won't be "consume frame vs. don't" flags involved.

You'll just either free the frame after you've applied it, or you won't. :man_shrugging:

1 Like