3 ms·
F# generates weird, verbose, opinionated IL code that is often JIT-unfriendly and rather focused on reference types. JIT is tailored to C# patterns and JIT is r
by buybackoff 6y ago
F# generates weird, verbose, opinionated IL code that is often JIT-unfriendly and rather focused on reference types. JIT is tailored to C# patterns and JIT is rather simple in .NET (e.g. vs JVM), so it's easy to break or miss some optimizations.
Yes, it's not a fundamental limitation of the language itself. But considering the amount of hypothetical efforts required to fix all that vs investments from MSFT in F#, it is fundamental. F# lags by several years behind JIT and TPL improvements, the issues are mostly known. But it feels like it's not that important for F#. It's is not for writing fast libraries, but for Python-like use cases, explorative programming, or writing code fast when performance is not important (no hot loops in F# code) but strict typing discipline makes code less buggy and more maintainable from the first attempt. They used to say that "if F# code compiles, it's probably correct" - that is close to the truth, but do not expect top performance without fighting with the F# compiler. Even one of the best known and oldest F# library FParsec has it's high-performance parts in C# - that's telling.
- piaste 6y ago> Even one of the best known and oldest F# library FParsec has it's high-performance parts in C# - that's telling. To be more specific, it has a C# library so it can use _unsafe {}_ blocks to perform manual memory management. This is one key performance-oriented feature that F# lacks. Other than that, you _can_ write F# that's about as fast as non-unsafe C#, although it means giving up most of the nicer and safer features of F#. Use mutable structs and classes instead of immutable records, for/while loops instead of higher-order functions, magic values instead of option/result types, etc... I've done it in a couple of hot paths where it made sense, but quite reluctantly. Inline functions are still fine though for writing zero-cost helpers, and I don't think C# has them (there's an <AggressiveInlining> attribute but IIRC it's just a hint, the compiler isn't required to actually obey). Ironically, F# _could_ become the best language for high-performance .NET programming, because it supports directly inlining IL instructions intermixed with regular code (whereas C# has to go through the ILGenerator class). Unfortunately it's considered such a niche case that it's not planned to be expanded beyond the standard library, and to use it in your code you have to enable an undocumented / unsupported compiler flag.
- buybackoff 6y ago> Inline functions are still fine though for writing zero-cost helpers, and I don't think C# has them (there's an <AggressiveInlining> attribute but IIRC it's just a hint, the compiler isn't required to actually obey). In F#, you cannot disable inlining of small functions. It does IL source code inlining, like copy-paste, not machine code inlining. That increases generated dll/exe size a lot for generics. And sometimes you do not want to do that because it's worse for performance. There is an issue for that: https://github.com/fsharp/fslang-suggestions/issues/838 https://github.com/fsharp/fslang-suggestions/issues/838 In C#, the AggressiveInlining attribute works well, in predictable manner. Non-inlineable cases are well known (`throw\switch\fixed\try..catch\calling delegates` and some more https://github.com/dotnet/runtime/blob/master/src/coreclr/jit/inline.def https://github.com/dotnet/runtime/blob/master/src/coreclr/ji...). > because it supports directly inlining IL instructions intermixed with regular code This works for very simple things only. And using InlineIL.Fody, Sigil, raw IL.Emit or just raw IL code as text is not that more difficult if you already know IL. > you _can_ write F# that's about as fast as non-unsafe C# I tried this several times. It always ends with fighting the compiler more than just rewriting in C#, even without the `unsafe` keyword. In most cases due to generated IL that I cannot control from F#.
- Smaug123 6y agoF# does a good job with tail-recursion, though - you can make the for/while loop safer this way. https://www.gresearch.co.uk/article/in-favour-of-recursive-functions-not-imperative-constructs-to-make-loops/ https://www.gresearch.co.uk/article/in-favour-of-recursive-f...
- buybackoff 6y agoBut it adds .tail prefix before every callvirt instruction and this breaks some JIT optimizations. E.g. generic type specialization 'if(typeof(T) == typeof(...))' breaks, I had to dig into compiler source code to find out that it doesn't add .tail before ctors, used a struct wrapper and called a method in the struct ctor to avoid .tail prefix, but it adds its own overhead. Also, most simple tail calls are rewritten by a compiler to a loop, but if you look at the generated IL, there are multiple temp variables, locals, needless assignments. Maybe JIT is able to eliminate those, or maybe not... In C# I could get IL that reflects what I write, without surprises.