Back
Thomas Rodgers
Executive VP and Chief Strategy & Business Development Officer, McKesson Corporation

CppCon 2014: Thomas Rodgers "Implementing Wire Protocols with Boost Fusion"

🎥 Sep 30, 2014 📺 CppCon ⏱ 55m
http://www.cppcon.org -- Presentation Slides, PDFs, Source Code and other presenter materials are available at: https://github.com/CppCon/CppCon2014 -- There are a number of common serialization formats available which work well for marshaling C++ types into messaging protocols, e.g. ProtoBufs, Thrift, JSON, XML, FIX, etc. Unfortunately, not every protocol uses one of these popular encodings and instead implements a unique binary protocol. The classical "C" way of handling binary protocols is to use packed structs, unfortunately there are many binary protocols which are not particularly friend...
Watch on YouTube

About Thomas Rodgers

Thomas Rodgers, Executive Vice President and Chief Strategy & Business Development Officer at McKesson, previously worked at DRW Trading, where he developed techniques for implementing wire protocol handlers in C++. In a 2014 CppCon presentation, Rodgers described challenges with binary protocols used by financial exchanges and third-party systems, noting that standard serialization formats like Protocol Buffers or Thrift are not always suitable when one does not control both ends of communication. He advocated for using Boost.Fusion to enable memberwise iteration and type dispatch on C++ structs, allowing generic reading and writing of binary data without the error-prone manual member-by-member conversion required by traditional packed-struct overlays. Rodgers stated that his team used these techniques in exchange market-data feed handlers, achieving processing times in the low hundreds of nanoseconds per tick. He also discussed the need for custom handling of signed integral types over the wire and described how some exchanges define prices using a signed exponent and 32-bit unsigned mantissa to avoid rounding errors. Rodgers referenced the C++ study group SG7, which was exploring compile-time reflection as a potential long-term solution for such protocol-handling challenges.

Source: AI-verified profile updated from Thomas Rodgers's recent appearances. Browse all interviews →

