 Hello folks welcome back to another stream this time is going to be another I guess we're calling them Impel Rust Streams now But this is going to be fairly different from the other Impel Rust Streams in that we're not tackling like You know an algorithm from a paper or some deeply complex technical convoluted data structure an algorithm concurrency primitive thing Majig instead we're actually going to tackle something that is you know ostensibly a a Fairly straightforward problem, which is we're going to play the word all the game and Specifically we're going to write a program that's going to play word all for us And we're going to try to make it as fast as possible So my hope here is that this is actually more approachable than many of the other Impel Rust videos We won't probably get into too many like you know weird unsafe pointer manipulation type things and instead we're just going to write rust code and then we're going to be Performance profiling and then optimizing that code over time Now I'm going to do a quick wordl intro for those of you who you know either have heard the name And I have no idea what it is or haven't even heard the name Wordl is this actually let me give you a dark mode here because that makes people happy the wordl is a Fairly simple word game where you are trying to guess a five letter word from the English language There is a sort of dictionary list of which words you can guess and you get six guesses to try and when you make a guess You are told which letters you have guessed correctly which letters You have guessed that aren't in the word and any letters that you have guessed that are in the word But aren't where you put them. So for example, let's say that I open here with bears Right. So here I learned that there is a B And the B is indeed in this first position in the in the final word There is an E, but it's not here. There is not an A. There is an R, but it's not here and there is not an S There could be more ease. There could be more ours. We don't know that from the guess that we did right now So for example, let's say that we think that it's gonna be BR with like BR seems like a thing the word start with and then there's gonna be an E somewhere so we could say uh Broil, oh Broil, okay, so you're not restricted to using letters that you have guessed in the past Like I know that there is supposed to be an E in here, but I can still guess something that doesn't have the E in it part of the reason why I'm going through this and Choosing particular words is also because I have done today's word all so I know what the answer is But let's do broil Okay, so we now learned that BR is in fact correct There is an I somewhere, but it's not in that position. There's no O and there's no L So at this point we're in a pretty good position, right? So we know that there's an I and an E in the word It could end in an I but that seems weird So it's probably an I there and so it's something that starts with Bri and has an E in the last two, right? So let's try bride. Okay, bride All right, that's pretty good So it's a word that starts with Brian ends with E and what other words are there were Brian, for example And I'm I apologize from the spoilers After the fact, but so in this case we managed to guess the correct word in four guesses and four guesses is You could describe it as par right like it's pretty good in general You're gonna get it in four Sometimes it'll take you more taking less generally either you have to get lucky or Or you're just really really good at this but in general The expectation is that most guesses are gonna gonna end up around four and I think if you look at the the word all stats That's also that sort of bell curve you end up seeing So that's the very basics of word all there's one word every day And everyone is guessing the same word each day now word all has a couple of a A couple of data sets that are of relevance to us it has a dictionary of words that you are allowed to guess So for example, if you try to guess, you know a word that's just all a's It's not gonna set accept that guess and you get to try again So there's sort of a dictionary of allowed words and then there is a dictionary of answer words And what we're gonna do is we're gonna try to solve the game Using only the same knowledge as a player would be so we're not gonna use the list of you know known answers Instead, we're just gonna use the list of allowed words which includes all of the answers But that's the only list that we're gonna be using here So that brings us to Today's topic which is Three blue one brown if you haven't heard of this YouTube channel. I suggest you you check it out It's basically a YouTube channel where Grant Sanderson just goes over a lot of interesting topics From science is the best way I can describe it. I really like grants explanation style But in particular this one video Posted back on February 6th of a video to solve word all using information theory And if you haven't watched this video, I highly recommend you go watch it. I'm gonna put the link in chat Because we're gonna be following the algorithm from this video So like I'm not gonna explain the algorithm in detail as we go through because there's sort of an expectation that you watch this video If you haven't and you're watching this Like play play press pause not play pause press pause Go watch that video and then come back in and resume. There is a follow-up video as well Where it turns out that there was a bug in Grant's implementation This one doesn't affect us that much. It doesn't really change the algorithm It's it has a couple of other interesting bits about How to do better if you incorporate the list of known answers and a little bit of like, you know the discussion of whether it ruins the game to have this kind of a Solver, but it's not sort of required watching where's the first one I think really is because this is the algorithm we're gonna be following and When I saw this video I got really excited because I Love doing stuff like this where it's just you know, you're you're just trying to build an algorithm to solve a problem But it got me particularly excited because I've been looking for Something that I can do up like a performance optimizing Video on in Rust where we write a program and then we optimize it to make it faster and faster And I haven't really come up with a good like sample project for profiling You really need, you know a real code base to benchmark It's it's kind of hard to benchmark and profile something that's just like an arbitrary little code snippet You really need a full program and this is a great example because there's a pretty large search space So the program is gonna run for a while You also have the ability to run it in multiple passes, right? We can have it try to play the game over the course of multiple days And then profile, you know across all of the runs and see where our bottlenecks are and then we can optimize And we can pretty easily figure out where where we can optimize and we can try to figure that out So so this is a great candidate for trying to do that kind of performance profiling and optimization And so that's what we're going to do today We're gonna implement the algorithm from this video We're also gonna obviously have to sort of implement wordl and we are going to Run the program see how long it takes to run and then we're gonna iteratively optimize its performance to try to make it As fast as we can in particular. We're gonna be using a tool called hyperfine hyperfine is Really great. It's a tool that basically lets you run Multiple commands that ostensibly do the same thing and that it measures their performance And tells you how much faster or slower one is compared to the other and Crucially it does this by running each command multiple times. So it tries to gather things like You know the the variance and execution time and actually give you some some measurement of these Statistical significance of the performance difference if if any so the hope here is that we don't just like do premature optimization We actually measure and see which optimizations make a difference and then hopefully iteratively Make the program faster and faster that way And We're probably not gonna get into the depths of like, you know inline assembly and that kind of stuff What we might look at the assembly, although I think it's unlikely I think they're gonna be much lower hanging fruit than that to easily fill out this video We might also get into multi-threading I haven't quite decided yet whether I think that's important and one of the reasons for this is because Multi-threading tends to hide The true performance of your program. So once you multi-thread It's a little harder to benchmark But also things tend to be so fast when you use many cores that it's hard to actually figure out What's slow whereas if you run it on one core? It's very obvious where the time is being spent and all optimizations are very visible very quickly And so it's almost like you kind of want to optimize it in single threaded and then make it multi-threaded Once it's you know as perform it as you can make the single threaded part and then you want to compare them Right, so you want to see what is the what was the cost of doing this? This multi-threading in the first place All right, so I think that's where we're gonna start As I mentioned, I'm not gonna go too much into The details of the video, but I'll mention that at the core of it lies the idea of information theory This is a really neat notion that again the video explains really well But the basics of it is that this formula allows you to sort of compute How much Information you can expect to get from a given guess and the idea is that we're gonna play word all and at each step of the game We're gonna pick the word that is likely that gives us the highest Expected amount of of information for that guess and that way in theory at least you should be whittling down the list of words As quickly as is possible. There are obviously ways to improve on this algorithm We're not gonna focus too much on the algorithm itself here though We're gonna implement the algorithm in a fairly straightforward way and then we're gonna focus on the optimization part But this is obviously something where you could spend hours and hours and hours and days and days and days on just Twiddling with all the things and getting the score slightly better That's not what I want to focus on here because I want to focus on the rust part of things rather than the algorithmic side of things Okay, great In order to do this To implement this thing There are a couple of things we're going to need the first thing we're going to need is the word all dictionary Because without the word all dictionary We wouldn't even be emulating the right game And it turns out that it's actually really easy to get the word all dictionary Because it's all in the source code. So specifically here's what we're going to do We go to application And then we go to no not application sources We open this javascript file We format it scroll down a little And Down where is it? Oh Trying to be helpful. Here we go. Obviously we have two arrays ma and oa And if you scroll right Oh, it's cutting it off. That's fine. I've already copied it elsewhere, but ma is the list of all of the All of the target words There's one for each day and what word all really does it just walks along this list So if you're going to look up the answer for you know today or tomorrow yesterday You can look it up using this list. It is in order And then there's oa oa here is the dictionary of all allowed words, but it does not contain ma So you sort of want the concatenation of these two And once you have that then now you just have the the dictionary words. And so that is Easy enough to get started. I've already fetched these locally. I'll show you in a second The other thing that we're going to need is In order to compute, you know, which words are more likely to be the answer or not without relying on the true list of actual answers What we're going to use is the google books n gram data set This one is openly available and it's a it's a very large data set that basically has Well, it has multiple different n grams and n gram here is word n grams So two grams for example are all of the sets of two work two consecutive words Across all the books that google books have scanned across all the years of over which they have scanned books And the counts of each one. So in the two grams data file, for example, you might find You know an entry for the world and the world is going to have account for each year that google books has records for And similarly for, you know, hello world. There would be a account for every occurrence of a low world for each year going on and it'll be sort of curve And you there are like viewers for this and you can scan it is a really neat Thing to be able to scan it has its problems. We're going to start there And specifically we're going to use the one grams data set which has one gram So single words. So it basically has the count of single words That appear in all of the text the google book scans So if we I actually have I've cheated a little here I did a little bit of this in advance. So this is the entirety of the One gram data set from google books. You see it comes in like 24 g-sip files And it's pretty large. That's why I did it beforehand and what we're going to do here is Use rg. So rg is ripgrep the command line tool that's sort of like grep but much faster and written in rust and it's great And what I'm doing here is Searching actually let me maybe give you a A sample of one of these files So this is um, I'm just picking out some random lines from one. So you see that the the construction here Is that every Uh, that's a terrible example, but I guess we'll do it. Um, I should not have used tail. That was still it Do I have a better one? Sure. Uh, so here you see there is a uh, at the start of every line There is a word. So this is the the one gram and then this is the year The number of occurrences of that word in that year and the number of distinct books That that word appears in Right, so you'll see that the left number is always higher than the right number and you see that there's a bunch of tabs separated Entries for this. So you see that, uh, you know, this word appeared Once in one book in 1816. Uh, and if you go back to 2012 it's still one in one and it's interesting that in the middle There are a couple of higher ones. Uh, that those are probably miss scans because in general this data set is cumulative Um, so this might be that they they've fixed up the way that they scan In any case the exact format here isn't that interesting instead. What we're going to do Um is we're going to use ripgrep to scan for all of the, um All of the entries here that start with, uh, five letters, uh, the this, um This underscore bit here is because, uh The google book's data sets They like annotate all the words with, uh With a, um, actually maybe they yeah, so with like noun Verb I guess some of them they don't even annotate Which is a little interesting. Um But like this is an adverb. This is a verb, etc The annotations aren't always accurate, but you know, it is what it is um So what we're going to do here is search for all of the five letter words across all of the files Um, and with this sort of suffix Um, and the dash z flag to to ripgrep makes us makes it scan compressed files The dash capital I flag You know that is a good question Uh Right because we're printing multiple files We wanted to not print out the file name that I found the match in we just want all the matches And then we're going to stick that into, uh five letters dot text Um, and so I've already run this command because that takes a while on this large data set Uh, and that has about two million lines. So there are about two million five letter, uh one grams in this data set And if we look through it a little bit You see that it has the sort of expected De-expected structure And then we can do a bunch of massaging here in particular We want to sort of trim off this bit over here and we want to extract only the count as the end There are many many ways to do this The way I ended up doing this was using awk because I like awk Let me see if I can dig up the command I used Uh Actually, I can just do five letters dot texts And find the command that I used there Um, so awk is a great little tool for Managing files that have like separated records. So you want to do something with Um, particularly here. I'm telling it that the field separator is tab And I'm telling it to print the first thing the first, um field. So that would be the word Uh, followed by a tab followed by the last field Which is nf here is number of fields of this file I'm going to substitute every underscore here with a comma And then I'm going to run awk work with comma as the separator and then print out the first Which is then just going to be the the stem of the word and the Second to last field which is going to be the Occurrence count for the last year in the data set and then I'm going to stick that in the five letters occur file Uh, so at this point we have this file Uh, it has the same number of entries, right? Because this is not filtering anything out It's just trimming out the fields and in fact, uh, if we look at it It just has, you know, the counts for each of these words Um And, uh, then We need to do a little bit of, uh, normalization Uh, which we can do again in a number of different ways What I ended up doing was Replacing all capital letters with lowercase letters using tr and then sorting that and that gives us the list and sorted order One thing you'll notice with five letters in lowercase sorted Is that there are multiple entries for the same word and this is because they had varying capitalizations So we really want to combine all of those And there are ways to do that too I know this is not directly rust related, but it's just to show you how I got the data set So five letters lc sorted dot text What I did to this and this is a little bit nasty, but this is using awk again is um, I'm using awk to just combine, uh Consecutive records with the same word and combine their counts. Uh, this is a very simple awk program You could do this in whatever you wanted. This is just an easy way to do it on the command line Um, and I wrote that into this, uh combined file And if we look at five letters lc sorted Combined you'll see that there are about 900,000 distinct words here Um If we look at the head of it, you know, it has just one, uh entry per each Um, and then if I do I'll see sorted combined dot text. Um, there are all sorts of interesting things we can do here. We can, um, you know, look at the Uh, where do we have the Where's my search for this? Uh, so we can do things like sort by the count and show the top 10 So these are the, uh top 10 Most frequently occurring words in this data set Um, we can also do things like, uh Compute the total count here. Oh, that's not what I meant to do Let me dig up where that is So what we're going to do here is extract the second field Combine them all with plus characters and then pass them to a calculator And that shows us that the total number of occurrences here is About one billion So there are about, uh, one billion occurrences here Um, and then we can divide in order to get the probability of any given word We can divide the number of occurrences of that word by the total, you know, number of words that occur anywhere Um, which we can do like this Um, I don't want to sum them. I want to I also only want the Nope I only want the sort of this Ah, I should have pruned my history a little bit before I did this. It would have made it nicer So we're going to pick the the like top 10, uh words again by rank and we're going to print out the Current count divided by the total count that we just computed and multiply it by 100 to get a percentage And so the probability of which occurring if you pick a random word out of an english book is about 2.5 Um Great, so, uh, that's a good place to start. Um, of course, then we need the wordl word list. I told you I had already, uh Imported that as a json file. Um, and so here This is the json of all of the two lists concatenated. You see here is where there used to be a split Um, because it goes from things that are not in order to things that are in order And then I just use jq to, um, pull out every entry and then sort it out. Um, and now we have a A dictionary file that has all of the possible or legal guesses and wordl including the answers And then we can use the join command to, um Join That's not what I want. I want Uh, this command so we're going to join the dictionary dot text file which is, um, has all of the valid wordl words with the, um Frequency count file and what join does is it looks for entries that are common to the two files. So this is basically gonna Only it's gonna Output the counts for all the words in the wordl list, but no other words Uh, and the dash a1 here is to include words from the dictionary that are that we don't have account for Um, and we can of course do, you know sort and k to to here as well and we can reverse sort and head Uh, and so now this list should be the same as the list that we had before except all of the words here are legal wordl guesses So if we look at, you know, this combined list has about, uh, 900,000 words whereas the, um Original wordl dictionary only has about 1300 words um, and the the join that we did Should have the same number of words, right? But the Difference is that it also has the occurrence count for each of those words And so that's where we're gonna start. Um, and for words that we don't have counts. Um, that is something we're gonna need Um, we can just look for anything that ends with a letter, right? So that would be anything where we don't have account And these words are allowed by wordl, but don't have an entry in the google ngrams dataset Um, so i'm clear whether these are really words Uh, but you know, they're they're in there. Um So what we're gonna do here is stick this into dictionary with counts dot text And then we're gonna edit that file And we're gonna look for a to e At the end of the file and we're gonna just gonna make all of these be have a count of one Um, because you know they they didn't appear in the ngrams dataset So it seems like they're pretty unlikely to occur, but we need to give them some value and don't want it don't want it to be zero um So now, uh, we have a list of you know space separated words and occurrence count for all of the words in the wordl dictionary Um And that is a good place to start I think that is sort of the data set that we're going to need there is technically one more data set that we're going to want um, which is this one um answers dot jason and the reason we want Answers dot jason or really the the corresponding text file In order is because we have to have something to run our bot against We could run it against every possible word and see how well it does it guessing But realistically we want to run it against the real wordl game, right? Which means we want to play every possible wordl game and every possible wordl game is to run each of the answer words So we're not going to allow our program to Use knowledge of this list in its algorithm, but we are going to use that as basically the test set Um, so that's where we're gonna That's where we're going to use that that file, but that's the only place we're going to use that file So at this point it's time to start writing rust. Um, and for that we are of course going to need to know What to name our? Dev streams And I have already come up with a name for one and that's going to be roget And there's a little bit of backstory here Roger's thesaurus is sort of the original thesaurus if you don't know what a thesaurus is It is basically a way to look up words that are related to other words. It's not just synonyms But it's things with the related meanings that lets you explore the the Knowledge space or the the related word space in a very abstract way And find better words to accurately Represent what you're trying to say And they're all cool all sorts of cool tidbits about roget and and roget's thesaurus But this seemed like a good place to start for a wordle solver to call it, you know Name it after a thing that lets you explore words So we're going to start out here with roget And we're going to copy in wordle Answers dot text is going to go in here and wordle dictionary With counts it's going to become dictionary dot text Yeah, roget is also five letters, which is very uh very nice um All right, so now we need to start somewhere uh f and main that seems great. So We're going to need two things We're going to need a function that lets you play wordle And we're going to need a function that Tries to do our guesses and so the way this is probably going to look is something like Play play is going to take Hmm That is a good question. How what exactly is it going to take? um I think we're going to go with and it takes an answer Which is a static string uh, and it takes a A guesser Which is going to be some g that's going to be generic that we haven't determined yet And it's going to play six rounds Where it invokes The guesser each round And you know guesser probably doesn't need to be a trait, right? It's maybe it's just really a function But let's let's see how that ends up going Um So What is the guesser going to do? Well when it needs to make a guess what information is it going to be given? Well, it needs it's going to have to return a word Um It's going to be allowed to take me itself um And in order to guess What is it going to be told? It's going to be told Uh All of its past guesses. I think it's the way to go about this Uh Guess so what is a guess? Uh a guess is the combination of The word that you guessed um and Uh And the sort of mask I guess, uh, which is going to be a um Say correctness of five And what is correctness correctness is either Uh green Uh, and let's give them better names than green But we can we can document that this is green, which is correct Um, this is yellow, which is that it is, uh What's a good word for this? Uh present um and gray Which is Missing misplaced as maybe misplay I kind of like misplaced um and I guess we could just call this wrong like a gray one just isn't there um So it is going to be given a list of guesses Which are I guess history maybe And string is going to be the next guess that it wants to make given this history um So this is the basic structure here, right and then what main is going to do we're going to have something like, you know, um Uh Answers or I guess games is maybe the way to describe it. Um, and that's going to be include stir of dot dot slash answers dot text And we're going to do four game or four answer In games dot split whitespace We're going to play Uh against that answer And we don't really know what the guesser is going to be here yet Um, but we're going to be playing against the guesser And uh, we can probably have a Just for convenience. We're going to implement this for at mu t where t implements guesser This is just so that, um It's not really necessary. Let's not do that for now. It's fine Uh, so this is going to pass in the guesser, which doesn't have to be mute anymore And that's all that's really going to do Uh, and I guess this is going to be something like, uh, you know, instantiate the algorithm And let's say that we call the algorithm, you know, uh naive new And maybe we make Actually, let's go ahead and uh, create a lib dot r s where we stick most of this So pub of n play And pub pub And these can be pub too. That's fine And so this is going to do, I guess what we'll do actually here is, uh, have a mod algorithms My thinking here is that we if we end up implementing multiple different implementations, um, we could just have them all be in the same, um Binary and that's switch based on a flag or something. So it's easy for us to uh, benchmark multiple ones at once um So in order to do that, uh, let's cd into rog it, uh, and make their srs lib algorithms I mean that Uh, algorithms and we're going to call it Let's do naive, um first And it's going to use, uh Super And it's going to get I guess it needs, uh, guesser guessing correctness Correctness is a little long And this is also going to be pub And if we go to main, uh, this is going to be rogit algorithms naive and this is going to be rogit play Right, and I need, uh, algorithms dot r s which is going to have pub mod I'm actually just going to have mod naive pub use naive naive And this is going to be I guess supposed create And then we're going to implement, uh, guesser for naive Why don't the code actions not work for me? That makes me sad um So we're going to take guess And this for now is going to be to do great So now we have a sort of basic structure for our thing And I think we're going to implement play first because that seems straightforward enough. Um All right So how does playing word will work? Well, uh, we have a target answer So I guess what we're going to do is, um Initially I guess for, uh, let mute history Is vknew For I in So here we could actually do I think normally you would limit wordal to, you know, you can only guess six times What we're actually going to do here is not limit the number of times you can guess Because we want to know whether the bot Succeeder or failed and it's nice to actually get the distribution in the tail rather than just like It got it in six or less or it didn't get it in which case we don't know how bad it is So we're actually going to let it guess forever. Uh, but this is one place where you could in theory limit the number of, uh, rounds So the guesser needs to guess um So for each sort of iteration of the loop, um, we're going to have guess is going to be guesser dot guess And we need to give it the history. Um, currently initially the history is empty and it's going to grow over time Um, and then we need to do something in order to compute the correctness Based on that guess And then we're going to do history dot push um Guess which is going to be The guess and the correctness And then we just keep going And we are going to say though that if the guess is equal to the answer Then we return And here we actually have it return the Like which iteration it succeeded at so here we're going to return i And maybe this is weird, right? We want to say starting with one because we want to say that it If you guessed correctly on your first guess, then we should return one and not zero because you did one guess um So if the guess is equal to answer then we return i otherwise We compute the correctness and keep going And I guess we called this mask, didn't we? Uh, we might not need this asref might take care of that for us um, and Oh, interesting. It doesn't realize that this is an infinite loop. It should not be possible to reach over here um Is did we not call it a word? Okay, um, and we need to have Uh, something that computes the correctness, so we're going to need a um A check which takes the answer And a guess And gives you back a correctness Which arguably could be you know, uh, impole it could just be A method on correctness is actually maybe nicer So we're going to do impole correctness Um, we're going to have it be called compute And we're going to have to figure out what this does At some point, uh, but that means that this is going to be correctness compute Answer guess Uh Oh, right. It doesn't actually return that Because the correctness is an array of length five Correctness of length five and so this is going to be just check I suppose You know, I still want it to be a method on here and I'll just have it be this It looks nicer Oh, it'll exit at U size max I suppose that's true um Did not finish within U size max guesses That seems like its own kind of problem Like if we're going to run out of memory before that happens because we're accumulating History is going to be a vector of the length of the number of guesses, right? We could bound this loop if we really wanted to Um, but like if we could guard against something like a Um, you could imagine that you implement a guesser that just, you know, never succeeds um Like you could have a guesser that always guesses the same word over and over and it's not the answer Right, so it actually just runs forever. So we could bound this if we wanted to we could say that like realistically, um Wordle, let's see wordle only allows six guesses we allow more to um Avoid chopping off the score distribution for stats purposes So let's say we allow, you know 32 guesses, why not and then we're going to make this be an option U size And this will be a sum And this will be a none that seems fine um Okay So now we need to compute the The correctness giving an answer and a guess And here we're going to assert that uh answer dot len Assert eke answer dot len is five and assert eke guest dot len is five Um Oh, you're right. That is Fine dot dot equals Um So here comes the question we're going to do I suppose, um How do we want to do this? Correctness initially is going to be actually let's make correctness a um derive Debug Clone copy partial eke and eke Um Correctness wrong Of five we're going to start out saying all of them are wrong Uh, then we're going to go Well, maybe the guess mechanism should return an option No, the the guesser should always be able to give a guess Right like the the assumption is that the guesser Is able to guess the correct answer You could implement one that can't but then that's a bug in your guesser, right? If you if you know Statically that it might not be able to find the answer That seems like its own kind of problem Um, like that sounds like a case in which the guesser should panic Uh, we could have a return result. I suppose. Um, but uh Let's skip that All right. Um, so There are a couple of ways we could implement this. Um So we could do something like for i in Uh, zero to five If answer i is equal to guess i Then c i is correctness correct Um, I suppose we can do this. That's fine Oh right This is the Annoying thing about using uh utf8 is you can't easily index into this we happen to know that this is valid asci I have an optimization plan for later to make this be a Five-length byte array, but I kind of don't want to do it yet. I want the code to be As naive as possible in the sort of initial iteration um So I suppose what we will do instead is, um For a All right, you're gonna hate this but for iag in uh answers dot Cars dot zip guess dot cars dot enumerate if a is equal to g um Oh and sir Let's see. So if they're equal then that gets correct. Um Else if um Actually, here's what we're gonna do Uh, this is where the bug by the way in um in grand implementation was is in the logic we're about to write um What we want to do is see if there are any other occurrences of this letter in the guess so If G if uh answer dot cars dot enumerate Uh Dot That's um It's gonna be a little bit annoying because we want to check whether this character occurs anywhere else and we haven't already marked it as misplaced That's where the the tricky part here comes in Um, oh, yeah, you might be right. We might actually need to Do the iteration twice All right Mark things correct Uh, or mark things green I suppose Mark things yellow I think we're actually gonna have to do an inner inner iteration here Which is uh, so obviously we can continue if that is the case because we have already marked as green Um, but otherwise we're gonna have to look for um If let sum j Is answer dot cars dot enumerate dot actually dot, uh Position Now I think we need enumerate Find map Because we're gonna have to map out the eye um What we want to find here is Yeah, there are there are a lot of ways to do this like we could also mark Even marking everything gray is non trivial. It's the same problem What we want to do is we want to see whether So here is for a given character in the answer if, uh Ooh, that's almost the wrong c. Um, I guess a is fine No, it can't be a uh Actually the second loop should just loop over the guess it doesn't need to loop over the answer and then if C of i Is equal to correctness Correct, then we're it's already marked in green um So this is from the answer if, uh C of i Actually, we can just match on that Yeah, I mean we could also do we could track the Count of how many times we've given out each letter I'm trying to find like an a nice way to represent this algorithm Uh, and I don't want to use something like a hash map because this is going to be run for every guess So it actually has to be fairly fast um We might not be able to do this with a simple iteration. We might actually need to keep track of The counts for each letter I think it's actually just uh Like marked is uh bool Something like false of five um, and and then for I in C dot Enter dot enumerate Actually, we can just do for um I see in C uh, if C is correctness Correct, instead of marked we can say used um If it is correct then used of i is true Uh, and then we can walk through here and If c of i And so now this is going to be look through all the characters of the answer um, and if you find a If a is equal to g And Not used i Then return sum j then used Uh Used i is true Otherwise none I'm gonna explain this logic in a second because i's this is something we need to write a test for to see that it does the right thing um Then c of i is equal to correctness um misplaced in C dot enter dot enumerate c of And in fact, we don't even need to We don't even need the value It's just if any Then return true Uh else or just false I suppose All right, so the intention here is we for each letter in the In the answer we need to mark whether that character has been Used to mark something green or yellow all of the green things have obviously been used to mark something as green and then we walk through all of the All the characters of the guests if they're already correct. We have nothing to do. They're definitely not yellow Otherwise if there are any characters in the answer that are equal to the guest character And haven't already been used to mark something as yellow then we mark it as used Uh, and then we say yep, this is a yellow character. Otherwise it is not and you need to keep keep looking Uh, so I Think that would do it All right, so, uh, that should then be all we need to return C And let's immediately write some tests for this because I have very little confidence Um That this is actually correct. Uh, and so we're going to have a sub module here for compute And tests Uh, this is going to be I suppose Basic So we're going to have, uh We're going to assert equal Uh I guess here we're going to use create Uh Correctness We're going to assert equal Correctness compute of let's say, uh, if the answer is a b c d e And we are guessing a b c d e Then we should get back Correctness correct five All right, this is Certainly the simplest test Uh, I have done Something in naive it needs to Implement naive Uh pubfn new return self Should arguably just be, um Just implement default really Use of moved value. Oh no Where in main Like so All right, so our basic test works Uh, let's now see if something I guess all green Um All gray If I guess F g h i j alphabets are hard all of them should be wrong Uh, and if I do all yellow a b c d and I guess, uh Rotate it by one so e a b c d Then they should all be misplaced See if that's the case That's fine Um And let's do something where there is, uh Repeat green So we're gonna do, uh a a b b b And we're gonna guess a a Uh c c c What we should get back is Uh And here we could actually we can make a little macro To make writing these correctness things a little nicer Uh, we're gonna have the syntax here be something like Um We're gonna say that m is gonna be turned into correctness misplaced This is gonna be this is gonna look real funky Uh That's gonna be wrong It's gonna be correct And we're gonna do Some repetition of tokens That's gonna turn into And let's say we do this right, um And we're gonna say that it's gonna turn into Uh for each of these We're gonna call this, uh Mask Um, it's gonna turn into mask of The t t Um So each character here It's gonna be repeated in here By calling the macro And then separating by a comma I'm lying this should just be that Uh And now we should be able to have this be just mask of Uh c c c c If I wrote my macro correctly Which perhaps I did not Oh, it ends up being one token fine We're gonna have to do this No rules expected this Uh, fine. Let's make it an ident then Why? Oh, you're right. I don't actually need the square brackets There we go. And I think this can now be t t Um, and then we can make these be square brackets and then things look nice This is gonna be w w w w Uh, this is gonna be m m m m m And this should be c c w w w Let's see if that is indeed true. Great. So repeat green works fine Uh repeat yellow So if I do here, uh, c c a a c Uh, then I should get wrong Wrong misplaced misplaced wrong Repeat some green. So this is gonna be if I have one correct But one wrong Then this should be marked correct. This should be marked misplaced All right, so this seems pretty good Um Right. So here we have something that it seems to actually be computing the the correctness Uh, correctly even when they're like weird repeats or some of them are yellow If you can think of other use cases here, I guess let's I I got one random one from chat From chat one Which was if the answer is a zz ac And I guess Uh a a a b b should only mark one yellow. Is that true? Uh, that one should be correct Uh, that one should be misplaced that one should be wrong That one should be wrong and that one should be wrong All right, we passed that Fantastic All right, I'll I'll call this uh, dr. Eamon from chat You have a test k named after you now Uh, here's another one Uh, it's a pock Uh b a c c c a a d d d So here, uh The first a should be wrong. The second a is correct The d's are all wrong We passed that All right, last one From re coello a b c d e And a a c d e should give wrong for the second a. All right, so the first one should be correct The second one should be Wrong Uh, this should be correct. This should be correct. This should be correct Amazing. All right, we pass all the all the from chat from chat tests That's good. I now have some more confidence that these are, uh, correct. We should arguably document it, but I'm not going to do that now Um, all right So we actually now have the the play algorithm done, right? Like This is all there really is to wordle. It's marking the correctness appropriately and just giving the guesser the ability to guess Most of the complication is in what do you do for the for the guesser? Oh, we do need to check whether the guess is valid um That is true Oh, that's going to be a little annoying. So we're going to have, um Imple we're going to actually have then a pub struct wordle which is going to have a dictionary It's going to be a hash set of stir We could actually make this a perfect hash phf um I wonder if I can I wonder if there's a ha phf set Could I make this happen? No, it has to be a macro. I can I might be able to do this with a const function if there was a const constructor for this Um set Set is there a Constructor for this No, it has to be a macro Um, the the reason I that would be nice is because the the dictionary is the same So really we could and it's known at compile time. So we could compute this set at compile time um That way You know, there's no need to construct this wordle and parse um at Runtime and one of the reasons why I don't want to do it at runtime is because that's going to be measured by the benchmark Which makes me a little sad But I guess it's fine for now. It just makes me sad So here we're going to say const const Dictionary is a stir. It's going to be include stir of dot dot slash dictionary Um And dictionary is going to be hash set from and I guess we're going to use collections Actually, you know, we could just have this be a uh sorted vector although it would give us um log two lookups which makes me a little sad Hmm Oh, we could use a build script Would that help? Yeah, I could make the build script emit what phf requires That's interesting Uh, I'm gonna leave that as an optimization. We might do later. Um, it's a good idea So dictionary split white space um Every other element Is there a method on iterator? Advanced by is nightly only Step by Yeah, so we want dot step by two the reason we want step by two is because um oops Is because every other value is the um the frequency count From iter And then we're actually going to have play be a function on wordl That takes self You can use lines Oh, you might be right Um line. Well, I could use lines, but if I use lines Uh, then I also need to map Line fine, I'll use lines Uh line dot split ones by space Uh Expect Uh Every line is word Plus space plus uh Occurrent, I guess it's not really the frequency but frequency um And dot one and then we don't need the step by Um, and so here if uh, not or and then I guess we could just do assert that Um self dot dictionary dot contains the Guess This is also something where we could make this be weird um We could make this be a debug assert right like The guesser shouldn't be guessing things that aren't in the dictionary um but Like because we're not we don't have a human player who doesn't know what the dictionary is right, so I suppose we could give them the dictionary Um, but I kind of don't want to do that So let's leave this as a An iterator right now Oh, does it allocate does lines actually allocate? Internally, I think that's when you read a um That's only when you read a file I mean this is easy enough to find out stir lines source Split yeah, it's just a split Terminator yeah, it just returns um string slices, so that's fine Um, so that's fine Do do do Great, and I'm going to make this So this dictionary is also going to be used by naive for example, right because it needs the frequency counts It's not actually going to be that naive. It's it's arguably the real implementation It may mean it's naive in terms of performance not in terms of algorithm It's arguably poorly named. Maybe unoptimized would have been better um But we can't change that now um Yeah, so this dictionary is going to be used by other files as well But here all we need is the um the set of uh of files Great So now we have this And what we now want to do Is I guess we could we could even have a um Uh test here for uh Game, you know, which is um test fn play So here we're going to do wordl new Uh wordl.play And I kind of want to implement Uh impo Uh impo guesser for fn of history guess To string So that it's easy to write, you know test implementations basically anything that doesn't need state um, and that's just going to do Uh Self of history And now I should be able to say I want to play with this closure I'm going to play against uh Moved And it's just going to return Moved dot to string And I want to assert equal That I should get a score of one Use create wordl And it should be sum of one And it's complaining because this closure doesn't implement Fn I thought that it closures automatically were fn if They didn't capture anything, but I guess maybe not I'll name it So we're just going to call this guess And it's not going to use history It is going to use guess and Now what Why And I can't easily make this generic over f because then I can't have implementations later Which makes me sad. This is fine. I'm fine requiring it to be an actual fn Um What why is it sad about this? The trade guesser is not implemented for that The following implementation was found But aren't these two the same? Yeah, but I don't want the trade. I specifically want the function pointer because that is what I pass it here Um That shouldn't matter Yeah, I I want a raw pointer. It's intentional But I think Like doesn't this match that am I blind? Oh, does the trait need to be in scope? No Well, that's real unfortunate Yeah, there's like Some lifetime you're not matching up, but I'm not quite sure why Uh I mean, I could have this take, you know mute g instead um And then I could have this be mute of this Still won't play ball, huh? All right, that's fine. We'll go back to um What we had and then I'll just have a helper here that's called. Let's say Uh Guesser which takes a History as an ident and then it takes a block. I guess impel which is a block And it returns Struct g it implements g uh it implements guesser for g Um It can't be the fn trait because then I would need a blanket implementation here For for all like generic over t and then you can't have another implementation of guesser It would be considered potentially overlapping The other alternative here is to make Play take a closure always like take an fn mute instead of taking a trait, but I don't really want to do that um I suppose like the guesser is always going to be kind of like this But I like the ability for the guesser to contain state that it might mutate over time But I think this is going to be basically essential for things like, you know cashing things we've computed in the past um Uh implement guesser for g guess is going to be this Uh, this is going to be history and this is going to be impel And now I should be able to do um I guess let's let's do something like Um Ident Is equal to that and then what I'll do here is Return an instance of g here And now I should be able to do let uh guesser is guesser of history And then I think now this should be fine this needs guesser And I guess I can make this Config test to get rid of the warning And main is complaining about this because It now needs to do Roger Wordle new And then this is going to be w doll play crashed, huh Every line is word plus space plus frequencies a lie It claims Really? Oh, did we miss one? Uh, no Oh dictionary I don't understand. Which word am I missing? Add view Wow, that search was just entirely wrong Why did I do a to e a to z? I have no idea why that's how I wrote that search that is just wrong Fantastic. Okay, so we can play a game now and we can see that if we immediately guess the right word Then we're good And then we could do something here like, you know, we could we can make this fancier and say, uh, if history dot What's a better word than moved? Uh Right Because because we're right Uh And if history dot len is equal to one Uh is equal to One then we return Right Otherwise we return wrong And this should now take two guesses I forget what the It's like they're called like, uh impressive and That's a good question. Let's let's figure out what the uh If I go here Okay, so it's genius So this is this is genius This is impressive. No, this is Magnificent This is Impressive, uh, this is splendid This is great And this is few I mean these are all the same Uh, right. This is just gonna be Genius is guess it immediately. Magnificent is guess it after one. This is guess it after two This is guess it after three Guess it after four Guess it after five Like after five previous guesses Oh Really Is right or wrong not in the dictionary? No, they are so All right, so something's wrong Guess wrong is not in the dictionary Why? Oh, yeah, you're right. I need to update the uh Some two some three some four some five Some six How is wrong Give me this hash set Because something ain't right It has all the numbers That's because this needs to be dot zero and i'm an idiot Great, there we go. All right Um Fantastic, okay, so we now have some confidence that our guesser Does the right thing um Oops We want to check that if we have something that only ever guesses wrong Then it eventually terminates Great, so now we have that uh checked in as well Fantastic, so I think here what we're going to do is uh go back to naive uh and Go ahead and Why does it claim that naive isn't there? Is it still claimed that naive is not there? Uh, so I'm going to run this with the release and it's going to panic and not get implemented. Okay, great Uh, so we're going to get ad dot and we're going to commit this as basic structure in place All right, so now we need to go through this, uh, this naive implementation The intuition here is that we're going to keep a um A list of A list of Words that match all the information we have so far Uh remaining we're going to call it and it is going to be holding um Basically the remaining word list And how we're going to hold that I guess we can Well, we'll we'll not think too carefully about it. Uh at the moment So we're just going to say it's going to be a hash map from static strings to uh their frequency counts And this is also going to bring in the dictionary And back in lib We're going to have remaining here Be this Uh word account is going to be that Uh account is going to be Count dot parse And we're going to expect that Every count is a number And then we're going to return word and count Uh, this is a parsing that really could happen just once So we might want to bring in, you know, like one cell or something to to cache this But again, this is named naive and it's named naive for a reason. So let's keep it like this for now Um And when we're given our history Uh, what we're going to do is we're going to For each of the remaining words each of the candidate words if you will We're going to compute how good of a guess this is and this is where we get back to the the three blue one brown video Which talks about exactly how we do that But basically, uh, we're going to loop over every word and count in self dot remaining Uh, and we can do this because we know that both of these are copy um And for each word Here we could also track the how good every word is but we only really care about the best one um It might be nice to keep, you know, a Um a list of the top ones like of the top 10 or something It might help with debugging, but for now we're just going to keep the the best one Um And if Let's sum What are we gonna We could have this be a two ball, but I like having name fields. Uh, so we're going to call this We're going to derive Debug copy and clone We're going to call this uh candidate It's going to be a word And it's going to be a count Um If let's um see is uh best Else if there is no current best then the best one Is obviously The current candidate But in here Is this one better? Is going to be the big question And in order to have, you know, a notion of which is better, we need like a score or a Goodness Right, and we're going to say that that is going to be a floating point number for now So here we're going to say if, uh, c.goodness is Less than Or rather if our goodness Is greater than c.goodness Then best is going to become us Candidate Um word count goodness But we need goodness We need to compute the goodness in here somehow And we don't really know how yet Uh, and then we're going to here assume that we found some best word Uh, so we're going to do best dot unwrap Dot word dot two string This is going to be an option candidate Great, um, obviously we want this to consider the history like there's all sorts of things that that we haven't done yet um specifically If the history dot, uh length if That some last is history dot last Uh, there's a to-do here, which is update self dot remaining based on history Right, so the idea here is that if we previously made a guess then we just now learned Which ones we got correct basically the correctness mask for our previous guess As we need to go through an update remaining based on that new information Which we don't like That might whittle down which words are actually uh possible guesses Um, we also need to figure out how we compute the goodness here, right? This is clearly also a to-do. How do we compute this? um Which one do we want to do first we want to compute the goodness first or do we want to update remaining first? Uh, let's do updating remaining first um So the idea here is that we have some mask in last uh, and we want to Only retain things Sorry in um in remaining that match the updated information we got so, uh We're gonna do self dot remaining dot retain Uh, and each of these right is a Uh word and a count And I don't think we need the count here the count is irrelevant for whether we retain something Um And we only we only want to retain a word if last dot mask dot or if last dot matches word All right, so this last odd matches, of course, we we don't have yet the idea here is to figure out whether a given um A given piece of information matches a word So if we're told that, you know um, the first letter is a green a Then it wouldn't match such a match function would not match a word that starts with b um, we don't actually need to scan all of the past guesses because we've already pruned ourselves down to things that match those past guesses, so that that's an easy uh easy optimization Uh, I want to generate a matches method Uh, oh that looks awful. I guess that's where it decided to place it Um, this one is going to be common to all the guessers, so we might as well just make it, you know general um, which is uh a given This is just a boolean thing Where either a a given guess This is not really guess right this is outcome of a guess, so maybe this should be something like um result um response I don't know what a good name for this is um I kind of like guess right like it this is a guess and the information associated with the result of that guess Attempt maybe but is attempt better than guess Uh I'm just gonna keep it guess for now. That's fine All right, so whether a word matches a guess Um, let's see So first check greens So for you're gonna hate this too, uh for I'm not gonna write this yet in self dot word dot cars dot zip Uh And here too we should assert eke um self dot word dot len is five And word dot len is five Dot zip um Self dot mask dot iter Probably don't need that uh dot zip word dot cars And so this is going to be um The outer zip is going to give you the thing from word The inner thing is going to give us the Previous guess and the mask And then we're going to match on the mask and If this is correctness and um This one is fine to do this with if correctness is correct Then uh, if g is equal to if g is not equal to w Uh, then we can return false like here. We know that um Basically, we we know that a given letter in the previous guess was green And that letter doesn't match the letter from the current word Therefore this word that we were given cannot possibly be the the right thing Uh, can we share this logic with the checker? We probably could But I don't know that there's a benefit to doing so because it's doing a it's a different algorithm. It's doing something different um We could make this play in hard mode if we wanted to but I think that's a modification we can do later um If they're the same then we don't know yet. We need to consider the other possibilities um If it's wrong and uh G is equal then we also know that it can't match right you guessed uh You guessed the letter that the previous guest told you was wrong Um, so this this is an unlikely case, but it can happen um And if it's misplaced Uh, then we don't know what to do yet. So first we're gonna check the greens And then down here comes the next question. Um Yeah, I think we're gonna have The logic is like similar to the computation for correctness, but I don't immediately see how we reuse it right, um I think We probably are gonna need a sort of used thing here, too um And I guess start numerate Just to really hammer it home Um So Then used i is true So we've we've used that Green um And then we're gonna walk Then we're gonna go walk all of the Might be that we could do this in one loop. Uh, we're gonna walk all of the characters of the Just the word um It might be that we can just use this actually. Um Yeah, I think the second loop might be all we need uh If I guess So we maybe we have all the information here Maybe what we want here is actually if if g is equal to if The previous guess is equal to the word then if The match is correctness correct Then return No Then return false So if we're matching the previous letter and the and that was not correct No, this is wrong. I am trying to Wrapping my head around this. So I think we can do this in one pass Because I think all you have to do is search for an unused letter that is yellow right, so What we do is We're gonna search for the I'm I'm so sure that there's a simple loop for this and I'm just trying to arrive at it um I think if m is equal to correctness correct then we have A fairly simple branch if the if that index is correct and if G is not equal to w then we know that it's wrong Otherwise we need to see if there are any Unused yellows for this character So if self.word.chars Map self.mask If there are any unused indices Find map actually position is probably fine So we don't need the enumerate Bear with me here If we find any other characters I guess we can just look for the first unused one In fact, if they're equal then we do nothing No that No, we still need to If it used to be green And now we guessed a different letter Yeah, I think this is this case Is just done Right The previous guess was correct in that location Then if the characters are different then we know that there's nothing to do otherwise Used of i is true And we continue um Otherwise I guess if m is correctness uh misplaced and The character I don't think we need that as a special case um What I want here is to find the first Occurrence of that character if any in the previous guess That is unused. So if uh unused of Oh, so we do need this Find map If not unused which is terrible um I'm gonna explain what's going on here once I wrap my head around with myself. Um, so if Oh, it's called used. That's why I'm confused if it's used. Uh, can't use this to Support this character If Geez, I need to give these better names than g and w Okay, so we're looking for the first unused occurrence of The current character in the previous guess That's what this is searching for Um, so this checks that it's the same character um If it's used this checks if it's used So otherwise this is To be concrete about it, uh We're looking at an x in word And have found an x in the previous guess Um The color of that previous x Will tell us whether this x Might be okay So here we're going to match on what that previous guess was Uh, no, that's not what I want Uh, that should say zip um If that previous guess was Correct then it should already be marked as Used or we should have returned so this should be unreachable um Although this implies that maybe we do need to do the loop twice Ones to mark all the used Oops Uh, and then here Yeah, so this should be unreachable Uh All correct guesses Should have resulted in return or be used Right because if a correct if a correct guess you guessed the same then it's used if a correct guess you get something different Then it's wrong and we've returned um If it is misplaced Then used of j is true Uh, and we can return Some of j We might not even care about the index here actually this might be the same case as we did earlier Um, so maybe this is just if any So this is going to be false false It's going to be true um, and if the if the other Occurrence of this is wrong um, then we know that um So this is like You guessed an x in some position and in the previous guess you were told that there is no x Well, then your whole guess must be wrong. So this is sort of a uh, this is a return False kind of situation Um Which really is a return false from the whole thing Which we can't do because we're in a closure. We can make this just be a for loop. I suppose like a nested for loop um Right because this is really an early return um, there shouldn't be any downside to Uh early returning here though To to not early returning like this being return false should be fine because Even if you keep going you're not then gonna find a better candidate, right? Uh, okay, so if we do find Uh, a letter we can use then uh Word might still match otherwise I guess this can be plausible is uh Plausible is initially true um, if we get here Then plausible Is false So if we found one of these then the word might still match else if Not plausible So in this case we should assert Plausible it shouldn't be possible to get here and also have taken this path Because the return true here should have caused this to return early Um, if it's not plausible then we know that it can't match This is we hit this case somewhere previously um Otherwise we have no information about character Uh W so word might still match So this I suppose is The character w um Was yellow in a previous In The previous guess and there's an additional part here, which is um We shouldn't allow misplaced if j is equal to i Right, then you know that it must be wrong Which we can just deal with up here, but we might as well just do it down here too um W I guess this should just be w because we have a concrete one for this Um, w was yellow in the same position Last time around So it must still be I guess that does that make the word plausible if in the previous guess We got a yellow here That means that this cannot be the answer Um, so w was yellow in the same position. It was yellow in the same position last time around Which means that uh word Cannot be the answer that match it cannot be the answer So plausible Is false And we return false And I think this means that we might hit the assertion failure because it might then look for a misplaced as in a later position um So I think we actually want Uh if this and plausible Possible I guess we could call it still plausible if we wanted to but I think that should do it Uh, and now I don't think we need whatever this last loop was and we don't need whatever this bit down here was um So if we walk through all of these and it's still plausible Then true All right This also clearly is something that we need to test because I have no confidence that this is actually true um So this is going to be Guess matcher use create guess um Fn matches Uh, all right Let's see. So how do we make one of these? I guess we construct a guess Which is going to be you know word abcde and Uh mask which is going to be here. We can use the the masker we made So let's move this to to up here And uh, I'll make that config test too. Uh the mask here is going to be mask of Let's say that it was Let's say all of them were correct. This is the Uh, certainly should be Uh, trivial check right, so abcde That should certainly be the case Uh, is the yellow in the same place assumption always true? What about Uh guessing against ab ab a first with Yes, so remember this is only one iteration This is telling you based just on your previous guess Is this new word feasible because in general, that's all we need right because We have already pruned out candidates that didn't match the the earlier guesses We've already pruned those out in a previous pass of the solver So we only need to solve this incrementally for one step at a time Um, so let's see that this in fact Oh 32 That's fine. All right, so at least our Our very simple check here worked I kind of want a nicer way to write these So let's write another macro. I love macros for for testing. Um So guess is going to take, you know a, um Word which is going to be I guess a literal Uh And it's going to take a mask Which is going to be a token string plus is the the mask that we used from before um Is equal to matches Prev and I guess next It's going to turn into this mask of Mask Plus Matches Next Is it not lit? Is it actually become literal? Nice It's okay. So I should be able to now write check um And I'm gonna have I'm gonna actually include the square bracket here Check that abc de Plus c c c c c Matches abcd And maybe matches is wrong. Maybe it should be allows So see that that still works. Yeah, so see how much nicer that check is um So it should also be the and then we can ha ha and then we could do even better we could do uh disallows And say that that should be assert Not this We could even share them and have it be um Eek It's gonna be annoying bang Oh, it's gonna be a TT question mark. You're gonna hate me for this Bang Question mark And now this is going to turn into check one one Prev mask allows next Uh and Yeah, that's what I was worried about. Um I don't really have a way to Make that nice. I think Ah, that's fine. We'll just leave it the way it was A little bit less macro magic is good sometimes Because I can say now that uh abc df And abc de Um That pattern should preclude this guess from being considered correct And it does Uh, for example, if I get yellow for the same character four times, we then know where it's located Um That's true Do we need to prune given all of the logic? Yeah, you might be right here. Um You might be right that doing it incrementally is insufficient. We actually need to check whether it matches the whole history Um Which is going to be a little bit sad Yeah That's a good point The point from chat here is um if you if your history is like, you know A in the first position is the only things you know is that A in the first position is yellow A in the second position is yellow from second guess A in the third position is yellow from the third guess A is In the fourth position is yellow from the fourth guess Then you know that A is in the fifth position But you can't determine that by scanning incrementally Is that true? No, I don't actually I don't think you're right because um Any word that has A in the first position Will be eliminated by the first scan of guess Right because if a word has A in the first position Then it cannot match a pattern that has A is yellow in the first position So it gets eliminated in the first round And therefore by the time you get to the fifth round You've eliminated all the words that have A in the first position all the ones that have A in the second the third and the fourth Um, and therefore the only ones that are left are the ones that have A in the fifth position Um, so yes, I I think I think doing it step by step is actually sufficient Um All right, give me some more tests here. Um, it should be the case that uh if I guess Um completely different letters abcde Uh And I get all Wrong Then it should allow a word a guess that has only different letters um And then we can shift this by We can do the good old shift all by one Should allow abcde Right Oh e abcd All right We got another one. Oh, I need to I guess I should name these, huh? Uh From chat I'm gonna keep them all as one here and then I'm just gonna Uh, let me do them as comments. So, uh, a a abb plus c m w w w should Disallow a ccaa Okay, so Is that true if a is correct in the first position? Yeah, uh, a is Incorrect in the second position Then and a is wrong in the third position. That means there can't be three a's and therefore this should be disallowed Um, and there are no b's but there are no b's there either Great we got that one. Uh, we are going to do the same information theory. Um Algorithm implementation is three blue one brand. Um, and in fact, we're we're getting there um So that's certainly a thing to check I'm gonna go with uh, john from john Currently i'm beating chat um b a a a a Where The b is wrong The a is correct Then this should Allow Um a a c c c But it should disallow this Right, so it should disallow this because um This pattern told us that a is yellow But we guessed it again, so it can't be right who On 240 is that oh the first one I wrote Am I wrong or is the algorithm wrong? If there is no b there's a c here No, there's a so that one's correct And that one is yellow Then I think it should allow this Right right if I have b a a a a And I'm guessing a a c c c And I know this one is correct and that one matches I know This one is yellow and so I moved it there And I know that there are no more a's and that's fine Uh, and I know that there's no b here and that's fine. So here there's a bug in the algorithm somewhere Because that should be allowed So let's figure out why this one breaks. Let's stick it in its own, um And then debug Test over here Oh, I got another for chat retubon a b c d e plus w w disallows b c d e a Why should that be disallowed? It's saying all of them are yellow And I have all the same letters, but in different Arrangement So why should that be disallowed? I think that should be allowed I'm gonna comment that out for the time being. Um, all right, let's see if we can figure out this, uh, this Debug thing And why it's confused Oh, they're all Oh, you're right. They're all gray Right. Yes, they're all wrong. I agree Um Yeah, that passes. So why is this one broken? Let's see if we can figure that out um So I want to check all of the early returns and check out Which one we end up taking We end up taking the one at 168 Okay, so we take this one down here. That's good because this up here seems pretty straightforward Um, so that means we end up with not plausible So do we take this not plausible? Or do we take this not plausible? We take This one and in fact we take that one twice Wrong So let's Get some more information here about this one. I want, um I want debug of Uh I w j G And m So it's looking at Um I1 so it's looking at the second a and Yeah, the second a and it's looking at the third character of the previous guess Which is also an a Uh And it's looking for a matching a oh, I see what this is This is because We are reevaluating things that have already been determined to be correct here um So basically These ones We should be skipping over when we walk here um So if Which we can do just by doing this because we know that um Must be correct or we'd have returned above In the previous loop The earlier loop Right, we were if if we're in the correct case Then we should have returned false here and the reason why it fails if we don't have this condition is because we set those to used So we don't get to use them again down here And so this loop fails to find a matching letter because it doesn't consider any that are correct um All right And if i'm not Using g here, then why am I even including it? Maybe I should just do word Dot cars dot zip mask Nice Let's see how that goes That does it and if I do all of them. Okay back in business And I can get rid of this and this And we can go back to the guesses ROT Candidates derived impulse for clone debug, but these are intentionally ignored doing dead code analysis Oh, that's fine All right, give me some more Optimization a b c d e Where the first three are correct and The fourth one is misplaced should disallow a b c f g Because the last character must be a d that's true. There are some optimizations here. We could do but that seems um That seems excessive um I'm still wondering why you can't just copy the game logic taking the candidate word To be the answer and comparing the two correctnesses element by element so the proposal is That we could do this by saying um See if I'm if I'm guessing you right here that should mean that you want us to do um correctness compute of the The candidate word to be the answer And I forget the argument order here and the previous one to be the guess And that we should assert equal that with mask of this And I assume the opposite here Yeah, I mean that seems like it does the same thing. Why does this work? um It's a cool insight um Let's see if we Take the new guess as the or the new word as the answer and we are computing What correctness you would get for the previous one? Oh, why are we guaranteed? They're the same I'm trying to figure out why this works right so So for example, this would be uh You're trying to find the answer. So if your guess was the answer it would have given the same correctness answer Oh, I see so the assumption that I see so what we're exploiting here is that if The the thing we're checking were in fact the answer then The previous guess should have given That same pattern Of correctnesses I still can't wrap my head around why This will always produce the same correctness I guess the algorithm is deterministic Hmm. I also don't know. I think the compute Is actually more expensive Or maybe not actually All right, I'm gonna keep this the way it is and process this in the background rather than bottlenecking us on this particular thing Because it seems like the two are equal here I mean we could even do property testing right to check that they actually produce the same answer For now, I'm gonna stick with the one we have and then we can always remove that Later on It's not that I don't believe you that they're equivalent It's that I can't on the fly realize why they are which means I can't explain it All right, so back to our naïve algorithm So we now have a way to prune down these and now we get to how do we compute the goodness And this is where the three blue one brown video talks a lot about information theory And basically the way that you compute the goodness here is You compute the expected amount of information that The expected amount of information from this guess, which means like the It's a combination of how likely is this to be the answer given the information We know how far and the word distributions that we have And how much information would we gain if it isn't the answer right So so the idea here being that we want to balance these two where We both want to try to guess the thing that's most likely to be the answer But we're okay picking something that is slightly less likely to be the answer if it gives us a lot more information if we get it wrong And the way that this is computed is using this formula that is the sum for each i of the probability of i multiplied by the log base two of p i in the video it's explained as one divided by We're going to do this instead that they're equivalent as far as logarithms go And the reason why this is minus is because p of i is already a probability So it's already less than one so the logarithm you get back is going to be negative And so we negate it to get a positive one Which is the right way to to compute this value So the question then becomes what is p i right so what is the probability of this word being you know the The correct answer and the probability of this word being the right answer is really just How the Relative likelihood of this word combined with the other possible words There's a little bit more where we have to consider the possible patterns But let's just figure out what the probability of the word is first And then we'll go from there So we're going to need something to compute the probability of a word Which means that we're going to need the total count of the remaining possibilities In fact, I think I think I want to do this a little No, let's do it silly first Um, so we want total count uh of the remaining words dot iter dot map Because we want to grab out the counts I guess this can be c Dot sum And I'll let this be a u-size um and Here we want, you know the P of word is the count of this word divided by the total count But we're going to want to do this computation as a floating point because otherwise we're going to lose all the precision and in theory the The value here is Uh The p of word uh multiplied by the p of word log two Minus, uh, and then this whole thing, uh dot neck Um, of course, you might wonder where did the sum go right? Oh, is it not? neck Really? I Guess we could do Abs, but I don't really want to do abs I guess we could do that Um, of course, we're missing the sum here um the uh The sum here is over all possible patterns So, um, this won't actually do quite the right thing because if you think about it, um Imagine we have a current set of candidates The The expected amount of information for any given word that we might get guess next is uh Is the the probability of That word being the answer Is like it differs depending on Let me I'm trying to find this is where I want to avoid Repeating and botching the stuff from the three blue one brown video But it is important enough to repeat that I have to find the right way to articulate it um All right, here's the basic intuition Um Arguably I should draw But I'm gonna not do that Okay, we have a bunch of words. Let's call them word one or two word n Right, um, and we want to Figure out the the goodness of word one the goodness of word one is the Sum of the goodness for word I Uh, the goodness is the sum Of the goodness Of each possible pattern Uh, we might see in the uh as a result of guessing it Uh multiplied by the likelihood of Uh that pattern occurring You're gonna see how this ends up tying into this formula in a second um And the the intuition here is that The probability of a word is the sum of the probabilities for all of the patterns that we might see I don't know if that's actually true. We'll get we'll get to that in a second I need I think we need to start coding this And then I think it's gonna become clear So, uh, how many possible patterns are there? Well, there are Three to the fifth possible patterns, right? There's correct wrong and um misplaced I guess we called it for each position So here's what we want to do for each possible pattern. How do I want to express each possible pattern? It's really like the it's not the power set but close enough to it. Um And I mean we could statically compute this list Right, it's just because it's the the set of candidate patterns is always the same. So in fact, maybe let's uh Do this Let's have this be a helper on Correctness Which is going to be something like pub f n all Or permutations, maybe And it's going to return something that implements an iterator where the items are going to be self five Yeah, it's not quite three to the five. You're right. There are some patterns that are just impossible But we might as so it doesn't really matter whether we consider them it might be an optimization but We would just find no candidates that match that right? So so it's not really a problem To include them in the list of permutations that we check. It's it's an optimization. It's not a correctness problem Because you would find no candidate words to place in that bucket. So the probability of that bucket would be zero I wonder actually if there is a helper here we could use from iter tools I feel like there almost certainly is Just because I don't want to reinvent the wheel here iter tools permutations I mean really what I want is power set But I don't quite want the power set because I want the power set but only of a certain length Which is a little nicer Ah There's a macro for it the I product macro I product Oh perfect cartesian product. Yes, that is indeed what I want Excellent So let's do cargo tumble iter tools is zero dot 10 And what I want here is iter tools i product And I guess this is going to be it's a little awkward, but it's fine self correct self misplaced self wrong one two three four five unresolved macro really no telling that's yeah uh And currently that gives a tuple which I think I can map Uh using From Okay, this seems like an implementation that should exist. Come on standard library right like A tuple where all the elements are of the same type Of a given length should implement into an array of the same length of that type That makes me sad. All right fine I b c d e to a b c d That's fine Um All right, so what we're going to do here is loop over all possible patterns um for pattern in uh Correct I guess let's call this patterns not correct, uh correctness patterns Yeah, this is where we're gonna sort of be able to very quickly see how naive the naive one is Because here what we're gonna have to do is uh compute A version of remaining that only has the words that match This pattern on this word Right so uh to to show this in code. It's going to be self dot remaining dot Itter dot filter Uh Candidate and we're gonna filter on pattern No, we're gonna filter on guess of uh Word Is candidate up two string we could optimize that a little bit. Uh mask is pattern Matches No word matches Candidate cloned in pattern Total Is zero dot i'll explain this in a second remaining is In pattern Left in pattern Uh cloned collect map. I guess we could uh We could clone first We do let g is guess if G those matches and g dot word in fact, we could do Filter map then g dot word Word dot Two string if g matches we we can optimize this a decent amount for things like allocations, but i'm gonna not do that right here um And I also want the In pattern total To be plus equals the count Let's bring that in Uh If g matches candidate left in pattern Um So this is computing for every pattern find all of the find the subset of the remaining words that also would exist if If we guess this word and that pattern matched right so in this world It like in this inside of each item of this loop. We're considering a world Where we did guess Word right so word is the the outer loop um and got pattern as the match As the um, I guess as the correctness I'll put that up top here Um Now compute what then is left Um All right, so that's what's left here Uh, and then what we want to compute is The Let's see here so the probability of us actually getting this pattern is the um Total count of the words that are left if we get this pattern divided on the total divided by the total of words outside of that pattern right so the the Probe the p of this Of us getting this pattern is the in pattern total We might not actually need the left in pattern here, but we might be able to use it um divided by the uh I guess this should be like remaining count um divided by the remaining count Yeah, I think this can just be for are in remaining For candidate and count in remaining Uh, I do this. I don't think we actually need the um I don't think we actually need the words we would pull out So the probability of this pattern is that and that's the thing that we want to sum across right so um We could turn this whole thing into like a Just a functional iterator uh in practice what we're going to do is sum is zero and we're going to do sum plus equals and then p of this pattern is the actual uh uh Yeah is is each p i So we're going to do uh sum of p of this pattern times p of this pattern dot log two And then at the end we're going to say sum is sum dot Or zero point zero minus the sum And that's going to be the goodness right so what we're computing here is um Given a particular candidate word if we guess this word What are the probabilities of getting each pattern and we sum together all those probabilities? And then we use that to compute the uh the entropy the information the the information amount in the um Uh in guessing this word So at least in theory that should do the right thing Uh, and I guess we don't actually use the count of the candidates so we can get rid of that Uh, and we don't use this either which is interesting All right, I think that's right Uh, and now the question becomes Uh, if we go back to main, let's let's just say this is correct for now um Match, uh, I guess if let score I guess sum score Is play then we're going to print line Yee print line Failed to guess And let's just see what happens if we run this well It takes a long time to run and my guess is that this is because This computation is very slow It's also single threaded right If we go over here and look at h top Like it's it's spinning over here. It's doing its thing. It's using my core Where is the actual I don't understand why why is this sorted the wrong way? That's very unfortunate Six back up to the top Come on There we go. It's just spin my cpu Of course, it's because it has to consider so many possibilities here Uh, I think we we might be able to do just minus some yeah, great Um, so the question now becomes okay. This is clearly too slow Right. Um, in fact, let's try to do like Let's just to monitor that it's making progress Do this Okay, so it is making progress right it is it is running that loop, but it just is really really slow um To the point where naive can't even solve one game Like it can't even finish one game because there's so many possibilities to consider here. Um In fact, let's try to print out here Um Is better than Just to see that it's like roughly doing the right thing So we're claiming that uh word is better than C dot word because goodness is better than C dot goodness And I guess we could say here starting with This Goodness is this Nan, okay, so clearly something is real bad here. Um, it's not going to make any progress if all of the uh All the values here in nan Let's do debug of this And debug of this All right, those seems like those seem like plausible numbers um Debug of this and debug of this Those are still also plausible numbers And debug of the whole thing Those are still plausible numbers And debug of sum Is nan That seems real unfortunate Oh, because it sometimes gets back nan from this computation See if we can spot Ah log two is infinity sometimes the probability is zero that makes sense Yeah Um, so we're gonna say if in pattern total Is equal to zero Then we can continue Right, because then if there are no if there are none in here the probability of this is zero So the information is zero is not even worth considering and we don't want to compute the log two of zero There we go. Okay. So now it's doing something reasonable So sum plus equals this See what it says All right, so it seems to be doing like something interesting here, right It's concluding that some words are better than other We don't really like we could look into the data set for why but like It's interesting at least that it is computing good initial candidates um Quees better than boons apparently And these all seem like, you know plausible looking words. Um one thing that I did notice in the um In the three blue one brown video is that grant hard codes the first guess Because it so significantly reduces the the search space. So how about we do the same thing? and we say If history dot is empty Then we're going to return and I think hard codes it deters Coach is better than couch Couch is better than coach But there's still a lot of possibilities here Oh, okay. So it finished its first game and got five I guess let let's have it actually print, you know score And Uh, let's not have it do this and not have it do this Uh, there is one other optimization that I think, um Um Was implemented here, which is I believe that Grant also implemented a sigmoid over this probability So rather than saying, you know, um Which is like infinitely more probable like Several orders of magnitude more probably the than, you know Couch like something that's that has a much smaller count than which We flatten the curve using something like a sigmoid where it becomes more of a cutoff Then it becomes like an actual like, you know exponential difference So The sigmoid I think you want to apply um It's a good question whether we could apply the sigmoid actually to the um To the counts themselves rather than Redoing it on every probability um But even so let's just see what happens if I run this for a while Uh, I guess let's also print out what the answer is for each one. Uh, so we'll do uh Guest this in Starting with cigar goodness minus zero It's a pretty small goodness Yeah, guess cigar in five. Well five is less than six Like it it did successfully guess a word as far as wordless concerned This is where it's nice to like have visualization tools and not just have, you know, this debug output So we actually could actually see, you know, how it makes its decisions for a given game Um and where things like top k's would be good, right? Like instead of just keeping track of the best one keep track of the top 10 and then display them before you guess So that we can see that it's actually making um somewhat reasonable choices Guest rebut in six sissy in five Humph in four. Okay, so it is like playing the game and doing something reasonable Right That's really slow, but that's sort of the point all right Let's do go ahead and get commit. Um First iteration of naive And again, like I guess we should uh put in here, you know, uh to do Apply sigmoid All right, um, but let's now try to figure out where it's spending its time Now there are a lot of ways to do this. Um, I kind of like Uh Why did I close one of these? Um, I really like using the like fundamental tools. Uh, it makes me happy Uh, so what I'm gonna do is I'm gonna run pgrap. No, I'm gonna run perf record Uh call graph dash p Uh six four five four two So I'm just recording some stats about this And now I have a perf dot data file And it's 200 megs large great and then I want to inferno collapse These are the tools that, um cargo flame graph uses under the hood Uh perf dot stacks I guess I can just do Inferno, uh flame graph and I want this to be into perf dot svg Whoa Weird event line. Oh, right Uh, I lied, uh perf script There we go. It's perf script I don't need dash i And then I pipe that into inferno collapse perf and I pipe that into inferno flame graph And that gives me Um perf dot svg All right, let's see how that looks Uh, so we're gonna go to uh home john Uh dev streams Roger And I want perf dot svg all right So, uh flame graph is it displays It's gonna be annoying that my face is in the way. I apologize for that But um flame graph is a tool that basically displays where your program is spending its time and the visualization here is that um The width is the total number of samples And so if something is half the width of the screen that means half of the times your program was sampled It was spending its time in this location. So here we can say all of our time is spent in, you know, main Which is unsurprising, but you can then zoom in to see where what does main then call and where is that time spent? So if we go all the way up to roget main we can go back up to um main guess And we see that in guess A lot of the time is spent in malloc and free and then sometime is spent in guess matches So this makes a lot of sense, right? Our time is spent allocating and deallocating And then computing whether a given guess matches Not surprising that that's where a lot of our time goes Um, but we can do this better, right? We can we can optimize this a little bit One thing you might have noticed is that these stack traces aren't ideal There are a couple of reasons for that and one of them is that we're running in release mode And so we don't actually have um as much debug symbols as we would like so we can do profile dot release Debo Debo equals true Uh, and let's try to run that And i'm gonna perf record Uh pgrep Uh roget Once this is running for a bit Let's see if that gives us any yeah, so here now you see we have much better information in our in our flame graph Of see if we zoom in to play what we got here Uh, all right, so all of it most basically all the time is spent in guessing right in Calling the the guess method of the guesser um And so now where does that time goes a lot of the time is spent in two string Inside of guess Uh, a lot of it is spent in dropping strings in guess Uh, and then a fair amount of spent is spent in matches But let's see if we can't do anything about these allocations um so We call two string here in the type loop where we compute over patterns And that seems really unfortunate. Let's see if we can do better than that, but before we do um, let's go ahead and actually This is so slow that I can't even really run hyperfine on it because it wouldn't complete We could limit this to a smaller number of guesses. Um, that is one way to go about it. Um, but Oh, do I want to do that? Actually, here's what we're going to do. Um, let's go back here and Give us a way to run different algorithms using the same binary Uh, so let's say that Uh Let's say that we do It makes me sad that naive has to each game it has to recompute the dictionary, but it's fine Um, let's go ahead and bring in clap here because I like clap Um, so here I want to arrive clap Use clap clap Clap clap clap clap clap Uh, let ops is opt I should arguably look at the manual and copy paste the example No clap clap and root. All right fine. I'll open the documentation Um, uh, it's clap parser Oh, you know close enough Uh, I still want games to appear up here because it's a constant um, and I guess it is this um, and what I want is Implementation Uh, and that's going to be a I want that to be an enum Enum implementation Uh, and I want it to be either Currently just naive Uh, and we're going to say that implementation is going to be implementation Um, and then I do actually want one more thing which is, uh Short long, uh max Which is a maximum number of games to play Fromster for implementation Uh type error is equal to string There are ways to just derive these, um For enums so you don't have to write the implementation yourself um But instead what i'm going to do is just Do it myself Because I don't want to look up where to do it Uh format Uh unknown Implementation This one So that's going to be error of this It's going to be okay of this And this has to be qualified Because why name it fully? um All right, and so guess her I'm going to have a little helper here, which is going to be play Um, and you'll see why in a second It's going to take all of these bits Um It's going to take a maker It's going to implement Fn Mute is fine And it's going to return a g It'll be generic over g Where g is a guesser So the idea here is you're going to pass it a constructor It's going to run the constructor And then play using that And we're going to hear match on args.implementation And if it is implementation naive Then we're going to call play with a closure that runs rogit algorithms naive new Which we can just replace with this because it's the same thing And now this can go away and play It can stay there This has to be mute. That's fine Uh, the reason I do it this way is because I want This to be uh monomorphized to the particular guesser I don't want like a dynamic dispatch for every play for example Because then you would end up with like dynamic dispatch over the calls to guess Which seems excessive Clap as a macro for enums. Ooh, does it? Uh clap arg enum instead of parser Ooh arg enum debug clone copy And I'm guessing I need to remove my from string implementation That doesn't seem right No, it still requires From string although maybe I'm just uh arg enum Oh, I guess I can say uh possible Oh, no, it should do that for me, right? Oh, I need to set this here So this is going to be arg enum as well nice That makes me happy And I'm going to open this again because otherwise Rust analyzer doesn't realize that I have new Traits Um And max is an option use size It doesn't require the you pass one Uh args dot max Max is an option use size Um, and here we're going to do This dot uh dot take max unwrap or Else use size max value Or use size max I suppose unwrap or Use size max implementation naive right Fantastic, and if I pass uh dash dash max say three Um, it should now run the naive implementation, but only for three games. So if I say say one Let's do two Then that will continue to run in the background the reason I want to do this is because I can now go to naive Uh, and I can copy paste this whole file Uh, and call it this, uh alex and alex is going to be The same as naive Except we're going to optimize the allocations a little bit The only reason I do this like normally you wouldn't keep all of these parallel ones around, right? um But It makes it much easier for us to compare Uh, how the different ones are doing So I can do the same thing here alex alex alex Great, uh, so now we have the naive implementation and I can go to alex And see if I can't make this a little bit better. So I'm going to make guess Oh, this is gonna it's actually going to be really awkward because I'm going to want to change some of the types which are going to make the other implementations not Work anymore Uh, I'm going to have guess Have a lifetime And allow word here to be a uh to be borrowed because there's nothing that really requires That this be owned Right all of our implementations on it just care about you having a string reference And the reason why this matters Is because over in Naive This will remain cow owned But over here it can be cow borrowed Now, which means we don't have to Allocate a new copy of the string for every guess That's actually the only difference. I think we're going to apply here um And just to see that this actually still runs Uh, so let's run this without a max and then try to Benchmark this one You see it's still kind of slow like it's still clearly not, you know spinning around But we're going to stop it And then we're going to do alox And if I now open alox.svg You see that now guess is not spending any of its time in This allocation stuff at all. It's in fact spending almost all of its time in the iterator next and in matches And we should be able to see this right in the performance. So if I do um So we have this as the compiler artifact So hyperfine is uh, basically you give it a a bunch of commands and it'll run each of them So i'm going to say Please run roget implementation naive max One And run this implementation alox max one and I think you can name them. I think it's name naive And uh name alox I guess it's just n And now you'll see it's gonna Run this command multiple times To to measure the variance and it's execution time Which like here we don't really need the variance because we happen to know that it should basically not vary at all And because the runtime is so long You see the eta is about three minutes for it to finish benchmarking naive and that's because it's going to run it multiple times We can tell it to Not need to run things as many times Um, but we'll see here what it what it comes out to and then we should be able to tell that We should be able to tell that the one where we remove the allocations is faster Even though they both looked like they were slow That was just like we eyeballed and it didn't return immediately Whereas this should actually tell us is there a statistical significance to the diff that we made While that's running in the background. Let's go look at these So now let's zoom in on matches here My face is in the way. Let's see if I can get my face to disappear Actually, can I just Sure, that works close enough. Um, I think now it's not cut off, right? It's not ideal. I know but, uh, otherwise I would have to switch to obs and stuff So matches, I know it might be small now, but we have a bunch of time spent in cow deref So that certainly suggests that we're like That that suggests that the problem here is the number of iterations Not what you're doing inside each iteration, right? Because deref does Very little, right? It's a it's following a pointer. Um, so once that starts to show up It suggests that you're doing it a ridiculous number of times um So like at this point it's it's more helpful to reduce the number of iterations than it is to Uh, reduce the amount of work you do within each iteration, but even so it's worth doing. Um All right, so we're also this is iterator next Which ends up doing next code point Uh, and this is also next code point Okay, so we're spending a lot of time just iterating over code points in utf8 strings Okay, that's not ideal. Um Let's see if we can do better for that and that's all inside of guess dot matches Um, let's see if we can improve the code of that a little bit Let's go back here. I'm gonna Me doing things in the editor is going to affect the benchmarks that are running here by a little bit The hope is that I have enough cores that it shouldn't really matter But um, let's go look All right, so next code point is a problem um Now of course we have a a second problem here, which is that Once we start changing guess naive is no longer going to work Well, actually no, maybe we can or rather naive is also going to benefit from the same performance improvement that we apply Because we're fixing, you know guess matches as opposed to Fixing something just for alloc or just for a new implementation So we're gonna have to be a little bit more sophisticated about the way that we do Benchmarking or we're just gonna say okay naive. We don't we don't care about naive anymore We're just gonna benchmark different binaries instead Okay, so this is saying I finished naive now the mean time to solve again just one game is uh 19.5 seconds plus minus, you know point three seconds And alox currently seems to be about nine seconds mean, okay So we cut the runtime in half by removing these allocations. That's pretty good, right? That's pretty good. Um Let's see how much better we can do If we fix up this um So let's say that uh G chars is going to be self dot word Dot as bytes Uh and let w cars is going to be self. All right. It's going to be word dot as bytes And then here we're going to loop over G chars and actually we can just do uh Bites Bites I'm just gonna make all these iterations be bytes instead of cars And it's gonna it's gonna yell at us here, right? Like That's we don't really care about the uh Oh great. I don't even need the itter It's gonna yell at us here because now we're trying actually maybe it won't maybe this is just gonna be fine because we're now We're effectively now just comparing Um the bytes of the string rather than the characters All right Yeah, so alox ran about two times faster than naive Great. So we now know that alox is is better than naive Um, now we're gonna have to do a little bit of a trick Uh, we're gonna have to copy this file to uh rojot utf 8 Because once we compile it with this fix Um now if I go cargo run Um In fact, let me do let me run that one and then try to gather some more data here And now this is going to be alox No utf 8 no utf 8 on svg. All right, so now guess matches You see now the time has been in d ref and in Nothing really and an ek and an iterator Any check Okay, so this is now Oh any check Oh, right. So this is um the inside of the closure That we end up applying here Right. So that is the closure over in Uh or this closure This is the one place where we could try to just like replace this with uh, oh, let's do that in a second Um, so let's try hyperfine here now and now we're gonna go Um The one that we copied over here With implementation alox um, so that's going to be Previous All right, let's let's call them old and new And new is going to be that one with max one. Let's see what we get Oh Oh, we called it roget utf 8 So old should be the same as the new like alox in the previous run, right? It should be should be about nine seconds Yeah, so that that because that one hasn't changed the same binary we used previously and um because we know um That there's there's not too much variance here. Uh, what we can do is do um max where's the max uh min runs We want to change min runs To be like two Because we're not expecting a lot of variance and running it 10 times seems excessive here That matters a lot more if you have programs that are much faster, right? If you have a really fast program, you need to run it many times to to Filter out a lot of the noise around things like process execution and spin-up time um How do you benchmark? Uh, the parts of measure can you add trace points or something once you want to do that, um There are tools you could use for this like you could also use criterion Right like you can write this as a benchmark inside of rust rather than benchmarking the whole binary Um, I kind of like benchmarking the whole binary like for things like this Um, but we might add criterion benchmarks for this too Okay, yeah, so you see that that also significantly speeded things up But but not as much as maybe we would have hoped right it would be nice if iterating over the bytes Uh, you know reduced all this cost for us, but it doesn't really It's only a little it's only, you know 50 faster um So in fact, let me um Let me go ahead and just commit some of this Um I'm gonna first commit the Change here to uh support choosing an algorithm Uh, and then I'm gonna add Allox And not the bytes change yet Add, uh, implementation without two string Uh, I also want to get ignore Uh star dot svg and star dot data Actually, let's do perf dot data and perf dot data dot old And also Roger dash star because we're gonna end up with more binaries here ignore work files Uh, and then we're gonna have commit, uh iterate over bytes not cars for ascii And then one thing I actually want to do here is Let's say head four I want to reword that one and this one Speed up so this was a Gets a 2x speed up This is uh gets a 1.44x speed up all right And then I wanted to try one more thing here Which is uh, let's copy this one to roget bytes And let's try Remember how someone pointed out that we could just use the other Uh, implementation directly Oh, these are all gonna fail now. That's fine Borrowed no, I do not What did I do? Cal borrowed This uh use stutter Borrow cal Is that gonna give me my test back? Great Um, so this is a fix up Of uh This one that fix up just merge us together the commits and the get history um Right, so someone pointed out that we can just use The correctness compute function here So how about we just do that? Just return this Uh Self.word And self.mask So if this is equal to that We're just going to reuse that implementation What what did I do? Oh Um Let's see how that does. Oh, I guess a lot faster Let's see if that's a meaningful difference Um, so we're gonna do old is going to be What did we call it? We called it, uh bytes Against the current one Let's see we get here That's interesting. So that is a speed up of 20x Um, and what this suggests it's that This method is way faster than the scan that we implemented here This makes this sort of means that I need to understand why this is correct because this is definitely a worthwhile optimization so a Word will match a pattern Oh, I see. Okay. The insight here is that if we say that the The the target word here is the correct answer Then guessing the previous word should give you This mask this set of correct bits relative to that answer and that set of correct things should be the same as Like I'm almost there. I believe you. I mean, okay. So we could obviously do this. Um Uh A is self dot matches word Uh, let b is equal to that cert eek a b And then return a Yes, so they don't always return the same thing I could be a bug right like Uh, let's do self dot word word mask self dot mask tears if the previous guess was tears and the new guesses brink And this pattern then the two disagree All right, so let's make this into a test case of uh, I guess matches debug From crash Uh, so this is saying that if we check tears with wrong misplaced misplaced wrong wrong So the question is what should that do against brink? Let's just work out whether this should be allowed. So there's no t. That's fine Uh, there is an a This does not have an a so this should be disallowed Right Because there's supposed to be an a somewhere and this one doesn't have an a um There's supposed to be an r. There is an r. It's not supposed to be need. There's no e should not be an s. There's no s. So The a is It doesn't check. Okay. So the the problem is that it doesn't check um Whether you've used all your yellows So i'm guessing this is actually uh bug in our matches. That's what I think Um cargo t From crash Right, of course, that just hits the same assertion. We just inserted. So let's get rid of that for a second Yeah, so when we delegate to our own Matches that we wrote that test case fails, which is actually our implementation of matches is incorrect But the the correctness compute seems to be right um So I guess we will leave this test and Oh All right fine fine fine fine fine fine fine you will get rid of this And we will say that this is just this To do explain why this is equivalent Uh The pattern shouldn't be able to distinguish between word and answer That's what getting a specific pattern means I see the pattern is in some sense like an x or where whether you give it the whether you use the guess guess with pattern gives answer or answer with pattern gives guess are um interchangeable or um indistinguishable So the idea is that if guess if guess g Gives matches Uh Gives mask c against answer a then Guess a Then guess a Should give mask c should Also give mask c against answer g Yeah, so it was slow and wrong nice um Now if I run this it passes That's great So we're gonna uh What was the speed up 18 times? 18.36 faster. Okay. Um Use Correctness computation Use correctness computation for Matches this Gives a 18.4x speed up Or about 18x speed up If something is correct, it can't move. Yeah, so that's commutative if yellow it needs to move Uh, which is commutative and if gray another letter is needed, which is commutative. Okay. Yeah, I believe you I believe you Um, all right, so let's figure out where time is being spent now Let's do the same thing. So we're gonna r This and now it's gonna be fast. So now we're just gonna yeah, look at that go And also because we fixed matches it actually does better now than it used to which is it's own kind of fun All right, let's grab some data here And print that to New match New match svg New oh No new match. All right. Where's our time being spent? Guess So we got iterator next Which does iteration over a Hash map This is the iteration over remaining And I think remaining actually Doesn't need to be Let's copy that to Uh, what are we calling this new match? Um, I actually know this can be a different, um This can be a different implementation Uh Alex With, um Vec remain Vec rem Because we'd like to be very brief here um So back to main here, we're gonna have Vec rem Vec rem All right, give me Vec remain here Oh, I called it the wrong thing didn't I? main Oh Fine fine fine fine fine fine fine fine Uh, that's great All right Vec remain so here the observation is that uh remaining doesn't actually need to be a map because we never look up into it We only ever iterate over it So why not just have it be a Vec of a tuple of these things? Now we can do Vec from iterator of that We do use it for retain but That's fine too um And we use it like this for iteration Uh, we Iterate over it here So Alex. Oh, right. So Vec remain doesn't even need hash map in here. That seems pretty promising So Alex does some things and Vec rem Oh Vec rem goes faster. It gets to see in five instead of four And that's probably because the order is different So ties get broken differently Which is Interesting that it matters Yeah, that's very interesting that it matters But okay, um All right, so hyper fine Please give us This one and I want, um Vec rem Versus Alex Show me what you got I can run much faster now Okay, so Oh, it shouldn't say Alex a naive. This is old And this is new But even so the the names don't matter. Okay, so it's slightly faster But if you look at it like the variance here is pretty high too So like it's a little better um But not clear it makes that much of a difference But even so like a speed up of 10% I'll I'll take it um Algorithms Vec remain um Switch to Vec for remainders This brings a 110 percent speed up Uh, all right. What else we got? What else we got here? Let's look at the flame graph again And then Bring that into This is now Vec rem I'm gonna close these older ones because they don't really do much SVG All right, I always see some other things start to creep in over here now. We've got Something over here Um, that's fine. All right, what do we got here now? compute There's uh next code point now. Oh, we got next code point back. We know what to do about that Um, so that's going to be in lib Uh chars Um, so let's do let's copy that over to Vec Uh Vec rem And now we're going to make this be bytes again bytes bytes bytes Ah, that seems uh seems faster still. Uh, let's see if hyperfine agrees This is going to be dot slash rogit Vec rem Against this one Oh for the max. Yeah, we could totally increase the max now um, the biggest thing that we'll do is um It means that we get the benchmark across more answers Um, which means we get the benchmark the earlier part of the process better too Um, which seems fine That's a good question actually is whether we always Oh, okay. So here's a problem That was Oh, maybe it's not that bad Okay, no, never mind. I thought I spotted up a bad thing. Um, okay, so Clearly this is faster Uh, right. This is Get commit Use bytes to compute correctness This brings a Speed up Let's have that run in the background. There's more benchmarking And now this is going to be bytes Let's see we get bytes dot svg All right, where does the time go now? You see we keep getting more things to creep in on the side and that's because we're getting better at optimizing the the remaining bits um So what do we got here now? We have iterator next zip Get unchecked from iter bytes Uh, and any so this is the closure that we have in correctness compute So which closure is that that's this closure. So this closure we're now spending Almost a third of execution time is spent in this closure Um, so let's see if we can do better here um That seems pretty straight forward um Although certainly one thing we can do here is this does an index look up and this looks an index look up Uh, which means that they're going to be bounce checked So how about we do zip? Um used Dot iter mute Then we don't need the enumerate and we get uh a and used And if it's not used And we set used equals true So this should get rid of the bounce check Let's see if that made a difference Uh, oh, I forgot to copy the binary first Um, so this this then is bytes I guess I overrode in earlier bytes, but that's fine um And run this and now I want hyperfine of bytes Against this one. Let's see how that does This one's only barely faster Um, let's make sure that it actually is like let's say we're going to run it against, you know, a couple of more solutions And we're going to run it a couple of more times Because I want to make sure that we're not just measuring noise here and it does actually make a difference it's interesting actually because This suggests that the bounce check was probably removed already like the Ah, it is it is faster actually Um Statistical outliers. All right. All right. Let's let's just Settle down here. There's a lot of noise here, but um Yeah, see old is faster than new here Which is interesting um It suggests that this actually slows things down which also suggests that, um The bounce check was probably already eliminated previously Because this should definitely not have the bounce check, right because it it um It's doing a zip on the iterator so it knows that every yielded item is in bounds. You see old here is way faster than new So that suggests we should keep this the way it was Which is interesting All right, so that's that this is an example of an optimization that turned out not to be worth doing So we're not going to do it. Um What else do we get get here? Okay? Here's a an obvious one that's been brought up in chat, too Which is why are we carrying strings around here? Well, we know that everything here is uh five bytes five bytes That seems silly. Why are we doing that? Um, so what we're going to do is Uh pub type word Is going to be a u8 of length five And we're going to say that this is going to be a word try into Uh Dot bytes Try into expect Every word should be five characters collect Try collect Word fine, um Oh as bytes try into bytes gives you an iterator over the bytes as bytes gives you a slice All right, so this is going to take a word um And we're going to say that This is going to return a word word Word we're all going to be words word word And where where where we got these strings word Where i'm getting getting uh You know that sense when it feels like it's not a real word anymore Word word word word word word Itter Itter there these are already bytes. There's no reason not to just Dot itter like there's no need for a dot bytes anymore Great word And now this is going to be b b So the b prefix here just means make this a byte string Instead of a uh utf8 string Same thing here Same thing here Same thing here All of these are byte string you get a byte string and you get a byte string and you get a byte string and this Could arguably do a Vim macro here, but i'm almost done Yeah, it's always one more Sorry, not sorry Uh, let's just see that this actually compiles which it of course does not because Consider dereferencing the borrow Oh That's fine uh, this is because That produces a static reference to an array of that length Um Whereas what we're telling it it needs to return is the actual um The actual five bytes Which is fine. I think what else we got 39 Um String from utf8 8 dot unwrap. That's fine Can't compare these two. That's fine too What else we got? Right naive So we're going to bring in word This is going to be word Um Dot uh as By actually, let's do word is word dot as bytes Dot try into dot expect Every word is five characters Every dictionary word is five characters Uh, and we can do this for all the algorithms So for alox we do the same Uh, and for vekrem we do the same Uh, and we'll stick in word here This is going to be word Word word word word word word word word word word word word word word Um Bring in word All right, what else we got? Um expected string in vekremain Right this returns a word We might even be able to remove this um this top thing now Oh candidate holds a word um Yeah, I think we might be fast enough that we can even Compute the first word this way. Um, that's not to say that we necessarily want to Oh, I think at this point naive doesn't really make sense anymore because there's no There isn't even a notion of a cow here or isn't gonna be so we're gonna get rm Algorithms naive And bird is the word word is the word word word word word word word word you're you're not wrong um Main let's get rid of naive because it doesn't exist anymore um And over in algorithms alox I don't want to substitute all sturs with word because it does um structs and stuff too But I can do this I also do it does make me wonder whether we actually would compute Um Tears if we tried I'm I'm guessing probably not because we don't do the smoothing Um main 43 Um right Answer is Going to be a Roger word and it's going to be answer as bytes Try into unwrap Uh vim replace doesn't like um backslash b because it doesn't have good enough regular expressions, which makes me sad, but um All answers are five letters And I guess we'll do this answer b so that answer can stay a string I think answer here can just be word. Why does it need to be our reference anymore 278 Uh, I can't implement display because word is a type alias um Oh, this is why I go To her Get off my lawn All right, how about that? Great. Whoa, those ran real fast. So in theory now, let's see, what's the last one we made? We made a bytes, right. So let's do a hyperfine of Roger bytes with this one and see how we do now. So now there are no more strings in our new implementation. That looks like it's only barely faster. It does look like it's faster, but not by much. Wow, 1.02. You know, I think this is because we're not implementing a slightly different optimization, which is I think most of our time now is actually spent in setting up a guesser. I should have implemented this first. I'm going to implement that first. I'm going to stash these changes and then I'm going to go to, and then I'm going to copy algorithms, Vecremain to algorithms once in it. And we're going to add one here. And you may pretty quickly realize where this is going. But this is going to be, this initial load is unfortunate because it doesn't really need to do what it's currently doing. And in fact, it's reading the entire dictionary at least once and allocating a vector for it every single time we load it, which seems unfortunate. So I want to see whether that makes a difference first. So I undid the string change just to do this first. So here what we're going to do is bring in one cell. And there are a couple of places actually where we could use this. We're going to go ahead and do, I'm going to have to stash these changes too and then I'm going to have to do this and then I'm going to copy this to Roger. I guess this is just Vecrem, which we already have. What I want here is, I want all the places where we load the dictionary into memory to only happen once. It already only happens once for Wordle. So this loading is going to be, every time we start a new question, if you will, like a new, there's a new answer, we construct a new one of these and each one has to construct this vector, which seems unnecessary. So what we're going to do is, where's my, this thing? We're going to say static initial is going to be one of these. And I guess I'll bring in this into scope too. One cell is like Lacey's cell, but better. So what we're going to do here is, this is going to be cow static of this because it's either going to be a borrow of initial or it's going to be owned once we start pruning it down. Again, this is, I've undone the string modification just to test this out first. So here, I forget what the, it's, where's the get or init, initial get or init cow borrowed. Let's see where it's going to be complaining here. It's probably going to be complaining down here where we make remaining is my guess. The magic of cow dot what is that method called cow to mute. And there's an argument here actually that instead of to mute, which is going to immediately clone the thing. I think what I want to do here is if, so this is going to clone the vector that has the entire dictionary in it and then call retain, which seems excessive. That's not really what we want here. Instead, I think what I want to do is match on this, match on self dot remaining or actually just do if matches self remaining cow owned. Then self dot remaining dot to mute, which will do nothing because it's already owned. Else self dot remaining equals cow owned of self dot remaining, iter map or filter dot collect type mismatch. It's complaining that cow owned of anything, it doesn't really matter. And then it's complaining over here because collect exists for bool. Aha, try that. Value of VEC that cannot be built over elements of type that. That's correct, needs to be copied. All right, so let's go ahead and try to compare these ones. Hyperfine give me the previous place where I did this one and I want M5 and I want to compare Vecrem with let's say seven to once in it. It's just called once. Let's see how that goes. Yeah, so that's, you see it's faster. It's not faster by very much. This makes me think that our overhead is starting to come from something else. I mean, it is faster, but like by how much, you know? I just want to gather a little bit more, even though the variance here is pretty small, but like it's not even clear that it's actually faster. That doesn't seem very much faster. And I wonder why? It should be using less memory, that's true. It's just that it's within the noise. Yeah, like it's all within the noise. It is like marginally faster here, but that's interesting. I wonder, this suggests that there's like optimization going on. In fact, I want to see where that time is going once. And guess, that's interesting. All of the, okay, so all of the time is still going into compute, which suggests that like it just doesn't matter that much that the initialization is faster because compute takes so much of the time. You are also right that it is actually allocating, it's allocating more memory technically because we're only running one game at a time. If we were running multiple games concurrently, this would make a difference, but one game at a time, we're now always keeping the VEC of the entire dictionary around. And then we're also keeping around the shorter vector of remaining guesses. So it's like worthwhile for later. It's just not worthwhile right now. That's interesting. All right, let's keep this one, algorithms once in it, add once in it implementation. And here we can note, not actually faster since bottleneck is compute. All right, and now we can go back to our thing where we have word. Now this can be word, word. Where's our try into, so we're here. And I guess we can copy that into once. Right, and this is gonna be word as well. All right, so let's see whether that made a difference. So here we're gonna run all slash roget once with implementation once. Let's do, you know, max of five is probably fine against this one. The sigmoid shouldn't necessarily reduce the number of iterations. It should just mean that we don't prefer more likely words quite as strongly as we do now. All right, so this is the benefit of moving to, moving to bytes. So it's not that great actually. I mean, we could try making these just be without the indirection here as well. This is now gonna be not a static, but just word, and then it doesn't need to do this. In fact, why is this a cow and not just a word? See if we can't. Yeah, these should just be words. What are we even doing here? Why are we passing around arguably the same with guess, but you know. Word, word, word, word, word, word, word, word, word, word. Um, yeah, no rules expected token. Can I make this be that? Is it gonna yell at me if I do this? Cause I guess it's technically a separate token. I wanna see if it even runs cause I don't think it does yet. Cow shouldn't be needed. This might make a difference. Um, once in it matches candidate. This takes a word. It's a little unclear why it doesn't allow me to... Oh, it's cause I have to do this. There we go. That's fine. It's not actually clear this is better. Oops, this has to be this. Um, to pass around the actual bytes everywhere. Arguably what I should have done is benchmark this before and after this change. But you know, you can't always get what you want as the song goes. Yeah, so same in Vechromane. It's gonna be this twice. No, just once. This is just gonna be word. This is gonna be this and this. Oh, word. This. This shouldn't need to do that. This can just be word. Move all the pointers. And no more need for cow. And alox. Oh, did I not remove this one? That's why I needed this and that's why I needed that. And that's why I needed this. And over here, same thing. This one too. Which makes this only take one, which means this can go away. This can go away. Candidate is here. Word is here. Parentheses are required to understand this expression. All right. And I don't need cow anymore. Oops. And same in Vechromane. I don't need cow anymore. And same in lib. I don't need cow anymore. So now, what do we have? What was the previous one called? It was called once. So hyperfine give me once versus this one. It's true the word is only five bytes, but U64s can be passed around really efficiently. And now we suddenly have like a packed representation. Let's do 10. Yeah, in fact, look, now that we're not using references anywhere, we're always passing around the like five byte array. It actually looks like it used to be faster. Yeah, all this lower. The new one is lower. It's actually, it seems like it's faster to actually pass around string references everywhere than it is to pass around the words, which is pretty interesting. We have one more place where there's a reference to a word and that is the wordal dictionary. Actually, I wanna try to benchmark this and see what we get here. Bites, bytes.svg. Yeah, like, all right. So we've got some time over here and guess. Equality for slices. Where is there an equality for slices? Oh, equality for arrays. Yeah, so there's a lot of equality of bytes here, which is not terribly surprising, right? Like, we're just spending a lot of time doing that. Oh, why does compute have such a large gap between the two columns? The time in between here is spent in the bytes of compute. So there's no function to be called. It's just, that's just where the time is spent. There is a tool for visualizing these, but I do not remember. It's called cache grind. Actually, we can look at this with, why am I opening that? With perf report, call graph to orf. Perf report dash g. Once init guess matches. So that's compute. So this is the command line interface of perf itself. So let's annotate compute. Well, that's not very helpful. What is the thing for showing? Oh, that's entirely unhelpful. Ah, yes. Really? Why does it not? Only available for source code lines. Why is it being unhelpful about this? I thought annotate would do that. Like normally that should also show the, I definitely had perf to show, like annotate should give me the, a real source code here. I'm a little surprised that it doesn't. Like if I go here, now, well, if I do this, well, that's not at all what I wanted. Yes, you see there are two columns here. There's children and self. Children is how much, how many cycles were spent on this function and any function that this function calls, like with this function in the call stack. And self is how many cycles were spent on the assembly in this function not counting its children. And that's where you see compute is really big. Like how many, how large of a fraction of the samples of this function were spent in the function's own assembly. But why am I not getting the debug symbols here? That's definitely a little disturbing. No, they shouldn't be optimized out because I have debug equals true here. But what might matter is build rust flags equal does she force frame pointers? Yes, let's see if that makes a difference. Build, right. Cargo, cargo config.toml. Is that not the name of it? Force frame pointers. Why does it think that this is clean? That's what I want to know. It's going to force it to recompile here. Is it even picking up my force frame pointers? It is not. So, see, you're not supposed to need frame pointers when you're using dwarf, but I found that every now and again, the dwarf info is just not enough. It is really weird that there's exactly one pop in there, which is why I'm trying to get the actual info from this. But I want to see why is build rust flags not taking effect. I guess I can just go back here and then do, no. Rust flags equals stash C force frame pointers equals Y. Oh, is there, can you hear like a water fountain in the background? Hmm, weird. All right, let's see here what we, yes, some people can't. Okay, let me plug it out. Interesting. Yeah, it's for the cats. All right, let's see. Well, that still didn't give me anything. Why? Why, why, why, why, why, why? Rust, perf, annotate, and annotation, source code. It's a very good question. Oh, if you hear it the whole time, you should have let me know. I mean, we could run it through cash grind instead, I suppose. All right, fine. We can use call grind instead, given that it doesn't seem like it wants to give this to me. That's fine. No, that's not what I want. And do I have valid grind installed? Do I have QT? Q, I do not. QT cash grind. Q cash grind. Yeah, you might be able to do this just by using, you might be able to do this using like dev mode and then setting the op level, but I kind of like to not have to do that using call graph LBR. I don't even know what that is. Let's try it, see what happens. No, sorry, that's fine. Okay, so what we're gonna do is use valid grind. So valid grind is a great tool for things like this. We're gonna do call grind on this. Is it not call grind? Val grind, call grind. That's why. So if you're not familiar with Val grind, Val grind is a tool that does on the fly rewriting of your binaries to have them include a lot more debug information. It gives you really, really good detailed information about where you're spending your time. The big downside with Val grind is that it slows down the execution of your program a lot, as you can see here. But it does give you really good information. So here I'm gonna terminate it and let's see what we now get if I go over here, I'll close this and then I'll open this and I'll try to open dev streams. Where is Roger? Call grind, let's see what we get here. All right, main, call emap. All right, so this gives a more like visual representation of what's going on. Let's go to correctness compute. So this is showing actually the time spent on initialization. This is the whole like thing that we optimized with once in it. As you see, we are spending a fair amount of our time in there, right? Like it's not, this is not zero amount of time. Where do I go into, why can't I see it here? Oh, I guess it's just, this is just the, if I go to source code here, it should show me where that time is going. Once in it, oh, I'm looking at guess. That's not what I want. I want to look at one cell initializer. Oh, interesting. So this is in guess for once in it. Where is that time going? So you see, it says 22% of the cycles are spent in the guess function. And let's see where, you'll see it's a little hard to tell because you see a bunch of time here is like, just read zero. And it's because optimization means that where you think the code is running is not necessarily where it's running. But here it should tell us, you see, like, it's not giving me inline information because it doesn't know what the source library is, but I can tell it that. Pretty sure. View, configure, source annotations, add. Dot rust up. No. Choose, no. Where is dot rust up tool chains stable? Am I running stable? Running beta. Glad I checked. Run beta people. Not share, but lib rust lib source rust. And that should be for that target. See if it now can find it. There we go. So now you see it shows the origin source in the standard library so it can tell me where that, where we're actually, which source code instructions in the standard library is where we're spending our time. All right, so let's go back to compute. Now let's look at the source code of compute and see where compute is spending its time. 9% of the time is spent in calling the function. That's good. You know, arguably we could inline compute. In fact, maybe that is what we should do. Initializing this array takes 1% of our execution time. If equal to correct, that's interesting. Yeah, so this, we're doing this comparison twice and we might be able to skip that. This is where all of our time is going. There's a little bit of time here, which is gonna be drop, but I don't think we allocate anything interesting in compute. I wish there was a way to see the actual code that gets executed here. But this comparison is where all the action is happening. Interesting. Yeah, I mean, it's not that much more helpful. I'm guessing this is the pop that we saw in Perf. Like over, where's our report? Yeah, let's run this again and then grab some data and then show me the report. And then when I go down here and annotate compute, like I'm guessing this pop is a part of the return. I wonder why that pop is so expensive. Feels wrong. But yeah, I feel like there's not a lot more to gain out of here. It's interesting that these assertions, oh, we don't even need these assertions anymore because word is now fixed length. F equals G and not used. Yeah, we might be able to make this not branch which would be nice. So pop doesn't drop anything, right? Like pop is just an assembly instruction that just, I mean, it doesn't do anything. It just abstracts from the stack pointer. Although it pops into a register, but not anything that should meaningfully affect our runtime here. 3% in there. Interesting. Yeah, I think it's probably a lost cause to try it off to might compute further. Like it might be possible, but I'm inclined to leave it the way it is for now. Oh, you're right. It could be triggering page faults, but that's also gonna be tricky for us to optimize here. So this suggests to me that we need to, instead of trying to do micro optimizations here, we need to try to do macro optimizations on which patterns we consider, for example, or we could try to make this concurrent. Both of these would obviously speed things up. I'm inclined to try to do algorithmic optimization first and then try to do it multicore, but first I have to go take a bio break. So let's take five and then meet up again. And I'll do this. All right, let's see. Maybe it's mispredicting. That could be. But there's no prediction here. In fact, if we look at the code, like there isn't even an early return. This compute is extremely optimized. Like in fact, there are no branching instructions here. Interesting. All right, so let's go look here at something else. One thing I wanna look at is, I want to see the sequence of guesses. Actually, let's... So one question here is, do we want to keep this move to words over strings? It's not clear to me, actually. Like it's not faster. And the strings generalize much better. Yeah, I think what I'll do is do a bytes. Words are bytes. Words use u8, 5, 4 words. Not faster. And go back to master. And then one thing I wanna look at here is, which word does it choose? And I think actually what I want to do is go to main. Over here, I want it to print, tears, grand, auger, and cigar. I want to see what happens if I change it to not have this optimization. It's certainly a lot slower. The observation here being, of course, that the first word you guess is always the same, because you start out with no information. So one game or another game makes no difference to you. In fact, it looks like it can't even generate the... I wanna see what the first guess is if we don't hard-code it. But that seems like it's hard-pressed to get that to happen. I guess I can try this that someone else suggested. This is perfect event, open, returned, invalid. So I can't use CyclesPP as someone suggested in chat. It's running. Let's doing something over there first. Let's go back here and look at first to see what we get out of there. Yeah, I mean, it's still spending all this time in that closure, a little bit in get unchecked of an iterator of the zip. Is this still running, really? I guess we could say is better than this word, is better than C.word. Goodness, C.goodness. Just because I wanna see something here. Aerons is better than eons. Alert is better than arrows. This is probably one place where the lack of the sigmoid is gonna come back to bite us because it's gonna heavily favor more common words, more so than it probably should. It's interesting though. Remember how the word that was proposed in the video was proposed in the video was tears and arets is the same letters just in a different arrangement. So I wonder whether it might actually arrive at the same, not the same. There's also in Grant's videos, he uses like a special algorithm for computing the first word, which takes into the account the expected information two steps down the line rather than one. And that's, I think he arrives at Crane and then in the follow-up video, he's like, actually it's Crate not Crane, which is appropriate, cause rust. Yeah, this is not, this is gonna take forever to finish. But like it's basically arriving at tears except arets, which is a different rearrangement of it, which is I think interesting. All right, I'm gonna cancel that cause I don't think this is where we're gonna get our performance boost from right now. But what I will do is I wanna see if I run this and don't print out these, and then I want to tee that into a file called, let's say, from tears. Text. I wanna see what happens if we start with a different word and see whether the scores are meaningfully better or worse. Like really we should just be compute, keeping track of the average score, right? Okay, so I ran some there and now I'm gonna go back here and I'm gonna change this to be Crate. And this is from Crate. Oh, why is it so slow for some of those? Why is it so fast for the first ones? It's definitely a lot of variability in the performance here. All right, and now what I wanna do is I wanna do this and look at the difference between from tears and from Crate. Yes, you'll see Crate seems to do, it's sometimes slower, but it definitely seems to be doing better than tears does. All right, so how about we just switched it to Crate because, you know, I like Crate's tears. Crate, naive tears, Crate. And I guess we could, wait, do I not have a Vecu? Aha, great. I want to cargo R this and then I wanna hyperfine. I wanna see whether that makes a difference, just which word we start with in terms of how fast it is. Shouldn't really matter that much, but there are two sort of macro-optimizations I want to implement. The first one is that, is the observation that whether two words, whether word plus pattern or whether basically the output of compute is deterministic based on its arguments. So we should monomorphize as in remember the outcome for any given three tuple and just store that. In fact, we might even pre-computed because we're gonna be reusing it over and over and over again for every single iteration. Whoa, it's so much slower with Crate. Yeah, so I guess this is because there's some pathological case with Crate where it doesn't eliminate as many words in the first step. So it needs to do the expensive search a second time more and hence the speed difference, even though it performs better. That's fascinating. All right, in that case, I'm not gonna change it to Crates yet. I'm gonna keep it as tears because I want it to be fast. But what I am gonna do is I wanna copy source algorithms once in it to be pre-calc. All right, so pre-calc, pre-calc is going to pre-calculate all of the, basically all of the combinations of word, word, pattern. Whether that's feasible is gonna be interesting to see. It might not be pre-calc, pre-calc, pre-calc. All right, so here is the way this is going to work. Sorry, I meant memoize, yeah, you're right, not monomorphize. You're also not doing the waiting to try to get the actual guess yet. You're just trying to get down to one candidate. I mean, it is still weighted by the probability of the word. So it should still, the probability of the word should still matter. Yeah, because if you have two equal options, if you have two options left where both of those two options are, actually, ooh, that reminds me of another optimization we could do. If you have, oh, I have too many thoughts in my head at once, if you have two possible candidate words, then both of them are gonna be computed as the sum of the probability of getting a pattern by the inverse of that probability. Oh, I see, which doesn't take into account the probability of the current word, it only takes into account the probability of the pattern. Yeah, you're right. Ooh, do I wanna do that one before pre-calc? All right, let me leave it to-do here so that we remember to do it, which is goodness, so that's gonna be the goodness here. Way this by P word. So that's a to-do. The other thing I thought of was over here, which is to do, don't consider correctness patterns that had no candidates in the previous iteration, right, because why would you? There's no reason to, but let's try pre-calculating here. So this is gonna be a match or something, and it's gonna be, how are we gonna represent this? This is, and in fact, we don't actually need the word list, right? We just need the number. So this is really gonna be a sort of, maybe it's a hash map, and it's a map from a static stir and another static stir and a correctness five to U-Size, two U-Size, two F64, to U-Size, the sum of their weights, I think is the value we care about here. And how big is that? That's gonna be humongous. So we're gonna go down here, and instead of here calling matches, we're gonna do matches is match, get or init. And then we're just gonna say matches.inPatternTotal, plus equals matches, get wordCandidatePattern, unwrap or zero. And then this is gonna move up to where we compute it. Now, we have to be careful here, there's an additional optimization we can do here, right? Which is, this is a matrix that is symmetrical, where if we compute, this is the same observation as why we can use correctness computation for matching, which is if the value of abyss versus words is gonna be the same correctness as words versus abyss. So we only need to compute the sort of upper left triangle of this. As long as we always look up the words in the same order. So if we're gonna say if word is less than candidate, so the key is gonna be, if word is less than candidate, then wordCandidate. Otherwise, candidate word, pattern, pattern, and this is gonna be key. Why? It's trade bounds, they need to implement hash, which correctness does not currently do. There's a chance that this is slower because of the hashing. Would it make sense to only make the memorization for common words? Maybe a word is cal copied. So here we're gonna do for every word in dictionary. It's true, I mean, we don't need to have all of the entries in here, but like, why not? For word one in initial.get, for word two in initial.get. And then we could do something like here if word two is less than word one, then break. Then break, and then we're gonna have this compute. Let mute out is hash map, new, borrowed word one. Mask is gonna be pattern, so this also then has to be for pattern in correctness patterns. And then we're gonna do, oh, I guess actually, it's a bool, it's not a u-size, what am I talking about? And then we're gonna do out insert, word one, word two, g.matches. And this is gonna be if matches unwrap in pattern total plus equals count. And then this returns out. We know that initial has been initialized, and we're gonna insert here word one, word two pattern. Of course, we can even tell it how large this is gonna be. This is gonna be let words is initial.get unwrap. Let patterns is equal to correctness patterns. And this is gonna be words.len times words patterns.count divided by two. Just to avoid the, yeah, it's true that there are a lot of uncommon words that we end up populating here, and there might be an optimization where we don't have to pre-compute them, but I just wanna see what the outcome of this is. Like whether this is even feasible, I'm not even sure. Pre-calc. Oh, did I do something silly? I did, this has to be pre-calc, pre-calc, and pre-calc. Memory allocation of that many bytes failed. Okay, so in other words, we can't have a thing of all of the words. But we can probably have one be, we can probably do this if we only do the common ones. So let's try that. Words is gonna be this. And then I want to words sort by key where each, I wanna sort by the count. Sort by sorts in what order? Oh, unstable sort by sort, unstable by key is fine. Forget whether it's sort, I think it sorts in ascending order, so we want it to be minus count. Cannot apply unary operator minus to u-size, unsized values cannot be negated. That's true. I guess there's also in, there's a neg, not neg. Rev, rev, comp-reverse. That's the one I want. Instead of doing this, I'm gonna do standard comp-reverse, which is a type that reverses the ordering of whatever type you're given, like so. And then I should be able to just say we're gonna take words, and we're only going to grab the first 1024. Over here, this is not gonna be unwrapped. It's gonna be unwrapped or else. And the or else is gonna have to compute the pattern. Just gonna be here. G.matchesCandidate, what does that do? So that's a smaller number, but it's still pretty large. Doesn't seem that much faster, you know? And I'm guessing it's cause you end up pruning a lot of the, let's just check that this is somewhat reasonable, that if I debug, print, all words, first 10 words, just to see that they are, in fact, the most common ones, they are, I'm guessing that most of them get pruned out early on anyway, so let's do 2048, just to try something. That's too much memory, all right, I mean, even 512, it might make it a little bit more, I mean, even 512, it might make a difference. And then we try hyperfine for this and we do once over 10 and pre-calc over 10. Let's see if that makes a difference. Mm, doesn't bode well, doesn't bode well at all. That is very slow. That is, in fact, much slower than I would expect it to be because I guess we are now doing this lookup, but something here seems wrong. All right, let's grab some stats here. I still have tears here, right? Okay, yeah, good. Pre-calc, pre-calc, SVG. Hash map get, yep, hashing, hashing and table lookups are real slow. I mean, this might be better with a B-tree map to save us from the hashing. So now this needs to be partial, or an ord, that's fine too. 73, I don't need that anymore, that's fine. Oh, you're right, this is the standard library hashing but it looks like it's not even, like the hashing is part of it, the SIP hash, but even just the lookups are where a lot of the time goes. I wonder whether, I actually wanna see whether we end up in this case. Echo. I just wanna see if we even, yeah, it looks like, it kinda looks like we never go in this. Yeah, we never succeeded. Oh, all right, so we do use it sometimes. Interesting, but pretty rarely, word candidate. Like it's very often just not used at all, which seems really surprising. Like it didn't for cigar and rebut, didn't use it at all. Here it used it a decent amount and then not for a while, which makes me think that many of the popular words, basically, oh, you know why this is probably because tears already eliminates many of the popular words. That's my guess here. So I don't think this gets us very far. Like I just don't, I don't think it's feasible to have, like it's such a large search space and it's not clear the picking the popular ones actually matters. That's interesting. All right, I mean, we can add this, but it doesn't seem like add slower with pre-calc. The problem is it's too large. The space is too large, you know? And I don't wanna start with a less good word just to satisfy this bit. All right, so let's then copy this and use wait. So what I wanna do with wait is apply a waiting of the, ooh, nope, algorithms needs to be added here too. What I wanna do here is make sure that in the goodness we actually compute in the probability of this word among all of the remaining candidates. Which should just be a matter of saying up here, p word is gonna be count as f64 divided by remaining count as f64 and p word times that. This is where we should probably have the sigmoid again. I just wanna see what happens if I run with this. So this is wait, oops. It's not what I meant to do. All right, I see some threes. What was the previous one we had was once and that's already in from tears. And then this is gonna be into use wait. I don't expect this to generally be faster, but I do expect it to do a little bit better in that it's gonna, say it gets something too now in that it should be more often trying a word that is more likely to be the answer. Again, it's gonna give that a little bit of an additional boost in addition to just it giving you more expected information. So basically it tries to go for the wind more often. You see this, like sometimes it goes over. In fact, we should see this for here use wait. So for five to four, four to five, four to five, five to four, five to three, four to three, five to four, four to three, three to two. But I wonder if we look on the left here, do we get any sevens? We don't on the right, we do get a seven. It's the word that this one hasn't tried yet, but I think generally what this is gonna do is it's gonna more often try a word just cause it seems more likely rather than go for the one that gives it the most information, which is likely to narrow down the list of candidates further. So it's gonna be able to push to lower values, but it's also going to end up going over sometimes cause it doesn't get as much information as the other one does. And we can try to be a little bit more robust about this, which is something like scores is zero, games is zero. And then here we're gonna do something like games plus equals one, score plus equals S. And in fact, we're always gonna do games plus equals one, and then this is gonna be counted as a score of plus equals, I don't know, 11. It doesn't really make, I guess, all right, fine. We won't count the games where it doesn't complete because we should be printing those out separately. And then at the end, what we'll do is print out average score as 0.2F of scores F64 divided by games as F64. And now I can do max 10, right, F is for other languages than Rust, so average score is 4.3 across the first 10. And for once, the average is also 4.3. All right, let's run with a few more then. And in fact, because we don't care about the implementation, wait, max 30, we don't actually care about the concurrent performance here, so we might as well just run them next to each other. Maybe 30 was low, maybe I should have done more. But all right, so that did four, let's do maybe 64 of these, and that did 3.67. So it seems to be better for some quantity of better. Why so many repetitions of word in the output? I don't think there are repetitions of the words. Are there? I don't see repetitions. So without this waiting, the average score is 4.05 and with this is 3.75. So this is gonna be that. Way go for the win more often. All right, and then let's do one last one maybe, which is gonna be from wait to prune. This one I actually expect to help a lot. What I wanna do here is not reconsider patterns that we've already eliminated, prune. Prune, prune, prune, prune, prune, prune. Prune, prune, and prune. So what we're gonna do now is we're gonna say the prune is also gonna have patterns, which is gonna be a veck of, do I want it to be a veck of patterns? Yeah, let's have it be a veck of correctness five. And patterns initially is gonna be a veck new. And down here, if patterns is empty, actually if the history is empty, then self.patterns equals veck new. Otherwise, we're gonna assert that self.patterns. No, it's not gonna be veck new, it's gonna be self.patterns is correctness patterns.collect, which arguably could be a once because that's always gonna be the same set of patterns. So let's do with cow static of that because why should we recompute those over and over and over again? Correctness five. So this is gonna be cow borrowed of patterns get or in it. And that's really just correctness patterns collect. Great. So in this case, it's gonna start out as cow borrowed of patterns.get.unwrap. We can unwrap because we know that it was constructed up here in new. Otherwise, just as a sanity check, there should obviously be patterns left if we are still guessing. But what we can do is for pattern in, in fact, we can do even better here. We can say self.patterns.retain pattern. This should be to mute. There's another one where I don't wanna clone the whole patterns thing every time. So let's do matches is does pattern match. It's gonna be a closure that takes a pattern and does this stuff. Otherwise return true. I guess check pattern is fine. Yeah. And then what we'll do is I guess we'll match. We'll say if matches self.patterns cow owned. Self.patterns cow owned. So this is the same trick, right? Where we don't want to ever copy the full vector and then trim it down. We want to start it out with the initial, we'll sort of seeding it with the initial set. So if it's already owned, then we're gonna do self.patterns.retain check pattern. Otherwise, we're gonna do self.patterns is cow owned of self.patterns.iter filter check pattern collect. Pattern here should be a correctness five, which it won't be because, because it requires, it expects a reference. That's fine. We can take a reference here. We can take a reference here. This. Itter.copied.filter. Cannot, oh patterns to mute, which we know it won't do anything because it's already owned, right? So now we're using wait as the comparison point. And I want to try that against prune. Whoa. I think that made a difference, wouldn't you say? All right, so let's tee this into wait.text. And then we're over here, we're gonna do prune, tee prune.text. Just to see that they actually got the same set of results. Now we're talking. And then I want to diff wait and what did I just make prune? It's disturbing that they're not the same. That's a little disturbing to me. Like why are they different? Like it does slightly worse, but it unstable sort. But do I even do a sort here? Like I don't, I'm not sorting. Like there's nothing non-deterministic here. There's no hash. Like there's no, there's no hash here either. This just walks the games in order. I mean, the dictionary is a hash set, but that's just used to check whether an entry shows up. So why is this not deterministic? That's extremely disturbing. Souls, algorithms, weights. I don't understand, there's no difference. I don't understand. What, why is this execution not totally deterministic? Retain should be deterministic. Hopefully floating point operations are deterministic. I have no idea. That's so weird. But okay. Well maybe patterns isn't. Why wouldn't it be though? That just iterates over these in order that allows guesses. I'm gonna just ignore this problem. Ha, ha, ha, he says. And I'm just gonna compare these. I have no warmup, because I don't believe in warmups. Weights, 32, and prune, 32. But there's no hashing of any note here. The only hashing is in the dictionary and the dictionary is just used to check whether a word is legal or not. It returns a boolean, it should be deterministic. Different number of remaining, they all start with the same word and they prune using the same function. Wow, well I think that pruning helped. It's almost 60 times faster. Yeah right, source algorithms prune. I like that one. Add mechanism for pruning patterns. This brings a, that's a very, very large speed up. How do you handle candidates with the same score? If they have the same score, then the first candidate should be chosen. And the candidates, we walk remaining in order remaining is a vector read from the dictionary. So like, there's no unstable sorting except in pre-calc, which we're not using. I mean, I am discarding patterns as I loop through but I'm only discarding patterns that are empty, you know? There's no iteration on hashtags. The hashtag is only used to look up whether a word is legal. Well, I mean, it also doesn't fail on any games. Nope, prune. So like, also to be clear, this is why I think it makes sense to optimize a single thread at one first because we might not have realized that we could make this modification until if we're in the concurrent setting. Okay, all right, I want to do one more. All right, so we're going to go from prune to... There's like the whole sigmoid here. Ooh, actually, I want to see what happens if I switch from tears to crate now. Can it actually run with crate? So previous one was like 3.9 something. It's slower when it starts with crate. Aren't you calculating probability based on remaining? Yeah, but I'm only pruning patterns that have nothing remaining in them. So they shouldn't affect the overall count anyway. Starting with crate did not change the score is interesting. But it is slower? Why? I wonder. Well, I mean, there are all sorts of reasons why that might be. Also, we can't be pruning any patterns that has non-zero matches or seems unlikely that we are because otherwise we would be eliminating categories that still had words in them, which should mean that we just fail to resolve at times. It might be possible for us to check this by doing main. If we just have, we could have this just iterate over the entire dictionary rather than just the answers. And we should make sure that we solve all of them. Probability is floating point based on remaining count, which is now different. The remaining count shouldn't be different. We're not pruning self.remaining. We're pruning the patterns that we consider. And we only prune a pattern once it has no words in it, at which point the count there is zero. So it doesn't affect the count at all. So it shouldn't make a difference. Please, I don't see how it does. So what I wanted to do was copy prune to cutoff. And this one I'm not entirely sure of, but I have this intuition that like, in terms of choosing what to guess, I wonder if there's a way for us to say, we're only gonna compute the probabilities for the most probable, the like, you know, N over two most probable words we have left rather than computing the probability for all of them and then picking. And the idea being that a word that's super unlikely, we don't even wanna check what the expected information is for it because it's unlikely that it's gonna be high enough to make up for how much more probable a given word is. So here I am gonna introduce on stability. Let's see, which is gonna be cutoff. And this is gonna be the last one and then I have to go eat. Oh yeah, there are also lots of tricks we could play with correctness, for example, by using bit fields and stuff, but that seemed excessive. All right, so here's the idea, the basic idea is gonna be, let mute words, it's gonna be that. And we're gonna do words sort unstable by key, were count, reverse of count. So we're gonna sort it so that the most frequently used words are first. And retain retains that order, right? Because it just removes things in place, it doesn't shift anything around. And then my thinking here is that I want, instead of this being that, I want it to be iter take, honestly, maybe just the first 50. You know, like it seems unlikely that a word that's, well, maybe 50 is bad, let's do end self.remaining.len and let's do end over two. And I wanna do hyperfine of prune versus cutoff. Ooh, that failed on line 113. All right, fine. So we're gonna do stop and instead of doing this, we have to stop after we've gotten to at least that many candidates. That's me being silly, which is here i plus equals one. If i is greater than stop or equal, it's fine, then break. All right, so that's half. And what I want to do now is, did I already compute for prune? Maybe I didn't 128 prune.text. I guess the average score is all I really need. Sure, let's just run that, I guess. And then I wanna run implementation cutoff. Just gonna let them both run. And I wanna see if they get the same score, 395, 394. Okay, so they're basically the same. And I wanna see if I make stop be, oh, why did I set that to the same length? Divide by two. That looks faster, but I'm not sure I believe it. Yeah, so see how much that saves. Another question is, is it, and the score is the same. And in fact, I bet you we can do like way better than this. I bet we could hard code this to like 10 and have it not even make a difference. Oh, interesting. So reducing to 10 didn't really make much of a difference compared to just having. You know, like 149, 148, they're basically about the same. All right, what if I do like 64 or something? They're fast enough anyway. Yeah, so that is 154. And if I instead say, cut it off at 10. Okay, so half gets us like most of the way there. Yeah, so one example, these came up in chat, like it's possible that the length of remaining is generally less than whatever number I said, like less than 10. So it doesn't really make a difference. You see the score it achieved is the same, 394, which I think half then is a good estimate to start with. But I actually want this to be, this should probably be something like max 10 or something where if we have very few candidates left, we probably want to consider all of them. Cargo car, I don't want it to include that. Yeah, so it's still faster. It's not quite as much faster, but I worry about it. When it gets down to like, there are only 10 more options. I wanted to carefully consider all 10 options rather than just going with the five most likely. I don't want it to do this halving all the way down. And maybe a hundred is too much, but I don't really know where to start here. When you whenever you get below 20, consider all of them or something. Nice. Don't search unlikely candidates. This brings a speed up, a nice. So I think we're now at like several, like probably two or maybe even three orders of magnitude faster than the solution we started with. We still haven't done the sigmoid, but it's interesting because the sigmoid should only improve the quality of our guesses, not the speed. I wonder, is there like a cheap way to approximate a sigmoid? Someone mentioned 10H, which like maybe is, like if I do, if I just do, you know, 10H, is that 394? Okay, so the 10H did nothing. That's fine. Try remaining length divided by three. Well, in that case, we can just check. Yeah, I guess we can check the same thing. Divided by three is faster. That's fine, I'll do that. There's a trade-off here, right? The fewer candidates you consider, the more likely you are to discard a candidate that would actually have been better than the ones you chose. 32, 3,200 X speed up, sounds about right. Okay, I think that's where we're gonna stop today. We're at, you know, a bit over six hours, which seems like enough coding time. I'm going to push this somewhere. I'll push it to GitHub and then I'll tweet out the link, I suppose. And like, by all means, try to make this faster. I don't think this is something that I'm gonna like maintain over the long period of time. This is more, I thought it was an interesting exercise in, you know, implementing an algorithm and it's like writing performance rust that looks pretty fun. All right, with that, I think we're done. Are there any sort of last-minute questions before we end? Anything you're curious about or would like to see? All right, in that case, thank you all for watching. I hope you learned something. I hope you found it interesting and otherwise I will see you in another stream. Bye, everyone.