各言語で文字列を結合する方法と、効率について検証しようと思います。
C++
+演算子では2つの string 文字列を連結して新しい string オブジェクトを作成します。
+=演算子や append 関数では既存の string オブジェクトの内部バッファに追記されるため、速い結果になりました。
内部バッファが足りない場合は新たにバッファが作成され、新しいバッファに既存文字列がコピーされます。結合後の長さが分かっているのであれば、あらかじめ reserve 関数で容量を確保しておけば、余分なコストを抑えられます。
VC では ostringstream は string::append より遅い結果となりました。
C言語の場合はバッファの確保などを自前でやる必要があるので面倒です。結合前後の文字列サイズが分かっていれば、memcpy を使って高速に結合できます。
std::string str = "abcdefghij";
// C++
std::string dst;
// + で結合
dst = dst + str;
// += で結合
dst += str;
// append で結合
dst.append(str);
// reserve で容量確保し、append で結合
dst.reserve(str.length() * 10);
dst.append(str);
// ostringstream << で結合
std::ostringstream os;
os << str;
// C
const char* str2 = "abcdefghij";
size_t str_len2 = strlen(str2);
char* dst2 = (char*)malloc(str_len2 * 10 + 1);
if (NULL == dst2) {
printf("NULL == (char*)malloc(str_len2 * 10 + 1);");
return 1;
}
dst2[0] = 0;
// strcat で結合
strcat(dst2, str2);
// strcat_s で結合
strcat_s(dst2, str_len2 * 10 + 1, str2);
// memcpy で結合
memcpy(dst2 + str_len2 * 2, str2, str_len2);
free(dst2);
C#
C# の string はイミュータブルなので、+, +=演算子では2つの文字列を連結して新しい string オブジェクトを作成します。
StringBuilder, StringWriter では既存のオブジェクトの内部バッファに追記されるため、速い結果になりました。
内部バッファが足りない場合は新たにバッファが作成され、新しいバッファに既存文字列がコピーされます。StringBuilder.capacity 関数であらかじめ容量を確保しても、ほぼ速度は変わりませんでした。
var str = "abcdefghij";
// + で結合
var dst = "";
dst = dst + str;
// += で結合
dst += str;
// Concat で結合
dst = string.Concat(dst, str);
// Join で結合
dst = string.Join("", dst, str);
// StringBuilder.Append で結合
var sb = new StringBuilder();
sb.Append(str);
// capacity で容量確保し、StringBuilder.Append で結合
sb = new (str.Length);
// StringWriter.Write で結合
var sw = new StringWriter();
sw.Write(str);
Java
Java の String はイミュータブルなので、+, +=演算子では2つの文字列を連結して新しい String オブジェクトを作成します。
StringBuilder, StringWriter では既存のオブジェクトの内部バッファに追記されるため、速い結果になりました。
内部バッファが足りない場合は新たにバッファが作成され、新しいバッファに既存文字列がコピーされます。結合後の長さが分かっているのであれば、あらかじめ容量を確保しておけば、余分なコストを抑えられます。
10年くらい前(.NET 2.0 のころ)は最適化が働いているのか、+演算子のほうが StringBuilder より速かったのですが、今回は逆転していました。
String str = "abcdefghij";
// + で結合
String dst = "";
dst = dst + str;
// += で結合
dst += str;
// concat で結合
dst = dst.concat(str);
// join で結合
dst = String.join("", dst, str);
// StringBuilder.append で結合
StringBuilder sb = new StringBuilder();
sb.append(str);
// StringBuilder.ensureCapacity で容量確保し、append で結合
sb.ensureCapacity(str.length() * 10);
sb.append(str);
// StringWriter.append で結合
StringWriter sw = new StringWriter();
sw.append(str);
// new StringWriter で容量確保し、append で結合
StringWriter sw2 = new StringWriter(str.length());
sw2.append(str);
JavaScript
JavaScript の文字列はイミュータブルなので、文字列を連結して新しい文字列オブジェクトを作成します。
計測時間が安定しませんでしたが、おそらく同じような傾向だと思います。
var str = "abcdefghij";
// + で結合
var dst = "";
dst = dst + str;
// += で結合
dst += str;
// concat で結合
dst = dst.concat(str);
計測結果
今回の計測結果を示します。プログラムやコンパイラの実装によって結果が変わる可能性があるので注意してください。
例外はありますが、概して以下のことが言えると思います。
・+, += 演算子のようなイミュータブルの結合より、StringBuilder.append のような内部バッファを使う結合のほうが早い。
・最終的な長さが分かるのであれば、あらかじめ容量確保しておいたほうが良い(速度やメモリ使用量の面で)
文字列長10 * 1回 * 100,000ループ = 1,000,000文字連結
VC | gcc | clang | C# | Java | JavaScript | |
+ | 2.314 | 5.494 | 7.159 | 25.874 | 5.010 | 0.00110 |
+= | 0.00230 | 0.000706 | 0.00250 | 30.280 | 5.524 | 0.002 |
concat | 29.396 | 5.043 | 0.00260 | |||
join | 29.767 | 5.146 | ||||
append | 0.00107 | 0.00179 | 0.00400 | 0.00116 | 0.00447 | |
(reserve) | 0.000423 | 0.000715 | 0.00156 | 0.00117 | 0.00256 | |
stream | 0.00599 | 0.001641 | 0.00306 | 0.000846 | 0.00603 | |
(reserve) | 0.00361 | |||||
strcat | 21.507 | 32.72 | 1.148 | |||
strcat_s1 | 34.886 | 33.46 | 1.075 | |||
memcpy | 0.0000868 | 0.000106 | 0.0000674 |
文字列長10 * 10回 * 10,000ループ = 1,000,000文字連結
VC | gcc | clang | C# | Java | JavaScript | |
+ | 1.384 | 0.412 | 0.521 | 2.916 | 0.530 | 0.000700 |
+= | 0.00176 | 0.00163 | 0.00191 | 30.148 | 5.086 | 0.00189 |
concat | 2.924 | 4.976 | 0.00130 | |||
join | 2.980 | 0.488 | ||||
append | 0.000785 | 0.00167 | 0.00156 | 0.000687 | 0.00221 | |
(reserve) | 0.000404 | 0.000740 | 0.000731 | 0.000563 | 0.00168 | |
stream | 0.00927 | 0.00167 | 0.00165 | 0.000628 | 0.00257 | |
(reserve) | 0.00247 | |||||
strcat | 21.876 | 0.0998 | 1.078 | |||
strcat_s1 | 35.258 | 0.101 | 1.228 | |||
memcpy | 0.0000728 | 0.0000596 | 0.0000937 |
検証環境
windows11 Intel Core i7-8750H 6/12 2.2-4.1GHz
gcc/clang
ubuntu VMWare 4core Intel Core i7-8750H 6/12 2.2-4.1GHz
Microsoft Visual Studio Community 2022
Version 17.4.2
VisualStudio.17.Release/17.4.2+33122.133
Microsoft .NET Framework
Version 4.8.09032
インストールされているバージョン:Community
Visual C++ 2022 00482-90000-00000-AA226
Microsoft Visual C++ 2022
ubuntu VMWare 4core Intel Core i7-8750H 6/12 2.2-4.1GHz
$ g++ –version
g++ (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$ clang++-14 –version
Ubuntu clang version 14.0.6-++20220827082222+f28c006a5895-1~exp1~20220827202233.158
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin
Microsoft Visual Studio Community 2022
Version 17.4.2
VisualStudio.17.Release/17.4.2+33122.133
Microsoft .NET Framework
Version 4.8.09032
インストールされているバージョン:Community
C# ツール 4.4.0-6.22565.8+53091686b435746d62a5df56abfab0e71203d83a
IDE で使用する C# コンポーネント。プロジェクトの種類や設定に応じて、異なるバージョンのコンパイラを使用できます。
java –version
java 19.0.1 2022-10-18
Java(TM) SE Runtime Environment (build 19.0.1+10-21)
Java HotSpot(TM) 64-Bit Server VM (build 19.0.1+10-21, mixed mode, sharing)
Chrome
バージョン: 108.0.5359.125(Official Build) (64 ビット)
今回作成したプログラムや詳細な測定結果をこちらで公開しています。
https://github.com/matsushima-terunao/samples/tree/main/test_string_concat
コメント