以下是我想要实现的目标:

  • 我需要将 32 位 IEEE 浮点数打包为 30 位。
  • 我想通过将尾数的大小减少 2 位来实现此目的。
  • 操作本身应该尽可能快。
  • 我知道会损失一些精度,这是可以接受的。
  • 如果此操作不会破坏 SNaN、QNaN、无穷大等特殊情况,那将是一个优势。但我已经准备好为了速度而牺牲这一点。

我想这个问题由两部分组成:

1)我可以简单地清除尾数的最低有效位吗?我已经尝试过了,到目前为止它有效,但也许我是在自找麻烦......就像是:

float f;
int packed = (*(int*)&f) & ~3;
// later
f = *(float*)&packed;

2)如果存在 1)失败的情况,那么实现这一目标的最快方法是什么?

提前致谢

有帮助吗?

解决方案 4

我无法选择任何答案作为明确的答案,因为它们中的大多数都有有效的信息,但不完全是我想要的。所以我只是总结一下我的结论。

根据 C++ 标准,我在问题的第 1) 部分中发布的转换方法显然是错误的,因此应该使用其他方法来提取浮点数。

最重要的是...据我通过阅读有关 IEEE754 浮点数的响应和其他来源了解到,可以从尾数中删除最低有效位。它主要只会影响精度,但有一个例外:sNaN。由于 sNaN 由设置为 255 的指数表示,并且尾数 != 0,因此可能存在尾数 <= 3 的情况,并且删除最后两位会将 sNaN 转换为 +/-Infinity。但由于 sNaN 不是在 CPU 浮点运算期间生成的,因此在受控环境下是安全的。

其他提示

实际上,这些重新解释的强制转换违反了严格的别名规则(C++ 标准第 3.10 节)。当您打开编译器优化时,这可能会在您面前爆炸。

C++ 标准,第 3.10 节第 15 段说:

如果程序尝试通过以下类型之一以外的左值访问对象的存储值,则行为未定义

  • 对象的动态类型,
  • 对象动态类型的 cv 限定版本,
  • 类似于对象的动态类型的类型,
  • 与对象的动态类型相对应的有符号或无符号类型,
  • 与对象动态类型的 cv 限定版本相对应的有符号或无符号类型,
  • 聚合或联合类型,其成员中包含上述类型之一(递归地包括子聚合或包含联合的成员),
  • 是对象动态类型的(可能是 cv 限定的)基类类型的类型,
  • char 或 unsigned char 类型。

具体来说,3.10/15 不允许我们通过 unsigned int 类型的左值访问 float 对象。其实我自己也被这个咬过。我写的程序在打开优化后停止工作。显然,GCC 并不期望 float 类型的左值对 int 类型的左值进行别名,这在 3.10/15 中是一个合理的假设。优化器根据利用 3.10/15 的 as-if 规则对指令进行了改组,并且停止工作。

在以下情况下 假设

  • float 实际上对应于 32 位 IEEE-float,
  • sizeof(float)==sizeof(int)
  • unsigned int 没有填充位或陷阱表示

你应该能够这样做:

/// returns a 30 bit number
unsigned int pack_float(float x) {
    unsigned r;
    std::memcpy(&r,&x,sizeof r);
    return r >> 2;
}

float unpack_float(unsigned int x) {
    x <<= 2;
    float r;
    std::memcpy(&r,&x,sizeof r);
    return r;
}

这不会受到“3.10 违规”的影响,并且通常速度非常快。至少 GCC 将 memcpy 视为一个内在函数。如果您不需要这些函数来处理 NaN、无穷大或具有极高数量级的数字,您甚至可以通过将“r >> 2”替换为“(r+1) >> 2”来提高准确性:

unsigned int pack_float(float x) {
    unsigned r;
    std::memcpy(&r,&x,sizeof r);
    return (r+1) >> 2;
}

即使由于尾数溢出而改变指数,该方法也有效,因为 IEEE-754 编码将连续的浮点值映射到连续的整数(忽略 +/- 0)。该映射实际上非常接近对数。

对于少量异常 NaN 编码,盲目删除浮点数的 2 个 LSB 可能会失败。

NaN 被编码为 exponent=255, mantissa!=0,但 IEEE-754 没有说明应使用哪些 mantissa 值。如果尾数值 <= 3,您可以将 NaN 变成无穷大!

您应该将其封装在一个结构中,这样您就不会意外地将标记浮点数与常规“unsigned int”的使用混合在一起:

#include <iostream>
using namespace std;

struct TypedFloat {
    private:
        union {
            unsigned int raw : 32;
            struct {
                unsigned int num  : 30;  
                unsigned int type : 2;  
            };
        };
    public:

        TypedFloat(unsigned int type=0) : num(0), type(type) {}

        operator float() const {
            unsigned int tmp = num << 2;
            return reinterpret_cast<float&>(tmp);
        }
        void operator=(float newnum) {
            num = reinterpret_cast<int&>(newnum) >> 2;
        }
        unsigned int getType() const {
            return type;
        }
        void setType(unsigned int type) {
            this->type = type;
        }
};

int main() { 
    const unsigned int TYPE_A = 1;
    TypedFloat a(TYPE_A);

    a = 3.4;
    cout << a + 5.4 << endl;
    float b = a;
    cout << a << endl;
    cout << b << endl;
    cout << a.getType() << endl;
    return 0;
}

但我不能保证它的便携性。

您需要多少精度?如果 16 位浮点就足够了(对于某些类型的图形来说足够了),那么 ILM 的 16 位浮点(“一半”)(OpenEXR 的一部分)就很棒,遵守各种规则(http://www.openexr.com/ ),将其打包到结构中后,您将剩余足够的空间。

另一方面,如果您知道它们将采用的值的大致范围,则应该考虑定点。它们比大多数人意识到的更有用。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top