.NET 애플리케이션에서의 성능 팁과 요량

 

엠마누엘 샨저
Microsoft Corporation

2001년 8월

요약: 이 문서는 관리되는 환경에서 최적의 성능을 위해 애플리케이션을 조정하려는 개발자를 위한 것입니다. 샘플 코드, 설명 및 디자인 지침은 데이터베이스, Windows Forms 및 ASP 애플리케이션뿐만 아니라 Microsoft Visual Basic 및 관리되는 C++에 대한 언어별 팁에 대해 다룹니다. (인쇄된 25페이지)

콘텐츠

개요
모든 애플리케이션에 대한 성능 팁
데이터베이스 액세스에 대한 팁
ASP.NET 애플리케이션에 대한 성능 팁
Visual Basic에서 포팅 및 개발을 위한 팁
관리되는 C++에서 포팅 및 개발을 위한 팁
추가 리소스
부록: 가상 호출 및 할당 비용

개요

이 백서는 .NET용 애플리케이션을 작성하고 성능을 개선하는 다양한 방법을 찾는 개발자를 위한 참조로 설계되었습니다. .NET을 접하는 개발자인 경우 플랫폼과 선택한 언어를 모두 잘 알고 있어야 합니다. 이 논문은 그 지식을 엄격하게 토대로 작성되었으며 프로그래머가 프로그램을 실행하기에 충분한 것을 이미 알고 있다고 가정합니다. 기존 애플리케이션을 .NET으로 이식하는 경우 포트를 시작하기 전에 이 문서를 읽어보는 것이 좋습니다. 여기에 있는 몇 가지 팁은 디자인 단계에서 유용하며 포트를 시작하기 전에 알아야 할 정보를 제공합니다.

이 문서는 프로젝트 및 개발자 유형별로 구성된 팁과 함께 세그먼트로 나뉩니다. 팁의 첫 번째 세트는 모든 언어로 작성하기 위해 반드시 읽어야 하며 CLR(공용 언어 런타임)의 대상 언어에 도움이 되는 조언이 포함되어 있습니다. 관련 섹션은 ASP 관련 팁을 따릅니다. 두 번째 팁 세트는 관리형 C++ 및 Microsoft® Visual Basic 사용에 대한 특정 팁을 다루는 언어별로 구성됩니다®.

일정 제한으로 인해 버전 1(v1) 런타임은 가장 광범위한 기능을 먼저 대상으로 지정한 다음 나중에 특수 사례 최적화를 처리해야 했습니다. 이로 인해 성능이 문제가 되는 몇 가지 비둘기 구멍 사례가 발생합니다. 따라서 이 문서에서는 이 사례를 방지하도록 설계된 몇 가지 팁을 다룹니다. 이러한 팁은 체계적으로 식별되고 최적화되므로 다음 버전(vNext)에서는 관련이 없습니다. 나는 우리가 갈 때 그들을 지적 할 것이다, 그것은 노력의 가치가 있는지 여부를 결정하는 것은 당신에게 달려 있습니다.

모든 애플리케이션에 대한 성능 팁

CLR을 모든 언어로 작업할 때 기억해야 할 몇 가지 팁이 있습니다. 이는 모든 사용자와 관련이 있으며 성능 문제를 처리할 때 첫 번째 방어선이어야 합니다.

더 적은 예외 throw

예외를 throw하는 것은 매우 비용이 많이 들 수 있으므로 예외를 많이 throw하지 않도록 합니다. Perfmon을 사용하여 애플리케이션에서 throw하는 예외 수를 확인합니다. 애플리케이션의 특정 영역이 예상보다 더 많은 예외를 throw하는 것을 발견하면 놀랄 수 있습니다. 세분성을 높이기 위해 성능 카운터를 사용하여 프로그래밍 방식으로 예외 번호를 검사 수도 있습니다.

예외가 많은 코드를 찾아 디자인하면 적절한 성능이 발생할 수 있습니다. try/catch 블록과는 아무런 관련이 없습니다. 실제 예외가 throw될 때만 비용이 발생합니다. 원하는 만큼 try/catch 블록을 사용할 수 있습니다. 예외를 무상으로 사용하면 성능이 저하됩니다. 예를 들어 제어 흐름에 예외를 사용하는 것과 같은 작업을 피해야 합니다.

다음은 비용이 많이 드는 예외의 간단한 예입니다. For 루프를 통해 실행하여 수천 개 또는 예외를 생성한 다음 종료합니다. throw 문을 주석으로 처리하여 속도 차이를 확인해 보세요. 이러한 예외로 인해 엄청난 오버헤드가 발생합니다.

public static void Main(string[] args){
  int j = 0;
  for(int i = 0; i < 10000; i++){
    try{   
      j = i;
      throw new System.Exception();
    } catch {}
  }
  System.Console.Write(j);
  return;   
}
  • 주의! 런타임은 자체적으로 예외를 throw할 수 있습니다. 예를 들어 Response.Redirect()ThreadAbort 예외를 throw합니다. 예외를 명시적으로 throw하지 않더라도 이를 수행하는 함수를 사용할 수 있습니다. Perfmon을 검사 실제 스토리를 얻고 디버거가 원본을 검사 있는지 확인합니다.
  • Visual Basic 개발자에게: Visual Basic은 기본적으로 int 검사를 켜고 오버플로 및 0으로 나누기 같은 항목이 예외를 throw하는지 확인합니다. 성능을 얻기 위해 이 기능을 해제할 수 있습니다.
  • COM을 사용하는 경우 HRESULTS가 예외로 반환할 수 있음을 명심해야 합니다. 이러한 항목을 주의 깊게 추적해야 합니다.

청키 호출

청키 호출은 개체의 여러 필드를 초기화하는 메서드와 같은 여러 작업을 수행하는 함수 호출입니다. 이는 매우 간단한 작업을 수행하고 여러 번 호출하여 작업을 완료해야 하는 번잡한 호출에 대해 볼 수 있습니다(예: 다른 호출을 사용하여 개체의 모든 필드를 설정하는 등). 간단한 AppDomain 내 메서드 호출보다 오버헤드가 높은 메서드 간에 수다스러운 호출보다는 두툼한 호출을 하는 것이 중요합니다. P/Invoke, interop 및 remoting 호출은 모두 오버헤드를 수행하며, 이를 아끼는 데 사용하려고 합니다. 이러한 각 경우에 오버헤드가 너무 많은 작고 빈번한 호출에 의존하지 않도록 애플리케이션을 디자인해야 합니다.

관리 코드가 관리되지 않는 코드에서 호출되고 그 반대의 경우도 마찬가지일 때마다 전환이 발생합니다. 런타임을 사용하면 프로그래머가 interop을 매우 쉽게 수행할 수 있지만 성능 가격이 책정됩니다. 전환이 발생하면 다음 단계를 수행해야 합니다.

  • 데이터 마샬링 수행
  • 호출 규칙 수정
  • 호출 수신자 저장 레지스터 보호
  • GC가 관리되지 않는 스레드를 차단하지 않도록 스레드 모드 전환
  • 관리 코드로 호출 시 예외 처리 프레임 설정
  • 스레드 제어(선택 사항)

전환 시간을 단축하려면 가능한 경우 P/Invoke를 사용하세요. 오버헤드는 31개의 지침과 데이터 마샬링이 필요한 경우 마샬링 비용이 적고, 그렇지 않으면 8개에 불과합니다. COM interop은 훨씬 더 비싸며 65 개 이상의 지침을 취합니다.

데이터 마샬링이 항상 비용이 많이 드는 것은 아닙니다. 기본 형식은 마샬링이 거의 필요하지 않으며 명시적 레이아웃이 있는 클래스도 저렴합니다. 실제 속도 저하는 ASCI에서 유니코드로의 텍스트 변환과 같은 데이터 변환 중에 발생합니다. 관리되는 경계를 넘어 전달되는 데이터가 필요한 경우에만 변환되는지 확인합니다. 프로그램 전체에서 특정 데이터 형식 또는 형식에 동의하면 많은 마샬링 오버헤드를 줄일 수 있습니다.

다음 형식을 blittable이라고 합니다. 즉, sbyte, byte, short, ushort, int, uint, long, ulong, float 및 double 형식을 마샬링하지 않고 관리/관리되지 않는 경계를 통해 직접 복사할 수 있습니다. Blittable 형식을 포함하는 ValueType 및 1차원 배열뿐만 아니라 무료로 전달할 수 있습니다. 마샬링의 세부 정보는 MSDN 라이브러리에서 자세히 탐색할 수 있습니다. 마샬링하는 데 많은 시간을 할애하는 경우 주의 깊게 읽는 것이 좋습니다.

ValueTypes를 사용하여 디자인

