JSON Parsing - Garbage Collection and Coding best practices?

Hello.

I'm currently using Newtonsoft.Json in order to parse JSON files that contain data related to things like health bar XY coordinates, anchors, etc, and while it all works, I'm just wondering if I can't do better?

I'm using classes to effectively serve as DTOs for the JSON files I made, but the thing is that once I find the existing health bars and set the values from the JSON DTO class instances, I never need the DTOs again.

To fight garbage accumulation, I run GC.Collect() after I set the values from the DTOs, and once more after I set all the DTOs to null. Is this actually doing anything? Am I indeed emptying the object, or just severing any reference to it and leaving it on the heap? I also ask because when trying to set game objects that exist in the inspector to null, they still exist during runtime. If I want to avoid creating garbage, should I instead make the DTOs structs? If I do that, what should be done about the strings that exist in the DTOs? Do their sheer presence eliminate any potential garbage accumulation avoidance benefits?

Please advise. Thank you.

1 Like

ANY C# object that isn't referenced anymore is up for garbage collection and will eventually be collected. When you call GC.Collect it should reclaim the memory. However, any C# object that is derived from UnityEngine.Object has a native C++ counter part in the engine's core. Such objects have to be Destroyed using Destroy. This is true for all such objects. The only case where they are "destroyed along" is when you destroy a parent gameobject it will automatically destroy any child gameobject and all the components attached to those gameobjects. However and other resource (Mesh, Material, ScriptableObject, Texture2D, ...) that you create dynamically at runtime (either creating or cloning by using Instantiate) will need to be explicitly destroyed.

There is a small exeption that you should not rely on. Any UnityEngine.Object asset that is no longer referenced by any managed code would be destroyed when you load a new scene as it calls Resources.UnloadUnusedAssets implicitly. Though this is just a safety net and may not work on all cases. So just keep in mind that you have to actively destroy things derived from UnityEngine.Object.

The managed part of those "engine objects" will be garbage collected like any other plain C# object. When you call Destroy on such an object you only kill the native C++ part of the object. The managed part is then just a left-over in the managed world and will eventually be garbage collected, given it's not referenced anymore.

So in short: Yes, when there's no reference to the object anymore and teh garbage collector runs, the object will be removed.

ps: Instead of parsing your json data into custom data objects, you could also parse it just into Dictionaries / Lists so you don't have to create all those data transfer classes. I've written a simple JSON parser that parses the json data into a few predefined classes which simplify the handling of the data. It's just a single file that needs to be included. Though the repositiory contains a few modular extension files, also one for Unity, that makes the conversion of certain data easier. Like reading a Vector3, Rect and some other types. It's also very extensible since all classes are declared partial. Though if you already have your system up and running, you probably should stick to it.

4 Likes

Thank you for the response, it really helped shed a lot of light on this matter. However, there are a few things that I still need some clarification on:

  1. I tried using UnityEngine.Object.Destroy() like you mentioned, and the Unity Console yielded the following error:

“Destroying assets is not permitted to avoid data loss.
If you really want to remove an asset use DestroyImmediate (theObject, true);”

However, this runs counter to Unity’s official recommendations from their own Documentation:

https://docs.unity3d.com/ScriptReference/Object.DestroyImmediate.html

"Destroys the object obj immediately. You are strongly recommended to use Destroy instead.

This function should only be used when writing editor code since the delayed destruction will never be invoked in edit mode. In game code you should use Object.Destroy instead. Destroy is always delayed (but executed within the same frame). Use this function with care since it can destroy assets permanently! Also note that you should never iterate through arrays and destroy the elements you are iterating over. This will cause serious problems (as a general programming practice, not just in Unity)."

To elaborate, I was trying to call Destroy() on the Texture, RawImage, RectTransform, and GameObject objects that make up the health bar in the name of cleaning up garbage (This all happens during Start(), by the way), and calling Destroy() on RawImage and GameObject were the only scenarios in which the RawImage disappeared and the GameObject itself was removed from the Inspector.

Naturally, that’s far too much for what I’m trying to do, as I’m not trying to delete them from the Inspector when I need them during run-time; I’m simply trying to delete the middle-man objects. However, said objects are mostly custom classes from the parsed JSON, or primitives like strings, ints, float arrays, etc, and in a few areas, Unity Texture objects.

So what should I do? What do you think about DestroyImmedate() - what does it do, exactly, and is it as bad as Unity says it is in the documentation? Is me nulling out these objects enough? Also, is calling the garbage collector before I null out these objects premature/redundant?

  1. When it comes to nulling out objects for better garbage collection, should I also null out child objects within the parent objects (E.g. arrays within a DTO class), or will nulling out the parent objects be enough?

  2. Where do you stand on the use of structs as substitutes for classes when it comes to JSON parsing? Will I still have to null them out and call the GC at the end as well? If not, why not, and are any garbage avoidance benefits lost if I give them heap-allocated objects like float arrays, or more importantly, strings - if yes, then should I substitute the strings for character arrays, or simply not have any arrays or collections to begin with?

Thank you for sharing your work, it looks fantastic, but for the actual JSON parsing, I think I’m good with what I made and have; it’s one of the few parts of my game that I’m not too worried about (Or at least don’t want to worry about, lol).

If it's just tens/hundreds of DTOs, and you're not serializing/deserializing each frame, just don't worry about it, it won't affect that much the performance since all devices nowadays have enough memory to handle millions of objects with no problem at all. As people here often say, don't assume, profile.

For nulling objects, you don't need to do it at all. As soon as an object is out of scope (out of an if block, for block, method..., i.e. braces { }) it's automatically marked as garbage and will be collected by GC when the time comes (it's the GC that decides when, even when calling GC.Collect as it's not guaranteed to run right away).

