È inefficace concatenare le stringhe una alla volta?


11

Ricordo dai miei giorni di programmazione in C che quando due stringhe sono unite, il sistema operativo deve allocare memoria per la stringa unita, quindi il programma può copiare tutto il testo della stringa nella nuova area in memoria, quindi la vecchia memoria deve manualmente essere rilasciato. Quindi, se ciò viene fatto più volte come nel caso di unirsi a un elenco, il sistema operativo deve allocare costantemente sempre più memoria, solo per averlo rilasciato dopo la successiva concatenazione. Un modo molto migliore per farlo in C sarebbe quello di determinare la dimensione totale delle stringhe combinate e allocare la memoria necessaria per l'intero elenco unito di stringhe.

Ora nei moderni linguaggi di programmazione (ad esempio C #), di solito vedo che i contenuti delle raccolte vengono uniti ripetendo iterando la raccolta e aggiungendo tutte le stringhe, una alla volta, a un singolo riferimento di stringa. Non è inefficiente, anche con la moderna potenza di calcolo?


lascialo al compilatore e al profiler, se ne occuperanno, il tuo tempo molto più costoso del tempo per concatenare le stringhe.
OZ_

7
Dipende dall'implementazione: dovresti davvero controllare la documentazione per la tua particolare libreria di stringhe. È possibile implementare stringhe che si concatenano per riferimento, nel tempo O (1). In ogni caso, se è necessario concatenare un elenco arbitrariamente lungo di stringhe, è necessario utilizzare classi o funzioni progettate per questo tipo di cose.
comingstorm

Si noti che cose come la concatenazione di stringhe sono generalmente gestite da una funzione di libreria, non dal sistema operativo. Il sistema operativo potrebbe essere coinvolto nell'allocazione della memoria, ma probabilmente non per oggetti relativamente piccoli come le stringhe.
Caleb,

@Caleb Il sistema operativo è coinvolto in TUTTA l'allocazione della memoria. Non seguire questa regola è un tipo di perdita di memoria. L'eccezione è quando nell'applicazione sono presenti stringhe codificate; quelli vengono scritti come dati binari all'interno dell'assembly generato. Ma non appena manipoli (o forse assegni) una stringa, questa deve essere archiviata nella memoria (ovvero, la memoria deve essere allocata).
JSideris,

4
@Bizorke In uno scenario tipico, un allocatore di memoria come malloc () (che fa parte della libreria standard C, non il sistema operativo) viene utilizzato per allocare vari blocchi di memoria dalla memoria che è già stata allocata al processo dal sistema operativo. Il sistema operativo non deve essere coinvolto a meno che il processo non abbia poca memoria e non richieda altro. Può anche prendere parte a un livello inferiore se un'allocazione provoca un errore di pagina. Quindi sì, il sistema operativo alla fine fornisce la memoria, ma non è necessariamente coinvolto nell'allocazione frammentaria di stringhe e altri oggetti all'interno del processo.
Caleb,

Risposte:


21

La tua spiegazione del perché sia ​​inefficiente è accurata, almeno nelle lingue che conosco (C, Java, C #), anche se non sarei d'accordo sul fatto che sia universalmente comune eseguire enormi quantità di concatenazione di stringhe. Nel codice C # io lavoro su, non v'è l'uso abbondante di StringBuilder, String.Formatecc che sono tutti di risparmio techiniques per evitare un eccesso di riallocazione della memoria.

Quindi, per arrivare alla risposta alla tua domanda, dobbiamo porre un'altra domanda: se non è mai davvero un problema concatenare le stringhe, perché le classi dovrebbero piacere StringBuildered StringBufferesistere ? Perché l'uso di tali classi è incluso anche nei libri e nelle classi di programmazione per principianti? Perché i consigli di ottimizzazione apparentemente pre-maturi dovrebbero essere così importanti?

Se la maggior parte degli sviluppatori concatenatori di stringhe dovesse basare la propria risposta esclusivamente sull'esperienza, la maggior parte direbbe che non fa mai la differenza e eviterebbe l'uso di tali strumenti a favore del "più leggibile" for (int i=0; i<1000; i++) { strA += strB; }. Ma non l'hanno mai misurato.

La vera risposta a questa domanda potrebbe essere trovata in questa risposta SO , che rivela che in un caso, quando si concatenano 50.000 stringhe (che a seconda della propria applicazione, può essere un evento comune), anche di piccole dimensioni, si ottiene un impatto sulle prestazioni 1000x .

Se le prestazioni letteralmente non significano nulla, concatenate sicuramente. Ma non sarei d'accordo sul fatto che l'uso di alternative (StringBuilder) sia difficile o meno leggibile , e quindi sarebbe una pratica pratica di programmazione che non dovrebbe invocare la difesa "ottimizzazione prematura".

AGGIORNARE:

Penso che ciò accada, è conoscere la tua piattaforma e seguire le sue migliori pratiche, che purtroppo non sono universali . Due esempi da due diverse "lingue moderne":

  1. In un'altra risposta SO , le esatte caratteristiche di prestazione opposte (array.join vs + =) sono risultate talvolta vere in JavaScript . In alcuni browser, la concatenazione di stringhe sembra essere ottimizzata automaticamente e in altri casi non lo è. Quindi la raccomandazione (almeno in quella domanda SO), è solo concatenare e non preoccuparsene.
  2. In un altro caso, un compilatore Java può sostituire automaticamente la concatenazione con un costrutto più efficiente come StringBuilder. Tuttavia, come altri hanno sottolineato, questo è indeterministico, non garantito e l'utilizzo di StringBuilder non danneggia la leggibilità. In questo caso particolare, tenderei a sconsigliare l'uso della concatenazione per raccolte di grandi dimensioni o fare affidamento su un comportamento del compilatore Java indeterministico. Allo stesso modo, in .NET, nessuna ottimizzazione dell'ordinamento viene mai eseguita .

Non è esattamente un peccato cardinale non conoscere subito ogni sfumatura di ogni piattaforma, ma ignorare importanti problemi di piattaforma come questo sarebbe quasi come passare da Java a C ++ e non preoccuparsi di deallocare memoria.


-1: contiene i principali BS. strA + strBè esattamente uguale all'utilizzo di StringBuilder. Ha un successo di prestazioni 1x. O 0x, a seconda di come stai misurando. Per maggiori dettagli, codinghorror.com/blog/2009/01/…
amara,

5
@sparkleshy: La mia ipotesi è che la risposta SO utilizza Java e l'articolo collegato utilizza C #. Concordo con coloro che affermano che "dipende dall'implementazione" e che "misurano per il tuo ambiente particolare".
Kai Chan,

1
@KaiChan: la concatenazione di stringhe è sostanzialmente la stessa in Java e C #
Amara,

3
@sparkleshy - Punto preso, ma usare StringBuilder, String.Join, ecc. per concatenare esattamente due stringhe è raramente una raccomandazione, mai. Inoltre, la domanda del PO riguarda in particolare "i contenuti delle raccolte unite", il che non è il caso (dove StringBuilder, ecc. È molto applicabile). Indipendentemente da ciò, aggiornerò il mio esempio per essere più al punto.
Kevin McCormick,

3
Non mi interessa il linguaggio ai fini di questa domanda. L'uso del stringbuilder dietro le quinte in alcune lingue spiega perché potrebbe non essere inefficace concatenare un intero elenco di stringhe, che risponde alla mia domanda. Questa risposta ha tuttavia spiegato che l'adesione a un elenco potrebbe essere potenzialmente pericolosa e ha raccomandato lo stringbuilder come alternativa. Consiglio di aggiungere alla compilazione l'uso del compilatore di stringbuilder dietro le quinte, al fine di evitare possibili perdite di reputazione o interpretazioni errate.
JSideris,

2

Non è efficiente, approssimativamente per i motivi che hai descritto. Le stringhe in C # e Java sono immutabili. Le operazioni sulle stringhe restituiscono un'istanza separata invece di modificare quella originale, diversamente da come avveniva in C. Quando si concatenano più stringhe, viene creata un'istanza separata ad ogni passaggio. L'allocazione e la successiva immondizia di raccolta di tali istanze non utilizzate possono causare un calo delle prestazioni. Solo questa volta la gestione della memoria viene gestita dal garbage collector.

Sia C # che Java introducono una classe StringBuilder come stringa mutabile specificatamente per questo tipo di attività. Un equivalente in C sarebbe utilizzare un elenco collegato di stringhe concatenate invece di unirle in un array. C # offre anche un comodo metodo Join su stringhe per unire una raccolta di stringhe.


1

A rigor di termini è un uso meno efficiente dei cicli della CPU, quindi hai ragione. Ma per quanto riguarda il tempo degli sviluppatori, i costi di manutenzione ecc. Se si aggiunge il costo del tempo all'equazione, è quasi sempre più efficiente fare ciò che è più semplice, quindi se necessario, profilare e ottimizzare i bit lenti.
"La prima regola di ottimizzazione del programma: non farlo. La seconda regola di ottimizzazione del programma (solo per esperti!): Non farlo ancora."


3
regole non molto efficaci, penso.
OZ_

@OZ_: Questa è una citazione ampiamente usata (Michael A. Jackson) e altre di artisti del calibro di Donald Knuth ... Poi ce n'è una, che di solito mi astengo dall'usare "Più peccati di calcolo sono commessi in nome dell'efficienza ( senza necessariamente raggiungerlo) che per qualsiasi altra singola ragione, inclusa la cieca stupidità ".
mattnz,

2
Dovrei sottolineare che Michael A. Jackson era un brit, quindi è l' ottimizzazione non l' ottimizzazione . Ad un certo punto dovrei davvero correggere la pagina di Wikipedia . * 8 ')
Mark Booth,

Sono totalmente d'accordo, dovresti correggere quegli errori di ortografia. Sebbene la mia lingua madre sia l'inglese delle regine, trovo più facile parlarci negli Stati Uniti .......
mattnz,

qualcuno non penserà agli utenti. Potresti renderlo leggermente più veloce per lo sviluppatore, ma ogni singolo cliente ne soffrirà. Scrivi il tuo codice per loro, non per te.
gbjbaanb,

1

È molto difficile dire qualcosa sulle prestazioni senza un test pratico. Di recente sono stato molto sorpreso di scoprire che in JavaScript una concatenazione di stringhe naïf era in genere più veloce della soluzione consigliata "make list and join" (prova qui , confronta t1 con t4). Sono ancora perplesso sul perché ciò accada.

Alcune domande che potresti porre quando ragionano sulle prestazioni (specialmente riguardo all'utilizzo della memoria) sono: 1) quanto è grande il mio input? 2) quanto è intelligente il mio compilatore? 3) in che modo il mio runtime gestisce la memoria? Questo non è esaustivo, ma è un punto di partenza.

  1. Quanto è grande il mio contributo?

    Una soluzione complessa avrà spesso un overhead fisso, forse sotto forma di operazioni extra da eseguire o forse in memoria aggiuntiva necessaria. Poiché tali soluzioni sono progettate per gestire casi importanti, gli implementatori di solito non avranno problemi a introdurre quel costo aggiuntivo, poiché il guadagno netto è più importante della microottimizzazione del codice. Quindi, se il tuo input è sufficientemente piccolo, una soluzione ingenua potrebbe avere prestazioni migliori rispetto a quella complessa, anche solo per evitare questo sovraccarico. (determinare ciò che è "sufficientemente piccolo" è la parte difficile però)

  2. Quanto è intelligente il mio compilatore?

    Molti compilatori sono abbastanza intelligenti da "ottimizzare" le variabili che vengono scritte, ma che non vengono mai lette. Allo stesso modo, un buon compilatore potrebbe anche essere in grado di convertire una concatenazione di stringhe ingenua in un uso di libreria (core) e, se molti di essi sono realizzati senza alcuna lettura, non è necessario riconvertirlo in una stringa tra tali operazioni (anche se il tuo codice sorgente sembra fare proprio questo). Non posso dire se qualche compilatore là fuori lo faccia o in che misura (AFAIK Java almeno sostituisce diversi concat nella stessa espressione con una sequenza di operazioni StringBuffer), ma è una possibilità.

  3. In che modo il mio runtime gestisce la memoria?

    Nelle moderne CPU il collo di bottiglia non è solitamente il processore, ma la cache; se il tuo codice accede a molti indirizzi di memoria "distanti" in breve tempo, il tempo necessario per spostare tutta quella memoria tra i livelli di cache supera la maggior parte delle ottimizzazioni nelle istruzioni utilizzate. Ciò è particolarmente importante nei runtime con i garbage collector generazionali, dal momento che le variabili create più di recente (all'interno dello stesso ambito di funzioni, ad esempio) si troveranno generalmente in indirizzi di memoria contigui. Questi runtime inoltre spostano regolarmente la memoria avanti e indietro tra le chiamate di metodo.

    Un modo in cui può influire sulla concatenazione di stringhe (dichiarazione di non responsabilità: questa è un'ipotesi selvaggia, non sono abbastanza informato per dirlo con certezza) sarebbe se la memoria per l'ingenuo fosse allocata vicino al resto del codice che la utilizza (anche se lo alloca e lo rilascia più volte), mentre la memoria per l'oggetto libreria è stata allocata lontano da esso (quindi i molti contesti cambiano mentre il tuo codice viene calcolato, la libreria consuma, il tuo codice ne calcola di più, ecc. genererebbe molti errori di cache). Ovviamente per grandi input OTOH le perdite di cache si verificheranno comunque, quindi il problema delle allocazioni multiple diventa più pronunciato.

Detto questo, non sto sostenendo l'uso di questo o quel metodo, solo che i test, la profilazione e il benchmarking dovrebbero precedere qualsiasi analisi teorica sulle prestazioni, poiché la maggior parte dei sistemi al giorno d'oggi sono troppo complessi per comprendere appieno senza una profonda esperienza in materia.


Sì, sono d'accordo che questa è sicuramente un'area in cui un compilatore potrebbe teoricamente rendersi conto che stai cercando di aggiungere un mucchio di stringhe insieme e quindi ottimizzare come se stessi usando un generatore di stringhe. Tuttavia, questa non è una cosa da poco, e non penso che sia implementata in nessun compilatore moderno. Mi hai appena dato un'ottima idea per un progetto di ricerca universitaria: D.
JSideris,

Controlla questa risposta , il compilatore Java utilizza già StringBuildersotto il cofano, tutto ciò che dovrebbe fare è non chiamare toStringfino a quando la variabile non è effettivamente necessaria. Se ricordo bene, lo fa per una singola espressione, il mio unico dubbio è se si applichi o meno a più istruzioni nello stesso metodo. Non so nulla degli interni di .NET, ma credo che una strategia simile potrebbe essere utilizzata anche dal compilatore C #.
mgibsonbr,

0

Joel ha scritto un ottimo articolo su questo argomento qualche tempo fa. Come altri hanno sottolineato, dipende fortemente dalla lingua. A causa del modo in cui le stringhe vengono implementate in C (zero terminato, senza campo di lunghezza), la routine della libreria strcat standard è molto inefficiente. Joel presenta un'alternativa con solo un piccolo cambiamento molto più efficiente.


-1

È inefficace concatenare le stringhe una alla volta?

No.

Hai letto "La triste tragedia del teatro della microottimizzazione" ?


4
"L'ottimizzazione precoce è la radice di tutti i mali." - Knuth
Scott C Wilson,

4
La radice di tutto il male nell'ottimizzazione sta prendendo questa frase senza contesto.
OZ_

Dire qualcosa è vero senza fornire alcune ragioni di supporto non è utile in un forum come questo.
Edward Strange,

@Crazy Eddie: hai letto perché Jeff Atwood ha dovuto dire?
Jim G.
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.