12

I've been reading a lot over the years why we should not use the notorious String class and how heap fragmentation is bad practice and not professional and we should never use it in our programs or we'll never enter the C hall of fame and get to be called programmers.

Fine, I get it, C string is pro, String is people toying with Arduino, but having read all that I still don't understand:

  1. Where is the risk? If I use String and my heap gets fragmented, and there's still enough memory for the program to run for months without crashing, why should I not use String if it makes my work so much easier? Just because it's a bad habit is a very lame answer.

  2. Is it possible to quantify the fragmentation and get a feel of what's safe and when it becomes risky? Are there any tools to measure / alert / help us know how close we are to fall off that cliff?

  3. And really, what is potential harm, in practical terms not theoretically, what are the limits, does anyone know when a program will crash?

Happy coding!

7
  • 2
    I totally agree. I use String all the time and in all the ways I know I shouldn't and my gadgets never have any problems. So, in practice, I don't see the harm in using String and I'll keep using them.
    – tavis
    Commented Jul 14, 2021 at 5:52
  • 3
    Interesting to see this discussion, because if you speak to infosec people they will tell you that C string handling is one of the profession's great disasters.
    – pjc50
    Commented Jul 14, 2021 at 10:55
  • 1
    Whether or not String is actively harmful depends on context. The problem is that ignorant (in the technical sense) usage of String can easily lead to difficult-to-diagnose problems: those least likely to understand how to diagnose the issues are the most likely to have the issues. Commented Jul 14, 2021 at 12:38
  • 5
    @pjc50 Indeed, but the issues faced by devs with a few K of memory are different than those faced by the majority of programmers. Commented Jul 14, 2021 at 12:40
  • 2
    You can create just as much mess on the heap allocating your own string memory with malloc() and free() as you can by using a string class. Especially considering that the code of the string class itself was probably written by someone who knows more about software development than you do.
    – alephzero
    Commented Jul 16, 2021 at 5:04

6 Answers 6

26

It's not that String itself is evil, it's more that it's very easy to abuse it and cause heap fragmentation. Used judiciously it's fine and a useful tool. Used in "bad" ways and you open the door to random crashes.

By "bad" ways I mean excessive (and not needed) data duplication and heap reallocation. The biggest pitfalls are:

  • Using .concat() or "adding" strings together with +
  • Passing String objects to functions by value instead of by reference

Basically anything where a new string is created and the old one thrown away leaves holes in the heap.

Those holes (fragmentation) themselves aren't a problem - the problem is the very tiny amount of RAM the typical low-end Arduino (i.e., ATMega328P based) has. When you have wasted space in your heap in the form of holes it's more and more likely that you will end up having the top of your heap meet the bottom of your stack, and that is when problems arise.

For transient operations String is fine. By "transient" I mean operations where all the data is discarded at the end. All temporary and source strings are destroyed and all the memory is returned to the pool, thus leaving no holes in the heap.

Also on microcontrollers with far more memory than a little Arduino UNO (such as ESP32, etc) heap fragmentation ceases to really be a problem. By the time the heap grows big enough to be noticeable any holes have got big enough that they start to get filled again.

2
  • Comments are not for extended discussion; this conversation has been moved to chat.
    – Majenko
    Commented Jul 16, 2021 at 7:04
  • (Outsider here, checking this out from the hot network questions) Returning String objects from functions by value instead of by reference Does the Arduino environment use C++, or C? In C++, copy-elision on return is now guaranteed, so I don't think this should be an issue.
    – Alexander
    Commented Jul 16, 2021 at 22:07
9

The short answer is that fragmentation crashes are infrequent because they require very specific conditions to arise—and that's the problem.

Imagine you have a crash that occurs once every few days and can't figure out why. It never happens while testing and doesn't seem to be caused by anything specific.

Then it turns out that your code is printing numbers to a string, usually 4 digits but sometimes 2 or 3. And it turns out that very specific sequences of the lengths of strings being allocated and deallocated trigger a crash. But there's no good way to analyse or predict the problem because it hinges on absolutely everything that's happened since boot. If there was a 3- instead of 4-digit number 8 hours ago, that completely changes what would need to happen now to trigger a crash.

