Check if C# optimizes variables away
Closed this issue · 2 comments
Two (1, 2) of the recent PRs use variables that look like they can get optimized away by the C# when running a Release build.
The Optimize option is enabled by default for a Release build configuration. It is off by default for a Debug and any other build configuration.
It would be good to check if the deletion happens or not on the Release build.
Pinging @sfoster1 #198 and @DaleStan #213 just in case they want to check it themselves.
If we need to disable the optimization selectively, it seems that we can use [MethodImpl(MethodImplOptions.NoOptimization)]
(link).
The compiler does the right thing.
#213
I'm guessing you're looking at the using (gui.EnterRowWithTooltip(...))
blocks?
Those blocks are the same as both of these; the return value of EnterRowWithTooltip
(like the return value of EnterRow
) cannot be optimized away.
using (IDisposable usingVariable = expression) {
// block body (cannot read or write usingVariable)
}
IDisposable usingVariable = expression;
try {
// block body (cannot read or write usingVariable)
}
finally {
usingVariable?.Dispose();
}
#198
Inspecting this code is easiest with knowledge of CIL and how the compiler handles closures in lambdas and delegates.
public TextureHandle Destroy() {
if (valid) {
var capturedHandle = handle;
Ui.DispatchInMainThread(_ => SDL.SDL_DestroyTexture(capturedHandle), null);
}
return default;
}
If I decompile the generated CIL to a version of C# that doesn't support closures, we get this code:
public TextureHandle Destroy() {
if (this.valid) {
Closure closure = new Closure();
closure.capturedHandle = this.handle;
SendOrPostCallback callback = new SendOrPostCallback(closure, Closure.DestroyHelper)
Ui.DispatchInMainThread(callback, null)
}
return default;
}
private sealed class Closure {
public IntPtr capturedHandle;
public void DestroyHelper(object _) {
SDL.SDL_DestroyTexture(this.capturedHandle)
}
}
The compiler generates unspeakable names instead of Closure
and DestroyHelper
, but the essence of the code is the same: the compiler copies this.handle
into a field of a helper object, stores that object an appropriate location (probably the heap, but that's an implementation detail), and passes an instance method of that object to DispatchInMainThread
.
All captured variables work this way; they get stored in a helper class and the lambda becomes an instance method of that class.
Thank you for the the explanation!
The thing that I was curious about in #213 is this line, but I noticed that row
is a local field after re-reading again, so I have no further questions.
public RowWithHelpIcon(ImGui gui, string tooltip, bool rightJustify) {
this.gui = gui;
this.tooltip = tooltip;
>>> (the line below)
row = gui.EnterRow(); // using (gui.EnterRow()) {
if (rightJustify) {
gui.allocator = RectAllocator.RightRow;
helpCenterX = gui.AllocateRect(1, 1).Center.X;
group = gui.EnterGroup(new Padding(), RectAllocator.RemainingRow); // using (gui.EnterGroup(...)) { // Required to produce the expected spacing/padding behavior.
gui.allocator = RectAllocator.LeftRow;
}
}