source

Function pointer를 사용하면 프로그램이 느려집니까?

goodcode 2022. 8. 29. 22:08
반응형

Function pointer를 사용하면 프로그램이 느려집니까?

C의 함수 포인터에 대해 읽었습니다.다들 그렇게 하면 내 프로그램이 느려진다고 했어그게 사실인가요?

확인할 수 있는 프로그램을 만들었습니다.그리고 두 사건 모두 같은 결과를 얻었어요(시간을 측정합니다).

그렇다면, 함수 포인터를 사용하는 것은 나쁜 것일까요?잘 부탁드립니다.

어떤 남자들을 위해서.나는 루프에서 비교한 시간 동안 '느리게 달린다'고 말했다.다음과 같습니다.

int end = 1000;
int i = 0;

while (i < end) {
 fp = func;
 fp ();
}

네가 이걸 실행할 때, 나도 똑같이 할 수 있어.

while (i < end) {
 func ();
}

그래서 저는 함수 포인터는 시간 차이가 없고 많은 사람들이 말하는 것처럼 프로그램이 느리게 돌아가지 않는다고 생각합니다.

사이클에서 함수를 여러 번 호출하는 등 성능 관점에서 실제로 중요한 상황에서는 성능이 전혀 다르지 않을 수 있습니다.

C 코드를 추상적인 C 머신에 의해 실행되는 것으로 생각하는 것에 익숙한 사람들에게는 이상하게 들릴 수 있습니다.그 "기계 언어"는 C 언어 자체와 밀접하게 관련되어 있습니다.이러한 맥락에서 함수에 대한 간접 콜은 콜의 타깃을 결정하기 위해 정식으로 추가 메모리액세스를 수반하기 때문에, 「디폴트」에서는, 실제로 직접 콜보다 느립니다.

그러나 현실에서 코드는 실제 머신에 의해 실행되며, 기본 머신 아키텍처에 대해 상당히 잘 알고 있는 최적화 컴파일러에 의해 컴파일됩니다.이 컴파일러는 특정 머신에 가장 적합한 코드를 생성하는 데 도움이 됩니다.또한 많은 플랫폼에서는 사이클에서 함수 호출을 실행하는 가장 효율적인 방법이 실제로 직접 호출과 간접 호출 모두에서 동일한 코드를 생성하므로 두 플랫폼의 성능이 동일한 것으로 판명될 수 있습니다.

예를 들어 x86 플랫폼을 예로 들 수 있습니다.직간접 통화를 문자 그대로 기계어로 변환하면 다음과 같은 결과가 될 수 있습니다.

// Direct call
do-it-many-times
  call 0x12345678

// Indirect call
do-it-many-times
  call dword ptr [0x67890ABC]

전자는 기계 명령에서 즉시 피연산자를 사용하며, 실제로 후자보다 더 빠릅니다. 후자는 독립적인 메모리 위치에서 데이터를 읽어야 합니다.

이 시점에서 x86 아키텍처에는 오퍼랜드를 제공하는 방법이 하나 더 있습니다.call설명.레지스터에서 타깃 주소를 제공하고 있습니다.그리고 이 포맷에서 매우 중요한 것은 보통 의 두 형식보다 빠르다는 것입니다.이게 우리에게 어떤 의미일까요?이는 최적화 컴파일러가 이 사실을 활용해야 한다는 것을 의미합니다.위의 사이클을 구현하기 위해 컴파일러는 두 경우 모두 레지스터를 통해 콜을 사용하려고 합니다.성공하면 최종 코드는 다음과 같습니다.

// Direct call

mov eax, 0x12345678

do-it-many-times
  call eax

// Indirect call

mov eax, dword ptr [0x67890ABC]

do-it-many-times
  call eax

여기서 중요한 부분(사이클 본체의 실제 콜)은 두 경우 모두 정확하고 정확하게 동일합니다.말할 필요도 없이 퍼포먼스는 거의 동일할 것입니다.

이 플랫폼에서는 다이렉트콜(즉시 오퍼랜드가 있는 콜)이 아무리 이상하게 들릴지 모르지만,call(메모리에 저장되는 것이 아니라) 레지스터에 간접 콜의 오퍼랜드가 지정되어 있는 한 간접 콜보다 느립니다.

물론 모든 것이 일반적인 경우처럼 쉽지는 않다.컴파일러는 제한된 레지스터 가용성, 에일리어스 문제 등에 대처해야 합니다.단, 이 예에서와 같은 단순한 경우(그리고 훨씬 복잡한 경우에서도)는 우수한 컴파일러에 의해 위의 최적화가 실행되어 주기적인 다이렉트 콜과 주기적인 간접 콜의 성능 차이가 완전히 제거됩니다.이 최적화는 가상 함수를 호출할 때 특히 C++에서 잘 작동합니다. 왜냐하면 일반적인 구현에서는 관련된 포인터가 컴파일러에 의해 완전히 제어되어 앨리어싱 이미지 및 기타 관련 정보에 대한 완전한 지식을 제공하기 때문입니다.

물론 컴파일러가 그런 것을 최적화할 수 있을 만큼 똑똑한지에 대해서는 항상 의문이 있습니다.

이 말은 함수 포인터를 사용하면 컴파일러 최적화(인라인)와 프로세서 최적화(브런치 예측)가 방해될 수 있다는 사실을 말하는 것이라고 생각합니다.하지만, 만약 함수 포인터가 당신이 하려고 하는 것을 성취하는 효과적인 방법이라면, 그것을 하는 다른 방법들도 같은 단점을 가지고 있을 것이다.

또, 퍼포먼스가 중요한 애플리케이션이나 매우 느린 임베디드 시스템에서 기능 포인터가 엄격한 루프에 사용되고 있지 않는 한, 어쨌든 그 차이는 무시할 수 있습니다.

다들 그렇게 하면 내 프로그램이 느려진다고 했어그게 사실인가요?

아마도 이 주장은 거짓일 것이다.예를 들어, 함수 포인터를 사용하는 것에 대한 대안이 다음과 같은 경우

if (condition1) {
        func1();
} else if (condition2)
        func2();
} else if (condition3)
        func3();
} else {
        func4();
}

이것은 단일 함수 포인터를 사용하는 것보다 상대적으로 훨씬 느릴 수 있습니다.포인터를 사용한 함수 호출에는 (일반적으로 무시할 수 있는) 오버헤드가 있지만 일반적으로 직접 기능 콜과 스루 포인트콜의 차이는 비교 대상이 아닙니다.

둘째, 측정 없이 성능을 최적화하지 마십시오.병목현상이 어디에 있는지 아는 것은 매우 어렵다(판독이 불가능하다).또한 이것은 매우 직관적이지 않은 경우가 있다(예를 들어 Linux 커널 개발자가 이 문제를 제거하기 시작했다).inline실제로 퍼포먼스를 해쳤기 때문에, 기능으로부터 키워드를 취득할 수 있습니다.

아마도.

정답은 함수 포인터가 무엇에 사용되는지, 따라서 대체 방법이 무엇인지에 따라 달라집니다.함수 포인터 호출과 직접 함수 호출을 비교하는 것은 프로그램 로직의 일부이며 단순히 제거할 수 없는 선택을 구현하기 위해 함수 포인터를 사용하는 경우 오해를 일으킬 수 있습니다.그래도 비교해서 나중에 다시 생각해 보겠습니다.

함수 포인터 호출은 인라인을 금지할 때 직접 함수 호출에 비해 성능이 저하될 가능성이 가장 높습니다.인라이닝은 게이트웨이 최적화이므로 함수 포인터가 동등한 직접 함수 호출보다 임의로 느린 병리학적 경우를 만들 수 있습니다.

void foo(int* x) {
    *x = 0;
}

void (*foo_ptr)(int*) = foo;

int call_foo(int *p, int size) {
    int r = 0;
    for (int i = 0; i != size; ++i)
        r += p[i];
    foo(&r);
    return r;
}

int call_foo_ptr(int *p, int size) {
    int r = 0;
    for (int i = 0; i != size; ++i)
        r += p[i];
    foo_ptr(&r);
    return r;
}

다음에 대해 생성된 코드call_foo():

call_foo(int*, int):
  xor eax, eax
  ret

좋아.foo()인스톨 되어 있을 뿐만 아니라, 인스톨 되어 있기 때문에, 컴파일러는 앞의 루프 전체를 삭제할 수 있게 되었습니다.생성된 코드는 레지스터를 XOR하여 반환 레지스터를 0으로 만든 후 반환됩니다.한편 컴파일러는 루프를 위한 코드를 생성해야 합니다.call_foo_ptr()(gcc 7.3을 사용하는 100개 이상의 회선) 이 코드의 대부분은 효과적으로 아무것도 하지 않습니다(단,foo_ptr여전히 가리키다foo()(보다 일반적인 시나리오에서는 작은 함수를 핫이너 루프에 삽입하면 실행 시간이 최대 약 1배 단축될 수 있습니다).

따라서 최악의 경우 함수 포인터 호출은 직접 함수 호출보다 임의로 느리지만 이는 오해의 소지가 있습니다.알고 보니 만약foo_ptr있었다const,그리고나서call_foo()그리고.call_foo_ptr()같은 코드를 생성했을 겁니다단, 이를 위해서는 다음 사용자가 제공하는 간접적인 기회를 포기해야 합니다.foo_ptr에 대해 '공정'한가?foo_ptr되려고const? 에서 제공하는 간접적인 방법에 관심이 있는 경우foo_ptr, 아니요. 하지만 이 경우 직접 함수 호출도 유효한 옵션이 아닙니다.

함수 포인터가 유용한 간접을 제공하기 위해 사용되는 경우, 우리는 간접을 이리저리 이동하거나 경우에 따라서는 조건이나 매크로를 위한 함수 포인터를 교환할 수 있지만 간단히 제거할 수는 없습니다.함수 포인터가 좋은 접근법이지만 퍼포먼스에 문제가 있다고 판단되면 일반적으로 콜스택의 인다이렉션을 끌어올려 외부 루프의 인다이렉션 비용을 지불하고 싶습니다.예를 들어 함수가 콜백을 받아 루프 내에서 콜하는 일반적인 경우 가장 안쪽의 루프를 콜백으로 이동(및 그에 따라 각 콜백 호출의 책임을 변경)하려고 할 수 있습니다.

많은 사람들이 좋은 답을 내놓았지만, 저는 여전히 놓치고 있는 부분이 있다고 생각합니다.함수 포인터는 추가 참조를 추가하므로 몇 개의 사이클이 느려집니다.그 수는 잘못된 분기 예측에 근거해 증가할 수 있습니다(우연히 함수 포인터 자체와는 거의 관계가 없습니다).포인터를 통해 호출되는 추가 함수는 인라인화할 수 없습니다.그러나 사람들이 놓치고 있는 것은 대부분의 사람들이 최적화로 함수 포인터를 사용한다는 것입니다.

c/c++ API에서 가장 일반적인 함수 포인터는 콜백 함수입니다.많은 API가 이렇게 하는 이유는 이벤트가 발생할 때마다 함수 포인터를 호출하는 시스템을 작성하는 것이 메시지 전달과 같은 다른 방법보다 훨씬 더 효율적이기 때문입니다.개인적으로 저는 좀 더 복잡한 입력 처리 시스템의 일부로 함수 포인터를 사용했는데, 키보드의 각 키에는 점프 테이블을 통해 함수 포인터가 매핑되어 있습니다.이를 통해 입력 시스템에서 분기 또는 논리를 제거하고 입력되는 키 프레스를 처리할 수 있었습니다.

이전 답변에서 많은 좋은 점들이 있었다.

단, Cqsort 비교 함수를 살펴보겠습니다.비교 함수는 인라인 할 수 없고 표준 스택베이스의 호출 규약에 따라야 하기 때문에 정렬의 총 실행 시간은 직접 인라인 가능한 콜을 사용하는 동일한 코드보다 정수 키의 경우 크기(정확히는 3~10배) 느릴 수 있습니다.

일반적인 인라인 비교는 일련의 간단한 CMP 명령과 CMOV/SET 명령일 수 있습니다.함수 호출에서는 CALL의 오버헤드, 스택프레임 설정, 비교, 스택프레임 해체 및 결과 반환도 발생합니다.스택 조작으로 인해 CPU 파이프라인 길이와 가상 레지스터로 인해 파이프라인이 정지될 수 있습니다.예를 들어 마지막으로 수정된 eax의 실행이 완료되기 전에 say eax 값이 필요한 경우(일반적으로 최신 프로세서에서는 12클럭 사이클이 소요됩니다).CPU가 이를 대기하기 위해 다른 명령을 실행할 수 없는 한 파이프라인 스톨이 발생합니다.

함수 포인터를 사용하면 함수가 다른 간접 레이어이기 때문에 호출하는 것보다 속도가 느려집니다.(함수의 메모리주소를 취득하려면 , 포인터를 참조할 필요가 있습니다).다른 모든 작업(파일 읽기, 콘솔 쓰기)에 비해 속도는 느리지만 무시할 수 있습니다.

만약 당신이 함수 포인터를 사용해야 한다면, 그것들을 사용하세요.왜냐하면 그것들을 사용하지 않는 것은 함수 포인터를 사용하는 것보다 더 느리고 유지보수가 어렵기 때문입니다.

함수 포인터를 통한 함수 호출은 정적 함수 호출보다 다소 느립니다.이는 이전 호출에 추가 포인터 참조가 포함되기 때문입니다.그러나 AFAIK는 대부분의 최신 머신에서는 이 차이는 무시할 수 있습니다(리소스가 매우 제한된 일부 특수 플랫폼 제외).

함수 포인터는 프로그램을 훨씬 더 단순하고 깔끔하고 유지보수가 쉽게 할 수 있기 때문에 사용됩니다(물론 적절하게 사용되었을 경우).이것은 가능한 아주 작은 속도 차이를 보완하는 것 이상입니다.

언급URL : https://stackoverflow.com/questions/2438539/does-function-pointer-make-the-program-slow

반응형