3

我正在处理使用大量数学函数的算法,最近我们在 Solaris 平台的 Ubuntu 系统上移植了 g++ 4.8.2 下的代码。

令人惊讶的是,一些算法比以前花费了很多时间。背后的原因是std::tan()函数比做的时间长两倍std::sin()/std::cos()

用 sin/cos 代替 tan 大大减少了相同结果的计算时间。我想知道为什么会有这样的差异。是因为标准库的实现吗?tan 函数不应该更有效吗?

我写了一个程序来检查函数的时间:

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

int main(int argc, char * argv[])
{
    using namespace std::chrono;

    auto start_tan = system_clock::now();

    for (int i = 0; i < 50000; ++i)
    {
        const double & a = static_cast<double>(i);
        const double & b = std::tan(a);
    }

    auto end_tan = system_clock::now();
    auto elapsed_time_tan = end_tan - start_tan;
    std::cout << "tan : ";
    std::cout << elapsed_time_tan.count() << std::endl;

    auto start_sincos = system_clock::now();

    for (int i =  0; i < 50000; ++i)
    {
        const double & a = static_cast<double>(i);
        const double & b = std::sin(a) / std::cos(a);
    }

    auto end_sincos = system_clock::now();
    auto elapsed_time_sincos = end_sincos - start_sincos;
    std::cout << "sincos : " << elapsed_time_sincos.count() << std::endl;

}

事实上,在输出中我有以下没有优化的时间:

tan : 8319960
sincos : 4736988

并通过优化 (-O2) :

tan : 294
sincos : 120

如果有人对此行为有任何想法。

编辑

我根据@Basile Starynkevitch 的回复修改了程序:

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

int main(int argc, char * argv[])
{
    using namespace std::chrono;

   if (argc != 2) 
   {
      std::cout << "Need one and only argument : the number of iteration." << std::endl;
      return 1;
   }

   int nb_iter = std::atoi(argv[1]);
   std::cout << "Number of iteration programmed : " << nb_iter << std::endl;


   double tan_sum = 0.0;
   auto start_tan = system_clock::now();
    for (int i = 0; i < nb_iter; ++i)
    {
        const double & a = static_cast<double>(i);
        const double b = std::tan(a);
      tan_sum += b;
    }

    auto end_tan = system_clock::now();
    auto elapsed_time_tan = end_tan - start_tan;
    std::cout << "tan : " << elapsed_time_tan.count() << std::endl;
   std::cout << "tan sum : " << tan_sum << std::endl;

   double sincos_sum = 0.0;
    auto start_sincos = system_clock::now();
    for (int i =  0; i < nb_iter; ++i)
    {
        const double & a = static_cast<double>(i);
        const double b = std::sin(a) / std::cos(a);
      sincos_sum += b;
    }

    auto end_sincos = system_clock::now();
    auto elapsed_time_sincos = end_sincos - start_sincos;
    std::cout << "sincos : " << elapsed_time_sincos.count() << std::endl;
   std::cout << "sincos sum : " << sincos_sum << std::endl;

}

现在结果我-O2只得到了类似的时间:

tan : 8345021
sincos : 7838740

但仍然有区别-O2 -mtune=native,但确实更快:

tan : 5426201
sincos : 3721938

我不会使用-ffast-math,因为我需要保持 IEEE 合规性。

4

2 回答 2

8

您不应该关心未优化的代码。

关于优化,GCC 编译器可能会抛出循环,因为您对结果不做任何事情。顺便说一句b,不应该是const double&参考,而是const double.

如果您想要一个有意义的基准,请尝试存储b(或求和)。并使迭代次数(50000)成为运行时参数(例如int nbiter = (argc>1)?atoi(argv[1]):1000;

您可能希望将-O2 -ffast-math -mtune=native优化标志传递给g++(注意-ffast-math在优化细节中不符合标准)

使用这些标志 a 与我的更改:

double sumtan=0.0, sumsincos=0.0;
int nbiter = argc>1?atoi(argv[1]):10000;

for (int i = 0; i < nbiter; ++i)
{
    const double & a = static_cast<double>(i);
    const double  b = std::tan(a);
    sumtan += b;
}

for (int i =  0; i < nbiter; ++i)
{
    const double & a = static_cast<double>(i);
    const double  b = std::sin(a) / std::cos(a);
    sumsincos += b;
}

std::cout << "tan : "  << elapsed_time_tan.count() 
          << " sumtan=" << sumtan << std::endl;

std::cout << "sincos : " << elapsed_time_sincos.count() 
          << " sumsincos=" << sumsincos << std::endl;

使用 GCC 4.9.2 编译

 g++ -std=c++11 -O2 -Wall -ffast-math -mtune=native b.cc -o b.bin

我得到了非常相似的时间:

  % ./b.bin 1000000
  tan : 77158579 sumtan=-3.42432e+06
  sincos : 70219657 sumsincos=-3.42432e+06

这是在 4 年前的台式机上(Intel(R) Xeon(R) CPU X3430 @ 2.40GHz)

clang++如果用3.5.0编译

tan : 78098229 sumtan=-3.42432e+06
sincos : 106817614 sumsincos=-3.42432e+06

PS。时间(和相对性能)与-O3. 有些处理器有机器指令sincostan它们可能不会被使用(因为编译器或libm知道它们比例程慢)。GCC 有这些内置函数

于 2015-01-06T12:57:04.067 回答
2

阅读英特尔开发人员手册。三角函数不如 x86 上的其他数学函数准确,因此 sin / cos 不会给出与 tan 相同的结果,如果 IEEE 合规性是您提出此问题的原因,您应该牢记这一点。

至于加速,sin 和 cos 可以从同一条指令中得到,只要编译器没有脑死。计算 tan 到相同的精度需要更多的工作。因此,编译器不能在不违反标准的情况下替换 sin/cos。

根据这些最后的小数位对您是否重要,您可能需要查看 x86 上三角指令的错误是什么?

于 2015-01-06T15:10:25.517 回答