C ++에서 긴 방정식을 구현할 때 높은 수준의 접근 방식을 통해 성능을 향상시킬 수있는 방법


92

엔지니어링 시뮬레이션을 개발 중입니다. 여기에는 고무와 같은 재료의 응력을 계산하기 위해 다음 방정식과 같은 몇 가지 긴 방정식을 구현하는 것이 포함됩니다.

T = (
    mu * (
            pow(l1 * pow(l1 * l2 * l3, -0.1e1 / 0.3e1), a) * a
            * (
                pow(l1 * l2 * l3, -0.1e1 / 0.3e1)
                - l1 * l2 * l3 * pow(l1 * l2 * l3, -0.4e1 / 0.3e1) / 0.3e1
            ) * pow(l1 * l2 * l3, 0.1e1 / 0.3e1) / l1
            - pow(l2 * pow(l1 * l2 * l3, -0.1e1 / 0.3e1), a) * a / l1 / 0.3e1
            - pow(l3 * pow(l1 * l2 * l3, -0.1e1 / 0.3e1), a) * a / l1 / 0.3e1
        ) / a
    + K * (l1 * l2 * l3 - 0.1e1) * l2 * l3
) * N1 / l2 / l3

+ (
    mu * (
        - pow(l1 * pow(l1 * l2 * l3, -0.1e1 / 0.3e1), a) * a / l2 / 0.3e1
        + pow(l2 * pow(l1 * l2 * l3, -0.1e1 / 0.3e1), a) * a
        * (
            pow(l1 * l2 * l3, -0.1e1 / 0.3e1)
            - l1 * l2 * l3 * pow(l1 * l2 * l3, -0.4e1 / 0.3e1) / 0.3e1
        ) * pow(l1 * l2 * l3, 0.1e1 / 0.3e1) / l2
        - pow(l3 * pow(l1 * l2 * l3, -0.1e1 / 0.3e1), a) * a / l2 / 0.3e1
    ) / a
    + K * (l1 * l2 * l3 - 0.1e1) * l1 * l3
) * N2 / l1 / l3

+ (
    mu * (
        - pow(l1 * pow(l1 * l2 * l3, -0.1e1 / 0.3e1), a) * a / l3 / 0.3e1
        - pow(l2 * pow(l1 * l2 * l3, -0.1e1 / 0.3e1), a) * a / l3 / 0.3e1
        + pow(l3 * pow(l1 * l2 * l3, -0.1e1 / 0.3e1), a) * a
        * (
            pow(l1 * l2 * l3, -0.1e1 / 0.3e1)
            - l1 * l2 * l3 * pow(l1 * l2 * l3, -0.4e1 / 0.3e1) / 0.3e1
        ) * pow(l1 * l2 * l3, 0.1e1 / 0.3e1) / l3
    ) / a
+ K * (l1 * l2 * l3 - 0.1e1) * l1 * l2
) * N3 / l1 / l2;

저는 Maple을 사용하여 실수를 방지하고 지루한 대수로 시간을 절약하기 위해 C ++ 코드를 생성합니다. 이 코드는 수천 번 (수백만은 아니더라도) 실행되므로 성능이 문제가됩니다. 불행히도 수학은 지금까지 단순화 될뿐입니다. 긴 방정식은 피할 수 없습니다.

이 구현을 최적화하기 위해 어떤 접근 방식을 취할 수 있습니까? 위의 예에 대한 특정 최적화가 아니라 이러한 방정식을 구현할 때 적용해야하는 고급 전략을 찾고 있습니다.

나는 g ++를 사용하여 --enable-optimize=-O3.

최신 정보:

반복되는 표현이 많다는 것을 알고 있으며 컴파일러가이를 처리 할 것이라는 가정을 사용하고 있습니다. 지금까지 내 테스트에 따르면 그렇습니다.

l1, l2, l3, mu, a, K 모두 양의 실수 (0이 아님)입니다.

l1*l2*l3동등한 변수 로 대체 했습니다 : J. 이것은 성능 향상에 도움이되었습니다.

교체 pow(x, 0.1e1/0.3e1)로하는 것은 cbrt(x)좋은 제안했다.

이것은 CPU에서 실행될 것입니다. 가까운 장래에 GPU에서 더 잘 실행될 것 같지만 현재로서는 해당 옵션을 사용할 수 없습니다.


32
가장 먼저 떠오르는 것은 (컴파일러가 자체적으로 최적화하지 않는 한) 모든 pow(l1 * l2 * l3, -0.1e1 / 0.3e1)변수를 변수 로 대체하는 것 입니다 ...하지만 코드가 빠르게 실행되는지 느리게 실행되는지 확인하려면 코드를 벤치마킹해야합니다.
SingerOfTheFall

6
또한 코드를 더 읽기 쉽게 형식화하십시오. 개선 가능성을 식별하는 데 도움이 될 수 있습니다.
Ed Heal

26
왜 모든 반대표와 투표가 마감됩니까? 수치 적 또는 과학적 프로그래밍을 좋아하지 않는 분들은 다른 질문을보십시오. 이 사이트에 잘 맞는 좋은 질문입니다. scicomp 사이트는 아직 베타 버전입니다. 마이그레이션에는 좋은 옵션이 없습니다. 코드 리뷰 사이트는 충분한 시선을 얻지 못합니다. OP가 과학 컴퓨팅에서 자주 발생하는 일 : 기호 수학 프로그램에서 문제를 구성하고 프로그램에 코드를 생성하도록 요청하고 생성 된 코드가 너무 엉망이므로 결과를 건드리지 마십시오.
David Hammen

6
@DavidHammen 코드 리뷰 사이트는 충분한 시선을 얻지 못합니다 . 닭과 계란 문제처럼 들리며 CR이 더 이상 그러한 시선을 얻는 데 도움이되지 않는 사고 방식입니다. scicomp 베타 사이트 가 베타이기 때문에 거절하는 아이디어에도 동일하게 적용됩니다. 모든 사람이 그렇게 생각한다면 성장할 유일한 사이트는 Stack Overflow 일 것입니다.
Mathieu Guindon

13
이 질문은 여기
NathanOliver

답변:


88

