객체 지향이 실제로 알고리즘 성능에 영향을 줍니까? 빠르고 쉽게

객체 지향은 많은 알고리즘을 구현하는 데 많은 도움이되었습니다. 그러나 객체 지향 언어는 때때로 “직접적인”접근 방식을 안내하며이 접근 방식이 항상 좋은지 의심 스럽습니다.

OO는 알고리즘을 빠르고 쉽게 코딩하는 데 실제로 도움이됩니다. 그러나이 OOP는 성능에 기반한 소프트웨어 즉, 프로그램이 얼마나 빨리 실행되는지에 대한 단점이 될 수 있습니까?

예를 들어, 데이터 구조에 그래프 노드를 저장하는 것은 처음에는 “직접적인”것처럼 보이지만 Node 객체에 많은 속성과 메소드가 포함되어 있으면 알고리즘이 느려질 수 있습니까?

다시 말해서, 많은 다른 객체들 사이에 많은 참조가 있거나 많은 클래스의 많은 메소드를 사용하면 “무거운”구현이 될 수 있습니까?



답변

객체 지향은 캡슐화로 인해 특정 알고리즘 최적화를 방해 할 수 있습니다. 두 알고리즘이 특히 잘 작동 할 수 있지만 OO 인터페이스 뒤에 숨겨져 있으면 시너지를 사용할 가능성이 없어집니다.

숫자 라이브러리를보십시오. 그들 중 많은 것들 (60 년대 나 70 년대에 쓰여진 것들만이 아니라)은 OOP가 아닙니다. 숫자 알고리즘 modules이 인터페이스와 캡슐화가있는 OO 계층 구조보다 분리 된 세트로 더 잘 작동하는 이유가 있습니다 .


답변

성능을 결정하는 요소는 무엇입니까?

기본 사항 : 데이터 구조, 알고리즘, 컴퓨터 아키텍처, 하드웨어. 오버 헤드가 추가되었습니다.

CS 이론에 의해 최적으로 간주되는 데이터 구조 및 알고리즘의 선택과 정확하게 일치 하도록 OOP 프로그램을 설계 할 수 있습니다 . 최적의 프로그램과 동일한 성능 특성과 약간의 오버 헤드가 있습니다. 일반적으로 오버 헤드를 최소화 할 수 있습니다.

그러나 기본 사항에 관계없이 초기에 OOP 만 고려하여 설계된 프로그램은 초기 에 차선책 일 수 있습니다. 하위 최적화는 때때로 리팩토링으로 제거 할 수 있습니다. 때로는 완전 재 작성이 필요하지 않습니다.

주의 사항 : 비즈니스 소프트웨어의 성능이 중요합니까?

그렇습니다. 그러나 TTM (Time-to-Market)은 규모별로 중요합니다. 비즈니스 소프트웨어는 복잡한 비즈니스 규칙에 대한 코드의 적응성을 강조합니다. 개발 수명주기 동안 성능 측정을 수행해야합니다. (섹션 : 최적의 성능의 의미는 무엇입니까? ) 마케팅 가능한 기능 만 개선해야하며 이후 버전에서는 점진적으로 도입해야합니다.

최적의 성능은 무엇을 의미합니까?

일반적으로 소프트웨어 성능의 문제는 “더 빠른 버전이 존재 함”을 증명하기 위해 더 빠른 버전이 먼저 존재해야한다는 것입니다 (즉, 다른 증거는 없습니다).

때로는 더 빠른 버전이 다른 언어 나 패러다임에서 처음 발견되기도합니다. 이것은 다른 언어 나 패러다임의 열등 성을 판단하는 것이 아니라 개선의 힌트로 여겨 져야합니다.

최적의 성능을 찾는 데 방해가 될 수있는 이유는 무엇입니까?

OOP는 “작업 성”과 코드의 비즈니스 가치를 향상시키기 위해 오버 헤드 (공간 및 실행)를 도입합니다. 이는 추가 개발 및 최적화 비용을 줄입니다. @MikeNakis를 참조하십시오 .

OOP의 어느 부분이 초기에 최적이 아닌 디자인을 장려 할 수 있습니까?

(i) 단순성 / 직관성을 장려하고 (ii) 기본 대신 구어체 디자인 방법을 사용하는 OOP의 부분 (iii) 동일한 목적의 여러 맞춤형 구현을 권장하지 않습니다.

  • 키스
  • 야 그니
  • 마른
  • 기본에 똑같은 생각을하지 않고 객체 디자인 (예 : CRC 카드 사용)

일부 OOP 지침 (캡슐화, 메시지 전달, 한 가지 잘 수행)을 엄격하게 적용하면 처음에는 코드 속도가 느려집니다. 성능 측정은 이러한 문제를 진단하는 데 도움이됩니다. 데이터 구조와 알고리즘이 이론적으로 예측 된 최적 설계와 일치하는 한 일반적으로 오버 헤드를 최소화 할 수 있습니다.

OOP 오버 헤드에 대한 일반적인 완화 방법은 무엇입니까?

앞에서 언급했듯이 설계에 가장 적합한 데이터 구조를 사용합니다.

