표준 :: 기능 대 템플릿


161

C ++ 11 덕분 std::function에 functor 래퍼 제품군을 받았습니다 . 불행히도, 나는 이러한 새로운 추가 사항에 대한 나쁜 것들만을 계속 듣고 있습니다. 가장 인기있는 것은 그들이 엄청 느리다는 것입니다. 나는 그것을 테스트했으며 템플릿과 비교할 때 정말 빨랐습니다.

#include <iostream>
#include <functional>
#include <string>
#include <chrono>

template <typename F>
float calc1(F f) { return -1.0f * f(3.3f) + 666.0f; }

float calc2(std::function<float(float)> f) { return -1.0f * f(3.3f) + 666.0f; }

int main() {
    using namespace std::chrono;

    const auto tp1 = system_clock::now();
    for (int i = 0; i < 1e8; ++i) {
        calc1([](float arg){ return arg * 0.5f; });
    }
    const auto tp2 = high_resolution_clock::now();

    const auto d = duration_cast<milliseconds>(tp2 - tp1);  
    std::cout << d.count() << std::endl;
    return 0;
}

111ms 대 1241ms 나는 템플릿이 멋지게 인라인 될 수 있고 function가상 호출을 통해 내부를 덮을 수 있기 때문이라고 생각 합니다.

분명히 템플릿에는 문제가 있습니다.

  • 라이브러리를 닫힌 코드로 해제 할 때 원하지 않는 헤더가 아닌 헤더로 제공되어야합니다.
  • extern template유사한 정책이 도입 되지 않으면 컴파일 시간이 훨씬 길어질 수 있습니다 .
  • 템플릿의 요구 사항 (개념, 누구?)을 나타내는 깨끗한 방법은 없습니다 (적어도 나에게 알려져 있음). 어떤 종류의 functor가 필요한지 설명하는 주석이 있습니다.

따라서 functions 를 통과 함수의 사실상 표준 으로 사용할 수 있고 고성능이 필요한 템플릿을 사용해야한다고 가정 할 수 있습니까?


편집하다:

내 컴파일러는 CTP가 없는 Visual Studio 2012 입니다.


16
std::function실제로 이기종 호출 가능 오브젝트 콜렉션이 필요한 경우에만 사용하십시오 (즉, 런타임시 더 이상의 식별 정보를 사용할 수 없음).
Kerrek SB

30
당신은 잘못된 것을 비교하고 있습니다. 템플릿은 두 경우 모두에 사용되며 " std::function또는 템플릿"이 아닙니다 . 여기서 문제는 단순히 람다를 std::function감싸지 않고 vs 람다를 감싸는 것입니다 std::function. 현재 귀하의 질문은 "사과 나 그릇을 선호해야합니까?"
궤도에서 가벼움 경주

7
1ns이든 10ns이든 관계 없습니다.
ipc

23
@ipc : 1000 %는 아무것도 아닙니다. OP가 식별 한대로 실제적인 목적으로 확장 성이 제공 될 때주의를 기울입니다.
궤도에서 가벼움 경주

18
@ipc 10 배 느리다. 속도는 기준과 비교되어야합니다. 그것은 단지 나노초이기 때문에 중요하지 않다고 생각하는 것입니다.
Paul Manta 2012

답변:


170

일반적으로 디자인 상황에 직면 한 경우 템플릿을 사용하십시오 . 집중해야 할 것은 사용 사례를 구분하는 것이라고 생각하기 때문에 디자인 이라는 단어를 강조했습니다.std::function 와 템플릿 .

일반적으로 템플릿 선택은 더 넓은 원칙의 예일뿐입니다 . 컴파일 타임에 가능한 한 많은 제약 조건을 지정하십시오 . 이론적 근거는 간단합니다. 오류가 발생하거나 유형이 일치하지 않으면 프로그램이 생성되기 전에 고객에게 버그가있는 프로그램을 제공하지 않습니다.

또한 올바르게 지적했듯이 템플릿 함수에 대한 호출은 정적으로 (즉, 컴파일 타임에) 해결되므로 컴파일러는 코드를 최적화하고 인라인하는 데 필요한 모든 정보를 가지고 있습니다. vtable).

그렇습니다. 템플릿 지원이 완벽하지는 않으며 C ++ 11에는 여전히 개념에 대한 지원이 부족합니다. 그러나 나는 std::function그런 점에서 당신을 어떻게 구할 수 있을지 모르겠습니다 . std::function템플릿의 대안이 아니라 템플릿을 사용할 수없는 디자인 상황을위한 도구입니다.

이러한 사용 사례 중 하나 는 특정 서명을 준수하지만 컴파일 타임에 구체적인 유형을 알 수없는 호출 가능 객체를 호출하여 런타임시 호출을 해결해야 할 때 발생합니다 . 이것은 일반적으로 잠재적으로 다른 유형 의 콜백 모음이 있지만 균일하게 호출 해야하는 경우입니다. . 등록 된 콜백의 유형과 수는 프로그램 상태와 응용 프로그램 논리에 따라 런타임에 결정됩니다. 이러한 콜백 중 일부는 펑 터일 수 있고 일부는 일반 함수일 수 있으며 일부는 다른 함수를 특정 인수에 바인딩 한 결과 일 수 있습니다.

std::function그리고 std::bind또한 가능하게하는 자연 관용구 제공 기능 프로그래밍 기능은 객체로 취급하고 자연스럽게 카레 및 기타 기능을 생성하기 위해 결합되는 C ++에 있습니다. 이러한 종류의 조합은 템플릿으로도 달성 할 수 있지만, 비슷한 디자인 상황은 일반적으로 런타임에 결합 된 호출 가능한 객체의 유형을 결정해야하는 유스 케이스와 함께 제공됩니다.

마지막으로, std::function피할 수없는 다른 상황 이 있습니다. 예를 들어, 반복적 인 람다 를 작성하려는 경우 ; 그러나 이러한 제한은 제가 생각하는 개념적 차이보다는 기술적 한계에 의해 결정됩니다.

요약하면, 디자인에 집중 하고이 두 구성의 개념적 사용 사례가 무엇인지 이해하려고 노력하십시오. 당신이 그랬던 것처럼 그들을 비교해 보면, 그들이 속해 있지 않은 경기장으로 강요하고있는 것입니다.


23
"이것은 일반적으로 잠재적으로 다른 유형의 콜백 모음이 있지만 균일하게 호출해야하는 경우입니다." 중요한 비트입니다. 경험상 " 인터페이스 std::function의 스토리지 끝과 템플릿 Fun을 선호 합니다 "입니다.
R. Martinho Fernandes

2
참고 : 콘크리트 유형을 숨기는 기술을 유형 삭제 라고 합니다 (관리 언어의 유형 삭제와 혼동하지 마십시오). 동적 다형성 측면에서 구현되는 경우가 많지만 더 강력합니다 (예 : unique_ptr<void>가상 소멸자가없는 유형의 경우에도 적절한 소멸자 호출).
ecatmur 2019

2
@ ecatmur : 용어에 약간 일치하지 않지만 물질에 동의합니다. 동적 다형성은 "컴파일 타임에 다른 형태를 가정"하는 것으로 해석되는 정적 다형성과 달리 "런타임에 다른 형태를 가정"을 의미합니다. 후자는 템플릿을 통해 달성 할 수 없습니다. 저에게있어 유형 삭제는 디자인 측면에서 동적 다형성을 달성하기위한 일종의 전제 조건입니다. 다른 유형의 객체와 상호 작용하려면 일정한 인터페이스가 필요하며 유형 삭제는 유형을 추상화하는 방법입니다. 특정 정보.
Andy Prowl 2013

2
@ecatmur : 다이나믹 다형성은 개념적인 패턴이고, 타입 삭제는 그것을 실현할 수있는 기술입니다.
Andy Prowl 2013

2
@ Downvoter :이 답변에서 잘못된 점을 알고 궁금합니다.
Andy Prowl

89

Andy Prowl은 디자인 문제를 훌륭하게 다루었습니다. 물론 이것은 매우 중요하지만 원래 질문은에 관련된 더 많은 성능 문제와 관련이 있다고 생각합니다 std::function.

우선, 측정 기술에 대한 빠른 언급 : 11ms에 대해 얻은 calc1것은 전혀 의미가 없습니다. 실제로 생성 된 어셈블리 (또는 어셈블리 코드 디버깅)를 살펴보면 VS2012의 옵티마이 저가 호출 결과가 calc1반복과 독립적이며 호출을 루프 밖으로 이동 시킨다는 것을 알기에 충분히 영리하다는 것을 알 수 있습니다 .

for (int i = 0; i < 1e8; ++i) {
}
calc1([](float arg){ return arg * 0.5f; });

또한 통화 calc1는 눈에 띄는 효과가 없으며 전화를 완전히 끊습니다. 따라서 111ms는 빈 루프가 실행되는 시간입니다. (최적화 프로그램이 루프를 유지 한 것에 놀랐습니다.) 따라서 루프의 시간 측정에주의하십시오. 이것은 보이는 것처럼 간단하지 않습니다.

지적했듯이 옵티마이 저는 이해하기가 더 어려우며 std::function호출을 루프 밖으로 이동시키지 않습니다. 따라서 1241ms는 공정한 측정입니다 calc2.

이 공지 사항, std::function호출 다른 유형의 객체를 저장할 수있다. 따라서 스토리지에 대해 유형 삭제 마법을 수행해야합니다. 일반적으로 이는 동적 메모리 할당을 의미합니다 (기본적으로에 대한 호출을 통해 new). 이것은 상당히 비용이 많이 드는 작업으로 잘 알려져 있습니다.

표준 (20.8.11.2.1 / 5)은 고맙게도 VS2012가 수행하는 작은 객체 (특히 원본 코드)에 대한 동적 메모리 할당을 피하기 위해 구현을 권장합니다.

메모리 할당과 관련하여 얼마나 느려질 수 있는지에 대한 아이디어를 얻으려면 람다 식을 3으로 캡처하도록 변경했습니다 float. 이것은 작은 객체 최적화를 적용하기에 호출 가능한 객체를 너무 크게 만듭니다.

float a, b, c; // never mind the values
// ...
calc2([a,b,c](float arg){ return arg * 0.5f; });

이 버전의 경우 시간은 약 16000ms (원래 코드의 경우 1241ms와 비교)입니다.

마지막으로 람다의 수명은 std::function. 이 경우 람다 사본을 저장하는 대신 std::function"참조"를 저장할 수 있습니다. "참조"에 의해 나는 말은 std::reference_wrapper쉽게 기능에 의해 구축되는 std::refstd::cref. 보다 정확하게는 다음을 사용합니다.

auto func = [a,b,c](float arg){ return arg * 0.5f; };
calc2(std::cref(func));

시간은 약 1860ms로 줄어 듭니다.

나는 그것에 대해 얼마 전에 썼다.

http://www.drdobbs.com/cpp/efficient-use-of-lambda-expressions-and/232500059

이 기사에서 언급했듯이 C ++ 11에 대한 지원이 부족하기 때문에 VS2010에는 인수가 적용되지 않습니다. 글을 쓰는 시점에는 베타 버전의 VS2012 만 사용할 수 있었지만 C ++ 11에 대한 지원은 이미이 문제에 충분했습니다.


부작용이 없기 때문에 컴파일러가 최적화 한 장난감 예제를 사용하여 코드 속도를 증명하고 싶을 때 실제로 흥미 롭습니다. 나는 실제 / 제작 코드없이 이러한 종류의 측정에 거의 베팅을 할 수 없다고 말합니다.
Ghita

Ghita @ :이 예에서는, 코드가 멀리 최적화 될 방지하기 위해 calc1테이크 수도 float이전 반복의 결과 일 것이다 인수를. 같은 것 x = calc1(x, [](float arg){ return arg * 0.5f; });. 또한을 사용해야 calc1합니다 x. 그러나 이것은 아직 충분하지 않습니다. 부작용을 만들어야합니다. 예를 들어, 측정 후 x화면에 인쇄 합니다. 그럼에도 불구하고, 나는 timimg 측정을 위해 장난감 코드를 사용하는 것이 항상 실제 / 제작 코드로 일어날 일을 완벽하게 나타내는 것은 아닙니다.
Cassio Neri

벤치 마크가 루프 내부에 std :: function 객체를 구성하고 루프에서 calc2를 호출하는 것으로 보입니다. 컴파일러가 이것을 최적화하지 않을 수도 있고 (및 생성자가 vptr을 저장하는 것만 큼 간단 할 수도 있음) 관계없이 함수가 한 번 생성되고 호출하는 다른 함수에 전달되는 경우에 더 관심이 있습니다. 루프에서. 즉, 생성 시간이 아닌 호출 오버 헤드 (및 calc2가 아닌 'f'의 호출). 루프에서 (calc2에서) f를 한 번이 아니라 호출하는 것이 호이 스팅으로부터 이익을 얻는다면 또한 흥미로울 것입니다.
greggo

