Repository navigation
What would a "FlatBuffers2" binary format look like? #5875
Description
FlatBuffer's binary format has been set in stone for 6.5 years now, because we value binary forwards/backwards compatibility highly, and because we have a large investment in 15? or so language implementations / parsers etc. that would not be easy to redo.
So "V2" in the sense of a new format that breaks backwards compatibility may never happen. But there is definitely a list of issues with the existing format that if a new format were to ever happen, would be nice to address. I realized I never made such a list. It would be nice to at least fantasize what such a format could look like :)
Please comment what you would like to see. Note that this list is purely for things that would change the binary encoding, or larger additions to the binary encoding. Anything that can be solved with code / new APIs outside of the binary format does not belong on this list.
- Remove all padding. Modern CPUs can access unaligned data at normal speed. This would shrink the format somewhat, and encourage other variable size things. If anyone ever needs padding to be compatible with a C struct, explicit padding can always be added, or it can be an opt-in feature.
- Make unions into a single field (and vectors of them into a single value). Also make the type part 16-bit while we're at it, so a union is always a 6-byte struct.
- Remove the 2nd field of the vtable. This field stores the table size, but it is never used in any implementations. This was intended for a streaming API that never happened.
- Allow different size vtable offsets. Currently they're always 16-bit, but for small tables 8-bit would be feasible. Since we code-gen vtable access, this would come at no cost. Use with care of course, because once you choose this smaller size you can't undo it when your table grows.
- Allow inline vtables when we determined they're unlikely to be shared. Saves an offset.
- Allow inline strings, vectors (and maybe scalars), meaning a vtable offset would refer directly to the string, rather than to the string offset. Saves the offset. Of course puts more pressure on the vtable offset size, so use with case. Similarly, could even do inline scalars of all small scalar types. Of course this makes it more likely that vtables are unequal, so this is a tradeoff.. would work well with inline vtables.
- Remove 0-termination of strings. Only C/C++ care for this, and C++ has been moving toward
string_viewrecently, and both have been usingsize_targuments for a long time rather than relying onstrlen. Other languages don't use it. For passing to super-old C APIs that expect 0-termination, either swap the terminating byte temporarily while passing that string, or copy. - Allow 8 and 16 bit size fields on strings and vectors, currently they're always 32. Good for small strings. Combine all the string optimisations above together, and the string
"a"goes from 12 bytes (2 vtable + 4 offset + 4 size + 1 string + 1 terminator) to 3 bytes (1 vtable + 1 size + 1 string). Of course this very inflexible and special purpose, but gives users more options for compact data storage. Again, like all format variation above, this comes at no runtime cost, just some codegen complexity. - Construct the buffer forwards (rather than backwards like currently all implementations). This simplifies a lot of code and buffer management. Unsigned child offsets would now always point downwards in memory. Downside: must now detect table fields pointing to the table itself.
- Always have a file_identifer, and make it the first thing in the buffer? Always have a length field as well?
- Support 64-bit offsets from the start. They would be optional for vectors and certain other things, allowing buffers >2GB. See https://github.com/google/flatbuffers/projects/10#card-14545298
- For a buffer that has entirely un-shared vtables (see 5), it now becomes more feasible to allow in-place mutation of more complex things. This is definitely a complex/contentious feature, but I think if we ever re-booted the format this should be designed in from the start if possible.
- Deeply integrated FlexBuffers, basically allowing any field to cheaply be a FlexBuffers value such that it effectively becomes FlatBuffers's "dynamic type". Sharing of strings across such values rather than being an isolated nested buffer.
- Nested vectors. Not strictly a breaking change, but a new format would probably want to have them from the start.
- Built-in LEBs (variable sized integers) as an optional
varinttype for fields. They could be added to the existing format but make a lot more sense in a system with no alignment.
@rw @mikkelfj @vglavnyy @mzaks @mustiikhalil @dnfield @dbaileychess @lu-wang-g @stewartmiles @alexames @paulovap @AustinSchuh @maxburke @svenk177 @jean-airoldie @krojew @iceb0y @evanw @KageKirin @llchan @schoetbi @evolutional
Activity
krojew commentedon Apr 26, 2020
Some quick thoughts:
Ad 1. We would need to (potentially) deal with compiler/target platform problems when dealing with unaligned access. Is adding padding such a big problem at the moment?
Ad 4. This sounds like a compatibility problem while migrating schemas.
aardappel commentedon Apr 26, 2020
@krojew
is not a problem. In the most basic case you make every scalar load go thru
memcpywhich gets optimized by every compiler (tested with clang, gcc and vs) into a single memory load, but one that is guaranteed to work unaligned. Padding is not a huge problem, but not having it enables a lot of other features (see the rest of the list) which would normally be pointless since 32-bit alignment is so prevalent.Not sure what you mean. You would opt-in to 8-bit offsets. Once you put that in your schema, that table will ways use 8-bit, in all languages, and never change.
mikkelfj commentedon Apr 26, 2020
random thoughts:
Overall, I like many of the suggestions, but there are too many optional and variable parameters in the proposal which would make things slow. There is a lot more branching going on, and code complexity.
detractors to Wouters original comment:
my inclusive comments:
I really would like to remove nested buffers - I has caused me so much headache and they haven't been properly implemented elsewhere. We could have a packed buffer format where multiple buffers an be embedded in the same file or memory block and referenced by some identifier without actually storing the buffer inside the other buffer. It could be an 8-byte random identifier. Buffers could also be given a random initial identifier in this format. Just a thought. This means how buffers are stacked or packed is not critical as long as they can be located.
mikkelfj commentedon Apr 26, 2020
We also need a proper NULL type for representing database data.
AustinSchuh commentedon Apr 26, 2020
Somehow I had persuaded myself that 9) doesn't need a format change and that it could be done by just building from the other end of the buffer and allocating space for the full message and vtable when creating the object. It would require a pretty massive API change though. Maybe I'm just hopeful.
My understanding of the premise of flatbuffers (and Cap'n'Proto, which we evaluated before picking flatbuffers) is that compression algorithms are wonderful, so don't burden the format with being clever and compact. Protobufs attempt that, and end up requiring a separate serialization/deserialization step. Sharing vtables goes against that.
Most of the rest of my feedback is at the API level. Happy to give it if there is interest.
maxburke commentedon Apr 27, 2020
One of the selection factors that resulted in us picking flatbuffers over other formats like protobufs was that flatbuffers doesn't use varints. If they pop up in v2, perhaps it could be an opt-in? Or maybe an attribute applied to fields?
I agree with @AustinSchuh about compression; I think flatbuffers' niche is that by default they are very efficient at runtime, even if they trade off space for that efficiency. In our use we transport flatbuffers over the wire in http that's gzipped/deflated/brotli'd, and on disk we persist them squashed with zstandard, so encoding tricks I think wouldn't really buy us much, but would hurt in our application use.
Please-oh-please add size prefixing by default.
I would almost prefer an inversion of the current required/optional field status, having fields set to be required by default unless annotated to be optional.
krojew commentedon Apr 27, 2020
If that's an explicit opt-in, then it seems fine.
I absolutely agree with this. I would extend this a bit further to add a proper optional scalar fields. Adding bool flags if a scalar is present or not, as we need to right now if 0 is a legitimate value, is a quite frustrating workaround, given non-scalars have this built-in.
AustinSchuh commentedon Apr 27, 2020
We have optional scalar fields today. It just isn't plumbed up to be accessible by default to the user. The offset for the scalar fields in the vtable is 0 if they aren't populated. See
CheckField<>andGetField<>(for how it handles defaults) for the gory implementation details. (I've got a patch which implementshas_it in C++ if there is upstream interest)Protobuf started out with required being the default and concluded that it was a bad idea. protocolbuffers/protobuf#2497 is a small part of the discussion.
krojew commentedon Apr 27, 2020
If something isn't publicly available, it doesn't exist from the user perspective. We should expose such information.
Additionally, I have an impression we're reaching the classic dilemma of speed vs size. So the first question we need to answer is - which one do we favor? For me, FB was always about performance, so if we need to keep padding or sacrifice some internal improvements for the sake of being fast, I would personally stick with that. If some improvement doesn't impact performance, then let's consider it.
rw commentedon Apr 27, 2020
Maybe a varint?
rw commentedon Apr 27, 2020
Would it be possible to have most (all?) of these features be specified with flags at the beginning of a payload? I know that that gets into "framing format" territory, but providing a set of feature flags at the beginning of a table (or at the beginning of an entire buffer) could allow a lot of flexibility at little cost.
It would probably just be an optional initial table at the beginning, containing metadata.
mzaks commentedon Apr 27, 2020
My 5 cents.
mustiikhalil commentedon Apr 27, 2020
My personal View on the following:
@maxburke
@krojew
This is an amazing idea, actually. Which made me think if we can actually remove the vtables completely, since all the fields are going to be required, or optional as mentioned above can be presented as a Bool. This would allow us to remove the vtable, and use the Generated table that's already predefined.
example:
when trying to encode the monster object, the following happens
4, 0, 0, 0, 0, 8, 0, 12, 0, 16, 20, 0, 24, 0, 28, 32, 36, 40, 44, 48, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 56, 0, 60, 64, 68, 0, 0, 0, 72, 0, 76, 0, 0, 80-> Monster.name, offset.4, 0, 0, 0, 0, 8, 0, 12, 0, 16, 20, 0, 24, 0, 28, 32, 36, 40, 44, 48, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 56, 0, 60, 64, 68, 0, 0, 0, 72, 0, 76, 0, 0, 80-> Monster.name, this will be removed and a reference will be created to the old encoded table245, 245, 250, 0.Now all we have to do is relate it the predefined table we have, where we can simply reference the number of the table instead.
mzaks commentedon Apr 27, 2020
Regarding the optional / required table field status mentioned by @maxburke. I think changing current behaviour is a bad idea in regards to backward and forward compatibility. Marking fields as non optional is a "lie" we make for API convenience. There are no guaranties that a field is present, as we have no control over, who created the buffer, given your system is not a closed one. When we define a field as required, we commit to not being able to deprecate this field. We also can't set a new field as required unless we can guarantee that no older clients which don't know this field exists, will send buffers to newer clients which require this field to exist.
Speaking of forwards and backwards compatibility, enums is another blind spot. Specifically the way enums are converted to JSON. I think in order to grant proper backwards and forwards compatibility enums need to be always represented as numbers, even though it looks nicer as text in JSON. But that breaks with new cases and also if a case has to be renamed.
mzaks commentedon Apr 27, 2020
There are two options how it can be solved in current FlatBuffers implementation.
0(I think most libraries have code for that)I guess for the new version this kind of feature could be addressed in a more straight forward way, by being able to define a table scalar field as optional in the schema directly.
113 remaining items
mikkelfj commentedon May 11, 2021
@victorstewart FlatCC supports creating nested buffers while the parent buffer is being constructed. This ensures correct alignment regardless of the nested buffer content. I hardly ever use nested buffers though.
GregBowyer commentedon Aug 5, 2021
I am late to this party (and if this exists then I would love to know!)
Supporting Lexicographic ordering on structs for key demarked things would be good. We use flatbuffers for storing things in K/V stores but typically encode the keys external to flatbuffers.
My dream would be
hassila commentedon Apr 25, 2022
Even later: 1, 4, 8, 9, 10 (definitely always frame with a length field for streaming, more important than file id IMHO).
aardappel commentedon Aug 12, 2022
Adding to item 15) in the original list (
varints): It would even more sense to have a dedicated "vector of varint", since that would make multiple varints even cheaper than multiple fields of them. The size field would be a varint itself, and you could optionally (or even by default) have these vectors be inline in the table for super-compact small vectors. They'd be accessed by iterators instead of indexing.Similarly, if you're going to have inline strings (item 8), then having the size field be varint (instead of having to declare 8/16/32) would make a lot of sense.
github-actions commentedon Mar 4, 2023
This issue is stale because it has been open 6 months with no activity. Please comment or label
not-stale, or this will be closed in 14 days.github-actions commentedon Mar 18, 2023
This issue was automatically closed due to no activity for 6 months plus the 14 day notice period.
vharron commentedon May 17, 2023
For Java, Is it possible to create an array with 64 bit entries as a set of multiple sequential 32 bit arrays. The
getElement(long idx)function would just shift and mask
idxinto an array number and an offset into that array.vharron commentedon May 17, 2023
How much space does this take? If trivial, should it be left in to leave the door open for read streaming?
I think it's okay to make an opinionated stand on this. People should use string_view and if they aren't, they can copy the string_view into a null terminated string.
aardappel commentedon May 17, 2023
That sounds incredibly slow, since anything larger (like a string) would have to be lifted out element by element into a secondary buffer before it can even be converted to a string since anything can straddle a buffer boundary.. All code we currently have that can work with the underlying array would stop working or need copies.
This is a single 16-bit quantity in the vtable which is currently still in there in all implementations, and typically set to 0. So yes, it could still be used for some future functionality, maybe.