문제

저는 Raytracer 취미 프로젝트를 하고 있는데 원래는 Vector 및 Ray 개체에 구조체를 사용하고 있었는데 Raytracer가 이를 사용하기에 완벽한 상황이라고 생각했습니다.수백만 개를 만들면 단일 방법보다 오래 지속되지 않으며 가볍습니다.그러나 Vector와 Ray에서 간단히 'struct'를 'class'로 변경함으로써 성능이 크게 향상되었습니다.

무엇을 제공합니까?둘 다 작으므로(벡터의 경우 플로트 3개, 광선의 경우 벡터 2개) 과도하게 복사되지 않습니다.물론 필요할 때 메서드에 전달하지만 이는 불가피합니다.그렇다면 구조체를 사용할 때 성능을 저하시키는 일반적인 함정은 무엇입니까?나는 읽었다 이것 MSDN 기사에는 다음과 같은 내용이 나와 있습니다.

이 예제를 실행하면 구조체 루프가 훨씬 더 빨라지는 것을 볼 수 있습니다.그러나 ValueType을 객체처럼 취급할 때는 사용에 주의하는 것이 중요합니다.이로 인해 프로그램에 추가 박싱 및 언박싱 오버헤드가 추가되고 결국 개체에 갇혔을 때보다 더 많은 비용이 발생할 수 있습니다!실제로 이를 확인하려면 foos 및 bar 배열을 사용하도록 위의 코드를 수정하세요.성능이 어느 정도 동일하다는 것을 알 수 있습니다.

그러나 그것은 꽤 오래된 것(2001년)이고 "배열에 넣으면 박싱/언박싱이 발생합니다"라는 전체 내용이 저를 이상하게 생각했습니다.그게 사실인가요?그러나 나는 기본 광선을 미리 계산하여 배열에 넣었으므로 이 기사를 선택하여 필요할 때 기본 광선을 계산하고 배열에 추가하지 않았지만 아무것도 변경되지 않았습니다.수업을 해도 여전히 1.5배 더 빨랐습니다.

저는 .NET 3.5 SP1을 실행 중인데 구조체 메서드가 인라인되지 않은 문제가 해결되었다고 생각합니다. 따라서 그럴 수도 없습니다.

그래서 기본적으로:팁, 고려해야 할 사항 및 피해야 할 사항은 무엇입니까?

편집하다:일부 답변에서 제안한 것처럼 구조체를 참조로 전달하려고 시도한 테스트 프로젝트를 설정했습니다.두 개의 벡터를 추가하는 방법:

public static VectorStruct Add(VectorStruct v1, VectorStruct v2)
{
  return new VectorStruct(v1.X + v2.X, v1.Y + v2.Y, v1.Z + v2.Z);
}

public static VectorStruct Add(ref VectorStruct v1, ref VectorStruct v2)
{
  return new VectorStruct(v1.X + v2.X, v1.Y + v2.Y, v1.Z + v2.Z);
}

public static void Add(ref VectorStruct v1, ref VectorStruct v2, out VectorStruct v3)
{
  v3 = new VectorStruct(v1.X + v2.X, v1.Y + v2.Y, v1.Z + v2.Z);
}

각각에 대해 다음 벤치마크 방법의 변형을 얻었습니다.

VectorStruct StructTest()
{
  Stopwatch sw = new Stopwatch();
  sw.Start();
  var v2 = new VectorStruct(0, 0, 0);
  for (int i = 0; i < 100000000; i++)
  {
    var v0 = new VectorStruct(i, i, i);
    var v1 = new VectorStruct(i, i, i);
    v2 = VectorStruct.Add(ref v0, ref v1);
  }
  sw.Stop();
  Console.WriteLine(sw.Elapsed.ToString());
  return v2; // To make sure v2 doesn't get optimized away because it's unused. 
}

모두 거의 동일한 성능을 보이는 것 같습니다.이 구조체를 전달하는 최적의 방법이 무엇이든 JIT에 의해 최적화될 수 있습니까?