Like the documentation it should be used in editor scripts. For example say you write an editor script (not game, not while the game is playing… but a script intended to run in the editor. Say like an EditorWindow), and that script loads up resources to process them for whatever reason. You’d call DestroyImmediate afterward to purge them.

As for calling destroy, I’m confused as to what you’re calling them on. You say:

You can’t destroy strings/ints/floats/arrays. You can only destroy things that inherit from UnityEngine.Object. Texture objects can be destroyed.

An object becomes eligible for garbage collection when nothing references it. During the GC call it does a pass called “marking” where it is basically marking if it’s referenced or not. If an object is referenced by an object that isn’t referenced, then that object is still marked eligible for collection because it’s not in the actual reference hierarchy.

So no… you don’t need to null everything away.

To give you an example… a List actually is a class that wraps a T[ ] array. When you mark the list null it’s eligible for collection, you don’t have to set its underlying array null though. You actually can’t since it’s private/encapsulated in the List.

You can’t null structs. You can only null reference types (class), which structs are not. (note you can make a struct nullable by wrapping it in the Nullable type, short hand T?).

Use a struct when you need a small packet of pure data. Think like Vector2/3/4, or Quaternion. These are structs because they are pure data.

My general rule of thumb is:

“If I have 2 of the same type with the same value, and I want to consider those values the same when they’re equal, then it’s a struct. Otherwise it’s a class.”

Think how 5 and 5 are the same, they’re both 5. Or <1,1,1> and <1,1,1> (vector3) are the same.

But 2 Lists or GameObjects aren’t necessarily the same even if they have similar properties. 2 GameObjects named “ObjectA” aren’t necessarily the same object, they’re independent objects with the same name. Or a List {1,23} and another List {1,2,3} aren’t necessarily the same list, they just contain the same sequence. This is significant because you might want a List that represents ALL values that can be guessed, and another that contains all values that have yet to be guessed. The 2nd list you remove values from as they’re guessed, but you want the first list to still contain ALL values because say you want to draw ALL values, but color those who weren’t selected yet.

You seem very preoccupied with your memory management at this point, but have yet to figure out how it works completely.

Maybe go read the fundamentals of GC in .Net:
https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/fundamentals

