| Just to give a bit of counterpoint, here are some trade-offs that Capn Proto makes compared with protobufs. (Full disclosure: I work at Google and know Kenton from his time here; I have my own protobuf library that I've worked on for several years). I'm sure Kenton will correct me if I get anything wrong. :) Capn Proto's key design characteristic is to use the same encoding on-the-wire as in-memory. Protobufs have a wire format that looks something like: [field number 3][value for field 3]
[field number 7][value for field 7]
etc.
The fieldnum/value pairs can come in any order, and may define as many or as few of the declared fields as are present. This serialization format doesn't work for in-memory usage because for general programming you need O(1) access to each value, so protobufs have a "parse" step that unpacks this into a C++ class where each field has its own member.Protobufs are heavily optimized so this parsing is fast, but it's still a very noticeable cost in high-volume systems. So Capn Proto defines its wire format such that it also has O(1) access to arbitrary fields. This makes it suitable as an in-memory format also. While this avoids a parsing step, it also means that your wire format has to preserve the empty spaces for fields that aren't present. So to get the "infinitely faster" advantage, you have to accept this cost. For dense messages, this can actually be smaller than the comparable protobuf because you don't have to encode the field numbers. But for very sparse messages, this can be arbitrarily larger. As Kenton points out on http://kentonv.github.io/capnproto/encoding.html , lots of zeros compress really well, so even sparse messages can become really small by compressing them. To do this you lose "infinitely faster", but according to Kenton this is still faster than protobufs. In both cases though, the tight coupling between the (uncompressed) wire format and the in-memory format imposes certain things on your application with regards to memory management and the mutation patterns the struct will allow. For example, it appears that the in-memory format was not sufficiently flexible for Python to wrap it directly, so the Python extension does in fact have a parse step. Other cases where you could need a parse/serialize step anyway: if you want to put the wire data into a specialized container like a map or set (or your own custom data classes), or if the supported built-in mutation patterns are not flexible enough for you (for example, the Capn Proto "List" type appears to have limitations on how and when a list can grow in size). It's very cool work, but I don't believe it obsoletes Protocol Buffers. I'm actually interested in making the two interoperate, along with JSON -- these key/value technologies are so similar in concept and usage that I think it's unfortunate they don't interoperate better. |
> For example, it appears that the in-memory format was not sufficiently flexible for Python to wrap it directly, so the Python extension does in fact have a parse step.
This is not correct. The Python wrapper directly wraps the C++ interface. You might be confused by Jason's claim that "The INFINITY TIMES faster part isn't so true for python", but this was apparently meant as a joke.
It is true, though, that the constraints of arena-style allocation (which Cap'n Proto necessarily must use to be truly zero-copy) mean that working with Cap'n Proto types is not quite as convenient as protobufs, although most users won't notice much of a difference. Lists not being dynamically resizable is the biggest sore point, though most use cases are better off not relying on dynamic resizing (it's slow), and the use cases that really do need it can get around the problem using orphans (build an std::vector<Orphan<T>>, then compile that into a List<T> when you're done).
OTOH, over the years, many people have requested the ability to use arena allocation with Protobufs due to the speed benefits, especially with Protobufs being rather heap-hungry. I always had to tell them "It would require such a massive redesign that it's not feasible."
And yes, there is the trade-off of padding on the wire. You have to decide whether your use case is more limited by bandwidth or CPU. With Cap'n Proto you get to choose between packing (removing the zeros, at the cost of a non-free encode/decode step) and not packing (infinitely-fast encode/decode, larger messages). For intra-datacenter traffic you'd probably send raw, whereas for cross-internet you'd pack. Protobufs essentially always packs without giving you a choice. And because it generates unique packing code for every type you define (rather than use a single, tight implementation that operates on arbitrary input bytes), Protobuf "packing" tends to be slower.