问题 F#与C#中异步的性能(是否有更好的方法来编写异步{...})


FWIW我认为这里详述的问题归结为c#编译器更智能,并且使得基于状态机的高效模型能够处理异步代码,而F#编译器创建了大量通常效率较低的对象和函数调用。

无论如何,如果我有下面的c#函数:

public async static Task<IReadOnlyList<T>> CSharpAsyncRead<T>(
         SqlCommand cmd,
         Func<SqlDataReader, T> createDatum)
{
    var result = new List<T>();
    var reader = await cmd.ExecuteReaderAsync();

    while (await reader.ReadAsync())
    {
        var datum = createDatum(reader);
        result.Add(datum);
    }

    return result.AsReadOnly();
}

然后将其转换为F#,如下所示:

let fsharpAsyncRead1 (cmd:SqlCommand) createDatum = async {
    let! reader =
        Async.AwaitTask (cmd.ExecuteReaderAsync ())

    let rec readRows (results:ResizeArray<_>) = async {
        let! readAsyncResult = Async.AwaitTask (reader.ReadAsync ())
        if readAsyncResult then
            let datum = createDatum reader
            results.Add datum
            return! readRows results
        else
            return results.AsReadOnly() :> IReadOnlyList<_>
    }

    return! readRows (ResizeArray ())
}

然后我发现f#代码的性能比c#版本慢得多,并且CPU更多。我想知道是否有更好的组成。我尝试删除递归函数(看起来有点难看,而不是!而且没有可变的!),如下所示:

let fsharpAsyncRead2 (cmd:SqlCommand) createDatum = async {
    let result = ResizeArray () 

    let! reader =
        Async.AwaitTask (cmd.ExecuteReaderAsync ())

    let! moreData = Async.AwaitTask (reader.ReadAsync ())
    let mutable isMoreData = moreData
    while isMoreData do
        let datum = createDatum reader

        result.Add datum

        let! moreData = Async.AwaitTask (reader.ReadAsync ())
        isMoreData <- moreData

    return result.AsReadOnly() :> IReadOnlyList<_>
}

但表现基本相同。

作为性能的一个例子,当我加载一条市场数据时,例如:

type OHLC = {
    Time  : DateTime
    Open  : float
    High  : float
    Low   : float
    Close : float
}

在我的机器上,F#异步版本需要大约两倍的时间,并且在整个运行时消耗了〜两倍的CPU资源 - 因此占用了大约4倍的资源(即内部必须旋转更多线程?)。

(可能读取这样一个简单的结构有点不确定吗?我真的只是在机器上查看它的作用。与非异步版本(即直接读取)相比,c#one完成了〜同时,但消耗> CPU的两倍。即直接Read()消耗<f#资源的1/8)

所以我的问题是,当我以“正确”的方式进行F#异步时(这是我第一次尝试使用)?

(...如果我,那么我只需要去修改编译器为编译的Asyncs添加基于状态机的实现......这有多难:-))


10229
2018-03-03 03:40


起源

你确定它们都运行相同的位数,优化等设置吗? - Fyodor Soikin
你看过了吗? hopac (github.com/Hopac/Hopac)?它的CPU开销明显低于 Async (和IIRC低于 Task)。仅供参考;我个人不会考虑使用 Async 要么 TPL 等我需要并行时。如果我受CPU约束,我很可能无法支持抽象。 - Just another metaprogrammer
@FuleSnabel BTW,我确实考虑过Ho​​pac,因为它的确比它快 async{}。但与C#(缺乏)的互操作是一个交易破坏者。 task{} 计算表达式提供了一种以F#惯用方式使用TPL(可能是.NET中最优化的部分)的方法。 - V.B.
@FyodorSoikin是的,确实有相同的位数,优化等等(c#代码只是作为参考添加,我只是交换了调用)。 (稍微偏离主题,但可能有趣,这里的事实是在32位gcServer = false,然后async c#版本比直接非异步Read更快。虽然使用64位gcServer = true但它消失了.I不记得其他配置时间。) - Paul Westcott
@FuleSnabel我记得很久以前读过Hopac,但是没有尝试过,而且忘记了。你的提醒已经重新点燃了我的兴趣。谢谢。 - Paul Westcott


答案:


F#的 Async 和TPL边界(Async.AwaitTask / Async.StartAsTask)是最慢的。但一般来说,F#Async本身较慢,应该用于IO绑定而不是CPU绑定任务。你可能会发现这个回购有趣: https://github.com/buybackoff/FSharpAsyncVsTPL

基本上,我对这两者进行了基准测试,也是一个最初来自的任务构建器计算表达式 FSharpx 项目。与TPL一起使用时,任务构建器要快得多。我在我的Spreads库中使用这种方法 - 这是用F#编写的,但它利用了TPL。上 这条线 是高度优化的计算表达式绑定,它有效地做了与C#的async / await在幕后相同的事情。我对每一次使用的基准测试 task{} 库中的计算表达式非常快(这个问题不是用于/计算表达式,而是递归)。此外,它使代码可以与C#互操作,而F#的异步不能从C#中消耗掉。


11
2018-03-03 08:34



我看不到任何与GetAwaiter()不同的性能。这是我的测试要点 - gist.github.com/manofstick/e0f1cbf14febf3666c06  - 我试过32/64位,有和没有gcServer。 (我检查了m.IsCompleted的快速路径没有一直被击中) - Paul Westcott
...实际上,如果你修改那个要点以增加创建的任务数量(百万),减少他们的工作负荷,这样他们会非常快,创建任务暂停,然后只是Start()他们在绑定之前,您发现ContinueWith优于GetAwaiter版本。 - Paul Westcott
@PaulWestcott当我测试时差异不大并且在误差范围内。使用AsyncTaskMethodBuilder而不是TaskCompletionSource实际上稍微慢一些(百分之几),但我决定尽可能不分配其他对象。 - V.B.
@PaulWestcott逻辑是,ContinueWith是面向用户的接口,有很多空检查和其他检查,而GetAwaiter打算由编译器使用(如MSDN所说)并且是非常低级别的。在我的机器上的Gist中,第一个仍然是非常无意义的数量。需要运行很长时间并进行t测试:)我也在一些地方直接使用GetAwaiter而没有 task{}  - 在某些特殊情况下使用回调很方便。 - V.B.
我把这个作为对FSharp lang设计的建议并在那里引用了你: github.com/fsharp/fslang-suggestions/issues/581 - dustinmoris