【问题标题】:Aggregation function - f# vs c# performance聚合函数 - f# vs c# 性能
【发布时间】:2013-10-18 13:41:29
【问题描述】:

我有一个经常使用的函数,因此性能需要尽可能好。它从 excel 中获取数据,然后根据数据是否在某个时间段内以及是否是高峰时段(周一至周五 8-20 日)对部分数据进行求和、平均或计数。

数据通常约为 30,000 行和 2 列(每小时日期、值)。数据的一个重要特征是日期列按时间顺序排列

我有三个实现,带有扩展方法的 c#(非常慢,除非有人感兴趣,否则我不会展示它)。

然后我有这个 f# 实现:

let ispeak dts =
    let newdts = DateTime.FromOADate dts
    match newdts.DayOfWeek, newdts.Hour with
    | DayOfWeek.Saturday, _ | DayOfWeek.Sunday, _ -> false
    | _, h when h >= 8 && h < 20 -> true
    | _ -> false

let internal isbetween a std edd =
    match a with
    | r when r >= std && r < edd+1. -> true
    | _ -> false

[<ExcelFunction(Name="aggrF")>]
let aggrF (data:float[]) (data2:float[]) std edd pob sac =
    let newd =
        [0 .. (Array.length data) - 1]
        |> List.map (fun i -> (data.[i], data2.[i])) 
        |> Seq.filter (fun (date, _) -> 
            let dateInRange = isbetween date std edd
            match pob with
            | "Peak" -> ispeak date && dateInRange
            | "Offpeak" -> not(ispeak date) && dateInRange
            | _ -> dateInRange)
   match sac with 
   | 0 -> newd |> Seq.averageBy (fun (_, value) -> value)
   | 2 -> newd |> Seq.sumBy (fun (_, value) -> 1.0)
   | _ -> newd |> Seq.sumBy (fun (_, value) -> value)

我发现这有两个问题:

  1. 我需要准备数据,因为日期和值都是双精度[]
  2. 我没有利用日期按时间顺序排列的知识,因此我进行了不必要的迭代。

现在我称之为蛮力命令式 c# 版本来了:

        public static bool ispeak(double dats)
    {
        var dts = System.DateTime.FromOADate(dats);
        if (dts.DayOfWeek != DayOfWeek.Sunday & dts.DayOfWeek != DayOfWeek.Saturday & dts.Hour > 7 & dts.Hour < 20)
            return true;
        else
            return false;
    }

    [ExcelFunction(Description = "Aggregates HFC/EG into average or sum over period, start date inclusive, end date exclusive")]
    public static double aggrI(double[] dts, double[] vals, double std, double edd, string pob, double sumavg)
    {
        double accsum = 0;
        int acccounter = 0;
        int indicator = 0;
        bool peakbool = pob.Equals("Peak", StringComparison.OrdinalIgnoreCase);
        bool offpeakbool = pob.Equals("Offpeak", StringComparison.OrdinalIgnoreCase);
        bool basebool = pob.Equals("Base", StringComparison.OrdinalIgnoreCase);


        for (int i = 0; i < vals.Length; ++i)
        {
            if (dts[i] >= std && dts[i] < edd + 1)
            {
                indicator = 1;
                if (peakbool && ispeak(dts[i]))
                {
                    accsum += vals[i];
                    ++acccounter;
                }
                else if (offpeakbool && (!ispeak(dts[i])))
                {
                    accsum += vals[i];
                    ++acccounter;
                }
                else if (basebool)
                {
                    accsum += vals[i];
                    ++acccounter;
                }
            }
            else if (indicator == 1)
            {
                break;
            }
        }

        if (sumavg == 0)
        {
            return accsum / acccounter;
        }
        else if (sumavg == 2)
        {
            return acccounter;
        }
        else
        {
            return accsum;
        }
    }

这要快得多(我猜主要是因为周期结束时退出循环),但明显不那么简洁。

我的问题:

  1. 有没有办法停止 f# Seq 模块中排序序列的迭代?

  2. 还有其他方法可以加快 f# 版本的速度吗?

  3. 有人能想出更好的方法吗? 非常感谢!

更新:速度比较

我设置了一个测试数组,其中包含 1/1/13-31/12/15 的每小时日期(大约 30,000 行)和相应的值。我在日期数组上进行了 150 次调用,并重复了 100 次 - 15000 次函数调用:

我上面的 csharp 实现(使用 string.compare 在循环之外)

1.36 秒

马修斯递归 fsharp

1.55 秒

Tomas 数组 fsharp

1 分 40 秒

我原来的 fsharp

2 分 20 秒

显然,这对我的机器来说总是主观的,但给出了一个想法,人们要求它......

我还认为应该记住,这并不意味着递归或 for 循环总是比 array.map 等更快,只是在这种情况下,它会进行很多不必要的迭代,因为它没有 c# 的早期退出迭代而f#递归方法有

【问题讨论】:

  • 你有性能数据吗?对我来说最明显的事情是在循环外进行字符串比较,而不是在可能执行 900,000 次的 for 循环内进行。
  • 感谢马修!确实,这会使 c# 更快一点。我将不得不制作性能数字,差异如此之大,以至于当我将函数拖到 100 多个 excel 单元格时我可以看到它......请耐心等待几分钟

