Shouldn't the compiler be able to devirtualize the call in the interface trampoline methods it generates for IFoo? It knows that it can only point to TFoo.Test

Shouldn't the compiler be able to devirtualize the call in the interface trampoline methods it generates for IFoo? It knows that it can only point to TFoo.Test

See this code:

type
IFoo = interface
procedure Test;
end;

TAbstractFoo = class(TInterfacedObject)
procedure Test; virtual; abstract;
end;

TFoo = class(TAbstractFoo)
procedure Test; override;
end;

TSealedFoo = class sealed(TFoo, IFoo)
end;

procedure TFoo.Test;
begin
end;

var
foo: IFoo;
fooA: TAbstractFoo;
fooB: TFoo;
fooC: TSealedFoo;
begin
foo := TSealedFoo.Create;
foo.Test;

fooC := TSealedFoo.Create;
fooB := fooC;
fooA := fooC;
fooA.Test; // virtual call
fooB.Test; // virtual call
fooC.Test; // static call to TFoo.Test

When calling the interface method and inspecting the disassembly you can see that it does the virtual call in the compiler generated trampoline method (and on win32 also suffers from a defect described here: http://andy.jgknet.de/blog/2016/05/whats-wrong-with-virtual-methods-called-through-an-interface/)

I strongly believe that the compiler should devirtualize these calls when it is able to. Before anyone asks why would you do virtual methods behind interfaces: because sometimes you want to provide a base class that already implements the base behavior and you only need to override if you want specific behavior.

Especially since we don't have interface helpers / extension methods you might have a design where the interface offers a rich API which internally just redirects to other methods where maybe only one needs to be implemented. So you can make that method virtual abstract while all the other methods ultimately call this one - all already implemented in the base class.

Comments

  1. This is an optimization issue, and I think not very important optimization issue. There are many many others; compared with GNU C++ optimizations Delphi compiler is decades behind.

    ReplyDelete
  2. It would require quite a bit more analysis though: either figure out what foo/fooA/fooB contain at this point through local analysis, or to figure out that IFoo is used only for TSealedFoo through whole program analysis in the case of foo.

    The "fooC.Test" on the other hand can be resolved as a static call with only the knowledge of the type of "fooC", no need for any analysis.

    I'm not even sure LLVM and C-compilers do the de-virtualisation outside peepholes in those cases.

    in practice, unless you do that virtual call in a tight loop and there is an inlining opportunity for the static call, I suspect you would be losing more CPU cycles to the interface reference-counting and implicit exception frames than to the virtual calls.

    ReplyDelete
  3. Eric Grange I think you missed the point. This is only about the compiler generated interface trampoline - the fooA, B, C cases are only there to show when devirtualization takes place on object method calls.

    When implementing IFoo in TSealedFoo the compiler detects the TFoo.Test method as a match for implementing IFoo.Test but since its a virtual method it generates the virtual call. But as it is implementing it in TSealedFoo it knows that this method cannot be overridden by anyone because you cannot inherit from TSealedFoo. If it was not sealed someone could override it and then IFoo.Test should call that method and not the one from TFoo.

    And yes C++ compilers do pretty good devirtualization. They even inline devirtualized methods (see https://marcofoco.com/the-power-of-devirtualization/).

    ReplyDelete
  4. Stefan Glienke oh, sorry I indeed thought you meant to de-virtualize the interface call itself, as in your snippet that's theoretically possible.

    ReplyDelete
  5. Stefan Glienke , if I remember correctly, C++ compilers only do that when WPO (whole program optimization) is enabled.

    ReplyDelete
  6. Funny, I believe that the Update2 at Andy's link is not correct. Though he did not answered then to my e-mail :-D

    ReplyDelete
  7. If they can not fix the XCHG in the trampoline (or did they finally?), there is little hope they would introduce more complex change like conditional emitting static call instead of virtual trampoline...

    ReplyDelete
  8. Sergey Kasandrov "compared with GNU C++ optimizations Delphi compiler is decades behind."

    That's a simple function of Delphi still using an archaic single-pass methodology. There are entire classes of optimizations that can't be performed in a single pass.

    I would borrow from Swift: give us an interpreter to run/test code immediately and also offer compilation for deployment. That way you drop the wait to run to zero so that it won't matter if the actual compilation wants to do 18 passes through the code. Best of both worlds.

    ReplyDelete
  9. Best of both worlds.....

    ....unless there come bugs in the compiler itself, or in the interpreter simulating compiled code less than perfectly.

    ReplyDelete
  10. Ugochukwu Mmaduekwe Do what? C++ compilers have numerous of ways to devirtualize, if they have enough information of the type to know. In the described scenarios is exactly that. You implement the interface you know if the method being chosen is virtual or not.

    Arioch The Actually no, the compiler already knows how to do static calls for trampolines as this is what it does not for non virtual methods. When he sees a virtual method he does the virtual call inside the trampoline. Here he can know that he does not have to. problem solved.

    ReplyDelete
  11. Stefan Glienke yep, that is different parts of interfaces implementation subsystem. And would there be a team enhancing amd testing that subsystem, those would had been different commits. But, there is hardly anyone to work on it. XCHG bug is much more pressing, if they did not invested into fixing such a bug, they wont into fixing a bug with much less exposure

    ReplyDelete

Post a Comment