-
Notifications
You must be signed in to change notification settings - Fork 309
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Iterators generating (many) vectors #722
Comments
Related: #682 |
I saw that PR some weeks or months ago but forgot about it, I only remember the crate name and that I thought it seemed quite complex. My approach with |
@phimuemue After diving in a bit, I agree that the problem lies with "Iterator" itself and that a "lending iterator" would be a better choice but I share the opinion that it should not be a maintenance burden to have an (evolving) additional dependency and that it would be preferable to wait it reaches the "core" if it is even a possibility (I don't know). I like my workaround but I guess I should close this issue? |
@Philippe-Cholet @phimuemue |
@Easyoakland Thanks for the link, I did not know about Still, I am unsure that going this way is the best solution:
To sum up: The proposed solution improves performance, but does not offer maximum convenience (due to complicating the API), and it is unclear if the performance could be even better (my guess is "it could be better with reasonable amount of work, depending on your use case"). As such, it "falls in the middle" (neither most convenient nor most performant). If that's impossible (or hard) to offer both convenience and performance, then the API should imho not sacrifice at both ends. Thus, I am reluctant to merge this, and would resort to using other, more efficient, algorithms if needed. @Philippe-Cholet Do you happen to have a comparison for your |
@phimuemue I did not compare
The first two lines tell us how much Heap's algo is faster than getting the natural order. Both allocates each item to a vector, while the last two do not. Well I'm closing this. And I'll reopen only if I try and really fasten it. EDIT: It was a nice exercise and I learned that it has to be really fast if it's not the most convenient, no compromise. |
@Philippe-Cholet Wow, thanks a lot for you effort. I agree with all your conclusions. |
For permutations (and 4 others), we could discard my Name it |
Problem: slow because of repetitive heap-allocation
The 5 iterators below generate
Vec<...>
items.It can be slow, because of the repetitive allocations, even if the user does not need to permanently store each permutation.
I think that in most cases, the user uses each vector to produce something else and then each vector is discarded. In that case, because those iterators generate (up to) many many items (product of the lengths, factorial-related, powers of two), it creates many temporary vectors which can slow down code execution.
Proposed solution: map slices
An alternative iterator could own one single (internal/private) vector and only give a reference to it to a function implementing
FnMut(&[...]) -> R
. Those iterators would then generateR
items instead of vectors.Usage examples
it.combinations(k)
andit.combinations_map(k, ToOwned::to_owned)
would produce the same elements, but the first should be used!it.combinations(k).map(closure)
andit.combinations_map(k, closure)
could produce the same elements if it does not require a vector. For what I tested, it's then roughly 4 to 5 times faster to go through all combinations, unless the iterator has very few elements (then.map
should be used).In
it.permutations(5).filter_map(|perm: Vec<_>| some_option)
, ifsome_option
does not need perm to be an owned vector then it could becomeit.permutations_map(5, |perm: &[_]| some_option).flatten()
where there is no heap-allocation for each permutation.Implementation (commits here)
New module
vec_items
(behinduse_alloc
feature) with:pub trait VecItems<T> { type Output; fn new_item<I: Iterator<Item = T>>(&mut self, iter: I) -> Self::Output; }
pub struct CollectToVec;
implementingVecItems
withVec<T>
output and justiter.collect()
(so a new vector each time, as it's currently done).pub struct MapSlice<F, T> { func: F, vec: Vec<T> }
implementingVecItems
withR
output whereF: FnMut(&[T]) -> R
and the internal vector is cleared but not dropped after use (and its length will not change often if ever).E.g. for
combinations_map
:Itertools
trait:#[cfg(feature = "use_alloc")] fn combinations_map<R, F: FnMut(&[Self::Item]) -> R>(self, k: usize, f: F) -> CombinationsMap<Self, F> where ...
src/combinations.rs
:Combinations<I>
toCombinationsBase<I, F>
with the new fieldmanager: F
F: VecItems<I::Item>
and item typeF::Output
pub type Combinations<I> = CombinationsBase<I, CollectToVec>;
pub type CombinationsMap<I, F> = CombinationsBase<I, MapSlice<F, <I as Iterator>::Item>>;
fn combinations_map<I, F>(iter: I, k: usize, f: F) -> CombinationsMap<I, F>
combinations
.Very similar for the 4 others.
powerset[_map]
is a bit simpler because it just usescombinations[_map]
internally ; and it was simpler formulti_cartesian_product
than anticipated.Closing thoughts
Avoid cloning in
MapSlice
case? I think(?) it might be possible to only have references:MapSlice { func: F, vec: Vec<&T> }
. ThenF: FnMut(&[&T]) -> R
and no cloning. But I struggle with the lifetimes (or more probably some hard barrier). But is it worth it? Probably not if it's just integers, but otherwise?The documentation I did not wrote yet would suggest to use the non
_map
variants if the user needs owned vectors and not slices (or if there is very little elements expected).PS: I got the idea while replacing
(0..nb).permutations(nb)
(after usingflamegraph
andheaptrack
) by a_map
version of mine ofpermutohedron::Heap
to avoid any (internal) heap-allocation while keeping a nice iterator structure. And for8! == 40320
times, that was also 5 times faster.The text was updated successfully, but these errors were encountered: