【问题标题】:How to store 32-bit floats using ruby-msgpack gem?如何使用 ruby​​-msgpack gem 存储 32 位浮点数?
【发布时间】:2019-02-10 11:04:17
【问题描述】:

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

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

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

我不需要 Ruby 的 64 位浮点数的精度。即使在 32 位的限制下,存储的数字也没有有意义的精度。所以我想使用 MessagePack 对 32 位浮点值的支持。这绝对存在。然而,Ruby 在任何 64 位系统上的默认行为是将 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 中使用的对象模型。我想做的事是否可行,我需要做什么?

【问题讨论】:

    标签: ruby msgpack


    【解决方案1】:

    我知道使用 MessagePack.pack 会很好,但是 Ruby shim 非常薄。它几乎没有为您提供进入 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"
    

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

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

    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 位浮点数,而无需我们进行任何摆弄。

    【讨论】:

      【解决方案2】:

      覆盖浮点数

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

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

      使用扩展

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

      创建自己的 Float 类

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

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

      覆盖msgpack_packer_write_value 的行为

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

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

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

      现在怎么办

      如果我处于你的位置,我会克隆该 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 中指定它,或者您管理您的 gem。

      【讨论】: