Performance for composition in F#
source link: https://dev.to/t4rzsan/performance-for-composition-in-f-3fl
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
In a previous 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 the Result
type.
In other words, we will compare the following operators:
// Monadic bind
let (>>=) m f =
Result.bind f m
// Monadic bind inline
let inline (>>==) m f =
Result.bind f m
// Kleisli
let inline (>=>) a b x =
match a x with
| Ok v -> b v
| Error e -> Error e
To test we compose five different functions. All the functions do is copy some data structure:
type Data =
{ Property1: string
Property2: int
Property3: DateTime
Property4: float
Property5: decimal }
let processA data = { data with Property1 = "some new string" }
let processB data = { data with Property2 = 100 }
let processC data = ...
let processResultA data = Ok { data with Property1 = "some new string" }
let processResultB data = Ok { data with Property2 = 100 }
let processResultC data = ...
We use function processData
for testing |>
and processDataResult
for testing the compositions that operate on Result
.
Now we can create the functions we wish to time:
let withPiping data =
data
|> processA
|> processB
|> processC
|> processD
|> processE
let withBinding data =
data
|> processResultA
|> Result.bind processResultB
|> Result.bind processResultC
|> Result.bind processResultD
|> Result.bind processResulte
let withOperatorWithoutInlining data =
data
|> processResultA
>>= processResultB
>>= processResultC
>>= processResultD
>>= processResultE
let withOperatorWithInlining data =
data
|> processResultA
>>== processResultB
>>== processResultC
>>== processResultD
>>== processResultE
let withKleisli data =
data
|> (processResultA
>=> processResultB
>=> processResultC
>=> processResultD
>=> processResultE)
Note that withBinding
, withOperatorWithoutInlining
and withOperatorWithInlining
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 function f
:
let timeFunction f data =
let sw = Stopwatch()
sw.Start()
[1 .. 10_000_000]
|> List.iter (fun _ -> f data |> ignore)
sw.Stop()
sw.Elapsed.TotalMilliseconds
That way we can time 10 million calls to a function like so:
data |> timeFunction withKleisli
To get a smaller variance on those 10 millions calls, let us define a run
function that runs timeFunction
a number of times and prints the average:
let run f fName =
let data =
{ Property1 = "some string"
Property2 = 42
Property3 = DateTime.Today
Property4 = 23.2
Property5 = 23m }
[1 .. 100]
|> List.averageBy (fun i -> timeFunction f data)
|> print fName
Now we can write the final setup:
run withPiping (nameof withPiping)
run withBinding (nameof withBinding)
run withOperatorWithInlining (nameof withOperatorWithInlining)
run withOperatorWithoutInlining (nameof withOperatorWithoutInlining)
run withKleisli (nameof withKleisli)
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 set withPiping
as index 100, so the graph shows how the other methods compare to withPiping
.
It shouldn't come as a surprise that withPiping
is the fastest.
I am a bit surprised that withBinding
is that much faster than withOperatorWithoutInlining
, 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.
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.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK