[llvm-dev] [RFC] Replacing inalloca with llvm.call.setup and preallocated

Reid Kleckner via llvm-dev llvm-dev at lists.llvm.org
Sat Mar 28 11:59:28 PDT 2020


Sorry for the delay. Arthur Eubanks has started working on the design here:
https://reviews.llvm.org/D74651
I felt I should follow up here about that.

On Mon, Jan 27, 2020 at 6:47 PM Eli Friedman <efriedma at quicinc.com> wrote:

> It doesn’t seem like multiple call sites should be a problem if they’re
> sufficiently similar?  If the argument layout for each callsite is the
> same, it doesn’t matter which callsite the backend chooses to compute the
> layout
>
It's feasible, but it seems like asking for bugs. What should the backend
do when it detects two preallocated calls with different layouts? Let's
imagine LTO is promoting one indirect call that uses call.setup into
several direct calls. The IR would look like this:

%cs = call.setup
switch i32 %callee ...
callee1:
  call void @callee1(i32 %x, %struct.Foo* preallocated(%struct.Foo) %foo)
  br label %rejoin
callee2:
  call void @callee2(i32 %x, %struct.Foo* preallocated(%struct.Foo) %foo)
  br label %rejoin
rejoin:
  ...

A logical next step would be to run DAE. Suppose one callee does not use
i32 %x above. Now the prototypes disagree, and we can't lower the call. We
could teach DAE that all calls using callsetup tokens have to have the same
prototype, but a simple verifier rule earlier (one call per call setup)
seems easier to enforce.

> > Nested setup is OK, but the verifier rule that there must be a paired
> call site should make it impossible to do in a loop. I guess we should have
> some rule to reject the following:
>
> %cs1 = llvm.call.setup()
>
> %cs2 = llvm.call.setup()
>
> call void @cs1() [ "callsetup"(token %cs1) ]
>
> call void @cs2() [ "callsetup"(token %cs2) ]
>
>
>
> I think in general, there can be arbitrary control flow between a token
> and its uses, as long as the definition dominates the use.  So you could
> call llvm.call.setup repeatedly in a loop, then call some function using
> the callsetup token in a different loop, unless some rule specific to
> callsetup forbids it.
>
I agree that the IR would validate according to the current rules, but I
would interpret that IR as meaning: setup N call sites in a loop, then
destroy just the last call site N times in a loop. For example, if you
replaced the call site token in this example with stacksave+alloca and
teardown with stackrestore, the semantics would be: allocate lots of stack,
then reset to the last saved stack pointer repeatedly.

I think we can write some non-verifier rules that prohibit this kind of
code pattern. Optimizations should already obey these rules since they
don't reorder things that modify inaccessible state. Things like:
- It is UB to stacksave ; call.setup ; stackrestore ; call
- UB to call.setup 1 ; call.setup 2 ; call 1 ; call 2
- UB to call.setup ; alloca ; call
etc

> It would be nice to make the rules strong enough to ensure we can
> statically compute the size of the stack frame at any point (assuming no
> dynamic allocas).  Code generated by clang would be statically well-nested,
> I think; not sure how hard it would be to ensure optimizations maintain
> that invariant.
>
I agree, that is definitely a goal of the redesign.

> Connecting nested llvm.call.setups using tokens might make it easier for
> passes to reason about the nesting, since the region nest would be
> explicitly encoded.
>
I agree, that could be useful, it would replicate what we did for exception
handling.

> > VLAs could use something like this, but they are generally of unknown
> size while call sites have a known fixed size. I think that makes them
> pretty different.
>
> I don’t think we need to implement it at the same time, but the systems
> would interact, so it might be worth planning out.
>
I do recall that MSVC does some pretty squirrelly stuff when you insert
`alloca` calls into a call sequence. In this example, arguments are
evaluated in the order 2, 3, 1:

struct Foo {
  Foo();
  Foo(const Foo &o);
  ~Foo();
  int x;
};
void receive_ptr(Foo, void *, Foo);
extern "C" void *_alloca(size_t n);
extern int gv;
void f(int n) { receive_ptr(Foo(), _alloca((++gv, n)), Foo()); }

The standard says order of argument evaluation is unspecified, so this
ordering is valid. However, I wouldn't want to implement the same behavior
in clang. We should probably implement some kind of warning, though.
Compiled with clang, this code has undefined behavior.

Is there really any other option for us here, other than to document that
using VLAs, alloca, and call.setup in the wrong order will result in UB? We
can precisely nail down what the "right order" is, but we basically can't
make arbitrary stack adjustment orderings work.

>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.llvm.org/pipermail/llvm-dev/attachments/20200328/61a848df/attachment-0001.html>


More information about the llvm-dev mailing list