2009-06-23 14 views
22

Ho un C++ frammento di seguito con un runtime for ciclo,Template -ing un ciclo 'for' in C++?

for(int i = 0; i < I; i++) 
    for (int j = 0; j < J; j++) 
    A(row(i,j), column(i,j)) = f(i,j); 

Il frammento viene chiamato ripetutamente. I limiti del ciclo 'I' e 'J' sono noti al momento della compilazione (I/J sono nell'ordine da 2 a 10). Vorrei srotolare i loop in qualche modo usando i template. Il collo di bottiglia principale sono le funzioni row() e column() e f(). Vorrei sostituirli con metaprogrammi equivalenti che vengono valutati in fase di compilazione, usando i trucchi row<i,j>::enum.

Quello che mi piacerebbe davvero è qualcosa che alla fine risolve il loop in una sequenza di affermazioni del tipo:

A(12,37) = 0.5; 
A(15,23) = 0.25; 
A(14,45) = 0.25; 

Ma mi piacerebbe farlo senza distruggendo la for - struttura for troppo. Qualcosa nello spirito di:

TEMPLATE_FOR<i,0,I> 
    TEMPLATE_FOR<j,0,J> 
    A(row<i,j>::value, column<i,j>::value) = f<i,j>::value 

Può aumentare :: lambda (o qualcos'altro) aiutarmi a creare questo?

risposta

4

Check out Template Metaprograms e bubble sort implementazioni.

+0

Il collegamento è interrotto (dominio * ubiety.uwaterloo. ca * non esiste). –

+0

@PeterMortensen, ok, l'ho risolto. –

12

Un buon compilatore dovrebbe srotolare per voi. Ad esempio, in gcc la compilazione con l'opzione -O2 attiva lo srotolamento del ciclo.

Se si prova a farlo manualmente, a meno che non si misuri attentamente le cose e si sappia davvero cosa si sta facendo, si rischia di finire con un codice più lento. Ad esempio, nel tuo caso con lo srotolamento manuale, è possibile impedire al compilatore di eseguire uno scambio di loop o l'ottimizzazione del stripmine (cercare --floop-interchange e -floop-strip-mine in the gcc docs)

+0

[OP] Non è l'overhead dei loop che sto cercando di ottimizzare, è più la valutazione di row(), column() e f(). Sono attualmente funzioni regolari e gratuite. Voglio garantire che vengano valutati in fase di compilazione anziché in fase di esecuzione. – RAC

+2

Se rendi queste routine "in linea", ciò potrebbe dare all'ottimizzatore informazioni aggiuntive sufficienti per farlo da solo. –

+2

Non c'è motivo per cui non dovrebbe essere in grado di incorporarli, a condizione che le definizioni complete delle funzioni siano visibili nel sito di chiamata. – jalf

4

Tu potrebbe usare Boost MPL.

Un esempio di srotolamento del loop è su questo mpl::for_each page.

for_each< range_c<int,0,10> >(value_printer()); 

Non sembra che sia tutto valutata al momento della compilazione, ma può essere un buon punto di partenza.

4

Direi che è una falsa buona idea.

In C++ questo: row<i,j>::value
significa che si avrà il maggior numero di differenti row<>() funzioni di quello che hai i * j. Non lo vuoi perché aumenta la dimensione del codice e fa un sacco di errori nella cache delle istruzioni.

Ho osservato questo quando stavo facendo le funzioni del modello per evitare un singolo controllo booleano.

Se è una funzione breve, è solo in linea.

+0

L'idea era di lasciare che il compilatore facesse i conti, non il processore . Per una matrice 10 x 10, si tratta di 200 funzioni valutate in fase di compilazione e sostituite da indirizzi nel file finale. Quello dovrebbe ancora adattarsi alla cache, non è vero? – xtofl

+0

Sì, ma dovrai caricare tutte le funzioni all'interno della cache per un singolo utilizzo invece di utilizzare 200 volte la stessa. E dopo la corsa, l'i-cache sarà riempito con codice inutile. @RAC Comunque potrei sbagliarmi, i problemi di cache sono sempre difficili da farci sapere se riesci a ottenere un miglioramento. – Ben

+7

con la riga essendo un tipo di classe, e il valore essendo una const const statica, non vedo come potrebbe gonfiare l'eseguibile. Se il debug è stato rimosso, non dovrebbe esserci codice generato per la riga :: value, sospetto –

3

È possibile utilizzare Boost.Mpl per implementare l'intero processo in fase di compilazione, ma non sono sicuro che sarà più veloce.(Mpl essenzialmente ri-implementa tutti gli algoritmi di STL come modelli metaprogrammazione a tempo di compilazione)

Il problema di questo approccio è che si finisce per srotolamento e inlining un sacco di codice, che può thrash la cache istruzioni e mangiare larghezza di banda di memoria che avrebbe potuto essere salvata. Questo può produrre codice enorme, gonfio e lento.

Probabilmente probabilmente preferirei che il compilatore incorporasse le funzioni che hanno senso. Finché le definizioni delle funzioni row e column sono visibili dal ciclo, il compilatore può inlineamente chiamare le chiamate e srotolare il numero di iterazioni che ritiene utili.

0

Non sono un fan della meta-programmazione dei modelli, quindi potresti prendere questa risposta con un pizzico di sale. Ma, prima che ho investito qualsiasi tempo su questo problema, mi chiedo quanto segue:

  1. È il mio for ciclo un collo di bottiglia?
  2. Lo srotolamento farà qualche differenza? Facilmente testato con srotolamento e misurazione manuale.

In molti compilatori/cpu, la versione "loop" può offrire prestazioni migliori a causa degli effetti della cache.

Ricorda: Misura prima, ottimizza dopo, se non del tutto.

+0

hehe sapevo che saresti da qualche parte intorno a questo Q. – Ben

+4

C'è una discussione rilevante sotto la risposta di TED - lo srotolamento non è il vero obiettivo, il vero obiettivo è eliminare le funzioni row() e column() con TMP equivalenti. Lo srotolamento del loop diventa un requisito secondario. Il codice di ordine in loop e arbitrario è stato profilato ed è decisamente più lento del codice legacy (non bloccato, in ordine fisso). – RAC

1

f sarebbe necessario restituire un double - che non può essere fatto in fase di compilazione.

+1

Certo, questa è una parte che è ancora un po 'torbida per me. Comunque, f() è sempre una frazione e posso fare TMP per il suo numeratore e denominatore indipendentemente, quindi static_cast per raddoppiare e fare il rapporto in pt mobile. (Io spero). Ma ottimo punto. – RAC

1

Se siete disposti a modificare la sintassi un po 'si può fare qualcosa di simile:

template <int i, int ubound> 
struct OuterFor { 
    void operator()() { 
     InnerFor<i, 0, J>()(); 
     OuterFor<i + 1, ubound>()(); 
    } 
}; 

template <int ubound> 
struct OuterFor <ubound, ubound> { 
    void operator()() { 
    } 
}; 

In InnerFor, i è il contatore loop esterno (compilazione costante di tempo), j è il contatore loop interno (inizialmente 0 - anche la costante di tempo di compilazione), quindi è possibile valutare la riga come un modello di tempo di compilazione.

È un po 'più complicato, ma come dici tu, row(), col() e f() sono comunque le tue parti complicate. Almeno provalo e vedi se ne vale la pena. Potrebbe valerne la pena investigare altre opzioni per semplificare le funzioni row(), etc.

+0

Mi piace davvero dove potrebbe essere diretta questa soluzione: potresti fornire un esempio che in realtà chiama/invoca/crea un'istanza dei loop? Inoltre, con che facilità è possibile estenderlo a loop nidificati più profondi? Un loop annidato a quattro profondità non è fuori questione. (scorre sullo stato di polarizzazione, x, yez). – RAC

1

non ho mai provato a fare questo in modo da prendere questa idea con un grano di sale ...

Sembra che si potrebbe usare per fare il Boost.Preprocessor srotolamento del loop (in particolare le BOOST_PP_FOR e BOOST_PP_FOR_r macro) e poi utilizzare i modelli per generare l'espressione costante effettiva.

+1

Dopo un po 'di riflessione, non penso che funzionerà. Il problema è che I e J usati per terminare il ciclo non sono realmente costanti, ma sono dedotti dagli argomenti del modello (non mostrati). Lo snippet che ho postato in precedenza è racchiuso all'interno di un blocco modello {...} e I e J sono calcolati da ORDER utilizzando altri TMP. IIRC il preprocessore viene invocato prima che avvenga l'espansione del modello, il che potrebbe rovinare tutto. Tuttavia, potrei sbagliarmi. – RAC

6

questo è il modo per farlo direttamente:

template <int i, int j> 
struct inner 
{ 
    static void value() 
    { 
    A(row<i,j>::value, column<i,j>::value) = f<i,j>::value; 
    inner<i, j+1>::value(); 
    } 
}; 

template <int i> struct inner<i, J> { static void value() {} }; 

template <int i> 
struct outer 
{ 
    static void value() 
    { 
    inner<i, 0>::value(); 
    outer<i+1>::value(); 
    } 
}; 

template <> struct outer<I> { static void value() {} }; 

void test() 
{ 
    outer<0>::value(); 
} 

Si può passare attraverso A come parametro per ciascuna delle value s se necessario.

Ecco un modo con i modelli variadic che non richiede ho codificato duro e J:

#include <utility> 

template <int j, class Columns> 
struct Inner; 

template <class Columns, class Rows> 
struct Outer; 

template <int j, int... i> 
struct Inner<j, std::index_sequence<i...>> 
{ 
    static void value() { (A(column<i, j>::value, row<i, j>::value), ...); } 
}; 


template <int... j, class Columns> 
struct Outer<std::index_sequence<j...>, Columns> 
{ 
    static void value() { (Inner<j, Columns>::value(), ...); } 
}; 

template <int I, int J> 
void expand() 
{ 
    Outer<std::make_index_sequence<I>, std::make_index_sequence<J>>::value(); 
} 

void test() 
{ 
    expand<3, 5>(); 
} 

(snippet con assembly generato: https://godbolt.org/g/DlgmEl)

+0

Grazie per l'esempio, il codice non è abbastanza compilabile. Penso che le due specializzazioni siano errate, presumibilmente 'template <> struct outer {static void value() {}};' dovrebbe essere 'template <> struct outer <0> {static void value() {}};'. Non sono sicuro dell'altro, forse 'modello struct inner {static void value() {}};'? (Immagino che siano pensati per terminare la ricorsione, è abbastanza divertente rimuoverli completamente e guardare la compilazione per sempre!) – pjcard

+0

Appena controllato - compila e funziona come previsto (dipende da alcuni valori impliciti nella domanda). Ecco uno snippet di lavoro completo: https://godbolt.org/g/bvngt9 –

+0

In questi giorni sarebbe molto più facile con variadics. –