String Interning in C# and Memory Optimization

String interning in C# is a mechanism by which the Common Language Runtime (CLR) optimizes memory by storing only a single copy of each distinct string literal in an internal table called the intern pool. When multiple variables or components use identical string values, they are made to reference the same memory address rather than creating separate heap objects. This article explains how the intern pool works under the hood, how to manually control interning, the specific performance and memory benefits it provides, and the trade-offs developers must consider.

What Is String Interning?

In .NET, strings are reference types and are immutable. Without optimization, creating ten instances of the exact same string value would result in ten distinct objects allocated on the managed heap, each consuming additional memory.

String interning resolves this redundancy. The CLR maintains a central lookup table—a hash table containing references to unique string instances known as the intern pool. When a string is interned, the CLR checks if an identical sequence of characters already exists in this pool:

  • If it exists, the CLR returns a reference to the existing string object.
  • If it does not exist, the string is added to the pool, and that new reference is returned.

Automatic Interning of Literals

By default, the C# compiler and CLR automatically intern all compile-time string literals.

string a = "hello";
string b = "hello";

bool sameReference = object.ReferenceEquals(a, b); // Returns True

In this example, both a and b reference the exact same memory location on the heap because the literal "hello" is automatically interned when the assembly is loaded.

Dynamically created strings (such as strings built at runtime via StringBuilder, user input, or I/O operations) are not automatically interned:

string a = "hello";
string b = new string(new char[] { 'h', 'e', 'l', 'l', 'o' });

bool sameReference = object.ReferenceEquals(a, b); // Returns False

Manual Interning: string.Intern and string.IsInterned

The .NET framework provides two static methods in the System.String class to interact with the intern pool explicitly at runtime:

  1. string.Intern(string str): Checks if the specified string is in the pool. If found, it returns the reference to the interned string. If not found, it adds the string to the pool and returns its reference.
  2. string.IsInterned(string str): Checks whether the string is already in the intern pool without adding it. It returns the interned reference if present, or null if it is not.
string dynamicString = new string(new char[] { 'h', 'e', 'l', 'l', 'o' });
string internedString = string.Intern(dynamicString);

bool sameReference = object.ReferenceEquals("hello", internedString); // Returns True

How String Interning Optimizes Memory and Performance

  1. Memory Deduplication: Applications that process vast amounts of repetitive data—such as parsing large JSON/XML files, CSV imports, or database records with repeated categorical values—can significantly reduce their managed heap usage by sharing single string instances instead of allocating millions of duplicate objects.
  2. Faster Equality Comparisons: Comparing two strings using character-by-character evaluation (String.Equals) has a time complexity proportional to string length (\(O(N)\)). When strings are guaranteed to be interned, they can be compared using reference equality (object.ReferenceEquals), reducing the comparison to a single, pointer-level operation (\(O(1)\)).

Trade-offs and Considerations

While string interning can yield large memory savings, it introduces specific costs:

  • Lifetime and Garbage Collection: Interned strings remain allocated for the entire lifetime of the process (or AppDomain). The Garbage Collector (GC) does not reclaim memory used by the intern pool, meaning over-interning unique or transient strings causes permanent memory leaks.
  • CPU Overhead: Calling string.Intern requires a hash-table lookup and synchronization overhead to ensure thread safety. If a string has few duplicates, the CPU cost of the lookup outweighs the memory savings.
  • Modern Alternatives: In modern .NET versions, alternative approaches such as custom string pools, ReadOnlySpan<char>, or third-party deduplication strategies are often preferred over string.Intern because they allow unused strings to be collected by the GC when no longer needed.