Il pattern "restituisci questo" può essere ottimizzato senza costi?

6

return this (o costrutto simile) consente il concatenamento del metodo. La mancanza di esso è dolorosa, perché devi scrivere tale codice (C #):

var list = new List<string>();
list.Add("hello");
list.Add("world");

invece di

list.Add("hello").Add("world");

L'elisir lo risolve bene per la funzione concatenamento, invece di fare affidamento su callee fa affidamento sul chiamante (perdonami i miei errori, non so l'elisir):

list |> add("hello") |> add("world");

Ma ora ho appena letto questa frase su wikipedia :

Returning an object of built-in type from a function usually carries little to no overhead, since the object typically fits in a CPU register.

Da un lato, il call non sa se il risultato verrà utilizzato o meno, d'altro canto il chiamante non può impedire a callee di impostare il valore del risultato. Quindi sono scettico su questo "piccolo", ma "non sovraccarico"?

Quindi LA MIA DOMANDA per questo schema molto particolare (cioè return this con concatenamento del metodo) - può essere ottimizzato senza sovraccarico? Come?

Domanda per esempio - diciamo che scriverò un framework e cospargerò ogni metodo possibile con return this solo per dare la possibilità di concatenare il metodo. La domanda sorge - chi userà chi non usa il metodo di concatenamento pagherà il prezzo delle prestazioni ridotte? In che modo il compilatore può ottimizzare il codice che questa funzionalità avrà costo zero.

Aggiornamento dopo i primi 2 commenti - "cheap"!="free", quindi forse un'altra prospettiva per la mia domanda, perché la differenza "piccola" rispetto a "nessun costo". Se può essere garantito è a costo zero, scriviamo "nessun costo", punto. Quindi presumo che non possa essere garantito, quindi "poco".

Chiarimento Non sto chiedendo come creare una sintassi simile a Elixir in un'altra lingua. Sto chiedendo come sia possibile per il compilatore ottimizzare l'interazione del chiamante callee su return this + concatenamento del metodo (o mancanza di esso, se non utilizzato).

    
posta greenoldman 27.01.2016 - 17:18
fonte

8 risposte

3

Returning an object of built-in type from a function usually carries little to no overhead, since the object typically fits in a CPU register.

Il punto di questo commento di wikipedia non è che la restituzione del valore non ha costi. L'overhead è un costo aggiuntivo, e qui il costo è in aggiunta a quello del meccanismo del valore di ritorno. L'articolo è in contrasto con il lavoro aggiuntivo richiesto per gli oggetti che devono essere copiati quando vengono restituiti.

Thus MY QUESTION for this very particular pattern (i.e return this with method chaining) -- can it be optimized with no overhead? How?

In generale, no. In casi particolari (come nel caso in cui la funzione può essere sottolineata) può essere.

    
risposta data 28.01.2016 - 05:07
fonte
10

Prima di tutto,

var list = new List<string>();
list.Add("hello");
list.Add("world");

È proprio come, se non più leggibile di

var list = new List<string>().Add("hello").Add("world");

Le linee di codice non sono in alcun modo un proxy per la pulizia o la semplicità del codice.

Thus my question for this very particular pattern (i.e "return this" for method chaining) -- can it be optimized with no overhead? How?

Quindi in teoria, certo. Questo sarebbe simile all'ottimizzazione delle chiamate tail, in cui non avresti bisogno di srotolare completamente il frame dello stack quando ti sposti tra le funzioni, potresti lasciare l'argomento this dove è quando hai finito (o semplicemente sbirciare piuttosto che saltarlo in un modello basato su stack). La sfida è che puoi farlo solo se sai che la prossima chiamata sarà una catena . Dal momento che le funzioni non conoscono realmente i loro chiamanti, non sanno se dovrebbero ripulire se stesse o meno. Potresti avere un contrassegno interno, che la funzione potrebbe verificare per sapere quale percorso intraprendere, ma sarebbe più costoso che passare in this sempre.

Il compilatore potrebbe anche creare due versioni della funzione e scegliere quella giusta in fase di compilazione. Ciò potrebbe gonfiare l'eseguibile risultante, il tempo di compilazione lento, ma probabilmente radere un'istruzione o due dall'effettivo runtime.

    
risposta data 27.01.2016 - 18:57
fonte
3

Almeno in C #, il tuo costrutto desiderato non è assolutamente necessario.

Puoi scrivere qualcosa come

var mylist = new List<string>(new[] {"Hello", "World"});
...
mylist.AddRange(new[] {"Add", "Some", "More", "Items"});

che è il più conciso possibile, pur rimanendo perfettamente leggibile.

Se vuoi davvero concatenare il metodo in una classe, anche se non ne hai l'origine, puoi utilizzare i metodi di estensione.

public static class ListExtender
{
    public static IList<T> ChainedAdd<T>(this IList<T> list, T item)
    {
        list.Add(item);
        return list;
    }
}

poi

 mylist.ChainedAdd("x").ChainedAdd("y").ChainedAdd("z");
    
risposta data 27.01.2016 - 19:13
fonte
3

