返回

assembly-为什么在强度降低乘法到循环携带加法之后,这段代码执行得更慢?

发布时间:2022-06-05 11:46:14 221

我正在阅读Agner Fog的优化手册,遇到了以下示例:

double data[LEN];

void compute()
{
    const double A = 1.1, B = 2.2, C = 3.3;

    int i;
    for(i=0; i<LEN; i++) {
        data[i] = A*i*i + B*i + C;
    }
}

Agner 指出有一种方法可以优化此代码 - 通过意识到循环可以避免使用代价高昂的乘法,而是使用每次迭代应用的“增量”。

我用一张纸来证实这个理论,首先...

...当然,他是对的——在每次循环迭代中,我们可以通过添加“delta”来基于旧结果计算新结果。这个增量从值“A+B”开始,然后在每一步增加“2*A”。

因此,我们将代码更新为如下所示:

void compute()
{
    const double A = 1.1, B = 2.2, C = 3.3;
    const double A2 = A+A;
    double Z = A+B;
    double Y = C;

    int i;
    for(i=0; i<LEN; i++) {
        data[i] = Y;
        Y += Z;
        Z += A2;
    }
}

就操作复杂度而言,这两个版本的功能差异确实是惊人的。与加法相比,乘法在我们的 CPU 中速度明显较慢而闻名。我们已经替换了 3 次乘法和 2 次加法……只有 2 次加法!

所以我继续添加一个循环来执行compute很多次 - 然后保持执行所需的最短时间:

unsigned long long ts2ns(const struct timespec *ts)
{
    return ts->tv_sec * 1e9 + ts->tv_nsec;
}

int main(int argc, char *argv[])
{
    unsigned long long mini = 1e9;
    for (int i=0; i<1000; i++) {
        struct timespec t1, t2;
        clock_gettime(CLOCK_MONOTONIC_RAW, &t1);
        compute();
        clock_gettime(CLOCK_MONOTONIC_RAW, &t2);
        unsigned long long diff = ts2ns(&t2) - ts2ns(&t1);
        if (mini > diff) mini = diff;
    }
    printf("[-] Took: %lld ns.\n", mini);
}

我编译这两个版本,运行它们。。。看看这个:

# gcc -O3 -o 1 ./code1.c

# gcc -O3 -o 2 ./code2.c

# ./1
[-] Took: 405858 ns.

# ./2
[-] Took: 791652 ns.

嗯,这是出乎意料的。由于我们报告了最短执行时间,因此我们放弃了;“噪音”;由操作系统的各个部分引起。我们还注意在一台完全不起任何作用的机器上运行。结果或多或少是可重复的-重新运行这两个二进制文件表明这是一个一致的结果:

# for i in {1..10} ; do ./1 ; done
[-] Took: 406886 ns.
[-] Took: 413798 ns.
[-] Took: 405856 ns.
[-] Took: 405848 ns.
[-] Took: 406839 ns.
[-] Took: 405841 ns.
[-] Took: 405853 ns.
[-] Took: 405844 ns.
[-] Took: 405837 ns.
[-] Took: 406854 ns.

# for i in {1..10} ; do ./2 ; done
[-] Took: 791797 ns.
[-] Took: 791643 ns.
[-] Took: 791640 ns.
[-] Took: 791636 ns.
[-] Took: 791631 ns.
[-] Took: 791642 ns.
[-] Took: 791642 ns.
[-] Took: 791640 ns.
[-] Took: 791647 ns.
[-] Took: 791639 ns.

接下来唯一要做的就是查看编译器为两个版本中的每一个创建了什么样的代码。

objdump -d -S显示第一个版本compute- “哑巴”,但不知何故快速代码 - 有一个看起来像这样的循环:

那么第二个优化版本呢——它只做了两个添加?

现在我不了解你,但就我自己而言,我……很困惑。第二个版本的指令减少了大约 4 倍,其中两个主要的只是基于 SSE 的添加 ( addsd)。第一个版本不仅有 4 倍多的指令......它也充满了(如预期的那样)乘法(mulpd)。

我承认我没想到会有这样的结果。不是因为我是 Agner 的粉丝(我是,但这无关紧要)。

知道我缺少什么吗?我在这里犯了什么错误,可以解释速度的差异吗?请注意,我已经在 Xeon W5580 和 Xeon E5 1620 上进行了测试——在这两个版本中,第一个(哑)版本比第二个版本快得多。

编辑:为了简单地再现结果,我在两个版本的代码中添加了两个要点:Dumb 但不知何故更快和优化,但不知何故更慢。

PS请不要评论浮点精度问题;这不是这个问题的重点。

特别声明:以上内容(图片及文字)均为互联网收集或者用户上传发布,本站仅提供信息存储服务!如有侵权或有涉及法律问题请联系我们。
举报
评论区(1)
按点赞数排序
用户头像
相关帖子