【问题标题】:Most efficient standard-compliant way of reinterpreting int as float将 int 重新解释为 float 的最有效的标准兼容方式
【发布时间】:2014-01-12 19:37:43
【问题描述】:

假设我保证 float 是 IEEE 754 binary32。给定一个对应于有效浮点数的位模式,存储在std::uint32_t 中,如何以最有效的标准兼容方式将其重新解释为float

float reinterpret_as_float(std::uint32_t ui) {
   return /* apply sorcery to ui */;
}

我有几种方法我知道/怀疑/假设有一些问题:

  1. 通过reinterpret_cast

    float reinterpret_as_float(std::uint32_t ui) {
        return reinterpret_cast<float&>(ui);
    }
    

    或等效

    float reinterpret_as_float(std::uint32_t ui) {
        return *reinterpret_cast<float*>(&ui);
    }
    

    存在别名问题。

  2. 通过union

    float reinterpret_as_float(std::uint32_t ui) {
        union {
            std::uint32_t ui;
            float f;
        } u = {ui};
        return u.f;
    }
    

    这实际上是不合法的,因为它只允许从最近写给成员的内容中读取。然而,似乎有些编译器 (gcc) 允许这样做。

  3. 通过std::memcpy

    float reinterpret_as_float(std::uint32_t ui) {
        float f;
        std::memcpy(&f, &ui, 4);
        return f;
    }
    

    哪个 AFAIK 是合法的,但是复制单个单词的函数调用似乎很浪费,尽管它可能会被优化掉。

  4. 通过reinterpret_casting 到char* 并复制,

    float reinterpret_as_float(std::uint32_t ui) {
        char* uip = reinterpret_cast<char*>(&ui);
        float f;
        char* fp = reinterpret_cast<char*>(&f);
        for (int i = 0; i < 4; ++i) {
            fp[i] = uip[i];
        }
        return f;
    }
    

    哪个 AFAIK 也是合法的,因为 char 指针不受别名问题的影响,并且手动字节复制循环节省了可能的函数调用。循环肯定会被展开,但是 4 个可能单独的一字节加载/存储令人担忧,我不知道这是否可优化为单个四字节加载/存储。

4 是我能想到的最好的。

到目前为止我是对的吗?有没有更好的方法来做到这一点,特别是可以保证单次加载/存储的方法?

【问题讨论】:

  • 位模式从何而来?
  • memcpy 看起来 像一个函数调用,在优化的代码中尝试一下,看看结果,它可能会让你大吃一惊。 4 违反了严格的别名规则(你可以别名为char* 用于读取,但不能用于写入)
  • @DavidRodríguez-dribeas:关于 #4:真的吗?
  • @Manu343726 因为这不是 reinterpret_cast 的工作方式。 reinterpret_cast&lt;T&amp;&gt;(e) 相当于 *reinterpret_cast&lt;T*&gt;(&amp;e) 根据 5.2.10/11
  • @willj 根据 5.2.10/7 这实际上与 reinterpret_cast 完全相同

标签: c++ c++11 type-conversion language-lawyer standards-compliance


【解决方案1】:

Afaik,只有两种方法符合严格的别名规则:memcpy() 和通过复制转换为 char*。所有其他人从属于uint32_t 的内存中读取float,并且允许编译器在写入该内存位置之前执行读取。它甚至可以完全优化写入,因为它可以证明根据严格的别名规则永远不会使用存储的值,从而导致垃圾返回值。

这真的取决于编译器/优化 memcpy()char* 复制是否更快。在这两种情况下,智能编译器可能会发现它可以加载和复制 uint32_t,但在我在生成的汇编代码中看到它之前,我不相信任何编译器会这样做。

编辑:
在使用 gcc 4.8.1 进行一些测试后,我可以说 memcpy() 方法是这个特定编译器的最佳选择,详情请参见下文。


编译

#include <stdint.h>

float foo(uint32_t a) {
    float b;
    char* aPointer = (char*)&a, *bPointer = (char*)&b;
    for( int i = sizeof(a); i--; ) bPointer[i] = aPointer[i];
    return b;
}

使用gcc -S -std=gnu11 -O3 foo.c 会产生以下汇编代码:

movl    %edi, %ecx
movl    %edi, %edx
movl    %edi, %eax
shrl    $24, %ecx
shrl    $16, %edx
shrw    $8, %ax
movb    %cl, -1(%rsp)
movb    %dl, -2(%rsp)
movb    %al, -3(%rsp)
movb    %dil, -4(%rsp)
movss   -4(%rsp), %xmm0
ret

这不是最优的。

做同样的事情
#include <stdint.h>
#include <string.h>

float foo(uint32_t a) {
    float b;
    char* aPointer = (char*)&a, *bPointer = (char*)&b;
    memcpy(bPointer, aPointer, sizeof(a));
    return b;
}

产量(除-O0 之外的所有优化级别):

movl    %edi, -4(%rsp)
movss   -4(%rsp), %xmm0
ret

这是最佳选择。

【讨论】:

【解决方案2】:

如果整数变量中的位模式与有效的float 值相同,那么联合可能是最好的和最合规的方法。如果您阅读规范,这实际上是合法的(暂时不记得该部分)。

【讨论】:

  • 我不确定union 是正确的方法。 9.5/1 中的注释似乎暗示了相反的情况。另一方面,memcpy 是正确的。
  • 在 C 中(我意识到这是一种不同的语言),通过联合进行别名在技术上是不合法的(尽管普遍支持)。它在 C++ 中是否明确合法?
  • @OliCharlesworth:我没有发现它是合法的,只有一条评论似乎暗示它不合法。 [ 注意:为了简化联合的使用,我们做了一个特殊的保证:如果一个标准布局联合包含几个标准布局结构,它们共享一个共同的初始序列(9.2),并且如果这个标准的一个对象-布局联合类型包含标准布局结构之一,允许检查任何标准布局结构成员的公共初始序列;见 9.2。 —尾注] 明确允许这似乎意味着它被禁止。
  • @Joachim:您能否提供在标准中启用此功能的部分/引用?
  • @OliCharlesworth С 实际上确实允许这样做,请参阅 this
【解决方案3】:

memcpy 总是安全的,但确实涉及副本

强制转换可能会导致问题

联合 - 在 C99 和 C11 中似乎是允许的,不确定 C++

看看:

What is the strict aliasing rule?

Is type-punning through a union unspecified in C99, and has it become specified in C11?

【讨论】:

  • memcpy 中的副本无关紧要。 OP 中列出的所有选项均返回副本。
  • @cmaster - 在 gcc 中 memcpy 是一个内置函数,这可能允许编译器以不同的方式处理类型双关语。
  • cmaster - 不是每个人都使用 gcc,所以评论仍然相关。
  • 在这个answer 中,我链接到我认为关于类型双关语和C++ 的重要讨论。结论似乎是在实践中大多数编译器都支持它,但在标准中没有详细说明。
【解决方案4】:
float reinterpret_as_float(std::uint32_t ui) {
   return *((float *)&ui);
}

作为普通函数,它的代码被翻译成这样的程序集(Pelles C for Windows):

fld [esp+4]
ret

如果定义为inline 函数,那么这样的代码(n 为无符号,x 为浮点数):

x = reinterpret_as_float (n);

被翻译成汇编器是这样的:

fld [ebp-4]  ;RHS of asignment. Read n as float
fstp dword ptr [ebp-8]  ;LHS of asignment

【讨论】:

  • 这仅证明了 Pelles C for Windows 没有在此代码上利用严格的别名规则。您是否将语言标准设置为 C11?
  • 是的,它被配置为遵循C11规则(-std:C11)
  • std::uint32_t 在 C 中是什么意思?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-05-30
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多