Как хранить 32-битные числа с плавающей запятой с помощью драгоценного камня ruby-msgpack?

Я работаю над системой данных, в которой необходимо хранить большие объемы простых, расширяемых данных (наряду с некоторыми специальными индексами, которые мы разрабатываем самостоятельно, и это не является частью этого вопроса). Я ожидаю, что там будут храниться миллиарды записей, поэтому эффективная сериализация является ключевой частью системы. Сериализация должна быть быстрой, компактной и поддерживаться на нескольких платформах и языках (поскольку за упаковку и распаковку этих данных будет отвечать клиентский компонент, а не часть системы хранения).

Тип данных фактически является хэшем с необязательными парами ключ/значение. Ключи будут представлять собой небольшие целые числа (интерпретируемые на прикладном уровне). Значения могут быть различными простыми типами данных — String, Integer, Float.

В качестве технологии мы выбрали MessagePack, и я пишу код для выполнения сериализации данных с помощью msgpack-ruby.

Мне не нужна точность 64-битного числа с плавающей запятой Ruby. Ни одно из сохраняемых чисел не имеет значимой точности даже до 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. Возможно ли то, что я хочу сделать, и что мне нужно сделать?


person Neil Slater    schedule 05.09.2018    source источник


Ответы (2)


Я знаю, что было бы неплохо использовать MessagePack.pack, но оболочка Ruby очень тонкая. Это едва ли дает вам точку входа в библиотеку C (или Java). И, как указал AnoE, я думаю, что вы можете настраивать to_msgpack_ext и self.from_msgpack_ext только для зарегистрированных типов, а не для встроенных типов.

Другая проблема с вашей попыткой заключается в том, что у вас нет доступа к packer и unpacker из этих методов. Я думаю, вам просто нужно было бы использовать Array#pack и String#unpack, даже если бы вы могли найти способ заставить библиотеку вызывать ваши методы. Чтобы получить дескриптор упаковщика, вам нужно переопределить другой метод:

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

А затем назовите его соответствующим образом (см. код, почему):

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

Однако это разваливается, когда вы вызываете #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-битное число с плавающей запятой без каких-либо действий с нашей стороны.

person mwp    schedule 09.09.2018

Переопределение поплавка

На данный момент (версия 1.2.4 msgpack-ruby) это невозможно именно так, как вы пробовали: Функция /packer.c#L139" rel="nofollow noreferrer">msgpack_packer_write_value сначала проверяет все жестко закодированные типы данных и обрабатывает их в своей реализации по умолчанию. Расширения обрабатываются только в том случае, если текущий объект не соответствует ни одному из этих типов.

Другими словами: вы не можете переопределить форматы пакетов по умолчанию с помощью MessagePack::DefaultFactory#register_type, вызов этого просто не будет работать.

Использование расширений

Кроме того, в любом случае механизм расширения — это не то, на что вы смотрите. Используя это, пакет сообщений будет выдавать байт маркера «это расширение», за которым следует идентификатор расширения (значение «0» в вашем примере), за которым следует то, что уже закодировано как float32 - в качестве альтернативы вам нужно будет обрабатывать двоичное кодирование /расшифровка себя.

Создание собственного класса Float

В принципе, вы могли бы создать свой собственный класс FloatX или что-то еще, но это просто очень плохой ход:

  • Float не имеет метода new, который вы могли бы пропатчить, и я не знаю, как заставить Ruby создать экземпляр FloatX, когда вы пишете 10.3 в своем коде. Таким образом, вам придется создавать объекты вручную во всем коде, что, вероятно, серьезно повлияет на производительность.
  • В любом случае вы получите механизм расширения, что невозможно, как показано выше.

Переопределение поведения msgpack_packer_write_value

Вам нужно будет переопределить msgpack_packer_write_value реализацию packer.c. К сожалению, вы не можете сделать это в мире ruby, поскольку для него не определен эквивалентный метод ruby. Таким образом, обычное исправление ruby ​​​​не может быть использовано.

Кроме того, метод вызывается из множества других методов внутри реализации packer.c, например, в соответствующих методах, отвечающих за запись массивов или хэшей. Те, конечно, тоже не назвали бы рубиновый метод с таким же именем, так как они полностью живут в своем бинарном мире.

Наконец, хотя использование фабричного механизма, кажется, подразумевает, что вы можете каким-то образом создавать различные реализации упаковщиков, я не вижу доказательств того, что это действительно так - читая C-код Gem, кажется, что ничего из этого не предусмотрено. своего рода. Похоже, что фабрика предназначена для обработки взаимодействия ruby‹->C Gem.

Что теперь

Если бы я был на вашем месте, я бы клонировал этот Gem и изменил 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));

Поэтому, если вы замените эту строку в msgpack_packer_write_float_value соответствующим образом, все будет готово. Должно быть выполнимо, даже если вы не так сильно разбираетесь в C.

После этого вы присваиваете Gem индивидуальный тег выпуска, создаете его самостоятельно и укажите это в своем Gemfile или как вы управляете своими драгоценными камнями.

person AnoE    schedule 07.09.2018