Description
In situations like this:
open System
let f (n: float32) =
Console.WriteLine n
let _ = Unchecked.defaultof<decimal>
let _ = Unchecked.defaultof<decimal>
let _ = Unchecked.defaultof<decimal>
let n' = n * 2.f
Console.WriteLine n'
I would expect the compiler to be able to eliminate these unused variables. The compiler sources imply that there's some kind of unused binding elimination (see
fsharp/src/Compiler/Optimize/Optimizer.fs
Line 1570 in 64bb76c
Here's the IL generated (Release mode of course):
.method public static void
f(
float32 n
) cil managed
{
.maxstack 4
.locals init (
[0] valuetype [System.Runtime]System.Decimal V_0
)
// [4 5 - 4 24]
IL_0000: ldarg.0 // n
IL_0001: call void [System.Console]System.Console::WriteLine(float32)
// [5 13 - 5 32]
IL_0006: ldloc.0 // V_0
IL_0007: pop
// [6 13 - 6 32]
IL_0008: ldloca.s V_0
IL_000a: initobj [System.Runtime]System.Decimal
IL_0010: ldloc.0 // V_0
IL_0011: pop
// [7 13 - 7 32]
IL_0012: ldloca.s V_0
IL_0014: initobj [System.Runtime]System.Decimal
IL_001a: ldloc.0 // V_0
IL_001b: pop
// [9 5 - 9 25]
IL_001c: ldarg.0 // n
IL_001d: ldc.r4 2
IL_0022: mul
IL_0023: call void [System.Console]System.Console::WriteLine(float32)
IL_0028: ret
} // end of method Program::f
This is relevant because you do sometimes see a pattern like this in the wild for dealing with SRTP:
open System.ComponentModel
open System.Diagnostics
open FSharp.Core.LanguagePrimitives
[<AbstractClass; Sealed; EditorBrowsable(EditorBrowsableState.Never)>]
type PreOps =
static member inline Double (n: float<'u>) : float<'u> = n * 2.
static member inline Double (n: float32<'u>) : float32<'u> = n * 2.f
// [FS0064] This construct causes code to be less generic than indicated by the type annotations. The type variable 'T has been constrained to be type 'OverloadedOperators'.
#nowarn "64"
module PreludeOperators =
let inline private nil<'T> = Unchecked.defaultof<'T>
let inline double (x: ^a) =
// These unused parameters turn into unused variable bindings which theoretically can be eliminated
let inline _call (_: ^M, input: ^I, _: ^R) = ((^M or ^I) : (static member Double : ^I -> ^R) input)
_call (nil<PreOps>, x, nil<^b>)
open System
open PreludeOperators
let doWork (n: float) =
Console.WriteLine n
let n' = double n
Console.WriteLine n'
See for example FSharpPlus: https://github.com/fsprojects/FSharpPlus/blob/db45914d2cacea7a749c3a271df876c56f7eeadb/src/FSharpPlus/Control/Tuple.fs#L185
The only way I can figure out to avoid unused bindings in these situations is to use explicit generic parameters on the inner function, which not only requires duplicating the generic constraints (increasing maintenance cost), but also manual lifting of the helper function to a top-level scope.
let inline private _callDouble<^M,^I,^R when (^M or ^I) : (static member Double : ^I -> ^R)> (_: ^M, input: ^I, _: ^R) =
((^M or ^I) : (static member Double : ^I -> ^R) input)
let inline double (x: ^a) = _callDouble (nil<PreOps>, x, nil<^b>)
I haven't done a benchmark yet so I'm not sure if the CLR JIT is actually able to properly take care of this kind of stuff, but I figured it would be useful to at least document anyway. Additionally, the kind of functions you'd use this pattern for are also the kind of functions that are more likely to show up all over the place in tight loops.
This affects F# 8 and 9. Haven't checked any older versions.
Metadata
Metadata
Assignees
Labels
Type
Projects
Status