The F# language has been around for a couple of years now but has been evolving gradually from a research project to a production-quality language. It already has some industry adoption, with projects in a few large banks being built using the language. With its inclusion in Visual Studio 2010 I expect that we’re going to see it being more widely used in the next few years. At first sight it seems a pleasant language to work with, but it’s been years since I’ve written even a moderately sized application in a functional language so it will be interesting to try.
I’m going to start out fairly slowly, to get a feel for how the code I write gets transformed into CIL, to understand how it’s implemented and so reassure myself that I’m not writing F# in a style that is going to compile down to badly performing IL.
In the first couple of posts I’m going to put together a simple option pricer using the closed-form Black-Scholes approach. This formula requires the standard Normal distribution’s PDF and CDF which, as a learning exercise, I’ll implement below.
For reference, I will be using the October 2009 CTP (1.9.7.8).
The Normal Probability Density function
The Standard Normal probability density function is:namespace Distributions open System module Normal = /// Internal constant used in the evaluation of the PDF. let private OneOverSqrt2PI = 1.0/Math.Sqrt(2.0 * Math.PI) /// Evaluates the standard normal PDF at point x. let n x = Math.Exp( -0.5*x*x ) * OneOverSqrt2PIFiring up Reflector we see the following IL for the
Normal.n
method:
.method public static float64 n(float64 x) cil managed { .maxstack 4 L_0000: nop L_0001: ldc.r8 -0.5 L_000a: ldarg.0 L_000b: mul L_000c: ldarg.0 L_000d: mul L_000e: call float64 [mscorlib]System.Math::Exp(float64) L_0013: call float64 Distributions.Normal::get_OneOverSqrt2PI() L_0018: mul L_0019: ret }
That was a little surprising: I had assumed that the private OneOverSqrt2PI
would have been translated into a private static double in the class implementing the module. Instead it had been implemented as a private property, with the constant 1/sqrt(2*PI) being retrieved by a call to the property’s getter.
A further surprise was that the double field backing the OneOverSqrt2PI
property and the .cctor
initialising it were not contained within the class implementing the module: they were placed in a private class named StartupCode$LibraryName.$Distributions
(after the namespace). The property's getter retrieved the calculated value from this class. It seems that the F# compiler systematically generates a "startup class" for each namespace within the application.
Going one step further, it’s interesting to see what the JIT does with this: will the method retrieving the constant be inlined, or will we incur the cost of an extra method call each time we evaluate the PDF?
By default Visual Studio will suppress the JIT optimisations when the code is being debugged, which would disable the inlining even if the JIT would normally inline the method. To run with optimisation enabled, it is first necessary to go to Tools -> Options -> debugging and untick Suppress JIT optimization on module load and Enable just my code.
Having done that and hit a breakpoint at Normal.n, it's possibly to get a nice disassembly of the optimized JIT'd code using SOS.dll. To do this type .load sos
in the immediate debug window, then use !u
to disassemble the code at the instruction pointer EIP (copied from the registers debug window):
push ebp mov ebp,esp sub esp,8 fld qword ptr [ebp+8] fld st(0) fmul dword ptr ds:[01519424h] fmulp st(1),st sub esp,8 fstp qword ptr [esp] call 675D746F (System.Math.Exp(Double)) fstp qword ptr [ebp-8] call dword ptr ds:[00143828h] (Distributions.Normal.get_OneOverSqrt2PI()) fld qword ptr [ebp-8] fmulp st(1),st mov esp,ebp pop ebp ret 8
In the disassembly above, it’s can be seen that the retrieval of the constant OneOverSqrt2PI
is not inlined. This will still run pretty quickly, but is there any way we can improve on this?
The Literal attribute?
Being an F# novice, the first possible solution that jumped out at me when skimming the specification was the [<Literal>]
attribute. The documentation states that a let binding annotated with [<Literal>]
would be treated as if it were a literal at the point of use. Trying it out, when the let is annotated with [<Literal>]
its value is inlined in the generated IL. Unfortunately to use literal the value must be a compile-time constant. Changing the F# source to:
namespace Distributions open System module Normal = /// Internal constant used in the evaluation of the PDF: equal to /// 1.0/sqrt(2 * PI) [<Literal>] let private OneOverSqrt2PI = 0.398942280401433 /// Evaluates the standard normal PDF at point x. let n x = Math.Exp( -0.5*x*x ) * OneOverSqrt2PIresults in the following IL, where it can be seen that the constant has been inlined at compile time:
.method public static float64 n(float64 x) cil managed { .maxstack 4 L_0000: nop L_0001: ldc.r8 -0.5 L_000a: ldarg.0 L_000b: mul L_000c: ldarg.0 L_000d: mul L_000e: call float64 [mscorlib]System.Math::Exp(float64) L_0013: ldc.r8 0.398942280401433 L_001c: mul L_001d: ret }
However, when the value of the let binding is a compile-time constant, then it it inlined even without the [<Literal>]
attribute. Reading Chris Smith's article, F# Zen - The Literal Attribute, it appears that the purpose of the literal attribute is to cause the binding to be interpreted as its value in constructs where it would otherwise be considered to be a new local binding of the same name - for example, to use it as a pattern to match against, not a name to bind the value to.
Wrapping up...
I had expected to cover both the PDF and the CDF in a single post, but having spent this many column-inches on just the simple PDF I'll save looking at the CDF for another post.
This post leaves me with a few questions still open:
- Why are the Module-level constant values are held in a per-namespace
StartupCode
class? (scope?) - Is there an alternative to using a compile-time constant that avoids a non-inlined method call?
- Why
get_OneOverSqrt2PI
wasn't inlined - it would seem a good candidate given it simply returns a readonly field
No comments:
Post a Comment