F# High Performance

By Eriawan Kusumawardhono
  • Instant online access to over 7,500+ books and videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Performing Common Optimizations in F#

About this book

F# is a functional programming language and is used in enterprise applications that demand high performance. It has its own unique trait: it is a functional programming language and has OOP support at the same time.

This book will help you make F# applications run faster with examples you can easily break down and take into your own work. You will be able to assess the performance of the program and identify bottlenecks.

Beginning with a gentle overview of concurrency features in F#, you will get to know the advanced topics of concurrency optimizations in F#, such as F# message passing agent of MailboxProcessor and further interoperation with .NET TPL. Based on this knowledge, you will be able to enhance the performance optimizations when implementing and using other F# language features.

The book also covers optimization techniques by using F# best practices and F# libraries. You will learn how the concepts of concurrency and parallel programming will help in improving the performance. With this, you would be able to take advantage of multi-core processors and track memory leaks, root causes, and CPU issues.

Finally, you will be able to test their applications to achieve scalability.

Publication date:
January 2017
Publisher
Packt
Pages
338
ISBN
9781786468079

 

Chapter 1. Performing Common Optimizations in F#

It's quite well-known today that F# has been a first class citizen, a built-in part of programming language support in Visual Studio, starting from Visual Studio 2010. F# is a programming language that has its own unique trait: it is a functional programming language and at the same time it has object-oriented programming (OOP) support. F# from the start has run on .NET, although we can also run F# on cross-platform, such as Android (using Mono).

Although F# mostly runs faster than C# or VB when doing computations, its own performance characteristics and some not so obvious bad practices and subtleties may have led to performance bottlenecks. The bottlenecks may or may not be faster than the C#/VB counterparts, although some of the bottlenecks may share the same performance characteristics, such as the use of .NET APIs. The main goal of this book is to identify performance problems in F#, measuring and also optimizing F# code to run more efficiently, while also maintaining the functional programming style as appropriately as possible.

Note

A basic knowledge of F# (including the functional programming concept and basic OOP) is required as a prerequisite to start understanding the performance problems and the optimization of F#.

There are many ways to define F# performance characteristics and at the same time to measure them, but understanding the mechanics of running F# code, especially on top of .NET, is crucial and is also a part of the performance characteristics itself. This includes other aspects of approaches to identify concurrency problems and language constructs. This chapter describes the optimization of F# code and will cover the following topics:

  • Understanding the nature of F# code

  • Overview of common bottlenecks

  • Commonly misunderstood concurrency problems

  • Overview of tooling in .NET including Visual Studio to help understanding the running code

  • Immediate testing of F# code in F# interactive

  • Introduction to debugging in F#

 

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:

  1. Select the Tools menu from Visual Studio's main menu.

  2. Select Import and Export Settings... and the Import and Export Settings Wizard screen is displayed:

  3. Select Reset all Settings and then Next to proceed.

  4. Select No, just reset my settings overwriting my current setting and then Next to proceed

  5. 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. This FSharpFunc 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 System.Web namespace that contains many classes related to enable browser/server communication, including HTTP and HTTPS.

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.

 

Overview of common bottlenecks


F# has common bottlenecks although they might be subtle as well.

In order to be able to quickly understand the bottleneck factors in F#, we will categorize the shared general bottlenecks of .NET as managed bottlenecks (also in C#/VB), and F#-only bottlenecks (this includes when using F# with other languages).

The following are managed .NET bottlenecks (from obvious to less obvious):

  • String concatenations, such as using string String.Concat instead of StringBuilder. This is often overlooked because of a lack of awareness of the string's immutability.

  • Usage of non-generic collections such as ArrayList.

  • Incorrectly handling side effects, such as exceptions and I/O.

  • Mutable objects usage, including casting.

  • Complex objects that will be serialized and deserialized, for example: sending DataSet that has DataTables over HTTP.

  • Ignoring performance profiling.