좋은 대답입니다. 두 가지 : std::reference_wrapper템플릿을 강제 사용하기위한 유효한 사용의 좋은 예입니다 . 일반 저장 공간이 아닙니다 .VS의 옵티마이 저가 빈 루프를 버리지 못하는 것을 보는 것은 재미 있습니다 ... 이 GCC 버그 re에서 알 수volatile 있습니다.
underscore_d

37

Clang을 사용하면 둘 사이에 성능 차이가 없습니다.

clang (3.2, 트렁크 166872) (Linux의 경우 -O2) 을 사용하면 두 경우의 이진이 실제로 동일합니다 .

포스트 끝에 클랜으로 돌아 올게요 그러나 먼저 gcc 4.7.2 :

이미 많은 통찰력이 있지만 calc1과 calc2의 계산 결과가 인라인 등으로 인해 동일하지 않다는 것을 지적하고 싶습니다. 예를 들어 모든 결과의 합계를 비교하십시오.

float result=0;
for (int i = 0; i < 1e8; ++i) {
  result+=calc2([](float arg){ return arg * 0.5f; });
}

되는 calc2로

1.71799e+10, time spent 0.14 sec

calc1을 사용하면

6.6435e+10, time spent 5.772 sec

이는 속도 차이가 ~ 40이고 값이 ~ 4입니다. 첫 번째는 OP가 게시 한 것 (Visual Studio 사용)보다 훨씬 큰 차이입니다. 실제로 최종 값을 인쇄하는 것은 컴파일러가 눈에 띄는 결과가없는 코드를 제거하지 못하게하는 것이 좋습니다 (있는 그대로). Cassio Neri는 이미 그의 대답에서 이것을 말했습니다. 결과의 차이에 유의하십시오-다른 계산을 수행하는 코드의 속도 계수를 비교할 때주의해야합니다.

또한 공정하게, f (3.3)를 반복적으로 계산하는 다양한 방법을 비교하는 것은 그리 흥미롭지 않을 것입니다. 입력이 일정하면 루프에 있지 않아야합니다. (옵티마이 저가 쉽게 알 수 있습니다)

사용자 제공 값 인수를 calc1과 2에 추가하면 calc1과 calc2 사이의 속도 계수가 40에서 5의 계수로 내려갑니다! Visual Studio를 사용하면 차이가 2 배에 가까우며, clang을 사용하면 차이가 없습니다 (아래 참조).

또한 곱셈이 빠르기 때문에 감속 요인에 대해 이야기하는 것은 종종 흥미롭지 않습니다. 더 흥미로운 질문은 함수가 얼마나 작고 실제 프로그램에서 병목 현상을 일으키는 것입니까?

그 소리:

Clang (3.2 사용) 은 예제 코드 (아래 게시)에 대해 calc1과 calc2 사이를 뒤집을 때 실제로 동일한 바이너리를 생성했습니다 . 질문에 게시 된 원래 예제를 사용하면 둘 다 동일하지만 시간이 전혀 걸리지 않습니다 (위에서 설명한 것처럼 루프는 완전히 제거됩니다). 내 수정 된 예에서 -O2로 :

실행할 시간 (초) :

clang:        calc1:           1.4 seconds
clang:        calc2:           1.4 seconds (identical binary)

gcc 4.7.2:    calc1:           1.1 seconds
gcc 4.7.2:    calc2:           6.0 seconds

VS2012 CTPNov calc1:           0.8 seconds 
VS2012 CTPNov calc2:           2.0 seconds 

VS2015 (14.0.23.107) calc1:    1.1 seconds 
VS2015 (14.0.23.107) calc2:    1.5 seconds 

MinGW (4.7.2) calc1:           0.9 seconds
MinGW (4.7.2) calc2:          20.5 seconds 

모든 바이너리의 계산 결과는 동일하며 모든 테스트는 동일한 머신에서 실행되었습니다. 클랜이나 VS 지식이 깊은 사람이 어떤 최적화가 수행되었는지에 대해 언급 할 수 있다면 흥미로울 것입니다.

