preface

We had a first look at TaggedPointer in the WWDC20-Runtime optimization. This article will use an interview question to deepen the understanding of it.

1. An interview question

What’s wrong with the following code

#import "ViewController.h"

@interface ViewController (a)

@property (nonatomic, strong) dispatch_queue_t  queue;
@property (nonatomic, strong) NSString *nameStr;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self taggedPointerDemo];

}

- (void)taggedPointerDemo {
  
    self.queue = dispatch_queue_create("Ryukie", DISPATCH_QUEUE_CONCURRENT);
    
    for (int i = 0; i<100000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"Ryukie"];
             NSLog(@"% @",self.nameStr); }); }} - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    for (int i = 0; i<100000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"RyukieRyukieRyukieRyukieRyukieRyukieRyukieRyukieRyukieRyukie"];
            NSLog(@"% @",self.nameStr);
        });
    }
}

@end
Copy the code

1.1 analysis

TaggedPointer (TaggedPointer, TaggedPointer) The difference between the two cases is simply the length of the string. And what we know about TaggedPointer is that Payload is used to store data. However, the length of the Payload is limited. If the Payload is too long, the data cannot be stored completely.

Therefore, the types of Pointers analyzed here are different.

1.2 Breakpoint Debugging

Shorter strings

Here we find a pointer of type NSTaggedPointerString. Obviously a TaggedPointer.

Longer strings

Here the pointer of type __NSCFString is an ordinary pointer.

1.3 the problem

A wild pointer crash occurred in the second code run.

1.4 thinking

Based on the phenomenon of the collapse, you might think. Do the Retain and Release of TaggedPointer differ from normal ones?

Second, source code analysis

Found in OC source:

- (id)retain {
    return _objc_rootRetain(self);
}
Copy the code

Further find the concrete implementation:

objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
{
    if (slowpath(isTaggedPointer())) return (id)this; . }Copy the code

From the Retain source, we can see that the return is a TaggedPointer without doing anything.

What about Release? Let’s explore the source code:

objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)
{
    if (slowpath(isTaggedPointer())) return false; . }Copy the code

Find that this is the same as Retain.

Third, topic analysis

With a look at the Retain and Release pairs and TaggedPointer, it’s easy to understand what happened in the interview question above

  • TaggedPointerThere is no Retain Release for the shorter string, so there is no wild pointer error in the multi-thread asynchronous read and write process
  • nonTaggedPointerA long string of string, due to multi-thread asynchronous read and write operation, continuous Retain Release, so there may be a wild pointer error.

summary

  • TaggedPointerIs for small objects of a specific type, not all of those typesTaggedPointer. The premise isPayloadIt has to fit.
  • Retain ReleaseTaggedPointerThe special handling has also been improvedTaggedPointerAt the same time, the security is relatively high in multi-threaded scenarios.

So far we’ve seen three Pointers: TaggedPointer, Nonpointer ISA, and pure ISA. Let’s dig deeper into the Retain Release source code to see how they differ in memory management.

Four, Retain

objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
{
    // Return without TaggedPointer
    if (slowpath(isTaggedPointer())) return (id)this; .do {
        transcribeToSideTable = false;
        newisa = oldisa;
        if (slowpath(! newisa.nonpointer)) {// Non-nonpointer is handled by sidetable
            ClearExclusive(&isa.bits);
            if (tryRetain) return sidetable_tryRetain()? (id)this : nil;
            else return sidetable_retain(sideTableLocked); }...uintptr_t carry; // flags whether the extra_rc capacity is still bearable
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++

        if (slowpath(carry)) {
            // Exceeds extra_rc capacity
            // newisa.extra_rc++ overflowed
            if(variant ! = RRVariant::Full) {ClearExclusive(&isa.bits);
                return rootRetain_overflow(tryRetain);
            }
            // Keep the general reference count and copy the other half to the side table
            // Leave half of the retain counts inline and 
            // prepare to copy the other half to the side table.
            if(! tryRetain && ! sideTableLocked)sidetable_lock(a); sideTableLocked =true;
            transcribeToSideTable = true;
            newisa.extra_rc = RC_HALF;
            newisa.has_sidetable_rc = true; }}while (slowpath(!StoreExclusive(&isa.bits, &oldisa.bits, newisa.bits)));

    if (variant == RRVariant::Full) {
        if (slowpath(transcribeToSideTable)) {
            // Copy the other half of the retain counts to the side table.
            sidetable_addExtraRC_nolock(RC_HALF);
        }

        if (slowpath(! tryRetain && sideTableLocked))sidetable_unlock(a); }else {
        ASSERT(! transcribeToSideTable);ASSERT(! sideTableLocked); }return (id)this;
}
Copy the code

