최신 컴파일러에서 함수 호출 비용은 여전히 ​​중요합니까?


95

나는 종교적인 사람이며 죄를 짓지 않기 위해 노력합니다. 그렇기 때문에 Clean Code 성경이 지시 한 몇 가지 계명을 준수하기 위해 작게 ( 로버트 C. 마틴을 말하면) 작게 작성하는 경향이 있습니다. 그러나 몇 가지 사항을 확인 하면서이 게시물에 착륙했으며 아래 에서이 의견을 읽었습니다.

언어에 따라 메서드 호출 비용이 상당 할 수 있습니다. 읽을 수있는 코드를 작성하는 것과 수행 코드를 작성하는 것은 거의 항상 상충 관계입니다.

오늘날 인용 된 풍부한 성능의 현대 컴파일러를 고려할 때이 인용문은 현재 어떤 조건에서 유효합니까?

그게 내 유일한 질문입니다. 그리고 길거나 작은 함수를 작성해야하는지에 관한 것이 아닙니다. 나는 단지 당신의 의견이 내 태도를 바꾸는 데 기여하지 않을 수도 있고, 신성 모 독자 들의 유혹에 저항 할 수 없다는 점을 강조합니다 .


11
읽기 쉽고 유지 관리 가능한 코드를 작성하십시오. 스택 오버플로 문제에 직면했을 때만 스프로 치를 다시 생각할 수 있습니다
Fabio

33
여기에 일반적인 대답은 불가능합니다. 너무 많은 다른 컴파일러가 있으며 너무 많은 다른 언어 사양을 구현합니다. 그리고 JIT 컴파일 언어, 동적으로 해석되는 언어 등이 있습니다. 그러나 최신 컴파일러로 네이티브 C 또는 C ++ 코드를 컴파일하는 경우 함수 호출 비용에 대해 걱정할 필요가 없습니다. 최적화 프로그램은 필요할 때마다이를 인라인합니다. 마이크로 최적화 애호가로서, 나는 컴파일러가 나와 내 벤치 마크가 동의하지 않는 결정을 내리는 것을 거의 볼 수 없습니다 .
코디 그레이

6
개인적인 경험으로 말하면, 나는 기능면에서 상당히 현대적인 독점 언어로 코드를 작성하지만 함수 호출은 일반적으로 for 루프조차도 속도 for(Integer index = 0, size = someList.size(); index < size; index++)대신 최적화 해야하는 시점까지 간단 for(Integer index = 0; index < someList.size(); index++)합니다. 지난 몇 년 동안 컴파일러가 만들어 졌다고해서 반드시 프로파일 링을 포기할 수있는 것은 아닙니다.
phyrfox

5
루프를 통해 매번 호출하는 대신 루프 외부의 someList.size () 값을 가져 오는 @phyrfox 독자와 작성자가 반복 중에 충돌을 시도 할 수있는 동기화 문제가 발생할 가능성이있는 경우 특히 그렇습니다.이 경우 반복 중에 변경 사항으로부터 목록을 보호하려고합니다.
Craig

8
작은 기능을 너무 많이 사용하면 모 놀리 식 메가 기능만큼 효율적으로 코드가 난독 화 될 수 있습니다. 당신이 나를 믿지 않는다면, ioccc.org 수상자 중 일부를 확인하십시오 main(). 요령은 항상 그렇듯이 좋은 균형을 유지 하는 것 입니다.
cmaster

답변:


148

도메인에 따라 다릅니다.

저전력 마이크로 컨트롤러 용 코드를 작성하는 경우 메소드 호출 비용이 상당 할 수 있습니다. 그러나 일반적인 웹 사이트 또는 응용 프로그램을 만드는 경우 나머지 코드와 비교하여 메서드 호출 비용을 무시할 수 있습니다. 이 경우 항상 메소드 호출과 같은 미세 최적화 대신 올바른 알고리즘과 데이터 구조에 중점을 둘 가치가 있습니다.

그리고 컴파일러가 메소드를 인라인하는 문제도 있습니다. 대부분의 컴파일러는 가능한 경우 함수를 인라인 할 수있을 정도로 지능적입니다.

그리고 마지막으로, 성능의 황금률이 ​​있습니다 : 항상 프로파일 우선. 가정에 따라 "최적화 된"코드를 작성하지 마십시오. 당신이 불분명 한 경우, 두 경우를 작성하고 어느 것이 더 나은지보십시오.


13
그리고 핫스팟 컴파일러는 performes 예 투기 인라인 이 경우에도 어떤 의미 인라인에, 하지 수 있습니다.
Jörg W Mittag

49
실제로, 웹 애플리케이션에서 전체 코드는 DB 액세스 및 네트워크 트래픽과 관련하여 중요하지
않을 수

72
나는 실제로 최적화의 의미를 거의 알지 못하는 매우 오래된 컴파일러를 사용하여 임베디드 및 초 저전력에 빠져 있으며 함수 호출이 중요하더라도 최적화를 찾는 것은 결코 처음이 아닙니다. 이 틈새 영역에서도이 경우 코드 품질이 가장 우선합니다.
Tim

2
@Mehrdad이 경우에도 코드를 최적화하는 데 더 관련이없는 것이 놀랍습니다. 코드를 프로파일 링 할 때 함수 호출보다 훨씬 무거운 것을 보았습니다. 이곳에서 최적화를 찾는 것이 중요합니다. 일부 개발자는 최적화되지 않은 하나 또는 두 개의 LOC에 열광하지만 SW를 프로파일 링하면 최소한 코드의 가장 큰 부분에 대해 디자인이 이것보다 중요하다는 것을 알 수 있습니다. 병목 현상을 발견하면이를 최적화하려고 시도 할 수 있으며 호출 오버 헤드를 피하기 위해 큰 함수를 작성하는 것과 같은 저수준 임의 최적화보다 훨씬 더 많은 영향을 미칩니다.
Tim

8
좋은 대답입니다! 마지막 요점은 다음과 같습니다 . 최적화 할 위치를 결정하기 전에 항상 프로파일하십시오 .
CJ 데니스

56

함수 호출 오버 헤드는 전적으로 언어와 최적화 수준에 따라 다릅니다.

매우 낮은 수준에서 함수 호출 및 그 이상으로 인해 가상 메소드 호출로 인해 분기 오판 또는 CPU 캐시 누락이 발생할 경우 비용이 많이들 수 있습니다. 어셈블러를 작성했다면 호출 주위에 레지스터를 저장하고 복원하기위한 몇 가지 추가 지침이 필요하다는 것도 알게 될 것입니다. 컴파일러가 언어의 의미론 (특히 인터페이스 메소드 디스패치 또는 동적으로로드 된 라이브러리와 같은 기능)에 의해 제한되기 때문에이 오버 헤드를 피하기 위해 "충분히 똑똑한"컴파일러가 올바른 함수를 인라인 할 수있는 것은 사실이 아닙니다.

높은 수준에서 Perl, Python, Ruby와 같은 언어는 함수 호출마다 많은 부기를 유지하므로 비용이 많이 듭니다. 이것은 메타 프로그래밍에 의해 악화됩니다. 한때 매우 뜨거운 루프에서 함수 호출을 올리는 것만으로 Python 소프트웨어 3x를 가속화했습니다. 성능이 중요한 코드에서 인라인 헬퍼 함수는 눈에 띄는 영향을 줄 수 있습니다.

그러나 대다수의 소프트웨어는 성능이 매우 중요하지 않으므로 함수 호출 오버 헤드를 알 수 있습니다. 어쨌든 깨끗하고 간단한 코드를 작성하면 다음과 같은 이점이 있습니다.

  • 코드의 성능이 중요하지 않은 경우 유지 관리가 쉬워집니다. 성능이 중요한 소프트웨어라도 대부분의 코드는 "핫스팟"이 아닙니다.

  • 코드가 성능에 중요한 경우 간단한 코드를 사용하면 코드를 쉽게 이해하고 최적화 기회를 찾을 수 있습니다. 가장 큰 승리는 일반적으로 함수 인라이닝과 같은 미세 최적화가 아니라 알고리즘 개선에서 비롯됩니다. 또는 다르게 표현하면 같은 일을 더 빨리하지 마십시오. 덜 할 수있는 방법을 찾으십시오.

"간단한 코드"가 "천 개의 작은 기능으로 인수 화됨"을 의미하지는 않습니다. 또한 모든 함수에는 약간의인지 오버 헤드가 발생 합니다.보다 추상적 인 코드에 대해서는 추론 하기가 어렵습니다 . 어떤 시점에서,이 작은 함수들은 그렇게하지 않으면 코드를 단순화 할 수 있습니다.


16
정말 똑똑한 DBA는 "내가 아플 때까지 정규화하고 그렇지 않을 때까지 비정규 화"라고 나에게 말했다. "아플 때까지 방법을 추출한 다음, 그렇지 않을 때까지 인라인"으로 표현할 수 있습니다.
RubberDuck

1
인지 오버 헤드 외에도 디버거 정보에는 기호 오버 헤드가 있으며 일반적으로 최종 바이너리의 오버 헤드는 피할 수 없습니다.
Frank Hileman

스마트 컴파일러와 관련하여 항상 그렇게 할 수는 없습니다. 예를 들어 jvm은 특정 메소드 / 인터페이스의 구현이 하나 뿐인 드문 경로 또는 인라인 다형성 함수에 대해 매우 저렴하고 자유로운 트랩으로 런타임 프로파일을 기반으로 사물을 인라인 한 다음 새 서브 클래스가 동적으로로드 될 때 적절하게 다형성에 대한 호출을 최적화 해제 할 수 있습니다 실행 시간. 그러나 그렇습니다. 일반적으로 비용 효율적이거나 불가능하지 않은 jvm에서도 그러한 것들이 불가능한 많은 언어가 있습니다.
Artur Biesiadowski

19

성능 조정 코드에 관한 거의 모든 속담은 암달의 법칙의 특별한 경우입니다 . 암달의 법칙에 대한 짧고 유머러스 한 진술은

프로그램의 한 부분이 런타임의 5 %를 차지하고 해당 부분을 최적화하여 이제 런타임의 0 %를 차지 하지 않으면 프로그램 전체가 5 % 더 빨라집니다.

(런타임의 0 %까지 일을 최적화하는 것은 완전히 가능합니다. 크고 복잡한 프로그램을 최적화하기 위해 앉을 때 적어도 실행 시간을 전혀 할 필요가없는 물건에 소비한다는 것을 알게 될 것입니다 .)

아무리 비싼 그들은 없습니다 : 사람들이 일반적으로 함수 호출 비용에 대해 걱정하지 말 이유는 일반적으로 전체 만 호출 오버 헤드에 미치는 런타임의 아주 작은 부분을 소비로 프로그램, 그래서 아주 많이 도움이되지 않는 그들을 가속화 .

그러나 모든 함수 호출을 더 빠르게 만드는 트릭이 있다면 그 트릭이 가치가 있습니다. 컴파일러 개발자는 함수 "프롤로그"및 "에필로그"를 최적화하는 데 많은 시간을 소비 합니다. 이는 각 컴파일러에 대해 조금만이라도 해당 컴파일러로 컴파일 된 모든 프로그램에 혜택을주기 때문입니다 .

그리고 프로그램 단순히 함수 호출을 수행하는 데 많은 런타임을 소비 한다고 믿을만한 이유가 있다면 해당 함수 호출 중 일부가 불필요한 지 여부를 생각해야합니다. 언제해야하는지 알기위한 몇 가지 규칙이 있습니다.

  • 함수의 호출 당 런타임이 밀리 초 미만이지만 해당 함수를 수십만 번 호출하면 아마도 인라인되어야합니다.

  • 프로그램 프로파일에 수천 개의 함수가 표시되고 그 중 어느 것도 런타임의 0.1 % 이상을 차지하지 않으면 함수 호출 오버 헤드가 전체적으로 중요 할 수 있습니다.

  • 다음 계층으로 디스패치하는 것 이상의 작업이 거의없는 추상화 계층이 많은 " lasagna code "가 있고 이러한 모든 계층이 가상 메소드 호출로 구현 된 경우 CPU가 낭비 할 가능성이 높습니다. 간접 분기 파이프 라인 정지에 많은 시간이 걸립니다. 불행히도, 이것에 대한 유일한 치료법은 일부 레이어를 제거하는 것인데, 종종 매우 어렵습니다.


7
중첩 루프에서 깊게 수행되는 값 비싼 물건을 조심하십시오. 하나의 기능을 최적화하고 10 배 빠르게 실행되는 코드를 얻었습니다. 그것은 프로파일 러가 범인을 지적한 후에였습니다. (O (n ^ 3)에서 작은
nO

"불행히도, 이것에 대한 유일한 치료법은 일부 레이어를 제거하는 것인데, 이는 종종 매우 어렵습니다." -이것은 언어 컴파일러 및 / 또는 가상 머신 기술에 따라 크게 다릅니다. 컴파일러가 인라인하기 쉽도록 코드를 수정할 수있는 경우 (예 : finalJava에 적용 가능한 클래스 및 메소드 또는 virtualC # 또는 C ++의 비 메소드를 사용하여) 컴파일러 / 런타임에서 간접적으로 제거 할 수 있습니다. 대규모 구조 조정 없이는 이익을 볼 수 있습니다. @JorgWMittag가 위에서 지적한 바와 같이, JVM이 최적화가 가능하지 않은 경우에도 인라인 할 수있다.
Jules

... 유효하기 때문에 어쨌든 계층화에도 불구하고 코드에서 수행하는 것이 좋습니다.
Jules

@Jules JIT 컴파일러 추론 적 최적화를 수행 할 수 있다는 것이 사실이지만 , 그러한 최적화 균일하게 적용되는 것은 아닙니다 . 특히 Java와 관련하여 필자의 경험은 개발자 문화가 레이어 위에 쌓인 레이어를 선호하여 매우 깊은 호출 스택을 초래한다는 것입니다. 일화 적으로, 이것은 많은 Java 응용 프로그램의 느리고 부풀어 오른 느낌에 기여합니다. 이와 같이 고도로 계층화 된 아키텍처는 계층이 기술적으로 인라인 가능하지 않더라도 JIT 런타임에 대해 작동합니다. JIT는 구조적 문제를 자동으로 해결할 수있는 마법의 총알이 아닙니다.
amon September

@amon "lasagna 코드"에 대한 나의 경험은 1990 년대에 시작된 많은 코드를 가진 매우 큰 C ++ 애플리케이션에서 나온 것입니다. C ++ 컴파일러는이 같은 프로그램의 추상화 처벌을 분쇄하기 위해 매우 영웅적인 노력에 가서, 여전히 당신은 (I-캐시 미스에 또 다른 큰 덩어리) 간접 분기의 파이프 라인 노점에 벽 시계 런타임의 상당 부분을 지출을 볼 수 있습니다 .
zwol

17

나는이 인용문에 도전 할 것이다.

읽을 수있는 코드를 작성하는 것과 수행 코드를 작성하는 것은 거의 항상 상충 관계입니다.

이것은 실제로 잘못된 주장이며 잠재적으로 위험한 태도입니다. 이 일부 특정 당신이 균형을해야 할 경우가 있지만, 일반적으로 두 가지 요소는 독립적이다.

필요한 트레이드 오프의 예는 단순한 알고리즘 대보다 복잡하지만 성능이 뛰어난 알고리즘을 사용하는 경우입니다. 해시 테이블 구현은 링크 된 목록 구현보다 훨씬 복잡하지만 조회 속도가 느리므로 단순성 (가독성에 영향을주는 요소)을 성능과 교환해야 할 수 있습니다.

함수 호출 오버 헤드와 관련하여 재귀 알고리즘을 반복으로 바꾸면 알고리즘과 언어에 따라 상당한 이점이있을 수 있습니다. 그러나 이것은 다시 매우 구체적인 시나리오이며 일반적으로 함수 호출의 오버 헤드는 무시할 수 있거나 최적화되지 않습니다.

(파이썬과 같은 일부 동적 언어에는 메서드 호출 오버 헤드가 상당히 큽니다. 그러나 성능이 문제가된다면 우선 파이썬을 사용해서는 안됩니다.)

읽을 수있는 코드에 대한 대부분의 원칙-일관된 형식, 의미있는 식별자 이름, 적절하고 유용한 주석 등은 성능에 영향을 미치지 않습니다. 또한 문자열 대신 열거 형을 사용하는 것과 같은 일부 성능에도 이점이 있습니다.


5

대부분의 경우 함수 호출 오버 헤드는 중요하지 않습니다.

그러나 인라인 코드에서 더 큰 이득은 인라인 후 새 코드를 최적화하는 것 입니다.

예를 들어 상수 인수로 함수를 호출하면 옵티마이 저는 이제 호출을 인라인하기 전에는 불가능했던 위치에서 해당 인수를 일정하게 접을 수 있습니다. 인수가 함수 포인터 (또는 람다) 인 경우 옵티마이 저는 이제 해당 람다에 대한 호출도 인라인 할 수 있습니다.

이것은 실제 함수 포인터가 콜 사이트까지 항상 접히지 않는 한 가상 함수와 함수 포인터가 전혀 인라인 할 수 없기 때문에 매력적이지 않은 큰 이유입니다.


5

성능이 프로그램에 중요하고 실제로 많은 호출이 있다고 가정하면 호출 유형에 따라 비용이 여전히 중요하거나 중요하지 않을 수 있습니다.

호출 된 함수가 작고 컴파일러가이를 인라인 할 수 있으면 비용은 본질적으로 0입니다. 최신 컴파일러 / 언어 구현에는 JIT, 링크 타임 최적화 및 / 또는 모듈 시스템이 있으며, 유용한 경우 함수를 인라인하는 기능을 최대화하도록 설계되었습니다.

OTOH에는 함수 호출에 대한 명백한 비용이 있습니다. 호출만으로도 호출 전후에 컴파일러 최적화를 방해 할 수 있습니다.

컴파일러가 호출 된 함수가 수행하는 작업 (예 : 가상 / 동적 디스패치 또는 동적 라이브러리의 함수)에 대해 추론 할 수없는 경우 함수가 부작용을 일으킬 수 있다고 비판적으로 가정해야 할 수 있습니다. 예외를 던지고 수정하십시오. 전역 상태이거나 포인터를 통해 표시되는 메모리를 변경하십시오. 컴파일러는 임시 값을 백 메모리에 저장하고 호출 후 다시 읽어야 할 수도 있습니다. 호출 주위에서 명령어를 재정렬 할 수 없으므로 루프를 벡터화하거나 루프에서 중복 계산을 수행하지 못할 수 있습니다.

예를 들어, 각 루프 반복에서 불필요하게 함수를 호출하는 경우 :

for(int i=0; i < /* gasp! */ strlen(s); i++) x ^= s[i];

컴파일러는 이것이 순수한 함수라는 것을 알고 루프 밖으로 이동시킬 수 있습니다 (이 예제와 같은 끔찍한 경우에도 실수로 O (n ^ 2) 알고리즘을 O (n)으로 수정합니다).

for(int i=0, end=strlen(s); i < end; i++) x ^= s[i];

그리고 와이드 / SIMD 명령어를 사용하여 한 번에 4/8/16 요소를 처리하도록 루프를 다시 작성할 수도 있습니다.

그러나 루프에서 불투명 한 코드에 대한 호출을 추가하면 호출이 아무것도하지 않고 자체적으로 저렴하더라도 컴파일러는 최악의 상황을 가정해야합니다. 호출은 s변경 과 동일한 메모리를 가리키는 전역 변수에 액세스합니다 그 내용 ( const함수에 있더라도 const다른 곳에서는 불가능할 수 있음 )으로 인해 최적화가 불가능합니다.

for(int i=0; i < strlen(s); i++) {
    x ^= s[i];
    do_nothing();
}

3

오래된 논문 은 귀하의 질문에 대답 할 수 있습니다.

Guy Lewis Steele, Jr. " ​​'비용이 많이 드는 절차 호출'신화 또는 유해한 것으로 간주되는 절차 호출 구현 또는 람다 : 궁극적 인 GOTO" MIT AI Lab. AI Lab 메모 AIM-443. 1977 년 10 월.

요약:

Folklore는 GOTO 문이 "저렴한"반면 프로 시저 호출은 "고가"라고 말합니다. 이 신화는 주로 제대로 구현되지 않은 언어 구현의 결과입니다. 이 신화의 역사적 성장이 고려됩니다. 이 신화에 어떤 이론적 아이디어와 기존의 구현이 논의되어 있는가. 프로 시저 호출을 제한없이 사용하면 멋진 스타일의 자유를 얻을 수 있습니다. 특히, 플로우 차트는 추가 변수를 도입하지 않고 "구조화 된"프로그램으로 작성할 수 있습니다. GOTO 문과 프로 시저 호출의 어려움은 추상적 인 프로그래밍 개념과 구체적인 언어 구성 사이의 충돌로 특징 지워집니다.


12
나는 구식의 논문이 " 현대 컴파일러 에서 여전히 함수 호출 비용이 중요한지"에 대한 질문에 답할 것이라고 의심한다 .
코디 그레이

6
@CodyGray 1977 년 이후 컴파일러 기술이 발전해야한다고 생각합니다. 따라서 1977 년에 함수 호출을 저렴하게 할 수 있다면 지금 할 수있을 것입니다. 따라서 대답은 '아니오'입니다. 물론 이것은 함수 인라이닝과 같은 일을 할 수있는 적절한 언어 구현을 사용한다고 가정합니다.
Alex Vong

4
@AlexVong 1977 컴파일러 최적화에 의존하는 것은 석기 시대의 원자재 가격 추세에 의존하는 것과 같습니다. 모든 것이 너무 많이 변했습니다. 예를 들어, 곱셈은 더 저렴한 연산으로 메모리 액세스로 대체되었습니다. 현재, 그것은 큰 요인에 의해 더 비싸다. 가상 메소드 호출은 예전보다 메모리 비용이 훨씬 비싸지 만 (메모리 액세스 및 분기 오해 예측) 종종 최적화 할 수 있고 가상 메소드 호출을 인라인 할 수 있기 때문에 (Java는 항상 수행) 정확히 제로. 1977 년에는 이와 같은 것이 없었습니다.
maaartinus

3
다른 사람들이 지적했듯이, 오래된 연구를 무효화 한 것은 컴파일러 기술의 변화가 아닙니다. 만약 마이크로 아키텍처가 크게 변하지 않고 컴파일러가 계속 개선 되었다면, 논문의 결론은 여전히 ​​유효 할 것이다. 그러나 그것은 일어나지 않았습니다. 마이크로 아키텍처는 컴파일러보다 더 많이 바뀌었다. 예전에는 빠르 던 것들이 상대적으로 느리고 느립니다.
코디 그레이

2
@AlexVong 종이를 쓸모 없게 만드는 CPU 변경 사항을보다 정확하게 설명하기 위해 : 1977 년에 주요 메모리 액세스는 단일 CPU주기였습니다. 오늘날 L1 (!) 캐시에 대한 간단한 액세스조차도 3-4주기의 대기 시간을 갖습니다. 이제 함수 호출은 메모리 액세스 (스택 프레임 생성, 반환 주소 저장, 로컬 변수에 대한 레지스터 저장)에서 상당히 무거워 단일 함수 호출 비용을 20 회 이상 주기로 쉽게 구동 할 수 있습니다. 함수가 인수를 다시 정렬하고 콜 스루에 전달할 다른 상수 인수를 추가하면 거의 100 % 오버 헤드가 발생합니다.
cmaster

3
  • C ++에서 인수를 복사하는 함수 호출을 디자인 할 때는 기본값이 "pass by value"입니다. 레지스터 저장 및 기타 스택 프레임 관련 항목으로 인한 함수 호출 오버 헤드는 의도하지 않은 (그리고 잠재적으로 매우 비싼) 객체의 복사본으로 인해 압도 될 수 있습니다.

  • 고도로 팩토링 된 코드를 포기하기 전에 조사해야하는 스택 프레임 관련 최적화가 있습니다.

  • 느린 프로그램을 처리해야 할 때 대부분의 경우 알고리즘 변경을 수행하면 인라인 함수 호출보다 속도가 훨씬 빨라졌습니다. 예를 들어, 다른 엔지니어가 맵 맵 구조를 채운 파서를 수정했습니다. 그 일환으로 그는 하나의 맵에서 논리적으로 연관된 인덱스로 캐시 된 인덱스를 제거했습니다. 이는 코드 견고성 향상에 도움이되었지만, 이후의 모든 액세스에 대해 해시 조회를 수행하고 저장된 인덱스를 사용하여 해시 조회를 수행하여 프로그램 속도를 100 배 낮추어 사용할 수 없게 만들었습니다. 프로파일 링은 대부분의 시간이 해싱 함수에 소비 된 것으로 나타났습니다.


4
첫 번째 조언은 약간 오래된 것입니다. C ++ 11부터는 움직일 수있었습니다. 특히, 인수를 내부적으로 수정해야하는 함수의 경우 값으로 인수를 가져 와서 적절하게 수정하는 것이 가장 효율적인 선택이 될 수 있습니다.
MSalters

@MSalters : 나는 당신이 "특히 더"또는 "다른 것"으로 착각했다고 생각합니다. 사본이나 참조를 전달하기로 한 결정은 C ++ 11 이전에있었습니다.
phresnel

@ phresnel : 나는 그것이 옳았다 고 생각합니다. 내가 언급하는 특정 사례는 호출자에서 임시를 만들고 인수 로 이동 한 다음 수신자에서 수정하는 경우입니다. C ++ 03은 비
-const

@MSalters : 그렇다면 처음 읽을 때 당신의 의견을 오해했습니다. C ++ 11 이전에는 값을 전달하는 것이 전달 된 값을 수정하려는 경우 수행 할 수있는 것이 아니라는 것을 암시하는 것처럼 보였습니다.
phresnel

'이동'의 출현은 외부보다 기능에 더 편리하게 구성되고 참조로 전달되는 객체의 반환에 가장 크게 도움이됩니다. 그 전에 함수에서 객체를 반환하면 복사본이 호출되어 종종 값 비싼 이동이 발생했습니다. 그것은 함수 인수를 다루지 않습니다. 컴파일러에 함수 인수 (&& 구문)로 '이동'할 수있는 권한을 컴파일러에 명시 적으로 부여해야하므로 주석에 "designing"이라는 단어를 신중하게 입력했습니다. 나는 복사 생성자를 '삭제'하는 습관을들이는 것이 귀중한 장소를 식별했습니다.
user2543191

3

다른 사람들이 말했듯이 프로그램의 성능을 먼저 측정해야하며 실제로 차이가 없을 것입니다.

아직도, 개념적 수준에서 나는 당신의 질문에 혼란 스러울 몇 가지를 정리할 것이라고 생각했습니다. 먼저, 당신은 묻는다 :

최신 컴파일러에서 함수 호출 비용이 여전히 중요합니까?

키워드 "function"과 "compilers"에 주목하십시오. 귀하의 견적은 미묘하게 다릅니다.

언어에 따라 메서드 호출 비용이 상당 할 수 있습니다.

이것은 객체 지향적 의미에서 메소드 에 대해 이야기하고 있습니다 .

"함수"와 "방법"은 종종 상호 교환 적으로 사용되지만 비용과 관련하여 (필요한) 비용과 컴파일과 관련하여 (주어진 맥락) 차이가 있습니다.

특히 정적 디스패치동적 디스패치에 대해 알아야 합니다. 현재로서는 최적화를 무시하겠습니다.

C와 같은 언어에서 우리는 보통 static dispatch를 가진 함수 를 호출 합니다 . 예를 들면 다음과 같습니다.

int foo(int x) {
  return x + 1;
}

int bar(int y) {
  return foo(y);
}

int main() {
  return bar(42);
}

컴파일러가 호출을 볼 때 foo(y)해당 foo이름이 참조 하는 함수를 알고 있으므로 출력 프로그램이 foo함수로 바로 이동할 수 있으므로 상당히 저렴합니다. 이것이 정적 디스패치의 의미입니다.

대안은 동적 디스패치 인데, 컴파일러 어떤 함수가 호출되는지 알 수 없습니다 . 예를 들어, 다음은 Haskell 코드입니다 (C 코드가 복잡하기 때문에).

foo x = x + 1

bar f x = f x

main = print (bar foo 42)

여기서 bar함수는 argument를 호출합니다 f. 따라서 컴파일러는 bar어디로 이동할지 모르기 때문에 빠른 점프 명령으로 컴파일 할 수 없습니다 . 대신에, 우리가 생성 한 코드 bar는 역 참조 f하여 어떤 함수를 가리키는 지 알아 낸 다음 점프합니다. 이것이 바로 동적 디스패치의 의미입니다.

이 두 예는 모두 기능을 위한 입니다. 동적으로 디스패치 된 특정 스타일의 함수로 생각할 수있는 메소드를 언급했습니다 . 예를 들어 다음은 Python입니다.

class A:
  def __init__(self, x):
    self.x = x

  def foo(self):
    return self.x + 1

def bar(y):
  return y.foo()

z = A(42)
bar(z)

y.foo()호출은 객체 에서 foo속성 의 값을 찾고 y찾은 것을 호출 하기 때문에 동적 디스패치를 ​​사용합니다 . 그것은 그 모르는 y클래스가됩니다 A, 또는 것을 A클래스가 포함 foo방법을, 그래서 우리는 단지 똑바로로 이동할 수 없습니다.

이것이 기본 아이디어입니다. 정적 디스패치는 컴파일 또는 해석 여부에 관계없이 동적 디스패치보다 빠릅니다 . 다른 모든 것은 평등합니다. 역 참조는 어느 쪽이든 추가 비용이 발생합니다.

그렇다면 이것이 현대 최적화 컴파일러에 어떤 영향을 미칩니 까?

가장 먼저 주목해야 할 것은 정적 디스패치가 더 크게 최적화 될 수 있다는 것입니다. 우리가 어떤 함수로 점프하는지 알면 인라인과 같은 일을 할 수 있습니다. 동적 디스패치를 ​​사용하면 런타임까지 점프하는 것을 알 수 없으므로 최적화가 그리 많지 않습니다.

둘째, 일부 언어 에서는 일부 동적 디스패치가 끝나는 위치 를 추론 하여 정적 디스패치로 최적화 할 수 있습니다. 이를 통해 인라인 등과 같은 다른 최적화를 수행 할 수 있습니다.

위의 파이썬 예제에서 파이썬은 다른 코드가 클래스와 속성을 재정의하도록 허용하기 때문에 그러한 추론은 절망적입니다.

예를 들어 어노테이션을 사용하여 y클래스 로 제한 하여 더 많은 제한 A을 적용 할 수있는 경우 해당 정보를 사용하여 대상 함수를 추론 할 수 있습니다. 서브 클래 싱 (클래스가있는 거의 모든 언어)이있는 언어에서는 y실제로 다른 (하위) 클래스를 가질 수 있으므로 실제로는 충분하지 않으므로 final어떤 함수가 호출되는지 정확히 알기 위해서는 Java 주석 과 같은 추가 정보가 필요합니다 .

하스켈은 OO 언어가 아니라 우리의 가치를 추론 할 수 f인라인으로 bar(하는 정적 으로 전달) main대체 foo를 위해 y. fooin 의 대상 main이 정적으로 알려지기 때문에 호출이 정적으로 전달되고 아마도 인라인되고 완전히 최적화 될 것입니다 (이러한 함수가 작기 때문에 컴파일러가 인라인 할 가능성이 더 높습니다. 일반적으로 믿을 수는 없지만) ).

따라서 비용은 다음과 같습니다.

  • 언어가 호출을 정적으로 또는 동적으로 전달합니까?
  • 후자의 경우 언어는 구현이 다른 정보 (예 : 유형, 클래스, 주석, 인라인 등)를 사용하여 대상을 유추 할 수 있습니까?
  • 정적 디스패치 (추론되거나 그렇지 않은)를 얼마나 적극적으로 최적화 할 수 있습니까?

동적 디스패치가 많고 컴파일러에서 사용할 수있는 보증이 거의없는 "매우 동적 인"언어를 사용하는 경우 모든 호출에 비용이 발생합니다. "매우 정적 인"언어를 사용하는 경우 성숙한 컴파일러는 매우 빠른 코드를 생성합니다. 당신이 사이에 있다면, 그것은 코딩 스타일과 구현이 얼마나 똑똑한 지에 달려 있습니다.


Haskell 예제와 같이 클로저 (또는 함수 포인터 ) 를 호출하는 것은 동적 디스패치 라는 데 동의하지 않습니다 . 동적 디스패치 는 클로저를 얻기 위해 일부 계산 (예 : vtable 사용 )을 포함하므로 간접 호출보다 비용이 많이 듭니다. 그렇지 않으면 좋은 대답입니다.
Basile Starynkevitch

2

그렇습니다. 누락 된 브랜치 예측은 수십 년 전에 비해 현대 하드웨어에서 비용이 많이 들지만 컴파일러는이를 최적화하는 데 훨씬 더 똑똑해졌습니다.

예를 들어 Java를 고려하십시오. 언뜻 보면,이 언어에서는 함수 호출 오버 헤드가 특히 지배적이어야합니다.

  • JavaBean 규칙으로 인해 작은 함수가 널리 퍼져 있습니다.
  • 함수는 가상으로 기본 설정되며 일반적으로
  • 컴파일 단위는 클래스입니다. 런타임은 이전에 단형 화 된 메소드를 대체하는 서브 클래스를 포함하여 언제든지 새로운 클래스로드를 지원

이러한 관행에 의해 충격을 받아, 평균 C 프로그래머는 Java가 C보다 적어도 1 배 느려 야한다고 예측할 것입니다. 그리고 20 년 전에 그는 옳았을 것입니다. 그러나 최신 벤치 마크에서는 관용적 인 C 코드의 몇 퍼센트 내에 관용적 Java 코드를 배치합니다. 어떻게 가능합니까?

현대 JVM이 인라인 함수 호출을 당연히 요구하기 때문입니다. 추측 인라이닝을 사용합니다.

  1. 새로로드 된 코드는 최적화없이 실행됩니다. 이 단계에서 모든 호출 사이트에 대해 JVM은 실제로 호출 된 메소드를 추적합니다.
  2. 코드가 성능 핫스팟으로 식별되면 런타임은 이러한 통계를 사용하여 가장 가능한 실행 경로를 식별하고 그 경로를 인라인하여 추론 적 최적화가 적용되지 않는 경우 조건부 분기를 접두사로 붙입니다.

즉, 코드 :

int x = point.getX();

다시 쓰다

if (point.class != Point) GOTO interpreter;
x = point.x;

물론, 포인트가 지정되지 않은 한 런타임은이 유형 검사를 위로 이동하거나 유형이 호출 코드에 알려진 경우이를 제거하기에 충분히 영리합니다.

요약하자면, Java조차도 자동 메소드 인라인을 관리하는 경우 컴파일러가 자동 인라인을 지원할 수없는 고유 한 이유가 없으며 그렇게하는 모든 이유는 인라인이 최신 프로세서에서 매우 유용하기 때문입니다. 따라서 가장 기본적인 최적화 전략을 모르는 현대의 주류 컴파일러는 거의 상상할 수 없으며 달리 입증되지 않는 한이 기능을 수행 할 수있는 컴파일러를 가정합니다.


4
"컴파일러가 자동 인라인을 지원할 수없는 고유 한 이유는 없습니다"– 있습니다. JIT 컴파일, 자체 수정 코드 (OS로 인해 보안으로 인해 방해 할 수 있음) 및 자동 프로파일 가이드 전체 프로그램 최적화 기능에 대해 이야기했습니다. 동적 연결을 허용하는 언어에 대한 AOT 컴파일러는 호출을 구체화하고 인라인하기에 충분하지 않습니다. OTOH : AOT 컴파일러는 가능한 모든 것을 최적화 할 시간이 있습니다. JIT 컴파일러는 핫스팟에서 저렴한 최적화에 집중할 시간이 있습니다. 대부분의 경우 JIT는 약간의 단점이 있습니다.
amon

2
"보안 때문에"Chrome을 실행하지 못하게하는 하나의 OS를 알려주십시오 (V8은 JavaScript를 런타임에 기본 코드로 컴파일합니다). 또한 AOT를 인라인하려는 것은 본질적인 이유가 아니며 (언어에 의해 결정되지는 않지만 컴파일러에 대해 선택한 아키텍처) 동적 링크는 컴파일 단위에서 AOT 인라인을 금지하지만 컴파일 인라인을 금지하지는 않습니다. 대부분의 통화가 이루어지는 단위. 실제로 동적 링크를 Java보다 덜 과도하게 사용하는 언어에서는 유용한 인라인이 더 쉽습니다.
meriton

4
특히 iOS는 권한이없는 앱의 JIT를 방지합니다. Chrome 또는 Firefox는 자체 엔진 대신 Apple 제공 웹보기를 사용해야합니다. AOT와 JIT는 언어 수준의 선택이 아니라 구현 수준이라는 점이 좋습니다.
amon September

@meriton Windows 10 S 및 비디오 게임 콘솔 운영 체제도 타사 JIT 엔진을 차단하는 경향이 있습니다.
Damian Yerrick

2

언어에 따라 메서드 호출 비용이 상당 할 수 있습니다. 읽을 수있는 코드를 작성하는 것과 수행 코드를 작성하는 것은 거의 항상 상충 관계입니다.

불행히도 이것은 다음에 크게 의존합니다.

  • JIT를 포함한 컴파일러 툴체인
  • 도메인.

우선, 성능 최적화의 첫 번째 법칙은 프로파일 입니다. 소프트웨어 부분 의 성능이 전체 스택의 성능과 관련없는 많은 도메인이 있습니다 : 데이터베이스 호출, 네트워크 작업, OS 작업 ...

즉, 소프트웨어 성능이 지연을 개선하지 않더라도 소프트웨어 성능이 완전히 관련이 없다는 것을 의미합니다. 소프트웨어를 최적화하면 에너지 절약 및 하드웨어 절약 (또는 모바일 앱의 배터리 절약)이 발생할 수 있습니다.

그러나 이것들은 일반적으로 시선을 사로 잡을 수 없으며 종종 알고리즘 개선이 미세 최적화보다 큰 마진을 능가합니다.

따라서 최적화하기 전에 최적화하려는 대상과 가치가 있는지 이해해야합니다.


이제는 순수한 소프트웨어 성능과 관련하여 도구 체인마다 크게 다릅니다.

함수 호출에는 두 가지 비용이 있습니다.

  • 런타임 비용
  • 컴파일 시간 비용.

런타임 비용은 다소 분명합니다. 함수 호출을 수행하려면 일정량의 작업이 필요합니다. 예를 들어 x86에서 C를 사용하면 함수 호출에 (1) 레지스터를 스택에 흘리거나 (2) 인수를 레지스터에 푸시하고, 호출을 수행 한 다음 (3) 스택에서 레지스터를 복원해야합니다. 참조 관련된 작업을보고 규칙을 호출이 요약 .

이 레지스터 유출 / 복원에는 사소한 시간이 걸립니다 (수십 번의 CPU 사이클).

일반적 으로이 비용은 함수를 실행하는 실제 비용과 비교하여 사소한 것으로 예상되지만 게터, 간단한 조건으로 보호되는 함수 등 일부 패턴은 비생산적입니다.

인터프리터 와는 별도로 프로그래머는 컴파일러 또는 JIT 가 불필요한 함수 호출을 최적화 하기를 희망합니다 . 비록이 희망이 때때로 열매를 맺지 못할 수도 있습니다. 옵티마이 저는 마술이 아니기 때문입니다.

옵티마이 저는 함수 호출이 사소한 것을 감지 하고 호출을 인라인 합니다. 본질적으로 호출 사이트에서 함수 본문을 복사 / 붙여 넣기합니다. 이것은 항상 좋은 최적화는 아니지만 (부풀림을 유발할 수 있음) 인라인 은 context를 노출 하고 컨텍스트는 더 많은 최적화를 가능하게 하기 때문에 일반적으로 가치가 있습니다.

일반적인 예는 다음과 같습니다.

void func(condition: boolean) {
    if (condition) {
        doLotsOfWork();
    }
}

void call() { func(false); }

func인라인 된 경우 옵티마이 저는 분기가 수행되지 않음을 인식하고로 최적화 call합니다 void call() {}.

그런 의미에서, 최적화 프로그램에서 정보를 숨겨 함수 호출 (아직 인라인되지 않은 경우)은 특정 최적화를 방해 할 수 있습니다. 가상 함수 호출은 특히 유죄입니다. 가상화 (실시간에 어떤 함수가 궁극적으로 호출되는지 증명)가 항상 쉬운 것은 아니기 때문입니다.


결론적으로, 내 충고는 작성하는 것입니다 명확 조기 알고리즘 pessimization (입방 복잡성 빠르게 악화 바이트)를 피하기 위해, 먼저, 다음에만 최적화 필요가 무엇을 최적화 할 수 있습니다.


1

"언어에 따라 메소드 호출 비용이 상당 할 수 있음을 기억하십시오. 읽을 수있는 코드를 작성하는 것과 수행 코드를 작성하는 것은 거의 항상 상충 관계입니다."

오늘날 인용 된 풍부한 성능의 현대 컴파일러를 고려할 때이 인용문은 현재 어떤 조건에서 유효합니까?

난 그냥 말하지 않을거야. 나는 그 견적을 그냥 던져 버리는 것이 무모하다고 믿습니다.

물론 나는 완전한 진실을 말하고 있지는 않지만 그렇게 진실한 것에 관심이 없습니다. 그것은 매트릭스 영화에서와 같이, 1 또는 2 또는 3이라면 잊어 버렸습니다. 나는 큰 멜론을 가진 섹시한 이탈리아 여배우가있는 것 같아요. 오라클 레이디는 키아누 리브스 (Kaanu Reeves)에게 "나는 당신이 듣고 싶은 것을 말해주었습니다." 또는이 효과에 대해 이야기했습니다.

프로그래머는 이것을들을 필요가 없습니다. 그들이 프로파일 러에 대해 경험이 있고 견적이 컴파일러에 어느 정도 적용될 수 있다면, 이미 알고있을 것이며 프로파일 링 출력을 이해하고 측정을 통해 특정 리프 호출이 핫스팟 인 이유를 이해하는 적절한 방법을 배울 것입니다. 그들이 경험이없고 코드를 프로파일 링 한 적이 없다면, 이것이 마지막으로 들려야 할 것입니다. 핫스팟을 식별하기 전에 코드를 작성하는 시점까지 코드를 작성하는 방식을 미묘하게 손상시켜야한다는 것입니다. 더 성능이 향상됩니다.

어쨌든보다 정확한 응답을 위해서는 의존합니다. 조건의 보트로드 중 일부는 이미 정답에 나열되어 있습니다. 하나의 언어를 선택하는 가능한 조건은 이미 가상 호출에서 동적 디스패치에 들어가야 할 때와 컴파일러와 링커에서 최적화 할 수있는 C ++과 같이 이미 거대합니다. 가능한 모든 언어의 조건을 다루고 컴파일러를 사용합니다. 하지만 "누가 신경 쓰나요?" 레이트 레이싱과 같은 성능이 중요한 영역에서도 작업하기 때문에 측정을 시작하기 전에 손으로 인라인하는 방법을 사용합니다.

나는 어떤 사람들은 측정하기 전에 마이크로 최적화를해서는 안된다는 제안에 열심을 가지고 있다고 생각합니다. 참조 카운트의 지역성을 마이크로 최적화로 최적화하는 경우 종종 성능 중심적 (예 : 레이트 레이싱 코드)이 될 것으로 알고있는 영역에서 데이터 지향 디자인 사고 방식으로 처음부터 이러한 최적화를 적용하기 시작합니다. 그렇지 않으면 수년간이 도메인에서 일한 후 곧 큰 섹션을 다시 작성해야한다는 것을 알고 있습니다. 캐시 적중에 대한 데이터 표현을 최적화하는 것은 2 차 시간 대 선형에 대해 이야기하지 않는 한 알고리즘 개선과 동일한 종류의 성능 향상을 가질 수 있습니다.

그러나 측정 전에 인라인을 시작 해야하는 좋은 이유를 보지 못했습니다. 특히 프로파일 러는 인라인의 이점을 밝히지 않지만 인라인되지 않은 이점을 밝히지 않기 때문에 (인라인하지 않으면 실제로 코드를 더 빠르게 만들 수 있기 때문에) 비선형 함수 호출은 드문 경우로 핫 코드의 icache에 대한 참조의 지역성을 개선하고 때로는 최적화 프로그램이 일반적인 사례 실행 경로에 대해 더 나은 작업을 수행하도록 허용합니다).

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.