可比性测试比%运算符更快?


23

我注意到计算机上有一件奇怪的事情。*手写除数测试比%操作员快得多。考虑最小的示例:

* AMD锐龙Threadripper 2990WX,GCC 9.2.0

static int divisible_ui_p(unsigned int m, unsigned int a)
{
    if (m <= a) {
        if (m == a) {
            return 1;
        }

        return 0;
    }

    m += a;

    m >>= __builtin_ctz(m);

    return divisible_ui_p(m, a);
}

该示例受奇数a和限制m > 0。但是,可以很容易地将其推广到所有am。该代码只是将除法转换为一系列的加法。

现在考虑使用以下命令编译的测试程序-std=c99 -march=native -O3

    for (unsigned int a = 1; a < 100000; a += 2) {
        for (unsigned int m = 1; m < 100000; m += 1) {
#if 1
            volatile int r = divisible_ui_p(m, a);
#else
            volatile int r = (m % a == 0);
#endif
        }
    }

...以及我计算机上的结果:

| implementation     | time [secs] |
|--------------------|-------------|
| divisible_ui_p     |    8.52user |
| builtin % operator |   17.61user |

因此快2倍以上。

问题:您能告诉我代码在您的计算机上如何运行吗?是否错过了GCC中的优化机会?您能否更快地执行此测试?


更新: 根据要求,这是一个最小的可复制示例:

#include <assert.h>

static int divisible_ui_p(unsigned int m, unsigned int a)
{
    if (m <= a) {
        if (m == a) {
            return 1;
        }

        return 0;
    }

    m += a;

    m >>= __builtin_ctz(m);

    return divisible_ui_p(m, a);
}

int main()
{
    for (unsigned int a = 1; a < 100000; a += 2) {
        for (unsigned int m = 1; m < 100000; m += 1) {
            assert(divisible_ui_p(m, a) == (m % a == 0));
#if 1
            volatile int r = divisible_ui_p(m, a);
#else
            volatile int r = (m % a == 0);
#endif
        }
    }

    return 0;
}

gcc -std=c99 -march=native -O3 -DNDEBUG在AMD Ryzen Threadripper 2990WX上编译

gcc --version
gcc (Gentoo 9.2.0-r2 p3) 9.2.0

UPDATE2:根据要求,可以处理任何a和的版本m(如果您还想避免整数溢出,则必须使用两倍于输入整数的整数类型来实现测试):

int divisible_ui_p(unsigned int m, unsigned int a)
{
#if 1
    /* handles even a */
    int alpha = __builtin_ctz(a);

    if (alpha) {
        if (__builtin_ctz(m) < alpha) {
            return 0;
        }

        a >>= alpha;
    }
#endif

    while (m > a) {
        m += a;
        m >>= __builtin_ctz(m);
    }

    if (m == a) {
        return 1;
    }

#if 1
    /* ensures that 0 is divisible by anything */
    if (m == 0) {
        return 1;
    }
#endif

    return 0;
}

评论不作进一步讨论;此对话已转移至聊天
塞缪尔·柳

我还希望看到一个测试,在该测试中您实际上断言所r计算的那两个确实彼此相等。
Mike Nakis

@MikeNakis我刚刚添加了。
达伯勒

2
大多数现实生活中的用途a % b具有b比小得多a。通过测试用例中的大多数迭代,它们的大小相似或b更大,并且在这种情况下,您的版本在许多CPU上可以更快。
马特·蒂默曼斯

Answers:


11

您正在做的事情称为强度降低:用一系列廉价的工序代替昂贵的工序。

许多CPU上的mod指令运行缓慢,因为从历史上来看,它没有在几个通用基准中进行过测试,因此设计人员因此优化了其他指令。如果必须进行多次迭代,该算法的性能会变差,并且%在仅需要两个时钟周期的CPU上性能会更好。

最后,请注意,有许多捷径可以将剩余的除以特定的常数。(尽管编译器通常会为您解决这个问题。)


历史上没有在几个通用基准中进行过测试 -也是因为除法运算本身是迭代的,并且很难快速进行!x86至少没有做为div/的一部分,idiv它们在Intel Penryn,Broadwell和IceLake(高基数硬件分隔器)中引起了一定的关注
Peter Cordes

1
我对“强度降低”的理解是,您可以用一个较轻的操作替换一个循环中的繁重操作,例如,而不是x = i * const每次迭代都进行x += const一次迭代。我不认为用移位/相加循环代替单个乘法将被称为强度降低。en.wikipedia.org/wiki/…表示可以用这种方式使用该术语,但要注意“该材料有争议。最好将其描述为窥孔优化和指令分配。”
Peter Cordes

9

我会自己回答我的问题。看来我成了分支预测的受害者。操作数的相互大小似乎并不重要,而仅取决于它们的顺序。

考虑以下实现

int divisible_ui_p(unsigned int m, unsigned int a)
{
    while (m > a) {
        m += a;
        m >>= __builtin_ctz(m);
    }

    if (m == a) {
        return 1;
    }

    return 0;
}

和数组

unsigned int A[100000/2];
unsigned int M[100000-1];

for (unsigned int a = 1; a < 100000; a += 2) {
    A[a/2] = a;
}
for (unsigned int m = 1; m < 100000; m += 1) {
    M[m-1] = m;
}

使用随机播放功能不随机播放的

不改组,结果仍然

| implementation     | time [secs] |
|--------------------|-------------|
| divisible_ui_p     |    8.56user |
| builtin % operator |   17.59user |

但是,一旦我重新排列这些数组,结果就会不同

| implementation     | time [secs] |
|--------------------|-------------|
| divisible_ui_p     |   31.34user |
| builtin % operator |   17.53user |
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.