developer tip

C ++에서 함수를 호출 할 때 얼마나 많은 오버 헤드가 있습니까?

copycodes 2020. 12. 12. 11:27
반응형

C ++에서 함수를 호출 할 때 얼마나 많은 오버 헤드가 있습니까?


많은 문헌에서 "함수 호출의 오버 헤드를 방지"하기 위해 인라인 함수를 사용하는 것에 대해 이야기합니다. 그러나 나는 정량화 가능한 데이터를 보지 못했습니다. 함수 호출의 실제 오버 헤드는 무엇입니까? 즉 함수를 인라인하여 어떤 종류의 성능 향상을 달성합니까?


대부분의 아키텍처에서 비용은 레지스터 전체 (또는 일부 또는 아예 없음)를 스택에 저장하고, 함수 인수를 스택에 푸시 (또는 레지스터에 넣음)하고, 스택 포인터를 증가시키고 시작 부분으로 점프하는 것으로 구성됩니다. 새로운 코드. 그런 다음 기능이 완료되면 스택에서 레지스터를 복원해야합니다. 이 웹 페이지 에는 다양한 호출 규칙에 관련된 설명이 있습니다.

대부분의 C ++ 컴파일러는 이제 함수를 인라인 할 수있을만큼 똑똑합니다. 인라인 키워드는 컴파일러에 대한 힌트 일뿐입니다. 일부는 번역 단위에서 도움이된다고 판단하는 경우 인라인 작업을 수행하기도합니다.


기술적이고 실용적인 답이 있습니다. 실질적인 대답은 결코 중요하지 않으며 매우 드문 경우지만 실제 프로파일 링 된 테스트를 통해서만 알 수 있습니다.

귀하의 문헌에서 언급하는 기술적 답변은 일반적으로 컴파일러 최적화로 인해 관련이 없습니다. 그러나 여전히 관심이 있다면 Josh 가 잘 설명합니다 .

"백분율"에 관해서는 함수 자체가 얼마나 비쌌는지 알아야합니다. 호출 된 함수의 비용 외에는 비용이 0 인 작업과 비교하기 때문에 백분율이 없습니다. 인라인 코드의 경우 비용이 들지 않으며 프로세서는 다음 명령어로 이동합니다. 인링의 단점은 스택 구성 / 해체 비용과 다른 방식으로 비용을 나타내는 더 큰 코드 크기입니다.


간단한 증분 함수에 대한 간단한 벤치 마크를 만들었습니다.

inc.c :

typedef unsigned long ulong;
ulong inc(ulong x){
    return x+1;
}

main.c

#include <stdio.h>
#include <stdlib.h>

typedef unsigned long ulong;

#ifdef EXTERN 
ulong inc(ulong);
#else
static inline ulong inc(ulong x){
    return x+1;
}
#endif

int main(int argc, char** argv){
    if (argc < 1+1)
        return 1;
    ulong i, sum = 0, cnt;
    cnt = atoi(argv[1]);
    for(i=0;i<cnt;i++){
        sum+=inc(i);
    }
    printf("%lu\n", sum);
    return 0;
}

Intel (R) Core (TM) i5 CPU M 430 @ 2.27GHz 에서 10 억 번 반복 실행하면 다음과 같은 이점 이 있습니다.

  • 인라이닝 버전의 경우 1.4 초
  • 정기적으로 링크 된 버전의 경우 4.4 초

(최대 0.2까지 변동하는 것처럼 보이지만 적절한 표준 편차를 계산하기에는 너무 게 으르거나 신경 쓰지 않습니다)

이것은이 컴퓨터에서 함수 호출의 오버 헤드가 약 3 나노 초임을 의미합니다.

내가 측정 한 가장 빠른 것은 약 0.3ns 였기 때문에 함수 호출 비용은 약 9 개의 원시 연산 비용이 듭니다 .

이 오버 헤드 는 PLT (공유 라이브러리의 함수)를 통해 호출 된 함수 에 대해 호출 당 2ns (총 시간 호출 시간 약 6ns ) 만큼 증가합니다 .


오버 헤드의 양은 컴파일러, CPU 등에 따라 달라집니다. 오버 헤드 비율은 인라인하는 코드에 따라 다릅니다. 알 수있는 유일한 방법은 코드 를 가져와 두 가지 방식으로 프로파일 링하는 것입니다. 그렇기 때문에 확실한 답이 없습니다.


귀하의 질문은 "절대 진실"이라고 부를 수있는 답이없는 질문 중 하나입니다. 일반 함수 호출의 오버 헤드는 다음 세 가지 요소에 따라 달라집니다.

  1. CPU. x86, PPC 및 ARM CPU의 오버 헤드는 매우 다양하며 하나의 아키텍처 만 사용하더라도 Intel Pentium 4, Intel Core 2 Duo 및 Intel Core i7 간에도 오버 헤드가 상당히 다릅니다. 캐시 크기, 캐싱 알고리즘, 메모리 액세스 패턴 및 호출 opcode 자체의 실제 하드웨어 구현과 같은 요소가 큰 영향을 미칠 수 있기 때문에 두 CPU가 동일한 클럭 속도로 실행 되더라도 오버 헤드는 Intel과 AMD CPU간에 눈에 띄게 다를 수 있습니다. 오버 헤드에 미치는 영향.

  2. The ABI (Application Binary Interface). Even with the same CPU, there often exist different ABIs that specify how function calls pass parameters (via registers, via stack, or via a combination of both) and where and how stack frame initialization and clean-up takes place. All this has an influence on the overhead. Different operating systems may use different ABIs for the same CPU; e.g. Linux, Windows and Solaris may all three use a different ABI for the same CPU.

  3. The Compiler. Strictly following the ABI is only important if functions are called between independent code units, e.g. if an application calls a function of a system library or a user library calls a function of another user library. As long as functions are "private", not visible outside a certain library or binary, the compiler may "cheat". It may not strictly follow the ABI but instead use shortcuts that lead to faster function calls. E.g. it may pass parameters in register instead of using the stack or it may skip stack frame setup and clean-up completely if not really necessary.

예를 들어 GCC를 사용하는 Linux의 Intel Core i5와 같이 위의 세 가지 요소의 특정 조합에 대한 오버 헤드를 알고 싶다면이 정보를 얻는 유일한 방법은 두 구현 간의 차이를 벤치마킹하는 것입니다. 코드를 호출자에게 직접 복사하십시오. 이렇게하면 인라인 문이 힌트 일 뿐이며 항상 인라인으로 이어지지는 않기 때문에 인라인을 확실히 강제 할 수 있습니다.

However, the real question here is: Does the exact overhead really matter? One thing is for sure: A function call always has an overhead. It may be small, it may be big, but it is for sure existent. And no matter how small it is if a function is called often enough in a performance critical section, the overhead will matter to some degree. Inlining rarely makes your code slower, unless you terribly overdo it; it will make the code bigger though. Today's compilers are pretty good at deciding themselves when to inline and when not, so you hardly ever have to rack your brain about it.

개인적으로 저는 개발 중에 인라인을 완전히 무시합니다. 프로파일 링 할 수있는 어느 정도 사용 가능한 제품이 있고 프로파일 링에서 특정 기능이 실제로 자주 호출되고 응용 프로그램의 성능이 중요한 섹션 내에서 호출된다는 것을 알 수있을 때까지만 이 함수의 "강제 인라인"을 고려하십시오.

지금까지 내 대답은 매우 일반적이며 C ++ 및 Objective-C에 적용되는만큼 C에도 적용됩니다. 마지막으로 C ++에 대해 특별히 말씀 드리겠습니다. 가상 메서드는 이중 간접 함수 호출입니다. 즉, 일반 함수 호출보다 함수 호출 오버 헤드가 높고 인라인 될 수 없습니다. 비가 상 메서드는 컴파일러에 의해 인라인 될 수도 있고 그렇지 않을 수도 있지만 인라인되지 않더라도 여전히 가상 메서드보다 훨씬 빠르기 때문에 실제로 메서드를 재정의하거나 재정의하지 않는 한 가상 메서드를 만들지 않아야합니다.


함수 호출의 (작은) 비용이 함수 본문의 (매우 작은) 비용에 비해 중요하기 때문에 매우 작은 함수의 경우 인라이닝이 의미가 있습니다. 몇 줄에 걸친 대부분의 기능에 대해 큰 승리는 아닙니다.


인라인 함수는 호출 함수의 크기를 증가시키고 함수의 크기를 증가시키는 것은 캐싱에 부정적인 영향을 미칠 수 있다는 점을 지적 할 가치가 있습니다. 경계에 맞다면 인라인 코드의 "단지 하나 이상의 얇은 박하"가 성능에 극적으로 부정적인 영향을 미칠 수 있습니다.


"함수 호출 비용"에 대해 경고하는 문헌을 읽고 있다면 최신 프로세서를 반영하지 않는 오래된 자료 일 수 있습니다. 임베디드 세계에 있지 않는 한 C가 "이동 가능한 어셈블리 언어"인 시대는 본질적으로 지나갔습니다. 지난 10 년 동안 칩 설계자들의 많은 독창성은 "예전의"일이 작동하는 방식과 근본적으로 다를 수있는 모든 종류의 낮은 수준의 복잡성에 들어갔다.


스택 (memory) 대신 레지스터 (CPU에서)를 통해 (최대 6?) 값을 전달할 수있는 '레지스터 섀도 잉'이라는 훌륭한 개념이 있습니다. 또한 내부에서 사용되는 함수와 변수에 따라 컴파일러는 프레임 관리 코드가 필요하지 않다고 결정할 수 있습니다 !!

또한 C ++ 컴파일러도 '꼬리 재귀 최적화'를 수행 할 수 있습니다. 즉, A ()가 B ()를 호출하면 B ()를 호출 한 후 A가 반환되면 컴파일러는 스택 프레임을 재사용합니다 !!

물론, 프로그램이 표준의 의미를 고수하는 경우에만이 모든 작업을 수행 할 수 있습니다 (포인터 앨리어싱 참조 및 최적화에 미치는 영향 참조).


최신 CPU는 매우 빠릅니다 (분명히!). 호출 및 인수 전달과 관련된 거의 모든 작업은 최대 속도 명령입니다 (간접 호출은 대부분 루프를 통해 처음으로 발생하는 경우 약간 더 비쌀 수 있음).

함수 호출 오버 헤드는 너무 작아서 함수를 호출하는 루프 만 호출 오버 헤드를 관련시킬 수 있습니다.

따라서 오늘 함수 호출 오버 헤드에 대해 이야기 (및 측정) 할 때 일반적으로 루프에서 공통 하위 표현식을 끌어 올릴 수없는 오버 헤드에 대해 이야기하고 있습니다. 함수가 호출 될 때마다 많은 (동일한) 작업을 수행해야하는 경우 컴파일러는 루프 밖으로 "이동"하고 인라인 된 경우 한 번 수행 할 수 있습니다. 인라인되지 않은 경우 코드는 아마도 계속해서 작업을 반복 할 것입니다.

인라인 함수는 호출 및 인수 오버 헤드 때문이 아니라 함수에서 끌어 올릴 수있는 공통 하위 표현식으로 인해 엄청나게 빨라 보입니다.

예:

Foo::result_type MakeMeFaster()
{
  Foo t = 0;
  for (auto i = 0; i < 1000; ++i)
    t += CheckOverhead(SomethingUnpredictible());
  return t.result();
}

Foo CheckOverhead(int i)
{
  auto n = CalculatePi_1000_digits();
  return i * n;
}

옵티마이 저는이 어리 석음을 통해 볼 수 있으며 다음을 수행 할 수 있습니다.

Foo::result_type MakeMeFaster()
{
  Foo t;
  auto _hidden_optimizer_tmp = CalculatePi_1000_digits();
  for (auto i = 0; i < 1000; ++i)
    t += SomethingUnpredictible() * _hidden_optimizer_tmp;
  return t.result();
}

실제로 루프에서 함수의 큰 덩어리를 끌어 올렸기 때문에 호출 오버 헤드가 엄청나게 줄어든 것처럼 보입니다 (CalculatePi_1000_digits 호출). 컴파일러는 CalculatePi_1000_digits가 항상 동일한 결과를 반환한다는 것을 증명할 수 있어야하지만 좋은 최적화 프로그램은 그렇게 할 수 있습니다.


여기에 몇 가지 문제가 있습니다.

  • 똑똑한 컴파일러가 있다면 인라인을 지정하지 않아도 자동 인라인을 수행합니다. 반면에 인라인 할 수없는 것들이 많이 있습니다.

  • 함수가 가상 인 경우 대상이 런타임에 결정되기 때문에 인라인 될 수없는 가격을 지불해야합니다. 반대로 Java에서는 메소드가 최종임을 표시하지 않는 한이 가격을 지불 할 수 있습니다.

  • 코드가 메모리에서 구성되는 방식에 따라 코드가 다른 곳에 위치하므로 캐시 미스 및 페이지 미스에 대한 비용을 지불 할 수 있습니다. 이는 일부 애플리케이션에 큰 영향을 미칠 수 있습니다.


특히 작은 (인라인 가능) 함수 또는 클래스의 경우에는 오버 헤드가 전혀 없습니다.

다음 예에는 각각 여러 번, 여러 번 실행되는 세 가지 테스트가 있습니다. 결과는 항상 시간 단위의 1000 분의 2 정도입니다.

#include <boost/timer/timer.hpp>
#include <iostream>
#include <cmath>

double sum;
double a = 42, b = 53;

//#define ITERATIONS 1000000 // 1 million - for testing
//#define ITERATIONS 10000000000 // 10 billion ~ 10s per run
//#define WORK_UNIT sum += a + b
/* output
8.609619s wall, 8.611255s user + 0.000000s system = 8.611255s CPU(100.0%)
8.604478s wall, 8.611255s user + 0.000000s system = 8.611255s CPU(100.1%)
8.610679s wall, 8.595655s user + 0.000000s system = 8.595655s CPU(99.8%)
9.5e+011 9.5e+011 9.5e+011
*/

#define ITERATIONS 100000000 // 100 million ~ 10s per run
#define WORK_UNIT sum += std::sqrt(a*a + b*b + sum) + std::sin(sum) + std::cos(sum)
/* output
8.485689s wall, 8.486454s user + 0.000000s system = 8.486454s CPU (100.0%)
8.494153s wall, 8.486454s user + 0.000000s system = 8.486454s CPU (99.9%)
8.467291s wall, 8.470854s user + 0.000000s system = 8.470854s CPU (100.0%)
2.50001e+015 2.50001e+015 2.50001e+015
*/


// ------------------------------
double simple()
{
   sum = 0;
   boost::timer::auto_cpu_timer t;
   for (unsigned long long i = 0; i < ITERATIONS; i++)
   {
      WORK_UNIT;
   }
   return sum;
}

// ------------------------------
void call6()
{
   WORK_UNIT;
}
void call5(){ call6(); }
void call4(){ call5(); }
void call3(){ call4(); }
void call2(){ call3(); }
void call1(){ call2(); }

double calls()
{
   sum = 0;
   boost::timer::auto_cpu_timer t;

   for (unsigned long long i = 0; i < ITERATIONS; i++)
   {
      call1();
   }
   return sum;
}

// ------------------------------
class Obj3{
public:
   void runIt(){
      WORK_UNIT;
   }
};

class Obj2{
public:
   Obj2(){it = new Obj3();}
   ~Obj2(){delete it;}
   void runIt(){it->runIt();}
   Obj3* it;
};

class Obj1{
public:
   void runIt(){it.runIt();}
   Obj2 it;
};

double objects()
{
   sum = 0;
   Obj1 obj;

   boost::timer::auto_cpu_timer t;
   for (unsigned long long i = 0; i < ITERATIONS; i++)
   {
      obj.runIt();
   }
   return sum;
}
// ------------------------------


int main(int argc, char** argv)
{
   double ssum = 0;
   double csum = 0;
   double osum = 0;

   ssum = simple();
   csum = calls();
   osum = objects();

   std::cout << ssum << " " << csum << " " << osum << std::endl;
}

10,000,000 회 반복 실행 (각 유형 : 단순, 6 개의 함수 호출, 3 개의 객체 호출)에 대한 출력은 다음과 같은 반 복잡한 작업 페이로드를 사용했습니다.

sum += std::sqrt(a*a + b*b + sum) + std::sin(sum) + std::cos(sum)

다음과 같이 :

8.485689s wall, 8.486454s user + 0.000000s system = 8.486454s CPU (100.0%)
8.494153s wall, 8.486454s user + 0.000000s system = 8.486454s CPU (99.9%)
8.467291s wall, 8.470854s user + 0.000000s system = 8.470854s CPU (100.0%)
2.50001e+015 2.50001e+015 2.50001e+015

간단한 작업 페이로드 사용

sum += a + b

각 경우에 대해 몇 배 더 빠른 것을 제외하고 동일한 결과를 제공합니다.


Each new function requires a new local stack to be created. But the overhead of this would only be noticeable if you are calling a function on every iteration of a loop over a very large number of iterations.


For most functions, their is no additional overhead for calling them in C++ vs C (unless you count that the "this" pointer as an unnecessary argument to every function.. You have to pass state to a function somehow tho)...

For virtual functions, their is an additional level of indirection (equivalent to a calling a function through a pointer in C)... But really, on today's hardware this is trivial.


I don't have any numbers, either, but I'm glad you're asking. Too often I see people try to optimize their code starting with vague ideas of overhead, but not really knowing.


Depending on how you structure your code, division into units such as modules and libraries it might matter in some cases profoundly.

  1. Using dynamic library function with external linkage will most of the time impose full stack frame processing.
    That is why using qsort from stdc library is one order of magnitude (10 times) slower than using stl code when comparison operation is as simple as integer comparison.
  2. Passing function pointers between modules will also be affected.
  3. The same penalty will most likely affect usage of C++'s virtual functions as well as other functions, whose code is defined in separate modules.

  4. Good news is that whole program optimization might resolve the issue for dependencies between static libraries and modules.


As others have said, you really don't have to worry too much about overhead, unless you're going for ultimate performance or something akin. When you make a function the compiler has to write code to:

  • Save function parameters to the stack
  • Save the return address to the stack
  • Jump to the starting address of the function
  • Allocate space for the function's local variables (stack)
  • Run the body of the function
  • Save the return value (stack)
  • Free space for the local variables aka garbage collection
  • Jump back to the saved return address
  • Free up save for the parameters etc...

However, you have to account for lowering the readability of your code, as well as how it will impact your testing strategies, maintenance plans, and overall size impact of your src file.

참고URL : https://stackoverflow.com/questions/144993/how-much-overhead-is-there-in-calling-a-function-in-c

반응형