더 효율적인 것은 무엇입니까? pow를 사용하여 제곱하거나 그 자체로 곱합니까?


119

이 두 가지 방법 중 C에서 더 효율적인 방법은 무엇입니까? 그리고 어떨까요 :

pow(x,3)

x*x*x // etc?

9
x정수 또는 부동 소수점은?
Matthew Flaschen

6
위의 두 가지 작업을 수행하는 프로그램을 작성하고 프로파일 링 라이브러리를 사용하여 실행하는 데 걸리는 시간을 확인할 수 있습니다. 그러면 실행 시간 측면에서 좋은 대답을 얻을 수 있습니다.
J. Polfer

3
효율적이라고 말할 때 시간 또는 공간 (즉, 메모리 사용량)을 의미합니까?
J. Polfer

4
@sheepsimulator : +1은 빠른 테스트를 작성하면 잠재적으로 모호하거나 잘못된 답변을받는 것보다 더 빨리 확실한 답변을 제공한다는 점을 (다시) 지적하는 데 필요한 시간을 절약 해줍니다.
내 올바른 의견입니다.

5
@kirill_igum 버그가 아닌 부동 소수점 값인 경우 부동 소수점 산술은 연관성이 없습니다.
effeffe 2013-10-14

답변:


82

이 코드를 사용하여 x*x*...pow(x,i)소형 의 성능 차이를 테스트했습니다 i.

#include <cstdlib>
#include <cmath>
#include <boost/date_time/posix_time/posix_time.hpp>

inline boost::posix_time::ptime now()
{
    return boost::posix_time::microsec_clock::local_time();
}

#define TEST(num, expression) \
double test##num(double b, long loops) \
{ \
    double x = 0.0; \
\
    boost::posix_time::ptime startTime = now(); \
    for (long i=0; i<loops; ++i) \
    { \
        x += expression; \
        x += expression; \
        x += expression; \
        x += expression; \
        x += expression; \
        x += expression; \
        x += expression; \
        x += expression; \
        x += expression; \
        x += expression; \
    } \
    boost::posix_time::time_duration elapsed = now() - startTime; \
\
    std::cout << elapsed << " "; \
\
    return x; \
}

TEST(1, b)
TEST(2, b*b)
TEST(3, b*b*b)
TEST(4, b*b*b*b)
TEST(5, b*b*b*b*b)

template <int exponent>
double testpow(double base, long loops)
{
    double x = 0.0;

    boost::posix_time::ptime startTime = now();
    for (long i=0; i<loops; ++i)
    {
        x += std::pow(base, exponent);
        x += std::pow(base, exponent);
        x += std::pow(base, exponent);
        x += std::pow(base, exponent);
        x += std::pow(base, exponent);
        x += std::pow(base, exponent);
        x += std::pow(base, exponent);
        x += std::pow(base, exponent);
        x += std::pow(base, exponent);
        x += std::pow(base, exponent);
    }
    boost::posix_time::time_duration elapsed = now() - startTime;

    std::cout << elapsed << " ";

    return x;
}

int main()
{
    using std::cout;
    long loops = 100000000l;
    double x = 0.0;
    cout << "1 ";
    x += testpow<1>(rand(), loops);
    x += test1(rand(), loops);

    cout << "\n2 ";
    x += testpow<2>(rand(), loops);
    x += test2(rand(), loops);

    cout << "\n3 ";
    x += testpow<3>(rand(), loops);
    x += test3(rand(), loops);

    cout << "\n4 ";
    x += testpow<4>(rand(), loops);
    x += test4(rand(), loops);

    cout << "\n5 ";
    x += testpow<5>(rand(), loops);
    x += test5(rand(), loops);
    cout << "\n" << x << "\n";
}

결과는 다음과 같습니다.

1 00:00:01.126008 00:00:01.128338 
2 00:00:01.125832 00:00:01.127227 
3 00:00:01.125563 00:00:01.126590 
4 00:00:01.126289 00:00:01.126086 
5 00:00:01.126570 00:00:01.125930 
2.45829e+54

컴파일러가 최적화하지 않도록 모든 pow 계산의 결과를 누적합니다.

std::pow(double, double)버전 및을 사용하면 loops = 1000000l다음을 얻습니다.

1 00:00:00.011339 00:00:00.011262 
2 00:00:00.011259 00:00:00.011254 
3 00:00:00.975658 00:00:00.011254 
4 00:00:00.976427 00:00:00.011254 
5 00:00:00.973029 00:00:00.011254 
2.45829e+52

이것은 Ubuntu 9.10 64 비트를 실행하는 Intel Core Duo에 있습니다. -o2 최적화와 함께 gcc 4.4.1을 사용하여 컴파일되었습니다.

따라서 C에서는 과부하 가 없기 때문에 yes가보다 x*x*x빠릅니다 . C ++에서는 거의 동일합니다. (내 테스트의 방법론이 정확하다고 가정합니다.)pow(x, 3)pow(double, int)


이것은 An Markm의 의견에 대한 응답입니다.

경우에도 using namespace std지침이 발행 된 두 번째 매개 변수가있는 경우, pow입니다 int, 다음 std::pow(double, int)에서 과부하가 <cmath>대신 호출됩니다 ::pow(double, double)에서 <math.h>.

이 테스트 코드는 다음과 같은 동작을 확인합니다.

#include <iostream>

namespace foo
{

    double bar(double x, int i)
    {
        std::cout << "foo::bar\n";
        return x*i;
    }


}

double bar(double x, double y)
{
    std::cout << "::bar\n";
    return x*y;
}

using namespace foo;

int main()
{
    double a = bar(1.2, 3); // Prints "foo::bar"
    std::cout << a << "\n";
    return 0;
}

1
이것은 "using namespace std"를 삽입하면 C 옵션을 선택하고 이것은 런타임에 해로울 수 있다는 것을 의미합니까?
Andreas

두 타이밍 루프에서 pow 계산은 아마도 한 번만 발생합니다. gcc -O2는 루프 불변 표현식을 루프 밖으로 끌어내는 데 문제가 없어야합니다. 따라서 컴파일러가 상수 추가 루프를 곱셈으로 전환하거나 상수 추가 루프를 최적화하는 데 얼마나 잘하는지 테스트하는 것입니다. 기록 된 버전의 경우에도 루프가 지수 = 1 대 지수 = 5와 동일한 속도 인 이유가 있습니다.
Peter Cordes

2
나는 godbolt에서 그것을 시도했습니다 (godbolt에는 Boost가 설치되어 있지 않기 때문에 타이밍이 주석으로 표시됨). 를 사용하지 않는 한 놀랍게도 실제로 std::pow8 * loops 시간 (지수> 2의 경우)을 호출 합니다 -fno-math-errno. 그런 다음 내가 생각했던 것처럼 루프에서 pow call을 당길 수 있습니다. 나는 errno를 글로벌, 스레드 안전이기 때문에 추측은 가능성이 errno를 여러 번 ... 특급 = 1을 설정하고 펑 호출이와 단지 루프 밖으로 게양되어 있기 때문에 특급 = 2 빠르다에이 펑를 호출해야합니다 -O3(.. 와 - ffast - 수학 , 너무, 합계-의-8 루프 외부 않습니다).
피터 코르

나는 내가 사용하고있는 godbolt 세션에서 내가 -ffast-math를 가지고 있다는 것을 깨닫기 전에 반대표를 던졌다. 그것 없이도 testpow <1>과 testpow <2>는 pow루프 밖으로 끌어 올려 진 호출 과 인라인되기 때문에 깨져서 거기에 큰 결함이 있습니다. 또한 모든 테스트가 동일한 시간에 실행되기 때문에 대부분 FP 추가 대기 시간을 테스트하는 것처럼 보입니다. test5보다 느릴 것으로 예상 test1되지만 그렇지 않습니다. 여러 누산기를 사용하면 종속성 체인이 분할되고 대기 시간이 숨겨집니다.
Peter Cordes 2015 년

@PeterCordes, 5 년 전에 어디에 계 셨나요? :-) pow끊임없이 변화하는 값 을 적용 하여 벤치 마크를 수정 해 보겠습니다 (반복되는 pow 표현이 튀어 나오지 않도록 방지).
Emile Cormier 2015 년

30

그것은 잘못된 종류의 질문입니다. 올바른 질문은 "내 코드를 읽는 사람이 이해하기 더 쉬운 것은 무엇입니까?"입니다.

속도가 중요하다면 (나중에) 묻지 말고 측정하십시오. (그리고 그 전에 이것을 최적화하는 것이 실제로 눈에 띄는 차이를 만드는지 측정하십시오.) 그때까지는 가장 읽기 쉽도록 코드를 작성하십시오.


이를 명확히하기 위해 편집하십시오 (이미 그래야 했음에도 불구하고) : 획기적인 속도 향상은 일반적으로 더 나은 알고리즘 사용 , 데이터 지역성 개선 , 동적 메모리 사용 감소 , 사전 계산 결과 등과 같은 것에서 비롯됩니다. 단일 함수 호출을 마이크로 최적화 하고 수행하는 경우 매우 적은 수의 장소 에서 수행합니다. 이는 신중 하고 시간이 많이 걸리는 프로파일 링을 통해서만 찾을수 있습니다. 매우 직관적이지 않은 작업을 수행하면 속도를 높일 수 있습니다. 물건 (삽입과 같은noop 한 플랫폼에 대한 최적화가 다른 플랫폼에 대한 비관적 일 때도 있습니다 (이는 환경을 완전히 알지 못하기 때문에 질문하는 대신 측정해야하는 이유입니다).

다시 한 번 강조하겠습니다. 그런 것들이 중요한 몇 안되는 애플리케이션에서도 사용되는 대부분의 장소에서는 중요하지 않으며 코드를 보면 중요한 위치를 찾을 가능성 거의 없습니다. 코드를 최적화 하는 것은 시간 낭비 일 뿐이므로 먼저 핫스팟식별 해야합니다 .

단일 작업 (예 : 일부 값의 제곱 계산)이 애플리케이션 실행 시간의 10 %를 차지 하더라도 (IME는 매우 드뭅니다), 최적화하더라도 해당 작업에 필요한 시간의 50 %를 절약 합니다 (IME가 훨씬 더 드물지만) 애플리케이션에 소요 되는 시간이 5 % 줄었습니다 .
사용자는이를 알아 채기 위해 스톱워치가 필요합니다. (난 아무것도에서 20 %의 속도 향상은 대부분의 사용자에게 주목 간다 대부분의 경우 같아요. 그리고 그것은 당신이 찾아야 사 개 등의 명소입니다.)


43
올바른 질문 일 수 있습니다. 아마도 그는 자신의 실제 프로젝트에 대해 생각하고 있지 않지만 언어 / 컴파일러의 작동 방식에만 관심이있을 것입니다.
Andreas Rejbrand

137
Stackoverflow에는 표준 고지 사항을 삽입하는 버튼이 있어야합니다. "조기 최적화가 악하다는 것을 이미 알고 있지만이 최적화 질문을 학술 목적으로 요청하고 있거나 코드 줄 / 블록을 병목 현상으로 이미 식별했습니다."
Emile Cormier

39
나는 여기서 가독성이 문제라고 생각하지 않습니다. x * x와 pow (x, 2)를 작성하는 것은 매우 명확 해 보입니다.
KillianDS

41
볼드체와 이탤릭체의 과도한 사용은 눈에 쉽지 않습니다.
stagas

24
이 답변에 전적으로 동의하지 않습니다. 성능에 대해 물어 보는 것은 유효한 질문입니다. 얻을 수있는 최상의 성능은 때때로 유효한 요구 사항이며 종종 다른 언어가 아닌 C ++를 사용하는 이유입니다. 측정이 항상 좋은 생각은 아닙니다. 나는 항목의 수가 매우 중요하다는 것을 알 수있는 배경이 없었기 때문에 나중에 내 1,000,000 개의 항목으로 매우 나쁜 선택 이었음을 알 수있는 배경이 없었기 때문에 내 10 개 항목으로 버블 정렬과 퀵 정렬을 측정하고 더 빨리 버블 정렬을 찾을 수 있습니다.
jcoder

17

x*xx*x*x보다 더 빨리 될 것입니다 pow때문에, pow반면, 일반적인 경우와 반드시 계약 x*x에만 적용됩니다. 또한 함수 호출 등을 제거 할 수 있습니다.

그러나 이와 같은 마이크로 최적화를 발견하면 프로파일 러를 확보하고 심각한 프로파일 링을 수행해야합니다. 압도적 인 확률은 둘 사이의 차이점을 결코 눈치 채지 못할 것입니다.


7
테스트하기 전까지는 똑같은 생각을하고있었습니다. 방금 timed 루프에서 x*x*x이중 테스트를 std::pow(double base, int exponent)했는데 통계적으로 의미있는 성능 차이를 볼 수 없습니다.
Emile Cormier

2
컴파일러에 의해 최적화되지 않았는지 확인하십시오.
Ponkadoodle

1
@Emile : 컴파일러에서 생성 한 코드를 확인합니다. 옵티마이 저는 가끔 까다 롭고 분명하지 않은 일을합니다. 또한 다양한 최적화 수준 (예 : -O0, -O1, -O2 및 -O3)에서 성능을 확인하십시오.
내 올바른 의견입니다.

2
일반화 된 함수가 더 느리다고 가정 할 수 없습니다. 컴파일러가 최적화하기 더 간단한 코드가 더 쉽기 때문에 때로는 그 반대가 사실입니다.
cambunctious

5

나는 또한 성능 문제에 대해 궁금해했으며 @EmileCormier의 답변을 기반으로 컴파일러에서 이것이 최적화되기를 희망했습니다. 그러나 그가 보여준 테스트 코드가 컴파일러가 std :: pow () 호출을 최적화 할 수있을 것이라고 걱정했습니다. 매번 호출에 동일한 값이 사용되어 컴파일러가 결과를 저장할 수 있고 루프에서 재사용-이것은 모든 경우에 대해 거의 동일한 런타임을 설명합니다. 그래서 나도 조사했습니다.

다음은 내가 사용한 코드 (test_pow.cpp)입니다.

#include <iostream>                                                                                                                                                                                                                       
#include <cmath>
#include <chrono>

class Timer {
  public:
    explicit Timer () : from (std::chrono::high_resolution_clock::now()) { }

    void start () {
      from = std::chrono::high_resolution_clock::now();
    }

    double elapsed() const {
      return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() - from).count() * 1.0e-6;
    }

  private:
    std::chrono::high_resolution_clock::time_point from;
};

