Comments 21
Несколько неочевидно, что по крайней мере при сериализации в память (и, соответственно, в любом generic коде) нельзя написать просто
Serialize(out, v1); Serialize(out, v2); ...
а надо
out = Serialize(out, v1); out = Serialize(out, v2); ...
Может стоит [[nodiscard]] добавить с пояснением?
Зачем вам сырые указатели? Есть ли проверка на LE / BE? Будет ли работать с учётом возможного разного выравнивания в структурах?
В самой статье очень не хватает наглядных примеров использования.
Проверка LE/BE в данном случае не нужна, так как целочисленные значения сериализуются последовательно семибитными порциями со сдвигом вправо на 7 бит после каждого записанного байта:
- записываем-байт-как-младшие-семь-бит-и-старший-бит-если-есть-биты-старше
- сдвигаем-сериализуемое-значение-на-семь-бит-вправо
- если-не-ноль-повторяем-цикл
Это позволяет в большинстве случаев записывать компактнее. И это избавляет от проблем с записью на одной, а чтением на другой платформе.
Как сказано выше, для ряда классов из std:: сделаны специализации сериализатора, а для собственных классов следует прописывать функцию-член Serialize.
Согласен, примеров использования стоило бы добавить.
А куда вы восьмой бит записываете? Как я понял, с вашим подходом для записи 2 байт надо 19 бит, то есть 3 байта. Зачем такие сложности? Вы уверены что побитовая обработка положительно сказывается на быстродействии?
Если можно - сделайте еще бенчмарк и сравните что по размеру и производительности у вас в сравнении с flatbuffers.
Я, если честно, всё равно не понял, как оно корректно отработает если сериализовать на машине с LE и распаковать на машине с BE.
И всё таки, что насчёт сырых указателей?
А что не так с "сырыми" указателями?
Для записи числа, которое можно представить 2 байтами (0 - 65535) нужен 1 байт, если число меньше 128, 2 байта, если число меньше 16384 и три байта в остальных случаях. В моих задачах это даёт экономию размера примерно 7%, а для uint32_t и того больше. Впрочем, писать таким способом или иным - вопрос выбора.
Изначально это делалось для словарей, так что данные, созданные на одной платформе, должны верно читаться и на обратном порядке следования байт (см. словари libmorph). Порции для записи берутся арифметическими операциями (см. код https://github.com/big-keva/mtc/blob/557336ee9bfe935c4a7afe31a2a06ee8fa9ab189/serialize.h#L227).
Сравнивать же с flatbuffers или с protobuf некорректно - это совсем разный функционал. Этот код, конечно же, быстрее, чем flatbuffers/protobuf, компактнее, если не использовать компрессию (zlib) и не имеет и половины того функционала, что есть в названных библиотеках.
Повторюсь, это инструмент сериализации данных в виде, позволяющем работать с ними без явной десериализации, то есть гарантировать, например, отсутствие операций резервирования памяти при эксплуатации такого словаря.
В следующей статье я покажу работу сериализованного базисного дерева, небольшое (~10%) замедление при работе по сравнению с его развёрнутой формой при снижении требований к памяти более чем на порядок.
Не, я понимаю, можно на си писать. Но по поводу плюсов вот тут многое объясняется: https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#S-resource
В данном случае замечание про владение очень не к месту.
Требовать здесь unique_ или shared_ptr - это примерно как возвращать из vector<>::data() или string::c_str() вместо указателя - std::*_ptr(), право!
::Serialize никоим образом и ни в один момент времени не является владельцем объекта по указателю.
Приветствую.
Делал похожее, только с параметром "..." (template parameter pack). Получилось коротко довольно, но правда не заморачивался с байтовой обрезкой в завис от значения, как у вас.
Такой принцип:
в начале пишем общую длину
затем для скаляров (int, double..) сразу пишем значения
для массива пишем сначала размер, потом значения
для строк тоже сначала размер, потом значение
Напишу прямо тут немного кода:
Скрытый текст
template<typename... Fields>
class SerialReader{
public:
SerialReader(const std::string& m, Fields&... fields):
in_(m){
const int allType = intSz_;
inSize_ = int(m.size());
char* pData = (char*)m.data();
if (inSize_ < allType || inSize_ != *(int*)(pData)){
ok_ = false;
return;
}
offs_ = allType;
(readField(fields), ...);
}
bool ok()const{
return ok_;
}
private:
bool checkFieldSize(int fieldSize){
if (offs_ + fieldSize > inSize_){
ok_ = false;
}
return ok_;
}
void readField(std::string& s){
if (ok_){
if (!checkFieldSize(intSz_)) return;
const char* pData = in_.data();
int strSz = *((int*)(pData + offs_)); offs_ += intSz_;
if (!checkFieldSize(strSz)) return;
s = std::string(pData + offs_, strSz); offs_ += strSz;
}
}
void readField(int& v){
if (ok_){
if (!checkFieldSize(intSz_)) return;
const char* pData = in_.data();
v = *((int*)(pData + offs_)); offs_ += intSz_;
}
}
void readField(std::vector<int>& out){
if (ok_){
if (!checkFieldSize(intSz_)) return;
const char* pData = in_.data();
int vsz = *((int*)(pData + offs_)); offs_ += intSz_;
if (!checkFieldSize(vsz * intSz_)) return;
out.reserve(vsz);
memcpy(out.data(), pData + offs_, vsz * intSz_);
offs_ += vsz * intSz_;
}
}
const int intSz_ = 4;
int inSize_{};
int offs_ = 0;
bool ok_ = true;
const std::string& in_;
};
template<typename... Fields>
class SerialWriter{
public:
SerialWriter(const Fields&... fields){
(fieldSize(fields), ...);
outSize_ += intSz_;
out_.resize(outSize_);
writeField(outSize_);
(writeField(fields), ...);
}
std::string out(){
return out_;
}
private:
void fieldSize(const std::string& s){
outSize_ += intSz_ + s.size();
}
void fieldSize(int){
outSize_ += intSz_;
}
void fieldSize(const std::vector<int>& v){
outSize_ += intSz_ + int(v.size()) * intSz_;
}
void writeField(const std::string& s){
char* pOut = out_.data();
const auto ssz = s.size();
*((int*)(pOut + offs_)) = ssz; offs_ += intSz_;
memcpy(pOut + offs_, s.data(), ssz); offs_ += ssz;
}
void writeField(int v){
char* pOut = out_.data();
*((int*)(pOut + offs_)) = v; offs_ += intSz_;
}
void writeField(const std::vector<int>& arr){
char* pOut = out_.data();
const int asz = int(arr.size());
*((int*)(pOut + offs_)) = asz; offs_ += intSz_;
memcpy(pOut + offs_, arr.data(), asz * intSz_);
offs_ += asz * intSz_;
}
const int intSz_ = 4;
int outSize_{};
int offs_ = 0;
std::string out_;
};
Там только типы int и string, любые другие понятно думаю как добавить.
Теперь как этим пользоваться, пусть есть структура:
struct MyStruct{
std::string field1;
int field2{};
std::string field3;
std::vector<int> field4;
std::string serialn();
bool deserialn(const std::string& m);
}
Добавили 2 метода ей: serialn и deserialn.
Вот что внутри пишем:
std::string MyStruct::serialn(){
const auto out = SerialWriter(field1,
field2,
field3,
field4).out();
return out;
}
bool MyStruct::deserialn(const std::string& m){
const auto ok = SerialReader(m,
field1,
field2,
field3,
field4).ok();
return ok;
}
Здесь этот код находится, он правда в контексте конкретном, то есть не вынесен в общий.
в мире уже существует 100500 библиотек сериализации, с хорошей документацией, примерами, юнит тестами...
чем ваша то лучше? свой велосипед ближе к телу?
Эта была сделана в своё время для построения дампов словарных данных, по которым можно эффективно искать, не десериализуя.Где активно и используется (libmorph).
я ж не спрашиваю, для чего была сделана
я спрашиваю, зачем выкладывать
чтобы кто-то начал использовать код в таком несъедобном виде, он должен обладать какими-то супер уникальными свойствами
Выложил для того, чтобы было на что ссылаться при выкладке библиотеки полнотекстового поиска.
я спрашиваю, зачем выкладывать
Ну как зачем? Просто опытом поделится, вдруг кому-то будет полезно. Оно же тут не толкается, считаю пусть будет yet another статья или библиотека, чем не будет.
У меня вопрос возник по поводу функций GetBufLen и SkipToEnd. В общем случае ведь, чтобы понять размер буфера требуемый для хранения структуры или пропустить ее в общем буфере. Нужно пройтись вглубь по всей структуре, разве это не будет равносильно по коду и времени сериализации/десериализации?
Пример есть класс, в котором вектор каких-то объектов.
Чтобы узнать размер буфера требуемого для хранения объекта этого класса, нужно узнать размер вектор, который известен, только в рантайме, и посетить каждый елемент этого вектора, так как внутри тоже могут быть динамические объекты.
При пропуске, нужно прочитать из буфера размер вектора, затем прочитать каждый элемент, потому как из-за динамических объектов внутри, каждый элемент может быть разного размера и сколько пропускать не понятно.
Дополнительно про упакованные числа. Чтобы понять сколько байт нужно пропустить для числа в буфере, его ведь тоже нужно прочитать сначала?
Да, вы совершенно правы: если нужна оценка размера некоторой массивной и кучерявой структуры данных, то GetBufLen проделывает весь путь Serialize, а время на её исполнение пропорционально времени Serialize с коэффициентом, равным отношению времени операции суммирования ко времени операции записи в конкретный коллектор.
С другой стороны, такая оценка нужна ровно в двух случаях: либо если вы собираетесь сделать дамп в памяти и хотите понять, сколько её надо резервировать, либо если вам надо строить внутренние таблицы релокации внутри такого дампа.
С другой стороны, такая оценка нужна ровно в двух случаях: либо если вы собираетесь сделать дамп в памяти и хотите понять, сколько её надо резервировать, либо если вам надо строить внутренние таблицы релокации внутри такого дампа.
Ну звучит как достаточно важная оптимизация, но потенциально время самого GetBufLen может ее нивелировать.
Ещё одна сериализация для C++