【问题标题】:Can a static_cast<float> from double, assigned to double be optimized away?可以从 double 分配给 double 的 static_cast<float> 进行优化吗?
【发布时间】:2018-11-02 23:07:42
【问题描述】:

我偶然发现了一个我认为不必要的功能,并且通常会吓到我:

float coerceToFloat(double x) {
    volatile float y = static_cast<float>(x);
    return y;
}

然后这样使用:

// double x
double y = coerceToFloat(x);

这与仅仅这样做有什么不同吗?:

double y = static_cast<float>(x);

其意图似乎是将双精度降低为单精度。闻起来像是出于极度的偏执狂写的东西。

【问题讨论】:

  • 不,没有区别。至于原因,这真的不是我们可以推测的(尤其是在没有更多上下文的情况下)。您必须询问原作者。
  • 我不知道代码作者为什么使用volatile 变量。据我所知,该功能与float coerceToFloat(double x) { return static_cast&lt;float&gt;(x); } 没有什么不同。
  • 我的意思是,给这个操作一个名字是个好习惯。 coerceToFloat 的意图肯定比普通的静态转换要明确得多。挥发性的……嗯。也许是为了调试?
  • 我可能找到了面包屑。 This 表示使用 volatile 可以分解浮点运算。也许作者出于同样的原因使用它,它强制编译器在此处截断,而不是优化它并且不吐出中间结果。
  • 哦! @NathanOliver ,然后 volatile 用于防止可能阻止代码执行缩进的优化!一个惊人的发现!

标签: c++ casting floating-point


【解决方案1】:

需要static_cast&lt;float&gt;(x) 来删除任何多余的精度,从而生成float。虽然 C++ 标准通常允许实现在表达式中保留多余的浮点精度,但该精度必须由强制转换和赋值运算符删除。

使用更高精度的许可在 C++ 草案 N4659 第 8 条第 13 段中:

浮动操作数的值和浮动表达式的结果可以用更大的形式表示 精度和范围超出类型要求;类型不会因此而改变。64

脚注 64 说:

强制转换和赋值运算符仍必须按照 8.4、8.2.9 和 8.18 中的说明执行其特定转换。

【讨论】:

  • 我不确定这是否正确。脚注是非规范性的。也许这个脚注的意图是说通过强制转换/分配,可以更改类型。但是多余的精度可以保留。这是实现的质量问题,因此编译器可能会降低额外的精度,但他们不需要这样做。我并不是说这是正确的解释,但对我来说似乎是合理的。
  • @geza:脚注是非规范性的,但这种行为在 C 和 C++ 中是众所周知的;还有其他讨论它的 Stack Overflow 问题和答案。虽然脚注是非规范性的,但它告诉我们关于强制转换运算符的规范性文本的正确解释是它们确实执行了他们所说的转换,,从double 的转换到float 实际上会产生float
  • @EricPostpischil:我认为作者认为标准是否在技术上允许与普遍预期相反但可能对某些目的有用的行为并不特别重要。如果该标准将禁止异常行为,则旨在从中显着受益的质量实现应同时支持以普遍预期方式表现的符合模式和以异常模式表现的不符合模式,但即使标准允许行为实现应该仍然提供相同的两种模式。
  • 不幸的是,在很多情况下编译器都不会这样做,尤其是当他们开始内联内容时,您可能会碰巧最终在扩展浮点寄存器中完成计算,结果甚至会超过 long双精度(通常为一位)。对我来说,经历 volatile 证明是唯一可靠的方法。
  • @PlasmaHH:请向我们展示一个代码示例,其中的实现在执行强制转换或赋值以及由该实现生成的程序集时不会消除多余的精度。
【解决方案2】:

跟进@NathanOliver 的评论——允许编译器以高于操作数类型所需的精度进行浮点数学运算。通常在 x86 上,这意味着它们将所有内容都作为 80 位值执行,因为这是硬件中最有效的。只有当一个值被存储时,它才必须恢复为类型的实际精度。即便如此,默认情况下,大多数编译器都会执行违反此规则的优化,因为强制更改精度会减慢浮点运算。大多数时候没关系,因为额外的精度是无害的。如果您是个顽固的人,您可以使用命令行开关来强制编译器遵守该存储规则,您可能会发现浮点计算明显变慢了。

在该函数中,标记变量volatile 告诉编译器它不能省略存储该值;反过来,这意味着它必须降低传入值的精度以匹配它存储的类型。所以希望这会强制截断。

而且,不,编写强制转换而不是调用该函数是不一样的,因为编译器(在其非一致性模式下)如果确定它可以生成更好的代码而不存储该值,它也可以跳过截断。请记住,我们的目标是尽可能快地运行浮点计算,而必须处理关于降低中间值精度的琐碎规则只会减慢速度。

在大多数情况下,通过跳过中间截断来完全运行是严肃的浮点应用程序所需要的。要求对存储进行截断的规则与其说是现实要求,不如说是一种希望。

附带说明,Java 最初要求所有浮点数学运算都以所涉及类型所需的精确精度完成。您可以通过告诉它不要将 fp 类型扩展到 80 位来在 Intel 硬件上做到这一点。这引起了数字计算者的强烈抱怨,因为这会使计算大大变慢。 Java 很快就变成了“严格”​​fp 和“非严格”fp 的概念,严重的数字运算使用非严格的,即,使其与硬件支持的速度一样快。彻底了解浮点数学的人(包括我)想要速度,并且知道如何处理结果的精度差异。

【讨论】:

  • 彻底了解浮点数学的人 这也不包括我。感谢您接受我的简介并变成连贯的答案 +1
  • “通常在 x86 上,这意味着它们以 80 位值执行所有操作,因为这是硬件中最高效的”是值得怀疑的。一些编译器过去可能使用了 80 位浮点寄存器,但处理器设计已经向前发展,现在使用这些旧寄存器及其操作存在缺点。
  • @EricPostpischil 我还没有看到符合标准的实现。很多时候,他们会不符合标准以更好地表现,并带有一个可以按照标准所说的标志,但要付出代价。
  • @NathanOliver:请向我们展示一个代码示例,其中的实现在执行强制转换或赋值以及由该实现生成的程序集时不会消除多余的精度。
  • @supercat -- 叹息;下兔子洞。 Java 中的 Strict-fp 要求涉及两个双精度的计算以双精度进行。它不允许以 80 位进行计算并将结果四舍五入;这可能会产生不同的结果。
【解决方案3】:

一些编译器有这种“扩展精度”的概念,其中双精度数携带超过 64 位的数据。这会导致浮点计算不符合 IEEE 标准。

上面的代码可能是为了防止编译器上的扩展精度标志消除精度损失。这样的标志明显违反了双精度和浮点值的精度假设。他们不会在 volatile 变量上这样做似乎是合理的。

【讨论】:

    【解决方案4】:

    无论是否允许优化此类转换,它确实会发生,并且 volatile 分配会阻止它发生。

    例如,MSVC 使用/Ox /fp:fast 编译为 32 位(因此使用 x87):

    _x$ = 8                                       ; size = 8
    float uselessCast(double) PROC                         ; uselessCast
            fld     QWORD PTR _x$[esp-4]
            ret     0
    float uselessCast(double) ENDP                         ; uselessCast
    
    _y$ = 8                                       ; size = 4
    _x$ = 8                                       ; size = 8
    float coerceToFloat(double) PROC                   ; coerceToFloat
            fld     QWORD PTR _x$[esp-4]
            fstp    DWORD PTR _y$[esp-4]
            fld     DWORD PTR _y$[esp-4]
            ret     0
    float coerceToFloat(double) ENDP 
    

    uselessCast 如下,coerceToFloat 在问题中。

    float uselessCast(double x)
    {
        return static_cast<float>(x);
    }
    

    类似地,GCC 和 Clang 与 -O3 -ffast-math -m32 -mfpmath=387

    uselessCast(double):
        fld     QWORD PTR [esp+4]
        ret
    coerceToFloat(double):
        sub     esp, 20
        fld     QWORD PTR [esp+24]
        fstp    DWORD PTR [esp+12]
        fld     DWORD PTR [esp+12]
        add     esp, 20
        ret
    

    Godbolt link for all the above

    当然,您可能会争辩说,对于 /fp:fast-ffast-math,您无论如何都不应该期望浮点运算有任何作用,但您可能需要它,但仍然能够丢弃多余的精度。

    【讨论】:

    • 我可以看到允许程序员在将值存储到寄存器限定的自动对象时允许编译器放弃截断/舍入的有用性。对于小的有符号整数类型和浮点类型都是如此(因此在register int8_t foo=127; foo = foo+1; 之后,foo 的值可以在编译器空闲时存储+128 或-128)。请注意,这样的分配不是 UB。但是,如果让编译器也将它们应用于诸如强制截断之类的强制转换之类的东西,程序员使用此类豁免的能力会受到损害。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-07-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多