Tristan Brindle

I I have this idea for a talk that I I might flesh out at some point. It's like, what is the most powerful algorithm in the STL? And just to give away the punchline, so uh my my conjecture is that it's standard find if. So I think, yeah.

Bryce

I mean he he's not he's not wrong because a find if is essentially just a reduction, and redu and the correct the correct answer is a reduction.

Conor

I was like, there's no way Bryce's answer is instead reduced. Welcome to ADSP the podcast, episode 126, recorded on April 5th, 2023. My name is Connor. Today with my co-host Bryce, we interviewed Tristan Brindle. This is part two of a three-part interview, and today we talk about his library's flux and flow.

Tristan Brindle

So, okay, so at this point, it's worth mentioning Barry Rizvin Ridsvin has this uh amazing talk that you've you've probably seen. I think he gave it a keynote in C Paris, where he compares the iteration schemes uh of uh various languages, right? Yeah, Rust D, C. He also mentions like C sharp and Java and Python and stuff. Uh and it's an amazing talk. So if anybody's interested in this stuff, like really do go and watch this. I can't I can't recommend it highly enough. Um but I I so I was kind of interested in like how before Barry's talk, but uh how how do other languages do this? Um and I started writing a new library that was uh implementing Rust style iterators in C. So for the maybe people who know C but don't know Rust, the Rust iteration scheme is really quite different to how it's done in C. So what they do is they have this object called an iterator, and the iterator has a function called next. And what next does is it returns you an optional with the next value in the sequence. Right? So you call next the first time, it returns you the first object, an optional containing the first object in the sequence. Call next again, it returns you the second object in the sequence, and so on until eventually the sequence is exhausted, and at that point it returns you uh an empty optional, which tells you that uh the iteration is complete. So it's an alternative way of doing iteration. Um there are pros and cons to it. The pro being it's very, very much simpler than the C model. Right? Um the downside is that uh it's quite more limited, it's not as powerful as what you can do with C, where you can go all the way to random access. Um writing this library, it's called uh that's quite a big limitation.

Bryce

Um especially once you start thinking about um wanting to have uh implementations of your algorithms that consume iterables that that are optimized for parallelism and for vectorization. If you don't have a way to do random access, that's uh that's kind of a problem.

Tristan Brindle

Yeah, um well this was so I you know I'm absolutely by no means a Rust expert, and I'm sure the Rust people will say, no, no, no, you do it this way instead, right? So I don't I don't want to step on anybody's gonna be able to do that.

Bryce

There must be some something that we're missing here, is my guess.

Tristan Brindle

Yeah, yeah. Um as I say, not my not my not my sort of field of expertise for how all this parallelism stuff is is done in Rust. Um but for me, so I I did this, I wrote this this library, uh it's actually uh on on GitHub, uh it's called Flow that uh kind of does this Rust iteration scheme in C. Uh just kind of for my own like just for fun, really, just to kind of see how this was done. Uh but I found this this limitation of not being able to go all the way to random access. Uh you know, this this bothered me. So I was like, okay, is there a way we can extend the Rust iteration scheme or Rust-like iteration scheme? It's actually used in a couple of other languages as well. Uh we can extend this to do something like Rust, uh something like random access. And it turns out that the way you actually implement these uh iterators in the Rust, or the way I implemented these, is you have like uh your iterator uh object and it holds a uh a cursor to the underlying uh sequence vector, let's say. So your your cursor could be just uh an integer index. And every time someone calls next, you bump the index and you return then the next element. So I was like, okay, well, what if we provided an API to expose these cursors to the user so that you could then move the cursor an arbitrary uh you know amount as you need to for random access. Um and then I I did some experiments with this, and I was like, you know, this this works. Uh but actually, if you do this thing, if you expose these cursors to the user and allow them to manipulate cursors, you really don't need this.next function at all. You can do it all with cursors. Um so that was my starting point for this new library that um I've been working on called flux. So uh I was like, okay, scrap everything I've done. I'm just gonna start again from the beginning, and we're gonna have this API based on cursors. So what is a cursor? So this is this is getting into flux now, this is the the new thing, right? What is a cursor? A cursor is no more and no less than a position in a sequence, right? So the most general kind of cursor is just an integer index into an array, right? Cursors are a generalization of array indices.

Bryce

It's it's it's it's so funny that that you say that because I've I've been using the same term for the same for the same thing in this like multidimensional indices library that I've been playing around with. I must have heard you say it before or something. Because I think I've I I I recall hearing somebody use this terminology. I just don't remember who.

Tristan Brindle

Maybe we were having like a chat at a conference or something, but I can't remember talking about it before. But um, yeah, so a curse is like a like a generalization uh of an index in the same way that uh an STL iterator is a generalization of an array pointer. But really the key thing is that you can't independently manipulate cursors. So if you think about STL iterators, STL iterators are smart, right? STL iterators know how to increment themselves, they know how to dereference themselves. Cursors don't. So a cursor just represents a position in a sequence and nothing else. And if you want to increment the cursor, you have to say to the sequence, please will you increment this for me. Uh if you want to access the element, if you want to like dereferencing in for iterators, you have to say to the sequence, please will you give me the element at this position. And this doesn't seem like a very big change compared to iterators, but it turns out this has pretty far reaching, and that's that's the core of flux, right? That's that's the difference between flux and ranges. Uh but it turns out this has pretty far-reaching consequences. Uh, number one is that it pretty dramatically uh improves safety, which is kind of a big deal at the moment, right? Like, because there are so many ways that you can accidentally stumble into undefined behavior when you're using iterators. Dereferencing a past-the-end iterator is undefined behavior, incrementing a past-the-end iterator is undefined behavior. Uh bidirectional sequence, decrementing a uh begin iterator is undefined behavior, right? Um if you're holding on to an iterator and you change the, if it's a vector and you change the underlying vector, you you're gonna have uh you can have a dangling iterator. Uh well, you can have dangling iterators because you're holding on to this iterator and the vector dies, but you've still got the iterator. You know, there are many ways in which you can stumble into undefined behavior. This is much more difficult. If a cursor just represents a position, this you it's much more difficult to accidentally stumble into undefined behavior land, right? Because to give you an example, if the cursor, let's just think of it as a as an integer index. If you're holding on to an integer index and your vector goes out of scope, like there's no danger there, because you can't do anything with the index without the original vector to like try and access it.

Conor

Right.

Tristan Brindle

Right? So just immediately uh dangling iterators like return from functions, let's say, are no longer a problem, like by design. Um, we can also do, because we've always got the sequence object available, we can do uh cheap bounds checking. So this is something that is quite expensive to do with iterators because it means iterators need to like have a reference to the parent array or to the to the parent range, whereas with influx, we do bounds checking universally and unless you specifically request uh request uh unchecked access. We will check and make sure that the uh that the your cursor is in bounds before we do the dereference, and otherwise um well, depends on the configuration what happens, either we crash or we throw an exception. But uh so we've got um bounds checked access. So this is like I I think I mean for me, I think that's really important. Um it's something that languages like Swift and and Rust and many languages do everywhere. Um so we've got bounds checking, we've got uh um no dangling. There are various other things that turn out to be uh better, like um in C we have to worry about iterators versus const iterators, uh which is uh you know just uh kind of a burden on the programmer to uh particularly if you're implementing ranges, you have to worry about um bad iterators and const iterators, and they're not quite the same thing, but you have to be able to convert from one to the other, and it's just a pain. Whereas with cursors, because a cursor is, you know, you can think of it as just being like an index, you can have the same index whether you're you have a const vector or a non-cons vector, it doesn't matter. Right? We we make the decision about const or non-const access based on the sequence that you pass in. Um there are uh a few other benefits as well when you get to writing adapters. So um when you are actually implementing range adapters, things like map, things like transform, the way that these uh the iterators for these things work is that you have to wrap the iterator of the underlying, you have to wrap the underlying iterator, if you see what I mean. So, like uh in general, anyway, uh if you think about the filter, um, the iterator for filter view, it needs to contain the iterator for the underlying range, but also uh have a pointer back to the filter view object so that it can access the predicate. Um, and so when you build up one of these ranges pipelines, your iterators, the size of your iterators grows. Um, like for each added stage of your pipeline, in general, your iterator object gets bigger and bigger. And since C algorithms, we assume that iterators really cheap to copy, you know, it's not a big deal, but it it it you know it's not ideal. Whereas in the the flux model with cursors, we there is less need to wrap these uh cursor objects and have them get bigger and bigger. Um, so that I think is uh is good. And also the what's also quite nice about this is we can provide compatibility with ranges code because if we need to form an iterator, we can form uh a C20 compatible iterator by having effectively like a pointer to a sequence and a cursor as a pair, and then provide all of the you know the whatever it is, like 20 odd functions you've got to you've got to write for an iterator. So we can be compatible. In some sense, this is this is even more fundamental than than iterators because we can we can provide compatibility uh with existing with existing iterators. So um I'm I'm very excited about this. I I think it's uh I think it's pretty good. Um this is I've never talked about this before anywhere. I've like tease it on Twitter a couple of times with um like, hey, look at this, you can do this with my code, doesn't it look awesome? But I've never actually talked to talked about this uh anywhere. So um yeah.

Conor

Announcing to the world. Well, like first part of this episode will be like uh, you know, nano nano ranges, implementing nano ranges, and how you can too, and then the the second part will be uh flux, and then in parentheses and flow, but mostly about flux.

Tristan Brindle

Uh yeah, that's the other thing to mention, and I didn't know this when I didn't know this when I started out, but we uh we we were talking about Swift before. It turns out that the Swift collection API is pretty much the same as what I'm doing in Flux. So I didn't know this when I started, right? Because I I don't really know much about Swift at all. I only found this out later. But the so I I mentioned before Swift has like these two uh different protocols, but the collection protocol is this same abstraction of the idea of an index. They use different terminology, they use different function names, but the kind of the fundamental design is the same, which actually gives me quite a lot of confidence that I'm I'm I'm going in the right direction, if you know what I mean. Like because Swift is a safe language in all the same ways that Rust does a safe language, and if it works for them, well, you know, there's a good chance it'll work for us as well.

Conor

From what I've heard too, uh Dave Abrahams is you know, he's a pretty a pretty novice programmer that doesn't have too many years of experience, right? Uh yeah, we'll just email it. Um Wow, that's uh that's quite interesting. Well, so obviously we'll definitely link to this. And and I mean a a a couple I mean we never got back to the tweet of why you ended up here, but the second tweet Oh um Yeah. Actually, did I link it?

Bryce

But before before we move on, I just gotta say I think I think uh you're you're on the right track with this uh uh with this model. Um and I think that it it opens up a lot of things, um, especially related to loop optimization that um are challenging with the iterator model. And and I think um, especially like in in in like numeric in comp in computational science spaces, this cursor model is um is potentially a much better fit.

Tristan Brindle

Yeah, yeah. So that's actually one thing I I didn't mention is that we have um an extra customization point that is not in ranges, uh, but we but uh in Flux we have this customization point for doing internal iteration.

Bryce

Oh, that's interesting. That's that's similar to what I've been messing around with in uh yeah.

Conor

This episode's not out, but the the one unreleased episode is basically Bright. I mean, all the listeners have heard it now if they're listening to this one, because it was like two episodes ago now, but it's Bryce trying to convince me that like filter is broken for the purposes of parallelization and then coming up with like a new filter, and then I'm like, my point is Bryce, that's not a filter, that's a transform. But anyway.

Bryce

Did we talk in that episode about the my my spaces idea? Which is which is solved, it tackles this problem of internal iteration.

Conor

You did end up screen sharing because I was asking for a more motivating example. Yeah, because your example I said was unmotivating, and then we got into an argument. You were like, that's your problem, not my problem. And I was like, it seems like a you problem, and then you showed me a motivating example, and then I said, Oh, I can't.

Bryce

So, Tristan, tell me how your model for internal iteration works.

Tristan Brindle

Okay, so we have um a function that uh it's called I called it for each while, which I don't necessarily think that's a great name. Um But the the idea is that uh a sequence has this fundamental um uh uh customization point that's called for each while, and you pass it a predicate, and you say uh iterate all of iterate over the sequence in the way the best way that you know how, iterate over every element uh until the predicate returns false. And at that point it returns us the cursor uh for the the element that for which the predicate returned false. And it turns out that for things like filter uh and map and things like that, and really an enormous number of these adapters, you can do internal iteration in a way that is more efficient than just stepping through and doing increment dereference, increment dereference, uh, you know, etc. etc. Um, and this makes a real difference when it comes to, although the algorithmic complexity is the same, it makes a real difference in terms of the generated code. Like the compiler can see through this. Yeah, this was literally our discussion last episode.

Conor

We're two episodes now for the listener.

Tristan Brindle

Uh so for the benefit of the part one of the of the of the discussion on views filter, and then you left me hanging, right? You were like, we're just gonna go and share the screen, and that's the end of the episode. So I I have not heard the conclusion of that. But yeah. So um, in particular, um one of the the I don't want to call it a problem, but one of the things that you have to bear in mind with C ranges, and this is true of flux as well, uh, and Barry talks about this in this this fantastic talk, is when you have transform and then filter, then you have this issue of double dereference because you have to uh when you're filtering, you have to see whether the element uh matches the filter. You do that in the the increment operation, and so that requires dereferencing the underlying iterator, and then when the user comes to dereference your filter iterator, you have to uh like again dereference the underlying iterator. So you get this uh double dereference happening that doesn't occur in the Rust model, for example, but there are other uh equivalent problems with the Rust model. But um it turns out when you do internal iteration, you don't need to do this double dereference, right? Um so right away, you're you know, that this is a tremendous benefit. And there are a huge number of algorithms that can be written in terms of uh internal iteration. Um I I have this idea for a talk that I I might flesh out at some point. It's like what is the most powerful algorithm in the STL? And just to give away the punchline, so uh my my conjecture is that it's standard find if. Really? So I think, yeah.

Bryce

I mean, he he's not he's not wrong because a find if is essentially just a reduction, and the correct the correct answer is a reduction.

Conor

I was like, there's no way Bryce's answer isn't std reduce or or std accumulator one. Okay, so so um Ben Dean has this, uh has a great talk where he std accumulate an algorithmic empire.

Tristan Brindle

Yes, yes. And he talks about how you know all the things you can write in terms of um standard accumulates, which is like our funny way of spelling fold. Um but it turns out that you can write a full you can write um accumulate in terms of finding. Well, yes, I I guess they're equivalent, it depends if you're happy to have a stateful thing that you put.

Conor

Wait, wait, wait. You're saying you can implement accumulate in terms of find if or find if in terms of accumulate? You can I think you I think you can do either. Well, what we first what was what we did.

Tristan Brindle

But you have to have some way of terminate you have to have some way of terminating the iteration early. That's the key, right? So that's what find if does.

Bryce

Yeah, well, well, okay. So we we we could have a whole discussion here now about uh You're coming we're you're coming back again, Tristan, and we're gonna have a find if versus the advantages and disadvantages of uh of uh having a a find if that is required to terminate early.

Conor

So but what what what was it that you initially said though? What would do you said that you can implement accumulate in terms of find if? Because that one seems easier than the other way around, if the other way around is even possible.

Tristan Brindle

That's so that is what uh I do in in Flux because I'm not worried about parallelism, which um you know I this is all just uh Connor.

Bryce

Are you suggesting that we can implement find if in terms of accumulate? Yes. I think you just said that backwards.

Conor

No. Why? And why is the what what is the number one reason that it's trickier? And you Should know this because I know you've tried this and you literally DM me on Twitter. How do you do this thing to get around this? I don't know. One returns an iterator and one returns a value. And trying to implement uh algorithms that return iterators.

Bryce

I don't care about I don't I I'm not interested in the per yeah, I'm not interested in the particular in the particulars of the interface. My my my point is that we're we're we're we're talking about these algorithms in the abstract.

Conor

I think when we were spelling out std colon colon findif and std. We were being pretty specific, Bryce. Um but like we will we'll we'll table this because this definitely could be like a whole 60-minute discussion. But so what you initially said was that implementing accumulate in terms of find if is basically what you do in flux, and it is your opinion that because find if then therefore becomes the in the hierarchy of what is built on top of what, because accumulate is a child of find if find if is the most basically uh important or fundamental algorithm that everything else is built on top of.

Tristan Brindle

That is my conjecture until Bryce convinces me otherwise. Yes. So just about every algorithm, just about well, a lot of algorithms that operate on a single that the the boil down to some sort of reduce operation, or things like uh all or any and none and all of those um you can you can write in terms of find if. So if you can do a find if more efficiently, um, for example, by using internal iteration, um then that's uh that that has tremendous uh benefits in terms of it.

Bryce

Well, I mean the the in in serial you always will want to have a specialized version of uh of a find if because you want to do it in early termination. But in in parallel, the way that you implement a reduction is um uh is to just do it with a or the in parallel the way that you implement a find if is just to do uh uh a reduction at least on something like a GPU.

Tristan Brindle

Yeah, for sure. I I I mean it's it's not my not my field of expertise, but the the yeah. One day perhaps we will look I'll I'll look at a parallel version of flux, but uh many years from now probably. I need to get the serial version working first.

Conor

So what's interesting is that um you probably I'm not sure if you've even seen uh I think you have seen the talk, but at the end of my intuition, algorithm intuition talk, I present this table, and I present find if and accumulate as like two of the most important, but the stat the distinguishing difference is that accumulate is a reduction that carries state with it, and a find if is a reduction that does not carry state with it. And so then what do you do for that then?

Tristan Brindle

Do you have like a uh So okay, so you're absolutely right, and in this standard model, you're not actually allowed to pass uh the I can't remember the exact term. It's a quote uh anyway, you're not allowed to pass a stateful thing to find if. So we have to slightly for in in flux, we slightly uh um weaken this requirement to allow it.

Bryce

And you're giving me a hard.

Conor

He's doing it in flux, though. We were talking about the standard library.

Bryce

I I I I understand, but we were having that was a different conversation than the conversation we were having now.

Tristan Brindle

Yeah, so so the basically all of the functional objects that you ever pass to almost all of the functional objects you pass to standard library uh algorithms have this requirement that they give you the same answer given the same input. Which is not quite the same thing as being stateless, like you're you're allowed to have side effects, right? But they can't give you the however many times you call them, they have to give you the same answer. Uh if we slightly weaken that, or not slightly, if we just if we weaken that, um then you're allowed to pass a stateful thing into this. That's why I call it for each while and not um and not find if, because it's it's more akin to for each than um than the strict stricter requirements that are on find if.

Conor

Oh, so this weakened find if is what is what is for each while.

Tristan Brindle

Yeah, exactly. And I'm I'm you know, if someone can come up with a better name than than for each while, then uh then uh you know, let me know. Naming is not something I'm good at.

Conor

You know what's great at naming? Chat GPT. Uh oh, oh well.

Bryce

I've drunk the Kool-Aid in the past week. It's gonna be a whole separate episode.

Conor

Yeah, we'll we'll we'll table that as well.

Bryce

But uh yeah, for each while actually, well, there's uh here, I'm gonna I'm gonna link a relevant a relevant tweet. Um well but before I go there, before before we go there, let me let me show uh Tristan the uh uh this is my multidimensional uh iteration thing, um which is that you have this notion of a space, which is essentially just a collection of ranges, and then um you know you have these like space aware algorithms that um uh recursively unpack them. Um and one of the key insights that I had was that if each inner loop needs to have the value of the induction variable at the outer loop. So, you know, if you have if you have a an a nested loop of you know k where you iterate over k and uh where you where you like the first the first for loop is a for loop over some something and it gives you you know an element k and the second the innermost for loop is uh an element j when you uh um when you call this space thing when you extract the range from the space thing in the inner loop, you have to give it the the value of whatever element you're at in the outer loop. Um and that lets you incrementally build up your position as you go. Yeah. I did a very, very poor job of explaining that, but they're able to look at codes.

Tristan Brindle

I'm getting like eerie, um like uh just remembering uh university lectures about tensors and stuff, and um yeah so uh horrible flashbacks to things I didn't really understand at the time.

Conor

Be sure to check the show notes either in your podcast app or at adsphepodcast.com for links to any of the things that we talked about in today's episode, as well as a link to a GitHub discussion where you can leave comments, questions, or thoughts that you have on this episode. Thanks for listening. We hope you enjoyed and have a great day.