YTread Logo
YTread Logo

A Complete .NET Developer's Guide to Span with Stephen Toub

Jun 05, 2024
Hello friends, I'm Scott Hanselman, we're back with another deep dotnet with Stephen, how are you doing sir? I'm doing great, how is Scott? I'm living the dream, living the dream, so what else can we do? I think a great place to go would be Span Spa of Tea, which is awesome because, in my opinion, there are two types of people: those who know all about Span of Tea and love it, and those who are using it. It's changing their lives every day and they don't even know it exists because it's ubiquitous across the platform exactly, so it is absolutely when one of the first difficult things when you do computer science is that you learn about your types and then you put them in a matrix and everyone's computer science or software engineering teacher had a metaphor that mine used cubes and they literally liked cubes lined up in a row and we would say that's zero with the cube and things like that that we've all written down if we grew up In a certain genre, we write a copy from memory, we would know how to move arrays manually, but maybe we should start from the beginning of why a variety of things are difficult or interesting, it's a great idea, yes, and I love it.
<

span

>span>
We'll talk about span because, you know, many people who use Net know that it has blocked significantly faster across the board over the last decade. Big improvements every release, release after release, you know, piling up big performance improvements or reductions. in memory consumption or startup or whatever and there are many reasons for that, sometimes you know, innovation that happens in the research world comes into the platform, sometimes it's fundamental improvements in code generation or in garbage collection, sometimes it's just kind of a quantity, you know the enormous amount of open source contributions that come in the release, but there have been some really critical turning points or revolutions, almost um, where some new feature appears and it just changes the way we do things day to day and at the top. from that list is span um and it's really changed what we do and how we do it and how we think about it so yeah let's start with an array so to your point let's say I just have a simple method that uses an array , so I'm going to have a static one, we're going to call it sum and this is just taking an array and we're going to summarize the values ​​in a simple way, so I'm going to create a sum and return the sum. and then I'm just going to do each over each value in the array and the sum plus equals.
a complete net developer s guide to span with stephen toub
I'm right, it's your very basic loop and as we talked about, I think in the video in link part one or link part two, this loop will actually be reduced by the C compiler to a really nice for loop for I equals z i is less than the matrix. length i++ and then Su plus equals I some performance code WR right Z here we will focus on the performance code but no one needs to worry because line six will be reduced, just the right things happen and we can see that the right things It just happens and I'm going to give a, you talked about how far we can go.
a complete net developer s guide to span with stephen toub

More Interesting Facts About,

a complete net developer s guide to span with stephen toub...

I'm going to take a very, very deep jump off the cliff here and we're going to go straight to looking at assembly code um just because you know I don't expect everyone watching to understand this particular dialect of assembly code, that's not the point, the point is, uh, we have an idea of ​​how this high-level code translates and then we'll be able to see as you make changes to how that affects the quality of the assembly code now, when you say assembler right now, we're in idiomatic C, then there is compiler generated C, then there is compiler generated Ilil and then there is Jitter or much assembly, which is processor specific, what are you talking about?
a complete net developer s guide to span with stephen toub
I'm talking about processor-specific code, so I'm running Windows on a 64-bit, uh, x64 architecture, so you would see different code if it were arm 64 or if it were Linux. You know the differences in other assembly languages ​​or in the calling conventions or whatever, but again, none of that really matters. We're just going to use it as kind of a starting point, so there are a variety of ways you could disassemble this feature and then advantages. and the disadvantages of each, a very simple one that I've alluded to in the past, is that I could just copy and paste this code into uh Shar laab Shar laab iio, so if I paste this here I have my addition function and we can see that I can look at the C.
a complete net developer s guide to span with stephen toub
I could see what the iil looks like for this function and I can also ask it to produce assembly code, so this is basically taking the C by compiling it to iil and then running it through the solo in the time compiler in this case on x64 using this particular version of ofet and generating some assembly code, so that's one way I can do it. You could also press F5 or control F5 and attach a debugger and look at the assembly code in the debugger. That is. another way to do it, um, however, there is a very useful one, uh oh, I can also use the benchmark.us diagnostics, so we saw in the previous video that it puts the memory diagnostic in my Benchmark if I just add the benchmark.us diagnostic. disassembly assembly which will also tell benchmark.com before and after you can see a difference between the before and after and see what was going on, but there is a really interesting approach that is also very valuable and that is that we can ask the jit itself to tell us what assembly code you are generating. and we can do this let me write a little while loop here let me say while true uh and all I want to do is call this function a bunch of times so that the jit spits out what I wanted to spit out so I'm going to create an array nevermind whatever, so just make a numeric range from 0 to th000, just warming it up.
This is the

guide

