런타임에 타입을 확인하여 최적의 알고리즘을 사용하라

 

제네릭 타입을 사용하면 개발자 입장에서 적은 코드양에 비해 많은 기능을 얻을 수 있다.

하지만 제네릭 타입을 사용하여 제한 조건을 추가하는 순간부터 런타임 중에 원치 않는 형 변환(Up Casting, Down Casting)을 시도하여 성능을 저하시킬 수도 있도 있고, 특정 최적화된 타입을 쓰지 못할 수도 있다. 따라서 최적의 성능을 원할 경우에는 제네릭 타입보다는 개발자가 직접 구현하는 것이 좋을 때도 있다. 아래 예시 코드로 시작하겠다.

 

public sealed class ReverseEnumerable<T> : IEnumerable<T>
{
    private class ReverseEnumerator : IEnumerator<T>
    {
        int currentindex;
        IList<T> collection;
        
        public ReverseEnumerator(IList<T> srcCollection)
        {
            collection = srcCollection;
            currentindex = collection.Count;
        }
        
        public T Current => collection[currentindex];
        
        public void Dispose()
        {
        	//IEnumerator<T>는 IDisposable을 상속받고 있다.
        }
        
        //IEnumerator<T>의 멤버들
        object System.Collections.IEnumerator.Current => this.Current;
        public bool MoveNext() => --currentIndex >= 0;
        public void Reset() => currentIndex = collection.Count;
    }
    
    IEnumerable<T> sourceSequence;
    IList<T> originalSequence;
    
    public ReverseEnumerable(IEnumerable<T> sequence)
    {
    	sourceSequence = sequence;
    }
    
    public IEnumerator<T> GetEnumerator()
    {
    	if(originalSequence == null)
        {
            originalSequence = new IList<T>();
            foreach(T item in sourceSequence)
                originalSequence.Add(item);
        }
        
        return new ReverseEnumerator(originalSequence);
    }
    
   	//IEnumerable<T>의 멤버
    System.Collections.IEnumerator
    	System.Collections.IEnumerable.GetEnumerator() =>
            this.GetEnumerator();
}

이 Class의 기능은 IEnumerable<T>를 상속받는 컬렉션을 매개변수로 입력 후 역순환 컬렉션을 반환하는 Class 이다.

자 우선 GetEnumerator()의 함수에서 originalSequence를 복사하는 동작이 거슬린다.

 

1. 생성자에서 originalSequence를 변환 시켜서 저장한다.

   - 만약 변환을 실패해도 기존의 null 처리가 되어있기 때문에 괜찮다.


    public ReverseEnumerable(IEnumerable<T> sequence)
    {
    	sourceSequence = sequence;
        
        //만약 sequence가 IList<T>가 구현되지 않았다면 기존의 복사동작이 이루어진다.
        //즉 성능 최적화 부분에 대해서 가능성을 열어 둔 부분이다.
        originalSequence = sequence as IList<T>;
    }

2. IList<T>자체를 받아오는 생성자를 오버로드한다.

  - 아시다시피 IList<T>는 IEnumerable<T>를 상속받기 때문에 초기 Downcasting으로 저장 시킨다. 

    public ReverseEnumerable(IEnumerable<T> sequence)
    {
    	sourceSequence = sequence;
        
        //만약 sequence가 IList<T>가 구현되지 않았다면 기존의 복사동작이 이루어진다.
        //즉 성능 최적화 부분에 대해서 가능성을 열어 둔 부분이다.
        originalSequence = sequence as IList<T>;
    }
    
     public ReverseEnumerable(IList<T> sequence)
    {
    	sourceSequence = sequence;
        originalSequence = sequence;
    }

3. sourceSequence에 IList<T> 타입이  들어 갈경우 ICollection<T> 성질을 이용하여 최적화를 추가 할 수 있다.

    - 컬렉션의 사이즈를 미리 지정하여 Add의 로드를 줄인다.

public IEnumerator<T> GetEnumerator()
{
    if (originalSequence == null)
    {
        //sourceSequence가 IList<T>를 받아온 경우 가능한 루프
        if (sourceSequence is ICollection<T>)
        {
            ICollection<T> source = sourceSequence as ICollection<T>;
            
            /**
            //컬렉션의 사이즈를 미리 지정한다.
            //기본적으로 가변형 컬렉션은 삽입, 삭제 시 사용하는 메모리를 가변 시키는 로드가 있으므로 
            //미리 할당 시켜놓고 사용하면 전자 보다 빠르다
            **/
            originalSequence = new List<T>(source.Count);
        }
        else
            originalSequence = new List<T>();


        foreach (T item in sourceSequence)
            originalSequence.Add(item);
    }

    return new ReverseEnumerator(originalSequence);
}

 

최종 코드

public sealed class ReverseEnumerable<T> : IEnumerable<T>
{
    private class ReverseEnumerator : IEnumerator<T>
    {
        int currrentIndex;
        IList<T> collection;

        public ReverseEnumerator(IList<T> srcCollection)
        {
            collection = srcCollection;
            currrentIndex = srcCollection.Count;
        }

        public T Current => collection[currrentIndex];

        public void Dispose()
        {
                
        }

        object IEnumerator.Current => this.Current;

        public bool MoveNext()
            => --currrentIndex >= 0;

        public void Reset()
            => currrentIndex = collection.Count;
    }

       
    IEnumerable<T> sourceSequence;
    IList<T> originalSequence;

    public ReverseEnumerable(IEnumerable<T> sequence)
    {
        sourceSequence = sequence;

        originalSequence = sequence as IList<T>;
    }

    public ReverseEnumerable(IList<T> sequence)
    {
        sourceSequence = sequence;

        originalSequence = sequence;
    }


    public IEnumerator<T> GetEnumerator()
    {
        if (originalSequence == null)
        {
            if (sourceSequence is ICollection<T>)
            {
                originalSequence = new List<T>(source.Count);
            }
            else
                originalSequence = new List<T>();


        foreach (T item in sourceSequence)
            originalSequence.Add(item);
    }

        return new ReverseEnumerator(originalSequence);
    }

    IEnumerator IEnumerable.GetEnumerator()
        => this.GetEnumerator();
}

위의 코드는 매개변수에 대한 테스트가 모두 런타임에 진행 된다. 즉 개발자가 유의깊게 잘 개발해야한다.

나중에 TDD 관련 공부도 해야겠다. 

 

결론

1. 제네릭의 제한조건에 너무 의존하지말자.

2. 런타임 최적화도 고려해보자

3. 런타임 최적화 방식은 런타임의 타입을 확인하여 처리하는 코드를 추가한다.

4. 제한 조건 vs 직접 코딩 상황에 따라 개발

 

'Program > Effective C#' 카테고리의 다른 글

Effective C# ITEM21  (0) 2020.11.26
Effective C# ITEM20  (0) 2020.11.25
Effective C# ITEM 18  (1) 2020.11.15
Effective C# ITEM 17  (0) 2020.11.08
Effective C# ITEM16  (0) 2020.11.05

+ Recent posts