can it be optimized with no overhead? How?

Come ha scritto Telastyn, un approccio consiste nell'avere un compilatore che fornisca due versioni di funzioni. Se fossi nel ruolo di un progettista di compilatori, lo gestirò in questo modo:

  • Vorrei creare un compilatore con il supporto inlining . Un tale strumento può ovviamente ottimizzare l'istruzione return this quando non viene utilizzata.

  • e se questo ipotetico compilatore decide di non incorporare una funzione specifica, l'ottimizzazione dell'equivalente del codice macchina di return this non ne vale la pena (se lo fosse, il compilatore si allinea).

Quindi questo non porta a "zero overhead" in senso puramente matematico, ma a "zero overhead" per qualsiasi scopo pratico.

    
risposta data 28.01.2016 - 08:09
fonte
2

Un compilatore può garantire che il pattern di concatenamento di funzioni return this + non abbia mai prestazioni peggiori rispetto a chiamare più metodi sullo stesso oggetto, riscrivendo sempre il precedente modello nel secondo.

Ma penso che sarebbe una riscrittura relativamente complicata e non sono sicuro che ne varrebbe la pena. Soprattutto perché ritengo che il modello return this abbia un potenziale per essere più efficiente, perché significa che this non deve essere ricordato sia dal chiamante che dal chiamato: il chiamante può dimenticare l'oggetto mentre la catena è in esecuzione.

Si noti che è improbabile che il compilatore lo riconosca. Quindi, per approfittare di questa ottimizzazione (teorica), invece di scrivere:

var list = new List<string>();
list.Add("hello").Add("world");
foo(list);

Scriveresti:

foo(new List<string>().Add("hello").Add("world"));
    
risposta data 28.01.2016 - 20:50
fonte
1

Dart ha questa funzione . Quindi puoi fare

StringList myList = new StringList()
    ..Add("hello")
    ..Add("world");

Qui possiamo trattare F(expr..m(...)) come zucchero della sintassi per:

var _generated_temp = expr;
_generated_temp.m(...);
F(_generated_temp);

Puoi vedere Dart che consente anche di impostare le proprietà per essere concatenate nello stesso modo:

Person myPerson = new Person()
    ..Name = "Jane Doe"
    ..Age  = 31;

C # ti fornisce un sottoinsieme di queste funzionalità con la sintassi di inizializzazione. Quindi puoi tradurre quanto sopra in

StringList myList = new StringList { "hello", "world" };

Person myPerson = new Person{ Name = "Jane Doe", Age = 31 };

Tuttavia, questo zucchero di sintassi è disponibile solo all'inizializzazione e non è disponibile per le chiamate al metodo arbitrario, quindi è strettamente meno espressivo della versione di Dart.

Quindi è possibile offrire interfacce fluenti come una funzionalità linguistica, che rimuoverà qualsiasi costo di prestazioni. Tuttavia, il costo delle prestazioni di un'interfaccia fluida è così trascurabile da essere effettivamente nullo, quindi non si dovrebbe argomentare su queste basi. (Si noti che potrebbe essere in teoria possibile che i metodi non virtuali restituiscano appena this da ottimizzare. Questa ottimizzazione non varrebbe la pena di essere eseguita.) Piuttosto, il vantaggio sarebbe l'opportunità di utilizzare un'interfaccia fluida anche quando non è stato progettato per.

    
risposta data 27.01.2016 - 18:23
fonte
1

Se l'implementazione utilizza una convenzione di chiamata che utilizza lo stesso registro per passare il puntatore this a una funzione che fa per restituire un valore dalla funzione, quindi return this è un'operazione a costo zero (perché il valore di ritorno è già lì).

    
risposta data 27.01.2016 - 23:23
fonte
0

In teoria, sì. C ++ ha un termine per questo: copia elision . Lo standard C ++ consente di copiare l'elisione. Il concetto che stai chiedendo, infatti, ha il suo tla: RVO (ottimizzazione del valore restituito).

Essenzialmente, il compilatore può (ma, sfortunatamente, non è obbligato a) decidere lo spazio in cui risiederà il valore di ritorno prima che la funzione venga chiamata. Ciò evita di spingere il valore di ritorno nello stack e quindi di copiarlo in questo spazio. Il compilatore può generare il codice funzione in modo tale che invece di modificare il valore da spingere nello stack, esso venga scritto direttamente nello spazio in cui quel valore dello stack verrà copiato dopo che la funzione è tornata.

Il motivo per cui questo non può essere fatto always (cioè, per impostazione predefinita) è che lo spazio in cui il valore deve essere copiato può contenere informazioni utilizzate durante l'esecuzione della funzione. Ma ci sono metodi ben noti per aggirare anche questo. Ad esempio, copy-on-write è ciò che i sistemi operativi fanno ai blocchi di memoria condivisi da più processi. I compilatori possono, allo stesso modo, fare copia-su-scrittura dello spazio che deve essere popolato al ritorno di una funzione.

    
risposta data 28.01.2016 - 06:33
fonte

Leggi altre domande sui tag