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

时间:2018-09-05 10:21:55

标签: ruby msgpack

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

数据类型实际上是具有可选键/值对的哈希。键将是小整数(在应用程序层解释)。值可以是各种简单的数据类型-字符串,整数,浮点数。

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

我不需要Ruby的64位Float的精度。所存储的数字甚至没有精确到32位的限制。因此,我想对32位浮点值使用MessagePack支持。这确实存在。但是,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中使用的对象模型。我想做些什么,我需要做些什么?

2 个答案:

答案 0 :(得分:3)

覆盖浮动

到目前为止(msgpack-ruby的1.2.4版本)不可能以您尝试的完全相同的方式进行:msgpack_packer_write_value函数首先检查所有硬编码的数据类型,并使用其默认实现。仅当当前对象不适合这些类型中的任何一种时。

换句话说:您无法使用MessagePack::DefaultFactory#register_type覆盖默认的包格式,将其称为无操作即可。

使用扩展名

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

创建自己的Float类

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

  • Float没有可以进行猴子修补的new方法,当您在代码中编写FloatX时,我无法告诉红宝石创建一个10.3实例。因此,您必须在整个代码中手动创建对象,这可能会对性能产生严重影响。
  • 无论如何,最终还是会遇到扩展机制,如上所示。

覆盖msgpack_packer_write_value

的行为

您需要覆盖msgpack_packer_write_value的{​​{1}}实现。不幸的是,在红宝石世界中您无法做到这一点,因为没有为其定义任何等效的红宝石方法。因此无法使用通常的红宝石猴子修补程序。

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

最后,虽然工厂机制的使用似乎暗示您可以以某种方式创建打包程序的不同实现,但我看不到任何证据证明这是真的-阅读Gem的C代码,似乎没有任何规定任何类似的东西。工厂似乎在那里处理宝石的红宝石<-> C相互作用。

现在怎么办

如果我穿上鞋子,我会克隆该宝石并修改packer.c中的msgpack_packer_write_value以使其表现出您想要的效果。检查packer.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)); 中适当地替换了该行,就可以完成。即使您不太喜欢C,也应该可行。

然后,给您的宝石一个单独的发行标签build it yourself,并在msgpack_packer_write_float_value中指定它,或者由您管理自己的宝石。

答案 1 :(得分:3)

我知道使用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"

但是,当您在包含浮点数的Hash上调用#to_msgpack时,这会分崩离析;它只是恢复为其内部方法来打包哈希键和值。这就是为什么我在上面说Ruby填充程序只是给您一个切入点:核心扩展仅用于初始调用。

我认为最好,最简单的解决方案是编写一个小的序列化函数,该序列化在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位浮点数,而不会引起我们的任何麻烦。