Question about Element's ARC

I’m making a cross-platform WeakRef class which is something as below on Cocoa:

public class WeakRef<T>
{
    public readonly __weak object value; // make public to test

    public WeakRef(T v) {
        value = v;
    }

    public T get() {
        return value as T;
    }
}

Then I find a problem that can be reproduced in below test code:

    public int Main(int argc, AnsiChar** argv)
    {
        WeakRef<object> r;

        // condition #1
        {
            var a = new object();
            r = new WeakRef<object>(a);
            doSth(r.value);
            a = null;
        }
        NSLog($"#1 value = {r.value}");
        // output: "#1 value = (null)"

        // condition #2
        {
            var a = new object();
            r = new WeakRef<object>(a);
            doSth(r.get()); // the only difference
            a = null;
        }
        NSLog($"#2 value = {r.value}");
        // output: "#2 value = <NSObject: 0x7f912a500460>"
    }

    private void doSth(object r){
    }

So when will r.value be released in condition #2?

Probably at the end of the scope, i.e. after Main() ends?

try adding

    using (__aurtoreleasepool)
    {
        var a = new object();
        r = new WeakRef<object>(a);
        doSth(r.value);
        a = null;
    }

to force things to get cleaned u earlier.

  1. I’ve also tried below code to test if a method and a pure {} scope are different:

     public int Main(int argc, AnsiChar** argv)
     {
         condition3();
         NSLog($"#3 value = {mRef.value}");
         // output: "#3 value = <NSObject: 0x7fbeb2c00e30>"
     }
     
     private WeakRef<object> mRef;
    
     private void condition3(){
         var a = new object();
         mRef = new WeakRef<object>(a);
         doSth(mRef.get());
         a = null;
     }
    

The result shows that mRef.value won’t be released even after the end of method. If this weak ref won’t be released until the end of this program/Main(), that’s a leak I think.

  1. Is it really necessary to declare an AutoReleasePool manually in this simple condition?

  2. The only difference in condition #2 is using a method to return the weak ref instead of using the ref directly. I think it’s easy to add retain/release calls by compiler here. I can’t figure out why this will cause a different memory lifecycle.

I had similar problem with Cococa. In ARC, object is destroyed if is out of scope. While You are in Main, then is never out of scope. Constantly creating new object in loop for example, will cause no memory release. Use AutoReleasePool and it will fix Your problem. Mine was fixed instantly. Read about AutoReleasePool and all will be clear :slight_smile:

Then what’s the definition of a scope? A nested {} bracket pair isn’t a scope?

Also refer to my test condition #3 to see the object won’t be destroyed even after the end of its method.

If an explicit AutoReleasePool is a must-have here, why will the condition #1 release the object as I expect?

I know an explicit AutoReleasePool can fix it, but that’s not the recommended practice for common issue in ARC (because ARC will do that for you in most cases which is the difference between MRC and ARC), and that’s also not the explanation for “Why ARC can’t work in this simple condition” I think.

Btw, I’m not sure what this case is in ARC. As Element’s doc said:
http://docs.elementscompiler.com/Concepts/ARC/

One item worth noting is that in contrast to Objective-C’s default, Elements considers local variables to go out of scope at the end of the block that defined them (i.e. usually at the end of the method, or at the end of a nested/pair). By contrast, Objective-C will release the references stored in local directly after the last time that variable is accessed, which can lead to some unexpected crashes when working with child objects or bridged objects.

In Objective-C, the attribute((objc_precice_lifetime)) attribute can be used to change this behavior; Elements behaves as if this attribute was defined, by default. You could say that Elements errs on the side of caution and more well-defined object lifetimes.

So in Elements’ ARC, the local variables in loop won’t be destroyed immediately after the last access, but they will be destroyed when ref count become 0.

My test code can prove it:

    public int Main(int argc, AnsiChar** argv)
    {
        __weak object weakRef;
        for(int i = 0; i<10; ++i){

            if(i>0) NSLog($"after last iteration = {weakRef}");
            // output: after last iteration = (null)

            var strongRef = new object();
            weakRef = strongRef;

            NSLog($"before the end of this iteration = {weakRef}");
            // output: before the end of this iteration = <NSObject: 0x7fa89a6003c0>
        }
    }

So, is there an actual bug/problem here, or is this discussion purely academic?

Out of scope mean what is not used anymore, but don’t think that object should be freed instantly. It will be freed when method finish. In Your case Main. Main call condition3 and You expect that object will be freed, but lifetime manager may free it later. Just wrap method with AutoReleasePool and check if object is destroyed. If not then probably is bug. I understand You perfectly, because I come from delphi world where even with ARC (Cococa) I can use DisposeOf to free object when I want and here You “fight” with non controlled behaviour.
Btw. Why You need object to be instantly destroyed? It’s must or You sniff memory leak? :slight_smile:

@mh
Sorry for academic discussion, just had similar case and trying to share experience

I think it’s a serious bug, which means I can’t use weak reference in Element’s ARC because the object will be leaked in very simple conditions.
I’ve done the same test on objc’s ARC, everything works fine, no explicit AutoReleasePool required.

If the only way to solve this leak is to use an AutoReleasePool explicitly, the Elements’ ARC is not a real ARC, it’s still a MAC. With this limitation, you can’t make a cross-platform weak reference, because there’s no concept of AutoReleasePool on Java/.NET.

Sorry but I think maybe you don’t really understand what ARC is? ARC is just helping you to insert retain/release calls in certain places automatically in compile time, there’s nothing called lifetime manager as mark-and-sweep GC.
In ARC, the objects will be destroyed at a certain time when the reference count become zero, which can be estimated if you know how the compiler works. This is very different from runtime GC.

Please read this post:

I know what is ARC. I had meaning lifetime management of object. To sounds correctly I should write memory manager. As I said, is not a problem in my case when object was released. RO compiler just work different than in Objective-C which You have actually mention from Element’s doc. It may be cons or pros. If is cons for You then mark is as bug then. For me is a way how RO compiler works and I life with it.

I don’t understand what that means. Yes, our ARC follows exactly Cocoa/Objective-C’s rules.

define “leak”. A leak is an object reference that gets “lost” and can/will never get freed because no code that will ever run again knows about the reference. Auto-released objects stay around until the auto-release pool that the current scope is in gets cleared. that usually is several layer sun the stack. That’s an “as designed” behavior of how ARC works (on Cocoa), not a bug or a leak.

Weak references are a feature of the Cocoa runtime, and specific to ARC. They do not work with GC, and thus they do not work cross-platform on . NET or Java — only (currently) on Cocoa.

it releases objects very slightly later, but still deterministically, in favor of safety, yes. Elements scope size have no relation to auto-release pools. This affects when objects get straight-released or auto-released, it does not affect how the ARPs work.

ok marc, just refer to condition #2

    {
        var a = new object();
        r = new WeakRef<object>(a);
        doSth(r.get()); // the only difference
        a = null;
    }
    NSLog($"#2 value = {r.value}");
    // output: "#2 value = <NSObject: 0x7f912a500460>"

Could you tell me which strong reference is still holding the object at the log line? Should the ref count of that object be zero? Should this object be destroyed in Cocoa reference-count system?

When an object has zero ref count which is still alive, it’s a leak in ARC.
(But it’s ok in GC)

Hard to say. It may just well be that calling r.get() and passing the value to doSomething causes s local retain/release or retain/auto release that causes for the reference to be held on a bit longer — til the end of the method, or until the containing ARP gets cleared. Who knows. ARC is not trivial, there’s A LOT of retaining and releasing going on under the hood to make it all work reliably.

If this whole code were called externally, there’d be an ARP around the calm to this method, and your object was still alive after that ARP has been cleared — then and only then would this be a leak, and thus a bug.

Which may well be the case — but your code snippet is not enough to prove one way or the other.

Compared to native objc/swift, Elements’ ARC behaves very differently.

So here is a more serious problem and test projects you can try:
http://talk.remobjects.com/t/memory-leak-on-ios/7460

different != wrong.

