Hello everyone,
I have a question regarding iterators. I know that iterators are lazy, but are they also composable the way that Elixir Streams are?
For example, will the following code lead to two iterators
At the point where your code is being optimized, there are not “two iterators” or one iterator. There are just functions (that we know as implementations of Iterator::next()) and data that they operate on. Unlike garbage-collected languages where there is often a concept of “object identity” that has to be preserved through run time, in Rust, there is no additional cost to “another object” beyond the data it carries and the code in its functions.
So, yes, it is very likely that the former code will be transformed to be equivalent to the latter code. But no, that transformation does not work on a principle that is anything like 'reducing the number of iterators'.
The standard library does sometimes have special cases for things like specific iterator combinations; for example, some_vec.into_iter().map(|x| x + 1).collect::<Vec<i32>>() will reuse the memory of the input vector rather than unnecessarily discarding it and allocating new memory. But most of the performance that the compiler gets you does not come from manipulating high-level entities like that, but from breaking everything down to individual operations and rewriting those into equivalent but more efficient forms.
And note that you can always use something like Matt Godbolt's Compiler Explorer to take a look at what codegen actually does.
For example, Compiler Explorer has your two example iterators - but t2 has compiled down into .set example::t2, example::t1 - saying that the compiler has determined that they're identical, and it need not output two sets of code.
Incidentally, if you compile this particular example using the default optimization settings and examine the output, you will see that Rust compiles each map into a closure.
The assembly code for 2 maps:
.LBB113_9:
mov rdi, qword ptr [rsp + 88]
call _Unwind_Resume@PLT
ud2
example::f_2maps::{{closure}}:
push rax
mov qword ptr [rsp], rsi
mov rax, rdi
mov rdi, qword ptr [rsp]
mov esi, 2
call <&i32 as core::ops::arith::Mul<i32>>::mul
pop rcx
ret
example::f_2maps::{{closure}}:
push rax
inc esi
mov dword ptr [rsp + 4], esi
seto al
test al, 1
jne .LBB115_2
mov eax, dword ptr [rsp + 4]
pop rcx
ret
Keep in mind that the codegen is completely different with -C opt-level=3. LLVM is smart enough to recognize that both functions reduce to the same exact machine code, so one copy is eliminated entirely. Compiler explorer
Change f_2maps just slightly, like making the addition + 2, and suddenly it will be output as its own function.
In general, if you're looking at Rust codegen, you need (at least) -O in your rustc command line - by default, there's very little optimization done, and you need to ask for optimizations if you want to see what "real" output looks like.
This is also why you're recommended to only deploy the result of a release build - the difference is significant.