如何使用 ruby-msgpack gem 存储 32 位浮点数?

How to store 32-bit floats using ruby-msgpack gem?

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

数据类型实际上是一个带有可选 key/value 对的散列。密钥将是小整数(在应用层解释)。值可以是各种简单的数据类型 - String、Integer、Float。

作为技术选择,我们选择了MessagePack, and I am writing code to perform data serialisation via Ruby's msgpack-rubygem。

我不需要 Ruby 的 64 位浮点数的精度。 None 所存储的数字即使在 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 中使用的对象模型的某些内容。我想做的事是否可行,我需要做什么?

覆盖浮动

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

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

使用扩展程序

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

创建自己的 Float class

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

  • Float 没有 new 方法,你可以用 monkeypatch,我知道没有办法告诉 ruby 在你写 [= 时创建一个 FloatX 实例19=] 在你的代码中。因此,您必须在整个代码中手动创建对象,这可能会对性能产生严重影响。
  • 无论如何你都会得到扩展机制,如上所示是不可行的。

覆盖 msgpack_packer_write_value

的行为

您需要覆盖 packer.cmsgpack_packer_write_value 实现。不幸的是,您不能在 ruby 世界中这样做,因为没有为它定义等效的 ruby 方法。所以通常的 ruby 的 monkeypatching 不能使用。

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

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

现在怎么办

如果我处在你的位置,我会克隆 Gem 并修改 packer.c 中的 msgpack_packer_write_value 以按照你的意愿运行。检查 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 一个单独的发布标签,build it yourself 并在你的 Gemfile 中指定它,或者你管理你的宝石。

我知道使用 MessagePack.pack 会很好,但是 Ruby 垫片非常薄。它几乎没有给你一个进入 C(或 Java)库的入口点。正如 AnoE 指出的那样,我认为您只能为注册类型自定义 to_msgpack_extself.from_msgpack_ext,而不是内置类型。

您尝试的另一个问题是您无权从这些方法访问 packerunpacker。我认为,您只需要使用 Array#packString#unpack,即使您可以想出一种方法让库调用您的方法。要获得加壳器的句柄,您必须覆盖不同的方法:

class Float
  private
  def to_msgpack_with_packer(packer)
    packer.write_float32 self
    packer
  end
end

然后适当地调用它(参见 this code 为什么):

10.3.to_msgpack(MessagePack::Packer.new).to_s # => "\xCAA$\xCC\xCD"

但是,当您在包含浮点数的散列上调用 #to_msgpack 时,这会崩溃;它只是恢复到其内部方法来打包散列键和值。这就是为什么我在上面说 Ruby shim 只是给你一个入口点:核心扩展只用于初始调用。

我认为最好、最简单的解决方案是编写一个小的序列化函数来遍历 Ruby 中的哈希,使用 MessagePack::Packer API 在看到浮点数时执行您想要的操作,等等. 零 C-hacking,零猴子补丁,零混淆,当有人试图在六个月内阅读您的代码时。

def pack_float32(obj, packer=MessagePack::Packer.new)
  case obj
  when Hash
    packer.write_map_header(obj.size)
    obj.each_pair do |key, value|
      pack_float32(value, pack_float32(key, packer))
    end
  when Enumerable
    packer.write_array_header(obj.size)
    obj.each do |value|
      pack_float32(value, packer)
    end
  when Float
    packer.write_float32(obj)
  else
    packer.write(obj)
  end

  packer
end

pack_float32(1=>[10.3]).to_s # => "\x81\x01\x91\xCAA$\xCC\xCD"

显然这没有经过严格测试,它可能无法处理所有边缘情况,但希望它足以让您入门。

另一条注意事项:您不必担心拆包。 msgpack-ruby 似乎正确地将 32 位浮点数解包为 64 位浮点数,而我们没有任何改动。