And maybe even go read/watch some vids on the fundamentals of theory behind GC (note GC is not unique to C#, you could watch/read about GC on a theory level to get a better idea).

As for what you’ve written, we can say very little of it, since we have no idea what you’ve written. You haven’t shown it.

1 Like

Thank you for the resource, I’m currently trying to better grasp Garbage Collection, now that I have the time to do so.

As for my code, it is as follows:

using UnityEngine;
using UnityEngine.UI;

public class DataSetterUI : MonoBehaviour
{
    #region Root Objects
    private string rootAssetPath;
    private string rootTexturePath;
    #endregion

    #region BarShared.json
    private string barSharedFileName;
    private Vector2 barRootAndCoverSizeVec;
    private float[] barRootAndCoverSizeArr;
    private Vector2 barBGAndBarSizeVec;
    private float[] barBGAndBarSizeArr;
    private Vector2 barBGPosVec;
    private float[] barBGPosArr;
    private Texture barBGTxre;
    private string barBGTxreFileName;
    private Vector2 barCoverPosVec;
    private float[] barCoverPosArr;
    private Texture barCoverTxre;
    private string barCoverTxreFileName;
    #endregion

    #region BarGeneric.json
    private string barGenericFileName;
    private Vector2 barMinAnchrVec;
    private float[] barMinAnchrArr;
    private Vector2 barMaxAnchrVec;
    private float[] barMaxAnchrArr;
    private Vector2 barPivVec;
    private float[] barPivArr;
    private Vector3 barScleVec;
    private float[] barScleArr;
    private bool bCanRaycastTarget;
    private Color barRootClr;
    private float[] barRootClrArr;
    private Color barObjClr;
    private float[] barObjClrArr;
    #endregion

    #region BarHealth.json
    private string barHealthFileName;
    private Vector2 barHPRootObjPosVec;
    private float[] barHPRootObjPosArr;
    private Vector2 barHPBarPosVec;
    private float[] barHPBarPosArr;
    private Texture barHPBarTxre;
    private string barHPBarTxreFileName;
    #endregion

    #region HealthBarRoot
    private string healthBarRootName;
    private GameObject healthBarRoot;
    private RectTransform healthBarRootRect;
    private RawImage healthBarRootRImg;
    #endregion

    #region HealthBarBG
    private string healthBarBGName;
    private GameObject healthBarBG;
    private RectTransform healthBarBGRect;
    private RawImage healthBarBGRImg;
    #endregion

    #region HealthBarBar
    private string healthBarName;
    private GameObject healthBar;
    private RectTransform healthBarRect;
    private RawImage healthBarRImg;
    #endregion

    #region HealthBarCover
    private string healthBarCoverName;
    private GameObject healthBarCover;
    private RectTransform healthBarCoverRect;
    private RawImage healthBarCoverRImg;
    #endregion

    void Start()
    {
        GenerateRootObjects();

        GenerateAndSetJSONSubObjects();

        FindBarUIObjects();

        GenerateSubObjects();

        GetBarUIComponents();

        SetBarUIObjects();

        CleanUpIrrelevantData();
    }

    //Consider having root paths and filenames stem from individual files as well
    private void GenerateRootObjects()
    {
        rootAssetPath = "Assets/Data/UI/";
        rootTexturePath = "Textures/UI/";

        barSharedFileName = "BarShared.json";
        barGenericFileName = "BarGeneric.json";

        barHealthFileName = "BarHealth.json";

        healthBarRootName = "HealthBarRoot";
        healthBarBGName = "HealthBarBG";
        healthBarName = "HealthBarBar";
        healthBarCoverName = "HealthBarCover";
    }

    private void GenerateAndSetJSONSubObjects()
    {
        DataSheetMeterComp meterComp = JSONHelper.DeserializeJSON<DataSheetMeterComp>(rootAssetPath + dataSheetMeterCompSharedFileName);
        DataSheetMeterGeneric meterGeneric = JSONHelper.DeserializeJSON<DataSheetMeterGeneric>(rootAssetPath + dataSheetMeterGenericSharedFileName);

        DataSheetMeterCombined meterHP = JSONHelper.DeserializeJSON<DataSheetMeterCombined>(rootAssetPath + dataSheetMeterHealthFileName);

        meterRootAndCoverSizeArr = meterComp.MeterRootAndCoverSize.Size.Dim2;
        meterBGAndBarSizeArr = meterComp.MeterBGAndBarSize.Size.Dim2;
        meterBGPosArr = meterComp.MeterBG.Pos.Dim3;
        meterBGTxreFileName = meterComp.MeterBG.TextureFileName;
        meterCoverPosArr = meterComp.MeterCover.Pos.Dim3;
        meterCoverTxreFileName = meterComp.MeterCover.TextureFileName;

        meterMinAnchrArr = meterGeneric.MeterShared.MinAnchr.Dim2;
        meterMaxAnchrArr = meterGeneric.MeterShared.MaxAnchr.Dim2;
        meterPivArr = meterGeneric.MeterShared.Piv.Dim2;
        meterScleArr = meterGeneric.MeterShared.Scle.Dim3;
        bCanRaycastTarget = meterGeneric.MeterShared.RaycastTarget;
        meterRootClrArr = meterGeneric.MeterRootColor.Color.Dim4;
        meterObjClrArr = meterGeneric.MeterObjectColor.Color.Dim4;

        SetMeterObjs(in meterHP, out meterHPRootObjPosArr, out meterHPBarPosArr, out meterHPBarTxreFileName);
    }

    private void SetMeterObjs(in DataSheetMeterCombined inputDataSheetMeter, out float[] outputDataMeterRootObjPosArr,
        out float[] outputDataMeterBarPosArr, out string outputDataMeterFileName)
    {
        outputDataMeterRootObjPosArr = inputDataSheetMeter.MeterRootObj.Pos.Dim3;
        outputDataMeterBarPosArr = inputDataSheetMeter.MeterBar.Pos.Dim3;
        outputDataMeterFileName = inputDataSheetMeter.MeterBar.TextureFileName;
    }

    private void FindMeterUIObjects()
    {
        FindMeterUIObjs(healthMeterTestNextName, healthMeterTestNextBGName, healthMeterBarName,
            healthMeterTestNextCoverName, out healthMeterTestNext, out healthMeterTestNextBG, out healthMeterBar,
            out healthMeterTestNextCover);
    }

    private void FindMeterUIObjs(string inputMeterRootObjName, string inputMeterBGObjName, string inputMeterBarName,
        string inputMeterCoverName, out GameObject outputMeterRootObj, out GameObject outputMeterBGObj,
        out GameObject outputMeterBarObj, out GameObject outputMeterCoverObj)
    {
        outputMeterRootObj = GameObject.Find(inputMeterRootObjName);

        outputMeterBGObj = GameObject.Find(inputMeterBGObjName);
        outputMeterBarObj = GameObject.Find(inputMeterBarName);
        outputMeterCoverObj = GameObject.Find(inputMeterCoverName);
    }

    private void GenerateSubObjects()
    {
        meterRootAndCoverSizeVec = new Vector2(meterRootAndCoverSizeArr[0], meterRootAndCoverSizeArr[1]);
        meterBGAndBarSizeVec = new Vector2(meterBGAndBarSizeArr[0], meterBGAndBarSizeArr[1]);
        meterBGPosVec = new Vector2(meterBGPosArr[0], meterBGPosArr[1]);
        meterBGTxre = Resources.Load<Texture>(rootTexturePath + meterBGTxreFileName);
        meterCoverPosVec = new Vector2(meterCoverPosArr[0], meterCoverPosArr[1]);
        meterCoverTxre = Resources.Load<Texture>(rootTexturePath + meterCoverTxreFileName);

        meterMinAnchrVec = new Vector2(meterMinAnchrArr[0], meterMinAnchrArr[1]);
        meterMaxAnchrVec = new Vector2(meterMaxAnchrArr[0], meterMaxAnchrArr[1]);
        meterPivVec = new Vector2(meterPivArr[0], meterPivArr[1]);
        meterScleVec = new Vector3(meterScleArr[0], meterScleArr[1], meterScleArr[2]);
        meterRootClr = new Color(meterRootClrArr[0], meterRootClrArr[1], meterRootClrArr[2], meterRootClrArr[3]);
        meterObjClr = new Color(meterObjClrArr[0], meterObjClrArr[1], meterObjClrArr[2], meterObjClrArr[3]);

        GenerateMeterSubObjs(in meterHPRootObjPosArr, in meterHPBarPosArr, rootTexturePath + meterHPBarTxreFileName,
            out meterHPRootObjPosVec, out meterHPBarPosVec, out meterHPBarTxre);
    }

    private void GenerateMeterSubObjs(in float[] inputMeterRootObjPosArr, in float[] inputMeterBarPosArr,
        string inputFilePathName, out Vector2 outputMeterRootObjPosVec, out Vector2 outputMeterBarPosVec,
        out Texture outputMeterBarTxre)
    {
        outputMeterRootObjPosVec = new Vector2(inputMeterRootObjPosArr[0], inputMeterRootObjPosArr[1]);

        outputMeterBarPosVec = new Vector2(inputMeterBarPosArr[0], inputMeterBarPosArr[1]);
        outputMeterBarTxre = Resources.Load<Texture>(inputFilePathName);
    }

    private void GetMeterUIComponents()
    {
        GetMeterUIRectsAndRImgs(in healthMeterTestNext, out healthMeterTestNextRect, out healthMeterTestNextRImg);
        GetMeterUIRectsAndRImgs(in healthMeterTestNextBG, out healthMeterTestNextBGRect, out healthMeterTestNextBGRImg);
        GetMeterUIRectsAndRImgs(in healthMeterBar, out healthMeterBarRect, out healthMeterBarRImg);
        GetMeterUIRectsAndRImgs(in healthMeterTestNextCover, out healthMeterTestNextCoverRect, out healthMeterTestNextCoverRImg);
    }

    private void GetMeterUIRectsAndRImgs(in GameObject inputGameObj, out RectTransform outputRect, out RawImage outputRImg)
    {
        outputRect = inputGameObj.GetComponent<RectTransform>();
        outputRImg = inputGameObj.GetComponent<RawImage>();
    }

    private void SetMeterUIObjects()
    {
        SetMeterUIComponentValues(healthMeterTestNextRect, healthMeterTestNextRImg, in meterHPRootObjPosVec,
            in meterRootAndCoverSizeVec, null, in meterRootClr);

        SetMeterUIComponentValues(healthMeterTestNextBGRect, healthMeterTestNextBGRImg, in meterBGPosVec,
            in meterBGAndBarSizeVec, meterBGTxre, in meterObjClr);

        SetMeterUIComponentValues(healthMeterBarRect, healthMeterBarRImg, in meterHPBarPosVec,
            in meterBGAndBarSizeVec, meterHPBarTxre, in meterObjClr);

        SetMeterUIComponentValues(healthMeterTestNextCoverRect, healthMeterTestNextCoverRImg, in meterCoverPosVec,
            in meterRootAndCoverSizeVec, meterCoverTxre, in meterObjClr);
    }

    private void SetMeterUIComponentValues(RectTransform outputRect, RawImage outputRImg, in Vector2 inputPosVec,
        in Vector2 inputSizeVec, in Texture inputTxre, in Color inputClr)
    {
        outputRect.anchorMin = meterMinAnchrVec;
        outputRect.anchorMax = meterMaxAnchrVec;
        outputRect.pivot = meterPivVec;
        outputRect.anchoredPosition = inputPosVec;
        outputRect.sizeDelta = inputSizeVec;

        outputRImg.texture = inputTxre;
        outputRImg.color = inputClr;
        outputRImg.raycastTarget = bCanRaycastTarget;
    }

    private void CleanUpIrrelevantData()
    {
        CleanupHelper.CollectGarbage();

        CleanupHelper.EmptyObj(rootAssetPath);
        CleanupHelper.EmptyObj(rootTexturePath);

        CleanupMeterCompShared();

        CleanupMeterGenericShared();

        CleanupMeterSubObjects(dataSheetMeterHealthFileName, meterHPRootObjPosArr, meterHPBarPosArr, meterHPBarTxre,
            meterHPBarTxreFileName);

        CleanupHealthMeterGameObjects();

        CleanupHelper.CollectGarbage();
    }

    private void CleanupHealthMeterGameObjects()
    {
        CleanupMeterGameObjects(healthMeterTestNextName, healthMeterTestNext, healthMeterTestNextRect,
                    healthMeterTestNextRImg);

        CleanupMeterGameObjects(healthMeterTestNextBGName, healthMeterTestNextBG, healthMeterTestNextBGRect,
                    healthMeterTestNextBGRImg);

        CleanupMeterGameObjects(healthMeterBarName, healthMeterBar, healthMeterBarRect,
                    healthMeterBarRImg);

        CleanupMeterGameObjects(healthMeterTestNextCoverName, healthMeterTestNextCover, healthMeterTestNextCoverRect,
                    healthMeterTestNextCoverRImg);
    }

    private void CleanupMeterCompShared()
    {
        CleanupHelper.EmptyObj(dataSheetMeterCompSharedFileName);
        CleanupHelper.EmptyObj(meterRootAndCoverSizeArr);
        CleanupHelper.EmptyObj(meterBGAndBarSizeArr);
        CleanupHelper.EmptyObj(meterBGPosArr);
        CleanupHelper.EmptyObj(meterBGTxre);
        CleanupHelper.EmptyObj(meterBGTxreFileName);
        CleanupHelper.EmptyObj(meterCoverPosArr);
        CleanupHelper.EmptyObj(meterCoverTxre);
        CleanupHelper.EmptyObj(meterCoverTxreFileName);
    }

    private void CleanupMeterGenericShared()
    {
        CleanupHelper.EmptyObj(dataSheetMeterGenericSharedFileName);
        CleanupHelper.EmptyObj(meterMinAnchrArr);
        CleanupHelper.EmptyObj(meterMaxAnchrArr);
        CleanupHelper.EmptyObj(meterPivArr);
        CleanupHelper.EmptyObj(meterScleArr);
        CleanupHelper.EmptyObj(meterRootClrArr);
        CleanupHelper.EmptyObj(meterObjClrArr);
    }

    private void CleanupMeterSubObjects(string targetDataSheetFileName, float[] targetRootObjPosArr, float[] targetBarPosArr,
        Texture targetBarTxre, string targetBarTxreFileName)
    {
        CleanupHelper.EmptyObj(targetDataSheetFileName);
        CleanupHelper.EmptyObj(targetRootObjPosArr);
        CleanupHelper.EmptyObj(targetBarPosArr);
        CleanupHelper.EmptyObj(targetBarTxre);
        CleanupHelper.EmptyObj(targetBarTxreFileName);
    }

    private void CleanupMeterGameObjects(string targetGameObjName, GameObject targetGameObj,
        RectTransform targetGameObjRect, RawImage targetGameObjRImg)
    {
        CleanupHelper.EmptyObj(targetGameObjName);
        CleanupHelper.EmptyObj(targetGameObj);
        CleanupHelper.EmptyObj(targetGameObjRect);
        CleanupHelper.EmptyObj(targetGameObjRImg);
    }
}
using System;
using UnityEngine;

public class CleanupHelper : MonoBehaviour
{
    public static void CollectGarbage()
    {
        GC.Collect();
    }

    public static void EmptyObj(object targetObj)
    {
        targetObj = null;
    }
}

It's late... I just wrapped up a long day of work... I can't be f'd to read the DataSetterUI right now.

BUT... CleanupHelper I can read and it's all of 15 lines. And ummm...

CollectGarbage - this method just does a single thing, and that is call System.GC.Collect. Why not just call System.GC.Collect when you need to collect GC??? Why have a method that wraps that? It isn't like you've abstracted out the concept (like say created a polymorphic class that has different ways of collecting garbage, and this is the simple implementation). It's a static method! It's redundant at this point!

EmptyObj - this is not doing what you think it's doing. Again it has the problem that CollectGarbage has in that it's doing a single line of code, why not just do that single line in line rather than calling this method? But worst off is that it literally is not doing what you think it is. You just set the local variable 'targetObj' null, that's it. Back in DataSetterUI anywhere you call this the thing is still there, it's not null! 'rootAssetPath' and 'rootTexturePath' at lines 244 and 245 are still what they were when you were at 242 (also... you collected GC before attempting to set something null???).

This technically could have worked if you used a 'ref' parameter on your EmptyObj method like so:

public static void EmptyObj(ref object targetObj) => targetObj = null;

This would actually set the variable that was passed in by ref, rather than just the local variable targetObj.

But still.... what's the point?

At line 244 instead of saying:

CleanupHelper.EmptyObj(rootAssetPath);

You could just:

rootAssetPath = null;

Honestly... it's shorter!

CleanupHelper - also, why is this class a MonoBehaviour? It only contains some static methods. You only need to inherit from MonoBehaviour if you're creating components that you attach to GameObjects.

...

I think before you start trying to wrap your head around GC... I think you need to first wrap your head around how variables work.

2 Likes

Organizational purposes, as I intend to put all functions pertaining to “cleanup” under one roof.

Same reasoning as above for why I did it the way I did. You’re right about the parameter not actually being set to null, however; only now is IntelliSense picking up on it, oddly enough.

Yes, I was calling the GC before attempting to set something to null. I wasn’t sure if that was the appropriate course of action, hence my posting the code and asking about it in the first place.

Tried that, kept getting build errors. What worked was making the function return an object and then setting the variable I intend to empty to this function variation while casting it.

However, that all involves more steps than simply setting it to null, as you suggested, and the organization benefits of my static class are eroded when compared to those accrued by doing it all on one line.

There are Unity-specific functions I intend to include down the line, also for similar purposes. Now that my IntelliSense is working, I can say that preliminary results look promising.

Asked and answered, already addressed.

I’m coming back to this after about a month of inactivity, so bear with me. Besides, you’re not the only one coming back from an arduous day of work and then looking at code while partaking in somnambulism.

You got build errors because you call ‘ref’ methods a specific way:

CleanupHelper.EmptyObj(ref rootAssetPath);

Did you do this? Please don’t tell me you did this?

public static object EmptyObj(object targetObj)
{
    targetObj = null;
    return targetObj;
}

And then call it as:

rootAssetPath = CleanupHelper.EmptyObj(targetObj);

Because if you did… you literally just added more effort to doing:

rootAssetPath = null;

Cause that’s all that’s going on there. You’re setting rootAssetPath to null. You’re just getting null in a long roundabout way!

OK, good.

I just meant I wasn’t going to take my time and spend it reading literally hundreds of lines of code.

I’m going to put it this way… your thread is titled: “JSON Parsing - Garbage Collection and Coding best practices?”

None of what I talked about is best practices. This cleanuphelper class inheriting from MonoBehaviour despite not needing to be with methods that do work that could have been done in a single line in line… not best practices. Hyper fixating on garbage collection when you don’t understand how variables work because intellisense didn’t tell you, not best practices.

In school when a math exam would happen invariably kids would ask if they could use a calculator. And the teacher would say no. The reason being, if they let you use the calculator you will come to rely on the calculator.

Intellisense is your calculator right now.

And I’ll end it with one more anecdote.

When my father trained me and my brother to do various tasks from plumbing, to electric, to truck driving. My brother would always complain about the “long way” to do things and insist that he KNOWS there’s a shorter way cause he sees dad do them. My dad would respond:

“First you need to learn the right way, then you learn the short cuts. This way when the short cut inevitably fails, you have the right way to fall back on.”

You need to learn the right way to do things before you go into this whole blindly adding stuff that you think is organizing your code, when it’s not. Adding in layers of marshmallow (useless sugary junk) to deal with GC when the GC is dealt with for you. We call C# a managed language for you, it’s cause C# manages your memory for a reason. That’s what ‘garbage’ is… garbage isn’t bad. Garbage is just what we call the stuff that the garbage collector cleans up for us on our behalf as opposed to us having to do it ourselves like you must do in say C/C++.

1 Like

Just to make that clear, you have the same issue with those methods:

    private void CleanupMeterGameObjects(string targetGameObjName, GameObject targetGameObj,
        RectTransform targetGameObjRect, RawImage targetGameObjRImg)
    {
        CleanupHelper.EmptyObj(targetGameObjName);
        CleanupHelper.EmptyObj(targetGameObj);
        CleanupHelper.EmptyObj(targetGameObjRect);
        CleanupHelper.EmptyObj(targetGameObjRImg);
    }

So replacing your “EmptyObj” calls with setting those variables to null just won’t do anything at all. So I’m with @lordofduct here:

Those are really fundamental things about C#. I think when it comes to variable passing during method calls you really have to be careful because there seems to be a really large group (maybe even the majority) who get this wrong and even teach that wrong, even on StackOverflow.

Some argue that value types are passed by value and reference types are passed by reference. This is WRONG. The type of the variable has no influence on the way it is PASSED to a method. Variables are ALWAYS passed by value, even reference types. What most people don’t seem to understand what the “value” or the content of a variable actually is. The actual value stored in a variable for values types is of course the actual value. However the value of a reference type is “the reference”. This is an actual value. Even though managed references are not the same as pointers, you can think of them that way. So the value is just an address / a number that “can be used” to refer to some object that is located elsewhere. When you pass a reference type variable to a method, that reference is COPIED into the local argument variable of the method like all other variables. Of course that reference can be used to reach out to the same referenced object and manipulate that object through that reference. However the variable that was passed to the method got not passed by reference.

Passing by reference means that we do not copy the value of a variable onto the stack (this is where method arguments live) but instead the actual memory address OF THE VARIABLE is stored on the stack. That means the local variable inside the method becomes an actual alias for the same memory location where the original variable is stored that holds the reference to that managed object. That means assigning a value to such a variable will actually change the content / value of the variable that was passed in. As our lord has already explained, passing by reference requires the argument to be a ref (or out) parameter and when you call the method you actually have to use the ref keyword.

Note that ref-parameters can ONLY accept VARIABLES. So it’s not possible to just pass a value to it. Two examples, one value type and one reference type example

void MyMethod1(int val)
{
    val = 42;
}

void MyMethod2(ref int val)
{
    val = 42;
}

int i = 5;

MyMethod1( i );
Debug.Log(i); // prints 5

MyMethod2( ref i );
Debug.Log( i ); // prints 42

MyMethod1( 12345 ); // valid
MyMethod2( ref 12345 ) // invalid, we can only pass variables

The same example with a reference type

void MyMethod1(SomeClass val)
{
    val = new SomeClass();
}

void MyMethod2(ref SomeClass val)
{
    val = new SomeClass();
}

SomeClass c = null;

MyMethod1( c );
Debug.Log( c ); // prints null

MyMethod2( ref c );
Debug.Log( c ); // prints "SomeClass"

MyMethod1( new SomeClass() ); // valid
MyMethod2(ref new SomeClass() ) // invalid, we can only pass variables

Hopefully this clears up some confusion. Note that in the second case, when we call MyMethod1, the method actually creates a new instance of SomeClass and stores it in it’s local variable “val”. However as soon as the method returns, this local variable will be removed and the newly created instance will be up for garbage collection because no variable is referencing this class anymore. Of course the content of variable “c” is not affected by this in any way since it’s value got just “copied” into val when the method was called.

In the case(s) of MyMethod2, the variable val is a “ref parameter”. It’s essentially an actual pointer to the memory location where the passed variable is stored. So changes to the content of val, directly affects the content of the passed variable “c”.

Also as you can see, you can not pass an object instance directly to a ref parameter since a ref parameter requires an actual variable, not just a “reference” to an object. Because ref paramters are actual direct links to the variable location, you can not “store” a ref argument itself for later use because that memory location may no longer exist at a later point in time. That’s also why coroutines / IEnumerators can not have ref parameters. Think of this example:

void MyMethod2(ref int val)
{
    val = 42;
}

void Caller()
{
    int i = 0;
    MyMethod2(ref i);
    Debug,Log( i ); // prints 42
}

Caller();
// variable "i" doesn't exist anymore at this point since it was a local variable inside the Caller method.
4 Likes

Thank you @Bunny83 , this morning I wanted to get into basically what you’re saying about how variables actually work, but I just didn’t have the brain energy.

1 Like

Too hot as well? :slight_smile: I’m literally melting…

1 Like

I personally enjoyed being sunburned today.

It's difficult to script nowadays. Especially in the heat. I developed some stress white in my hair, so I seriously cut down my hours to maybe 4 hours a week. I barely enjoy those hours as well

But I lurk the forum still, for inspiration. Reading content. Habit I guess. Maybe come winter.. mm I just can't be bothered anymore. It's very difficult, feels like slavery sometimes.

As I said, there’s more to my code that I intend to add later, and said code to be added can only be implemented via inheriting from MonoBehaviour as it depends on its functions.

I take it from your comments then that if I do want to manually call the GC, I should do so after de-referencing/nulling all variables? Just want to make sure.

For knowing how variables work, in addition to parameter modifiers, I feel I understand quite a bit: Primitives like ints are pass-by value, and any changes made to them within the scope of a function are not reflected within the rest of the program upon completion of function execution. Objects are pass-by reference, and thus changes made to them within a function’s scope that takes them as parameters are reflected outside the function’s scope. Additionally, objects point to memory locations, and there can be multiple references to the same memory location, which also means changes made to one reference is reflected among all references within the program.

Parameter modifiers, naturally, modify the parameters: Out and Ref both make a parameter pass-by reference, so changes made to it within the function’s scope are reflected outside of it - very useful if you want the return type to be void and don’t want to deal with setting an object or a primitive variable to another, and potentially deal with an expensive copy.

Ref and In demand that a parameter already be instantiated, Out does not. However, Out does demand that the parameter with the Out modifier gets modified within the function’s scope, with nothing blocking this modification, on a compile-time level. Parameter modification using Ref is optional, however. In makes a parameter read-only, and doesn’t allow for the modification of the parameter using it to be modified within the function - however, changes made to a parameter’s data members, if said parameter is using In, will be reflected outside the function’s scope.

By all means, do tell if I missed anything.

As you can see, keeping the knowledge of how objects are pass-by reference in mind, it just seemed like EmptyObj() would work. I’ve also never written such a function before, as I lacked a use case, hence my missteps.

I’m concerned about garbage collection because I don’t want objects in memory that I only need temporarily as middle-man DTOs hogging resources that could be used for more substantial purposes, hence why I need to know how to best call the GC early on just to free this memory, and before run-time as well. I don’t want to rely on the GC to do this automatically because I want the game to be as smooth and to have as few lag spikes as possible due to the GC running. I noticed that this even occurs with far more professional games like Furi, also made with Unity, and it seems to be the cause.

As far as CleanupHelper needing to have different ways of calling the GC or other such functionality to warrant its existence, that will be the case later on anyway, and I want to have organization as far in advance as possible in my project so that my game’s development goes smoothly.

Don’t get the wrong idea: This isn’t my first time programming, so I’m not starting from Square 1. However, there is room for me to improve, and there will be slip-ups on my end that I intend to fix. I am hear to learn and improve through my efforts.

Thank you, this was very helpful; I learned quite a bit more about parameter modifiers through it.

The main thing I’d like to add, and this is only based on a hunch, I think you may not have a clear concept between the variable and a value/reference.

So when we say something like “reference type” the object is referenced. So:

void DoAThing(SomeRefType obj)
{
    obj.name = "Changed Name";
}

This modifies the object that was passed in. Because your local variable ‘obj’ references the object in question. This is opposed to:

void DoAThing(SomeRefType obj)
{
    obj = new SomeRefType();
    obj.name = "Changed Name";
}

In this case we created a new SomeRefType locally. The “new” is the new object. We recycled the parameter variable to do so. The variable is independent of the variable.

Of course this recycling of the parameter would be weird… what’s the point of the parameter if you’re going to overwrite it immediately. But it’s technically legal C#.

You can actually create new objects without assigning them to variables:

(new List<int>()).Add(5);

This creates a list, adds a value to it, and immediately it’s lost to the either.

In the case of a List you likely wouldn’t do this very often because why create a list to just not use it? But sometimes you might actually do this. For instance maybe create a serializer to deserialize something inline:

var obj = (new Serializer()).Deserialize(somedata);

Where the ‘ref’ modifier moves the reference to the variable itself:

void FooA()
{
    int i = 5;
    Debug.Log(i); //prints 5
    FooB(ref i);
    Debug.Log(i); //prints 6
}

void FooB(ref int value)
{
    value += 1;
}

See variables are basically slots in which a value/object can be stored. That slot has a scope, may that be the function it’s declared in, or the class it’s defined in as a field (making it a part of the object instanced from the class). The type of the variable defines how the variable will behave. If it’s a “value type” then the variable slot itself is the value, the memory foot print of the variable is the size of that type, and where that variable is declared is where that value will exist. So in the case of a basic function that variable and therefore its value will exist “on the stack”. If it was declared as a field of a class, then it’ll exist directly as value inside the object on the heap.

But, when the variable is of a reference type. The variable really is just a pointer at the object. It holds the address of the object where that object sits on the heap. When you say:

object a = new object();
object b = a; //this right here

In that line what you’re saying is:

  1. declare a variable of type object, meaning that this variable is just an address for ‘objects’.
  2. set that address stored in b to the same address that is stored in a.

So… now if we change the above to:

void FooA()
{
    List<int> lst = new List<int>();
    lst.Add(5);
    Debug.Log(lst.Count); //prints 1
    FooB(ref lst);
    Debug.Log(lst.Count); //prints 0
}

void FooB(ref List<int> lstref)
{
    lstref = new List<int>();
}

What we’re doing here is passing a reference to the memory location of ‘lst’… not to the List, but to the variable ‘lst’ itself. When that variable was a value… it basically was like you didn’t copy the value. But now that the variable is a reference type, it’s a little weird. Sure you have a reference to the List as well, but that’s just because you have a reference to the address of ‘lst’ and that happens to point at the List. By doing this we can either modify the List by accessing it (saying calling ‘Add’ on it), OR we could modify the variable itself by setting it.

When you allocate objects (ref types) they’re placed on the heap. The system has a fair amount of heap memory allocated. The .net runtime will allocate more heap than you actually need. This is actually why if you run your profiler you might see something like:
9116383--1264633--upload_2023-6-30_11-7-59.png

Note how the “Managed Heap” is labeled as:
Managed Heap (In Use/Reserved) 383.3 / 458.6 MB

It’s because the runtime as reserved (asked the OS to allocate) a large swath of memory, but we’ve only filled that swath about 2/3’s of the way.

This is where your ref objects are going when instanced. Including your DTOs.

Now what will happen is that when you need to create a new object on the heap the runtime will look at its reserved memory to find a place in it to put your new object. If it determines it can’t quickly find space for it, it’ll attempt to “clean up” the heap. This cleaning up is “garbage collection”. It will automatically look through the heap, find all objects that aren’t being referenced, and mark that section of RAM available to be for creating your object. (it gets a bit more complicated then that with generational gc, but I want to keep it simple right now… this is the jist of it)

GC doesn’t really deallocate the RAM. Honestly it very seldom will do that. If you’ve created a super large object (say a massive array) the memory manager may hold onto that memory as reserved for quite some time because it assumes you may need it again in the future. And requesting to allocate RAM from the OS is slower than just keeping it reserved. Sure, it’ll relinquish it sometime in the future, but you don’t really get a say in it.

This is why it’s called “managed”… you don’t have to manage it, .Net manages it for you!

The best you got is that System.GC class, but it doesn’t necessarily give you full control. It basically allows you to ask the memory manager to do some things for you rather than automatically do them. You are still bound by the rules of how the memory manager behaves.

Calling GC.Collect is useful in one major way.

Lets say you performed a task that consumes massive amounts of the heap. You’ve created big ol’ arrays (I’m talking BIG), and you filled every slot of those arrays with tons of objects (not ints, objects… ref type objects). You’ve cluttered up that heap with a bunch of junk. And now that you’re done, you’ve exited that work, there are no references left (either the locally scoped variables are null, or the function where all of it is has reached its end and returned). And you know from profiling that the next time GC hits that this thing is going to take a solid 5 frames to complete causing a huge frame drop in your gameplay.

This is a problem… that 5 frame drop might cause your fast paced game feel clunky for a split moment! And we don’t want that happening during some break neck moment like a boss fight. But since the memory manager/GC will do its thing when it gets to it, and the the memory manager/GC doesn’t understand the concept of a “break neck boss fight”, there is a possibility this will happen during your boss fight and ruin the player experience.

Thing is when you did all that work… it was expensive. Just creating all those objects takes up time. So you probably coded all of that during the loading sequence of said boss fight. Maybe you’re all flashing WARNING on the screen ala Sh’mup tradition:
9116383--1264651--upload_2023-6-30_11-24-59.jpeg
(Radiant Silvergun, if you haven’t played it, play it)

Well we already created a bunch of slow down for this MASSIVE job you did. Now is a good time to toss a GC.Collect on the end to force the memory manager to clean everything up to avoid the potential frame drop later.

Note this hasn’t avoided the frame drop. The frame drop is STILL going to happen.

You just decided WHEN it would happen during a controlled moment.

Here’s the thing though… if there is no frame drop ever because your GC call was minor. Guess what… you didn’t need to call GC.Collect because you used what is a normal amount of memory for a normal amount of objects. And the memory manager will clean it up in due time with very little sweat and you won’t even notice it.

Cause your memory manager is doing it all the time while your game runs! You’re creating garbage constantly. That call to ‘GetComponents’ (note the s)? FindObjectsOfType? Starting a coroutine, or a Task? System.Linq and iterator functions? These all take up little bits of the heap and are relinquished once they’re done.

Cause I mean… what are these DTOs of yours? How big are they really?

Do you know how big 100 Vector3’s are? Say you had a Vector3[ ] of length 100. It’s 1,200 bytes (give or take some header bytes). 1.2kb.

Go back up and look at my pic of the profiler… I had 383.3 megs of a reserved 458.6. I doubt 1.2k is going to impact that all that much.

We’re not writing games that run on the NES with 2 whole kilobytes of work RAM. We’re writing games whose lowest end is a cellphone with GIGABYTES of RAM.

Your DTO is a spec of sand on the beaches that is your games memory usage.

If you’re needing to resort to calling GC.Collect in a controlled setting. You’ve just done some intense work, and you’ve used the profiler to its ends to determine what it is exactly, and you’re mitigating a design that can’t be avoided.

Basically if you got to that point you’re either A) a master programmer doing some insane stuff or B) a naive programmer who has no idea how to optimize their code… and is doing some insane stuff.

