 So just I checked this earlier, but the lighting is a little different. So my friend Santiago's in the back. Is this good? Okay, cool. Also, you should you should try compiling this on the latest nightly. It's pretty funny All right, so my name is Sean Griffin. I'm a 10x hacker ninja guru at Shopify if you're wondering If you're wondering what that means it means that when I was applying for my work permit in Canada I thought it would be funny to see what would happen if I put that as my job title Turns out what happens is I can no longer legally work in Canada as any job title other than 10x hacker ninja guru And I'm no longer allowed to fill out government paperwork unsupervised These are the things I'm known for making I also made this thing recently My friend Steve who I've seen give a lot of talks at Ruby conferences around this point He always goes up and he's he said some long lines of you know I mostly write rust these days But I still really love Ruby and you know how much I love it because I have it one tattooed on my body Well, I'm sorry to one-up you Steve, but I literally named my firstborn child Ruby. So She's also a big fan of rust All right on to the actual talk So the ultimately the heart of software engineering is managing trade-offs and There are a lot of trade-offs in your choice of language For example, if you were to try and map out languages on a spectrum from high level to low level You might have something like Haskell on one side and something like see on the other And if you were to just map out a arbitrary pro of Haskell Well, most people would say it's got a really really good type system And one of the cons of it is that you're going to lose control over the layout of your data So on the other side we have see and see gives you very very tight control over how your data is laid out in memory And you give up a lot of things in exchange for that enter rust Rust really loves to blow up these sort of traditional trade-offs Concurrency without data races memory safety without garbage collection and so on This is a big part of rust secret sauce. It's what makes rust special. You can even call it rusts fire flower The combination of having control over memory layout combined with a proper type system can enable some really cool things Let's take a look at an example a Hashset is an unordered collection of unique elements it works very much like a hash map it where you only look at the keys and not the values and Rust implements it exactly like this. It wraps a implants hash that as a very thin wrapper around hash map a Hash that has a single field which is a hash map where the key is whatever We are putting into the set and the value is an empty tuple also known as unit The value itself doesn't actually matter for the implementation of this hash set to be correct We could use any type For example in ruby a set is implemented in exactly the same way, but they use a boolean rather than an empty tuple Well both implementations are correct. There's a big performance difference Specifically it rust is able to take advantage of the fact that the value is zero sized And it eliminates a bunch of code because of that and that creates a huge different before difference in performance How big? Well, I did a very very unscientific benchmark And I found that switching a hash set to use boolean instead of unit gave me a 20% loss of performance That's free performance and you can take advantage of those same kind of gains just by understanding some of the guarantees that Rust can provide so I've been talking a lot about this type being a zero size I would want to talk about how you know the size of a type you could start out with the simplest case Primitives a lot of the primitives in rust actually happened to have the size in the name u32 u16 etc We have car here car is a is a single unicode character and it is four bytes If you have a struct with a single field That struct is going to be the same as the size of its field just like in C If you have more than one field the struct is going to be the size of each of its fields added together So in this case foo is going to be the size of two cars Enums act very much the same like structs at least if they only have a single variant You can pretend that bar was a struct and whatever the size of bar is is going to be the size of foo Only as one field that is car so the size of foo is car When you add on a second variant You're going to add first of all one bite so that rust knows whether you have a bar or a bass And then the size of foo is going to be that one bite plus the size of its largest variant And you can determine the size of each variant by pretending it were a struct So bass here if we wrote as a struct would look like this It has no fields and its size is zero So when we come back to this we already looked at what bar would be earlier We know that one that four is in fact larger than zero so the size of foo is going to be five We changed bass to be a different type one that was larger than car now The size of foo is going to be the size of that larger type plus one So let's look at a little bit more complex type Let's say that you wanted to implement a new string type and You wanted to implement that as a singly linked list because there's absolutely no way that could ever cause pains for anybody So you might if you're if you're new to rust you might try writing it like this Let's talk about finding the size of list string. So we need to first figure out which of its variants is the largest Well, nil has no fields and so it's going to have a size of zero and I'm going to guess that's not the largest one here So we're going to skip over that So we look at this variant called cons So we need to add up the size of each of its fields Head is a car and tail is a list string. So car is four bytes and the size of a list string Well, so to determine the size of a list string first You need to figure out which of its variants is the largest and oh no I actually really love this error because you basically only ever run into it when you're trying to implement a singly linked list and so the And the error message if you do a rusty explain is literally just you're trying to do a singly linked list Here's exactly the code you need to write. It's it's great anyway, so it's complaining because A struct this enum has itself as a member, which means that its size would be theoretically infinite So we need to introduce some in direction there So we're going to change this to box up the list room So box is the heap allocated pointer There are those that aren't familiar with it and the size of a pointer at least on my machine is going to be 8 bytes So now the size of this whole thing is the 1 byte for the discriminant 8 bytes for the pointer 4 bytes for the car for a total of 13 bytes for those of you shaking your head about why I'm wrong I am purposely ignoring padding and alignment and null pointer optimizations because they're not super relevant to the to the point of this talk Now One of the kind of curses of rust is it makes it because it makes it so easy to write really really performant code and it nudges you towards Doing things that have very little overhead It makes you super aware when you're doing something that isn't the most possible performant thing So you so a lot of people look at this and scream no no my singly linked list can't possibly afford the cost of a pointer But let's look at how we could how we could do this differently and avoid actually having to to do heap allocation So rather than having list string be an enum We could instead move any of the behavior that we needed to group together here onto a trait and have two structs instead So I've omitted the trait here But we have we've turned each of our variants into a completely separate struct and specifically the case of Cons where we have at least one element is now generic over the rest of the list And I haven't talked yet about about how to calculate the size of a generic struct So let's say we had a struck called pizza pizza's generic over its topping so if we were to say have a topping let's call it pineapple and We asked the compiler what the size of a pineapple pizza was it would error because pineapple doesn't belong on a pizza Steve No, so of course pineapple has a size of zero if we pretended that topping were pineapple Then this would be a single field that is of the type pineapple that would have that has a size of zero So the struct has a size of zero and if you had a pizza with a giant hole in it Get it because because it's topping is a bite Then its size would be one bite So the size of our construct here is going to be the size of a car plus the size of the rest of the list So it's interesting here is that the size of the entire list is going to be Exactly the smallest size possible The size of an empty list is going to be zero The size of a list containing one character is going to be the size of exactly one character And if it had two characters, it'll be the size of two characters three characters so on There's some other interesting properties for example You're now actually encoding the length of your string in the type system So things like length will we can potentially get in line by the compiler and replaced with just a literal You could also you could also do generic code and implement traits for example only for empty strings and have that verified at compile time But this isn't without traders You can no longer write code like this if you want to take a list string as an argument You can't just take a type called list string. You have to take you have to write a generic function You're taking some type t where t implements list string And if nothing else This just looks more complicated to me. I read you if you if what I think about it's like no But it's really not doing that much different, but you just look at it You also can no longer pattern match So if you want to write a function that determined if your if your list string in letter a you might write it like this And if you were if you were to do it generically you can no longer assume that you either have specifically cons or nil Since list string is now a trait theoretically any structs could actually implement it So you can kind of like maybe write a function that returns the head and an option of the head and tail and do something kind of similar But just in general your code is just gonna feel a little bit more complex You're gonna be your the cost that you're gonna be paying for this is gonna be complexity in your code base Now for a little interim from Ruby if you need high if you need high quality rust code or training Ruby Thinks you should go with integer 32 LLC So let's look at some more code here Let's look at what happens when we have when we When we deal with treats So we have a trait called robot robots have a username. We have a lot of great robots in rust They they they manage the rust repo for us. They're great. We're gonna say hi to them because we love our robots We either use a name to say hi to them One of our robots is named Boris Boris is responsible for merging all of the code into the rest repo and running CI and doing other great things Another robot is named Alex Alex is responsible for Alex is responsible for implementing things. It's a sentient AI so when When the compiler is going through this code, it's gonna go in traits aren't really a thing that exists in your final binary Your operating system doesn't know what a trait is or how to call a method on a trait So the compiler is gonna go in it's gonna replace these functions with just an actual playing function So it the names are just sort of made up But in this case we could pretend it was called robot username Boris It's gonna go take this one and turn that into robot username Alex Now our say hi function what Russ is gonna do when we've written it this way is it's going to do Transformation to it called a monomorphization, which is a big scary and kind of poorly named term But this function right now is polymorphic. It can take multiple types And what monomorphization means is that the compiler is gonna take this function It's gonna copy it for every type that you're calling it with So we're gonna get a function called say hi Boris and say hi Alex and Rather than trying to calling a method from this trait It's instead going to be calling these specific named functions that we know that we know exist Now this is up to a certain extent a gateway optimization This is the benefit of this optimization in a vacuum is just that you get you have one less dynamic function call It's not a huge win on its own But this enables other optimizations to occur because now the compiler can go look at this function and say okay Well robot username Boris. That's a really simple function. I can just inline that and then it can do the same thing over here This is about as far as the compiler is gonna be able to go It can't quite take that last step to look at this and say well I could just combine both of these into a single string literal Be cool if it did but you know it that's fine As with all things in programming there's a trade-off here The cost that we're paying for the compiler to generate more efficient code is that it's increasing the size of our binary It's copying these functions over and over again, and it's going to likely increase your compilation times as well In some cases you can opt out of this if you'd rather not pay that cost By taking a reference to robot We're taking what's called a trait object This is sometimes also referred to as type erasure When we take a trait object the compiler no longer knows anything about the specific type that we've passed to it The type has been erased So what that means is that compiler can't do monomorphization It can't generate a unique version of this for the type you're passing to it because it doesn't know what type that is But it also means that the function is not going to get optimized beyond this point This is as far as the compiler is going to go it can no longer see past username Baby Ruby says if you also have trouble figuring out which way to hold a bottle integer 32 LLC Just all of my cute baby pictures. I noticed we're in rust one season. It's great All right, so this is typesys and tricks for the real world And I've only talked about some world where people use singly linked lists for string and that's just crazy talk So let's talk about a real-world example Do you go into diesel's code base and you look at our test suite you're gonna find this test Where we build our increasingly more complex queries and we assert that the size of that query is zero Now I've talked to a good bit about zero size types here You may be wondering at this point. Why on earth do we care whether the size of this is zero? Well zero size types are particularly interesting. Let me go over a list of all the all the things that you can do with them And that's it There it occupies no data. There is nothing you can do with them at runtime So if a type is zero size it means that you've structured your code in such a way that everything you're doing with it Is something that is completely done at compile time the type itself is guaranteed to be erased Because there is nothing you can do with it So it makes this a little bit more clear I'd like to go through what the compiler is going to do in when diesel tries to construct the sequel for this for this query Users dot find one this would generate select star from users where users id equals one The type that this thought that this expression is going to return looks like this In the interest of keeping things a size that fits on a slide We're going to omit a lot of this code and pretend their type only looks like this instead Which is still a slightly large type but is smaller most of the things that I just took off are Types that are zero size and basically are saying this part of the query isn't there and are guaranteed to get eliminated If you want to construct it you would do it like this This isn't super the specifics of this isn't super relevant But the point being that these are all actual concrete types. There is nothing behind a box There is no there are no trade objects involved So this is the function that the compiler is going to start with this is the code I copy pasted it directly from diesel and then took off a few generic parameters to make it fit on a slide This first line here says add this sequel string to the query being constructed The definition of push sequel is going to get in line, but we're going to leave it alone for now and come back to it later This section here we know that the type of self dot distinct is no distinct clause So we could just replace it like that This function is going to get in line the body of that function is literally do nothing and so that line gets deleted we know that self dot select is of the type default select clause and The body of default select clause is grab the default selection the default selection is a tuple of all the columns and This is going to be in line to the definition of walk AST for a two element tuple Which is going to look a little bit funky because this code is generated by a macro But it's okay because the compiler is going to come and clean up my mess for me Now you know in a massive breakthrough of computer science rust is able to figure out that zero is in fact not equal to zero It's also able to figure out that code that behind if false will never get run. So that just goes away Same thing massive breakthrough one is in fact not equal to zero and if true is always going to get run So we just replace that with push a comma Going up to here Users ID first we first we put user push users then we push a dot and then we push ID Push identifier is going to put a double quote and that's going to put the the identifier that we want to put out there with Replacing all double quotes with double double quotes Polar is also going to be able to see that there are no double quotes in this string So we just eliminate that entirely same thing happens for ID Users name. Oh, hold on. Let's scroll. Let me scroll up here. Yeah, we're running out of space Same thing From clause, that's just the user stable. We know how to do that where clause So we know specifically that we do have a where clause So this gets in line and we're just going to push where out there And then we're going to and then we're going to push the actual value of the where clause the where clause is ID equals one so first we're going to put on users ID and Then we're going to put out one now one is interesting because this is the only part of this query That is in any way dynamic So this is just going to be the code that is push this dynamic value This will actually end up getting in line much much farther to very specifically like create a new vector and stick and stick these exact Bites on there, but that's very not interesting. So we're going to leave it at this stage So this is the code that we started with and this is what it got optimized down to Now you'll notice two things about this Number one. This is very much larger than what was there before We also notice if you squint at it. It's really flat There are no conditionals here. There's no dynamic dispatch going on This is sort of code that your CPU is going to go through very very quickly now. This function itself is also going to get inline What so when we go to this line push sequel? So this is this is the What happens when that function gets in line? Ast pass the thing that we've been operating on here is an enum Diesel will actually call this function four times once with each variant of the enum to do all the different things That we want to do with that value The one that we're caring about right now is actually constructing the sequel string. So that's called the two sequel pass But because this function will get inlined itself and the line immediately before we called the function Will be the thing that constructs the AST pass like power is actually just going to know Oh, this is this is we always have that variant. So we can we don't need to have that conditional We know that we're always going to run this code And then the body of push sequel is is just push pushster, which is a method on string from the standard library And so it's going to do the same thing to all of these other lines I'm not going to make you watch every single one of those get transformed And again, this is this is as far as the compiler is going to be able to take it It is not able to then take that last step that would look really cool on a slide if it did and turn that into a string Literal, but it's about as close as it can get this is effectively let mute s equals Equals string not new and then pushing each of these little fragments onto it the the bind parameter where the one goes at the Very bottom that's just going to get in line to push the sequel for a placeholder for a value that gets sent separately And this is mildly interesting and a decent win for constructing the sequel query What's really interesting is is what happens for our other three AST passes Rails when it constructs a sequel query has to go through goes through a very similar structure and wants all the same information that diesel has But when I'm doing this in rails I have to try very hard to shoehorn all of this into a single call We cannot do multiple passes over the AST because it's very expensive But in rust this is the past where we actually collect the dynamic data So we still care about this push by that last call a push bind program Which is that got in line to push bound value This is the thing where we'll actually serialize the one to the bytes that the database cares about If we had more than one value there might be some additional optimizations of Specifically the order that we're pushing things, but it's not terribly interesting What's important though is all of that code that would have been conditionally if we're in this past do this or all the Code that was there for traversing the AST for everything that didn't have a dynamic piece got eliminated Another pass that we do is Determining whether or not a query is safe to store in our prepared statement cache And if you don't know what that means, it's okay. What's important to know is that yes This one is and we just know that when people talk about building zero cost abstractions These optimizations are what make that possible. This is what makes iterator fast. This is what makes futures fast This is what makes hash set fast At the beginning of this talk I mentioned that using unit instead of bull was about 10 to 20 percent faster But I glossed over why that was the case The reasons because the compiler knows that this value with zero bytes can never be read it can never be used So all of the generic code that's responsible for storing the values of a hash map gets eliminated Now the compiler could have in theory done that same optimization by looking Oh, well, we're just never using the values. Let's eliminate the values in this in practice. It just happens not to But the point of that is The fact that this type of zero size it doesn't enable the optimization But it does guarantee it and what we end up with At the end of the day when we get to our machine code is the same code that we had have if we wrote a super optimized hash set Ourselves I'd like to thank shop find my employer. They paid for me to be here Please ask me questions now Thank you very much