libobjc2 & arc & multithreading

classic Classic list List threaded Threaded
4 messages Options
Reply | Threaded
Open this post in threaded view
|

libobjc2 & arc & multithreading

Andreas Fink-2
Hello all,

I have a heavily multithreaded application which runs under MacOS X and Linux. Under linux we use gnustep-base and libobjc2 (its a server app with no GUI). The application is completely written with automatic reference counting (ARC). Under heavy load under Linux we now noticed that we are very often sitting in locks triggered by ARC releasing objects in memory it doesn't need anymore. Usually my code is at a }  and libobjc2 waits on some global lock on freeing objects while other threads sit on a init or alloc call or also on a ARC release.

There is nothing generally wrong with this but the performance is impacted. 99% of the objects which get released with ARC that way are being created in the same thread which means no lock would be needed. Only when an object was created or held by another thread, things should be done more carefully. I believe this could speed things up a lot.

There is obviously a few things in libobjc2 which need better maintenance.
For example we always have to remove a debug log output related to a dtrace release which otherwise would spam our stdout as its being constantly called.

Is there anyone on this list who would like to join our efforts in this area or has some feedback on this subject? We have not touched libobjc2 code part until now but its essentially our performance bottleneck now.


Andreas Fink
------------------------------------------------------------------
Fink Telecom Services, Paradieshofstrasse 101, 4054 Basel, Switzerland
Mobile: +41-78-6677333
Skype: andreasfink    Jabber/XMPP: [hidden email] ICQ: 8239353
------------------------------------------------------------------






_______________________________________________
Discuss-gnustep mailing list
[hidden email]
https://lists.gnu.org/mailman/listinfo/discuss-gnustep
Reply | Threaded
Open this post in threaded view
|

Re: libobjc2 & arc & multithreading

David Chisnall
Hi,

> On 27 Nov 2017, at 06:56, Andreas Fink <[hidden email]> wrote:
>
> Hello all,
>
> I have a heavily multithreaded application which runs under MacOS X and Linux. Under linux we use gnustep-base and libobjc2 (its a server app with no GUI). The application is completely written with automatic reference counting (ARC). Under heavy load under Linux we now noticed that we are very often sitting in locks triggered by ARC releasing objects in memory it doesn't need anymore. Usually my code is at a }  and libobjc2 waits on some global lock on freeing objects while other threads sit on a init or alloc call or also on a ARC release.
>
> There is nothing generally wrong with this but the performance is impacted. 99% of the objects which get released with ARC that way are being created in the same thread which means no lock would be needed. Only when an object was created or held by another thread, things should be done more carefully. I believe this could speed things up a lot.

Thanks for the report.  Please can you open an issue on GitHub so that I don’t forget it?

It appears that this is related to the release of weak variables.  I haven’t tested the code with a heavily multithreaded environment, but it’s clear that this can be a bottleneck.

There are a couple of things I can imagine doing to improve this:

The first is simply to hash the object pointer value and have multiple weak reference tables, each with separate locks.  This would reduce contention.

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.  

Note that ‘no lock is needed’ is not correct, because we have no write barrier for a store of an object pointer to memory accessible by another thread (ObjC GC mode did have this, but it proved somewhat useless because almost all object pointers were stored to the heap at some point and so became potentially globally reachable).  

> There is obviously a few things in libobjc2 which need better maintenance.
> For example we always have to remove a debug log output related to a dtrace release which otherwise would spam our stdout as its being constantly called.

Please open a GitHub issue.

> Is there anyone on this list who would like to join our efforts in this area or has some feedback on this subject? We have not touched libobjc2 code part until now but its essentially our performance bottleneck now.

I am the maintainer of libobjc2, so I am always interested in feedback (and patches). Most of my time on libobjc2 is currently being spent on a new ABI, which should remove a number of restrictions inherited from the GCC ABI, but that can proceed in parallel with improvements to ARC.

David


_______________________________________________
Discuss-gnustep mailing list
[hidden email]
https://lists.gnu.org/mailman/listinfo/discuss-gnustep
Reply | Threaded
Open this post in threaded view
|

Re: libobjc2 & arc & multithreading

David Chisnall
On 27 Nov 2017, at 09:03, David Chisnall <[hidden email]> 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.
  */


_______________________________________________
Discuss-gnustep mailing list
[hidden email]
https://lists.gnu.org/mailman/listinfo/discuss-gnustep
Reply | Threaded
Open this post in threaded view
|

Re: libobjc2 & arc & multithreading

Andreas Fink-2
I will give it a throughout test the coming days...

> On 27 Nov 2017, at 18:33, David Chisnall <[hidden email]> wrote:
>
> On 27 Nov 2017, at 09:03, David Chisnall <[hidden email]> 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.
>  */
>



_______________________________________________
Discuss-gnustep mailing list
[hidden email]
https://lists.gnu.org/mailman/listinfo/discuss-gnustep