i’ve yet to see proof of a concrete actual leak thats to an academic discussion about when exactly you’d like to see the disposal to happen.

I want to confirm a very important difference between Elements’ ARC and objc’s ARC which is totally not mentioned in Elements’ document: Automatic Reference Counting
Then I will explain why I’m so stubborn on this issue later.

Run RO C# code as:

     public object createObj(){ return new object(); }
     ...
     var a = createObj();
     a = null;

We will find the temporary object won’t be deallocated until the end of the closeast ARP, because the createObj() method will mark the return value as autorelease.
So if we use lots of temporary objects returned from methods, we must use explicit ARP to avoid high memory peak usage or OOM.

But this won’t happen in objc’s ARC, where that temporary object will be deallocated immediately after a=nil.
Because there’s a very good optimization in objc’s ARC: objc_retainAutoreleasedReturnValue. The redundant autorelease/retain calls will be eliminated at all.

Just refer to this article:

The concept behind objc_retainAutoreleasedReturnValue is that if a value is to be returned from a function autoreleased, but the very next thing that is to be done is a retain on that object then it’s absolutely pointless doing the autorelease and retain – we’re just wasting cycles.

This optimization is not for some edge cases but almost 90% cases in our code (think about a getter of a property).
I don’t know much about compiler architecture, but it seems be not too difficult to add this optimization for Elements, because objc_retainAutoreleasedReturnValue is only an existed objc runtime func.

Except for performance/memory footprint problem, the reason for me to make this issue clear instead of a smattering is that I want to write cross-platform code (that’s why I chose Elements) with as few as possible explicit ARP.

I can surely create a cross-platform WeakRef like:

#if COOPER
public __mapped class WeakRef<T> => java.lang.@ref.WeakReference<T>
#elif ECHOES
public __mapped class WeakRef<T> => System.WeakReference<T>
#elif NOUGAT
public class WeakRef<T>
#endif
{
    #if NOUGAT
    private __weak object mValue;
    #endif

    public WeakRef(T v) {
        #if COOPER
        return new java.lang.@ref.WeakReference<T>(v);
        #elif ECHOES
        return new System.WeakReference<T>(v);
        #elif NOUGAT
        mValue = v;
        #endif
    }

    public T get() {
        #if COOPER
        return __mapped.get();
        #elif ECHOES
        T v = null;
        __mapped.TryGetTarget(out v);
        return v;
        #elif NOUGAT
        return mValue as T;
        #endif
    }
}

So I can write the same code in ARC and GC.
The __weak keyword is also cross-platform which is ignored by GC platform, so circular retain can be eliminated without affecting GC code much.
But __autoreleasepool isn’t, I have to make my clean code become sth like:

        #if NOUGAT
        using(__autoreleasepool)
        #endif
        {
            //...
        }

There’s also another good proposal to make __autoreleasepool ignorable by GC platform. But it still costs an extra nested {} and maybe much time to figure out whether a piece of code (especially any loop I used) has memory footprint problem only in ARC which can’t be told intuitively by your eyes.

So I just hope explicit ARP is only for very rare edge cases in Elements’ ARC, like what we can enjoy in objc/swift.

Thanks, logged as bugs://73551 to look into our use of objc_retainAutoreleasedReturnValue

@Esword:

I checked the codegen again and this is what gets generated:

namespace issueotest
{
using Foundation;

    class Test
    {
        public static object createObj(){ 
          return new object(); 
          /* 
          Becomes:
              NSObject Result = null;
              objc_StoreStrong(Result, NSObject.alloc().init());
              return objc_autoreleaseReturnValue(result);
                      
          
          */
        }
        public static void Main()
        {
            {
              var a = createObj();
              a = null;
              writeLn("nothere");
            }
            writeLn("here");
            
            /*
  NSObject a = null;
  var tmp = objc_retainAutoreleasedReturnValue(this.createObject());
  objc_storeStrong(a, tmp);
  objc_release(tmp);
  objc_storeStrong(a, null);
  puts("nothere");
  objc_storeStrong(a, null); // end of scope cleanup
  puts("here");
             */
        }

    }
}

which afaik is the exact same thing objc does…