programing

함수의 조기 복귀 효율성

copysource 2022. 7. 21. 22:30
반응형

함수의 조기 복귀 효율성

이것은 경험이 부족한 프로그래머로서 자주 접하는 상황이며, 특히 최적화하려고 하는 야심차고 속도 집약적인 프로젝트에 대해 궁금해하고 있습니다.주요 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 또는 ingreturn프레임은 되어 있습니까?스택 프레임은 어떻게 관련되어 있습니까?적화화 특수 ??? ??? ???이에 큰 영향을 미칠 수 있는 요소(인라인 또는 "Do Stuff" 크기 등)가 있습니까?

저는 항상 사소한 최적화(파라미터 검증에서 foo1을 많이 볼 수 있음)보다 읽기 쉽다는 것을 지지합니다만, 이것은 매우 빈번하게 언급되기 때문에, 마지막으로 모든 걱정을 떨쳐버리고 싶습니다.

그리고 조기 최적화의 함정에 대해서도 잘 알고 있습니다.아픈 기억들이네요

편집:만, 에서는, 「EJP」, 「EJP」, 「EJP」, 「EJP」의 .return에서는) 수 있습니다.return.브런치에 의해 PC 레지스터가 변경되어 캐시와 파이프라인에도 영향을 줄 수 있습니다.을 사용법)의이 에는 둘 다 똑같기 에 말 .if/elsereturn함수의 끝에 동일한 분기를 만듭니다.

차이는 전혀 없습니다.

=====> 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
=====> 

2개의 컴파일러로 최적화하지 않아도 생성된 코드에는 전혀 차이가 없습니다.

간단한 대답은, 차이가 없다는 것이다.이 일에 대해 걱정하지 말고 당신 자신을 위해 일하세요.최적화 컴파일러는 거의 항상 당신보다 똑똑합니다.

가독성과 유지보수에 중점을 둡니다.

어떤 일이 일어나는지 알고 싶다면 최적화를 켜고 조립기 출력을 확인하십시오.

흥미로운 답변:저는 그 모든 것에 동의하지만, 이 질문에는 지금까지 완전히 무시되어 온 함축적인 의미가 있을 수 있습니다.

위의 간단한 예를 리소스 할당과 함께 확장한 후 잠재적인 리소스 개방에 대한 오류 검사를 수행하면 상황이 달라질 수 있습니다.

초보자가 취할 수 있는 순진한 접근법에 대해 생각해 봅시다.

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 예를 본 후 의 catch block 안에 브레이크리스 스위치/케이스를 사용할 것을 제안했습니다.스위치(type of(e))를 전환하고 나서, 그 후 를 통과할 수 있습니다.free_resourcex()이것은 단순한 것이 아니기 때문에 설계상의 고려가 필요합니다.그리고 브레이크가 없는 스위치/케이스는 데이지 체인 라벨이 아래에 있는 goto와 똑같다는 것을 기억하십시오.

Mark B가 지적한 바와 같이 C++에서는 Resource Acquisition 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
    }
}

나는 블루시프트에 전적으로 동의한다: 가독성과 유지보수가 우선이다!다만, 정말로 고민하고 있는 경우(또는 컴파일러가 무엇을 하고 있는지 알고 싶은 경우)는, 스스로 생각해 보는 것이 좋습니다.

이는 디컴파일러를 사용하거나 낮은 수준의 컴파일러 출력(어셈블리 언어 등)을 확인하는 것을 의미합니다.C# 또는 any로 지정합니다.인터넷 언어, 여기에 기재되어 있는 로 필요한 것을 얻을 수 있습니다.

그러나 여러분 자신이 관찰한 바와 같이 이는 시기상조일 수 있습니다.

클린 코드: 신속한 변화를 위한 소프트웨어 기술 핸드북

플래그 인수는 추합니다.부울을 함수에 전달하는 것은 정말 끔찍한 작업입니다.이 기능은 여러 가지 기능을 수행한다고 큰 소리로 선언하여 메서드의 시그니처를 즉시 복잡하게 만듭니다.그것은 깃발이 참이면 다른 일을 한다. 깃발이 거짓이면 다른 일을 한다!

foo(true);

in code는 판독기가 함수로 이동하여 foo(플래그)를 읽는 데 시간을 낭비하도록 만들 뿐입니다.

코드 베이스의 구조를 개선하면, 코드를 최적화할 수 있는 기회가 향상됩니다.

코드 읽기 및 디버깅을 용이하게 하기 위해서는 구조적인 관점에서 모든 함수가 하나의 반환점만을 가져야 한다는 사고방식(현재는 그것을 제안한 에그헤드를 기억하지 못한다)이 있습니다.그것은 종교 토론을 프로그램하기 위한 것이라고 생각한다.

이 규칙을 위반하는 함수의 종료 시기와 방법을 제어해야 하는 기술적 이유 중 하나는 실시간어플리케이션을 코딩할 때 함수를 통과하는 모든 제어 패스가 클럭사이클의 같은 수를 사용하여 완료되도록 하는 것입니다.

네가 이 문제를 꺼내서 기뻐.조기 반납 시에는 항상 지점을 이용해야 합니다.왜 저기 멈추지?가능한 한 모든 기능을 하나로 통합합니다.이것은 재귀가 없는 경우에 실행할 수 있습니다.결국, 하나의 거대한 주요 기능을 갖게 될 것입니다만, 이러한 기능을 필요로 하거나 필요로 하는 것은 바로 그것입니다.그 후, 가능한 한 짧게 식별자의 이름을 변경합니다.이렇게 하면 코드가 실행될 때 이름을 읽는 시간이 줄어듭니다.다음 작업...

언급URL : https://stackoverflow.com/questions/7884705/efficiency-of-premature-return-in-a-function

반응형