fsharp / fslang-suggestions

The place to make suggestions, discuss and vote on F# language and core library features
344 stars 21 forks source link

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

Open charlesroddie opened 4 years ago

charlesroddie commented 4 years ago

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:

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>:

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

am11 commented 2 years ago

I was testing .NET 7 preview 5 SDK (daily build), which has added out of the box support for NativeAot. It seems to handle %A with a simple program (despite some errors and warnings in ILC step).

A quick How-To OS: Ubuntu 20.04 x64 Setup: ```sh mkdir ~/.dotnet7 curl -sSL https://aka.ms/dotnet/7.0.1xx/daily/dotnet-sdk-linux-x64.tar.gz | tar xzf - -C ~/.dotnet7 alias dotnet7=~/.dotnet7/dotnet cat > ~/NuGet.config < EOF ``` Create project: ```sh dotnet7 new console -n fsharpnative1 --language f# cd fsharpnative1 ``` Program.fs ```f# // snippet copy & paste from https://fsharpforfunandprofit.com/posts/printf/ open System // tuple printing let t = (1,2) Console.WriteLine("A tuple: {0}", t) printfn "A tuple: %A" t // record printing type Person = {First:string; Last:string} let johnDoe = {First="John"; Last="Doe"} Console.WriteLine("A record: {0}", johnDoe ) printfn "A record: %A" johnDoe // union types printing type Temperature = F of int | C of int let freezing = F 32 Console.WriteLine("A union: {0}", freezing ) printfn "A union: %A" freezing ``` Publish: ```sh dotnet7 publish -c Release --use-current-runtime -p:PublishAot=true ``` there are warnings/errors, but the build succeeds: ``` Microsoft (R) Build Engine version 17.3.0-preview-22263-02+78bb45f0f for .NET Copyright (C) Microsoft Corporation. All rights reserved. Determining projects to restore... Restored /home/am11/projects/fsharpnative1/fsharpnative1.fsproj (in 436 ms). /home/am11/.dotnet7/sdk/7.0.100-preview.5.22273.1/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.RuntimeIdentifierInference.targets(219,5): message NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy [/home/am11/projects/fsharpnative1/fsharpnative1.fsproj] fsharpnative1 -> /home/am11/projects/fsharpnative1/bin/Release/net7.0/linux-x64/fsharpnative1.dll Generating compatible native code. To optimize for size or speed, visit https://aka.ms/OptimizeNativeAOT /home/am11/.nuget/packages/fsharp.core/6.0.5-beta.22253.3/lib/netstandard2.1/FSharp.Core.dll : warning IL2104: Assembly 'FSharp.Core' produced trim warnings. For more information see https://aka.ms/dotnet-illink/libraries [/home/am11/projects/fsharpnative1/fsharpnative1.fsproj] /home/am11/.nuget/packages/fsharp.core/6.0.5-beta.22253.3/lib/netstandard2.1/FSharp.Core.dll : warning IL3053: Assembly 'FSharp.Core' produced AOT analysis warnings. [/home/am11/projects/fsharpnative1/fsharpnative1.fsproj] /home/am11/.nuget/packages/runtime.linux-x64.microsoft.dotnet.ilcompiler/7.0.0-preview.5.22271.4/framework/System.Linq.Expressions.dll : warning IL3053: Assembly 'System.Linq.Expressions' produced AOT analysis warnings. [/home/am11/projects/fsharpnative1/fsharpnative1.fsproj] /_/src/libraries/System.Private.CoreLib/src/System/Resources/ManifestBasedResourceGroveler.cs(239): Trim analysis warning IL2026: System.Resources.ManifestBasedResourceGroveler.CreateResourceSet(Stream,Assembly): Using member 'System.Resources.ManifestBasedResourceGroveler.InternalGetResourceSetFromSerializedData(Stream,String,String,ResourceManagerMediator)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. The CustomResourceTypesSupport feature switch has been enabled for this app which is being trimmed. Custom readers as well as custom objects on the resources file are not observable by the trimmer and so required assemblies, types and members may be removed. [/home/am11/projects/fsharpnative1/fsharpnative1.fsproj] /home/am11/.nuget/packages/runtime.linux-x64.microsoft.dotnet.ilcompiler/7.0.0-preview.5.22271.4/framework/System.Formats.Asn1.dll : warning IL3053: Assembly 'System.Formats.Asn1' produced AOT analysis warnings. [/home/am11/projects/fsharpnative1/fsharpnative1.fsproj] /home/am11/.nuget/packages/runtime.linux-x64.microsoft.dotnet.ilcompiler/7.0.0-preview.5.22271.4/framework/System.Diagnostics.DiagnosticSource.dll : warning IL3053: Assembly 'System.Diagnostics.DiagnosticSource' produced AOT analysis warnings. [/home/am11/projects/fsharpnative1/fsharpnative1.fsproj] ILC: Method '[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+FSharpFunc`6..ctor()' will always throw because: Failed to load type 'Microsoft.FSharp.Core.FSharpFunc`2>>>>' from assembly 'FSharp.Core, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' fsharpnative1 -> /home/am11/projects/fsharpnative1/bin/Release/net7.0/linux-x64/publish/ ``` Run it: ``` bin/Release/net7.0/linux-x64/publish/fsharpnative1 ``` Output: ``` A tuple: (1, 2) A tuple: (1, 2) A record: { First = "John" Last = "Doe" } A record: { First = "John" Last = "Doe" } A union: F A union: F ```

Despite the error:

Failed to load type 'Microsoft.FSharp.Core.FSharpFunc2<T1_System.__Canon, Microsoft.FSharp.Core.FSharpFunc2<T2_System.Canon, Microsoft.FSharp.Core.FSharpFunc2<T3_System.__Canon, Microsoft.FSharp.Core.FSharpFunc2<T4_System.Canon, Microsoft.FSharp.Core.FSharpFunc`2<T5_System.__Canon, TResult_System.__Canon>>>>>' from assembly 'FSharp.Core, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'

ILC succeeds. If we can work this error out first in .NET 7 timeframe, that would be cool (since it seems benign, and showing up for a simple "Hello from F#" app as well).

cc @MichalStrehovsky

kant2002 commented 2 years ago

Regular NativeAOT works just fine with %A but there extra goal to make it work in reflection-free mode which is not supported (but working).

Having reflection obviously fine, but being not rely on it is also goal which a lot of people want. This issue is to improve reflectionfree mode too.

https://github.com/kant2002/RdXmlLibrary/blob/main/FSharp.Core.xml

You can plug this file with

<RdXmlFile Include="FSharp.Core.rd.xml" />
MichalStrehovsky commented 2 years ago

Failed to load type 'Microsoft.FSharp.Core.FSharpFunc2<T1_System.Canon, Microsoft.FSharp.Core.FSharpFunc2<T2_System.__Canon, Microsoft.FSharp.Core.FSharpFunc2<T3_System.Canon, Microsoft.FSharp.Core.FSharpFunc2<T4_System.Canon, Microsoft.FSharp.Core.FSharpFunc`2<T5_System.__Canon, TResult_System.Canon>>>>>' from assembly 'FSharp.Core, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'

The compiler detected there's a generic cycle in the assembly and instead of compiling until it runs out of memory (or the heath death of the universe, whichever comes first) cut off the generic expansion at the point when it ran over the cutoff. If the reported method is reached at runtime, it will throw.

