Can I stop .NET 4 performing tail-call elimination?
We are in the process of migrating an app to .NET 4.0 (from 3.5). One of the problems we are running into is only reproducible under very specific conditions:
- Only in a Release build
- Only with optimization enabled and/or debug info set to pdb-only.
By this I mean, if I disable optimization and set debug info to full, the problem goes away.
The code in question works fine on .NET 3.5, in Release mode with optimization etc enabled, and has done for a long time.
I really don't want to suggest that there's a bug in the C# compiler, so really my question is whether there are any techniques I can use for tracking down what we might be doing wrong to cause an incorrect optimization?
I'm in the process of trying to narrow this problem down to a small test case so I can post some code here.
Edit:
I've tracked down the problem to the following:
We have this code in the constructor of a Form:
public ConnectionForm()
{
LocalControlUtil.Configure("ConnectionForm", "Username", usernameLabel);
LocalControlUtil.Configure("ConnectionForm", "Password", passwordLabel);
LocalControlUtil.Configure("ConnectionForm", "Domain", domainLabel);
LocalControlUtil.Configure("ConnectionForm", "Cancel", cancelButton);
LocalControlUtil.Configure("ConnectionForm", "OK", okButton);
}
These calls are to some custom localisation code. The constructor for th开发者_如何学Cis form is called from another assembly. The LocalControlUtil.Configure
method calls Assembly.GetCallingAssembly()
, which returns the correct value for all of the above calls, except the last one.
I can reorder the lines above, add new ones or remove current ones, and every time it is the last line which doesn't work.
I assume that this is JIT inlining the last method call to the place where the constructor was called (in another assembly). Adding [MethodImpl(MethodImplOptions.NoInlining)]
to the constructor above fixes the problem.
Does anybody know why this happens? It seems strange to me that the last line only can be inlined. Is this new behaviour in .NET 4.0?
Edit 2:
I've narrowed this down now to a tail-call elimination, I assume caused by the new tail-call stuff in .NET 4.
In the code above, the last call to LocalControlUtil.Configure
in the constructor is eliminated and put in the calling method, which is in another assembly. As the method calls Assembly.GetCallingAssembly
, we don't get the correct assembly back.
Is there any way to stop the compiler (or the JIT or whatever does this) from eliminating the tail call?
I'd just put this in a comment were it not too long, but have you tried:
public ConnectionForm()
{
try
{
LocalControlUtil.Configure("ConnectionForm", "Username", usernameLabel);
LocalControlUtil.Configure("ConnectionForm", "Password", passwordLabel);
LocalControlUtil.Configure("ConnectionForm", "Domain", domainLabel);
LocalControlUtil.Configure("ConnectionForm", "Cancel", cancelButton);
LocalControlUtil.Configure("ConnectionForm", "OK", okButton);
}
catch
{
throw;
}
}
The fact that any exception raised is thrown makes this pretty much a null change in terms of the written code, but try-catch boundaries often prevent optimisations on the part of the compiler and the jitter.
I think you're already on the right track for finding the issue... Narrowing it down to the smallest test case is almost always a great first step.
The next will be to compare the IL that is generated for each version in something like Reflector. Reading IL is non-trivial (unless you have a good amount of experience) so minimizing the amount of code you have to look through is critical.
UPDATE: I thought about this some more and if the problem isn't discernible at the IL level, it might be at the JIT (Just In Time) level, which is significantly more difficult to debug. I've never had to work that level, so I don't have any insight into that part of the problem (if it comes to that).
No, you can't.
.NET 4.0 optimises more tail-calls than 3.5, which is a good thing. Our code was crazy-stupid.
精彩评论