Side effects mean all of the elements outside the formal computation (it is often called the outside world) that we interact with, and this includes the changing global state. The outside world can be all of the things that we cannot fully determine as the end result. Examples of the outside world include:

  • I/O: This is included as being part of the outside world because you cannot determine or guarantee any kind of work you pass to I/O to be successfully completed. For example, when sending a command to a printer to print a document, we cannot guarantee 100% success of the printing operation. We cannot even guarantee that the process of sending the data to the printer will be successful or not before the printer receives the data and begins to print the document.

  • Global static mutable variables: A quick example of this is when we define a public static variable in the scope of ASP.NET. Every value change will always change the condition of any user of the ASP.NET application.

  • Functions or properties that always have different results when they are invoked, such as DateTime.Now.

Note

DateTime.Now will always return different results and this is as expected because the result must change every time it is called or instantiated. It is not free of side effects, but it is still expected to always return a different result.

Side effects are not just for functional programming developers, as many of us are now becoming quite aware. There are no absolute side effect-free computations because we should learn and be able to correctly handle them. For example, even printing a screen to a console is also a side effect because it involves I/O, and it changes the state of the outside world.

The following are F#'s unique bottlenecks:

  • Incorrect use of data structures and collections

  • Incorrect use of auto generalization and other language constructs

  • Incorrectly implemented concurrency problems, such as mixing synchronous and asynchronous although the original intention is asynchronous

  • Slow performance when having to interoperate with other languages' class libraries such as C#/VB

  • Scaling MailboxProcessor in F#

  • Identifying when tail call optimization should occur

  • Slow response when resolving type in type provider implementation

  • Slow performance when implementing computation workflow

 

Common samples of misunderstood concurrent problems


Many of us, when dealing with concurrent problems, sometimes try to use a hammer for every nail. There is no silver bullet for all of the problems of implementing concurrency.

It is also recommended to understand concurrency, as concurrency is now becoming more relevant because of the many core models in the releases of modern microprocessors (or simply processors) in the last 7 years. This fact is also becoming a trend as the clock speed of the latest processors has been usually limited to 3.2 GHz for the last 3 years.

Microsoft's Visual C++ architect, Herb Sutter, has written a very thorough article in the form of a whitepaper famously known as The Free Lunch Is Over: 

http://www.gotw.ca/publications/concurrency-ddj.htm

Let's understand first what concurrency is and the F# supports.

Introduction to concurrency in F#

Before we dive deeper into concurrency in F#, we should understand the definition of concurrency.

Concurrency is one of the main disciplines of computer science and it is still one of the main problems of computations.

Simply defined, concurrency is the composition of the order of independent process units or partially-ordered process units that can be executed in parallel or not in parallel, but not in sequential order. The term order in this context means ordered as sequentially.

The following diagram illustrates the concept of sequential (not concurrent) in action:

Process 1 to Process 4 as shown in the preceding diagram is executed sequentially step by step. Process 2 must wait for Process 1 to be completed first, as do Process 3 and Process 4.

This sequence is also called a synchronous process or is simply referred to as being synchronous.

The following figure is a sample illustration of a parallel concurrency combination of parallel and synchronous processes:

Processes  1A, 2A, and 3A run in parallel, although each parallel lane has its own sequence of processes that are run sequentially.

The term parallel means that it is not just executing simultaneously in parallel, but parallel also means that it may run on many processors or on many cores, as is common in modern processors that have multiple cores.

Defining asynchronous

A simple definition of asynchronous means not synchronous. This means that if we have an asynchronous flow, the process is not run synchronously.

These are the implications of an asynchronous flow:

  • Processes run not sequentially. For example, if the first process is running asynchronously, the next process doesn't have to wait for the first process to be completed.

  • There has to be a way of scheduling and telling the scheduler to inform that the asynchronous process is completed. Typically, the asynchronous process is usually related to blocking I/O or some long computations.

  • At first, the processes may look sequential, but the next process run may not be sequential at all.

