使模板化优化更加可维护

Making templatized optimization more maintainable

本文关键字:维护 优化      更新时间:2023-10-16

有时,通过使用不变的模板化内部实现,编译器可以更好地优化一块代码。例如,如果您的图像中有许多频道数量,而不是做类似:

的事情
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指令))。

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

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)... );
}

创建staticFunctor::action表,然后对其进行查找并调用它。

在C 03中,我们没有...语法,因此我们必须手动做更多的事情,而不是完美的转发。我要做的就是创建一个std::vector表。

首先,一个可爱的小程序,以i在[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

我们可以轻松地调用跳台的ITH元素。

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像素宽,否则数十亿像素。

上述代码仍然需要审核以进行正确性:它是编写而没有编译的。

yakk的C 11/1Y技术很棒,但是如果C 03版本对您来说有点太多模板欺骗复制和粘贴开关语句,仅给您一个开关语句以维护:

#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));
}

这显然不是一种可取的方法。但它有效。