수정 된 테스트 코드 :

#include <functional>
#include <chrono>
#include <iostream>

template <typename F>
float calc1(F f, float x) { 
  return 1.0f + 0.002*x+f(x*1.223) ; 
}

float calc2(std::function<float(float)> f,float x) { 
  return 1.0f + 0.002*x+f(x*1.223) ; 
}

int main() {
    using namespace std::chrono;

    const auto tp1 = high_resolution_clock::now();

    float result=0;
    for (int i = 0; i < 1e8; ++i) {
      result=calc1([](float arg){ 
          return arg * 0.5f; 
        },result);
    }
    const auto tp2 = high_resolution_clock::now();

    const auto d = duration_cast<milliseconds>(tp2 - tp1);  
    std::cout << d.count() << std::endl;
    std::cout << result<< std::endl;
    return 0;
}

최신 정보:

vs2015를 추가했습니다. 또한 calc1, calc2에 이중-> 부동 변환이 있음을 알았습니다. 그것들을 제거해도 Visual Studio의 결론은 바뀌지 않습니다 (둘 다 훨씬 빠르지 만 비율은 거의 같습니다).


8
벤치 마크가 잘못되었다는 것을 논할 수 있습니다. 흥미로운 사용 사례는 호출 코드가 다른 곳에서 함수 객체를 수신하는 것이므로 컴파일러는 호출을 컴파일 할 때 std :: function의 출처를 알지 못합니다. 여기서 컴파일러는 calc2를 인라인으로 확장하여 호출 할 때 std :: function의 구성을 정확하게 알고 있습니다. 9 월에 calc2를 'extern'으로 만들어 쉽게 고칠 수 있습니다. 소스 파일. 그런 다음 사과와 오렌지를 비교합니다. calc2가 calc1이 할 수없는 일을하고 있습니다. 그리고 루프는 calc 안에있을 수 있습니다 (많은 f 호출). 함수 객체의 ctor 주위가 아닙니다.
greggo

1
적절한 컴파일러를 얻을 수있을 때. (a) 실제 std :: function에 대한 ctor는 'new'를 호출합니다. (b) 목표가 실제 함수와 일치 할 때 호출 자체는 매우 희박합니다. (c) 바인딩의 경우, 함수 obj의 코드 ptr에 의해 선택된 적응을 수행하고 함수 obj에서 데이터 (바운드 파스)를 선택하는 코드 덩어리가 있습니다. (d) 'bound'함수는 컴파일러가 볼 수 있으면 해당 어댑터에 인라인됩니다.
greggo

설명 된 설정으로 새로운 답변이 추가되었습니다.
greggo

3
BTW 벤치 마크가 잘못되지 않았으므로 질문 ( "std :: function vs template")은 동일한 컴파일 단위의 범위에서만 유효합니다. 함수를 다른 단위로 이동하면 템플릿을 더 이상 사용할 수 없으므로 비교할 것이 없습니다.
rustyx

13

다른 것은 다릅니다.

템플릿이 할 수없는 일을하기 때문에 속도가 느립니다. 특히, 주어진 인수 유형으로 호출 할 수 있고 리턴 유형이 동일한 코드에서 주어진 리턴 유형으로 변환 가능한 함수 를 호출 수 있습니다 .

void eval(const std::function<int(int)>& f) {
    std::cout << f(3);
}

int f1(int i) {
    return i;
}

float f2(double d) {
    return d;
}

int main() {
    std::function<int(int)> fun(f1);
    eval(fun);
    fun = f2;
    eval(fun);
    return 0;
}

합니다 같은 기능 객체가 fun에 모두 호출에 전달되고있다 eval. 두 가지 기능이 있습니다.

그렇게 할 필요가 없으면를 사용 하지 않아야 합니다 std::function.


2
'fun = f2'가 끝나면 'fun'객체가 숨겨진 함수를 가리키고 int를 double로 변환하고 f2를 호출하며 double 결과를 int로 다시 변환한다는 것을 지적하고 싶습니다 (실제 예에서 , 'f2'는 해당 함수에 인라인 될 수 있습니다). std :: bind를 fun에 할당하면 'fun'객체가 바운드 매개 변수에 사용될 값을 포함 할 수 있습니다. 이러한 유연성을 지원하기 위해 'fun'(또는 init of)에 할당하면 메모리 할당 / 할당 해제가 필요할 수 있으며 실제 호출 오버 헤드보다 시간이 오래 걸릴 수 있습니다.
greggo

8

여기에 이미 좋은 대답이 있으므로, std :: function과 템플릿을 비교하는 것은 가상 함수를 함수와 비교하는 것과 같습니다. 가상 함수를 함수에 "선호"하지 말고 문제에 맞을 때 가상 함수를 사용하여 의사 결정을 컴파일 시간에서 런타임으로 이동하십시오. 아이디어는 (점프 테이블과 같은) 맞춤형 솔루션을 사용하여 문제를 해결하는 대신 컴파일러에게 더 나은 최적화 기회를 제공하는 것을 사용한다는 것입니다. 표준 솔루션을 사용하는 경우 다른 프로그래머에게도 도움이됩니다.


6

이 답변은 기존 답변 세트에 기여하여 std :: function 호출의 런타임 비용에 대한보다 의미있는 벤치 마크라고 생각합니다.

std :: function 메커니즘은 제공하는 항목에 대해 인식되어야합니다. 호출 가능한 엔티티는 적절한 서명의 std :: function으로 변환 될 수 있습니다. z = f (x, y)로 정의 된 함수에 표면을 맞추는 라이브러리가 있다고 가정하고 a를 승인 std::function<double(double,double)>하도록 라이브러리를 작성할 수 있으며 라이브러리 사용자는 호출 가능한 엔티티를 쉽게 변환 할 수 있습니다. 일반적인 함수, 클래스 인스턴스의 메소드 또는 람다 또는 std :: bind에서 지원하는 것입니다.

템플릿 접근 방식과 달리 이것은 다른 경우에 라이브러리 함수를 다시 컴파일하지 않고도 작동합니다. 따라서 추가 사례마다 컴파일 된 코드가 거의 필요하지 않습니다. 항상 이런 일이 가능해졌지만, 어색한 메커니즘이 필요했고, 라이브러리 사용자는 기능을 중심으로 어댑터를 구성해야 작동 할 수 있습니다. std :: function 은 모든 경우에 대해 공통 런타임 호출 인터페이스 를 얻는 데 필요한 모든 어댑터를 자동으로 구성 합니다. 이는 새롭고 매우 강력한 기능입니다.

내 생각에 이것은 성능과 관련하여 std :: function의 가장 중요한 사용 사례입니다. 한 번 구성된 후 std :: function을 여러 번 호출하는 비용에 관심이 있으며 컴파일러가 실제로 호출되는 함수를 알고 호출을 최적화 할 수없는 상황이됩니다 (즉, 적절한 벤치 마크를 얻으려면 다른 소스 파일에서 구현을 숨겨야합니다).

OP와 비슷한 아래 테스트를 수행했습니다. 그러나 주요 변경 사항은 다음과 같습니다.

  1. 각 사례는 10 억 회 반복되지만 std :: function 객체는 한 번만 생성됩니다. 실제 std :: function 호출을 구성 할 때 'operator new'가 호출되는 출력 코드를 살펴 보았습니다 (최적화되지 않을 수도 있음).
  2. 테스트는 원하지 않는 최적화를 방지하기 위해 두 개의 파일로 분할됩니다
  3. 내 경우는 다음과 같습니다. (a) 함수가 인라인 됨 (b) 함수가 일반 함수 포인터에 의해 전달됨 (c) 함수는 std :: function (d) 함수가 std :: std :: function으로 감싸 인 바인드

내가 얻는 결과는 다음과 같습니다

  • 사례 (a) (인라인) 1.3 nsec

  • 다른 모든 경우 : 3.3 nsec.

경우 (d)는 약간 느리지 만, 차이 (약 0.05nsec)는 잡음에 흡수됩니다.

결론은 std :: function이 실제 함수에 간단한 '바인드'적응이있는 경우에도 함수 포인터를 사용하는 것과 비슷한 호출 오버 헤드입니다. 인라인은 다른 것보다 2ns 빠르지 만 인라인이 런타임에 '하드 와이어'된 유일한 경우이기 때문에 예상되는 트레이드 오프입니다.

johan-lundberg의 코드를 동일한 컴퓨터에서 실행하면 루프 당 약 39 nsec가 표시되지만 std :: function의 실제 생성자 및 소멸자를 포함하여 루프에 훨씬 더 많이 있습니다. 새로운 것과 삭제되기 때문에.

