Skip to content
This repository

Supporting Automatic Reference Counting (ARC) #37

Closed
scelis opened this Issue July 08, 2011 · 23 comments
Sebastian Celis
scelis commented July 08, 2011

I would like to discuss your future plans for supporting Automatic Reference Counting (ARC). I see in commit c2146ff that you added a section to the README in which you state:

JSONKit is not designed to be used with Objective-C Automatic Reference Counting (ARC). The behavior of JSONKit when compiled with -fobjc-arc is undefined. The behavior of JSONKit compiled without ARC mixed with code that has been compiled with ARC is normatively undefined since at this time no analysis has been done to understand if this configuration is safe to use. At this time, there are no plans to support ARC in JSONKit. Although tenative, it is extremely unlikely that ARC will ever be supported, for many of the same reasons that Mac OS X Garbage Collection is not supported.

Is this the long-term plan? What are your reasons for not supporting ARC?

Note: Edited to remove unsubstantiated claims about ARC, as well as a dumb suggestion for creating an ARC branch of the code. No one wants to maintain two branches for something like this.

Tyler Stromberg

Xcode seems to do an OK job for simple projects. I imagine this one is a little more complicated than the migrator is designed to handle.

That being said, there's nothing stopping anybody from forking and adding ARC support themselves...

Jonathan Sterling

Yeah… I may fork this at some point and see if I can get ARC working nicely. At my work, we're using JSONKit, and we're also anticipating using ARC for future projects.

Edit: @AquaGeek, just saw you'd forked this with similar intentions… :)

jamesprinc3

XCode doesn't even attempt to refactor JSONKit to be ARC compliant :(

Tyler Stromberg

Yeah, I noticed that as well. There's a lot going on with toll-free bridging with CF objects that the migrator doesn't support without some modifications. I still plan on looking into this more, but it's not going to be an easy fix.

Sam Soffes

I know @johnezang is very against ARC and can talk at great length about it :)

I think a simple solution is making JSONKit a static library and then you can use it with ARC code or non-ARC code since you can mix and match between targets.

Benjamin Ragheb

This README sentence concerns me:

Although tenative, it is extremely unlikely that ARC will ever be supported, for many of the same reasons that Mac OS X Garbage Collection is not supported.

No specific reasons for unsupporting GC are mentioned, so it's hard to know what the reasoning is here. However, I fear there is a misconception about how ARC works.

GC is a run-time technology, and results in a notable performance trade-off, as the garbage collector must intermittently pause execution to harvest orphaned objects. Since JSONKit is a performance-focused library, it makes sense to forgo support of GC.

ARC is a compile-time technology, which actually results in a notable performance benefit. The compiler generates calls to retain/release/autorelease on your behalf, instead of you inserting them manually. Nothing new happens at run-time, which is why ARC can be enabled/disabled on a file-by-file basis when compiling an Xcode project.

JSONKit makes extensive use of Core Foundation classes like CFStringRef; when bridging these to NSString variables some care needs to be taken, but it is not impossible. The code can continue to use CFStringRef.

While I can understand avoiding support for GC, I don't think there is a good reason to avoid support for ARC. If there is one, the README file needs clarification.

Sean Roehnelt

It was quick to make it happily build as a static library. I now have it working as a build dependency by dragging the project file into my Xcode project and adding it's product to my Target Dependencies (Build Phases tab), and adding the build product also to 'Link Binary with Libraries'. Is not arc, but it still is fast.

... all that said, bump benzado's comment above mine. I can't discount your your experience or arguments against ARC ... primarily because you don't give us any idea of what they might be. It's possible you have overlooked something, or we could learn something from your experience. Thank you.

Florent Morin

In fact, I think there's another solution.

With ARC, following official documentation from LLVM, you can use

#ifdef __has_feature(objc_arc)

#end

Gwendal Roué

My two cents : I chose to ship my own https://github.com/groue/GRMustache as a static library. ARC and non-ARC users are happy, and I have not spent a second explaining why the source code is still non-ARC.

Tip for @johnezang : the only drawback was that I had to write a custom Makefile, in order to build a single library for several architectures.

Cédric Luthi

Or you could use iOS Universal Framework which takes care of building for all architectures and packages a nice static framework instead of a static library plus separated header(s).

Florent Morin

