c++函数静态局部,性能好

C++ Function static local, performance?

本文关键字:性能 函数 静态局 c++      更新时间:2023-10-16

我有一个函数,它必须根据代表操作符类型的成员枚举的值返回真或假。

我想知道以下选项中最快的是什么,因为我不确定编译器会做什么隐式优化,如果有的话。

   inline bool isBinaryOper( void ) const // Fastest i assume.
   {
      static const bool arr[] =
      {
         true,  // E_PLUS
         true,  // E_MINUS
         true,  // E_MULTIPLY
         true,  // E_DIVIDE
         false, // E_LPARENT
         false, // E_RPARENT
         false, // E_COMMA
         false  // E_SEMICOLON
      };
      return arr[ size_t( this->_eType ) ]; // Assuming values are valid indexes.
   }

或:

   inline bool isBinaryOper( void ) const
   {
      switch( this->_eType )
      {
         case E_PLUS      : return true;
         case E_MINUS     : return true;
         case E_MULTIPLY  : return true;
         case E_DIVIDE    : return true;
         case E_LPARENT   : return false;
         case E_RPARENT   : return false;
         case E_COMMA     : return false;
         case E_SEMICOLON : return false;
         default : ...
      };
   }

或者,我猜这和上一个很像:

   inline bool isBinaryOper( void ) const
   {
      if      ( this->_eType == E_PLUS  ) return true;
      else if ( this->_eType == E_MINUS ) return true;
      // etc...
   }

哪个是最快的,为什么?

这个问题让我觉得是一个过早优化的例子,但就其价值而言,我还是选择switch语句,尽管它可能会稍微慢一些,因为:

  1. 你不会注意到减速。

  2. 假设您填写了default:的情况下,开关实现保护您免受无效数据或更改枚举定义,这将简化调试。

  3. gcc和clang(可能还有其他好的编译器)都将优化转换为二进制搜索或跳转表,这取决于替代方案的排序方式和目标平台的精确特征。在这两种情况下,它都不会简单地对每个可能的值进行线性序列检查,比如if ... else if ... else if ...选项,这几乎肯定是最慢的。

    这使您不必考虑如何对备选项排序,特别是当您可能需要具有不同排序的各种布尔函数时。除非您是计算机体系结构方面的专家,否则您可以合理地假设您的编译器更了解它。

使用该值作为数组的索引要比switch语句快得多。

你的第二个和第三个代码块的表现差不多。但是第一个函数可以快速获取索引并使用它来访问所需的数组元素。这是我的偏好;但是,您可能还需要添加错误检查,以确保参数在预期范围内。

如果对枚举进行了分区,以便所有返回true的值都在返回false的值之前,那么您可以这样做:

inline bool isBinaryOper() const
{
   return this->_eType < E_LPARENT;
}

我想说,数组查找很可能是最有效的事情。它根本没有"脂肪"供优化编译器删减。

当然,这个表很可能会被放在其他段()中。Rdata而不是.text),因此表将占用更多的缓存行。然而,你遇到任何负面影响的机会是可以忽略不计的。

当然,编译器可能会在表查找中实现具有密集大小写值的switch。这将给'naïve'级联if实现带来巨大的改进。然而,不能保证这将以最直接的方式完成。


一个非常简单的快速实验证实了我的推理:

#include <stdio.h>
#include <time.h>
enum E
{
    E0,
    E1,
    E2,
    E3,
    E4,
    E5,
    E6,
    E7,
};
bool f1(E x)
{
    if (x > E7 || x < E0)
        throw "ohbadbad";
    static const bool t[] =
    {
        true,
        true,
        true,
        true,
        false,
        false,
        false,
        false,
    };
    return t[x];
}
bool f2(E x)
{
    switch (x)
    {
    case E0: return true;
    case E1: return true;
    case E2: return true;
    case E3: return true;
    case E4: return false;
    case E5: return false;
    case E6: return false;
    case E7: return false;
    default: throw "ohbadbad";
    }
}
int main(int argc, char* argv[])
{
    bool (*f)(E) = (argc > 1 && argv[1][0] == 's')
        ? f2
        : f1;
    clock_t t = clock();
    int r = 0;
    for (int i = 0; i < 10000; ++i)
        for (int j = 0; j < 100000; ++j)
            r += f((E)(j & E7));
    printf("%d %I64dn", r, __int64(clock() - t));
    return 0;
}

使用msvc++ 16在x86和x64上编译(带-O2选项),f1给出的时钟比f2好3倍以上。

分析目标代码,很容易看出原因:switch确实是使用表实现的——但它是一个标签表。代码从表中获取一个地址,然后跳转到该地址。一个分支有效地执行return 0,另一个执行return 1。这不仅是一个不必要的步骤,而且还会导致频繁的分支错误预测。