Somma efficiente e stabile dei numeri ordinati


12

Ho un elenco abbastanza lungo di numeri positivi in ​​virgola mobile ( std::vector<float>, dimensione ~ 1000). I numeri sono ordinati in ordine decrescente. Se li riassumo seguendo l'ordine:

for (auto v : vec) { sum += v; }

Immagino di poter avere qualche problema di stabilità numerica, poiché vicino alla fine del vettore sumsarà molto più grande di v. La soluzione più semplice sarebbe quella di attraversare il vettore in ordine inverso. La mia domanda è: è efficace così come il caso a termine? Mi mancherà più cache?

C'è qualche altra soluzione intelligente?


1
Alla domanda di velocità è facile rispondere. Benchmark it.
Davide Spataro,

La velocità è più importante della precisione?
rigido

Una domanda non abbastanza duplicata, ma molto simile: somma delle serie usando float
acraig5075

4
Potrebbe essere necessario prestare attenzione ai numeri negativi.
Approgrammatore

3
Se ti interessa davvero la precisione ad alti livelli, dai un'occhiata alla somma di Kahan .
Max Langhof,

Risposte:


3

Immagino di poter avere qualche problema di stabilità numerica

Quindi prova per questo. Attualmente hai un ipotetico problema, vale a dire nessun problema.

Se esegui il test e l'ipotetico si materializza in un problema reale , allora dovresti preoccuparti di risolverlo effettivamente.

Cioè: la precisione in virgola mobile può causare problemi, ma puoi confermare se lo fa davvero per i tuoi dati, prima di dare la priorità a tutto il resto.

... mi mancherà più cache?

Mille float sono 4Kb - si inseriranno nella cache di un moderno sistema di mercato di massa (se hai in mente un'altra piattaforma, dicci di cosa si tratta).

L'unico rischio è che il prefetcher non ti aiuti durante l'iterazione all'indietro, ma ovviamente il tuo vettore potrebbe essere già nella cache. Non è possibile determinarlo fino a quando non si profila nel contesto dell'intero programma, quindi non c'è motivo di preoccuparsi finché non si ha un programma completo.

C'è qualche altra soluzione intelligente?

Non preoccuparti di cose che potrebbero diventare problemi, fino a quando non diventano effettivamente problemi. Al massimo vale la pena notare possibili problemi e strutturare il codice in modo da poter sostituire la soluzione più semplice possibile con una ottimizzata con cura in seguito, senza riscrivere tutto il resto.


5

Ho contrassegnato il banco con il tuo caso d'uso e i risultati (vedi immagine allegata) indicano la direzione in cui non fa alcuna differenza in termini di prestazioni per andare avanti o indietro.

Potresti voler misurare anche sul tuo hardware + compilatore.


L'utilizzo di STL per eseguire la somma è veloce come il ciclo manuale sui dati ma molto più espressivo.

utilizzare quanto segue per l'accumulo inverso:

std::accumulate(rbegin(data), rend(data), 0.0f);

mentre per l'accumulo in avanti:

std::accumulate(begin(data), end(data), 0.0f);

inserisci qui la descrizione dell'immagine


quel sito è fantastico. Giusto per essere sicuri: non stai programmando la generazione casuale, giusto?
Ruggero Turra,

No, solo la parte nel stateloop è temporizzata.
Davide Spataro,

2

La soluzione più semplice sarebbe quella di attraversare il vettore in ordine inverso. La mia domanda è: è efficace così come il caso a termine? Mi mancherà più cache?

Sì, è efficiente. La previsione del ramo e la strategia della cache intelligente dal tuo hardware sono ottimizzate per l'accesso sequenziale. Puoi accumulare in sicurezza il tuo vettore:

#include <numeric>

auto const sum = std::accumulate(crbegin(v), crend(v), 0.f);

2
Potete chiarire: in questo contesto "accesso sequenziale" significa avanti, indietro o entrambi?
Ruggero Turra,

1
@RuggeroTurra Non posso, a meno che non riesca a trovare una fonte, e non sono in vena di leggere i fogli dati della CPU in questo momento.
YSC

@RuggeroTurra Di solito l'accesso sequenziale significherebbe in avanti. Tutti i prefetcher di memoria semi-decente catturano l'accesso sequenziale.
Spazzolino

@ Spazzolino da denti, grazie. Quindi, se giro indietro, in linea di principio, può essere un problema di prestazioni
Ruggero Turra,

In linea di principio, su almeno un po 'di hardware, se l'intero vettore non è già nella cache L1.
Inutile il

2

A tale scopo puoi utilizzare l'iteratore inverso senza alcuna trasposizione nel tuo std::vector<float> vec:

float sum{0.f};
for (auto rIt = vec.rbegin(); rIt!= vec.rend(); ++rIt)
{
    sum += *rit;
}

Oppure fai lo stesso lavoro usando l'algortitmo standard:

float sum = std::accumulate(vec.crbegin(), vec.crend(), 0.f);

Le prestazioni devono essere le stesse, cambiate solo la direzione di bypass del tuo vettore


Correggimi se sbaglio, ma penso che sia ancora più efficiente di quanto non usi l'istruzione foreach OP, in quanto introduce un sovraccarico. YSC ha ragione sulla parte di stabilità numerica, comunque.
sephiroth,

4
@sephiroth No, a qualsiasi compilatore decente non importa se hai scritto un range-for o un iteratore.
Max Langhof,

1
Le prestazioni del mondo reale non sono sicuramente le stesse, a causa di cache / prefetching. È ragionevole che l'OP sia diffidente.
Max Langhof,

1

Se per stabilità numerica intendi precisione, allora sì, potresti finire con problemi di precisione. A seconda del rapporto tra i valori più grandi e quelli più piccoli e i requisiti di accuratezza nel risultato, questo può o meno essere un problema.

Se vuoi avere un'elevata precisione, considera la somma di Kahan - questo utilizza un galleggiante extra per la compensazione degli errori. C'è anche una somma a coppie .

Per un'analisi dettagliata del compromesso tra precisione e tempo, consultare questo articolo .

AGGIORNAMENTO per C ++ 17:

Alcune delle altre risposte menzionano std::accumulate. A partire dal C ++ 17 esistono delle politiche di esecuzione che consentono di parallelizzare gli algoritmi.

Per esempio

#include <vector>
#include <execution>
#include <iostream>
#include <numeric>

int main()
{  
   std::vector<double> input{0.1, 0.9, 0.2, 0.8, 0.3, 0.7, 0.4, 0.6, 0.5};

   double reduceResult = std::reduce(std::execution::par, std::begin(input), std::end(input));

   std:: cout << "reduceResult " << reduceResult << '\n';
}

Ciò dovrebbe rendere più veloce la somma di set di dati di grandi dimensioni a costo di errori di arrotondamento non deterministici (suppongo che l'utente non sarà in grado di determinare il partizionamento dei thread).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.