FSC generates nop instructions after void calls because of this
https://github.com/fsharp/fsharp/blob/6819e1c769269edefcea2263c98f993e90b623e2/src/fsharp/IlxGen.fs#L2702
But he also continues to do so even in release configuration!
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="Program.fs" />
</ItemGroup>
</Project>
module Main
type Foo = struct
end
and [<AbstractClass>] Bar() =
abstract bar: foo: byref<Foo> -> unit
let bar =
{ new Bar() with
override x.bar (foo: byref<Foo>) =
x.bar(&foo) }
[<EntryPoint>]
let main _ =
let mutable a = new Foo()
bar.bar(&a)
0
dotnet run -c ReleaseAttached project
ConsoleApp18.zip
There shouldn't be nop instructions in IL (this is Release config)
And as neat side effect it should be JIT-friendly to be optimized by JIT and run forever without Stack Overflow. (callvirt just before ret)
bar.bar should be like this:

Nop instruction after every void call
JIT can't optimize this pattern:
callvirt ...
nop
ret
and everything fails with SO
bar.bar looks like this:

Adding <DebugType>ByDesign</DebugType> fixes the issue.
Why? Glad you asked.
Because SetDebugSwitch method fails horribly on this line with ByDesign switch (also with none but ByDesign is better)
https://github.com/fsharp/fsharp/blob/6819e1c769269edefcea2263c98f993e90b623e2/src/fsharp/CompileOptions.fs#L480
and doesn't execute this line
tcConfigB.debuginfo <- s = OptionSwitch.On
This fixes everything (but breaks something else). nop instructions won't be emited.
Provide any related information
Hilarious workaround 馃槀
Any chance you could send 2 PRs?
@forki I could, but I honestly not sure which way to fix it is better.
Fix 1:
Remove these 2 lines as a whole. C# seems not care too much about nop after void calls.
https://github.com/fsharp/fsharp/blob/6819e1c769269edefcea2263c98f993e90b623e2/src/fsharp/IlxGen.fs#L2704
Fix 2:
Make same 2 lines aware about <Optimize> flag and forget about debug symbols presence. Because we could have PDBs even in release configuration, but nop should be removed only (?) in optimized builds
Does C# add nop there in debug configuration? If so, then it's probably a good idea to add it, but only with optimization disabled. Looks like the nop-generating code was added without too much reason, just to mirror C# behavior.
@ForNeVeR C# indeed generates nop in this particular case for debug builds:
using System;
namespace ConsoleApp19
{
struct Foo { }
abstract class Bar
{
public abstract void bar(ref Foo foo);
}
class RealBar : Bar
{
public override void bar(ref Foo foo)
{
bar(ref foo);
}
}
class Program
{
static int Main(string[] args)
{
var a = new Foo();
var bar = new RealBar();
bar.bar(ref a);
return 0;
}
}
}
Debug:

Release:

AFAIK the reason for the nops is so that you can set a breakpoint on lines that otherwise dont map to an instruction (e.g. closing braces)
question: is there any benefit from any nop in optimized builds? Shouldn't they be eliminated everywhere?
question: is there any benefit from any nop in optimized builds?
no benefits. They are useful for placing breakpoints. And because they are wasting cycles they should be eliminated from optimized builds.
Shouldn't they be eliminated everywhere?
they should
@forki, no, not in IL, but in native code opcodes it's common, and usually used to machine-word-align instructions or improve pipelining hints to the CPU. There maybe other cases, but in IL itself I doubt it's useful, let alone that it needs instruction alignment.
Yeah, this is more of a bug that this happens. It should only be for debug.
I'm in two minds about this.
The tailcall is not guaranteed in this situation, because this is passing a byref parameter, and we never take tailcalls for calls involving byrefs as they may come from the current stack frame.
In theory the nop should be harmless for performance
Removing the nop may plausibly degrade something
However in practice there appear to be JIT optimizations which are sensitive to the presence of nop, so I guess we can remove them from release code on that basis.
Based on the earlier comment on importer it seems that nops only imported in debug code (search CEE_NOP in the code): https://github.com/dotnet/runtime/blob/857b2db411741ee57bfe24da58f77e6e124aa110/src/coreclr/src/jit/importer.cpp#L16345
Most helpful comment
question: is there any benefit from any
nopin optimized builds? Shouldn't they be eliminated everywhere?