요약 수정

  • 내 원래 대답은 코드에 많은 복제 계산이 포함되어 있고 많은 힘이 1/3의 요소와 관련이 있다는 점에 주목했습니다. 예를 들어 pow(x, 0.1e1/0.3e1)cbrt(x).
  • 두 번째 편집은 틀렸고 세 번째 편집은이 잘못을 외삽했습니다. 이것은 사람들이 문자 'M'으로 시작하는 상징적 수학 프로그램에서 오라클과 같은 결과를 변경하는 것을 두려워하게 만드는 것입니다. 나는 그 편집을 스트라이크 (즉, 스트라이크 ) 하고이 답변의 현재 개정판의 맨 아래로 밀어 넣었습니다. 그러나 나는 그것들을 삭제하지 않았다. 나는 인간이다. 우리는 실수하기 쉽습니다.
  • 나의 네 번째 편집 올바르게 문제의 복잡한 표현을 나타내는 매우 컴팩트 한 표정으로 개발 IF 매개 변수가 l1, l2그리고 l3긍정적 인 실제 숫자와 경우에 a비 제로 실수입니다. (이 계수의 특정 특성에 대해서는 아직 OP로부터 듣지 못했습니다. 문제의 특성을 감안할 때 이는 합리적인 가정입니다.)
  • 이 편집은 이러한 표현을 단순화하는 방법에 대한 일반적인 문제에 대한 답변을 시도합니다.

먼저 첫 번째 것들

실수를 피하기 위해 Maple을 사용하여 C ++ 코드를 생성합니다.

Maple과 Mathematica는 때때로 명백한 것을 놓칩니다. 더욱 중요한 것은 Maple 및 Mathematica 사용자가 때때로 실수를한다는 것입니다. "때때로"대신 "자주"또는 "거의 항상"으로 대체하는 것은 아마도 마크에 더 가깝습니다.

Maple이 문제의 매개 변수에 대해 말함으로써 해당 표현을 단순화하는 데 도움이 될 수 있습니다. 손의 예에서, 그 의심 l1, l2그리고 l3있습니다 긍정적 인 실수를하고는 a비 제로 실수입니다. 그럴 경우 그렇게 말하십시오. 이러한 상징적 수학 프로그램은 일반적으로 당면한 양이 복잡하다고 가정합니다. 도메인을 제한하면 프로그램이 복소수에서 유효하지 않은 가정을 할 수 있습니다.


기호 수학 프로그램에서 이러한 큰 혼란을 단순화하는 방법 (이 편집)

기호 수학 프로그램은 일반적으로 다양한 매개 변수에 대한 정보를 제공하는 기능을 제공합니다. 특히 문제가 나눗셈이나 지수화와 관련된 경우 그 능력을 사용하십시오. 손의 예에서, 당신은 단풍 나무가 그것을 말함으로써 그 표현을 단순화 도움이 수 l1, l2l3포지티브 실수를하고는 a비 제로 실수입니다. 그럴 경우 그렇게 말하십시오. 이러한 상징적 수학 프로그램은 일반적으로 당면한 양이 복잡하다고 가정합니다. 도메인을 제한하면 프로그램이 a x b x = (ab) x 와 같은 가정을 할 수 있습니다 . 이 경우에만입니다 ab긍정적 인 실수하고있는 경우는 x진짜입니다. 복소수에서는 유효하지 않습니다.

궁극적으로 이러한 상징적 수학 프로그램은 알고리즘을 따릅니다. 함께 도와주세요. 코드를 생성하기 전에 확장, 수집 및 단순화를 시도해보십시오. 이 경우, 당신은 그의 요소를 포함하는 용어 수집 한 수 mu와의 요소를 포함하는 사람들을 K. 표현을 "가장 단순한 형태"로 줄이는 것은 약간의 예술로 남아 있습니다.

생성 된 코드가 엉망이 될 때 만져서는 안된다는 사실로 받아들이지 마십시오. 직접 단순화하십시오. 기호 수학 프로그램이 코드를 생성하기 전에 무엇을 가지고 있는지 살펴보십시오. 내가 어떻게 당신의 표현을 훨씬 더 간단하고 훨씬 빠르게 줄 였는지, 그리고 Walter의 대답 이 어떻게 내 여러 단계를 더 발전 시켰 는지 보세요. 마법의 레시피는 없습니다. 마법의 레시피가 있다면 메이플은 그것을 적용하고 월터가 준 대답을했을 것입니다.


구체적인 질문에 대해

당신은 그 계산에서 많은 덧셈과 뺄셈을하고 있습니다. 서로 거의 취소되는 조건이 있으면 심각한 문제에 빠질 수 있습니다. 다른 용어보다 우세한 용어가 있으면 CPU를 많이 낭비하게됩니다.

다음으로 반복 계산을 수행하여 많은 CPU를 낭비하고 있습니다. -ffast-math컴파일러가 IEEE 부동 소수점 규칙 중 일부를 위반 하도록 허용 하는 을 활성화하지 않는 한 컴파일러는 해당 표현식을 단순화하지 않습니다 (실제로는 안 됨). 대신 당신이 말한대로 정확하게 할 것입니다. 최소한 l1 * l2 * l3그 혼란을 계산 하기 전에 계산해야합니다 .

마지막으로를 많이 호출하고 pow있는데 이는 매우 느립니다. 이러한 호출 중 일부는 (l1 * l2 * l3) (1/3) 형식 입니다. 에 대한 많은 호출 pow은 단일 호출로 수행 할 수 있습니다 std::cbrt.

l123 = l1 * l2 * l3;
l123_pow_1_3 = std::cbrt(l123);
l123_pow_4_3 = l123 * l123_pow_1_3;

이것으로

  • X * pow(l1 * l2 * l3, 0.1e1 / 0.3e1)됩니다 X * l123_pow_1_3.
  • X * pow(l1 * l2 * l3, -0.1e1 / 0.3e1)됩니다 X / l123_pow_1_3.
  • X * pow(l1 * l2 * l3, 0.4e1 / 0.3e1)됩니다 X * l123_pow_4_3.
  • X * pow(l1 * l2 * l3, -0.4e1 / 0.3e1)됩니다 X / l123_pow_4_3.


메이플은 명백한 것을 놓쳤습니다.
예를 들어 훨씬 더 쉽게 작성할 수있는 방법이 있습니다.

(pow(l1 * l2 * l3, -0.1e1 / 0.3e1) - l1 * l2 * l3 * pow(l1 * l2 * l3, -0.4e1 / 0.3e1) / 0.3e1)

그 가정 l1, l2그리고 l3실제 다소 복잡한 숫자보다, 그리고 실제 큐브 루트가 (오히려 원칙 복잡한 루트가 아닌) 추출 할 것을, 위에서 언급 한에 감소

2.0/(3.0 * pow(l1 * l2 * l3, 1.0/3.0))

또는

2.0/(3.0 * l123_pow_1_3)

cbrt_l123대신 사용 l123_pow_1_3하면 질문의 불쾌한 표현이 다음과 같이 감소합니다.

l123 = l1 * l2 * l3; 
cbrt_l123 = cbrt(l123);
T = 
  mu/(3.0*l123)*(  pow(l1/cbrt_l123,a)*(2.0*N1-N2-N3)
                 + pow(l2/cbrt_l123,a)*(2.0*N2-N3-N1)
                 + pow(l3/cbrt_l123,a)*(2.0*N3-N1-N2))
 +K*(l123-1.0)*(N1+N2+N3);

항상 두 번 확인하지만 항상 단순화하십시오.


위의 단계에 도달하는 몇 가지 단계는 다음과 같습니다.

// Step 0: Trim all whitespace.
T=(mu*(pow(l1*pow(l1*l2*l3,-0.1e1/0.3e1),a)*a*(pow(l1*l2*l3,-0.1e1/0.3e1)-l1*l2*l3*pow(l1*l2*l3,-0.4e1/0.3e1)/0.3e1)*pow(l1*l2*l3,0.1e1/0.3e1)/l1-pow(l2*pow(l1*l2*l3,-0.1e1/0.3e1),a)*a/l1/0.3e1-pow(l3*pow(l1*l2*l3,-0.1e1/0.3e1),a)*a/l1/0.3e1)/a+K*(l1*l2*l3-0.1e1)*l2*l3)*N1/l2/l3+(mu*(-pow(l1*pow(l1*l2*l3,-0.1e1/0.3e1),a)*a/l2/0.3e1+pow(l2*pow(l1*l2*l3,-0.1e1/0.3e1),a)*a*(pow(l1*l2*l3,-0.1e1/0.3e1)-l1*l2*l3*pow(l1*l2*l3,-0.4e1/0.3e1)/0.3e1)*pow(l1*l2*l3,0.1e1/0.3e1)/l2-pow(l3*pow(l1*l2*l3,-0.1e1/0.3e1),a)*a/l2/0.3e1)/a+K*(l1*l2*l3-0.1e1)*l1*l3)*N2/l1/l3+(mu*(-pow(l1*pow(l1*l2*l3,-0.1e1/0.3e1),a)*a/l3/0.3e1-pow(l2*pow(l1*l2*l3,-0.1e1/0.3e1),a)*a/l3/0.3e1+pow(l3*pow(l1*l2*l3,-0.1e1/0.3e1),a)*a*(pow(l1*l2*l3,-0.1e1/0.3e1)-l1*l2*l3*pow(l1*l2*l3,-0.4e1/0.3e1)/0.3e1)*pow(l1*l2*l3,0.1e1/0.3e1)/l3)/a+K*(l1*l2*l3-0.1e1)*l1*l2)*N3/l1/l2;

// Step 1:
//   l1*l2*l3 -> l123
//   0.1e1 -> 1.0
//   0.4e1 -> 4.0
//   0.3e1 -> 3
l123 = l1 * l2 * l3;
T=(mu*(pow(l1*pow(l123,-1.0/3),a)*a*(pow(l123,-1.0/3)-l123*pow(l123,-4.0/3)/3)*pow(l123,1.0/3)/l1-pow(l2*pow(l123,-1.0/3),a)*a/l1/3-pow(l3*pow(l123,-1.0/3),a)*a/l1/3)/a+K*(l123-1.0)*l2*l3)*N1/l2/l3+(mu*(-pow(l1*pow(l123,-1.0/3),a)*a/l2/3+pow(l2*pow(l123,-1.0/3),a)*a*(pow(l123,-1.0/3)-l123*pow(l123,-4.0/3)/3)*pow(l123,1.0/3)/l2-pow(l3*pow(l123,-1.0/3),a)*a/l2/3)/a+K*(l123-1.0)*l1*l3)*N2/l1/l3+(mu*(-pow(l1*pow(l123,-1.0/3),a)*a/l3/3-pow(l2*pow(l123,-1.0/3),a)*a/l3/3+pow(l3*pow(l123,-1.0/3),a)*a*(pow(l123,-1.0/3)-l123*pow(l123,-4.0/3)/3)*pow(l123,1.0/3)/l3)/a+K*(l123-1.0)*l1*l2)*N3/l1/l2;

// Step 2:
//   pow(l123,1.0/3) -> cbrt_l123
//   l123*pow(l123,-4.0/3) -> pow(l123,-1.0/3)
//   (pow(l123,-1.0/3)-pow(l123,-1.0/3)/3) -> 2.0/(3.0*cbrt_l123)
//   *pow(l123,-1.0/3) -> /cbrt_l123
l123 = l1 * l2 * l3;
cbrt_l123 = cbrt(l123);
T=(mu*(pow(l1/cbrt_l123,a)*a*2.0/(3.0*cbrt_l123)*cbrt_l123/l1-pow(l2/cbrt_l123,a)*a/l1/3-pow(l3/cbrt_l123,a)*a/l1/3)/a+K*(l123-1.0)*l2*l3)*N1/l2/l3+(mu*(-pow(l1/cbrt_l123,a)*a/l2/3+pow(l2/cbrt_l123,a)*a*2.0/(3.0*cbrt_l123)*cbrt_l123/l2-pow(l3/cbrt_l123,a)*a/l2/3)/a+K*(l123-1.0)*l1*l3)*N2/l1/l3+(mu*(-pow(l1/cbrt_l123,a)*a/l3/3-pow(l2/cbrt_l123,a)*a/l3/3+pow(l3/cbrt_l123,a)*a*2.0/(3.0*cbrt_l123)*cbrt_l123/l3)/a+K*(l123-1.0)*l1*l2)*N3/l1/l2;

// Step 3:
//   Whitespace is nice.
l123 = l1 * l2 * l3;
cbrt_l123 = cbrt(l123);
T =
  (mu*( pow(l1/cbrt_l123,a)*a*2.0/(3.0*cbrt_l123)*cbrt_l123/l1
       -pow(l2/cbrt_l123,a)*a/l1/3
       -pow(l3/cbrt_l123,a)*a/l1/3)/a
   +K*(l123-1.0)*l2*l3)*N1/l2/l3
 +(mu*(-pow(l1/cbrt_l123,a)*a/l2/3
       +pow(l2/cbrt_l123,a)*a*2.0/(3.0*cbrt_l123)*cbrt_l123/l2
       -pow(l3/cbrt_l123,a)*a/l2/3)/a
   +K*(l123-1.0)*l1*l3)*N2/l1/l3
 +(mu*(-pow(l1/cbrt_l123,a)*a/l3/3
       -pow(l2/cbrt_l123,a)*a/l3/3
       +pow(l3/cbrt_l123,a)*a*2.0/(3.0*cbrt_l123)*cbrt_l123/l3)/a
   +K*(l123-1.0)*l1*l2)*N3/l1/l2;

// Step 4:
//   Eliminate the 'a' in (term1*a + term2*a + term3*a)/a
//   Expand (mu_term + K_term)*something to mu_term*something + K_term*something
l123 = l1 * l2 * l3;
cbrt_l123 = cbrt(l123);
T =
  (mu*( pow(l1/cbrt_l123,a)*2.0/(3.0*cbrt_l123)*cbrt_l123/l1
       -pow(l2/cbrt_l123,a)/l1/3
       -pow(l3/cbrt_l123,a)/l1/3))*N1/l2/l3
 +K*(l123-1.0)*l2*l3*N1/l2/l3
 +(mu*(-pow(l1/cbrt_l123,a)/l2/3
       +pow(l2/cbrt_l123,a)*2.0/(3.0*cbrt_l123)*cbrt_l123/l2
       -pow(l3/cbrt_l123,a)/l2/3))*N2/l1/l3
 +K*(l123-1.0)*l1*l3*N2/l1/l3
 +(mu*(-pow(l1/cbrt_l123,a)/l3/3
       -pow(l2/cbrt_l123,a)/l3/3
       +pow(l3/cbrt_l123,a)*2.0/(3.0*cbrt_l123)*cbrt_l123/l3))*N3/l1/l2
 +K*(l123-1.0)*l1*l2*N3/l1/l2;

// Step 5:
//   Rearrange
//   Reduce l2*l3*N1/l2/l3 to N1 (and similar)
//   Reduce 2.0/(3.0*cbrt_l123)*cbrt_l123/l1 to 2.0/3.0/l1 (and similar)
l123 = l1 * l2 * l3;
cbrt_l123 = cbrt(l123);
T =
  (mu*( pow(l1/cbrt_l123,a)*2.0/3.0/l1
       -pow(l2/cbrt_l123,a)/l1/3
       -pow(l3/cbrt_l123,a)/l1/3))*N1/l2/l3
 +(mu*(-pow(l1/cbrt_l123,a)/l2/3
       +pow(l2/cbrt_l123,a)*2.0/3.0/l2
       -pow(l3/cbrt_l123,a)/l2/3))*N2/l1/l3
 +(mu*(-pow(l1/cbrt_l123,a)/l3/3
       -pow(l2/cbrt_l123,a)/l3/3
       +pow(l3/cbrt_l123,a)*2.0/3.0/l3))*N3/l1/l2
 +K*(l123-1.0)*N1
 +K*(l123-1.0)*N2
 +K*(l123-1.0)*N3;

// Step 6:
//   Factor out mu and K*(l123-1.0)
l123 = l1 * l2 * l3;
cbrt_l123 = cbrt(l123);
T =
  mu*(  ( pow(l1/cbrt_l123,a)*2.0/3.0/l1
         -pow(l2/cbrt_l123,a)/l1/3
         -pow(l3/cbrt_l123,a)/l1/3)*N1/l2/l3
      + (-pow(l1/cbrt_l123,a)/l2/3
         +pow(l2/cbrt_l123,a)*2.0/3.0/l2
         -pow(l3/cbrt_l123,a)/l2/3)*N2/l1/l3
      + (-pow(l1/cbrt_l123,a)/l3/3
         -pow(l2/cbrt_l123,a)/l3/3
         +pow(l3/cbrt_l123,a)*2.0/3.0/l3)*N3/l1/l2)
 +K*(l123-1.0)*(N1+N2+N3);

// Step 7:
//   Expand
l123 = l1 * l2 * l3;
cbrt_l123 = cbrt(l123);
T =
  mu*( pow(l1/cbrt_l123,a)*2.0/3.0/l1*N1/l2/l3
      -pow(l2/cbrt_l123,a)/l1/3*N1/l2/l3
      -pow(l3/cbrt_l123,a)/l1/3*N1/l2/l3
      -pow(l1/cbrt_l123,a)/l2/3*N2/l1/l3
      +pow(l2/cbrt_l123,a)*2.0/3.0/l2*N2/l1/l3
      -pow(l3/cbrt_l123,a)/l2/3*N2/l1/l3
      -pow(l1/cbrt_l123,a)/l3/3*N3/l1/l2
      -pow(l2/cbrt_l123,a)/l3/3*N3/l1/l2
      +pow(l3/cbrt_l123,a)*2.0/3.0/l3*N3/l1/l2)
 +K*(l123-1.0)*(N1+N2+N3);

// Step 8:
//   Simplify.
l123 = l1 * l2 * l3;
cbrt_l123 = cbrt(l123);
T =
  mu/(3.0*l123)*(  pow(l1/cbrt_l123,a)*(2.0*N1-N2-N3)
                 + pow(l2/cbrt_l123,a)*(2.0*N2-N3-N1)
                 + pow(l3/cbrt_l123,a)*(2.0*N3-N1-N2))
 +K*(l123-1.0)*(N1+N2+N3);


오답, 의도적으로 겸손하게 유지

이것은 위험하다는 점에 유의하십시오. 틀렸어.

최신 정보

메이플은 명백한 것을 놓쳤습니다. 예를 들어 작성하는 훨씬 쉬운 방법이 있습니다.

(pow (l1 * l2 * l3, -0.1e1 / 0.3e1)-l1 * l2 * l3 * pow (l1 * l2 * l3, -0.4e1 / 0.3e1) / 0.3e1)

그 가정 l1, l2그리고 l3복잡한 숫자보다 실제가 아니라이며, 실제 큐브 루트 (보다는 원칙 복잡한 루트)입니다 추출 할 것을, 위에서 제로로 줄일 수 있습니다. 이 0의 계산은 여러 번 반복됩니다.

두 번째 업데이트

내가 수학을 제대로했다면 (내가 수학을 제대로 했다는 보장 은 없다 ), 질문의 불쾌한 표현은 다음과 같이 줄어든다.

l123 = l1 * l2 * l3; 
cbrt_l123_inv = 1.0 / cbrt(l123);
nasty_expression =
    K * (l123 - 1.0) * (N1 + N2 + N3) 
    - (  pow(l1 * cbrt_l123_inv, a) * (N2 + N3) 
       + pow(l2 * cbrt_l123_inv, a) * (N1 + N3) 
       + pow(l3 * cbrt_l123_inv, a) * (N1 + N2)) * mu / (3.0*l123);

위는 가정 l1, l2그리고 l3긍정적 인 실제 숫자입니다.


2
글쎄, CSE 제거 완화 된 의미론과는 독립적으로 작동 해야 합니다 (그리고 주석에 표시된 OP). 물론 중요하다면 (측정) 검사 (생성 된 어셈블리)해야합니다. 지배적 인 용어, 누락 된 수식 단순화, 더 나은 특수 기능 및 취소 위험에 대한 귀하의 요점은 매우 좋습니다.
Deduplicator

3
@Deduplicator-부동 소수점이 아닙니다. 하나는 (예를 들어, 지정하여 안전하지 않은 수학의 최적화를 가능하게하지 않는 한 -ffast-mathGCC 또는 그 소리와 함께)를 컴파일러에 의존 할 수 pow(x,-1.0/3.0)와 같은 것 x*pow(x,-4.0/3.0). 후자는 언더 플로 될 수 있지만 첫 번째는 그렇지 않을 수 있습니다. 부동 소수점 표준을 준수하려면 컴파일러가 해당 계산을 0으로 최적화하지 않아야합니다.
David Hammen 2015-10-02

글쎄, 그것들은 내가 의미하는 것보다 훨씬 더 야심적입니다.
Deduplicator

1
@Deduplicator : 다른 답변에 대해 언급 했듯이 : -fno-math-errnoG ++에서 CSE 동일한 pow호출이 필요 합니다. (어쩌면 그 펑를 증명할 수없는 한 세트 errno를 할 필요가 없습니다?)
피터 코르에게

1
@Lefti-Walter의 대답을 많이 받아들이십시오. 훨씬 더 빠릅니다. 이 모든 답변에는 숫자 취소라는 잠재적 인 문제가 있습니다. 당신 N1N2,, 그리고 N3가 음수가 아니라고 가정하면 2*N_i-(N_j+N_k), 하나는 부정적 일 것이고, 하나는 긍정적일 것이고, 다른 하나는 그 사이 어딘가에있을 것입니다. 이로 인해 쉽게 수치 취소 문제가 발생할 수 있습니다.
David Hammen

32

가장 먼저 주목해야 할 점은 pow정말 비싸기 때문에 가능한 한 많이 제거해야한다는 것입니다. 표현을 훑어 보면 pow(l1 * l2 * l3, -0.1e1 / 0.3e1)과의 반복이 많이 보입니다 pow(l1 * l2 * l3, -0.4e1 / 0.3e1). 그래서 나는 그것들을 미리 계산함으로써 큰 ​​이득을 기대할 것입니다.

 const double c1 = pow(l1 * l2 * l3, -0.1e1 / 0.3e1);
const double c2 = boost::math::pow<4>(c1);

부스트 파워 기능을 사용하고 있습니다.

또한 powexponent으로 더 많은 것을 얻을 수 있습니다 a. aInteger이고 컴파일러 시간에 알려진 경우 boost::math::pow<a>(...)추가 성능을 얻기 위해 이를로 바꿀 수도 있습니다 . 곱셈이 나눗셈보다 빠르기 때문에 a / l1 / 0.3e1a / (l1 * 0.3e1)같은 용어를 바꾸는 것이 좋습니다 .

마지막으로, g ++를 사용하는 -ffast-math경우 옵티마이 저가 방정식을보다 적극적으로 변환 할 수 있도록하는 플래그를 사용할 수 있습니다 . 이 플래그 는 부작용이 있기 때문에 실제로 무엇을하는지 읽어보십시오 .


5
우리 코드 -ffast-math에서을 사용 하면 코드가 불안정 해 지거나 잘못된 답을 줄 수 있습니다. 인텔 컴파일러와 비슷한 문제가있어 -fp-model precise옵션 을 사용해야합니다 . 그렇지 않으면 코드가 터지거나 잘못된 답변을 제공합니다. 따라서 -ffast-math속도를 높일 수 있지만 연결된 질문에 나열된 부작용 외에도 해당 옵션을 매우 신중하게 진행하는 것이 좋습니다.
tpg2114

2
@ tpg2114 : 내 테스트에 따르면-fno-math-errno g ++ 만 있으면pow 루프 외부로 동일한 호출을 끌어 올 수 있습니다 . 이것은 대부분의 코드에서 -ffast-math의 가장 "위험한"부분입니다.
Peter Cordes

1
@PeterCordes 흥미로운 결과입니다! 우리는 또한 pow 매우 느리다 는 문제 dlsym가 있었으며 실제로 약간 덜 정밀하게 할 수있을 때 상당한 성능 향상을 얻기 위해 주석에 언급 된 해킹 을 사용 하게되었습니다.
tpg2114

GCC는 pow가 순수한 기능이라는 것을 이해하지 못합니까? 그것은 아마도 내장 된 지식 일 것입니다.
usr

6
@usr : 그게 요점이라고 생각합니다. 표준에 따르면 일부 상황에서 설정되어야하기 때문에 순수한 기능 pow아닙니다errno . 같은 설정 플래그 -fno-math-errno가 원인이 되지 설정 errno(따라서 표준 위반)하지만 그때는 순수 기능과 같은 최적화 할 수 있습니다.
Nate Eldredge

20

