模运算似乎不适用于所有值的 64 位值

The modulo operation doesn't seem to work on a 64-bit value of all ones

所以...模运算似乎不适用于所有 1 的 64 位值。

这是我设置边缘情况的 C 代码:

#include <stdio.h>

int main(int argc, char *argv[]) {
    long long max_ll =   0xFFFFFFFFFFFFFFFF;
    long long large_ll = 0x0FFFFFFFFFFFFFFF;
    long long mask_ll =  0x00000F0000000000;

    printf("\n64-bit numbers:\n");
    printf("0x%016llX\n", max_ll % mask_ll);
    printf("0x%016llX\n", large_ll % mask_ll);

    long max_l =   0xFFFFFFFF;
    long large_l = 0x0FFFFFFF;
    long mask_l =  0x00000F00;

    printf("\n32-bit numbers:\n");
    printf("0x%08lX\n", max_l % mask_l);
    printf("0x%08lX\n", large_l % mask_l);

    return 0;
}

输出显示如下:

64-bit numbers:
0xFFFFFFFFFFFFFFFF
0x000000FFFFFFFFFF

32-bit numbers:
0xFFFFFFFF
0x000000FF

这是怎么回事?

为什么模运算不能对所有 1 的 64 位值起作用,但它可以对所有 1 的 32 位值起作用?

这是 Intel CPU 的错误吗?或者以某种方式使用 C?还是其他原因?

更多信息

我在一台装有 Intel i5-4570S CPU 的 Windows 10 机器上。我使用了 Visual Studio 2015 年的 cl 编译器。

我还通过进入程序员模式使用 Windows 计算器应用程序(版本 10.1601.49020.0)验证了这个结果。如果你尝试用任何东西取模 0xFFFF FFFF FFFF FFFF,它只是 returns 本身。

指定无符号与有符号似乎没有任何区别。

请赐教:)我确实有这个操作的用例......所以它不是纯粹的学术。

尝试将 unsigned 放在 long long 之前。作为有符号数,您的 0xFF...FF 在大多数平台上实际上是 -1。

此外,在您的代码中,您的 32 位数字仍然是 64 位的(您也将它们声明为 long long)。

实际上 确实 将值定义为 signedunsigned:

会有所不同
#include <stdio.h>
#include <limits.h>

int main(void) {
#if ULLONG_MAX == 0xFFFFFFFFFFFFFFFF
    long long max_ll =   0xFFFFFFFFFFFFFFFF;  // converts to -1LL
    long long large_ll = 0x0FFFFFFFFFFFFFFF;
    long long mask_ll =  0x00000F0000000000;

    printf("\n" "signed 64-bit numbers:\n");
    printf("0x%016llX\n", max_ll % mask_ll);
    printf("0x%016llX\n", large_ll % mask_ll);

    unsigned long long max_ull =   0xFFFFFFFFFFFFFFFF;
    unsigned long long large_ull = 0x0FFFFFFFFFFFFFFF;
    unsigned long long mask_ull =  0x00000F0000000000;

    printf("\n" "unsigned 64-bit numbers:\n");
    printf("0x%016llX\n", max_ull % mask_ull);
    printf("0x%016llX\n", large_ull % mask_ull);
#endif

#if UINT_MAX == 0xFFFFFFFF
    int max_l =   0xFFFFFFFF;  // converts to -1;
    int large_l = 0x0FFFFFFF;
    int mask_l =  0x00000F00;

    printf("\n" "signed 32-bit numbers:\n");
    printf("0x%08X\n", max_l % mask_l);
    printf("0x%08X\n", large_l % mask_l);

    unsigned int max_ul =   0xFFFFFFFF;
    unsigned int large_ul = 0x0FFFFFFF;
    unsigned int mask_ul =  0x00000F00;

    printf("\n" "unsigned 32-bit numbers:\n");
    printf("0x%08X\n", max_ul % mask_ul);
    printf("0x%08X\n", large_ul % mask_ul);
#endif
    return 0;
}

产生这个输出:

signed 64-bit numbers:
0xFFFFFFFFFFFFFFFF
0x000000FFFFFFFFFF

unsigned 64-bit numbers:
0x000000FFFFFFFFFF
0x000000FFFFFFFFFF

signed 32-bit numbers:
0xFFFFFFFF
0x000000FF

unsigned 32-bit numbers:
0x000000FF
0x000000FF

64 位十六进制常量 0xFFFFFFFFFFFFFFFF 在存储到 long long 中时具有值 -1。这实际上是实现定义的,因为超出范围转换为有符号类型,但在 Intel 处理器上,使用当前的编译器,转换只保持相同的位模式。

请注意,您没有使用 <stdint.h> 中定义的固定大小整数:int64_tuint64_tint32_tuint32_tlong long 类型在标准中被指定为至少有 64 位,在英特尔 x86_64 上,它们有,并且 long 至少有 32 位,但对于相同的处理器,大小因环境而异:Windows10 中为 32 位(即使在 64 位模式下),MaxOS/10 和 linux64 中为 64 位。这就是为什么您在 long 情况下观察到令人惊讶的行为,其中 unsignedsigned 可能产生相同的结果。它们在 Windows 上不存在,但在 linux 和 MacOS 中存在,因为计算是在 64 位中完成的,并且这些值只是正数。

还要注意 LLONG_MIN / -1LLONG_MIN % -1 都会因为有符号算术溢出而调用未定义的行为,并且这个在 Intel PC 上不会被忽略,它通常会触发一个未捕获的异常并退出程序,就像 1 / 01 % 0.

您的程序使用错误的格式说明符导致 undefined behaviour

%llX 只能用于 unsigned long long。如果您使用正确的说明符,%lld 那么表面上的谜团就会消失:

#include <stdio.h>

int main(int argc, char* argv[])
{
    long long max_ll =   0xFFFFFFFFFFFFFFFF;
    long long mask_ll =  0x00000F0000000000;

    printf("%lld %% %lld = %lld\n", max_ll, mask_ll, max_ll % mask_ll);
}

Output:

-1 % 16492674416640 = -1

在 ISO C 中,% 运算符的定义是 (a/b)*b + a%b == a。此外,对于负数,/ 遵循 "truncation towards zero".

所以 -1 / 164926744166400,因此 -1 % 16492674416640 必须是 -1 才能使上面的公式有效。


正如评论中所讨论的,下面一行:

long long max_ll =   0xFFFFFFFFFFFFFFFF;

导致 实现定义的 行为(假设您的系统具有 long long 作为 64 位类型)。常量 0xFFFFFFFFFFFFFFFF 的类型为 unsigned long long,它超出了 long long 的范围,其最大允许值为 0x7FFFFFFFFFFFFFFF.

当对有符号类型进行超出范围的赋值时,行为是实现定义的,这意味着编译器文档必须说明发生了什么。

通常,这将被定义为生成 long long 范围内的值,并且具有与 unsigned long long 常量相同的 表示 。在 2 的补码中,(long long)-1unsigned long long0xFFFFFFFFFFFFFFFF 具有相同的表示形式,这解释了为什么最终 max_ll 持有值 -1.