일부 언어는 일부 런타임 성능을 복구 할 수있는 코드 인라인을 지원합니다.

성능 저하없이 어떻게 OOP를 채택 할 수 있습니까?

OOP와 기본 사항을 모두 배우고 적용하십시오.

OOP를 엄격하게 준수하면 더 빠른 버전을 작성하지 못할 수 있습니다. 때로는 더 빠른 버전을 처음부터 새로 작성할 수 있습니다. 그렇기 때문에 서로 다른 알고리즘과 패러다임 (OOP, 일반, 기능, 수학, 스파게티)을 사용하여 여러 버전의 코드를 작성한 다음 최적화 도구를 사용하여 각 버전이 관찰 된 최대 성능에 접근 할 수 있습니다.

OOP에서 도움이되지 않는 코드 유형이 있습니까?

([@quant_dev], [@ SK-logic] 및 [@MikeNakis] 간의 토론에서 확대됨)

  1. 수학에서 비롯된 수치 레시피.
    • 수학 방정식과 변환 자체는 객체로 이해 될 수 있습니다.
    • 효율적인 실행 코드를 생성하려면 매우 정교한 코드 변환 기술이 필요합니다. 순진한 ( “화이트 보드”) 구현은 끔찍한 성능을 갖습니다.
    • 그러나 오늘날의 주류 컴파일러는 그렇게 할 수 없습니다.
    • 특수 소프트웨어 (MATLAB 및 Mathematica 등)에는 JIT 및 기호 솔버가 있으며 일부 하위 문제에 대해 효율적인 코드를 생성 할 수 있습니다. 이러한 특수 솔버는 특수 목적 컴파일러 (인간 판독 가능 코드와 기계 실행 가능 코드 간 조정자)로 간주되어 OOP 설계의 이점을 얻을 수 있습니다.
    • 각 하위 문제에는 자체 “컴파일러”및 “코드 변환”이 필요합니다. 따라서 이것은 매년 새로운 결과가 나타나는 매우 활발한 개방형 연구 분야입니다.
    • 연구 시간이 오래 걸리기 때문에 소프트웨어 작성자는 종이에 대한 최적화를 수행하고 최적화 된 코드를 소프트웨어에 기록해야합니다. 전사 된 코드는 실제로 이해하기 어려울 수 있습니다.
  2. 매우 낮은 수준의 코드.
      *

답변

그것은 컨테이너와 같은 객체 지향에 관한 것이 아닙니다. 이중 링크 목록을 사용하여 비디오 플레이어에 픽셀을 저장하면 문제가 발생합니다.

그러나 올바른 컨테이너를 사용하면 std :: vector가 배열보다 느릴 이유가 없으며 전문가가 이미 작성한 일반적인 알고리즘이 있기 때문에 홈 롤 배열 코드보다 빠릅니다.


답변

OOP는 분명히 좋은 아이디어이며 다른 좋은 아이디어와 마찬가지로 과도하게 사용될 수 있습니다. 내 경험상 그것은 과도하게 사용됩니다. 성능 저하 및 유지 보수성 저하 결과.

가상 함수 호출의 오버 헤드와는 아무런 관련이 없으며 옵티 마이저 / 지터와는 관련이 없습니다.

그것은 최고의 big-O 성능을 가지면서도 상수 요소가 매우 나쁜 데이터 구조와 관련이 있습니다. 이는 앱에 성능 제한 문제가있는 경우 다른 곳에 있다고 가정하여 수행됩니다.

이것이 나타나는 한 가지 방법은 초당 새로운 횟수 가 수행되는 것인데, 이는 O (1) 성능을 가진 것으로 가정되지만 수백에서 수천 개의 명령 (일치하는 삭제 또는 GC 시간 포함)을 실행할 수 있습니다 . 사용 된 객체를 저장하면 완화 할 수 있지만 코드가 덜 “깨끗해”집니다.

그것이 나타내는 또 다른 방법은 사람들이 속성 함수, 알림 핸들러, 기본 클래스 함수에 대한 호출, 일관성을 유지하기 위해 존재하는 모든 종류의 지하 함수 호출을 작성하도록 장려하는 방법입니다. 일관성을 유지하기 위해 성공률은 제한적이지만 낭비되는주기에서 크게 성공했습니다. 프로그래머는 정규화 된 데이터 의 개념을 이해 하지만 데이터베이스 디자인에만 적용하는 경향이 있습니다. OOP가 필요하지 않다고 알려주기 때문에 데이터 구조 설계에 적용하지 않습니다. 객체에 수정 된 비트를 설정하는 것만으로도 데이터 구조를 통해 쓰나미 업데이트가 발생할 수 있습니다. 왜냐하면 코드를 사용할 가치가있는 클래스는 수정 된 호출을 취하지 않고 저장 하기 때문입니다.

아마도 주어진 앱의 성능이 서면으로 괜찮을 수도 있습니다.

반면에 성능 문제가있는 경우 예를 들어 보겠습니다. 조정 방법에 있습니다. 다단계 프로세스입니다. 각 단계에서 일부 특정 활동은 많은 시간을 차지하며 더 빠른 것으로 대체 될 수 있습니다. ( “병목 현상”이라고 말하지 않았습니다. 이들은 프로파일 러가 찾기에 좋은 종류가 아닙니다.)이 프로세스는 데이터 구조의 도매 교체를 가속화하기 위해 종종 필요합니다. 종종 데이터 구조가 권장되는 OOP 방식이기 때문에 데이터 구조가 존재합니다.


답변

이론적으로 속도가 느려질 수 있지만 느린 알고리즘이 아니라 구현이 느려질 수 있습니다. 실제로 객체 지향을 사용하면 다양한 가정 시나리오를 시도하거나 향후 알고리즘을 다시 방문 할 수 있습니다. 알고리즘 개선을 할 수 있습니다. 작업이 어려울 것이기 때문입니다. (기본적으로 모든 것을 다시 작성해야합니다.)

예를 들어, 다양한 작업과 개체를 분할하여 개체를 정리하면 나중에 쉽게 들어 와서 일부 개체 사이에 캐싱 기능을 포함시킬 수 있습니다 (투명한 경우). 배 개선.

일반적으로, 저수준 언어 (또는 고급 언어를 사용하는 영리한 트릭)를 사용하여 달성 할 수있는 개선 유형은 지속적 (선형) 시간 개선을 제공하며, 이는 큰 표기법으로 표현되지 않습니다. 알고리즘 개선을 통해 비선형 개선을 달성 할 수 있습니다. 귀중합니다.


답변

그러나이 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 공용 인터페이스와 관련이 있고 나중에 마음이 바뀌기 . 레거시 코드베이스를 최적화하기 위해 많은 노력을 기울였으며, 대량 디자인을 사용하기 위해 수만 줄의 코드를 신중하게 다시 작성하는 데 수개월이 걸릴 수 있습니다. 이는로드를 많이 예상 할 수있는 경우 사전에 설계하는 방식에 이상적입니다.

많은 성능 질문, 특히 객체 지향 디자인과 관련된 질문에 어떤 형태로든이 답변을 계속 반영합니다. 객체 지향 디자인은 여전히 ​​최고 성능 요구 사항과 호환 될 수 있지만 생각 방식을 조금 바꿔야합니다. 우리는 그 치타에게 최대한 빨리 달릴 수있는 공간을 주어야합니다. 그리고 우리가 간신히 작은 상태의 물건을 저장하는 작은 물체를 설계한다면 불가능합니다.


답변

예, 객체 지향 사고 방식은 알고리즘 수준과 구현 수준 모두에서 고성능 프로그래밍에있어 중립적이거나 부정적 일 수 있습니다. OOP가 알고리즘 분석을 대체하는 경우 조기 구현으로 이어질 수 있으며 가장 낮은 수준에서는 OOP 추상화를 제외해야합니다.

이 문제는 개별 사례에 대한 OOP의 강조에서 비롯됩니다. 알고리즘에 대한 OOP 사고 방식은 특정 값 집합에 대해 생각하고 구현하는 것입니다. 그것이 최고 수준의 경로라면, 큰 O 이익을 가져올 변형이나 구조 조정을 실현하지 못할 것입니다.

알고리즘 수준에서는 종종 더 큰 그림과 큰 O 게인으로 이어지는 값 사이의 제약 조건 또는 관계에 대해 생각합니다. 예를 들어 OOP 마인드에 “연속적인 정수 범위 합”을 루프에서(max + min) * n/2

구현 수준에서는 컴퓨터가 대부분의 응용 프로그램 수준 알고리즘에 대해 “충분히 빠르지 만”낮은 수준의 성능이 중요한 코드에서는 로컬성에 대해 많은 우려가 있습니다. 다시 한 번, 개별 인스턴스에 대한 사고와 루프를 통과하는 하나의 값에 대한 OOP 강조는 부정적 일 수 있습니다. 고성능 코드에서는 간단한 루프를 작성하는 대신 루프를 부분적으로 풀고 상단에 여러로드 명령어를 그룹화 한 다음 그룹으로 변환 한 다음 그룹으로 작성하는 것이 좋습니다. 중간 계산과 캐시 및 메모리 액세스에 많은 관심을 기울이고 있습니다. OOP 추상화가 더 이상 유효하지 않은 문제. 그리고 따라 오면 오해의 소지가있을 수 있습니다.이 수준에서는 기계 수준 표현에 대해 알고 생각해야합니다.

인텔의 퍼포먼스 프리미티브와 같은 것을 살펴보면 문자 그대로 수천 개의 고속 푸리에 변환 (Fast Fourier Transform)을 구현했으며, 각각 특정 데이터 크기 및 시스템 아키텍처에 더 적합하도록 조정되었습니다. ( 정말로 , 이러한 구현의 대부분은 기계로 생성됩니다. Markus Püschel Automatic Performance Programming )

물론 대부분의 대답에서 알 수 있듯이 대부분의 개발에서 대부분의 알고리즘에서 OOP는 성능과 관련이 없습니다. “비조 기적으로 비관 화하지 않고 많은 비 로컬 호출을 추가 this하지 않는 한 , 포인터는 여기도 거기도 없습니다.