Как сделать неизменяемый F # более производительным? - PullRequest
12 голосов
/ 14 апреля 2011

Я хочу написать большой кусок кода на C #, используя неизменный 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'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

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

Ответы [ 4 ]

12 голосов
/ 15 апреля 2011

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

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 s + вместо 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 }

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

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 }
7 голосов
/ 14 апреля 2011

Я подозреваю, что проблема с производительностью возникает из-за обнуления памяти блока при клонировании записи (плюс незначительное время для ее выделения и последующего сбора мусора) на каждой итерации цикла.Вы можете переписать ваш пример, используя структуру:

[<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 байт, иначе она будет скопирована так же медленно, как и запись.

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

7 голосов
/ 14 апреля 2011

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

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

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

4 голосов
/ 31 декабря 2011

Использование следующего кортежа в 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 при синхронизации.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...