I believe I’ve read somewhere that string interpolation is to be preferred over concatenation at all times and is generally more performant.
Is this still true when you’re only trying to merge two very small strings? Something like this for example where I am trying to merge two string variables
str1 = "something"
str2 = "something else"
str1 + str2
A quick benchmark shows that concatenation is slightly faster.
I know that the difference is negligible and I’m not seriously trying to perform such a micro-optimization here.
It just got me thinking about the advice to always use interpolation over concatenation after I encountered a short concatenation statement in some old code.
try str1+" "+str2
. For me it’s 1.5 times slower than interpolation. This is because interpolation can calculate size and allocate memory once, but +
can’t do it so will likely allocate twice and copy (or allocate once but more than needed).
1 Like
What exactly are you benchmarking and how?
In general, interpolation should unlikely be less efficient than concatenation. And in the odd case that it is, it’s probably not very relevant.
So I’d definitely recommend interpolation always.
Here’s the benchmark I did
require "benchmark"
Benchmark.ips do | bm |
bm.report("interpolation") {
other_str = "some other string"
"some string #{other_str}"
}
bm.report("concatenation") {
other_str = "some other string"
"some string " + other_str
}
end
> crystal run -p test.cr --release
interpolation 69.25M ( 14.44ns) (± 0.80%) 48.0B/op 1.67× slower
concatenation 115.58M ( 8.65ns) (± 1.22%) 48.0B/op fastest
The result is the same when you combine two separate strings into a third though:
require "benchmark"
Benchmark.ips do | bm |
bm.report("interpolation") {
first_str = "some string "
second_string = "some other string"
"#{first_str}#{second_string}"
}
bm.report("concatenation") {
first_str = "some string "
second_string = "some other string"
# I'm assuming an empty string here is needed to be equivalent to the interpolation test
first_str + second_string + ""
}
end
> crystal run -p test.cr --release
interpolation 72.12M ( 13.87ns) (± 1.01%) 48.0B/op fastest
concatenation 71.80M ( 13.93ns) (± 0.31%) 48.0B/op 1.00× slower
Apparently concatenation could still be 1.06x
faster depending on the position of the ""
? But I think the difference there is close enough to be dismissed.
I was adding " " (one space) not empty string.
+
between two strings basically does same thing as interpolation - get sizes of two strings, allocate sum of them, copy buffers.
But when you use first_str + second_string + " "
(in any order, but empty strings are perhaps removed by optimizer so all three must be nonempty) interpolation become much faster - it can count all three sizes at once but +
can only work step by step (concatenate first two strings, then add third one).
So if you just want concatenate two strings - you can do it with +
. But once you need to concatenate three or more - interpolation is much faster and memory efficient.
My benchmark:
require "benchmark"
str1 = "something"
str2 = "something else"
Benchmark.ips do |bench|
bench.report("str1+str2") { str1 + str2 }
bench.report("interpolate") { "#{str1}#{str2}" }
bench.report("str1+' '+str2") { str1 +" "+ str2 }
bench.report("interpolate2") { "#{str1} #{str2}" }
bench.report("str1+str2+' '") { str1 + str2 + " " }
bench.report("str1+''+str2") { str1 +""+ str2 }
bench.report("str1+str2+''") { str1 + str2 + "" }
end
results:
str1+str2 6.22M (160.83ns) (± 4.41%) 48.0B/op 1.03× slower
interpolate 6.35M (157.38ns) (± 2.65%) 48.0B/op 1.01× slower
str1+' '+str2 3.84M (260.61ns) (± 1.61%) 80.0B/op 1.67× slower
interpolate2 6.26M (159.71ns) (± 1.74%) 48.0B/op 1.03× slower
str1+str2+' ' 3.27M (306.05ns) (± 1.69%) 96.0B/op 1.97× slower
str1+''+str2 6.40M (156.34ns) (± 1.93%) 48.0B/op 1.00× slower
str1+str2+'' 6.43M (155.59ns) (± 2.07%) 48.0B/op fastest
2 Likes
Huh. It looks like using pre-allocated strings outside of the benchmark blocks will change the results quite a bit.
If you allocate a new pair of str1
and str2
for each run
str1+str2 79.52M ( 12.58ns) (± 0.57%) 48.0B/op fastest
interpolate 68.43M ( 14.61ns) (± 1.60%) 48.0B/op 1.16× slower
str1+' '+str2 40.91M ( 24.44ns) (± 1.12%) 80.0B/op 1.94× slower
interpolate2 55.11M ( 18.15ns) (± 0.56%) 48.0B/op 1.44× slower
str1+str2+' ' 38.45M ( 26.01ns) (± 0.96%) 96.0B/op 2.07× slower
str1+''+str2 70.04M ( 14.28ns) (± 0.41%) 48.0B/op 1.14× slower
str1+str2+'' 72.72M ( 13.75ns) (± 3.40%) 48.0B/op 1.09× slower
str1+str2
becomes faster in comparison to everyone else
At least in the case of my benchmark above where you allocate a new str1
and str2
each run the empty string does not seem to get removed?
require "benchmark"
Benchmark.ips do | bm |
bm.report("interpolation") {
str1 = "some string "
str2 = "some other string"
"#{str1}#{str2}"
}
bm.report("str1+str2") {
str1 = "some string "
str2 = "some other string"
str1 + str2
}
bm.report("str1 + '' + str2") {
str1 = "some string "
str2 = "some other string"
str1 + "" + str2
}
bm.report("str1 + str2 + ''") {
str1 = "some string "
str2 = "some other string"
str1 + str2 + ""
}
end
interpolation 72.03M ( 13.88ns) (± 0.62%) 48.0B/op 1.12× slower
str1+str2 80.73M ( 12.39ns) (± 0.90%) 48.0B/op fastest
str1 + '' + str2 75.32M ( 13.28ns) (± 1.71%) 48.0B/op 1.07× slower
str1 + str2 + '' 72.77M ( 13.74ns) (± 2.92%) 48.0B/op 1.11× slower
But if you only run the first two blocks str1+str2
becomes 1.65x faster than interpolation
, taking only ~ 8 nanoseconds to complete for some reason.
These benchmark results doesn’t really matter that much. I just find it interesting how much they vary.
Some things like this can be hard to benchmark because LLVM can just optimize things away to where it’s no different than benchmarking a hard-coded string.
1 Like