디버깅 - 배열 항목 할당이 C#프로그램 성능을 저하시키는 이유는 무엇입니까?



visual studio (4)

나는 이것이 배열 할당으로 (정말로) 할 일이 없다고 생각한다. 나중에 참조 할 수 있도록 항목과 포함 된 객체를 보관해야하는 시간과 관련이 있습니다. 힙 할당 및 쓰레기 수거 세대와 관련이 있습니다.

처음 item 할당하면 문자열이 "generation 0"이됩니다. 이것은 종종 가비지 수집하고 매우 뜨거운, 어쩌면 캐시 된 메모리입니다. 루프의 다음 몇 번의 반복에서 전체 "세대 0"이 GC로 처리되고 메모리가 새 items 과 문자열에 재사용 될 가능성이 매우 높습니다. 할당을 배열에 추가하면 객체에 대한 참조가 여전히 있기 때문에 객체를 가비지 수집 할 수 없습니다. 이로 인해 메모리 소비가 증가합니다.

나는 당신이 코드를 실행하는 동안 메모리가 증가 할 것이라고 생각한다 : 문제는 항상 캐시 메모리 미스 (cache-misses)와 결합 된 힙의 메모리 할당은 "신선한"메모리를 사용해야하고 하드웨어 메모리 캐싱의 이점을 누릴 수 없기 때문이다.

https://src-bin.com

개요

대형 텍스트 파일을 처리하는 동안 설명 할 수없는 다음과 같은 예상치 못한 성능 저하가 발생했습니다. 이 질문에 대한 나의 목표는 다음과 같습니다.

  • 아래 설명 된 속도 저하 원인 파악
  • 대형 비 기본 배열을 신속하게 초기화하는 방법 알아보기

문제

  • 배열에 기본이 아닌 참조 항목이 있습니다.
    • int[] 아니라 MyComplexType[]
    • MyComplexType 은 클래스가 아니라 구조체입니다.
    • MyComplexType 에는 일부 string 속성이 포함되어 있습니다.
  • 배열이 미리 할당 됨
  • 배열이 큽니다.
  • 항목이 배열에 지정되지 않고 만들어지고 사용되면 프로그램이 빠릅니다.
  • 항목이 만들어져 배열 항목에 할당 되면 프로그램의 속도가 현저하게 떨어집니다
    • 배열 항목 할당이 간단한 참조 할당이 될 것으로 예상했으나 아래의 테스트 프로그램에 대한 내 결과를 기반으로하지는 않습니다.

테스트 프로그램

다음 C# 프로그램을 고려하십시오.

namespace Test
{
    public static class Program
    {
        // Simple data structure
        private sealed class Item
        {
            public Item(int i)
            {
                this.Name = "Hello " + i;
                //this.Name = "Hello";
                //this.Name = null;
            }
            public readonly string Name;
        }

        // Test program
        public static void Main()
        {
            const int length = 1000000;
            var items = new Item[length];

            // Create one million items but don't assign to array
            var w = System.Diagnostics.Stopwatch.StartNew();
            for (var i = 0; i < length; i++)
            {
                var item = new Item(i);
                if (!string.IsNullOrEmpty(item.Name)) // reference the item and its Name property
                {
                    items[i] = null; // do not remember the item
                }
            }
            System.Console.Error.WriteLine("Without assignment: " + w.Elapsed);

            // Create one million items and assign to array
            w.Restart();
            for (var i = 0; i < length; i++)
            {
                var item = new Item(i);
                if (!string.IsNullOrEmpty(item.Name)) // reference the item and its Name property
                {
                    items[i] = item; // remember the item
                }
            }
            System.Console.Error.WriteLine("   With assignment: " + w.Elapsed);
        }
    }
}

두 개의 거의 동일한 루프가 포함되어 있습니다. 각 루프는 1 백만 개의 Item 클래스 인스턴스를 만듭니다. 첫 번째 루프는 생성 된 항목을 사용한 다음 참조를 버립니다 ( items 배열에 유지되지 않음). 두 번째 루프는 생성 된 항목을 사용하고 items 배열에 참조를 저장합니다. 배열 항목 할당은 루프 간의 유일한 차이점입니다.

내 결과

  • 내 컴퓨터에서 Release 빌드 (최적화 활성화)를 실행하면 다음과 같은 결과가 나타납니다.

    Without assignment: 00:00:00.2193348
       With assignment: 00:00:00.8819170
    

    배열 할당 루프는 할당이없는 루프보다 느립니다 (~ 4 배 느림).

  • Name 속성에 상수 문자열을 할당하기 위해 Item 생성자를 변경하는 경우 :

    public Item(int i)
    {
        //this.Name = "Hello " + i;
        this.Name = "Hello";
        //this.Name = null;
    }
    

    나는 다음과 같은 결과를 얻는다.

    Without assignment: 00:00:00.0228067
       With assignment: 00:00:00.0718317
    

    과제가있는 루프는 아직없는 것보다 약 3 배 느립니다.

  • 마지막으로 Name 속성에 null 을 할당하면 다음과 같습니다.

    public Item(int i)
    {
        //this.Name = "Hello " + i;
        //this.Name = "Hello";
        this.Name = null;
    }
    

    나는 다음과 같은 결과를 얻는다 :

    Without assignment: 00:00:00.0146696
       With assignment: 00:00:00.0105369
    

    일단 문자열이 할당되지 않으면 할당이없는 버전이 마침내 약간 느려집니다 (모든 인스턴스가 가비지 수집을 위해 릴리스되기 때문에 가정합니다)

질문들

  • 배열 항목 할당이 테스트 프로그램을 너무 느리게하는 이유는 무엇입니까?

  • 할당을 가속화 할 속성 / 언어 구문 / etc가 있습니까?

추신 : 나는 속도 저하를 조사하기 위해 dotTrace를 사용해 보았지만 결정적이지 않았습니다. 내가 본 한 가지는 더 많은 문자열 복사 및 가비지 수집 오버 헤드가 할당없이 루프에있는 루프보다 할당 (비록 내가 그 반대를 기대 했음에도 불구하고)이었다.


Answer #1

나는 타이밍 문제의 대부분이 메모리 할당과 관련이 있다고 생각한다.

항목을 배열에 할당하면 가비지 수집 대상이되지 않습니다. 상수 (interned) 또는 널 (null)이 아닌 문자열로 속성을 지정하면 메모리 할당 요구 사항이 올라 가게됩니다.

첫 번째 경우에 나는 무엇이 일어나고 있는지 의심 스럽습니다. 개체를 빠르게 휘젓다가 Gen0에 머물러있어 신속하게 GCed 될 수 있고 메모리 세그먼트를 재사용 할 수 있습니다 . 이는 OS에서 더 많은 메모리를 할당 할 필요가 없다는 것을 의미합니다.

두 번째 경우에는 두 개의 할당 인 객체 내에서 문자열을 작성한 다음이를 저장하여 GC에 적합하지 않게합니다. 어떤 시점에서 더 많은 메모리를 가져와야하므로 할당 된 메모리가 확보됩니다.

최종 확인에 관해서는, Namenull 설정하면 if (!string.IsNullOrEmpty(item.Name)) 체크가 추가되지 않습니다. 따라서 첫 번째 코드는 약간 느리지 만 (두 번째 코드 경로는 JIT가 처음 실행되기 때문에) 동일하게됩니다.


Answer #2

시도하고 실제 문제를 해결하기 위해 (이것은 해결할 재미있는 퍼즐 이었지만). 나는 몇 가지 것을 권할 것이다 :

  1. 연결 문자열을 생성시 저장하지 마십시오. get 접근자를 사용하여 문자열 값을 반환하십시오. 배열 할당을 진단 할 때 문자열 연결을 그림에서 제거합니다. 계산 된 값을 처음 get 때 계산 된 값을 "캐시"하고 싶다면이 값을 확인해야합니다.
  2. 실제 프로그램에 대해 dotTrace를 실행하여 시간이 소비되는 장소를 더 잘 파악할 수 있습니다. 어레이 할당 속도를 높이기 위해 할 수있는 일은 없기 때문에 변경할 수있는 다른 영역을 찾으십시오.

Answer #3

제 생각에는 지점 예측의 희생자입니다. 당신이하는 일에 대해 자세히 살펴 보겠습니다.

"without assigment"의 경우 배열 항목 의 모든 요소에 null 을 할당하면 됩니다 . 그렇게함으로써 프로세서는 for 루프의 반복 후에 배열 항목에 동일한 값 (심지어 null )을 할당한다는 것을 알 수 있습니다. 따라서 if 문은 더 이상 필요하지 않습니다. 프로그램이 더 빨리 실행됩니다.

"With assignment"의 경우, 프로세서는 새로운 생성 된 항목의 진행을 알지 못합니다. if 문은 for 루프의 각 반복이라고 불립니다. 이것은 느리게 실행되는 프로그램으로 연결됩니다 ...

이 동작은 브랜치 예측 유닛 (Prediction Unit)이라는 프로세서 하드웨어의 일부 (칩의 상당 부분을 차지함)에 의존합니다. 비슷한 주제가 여기에 잘 설명되어 있습니다. 왜 정렬되지 않은 배열보다 정렬 된 어레이를 처리하는 것이 더 빠릅니까?





performance