ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [.NET] IEnumable에 대한 성능팁
    .NET/개념 및 유용한 팁 2021. 6. 5. 16:31
    반응형

    매일 수신받는 medium 에서 IEnumable에 대한 유용한 글을 수신받았다. 번역기가 있어 어느정도 이해할 순 있지만 내용이 좀 길어 짧게 정리할 겸 포스트를 쓴다.

    1. Count() != 0 보단 Any()를 써라

    종종 배열(IEnumable)에 요소가 있는지 여부를 사용할 때, Count() != 0 조건을 사용하는 경우가 많다. 그러나 Count()는 각 요소의 개수를 구하기 위해 아래와 같은 구현으로 동작한다.

    public static int Count<T>(this IEnumerable<T> enumerable) 
    {
        var count = 0;
        using(var enumerator = enumerable.GetEnumerator())
        {
            while (enumerator.MoveNext())
                count++;
        }
        return count;
    }

    배열이 10개면 10번의 while 반복문이, 100개면 100번의 while 반복문이 동작하게 되는데 n개 마다 반복수행하므로, big-O 표기법에 의해 O(n) 만큼의 작업이 소요된다.

     

    반면 Any의 경우 아래와 같이 동작한다.

    public static bool Any<T>(this IEnumerable<T> enumerable) 
    {
      using (var enumerator = enumerable.GetEnumerator())
      {
          return enumerator.MoveNext();
      }
    }

     

    MoveNext() 호출시 다음 배열이 없다면 false를 반환한다. 빈 배열이라면 false를 반환하게 되며, 배열에 요소가 1개라도 있다면 true를 반환한다. 배열요소가 몇개인지 확인할 필요는 없으므로 O(1) 만큼 동작한다.

     

    2. Count()와 Count 둘다 사용할 수 있다면, Count를 사용하라

    IReadOnlyCollection 은 IEnumable 인터페이스에서 Count 속성이 추가된 파생 인터페이스이며, List와 같은 제네릭(Generic) 배열형식은 IEnumable, IReadOnlyCollection 모두 파생받은 구현체이다. 따라서 내부적으로 요소의 변형이 있을 때 Count 속성값을 수정하게 된다.

     

    앞서 IEnumable에 정의된 Count() 메소드가 O(n)만큼 소요된다고 했었다. Count는 속성값이므로 O(1)만큼 소요된다. 따라서 List와 같이 Count 속성이 존재하는 요소에 대해선 Count() 메소드 보다 Count를 사용해주는 것이 성능면에서 유리하다.

     

    3. yield

    IEnumable 유형의 객체를 return 할때, 컬렉션 전체를 메모리에 할당하는것을 방지하기위해 yield 를 사용하는 경우가 있다. 아래 예제는 start와 count 값을 받아 start 부터 count 까지 +1씩 더해진 컬렉션 유형을 반환하는 메소드이다.

     

    public static IEnumerable<int> MyRange(int start, int count)
    {
        if (count < 0)
            throw new ArgumentOutOfRangeException(nameof(count));
    
        var end = start + count;
        for (int value = start; value < end; value++)
        {
            yield return value;
        }
    }

     

    메소드엔 count 값이 음수이면 ArgumentOutofRangeException이 발생하도록 처리되어 있다. 소스상 count 값이 음수이면 바로 예외발생할 듯 하지만, 실질적으론 발생하지 않고 반환된 IEnumerable 를 사용하는 시점에서 발생하게 된다.

     

    이를 해결하려면 local method를 사용해 yield return 부분을 분리해야 한다.

    public static IEnumerable<int> MyRange(int start, int count)
    {
        if (count < 0)
            throw new ArgumentOutOfRangeException(nameof(count));
    
        var end = start + count;
        return RangeEnumeration();
    
        // local function 
        IEnumerable<int> RangeEnumeration()
        {
            for (var value = start; value < end; value++)
            {
                yield return value;
            }
        }
    }

     

    4. ToList() 나 ToArray()를 남발하지 말라

    ToList나 ToArray 함수는 컬렉션을 새로운 컬렉션 변수로 깊은 복사(DeepCopy) 해주는 메소드이다. 예제 소스와 같이 특정 컬렉션에서 각 조건(Where)에 따른 결과를 받고 싶다면 LINQ 재사용(reused) 특성을 이용해 처리하도록 하자.

    var numbers = Enumerable.Range(start, count);
            
    var evenNumbers = numbers
        .Where(number => (number & 0x01) == 0);
    
    var oddNumbers = numbers
        .Where(number => (number & 0x01) != 0);
    
    Console.WriteLine("Even Numbers:");
    foreach(var number in evenNumbers)
        Console.WriteLine(number);
    
    Console.WriteLine("Odd Numbers:");
    foreach(var number in oddNumbers)
        Console.WriteLine(number);

     

    5. null과 Enumerable.Empty를 구분하여 사용하라

    컬렉션을 반환하는 메소드에서 예외처리를 위해 try catch 후 return 값을 null로 반환하는 경우가 있다. null 사용하면 해당 메소드를 사용하는 로직에서 결과값 null 여부에 따른 별도의 동작을 구현해야할 수 있다. 일반적으로 컬렉션 변수를 사용하면 foreach와 같은 동작이 수반되므로 null 반환시 오류가 발생할 수 있다.

     

    empty를 사용하면 후속작업인 foreach에서도 오류가 발생하지 않으며, 요소가 없어 foreach 작업이 수행되지 않으므로 Empty의 사용을 권장한다.

     

    yield의 경우 yield break를 사용하는 것을 권장한다.

     

    출처 : Enumeration in .NET. This is part of a series of articles: | by Antão Almada | Noteworthy - The Journal Blog (usejournal.com)

     

    반응형

    댓글

Designed by Tistory.