Skip to content

Make F# string functions fast and AOT/linker friendly #919

Description

@charlesroddie

The need for AOT and linker support

.Net code is deployed to devices where performance - including startup time - and download size are important.
JIT is ruled out by this and may be explicitly prohibited (UWP and iOS) or result in unacceptably slow application startup (Xamarin.Android).

Current .Net plans (.Net Form Factors) involve a supported AOT option across .Net, with greater usage of linkers:

We will introduce analyzers that report errors or warnings for code patterns that are not compatible with the specific form factors.

We will introduce a linker compatibility analyzer to detect code patterns that cannot be reliably analyzed by the linker.

Source generators are going to be the preferred [reflection/runtime code generation] mitigation for AOT compatibility, same as for linker compatibility above.

F# string problems affecting AOT/linker support

F# Support lists incompatibilities with CoreRT and .Net Native. These pick out the aspects of F# that are likely to be problematic for any performant AOT and linkers. (Note: F# is generally compatible with current mono AOT but this is not performant, and compatibility may not include linking.)

The largest problem here is F# string methods:

  • F# string methods use reflection and codegen, so are slow and AOT and linker unfriendly (i.e. are liable to crash on runtime).
  • The variety of string methods is difficult to understand: it is hard to impossible for F# users to find out what string, ToString(), and sprintf %A do.

The offending part of FSharp is lacking in type safety, with everything done via casting, uses reflection without restraint, and encapsulates poorly (e.g. .ToString() methods in FSharp record and DU types just calling sprintf "%A" on the entire record).

Solution part 1: localize by generating ToString() overrides on F# types

We have effectively, on F# record and DU types (just check SharpLab to confirm this):

override t.ToString() = sprintf "%A" t

The sprintf "function" doesn't have legitimate access to the data needed to generate the string, so has to use reflection to get the structure.

Instead this method should be compiled:

type DU = | Case0 of int | Case1 of Record | Case2 of obj

// A compiled version of the following should be generated:
override t.ToString() =
    match t with
    | Case0 i ->
        // I.e. "Case0" + i.ToString() + ")"
        // The inner string results from CompiledToString<int>(i)
        "Case0(%s{i.ToString()})" // i.e. "Case0" + i.ToString() + ")"
    | Case1 r -> "Case1(%s{r.ToSting()})"
    | Case2 o ->
        // if we absolutely need to preserve backwards compat
        "Case1(%A{o})" // i.e. "Case1(" + FSharp.FormatObj o + ")"
        // otherwise
	"Case2(%s{o.ToString()})" // i.e. "Case1(" + o.ToString() + ")"

Note that once this is done, CompiledToString does not need to know how to print records and DUs.

Solution part 2: compile

A method (represented above as pseudocode CompiledToString<'t>) should be created to generate compiled code for any concrete type 't, to be used in place of the current dynamic sprintf code where possible.

Where the method sees only obj it could preferably use .ToString(), or else use a very light form of reflection, making sure that codegen is not used.

Solution part 3: integrate

We need to decide where to use the method CompiledToString<'t>:

  • String interpolation.
    This has so much nicer ergonomics than sprintf that it is likely to replace sprintf in most F# code. If string interpolation is always compiled and ToString() methods work as above, then it will be easy for F# users to avoid AOT/linker incompatibilities.
  • sprintf: Compile sprintf where possible dotnet/fsharp#8621 . Note that we'd need to preserve some functionality for displaying records to deal with F# libraries compiled with earlier versions of F#: if they have override t.ToString() = sprintf "%A" t, then sprintf "%A" t can't use ToString().

It may be simplest to start by doing this for string interpolation as the first step, adding extra methods and preserving existing sprintf code, and afterwards migrate this work to sprintf.

TBD

Generics/inlines

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions