Programing

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

crosscheck 2020. 8. 25. 07:32
반응형

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


이것은 경험이없는 프로그래머로서 자주 접하는 상황이며 특히 최적화하려는 야심 차고 속도 집약적 인 프로젝트에 대해 궁금합니다. 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;
}

The above would represent an extreme version of the style of returning prematurely. Notice how the code becomes very repetitive and non-maintainable over time when its complexity grows. Nowadays people might use exception handling to catch these.

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 suggested, after looking at the goto example below, to use a break-less switch/case inside the catch block above. One could switch(typeof(e)) and then fall through the free_resourcex() calls but this is not trivial and needs design consideration. And remember that a switch/case without breaks is exactly like the goto with daisy-chained labels below...

As Mark B pointed out, in C++ it is considered good style to follow the Resource Aquisition is Initialization principle, RAII in short. The gist of the concept is to use object instantiation to aquire resources. The resources are then automatically freed as soon as the objects go out of scope and their destructors are called. For interdepending resources special care has to be taken to ensure the correct order of deallocation and to design the types of objects such that required data is available for all destructors.

Or in pre-exception days might do:

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;
}

But this over-simplified example has several drawbacks: It can be used only if the allocated resources do not depend on each other (e.g. it could not be used for allocating memory, then opening a filehandle, then reading data from the handle into the memory), and it does not provide individial, distinguishable error codes as return values.

To keep code fast(!), compact, and easily readable and extensible Linus Torvalds enforced a different style for kernel code that deals with resources, even using the infamous goto in a way that makes absolutely sense:

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;
}

The gist of the discussion on the kernel mailing lists is that most language features that are "preferred" over the goto statement are implicit gotos, such as huge, tree-like if/else, exception handlers, loop/break/continue statements, etc. And goto's in the above example are considered ok, since they are jumping only a small distance, have clear labels, and free the code of other clutter for keeping track of the error conditions. This question has also been discussed here on stackoverflow.

However what's missing in the last example is a nice way to return an error code. I was thinking of adding a result_code++ after each free_resource_x() call, and returning that code, but this offsets some of the speed gains of the above coding style. And it's hard to return 0 in case of success. Maybe I'm just unimaginative ;-)

So, yes, I do think there is a big difference in the question of coding premature returns or not. But I also think it is apparent only in more complicated code that is harder or impossible to restructure and optimize for the compiler. Which is usually the case once resource allocation comes into play.


Even though this isn't much an answer, a production compiler is going to be much better at optimizing than you are. I would favor readability and maintainability over these kinds of optimizations.


To be specific about this, the return will be compiled into a branch to the end of the method, where there will be a RET instruction or whatever it may be. If you leave it out, the end of the block before the else will be compiled into a branch to the end of the else block. So you can see in this specific case it makes no difference whatsoever.


If you really want to know if there's a difference in compiled code for your particular compiler and system, you'll have to compile and look at the assembly yourself.

However in the big scheme of things it's almost certain that the compiler can optimize better than your fine tuning, and even if it can't it's very unlikely to actually matter for your program's performance.

Instead, write the code in the clearest way for humans to read and maintain, and let the compiler do what it does best: Generate the best assembly it can from your source.


In your example, the return is noticeable. What happens to the person debugging when the return is a page or two above/below where //do different stuff occurs? Much harder to find/see when there is more code.

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

    //Do different stuff
}

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

I agree strongly with blueshift: readability and maintainability first!. But if you're really worried (or just want to learn what your compiler is doing, which definitely a good idea in the long run), you should look for yourself.

This will mean using a decompiler or looking at low level compiler output (e.g. assembly lanuage). In C#, or any .Net language, the tools documented here will give you what you need.

But as you yourself have observed, this is probably premature optimization.


From Clean Code: A Handbook of Agile Software Craftsmanship

Flag arguments are ugly. Passing a boolean into a function is a truly terrible practice. It immediately complicates the signature of the method, loudly proclaiming that this function does more than one thing. It does one thing if the flag is true and another if the flag is false!

foo(true);

in code will just make the reader to navigate to the function and waste time reading foo(boolean flag)

Better structured code base will give you better opportunity to optimize code.


One school of thought (can't remember the egghead who proposed it at the moment) is that all function should only have one return point from a structural point of view to make the code easier to read and debug. That, I suppose, is more for programming religious debate.

One technical reason you may want to control when and how a function exits that breaks this rule is when you are coding real-time applications and you want to make sure that all control paths through the function take the same number of clock cycles to complete.


I'm glad you brought this question up. You should always use the branches over an early return. Why stop there? Merge all your functions into one if you can (at least as much as you can). This is doable if there is no recursion. In the end, you will have one massive main function, but that is what you need/want for this sort of thing. Afterward, rename your identifiers to be as short as possible. That way when your code is executed, less time is spent reading names. Next do ...

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

반응형