But that’s not organization.

Building a box to put air so you can have a box with air in it when air is all around you at all times is… redundant.

Like literally… that is what System.GC is. It’s the box, it’s the box with the air. You just call System.GC. Why call CleanupHelper when System.GC is right there!?

Sometimes I’ll be shopping with my wife and I’ll be grabbing items and putting them in the cart and I’ll ask her to grab the lemon juice. And she just looks at me like “why would I grab the lemon juice” and I look down and sure enough the lemon juice is in front of me. She would literally have to reach around me to get to the lemon juice. And so… I grab the lemon juice myself.

That’s what you’re doing… you’re having your wife (CleanupHelper.CollectGarbage) take the long way around to pick up the lemon juice (call GC.Collect) when you could have just picked it up yourself (call GC.Collect instead of call CleanupHelper.CollectGarbage).

That’s awesome, you’re wanting to learn.

But… again… your post title was “Garbage Collection and Coding best practices?”.

And when it was suggested that the way you went about writing it is not best practices… your response was basically “but I wanna do it this way!”

If I wanted to run electric out to my barn/garage, and I did it against code, then I went to an electrician and said “hey, so I want to learn how to this correctly” and the electrician said “well for starters you used the wrong conduit, and the gauge of your wire isn’t thick enough for the amperage you want to push”… it’d be weird for me to go “WELL… I’m learning here, and this is the gauge and conduit I want to use.”

