Understanding the nature of F# code
Understanding the nature of F# code is very crucial and is a definitive prerequisite before we begin to measure how long it runs and its effectiveness. We can measure a running F# code by running time, but to fully understand why it may run slow or fast, there are some basic concepts we have to consider first.
Before we dive more into this, we must meet the basic requirements and setup.
After the requirements have been set, we need to put in place the environment setting of Visual Studio 2015. We have to set this because we need to maintain the consistency of the default setting of Visual Studio. The setting should be set to General.
These are the steps:
- Select the Tools menu from Visual Studio's main menu.
- Select Import and Export Settings... and the Import and Export Settings Wizard screen is displayed:
- Select Reset all Settings and then Next to proceed.
- Select No, just reset my settings overwriting my current setting and then Next to proceed
- Select General and then click on Finish:
After setting it up, we will have a consistent layout to be used throughout this book, including the menu locations and the look and feel of Visual Studio.
Now, we are going to scratch the surface of F# runtime with an introductory overview of common F# runtime, which will give us some insights into F# performance.
F# runtime characteristics
The release of Visual Studio 2015 occurred at the same time as the release of .NET 4.6 and the rest of the tools, including the F# compiler. The compiler version of F# in Visual Studio 2015 is F# 4.0.
F# 4.0 has no large differences or notable new features compared to the previous version, F# 3.0 in Visual Studio 2013.
Its runtime characteristic is essentially the same as F# 4.0, although there are some subtle performance improvements and bug fixes.
For more information on what's new in F# 4.0 (described as release notes) visit:
https://github.com/Microsoft/visualfsharp/blob/fsharp4/CHANGELOG.md
Note
At the time of writing this book, the online and offline MSDN Library of F# in Visual Studio does not have F# 4.0 release notes documentation, but you can always go to the GitHub repository of F# to check the latest update.
These are the common characteristics of F# as part of managed programming language:
- F# must conform to .NET CLR. This includes the compatibilities, the IL emitted after compilation, and support for .NET BCL (the basic class library). Therefore, F# functions and libraries can be used by other CLR-compliant languages such as C#, VB, and managed C++.
- The debug symbols (PDB) have the same format and semantics as the other CLR-compliant languages. This is important because F# code must be able to be debugged from other CLR-compliant languages as well.
From the managed languages perspective, measuring the performance of F# is similar when measured by tools such as the CLR profiler. But from an F# unique perspective, the following are the unique characteristics of F#:
- By default, all types in F# are immutable. Therefore, it's safe to assume it is intrinsically thread safe.
- F# has a distinctive collection library, and it is immutable by default. It is also safe to assume it is intrinsically thread safe.
- F# has a strong type inference model, and when a generic type is inferred without any concrete type, it automatically performs generalizations.
- Default functions in F# are implemented internally by creating an internal class derived from F#'s
FSharpFunc
. ThisFSharpFunc
is essentially a delegate that is used by F# to apply functional language constructs such as currying and partial application. - With tail call recursive optimization in the IL, the F# compiler may emit
.tail
IL, and then the CLR will recognize this and perform optimization at runtime. More on this in Chapter 7, Language Features and Constructs Optimization. - F# has inline functions as options. More on this in Chapter 7, Language Features and Constructs Optimization.
- F# has a computation workflow that is used to compose functions. This will be described in more detail in Chapter 8, Optimizing Computation Expressions.
- F# async computation doesn't need
Task<T>
to implement it.
Note
Although F# async doesn't need the Task<T>
object, it can operate well with the async-await model in C# and VB. The async-await model in C# and VB is inspired by F# async, but behaves semantically differently based on more things than just the usage of Task<T>
. More on this in Chapter 4, Introduction to Concurrency in F#.
All of those characteristics are not only unique, but they can also have performance implications when used to interoperate with C# and VB.
Relation between F# code and its generated assembly
The F# assembly (commonly known as DLL or executable EXE in .NET running on Windows) is the same as the C#/VB assembly upon compilation. The end product of the compiler is a .NET assembly.
An assembly may contain multiple namespaces, and each namespace may contain multiple files of modules, classes, or a mix of both.
The following table describes the F# relation of code and compiled code (assembly):
F# code |
Description |
Compiled code |
Project |
An organization of an F# project. It may contain F# script (FSX) and F# source files (FS).
In the conceptual layout, a project may contain multiple namespaces that spawn across multiple files of FSX and F# script.
|
An assembly of either executable EXE or DLL class library |
Namespace |
A logical organization of modules and classes to help organizing within an organization, company, or functionality.
For example: the
|
A namespace may spawn across different assemblies instead of a namespace for only one assembly |
Module |
A module in F# is equal to a C# static class or module in VB. An F# FS file may contain multiple modules, although it is not recommended to have this practice. |
Part of a generated assembly |
Classes and interfaces |
A file can contain multiple classes and interfaces under different namespaces. It is recommended to have not more than one namespace for each file as this also minimizes compilation time when it tries to resolve references. |
Part of a generated assembly |
Immutability versus mutability
F# implementation of types and collection types are immutable. Immutable in this sense means it is read-only, and we can only initialize the object with an initial value and we can't change it afterwards.
Mutability means once we initialize an object, it can be changed afterwards. This is why it is sometimes called a mutating object value instead of a changing object value.
For example consider the following:
let anynumber = 0
By default, anynumber
is immutable and the value of it will always be 0
.
To mark a variable as mutable, F# has the mutable
keyword and we can use mutable
in the let
declaration, as in this example:
let mutable anymutablenumber = 0
However, changing the value requires the use of the <-
symbol in F#, for example:
anymutablenumber <- anymutablenumber + 1
Since the nature of F# is functional, a symbol can be both a data and a function. The content of the symbol is read-only, so does a function in it.
Immutability also has another advantage: it scales well across multiple threads or even in parallel, no matter whether it's a value or a function. The immutability guarantee means that it is free of side effects. It is then safe to spawn multiple symbols in parallel because the result of an execution will be guaranteed to have the same result. This is also simply called thread safe.
The fact that F# has a mixed support for functional and OOP at the same time (including having support for the inherent mutable state of OOP) may lead to bottlenecks as described next.