FWIW, the cycle(s) involve following entities. The compiler should print them, not sure why it's not kicking in here:

    [0]: {[FSharp.Core]Microsoft.FSharp.Core.FSharpFunc`2}
    [1]: {[FSharp.Core]<StartupCode$FSharp-Core>.$Prim-types+op_Implicit@3421}
    [2]: {[FSharp.Core]<StartupCode$FSharp-Core>.$Prim-types+op_Implicit@3426-1}
    [3]: {[FSharp.Core]<StartupCode$FSharp-Core>.$Prim-types+op_Implicit@3429-2}
    [4]: {[FSharp.Core]<StartupCode$FSharp-Core>.$Prim-types+op_Implicit@3432-3}
    [5]: {[FSharp.Core]<StartupCode$FSharp-Core>.$Prim-types+FromConverter@3434}
    [6]: {[FSharp.Core]<StartupCode$FSharp-Core>.$Prim-types+ToConverter@3436}
    [7]: {[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+FSharpFunc`3}
    [8]: {[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+FSharpFunc`4}
    [9]: {[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+FSharpFunc`5}
    [10]: {[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+FSharpFunc`6}
    [11]: {[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+Invoke@3302}
    [12]: {[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+Adapt@3309}
    [13]: {[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+Invoke@3316-1}
    [14]: {[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+Adapt@3325-1}
    [15]: {[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+Adapt@3329-2}
    [16]: {[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+Adapt@3343-3}
    [17]: {[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+Adapt@3348-4}
    [18]: {[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+Adapt@3352-5}
    [19]: {[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+Invoke@3355-2}
    [20]: {[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+Invoke@3363-3}
    [21]: {[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+Adapt@3373-6}
    [22]: {[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+Adapt@3378-7}
    [23]: {[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+Adapt@3383-8}
    [24]: {[FSharp.Core]Microsoft.FSharp.Core.OptimizedClosures+Adapt@3387-9}
kant2002 commented 2 years ago

Just for information about other reflection-free mode limitations.

Construct $"test {name}" produce exceptions like this.

Unhandled Exception: EETypeRva:0x004CDE08: TypeInitialization_Type_NoTypeAvailable
 ---> EETypeRva:0x004CDE08: TypeInitialization_Type_NoTypeAvailable
 ---> EETypeRva:0x004CD0E8: Reflection_Disabled
   at Internal.Reflection.RuntimeTypeInfo.GetMethodImpl(String, BindingFlags, Binder, CallingConventions, Type[], ParameterModifier[]) + 0x33
   at System.Type.GetMethod(String, BindingFlags) + 0x27
   at <StartupCode$FSharp-Core>.$Printf..cctor() + 0x3c
   at System.Runtime.CompilerServices.ClassConstructorRunner.EnsureClassConstructorRun(StaticClassConstructionContext*) + 0xc6
   Exception_EndOfInnerExceptionStack
   at System.Runtime.CompilerServices.ClassConstructorRunner.EnsureClassConstructorRun(StaticClassConstructionContext*) + 0x167
   at System.Runtime.CompilerServices.ClassConstructorRunner.CheckStaticClassConstructionReturnNonGCStaticBase(StaticClassConstructionContext*, IntPtr) + 0xd
   at Microsoft.FSharp.Core.PrintfImpl..cctor() + 0x9
   at System.Runtime.CompilerServices.ClassConstructorRunner.EnsureClassConstructorRun(StaticClassConstructionContext*) + 0xc6
   Exception_EndOfInnerExceptionStack
   at System.Runtime.CompilerServices.ClassConstructorRunner.EnsureClassConstructorRun(StaticClassConstructionContext*) + 0x167
   at System.Runtime.CompilerServices.ClassConstructorRunner.CheckStaticClassConstructionReturnNonGCStaticBase(StaticClassConstructionContext*, IntPtr) + 0xd
   at Microsoft.FSharp.Core.PrintfImpl.buildStep$cont@1141(PrintfImpl.FormatSpecifier, Type[], String, Unit) + 0x16
   at Microsoft.FSharp.Core.PrintfImpl.FormatParser`4.parseAndCreateStepsForCapturedFormatAux(FSharpList`1, String, Int32&) + 0xb3
   at Microsoft.FSharp.Core.PrintfImpl.FormatParser`4.parseAndCreateStepsForCapturedFormat() + 0x3a
   at Microsoft.FSharp.Core.PrintfImpl.FormatParser`4.GetStepsForCapturedFormat() + 0x17
   at Microsoft.FSharp.Core.PrintfModule.PrintFormatToStringThen[T](PrintfFormat`4) + 0x4b
   at Program.drawScene@108-1.Invoke(IImageProcessingContext) + 0xcf
   at Program.drawScene(Image`1, Font, JS.DataView, Int32, PlatformModel.Texture[], Model.Game) + 0x26b
   at Program.initScene@117-1.Invoke(PlatformModel.Texture[], Model.Game) + 0x2d
   at App.Game.gameLoop@38.Invoke(Model.Game, Double) + 0x2b
   at Program.render(Double) + 0x4a
   at Program.main@211-1.Invoke(Double) + 0x9
   at Silk.NET.Windowing.Internals.ViewImplementationBase.DoRender() + 0x16e
   at Silk.NET.Windowing.Internals.ViewImplementationBase.Run(Action) + 0x15
   at Silk.NET.Windowing.WindowExtensions.Run(IView) + 0x61
   at Program.main(String[]) + 0x227
   at FSharpWolfenstein.Desktop!<BaseAddress>+0x381723
kant2002 commented 2 years ago

I also receive this error with string interpolation in regular NativeAOT

Unhandled Exception: System.Collections.Generic.KeyNotFoundException: An index satisfying the predicate was not found in the collection.
   at Microsoft.FSharp.Collections.ArrayModule.loop@596-36[T, TResult](FSharpFunc`2, T[], Int32) + 0x9e
   at Microsoft.FSharp.Reflection.Impl.getUnionCaseTyp(Type, Int32, BindingFlags) + 0x3e
   at Microsoft.FSharp.Reflection.Impl.fieldsPropsOfUnionCase(Type, Int32, BindingFlags) + 0x143
   at Microsoft.FSharp.Reflection.FSharpValue.GetUnionFields(Object, Type, FSharpOption`1) + 0x6f
   at Microsoft.FSharp.Text.StructuredPrintfImpl.ReflectUtils.Value.GetValueInfoOfObject$cont@525(BindingFlags, Object, Type, Unit) + 0x57
   at Microsoft.FSharp.Text.StructuredPrintfImpl.Display.ObjectGraphFormatter.objL(Display.ShowMode, Int32, Display.Precedence, Object, Type) + 0x3f
   at Microsoft.FSharp.Text.StructuredPrintfImpl.Display.Format@1515.Invoke(Int32, Display.Precedence, Tuple`2) + 0x3a
   at Microsoft.FSharp.Text.StructuredPrintfImpl.Display.ObjectGraphFormatter.Format[a](Display.ShowMode, a, Type) + 0x83
   at Microsoft.FSharp.Text.StructuredPrintfImpl.Display.anyToStringForPrintf[T](FormatOptions, BindingFlags, T, Type) + 0x5c
   at Microsoft.FSharp.Core.PrintfImpl.ObjectPrinter.GenericToStringCore[T](T, FormatOptions, BindingFlags) + 0x47
   at Microsoft.FSharp.Core.PrintfImpl.OneStepWithArg@508-1.Invoke(A) + 0x37
   at System.Text.ValueStringBuilder.AppendFormatHelper(IFormatProvider, String, ParamsArray) + 0x644
   at System.String.FormatHelper(IFormatProvider, String, ParamsArray) + 0xae
   at Microsoft.FSharp.Core.PrintfImpl.InterpolandToString@924.Invoke(Object) + 0x75
   at Microsoft.FSharp.Core.PrintfImpl.PrintfEnv`3.RunSteps(Object[], Type[], PrintfImpl.Step[]) + 0xb4
   at Microsoft.FSharp.Core.PrintfModule.PrintFormatToStringThen[T](PrintfFormat`4) + 0x6a
   at App.AI.preProcess(Model.Game, Model.Enemy) + 0x110
   at App.AI.applyAi(Double, Model.Game, Model.GameObject) + 0x5b
   at App.Update.updatedGameObjects@254.Invoke(Int32, Model.GameObject) + 0x95
   at Microsoft.FSharp.Primitives.Basics.List.mapiToFreshConsTail[a, b](FSharpList`1, OptimizedClosures.FSharpFunc`3, FSharpList`1, Int32) + 0x5c
   at Microsoft.FSharp.Primitives.Basics.List.mapi[T, TResult](FSharpFunc`2, FSharpList`1) + 0xb1
   at App.Update.updateEnemies@251(Double, Model.WallRenderingResult, Model.Game, Boolean) + 0x67
   at App.Update.updateFrame(Model.Game, Double, Model.WallRenderingResult) + 0x295
   at Program.render(Double) + 0x4a
   at Program.main@198-1.Invoke(Double) + 0x9
   at Silk.NET.Windowing.Internals.ViewImplementationBase.DoRender() + 0x16e
   at Silk.NET.Windowing.Internals.ViewImplementationBase.Run(Action) + 0x15
   at Silk.NET.Windowing.WindowExtensions.Run(IView) + 0x61
   at Program.main(String[]) + 0x149
   at FSharpWolfenstein.Desktop!<BaseAddress>+0x462583
abelbraaksma commented 2 years ago

Related, adding for prosperity: https://github.com/fsharp/fslang-suggestions/issues/429