【问题标题】:fast c++ sign function快速 C++ 符号函数
【发布时间】:2021-06-03 15:13:56
【问题描述】:

在我的代码中,我在一个循环中多次对 double 进行符号检查,并且该循环通常在执行期间运行数百万次。

我的符号检查是使用fabs() 进行的非常基本的计算,所以我认为必须有其他可能更快的方法,因为“除法很慢”。我遇到了一个模板函数和copysign(),并创建了一个简单的程序来运行速度比较。我已经用下面的代码测试了三种可能的解决方案。

// C++ program to find out execution time of  of functions 
#include <chrono> 
#include <iostream> 
#include <math.h>

using namespace std; 
using namespace std::chrono; 

template<typename Clock>

void printResult(const std::string name, std::chrono::time_point<Clock> start, std::chrono::time_point<Clock> stop, const int iterations)
{
    // Get duration. 
    std::chrono::duration my_duration = duration_cast<nanoseconds>(stop - start); 
    my_duration /= iterations;

    cout << "Time taken by "<< name <<" function: " << my_duration.count() << " ns avg. for " << iterations << " iterations." << endl << endl; 
}


template <typename T> int sgn(T val) 
{
    return (T(0) < val) - (val < T(0));
}


int main() {

    // ***************************************************************** //
    int numiters = 100000000;
    double vel = -0.6574;
    double result = 0;
    
    // Get starting timepoint 
    auto start_1 = high_resolution_clock::now(); 
    for(int x = 0; x < numiters; x++) 
    {

        result = (vel/fabs(vel)) * 12.1;

    }

    // Get ending timepoint 
    auto stop_1 = high_resolution_clock::now(); 
    cout << "Result is: " << result << endl;
    printResult("fabs", start_1, stop_1, numiters);

    // Get starting timepoint 
    result = 0;
    auto start_2 = high_resolution_clock::now(); 
    for(int x = 0; x < numiters; x++) 
    {

        result = sgn(vel) * 12.1;

    }

    // Get ending timepoint 
    auto stop_2 = high_resolution_clock::now(); 
    cout << "Result is: " << result << endl;
    printResult("sgn", start_2, stop_2, numiters);


    // Get starting timepoint 
    result = 0;
    auto start_10 = high_resolution_clock::now(); 
    for(int x = 0; x < numiters; x++) 
    {

        result = copysign(12.1, vel);

    }

    // Get ending timepoint 
    auto stop_10 = high_resolution_clock::now(); 
    cout << "Result is: " << result << endl;
    printResult("copysign", start_10, stop_10, numiters);

    cout << endl;


}

当我运行程序时,我有点惊讶地发现fabs() 解决方案和copysign 解决方案在执行时间上几乎相同。此外,当我多次运行时,我发现结果可能会变化很大。

我的时间正确吗?还有比我测试过的三个例子更好的方法吗?

更新

我已经在quick-bench.com 上实现了测试,其中可以指定编译器设置,并且所有 3 个结果似乎在那里几乎相同。我想我可能有问题: https://quick-bench.com/q/PJiAmoC2NQIJyuvbdz5ZHUALu2M

【问题讨论】:

  • 有一个std::signbit。在IEEE 754(通常用于floatdouble)中,浮点的符号是一个额外的位。因此,我希望它会非常快。
  • 你确定编译器没有完全忽略你的循环吗?除非有一些与浮点相关的怪异之处,否则我不会在程序集中的 main 中看到单个循环。
  • 根据Compiler Explorer,它似乎实际上浓缩为一个简单的位检查(如果我正确解释了ASM)。
  • 您使用什么编译器标志?您可以使用 quick-bench.com 将其重写为微基准测试的任何机会
  • 编写性能测试并不是那么简单。很容易犯无形的错误,从而导致错误的结果。构建标志很重要,代码也必须仔细编写,这样编译器才不会删除测试代码(ASIF 规则提供了这种可能性)。请注意上面的评论和提供的在线工具链接。

标签: c++ benchmarking timing


【解决方案1】:

您的测试无效,因为您在计时范围内执行阻塞 I/O。

但是,我们可以使用 quick-bench 来分析:https://quick-bench.com/q/gt2KzKOFP4iV3ajmqANL_MhnMZk。这表明时间几乎都是相同的。编译器生成的汇编代码呢?

double result = (vel/fabs(vel)) * 12.1;
   movabs $0xc028333333333333,%rax
   mov    %rax,0x8(%rsp)
   add    $0xffffffffffffffff,%rbx


double result = sgn(vel) * 12.1;
   movabs $0xc028333333333333,%rax
   mov    %rax,0x8(%rsp)
   add    $0xffffffffffffffff,%rbx


double result = copysign(12.1, vel);
   movabs $0xc028333333333333,%rax
   mov    %rax,0x8(%rsp)
   add    $0xffffffffffffffff,%rbx

优化代码时:答案总是首先测量以找出程序中实际上最慢的部分,然后重写它以完全不执行任何代码。 p>

【讨论】:

  • @MooningDuck 你误读了程序集。在godbolt上更容易阅读。看我的回答。基本上循环只包含两条​​指令:movadd
  • @MarekR:我基本上根本看不懂汇编,但关键是它们是相同的:P
【解决方案2】:

因为我穿你的测试不要测量任何东西!

从您的quick-bench.com 链接点击godbolt 图标和see this disassembly

请注意,您的所有版本都转换为此汇编代码:

        movabs  rax, -4600370724363619533 # compile time evaluated result move outside measurement loop
.LBB0_3:                                  # =>This Inner Loop Header: Depth=1
        mov     qword ptr [rsp + 8], rax
        add     rbx, -1                   # measurement loop counter
        jne     .LBB0_3

所以基本上编译器能够完全删除测试代码,因为它注意到所有代码都可以在编译时进行 const 评估!

所以你必须提供一些值来测试编译器在编译时无法确定的值。

这里是 my attempt to fix 您的测试和 its assembly 以查看已优化的内容。我不保证这会衡量你必须自己做的正确事情。测量如此小而活泼的代码是很难的。事实上,在如此少的 CPU 周期内执行的任何事情都无法通过软件精确可靠地测量。

【讨论】:

  • 非常感谢,这些结果至少是有道理的 - 所有花费的时间都比 Noop 值更长,我预计 fabs 的速度会变慢,但我不知道如何测试那个理论。慢慢学习!
  • 是的,std::copysign() 显然是最有效的,只需andps 隔离符号位,orps 应用它。 (12.1 是一个已知的正常数,其中不需要 andps。)sgn 测试并不理想,因为编译器将 i%4 数组索引优化为仅基于索引,但实际上并未使用双精度值。
猜你喜欢
  • 2013-01-12
  • 2012-08-13
  • 1970-01-01
  • 2018-02-26
  • 2012-03-21
  • 1970-01-01
  • 1970-01-01
  • 2012-10-15
  • 1970-01-01
相关资源
最近更新 更多