【问题标题】:unrolling for loops in a special case function在特殊情况函数中展开 for 循环
【发布时间】:2017-09-18 10:31:42
【问题描述】:

所以我正在尝试优化一些代码。我有一个带有可变大小循环的函数。但是,为了提高效率,我想用 1、2 和 3 大小的循环制作完全展开的特殊情况。到目前为止,我的方法是将循环大小声明为 const 参数,然后定义调用主函数的包装函数,并将其传递给 const 值的文字。我已经包含了一个代码片段,它说明了我的想法。

inline void someFunction (const int a)
{
    for (int i=0; i<a; i++)
    {
        // do something with i.
    }
}

void specialCase()
{
    someFunction (3);
}

void generalCase(int a)
{
    someFunction (a);
}

所以我的问题是,我期望我的编译器 (GCC) 在 specialCase 中展开 for 循环是否合理。我的意思是显然我可以复制 - 将 someFunction 的内容粘贴到 specialCase 中并将 a 替换为 3 但为了清楚起见,我宁愿只在代码中处理 someFunction 的一个定义。

【问题讨论】:

  • 这里有Godbolt,自己测试一下。
  • 您真的会从展开 1、2、3 大小的短循环而不是尝试优化长循环中受益吗?
  • 如果您不想复制粘贴,为什么不将do_something_with(i) 设为一个单独的(内联)函数,让编译器为do_something_with(1); do_something_with(2); 进行复制粘贴。
  • 是的,因为在我的实际示例中,它不仅仅是函数中的一个循环,而是多个循环,其中一些嵌套了 3 或 4 个循环。这个函数会被一遍又一遍地调用,它是程序的主要瓶颈。之前版本的程序手动展开了所有循环,不可读(只支持3次通过该类循环,我想至少支持1、2或3)
  • 可悲的是,godbolt 建议循环不会展开。

标签: c++ gcc loop-unrolling


【解决方案1】:

但是,为了提高效率,我想用 1、2 和 3 大小的循环制作完全展开的特殊情况。

您是否测量过这实际上更快?我怀疑它会(或者编译器不会自动展开循环)。


到目前为止,我的方法是将循环大小声明为 const 参数,然后定义调用主函数的包装函数,并将其传递给 const 值的文字。

const 在这里没有任何意义。它不会影响编译器展开循环的能力。这只是意味着a 不能在函数体内发生变异,但它仍然是一个运行时参数。


如果你想确保展开,那么强制它。使用 C++17 很容易。

template <typename F, std::size_t... Is>
void repeat_unrolled_impl(F&& f, std::index_sequence<Is...>)
{
    (f(std::integral_constant<std::size_t, Is>{}), ...);
}

template <std::size_t Iterations, typename F>
void repeat_unrolled(F&& f)
{
    repeat_unrolled_impl(std::forward<F>(f), 
                         std::make_index_sequence<Iterations>{});
}

live example on godbolt

【讨论】:

  • 简单的答案是我不了解超出基础的模板。
【解决方案2】:

如果你不喜欢模板并且不信任你的编译器,总有这种方法,它的灵感来自于被称为“duff's device”的手动展开循环的过时方法:

void do_something(int i);

void do_something_n_times(int n)
{
    int i = 0;
    switch(n)
    {
        default:
            while(n > 3) {
                do_something(i++);
                --n;
            }
        case 3: do_something(i++);
        case 2: do_something(i++);
        case 1: do_something(i++);
    }
}

但我认为值得一提的是,如果你不相信你的编译器会为你做一些简单的事情,比如循环展开,那么可能是时候考虑一​​个新的编译器了。

请注意,Duff 的设备最初是作为一种微优化策略而发明的,用于使用不会自动应用循环展开优化的编译器编译的程序。

它是汤姆·达夫在 1983 年发明的。

https://en.wikipedia.org/wiki/Duff%27s_device

它与现代编译器的使用是值得怀疑的。

【讨论】:

  • 将主循环的主体部分放入内联函数是我最后的手段。特别是内联,因为要传递的参数太多。
  • @pclark 我不知道你的确切用例,但不要害怕将你的函数的一部分委托给 lambdas,使用变量捕获、参数或两者兼而有之。编译器的优化器应该可以轻松完成所有这些工作。
  • lambda 演算不是我太熟悉的东西,更不用说它在 c++ 中的实现了。我对 c++ 11 及更高版本的了解非常不稳定。因此,我认为最好在这里寻求一些建议。我的应用程序是一类 ODE 的 ODE 系统求解器,其中第 1、第 2 和第 3 种情况最为常见。
  • @pclark 委托给一个函数或一个 Lamba,如果它们被定义在同一个文件中,将尽可能优化,因为编译器可以看到所有代码路径。将代码结构化为更小的函数当然可以使代码更易于理解。但是您已经知道了 :) 值得重申的是,c++ 编译器已经非常擅长辨别您的意图。最终的代码将采用可能会让您感到惊讶的捷径,所以不要太担心手动优化。这通常会妨碍编译器。
  • 我现在删除了我的 cmets,因为它们不再相关 :-)
【解决方案3】:

如果您愿意使用所有流行编译器的强制内联(非标准)功能,我宁愿这样做:

__attribute__((always_inline))
void bodyOfLoop(int i) {
  // put code here
}

void specialCase() {
    bodyOfLoop(0);
    bodyOfLoop(1);
    bodyOfLoop(2);
}

void generalCase(int a) {
    for (int i=0; i<a; i++) {
        bodyOfLoop(i);
    }
}

注意:这是 GCC/Clang 解决方案。对 MSVC 使用 __forceinline

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-05-16
    • 2019-07-16
    • 1970-01-01
    • 1970-01-01
    • 2017-04-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多