logo MSJO.kr

Free the memory used by a collection

2025-01-24
MsJ
 

C# .NET은 Managed Code이다. .NET의 구성요소인 CLR(Common Language Runtime)에서 실행되는 코드 유형이다. 상대적으로 Unmanaged Code는 CLR의 개입 없이 운영체제나 하드웨어에서 직접 실행되는 코드 유형이다. C#보다 머신 수준에 더 가까운 C, C++ 와 같은 언어로 작성된다.1 Managed Code인 C#은 런타임 환경에서 자동으로 메모리 관리(GC, Garbage Collection)를 관리한다.

IDisposable을 구현하는 개체를 사용할 때 using StreamReader와 같이 사용하면 함수의 블록이 끝날 때 자동으로 메모리에서 개체를 메모리에서 해제한다.2 간혹 개발자 중에 직접 메모리를 해제하고자 할 때 아래와 같이 코드를 작성하곤 하는 데 이러한 방법은 GC를 오해한 까닭에서 나온 결과이다.3

개선 전 코드
public static void ClearMemory<T>(this List<T> list)
{
    int id = GC.GetGeneration(list);
    list.Clear();
    GC.Collect(id, GCCollectionMode.Forced);
}

GC.Collect()를 호출하면 가비지 컬렉션이 실행될 가능성을 높이지만 Unmanaged 언어와 같이 바로 메모리가 반환되는 것을 보장하지 않는다. 즉, 힌트를 준 정도의 역할만 할 뿐이고 특히 GC를 자주 호출하면 CPU 시간을 소모하는 작업이기 때문에 오히려 성능에 악영향을 미칠 수 있다.

아래의 예제는 권장하는 코드와 함께 공부 차원에서 직접 작성한 몇 가지 클래스이다.

권장하는 코드
public static void ClearAll<T>(this List<T> list)  
{  
    list.Clear();  
    list.Capacity = 0;  
}

위에서처럼 Clear()를 사용하여 리스트가 차지하고 있던 메모리 공간을 가비지 컬렉션의 대상이 되게 한다. 그리고 Capacity = 0을 적용하여 리스트가 내부적으로 가지고 있는 배열의 메모리도 가비지 컬렉션의 대상이 되게 한다. 닷넷에서는 이렇게 하여 자동으로 GC가 일어나도록 힌트를 줄 수 있다. 나머지는 .NET이 알아서 자동 처리한다.

확장 함수 구현

Disposable한 컬렉션은 DisposeAll()을 사용하고 그렇지 않은 개체는 ClearAll()을 사용한다.

public static class DisposableExtensions  
{  
    public static void DisposeAll<T>(this List<T> list) where T : IDisposable  
    {  
        list.ForIn(x => x.Dispose());  
    }  
  
    public static void DisposeAll<T>(this T[] array) where T : IDisposable  
    {  
        array.ForIn(x => x.Dispose());  
    }  
  
    public static void DisposeAll<T>(this IEnumerable<T> collection) where T : IDisposable  
    {  
        collection.ForIn(x => x.Dispose());  
    }  
  
    public static void ClearAll<T>(this List<T> list)  
    {  
        list.Clear();  
        list.Capacity = 0;  
    }  
  
    private static void ForIn<T>(this IEnumerable<T>? seq, Action<T> act) where T : IDisposable  
    {  
        if (seq == null)  
        {  
            return;  
        }  
  
        List<Exception> exceptions = [];  
  
        foreach (T item in seq)  
        {  
            try  
            {  
                act(item);  
            }  
            catch (Exception ex)  
            {  
                exceptions.Add(ex);  
            }  
        }  
  
        if (exceptions.Count > 0)  
        {  
            throw new AggregateException($"Disposing Item ERROR : {exceptions}");  
        }  
    }  
}
IDisposable 패턴 적용
public sealed class DisposeList<T> : List<T>, IDisposable  
{  
    private bool mDisposedValue;  
  
    private void Dispose(bool disposing)  
    {  
        if (mDisposedValue)  
        {  
            return;  
        }  
  
        if (disposing)  
        {  
            foreach (IDisposable item in this.OfType<IDisposable>())  
            {  
                try  
                {  
                    item.Dispose();  
                }  
                catch (Exception ex)  
                {  
                    LogHelper.Logger.Error($"Disposing Item ERROR : {ex.Message}");  
                }  
            }  
  
            Clear();  
            Capacity = 0;  
        }  
  
        mDisposedValue = true;  
    }  
  
