4

以下是我正在努力实现的目标:

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

我想这个问题包括两部分:

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

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

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

提前致谢

4

5 回答 5

10

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

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

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

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

具体来说,3.10/15 不允许我们通过 unsigned int 类型的左值访问浮点对象。我真的被这个咬了。我写的程序在开启优化后停止工作。显然,GCC 并不期望 float 类型的左值别名 int 类型的左值,这是 3.10/15 的合理假设。优化器在利用 3.10/15 的 as-if 规则下对指令进行了洗牌,并且它停止了工作。

在以下假设下

  • float 真的对应一个 32bit 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 编码将连续浮点值映射到连续整数(忽略 +/- 零)。这种映射实际上非常接近对数。

于 2010-10-02T15:52:04.337 回答
8

对于少数不寻常的 NaN 编码,盲目地丢弃浮点数的 2 个 LSB 可能会失败。

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

于 2010-10-02T16:14:06.100 回答
2

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

#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;
}

我不能保证它的便携性。

于 2010-10-02T17:14:42.913 回答
2

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

On the other hand, if you know the approximate range of values they're going to take, you should consider fixed point. They're more useful than most people realize.

于 2011-11-02T23:38:05.090 回答
1

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

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

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

于 2010-10-11T15:00:01.227 回答