Why String Concatenation Can Kill Performance in Large Text Generation
Generating large configuration files efficiently is critical for infrastructure-as-code tools like Terraform. When Terraform's query -generate-config-out command produces huge HCL outputs, slowdowns and high memory usage can frustrate users and complicate automation pipelines. This performance bottleneck stemmed from a seemingly innocent pattern: repeated string concatenation.
The Problem with Repeated String Concatenation
In Go, strings are immutable. Each time you concatenate strings with the + operator or +=, a new string is allocated and the contents copied over. This is fine for small or fixed-size strings but becomes problematic when building very large outputs incrementally.
Terraform’s internal/cloud/backend_query.go was accumulating generated HCL text for each resource address by repeatedly concatenating strings. As the output grew, each concatenation copied the entire accumulated string plus the new addition, leading to quadratic time complexity and ballooning memory allocations. This inefficiency was especially visible in large-scale queries, causing significant runtime delays and increased garbage collection pressure.
The Fix: Per-Address *strings.Builder Instances
To address this, the PR introduced a more scalable approach: instead of a single string accumulating all output via concatenation, Terraform now uses a dedicated *strings.Builder for each resource address.
A strings.Builder in Go provides an efficient way to build strings by writing bytes to an internal buffer without repeated reallocations and copying. By having a separate builder per logical unit (each address), the code can append lines of HCL incrementally and linearly.
This approach preserves the original grouping and ordering of HCL blocks, which is important for maintaining the output’s structure, especially when lists are interleaved. After all builders have accumulated their respective blocks, their contents are concatenated once at the end, minimizing copying and memory overhead.
Why This Matters
- Linear Time Complexity: Each piece of text is appended exactly once to a builder buffer, avoiding repeated copying.
- Reduced Memory Allocations: Builders allocate internal buffers with exponential growth, reducing the number of allocations and GC pressure.
- Preserved Output Semantics: The output remains logically grouped per address, maintaining correctness.
Lessons Learned
This change highlights a common pitfall in large-scale text generation: naive string concatenation can cause severe performance degradation. When dealing with large or numerous text fragments, consider using dedicated builders or buffers for each logical unit. This strategy achieves linear time complexity and drastically reduces memory usage.
For Go developers, strings.Builder is the idiomatic tool for efficient string assembly. More generally, buffering outputs per group or segment before a final concatenation can be a powerful pattern in many languages.
If you are building tools that generate large configuration files, logs, or reports, watch out for repeated concatenation patterns. Profiling your code and inspecting memory allocation patterns can reveal these hidden inefficiencies.
This article is based on the merged PR "TF-42218 Replaces repeated generated-HCL string concatenation with per-address builders" in the hashicorp/terraform repository. Read the full breakdown on nossletter.tech.
Originally published on Nossletter.
