开发者

How to make immutable F# more performant?

I'm wanting to write a big chunk of C# code using immutable F#. It's a device monitor, and the current implementation works by constantly getting data from a serial port and updating member variables based on new data. I'd like to transfer that to F# and get the benefits of immutable records, but my first shot at a proof-of-concept implementation is really slow.

open System
open System.Diagnostics

type DeviceStatus = { RPM         : int;
                      Pressure    : int;
                      Temperature : int }

// I'm assuming my actual implementation, using serial data, would be something like 
// "let rec UpdateStatusWithSerialReadings (status:DeviceStatus) (serialInput:string[])".
// where serialInput is whatever the device streamed out since the previous check: something like
// ["RPM=90","Pres=50","Temp=85","RPM=40","Pres=23", etc.]
// The device streams out different parameters at different intervals, so I can't just wait for them all to arrive and aggregate them all at once.
// I'm just doing a POC here, so want to eliminate noise from parsing etc.
// So this just updates the status's RPM i times and returns the result.
let rec UpdateStatusITimes (status:DeviceStatus) (i:int) = 
    match i with
    | 0 -> status
    | _ -> UpdateStatusITimes {status with RPM = 90} (i - 1)

let initStatus = { RPM = 80 ; Pressure = 100 ; Temperature = 70 }
let stopwatch = new Stopwatch()

stopwatch.Start()
let endStatus = UpdateStatusITimes initStatus 100000000
stopwatch.Stop()

printfn "endStatus.RPM = %A" endStatus.RPM
printfn "stopwatch.ElapsedMilliseconds = %A" stopwatch.ElapsedMilliseconds
Console.ReadLine() |> ignore

This runs in about 1400 ms on my machine, whereas the equivalent C# code (with mutable member variables) runs in around 310 ms. Is there any way to speed this up without losing the immutability? I was hoping that the F# compiler would notice that initStatus and all the intermediate status variables were never reused, and thus simply mutate those records behind the scene开发者_如何学编程, but I guess not.


In the F# community, imperative code and mutable data aren't frowned upon as long as they're not part of your public interface. I.e., using mutable data is fine as long as you encapsulate it and isolate it from the rest of your code. To that end, I suggest something like:

type DeviceStatus =
  { RPM         : int
    Pressure    : int
    Temperature : int }

// one of the rare scenarios in which I prefer explicit classes,
// to avoid writing out all the get/set properties for each field
[<Sealed>]
type private DeviceStatusFacade =
    val mutable RPM         : int
    val mutable Pressure    : int
    val mutable Temperature : int
    new(s) =
        { RPM = s.RPM; Pressure = s.Pressure; Temperature = s.Temperature }
    member x.ToDeviceStatus () =
        { RPM = x.RPM; Pressure = x.Pressure; Temperature = x.Temperature }

let UpdateStatusITimes status i =
    let facade = DeviceStatusFacade(status)
    let rec impl i =
        if i > 0 then
            facade.RPM <- 90
            impl (i - 1)
    impl i
    facade.ToDeviceStatus ()

let initStatus = { RPM = 80; Pressure = 100; Temperature = 70 }
let stopwatch = System.Diagnostics.Stopwatch.StartNew ()
let endStatus = UpdateStatusITimes initStatus 100000000
stopwatch.Stop ()

printfn "endStatus.RPM = %d" endStatus.RPM
printfn "stopwatch.ElapsedMilliseconds = %d" stopwatch.ElapsedMilliseconds
stdin.ReadLine () |> ignore

This way, the public interface is unaffected – UpdateStatusITimes still takes and returns an intrinsically immutable DeviceStatus – but internally UpdateStatusITimes uses a mutable class to eliminate allocation overhead.

EDIT: (In response to comment) This is the style of class I would normally prefer, using a primary constructor and lets + properties rather than vals:

[<Sealed>]
type private DeviceStatusFacade(status) =
    let mutable rpm      = status.RPM
    let mutable pressure = status.Pressure
    let mutable temp     = status.Temperature
    member x.RPM         with get () = rpm      and set n = rpm      <- n
    member x.Pressure    with get () = pressure and set n = pressure <- n
    member x.Temperature with get () = temp     and set n = temp     <- n
    member x.ToDeviceStatus () =
        { RPM = rpm; Pressure = pressure; Temperature = temp }

But for simple facade classes where each property will be a blind getter/setter, I find this a bit tedious.

F# 3+ allows for the following instead, but I still don't find it to be an improvement, personally (unless one dogmatically avoids fields):

[<Sealed>]
type private DeviceStatusFacade(status) =
    member val RPM         = status.RPM with get, set
    member val Pressure    = status.Pressure with get, set
    member val Temperature = status.Temperature with get, set
    member x.ToDeviceStatus () =
        { RPM = x.RPM; Pressure = x.Pressure; Temperature = x.Temperature }


This won't answer your question, but it's probably worth stepping back and considering the big picture:

  1. What do you perceive as the advantage of immutable data structures for this use case? F# supports mutable data structures, too.
  2. You claim that the F# is "really slow" - but it's only 4.5 times slower than the C# code, and is making more than 70 million updates per second... Is this likely to be unacceptable performance for your actual application? Do you have a specific performance target in mind? Is there reason to believe that this type of code will be the bottleneck in your application?

Design is always about tradeoffs. You may find that for recording many changes in a short period of time, immutable data structures have an unacceptable performance penalty given your needs. On the other hand, if you have requirements such as keeping track of multiple older versions of a data structure at once, then the benefits of immutable data structures may make them attractive despite the performance penalty.


I suspect the performance problem you are seeing is due to the block memory zeroing involved when cloning the record (plus a negligible time for allocating it and subsequently garbage collecting it) in every iteration of the loop. You could rewrite your example using a struct:

[<Struct>]
type DeviceStatus =
    val RPM : int
    val Pressure : int
    val Temperature : int
    new(rpm:int, pres:int, temp:int) = { RPM = rpm; Pressure = pres; Temperature = temp }

let rec UpdateStatusITimes (status:DeviceStatus) (i:int) = 
    match i with
    | 0 -> status
    | _ -> UpdateStatusITimes (DeviceStatus(90, status.Pressure, status.Temperature)) (i - 1)

let initStatus = DeviceStatus(80, 100, 70)

The performance will now be close to that of using global mutable variables or by redefining UpdateStatusITimes status i as UpdateStatusITimes rpm pres temp i. This will only work if your struct is no more than 16 bytes long as otherwise it will get copied in the same sluggish manner as the record.

If, as you've hinted at in your comments, you intend to use this as part of a shared-memory multi-threaded design then you will need mutability at some point. Your choices are a) a shared mutable variable for each parameter b) one shared mutable variable containing a struct or c) a shared facade object containing mutable fields (like in ildjarn's answer). I would go for the last one since it is nicely encapsulated and scales beyond four int fields.


Using a tuple as follows is 15× faster than your original solution:

type DeviceStatus = int * int * int

let rec UpdateStatusITimes (rpm, pressure, temp) (i:int) = 
    match i with
    | 0 -> rpm, pressure, temp
    | _ -> UpdateStatusITimes (90,pressure,temp) (i - 1)

while true do
  let initStatus = 80, 100, 70
  let stopwatch = new Stopwatch()

  stopwatch.Start()
  let rpm,_,_ as endStatus = UpdateStatusITimes initStatus 100000000
  stopwatch.Stop()

  printfn "endStatus.RPM = %A" rpm
  printfn "Took %fs" stopwatch.Elapsed.TotalSeconds

BTW, you should use stopwatch.Elapsed.TotalSeconds when timing.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