Skip to content

Commit b66594d

Browse files
authored
Merge pull request #81 from madsmtm/optimized-autorelease-retain
Add `Id::retain_autoreleased` An optimized version of `Id::retain` that uses `objc_retainAutoreleasedReturnValue`.
2 parents 0e868ca + e149351 commit b66594d

File tree

24 files changed

+424
-20
lines changed

24 files changed

+424
-20
lines changed

.github/workflows/ci.yml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,13 @@ jobs:
302302
args: --features ${{ env.FEATURES }} ${{ env.TESTARGS }}
303303

304304
- name: Test in release mode
305+
if: ${{ !matrix.dinghy }}
306+
uses: actions-rs/cargo@v1
307+
with:
308+
command: test
309+
args: --no-default-features ${{ env.TESTARGS }} --release
310+
311+
- name: Test in release mode with features
305312
if: ${{ !matrix.dinghy }}
306313
uses: actions-rs/cargo@v1
307314
with:
@@ -346,10 +353,12 @@ jobs:
346353
xcrun simctl boot $SIM_ID
347354
348355
# Build
349-
cargo dinghy build
356+
cargo dinghy --device=$SIM_ID build
350357
351358
# Run tests
352359
cargo dinghy --device=$SIM_ID test --no-default-features
360+
cargo dinghy --device=$SIM_ID test --release
361+
353362
# Enable a few features. We're doing it this way because cargo dingy
354363
# doesn't support specifying features from a workspace.
355364
sed -i -e '/\[features\]/a\

