Skip to content

Unchecked.defaultof prevents other optimizations from working #18128

Open
@jwosty

Description

@jwosty

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

// Dead binding elimination
), so I'm not sure if what I'm seeing is a bug or if Optimizer.fs is talking about something slightly different. Either way I would think it would be sane to remove those bindings up there.

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

No one assigned

    Labels

    Area-Compiler-OptimizationThe F# optimizer, release code gen etc.BugImpact-Low(Internal MS Team use only) Describes an issue with limited impact on existing code.

    Type

    No type

    Projects

    Status

    New

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions