Il ciclo su ranged è favorevole alle prestazioni?

Leggendo varie domande qui su Stack Overflow su iteratori e prestazioni C ++ **, ho iniziato a chiedermi se for(auto& elem : container) viene “espanso” dal compilatore nella migliore versione ansible? (Un po ‘come l’ auto , che il compilatore inserisce subito nel giusto tipo e quindi non è mai più lento e talvolta più veloce).

** Ad esempio, importa se scrivi

 for(iterator it = container.begin(), eit = container.end(); it != eit; ++it) 

o

 for(iterator it = container.begin(); it != container.end(); ++it) 

per contenitori non invalidanti?

Lo standard è tuo amico, vedi [stmt.ranged] / 1

Per un intervallo basato su dichiarazione del modulo

 for ( for-range-declaration : expression ) statement 

lascia che range-init sia equivalente all’espressione circondata da parentesi

 ( expression ) 

e per un range-based per la dichiarazione del modulo

 for ( for-range-declaration : braced-init-list ) statement 

lascia che range-init sia equivalente alla parentesi-init-list. In ogni caso, una dichiarazione basata sull’intervallo è equivalente a

 { auto && __range = range-init; for ( auto __begin = begin-expr, __end = end-expr; __begin != __end; ++__begin ) { for-range-declaration = *__begin; statement } } 

Quindi sì, lo standard garantisce che venga raggiunta la migliore forma ansible.

E per un numero di contenitori, come il vector , è un comportamento indefinito modificare (inserirli / cancellarli) durante questa iterazione.

Range-for è il più veloce ansible dato che memorizza nella cache l’iteratore finale [ citazione fornita ] , usa pre-incremento e solo dereferenzia l’iteratore una volta.

quindi se tendi a scrivere:

 for(iterator i = cont.begin(); i != cont.end(); i++) { /**/ } 

Quindi, sì, range-for potrebbe essere leggermente più veloce, dal momento che è anche più facile scrivere non c’è motivo per non usarlo (quando appropriato).

NB Ho detto che è il più veloce ansible, non è comunque più veloce di quanto ansible . È ansible ottenere esattamente le stesse prestazioni se si scrivono attentamente i loop manuali.

Per curiosità ho deciso di guardare il codice assembly per entrambi gli approcci:

 int foo1(const std::vector& v) { int res = 0; for (auto x : v) res += x; return res; } int foo2(const std::vector& v) { int res = 0; for (std::vector::const_iterator it = v.begin(); it != v.end(); ++it) res += *it; return res; } 

E il codice assembly (con -O3 e gcc 4.6) è esattamente lo stesso per entrambi gli approcci (il codice per foo2 è omesso, poiché è esattamente lo stesso):

 080486d4  > const&)>: 80486d4: 8b 44 24 04 mov 0x4(%esp),%eax 80486d8: 8b 10 mov (%eax),%edx 80486da: 8b 48 04 mov 0x4(%eax),%ecx 80486dd: b8 00 00 00 00 mov $0x0,%eax 80486e2: 39 ca cmp %ecx,%edx 80486e4: 74 09 je 80486ef  > const&)+0x1b> 80486e6: 03 02 add (%edx),%eax 80486e8: 83 c2 04 add $0x4,%edx 80486eb: 39 d1 cmp %edx,%ecx 80486ed: 75 f7 jne 80486e6  > const&)+0x12> 80486ef: f3 c3 repz ret 

Quindi, sì, entrambi gli approcci sono gli stessi.

AGGIORNAMENTO : la stessa osservazione vale per altri contenitori (o tipi di elementi) come vector e map . In questi casi, è particolarmente importante utilizzare un riferimento nel ciclo a distanza. Altrimenti viene creato un temporaneo e viene visualizzato un sacco di codice extra (negli esempi precedenti non era necessario poiché il vector conteneva solo valori int ).

Per il caso di map lo snippet di codice C ++ utilizzato è:

 int foo1(const std::map& v) { int res = 0; for (const auto& x : v) { res += (x.first.size() + x.second.size()); } return res; } int foo2(const std::map& v) { int res = 0; for (auto it = v.begin(), end = v.end(); it != end; ++it) { res += (it->first.size() + it->second.size()); } return res; } 

E il codice assembly (per entrambi i casi) è:

 8048d70: 56 push %esi 8048d71: 53 push %ebx 8048d72: 31 db xor %ebx,%ebx 8048d74: 83 ec 14 sub $0x14,%esp 8048d77: 8b 74 24 20 mov 0x20(%esp),%esi 8048d7b: 8b 46 0c mov 0xc(%esi),%eax 8048d7e: 83 c6 04 add $0x4,%esi 8048d81: 39 f0 cmp %esi,%eax 8048d83: 74 1b je 8048da0 8048d85: 8d 76 00 lea 0x0(%esi),%esi 8048d88: 8b 50 10 mov 0x10(%eax),%edx 8048d8b: 03 5a f4 add -0xc(%edx),%ebx 8048d8e: 8b 50 14 mov 0x14(%eax),%edx 8048d91: 03 5a f4 add -0xc(%edx),%ebx 8048d94: 89 04 24 mov %eax,(%esp) 8048d97: e8 f4 fb ff ff call 8048990  8048d9c: 39 c6 cmp %eax,%esi 8048d9e: 75 e8 jne 8048d88 8048da0: 83 c4 14 add $0x14,%esp 8048da3: 89 d8 mov %ebx,%eax 8048da5: 5b pop %ebx 8048da6: 5e pop %esi 8048da7: c3 ret 

No. È uguale al vecchio ciclo per gli iteratori. Dopotutto, il range-based funziona con gli iteratori internamente. Il compilatore produce solo codice equivalente per entrambi.

È probabilmente più veloce, in rari casi. Poiché non è ansible denominare l’iteratore, un ottimizzatore può dimostrare più facilmente che il loop non può modificare l’iteratore. Ciò influisce, ad esempio, sull’ottimizzazione dello srotolamento del loop.