performance - 我可以在 F# 中与收集函数并行执行两个求和,或者通常优化整个事情

标签 performance collections f#

我有以下代码:

// volume queue
let volumeQueue = Queue<float>()
let queueSize = 10 * 500 // 10 events per second, 500 seconds max

// add a signed volume to the queue
let addToVolumeQueue x =
    volumeQueue.Enqueue(x)
    while volumeQueue.Count > queueSize do volumeQueue.TryDequeue() |> ignore

// calculate the direction of the queue, normalized between +1 (buy) and -1 (sell)
let queueDirection length =
    let subQueue =
        volumeQueue
        |> Seq.skip  (queueSize - length)

    let boughtVolume =
        subQueue
        |> Seq.filter (fun l -> l > 0.)
        |> Seq.sum

    let totalVolume =
        subQueue
        |> Seq.sumBy (fun l -> abs l)

    2. * boughtVolume / totalVolume - 1.

它的作用是运行一个固定长度的队列,向其中添加事务量,一些是正的,一些是负的。

然后它计算正项与负项的累积比率,并将其在 +1 和 -1 之间归一化,0 表示总和为一半。

目前没有优化,但这段代码的性能很重要。所以我想让它变快,同时又不影响可读性(大约每 100 毫秒调用一次)。

首先想到的是在一个循环中同时计算两个和(正数和所有数字)。用for循环很容易搞定,但是用集合函数可以搞定吗?

我考虑的下一个选项是摆脱队列并使用循环缓冲区,但由于代码在缓冲区的一部分(最后的“长度”项)上运行,我必须处理环绕部分;我想我可以将缓冲区扩展到 2 的幂的大小并以这种方式自动环绕。

欢迎提出任何想法,但我的第一个原始问题是:我可以使用集合函数在一次传递中完成两个总和吗?我不能用索引器在队列中迭代,所以我不能使用 for 循环(或者我想我必须实例化一个迭代器)

最佳答案

首先,在 F# 中使用可变变量和循环本身并没有错。尤其是在小范围内(例如在函数内部),这通常非常可读 - 或者至少,如果有合适的注释,则很容易理解。

要使用单次迭代执行此操作,您可以使用 fold。这基本上以牺牲一些可读性为代价在一次迭代中计算两个总和:

let queueDirectionFold length =
  let boughtVolume, totalVolume =
      volumeQueue
      |> Seq.skip  (queueSize - length)
      |> Seq.fold (fun (bv, tv) v ->
        (if v > 0.0 then bv else bv + v), tv + abs v) (0.0, 0.0)   
  2. * boughtVolume / totalVolume - 1.

正如我之前提到的,我也会考虑使用循环。循环本身非常简单,但是由于您需要跳过一些元素而增加了一些复杂性。不过,我认为这很清楚:

let queueDirectionLoop length =
  let mutable i = 0 
  let mutable boughtVolume = 0.
  let mutable totalVolume = 0.
  for v in volumeQueue do
    if i >= queueSize - length then 
      totalVolume <- totalVolume + abs v
      if v > 0. then boughtVolume <- boughtVolume + v
    i <- i + 1
  2. * boughtVolume / totalVolume - 1.

我使用 4000 个元素测试了性能,这是我得到的结果:

#time 
let rnd = System.Random()
for i in 0 .. 4000 do volumeQueue.Enqueue(rnd.NextDouble())

for i in 0 .. 10000 do ignore(queueDirection 1000)      // ~900 ms 
for i in 0 .. 10000 do ignore(queueDirectionFold 1000)  // ~460 ms
for i in 0 .. 10000 do ignore(queueDirectionLoop 1000)  // ~370 ms

只对队列进行一次迭代绝对有助于提高性能。在命令式循环中执行此操作有助于提高性能——如果您关心性能,这可能是值得的。代码的可读性可能比原始代码差一点,但我认为它并不比 fold 差多少。

关于performance - 我可以在 F# 中与收集函数并行执行两个求和,或者通常优化整个事情,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/63746816/

相关文章:

C# 应用程序每次调用都需要更多内存

java - Java Collections API 上的依赖注入(inject)

java - 集合框架中的接口(interface)

java - 是否有与 Javascript 的 "some"方法等效的 Java?

永远不会匹配与元组规则匹配的 F# 模式

f# - 如何在 F# 中合并两个日期列表?

c - 霍夫曼表熵解码简化(在 C 中)

c - 如何乘以 TB 大小的数字?

java - PDF 的高效 SVG 渲染(Java、Batik、Flying Saucer)

f# - 为什么 $ 允许但 $$ 或 <$> 不允许作为运算符 (FS0035) 以及 $ 的特殊之处?