While answering this question I tried to come up with some code that would leave memory very fragmented. It was very difficult. Memory fragmentation is very sensitive to what happens, in what order. Basically it's a highly chaotic system.

This is reminiscent of problems related to multithreading, where running a simple program multiple times can easily give different results. Problems like this are really, really frustrating to identify and diagnose. A lot of people have felt the pain and that is why there is such stigma about String.

To answer your questions:

  1. why should I not use String if it makes my work so much easier?

As long as you understand the pitfalls and how to avoid them, sometimes Strings are the best tool for the job.

The main goal of avoiding String is to prevent severe heap fragmentation. There are other ways to do that, though:

  • Don't store lots of heap-allocated objects on a long-term basis. If you only have 3 global String variables then it's probably fine to allocate 100 small strings if you delete them before doing anything else
  • If you have to store many heap-allocated objects long-term, try not to allocate them during a period of a lot of heap allocations, deallocations, and resizings (such as during a loop when a string is being built). You want to avoid these long-term objects being given "random" positions through memory.

If you want to use String in an application where reliable long-term operation is important, you really have to be thorough in understanding your memory usage patterns and why they are unlikely to trigger a problem.

I should add that each String adds several bytes of memory overhead, both for the string descriptor and for the invisible bookkeeping that the malloc() implementation does. They are also slower to access than char []s.

  1. Is it possible to quantify the fragmentation and get a feel of what's safe and when it becomes risky?

This is difficult because of how sensitive things are to tiny differences in allocation history. There are a few simple analyses you can do, though.

If you have n heap-allocated objects, you have n+1 fragments (although some fragments may be zero size.) The average fragment size will be ≤ the amount of free space divided by n+1. For example if you have 19 allocated objects and 2000 bytes of remaining space, your average fragment size will be 100 bytes. Mathematically this implies that at least one fragment is ≥ this size, and therefore you are guaranteed to be able to allocate anything smaller than 100 bytes. Keep in mind though that every allocated object consumes a few bytes of invisible bookkeeping data used by the runtime's implementation of malloc(), and there may be other hidden things floating around in the system, like some objects allocated by libraries. Also remember that on Arduino, SRAM is shared by stack and heap.

Now let's say you have 8KB of heap space and you allocate a lot of stuff, then deallocate a lot of stuff, leaving exactly 3 small objects allocated. This will divide your heap into 4 regions. The average size will be just under 2KB and so no matter how unlucky you are, one or more of the regions will be at least that size. So you should be able to allocate one large object of around 2KB, or, allocate hundreds of small objects, or allocate one object and keep resizing it anywhere up to 2KB. If you then free everything except for 3 small objects, you will be back in the same situation: you are guaranteed a region of at least 2KB.

There are two noteworthy conclusions:

  • If the maximum number of items that will ever be allocated at a time is n, and their maximum size is s, and your heap size is at least (2n+1) × (s+4), you will never have a fragmentation problem. (3 is an architecture-dependent fudge factor to cover malloc bookkeeping and padding issues.)
  • If there are "checkpoints" in your code where only a few objects are allocated, this makes it easier to reason about what can happen between these checkpoints.

Are there any tools to measure / alert / help us know how close we are to fall off that cliff?

Not for Arduino that I know of. The problem is, a program susceptible to severe fragmentation may only display signs for short periods once every few days or something.

The easiest technique you can use is to consume say 30% of your memory with malloc() at the start of your program to put more heap pressure on it, and see if it still works. This isn't guaranteed to bring the problems out of the woodwork, but it greatly boosts the chances that you will see something.

You can also write a function to try to allocate the largest objects possible over and over, slowly reducing the size, and see to what extent you are prevented from allocating large objects but can allocate small ones. I was going to post one but I decided it would be of limited benefit.

If you really want to see what is going on with your heap you can look at the source of your malloc() implementation and inspect its data structures from within your program. In this case it seems to be as simple as putting this at the start of your code

struct __freelist {
  size_t sz;
  struct __freelist *nx;
};
extern struct __freelist *__flp;

and then you can traverse the contents of the freelist using __flp.

6

I'd like to answer your question without discussing the more technical side. This has been admirably covered by my learned colleagues in their own answers. I am going to address the cost-benefit side of using String.

