【问题标题】:strange behavior of x86 "cmp" instructionx86“cmp”指令的奇怪行为
【发布时间】:2012-01-12 16:20:16
【问题描述】:

代码如下:

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

using namespace std;

#define ARR_LENGTH 1000000
#define TEST_NUM 0
typedef unsigned int uint;

uint arr[ARR_LENGTH];

uint inc_time(uint x) {
    uint y = 0, tm = clock();
    for (uint i = 0; i < x; i++) y++;
        return clock() - tm;
}

int main() {
    uint div = 0, mod = 0, tm = 0, overall = 0, inc_tm;
    srand(time(NULL));
    for (uint i = 0; i < ARR_LENGTH; i++) arr[i] = (uint)rand() + 2;

    tm = clock();
    for (uint i = 0; i < ARR_LENGTH - 1; i++)
        if (arr[i] % arr[i+1] != TEST_NUM) mod++;
    overall = clock() - tm;
    inc_tm = inc_time(mod);
    cout << "mods - " << mod << endl;
    cout << "Overall time - " << overall<< endl;
    cout << "   wasted on increment - " << inc_tm << endl;
    cout << "   wasted on condition - " << overall - inc_tm << endl << endl;

    tm = clock();
    for (uint i = 0; i < ARR_LENGTH - 1; i++)
        if (arr[i]/arr[i+1] != TEST_NUM) div++;
    overall = clock()-tm;
    inc_tm = inc_time(div);
    cout << "divs - " << div << endl;
    cout << "Overall time - " << overall << endl;
    cout << "   wasted on increment - " << inc_tm << endl;
    cout << "   wasted on condition - " << overall - inc_tm << endl << endl;

    return 0;
}

如果您使用 Visual Studio,只需在 DEBUG(而非 RELEASE)模式下编译,如果您使用 GCC,则禁用死代码消除 (-fno-dce),否则某些部分代码将无法工作。

所以问题是:当您将 TEST_NUM 常量设置为非零(例如 5)时,两个条件(模数和除法)几乎同时执行,但是当您将 TEST_NUM 设置为 0 时,第二个条件执行较慢(最多 3 次!)。为什么?

这里是反汇编列表:disassembly listing image http://img213.imageshack.us/slideshow/webplayer.php?id=wp000076.jpg

如果为 0,则使用 test 指令而不是 cmp X, 0,但即使您将 cmp X, 5(如果为 5)修补到 cmp X, 0,您也会看到它不会影响模运算, 但会影响除法运算。

在更改TEST_NUM 常量时,请仔细观察操作计数和时间的变化情况。

如果有人可以,请解释一下这是怎么发生的?
谢谢。

【问题讨论】:

  • 请您编辑您的问题以包含测试代码;我不想点击链接!
  • 在没有优化的情况下编译时测试性能没有多大意义。
  • @OliCharlesworth 我已经包含了代码,但如果您不想制作自己的反汇编列表,您仍然需要点击链接(在我的情况下,它在纸上,抱歉)
  • @interjay 感谢您的评论。我知道我在做什么 :) 您只需要禁用死代码消除,或者您可以编写自己的 inc_time() 函数,该函数将能够准确计算增量时间。如您所见,此函数中没有更多有用的计算,并且由于“i”的值可以在编译时计算,因此删除了“死代码”。但是,如果您使用优化进行编译,您将获得相同的策略结果。
  • 如果您想像尝试那样与它们进行比较,则需要将时间变量声明为 volatile。在没有优化的情况下授予并设置为调试无关紧要,无论如何它们都被视为易失性。

标签: performance architecture assembly x86 cmp


【解决方案1】:

TEST_NUM == 0 的情况下,第一个条件很少为真。分支预测将识别这一点并将条件预测为始终为假。这种预测在大多数情况下都是正确的,因此很少需要执行代价高昂的错误预测分支。

“TEST_NUM == 5”的情况几乎相同:第一个条件很少为真。

对于第二个条件 abd TEST_NUM == 0,除法的结果对于每个 arr[i] &lt; arr[i+1] 都为零,其概率约为 0.5。对于分支预测器来说,这是最坏的情况 - 每隔一秒就会预测出错误的分支。平均而言,您将获得错误预测分支所需时钟周期的一半(取决于架构,这可能在 10 到 20 个周期之间)。

如果您的值为TEST_NUM == 5,则第二个条件现在很少为真,概率约为 0.1(此处不太确定)。这是更好的“可预测”。通常,预测器将预测为(几乎)总是错误的,中间有一些随机的正确,但这取决于处理器的内部结构。但无论如何,对于错误的预测分支,您不会经常获得额外的循环,每五次就会出现一次最差的情况。

【讨论】:

  • 一个体面的编译器当然会消除条件分支(以及对分支预测的需要)如果启用了完全优化。道德:分析未优化的代码是浪费时间。
  • 是的,它可能会被优化。在这种情况下,很容易找到代码的无分支版本,因为根据条件基本上只有一两条指令(mod++)。因此编译器可能会无条件地增加一个临时值并将cmov 它返回。但如果条件中的块较大,这可能是不可能的。或者更糟糕的是,这可能是可能的,但编译器不会这样做 - 不知道这是随机采用分支的最坏情况。
  • 编译器可以使用更多技巧,如矢量化、配置文件引导优化等,这些都会影响代码的运行时间。您很好地分析了未优化的代码。我的观点是,在现实世界中,分析未优化的代码几乎没有用处,因为最终您将发布经过优化的代码,这些代码的外观和行为将大不相同。
  • @drhirsch 谢谢,看来分支预测确实是原因。而满足第二个条件的概率也是0.1
  • @MackieMesser 别忘了这是一头球形奶牛,我只是想分析一些不同的情况。例如,在 ARM 皮质处理器上,结果几乎相反,尽管它们也有分支预测......
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-01-10
  • 2017-09-24
  • 2017-01-23
  • 2013-07-03
  • 2013-06-22
  • 1970-01-01
相关资源
最近更新 更多