Esiste un paradigma per la composizione di funzioni di "aggiornamento incrementale" in uno stile di flusso di dati puro?


10

Non conosco la terminologia corretta per porre questa domanda, quindi la descriverò con molte parole, abbiate pazienza.

Sfondo , solo così siamo sulla stessa pagina: i programmi spesso contengono cache - un compromesso tempo / memoria. L'errore di un programmatore comune è dimenticare di aggiornare un valore memorizzato nella cache dopo aver modificato una delle sue origini / precedenti a monte. Ma il flusso di dati o il paradigma di programmazione FRP è immune da tali errori. Se abbiamo un numero di funzioni pure e le colleghiamo insieme in un grafico di dipendenza diretto, i nodi possono avere il loro valore di output memorizzato nella cache e riutilizzato fino a quando uno qualsiasi degli input della funzione cambia. Questa architettura di sistema è descritta nel documento Caching In Dataflow-based Environments e in un linguaggio imperativo è più o meno analogo alla memoizzazione.

Problema : quando uno degli input di una funzione cambia, dobbiamo ancora eseguire la funzione nel suo insieme, eliminando l'output memorizzato nella cache e ricalcolando da zero. In molti casi, questo mi sembra dispendioso. Considera un semplice esempio che genera un elenco dei "primi 5 qualunque". I dati di input sono un elenco non ordinato di qualunque cosa. Viene passato come input a una funzione che genera un elenco ordinato. Che a sua volta viene inserito in una funzione che accetta solo i primi 5 elementi. In pseudocodice:

input = [5, 20, 7, 2, 4, 9, 6, 13, 1, 45]
intermediate = sort(input)
final_output = substring(intermediate, 0, 5)

La complessità della funzione di ordinamento è O (N log N). Ma considera che questo flusso viene utilizzato in un'applicazione in cui l'input cambia solo un po 'alla volta, aggiungendo 1 elemento. Invece di riordinare ogni volta da zero, sarebbe più veloce, infatti O (N), utilizzare una funzione che aggiorna il vecchio elenco ordinato memorizzato nella cache inserendo il nuovo elemento nella posizione corretta. Questo è solo un esempio: molte funzioni "da zero" hanno simili controparti di "aggiornamento incrementale". Inoltre, forse l'elemento appena aggiunto non apparirà nemmeno in final_output perché è dopo la 5a posizione.

La mia intuizione suggerisce che potrebbe essere possibile in qualche modo aggiungere tali funzioni di "aggiornamento incrementale" a un sistema di flusso di dati, fianco a fianco con le esistenti funzioni "da zero". Naturalmente, il ricalcolo di tutto da zero deve sempre dare lo stesso risultato di una serie di aggiornamenti incrementali. Il sistema dovrebbe avere la proprietà che se ciascuna delle singole coppie FromScratch-incrementale primitive sempre danno lo stesso risultato, quindi le funzioni composte grandi costruite da loro dovrebbero dare automaticamente lo stesso risultato.

Domanda : è possibile avere un sistema / architettura / paradigma / meta-algoritmo in grado di supportare sia le funzioni FromScratch che le loro controparti incrementali, cooperando per l'efficienza e composto in grandi flussi? Se no, perché? Se qualcuno ha già studiato questo paradigma e lo ha pubblicato, come si chiama, e posso ottenere un breve riassunto di come funziona?


A proposito, nel caso specifico del tuo esempio, una soluzione ancora più efficiente sarebbe quella di utilizzare un heap . L'inserimento di un elemento ora è solo e la generazione di un elenco ordinato aggiornato dei principali valori ora è solo . O(logn)KO(Klogn)
j_random_hacker

Risposte:


7

Questo campo è stato inventato molte volte e ha molti nomi, come:

(E forse di più.) Quelli non sono uguali, ma correlati.

Parafrasando Cai et al (1): Esistono due modi principali per implementare genericamente algoritmi online (cioè senza riferimento a specifici problemi algoritmici):

  • Incremento progressivo statico. Gli approcci statici analizzano un programma in fase di compilazione e producono una versione incrementale che aggiorna in modo efficiente l'output del programma originale in base alla modifica degli input. Gli approcci statici hanno il potenziale per essere più efficienti degli approcci dinamici, poiché non è richiesta la contabilità in fase di esecuzione. Inoltre, le versioni incrementali calcolate possono spesso essere ottimizzate utilizzando tecniche di compilazione standard come la piegatura o l'allineamento costanti. Questo è l'approccio studiato in (1).

  • Incrementalizzazione dinamica. Gli approcci dinamici creano grafici di dipendenza dinamici mentre il programma è in esecuzione e propagano le modifiche lungo questi grafici. L'approccio più noto è il calcolo autoregolante di Acar. L'idea chiave è semplice: i programmi vengono eseguiti sull'input originale in un ambiente di runtime avanzato che tiene traccia delle dipendenze tra i valori in un grafico di dipendenza dinamico; i risultati intermedi vengono memorizzati nella cache. (Come si può immaginare, questo tende a utilizzare molta memoria e molte ricerche in questo campo riguardano come limitare l'utilizzo della memoria.) Successivamente, le modifiche all'input si propagano attraverso i grafici delle dipendenze dai cambiamenti agli input ai risultati, aggiornando sia intermedio che risultati finali; questa elaborazione è spesso più efficiente della ricalcolo. Tuttavia, la creazione di grafici di dipendenza dinamica impone un elevato sovraccarico di fattore costante durante il runtime, che varia da 2 a 30 negli esperimenti riportati.

Inoltre, si può sempre provare a trovare "a mano" una versione online di un determinato algoritmo. Questo può essere difficile.


(1) Y. Cai, PG Giarrusso, T. Rendel, K. Ostermann, Una teoria dei cambiamenti per le lingue di ordine superiore: incrementare i calcoli λ mediante differenziazione statica .


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.