와, 정말 대단한 표정 이군요. 실제로 Maple로 표현을 만드는 것은 여기서 차선책이었습니다. 결과는 단순히 읽을 수 없습니다.

  1. 말하기 변수 이름을 선택했습니다 (l1, l2, l3이 아니라 높이, 너비, 깊이, 그것이 의미하는 경우). 그러면 자신의 코드를 더 쉽게 이해할 수 있습니다.
  2. 여러 번 사용하는 하위 용어를 계산하고 결과를 말하는 이름이있는 변수에 저장합니다.
  3. 당신은 표현이 매우 여러 번 평가된다는 것을 언급합니다. 대부분의 내부 루프에서 매개 변수가 거의 변하지 않는 것 같습니다. 해당 루프 이전의 모든 불변 하위 용어를 계산하십시오. 두 번째 내부 루프에 대해 모든 불변이 루프 외부에있을 때까지 반복합니다.

이론적으로 컴파일러는이 모든 작업을 수행 할 수 있어야하지만 때로는 불가능합니다. 예를 들어 루프 중첩이 다른 컴파일 단위의 여러 함수에 걸쳐 확산되는 경우입니다. 어쨌든, 그것은 당신에게 훨씬 더 읽기 쉽고, 이해하기 쉬우 며 유지 가능한 코드를 줄 것입니다.


8
"컴파일러가해야하지만 때로는 그렇지 않습니다"가 핵심입니다. 물론 가독성 외에.
Javier

3
컴파일러가 어떤 작업을 수행 할 필요가 없다면 거의 항상 잘못되었다고 가정합니다.
edmz

4
다시 말하기 변수 이름 선택 -수학을 할 때 좋은 규칙이 적용되지 않는 경우가 많습니다. 과학 저널에서 알고리즘을 구현해야하는 코드를 살펴보면 코드의 기호가 저널에 사용 된 것과 정확히 일치하는 것이 훨씬 더 좋습니다. 일반적으로 이는 매우 짧은 이름 (아래 첨자 포함)을 의미합니다.
David Hammen

8
"결과는 단순히 읽을 수 없습니다"-왜 이것이 문제입니까? 어휘 분석기 또는 파서 생성기의 고수준 언어 출력이 (인간에 의해) "읽을 수 없음"이라고해도 상관 없습니다. 여기서 중요한 것은 코드 생성기 (Maple)에 대한 입력을 읽고 확인할 수 있다는 것입니다. 것은 없습니다 당신이 오류가없는 것입니다 확신하려면 할 수는 손으로 생성 된 코드 편집이다.
alephzero

3
@DavidHammen : 음, 그 경우에, 하나의 문자 것들 입니다 은 "말 이름". 예를 들면, 2 차원 데카르트의 기하학을 할 때 좌표계 x하고 y있습니다 하지 가 전체이며, 의미없는 단일 문자 변수 단어 정확한 정의와 잘 널리 이해 의미.
Jörg W Mittag 2015 년

17

David Hammen의 대답 은 좋지만 여전히 최적과는 거리가 멀습니다. 그의 마지막 표현을 계속합시다 (이 글을 쓰는 시점에서)

auto l123 = l1 * l2 * l3;
auto cbrt_l123 = cbrt(l123);
T = mu/(3.0*l123)*(  pow(l1/cbrt_l123,a)*(2.0*N1-N2-N3)
                   + pow(l2/cbrt_l123,a)*(2.0*N2-N3-N1)
                   + pow(l3/cbrt_l123,a)*(2.0*N3-N1-N2))
  + K*(l123-1.0)*(N1+N2+N3);

더 최적화 할 수 있습니다. 특히에 대한 호출 cbrt()과 호출 중 하나 를 피할 수 있습니다.pow() 일부 수학적 정체성을 악용 경우 . 이 작업을 단계별로 다시 해보겠습니다.

// step 1 eliminate cbrt() by taking the exponent into pow()
auto l123 = l1 * l2 * l3;
auto athird = 0.33333333333333333 * a; // avoid division
T = mu/(3.0*l123)*(  (N1+N1-N2-N3)*pow(l1*l1/(l2*l3),athird)
                   + (N2+N2-N3-N1)*pow(l2*l2/(l1*l3),athird)
                   + (N3+N3-N1-N2)*pow(l3*l3/(l1*l2),athird))
  + K*(l123-1.0)*(N1+N2+N3);

또한 etc로 최적화 2.0*N1했습니다 N1+N1. 다음으로 pow().

// step 2  eliminate one call to pow
auto l123 = l1 * l2 * l3;
auto athird = 0.33333333333333333 * a;
auto pow_l1l2_athird = pow(l1/l2,athird);
auto pow_l1l3_athird = pow(l1/l3,athird);
auto pow_l2l3_athird = pow_l1l3_athird/pow_l1l2_athird;
T = mu/(3.0*l123)*(  (N1+N1-N2-N3)* pow_l1l2_athird*pow_l1l3_athird
                   + (N2+N2-N3-N1)* pow_l2l3_athird/pow_l1l2_athird
                   + (N3+N3-N1-N2)/(pow_l1l3_athird*pow_l2l3_athird))
  + K*(l123-1.0)*(N1+N2+N3);

호출 이후 pow() 은 여기에서 가장 비용이 많이 드는 작업이므로 가능한 한 많이 줄이는 것이 cbrt()좋습니다 (다음으로 비용이 많이 드는 작업은에 대한 호출로 제거했습니다).

우연히 a정수이면 pow호출을 cbrt(+ 정수 거듭 제곱) 호출에 최적화 할 수 있고 , athird반 정수이면 sqrt(+ 정수 거듭 제곱) 사용할 수 있습니다 . 또한, 혹시 경우 l1==l2l1==l3또는 l2==l3하나 또는 둘 모두 호출을하기 위해 pow제거 할 수있다. 따라서 그러한 기회가 현실적으로 존재한다면 특수한 경우로 간주 할 가치가 있습니다.


@gnat 편집 해주셔서 감사합니다 (내가 직접 해보려고 생각했습니다). David의 답변이 이것에 연결된다면 더 공평하다고 생각했을 것입니다. David의 대답도 비슷한 방식으로 편집하지 않는 이유는 무엇입니까?
Walter