summary

  • TaggedPointerReturn without processing
  • Nonpointer isa
    • BITFIELDSideTableUse a combination of
      • whenBITFIELDextra_rcContinue ++ when the capacity is sufficient
      • whenBITFIELDextra_rcWhen there is not enough capacity
        • extra_rcCut it in half. Copy it in halfSideTablemanage
        • Simultaneous flag bithas_sidetable_rcchange
  • Pure isa
    • throughSideTablemanagement

Five, the Release

objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)
{
    // Return without TaggedPointer
    if (slowpath(isTaggedPointer())) return false; . retry:do {
        newisa = oldisa;
        if (slowpath(! newisa.nonpointer)) {// Non-nonpointer is handled by sidetable
            ClearExclusive(&isa.bits);
            return sidetable_release(sideTableLocked, performDealloc); }...// don't check newisa.fast_rr; we already called any RR overrides
        uintptr_t carry;
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
        if (slowpath(carry)) {
            // don't ClearExclusive()
            // extra_rc bottom jump executes underflow
            gotounderflow; }}while (slowpath(!StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits)));

    if (slowpath(newisa.isDeallocating()))
        gotodeallocate; .return false;

 underflow:
    // When extra_rc hits bottom, retrieve the reference count from SideTable or destroy it
    // newisa.extra_rc-- underflowed: borrow from side table or deallocate

    // abandon newisa to undo the decrement
    newisa = oldisa;

    if (slowpath(newisa.has_sidetable_rc)) {
        // If SideTable is used
        if(variant ! = RRVariant::Full) {ClearExclusive(&isa.bits);
            return rootRelease_underflow(performDealloc);
        }

        // Transfer retain count from side table to inline storage.
        
        if(! sideTableLocked) {ClearExclusive(&isa.bits);
            sidetable_lock(a); sideTableLocked =true;
            // Need to start over to avoid a race against 
            // the nonpointer -> raw pointer transition.
            oldisa = LoadExclusive(&isa.bits);
            goto retry;
        }

        // Remove the reference count from SideTable
        // Try to remove some retain counts from the side table.        
        auto borrow = sidetable_subExtraRC_nolock(RC_HALF);

        bool emptySideTable = borrow.remaining == 0; // we'll clear the side table if no refcounts remain there

        if (borrow.borrowed > 0) {
            // Side table retain count decreased.
            // Try to add them to the inline count.
            bool didTransitionToDeallocating = false;
            // Change the extra_rc and flag bits
            newisa.extra_rc = borrow.borrowed - 1;  // redo the original decrement toonewisa.has_sidetable_rc = ! emptySideTable; . }else {
            // Side table is empty after all. Fall-through to the dealloc path.
        }
    }

deallocate:
    // Really deallocate.
    / / destroy.if (performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(thisThe @selector(dealloc));
    }
    return true;
}
Copy the code

summary

  • TaggedPointerReturn without processing
  • Nonpointer isa
    • BITFIELDSideTableUse a combination of
      • whenBITFIELDextra_rcContinued when capacity has not reached bottom —
      • whenBITFIELDextra_rcWhen capacity hits bottom
        • Check flag bithas_sidetable_rc
          • Using theSideTable
            • willSideTableThe reference count inBITFIELDextra_rcFor processing
          • Don’t useSideTable
            • Need to destroy
  • Pure isa
    • throughSideTablemanagement

conclusion

An exploration of the Retain and Release source code shows that using TaggedPointer and Nonpointer ISA is more efficient in memory management. Once SideTable is involved, the process becomes more complex and needs to be locked and unlocked, affecting efficiency. Although this is not often the case in the development process, understanding these issues and phenomena makes it easier to see the nature of the problem.

reference

Object nature inquiry and ISA

Memory management