[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
Re: libobjc2 & arc & multithreading
From: |
Andreas Fink |
Subject: |
Re: libobjc2 & arc & multithreading |
Date: |
Mon, 27 Nov 2017 23:38:19 +0100 |
I will give it a throughout test the coming days...
> On 27 Nov 2017, at 18:33, David Chisnall <theraven@sucs.org> wrote:
>
> On 27 Nov 2017, at 09:03, David Chisnall <theraven@sucs.org> wrote:
>>
>> The other thing that would probably make a bigger difference is to use a bit
>> in the refcount to indicate whether an object has any weak references at
>> all. This would allow us to completely avoid having the lock.
>
> The attached patch should improve things a lot for you. Please can you test
> it and report back? I’m a bit hesitant to commit it without feedback,
> because there aren’t any multithreaded tests in the libobjc2 test suite and
> it’s possible that I’ve managed to introduce a subtle race condition.
>
> David
>
> diff --git a/arc.m b/arc.m
> index b7a3b3b..24f8b7c 100644
> --- a/arc.m
> +++ b/arc.m
> @@ -1,4 +1,5 @@
> #include <stdlib.h>
> +#include <stdbool.h>
> #include <assert.h>
> #import "stdio.h"
> #import "objc/runtime.h"
> @@ -163,6 +164,17 @@ extern BOOL FastARCAutorelease;
>
> static BOOL useARCAutoreleasePool;
>
> +/**
> + * We use the top bit of the reference count to indicate whether an object
> has
> + * ever had a weak reference taken. This lets us avoid acquiring the weak
> + * table lock for most objects on deallocation.
> + */
> +static const long weak_mask = ((size_t)1)<<((sizeof(size_t)*8)-1);
> +/**
> + * All of the bits other than the top bit are the real reference count.
> + */
> +static const long refcount_mask = ~weak_mask;
> +
> static inline id retain(id obj)
> {
> if (isSmallObject(obj)) { return obj; }
> @@ -174,14 +186,40 @@ static inline id retain(id obj)
> }
> if (objc_test_class_flag(cls, objc_class_flag_fast_arc))
> {
> - intptr_t *refCount = ((intptr_t*)obj) - 1;
> - // Note: this should be an atomic read, so that a sufficiently
> clever
> - // compiler doesn't notice that there's no happens-before
> relationship
> - // here.
> - if (*refCount >= 0)
> - {
> - __sync_add_and_fetch(refCount, 1);
> - }
> + uintptr_t *refCount = ((uintptr_t*)obj) - 1;
> + uintptr_t refCountVal = __sync_fetch_and_add(refCount, 0);
> + uintptr_t newVal = refCountVal;
> + do {
> + refCountVal = newVal;
> + long realCount = refCountVal & refcount_mask;
> + // If this object's reference count is already less
> than 0, then
> + // this is a spurious retain. This can happen when one
> thread is
> + // attempting to acquire a strong reference from a weak
> reference
> + // and the other thread is attempting to destroy it.
> The
> + // deallocating thread will decrement the reference
> count with no
> + // locks held and will then acquire the weak ref table
> lock and
> + // attempt to zero the weak references. The caller of
> this will be
> + // `objc_loadWeakRetained`, which will also hold the
> lock. If the
> + // serialisation is such that the locked retain happens
> after the
> + // decrement, then we return nil here so that the
> weak-to-strong
> + // transition doesn't happen and the object is actually
> destroyed.
> + // If the serialisation happens the other way, then the
> locked
> + // check of the reference count will happen after we've
> referenced
> + // this and we don't zero the references or deallocate.
> + if (realCount < 0)
> + {
> + return nil;
> + }
> + // If the reference count is saturated, don't increment
> it.
> + if (realCount == refcount_mask)
> + {
> + return obj;
> + }
> + realCount++;
> + realCount |= refCountVal & weak_mask;
> + uintptr_t updated = (uintptr_t)realCount;
> + newVal = __sync_val_compare_and_swap(refCount,
> refCountVal, updated);
> + } while (newVal != refCountVal);
> return obj;
> }
> return [obj retain];
> @@ -203,12 +241,37 @@ static inline void release(id obj)
> }
> if (objc_test_class_flag(cls, objc_class_flag_fast_arc))
> {
> - intptr_t *refCount = ((intptr_t*)obj) - 1;
> + uintptr_t *refCount = ((uintptr_t*)obj) - 1;
> + uintptr_t refCountVal = __sync_fetch_and_add(refCount, 0);
> + uintptr_t newVal = refCountVal;
> + bool isWeak;
> + bool shouldFree;
> + do {
> + refCountVal = newVal;
> + size_t realCount = refCountVal & refcount_mask;
> + // If the reference count is saturated, don't decrement
> it.
> + if (realCount == refcount_mask)
> + {
> + return;
> + }
> + realCount--;
> + isWeak = (refCountVal & weak_mask) == weak_mask;
> + shouldFree = realCount == -1;
> + realCount |= refCountVal & weak_mask;
> + uintptr_t updated = (uintptr_t)realCount;
> + newVal = __sync_val_compare_and_swap(refCount,
> refCountVal, updated);
> + } while (newVal != refCountVal);
> // We allow refcounts to run into the negative, but should only
> // deallocate once.
> - if (__sync_sub_and_fetch(refCount, 1) == -1)
> + if (shouldFree)
> {
> - objc_delete_weak_refs(obj);
> + if (isWeak)
> + {
> + if (!objc_delete_weak_refs(obj))
> + {
> + return;
> + }
> + }
> [obj dealloc];
> }
> return;
> @@ -524,6 +587,7 @@ void* block_load_weak(void *block);
>
> id objc_storeWeak(id *addr, id obj)
> {
> + LOCK_FOR_SCOPE(&weakRefLock);
> id old = *addr;
>
> BOOL isGlobalObject = (obj == nil) || isSmallObject(obj);
> @@ -538,16 +602,44 @@ id objc_storeWeak(id *addr, id obj)
> isGlobalObject = YES;
> }
> }
> - if (cls && objc_test_class_flag(cls, objc_class_flag_fast_arc))
> + if (obj && cls && objc_test_class_flag(cls, objc_class_flag_fast_arc))
> {
> - intptr_t *refCount = ((intptr_t*)obj) - 1;
> - if (obj && *refCount < 0)
> + uintptr_t *refCount = ((uintptr_t*)obj) - 1;
> + if (obj)
> {
> - obj = nil;
> - cls = Nil;
> + uintptr_t refCountVal = __sync_fetch_and_add(refCount,
> 0);
> + uintptr_t newVal = refCountVal;
> + do {
> + refCountVal = newVal;
> + long realCount = refCountVal & refcount_mask;
> + // If this object has already been deallocated
> (or is in the
> + // process of being deallocated) then don't
> bother storing it.
> + if (realCount < 0)
> + {
> + obj = nil;
> + cls = Nil;
> + break;
> + }
> + // The weak ref flag is monotonic (it is set,
> never cleared) so
> + // don't bother trying to re-set it.
> + if ((refCountVal & weak_mask) == weak_mask)
> + {
> + break;
> + }
> + // Set the flag in the reference count to
> indicate that a weak
> + // reference has been taken.
> + //
> + // We currently hold the weak ref lock, so
> another thread
> + // racing to deallocate this object will have
> to wait to do so
> + // if we manage to do the reference count
> update first. This
> + // shouldn't be possible, because `obj` should
> be a strong
> + // reference and so it shouldn't be possible to
> deallocate it
> + // while we're assigning it.
> + uintptr_t updated = ((uintptr_t)realCount |
> weak_mask);
> + newVal = __sync_val_compare_and_swap(refCount,
> refCountVal, updated);
> + } while (newVal != refCountVal);
> }
> }
> - LOCK_FOR_SCOPE(&weakRefLock);
> if (nil != old)
> {
> WeakRef *oldRef = weak_ref_table_get(weakRefs, old);
> @@ -583,7 +675,8 @@ id objc_storeWeak(id *addr, id obj)
> }
> else if (objc_test_class_flag(cls, objc_class_flag_fast_arc))
> {
> - if ((*(((intptr_t*)obj) - 1)) < 0)
> + uintptr_t *refCount = ((uintptr_t*)obj) - 1;
> + if ((long)((__sync_fetch_and_add(refCount, 0) & refcount_mask))
> < 0)
> {
> return nil;
> }
> @@ -648,20 +741,38 @@ static void zeroRefs(WeakRef *ref, BOOL shouldFree)
> }
> }
>
> -void objc_delete_weak_refs(id obj)
> +BOOL objc_delete_weak_refs(id obj)
> {
> LOCK_FOR_SCOPE(&weakRefLock);
> + if (objc_test_class_flag(classForObject(obj), objc_class_flag_fast_arc))
> + {
> + // If another thread has done a load of a weak reference, then
> it will
> + // have incremented the reference count with the lock held. It
> may
> + // have done so in between this thread's decrementing the
> reference
> + // count and its acquiring the lock. In this case, report
> failure.
> + uintptr_t *refCount = ((uintptr_t*)obj) - 1;
> + if ((long)((__sync_fetch_and_add(refCount, 0) & refcount_mask))
> < 0)
> + {
> + return NO;
> + }
> + }
> WeakRef *oldRef = weak_ref_table_get(weakRefs, obj);
> if (0 != oldRef)
> {
> zeroRefs(oldRef, NO);
> weak_ref_remove(weakRefs, obj);
> }
> + return YES;
> }
>
> id objc_loadWeakRetained(id* addr)
> {
> LOCK_FOR_SCOPE(&weakRefLock);
> + // *addr can only be zeroed by another thread if it holds the
> weakRefLock.
> + // It is possible for another thread to zero the reference count here,
> but
> + // it will then acquire the weak ref lock prior to zeroing the weak
> + // references and deallocating the object. If this thread has
> incremented
> + // the reference count, then it will skip deallocating.
> id obj = *addr;
> if (nil == obj) { return nil; }
> // Small objects don't need reference count modification
> @@ -674,14 +785,7 @@ id objc_loadWeakRetained(id* addr)
> {
> obj = block_load_weak(obj);
> }
> - else if (objc_test_class_flag(cls, objc_class_flag_fast_arc))
> - {
> - if ((*(((intptr_t*)obj) - 1)) < 0)
> - {
> - return nil;
> - }
> - }
> - else
> + else if (!objc_test_class_flag(cls, objc_class_flag_fast_arc))
> {
> obj = _objc_weak_load(obj);
> }
> diff --git a/objc/objc-arc.h b/objc/objc-arc.h
> index 0da4030..203cfef 100644
> --- a/objc/objc-arc.h
> +++ b/objc/objc-arc.h
> @@ -94,9 +94,11 @@ void objc_release(id obj);
> * weak pointers will return 0. This function should be called in -release,
> * before calling [self dealloc].
> *
> + * This will return `YES` if the weak references were deleted, `NO`
> otherwise.
> + *
> * Nonstandard extension.
> */
> -void objc_delete_weak_refs(id obj);
> +BOOL objc_delete_weak_refs(id obj);
> /**
> * Returns the total number of objects in the ARC-managed autorelease pool.
> */
>