在“循环展开”中,是否所有展开的表达式都已执行?

时间:2013-08-15 09:35:42

标签: c++ performance

我始终认为,foo2以下的功能在测试后比foo3更快。

以下所有代码:

#include <iostream>
#include <boost/timer.hpp>
#include <boost/lexical_cast.hpp>
#include <stdint.h>

struct session {
    bool operator==(const session& r) const;

    uint8_t proto;
    uint16_t sport;
    uint16_t dport;
    uint32_t sip;
    uint32_t dip;
};

bool session::operator==(const session& r) const {
    return proto == r.proto && sport == r.sport && dport == r.dport 
        && sip == r.sip && dip == r.dip;
}

// my L1,L2,L3 total cache size is 16MB, so set it 32MB to overflow all 16MB caches.
static const int SIZE = 32 * 1024 * 1024 / sizeof(session);
int sum;

void foo1(session* p) {
    session s = {1, 2, 3, 4, 5};
    for (int i = 0; i < SIZE; i++)
        if (p[i] == s)
            sum++;
}

void foo2(session* p) {
    session s = {1, 2, 3, 4, 5};
    int n = SIZE - SIZE % 4;
    int i;

    for (i = 0; i < n; i += 4) {
        if (p[i + 0] == s)
            sum++;
        if (p[i + 1] == s)
            sum++;
        if (p[i + 2] == s)
            sum++;
        if (p[i + 3] == s)
            sum++;
    }
    /*
    for (; i < SIZE; i++)
            if (p[i] == s)
               sum++;
    */
}

void foo3(session* p) {
    session s = {1, 2, 3, 4, 5};
    int n = SIZE - SIZE % 4;
    int i;

    for (i = 0; i < n; i += 4) {
        if (p[i + 0] == s)
            sum++;
        else if (p[i + 1] == s)
            sum++;
        else if (p[i + 2] == s)
            sum++;
        else if (p[i + 3] == s)
            sum++;
    }
    /*
    for (; i < SIZE; i++)
            if (p[i] == s)
               sum++;
    */
}

int main(int argc, char* argv[]) {
    if (argc < 2)
        return -1;

    int n = boost::lexical_cast<int>(argv[1]);
    session* p = new session[SIZE];

    boost::timer t;
    for (int i = 0; i < n; i++)
        foo1(p);
    std::cout << t.elapsed() << std::endl;

    t.restart();
    for (int i = 0; i < n; i++)
        foo2(p);
    std::cout << t.elapsed() << std::endl;

    t.restart();
    for (int i = 0; i < n; i++)
        foo3(p);
    std::cout << t.elapsed() << std::endl;

    delete [] p;
    return 0;
}

测试1000次,./a.out 1000

输出:

4.36
3.98
3.96

我的机器:

CPU:Intel(R)Xeon(R)CPU E5-2420 0 @ 1.90GHz

缓存:

L1d缓存:32K

L1i缓存:32K

二级缓存:256K

L3缓存:15360K

在测试中,foo2foo3具有等效性能。由于foo2可能的情况 CPU并行执行所有展开的表达式,因此foo3是相同的。那对吗?如果是这样,else if语法违反了C / C ++基本else if语义。

有人解释一下吗?非常感谢。

更新

我的编译器是gcc 4.4.6 ins RedHat

g ++ -Wall -O2 a.cpp

3 个答案:

答案 0 :(得分:3)

在某些情况下,我希望foo3更快,因为它可能会短路(会发生一些小于或等于4的分支,而在foo2中,总会发生4个分支)。在s不等于4个数组元素中的任何一个的情况下(在这种情况下非常可能),foo2和foo3基本上是相同的代码。在这种情况下,两个函数都会发生4个分支。

考虑一下foo3的真实情况(就分支而言):

if (p[i + 0] == s)
    sum++;
else
    if (p[i + 1] == s)
        sum++;
    else 
        if (p[i + 2] == s)
            sum++;
        else 
            if (p[i + 3] == s)
                sum++;

这应该表明,只要if不断出现错误,子分支就会发生。这意味着在没有ifs为真的情况下,它将执行与foo2相同数量的操作(虽然功能不同)。

考虑它的粗略方式就好像每个if都有成本(不是if的实体,实际的if)。换句话说,每次在执行流程中达到if时,都需要一定的成本。这是因为必须完成分支。以这种方式思考,很清楚地看到当foo3的流不短路时(当遇到foo3 s if的所有4个时),每个函数的成本是相同的。 (正如KillianDS指出的那样,如果分支预测是错误的,那么foo3实际上需要更长的时间,因为错误的分支必须被倒回而正确的分支被执行。虽然总是选择正确的分支,但似乎对你来说。)

有点像下面的代码片段可以具有相同的性能:

if (short_runtime()) {}

if (short_runtime() && long_runtime()) {}

如果short_runtime返回true,那么具有第二个函数调用的那个显然会花费更长的时间。如果short_runtime()返回为false,则long_runtime()调用将永远不会发生,因此运行时间将相同(或至少相似)。


要测试这个理论,你可以做到p[i + 0] == s为真。只需初始化数组(session* p = new session[SIZE]();),并在本地使用session s = {1, 2, 3, 4, 5};


关于循环展开的目的/结果似乎有些混乱。这样做可以减少必须发生的跳跃。如果必须完成n事件,而不是每次迭代1次操作发生n次迭代(跳转),则可以发生n/k次迭代(跳转)。当一切都可以放入缓存时,这可以提供速度提升(如果它不能适应缓存,它实际上可以扼杀性能!)。

指令不会同时发生(如果是,sum需要一个互斥量,这将是非常昂贵的)。它们只是以4组而不是1组发生。

答案 1 :(得分:2)

这是branch prediction

使用你的程序我得到这些速度(这里foo3有点慢,g ++ 4.8):

7.57
0.63
0.99

现在会发生什么?您没有初始化您的初始会话数组,因为session中的所有变量都是POD,它们不会默认初始化并且基本上包含垃圾。因此,代码中的if将很快收敛到始终预测未采用的分支。在这种情况下,foo3foo2非常相似 lar,foo2将无条件地执行所有,foo3将执行它,因为它是预测的。我真的不明白为什么foo3仍然有点慢,我将不得不查看反汇编代码。

现在看看如果添加以下默认构造函数会发生什么:

session() : proto(1), sport(2), dport(3), sip(4), dip(5) {}

我当然还必须将foo中的会话变量更改为session s;现在我的时间变为:

9.7
1.5
0.75

突然foo3快了很多。仅仅因为现在分支将被大多(正确地)预测为“被采取”。在foo3的情况下,这意味着只执行第一个条件并且函数快速退出。 foo2仍需要评估所有分支,即使预测良好,这显然会使其变慢。

答案 2 :(得分:0)

  • foo2和foo3表现出相同的性能,因为输出中只有0.02 ms或s的差异。您可以使用不同的会话大小对foo2和foo3进行多次测试;尺寸为10,19,20,21,50,80,99等。这将为您提供更多输出,以确定它们的性能是否仍然相同。
  • 在这个问题中,您试图通过执行循环展开来利用编译器。 if和else if语句可能对编译器没有多大意义,因为它不是并行性和优化的一部分,但它可能仍然有用。