优化for循环的函数调用

时间:2013-11-04 23:48:44

标签: c++ function optimization for-loop

我有一些简单的功能

int f_0(int);
int f_1(int);
...
int f_n(int);

然后我有一些for循环,我调用f_i(),这个循环中的条件不必相同

for (int i = 0; i < n; i++) {
   ...
   if (condition) {
      int myInt = f_i(); // this is not real implementation but shows the result
                         // I want to achieve
      ... //edit
   }
...
}

以下是我尝试实现此方法的方法:

  • 分解for循环并调用相应部分中的每个函数。这样可以获得最快的代码,但这种代码非常不优雅,而且这些代码很难进一步开发。
  • 功能指针

    typedef int (*Foo) (int);

    Foo fptr[] = { f_0, f_1, ... , f_n };

这是一种优雅的方法,但在我的情况下,它比分解循环慢4.4。函数的常量指针会产生相似的结果。

  • 将我的功能封装到开关功能中。这比打破循环慢2.6。

有没有更好的方法来实现这个? 理想的解决方案是具有紧凑代码的解决方案,但编译器会打破循环并让计算最快。

我正在使用MSVC 2012并在发布模式下运行,并将优化设置为最大化速度。

修改

这是我的测试代码:

head.h

namespace c {
const int w = 1024;
const int A = w * w;
}

inline int f_0(int pos)  { return (pos - c::w + c::A) % c::A;           }
inline int f_1(int pos)  { return (pos + 1 - c::w + c::A) % c::A;       }
inline int f_2(int pos)  { return (pos + 1) % c::A;                     }
inline int f_3(int pos)  { return (pos + c::w) % c::A;                  }
inline int f_4(int pos)  { return (pos - 1 + c::w) % c::A;              }
inline int f_5(int pos)  { return (pos - 1 + c::A) % c::A;              }

typedef int (*NEIGH_F) (int);
typedef int (* const CNEIGH_F) (int);

const NEIGH_F  fptr[]  = { f_0, f_1, f_2, f_3, f_4, f_5 };
const CNEIGH_F cfptr[] = { f_0, f_1, f_2, f_3, f_4, f_5 };

inline int fswitch(int i, int pos) {
    switch(i) {
    case 0 : return f_0(pos); break;
    case 1 : return f_1(pos); break;
    case 2 : return f_2(pos); break;
    case 3 : return f_3(pos); break;
    case 4 : return f_4(pos); break;
    case 5 : return f_5(pos); break;
    default : return -1; break;
    }
}

的main.cpp

#include "head.h"
#include <iostream>
#include <time.h>

int main()
{
    int maxRepeat = 100;

    clock_t startTime = clock();
    double sum = 0;
    for (int repeat = 0; repeat < maxRepeat; repeat++)
        for (int i = 0; i < c::A; i++) {
            sum += f_0(i);
            sum += f_1(i);
            sum += f_2(i);
            sum += f_3(i);
            sum += f_4(i);
            sum += f_5(i);
        }
    std::cout << "normal time:        " << (clock() - startTime)/(double)CLOCKS_PER_SEC
                 << "  sum is: " << sum << std::endl;

    startTime = clock();
    sum = 0;
    for (int repeat = 0; repeat < maxRepeat; repeat++)
        for (int i = 0; i < c::A; i++) {
            for (int j = 0; j < 6; j++)
                sum += fptr[j](i);
        }
    std::cout << "pointer time:       " << (clock() - startTime)/(double)CLOCKS_PER_SEC
                 << "  sum is: " << sum << std::endl;

    startTime = clock();
    sum = 0;
    for (int repeat = 0; repeat < maxRepeat; repeat++)
        for (int i = 0; i < c::A; i++) {
            for (int j = 0; j < 6; j++)
                sum += cfptr[j](i);
        }
    std::cout << "const pointer time: " << (clock() - startTime)/(double)CLOCKS_PER_SEC
                 << "  sum is: " << sum << std::endl;

    startTime = clock();
    sum = 0;
    for (int repeat = 0; repeat < maxRepeat; repeat++)
        for (int i = 0; i < c::A; i++) {
            for (int j = 0; j < 6; j++)
                sum += fswitch(j, i);
        }
    std::cout << "switch time:        " << (clock() - startTime)/(double)CLOCKS_PER_SEC
                 << "  sum is: " << sum << std::endl;
    std::cin.ignore();

    return 0;
}

函数f_i是我在实际实现中使用的函数,但由于实际实现中的测试目的,这里的循环要简单得多,在问题的第二个代码片段中有几个不同的表单循环。

EDIT2:

我的循环形式应保持不变我只是想找到如何将f_i放入循环的最佳方法。

4 个答案:

答案 0 :(得分:4)

您可以使用模板功能代替f_0f_1 ...更好维护。

template <int N>
void f();

template <>
void f<0>()
{
    printf("f<0>");
}

template <>
void f<1>()
{
    printf("f<1>");
}

int main() {
    f<0>();
    f<1>();
    //f<2>(); // this is compile error
    return 0;
}

但是,模板参数必须作为编译时常量提供,因此不能调用像int i = 0; f<i>()这样的函数

解决这个问题,你可以使用switch-case来调用函数,不是很漂亮,但是可以正常工作

void call_f(int i)
{
    switch(i)
    {
        case 0:
            f<0>();
            break;
        case 1:
            f<1>();
            break;
        default:
            // invalid i, report error
            break;
    }
}

但是,i

没有编译时检查

put all together

答案 1 :(得分:2)

我认为Bryan Chen基于模板的解决方案很有意义。维护和理解会更容易。我赞成了这个解决方案。

也就是说,如果你想要一个没有switch语句的更通用的解决方案,并且你想以“展开”方式测试所有条件,你可以使用带有模板的编译时递归。​​

我使用3个函数完成了它,它基于带有单个整数参数的 Condition 仿函数。显然,根据您的需要,您可以使条件更简单或更复杂。

这个核心涉及一个递归的模板定义,以及一个模板专门化来停止递归:

template <int N>
struct Condition;  // provides bool operator()(int arg)

template <int N>
void f();

template <int N>
void applyFunctions(int arg);

// Specialization placed first for clarity
template <>
void applyFunctions<0>(int arg)
{
  if (Condition<0>()(arg))
  {
    f<0>();
  }
  // End recursion
};

template <int N>
void applyFunctions(int arg)
{
  if (Condition<N>()(arg))
  {
    f<N>();
  }

  applyFunctions<N - 1>(arg);
};

这是一些输出。在条件检查中打印短语,同时在函数调用中打印[f<i>]。为了清晰起见,我对齐了打印输出。

Loop
j = 0:                       Is even. [f<1>]       Always true. [f<0>]
j = 1:                                             Always true. [f<0>]
j = 2:  Is prime. [f<2>]     Is even. [f<1>]       Always true. [f<0>]
j = 3:  Is prime. [f<2>]                           Always true. [f<0>]
j = 4:                       Is even. [f<1>]       Always true. [f<0>]
j = 5:  Is prime. [f<2>]                           Always true. [f<0>]
j = 6:                       Is even. [f<1>]       Always true. [f<0>]
j = 7:  Is prime. [f<2>]                           Always true. [f<0>]
j = 8:                       Is even. [f<1>]       Always true. [f<0>]
j = 9:                                             Always true. [f<0>]
j = 10:                      Is even. [f<1>]       Always true. [f<0>]

完整的计划如下。如果你真的想做一些很酷的事情,你可以让Condition结构有一个以constexpr方式计算的成员变量,以便在编译时确定包含结果代码。如果这对您没有任何意义,您可能需要阅读模板,模板实例化和元编程。

#include <iostream>
#include <iomanip>

static int fw = 20;

template <int N>
struct Condition;

template <int N>
void f();


// Specialization 0
template <>
struct Condition<0>
{
  bool operator() (int arg)
  {
    std::cout << std::setw(fw) << " Always true. ";
    return true;
  }
};

template <>
void f<0>()
{
  std::cout << "[f<0>]";
}

// Specialization 1
template <>
struct Condition<1>
{
  bool operator() (int arg)
  {
    bool isEven = (arg % 2 == 0);
    if (isEven)
      std::cout << std::setw(fw) << " Is even. ";
    else 
      std::cout << std::setw(fw) << " ";
    return isEven;
  }
};

template <>
void f<1>()
{
  std::cout << "[f<1>]";
}


// Specialization 2
template <>
struct Condition<2>
{
  bool operator() (int arg)
  {
    bool isPrime = (arg == 2 || arg == 3 || arg == 5 || arg == 7);
    if (isPrime)
      std::cout << std::setw(fw) << " Is prime. ";
    else 
      std::cout << std::setw(fw) << " ";
    return isPrime;
  }
};

template <>
void f<2>()
{
  std::cout<< "[f<2>]";
}


template <int N>
void applyFunctions(int arg);