d profile optimization where you can run it by a couple. times and says oh, finally I know that's the best and most optimized exactly us and we'll go through exactly that part, so um uh, now I can build this, we can see, it will build and it spits out a path to Okay and there's no way to so we can see that unless you control everything that was just a link to a gotcha link to a d yeah so I'm GNA come here to my console the fonts are big enough I think so yeah sure um and I'm going to set an environment variable, that environment variable is going to be net jit dism and what this does is it tells the jit.
Hey, I'd like you to generate and show me the assembly code and then I can give it a pattern. which functions I want you to show me them for, so I'll ask you to show me the assembly code for anything that includes something in the method name. Now I just ran it and you'll notice a bunch of stuff just got thrown out. Wow, I just removed it because it had an infinite loop, but if you were talking about how it can generate multiple levels, you know, the first time it was run here we got what it calls level zero, this is something like that.
I'm not going to do any optimization. I'm just going to generate code as fast as I can, but I'm instrumenting it. I'm adding some tracking and the code here doesn't look particularly good as this is a lot of code for what should be something really simple, right, actually, right, we talked a couple of episodes ago that you can get it right fast or cheap pick two and this was a fast and cheap instrument so I could do it right later exactly so if I scroll down we see we now have a level one build there's a lot less code here and the jit is basically this is the optimized version of the code with whatever learnings you've had. from an older version um and we know we see it's a little thing with 29 bytes and there's all this other information that jit can tell us because we're not just disassembling the bytes of code in memory, this is actually the jit shows us that you know your vision of what you're producing, so the kind of internal aspects of what's happening, um, so this is another way that I can get at that and I use it all the time, very useful, however, the one of the

developer

