首页 > 解决方案 > 我可以在 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 循环(或者我想我必须实例化一个迭代器)

标签: performancecollectionsf#

解决方案


首先,在 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.


推荐阅读