IT_Programming/C#

[펌] using / foreach와 try / catch 확장

JJun ™ 2008. 12. 18. 02:30

원본 출처: http://network.hanb.co.kr/view.php?bi_id=979

 

using 문의 try/catch 확장
using과 foreach 문을 사용하게 되면 IL 코드로 변환될 때 try/catch 구조로 변환된다.

예를 들어, C# 명세서 8.13 the using statement에 설명된 것을 보자.

using (ResourceType resource = expression) statement

C# 명세서에 따르면 위와 같은 코드에서 ResourceType이 값 타입(value type)이면

다음과 같은 형태로 변환된다.

{
   ResourceType resource = expression;
   try {
      statement;
   }
   finally {
      ((IDisposable)resource).Dispose();
   }
}

반면에, ResourceType이 참조 타입(reference type)이면 다음과 같다.

{
   ResourceType resource = expression;
   try {
      statement;
   }
   finally {
      if (resource != null) ((IDisposable)resource).Dispose();
   }
}

foreach문의 try/catch 확장
C# 명세 8.3.3 foreach statement를 살펴보면

foreach 문에서 사용하는 컬렉션에 따라 그 확장이 달라지게 된다.

foreach (ElementType element in collection)

이와 같은 코드가 있다고 할 때 컬렉션의 형식이 컬렉션 패턴을 구현하고 있다면 다음과 같이 확장된다.

E enumerator = (collection).GetEnumerator();
try {
   while (enumerator.MoveNext()) {
      ElementType element = (ElementType)enumerator.Current;
      statement;
   }
}
finally {
   IDisposable disposable = enumerator as System.IDisposable;
   if (disposable != null) disposable.Dispose();
}

컬렉션 패턴을 구현하지 않고 IEnumerable을 구현하는 경우에 foreach 확장은 다음과 같이 된다.

IEnumerator enumerator =
       ((System.Collections.IEnumerable)(collection)).GetEnumerator();
try {
   while (enumerator.MoveNext()) {
      ElementType element = (ElementType)enumerator.Current;
      statement;
   }
}
finally {
   IDisposable disposable = enumerator as System.IDisposable;
   if (disposable != null) disposable.Dispose();
}

컬렉션 패턴에 대해서는 C# 스펙 8.3.3 foreach statement를 참고하기 바란다.

 

 


foreach와 수행 성능의 관계
Rico Mariani가 지적한 것처럼 foreach를 사용하는 방법과 전통적인 for 루프를 사용한

Knuth 스타일을 비교해보자.

radParser.cs
using System;
using System.IO;
class RadParser
{
   static void Main(String[] args)
   {
      StreamReader stream = new StreamReader(args[0]);
      String line;
      Char[] delims = { ' ', '\t' };
      int sum = 0;
      while ((line = stream.ReadLine()) != null)
      {
         String[] fields = line.Split(delims);
         foreach (String field in fields)
         {
               sum += Int32.Parse(field);
         }
      }
      Console.WriteLine("The total of the ints is: {0,8:n0}", sum);
   }
}

classicParser.cs
using System;
using System.IO;
class ClassicParser
{
   static void Main(String[] args)
   {
      FileStream fs = new FileStream(args[0], FileMode.Open, FileAccess.Read);
      Byte[] bytes = new Byte[4096];
      int sum = 0;
      int tmp = 0;
      bool negative = false;
      bool digits = false;
      for (;;)
      {
         int count = fs.Read(bytes, 0, bytes.Length);
         if (count <= 0)
         {
               if (negative)
                  sum -= tmp;
               else
                  sum += tmp;
               break;
         }
         for (int i = 0; i < count; i++)
         {
               switch ((char)bytes[i])
               {
               case ' ': case '\t': case '\r': case '\n':
                  if (negative)
                     sum -= tmp;
                  else
                     sum += tmp;
                  negative = false;
                  digits = false;
                  tmp = 0;
                  break;
               case '-':
                  if (negative)
                     throw new FormatException("two negatives");
                  if (digits)
                     throw new FormatException("negative after digits");
                  negative = true;
                  break;
               case '0': case '1': case '2': case '3': case '4':
               case '5': case '6': case '7': case '8': case '9':
                  digits = true;
                  tmp = tmp*10 + bytes[i] - (Byte)'0';
                  break;
               default:
                  throw new FormatException("invalid character");
               }
         }
      }
      Console.WriteLine("The total of the ints is: {0,8:n0}", sum);
   }
}

입력 데이터
234 2348 -7295 27?52932 -2352 2352 252532 1 34 452 52 345 2 245 4525

위 숫자를 2500라인이 들어간 입력 파일로 작성한다.

이와 같은 Rico Mariani의 두 가지 파서에 대한 비교 결과를 살펴보면 radParser의 수행시간이

1.092s로 classicParser의 수행시간 0.149에 비해 거의 7배 가량 느리다는 것을 알 수 있다.

foreach와 for와에 대한 다양한 논의는 Rico Mariani의 블로그에서 확인할 수 있으니 참고자료를

확인하기 바란다.

물론, 수행속도의 차이에 대한 해답도 Rico의 블로그에 있지만 간단히 정리하자면,

수행속도가 느려지는 이유는 foreach문의 try/catch 확장 때문이다. 해당 foreach마다 그에 해당하는 예외를 위한 스택이 쌓이게 된다. 마찬가지로, 비결정적인 가비지 컬렉터에 의한 가비지 컬렉션도 상당한 비용을

발시킨다.

결국, 효율적인 응용 프로그램을 구축하기 위해서는 foreach, using문의 예외 확장과 예외에 들어가는 비용을 알고 있어야한다. 이에 대해서는 The cost of exception을 확인하기 바란다.