s on the jit team, Microsoft's just-in-time compiler team, took advantage of that same result and wrote a really cool extension for Visual Studio called dismo uh dis asmo uh and if I just trim my uh so you can get it from extensions, manage extensions in Visual Studio I just put my cursor here and I control the scrolling d this opens this other window here where it automates that process so it builds and then loads, you control the scrolling there, it respects the font sizes, there's a little bit of opening and vs editor that opens it in something that does it.
I look great, oh wow, so I can make all of that work for you effectively. It automated what we just did manually on the command line, yes, with the exception that it didn't actually run the code, it built it and then ran it via jit, but it didn't actually break, as far as you can see here at the top right it says Complete Operations, so this was basically disabling layered compilation and just jumping straight to the do type. it comes out right the first time and take your time and get it right so this is really cool now I can click on any function and I can tell what the assembly code is you know control uh what is alt alt shift d ?
I always have to look to see what my CU fingers are doing. I learn it and then I forget. Your fingers know that your muscles know, but your brain doesn't exactly know, so this is very strict assembly code and the most relevant part is this. part here, this is the loop, you can see it's a loop because when I click on this label you see there's a bigger jump here that jumps to this label, so this part is just doing this and that's that tight inner loop, um so it does the comparison on line 2 three and then the result is like okay I did the comparison if it's greater or less than 24 it makes the call and if it's greater than it jumps back and starts all over again exactly and yes I don't know how to assemble, not that you spend a lot of time talking about AI, but one of the things like gp4 or you know chat GPT or Bing chat or gemini or whatever you prefer, they are actually very good at explaining. assembly code because they've seen a lot of it, so if I just highlight this code and mention the GitHub copilot so I can say, ask the copilot, explain in detail what this assembly code is doing. um, it's going to go instruction after instruction teaching you what each of these instructions does, where it's putting things and nine times out of 10 it's not only correct, but it's like clairvoyant guessing what you know, what was happening, wow and really to give you.
I have a sense, if you're ever looking, paste it into GPT chat or whatever your favorite thing is, but yeah, super great way to understand assembly and I want to mention something that a couple of people have had. in the chat not just a little bit, but from time to time some people say you know why Stephen uses the copilot. Are we trying to do it? Is this an advertisement? This is a 90 minute long ad for the co-pilot. That's not how this is coded. Are you using light mode? This is your Visual Studio. This is your real computer.
That's how you work. By doing this, you discovered that you were good at assembler because you weren't sure what was going on in a small region. and then I asked him, oh, that makes sense, it makes sense because there's 50 or 60 years of assembler out there, that makes sense, why would a big language model be good at exactly that, so the key part here is this loop really tight. where you know we're just incrementing um, you know, incrementing the iteration variable that we're checking to see if it's still within bounds, if so, we'll go back, we're adding the next value to our sum and just keep doing that until let's finish storing everything in eax, which in this calling convention is what is returned from the method at the end, so eax here is fine, so um really very tight code for our array and that's great.
Great, but you know, let's say I have a number number and I just wanted to add 50 of them, so what do I do? I probably have to pass additional information here, like if I need an offset and a length, let me ask you this. I, I know where you're going here I want, I want to make sure we don't lose anyone who might be wondering. I, you know, who may not speak assembly, but they may be fine. I can see where the increase is. the assembler I can see where the comparison is, but where is the array?
Can we visualize the matrix because I can see on line one where you just like, without mentioning it, you made a matrix with one nth from zero to a thousand, but where is it? This array is stored on the heap, so there's an object on the heap that's nth integers long plus some initial stuff about how long the array is, so there's like an integer that says thousand followed by those a thousand integers basically and then I have a local one on my stack. I have a local variable that I've called an array, no matter what I call it, but it's just a bit of space on the stack that stores the address of that. array is now storing it in a special way, this is not just a pointer like you would have in C or rather it isa pointer, but with some special capabilities surrounding it and that special capability is that it is tracked by a combination of the jit and the GC because that array is on the heap.
If I create a bunch of objects and some of them are released and then I create more objects and some of them are released, I can end up with fragmentation gaps between them. these objects and then I have to allocate something new and I don't have enough space to render my new continuous object because it's all these little spaces between these objects so these things can move around, the GC can move them around to defragment the heap and put them all together, which means that their addresses can change and when that happens the GC must update my local that is on the stack with a new address inside, let me tell me what you think about this analogy because when I used to teach this, I described it at the end of Indiana Jones when they put the Arc of the Covenant again in the giant C in the big giant warehouse is full of boxes is in a giant warehouse full of boxes and they are all the same, they are containers but there is someone in front with a clipboard who has a list of the location of each of the containers and If you come with a thousand containers, there may not be a space for a thousand, but you could put zero to 150 here.
I will write it. I'll put 150 to 200 in there so the thousand containers can be stored, but there's a couple in the corner here and there's a couple there a couple in front but I'm coming to you because I'm the clipboard guy at that moment if someone comes in and does a big clean out of all the old stuff and it leaves a lot of holes, opportunity opportunity, you can just go to the front and just push everything back, up, you got it right, that makes a lot of sense, good deal, so this reference here and this will also be important later. uh reference is basically a pointer that can be updated by garbage is a managed pointer that is being managed by the system now in terms of how it enters this function, like what is the array inside this function, we are passing some argument to this function and that means that the function needs to know where to go to get this array, so there are things called calling conventions which are an agreement between a caller and a caller E about where the caller will put things and where it will get the colli. things and by default Windows x64 uses a calling convention where the first argument will be passed in the ECX or rcx register, so if you see here, this is basically the register stores the address of the array, so this is verified here. where i am moving something to register r8d, this is loading what is stored at offset 0x8 from the pointer, it is loading what is in that memory location in r, it happens that what lives in that memory location is the length of the array then it is loading the length r of the array into R it is checking if the length is zero and if it is zero there is nothing to do so it will jump to the end and we will be done, if it is not zero then it is going to enter our type tight effectively what became a do while loop uh here so that's where the array is yeah yeah that rcx register is really interesting because it's part of x64, but it's extending the way we used to do things in x86 with e a backwards. supported extension which is really interesting and we'll talk about that another day, yeah, so if I come back here, let's say instead of passing the actual array, let's say I wanted to pass only the first 50 elements of the array which I wanted to summarize nicely.
I could do this weird dance where I would say copy equals new int 50 and then I see an array. police, direct a part that you want to like, say I only care about this middle part here and then go over and copy, but that's a big waste if I have to create a new array, copy the relevant part as if you do. I don't want to do that, so let me ask you, this would be to go ahead and put that Z control in the back and make it bigger. Anyone on line five? Is there an implicit for loop on line five?
Or is it directed at someone who is hunting? either they search or they just go to go directly to 50 but they still make a copy no one is looking in the array in that context no no so this is just making a memory copy this just says I want to copy 50 elements of the array to copy and then I'll pass the one for that subregion which may or may not be fragmented, we don't know if that's hidden from us, so this is in my number array. I have a thousand numbers but I only want to add the first 50 of them, so I'm going to create a new array.
I'm going to copy the first 50 numbers from the original array to my new array and then I'm going to pass in my new array, but I'm pointing out that the 50 could be in 50 different places or they could be three different places that are also hidden from us. Well, in this case, they are all contiguous at the beginning of the ray that I am copying I am copying it from the Subzero array to copy to Subzero the first 50 elements of the array and put them in the first five elements of the copy I guess I was confused at the time. thinking that again you know what's on the heap and what's on the stack and trying to think if the heap has been fragmented oh I see right, the array itself, the thousand elements of the array are always fluid, they don't have, they are intrinsic types intrinsic types, if they were not intrinsic types, could they become intrinsic types? fragmented no, they would always make it right that whatever is actually stored in the array is always contiguous, that's fine, but if what was stored in the array was just a reference to something else, like a pointer to something else, all pointers they would be contiguous, but they could reference things anywhere in memory, okay, I'm with you now, so take the 50 to be able to do this, but that's very expensive, I don't want to do that, so if you see older apis on the web, you'll be able to see things like taking an offset and a correct length um uh and then I could go in here and I can change what I'm doing here.
I could say I'm going to end up at some value which is doing that, just saying inside. I is equal to the offset I is less than the offset plus the length I++ and then I'm going to add whatever is in the array sub I, so I'm iterating over just a particular portion of this array so I can do that, that's me will give For me, the correct answer and not here. I could just pass zero and 50 um. This should be an array again, but if I get the teardown of this from time to time, I'm going to ask for a difference that I'll mention.
This is one of the reasons why I really like this tool. If you look at our loop, I'm focusing on the loop because it accounts for most of the processing if it were a million elements. We're going to do this a million times, you can see in our loop that now we have some extra instructions here, we have another branch, now another comparison inside the loop to make sure that the access to the array is still in bounds because this Isn't that super clean pattern from zero to array? The length that the jit can easily process this offset at this length could be anything that could have happened at an offset of 2000, although my length was only 1000, so the jit has to do a bounds check to make sure than what I'm doing.
I'm trying to access is still in scope, in fact you can tell at any time that there is a bounce check if you go to the end of the method and see this telltale pattern of calling main information, the help range check fails, this is a helper function i.e. jit will call when a bounds check fails and this will actually end up throwing an index out of range exception, so if this bounds check fails you can see it will jump here to this cold code at the end of the method for which you will end up throwing an exception and I highlight this because I just wanted to get a little bit of the array.
I didn't want to do everything, now I'm paying a cost for it and it doesn't seem like I can. You should have to do that, but yes, it's a significant tax, yes, per element you have to do this extra branch, so it was with an array and a length offset, but there are other things you might want to pass in here as well . So what if I wanted a list of int right uh and maybe then I also want to have lint? Maybe you also want to pass a subset of that list with a length offset, you know, and we were taught in net, which you know.
For many of us it happened later in our careers, we were taught to treat lists like a matrix, they are very useful, they are very useful, yes there is a bit of overhead, but we were told not to worry about the overhead. we, um, in general, no, you shouldn't, uh, the overhead is minimal, um, if it was a super super tight loop, like there were some extra instructions, we can see it here, so if I were to take this apart again, let's go to review. scrolling list like there's not a lot of code here and we're still trying to zoom in as much as possible uh wow wrong window here we go so you know again we have a relatively tight loop here um Note that there are a couple of comparisons in comparisons.
We had the one we saw for the displacement and the length. Now there's one more because we're dealing with a slightly higher level of extraction from the list at the top of the array, but again no. a huge cost, not a huge cost, the higher cost is good if you really wanted to have a single implementation that was able to handle the contiguous memory of an array and the contiguous memory of a part of an array and the contiguous memory of a list and the contiguous memory of a part of this list, what do I do, what is the only function that I write that allows all of those things to be expressed once because I could make four copies of this, I could have one for you know, list without offsets and one for the list with offsets and I could have another one if this was just going back to my array uh, you know, this is obviously, this is not a build because my build stuff, but it just shows all these different copies of the code to achieve um kind of writing it once being able to use it for multiple things now some people might be seeing saying well what about interfaces?
Isn't that what interfaces are for? And sure I could write a single method that took a list of eyes and because I and I could even, you know, make it a list of eyes, but then the caller needs to create a new list of eyes for the relevant subset, like so which I could if I took one that was a scrolling list, you know, and compensated for its length. and now I look at the teardown for that, now we can see that it's pretty tight, but all the work is actually happening in this interface call, so now there's overhead associated with this dispatch every time we come back to this. we're doing this interface dispatch correctly, you have a closed loop and then an interface dispatch in the middle of the tight loop that does God knows what exactly and then you know I'm okay, so maybe I'm okay with that cost, but now, What about other types of tickets?
What if you got an interrupt and you got a pointer and a length of C or rust or forever and you wanted to process? I had some zoom. on the left side there for me sorry yeah uh or or as suggested to me what if I just had one alic stack so if I had a pointer here and a length and I wanted to say in some way that I want to sum all the values ​​that are stored in this pointer up to that length. Now I need to write a full sum function that uses unsafe code to process those pointers?
It becomes this debate between performance and maintainability. I really want the one implementation that I can give arrays and lists and stack data and data that I got from the interop. I want to be able to write that once and have all the efficiency, but I don't really have a good way to do all that. and like having my cake and eating it too, why isn't it a matrix like this? The array feels intrinsic like it's a bunch of things on one line, but I feel like we know too much and there are hidden implementations like I want. something that will hide parts of things from me, but I also want a low l type, well, could I know if I had my matrix here UHS?
Whatever it has, it doesn't matter what it actually has in it. um, I can, this is just data on one line and I can on an unsafe block. I can say I want to get a pointer to the first element of this array, but now I'm writing C, but now I'm writing C, now this. the sum function becomes uh int star pointer length int and now I have friends who are like what and as soon as he said unsure now you've made a foot gun uh unsure is your foot gun and you're basically saying good luck, in actually unsafe is disabling what the jit gives me in terms of things like debounce checking when I know I can exit the end of an array and the jit goes whoa whoa and throws an exception the moment I try to do it with a pointer . have fun, enjoy your AVS and you know the security vulnerabilities, um, so you know, we try to avoid using pointers whenever we can and reduce thesurface area in which we use pointers to the minimum, but if it is the only way.
The only way I could write a function that would handle arrays, pointers, lists and slices is by using pointers, so I'm forced into that world, so this is where the interval comes into play, the interval is what allows me to have my cake and eat it. Also, if I go back to the code that I had here, I'll go back to my usual value for each loop and I can say I have a span of int and I'll say for each uh in value. in Span sum plus equals value um and I can if I look at the disassembly of this here is my I want to zoom in uh I'm learning uh here's my tight inner loop I notice that there are no extra unnecessary branches and I can still call this function with my array I can call this with a new interval around my array that accesses only the first 50 elements.
I can call this if I have some pointer that I stack alect. I can call this with a new interval int uh pointer 1000 I need to put this in a safe block for my pointer usage is like I can use this implementation for arrays for portions of arrays for FS for lists if I want something that has a kind of piece of contiguous memory. Now I wrote my one function and I get the nice gen code for my one function, but I can use it with all these different things, so the interval is this great unifier that recognizes that it's more than an array, but it's an array within the context of this world, we have this net execution time, exactly you know when It was when we were talking about async and await.
I think I talked about how one of the big innovations with tasks was not so much an innovation but just a recognition that by having one thing you can take all your async functions. that are doing different things but they all generate a task, so all you have to do is that one task doesn't matter. I have a million different implementations that produce a task. I can write help in terms of task I can write a language feature like async and a weit in terms of task is this unifying interval is exactly the same for contiguous memory um uh computer science the joke about what are the three the two things about the two difficult things in computer science or are they naming things, uh, what about a mistake?
And, uh, I know how to name things. I don't remember what the third one was, but span is an incredibly awesome name when I find a name like I don't know if. this happened if you were in the room, but there must have been a time where there was a group of people with a whiteboard and a thesaurus trying to figure out what to call the name, yes there is no interval, there is a concept of interval in linear algebra, right? It is the span of a set of vectors, but it may have been obvious, but to me a span spans like a bridge spanning space, it was a word that is not a matrix, but is expressed by being more than the contiguous space. across the bridge. of something that I just love because now that I understand it and it has a great name you didn't just call it Foo, it clicks for me, it expands the space and it's also nice because we use it so much that it's nice. which is something relatively short and concise that we can easily express because now it appears everywhere, it is, it is, it permeates the entire set of core libraries that make up up.net.
When we say span, we don't say span. Cally, we say span. of tea span of tea span of tea are you absolutely married to the concept of generics generics made the span of tea possible Correct, there is an object or there is always a span of T it has to be a type that is specific not only so that it does not there's no generic span there's just a te span and a te read-only span there's also this could also have been uh a te read-only span that's a great point actually um and basically there's these two that and we're going to below us We'll get to the core of the difference of what they are, we'll actually implement them and see what the difference is, but there's a very, very small difference between the two.
Main can I write to it or not, yes it is span, if not it is read only, span, yes, like my floppy drive has a jumper and I pull the jumper and now I can't write to the disk exactly and you can see I'm passing in a span here, but there's an implicit conversion from span to read span which is pulling that bridge right now, so now here if I tried to write to a span of zero, it would say what are you talking about, you can't doing that doesn't even co- The pilot knows, oh wow, that's cool, um, yeah, so span is this great unifying force and it's allowed us to do wonderful things, not only has it allowed us to consolidate these places where we had multiple copies of routes of code, but it has also allowed us to take. um code that we otherwise wouldn't have been able to express well or efficiently in managed code and do it, for example, in an array. sort was implemented in the entire framework, it still is and before Net 5, I think it was implemented in C or C++ in the core runtime, so you called lightning. type, you would end up using native code and that means pointers everywhere and that's mainly because a lot of the code where we were doing things, um, was attacking these inefficiencies where you know there would be additional limits. checking and all that and we didn't really want to allow that in this really central critical code path that needs to be really efficient for sorting, but with span we were able to take a lot of this pointer-based code from C and basically take it out, paste it into a c , fixed a few things in terms of extension and 99% of that code is now

complete

ly safe using span and readon span.
There's a little place, I think, a function in the middle where there's a critical loop where the limit is. The jit today failed to detect that the bounds checks could be removed. We left that piece using pointers in C, but everything else is just spam and then an array. sorting is just calling that code based on spam, uh, but we were also able to accept that now that we have an array. sort in inter we have interval sorting in terms of interval, let's expose that right now, we have public apis in terms of interval for sorting and now anywhere you get an interval, whether it's interop or a list of t or uh on the stack or whatever it is, now you can use that functionality and there are these benefits of KnockOn where you take it just in terms of arrays, now you take it in terms of spans, you maintain your efficiency, you maintain your security, but now you can use that same functionality . in all these different places, it also allows you to do some really interesting things, so for example, I can write um readon span int uh and I'm going to use the collection expression syntax that was introduced in C2 so I can have a set of numbers here um and what this says is that I just want to construct an interval with three numbers 42 43 I think I meant 44 nevermind 40 I can't write 40 44 um, but the really cool thing here This is not assigning an array, it's not just putting this on the stack, it's actually recognizing, okay, you've got some contiguous data here and you want to be able to reference it by something that you can't write to, so this data. is immutable and we could actually include this data in the assembly itself, so just let it live in the data part of the assembly and when I create a read range around it, I'm literally referencing the data directly in the binary, it's Okay, so hang up. in put, make a regular just int array with those three the way we would have done it in the old days, like not above, just you know, you know, so, you have, you're, you're in class, it's, it's what to be. 101 and you can create a new one in this case here about what we know what the compiler knows on line four that it doesn't know on line three that can make it more powerful the compiler doesn't know if you're going to do this, but we're hinting that we're this Yes, I can do that, so this will end up being basically allocated space on the stack, but the fact that this is a read thing that is intrinsically known by just-in-time. known for the C compiler known for the just-in-time compiler says, "okay, you're never going to change this." I can put it somewhere, you know, where I just convert it directly into the binary and then there are things that build on top of that, so if I said um read-only bite and I used uh hi Scott using the utf8 syntax uh that came in C 11 , so this is a utf8 string basically the C compiler is generating the utf8 bytes from this string and passing them directly into the assembly and then this span just points directly to that data in the binary, okay this could be Dumb question huh, but that's the value I'm providing in these conversations, as you know in the Unix world there's a thing called M protect. like a system call to go and say here are some pages of memory that are read-only and protected and there you can't write to them, you can't, it's basically schmod for memory, I say schmod chod uh, you know, you're basically saying chod minus Is there anything I can do to say that that is not only read-only but protected at the OS level? you can prevent it from being written to uh, you can, that would depend on the runtime to do the runtime, um it does things like, um, ensure that, wherever jit wrote the code, it changes to U, you know, it can't be written once it changes to executable and stuff like that, but there's nothing that's exposed in the managed APIs for a C