할 수 있을 때와 boxing 및 unboxing을 많이 하지 않을 때 간단한 구조체를 사용합니다. 다음은 속도 차이를 보여 주는 간단한 예제입니다.

using System;

네임스페이스 콘솔애플리케이션{

  public struct foo{
    public foo(double arg){ this.y = arg; }
    public double y;
  }
  public class bar{
    public bar(double arg){ this.y = arg; }
    public double y;
  }
  class Class1{
    static void Main(string[] args){
      System.Console.WriteLine("starting struct loop...");
      for(int i = 0; i < 50000000; i++)
      {foo test = new foo(3.14);}
      System.Console.WriteLine("struct loop complete. 
                                starting object loop...");
      for(int i = 0; i < 50000000; i++)
      {bar test2 = new bar(3.14); }
      System.Console.WriteLine("All done");
    }
  }
}

이 예제를 실행하면 구조체 루프가 크기가 더 빠른 순서임을 알 수 있습니다. 그러나 ValueType을 개체처럼 취급할 때는 ValueType을 사용하는 것을 조심해야 합니다. 이렇게 하면 프로그램에 복싱과 언박싱 오버헤드가 추가되며, 개체를 고집한 경우보다 많은 비용이 들 수 있습니다. 이 동작을 확인하려면 위의 코드를 수정하여 foos 및 막대 배열을 사용합니다. 성능이 다소 동일하다는 것을 알 수 있습니다.

장단점 ValueType은 개체보다 훨씬 덜 유연하며 잘못 사용하면 성능이 저하됩니다. 언제 어떻게 사용하는지에 대해 매우 주의해야 합니다.

위의 샘플을 수정하고 배열 또는 해시 테이블 내에 foos 및 막대를 저장해 보세요. 번의 복싱 및 언박싱 작업으로 속도 증가가 사라지는 것을 볼 수 있습니다.

GC 할당 및 컬렉션을 확인하여 상자 및 언박스의 사용 빈도를 추적할 수 있습니다. 이 작업은 코드에서 Perfmon 외부 또는 성능 카운터를 사용하여 수행할 수 있습니다.

.NET Framework Run-Time 기술의 성능 고려 사항에서 ValueTypes에 대한 자세한 설명을 참조하세요.

AddRange를 사용하여 그룹 추가

컬렉션의 각 항목을 반복적으로 추가하는 대신 AddRange 를 사용하여 전체 컬렉션을 추가합니다. 거의 모든 창 컨트롤과 컬렉션에는 AddAddRange 메서드가 모두 있으며 각각은 다른 용도로 최적화됩니다. 추가 는 단일 항목을 추가하는 데 유용하지만 AddRange 에는 약간의 추가 오버헤드가 있지만 여러 항목을 추가할 때 우선 적용됩니다. 다음은 AddAddRange를 지원하는 몇 가지 클래스입니다.

  • StringCollection, TraceCollection 등
  • HttpWebRequest
  • UserControl
  • ColumnHeader

작업 집합 자르기

작업 집합을 작게 유지하는 데 사용하는 어셈블리 수를 최소화합니다. 하나의 메서드를 사용하기 위해 전체 어셈블리를 로드하는 경우 아주 적은 혜택으로 엄청난 비용을 지불하게 됩니다. 이미 로드한 코드를 사용하여 해당 메서드의 기능을 복제할 수 있는지 확인합니다.

작업 세트를 추적하는 것은 어렵고 전체 논문의 주제가 될 수 있습니다. 다음은 도움이 되는 몇 가지 팁입니다.

  • vadump.exe 사용하여 작업 집합을 추적합니다. 관리되는 환경에 대한 다양한 도구를 다루는 또 다른 백서에서 설명합니다.
  • Perfmon 또는 성능 카운터를 살펴봅니다. 로드하는 클래스 수 또는 JITed를 가져오는 메서드 수에 대한 자세한 피드백을 제공할 수 있습니다. 로더에 소요되는 시간 또는 페이징에 소요되는 실행 시간의 백분율에 대한 읽기를 얻을 수 있습니다.

문자열 반복에 For 루프 사용 - 버전 1

C#에서 foreach 키워드(keyword) 사용하면 목록, 문자열 등의 항목을 검색하고 각 항목에 대한 작업을 수행할 수 있습니다. 이 도구는 여러 형식에 대한 범용 열거자 역할을 하므로 매우 강력한 도구입니다. 이 일반화의 단점은 속도이며 문자열 반복에 크게 의존하는 경우 For 루프를 대신 사용해야 합니다. 문자열은 단순 문자 배열이므로 다른 구조체보다 훨씬 적은 오버헤드를 사용하여 걸을 수 있습니다. JIT는 For 루프 내에서 경계 검사 및 기타 작업을 최적화할 수 있을 만큼 스마트하지만 foreach 워크에서는 이 작업을 수행할 수 없습니다. 최종 결과는 버전 1에서 문자열의 For 루프가 foreach를 사용하는 것보다 최대 5배 더 빠릅니다. 이는 이후 버전에서 변경되지만 버전 1의 경우 성능을 향상시키는 확실한 방법입니다.

속도의 차이를 보여 주는 간단한 테스트 방법은 다음과 같습니다. 실행한 다음 For 루프를 제거하고 foreach 문의 주석 처리를 제거합니다. 내 컴퓨터에서 For 루프는 foreach 문에 대해 약 3초가 걸렸습니다.

public static void Main(string[] args) {
  string s = "monkeys!";
  int dummy = 0;

  System.Text.StringBuilder sb = new System.Text.StringBuilder(s);
  for(int i = 0; i < 1000000; i++)
    sb.Append(s);
  s = sb.ToString();
  //foreach (char c in s) dummy++;
  for (int i = 0; i < 1000000; i++)
    dummy++;
  return;   
  }
}

절충Foreach 는 훨씬 더 읽을 수 있으며, 미래에는 문자열과 같은 특수 사례에 대한 For 루프만큼 빠를 것입니다. 문자열 조작이 실제 성능 돼지가 아니라면 약간 더 복잡한 코드는 가치가 없을 수 있습니다.

복합 문자열 조작에 StringBuilder 사용

문자열이 수정되면 런타임에서 새 문자열을 만들고 반환하여 원래 문자열을 가비지 수집되도록 합니다. 대부분의 경우 이 작업을 수행하는 빠르고 간단한 방법이지만 문자열이 반복적으로 수정되면 성능에 부담이 되기 시작합니다. 이러한 할당은 모두 결국 비용이 많이 듭니다. 다음은 문자열에 50,000번 추가한 다음 StringBuilder 개체를 사용하여 문자열을 수정하는 프로그램의 간단한 예입니다. StringBuilder 코드는 훨씬 빠르며 실행하면 즉시 명확해집니다.

namespace ConsoleApplication1.Feedback{
  using System;
  
  public class Feedback{
    public Feedback(){
      text = "You have ordered: \n";
    }
    public string text;
    public static int Main(string[] args) {
      Feedback test = new Feedback();
      String str = test.text;
      for(int i=0;i<50000;i++){
        str = str + "blue_toothbrush";
      }
      System.Console.Out.WriteLine("done");
      return 0;
    }
  }
}
namespace ConsoleApplication1.Feedback{
  using System;
  public class Feedback{
    public Feedback(){
      text = "You have ordered: \n";
    }
    public string text;
    public static int Main(string[] args) {
      Feedback test = new Feedback();
      System.Text.StringBuilder SB = 
        new System.Text.StringBuilder(test.text);
      for(int i=0;i<50000;i++){
        SB.Append("blue_toothbrush");
      }
      System.Console.Out.WriteLine("done");
      return 0;
    }
  }
}

수천 개의 문자열을 할당하지 않고 얼마나 많은 시간이 저장되는지 확인하려면 Perfmon을 살펴보십시오. .NET CLR 메모리 목록 아래의 "%time in GC" 카운터를 살펴봅니다. 저장한 할당 수와 컬렉션 통계를 추적할 수도 있습니다.

절충= 시간 및 메모리 모두에서 StringBuilder 개체를 만드는 것과 관련된 오버헤드가 있습니다. 메모리가 빠른 컴퓨터에서는 약 5개의 작업을 수행하는 경우 StringBuilder 가 가치가 있습니다. 엄지 손가락의 규칙으로, 나는 10 개 이상의 문자열 작업이 어떤 컴퓨터에 오버 헤드에 대한 근거라고 말할 것이다, 심지어 느린 하나.

애플리케이션 Windows Forms 사전 컴파일

메서드는 처음 사용될 때 JITed됩니다. 즉, 애플리케이션이 시작 중에 많은 메서드 호출을 수행하는 경우 더 큰 시작 페널티를 지불합니다. Windows Forms OS에서 많은 공유 라이브러리를 사용하며 시작 시 오버헤드가 다른 종류의 애플리케이션보다 훨씬 높을 수 있습니다. 항상 그렇지는 않지만 Windows Forms 애플리케이션을 미리 컴파일하면 일반적으로 성능이 향상됩니다. 다른 시나리오에서는 일반적으로 JIT가 이를 처리하도록 하는 것이 가장 좋지만, Windows Forms 개발자인 경우 살펴볼 수 있습니다.

Microsoft를 사용하면 를 호출 ngen.exe하여 애플리케이션을 미리 컴파일할 수 있습니다. 설치 시간 동안 또는 애플리케이션을 배포하기 전에 ngen.exe 실행하도록 선택할 수 있습니다. 설치하는 컴퓨터에 애플리케이션이 최적화되어 있는지 확인할 수 있으므로 설치 시간 동안 ngen.exe 실행하는 것이 가장 좋습니다. 프로그램을 배송하기 전에 ngen.exe 실행하는 경우 최적화를 컴퓨터에서 사용할 수 있는 최적화로 제한 합니다 . 미리 컴파일하는 것이 얼마나 도움이 될 수 있는지에 대한 아이디어를 제공하기 위해 컴퓨터에서 비공식 테스트를 실행했습니다. 다음은 약 100개의 컨트롤이 있는 winforms 애플리케이션인 ShowFormComplex의 콜드 시작 시간입니다.

코드 상태 Time
프레임워크 JITed

ShowFormComplex JITed

3.4초
프레임워크 미리 컴파일됨, ShowFormComplex JITed 2.5초
프레임워크 미리 컴파일됨, ShowFormComplex 미리 컴파일됨 2.1초

각 테스트는 다시 부팅 후 수행되었습니다. 보듯이 Windows Forms 애플리케이션은 많은 메서드를 미리 사용하므로 미리 컴파일하는 데 상당한 성능이 향상됩니다.

들쭉날쭉한 배열 사용 - 버전 1

v1 JIT는 사각형 배열보다 들쭉날쭉한 배열(단순히 '배열')을 더 효율적으로 최적화하며, 그 차이는 매우 두드러집니다. 다음은 C# 및 Visual Basic 모두에서 직사각형 배열 대신 들쭉날쭉한 배열을 사용하여 발생하는 성능 향상을 보여 주는 표입니다(숫자가 높을수록 좋습니다).

  C# Visual Basic 7
배정(들쭉날쭉한)

대입(사각형)

14.16

8.37

12.24

8.62

신경망(들쭉날쭉한)

신경망(사각형)

4.48

3.00

4.58

3.13

숫자 정렬(들쭉날쭉한)

숫자 정렬(사각형)

4.88

2.05

5.07

2.06

과제 벤치마크는 비즈니스용 정량적 의사 결정 (Gordon, Pressman 및 Cohn)에 있는 단계별 가이드에서 조정된 간단한 할당 알고리즘입니다. 프렌티스 홀; 인쇄할 수 없습니다.) 신경망 테스트는 작은 신경망을 통해 일련의 패턴을 실행하며 숫자 정렬은 자체 설명입니다. 이러한 벤치마크를 종합하면 실제 성능을 나타내는 좋은 지표입니다.

보듯이 들쭉날쭉한 배열을 사용하면 성능이 상당히 향상됩니다. 들쭉날쭉한 배열에 대한 최적화는 이후 버전의 JIT에 추가되지만 v1의 경우 들쭉날쭉한 배열을 사용하여 많은 시간을 절약할 수 있습니다.

IO 버퍼 크기를 4KB에서 8KB 사이로 유지

거의 모든 애플리케이션에서 4KB에서 8KB 사이의 버퍼는 최대 성능을 제공합니다. 매우 구체적인 인스턴스의 경우 더 큰 버퍼(예: 예측 가능한 크기의 큰 이미지 로드)에서 향상된 성능을 얻을 수 있지만, 99.99%의 경우 메모리만 낭비합니다. BufferedStream에서 파생된 모든 버퍼를 사용하면 크기를 원하는 대로 설정할 수 있지만 대부분의 경우 4와 8은 최상의 성능을 제공합니다.

비동기 IO 기회 찾기

드문 경우에서 비동기 IO를 활용할 수 있습니다. 한 가지 예로 일련의 파일을 다운로드하고 압축을 풀 수 있습니다. 한 스트림에서 비트를 읽고 CPU에서 디코딩한 다음 다른 스트림에 쓸 수 있습니다. 비동기 IO를 효과적으로 사용하려면 많은 노력이 필요하며 제대로 수행되지 않으면 성능 손실 이 발생할 수 있습니다. 장점은 올바르게 적용할 때 비동기 IO가 성능을 10배까지 제공할 수 있다는 것입니다.

비동기 IO를 사용하는 프로그램의 훌륭한 예는 MSDN 라이브러리에서 사용할 수 있습니다.

  • 한 가지 주의해야 할 점은 비동기 호출에 대한 보안 오버헤드가 적다는 것입니다. 비동기 호출을 호출하면 호출자 스택의 보안 상태가 캡처되어 실제로 요청을 실행할 스레드로 전송됩니다. 콜백이 많은 코드를 실행하거나 비동기 호출이 과도하게 사용되지 않는 경우 이는 문제가 되지 않을 수 있습니다.

데이터베이스 액세스에 대한 팁

데이터베이스 액세스에 대한 튜닝의 철학은 필요한 기능만 사용하고 '연결이 끊긴' 접근 방식을 중심으로 디자인하는 것입니다. 한 연결을 오랫동안 열어 두지 않고 여러 연결을 순서대로 만듭니다. 이 변경 내용은 이 변경 내용의 고려 사항 및 디자인에 따라 변경해야 합니다.

Microsoft는 직접 클라이언트-데이터베이스 연결과는 달리 최대 성능을 위해 N 계층 전략을 권장합니다. 많은 기술이 다중 피곤 시나리오를 활용하도록 최적화되어 있으므로 이를 디자인 철학의 일부로 고려합니다.

최적 관리되는 공급자 사용

제네릭 접근자를 사용하는 대신 관리되는 공급자를 올바르게 선택합니다. SQL(System.Data.SqlClient)과 같은 다양한 데이터베이스에 대해 특별히 작성된 관리되는 공급자가 있습니다. 특수 구성 요소를 사용할 수 있을 때 System.Data.Odbc와 같은 보다 일반적인 인터페이스를 사용하는 경우 추가된 간접 참조 수준을 처리하는 성능이 손실됩니다. 최적의 공급자를 사용하면 다른 언어를 사용할 수도 있습니다. 관리되는 SQL 클라이언트는 SQL 데이터베이스에 TDS를 사용하여 일반 OleDbprotocol보다 크게 향상됩니다.

가능하면 데이터 집합을 통해 데이터 판독기 선택

데이터를 보관할 필요가 없을 때마다 데이터 판독기를 사용합니다. 이렇게 하면 데이터를 빠르게 읽을 수 있으며 사용자가 원하는 경우 캐시할 수 있습니다. 판독기는 도착 시 데이터를 읽은 다음 더 많은 탐색을 위해 데이터 세트에 저장하지 않고 삭제할 수 있는 상태 비주류 스트림일 뿐입니다. 즉시 데이터를 사용할 수 있으므로 스트림 접근 방식이 더 빠르고 오버헤드가 줄어듭니다. 탐색을 위한 캐싱이 적합한지 여부를 결정하기 위해 동일한 데이터가 필요한 빈도를 평가해야 합니다. 다음은 서버에서 데이터를 가져올 때 ODBC 및 SQL 공급자 모두에서 DataReader와 DataSet의 차이점을 보여 주는 작은 테이블입니다(더 높은 숫자는 더 낫다).

  ADO SQL
데이터 세트 801 2507
DataReader 1083 4585

보듯이 데이터 판독기와 함께 최적의 관리 공급자를 사용할 때 가장 높은 성능이 달성됩니다. 데이터를 캐시할 필요가 없는 경우 데이터 판독기를 사용하면 엄청난 성능 향상을 제공할 수 있습니다.

MP 머신에 Mscorsvr.dll 사용

독립 실행형 중간 계층 및 서버 애플리케이션의 경우 다중 프로세서 머신에 사용되고 있는지 확인 mscorsvr 합니다. Mscorwks는 크기 조정 또는 처리량에 최적화되지 않지만 서버 버전에는 둘 이상의 프로세서를 사용할 수 있을 때 잘 스케일링할 수 있는 몇 가지 최적화가 있습니다.

가능하면 저장 프로시저 사용

저장 프로시저는 효과적으로 사용될 때 우수한 성능을 제공하는 고도로 최적화된 도구입니다. 데이터 어댑터를 사용하여 삽입, 업데이트 및 삭제를 처리하도록 저장 프로시저를 설정합니다. 저장 프로시저는 클라이언트에서 해석, 컴파일 또는 전송할 필요가 없으며 네트워크 트래픽과 서버 오버헤드를 모두 줄일 수 있습니다. CommandType.Text 대신 CommandType.StoredProcedure를 사용해야 합니다.

동적 연결 문자열에 주의

연결 풀링 은 각 요청에 대한 연결을 열고 닫는 오버헤드를 지불하는 대신 여러 요청에 대한 연결을 다시 사용하는 유용한 방법입니다. 암시적으로 수행되지만 고유한 연결 문자열당 하나의 풀을 가져옵니다. 연결 문자열을 동적으로 생성하는 경우 풀링이 발생할 때마다 문자열이 동일한지 확인합니다. 또한 위임이 발생하는 경우 사용자당 하나의 풀을 얻게 됩니다. 연결 풀에 대해 설정할 수 있는 많은 옵션이 있으며 Perfmon을 사용하여 응답 시간, 트랜잭션/초 등의 항목을 추적하여 풀의 성능을 추적할 수 있습니다.

사용하지 않는 기능 끄기

필요하지 않은 경우 자동 트랜잭션 인리스트먼트를 끕니다. SQL 관리되는 공급자 연결 문자열을 통해 수행됩니다.

SqlConnection conn = new SqlConnection(
"Server=mysrv01;
Integrated Security=true;
Enlist=false");

데이터 어댑터로 데이터 세트를 채울 때 필요하지 않은 경우 기본 키 정보를 얻지 않습니다(예: MissingSchemaAction.Add를 키로 설정하지 않음).

public DataSet SelectSqlSrvRows(DataSet dataset,string connection,string query){
    SqlConnection conn = new SqlConnection(connection);
    SqlDataAdapter adapter = new SqlDataAdapter();
    adapter.SelectCommand = new SqlCommand(query, conn);
    adapter.MissingSchemaAction = MissingSchemaAction.AddWithKey;
    adapter.Fill(dataset);
    return dataset;
}

자동 생성 명령 방지

데이터 어댑터를 사용하는 경우 자동 생성 명령을 사용하지 않습니다. 이를 위해서는 메타 데이터를 검색하고 더 낮은 수준의 상호 작용 제어를 제공하기 위해 서버로의 추가 여행이 필요합니다. 자동 생성 명령을 사용하는 것은 편리하지만 성능이 중요한 애플리케이션에서 직접 수행하는 것이 좋습니다.

ADO 레거시 디자인 주의

어댑터에서 명령 또는 호출 채우기를 실행할 때 쿼리에 지정된 모든 레코드가 반환됩니다.

서버 커서가 절대적으로 필요한 경우 t-sql의 저장 프로시저를 통해 구현할 수 있습니다. 서버 커서 기반 구현의 크기가 잘 조정되지 않으므로 가능한 경우를 방지합니다.

필요한 경우 상태 비정상 및 연결 없는 방식으로 페이징을 구현합니다. 다음을 통해 데이터 세트에 레코드를 추가할 수 있습니다.

  • PK 정보가 있는지 확인
  • 데이터 어댑터의 select 명령을 적절하게 변경하고
  • 채우기 호출

데이터 세트를 린(Lean)으로 유지

필요한 레코드만 데이터 세트에 넣습니다. 데이터 세트는 모든 데이터를 메모리에 저장하며, 요청하는 데이터가 많을수록 유선을 통해 전송하는 데 더 오래 걸립니다.

순차적 액세스를 가능한 한 자주 사용

데이터 판독기를 사용하여 CommandBehavior.SequentialAccess를 사용합니다. 이는 Blob 데이터 형식을 처리하는 데 필수적입니다. 이 데이터 형식은 데이터를 작은 청크로 읽어내도록 허용하기 때문에 필수적입니다. 한 번에 하나의 데이터만 작업할 수 있지만 대용량 데이터 형식을 로드하는 데 걸리는 대기 시간은 사라집니다. 전체 개체를 한 번에 작업할 필요가 없는 경우 순차적 액세스를 사용하면 성능이 훨씬 향상됩니다.

ASP.NET 애플리케이션에 대한 성능 팁

적극적으로 캐시

ASP.NET 사용하여 앱을 디자인할 때 캐싱을 주시하여 디자인해야 합니다. OS의 서버 버전에서는 서버 및 클라이언트 쪽에서 캐시 사용을 조정하기 위한 많은 옵션이 있습니다. ASP에는 성능을 얻기 위해 사용할 수 있는 몇 가지 기능과 도구가 있습니다.

출력 캐싱 - ASP 요청의 정적 결과를 저장합니다. 지시문을 사용하여 <@% OutputCache %> 지정됨:

  • 기간 - 캐시에 시간 항목이 있음
  • VaryByParam - Get/Post 매개 변수별로 캐시 항목에 따라 다름
  • VaryByHeader - Http 헤더별로 캐시 항목에 따라 다름
  • VaryByCustom - 브라우저별 캐시 항목에 따라 다름
  • 원하는 대로 변경하려면 재정의합니다.
    • 조각 캐싱 - 전체 페이지(개인 정보, 개인 설정, 동적 콘텐츠)를 저장할 수 없는 경우 조각 캐싱을 사용하여 나중에 더 빠르게 검색할 수 있도록 부분 저장을 수행할 수 있습니다.

      a) VaryByControl - 캐시된 항목의 값에 따라 다릅니다.

    • 캐시 API - 캐시된 개체의 해시 테이블을 메모리(System.web.UI.caching)에 유지하여 캐싱을 위한 매우 세분성을 제공합니다. 또한 다음을 수행합니다.

      a) 종속성 포함(키, 파일, 시간)

      b) 사용하지 않는 항목이 자동으로 만료됨

      c) 콜백 지원

지능적으로 캐싱하면 뛰어난 성능을 제공할 수 있으며 필요한 캐싱 종류에 대해 생각하는 것이 중요합니다. 로그인을 위해 여러 정적 페이지가 있는 복잡한 전자 상거래 사이트와 이미지 및 텍스트가 포함된 동적으로 생성된 수많은 페이지를 상상해 보세요. 해당 로그인 페이지에 출력 캐싱을 사용한 다음 동적 페이지에 조각 캐싱을 사용할 수 있습니다. 예를 들어 도구 모음은 조각으로 캐시될 수 있습니다. 성능을 향상시키려면 일반적으로 사용되는 이미지를 캐시하고 캐시 API를 사용하여 사이트에 자주 표시되는 상용구 텍스트를 캐시할 수 있습니다. 캐싱(샘플 코드 사용)에 대한 자세한 내용은 ASP. NET 웹 사이트를 검사.

필요한 경우에만 세션 상태 사용

ASP.NET 매우 강력한 기능 중 하나는 전자 상거래 사이트의 쇼핑 카트 또는 브라우저 기록과 같은 사용자에 대한 세션 상태를 저장하는 기능입니다. 이 기능은 기본적으로 사용되므로 사용하지 않더라도 메모리 비용을 지불합니다. 세션 상태를 사용하지 않는 경우 asp에 @% EnabledSessionState = false %>를 추가하여< 이를 끄고 오버헤드를 저장합니다. ASP. NET 웹 사이트에서 설명하는 몇 가지 다른 옵션이 함께 제공됩니다.

세션 상태만 읽는 페이지의 경우 EnabledSessionState=readonly를 선택할 수 있습니다. 이렇게 하면 전체 읽기/쓰기 세션 상태보다 오버헤드가 적으며 기능의 일부만 필요하고 쓰기 기능에 대한 비용을 지불하지 않으려는 경우에 유용합니다.

필요한 경우에만 보기 상태 사용

보기 상태의 예는 사용자가 작성해야 하는 긴 양식일 수 있습니다. 브라우저에서 뒤로 를 클릭한 다음 반환하면 양식이 채워진 상태로 유지됩니다. 이 기능을 사용하지 않으면 이 상태는 메모리와 성능을 유지합니다. 아마도 여기서 가장 큰 성능 저하는 페이지를 로드할 때마다 네트워크를 통해 왕복 신호를 전송하여 캐시를 업데이트하고 확인해야 한다는 것입니다. 기본적으로 설정되어 있으므로 @% EnabledViewState = false %>와 함께 <보기 상태를 사용하지 않도록 지정해야 합니다. 액세스 권한이 있는 다른 옵션 및 설정에 대해 알아보려면 ASP. NET 웹 사이트의 상태 보기에 대해 자세히 알아보세요.

STA COM 방지

아파트 COM은 관리되지 않는 환경에서 스레딩을 처리하도록 설계되었습니다. 아파트 COM에는 단일 스레드 및 다중 스레드의 두 가지 종류가 있습니다. MTA COM은 다중 스레딩을 처리하도록 설계된 반면 STA COM은 메시징 시스템을 사용하여 스레드 요청을 직렬화합니다. 관리되는 세계는 자유 스레드이며 단일 스레드 아파트먼트 COM을 사용하려면 관리되지 않는 모든 스레드가 기본적으로 interop용 단일 스레드를 공유해야 합니다. 이로 인해 엄청난 성능이 저하되며 가능하면 피해야 합니다. Apartment COM 개체를 관리되는 세계로 포팅할 수 없는 경우 해당 개체를 사용하는 페이지에 @%AspCompat = "true" %를> 사용합니다<. STA COM에 대한 자세한 설명은 MSDN 라이브러리를 참조하세요.

Batch Compile

웹에 큰 페이지를 배포하기 전에 항상 일괄 처리 컴파일합니다. 디렉터리당 페이지에 대해 하나의 요청을 수행하고 CPU가 다시 유휴 상태가 될 때까지 대기하여 이 작업을 시작할 수 있습니다. 이렇게 하면 웹 서버가 페이지 제공을 시도하는 동안 컴파일에 얽매이는 것을 방지할 수 있습니다.

불필요한 Http 모듈 제거

사용된 기능에 따라 파이프라인에서 사용되지 않거나 불필요한 http 모듈을 제거합니다. 추가된 메모리 및 낭비된 주기를 회수하면 약간의 속도 향상을 제공할 수 있습니다.

Autoeventwireup 기능 방지

autoeventwireup에 의존하는 대신 페이지의 이벤트를 재정의합니다. 예를 들어 Page_Load() 메서드를 작성하는 대신 public void OnLoad() 메서드를 오버로드해 봅니다. 이렇게 하면 런타임에서 모든 페이지에 대해 CreateDelegate() 를 수행할 필요가 없습니다.

UTF가 필요하지 않은 경우 ASCII를 사용하여 인코딩

기본적으로 ASP.NET 요청 및 응답을 UTF-8로 인코딩하도록 구성됩니다. ASCII가 모든 애플리케이션 요구 사항인 경우 UTF 오버헤드를 제거하면 몇 가지 주기를 다시 제공할 수 있습니다. 이 작업은 애플리케이션별로만 수행할 수 있습니다.

최적 인증 절차 사용

사용자를 인증하는 방법에는 여러 가지가 있으며 일부는 다른 사용자보다 더 비쌉니다(비용 증가 순서: 없음, Windows, Forms, Passport). 요구 사항에 가장 적합한 가장 저렴한 항목을 사용해야 합니다.

Visual Basic에서 포팅 및 개발 팁

내부적으로 Microsoft Visual Basic 6에서 Microsoft®® Visual® Basic® 7로 많이 변경되었으며 성능 맵이 변경되었습니다. CLR의 추가된 기능 및 보안 제한으로 인해 일부 함수는 Visual Basic 6에서와 같이 빠르게 실행할 수 없습니다. 실제로 Visual Basic 7이 이전 버전에 의해 흐리게 되는 몇 가지 영역이 있습니다. 다행히도, 좋은 소식의 두 조각이 있다:

  • 대부분의 최악의 속도 저하는 처음으로 컨트롤 로드와 같은 일회성 함수 중에 발생합니다. 비용은 있지만 한 번만 지불하면 됩니다.
  • Visual Basic 7이 더 빠른 영역이 많이 있으며 이러한 영역은 런타임 중에 반복되는 함수에 있는 경향이 있습니다. 즉, 시간이 지남에 따라 혜택이 증가하고, 경우에 따라 일회성 비용보다 더 큽니다.

대부분의 성능 문제는 런타임이 Visual Basic 6의 기능을 지원하지 않으며 Visual Basic 7에서 기능을 유지하기 위해 추가해야 하는 영역에서 비롯됩니다. 런타임 외 작업 속도가 느려지므로 일부 기능을 사용하는 데 훨씬 더 많은 비용이 듭니다. 밝은 면은 약간의 노력으로 이러한 문제를 피할 수 있다는 것입니다. 성능을 최적화하기 위해 작업이 필요한 두 가지 기본 영역과 여기 저기에서 수행할 수 있는 몇 가지 간단한 조정이 있습니다. 이를 종합하면 성능 저하를 한 단계씩 처리하고 Visual Basic 7에서 훨씬 더 빠른 함수를 활용하는 데 도움이 될 수 있습니다.

오류 처리

첫 번째 관심사는 오류 처리입니다. 이는 Visual Basic 7에서 많이 변경되었으며 변경과 관련된 성능 문제가 있습니다. 기본적으로 OnErrorGotoResume 을 구현하는 데 필요한 논리는 매우 비쌉니다. 코드를 빠르게 살펴보고 Err 개체를 사용하는 모든 영역 또는 오류 처리 메커니즘을 강조 표시하는 것이 좋습니다. 이제 이러한 각 인스턴스를 살펴보고 try/catch를 사용하도록 다시 작성할 수 있는지 확인합니다. 많은 개발자가 이러한 대부분의 경우 쉽게 시도/catch 로 변환할 수 있으며 프로그램에서 좋은 성능 향상을 볼 수 있습니다. 엄지 손가락의 규칙은 "번역을 쉽게 볼 수 있다면 수행"입니다.

다음은 Try/catch 버전과 비교하여 On Error Goto를 사용하는 간단한 Visual Basic 프로그램의 예입니다.

Sub SubWithError()
On Error Goto SWETrap
  Dim x As Integer
  Dim y As Integer
  x = x / y
SWETrap:  Exit Sub
  End Sub
 
Sub SubWithErrorResumeLabel()
  On Error Goto SWERLTrap
  Dim x As Integer
  Dim y As Integer
  x = x / y 
SWERLTrap:
  Resume SWERLExit
  End Sub
SWERLExit:
  Exit Sub
Sub SubWithError()
  Dim x As Integer
  Dim y As Integer
  Try    x = x / y  Catch    Return  End Try
  End Sub
 
Sub SubWithErrorResumeLabel()
  Dim x As Integer
  Dim y As Integer
  Try
    x = x / y
  Catch
  Goto SWERLExit
  End Try
 
SWERLExit:
  Return
  End Sub

속도 증가가 눈에 띄습니다. SubWithError()OnErrorGoto를 사용하는 데 244밀리초가 걸리고 try/catch를 사용하는 데는 169밀리초밖에 걸리지 않습니다. 두 번째 함수는 최적화된 버전에 대해 164밀리초에 비해 179밀리초가 걸립니다.

초기 바인딩 사용

두 번째 문제는 개체 및 형식 캐스팅을 다룹니다. Visual Basic 6은 개체 캐스팅을 지원하기 위해 내부적으로 많은 작업을 수행하며 많은 프로그래머는 이를 인식하지 못합니다. Visual Basic 7에서는 많은 성능을 짜낼 수 있는 영역입니다. 컴파일할 때 초기 바인딩을 사용합니다. 이렇게 하면 명시적으로 언급된 경우에만 형식 강제 변환 을 삽입하도록 컴파일러에 지시합니다. 여기에는 두 가지 주요 효과가 있습니다.

  • 이상한 오류는 추적하기 쉬워집니다.
  • 불필요한 강제 변환이 제거되어 성능이 크게 향상됩니다.

개체를 다른 형식인 것처럼 사용하면 지정하지 않으면 Visual Basic에서 개체를 강제 변환합니다. 프로그래머가 적은 코드에 대해 걱정해야 하기 때문에 편리합니다. 단점은 이러한 강제 변환이 예기치 않은 작업을 수행할 수 있으며 프로그래머가 이를 제어할 수 없다는 것입니다.

지연 바인딩을 사용해야 하는 경우가 있지만, 확실하지 않은 경우 대부분의 경우 초기 바인딩을 벗어날 수 있습니다. Visual Basic 6 프로그래머의 경우 이전보다 형식에 대해 더 걱정해야 하므로 처음에는 약간 어색할 수 있습니다. 이는 새 프로그래머에게 쉬울 것이며 Visual Basic 6에 익숙한 사용자는 이 작업을 한 번에 선택할 수 있습니다.

엄격하고 명시적인 옵션 켜기

Option Strict on을 사용하면 실수로 인한 지연 바인딩으로부터 자신을 보호하고 더 높은 수준의 코딩 분야를 적용할 수 있습니다. Option Strict와 함께 제공되는 제한 사항 목록은 MSDN 라이브러리를 참조하세요. 이에 대한 주의 사항은 모든 축소 형식 강제 변환을 명시적으로 지정해야 한다는 것입니다. 그러나 이는 그 자체로 이전에 생각했던 것보다 더 많은 작업을 수행하는 코드의 다른 섹션을 발견할 수 있으며 프로세스에서 일부 버그를 밟는 데 도움이 될 수 있습니다.

Option Explicit는 Option Strict보다 덜 제한적이지만 프로그래머가 코드에 더 많은 정보를 제공하도록 강제합니다. 특히 변수를 사용하기 전에 선언해야 합니다. 이렇게 하면 형식 유추가 런타임에서 컴파일 시간으로 이동합니다. 이렇게 하면 검사 제거되어 성능이 향상됩니다.

Option Explicit로 시작한 다음 Option Strict를 켜는 것이 좋습니다. 이렇게 하면 컴파일러 오류의 홍수로부터 보호하고 더 엄격한 환경에서 점진적으로 작업을 시작할 수 있습니다. 이러한 두 옵션을 모두 사용하면 애플리케이션에 대한 최대 성능을 보장합니다.

텍스트에 이진 비교 사용

텍스트를 비교할 때 텍스트 비교 대신 이진 비교를 사용합니다. 런타임에 이진 파일의 오버헤드가 훨씬 더 가벼워집니다.

형식 사용 최소화()

가능하면 format() 대신 toString()을 사용합니다. 대부분의 경우 필요한 기능을 제공하며 오버헤드가 훨씬 적습니다.

Charw 사용

char 대신 charw를 사용합니다. CLR은 내부적으로 유니코드를 사용하며, 사용되는 경우 런타임에 char 를 변환해야 합니다. 이로 인해 성능이 크게 저하되고 문자가 전체 단어 길이로 지정됩니다(charw) 를 사용하면 이 변환이 제거됩니다.

할당 최적화

exp = exp + val 대신 exp += val을 사용합니다. exp 는 임의로 복잡할 수 있으므로 불필요한 작업이 많이 발생할 수 있습니다. 이렇게 하면 JIT가 exp의 두 복사본을 모두 평가하게 되며, 필요하지 않은 경우가 많습니다. JIT는 exp 를 두 번 평가하는 것을 방지할 수 있으므로 첫 번째 문은 두 번째 문보다 훨씬 더 잘 최적화할 수 있습니다.

불필요한 간접 참조 방지

byRef를 사용하는 경우 실제 개체 대신 포인터를 전달합니다. 여러 번 이것이 의미가 있지만(예: 부작용 함수) 항상 필요한 것은 아닙니다. 포인터를 전달하면 스택에 있는 값에 액세스하는 것보다 느린 간접 참조가 발생합니다. 힙을 통과할 필요가 없는 경우 힙을 방지하는 것이 가장 좋습니다.

하나의 식에 연결 배치

여러 줄에 여러 연결이 있는 경우 모두 하나의 식에 고정해 봅니다. 컴파일러는 문자열을 현재 위치에서 수정하여 최적화하여 속도와 메모리 향상을 제공할 수 있습니다. 문이 여러 줄로 분할된 경우 Visual Basic 컴파일러는 현재 위치 연결을 허용하도록 MSIL(Microsoft Intermediate Language)을 생성하지 않습니다. 앞에서 설명한 StringBuilder 예제를 참조하세요.

Return 문 포함

Visual Basic을 사용하면 함수가 return 문을 사용하지 않고 값을 반환할 수 있습니다. Visual Basic 7은 이를 지원하지만 반환 을 명시적으로 사용하면 JIT에서 약간 더 많은 최적화를 수행할 수 있습니다. return 문이 없으면 각 함수에는 키워드(keyword) 없이 반환 값을 투명하게 지원하기 위해 스택에 여러 지역 변수가 제공됩니다. 이러한 기능을 유지하면 JIT를 최적화하기가 더 어려워지고 코드 성능에 영향을 미칠 수 있습니다. 함수를 살펴보고 필요에 따라 반환 을 삽입합니다. 코드의 의미 체계는 전혀 변경되지 않으며 애플리케이션에서 속도를 높이는 데 도움이 될 수 있습니다.

관리되는 C++에서 포팅 및 개발을 위한 팁

Microsoft는 특정 개발자 집합에서 관리되는 C++(MC++)를 대상으로 합니다. MC++는 모든 작업에 가장 적합한 도구가 아닙니다. 이 문서를 읽은 후 C++가 최상의 도구가 아니며 장단점 비용이 이점을 얻을 가치가 없다고 결정할 수 있습니다. MC++에 대해 잘 모르는 경우 결정을 내리는 데 도움이 되는 많은 유용한 리소스 가 있습니다. 이 섹션은 이미 MC++를 어떤 식으로든 사용하기로 결정한 개발자를 대상으로 하며, MC++의 성능 측면에 대해 알고자 합니다.

C++ 개발자의 경우 관리되는 C++를 사용하려면 몇 가지 결정을 내려야 합니다. 이전 코드를 이식하고 있나요? 그렇다면 전체 항목을 관리되는 공간으로 이동하시겠습니까? 아니면 래퍼를 구현할 계획인가요? 프로그래머가 성능 차이를 발견할 수 있는 시나리오이기 때문에 'port-everything' 옵션에 집중하거나 이 토론의 목적을 위해 MC++를 처음부터 작성하는 방법을 다루겠습니다.

Managed World의 이점

관리되는 C++의 가장 강력한 기능은 식 수준에서 관리 코드와 관리되지 않는 코드를 혼합하고 일치시킬 수 있는 기능입니다. 다른 어떤 언어도 이 작업을 수행할 수 없으며, 제대로 사용하면 얻을 수 있는 몇 가지 강력한 이점이 있습니다. 나중에 이에 대한 몇 가지 예를 살펴보겠습니다.

관리되는 세계는 또한 많은 일반적인 문제가 당신을 위해 처리된다는 것을, 당신에게 거대한 디자인 승리를 제공합니다. 원하는 경우 메모리 관리, 스레드 예약 및 형식 강제 변환을 런타임에 남겨 두면 필요한 프로그램 부분에 에너지를 집중할 수 있습니다. MC++를 사용하면 유지할 컨트롤의 양을 정확히 선택할 수 있습니다.

MC++ 프로그래머는 IL로 컴파일한 다음 그 위에 JIT를 사용할 때 Microsoft VC7(Visual C® 7) 백 엔드를 사용할 수 있습니다. Microsoft C++ 컴파일러를 사용하는 데 사용되는 프로그래머는 매우 빠른 작업에 사용됩니다. JIT는 서로 다른 목표를 가지고 설계되었으며, 강점과 약점의 다른 세트를 가지고있다. JIT의 시간 제한에 바인딩되지 않은 VC7 컴파일러는 전체 프로그램 분석, 보다 적극적인 인라인 처리 및 등록과 같이 JIT에서 수행할 수 없는 특정 최적화를 수행할 수 있습니다. 또한 typesafe 환경에서만 수행할 수 있는 몇 가지 최적화가 있어 C++가 허용하는 것보다 더 많은 속도를 확보할 수 있습니다.

JIT의 우선 순위가 다르기 때문에 일부 작업은 이전보다 빠르지만 다른 작업은 더 느립니다. 안전과 언어 유연성을 위해 장단 사항이 있으며, 그 중 일부는 저렴하지 않습니다. 다행히 프로그래머가 비용을 최소화하기 위해 할 수 있는 일이 있습니다.

포팅: 모든 C++ 코드가 MSIL로 컴파일할 수 있습니다.

더 나아가기 전에 모든 C++ 코드를 MSIL로 컴파일 수 있다는 점에 유의해야 합니다. 모든 것이 작동하지만 형식 안전을 보장할 수는 없으며 interop을 많이 수행하면 마샬링 페널티를 지불합니다. 이점이 없는 경우 MSIL로 컴파일하는 것이 유용한 이유는 무엇인가요? 대규모 코드 베이스를 포팅하는 경우 코드를 점진적으로 포팅할 수 있습니다. MC++를 사용하는 경우 포팅되고 아직 이식되지 않은 코드를 함께 붙이기 위해 특수 래퍼를 작성하는 대신 더 많은 코드를 포팅하는 데 시간을 할애할 수 있으며, 이로 인해 큰 승리를 거둘 수 있습니다. 이는 애플리케이션 포팅을 매우 클린 프로세스로 만듭니다. C++를 MSIL로 컴파일하는 방법에 대해 자세히 알아보려면 /clr 컴파일러 옵션을 살펴보세요.

그러나 단순히 C++ 코드를 MSIL로 컴파일해도 관리되는 세계의 보안 또는 유연성을 제공하지는 않습니다. MC++로 작성해야 하며 v1에서는 몇 가지 기능을 포기해야 합니다. 아래 목록은 현재 버전의 CLR에서 지원되지 않지만 향후에 있을 수 있습니다. Microsoft는 가장 일반적인 기능을 먼저 지원하기로 결정했으며, 배송을 위해 다른 기능을 잘라야 했습니다. 나중에 추가되는 것을 방지하는 것은 없지만 그 동안에는 다음 작업 없이 수행해야 합니다.

  • 다중 상속
  • 템플릿
  • 결정적 종료

이러한 기능이 필요한 경우 항상 안전하지 않은 코드와 상호 운용할 수 있지만 데이터를 앞뒤로 마샬링하면 성능이 저하됩니다. 또한 이러한 기능은 관리되지 않는 코드 내에서만 사용할 수 있습니다. 관리되는 공간에는 해당 존재에 대한 지식이 없습니다. 코드를 포팅하기로 결정한 경우 디자인에서 이러한 기능에 얼마나 의존하는지 생각해 보세요. 경우에 따라 재설계 비용이 너무 많이 들고 관리되지 않는 코드를 고수하려고 합니다. 해킹을 시작하기 전에 먼저 결정해야 합니다.

C# 또는 Visual Basic보다 MC++의 이점

관리되지 않는 백그라운드에서 제공되는 MC++는 안전하지 않은 코드를 처리하는 많은 기능을 유지합니다. 관리 코드와 관리되지 않는 코드를 원활하게 혼합하는 MC++의 기능은 개발자에게 많은 기능을 제공하며 코드를 작성할 때 배치할 그라데이션의 위치를 선택할 수 있습니다. 한 가지 극단적인 경우 모든 것을 직선적이고 순수한 C++로 작성하고 /clr로 컴파일할 수 있습니다. 다른 한편으로 모든 항목을 관리되는 개체로 작성하고 위에서 언급한 언어 제한 사항 및 성능 문제를 처리할 수 있습니다.

하지만 MC++의 진정한 힘은 그 사이 어딘가에서 선택할 때 옵니다. MC++를 사용하면 안전하지 않은 기능을 사용하는 시기를 정확하게 제어하여 관리 코드에 내재된 성능 적중 횟수 중 일부를 조정할 수 있습니다. C#에는 안전하지 않은 키워드(keyword) 이 기능 중 일부가 있지만 언어의 필수적인 부분이 아니며 MC++보다 훨씬 덜 유용합니다. MC++에서 사용할 수 있는 세부적인 세분성을 보여 주는 몇 가지 예제를 단계별로 살펴보고 편리한 상황에 대해 살펴보겠습니다.

일반화된 "byref" 포인터

C#에서는 ref 매개 변수에 전달하여 클래스의 일부 멤버의 주소만 사용할 수 있습니다. MC++에서 byref 포인터는 일류 구문입니다. 배열 중간에 있는 항목의 주소를 가져와서 함수에서 해당 주소를 반환할 수 있습니다.

Byte* AddrInArray( Byte b[] ) {
   return &b[5];
}

도우미 루틴을 통해 System.String의 "문자"에 대한 포인터를 반환하기 위해 이 기능을 활용하고 다음 포인터를 사용하여 배열을 반복할 수도 있습니다.

System::Char* PtrToStringChars(System::String*);   
for( Char*pC = PtrToStringChars(S"boo");
  pC != NULL;
  pC++ )
{
      ... *pC ...
}

"다음" 필드의 주소를 사용하여 MC++에서 삽입을 사용하여 연결된 목록 통과를 수행할 수도 있습니다(C#에서는 수행할 수 없음).

Node **w = &Head;
while(true) {
  if( *w == 0 || val < (*w)->val ) {
    Node *t = new Node(val,*w);
    *w = t;
    break;
  }
  w = &(*w)->next;
}

C#에서는 "Head"를 가리키거나 "다음" 필드의 주소를 사용할 수 없으므로 첫 번째 위치에 삽입하는 특수 사례를 만들거나 "Head"가 null인 경우를 만듭니다. 또한 코드에서 항상 한 노드를 미리 확인해야 합니다. 이를 좋은 C#이 생성하는 것과 비교합니다.

if( Head==null || val < Head.val ) {
  Node t = new Node(val,Head);
  Head = t;
}else{
  // we know at least one node exists,
  // so we can look 1 node ahead
  Node w=Head;
while(true) {
  if( w.next == null || val < w.next.val ){
    Node t = new Node(val,w.next.next);
    w.next = t;
    break;
  }
  w = w.next;
  }
}         

Boxed 형식에 대한 사용자 액세스

OO 언어에서 흔히 발생하는 성능 문제는 값을 boxing 및 unboxing하는 데 소요된 시간입니다. MC++를 사용하면 이 동작을 훨씬 더 많이 제어할 수 있으므로 값에 액세스하기 위해 동적 또는 정적으로 unbox를 해제할 필요가 없습니다. 이는 또 다른 성능 향상입니다. __box 키워드(keyword) 형식 앞에 배치하여 상자 형식을 나타내기만 하면됩니다.

__value struct V {
  int i;
};
int main() {
  V v = {10};
  __box V *pbV = __box(v);
  pbV->i += 10;           // update without casting
}

C#에서 "v"에 대한 받은 편지함을 해제한 다음, 값을 업데이트하고 개체로 다시 상자를 다시 지정해야 합니다.

struct B { public int i; }
static void Main() {
  B b = new B();
  b.i = 5;
  object o = b;         // implicit box
  B b2 = (B)o;            // explicit unbox
  b2.i++;               // update
  o = b2;               // implicit re-box
}

STL 컬렉션과 관리되는 컬렉션- v1

나쁜 소식: C++에서는 STL 컬렉션을 사용하는 것이 해당 기능을 직접 작성하는 것만큼 빠른 경우가 많습니다. CLR 프레임워크는 매우 빠르지만 boxing 및 unboxing 문제가 발생합니다. 모든 항목은 개체이며 템플릿 또는 일반 지원이 없으면 런타임에 모든 작업을 확인해야 합니다.

좋은 소식: 장기적으로 제네릭이 런타임에 추가되면 이 문제가 사라질 것이라고 내기를 할 수 있습니다. 오늘 배포하는 코드는 변경하지 않고 속도 향상을 경험하게 됩니다. 단기적으로는 정적 캐스팅을 사용하여 검사 방지할 수 있지만 더 이상 안전하지 않습니다. 성능이 절대적으로 중요한 타이트한 코드에서 이 메서드를 사용하는 것이 좋으며, 두세 개의 핫스폿을 확인했습니다.

스택 관리 개체 사용

C++에서는 스택 또는 힙에서 개체를 관리해야 한다고 지정합니다. MC++에서는 이 작업을 계속 수행할 수 있지만 알고 있어야 하는 제한 사항이 있습니다. CLR은 모든 스택 관리 개체에 ValueTypes를 사용하며 ValueTypes에서 수행할 수 있는 작업에는 제한이 있습니다(예: 상속 없음). 자세한 내용은 MSDN 라이브러리에서 확인할 수 있습니다.

코너 사례: 관리 코드 내에서 간접 호출을 조심합니다. v1

v1 런타임에서 모든 간접 함수 호출은 기본적으로 수행되므로 관리되지 않는 공간으로 전환해야 합니다. 모든 간접 함수 호출은 기본 모드에서만 수행할 수 있습니다. 즉, 관리 코드의 모든 간접 호출에는 관리되는 전환에서 관리되지 않는 전환이 필요합니다. 이 문제는 테이블이 관리되는 함수를 반환할 때 심각한 문제입니다. 함수를 실행하려면 두 번째 전환이 이루어져야 하기 때문이다. 단일 통화 명령을 실행하는 비용과 비교할 때 비용은 C++보다 50~100배 느립니다.

다행히 가비지 수집 클래스 내에 있는 메서드를 호출할 때 최적화는 이를 제거합니다. 그러나 /clr을 사용하여 컴파일된 일반 C++ 파일의 특정 경우 메서드 반환은 관리되는 것으로 간주됩니다. 최적화를 통해 제거할 수 없으므로 전체 이중 전환 비용이 표시됩니다. 다음은 이러한 사례의 예입니다.

//////////////////////// a.h:    //////////////////////////
class X {
public:
   void mf1();
   void mf2();
};

typedef void (X::*pMFunc_t)();


////////////// a.cpp: compiled with /clr  /////////////////
#include "a.h"

int main(){
   pMFunc_t pmf1 = &X::mf1;
   pMFunc_t pmf2 = &X::mf2;

   X *pX = new X();
   (pX->*pmf1)();
   (pX->*pmf2)();

   return 0;
}


////////////// b.cpp: compiled without /clr /////////////////
#include "a.h"

void X::mf1(){}


////////////// c.cpp: compiled with /clr ////////////////////
#include "a.h"
void X::mf2(){}

이를 방지하는 방법에는 여러 가지가 있습니다.

  • 클래스를 관리되는 클래스로 만들기("__gc")
  • 가능하면 간접 호출 제거
  • 클래스를 관리되지 않는 코드로 컴파일된 상태로 둡니다(예: /clr 사용 안 함).

성능 적중 최소화 - 버전 1

버전 1 JIT에서 MC++에서 비용이 더 많이 드는 몇 가지 작업 또는 기능이 있습니다. 나는 그들을 나열하고 몇 가지 설명을 줄 것이다, 우리는 당신이 그들에 대해 무엇을 할 수 있는지에 대해 이야기 할 것이다.

  • 추상화 - 이 영역은 비프하고 느린 C++ 백 엔드 컴파일러가 JIT를 크게 이기는 영역입니다. 추상화 목적으로 클래스 내에서 int를 래핑하고 int로 엄격하게 액세스하는 경우 C++ 컴파일러는 래퍼의 오버헤드를 거의 아무것도 줄일 수 없습니다. 비용을 늘리지 않고 래퍼에 여러 수준의 추상화 를 추가할 수 있습니다. JIT는 이 비용을 제거하는 데 필요한 시간을 할애할 수 없으므로 MC++에서 심층 추상화 비용이 더 많이 듭니다.
  • 부동 소수점 - v1 JIT는 현재 VC++ 백 엔드가 수행하는 모든 FP별 최적화를 수행하지 않으므로 현재 부동 소수점 작업이 더 비쌉니다.
  • 다차원 배열 - JIT는 다차원 배열보다 들쭉날쭉한 배열을 처리하는 데 더 적합하므로 대신 들쭉날쭉한 배열을 사용합니다.
  • 64비트 산술 - 이후 버전에서는 64비트 최적화가 JIT에 추가됩니다.

수행할 수 있는 일

개발의 모든 단계에서 수행할 수 있는 몇 가지 작업이 있습니다. MC++를 사용하면 디자인 단계가 가장 중요한 영역일 수 있습니다. 이는 결국 얼마나 많은 작업을 수행하고 그 대가로 얼마나 많은 성능을 얻을 수 있는지를 결정하기 때문일 것입니다. 앉아서 애플리케이션을 작성하거나 이식할 때는 다음 사항을 고려해야 합니다.

  • 여러 상속, 템플릿 또는 결정적 마무리를 사용하는 영역을 식별합니다. 이러한 항목을 제거하거나 코드의 해당 부분을 관리되지 않는 공간에 남겨 두어야 합니다. 재설계 비용을 고려하고 포팅할 수 있는 영역을 식별합니다.
  • 관리되는 공간에서 심층 추상화 또는 가상 함수 호출과 같은 성능 핫스폿을 찾습니다. 설계 결정도 필요합니다.
  • 스택 관리로 지정된 개체를 찾습니다. ValueTypes로 변환할 수 있는지 확인합니다. 다른 개체를 힙 관리 개체로 변환하도록 표시합니다.

코딩 단계에서는 비용이 더 많이 드는 작업과 이를 처리하는 데 필요한 옵션을 알고 있어야 합니다. MC++에 대한 가장 좋은 점 중 하나는 코딩을 시작하기 전에 모든 성능 문제를 미리 파악하는 것입니다. 이는 나중에 작업을 구문 분석하는 데 유용합니다. 그러나 코딩 및 디버그하는 동안 수행할 수 있는 몇 가지 조정이 여전히 있습니다.

부동 소수점 산술, 다차원 배열 또는 라이브러리 함수를 많이 사용하는 영역을 결정합니다. 이러한 영역 중 성능이 중요한 영역은 무엇입니까? 프로파일러를 사용하여 오버헤드로 인해 비용이 가장 많이 드는 조각을 선택하고 가장 적합한 옵션을 선택합니다.

  • 전체 조각을 관리되지 않는 공간에 유지합니다.
  • 라이브러리 액세스에서 정적 캐스트를 사용합니다.
  • boxing/unboxing 동작을 조정해 보세요(나중에 설명).
  • 고유한 구조를 코딩합니다.

마지막으로 전환 횟수를 최소화하기 위해 노력합니다. 관리되지 않는 코드가 있거나 interop 호출이 루프에 있는 경우 전체 루프를 관리되지 않도록 합니다. 이렇게 하면 루프의 각 반복이 아니라 전환 비용을 두 번만 지불합니다.

추가 리소스

.NET Framework 성능에 대한 관련 topics 다음과 같습니다.

디자인, 아키텍처 및 코딩 철학 개요, 관리되는 세계의 성능 분석 도구 연습, .NET과 현재 사용 가능한 다른 엔터프라이즈 애플리케이션의 성능 비교를 포함하여 현재 개발 중인 향후 문서를 확인하세요.

부록: 가상 호출 및 할당 비용

통화 유형 # 통화/초
ValueType 비 가상 호출 809971805.600
클래스 비 가상 호출 268478412.546
클래스 가상 호출 109117738.369
ValueType Virtual(Obj 메서드) 호출 3004286.205
ValueType Virtual(재정의된 Obj 메서드) 호출 2917140.844
새로 만들기로 형식 로드(비정적) 1434.720
새로 만들기로 형식 로드(가상 메서드) 1369.863

참고 테스트 머신은 서비스 팩 2에서 Windows 2000 Professional을 실행하는 PIII 733Mhz입니다.

이 차트는 다양한 유형의 메서드 호출과 관련된 비용과 가상 메서드가 포함된 형식을 인스턴스화하는 비용을 비교합니다. 숫자가 높을수록 초당 더 많은 호출/인스턴스화를 수행할 수 있습니다. 이러한 숫자는 확실히 다른 컴퓨터 및 구성에 따라 달라지지만, 다른 호출을 통해 한 호출을 수행하는 상대적인 비용은 여전히 중요합니다.

  • ValueType 비 가상 호출: 이 테스트는 ValueType 내에 포함된 빈 가상이 아닌 메서드를 호출합니다.
  • 클래스 비 가상 호출: 이 테스트는 클래스 내에 포함된 빈 가상이 아닌 메서드를 호출합니다.
  • 클래스 가상 호출: 이 테스트는 클래스 내에 포함된 빈 가상 메서드를 호출합니다.
  • ValueType Virtual(Obj 메서드) 호출: 이 테스트는 기본 개체 메서드를 사용하는 ValueType에서 ToString()( 가상 메서드)을 호출합니다.
  • ValueType Virtual(재정의된 Obj 메서드) 호출: 이 테스트는 기본값을 재정의한 ValueType에서 ToString()( 가상 메서드)을 호출합니다.
  • 새로 만들기로 형식 로드(정적): 이 테스트는 정적 메서드만 있는 클래스에 대한 공간을 할당합니다.
  • 새로 만들기로 형식 로드(가상 메서드): 이 테스트는 가상 메서드가 있는 클래스에 대한 공간을 할당합니다.

한 가지 결론은 가상 함수 호출이 클래스에서 메서드를 호출할 때 일반 호출보다 약 2배 더 비싸다는 것입니다. 호출은 시작하기에 저렴하므로 모든 가상 호출을 제거하지는 않습니다. 이렇게 하려면 항상 가상 메서드를 사용해야 합니다.

  • JIT는 가상 메서드를 인라인할 수 없으므로 가상이 아닌 메서드를 제거하면 잠재적인 최적화가 손실됩니다.
  • 가상 메서드가 있는 개체에 대한 공간 할당은 가상 테이블의 공간을 찾기 위해 추가 작업을 수행해야 하므로 가상 메서드가 없는 개체에 대한 할당보다 약간 느립니다.

ValueType 내에서 가상이 아닌 메서드를 호출하는 것은 클래스보다 3배 이상 빠르지만 클래스 처리하면 몹시 손실됩니다. 이는 ValueType의 특징입니다. 구조체처럼 취급하고 빠르게 조명합니다. 클래스처럼 취급하고 고통스럽게 느립니다. ToString() 은 가상 메서드이므로 호출하기 전에 구조체를 힙의 개체로 변환해야 합니다. ValueType에서 가상 메서드를 호출하는 속도가 두 배 느려지는 대신 이제 18배 느립니다. 이야기의 도덕? ValueType을 클래스로 처리하지 마세요.

이 문서에 대한 질문이나 의견이 있는 경우 프로그램 관리자인 클라우디오 칼다토(Claudio Caldato)에게 .NET Framework 성능 문제에 대해 문의하세요.