int main (int argc, char* argv[])
{
  double total;
  Timer timer;



  total = 0.0;
  timer.start();
  for (double i = 0.0; i < 1.0; i += 1e-8)
    total += std::pow (i,2);
  std::cout << "std::pow(i,2): " << timer.elapsed() << "s (result = " << total << ")\n";

  total = 0.0;
  timer.start();
  for (double i = 0.0; i < 1.0; i += 1e-8)
    total += i*i;
  std::cout << "i*i: " << timer.elapsed() << "s (result = " << total << ")\n";

  std::cout << "\n";

  total = 0.0;
  timer.start();
  for (double i = 0.0; i < 1.0; i += 1e-8)
    total += std::pow (i,3);
  std::cout << "std::pow(i,3): " << timer.elapsed() << "s (result = " << total << ")\n";

  total = 0.0;
  timer.start();
  for (double i = 0.0; i < 1.0; i += 1e-8)
    total += i*i*i;
  std::cout << "i*i*i: " << timer.elapsed() << "s (result = " << total << ")\n";


  return 0;
}

이것은 다음을 사용하여 컴파일되었습니다.

g++ -std=c++11 [-O2] test_pow.cpp -o test_pow

기본적으로 차이점은 std :: pow ()에 대한 인수가 루프 카운터라는 것입니다. 내가 두려워했던 것처럼 성능의 차이가 뚜렷합니다. -O2 플래그가 없으면 시스템 (Arch Linux 64 비트, g ++ 4.9.1, Intel i7-4930)의 결과는 다음과 같습니다.

std::pow(i,2): 0.001105s (result = 3.33333e+07)
i*i: 0.000352s (result = 3.33333e+07)