일반적으로 할 수 있는 이야기는 C++과 같은 비관리(unmanaged) 환경과 C#/Java와 같은 관리(managed) 환경에 따라 예외에 따른 비용이 다르다는 것이다. 예외가 발생하지 않는 경우에 try/finally 블록을 사용했을 때 발생하는 비용은 상대적으로 낮은 것이어서 문제가 되지 않지만, 예외를 발생한 경우 예외를 throw하는 비용이 더 높다는 것이다.

예외를 throw하는 경우에 대한 비용 문제에 대해서도 여러가지 논쟁들이 있다. Java에서 처럼 코드의 이곳저곳에서 생성되는 예외 생성 비용을 줄이기 위해 정적 예외(static exception)을 쓰는 방법도 있으나, 예외가 발생되는 시점에서만 알 수 있는 정보를 전달하지 못하는 문제가 있으며 매우 잘못된 패턴이라는 것이다.


배열과 foreach 문의 확장
Mark Michaelis가 설명한 코드를 인용하겠습니다. 그에 따르면 ArrayList를 사용하는 경우와 배열을 사용하는 경우에 foreach의 확장이 다르다는 것을 알 수 있다. 만약, 독자가 직접 눈으로 확인하고 싶다면 간단한

 예제를 작성하고 컴파일한후 ildasm.exe로 IL 코드를 직접 살펴보면 된다.

ArrayList array = new ArrayList();
...
foreach (int i in array)
{
   Console.WriteLine(i);
}

위와 같은 foreach문은 다음과 같이 확장된다.

ArrayList array;
int number;
IEnumerator enumerator;
IDisposable disposable;

array = new ArrayList();
...
enumerator = array.GetEnumerator();
try
{
   while (enumerator.MoveNext())
   {
      number = ((int)enumerator.Current);
      Console.WriteLine(number);
   }
}
finally
{
   disposable = (enumerator as IDisposable);
   if (disposable != null)
   {
      disposable.Dispose();
   }
}

즉, ArrayList 클래스는 C# 스펙 8.3.3에 설명된 것처럼 컬렉션 패턴을 따르고 있으므로

이에 따라 IL 코드가 확장되는 것을 확인할 수 있다. foreach에 배열이 사용된 경우를 살펴보자.

int[] array =
array = new int[]{1, 2, 3, 4, 5, 6};

foreach (int i in array)
{
   Console.WriteLine(i);
}

이러한 형태는 다음과 같이 확장된다.

int number;
int[] tempArray;
int[] array = new int[]{1, 2, 3, 4, 5, 6};

tempArray = array;
for (int counter = 0; (counter < tempArray.Length); counter++)
{
Console.WriteLine(tempArray[counter]);
}

여기서 알 수 있는 것처럼 배열에 대한 foreach는 for로 확장되는 것을 알 수 있다.

따라서, 경우에 따라 최적화를 수행하기 위해 ArrayList 또는 컬렉션 패턴을 정의한 객체를 이용할 것인가

배열을 이용할 것인가를 결정해야 한다.

Mark는 이 외에 C# Generic에서의 최적화에 대해서도 언급하고 있으므로 그의 블로그를 확인하기 바란다.

결론
foreach/using 키워드의 사용은 try/catch로 확장된다. 예외란 프로그래밍 환경에서 비싼 객체에 속한다.

예외를 언제 생성하는 것이 좋은가? 예외는 어떨때 써야하는가에 대한 문제가 여러분을 괴롭힐 것이고,

내게도 여전히 괴로운 문제다.

When to create exception objectsException Cose: When to throw and when not to를 살펴보는 것이

좋을 것 같다.

개인적으로 예외에 대한 조언을 하자면 다음과 같다. 물론, 절대적인 규칙은 아니다.

1. 프로그래머가 예상할 수 있는 문제에 대해서는 예외를 사용하지 않는다.
2. 함수에 전달되는 매개변수에 잘못된 인자가 전달되는 경우를 처리하기 위해 예외를 사용한다.
3. 사용자 입력에 대해서는 예외를 사용하는 대신 직접 에러를 처리하는 코드를 작성한다.

3번과 관련해 부연하자면 예외란 에러가 아니다. 잘못된 생각중에 하나는 예외 = 에러라는 공식일 것이다. 1번과 관련해서는 예상할 수 없는 경우에 예외를 사용해야 한다. 1번은 주로 클래스 디자이너와 밀접한 관계가 있다. 이에 대해서는 Applied .Net Framework Programming의 18장 Exceptions를 살펴보기 바란다. 예외와 관련된 첨예한 문제들에 대해서 나의 경우 많은 도움을 받았고, 필요할 때 마다 다시 보는 부분이다.

Applied .Net Framework Programming에서는 FCL에서 예외를 잡아버림으로 외부로 예외를 전달하지 못하는 FCL의 문제점까지 지적하고 있다. 한가지 더 추가하자면, finally에 return을 작성하면 그 어떤 예외도 반환되지 않는다. - 가끔, 그런 분도 있다.

예외로도 이야기하기에 상당히 많은 주제들이 관련되어 있다는 것을 알 수 있을 것이다.

아마, 여기에 내가 알고 있는 것은 하나도 없는 듯 하다. 다만, 늘 익힌 지식들을 사용하거나 다른 사람에게 이야기하는 경우가 대부분인 듯하다. 그래서, 모든 것은 유용한 이야기를 쓴 사람들 덕분이다.

참고자료

Performance Quiz #3
Performance Quiz #3 - Solution
foreach and performance rules
The cost of exception
C# foreach with array
Mother/Nanny Pattern for C#
When to create exception objects
Implementing a Dispose Method
C# Language Specification 8.13. the using statement
C# Language Specification 8.8.4 the foreach statement
C# foreach with arrays
The Trouble with Checked Exceptions: A conversation with Anders Hejlsverg