Skip to content

Choose the autorelease pool implementation once - #410

Open
DTW-Thalion wants to merge 2 commits into
gnustep:masterfrom
DTW-Thalion:perf/autorelease-pool-choice
Open

Choose the autorelease pool implementation once#410
DTW-Thalion wants to merge 2 commits into
gnustep:masterfrom
DTW-Thalion:perf/autorelease-pool-choice

Conversation

@DTW-Thalion

Copy link
Copy Markdown
Contributor

initAutorelease guarded its work with a test of AutoreleasePool, which stays Nil in a program that has no NSAutoreleasePool. The search was therefore repeated on every objc_autoreleasePoolPush and on every objc_autorelease of a fast ARC object, and objc_getClass hashes a string and walks the class table.

Callgrind over a loop of push and pop with no Foundation gives 516 instructions per pair, 65 percent of them in that lookup. Timed inside the process, ns per push and pop pair: 27.53 against 5.17 with no Foundation, and 5.17 either way with it, where the class is found on the first call and the guard closes. The suite passes, 198 of 198.

The implementation is now chosen once. Before this, a program that pushed an ARC pool and then loaded Foundation would switch, and pop through NSAutoreleasePool a pool the ARC path had pushed. Draft because that is a change in behaviour, and the answer may instead be that the late switch is worth keeping and only the lookup should be cached.

initAutorelease guarded its work with a test of AutoreleasePool, which stays Nil in a program that has no NSAutoreleasePool. The search was therefore repeated on every objc_autoreleasePoolPush and on every objc_autorelease of a fast ARC object, and objc_getClass hashes a string and walks the class table.

Callgrind over a loop of push and pop in a program with no Foundation gives 516 instructions per pair, 65 percent of them in that lookup. Timed inside the process, ns per push and pop pair: 27.53 against 5.17 with no Foundation, and 5.17 either way with it, where the class is found on the first call and the guard closes.

The implementation is chosen once rather than on the first call that finds the class. A pool is pushed and popped through whichever implementation is in force, so a program that pushed an ARC pool and then loaded Foundation would pop through NSAutoreleasePool a pool the ARC path had pushed.

The suite passes, 198 of 198.
Comment thread arc.mm
Comment on lines +558 to +559
static BOOL chosen;
if (!chosen)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think I'd expect this to be faster. Checking a boolean and checking against nil compile to the same instruction on most architectures.

You might have some luck sticking a __builtin_expect here to make sure that the compiler knows that this is a slow path.

@DTW-Thalion DTW-Thalion Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right that the two guards compile to the same thing. Master is cmpq $0, AutoreleasePool / je / ret, this is cmpb $0, chosen / jne / ret.

The difference is not the compare, it is that master's guard never closes in a program with no NSAutoreleasePool. AutoreleasePool stays Nil, so every push and every autorelease of a fast ARC object falls through and calls objc_getClass again.

Callgrind over push and pop with no Foundation: 507.5 instructions per pair on master, 70 percent of the program's instructions in objc_getClass and the class table lookup under it.

With the lookup done once, 142.6 per pair. Wall clock 27.8 ns against 5.16.

I built the __builtin_expect version. It executes the same instruction count, 1.0151 billion either way for the same run, differing by about 20 in a billion, and 27.2 ns against 27.8 where two control runs of master in the same rounds gave 27.4 and 27.5. It moves the cold block, but the call is still on the path that is always taken.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was intentional, because it’s possible for this code path to be hit the first time before NSAutoreleasePool has been loaded. It’s less likely with the v2 ABI, but happened in some +load methods with the v1 ABI.

What is the use case for running without an NSAutoreleasePool implementation after the program has loaded?

We could move this initialisation into the class loader: when you load a new class, see if it’s NSAutoreleasePool and set this variable if so?

@DTW-Thalion
DTW-Thalion marked this pull request as ready for review August 13, 2026 18:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants