【问题标题】:Minimize function selection and function call overhead?最小化函数选择和函数调用开销?
【发布时间】:2015-05-22 17:16:30
【问题描述】:

我有一个大数组(图像),我需要对该数据进行许多小的可配置计算。 我将在这里发布一个示例。 注意:这不是实际问题,而是我需要做的一个最小/希望说明性的示例。

// different functions that can be called based on the configuration
float func1( float* a )
{
    return (*a) * (*a);
}

float func2( float* a )
{
    return (*a) + (*a);
}

float func3( float* a )
{
    return 2 * (*a) * (*a);
}

// my data
float* data = new float[1024*1024];

// function that manages the configurations
int main(  )
{
    int param1 = 0;
    int param2 = 1;

    for ( int k = 0; k < 1024*1024; k++ )
    {
        if ( param1 == 2 && param2 == 0 )
            data[k] = func1( data + k );
        else if ( param1 == 1 && param2 == 1 )
            data[k] = func2( data + k );            
        else if ( param1 == 0 && param2 == 1 )
            data[k] = func3( data + k );
        else
            continue;

    }
}

在我的代码中,将循环放在每个函数中是有意义的。

但是,param1 和 param2 在循环期间保持不变,它们在编译时是已知的。

有没有办法消除 if/elseif 语句的影响?

【问题讨论】:

  • 将变量标记为constexpr(或const),编译器应该能够优化检查并只调用一个函数。如果不确定,请查看生成的汇编代码。此外,如果未调用函数,则没有开销。如果您调用的函数很小,请将它们标记为 inline 以获取更多优化替代方案。如果您不修改它们,请将参数标记为const。为什么将参数作为指针传递?间接会增加开销。
  • @JoachimPileborg 你怎么看汇编代码?它是什么文件,如何理解?
  • 几乎所有编译器都有停止和生成汇编代码而不是二进制目标文件的选项。该选项当然取决于编译器,在 GCC 和 Clang 上它是 -S
  • 真的很想知道“气味可配置计算”是如何完成的;)
  • @PaulEvans 一个人不能放纵他的嗅觉吗?这几天世界怎么了! ;)

标签: c++ performance branch function-pointers branch-prediction


【解决方案1】:

可能是这样的

#include <iostream>
#include <map>
#include <functional>
#include <utility>

typedef std::pair<size_t, size_t> pair;
typedef std::map< pair, std::function<float( float* )>> map;

// different functions that can be called based on the configuration
float func1( float* a )
{
    return ( *a ) * ( *a );
}

float func2( float* a )
{
    return ( *a ) + ( *a );
}

float func3( float* a )
{
    return 2 * ( *a ) * ( *a );
}

// my data
float* data = new float[1024 * 1024];

void init( map &myMap )
{
    myMap.insert( pair, std::function<float( float* )>>
                ( pair( 2, 0 ), std::function< float( float* )>( func1 ) ) );
    myMap.insert( pair, std::function<float( float* )>>
                ( pair( 1, 1 ), std::function< float( float* )>( func2 ) ) );
    myMap.insert( pair, std::function<float( float* )>>
                ( pair( 0, 2 ), std::function< float( float* )>( func3 ) ) );
}

// function that manages the configurations
int main( )
{
    int param1 = 0;
    int param2 = 1;

    map myMap;

    init( myMap );

    for( int k = 0; k < 1024 * 1024; k++ )
    {
        data[k] = myMap[pair( param1, param2 )]( data + k );
    }
}

【讨论】:

  • int main() 而不是 void main()
  • 你永远不会调用 init() 函数。
  • @stryku:你能解释一下发生了什么吗?
  • 在 init 中,map 被填充了与某个函数相对应的整数对,并且在循环中,函数在 map 中被执行。在性能方面这是一个糟糕的解决方案 - 由于参数在编译期间已知,映射是完全冗余的,它需要对循环的每次迭代和映射查找(对于 std::map 为 O(log(n)) 进行配对分配。
【解决方案2】:

您可以将选择要使用的适当函数的 if-else 语句移出循环,从而达到:

#include <functional>
// different functions that can be called based on the configuration
float func1( float* a )
{
    return (*a) * (*a);
}

float func2( float* a )
{
    return (*a) + (*a);
}

float func3( float* a )
{
    return 2 * (*a) * (*a);
}

// my data
float* data = new float[1024*1024];

// function that manages the configurations
int main(  )
{
    int param1 = 0;
    int param2 = 1;

    std::function< float( float* )> functionToUse = nullptr;

    if ( param1 == 2 && param2 == 0 )
        functionToUse = std::function<float(float*)>(func1);
    else if ( param1 == 1 && param2 == 1 )
        functionToUse = std::function<float(float*)>(func2);            
    else if ( param1 == 0 && param2 == 1 )
        functionToUse = std::function<float(float*)>(func3);

    if(functionToUse){
        for ( int k = 0; k < 1024*1024; k++ )
        {
            data[k] = functionToUse( data + k );
        }    
    }
}

关于在编译期间选择要使用的函数,我建议查看这个问题: if/else at compile time?

这个问题可能也很有趣: Is cutting if statements by using function pointers going to be more efficient?

【讨论】:

  • 这么慢不是因为每次调用都需要解析函数指针吗?
  • 请阅读第二个链接(关于使用函数指针切割 if 语句的链接)。 “慢”是一个非常广泛且非常不准确的术语俱乐部 - 以确定您需要描述的慢度。长话短说——现代编译器非常擅长优化,分支预测是优化得非常好的领域之一。在循环内使用 if-else 可能更有效。但也可能不是。
【解决方案3】:

只要参数是const 或者编译器可以 100% 确定它们的变量没有别名,因此不会改变(对编译器来说更难)我完全希望优化器完全删除运行时分支并在编译时完成所有工作。

如果您不喜欢依赖优化器,您可以使用模板:

template <int c1, int c2>
float func(float* a)
{
    // No-op.
}

template <>
float func<2, 0>(float* a)
{
    return (*a) * (*a);
}

template <>
float func<1, 1>(float* a)
{
    return (*a) + (*a);
}

template <>
float func<0, 1>(float* a)
{
    return 2 * (*a) * (*a);
}

int main()
{
    const int param1 = 0;
    const int param2 = 1;

    for ( int k = 0; k < 1024*1024; k++ )
    {
        func<param1, param2>(<float ptr>);
    }
}

【讨论】:

  • 在这种情况下,您的 func 实现及其特化看起来如何(如果它们与我的示例一致)?
  • 模板 float func( float* a ) { return (*a) * (*a); } 类似的东西
猜你喜欢
  • 2014-09-12
  • 2017-01-11
  • 1970-01-01
  • 2011-11-09
  • 2015-07-30
  • 1970-01-01
  • 2015-11-04
  • 2017-12-04
  • 1970-01-01
相关资源
最近更新 更多