Language: | C++ |
Tools Used: | Visual Studio |
Duration: | ~1 week |
Completion: | 2024 |
This is a C++ string class I built from scratch,
designed to minimize heap allocations and optimize memory usage.
It operates at a low level to manage memory efficiently,
with features like
Reserve
,
format
,
Append
, a powerful
Format
method that uses templates and variadic arguments to handle a wide
range of input types, and more!
It supports transformations between
char
,
std::string
, and numeric types like
int
,
long
,
double
, or
float
(all via variadic templates),
along with other useful methods.
One of the highlights is its performance. It's around 20 times
faster than Unreal Engine's
FString
and about 10 times
faster than
std::string
for string interpolation. This is because the string data and all temporary string objects needed for building
strings are constructed in place (reusing the existing buffer), which avoids allocations,
garbage generation and potential GC spikes altogether while improving speed.
This project gave me the opportunity to brush up on my C++ skills (which I had my first contact when I was 15) and enjoy the advanced memory management that C++ offers, something that higher-level languages like C#, Java, or Python often abstract away. It's a tool I'm proud of, and I used it in some of my other projects.
Technical Details and Benchmark
Here's a quick overview of some of the features it offers (summarized header file):
|
|
The following shows how I handled the
Format
method, perhaps not the most efficient and/or robust way, but good and fast enough
for my use cases:
|
|
I've tested the class against many string implementations, including .NET's
System.String
and the Standard Library's
std::string
with libstdc++ versions 11, 17, and 20, Unity's and UE's strings, etc.
The result is similar in all cases.
Even when employing Small String Optimization (SSO),
every string solution allocates temporary objects on the heap when interpolating/concatenating string objects,
regardless of the string size.
I also tested in build/production configurations, with compiler optimizations enabled,
but these temporary allocations exist, leading to performance overhead compared
to the custom string class.
For the benchmark, I'll
show its performance relative Unreal Engine's
FString
. While
FString
is more lightweight than
FText
(because it doesn't have the overhead of localization and is meant for fast string operations) and
is complete and full of features, it follows common practices that I've seen in
many standard string libraries: they produce unnecessary memory allocations and
performance overhead when manipulating and concatenating strings.
The custom string class was built to address these inefficiencies, focusing
on minimizing allocations while maintaining a flexible and efficient API. The idea is to
have a buffer that can grow on demand, but the central principle is that the buffer is
always reused, thereby avoiding heap allocations (mutable string). And again, another key
aspect is that temporary string objects (such as the ones resulting from methods like
FString::FromInt()
) are constructed in place, resulting in
no allocations.
The benchmark showcases the differences in speed and memory usage between
FString
(UE's) and
FFastString
(the custom string class),
highlighting
how reduced allocations lead to better performance and lower heap fragmentation.
Unreal's
FString::Printf
and similar methods create a temporary object each time they're
called,
whereas
FFastString
uses a variadic template-based
Format
function that construct in place,
formatting without any allocations. Similarly, Append operations in
FString
create transient objects when converting numbers to strings, while
FFastString
directly appends numeric
values (and other types) to the buffer. This seemingly small difference adds up over thousands of
operations.
For the benchmark, both strings were preallocated with
Reserve(64)
to ensure no
extra allocations inside the loop. The first test focused on formatted string
creation, generating a message containing a player name, ID, and score. The second
test measured repeated appends of numbers, something commonly seen in logs and UI
updates:
|
|
PS: By default, floating-point numbers are displayed with sufficient precision to represent their value, up to a maximum of 20 decimal digits (hard cap). The way it works is by either reaching the cap, or stopping after it encounters two zeros in a row. For instance, 2.34001 will output: '2.34'. You can also specify a desired precision to override this rule and display a specific number of decimal places.
The results are:
Here's the Timing comparison, using Unreal Insights:
The Format method, around 2-5x times faster, depending on the iterations (the more, the larger the difference, 1.5-2x with a single call):
The Append method, around 20x times faster due to the lack of allocations (no temporary string objects allocated):
On the memory side, using UE's Memory Insights,
FString
required 10,001 allocations (1 MiB) for formatting
and 20,000 allocations (271.5 KiB) for appending (2 temporary string objects from numeric
types per iteration), whereas
FFastString
completed both tests with zero allocations.
As you can see, the 'MemTag's for the
FFastString
(
Format
and
Append
) don't show up since no allocation was registered. In fact, if we include the initial
allocation due to the
Reserve(64)
call, we see that
FFastString
shows a single allocation in the whole benchmark:
A speed difference of 20x means that you could do another 190,000 string operations at the same cost as Unreal's 10,000 operations. But this is not just about raw speed: allocating unnecessarily can cause bigger issues down the line. Small, frequent allocations can lead to heap fragmentation, where memory becomes increasingly inefficient over time. They also introduce cache misses due to non-contiguous memory usage, and worst of all, they contribute to garbage build-up, causing unpredictable GC spikes that can introduce frame hitches in real-time applications.
The custom string class was built to solve these problems in a way that remains simple
in concept.
Now, of course, it doesn't provide every utility of FString, it just covers for now
the core functionality efficiently:
Format
,
ToString
,
Assign
,
Append
,
TempString
, operator overloading, and full support for all numeric types (
long long
,
long
,
int
,
float
,
double
, etc.). It also provides
conversion with
const char*
,
std::string
and
FString
, and additional support could easily be added
for other string types.
For personal projects, this lightweight design was good enough. Expanding it further is always an option, but the main takeaway here is that even something as simple as optimizing string operations can bring improvements in performance and memory efficiency. A bit of mindful memory management goes a long way.