template <>
void applyFunctions<0>(int arg)
{
  if (Condition<0>()(arg))
  {
    f<0>();
  }
  // End recursion
};

template <int N>
void applyFunctions(int arg)
{
  if (Condition<N>()(arg))
  {
    f<N>();
  }

  applyFunctions<N - 1>(arg);
};


int main()
{
  applyFunctions<2>(4);

  std::cout << std::endl << "Loop" << std::endl;
  for (int j = 0; j < 11; ++j)
  {
    std::cout << "j = " << j << ": ";
    applyFunctions<2>(j);
    std::cout << std::endl;
  }
}

答案 2 :(得分:2)

以下两个调整从根本上改变了程序结果的输出(感谢干净的编译代码!)。这些证明了性能优化在构建时间与运行时间不确定性之间有明确的权衡:如果您知道要调用的函数或将要运行的目标机器,则可以编写更优的代码。

通过指针进行函数调用使您可以灵活地在运行时调用函数,但代价是不内联函数调用。修改对以下内容的调用使指针时间等于正常时间。

normal time:        1.36  sum is: 3.29853e+14
pointer time:       1.36  sum is: 3.29853e+14
const pointer time: 1.35  sum is: 3.29853e+14
switch time:        1.14  sum is: 3.29853e+14

更改是在循环中展开函数调用,因此:

   sum += fptr[1](i);
   sum += fptr[2](i);
   sum += fptr[3](i);
   sum += fptr[4](i);
   sum += fptr[5](i);
对于您所显示的案例,

fswitch()比正常情况更快,因为内联fswitch()内部会创建一组缓存的指令。也许具有必要专业知识的人可以通过反汇编生成的可执行文件来证明这一点。对于我的测试,我稍微扩大了开关功能(通过复制它们的双switch分支,如下所示),并发现它的运行速度比正常慢大约4倍:

normal time:        2.35  sum is: 6.59706e+14
pointer time:       2.35  sum is: 6.59706e+14
const pointer time: 2.34  sum is: 6.59706e+14
switch time:        9.61  sum is: 6.59706e+14

变化是:

case 6 : return f_0(pos); break;
case 7 : return f_1(pos); break;
case 8 : return f_2(pos); break;
case 9 : return f_3(pos); break;
case 10 : return f_4(pos); break;
case 11 : return f_5(pos); break;

...

for (int j = 0; j < 12; j++)
    sum += fswitch(j, i);

...

const NEIGH_F  fptr[]  = { f_0, f_1, f_2, f_3, f_4, f_5, f_0, f_1, f_2, f_3, f_4, f_5 };
const CNEIGH_F cfptr[] = { f_0, f_1, f_2, f_3, f_4, f_5, f_0, f_1, f_2, f_3, f_4, f_5 };

...

for (int j = 0; j < 12; j++)
    sum += fptr[j](i);

...

答案 3 :(得分:1)

f_i()函数以及Aw常量是否真的是这些?因为如果它们是,这个问题是否可以简单地简化为表查找,添加和按位AND?

/* Includes */
#include <stdio.h>
#include <time.h>


/* Constants */
const int w = 1024;
const int A = 1024*1024;
const int addconst[6] = {0xFFC00, 0xFFC01, 0x00001, 0x00400, 0x003FF, 0xFFFFF};
                      /*     A-w,   A-w+1,       1,       w,     w-1,     A-1 */

/* THE NOVELTY */
int ftable(int i, int pos){
    return (pos + addconst[i]) & 0xFFFFF;
}

/* Main */
int main(int argc, char* argv[]){
    clock_t timeTaken;
    int     repeat, maxRepeat = 100;
    int     i, j;
    long    sum = 0;

    timeTaken  = -clock();
    for(repeat=0;repeat<maxRepeat;repeat++)
        for(i=0;i<A;i++)
            for(j=0;j<6;j++)
                sum += ftable(j, i);
    timeTaken += clock();

    printf("Stop! Hammertime!        %f  sum is: %f\n",
           timeTaken/(double)CLOCKS_PER_SEC, (double)sum);
    return 0;
}

请注意,当sum变量为long时,所用时间为:

Stop! Hammertime!        0.348295  sum is: 329853173760000.000000

当它是double时,它需要的时间超过两倍:

Stop! Hammertime!        0.861563  sum is: 329853173760000.000000

我的编译标志是:

gcc -O3 -funroll-loops -finline-functions tmp.c -o tmp

如果您可以解释一下函数索引如何依赖于循环索引,我可以进一步优化。