Sometimes its best to consult the documentation: Array.concat, and String.concat.
Simply, Array.concat()
is used to create a new array equivalent to the flat merging of all passed in objects (arrays or otherwise). String.concat()
is used to create a new string, which is equivalent to the merging of all passed in strings.
However, as MDN hints at, String.concat()
should not be used as the assignment +, +=
operators are much faster. Why then would you use String.concat()
? You wouldn't. Why have it then? It's part of the spec: See Page 111 - 112 (Section: 15.5.4.6).
So on to the question of Why is String.Concat
so slow?. I did some digging through Chrome's V8 Engine. To start with, behind the scenes, this is what a call to String.prototype.concat
is doing:
// ECMA-262, section 15.5.4.6
// https://github.com/v8/v8/blob/master/src/string.js#L64
function StringConcat(other /* and more */) { // length == 1
CHECK_OBJECT_COERCIBLE(this, "String.prototype.concat");
var len = %_ArgumentsLength();
var this_as_string = TO_STRING_INLINE(this);
if (len === 1) {
return this_as_string + other;
}
var parts = new InternalArray(len + 1);
parts[0] = this_as_string;
for (var i = 0; i < len; i++) {
var part = %_Arguments(i);
parts[i + 1] = TO_STRING_INLINE(part);
}
return %StringBuilderConcat(parts, len + 1, "");
}
As you can see all of the real work happens in StringBuilderConcat
, which then calls a StringBuilderConcatHelper
which then finally calls String::WriteToFlat
to build a string. These are each extremely long functions and I've cut most of it out for brevity. But if you'd like to look for your self have a look in github:
StringBuilderConcat
// https://github.com/v8/v8/blob/master/src/runtime.cc#L7163
RUNTIME_FUNCTION(Runtime_StringBuilderConcat) {
// ...
StringBuilderConcatHelper(*special,
answer->GetChars(),
FixedArray::cast(array->elements()),
array_length);
// ...
}
StringBuilderConcatHelper
template <typename sinkchar>
static inline void StringBuilderConcatHelper(String* special,
sinkchar* sink,
FixedArray* fixed_array,
int array_length) {
// ...
String::WriteToFlat(string, sink + position, 0, element_length);
// ...
}
String::WriteToFlat
// https://github.com/v8/v8/blob/master/src/objects.cc#L8373
template <typename sinkchar>
void String::WriteToFlat(String* src,
sinkchar* sink,
int f,
int t) {
String* source = src;
int from = f;
int to = t;
while (true) {
// ...
// Do a whole bunch of work to flatten the string
// ...
}
}
}
Now what's different about the assignment pathway? Lets start with the JavaScript addition function:
// ECMA-262, section 11.6.1, page 50.
// https://github.com/v8/v8/blob/master/src/runtime.js#L146
function ADD(x) {
// Fast case: Check for number operands and do the addition.
if (IS_NUMBER(this) && IS_NUMBER(x)) return %NumberAdd(this, x);
if (IS_STRING(this) && IS_STRING(x)) return %_StringAdd(this, x);
// Default implementation.
var a = %ToPrimitive(this, NO_HINT);
var b = %ToPrimitive(x, NO_HINT);
if (IS_STRING(a)) {
return %_StringAdd(a, %ToString(b));
} else if (IS_STRING(b)) {
return %_StringAdd(%NonStringToString(a), b);
} else {
return %NumberAdd(%ToNumber(a), %ToNumber(b));
}
}
First thing to note, there's no loops and its quite a bit shorter compared to StringConcat
up above. But most of the work we're interested in happens in the %_StringAdd
function:
// https://github.com/v8/v8/blob/master/src/runtime.cc#L7056
RUNTIME_FUNCTION(Runtime_StringAdd) {
HandleScope scope(isolate);
DCHECK(args.length() == 2);
CONVERT_ARG_HANDLE_CHECKED(String, str1, 0);
CONVERT_ARG_HANDLE_CHECKED(String, str2, 1);
isolate->counters()->string_add_runtime()->Increment();
Handle<String> result;
ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
isolate, result, isolate->factory()->NewConsString(str1, str2));
return *result;
}
This is pretty simple actually, some counters and a call to something called NewConsString
with the left and right operands. NewConsString
is also pretty simple:
// https://github.com/v8/v8/blob/master/src/ast-value-factory.cc#L260
const AstConsString* AstValueFactory::NewConsString(
const AstString* left, const AstString* right) {
// This Vector will be valid as long as the Collector is alive (meaning that
// the AstRawString will not be moved).
AstConsString* new_string = new (zone_) AstConsString(left, right);
strings_.Add(new_string);
if (isolate_) {
new_string->Internalize(isolate_);
}
return new_string;
}
So this just returns a new AstConsString
, what's that:
// https://github.com/v8/v8/blob/master/src/ast-value-factory.h#L117
class AstConsString : public AstString {
public:
AstConsString(const AstString* left, const AstString* right)
: left_(left),
right_(right) {}
virtual int length() const OVERRIDE {
return left_->length() + right_->length();
}
virtual void Internalize(Isolate* isolate) OVERRIDE;
private:
friend class AstValueFactory;
const AstString* left_;
const AstString* right_;
};
Well this doesn't look like a string at all. Its actually an 'Abstract Syntax Tree', this structure forms a 'Rope' which is efficient for modifying strings. It turns out most of the other browsers now use this type or rope structure when doing string addition.
The take away from this, is that the addition pathway uses a more efficient data structure, where as StringConcat
does significantly more work with a different data structure.