With Xcode 4.2, you can specific compiler flags per file.
You can use "-fno-objc-arc" only for JSONKit.

Steps:

  • select project
  • select target
  • open "Build Phases" tab
  • open "Compile Sources (nn items)"
  • on second column, you can specify "Compiler Flags"
  • set to "-fno-objc-arc" for each file you want.
Sean Roehnelt

Yes, "-fno-objc-arc" works fine when you need a one off class in an ARC project (and don't have time or don't want to convert a specific class(es)... but remember that ARC is still running in a reference counted environment so a well written ref counted library is just as good as an ARC library (yes, potentially an ARC version could be a little faster and/or free memory sooner which could reduce max memory requirements)...

bitfool

thanks @florentmorin, that works a charm for now.

johnezang
Owner

In case anyone is wondering.... the short answer is no, ARC will not be supported in JSONKit.

The solution is to use the per-file -fno-objc-arc option hackery. Works just fine.

The longer answer is... too long for github.com issue post.. :) But seriously, anyone suggesting that ARC is the best f'ing thing since slice bread and, so help me, better than the invention of the symbolic assembler (no more translating your program by hand in to a bunch of random numbers that you then had to enter in to the machine by flipping individual dip switches... effective bits per second for loading a program this way: < 1 (one) bps..)... you all are just going to stop drinking all that Kool-Aide and come down off that high.

Anyone who want's to suggest that ARC is somehow faster or... whatever... better have some honest to god hard core numbers to back it up... numbers that I can replicate myself. Because any claim without the numbers to back it up means you're just making sh*t up.

I apologize for being so blunt, but... it's quite clear that a lot of people have not given ARC a lot of critical thought. The fact the official sales pitch (er, documentation) is completely devoid of actual numbers and is pretty vague on the specifics should be a warning sign. The other big warning sign is the fact that you have never had this thought cross your mind: "Man, I'm totally bottlenecked and limited by retain / release overhead.." Because the fact is even if retain / release represents 10% of your programs execution time.... magically (and generously) cutting that in half with ARC is only going to net you about a 5% overall increase (all things being equal and the usual disclaimers).

Jock Murphy

"Premature optimization is the root of all evil" -- Knuth (and/or Hoare)

The funny thing is I have yet to meet a single person who believes the primary reason for GC or ARC is performance. I can't imagine anyone would. The reason for them is to remove one of the most common sources of memory leaks and programmer error. Except for a handful of limited cases the compiler and/or runtime environment can do the job of managing memory as good (or better than) the engineer.

And I for one would be willing to take a pretty sever performance hit for them. Because even a 100% performance hit on memory allocation and deallocation is utterly trivial to everything else going on in the system.

Indeed I challenge anyone to give real world numbers that would so that ARC would result in a serious performance loss.

It's your library, you can do what you want with it. But I think you are wrong about your reasons.

Tyler

Just watched the 2011 WWDC video on ARC and from that it is pretty clear there are cases where ARC and the related compiler optimizations will result in faster code, but there are also cases where it might slow things down.

The ReadMe for JSONKit should be updated to reflect that -fno-objc-arc works, if it in fact does. Because at the moment it says:

The behavior of JSONKit compiled without ARC mixed with code that has been
compiled with ARC is normatively undefined since at this time no analysis has
been done to understand if this configuration is safe to use

and yet in the comment above @johnezang says:

The solution is to use the per-file -fno-objc-arc option hackery. Works just fine.

Leaves one wondering, "which is it? 'Works just fine' or is 'normatively undefined'".

johnezang
Owner

@haikusw,

Leaves one wondering, "which is it? 'Works just fine' or is 'normatively undefined'".

Which one is it? Both.

ARC is "supposed" to interoperate just fine with non-ARC code, but there is virtually no documentation about the technical details of "how" ARC works that would allow me to say with confidence that there wont be any issues.

On the other hand, you can compile JSONKit using the -fno-objc-arc compiler option and it should "work just fine". I'm aware of a few projects that have switched to ARC and continued to use JSONKit (compiled with -fno-objc-arc) in those projects. To date, I have yet to receive or hear about any -fno-objc-arc related problems- but an absence of problem reports does not necessarily mean that there aren't any problems.

: Caveat Emptor, YMMV, etc.

Why is it important to make a distinction between what's officially and unofficially supported? Because JSONKit isn't exactly your run of the mill Objective-C source code, which means it's much more likely to trip over corner cases in the way that ARC "magically" handles reference counting for you behind the scenes.

The "official" ARC documentation essentially amounts to "ARC just works! And you should use it!" But when I start to dig in to the details of how all this "magical" automatic reference counting works behind the scenes, I see a lot of things that really concern me. For example, the function callerAcceptsFastAutorelease() in objc-arr.mm has the following comment:

objc_autoreleaseReturnValue() examines the caller's instructions following
the return. If the caller's instructions immediately call
objc_autoreleaseReturnValue, then the callee omits the -autorelease and saves
the result in thread-local storage. If the caller does not look like it
cooperates, then the callee calls -autorelease as usual.

... which just leaves me speechless. I would have loved to sit in on the discussions where the compiler guys signed off on this... "Yea, you need a hard guarantee that the compiler will emit precisely this sequence of bytes from this point forward, no matter what? No f'ing problem. We'll just stick a comment somewhere that all future modifications to the compiler and optimization passes have to make sure they never violate this insignificant little detail."

Then there's tiny little details like this and this (from the clang / llvm website)-

The optimizer assumes that when a new value enters local control, e.g. from a load of a non-local object or as the result of a function call, it is instaneously valid. Subsequently, a retain and release of a value are necessary on a computation path only if there is a use of that value before the release and after any operation which might cause a release of the value (including indirectly or non-locally), and only if the value is not demonstrably already retained.

The complete optimization rules are quite complicated, but it would still be useful to document them here.

Naturally the incredibly important "useful" optimization rules are no where to be found.

It is undefined behavior if a computation history featuring a send of retain followed by a send of release to the same object, with no intervening release on that object, is not equivalent under the high-level semantics to a computation history in which these sends are removed. Note that this implies that these methods may not raise exceptions.

Unfortunately, while the above is true for the majority of cases, it's that 0.0001% of the time where it's actually pretty f'ing important that a retain and (auto)release take place exactly as you wrote them-

NSSet objectToReturn = nil;

[lock lock];
objectToReturn = [[arrayProtectedByMutexBarrier objectAtIndex:0] retain];
[lock unlock];

return([objectToReturn autorelease]);
johnezang johnezang closed this March 23, 2012
Christopher Bowns

@haikusw “Works just fine” in combination with that compiler flag should be construed as “works just fine”: the compiler is treating the file with normal retain-release gloves, so the emitted assembly will (or should) be exactly the same as compiling JSONKit in a non-ARC project.

Tyler

Wow, my comment was the one that made @johnezang close this issue - I feel honored. :) Nice reply from @johnezang though (thanks) with some excellent specifics to feed nightmares. Fodder for a few Radars I suppose. After a quarter century of programming I have to admit to a level of distrust of "magic! it just works!" nearly as high as yours johnezang. :-P

That said, I found WWDC 2011 session 322 "Objective-C Advancements In Depth" to be quite informative about ARC, especially the second half "ARC Internals" by Greg Parker. Has a lot of illustrations of what exactly the compiler inserts into your code and then what the optimizer does to that code. Still clearly a lot of "magic" going on and I predict that at some point I'm going to be chasing bugs down in the "magic" and cursing ARC when that happens…

Gwendal Roué
groue commented May 16, 2012

@johnezang, I'm curious : could you please explain why the retain/autorelease dance is so important in the lock/unlock example?

johnezang
Owner

Well, it has a lot to do with what the following means, for some strong, hyper pedantic definition of means. I'm going to make one change to the previous example- Instead of using [arrayProtectedByMutexBarrier objectAtIndex:0] as the object to be retained and release, I'm going to use ivarProtectedByMutexBarrier, which for simplicity is nothing more than an ivar defined in the examples subclass and is of type id. So we have the following:

NSSet *objectToReturn = nil;

[lock lock];
objectToReturn = [ivarProtectedByMutexBarrier retain];
[lock unlock];

return([objectToReturn autorelease]);

The question is: What, exactly, does the above mean? And because there is no Objective-C standard (and by standard I mean something like the ANSI C99 standard, not the informal prose description of the language from Apple), it's actually pretty difficult to say with any authority what the above means.

