21
votes

Yesterday I found this strange behavior in my C# code:

Stack<long> s = new Stack<long>();

s.Push(1);           // stack contains [1]
s.Push(2);           // stack contains [1|2]
s.Push(3);           // stack contains [1|2|3]

s.Push(s.Pop() * 0); // stack should contain [1|2|0]

Console.WriteLine(string.Join("|", s.Reverse()));

I assumed the program would print 1|2|0 but in fact it printed 1|2|3|0.

Looking at the generated IL code (via ILSpy) you can see that s.Pop() * 0 is optimized to simply 0:

// ...
IL_0022: ldloc.0
IL_0023: ldc.i4.0
IL_0024: conv.i8
IL_0025: callvirt instance void class   [System]System.Collections.Generic.Stack`1<int64>::Push(!0)
// ...

ILSpy decompilation:

Stack<long> s = new Stack<long>();
s.Push(1L);
s.Push(2L);
s.Push(3L);
s.Push(0L); // <- the offending line
Console.WriteLine(string.Join<long>("|", s.Reverse<long>()));

First I tested this initially under Windows 7 with Visual Studio 2015 Update 3 with both Release mode (/optimize) and Debug mode and with various target frameworks (4.0, 4.5, 4.6 and 4.6.1). In all 8 cases the result was the same (1|2|3|0).

Then I tested it under Windows 7 with Visual Studio 2013 Update 5 (again with all the combinations of Release/Debug mode and target framework). To my surprise the statement is here not optimized away and yields the expected result 1|2|0.

So I can conclude that this behavior is neither dependent on /optimize nor the target framework flag but rather on the used compiler version.

Out of interest I wrote a similar code in C++ and compiled it with the current gcc version. Here a function call multiplied with zero is not optimized away and the function is properly executed.

I think such an optimization would only be valid if stack.Pop() were a pure function (which it definitely isn't). But I'm hesitant to call this a bug, I assume it's just a feature unknown to me?

Is this "feature" anywhere documented and is there an (easy) way to disable this optimization?

1
Yuck, yes, I can repro with VS2015 Update 2. Roslyn has been a major bug generator. Click the New Issue button to report it. Limp along by using a variable to store the Pop return value, it will be eliminated again by the jitter optimizer. - Hans Passant
This most likely it can't be answered without a link to an off site resource (which makes this off topic). This is an awesome bug report which should be submitted here github.com/dotnet/roslyn/issues Of course, someone from the team may be able to answer... maybe Jared Parsons? - user1228
@HansPassant: I kinda assumed that this isn't a bug but a feature :). But if it is I will open an issue and change my code-generator for now to explicitly call Stack.Pop() in it's own line. - Mikescher
@Mikescher: Certainly a bug, the optimizer is trying to eliminate the redundant multiplication but should be running all side effects so you cannot tell. - Guvante
@SledgeHammer: I agree its stupid. But in my defence it's not really code I have written. It's programmatically generated code from my befunge-93 transpiler. - Mikescher

1 Answers

12
votes

Yes, it is definitely a bug. < expr > * 0, should not be optimized into 0 if < expr > has sideeffects.

Thanks for reporting the issue!!

You can track the progress of the bug/fix at https://github.com/dotnet/roslyn/issues/13486