    ~DisposeList() => Dispose(false);  
  
    public void Dispose()  
    {  
        Dispose(disposing: true);  
        GC.SuppressFinalize(this);  
    }  
}
public sealed class DisposeArray<T>(uint initialLength = 0) : IDisposable  
{  
    private T[]? mArray = initialLength > 0 ? new T[initialLength] : null;  
    private bool mDisposedValue;  
  
    public T this[int index]  
    {  
        get  
        {  
            ObjectDisposedException.ThrowIf(mDisposedValue, nameof(DisposeArray<T>));  
  
            if (mArray == null || index < 0 || index >= Count)  
            {  
                throw new IndexOutOfRangeException();  
            }  
  
            return mArray[index];  
        }  
        set  
        {  
            ObjectDisposedException.ThrowIf(mDisposedValue, nameof(DisposeArray<T>));  
  
            if (index < 0)  
            {  
                throw new IndexOutOfRangeException();  
            }  
  
            if (mArray == null)  
            {  
                mArray = new T[Math.Max(index + 1, 4)];  
            }  
            else if (index >= mArray.Length)  
            {  
                Array.Resize(ref mArray, Math.Max(index + 1, mArray.Length * 2));  
            }  
  
            mArray[index] = value;  
            Count = Math.Max(Count, index + 1);  
        }  
    }  
  
    public int Count { get; private set; }  
  
    public int Capacity  
    {  
        get => mArray?.Length ?? 0;  
    }  
  
    public T[] ToArray()  
    {  
        ObjectDisposedException.ThrowIf(mDisposedValue, nameof(DisposeArray<T>));  
  
        if (mArray == null || Count == 0)  
        {  
            return [];  
        }  
  
        T[] newArray = new T[Count];  
        Array.Copy(mArray, newArray, Count);  
        return newArray;  
    }  
  
    public void Clear()  
    {  
        ObjectDisposedException.ThrowIf(mDisposedValue, nameof(DisposeArray<T>));  
  
        if (mArray != null)  
        {  
            Array.Clear(mArray, 0, mArray.Length);  
        }  
  
        Count = 0;  
    }  
  
    private void Dispose(bool disposing)  
    {  
        if (mDisposedValue) return;  
  
        if (disposing)  
        {  
            if (typeof(IDisposable).IsAssignableFrom(typeof(T)) && mArray != null)  
            {  
                List<Exception> exceptions = [];  
  
                foreach (IDisposable item in mArray.OfType<IDisposable>())  
                {  
                    try  
                    {  
                        item.Dispose();  
                    }  
                    catch (Exception ex)  
                    {  
                        exceptions.Add(ex);  
                    }  
                }  
  
                if (exceptions.Count != 0)  
                {  
                    throw new AggregateException($"Disposing Item ERROR : {exceptions}");  
                }  
            }  
            else  
            {  
                mArray = null;  
            }  
        }  
  
        mDisposedValue = true;  
    }  
  
    ~DisposeArray() => Dispose(false);  
  
    public void Dispose()  
    {  
        Dispose(true);  
        GC.SuppressFinalize(this);  
    }  
}
// 사용 예제
using DisposeList<string> comList = [];  
using DisposeArray<string> comArray = new(10); //완벽하지 않지만 동적 배열 가능

위의 클래스는 ‘확장 함수 구현’에서 보여준 것을 IDisposable 패턴을 적용하여 자동으로 처리하도록 구현하였다. 굳이 이렇게 사용할 필요는 없고 Clear(), Capacity = 0으로 충분하며, 당연하지만 StreamReader, Network의 Connection과 같이 IDisposable이 적용된 개체는 using을 사용해야 한다.

Reference
  1. c-sharpcorner.com, “Managed vs. Unmanaged Code in .NET”
  2. C++에서 smart pointer를 사용하고, Rust 언어에서는 언어 수준 자체의 기능이다.
  3. stackoverflow.com, “How free memory used by a large list in C#?, Answer:Oswaldo Junior”

Prεv(Θld)  
Content
Search     RSS Feed     BY-NC-ND