Skip to content
Select theme

How to append to a String without extra allocations

Short answer: Keep one String and push_str into it. A buffer you keep grows by doubling when it runs out of room, so appending stays cheap however long the text gets, and String::with_capacity sizes it up front so it never reallocates at all. The thing to avoid is format! in a loop: every call builds a brand-new String and copies everything so far into it.

From a fresh project — cargo new string-demo, then replace src/main.rs with each block below. The project name doesn’t matter; nothing in the output depends on it.

Building text a piece at a time with format! reads nicely and hides its cost:

ababababab

Nothing in that output shows what it cost. Count the allocations and it does. This installs a global allocator that counts every request and forwards it to the system one:

format!: 200 bytes, 196 allocations
push_str: 200 bytes, 6 allocations
+: 200 bytes, 6 allocations

All three loops end with the same 200 bytes. format! spent 196 allocations getting there — close to two per append, because each call starts a fresh String and grows it while it writes. push_str spent six.

The push_str loop is the whole fix:

ababababab

Same output as the format! version. The binding needs mut, and the method borrows the String, so it stays yours for as many appends as you like — building a string from parts has the full tour of push_str and push. What’s new here is what the buffer does when it fills up.

Print the capacity next to the length and the growth is on screen:

5 bytes, capacity 8
10 bytes, capacity 16
15 bytes, capacity 16
20 bytes, capacity 32
25 bytes, capacity 32
30 bytes, capacity 32
35 bytes, capacity 64
40 bytes, capacity 64

When an append would run past the capacity, the buffer moves to a new allocation twice the size — 8, then 16, 32, 64. The reallocations get rarer as the text grows, which is why six of them covered all 200 bytes in the counter above.

Know the size, or a good guess at it, and with_capacity skips the growth entirely:

5 bytes, capacity 30
10 bytes, capacity 30
15 bytes, capacity 30
20 bytes, capacity 30
25 bytes, capacity 30
30 bytes, capacity 30

One allocation, and the capacity never moves. Guessed high and the String outlives the building? shrink_to_fit() hands the slack back — a 5-byte String built with with_capacity(64) drops to a capacity of 5.

That third row is the one worth pausing on: with_plus + piece cost the same six allocations as push_str. + on a String builds nothing new — it moves the left operand and pushes the right one into it, growing the same buffer. So text = text + piece in a loop is not the allocation disaster it’s rumoured to be. What it can’t do is take anything but &str on the right, or pre-size the buffer; and a piece you computed, like a number, still pays for its own to_string first.

When the piece is a value rather than a literal, write! appends it straight into the buffer:

Ada, born 1843

write! is format! that appends instead of allocating — same format syntax, writing into a String you already own. The unwrap() never fires; appending to a String can’t fail. The format mini-language itself — named arguments, inline captures, widths — has its own article later in this chapter.