An honest benchmark approach instead of folk wisdom about PHP string operations
Few PHP topics get discussed with as much confident half knowledge as whether concatenation, interpolation, heredoc, or sprintf is fastest. The honest answer is that for the vast majority of real applications the difference is irrelevant, and anyone who still wants to speak with authority needs a clean benchmark setup rather than a quick microtime comparison inside a loop. This article shows how to measure this properly, what OPcache actually changes, and when readability should completely dominate the performance question.
Table of Contents
- 1. Four ways to build a string
- 2. How PHP parses interpolation internally
- 3. Benchmark approach: how to measure string operations correctly
- 4. Actual numbers and how to interpret them
- 5. Heredoc and Nowdoc: quirks and when they win on readability
- 6. sprintf and vsprintf: separating formatting cleanly
- 7. OPcache's impact on string operations and interned strings
- 8. Practical recommendation by context
- 9. Common misconceptions and the microoptimization trap
- 10. Summary
- 11. FAQ
1. Four ways to build a string
PHP offers at least four different syntax forms for the same purpose of embedding dynamic values into text, differing in readability, resistance to mistakes, and supposed performance. Concatenation with the dot operator explicitly builds the string from individual fragments, interpolation embeds variables directly inside double quotes, heredoc works like interpolation but allows multi-line text without escaping quotes, and sprintf or vsprintf fully separate the format string from the values.
Each of these forms has a legitimate primary use case: concatenation suits a few short fragments, interpolation suits single-line text with a handful of embedded variables, heredoc suits multi-line text blocks such as email templates or SQL fragments, and sprintf suits cases requiring formatting such as decimal places, zero padding, or locale-dependent number formats. The performance question has historically been so persistent because early PHP versions did show measurable differences between these forms, something that has largely faded with modern PHP versions and OPcache.
2. How PHP parses interpolation internally
Interpolated strings in double quotes are not treated as plain text, they already go through a dedicated lexer state during parsing that distinguishes literal text from embedded variables. For the simple form $variable, the lexer recognizes a dollar sign followed by a valid identifier and replaces that section with the corresponding value, while more complex expressions such as array access or object properties require the curly brace syntax {$obj->property} so the parser can unambiguously delimit the expression.
Internally, PHP compiles an interpolated string at compile time into a sequence of opcodes that essentially correspond to the same concatenation operations an explicit dot operator would produce. The supposed performance difference between interpolation and concatenation practically no longer exists at the opcode level in current PHP versions, since the compiler maps both forms onto the same internal mechanism, only the compile-time parsing effort differs slightly, which is irrelevant to runtime performance.
3. Benchmark approach: how to measure string operations correctly
A naive benchmark that simply loops a million times and calls microtime before and after usually measures more noise than real difference, since a single run gets distorted by garbage collection, CPU frequency scaling, or background processes. A valid measurement instead uses multiple repetitions of the same benchmark, a separate warmup run that gets discarded, and hrtime instead of microtime, since hrtime provides a monotonic clock with nanosecond resolution that is unaffected by system time adjustments.
It is equally important to keep the actual test scenario realistic: a benchmark that assembles the exact same string a thousand times often only measures how well OPcache optimizes a constant expression sequence, not real behavior with actually varying runtime values. For robust results, the embedded values should therefore vary on every iteration, for example using a random value or the loop counter itself.
<?php
declare(strict_types=1);
/**
* Runs a benchmark multiple times and returns the median of the
* measured durations in nanoseconds, cushioning outliers caused by
* GC pauses or system noise.
*
* @param callable $benchmark The code block being measured
* @param int $iterationsPerRun Number of iterations per measurement run
* @param int $runs Number of independent measurement runs
* @return float The median duration in nanoseconds
*/
function benchmarkMedian(callable $benchmark, int $iterationsPerRun, int $runs): float
{
$durations = [];
for ($run = 0; $run < $runs; $run++) {
$start = hrtime(true);
for ($i = 0; $i < $iterationsPerRun; $i++) {
$benchmark($i);
}
$durations[] = hrtime(true) - $start;
}
sort($durations);
return $durations[(int) floor(count($durations) / 2)];
}
// Warmup run that is deliberately discarded.
benchmarkMedian(fn (int $i) => "Value: {$i}", 10_000, 1);
$result = benchmarkMedian(fn (int $i) => "Value: {$i}", 100_000, 7);
4. Actual numbers and how to interpret them
Running such a clean benchmark on a current PHP 8.4 installation with OPcache enabled, concatenation, interpolation, and heredoc for simple cases with a handful of embedded variables typically sit within a few percentage points of each other, a difference that completely disappears in any real request alongside database access and network latency. sprintf, on the other hand, shows a consistently measurable but still small overhead compared to the other three variants, because it has to parse the format string at runtime instead of already being resolved into opcodes at compile time.
That overhead of sprintf grows noticeably with the number and complexity of format specifiers, for example decimal rounding, padding, or multiple positional arguments, but even then it stays well below what a single additional database query or an additional HTTP request would cost in the overwhelming majority of applications. The correct conclusion from such numbers is almost always that the choice of string method remains a readability and maintainability question, not a performance question, except in narrowly scoped hot paths with millions of calls per second.
5. Heredoc and Nowdoc: quirks and when they win on readability
Since PHP 7.3, heredoc supports a so-called flexible syntax where the closing marker may be indented, and PHP automatically strips that indentation from every line of content, finally making heredoc blocks integrate cleanly into indented code without wrecking the readability of the surrounding method. Before that change, the closing marker had to sit strictly in column one, which made heredoc unwieldy and visually inconsistent in deeply nested code.
Nowdoc, recognizable by single quotes around the opening marker, behaves like heredoc but performs no interpolation whatsoever, making it ideal for multi-line text blocks that themselves contain dollar signs, such as shell scripts, regular expressions, or templates for other templating languages, where accidental interpolation would cause hard-to-find bugs. Heredoc clearly wins on readability over concatenation whenever more than about three or four lines or multiple embedded values are involved, because the reading flow is no longer interrupted by dot operators and quotes.
<?php
declare(strict_types=1);
function buildInvoiceMail(string $customerName, float $amount, string $orderId): string
{
return <<<MAIL
Hello {$customerName},
thank you for your order {$orderId}.
The invoice amount is {$amount} EUR.
Best regards
MAIL;
}
6. sprintf and vsprintf: separating formatting cleanly
The real value of sprintf lies not in the concatenation itself, but in the clean separation of format string and values, combined with built-in formatting rules that would otherwise have to be rebuilt manually with plain concatenation or interpolation. An expression like sprintf('%05.2f', $value) handles, in a single declarative call, both padding with leading zeros and rounding to two decimal places, something that would take several lines with number_format and concatenation.
The extra value shows up particularly with localization and positional arguments of the form %1$s, which let the same variable be used multiple times or in a different order across different translations of the format string, without having to reorder the actual value list. For exactly this use case, multilingual text fragments with variable word order, sprintf is practically without alternative compared to interpolation, even if it is minimally slower.
7. OPcache's impact on string operations and interned strings
OPcache affects string performance on two independent levels. First, purely constant strings, meaning ones with no runtime variable at all, are already folded into a single literal at compile time, so a purely static heredoc block with no embedded variables incurs no assembly cost whatsoever at runtime, regardless of how it was written in the source code. Second, PHP's Zend Engine uses so-called interned strings, an internal pooling system that keeps identical string literals in memory only once across the entire process and hands out references to it instead.
For strings assembled dynamically at runtime, as happens with interpolation, concatenation, or sprintf with actually varying values, this interning mechanism naturally does not apply, since every newly produced string is potentially unique and has to be allocated separately. The practical effect is that OPcache mainly helps with repeatedly identical, static text fragments, such as SQL skeletons or HTML template parts, while the actual question of concatenation versus heredoc versus sprintf for genuinely dynamic content remains largely unaffected by it.
8. Practical recommendation by context
For short, single-line log messages or error messages with one or two embedded values, interpolation is usually the most readable and therefore correct choice, because it creates the least visual overhead. For multi-line text blocks such as email templates, SQL statements, or JSON fixtures in tests, heredoc is almost always the better choice over chained concatenation lines, because the reading flow matches the actual target text instead of being torn apart by PHP syntax.
For cases with genuine formatting needs, such as monetary amounts, percentages, or positional arguments for translations, sprintf remains the right choice despite its small overhead, because the alternative of manually rebuilding formatting logic with string functions is both more error-prone and harder to read. Plain concatenation with the dot operator fits best for incrementally building a string across several conditional code paths, for example when assembling a dynamic SQL query fragment inside a loop.
9. Common misconceptions and the microoptimization trap
A widespread misconception is the belief that concatenation with the dot operator is inherently faster than interpolation, a belief that stems from very old PHP versions and no longer holds up empirically on current versions with a working OPcache. Similarly common is the assumption that sprintf is too slow for practically every use case, even though its actual overhead only reaches a relevant order of magnitude at very many millions of calls per second.
The real microoptimization trap lies in spending time on the choice of string method while genuine performance problems almost always sit somewhere else, such as N+1 database queries, missing indexes, or unnecessary network round trips. A realistic approach measures first with a profiler such as Xdebug or Blackfire to find where time is actually being lost, and only turns to string assembly if a profiler actually flags it as a relevant share of total runtime, which in practice happens extremely rarely.
| Method | Readability | Relative performance | Recommended use |
|---|---|---|---|
| Concatenation (.) | Medium with few fragments | Baseline | Incremental building inside loops |
| Interpolation ("...") | High for single-line text | Nearly identical to concatenation | Short log and error messages |
| Heredoc | High for multi-line text | Nearly identical to concatenation | Email templates, SQL blocks |
| Nowdoc | High, no interpolation | Nearly identical to concatenation | Shell scripts, regex templates |
| sprintf/vsprintf | High when formatting is needed | Slightly higher overhead | Monetary values, positional args |
Mironsoft
PHP modernization, code quality, and legacy refactoring
Grown PHP code nobody wants to touch anymore?
We modernize PHP codebases to current language standards, introduce static analysis and coding standards, and refactor legacy code step by step without endangering live operations.
Legacy Refactoring
Modernize grown PHP code in a structured, low-risk way.
Establishing Code Quality
Anchor PHPStan, coding standards, and CI checks sustainably in the team.
Version Upgrades
Plan and execute PHP major version upgrades safely, without downtime.
10. Summary
PHP String Performance: The Essentials at a Glance
No clear performance winner
Concatenation, interpolation, and heredoc differ under OPcache by only a few, practically irrelevant percentage points.
sprintf costs a bit more
The runtime parser for the format string causes measurable but usually meaningless overhead in everyday use.
Readability decides
The choice of string method should almost always be made based on readability, not performance.
Measure cleanly, don't guess
A valid benchmark needs warmup, multiple runs, hrtime, and varying rather than constant values.