And you know what, it's actually kind of important that you be able to say exactly what it means. Like, really important. So, for the purposes of our discussion, I'm going to define Objective-C as "A strict superset extension to the ANSI C99 grammar, where the Objective-C extensions are defined in terms of their equivalent "source to source" ANSI C99 translation." This vastly simplifies things- under this definition, Objective-C is not just a "strict superset of C", it literally is C, the Objective-C extensions are pure syntactic sugar that are there to make your job easier. It also helps that this was the de facto case for nearly 20 years (although in practice, the "source to source" preprocessor was rolled up into gcc and generated the equivalent C directly in gccs intermediate representation).

Some might take exception to this definition, but without a formal standard... oh wait, that fact that it's even possible to have a serious argument over what the definition of Objective-C is means... I'm right, so there. :) Moving on, using The One True Definition...

In ANSI C99, the example above would become something like:

NSSet *objectToReturn = NULL;

objc_msgSend(self->lock, sel_registerName("lock"));
objectToReturn = objc_msgSend(self->ivarProtectedByMutexBarrier, sel_registerName("retain"));
objc_msgSend(self->lock, sel_registerName("unlock"));

return(objc_msgSend(objectToReturn, sel_registerName("autorelease")));

... and Objective-C classes (i.e., NSSet above) are (very roughly and informally) defined as a C struct, similar to:

typedef struct {
  // List of ivars, built by appending the ivars from each subclass.
} NSSet; // The ObjC class name...

Again, it's pure syntactic sugar, and until recently, was how things actually worked (witness the now deprecated @defs directive).

The C99 standard has quite a lot to say about the above. What tends to catch even experienced C programmers off guard is that the C standard does not require that the statements above by executed in the linear sequence implied by the example. In fact, the C standard doesn't even require any of the statements above to literally be executed. From the C99 standard (section 5.1.2.3, clause 3, 5, and 9):

5.1.2.3 Program execution

3) In the abstract machine, all expressions are evaluated as specified by the semantics. An actual implementation need not evaluate part of an expression if it can deduce that its value is not used and that no needed side effects are produced (including any caused by calling a function or accessing a volatile object).

5) The least requirements on a conforming implementation are:

  • At sequence points, volatile objects are stable in the sense that previous accesses are complete and subsequent accesses have not yet occurred.
  • At program termination, all data written into files shall be identical to the result that execution of the program according to the abstract semantics would have produced.
  • The input and output dynamics of interactive devices shall take place as specified in 7.19.3. The intent of these requirements is that unbuffered or line-buffered output appear as soon as possible, to ensure that prompting messages actually appear prior to a program waiting for input.

9) Alternatively, an implementation might perform various optimizations within each translation unit, such that the actual semantics would agree with the abstract semantics only when making function calls across translation unit boundaries. In such an implementation, at the time of each function entry and function return where the calling function and the called function are in different translation units, the values of all externally linked objects and of all objects accessible via pointers therein would agree with the abstract semantics. Furthermore, at the time of each such function entry the values of the parameters of the called function and of all objects accessible via pointers therein would agree with the abstract semantics. In this type of implementation, objects referred to by interrupt service routines activated by the signal function would require explicit specification of volatile storage, as well as other implementation-defined restrictions.

... in other words, when you flip the optimizer on, it has a lot of freedom as to what it can and can't do, and the C99 standard says it can do a lot of things that you would intuitively think it couldn't. The golden rule, though, is that the optimized code must behave "as-if" it was executed according to the semantics of the C99 abstract machine. In practice most compilers will treat function calls that cross translation units as described in clause 9 above, but this is not strictly required by the standard.

So what happens when we "get rid of" retain and autorelease? Lets take a look:

NSSet *objectToReturn = NULL;

objc_msgSend(self->lock, sel_registerName("lock"));
objectToReturn = self->ivarProtectedByMutexBarrier;
objc_msgSend(self->lock, sel_registerName("unlock"));

return(objectToReturn);

What happens when you turn on the optimizer? Well, it depends on the compiler, but there is a very real possibility that it's going to turn the above in to something like the code below due to constant propagation:

objc_msgSend(self->lock, sel_registerName("lock"));
objc_msgSend(self->lock, sel_registerName("unlock"));

