首页 > 解决方案 > 如何使用 ruby​​-msgpack gem 存储 32 位浮点数?

问题描述

我正在开发一个需要存储大量简单、可扩展数据的数据系统(以及我们正在内部开发的一些专业索引,而不是这个问题的一部分)。我预计将存储数十亿条记录,因此有效的序列化是系统的关键部分。序列化需要快速、节省空间并支持多种平台和语言(因为打包和解包这些数据将是客户端组件的职责,而不是存储系统的一部分)

数据类型实际上是带有可选键/值对的散列。键将是小整数(在应用层解释)。值可以是各种简单的数据类型——字符串、整数、浮点数。

作为技术选择,我们选择了MessagePack,我正在编写代码以通过 Ruby 的msgpack-ruby gem 执行数据序列化。

我不需要 Ruby 的 64 位浮点数的精度。即使在 32 位的限制下,存储的数字也没有有意义的精度。所以我想使用 MessagePack 对 32 位浮点值的支持。这绝对存在。但是,任何 64 位系统上的 Ruby 的默认行为是将 Float 序列化为 64 位:

MessagePack.pack(10.3)
 => "\xCB@$\x99\x99\x99\x99\x99\x9A"

查看 MessagePack 代码,似乎有一个方法MessagePack::Packer#write_float32,这符合我的期望:

MessagePack::DefaultFactory.packer.write_float32(10.3).to_s
 => "\xCAA$\xCC\xCD"

. . . 但我找不到设置默认打包程序或创建新打包程序的方法,在序列化更大的结构时将使用此方法。

作为对我理解能力的测试,我尝试了这个:

class Float
  def to_msgpack_ext
    packer.write_float32(self)
  end

  def self.from_msgpack_ext s
    unpacker.read(s)
  end
end

MessagePack::DefaultFactory.register_type(0, Float )

MessagePack.pack(10.3)
 => "\xCB@$\x99\x99\x99\x99\x99\x9A"

完全没有区别。. . 显然我遗漏或误解了 MessagePack 中使用的对象模型。我想做的事是否可行,我需要做什么?

标签: rubymsgpack

解决方案


覆盖浮点数

截至目前(版本 1.2.4 msgpack-ruby),这不可能以您尝试的确切方式:该msgpack_packer_write_value函数首先检查所有硬编码数据类型,并使用其默认实现处理它们。只有当当前对象不适合任何这些类型时,才会处理扩展。

换句话说:你不能用 覆盖默认的包格式MessagePack::DefaultFactory#register_type,调用这将只是一个空操作。

使用扩展

此外,无论如何,扩展机制并不是您所看到的。使用它,messagepack 将发出一个标记字节“这是一个扩展”,然后是扩展 ID(您的示例中的值“0”),然后是已经编码为 float32 的内容 - 或者您需要处理二进制编码/自己解码。

创建自己的 Float 类

原则上,您可以创建自己的FloatX类或其他任何东西,但这只是一个非常糟糕的举动:

  • Float没有new可以猴子补丁的方法,而且我知道在您编写代码时无法告诉 ruby​​ 创建一个FloatX实例10.3。因此,您必须在整个代码中手动创建对象,这可能会对性能产生严重影响。
  • 无论如何,您最终都会使用扩展机制,如上所示是不可行的。

覆盖的行为msgpack_packer_write_value

您将需要覆盖msgpack_packer_write_value. packer.c不幸的是,你不能在 ruby​​ 世界中这样做,因为没有为它定义等效的 ruby​​ 方法。所以不能使用通常的红宝石猴子补丁。

此外,该方法是从packer.c实现中的许多其他方法调用的,例如在负责写入数组或哈希的相应方法中。当然,那些也不会调用同名的 ruby​​ 方法,因为他们完全生活在他们的二进制世界中。

最后,虽然工厂机制的使用似乎意味着您可以以某种方式创建打包器的不同实现,但我没有看到任何证据表明这实际上是真的 - 阅读 Gem 的 C 代码,似乎没有任何规定种类。工厂似乎在那里处理 Gem 的 ruby​​<->C 交互。

现在怎么办

如果我在你的鞋子里,我会克隆那个 Gem 并修改msgpack_packer_write_valuepacker.c你希望的行为。检查case T_FLOAT并从那里继续。代码看起来很简单——它很快就会进入以下方法packer.h

static inline void msgpack_packer_write_float_value(msgpack_packer_t* pk, VALUE v)
{
    msgpack_packer_write_double(pk, rb_num2dbl(v));
}

...这当然是真正的罪魁祸首。

从另一个方向(write_float32您已经找到)接近它,可比较的代码是:

msgpack_packer_write_float(pk, (float)rb_num2dbl(numeric));

因此,如果您适当地替换该行msgpack_packer_write_float_value,您将完成。即使你不太喜欢 C 也应该是可行的。

之后,你给你的 Gem 一个单独的发布标签,自己构建它并在你的 gem 中指定它,Gemfile或者你管理你的 gem。


推荐阅读