Unusual Ways of Boosting Up App Performance. Strings
This is the second post in the series. The other ones can be found here:
- Unusual ways of boosting up app performance. Boxing and Collections
- Unusual Ways of Boosting Up App Performance. Lambdas and LINQs
This post will focus on best approaches of working with strings.
Changing String Contents
String is an immutable type, meaning that the contents of a string object cannot be changed. When you change string contents, a new string object is created. This fact is the main source of performance issues caused by strings. The more you change string contents, the more memory is allocated. This, in turn, triggers garbage collections that impact app performance. A relatively simple solution is to optimize your code so as to minimize the creation of new string objects.
How to Find
Check all string instances that are not created by your code, but by the methods of the
String class. The most obvious example is the
String.Concat method that creates a new string each time you combine strings with the + operator.
To do this in dotMemory:
In the Memory Traffic view, locate and select the
Find all methods of the
Stringclass that create the selected strings.
Consider an example of the function that reverses strings:
An app that uses this function to revert a 1000-character line generates enormous memory traffic (more than 5 MB of allocated and collected memory). A memory snapshot taken with dotMemory reveals that most of the traffic (4 MB of allocations) comes from the
String.Concat method, which, in turn, is called by the
The Heap Allocations Viewer plug-in will also warn you about allocations by highlighting the corresponding line of code:
How to Fix
In most cases, the fix is to use the
StringBuilder class or handle a string as an array of chars using specific array methods. Considering the ‘reverse string’ example, the code could be as follows:
dotMemory shows that traffic dropped by over 99% after the fix:
When seeking ways to optimize your project, take a look at the logging subsystem. In complex applications, for the sake of stability and support convenience, almost all actions are logged. This results in significant memory traffic from the logging subsystem. That’s why it is important to minimize allocations when writing messages to log. There are multiple ways to improve logging.*
*Actually, the optimization approaches shown in this section are universal. The logging subsystem was taken as an example because it works with strings most intensively.
Empty Arrays Allocation
LogMessage method looks as follows:
What are the pitfalls of such implementation? The main concern here is how you call this method. For example, the call
will cause allocation of an empty array. In other words, this line will be equivalent to
How to Find
These allocations would be difficult to detect in the memory snapshot manually, but you can use the Heap Allocations Viewer plug-in to find it very quickly:
How to Fix
The best solution is to create a number of method overloads with explicitly specified arguments. For instance:
The implementation above has a small drawback. What if you pass a value type to, say, the following method?
As the method accepts only the
object argument, which is a reference type, boxing will take place.
How to Find
As with any other boxing, the main clue is a value type on the heap. So, all you need to do is look at the memory traffic and find a value type. In our case this will look as follows:
Of course, the Heap Allocations Viewer will also warn you:
How to Fix
The easiest way is to use generics—a mechanism for deferring type specification until it is declared by client code. Thus, the revised version of the
LogMessage method should look as follows:
Early String Allocation
The advice to defer variable allocation as much as possible is quite obvious. Still, sometimes stating the obvious is useful.
Consider the code below. Here the
logmsg string is created regardless of whether logging is turned on or off:
A better solution would be:
If you use logging for debugging purposes, make sure log calls never reach the release build. You can do this by using the
In the example below, the
LogMessage method will be called only if the DEBUG attribute is explicitly defined.