【问题标题】:C++ Loop Unrolling Performance Difference (Project Euler)C++ 循环展开性能差异(Project Euler)
【发布时间】:2013-11-08 19:18:00
【问题描述】:

我有一个关于 Project Euler 问题和使用循环展开进行优化的问题。

问题描述: 2520 是可以除以 1 到 10 的每个数字而没有任何余数的最小数字。能被 1 到 20 的所有数整除的最小正数是多少?

解决方案:

#include <iostream>
#include <limits.h>
#include <stdio.h>
#include <time.h>

using namespace std;

int main() {

    clock_t startTime = clock();

    for (int i = 1; i < INT_MAX; i++)
    {
        bool isDivisible = true;

        //CODE BLOCK #1
        /*for (int j = 2; j <= 20; j++)
        {
                if ( i % j != 0)
                {
                        isDivisible = false;
                        break;
                {
        }*/

        //CODE BLOCK #2
        /*if (i % 2 != 0 || i % 3 != 0 ||
                i % 4 != 0 || i % 5 != 0 ||
                i % 6 != 0 || i % 7 != 0 ||
                i % 8 != 0 || i % 9 != 0 ||
                i % 10 != 0 || i % 11 != 0 ||
                i % 12 != 0 || i % 13 != 0 ||
                i % 14 != 0 || i % 15 != 0 ||
                i % 16 != 0 || i % 17 != 0 ||
                i % 18 != 0 || i % 19 != 0 ||
                i % 20 != 0 )
                isDivisible = false;*/

        if (isDivisible)
        {
                cout << "smallest: " << i << endl;

                cout << "Ran in: " << clock() -  startTime  << " cycles" << endl;
                break;
        }
    }

return 0;
}

现在,注释掉 CODE BLOCK #1 或 CODE BLOCK #2 会给我正确的答案 (232792560)。但是,代码块 #2 比代码块 #1 快得多。

代码块 #1:3,580,000 个循环(我刚刚在代码块 #1 中添加了中断,它运行得更快。但是仍然比复合 IF 语句慢得多。)

代码块 #2:970,000 次循环

有谁知道为什么会出现这种巨大的性能差异?

【问题讨论】:

    标签: c++ for-loop loop-unrolling


    【解决方案1】:

    使用|| 意味着只要一个为真,其余的条件都不会计算。这相当于循环:

        for (int j = 2; j <= 20; j++)
        {
            if ( i % j != 0){
                isDivisible = false;
                break;
            }
        }
    

    如果您尝试这样做,您可能会发现运行时间的差距已经缩小。任何其他差异都可能归因于循环开销,但在您的编译器中启用优化后,我怀疑它们会以相同的速度运行(或者至少有更多相似的时间)。

    编辑关于新的性能差异:
    有许多优化的方法可以检查数字是否被常数整除,例如 N 的任何 2 的幂 i % N != 0 可以替换为 i &amp; (N-1),其他方法也存在但不那么明显。
    编译器知道很多这些小技巧,并且在第二个代码块中可能能够优化大部分(如果不是全部)可分割性检查(因为它们是由您直接写出的),而在第一个代码块中它必须决定展开首先循环,然后用常量替换循环变量,甚至可以推断出不同的检查。
    这种差异可能会导致块 2 中的代码比块 1 中的代码优化得更好。

    【讨论】:

    • 嗯,有道理。我刚刚将中断添加到 CODE BLOCK #1 中,它运行得更快。但是,仍然比复合 IF 语句慢得多。带中断语句:3,580,000 次循环
    【解决方案2】:

    3,580,000 vs 970,000 不仅仅是循环开销...

    在您的最后一个内核中,您似乎打算将 Up、Down 和 square 块保留在另一个循环之间,但这些块是“简洁的”本地块,因此它们包含的数据不会在分支之间共享。不幸的是,即使它们在分支之间共享,您的方法也行不通。

    在您的内部循环中,当前循环使用上一轮计算的数据。并行化这样的循环并非完全微不足道,有时根本无法做到。在您的情况下,一个简单的解决方案是使用原子运算符来增加 Up 和 Down 计数器,但这不会有效,因为原子运算符会导致操作的隐式序列化。

    您可能应该考虑使用已经优化的现有并行原语(例如前缀和)来解决这个问题。例如,CUB 或 Thrust 中的那些。

    【讨论】:

    • 这个答案是否属于不同的问题?我在发布的代码中没有看到任何数组或线程。
    • 不确定您指的是什么。我正在回复 Project Euler 的帖子。
    • 即使在您编辑之后,我也不确定这个答案如何适用。什么是“上、下、方的块”?此外,与其尝试并行化此代码,还有一个使用最小公倍数的简单(且快速)的解决方案。
    • 请详细说明最小公倍数法。我认为循环开销不能解释原始海报问题中的 4 倍加速。我的例子是臭名昭著的无处不在的算法,它在 Θ(n!) 时间内运行。我已经探索了新的 LCM 方法来理解冗余,而校验和不能达到这个目的。我认为没有理由不使用启发式循环阻塞来控制 INT 搜索。
    • 你可以看到我的解决方案here on ideone.com。简单快捷。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-04-03
    • 2010-11-13
    • 2010-11-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多