【问题标题】:The modulo operation doesn't seem to work on a 64-bit value of all ones模运算似乎不适用于全 1 的 64 位值
【发布时间】:2016-01-31 01:17:39
【问题描述】:

所以...模运算似乎不适用于全 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 位值起作用?

这是英特尔 CPU 的错误吗?或者以某种方式使用C?还是别的什么?

更多信息

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

我还使用 Windows 计算器应用程序(版本 10.1601.49020.0)通过进入程序员模式验证了此结果。如果您尝试对 0xFFFF FFFF FFFF FFFF 进行模数运算,它只会返回自身。

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

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

【问题讨论】:

  • 可在 ideone 上重现:ideone.com/GzMAxK
  • 你知道0xFFFFFFFFFFFFFFFF 就是-1 对吧?
  • 这有什么问题? 0xFFFFFFFFFFFFFFFFlong long 的范围之外,所以它会环绕到-1,而带有任何东西的-1 mod 将返回-1。 0x0FFFFFFFFFFFFFFF 是正确的 long long
  • @Mysticial:不,0xFFFFFFFFFFFFFFFF18446744073709551615。十六进制常量表示值,而不是表示。 0xFFFFFFFFFFFFFFFF 很可能是 unsigned long long 类型。当转换为long long 时,它会产生一个实现定义的值,可能是-1LL

标签: c cpu intel modulo modulus


【解决方案1】:

您的程序使用错误的格式说明符导致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。此外,对于负数,/ 遵循“向零截断”。

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


如 cmets 中所述,以下行:

long long max_ll =   0xFFFFFFFFFFFFFFFF;

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

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

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

【讨论】:

  • 我从未意识到十六进制常量本质上是无符号类型,并且将其分配给有符号数据类型会启动实现定义的转换。有趣的。所以我想分配它的“纯粹”方式是把“long long max_ll = -1;”
  • @Hintron 十六进制常量本身并不是无符号的,只是这个特定的。 0x7FFFFFFFFFFFFFFF 已签名 (long long int) 。较大的常量转为无符号,因为它们不适合任何有符号类型。
  • @M.M:十六进制常量的情况非常微妙:C11 6.4.4.1:整数常量的类型是对应列表中可以表示其值的第一个。对于八进制和十六进制常量,列表为 intunsigned intlong intunsigned long intlong long intunsigned long long int 因此,如果 int 是 32 位,0xFFFFFFFF 是本质上是无符号的和-0xFFFFFFFF != -4294967295
【解决方案2】:

实际上确实将值定义为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。这实际上是由于超出范围转换为有符号类型而定义的实现,但在英特尔处理器上,使用当前编译器,转换只是保持相同的位模式。

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

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

【讨论】:

  • 超出范围的转换是实现定义的。当使用算术运算符并且算术运算的结果超出范围时,会发生算术溢出。 (赋值不是算术运算符;反正这不是赋值,因为它是初始化)。
  • @M.M:好点,我更新了答案。更准确地说:C11 6.3.1.3:否则,新类型是有符号的,值不能在其中表示;结果要么是实现定义的,要么是产生实现定义的信号。 但对于浮点,6.3.1.4:当实浮点类型的有限值转换为_Bool以外的整数类型时,小数部分被丢弃(即,该值被截断为零)。如果整数部分的值不能用整数类型表示,则行为未定义。。 UB 包括无符号目标类型。
【解决方案3】:

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

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

【讨论】:

  • OP 写道unsigned 似乎没有什么区别。我想只有当你把掩码和数字都设为无符号时它才会起作用:ideone.com/H9Oad8
  • @mihyar 您的意思是称它为 max_l 而不是 max_ll 不会改变它很长的事实吗? ;) 感谢您发现这一点,我什至没有注意到。我将通过修复更新问题,因为这并不是问题的主要症结。
猜你喜欢
  • 2013-04-21
  • 1970-01-01
  • 2017-05-03
  • 1970-01-01
  • 1970-01-01
  • 2016-12-13
  • 2020-02-15
  • 2020-04-04
  • 2023-04-06
相关资源
最近更新 更多