This is a sample case of asynchronous: a customer is going to have dinner in a restaurant. The flows are:

  1. Customer A orders some food or drinks, and the order is noted by waiter X. Usually, most restaurants have more than one waiter, but for this illustration, the waiter available currently to serve customer A is waiter X.

  2. Waiter X then gives the list of the customer's order to chef Y.

  3. Chef Y accepts the order, and checks if he is currently occupied or not. If he is occupied, the order is registered as part of his cooking queue. Otherwise, he will start to cook the order.

  4. The waiter does not have to wait for the chef to complete his cooking. He can then serve other customers who have just arrived or there might be customers that want to add more food or drinks as well.

  5. Chef Y finishes his cooking for customer A, and then gives a notification to waiter X to inform that his cooking for customer A is finished. Or he can inform all waiters to tell that the cooking for customer A is finished. This concept of informing to tell a process is finished is commonly called a callback.

  6. Waiter X (or any other waiter) delivers the finished food to customer A.

The asynchronous model that uses a notification to inform that a process is completed is called asynchronous callback.

The result returned at the end of the execution later (or in the future) is called a Future. It is also the future, in a sense, when many processes are executed in parallel, having results later.

This is the official documentation of Future in MSDN Library: 

https://msdn.microsoft.com/en-us/library/ff963556.aspx

For I/O operations, such as printing a document, we cannot determine whether the printing is successful or not, so the notification of the end process is not available. We can implement an asynchronous operation on I/O, and the fact that there is no observable notification of this is why this asynchronous model is called the asynchronous fire and forget model.

Misunderstood concurrency problems

Many developers, even seasoned or experienced developers, still think that concurrency and parallel programming are different. Actually, parallel programming is just one member within the concurrency discipline, together with the differentiation of asynchronous and synchronous processing models.

This is also one of the most misunderstood concurrency concepts or problems, and there are many more regarding how we approach concurrency.

These are some common organized sample cases of misunderstood concurrency problems:

  1. Assuming all concurrent problems can be solved using parallel programming.

    Fact: Not all concurrent problems are easily solved with parallelism.

  2. Assuming all implementation of asynchronous is asynchronous.

    Fact: This depends on how we implement async; sometimes the execution of an async construct is executed synchronously.

  3. Ignoring blocking threads such as I/O.

    Fact: Blocking I/O threads should be handled asynchronously; otherwise, the current thread is always waiting indefinitely until the I/O thread is finished.

  4. The synchronized lock is blocking.

    Fact: The lock is not a blocking thread.

  5. Relying on the CPU speed.

    Fact: The CPU speed increase is becoming less of an issue. The research and development of modern CPUs is focusing on multiple core CPUs.

A few sample cases of concurrent problems are mentioned as follows:

The case samples of the first case are:

  • Ordering or sorting a collection: Ordering is by default a sequential process, and it requires iterating all the elements of the collection. Therefore, it's useless to use parallelism.

  • Grouping data: Grouping data is implicitly one of the sequential processes; it is also quite useless to use parallelism.

  • Printing reports: Printing is part of I/O and I/O is intrinsically without support for parallelism. Unless the I/O is part of I/O parallelism, it is useless to use parallelism in this context.

Sample cases of the second case are listed as follows:

  • Mixing Parallel.For that has F# async in it. The implications of having Parallel.For is by default taking a multiple core or a CPU to run it is not the same as running asynchronously, as it is not guaranteed to run as a combined async in parallel.

  • Using Thread.Sleep instead of Async.Sleep to signify a wait operation. The call to Thread.Sleep will instead make the flow synchronous, as the Sleep method simply puts on hold the current thread as a delay synchronously.

Note

RAID array in the storage I/O is one of the best samples of parallelism in I/O. It stores data in parallel across multiple disks. It is faster than common I/O because data is stored in parts (not whole data to a disk) to several disks in parallel.

The third case is related to all of the I/O operations including sending data to a printer and saving large data into a disk. These operations are always blocking threads.

For the case of lock, Microsoft has issued official statements that lock in .NET used by C# and VB is executed without any interruption, and it only locks an object until it has finished executing the block in the synchronized lock. It's still allowing other threads to run without waiting for the thread that has the lock to finish.

This is the official thread synchronization of C# and VB in MSDN: 

https://msdn.microsoft.com/en-us/library/ms173179.aspx

It is recommended to always check online the MSDN Library of the .NET class library, as this is always updated.

Introduction to concurrency support in .NET and F#

Concurrency support in F# is based on the existing work of concurrency support features in .NET BCL (the Base Class Library). It's also by design, since F# runs on top of .NET CLR and can use .NET BCL. F# also has its unique ways that bring more features other than just language features (for example, asynchronous computations).

The .NET BCL part of concurrency has basic support for the following:

  • Thread

  • Lock

  • Mutex

Beginning with .NET 4.0, we have the Task Parallel Library (TPL). This library makes concurrent support easier. TPL consists of the following:

  • Data parallelism (for example: Parallel.For and ForEach)

  • Task parallelism

  • Asynchronous task (this is also the base foundation of C#/VB's async-await)

  • Parallel LINQ (often abbreviated as PLINQ)

For a more complete reference of concurrency support in .NET, please visit https://msdn.microsoft.com/en-us/library/hh156548(v=vs.110).aspx.

Note

.NET has no support yet for fiber API in Win32 API. Microsoft currently has no definite plan for fiber support.

F# has its own unique features of concurrency supports. They are:

  • Asynchronous workflow or computation

  • MailboxProcessor

  • Parallel async

  • Parallel async combined with I/O

More on concurrency support in F# is available in Chapter 4, Introduction to Concurrency in F# and Chapter 5, Advanced Concurrency Support in F#.

Now it's time to dive more into some codes. To start writing F# code, we can use F# and Visual Studio combined. This includes IDE supports for F#.

 

Overview of F# tooling in Visual Studio


F# has been supported in Visual Studio since Visual Studio 2010, and in Visual Studio 2015 the support has improved with better syntax colorizations than Visual Studio 2010, not just IDE. This F# IDE support is officially called Visual F#.

This tooling is available as open source from Microsoft and it is available to be downloaded from GitHub at https://github.com/Microsoft/visualfsharp/.

And the F# compiler itself is open source and it is also available from GitHub (including the design proposal discussions) at https://github.com/fsharp.

The tooling is under the governance of Microsoft, but it is welcoming community contributions as it is available on GitHub. All of the community participations of tooling, compilers and the language specifications are under the governance of the F# Software Foundation (FSSF).

We can also support FSSF directly. For more information about FSSF, please visit http://fsharp.org/.

Note

The F# community projects are also managed by FSSF, and it is welcoming contributions as well. FSSF is an independent entity and it is not tied to Microsoft.

Visual F# in Visual Studio 2015 has mainly the following capabilities:

  • Project template support, including the NuGet package addition and references to other projects in the same solution file. The other projects can be VB or C# projects, not just F#.

  • AssemblyInfo support in a separate file. This feature has been available since Visual Studio 2015. Previously it was only available in C# and VB projects.

  • The F# compiler, FSC, is used to compile F# into .NET executable and libraries in the form of DLL.

  • Integration of the F# compiler, MSBuild infrastructure, and also Intellisense.

  • F# libraries, a foundation to the functional programming constructs of F# and F# unique concurrency features such as asynchronous workflow and MailboxProcessor. It also contains many useful functions to further interoperate with C#/VB, including interoperating with .NET delegates.

  • Interactive support for the F# interactive (FSI) prompt in the IDE.

For more information about F# tooling, this is the official MSDN Library link: 

https://msdn.microsoft.com/visualfsharpdocs/conceptual/visual-fsharp

It is recommended to always consult the Visual F# documentation on GitHub first, then combine it with the online MSDN Library section of F#.

Microsoft has planned to rebuild all of the online MSDN Library to use the GitHub participation model, so developers can submit a pull request to modify or to amend any documentation page. Visual F# is now being reworked on as well, but there are some pages still left behind inside Visual F# GitHub repo.

To always check the latest development of F# documentation on GitHub, visit https://github.com/Microsoft/visualfsharpdocs.

Interactive support for F# interactive

F# interactive is a tool to interpret your F# code and run it immediately. It will also process and show the results, the types, and the syntax errors. The code can be run in the interactive prompt or taken from the source code and then run into F# interactive.

Note

The concept of having interpreted code, executing it, and seeing the results is called REPL. REPL is abbreviated from Read-Eval-Print-Loop, and it was first available as the system's command-line prompt. There is nothing new about this REPL concept, as other programming languages such as Python already have had REPL before F#.

Compared to C# and VB, F# interactive is the first to have interactive REPL support since Visual Studio 2010. Unfortunately, there is no Visual Studio's Intellisense support for F# interactive yet.

There are two ways to use F# interactive:

  • In Visual Studio IDE

  • In Command Prompt

The most common usage of F# interactive is within Visual Studio IDE.

We have to set up the F# Interactive window to be displayed in order to use F# interactive.

These are the steps to display the F# Interactive window:

  1. Open the View menu from the Visual Studio main menu.

  2. Choose Other Windows.. and then choose F# Interactive.

A window that hosts the F# Interactive within Visual Studio will appear and it will be ready to interpret our F# code:

As a starter, type #help followed by ;; to mark as closing statements to be evaluated. We now see some further options:

F# interactive can be used to not only interpret and run F# code but also as a way to see immediate results of a calculation.

Type 5 * 25;; and press Enter.

We now see the result of that calculation:

We can also execute codes in the Visual Studio editor when we are opening F# source code file.

For example, create a new project using the F# tutorial project template:

You may find that your display of Visual Studio is different from the previous screenshot. Actually, the aforementioned display depends on what Visual Studio edition we have. For Visual Studio Enterprise, more templates are available for us to use, such as the Modeling Projects to create UML.

For the purpose of F#, the project templates of the F# projects are the same for the Community Edition and above.

After creating the project, an F# project contains Tutorial.fsx.

Before we use F# interactive, we should turn on the option for displaying line numbers. It is also recommended to have this option always turned on, as it will provide easier navigation to the code we write:

  1. Go to the Tools menu and choose Options. It is available in F# options in the Options dialog:

  2. Now double-click Tutorial.fsx, and highlight lines 44 to 61:

  3. Then press Alt + Enter. F# interprets the code. We can see the result of the interpretation in F# Interactive:

We have tried F# interactive from within the Visual Studio IDE. Let's use F# interactive from Command Prompt.

Note

We can also use Ctrl + Alt + F to activate or open F# Interactive.

To use F# interactive from Command Prompt, we call the executable FSI directly from Command Prompt.

The best way to run FSI is from Visual Studio's developer Command Prompt. This developer Command Prompt is available under the Visual Studio 2015 folder on the start menu of the Windows desktop menu bar.

Select it, and now we have the Developer Command Prompt for VS2015:

Type FSI and press Enter.

We can try to write some code to evaluate, such as:

let anynumber = 5 * 25;; 

Press Enter. The immediate result will be displayed:

To quit the FSI, type #quit;; and press Enter.

Using F# interactive from Command Prompt is faster but it is also not quite so user-friendly because we cannot evaluate multiple lines of code easily. It is easier to evaluate this in Visual Studio IDE.

Note

For simplicity and ease of use, the rest of this book will always use FSI within the Visual Studio IDE.

For more information about F# FSI, consult the FSI reference from the MSDN Library at  https://msdn.microsoft.com/visualfsharpdocs/conceptual/fsharp-interactive-%5bfsi.exe%5d-reference.

FSI is also configurable. We can configure FSI further by leveraging the FSI class library in the Microsoft.FSharp.Compiler.Interactive namespace. More information on this library is also available at the F# FSI URL mentioned previously.

 

Introduction to debugging in F#


There is one aspect of understanding running F# code that is crucial: debugging F# code. We have to be able to debug F# code, especially when we have very large projects that have hundreds of F# code files, not to mention when each of the code files may have too many lines of code. For example, having to check a running F# code that has more than 2,000 lines.

The following are the advantages of the debug features:

  • Isolating the error and focusing on it by inserting a breakpoint can ease the fixing of an error or bug. Developers are gaining more productivity because they can fix errors/bugs faster.

  • Debugging can also provide insightful information about the correctness of any value returning from a function.

  • Debugging can also be used to trace bugs further by examining the results from other referenced libraries as well. It is possible that we may use the referenced library incorrectly or the referenced library may also have bugs.

Visual F# in Visual Studio 2015 also has debugging capabilities. It was not as powerful when it was introduced in Visual Studio 2008 as additional add-on, but now the debugging experience is much better. It has been integrated with the Visual Studio extensibility model nicely, providing, for example, faster execution while running in the debug mode and having conditional breakpoints.

It is different from the C#/VB debugger because F#, although being a strongly and strictly typed language, currently has no support for evaluating expressions in the debugger's immediate windows in Visual Studio 2015.

Note

Some experienced F# developers may argue that this additional debug feature is not a big concern at all as F# has a tendency to enforce type restriction and correctness at the fabric of F# as a programming language. But for most other developers, especially those who jump from C#/VB to F#, the overall debugging experience is still lacking some features.

Currently these are the differences between the F# and C#/VB debugger in Visual Studio 2015:

Feature

F#

C#/VB

Breakpoint insertion

Yes.

Yes

Condition in breakpoint

Yes.

Yes

Intellisense in editing condition in breakpoint

Not supported because Intellisense is not yet linked to the whole infrastructure of the Visual Studio 2015 IDE extensibility and the F# compiler. There is a plan to have this feature for the next Visual Studio release after Visual Studio 2015.

Yes

Lightbulb assistant

Not available. There is a plan to have this feature for the next Visual Studio release after Visual Studio 2015, but the exact planned release is not quite clear.

Yes

Expression evaluation in immediate window

Not available.

Yes

Locals value

Yes.

Yes

Auto watch value

Yes.

Yes

Other than the features in the previous table, basic debugging with breakpoints in Visual F# is essentially the same as debugging in C#/VB.

Let's take some code to debug. To quickly have some code, we can use the F# 3.0 sample from CodePlex at:

http://fsharp3sample.codeplex.com/

After downloading the ZIP file of the code samples, unzip it to a folder and open the SampleProject.sln solution file in Visual Studio.

Note

You may read Readme.txt first before using the whole sample code. This readme guide is available in the Solution Item folder when opened in Solution Explorer.

Now, your screen should look like this:

Some of the samples in F# 3.0 are not valid anymore. You have to register for Azure DataMarket to access the F# Type Provider of Azure DataMarket.

There are some compiler errors if we try to rebuild the solution without changing the code at all, and one of the sample type providers, ESRI DataMarket, is not working.

Based on those invalid type provider conditions, to build this sample solution successfully, you have to follow these steps:

  1. Register with Azure DataMarket. You need to have your own Azure account key to access Azure DataMarket.

  2. The ESRI sample has not been working since 2014. Please comment the lines from line 135 to line 157 in the Samples.TypeProviders.AzureMarketPlace.fs file.

  3. Rebuild the solution. This will compile the whole solution and also resolve the type provider in the background for us to use.

  4. Now open the Samples.Beginners.fs file. Put the debugger breakpoints at lines 19 and 20 by clicking the line.

  5. To add breakpoints, you can simply toggle the highlighted column on the left before the line number like this:

  6. And we can also add breakpoints by right clicking and choosing Breakpoints.. and then Insert Breakpoint.

  7. Compile the code by initiating the Build Solution. Then press F5 to run. The window of F# Micro Sample Explore is displayed:

    This sample is actually a showcase of many F# features, from basic language constructs, units of measure, type providers, and LINQ, to concurrency such as async and parallelism.

  8. Now expand the Basic node on the left and choose Basic Data Types, and then choose the last node of Integer Arithmetic, as illustrated here:

  9. Going back to the source code of Samples.Beginner.fs, we can see that the node name is also the same as the name of the attributes in the code to categorize:

          [<Category("Basic Data Types"); 
            Title("Integer Arithmetic"); 
            Description("This sample shows some basic integer arithmetic")>] 
          let SampleArithmetic1() = 
              let x = 10 + 12 - 3  
              let y = x * 2 + 1  
              let r1,r2 = x/3, x%3 
              printfn "x = %d, y = %d, r1 = %d, r2 = %d" x y r1 r2 
    
  10. Click the Run Sample! button and Visual Studio will stop the execution at the breakpoint:

    Now we can debug our code easily. We can also look at the value of the variables or symbols that are currently in scope by checking the value at the Locals window.

  11. Press F10 to step over, and now we see the evaluated value of x and y:

Any local variables in Locals and watch expressions displayed in the Watch1 window always have the name, value, and type of the variables. The type displayed is using the F# keyword, not the full type name.

For example, int is displayed instead of System.Int32 as shown in Locals.

We can always check other values as well if we have another global or static global variable in the Watch1 window. The values can contain immediate values from an expression, for example DateTime.Now.

Unfortunately, we have to write using the full namespace of System.DateTime, so we have to write the expression as System.DateTime.Now:

This requirement to have the full namespace proves that debugger support in Visual F# still requires improvements compared to its C#/VB counterparts. Typing the full object name may be error prone, as F# watch does not support Intellisense yet.

After we have finished debugging and bug fixing, it is recommended to change the compilation to the Release mode. The Release mode will have a smaller compiled F# code and it executes faster because it does not contain debug symbols and any other debug information attached to the compiled code.

To change back and forth between Debug and Release is quite easy. We can simply change the mode at the drop-down menu in the Visual Studio toolbar:

There is no apparent distinction on the compiled DLL or EXE filename, other than the smaller size of the release mode.

To summarize, the following are the differences of the Debug mode and the Release mode:

Elements

Debug

Release

Debug symbol (PDB)

Included

Not included.

Size of compiled code

Bigger than the release mode, excluding the PDB file

Smaller than the debug mode.

Code optimization

Not optimized, as it is focused for debugging and it is also synchronized

Yes, but the code will not be able to be debugged easily, as the code is optimized for executions.

In .NET 4.6 and Windows 10, it is optimized further by compiling into native code using the Ahead Of Time (AOT) model instead of Just In Time (JIT).

Compilation symbol availability

DEBUG

Not applicable.

Execution

Slower than Release, as there is no optimization

Fast, since it is optimized for runtime, and there is no debug symbol overhead.

For more information about AOT and JIT, consult the MSDN Library at https://msdn.microsoft.com/en-us/library/dn807190(v=vs.110).aspx.

 

Summary


We discussed the introduction to performing common performance optimizations, from the performance characteristics of F# and .NET to the most commonly used optimization concepts such as concurrency. We also have a basic knowledge to start troubleshooting performance problems by debugging using Visual Studio. But debugging running F# code is still a small part of performance optimization because debugging only provides an insight of the values and the states of objects in our code.

The debug support of F# tooling itself is not enough to fully understand the details of performance problems because most of the detailed performance optimization requires us to measure the benchmark of our code. The measurement of performance in order to objectively measure optimization will be described in Chapter 2 , Performance Measurement.

About the Author

  • Eriawan Kusumawardhono

    Eriawan Kusumawardhono is a veteran senior software development engineer (SDE) who has software development experience of more than 13 years, with 11 years of having developed in .NET, since the introduction of Visual Studio 2003. On usual working day he works at Allegro Development, a software development company, and in his spare time, he does speaking and training for software development and programming language communities in Indonesia. He is also a member of F# Foundation speaker program. He enjoys being a full-time technical polyglot software developer, mingling both OOP and functional programming at the same time.

    He currently holds a Microsoft MVP award in Visual Studio development platform competency, from 2011 to 2017, for more than 5 consecutive years.

    He lives in Jakarta with his wife, enjoying reading and cooking heterogeneous exotic food in Indonesia.

    Browse publications by this author

Recommended For You

C# 8.0 and .NET Core 3.0 – Modern Cross-Platform Development - Fourth Edition

Learn the fundamentals, practical applications, and latest features of C# 8.0 and .NET Core 3.0 from expert teacher Mark J. Price.

By Mark J. Price
ASP.NET Core 3 and React

Build modern, scalable, and cloud-ready single-page applications using ASP.NET Core, React, TypeScript, and Azure

By Carl Rippon
Hands-On Software Architecture with C# 8 and .NET Core 3

Design scalable and high-performance enterprise applications using the latest features of C# 8 and .NET Core 3

By Francesco Abbruzzese and 1 more
Hands-On Neuroevolution with Python

Increase the performance of various neural network architectures using NEAT, HyperNEAT, ES-HyperNEAT, Novelty Search, SAFE, and deep neuroevolution

By Iaroslav Omelianenko