【问题标题】:Making templatized optimization more maintainable使模板化优化更易于维护
【发布时间】:2014-02-05 22:25:54
【问题描述】:

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

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(其中枚举的值恰好映射到通道数)。由于枚举只有一定范围的已知值,因此很容易尝试这样做:

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 逻辑来覆盖所有枚举值。

那么,我的问题是:是否有另一种方法可以让编译器生成不变值优化代码,而无需在每次函数调用时进行 switch-case 扩展?

【问题讨论】:

  • 我认为你可以使用 boost::variant 之类的东西(即类型安全的联合)。您仍然必须枚举所有要支持的c 值,但该枚举更紧凑。我认为一般的解决方案是代码生成器。
  • @Adam 我不确定 boost::variant 在这种情况下有何帮助...您可以尝试制定一种方法并发布答案吗? :)
  • 一个通道应该包含数百万个图像像素,所以我认为循环通过所有通道的开销与处理每个通道的工作量相比可以忽略不计。您是否真的观察到“优化”前后运行时性能的任何实质性差异?
  • @nodakai 是的,这非常重要。和一个共同的优化。如果 A 很大,A*B 在 B. 中是敏感的:同时 A+B 不是。每像素操作中最里面的代码是乘法情况。
  • 适度的安装成本是可以的,对吧?每个图像的案例数量像 O(n) 一样吗? O(lg n)具有大常数?或者每个程序执行一次 O(n)? (每个图像 n 最简单,然后每次执行,并且 lg n 解决方案是一团糟)

标签: c++ templates optimization enums


【解决方案1】:

制作跳转表数组,然后调用。目标是创建一个包含各种函数的数组,然后进行数组查找并调用你想要的。

首先,我会做 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&lt;I&gt;() for 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 up。

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 亿像素高。

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

【讨论】:

  • 我也对此感兴趣,但我不完全理解您在做什么。您能否进一步扩展 @fluffy 的示例?
  • 我支持@Adam - 看起来像是一个很好的答案的核心,但我真的不明白答案(我认为我对这些东西相当了解!)。另外,我更喜欢不需要 C++11 的东西(提升很好,但我坚持支持 g++ 4.1)
  • 没有 11 很难:更多细节很容易。只是在出租车上写的,所以保持简短。我可以在 03 中完成,但我们最终会得到一个 ifs 链(可能是二叉树),它只有在分支远离工作时才能正常工作,并且重复工作。 Otoh,运行时的初始低效设置是否可以接受? (这么慢的setuo,快的分支?)
  • @Adam 添加了更多说明。包括 C++03 版本。 C++03 版本比较烂,但是你会怎么做呢?
  • C++11 版本非常适合我可以使用 C++11 时。不过,我认为我更喜欢@mattnewport,因为它易于理解和可移植性等等(而且每次调用的额外开销并不值得担心 - 谁在乎调用时的一些额外分支当它是关于通过一个巨大的因素加速一个紧密的内部循环时?),但这仍然得到我的 +1 只是因为它教会了我很多我不知道的东西。
【解决方案2】:

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

【讨论】:

  • 这很可爱,我认为即使每个动作都有不同的参数计数(因为参数可以绑定在动作ctor中),它也可以使用。无论如何,比我目前的#define hack 要好得多。我可能最终会使用它。谢谢! :)
  • ...尽管实际上将其用于对象上的方法有点棘手。要么您必须将对象作为 self ref 传递(并让它在外部操作),要么您有一个每个方法的结构,然后只委托给方法本身。不过,记住这是一个很好的技术。
【解决方案3】:

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-12-19
    • 2020-10-07
    • 1970-01-01
    • 1970-01-01
    • 2020-07-18
    • 2013-06-02
    • 2015-01-10
    • 1970-01-01
    相关资源
    最近更新 更多