You asked the electrician how to do it. And the electrician just pointed out what you’re doing wrong. The electrician is literally just doing what you asked.

1 Like

TLDR;

[quote]
I'm concerned about garbage collection because I don't want objects in memory that I only need temporarily as middle-man DTOs hogging resources that could be used for more substantial purposes, hence why I need to know how to best call the GC early on just to free this memory, and before run-time as well
[/quote]

You don't need to be concerned.

GC will be called by the memory manager when it needs to be called. That's the whole point of a managed memory system. It does it for you. This isn't C/C++ where you need to manage it all yourself (a so-called 'unmanaged' situation).

The only time you should ever need to call GC.Collect manually is if you've witnessed a poor experience in your game, ran the profiler, and determined that the garbage generated by a very large job is causing a huge stall for GC later on at times that is not optimal for the user experience.

Cause mind you... calling GC.Collect will still cause the stutter. It's just WHEN the stutter occurs gets moved.

If you wanted to get rid of it all together... you'd have to not generate garbage in the first place. Which is also a thing many people get hyper focused on. And well these days Unity has a better garbage collector than it used to. It's not as major of a worry unless you're creating garbage non-stop (like say during Update you habitually create lots of arrays).

1 Like

100% I raise my hand as the type of person to get hyper focused on micro-optimization, despite the convential wisdom. I have spent more time than I care to admit experimenting on things related to garbage collection. At the end of it all, I would probably never call GC.Collect(). Although, I never say never. My thinking is, don’t generate the garbage in the first place. Everything else should take care of itself.

That being said, you can’t and should not try to eliminate absolutely every source of memory allocation for later garbage collection. The best thing I gained in the end is more knowledge, and now I happily allocate memory for the garbage collector when I know it’s something small, event based (not occuring constantly in the game loop), and doing it any other way would just create more inconvenience. The best tool is simply knowing when it does and does not occur. If you can find a constant stream of garbage allocation, which shouldn’t be any trouble with a bit of profiling, you should be able to plug those up. Other than that, a little garbage here and there is natural, and the garbage collector is quite amazing at what it does.

2 Likes