Skip to content

Instantly share code, notes, and snippets.

@Horusiath
Created August 30, 2016 09:39
Show Gist options
  • Save Horusiath/b212ca115aee4ad688326c0515a3c8a6 to your computer and use it in GitHub Desktop.
Save Horusiath/b212ca115aee4ad688326c0515a3c8a6 to your computer and use it in GitHub Desktop.
AsyncVals - uniform API for working with values and F# Async computations
namespace FSharp.Data.GraphQL
open System
[<Struct>]
type AsyncVal<'T> =
val Value : 'T
val Async : Async<'T>
new (value: 'T) = { Value = value; Async = Unchecked.defaultof<Async<'T>> }
new (async: Async<'T>) = { Value = Unchecked.defaultof<'T>; Async = async }
member x.IsAsync = not (System.Object.ReferenceEquals(x.Async, null))
member x.IsSync = System.Object.ReferenceEquals(x.Async, null)
static member Zero = AsyncVal<'T>(Unchecked.defaultof<'T>)
[<RequireQualifiedAccess>]
module AsyncVal =
/// Returns true if AsyncVal wraps an Async computation, otherwise false.
let inline isAsync (x: AsyncVal<'T>) = x.IsAsync
/// Returns true if AsyncVal contains immediate result, otherwise false.
let inline isSync (x: AsyncVal<'T>) = x.IsSync
/// Returns value wrapped by current AsyncVal. If it's part of Async computation,
/// it's executed synchronously and then value is returned.
let get (x: AsyncVal<'T>) =
if x.IsSync
then x.Value
else x.Async |> Async.RunSynchronously
/// Create new AsyncVal from Async computation.
let inline ofAsync (a: Async<'T>) = AsyncVal<'T>(a)
/// Returns an AsyncVal wrapper around provided Async computation.
let inline wrap (v: 'T) = AsyncVal<'T>(v)
/// Converts AsyncVal to Async computation.
let toAsync (x: AsyncVal<'T>) =
if x.IsSync
then async.Return x.Value
else x.Async
/// Returns an empty AsyncVal with immediatelly executed value.
let inline empty<'T> : AsyncVal<'T> = AsyncVal<'T>.Zero
/// Maps content of AsyncVal using provided mapping function, returning new
/// AsyncVal as the result.
let map (fn: 'T -> 'Res) (x: AsyncVal<'T>) =
if x.IsSync
then AsyncVal<'Res>(fn x.Value)
else AsyncVal<'Res> (async {
let! result = x.Async
return fn result })
/// Folds content of AsyncVal over provided initial state zero using provided fn.
/// Returns new AsyncVal as a result.
let fold (fn: 'State -> 'T -> 'State) (zero: 'State) (x: AsyncVal<'T>) : AsyncVal<'State> =
if x.IsSync
then AsyncVal<_> (fn zero x.Value)
else ofAsync <| async {
let! res = x.Async
return fn zero res }
/// Binds AsyncVal using binder function to produce new AsyncVal.
let bind (binder: 'T -> AsyncVal<'U>) (x: AsyncVal<'T>) : AsyncVal<'U> =
if x.IsSync
then binder x.Value
else ofAsync <| async {
let! value = x.Async
let bound = binder value
if bound.IsSync
then return bound.Value
else return! bound.Async }
/// Converts array of AsyncVals into AsyncVal with array results.
/// In case when are non-immediate values in provided array, they are
/// executed asynchronously, one by one with regard to their order in array.
let collectSequential (values: AsyncVal<'T> []) : AsyncVal<'T []> =
let i, a = values |> Array.partition isSync
match i, a with
| [||], [||] -> AsyncVal<_> [||]
| immediates, [||] ->
let x = immediates |> Array.map (fun v -> v.Value)
AsyncVal<_> x
| [||], awaitings ->
let asyncs = awaitings |> Array.map (fun v -> v.Async)
let x = async {
let results = Array.zeroCreate asyncs.Length
let mutable i = 0
for a in asyncs do
let! res = a
results.[i] <- res
i <- i + 1
return results
}
ofAsync x
| immediates, awaitings ->
//TODO: optimize
let ready = immediates |> Array.map (fun v -> v.Value)
let asyncs = awaitings |> Array.map (fun v -> v.Async)
let x = async {
let results = Array.zeroCreate (ready.Length + asyncs.Length)
Array.Copy(ready, results, ready.Length)
let mutable i = ready.Length
for a in asyncs do
let! res = a
results.[i] <- res
i <- i + 1
return results
}
ofAsync x
/// Converts array of AsyncVals into AsyncVal with array results.
/// In case when are non-immediate values in provided array, they are
/// executed all in parallel, in unordered fashion.
let collectParallel (values: AsyncVal<'T> []) : AsyncVal<'T []> =
let i, a = values |> Array.partition isSync
match i, a with
| [||], [||] -> AsyncVal<_> [||]
| immediates, [||] ->
let x = immediates |> Array.map (fun v -> v.Value)
AsyncVal<_> x
| [||], awaitings ->
let x = awaitings |> Array.map (fun v -> v.Async) |> Async.Parallel
ofAsync x
| immediates, awaitings ->
//TODO: optimize
let len = immediates.Length
let asyncs = awaitings |> Array.map (fun v -> v.Async)
let results = Array.zeroCreate (len + asyncs.Length)
for i = 0 to len - 1 do
results.[i] <- immediates.[i].Value
let x = async {
let! asyncResults = asyncs |> Async.Parallel
Array.Copy(asyncResults, 0, results, len, asyncResults.Length)
return results
}
ofAsync x
type AsyncValBuilder () =
member x.Zero () = AsyncVal.empty
member x.Return v = AsyncVal.wrap v
member x.ReturnFrom (v: AsyncVal<_>) = v
member x.ReturnFrom (a: Async<_>) = AsyncVal.ofAsync a
member x.Bind (v: AsyncVal<'T>, binder: 'T -> AsyncVal<'U>) =
AsyncVal.bind binder v
member x.Bind (a: Async<'T>, binder: 'T -> AsyncVal<'U>) =
AsyncVal.ofAsync <| async {
let! value = a
let bound = binder value
if bound.IsSync
then return bound.Value
else return! bound.Async }
[<AutoOpen>]
module AsyncExtensions =
/// Computation expression for working on AsyncVals.
let asyncVal = AsyncValBuilder ()
/// Active pattern used for checking if AsyncVal contains immediate value.
let (|Immediate|_|) (x: AsyncVal<'T>) = if x.IsSync then Some x.Value else None
/// Active patter used for checking if AsyncVal wraps an Async computation.
let (|Async|_|) (x: AsyncVal<'T>) = if x.IsAsync then Some x.Async else None
type Microsoft.FSharp.Control.AsyncBuilder with
member x.ReturnFrom (v: AsyncVal<'T>) =
if v.IsSync
then async.Return v.Value
else async.ReturnFrom v.Async
member x.Bind (v: AsyncVal<'T>, binder) =
if v.IsSync
then async.Bind (async.Return v.Value, binder)
else async.Bind (v.Async, binder)
Host Process Environment Information:
BenchmarkDotNet.Core=v0.9.9.0
OS=Microsoft Windows NT 6.2.9200.0
Processor=Intel(R) Core(TM) i5-6300HQ CPU 2.30GHz, ProcessorCount=4
Frequency=2250003 ticks, Resolution=444.4439 ns, Timer=TSC
CLR=MS.NET 4.0.30319.42000, Arch=32-bit RELEASE
GC=Concurrent Workstation
JitModules=clrjit-v4.6.1080.0

Type=AsyncValBenchmark  Mode=Throughput  
                   Method |         Median |        StdDev |           Mean |            Min |            Max |         Op/s |  Gen 0 | Gen 1 | Gen 2 | Bytes Allocated/Op |

----------------------------- |--------------- |-------------- |--------------- |--------------- |--------------- |------------- |------- |------ |------ |------------------- | AsyncValImmediate | 1.6839 ns | 0.0751 ns | 1.6706 ns | 1.5039 ns | 1.9003 ns | 598601005.38 | - | - | - | 0,00 | AsyncValAwaiting | 13.3068 ns | 0.2712 ns | 13.2326 ns | 12.5351 ns | 13.6924 ns | 75571012.57 | 0.98 | - | - | 16,84 | AsyncReturnImmediatelly | 5,941.8629 ns | 128.0423 ns | 5,946.7189 ns | 5,691.9751 ns | 6,294.0658 ns | 168159.96 | 11.73 | - | - | 225,95 | AsyncValCollectionAllSync | 1,552.9366 ns | 31.5452 ns | 1,552.2934 ns | 1,508.9922 ns | 1,592.1864 ns | 644208.09 | 76.77 | - | - | 1 374,25 | AsyncValCollectionAllAsync | 55,857.6729 ns | 1,508.7384 ns | 56,247.1744 ns | 53,983.7605 ns | 59,062.2042 ns | 17778.67 | 989.99 | 17.25 | - | 19 895,02 | AsyncCollection | 52,576.9698 ns | 587.3722 ns | 52,468.4413 ns | 50,815.5789 ns | 53,362.3420 ns | 19059.08 | 986.00 | 18.00 | - | 20 248,36 | AsyncValCollectionMixed90x10 | 17,619.9174 ns | 149.0434 ns | 17,605.3680 ns | 17,372.0065 ns | 17,883.7110 ns | 56800.86 | 223.00 | 4.50 | - | 4 028,43 |

@Horusiath
Copy link
Author

Horusiath commented Aug 30, 2016

This is still PoC, and some operations used in the benchmarks still can be optimized.

It already contains API to work with AsyncVals as well as computation expression asyncVal which allow to work using them in combination with F# Async + interop methods to make possible using it from within Async computation.

  • AsyncValImmediate vs AsyncReturnImmediatelly allows to compare the performance of retrieving value directly from AsyncVal<> vs using async { return x }. As you may see, the AsyncVal introduces no heap allocations and is roughly 3500 times faster than using Async.
  • Next comparison is related to AsyncCollection row. It's measuring returning value after flattening Async<'a>[] into Async<'a []>. To compare:
    • AsyncValCollectionAllSync shows an example where instead of Async<'a> []Async<'a []> we use AsyncVal<'a> []AsyncVal<'a []> where all values are immedate (so there are no asyncs inside). It's around 34 times faster and uses 14 times less memory.
    • AsyncValCollectionAllAsync shows the same example, but here all AsyncVals contains async computation instead. Memory overhead in that case is minimal (less than 10% more than using pure Async).
    • AsyncValCollectionMixed90x10 is the most interesting one, as it flattens array where 90% of AsyncVals are synchronous and 10% contain async computation. It's still almost 3 times faster and uses 5 times less memory tha using Async.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment