거대한“스위치”설명 대신 OO 접근 방식을 사용해야하는 이유는 무엇입니까? “케이스”와 함께

저는 .Net, C # 상점에서 일하고 있으며 더 많은 객체 지향 접근 방식이 아닌 많은 “케이스”와 함께 코드에 거대한 Switch 문을 사용해야한다고 주장하는 동료가 있습니다. 그의 주장은 Switch 문이 “cpu jump table”로 컴파일되고 따라서 가장 빠른 옵션이라는 사실로 일관되게 되돌아갑니다.

나는 솔직히 이것에 대해 논쟁을하지 않습니다 … 왜냐하면 그가 무슨 말을하는지 알지 못하기 때문입니다.
그가 맞습니까?
방금 엉덩이를 말하는거야?
여기서 배우려고합니다.



답변

그는 아마도 오래된 C 해커 일 것입니다. 그렇습니다. .Net은 C ++이 아닙니다. .Net 컴파일러는 계속해서 나아지고 있으며 오늘날 대부분의 .Net 버전에서는 대부분의 영리한 핵이 역효과를냅니다. .Net JIT는 사용하기 전에 모든 기능을 한 번만 사용하므로 작은 기능이 바람직합니다. 따라서 프로그램의 LifeCycle 중에 일부 사례가 발생하지 않으면 JIT 컴파일시 비용이 발생하지 않습니다. 어쨌든 속도가 문제가되지 않으면 최적화가 없어야합니다. 프로그래머를 위해, 컴파일러를 위해 두 번째로 작성하십시오. 동료는 쉽게 확신 할 수 없으므로 체계적으로 코드를 작성하면 코드가 더 빠르다는 것을 경험적으로 증명할 수 있습니다. 그의 최악의 예 중 하나를 골라 더 나은 방법으로 다시 작성한 다음 코드가 더 빠르도록하십시오. 필요한 경우 체리 픽. 그런 다음 몇 백만 번 실행하고 프로파일 링하고 보여주십시오.

편집하다

빌 바그너는 다음과 같이 썼습니다.

항목 11 : 작은 함수의 매력 이해 (효과적인 C # Second Edition) C # 코드를 컴퓨터 실행 가능 코드로 변환하는 것은 2 단계 프로세스입니다. C # 컴파일러는 어셈블리로 제공되는 IL을 생성합니다. JIT 컴파일러는 필요에 따라 각 메소드 (또는 인라인이 관련된 메소드 그룹)에 대한 머신 코드를 생성합니다. 작은 함수를 사용하면 JIT 컴파일러가 해당 비용을 쉽게 상각 할 수 있습니다. 작은 함수도 인라인 후보가 될 가능성이 높습니다. 단지 작은 것이 아닙니다. 간단한 제어 흐름이 중요합니다. 함수 내부의 제어 분기가 줄어들면 JIT 컴파일러가 변수를 쉽게 등록 할 수 있습니다. 더 명확한 코드를 작성하는 것은 좋은 습관이 아닙니다. 런타임에보다 효율적인 코드를 만드는 방법입니다.

EDIT2 :

따라서 … 한 비교는 대수이고 다른 비교는 선형이기 때문에 switch 문이 if / else 문보다 빠르고 빠릅니다.
http://sequence-points.blogspot.com/2007/10/why-is-switch-statement-faster-than-if.html

글쎄, 거대한 switch 문을 대체하는 가장 좋아하는 접근법은 사전 (또는 때로는 열거 형이나 작은 정수를 전환하는 경우 배열)을 사용하여 값을 응답으로 호출되는 함수에 매핑하는 것입니다. 그렇게하면 많은 불쾌한 공유 스파게티 상태를 제거해야하지만 좋은 일입니다. 큰 스위치 설명은 일반적으로 유지 관리의 악몽입니다. 따라서 … 배열과 사전을 사용하면 조회에 일정한 시간이 걸리고 여분의 메모리 낭비가 거의 없습니다.

나는 여전히 switch 문이 더 낫다는 것을 확신하지 못한다.


답변

동료가 증거를 제공 할 수 없다면이 변경이 전체 응용 프로그램의 규모에 대해 실질적으로 측정 가능한 이점을 제공한다는 점 에서 접근 방식 (예 : 다형성)보다 열등하므로 실제로 이러한 이점을 제공합니다.

병목 현상이 해결 된 후에 만 미세 최적화를 수행해야 합니다. 조기 최적화는 모든 악의 근원입니다 .

속도는 정량화 할 수 있습니다. “접근 A가 접근 B보다 빠릅니다”에는 유용한 정보가 거의 없습니다. 문제는 ” 얼마나 빠릅니까? “입니다.


답변

누가 더 빠른지 누가 신경 쓰나요?

실시간 소프트웨어를 작성하지 않는 한 완전히 미친 방식으로 무언가를 수행함으로써 얻을 수있는 약간의 속도 향상이 고객에게 큰 영향을 줄 것 같지 않습니다. 나는 이것의 속도를 앞당기 지 않을 것입니다.이 녀석은 분명히 주제에 대한 논쟁을 듣지 않을 것입니다.

그러나 유지 관리는 게임의 목표이며, 거대한 스위치 설명은 약간 유지 관리가 불가능합니다. 코드를 통해 새로운 사람들에게 이르는 다른 경로를 어떻게 설명합니까? 문서는 코드 자체만큼 길어야합니다!

또한, 단위 테스트를 효과적으로 수행 할 수 없어 (인터페이스 부족 등은 말할 것도없고 너무 많은 경로) 코드를 유지 관리하기가 훨씬 어려워졌습니다.

[관심있는 측면에서 : JITter는 더 작은 방법에서 더 나은 성능을 발휘하므로 대형 스위치 문 (및 본질적으로 큰 방법)은 대형 조립품 (IIRC)에서 속도를 떨어 뜨립니다.]


답변

switch 문에서 멀어 지십시오 …

이러한 유형의 switch 문은 Open Closed Principle을 위반하기 때문에 전염병처럼 피해야합니다 . 새로운 코드를 추가하는 대신 새로운 기능을 추가해야 할 때 팀이 기존 코드를 변경하도록합니다.


답변

나는 거대한 스위치 문으로 조작되는 거대한 유한 상태 기계로 알려진 악몽에서 살아 남았습니다. 더 나쁜 것은 제 경우에는 FSM이 3 개의 C ++ DLL에 걸렸으며 C에 정통한 누군가가 코드를 작성하는 것이 매우 분명했습니다.

주의해야 할 측정 항목은 다음과 같습니다.

  • 변화 속도
  • 문제가 발생했을 때 발견하는 속도

나는 그 DLL 세트에 새로운 기능을 추가하는 작업을 받았으며, 원숭이 패치에 대한 것처럼 하나의 적절한 객체 지향 DLL만큼 3 개의 DLL을 다시 작성하는 데 시간이 걸릴 것이라고 경영진에게 확신시킬 수있었습니다. 배심원은 이미 존재했던 것에 솔루션을 조작합니다 새로운 기능을 지원했을뿐만 아니라 확장하기가 훨씬 쉬우므로 재 작성은 큰 성공을 거두었습니다. 실제로, 일주일이 걸리면 아무것도 끊지 않도록하는 데 몇 시간이 걸릴 수 있습니다.

실행 시간은 어떻습니까? 속도 증가 또는 감소는 없었습니다. 공정하게 말하면 시스템 드라이버가 성능을 조절했기 때문에 객체 지향 솔루션이 실제로 느린 경우에는 알 수 없습니다.

OO 언어에 대한 대규모 스위치 문에 어떤 문제가 있습니까?

  • 프로그램 제어 흐름은 개체가 속한 개체에서 제거되어 개체 외부에 배치됩니다.
  • 많은 외부 제어 지점을 검토해야하는 많은 장소로 변환
  • 스위치가 루프 내부에있는 경우 상태가 저장된 위치가 확실하지 않습니다.
  • 가장 빠른 비교는 전혀 비교가 필요하지 않습니다 (좋은 객체 지향 디자인으로 많은 비교가 필요하지 않음)
  • 객체를 반복하는 것이 더 효율적이며 항상 객체 유형 또는 유형을 인코딩하는 열거 형을 기반으로 코드를 변경하는 것보다 모든 객체에서 동일한 메소드를 호출하는 것이 좋습니다.

답변

나는 성능 주장을 사지 않는다. 그것은 코드 유지 관리에 관한 것입니다.

그러나 때로는 거대한 기본 스위치의 가상 기능을 재정의하는 여러 개의 작은 클래스보다 거대한 switch 문을 유지 관리하기가 더 쉽습니다 (코드가 적음). 예를 들어, CPU 에뮬레이터를 구현하려는 경우 각 명령의 기능을 별도의 클래스로 구현하지 않을 것입니다. opcode의 거대한 swtich에 넣어서 복잡한 명령을위한 도우미 함수를 호출 할 수 있습니다.

경험적 규칙 : 스위치가 TYPE에서 어떻게 든 수행되는 경우 상속 및 가상 기능을 사용해야합니다. 고정 유형의 VALUE (예 : 위의 명령어 opcode)에서 스위치를 수행하는 경우 그대로 두는 것이 좋습니다.


답변

당신은 저를 설득 할 수 없습니다 :

void action1()
{}

void action2()
{}

void action3()
{}

void action4()
{}

void doAction(int action)
{
    switch(action)
    {
        case 1: action1();break;
        case 2: action2();break;
        case 3: action3();break;
        case 4: action4();break;
    }
}

다음보다 훨씬 빠릅니다.

struct IAction
{
    virtual ~IAction() {}
    virtual void action() = 0;
}

struct Action1: public IAction
{
    virtual void action()    { }
}

struct Action2: public IAction
{
    virtual void action()    { }
}

struct Action3: public IAction
{
    virtual void action()    { }
}

struct Action4: public IAction
{
    virtual void action()    { }
}

void doAction(IAction& actionObject)
{
    actionObject.action();
}

또한 OO 버전은 유지 관리가 더 쉽습니다.