This is an archive of the discontinued LLVM Phabricator instance.

[ObjC] Replace uses of the argument of a call to objc_autorelease with the result in MRR
AbandonedPublic

Authored by ahatanak on May 10 2019, 3:29 PM.

Details

Summary

This is needed to enable performing tail-call optimization on calls to objc_autorelease in MRR, which enables objc_autorelease to perform the retainRV/ autoreleaseRV handshake that keeps the returned object out of the autorelease pool.

With this patch, the backend can tail-call the call to objc_autorelease in function bar in the code below.

// We currently tail call the call to `objc_autorelease` in `foo`
NSObject *foo(void) {
  NSObject *t = [NSObject new];
  return [t autorelease];
}

// We currently don't tail-call the call to `objc_autorelease` in `bar` because its result isn't used by the return instruction.
//
// %call = tail call i8* bitcast (i8* (i8*, i8*, ...)* @objc_msgSend to i8* (i8*, i8*)*)(i8* %0, i8* %1)
// %2 = bitcast i8* %call to %0*
// %3 = tail call i8* @objc_autorelease(i8* %call) #2
// ret %0* %2

NSObject *bar(void) {
  NSObject *t = [NSObject new];
  [t autorelease];
  return t;
}

rdar://problem/50353574

Diff Detail

Repository
rL LLVM

Event Timeline

ahatanak created this revision.May 10 2019, 3:29 PM
ahatanak planned changes to this revision.May 11 2019, 12:39 AM

Someone reminded me that I could replace the MRR call to objc_autorelease with the ARC call to objc_autoreleaseReturnValue in the front-end. I'm going to try that idea.

The assumption that these methods return the receiver is not true without additional semantic assumptions about these methods, so the current behavior of unconditionally turning [x autorelease] into objc_autorelease(x), x seems quite broken.

The assumption that these methods return the receiver is not true without additional semantic assumptions about these methods, so the current behavior of unconditionally turning [x autorelease] into objc_autorelease(x), x seems quite broken.

Because it's possible to override an autorelease method and make the method return something other than the passed argument, for example?

If it's not possible to assume that the method returns the passed argument in MRR, the direction of this patch and https://reviews.llvm.org/D61970 is completely wrong.

The assumption that these methods return the receiver is not true without additional semantic assumptions about these methods, so the current behavior of unconditionally turning [x autorelease] into objc_autorelease(x), x seems quite broken.

Because it's possible to override an autorelease method and make the method return something other than the passed argument, for example?

If it's not possible to assume that the method returns the passed argument in MRR, the direction of this patch and https://reviews.llvm.org/D61970 is completely wrong.

Well, objc_retain and objc_autorelease (and objc_release, trivially) do return the normal return value of the message send in MRR, so assuming that's preserved in the IR, you just need to use that as the expression result. But if the backend's going to unconditionally optimize the intrinsics based on the assumption that those values are the same, that's problematic, just as it would be problematic for it to do ARC-approved reordering on intrinsics emitted for MRR.

The assumption that these methods return the receiver is not true without additional semantic assumptions about these methods, so the current behavior of unconditionally turning [x autorelease] into objc_autorelease(x), x seems quite broken.

Because it's possible to override an autorelease method and make the method return something other than the passed argument, for example?

If it's not possible to assume that the method returns the passed argument in MRR, the direction of this patch and https://reviews.llvm.org/D61970 is completely wrong.

Well, objc_retain and objc_autorelease (and objc_release, trivially) do return the normal return value of the message send in MRR, so assuming that's preserved in the IR, you just need to use that as the expression result. But if the backend's going to unconditionally optimize the intrinsics based on the assumption that those values are the same, that's problematic, just as it would be problematic for it to do ARC-approved reordering on intrinsics emitted for MRR.

Okay, that means it's not legal to tail-call the call to objc_autorelease in function bar in the summary since we can't assume t is the same as the result of [t autorelease]. Also, it's not correct to emit the calls to objc_autoreleaseReturnValue and objc_retainAutoreleasedReturnValue as intrinsics calls and let the ARC optimizer and ARC contract pass optimize them as if they were compiled in ARC as I did in https://reviews.llvm.org/D61970.

Oh, yes, that's correct, at least under base ObjC rules.

Now, we *are* talking here about making some other higher-level assumptions about retain/release/autorelease that already exceed base ObjC rules. Turning autorelease into autoreleaseRV isn't sound under base ObjC rules because nothing in base ObjC permits us to not do a message send that's clearly called for in the source; but nonetheless we do want to do some optimizations like that even in MRR, and we think that's tolerable in part because retain and release are already special in ObjC (because of retain properties and block captures). So maybe it's not unreasonable to consider this assumption about the return value of retain and autorelease to be part of the higher-level semantics we want to assume for these messages.

However, (1) we need to carefully document those higher-level assumptions and (2) we need to make sure they're all disabled by this same no-builtin-retain-release flag, which means propagating information to the optimizer somehow (by just emitting non-builtin messages as normal message sends if necessary).

We discussed this before, but we already break the base ObjC rules since the runtime elides the autorelease message send in MRR when it's possible to perform the retainRV/autoreleaseRV optimization, which only happens when the caller (or a caller higher up in the call-chain if it's tail-called) is compiled with ARC. IIUC, the only semantic change we''ll be making if we emit an objc_autoreleaseReturnValue call instead of an objc_autorelease call is that we no longer guarantee that the message send will happen when the class overrides the autorelease method, which we do guarantee with calls to objc_autorelease (see objc_object::autorelease and objc_object::rootAutorelease in the link below).

https://opensource.apple.com/source/objc4/objc4-750/runtime/objc-object.h.auto.html

If we also emit an objc_retainAutoreleasedReturnValue call instead of an objc_retain call, the retainRV/autoreleaseRV handshake will happen more frequently since now the caller doesn't have to be compiled in ARC in order for it to happen.

I don't know what you mean. Currently, if the callee tail-calls autorelease (and necessarily this has to be MRR code), we perform an ordinary message send of autorelease. It happens to be the case that the standard implementation of autorelease will allow the autorelease to be reclaimed, but nothing about that choice is in any way non-standard on the MRR side.

IIUC, this is what happens when there is an autorelease message send in the source code:

  1. When there is an autorelease message send, clang's IRGen converts it to a call to objc_autorelease. This is the result of Pete's work that was committed a few months ago.
  2. objc_autorelease calls objc_object::autorelease.
  3. objc_object::autorelease calls rootAutorelease if !ISA()->hasCustomRR() is true. Otherwise, it sends an autorelease message.
  4. rootAutorelease returns (id)this if prepareOptimizedReturn(ReturnAtPlus1) is true.

So the autorelease message isn't sent if !ISA()->hasCustomRR(). Doesn't that mean it is breaking the basic ObjC rules?

No, because hasCustomRR is set whenever the method implementation isn't the standard -[NSObject autorelease] implementation. In other words, this is just inlining the standard implementation when it's dynamically known that the dispatch would end up there anyway.

Okay, I see. The result of calling rootAutorelease when !ISA()->hasCustomRR() is true is the same as a message send, so it doesn't break the basic rules.

Right, exactly.

ahatanak abandoned this revision.May 24 2019, 1:25 PM

This patch is based on the assumption that the argument and the return value of objc_autorelease are equal, which isn't always true in MRR.