Как хранить 32-битные числа с плавающей запятой, используя гем ruby-msgpack? - PullRequest
0 голосов
/ 05 сентября 2018

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

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

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

Мне не нужна точность 64-битного Float в 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. Возможно ли то, что я хочу сделать, и что мне нужно сделать?

Ответы [ 2 ]

0 голосов
/ 09 сентября 2018

Я знаю, что было бы неплохо использовать MessagePack.pack, но рубиновая прокладка очень тонкая. Он едва дает вам точку входа в библиотеку 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 для Hash, содержащего float; он просто возвращается к своим внутренним методам для упаковки ключей и значений хеша. Вот почему я сказал выше, что прокладка Ruby просто дает вам точку входа: основные расширения используются только для первоначального вызова.

Я думаю, что самое лучшее и простое решение - написать небольшую функцию сериализации, которая перебирает хеш в Ruby, используя MessagePack :: Packer API , чтобы делать то, что вы хотите, когда он видит float, и т. Д. Ноль взлома 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-битный float в 64-битный Float без каких-либо проблем с нашей стороны.

0 голосов
/ 07 сентября 2018

Переопределяющий поплавок

На данный момент (версия 1.2.4 из msgpack-ruby) это невозможно в точности, как вы пытались: функция msgpack_packer_write_value сначала проверяет все жестко закодированные типы данных и обрабатывает их с его реализацией по умолчанию. Расширения обрабатываются только в том случае, если текущий объект не соответствует ни одному из этих типов.

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

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

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

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

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

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

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

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

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

Наконец, хотя использование фабричного механизма, по-видимому, подразумевает, что вы можете каким-то образом создавать различные реализации упаковщиков, я не вижу никаких доказательств того, что это действительно так - читая C-код Gem, похоже, не предусмотрено что-нибудь в этом роде. Кажется, что фабрика работает с рубиновым <-> C взаимодействием 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 или, тем не менее, управляете своими драгоценными камнями.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...