1
나는 당신이 명시 적으로 언급하는 것을 보았 기 때문에 편집했습니다. David의 답변을 다시 읽었지만 거기에서 귀하의 답변에 대한 참조를 찾을 수 없습니다. 내가 추가 한 내용이 작성자의 의도와 일치한다는 것이 100 % 명확하지 않은 경우 편집을 피하려고합니다
gnat

1
@Walter-이제 내 답변이 귀하의 답변으로 연결됩니다.
David Hammen

1
확실히 내가 아니었다. 며칠 전에 귀하의 답변을 찬성했습니다. 나는 또한 내 답변에 대해 무작위 플라이 비 반대표를 받았습니다. 가끔 일이 발생합니다.
David Hammen

1
당신과 나는 각각 미약 한 반대표를 받았습니다. 질문에 대한 모든 반대표를보세요! 현재이 질문은 16 개의 반대표를 받았습니다. 또한 모든 다운 보터를 상쇄하는 것보다 더 많은 80 개의 업 보트를 받았습니다.
데이비드 Hammen

12
  1. "다수"는 몇 개입니까?
  2. 얼마나 시간이 걸려요?
  3. ALL의 매개 변수는이 공식의 재 계산 사이에서 변경? 아니면 미리 계산 된 값을 캐시 할 수 있습니까?
  4. 나는 그 공식을 수동으로 단순화하려고 시도했는데 그것이 저장되는 것이 있는지 알고 싶습니까?

    C1 = -0.1e1 / 0.3e1;
    C2 =  0.1e1 / 0.3e1;
    C3 = -0.4e1 / 0.3e1;
    
    X0 = l1 * l2 * l3;
    X1 = pow(X0, C1);
    X2 = pow(X0, C2);
    X3 = pow(X0, C3);
    X4 = pow(l1 * X1, a);
    X5 = pow(l2 * X1, a);
    X6 = pow(l3 * X1, a);
    X7 = a / 0.3e1;
    X8 = X3 / 0.3e1;
    X9 = mu / a;
    XA = X0 - 0.1e1;
    XB = K * XA;
    XC = X1 - X0 * X8;
    XD = a * XC * X2;
    
    XE = X4 * X7;
    XF = X5 * X7;
    XG = X6 * X7;
    
    T = (X9 * ( X4 * XD - XF - XG) / l1 + XB * l2 * l3) * N1 / l2 / l3 
      + (X9 * (-XE + X5 * XD - XG) / l2 + XB * l1 * l3) * N2 / l1 / l3 
      + (X9 * (-XE - XF + X6 * XD) / l3 + XB * l1 * l2) * N3 / l1 / l2;

[추가됨] 나는 마지막 3 줄 공식에 대해 좀 더 작업했고,이 아름다움을 얻었습니다.

T = X9 / X0 * (
      (X4 * XD - XF - XG) * N1 + 
      (X5 * XD - XE - XG) * N2 + 
      (X5 * XD - XE - XF) * N3)
  + XB * (N1 + N2 + N3)

내 작업을 단계별로 보여 드리겠습니다.

T = (X9 * (X4 * XD - XF - XG) / l1 + XB * l2 * l3) * N1 / l2 / l3 
  + (X9 * (X5 * XD - XE - XG) / l2 + XB * l1 * l3) * N2 / l1 / l3 
  + (X9 * (X5 * XD - XE - XF) / l3 + XB * l1 * l2) * N3 / l1 / l2;


T = (X9 * (X4 * XD - XF - XG) / l1 + XB * l2 * l3) * N1 / (l2 * l3) 
  + (X9 * (X5 * XD - XE - XG) / l2 + XB * l1 * l3) * N2 / (l1 * l3) 
  + (X9 * (X5 * XD - XE - XF) / l3 + XB * l1 * l2) * N3 / (l1 * l2);

T = (X9 * (X4 * XD - XF - XG) + XB * l1 * l2 * l3) * N1 / (l1 * l2 * l3) 
  + (X9 * (X5 * XD - XE - XG) + XB * l1 * l2 * l3) * N2 / (l1 * l2 * l3) 
  + (X9 * (X5 * XD - XE - XF) + XB * l1 * l2 * l3) * N3 / (l1 * l2 * l3);

T = (X9 * (X4 * XD - XF - XG) + XB * X0) * N1 / X0 
  + (X9 * (X5 * XD - XE - XG) + XB * X0) * N2 / X0 
  + (X9 * (X5 * XD - XE - XF) + XB * X0) * N3 / X0;

T = X9 * (X4 * XD - XF - XG) * N1 / X0 + XB * N1 
  + X9 * (X5 * XD - XE - XG) * N2 / X0 + XB * N2
  + X9 * (X5 * XD - XE - XF) * N3 / X0 + XB * N3;


T = X9 * (X4 * XD - XF - XG) * N1 / X0 
  + X9 * (X5 * XD - XE - XG) * N2 / X0
  + X9 * (X5 * XD - XE - XF) * N3 / X0
  + XB * (N1 + N2 + N3)

2
그게 눈에 띄죠? :) FORTRAN, IIRC는 효율적인 공식 계산을 위해 설계되었습니다 ( "FOR"는 공식을 나타냄).
Vlad Feinstein

내가 본 대부분의 F77 코드는 그렇게 보였습니다 (예 : BLAS & NR). Fortran 90-> 2008이 매우 기쁩니다. :)
Kyle Kanos

예. 수식을 번역하는 경우 FORmulaTRANslation보다 더 좋은 방법은 무엇입니까?
Brian Drummond

1
당신의 '최적화'는 잘못된 곳을 공격합니다. 값 비싼 비트는std::pow() 이며,이 중 필요한 것보다 6 배, 3 배 더 많습니다. 즉, 코드가 가능한 것보다 3 배 느립니다.
월터

7

이것은 약간 간결 할 수 있지만 실제로 기본적으로 .txt ax^3 + bx^2 + cx + d로 다시 작성하는 Horner Form을 사용하여 다항식 (에너지 함수 보간)에 대한 속도 향상을 찾았습니다 d + x(c + x(b + x(a))). 이것은을 반복 호출을 많이 피할 pow()와 별도로 호출 같은 어리석은 일을에서 중지 pow(x,6)하고 pow(x,7)대신에 불과하고x*pow(x,6) .