std::pow(i,3): 0.006034s (result = 2.5e+07)
i*i*i: 0.000328s (result = 2.5e+07)

최적화를 통해 결과는 똑같이 놀랍습니다.

std::pow(i,2): 0.000155s (result = 3.33333e+07)
i*i: 0.000106s (result = 3.33333e+07)

std::pow(i,3): 0.006066s (result = 2.5e+07)
i*i*i: 9.7e-05s (result = 2.5e+07)

따라서 컴파일러가 최소한 std :: pow (x, 2) 케이스를 최적화하려고 시도하지만 std :: pow (x, 3) 케이스는 그렇지 않은 것 같습니다 (std :: pow보다 40 배 더 오래 걸립니다). (x, 2) 케이스). 모든 경우에 수동 확장이 더 나은 성능을 보였지만 특히 파워 3 케이스의 경우 (60 배 더 빠름) 성능이 좋았습니다. 타이트한 루프에서 2보다 큰 정수 거듭 제곱으로 std :: pow ()를 실행하는 경우 명심할 가치가 있습니다.


4

가장 효율적인 방법은 곱셈의 기하 급수적 인 증가를 고려하는 것입니다. 이 코드에서 p ^ q를 확인하십시오.

template <typename T>
T expt(T p, unsigned q){
    T r =1;
    while (q != 0) {
        if (q % 2 == 1) {    // if q is odd
            r *= p;
            q--;
        }
        p *= p;
        q /= 2;
    }
    return r;
}

2

지수가 일정하고 작 으면 확장하여 곱셈 수를 최소화합니다. (예를 들어, x^4최적 아니다 x*x*x*x하지만 y*y여기서 y=x*x그리고. x^5y*y*xy=x*x등.). 일정한 정수 지수를 들어, 단지 이미 최적화 된 양식을 작성; 지수가 작은 경우 이는 코드가 프로파일 링되었는지 여부에 관계없이 수행되어야하는 표준 최적화입니다. 최적화 된 양식은 기본적으로 항상 수행 할 가치가있는 많은 경우에서 더 빠릅니다.

(Visual C ++를 사용하는 경우 std::pow(float,int)내가 언급 한 최적화를 수행하여 연산 순서가 지수의 비트 패턴과 관련되어 있습니다.하지만 컴파일러가 루프를 풀어줄 것이라는 보장은 없지만 그래도 할 가치가 있습니다. 손으로.)

BTW pow는 프로파일 러 결과에 (un) 놀라운 경향이 있습니다. 절대적으로 필요하지 않고 (예 : 지수가 크거나 상수가 아님) 성능에 대해 전혀 걱정하지 않는 경우 최적의 코드를 작성하고 프로파일 러가이를 알려줄 때까지 기다리는 것이 가장 좋습니다 (놀랍게도 ) 더 생각하기 전에 시간 낭비. (대안은 전화를 걸어 pow프로파일 러가 (당연히) 시간을 낭비하고 있다고 알려주도록하는 것입니다. 지능적으로 수행하여이 단계를 단축하고 있습니다.)


0

나는 비슷한 문제로 바빴고 결과에 상당히 당황합니다. 나는 n 체 상황에서 뉴턴 중력에 대해 x⁻³ / ²를 계산했습니다 (거리 벡터 d에 위치한 질량 M의 다른 물체에서 발생하는 가속도) : a = M G d*(d²)⁻³/²(여기서 d²는 d 자체의 도트 (스칼라) 곱), 계산 M*G*pow(d2, -1.5)이 더 간단 하다고 생각했습니다.M*G/d2/sqrt(d2)

트릭은 작은 시스템의 경우에는 사실이지만 시스템의 크기 M*G/d2/sqrt(d2)가 커짐에 따라 효율성이 높아지고 다른 데이터에 대한 작업을 반복하는 것은 그렇지 않기 때문에 시스템의 크기가이 결과에 영향을 미치는 이유를 이해할 수 없습니다. 시스템이 성장함에 따라 가능한 최적화가있는 것처럼 보이지만pow

여기에 이미지 설명 입력

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