Programing

표준 :: 기능 대 템플릿

lottogame 2020. 6. 7. 00:41
반응형

표준 :: 기능 대 템플릿


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 입니다.


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

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

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

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

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

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

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

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


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에 대한 지원은 이미이 문제에 충분했습니다.


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의 결론은 바뀌지 않습니다 (둘 다 훨씬 빠르지 만 비율은 거의 같습니다).


Different isn't the same.

It's slower because it does things that a template can't do. In particular, it lets you call any function that can be called with the given argument types and whose return type is convertible to the given return type from the same code.

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

Note that the same function object, fun, is being passed to both calls to eval. It holds two different functions.

If you don't need to do that, then you should not use std::function.


You already have some good answers here, so I'm not going to contradict them, in short comparing std::function to templates is like comparing virtual functions to functions. You never should "prefer" virtual functions to functions, but rather you use virtual functions when it fits the problem, moving decisions from compile time to run time. The idea is that rather than having to solve the problem using a bespoke solution (like a jump-table) you use something that gives the compiler a better chance of optimizing for you. It also helps other programmers, if you use a standard solution.


This answer is intended to contribute, to the set of existing answers, what I believe to be a more meaningful benchmark for the runtime cost of std::function calls.

The std::function mechanism should be recognized for what it provides: Any callable entity can be converted to a std::function of appropriate signature. Suppose you have a library that fits a surface to a function defined by z = f(x,y), you can write it to accept a std::function<double(double,double)>, and the user of the library can easily convert any callable entity to that; be it an ordinary function, a method of a class instance, or a lambda, or anything that is supported by std::bind.

Unlike template approaches, this works without having to recompile the library function for different cases; accordingly, little extra compiled code is needed for each additional case. It has always been possible to make this happen, but it used to require some awkward mechanisms, and the user of the library would likely need to construct an adapter around their function to make it work. std::function automatically constructs whatever adapter is needed to get a common runtime call interface for all the cases, which is a new and very powerful feature.

To my view, this is the most important use case for std::function as far as performance is concerned: I'm interested in the cost of calling a std::function many times after it has been constructed once, and it needs to be a situation where the compiler is unable to optimize the call by knowing the function actually being called (i.e. you need to hide the implementation in another source file to get a proper benchmark).

I made the test below, similar to the OP's; but the main changes are:

  1. Each case loops 1 billion times, but the std::function objects are constructed only once. I've found by looking at the output code that 'operator new' is called when constructing actual std::function calls (maybe not when they are optimized out).
  2. Test is split into two files to prevent undesired optimization
  3. My cases are: (a) function is inlined (b) function is passed by an ordinary function pointer (c) function is a compatible function wrapped as std::function (d) function is an incompatible function made compatible with a std::bind, wrapped as std::function

The results I get are:

  • case (a) (inline) 1.3 nsec

  • all other cases: 3.3 nsec.

Case (d) tends to be slightly slower, but the difference (about 0.05 nsec) is absorbed in the noise.

Conclusion is that the std::function is comparable overhead (at call time) to using a function pointer, even when there's simple 'bind' adaptation to the actual function. The inline is 2 ns faster than the others but that's an expected tradeoff since the inline is the only case which is 'hard-wired' at run time.

When I run johan-lundberg's code on the same machine, I'm seeing about 39 nsec per loop, but there's a lot more in the loop there, including the actual constructor and destructor of the std::function, which is probably fairly high since it involves a new and delete.

-O2 gcc 4.8.1, to x86_64 target (core i5).

Note, the code is broken up into two files, to prevent the compiler from expanding the functions where they are called (except in the one case where it's intended to).

----- first source file --------------

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

----- second source file -------------

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

For those interested, here's the adaptor the compiler built to make 'mul_by' look like a float(float) - this is 'called' when the function created as bind(mul_by,_1,0.5) is called:

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

(so it might have been a bit faster if I'd written 0.5f in the bind...) Note that the 'x' parameter arrives in %xmm0 and just stays there.

Here's the code in the area where the function is constructed, prior to calling test_stdfunc - run through 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)

I found your results very interesting so I did a bit of digging to understand what is going on. First off as many others have said with out having the results of the computation effect the state of the program the compiler will just optimize this away. Secondly having a constant 3.3 given as an armament to the callback I suspect that there will be other optimizations going on. With that in mind I changed your benchmark code a little bit.

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

Given this change to the code I compiled with gcc 4.8 -O3 and got a time of 330ms for calc1 and 2702 for calc2. So using the template was 8 times faster, this number looked suspects to me, speed of a power of 8 often indicates that the compiler has vectorized something. when I looked at the generated code for the templates version it was clearly vectoreized

.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)

Where as the std::function version was not. This makes sense to me, since with the template the compiler knows for sure that the function will never change throughout the loop but with the std::function being passed in it could change, therefor can not be vectorized.

This led me to try something else to see if I could get the compiler to perform the same optimization on the std::function version. Instead of passing in a function I make a std::function as a global var, and have this called.

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

With this version we see that the compiler has now vectorized the code in the same way and I get the same benchmark results.

  • template : 330ms
  • std::function : 2702ms
  • global std::function: 330ms

So my conclusion is the raw speed of a std::function vs a template functor is pretty much the same. However it makes the job of the optimizer much more difficult.

참고URL : https://stackoverflow.com/questions/14677997/stdfunction-vs-template

반응형