标签: f#


【解决方案1】:

使用Array 代替ListSeq 可以使这个速度快3-4 倍。您无需生成索引列表,然后将其映射以查找两个数组中的项目 - 您可以使用 Array.zip 将两个数组合并为一个数组,然后使用 Array.filter

一般来说,如果你想要性能,那么使用数组作为你的数据结构是有意义的(除非你有很长的管道)。像 Array.zipArray.map 这样的函数可以计算整个数组的大小,分配它,然后执行高效的命令式操作(同时从外部看起来仍然是功能性的)。

let aggrF (data:float[]) (data2:float[]) std edd pob sac =
    let newd =
        Array.zip data data2 
        |> Array.filter (fun (date, _) -> 
            let dateInRange = isbetween date std edd
            match pob with
            | "Peak" -> ispeak date && dateInRange
            | "Offpeak" -> not(ispeak date) && dateInRange
            | _ -> dateInRange)
    match sac with 
    | 0 -> newd |> Array.averageBy (fun (_, value) -> value)
    | 2 -> newd |> Array.sumBy (fun (_, value) -> 1.0)
    | _ -> newd |> Array.sumBy (fun (_, value) -> value)

我还更改了isbetween - 它可以简化为一个表达式,您可以将其标记为inline,但这并没有增加那么多:

let inline isbetween r std edd = r >= std && r < edd+1.

为了完整起见,我使用以下代码(使用 F# Interactive)对此进行了测试:

#time 
let d1 = Array.init 1000000 float
let d2 = Array.init 1000000 float
aggrF d1 d2 0.0 1000000.0 "Test" 0

原始版本约为 600 毫秒,而使用数组的新版本需要 160 毫秒到 200 毫秒。 Matthew 的版本大约需要 520 毫秒。

除此之外,我在 BlueMountain Capital 度过了过去两个月,致力于为 F# 开发一个时间序列/数据框架库,这将使这变得更加简单。它正在进行中,库的名称也将更改,但您可以在BlueMountain GitHub 中找到它。代码看起来像这样(它使用时间序列是有序的事实,并在过滤之前使用切片来获取相关部分):

let ts = Series(times, values)
ts.[std .. edd] |> Series.filter (fun k _ -> not (ispeak k)) |> Series.mean

目前,这不会像直接数组操作那样快,但我会研究一下 :-)。

【讨论】:

  • 感谢托马斯。我会在github页面上看看你做了什么
【解决方案2】:

加快速度的一种直接方法是将这些结合起来:

[0 .. (Array.length data) - 1]
    |> List.map (fun i -> (data.[i], data2.[i])) 
    |> Seq.filter (fun (date, _) -> 

进入一个单一的列表理解,也正如另一个马修所说,做一个单一的字符串比较:

let aggrF (data:float[]) (data2:float[]) std edd pob sac =
    let isValidTime = match pob with
                        | "Peak" -> (fun x -> ispeak x)
                        | "Offpeak" -> (fun x -> not(ispeak x))
                        | _ -> (fun _ -> true)

    let data = [ for i in 0 .. (Array.length data) - 1 do 
                  let (date, value) = (data.[i], data2.[i])
                  if isbetween date std edd && isValidTime date then
                      yield (date, value)
                  else
                      () ]

    match sac with 
    | 0 -> data |> Seq.averageBy (fun (_, value) -> value)
    | 2 -> data.Length
    | _ -> data |> Seq.sumBy (fun (_, value) -> value)

或者使用尾递归函数:

let aggrF (data:float[]) (data2:float[]) std edd pob sac =
    let isValidTime = match pob with
                        | "Peak" -> (fun x -> ispeak x)
                        | "Offpeak" -> (fun x -> not(ispeak x))
                        | _ -> (fun _ -> true)

    let endDate = edd + 1.0

    let rec aggr i sum count =
        if i >= (Array.length data) || data.[i] >= endDate then
            match sac with 
            | 0 -> sum / float(count)
            | 2 -> float(count)
            | _ -> float(sum)
        else if data.[i] >= std && isValidTime data.[i] then
            aggr (i + 1) (sum + data2.[i]) (count + 1)
        else
            aggr (i + 1) sum count

    aggr 0 0.0 0

【讨论】:

  • 感谢马修!我真的很喜欢f#它是如此的整洁!这个版本比旧的 fsharp 快,但仍然比 c# 慢,我什至还没有更正循环外的 string.compare...
  • 这里的另一个优化可能是让搜索函数在数据数组中找到日期范围的开始和结束,然后遍历该范围。完全以另一种方式做到这一点,您可以让尾递归函数执行 c# 函数中的 for 循环正在执行的操作
  • 在我的机器上,这个版本的运行时间约为 520 毫秒,而原始版本约为 600 毫秒。如果你使用Array.zipArray.filter,大约需要~180ms :-)
  • @TomasPetricek 你的提速非常快,尾递归函数比较如何?
  • @MatthewMcveigh 我想这会更快,但我还没有测试过。
猜你喜欢
  • 2011-03-15
  • 2016-06-29
  • 2013-02-13
  • 2020-06-07
  • 2023-04-01
  • 1970-01-01
  • 2013-06-25
  • 2015-02-17
  • 2016-04-09
相关资源
最近更新 更多