【问题标题】:How to implement base 2 loop unrolling at run-time for optimization purposes如何在运行时实现 base 2 循环展开以进行优化
【发布时间】:2015-11-27 12:32:56
【问题描述】:

考虑一些需要重复执行 1-1,000,000 次的代码,并且在编译时不知道重复次数。据我了解,考虑到大量循环,循环展开将是一个微不足道的优化,并且只会优化到编译时指定的 max_unrolls。我想出的想法是实现一个二进制(或基数 2)部分循环展开器,它基本上重复执行 some_function 在运行时指定的次数。我想出了一些代码来演示这个想法,下面显示了一个浓缩版本。从可用性的角度来看,以下代码中使用的方法存在许多问题。

  1. 它需要编码人员手动复制出 base 2 unroll 基本上复制出 unroll 2^n-1 次。
  2. 对于需要使用此方法的每个新功能,也需要重新执行此操作。

我的问题有三个方面。首先,我是否遗漏了一些东西,编译器是否已经足够智能,可以自己做这件事?其次,什么是实现这一点的有效方法,以便将其与标准for 循环进行基准测试,同时解决上述问题。第三,据您所知,有一个库已经实现了这一点。

请注意:我这样做纯粹是为了好玩,但不知道这是否有效。我已经对代码进行了测试,但只发现了非常小的改进,但是我相信我无法手动展开足够远的距离来进行公平的比较。我也知道这种方法有可能创建大量的二进制大小,但是我相信这将是一个值得的时间内存权衡。另外,如果您发布任何程序集,我可能需要一年左右的时间才能理解它。

inline void some_reapeated_function(int &function_parameter_1, int &function_parameter_2)
{
    function_parameter_1 /= function_parameter_2;
}

// Function to be called when you need it to be unrolled.
int runtime_unroll(unsigned int &no_of_loops, int &function_parameter_1, int &function_parameter_2)
{
    std::vector<bool> binary_vector;

    // Stores the number of loops in a binary representation in a vector.
    binary_function.reserve(no_of_loops);
     while(no_of_loops) 
    {
        if (no_of_loops&1)
          binary_vector.push_back(false);
        else
          binary_vector.push_back(true);
        no_of_loops>>=1;
    } 

    // If binary of no_of_loops contains a 2^0 execute once.
    if (binary_vector[0])
    {
        some_reapeated_function(function_parameter_1,function_parameter_2);
    }
    // If binary of no_of_loops contains a 2^1 execute twice.
    if (binary_vector[1])
    {
        some_reapeated_function(function_parameter_1,function_parameter_2);
        some_reapeated_function(function_parameter_1,function_parameter_2);
    }
    //If binary of no_of_loops contains a 2^2 execute 4 times.
    if (binary_vector[2])
    {
        some_reapeated_function(function_parameter_1,function_parameter_2);
        some_reapeated_function(function_parameter_1,function_parameter_2);
        some_reapeated_function(function_parameter_1,function_parameter_2);
        some_reapeated_function(function_parameter_1,function_parameter_2);
    }


    /* This example only covers from 1 to 2^3-1 or up to 7 unrolls. 
    This can continue depending on the number of repetitions needed and 
    could incorporate a for loop to continue after the loop has fully unrolled */
}

【问题讨论】:

  • 更标准的做法是部分展开循环,每次迭代执行一定数量的操作(例如 8 次),然后第二个循环执行其余操作。超过一定的循环大小,进一步展开不会有明显的好处,并且由于使用更多的指令缓存可能会使事情变得更糟。您也可以尝试Duff's device 摆脱第二个循环。
  • @interjay 我试图实现的函数背后的想法是使展开次数完全灵活,从而无论需要多少次迭代都可以优化循环。想象一个二进制形式的int,其中每个 1 代表一个函数被执行 2^n 次,其中 n 是整数中每个位的索引? Duffs 设备很有趣,谢谢 :)

标签: c++ loops optimization loop-unrolling


【解决方案1】:

您可以使用 C++ 模板轻松实现类似的功能。请注意,您仍然受制于编译器:不能保证所有函数调用都会被内联。如果不是,您可以尝试使用 __forceinline 关键字(或等效关键字)。

首先,您需要一个 unroller,它以函数为参数并在完全展开的循环中执行 K 次。函数调用必须是内联的,所以你必须使用函子对象而不是函数指针或std::function-s,函子的类型必须是模板。展开器本身可以通过整数模板参数实现为递归循环。由于 C++ 中的函数不能有部分模板特化,我们必须使我们的展开器成为模板类。这是示例代码:

// execute UnrollCnt times in unrolled fashion
template<int UnrollCnt, class Functor> struct Unroller {
    static inline void Run(int base, const Functor &func) {
        func(base);
        Unroller<UnrollCnt - 1, Functor>::Run(base + 1, func);
    }
};
template<class Functor> struct Unroller<0, Functor> {
    static inline void Run(int base, const Functor &func) {
    }
};

给定展开器,我们可以轻松实现展开循环。如果我们有 N 次迭代,那么我们可以调用我们的展开器 [N/K] 次,然后像往常一样执行一些剩余的调用。请注意,函子的类型在这里仍然必须是模板。代码如下:

// execute with argument in range [begin, end)
template<int UnrollCnt, class Functor>
void UnrolledFor(int begin, int end, const Functor &func) {
    // iterations with unrolling
    int iter = begin;
    for (; iter <= end - UnrollCnt; iter += UnrollCnt)
        Unroller<UnrollCnt, Functor>::Run(iter, func);
    // last iterations without unrolling
    for (; iter < end; iter++)
        func(iter);
}

现在我们可以为任何接受单个参数的函数调用UnrolledFor 循环,作为循环的迭代计数。例如,我们可以计算从 0N-1 的数字之和:

long long ans = 0;
int main() {
    int cnt = 0;
    scanf("%d", &cnt);
    int start = clock();
    // note: passing a lambda function here, requesting 8x unrolling
    UnrolledFor<8>(0, cnt, [](int i) {
        ans += i;
    });
    int elapsed = clock() - start;
    printf("%lld   (%d pg)\n", ans, elapsed);
    return 0;
}

但是请注意,手动展开可能会更快,因为这里的厚抽象级别对于编译器来说并非易事。例如,以下是我观察到的示例代码的一些时序(N = 2000000000):

With MSVC 2013 x64:
1999999999000000000   (421 pg)   // 8x unrolling, ans is global
1999999999000000000   (1389 pg)  // 1x unrolling, ans is global
1999999999000000000   (4664 pg)  // 8x unrolling, ans is local
1999999999000000000   (1388 pg)  // 1x unrolling, ans is local
With MinGW GCC 5.1.0 x64:
1999999999000000000   (1388 pg)  // 1x unrolling, ans is global
1999999999000000000   (1404 pg)  // 8x unrolling, ans is global
1999999999000000000   (1389 pg)  // 1x unrolling, ans is local
1999999999000000000   (1393 pg)  // 8x unrolling, ans is local

如您所见,只有具有全局 ans 变量的 MSVC 确实从展开中获胜。但是使用本地 ans 变量(通过引用捕获)它反而慢了好几倍。

所以如果你真的对性能很着迷,我建议使用宏来展开循环,它们绝对不会增加任何开销。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-12-26
    • 2017-02-20
    • 2011-12-03
    • 1970-01-01
    • 1970-01-01
    • 2012-06-23
    • 1970-01-01
    • 2014-09-20
    相关资源
    最近更新 更多