dblaikie reopened this revision.
dblaikie added a comment.
This revision is now accepted and ready to land.

Sorry about the delays in review - but please revert this patch until we can 
hash out a few more details. I really don't think this is the best direction 
forward for a core abstraction & I'll do my best to explain why & try to 
understand where you're coming from.

In D83088#2232893 <https://reviews.llvm.org/D83088#2232893>, @nhaehnle wrote:

>>>>> The most immediate problem is divergence analysis, which is extremely 
>>>>> complex and difficult to get right. If I had tried to fight the 
>>>>> accidental complexity that comes with attempting to write such an 
>>>>> algorithm as C++ templates in addition to the inherent complexity of the 
>>>>> algorithm at the same time, I'm not sure I would have been able to 
>>>>> produce anything workable at all.
>>>>>
>>>>> Frankly, I suspect that our dominator tree implementation also suffer 
>>>>> because of this, though at least dominator trees are much more well 
>>>>> studied in the academic literature, so that helps keep the inherent 
>>>>> complexity under control.
>>>>
>>>> I'm totally open to discussing making APIs more usable, for sure - though 
>>>> I'm thinking it's likely a concept (like containers in the C++ standard 
>>>> library) might be the better direction.
>>>>
>>>> Perhaps some code samples showing how one would interact (probably not 
>>>> whole algorithms - maybe something simple like generating a dot diagram 
>>>> for a graph) with these things given different APIs (traits, concepts, and 
>>>> runtime polymorphism) - and implementations of each kind too.
>>>
>>> Take a look here for example: 
>>> https://github.com/nhaehnle/llvm-project/blob/715450fa7f968ceefaf9c3b04b47066866c97206/llvm/lib/Analysis/GenericConvergenceUtils.cpp#L499
>>>  -- this is obviously still fairly simple, but it's an example of printing 
>>> out the results of an analysis in a way that's generic over the underlying 
>>> CFG and SSA form.
>>
>> I'm having trouble following this example - I'm not sure what the CfgPrinter 
>> abstraction is/why it's first-class, and why this "print" function is 
>> calling what look like mutation operations like "appendBlocks". I guess 
>> perhaps the question is - what's it printing from and what's it printing to?
>>
>> Ah, I see, the "append" functions are accessors, of a sort. Returning a 
>> container might be more clear than using an out parameter - alternatively, a 
>> functor parameter (ala std::for_each) that is called for each element, that 
>> can then be used to populate an existing container if desired, or to do 
>> immediate processing without the need for an intermediate container.
>
> The code is trying to strike a balance here in terms of performance. Since 
> dynamic polymorphism is used, a functor-based traversal can't be inlined and 
> so the number of indirect function calls increases quite a bit. There are a 
> number of use cases where you really do want to just append successors or 
> predecessors to a vectors, like during a graph traversal. An example graph 
> traversal is here: 
> https://github.com/nhaehnle/llvm-project/blob/controlflow-wip-v7/llvm/lib/Analysis/GenericConvergenceUtils.cpp#L329

One way to simplify the dynamic polymorphism overhead of iteration would be to 
invert/limit the API - such as having a "node.forEachEdge([](const Edge& E) { 
... });" or the like.

>> Though the printer abstraction still strikes me as a bit strange - 
>> especially since it doesn't seem to be printing itself. This function was 
>> passed a printer and a stream - the printer prints to the stream (perhaps 
>> it'd make more sense for the printer to take the stream on construction) and 
>> the function isn't passed the thing to print at all - that thing is accessed 
>> from the printer. That seems fairly awkward to me - I'd expect a printing 
>> operation to take a thing to be printed and a thing to print to.
>
> Keep in mind that where those `printBlockName` / `printValue` methods are 
> used, they will always be mixed in with printing of other things. So the 
> alternative you're describing would result in code like:
>
>   dbgs() << " currently at: ";  // explicit stream
>   printer.printBlockName(block); // implicit stream
>   dbgs() << '\n'; // explicit stream
>
> I would argue that that ends up being more awkward because it mixes implicit 
> streams with explicitly given streams.
>
> We could perhaps add versions of the methods that return a `Printable` 
> object, so that we can write:
>
>   dbgs() << "currently at: " << printer.printableBlockName(block) << '\n';
>
> By the way, the main motivation for making the CfgPrinter a separate object 
> is that printing LLVM IR efficiently requires keeping a ModuleSlotTracker 
> around. Splitting the CfgPrinter off from the CfgInterface allows the 
> fast-path of code that doesn't want debug prints to not have to carry a 
> reference to a ModuleSlotTracker around, even if it's just an empty 
> std::unique_ptr. That's a comparatively small burden though, so I'd be fine 
> with merging the CfgPrinter back into the CfgInterface.

I'm generally worried about the genericity of these abstractions - whether or 
not a generic abstraction over printing is required/pulling its weight. Are 
there common abstractions over printing you have in mind using this abstraction?

>>> A statically polymorphic wrapper is here: 
>>> https://github.com/nhaehnle/llvm-project/blob/715450fa7f968ceefaf9c3b04b47066866c97206/llvm/include/llvm/Analysis/GenericConvergenceUtils.h#L569
>>>
>>> The simple example might be bearable writing as a template, precisely 
>>> because it's simple -- so only looking at simple examples is unlikely to 
>>> really capture the motivation. Really what the motivation boils down to is 
>>> stuff like this: 
>>> https://github.com/nhaehnle/llvm-project/blob/controlflow-wip-v7/llvm/lib/Analysis/GenericUniformAnalysis.cpp
>>>  -- I don't fancy writing all this as a template.
>>>
>>> Third motivation would essentially go away if C++ could type-check against 
>>> traits in the way that Rust and other languages like it can -- but it 
>>> can't, so here we are.
>>
>> I hesitate to write code that's more idiomatic in a language that isn't C++. 
>> Agreed that long/complicated algorithms as templates aren't the best thing - 
>> but sometimes can be quite suitable/idiomatic C++ (see the C++ standard 
>> library).
>
> I think there's a misunderstanding here. The code I'm writing based on 
> CfgTraits would not be more idiomatic in Rust or any other language that I'm 
> aware of. The point is that writing the code in the way that it seems you 
> want it to be written would be feasible in Rust, but isn't feasible in C++. 
> It's about how type-checking of generics/templates works in those languages.
>
> Comparing to the C++ STL, the STL is significantly simpler on the aspect that 
> matters here. The templates in the STL have comparatively simple contracts 
> with their template parameters. In most cases, they only care about a very 
> small number of things, like copy-/move-ability and comparison operators. The 
> "interface" of the CfgTraits is broader and deeper. Repeating myself again, 
> this is in large part because it also has a notion of SSA values, but there 
> are other reasons, e.g. caring about printability to make debugging less 
> painful.

Ah, sorry - I don't mean to treat CfgGraph to be thought of like the template 
parameter to, say, std::vector  - I meant thinking of CfgGraph as something 
like std::vector itself. Rather than using traits to access containers in the 
C++ standard library, the general concept of a container is used to abstract 
over a list, a vector, etc.

eg, if you want to print the elements of any C++ container, the code looks like:

  template<typename Container>
  void print(const Container &C, std::ostream &out) {
    out << '{';
    bool first = true;
    for (const auto &E : C) {
      if (!first)
        out << ", ";
      first = false;
      out << E;
    }
    out << '}';
  }

Which, yes, is much more legible than what one could imagine a 
GraphTraits-esque API over containers might be:

  template<typename Container, typename Traits = ContainerTraits<Container>>
  void print(const Container &C, std::ostream &out) {
    out << '{';
    bool first = true;
    for (const auto &E : Traits::children(C)) {
      if (!first)
        out << ", ";
      first = false;
      out << Traits::element(E);
    }
    out << '}';
  }

Or something like that - and the features you'd gain from that would be the 
ability to sort of "decorate" your container without having to create an actual 
container decorator - instead implementing a custom trait type that, say, 
iterates container elements in reverse. But generally a thin decorator using 
the first non-traits API would be nicer (eg: llvm::reverse(container) which 
gives you a container decorator that reverses order).

If you had a runtime polymorphic API over containers in C++, then it might look 
something like this:

  void print(const ContainerInterface& C, std::ostream& out) {
    out << '{';
    bool first = true;
    C.for_each([&](const auto &E) {
      if (!first)
        out << ", ";
      first = false;
      E.print(out);
    });
    out << '}';
  }

(printing, as you've mentioned, might get a bit complicated - perhaps a 
"visitor" pattern would be suitable for printing, then:

  void print(const ContainerInterface& C, std::ostream& out) {
    out << '{';
    bool first = true;
    C.for_each([&](const auto &E) {
      if (!first)
        out << ", ";
      first = false;
      E.print(out);
    });
    out << '}';
  }

I'd really like to see examples like this ^ using the different abstractions 
under consideration here (classic GraphTraits, CfgTraits dynamic and static 
typed, perhaps what a static API would look like if it wasn't trying to be 
dynamic API compatible).

>> That said, I'd like to help make things more usable, for sure - but I'm not 
>> sure/currently feeling like this might not be the best direction for 
>> achieving that goal & I think some clear comparisons - even for overly 
>> simplistic code, where the overhead of a more complex solution may not be 
>> felt as acutely (actually, small examples might show syntactic overhead more 
>> acutely - if it takes more code to do the same thing, when that code isn't 
>> washed out by a lot of code that would be the same regardless of 
>> implementation, it will hopefully be more obvious, rather than less), 
>> hopefully it'll be more a more clear/concrete basis on which to discuss 
>> relative design tradeoffs.
>
> It's not really about needing to write more or less source code -- see the 
> example above. It's about the fact that C++ can't type-check templates in a 
> useful way. Templates are effectively duck-typed and type-checking only 
> happens at instantiation, which makes their maintenance a pain over time. A 
> nice example of the negative effects that this can have in practice is you 
> end up with stuff like (from mlir/include/mlir/IR/Block.h):
>
>   /// Print out the name of the block without printing its body.
>   /// NOTE: The printType argument is ignored.  We keep it for compatibility
>   /// with LLVM dominator machinery that expects it to exist.
>   void printAsOperand(raw_ostream &os, bool printType = true);
>
> Why is this method called `printAsOperand`, which makes no sense for a 
> generic interface of a graph? And why does it have a `printType` argument? 
> Presumably this happened because dominator trees were first written for IR 
> and then genericized from there without taking the time to properly think 
> about and define what the interface between generic dominator trees and their 
> template parameter should be. So then an implementation detail of 
> llvm::BasicBlock ended up leaking all over the place.
>
> When I started out writing the algorithms around `CfgTraits`, I didn't even 
> know what the right interface should be, and I expect that there may well 
> still be tweaks to the interface going forward. Writing the code in the way 
> that I'm writing it helps keeps us honest and prevents weird escapes like 
> `printAsOperand`. Perhaps even more importantly, as @kuhar also suggested, 
> the quality-of-life when writing and maintaining code is much improved 
> because you get more useful type-checking.

Indeed template code can get a bit weird - but I'm not sure it's quite enough 
to justify the change in/complications of mulitple (static and dynamic) 
abstractions here just yet. It might be that taking a more structured C++ 
idiomatic approach to template design (like C++ standard container concept 
abstractions) might lead to more usable code without /some/ complexities 
(though may trade others).


Repository:
  rG LLVM Github Monorepo

CHANGES SINCE LAST ACTION
  https://reviews.llvm.org/D83088/new/

https://reviews.llvm.org/D83088

_______________________________________________
cfe-commits mailing list
cfe-commits@lists.llvm.org
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to