Certainly, in the short term, it is almost certainly faster and easier to use String than to allocate buffers of characters, keeping track of how full they are, appending to the end of them, and so on.

For example, to read serial input, this is easy:

String inputBuffer;

void loop()
  {
  while (Serial.available () > 0)
    inputBuffer += Serial.read ();

  if (inputBuffer == "something")
    doSomething ();
  }  // end of loop

This, by the way, is the very sort of code that is likely to cause heap fragmentation and a crash. :)

So, you may well save an hour or two of coding by using the String class. So far, so good. You are ahead.

Then you start testing. Things go well for a while, but then after your code runs for an hour it mysteriously crashes, or probably, hangs. Your device stops doing what it is supposed to do, and you start looking for bugs. There is nothing obviously wrong, so you spend quite a bit of time trying to solve the issue. Heap fragmentation doesn't jump out and slap you in the face like, say, forgetting to add one variable to another.

So you look over the code, maybe post a question on Stack Exchange, maybe ask friends, maybe ask your teacher (where appropriate). Soon enough, time is mounting. You spend an hour debugging, two hours, four hours, and soon you have spent eight hours on it. Now you have wasted all the benefit you got from using the "easy" String class. Not only that, you are way behind, compared to allocating a fixed-size buffer and managing putting data into it yourself.

Sooner or later you identify that heap fragmentation is the culprit, and have to rewrite it. In other words, you eventually have to stop using the String class. So the whole episode has been a waste of time.

Certainly this won't always happen, and for the occasional use of fixed-length String objects, there won't be an issue. But if you look at the conditions mentioned in the other answers, you can see that it is easier, in fact, to "just don't use String" than to use them in very careful ways, and hope that your project doesn't break today, tomorrow, next week, or after three months "in the field".

why should I not use String if it makes my work so much easier?

Certainly you should, if it makes your work easier, and if it doesn't crash.

does anyone know when a program will crash?

No, probably not. If you are "lucky" it will crash soon after you start it, and you will at least know you have a problem. If you are unlucky your fish feeder will stop feeding your fish two weeks into your holiday and they will die.

1
  • As you mentioned, amazing answers given here and your adds to that so thanks everyone for taking the time.
    – Nino
    Commented Jul 15, 2021 at 6:33
3

I'm not an Arduino programmer, but I am a C programmer. And the problem of string types in C -- or, rather, the absence of a first-class string type in C -- is an old but important one.

The fundamental problem is this. In C, you can say things like

int a, b, c, d, f();
a = b + c;
d = f();

And you can say things like

float q, r, s, t();
q = r / s + t();

But you can not say

string x, y, z();
x = y + z();

C has no first-class string type. In pure C, if you want to manipulate strings, you have to use some combination of char [] arrays and char * pointers. You have to remember to allocate enough memory to hold the strings you're working with today. In many cases you can't assign strings; you may have to call strcpy or the like. In most cases you can't directly compare strings; you usually have to call strcmp or the like. This can all be a real nuisance, and if you get the details wrong, you get strange bugs and crashes.

So, especially if you're a beginning or casual programmer, It Would Be Nice if C had a true, first-class string type. (And of course C++ does have such a type.)

But there's a very good reason that plain C doesn't have such a type. Manipulating strings is just plain harder than manipulating ints or floats. The reason it's harder is precisely because they're variable-length, and potentially rather large. Under C's low-level philosophy, the best person to make good tradeoffs when it comes to deciding how to handle a particular program's strings is that program's author, not the compiler or library writer.

You can write a decently good, decently efficient, high-level, first-class string type. But, it's never going to be as efficient, for every problem, as handcrafted char [] and char ** code would be. Also, if there are a bunch of rules for how to use a first-class string type efficiently, avoiding fragmentation and the like, those rules are probably going to end up being almost as complicated as the rules for using char [] and char * correctly -- but the whole point of introducing the first-class string type was so that programmers wouldn't have to worry about those low-level details all the time! It's really rather a pretty pickle.

So when someone says, "you shouldn't use that high-level string type, it's wasteful and inefficient", the question to ask is, is it unacceptably wasteful or inefficient?

