GC Behavior Inconsistent Between 32-bit and 64-bit Applications
Asked Answered
T

1

9

I have noticed inconsistent behavior from the GC when compiling console applications under both 32-bit and 64-bit in .Net 4.0 using VS 2013.

Consider the following code:

class Test
{
    public static bool finalized = false;
    ~Test()
    {
        finalized = true;
    }
}

and in Main() ...

var t = new Test();
t = null;
GC.Collect();
GC.WaitForPendingFinalizers();
if (!Test.finalized)
    throw new Exception("oops!");

When running in 64-bit (debug) mode this works every time without fail; however, running in 32-bit mode I cannot force this object to get collected (even if I create more objects and wait a period of time, which I have tried).

Does anyone have any ideas as to why this is? It's causing me trouble when trying to debug some code that must deal with releasing unmanaged proxy data for the 32-bit version of the assemblies. There's a lot of objects in 32-bit mode that just sit there until a long time later (no so in 64-bit).

I'm trying to debug something in 32-bit mode, but the finalizers are not getting called (at least, not by force). The objects just sit there and never get collected (I can see all the weak references still having a value). In 64-bit mode, all the weak references are cleared as expected, and all finalizers get called.

Note: Though the code above is on a very small scale, I have noticed in 32-bit mode many more objects stuck in the GC until more objects get created later (even when "Collect" and "WaitForPendingFinalizers" is called). This is never the case in 64-bit mode. I have one user wondering why so many objects where not getting collected, which caused me to investigate, to which I found out that everything seems to work better in 64-bit mode than 32. Just trying to understand why.

Edit: Here is better code to show the differences:

class Program
{
    class Test
    {
        public static bool Finalized = false;
        public int ID;
        public Test(int id)
        {
            ID = id;
        }
        ~Test()
        { // <= Put breakpoint here
            Finalized = true;
            Console.WriteLine("Test " + ID + " finalized.");
        }
    }

    static List<WeakReference> WeakReferences = new List<WeakReference>();

    public static bool IsNet45OrNewer()
    {
        // Class "ReflectionContext" exists from .NET 4.5 onwards.
        return Type.GetType("System.Reflection.ReflectionContext", false) != null;
    }

    static void Main(string[] args)
    {
        Console.WriteLine("Is 4.5 or newer: " + IsNet45OrNewer());
        Console.WriteLine("IntPtr: " + IntPtr.Size + Environment.NewLine);

        Console.WriteLine("Creating the objects ...");

        for (var i = 0; i < 10; ++i)
            WeakReferences.Add(new WeakReference(new Test(i)));

        Console.WriteLine("Triggering collect ...");
        GC.Collect();

        Console.WriteLine("Triggering finalizers ..." + Environment.NewLine);
        GC.WaitForPendingFinalizers();

        Console.WriteLine(Environment.NewLine + "Checking for objects still not finalized ...");

        bool ok = true;

        for (var i = 0; i < 10; ++i)
            if (WeakReferences[i].IsAlive)
            {
                var test = (Test)WeakReferences[i].Target;
                if (test != null)
                    Console.WriteLine("Weak references still exist for Test " + test.ID + ".");
                ok = false;
            }

        if (ok)
            Console.WriteLine("All Test objects successfully collected and finalized.");

        Console.WriteLine(Environment.NewLine + "Creating more objects ...");

        for (var i = 0; i < 10; ++i)
            WeakReferences.Add(new WeakReference(new Test(i)));

        Console.WriteLine("Triggering collect ...");
        GC.Collect();

        Console.WriteLine("Triggering finalizers ..." + Environment.NewLine);
        GC.WaitForPendingFinalizers();

        Console.WriteLine(Environment.NewLine + "Checking for objects still not finalized ...");

        ok = true;

        for (var i = 0; i < 10; ++i)
            if (WeakReferences[i].IsAlive)
            {
                var test = (Test)WeakReferences[i].Target;
                if (test != null)
                    Console.WriteLine("Weak references still exist for Test " + test.ID + ".");
                ok = false;
            }

        if (ok)
            Console.WriteLine("All Test objects successfully collected and finalized.");

        Console.WriteLine(Environment.NewLine + "Done.");

        Console.ReadKey();
    }
}

It works in 64-bit, but not in 32-bit. On my system, "Test #9" never gets collected (a weak reference remains), even after creating more objects, and trying a second time.

FYI: The main reason for asking the question is because I have a \gctest option in my console to test garbage collection between V8.Net and the underlying V8 engine on the native side. It works in 64-bit, but not 32.

Tisbee answered 15/5, 2015 at 8:5 Comment(16)
Have you tried adding a bit of code to ~Test()? Perhaps empty finalizers are being ignored somehow.Oblique
Yes, this is a simplified test version of the original. As well, optimization is not enabled.Tisbee
Are you running in a debugger? It does a lot of stuff to make debugging easier, including extending the lifetime of locals. Does the problem still show if you run outside of a debugger? Or if you exit the method? Are you sure there's no remaining strong reference?Goings
You need to show us a code which reproduces the problem. I can't reproduce the problem with the code provided. Can you?Meditate
I am using VS2013, and running in debug mode. I haven't tried it outside of that yet.Tisbee
That's IS the code that has the problem, it's much the same, and fails in a 32-bit mode console app. The actual code is in the "\gctest" option of the V8.Net test console app.Tisbee
@JamesWilkins It is much the same isn't helpful. Can you reproduce the problem with this code provided? I mean with the Test class? If not, you'll have to create another code sample which reproduces the problem.Meditate
Yes I can, otherwise I would not have posted it. ;)Tisbee
I'm also unable to reproduce the issue. So can you please create a MCVE so that we can help you?Phlogistic
Try doing the object creation in a separate method from Main. With a contrived example you can prove contrived problems. Are you sure that the finalizer is not just optimized away? I mean, it does nothing, why would you even have it? Under the debugger, the lifetime of variable (even temporary variables you don't see) is extended until the end of the method.Leonhard
If it were optimized away in 32-bit, then why not in 64? ;)Tisbee
32-bit and 64-bit are different and the way a debugger interacts with a 32-bit and 64-bit program are also slightly different. You should create an example that shows the difference without running under the debugger.Leonhard
Is this question out of curiosity? Because GC behavior is not guaranteed you can't rely on anything that you find to be true by testing anyway. Could change at any time.Zoolatry
It causes a lot of objects to not get finalized - and objects not finalized means a lot of native side stuff still not handled, and makes debugging less desirable than it already is. ;)Tisbee
OK, if you want a workaround: Don't store the ref to Test in a local. Store it in a heap-based wrapper class such as class Box<T> { public T Value; }. Given the current JITs it should be more likely that you are seeing deterministic reference behavior doing that. (No guarantees - but I think you understand that.)Zoolatry
There is no inconsistency - the GCs aren't supposed to be identical nor can they be. The 64-bit GC has to deal with a whole lot more memory than the 32-bit GC and types that are twice as big. The 32 and 64-bit GCs are different just as the client and server GCs are different. Some of those differences are significant - the 64 bit runtime allows for tail-recursion optimizations when the 32 bit runtime throws a StackOverflowException.Fortier
C
3

No repro on VS2013 + .NET 4.5 on x86 and x64:

class Program
{
    class Test
    {
        public readonly int I;

        public Test(int i)
        {
            I = i;
        }

        ~Test()
        {
            Console.WriteLine("Finalizer for " + I);
        }
    }

    static void Tester()
    {
        var t = new Test(1);
    }

    public static bool IsNet45OrNewer()
    {
        // Class "ReflectionContext" exists from .NET 4.5 onwards.
        return Type.GetType("System.Reflection.ReflectionContext", false) != null;
    }

    static void Main(string[] args)
    {
        Console.WriteLine("Is 4.5 or newer: " + IsNet45OrNewer());
        Console.WriteLine("IntPtr: " + IntPtr.Size);

        var t = new Test(2);
        t = null;

        new Test(3);

        Tester();

        Console.WriteLine("Pre GC");
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.WriteLine("Post GC");

        Console.ReadKey();
    }
}

Note that (as noticed by @Sriram Sakthivel), there could be "ghost" copies of your variable around, that have their lifespan extended until the end of the method (when you compile the code in Debug mode or you have a debugger attached (F5 instead of Ctrl+F5), the lifespan of your variables is extended until the end of the method to ease debugging)

For example object initialization (when you do something like new Foo { I = 5 }) creates an hidden local variable.

Cervicitis answered 15/5, 2015 at 8:47 Comment(8)
What you're showing is different. You show an example of object initializer which has the hidden local. You should try OP's exact code. It will not be extended to the end of the method.Meditate
As I said earlier, it is not equal to OP's code. Object initializer will create a hidden local which is not being set to null. So the object is reachable(through the hidden local). With OP's code it is not a problem, variable will go unreachable. In fact, I can't reproduce it and voted to close as can't reproduce.Meditate
@SriramSakthivel You are right. Now it is a no-repro in the opposite sense... The debugger always finalizes the objects.Cervicitis
@JamesWilkins Even targetting the .NET 4.0, the runtime it will use is 4.5 . (but just to be sure I've checked it) I've modified the code to show if it is using .NET 4.5 or 4.0 . Sadly I don't have any machine with Visual Studio without .NET 4.5Cervicitis
I have forced the language version to 3.0, 4.0, and 5.0 (under advanced settings), and all in x86 fail to collect.Tisbee
Your code (with a lot more going on) does work. "Is 4.5 or newer" is true, and IntPtr is 4. However, the finalization order is different between bit versions (not that it should be in any order I know).Tisbee
@JamesWilkins .NET 4.5 is a binary replacement for .NET 4.0. If you've installed the 4.5 runtime on your machine, you are using the .NET 4.5 binaries. And the GCs are different because they target different CPU architectures, CPU commands, memory and type sizesFortier
By the way, you can't claim "No repro" when your code is different from the posted code.Tisbee

© 2022 - 2024 — McMap. All rights reserved.