Page MenuHomePhabricator

Extensible LLVM RTTI
AcceptedPublic

Authored by lhames on Oct 19 2017, 10:28 PM.

Details

Summary

This patch extracts the RTTI part of llvm::ErrorInfo into its own class (RTTIExtends) so that it can be used in other non-error hierarchies, and makes it compatible with the existing LLVM RTTI function templates (isa, cast, dyn_cast, dyn_cast_or_null) by adding the classof method.

This system makes different trade-offs to the existing LLVM RTTI approach (described in https://llvm.org/docs/HowToSetUpLLVMStyleRTTI.html) and would be complementary to the existing system rather than replacing it. Specifically: the extensible scheme is slower than the existing scheme (requiring a virtual call per isa check), but it leaves hierarchies open for extension and is slightly easier to use (since it eliminates the enumeration and switch boilerplate).

I believe that extensible RTTI makes sense for the Error hierarchy (which is why it was originally developed there) and I speculate that it may be useful in other hierarchies (e.g. Object) where casting is expected to be rare and not performance sensitive. I don't have a use-case in tree yet and am in no rush for this to go in, but thought I would throw it up for discussion, and in case it is useful to anyone else.

Diff Detail

Repository
rL LLVM

Event Timeline

lhames created this revision.Oct 19 2017, 10:28 PM
dblaikie edited edge metadata.Oct 20 2017, 10:23 AM

Interesting idea - yeah, curious to hear how others feel about the tradeoff (non-closed, virtual call).

The virtual call could be removed, perhaps, by having the base class hold a non-static member void* and derived classes (possibly through CRTP - which could be used in the existing design too, maybe? to reduce boilerplate) could pass the address of their ID through to the base ctor to initialize that member.

The virtual call could be removed, perhaps, by having the base class hold a non-static member void*...

Oh that's clever. I hadn't thought of that. There is a space trade-off though as it would require one char per RTTI object (in addition to the current requirement of one BSS char per class).

My current thinking is that open hierarchies are primarily (perhaps only?) useful for types that use virtual methods already, and RTTI calls are likely to be rare / non-performance-sensitive for such types. If it were accepted, my advice was going to be "Use the new system if you know you need an open hierarchy, or you know you don't care about performance". E.g. libObject. (If it sounds surprising that Object should be an open hierarchy, consider Apple's text-based API files: We want them to present as symbolic files, but they're unlike any in-tree format. It seems reasonable that other users may have similarly quirky formats that they still might want to work with llvm-nm, etc.).

dblaikie added inline comments.Oct 24 2017, 9:04 AM
Support/ExtensibleRTTITest.cpp
21 ↗(On Diff #119629)

These chars could be provided in the CRTP base, perhaps? Less boilerplate. (though this would mean they'd have linkonce linkage, rather than having strong external definitions - but I doubt that duplication in objects would be too painful?)

Could even be local to the RTTIExtends::classID function (& dynamicClassID would just call classID)

chandlerc edited edge metadata.Oct 25 2017, 4:17 PM

The virtual call could be removed, perhaps, by having the base class hold a non-static member void*...

Oh that's clever. I hadn't thought of that. There is a space trade-off though as it would require one char per RTTI object (in addition to the current requirement of one BSS char per class).

I'm not sure... If there are other virtual functions, then there would need to be some other base class with that virtual interface? At that point, you get multiple inheritance, and it isn't clear this is a win.

This actually seems a bit hard to use interestingly with RTTIRoot and virtual methods.... Hmm...

Oh, I guess the pattern is that you have your base, abstract interface derive from RTTIRoot, and then you extend that abstract interface further using the CRTP RTTIExtends? I think having this spelled out a bit more in the comments would be helpful.

The interesting tradeoff here is that if the user is going to devirtualize their interface by using dyn_cast and isa, they might end up preferring a simpler mechanism using a data member in the base class because they wouldn't have a vptr. But if they *do* have a vptr, almost certainly want to use this to avoid growing the memory of the base class.

You could design this to be parameterizable and handle both cases, but not sure if you want to do that now or wait for a concrete use case...

My current thinking is that open hierarchies are primarily (perhaps only?) useful for types that use virtual methods already, and RTTI calls are likely to be rare / non-performance-sensitive for such types. If it were accepted, my advice was going to be "Use the new system if you know you need an open hierarchy, or you know you don't care about performance".

My preference would be to focus *only* on open vs. closed. Maybe something along the lines of: "If at all possible, prefer a closed hierarchy rather than an open one, and use <technique> as it is faster and <reasons>. However, if you need an open, extensible type system use <technique> but be aware of <performance implications>."

Support/ExtensibleRTTITest.cpp
21 ↗(On Diff #119629)

Sadly, this will (I suspect) run into the non-conforming implementation of static data members of class templates on MSVC between DLLs. Specifically, the address of ID will be different between two DLLs. =[

I'm actually a bit hesitant with this entire thing as a consequence. We need to really clearly document the hard constraints on how this CRTP base is used as a consequence of this (you cannot use this as part of class templates unless you explicitly instantiate all specializations of them).

llvm/Support/ExtensibleRTTI.h
22 ↗(On Diff #119629)

Stale comment referencing error info...

Actually, this type doesn't really make a lot of sense to me (see my larger comment above)

Oh, I guess the pattern is that you have your base, abstract interface derive from RTTIRoot, and then you extend that abstract interface further using the CRTP RTTIExtends? I think having this spelled out a bit more in the comments would be helpful.

Yep. More documentation is definitely required. I just wanted to sanity check the concept before I went to the trouble of writing it. :)

My preference would be to focus *only* on open vs. closed. Maybe something along the lines of: "If at all possible, prefer a closed hierarchy rather than an open one, and use <technique> as it is faster and <reasons>. However, if you need an open, extensible type system use <technique> but be aware of <performance implications>."

That seems reasonable.

Support/ExtensibleRTTITest.cpp
21 ↗(On Diff #119629)

Sadly, this will (I suspect) run into the non-conforming implementation of static data members of class templates on MSVC between DLLs. Specifically, the address of ID will be different between two DLLs. =[

I'm not familiar with this. Is it only a problem for static data members on class templates, or does it apply to static members of ordinary classes too?

In this case MyBaseClass is an ordinary class, so I would have hoped it would be safe.

I had originally defined ID in the RTTIExtends class template so the user didn't have to write anything at all, but ran in to an issue (similar to the one you described) with MinGW, so abandoned that idea.

chandlerc added inline comments.Nov 7 2017, 3:02 PM
Support/ExtensibleRTTITest.cpp
21 ↗(On Diff #119629)

Only an issue for static data members on class templates.

So, as long as the types providing the IDs are non-templates and you're getting the correct one's address via a virtual function call, this should work out fine IIRC. But moving them into the CRTP base class won't work. =/

lhames updated this revision to Diff 176753.Dec 4 2018, 7:16 PM

Updated to add documentation, comments.

lhames added a comment.Dec 4 2018, 7:23 PM

Swinging back around to this, as I have a concrete use-case now (allowing JIT clients to query MaterializationUnit types when deciding how to dispatch compiler invocations in the JIT).

Gentle ping.

Herald added a project: Restricted Project. · View Herald TranscriptFeb 6 2019, 3:59 PM

Sorry this completely fell off my radar. It is back on my radar, and I'll try to get to it in the next day or two.

Gentle ping. Work on JITLink bought me some time, but I'm starting to hit situations where I would like to use this again. ;)

@dblaikie, @chandlerc -- Any further thoughts on this one?

I'd like to get this in as we have some use cases in the JIT (e.g. querying and down casting to user defined MaterializationUnit types when dispatching JIT work). At the time these were future use cases, now they're current ones. :)

@dblaikie, @chandlerc -- Any further thoughts on this one?

I'd like Chandler to come back around on this, given the reservations he expressed, but not sure he'll get to it - so might have to just figure it out ourselves. I share some of those reservations (that'd be easy to misuse this & have code that doesn't work on Windows - people won't read the documentation & might write the broken template situation), but maybe less concerned than Chandler was.

I'd like to get this in as we have some use cases in the JIT (e.g. querying and down casting to user defined MaterializationUnit types when dispatching JIT work). At the time these were future use cases, now they're current ones. :)

Could you sketch up a user/use case to demonstrate the use (maybe a Kaleidoscope example?)?

lhames added a comment.EditedWed, Mar 18, 10:14 AM

@dblaikie, @chandlerc -- Any further thoughts on this one?

I'd like Chandler to come back around on this, given the reservations he expressed, but not sure he'll get to it - so might have to just figure it out ourselves. I share some of those reservations (that'd be easy to misuse this & have code that doesn't work on Windows - people won't read the documentation & might write the broken template situation), but maybe less concerned than Chandler was.

Were there reservations about windows related to this patch? This certainly came up with the original design of the RTTI for Error: ErrorInfo originally defined the ID member for you, which Chandler pointed out is unsafe on Windows. We solved that in the ErrorInfo case by requiring you to define the ID member in your own class, and RTTIExtends takes the same approach.

I'd like to get this in as we have some use cases in the JIT (e.g. querying and down casting to user defined MaterializationUnit types when dispatching JIT work). At the time these were future use cases, now they're current ones. :)

Could you sketch up a user/use case to demonstrate the use (maybe a Kaleidoscope example?)?

Here are some simplified versions of the two use cases I want to use straight away:

In LLJIT set up we want the object linking layer type to be detectable to simplify setup code. E.g.

auto J = ExitOnErr(LLJITBuilder().create());
if (auto *RTDyldLinkLayer = dyn_cast<RTDyldObjectLinkingLayer>(J->getObjLinkingLayer()))
  RTDyldLinkLayer->setProcessAllSections(...);
else if (auto *ObjLinkLayer = dyn_cast<ObjectLinkingLayer>(J->getObjLinkingLayer()))
  installObjLinkingLayerPlugins(*ObjLinkingLayer);

In the dispatcher we want to be able to inspect the dynamic type of MU to decide how to dispatch the work:

ES.setDispatchMaterialization([](JITDylib &JD, std::unique_ptr<MaterializationUnit> MU) {
  if (isa<SplattingMaterializationUnit>(*MU))
    MU->doMaterialize(JD); // Materialize splatting MUs in-place.
  else if (auto &IRMU = dyn_cast<IRMaterializationUnit>(*MU))
    dispatchIRMU(std::move(MU)); // Analyze IR and decide how to dispatch
  else
    CompileThreads->async([MU=std::move(MU), &JD]() {
      MU->doMaterialization(JD); // Otherwise default to materializing on background thread
    });
});

As soon as this lands I'll add an OrcV2Example project covering it. Stefan is also planning to use this in his ThinLtoJIT example.

@dblaikie, @chandlerc -- Any further thoughts on this one?

I'd like Chandler to come back around on this, given the reservations he expressed, but not sure he'll get to it - so might have to just figure it out ourselves. I share some of those reservations (that'd be easy to misuse this & have code that doesn't work on Windows - people won't read the documentation & might write the broken template situation), but maybe less concerned than Chandler was.

Were there reservations about windows related to this patch?

I think this bit amounts to "reservations because of Windows":

"Sadly, [using a CRTP base to provide the ID] will (I suspect) run into the non-conforming implementation of static data members of class templates on MSVC between DLLs. Specifically, the address of ID will be different between two DLLs. =[

I'm actually a bit hesitant with this entire thing as a consequence. We need to really clearly document the hard constraints on how this CRTP base is used as a consequence of this (you cannot use this as part of class templates unless you explicitly instantiate all specializations of them)."

This certainly came up with the original design of the RTTI for Error: ErrorInfo originally defined the ID member for you, which Chandler pointed out is unsafe on Windows. We solved that in the ErrorInfo case by requiring you to define the ID member in your own class, and RTTIExtends takes the same approach.

The existence of the ErrorInfo doing something similar (could it be ported to use this infrastructure, then?) does help justify this a bit, but I appreciate the concerns of generalizing this further.

I'd like to get this in as we have some use cases in the JIT (e.g. querying and down casting to user defined MaterializationUnit types when dispatching JIT work). At the time these were future use cases, now they're current ones. :)

Could you sketch up a user/use case to demonstrate the use (maybe a Kaleidoscope example?)?

Here are some simplified versions of the two use cases I want to use straight away:

In LLJIT set up we want the object linking layer type to be detectable to simplify setup code. E.g.

auto J = ExitOnErr(LLJITBuilder().create());
if (auto *RTDyldLinkLayer = dyn_cast<RTDyldObjectLinkingLayer>(J->getObjLinkingLayer()))
  RTDyldLinkLayer->setProcessAllSections(...);
else if (auto *ObjLinkLayer = dyn_cast<ObjectLinkingLayer>(J->getObjLinkingLayer()))
  installObjLinkingLayerPlugins(*ObjLinkingLayer);

This currently would vary by platform & some other things, such that it's a dynamic choice you want to check for?

But for a user-defined ObjLinkingLayer - the user would know which type they had used and could static_cast instead, yes?

I guess maybe that raises a thought: Is this about having an open hierarchy, where you can dyn_cast to specific pre-provided types, or necessarily to support dyn_casting to user-provided types too? Would a solution that only allows dyn_cast between LLVM-provided types be sufficient for your use cases? (yeah, I know it'd be weird & I could see not wanting to do that because of the awkward asymmetry, but trying to understand the problem space a bit further)

In the dispatcher we want to be able to inspect the dynamic type of MU to decide how to dispatch the work:

ES.setDispatchMaterialization([](JITDylib &JD, std::unique_ptr<MaterializationUnit> MU) {
  if (isa<SplattingMaterializationUnit>(*MU))
    MU->doMaterialize(JD); // Materialize splatting MUs in-place.
  else if (auto &IRMU = dyn_cast<IRMaterializationUnit>(*MU))
    dispatchIRMU(std::move(MU)); // Analyze IR and decide how to dispatch
  else
    CompileThreads->async([MU=std::move(MU), &JD]() {
      MU->doMaterialization(JD); // Otherwise default to materializing on background thread
    });
});

Same sort of question - I guess the LLVM version of this code would be closed, but users would provide their own kinds of MUs - to support those users using both their own custom ones and standard ones, do the users control the MUs, such that they could test for all the types of MUs they know they are using in their JIT. Yeah, that'd be a bit awkward to be "is it MyCustomMU3" ends up being "is it not an IRMU, not a <other LLVM-provided MU>, etc... then it must be a MyCustomMU of some kind, so then do a different pseudo RTTI test".

I think this bit amounts to "reservations because of Windows":

"Sadly, [using a CRTP base to provide the ID] will (I suspect) run into the non-conforming implementation of static data members of class templates on MSVC between DLLs. Specifically, the address of ID will be different between two DLLs. =["

I think this was Chandler responding to your suggestion that the ID be moved into the CRTP base, rather than to this patch. :)

"I'm actually a bit hesitant with this entire thing as a consequence. We need to really clearly document the hard constraints on how this CRTP base is used as a consequence of this (you cannot use this as part of class templates unless you explicitly instantiate all specializations of them)."

The "entire thing" presumably applies to this patch, but I think this just requires sufficient documentation. The rules are simple enough to convey:

  1. Your class must provide a static char ID member (failure to do so will result in RTTI failures at runtime).
  1. RTTIExtends should *only* be subclassed by classes, not class templates, due to Windows handling of ODR static data members.

If we wanted to preclude the problem with (1) we could switch to a traits class for supplying the ID, but we can't do much for (2) except document.

This certainly came up with the original design of the RTTI for Error: ErrorInfo originally defined the ID member for you, which Chandler pointed out is unsafe on Windows. We solved that in the ErrorInfo case by requiring you to define the ID member in your own class, and RTTIExtends takes the same approach.

The existence of the ErrorInfo doing something similar (could it be ported to use this infrastructure, then?)...

That's my plan: Replace ErrorInfo with RTTIExtends if this lands.

This currently would vary by platform & some other things, such that it's a dynamic choice you want to check for?

Yep. The idea is that the client shouldn't have to replicate the JIT's internal logic to intuit the layer type, since that internal logic is subject to change and casting to the wrong type could (in the worst case) fail quietly but disastrously. Since the client may be using a repackaged JIT that could mix in its own layer types / setup I would consider this a dynamic choice on an effectively open hierarchy.

I guess maybe that raises a thought: Is this about having an open hierarchy, where you can dyn_cast to specific pre-provided types, or necessarily to support dyn_casting to user-provided types too?

This is specifically to support a fully open hierarchy. A solution that allows dyn_cast between LLVM-provided types only (or any fixed set of types) would be insufficient.

Same sort of question - I guess the LLVM version of this code would be closed, but users would provide their own kinds of MUs - to support those users using both their own custom ones and standard ones, do the users control the MUs, such that they could test for all the types of MUs they know they are using in their JIT. Yeah, that'd be a bit awkward to be "is it MyCustomMU3" ends up being "is it not an IRMU, not a <other LLVM-provided MU>, etc... then it must be a MyCustomMU of some kind, so then do a different pseudo RTTI test".

Yep. And what if the client is using a JIT built on top of ORC that has their own set of MU types? Then you can't even reason by elimination: Ok, it's not an LLVM MU, but is it one of mine, or one of the other library author's?

dblaikie accepted this revision.Wed, Mar 18, 3:11 PM

Fair enough :)

This revision is now accepted and ready to land.Wed, Mar 18, 3:11 PM