Giter VIP home page Giter VIP logo

Comments (7)

rspeele avatar rspeele commented on June 1, 2024

Hi,

Thanks so much for reporting this issue!

My first observation is that this issue does not present itself if I build in release configuration, or in debug configuration with "generate tail calls" enabled under project properties -> build. So that's one workaround.

I will take a look and see if I can fix it without tail calls.

from taskbuilder.fs.

rspeele avatar rspeele commented on June 1, 2024

This should be resolved by b804b6f .

Let me know if it works for you so I can close this.

from taskbuilder.fs.

mbenoni7 avatar mbenoni7 commented on June 1, 2024

Wow, great turnaround. That does indeed fix the issue.

One more question: is this intended to support the recursive CE style as well? If so, there's a similar problem there. For example, if I again rewrite the readFile function from above ...

  let readFile path = task {
        let buffer = Array.zeroCreate bufferSize
        use file = File.OpenRead(path)
        let rec loop n = task {
            if n < 10000 then
                let! countRead = file.ReadAsync(buffer, 0, buffer.Length)
                return! loop (n + 1)
            else
                return ()
        }
        return! loop 0
    }

... this also gives me a stack overflow (in release mode, BTW).

from taskbuilder.fs.

rspeele avatar rspeele commented on June 1, 2024

Another good catch -- I'll take a look at this one tomorrow.

from taskbuilder.fs.

rspeele avatar rspeele commented on June 1, 2024

After mulling this over for a couple days, I don't think it can be done. At least, not with TaskBuilder returning a Task<'a>. It could probably be made to work if TaskBuilder returned its internal Step monad and let you convert that to a task separately, but I think keeping it simple for translation from C# -> F# is too important to do that. So I have chosen to document the behavior in the README instead of change it. I am keeping the refactoring I did while working on this though, since it does at least improve the situation a little.

My reasoning for why it can't work:

If you have some async work followed by a tail call into another task, and you don't know anything about the tail position task other than that it is some Task<'a>, then what you have must be a Task<Task<'a>>.

This is now what the async state machines in TaskBuilder.fs use. The built-in way to convert a Task<Task<'a>> to a Task<'a> is task.Unwrap(), which again, is what TaskBuilder.fs uses now. However, using this at best grows memory usage proportional to the number of iterations (on .NET), and at worst still stack overflows (on Mono). This is because Unwrap, despite its name, does return a wrapper Task to represent its computation and so you end up with a wrapper around a wrapper around a wrapper ... etc. When the innermost wrapper completes, it all unwinds (and may or may not stack overflow in the process), but till then you are at least eating a bunch of heap memory for all those wrappers.

from taskbuilder.fs.

mbenoni7 avatar mbenoni7 commented on June 1, 2024

Interesting. The Task builder in FSharpx.Extras supports this usage, but according to some casual benchmarks of mine, that builder is significantly slower than this one (and generates a ton more garbage). That suggests to me that they have accepted some of the negative performance trade-offs you allude to in your last paragraph.

I can live with a more imperative coding style to get closer to C#-level performance, so I appreciate your contribution. I do hope that one day somebody figures out how to support the idiomatic style with good performance, though.

from taskbuilder.fs.

rspeele avatar rspeele commented on June 1, 2024

Unfortunately, I think the FSharpx.Extras implementation suffers even worse. Because they always use Task.ContinueWith(...).Unwrap() for Bind instead of creating a state machine, there is no way to write a correct long-running loop using that builder.

With FSharpx, both of the below code samples will eventually fail with an OutOfMemoryException. You can watch the memory usage grow into the gigabytes over the course of a minute or two. That memory is being taken up by nested tasks that legitimately cannot be collected (wrappers around wrappers around wrappers).

recursive:

let tailRec () =
    task {
        do! Task.FromResult(())
        return! tailRec ()
    }

iterative:

let whileLoop() =
    task {
        while true do
            do! Task.FromResult(())
    }

With TaskBuilder.fs, the recursive version fails with a StackOverflowException (although if you change Task.FromResult to Task.Yield you'll get an OutOfMemoryException just like the one in FSharpx).

However, in the current TaskBuilder.fs, the iterative version has very stable memory usage. It does allocate as it runs, but the allocations can be collected so quickly that you basically don't see memory usage change after the first few seconds of runtime.

I should probably report the while loop behavior as a bug to FSharpx. It might be awkward for them to fix while keeping the same public API, since their builder lets you specify TaskContinuationOptions, which only make sense when using Task.ContinueWith for bind...

from taskbuilder.fs.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.