그러나이 OOP는 성능에 기반한 소프트웨어 즉, 프로그램이 얼마나 빨리 실행되는지에 대한 단점이 될 수 있습니까?
종종 그렇습니다 !!! 그러나...
다시 말해서, 많은 다른 객체들 사이에 많은 참조가 있거나 많은 클래스의 많은 메소드를 사용하면 "무거운"구현이 될 수 있습니까?
반드시 그런 것은 아닙니다. 언어 / 컴파일러에 따라 다릅니다. 예를 들어, 가상 함수를 사용하지 않는 경우 최적화 C ++ 컴파일러는 종종 오브젝트 오버 헤드를 0으로 축소합니다. 랩퍼를 작성하는 등의 작업을 수행 할 수 있습니다.int
이 평범한 이전 데이터 유형을 직접 사용하는 것만 큼 빠르게 수행되는 평범한 이전 포인터를 통해 하거나 범위가 지정된 스마트 포인터를 작성하는 등의 작업을 수행 할 수 있습니다.
Java와 같은 다른 언어에서는 객체에 약간의 오버 헤드가 있습니다 (종종 꽤 작은 객체이지만 실제로는 매우 작은 객체의 경우 드문 경우에는 천문학입니다). 예를 들어, 64 비트에서 4와 반대로 16 바이트를 사용하는 Integer
것보다 훨씬 덜 효율적 int
입니다. 그러나 이것은 단지 노골적인 폐기물이나 그런 종류의 것이 아닙니다. 그 대신에 Java는 모든 단일 사용자 정의 유형에 대해 균일하게 반영하는 기능과로 표시되지 않은 함수를 재정의하는 기능을 제공 final
합니다.
아래 객체 인터페이스를 최적화 할 수있는 최적화 C ++ 컴파일러 : 아직의 최선의 시나리오 보자 제로 오버 헤드를. 그럼에도 불구하고 OOP는 종종 성능을 저하시키고 피크에 도달하지 못하게합니다. 그것은 완전한 역설처럼 들릴 수 있습니다. 어떻게 될 수 있습니까? 문제는 다음과 같습니다.
인터페이스 디자인 및 캡슐화
문제는 컴파일러가 객체의 구조를 오버 헤드 없이 제로 오버 헤드 (C ++ 컴파일러를 최적화하는 경우에 매우 흔하게) 로 스쿼시 할 수있는 경우에도 세밀한 객체의 캡슐화 및 인터페이스 디자인 (및 종속성)이 종종 대중에 의해 집계되는 객체에 대한 가장 최적의 데이터 표현 (종종 성능에 중요한 소프트웨어의 경우)
이 예제를 보자 :
class Particle
{
public:
...
private:
double birth; // 8 bytes
float x; // 4 bytes
float y; // 4 bytes
float z; // 4 bytes
/*padding*/ // 4 bytes of padding
};
Particle particles[1000000]; // 1mil particles (~24 megs)
우리의 메모리 액세스 패턴은 단순히 이러한 파티클을 순차적으로 반복하고 각 프레임 주위로 반복적으로 이동하여 화면의 모서리에서 튀어 나와 결과를 렌더링하는 것입니다.
birth
입자가 연속적으로 모일 때 멤버를 올바르게 정렬하는 데 필요한 눈부신 4 바이트 패딩 오버 헤드가 이미 있습니다 . 메모리의 약 16.7 %가 정렬에 사용 된 데드 스페이스로 낭비됩니다.
요즘 우리는 기가 바이트의 DRAM을 가지고 있기 때문에 문제가 될 수 있습니다. 그러나 오늘날 우리가 가지고있는 가장 야수적인 기계조차도 CPU 캐시 (L3) 의 가장 느리고 가장 큰 영역에 관해서는 단지 8MB에 불과 합니다. 우리가 더 잘 맞지 않을수록 반복적 인 DRAM 액세스 측면에서 더 많은 비용을 지불하고 속도가 느려집니다. 갑자기 16.7 %의 메모리 낭비가 더 이상 사소한 일처럼 보이지 않습니다.
필드 정렬에 영향을주지 않고이 오버 헤드를 쉽게 제거 할 수 있습니다.
class Particle
{
public:
...
private:
float x; // 4 bytes
float y; // 4 bytes
float z; // 4 bytes
};
Particle particles[1000000]; // 1mil particles (~12 megs)
double particle_birth[1000000]; // 1mil particle births (~8 bytes)
이제 메모리를 24 메가에서 20 메가로 줄였습니다. 순차 액세스 패턴을 통해 머신은 이제이 데이터를 약간 더 빨리 소비합니다.
그러나이 birth
필드를 좀 더 자세히 살펴 보겠습니다 . 입자가 생성 (생성) 된 시작 시간을 기록한다고 가정 해 봅시다. 입자가 처음 생성 될 때와 10 초마다 입자가 화면의 임의의 위치에서 죽어 다시 태어날 지 여부를 확인하기 위해 필드에 액세스한다고 상상해보십시오. 이 경우 birth
냉장입니다. 성능이 중요한 루프에서는 액세스 할 수 없습니다.
결과적으로 실제 성능에 중요한 데이터는 20MB가 아니라 실제로 12MB의 연속 블록입니다. 자주 액세스하는 실제 핫 메모리 크기 가 절반 으로 줄었습니다 ! 우리의 오리지널 24 메가 바이트 솔루션에 비해 상당한 속도 향상을 기대하십시오 (측정 할 필요는 없습니다. 이미 이런 종류의 작업을 천 번 수행했지만 의심 스러우면 자유롭게 느끼십시오).
그러나 우리가 여기서 무엇을했는지 주목하십시오. 이 입자 객체의 캡슐화를 완전히 끊었습니다. 상태는 이제 Particle
유형의 개인 필드와 별도의 병렬 배열로 나뉩니다 . 그리고 세분화 된 객체 지향 디자인이 시작됩니다.
게임에서 단일 입자, 단일 픽셀, 단일 4 성분 벡터, 심지어 단일 "생물"객체와 같은 매우 세밀한 단일 객체의 인터페이스 디자인에 국한된 경우 최적의 데이터 표현을 표현할 수 없습니다 치타의 속도는 2 평방 미터의 작은 섬에 서있을 경우 낭비 될 것이며, 이는 매우 세분화 된 객체 지향 디자인이 종종 성능 측면에서하는 것입니다. 데이터 표현을 차선책으로 제한합니다.
더 나아가서 파티클을 움직이기 때문에 실제로 3 개의 개별 루프에서 x / y / z 필드에 액세스 할 수 있다고 가정 해 봅시다. 이 경우 8 개의 SPFP 연산을 병렬로 벡터화 할 수있는 AVX 레지스터를 사용하여 SoA 스타일 SIMD 내장 기능을 활용할 수 있습니다. 그러나 이렇게하려면 이제 다음 표현을 사용해야합니다.
float particle_x[1000000]; // 1mil particle X positions (~4 megs)
float particle_y[1000000]; // 1mil particle Y positions (~4 megs)
float particle_z[1000000]; // 1mil particle Z positions (~4 megs)
double particle_birth[1000000]; // 1mil particle births (~8 bytes)
이제 파티클 시뮬레이션으로 비행하고 있지만 파티클 디자인에 어떤 일이 일어 났는지 살펴보십시오. 그것은 완전히 철거되었고, 우리는 지금 4 개의 병렬 배열을보고 있으며 그것들을 어 그리 게이션하기위한 객체가 없습니다. 우리의 객체 지향 Particle
디자인은 말로 사라졌습니다.
이것은 사용자가 속도를 요구하는 성능이 중요한 분야에서 여러 번 일어 났으며 정확성이 더 요구되는 한 가지였습니다. 이 작은 조그만 객체 지향 설계는 철거되어야했고 계단식 파손은 종종 더 빠른 설계에 느린 지원 중단 전략을 사용해야했습니다.
해결책
위의 시나리오는 세분화 된 객체 지향 설계 에만 문제가 있습니다. 이러한 경우 SoA 담당자, 핫 / 콜드 필드 분할, 순차적 액세스 패턴의 패딩 감소 (패딩이 임의 액세스의 성능에 도움이되는 경우가 있음)의 결과로보다 효율적인 표현을 표현하기 위해 구조를 철거해야하는 경우가 종종 있습니다. AoS의 경우 패턴, 그러나 거의 항상 순차 액세스 패턴에 대한 장애) 등
그러나 우리는 그 최종 표현을 취하여 객체 지향 인터페이스를 모델링 할 수 있습니다.
// Represents a collection of particles.
class ParticleSystem
{
public:
...
private:
double particle_birth[1000000]; // 1mil particle births (~8 bytes)
float particle_x[1000000]; // 1mil particle X positions (~4 megs)
float particle_y[1000000]; // 1mil particle Y positions (~4 megs)
float particle_z[1000000]; // 1mil particle Z positions (~4 megs)
};
이제 우린 좋아 우리는 원하는 모든 객체 지향 상품을 얻을 수 있습니다. 치타에는 온 나라가 최대한 빨리 달려야합니다. 우리의 인터페이스 디자인은 더 이상 병목 현상을 일으키지 않습니다.
ParticleSystem
잠재적으로 추상적 일 수 있으며 가상 기능을 사용할 수 있습니다. 지금 당장 우리는 파티클 당 레벨 대신 파티클 레벨 에서 오버 헤드를 지불하고 있습니다. 오버 헤드는 개별 파티클 레벨에서 오브젝트를 모델링 할 경우의 1 / 1,000,000 번째입니다.
따라서 많은 부하를 처리하고 모든 종류의 프로그래밍 언어 (이 기술에는 C, C ++, Python, Java, JavaScript, Lua, Swift 등이 있습니다) 를 처리하는 성능이 중요한 영역의 솔루션입니다 . 인터페이스 디자인 및 아키텍처 와 관련되어 있으므로 "초기 최적화"라고 쉽게 레이블을 지정할 수 없습니다 . 단일 입자를 모델링하는 코드베이스를 클라이언트 종속성의 보트로드가있는 객체로 작성할 수는 없습니다.Particle's
공용 인터페이스와 관련이 있고 나중에 마음이 바뀌기 . 레거시 코드베이스를 최적화하기 위해 많은 노력을 기울였으며, 대량 디자인을 사용하기 위해 수만 줄의 코드를 신중하게 다시 작성하는 데 수개월이 걸릴 수 있습니다. 이는로드를 많이 예상 할 수있는 경우 사전에 설계하는 방식에 이상적입니다.
많은 성능 질문, 특히 객체 지향 디자인과 관련된 질문에 어떤 형태로든이 답변을 계속 반영합니다. 객체 지향 디자인은 여전히 최고 성능 요구 사항과 호환 될 수 있지만 생각 방식을 조금 바꿔야합니다. 우리는 그 치타에게 최대한 빨리 달릴 수있는 공간을 주어야합니다. 그리고 우리가 간신히 작은 상태의 물건을 저장하는 작은 물체를 설계한다면 불가능합니다.