Как сделать неизменным 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
при синхронизации.