Как сделать неизменным F # более совершенным?

Я хочу написать большой кусок кода С#, используя неизменяемый F #. Это монитор устройства, и текущая реализация работает, постоянно получая данные из последовательного порта и обновляя переменные-члены на основе новых данных. Я бы хотел передать это на F # и получить преимущества неизменных записей, но мой первый выстрел по реализации концепции доказательств очень медленный.

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 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

Это работает примерно на 1400 мс на моей машине, тогда как эквивалентный код С# (с изменяемыми переменными-членами) работает примерно в 310 мс. Есть ли способ ускорить это, не теряя неизменности? Я надеялся, что компилятор F # заметил бы, что initStatus и все промежуточные переменные состояния никогда не использовались повторно, и, таким образом, просто мутировать эти записи за сценой, но я думаю, что нет.

Ответ 1

В сообществе F # императивный код и изменяемые данные не подвергаются сомнению, если они не являются частью вашего открытого интерфейса. I.e., используя изменяемые данные, является прекрасным, пока вы инкапсулируете его и изолируете его от остальной части вашего кода. С этой целью я предлагаю что-то вроде:

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

Таким образом, открытый интерфейс не затрагивается – UpdateStatusITimes по-прежнему принимает и возвращает неотъемлемо неизменяемый DeviceStatus – но внутри UpdateStatusITimes использует изменяемый класс, чтобы исключить издержки распределения.

РЕДАКТИРОВАТЬ: (В ответ на комментарий) Это стиль класса, который я обычно предпочитаю, используя первичный конструктор и let + свойства, а не val s:

[<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 }

Но для простых классов фасадов, где каждое свойство будет слепым getter/setter, я считаю это немного утомительным.

F # 3+ позволяет вместо этого использовать следующее, но я до сих пор не считаю его улучшением лично (если только догматически не избегает полей):

[<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 }

Ответ 2

Это не ответит на ваш вопрос, но, вероятно, стоит отступить и рассмотреть общую картину:

  • Что вы воспринимаете как преимущество неизменных структур данных для этого варианта использования? F # также поддерживает измененные структуры данных.
  • Вы утверждаете, что F # "очень медленный", но он всего в 4,5 раза медленнее, чем код С#, и делает более 70 миллионов обновлений в секунду... Возможно, это неприемлемая производительность для вашего реального приложения? У вас есть конкретная целевая задача? Есть ли основания полагать, что этот тип кода станет узким местом в вашем приложении?

Дизайн всегда связан с компромиссами. Вы можете обнаружить, что для записи многих изменений за короткий промежуток времени неизменные структуры данных имеют неприемлемое ограничение производительности при ваших потребностях. С другой стороны, если у вас есть требования, такие как отслеживание нескольких старых версий структуры данных одновременно, преимущества неизменяемых структур данных могут сделать их привлекательными, несмотря на снижение производительности.

Ответ 3

Я подозреваю, что проблема с производительностью, которую вы видите, связана с обнулением памяти блока при клонировании записи (плюс незначительное время для ее выделения и последующего сбора мусора) на каждой итерации цикла. Вы можете переписать свой пример с помощью 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)

Теперь производительность будет близка к производительности с использованием глобальных изменяемых переменных или путем переопределения UpdateStatusITimes status i как UpdateStatusITimes rpm pres temp i. Это будет работать, только если ваша структура имеет длину не более 16 байтов, так как в противном случае она будет скопирована тем же вялым способом, что и запись.

Если, как вы намекали в своих комментариях, вы намерены использовать это как часть многопоточного дизайна с разделяемой памятью, тогда вам понадобится изменчивость в какой-то момент. Ваши варианты: a) общая переменная переменной для каждого параметра; b) одна разделяемая переменная, содержащая struct или c) общий объект фасада, содержащий изменяемые поля (например, в ответ ildjarn). Я бы пошел на последний, так как он красиво инкапсулирован и масштабируется за пределы четырех полей int.

Ответ 4

Использование кортежа в следующем порядке на 15 раз быстрее, чем ваше исходное решение:

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

Кстати, вы должны использовать stopwatch.Elapsed.TotalSeconds при синхронизации.