@@ -13,12 +13,29 @@ use pin_project_lite::pin_project;
1313
1414pin_project ! {
1515 /// Future for the [JoinIterExt::join] method.
16+ ///
17+ /// Uses an enum to optimize the empty iterator case by avoiding
18+ /// heap allocation and executor overhead when there are no futures to join.
1619 pub struct Join <F >
1720 where
1821 F : Future ,
1922 {
2023 #[ pin]
21- inner: JoinAll <F >,
24+ inner: JoinInner <F >,
25+ }
26+ }
27+
28+ pin_project ! {
29+ #[ project = JoinInnerProj ]
30+ enum JoinInner <F >
31+ where
32+ F : Future ,
33+ {
34+ Empty ,
35+ NonEmpty {
36+ #[ pin]
37+ inner: JoinAll <F >,
38+ } ,
2239 }
2340}
2441
3249 self : std:: pin:: Pin < & mut Self > ,
3350 cx : & mut std:: task:: Context < ' _ > ,
3451 ) -> std:: task:: Poll < Self :: Output > {
35- self . project ( ) . inner . poll ( cx)
52+ match self . project ( ) . inner . project ( ) {
53+ JoinInnerProj :: Empty => std:: task:: Poll :: Ready ( Vec :: new ( ) ) ,
54+ JoinInnerProj :: NonEmpty { inner } => inner. poll ( cx) ,
55+ }
3656 }
3757}
3858
@@ -42,18 +62,39 @@ where
4262{
4363 /// Returns a future that resolves to a vector of the outputs of the futures
4464 /// in the iterator.
65+ ///
66+ /// This method is optimized for empty iterators - when the iterator is empty,
67+ /// it avoids creating a `JoinAll` future and its associated heap allocation,
68+ /// returning a ready future directly instead.
4569 fn join ( self ) -> Join < F > ;
4670}
4771
4872pin_project ! {
4973 /// Future for the [TryJoinIterExt::try_join] method.
74+ ///
75+ /// Uses `Either` to optimize the empty iterator case by avoiding
76+ /// heap allocation and executor overhead when there are no futures to join.
5077 #[ must_use]
5178 pub struct TryJoin <F >
5279 where
5380 F : Future ,
5481 {
5582 #[ pin]
56- inner: JoinAll <F >,
83+ inner: TryJoinInner <F >,
84+ }
85+ }
86+
87+ pin_project ! {
88+ #[ project = TryJoinInnerProj ]
89+ enum TryJoinInner <F >
90+ where
91+ F : Future ,
92+ {
93+ Empty ,
94+ NonEmpty {
95+ #[ pin]
96+ inner: JoinAll <F >,
97+ } ,
5798 }
5899}
59100
@@ -67,11 +108,14 @@ where
67108 self : std:: pin:: Pin < & mut Self > ,
68109 cx : & mut std:: task:: Context < ' _ > ,
69110 ) -> std:: task:: Poll < Self :: Output > {
70- match self . project ( ) . inner . poll_unpin ( cx) {
71- std:: task:: Poll :: Ready ( res) => {
72- std:: task:: Poll :: Ready ( res. into_iter ( ) . collect :: < Result < Vec < _ > > > ( ) )
73- }
74- std:: task:: Poll :: Pending => std:: task:: Poll :: Pending ,
111+ match self . project ( ) . inner . project ( ) {
112+ TryJoinInnerProj :: Empty => std:: task:: Poll :: Ready ( Ok ( Vec :: new ( ) ) ) ,
113+ TryJoinInnerProj :: NonEmpty { mut inner } => match inner. poll_unpin ( cx) {
114+ std:: task:: Poll :: Ready ( res) => {
115+ std:: task:: Poll :: Ready ( res. into_iter ( ) . collect :: < Result < Vec < _ > > > ( ) )
116+ }
117+ std:: task:: Poll :: Pending => std:: task:: Poll :: Pending ,
118+ } ,
75119 }
76120 }
77121}
@@ -85,6 +129,10 @@ where
85129 ///
86130 /// Unlike `Futures::future::try_join_all`, this returns the Error that
87131 /// occurs first in the list of futures, not the first to fail in time.
132+ ///
133+ /// This method is optimized for empty iterators - when the iterator is empty,
134+ /// it avoids creating a `JoinAll` future and its associated heap allocation,
135+ /// returning a ready future directly instead.
88136 fn try_join ( self ) -> TryJoin < F > ;
89137}
90138
@@ -95,8 +143,19 @@ where
95143 It : Iterator < Item = IF > ,
96144{
97145 fn join ( self ) -> Join < F > {
98- Join {
99- inner : join_all ( self . map ( |f| f. into_future ( ) ) ) ,
146+ // Collect futures into a Vec first to enable empty check optimization.
147+ // This avoids heap allocation from JoinAll when the iterator is empty.
148+ let futures: Vec < F > = self . map ( |f| f. into_future ( ) ) . collect ( ) ;
149+ if futures. is_empty ( ) {
150+ Join {
151+ inner : JoinInner :: Empty ,
152+ }
153+ } else {
154+ Join {
155+ inner : JoinInner :: NonEmpty {
156+ inner : join_all ( futures) ,
157+ } ,
158+ }
100159 }
101160 }
102161}
@@ -108,20 +167,48 @@ where
108167 It : Iterator < Item = IF > ,
109168{
110169 fn try_join ( self ) -> TryJoin < F > {
111- TryJoin {
112- inner : join_all ( self . map ( |f| f. into_future ( ) ) ) ,
170+ // Collect futures into a Vec first to enable empty check optimization.
171+ // This avoids heap allocation from JoinAll when the iterator is empty.
172+ let futures: Vec < F > = self . map ( |f| f. into_future ( ) ) . collect ( ) ;
173+ if futures. is_empty ( ) {
174+ TryJoin {
175+ inner : TryJoinInner :: Empty ,
176+ }
177+ } else {
178+ TryJoin {
179+ inner : TryJoinInner :: NonEmpty {
180+ inner : join_all ( futures) ,
181+ } ,
182+ }
113183 }
114184 }
115185}
116186
117187pin_project ! {
118188 /// Future for the [TryFlatJoinIterExt::try_flat_join] method.
189+ ///
190+ /// Uses an enum to optimize the empty iterator case by avoiding
191+ /// heap allocation and executor overhead when there are no futures to join.
119192 pub struct TryFlatJoin <F >
120193 where
121194 F : Future ,
122195 {
123196 #[ pin]
124- inner: JoinAll <F >,
197+ inner: TryFlatJoinInner <F >,
198+ }
199+ }
200+
201+ pin_project ! {
202+ #[ project = TryFlatJoinInnerProj ]
203+ enum TryFlatJoinInner <F >
204+ where
205+ F : Future ,
206+ {
207+ Empty ,
208+ NonEmpty {
209+ #[ pin]
210+ inner: JoinAll <F >,
211+ } ,
125212 }
126213}
127214
@@ -134,16 +221,18 @@ where
134221 type Output = Result < Vec < U :: Item > > ;
135222
136223 fn poll ( self : Pin < & mut Self > , cx : & mut std:: task:: Context < ' _ > ) -> Poll < Self :: Output > {
137- match self . project ( ) . inner . poll_unpin ( cx) {
138- Poll :: Ready ( res) => {
139- let mut v = Vec :: new ( ) ;
140- for r in res {
141- v. extend ( r?) ;
224+ match self . project ( ) . inner . project ( ) {
225+ TryFlatJoinInnerProj :: Empty => Poll :: Ready ( Ok ( Vec :: new ( ) ) ) ,
226+ TryFlatJoinInnerProj :: NonEmpty { mut inner } => match inner. poll_unpin ( cx) {
227+ Poll :: Ready ( res) => {
228+ let mut v = Vec :: new ( ) ;
229+ for r in res {
230+ v. extend ( r?) ;
231+ }
232+ Poll :: Ready ( Ok ( v) )
142233 }
143-
144- Poll :: Ready ( Ok ( v) )
145- }
146- Poll :: Pending => Poll :: Pending ,
234+ Poll :: Pending => Poll :: Pending ,
235+ } ,
147236 }
148237 }
149238}
@@ -161,6 +250,10 @@ where
161250 ///
162251 /// Unlike `Futures::future::try_join_all`, this returns the Error that
163252 /// occurs first in the list of futures, not the first to fail in time.
253+ ///
254+ /// This method is optimized for empty iterators - when the iterator is empty,
255+ /// it avoids creating a `JoinAll` future and its associated heap allocation,
256+ /// returning a ready future directly instead.
164257 fn try_flat_join ( self ) -> TryFlatJoin < F > ;
165258}
166259
@@ -173,8 +266,19 @@ where
173266 U : Iterator ,
174267{
175268 fn try_flat_join ( self ) -> TryFlatJoin < F > {
176- TryFlatJoin {
177- inner : join_all ( self . map ( |f| f. into_future ( ) ) ) ,
269+ // Collect futures into a Vec first to enable empty check optimization.
270+ // This avoids heap allocation from JoinAll when the iterator is empty.
271+ let futures: Vec < F > = self . map ( |f| f. into_future ( ) ) . collect ( ) ;
272+ if futures. is_empty ( ) {
273+ TryFlatJoin {
274+ inner : TryFlatJoinInner :: Empty ,
275+ }
276+ } else {
277+ TryFlatJoin {
278+ inner : TryFlatJoinInner :: NonEmpty {
279+ inner : join_all ( futures) ,
280+ } ,
281+ }
178282 }
179283 }
180284}
0 commit comments