
In aprevious post I talked about how we at work simplified composition in an F# project because we found that we took a performance hit when we used monadic binds in our code.
I was a bit surprised how expensive the construct was so I wanted to dig deeper into it with this article.
The setup
Composition in F# can take a number of forms. Below we will take a closer look at the simple straight forward pipe (|>
) operator and compare it to the Kleisli composition and monadic bind composition operating on theResult
type.
In other words, we will compare the following operators:
// Monadic bindlet(>>=)mf=Result.bindfm// Monadic bind inlineletinline(>>==)mf=Result.bindfm// Kleisliletinline(>=>)abx=matchaxwith|Okv->bv|Errore->Errore
To test we compose five different functions. All the functions do is copy some data structure:
typeData={Property1:stringProperty2:intProperty3:DateTimeProperty4:floatProperty5:decimal}letprocessAdata={datawithProperty1="some new string"}letprocessBdata={datawithProperty2=100}letprocessCdata=...letprocessResultAdata=Ok{datawithProperty1="some new string"}letprocessResultBdata=Ok{datawithProperty2=100}letprocessResultCdata=...
We use functionprocessData
for testing|>
andprocessDataResult
for testing the compositions that operate onResult
.
Now we can create the functions we wish to time:
letwithPipingdata=data|>processA|>processB|>processC|>processD|>processEletwithBindingdata=data|>processResultA|>Result.bindprocessResultB|>Result.bindprocessResultC|>Result.bindprocessResultD|>Result.bindprocessResulteletwithOperatorWithoutInliningdata=data|>processResultA>>=processResultB>>=processResultC>>=processResultD>>=processResultEletwithOperatorWithInliningdata=data|>processResultA>>==processResultB>>==processResultC>>==processResultD>>==processResultEletwithKleislidata=data|>(processResultA>=>processResultB>=>processResultC>=>processResultD>=>processResultE)
Note thatwithBinding
,withOperatorWithoutInlining
andwithOperatorWithInlining
are essentially the same but as we will see later there is a difference in performance between them.
Let's also define a function for timing a number of calls to a functionf
:
lettimeFunctionfdata=letsw=Stopwatch()sw.Start()[1..10_000_000]|>List.iter(fun_->fdata|>ignore)sw.Stop()sw.Elapsed.TotalMilliseconds
That way we can time 10 million calls to a function like so:
data|>timeFunctionwithKleisli
To get a smaller variance on those 10 millions calls, let us define arun
function that runstimeFunction
a number of times and prints the average:
letrunffName=letdata={Property1="some string"Property2=42Property3=DateTime.TodayProperty4=23.2Property5=23m}[1..100]|>List.averageBy(funi->timeFunctionfdata)|>printfName
Now we can write the final setup:
runwithPiping(nameofwithPiping)runwithBinding(nameofwithBinding)runwithOperatorWithInlining(nameofwithOperatorWithInlining)runwithOperatorWithoutInlining(nameofwithOperatorWithoutInlining)runwithKleisli(nameofwithKleisli)
Granted, the setup is a bit lame but it does give some indication of differences in performance between the different types of composition.
The results are in
They say that an image is worth a thousand words, so here is a graph for you. I setwithPiping
as index 100, so the graph shows how the other methods compare towithPiping
.
It shouldn't come as a surprise thatwithPiping
is the fastest.
I am a bit surprised thatwithBinding
is that much faster thanwithOperatorWithoutInlining
, i.e. the>>=
operator.
Kleisli composition is surprisingly slow, but monadic bind is more useful in F# anyways so you would probably not use Kleisli that often.
Code
I put the code on Github if you want to have a look. I created it on .NET 6 RC using the preview of Visual Studio 2022.
Top comments(2)

Have you considered usinggithub.com/dotnet/BenchmarkDotNet for this comparison? There's a bit of a learning curve to get started with it, and it can take a while to run, but the reward is more robust, statistically significant results.

- LocationDenmark
- EducationMaster of Actuarial Science
- Pronounshe/him/his
- WorkActuary at AP Pension
- Joined
Nice, thank you. I did not know about that one.
For further actions, you may consider blocking this person and/orreporting abuse