shpaass/yafc-ce

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;
                }
            }