To refresh our memory, this was the F# code of "simulate":
let simulate intg_func t0 t_fin delta pos0 speed0 = let rec repeat t0 t_fin pos0 speed0 = if t0 >= t_fin then pos0, speed0 else let t1 = t0 + delta let pos1, speed1 = intg_func pos0 speed0 delta in repeat t1 t_fin pos1 speed1 repeat t0 t_fin pos0 speed0
The disassembled code in C# looks approximately as follows (I have simplified and cleaned up the text a bit):
internal static Tuple simulate@37(FastFunc intg_func, double t0, double t_fin, double delta, double pos0, double speed0) { FastFunc f = new clo@39(intg_func, delta); return FastFunc.InvokeFast4(f, t0, t_fin, pos0, speed0); }
"clo@39" must be some sort of wrapper for the "repeat" inner function I defined in "simulate". Notice how the generated code handles "intg_func" and "delta" differently from the other variables "t0", "t_fin", "pos0" and "speed0". The former are passed once during the construction of "clo@39", the latter are passed as arguments when invoking the "repeat" function.
Looking back to the F# code, notice that "intg_func" and "delta" indeed are special. Although they are used by "repeat", they are not parameters of it. This is called a closure. The reason for using a closure will become clear if you continue reading.
Let us look at "clo@39". It has an "Invoke" method which is called when "repeat" is called in the F# code:
public override Tuple Invoke(double t0, double t_fin, double pos0, double speed0) { while (t0 < t_fin) { double num = t0 + this.delta; Tuple tuple = FastFunc.InvokeFast3(this.intg_func, pos0, speed0, this.delta); speed0 = tuple.Item2; pos0 = tuple.Item1; t_fin = t_fin; t0 = num; } return new Tuple(pos0, speed0); }I was not surprised when I noticed it, but the F# compiler has turned my recursive function "repeat" into a non-recursive loop. This is an important optimization that most functional languages support. All tail-recursive functions can easily be turned into non-recursive functions, which saves a lot of stack space.
The result would be beautiful, if it were not for this strange thing: "t_fin = t_fin;". I can see that the original F# code does indeed copy "t_fin" every iteration, and in that sense the generated code remains faithful to the original code. I wish F# had optimized this unnecessary copying away, but it probably has its reasons for not doing so. Happily, there is a work-around, which is to use a closure. This is precisely what I did for "intg_func" and "delta".
One might even say that this work-around is more than that, it actually is the right way of doing things. Constant parameters should not be passed explicitly as parameters to recursive functions.
Let us continue and look into "runge_kutta_4". First, the F# code from the previous tutorial:
let runge_kutta_4 accel_func pos speed delta = let half_delta = 0.5 * delta let deriv_func = adapt accel_func let s1, a1 = deriv_func pos speed ...
The generated code, translated to C#:
internal static Tuple runge_kutta_4@26(FastFunc accel_func, double pos, double speed, double delta) { double num = 0.5 * delta; FastFunc f = new clo@28(accel_func); Tuple tuple = FastFunc.InvokeFast2(f, pos, speed); ...Nothing really suprising here, but I just took notice of how the adapted variant of "accel_func" is recreated in each call. That is obviously not very efficient, considering how often this function is called.
In the next tutorial, I will present an optimized version of the code and compare it to implementations in C++ and OCaml.