在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 都是正实数(不为零)。

我已替换l1*l2*l3为等效变量:J。这确实有助于提高性能。

更换pow(x, 0.1e1/0.3e1)cbrt(x)是一个很好的建议。

这将在CPU上运行。在不久的将来,它可能会在GPU上更好地运行,但是目前该选项不可用。


32
好吧,我想到的第一件事(除非编译器会对其自身进行优化)是pow(l1 * l2 * l3, -0.1e1 / 0.3e1)用一个变量替换所有变量。您需要对代码进行基准测试,以确保它运行的快慢。
SingerOfTheFall 2015年

6
同时格式化代码以使其更具可读性-可能有助于确定改进的可能性。
埃德·赫尔

26
为什么所有的否决票和票都关闭?对于那些不喜欢数值或科学编程的人,请看看其他问题。这是一个很好的问题,非常适合此站点。scicomp网站仍为beta;迁移没有一个好的选择。代码审查站点没有引起足够多的关注。OP在科学计算中经常发生的事情:在符号数学程序中构造问题,要求程序生成代码,并且不要触摸结果,因为生成的代码太乱了。
戴维·哈门

6
@DavidHammen 代码审查站点的视线不够好 -听起来像是鸡和蛋的问题,而且这种心态不利于CR获得更多此类眼睛。拒绝scicomp beta网站的想法也是如此,因为它是beta版本 -如果每个人都这样认为,唯一增长的网站就是Stack Overflow。
Mathieu Guindon 2015年

13
这个问题是在元正在讨论在这里
NathanOliver

Answers:


88

编辑摘要

  • 我最初的回答只是指出该代码包含许多重复的计算,并且许多幂涉及1/3的因数。例如,pow(x, 0.1e1/0.3e1)与相同cbrt(x)
  • 我的第二次编辑是错误的,而我的第三次编辑是根据这种错误推断的。这就是使人们害怕更改以字母“ M”开头的符号数学程序的类似于oracle的结果的原因。我已经剔除(即删除)这些编辑并将它们推到该答案的当前修订版本的底部。但是,我没有删除它们。我是人类。我们很容易犯错。
  • 我的第四个编辑开发了一个非常紧凑的表达式正确代表的问题令人费解的表情IF参数l1l2l3是正实数,如果a是非零实数。(我们尚未从OP那里听到有关这些系数的具体性质的信息。鉴于问题的性质,这些都是合理的假设。)
  • 此编辑试图回答如何简化这些表达式的一般性问题。

第一件事

我使用Maple生成C ++代码以避免错误。

枫树和Mathematica有时会错过明显的地方。更重要的是,Maple和Mathematica的用户有时会犯错误。代替“有时”或什至“几乎总是”代替“有时可能更接近商标”。

您可以通过告诉Maple有关的参数来帮助Maple简化该表达式。在手边的例子,我怀疑l1l2l3是正实数,并且a是非零实数。如果是这样,请告知。这些符号数学程序通常假定手头的数量很复杂。限制域可以使程序做出在复数中无效的假设。


如何简化符号数学程序带来的麻烦(此编辑)

符号数学程序通常提供提供有关各种参数的信息的能力。使用该功能,尤其是当您的问题涉及除法或求幂时。在手边的例子,你可以帮助枫告诉它该简化的表达l1l2l3是正实数,并且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)

假设l1l2l3是实而非复数,而真正立方根(而不是原则复杂根)将被提取的,上述减少到

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)

假设l1l2l3是实而非复数,而真正立方根(而不是原则复杂根)是要被提取,上述减小到零。零的计算重复了很多次。

第二次更新

如果我已经正确地进行了数学运算(不能保证我已经正确地进行了数学运算),则问题中令人讨厌的表达式将简化为

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

以上假设l1l2l3是正实数。


2
好吧,CSE消除应该独立于宽松的语义(并且OP在其注释中指出)也可以起作用。当然,如果重要(已测量),则应进行检查(生成的装配)。关于支配术语,错过公式简化,更好的专用功能以及取消危险的观点非常好。
Deduplicator 2015年

3
@Deduplicator-不带浮点数。除非启用了不安全的数学优化(例如,通过指定-ffast-mathgcc或clang),否则编译器不能依赖于pow(x,-1.0/3.0)等于x*pow(x,-4.0/3.0)。后者可能会下溢,而第一个可能不会下溢。为了符合浮点标准,编译器不得将该计算优化为零。
大卫·哈门

好吧,这些远比我想说的要雄心勃勃。
Deduplicator 2015年

1
@Deduplicator:正如我在另一个答案中评论的那样:您需要-fno-math-errnog ++才能对CSE进行相同的pow调用。(除非它可以证明战俘不需要设置errno?)
彼得·科德斯

1
@Lefti-在Walter的回答上要多花点功夫。快很多。所有这些答案都有一个潜在的问题,那就是数值抵消。假设您的N1N2N3为非负数,其中一个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);

我在哪里使用boost pow功能。

此外,您还可以pow使用exponent a。如果a是Integer且在编译器时已知,则也可以将其替换为boost::math::pow<a>(...)以获得进一步的性能。我还建议将术语替换为a / l1 / 0.3e1a / (l1 * 0.3e1)因为乘法比除法要快。

最后,如果使用g ++,则可以使用-ffast-math允许优化器在转换方程式时更积极的标志。了解此标志的实际作用,尽管它有副作用。


5
在我们的代码中,使用-ffast-mathLead会使代码变得不稳定或给出错误的答案。英特尔编译器存在类似的问题,必须使用该-fp-model precise选项,否则代码会崩溃或给出错误的答案。因此-ffast-math可以加快速度,但是除了链接问题中列出的副作用之外,我建议您谨慎选择该选项。
tpg2114

2
@ tpg2114:根据我的测试,您只需要-fno-math-errno g ++就能将相同的调用吊pow出循环。对于大多数代码来说,这是-ffast-math最少的“危险”部分。
彼得·科德斯

1
@PeterCordes那些结果很有趣!我们还遇到了pow 非常慢的问题,最终使用dlsym注释中提到的hack来提高性能,而实际上我们可以以更低的精度进行处理。
tpg2114

GCC会不会理解pow是一个纯函数?那可能是内在的知识。
usr

6
@usr:我想这就是重点。 pow不是一个纯粹的功能,根据该标准,因为它应该设置errno在某些情况下。设置诸如的标志-fno-math-errno会导致其设置errno(从而违反了标准),但是它是一个纯函数,因此可以对其进行优化。
Nate Eldredge,2015年

20

哇,这真是个表情。使用Maple创建表达式实际上是次优选择。结果简直不可读。

  1. 选择说话的变量名(不是l1,l2,l3,而是高度,宽度,深度,如果那是它们的意思)。然后,您更容易理解自己的代码。
  2. 计算您多次使用的子术语,并将结果预先存储在具有口语名称的变量中。
  3. 您提到,该表达式被评估了很多次。我猜想,最内部的循环中只有很少的参数有所不同。计算该循环之前的所有不变子项。重复第二个内部循环,依此类推,直到所有不变式都在循环之外。

从理论上讲,编译器应该能够为您完成所有这些工作,但有时却不能—例如,当循环嵌套遍及不同编译单元中的多个函数时。无论如何,这将为您提供更好的可读性,可理解性和可维护性的代码。


8
“编译器应该这样做,但有时不这样做”,这是关键。当然,除了可读性。
哈维尔2015年

3
如果不需要编译器做某事,那么假设这几乎总是错误的。
edmz 2015年

4
重新选择有说服力的变量名 -很多时候,在做数学时,好的规则并不适用。在查看应该在科学期刊中实现算法的代码时,我宁愿看到代码中的符号与期刊中使用的符号完全相同。通常,这意味着极短的名称,可能带有下标。
大卫·哈门

8
“结果简直无法读取”-为什么会有这个问题?您不会在意词法分析器生成器或解析器生成器的高级语言输出是“不可读的”(人类)。这里重要的是,代码生成器(Maple)的输入是可读且可检查的。事情没有做的是手工编辑生成的代码,如果你想成为自信是没有错误的。
alephzero 2015年

3
@DavidHammen:那么,在这种情况下,一个字母的人 “会说话的名字”。例如,在一个2维笛卡儿做时几何坐标系统,x并且y无意义的单字母的变量,它们是全具有精确定义和阱和广泛理解的含义。
约尔格W¯¯米塔格

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

请注意,我也2.0*N1N1+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()到目前为止,对to的调用是最昂贵的操作,因此有必要尽可能减少它们的调用(下一个昂贵的操作是to的调用cbrt(),我们已经消除了)。

如果碰巧a是整数,则对的调用pow可以优化为调用cbrt(加上整数幂),或者如果athird是半整数,我们可以使用sqrt(加上整数幂)。此外,如果有机会l1==l2l1==l3打败l2==l3一个或两个,pow都可以消除。因此,如果实际存在此类机会,则有必要将其视为特殊情况。


@gnat我很感谢您的编辑(我想自己做这个),但是如果David的回答也可以链接到此内容,那将使它更加公平。您为什么不也以类似方式编辑David的答案?
Walter

1
我之所以进行编辑,是因为我看到你明确提到了它。我重新阅读了大卫的答案,在那找不到您的答案的参考。我会尽量避免修改它不是100%清除的东西我想补充的比赛作者的意图
蚊蚋

1
@Walter-我的答案现在链接到您的答案。
大卫·哈门

1
当然不是我。几天前,我对您的回答表示支持。我还收到了我的答案的随机flyby downvote。东西有时会发生。
大卫·哈门

1
您和我分别收到微不足道的票。看看这个问题的所有否决票!截至目前,该问题已获得16票赞成票。它也收到了80份赞成票,足以抵消所有这些赞成票。
大卫·哈门

12
  1. “很多”是多少?
  2. 多久时间?
  3. DO 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;

[添加]我在最后的三行公式中做了更多工作,并将其归结为这种美感:

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”用于公式)。
弗拉德·费恩斯坦

我见过的大多数F77代码看起来都是这样(例如BLAS和NR)。非常高兴Fortran 90-> 2008存在:)
Kyle Kanos

是。如果您要转换公式,还有什么比FORmulaTRANslation更好的方法?
Brian Drummond 2015年

1
您的“优化”攻击了错误的位置。费用昂贵std::pow(),是您对的调用,而您的调用数仍然比需要的多6到3倍。换句话说,您的代码慢了3倍。
Walter

7

这可能有点简洁,但实际上我已经通过使用Horner Form(基本上重写ax^3 + bx^2 + cx + d为)发现多项式(能量函数的插值)有很好的加速d + x(c + x(b + x(a)))。这样可以避免多次重复呼叫pow()并阻止您做一些愚蠢的事情,例如单独打电话pow(x,6)pow(x,7)不是仅仅做x*pow(x,6)

这并不直接适用于您当前的问题,但是如果您具有带整数次幂的高阶多项式,则可以提供帮助。你可能要注意数值稳定性和溢出问题,因为操作的顺序是那么重要(尽管一般来说其实,我觉得霍纳形式帮助这一点,因为x^20x通常许多数量级分开)。

另外,作为一个实用技巧,如果您还没有这样做,请尝试首先简化maple中的表达式。您可能可以让它为您完成大多数常见的子表达式消除。我不知道它对程序中的代码生成器有多大影响,但是我知道在Mathematica中生成代码之前执行FullSimplify会导致巨大的差异。


Horner格式是编码多项式的相当标准,这与问题完全无关。
Walter

1
考虑到他的例子,这可能是正确的,但是您会注意到他说的是“这种类型的等式”。我认为,如果发布者的系统中包含多项式,则答案将很有用。我特别注意到,除非您特别要求,CAS程序(例如Mathematica和Maple)的代码生成器通常不会为您提供Horner形式。它们默认为您通常将多项式写为人类的方式。
neocpp 2015年

3

看来您正在进行许多重复操作。

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

您可以预先计算这些值,这样就不必重复调用pow可能会很昂贵的函数。

您也可以预先计算

l1 * l2 * l3

当您反复使用该术语时。


6
我敢打赌,优化程序已经为您完成了这项工作……尽管它至少使代码更具可读性。
Karoly Horvath

我这样做了,但是根本没有加快速度。我认为这是因为编译器优化已经在处理它。

虽然存储l1 * l2 * l3确实可以加快速度,但是不确定为什么要使用编译器优化

因为编译器有时无法做一些优化,或者发现它们与其他选项冲突。
哈维尔

1
实际上,除非-ffast-math启用了编译器,否则不得进行这些优化,并且如@ tpg2114的注释中所述,该优化会产生极其不稳定的结果。
大卫·哈门

0

如果您拥有Nvidia CUDA显卡,则可以考虑将计算任务转移到显卡上-显卡本身更适合于复杂的计算。

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

如果没有,您可能需要考虑多个线程进行计算。


10
这个答案与眼前的问题正交。尽管GPU确实有很多处理器,但与嵌入CPU的FPU相比,它们的速度相当慢。使用GPU执行单个串行计算的损失很大。CPU必须填充到GPU的流水线,等待速度较慢的GPU执行该单个任务,然后卸载结果。当问题可以大规模并行化时,GPU绝对是很棒的选择,而执行串行任务时,GPU却是非常糟糕的选择。
大卫·哈门

1
最初的问题是:“由于该代码执行了很多次,因此性能是一个问题。” 比“许多”还多。op可以以线程方式发送计算。
user3791372 2015年

0

借此机会,您可以象征性地提供计算。如果有向量运算,您可能真的想研究使用blas或lapack的情况,在某些情况下可以并行运行运算。

可以想象(可能会失去话题吗?)您可以将python与numpy和/或scipy一起使用。在可能的范围内,您的计算可能更具可读性。


0

正如您明确询问的高级优化一样,尝试不同的C ++编译器可能值得。如今,编译器是非常复杂的优化难题,CPU供应商可能会实施非常强大且特定的优化。但请注意,其中一些不是免费的(但可能会有免费的学术课程)。

  • GNU编译器集合是免费,灵活的,并且可以在许多体系结构上使用
  • 英特尔编译器非常快速,非常昂贵,并且对于AMD架构也可能产生良好的结果(我相信有一个学术程序)
  • Clang编译器快速,免费,并且可能产生与GCC类似的结果(有人说它们更快,更好,但是对于每个应用程序情况可能有所不同,我建议您自己做一次)
  • 作为英特尔编译器,PGI(波特兰集团)不是免费的。
  • PathScale编译器可能在AMD架构上表现良好

我看到代码片段的执行速度相差2倍,仅通过更改编译器即可(当然,要进行全面优化)。但是要注意检查输出的身份。积极的优化可能导致不同的输出,这是您绝对要避免的事情。

祝好运!

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.