스택 크기가 50 배인 스레드를 기본값으로 만들 때 어떤 위험이 있습니까? ! 따라서 추가 조사없이이 옵션을

나는 현재 매우 성능이 중요한 프로그램을 연구 중이며 리소스 소비를 줄이는 데 도움이 될 수있는 하나의 경로로 작업자 스레드의 스택 크기를 늘리고 있으므로 액세스 할 데이터의 대부분을 이동할 float[]수 있습니다 스택 (을 사용하여 stackalloc).

나는 한 읽기 스레드의 기본 스택 크기가 너무 내 모든 이동하기 위해, 1MB입니다 것을 float[]내가 (50메가바이트 ~에) 약 50 배 스택을 확장해야 할 것들.

나는 이것이 일반적으로 “안전하지 않은”것으로 간주되고 권장되지 않는다는 것을 이해하지만이 방법에 대해 현재 코드를 벤치 마크 한 후 처리 속도 530 % 증가 함을 ! 따라서 추가 조사없이이 옵션을 간단히 통과 할 수 없기 때문에 내 질문으로 이어집니다. 스택을 그렇게 큰 크기로 늘리는 것과 관련된 위험은 무엇이며 (잘못 될 수있는 것), 그러한 위험을 최소화하기 위해 어떤 예방책을 취해야합니까?

내 테스트 코드

public static unsafe void TestMethod1()
{
    float* samples = stackalloc float[12500000];

    for (var ii = 0; ii < 12500000; ii++)
    {
        samples[ii] = 32768;
    }
}

public static void TestMethod2()
{
    var samples = new float[12500000];

    for (var i = 0; i < 12500000; i++)
    {
        samples[i] = 32768;
    }
}



답변

테스트 코드를 Sam과 비교할 때 우리 모두가 옳았다 고 판단했습니다!
그러나 다른 것들에 대해 :

  • 메모리 액세스 (읽기 및 쓰기)는 스택, 전역 또는 힙 등 어디에서나 빠릅니다 .
  • 그러나이를 할당 하는 것은 스택에서 가장 빠르며 힙에서 가장 느립니다.

다음과 같이 진행됩니다 : stack<global < heap. (할당 시간)
기술적으로 스택 할당은 실제로 할당이 아니며 런타임은 스택의 일부 (프레임?)가 배열에 예약되어 있는지 확인합니다.

그래도 나는 이것을 조심하는 것이 좋습니다.
다음을 권장합니다.

  1. 자주 배열을 만들어야 할 때 함수를 떠나지 경우 (예 : 참조를 전달하여) 스택을 사용하면 크게 향상됩니다.
  2. 어레이를 재활용 할 수 있다면 가능할 때마다 재활용하십시오! 힙은 장기 오브젝트 스토리지에 가장 적합한 장소입니다. (글로벌 메모리를 오염시키는 것은 좋지 않습니다; 스택 프레임이 사라질 수 있습니다)

( 참고 : 1. 값 유형에만 적용됩니다. 참조 유형은 힙에 할당되며 혜택은 0으로 줄어 듭니다)

질문 자체에 대답하기 위해 : 대형 스택 테스트에서 전혀 문제가 발생하지 않았습니다.
시스템이 부족한 경우 스레드를 만들 때 함수 호출에주의하고 메모리가 부족하면 스택 오버플로가 발생할 수있는 유일한 문제라고 생각합니다.

아래 섹션은 나의 초기 답변입니다. 잘못된 것으로 테스트가 정확하지 않습니다. 참조 용으로 만 유지됩니다.


내 테스트는 스택 할당 메모리와 전역 메모리가 어레이에서 사용하기 위해 힙 할당 메모리보다 적어도 15 % 느리다는 것을 나타냅니다 (시간 120 % 소요)!

이것은 내 테스트 코드 이며 샘플 출력입니다.

Stack-allocated array time: 00:00:00.2224429
Globally-allocated array time: 00:00:00.2206767
Heap-allocated array time: 00:00:00.1842670
------------------------------------------
Fastest: Heap.

  |    S    |    G    |    H    |
--+---------+---------+---------+
S |    -    | 100.80 %| 120.72 %|
--+---------+---------+---------+
G |  99.21 %|    -    | 119.76 %|
--+---------+---------+---------+
H |  82.84 %|  83.50 %|    -    |
--+---------+---------+---------+
Rates are calculated by dividing the row's value to the column's.

.NET 4.5.1에서 i7 4700 MQ를 사용하여 Windows 8.1 Pro (업데이트 1 포함)에서 테스트했습니다.
테스트했으며 x86 및 x64로 테스트했으며 결과는 동일합니다.

편집 : 모든 스레드 201MB의 스택 크기를 늘리고 샘플 크기를 5 천만으로 늘리고 반복을 5로 줄였습니다
. 결과는 위와 같습니다.

Stack-allocated array time: 00:00:00.4504903
Globally-allocated array time: 00:00:00.4020328
Heap-allocated array time: 00:00:00.3439016
------------------------------------------
Fastest: Heap.

  |    S    |    G    |    H    |
--+---------+---------+---------+
S |    -    | 112.05 %| 130.99 %|
--+---------+---------+---------+
G |  89.24 %|    -    | 116.90 %|
--+---------+---------+---------+
H |  76.34 %|  85.54 %|    -    |
--+---------+---------+---------+
Rates are calculated by dividing the row's value to the column's.

그러나 스택이 실제로 느려지 는 것처럼 보입니다 .


답변

처리 속도가 530 % 증가했습니다.

그것은 내가 말한 가장 큰 위험입니다. 벤치 마크에 심각한 문제가 있습니다.이 코드를 예측할 수없는 코드는 어딘가에 숨겨진 버그가 있습니다.

과도한 재귀를 제외하고 .NET 프로그램에서 많은 스택 공간을 사용하는 것은 매우 어렵습니다. 관리되는 메소드의 스택 프레임 크기는 스톤으로 설정됩니다. 단순히 메소드의 인수와 메소드의 로컬 변수의 합계입니다. CPU 레지스터에 저장 될 수있는 것을 빼면 무시할 수 있습니다.

스택 크기를 늘려도 아무 것도 달성 할 수 없으며 사용되지 않는 주소 공간을 확보하게됩니다. 물론 메모리를 사용하지 않음으로써 성능 향상을 설명 할 수있는 메커니즘은 없습니다.

이것은 기본 프로그램, 특히 C로 작성된 프로그램과 달리 스택 프레임에 배열을위한 공간을 예약 할 수도 있습니다. 스택 버퍼 뒤의 기본 맬웨어 공격 벡터가 넘칩니다. C #에서도 가능합니다.stackalloc 키워드 . 그렇게하면 명백한 위험은 임의의 스택 프레임 손상뿐만 아니라 그러한 공격에 노출되는 안전하지 않은 코드를 작성해야합니다. 버그를 진단하기가 매우 어렵습니다. 나중에 지터에서 이에 대한 대책이 있습니다. 지터가 .NET 4.0부터 시작하여 스택 프레임에 “쿠키”를 넣는 코드를 생성하고 메소드가 반환 될 때 여전히 쿠키가 손상되지 않았는지 확인합니다. 사고가 발생할 경우이를 가로 채거나보고 할 수있는 방법없이 데스크탑에 즉시 충돌이 발생합니다. 그것은 … 사용자의 정신 상태에 위험합니다.

운영 체제에서 시작한 프로그램의 기본 스레드는 x64를 대상으로 프로그램을 컴파일 할 때 기본적으로 1MB 스택, 4MB를 갖습니다. 빌드 빌드 후 이벤트에서 / STACK 옵션을 사용하여 Editbin.exe를 실행해야합니다. 32 비트 모드에서 실행할 때 프로그램을 시작하기 전에 일반적으로 최대 500MB를 요청할 수 있습니다. 스레드는 물론 훨씬 쉬울 수 있으며 위험 영역은 일반적으로 32 비트 프로그램의 경우 약 90MB를 가리 킵니다. 프로그램이 오랫동안 실행되고 주소 공간이 이전 할당에서 조각난 경우 트리거됩니다. 이 실패 모드를 얻으려면 전체 주소 공간 사용량이 공연보다 높아야합니다.

