programing

내 코드를 빠르게 하는 시도?

oldcodes 2023. 5. 4. 20:31
반응형

내 코드를 빠르게 하는 시도?

저는 트라이캐치의 영향을 테스트하기 위한 코드를 작성했지만, 놀라운 결과를 보았습니다.

static void Main(string[] args)
{
    Thread.CurrentThread.Priority = ThreadPriority.Highest;
    Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.RealTime;

    long start = 0, stop = 0, elapsed = 0;
    double avg = 0.0;

    long temp = Fibo(1);

    for (int i = 1; i < 100000000; i++)
    {
        start = Stopwatch.GetTimestamp();
        temp = Fibo(100);
        stop = Stopwatch.GetTimestamp();

        elapsed = stop - start;
        avg = avg + ((double)elapsed - avg) / i;
    }

    Console.WriteLine("Elapsed: " + avg);
    Console.ReadKey();
}

static long Fibo(int n)
{
    long n1 = 0, n2 = 1, fibo = 0;
    n++;

    for (int i = 1; i < n; i++)
    {
        n1 = n2;
        n2 = fibo;
        fibo = n1 + n2;
    }

    return fibo;
}

내 컴퓨터에서는 0.96 정도의 값을 지속적으로 출력합니다.

Fibo() 내부의 for 루프를 다음과 같은 시도-캐치 블록으로 감쌀 때:

static long Fibo(int n)
{
    long n1 = 0, n2 = 1, fibo = 0;
    n++;

    try
    {
        for (int i = 1; i < n; i++)
        {
            n1 = n2;
            n2 = fibo;
            fibo = n1 + n2;
        }
    }
    catch {}

    return fibo;
}

지금은 꾸준히 0.69를 출력합니다. -- 실제로는 더 빨리 달립니다!하지만 왜 그랬을까?

참고: 릴리스 구성을 사용하여 이 파일을 컴파일하고 직접 EXE 파일을 실행했습니다(Visual Studio 외부).

편집: Jon Skeet의 훌륭한 분석은 트라이캐치가 어떻게든 x86 CLR이 이 특정한 경우에 CPU 레지스터를 더 유리한 방식으로 사용하게 한다는 것을 보여줍니다(그리고 우리는 아직 그 이유를 이해하지 못하고 있다고 생각합니다).x64 CLR은 이 차이가 없고 x86 CLR보다 빠르다는 Jon의 발견을 확인했습니다.또한 다음을 사용하여 테스트했습니다.int 내드유형의 대신 안에 있는 long. x86 CLR빠 x64 CLR 릅


업데이트: 로슬린이 이 문제를 해결한 것 같습니다.동일한 시스템, 동일한 CLR 버전 -- VS 2013과 함께 컴파일할 때는 위와 같은 문제가 남아 있지만 VS 2015와 함께 컴파일할 때는 문제가 사라집니다.

스택 사용의 최적화를 이해하는 것을 전문으로 하는 Roslin 엔지니어 중 한 명이 이것을 보고 C# 컴파일러가 로컬 변수 저장소를 생성하는 방식과 JIT 컴파일러가 해당 x86 코드에 스케줄링을 등록하는 방식 사이의 상호 작용에 문제가 있는 것 같다고 보고했습니다.결과적으로 로컬의 로드 및 저장소에 대한 코드 생성이 차선입니다.

우리 모두에게 불분명한 어떤 이유로, 문제가 있는 코드 생성 경로는 J일 때 회피됩니다.IT 담당자는 블록이 시도 방지 영역에 있음을 알고 있습니다.

이건 꽤 이상해요.J와 함께 후속 조치를 취하겠습니다.IT 팀에서 버그를 입력하여 문제를 해결할 수 있는지 확인합니다.

또한 활성화 기간 동안 스택의 특정 위치를 할당하는 대신 로컬이 언제 "일시적"으로 바뀔 수 있는지를 결정하기 위한 C# 및 VB 컴파일러의 알고리즘에 대한 Roslin의 개선 작업을 진행하고 있습니다.우리는 J가지역 주민들이 일찍 "죽을" 수 있는 시기에 대해 더 나은 힌트를 준다면 ITter는 레지스터 할당을 더 잘 할 수 있을 것입니다.

이것을 우리에게 알려주셔서 감사하고 이상한 행동에 대해 사과드립니다.

음, 당신이 시간을 재는 방식이 제게는 꽤나 끔찍해 보입니다.전체 루프의 시간을 조정하는 것이 훨씬 더 합리적일 것입니다.

var stopwatch = Stopwatch.StartNew();
for (int i = 1; i < 100000000; i++)
{
    Fibo(100);
}
stopwatch.Stop();
Console.WriteLine("Elapsed time: {0}", stopwatch.Elapsed);

그렇게 하면 작은 타이밍, 부동소수점 산술 및 누적 오차에 좌우되지 않습니다.

변경한 후에도 "non-catch" 버전이 "catch" 버전보다 느리는지 확인합니다.

편집: 네, 제가 직접 시도해 봤는데, 같은 결과가 나와요.이요캐해. try가 안 하는 , 저는트라/치가나인쁜비라을활하화는다것성니사만, 궁습했용지했금상아.[MethodImpl(MethodImplOptions.NoInlining)]하지만 도움이 되지 않았습니다...

기본적으로 최적화된 J를 살펴봐야 합니다.코드비그 아래에 정보기술(IT) 코드를 넣었군요, 아마...

편집: 몇 가지 추가 정보:

  • 시도/캐치를 사용하는 것은 단지n++;시키지만, 만큼은 아닙니다.
  • 예외예외)가 하면,ArgumentException.
  • 캡처 블록에 예외를 인쇄해도 여전히 빠릅니다.
  • 예외를 캐치 블록에 다시 던지면 다시 느려집니다.
  • 캐치 블록 대신 최종 블록을 사용하면 다시 느려집니다.
  • 캐치 블록뿐만 아니라 최종 블록도 사용하면 빠릅니다.

이상한...

좋아요, 분해가 있습니다...

이것은 C#2 컴파일러와 를 사용하는 것입니다.NET 2(32비트) CLR, mdbg로 분해(기계에 코드bg가 없기 때문에).디버거를 사용해도 여전히 동일한 성능 효과를 볼 수 있습니다. 버전은 Fast 전사다용니합음을 합니다.try변수 선언과 반환문 사이의 모든 것을 단지 하나로 차단합니다.catch{}핸러들 느린 은 트라이없는 .확실히 느린 버전은 트라이/캐치가 없는 경우를 제외하고 동일합니다.호출 코드(즉, 메인)는 두 경우 모두 동일하고 어셈블리 표현이 동일합니다(따라서 인라인 문제가 아닙니다).

빠른 버전을 위한 코드 분해:

 [0000] push        ebp
 [0001] mov         ebp,esp
 [0003] push        edi
 [0004] push        esi
 [0005] push        ebx
 [0006] sub         esp,1Ch
 [0009] xor         eax,eax
 [000b] mov         dword ptr [ebp-20h],eax
 [000e] mov         dword ptr [ebp-1Ch],eax
 [0011] mov         dword ptr [ebp-18h],eax
 [0014] mov         dword ptr [ebp-14h],eax
 [0017] xor         eax,eax
 [0019] mov         dword ptr [ebp-18h],eax
*[001c] mov         esi,1
 [0021] xor         edi,edi
 [0023] mov         dword ptr [ebp-28h],1
 [002a] mov         dword ptr [ebp-24h],0
 [0031] inc         ecx
 [0032] mov         ebx,2
 [0037] cmp         ecx,2
 [003a] jle         00000024
 [003c] mov         eax,esi
 [003e] mov         edx,edi
 [0040] mov         esi,dword ptr [ebp-28h]
 [0043] mov         edi,dword ptr [ebp-24h]
 [0046] add         eax,dword ptr [ebp-28h]
 [0049] adc         edx,dword ptr [ebp-24h]
 [004c] mov         dword ptr [ebp-28h],eax
 [004f] mov         dword ptr [ebp-24h],edx
 [0052] inc         ebx
 [0053] cmp         ebx,ecx
 [0055] jl          FFFFFFE7
 [0057] jmp         00000007
 [0059] call        64571ACB
 [005e] mov         eax,dword ptr [ebp-28h]
 [0061] mov         edx,dword ptr [ebp-24h]
 [0064] lea         esp,[ebp-0Ch]
 [0067] pop         ebx
 [0068] pop         esi
 [0069] pop         edi
 [006a] pop         ebp
 [006b] ret

느린 버전을 위한 코드 분해:

 [0000] push        ebp
 [0001] mov         ebp,esp
 [0003] push        esi
 [0004] sub         esp,18h
*[0007] mov         dword ptr [ebp-14h],1
 [000e] mov         dword ptr [ebp-10h],0
 [0015] mov         dword ptr [ebp-1Ch],1
 [001c] mov         dword ptr [ebp-18h],0
 [0023] inc         ecx
 [0024] mov         esi,2
 [0029] cmp         ecx,2
 [002c] jle         00000031
 [002e] mov         eax,dword ptr [ebp-14h]
 [0031] mov         edx,dword ptr [ebp-10h]
 [0034] mov         dword ptr [ebp-0Ch],eax
 [0037] mov         dword ptr [ebp-8],edx
 [003a] mov         eax,dword ptr [ebp-1Ch]
 [003d] mov         edx,dword ptr [ebp-18h]
 [0040] mov         dword ptr [ebp-14h],eax
 [0043] mov         dword ptr [ebp-10h],edx
 [0046] mov         eax,dword ptr [ebp-0Ch]
 [0049] mov         edx,dword ptr [ebp-8]
 [004c] add         eax,dword ptr [ebp-1Ch]
 [004f] adc         edx,dword ptr [ebp-18h]
 [0052] mov         dword ptr [ebp-1Ch],eax
 [0055] mov         dword ptr [ebp-18h],edx
 [0058] inc         esi
 [0059] cmp         esi,ecx
 [005b] jl          FFFFFFD3
 [005d] mov         eax,dword ptr [ebp-1Ch]
 [0060] mov         edx,dword ptr [ebp-18h]
 [0063] lea         esp,[ebp-4]
 [0066] pop         esi
 [0067] pop         ebp
 [0068] ret

에 각의경우에각,에,*디버거가 단순한 "단계 입력"으로 입력된 위치를 보여줍니다.

파일: 이제코살펴데각어버떻작동게다같것습니알있. 느린 더 적은 더 많은 공간을 합니다.그리고 느린 버전은 더 느리다고 생각합니다. 왜냐하면 더 적은 레지스터와 더 많은 스택 공간을 사용하기 때문입니다.은 값 경 우의 작은 n그게 더 빠를 수도 있지만 루프가 대부분의 시간을 차지할 때는 더 느립니다.

시도/캐치 블록은 더 많은 레지스터를 저장 및 복원하도록 강제하기 때문에 JIT는 이러한 레지스터도 루프에 사용합니다.전체적으로 성능이 향상됩니다.JIT가 "정상" 코드에서 많은 레지스터를 사용하지 않는 이 합리적인 결정인지는 명확하지 않습니다.

편집: 방금 제 x64 컴퓨터에서 이것을 시도했습니다.x64 CLR은 이 코드의 x86 CLR보다 훨씬 빠르며(약 3-4배 빠름) x64에서는 시도/캐치 블록이 눈에 띄는 차이를 보이지 않습니다.

두한 (Jon의 분해두차빠이가버른레전쌍다것보니지줍여을는사용다을한스터이는전버의▁(jon▁jon▁a▁of다▁uses보니▁the줍여▁registers▁thats▁is것▁pair▁version▁fast'▁showlies의,▁dis을▁that▁versionsassembrence▁between▁the▁two)를 사용한다는 것입니다.esi,edi로컬 변수 중 하나를 저장합니다(슬로우 버전은 저장하지 않습니다.

JIT 컴파일러는 트라이캐치 블록을 포함하는 코드와 그렇지 않은 코드의 레지스터 사용에 대해 서로 다른 가정을 합니다.이렇게 하면 다른 레지스터 할당을 선택할 수 있습니다.이 경우, 이것은 트라이캐치 블록이 있는 코드에 적합합니다.코드가 다르면 반대 효과가 발생할 수 있으므로 범용 스피드업 기술로 간주하지 않습니다.

결국, 어떤 코드가 가장 빨리 실행될지는 알 수 없습니다.레지스터 할당 및 이에 영향을 미치는 요인과 같은 것은 매우 낮은 수준의 구현 세부사항이므로 특정 기술이 어떻게 더 빠른 코드를 안정적으로 생성할 수 있는지 모르겠습니다.

예를 들어, 다음 두 가지 방법을 생각해 보십시오.이들은 실제 사례에서 수정되었습니다.

interface IIndexed { int this[int index] { get; set; } }
struct StructArray : IIndexed { 
    public int[] Array;
    public int this[int index] {
        get { return Array[index]; }
        set { Array[index] = value; }
    }
}

static int Generic<T>(int length, T a, T b) where T : IIndexed {
    int sum = 0;
    for (int i = 0; i < length; i++)
        sum += a[i] * b[i];
    return sum;
}
static int Specialized(int length, StructArray a, StructArray b) {
    int sum = 0;
    for (int i = 0; i < length; i++)
        sum += a[i] * b[i];
    return sum;
}

하나는 다른 하나의 일반 버전입니다.을 일반유대로 StructArray방법을 동일하게 만들 수 있습니다. ㅠㅠStructArray값 형식이며, 일반 메서드의 컴파일된 자체 버전을 가져옵니다.그러나 실제 실행 시간은 전문적인 방법보다 상당히 길지만 x86의 경우에만 가능합니다.x64의 경우 시간이 거의 동일합니다.다른 경우에도 x64에 대한 차이를 관찰했습니다.

이것은 인라인이 고장난 경우처럼 보입니다.x86 코어에서 지터는 ebx, edx, esi 및 edi 레지스터를 로컬 변수의 범용 저장에 사용할 수 있습니다.ecx 레지스터는 정적 방법으로 사용할 수 있게 되며, 이를 저장할 필요가 없습니다.계산에는 종종 eax 레지스터가 필요합니다.그러나 이것들은 32비트 레지스터이며, 길이가 긴 변수의 경우 레지스터 쌍을 사용해야 합니다.계산에는 edx:eax, 스토리지에는 edi:ebx입니다.

느린 버전의 디스어셈블리에서 눈에 띄는 것은 에디나 ebx 둘 다 사용되지 않는다는 것입니다.

지터가 로컬 변수를 저장하기에 충분한 레지스터를 찾지 못하면 스택 프레임에서 로드 및 저장할 코드를 생성해야 합니다.이것은 코드 속도를 늦추고 레지스터의 여러 복사본을 사용하고 슈퍼 스칼라 실행을 허용하는 내부 프로세서 코어 최적화 트릭인 "register rename"이라는 프로세서 최적화를 방지합니다.이렇게 하면 같은 레지스터를 사용하는 경우에도 여러 명령을 동시에 실행할 수 있습니다.레지스터가 충분하지 않은 것은 8개의 추가 레지스터(r9 ~ r15)가 있는 x64에서 해결된 x86 코어의 일반적인 문제입니다.

지터는 다른 코드 생성 최적화를 적용하기 위해 최선을 다할 것이며, 피보() 메서드를 인라인화하려고 할 것입니다.즉, 메서드에 호출하지 않고 Main() 메서드에서 메서드에 대한 코드를 인라인으로 생성합니다.예를 들어, C# 클래스의 속성을 무료로 만들어 필드의 성능을 제공하는 매우 중요한 최적화입니다.메서드를 호출하고 스택 프레임을 설정하는 오버헤드를 방지하여 몇 나노초의 시간을 절약할 수 있습니다.

메서드에 인라인을 적용할 수 있는 시기를 정확하게 결정하는 여러 규칙이 있습니다.그들은 정확히 문서화되어 있지 않지만 블로그 게시물에 언급되어 있습니다.한 가지 규칙은 메서드 본문이 너무 클 때는 발생하지 않는다는 것입니다.인라인을 사용하면 이득을 얻지 못하고 L1 명령 캐시에 맞지 않는 코드를 너무 많이 생성합니다.여기에 적용되는 또 다른 어려운 규칙은 메서드가 try/catch 문을 포함할 때 밑줄이 그어지지 않는다는 것입니다.그 배경에는 예외의 구현 세부 사항이 있습니다. 예외는 스택 프레임 기반의 SEH(Structure Exception Handling)에 대한 Windows의 기본 제공 지원에 다시 연결됩니다.

지터에서 레지스터 할당 알고리즘의 한 가지 동작은 이 코드를 가지고 노는 것에서 추론할 수 있습니다.이것은 지터가 메서드를 인라인화하려고 할 때 인식하는 것으로 보입니다.edx:eax 레지스터 쌍만 유형이 긴 로컬 변수가 있는 인라인 코드에 사용할 수 있다는 규칙이 있습니다.하지만 에디: ebx는 아닙니다.의심할 여지 없이, 그것은 호출 방법을 위한 코드 생성에 너무 해로울 것이기 때문에, edi와 ebx 모두 중요한 저장 레지스터입니다.

따라서 메서드 본문에 try/catch 문이 포함되어 있다는 것을 지터가 미리 알고 있기 때문에 빠른 버전을 얻을 수 있습니다.긴 변수를 저장하기 위해 edi:ebx를 쉽게 사용할 수 없다는 것을 알고 있습니다.지터가 인라인이 작동하지 않을 것이라는 것을 미리 몰랐기 때문에 느린 버전을 얻었습니다.메소드 본문에 대한 코드를 생성한 후에야 알게 되었습니다.

그러면 결함은 이전으로 돌아가서 메서드에 대한 코드를 다시 생성하지 않았다는 것입니다.그것이 작동해야 하는 시간적 제약을 고려할 때 이해할 수 있습니다.

x64에는 레지스터가 8개 더 있기 때문에 이 속도 저하는 발생하지 않습니다.또 다른 이유는 하나의 레지스터(락스처럼)에 함께 저장할 수 있기 때문입니다.그리고 지터가 레지스터를 선택하는 데 훨씬 더 유연하기 때문에 길이가 긴 대신 int를 사용할 때 속도 저하가 발생하지 않습니다.

저는 이것이 사실일 가능성이 정말로 확실하지 않기 때문에 코멘트로 넣었을 것입니다. 하지만 제가 기억하기로는 try/except 문은 컴파일러의 가비지 처리 메커니즘이 작동하는 방식에 대한 수정을 포함하지 않습니다. 즉, 객체 메모리 할당을 스택에서 재귀적인 방식으로 정리한다는 점입니다.이 경우 정리할 개체가 없거나 for 루프가 가비지 수집 메커니즘이 다른 수집 방법을 시행하기에 충분하다고 인식하는 폐쇄를 구성할 수 있습니다.아닐 수도 있지만, 다른 곳에서 논의되는 것을 본 적이 없기 때문에 언급할 가치가 있다고 생각했습니다.

9년 후에도 그 벌레는 여전히 그곳에 있습니다!다음을 통해 쉽게 확인할 수 있습니다.

   static void Main( string[] args )
    {
      int hundredMillion = 1000000;
      DateTime start = DateTime.Now;
      double sqrt;
      for (int i=0; i < hundredMillion; i++)
      {
        sqrt = Math.Sqrt( DateTime.Now.ToOADate() );
      }
      DateTime end = DateTime.Now;

      double sqrtMs = (end - start).TotalMilliseconds;

      Console.WriteLine( "Elapsed milliseconds: " + sqrtMs );

      DateTime start2 = DateTime.Now;

      double sqrt2;
      for (int i = 0; i < hundredMillion; i++)
      {
        try
        {
          sqrt2 = Math.Sqrt( DateTime.Now.ToOADate() );
        }
        catch (Exception e)
        {
          int br = 0;
        }
      }
      DateTime end2 = DateTime.Now;

      double sqrtMsTryCatch = (end2 - start2).TotalMilliseconds;

      Console.WriteLine( "Elapsed milliseconds: " + sqrtMsTryCatch );

      Console.WriteLine( "ratio is " + sqrtMsTryCatch / sqrtMs );

      Console.ReadLine();
    }

이 비율은 MSVS 2019의 최신 버전을 실행하는 내 컴퓨터에서 1 미만입니다.NET 4.6.1

언급URL : https://stackoverflow.com/questions/8928403/try-catch-speeding-up-my-code

반응형