模运算似乎不适用于所有值的 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
)。
实际上 确实 将值定义为 signed
或 unsigned
:
会有所不同
#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_t
、uint64_t
、int32_t
和 uint32_t
。 long long
类型在标准中被指定为至少有 64 位,在英特尔 x86_64 上,它们有,并且 long
至少有 32 位,但对于相同的处理器,大小因环境而异:Windows10 中为 32 位(即使在 64 位模式下),MaxOS/10 和 linux64 中为 64 位。这就是为什么您在 long
情况下观察到令人惊讶的行为,其中 unsigned
和 signed
可能产生相同的结果。它们在 Windows 上不存在,但在 linux 和 MacOS 中存在,因为计算是在 64 位中完成的,并且这些值只是正数。
还要注意 LLONG_MIN / -1
和 LLONG_MIN % -1
都会因为有符号算术溢出而调用未定义的行为,并且这个在 Intel PC 上不会被忽略,它通常会触发一个未捕获的异常并退出程序,就像 1 / 0
和 1 % 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);
}
-1 % 16492674416640 = -1
在 ISO C 中,%
运算符的定义是 (a/b)*b + a%b == a
。此外,对于负数,/
遵循 "truncation towards zero".
所以 -1 / 16492674416640
是 0
,因此 -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)-1
与 unsigned long long
值 0xFFFFFFFFFFFFFFFF
具有相同的表示形式,这解释了为什么最终 max_ll
持有值 -1
.
所以...模运算似乎不适用于所有 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
)。
实际上 确实 将值定义为 signed
或 unsigned
:
#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_t
、uint64_t
、int32_t
和 uint32_t
。 long long
类型在标准中被指定为至少有 64 位,在英特尔 x86_64 上,它们有,并且 long
至少有 32 位,但对于相同的处理器,大小因环境而异:Windows10 中为 32 位(即使在 64 位模式下),MaxOS/10 和 linux64 中为 64 位。这就是为什么您在 long
情况下观察到令人惊讶的行为,其中 unsigned
和 signed
可能产生相同的结果。它们在 Windows 上不存在,但在 linux 和 MacOS 中存在,因为计算是在 64 位中完成的,并且这些值只是正数。
还要注意 LLONG_MIN / -1
和 LLONG_MIN % -1
都会因为有符号算术溢出而调用未定义的行为,并且这个在 Intel PC 上不会被忽略,它通常会触发一个未捕获的异常并退出程序,就像 1 / 0
和 1 % 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);
}
-1 % 16492674416640 = -1
在 ISO C 中,%
运算符的定义是 (a/b)*b + a%b == a
。此外,对于负数,/
遵循 "truncation towards zero".
所以 -1 / 16492674416640
是 0
,因此 -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)-1
与 unsigned long long
值 0xFFFFFFFFFFFFFFFF
具有相同的表示形式,这解释了为什么最终 max_ll
持有值 -1
.