코드를 세 번 확인하십시오. 매우 잘못된 것이 있습니다. 명시 적으로 코드를 작성하지 않으면 더 큰 스택으로 x5 속도를 높일 수 없습니다. 항상 안전하지 않은 코드가 필요합니다. C #에서 포인터를 사용하면 항상 더 빠른 코드를 생성 할 수있는 이점이 있으며 배열 범위 검사를받지 않습니다.


답변

나는 권한, GC (스택을 스캔해야 함) 등을 예측하는 방법을 모르는 예약을 할 것입니다. 모두 영향을받을 수 있습니다. 관리되지 않는 메모리를 대신 사용하고 싶을 것입니다.

var ptr = Marshal.AllocHGlobal(sizeBytes);
try
{
    float* x = (float*)ptr;
    DoWork(x);
}
finally
{
    Marshal.FreeHGlobal(ptr);
}


답변

잘못 될 수있는 한 가지는 그렇게 할 권한이 없다는 것입니다. 완전 신뢰 모드에서 실행하지 않는 한 프레임 워크는 더 큰 스택 크기에 대한 요청을 무시합니다 (MSDN 참조 Thread Constructor (ParameterizedThreadStart, Int32)).

시스템 스택 크기를 엄청난 수로 늘리는 대신 힙에서 반복 및 수동 스택 구현을 사용하도록 코드를 다시 작성하는 것이 좋습니다.


답변

고성능 C는 일반 C #과 같은 방식으로 액세스 할 수 있지만 문제의 시작일 수 있습니다. 다음 코드를 고려하십시오.

float[] someArray = new float[100]
someArray[200] = 10.0;

범위를 벗어난 예외가 예상되며 요소 200에 액세스하려고하지만 허용되는 최대 값은 99이므로이 방법이 완전히 의미가 있습니다. stackalloc 경로로 이동하면 배열 주위에 바운드 검사를하는 객체가 없습니다. 다음은 예외를 나타내지 않습니다.

Float* pFloat =  stackalloc float[100];
fFloat[200]= 10.0;

위에서 당신은 100 개의 float를 저장하기에 충분한 메모리를 할당하고이 메모리의 시작 위치에서 시작하는 sizeof (float) 메모리 위치 + float 값 10을 유지하기 위해 200 * sizeof (float)를 설정하고 있습니다. 플로트에 할당 된 메모리이며 아무도 그 주소에 무엇을 저장할 수 있는지 알 수 없습니다. 운이 좋으면 현재 사용하지 않는 메모리를 사용했을 수도 있지만 동시에 다른 변수를 저장하는 데 사용 된 위치를 덮어 쓸 수도 있습니다. 요약 : 예측할 수없는 런타임 동작.


답변

Java 또는 C #과 같은 JIT 및 GC를 사용한 마이크로 벤치마킹 언어는 약간 복잡 할 수 있으므로 일반적으로 기존 프레임 워크를 사용하는 것이 좋습니다. Java는 mhf 또는 Caliper를 제공합니다. 그들에게 다가가는 것. Jon Skeet는 이것을 썼다 맹목적으로 가장 중요한 것들을 돌보는 것으로 가정했습니다. 워밍업 후 테스트 당 30 초가 내 인내심이 너무 높기 때문에 타이밍을 약간 조정했습니다 (5 초).

따라서 먼저 Windows 7 x64의 .NET 4.5.1 결과-숫자는 5 초 동안 실행될 수있는 반복을 나타내므로 높을수록 좋습니다.

x64 JIT :

Standard       10,589.00  (1.00)
UnsafeStandard 10,612.00  (1.00)
Stackalloc     12,088.00  (1.14)
FixedStandard  10,715.00  (1.01)
GlobalAlloc    12,547.00  (1.18)

x86 JIT (그래서 여전히 슬프다) :

Standard       14,787.00   (1.02)
UnsafeStandard 14,549.00   (1.00)
Stackalloc     15,830.00   (1.09)
FixedStandard  14,824.00   (1.02)
GlobalAlloc    18,744.00   (1.29)

이것은 최대 14 %의 훨씬 더 합리적인 속도를 제공합니다 (그리고 대부분의 오버 헤드는 GC를 실행해야하기 때문에 현실적으로 최악의 시나리오라고 생각합니다). x86 결과는 흥미 롭습니다-무슨 일이 일어나고 있는지 완전히 명확하지는 않습니다.

코드는 다음과 같습니다.

public static float Standard(int size) {
    float[] samples = new float[size];
    for (var ii = 0; ii < size; ii++) {
        samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
    }
    return samples[size - 1];
}

public static unsafe float UnsafeStandard(int size) {
    float[] samples = new float[size];
    for (var ii = 0; ii < size; ii++) {
        samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
    }
    return samples[size - 1];
}

public static unsafe float Stackalloc(int size) {
    float* samples = stackalloc float[size];
    for (var ii = 0; ii < size; ii++) {
        samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
    }
    return samples[size - 1];
}

public static unsafe float FixedStandard(int size) {
    float[] prev = new float[size];
    fixed (float* samples = &prev[0]) {
        for (var ii = 0; ii < size; ii++) {
            samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
        }
        return samples[size - 1];
    }
}

public static unsafe float GlobalAlloc(int size) {
    var ptr = Marshal.AllocHGlobal(size * sizeof(float));
    try {
        float* samples = (float*)ptr;
        for (var ii = 0; ii < size; ii++) {
            samples[ii] = 32768 + (ii != 0 ? samples[ii - 1] : 0);
        }
        return samples[size - 1];
    } finally {
        Marshal.FreeHGlobal(ptr);
    }
}

static void Main(string[] args) {
    int inputSize = 100000;
    var results = TestSuite.Create("Tests", inputSize, Standard(inputSize)).
        Add(Standard).
        Add(UnsafeStandard).
        Add(Stackalloc).
        Add(FixedStandard).
        Add(GlobalAlloc).
        RunTests();
    results.Display(ResultColumns.NameAndIterations);
}


답변

성능 차이가 너무 크기 때문에 문제는 할당과 거의 관련이 없습니다. 어레이 액세스 때문일 수 있습니다.

함수의 루프 본문을 분해했습니다.

TestMethod1 :

IL_0011:  ldloc.0
IL_0012:  ldloc.1
IL_0013:  ldc.i4.4
IL_0014:  mul
IL_0015:  add
IL_0016:  ldc.r4 32768.
IL_001b:  stind.r4 // <----------- This one
IL_001c:  ldloc.1
IL_001d:  ldc.i4.1
IL_001e:  add
IL_001f:  stloc.1
IL_0020:  ldloc.1
IL_0021:  ldc.i4 12500000
IL_0026:  blt IL_0011

TestMethod2 :

IL_0012:  ldloc.0
IL_0013:  ldloc.1
IL_0014:  ldc.r4 32768.
IL_0019:  stelem.r4 // <----------- This one
IL_001a:  ldloc.1
IL_001b:  ldc.i4.1
IL_001c:  add
IL_001d:  stloc.1
IL_001e:  ldloc.1
IL_001f:  ldc.i4 12500000
IL_0024:  blt IL_0012

ECMA 사양 에서 발생하는 예외와 명령 사용법을 확인할 수 있습니다 .

stind.r4: Store value of type float32 into memory at address

예외가 발생합니다.

System.NullReferenceException

stelem.r4: Replace array element at index with the float32 value on the stack.

예외는 다음과 같습니다.

System.NullReferenceException
System.IndexOutOfRangeException
System.ArrayTypeMismatchException

보시다시피, stelem배열 범위 검사 및 유형 검사에서 더 많은 작업을 수행합니다. 루프 본문은 거의 수행하지 않기 때문에 (값만 할당), 검사 오버 헤드가 계산 시간을 지배합니다. 따라서 성능이 530 % 차이가 나는 이유입니다.

그리고 이것은 또한 귀하의 질문에 대답합니다 : 위험은 배열 범위 및 유형 검사가 없다는 것입니다. 이것은 함수 선언; D에서 언급 한 것처럼 안전하지 않습니다.