objc2-foundation/src/array.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ impl<T: Message> NSArray<T, Shared> {
140140
pub fn get_retained(&self, index: usize) -> Id<T, Shared> {
141141
let obj = self.get(index).unwrap();
142142
// SAFETY: The object is originally shared (see `where` bound).
143-
unsafe { Id::retain(obj as *const T as *mut T).unwrap_unchecked() }
143+
unsafe { Id::retain_autoreleased(obj as *const T as *mut T).unwrap_unchecked() }
144144
}
145145

146146
pub fn to_shared_vec(&self) -> Vec<Id<T, Shared>> {
@@ -247,7 +247,7 @@ impl<T: Message, O: Ownership> NSMutableArray<T, O> {
247247
pub fn replace(&mut self, index: usize, obj: Id<T, O>) -> Id<T, O> {
248248
let old_obj = unsafe {
249249
let obj = self.get(index).unwrap();
250-
Id::retain(obj as *const T as *mut T).unwrap_unchecked()
250+
Id::retain_autoreleased(obj as *const T as *mut T).unwrap_unchecked()
251251
};
252252
unsafe {
253253
let _: () = msg_send![
@@ -262,7 +262,7 @@ impl<T: Message, O: Ownership> NSMutableArray<T, O> {
262262
#[doc(alias = "removeObjectAtIndex:")]
263263
pub fn remove(&mut self, index: usize) -> Id<T, O> {
264264
let obj = if let Some(obj) = self.get(index) {
265-
unsafe { Id::retain(obj as *const T as *mut T).unwrap_unchecked() }
265+
unsafe { Id::retain_autoreleased(obj as *const T as *mut T).unwrap_unchecked() }
266266
} else {
267267
panic!("removal index should be < len");
268268
};

objc2-foundation/src/dictionary.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ impl<K: Message, V: Message> NSDictionary<K, V> {
103103
pub fn keys_array(&self) -> Id<NSArray<K, Shared>, Shared> {
104104
unsafe {
105105
let keys = msg_send![self, allKeys];
106-
Id::retain(keys).unwrap()
106+
Id::retain_autoreleased(keys).unwrap()
107107
}
108108
}
109109

@@ -130,7 +130,7 @@ impl<K: Message, V: Message> NSDictionary<K, V> {
130130
pub fn into_values_array(dict: Id<Self, Owned>) -> Id<NSArray<V, Owned>, Shared> {
131131
unsafe {
132132
let vals = msg_send![dict, allValues];
133-
Id::retain(vals).unwrap()
133+
Id::retain_autoreleased(vals).unwrap()
134134
}
135135
}
136136
}

objc2-foundation/src/enumerator.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ impl<'a, T: Message> NSEnumerator<'a, T> {
2323
/// ownership.
2424
pub unsafe fn from_ptr(ptr: *mut Object) -> Self {
2525
Self {
26-
id: unsafe { Id::retain(ptr) }.unwrap(),
26+
id: unsafe { Id::retain_autoreleased(ptr) }.unwrap(),
2727
item: PhantomData,
2828
}
2929
}

objc2-foundation/src/object.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ impl NSObject {
2424
unsafe {
2525
let result: *mut NSString = msg_send![self, description];
2626
// TODO: Verify that description always returns a non-null string
27-
Id::retain(result).unwrap()
27+
Id::retain_autoreleased(result).unwrap()
2828
}
2929
}
3030

objc2-foundation/src/process_info.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ impl NSProcessInfo {
2222
// currentThread is @property(strong), what does that mean?
2323
let obj: *mut Self = unsafe { msg_send![Self::class(), processInfo] };
2424
// TODO: Always available?
25-
unsafe { Id::retain(obj).unwrap() }
25+
unsafe { Id::retain_autoreleased(obj).unwrap() }
2626
}
2727

2828
pub fn process_name(&self) -> Id<NSString, Shared> {
2929
let obj: *mut NSString = unsafe { msg_send![Self::class(), processName] };
30-
unsafe { Id::retain(obj).unwrap() }
30+
unsafe { Id::retain_autoreleased(obj).unwrap() }
3131
}
3232
}

objc2-foundation/src/thread.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ impl NSThread {
2020
// TODO: currentThread is @property(strong), what does that mean?
2121
let obj: *mut Self = unsafe { msg_send![Self::class(), currentThread] };
2222
// TODO: Always available?
23-
unsafe { Id::retain(obj).unwrap() }
23+
unsafe { Id::retain_autoreleased(obj).unwrap() }
2424
}
2525

2626
/// Returns the [`NSThread`] object representing the main thread.
@@ -29,7 +29,7 @@ impl NSThread {
2929
let obj: *mut Self = unsafe { msg_send![Self::class(), mainThread] };
3030
// The main thread static may not have been initialized
3131
// This can at least fail in GNUStep!
32-
unsafe { Id::retain(obj).expect("Could not retrieve main thread.") }
32+
unsafe { Id::retain_autoreleased(obj).expect("Could not retrieve main thread.") }
3333
}
3434

3535
/// Returns `true` if the thread is the main thread.
@@ -41,7 +41,7 @@ impl NSThread {
4141
/// The name of the thread.
4242
pub fn name(&self) -> Option<Id<NSString, Shared>> {
4343
let obj: *mut NSString = unsafe { msg_send![self, name] };
44-
unsafe { Id::retain(obj) }
44+
unsafe { Id::retain_autoreleased(obj) }
4545
}
4646
}
4747

objc2/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
1313
* Added `Bool::as_bool` (more descriptive name than `Bool::is_true`).
1414
* Added convenience method `Id::as_ptr`.
1515
* The `objc2-encode` dependency is now exposed as `objc2::encode`.
16+
* Added `Id::retain_autoreleased` to allow following Cocoas memory management
17+
rules more efficiently.
1618

1719
### Changed
1820
* **BREAKING**: Changed signature of `Id::new` and `Id::retain` from

objc2/benches/autorelease.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
use core::ffi::c_void;
22
use std::mem::ManuallyDrop;
33

4-
use objc2::ffi;
54
use objc2::rc::{autoreleasepool, Id, Shared};
65
use objc2::runtime::{Class, Object, Sel};
76
use objc2::{class, msg_send, sel};
@@ -94,8 +93,7 @@ fn autoreleased_nsstring() -> *mut Object {
9493
}
9594

9695
fn retain_autoreleased(obj: *mut Object) -> Id<Object, Shared> {
97-
let obj = unsafe { ffi::objc_retainAutoreleasedReturnValue(obj.cast()) };
98-
unsafe { Id::new(obj.cast()).unwrap_unchecked() }
96+
unsafe { Id::retain_autoreleased(obj.cast()).unwrap_unchecked() }
9997
}
10098

10199
fn autoreleased_nsdata_pool_cleanup() -> *mut Object {
@@ -133,6 +131,7 @@ macro_rules! main_with_warmup {
133131
)+
134132
}
135133

134+
// Required to get DYLD to resolve the stubs on x86_64
136135
fn warmup() {
137136
$(
138137
warmup_fns::$f();

objc2/src/rc/id.rs

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,9 @@ impl<T: Message, O: Ownership> Id<T, O> {
210210
/// some API, and you would like to ensure that the object stays around
211211
/// so that you can work with it.
212212
///
213+
/// If said API is a normal Objective-C method, you probably want to use
214+
/// [`Id::retain_autoreleased`] instead.
215+
///
213216
/// This is rarely used to construct owned [`Id`]s, see [`Id::new`] for
214217
/// that.
215218
///
@@ -249,6 +252,122 @@ impl<T: Message, O: Ownership> Id<T, O> {
249252
unsafe { Self::new(res as *mut T) }
250253
}
251254

255+
/// Retains a previously autoreleased object pointer.
256+
///
257+
/// This is useful when calling Objective-C methods that return
258+
/// autoreleased objects, see [Cocoa's Memory Management Policy][mmRules].
259+
///
260+
/// This has exactly the same semantics as [`Id::retain`], except it can
261+
/// sometimes avoid putting the object into the autorelease pool, possibly
262+
/// yielding increased speed and reducing memory pressure.
263+
///
264+
/// Note: This relies heavily on being inlined right after [`msg_send!`],
265+
/// be careful not accidentally require instructions between these.
266+
///
267+
/// [mmRules]: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmRules.html
268+
///
269+
/// # Safety
270+
///
271+
/// Same as [`Id::retain`].
272+
#[doc(alias = "objc_retainAutoreleasedReturnValue")]
273+
#[inline(always)]
274+
pub unsafe fn retain_autoreleased(ptr: *mut T) -> Option<Id<T, O>> {
275+
// Add magic nop instruction to participate in the fast autorelease
276+
// scheme.
277+
//
278+
// See `callerAcceptsOptimizedReturn` in `objc-object.h`:
279+
// https://github.com/apple-oss-distributions/objc4/blob/objc4-838/runtime/objc-object.h#L1209-L1377
280+
//
281+
// We will unconditionally emit these instructions, even if they end
282+
// up being unused (for example because we're unlucky with inlining,
283+
// some other work is done between the objc_msgSend and this, or the
284+
// runtime version is too old to support it).
285+
//
286+
// It may seem like there should be a better way to do this, but
287+
// emitting raw assembly is exactly what Clang and Swift does:
288+
// swiftc: https://github.com/apple/swift/blob/swift-5.5.3-RELEASE/lib/IRGen/GenObjC.cpp#L148-L173
289+
// Clang: https://github.com/llvm/llvm-project/blob/889317d47b7f046cf0e68746da8f7f264582fb5b/clang/lib/CodeGen/CGObjC.cpp#L2339-L2373
290+
//
291+
// Resources:
292+
// - https://www.mikeash.com/pyblog/friday-qa-2011-09-30-automatic-reference-counting.html
293+
// - https://www.galloway.me.uk/2012/02/how-does-objc_retainautoreleasedreturnvalue-work/
294+
// - https://github.com/gfx-rs/metal-rs/issues/222
295+
// - https://news.ycombinator.com/item?id=29311736
296+
// - https://stackoverflow.com/a/23765612
297+
//
298+
// SAFETY:
299+
// Based on https://doc.rust-lang.org/stable/reference/inline-assembly.html#rules-for-inline-assembly
300+
//
301+
// We don't care about the value of the register (so it's okay to be
302+
// undefined), and its value is preserved.
303+
//
304+
// nomem: No reads or writes to memory are performed (this `mov`
305+
// operates entirely on registers).
306+
// preserves_flags: `mov` doesn't modify any flags.
307+
// nostack: We don't touch the stack.
308+
309+
// Only worth doing on the Apple runtime.
310+
// Not supported on TARGET_OS_WIN32.
311+
#[cfg(all(apple, not(target_os = "windows")))]
312+
{
313+
// Supported since macOS 10.7.
314+
#[cfg(target_arch = "x86_64")]
315+
{} // x86_64 looks at the next call instruction
316+
317+
// Supported since macOS 10.8.
318+
#[cfg(target_arch = "arm")]
319+
unsafe {
320+
core::arch::asm!("mov r7, r7", options(nomem, preserves_flags, nostack))
321+
};
322+
323+
// Supported since macOS 10.10.
324+
#[cfg(target_arch = "aarch64")]
325+
unsafe {
326+
core::arch::asm!("mov fp, fp", options(nomem, preserves_flags, nostack))
327+
};
328+
329+
// Supported since macOS 10.12.
330+
#[cfg(target_arch = "x86")]
331+
unsafe {
332+
core::arch::asm!("mov ebp, ebp", options(nomem, preserves_flags, nostack))
333+
};
334+
}
335+
336+
let ptr = ptr as *mut ffi::objc_object;
337+
338+
// SAFETY: Same as `retain`, `objc_retainAutoreleasedReturnValue` is
339+
// just an optimization.
340+
let res = unsafe { ffi::objc_retainAutoreleasedReturnValue(ptr) };
341+
342+
// Ideally, we'd be able to specify that the above call should never
343+
// be tail-call optimized (become a `jmp` instruction instead of a
344+
// `call`); Rust doesn't really have a way of doing this currently, so
345+
// we just emit a simple `nop` to make such tail-call optimizations
346+
// less likely to occur.
347+
//
348+
// This is brittle! We should find a better solution!
349+
#[cfg(all(apple, not(target_os = "windows"), target_arch = "x86_64"))]
350+
{
351+
// SAFETY: Similar to above.
352+
unsafe { core::arch::asm!("nop", options(nomem, preserves_flags, nostack)) };
353+
// TODO: Possibly more efficient alternative?
354+
// #![feature(asm_sym)]
355+
// core::arch::asm!(
356+
// "mov rdi, rax",
357+
// "call {}",
358+
// sym objc2::ffi::objc_retainAutoreleasedReturnValue,
359+
// inout("rax") obj,
360+
// clobber_abi("C"),
361+
// );
362+
}
363+
364+
debug_assert_eq!(
365+
res, ptr,
366+
"objc_retainAutoreleasedReturnValue did not return the same pointer"
367+
);
368+
unsafe { Self::new(res as *mut T) }
369+
}
370+
252371
#[inline]
253372
fn autorelease_inner(self) -> *mut T {
254373
// Note that this (and the actual `autorelease`) is not an associated
@@ -267,7 +386,6 @@ impl<T: Message, O: Ownership> Id<T, O> {
267386
res as *mut T
268387
}
269388

270-
// TODO: objc_retainAutoreleasedReturnValue
271389
// TODO: objc_autoreleaseReturnValue
272390
// TODO: objc_retainAutorelease
273391
// TODO: objc_retainAutoreleaseReturnValue

0 commit comments

Comments
 (0)