Производительность async в F # vs С# (есть ли лучший способ написать async {...})

FWIW Я думаю, что проблемы, описанные здесь, просто сводятся к тому, что компилятор С# умнее, и делает эффективную модель на основе конечного автомата для обработки асинхронного кода, тогда как компилятор F # создает множество объектов и вызовов функций, которые обычно менее эффективны.

В любом случае, если у меня есть функция С# ниже:

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 # значительно медленнее и больше головок процессора, чем версия С#. Мне было интересно, лучше ли это сочинить. Я попытался удалить рекурсивную функцию (которая казалась немного уродливой с отсутствием времени!) И не изменяет let! S) следующим образом:

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 # заняла ~ два раза и потребляла ~ вдвое больше ресурсов ЦП за все время ее работы - таким образом, занимая примерно 4 раза больше ресурсов (т.е. внутренне она должна откручивать больше потоков?).

(Возможно, несколько сомневаюсь, что вы читаете такую ​​тривиальную структуру? Я действительно просто выталкиваю машину, чтобы посмотреть, что она делает. По сравнению с не-асинхронной версией (то есть просто прямым чтением) С# один заканчивается в одно и то же время, но потребляет > вдвое больше CPU, т.е. прямое чтение() потребляет < 1/8 ресурсов f #)

Итак, мой вопрос в том, что я делаю F # async "правильным" способом (это было мое первое попытку использования)?

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

Ответ 1

F # Async и граница TPL (Async.AwaitTask/Async.StartAsTask) - самая медленная вещь. Но в целом F # Async работает медленнее и должен использоваться для привязки IO, а не к задачам, связанным с ЦП. Вы можете найти этот репо интересным: https://github.com/buybackoff/FSharpAsyncVsTPL

В основном, я сравнивал два, а также выражение вычисления компоновщика задач, которое первоначально было из проекта FSharpx. Задание задач намного быстрее при использовании вместе с TPL. Я использую этот подход в своей библиотеке Spreads, которая написана в F #, но использует TPL. На эта строка является высоко оптимизированной связью выражения вычислений, которая фактически делает то же самое, что и С# async/ждет за кулисами. Я сравнивал каждое использование выражения вычисления task{} в библиотеке и очень быстро (gotcha не должен использоваться для выражения while, а рекурсии). Кроме того, он делает код совместимым с С#, в то время как асинхронный вызов F # не может быть использован с С#.