使模板化优化更易于维护

有时,编译器可以通过对不变量使用模板化内部实现来更好地优化一段代码。 例如,如果您在图片中拥有已知数量的频道,而不是执行如下操作:

Image::doOperation() {
    for (unsigned int i = 0; i < numPixels; i++) {
        for (unsigned int j = 0; i j mChannels; j++) {
            // ...
        }
    }
}

你可以这样做:

template<unsigned int c> Image::doOperationInternal() {
    for (unsigned int i = 0; i < numPixels; i++) {
        for (unsigned int j = 0; j < c; j++) {
            // ...
        }
    }
}

Image::doOperation() {
    switch (mChannels) {
        case 1: doOperation<1>(); break;
        case 2: doOperation<2>(); break;
        case 3: doOperation<3>(); break;
        case 4: doOperation<4>(); break;
    }
}

它允许编译器针对不同的通道计数生成不同的展开循环(这可以极大地提高运行时效率并且还打开不同的优化,例如SIMD指令等等)。

但是,这通常会扩展为一些相当大的case语句,并且以这种方式优化的任何方法都必须具有展开的case语句。 所以,让我们说,相反,我们有一个enum Format为已知的图像格式(其中enum的值恰好映射到通道计数)。 由于枚举只有一定范围的已知值,因此有这种尝试的诱惑:

template<Image::Format f> Image::doOperationInternal() {
    for (unsigned int i = 0; i < numPixels; i++) {
        for (unsigned int j = 0; j < static_cast<unsigned int>(f); j++) {
            // ...
        }
    }
}

Image::doOperation() {
    const Format f = mFormat;
    doOperationInternal<f>();
}

然而,在这种情况下,编译器(正确)抱怨f不是一个常量表达式,尽管它只有有限的范围,理论上编译器可以生成switch逻辑来覆盖所有枚举值。

所以,我的问题是:是否有另一种方法允许编译器生成不变值优化的代码,而不需要每个函数调用都需要一个开关事件爆发?


创建跳转表数组,然后调用。 目标是创建各种函数的数组,然后执行数组查找并调用所需的数组。

首先,我会做C ++ 11的。 C ++ 1y包含自己的整型序列类型,并且易于编写auto返回类型:C ++ 11将返回void

我们的函数类看起来像这样:

struct example_functor {
  template<unsigned N>
  static void action(double d) const {
    std::cout << N << ":" << d << "n"; // or whatever, N is a compile time constant
  }
};

在C ++ 11中,我们需要一些样板:

template<unsigned...> struct indexes {};
template<unsigned Max, unsigned... Is> struct make_indexes:make_indexes< Max-1, Max-1, Is... > {};
template<unsigned... Is> struct make_indexes<0, Is...>:indexes<Is...> {};

创建和模式匹配的索引包。

界面如下所示:

template<typename Functor, unsigned Max, typename... Ts>
void invoke_jump( unsigned index, Ts&&... ts );

并被称为像:

invoke_jump<example_functor, 10>( 7, 3.14 );

我们首先创建一个帮手:

template<typename Functor, unsigned... Is, typename... Ts>
void do_invoke_jump( unsigned index, indexes<Is...>, Ts&&... ts ) {
  static auto table[]={ &(Functor::template action<Is>)... };
  table[index]( std::forward<Ts>(ts)... )
}
template<typename Functor, unsigned Max, typename... Ts>
void invoke_jump( unsigned index, Ts&&... ts ) {
  do_invoke_jump( index, make_indexes<Max>(), std::forward<Ts>(ts)... );
}

它创建一个Functor::actionstatic表,然后对它们进行查找并调用它。

在C ++ 03中,我们没有...语法,所以我们必须手动做更多的事情,并且没有完美的转发。 我要做的是创建一个std::vector表。

首先,一个可爱的小程序,按顺序在[Begin,End]中为我运行Functor.action<I>()

template<unsigned Begin, unsigned End, typename Functor>
struct ForEach:ForEach<Begin, End-1, Functor> {
  ForEach(Functor& functor):
    ForEach<Begin, End-1, Functor>(functor)
  {
    functor->template action<End-1>();
  }
};
template<unsigned Begin, typename Functor>
struct ForEach<Begin,Begin,Functor> {};

我会承认它太可爱了(链是由构造函数依赖关系隐式创建的)。

然后,我们用它来建立一个vector

template<typename Signature, typename Functor>
struct PopulateVector {
  std::vector< Signature* >* target; // change the signature here to whatever you want
  PopulateVector(std::vector< Signature* >* t):target(t) {}
  template<unsigned I>
  void action() {
    target->push_back( &(Functor::template action<I>) );
  }
};

然后我们可以把这两个钩起来:

template<typename Signature, typename Functor, unsigned Max>
std::vector< Signature* > make_table() {
  std::vector< Signature* > retval;
  retval.reserve(Max);
  PopulateVector<Signature, Functor> worker(&retval);
  ForEach<0, Max>( worker ); // runtime work basically done on this line
  return retval;
}

它将我们的跳转表构建为std::vector

然后我们可以很容易地调用跳转表的第I个元素。

struct example_functor {
  template<unsigned I>
  static void action() {
    std::cout << I << "n";
  }
};
void test( unsigned i ) {
  static std::vector< void(*)() > table = make_table< void(), example_functor, 100 >();
  if (i < 100)
    table[i]();
}

当传递整数i打印它,然后换行。

表中函数的签名可以是任何你想要的,所以你可以传入一个指向类型的指针并调用一个方法, I是一个编译时常量。 action方法必须是static ,但它可以调用其非参数的非static方法。

C ++ 03的巨大差异在于,你需要针对跳转表的不同签名使用不同的代码,以及构建跳转表的很多机器(以及std::vector而不是静态数组)。

在进行严肃的图像处理时,您会希望以这种方式生成扫描线函数,并且可能会在生成的扫描线函数中的某处嵌入每像素操作。 每条扫描线执行一次跳跃调度通常足够快,除非您的图像宽1像素,高10亿像素。

上面的代码仍然需要审核正确性:它是在没有编译的情况下编写的。


Yakk的C ++ 11 / 1y技术非常棒,但如果C ++ 03版本对你来说有点太过模板欺骗,那么至少会避免复制和粘贴switch语句,并且只给你一个简单/不太优雅的版本switch语句保持:

#include<iostream>

using namespace std;

struct Foo {
    template<unsigned int c>
    static void Action() {
        std::cout << "c: " << c << endl;
    }
};

template<typename F>
void Dispatch(unsigned int c) {
    switch (c) {
    case 1: F::Action<1>(); break;
    case 2: F::Action<2>(); break;
    case 3: F::Action<3>(); break;
    }
}

int main() {
    for (int i = 0; i < 4; ++i)
        Dispatch<Foo>(i);
}

为了完整起见,这里是我在此期间提出的(临时)解决方案:

#define DISPATCH_TEMPLATE_CALL(func, args) do { 
    switch (mChannels) { 
    case 1: func<1> args; break; 
    case 2: func<2> args; break; 
    case 3: func<3> args; break; 
    case 4: func<4> args; break; 
    default: throw std::range_error("Unhandled format"); 
    } 
} while (0)

template<unsigned int n> void Image::doSomethingInternal(a, b, c) {
    // ...
}

void Image::doSomething(a, b, c) {
    DISPATCH_TEMPLATE_CALL(doSomethingInternal, (a, b, c));
}

这显然不是一个可取的方法。 但它的工作。

链接地址: http://www.djcxy.com/p/88659.html

上一篇: Making templatized optimization more maintainable

下一篇: Mocking Java enum to add a value to test fail case