developer

that would have to invoke P to do stuff like that, okay, it's interesting, so here we have a read-only byte range and it's UTF bytes and them.
They're in the assembly, they're just hardcoded and that's fine because it's reasonable to be hardcoded, yeah, and there's really cool things you can do or, um, let me comment on this, you know, one of the takes something like a string. um strings Inn net are immutable objects, we really don't want people trying to use unsafe code to modify the contents of a string. You can do it sometimes and it might work, but it's a very bad idea to go and mutate immutables. things that use pointers and trample on data that really should be immutable, so you know for a long time we wanted to have an API that would allow you to create a string and at the beginning of its lifespan fill it with something, but then change it. to a point where okay, now it's immutable, now that you're done writing this and you know we contemplated exposing it, imagine you had an API like create that required a delegate, let's say, let's say, let's say you took a character array action . or something like that, um and uh, then you could call this create with array and you could write to this array to fill it with whatever data you wanted.
The problem is if we actually passed that mutable string here and let you write. to that mutable string, you could store this somewhere, you could have some static field, static character array and then here I could say I'm going to hide this, oops, array equals array and that would violate what we were trying to achieve because now you have these m things here that you could mutate later and we don't want to let you do that, but with spans spans, for a reason we'll see very soon, they can't live on the Heap, they can only live on the stack. and that means you can't have a static field that contains an interval, you can't take an interval and store it somewhere, which means that now there is an API on a string called string.c create where I can say let's say I If I want create something that's 34 characters, I'll create one that's ID followed by some guid, so two characters for ID followed by 32 characters for a guid, so I'll pass the actual guid here and then I can say uh.
I'm going to get a oh, I had these intervals going backwards okay, so now this is basically creating a string that is immutable at this point, but here I can write to the interval that points to the string data, so I could say something like um ID as copy from interval to interval and then Guidry format uh in uh interval. cut two and I want to write just the characters so I can do something like this to basically get ID followed by all the hex characters from my guid into my string and I can be sure that no one can take this span and hide it.
Store it somewhere and then mutate my string, uh, because there's no way to do it, you can't retrieve this span from this call and so it allows us to expose them like this, where do I file them as an application developer? because, like string.c create, it allocates a string whose sides are determined beforehand, right, you have it a priori and it will be a heap allocation, it will not perform multiple allocations and also string.c creates has access to private data that does not we can see and they have hidden in that kind of things, I guess they are in that closure, uh, usually you know that strings and network are immutable, so any modification will generate a new string And how many assignments happened here and and what was modified and what was not modified aassignment for the string that was 34 characters long and basically the runtime says okay, strings are immutable, but as part of building the string containing this data, I'm going to temporarily give you access to its buffer. backup to write the data directly into it, knowing that you won't be able to do it later because you can't take the time I'm giving you and redeem it, so it allows us. exposing additional things like that and we're constantly discovering these new benefits that span and span read only, it's interesting because it's almost like I probably did it in a UN.
I probably would have thought of unsafe, but this is kind of like unsafe, but it's probably okay, this is guaranteed by the runtime to be safe, yeah, and using unsafe with pointers doesn't work, it's not so true, yeah, so We only have about 15 minutes left, I think that's what I would like. What we need to do now that we've talked about the benefits of span is actually implement span because it turns out that even with all the benefits that we've seen it's incredibly simple what it is, one of those things that we liked. ah we should have done this years ago yes but it took a lot of steps to get there and you know for example since C 1.0 we have had the right reference so if I have an integer I can have some function that requires a reference to an integer, no, so I can call this and say refi and what they're passing to me like we talked about with the type of arrays and then the pointer, I'm passing a pointer to this location, yeah, and if here uh IIf you knew that if I were to console the .ti I line and here, if I set I to 42, I would run this code and eventually my window should appear somewhere.
Probably on another monitor we can see that you know the I. here, even though I initialized it is zero, I was passing the address and then I set it to 42 here because I'm writing to what was at that address, not the address itself, not the location itself, um, and I get my my 42 came out um, so we've had this reference concept for a long time, but this was the only place where you could do it as part of these functions, but then with the C versions, you started being able to use ref in more places like you could also have a reference return value and then you could say return refi and now this function can also return return references and you could have locals that you might as well keep when you return that ref eye Don't you?
It's all the same eye like you have the referee now one two three four five six times five times six times this is it they're all the same you're just saying all those referees just right? touch that eye and make sure it's the same one and avoid well, they're all, they're all pointing in the same direction, I got you, um, but the critical innovation for span was being able to have one of these not just as a plot. uh, not just as a return, not just as a local but as a field, because having it as a field basically means that I can then store it along with some other state, so imagine here, let me control Z, control Z, return to Glory , that's what we're saying okay, so let's call this reference and imagine that I also accepted a longitude, right, that's all I need to know about a contiguous region of space.
I know where something starts and I know how long it is and then I just want to be able to pass those two pieces of State together because if I can pass them together then my sum function knows where it's going to start and it knows how long it's going to last and then whatever I can obtain. reference to can I get a reference to the beginning of the data in the array if can I get a reference to the beginning of the data in a list can I get a reference that is just a pointer because I can actually treat pointers as references in fact we can see that if I have some unsafe code here and I write star pointer equal, I'm going to take the address of I, then I can also have an INT, ref and I say reference star pointer and now I have a reference a managed reference for this same pointer value now I can use this function with that reference and that length and do whatever I want and if I could just put them together as a little tupal basically and pass that tupal, that's what it encompasses In fact, if we just implement ours, I can say :uh, I want a read structure, we'll call it my T interval and I want it to have a pair of data.
I want that length and then I also wanted to be able to store a reference, so I'm going to say I want a private read reference to a T and a reference. Now you'll notice that I'm getting a bit of SLE here, we're not just allowing these references to be stored wherever we are. uh we want these things to be forced to always be on the stack, so I can't put them on the heap and that means I can't make this just be part of a class or a struct that could be part of a class , so now we also have the notion of a ref struct, which is just a struct that is allowed to hold references, but that also means that now anyone who consumes this, if they try to put this part of something else, that thing itself has to be a breaking structure this is a span which is literally a span there are helpers and there are helper functions but that's all it's my uh uh friend of mine uh dougas crockford uh came up with Jason's optical JavaScript notation and every time you ask him like Hey, did you make this up? he says no, I figured it out, yeah, it was always there, there's one of those types of situations where it was always there, he just got the disk, um, and then you know we can start, you know there are obviously operations associated with SP , so we can begin. adding them, but from a state perspective, if you go and look at the source.net source or inet runtime, this is what you'll see seriously and that's it, so let's add some things, so let's add a constructor, so I want to be able to to take um, let's say I want to be able to take a reference to a single element, so I'm just going to say reference.
I didn't want my T here, so I can store my reference and I'll set the length to one and now. here I can say let's say I had my I uh 42 and then I want a span that is new my int refi span so now I have a span that refers to the single element contiguous memory region for I and if I had a interval indexer which we'll add in a moment and I came in here and said it's equal to 43, so if I printed this it would print 43 um. I could add another Constructor that also took a length.
Interestingly, this Constructor does not exist in real span because it is very unsafe to pass an arbitrary reference and an arbitrary length. Could you know if I would have done it here? Thousand. I'm passing a reference to this, but I'm saying this is a thousand integers long. I just exited, you know, I exited valid memory, so this doesn't exist in the range, but it does exist. We place unsafe things in places that are clearly marked as unsafe. Somehow this exists as an API called corrupted memory. creat span and if you go, look at the Memory Marshal implementation. creat span which is the implementation of Memory Marshal. creat span um so you know we can, we can build that kind of thing, um, we could come in here and we could have another one that takes an array, so I could say my span and I want this to take an array, um, well, we know the The length here is the array. length and we should probably do some argument validation if it's a null array um and then what am I going to do as a reference here?
Well I want to get the first element of the array and this will work unless the array is empty, where In case this blows up, but that's basically what it's doing, there's a dedicated API you can call for this called Memory Marshall. getet array data reference which basically returns the Subzero reference array, but if it's empty, you know how to handle that, um, and you know, we can construct our range this way, so we've had some, we have some constructors, let's add the things that actually make this useful, so I want to be able to have something like that.
I'll deal with the syntax in a second. um, it's going to return a t for the moment and we're going to say this requires an INT index and I'm going to have a getter. Now I need to do a little validation, so I want to validate that this index is within limits. I think we talked about this last time when we talked about linking a really easy way to both validate that the index has a length less than length but not less than zero is to convert it to u so that it fits. The co-pilot knew it too and that's what he suggested I do, um, but uh, and then I could just here. return um and then use this method uh unsafe add this is taking the reference and it's just doing pointer arithmetic, it's doing pointer more, but in a way that the GC can track, so now I have my implementation and I might as well write a Setter o You could simply make this return a reference using the reference returns we just saw.
I don't need a Setter because I'm just returning the reference to that location that I can write to, so if I come back here now I can actually write what I was talking about. I can write span Sub 0 just the same. I don't remember what number I selected and if I print I get 54 for writing through this spam um and you know, or we can build um uh we can create a cuts routine the cuts are very valuable cuts, I just have my reference and my length. I'm going to shrink one side or shrink the other side or shrink both sides so I can write For example, I want to return a new interval.
I'll call it cut and let's just cut from the beginning, so, as you know, move the beginning a little bit up again. We want to do some validation so we can say if, if the offset is greater than the length, we throw an argument exception and then we'll just return a new span and I'll let it write it for me, where we're going to offset, we're going to increase. our reference by the offset and then decrease our length by the offset and now we have a slice and so on here if I were to go in and if I said my character range is equal to new, let me give you a character array, hello world. say while, oh, let me also make public a little public property here in length, while space .length is greater than Z and console. the interval of the right line the interval sub Z is equal to the interval. cut Subzero, so I'm going to print the first character and then I'm going to create a new span that's missing that character and then it's missing the next one and it's missing the next one, and so on, and if I run this, whoops, uh Oh what happened!
Wait, I have those extra ticks in there, yes, I did something wrong, oh, I passed the wrong value here, yes, you can't divide the zero, I can, but, it just returns itself, so now if I look at my console here we can see that you know we're getting exactly what we expected we're just changing the initial offset as we go and that's span and now we can type readon span and literally like 30 seconds I'm just going to copy and paste this and you're going to set up a bridge yeah , I'm going to do a search and replace my span, my read span and now the only difference here, uh, is that this needs to be read, it's that serious, that's it, now it says where we are.
I have a reference to a read T. I can't, I can't write to this right now, there is another critical difference that we don't have enough time to analyze, but there is an additional check that is done as part of the span Constructor that doesn't have to be done as part of the read span constructor and it has to do with the covariance of the array uh or the variance of the array, so there's actually one more check here in the Constructor that says if the type of T is not a type of value and an array. getet type uh is not equal to the type of this so it throws an exception um and if we want to talk more about why that is, we can talk in another video about it uh but that's it, that's what these things are and um, ya you know and you However, point out that things needed to be quietly built to make this happen.
You know you need it. You have memory. Marshall array data reference down there. You know you have the reference. Concepts that you have only read. as a key word, these things had to exist silently before they could be discovered in such a clean way once everyone did, although how hard was it to convince everyone and how did they convince each other as if they knew we could start? PL, removing things from underneath and making the network faster, was there any pushback or was everyone just excited to start tearing it down and replacing it once we had it?
It really was a no-brainer and, as you know, there were always discussions about whether we want to. adding X would allow y and we still have them because you know the incremental value you get by adding something incremental. It's worth it? The return for you is a return on investment or not, but in essence it was a no. -It's a no-brainer and using it was a no-brainer now that we had a lot of debate at the beginning about whether this is exactly what we wanted because there are other ways you can do similar things. For example, you could say that matrices are spans and intervals are matrices,intervals are not just a view of a part of memory, maybe you know maybe they are the same thing and that would have a lot of different benefits and a lot of different costs associated with it, but once we get to this like where it was, a lot other things just happened and we actually got span before we had the C ability in C to write references, so we created some kind of special recognition at runtime so we could write this without actually having it and then , once we got the capacity in C we changed it, yeah, and it was very clever, it feels like making an extension array, but not trying to pretend it's an array, it was a clever thing because you're doing it on its own type , you can all live your lives and choose to write your applications and only use the stuff in BCL and the Base Class library that you're used to using lists and arrays and stuff like that, and span will just do its job silently. but then people who are very focused can choose to level up span and make it part of their lexicon and they will also be successful, so really it was a win-win when span of te came into the picture.
I think so and you know it requires a video. I like this to understand the concept and how it fits, but once you do it, when you care, it's invaluable. Fan Freak Tastic de.net delves into tea. Thank you very much for the gift of your time and thank you. audience, certainly for the gift of your audience, if you like this, share it, tell people and let us know in the comments what you like and what you don't like and we will try to keep collecting more of these that we still have. More videos to come.
I know you have a lot more in you and soon we will start introducing other friends of the team too to delve into deeper areas that we don't know about. Thank you so much.

If you have any copyright issue, please Contact