Once upon a time, people said we shouldn't use compiled languages, since they're wasteful and inefficient. Those people thought we should all write everything in assembly language. (And there are still a few people who say this.) Most of the time, though, we've discovered that compiled code is adequately efficient, and its other advantages more than make up for any slight lingering inefficiencies.

And the question for C strings versus Strings is almost perfectly equivalent. If your program is making significant use of strings, expending a large fraction of its time or a large fraction of the available memory towards them, the inefficiencies of Strings might well be unacceptable. If there are relatively few strings, however, the convenience to the programmer of using a high-level String type (along with the absence of bugs) might be well worth it, and the costs might be inconsequential.

But there's no one right answer to the question of "Do or don't use Strings", because it's a subjective question, and different people under different circumstances will legitimately come to different conclusions.

1
  • C wouldn't need to add much to allow programmers to implement things that are much closer to be usable as first-class types. If it included static const compound literals, and offered a strong recommendation that such literals should be omitted if their addresses are taken and directly used as the second operand of a ?: operator whose first operand is always zero, or the third operand of one whose first operand is never zero, better string types could be easier to use than C strings.
    – supercat
    Commented Jul 14, 2021 at 17:57
3

Mega point is: don't use String (or anything else that uses the heap) inside interrupts - even with objects created outside the interrupt. the underlying memory allocation functions cannot deal with it and you've got a good chance something will go boom.

As for the rest, so much of this is dependant on implementation

Nick's answer with the input buffer yes will crash, he never clears it! (j/k) But even if he did after matching with something, i.e.

      if (inputBuffer == "something") {
            inputBuffer="";
            doSomething ();
      }

after we've hit this there will be no more (possible) fragmenting to do with this code, as the implementation for String on the copy (or a concatenation) there does not reallocate the memory buffer unless it is too small. So as we go round this whole loop after the first time, no heap allocations will happen.

We could also remove any reallocations during the initial building of the 'something' string in the inputBuffer by allocating enough space right at the beginning of the code with

    inputBuffer.reserve(9);

where 9 is the length of the string 'something' (reserve will add an extra byte on the buffer for the string terminator)

You can also use this to reserve space for doing multiple concatenations . Anything above 1 concat / += will save time because of not multiply reallocating space, plus stop the fragmentation.

    int newSize=a.length()+b.length()+c.length();
    a.reserve(newSize); // only memory realloc happens here
    a.concat(b);
    a.concat(c);

Similarly you could create a much bigger buffer to briefly handle something you're not sure the size of (say, reserve 512 bytes) but beware! because of the point above (.reserve() does not reallocate the buffer unless the requested size is larger) you are stuck with that buffer until the string object is destroyed by going out of scope or being explicitly deleted. Global objects are stuck with it. So if you need a global object that could get bigger or smaller, make it a pointer to a String then use new/delete as required.

Also we need to consider object lifetime. Even if you use a bunch of String objects in a function, if they are all destroyed at the end, and you haven't created any other heap objects within that function, when you have left and all the Strings are auto destroyed there should be no impact on the heap, it should return to its previous state as the memory is reclaimed

If you are creating String objects on the heap (via new) though it does compound any fragmentation problems you may have in your code anyway as you end up with 2 heap items, the String wrapper object and the data itself. If you're just using a local or global variable the wrapper won't be in the heap, either in global memory or on the stack.

2

As you already got good answers to most of your questions, I will try to cover here only this one:

Is it possible to quantify the fragmentation and get a feel of what's safe and when it becomes risky?

A quick Web search for “Arduino free memory” finds a few code snippets that can be used to measure, at run time, the amount of free memory:

  • Available Memory, in the Arduino Playground, only works on AVR. It measures the total amount of available RAM, including the holes in the heap.

  • Measuring Memory Usage, an Adafruit tutorial, shows some portable (AVR and ARM) code that measures the available RAM between the heap and the stack. This does not include the holes in the heap.

If you compare the AVR versions of these two code snippets, you can see the difference is in the freeListSize() function of the Playground version. This function returns the total memory residing in heap holes, which could be a measure of the heap fragmentation.

The function is AVR-specific, but then AVR is where heap fragmentation is most worrisome.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.