return(self->ivarProtectedByMutexBarrier);

... which, when you think about it, looks disturbingly like how you'd be forced to write code using ARC:

NSSet *objectToReturn = nil;

[lock lock];
objectToReturn = ivarProtectedByMutexBarrier;
[lock unlock];

return(objectToReturn);

... which the optimizer might just be silently turning in to:

[lock lock];
[lock unlock];

return(ivarProtectedByMutexBarrier);

... which is also known as a Race Condition, and are, by far, the most difficult bugs I have had to deal with.

Think this can't happen? Let's take a look at the Objective-C Garbage Collection implementation... The core primitive, which absolutely everything depends on, made it all the way through testing, shipped in 10.5, and lasted all the way until 10.5.2 before it was caught. I give you objc_assign_strongCast (from objc4-371)-

__private_extern__ id objc_assign_strongCast_gc(id value, id *slot) 
{
    objc_strongCast_write_barrier(value, slot);
    return (*slot = value);
}

That little gem was fixed in objc4-371.1 (along with a number of other GC race condition bugs that were so bad that it was amazing things even ran).

That little GC bastard above consumed three solid weeks of my life, where I spent ten hours a day trying to figure out why that which could not happen by definition was actually happening.

Probably the easiest way to tell if you're getting screwed by ARC behind your back is-

  1. You spend 90% of your time developing in Debug, which doesn't have the optimizer turned on.
  2. When you switch to Release, there is a sudden increase in completely random, totally unreproducible crashing bugs that absolutely mystifies everyone.

Some indications that there are some fundamental problems:

#import <Foundation/Foundation.h>

int main(int argc, char *argv[]) {
  @autoreleasepool {
    void     *ptr    = NULL;
    NSString *string = NULL;

    if(string == NULL)        { abort(); } // Line 8
    if(string == (void *)0)   { abort(); } // Line 9
    if(string == (void *)1-1) { abort(); } // Line 10

    if(string == ptr)         { abort(); } // Line 12
  }

  return(0);
}

When compiled:

x.m:10:18: error: implicit conversion of C pointer type 'void *' to Objective-C pointer type 'NSString *' requires a bridged cast
    if(string == (void *)1-1) { abort(); } // Line 10
                 ^~~~~~~~~~~
x.m:10:18: note: use __bridge to convert directly (no change in ownership)
x.m:10:18: note: use __bridge_transfer to transfer ownership of a +1 'void *' into ARC
x.m:12:18: error: implicit conversion of C pointer type 'void *' to Objective-C pointer type 'NSString *' requires a bridged cast
    if(string == ptr)         { abort(); } // Line 12
                 ^~~
x.m:12:18: note: use __bridge to convert directly (no change in ownership)
x.m:12:18: note: use __bridge_transfer to transfer ownership of a +1 'void *' into ARC
2 errors generated.

Lines 8, 9, and 10 are required, by definition, to be equivalent (see page 748). The C99 standard also requires line 12, which compares the value of two pointer variables that have been assigned NULL to "compare equal". To me at least, the fact that the compiler is barfing on these kind of nit pick details means that no one really thought through any of the finer points and corner cases when they "added" ARC to Objective-C.

Benjamin Ragheb
benzado commented May 17, 2012

@johnezang Regarding your last point, that "no one really thought through any of the finer points and corner cases" --- the C99 standard has rules about "C pointers" but doesn't cover "Objective-C pointers". It's right there in the compiler error message:

x.m:10:18: error: implicit conversion of C pointer type 'void *' to Objective-C pointer type 'NSString *' requires a bridged cast

ARC effectively introduces a new type, with new rules, in order to make the behavior of ARC predictable. It's much more than just magically inserting retain/release calls in C code. It's a vast improvement over non-deterministic garbage-collection for precisely this reason.

I don't care if you want to continue using retain/release in JSONKit. Since it's a per-file compilation option, it really doesn't matter. You've spent a long time fine-tuning the code for performance, and switching it to use ARC would be a substantial change. I am personally curious to see how it would impact performance, but it is by no means clear right now whether it would be worth the significant investment of time. And you're right, it increases the potential to introduce subtle, infuriating bugs.

But, don't go around accusing other engineers who work just as hard and care just as much as you do of being careless, just because you don't know them personally. That's just really lousy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.