You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
A function calling Assembly.GetExecutingAssembly() gets inlined across assembly boundaries #9283
changed the title [-]Assembly.GetExecutingAssembly() gets inlined across assembly boundaries[/-][+]A function calling Assembly.GetExecutingAssembly() gets inlined across assembly boundaries[/+]on May 24, 2020
I think this is a feature and not a bug. Assembly.GetExecutingAssembly() is defined to walk the stack to find out where it is. If functions get inlined, either by JIT (which I think happens here) or F# (when using inline implicitly or explicitly), the stack changes, and so does the result of this call. This is also suggested in this SO answer from 2013: https://stackoverflow.com/a/19845144/111575.
In my local logging framework I exploit this behavior to get the executing assembly from the call to the log, not the log assembly itself. Changing this behavior would break that code and possibly others that use similar techniques.
You probably want to use Type.GetExecutingAssembly instead. It doesn't have the same issue, I believe.
If functions get inlined, either by JIT (which I think happens here)
No, in this case, the function is inlined by the F# compiler; I was able to confirm this hypothesis by reading the decompiled code of my assembly. The entry point looks like this:
publicstaticintmain(string[]argv){PrintfFormat<Unit,TextWriter,Unit,Unit>printfFormat=newPrintfFormat<Unit,TextWriter,Unit,Unit,Unit>("Hello World from F#!");PrintfModule.PrintFormatLineToTextWriter<Unit>(Console.Out,printfFormat);PrintfFormat<FSharpFunc<AssemblyName,Unit>,TextWriter,Unit,Unit>printfFormat2=newPrintfFormat<FSharpFunc<AssemblyName,Unit>,TextWriter,Unit,Unit,AssemblyName>("ProjectA: %A");PrintfModule.PrintFormatLineToTextWriter<FSharpFunc<AssemblyName,Unit>>(Console.Out,printfFormat2).Invoke(Assembly.GetExecutingAssembly().GetName());printfFormat2=newPrintfFormat<FSharpFunc<AssemblyName,Unit>,TextWriter,Unit,Unit,AssemblyName>("ProjectB: %A");PrintfModule.PrintFormatLineToTextWriter<FSharpFunc<AssemblyName,Unit>>(Console.Out,printfFormat2).Invoke(Assembly.GetExecutingAssembly().GetName());return0;}
Here, the first call to Assembly.GetExecutingAssembly().GetName() is, in fact, inlined code from another F# assembly.
Also, I believe that Assembly.GetExecutingAssembly is marked by a special attribute (System.Security.DynamicSecurityMethod) to avoid this JIT behavior:
All methods that use StackCrawlMark should be marked with this attribute. This attribute disables inlining of the calling method to allow stackwalking to find the exact caller.
It may or may not be a problem that I wasn't able to find DynamicSecurityMethod attribute on the Assembly.GetExecutingAssembly in the ref assemblies I (seemingly) compile my .NET Core code against.
I mean, if F# compiler starts respecting this attribute at some point (currently it seems it doesn't), its absence from the ref assemblies may impose a problem.
In my local logging framework I exploit this behavior to get the executing assembly from the call to the log, not the log assembly itself.
How do you work around the fact that this approach doesn't work in Debug mode?
You probably want to use Type.GetExecutingAssembly instead. It doesn't have the same issue, I believe.
You probably meant Type.GetAssembly(), and, yes, this is a viable work around this particular problem, too. But sometimes this itself may be a problem, since it's not that easy to get any meaningful type from an F# assembly, e.g. typeof<MyModule> doesn't work.
To conclude, currently I believe this is, in fact, a bug and not a feature. F# compiler inlines the code that shouldn't be inlined, as described by DynamicSecurityMethod documentation, which leads to unreliable runtime behavior (different between debug and release builds). Any method calling a method marked with DynamicSecurityMethod shouldn't be inlined — either by JIT, or by the F# compiler (since it moslty respects attributes used to control JIT).
Not a big deal? Probably. A feature? Nope.
Reacted by Ayrat Hudaygulov, vshapenko and Ingerdev
@ForNeVeR
To disable compiler inlining use: [<MethodImpl(MethodImplOptions.NoInlining)>]
Try this:
// projecta/Program.fs
module ProjectA
open System.Reflection
open System.Runtime.CompilerServices
[<MethodImpl(MethodImplOptions.NoInlining)>]
let getName() =
Assembly.GetExecutingAssembly().GetName()
Produces this output on retail build:
Hello World from F#!
ProjectA: ClassLibrary1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
ProjectB: ConsoleApp5, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
Reacted by Friedrich von Never and Ayrat Hudaygulov
How do you work around the fact that this approach doesn't work in Debug mode?
@ForNeVeR, I was wrong, the code uses Assembly.GetCallingAssembly instead, and to find the line number it injects a dummy function (and extracts the line number from that), and further some magic to distinguish between lambdas and local functions, to get the relevant function name.
All these methods are marked debug-only, so I'm doing the inverse: when in debug mode, I know certain functions are not erased or inlined (unless inline is present) and leverage that info. The release-mode log functions don't do this, as the same trick wouldn't work.
Sorry for the clutter, as I deliberately wrote in my code not to use Assembly.GetExecutingAssembly. I searched for the places where I do use it, and these are all module-level let-bindings. Since these are never inlined anyway, and will only execute once, this gives me the right assembly (which is, btw, another way to work around the inlining issue).
Not a big deal? Probably. A feature? Nope.
Totally in agreement there now, thanks for the insights!
@KevinRansom, thanks, I confirm that this workaround works (and already mentioned it in my opening post). I do still believe the current behavior is an issue, but I don't think it's a critical one.
// DynamicSecurityMethodAttribute:// All methods that use StackCrawlMark should be marked with this attribute. This attribute// disables inlining of the calling method to allow stackwalking to find the exact caller.//// This attribute used to indicate that the target method requires space for a security object// to be allocated on the callers stack. It is not used for this purpose anymore because of security// stackwalks are not ever done in CoreCLR.
And I can confirm, by looking at the generated IL from the FSC compiler that @ForNeVeR is correct that the method that uses GetExecutingAssembly gets inlined indeed, and the NoInlining from the BCL library seems to have not been taken into account.
Yes, the workaround is to add NoInlining to your own function, but surely, that oughtn't be needed? Or I'm missing something in how NoInlining is supposed to work.
Reacted by Friedrich von Never and Ayrat Hudaygulov
I agree with the previous comment. It looks like this is a compiler bug (or, let's say, an open issue in compiler), even if a minor one. I think it should be reopened, if there's no policy to close minor/archived/postponed bugs.
Reacted by Eugene Auduchinok, Abel Braaksma and Ayrat Hudaygulov
If we optimize and inline small methods not marked inline, we should use a deterministic other way to find out whether it's eligible or not.
And I was ready to create an issue about that...
My opinion is that inlining functions at the compiler-level without the inline keyword is a bad idea. It affects generated code size and such automatic inlining for the purposes of optimization is the job of the JIT, not the compiler. It also widens the gap between the IL code the developer expects to be generated, with the one that gets actually generated.
However, such automatic inlining looks OK for limited cases like small nested functions or active patterns.
Automatically inlining functions across assembly boundaries, like what this issue is concerned with, is a terrible idea because it breaches the barrier of abstraction between assemblies and should never be performed without the inline keyword.
@teo-tsirpanis -- I don't know that I agree that it is a terrible idea, however, it is often quite inconvenient, and makes the source code a lot harder to reason about. I think that having fslint tell developers this is an iffy construct and they should use the noinlining attribute is the best result we can do right now. For sure turning off automagic inlining would ensure that the performance characteristics of most code would change, and probably not for the better.
I agree with Teo that automatic cross-assembly inlining is bad idea. Code tends to change and once correct code can cause MissingMethodException/TypeLoadException or old/incorrect behavior. Example: application with plugins - you can't control all code and recompile every library. As developer you don't change public api, but F# compiler goes through public methods and can inline internal/private code.
Maybe compiler should have flag to control implicit inlining, e.g. none, projectReferences, all: <ImplicitInlines>none</ImplicitInlines>. By default set it to projectReferences as safe version, but people could extend it to all references to improve performance
The dotnet bug report led to a new API proposal that would allow to implicitly pass caller identity, which would ultimately be an alternative for the flawed behavior explained in this issue, see: dotnet/csharplang#4984.
I am going to close this, there is nothing really for us to do. To address this a developer can add [<MethodImpl(MethodImplOptions.NoInlining)>], there's no reliable attribute for us to detect, and lists of well-known APIs are always difficult to reason about and maintain, and the developer can also suppress cross-assembly inlining when needed.
To address this a developer can add [<MethodImpl(MethodImplOptions.NoInlining)>],
@KevinRansom, the point of this whole discussion and issue was, that NoInliningdoes not work, except only partially. Unless that has been fixed meanwhile. I.e.:
As you said, NoInlining is not transitive, but security-criticality is
I'm not saying it is wrong to close this, I consider it "not worth the effort" (however much this makes GetExecutingAssembly unreliable), but C# solved it, so why can't F#? But IIRC, there's an effort underway in .NET to use caller identity: dotnet/csharplang#4984, which would allow for a reliable workaround. It may not happen before .NET 8 though.
Even if we wanted to fix this behavior, not having reqsecobj in reference assemblies makes this next to impossible or really awkward (#9283 (comment)).
It is sad, but looks like you are right in that there's nothing we can do here...
Activity
[-]Assembly.GetExecutingAssembly() gets inlined across assembly boundaries[/-][+]A function calling Assembly.GetExecutingAssembly() gets inlined across assembly boundaries[/+]abelbraaksma commentedon May 24, 2020
I think this is a feature and not a bug.
Assembly.GetExecutingAssembly()is defined to walk the stack to find out where it is. If functions get inlined, either by JIT (which I think happens here) or F# (when usinginlineimplicitly or explicitly), the stack changes, and so does the result of this call. This is also suggested in this SO answer from 2013: https://stackoverflow.com/a/19845144/111575.In my local logging framework I exploit this behavior to get the executing assembly from the call to the log, not the log assembly itself. Changing this behavior would break that code and possibly others that use similar techniques.
You probably want to use
Type.GetExecutingAssemblyinstead. It doesn't have the same issue, I believe.ForNeVeR commentedon May 25, 2020
@abelbraaksma
No, in this case, the function is inlined by the F# compiler; I was able to confirm this hypothesis by reading the decompiled code of my assembly. The entry point looks like this:
Here, the first call to
Assembly.GetExecutingAssembly().GetName()is, in fact, inlined code from another F# assembly.Also, I believe that
Assembly.GetExecutingAssemblyis marked by a special attribute (System.Security.DynamicSecurityMethod) to avoid this JIT behavior:It may or may not be a problem that I wasn't able to find
DynamicSecurityMethodattribute on theAssembly.GetExecutingAssemblyin the ref assemblies I (seemingly) compile my .NET Core code against.I mean, if F# compiler starts respecting this attribute at some point (currently it seems it doesn't), its absence from the ref assemblies may impose a problem.
How do you work around the fact that this approach doesn't work in Debug mode?
You probably meant
Type.GetAssembly(), and, yes, this is a viable work around this particular problem, too. But sometimes this itself may be a problem, since it's not that easy to get any meaningful type from an F# assembly, e.g.typeof<MyModule>doesn't work.To conclude, currently I believe this is, in fact, a bug and not a feature. F# compiler inlines the code that shouldn't be inlined, as described by
DynamicSecurityMethoddocumentation, which leads to unreliable runtime behavior (different between debug and release builds). Any method calling a method marked withDynamicSecurityMethodshouldn't be inlined — either by JIT, or by the F# compiler (since it moslty respects attributes used to control JIT).Not a big deal? Probably. A feature? Nope.
KevinRansom commentedon May 26, 2020
@ForNeVeR
To disable compiler inlining use: [<MethodImpl(MethodImplOptions.NoInlining)>]
Try this:
Produces this output on retail build:
abelbraaksma commentedon May 27, 2020
@ForNeVeR, I was wrong, the code uses
Assembly.GetCallingAssemblyinstead, and to find the line number it injects a dummy function (and extracts the line number from that), and further some magic to distinguish between lambdas and local functions, to get the relevant function name.All these methods are marked debug-only, so I'm doing the inverse: when in debug mode, I know certain functions are not erased or inlined (unless
inlineis present) and leverage that info. The release-mode log functions don't do this, as the same trick wouldn't work.Sorry for the clutter, as I deliberately wrote in my code not to use
Assembly.GetExecutingAssembly. I searched for the places where I do use it, and these are all module-level let-bindings. Since these are never inlined anyway, and will only execute once, this gives me the right assembly (which is, btw, another way to work around the inlining issue).Totally in agreement there now, thanks for the insights!
ForNeVeR commentedon May 27, 2020
@KevinRansom, thanks, I confirm that this workaround works (and already mentioned it in my opening post). I do still believe the current behavior is an issue, but I don't think it's a critical one.
KevinRansom commentedon May 27, 2020
If you don't mind, I'm going to close this, everything is working as it is intended.
Thanks for the report.
abelbraaksma commentedon May 27, 2020
@KevinRansom, I don't think it is working as expected.
You wrote:
But in this case, the method
Assembly.GetExecutingAssemblyis actually marked with precisely that, but this isn't honored:And:
And I can confirm, by looking at the generated IL from the FSC compiler that @ForNeVeR is correct that the method that uses
GetExecutingAssemblygets inlined indeed, and theNoInliningfrom the BCL library seems to have not been taken into account.Yes, the workaround is to add
NoInliningto your own function, but surely, that oughtn't be needed? Or I'm missing something in howNoInliningis supposed to work.ForNeVeR commentedon May 30, 2020
I agree with the previous comment. It looks like this is a compiler bug (or, let's say, an open issue in compiler), even if a minor one. I think it should be reopened, if there's no policy to close minor/archived/postponed bugs.
cartermp commentedon May 30, 2020
Yeah, this looks like a bug.
22 remaining items
teo-tsirpanis commentedon Sep 3, 2020
And I was ready to create an issue about that...
My opinion is that inlining functions at the compiler-level without the
inlinekeyword is a bad idea. It affects generated code size and such automatic inlining for the purposes of optimization is the job of the JIT, not the compiler. It also widens the gap between the IL code the developer expects to be generated, with the one that gets actually generated.However, such automatic inlining looks OK for limited cases like small nested functions or active patterns.
Automatically inlining functions across assembly boundaries, like what this issue is concerned with, is a terrible idea because it breaches the barrier of abstraction between assemblies and should never be performed without the
inlinekeyword.KevinRansom commentedon Sep 3, 2020
@teo-tsirpanis -- I don't know that I agree that it is a terrible idea, however, it is often quite inconvenient, and makes the source code a lot harder to reason about. I think that having fslint tell developers this is an iffy construct and they should use the noinlining attribute is the best result we can do right now. For sure turning off automagic inlining would ensure that the performance characteristics of most code would change, and probably not for the better.
jl0pd commentedon Oct 29, 2021
I agree with Teo that automatic cross-assembly inlining is bad idea. Code tends to change and once correct code can cause
MissingMethodException/TypeLoadExceptionor old/incorrect behavior. Example: application with plugins - you can't control all code and recompile every library. As developer you don't change public api, but F# compiler goes through public methods and can inline internal/private code.Maybe compiler should have flag to control implicit inlining, e.g.
none,projectReferences,all:<ImplicitInlines>none</ImplicitInlines>. By default set it toprojectReferencesas safe version, but people could extend it toallreferences to improve performancedsyme commentedon Oct 29, 2021
You can disable cross-assembly inlining using
--crossoptimize-Libraries can suppress cross-assembly inline information using
--nooptimizationdataabelbraaksma commentedon Feb 23, 2022
The dotnet bug report led to a new API proposal that would allow to implicitly pass caller identity, which would ultimately be an alternative for the flawed behavior explained in this issue, see: dotnet/csharplang#4984.
KevinRansom commentedon Mar 14, 2023
I am going to close this, there is nothing really for us to do. To address this a developer can add [<MethodImpl(MethodImplOptions.NoInlining)>], there's no reliable attribute for us to detect, and lists of well-known APIs are always difficult to reason about and maintain, and the developer can also suppress cross-assembly inlining when needed.
Thanks for the discussion
Kevin
abelbraaksma commentedon Mar 21, 2023
@KevinRansom, the point of this whole discussion and issue was, that
NoInliningdoes not work, except only partially. Unless that has been fixed meanwhile. I.e.:See this comment for a summary: #9283 (comment) and this:
I'm not saying it is wrong to close this, I consider it "not worth the effort" (however much this makes
GetExecutingAssemblyunreliable), but C# solved it, so why can't F#? But IIRC, there's an effort underway in .NET to usecaller identity: dotnet/csharplang#4984, which would allow for a reliable workaround. It may not happen before .NET 8 though.abelbraaksma commentedon Mar 21, 2023
Even if we wanted to fix this behavior, not having
reqsecobjin reference assemblies makes this next to impossible or really awkward (#9283 (comment)).It is sad, but looks like you are right in that there's nothing we can do here...
ForNeVeR commentedon Mar 23, 2023
C#, notably, "solved" it by not having inlining at all.