이것은 현재 문제에 직접 적용 할 수 없지만 정수 거듭 제곱을 가진 고차 다항식이있는 경우 도움이 될 수 있습니다. 연산 순서가 중요하기 때문에 수치 안정성과 오버플로 문제를주의해야 할 수도 있습니다 (일반적으로 Horner Form이이를 위해 실제로 도움이된다고 생각하지만 x^20,x 떨어져 보통 크기의 많은 주문입니다).

또한 실용적인 팁으로 아직하지 않았다면 먼저 메이플 표현을 단순화하십시오. 대부분의 일반적인 하위 표현식 제거를 수행하도록 할 수 있습니다. 특히 해당 프로그램의 코드 생성기에 얼마나 영향을 미치는지 모르겠지만 Mathematica에서 코드를 생성하기 전에 FullSimplify를 수행하면 큰 차이가 발생할 수 있다는 것을 알고 있습니다.


Horner 형식은 다항식 코딩을위한 꽤 표준이며 이것은 질문과 전혀 관련이 없습니다.
Walter

1
그의 예를 보면 사실 일 수 있지만 그가 "이 유형의 방정식"이라고 말한 것을 알 수 있습니다. 포스터에 그의 시스템에 다항식이 있으면 대답이 유용 할 것이라고 생각했습니다. 특히 Mathematica 및 Maple과 같은 CAS 프로그램 용 코드 생성기는 특별히 요청하지 않는 한 Horner 양식을 제공하지 않는 경향이 있음을 확인했습니다. 일반적으로 인간으로서 다항식을 작성하는 방식으로 기본 설정됩니다.
neocpp

3

반복되는 작업이 많은 것 같습니다.

pow(l1 * l2 * l3, -0.1e1 / 0.3e1)
pow(l1 * l2 * l3, -0.4e1 / 0.3e1)

pow비용이 많이 드는 함수를 반복적으로 호출하지 않도록 미리 계산할 수 있습니다.

미리 계산할 수도 있습니다.

l1 * l2 * l3

그 용어를 반복해서 사용하면됩니다.


6
나는 옵티마이 저가 이미 당신을 위해 이것을 할 것이라고 확신합니다 ... 적어도 코드를 더 읽기 쉽게 만듭니다.
Karoly Horvath 2015 년

나는 이것을했지만 전혀 속도를 내지 못했습니다. 컴파일러 최적화가 이미 처리하고 있기 때문이라고 생각했습니다.

저장 L1 * L2 * L3 속도 일을 위로하지만, 확실하지 왜 컴파일러 최적화

컴파일러는 때때로 일부 최적화를 수행 할 수 없거나 다른 옵션과 충돌하는 것을 발견하기 때문입니다.
Javier

1
실제로 컴파일러 -ffast-math는 활성화 되지 않는 한 이러한 최적화를 수행해서는 안되며 @ tpg2114의 주석에서 언급했듯이 최적화는 매우 불안정한 결과를 생성 할 수 있습니다.
David Hammen

0

Nvidia CUDA 그래픽 카드가있는 경우 계산을 그래픽 카드로 오프로드하는 것을 고려할 수 있습니다. 이는 계산적으로 복잡한 계산에 더 적합합니다.

https://developer.nvidia.com/how-to-cuda-c-cpp

그렇지 않은 경우 계산을 위해 여러 스레드를 고려할 수 있습니다.


10
이 답변은 당면한 질문과 직교합니다. GPU에는 많은 프로세서가 있지만 CPU에 내장 된 FPU에 비해 ​​상당히 느립니다. GPU로 단일 직렬 계산을 수행하는 것은 큰 손실입니다. CPU는 GPU에 대한 파이프 라인을 채우고 느린 GPU가 단일 작업을 수행 할 때까지 기다린 다음 결과를 언로드해야합니다. 당면한 문제가 대규모 병렬화가 가능할 때 GPU는 절대적으로 환상적이지만 직렬 작업을 수행 할 때는 절대적으로 끔찍합니다.
David Hammen 2015 년

1
원래 질문에서 : "이 코드는 여러 번 실행되므로 성능이 문제입니다." 그것은 "다"보다 하나 더 많습니다. 연산은 스레드 방식으로 계산을 보낼 수 있습니다.
user3791372

0

혹시 상징적으로 계산을 해주시겠습니까? 벡터 작업이있는 경우 blas 또는 lapack을 사용하여 실제로 조사하고 싶을 수 있습니다. 일부 경우에는 작업을 병렬로 실행할 수 있습니다.

numpy 및 / 또는 scipy와 함께 python을 사용할 수 있다는 것은 상상할 수 있습니다 (주제에서 벗어난 위험이 있습니까?). 가능한 한 계산이 더 읽기 쉬울 수 있습니다.


0

높은 수준의 최적화에 대해 명시 적으로 질문했듯이 다른 C ++ 컴파일러를 사용해 볼 가치가 있습니다. 오늘날 컴파일러는 매우 복잡한 최적화 야수이며 CPU 공급 업체는 매우 강력하고 구체적인 최적화를 구현할 수 있습니다. 그러나 그들 중 일부는 무료가 아닙니다 (그러나 무료 학업 프로그램이있을 수 있습니다).

  • GNU 컴파일러 컬렉션은 무료이며 유연하며 많은 아키텍처에서 사용할 수 있습니다.
  • Intel 컴파일러는 매우 빠르고 매우 비싸며 AMD 아키텍처에서도 좋은 결과를 얻을 수 있습니다 (학술 프로그램이 있다고 생각합니다).
  • Clang 컴파일러는 빠르고 무료이며 GCC와 유사한 결과를 생성 할 수 있습니다 (일부 사람들은 더 빠르고 더 좋다고 말하지만 이는 각 애플리케이션 사례에 따라 다를 수 있으므로 자신 만의 경험을 만드는 것이 좋습니다)
  • PGI (Portland Group)는 인텔 컴파일러로서 무료가 아닙니다.
  • PathScale 컴파일러는 AMD 아키텍처에서 좋은 결과를 수행 할 수 있습니다.

컴파일러를 변경하는 것 (물론 전체 최적화 포함)을 통해서만 코드 조각의 실행 속도가 2 배 차이가 나는 것을 보았습니다. 그러나 출력의 신원을 확인해야합니다. 적극적인 최적화는 다른 출력으로 이어질 수 있으며, 이는 확실히 피하고 싶은 것입니다.

행운을 빕니다!

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