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

DaB*_*ler 23 c math x86 modulo compiler-optimization

我注意到我的电脑上有一个奇怪的东西。*手写的可分性测试明显比%算子快。考虑最小的例子:

* 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);
}
Run Code Online (Sandbox Code Playgroud)

该示例受奇数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
        }
    }
Run Code Online (Sandbox Code Playgroud)

...以及我电脑上的结果:

| implementation     | time [secs] |
|--------------------|-------------|
| divisible_ui_p     |    8.52user |
| builtin % operator |   17.61user |
Run Code Online (Sandbox Code Playgroud)

因此快了 2 倍以上。

问题:你能告诉我代码在你的机器上的表现吗?是否错过了 GCC 中的优化机会?你能更快地做这个测试吗?


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

| implementation     | time [secs] |
|--------------------|-------------|
| divisible_ui_p     |    8.52user |
| builtin % operator |   17.61user |
Run Code Online (Sandbox Code Playgroud)

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

gcc --version
gcc (Gentoo 9.2.0-r2 p3) 9.2.0
Run Code Online (Sandbox Code Playgroud)

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

#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;
}
Run Code Online (Sandbox Code Playgroud)

Dav*_*lor 12

您正在做的事情称为强度降低:用一系列便宜的操作替换昂贵的操作。

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

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


DaB*_*ler 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;
}
Run Code Online (Sandbox Code Playgroud)

和数组

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;
}
Run Code Online (Sandbox Code Playgroud)

哪些是 / 没有使用shuffle函数进行混洗。

不洗牌,结果依旧

| implementation     | time [secs] |
|--------------------|-------------|
| divisible_ui_p     |    8.56user |
| builtin % operator |   17.59user |
Run Code Online (Sandbox Code Playgroud)

但是,一旦我洗牌这些数组,结果就不同了

| implementation     | time [secs] |
|--------------------|-------------|
| divisible_ui_p     |   31.34user |
| builtin % operator |   17.53user |
Run Code Online (Sandbox Code Playgroud)