 So I've been delving, you know, as part of this this audit on string here I've been delving into things for long enough that I'm ready to talk about some of the changes that are coming up Because it's not there's a lot of internal changes and a lot of You're not gonna work with it too differently, but it's gonna work a lot better a lot smoother. Wow I can't really say that to some extent you're gonna work with things a little bit differently But we'll get into that and how you'll get a different type back from certain things, but it should still feel the same Okay I'll get into one of the most super I Can't even say superficial one of the one of the big driving things Was that you know as part of the patterns engine I had Largely discovered Actually, no Jesus. We need to go even further back to one of the motivations for developing this on net in the first place because one of the reasons not all of them, but one of the reasons actually isn't a reason anymore and Doesn't matter so string interning is a very well interning in general is a very useful optimization, but For strings, and I would really argue any contiguous sets of memory It's not really that necessary and there's better optimizations but the idea behind interning and interning pools is You've got an object It's expensive to copy objects, especially larger ones So you store them in a pool and when you Need a object of the same identity you return a reference to the same thing in the pool This would be with string specifically You've got a string already in the pool for hello world and you need another instance of the hello world string Well instead of actually getting another instance get a reference to that exact same string. That's already in the pool the process of interning itself is Obviously going to be more expensive than just creating the new instance in the first place But certain operations, especially equality wind up being so much faster and The overall memory pressure is so much less that interning is often justified And it depends on the exact use case, but strings are a notorious example for it So if this optimization is so profoundly beneficial, why am I saying it's not even relevant anymore? Well remember what the point is to be reusing existing stuff You've got the string hello world again as an example and you want to get the substring from You know starting at zero with a link to five so the hello part of that using Strings you end up getting back a new string instance a new Instance Okay, I mean you can intern that instance. So it's not that bad, right? Sure, sure you can There's a better way slicing spans Really just they're fat pointers. That's what those are. They're fat pointers but because of the Semantics build up around them. They allowed you to make several assumptions that wind up being incredibly beneficial Now this approach Now we're going to explain how it works, but this approach only works for contiguous memory arrays strings however are arrays and the idea behind it is that You keep each string in memory only once just like a pool But instead of taking this thing and putting it in a pool It's just put in memory somewhere given how net works. It would always be in the heap And because you can't turn the string and turning off it would wind up getting interned But that's not potentially but that's not really an issue Because this approach still works regardless Maybe a little bit better You would avoid the heavy cost of interning in the first place if it was always put in memory But you would need a native language and even more specifically you would need a systems programming language That does not use a garbage collector so I had a without a garbage collector attached to a pool or C or C++ rust would be another example And rust actually supports this natively So that's fantastic C supports everything You have to actually implement it yourself, but C literally supports everything I'm not sure about how to actually implement this I Never tried I've never needed to just I avoid add a for string processing or text processing like the plague because it's so Not well suited for it but the idea is you've got these fat pointers called spans and the net world or slices and rust But they just reference the existing text that's already out there So obviously a fat pointer to the hello portion by pointing at the hello at the H in hello And then having a length of five. So we've got two integers one for the address and one for the length That's obviously very lightweight compared to actually creating a new string instance for hello, which would require the Five sixteen bits So we're already above the address and the length But it would also need the length and a Null terminator because even if your language doesn't rely on null terminators, which is smart for compatibility sake efficient compatibility sake you still add that null and Most language designers aren't doing the smart and clever thing that FB string does So you've got both a null pointer a null Terminator, so we're now looking at six 16-bit integers and The length field so the equivalent of four 32-bit integers and that's only going to grow with larger substrings so this Obviously saves huge amounts of memory so string interning Isn't a necessary thing anymore But because this saves so much memory because this is such a profoundly efficient optimization for for strings It makes a lot of sense for me to and I was thinking originally Convert the entirety of string ear to use read-only span of car by default so that is accept that as a parameter use that as a exclusive return type with the exception of certain situations and Reap the benefits and So far from benchmarks that I've been collecting. It's a definite improvement Sometimes barely any improvement at all because you're forced to still do an allocation because of how the the the algorithm works Pad is an example of that you are adding text potentially you may just return the same exact instance, but You were typically going to be adding text what Makes a little bit more sense than using read-only span of car for everything. Oh I'll get this aside real quick Because there's numerous types in net that can represent text and can easily be converted to read-only span of car What I've been doing is providing overloads and I will continue to do this. This is a definite thing Provide overloads for those various text types So they can all share one single implementation and what the overloads do Is because you keep that there's no common generic for all of these what the overloads do is a Simply convert that type to a read-only span of car internally and then call the actual implementation So you can provide numerous overloads that use one single common implementation Typically speaking it winds up being string character array span of car read only span of car and a Fat character pointer. So that's five So typically speaking we're talking five overloads with one common implementation. That's fantastic because that's a lot less effort But a lot better type coverage And doesn't require the downstream programmer to do the conversion So there's no maintenance overhead for this They just there's there's the one implementation if there's any bugs to fix it is in that implementation What I've been doing is read-only span of car as the only return type again with some exceptions will get into that but That being the return type using that instead of string and the reason being that well, there's many situations Well read-only span of car is the general text type anyways any actual text type can become that So it just makes sense to use as a generic anyways, but But what I was thinking there are certain algorithms where You don't actually need to allocate a new instance You can get the entirety of the text that you would return from the text that was passed to you trim is a fantastic Example of that because what trimming actually does is finds the indexes Depending on what trim you're calling specifically it finds different indexes You know trim end gets the very beginning and then goes to the ending that you actually trim at and returns the portion from the beginning to that index Trim start Gets the point at which it starts and then just grabs the entire rest of it And the the full trim gets the starting and ending indexes and trims the inside of it But in every single instance you don't need to allocate anything new and that's an optimization You are not allocating new things in memory That's fantastic But there are algorithms pad being the example one example, but there's numerous others Where you have to allocate new stuff because you are literally adding new stuff You can't reuse the existing string. You have to create a new one And so what I was doing in those instances was still returning a read-only span of car Which is totally fine. There there's no reason why I can't continue to do that But it was allocating a character array internally and I used character array rather than string so that I can mutate it throughout the Lifetime of the function, but then returning specifically a read-only span of car so that when it was returned it was immutable You know keeping with my design philosophy of mutable internally, but immutable externally Which is fine. That's that's actually sort of the ideal way to do it because you still get the immutable side of programming the functional programming advocates which is good But you don't want to do that internally because the performance implications are horrifying There's a problem with this Not a technical problem, but a user understanding problem Do you document in each one of those that an allocation is happening? I mean you could you could add into the documentation in the remarks section. Hey, this this algorithm allocates So when you call it it will be forced to allocate a new thing onto the heap. I mean, that's fine but Obviously, I'm a big proponent of keeping documentation extensive documentation, but it's always better when the code itself is documented You can't always do that for example code for Well, one fantastic example is the Rabin-Karp algorithm calling it Rabin-Karp only means stuff to people who know what Rabin-Karp is And how it works and why you would want it over a different one You are forced to add an actual Documentation comments to fill in the difference for the people who don't understand that and Ideally you would have an abstract enough just search function that used Whatever search algorithm as appropriate, but he I still like having those and still need those That to call specific algorithms and specific certain stances. So they're still exposed publicly regardless But you you can explicitly declare that an allocation is happening for these text types Instead of returning a read-only span of car when a Allocation is happening return a string when an allocation is happening. You're allocating regardless. There's no additional overhead Use the character array for a mutable I mean, you could use a span of car regardless, but You're Have to allocate the actually no you might be able to use stack alloc I could I could Because I'm putting it into a string stack alloc might be fine because it's going to do the allocation onto the heap using that data Off to benchmark that specifically but for algorithms that have to allocate return a string The fact that I'm returning two different types isn't going to matter because as I had stated I'm providing Overloads for all sorts of different text types to be used regardless So whether you get a string or a read-only span of car doesn't matter as far as using the entire library You can use any of these functions with it regardless But you now have inside of the function signature declaration itself Clear documentation that this allocates because you are getting a string back this also means It may be put into the interning pool if the dotnet runtime decides that is the appropriate thing to do It could skip that that depends but regardless it does make clear that you have a new object on the heap Without needing to put it in a documentation So that's fantastic But as I had been stating there are situations in which I simply cannot do a read-only span of car as a return So what I what I mean Well, there's two functions which should not allocate But work and they were doing very expensive allocations Chop and split split has to internally, but it should not allocate for its return type Chop does not need to allocate a single thing But it was Chop was allocating a new string instance for every single chunk and a new array which stored all of those instances That is a very Expensive operation split as I had stated has to allocate internally It has to allocate either an array of the indexes to split at or Well, depending on the split specific split algorithm Depending on the parameters that you provide different algorithms can be used but for the purposes of this let's just say that you have to have either an array of the indexes or a List yes list of the indexes Now you can use a list interface that is actually using a dynamically resizing an array and then start it off with such a reasonable size that it's very unlikely to have to resize it at all in which case you actually get a Tiny amount of performance overhead, but essentially the performance of an array That's the ideal thing to do in that situation, but Yeah, that actually has to be done for split to work because you can't It doesn't it doesn't matter if you want to know why that has to be done Look at the algorithm, but I don't need to use that. I don't need to explain that to explain what I'm trying to explain In both instances though, you are Return you you want to be returning information that is already part of the string itself You're not adding anything new. You're literally just sectioning up the existing text Just based on specific rules about what those functions do Chop splits it into chunks of a particular size Split splits it up into chunks based on a specific separator That's you then have to search for and that's Why the allocations are happening internally, but in both cases you don't need to allocate new chunks. You can use slices spans to Reference those chunks But there's a problem with that in ah you motherfucker. I just got bit now In both cases reference with these these slices are rough strokes, which you cannot use as a generic type parameter Which means You cannot put them in an array or a list. So how do you return? multiple of them shit Here's the reason for the unique return type For chop you return a chopped string for split you return a split string And there may be other instances where this type of approach is justified But what those types are are themselves restructs, which can of course contain other restructs do I Make them as sort of link lists where Each of these restructs hold nodes that are themselves restructs and they link to other ones Because that's expensive that doesn't seem to actually solve the problem that we're trying to do you algorithmically calculate them Whenever you can algorithmically calculate something rather than reside rely on a dynamic data structure you have huge improvements For chop what you do what the chop algorithm actually does is creates one of these chop strings Precalculates the total amount of chunks and then that's it If you do not do anything with the chop string then The only performance overhead was creating the chop string object Which is a restruct that has barely anything in it and the cost of calculating the total amount of chunks Which is some very basic math What it then does is exposes an API that looks a lot like an array of strings There's an indexer that indexer Actually slices out the chunk Based on the information that it now has it uses the max chunk side the max chunk count as An upper bound so that if your index is above that it knows to throw the appropriate exception and Uses the fact that the chop size was passed into it And the string or any text that it's referencing Now has everything it needs to take the index you specified and algorithmically calculate the index of the Chunk to slice and return back the slice for No Allocation as far as the heap goes enumeration is Actually really easy to implement as well now you can naively do it based on just iterating over the indexes and as of right now, that's how I do things But there's a further optimization you can do because again you have all this information stored in the chop string type You can completely bypass the indexer and do the iteration manually, which is actually a little faster But as far as performance goes, I don't think I'll have a benchmark so that I can show you Well, I'll see if I do that I can add these in but I'm gonna be doing large-scale comparisons later But the performance improvements I've got for creating the chop string itself is Anywhere from like two to eight times faster indexing any particular component was like two to four two to five times faster and Iterating over the entire day of it was exactly the same speed But again like I had stated I don't actually have the optimal iterator for that yet So there's a minor improvement that can be gotten from that And then also keep in mind that more long term These aren't allocating anything onto the heap the cleanup for them is done immediately when the function that you're doing this inside of is Finished because it just disposes of the entire function stack frame Which means it disposes of all the restructs That's fantastic. So More serious programs that are utilizing these things The garbage collection pressure is going to drop Now it's harder to measure that But it's clear that is going to happen We're talking about way less actual allocations split similarly works on a similar mechanism the Amount of pre-work done is much higher though because it needs to actually get all of the indexes Which means it needs to search through the entire thing first That's a lot more expensive, but regardless We're still not doing the allocations that were being done before so there's still an improvement It's much less than chop, but it's still an improvement yeah So it's clear that we can get performance boosts out of using these specialized collections There's More opportunities to do this see you can Take the functions that were described in stringing your core and split them into two broad categories on one hand you have literally just text processing algorithms the Honestly the majority of it was text processing algorithms and that's because I didn't even fully realize what the other category was See the other category despite still being Text processing happened to share a lot of similarities to link now This video is already about half an hour long after a little trimming. It's going to be just a few minutes less than that, but There's a lot to cover as far as link in that side of things goes so I'll do another video I don't know later today tomorrow something I can cover that side of things, but that's That's it's all another FD portion and I want to get back to working on things. I don't want it produce insanely long videos so That's it for this. We are going to be talking about the more linkish side of things and why that stuff matters and changes involving that and more collection types that need to be done and Problems that are introduced because of that and because of not Greatly thought-out parts of the dot net ecosystem Parts of the runtime that weren't greatly thought out and Some things that can be done about that Because I'm like just bitching. I like solutions And computer science has exposed a lot of solutions that don't always make it into the practical side of things But there are so Have a good one guys. We'll pick up in the next video