F# under the covers XIX -- `with` clauses
Once again, the half-year update has included some interesting changes in code generation, this time around try
/with
clauses.
Two cases to show this, one simple, one more complex. As always, decompilations are for debug (unoptimized) builds, though in this case, the release builds differ only in details of the branch instructions.
try ... with | :? FormatException -> Default
try ... with | :? ArgumentException as a -> a |> (logException store) | :? NotSupportedException as n -> n |> (logException store) | :? IOException as i -> i |> (logException store) | :? System.Security.SecurityException as s -> s |> (logException store) | :? UnauthorizedAccessException as u -> u |> (logException store)
Before the 6.0.300 SDK update, the simple case was entirely direct in its handling of the exception
.try { ... IL_006d: leave.s IL_0098 } // end .try catch [netstandard]System.Object { IL_006f: castclass [netstandard]System.Exception IL_0074: stloc.s 8 IL_0076: ldloc.s 8 IL_0078: isinst [netstandard]System.FormatException
but afterwards it becomes much fancier, with duplication of effort in the handled case, and only makes sense if the exit from the filter
in the unhandled case is faster than from a catch
.
.try { ... IL_006e: leave.s IL_00b5 } // end .try filter { IL_0070: castclass [netstandard]System.Exception IL_0075: stloc.s 8 IL_0077: ldloc.s 8 IL_0079: isinst [netstandard]System.FormatException IL_007e: stloc.s 9 ... } // end filter catch { IL_008c: castclass [netstandard]System.Exception IL_0091: stloc.s 10 IL_0093: ldloc.s 10 IL_0095: isinst [netstandard]System.FormatException IL_009a: stloc.s 11 ... } // end handler
the new code being equivalent to C# catch (object obj2) when ((((Exception)obj2) is FormatException) ? true : false)
The more complex case was like
catch [netstandard]System.Object { IL_0010: castclass [netstandard]System.Exception IL_0015: stloc.1 // ArgumentException ex2 = ex as ArgumentException; IL_0016: ldloc.1 IL_0017: isinst [netstandard]System.ArgumentException IL_001c: stloc.2 // if (ex2 == null) IL_001d: ldloc.2 IL_001e: brfalse.s IL_0022 ...
or, decompiled
catch (object obj) { Exception ex = (Exception)obj; ArgumentException ex2 = ex as ArgumentException; if (ex2 == null) { NotSupportedException ex3 = ex as NotSupportedException; if (ex3 == null) { // ... throw on exhaustion } NotSupportedException j = ex3; Exception e4 = j; logException(store, e4); return result; } ArgumentException a = ex2; Exception e5 = a; logException(store, e5); return result; }
but now it looks like
catch [netstandard]System.Object { IL_0010: castclass [netstandard]System.Exception IL_0015: stloc.1 // object obj2 = ex; IL_0016: ldloc.1 IL_0017: stloc.2 // if (!(obj2 is ArgumentException)) IL_0018: ldloc.2 IL_0019: isinst [netstandard]System.ArgumentException IL_001e: ldnull // omitted in release IL_001f: cgt.un // omitted in release IL_0021: brtrue.s IL_0065 ... IL_0065: ldloc.1 IL_0066: unbox.any [netstandard]System.ArgumentException IL_006b: stloc.s 7
or in C#
catch (object obj) { Exception ex = (Exception)obj; object obj2 = ex; if (!(obj2 is ArgumentException)) { object obj3 = ex; if (!(obj3 is NotSupportedException)) { // ... throw on exhaustion } NotSupportedException j = (NotSupportedException)(object)ex; NotSupportedException ex5 = j; bool store5 = store; NotSupportedException e4 = ex5; logException(store5, e4); return result; } ArgumentException a = (ArgumentException)(object)ex; ArgumentException ex6 = a; bool store6 = store; ArgumentException e5 = ex6; logException(store6, e5); return result; }
which discards the results of the isinst
instructions, then does an unbox.any
for any case that actually matches; which could provide a marginal space improvement, in the case where no clause matches -- i.e. another possible optimization the unhandled exception scenario; which in both cases are the "I don't care, this has just failed completely" route.