[java] 함수에서 조기 반환의 효율성

이것은 경험이없는 프로그래머로서 자주 접하는 상황이며 특히 최적화하려는 야심 차고 속도 집약적 인 프로젝트에 대해 궁금합니다. C와 유사한 주요 언어 (C, objC, C ++, Java, C # 등) 및 일반적인 컴파일러의 경우이 두 함수가 효율적으로 실행됩니까? 컴파일 된 코드에 차이가 있습니까?

void foo1(bool flag)
{
    if (flag)
    {
        //Do stuff
        return;
    }

    //Do different stuff
}

void foo2(bool flag)
{
    if (flag)
    {
        //Do stuff
    }
    else
    {
        //Do different stuff
    }
}

기본적으로 일찍 break노래하거나 return노래 할 때 직접적인 효율성 보너스 / 페널티가 있습니까? 스택 프레임은 어떻게 관련됩니까? 최적화 된 특수 사례가 있습니까? 여기에 중대한 영향을 미칠 수있는 요소 (예 : 인라인 또는 “Do stuff”의 크기)가 있습니까?

저는 항상 사소한 최적화에 비해 가독성 향상을 옹호하지만 (매개 변수 유효성 검사를 통해 foo1을 많이 봅니다), 이것은 너무 자주 발생하여 모든 걱정을 한 번에 모두 제쳐두고 싶습니다.

그리고 저는 조기 최적화의 함정을 알고 있습니다 … 으, 그것은 고통스러운 기억입니다.

편집 : 나는 대답을 수락했지만 EJP의 대답은 a의 사용 return이 실제로 무시할만한 이유를 매우 간결하게 설명합니다 (어셈블리 return에서 함수 끝까지 ‘분기’를 생성하며 이는 매우 빠릅니다. 분기는 PC 레지스터를 변경하고 모두 있기 때문에 또한 캐시와 파이프 라인, 꽤 소문자입니다.) 특히이 경우에 대한 영향을 미칠 수있는, 말 그대로 차이가 없습니다 if/else와이 return함수의 마지막에 같은 지점을 만들 수 있습니다.



답변

전혀 차이가 없습니다.

=====> cat test_return.cpp
extern void something();
extern void something2();

void test(bool b)
{
    if(b)
    {
        something();
    }
    else
        something2();
}
=====> cat test_return2.cpp
extern void something();
extern void something2();

void test(bool b)
{
    if(b)
    {
        something();
        return;
    }
    something2();
}
=====> rm -f test_return.s test_return2.s
=====> g++ -S test_return.cpp
=====> g++ -S test_return2.cpp
=====> diff test_return.s test_return2.s
=====> rm -f test_return.s test_return2.s
=====> clang++ -S test_return.cpp
=====> clang++ -S test_return2.cpp
=====> diff test_return.s test_return2.s
=====> 

두 개의 컴파일러에서 최적화 없이도 생성 된 코드에 차이가 없음을 의미합니다.


답변

짧은 대답은 차이가 없다는 것입니다. 자신에게 호의를 베풀고 이것에 대해 걱정하지 마십시오. 최적화 컴파일러는 거의 항상 당신보다 똑똑합니다.

가독성과 유지 보수성에 집중하십시오.

어떤 일이 발생하는지 확인하려면 최적화를 사용하여 빌드하고 어셈블러 출력을 살펴보십시오.


답변

흥미로운 답변 : (지금까지) 모두 동의하지만, 지금까지 완전히 무시 된이 질문에 대한 가능한 함축적 의미가 있습니다.

위의 간단한 예를 리소스 할당으로 확장 한 다음 잠재적 인 리소스 해제로 오류 검사를 수행하면 그림이 변경 될 수 있습니다.

초보자가 취할 수 있는 순진한 접근 방식을 고려하십시오 .

int func(..some parameters...) {
  res_a a = allocate_resource_a();
  if (!a) {
    return 1;
  }
  res_b b = allocate_resource_b();
  if (!b) {
    free_resource_a(a);
    return 2;
  }
  res_c c = allocate_resource_c();
  if (!c) {
    free_resource_b(b);
    free_resource_a(a);
    return 3;
  }

  do_work();

  free_resource_c(c);
  free_resource_b(b);
  free_resource_a(a);

  return 0;
}

위의 내용은 조기 복귀 스타일의 극단적 인 버전을 나타냅니다. 코드가 복잡 해짐에 따라 시간이 지남에 따라 코드가 매우 반복적이고 유지 관리가 불가능 해집니다. 요즘 사람들은 예외 처리 를 사용 하여이를 포착 할 수 있습니다 .

int func(..some parameters...) {
  res_a a;
  res_b b;
  res_c c;

  try {
    a = allocate_resource_a(); # throws ExceptionResA
    b = allocate_resource_b(); # throws ExceptionResB
    c = allocate_resource_c(); # throws ExceptionResC
    do_work();
  }
  catch (ExceptionBase e) {
    # Could use type of e here to distinguish and
    # use different catch phrases here
    # class ExceptionBase must be base class of ExceptionResA/B/C
    if (c) free_resource_c(c);
    if (b) free_resource_b(b);
    if (a) free_resource_a(a);
    throw e
  }
  return 0;
}

Philip은 아래의 goto 예제를 살펴본 후 위의 캐치 블록 내부에 끊김없는 스위치 / 케이스 를 사용할 것을 제안했습니다 . 하나는 switch (typeof (e)) 다음 free_resourcex()호출을 통과 할 수 있지만 이것은 사소한 것이 아니며 디자인 고려가 필요합니다 . 그리고 끊김없는 스위치 / 케이스는 아래에 데이지 체인 레이블이있는 goto와 똑같다는 것을 기억하십시오.

Mark B가 지적했듯이 C ++에서는 Resource Aquisition is Initialization 원칙, 간단히 말해서 RAII 를 따르는 것이 좋은 스타일로 간주됩니다 . 개념의 요점은 개체 인스턴스화를 사용하여 리소스를 획득하는 것입니다. 그런 다음 개체가 범위를 벗어나 해당 소멸자가 호출되는 즉시 리소스가 자동으로 해제됩니다. 상호 의존적 인 리소스의 경우 올바른 할당 해제 순서를 보장하고 모든 소멸자에 필요한 데이터를 사용할 수 있도록 개체 유형을 설계하기 위해 특별한주의를 기울여야합니다.

또는 사전 예외 일에 다음을 수행 할 수 있습니다.

int func(..some parameters...) {
  res_a a = allocate_resource_a();
  res_b b = allocate_resource_b();
  res_c c = allocate_resource_c();
  if (a && b && c) {
    do_work();
  }
  if (c) free_resource_c(c);
  if (b) free_resource_b(b);
  if (a) free_resource_a(a);

  return 0;
}

그러나이 지나치게 단순화 된 예제에는 몇 가지 단점이 있습니다. 할당 된 리소스가 서로 의존하지 않는 경우에만 사용할 수 있습니다 (예 : 메모리를 할당 한 다음 파일 핸들을 연 다음 핸들에서 메모리로 데이터를 읽는 데 사용할 수 없음). ), 그리고 반환 값으로 구별 가능한 개별 오류 코드를 제공하지 않습니다.

코드를 빠르게 (!) 유지하기 위해 간결하고 쉽게 읽을 수 있고 확장 할 수있는 Linus Torvalds는 악명 높은 goto 를 절대적으로 합당한 방식으로 사용하더라도 리소스를 다루는 커널 코드에 대해 다른 스타일을 적용했습니다 .

int func(..some parameters...) {
  res_a a;
  res_b b;
  res_c c;

  a = allocate_resource_a() || goto error_a;
  b = allocate_resource_b() || goto error_b;
  c = allocate_resource_c() || goto error_c;

  do_work();

error_c:
  free_resource_c(c);
error_b:
  free_resource_b(b);
error_a:
  free_resource_a(a);

  return 0;
}

커널 메일 링리스트에 대한 논의의 요점은 goto 문보다 “선호되는”대부분의 언어 기능이 거대한, 트리와 같은 if / else, 예외 처리기, 루프 / 중단 / 계속 문 등과 같은 암시 적 gotos라는 것입니다. 그리고 위의 예에서 goto는 작은 거리 만 점프하고 명확한 레이블을 가지고 있으며 오류 조건을 추적하기 위해 다른 코드를 제거하기 때문에 괜찮은 것으로 간주됩니다. 이 질문은 여기서 stackoverflow에서 논의되었습니다 .

그러나 마지막 예제에서 누락 된 것은 오류 코드를 반환하는 좋은 방법입니다. result_code++free_resource_x()호출 후에 를 추가하고 해당 코드를 반환 하려고 생각 했지만 이로 인해 위 코딩 스타일의 속도 향상이 일부 상쇄됩니다. 그리고 성공한 경우 0을 반환하기가 어렵습니다. 어쩌면 나는 상상력이 부족할지도 모릅니다 😉

그래서, 예, 저는 조기 수익을 코딩하는 문제에 큰 차이가 있다고 생각합니다. 그러나 컴파일러를 위해 재구성하고 최적화하는 것이 더 어렵거나 불가능한 더 복잡한 코드에서만 분명하다고 생각합니다. 일반적으로 자원 할당이 시작되면 일반적으로 발생합니다.


답변

이것이 그다지 답은 아니지만 프로덕션 컴파일러는 당신보다 최적화에 훨씬 더 잘할 것입니다. 나는 이러한 종류의 최적화보다 가독성과 유지 보수성을 선호합니다.


답변

이것에 대해 구체적 return으로 말하면는 메소드의 끝 부분으로 컴파일됩니다. 여기에는 RET명령어가 있거나 무엇이든있을 수 있습니다. 생략하면 블록 else의 끝 부분이 브랜치로 컴파일됩니다 else. 따라서이 특정 경우에는 아무런 차이가 없음을 알 수 있습니다.


답변

특정 컴파일러와 시스템에 대해 컴파일 된 코드에 차이가 있는지 정말로 알고 싶다면 어셈블리를 직접 컴파일하고 살펴 봐야합니다.

그러나 큰 계획에서 컴파일러가 미세 조정보다 더 잘 최적화 할 수 있다는 것은 거의 확실하며, 그렇게 할 수 없더라도 실제로 프로그램 성능에 문제가되지는 않습니다.

대신, 사람이 읽고 유지 관리 할 수있는 가장 명확한 방법으로 코드를 작성하고 컴파일러가 최선을 다하도록하십시오. 소스에서 가능한 최상의 어셈블리를 생성하십시오.


답변

귀하의 예에서 수익이 눈에.니다. 반환이 // 다른 일이 발생하는 페이지 위 / 아래 페이지 일 때 디버깅하는 사람은 어떻게됩니까? 더 많은 코드가있을 때 찾기 /보기가 훨씬 더 어렵습니다.

void foo1(bool flag)
{
    if (flag)
    {
        //Do stuff
        return;
    }

    //Do different stuff
}

void foo2(bool flag)
{
    if (flag)
    {
        //Do stuff
    }
    else
    {
        //Do different stuff
    }
}