편집2:테스트 프로젝트에서 구조체를 사용한다는 점에 주목해야 합니다. ~이다 클래스를 사용하는 것보다 약 50% 빠릅니다.왜 이것이 내 레이트레이서에서 다른지 모르겠습니다.

도움이 되었습니까?

해결책

기본적으로 너무 크게 만들지 말고 가능하면 심판으로 전달하세요.저도 똑같은 방법으로 발견했는데..내 Vector 및 Ray 클래스를 구조체로 변경합니다.

더 많은 메모리가 전달되면 캐시 스래싱이 발생하게 됩니다.

다른 팁

구조체 배열은 메모리에서 연속된 단일 구조인 반면, 객체 배열(참조 유형의 인스턴스)의 항목은 포인터(예:가비지 수집 힙의 개체에 대한 참조).따라서 한 번에 많은 항목 컬렉션을 처리하는 경우 구조체는 더 적은 간접 참조가 필요하므로 성능이 향상됩니다.또한 구조체는 상속될 수 없으므로 컴파일러가 추가 최적화를 수행할 수 있습니다(그러나 이는 단지 가능성일 뿐이며 컴파일러에 따라 다릅니다).

그러나 구조체는 할당 의미가 상당히 다르며 상속될 수도 없습니다.그러므로 나는 필요할 때 주어진 성능상의 이유를 제외하고는 일반적으로 구조체를 피합니다.


구조체

구조체(값 유형)로 인코딩된 값 배열은 메모리에서 다음과 같습니다.

vvvv

수업

클래스(참조 유형)로 인코딩된 값 배열은 다음과 같습니다.

pppp

..v..v...v.v..

여기서 p는 힙의 실제 값 v를 가리키는 this 포인터 또는 참조입니다.점은 힙에 산재할 수 있는 다른 개체를 나타냅니다.참조 유형의 경우 해당 p를 통해 v를 참조해야 하며, 값 유형의 경우 배열의 오프셋을 통해 직접 값을 가져올 수 있습니다.

구조체 사용 시기에 대한 권장 사항에서는 구조체가 16바이트보다 커서는 안 된다고 나와 있습니다.벡터는 12바이트로 한도에 가깝습니다.Ray에는 두 개의 벡터가 있으며 24바이트로 권장 제한을 확실히 초과합니다.

구조체가 16바이트보다 커지면 단일 명령어 세트로는 더 이상 효율적으로 복사할 수 없으며 대신 루프가 사용됩니다.따라서 이 "마법의" 제한을 통과하면 객체에 대한 참조를 전달할 때보다 구조체를 전달할 때 실제로 더 많은 작업을 수행하게 됩니다.이것이 객체를 할당할 때 더 많은 오버헤드가 있음에도 불구하고 클래스를 사용하면 코드가 더 빠른 이유입니다.

Vector는 여전히 구조체일 수 있지만 Ray는 구조체로 잘 작동하기에는 너무 큽니다.

.NET 제네릭 이전에 boxing/unboxing에 관해 작성된 모든 내용은 약간의 의미가 있을 수 있습니다.일반 컬렉션 유형을 사용하면 값 유형을 boxing 및 unboxing할 필요가 없으므로 이러한 상황에서 구조체를 사용하는 것이 더 가치 있습니다.

특정 속도 저하에 관해서는 아마도 일부 코드를 확인해야 할 것입니다.

내 생각에 핵심은 귀하의 게시물에 있는 다음 두 가지 진술에 있습니다.

당신은 수백만 개의 것을 만듭니다

그리고

물론 필요할 때 메서드에 전달합니다.

이제 구조체 크기가 4바이트(또는 64비트 시스템의 경우 8바이트)보다 작거나 같지 않으면 단순히 개체 참조를 전달한 경우보다 각 메서드 호출에서 훨씬 더 많은 것을 복사하게 됩니다.

제가 가장 먼저 찾아야 할 것은 Equals 및 GetHashCode를 명시적으로 구현했는지 확인하는 것입니다.이를 수행하지 못한다는 것은 이들 각각의 런타임 구현이 두 구조체 인스턴스를 비교하기 위해 매우 비용이 많이 드는 작업을 수행한다는 것을 의미합니다(내부적으로는 리플렉션을 사용하여 각 전용 필드를 결정한 다음 동일한지 확인하므로 상당한 양의 할당이 발생합니다). .

하지만 일반적으로 할 수 있는 가장 좋은 방법은 프로파일러에서 코드를 실행하고 느린 부분이 어디에 있는지 확인하는 것입니다.눈을 뜨게 만드는 경험이 될 수 있습니다.

애플리케이션을 프로파일링하셨나요?프로파일링은 실제 성능 문제가 어디에 있는지 확인할 수 있는 유일하고 확실한 방법입니다.구조체에는 일반적으로 더 좋고/나쁜 작업이 있지만 프로필을 작성하지 않으면 문제가 무엇인지 추측만 할 수 있습니다.

기능은 비슷하지만 일반적으로 구조가 클래스보다 더 효율적입니다.클래스보다는 구조를 정의해야 합니다. 유형이 더 나은 성능을 발휘할 경우 참조 유형이 아닌 값 유형으로 사용됩니다.

특히 구조 유형은 다음 기준을 모두 충족해야 합니다.

  • 단일 값을 논리적으로 나타냅니다.
  • 인스턴스 크기가 16바이트 미만입니다.
  • 생성 후에는 변경되지 않습니다.
  • 참조 유형으로 캐스팅되지 않습니다.

저는 기본적으로 매개변수 객체에 구조체를 사용하고, 함수에서 여러 정보를 반환하고...다른 것은 없습니다.그것이 "옳은" 것인지 "그른" 것인지는 모르지만, 나는 그렇게 한다.

내 레이 트레이서도 구조체 벡터(레이는 아님)를 사용하며 벡터를 클래스로 변경해도 성능에 아무런 영향을 미치지 않는 것 같습니다.현재 벡터에 3개의 double을 사용하고 있으므로 예상보다 클 수 있습니다.한 가지 주목할 점은 이것이 분명할 수도 있지만 저에게는 해당되지 않았으며 Visual Studio 외부에서 프로그램을 실행하는 것입니다.최적화된 릴리스 빌드로 설정하더라도 VS 외부에서 exe를 시작하면 엄청난 속도 향상을 얻을 수 있습니다.벤치마킹을 할 때마다 이 점을 고려해야 합니다.

구조체가 작고 한 번에 너무 많지 않은 경우 구조체를 힙이 아닌 스택(로컬 변수이고 클래스의 멤버가 아닌 한)에 배치해야 합니다. 이는 GC가 그렇지 않음을 의미합니다. 호출할 필요가 없으며 메모리 할당/할당 해제는 거의 즉각적으로 이루어져야 합니다.

구조체를 함수에 매개변수로 전달하면 구조체가 복사됩니다. 이는 더 많은 할당/할당 해제(스택에서 거의 즉각적이지만 여전히 오버헤드가 있음)를 의미할 뿐만 아니라 2개의 복사본 간에 데이터를 전송하는 데 따른 오버헤드도 발생합니다. .참조를 통해 전달하는 경우 데이터를 복사하는 것이 아니라 데이터를 읽을 위치를 알려주기 때문에 문제가 되지 않습니다.

이에 대해 100% 확신할 수는 없지만 'out' 매개변수를 통해 배열을 반환하면 속도가 향상될 수도 있다고 생각됩니다. 스택의 메모리는 이를 위해 예약되어 있고 스택으로 복사할 필요가 없기 때문입니다. 함수 호출이 끝나면 "해제"됩니다.

구조체를 Nullable 개체로 만들 수도 있습니다.사용자 정의 클래스를 생성할 수 없습니다.

~처럼

Nullable<MyCustomClass> xxx = new Nullable<MyCustomClass>

구조체가 null을 허용하는 경우

Nullable<MyCustomStruct> xxx = new Nullable<MyCustomStruct>

그러나 당신은 (분명히) 모든 상속 기능을 잃게 될 것입니다

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top