-O2 gcc 4.8.1, x86_64 대상 (core i5).

코드는 두 개의 파일로 나누어 져 컴파일러가 호출되는 기능을 확장하지 못하도록합니다 (한 경우를 제외하고).

----- 첫 번째 소스 파일 --------------

#include <functional>


// simple funct
float func_half( float x ) { return x * 0.5; }

// func we can bind
float mul_by( float x, float scale ) { return x * scale; }

//
// func to call another func a zillion times.
//
float test_stdfunc( std::function<float(float)> const & func, int nloops ) {
    float x = 1.0;
    float y = 0.0;
    for(int i =0; i < nloops; i++ ){
        y += x;
        x = func(x);
    }
    return y;
}

// same thing with a function pointer
float test_funcptr( float (*func)(float), int nloops ) {
    float x = 1.0;
    float y = 0.0;
    for(int i =0; i < nloops; i++ ){
        y += x;
        x = func(x);
    }
    return y;
}

// same thing with inline function
float test_inline(  int nloops ) {
    float x = 1.0;
    float y = 0.0;
    for(int i =0; i < nloops; i++ ){
        y += x;
        x = func_half(x);
    }
    return y;
}

----- 두 번째 소스 파일 -------------

#include <iostream>
#include <functional>
#include <chrono>

extern float func_half( float x );
extern float mul_by( float x, float scale );
extern float test_inline(  int nloops );
extern float test_stdfunc( std::function<float(float)> const & func, int nloops );
extern float test_funcptr( float (*func)(float), int nloops );

int main() {
    using namespace std::chrono;


    for(int icase = 0; icase < 4; icase ++ ){
        const auto tp1 = system_clock::now();

        float result;
        switch( icase ){
         case 0:
            result = test_inline( 1e9);
            break;
         case 1:
            result = test_funcptr( func_half, 1e9);
            break;
         case 2:
            result = test_stdfunc( func_half, 1e9);
            break;
         case 3:
            result = test_stdfunc( std::bind( mul_by, std::placeholders::_1, 0.5), 1e9);
            break;
        }
        const auto tp2 = high_resolution_clock::now();

        const auto d = duration_cast<milliseconds>(tp2 - tp1);  
        std::cout << d.count() << std::endl;
        std::cout << result<< std::endl;
    }
    return 0;
}

관심있는 사람들을 위해 다음은 'mul_by'를 float (float)처럼 보이도록 컴파일러가 빌드 한 어댑터입니다. bind (mul_by, _1,0.5)로 작성된 함수가 호출 될 때 '호출'됩니다.

movq    (%rdi), %rax                ; get the std::func data
movsd   8(%rax), %xmm1              ; get the bound value (0.5)
movq    (%rax), %rdx                ; get the function to call (mul_by)
cvtpd2ps    %xmm1, %xmm1        ; convert 0.5 to 0.5f
jmp *%rdx                       ; jump to the func

(바인드에 0.5f를 쓰면 조금 더 빠를 수도 있습니다 ...) 'x'매개 변수는 % xmm0에 도착하고 그대로 유지됩니다.

다음은 test_stdfunc를 호출하기 전에 함수가 구성된 영역의 코드입니다. c ++ filt를 통해 실행하십시오.

movl    $16, %edi
movq    $0, 32(%rsp)
call    operator new(unsigned long)      ; get 16 bytes for std::function
movsd   .LC0(%rip), %xmm1                ; get 0.5
leaq    16(%rsp), %rdi                   ; (1st parm to test_stdfunc) 
movq    mul_by(float, float), (%rax)     ; store &mul_by  in std::function
movl    $1000000000, %esi                ; (2nd parm to test_stdfunc)
movsd   %xmm1, 8(%rax)                   ; store 0.5 in std::function
movq    %rax, 16(%rsp)                   ; save ptr to allocated mem

   ;; the next two ops store pointers to generated code related to the std::function.
   ;; the first one points to the adaptor I showed above.

movq    std::_Function_handler<float (float), std::_Bind<float (*(std::_Placeholder<1>, double))(float, float)> >::_M_invoke(std::_Any_data const&, float), 40(%rsp)
movq    std::_Function_base::_Base_manager<std::_Bind<float (*(std::_Placeholder<1>, double))(float, float)> >::_M_manager(std::_Any_data&, std::_Any_data const&, std::_Manager_operation), 32(%rsp)


call    test_stdfunc(std::function<float (float)> const&, int)

1
clang 3.4.1 x64의 결과는 (a) 1.0, (b) 0.95, (c) 2.0, (d) 5.0입니다.
rustyx

4

나는 당신의 결과가 매우 흥미로워 서 무슨 일이 일어나고 있는지 이해하기 위해 조금 파고 들었습니다. 첫째로 많은 다른 사람들이 계산 결과를 얻지 못하면서 컴파일러가 프로그램 상태를 최적화 할 것이라고 말했습니다. 두 번째로 콜백에 대한 무장으로 상수 3.3을 부여하면 다른 최적화가 진행될 것으로 생각됩니다. 이를 염두에두고 벤치 마크 코드를 약간 변경했습니다.

template <typename F>
float calc1(F f, float i) { return -1.0f * f(i) + 666.0f; }
float calc2(std::function<float(float)> f, float i) { return -1.0f * f(i) + 666.0f; }
int main() {
    const auto tp1 = system_clock::now();
    for (int i = 0; i < 1e8; ++i) {
        t += calc2([&](float arg){ return arg * 0.5f + t; }, i);
    }
    const auto tp2 = high_resolution_clock::now();
}

이 변경 사항을 gcc 4.8 -O3으로 컴파일하고 calc1의 경우 330ms, calc2의 경우 2702를 얻었습니다. 따라서 템플릿을 사용하는 것이 8 배 빨랐습니다.이 숫자는 나에게 의심 스러웠습니다 .8의 거듭 제곱 속도는 종종 컴파일러가 무언가를 벡터화했음을 나타냅니다. 템플릿 버전에 대해 생성 된 코드를 보면 명확하게 벡터화되었습니다.

.L34:
cvtsi2ss        %edx, %xmm0
addl    $1, %edx
movaps  %xmm3, %xmm5
mulss   %xmm4, %xmm0
addss   %xmm1, %xmm0
subss   %xmm0, %xmm5
movaps  %xmm5, %xmm0
addss   %xmm1, %xmm0
cvtsi2sd        %edx, %xmm1
ucomisd %xmm1, %xmm2
ja      .L37
movss   %xmm0, 16(%rsp)

std :: function 버전이 아닌 곳. 템플릿을 사용하면 컴파일러가 루프 전체에서 함수가 절대 변경되지 않지만 std :: function이 전달되면 변경 될 수 있으므로 벡터화 할 수 없다는 것을 알고 있기 때문에 이것은 나에게 의미가 있습니다.

이것은 컴파일러가 std :: function 버전에서 동일한 최적화를 수행 할 수 있는지 확인하기 위해 다른 것을 시도하게했습니다. 함수를 전달하는 대신 std :: function을 전역 var로 만들고 이것을 호출했습니다.

float calc3(float i) {  return -1.0f * f2(i) + 666.0f; }
std::function<float(float)> f2 = [](float arg){ return arg * 0.5f; };

int main() {
    const auto tp1 = system_clock::now();
    for (int i = 0; i < 1e8; ++i) {
        t += calc3([&](float arg){ return arg * 0.5f + t; }, i);
    }
    const auto tp2 = high_resolution_clock::now();
}

이 버전에서는 컴파일러가 이제 코드를 같은 방식으로 벡터화했으며 동일한 벤치 마크 결과를 얻었습니다.

  • 템플릿 : 330ms
  • std :: function : 2702ms
  • 글로벌 표준 :: 기능 : 330ms

내 결론은 std :: function의 원시 속도와 템플릿 functor의 속도가 거의 동일하다는 것입니다. 그러나 최적화 프로그램의 작업을 훨씬 더 어렵게 만듭니다.


1
요점은 펑터를 매개 변수로 전달하는 것입니다. 귀하의 calc3경우는 말이되지 않습니다. calc3은 이제 f2를 호출하도록 하드 코딩되었습니다. 물론 최적화 할 수 있습니다.
rustyx

실제로, 이것은 내가 보여 주려고했던 것입니다. 그 calc3는 템플릿과 동일하며, 그 상황에서 템플릿과 같은 컴파일 타임 구성입니다.
Joshua Ritterman
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.