C程序中系数的两个uint64_t数乘除法GCC编译错误

0 c assembly gcc compilation

众所周知,定点算术等同于具有某些约束的整数算术。这样我就抓住了这个问题。让我们来看看:

#include <stdio.h>
#include <stdint.h>

#define USE_C_FUNC 1

#if USE_C_FUNC != 0
uint64_t fixmd(uint64_t a, uint64_t b)
{
    uint64_t c = a * b / 10000000000LL;
    return c;
}
#else
uint64_t fixmd(uint64_t a, uint64_t b)
{
    uint64_t c;

    asm (
    "mov %1, %%rax\n"
    "mul %2\n"
    "mov $10000000000, %%r8\n"
    "div %%r8\n"
    "mov %%rax, %0\n"
    : "=r" (c) 
    : "r" (a), "r" (b)
    : "rax", "rdx", "r8"
    );
    return c;
}
#endif

void main(void)
{
    uint64_t x = 12589254118LL;
    uint64_t y = fixmd(x, x);
    printf("x=0x%llX=%llu, y=0x%llX=%llu\n", x, x, y, y);
}
Run Code Online (Sandbox Code Playgroud)

如果#define USE_C_FUNC 1程序打印出错误的结果:

x=0x2EE60C5E6=12589254118, y=0x410F8719=1091536665
Run Code Online (Sandbox Code Playgroud)

如果#define USE_C_FUNC 0程序(使用内联汇编)打印正确的结果:

x=0x2EE60C5E6=12589254118, y=0x3B0AB8254=15848931924
Run Code Online (Sandbox Code Playgroud)

我在没有任何优化的情况下编译了这个例子(使用 gcc 选项 -O0)。

#include <stdio.h>
#include <stdint.h>

#define USE_C_FUNC 1

#if USE_C_FUNC != 0
uint64_t fixmd(uint64_t a, uint64_t b)
{
    uint64_t c = a * b / 10000000000LL;
    return c;
}
#else
uint64_t fixmd(uint64_t a, uint64_t b)
{
    uint64_t c;

    asm (
    "mov %1, %%rax\n"
    "mul %2\n"
    "mov $10000000000, %%r8\n"
    "div %%r8\n"
    "mov %%rax, %0\n"
    : "=r" (c) 
    : "r" (a), "r" (b)
    : "rax", "rdx", "r8"
    );
    return c;
}
#endif

void main(void)
{
    uint64_t x = 12589254118LL;
    uint64_t y = fixmd(x, x);
    printf("x=0x%llX=%llu, y=0x%llX=%llu\n", x, x, y, y);
}
Run Code Online (Sandbox Code Playgroud)

objdump -D uint64对于WRONG为函数fixmd结果()为:

x=0x2EE60C5E6=12589254118, y=0x410F8719=1091536665
Run Code Online (Sandbox Code Playgroud)

objdump -D uint64GOOD为功能fixmd)结果(是:

x=0x2EE60C5E6=12589254118, y=0x3B0AB8254=15848931924
Run Code Online (Sandbox Code Playgroud)

在 C 中 fixmd() 的代码中,问题很清楚:gcc 对无符号值使用有符号乘法 (imul),并用系数乘法(溢出)和二进制移位代替除法。看来,是bug吧?

dbu*_*ush 5

这不是一个错误。你的期望是不正确的。

无符号整数算术以该类型可以容纳的最大值 +1 为模进行。粗略地说,这意味着对于 64 位类型,任何溢出 64 位的内容都会被截断。

在您的特定情况下,您将乘以 12589254118 * 12589254118。其算术结果为 158,489,319,247,579,957,924。这比适合 64 位类型的要大,所以结果是模 2 64,得到 10,915,366,657,903,544,996,除以 10000000000 得到 1,091,536,665,与 C 代码生成的内容相匹配。

gcc 支持 128 位类型,因此您可以通过将其中一个操作数强制转换为__int128128 位来执行数学运算来解决此问题。

uint64_t c = (unsigned __int128)a * b / 10000000000LL;
Run Code Online (Sandbox Code Playgroud)

  • @tum_:阅读我之前的评论和链接的问答;GCC 不会尝试证明 `a * (unsigned __int128)b / 123456` 可以通过单个加宽-`mul` 来完成;如果 128/64 位除法结果不适合 64 位寄存器,则缩小“div”,但不带“div”的 #DE 错误。(C 抽象机不会,因为它正在执行 128 / 128 位 =&gt; 128 位除法)。GCC 仅调用 libgcc 辅助函数,并在 *that* 函数内检查高半部分是否为零。即使经过优化也是如此。 (2认同)