C 和 x86 汇编中的自制“fabs”函数

Homemade 'fabs' function in C and x86 Assembly

提问人:Cuco 提问时间:9/10/2023 最后编辑:Cuco 更新时间:9/12/2023 访问量:167

问:

我正在尝试在 GNU C 中制作一个返回 32 位浮点数绝对值的 fabs 函数。我有三种不同的方法,分别称为 fabs1、fabs2 和 fabs3:

#include <math.h>
#include <stdio.h>

typedef union
{
    float v;
    struct
    {
        int mantissa : 23;
        int exponent : 8;
        int negative : 1;
    } b;
} components;

float fabs1(float f)
{
    return f >= 0.0 ? f : -f;
}

float fabs2(float f)
{
    components c;

    c.v = f;
    c.b.negative = 0;

    return c.v;
}

float fabs3(float f)
{
    double aux = f;
    unsigned short cw;

    __asm__
    (
        "finit;\
        fstcw %[cw];\
        andw $0xf0ff, %[cw];\
        orw $0x0200, %[cw];\
        fldcw %[cw];\
        fldl %[aux];\
        fabs;\
        fstpl %[aux];"
        : [aux] "=mr" (aux) : "m" (aux), [cw] "m" (cw)
    );

    return aux;
}

void main(void)
{
    printf("fabs(-189.55f) = %f\n", fabs(-189.55f));

    printf("fabs1(-189.55f) = %f\n", fabs1(-189.55f));
    printf("fabs2(-189.55f) = %f\n", fabs2(-189.55f));
    printf("fabs3(-189.55f) = %f\n", fabs3(-189.55f));
}

有三个不同的函数,一个使用简单的决策,一个使用联合的复杂一些,最后一个使用 x86 程序集。我正在用 Cygwin 32 位编译它:

C:/Developer/Cygwin/bin/i686-w64-mingw32-gcc -masm=att -I.. -std=c99 -o main.exe main.c

我在 Windows 11 中运行它,结果是:

fabs(-189.55f) = 189.550000
fabs1(-189.55f) = 189.550003
fabs2(-189.55f) = 189.550003
fabs3(-189.55f) = 189.550003

但它们真的应该是:

fabs(-189.55f) = 189.550000
fabs1(-189.55f) = 189.550000
fabs2(-189.55f) = 189.550000
fabs3(-189.55f) = 189.550000

你能看出其中的区别吗?在这三种情况下,我如何摆脱额外的 0.000003?

C 程序集 x86 浮点 常量

评论

3赞 Retired Ninja 9/10/2023
您能解释一下“它在 64 位工作正常,但在 32 位中不工作”的真正含义吗?错误?结果不正确?请提供详细信息。
5赞 Nate Eldredge 9/10/2023
ia64 指的是 Itanium,我怀疑它不是你所拥有的。常见的 64 位桌面体系结构称为 x86-64 或 amd64(或 Microsoft 的 x64)。但是,由于您编译为 32 位代码,因此您也没有使用它;这只是 x86。
5赞 Nate Eldredge 9/10/2023
我认为该版本可能能够将值直接提升为 ,因为是用参数和返回值定义的,然后你会得到最接近的。我必须仔细检查 C 的浮点文字规则。我怀疑如果您改用,您将获得与其他版本相同的结果。fabsdoublefabsdoubledouble-189.55fabsf
3赞 interjay 9/10/2023
你没有说问题出在哪里。
2赞 fuz 9/10/2023
我不确定你为什么要弄乱四舍五入模式。 切换符号位,则不应导致舍入。fabs

答:

0赞 Chris Dodd 9/12/2023 #1

导致 出现的基本问题是,数字不能以 -- 最接近的值是(十六进制),当在小数点后打印 6 位数字时,它是3189.550000float189.55000305175781250x1.7b199ap+7189.550003

编译器被允许以更高的精度执行操作,因此当您使用 (可能是内置的并返回 ) 时,您可能会获得值(使用双精度可以得到的最接近的值 -- 以十六进制为单位),但所有手写函数都返回 float 值fabsdouble189.550000000000011368683772161602973937988281250x1.7b1999999999ap+7189.5500030517578125

要摆脱 3,您可以:

  • 将一切变为精确double
  • 将输出更改为小数点后 5 个字符(格式为%.5f

然而,两者都不能解决IEEE二进制浮点数不能精确表示以10为基数的分数的根本问题,因此总是会出现四舍五入和不精确的情况。

评论

2赞 Eric Postpischil 9/12/2023
每次调用 OP 代码中的某个变体都会作为参数传递。这应该始终是相同的值,并且其中一个调用是 to 并且具有参数和返回类型这一事实应该是无关紧要的。 应该在传递给 之前生成一个值,并且传递给 不应更改该值。彼得·科德斯(Peter Cordes)已经发现了问题,即编译器缺陷。fabs-189.55ffloatfabsfabsdoubledouble-189.55ffloatfabsfabs
0赞 Chris Dodd 9/12/2023
@EricPostpischil 的精度只是一个最低限度——如果编译器愿意,它总是被允许以更高的精度计算事物,但这不是必需的。但这与OPs为什么出现(以及如何摆脱它)的问题无关,这是由于使用精度。float3float
0赞 Eric Postpischil 9/12/2023
C 标准明确规定,相同形式的浮点文字(源文本中的字符完全相同)的所有实例都必须转换为相同的值。C 2018 6.4.4.2 5:“......同一源形式的所有浮点常量应转换为具有相同值的相同内部格式。
0赞 Peter Cordes 9/12/2023
编译器被允许以更高的精度执行操作 - 是的,但这意味着任何操作的起点仍然必须是实际存在的,正如 Eric 所说。该文本的值为 类型 。要获得超出此范围的精度,就需要对 进行解析,并将其更改为将其解析为 ,正如 Eric 所说,这不是标准所说的应该发生的事情。godbolt.org/z/n53nsKhW6 表明,作为 printf 的参数,在尾数中具有预期的低零,但普通没有。189.55ffloatfloatfloatdouble(float)189.55f189.55f
1赞 Eric Postpischil 9/19/2023
@PeterCordes:呃。糟糕的借口。我预计该标准允许过高精度的原因是为了运行时性能 - 使用一个 FMA 而不是两个指令,在处理器上使用双精度指令而不使用单精度指令,等等。将该标准的计算自由度解释为适用于转换常数是值得怀疑的。我希望大多数有经验的浮点程序员会认为这是指一个特定的值,所以 GCC 的行为会让他们感到惊讶。189.55f