Transcript (84 segments)
T
Thomas Rodgers0:10
Participants, and this is a technique that kind of born out of some of our frustrations with writing wire protocol handlers. It's a pretty code-heavy presentation, so if I'm going too quick because I'm nervous, let me know and I'll try to slow down.
So the first question is, why not use Proto Buffs, Thrift, or whatever? These are great when you control both ends of the communication, but there are many important use cases where this is not the case. We tend to run into the third-party systems case, financial exchanges. We also, because of latency-sensitive concerns, sometimes have the embedded device case.
Proto Buffs isn't going to work for you. There is always the classic C pack structure and overlay way. This is very simple and efficient. You declare a pack struct that looks like the bytes on the wire, you do a reinterpret cast to get that view off of a byte buffer, and except for dealing with host network ordering issues, it's pretty straightforward. But you still have to go member by member in your struct and fix up all of the integral types which will be in network order to get them into host order, if you're unlucky and on 99% of the machines in the world which are little-endian.
But you're left with pretty limited abstraction capabilities. Plain data types are all you get. It forces whatever the third party's types are into your type domain, so you can't use your types to represent the wire types. We still have to do all the memberwise fix-ups for endianness, and this gets to be an error-prone maintenance issue over time. In my experience, this approach really starts to fall apart once you have more than one variable-length field in a data structure. It's nice if you have one variable-length data structure at the end of your... Did I not turn? Sorry. One variable-length data structure at the end of your struct is manageable, but the resulting code doesn't really lend itself to reuse at all in my experience.
Anyway, reflection. If there was a way to do memberwise iteration and dispatch on types in C++, this would solve the problem and we wouldn't need to use the technique, or at least part of the technique that I'm going to present here. But there's nothing available in the standard today. There is an active study group looking at this, which is SG7, chaired by Chandler Carruth. So if you want reflection in C++, you should go hassle him about that. Their initial focus is on compile-time reflection, so you can ask information about types at compile time, which is very similar to what we're going to be doing.
Programming, whereas STL works on containers of values and Boost MPL will work on containers of types, Fusion lets you work on both containers and types at the same time from runtime. So I would submit that this actually can be viewed as a container of types and values. Think of the type list as a Boost MPL list of all the types that are in this struct, and you can think about the fields as being references to those types in a tuple. In fact, if you had both of those forms, you could basically get zero on the MPL list and have the type, and get zero on the tuple and have a reference to that field within the struct.
Structure declaration using this defined struct macro to generate what is essentially those two pieces. You get an MPL list of the types in your struct, and you get an ability to reference every single field that's in a struct. We haven't seen that yet.
But before I go too much further, I want to talk a little bit about ASIO buffers. In particular, a lot of the code I write tends to rely on ASIO, or however you want to pronounce it. Two of the types exposed from this library that are going to be used in my code samples are const_buffer and mutable_buffer. They're non-owning types; they don't own the underlying storage, they only contain a pointer to some data and a length. They support an addition operator.
What we do is visit each member in a struct in a memberwise fashion. We're going to create a reader visitor. I know STL hates visitors, but that's how you have to use it here. We'll go into the details of reader shortly. We're going to construct it with a buffer, we're going to create the type that we want to actually read from the buffer, and then we call Fusion for each with our type and our visitor, and then return our type or an instance of our type.
The reader in our case is going to look kind of like this. It's going to hold a const_buffer which we're going to consume, and then you implement a call operator. In this case, we're going to make our state of our reader mutable and declare our call operator const. This is just an annoying artifact of how for each is written.
And so the writer is going to look basically the same at this point. So if you want to write out a Fusion structure, you're going to write it into a mutable buffer instead of consuming it from a const buffer.
The first order of business is we need to fix up... I can, if you want I... yeah.
A
Audience Member8:11
Other machines like about later, about performance of them. Or there's no way to specify this as a pack struct that I'm aware of.
T
Thomas Rodgers8:24
It is standard layout done this way. So the whole point here is that we're going to try to get away from needing to do the overlays with a pack struct.
A
Audience Member8:38
Your function operator has it depends on... right, right. So it doesn't basically on type rather. So how do you... you have value, you get passed by reference, so you need something in there that says I've already been called once or twice or whatever. You keep time.
Second question: the for each iteration, does that... it's literally going... if we go here, right, it can... it's going first uint16 corresponds to that uint16 in this tuple here, approximately right. Second one, third one. But it's this entire... when you call for each, it will visit each one of these individually. There's no keeping track of which one's been called because that's captured in the iteration outside. That's part of the for each.
T
Thomas Rodgers10:11
Now this handles endianness not particularly well. We're going to become more selective about this as we go. So right now, each time that this gets called, it's going to present a different T, right? A reference to a different T, and it'll be called one for each element that's visited by for each. It'll be called once. And so that's the writer.
So the first thing that we typically will want to do is handle those types. There is a proposal to add a generic or somewhat generic ntoh and hton, but it's only for unsigned integral types. So if you happen to receive signed integral types over the wire, the proposal as it stands today is not going to help you. But it's pretty easy to write your own generic versions of these that do the right thing for the integral types that you're going to expect to get. Having done so, basically for each type, for each T, you can say net to host, and then you do the buffer cast to a T star, dereference that for a given buffer, and assign into your value.
Then you assign... call where to start reading from by doing this. The writer case is basically the same thing, except we're assigning into the buffer instead and advancing the buffer.
So it's often that you will have fields that represent enumerated values. Message type is kind of a prime candidate in this structure. C++11 added scoped enumerations, which has this convenient ability to specify the actual underlying type that you want to use, so you don't get whatever the default type for enum is. You can be explicit about it.
Then we need to make this more selective. And that's where this auto arrow syntax... is this familiar to everybody? Everybody pretty familiar now? Okay, not you. Okay. So the idea is that with auto, this is not auto as a C++14 return type deduction; it's basically a placeholder to say I'm going to tell you the return type later, and the arrow is pointing to or essentially the type that's going to be yielded from this function.
Next thing is, who all is familiar with enable_if and how that works? The whole thing gets removed from the set of considered overloads; it won't be compiled, and that's okay in C++ because there's this SFINAE, or substitution failure is not an error property with templates. But if it is a valid overload, the return type will be void, just like it was before. This is just a way to push this goo for selecting the type down so it's not part of the interface contract for this function. You could have just as easily put the typename enable_if before the operator call, but then what you're actually calling and what it expects is kind of lost in the noise of the enable_if. I'm told by Andrew Sutton that...
So at this point, this will now only select for integral constants. We won't accidentally be able to pass an enum to it. And then we can just add an overload that knows how to handle any type of enumeration using the same approach. In that case, we will ask the enum for its underlying type. So this will get back the uint or uint32_t for us as type V. As the type of V, we can just delegate back into the reader at that point because we already know how to read that type. And then we can static cast it back to the enumeration type, and we're done with the enumerations.
So a lot of protocols will have fixed tag data. These are things like magic numbers. Appropriately named magic inversion seems to be a good candidate for this. So we can declare those as an integral constant uint16_t, the type or the value that we expect, and uint16_t value we expect for magic inversion. And then you add the corresponding overload to your reader. One thing to note here: integral constant doesn't store any data, so that member of your struct will still have space associated with it; there'll be no value there, there's nothing that's being set into it. We grab out the underlying value type, which in this case will be uint16_t, we delegate back into the reader to read it, and then we can test to make sure it matches the expected value.
Time value that's baked into the integral constant T and write it to the wire.
So the more interesting part of all this is dealing with variable-length types: strings, arrays of integral types, maps, whatever. For the purposes of this talk, I'm going to assume that we represent strings as a uint16_t length and some variable number of chars that will be read immediately following the uint16_t. The implementation of this is pretty unsurprising: you just add an overload for std::string, read the length, construct a string for that many chars in the buffer, advance the buffer by the length of the string.
Length prefix, and you're going to read some number of elements of type T. We don't care about T because we've already started to build up a structure for generically reading T's from the buffer. So read the length, consume each T in order, and then place them in the back of the vector that we're building. Maps will work... oops, got a... magically better. I am eliding that check here. There's also other cases where you can, on the right side, if you pass in a vector bigger than is representable by the length...
Length of the map, you write out the key, you write out the value. Or is this reading? So you read in the key, read in the value, and then place that key and value into your unordered_map.
So at this point, you can pretty much do this kind of a structure. I've got a bit of fixed-length stuff at the beginning, a variable-length structure of variable-length header properties, a message type, and then some variable-length content at the end. Doing this with fixed overlays gets to be pretty tedious, just to cover this much, in my experience anyway.
So I've totally glossed over the issue of framing. At the end of the wire, they'll send you a fixed-length header that'll tell you how much data you're going to get back, and then you can read that fixed-length header. When you've got that many bytes, then you can read length number of bytes and then continue to process from there. If you have framing that's essentially encoded in reading the types... I've worked on a couple of examples implemented using Boost ASIO's stackless coroutines to do this. It's pretty onerous, but it can be made to work, but it's beyond the scope of what I want to talk about today. So we're just going to assume that the fixed header tells me everything I need to know to read the rest: the number of bytes to consume, everything else I need in a given message.
32 bytes of header off, and I want to return buffer minus 32 bytes to be consumed by a subsequent call. Something like this: you'd read your fixed header, you would read whatever the length number of bytes you got out of your fixed header were, and then you would call read to deserialize the remainder of the buffer for however many types that you had in here. Obviously at this point, you'd probably switch on message type and then make further decisions about how to decode the remainder of the message.
So user-defined types. One of the things we deal with in the financial world are prices, and people don't like their prices being rounded. What they use is a signed exponent and a 32-bit unsigned mantissa. So this is a case where the standard proposed ntoh and hton wouldn't work because you've got a signed 8-bit quantity, and you would not be able to pass that to the proposed standard hton and ntoh generics. The reasoning there is that somewhere in a museum somewhere, there might be a one's complement machine that might want to still exchange data with you over the wire. And so representing negative numbers becomes a problem if you just blindly cast them to the unsigned bit representation and back. But in practice, people do it all the time and nobody gets bit by it.
So for user-defined types to use this technique, it's pretty important that they have cheap default constructors. Otherwise, this all falls apart. So this is obviously a pretty cheap default constructor, and you could go ahead and read it this way. You add an overload for decimal T, read it for the exponent, read it for the mantissa, and then just construct your decimal and assign it through. But we're going to come back to that in a minute because that's not the most general way of doing it, because you're left basically writing those overloads for every single user-defined data type that you want to be able to instantiate.
One of the things I deal with a lot are option products. Not... will be an option over a given underlying. So in this case, this is a September 567.5 call, I believe on Google. The price is encoded there, and it expires on the 13th of September. And a put or call, indicating whether or not I have the right to buy at that price or the right to sell. There are other fields in options as well, a lot of those are exchange-defined, but this is sort of the meat of what you would use to represent one, and it would end up looking like this. So this is a variable-length structure because it has two strings representing contract and underlying ID in it, but it's otherwise not terribly interesting. And I haven't really covered price date reading, but...
They change them from time to time, but that's kind of beyond the scope of this talk. But another thing that we also deal with is, once you have options, you may want to create a standardized contract which lets me buy and sell different combinations of them and then give that a name. And that's what a listed spread is. It'll have an exchange-assigned symbol ID, and it'll have some variable-length collection of contracts associated with it. So it looks like this. But unfortunately, we can't actually read this. We know how to read the vector, we know how to read the contract ID, but what we don't really know how to do is read T when T itself is a Fusion struct.
Implementing that is pretty straightforward. We can test to see if T is a Fusion struct, and if so, we can recursively read it. The writer case is the same exact thing, except it's going to take a constant reference to the field to write.
A
Audience Member26:23
Yes, it's a... part of... I think... did I... yeah, that's is_sequence is a Fusion trait.
T
Thomas Rodgers26:33
You have to really in the code do this, but that again, slideware. All these long names don't really fit very well, sorry.
Having done that, we can go back and more generally deal with user-defined data types because there's another macro that will take an existing type and then write all the boilerplate magic to make it a Fusion struct. So we don't need a specific overload for decimal T; our reader would just read it fine.
A
Audience Member27:21
Yeah, it's... this is statically compiled data. So there's nothing that is actually... when you instantiate a type that is a Fusion struct, none of the Fusion bits get instantiated with that. That is some static bit of information that's compiled, and for each knows how to get at that static table and then take a given instance and adapt it so that it can pull out the fields. So there's no overhead over just what a plain old struct would be in terms of construction at that point. Obviously, that cost will depend on how...
T
Thomas Rodgers28:12
More generic because now we know how to read Fusion sequences. We no longer have to specifically do the for each here; we can just call the reader directly. And so it now generically reads any type T, whether it started out as a Fusion sequence or not. And then the writer has a similar change.
So optional fields, another kind of variable-length component to protocols that you might encounter. Typically, these are indicated by some integral bit field where each bit indicates whether or not a given optional field is then present in the message.
Optional field as a boost::optional. Has people used boost::optional? Okay. It's an optionally empty value. And there is an optional being considered for standardization and probably will end up in some capacity in C++17. The only thing that we really need to do beyond what optional already provides is encode the bit position that we need to reference later. Then you can define type defs for your optional fields and what your field set consists of. In this case, we're going to encode 16 bits, so we add up to 16 optional fields if we chose to use them. So you could go do something like this.
We also use optional here to encode whether or not we saw the bit mask and to capture it. Because you're reading this stuff linearly, you have to use fields in your reader to mark and capture data as you're reading through that you want to consume later, even though you don't necessarily care about it once you're done reading. You need to know which field you actually care to read. And so that's all this does here: it reads the underlying type and constructs the bit mask or the bitset from the underlying type after it's read.
And then in the case of reading fields, if you haven't seen the field marker, it's a bad message, fail, can't do it.
Paste error. We don't actually need to do this; this just... star this... so bad copy and paste here. So ignore this, ignore this, and put... there. Keynotes: a very forgiving runtime environment.
The writer is a little more complicated actually because what we have to do here is not only capture the bitset, we have to capture the pointer back into the buffer where we're going to have to set those bits after we've looked at each optional field as to whether or not it was set. So that's what opt_ptr is doing here. We start out with it as null, and then when we see the bit mask, we capture the pointer to that location in the buffer.
16 bits of bit flags back into the stream when we're done, and then advance the buffer. Then for each field, if we haven't actually seen that marker, it's a bad message again. Otherwise, we... and this is actually also a bit of... this has an error: there should be an if statement wrapping this: if val, then set it in, and you assign through the current state of the bit field to the location in the buffer, and then read val. Actually, am I doing this? This is the right case again. I was tweaking these earlier and I apparently...
The bit mask back into the buffer, and you write the value back to the stream. So there should be an if check around that.
A
Audience Member33:35
Accumulate? Yeah, it makes sense. I mean, you just have to break up the processing into two discrete steps at that point. You'd have to say, well, this is a collection of optional fields by this, and you have to have some other way of indicating that, like maybe another struct that contained all your optionals. I don't think that this is any less efficient because once you've...
Writing the length of the frame, you might need to send out the... your field before all your values.
T
Thomas Rodgers34:21
Yeah, I mean, that's a consideration. If that's the case, then you would have to capture both frames potentially in your writer. So you write to your header frame, and then you process your body frame. That would be one sort of quick way of doing that.
A
Audience Member34:43
The one with the missing if... so... the... call the end... uh-huh.
Then it never gets written out. Oh, actually that's not true. I mean, it gets written out every single time here. Basically, so you're saying opt to unsigned long and then you cast it to the underlying type that you want to write, and you're just assigning that to the pointer or to the reference pointer. So we write it over and over again, right? And so the assumption here is that this is a pretty cheap call, which I'm pretty sure it is. Haven't actually looked at bitset's implementation, but I can't imagine that being costly.
And then write, right? And then write. Yeah, you'd...
T
Thomas Rodgers36:10
Everything that we're getting, or we don't care about it necessarily right now. We might want to apply some filter criteria before spending time actually deserializing it. Strings, maps are actually particularly horrible because they do a lot of allocation, and vectors also do allocation. So particularly in my business, this is something we look for ways to avoid.
We'll start with strings. Boost has a type called string_ref, written by Marshall Clow. So if you see him, thank him for it. It's a wonderful piece of software. It's a non-owning type with an interface like std::string. Jeffrey Yasskin put together a proposal called string_view, which is likely also to be in C++17. There are a number of use cases for this, particularly where you receive a buffer but I'm not actually going to copy it out; I'm just going to record its location and its length and then give it a standard string-like interface.
So starting from our string implementation, making this a string_ref becomes pretty much the same thing. You read the length, and then you construct the string_ref by getting a const char* from the buffer, giving a length, and then advancing your buffer to consume that string.
To do arbitrary lazy types, this actually gets to be a little more complicated depending on how elaborate you want to get. But the main thing we need to be able to do is cheaply determine how long some type is. So if we can skip through and figure out how long it is without spending a lot of time, we can defer the actual read to get that type on demand.
So you might declare a type that looks like this. It doesn't really fit very well on the slide; I'll have to apologize for that. But you need this type that would accumulate size. It's very much like the reader case, you just get rid of most the reads. The only read that we really need to preserve is the ability to read integral constants because we need to be able to read the length fields and accumulate them. And then for every type, you just accumulate size and advance the buffer. In fact, the sample code I even went so far as to get rid of size and just do it from the resulting buffer versus the buffer I started from. That would be done inside this get_size function.
Type T would be encountered in that buffer on the wire, and then later to get it back out, at that point we can take our buffer there and delegate to read to read that type T. And we need to be able to get its buffer size because we want to be able to read these lazy types when they're encountered in a stream. So basically, lazy_type does a bit of buffer sizing and a bit of buffer arithmetic, assigns that into val, and then just advances buff by the size that would be read had it went ahead and aggressively read the data at that point. So it skips the buffer over it.
A
Audience Member39:50
Yes, do you assume that every type T is represented in one, two, four, or eight bytes on the wire? So for every type T, you assume that, so you don't have to worry about reading an arbitrary struct off the wire as a T that's not being visited as if it was a Fusion struct. But if you had an arbitrary struct that you wanted to read, say it was specified in the protocol but it made no sense to adapt it, you would have to declare that packed, and then this would still work.
T
Thomas Rodgers40:39
Yeah, I don't know. Presumably it could be a Fusion struct. Yeah, I assume that actually in the default case. But if that weren't the case, you would have to deal with pack structs.
A
Audience Member41:11
Something the reader already knows how to consume correctly. So you can't just put any... I don't know if I could get a concept to constrain that. Maybe I get you to write it for me.
T
Thomas Rodgers41:33
So at this point, we actually can read this. We have a variable-length struct of variable-length types. And at this point, I would submit that this would pretty much read anything fitting that general pattern. And you can get it lazily, so we can filter on the fly.
Sample code: there's an implementation of a lazy range which takes this one step further and just gets rid of the vector. You get a begin and end, and when you iterate it, it will materialize the individual types out of the stream at that point. So I'll post links to the sample code at the end of the talk.
So another thing that you might want to be able to do, once you've gone to all the trouble of marking up your types as Fusion structures, is be able to pretty-print them. You know, say some Node hipsters took over your back office and they want you to send all the compliance reports in JSON. It'd be nice to be able to do that without having to write a bunch of extra code again on top of what you've already done to get it from the exchange in the first place.
And I'd like to be able to do it generically. It defines a streaming operator that knows how to visit the contents of the Fusion struct. Unfortunately, we can't use the Fusion for each technique to do this because the way Fusion lets you get at the field names, you have to ask them by a compile-time known index. And so that's what this range_c business up here is doing. It's basically saying give me the size of T as an MPL list, and generate the integer values from zero to that as this type-dependent thing. The thing to remember here is that Fusion is a compile-time bit of machinery, and the only bridge between MPL and runtime is this for each.
So what'll end up happening is this is going to call our visitor with 0, 1, 2, 3, 4, 5, 6 for each field that's there. It's pretty mundane, but unfortunately this is the only way to actually make the rest of it work.
You can then create some writer that knows how to format things as JSON. You know, captures your ostream that you want to write into and passes that on to the writer, as well as your message type. Did I not pass my message to my constructor here? This is unfortunate. The sample code actually has it right, but copying and pasting back and forth, it seemed to have missed it.
And then the visitor is by ordinal index here. It's the message type, the Fusion type. The value of N, which is the compile-time known index of that field, and that'll give you back a const char* with the field name as you compiled it into the code. And I'm just presuming that writer is going to be able to write, you know, quote field name colon as you would in JSON. And then you do the same thing to get the Fusion type here, so that'll return a type T. And then the assumption is that our writer type knows how to format each of the individual types that we can read out of our Fusion struct as JSON, and then interleave commas if we need to.
A
Audience Member46:11
So now the problem is I want to have... I'll put the XML or JSON, but one of my field names is wrong. Then you'll have to figure out some way of translating from what your field names are, you know, your compiled source, to what you actually want to pretty-print them as. So you could do a map to translate those, but this will give you like literally the compiled-in string that you specify.
T
Thomas Rodgers46:42
The vectors, the structures for Fusion, I think are limited to like 20 or something. Your Fusion vectors are pretty small. And for everything else, obviously you had 30, 40, 50 members in a class, you'd start to run into the Fusion limits because these aren't variadic, unfortunately. Although I think as of 1.56, at least the MPL side of it has added variadic list support, so they can be arbitrary length. But I don't know if Fusion has accommodated that change. I haven't, for the protocols I've written to date, I have not run into those limits. Although one of the problems with using Fusion and MPL is because it does machine-generated expansions for a list of zero, a list of one, a list of two, a list of three... all those template expansions get to be pretty onerous from a compile-time standpoint. So the sooner that Boost gets proper variadic support, the better.
So that's basically what I have. Questions?
A
Audience Member48:16
If you had like session-wide context, you just pass references? Is that... yeah, that's how you do it. You just pass it by reference on your read or write call and just pass it through to the reader or writer type underneath. They're temporaries, right? So there's no need to make that copy or anything. And that's how we've typically done it, and it is pretty typical that we actually have that in real code. I just kind of skipped it here because it begins to obfuscate what is already somewhat obfuscated.
Stefanus, have you thought about how you would extend this if you have to deal with a purely bit-banged protocol?
T
Thomas Rodgers49:11
Actually, in that case, we're actually laying things out as they would be sent to an exchange on the other end because we're basically writing them into the memory of a very specialized component that knows how to stomp on a send line on TCP as quickly as possible. So they tend to be pretty standard layout. I never had to really deal with the case where you're bit-banging fields like on a serial line or whatever, if I understand your question. Yeah, I imagine most of it would still apply. Reading and writing would definitely get messier, and I think one of the problems you start running into there is that ASIO obviously wants to move by bytes, so you'd have to figure out how to handle that.
A
Audience Member50:10
Basically from it... yes... huh...
For the code that we have, they're not too bad. It's kind of hard to compare that. The sample code on my Mac that does just this part of the protocol and a few other additional bits takes a couple of seconds. The problem is that depending on where we end up using this in practice, I have other pathologically bad compile times for reasons completely unrelated to this technique. So it's hard to say how much of those compile times are due to this at that point. But I haven't found it to be a problem.
Also have some of this going on, so it's kind of hard to separate who's doing what because the compilers also don't give me great instrumentation either. From a runtime standpoint, Fusion will unroll loops automatically. And while I haven't looked at the assembly output, I'm going to speculate that most compilers will be able to unroll the loops even further because the length of the sequence is known statically at compile time. So it should be able to, within limits, not blowing out your micro-op cache or whatever, unroll these loops even further. So it shouldn't be, for all intents and purposes, any different than if you had written it by hand.
To access efficiently, you have to copy it out into some unpacked type anyway before you can really do anything with the fields. So I think in that case, this should be pretty much performance competitive. I will say that we use these in exchange market data feed handlers, and this technique does not impose any significant performance penalty on reading market data. We're definitely in the hundreds of nanoseconds range to process a tick using this technique.
How would you mark a field as never serialized?
T
Thomas Rodgers52:53
Ah, I have an example of that. In that case, you just... there's nothing to write; you're just skipping that many bytes and leaving that hole in the buffer. Part of the problem in the sample code, like lazy here, doesn't handle the case where I'm sending and receiving the same kind of structure that happens to have a lazy in it. It's almost always the case that you want lazy on the read side, not the write side. And so I have a much more complicated implementation of lazy so I can support round-tripping to make sure all the code works. So there are some complications in the sample code that you wouldn't actually have in practice, because typically on lazy or completely ignored fields, you're just going to skip them with simple buffer arithmetic at that point.
A
Audience Member54:12
A lot of times they'll add fields we don't care about, so we'll just mark them ignore. It's very often that they add fields for specific use cases for instruments we don't trade or aren't going to trade now. And so that's where the 'never read this' example comes in. You add another version to the version list that you check before you blow up, and you just ignore the fields you don't care about. It would get trickier if you had to sort of optionally read them between versions, and I think in that case you'd have to go back into the types and start adding some sort of version marker, basically saying 'this only works for this protocol version.' You could encode that as an integral constant that's part of that type and then check that each time, but you'd have to stash the version once you saw it in your reader.