Dubbi su utilizzo della classe String

Ciao a tutti!!!

Sono consapevole che l'utilizzo della classe "String" con la S maiuscola molti dicono che è il MALE ASSOLUTO poiché, se non gestito bene, può causare frammentazione della memoria RAM, soprattutto se si usano microcontrollori con poca memoria come l'ATMEGA328 e altri suoi simili...

Sto già provando altre alternative come le stringhe fatte nel "classico" modo ovvero con gli array, ma l'intento di questo post è ricevere delle delucidazioni nel caso ho desiderio di continuare ad utilizzare la classe String sia perché, se è stata realizzata, dovrebbe avere un suo senso e quindi utilizzarla al meglio evitando pasticci :smiley:

Detto questo ho dei dubbi che in parte sono riuscito a capire leggendo un po' da vari forum, dove in alcuni ho preso spavento tipo questo:

[The Evils of Arduino Strings]
(The Evils of Arduino Strings | Majenko's Hardware Hacking Blog)

E in altri dove "tranquillizzano" di più l'uso:

[String class warning on Arduino forum]
(String class warning on Arduino forum | Teensy Forum)

Ho usato anche chatGPT e non ho notato grandi differenze, cioè non sembra che mi abbia detto cose sbagliate riguardo all'utilizzo della classe "String".

Nei miei progetti non ho mai utilizzato più di una variabile Stringa globale proprio perché sapevo in maniera generale sul fatto che allocasse dinamicamente il proprio spazio sulla RAM.

L'utilizzo principale è sempre e SOLO stato il ricevimento dei dati da parte della funzione:
Serial.readStringUntil(), e di conseguenza la comparazione di essa per gestire messaggi seriali dal programma.

Finora, nonostante programmi alquanto complessi, non ho mai ottenuto problemi di prestazioni anche perché tutti i protocolli che ho costruito non hanno mai richiesto messaggi complessi; cioè, sono sempre riuscito a gestire la stringa con messaggi di meno di 16 caratteri e non ha mai avuto di più.

Ho imparato ad usare la funzione "String.reserve()" per la variabile globale in maniera tale che mi allochi sempre all'inizio uno spazio riservato per essa sapendo che i messaggi non saranno mai più grandi e quindi nessuna nuova riallocazione... ALMENO SPERO :frowning:

DUBBIO PRINCIPALE:

Quando si utilizza la funzione: Serial.readStringUntil() ho visto al suo interno, nel file "Stream.cpp", che ha una variabile Stringa temporanea.
Da questo sono rimasto dubbioso perché essendo temporanea significa che viene ogni volta allocato uno spazio e deallocato per trasferire il messaggio sulla variabile globale esterna (progetto dell'utente).

Quello che mi chiedo é: se in tutto il progetto è l'unica variabile Stringa temporanea che ogni volta che la funzione viene eseguita viene creata e riaggiornata come nuova alla prossima chiamata, succede che avrò una evitabile frammentazione della memoria?

E' peggio avere una variabile String di tipo temporaneo all'interno di una funzione?
O ogni volta che viene deallocata la sua entità, il suo oggetto sparisce e la memoria HEAP è libera per ricevere altre allocazioni?

Se quello che penso è vero non capisco il senso di tale funzione, significa che ad ogni chiamata frammenta la memoria e spero mi sbaglio :frowning:

Questo vale anche per funzioni che accettano un parametro di tipo Stringa: il passaggio del messaggio avviene creando una allocazione temporanea nell'HEAP per poi essere deallocata? O in questo caso entra in gioco la memoria STACK ? :frowning:

O la classe String è stata progettata con un minimo di gestione per semplici casi come questo?

SECONDO DUBBIO:

Se uso la funzione "reserve()" per l'unica Stringa nel programma e inserisco un messaggio formato sempre da meno caratteri di quanto sia lo spazio allocato con "reserve()" non dovrei avere nessuna riallocazione? Giusto?

E finché userò messaggi variabili ma sempre formati da caratteri minori del numero dello spazio allocato con "reserve()" non dovrei avere nuove allocazioni? Giusto?

Perché ho letto che i problemi ci sono quando vengono eseguite concatenazioni e quindi nuove allocazioni e di conseguenza la probabile e inevitabile creazione di "buchi" nella memoria HEAP :frowning:

Grazie a tutti in ANTICIPO :smiley:

Aggiornamento:

Ho creato il seguente programma per test per vedere se il programma gira tranquillamente per abbastanza tempo e sembra che funzioni perfettamente:

#include <MemoryFree.h>;

#define LED 13

String Counter;

int Contatore;

void setup() {

  delay(10);

  Serial.begin(115200);

  Counter = "Cocco";

  pinMode(LED, OUTPUT);

  delay(100);

}

void loop() {

  String Data = Counter;

  if (Counter == "Cocco") {
    Counter = "Ape";
  } else {
    Counter = "Cocco";
  }

  if (Contatore < 50) {

    Contatore++;

  } else {

    Serial.print("Testo : ");
    Serial.println(Data);

    Serial.println("Memory Free : ");
    Serial.println(freeMemory());

    Contatore = 0;

    digitalWrite(LED, !digitalRead(LED));

  }

  delay(10);

}

Il programma funziona come segue:

1.Scrivo nella variabile stringa Counter la parola "Cocco"
2.Trasferisco il messaggio di Counter a Data, Data è una Stringa temporanea.
3.Quando Counter è uguale a "Cocco" viene cambiata con "Ape"
4.E poi di nuovo a "Cocco"
5.Il ciclo si ripete ogni 10 millisecondi.
6.La variabile Contatore è incrementata di +1 ogni ciclo.
7.Quando raggiunge il valore 50 stampo su seriale il contenuto della varibile Data e quanta memoria ram ho disponibile.
8.Cambio lo stato del pin GPIO 13 ad ogni ciclo (ON,OFF - Toggle)
9.La variabile Contatore si resetta e il ciclo si ripete.

In pratica ogni 10 millisecondi vado a modificare sia la Stringa globale Counter sia la Stringa temporanea Data.

Vedo che la memoria disponibile, grazie alla librerira "MemoryFree.h" diminuisce quando inserisco la parola "Cocco" e diminuisce di 2 byte quando scrivo "Ape"

Non vedo diminuzione della memoria e ho lasciato l'Arduino UNO per un po' di minuti ma il lampeggio del led era sempre costante.

E' da tenere conto che la trasmissione dei dati sulle stringhe viene eseguita ogni 10 millisecondi un tempo abbastanza veloce per creare frammentazione e fare disastri, ma nulla di grave è successo...

Posso pensare che sia un caso? O è normale perché per logica non vado ad a creare mai nuove allocazioni? O comunque stringhe enormi?

Ho trovato altri link utili:

How to use Arduino Strings

Taming Arduino Strings Avoiding Fragementation and Out-of-Memory Issues

Grazie ancora dell'aiuto! :smiley:

Sì sembra un discorso sensato, ma non lo è

La classe String fa parte delle librerie del Cpp
Che è pensato da, per e su dei PC ben più performanti di una UNO

Poi, siccome Arduino si programma in C/Cpp ecco che la ha ereditata

Senza però un sistema operativo che faccia la raccolta della spazzatura

Che infatti si accumula a fare danni

Quindi e' vero che a starci attenti nelle allocazioni si riesce ad evitare i problemi

Io sono capace di starci attento,
Tu sei capace
Alcuni di noi, qui, anche

Ma il primo niubbo che passa lo è?
Te la prendi tu la responsabilità di dirgli che può andare sicuro, se prende le sue protezioni?

Io NO, io gli dico che mi fa il santo piacere di usare le stringhe di C, che impara molto di più

1 Like

Se effettui un'allocazione e deallocazione in modalità LIFO (Last In First Out), non creerai frammentazione. La memoria all'uscita della funzione sarà identica a quella presente al momento della chiamata della funzione. Il rischio di frammentazione si presenta soprattutto se allochi buffer temporanei e buffer non temporanei alternandoli. In questo caso, quando andrai a liberare i buffer temporanei, non potrai più "ricongiungere" i blocchi di memoria perché non saranno più adiacenti a causa delle allocazioni non temporanee.

L'altro problema (più grave secondo me) è che tutti questi metodi sulle String richiedono molta memoria e, quando non c'è più memoria disponibile (almeno su AVR), non causano un crash, ma non fanno ciò che ci si aspetta ➜ questo genera un comportamento imprevedibile del programma, molto difficile da debuggare.

È per questo che i programmatori esperti in soluzioni embedded non amano l'allocazione dinamica: è difficile dimostrare il corretto comportamento atteso del programma.

Hai due modi per passare una String a una funzione: per riferimento o per copia.

void farequalcosaConRiferimento(String & s) // nota la & 
{
   ...
}

void farequalcosaConCopia(String s) 
{
   ...
}

Nel passaggio per riferimento, non viene effettuata alcuna copia del parametro, la funzione lavora sulla String originale. Si guadagna una copia e si può (attenzione) modificare la String originale se lo si desidera.

Nel passaggio per copia, il compilatore alloca una nuova String (allocazione dinamica) e copia al suo interno la String originale. Se la memoria è insufficiente, questa copia non funzionerà e la funzione sarà chiamata con una String vuota, ma il compilatore non ve lo dirà...

Lo STACK entra in gioco solo per memorizzare l'indirizzo dell'oggetto, l'allocazione dinamica avviene sempre nell'heap.

Sì, se hai usato reserve() ma la dimensione non era abbastanza grande e vuoi aggiungere un carattere, la classe String rialloccherà un buffer della nuova dimensione, copierà al suo interno la vecchia String e libererà la memoria della vecchia String. Come spiegato precedentemente, se tra l'allocazione originaria e questa nuova allocazione nell'heap hai creato altri elementi persistenti con memoria nell'heap, allora probabilmente hai frammentato la memoria, i blocchi non sono più contigui.

Se rimani all'interno dello spazio allocato con reserve(), allora effettivamente non ci sarà una nuova allocazione dinamica.

1 Like

@mksamuele97:

Ci sono interessanti alternative, hai già visto QUI ?

... il problema c'è su minuscole MCU (come il ATmega328P) dove non c'è un sistema operativo (OS) ... normalmente esiste un processo degli OS che si occupa in continuazione di ripulire e sistemare la memoria ... se non c'è l'OS, chi lo fa?

Giusto.

Guglielmo

1 Like

Nel tuo codice, la funzione loop() crea una variabile temporanea Data (la cui durata di vita è limitata all'esecuzione della funzione loop()). Questa variabile viene liberata alla fine dell'esecuzione della funzione loop(). La variabile globale Counter, invece, alterna tra "Cocco" e "Ape". Una volta raggiunta la sua dimensione massima (quella di "Cocco"), quando ci scrivi dentro "Ape", non c'è alcuna nuova allocazione dinamica, poiché la memoria è già stata allocata con una dimensione sufficiente per contenere questi valori.

1 Like

Non trovo che SafeString porti davvero il suo nome, perché è "safe" solo se testate correttamente l'esecuzione di ciascuna delle funzioni (cosa che i principianti non fanno mai), e non è come String, poiché non c'è allocazione dinamica. L'unica cosa che offre rispetto a String è dirvi se l'operazione che state cercando di eseguire ha funzionato — a condizione che testiate questa informazione... A mio avviso, quindi, non serve a molto, tanto vale usare le cStrings.

È proprio ciò che fa la libreria SafeString, utilizza cString ovunque. Poiché la libreria non è nel standard, è meglio imparare a gestire le funzioni abituali di C e C++ piuttosto che imparare la sintassi particolare di questa libreria. Avrete almeno una competenza che potrete applicare su qualsiasi ambiente C o C++.

1 Like

Quello SEMPRE e COMUNQUE ... poi ci sono alternative che, se correttamente usate, possono essere utili :wink:

Guglielmo

1 Like

Quindi ne puoi fare tranquillamente a meno, ed usare in sua vece Serial.readBytesUntil(), che fa la stessa cosa usando però un array di char. Per controllare il risultato puoi poi usare le classiche funzioni dello string.h del C standard.

Ciao, Ale.

1 Like

(via Google translate)
Se controlli Taming Arduino Strings Avoiding Fragementation and Out-of-Memory Issues vedrai che è possibile utilizzare Strings su UNO evitando problemi di memoria.

D'altra parte,
La definizione di "sicuro" di SafeString è che il tuo programma non si bloccherà.
vedere The SafeString alternative to Arduino Strings for Beginners Safe, Robust, Debuggable replacement String class for Arduino

Se abiliti la funzione di debug SafeString e provi a scrivere più caratteri su SafeString di quanti ne possa contenere, riceverai un messaggio di errore dettagliato con il nome della variabile che presenta il problema.
per esempio.

Error: msgStr.concat() needs capacity of 8 for the first 3 chars of the input.
        Input arg was '598'
        msgStr cap:5 len:5 'A0 = '

dove msgStr è il nome della variabile del tuo programma

Se insisti nell'usare metodi C-string, il tuo programma probabilmente si bloccherà o si bloccherà come qualche altro punto non correlato.

Le stringhe C non sono consigliate per l'uso nei programmi di nessuna delle grandi società di software per motivi di affidabilità.

1 Like

scrive programmi per la UNO

1 Like

Aggiungo i miei 2 cents sull'argomento...

Fermo restando che imparare ad usare le funzioni di manipolazione delle stringhe disponibili in C è un must e non ci sono scorciatoie che tengono, a mio avviso la "pericolosità" della classe String è decisamente sovrastimata e se ne è scritto fin troppo a tal proposito.

Come ha fatto notare @J-M-L la questione è tra allocazione dinamica ed allocazione statica della memoria: l'allocazione statica è tipicamente più "safe" e veloce, mentre quella dinamica richiede qualche accorgimento in più.

Ma non dimentichiamoci del contesto: se sviluppiamo nel framework Arduino, stiamo sviluppando essenzialmente in C++ e ci sono un gran numero di casi in cui allochiamo dinamicamente la memoria, ma stranamente additiamo solo la classe String.
E la classe File allora? E la Serial? E la Client? Sono tutte classi derivate da Stream, proprio come String e quindi hanno gli stessi vizi e virtù.

Il succo della questione è che se scegliamo di sviluppare con Arduino, dobbiamo imparare a convivere bene con l'allocazione dinamica e con la gestione della memoria. Fare inutili guerre di Pirro contro String è solo una perdita di tempo che distoglie l'attenzione da errori e vizi di programmazione ben più gravi (come ad esempio l'omnipresente delay() in tutorial ed esempi e di conseguenza negli sketch dei beginner).

In conclusione il mio consiglio è quello di concentrarti su altro e quando vuoi usare String fallo serenamente seguendo gli accorgimenti che sono stati giù suggeriti (variabile locale, concatenazione senza creare inutili copie, passaggio per riferimento alle funzioni etc etc).
Se arrivi alle condizioni in cui l'allocazione potrebbe fare danni (ovvero poca SRAM disponibile), allora l'errore principale è stato la scelta della MCU da usare per il tuo progetto.

3 Likes

Le mie 100 lire (5 centesimi al cambio attuale)

Usare gli oggetti String su arduino è
Sbagliato
Autolesionismo
E autobloccante (nel senso che si finisce di diritto nel mio killfile)

Poi ognuno fa la sue scelte

Che sciocchezza...
Le "String" sono utili al programmatore per fare "cose facili" per lui, ma poi la frammentazione di memoria (in assenza di un garbage collector) può uscire e fare danni.
Le "C-string" sono "ostiche" da usare per chi programma da poco (e per questo in genere usano le String, più facili e conosciute su architetture evolute), ma chi ha esperienza sa come usarle e sa evitare "sforamenti" di allocazioni e/o di puntatori (e comunque anche con le "C-string" si può utilizzare una allocazione dinamica, anche questa è "ostica" ma basta avere sufficiente esperienza).
Che poi anche in varie librerie ci siano delle "String" è poco rilevante perché evidentemente vengono gestite in modo oculato (almeno per quelle più note e comuni).

Per cui la mia posizione è evitare di usare le String per quanto possibile (soprattutto sulle UNO), poi per il resto dato che non si costruiscono sistemi di controllo aereo con Arduino se a uno piacciono le String perché "si fa prima", ma poi lo sketch si blocca o fa cose strane, sono problemi suoi... :sunglasses:

1 Like

(via google translate)
Il problema con gli errori C-string è che di solito causano l'arresto anomalo del programma e il riavvio continuo. Questo riavvio, a causa del sovraccarico del buffer, potrebbe verificarsi non nel punto in cui si è verificato l'errore, ma in un punto successivo del programma. Inoltre, quando si verifica un arresto anomalo del sistema/riavvio, l'ultimo output di stampa di debug nel buffer Tx seriale viene perso.

Ciò rende il debug degli errori C-string difficile e dispendioso in termini di tempo.

D'altra parte, SafeString ha queste tre garanzie che semplificano il debug:-

i) Le operazioni SafeString non causeranno l'arresto anomalo del programma. Hai sempre un programma stabile con cui lavorare e puoi vedere tutto l'output di stampa del debug.

ii) Le operazioni SafeString vengono completate interamente o non vengono completate affatto. Le stringhe parziali non vengono aggiunte se non c'è abbastanza spazio.

iii) Se un'operazione fallisce, viene impostato il flag di errore globale della classe SafeString.

Pertanto, durante il test, se l'output del tuo programma non è quello previsto, controlla il flag di errore SafeString.

Se il flag di errore non è impostato, il problema è legato alla logica e non alla dimensione di un SafeString.

Se il flag di errore è impostato, abilita l'output di debug di SafeString che ti mostrerà esattamente quale SafeString ha esaurito lo spazio e quanto spazio aggiuntivo è necessario.

SafeString non elimina la necessità di testare il tuo programma, lo rende solo molto più diretto, semplice e veloce.

1 Like

Ma che è chatGPT ??? :open_mouth:

Guglielmo

1 Like

Bah

Sarà anche

Ma io non ho mai avuto problemi con le stringhe asciiz

Dal mio punto di vista chi ha problemi con le stringhe ha in generale problemi con gli array

E si salva dalle stringhe solo per poi prendersi in fronte in pieno un bell'array di int, tutto farcito di "brutti e grossi numeri"

Che studino quello che devono studiare e facciano quello che devono fare

1 Like

No, ma ci somiglia.

Ciao, Ale.

2 Likes

E un assistente IA ovvero intelligenza artificiale, se le poni bene le domande riesce a crearti persino un codice sensato... tipo calcoli ecc ecc... ma può sbagliare e bisogna avvisarla...

Grazie a tutti dell'aiuto!!!

Ho capito che per come la uso io la classe String non dovrei avere problemi perché non faccio mai concatenazioni e spostamenti di dati tra stringhe...

:smiley: