Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Right. Would you happen to know if there's a way to make a retained object actually deallocate on last "release," other than putting an autorelease pool around it? Would you also happen to know if the Swift bindings have this same wonky autorelease behavior, or more straightforward Swift-style refcounting?


From a quick grep, it looks like this wrapper does not automatically call autorelease, so autorelease happens only when a method implementation autoreleases its own return value (or if you call autorelease yourself).

Objective-C has a standard rule for when a method is supposed to autorelease its return value: to quote the metal-cpp readme, it's when "you create an object with a method that does not begin with `alloc`, `new`, `copy`, or `mutableCopy`". In many cases, these are convenience methods that also have equivalent "initWith" methods, e.g.

    [NSString stringWithFormat:@"%d", 42]
is equivalent to

    [[[NSString alloc] initWithFormat:@"%d", 42] autorelease]
But I'm not too familiar with Metal, and… it looks like Metal doesn't have very many of these methods in the first place. Instead it has a lot of 'new' methods, which shouldn't use autorelease.

When a method does call autorelease, Swift doesn't have any way of getting around it, though if there are autoreleasing and alloc/init variants of the same method, it will prefer alloc/init. Other than that, Swift likes to use the function objc_retainAutoreleasedReturnValue [1], which may prevent the object from going on the autorelease pool (with the 'optimized return' magic), but only as a non-guaranteed optimization.

[1] https://github.com/apple-opensource/objc4/blob/a367941bce42b...


Thanks, this is helpful. The specific method that's causing me trouble right at the moment is "computeCommandEncoder"[1], which is a method off MTLCommandBuffer, and I think not in the "new" class. In the Rust bindings[2], this is just a msg_send! and from what I can tell is getting an autoreleased reference, not a retained one.

It looks like objc_retainAutoreleasedReturnValue might be exactly what I'm looking for, even if it isn't 100% guaranteed; if I'm understanding, it would be safe, and wouldn't actually leak as long as you had an autorelease pool somewhere in the chain. However, I'm not seeing a binding to it in the objc crate[3]. Sounds like maybe I should file an issue?

Also, given that such a method seems obviously useful, I wonder why it's not being called from these C++ bindings?

[1]: https://developer.apple.com/documentation/metal/mtlcommandbu...

[2]: https://github.com/gfx-rs/metal-rs/blob/master/src/commandbu...

[3]: https://docs.rs/objc/0.2.7/objc/runtime/index.html


> The specific method that's causing me trouble right at the moment is "computeCommandEncoder"

Yeah, it looks like there's no way to avoid autorelease here.

> It looks like objc_retainAutoreleasedReturnValue might be exactly what I'm looking for, even if it isn't 100% guaranteed; if I'm understanding, it would be safe, and wouldn't actually leak as long as you had an autorelease pool somewhere in the chain.

Indeed it would be safe and wouldn't leak, but the optimization is very much not guaranteed. It's based on the autorelease implementation manually reading the instruction at its return address to see if it's about to call objc_retainAutoreleaseReturnValue. See the description here:

https://github.com/apple-opensource/objc4/blob/a367941bce42b...

In fact – I did not know this before just now – on every arch other than x86-64 it requires a magic assembly sequence to be placed between the call to an autoreleasing method and the call to objc_retainAutoreleaseReturnValue.

It looks like swiftc implements this by just emitting LLVM inline asm blocks:

    %6 = call %1* bitcast (void ()* @objc_msgSend to %1* (i8*, i8*, %0*)*)(i8* %5, i8* %3, %0* %4) #4
    call void asm sideeffect "mov\09fp, fp\09\09// marker for objc_retainAutoreleaseReturnValue", ""()
    %7 = bitcast %1* %6 to i8*
    %8 = call i8* @llvm.objc.retainAutoreleasedReturnValue(i8* %7)
This is optimistically assuming that LLVM won't emit any instructions between the call instruction and the magic asm, which is not guaranteed, especially if compiler optimizations are off. But if it does emit extra instructions, then you just don't get the autorelease optimization: the object is added to the autorelease pool, and objc_retainAutoreleaseReturnValue simply calls objc_retain.

(…Though, on second look, it seems that swiftc and clang sometimes use a different, more robust approach to emitting the same magic instruction… but only sometimes.)

Regardless, enough stars have to align for the optimization to work that you shouldn't rely on it to avoid a (temporary) memory leak; you should only treat it as an optional micro-optimization.

That said, the C++ buildings could have implemented the same scheme using inline assembly. And so could the Rust crate (edit: well, I guess inline asm is not stable in Rust yet). It's not the like magic instructions are ABI unstable or anything, given that clang and swiftc happily stick them in when compiling any old Objective-C or Swift code. But I'm guessing the authors of the C++ bindings either didn't want to bother with inline assembly, or considered it an unnecessary micro-optimization. Or perhaps didn't even know about it. /shrug/


Thanks very much for this detailed information, it's very helpful. I've filed https://github.com/gfx-rs/metal-rs/issues/222 to track it in the Rust ecosystem side.


If the object is not autoreleased then doing a release call will deallocate the object, otherwise it will be added to the nearest autorelease pool from the current stack and be deallocated when the pool is drained.

Swift and obj-c have the same ARC semantics, so I'm not sure what you mean by swift-style refcounting. It should be identical to the obj-c ARC semantics.

https://clang.llvm.org/docs/AutomaticReferenceCounting.html outlines the ARC semantics, including the autorelease behavior.


By "swift-style refcounting" I mean the object is reliably deallocated exactly when the last release is called. Following up on the response to comex, I would say I would get these semantics if I called objc_retainAutoreleasedReturnValue on the allocating method's return value, and it actually worked.


In general, a retained object is deallocated on last release. However ownership of some objects somewhere may have been given to an autoreleasepool, in which case “the last release” for those objects will come from the pool. To what extent this happens is implementation-defined.

Swift and ObjC implementations have levers which discourage objects being sent to the pool in common cases. It is possible to pull them from other languages but not easy.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: